pax_global_header00006660000000000000000000000064150133720310014505gustar00rootroot0000000000000052 comment=fbe45bcbe5efe4a919600d76ab9e6c43305b0d80 fwupd-2.0.10/000077500000000000000000000000001501337203100127125ustar00rootroot00000000000000fwupd-2.0.10/.clang-format000066400000000000000000000026141501337203100152700ustar00rootroot00000000000000--- AlignAfterOpenBracket: 'Align' AlignConsecutiveAssignments: 'false' AlignConsecutiveDeclarations: 'false' AlignConsecutiveMacros: 'true' AlignOperands: 'true' AlignTrailingComments: 'true' AllowAllArgumentsOnNextLine: 'false' AllowAllParametersOfDeclarationOnNextLine: 'false' AllowShortBlocksOnASingleLine: 'false' AllowShortCaseLabelsOnASingleLine: 'false' AllowShortFunctionsOnASingleLine: 'Inline' AllowShortIfStatementsOnASingleLine: 'false' AlwaysBreakAfterReturnType: 'All' BinPackParameters: 'false' BinPackArguments: 'false' BreakBeforeBraces: 'Linux' ColumnLimit: '100' DerivePointerAlignment: 'false' IndentCaseLabels: 'false' IndentWidth: '8' IncludeBlocks: 'Regroup' KeepEmptyLinesAtTheStartOfBlocks: 'false' MaxEmptyLinesToKeep: '1' PointerAlignment: 'Right' SortIncludes: 'true' SpaceAfterCStyleCast: 'false' SpaceBeforeAssignmentOperators : 'true' SpaceBeforeParens: 'ControlStatements' SpaceInEmptyParentheses: 'false' SpacesInSquareBrackets: 'false' TabWidth: '8' UseTab: 'Always' PenaltyBreakAssignment: '3' PenaltyBreakBeforeFirstCallParameter: '15' --- Language: 'Proto' --- Language: 'Cpp' IncludeCategories: - Regex: '^"config.h"$' Priority: '0' - Regex: '' Priority: '1' - Regex: '^<' Priority: '2' - Regex: 'fwupd' Priority: '3' - Regex: '.*' Priority: '4' ... fwupd-2.0.10/.clang-tidy000066400000000000000000000023561501337203100147540ustar00rootroot00000000000000--- Checks: "-*,\ bugprone-*,\ -bugprone-assignment-in-if-condition,\ -bugprone-easily-swappable-parameters,\ -bugprone-implicit-widening-of-multiplication-result,\ -bugprone-macro-parentheses,\ -bugprone-misplaced-widening-cast,\ -bugprone-narrowing-conversions,\ -bugprone-reserved-identifier,\ -bugprone-too-small-loop-variable,\ -bugprone-unchecked-optional-access,\ misc-*,\ -misc-confusable-identifiers,\ -misc-const-correctness,\ -misc-non-private-member-variables-in-classes,\ -misc-no-recursion,\ -misc-static-assert,\ -misc-unused-parameters,\ modernize-*,\ -modernize-macro-to-enum,\ -modernize-use-trailing-return-type,\ -modernize-use-transparent-functors,\ performance-*,\ -performance-inefficient-vector-operation,\ -performance-no-int-to-ptr,\ readability-*,\ -readability-avoid-const-params-in-decls,\ -readability-braces-around-statements,\ -readability-function-cognitive-complexity,\ -readability-identifier-length,\ -readability-identifier-naming,\ -readability-implicit-bool-conversion,\ -readability-isolate-declaration,\ -readability-magic-numbers,\ -readability-non-const-parameter,\ -readability-qualified-auto,\ -readability-redundant-declaration,\ -readability-suspicious-call-argument,\ -readability-uppercase-literal-suffix,\ " ... fwupd-2.0.10/.codecov.yml000066400000000000000000000004131501337203100151330ustar00rootroot00000000000000comment: off coverage: range: 40..60 status: project: default: target: auto threshold: 1% informational: true patch: default: target: auto threshold: 1% informational: true ignore: - "**/*.h" fwupd-2.0.10/.editorconfig000066400000000000000000000006611501337203100153720ustar00rootroot00000000000000root = true [*] charset = utf-8 end_of_line = lf indent_size = 8 indent_style = space insert_final_newline = true max_line_length = 100 tab_width = 8 trim_trailing_whitespace = true [.git/**] max_line_length = 72 [*.md] max_line_length = 80 [*.{yml,yaml}] indent_size = 2 [meson{.build,.options,_options.txt}] indent_size = 2 [*.py] indent_size = 4 [*.sh] indent_size = 4 [*.rs] indent_size = 4 [*.{c,h}] indent_style = tab fwupd-2.0.10/.git-blame-ignore-revs000066400000000000000000000004211501337203100170070ustar00rootroot00000000000000# trivial: reformat the whole tree to match new format 55de39c077ca8d793178054b5d3ba7cbdf8a82a2 # dell-dock: Fix a trivial clang-format issue 61fe427d41347de3f98d9c5bed200d6fbfaac0b0 # trivial: reformat codebase with clang-format 87dcdf5412c4c9da65e4530255b1636fc0d167dd fwupd-2.0.10/.gitconfig000066400000000000000000000000611501337203100146610ustar00rootroot00000000000000[blame] ignoreRevsFile = .git-blame-ignore-revs fwupd-2.0.10/.github/000077500000000000000000000000001501337203100142525ustar00rootroot00000000000000fwupd-2.0.10/.github/ISSUE_TEMPLATE/000077500000000000000000000000001501337203100164355ustar00rootroot00000000000000fwupd-2.0.10/.github/ISSUE_TEMPLATE/bug-report-general.md000066400000000000000000000015351501337203100224640ustar00rootroot00000000000000--- name: Bug report (General) about: Create a report to help us improve title: '' labels: bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **Steps to Reproduce** Steps to reproduce the behavior. **Expected behavior** A clear and concise description of what you expected to happen. **fwupd version information** Please provide the version of the daemon and client. ```shell fwupdtool get-report-metadata fwupdtool hwids ``` Please note how you installed it (`apt`, `dnf`, `pacman`, source, etc):
**fwupd device information** Please provide the output of the fwupd devices recognized in your system. ```shell fwupdmgr get-devices --show-all-devices ```
**Additional questions** - Operating system and version: - Have you tried rebooting? - Is this a regression? fwupd-2.0.10/.github/ISSUE_TEMPLATE/bug-report-uefi.md000066400000000000000000000022171501337203100217750ustar00rootroot00000000000000--- name: Bug report (UEFI Updates) about: Issues involving UEFI device updates title: '' labels: bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **Steps to Reproduce** Steps to reproduce the behavior. **Expected behavior** A clear and concise description of what you expected to happen. **fwupd version information** Please provide the version of the daemon and client. ```shell fwupdtool get-report-metadata fwupdtool hwids ``` Please note how you installed it (`apt`, `dnf`, `pacman`, source, etc):
**fwupd device information** Please provide the output of the fwupd devices recognized in your system. ```shell fwupdmgr get-devices --show-all-devices ```
**System UEFI configuration** Please provide the output of the following commands: ```shell efibootmgr -v ``` ```shell efivar -l | grep fw ``` ```shell tree /boot ``` **Additional questions** - Operating system and version: - Have you tried rebooting? - Is this a regression? - Are you using an NVMe disk? - Is secure boot enabled? - Is this a Lenovo system with 'Boot Order Lock' turned on in the BIOS? fwupd-2.0.10/.github/ISSUE_TEMPLATE/bug-report-wd19.md000066400000000000000000000031421501337203100216270ustar00rootroot00000000000000--- name: Bug report (Dell WD19) about: Create a report to help us improve title: 'Dell WD19 upgrade issue' labels: bug assignees: 'cragw' --- **Describe the bug** A clear and concise description of what the bug is. **Steps to Reproduce** Steps to reproduce the behavior. **Expected behavior** A clear and concise description of what you expected to happen. **fwupd version information** Please provide the version of the daemon and client. ```shell fwupdtool get-report-metadata ``` Please note how you installed it (`apt`, `dnf`, `pacman`, source, etc):
**fwupd device information** Please provide the output of the external fwupd devices recognized in your system. ```shell fwupdmgr get-devices --filter=~internal ```
**Dock SKU** Please mention which module is installed in your WD19. - [ ] WD19 (Single-C) - [ ] WD19TB (Thunderbolt) - [ ] WD19DC (Dual-C) **Peripherals connected to the dock** Please describe all devices connected to the dock. Be as specific as possible, including USB devices, hubs, monitors, and downstream type-C devices. **Verbose daemon logs** First enable daemon verbose logs collection. ```shell fwupdmgr modify-config "VerboseDomains" "*" ``` Then try to reproduce the issue. Even if it doesn't reproduce, please attach the daemon verbose logs collected from the system journal. ```shell journalctl -b -u fwupd.service ``` **Additional questions** - Operating system and version: - Have you tried unplugging the dock or any peripherals from your machine? - Have you tried to power cycle the dock from the AC adapter? - Is this a regression? fwupd-2.0.10/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011341501337203100221610ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. fwupd-2.0.10/.github/ISSUE_TEMPLATE/question.md000066400000000000000000000006221501337203100206260ustar00rootroot00000000000000--- name: Question about: Ask a question about the project or how to do something title: '' labels: question assignees: '' --- **Describe the question** A clear and concise description of what you are wondering about. **fwupd version information** Please provide the version of the daemon and client if applicable to your current installation of `fwupd`. ```shell fwupdtool get-report-metadata ``` fwupd-2.0.10/.github/dependabot.yml000066400000000000000000000001661501337203100171050ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" fwupd-2.0.10/.github/pull_request_template.md000066400000000000000000000002731501337203100212150ustar00rootroot00000000000000Type of pull request: - [ ] New plugin (Please include [new plugin checklist](https://github.com/fwupd/fwupd/wiki/New-plugin-checklist)) - [ ] Code fix - [ ] Feature - [ ] Documentation fwupd-2.0.10/.github/stale.yml000066400000000000000000000023251501337203100161070ustar00rootroot00000000000000# Number of days of inactivity before an issue becomes stale daysUntilStale: 30 # Number of days of inactivity before a stale issue is closed daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - enhancement - regression # Label to use when marking an issue as stale staleLabel: wontfix # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Please note: We are just a few people who contribute to a shared project, and it's impossible for us to fix every bug with such limited resources. If you want to investigate and try to help solve this yourself, we will review all pull requests from new contributors. If this is issue is important to you for your business please talk with your technical account manager about arranging resources to solve this issue. You might even consider hiring someone to write the code if you're unable to do so yourself, e.g. see: https://fwupd.org/lvfs/docs/consulting # Comment to post when closing a stale issue. Set to `false` to disable closeComment: false fwupd-2.0.10/.github/workflows/000077500000000000000000000000001501337203100163075ustar00rootroot00000000000000fwupd-2.0.10/.github/workflows/ci.yml000066400000000000000000000024011501337203100174220ustar00rootroot00000000000000name: Continuous Integration on: push: branches: [ main ] permissions: contents: read jobs: snap: uses: ./.github/workflows/snap.yml with: deploy: true secrets: inherit matrix: uses: ./.github/workflows/matrix.yml with: publish: false secrets: inherit fuzzing: permissions: actions: read # to fetch the artifacts (google/oss-fuzz/infra/cifuzz/actions/run_fuzzers) contents: read # to clone the repo (google/oss-fuzz/infra/cifuzz/actions/run_fuzzers) runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Build Fuzzers id: build uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@32f1d4deadc82279ec9001a837f2424e185c69a2 # master with: oss-fuzz-project-name: 'fwupd' dry-run: false - name: Run Fuzzers uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@32f1d4deadc82279ec9001a837f2424e185c69a2 # master with: oss-fuzz-project-name: 'fwupd' fuzz-seconds: 150 dry-run: false - name: Upload Crash uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: failure() && steps.build.outcome == 'success' with: name: artifacts path: ./out/artifacts fwupd-2.0.10/.github/workflows/codeql-analysis.yml000066400000000000000000000024001501337203100221160ustar00rootroot00000000000000name: "CodeQL" on: push: branches: [ main ] pull_request: branches: [ main ] permissions: contents: read jobs: analyze: name: Analyze runs-on: ubuntu-22.04 permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'cpp', 'python' ] steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Initialize CodeQL uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 with: languages: ${{ matrix.language }} - name: Install dependencies run: | sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list sudo apt-get update sudo ./contrib/ci/fwupd_setup_helpers.py install-dependencies --yes -o ubuntu python3 -m pip install --user "meson >= 0.62.0" - name: Build run: | mkdir -p $GITHUB_WORKSPACE/build cd $GITHUB_WORKSPACE/build meson setup .. -Dman=false --prefix=$GITHUB_WORKSPACE/dist -Dlibxmlb:gtkdoc=false ninja - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 fwupd-2.0.10/.github/workflows/create_containers.yml000066400000000000000000000023661501337203100225310ustar00rootroot00000000000000name: Create containers on: workflow_dispatch: schedule: - cron: '0 0 * * *' permissions: contents: read jobs: push_to_registry: permissions: packages: write # for docker/build-push-action runs-on: ubuntu-latest strategy: fail-fast: false matrix: os: [precommit, fedora, debian-x86_64, arch, debian-i386, ubuntu-x86_64] steps: - name: Check out the repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: "Generate Dockerfile" env: OS: ${{ matrix.os }} run: ./contrib/ci/generate_docker.py - name: Set up Docker Buildx uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - name: Login to GitHub Container Registry uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Push to GitHub Packages uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0 with: context: . push: true tags: ghcr.io/fwupd/fwupd/fwupd-${{matrix.os}}:latest fwupd-2.0.10/.github/workflows/dependency-review.yml000066400000000000000000000017121501337203100224500ustar00rootroot00000000000000# Dependency Review Action # # This Action will scan dependency manifest files that change as part of a Pull Request, # surfacing known-vulnerable versions of the packages declared or updated in the PR. # Once installed, if the workflow run is marked as required, # PRs introducing known-vulnerable packages will be blocked from merging. # # Source repository: https://github.com/actions/dependency-review-action name: 'Dependency Review' on: [pull_request] permissions: contents: read jobs: dependency-review: runs-on: ubuntu-latest steps: - name: Harden Runner uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: egress-policy: audit - name: 'Checkout Repository' uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: 'Dependency Review' uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 fwupd-2.0.10/.github/workflows/matrix.yml000066400000000000000000000161231501337203100203410ustar00rootroot00000000000000on: workflow_call: inputs: publish: required: true type: boolean permissions: contents: read jobs: build: runs-on: ubuntu-latest needs: library strategy: fail-fast: false matrix: os: [fedora, debian-x86_64, arch, debian-i386, ubuntu-x86_64] steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Docker login run: docker login docker.pkg.github.com -u $GITHUB_ACTOR -p $GITHUB_TOKEN env: GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - name: Download tarball if: matrix.os == 'fedora' uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 id: download with: name: tarball - name: Build in container env: CI_NETWORK: true CI: true VERSION: ${{ needs.library.outputs.version }} run: | docker run --privileged -e CI_NETWORK=$CI_NETWORK -e CI=$CI -e VERSION=$VERSION -t \ -v $GITHUB_WORKSPACE:/github/workspace \ docker.pkg.github.com/fwupd/fwupd/fwupd-${{matrix.os}}:latest - name: Save any applicable artifacts uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: ${{ matrix.os }} path: ${{ github.workspace }}/dist/* if-no-files-found: ignore - name: Test in container env: CI_NETWORK: true CI: true run: | docker run --privileged -e CI_NETWORK=$CI_NETWORK -e CI=$CI -t \ -v $GITHUB_WORKSPACE:/github/workspace \ docker.pkg.github.com/fwupd/fwupd/fwupd-${{matrix.os}}:latest \ contrib/ci/${{matrix.os}}-test.sh - name: Save any coverage data uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: coverage-${{ join(matrix.*, '-') }} path: ${{ github.workspace }}/coverage.xml - name: Upload to codecov if: matrix.os == 'debian-x86_64' || matrix.os == 'debian-i386' || matrix.os == 'arch' uses: codecov/codecov-action@v5 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} openbmc: runs-on: ubuntu-22.04 if: ${{ !inputs.publish }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Refresh dependencies run: | sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list sudo apt update - name: Install dependencies run: | sudo ./contrib/ci/fwupd_setup_helpers.py install-dependencies -o ubuntu --yes sudo ./contrib/ci/fwupd_setup_helpers.py test-meson - name: Build run: | ./contrib/build-openbmc.sh --prefix=/home/runner/.root library: runs-on: ubuntu-latest outputs: version: ${{ steps.version.outputs.version }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Refresh dependencies run: | sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list sudo apt update - name: Install dependencies run: | sudo ./contrib/ci/fwupd_setup_helpers.py install-dependencies -o ubuntu --yes sudo ./contrib/ci/fwupd_setup_helpers.py test-meson - name: Build run: | meson setup build \ -Dbuild=library \ -Ddocs=disabled \ -Dman=false \ -Dauto_features=disabled \ -Dtests=false ninja -C build dist - name: Save version id: version run: | echo "version=$(meson introspect build --projectinfo | jq -r .version)" >> $GITHUB_OUTPUT - name: Save tarball uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: tarball path: ${{ github.workspace }}/build/meson-dist/*xz macos: runs-on: macos-latest if: ${{ !inputs.publish }} steps: - name: install dependencies run: | brew install meson usb.ids gobject-introspection libarchive json-glib protobuf-c vala gi-docgen python3 -m pip install --user jinja2 --break-system-packages - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: configure run: ./contrib/ci/build_macos.sh - name: build run: ninja -C build-macos build-windows: runs-on: ubuntu-latest container: image: fedora:41 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: configure run: ./contrib/ci/build_windows.sh - name: upload-artifact uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: windows path: | ${{ github.workspace }}/dist/setup/*.msi ${{ github.workspace }}/dist/news.txt publish-docs: name: Publish docs if: ${{ inputs.publish }} runs-on: ubuntu-latest needs: build steps: - uses: actions/checkout@v4 - name: Download artifact uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 id: download with: name: ubuntu-x86_64 - name: Install SSH key uses: shimataro/ssh-key-action@v2 with: key: ${{ secrets.FWUPD_GITHUB_IO_SSH_KEY }} name: id_rsa # optional known_hosts: unnecessary if_key_exists: fail # replace / ignore / fail; optional (defaults to fail) - name: Clone docs run: | cd share/doc/fwupd git clone --depth 1 git@github.com:fwupd/fwupd.github.io.git - name: Trigger docs deployment run: | cd share/doc/fwupd/fwupd.github.io git config credential.helper 'cache --timeout=120' git config user.email "info@fwupd.org" git config user.name "Documentation deployment Bot" rm -rf * cp ../../libfwupd* ../*html . -R git add . git commit -a --allow-empty -m "Trigger deployment" git push git@github.com:fwupd/fwupd.github.io.git publish-windows: name: Publish Windows binaries runs-on: ubuntu-latest if: ${{ inputs.publish }} needs: build-windows steps: - uses: actions/checkout@v4 - name: Download artifact uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 id: download with: name: windows - name: Populate release body id: read_release shell: bash run: | r=$(cat dist/news.txt) echo "RELEASE_BODY=$r" >> $GITHUB_OUTPUT - name: Upload Binaries to Release uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} tag: ${{ github.ref }} file_glob: true file: dist/setup/*.msi body: | ${{ steps.read_release.outputs.RELEASE_BODY }} # <--- Use environment variables that was created earlier fwupd-2.0.10/.github/workflows/pull-request-reviews.yml000066400000000000000000000015551501337203100231640ustar00rootroot00000000000000name: Pull Request reviews on: pull_request: branches: [ main ] permissions: contents: read # to fetch code (actions/checkout) jobs: pre-commit: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Docker login run: docker login docker.pkg.github.com -u $GITHUB_ACTOR -p $GITHUB_TOKEN env: GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - name: Run pre-commit hooks run: | docker run --privileged -t -v $GITHUB_WORKSPACE:/github/workspace docker.pkg.github.com/fwupd/fwupd/fwupd-precommit:latest snap: needs: pre-commit uses: ./.github/workflows/snap.yml with: deploy: false secrets: inherit matrix: needs: pre-commit uses: ./.github/workflows/matrix.yml with: publish: false secrets: inherit fwupd-2.0.10/.github/workflows/release.yml000066400000000000000000000002621501337203100204520ustar00rootroot00000000000000name: Publish on: push: tags: - '[0-9]+.[0-9]+.[0-9]+' jobs: publish: uses: ./.github/workflows/matrix.yml with: publish: true secrets: inherit fwupd-2.0.10/.github/workflows/scorecard.yml000066400000000000000000000060651501337203100210060ustar00rootroot00000000000000# This workflow uses actions that are not certified by GitHub. They are provided # by a third-party and are governed by separate terms of service, privacy # policy, and support documentation. name: Scorecard supply-chain security on: # For Branch-Protection check. Only the default branch is supported. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection branch_protection_rule: # To guarantee Maintained check is occasionally updated. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained schedule: - cron: '25 11 * * 1' push: branches: [ "main" ] # Declare default permissions as read only. permissions: read-all jobs: analysis: name: Scorecard analysis runs-on: ubuntu-latest permissions: # Needed to upload the results to code-scanning dashboard. security-events: write # Needed to publish results and get a badge (see publish_results below). id-token: write # Uncomment the permissions below if installing in a private repository. # contents: read # actions: read steps: - name: "Checkout code" uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Run analysis" uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 with: results_file: results.sarif results_format: sarif # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: # - you want to enable the Branch-Protection check on a *public* repository, or # - you are installing Scorecard on a *private* repository # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. # repo_token: ${{ secrets.SCORECARD_TOKEN }} # Public repositories: # - Publish results to OpenSSF REST API for easy access by consumers # - Allows the repository to include the Scorecard badge. # - See https://github.com/ossf/scorecard-action#publishing-results. # For private repositories: # - `publish_results` will always be set to `false`, regardless # of the value entered here. publish_results: true # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: SARIF file path: results.sarif retention-days: 5 # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 with: sarif_file: results.sarif fwupd-2.0.10/.github/workflows/snap.yml000066400000000000000000000057121501337203100200000ustar00rootroot00000000000000name: Snap workflow on: workflow_call: inputs: deploy: required: true type: boolean permissions: contents: read jobs: build-snap: runs-on: ubuntu-latest outputs: snap_name: ${{ steps.snapcraft.outputs.snap }} channel: ${{ steps.channel.outputs.channel }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - id: channel run: | if git describe --exact-match; then echo "channel=candidate" >> $GITHUB_OUTPUT else echo "channel=edge" >> $GITHUB_OUTPUT fi - id: prep run: | mkdir -p snap ln -s ../contrib/snap/snapcraft.yaml snap/snapcraft.yaml - uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1.3.0 id: snapcraft - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: snap path: ${{ steps.snapcraft.outputs.snap }} test-snap: needs: build-snap runs-on: ubuntu-latest steps: - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 id: download with: name: snap - id: install-snap run: | sudo apt remove fwupd -y sudo snap install --dangerous ${{ needs.build-snap.outputs.snap_name }} sudo snap connect fwupd:polkit :polkit sudo fwupd.fwupdtool enable-test-devices - name: Set up snap specific environment run: | # location which can be used as TMPDIR and is accessible from both snap # sandbox and from the host SNAP_TEST_TMPDIR=/var/snap/fwupd/common/fwupd-tests sudo mkdir -p "$SNAP_TEST_TMPDIR" echo "SNAP_TEST_TMPDIR=$SNAP_TEST_TMPDIR" >> "$GITHUB_ENV" - name: Run fwupdmgr tests run: | sudo TMPDIR="$SNAP_TEST_TMPDIR" \ /snap/fwupd/current/share/installed-tests/fwupd/fwupdmgr.sh - name: Run fwupd tests run: | sudo TMPDIR="$SNAP_TEST_TMPDIR" \ /snap/fwupd/current/share/installed-tests/fwupd/fwupd.sh - name: Run fwupdtool tests run: | sudo TMPDIR="$SNAP_TEST_TMPDIR" \ /snap/fwupd/current/share/installed-tests/fwupd/fwupdtool.sh - name: Run fwupdtool efiboot specific tests run: | sudo TMPDIR="$SNAP_TEST_TMPDIR" \ /snap/fwupd/current/share/installed-tests/fwupd/fwupdtool-efiboot.sh deploy-store: needs: [build-snap, test-snap] runs-on: ubuntu-latest if: ${{ inputs.deploy }} steps: - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 id: download with: name: snap - uses: snapcore/action-publish@214b86e5ca036ead1668c79afb81e550e6c54d40 # v1.2.0 env: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }} with: snap: ${{ needs.build-snap.outputs.snap_name }} release: ${{ needs.build-snap.outputs.channel }} fwupd-2.0.10/.gitignore000066400000000000000000000007201501337203100147010ustar00rootroot00000000000000/build /build-win32 /dist /.vscode .vscode-ctags /build-dir /.flatpak-builder /repo *.flatpak *.snap /fwupd_source.tar.bz2 /parts /prime /stage /snap/.snapcraft /libxmlb /*.deb /*.ddeb /*.changes /*.buildinfo /fwupd*.build /*.dsc /*.xz /*.gz /venv __pycache__ plugins/acpi-dmar/tests/ plugins/acpi-facp/tests/ plugins/nvme/tests/ plugins/synaptics-prometheus/tests/ plugins/tpm-eventlog/tests/ .buildconfig .ossfuzz *.rej /snap fwupd.txt fwupdtool.txt *.efi *.zip fwupd-2.0.10/.gitmodules000066400000000000000000000001561501337203100150710ustar00rootroot00000000000000[submodule "contrib/flatpak"] path = contrib/flatpak url = https://github.com/flathub/org.freedesktop.fwupd fwupd-2.0.10/.markdownlint.json000066400000000000000000000002131501337203100163700ustar00rootroot00000000000000{ "default": true, "MD033": false, "MD041": false, "MD036": false, "MD013": { "tables": false, "line_length": 1000 } } fwupd-2.0.10/.pre-commit-config.yaml000066400000000000000000000110011501337203100171640ustar00rootroot00000000000000default_stages: [commit] repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: no-commit-to-branch args: [--branch, main, --pattern, 1_.*_X] - id: check-added-large-files - id: check-byte-order-marker - id: check-executables-have-shebangs - id: forbid-new-submodules - id: check-yaml exclude: '.clang-format' - id: check-json - id: pretty-format-json args: ['--no-sort-keys', '--autofix'] - id: check-symlinks - id: check-xml - id: end-of-file-fixer types_or: [c, shell, python, proto] - id: trailing-whitespace types_or: [c, shell, python, xml] - id: check-docstring-first - id: check-merge-conflict - id: mixed-line-ending args: [--fix=lf] - repo: https://github.com/codespell-project/codespell rev: v2.2.2 hooks: - id: codespell args: ['--config', './contrib/codespell.cfg', --write-changes] - repo: https://github.com/ambv/black rev: 23.12.1 hooks: - id: black - repo: local hooks: - id: no-binaries name: prevent committing binaries to tree language: script entry: /usr/bin/false types: [binary] exclude: | (?x)^( contrib/qubes/doc/img/\w+\.(png|jpg)| data/fwupd\.ico| data/icons/(128x128|64x64)/org\.freedesktop.fwupd\.png| docs/device-emulation-(gnome-firmware-record|assets)\.png| docs/debug_(attached|tool_selector|breakpoint|task)\.png| docs/test_task\.png| docs/win32-(start-menu|run-anyway|term1|term2|uac|uac2)\.png| data/org\.freedesktop\.fwupd\.png| data/tests/fakedevice\d{3}\.(bin|jcat)| libfwupdplugin/tests/efi/efivars/(Boot\d{4}-8be4df61-93ca-11d2-aa0d-00e098032b8c| BootOrder-8be4df61-93ca-11d2-aa0d-00e098032b8c| OsIndicationsSupported-8be4df61-93ca-11d2-aa0d-00e098032b8c| fwupd-ddc0ee61-e7f0-4e7d-acc5-c070a398838e-0-0abba7dc-e516-4167-bbf5-4d9d1c739416)| plugins/ch341a/ch341a-vmod\.png| plugins/lenovo-thinklmi/tests/efi/efivars/(fwupd-ddc0ee61-e7f0-4e7d-acc5-c070a398838e-0-0abba7dc-e516-4167-bbf5-4d9d1c739416| OsIndicationsSupported-8be4df61-93ca-11d2-aa0d-00e098032b8c)| plugins/logitech-hidpp/data/dump\.(csv\.gz|tdc)| plugins/pci-bcr/config| plugins/redfish/tests/efi/efivars/(RedfishIndications-16faa37e-4b6a-4891-9028-242de65a3b70| RedfishOSCredentials-16faa37e-4b6a-4891-9028-242de65a3b70)| plugins/uefi-capsule/tests/test\.bmp| src/tests/auth/firmware\.xml\.gz| src/tests/history_v[12]\.db| src/tests/sys/devices/pci0000:00/0000:00:14\.0/usb1/1-1/descriptors )$ - id: check-null-false-returns name: check for null / false return mismatch language: script entry: ./contrib/ci/check-null-false-returns.py - id: check-potfiles name: check for missing translated files from potfiles language: script entry: ./contrib/ci/check-potfiles.py - id: check-finalizers name: check for missing GObject parent finalize language: script entry: ./contrib/ci/check-finalizers.py - id: check-headers name: check for superfluous includes language: script entry: ./contrib/ci/check-headers.py - id: check-source name: check source code for common issues language: script entry: ./contrib/ci/check-source.py - id: check-quirks name: check quirk style language: script entry: ./contrib/ci/check-quirks.py - id: shellcheck name: check shellscript style language: system entry: shellcheck --severity=warning -e SC2068 types: [shell] - id: run-tests name: run tests before pushing language: system entry: "test-fwupd" stages: [push] - id: clang-format name: clang-format language: script entry: ./contrib/reformat-code.py types: [c] - id: check-license name: Check license header types_or: [shell, c, python] language: script entry: ./contrib/ci/check-license.py - repo: https://github.com/igorshubovych/markdownlint-cli rev: v0.38.0 hooks: - id: markdownlint args: ['--fix', '--ignore', '.github'] - repo: https://github.com/gitleaks/gitleaks rev: v8.16.3 hooks: - id: gitleaks fwupd-2.0.10/.tx/000077500000000000000000000000001501337203100134235ustar00rootroot00000000000000fwupd-2.0.10/.tx/config000066400000000000000000000002411501337203100146100ustar00rootroot00000000000000[main] host = https://www.transifex.com [o:freedesktop:p:fwupd:r:main] file_filter = po/.po source_file = po/fwupd.pot source_lang = en type = PO fwupd-2.0.10/CODE_OF_CONDUCT.md000066400000000000000000000062251501337203100155160ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at . The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ fwupd-2.0.10/COMMITMENT000066400000000000000000000037761501337203100143260ustar00rootroot00000000000000Common Cure Rights Commitment, version 1.0 Before filing or continuing to prosecute any legal proceeding or claim (other than a Defensive Action) arising from termination of a Covered License, we commit to extend to the person or entity ('you') accused of violating the Covered License the following provisions regarding cure and reinstatement, taken from GPL version 3. As used here, the term 'this License' refers to the specific Covered License being enforced. However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. We intend this Commitment to be irrevocable, and binding and enforceable against us and assignees of or successors to our copyrights. Definitions 'Covered License' means the GNU General Public License, version 2 (GPLv2), the GNU Lesser General Public License, version 2.1 (LGPLv2.1), or the GNU Library General Public License, version 2 (LGPLv2), all as published by the Free Software Foundation. 'Defensive Action' means a legal proceeding or claim that We bring against you in response to a prior proceeding or claim initiated by you or your affiliate. 'We' means each contributor to this repository as of the date of inclusion of this file, including subsidiaries of a corporate contributor. This work is available under a Creative Commons Attribution-ShareAlike 4.0 International license. fwupd-2.0.10/CONTRIBUTING.md000066400000000000000000000056421501337203100151520ustar00rootroot00000000000000# Contributor Guidelines ## Getting started To set up your local fwupd development environment, from the top level of the checkout run: ```shell ./contrib/setup ``` This will create pre-commit hooks to fixup many code style issues before your code is submitted. On some Linux distributions this will install all build dependencies needed to compile fwupd as well. A [virtualenv](https://virtualenv.pypa.io/en/latest/user_guide.html) will be created in `venv/` in the checkout that is used for building and running fwupd without affecting the local system installation. To enter this virtualenv run: ```shell source venv/bin/activate ``` To build fwupd in the venv run: ```shell build-fwupd ``` Wrappers are configured while in the venv to run `fwupdtool`, `fwupd`, and `fwupdmgr` using the virtualenv directory structure. To leave the virtualenv run: ```shell deactivate ``` ## Coding Style The coding style to respect in this project is very similar to most GLib projects. In particular, the following rules are largely adapted from the PackageKit Coding Style. * 8-space tabs for indentation * Prefer lines of less than <= 100 columns * No spaces between function name and braces (both calls and macro declarations) * If function signature/call fits in a single line, do not break it into multiple lines * Prefer descriptive names over abbreviations (unless well-known) and shortening of names. e.g `device` not `dev` * Single statements inside if/else should not be enclosed by '{}' * Use comments to explain why something is being done, but also avoid over-documenting the obvious. Here is an example of useless comment: // Fetch the document fetch_the_document(); * Comments should not start with a capital letter or end with a full stop and should be C-style, not C++-style, e.g. `/* this */` not `// this` * Each object should go in a separate .c file and be named according to the class * Use g_autoptr() and g_autofree whenever possible, and avoid `goto out` error handling * Failing methods should return FALSE with a suitable `GError` set * Trailing whitespace is forbidden * Pointers should be checked for NULL explicitly, e.g. `foo != NULL` not `!foo` * Use the correct debug level: * `g_debug()` -- low level plugin and daemon development, typically only useful to programmers * `g_info()` -- generally useful messages, typically shown when using `--verbose` * `g_message()` -- important messages, typically shown in service output * `g_warning()` -- warning messages, typically shown in service output * `g_critical()` -- critical messages, typically shown before the daemon aborts **NOTE:** Do not use `g_error()` -- it's not appropriate to abort the daemon on error. `./contrib/reformat-code.py` can be used in order to get automated formatting. Calling the script without arguments formats the current patch while passing commits will do formatting on everything changed since that commit. fwupd-2.0.10/COPYING000066400000000000000000000636421501337203100137600ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. 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 not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the 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 specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library 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 2.1 of the License, or (at your option) any later version. This library 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 library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! fwupd-2.0.10/MAINTAINERS000066400000000000000000000001131501337203100144020ustar00rootroot00000000000000Richard Hughes Mario Limonciello fwupd-2.0.10/README.md000066400000000000000000000137521501337203100142010ustar00rootroot00000000000000# fwupd [![Build Status](https://github.com/fwupd/fwupd/actions/workflows/ci.yml/badge.svg)](https://github.com/fwupd/fwupd/actions/workflows/ci.yml) [![CodeQL](https://github.com/fwupd/fwupd/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/fwupd/fwupd/actions/workflows/codeql-analysis.yml) [![Coverity Scan Build Status](https://scan.coverity.com/projects/10744/badge.svg)](https://scan.coverity.com/projects/10744) [![Fuzzing Status](https://oss-fuzz-build-logs.storage.googleapis.com/badges/fwupd.svg)](https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:fwupd) [![Codecov Coverage Status](https://codecov.io/gh/fwupd/fwupd/graph/badge.svg?token=vykt2ROfu9)](https://codecov.io/gh/fwupd/fwupd) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/fwupd/fwupd/badge)](https://securityscorecards.dev/viewer/?uri=github.com/fwupd/fwupd) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8751/badge)](https://www.bestpractices.dev/projects/8751) This project aims to make updating firmware on Linux automatic, safe, and reliable. Additional information is available [at the website](https://fwupd.org/). ## Where to get help? - GitHub issues & discussions in [this repository](https://github.com/fwupd/fwupd) ## Compiling See [Building and Debugging](docs/building.md) for how to build the fwupd development environment. **NOTE:** In most cases, end users should not compile fwupd from scratch; it's a complicated project with dozens of dependencies (and as many configuration options) and there's just too many things that can go wrong. Users should just have fwupd installed and updated by their distro, managed and tested by the package maintainer. The distribution will have also done some testing with how fwupd interacts with other software on your system, for instance using GNOME Software. Installing fwupd using [Snap](https://github.com/fwupd/fwupd/wiki/fwupd-snap) or using [Flatpak](https://github.com/fwupd/fwupd/wiki/fwupd-flatpak) might be useful to update a specific device on the command line that needs a bleeding edge fwupd version, but it should not be considered as a replacement to the distro-provided system version. ### Using Tartan [Tartan](https://gitlab.freedesktop.org/tartan/tartan/-/wikis/home) is a LLVM static analysis plugin built to analyze GLib code. It can be installed and then run using: mkdir build-tartan CC=clang-17 meson ../ SCANBUILD=../contrib/tartan.sh ninja scan-build ## LVFS This project is configured by default to download firmware from the [Linux Vendor Firmware Service (LVFS)](https://fwupd.org/). This service is available to all OEMs and firmware creators who would like to make their firmware available to Linux users. You can find more information about the technical details of creating a firmware capsule in the hardware vendors section of the [fwupd website](https://fwupd.org). ## Basic usage flow (command line) If you have a device with firmware supported by fwupd, this is how you can check for updates and apply them using fwupd's command line tools. `# fwupdmgr get-devices` This will display all devices detected by fwupd. `# fwupdmgr refresh` This will download the latest metadata from LVFS. `# fwupdmgr get-updates` If updates are available for any devices on the system, they'll be displayed. `# fwupdmgr update` This will download and apply all updates for your system. - Updates that can be applied live will be done immediately. - Updates that run at bootup will be staged for the next reboot. You can find more information about the update workflow in the end users section of the [fwupd website](https://fwupd.org). ## Passim If the [Passim](https://github.com/hughsie/passim/blob/main/README.md) project is also installed and enabled, fwupd will re-publish the downloaded metadata file to be served on `0.0.0.0:27500` by default. Other clients on the same network can make use of this via mDNS/LLMNR to reduce network bandwidth to configured remotes. To disable this functionality either set `P2pPolicy=none` in `/etc/fwupd/daemon.conf`, uninstall the passim package or use `systemctl mask passim.service` on the terminal. ## Reporting status fwupd will encourage users to report both successful and failed updates back to LVFS. This is an optional feature, but encouraged as it provides valuable feedback to LVFS administrators and OEM developers regarding firmware update process efficacy. The privacy policy regarding this data can be viewed on the [lvfs readthedocs site](https://lvfs.readthedocs.io/en/latest/privacy.html). To report the status of an update, run: `# fwupdmgr report-history` Only updates that were distributed from the LVFS will be reported to the LVFS. ## Enterprise use The flow of updates can be controlled in the enterprise using the "approved updates" feature. This allows the domain administrator to filter the possible updates from a central server (e.g. the LVFS, or a mirror) to only firmware that have been tested specifically in your organization. The list of approved updates can be enabled by adding `ApprovalRequired=true` to the remote configuration file, e.g. `lvfs.conf`. Once enabled, the list of approved updates can be set in `fwupd.conf` using a comma-delimited list. For example: ApprovedFirmware=foo,bar Where `foo,bar` refers to the container checksums that would correspond to two updates in the metadata file. Additionally, the list of approved firmware can be supplemented using `fwupdmgr set-approved-firmware baz` or using the D-Bus interface. ## Other frontends fwupdmgr is a command line client, but various additional graphical frontends are enumerated in the [fwupdmgr man page](https://fwupd.github.io/libfwupdplugin/fwupdmgr.html#description). ## SAST Tools - [Coverity](https://scan.coverity.com/) - static analyzer for Java, C/C++, C#, JavaScript, Ruby, and Python code. - [PVS-Studio](https://pvs-studio.com/en/pvs-studio/?utm_source=website&utm_medium=github&utm_campaign=open_source) - static analyzer for C, C++, C#, and Java code. fwupd-2.0.10/RELEASE000066400000000000000000000030541501337203100137170ustar00rootroot00000000000000fwupd Release Notes Forking stable branch: When forking main into a stable 2_1_X, be sure to disable the following CI jobs: * publish-docs (`.circleci/config.yml`) * deploy-store (`snap.yaml`) Also update `SECURITY.md`, removing the oldest branch and add the new branch at the top. To make sure it's done right, you can reference commit 433e809318c68c9ab6d4ae50ee9c4312503185d8 Check IFD parsing (if the files are available): ../../contrib/check-ifd-firmware.py ../../fwupd-test-roms/ Write release entries: ../../contrib/generate-release.py # copy into ../../data/org.freedesktop.fwupd.metainfo.xml appstream-util appdata-to-news ../../data/org.freedesktop.fwupd.metainfo.xml > NEWS Update translations: ninja-build fwupd-pot cd ../.. tx push --source tx pull --all --force --minimum-perc=5 cd venv/build ninja-build fix-translations git add ../../po/*.po 2. Commit changes to git: # MAKE SURE THIS IS CORRECT export release_ver="2.0.10" git commit -a -m "Release fwupd ${release_ver}" --no-verify git tag -s -f -m "Release fwupd ${release_ver}" "${release_ver}" git push --tags git push 3. Generate the tarball: ninja dist 3a. Generate the additional verification metadata gpg -b -a meson-dist/fwupd-${release_ver}.tar.xz 4. Create release and upload tarball to https://github.com/fwupd/fwupd/tags 5. Do post release version bump in meson.build 6. Commit changes: git commit -a -m "trivial: post release version bump" --no-verify git push 7. Update flatpak package for new release: https://github.com/flathub/org.freedesktop.fwupd fwupd-2.0.10/SECURITY.md000066400000000000000000000167651501337203100145220ustar00rootroot00000000000000# Security Policy Due to the nature of what we are doing, fwupd takes security very seriously. If you have any concerns please [let us know](https://github.com/fwupd/fwupd/security/advisories/new). ## Supported Versions The `main`, and `1.9.x`, branches are fully supported by the upstream authors. Additionally, the `1.8.x` branch is supported just for security fixes. | Version | Supported | EOL | | ------- | ------------------ | ---------- | | 2.0.x | :heavy_check_mark: | 2028-01-01 | | 1.9.x | :heavy_check_mark: | 2027-01-01 | | 1.8.x | :white_check_mark: | 2025-01-01 | | 1.7.x | :x: | 2024-06-01 | | 1.6.x | :x: | 2024-01-01 | | 1.5.x | :x: | 2022-01-01 | | 1.4.x | :x: | 2021-05-01 | | 1.3.x | :x: | 2020-07-01 | | 1.2.x | :x: | 2019-12-01 | | 1.1.x | :x: | 2018-11-01 | | 1.0.x | :x: | 2018-10-01 | | 0.9.x | :x: | 2018-02-01 | Older releases than this are unsupported by upstream but may be supported by your distributor or distribution. If you open an issue with one of these older releases the very first question from us is going to be asking if it's fixed on a supported branch. You can use the flatpak or snap packages if your distributor is unwilling to update to a supported version. ## Reporting a Vulnerability If you find a vulnerability in fwupd you should let us know using a [private vulnerability disclosure](https://github.com/fwupd/fwupd/security/advisories/new) on GitHub, with a description of the issue, the steps you took to create the issue, affected versions, and, if known, mitigations for the issue. Failing that, please report the issue against the `fwupd` component in Red Hat bugzilla, with the security checkbox set. You should get a response within 3 days. We have no bug bounty program, but we're happy to credit you in updates if this is what you would like us to do. ## Threat Modeling ### Who We Trust In this diagram, the arrow shows the flow of *information* from one entity to another. Important things to note: * OEMs and ODMs have to apply for a LVFS account and the website and email domain is verified * OEMs and ODMs can only upload for devices that match their device-supplied vendor-id * The relationship between the OEM/ODM and affiliate ISV/IBV is implemented using per-task ACLs * The LVFS is assumed to be managed by a vendor-neutral trusted team * Signing of the metadata and firmware is implemented using PKCS#7 and GPG * End users only trust the LVFS signing signatures by default * Metadata contains SHA-1 and SHA-256 hashes of the firmware archive * Access to the embargo remotes requires knowing the secret vendor hash, but not a token * The firmware archive internal metadata and firmware payload are both signed * Reports have to be signed by the user machine key to be attributable to an OEM or QA team * Signed reports are uploaded using a username and access token * SBOM metadata is extracted from the payload by the LVFS and formatted into HTML/SWID formats * Security researchers can only run FwHunt/Yara scans on public firmware ```mermaid flowchart TD LVFS((LVFS Webservice)) -- "SBOM.html" --> User(End User) LVFS -- "md.[xml|jcat] 🔒" --> CDN(Fastly CDN) CDN -- "md.[xml|jcat] 🔒" --> User LVFS -- "embargo.[xml|jcat] 🔒" --> User LVFS -- "fw.cab 🔒" --> User User -. "report.json" .-> LVFS User -. "hsi.json" .-> LVFS QA(QA Teams) -- "report.json 🔒" --> LVFS OEM(Device Vendor) -- "fw.cab" --> LVFS ODM(Device Manufacturer) -- "fw.[bin|cab]" --> OEM OEM -. "report.json 🔒" .-> LVFS ODM -. "fw.cab" .-> LVFS IBV(BIOS Vendor) -- "fw.bin" --> ODM ISV(Silicon Vendor) -- "fw.bin" --> ODM User -. "md.xml 🔒" .-> User2(Other LAN Users) User -. "fw.cab 🔒" .-> User2 LVFS -- "FwHunt|Yara" --> SecAlert(Security Researchers) ``` ### What We Trust In this diagram, the arrow shows the flow of *data* between different processes or machines. Important things to note: * User-facing clients like `fwupdmgr` and `gnome-software` should not be run as the root user * The fwupd daemon should be run as a privileged user and have no access to the network * Privilege escalation is performed using PolicyKit based on fine-grained ACLs, if available * Passwords may be in plaintext in `remotes.d` or config files, and should be readable only by root * The fwupd daemon will only install firmware archives signed by the LVFS unless modified * The fwupd daemon scans and verifies the mtime of various files at startup to build caches * If SecureBoot is turned on then `fwupd-efi` has to be signed by a trusted key * Files are passed between the user client and fwupd using an open file-descriptor, not a filename * There is no public interface to either the PostgreSQL or EFS data stores * The fwupd daemon may need to mount the EFI system partition to copy in capsule payloads * The `fwupdtool` debug tool requires root access to perform updates on devices ```mermaid flowchart TD subgraph Vendor OEM(OEM/ODM/ISV Firmware) end subgraph User fwupdmgr((fwupdmgr\ngnome-software)) end subgraph Local Network User fwupdmgr2((fwupdmgr\ngnome-software)) end subgraph Privileged fwupd((fwupd\ndaemon)) passim((passimd)) fwupdengine(FuEngine) fwupdtool(fwupdtool\ndebug\ntool) fwupd-efi(fwupd capsule loader) Pending[(history.db)] Kernel((OS Kernel)) ESP[(EFI\nSystem\nPartition)] SPI[(System SPI)] UsbDevice(USB Device) UsbDeviceEEPROM[(USB Device\nEEPROM)] State[(/var/lib/fwupd)] NVRAM[(Non-volatile\nRAM)] end subgraph Internet LVFS((LVFS\nInstance)) CDN(Fastly\nCDN) EFS[(Amazon\nEFS)] Postgres[(Amazon\nRDS)] Worker(Async Workers) end LVFS -- "fw.cab" --> Worker Worker -- "md.xml 🔒" --> EFS EFS <-- "fw.cab 🔒" --> Worker OEM -- "firmware.cab" --> LVFS LVFS -. "report.html" .-> OEM EFS <--> LVFS Postgres <--> Worker Postgres <--> LVFS fwupd <--> fwupdengine fwupdengine <-- "sqlite" --> Pending UsbDevice <-- "i²c" --> UsbDeviceEEPROM fwupdengine <-- "libusb" --> UsbDevice fwupdtool <---> fwupdengine fwupdengine <-- "ioctl()\nread()\nwrite()" --> Kernel fwupdengine -. "fwupdx64.efi" .-> ESP fwupdengine -- "fw.bin" --> ESP fwupdengine -- "fw.bin" --> Kernel fwupdengine -- "efivar" ---> Kernel Kernel -. "HSI attrs" .-> fwupdengine Kernel <-- "efivars" --> NVRAM fwupd-efi -- "fw.cap ðŸ”" ---> SPI fwupd-efi <-- "efivars" --> NVRAM ESP --> fwupd-efi fwupdmgr -- "md.[xml|jcat] 🔒ðŸš" --> fwupd fwupd -- "Devices\nHSI attrs\nReleases ðŸš" --> fwupdmgr fwupdmgr -- "fw.cab 🔒ðŸš" --> fwupd CDN -- "md.[xml|jcat] 🔒" --> fwupdmgr LVFS -- "md.[xml|jcat] 🔒" --> CDN LVFS -- "fw.cab 🔒" --> fwupdmgr LVFS -- "embargo.[xml|jcat] 🔒" --> fwupdmgr fwupdmgr -. "report.json" .-> LVFS fwupdmgr -. "report.json 🔒" .-> LVFS State <-- "fw.cab 🔒" --> fwupd passim -. "md.md|fw.cab 🔒\nmDNS with TLS" .-> fwupdmgr2 fwupd -. "md.md|fw.cab 🔒ðŸš" .-> passim User ~~~~ Privileged Internet ~~~~~ User Vendor ~~~~~ Internet ``` ### Key * ðŸš: D-Bus * 🔒: Signed using JCat file * ðŸ”: Signed using Platform Key fwupd-2.0.10/contrib/000077500000000000000000000000001501337203100143525ustar00rootroot00000000000000fwupd-2.0.10/contrib/PKGBUILD000066400000000000000000000041151501337203100154770ustar00rootroot00000000000000# Maintainer: Bruno Pagani # Maintainer: Filipe Laíns (FFY00) # Maintainer: Frederik Schwan # Contributor: Mirco Tischler pkgname=fwupd pkgver=dummy pkgrel=1 pkgdesc='A system daemon to allow session software to update firmware' arch=('i686' 'x86_64') url='https://github.com/fwupd/fwupd' license=(LGPL-2.1-or-later) depends=( bash curl flashrom fwupd-efi gcc-libs glib2 glibc gnutls gsettings-desktop-schemas hicolor-icon-theme json-glib libarchive libcbor libusb libjcat libmbim libmm-glib libqmi libxmlb passim polkit protobuf-c python shared-mime-info sqlite systemd-libs tpm2-tss xz zlib ) makedepends=( bash-completion gi-docgen gnu-efi-libs gobject-introspection libdrm meson noto-fonts noto-fonts-cjk python-cairo python-dbus python-gobject vala valgrind ) checkdepends=(umockdev) pkgver() { cd ${pkgname} VERSION=$(git describe | sed 's/-/.r/;s/-/./') [ -z $VERSION ] && VERSION=$(head meson.build | grep ' version:' | cut -d \' -f2) echo $VERSION } build() { cd ${pkgname} if [ -n "$CI" ]; then export CI="--wrap-mode=default -Db_coverage=true" fi arch-meson -D b_lto=false $CI ../build \ -Ddocs=enabled \ -Defi_binary=false \ -Dplugin_uefi_capsule_splash=false \ -Dbluez=disabled XDG_DATA_HOME=/tmp meson compile -C ../build } check() { CACHE_DIRECTORY=/tmp meson test -C build --print-errorlogs } package() { depends+=( libarchive.so libcbor.so libcurl.so libjson-glib-1.0.so libmm-glib.so libpassim.so libprotobuf-c.so libqmi-glib.so ) optdepends=( 'python-dbus: Firmware packaging tools' 'python-gobject: Firmware packaging tools' 'udisks2: UEFI firmware upgrade support' ) provides=(libfwupd.so) backup=( 'etc/fwupd/fwupd.conf' 'etc/fwupd/remotes.d/lvfs-testing.conf' 'etc/fwupd/remotes.d/lvfs.conf' 'etc/fwupd/remotes.d/vendor-directory.conf' ) meson install -C build --destdir "${pkgdir}" } fwupd-2.0.10/contrib/README.md000066400000000000000000000052071501337203100156350ustar00rootroot00000000000000# Distribution packages The relevant packaging necessary to generate *RPM*, *DEB* and *PKG* distribution packages is contained here. It is used regularly for continuous integration using [Travis CI](http://travis-ci.org). The generated packages can be used on a distribution such as Fedora, Debian, Ubuntu or Arch Linux. The build can be performed using Linux containers with [Docker](https://www.docker.com). ## RPM packages A Dockerfile for Fedora can be generated in `contrib`. To prepare the Docker container run this command: ```shell OS=fedora ./generate_docker.py build ``` To build the RPMs run this command (from the root of your git checkout): ```shell docker run --privileged -t -v `pwd`:/github/workspace fwupd-fedora ``` RPMs will be made available in your working directory when complete. To build additional RPM packages for Qubes OS (fwupd-qubes-dom0 and fwupd-qubes-vm) add `QUBES=true` environment variable: ```shell docker run --privileged -e QUBES=true -t -v `pwd`:/github/workspace fwupd-fedora ``` ## DEB packages A Dockerfile for Debian or Ubuntu can be generated in `contrib`. To prepare the Docker container run one of these commands: ```shell OS=debian-x86_64 ./generate_docker.py build OS=debian-i386 ./generate_docker.py build OS=ubuntu-x86_64 ./generate_docker.py build ``` To build the DEBs run one of these commands (from the root of your git checkout): ```shell docker run --privileged -t -v `pwd`:/github/workspace fwupd-debian-x86_64 docker run --privileged -t -v `pwd`:/github/workspace fwupd-debian-i386 docker run --privileged -t -v `pwd`:/github/workspace fwupd-ubuntu-x86_64 ``` DEBs will be made available in your working directory when complete. To build additional DEB package for Qubes OS (fwupd-qubes-vm-whonix) add `QUBES=true` environment variable: ```shell docker run --privileged -t -v `pwd`:/github/workspace fwupd-debian-x86_64-qubes ``` ## PKG packages A Dockerfile for Arch can be generated in `contrib`. To prepare the Docker container run this command: ```shell OS=arch ./generate_docker.py ``` To build the PKGs run this command (from the root of your git checkout): ```shell docker run -t -v `pwd`:/build fwupd-arch ``` PKGs will be made available in your working directory when complete. ## Additional packages Submissions for generating additional packages for other distribution mechanisms are also welcome. All builds should occur in Docker containers. Please feel free to submit the following: * Dockerfile for the container for your distro * Relevant technical packaging scripts (such as ebuilds, spec file etc) * A shell script that can be launched in the container to generate distribution packages fwupd-2.0.10/contrib/build-openbmc.sh000077500000000000000000000007461501337203100174400ustar00rootroot00000000000000#!/bin/sh rm -rf build-openbmc meson setup build-openbmc \ -Dauto_features=disabled \ -Ddocs=disabled \ -Dpolkit=disabled \ -Dbash_completion=false \ -Dfish_completion=false \ -Dfirmware-packager=false \ -Dhsi=disabled \ -Dman=false \ -Dmetainfo=false \ -Dtests=true \ -Dsystemd_root_prefix=/tmp \ -Dlibxmlb:gtkdoc=false \ $@ ninja install -C build-openbmc build-openbmc/src/fwupdtool get-devices --verbose test $? -eq 2 || exit 1 fwupd-2.0.10/contrib/build-venv.sh000077500000000000000000000017671501337203100167770ustar00rootroot00000000000000#!/bin/sh -e VENV=$(dirname $0)/.. BUILD=${VENV}/build DIST=${VENV}/dist EXTRA_ARGS="-Dlibxmlb:gtkdoc=false -Dsystemd=disabled" #build and install if [ -d /opt/homebrew/opt/libarchive/lib/pkgconfig ]; then EXTRA_ARGS="${EXTRA_ARGS} -Dpkg_config_path=/opt/homebrew/opt/libarchive/lib/pkgconfig" fi if [ ! -d ${BUILD} ] || ! [ -e ${BUILD}/build.ninja ]; then meson setup ${BUILD} --prefix=${DIST} ${EXTRA_ARGS} $@ fi ninja -C ${BUILD} install # check whether we have an existing fwupd EFI binary in the host system to use EFI_PREFIX=$(pkg-config fwupd-efi --variable=prefix 2>/dev/null || echo "/usr") EFI_DIR=libexec/fwupd/efi BINARIES=$(find "${EFI_PREFIX}/${EFI_DIR}" -name "*.efi*" -type f -print) if [ -n "${BINARIES}" ]; then mkdir -p ${DIST}/${EFI_DIR} for i in ${BINARIES}; do if [ -f "${DIST}/${EFI_DIR}/$(basename $i)" ]; then continue fi ln -s $i "${DIST}/${EFI_DIR}/$(basename $i)" done fi fwupd-2.0.10/contrib/build-windows.sh000077500000000000000000000050471501337203100175060ustar00rootroot00000000000000#!/bin/sh set -e root=$(pwd) export DESTDIR=${root}/dist build=$root/build-win32 mkdir -p "$build" && cd "$build" # install deps if [ ! -f /usr/share/mingw/toolchain-mingw64.meson ]; then ./contrib/ci/fwupd_setup_helpers.py -v mingw64 install-dependencies fi # try to keep this and ../contrib/ci/build_windows.sh in sync as much as makes sense meson setup .. \ --cross-file=/usr/share/mingw/toolchain-mingw64.meson \ --cross-file=../contrib/mingw64.cross \ --prefix=/ \ --sysconfdir="etc" \ --libexecdir="bin" \ --bindir="bin" \ -Dbuild=all \ -Ddbus_socket_address="tcp:host=localhost,port=1341" \ -Dman=false \ -Dfish_completion=false \ -Dbash_completion=false \ -Dfirmware-packager=false \ -Dmetainfo=false \ -Dlibxmlb:introspection=false \ -Dlibxmlb:gtkdoc=false \ -Dlibjcat:man=false \ -Dlibjcat:gpg=false \ -Dlibjcat:tests=false \ -Dlibjcat:introspection=false # run tests export WINEPATH="/usr/x86_64-w64-mingw32/sys-root/mingw/bin/;$build/libfwupd/;$build/libfwupdplugin/;$build/subprojects/libxmlb/src/;$build/subprojects/libjcat/libjcat/" ninja -C "$build" install ninja -C "$build" test MINGW32BINDIR=/usr/x86_64-w64-mingw32/sys-root/mingw/bin #disable motd for Windows sed -i 's,UpdateMotd=.*,UpdateMotd=false,' "$DESTDIR/etc/fwupd/fwupd.conf" # copy deps cp -f -v \ $MINGW32BINDIR/gspawn-win64-helper-console.exe \ $MINGW32BINDIR/gspawn-win64-helper.exe \ $MINGW32BINDIR/iconv.dll \ $MINGW32BINDIR/libarchive-13.dll \ $MINGW32BINDIR/libbrotlicommon.dll \ $MINGW32BINDIR/libbrotlidec.dll \ $MINGW32BINDIR/libbz2-1.dll \ $MINGW32BINDIR/libcrypto-3-x64.dll \ $MINGW32BINDIR/libcurl-4.dll \ $MINGW32BINDIR/libffi-*.dll \ $MINGW32BINDIR/libgcc_s_seh-1.dll \ $MINGW32BINDIR/libgio-2.0-0.dll \ $MINGW32BINDIR/libglib-2.0-0.dll \ $MINGW32BINDIR/libgmodule-2.0-0.dll \ $MINGW32BINDIR/libgmp-10.dll \ $MINGW32BINDIR/libgnutls-30.dll \ $MINGW32BINDIR/libgobject-2.0-0.dll \ $MINGW32BINDIR/libhogweed-*.dll \ $MINGW32BINDIR/libidn2-0.dll \ $MINGW32BINDIR/libintl-8.dll \ $MINGW32BINDIR/libjson-glib-1.0-0.dll \ $MINGW32BINDIR/liblzma-5.dll \ $MINGW32BINDIR/libnettle-*.dll \ $MINGW32BINDIR/libp11-kit-0.dll \ $MINGW32BINDIR/libpcre2-8-0.dll \ $MINGW32BINDIR/libsqlite3-0.dll \ $MINGW32BINDIR/libssh2-1.dll \ $MINGW32BINDIR/libssl-3-x64.dll \ $MINGW32BINDIR/libssp-0.dll \ $MINGW32BINDIR/libtasn1-6.dll \ $MINGW32BINDIR/libusb-1.0.dll \ $MINGW32BINDIR/libwinpthread-1.dll \ $MINGW32BINDIR/libxml2-2.dll \ $MINGW32BINDIR/libzstd.dll \ $MINGW32BINDIR/zlib1.dll \ "$DESTDIR/bin/" fwupd-2.0.10/contrib/cfu-inf-to-quirk.py000077500000000000000000000036561501337203100200410ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2017 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later # # pylint: disable=invalid-name,missing-docstring import sys def _convert_inf_to_quirk(fn: str) -> None: with open(fn, "rb") as f: lines = f.read().decode().split("\n") instance_id = None comment = None dict_convert = { "VersionsFeatureValueCapabilityUsageRangeMinimum": "CfuVersionGetReport", "OfferOutputValueCapabilityUsageRangeMinimum": "CfuOfferSetReport", "OfferInputValueCapabilityUsageRangeMinimum": "CfuOfferGetReport", "PayloadOutputValueCapabilityUsageRangeMinimum": "CfuContentSetReport", "PayloadInputValueCapabilityUsageRangeMinimum": "CfuContentGetReport", } is_cfu = False data = {"Plugin": "cfu"} for line in lines: line = line.split(";")[0] if line.find("HidCfu.NT.Services") != -1: is_cfu = True if line.find("FwUpdateFriendlyName") != -1: try: comment = line.split("=")[1] for token in ['"', "Firmware Update", "(TM)"]: comment = comment.replace(token, "") comment = comment.strip() except IndexError: pass if line.find("HID\\VID_") != -1: instance_id = line.split(",")[1].strip().upper() instance_id = instance_id.replace("HID\\", "USB\\") instance_id = "&".join(instance_id.split("&")[:2]) for inf_key, quirk_key in dict_convert.items(): if line.find(inf_key) != -1: data[quirk_key] = line.split(",")[4].strip() if is_cfu: if comment: print(f"# {comment}") print(f"[{instance_id}]") for key, value in data.items(): print(f"{key} = {value}") else: sys.exit("this is not a CFU firmware.inf") for fn in sys.argv[1:]: _convert_inf_to_quirk(fn) fwupd-2.0.10/contrib/check-ifd-firmware.py000077500000000000000000000064061501337203100203640ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2024 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later # # pylint: disable=invalid-name,missing-docstring,too-few-public-methods from typing import Dict import os import glob import sys import subprocess import json import argparse from collections import defaultdict from termcolor import colored def _scan_file(fn: str) -> Dict[str, int]: new_map: Dict[str, int] = defaultdict(int) try: print(f"loading {fn}…") args = [ "./src/fwupdtool", "firmware-parse", fn, "ifd-firmware", "--json", "--no-timestamp", ] p = subprocess.run(args, check=True, capture_output=True) except subprocess.CalledProcessError as e: print(f"{' '.join(args)}: {e}") else: for line in p.stdout.decode().split("\n"): new_map["Lines"] += 1 if line.find("gtype=") == -1: continue sections = line.split('"') if not sections[1].startswith("Fu"): continue new_map[sections[1]] += 1 for line in p.stderr.decode().split("\n"): if not line: continue new_map["WarningLines"] += 1 print(line) return new_map def _scan_dir(path: str, force_save: bool = False) -> bool: all_okay: bool = True needs_save: bool = False results: Dict[str, Dict[str, int]] = {} # support folders or paths if os.path.isdir(path): for fn in glob.glob(f"{path}/*.bin"): results[fn] = _scan_file(fn) else: results[path] = _scan_file(path) # go through each result print(f" {os.path.basename(sys.argv[0])}:") for fn, new_map in results.items(): try: with open(f"{fn}.json", "rb") as f: old_map = json.loads(f.read().decode()) except FileNotFoundError: old_map = {} print(f" {fn}") for key in sorted(set(list(old_map.keys()) + list(new_map.keys()))): cnt_old = old_map.get(key, 0) cnt_new = new_map.get(key, 0) if cnt_new > cnt_old: key_str: str = colored(key, "green") needs_save = True elif cnt_new < cnt_old: key_str = colored(key, "red") if key != "WarningLines": all_okay = False else: continue print(f" {key_str:36}: {cnt_old} -> {cnt_new}") # save new results if all better if (needs_save and all_okay) or force_save: for fn, new_map in results.items(): with open(f"{fn}.json", "wb") as f: f.write(json.dumps(new_map, sort_keys=True, indent=4).encode()) return all_okay if __name__ == "__main__": rc: int = 0 parser = argparse.ArgumentParser( prog="check-ifd-firmware", description="Check IFD firmware parsing" ) parser.add_argument("paths", nargs="+") parser.add_argument( "--force-save", action="store_true", help="always save the json reports" ) _args = parser.parse_args() for _path in _args.paths: if not _scan_dir(_path, force_save=_args.force_save): rc = 1 sys.exit(rc) fwupd-2.0.10/contrib/ci/000077500000000000000000000000001501337203100147455ustar00rootroot00000000000000fwupd-2.0.10/contrib/ci/Dockerfile-arch.in000066400000000000000000000005771501337203100202700ustar00rootroot00000000000000FROM archlinux:latest %%%OS%%% ENV LANG=en_US.UTF-8 ENV LC_ALL=en_US.UTF-8 ENV CI_NETWORK=true RUN echo fubar > /etc/machine-id RUN sed "s,#en_US.UTF-8,en_US.UTF-8," /etc/locale.gen -i RUN echo "LANG=en_US.UTF-8" > /etc/locale.conf RUN locale-gen RUN pacman -Syu --noconfirm archlinux-keyring %%%INSTALL_DEPENDENCIES_COMMAND%%% WORKDIR /github/workspace CMD ["./contrib/ci/arch.sh"] fwupd-2.0.10/contrib/ci/Dockerfile-debian.in000066400000000000000000000004231501337203100205630ustar00rootroot00000000000000FROM %%%ARCH_PREFIX%%%debian:testing %%%OS%%% ENV CI_NETWORK=true RUN echo fubar > /etc/machine-id %%%ARCH_SPECIFIC_COMMAND%%% %%%INSTALL_DEPENDENCIES_COMMAND%%% RUN apt install -yq --no-install-recommends python3-apt WORKDIR /github/workspace CMD ["./contrib/ci/debian.sh"] fwupd-2.0.10/contrib/ci/Dockerfile-fedora.in000066400000000000000000000004121501337203100205770ustar00rootroot00000000000000FROM fedora:41 %%%OS%%% ENV LANG=en_US.UTF-8 ENV LANGUAGE=en_US:en ENV LC_ALL=en_US.UTF-8 RUN echo fubar > /etc/machine-id RUN dnf -y update RUN echo fubar > /etc/machine-id %%%INSTALL_DEPENDENCIES_COMMAND%%% WORKDIR /github/workspace CMD ["./contrib/ci/fedora.sh"] fwupd-2.0.10/contrib/ci/Dockerfile-precommit.in000066400000000000000000000007571501337203100213520ustar00rootroot00000000000000FROM debian:testing ENV SKIP=no-commit-to-branch RUN apt update && apt install -yq --no-install-recommends \ git \ patch \ pre-commit \ shellcheck \ clang-format RUN apt install wget -yq RUN mkdir -p /tmp/repo && \ cd /tmp/repo && \ git init && \ wget https://raw.githubusercontent.com/fwupd/fwupd/main/.pre-commit-config.yaml RUN cd /tmp/repo && pre-commit run RUN rm -rf /tmp/repo WORKDIR /github/workspace CMD ["./contrib/ci/precommit.sh"] fwupd-2.0.10/contrib/ci/Dockerfile-ubuntu.in000066400000000000000000000004411501337203100206630ustar00rootroot00000000000000FROM ubuntu:rolling %%%OS%%% ENV CI_NETWORK=true ENV CC=clang ENV DEBIAN_FRONTEND=noninteractive RUN echo fubar > /etc/machine-id %%%ARCH_SPECIFIC_COMMAND%%% RUN apt update -qq && apt install -yq --no-install-recommends python3-apt WORKDIR /github/workspace CMD ["./contrib/ci/ubuntu.sh"] fwupd-2.0.10/contrib/ci/README.md000066400000000000000000000102561501337203100162300ustar00rootroot00000000000000# Continuous Integration By using CI, builds are exercised across a variety of environments attempting to maximize code coverage. For every commit or pull request 6 builds are performed: ## Fedora (x86_64) * A fully packaged RPM build with all plugins enabled * Compiled under gcc with AddressSanitizer * Tests with -Werror enabled * Tests with the built in local test suite for all plugins. * All packages are installed * An installed testing run with the "test" plugin and pulling from LVFS. * With modem manager disabled ## Debian testing (x86_64) * A fully packaged DEB build with all plugins enabled * Compiled under gcc * Tests with -Werror enabled * Tests with the built in local test suite for all plugins. * All packages are installed * An installed testing run with the "test" plugin and pulling from LVFS. * All packages are removed ## Debian testing (i386) * A fully packaged DEB build with all plugins enabled * Compiled under gcc * Tests with -Werror enabled * Tests with the built in local test suite for all plugins. * All packages are installed * An installed testing run with the "test" plugin and pulling from LVFS. * All packages are removed ## Ubuntu devel release (x86_64) * A fully packaged DEB build with all plugins enabled * Compiled under clang * Tests without -Werror enabled * Tests with the built in local test suite for all plugins. * All packages are installed * An installed testing run with the "test" plugin and pulling from LVFS. * All packages are removed ## Debian testing (cross compile s390x) * Not packaged * Tests for missing translation files * No redfish support * Compiled under gcc * Tests with -Werror enabled * Runs local test suite using qemu-user * Modem manager disabled ## Arch Linux (x86_64) * A fully packaged pkg build with all plugins enabled * Compiled under gcc * Tests with -Werror enabled * Compile with the deprecated USB plugin enabled * Tests with the built in local test suite for all plugins. * All packages are installed ## Flatpak * A flatpak bundle with all plugins enabled * Compiled under gcc with the org.gnome.Sdk/x86_64/3.28 runtime * Builds without the daemon, so only fwupdtool is available * No GPG, PKCS-7, GObjectIntrospection, systemd or ConsoleKit support * No tests ## Adding a new target Dockerfiles are generated dynamically by the python script ```generate_dockerfile.py```. The python script will recognize the environment variable `OS` to determine what target to generate a Dockerfile for. ### dependencies.xml Initially the python script will read in `dependencies.xml` to generate a dependency list for that target. The XML is organized by a top level element representing the dependencies needed for building fwupd. The child elements represent individual dependencies for all distributions. * This element has an attribute `id` that represents the most common package name used by distributions * This element has an attribute `type` that represents if the package is needed at build time or runtime. Each dependency then has a mapping to individual distributions (`distro`). * This element has an attribute `id` that represents the distribution. Each distribution will have `package` elements and `control` elements. `Package` elements represent the name of the package needed for the distribution. * An optional attribute `variant` represents one deviation of that distribution. For example building a specific architecture or with a different compiler. * If the `package` element is empty the `id` of the `dependency` element will be used. * `Control` elements represent specific requirements associated to a dependency. They will contain elements with individual details. * `version` elements represent a minimum version to be installed * `inclusive` elements represent an inclusive list of architectures to be installed on * `exclusive` elements represent an exclusive list of architectures to not be installed on For convenience there is also a helper script `./contrib/ci/fwupd_setup_helpers.p install-dependencies` that parses `dependencies.xml`. ### Dockerfile.in The `Dockerfile.in` file will be used as a template to build the container. No hardcoded dependencies should be put in this file. They should be stored in `dependencies.xml`. fwupd-2.0.10/contrib/ci/abidiff.suppr000066400000000000000000000005241501337203100174250ustar00rootroot00000000000000[suppress_type] type_kind = enum changed_enumerators = FWUPD_ERROR_LAST,FWUPD_GUID_FLAG_LAST,FWUPD_INSTALL_FLAG_LAST,FWUPD_KEYRING_KIND_LAST,FWUPD_REMOTE_KIND_LAST,FWUPD_SELF_SIGN_FLAG_LAST,FWUPD_STATUS_LAST,FWUPD_TRUST_FLAG_LAST,FWUPD_UPDATE_STATE_LAST,FWUPD_VERSION_FORMAT_LAST,FWUPD_CLIENT_DOWNLOAD_FLAG_LAST,FWUPD_FEATURE_FLAG_LAST fwupd-2.0.10/contrib/ci/arch-test.sh000077500000000000000000000031261501337203100172000ustar00rootroot00000000000000#!/bin/sh -ex pacman -Sy --noconfirm qt5-base gcovr python-flask swtpm tpm2-tools pacman -U --noconfirm dist/*.pkg.* # run custom redfish simulator plugins/redfish/tests/redfish.py & # run custom snapd simulator plugins/uefi-dbx/tests/snapd.py --datadir /usr/share/installed-tests/fwupd/tests & # run TPM simulator #swtpm socket --tpm2 --server port=2321 --ctrl type=tcp,port=2322 --flags not-need-init --tpmstate "dir=$PWD" & #trap 'kill $!' EXIT # extend a PCR0 value for test suite #sleep 2 #tpm2_startup -c #tpm2_pcrextend 0:sha1=f1d2d2f924e986ac86fdf7b36c94bcdf32beec15 # mark as disabled until it is fixed #export TPM_SERVER_RUNNING=1 #run the CI tests for Qt5 meson qt5-thread-test contrib/ci/qt5-thread-test --werror -Db_coverage=true ninja -C qt5-thread-test test #get the test firmware ./contrib/ci/get_test_firmware.sh /usr/share/installed-tests/fwupd/ # gnome-desktop-testing is missing, so manually run these tests export G_TEST_SRCDIR=/usr/share/installed-tests/fwupd G_TEST_BUILDDIR=/usr/share/installed-tests/fwupd mkdir -p /run/dbus /usr/bin/dbus-daemon --system /usr/lib/polkit-1/polkitd & sleep 5 fwupdtool enable-test-devices # tag test device for emulation before starting daemon fwupdtool emulation-tag 08d460be0f1f9f128413f816022a6439e0078018 NO_COLOR=1 G_DEBUG=fatal-criticals /usr/lib/fwupd/fwupd --verbose --no-timestamp >fwupd.txt 2>&1 & sleep 10 /usr/share/installed-tests/fwupd/fwupdmgr.sh /usr/share/installed-tests/fwupd/fwupd.sh /usr/share/installed-tests/fwupd/fwupdtool.sh /usr/share/installed-tests/fwupd/fwupdtool-efiboot.sh # generate coverage report ./contrib/ci/coverage.sh fwupd-2.0.10/contrib/ci/arch.sh000077500000000000000000000012711501337203100162220ustar00rootroot00000000000000#!/bin/bash set -e set -x shopt -s extglob #refresh package cache and update image pacman -Syu --noconfirm #install anything missing from the container ./contrib/ci/fwupd_setup_helpers.py install-dependencies -o arch # check that we got the bare minimum if [ ! -f /usr/bin/git ]; then echo "git not found, pacman possibly failed?" exit 1 fi # prepare the build tree rm -rf build mkdir build && pushd build cp ../contrib/PKGBUILD . mkdir -p src/fwupd && pushd src/fwupd cp -R ../../../!(build|dist) . popd chown nobody . -R # build the package sudo -E -u nobody PKGEXT='.pkg.tar' makepkg -e --noconfirm --nocheck # move the package to artifact dir mkdir -p ../dist mv *.pkg.* ../dist fwupd-2.0.10/contrib/ci/build_freebsd_package.sh000077500000000000000000000041031501337203100215460ustar00rootroot00000000000000#!/bin/sh GITHUB_SHA= GITHUB_REPOSITORY= GITHUB_REPOSITORY_OWNER= GITHUB_TAG= while [ -n "$1" ]; do case $1 in --GITHUB_SHA=*) GITHUB_SHA=${1#--GITHUB_SHA=} ;; --GITHUB_REPOSITORY=*) GITHUB_REPOSITORY=${1#--GITHUB_REPOSITORY=} ;; --GITHUB_REPOSITORY_OWNER=*) GITHUB_REPOSITORY_OWNER=${1#--GITHUB_REPOSITORY_OWNER=} ;; --GITHUB_TAG=*) GITHUB_TAG=${1#--GITHUB_TAG=} ;; *) echo "Command $1 unknown. exiting..." exit 1 ;; esac shift done if [ -z "$GITHUB_SHA" ] || [ -z "$GITHUB_REPOSITORY" ] || \ [ -z "$GITHUB_REPOSITORY_OWNER" ] || [ -z "$GITHUB_TAG" ]; then exit 1 fi # Include-file of libefivar port uses GCC-specific builtin function export CC=gcc set -xe mkdir -p /usr/local/etc/pkg/repos/ # Fix meson flag problem https://www.mail-archive.com/freebsd-ports@freebsd.org/msg86617.html cp /etc/pkg/FreeBSD.conf /usr/local/etc/pkg/repos/FreeBSD.conf # Use latest pkg repo instead of quarterly https://wiki.freebsd.org/Ports/QuarterlyBranch sed -i .old 's|url: "pkg+http://pkg.FreeBSD.org/${ABI}/quarterly"|url: "pkg+http://pkg.FreeBSD.org/${ABI}/latest"|' \ /usr/local/etc/pkg/repos/FreeBSD.conf pkg install -y meson efivar pkg upgrade -y meson cd /usr git clone https://github.com/3mdeb/freebsd-ports.git --depth 1 -b fwupd ports cd /usr/ports/sysutils/fwupd rm -rf ./* ls . cp -r ~/work/fwupd/fwupd/contrib/freebsd/* . ls . sed -i .old "s/GH_TAGNAME=.*$/GH_TAGNAME=\t${GITHUB_SHA}/" Makefile sed -i .old "s/GH_ACCOUNT=.*$/GH_ACCOUNT=\t${GITHUB_REPOSITORY_OWNER}/" Makefile sed -i .old "s/DISTVERSION=.*$/DISTVERSION=\t${GITHUB_TAG}/" Makefile make makesum make clean make # Generate current list of files in the pkg-plist make makeplist > pkg-plist sed -i "" "1d" pkg-plist sed -i "" "s/%%PORTDOCS%%%%DOCSDIR%%/%%DOCSDIR%%/g" pkg-plist # Build artifact make clean make package make install cp /usr/ports/sysutils/fwupd/work/pkg/fwupd*.pkg \ ~/work/fwupd/fwupd/fwupd-freebsd-${GITHUB_TAG}-${GITHUB_SHA}.pkg || exit 1 fwupd-2.0.10/contrib/ci/build_macos.sh000077500000000000000000000005211501337203100175630ustar00rootroot00000000000000#!/bin/bash set -e set -x export PKG_CONFIG_PATH="/opt/homebrew/Cellar/readline/8.2.13/lib/pkgconfig:$PKG_CONFIG_PATH" mkdir -p build-macos && cd build-macos meson setup .. \ -Dbuild=all \ -Ddbus_socket_address="unix:path=/var/run/fwupd.socket" \ -Dman=false \ -Dlibjcat:gpg=false \ -Dlibxmlb:gtkdoc=false \ $@ fwupd-2.0.10/contrib/ci/build_windows.sh000077500000000000000000000117401501337203100201600ustar00rootroot00000000000000#!/bin/sh set -e # if invoked outside of CI if [ "$CI" != "true" ]; then echo "Not running in CI, please manually configure Windows build" exit 1 fi # install deps if [ "$(id -u)" -eq 0 ]; then dnf install -y python3 dnf install -y xvfb-run ./contrib/ci/fwupd_setup_helpers.py --yes -o fedora -v mingw64 install-dependencies fi #prep export LC_ALL=C.UTF-8 root=$(pwd) export DESTDIR=${root}/dist build=$root/build-win32 rm -rf $DESTDIR $build mkdir -p $build $DESTDIR && cd $build # Hack for Fedora bug sed -i '/^Requires.private: termcap/d' /usr/x86_64-w64-mingw32/sys-root/mingw/lib/pkgconfig/readline.pc # run before using meson export WINEPREFIX=$build/.wine # For logitech bulk controller being disabled (-Dprotobuf=disabled): # See https://bugzilla.redhat.com/show_bug.cgi?id=1991749 # When fixed need to do the following to enable: # 1. need to add mingw64-protobuf mingw64-protobuf-tools to CI build deps # 2. add protoc = /path/to/protoc-c.exe in mingw64.cross # 3. Only enable when not a tagged release (Unsupported by Logitech) # try to keep this and ../contrib/build-windows.sh in sync as much as makes sense xvfb-run meson setup .. \ --cross-file=/usr/share/mingw/toolchain-mingw64.meson \ --cross-file=../contrib/mingw64.cross \ --prefix=/ \ --sysconfdir="etc" \ --libexecdir="bin" \ --bindir="bin" \ -Dbuild=all \ -Dman=false \ -Dtests=false \ -Dbuildtype=release \ -Ddbus_socket_address="tcp:host=localhost,port=1341" \ -Dfish_completion=false \ -Dbash_completion=false \ -Dfirmware-packager=false \ -Dmetainfo=false \ -Dpassim=disabled \ -Dlibjcat:man=false \ -Dlibjcat:gpg=false \ -Dlibjcat:tests=false \ -Dlibjcat:introspection=false \ $@ VERSION=$(meson introspect . --projectinfo | jq -r .version) ninja --verbose -C "$build" -v install #disable motd for Windows cd $root sed -i 's,UpdateMotd=.*,UpdateMotd=false,' "$DESTDIR/etc/fwupd/fwupd.conf" # create a setup binary CERTDIR=/etc/pki/tls/certs MINGW32BINDIR=/usr/x86_64-w64-mingw32/sys-root/mingw/bin # deps find $MINGW32BINDIR \ -name gspawn-win64-helper-console.exe \ -o -name gspawn-win64-helper.exe \ -o -name iconv.dll \ -o -name libarchive-13.dll \ -o -name libbrotlicommon.dll \ -o -name libbrotlidec.dll \ -o -name libbz2-1.dll \ -o -name libcrypto-3-x64.dll \ -o -name libcurl-4.dll \ -o -name "libffi-*.dll" \ -o -name libgcc_s_seh-1.dll \ -o -name libgio-2.0-0.dll \ -o -name libglib-2.0-0.dll \ -o -name libgmodule-2.0-0.dll \ -o -name libgmp-10.dll \ -o -name libgnutls-30.dll \ -o -name libgobject-2.0-0.dll \ -o -name "libhogweed-*.dll" \ -o -name libidn2-0.dll \ -o -name libintl-8.dll \ -o -name libjson-glib-1.0-0.dll \ -o -name liblzma-5.dll \ -o -name "libnettle-*.dll" \ -o -name libp11-kit-0.dll \ -o -name libpcre2-8-0.dll \ -o -name libsqlite3-0.dll \ -o -name libssh2-1.dll \ -o -name libssl-3-x64.dll \ -o -name libssp-0.dll \ -o -name libtermcap-0.dll \ -o -name libreadline8.dll \ -o -name libtasn1-6.dll \ -o -name libusb-1.0.dll \ -o -name libwinpthread-1.dll \ -o -name libxml2-2.dll \ -o -name libxmlb-2.dll \ -o -name libzstd.dll \ -o -name zlib1.dll \ | wixl-heat \ -p $MINGW32BINDIR/ \ --win64 \ --directory-ref BINDIR \ --var "var.MINGW32BINDIR" \ --component-group "CG.fwupd-deps" | \ tee $build/contrib/fwupd-deps.wxs echo $CERTDIR/ca-bundle.crt \ | wixl-heat \ -p $CERTDIR/ \ --win64 \ --directory-ref BINDIR \ --var "var.CERTDIR" \ --component-group "CG.fwupd-crts" | \ tee $build/contrib/fwupd-crts.wxs # no static libraries find "$DESTDIR/" -type f -name "*.a" -print0 | xargs rm -f # our files find "$DESTDIR" | \ wixl-heat \ -p "$DESTDIR/" \ -x include/ \ -x share/fwupd/device-tests/ \ -x share/tests/ \ -x share/man/ \ -x share/doc/ \ -x lib/pkgconfig/ \ --win64 \ --directory-ref INSTALLDIR \ --var "var.DESTDIR" \ --component-group "CG.fwupd-files" | \ tee "$build/contrib/fwupd-files.wxs" #add service install key sed -i "$build/contrib/fwupd-files.wxs" -f - << EOF s,fwupd.exe"/>,fwupd.exe"/>\\ , EOF MSI_FILENAME="$DESTDIR/setup/fwupd-$VERSION-setup-x86_64.msi" mkdir -p "$DESTDIR/setup" wixl -v \ "$build/contrib/fwupd.wxs" \ "$build/contrib/fwupd-crts.wxs" \ "$build/contrib/fwupd-deps.wxs" \ "$build/contrib/fwupd-files.wxs" \ -D CERTDIR=$CERTDIR \ -D MINGW32BINDIR=$MINGW32BINDIR \ -D Win64="yes" \ -D DESTDIR="$DESTDIR" \ -o "${MSI_FILENAME}" #generate news release echo "Generating news for version $VERSION" contrib/ci/generate_news.py $VERSION | tee -a $DESTDIR/news.txt # check the msi archive can be installed and removed (use "wine uninstaller" to do manually) wine msiexec /i "${MSI_FILENAME}" ls -R ${WINEPREFIX}/drive_c/Program\ Files/fwupd/ wine ${WINEPREFIX}/drive_c/Program\ Files/fwupd/bin/fwupdtool.exe get-plugins --json wine msiexec /x "${MSI_FILENAME}" fwupd-2.0.10/contrib/ci/check-abi000077500000000000000000000070501501337203100165030ustar00rootroot00000000000000#!/usr/bin/env python3 import argparse import contextlib import os import shutil import subprocess import sys def format_title(title): box = { "tl": "â•”", "tr": "â•—", "bl": "╚", "br": "â•", "h": "â•", "v": "â•‘", } hline = box["h"] * (len(title) + 2) return "\n".join( [ f"{box['tl']}{hline}{box['tr']}", f"{box['v']} {title} {box['v']}", f"{box['bl']}{hline}{box['br']}", ] ) def rm_rf(path): try: shutil.rmtree(path) except FileNotFoundError: pass def sanitize_path(name): return name.replace("/", "-") def get_current_revision(): revision = subprocess.check_output( ["git", "rev-parse", "--abbrev-ref", "HEAD"], encoding="utf-8" ).strip() if revision == "HEAD": # This is a detached HEAD, get the commit hash revision = ( subprocess.check_output(["git", "rev-parse", "HEAD"]) .strip() .decode("utf-8") ) return revision @contextlib.contextmanager def checkout_git_revision(revision): current_revision = get_current_revision() subprocess.check_call(["git", "checkout", "-q", revision]) try: yield finally: subprocess.check_call(["git", "checkout", "-q", current_revision]) def build_install(revision): build_dir = "_build" dest_dir = os.path.abspath(sanitize_path(revision)) print( format_title(f"# Building and installing {revision} in {dest_dir}"), end="\n\n", flush=True, ) with checkout_git_revision(revision): rm_rf(build_dir) rm_rf(revision) subprocess.check_call( [ "meson", build_dir, "--prefix=/usr", "--libdir=lib", "-Dauto_features=disabled", "-Db_coverage=false", "-Dtests=false", ] ) subprocess.check_call(["ninja", "-v", "-C", build_dir]) subprocess.check_call( ["ninja", "-v", "-C", build_dir, "install"], env={"DESTDIR": dest_dir} ) return dest_dir def compare(old_tree, new_tree): print(format_title(f"# Comparing the two ABIs"), end="\n\n", flush=True) old_headers = os.path.join(old_tree, "usr", "include") old_lib = os.path.join(old_tree, "usr", "lib", "libfwupd.so") new_headers = os.path.join(new_tree, "usr", "include") new_lib = os.path.join(new_tree, "usr", "lib", "libfwupd.so") subprocess.check_call( [ "abidiff", "--headers-dir1", old_headers, "--headers-dir2", new_headers, "--drop-private-types", "--suppressions", "contrib/ci/abidiff.suppr", "--fail-no-debug-info", "--no-added-syms", old_lib, new_lib, ] ) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("old", help="the previous revision, considered the reference") parser.add_argument("new", help="the new revision, to compare to the reference") args = parser.parse_args() if args.old == args.new: print("Let's not waste time comparing something to itself") sys.exit(0) old_tree = build_install(args.old) new_tree = build_install(args.new) try: compare(old_tree, new_tree) except subprocess.CalledProcessError: sys.exit(1) print(f"Hurray! {args.old} and {args.new} are ABI-compatible!") fwupd-2.0.10/contrib/ci/check-finalizers.py000077500000000000000000000032201501337203100205400ustar00rootroot00000000000000#!/usr/bin/env python3 # pylint: disable=invalid-name,missing-docstring,consider-using-f-string # pylint: disable=too-few-public-methods # # Copyright 2022 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later import glob import sys from typing import List class ReturnValidator: def __init__(self): self.warnings: List[str] = [] def parse(self, fn: str) -> None: with open(fn, "rb") as f: infunc = False has_parent_finalize = False for line in f.read().decode().split("\n"): # found the function, but ignore the prototype if line.find("_finalize(") != -1: if line.endswith(";"): continue infunc = True continue # got it if line.find("->finalize(") != -1: has_parent_finalize = True continue # finalize is done if infunc and line.startswith("}"): if not has_parent_finalize: self.warnings.append(f"{fn} did not have parent ->finalize()") break def test_files(): # test all C source files validator = ReturnValidator() for fn in ( glob.glob("libfwupd/*.c") + glob.glob("libfwupdplugin/*.c") + glob.glob("plugins/*/*.c") + glob.glob("src/*.c") ): validator.parse(fn) for warning in validator.warnings: print(warning) return 1 if validator.warnings else 0 if __name__ == "__main__": # all done! sys.exit(test_files()) fwupd-2.0.10/contrib/ci/check-headers.py000077500000000000000000000107161501337203100200150ustar00rootroot00000000000000#!/usr/bin/env python3 # pylint: disable=invalid-name,missing-module-docstring,missing-function-docstring # # Copyright 2021 Richard Hughes # Copyright 2021 Mario Limonciello # # SPDX-License-Identifier: LGPL-2.1-or-later import glob import sys import os from typing import List def __get_includes(fn: str) -> List[str]: includes: List[str] = [] with open(fn) as f: for line in f.read().split("\n"): if line.find("#include") == -1: continue if line.find("waive-pre-commit") > 0: continue for char in ["<", ">", '"']: line = line.replace(char, "") for char in ["\t"]: line = line.replace(char, " ") includes.append(line.split(" ")[-1]) return includes def test_files() -> int: rc: int = 0 lib_headers1 = glob.glob("libfwupd/*.h") lib_headers1.remove("libfwupd/fwupd.h") lib_headers2 = glob.glob("libfwupdplugin/*.h") lib_headers2.remove("libfwupdplugin/fwupdplugin.h") toplevel_headers = ["libfwupd/fwupd.h", "libfwupdplugin/fwupdplugin.h"] toplevel_headers_nopath = [os.path.basename(fn) for fn in toplevel_headers] lib_headers = lib_headers1 + lib_headers2 lib_headers_nopath = [os.path.basename(fn) for fn in lib_headers] # test all C and H files for fn in ( glob.glob("libfwupd/*.[c|h]") + glob.glob("libfwupdplugin/*.[c|h]") + glob.glob("plugins/*/*.[c|h]") + glob.glob("src/*.[c|h]") ): # we do not care if fn in [ "libfwupd/fwupd-context-test.c", "libfwupd/fwupd-thread-test.c", "libfwupdplugin/fu-fuzzer-main.c", ]: continue includes = __get_includes(fn) if ( fn.startswith("plugins") and not fn.endswith("self-test.c") and not fn.endswith("tool.c") ): for include in includes: # check for using private header use in plugins if include.endswith("private.h"): print(f"{fn} uses private header {include}") rc = 1 continue # check for referring to anything but top level header if include in lib_headers or include in lib_headers_nopath: print( f"{fn} contains {include}, should only use top level includes" ) rc = 1 # check for double top level headers for toplevel_header in toplevel_headers: toplevel_fn = os.path.basename(toplevel_header) toplevel_includes = __get_includes(toplevel_header) toplevel_includes_nopath = [ os.path.basename(fn) for fn in toplevel_includes ] # we do not need both toplevel headers if set(toplevel_headers_nopath).issubset(set(includes)): print(f"{fn} contains both {', '.join(toplevel_headers_nopath)}") # toplevel not listed if toplevel_fn not in includes: continue # includes toplevel and *also* something listed in the toplevel for include in includes: if include in toplevel_includes or include in toplevel_includes_nopath: print(f"{fn} contains {toplevel_fn} but also includes {include}") rc = 1 # check for missing config.h if fn.endswith(".c") and "config.h" not in includes: print(f"{fn} does not include config.h") rc = 1 # check for one header implying the other implied_headers = { "fu-common.h": ["xmlb.h"], "fwupdplugin.h": [ "gio/gio.h", "glib.h", "glib-object.h", "xmlb.h", "fwupd.h", ] + lib_headers1, "gio/gio.h": ["glib.h", "glib-object.h"], "glib-object.h": ["glib.h"], "json-glib/json-glib.h": ["glib.h", "glib-object.h"], "xmlb.h": ["gio/gio.h"], } for key, values in implied_headers.items(): for value in values: if key in includes and value in includes: print(f"{fn} contains {value} which is implied by {key}") rc = 1 return rc if __name__ == "__main__": # all done! sys.exit(test_files()) fwupd-2.0.10/contrib/ci/check-license.py000077500000000000000000000033571501337203100200270ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2021 Richard Hughes # Copyright 2021 Mario Limonciello # # SPDX-License-Identifier: LGPL-2.1-or-later import glob import os import sys import fnmatch def test_files() -> int: rc: int = 0 build_dirs = [os.path.dirname(cf) for cf in glob.glob("**/config.h")] for fn in ( glob.glob("libfwupd/*.[c|h|py|rs]") + glob.glob("libfwupdplugin/.[c|h|py|rs]") + glob.glob("plugins/*/.[c|h|py|rs]") + glob.glob("src/.[c|h|py|rs]") ): if fn.endswith("check-license.py"): continue lic: str = "" cprts: list[str] = [] lines: list[str] = [] with open(fn) as f: for line in f.read().split("\n"): lines.append(line) if len(lines) < 2: continue for line in lines: if line.find("SPDX-License-Identifier:") != -1: lic = line.split(":")[1] if line.find("Copyright") != -1: cprts.append(line.strip()) if not lic: print(f"{fn} does not specify a license") rc = 1 continue if not cprts: print(f"{fn} does not specify any copyright") rc = 1 continue if "LGPL-2.1-or-later" not in lic: print(f"{fn} does not contain LGPL-2.1-or-later ({lic})") rc = 1 continue for cprt in cprts: for word in ["(C)", "(c)", "©", " "]: if cprt.find(word) != -1: print(f"{fn} should not contain {word} in the string {cprt}") rc = 1 return rc if __name__ == "__main__": # all done! sys.exit(test_files()) fwupd-2.0.10/contrib/ci/check-null-false-returns.py000077500000000000000000000174101501337203100221420ustar00rootroot00000000000000#!/usr/bin/env python3 # pylint: disable=invalid-name,missing-docstring,too-many-branches # pylint: disable=too-many-statements,too-many-return-statements,too-few-public-methods # # Copyright 2021 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later import glob import os import sys from typing import List def _tokenize(line: str) -> List[str]: # remove whitespace line = line.strip() line = line.replace("\t", "") line = line.replace(";", "") # find value line = line.replace(" ", "|") line = line.replace(",", "|") line = line.replace(")", "|") line = line.replace("(", "|") # return empty tokens tokens = [] for token in line.rsplit("|"): if token: tokens.append(token) return tokens class ReturnValidator: def __init__(self): self.warnings: List[str] = [] # internal state self._fn = None self._line_num = None self._value = None self._nret = None self._rvif = None self._line = None @property def _tokens(self) -> List[str]: return _tokenize(self._line) @property def _value_relaxed(self) -> str: if self._value in ["0x0", "0x00", "0x0000"]: return "0" if self._value in ["0xffffffff"]: return "G_MAXUINT32" if self._value in ["0xffff"]: return "G_MAXUINT16" if self._value in ["0xff"]: return "G_MAXUINT8" if self._value in ["G_SOURCE_REMOVE"]: return "FALSE" if self._value in ["G_SOURCE_CONTINUE"]: return "TRUE" return self._value def _test_rvif(self) -> None: # parse "g_return_val_if_fail (SOMETHING (foo), NULL);" self._value = self._tokens[-1] # enumerated enum, so ignore if self._value.find("_") != -1: return # is invalid if self._rvif and self._value_relaxed not in self._rvif: self.warnings.append( "{} line {} got {}, expected {}".format( self._fn, self._line_num, self._value, ", ".join(self._rvif) ) ) def _test_return(self) -> None: # parse "return 0x0;" self._value = self._tokens[-1] # is invalid if self._nret and self._value_relaxed in self._nret: self.warnings.append( "{} line {} got {}, which is not valid -- expected {}".format( self._fn, self._line_num, self._value, "|".join(self._rvif) ) ) def parse(self, fn: str) -> None: self._fn = fn with open(fn) as f: self._rvif = None self._nret = None self._line_num = 0 for line in f.readlines(): self._line_num += 1 line = line.replace("LIBUSB_CALL", "") line = line.rstrip() if not line: continue if line.endswith("\\"): continue if line.endswith("&&"): continue self._line = line idx = line.find("g_return_val_if_fail") if idx != -1: self._test_rvif() continue idx = line.find("return") if idx != -1: # continue if len(self._tokens) == 2: self._test_return() continue # not a function header if line[0] in ["#", " ", "\t", "{", "}", "/"]: continue # label if line.endswith(":"): continue # remove prefixes if line.startswith("static"): line = line[7:] if line.startswith("inline"): line = line[7:] # a pointer if line.endswith("*"): self._rvif = ["NULL"] self._nret = ["FALSE"] continue # not a leading line if line.find(" ") != -1: continue # a type we know if line in ["void"]: self._rvif = [] self._nret = [] continue if line in ["gpointer"]: self._rvif = ["NULL"] self._nret = ["FALSE"] continue if line in ["gboolean"]: self._rvif = ["TRUE", "FALSE"] self._nret = ["NULL", "0"] continue if line in ["guint32"]: self._rvif = ["0", "G_MAXUINT32"] self._nret = ["NULL", "TRUE", "FALSE"] continue if line in ["GQuark", "GType"]: self._rvif = ["0"] self._nret = ["NULL", "0", "TRUE", "FALSE"] continue if line in ["guint64"]: self._rvif = ["0", "G_MAXUINT64"] self._nret = ["NULL", "TRUE", "FALSE"] continue if line in ["guint16"]: self._rvif = ["0", "G_MAXUINT16"] self._nret = ["NULL", "TRUE", "FALSE"] continue if line in ["guint8"]: self._rvif = ["0", "G_MAXUINT8"] self._nret = ["NULL", "TRUE", "FALSE"] continue if line in ["gint64"]: self._rvif = ["0", "-1", "G_MAXINT64"] self._nret = ["NULL", "TRUE", "FALSE"] continue if line in ["gint32"]: self._rvif = ["0", "-1", "G_MAXINT32"] self._nret = ["NULL", "TRUE", "FALSE"] continue if line in ["gint16"]: self._rvif = ["0", "-1", "G_MAXINT16"] self._nret = ["NULL", "TRUE", "FALSE"] continue if line in ["gint8"]: self._rvif = ["0", "-1", "G_MAXINT8"] self._nret = ["NULL", "TRUE", "FALSE"] continue if line in ["gint", "int"]: self._rvif = ["0", "-1", "G_MAXINT"] self._nret = ["NULL", "TRUE", "FALSE"] continue if line in ["guint"]: self._rvif = ["0", "G_MAXUINT"] self._nret = ["NULL", "TRUE", "FALSE"] continue if line in ["gulong"]: self._rvif = ["0", "G_MAXLONG"] self._nret = ["NULL", "TRUE", "FALSE"] continue if line in ["gsize", "size_t"]: self._rvif = ["0", "G_MAXSIZE"] self._nret = ["NULL", "TRUE", "FALSE"] continue if line in ["gssize", "ssize_t"]: self._rvif = ["0", "-1", "G_MAXSSIZE"] self._nret = ["NULL", "TRUE", "FALSE"] continue # print('unknown return type {}'.format(line)) self._rvif = None self._nret = None def test_files(): # test all C source files validator = ReturnValidator() for fn in ( glob.glob("libfwupd/*.c") + glob.glob("libfwupdplugin/*.c") + glob.glob("plugins/*/*.c") + glob.glob("src/*.c") ): validator.parse(fn) for warning in validator.warnings: print(warning) return 1 if validator.warnings else 0 if __name__ == "__main__": # all done! sys.exit(test_files()) fwupd-2.0.10/contrib/ci/check-potfiles.py000077500000000000000000000022761501337203100202310ustar00rootroot00000000000000#!/usr/bin/env python3 # pylint: disable=invalid-name,missing-docstring,consider-using-f-string # pylint: disable=too-few-public-methods # # Copyright 2022 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later import glob import sys from typing import List def test_files(): # compare with POTFILES.in with open("po/POTFILES.in", "rb") as f: potfiles_fns: List[str] = f.read().decode().split("\n") for fn in sorted( glob.glob("src/*.c") + glob.glob("plugins/*/*.c") + glob.glob("policy/*.policy.in") + glob.glob("data/*/*.xml") + glob.glob("libfwupdplugin/tests/bios-attrs/*/*.txt") ): if ( fn.startswith("dist/") or fn.startswith("subprojects/") or fn.startswith("build/") ): continue with open(fn, "rb") as f: blob = f.read().decode() if blob.find('_("') != -1 or blob.find("TRANSLATORS") != -1: if fn not in potfiles_fns: print(f"{fn} is missing from po/POTFILES.in") return 1 # success return 0 if __name__ == "__main__": # all done! sys.exit(test_files()) fwupd-2.0.10/contrib/ci/check-quirks.py000077500000000000000000000051641501337203100177210ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2021 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later # # pylint: disable=missing-module-docstring,missing-function-docstring,invalid-name,too-many-branches import glob import sys from typing import List def gtype_is_valid(gtype: str) -> bool: if not gtype: return False for token in ["Plugin", "Firmware", "Backend", "Image"]: if gtype.endswith(token): return False return True def test_files() -> int: rc: int = 0 gtypes: List[str] = [] # find the possible GTypes for fn in glob.glob("libfwupdplugin/fu-*-device.c") + glob.glob("plugins/*/fu-*.c"): with open(fn) as f: for line in f.read().split("\n"): gtype: str = "" if line.startswith("G_DEFINE_TYPE("): gtype = line[14:].split(",")[0] elif line.startswith("G_DEFINE_TYPE_WITH_PRIVATE("): gtype = line[27:].split(",")[0] elif line.startswith("G_DEFINE_TYPE_WITH_CODE("): gtype = line[24:].split(",")[0] if gtype_is_valid(gtype): gtypes.append(gtype) # check each quirk file for fn in glob.glob("data/*.quirk") + glob.glob("plugins/*/*.quirk"): with open(fn) as f: for line in f.read().split("\n"): if line.startswith(" ") or line.endswith(" "): print(f"{fn} has leading or trailing whitespace: {line}") rc = 1 continue if not line or line.startswith("#"): continue if line.startswith("["): if not line.endswith("]"): print(f"{fn} has invalid section header: {line}") rc = 1 continue else: sections = line.split(" = ") if len(sections) != 2: print(f"{fn} has invalid line: {line}") rc = 1 continue if sections[0] == "GType" and sections[1] not in gtypes: print(f"{fn} has invalid GType: {line}") rc = 1 continue for section in sections: if section.strip() != section: print(f"{fn} has invalid spacing: {line}") rc = 1 break return rc if __name__ == "__main__": # all done! sys.exit(test_files()) fwupd-2.0.10/contrib/ci/check-source.py000077500000000000000000000420011501337203100176720ustar00rootroot00000000000000#!/usr/bin/env python3 # pylint: disable=invalid-name,missing-module-docstring,missing-function-docstring # # Copyright 2023 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later import glob import sys import os from typing import List, Optional def _find_func_name(line: str) -> Optional[str]: # these are not functions for prefix in ["\t", " ", "typedef", "__attribute__", "G_DEFINE_"]: if line.startswith(prefix): return None # ignore prototypes if line.endswith(";"): return None # strip to function name then test for validity idx: int = line.find("(") if idx == -1: return None func_name = line[:idx] if func_name.find(" ") != -1: return None # success! return func_name class SourceFailure: def __init__(self, fn=None, linecnt=None, message=None, nocheck=None): self.fn: Optional[str] = fn self.linecnt: Optional[int] = linecnt self.message: Optional[str] = message self.nocheck: Optional[str] = nocheck class Checker: MAX_FUNCTION_LINES: int = 400 MAX_FUNCTION_SWITCH: int = 2 def __init__(self): self.failures: List[SourceFailure] = [] self._current_fn: Optional[str] = None self._current_linecnt: Optional[int] = None self._current_nocheck: Optional[str] = None def add_failure(self, message=None): self.failures.append( SourceFailure( fn=self._current_fn, linecnt=self._current_linecnt, message=message, nocheck=self._current_nocheck, ) ) def _test_line_function_names_private(self, func_name: str) -> None: valid_prefixes = ["_fwupd_", "_fu_", "_g_", "_xb_"] for prefix in valid_prefixes: if func_name.startswith(prefix): return self.add_failure( f"invalid function name {func_name} should have {'|'.join(valid_prefixes)} prefix" ) def _test_line_function_names_valid(self, func_name: str) -> None: # sanity check if not self._current_fn: return # ignore headers if self._current_fn.endswith(".h"): return # ignore all self tests FIXME: 335 failures if self._current_fn.endswith("-test.c"): return # namespacing is strange here if self._current_fn.endswith("fwupd-enums.c"): return # doh if func_name in ["main", "fu_plugin_init_vfuncs"]: return # this is stuff that should move to GLib if func_name.startswith("_g_"): return # remove some suffixes we do not care about prefix = os.path.basename(self._current_fn).split(".")[0].replace("-", "_") for suffix in [ "_common", "_darwin", "_freebsd", "_helper", "_impl", "_linux", "_sync", "_windows", ]: if prefix.endswith(suffix): prefix = prefix[: -len(suffix)] # allowed truncations valid_prefixes = [] valid_prefixes.append(prefix) for key, value in { "fu_crc": "fu_misr", # FIXME: split out to fu-misr.[c|h] "fu_darwin_efivars": "fu_efivars", "fu_dbus_daemon": "fu_daemon", "fu_dbxtool": "fu_util", "fu_freebsd_efivars": "fu_efivars", "fu_linux_efivars": "fu_efivars", "fu_logitech_hidpp_hidpp": "fu_logitech_hidpp", "fu_logitech_hidpp_hidpp_msg": "fu_logitech_hidpp", "fu_self_test": "fu_test", "fu_string": "fu_str,fu_utf", "fu_tool": "fu_util", "fu_windows_efivars": "fu_efivars", }.items(): if prefix == key: valid_prefixes.extend(value.split(",")) for prefix in valid_prefixes: if func_name.startswith(prefix): return self.add_failure( f"invalid function name {func_name} should have {'|'.join(valid_prefixes)} prefix" ) def _test_line_function_names(self, line: str) -> None: # empty line if not line: return # skip! self._current_nocheck = "nocheck:name" if line.find(self._current_nocheck) != -1: return # parse func_name: str = _find_func_name(line) if not func_name: return if func_name.startswith("_"): self._test_line_function_names_private(func_name) return self._test_line_function_names_valid(func_name) def _test_line_enums(self, line: str) -> None: # skip! self._current_nocheck = "nocheck:prefix" if line.find(self._current_nocheck) != -1: return # needs Fu prefix enum_name = None valid_prefixes = ["Fwupd", "Fu"] if line.startswith("enum ") and line.endswith("{"): enum_name = line[5:-2] elif line.startswith("typedef enum ") and line.endswith("{"): enum_name = line[13:-2] if not enum_name: return for prefix in valid_prefixes: if enum_name.startswith(prefix): return self.add_failure( f"invalid enum name {enum_name} should have {'|'.join(valid_prefixes)} prefix" ) def _test_line_debug_fns(self, line: str) -> None: # no console output expected self._current_nocheck = "nocheck:print" if line.find(self._current_nocheck) != -1: return if self._current_fn and os.path.basename(self._current_fn) in [ "fu-console.c", "fu-daemon.c", "fu-dbxtool.c", "fu-debug.c", "fu-fuzzer-main.c", "fu-gcab.c", "fu-main.c", "fu-main-windows.c", "fu-self-test.c", "fu-tpm-eventlog.c", "fwupd-self-test.c", ]: return for token, msg in { "g_print(": "Use g_debug() instead", "g_printerr(": "Use g_debug() instead", }.items(): if line.find(token) != -1: self.add_failure(f"contains blocked token {token}: {msg}") def _test_line_blocked_fns(self, line: str) -> None: self._current_nocheck = "nocheck:blocked" if line.find(self._current_nocheck) != -1: return for token, msg in { "cbor_get_uint8(": "Use cbor_get_int() instead", "cbor_get_uint16(": "Use cbor_get_int() instead", "cbor_get_uint32(": "Use cbor_get_int() instead", "g_error(": "Use GError instead", "g_byte_array_free_to_bytes(": "Use g_bytes_new() instead", "g_ascii_strtoull(": "Use fu_strtoull() instead", "g_ascii_strtoll(": "Use fu_strtoll() instead", "g_random_int_range(": "Use a predicatable token instead", "g_assert(": "Use g_set_error() or g_return_val_if_fail() instead", "HIDIOCSFEATURE": "Use fu_hidraw_device_set_feature() instead", "HIDIOCGFEATURE": "Use fu_hidraw_device_get_feature() instead", "|= 1 <<": "Use FU_BIT_SET() instead", "|= 1u <<": "Use FU_BIT_SET() instead", "|= 1ull <<": "Use FU_BIT_SET() instead", "|= (1 <<": "Use FU_BIT_SET() instead", "|= (1u <<": "Use FU_BIT_SET() instead", "|= (1ull <<": "Use FU_BIT_SET() instead", "&= ~(1 <<": "Use FU_BIT_CLEAR() instead", "&= ~(1u <<": "Use FU_BIT_CLEAR() instead", "&= ~(1ull <<": "Use FU_BIT_CLEAR() instead", "__attribute__((packed))": "Use rustgen instead", "memcpy(": "Use fu_memcpy_safe or rustgen instead", "GUINT16_FROM_BE(": "Use fu_memread_uint16_safe() or rustgen instead", "GUINT16_FROM_LE(": "Use fu_memread_uint16_safe() or rustgen instead", "GUINT16_TO_BE(": "Use fu_memwrite_uint16_safe() or rustgen instead", "GUINT16_TO_LE(": "Use fu_memwrite_uint16_safe() or rustgen instead", "GUINT32_FROM_BE(": "Use fu_memread_uint32_safe() or rustgen instead", "GUINT32_FROM_LE(": "Use fu_memread_uint32_safe() or rustgen instead", "GUINT32_TO_BE(": "Use fu_memwrite_uint32_safe() or rustgen instead", "GUINT32_TO_LE(": "Use fu_memwrite_uint32_safe() or rustgen instead", "GUINT64_FROM_BE(": "Use fu_memread_uint64_safe() or rustgen instead", "GUINT64_FROM_LE(": "Use fu_memread_uint64_safe() or rustgen instead", "GUINT64_TO_BE(": "Use fu_memwrite_uint64_safe() or rustgen instead", "GUINT64_TO_LE(": "Use fu_memwrite_uint64_safe() or rustgen instead", " ioctl(": "Use fu_udev_device_ioctl() instead", }.items(): if line.find(token) != -1: self.add_failure(f"contains blocked token {token}: {msg}") def _test_lines_gerror(self, lines: List[str]) -> None: self._current_nocheck = "nocheck:error" linecnt_g_set_error: int = 0 for linecnt, line in enumerate(lines): if line.find(self._current_nocheck) != -1: continue self._current_linecnt = linecnt + 1 # do not use G_IO_ERROR internally if line.find("g_set_error") != -1: linecnt_g_set_error = linecnt if linecnt - linecnt_g_set_error < 5: for error_domain in ["G_IO_ERROR", "G_FILE_ERROR"]: if line.find(error_domain) != -1: self.add_failure("uses g_set_error() without using FWUPD_ERROR") break def _test_lines_function_length(self, lines: List[str]) -> None: self._current_nocheck = "nocheck:lines" func_n_switch: int = 0 func_begin: int = 0 func_name: Optional[str] = None for linecnt, line in enumerate(lines): if line.find(self._current_nocheck) != -1: func_begin = 0 continue if line.find("switch (") != -1: func_n_switch += 1 if line == "{": func_begin = linecnt continue if func_begin > 0 and line == "}": self._current_linecnt = func_begin if func_n_switch > self.MAX_FUNCTION_SWITCH: self.add_failure( f"{func_name} has too many switches ({func_n_switch}), limit of {self.MAX_FUNCTION_SWITCH}" ) if linecnt - func_begin > self.MAX_FUNCTION_LINES: if func_name: self.add_failure( f"{func_name} is too long, was {linecnt - func_begin} of {self.MAX_FUNCTION_LINES}" ) else: self.add_failure( f"function is too long, was {linecnt - func_begin} of {self.MAX_FUNCTION_LINES}" ) if func_name and linecnt - func_begin < 3: if func_name.endswith("_finalize"): self.add_failure(f"{func_name} is redundant and can be removed") func_begin = 0 func_n_switch = 0 func_name = None continue # is a function? func_name_tmp: Optional[str] = _find_func_name(line) if func_name_tmp: func_name = func_name_tmp def _test_lines_firmware_convert_version(self, lines: List[str]) -> None: self._current_nocheck = "nocheck:set-version" if self._current_fn and os.path.basename(self._current_fn) in [ "fu-firmware.c", "fu-firmware.h", ]: return # contains fu_firmware_set_version_raw() _set_version_raw: bool = False for line in lines: if line.find("fu_firmware_set_version_raw(") != -1: _set_version_raw = True break if not _set_version_raw: return # also contains fu_firmware_set_version() for linecnt, line in enumerate(lines): if line.find(self._current_nocheck) != -1: continue self._current_linecnt = linecnt + 1 if line.find("fu_firmware_set_version(") != -1: self.add_failure( "Use FuFirmwareClass->convert_version rather than fu_firmware_set_version()" ) def _test_lines_device_convert_version(self, lines: List[str]) -> None: self._current_nocheck = "nocheck:set-version" if self._current_fn and os.path.basename(self._current_fn) in [ "fu-device.c", "fu-device.h", "fu-self-test.c", ]: return # contains fu_firmware_set_version_raw() _set_version_raw: bool = False for line in lines: if line.find("fu_device_set_version_raw(") != -1: _set_version_raw = True break if not _set_version_raw: return # also contains fu_firmware_set_version() for linecnt, line in enumerate(lines): if line.find(self._current_nocheck) != -1: continue self._current_linecnt = linecnt + 1 if line.find("fu_device_set_version(") != -1: self.add_failure( "Use FuDeviceClass->convert_version rather than fu_device_set_version()" ) def _test_lines_depth(self, lines: List[str]) -> None: # check depth self._current_nocheck = "nocheck:depth" depth: int = 0 for linecnt, line in enumerate(lines): if line.find(self._current_nocheck) != -1: continue self._current_linecnt = linecnt + 1 for char in line: if char == "{": depth += 1 if depth > 5: self.add_failure("is nested too deep") success = False continue if char == "}": if depth == 0: self.add_failure("has unequal nesting") success = False continue depth -= 1 # sanity check self._current_linecnt = None if depth != 0: self.add_failure("nesting was weird") success = False def _test_lines(self, lines: List[str]) -> None: lines_nocheck: List[str] = [] # tests we can do line by line for linecnt, line in enumerate(lines): self._current_linecnt = linecnt + 1 # test for blocked functions self._test_line_blocked_fns(line) # test for debug lines self._test_line_debug_fns(line) # test for function names self._test_line_function_names(line) # test for invalid enum names self._test_line_enums(line) # using FUWPD_ERROR domains self._test_lines_gerror(lines) # not nesting too deep self._test_lines_depth(lines) # functions too long self._test_lines_function_length(lines) # should use FuFirmwareClass->convert_version self._test_lines_firmware_convert_version(lines) # should use FuDeviceClass->convert_version self._test_lines_device_convert_version(lines) def test_file(self, fn: str) -> None: self._current_fn = fn with open(fn, "rb") as f: try: self._test_lines(f.read().decode().split("\n")) except UnicodeDecodeError as e: print(f"failed to read {fn}: {e}") def test_files() -> int: # test all C and H files rc: int = 0 checker = Checker() # use any file specified in argv, falling back to scanning the entire tree fns: List[str] = [] if len(sys.argv) > 1: for fn in sys.argv[1:]: try: ext: str = fn.rsplit(".", maxsplit=1)[1] except IndexError: continue if ext in ["c", "h"]: fns.append(fn) else: fns.extend(glob.glob("libfwupd/*.[c|h]")) fns.extend(glob.glob("libfwupdplugin/*.[c|h]")) fns.extend(glob.glob("plugins/*/*.[c|h]")) fns.extend(glob.glob("src/*.[c|h]")) for fn in fns: if os.path.basename(fn) == "check-source.py": continue checker.test_file(fn) # show issues for failure in checker.failures: line: str = "" if failure.fn: line += failure.fn if failure.linecnt: line += f":{failure.linecnt}" line += f": {failure.message}" if failure.nocheck: line += f" -- use a {failure.nocheck} comment to ignore" print(line) if checker.failures: print(f"{len(checker.failures)} failures!") return 1 if checker.failures else 0 if __name__ == "__main__": # all done! sys.exit(test_files()) fwupd-2.0.10/contrib/ci/check-unused.py000077500000000000000000000040501501337203100176770ustar00rootroot00000000000000#!/usr/bin/env python3 # pylint: disable=invalid-name,missing-module-docstring,missing-function-docstring # # Copyright 2023 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later import glob import fnmatch import os import sys import subprocess def test_files() -> int: fns = sys.argv[1:] if not fns: build = os.environ["BUILD"] if "BUILD" in os.environ else "" fns.append(os.path.join(".", build, "plugins")) fns.append(os.path.join(".", build, "src")) data = [] # find all .o files for fn in fns: for fn in glob.glob(f"{fn}/**/*.o", recursive=True): print(f"Analyzing {fn}...") p = subprocess.run(["nm", fn], check=True, capture_output=True) # parse data for line in p.stdout.decode().split("\n"): line = line.rstrip() if len(line) == 0: continue if line.endswith(".o:"): continue t = line[17:18] if t == "b" or t == "t" or t == "r" or t == "d" or t == "a": continue symb = line[19:] data.append((t, symb)) # collect all the symbols defined symbs = [] for t, symb in data: if t != "T": continue if symb.endswith("_get_type"): continue if fnmatch.fnmatch(symb, "fu_struct_*_set_*"): continue if fnmatch.fnmatch(symb, "fu_struct_*_get_*"): continue if symb.find("__proto__") != -1: continue if symb in ["main", "fu_plugin_init_vfuncs"]: continue if symb not in symbs: symbs.append(symb) # remove the ones used for t, symb in data: if t != "U": continue if symb in symbs: symbs.remove(symb) # display symbs.sort() for symb in symbs: print("Unused: ", symb) return 1 if symbs else 0 if __name__ == "__main__": # all done! sys.exit(test_files()) fwupd-2.0.10/contrib/ci/check_missing_translations.sh000077500000000000000000000001251501337203100227110ustar00rootroot00000000000000#!/bin/sh set -e cd po intltool-update -m if [ -f missing ]; then exit 1 fi fwupd-2.0.10/contrib/ci/coverage.sh000077500000000000000000000013471501337203100171040ustar00rootroot00000000000000#!/bin/sh -e # if invoked outside of CI if [ "$CI" != "true" ]; then echo "Not running in CI" exit 1 fi gcovr -x \ --filter build/libfwupd \ --filter build/libfwupdplugin \ --filter build/plugins \ --filter build/src \ --exclude build/libfwupd/fwupd-context-test.c \ --exclude build/libfwupd/fwupd-thread-test.c \ --exclude-lines-by-pattern '^.*(G_OBJECT_WARN_INVALID|G_DEFINE_TYPE|JSON_NODE_HOLDS_OBJECT|g_autoptr|g_critical|g_warning|g_assert_cmpfloat_with_epsilon|g_assert_cmpint|g_assert_cmpstr|g_assert_cmpuint|g_assert_error|g_assert_false|g_assert_no_error|g_assert_nonnull|g_assert_not_reached|g_assert_null|g_assert_true|g_return_if_fail|g_return_val_if_fail).*$' \ -o coverage.xml sed "s,build/,,g" coverage.xml -i fwupd-2.0.10/contrib/ci/debian-i386-test.sh000077700000000000000000000000001501337203100236512debian-x86_64-test.shustar00rootroot00000000000000fwupd-2.0.10/contrib/ci/debian-x86_64-test.sh000077500000000000000000000012011501337203100204310ustar00rootroot00000000000000#!/bin/bash -e # Set up fatal-criticals systemd override SYSTEMD_OVERRIDE="/etc/systemd/system/fwupd.service.d" mkdir -p ${SYSTEMD_OVERRIDE} cp contrib/fwupd-systemd-fatal-criticals.conf ${SYSTEMD_OVERRIDE}/override.conf # install apt update apt install -y gcovr ./dist/*.deb fwupdtool enable-test-devices fwupdtool emulation-tag 08d460be0f1f9f128413f816022a6439e0078018 # run tests ./contrib/ci/get_test_firmware.sh /usr/share/installed-tests/fwupd/ service dbus restart gnome-desktop-testing-runner --timeout=600 fwupd # generate coverage report ./contrib/ci/coverage.sh # cleanup apt purge -y fwupd fwupd-doc libfwupd3 libfwupd-dev fwupd-2.0.10/contrib/ci/debian.sh000077500000000000000000000040111501337203100165220ustar00rootroot00000000000000#!/bin/bash set -e set -x export QUBES_OPTION= #although it's debian, we don't build packages if [ "$OS" = "debian-s390x" ]; then ./contrib/ci/debian_s390x.sh exit 0 fi # Set Qubes Os vars if -Dqubes=true is parameter if [ "$QUBES" = "true" ]; then export QUBES_OPTION='-Dqubes=true' fi #prepare export DEBFULLNAME="CI Builder" export DEBEMAIL="ci@travis-ci.org" VERSION=`git describe | sed 's/-/+r/;s/-/+/'` [ -z $VERSION ] && VERSION=`head meson.build | grep ' version:' | cut -d \' -f2` rm -rf build/ mkdir -p build shopt -s extglob cp -R !(build|dist|venv) build/ pushd build mv contrib/debian . sed s/quilt/native/ debian/source/format -i #generate control file ./contrib/ci/generate_debian.py #check if we have all deps available apt update -qq && apt install python3-apt -y ./contrib/ci/fwupd_setup_helpers.py install-dependencies -o debian --yes || true dpkg-checkbuilddeps #disable unit tests if fwupd is already installed (may cause problems) if [ -x /usr/lib/fwupd/fwupd ]; then export DEB_BUILD_OPTIONS=nocheck fi #build the package EDITOR=/bin/true dch --create --package fwupd -v $VERSION "CI Build" debuild --no-lintian --preserve-envvar CI --preserve-envvar CC \ --preserve-envvar QUBES_OPTION #check lintian output #suppress tags that are side effects of building in docker this way lintian ../*changes \ -IE \ --pedantic \ --no-tag-display-limit \ --suppress-tags missing-build-dependency-for-dh-addon \ --suppress-tags library-not-linked-against-libc \ --suppress-tags bad-distribution-in-changes-file \ --suppress-tags debian-watch-file-in-native-package \ --suppress-tags source-nmu-has-incorrect-version-number \ --suppress-tags no-symbols-control-file \ --suppress-tags gzip-file-is-not-multi-arch-same-safe \ --suppress-tags missing-dependency-on-libc \ --suppress-tags arch-dependent-file-not-in-arch-specific-directory \ --suppress-tags package-installs-ieee-data \ --allow-root #place built packages in dist outside docker mkdir -p ../dist PACKAGES=$(find .. -type f -name "*deb") cp $PACKAGES ../dist fwupd-2.0.10/contrib/ci/debian_s390x.sh000077500000000000000000000012231501337203100174720ustar00rootroot00000000000000#!/bin/sh set -e set -x export LC_ALL=C.UTF-8 #evaluate using Debian's build flags eval "$(dpkg-buildflags --export=sh)" #filter out -Bsymbolic-functions LDFLAGS=$(dpkg-buildflags --get LDFLAGS | sed "s/-Wl,-Bsymbolic-functions\s//") export LDFLAGS rm -rf build mkdir -p build cp contrib/ci/s390x_cross.txt build/ cd build meson setup .. \ --cross-file s390x_cross.txt \ --werror \ -Dplugin_flashrom=disabled \ -Dplugin_modem_manager=disabled \ -Dintrospection=false \ -Dlibxmlb:introspection=false \ -Dlibxmlb:gtkdoc=false \ -Dman=false ninja -v ninja test -v cd .. #test for missing translation files ./contrib/ci/check_missing_translations.sh fwupd-2.0.10/contrib/ci/dependencies.xml000066400000000000000000001464721501337203100201330ustar00rootroot00000000000000 bash-completion bash-completion-devel bash-completion-devel amd64 arm64 armhf i386 clang-tools-extra mingw64-gcc cairo-devel cairo-devel cairo-devel libcairo-dev:s390x cairo-gobject-devel cairo-gobject-devel cairo-gobject-devel libcairo-gobject2:s390x libdrm libdrm-devel libdrm-devel libdrm-devel json-glib json-glib-devel json-glib-devel json-glib-devel mingw64-json-glib json-glib (>= 1.1.1) libjson-glib-dev:s390x (>= 1.1.1) libftdi libftdi-devel libftdi-devel libftdi-devel pciutils pciutils-devel pciutils-devel pciutils-devel noto-fonts google-noto-sans-cjk-ttc-fonts google-noto-sans-cjk-ttc-fonts google-noto-sans-cjk-ttc-fonts (>= 12) (>= 12) freetype freetype freetype libfreetype-dev:s390x readline-devel readline-devel readline-devel mingw64-readline fwupd-efi ia64 ia64 flashrom flashrom-devel flashrom-devel ia64 ia64 gettext mingw64-gettext (>= 0.19.8.1) (>= 0.19.8.1) gnu-efi-libs amd64 arm64 armhf i386 gnu-efi gnu-efi amd64 arm64 armhf i386 gnu-efi gnu-efi gnu-efi glib2-devel glib2-devel glib2-devel glib2-devel mingw64-glib2 gsettings-desktop-schemas (>= 2.45.8) libglib2.0-dev:s390x (>= 2.45.8) glibc-langpack-en glibc-langpack-en gobject-introspection-devel gobject-introspection-devel gobject-introspection-devel gnutls-devel gnutls-devel gnutls-devel mingw64-gnutls gnutls libgnutls28-dev:s390x libgnutls28-dev gnutls-utils gnutls-utils gnutls-utils libxmlb libxmlb-devel libxmlb-devel mingw64-libxmlb (>= 0.1.13) libxmlb-dev:s390x (>= 0.1.13) libjcat libjcat-devel libjcat-devel libjcat-devel util-linux-libs libblkid-devel libblkid-devel libblkid-devel xz-devel xz-devel xz-devel mingw64-zstd libzstd-dev:s390x libarchive-devel libarchive-devel libarchive-devel mingw64-libarchive libarchive-dev:s390x libarchive libcbor libcbor-devel libcbor-devel libcbor-dev:s390x libusb libusb1-devel libusb1-devel libusb1-devel mingw64-libusbx libusb (>= 1.0.9) libusb-1.0-0-dev:s390x (>= 0.3.3) libicu-dev:s390x libidn2-0-dev:s390x curl libcurl-devel libcurl-devel libcurl-devel mingw64-curl curl libcurl4-gnutls-dev:s390x passim-devel passim-devel passim kernel-headers kernel-headers amd64 arm64 armhf i386 amd64 arm64 armhf i386 pango-devel pango-devel pango-devel mingw64-pkg-config polkit polkit polkit polkit ModemManager-glib-devel ModemManager-glib-devel ModemManager-glib-devel modemmanager libmm-glib-dev:s390x libqmi-devel libqmi-devel libqmi libqmi-glib-dev:s390x libmbim-devel libmbim-devel libmbim libmbim-glib-dev:s390x libqrtr-glib-dev:s390x polkit polkit-devel polkit-devel polkit-devel libpolkit-gobject-1-dev:s390x python-pip virtualenv python-dbusmock python-cairo python3-cairo python3-cairo python3-cairo python-pefile python-gobject python-packaging qemu-user sqlite-devel sqlite-devel sqlite-devel sqlite-devel mingw64-sqlite libsqlite3-dev:s390x sqlite (>= 249) systemd-dev systemd-dev systemd-dev (>= 249) systemd-dev systemd-dev systemd-devel systemd-devel systemd-devel libsystemd-dev:s390x umockdev-devel umockdev-devel vala vala vala vala vala valgrind-devel valgrind-devel valgrind-devel valgrind-if-available valgrind-if-available valgrind-if-available valgrind-if-available valgrind-if-available tpm2-tss tpm2-tss-devel tpm2-tss-devel tpm2-tss-devel libtss2-dev:s390x amd64 arm64 armhf i386 ShellCheck protobuf-c protobuf-c-devel protobuf-c-devel protobuf-c-devel protobuf-c python-virtualenv wine msitools fwupd-2.0.10/contrib/ci/fedora-test.sh000077500000000000000000000014521501337203100175230ustar00rootroot00000000000000#!/bin/sh -e # workaround dnf bug export FORCE_COLUMNS=100 #install RPM packages dnf install -y dist/*.rpm dnf install -y gcovr fwupdtool enable-test-devices # set up enough PolicyKit and D-Bus to run the daemon mkdir -p /run/dbus mkdir -p /var ln -s /var/run /run dbus-daemon --system --fork /usr/lib/polkit-1/polkitd & sleep 5 # Enable testing capturing emulation data fwupdtool emulation-tag 08d460be0f1f9f128413f816022a6439e0078018 # run the daemon startup to check it can start /usr/libexec/fwupd/fwupd --immediate-exit --verbose # run the installed tests whilst the daemon debugging NO_COLOR=1 G_DEBUG=fatal-criticals /usr/libexec/fwupd/fwupd --verbose --no-timestamp >fwupd.txt 2>&1 & sleep 10 gnome-desktop-testing-runner --timeout=600 fwupd # generate coverage report ./contrib/ci/coverage.sh fwupd-2.0.10/contrib/ci/fedora.sh000077500000000000000000000016361501337203100165520ustar00rootroot00000000000000#!/bin/bash set -e set -x #get any missing deps from the container ./contrib/ci/fwupd_setup_helpers.py install-dependencies --yes -o fedora # disable the safe directory feature git config --global safe.directory "*" if [ "$QUBES" = "true" ]; then QUBES_MACRO=(--define "qubes_packages 1") fi # do the full-fat build RPMVERSION=${VERSION//-/.} mkdir -p $HOME/rpmbuild/SOURCES/ mv fwupd-$VERSION.tar.xz $HOME/rpmbuild/SOURCES/ #generate a spec file mkdir -p build sed "s,#VERSION#,$RPMVERSION,; s,#BUILD#,1,; s,#LONGDATE#,`date '+%a %b %d %Y'`,; s,#ALPHATAG#,alpha,; s,enable_dummy 0,enable_dummy 1,; s,Source0.*,Source0:\tfwupd-$VERSION.tar.xz," \ contrib/fwupd.spec.in > build/fwupd.spec if [ -n "$CI" ]; then sed -i "s,enable_ci 0,enable_ci 1,;" build/fwupd.spec fi #build RPM packages rpmbuild -ba "${QUBES_MACRO[@]}" build/fwupd.spec mkdir -p dist cp $HOME/rpmbuild/RPMS/*/*.rpm dist fwupd-2.0.10/contrib/ci/flatpak.py000077500000000000000000000061221501337203100167450ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2018 Mario Limonciello # # SPDX-License-Identifier: LGPL-2.1-or-later import subprocess import os import json import shutil def prepare(target): # clone the flatpak json cmd = ["git", "submodule", "update", "--remote", "contrib/flatpak"] subprocess.run(cmd, check=True) # clone the submodules for that cmd = ["git", "submodule", "update", "--init", "--remote", "shared-modules/"] subprocess.run(cmd, cwd="contrib/flatpak", check=True) # parse json if os.path.isdir("build"): shutil.rmtree("build") data = {} with open("contrib/flatpak/org.freedesktop.fwupd.json") as rfd: data = json.load(rfd, strict=False) platform = f"runtime/{data['runtime']}/x86_64/{data['runtime-version']}" sdk = f"runtime/{data['sdk']}/x86_64/{data['runtime-version']}" num_modules = len(data["modules"]) # update to build from main data["branch"] = "main" for index in range(0, num_modules): module = data["modules"][index] if type(module) != dict or "name" not in module: continue name = module["name"] if "fwupd" not in name: continue data["modules"][index]["sources"][0].pop("url") data["modules"][index]["sources"][0].pop("sha256") data["modules"][index]["sources"][0]["type"] = "dir" data["modules"][index]["sources"][0]["skip"] = [".git"] data["modules"][index]["sources"][0]["path"] = ".." # write json os.mkdir("build") with open(target, "w") as wfd: json.dump(data, wfd, indent=4) os.symlink("../contrib/flatpak/shared-modules", "build/shared-modules") # install the runtimes (parsed from json!) repo = "flathub" repo_url = "https://dl.flathub.org/repo/flathub.flatpakrepo" print("Installing dependencies") cmd = ["flatpak", "remote-add", "--if-not-exists", repo, repo_url] subprocess.run(cmd, check=True) cmd = ["flatpak", "install", "--assumeyes", repo, sdk] subprocess.run(cmd, check=True) cmd = ["flatpak", "install", "--assumeyes", repo, platform] subprocess.run(cmd, check=True) def build(target): cmd = [ "flatpak-builder", "--repo=repo", "--force-clean", "--disable-rofiles-fuse", "build-dir", target, ] subprocess.run(cmd, check=True) cmd = ["flatpak", "build-bundle", "repo", "fwupd.flatpak", "org.freedesktop.fwupd"] subprocess.run(cmd, check=True) if __name__ == "__main__": t = os.path.join("build", "org.freedesktop.fwupd.json") prepare(t) build(t) # to run from the builddir: # sudo flatpak-builder --run build-dir org.freedesktop.fwupd.json /app/libexec/fwupd/fwupdtool get-devices # install the single file bundle # flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo # flatpak install fwupd.flatpak # to run a shell in the same environment that flatpak sees: # flatpak run --command=sh --devel org.freedesktop.fwupd # to run fwupdtool as root: # sudo -i flatpak run org.freedesktop.fwupd --verbose get-devices fwupd-2.0.10/contrib/ci/fwupd_setup_helpers.py000077500000000000000000000205761501337203100214230ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2017 Dell, Inc. # Copyright 2020 Intel, Inc. # Copyright 2021 Mario Limonciello # # SPDX-License-Identifier: LGPL-2.1-or-later # import os import sys import argparse WARNING = "\033[93m" ENDC = "\033[0m" # Minimum version of markdown required MINIMUM_MARKDOWN = (3, 2, 0) def get_possible_profiles(): return ["fedora", "centos", "debian", "ubuntu", "arch", "darwin"] def detect_profile(): if os.path.exists("/Library/Apple"): return "darwin" try: import distro target = distro.id() if target not in get_possible_profiles(): target = distro.like() return target except ModuleNotFoundError: pass # fallback try: with open("/etc/os-release", "rb") as f: for line in f.read().decode().split("\n"): if line.startswith("ID="): target = line[3:].replace('"', "") if target == "rhel": return "centos" return target except FileNotFoundError: pass # failed return "" def pip_install_package(debug, name): import subprocess cmd = ["python3", "-m", "pip", "install", "--upgrade", name] if debug: print(cmd) subprocess.call(cmd) def test_jinja2(debug): try: import jinja2 except ModuleNotFoundError: print("python3-jinja2 must be installed/upgraded") pip_install_package(debug, "jinja2") def test_markdown(debug): try: import markdown new_enough = markdown.__version_info__ >= MINIMUM_MARKDOWN except ModuleNotFoundError: new_enough = False if not new_enough: print("python3-markdown must be installed/upgraded") pip_install_package(debug, "markdown") def get_minimum_meson_version(): import re directory = os.path.join(os.path.dirname(sys.argv[0]), "..", "..") with open(os.path.join(directory, "meson.build")) as f: for line in f: if "meson_version" in line: return re.search(r"(\d+\.\d+\.\d+)", line).group(1) def test_meson(debug): from importlib.metadata import version, PackageNotFoundError minimum = get_minimum_meson_version() try: new_enough = version("meson") >= minimum except PackageNotFoundError: import subprocess try: ver = ( subprocess.check_output(["meson", "--version"]).strip().decode("utf-8") ) new_enough = ver >= minimum except FileNotFoundError: new_enough = False if not new_enough: print("meson must be installed/upgraded") pip_install_package(debug, "meson") def parse_dependencies(OS, variant, add_control): import xml.etree.ElementTree as etree deps = [] dep = "" directory = os.path.dirname(sys.argv[0]) tree = etree.parse(os.path.join(directory, "dependencies.xml")) root = tree.getroot() for child in root: if "id" not in child.attrib: continue for distro in child: if "id" not in distro.attrib: continue if distro.attrib["id"] != OS: continue control = "" if add_control: inclusive = [] exclusive = [] if not distro.findall("control"): continue for control_parent in distro.findall("control"): for obj in control_parent.findall("inclusive"): inclusive.append(obj.text) for obj in control_parent.findall("exclusive"): exclusive.append(obj.text) if inclusive or exclusive: inclusive = " ".join(inclusive).strip() exclusive = " !".join(exclusive).strip() if exclusive: exclusive = f"!{exclusive}" control = f" [{inclusive}{exclusive}]" for package in distro.findall("package"): if variant and "variant" in package.attrib: if package.attrib["variant"] != variant: continue if package.text: dep = package.text else: dep = child.attrib["id"] dep += control if dep: deps.append(dep) return deps def _validate_deps(profile: str, deps): validated = deps if profile == "debian" or profile == "ubuntu": try: from apt import cache cache = cache.Cache() for pkg in deps: if not cache.has_key(pkg) and not cache.is_virtual_package(pkg): print( f"{WARNING}WARNING:{ENDC} ignoring unavailable package %s" % pkg ) validated.remove(pkg) except ModuleNotFoundError: print( f"{WARNING}WARNING:{ENDC} Unable to validate package dependency list without python3-apt" ) return validated def get_build_dependencies(profile: str, variant: str): parsed = parse_dependencies(profile, variant, False) return _validate_deps(profile, parsed) def _get_installer_cmd(profile: str, yes: bool): if profile == "darwin": return ["brew", "install"] if profile in ["debian", "ubuntu"]: installer = ["apt", "install"] elif profile in ["fedora", "centos"]: installer = ["dnf", "install"] elif profile == "arch": installer = ["pacman", "-Syu", "--noconfirm", "--needed"] else: print("unable to detect OS profile, use --os= to specify") print(f"\tsupported profiles: {get_possible_profiles()}") sys.exit(1) if os.geteuid() != 0: installer.insert(0, "sudo") if yes: installer += ["-y"] return installer def install_packages(profile: str, variant: str, yes: bool, debugging: bool, packages): import subprocess if packages == "build-dependencies": packages = get_build_dependencies(profile, variant) installer = _get_installer_cmd(profile, yes) installer += packages if debugging: print(installer) subprocess.check_call(installer) if __name__ == "__main__": command = None # compat mode for old training documentation if "generate_dependencies.py" in sys.argv[0]: command = "get-dependencies" parser = argparse.ArgumentParser() if not command: parser.add_argument( "command", choices=[ "get-dependencies", "test-markdown", "test-jinja2", "test-meson", "detect-profile", "install-dependencies", "install-pip", ], help="command to run", ) parser.add_argument( "-o", "--os", default=detect_profile(), choices=get_possible_profiles(), help="calculate dependencies for OS profile", ) parser.add_argument( "-v", "--variant", help="optional machine variant for the OS profile" ) parser.add_argument( "-y", "--yes", action="store_true", help="Don't prompt to install" ) parser.add_argument( "-d", "--debug", action="store_true", help="Display all launched commands" ) args = parser.parse_args() # fall back in all cases if not args.variant: args.variant = os.uname().machine if not command: command = args.command # command to run if command == "test-markdown": test_markdown(args.debug) elif command == "test-jinja2": test_jinja2(args.debug) elif command == "test-meson": test_meson(args.debug) elif command == "detect-profile": print(detect_profile()) elif command == "get-dependencies": dependencies = get_build_dependencies(args.os, args.variant) print(*dependencies, sep="\n") elif command == "install-dependencies": install_packages( args.os, args.variant, args.yes, args.debug, "build-dependencies" ) elif command == "install-pip": if args.os == "darwin": install_packages(args.os, args.variant, args.yes, args.debug, ["python"]) else: install_packages( args.os, args.variant, args.yes, args.debug, ["python3-pip"] ) fwupd-2.0.10/contrib/ci/generate_debian.py000077500000000000000000000060631501337203100204230ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2017 Dell, Inc. # # SPDX-License-Identifier: LGPL-2.1-or-later # import os import sys from fwupd_setup_helpers import parse_dependencies def parse_control_dependencies(): QUBES = os.getenv("QUBES") return parse_dependencies("debian", "x86_64", True), QUBES def update_debian_control(target): control_in = os.path.join(target, "control.in") control_out = os.path.join(target, "control") if not os.path.exists(control_in): print(f"Missing file {control_in}") sys.exit(1) with open(control_in) as rfd: lines = rfd.readlines() deps, QUBES = parse_control_dependencies() deps.sort() if QUBES: lines += "\n" control_qubes_in = os.path.join(target, "control.qubes.in") with open(control_qubes_in) as rfd: lines += rfd.readlines() with open(control_out, "w") as wfd: for line in lines: if "Build-Depends:" in line and "%%%DYNAMIC%%%" in line: wfd.write("Build-Depends:\n") for i in range(0, len(deps)): wfd.write(f"\t{deps[i]},\n") elif "fwupd-qubes-vm-whonix" in line and not QUBES: break else: wfd.write(line) def update_debian_copyright(directory): copyright_in = os.path.join(directory, "copyright.in") copyright_out = os.path.join(directory, "copyright") if not os.path.exists(copyright_in): print(f"Missing file {copyright_in}") sys.exit(1) # Assume all files are remaining LGPL-2.1-or-later copyrights = [] for root, dirs, files in os.walk("."): for file in files: target = os.path.join(root, file) # skip translations and license file if target.startswith("./po/") or file == "COPYING": continue try: with open(target) as rfd: # read about the first few lines of the file only lines = rfd.readlines(220) except UnicodeDecodeError: continue except FileNotFoundError: continue for line in lines: if "Copyright " in line: parts = line.split("Copyright")[ 1 ].strip() # split out the copyright header partition = parts.partition(" ")[2] # remove the year string copyrights += [f"{partition}"] copyrights = "\n\t ".join(sorted(set(copyrights))) with open(copyright_in) as rfd: lines = rfd.readlines() with open(copyright_out, "w") as wfd: for line in lines: if line.startswith("%%%DYNAMIC%%%"): wfd.write("Files: *\n") wfd.write(f"Copyright: {copyrights}\n") wfd.write("License: LGPL-2.1-or-later\n") wfd.write("\n") else: wfd.write(line) directory = os.path.join(os.getcwd(), "debian") update_debian_control(directory) update_debian_copyright(directory) fwupd-2.0.10/contrib/ci/generate_dependencies.py000077700000000000000000000000001501337203100262532fwupd_setup_helpers.pyustar00rootroot00000000000000fwupd-2.0.10/contrib/ci/generate_docker.py000077500000000000000000000056631501337203100204550ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2017 Dell, Inc. # # SPDX-License-Identifier: LGPL-2.1-or-later # import os import subprocess import sys import shutil from fwupd_setup_helpers import parse_dependencies def get_container_cmd(): """return docker or podman as container manager""" if shutil.which("docker"): return "docker" if shutil.which("podman"): return "podman" directory = os.path.dirname(sys.argv[0]) TARGET = os.getenv("OS") if TARGET is None: print("Missing OS environment variable") sys.exit(1) OS = TARGET SUBOS = "" split = TARGET.split("-") if len(split) >= 2: OS = split[0] SUBOS = split[1] deps = parse_dependencies(OS, SUBOS, False) f = os.path.join(directory, f"Dockerfile-{OS}.in") if not os.path.exists(f): print(f"Missing input file {f} for {OS}") sys.exit(1) with open(f) as rfd: lines = rfd.readlines() with open("Dockerfile", "w") as wfd: for line in lines: if line.startswith("FROM %%%ARCH_PREFIX%%%"): if (OS == "debian" or OS == "ubuntu") and SUBOS == "i386": replace = SUBOS + "/" else: replace = "" wfd.write(line.replace("%%%ARCH_PREFIX%%%", replace)) elif line == "%%%INSTALL_DEPENDENCIES_COMMAND%%%\n": if OS == "fedora": wfd.write("RUN dnf --enablerepo=updates-testing -y install \\\n") elif OS == "centos": wfd.write("RUN yum -y install \\\n") elif OS == "debian" or OS == "ubuntu": wfd.write("RUN apt update -qq && \\\n") wfd.write( "\tDEBIAN_FRONTEND=noninteractive apt install -yq --no-install-recommends\\\n" ) elif OS == "arch": wfd.write("RUN pacman -Syu --noconfirm --needed\\\n") for i in range(0, len(deps)): if i < len(deps) - 1: wfd.write(f"\t{deps[i]} \\\n") else: wfd.write(f"\t{deps[i]} || true\n") elif line == "%%%ARCH_SPECIFIC_COMMAND%%%\n": if OS == "debian" and SUBOS == "s390x": # add sources wfd.write( 'RUN cat /etc/apt/sources.list | sed "s/deb/deb-src/" >> /etc/apt/sources.list\n' ) # add new architecture wfd.write(f"RUN dpkg --add-architecture {SUBOS}\n") elif line == "%%%OS%%%\n": wfd.write(f"ENV OS={TARGET}\n") else: wfd.write(line) wfd.flush() if len(sys.argv) == 2 and sys.argv[1] == "build": cmd = get_container_cmd() args = [cmd, "build", "-t", f"fwupd-{TARGET}"] if "http_proxy" in os.environ: args += [f"--build-arg=http_proxy={os.environ['http_proxy']}"] if "https_proxy" in os.environ: args += [f"--build-arg=https_proxy={os.environ['https_proxy']}"] args += ["-f", "./Dockerfile", "."] subprocess.check_call(args) fwupd-2.0.10/contrib/ci/generate_news.py000077500000000000000000000014171501337203100201530ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2020 Dell Inc. # # SPDX-License-Identifier: LGPL-2.1-or-later # import os import argparse import xml.etree.ElementTree as etree if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("version", help="Generate news for release") args = parser.parse_args() tree = etree.parse(os.path.join("data", "org.freedesktop.fwupd.metainfo.xml")) root = tree.getroot() for release in root.iter("release"): if "version" not in release.attrib: continue if release.attrib["version"] != args.version: continue description = release.find("description") result = etree.tostring(description, encoding="unicode", method="text") print(result.strip()) fwupd-2.0.10/contrib/ci/get_test_firmware.sh000077500000000000000000000007641501337203100210250ustar00rootroot00000000000000#!/bin/sh if [ "$CI_NETWORK" = "true" ]; then #clone fwupd-test-firmware rm -rf fwupd-test-firmware git clone https://github.com/fwupd/fwupd-test-firmware #If argument is set copy for installed tests if [ -n "$1" ]; then cp fwupd-test-firmware/installed-tests/* $1 -LRv #copy data for self-tests into the source tree else cp fwupd-test-firmware/ci-tests/* . -Rv fi rm -rf fwupd-test-firmware fi fwupd-2.0.10/contrib/ci/oss-fuzz.py000077500000000000000000000444651501337203100171370ustar00rootroot00000000000000#!/usr/bin/env python3 # pylint: disable=invalid-name,missing-docstring # # Copyright 2021 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later # # pylint: disable=too-many-instance-attributes,no-self-use import os import sys import subprocess import glob from typing import Dict, Optional, List, Union DEFAULT_BUILDDIR = ".ossfuzz" class Builder: def __init__(self) -> None: self.cc = self._ensure_environ("CC", "gcc") self.cxx = self._ensure_environ("CXX", "g++") self.builddir = self._ensure_environ("WORK", os.path.realpath(DEFAULT_BUILDDIR)) self.installdir = self._ensure_environ( "OUT", os.path.realpath(os.path.join(DEFAULT_BUILDDIR, "out")) ) self.srcdir = self._ensure_environ("SRC", os.path.realpath("..")) self.ldflags = [ "-lpthread", "-lresolv", "-ldl", "-lffi", "-lz", "-llzma", "-lzstd", ] # defined in env self.cflags = ["-Wno-deprecated-declarations", "-g"] if "CFLAGS" in os.environ: self.cflags += os.environ["CFLAGS"].split(" ") self.cxxflags = [] if "CXXFLAGS" in os.environ: self.cxxflags += os.environ["CXXFLAGS"].split(" ") # set up shared / static os.environ["PKG_CONFIG"] = "pkg-config --static" if "PATH" in os.environ: os.environ["PATH"] = "{}:{}".format( os.environ["PATH"], os.path.join(self.builddir, "bin") ) else: os.environ["PATH"] = os.path.join(self.builddir, "bin") os.environ["PKG_CONFIG_PATH"] = os.path.join(self.builddir, "lib", "pkgconfig") # writable os.makedirs(self.builddir, exist_ok=True) os.makedirs(self.installdir, exist_ok=True) def _ensure_environ(self, key: str, value: str) -> str: """set the environment unless already set""" if key not in os.environ: os.environ[key] = value return os.environ[key] def checkout_source( self, name: str, url: str, commit: Optional[str] = None, patches: Optional[List[str]] = None, ) -> str: """checkout source tree, optionally to a specific commit""" srcdir_name = os.path.join(self.srcdir, name) if os.path.exists(srcdir_name): return srcdir_name subprocess.run(["git", "clone", url], cwd=self.srcdir, check=True) if commit: subprocess.run(["git", "checkout", commit], cwd=srcdir_name, check=True) if patches: for fn in patches: with open(os.path.join(self.srcdir, "fwupd", fn), "rb") as f: subprocess.run( ["patch", "-p1"], cwd=srcdir_name, check=True, input=f.read() ) return srcdir_name def build_meson_project(self, srcdir: str, argv) -> None: """configure and build the meson project""" srcdir_build = os.path.join(srcdir, DEFAULT_BUILDDIR) if not os.path.exists(srcdir_build): subprocess.run( [ "meson", "--prefix", self.builddir, "--libdir", "lib", "--default-library", "static", ] + argv + [DEFAULT_BUILDDIR], cwd=srcdir, check=True, ) subprocess.run(["ninja", "install"], cwd=srcdir_build, check=True) def build_cmake_project(self, srcdir: str, argv=None) -> None: """configure and build the meson project""" if not argv: argv = [] srcdir_build = os.path.join(srcdir, DEFAULT_BUILDDIR) if not os.path.exists(srcdir_build): os.makedirs(srcdir_build, exist_ok=True) subprocess.run( [ "cmake", f"-DCMAKE_INSTALL_PREFIX:PATH={self.builddir}", "-DCMAKE_INSTALL_LIBDIR=lib", ] + argv + [".."], cwd=srcdir_build, check=True, ) subprocess.run(["make", "all", "install"], cwd=srcdir_build, check=True) def add_work_includedir(self, value: str) -> None: """add a CFLAG""" self.cflags.append(f"-I{self.builddir}/{value}") def add_src_includedir(self, value: str) -> None: """add a CFLAG""" self.cflags.append(f"-I{self.srcdir}/{value}") def add_build_ldflag(self, value: str) -> None: """add a LDFLAG""" self.ldflags.append(os.path.join(self.builddir, value)) def substitute(self, src: str, replacements: Dict[str, str]) -> str: """map changes""" dst = os.path.basename(src).replace(".in", "") with open(os.path.join(self.srcdir, src)) as f: blob = f.read() for key in replacements: blob = blob.replace(key, replacements[key]) with open(os.path.join(self.builddir, dst), "w") as out: out.write(blob) return dst def compile(self, src: str) -> str: """compile a specific source file""" argv = [self.cc] argv.extend(self.cflags) fullsrc = os.path.join(self.srcdir, src) if not os.path.exists(fullsrc): fullsrc = os.path.join(self.builddir, src) dst = os.path.basename(src).replace(".c", ".o") argv.extend(["-c", fullsrc, "-o", os.path.join(self.builddir, dst)]) print(f"building {src} into {dst}") try: subprocess.run(argv, cwd=self.srcdir, check=True) except subprocess.CalledProcessError as e: print(e) sys.exit(1) return os.path.join(self.builddir, f"{dst}") def rustgen(self, src: str) -> str: fn_root = os.path.basename(src).replace(".rs", "") fulldst_c = os.path.join(self.builddir, f"{fn_root}-struct.c") fulldst_h = os.path.join(self.builddir, f"{fn_root}-struct.h") try: subprocess.run( [ "python", "fwupd/libfwupdplugin/rustgen.py", src, fulldst_c, fulldst_h, ], cwd=self.srcdir, check=True, ) except subprocess.CalledProcessError as e: print(e) sys.exit(1) return fulldst_c def link(self, objs: List[str], dst: str) -> str: """link multiple objects into a binary""" argv = [self.cxx] + self.cxxflags for obj in objs: if obj.startswith("-"): argv.append(obj) else: argv.append(os.path.join(self.builddir, obj)) argv += ["-o", os.path.join(self.installdir, dst)] argv += self.ldflags print(f"building {','.join(objs)} into {dst}") subprocess.run(argv, cwd=self.srcdir, check=True) return os.path.join(self.installdir, dst) def mkfuzztargets(self, exe: str, globstr: str) -> List[str]: """make binary fuzzing targets from builder.xml files""" builder_xmls = glob.glob(globstr) corpus: List[str] = [] if not builder_xmls: print(f"failed to find {globstr}") for fn_src in builder_xmls: fn_dst = os.path.join( self.builddir, os.path.basename(fn_src).replace(".builder.xml", ".bin") ) print(f"building {fn_src} into {fn_dst}") try: argv = [exe, fn_src, fn_dst] subprocess.run(argv, check=True) except subprocess.CalledProcessError as e: print(f"tried to run: `{' '.join(argv)}` and got {str(e)}") sys.exit(1) corpus.append(fn_dst) return corpus def write_header( self, dst: str, defines: Dict[str, Optional[Union[str, int]]] ) -> None: """write a header file""" dstdir = os.path.join(self.builddir, os.path.dirname(dst)) os.makedirs(dstdir, exist_ok=True) print(f"writing {dst}") with open(os.path.join(dstdir, os.path.basename(dst)), "w") as f: for key in defines: value = defines[key] if value is not None: if isinstance(value, int): f.write(f"#define {key} {value}\n") else: f.write(f'#define {key} "{value}"\n') else: f.write(f"#define {key}\n") self.add_work_includedir(os.path.dirname(dst)) def makezip(self, dst: str, corpus: List[str]) -> None: """create a zip file archive from a glob""" if not corpus: return argv = ["zip", "--junk-paths", os.path.join(self.installdir, dst)] + corpus print(f"assembling {dst}") subprocess.run(argv, cwd=self.srcdir, check=True) def grep_meson(self, src: str, token: str = "fuzzing") -> List[str]: """find source files tagged with a specific comment""" srcs = [] with open(os.path.join(self.srcdir, src, "meson.build")) as f: for line in f.read().split("\n"): if line.find(token) == -1: continue # get rid of token line = line.split("#")[0] # get rid of variable try: line = line.split("=")[1] except IndexError: pass # get rid of whitespace for char in ["'", ",", " "]: line = line.replace(char, "") # all done srcs.append(os.path.join(src, line)) return srcs class Fuzzer: def __init__(self, name, srcdir=None, pattern=None) -> None: self.name = name self.srcdir = srcdir or name self.globstr = f"{name}*.bin" self.pattern = pattern or f"{name}-firmware" @property def new_gtype(self) -> str: return f"g_object_new(FU_TYPE_{self.pattern.replace('-', '_').upper()}, NULL)" @property def header(self) -> str: return f"fu-{self.pattern}.h" def _build(bld: Builder) -> None: # CBOR src = bld.checkout_source( "libcbor", url="https://github.com/PJK/libcbor.git", commit="b223daaaa34dcb83f9c25576f05e4f1646f44bf9", ) bld.build_cmake_project(src, argv=["-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF"]) bld.add_build_ldflag("lib/libcbor.a") # GLib src = bld.checkout_source( "glib", url="https://github.com/GNOME/glib.git", commit="glib-2-68" ) bld.build_meson_project( src, [ "-Dlibmount=disabled", "-Dselinux=disabled", "-Dnls=disabled", "-Dlibelf=disabled", "-Dbsymbolic_functions=false", "-Dtests=false", "-Dinternal_pcre=true", "--force-fallback-for=libpcre", ], ) bld.add_work_includedir("include/glib-2.0") bld.add_work_includedir("lib/glib-2.0/include") bld.add_build_ldflag("lib/libgio-2.0.a") bld.add_build_ldflag("lib/libgmodule-2.0.a") bld.add_build_ldflag("lib/libgobject-2.0.a") bld.add_build_ldflag("lib/libglib-2.0.a") bld.add_build_ldflag("lib/libgthread-2.0.a") # JSON-GLib src = bld.checkout_source( "json-glib", url="https://github.com/GNOME/json-glib.git", commit="1.8.0-actual", ) bld.build_meson_project( src, ["-Dgtk_doc=disabled", "-Dtests=false", "-Dintrospection=disabled"] ) bld.add_work_includedir("include/json-glib-1.0/json-glib") bld.add_work_includedir("include/json-glib-1.0") bld.add_build_ldflag("lib/libjson-glib-1.0.a") # libxmlb src = bld.checkout_source("libxmlb", url="https://github.com/hughsie/libxmlb.git") bld.build_meson_project( src, ["-Dgtkdoc=false", "-Dintrospection=false", "-Dtests=false"] ) bld.add_work_includedir("include/libxmlb-2") bld.add_work_includedir("include/libxmlb-2/libxmlb") bld.add_build_ldflag("lib/libxmlb.a") # write required headers bld.write_header( "libfwupd/fwupd-version.h", { "FWUPD_MAJOR_VERSION": 0, "FWUPD_MINOR_VERSION": 0, "FWUPD_MICRO_VERSION": 0, }, ) bld.write_header( "config.h", { "FWUPD_DATADIR": "/tmp", "FWUPD_DATADIR_VENDOR_IDS": "/tmp", "FWUPD_LOCALSTATEDIR": "/tmp", "FWUPD_LIBDIR_PKG": "/tmp", "FWUPD_SYSCONFDIR": "/tmp", "FWUPD_LIBEXECDIR": "/tmp", "HAVE_CBOR": None, "HAVE_CBOR_SET_ALLOCS": None, "HAVE_REALPATH": None, "PACKAGE_NAME": "fwupd", "PACKAGE_VERSION": "0.0.0", }, ) # libfwupd + libfwupdplugin built_objs: List[str] = [] fuzzing_objs: List[str] = [] bld.add_src_includedir("fwupd") for path in ["fwupd/libfwupd", "fwupd/libfwupdplugin"]: bld.add_src_includedir(path) for src in bld.grep_meson(path): if src.endswith(".c"): built_objs.append(bld.compile(src)) elif src.endswith(".rs"): built_objs.append(bld.compile(bld.rustgen(src))) # dummy binary entrypoint if "LIB_FUZZING_ENGINE" in os.environ: fuzzing_objs.append(os.environ["LIB_FUZZING_ENGINE"]) else: fuzzing_objs.append(bld.compile("fwupd/libfwupdplugin/fu-fuzzer-main.c")) # built in formats for fzr in [ Fuzzer("efi-lz77", pattern="efi-lz77-decompressor"), Fuzzer("csv"), Fuzzer("cab"), Fuzzer("dfuse"), Fuzzer("edid", pattern="edid"), Fuzzer("elf"), Fuzzer("fdt"), Fuzzer("fit"), Fuzzer("fmap"), Fuzzer("hid-descriptor", pattern="hid-descriptor"), Fuzzer("ihex"), Fuzzer("srec"), Fuzzer("intel-thunderbolt"), Fuzzer("ifwi-cpd"), Fuzzer("ifwi-fpt"), Fuzzer("oprom"), Fuzzer("uswid"), Fuzzer("efi-filesystem", pattern="efi-filesystem"), Fuzzer("efi-volume", pattern="efi-volume"), Fuzzer("efi-load-option", pattern="efi-load-option"), Fuzzer("ifd"), ]: src = bld.substitute( "fwupd/libfwupdplugin/fu-fuzzer-firmware.c.in", { "@FIRMWARENEW@": fzr.new_gtype, "@INCLUDE@": os.path.join("libfwupdplugin", fzr.header), }, ) exe = bld.link( [bld.compile(src)] + fuzzing_objs + built_objs, f"{fzr.name}_fuzzer" ) src_generator = bld.substitute( "fwupd/libfwupdplugin/fu-fuzzer-generate.c.in", { "@FIRMWARENEW@": fzr.new_gtype, "@INCLUDE@": os.path.join("libfwupdplugin", fzr.header), }, ) exe_generator = bld.link( [bld.compile(src_generator)] + built_objs, f"{fzr.name}_generator" ) corpus = bld.mkfuzztargets( exe_generator, os.path.join( bld.srcdir, "fwupd", "libfwupdplugin", "tests", f"{fzr.name}*.builder.xml", ), ) bld.makezip( f"{fzr.name}_fuzzer_seed_corpus.zip", corpus, ) # plugins for fzr in [ Fuzzer("acpi-phat", pattern="acpi-phat"), Fuzzer("bcm57xx"), Fuzzer("ccgx"), Fuzzer("ccgx-dmc"), Fuzzer("cros-ec"), Fuzzer("ebitdo"), Fuzzer("elanfp"), Fuzzer("elantp"), Fuzzer("genesys-scaler", srcdir="genesys", pattern="genesys-scaler-firmware"), Fuzzer("genesys-usbhub", srcdir="genesys", pattern="genesys-usbhub-firmware"), Fuzzer("pixart", srcdir="pixart-rf", pattern="pxi-firmware"), Fuzzer("redfish-smbios", srcdir="redfish", pattern="redfish-smbios"), Fuzzer("synaptics-prometheus", pattern="synaprom-firmware"), Fuzzer("synaptics-cape"), Fuzzer("synaptics-mst"), Fuzzer("synaptics-rmi"), Fuzzer("uf2"), Fuzzer("wacom-usb", pattern="wac-firmware"), ]: fuzz_objs = [] for obj in bld.grep_meson(f"fwupd/plugins/{fzr.srcdir}"): if obj.endswith(".c"): fuzz_objs.append(bld.compile(obj)) elif obj.endswith(".rs"): fuzz_objs.append(bld.compile(bld.rustgen(obj))) src = bld.substitute( "fwupd/libfwupdplugin/fu-fuzzer-firmware.c.in", { "@FIRMWARENEW@": fzr.new_gtype, "@INCLUDE@": os.path.join("plugins", fzr.srcdir, fzr.header), }, ) exe = bld.link( fuzz_objs + built_objs + fuzzing_objs + [bld.compile(src)], f"{fzr.name}_fuzzer", ) # generate the corpus src_generator = bld.substitute( "fwupd/libfwupdplugin/fu-fuzzer-generate.c.in", { "@FIRMWARENEW@": fzr.new_gtype, "@INCLUDE@": os.path.join("plugins", fzr.srcdir, fzr.header), }, ) exe_generator = bld.link( fuzz_objs + built_objs + [bld.compile(src_generator)], f"{fzr.name}_generator", ) corpus = bld.mkfuzztargets( exe_generator, os.path.join( bld.srcdir, "fwupd", "plugins", fzr.srcdir, "tests", f"{fzr.name}*.builder.xml", ), ) bld.makezip( f"{fzr.name}_fuzzer_seed_corpus.zip", corpus, ) if __name__ == "__main__": # install missing deps here rather than patching the Dockerfile in oss-fuzz try: subprocess.check_call( [ "apt-get", "install", "-y", "liblzma-dev", "libzstd-dev", "libcbor-dev", "python3", "python3-jinja2", "python3-packaging", ], stdout=open(os.devnull, "wb"), ) except FileNotFoundError: pass except subprocess.CalledProcessError as e: print(e.output) sys.exit(1) _builder = Builder() _build(_builder) sys.exit(0) fwupd-2.0.10/contrib/ci/populate-bios-setting-translations.py000077500000000000000000000022061501337203100242770ustar00rootroot00000000000000#!/usr/bin/env python3 # # Helper script to generate a list of translations # Sample call: # ./contrib/ci/populate-bios-attr-translations.py ./libfwupdplugin/tests/bios-attrs/dell-xps13-9310/ # will lead to ./libfwupdplugin/tests/bios-attrs/dell-xps13-9310/strings.txt # which can be added to po/POTFILES.in # # Copyright 2022 Mario Limonciello # # SPDX-License-Identifier: LGPL-2.1-or-later import os import sys def populate_translations(path): output = open(os.path.join(path, "strings.txt"), "w") for root, _, files in os.walk(path): for file in files: val: str = "" if not file.endswith("display_name"): continue with open(os.path.join(root, file)) as f: val = f.read().replace('"', "").strip() if not val: continue output.write("#TRANSLATORS: Description of BIOS setting\n") output.write(f"{val}\n\n") output.close() if __name__ == "__main__": if len(sys.argv) != 2: print("path to bios settings directory required") sys.exit(1) populate_translations(sys.argv[1]) fwupd-2.0.10/contrib/ci/precommit.sh000077500000000000000000000002441501337203100173030ustar00rootroot00000000000000#!/bin/sh -e # disable the safe directory feature git config --global safe.directory "*" SKIP=no-commit-to-branch pre-commit run -v --hook-stage commit --all-files fwupd-2.0.10/contrib/ci/qt5-thread-test/000077500000000000000000000000001501337203100177005ustar00rootroot00000000000000fwupd-2.0.10/contrib/ci/qt5-thread-test/meson.build000066400000000000000000000010531501337203100220410ustar00rootroot00000000000000project('qt5-thread-test', 'cpp', license: 'LGPL-2.1-or-later', ) add_project_arguments('-fPIC', language: 'cpp') qt5core = dependency('Qt5Core') qt5concurrent = dependency('Qt5Concurrent') glib2 = dependency('glib-2.0') gio2 = dependency('gio-2.0') fwupd = dependency('fwupd') env = environment() env.set('G_DEBUG', 'fatal-criticals') e = executable( 'qt-thread-test', sources: [ 'qt-thread-test.cpp' ], dependencies: [ qt5core, qt5concurrent, glib2, gio2, fwupd, ], ) test('qt-thread-test', e, timeout: 60, env: env) fwupd-2.0.10/contrib/ci/qt5-thread-test/qt-thread-test.cpp000066400000000000000000000013141501337203100232510ustar00rootroot00000000000000/* * Copyright 2020 Aleix Pol * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include #include #include #include int main(int argc, char** argv) { QCoreApplication app(argc, argv); auto client = fwupd_client_new(); auto cancellable = g_cancellable_new(); g_autoptr(GError) error = nullptr; auto fw = new QFutureWatcher(&app); QObject::connect(fw, &QFutureWatcher::finished, [fw]() { QCoreApplication::exit(0); }); fw->setFuture(QtConcurrent::run([&] { return fwupd_client_get_devices(client, cancellable, &error); })); return app.exec(); } fwupd-2.0.10/contrib/ci/s390x_cross.txt000066400000000000000000000004221501337203100176030ustar00rootroot00000000000000[binaries] c = 's390x-linux-gnu-gcc' cpp = 's390x-linux-gnu-cpp' ar = 's390x-linux-gnu-ar' strip = 's390x-linux-gnu-strip' pkgconfig = 's390x-linux-gnu-pkg-config' exe_wrapper = 'qemu-s390x' [host_machine] system = 'linux' cpu_family = 's390x' cpu = 's390x' endian = 'big' fwupd-2.0.10/contrib/ci/ubuntu-x86_64-test.sh000077700000000000000000000000001501337203100222602/bin/trueustar00rootroot00000000000000fwupd-2.0.10/contrib/ci/ubuntu.sh000077500000000000000000000034231501337203100166300ustar00rootroot00000000000000#!/bin/sh set -e set -x #check for and install missing dependencies ./contrib/ci/fwupd_setup_helpers.py install-dependencies --yes -o ubuntu #check we have pip ./contrib/ci/fwupd_setup_helpers.py install-pip --yes -o ubuntu #check meson is new enough ./contrib/ci/fwupd_setup_helpers.py test-meson #check markdown is new enough ./contrib/ci/fwupd_setup_helpers.py test-markdown #check jinja2 is installed ./contrib/ci/fwupd_setup_helpers.py test-jinja2 #clone test firmware if necessary . ./contrib/ci/get_test_firmware.sh #evaluate using Ubuntu's buildflags #evaluate using Debian/Ubuntu's buildflags #disable link time optimization, Ubuntu currently only sets it for GCC export DEB_BUILD_MAINT_OPTIONS="optimize=-lto" eval "$(dpkg-buildflags --export=sh)" #filter out -Bsymbolic-functions LDFLAGS=$(dpkg-buildflags --get LDFLAGS | sed "s/-Wl,-Bsymbolic-functions\s//") export LDFLAGS root=$(pwd) export BUILD=${root}/build rm -rf ${BUILD} chown -R nobody ${root} sudo -u nobody meson ${BUILD} \ -Db_coverage=true \ -Dman=false \ -Ddocs=enabled \ -Dlibxmlb:gtkdoc=false \ --prefix=${root}/target #build with clang sudo -u nobody ninja -C ${BUILD} -v sudo -u nobody meson test -C ${BUILD} --print-errorlogs --verbose # check for unused symbols ./contrib/ci/check-unused.py # check the daemon aborts set +e FWUPD_SYSCALL_FILTER=systemd ${BUILD}/src/fwupd --immediate-exit if [ $? -ne 1 ] ; then echo "failed to detect missing syscall filtering" exit 1 fi #make docs available outside of docker ninja -C ${BUILD} install -v mkdir -p ${root}/dist/share mv ${root}/target/share/doc ${root}/dist/share # generate coverage report ./contrib/ci/coverage.sh fwupd-2.0.10/contrib/codespell.cfg000066400000000000000000000004211501337203100170020ustar00rootroot00000000000000[codespell] builtin = clear,informal,en-GB_to_en-US skip = *.po,*.csv,*.svg,*.p7c,subprojects,.git,pcrs,build*,.ossfuzz,*/tests/*,contrib/codespell.cfg ignore-words-list = conexant,Conexant,gir,GIR,hsi,HSI,cancelled,Cancelled,te,mitre,distroname,wel,FPT,$FPT,inout,som,SoM fwupd-2.0.10/contrib/create-plugin.py000077500000000000000000000061411501337203100174700ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2023 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later # # pylint: disable=invalid-name,missing-docstring,consider-using-f-string import os import datetime import argparse import glob import sys from jinja2 import Environment, FileSystemLoader, select_autoescape subst = {} # convert a snake-case name into CamelCase def _to_camelcase(value: str) -> str: return "".join([tmp[0].upper() + tmp[1:].lower() for tmp in value.split("_")]) def _subst_replace(data: str) -> str: for key, value in subst.items(): data = data.replace(key, value) return data if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( "--vendor", type=str, help="Vendor name", required=True, ) parser.add_argument( "--example", type=str, help="Plugin basename", required=True, ) parser.add_argument("--parent", type=str, default="Usb", help="Device parent GType") parser.add_argument( "--year", type=int, default=datetime.date.today().year, help="Copyright year" ) parser.add_argument("--author", type=str, help="Copyright author", required=True) parser.add_argument("--email", type=str, help="Copyright email", required=True) args = parser.parse_args() try: vendor: str = args.vendor.replace("-", "_") example: str = args.example.replace("-", "_") # first in list subst["VendorExample"] = _to_camelcase(vendor) + _to_camelcase(example) subst["vendor_example"] = vendor.lower() + "_" + example.lower() subst["vendor_dash_example"] = vendor.lower() + "-" + example.lower() subst["VENDOR_EXAMPLE"] = vendor.upper() + "_" + example.upper() # second for key, value in { "Vendor": vendor, "Example": example, "Parent": args.parent, "Year": str(args.year), "Author": args.author, "Email": args.email, }.items(): subst[key] = _to_camelcase(value) subst[key.lower()] = value.lower() subst[key.upper()] = value.upper() except NotImplementedError as e: print(e) sys.exit(1) template_src: str = "vendor-example" os.makedirs(os.path.join("plugins", _subst_replace(template_src)), exist_ok=True) srcdir: str = sys.argv[0].rsplit("/", maxsplit=2)[0] env = Environment( loader=FileSystemLoader(srcdir), autoescape=select_autoescape(), keep_trailing_newline=True, ) for fn in glob.iglob(f"{srcdir}/plugins/{template_src}/**", recursive=True): if os.path.isdir(fn): fn_rel: str = os.path.relpath(fn, srcdir) os.makedirs(_subst_replace(fn_rel), exist_ok=True) continue fn_rel: str = os.path.relpath(fn, srcdir) template = env.get_template(fn_rel) filename: str = _subst_replace(fn_rel.replace(".in", "")) with open(filename, "wb") as f_dst: f_dst.write(template.render(subst).encode()) print(f"wrote {filename}") fwupd-2.0.10/contrib/debian/000077500000000000000000000000001501337203100155745ustar00rootroot00000000000000fwupd-2.0.10/contrib/debian/README.Debian000066400000000000000000000013421501337203100176350ustar00rootroot00000000000000signed vs unsigned fwupd programs ------------------------------------ fwupd 1.1.0 is configured to understand when to use a signed version of the EFI binary. If the signed version isn't installed but secure boot is turned on, it will avoid copying to the EFI system partition. This allows supporting secure boot even if not turned on at install, or changed later after install. In Ubuntu, both fwupd-signed and fwupd are seeded in the default installation. Nothing is installed to the ESP until it's needed. In Debian, the package name for the signed version is slightly different due to different infrastructure. fwupd-signed-$ARCH and fwupd should both be installed and then things will work similarly to what's described above. fwupd-2.0.10/contrib/debian/README.source000066400000000000000000000004201501337203100177470ustar00rootroot00000000000000fwupd for Debian ---------------- To build from the git tree, run: git-buildpackage -us -uc -S Then, if using sbuild, you can use something like: sbuild -s -c sid-amd64 -d unstable -- Daniel Jared Dominguez Thu, 21 May 2015 13:44:16 -0500 fwupd-2.0.10/contrib/debian/clean000066400000000000000000000000011501337203100165700ustar00rootroot00000000000000 fwupd-2.0.10/contrib/debian/compat000066400000000000000000000000031501337203100167730ustar00rootroot0000000000000012 fwupd-2.0.10/contrib/debian/control.in000066400000000000000000000113271501337203100176100ustar00rootroot00000000000000Source: fwupd Priority: optional Maintainer: Debian EFI Uploaders: Steve McIntyre <93sam@debian.org>, Matthias Klumpp , Mario Limonciello Build-Depends: %%%DYNAMIC%%% Build-Depends-Indep: gi-docgen , Rules-Requires-Root: no Standards-Version: 4.6.0.1 Section: admin Homepage: https://github.com/fwupd/fwupd Vcs-Git: https://salsa.debian.org/efi-team/fwupd.git Vcs-Browser: https://salsa.debian.org/efi-team/fwupd Package: libfwupd3 Section: libs Architecture: linux-any Depends: ${misc:Depends}, ${shlibs:Depends} Multi-Arch: same Description: Firmware update daemon library fwupd is a daemon to allow session software to update device firmware. You can either use a GUI software manager like GNOME Software to view and apply updates, the command-line tool or the system D-Bus interface directly. Firmware updates are supported for a variety of technologies. See for details . This package provides the library used by the daemon. Package: fwupd Architecture: linux-any Depends: ${misc:Depends}, ${shlibs:Depends}, libfwupd3 (= ${binary:Version}), shared-mime-info, systemd-sysusers Recommends: python3, bolt, dbus, secureboot-db, udisks2, fwupd-signed, jq Suggests: gir1.2-fwupd-2.0 Provides: fwupdate Conflicts: fwupdate-amd64-signed, fwupdate-i386-signed, fwupdate-arm64-signed, fwupdate-armhf-signed Breaks: gir1.2-dfu-1.0 (<< 0.9.7-1), libdfu1 (<< 0.9.7-1), fwupdate (<< 12-7), libdfu-dev (<< 0.9.7-1) Replaces: gir1.2-dfu-1.0 (<< 0.9.7-1), libdfu1 (<< 0.9.7-1), libdfu-dev (<< 0.9.7-1), fwupdate (<< 12-7) Multi-Arch: foreign Description: Firmware update daemon fwupd is a daemon to allow session software to update device firmware. You can either use a GUI software manager like GNOME Software to view and apply updates, the command-line tool or the system D-Bus interface directly. Firmware updates are supported for a variety of technologies. See for details Package: fwupd-tests Architecture: linux-any Depends: ${misc:Depends}, ${shlibs:Depends}, ca-certificates, dbus-x11, fwupd, gnome-desktop-testing, polkitd | policykit-1, python3, python3-gi, python3-requests, Breaks: fwupd (<< 0.9.4-1) Replaces: fwupd (<< 0.9.4-1) Multi-Arch: foreign Description: Test suite for firmware update daemon fwupd is a daemon to allow session software to update device firmware. You can either use a GUI software manager like GNOME Software to view and apply updates, the command-line tool or the system D-Bus interface directly. Firmware updates are supported for a variety of technologies. See for details . This package provides a set of installed tests that can be run to validate the daemon in a continuous integration system. Package: fwupd-doc Section: doc Architecture: all Multi-Arch: foreign Depends: ${misc:Depends}, Build-Profiles: Description: Firmware update daemon documentation (HTML format) fwupd is a daemon to allow session software to update device firmware. You can either use a GUI software manager like GNOME Software to view and apply updates, the command-line tool or the system D-Bus interface directly. Firmware updates are supported for a variety of technologies. See for details . This package provides development documentation for creating a package that uses fwupd. Package: libfwupd-dev Architecture: linux-any Multi-Arch: same Depends: libfwupd3 (= ${binary:Version}), gir1.2-fwupd-2.0 (= ${binary:Version}), libcurl4-gnutls-dev, libglib2.0-dev (>= 2.45.8), libjcat-dev, libjson-glib-dev (>= 1.1.1), ${misc:Depends} Breaks: fwupd-dev (<< 0.5.4-2~) Replaces: fwupd-dev (<< 0.5.4-2~) Section: libdevel Description: development files for libfwupd fwupd is a daemon to allow session software to update device firmware. You can either use a GUI software manager like GNOME Software to view and apply updates, the command-line tool or the system D-Bus interface directly. Firmware updates are supported for a variety of technologies. See for details . This package provides the development files for libfwupd Package: gir1.2-fwupd-2.0 Architecture: linux-any Multi-Arch: same Depends: ${misc:Depends}, ${gir:Depends} Section: introspection Description: GObject introspection data for libfwupd This package provides the introspection data for libfwupd. . It can be used by packages using the GIRepository format to generate dynamic bindings. fwupd-2.0.10/contrib/debian/control.qubes.in000066400000000000000000000002451501337203100207230ustar00rootroot00000000000000Package: fwupd-qubes-vm-whonix Architecture: amd64 Description: Whonix support for Qubes OS This package is used to download firmware updates and metadata via TOR. fwupd-2.0.10/contrib/debian/copyright.in000066400000000000000000000201261501337203100201350ustar00rootroot00000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: fwupd Source: https://github.com/fwupd/fwupd %%%DYNAMIC%%% Files: *.metainfo.xml Copyright: Richard Hughes License: CC0-1.0 Files: debian/* Copyright: 2015 Daniel Jared Dominguez 2015 Mario Limonciello License: LGPL-2.1-or-later License: LGPL-2.1-or-later This package 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 2.1 of the License, or (at your option) any later version. . This package 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 General Public License for more details. . You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . On Debian systems, the complete text of the GNU Lesser General Public License version 2.1 can be found in "/usr/share/common-licenses/LGPL-2.1". License: CC0-1.0 Creative Commons CC0 1.0 Universal CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. . Statement of Purpose . The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. . 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; ii. moral rights retained by the original author(s) and/or performer(s); iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; v. rights protecting the extraction, dissemination, use and reuse of data in a Work; vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. . 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. . 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. . 4. Limitations and Disclaimers. a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. fwupd-2.0.10/contrib/debian/docs000066400000000000000000000000011501337203100164360ustar00rootroot00000000000000 fwupd-2.0.10/contrib/debian/fwupd-doc.links000066400000000000000000000001741501337203100205300ustar00rootroot00000000000000usr/share/doc/libfwupd /usr/share/gtk-doc/html/libfwupd usr/share/doc/libfwupdplugin /usr/share/gtk-doc/html/libfwupdplugin fwupd-2.0.10/contrib/debian/fwupd-qubes-vm-whonix.install000066400000000000000000000001351501337203100233570ustar00rootroot00000000000000usr/libexec/qubes-fwupd/fwupd_download_updates.py usr/libexec/qubes-fwupd/fwupd_common_vm.py fwupd-2.0.10/contrib/debian/fwupd-qubes-vm-whonix.postinst000066400000000000000000000005221501337203100235740ustar00rootroot00000000000000#!/bin/bash HOME_DIR=`getent passwd user | awk '{ split($$0,a,":"); print a[6]}'` if [ -z "$HOME_DIR" ]; then echo "Default user does not exist!!" >&2 echo "Package does not create fwupd directories" >&2 else mkdir -p $HOME_DIR/.cache/fwupd_download_updates chown -R user:user $HOME_DIR/.cache/fwupd_download_updates fi fwupd-2.0.10/contrib/debian/fwupd-qubes-vm-whonix.postrm000066400000000000000000000002531501337203100232360ustar00rootroot00000000000000#!/bin/bash HOME_DIR=`getent passwd user | awk '{ split($$0,a,":"); print a[6]}'` if [ -z "$HOME_DIR" ] && [ "$1" = "purge" ]; then rm -rf $HOME_DIR/.cache/fwupd fi fwupd-2.0.10/contrib/debian/fwupd-tests.install000066400000000000000000000005561501337203100214570ustar00rootroot00000000000000#These are in a generic looking directory because #that is where gnome-desktop-testing expects to #find them. for more information see: #https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=872458 usr/share/installed-tests/* usr/libexec/installed-tests/fwupd/*-self-test debian/lintian/fwupd-tests usr/share/lintian/overrides usr/share/fwupd/remotes.d/fwupd-tests.conf fwupd-2.0.10/contrib/debian/fwupd-tests.postinst000066400000000000000000000004021501337203100216620ustar00rootroot00000000000000#!/bin/sh set -e #DEBHELPER# #only enable on installation not upgrade if [ "$1" = configure ] && [ -z "$2" ]; then if [ "$CI" = "true" ]; then fwupdtool enable-test-devices else echo "To enable test suite, run `fwupdtool enable-test-devices`" fi fi fwupd-2.0.10/contrib/debian/fwupd-tests.postrm000066400000000000000000000003761501337203100213350ustar00rootroot00000000000000#!/bin/sh set -e #DEBHELPER# # don't run on purge; the commands might be missing if [ "$1" = remove ]; then if [ "$CI" = "true" ]; then fwupdtool disable-test-devices else echo "To disable test suite, run `fwupdtool disable-test-devices`" fi fi fwupd-2.0.10/contrib/debian/fwupd.dirs000066400000000000000000000000301501337203100175750ustar00rootroot00000000000000var/cache/app-info/xmls fwupd-2.0.10/contrib/debian/fwupd.install000066400000000000000000000010051501337203100203050ustar00rootroot00000000000000usr/bin/* etc/* usr/share/bash-completion usr/share/fish/vendor_completions.d usr/share/fwupd/*.* usr/share/fwupd/metainfo/* usr/share/fwupd/remotes.d/vendor/* usr/share/dbus-1/* usr/share/icons/* usr/share/polkit-1/* usr/share/locale usr/share/metainfo/* usr/libexec/fwupd/* usr/lib/sysusers.d/* usr/share/man/* data/fwupd.conf etc/fwupd debian/fwupd.pkla /var/lib/polkit-1/localauthority/10-vendor.d usr/lib/*/fwupd-*/*.so debian/lintian/fwupd usr/share/lintian/overrides obj*/data/motd/85-fwupd /etc/update-motd.d fwupd-2.0.10/contrib/debian/fwupd.maintscript000066400000000000000000000010371501337203100212010ustar00rootroot00000000000000 rm_conffile /etc/fwupd.conf 1.0.0~ rm_conffile /etc/fwupd/remotes.d/fwupd.conf 1.2.7~ rm_conffile /etc/dbus-1/system.d/org.freedesktop.fwupd.conf 1.3.2~ rm_conffile /etc/modules-load.d/fwupd-msr.conf 1.5.3~ rm_conffile /etc/modules-load.d/fwupd-platform-integrity.conf 1.5.3~ rm_conffile /etc/fwupd/ata.conf 1.5.5~ mv_conffile /etc/fwupd/uefi.conf /etc/fwupd/uefi_capsule.conf 1.5.5~ rm_conffile /etc/pki/fwupd/GPG-KEY-Hughski-Limited 1.9.10~ rm_conffile /etc/fwupd/upower.conf 1.9.10~ rm_conffile /etc/fwupd/remotes.d/dell-esrt.conf 1.9.11~ fwupd-2.0.10/contrib/debian/fwupd.pkla000066400000000000000000000002071501337203100175710ustar00rootroot00000000000000[Call internal fwupd actions] Identity=unix-group:admin;unix-group:sudo Action=org.freedesktop.fwupd.update-internal ResultActive=yes fwupd-2.0.10/contrib/debian/fwupd.postinst000066400000000000000000000001611501337203100205240ustar00rootroot00000000000000#!/bin/sh set -e #DEBHELPER# if [ -d /run/systemd/system ]; then deb-systemd-invoke reload dbus || true fi fwupd-2.0.10/contrib/debian/fwupd.postrm000066400000000000000000000002421501337203100201650ustar00rootroot00000000000000#!/bin/sh set -e #DEBHELPER# if [ "$1" = purge ]; then rm -rf /var/lib/fwupd /var/cache/fwupd /var/cache/fwupdmgr rm -f /var/cache/app-info/xmls/fwupd.xml fi fwupd-2.0.10/contrib/debian/fwupd.preinst000066400000000000000000000004151501337203100203270ustar00rootroot00000000000000#!/bin/sh set -e #DEBHELPER# # 1.3.2 had fwupd-refresh.service and fwupd.service both claiming # this directory, but fwupd-refresh.service used DynamicUser directive # meaning no other unit could access it. if [ -L /var/cache/fwupd ]; then rm -f /var/cache/fwupd fi fwupd-2.0.10/contrib/debian/gbp.conf000066400000000000000000000001611501337203100172110ustar00rootroot00000000000000[DEFAULT] debian-branch = debian upstream-tag = %(version)s [buildpackage] sign-tags = True dist = experimental fwupd-2.0.10/contrib/debian/gir1.2-fwupd-2.0.install000066400000000000000000000000531501337203100216040ustar00rootroot00000000000000usr/lib/*/girepository-1.0/Fwupd-*.typelib fwupd-2.0.10/contrib/debian/libfwupd-dev.install000066400000000000000000000003101501337203100215460ustar00rootroot00000000000000usr/include/fwupd-*/fwupd.h usr/include/fwupd-*/libfwupd usr/lib/*/libfwupd.so usr/lib/*/pkgconfig/fwupd.pc usr/share/gir-1.0/Fwupd-*.gir usr/share/vala/vapi/fwupd.deps usr/share/vala/vapi/fwupd.vapi fwupd-2.0.10/contrib/debian/libfwupd3.install000066400000000000000000000000301501337203100210540ustar00rootroot00000000000000usr/lib/*/libfwupd.so.* fwupd-2.0.10/contrib/debian/lintian/000077500000000000000000000000001501337203100172325ustar00rootroot00000000000000fwupd-2.0.10/contrib/debian/lintian/fwupd000066400000000000000000000001321501337203100202760ustar00rootroot00000000000000#see Debian bug 896012 fwupd: library-not-linked-against-libc usr/lib/*/fwupd-plugins-*/* fwupd-2.0.10/contrib/debian/lintian/fwupd-tests000066400000000000000000000001401501337203100214350ustar00rootroot00000000000000#see Debian bug 896012 fwupd-tests: library-not-linked-against-libc usr/lib/*/fwupd-plugins-*/* fwupd-2.0.10/contrib/debian/not-installed000066400000000000000000000002011501337203100202650ustar00rootroot00000000000000usr/libexec/qubes-fwupd/fwupd_usbvm_validate.py usr/sbin/qubes-fwupdmgr usr/share/qubes-fwupd/src/* usr/share/qubes-fwupd/test/* fwupd-2.0.10/contrib/debian/rules000077500000000000000000000050231501337203100166540ustar00rootroot00000000000000#!/usr/bin/make -f # -*- makefile -*- export LC_ALL := C.UTF-8 export DEB_BUILD_MAINT_OPTIONS = hardening=+all export DEB_LDFLAGS_MAINT_STRIP=-Wl,-Bsymbolic-functions #GPGME needs this for proper building on 32 bit archs ifeq ($(DEB_HOST_ARCH_BITS),32) export DEB_CFLAGS_MAINT_APPEND = -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE endif CONFARGS = ifneq ($(CI),) CONFARGS += --werror -Db_coverage=true else CONFARGS += -Dsupported_build=enabled endif ifneq ($(DEB_HOST_ARCH_CPU),ia64) CONFARGS += -Dplugin_flashrom=enabled else CONFARGS += -Dplugin_flashrom=disabled endif ifneq ($(QUBES_OPTION),) CONFARGS += -Dqubes=true endif ifneq ($(filter nodoc,$(DEB_BUILD_PROFILES)),) CONFARGS += -Ddocs=disabled endif CONFARGS += -Dplugin_modem_manager=enabled %: dh $@ --with gir override_dh_auto_clean: rm -fr obj-* rm -fr debian/build override_dh_auto_configure: dh_auto_configure -- $(CONFARGS) override_dh_install: find debian/tmp/usr -type f -name "*a" -print | xargs rm -f sed -i 's,wheel,sudo,' debian/tmp/usr/share/polkit-1/rules.d/org.freedesktop.fwupd.rules dh_install dh_install -pfwupd $$(pkg-config --variable=systemdsystemunitdir systemd | sed s,^/,,) #install MSR conf if needed (depending on distro) [ ! -d debian/tmp/usr/lib/modules-load.d ] || dh_install -pfwupd usr/lib/modules-load.d [ ! -d debian/tmp/lib/modules-load.d ] || dh_install -pfwupd lib/modules-load.d [ ! -d debian/tmp/usr/share/fwupd/quirks.d ] || dh_install -pfwupd usr/share/fwupd/quirks.d #install docs (maybe) [ ! -d debian/tmp/usr/share/doc ] || dh_install -pfwupd-doc usr/share/doc #install devices-tests (maybe) [ ! -d debian/tmp/usr/share/fwupd/device-tests/ ] || dh_install -pfwupd-tests usr/share/fwupd/device-tests #/usr merge or not (make backporting easier) [ ! -d debian/tmp/lib/systemd ] || dh_install -pfwupd lib/systemd [ ! -d debian/tmp/usr/lib/systemd ] || dh_install -pfwupd usr/lib/systemd dh_missing -a --fail-missing #this is placed in fwupd-tests rm -f debian/fwupd/usr/share/fwupd/remotes.d/fwupd-tests.conf # avoid shipping an empty directory [ ! -d debian/fwupd/lib/systemd ] || find debian/fwupd/lib/systemd -type d -empty -delete [ ! -d debian/fwupd/usr/lib/systemd ] || find debian/fwupd/usr/lib/systemd -type d -empty -delete # the below step is automatic with debhelper >= 14 dh_installsysusers override_dh_strip_nondeterminism: dh_strip_nondeterminism -Xfirmware-example.xml.gz ifneq (yes,$(shell command -v valgrind >/dev/null 2>&1 && echo yes)) override_dh_auto_test: : endif override_dh_builddeb: dh_builddeb fwupd-2.0.10/contrib/debian/source/000077500000000000000000000000001501337203100170745ustar00rootroot00000000000000fwupd-2.0.10/contrib/debian/source/format000066400000000000000000000000141501337203100203020ustar00rootroot000000000000003.0 (quilt) fwupd-2.0.10/contrib/debian/source/lintian-overrides000066400000000000000000000001231501337203100224510ustar00rootroot00000000000000#github doesn't have these fwupd source: debian-watch-does-not-check-gpg-signature fwupd-2.0.10/contrib/debian/source/options000066400000000000000000000000641501337203100205120ustar00rootroot00000000000000extend-diff-ignore=".vscode|venv|subprojects|build" fwupd-2.0.10/contrib/debian/tests/000077500000000000000000000000001501337203100167365ustar00rootroot00000000000000fwupd-2.0.10/contrib/debian/tests/ci000077500000000000000000000005711501337203100172620ustar00rootroot00000000000000#!/bin/sh -e exec 2>&1 # try loading the mtdram module to run our mtd tests modprobe mtdram || true fwupdtool enable-test-devices fwupdtool modify-config VerboseDomains "*" sed "s,ConditionVirtualization=.*,," \ /lib/systemd/system/fwupd.service > \ /etc/systemd/system/fwupd.service systemctl daemon-reload # run the tests gnome-desktop-testing-runner --timeout=600 fwupd fwupd-2.0.10/contrib/debian/tests/control000066400000000000000000000002231501337203100203360ustar00rootroot00000000000000Tests: ci Restrictions: needs-root Tests: libfwupd-dev Depends: build-essential, libfwupd-dev, pkg-config Restrictions: allow-stderr, superficial fwupd-2.0.10/contrib/debian/tests/libfwupd-dev000077500000000000000000000013551501337203100212600ustar00rootroot00000000000000#!/bin/sh # Copyright 2020 Collabora Ltd. # Copyright 2021 Simon McVittie # SPDX-License-Identifier: LGPL-2.1-or-later set -eux WORKDIR="$(mktemp -d)" trap 'cd /; rm -fr "$WORKDIR"' 0 INT QUIT ABRT PIPE TERM if [ -n "${DEB_HOST_GNU_TYPE:-}" ]; then CROSS_COMPILE="$DEB_HOST_GNU_TYPE-" else CROSS_COMPILE= fi CC="${CROSS_COMPILE}gcc" PKG_CONFIG="${CROSS_COMPILE}pkg-config" cd "$WORKDIR" cat > trivial.c <<'EOF' #undef NDEBUG #include #include int main (void) { assert (fwupd_error_to_string (FWUPD_ERROR_NOTHING_TO_DO) != NULL); return 0; } EOF # Deliberately word-splitting pkg-config's output: # shellcheck disable=SC2046 "${CC}" -otrivial trivial.c $("${PKG_CONFIG}" --cflags --libs fwupd) ./trivial fwupd-2.0.10/contrib/debian/watch000066400000000000000000000003531501337203100166260ustar00rootroot00000000000000# You can run the "uscan" command to check for upstream updates and more. # See uscan(1) for format version=3 opts=filenamemangle=s/.+\/v?(\d\S*)\.tar\.gz/fwupd-$1\.tar\.gz/ \ https://github.com/fwupd/fwupd/tags .*/v?(\d\S*)\.tar\.gz fwupd-2.0.10/contrib/dump-mtd-ifd.py000077500000000000000000000140611501337203100172200ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2024 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later # # pylint: disable=invalid-name,missing-docstring,too-few-public-methods,too-many-locals import sys import os import struct import subprocess import io from typing import List, Optional import argparse class IfdPartition: REGION_NAMES = [ "desc", "bios", "me", "gbe", "platform", "devexp", "bios2", "ec", "ie", "10gbe", ] def __init__(self, region: int = 0, offset: int = 0, size: int = 0) -> None: self.region: int = region self.offset: int = offset self.size: int = size def __str__(self) -> str: try: region_name: str = IfdPartition.REGION_NAMES[self.region] except IndexError: region_name = "unknown" return ( "IfdPartition(" f"region=0x{self.region} ({region_name}), " f"offset=0x{self.offset:x}, " f"size=0x{self.size:x})" ) def _read_partitions(f: io.BufferedReader) -> bytearray: f.seek(0) blob_ifd: bytes = f.read(0x1000) ( signature, descriptor_map0, descriptor_map1, descriptor_map2, ) = struct.unpack_from("<16xIIII", blob_ifd, offset=0) if signature != 0x0FF0A55A: sys.exit(f"Not IFD signature 0x0FF0A55A, got 0x{signature:X}") # read out the descriptor maps print(f"descriptor_map0=0x{descriptor_map0:X}") print(f"descriptor_map1=0x{descriptor_map1:X}") print(f"descriptor_map2=0x{descriptor_map2:X}") # default to all partitions num_regions = (descriptor_map0 >> 24) & 0b111 if num_regions == 0: num_regions = 10 print(f"num_regions=0x{num_regions:X}") # read out FREGs flash_region_base_addr = (descriptor_map0 >> 12) & 0x00000FF0 print(f"flash_region_base_addr=0x{flash_region_base_addr:X}") flash_descriptor_regs: List[int] = [] for region in range(num_regions): flash_descriptor_regs.append( struct.unpack_from( "> 4) & 0x07FFF000) | 0x00000FFF # invalid if freg_base > freg_limit: continue fregs.append( IfdPartition(region=i, offset=freg_base, size=freg_limit - freg_base) ) # create a binary blob big enough image_size: int = 0 for freg in fregs: if freg.offset + freg.size > image_size: image_size = freg.offset + freg.size print(f"image_size=0x{image_size:x}") blob: bytearray = bytearray(image_size) # copy each partition for freg in fregs: print("reading...", freg) try: f.seek(freg.offset) blob_part: bytes = f.read(freg.size) blob_size: int = len(blob_part) if blob_size != freg.size: print(f"tried to read 0x{freg.size:x} and instead got 0x{blob_size:x}") if blob_size: blob[freg.offset : freg.offset + blob_size] = blob_part except OSError as e: print(f"failed to read: {e}") return blob def _read_device_to_file(devname: str, filename: Optional[str]) -> None: # grab system info from sysfs if not filename: filename = "" for sysfs_fn in [ "/sys/class/dmi/id/sys_vendor", "/sys/class/dmi/id/product_family", "/sys/class/dmi/id/product_name", "/sys/class/dmi/id/product_sku", ]: try: with open(sysfs_fn, "rb") as f: if filename: filename += "-" filename += ( f.read().decode().replace("\n", "").replace(" ", "_").lower() ) except FileNotFoundError: pass if filename: filename += ".bin" else: filename = "bios.bin" # check this device name is what we expect print(f"checking {devname}...") try: with open(f"/sys/class/mtd/{os.path.basename(devname)}/name", "rb") as f_name: name = f_name.read().decode().replace("\n", "") except FileNotFoundError as e: sys.exit(str(e)) if name != "BIOS": sys.exit(f"Not Intel Corporation PCH SPI Controller, got {name}") # read the IFD header, then each partition try: with open(devname, "rb") as f_in: print(f"reading from {devname}...") blob = _read_partitions(f_in) except PermissionError as e: sys.exit(f"cannot read mtd device: {e}") print(f"writing {filename}...") with open(filename, "wb") as f_out: f_out.write(blob) # this is really helpful for debugging print(f"getting additional data from {devname}...") try: p = subprocess.run(["mtdinfo", devname], check=True, capture_output=True) except subprocess.CalledProcessError as e: print(f"{' '.join(args)}: {e}") else: for line in p.stdout.decode().split("\n"): if not line: continue print(line) print("done!") if __name__ == "__main__": # both have defaults parser = argparse.ArgumentParser( prog="dump-mtd-ifd", description="Dump local SPI contents using MTD" ) parser.add_argument( "--filename", action="store", type=str, help="Output filename", default=None, ) parser.add_argument( "--devname", action="store", type=str, help="Device name, e.g. /dev/mtd0", default="/dev/mtd0", ) args = parser.parse_args() _read_device_to_file(args.devname, args.filename) fwupd-2.0.10/contrib/firmware_packager/000077500000000000000000000000001501337203100200235ustar00rootroot00000000000000fwupd-2.0.10/contrib/firmware_packager/README.md000066400000000000000000000114241501337203100213040ustar00rootroot00000000000000# Firmware Packager This script is intended to make firmware updating easier until OEMs upload their firmware packages to the LVFS. It works by extracting the firmware binary contained in a Microsoft .exe file (intended for performing the firmware update from a Windows system) and repackaging it in a cab file usable by fwupd. The cab file can then be install using `fwupdmgr install` ## Prerequisites To run this script you will need 1. Python3.5, a standard install should include all packages you need 2. 7z (for extracting .exe files) ## Usage To create a firmware package, you must supply, at a minimum: 1. A string ID to name the firmware (`--firmware-id`). You are free to choose this, but [fwupd.org](http://fwupd.org/vendors.html) recommends using "a reverse-DNS prefix similar to java" and to "always use a .firmware suffix" (e.g. net.queuecumber.DellTBT.firmware) 2. A short name for the firmware package, again you are free to choose this (`--firmware-name`). 3. The unique ID of the device that the firmware is intended for (`--device-unique-id`). This *must* match the unique ID from `fwupdmgr get-devices` 4. The firmware version (`--release-version`), try to match the manufacturers versioning scheme 5. The path to the executable file to repackage (`--exe`) 6. The path *relative to the root of the exe archive* of the .bin file to package (`--bin`). Use 7z or archive-manager to inspect the .exe file and find this path. For example, if I want to package `dell-thunderbolt-firmware.exe` and I open the .exe with archive-manager and find that `Intel/tbt.bin` is the path to the bin file inside the archive, I would pass `--exe dell-thunderbolt-firmware.exe --bin Intel/tbt.bin` 7. The path to the cab file to output (`--out`). ## Documentation `--firmware-name` Short name of the firmware package can be customized (e.g. DellTBT) **REQUIRED** `--firmware-summary` One line description of the firmware package (e.g. Dell thunderbolt firmware) `--firmware-description` Longer description of the firmware package. Theoretically this can include HTML but I haven't tried it `--device-guid` GUID ID of the device this firmware will run on, this *must* match the output from `fwupdmgr get-devices` (e.g. 72533768-6a6c-5c06-994a-367374336810) **REQUIRED** `--firmware-homepage` Website for the firmware provider (e.g. ) `--contact-info` Email address of the firmware developer (e.g. ) `--developer-name` Name of the firmware developer (e.g. Dell) **REQUIRED** `--release-version` Version number of the firmware package (e.g. 4.21.01.002) **REQUIRED** `--update-protocol` Protocol using (e.g. org.uefi.capsule) **REQUIRED** `--release-description` Description of the firmware release, again this can theoretically include HTML but I didn't try it. `--exe` Executable file to extract firmware from (e.g. `dell-thunderbolt-firmware.exe`) `--bin` Path to the .bin file inside the executable to use as the firmware image', relative to the root of the archive (e.g. `Intel/tbt.bin`) **REQUIRED** `--out` Output cab file path (e.g. `updates/firmware.cab`) **REQUIRED** ## Example Let's say we downloaded `Intel_TBT3_FW_UPDATE_NVM21_318RY_A01_4.21.01.002.exe` (available [here](https://downloads.dell.com/FOLDER04421073M/1/Intel_TBT3_FW_UPDATE_NVM21_318RY_A01_4.21.01.002.exe)) containing updated firmware for Dell laptops thunderbolt controllers. Since Dell hasn't made this available on the LVFS yet, we want to package and install it ourselves. Opening the .exe with archive manager, we see it has a single folder: `Intel` and inside that, a set of firmware binaries (along with some microsoft junk). We pick the file `0x07BE_secure.bin` since we have a Dell XPS 9560 and that is its device string. Next we use `fwupdmgr` to get the device ID for the thunderbolt controller: ```shell $ fwupdmgr get-devices Thunderbolt Controller Guid: 72533768-6a6c-5c06-994a-367374336810 DeviceID: 08001575 Plugin: thunderbolt Flags: internal|allow-online DeviceVendor: Intel Version: 21.00 Created: 2017-08-16 ``` The GUID field contains what we are looking for We can then run the firmware-packager with the following arguments: ```shell $ firmware-packager --firmware-id net.queuecumber.DellTBT.firmware --firmware-name DellTBT --device-unique-id 72533768-6a6c-5c06-994a-367374336810 --release-version 4.21.01.002 --exe ~/Downloads/Intel_TBT3_FW_UPDATE_NVM21_318RY_A01_4.21.01.002.exe --bin Intel/0x07BE_secure.bin --out firmware.cab Using temp directory /tmp/tmpoey6_zx_ Extracting firmware exe Locating firmware bin Creating metainfo Cabbing firmware files Done ``` And we should have a firmware.cab that contains the packaged firmware. We can then install this firmware with `fwupdmgr install firmware.cab`. fwupd-2.0.10/contrib/firmware_packager/__init__.py000066400000000000000000000000001501337203100221220ustar00rootroot00000000000000fwupd-2.0.10/contrib/firmware_packager/add_capsule_header.py000077500000000000000000000047261501337203100241650ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2019 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later import sys import uuid import argparse import ctypes CAPSULE_FLAGS_PERSIST_ACROSS_RESET = 0x00010000 CAPSULE_FLAGS_POPULATE_SYSTEM_TABLE = 0x00020000 CAPSULE_FLAGS_INITIATE_RESET = 0x00040000 def add_header(infile, outfile, gd, fl=None): # parse GUID from command line try: guid = uuid.UUID(gd) except ValueError as e: print(e) return 1 import struct try: with open(infile, "rb") as f: bin_data = f.read() except FileNotFoundError as e: print(e) return 1 # check if already has header hdrsz = struct.calcsize("<16sIII") if len(bin_data) >= hdrsz: hdr = struct.unpack("<16sIII", bin_data[:hdrsz]) imgsz = hdr[3] if imgsz == len(bin_data): print("Replacing existing CAPSULE_HEADER of:") guid_mixed = uuid.UUID(bytes_le=hdr[0]) hdrsz_old = hdr[1] flags = hdr[2] print(f"GUID: {guid_mixed}") print(f"HdrSz: 0x{hdrsz_old:04x}") print(f"Flags: 0x{flags:04x}") print(f"PayloadSz: 0x{imgsz:04x}") bin_data = bin_data[hdrsz_old:] # set header flags flags = ( CAPSULE_FLAGS_PERSIST_ACROSS_RESET | CAPSULE_FLAGS_INITIATE_RESET | CAPSULE_FLAGS_POPULATE_SYSTEM_TABLE ) if fl: flags = int(fl, 16) # build update capsule header hdrsz = 4096 imgsz = hdrsz + len(bin_data) hdr = ctypes.create_string_buffer(hdrsz) struct.pack_into("<16sIII", hdr, 0, guid.bytes_le, hdrsz, flags, imgsz) with open(outfile, "wb") as f: f.write(hdr) f.write(bin_data) print(f"Wrote capsule {outfile}") print(f"GUID: {guid}") print(f"HdrSz: 0x{hdrsz:04x}") print(f"Flags: 0x{flags:04x}") print(f"PayloadSz: 0x{imgsz:04x}") return 0 if __name__ == "__main__": parser = argparse.ArgumentParser(description="Add capsule header on firmware") parser.add_argument("--guid", help="GUID of the device", required=True) parser.add_argument("--bin", help="Path to the .bin file", required=True) parser.add_argument("--cap", help="Output capsule file path", required=True) parser.add_argument("--flags", help="Flags, e.g. 0x40000", default=None) args = parser.parse_args() sys.exit(add_header(args.bin, args.cap, args.guid, args.flags)) fwupd-2.0.10/contrib/firmware_packager/add_dfu_header.py000077500000000000000000000030261501337203100232770ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2020 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later import struct import zlib import argparse def main(bin_fn, dfu_fn, pad, vid, pid, rev): # read binary file with open(bin_fn, "rb") as f: blob = f.read() # pad blob to a specific size if pad: while len(blob) < int(pad, 16): blob += b"\0" # create DFU footer with checksum blob += struct.pack( " org.{developer_name}.guid{firmware_id} {firmware_name} {firmware_summary} {firmware_description} {device_guid} {firmware_homepage} CC0-1.0 proprietary {contact_info} {developer_name} {release_description} {version_format} {update_protocol} """ def make_firmware_metainfo(firmware_info, dst): local_info = vars(firmware_info) local_info["firmware_id"] = local_info["device_guid"][0:8] firmware_metainfo = firmware_metainfo_template.format( **local_info, timestamp=time.time() ) with open(os.path.join(dst, "firmware.metainfo.xml"), "w") as f: f.write(firmware_metainfo) def extract_exe(exe, dst): command = ["7z", "x", f"-o{dst}", exe] subprocess.check_call(command, stdout=subprocess.DEVNULL) def get_firmware_bin(root, bin_path, dst): with cd(root): shutil.copy(bin_path, os.path.join(dst, "firmware.bin")) def create_firmware_cab(exe, folder): with cd(folder): if os.name == "nt": directive = os.path.join(folder, "directive") with open(directive, "w") as wfd: wfd.write(".OPTION EXPLICIT\r\n") wfd.write(".Set CabinetNameTemplate=firmware.cab\r\n") wfd.write(".Set DiskDirectory1=.\r\n") wfd.write("firmware.bin\r\n") wfd.write("firmware.metainfo.xml\r\n") command = ["makecab.exe", "/f", directive] else: command = [ "fwupdtool", "build-cabinet", "firmware.cab", "firmware.bin", "firmware.metainfo.xml", ] subprocess.check_call(command) def main(args): with tempfile.TemporaryDirectory() as d: print(f"Using temp directory {d}") if args.exe: print("Extracting firmware exe") extract_exe(args.exe, d) print("Locating firmware bin") get_firmware_bin(d, args.bin, d) print("Creating metainfo") make_firmware_metainfo(args, d) print("Creating cabinet file") create_firmware_cab(args, d) print("Done") shutil.copy(os.path.join(d, "firmware.cab"), args.out) if __name__ == "__main__": parser = argparse.ArgumentParser( description="Create fwupd packaged from windows executables" ) parser.add_argument( "--firmware-name", help="Name of the firmware package can be customized (e.g. DellTBT)", required=True, ) parser.add_argument( "--firmware-summary", help="One line description of the firmware package" ) parser.add_argument( "--firmware-description", help="Longer description of the firmware package" ) parser.add_argument( "--device-guid", help="GUID of the device this firmware will run on, this *must* match the output of one of the GUIDs in `fwupdmgr get-devices`", required=True, ) parser.add_argument("--firmware-homepage", help="Website for the firmware provider") parser.add_argument( "--contact-info", help="Email address of the firmware developer" ) parser.add_argument( "--developer-name", help="Name of the firmware developer", required=True ) parser.add_argument( "--release-version", help="Version number of the firmware package", required=True, ) parser.add_argument( "--version-format", help="Version format, e.g. quad or triplet", required=True, ) parser.add_argument( "--update-protocol", help="Update protocol, e.g. org.uefi.capsule", required=True, ) parser.add_argument( "--release-description", help="Description of the firmware release" ) parser.add_argument( "--exe", help="(optional) Executable file to extract firmware from" ) parser.add_argument( "--bin", help="Path to the .bin file (Relative if inside the executable; Absolute if outside) to use as the firmware image", required=True, ) parser.add_argument("--out", help="Output cab file path", required=True) args = parser.parse_args() main(args) fwupd-2.0.10/contrib/firmware_packager/install_dell_bios_exe.py000077500000000000000000000100271501337203100247230ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2019 Mario Limonciello # # SPDX-License-Identifier: LGPL-2.1-or-later import dbus import os.path import sys import tempfile import gi try: gi.require_version("Fwupd", "2.0") except ValueError: print("Missing gobject-introspection packages. Try to install gir1.2-fwupd-2.0.") sys.exit(1) from gi.repository import Fwupd # pylint: disable=wrong-import-position from simple_client import get_daemon_property, install, check_exists, modify_config from add_capsule_header import add_header from firmware_packager import make_firmware_metainfo, create_firmware_cab class Variables: def __init__(self, device_guid, version): self.device_guid = device_guid self.developer_name = "Dell Inc" self.firmware_name = "New firmware" self.firmware_summary = "Unknown" self.firmware_description = "Unknown" self.firmware_homepage = "https://support.dell.com" self.contact_info = "Unknown" self.release_version = version self.release_description = "Unknown" self.update_protocol = "org.uefi.capsule" self.version_format = "dell-bios" def parse_args(): """Parse arguments for this client""" import argparse parser = argparse.ArgumentParser(description="Interact with fwupd daemon") parser.add_argument("exe", nargs="?", help="exe file") parser.add_argument("deviceid", nargs="?", help="DeviceID to operate on(optional)") args = parser.parse_args() return args def generate_cab(infile, directory, guid, version): output = os.path.join(directory, "firmware.bin") ret = add_header(infile, output, guid) if ret: sys.exit(ret) variables = Variables(guid, version) make_firmware_metainfo(variables, directory) create_firmware_cab(variables, directory) cab = os.path.join(directory, "firmware.cab") print(f"Generated CAB file {cab}") return cab def find_uefi_device(client, deviceid): devices = client.get_devices() for item in devices: # match the device we were given if deviceid: if item.get_id() != deviceid: continue # internal if not item.has_flag(Fwupd.DeviceFlags.INTERNAL): continue # needs reboot if not item.has_flag(Fwupd.DeviceFlags.NEEDS_REBOOT): continue # return the first hit for UEFI plugin if item.get_plugin() == "uefi" or item.get_plugin() == "uefi_capsule": print(f"Installing to {item.get_name()}") return item.get_guid_default(), item.get_id(), item.get_version() print("Couldn't find any UEFI devices") sys.exit(1) def set_conf_only_trusted(client, setval): prop = "OnlyTrusted" current_val = get_daemon_property(prop) if current_val: pass elif setval: pass else: return False modify_config(client, "fwupd", prop, str(setval).lower()) return get_daemon_property(prop) == setval def prompt_reboot(): print("An update requires a reboot to complete") while True: res = input("Restart now? (Y/N) ") if res.lower() == "n": print("Reboot your machine manually to finish the update.") break if res.lower() != "y": continue # reboot using logind obj = dbus.SystemBus().get_object( "org.freedesktop.login1", "/org/freedesktop/login1" ) obj.Reboot(True, dbus_interface="org.freedesktop.login1.Manager") if __name__ == "__main__": ARGS = parse_args() CLIENT = Fwupd.Client() check_exists(ARGS.exe) try: is_restore_required = set_conf_only_trusted(CLIENT, False) directory = tempfile.mkdtemp() guid, deviceid, version = find_uefi_device(CLIENT, ARGS.deviceid) cab = generate_cab(ARGS.exe, directory, guid, version) install(CLIENT, cab, deviceid, True, True) except Exception as e: print(e) if is_restore_required: set_conf_only_trusted(CLIENT, True) prompt_reboot() fwupd-2.0.10/contrib/firmware_packager/meson.build000066400000000000000000000010161501337203100221630ustar00rootroot00000000000000if get_option('firmware-packager') install_data('firmware_packager.py', install_dir: 'share/fwupd') install_data('add_capsule_header.py', install_dir: 'share/fwupd') install_data('install_dell_bios_exe.py', install_dir: 'share/fwupd') con2 = configuration_data() con2.set('FWUPD_VERSION', fwupd_version) configure_file( input: 'simple_client.py', output: 'simple_client.py', configuration: con2, install: true, install_dir: 'share/fwupd', ) endif fwupd-2.0.10/contrib/firmware_packager/simple_client.py000077500000000000000000000146331501337203100232360ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2019 Mario Limonciello # # SPDX-License-Identifier: LGPL-2.1-or-later """A simple fwupd frontend""" import sys import os import dbus import gi from gi.repository import GLib gi.require_version("Fwupd", "2.0") from gi.repository import Fwupd # pylint: disable=wrong-import-position class Progress: """Class to track the signal changes of progress events""" def __init__(self): self.device = None self.status = None self.percent = 0 self.erase = 0 def device_changed(self, new_device): """Indicate new device string to track""" if self.device != new_device: self.device = new_device print(f"\nUpdating {self.device}") def status_changed(self, percent, status): """Indicate new status string or % complete to track""" if self.status != status or self.percent != percent: for i in range(0, self.erase): sys.stdout.write("\b \b") self.status = status self.percent = percent status_str = "[" for i in range(0, 50): if i < percent / 2: status_str += "*" else: status_str += " " status_str += "] %d%% %s" % (percent, status) self.erase = len(status_str) sys.stdout.write(status_str) sys.stdout.flush() if "idle" in status: sys.stdout.write("\n") def parse_args(): """Parse arguments for this client""" import argparse parser = argparse.ArgumentParser(description="Interact with fwupd daemon") parser.add_argument( "--allow-older", action="store_true", help="Install older payloads(default False)", ) parser.add_argument( "--allow-reinstall", action="store_true", help="Reinstall payloads(default False)", ) parser.add_argument( "command", choices=[ "get-devices", "get-details", "install", "refresh", "get-bios-setting", ], help="What to do", ) parser.add_argument("cab", nargs="?", help="CAB file") parser.add_argument("deviceid", nargs="?", help="DeviceID to operate on(optional)") parser.add_argument("--setting", help="BIOS setting to operate on(optional)") args = parser.parse_args() return args def refresh(client): """Uses fwupd client to refresh metadata""" remotes = client.get_remotes() client.set_user_agent_for_package("simple_client", "@FWUPD_VERSION@") for remote in remotes: if not remote.get_enabled(): continue if remote.get_kind() != Fwupd.RemoteKind.DOWNLOAD: continue client.refresh_remote(remote) def get_devices(client): """Use fwupd client to fetch devices""" devices = client.get_devices() for item in devices: print(item.to_string()) def get_details(client, cab): """Use fwupd client to fetch details for a CAB file""" devices = client.get_details(cab, None) for device in devices: print(device.to_string()) def get_bios_settings(client, setting): """Use fwupd client to get BIOS settings""" settings = client.get_bios_settings() for i in settings: if not setting or setting == i.get_name() or setting == i.get_id(): print(i.to_string()) def status_changed(client, spec, progress): # pylint: disable=unused-argument """Signal emitted by fwupd daemon indicating status changed""" progress.status_changed( client.get_percentage(), Fwupd.status_to_string(client.get_status()) ) def device_changed(client, device, progress): # pylint: disable=unused-argument """Signal emitted by fwupd daemon indicating active device changed""" progress.device_changed(device.get_name()) def modify_config(client, section, key, value): """Use fwupd client to modify daemon configuration value""" try: print(f"setting configuration key {key} to {value}") client.modify_config(section, key, value, None) except Exception as e: print(f"{str(e)}") sys.exit(1) def install(client, cab, target, older, reinstall): """Use fwupd client to install CAB file to applicable devices""" # FWUPD_DEVICE_ID_ANY if not target: target = "*" flags = Fwupd.InstallFlags.NONE if older: flags |= Fwupd.InstallFlags.ALLOW_OLDER if reinstall: flags |= Fwupd.InstallFlags.ALLOW_REINSTALL progress = Progress() parent = super(client.__class__, client) parent.connect("device-changed", device_changed, progress) parent.connect("notify::percentage", status_changed, progress) parent.connect("notify::status", status_changed, progress) try: client.install(target, cab, flags, None) except GLib.Error as glib_err: # pylint: disable=catching-non-exception progress.status_changed(0, "idle") print(f"{glib_err}") sys.exit(1) print("\n") def get_daemon_property(key: str): try: bus = dbus.SystemBus() proxy = bus.get_object(bus_name="org.freedesktop.fwupd", object_path="/") iface = dbus.Interface(proxy, "org.freedesktop.DBus.Properties") val = iface.Get("org.freedesktop.fwupd", key) if isinstance(val, dbus.Boolean): print(f"org.freedesktop.fwupd property {key}, current value is {bool(val)}") else: print(f"org.freedesktop.fwupd property {key}, current value is {val}") return val except dbus.DBusException as e: print(e) return None def check_exists(cab): """Check that CAB file exists""" if not cab: print("Need to specify payload") sys.exit(1) if not os.path.isfile(cab): print(f"{cab} doesn't exist or isn't a file") sys.exit(1) if __name__ == "__main__": ARGS = parse_args() CLIENT = Fwupd.Client() if ARGS.command == "get-devices": get_devices(CLIENT) elif ARGS.command == "get-details": check_exists(ARGS.cab) get_details(CLIENT, ARGS.cab) elif ARGS.command == "refresh": refresh(CLIENT) elif ARGS.command == "install": check_exists(ARGS.cab) install(CLIENT, ARGS.cab, ARGS.deviceid, ARGS.allow_older, ARGS.allow_reinstall) elif ARGS.command == "get-bios-setting": get_bios_settings(CLIENT, ARGS.setting) fwupd-2.0.10/contrib/flatpak/000077500000000000000000000000001501337203100157745ustar00rootroot00000000000000fwupd-2.0.10/contrib/freebsd/000077500000000000000000000000001501337203100157645ustar00rootroot00000000000000fwupd-2.0.10/contrib/freebsd/Makefile000066400000000000000000000023701501337203100174260ustar00rootroot00000000000000# Created by: Norbert KamiÅ„ski # $FreeBSD$ PORTNAME= fwupd DISTVERSION= GH_TAGNAME= CATEGORIES= sysutils MAINTAINER= norbert.kaminski@3mdeb.com COMMENT= Update firmware automatically, safely, and reliably LICENSE= LGPL21 BUILD_DEPENDS= gi-docgen:textproc/gi-docgen \ help2man:misc/help2man \ vala:lang/vala \ ${LOCALBASE}/libexec/fwupd/efi/fwupdx64.efi:sysutils/fwupd-efi \ ${PYTHON_PKGNAMEPREFIX}gobject3>0:devel/py-gobject3@${PY_FLAVOR} LIB_DEPENDS= libcurl.so:ftp/curl \ libefiboot.so:devel/libefiboot \ libgnutls.so:security/gnutls \ libgpg-error.so:security/libgpg-error \ libgpgme.so:security/gpgme \ libusb.so:devel/libusb \ libjcat.so:textproc/libjcat \ libjson-glib-1.0.so:devel/json-glib \ libprotobuf-c.so:devel/protobuf-c \ libcbor.so:devel/libcbor \ libxmlb.so:textproc/libxmlb \ libefiboot.so:devel/gnu-efi RUN_DEPENDS= ${LOCALBASE}/libexec/fwupd/efi/fwupdx64.efi:sysutils/fwupd-efi USES= gnome libarchive meson pkgconfig python:3.8+ shebangfix sqlite USE_GITHUB= yes USE_GNOME= glib20 introspection:build GH_ACCOUNT= INSTALLS_ICONS= yes USE_LDCONFIG= yes SHEBANG_GLOB= *.py MESON_ARGS= -Dpolkit=disabled \ -Dsystemd=disabled \ -Dtests=false \ -Ddocs=enabled .include fwupd-2.0.10/contrib/freebsd/pkg-descr000066400000000000000000000006321501337203100175670ustar00rootroot00000000000000Make firmware updates automatic, safe, and reliable. fwupd is a system daemon to allow session software to update device firmware on your local machine. It is designed for desktops, but also usable on phones and headless servers. You can either use a GUI software manager like GNOME Software to view and apply updates, the command-line tool, or the system D-Bus interface directly. WWW: https://fwupd.org/ fwupd-2.0.10/contrib/fwupd-systemd-fatal-criticals.conf000066400000000000000000000000601501337203100230700ustar00rootroot00000000000000[Service] Environment="G_DEBUG=fatal-criticals" fwupd-2.0.10/contrib/fwupd.spec.in000066400000000000000000000265131501337203100167670ustar00rootroot00000000000000%global glib2_version 2.45.8 %global libxmlb_version 0.1.3 %global libusb_version 1.0.9 %global libcurl_version 7.62.0 %global libjcat_version 0.1.0 %global systemd_version 249 %global json_glib_version 1.1.1 # although we ship a few tiny python files these are utilities that 99.99% # of users do not need -- use this to avoid dragging python onto CoreOS %global __requires_exclude ^%{python3}$ %define alphatag #ALPHATAG# %global enable_ci 0 %global enable_tests 1 %global __meson_wrap_mode nodownload # fwupd.efi is only available on these arches %ifarch x86_64 aarch64 riscv64 %global have_uefi 1 %endif # flashrom is only available on these arches %ifarch i686 x86_64 armv7hl aarch64 ppc64le riscv64 %global have_flashrom 1 %endif %ifarch i686 x86_64 %global have_msr 1 %endif # Until we actually have seen it outside x86 %ifarch i686 x86_64 %global have_thunderbolt 1 %endif # only available recently %if 0%{?fedora} >= 30 %global have_modem_manager 1 %endif %if 0%{?fedora} %global have_passim 1 %endif Summary: Firmware update daemon Name: fwupd Version: #VERSION# Release: 0.#BUILD#%{?alphatag}%{?dist} License: LGPL-2.1-or-later URL: https://github.com/fwupd/fwupd Source0: http://people.freedesktop.org/~hughsient/releases/%{name}-%{version}.tar.xz BuildRequires: gettext BuildRequires: glib2-devel >= %{glib2_version} BuildRequires: libxmlb-devel >= %{libxmlb_version} BuildRequires: libusb1-devel >= %{libusb_version} BuildRequires: libcurl-devel >= %{libcurl_version} BuildRequires: libjcat-devel >= %{libjcat_version} BuildRequires: polkit-devel >= 0.103 BuildRequires: protobuf-c-devel BuildRequires: python3-packaging BuildRequires: python3-jinja2 BuildRequires: sqlite-devel BuildRequires: systemd >= %{systemd_version} BuildRequires: systemd-devel BuildRequires: libarchive-devel BuildRequires: libcbor-devel BuildRequires: libblkid-devel BuildRequires: readline-devel %if 0%{?have_passim} BuildRequires: passim-devel %endif BuildRequires: gobject-introspection-devel %ifarch %{valgrind_arches} BuildRequires: valgrind BuildRequires: valgrind-devel %endif BuildRequires: gi-docgen BuildRequires: gnutls-devel BuildRequires: gnutls-utils BuildRequires: meson BuildRequires: json-glib-devel >= %{json_glib_version} BuildRequires: vala BuildRequires: pkgconfig(bash-completion) BuildRequires: git-core %if 0%{?have_flashrom} BuildRequires: flashrom-devel >= 1.2-2 %endif BuildRequires: libdrm-devel %if 0%{?have_modem_manager} BuildRequires: ModemManager-glib-devel >= 1.10.0 BuildRequires: libqmi-devel >= 1.22.0 BuildRequires: libmbim-devel %endif %if 0%{?have_uefi} BuildRequires: python3 python3-cairo python3-gobject BuildRequires: pango-devel BuildRequires: cairo-devel cairo-gobject-devel BuildRequires: freetype BuildRequires: fontconfig BuildRequires: google-noto-sans-cjk-ttc-fonts BuildRequires: tpm2-tss-devel >= 2.2.3 %endif Requires(post): systemd Requires(preun): systemd Requires(postun): systemd Requires: glib2%{?_isa} >= %{glib2_version} Requires: libxmlb%{?_isa} >= %{libxmlb_version} Requires: libusb1%{?_isa} >= %{libusb_version} Requires: shared-mime-info %if 0%{?rhel} > 7 || 0%{?fedora} > 28 Recommends: python3 %endif Obsoletes: dbxtool < 9 Provides: dbxtool %if 0%{?rhel} > 7 Obsoletes: fwupdate < 11-4 Obsoletes: fwupdate-efi < 11-4 Provides: fwupdate Provides: fwupdate-efi %endif # optional, but a really good idea Recommends: udisks2 Recommends: bluez Recommends: jq %if 0%{?have_passim} Recommends: passim %endif %if 0%{?have_modem_manager} Recommends: %{name}-plugin-modem-manager %endif %if 0%{?have_flashrom} Recommends: %{name}-plugin-flashrom %endif %if 0%{?have_uefi} Recommends: %{name}-efi Recommends: %{name}-plugin-uefi-capsule-data %endif %description fwupd is a daemon to allow session software to update device firmware. %package devel Summary: Development package for %{name} Requires: %{name}%{?_isa} = %{version}-%{release} Obsoletes: libebitdo-devel < 0.7.5-3 Obsoletes: libdfu-devel < 1.0.0 %description devel Files for development with %{name}. %package tests Summary: Data files for installed tests Requires: %{name}%{?_isa} = %{version}-%{release} %description tests Data files for installed tests. %if 0%{?have_modem_manager} %package plugin-modem-manager Summary: fwupd plugin using ModemManger Requires: %{name}%{?_isa} = %{version}-%{release} %description plugin-modem-manager This provides the optional package which is only required on hardware that might have mobile broadband hardware. It is probably not required on servers. %endif %if 0%{?have_flashrom} %package plugin-flashrom Summary: fwupd plugin using flashrom Requires: %{name}%{?_isa} = %{version}-%{release} %description plugin-flashrom This provides the optional package which is only required on hardware that can be flashed using flashrom. It is probably not required on servers. %endif %if 0%{?have_uefi} %package plugin-uefi-capsule-data Summary: Localized data for the UEFI UX capsule Requires: %{name}%{?_isa} = %{version}-%{release} %description plugin-uefi-capsule-data This provides the pregenerated BMP artwork for the UX capsule, which allows the "Installing firmware update…" localized text to be shown during a UEFI firmware update operation. This subpackage is probably not required on embedded hardware or server machines. %endif %if 0%{?qubes_packages} %package qubes-dom0 Summary: fwupd wrapper for Qubes OS - dom0 scripts Requires: fwupd >= 2.0.0 Requires: libjcat >= 0.1.6 Requires: python3-packaging Requires: sequoia-sqv %description qubes-dom0 fwupd wrapper for Qubes OS %package qubes-vm Summary: fwupd wrapper for Qubes OS - VM scripts Requires: fwupd >= 2.0.0 Requires: libjcat >= 0.1.6 Requires: python3-packaging %description qubes-vm fwupd wrapper for Qubes OS %endif %prep %autosetup -p1 %build %meson \ %if 0%{?enable_ci} --werror \ -Db_coverage=true \ -Db_sanitize=address,undefined \ %else -Dsupported_build=enabled \ %endif -Dumockdev_tests=disabled \ -Ddocs=enabled \ %if 0%{?enable_tests} -Dtests=true \ %else -Dtests=false \ %endif %if 0%{?have_flashrom} -Dplugin_flashrom=enabled \ %else -Dplugin_flashrom=disabled \ %endif %if 0%{?have_modem_manager} -Dplugin_modem_manager=enabled \ %else -Dplugin_modem_manager=disabled \ %endif %if 0%{?have_passim} -Dpassim=enabled \ %else -Dpassim=disabled \ %endif %if 0%{?qubes_packages} -Dqubes=true \ %endif -Dman=true \ -Dsystemd_unit_user="" \ -Dbluez=enabled %meson_build %if 0%{?enable_tests} %if 0%{?enable_ci} ./contrib/ci/get_test_firmware.sh %endif %check %meson_test %endif %install %meson_install mkdir -p --mode=0700 $RPM_BUILD_ROOT%{_localstatedir}/lib/fwupd/gnupg # workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1757948 mkdir -p $RPM_BUILD_ROOT%{_localstatedir}/cache/fwupd %find_lang %{name} %post %systemd_post fwupd.service fwupd-refresh.timer %preun %systemd_preun fwupd.service fwupd-refresh.timer %postun %systemd_postun_with_restart fwupd.service fwupd-refresh.timer %triggerun -- fedora-release-common < 39-0.28 # For upgrades from versions before fwupd-refresh.timer was enabled by default systemctl --no-reload preset fwupd-refresh.timer &>/dev/null || : %files -f %{name}.lang %doc README.md %license COPYING %config(noreplace)%{_sysconfdir}/fwupd/fwupd.conf %dir %{_libexecdir}/fwupd %{_libexecdir}/fwupd/fwupd %ifarch x86_64 %{_libexecdir}/fwupd/fwupd-detect-cet %endif %{_bindir}/dbxtool %{_bindir}/fwupdmgr %{_bindir}/fwupdtool %dir %{_sysconfdir}/fwupd %dir %{_sysconfdir}/fwupd/bios-settings.d %{_sysconfdir}/fwupd/bios-settings.d/README.md %dir %{_sysconfdir}/fwupd/remotes.d %config(noreplace)%{_sysconfdir}/fwupd/remotes.d/lvfs.conf %config(noreplace)%{_sysconfdir}/fwupd/remotes.d/lvfs-testing.conf %config(noreplace)%{_sysconfdir}/fwupd/remotes.d/vendor-directory.conf %config(noreplace)%{_sysconfdir}/pki/fwupd %{_sysconfdir}/pki/fwupd-metadata %if 0%{?have_msr} /usr/lib/modules-load.d/fwupd-msr.conf %endif %{_datadir}/dbus-1/system.d/org.freedesktop.fwupd.conf %{_datadir}/bash-completion/completions/fwupdmgr %{_datadir}/bash-completion/completions/fwupdtool %{_datadir}/fish/vendor_completions.d/fwupdmgr.fish %dir %{_datadir}/fwupd %dir %{_datadir}/fwupd/metainfo %{_datadir}/fwupd/metainfo/org.freedesktop.fwupd*.metainfo.xml %dir %{_datadir}/fwupd/remotes.d %dir %{_datadir}/fwupd/remotes.d/vendor %dir %{_datadir}/fwupd/remotes.d/vendor/firmware %{_datadir}/fwupd/remotes.d/vendor/firmware/README.md %{_datadir}/dbus-1/interfaces/org.freedesktop.fwupd.xml %{_datadir}/polkit-1/actions/org.freedesktop.fwupd.policy %{_datadir}/polkit-1/rules.d/org.freedesktop.fwupd.rules %{_datadir}/dbus-1/system-services/org.freedesktop.fwupd.service %{_mandir}/man1/fwupdtool.1* %{_mandir}/man1/dbxtool.* %{_mandir}/man1/fwupdmgr.1* %{_mandir}/man5/* %{_mandir}/man8/* %{_datadir}/metainfo/org.freedesktop.fwupd.metainfo.xml %{_datadir}/icons/hicolor/*/apps/org.freedesktop.fwupd.* %{_datadir}/fwupd/firmware_packager.py %{_datadir}/fwupd/simple_client.py %{_datadir}/fwupd/add_capsule_header.py %{_datadir}/fwupd/install_dell_bios_exe.py %{_unitdir}/fwupd.service %{_unitdir}/fwupd-refresh.service %{_unitdir}/fwupd-refresh.timer %dir %{_localstatedir}/lib/fwupd %dir %{_localstatedir}/cache/fwupd %dir %{_datadir}/fwupd/quirks.d %{_datadir}/fwupd/quirks.d/builtin.quirk.gz %{_datadir}/doc/fwupd/*.html %if 0%{?have_uefi} %config(noreplace)%{_sysconfdir}/grub.d/35_fwupd %endif %{_libdir}/libfwupd.so.3* %{_libdir}/girepository-1.0/Fwupd-2.0.typelib /usr/lib/systemd/system-shutdown/fwupd.shutdown %dir %{_libdir}/fwupd-%{version} %{_libdir}/fwupd-%{version}/libfwupd*.so %ghost %{_localstatedir}/lib/fwupd/gnupg %if 0%{?have_modem_manager} %files plugin-modem-manager %{_libdir}/fwupd-%{version}/libfu_plugin_modem_manager.so %endif %if 0%{?have_flashrom} %files plugin-flashrom %{_libdir}/fwupd-%{version}/libfu_plugin_flashrom.so %endif %if 0%{?have_uefi} %files plugin-uefi-capsule-data %{_datadir}/fwupd/uefi-capsule-ux.tar.xz %endif %files devel %{_datadir}/gir-1.0/Fwupd-2.0.gir %{_datadir}/doc/fwupd/libfwupdplugin %{_datadir}/doc/fwupd/libfwupd %{_datadir}/doc/libfwupdplugin %{_datadir}/doc/libfwupd %{_datadir}/vala/vapi %{_includedir}/fwupd-3 %{_libdir}/libfwupd*.so %{_libdir}/pkgconfig/fwupd.pc %files tests %if 0%{?enable_tests} %{_datadir}/fwupd/host-emulate.d/*.json.gz %{_datadir}/installed-tests/fwupd %{_libexecdir}/installed-tests/fwupd %{_datadir}/fwupd/remotes.d/fwupd-tests.conf %endif %if 0%{?qubes_packages} %files qubes-vm %{_libexecdir}/qubes-fwupd/fwupd_common_vm.py %{_libexecdir}/qubes-fwupd/fwupd_download_updates.py %files qubes-dom0 %{_datadir}/qubes-fwupd/src/fwupd_receive_updates.py %{_sbindir}/qubes-fwupdmgr %{_datadir}/qubes-fwupd/src/qubes_fwupdmgr.py %{_datadir}/qubes-fwupd/src/qubes_fwupd_common.py %{_datadir}/qubes-fwupd/src/qubes_fwupd_heads.py %{_datadir}/qubes-fwupd/src/qubes_fwupd_update.py %{_datadir}/qubes-fwupd/src/__init__.py %{_datadir}/qubes-fwupd/test/fwupd_logs.py %{_datadir}/qubes-fwupd/test/test_qubes_fwupdmgr.py %{_datadir}/qubes-fwupd/test/test_qubes_fwupd_heads.py %{_datadir}/qubes-fwupd/test/__init__.py %{_datadir}/qubes-fwupd/test/logs/get_devices.log %{_datadir}/qubes-fwupd/test/logs/get_updates.log %{_datadir}/qubes-fwupd/test/logs/help.log %{_datadir}/qubes-fwupd/test/logs/firmware.metainfo.xml %{_datadir}/qubes-fwupd/test/logs/metainfo_name/firmware.metainfo.xml %{_datadir}/qubes-fwupd/test/logs/metainfo_version/firmware.metainfo.xml %endif %changelog %autochangelog fwupd-2.0.10/contrib/fwupd.wxs.in000066400000000000000000000035541501337203100166560ustar00rootroot00000000000000 NOT NEWERVERSIONDETECTED fwupd-2.0.10/contrib/generate-ds20.py000077500000000000000000000041531501337203100172720ustar00rootroot00000000000000#!/usr/bin/env python3 # pylint: disable=invalid-name,missing-docstring # # Copyright 2022 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later # # pylint: disable=consider-using-f-string import sys import argparse import configparser import base64 from typing import List if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( "-b", "--bufsz", type=int, help="Buffer size in bytes", ) parser.add_argument("-i", "--instance-id", type=str, help="Device instance ID") parser.add_argument("filename", action="store", type=str, help="Quirk filename") args = parser.parse_args() config = configparser.ConfigParser() config.optionxform = str try: config.read(args.filename) except configparser.MissingSectionHeaderError: print("Not a quirk file") sys.exit(1) # fall back to the default if there is only one device in the quirk file if not args.instance_id: sections = config.sections() if len(sections) != 1: print("Multiple devices found, use --instance-id to choose between:") for section in sections: print(f" • {section}") sys.exit(1) args.instance_id = sections[0] # create the smallest kv store possible lines: List[str] = [] try: for key in config[args.instance_id]: if key in ["Inhibit", "Issue"]: print(f"WARNING: skipping key {key}") continue value = config[args.instance_id][key] lines.append(f"{key}={value}") except KeyError: print(f"No {args.instance_id} section") sys.exit(1) # pad to the buffer size buf: bytes = "\n".join(lines).encode() if args.bufsz: if len(buf) > args.bufsz: print("Quirk data is larger than bufsz") sys.exit(1) buf = buf.ljust(args.bufsz, b"\0") # success print("DS20 descriptor control transfer data:") print(", ".join([f"0x{val:02x}" for val in list(buf)])) print(base64.b64encode(buf).decode()) fwupd-2.0.10/contrib/generate-emulation.py000077500000000000000000000074421501337203100205230ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2017 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later # # pylint: disable=invalid-name,missing-docstring,consider-using-f-string import json import sys from typing import Dict, List, Any import gi from gi.repository import GLib gi.require_version("Fwupd", "2.0") gi.require_version("Json", "1.0") from gi.repository import Fwupd # pylint: disable=wrong-import-position from gi.repository import Json # pylint: disable=wrong-import-position def _minimize_json(json_str: str) -> str: nodes = json.loads(json_str) new_attrs: List[Dict[str, Any]] = [] new_devices: List[Dict[str, Any]] = [] new_bios_settings: List[Dict[str, Any]] = [] try: for attr in nodes["SecurityAttributes"]: new_attr: Dict[str, Any] = {} for key in attr: if key in ["AppstreamId", "HsiResult", "Flags", "Plugin"]: new_attr[key] = attr[key] new_attrs.append(new_attr) except KeyError: pass try: for device in nodes["Devices"]: new_device: Dict[str, Any] = {} for key in device: if key not in ["Created", "Modified", "Releases", "Plugin"]: new_device[key] = device[key] new_devices.append(new_device) except KeyError: pass try: for device in nodes["BiosSettings"]: new_attr: Dict[str, Any] = {} for key in device: if key not in ["Filename"]: new_attr[key] = device[key] new_bios_settings.append(new_attr) except KeyError: pass return json.dumps( { "SecurityAttributes": new_attrs, "Devices": new_devices, "BiosSettings": new_bios_settings, }, indent=2, separators=(",", " : "), ) def _get_host_devices_and_attrs() -> str: # connect to the running daemon client = Fwupd.Client() builder = Json.Builder() builder.begin_object() # add devices try: devices = client.get_devices() except GLib.GError as e: print(f"ignoring {e}") else: builder.set_member_name("Devices") builder.begin_array() for device in devices: builder.begin_object() device.to_json_full(builder, Fwupd.DEVICE_FLAG_TRUSTED) builder.end_object() builder.end_array() # add security attributes try: attrs = client.get_host_security_attrs() except GLib.GError as e: print(f"ignoring {e}") else: builder.set_member_name("SecurityAttributes") builder.begin_array() for attr in attrs: builder.begin_object() attr.add_json(builder) builder.end_object() builder.end_array() # add BIOS settings try: attrs = client.get_bios_settings() except GLib.GError as e: print(f"ignoring {e}") else: builder.set_member_name("BiosSettings") builder.begin_array() for attr in attrs: builder.begin_object() attr.add_json(builder) builder.end_object() builder.end_array() # export to JSON builder.end_object() generator = Json.Generator() generator.set_pretty(True) generator.set_root(builder.get_root()) return generator.to_data()[0] if len(sys.argv) < 2: sys.stdout.write(_minimize_json(sys.stdin.read())) else: for fn in sys.argv[1:]: try: with open(fn, "rb") as f_in: json_in = f_in.read().decode() except FileNotFoundError: json_in = _get_host_devices_and_attrs() json_out = _minimize_json(json_in).encode() with open(fn, "wb") as f_out: f_out.write(json_out) fwupd-2.0.10/contrib/generate-release.py000077500000000000000000000046541501337203100201500ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2023 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later # # pylint: disable=invalid-name,missing-docstring import sys import os import subprocess import datetime from jinja2 import Environment, FileSystemLoader def _get_last_release() -> str: return ( subprocess.check_output(["git", "describe", "--tags", "--abbrev=0"]) .decode() .replace("\n", "") ) def _get_next_release(last_tag: str) -> str: try: triplet: list[str] = last_tag.split(".") return f"{triplet[0]}.{triplet[1]}.{int(triplet[2])+1}" except IndexError: return last_tag def _get_appstream_date() -> str: return datetime.datetime.now().strftime("%Y-%m-%d") def _generate_release_notes(last_tag: str, next_tag: str) -> str: lines_feat: list[str] = [] lines_bugs: list[str] = [] lines_devs: list[str] = [] for line in ( subprocess.check_output( [ "git", "log", "--format=%s", f"{last_tag}...", ] ) .decode() .split("\n") ): if not line: continue if line.find("trivial") != -1: continue if line.find("Typo") != -1: continue if line.find("Merge") != -1: continue if line.find("build(deps)") != -1: continue if line in lines_feat or line in lines_bugs or line in lines_devs: continue if line.startswith("Add "): lines_feat.append(line) continue lines_bugs.append(line) env = Environment( loader=FileSystemLoader(os.path.dirname(os.path.realpath(__file__))), autoescape=False, keep_trailing_newline=False, ) template = env.get_template("generate-release.xml") return template.render( { "version": next_tag, "date": _get_appstream_date(), "features": lines_feat, "bugs": lines_bugs, "devices": lines_devs, } ) if __name__ == "__main__": try: _last_tag: str = sys.argv[1] except IndexError: _last_tag: str = _get_last_release() try: _next_tag: str = sys.argv[2] except IndexError: _next_tag: str = _get_next_release(_last_tag) xml: str = _generate_release_notes(_last_tag, _next_tag) print(xml) fwupd-2.0.10/contrib/generate-release.xml000066400000000000000000000011221501337203100203000ustar00rootroot00000000000000

This release adds the following features:

    {%- for item in features %}
  • {{item}}
  • {%- endfor %}

This release fixes the following bugs:

    {%- for item in bugs %}
  • {{item}}
  • {%- endfor %}

This release adds support for the following hardware:

    {%- for item in devices %}
  • {{item}}
  • {%- endfor %}
fwupd-2.0.10/contrib/launch-venv.sh000077500000000000000000000043571501337203100171500ustar00rootroot00000000000000#!/usr/bin/env -S bash -e gcc=$(gcc -dumpmachine) DIST="$(dirname $0)/../dist" BIN="$(basename $0)" COMMAND="$1" ARGUMENT="$2" DBUSPOLICY="/usr/share/dbus-1/system.d/org.freedesktop.fwupd.conf" PKPOLICY="/usr/share/polkit-1/actions/org.freedesktop.fwupd.policy" export FWUPD_LOCALSTATEDIR=${DIST} export FWUPD_SYSCONFDIR=${DIST}/etc export LD_LIBRARY_PATH=${DIST}/lib/${gcc}:${DIST}/lib64:${DIST}/lib if [ -n "${DEBUG}" ]; then if ! which gdbserver 1>/dev/null 2>&1; then echo "install gdbserver to enable debugging" exit 1 fi DEBUG="gdbserver localhost:9091" fi if [ -f ${DIST}/libexec/fwupd/${BIN} ]; then EXE=${DIST}/libexec/fwupd/${BIN} else EXE=${DIST}/bin/${BIN} fi if [ ! -f ${EXE} ]; then echo "Not yet built! Please run:" echo "" echo "# build-fwupd" exit 1 fi if [ -z "${G_DEBUG}" ]; then G_DEBUG="fatal-criticals" fi if [ -z "${GLIBC_TUNABLES}" ]; then GLIBC_TUNABLES=glibc.cpu.hwcaps=SHSTK fi ENV="FWUPD_POLKIT_NOCHECK=1 \ G_DEBUG=${G_DEBUG} \ GLIBC_TUNABLES=${GLIBC_TUNABLES} \ LD_LIBRARY_PATH=${LD_LIBRARY_PATH}" for var in $(env | grep FWUPD | cut -d= -f1); do ENV="${ENV} ${var}=${!var}" done SUDO=$(which sudo) if [ "${BIN}" = "fwupd" ] && \ [ -d "$(dirname ${DBUSPOLICY})" ] && \ [ ! -f ${DBUSPOLICY} ]; then echo "Missing D-Bus policy in ${DBUSPOLICY}" echo "Copy into filesystem? [y/N]" read -r answer if [ "${answer}" != "y" ]; then exit 1 fi ${SUDO} cp ${DIST}/share/dbus-1/system.d/org.freedesktop.fwupd.conf ${DBUSPOLICY} fi if [ "${BIN}" = "fwupdmgr" ] && [ -d "$(dirname ${PKPOLICY})" ] && \ ! grep "org.freedesktop.fwupd.emulation-save" $PKPOLICY 1>/dev/null 2>&1; then echo "Missing or outdated PolicyKit policy in ${PKPOLICY}" echo "Copy into filesystem? [y/N]" read -r answer if [ "${answer}" != "y" ]; then exit 1 fi ${SUDO} cp ${DIST}/share/polkit-1/actions/org.freedesktop.fwupd.policy ${PKPOLICY} fi ${SUDO} ${ENV} ${DEBUG} ${EXE} "$@" if [ "${BIN}" = "fwupdmgr" ] && [ "${COMMAND}" = "emulation-save" ]; then ${SUDO} chown "$(id -u)":"$(id -g)" ${ARGUMENT} fi fwupd-2.0.10/contrib/meson.build000066400000000000000000000011711501337203100165140ustar00rootroot00000000000000subdir('firmware_packager') if get_option('qubes') subdir('qubes') endif con2 = configuration_data() con2.set('FWUPD_VERSION', fwupd_version) configure_file( input: 'fwupd.spec.in', output: 'fwupd.spec.in', configuration: con2, ) uswid = find_program('uswid', required: false) if uswid.found() custom_target('gen-sbom', input: 'sbom.cdx.json', output: 'sbom.cdx.json', command: [ uswid, '--load', '@INPUT@', '--save', '@OUTPUT@', ], ) endif if host_machine.system() == 'windows' configure_file( input: 'fwupd.wxs.in', output: 'fwupd.wxs', configuration: conf ) endif fwupd-2.0.10/contrib/migrate.py000077500000000000000000000231361501337203100163640ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2022 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later # # import os import sys import glob if __name__ == "__main__": fns = [] if len(sys.argv) > 1: fns.extend(sys.argv[1:]) else: exts = ["c", "h", "map"] for ext in exts: for fn in glob.glob(f"**/*.{ext}", recursive=True): if fn.startswith("build"): continue if fn.startswith("subprojects"): continue if fn.startswith(".git"): continue fns.append(fn) for fn in fns: modified: bool = False with open(fn) as f: buf = f.read() for old, new in { "fu_common_sum8": "fu_sum8", "fu_common_sum8_bytes": "fu_sum8_bytes", "fu_common_sum16": "fu_sum16", "fu_common_sum16_bytes": "fu_sum16_bytes", "fu_common_sum16w": "fu_sum16w", "fu_common_sum16w_bytes": "fu_sum16w_bytes", "fu_common_sum32": "fu_sum32", "fu_common_sum32_bytes": "fu_sum32_bytes", "fu_common_sum32w": "fu_sum32w", "fu_common_sum32w_bytes": "fu_sum32w_bytes", "fu_common_crc8": "fu_crc8", "fu_common_crc8_full": "fu_crc8", "fu_common_crc16": "fu_crc16", "fu_common_crc16_full": "fu_crc16", "fu_common_crc32": "fu_crc32", "fu_common_crc32_full": "fu_crc32", "fu_byte_array_set_size_full": "fu_byte_array_set_size", "fu_common_string_replace": "g_string_replace", "fu_common_string_append_kv": "fwupd_codec_string_append", "fu_common_string_append_ku": "fwupd_codec_string_append_int", "fu_common_string_append_kx": "fwupd_codec_string_append_hex", "fu_common_string_append_kb": "fwupd_codec_string_append_bool", "fu_common_strnsplit": "fu_strsplit", "fu_common_strnsplit_full": "fu_strsplit_full", "fu_common_strjoin_array": "fu_strjoin", "fu_common_strsafe": "fu_strsafe", "fu_common_strwidth": "fu_strwidth", "fu_common_strstrip": "fu_strstrip", "fu_common_strtoull": "fu_strtoull", "fu_common_strtoull_full": "fu_strtoull", "FuCommonStrsplitFunc": "FuStrsplitFunc", "fu_common_bytes_pad": "fu_bytes_pad", "fu_common_bytes_new_offset": "fu_bytes_new_offset", "fu_common_bytes_align": "fu_bytes_align", "fu_common_bytes_is_empty": "fu_bytes_is_empty", "fu_common_bytes_compare(": "fu_bytes_compare(", "fu_common_set_contents_bytes": "fu_bytes_set_contents", "fu_common_get_contents_bytes": "fu_bytes_get_contents", "fu_common_get_contents_stream": "fu_input_stream_read_bytes", "fu_common_read_uint8_safe": "fu_memread_uint8_safe", "fu_common_read_uint16_safe": "fu_memread_uint16_safe", "fu_common_read_uint32_safe": "fu_memread_uint32_safe", "fu_common_read_uint64_safe": "fu_memread_uint64_safe", "fu_common_write_uint8_safe": "fu_memwrite_uint8_safe", "fu_common_write_uint16_safe": "fu_memwrite_uint16_safe", "fu_common_write_uint32_safe": "fu_memwrite_uint32_safe", "fu_common_write_uint64_safe": "fu_memwrite_uint64_safe", "fu_common_write_uint16": "fu_memwrite_uint16", "fu_common_write_uint24": "fu_memwrite_uint24", "fu_common_write_uint32": "fu_memwrite_uint32", "fu_common_write_uint64": "fu_memwrite_uint64", "fu_common_read_uint16": "fu_memread_uint16", "fu_common_read_uint24": "fu_memread_uint24", "fu_common_read_uint32": "fu_memread_uint32", "fu_common_read_uint64": "fu_memread_uint64", "fu_common_bytes_compare_raw": "fu_memcmp_safe", "FuOutputHandler": "FuSpawnOutputHandler", "fu_common_kernel_locked_down": "fu_kernel_locked_down", "fu_common_check_kernel_version": "fu_kernel_check_version", "fu_common_get_firmware_search_path": "fu_kernel_search_path_locker_new", "fu_common_set_firmware_search_path": "fu_kernel_search_path_locker_new", "fu_common_reset_firmware_search_path": "fu_kernel_search_path_locker_new", "fu_common_firmware_builder": "fu_firmware_builder_process", "fu_common_uri_get_scheme": "fu_release_uri_get_scheme", "fu_common_dump_raw": "fu_dump_raw", "fu_common_dump_full": "fu_dump_full", "fu_common_dump_bytes": "fu_dump_bytes", "fu_common_error_array_get_best": "fu_engine_error_array_get_best", "fu_common_get_path": "fu_path_from_kind", "fu_common_filename_glob": "fu_path_glob", "fu_common_fnmatch": "g_pattern_match_simple", "fu_common_rmtree": "fu_path_rmtree", "fu_common_get_files_recursive": "fu_path_get_files", "fu_common_mkdir": "fu_path_mkdir", "fu_common_mkdir_parent": "fu_path_mkdir_parent", "fu_common_find_program_in_path": "fu_path_find_program", "fu_common_cpuid": "fu_cpuid", "fu_common_get_cpu_vendor": "fu_cpu_get_vendor", "fu_common_vercmp_full": "fu_version_compare", "fu_common_version_ensure_semver_full": "fu_version_ensure_semver", "fu_common_version_from_uint16": "fu_version_from_uint16", "fu_common_version_from_uint32": "fu_version_from_uint32", "fu_common_version_from_uint64": "fu_version_from_uint64", "fu_common_version_guess_format": "fu_version_guess_format", "fu_common_version_parse_from_format": "fu_version_parse_from_format", "fu_common_version_verify_format": "fu_version_verify_format", "fu_common_get_volumes_by_kind": "fu_volume_new_by_kind", "fu_common_get_volume_by_device": "fu_volume_new_by_device", "fu_common_get_volume_by_devnum": "fu_volume_new_by_devnum", "fu_common_get_esp_default": "fu_context_get_esp_volumes", "fu_smbios_to_string": "fu_firmware_to_string", "fu_i2c_device_read_full": "fu_i2c_device_read", "fu_i2c_device_write_full": "fu_i2c_device_write", "fu_path_fnmatch": "g_pattern_match_simple", "fu_string_replace": "g_string_replace", "fu_efi_firmware_decompress_lzma": "fu_lzma_decompress_bytes", "fu_device_build_instance_id_quirk": "fu_device_build_instance_id_full", "fwupd_bios_setting_array_from_variant": "fwupd_codec_from_variant", "fwupd_bios_setting_from_json": "fwupd_codec_from_json", "fwupd_bios_setting_from_variant": "fwupd_codec_from_variant", "fwupd_bios_setting_to_json": "fwupd_codec_to_json", "fwupd_bios_setting_to_string": "fwupd_codec_to_string", "fwupd_bios_setting_to_variant": "fwupd_codec_to_variant", "fwupd_device_array_from_variant": "fwupd_codec_from_variant", "fwupd_device_from_json": "fwupd_codec_from_json", "fwupd_device_from_variant": "fwupd_codec_from_variant", "fwupd_device_to_json_full": "fwupd_codec_to_json", "fwupd_device_to_json": "fwupd_codec_to_json", "fwupd_device_to_string": "fwupd_codec_to_string", "fwupd_device_to_variant_full": "fwupd_codec_to_variant", "fwupd_device_to_variant": "fwupd_codec_to_variant", "fwupd_plugin_array_from_variant": "fwupd_codec_from_variant", "fwupd_plugin_from_variant": "fwupd_codec_from_variant", "fwupd_plugin_to_json": "fwupd_codec_to_json", "fwupd_plugin_to_string": "fwupd_codec_to_string", "fwupd_plugin_to_variant": "fwupd_codec_to_variant", "fwupd_release_array_from_variant": "fwupd_codec_from_variant", "fwupd_release_from_variant": "fwupd_codec_from_variant", "fwupd_release_to_json": "fwupd_codec_to_json", "fwupd_release_to_string": "fwupd_codec_to_string", "fwupd_release_to_variant": "fwupd_codec_to_variant", "fwupd_remote_array_from_variant": "fwupd_codec_from_variant", "fwupd_remote_from_variant": "fwupd_codec_from_variant", "fwupd_remote_to_json": "fwupd_codec_to_json", "fwupd_remote_to_variant": "fwupd_codec_to_variant", "fwupd_report_from_variant": "fwupd_codec_from_variant", "fwupd_report_to_json": "fwupd_codec_to_json", "fwupd_report_to_string": "fwupd_codec_to_string", "fwupd_report_to_variant": "fwupd_codec_to_variant", "fwupd_request_from_variant": "fwupd_codec_from_variant", "fwupd_request_to_string": "fwupd_codec_to_string", "fwupd_request_to_variant": "fwupd_codec_to_variant", "fwupd_security_attr_array_from_variant": "fwupd_codec_from_variant", "fwupd_security_attr_from_json": "fwupd_codec_from_json", "fwupd_security_attr_from_variant": "fwupd_codec_from_variant", "fwupd_security_attr_to_json": "fwupd_codec_to_json", "fwupd_security_attr_to_string": "fwupd_codec_to_string", "fwupd_security_attr_to_variant": "fwupd_codec_to_variant", }.items(): if buf.find(old) == -1: continue buf = buf.replace(old, new) modified = True if modified: print(f"MODIFIED: {fn}") with open(fn, "w") as f: f.write(buf) sys.exit(0) fwupd-2.0.10/contrib/mingw64.cross000066400000000000000000000001161501337203100167160ustar00rootroot00000000000000[binaries] windmc = '/usr/bin/x86_64-w64-mingw32-windmc' exe_wrapper = 'wine' fwupd-2.0.10/contrib/minimize-emulation.py000077500000000000000000000030661501337203100205500ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2024 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later # # pylint: disable=invalid-name,missing-docstring import json import sys import hashlib import zipfile def _minimize_usb_events(data) -> None: for device in data["UsbDevices"]: try: for event in device.get("UsbEvents", []) + device.get("Events", []): if event["Id"].startswith("#"): continue event["Id"] = "#" + hashlib.sha1(event["Id"].encode()).hexdigest()[:8] except KeyError: pass def _minimize_json(fn: str) -> None: with open(fn, "rb") as f: data = json.loads(f.read()) _minimize_usb_events(data) with open(fn, "wb") as f: f.write(json.dumps(data, indent=2).encode()) def _minimize_zip(fn: str) -> None: files = {} with zipfile.ZipFile(fn) as myzip: for name in myzip.namelist(): with myzip.open(name) as f: data = json.loads(f.read()) _minimize_usb_events(data) files[name] = data fn_new = fn.replace(".zip", "-min.zip") with zipfile.ZipFile( fn_new, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9 ) as myzip: for name, data in files.items(): myzip.writestr(name, json.dumps(data, separators=(",", ":")).encode()) if __name__ == "__main__": for fn in sys.argv[1:]: if fn.endswith(".json"): _minimize_json(fn) if fn.endswith(".zip"): _minimize_zip(fn) fwupd-2.0.10/contrib/pcap2emulation.py000077500000000000000000001005321501337203100176530ustar00rootroot00000000000000#!/usr/bin/env python3 # pylint: disable=invalid-name,missing-docstring # # Copyright 2023 Collabora Ltd # Author: Frédéric Danis # # SPDX-License-Identifier: LGPL-2.1-or-later import argparse import base64 import json import os import subprocess import sys from typing import Any, Dict, List, Optional, Tuple from zipfile import ZipFile, ZIP_DEFLATED URB_INTERRUPT = 1 URB_CONTROL = 2 URB_BULK = 3 DESCRIPTOR_DEVICE = 1 DESCRIPTOR_CONFIGURATION = 2 DESCRIPTOR_STRING = 3 DESCRIPTOR_INTERFACE = 4 DESCRIPTOR_ENDPOINT = 5 DESCRIPTOR_EXTRA = 33 INTERFACE_CLASS_HID = 3 INTERFACE_CLASS_SMARTCARD = 11 CCID_PC_TO_RDR_SET_PARAMETERS = 0x61 CCID_PC_TO_RDR_ICC_POWER_ON = 0x62 CCID_PC_TO_RDR_ICC_POWER_OFF = 0x63 CCID_PC_TO_RDR_GET_SLOT_STATUS = 0x65 CCID_PC_TO_RDR_ESCAPE = 0x6B CCID_PC_TO_RDR_TRANSFER_BLOCK = 0x6F CCID_RDR_TO_PC_DATA_BLOCK = 0x80 CCID_RDR_TO_PC_SLOT_STATUS = 0x81 CCID_RDR_TO_PC_PARAMETERS = 0x82 CCID_RDR_TO_PC_ESCAPE = 0x83 def get_int(data: str) -> int: if data[:2] == "0x": return int(data, 16) return int(data) def add_bytes(array: bytearray, string: str, size: int) -> None: array += get_int(string).to_bytes(length=size, byteorder="little") class Pcap2Emulation: def __init__(self, device_ids: str): self.device: Dict[str, Any] = {} self.platform_id = "" self.phases: List[Any] = [] self.device_ids: List[List[str]] = [] self.interface_index = 0 self.endpoint_index = 0 self.previous_data: Optional[str] self.bulk_incoming_lens: Dict[str, int] = {} self.usb_port = None self.enumerate = False for i in range(len(device_ids)): device_id = device_ids[i].split(":") if len(device_id) > 2: sys.stderr.write(f"Malformed device ID: {device_ids[i]}\n\n") exit(1) if len(device_id) == 2 and len(device_id[1]) == 0: del device_id[1] if device_id not in self.device_ids: self.device_ids.append(device_id) def _save_phase(self) -> None: self.phases.append({"UsbDevices": [self.device]}) self.interface_index = 0 self.endpoint_index = 0 def save_archive(self, path: str) -> None: if not self.phases: return print(f"Found {len(self.phases)} phases:") phase = 0 if path.endswith(".zip"): emulation_file = path else: emulation_file = path + ".zip" with ZipFile(emulation_file, "w", compression=ZIP_DEFLATED) as write_file: print(f"- phase {phase} as setup.json") json_string = json.dumps( self.phases[phase], indent=2, separators=(",", " : ") ) write_file.writestr("setup.json", json_string) phase += 1 if len(self.phases) > 2: print(f"- phase {phase} as install.json") json_string = json.dumps( self.phases[phase], indent=2, separators=(",", " : ") ) write_file.writestr("install.json", json_string) phase += 1 print(f"- phase {phase} as reload.json") json_string = json.dumps( self.phases[phase], indent=2, separators=(",", " : ") ) write_file.writestr("reload.json", json_string) phase += 1 print("Emulation file saved to " + emulation_file) while phase < len(self.phases): phase_path = f"{path}-{phase}.json" with open(phase_path, "w") as dump_file: json.dump( self.phases[phase], dump_file, indent=2, separators=(",", " : "), ) print(f"- unused phase {phase} saved to {phase_path}") phase += 1 def _run_tshark(self, file: str, tshark_filter: str) -> Any: cmd = ["tshark", "-n", "-T", "ek", "-l", "-2", "-r", file, "-R"] print("running: " + " ".join(cmd) + ' "' + tshark_filter + '"') cmd.append(tshark_filter) return subprocess.Popen(cmd, stdout=subprocess.PIPE) def _get_usb_addrs(self, file: str) -> Tuple[str, List[str]]: tshark_filter = "" for i in range(len(self.device_ids)): if len(tshark_filter) == 0: tshark_filter += "(" else: tshark_filter += " or (" tshark_filter += "usb.idVendor == 0x" + self.device_ids[i][0] if len(self.device_ids[i]) == 2 and len(self.device_ids[i][1]) > 0: tshark_filter += " and usb.idProduct == 0x" + self.device_ids[i][1] tshark_filter += ")" usb_bus = "" usb_addrs: List[str] = [] p = self._run_tshark(file, tshark_filter) for line in p.stdout: pcap_data = json.loads(line) if "layers" in pcap_data: if not usb_bus: usb_bus = pcap_data["layers"]["usb"]["usb_usb_bus_id"] elif usb_bus != pcap_data["layers"]["usb"]["usb_usb_bus_id"]: print( "* Warning: Found different USB Bus ID: expected {}, found {}".format( usb_bus, pcap_data["layers"]["usb"]["usb_usb_bus_id"] ) ) addr = pcap_data["layers"]["usb"]["usb_usb_device_address"] if addr not in usb_addrs: usb_addrs.append(addr) return usb_bus, usb_addrs def _get_interrupt_event(self, layers: Dict[str, Any]) -> Dict[str, str]: if "usb_usb_capdata" in layers: captured_data = str( base64.b64encode( bytes.fromhex(layers["usb_usb_capdata"].replace(":", "")) ), "utf-8", ) s = "InterruptTransfer:Endpoint=0x{:02x}".format( get_int(layers["usb"]["usb_usb_endpoint_address"]) ) if layers["usb"]["usb_usb_endpoint_address_direction"] == "1": if hasattr(self, "previous_data") and self.previous_data: s += f",Data={self.previous_data}" self.previous_data = None else: s += ",Data=" else: self.previous_data = captured_data s += f",Data={captured_data}" s += f",Length=0x{int(layers['usb']['usb_usb_data_len']):x}" return {"Id": s, "Data": captured_data} return {} def _get_bulk_event(self, layers: Dict[str, Any]) -> Dict[str, str]: captured_data = None if "usbccid" in layers: message_type = get_int(layers["usbccid"]["usbccid_usbccid_bMessageType"]) ccid = bytearray([message_type]) add_bytes(ccid, layers["usbccid"]["usbccid_usbccid_dwLength"], 4) add_bytes(ccid, layers["usbccid"]["usbccid_usbccid_bSlot"], 1) add_bytes(ccid, layers["usbccid"]["usbccid_usbccid_bSeq"], 1) if message_type == CCID_PC_TO_RDR_SET_PARAMETERS: add_bytes(ccid, layers["usbccid"]["usbccid_usbccid_bProtocolNum"], 1) add_bytes( ccid, layers["usbccid"]["usbccid_usbccid_hf_ccid_Reserved"], 2 ) ccid += bytearray.fromhex( layers["data"]["data_data_data"].replace(":", "") ) elif message_type == CCID_PC_TO_RDR_ICC_POWER_ON: add_bytes(ccid, layers["usbccid"]["usbccid_usbccid_bPowerSelect"], 1) add_bytes( ccid, layers["usbccid"]["usbccid_usbccid_hf_ccid_Reserved"], 2 ) elif ( message_type == CCID_PC_TO_RDR_ICC_POWER_OFF or message_type == CCID_PC_TO_RDR_GET_SLOT_STATUS ): add_bytes( ccid, layers["usbccid"]["usbccid_usbccid_hf_ccid_Reserved"], 3 ) elif message_type == CCID_PC_TO_RDR_ESCAPE: ccid += bytearray.fromhex( layers["usbccid"]["usbccid_usbccid_abRFU"].replace(":", "") ) ccid += bytearray.fromhex( layers["data"]["data_data_data"].replace(":", "") ) elif message_type == CCID_PC_TO_RDR_TRANSFER_BLOCK: add_bytes(ccid, layers["usbccid"]["usbccid_usbccid_bBWI"], 1) add_bytes(ccid, layers["usbccid"]["usbccid_usbccid_wLevelParameter"], 2) ccid += bytearray.fromhex( layers["data"]["data_data_data"].replace(":", "") ) elif message_type == CCID_RDR_TO_PC_DATA_BLOCK: add_bytes(ccid, layers["usbccid"]["usbccid_usbccid_bStatus"], 1) add_bytes(ccid, layers["usbccid"]["usbccid_usbccid_bError"], 1) add_bytes(ccid, layers["usbccid"]["usbccid_usbccid_bChainParameter"], 1) if "data" in layers: ccid += bytearray.fromhex( layers["data"]["data_data_data"].replace(":", "") ) elif message_type == CCID_RDR_TO_PC_SLOT_STATUS: add_bytes(ccid, layers["usbccid"]["usbccid_usbccid_bStatus"], 1) add_bytes(ccid, layers["usbccid"]["usbccid_usbccid_bError"], 1) add_bytes(ccid, layers["usbccid"]["usbccid_usbccid_bClockStatus"], 1) elif message_type == CCID_RDR_TO_PC_PARAMETERS: add_bytes(ccid, layers["usbccid"]["usbccid_usbccid_bStatus"], 1) add_bytes(ccid, layers["usbccid"]["usbccid_usbccid_bError"], 1) add_bytes(ccid, layers["usbccid"]["usbccid_usbccid_bProtocolNum"], 1) elif message_type == CCID_RDR_TO_PC_ESCAPE: add_bytes(ccid, layers["usbccid"]["usbccid_usbccid_bStatus"], 1) add_bytes(ccid, layers["usbccid"]["usbccid_usbccid_bError"], 1) add_bytes(ccid, layers["usbccid"]["usbccid_usbccid_bRFU"], 1) ccid += bytearray.fromhex( layers["data"]["data_data_data"].replace(":", "") ) else: print(f"Unknown USB CCID Bulk message type: 0x{message_type:02X}") captured_data = str(base64.b64encode(ccid), "utf-8") elif "usb_usb_capdata" in layers: captured_data = str( base64.b64encode( bytes.fromhex(layers["usb_usb_capdata"].replace(":", "")) ), "utf-8", ) if captured_data: s = "BulkTransfer:Endpoint=0x{:02x}".format( get_int(layers["usb"]["usb_usb_endpoint_address"]) ) if layers["usb"]["usb_usb_endpoint_address_direction"] == "1": if layers["usb"]["usb_usb_request_in"] in self.bulk_incoming_lens: length = self.bulk_incoming_lens[ layers["usb"]["usb_usb_request_in"] ] else: length = get_int(layers["usb"]["usb_usb_data_len"]) data = str( base64.b64encode(bytes.fromhex("00" * length)), "utf-8", ) s += f",Data={data}" else: length = get_int(layers["usb"]["usb_usb_data_len"]) s += f",Data={captured_data}" s += f",Length=0x{length:x}" return {"Id": s, "Data": captured_data} elif layers["usb"]["usb_usb_endpoint_address_direction"] == "1": if "usb_usb_urb_len" in layers["usb"]: self.bulk_incoming_lens[ layers["frame"]["frame_frame_number"] ] = get_int(layers["usb"]["usb_usb_urb_len"]) return {} def _get_interface_descriptor( self, layers: Dict[str, Any], descriptor_index: int ) -> Any: table = { "usb_usb_bInterfaceNumber": "InterfaceNumber", "usb_usb_bInterfaceClass": "InterfaceClass", "usb_usb_bInterfaceSubClass": "InterfaceSubClass", "usb_usb_bInterfaceProtocol": "InterfaceProtocol", "usb_usb_iInterface": "Interface", "usb_usb_bNumEndpoints": "NumEndpoints", } # Interface descriptor frame may occur multiple times, # it should not change unless device has been re-enumerated if len(layers["usb_usb_bInterfaceNumber"]) <= self.interface_index: return None interface: Dict[str, Any] = { "Length": get_int(layers["usb_usb_bLength"][descriptor_index]), "DescriptorType": 4, } for key in table: # data can be a string or a list of strings if type(layers[key]) is str: val = get_int(layers[key]) else: val = get_int(layers[key][self.interface_index]) if key not in layers: continue if key == "usb_usb_bInterfaceNumber" and val == 0: continue interface[table[key]] = val if key == "usb_usb_bNumEndpoints": interface["UsbEndpoints"] = [] return interface def _get_endpoint_descriptor( self, layers: Dict[str, Any], index: int ) -> Dict[str, int]: table = { "usb_usb_bEndpointAddress": "EndpointAddress", "usb_usb_bInterval": "Interval", "usb_usb_wMaxPacketSize": "MaxPacketSize", } endpoint = { "DescriptorType": 5, } for key in table: val = get_int(layers[key][index]) if val != 0: endpoint[table[key]] = val return endpoint def _save_event(self, event: Dict[str, str]) -> None: if not self.device: return self.device["UsbEvents"].append(event) def parse_file(self, file: str) -> None: bus_id, addrs = self._get_usb_addrs(file) if len(addrs) == 0: print("Device(s) not found in pcap file") return # Filter the device related packets and the C_PORT_CONNECTION clear # feature packets to allow detection of the re-plug/re-enumerate events tshark_filter = f"usb.bus_id == {bus_id}" tshark_filter += " and (usbhub.setup.PortFeatureSelector == 16" for addr in addrs: tshark_filter += f" or usb.device_address == {addr}" tshark_filter += ")" p = self._run_tshark(file, tshark_filter) for line in p.stdout: pcap_data = json.loads(line) if "layers" in pcap_data: layers = pcap_data["layers"] usb_port = None if ( layers["frame"]["frame_frame_cap_len"] != layers["frame"]["frame_frame_len"] ): print( "* Incomplete frame {}: {} bytes captured < {}".format( layers["frame"]["frame_frame_number"], layers["frame"]["frame_frame_cap_len"], layers["frame"]["frame_frame_len"], ) ) # Store the USB port of C_PORT_CONNECTION clear feature packet # Trigger an enumeration requirement if it's the same port that # was used for the previous device description if "usbhub_usbhub_setup_Port" in layers: usb_port = layers["usbhub_usbhub_setup_Port"] if usb_port == self.usb_port: self.enumerate = True if get_int(layers["usb"]["usb_usb_transfer_type"]) == URB_INTERRUPT: event = self._get_interrupt_event(layers) if len(event) > 0: self._save_event(event) elif get_int(layers["usb"]["usb_usb_transfer_type"]) == URB_CONTROL: if "usb_usb_bDescriptorType" in layers: descriptor_index = -1 # usb_usb_bDescriptorType can be a string or a list of strings if type(layers["usb_usb_bDescriptorType"]) is str: layer = [layers["usb_usb_bDescriptorType"]] else: layer = layers["usb_usb_bDescriptorType"] for descriptor_type in layer: descriptor_index += 1 descriptor_type = get_int(descriptor_type) if descriptor_type == DESCRIPTOR_DEVICE: # Check this is a reply for the Vid[:Pid] expected if layers["usb"]["usb_usb_src"] == "host": continue found = False for i in range(len(self.device_ids)): if get_int(layers["usb_usb_idVendor"]) == int( self.device_ids[i][0], 16 ): if len(self.device_ids[i]) == 1 or get_int( layers["usb_usb_idProduct"] ) == int(self.device_ids[i][1], 16): found = True break if not found: continue # Save the previous USB device if ( self.device and len(self.device["UsbInterfaces"]) > 0 ): self._save_phase() # Create a new USB device # using a fake PlatformId based on USB bus id and device address, # this PlatformId should be stable for all recorded devices if not self.platform_id: self.platform_id = "{:x}-{:x}".format( get_int(layers["usb"]["usb_usb_bus_id"]), get_int( layers["usb"]["usb_usb_device_address"] ), ) # Device re-enumeration is triggered when 'Created' time differs # from previous phase, this keeps the 'Created' time from previous # phase unless a re-enumeration requirement has been detected if not self.device or self.enumerate: frame_time = layers["frame"]["frame_frame_time"] self.usb_port = usb_port self.enumerate = False else: frame_time = self.device["Created"] self.device = { "GType": "FuUsbDevice", "PlatformId": self.platform_id, "Created": frame_time, "IdVendor": get_int(layers["usb_usb_idVendor"]), "IdProduct": get_int(layers["usb_usb_idProduct"]), "Device": get_int(layers["usb_usb_bcdDevice"]), "USB": get_int(layers["usb_usb_bcdUSB"]), "Manufacturer": get_int( layers["usb_usb_iManufacturer"] ), "Product": get_int(layers["usb_usb_iProduct"]), "UsbInterfaces": [], "UsbEvents": [], } elif descriptor_type == DESCRIPTOR_CONFIGURATION: if "usb_usb_iConfiguration" in layers: # The GetConfigurationIndex USB event is not directly # related to a specific USB event, but data can be # retrieved from the DESCRIPTOR CONFIGURATION request index = ( layers["usb_usb_iConfiguration"] .encode("utf-8") .hex() ) event = { "Id": "GetConfigurationIndex", "Data": str( base64.b64encode(bytes.fromhex(index)), "utf-8", ), } self._save_event(event) elif descriptor_type == DESCRIPTOR_STRING: if "usb_usb_DescriptorIndex" in layers: desc_index = get_int( layers["usb_usb_DescriptorIndex"] ) if desc_index == 0: # The list of supported languages are not recorded for the emulation continue event_str = { "Id": "GetStringDescriptor:DescIndex=0x{:02x}".format( desc_index ) } # duplicate the event so it can also be used for GetStringDescriptorBytes language_id = get_int(layers["usb_usb_LanguageId"]) length = get_int(layers["usb_usb_setup_wLength"]) event_bytes = { "Id": "GetStringDescriptorBytes:DescIndex=0x{:02x}".format( desc_index ) } event_bytes["Id"] += f",Langid=0x{language_id:04x}" event_bytes["Id"] += f",Length=0x{length:x}" elif "usb_usb_bString" in layers: if get_int(layers["usb_usb_bLength"]) != len( layers["usb_usb_bString"].encode("utf-16") ): # Discard frame used to retrieve STRING DESCRIPTOR length continue # Found a new STRING DESCRIPTOR response data = ( layers["usb_usb_bString"].encode("utf-8").hex() ) if "usb_usb_capdata" in layers: # Add leftover capture data data += "00" data += layers["usb_usb_capdata"].replace( ":", "" ) event_str["Data"] = str( base64.b64encode(bytes.fromhex(data)), "utf-8" ) # now that the event is completed it can be added to the device events self._save_event(event_str) # duplicate the event so it can also be used for GetStringDescriptorBytes event_bytes["Data"] = event_str["Data"] self._save_event(event_bytes) elif descriptor_type == DESCRIPTOR_INTERFACE: interface = self._get_interface_descriptor( layers, descriptor_index ) if not interface: continue # Add the interface to the device self.device["UsbInterfaces"].append(interface) if ( "InterfaceClass" in interface and "InterfaceSubClass" in interface and "InterfaceProtocol" in interface ): # The GetCustomIndex USB event is not directly # related to a specific USB event, but data can be # retrieved from the DESCRIPTOR INTERFACE request index = interface["Interface"].to_bytes(1, "big") event = { "Id": "GetCustomIndex:ClassId=0x{:02x},SubclassId=0x{:02x},ProtocolId=0x{:02x}".format( interface["InterfaceClass"], interface["InterfaceSubClass"], interface["InterfaceProtocol"], ), "Data": str( base64.b64encode(index), "utf-8", ), } self._save_event(event) self.interface_index += 1 elif descriptor_type == DESCRIPTOR_ENDPOINT: if ( len(layers["usb_usb_bEndpointAddress"]) > self.endpoint_index ): endpoint = self._get_endpoint_descriptor( layers, self.endpoint_index ) # Add the endpoint to the first interface with missing endpoint for interface in self.device["UsbInterfaces"]: if ( "UsbEndpoints" in interface and len(interface["UsbEndpoints"]) < interface["NumEndpoints"] ): interface["UsbEndpoints"].append(endpoint) break self.endpoint_index += 1 elif descriptor_type == DESCRIPTOR_EXTRA: pass else: sys.stderr.write( "Unknown descriptor type: " + descriptor_type ) exit(1) elif "usb_usb_bmRequestType_type" in layers: # Found vendor CONTROL URB request direction = not (layers["usb_usb_bmRequestType_direction"]) s = f"ControlTransfer:Direction=0x{direction:02x}" s += ",RequestType=0x{:02x}".format( get_int(layers["usb_usb_bmRequestType_type"]) ) s += ",Recipient=0x{:02x}".format( get_int(layers["usb_usb_bmRequestType_recipient"]) ) if "usbhid_usbhid_setup_bRequest" in layers: s += f",Request=0x{get_int(layers['usbhid_usbhid_setup_bRequest']):02x}" elif "usb_usb_setup_bRequest" in layers: s += f",Request=0x{get_int(layers['usb_usb_setup_bRequest']):02x}" if ( "usbhid_usbhid_setup_ReportID" in layers and "usbhid_usbhid_setup_ReportType" in layers ): rep_id = get_int(layers["usbhid_usbhid_setup_ReportID"]) typ = get_int(layers["usbhid_usbhid_setup_ReportType"]) val = typ << 8 | rep_id s += f",Value=0x{val:04x}" elif "usb_usb_setup_wValue" in layers: s += f",Value=0x{get_int(layers['usb_usb_setup_wValue']):04x}" if "usbhid_usbhid_setup_wIndex" in layers: s += f",Idx=0x{get_int(layers['usbhid_usbhid_setup_wIndex']):04x}" elif "usb_usb_setup_wIndex" in layers: s += f",Idx=0x{get_int(layers['usb_usb_setup_wIndex']):04x}" if "usbhid_usbhid_setup_wLength" in layers: length = get_int(layers["usbhid_usbhid_setup_wLength"]) elif "usb_usb_setup_wLength" in layers: length = get_int(layers["usb_usb_setup_wLength"]) else: length = 0 if "usb_usb_data_fragment" in layers: data = layers["usb_usb_data_fragment"].replace(":", "") else: data = "00" * length s += ",Data={}".format( str(base64.b64encode(bytes.fromhex(data)), "utf-8") ) s += f",Length=0x{length:x}" event = {"Id": s} if direction: # Duplicate the outgoing data as response and add the event to the device events event["Data"] = str( base64.b64encode(bytes.fromhex(data)), "utf-8" ) self._save_event(event) elif "usb_usb_control_Response" in layers: # Found CONTROL URB response data = layers["usb_usb_control_Response"].replace(":", "") event["Data"] = str( base64.b64encode(bytes.fromhex(data)), "utf-8" ) # now that the event is complete it can be added to the device events self._save_event(event) elif get_int(layers["usb"]["usb_usb_transfer_type"]) == URB_BULK: # TODO: check it event = self._get_bulk_event(layers) if event: self._save_event(event) else: if not self.device: continue print( "Unknown frame type: " + layers["usb"]["usb_usb_transfer_type"] ) # Save the last USB device if "UsbInterfaces" in self.device: self._save_phase() if __name__ == "__main__": options = argparse.ArgumentParser(description="Convert pcap file to emulation file") options.add_argument("input_pcap", type=str, help="pcap file to convert") options.add_argument("output_archive", type=str, help="Output archive path") options.add_argument( "device_id", metavar=("VendorID[:ProductID]"), type=str, nargs="+", help="Device ID in hexadecimal", ) args = options.parse_args() path = os.path.abspath(os.path.expanduser(os.path.expandvars(args.input_pcap))) parser = Pcap2Emulation(args.device_id) parser.parse_file(path) parser.save_archive(args.output_archive) fwupd-2.0.10/contrib/qubes/000077500000000000000000000000001501337203100154715ustar00rootroot00000000000000fwupd-2.0.10/contrib/qubes/README.md000066400000000000000000000162501501337203100167540ustar00rootroot00000000000000# qubes-fwupd fwupd wrapper for QubesOS ## Table of Contents * [Requirements](#requirements) * [Usage](#usage) * [Installation](#installation) * [Testing](#testing) * [Whonix support](doc/whonix.md) * [UEFI capsule update](doc/uefi_capsule_update.md) * [Heads update](doc/heads_update.md) ## Requirements **Operating System:** Qubes OS R4.1 **Admin VM (dom0):** Fedora 32 **Template VM:** Fedora 32 **Whonix VM:** whonix-gw-15 ## Usage ```text ========================================================================================== Usage: ========================================================================================== Command: qubes-fwupdmgr [OPTION…][FLAG..] Example: qubes-fwupdmgr refresh --whonix --url= Options: ========================================================================================== get-devices: Get all devices that support firmware updates get-updates: Get the list of updates for connected hardware refresh: Refresh metadata from remote server update: Update chosen device to latest firmware version update-heads: Updates heads firmware to the latest version downgrade: Downgrade chosen device to chosen firmware version clean: Delete all cached update files Flags: ========================================================================================== --whonix: Download firmware updates via Tor --device: Specify device for heads update (default - x230) --url: Address of the custom metadata remote server Help: ========================================================================================== -h --help: Show help options ``` ## Installation For development purpose: * Build the package for fedora and debian as it is shown in the contrib [README](../README.md). * The build artifacts are placed in `dist` directory: -- dom0 package - `dist/fwupd-qubes-dom0--0.1alpha.fc32.x86_64.rpm` -- vm package - `dist/fwupd-qubes-vm--0.1alpha.fc32.x86_64.rpm` -- whonix package - `dist/fwupd-qubes-vm-whonix-_amd64.deb` * Copy packages to the Qubes OS. * Move the `fwupd-qubes-vm--0.1alpha.fc32.x86_64.rpm` to the Fedora 32 template VM (replace `` with the current version) ```shell qvm-copy fwupd-qubes-vm--0.1alpha.fc32.x86_64.rpm ``` * Install package dependencies ```shell # dnf install fwupd ``` * Run terminal in the template VM and go to `~/QubesIncoming/`. Compare SHA sums of the package in TemplateVM and qubes-builder VM. If they match, install the package: ```shell # rpm -U fwupd-qubes-vm--0.1alpha.fc32.x86_64.rpm ``` * Shutdown TemplateVM * Run whonix-gw-15 and copy whonix a package from qubes builder VM ```shell qvm-copy fwupd-qubes-vm-whonix-_amd64.deb ``` * Install dependencies ```shell # apt install fwupd ``` * Run terminal in the whonix-gw-15 and go to `~/QubesIncoming/qubes-builder`. Compare SHA sums of the package in TemplateVM and qubes-builder VM. If they match, install the package: ```shell # dpkg -i fwupd-qubes-vm-whonix-_amd64.deb ``` * Shutdown whonix-gw-15 * Run dom0 terminal in the dom0 and copy package: ```shell $ qvm-run --pass-io \ 'cat /qubes-src/fwupd/pkgs/dom0-fc32/x86_64/fwupd-qubes-dom0--0.1alpha.fc32.x86_64.rpm' > \ fwupd-qubes-dom0--0.1alpha.fc32.x86_64.rpm ``` * Install package dependencies: ```shell # qubes-dom0-update fwupd python36 ``` * Make sure that sys-firewall and sys-whonix are running. * Compare the SHA sums of the package in dom0 and qubes-builder VM. If they match, install the package: ```shell # rpm -U qubes-fwupd-dom0-0.2.0-1.fc32.x86_64.rpm ``` * Reboot system (or reboot sys-firewall and sys-whonix) * Run the tests to verify the installation process ## Testing ### Outside the Qubes OS A test case covers the whole qubes_fwupdmgr script. It could be run outside the Qubes OS. If the requirements of a single test are not met, it will be omitted. To run the tests, move to the repo directory and type the following: ```shell $ python3 -m unittest -v test.test_qubes_fwupdmgr test_clean_cache (test.test_qubes_fwupdmgr.TestQubesFwupdmgr) ... ok test_downgrade_firmware (test.test_qubes_fwupdmgr.TestQubesFwupdmgr) ... skipped 'Required device not connected' test_download_firmware_updates (test.test_qubes_fwupdmgr.TestQubesFwupdmgr) ... skipped 'requires Qubes OS' test_download_metadata (test.test_qubes_fwupdmgr.TestQubesFwupdmgr) ... skipped 'requires Qubes OS' test_get_devices (test.test_qubes_fwupdmgr.TestQubesFwupdmgr) ... skipped 'requires Qubes OS' test_get_devices_qubes (test.test_qubes_fwupdmgr.TestQubesFwupdmgr) ... skipped 'requires Qubes OS' test_get_updates (test.test_qubes_fwupdmgr.TestQubesFwupdmgr) ... skipped 'requires Qubes OS' test_get_updates_qubes (test.test_qubes_fwupdmgr.TestQubesFwupdmgr) ... skipped 'requires Qubes OS' test_help (test.test_qubes_fwupdmgr.TestQubesFwupdmgr) ... ok test_output_crawler (test.test_qubes_fwupdmgr.TestQubesFwupdmgr) ... ok test_parse_downgrades (test.test_qubes_fwupdmgr.TestQubesFwupdmgr) ... ok test_parse_parameters (test.test_qubes_fwupdmgr.TestQubesFwupdmgr) ... ok test_parse_updates_info (test.test_qubes_fwupdmgr.TestQubesFwupdmgr) ... ok test_refresh_metadata (test.test_qubes_fwupdmgr.TestQubesFwupdmgr) ... skipped 'requires Qubes OS' test_user_input_choice (test.test_qubes_fwupdmgr.TestQubesFwupdmgr) ... ok test_user_input_downgrade (test.test_qubes_fwupdmgr.TestQubesFwupdmgr) ... ok test_user_input_empty_list (test.test_qubes_fwupdmgr.TestQubesFwupdmgr) ... ok test_user_input_n (test.test_qubes_fwupdmgr.TestQubesFwupdmgr) ... ok test_verify_dmi (test.test_qubes_fwupdmgr.TestQubesFwupdmgr) ... ok test_verify_dmi_argument_version (test.test_qubes_fwupdmgr.TestQubesFwupdmgr) ... ok test_verify_dmi_version (test.test_qubes_fwupdmgr.TestQubesFwupdmgr) ... ok test_verify_dmi_wrong_vendor (test.test_qubes_fwupdmgr.TestQubesFwupdmgr) ... ok ---------------------------------------------------------------------- Ran 22 tests in 0.003s OK (skipped=8) ``` ### In the Qubes OS In the dom0, move to: ```shell cd /usr/share/qubes-fwupd/ ``` #### Qubes OS 4.1 Run the tests with sudo privileges: ```shell # python3 -m unittest -v test.test_qubes_fwupdmgr ``` Note: If the whonix tests failed, make sure that you are connected to the Tor ## Whonix support ```shell # qubes-fwupdmgr [refresh/update/downgrade] --whonix [FLAG] ``` More specified information you will find in the [whonix documentation](doc/whonix.md). ## UEFI capsule update ```shell # qubes-fwupdmgr [update/downgrade] ``` Requirements and more specified information you will find in the [UEFI capsule update documentation](doc/uefi_capsule_update.md). ## Heads update ```shell # qubes-fwupdmgr update-heads --device=x230 --url= ``` Requirements and more specified information you will find in the [heads update documentation](doc/heads_update.md). fwupd-2.0.10/contrib/qubes/doc/000077500000000000000000000000001501337203100162365ustar00rootroot00000000000000fwupd-2.0.10/contrib/qubes/doc/heads_update.md000066400000000000000000000026721501337203100212150ustar00rootroot00000000000000# Heads update The Heads update was tested on the `Lenovo ThinkPad x230`. ## Requirements You need to build and flash Heads ROM from the [3mdeb fork](https://github.com/3mdeb/heads/tree/qubes-fwupd). You will find there Heads ROMs for ThinkPad x230. ## Update process ThinkPad x230 is now the only laptop that has Heads ROM in the custom LVFS storage. Nevertheless, qubes-fwupd has already implemented a `device` flag, that will allow updates for other hardware. At first run the qubes-fwupd Heads update. ```shell sudo qubes-fwupdmgr update-heads --device=x230 ``` Press Y to reboot the device. In the main menu, choose `options` and then go to `Flash/Update the BIOS` ![img](img/heads_options.jpg) Decide to retain or erase the settings. ![img](img/heads_firmware_management_menu.jpg) The tool will inform you that heads update has been detected in `/boot` directory. If you will decide not to update, you will be asked to attach the USB drive. ![img](img/heads_detected.jpg) Select a ROM file. ![img](img/heads_selecting_rom.jpg) Press yes to confirm the choice. The Heads update will begin. ![img](img/heads_flash_rom.jpg) Wait until the end of the update process. ![img](img/heads_update_process.jpg) Press OK to reboot the system. ![img](img/heads_success.jpg) ## Test Change directory to `/usr/share/qubes-fwupd` and run test case with sudo privileges. ### Qubes OS R4.1 ```shell # python3 -m unittest -v test.test_qubes_fwupd_heads fwupd-2.0.10/contrib/qubes/doc/img/000077500000000000000000000000001501337203100170125ustar00rootroot00000000000000fwupd-2.0.10/contrib/qubes/doc/img/heads_detected.jpg000066400000000000000000003060111501337203100224420ustar00rootroot00000000000000ÿØÿâ øICC_PROFILE èmntrRGB XYZ Ù$acspöÖÓ-)ø=Þ¯òU®xBúäʃ9 descDybXYZÀbTRCÔ dmdd àˆgXYZ hgTRCÔ lumi |meas $bkpt ´rXYZ ÈrTRCÔ tech Ü vued è‡wtpt pcprt „7chad ¼,descsRGB IEC61966-2-1 black scaledXYZ $ „¶Ïcurv #(-27;@EJOTY^chmrw|†‹•šŸ¤©®²·¼ÁÆËÐÕÛàåëðöû %+28>ELRY`gnu|ƒ‹’š¡©±¹ÁÉÑÙáéòú &/8AKT]gqz„Ž˜¢¬¶ÁËÕàëõ !-8COZfr~Š–¢®ºÇÓàìù -;HUcq~Œš¨¶ÄÓáðþ +:IXgw†–¦µÅÕåö'7HYj{Œ¯ÀÑãõ+=Oat†™¬¿Òåø 2FZn‚–ª¾Òçû  % : O d y ¤ º Ï å û  ' = T j ˜ ® Å Ü ó " 9 Q i € ˜ ° È á ù  * C \ u Ž § À Ù ó & @ Z t Ž © Ã Þ ø.Id›¶Òî %A^z–³Ïì &Ca~›¹×õ1OmŒªÉè&Ed„£Ãã#Ccƒ¤Åå'Ij‹­Îð4Vx›½à&Il²ÖúAe‰®Ò÷@eНÕú Ek‘·Ý*QwžÅì;cвÚ*R{£ÌõGp™Ãì@j”¾é>i”¿ê  A l ˜ Ä ð!!H!u!¡!Î!û"'"U"‚"¯"Ý# #8#f#”#Â#ð$$M$|$«$Ú% %8%h%—%Ç%÷&'&W&‡&·&è''I'z'«'Ü( (?(q(¢(Ô))8)k))Ð**5*h*›*Ï++6+i++Ñ,,9,n,¢,×- -A-v-«-á..L.‚.·.î/$/Z/‘/Ç/þ050l0¤0Û11J1‚1º1ò2*2c2›2Ô3 3F33¸3ñ4+4e4ž4Ø55M5‡5Â5ý676r6®6é7$7`7œ7×88P8Œ8È99B99¼9ù:6:t:²:ï;-;k;ª;è<' >`> >à?!?a?¢?â@#@d@¦@çA)AjA¬AîB0BrBµB÷C:C}CÀDDGDŠDÎEEUEšEÞF"FgF«FðG5G{GÀHHKH‘H×IIcI©IðJ7J}JÄK KSKšKâL*LrLºMMJM“MÜN%NnN·OOIO“OÝP'PqP»QQPQ›QæR1R|RÇSS_SªSöTBTTÛU(UuUÂVV\V©V÷WDW’WàX/X}XËYYiY¸ZZVZ¦Zõ[E[•[å\5\†\Ö]']x]É^^l^½__a_³``W`ª`üaOa¢aõbIbœbðcCc—cëd@d”dée=e’eçf=f’fèg=g“géh?h–hìiCišiñjHjŸj÷kOk§kÿlWl¯mm`m¹nnknÄooxoÑp+p†pàq:q•qðrKr¦ss]s¸ttptÌu(u…uáv>v›vøwVw³xxnxÌy*y‰yçzFz¥{{c{Â|!||á}A}¡~~b~Â#„å€G€¨ kÍ‚0‚’‚ôƒWƒº„„€„ã…G…«††r†×‡;‡ŸˆˆiˆÎ‰3‰™‰þŠdŠÊ‹0‹–‹üŒcŒÊ1˜ÿŽfŽÎ6žnÖ‘?‘¨’’z’ã“M“¶” ”Š”ô•_•É–4–Ÿ— —u—à˜L˜¸™$™™üšhšÕ›B›¯œœ‰œ÷dÒž@ž®ŸŸ‹Ÿú i Ø¡G¡¶¢&¢–££v£æ¤V¤Ç¥8¥©¦¦‹¦ý§n§à¨R¨Ä©7©©ªª««u«é¬\¬Ð­D­¸®-®¡¯¯‹°°u°ê±`±Ö²K²Â³8³®´%´œµµŠ¶¶y¶ð·h·à¸Y¸Ñ¹J¹Âº;ºµ».»§¼!¼›½½¾ ¾„¾ÿ¿z¿õÀpÀìÁgÁãÂ_ÂÛÃXÃÔÄQÄÎÅKÅÈÆFÆÃÇAÇ¿È=ȼÉ:ɹÊ8Ê·Ë6˶Ì5̵Í5͵Î6ζÏ7ϸÐ9кÑ<ѾÒ?ÒÁÓDÓÆÔIÔËÕNÕÑÖUÖØ×\×àØdØèÙlÙñÚvÚûÛ€ÜÜŠÝÝ–ÞÞ¢ß)߯à6à½áDáÌâSâÛãcãëäsäü儿 æ–çç©è2è¼éFéÐê[êåëpëûì†ííœî(î´ï@ïÌðXðåñrñÿòŒóó§ô4ôÂõPõÞömöû÷Šøø¨ù8ùÇúWúçûwüü˜ý)ýºþKþÜÿmÿÿdesc.IEC 61966-2-1 Default RGB Colour Space - sRGBXYZ b™·…ÚXYZ PmeasXYZ 3¤XYZ o¢8õsig CRT desc-Reference Viewing Condition in IEC 61966-2-1XYZ öÖÓ-textCopyright International Color Consortium, 2009sf32 Dßÿÿó&”ýÿÿû¡ÿÿý¢ÛÀuÿàJFIFÿÛC    ++&.%#%.&D5//5DNB>BN_UU_wqwœœÑÿÛC    ++&.%#%.&D5//5DNB>BN_UU_wqwœœÑÿÂÆ"ÿÄÿÄÿÚ üÉ7›°eÀ@%„¤€°P¨(Š€B °¹¡,ÂÁRÁ ¨,B °P,"…\è,(*Xe @@ PYe ©@H¢M ÏAç=GŒ÷§<éW,ë™í§ ¸pO¡|8]£ŠõŽ;ÕgD<^°ÃP$7|Ç¥ò·Ä{¼½ç.ttOîñÏ!êó:»¸T%JDš„š‹"U,)™’‰A¸íÌ HK BÀ BÁ*À"ÀA`‚ °Å± ‚Ä %,b-eUÍ*RˆX(X* A) ah(ˆP…‚€” ‚¥¨(…‚¥¨JCH[ËDÌôSØsΡÉ;Å;‡ú#çO¤>cé˜úPùÓèÊùîøp»¡Åzòs:!âõÉ– di»ä¯G˜ôyÕä=oŒ=Þ#Ùâ=žCÑæ; ×@²‚)BT(!`,¢AA`P XÁ`¤U€@¬©AQ°[‘¤-R€•J²‚’ÊRÂÊ,, @¼’€PPÀ,¡(¡*¡(ʰTJ,¤”fnž£Áì®yÓ›=q93Û,÷ÊàÏ|8ªƒ®*RX’Z `¨) ˆ°” °,* K À³Y@"¬  ¥RQb(E* *P ”©` €  «*PH¢PP%P!J” %"€” ” @ (Ѝ¹ ²™² ® ² €,Á,[,@, B ° %H”¥AUe¢E‚€ ”X©aTŠ @ `€°(T( P°@‰H(ZŠ¥@€‚*,K Ë,B¦l ¥:DÉÀ* ‚¢¢l,-(‹  X”, ”)VQe@5¢(Y@*Å k4%€ Š©E‚¥YITI@ %"€* H "ˆQ, A$²¤A (ÜPÀ IH,ÖA, @KXˆ* B€¢YE”XT`  YA¢IAAH  Q,%À‚Å Ô I@‚€R- (”±@ @²‘ !ÊK’å)›JCPŠB °À €%@¢‹ X°, €H ¥BÂ¥ e !e©E•@Y@5%P ” R YBÀX”(”@T–RYVP” J² ‚Ä@dÔŠ@„D+3Q`M+IA* Š”€ @¨,‹ÀŠ"Á,Å€¡ ©KI e%()I@²(¢€–JI(%Z”¶PP¡e€ %RPb¢R€ÊI@ ²À– ,),!ŠH,‹ VK’ÁÊ’Àƒb€,€ €•,Rˆ–"©±€)` "Á(€Š¨°-2¢(Š(Š  ±PP€‚€X P@À"±E”¶QVU ”(” ((Q BPJ ,JKŠD*ÄD(@, JI@‚ €P*"¥%” `@€ ŠˆTfЋ°MB, ˆ¢(Š%(I@AIAT”AI@ R(,PXe !e R (€’ÂYF Õš…–%‰@RP€ PD¢(‹Kæ’ÂDD²¬’„’ª¢J !`©V, Q€,ˆˆ*Ë"ˆ°š ¤°(–ZŠ"ˆ¢*ÉT€(Š %P€ R(e(Ae€(( ˆ­dK),!j*-”º–ZX(–È-"‰@I@¢(Š"Á@"ÀJ@’Ê„D²¡*@*"ËšŠ$²* * , À‹ H¢)aH¤‹*(ʉ*¥¡5ªÍ¢)%*U3hŠ %RX*Qe°(*P¥@Š•l `¨(©@A(Š%Qe.¥†¥…U”…‰TŠ"‰@¢(”‰B(‹ Á(Ȳ@D¨*@ €€Š3¨%¢MB(ÀȈ¨[rŠ @ ˆ²–PJ"„ `¤€KR,P¬‹$ªŠ"«6ˆ¢(–¬Š"ˆ¬¢(‹I@  ¢("€¢P(” ,(¨((´šš¡f XP(–‰j3hŠ%ˆ¥Š"ˆ£*I)$ª’Á,$ @K*€‹$Ô"Á-3A,Y2Œ¨‰E"À`° ±B€D %*PŠ"’(‹”‚’ˆ¢J¨¢J¨¢(,°¢Z ³+LÝB-3j¢’U"‰TŠ"ˆ¢-"‚ˆ´ÍS6’*%¥Š"‰TŠ%°Š%UB*#P” IA¥V¥ŠXP-ˆ¢Z"€"‰A(Šˆ²ˆ²™Ô$ªÌ°‹*ЍB(“BJL¨‹¥€”%u ¨ò±‹@š„  €¡, µEI EUXJTXJDI*‘¥ejB‘U*¤QQI„ªE¡–„Q*‘DQDhE„heªeDZE±•¤RŦmFZ,Q*’Ñ4E*Ú°¤*‘D´E¤X&‘&†TEheFhE„•RXIeI¬‘eˆ,YE&TIDQ•Ñæ¥Š"£4 2!ŠŠJ‘HT¡ @ @… K@ €-’o TQE¥EÒETRJY-Ñ&•fÑ(’©ED´fÚeªe¡›U›¡›FmlfÑ›DnVZFmÑ›K›Dj ¤¹j™j«*Áhµ ÒÅD´F‘›DQE„Q I¨ID”eeIa%–ID•P DXEDš„Q•,QE†Aác@ Q@RRª‚¥  @ "ˆª ²(Ѝ ¨¢UH¢U©h•LÕHªŠAj)ˆ´Š"ÒMS-´Š$Õ"ˆÐ‹L´2Õ2ÐËTÌÕL´\´"Ó- ´"Ó- µL[W- ´ŒÝ Ú–[H¶U´-ˆ´Í¨‹I-2´ËC*\µ ´2°“P’‰,±C+ ¬Ê‹£*$´Êˆ°, 434$£*\Ût'š3ssl ¡I¢€D„)A*¢’*¢ª…¨¤…%PZŠ"Ò-I@ZŠHÔ%ªŠHÒ³t3h Ú%´ËBM ê“6Ó6ŒÝ Ú%¢5ÐËpÍÐËC7CC- ¶2ÑrÕ362ÐËV0Õ0Ùqt‰jY«LêØ—Eͨ µÔ#PŠ$ÐÌÔ2Ô$ÞI(ÊÂK 5*Md“Pʈ°ŠP“PŠ"ªJ‰5Ô$¥’Œ¬Œ¬8ÞlßQÊ€)(bX  @T-HXD«RªJ¦mµ›i›je¥Ei¦VÔZfÚ™j™hEi™ZF„šÑ†©–†m¦m¦V™hE¦Zj™he¡›¥fÛºhe¡–‹j«j™º²æÝFn’ÅÑ–‹›Q–†Zjhfj êj:„–T–I™©Yjhfhfj¨ERÅ ¨EjTe¬®Z„Ʊ9†5Ô•(Q`R I@RR– ÷ý Ÿ¯õ¾Ý1ùëÖ~;?³‹ŸµWâsûølþ¼‡á»‡á'î‡á¹Wágîaøgîr~"~âˆ~Û'ã²Éùë²~Rþ«'楇ç¢Ê|Þ‹ðŸs'Å¿c)ò_V1ô¡óÝò¸od9C™Ñ)ì˜3ãïË9\õÖ*¥@Š%”‚€E(¿‡è«é}K;ñK°,¨±J À¡,€J’ˆ„K)ˆ¨°“Y$Ô2Ðó›KŒúCôM£ÎzCõ‹å=dy½2bnjšÈHiˆ¾({<2t¹dv8¢÷8!ô/ÌGÓ|¸}[òØŸ/Ù|qö_jüJ¿jü]GÙ¿Ëö/ǧ×|”}iò‡Õ|±õ'ÌMóGÒŸ8}óÕß8Glãs’ÙÓ3ªËQ37+3C+¢J$Ô2Ü2¥ŠŒ¨Š$ÔY42¢J2Ô‰,&u’pwüÌëX½IhX€ÖE””°@5ûÅþóy÷Íy‹dQE‚QEQII¡•FTEhf„Q–¡&¡–¡–¥fuYÑ”ð:„š•—µ9– a&ºeãuøž3~ç$ôÙà÷ð‰(ÌÐÎw‚5™dÖIÃ2Œ)säÌÔ3f$Ô\ÊŒ,–gpÄÜŒMÉs4\Í"*Ymˆ¶\´Œ¨“Is5 µ ¬$°’ÂK‘,©,=;þVî~Þ¸½:g«^>º…„šVT@EK%$š.V™š„Te¨E„X±FUš„Æògã}Ÿ‡›¦˜×¸ª!e(‰@(P¢)%¥OèÏ oÒºb*È¢(Š"ˆª”‚ˆªŠ2ÔI4$Ô"ˆ°Š"–M ¨ÌÑ37“£œ…öòèwC”™Ì8ý"ÎÜK+Ó¦±ËÝàq}ŸôÏ,c“³æŸC×ÏŒõãú¿,·_f_…¯¥Ïg§ÎïàŽï™ôð|­çë¯ÌçûŸoéÓáïêýCòO¥õ×ò™ýGæ¥ï¯HùÞþcߓӼü¿Ô÷ë?,¬ë3y34— Œ­\ÚÍ•b*YB,†u%„ÂKdD*Kfîøks¿«å} N¤Öó&òE:ZdiheFT¹[šQ–²E.eF3¨x|©òñ¯Ufú¥ª  RP”É@@¢ÂÊ «ûßÁ~çYî•ÓœP•QDQE D U%Dš„Q%DQ FT“:†Tk–@HI¨{yæ<Òìå/W‡½|C~ü¸;¾~‘ŽÞHtçŸ'_Ÿ„=}9a§óó/Öø»‡?8íëøôúÿ'Ûüÿ¡~·6ï£ð÷<>@ý?Íù0òi.&¤¹š‘™¢á¦l«KjKD”fYk–T–\Œë"YR­¸±ô>g~ŸKÛãxÊ«-C-Ã-C*$ÐËPËR2Ü2¥“R"ŒÍBJ\MHÎ}2|Þ®^zõ^¥±e€h ©@° ¢(ZU©U%í¿û=O­,éÍ(” (€€¢(€Š"‰42ªËPŠˆ£3JÌÔ$ÔI5 ¨Ê«+¨Ê«3C3PŠ2£ M 4ŒMÃ9ÐÌÑ|ÚjQ‰¼ÆfáŒúCÞW3pó›‘ŒúdÄÜ\MHÎw çr\*\µ"-–*Y4Œ¬‰)d² 35’fÂgY$°’Ê“Y$°’Ž®nÎKßÁôO£ÓáÑ×;‰•heFT²jnQ&¡Dš.f¤3¡‰¨¹šÌ1¬Ÿ 2òÞíb­ˆª²¨RQ Y@¢ˆ«%ZŠ'ë¿%ú‹?CNœâˆîðËÂÍi¾u™³'¹Í{ü³y]ø8Þº³ÄQÓÉè®t¡OcÄ…4e¼¼™XIëƒ-âÄ 43:¼eó@3taDš†f¡%„T¹šÊI¨Lï+3¸g:šF&òg;†Êæo1‰¨fjϦW3R153L²±e\Ø©d¢gHÌÔ‰,Y,Œ¬3.I,¬Ë ,£2Œ’¾ÍúŸ*ÏO¥óþ¥wôxûô癡†„ÎòEQ¹jjiQ™¸e©.f¡™¨¹Îññ÷ä‹éN{"=ìº DQU4EªEPY( R©OÒ~sïÙú©gNqGÑÇ+ËQ¹ÙyX¿GŸËÎ>·–O/o-tŸC›ÃX¾OŸÆ>ÅxörznuIÏÏ_C’x¤ú[¯|øê_.Î]YÍö>OaåïÉÑ/7—¯ŸLo;̽/LÞ5tÏÒù½|ü÷ÙÈè“׿}•gœ¾ý'½æéå¾oMÛ9z4^>þOtù–θèíórß'?o.§gFW| âË“£ËGü>¦§Ì÷úRòc£ëžž™Þr´óQ†²g>Æ}2a¬™›†3¹.3¸bo&f¢á¬™šK‰©jK,TI©,š‘œê:‘&²³:†s¬¦e†f³l–XI¬’X}O‘ö¾5Wæýk;=±éÓš‚Q3¸Ii™¨e¡•jDRÉi•jš’æk&f³.~wÒùüÿo/Nzˆ^‘b©,¢Ê( ’•R‚Ôª‘@¤UPµñ~©û9©ÓœQ( EDU‘DZeDPš†Zhe¡•FZ´EQ•a™µbzHóÕV:¹Ñ‹¨fØcaç°ÌØ–HgR™Ô=¼,—ׯß(\R{zq%öòʲ«3ÃPÃPÎw“3PÌÞLçr\MCPÃY35%ÌÔ35%ʥͬÙ,•*15 Íd“YŒË çy3J̰‘J3(–{Ÿ#éüÛ;~·ËûÏW¦u¼Ìèe¨IDš„Q•TI¨fÒÉD•XI¬®f†3¼K>ÞüÞnwŸLkèl¨ BR€)T R‰()@@ФúGï³¼tæ)K©ÅP€E DX%FV DXE¦TID”IDQ††e¬“;É¸š‰™¨bzdÄÔ3äÊÃ3PÎw%Îw“y35 Mås5#3Is5"M%ËR\µ2£ f&w“9Ô3C9ÖIdÌÖI,$°’ÅÏÕN._\WÑúß3ëï>öµ™(’Â(‹ 5²$ГPËPŠ\¬ˆ±dÖLÍdÎw™|¿5úÎâújLì‹6Ê(¥%KK(¡Rز‚ÐeER¤öòÑý6kÔXvãXå¯?O\׳yôžÚÍçtX從ÕÄŽ™÷òúœÜ·áŸCŸËß6zñõrÙ}ü=jyëÞ^|öðY¯O.³×£9¼›½Ãé×âsk¯¦>K»Ðù¾þ”x\×’·˜°“PJ2¢(Í£*2°“y–, $°ÊÊ™Ô35 çyLÍdÌÞLÍC3Qq7˜ÌÔ17“3R353IpÒ\5"MId£ — çyŒÍdÎw“9ÖLçy3,©C2£2ÅŸkâýë>üzO§õ¾oÓée–EQ%a™¹`”eD”E‘•.Tg;‹‰¨bY/ÏøWåã^¹ÖsH:lº(”  Ê,V’¢ÁE, X()*سDª@ôçèÖAÓ#>¸¦mWGŽY,V®i©rK霢R½|hг×ÎI}<¨Ïw³Óá Ñ΋íà5¯!í<¡sm™¥I¨EQDJf„T²jjQ•„\’j4fk$Î¥a¬¤”bo&f¢ânF&¡™¨g;̳:Ff¢å©T¹š‹™©•.&¡™dfk&s¼Ös¬™Î¡…†f²Ia%‹?Aùÿгùž¾NåúßOƒèôÆå–E„XE„Y a&„”I¨±FU&Z†f¢æjš’ã:‹ðx}¼ùïy²z¥±fŠ,¢ÊT¥KeYR‚ØP¥*¢‡íû>ÑH«©sÄw¾Xú—åÓé¾`úo›Sè¾u>ƒç—è8w¸)Üá‡{…®5v8éÖãnQÔåNQÒåÑîðžÏí<‹êò'£Îš`ndi‘Y€Î†Z’³¡–©ç=aæØÆ}›póžóžóž™1Ÿ\לØóžóžòžóϬ9±å=sϬ_'¤9ë%ó›KäÜ17͹Îäbo+‰¼™Îòc;‡œÞLgy3äÌÖIBgY'è¾سóßGçýI~·'gNvjT”I©jjQ%h¹Q%Q–³boæk&<ý9Wóºóõ届j,¶PZ (¥–Álµ(Še(²ØÖt+U~³ë|_·&ieð÷üýsó{àðßë©ùëéøûúú~:þÀ~=ûøùû~5û%~3_±§ã_²Œ~Ì~-ûQø§íâßµŠÏíáø§íGâ¯íø«ûH~1û(~9ûøçì)ø÷ëaù7ëä߬‡äߪ‡åŸ©G埩•ù[ú‰™~”~j~˜~eúlŸœŸ¤‡çw÷þ1Îõ‹åw³ðù¹¿eñ‹ö'ȇٟañ¡ö_f|h}‡Åk?f|aö_}œüt}‡È]ò,¿Vü›/Õ|«QóÒ|Û/Ñ|áô_9AóÇ|àÎwN"öN(vçŽ'\ä•Õ9aÕ9aÓŸjÛÇÚÌËÏ{ËgÖù`ú½|ýÅ–QQ&¡¥‚TeD”IQœë+1¼ùÿCäKò=|½yï!H:i¼¨RŠ 5@” URKR‰l¶(4UQf¡ú/Ð~oô±’Ü¿-úŸÌÔÔ§éæäfÑ™hfÕEDZEDQE¦Z„QEDj€ DXQ@"j ¨Eþó›Ç£SÓ>—Såu–uÑÖ8çd8çfS’vC×GPäuWT^gJ9#šô¥çtXç¾åðt#Áî¼<^Åñžò<^Ðñz¬<§´ÿæ³y½1¼o+¡¼Ûl¥BhÊ,´²¢©)J©*Ш«JÑ*¥)õ¿WøÿØËçK›ù¿Ò~~¼5ËúÌîFZ&Zhe¥F†Zhe¡–†Zhe¡–†m±‹Õå›ã:±^§ÂÌÍ+-#3p“BM ´1t2ÐËC3c3p“c XÏÏú_:çóµnÛÎÏ›í¾“Šv£ŽvÃŽwCŠv+ŠwC†wÝt8§pávŽj8¥âvf8ïì3Ï_’}¬iòTäuŽIÖŽI×–uYÔŽYÕYÕYÕ“›=R¹gNNiÓštÚ{ø¯?G‡Aœïéÿ'úïȧoÚù?oS»×ÏÓYf‰,"‰(„ˆ,„°‹£+%’ŒçPÎuƒ”ýGåq«¨ÎÒŠÈê³]2( AKK4J %²–Uж MJZªu~ãð½—È3~ÜøÕÁ¬ÛeŸLóe¥e¡–†Zlbèe¡–†Zla´a±‡ óz yé›ëœ¥õæô–yÏV§“ÕOQäõ‡›Ðy½•Ý<ç¨óž°ÃÐy=açv<Þƒ/ì|OÏ[6´'o‡|s:Ï:;¢ê‡4êÉÌê‡,ꮑÈ골rºG+ªG6;$½ŸcóNzú˜ùù³—^÷NyÐŽgD9çL999çL^iÓiÓiÑiÓ“štäæ2ÎiÐ8y;¸¦¹ºyú3säýGä¿[ù;>—ÛùjÎÍgZÌ”e`”ID”IKdI¡•XA,Ρ™¬™Æòpþoîü{õšÌÔ)Ö:fÙQAe¥ ¡DYi©AQf¬š–•EQZI©¢U5ûïÀ~òX¿+êq×ĹÝ~Õ=xïÍèO7 óz—Íè;±†éæô.la´e¡–†Z.ZhfÑ›FZhe¡–†Zh™h¹h™h¸l˜la±†Æ~'ÝøzÏçn³&óŸO—ê“°qÎØq»®œNÒq^ÁÅzËÉ;aÄíS¸p»¡ÂîG¾/úàwÈàwžï>}óçЇ~Œ>sèÃæ¾Œ>túPù¹úpù“é—>¤¯—Ÿ«—> ü·Íúÿ:òöóôγëåÜ}Î}¯ŸWìü¯¯¹Ò«™, ! ,ÉRÁ(’Á(€‹%̰™Ô1(ø+·žý3dÒXuYzf‹(-”)Aie*T¶ZRËBÐ¥F¥¥R~ãñ´Íö اÁ}Ï›§Øøüy×é_š±úGçG蟞¢~vŸ¡~~Ÿ}ðß| }çÁ‡ß|÷¯À§è3ðGÞ|÷Ÿ¿~| ~ùáúçÇ蟠¿ž¡~x~†~x~‰ùÁú7ç úçÇ蟢~t~‰ùØ~Ž~t~‰ùÑú,þx~…ùè~‡ácÖÌyi©Ÿ_8_©ÁãUòÖ|‘õ_*YòÖ|qöGÙ|h}—ÆÉöß/Ü|8}×ÂqðÑ÷ }ÇÃK÷}ÇÂ/ÝŸv|8}Ùðá÷_GÝ|}éðߟ/Þ|}ùùøŸ¡Ïçò~†~{'è¼>"½>w®¥“Y\ýúùœ›'Öúÿ7éï>Ë,’Â,"Â,¥€€€€‹%’ä’ä’ù•óšåÒ‘RÀË/\,¢ÊR¢” ³@%ÔµSVMgB¨³H³T³B©l¤ý‡ä?U›Þe Ò r4Í* ‚Ü’ Ô’´ÉtÈÓ#L“R ‚ÉB €‚ ¨Á(’ˆ°€„K)h@H"Á(’Â(’ÂVh̹„Id¹æY,$²2E™Ô3d™ÖI›%™ÖLçPÎu“Þ ÍàÎu Ê3dÏéÿ1úØüOЯ±ô¸>޳¹r‰`)` – `•–.f²N>Ï™/ç=<ý9í,T°²¯\l¨²Õ)RÕ*(”µJ*Š¥±eÔ£R–©>߯Ñû=ã–…%”ˆ €D¢,* ÂË‹’ˆ°“C3q37 ÍÃ*37 4¬ÍBgP@ËP“PÊÀBä¹Vl‰›u"KK ,‰ed²3,$¹$±sC9±flŒË çY1.LÍd̰̰™´èûÿ†Œ}_Ÿõ«êwrõëXID–,”@%ÊT) d%‹3¬’jâ}ÏÍKó=1¾{D ;)× (³V,¢©,ÒMKMKK*[4J¢ªRÒÍ ¥4*ŠÑÙúïÁtåúéç×-Je "H¢(ËC- ´2ÐËTÃc S ŒOAæôoAæôsÕ^SÔyÏJyÏRy=G”õOQäõ/Qå=‘âõ/jsº5èÓ¤rºaÌé‡4鮘s=¡å=aç=açŸqÏ:!Í:!ÏŸz¼Ó¥®¨rΤqº‹ÉžÌ¹ë§{dpÎü3¼|ùßççèÃçgéI~dúƒäçëäùû9>>>Ü>û’¾~ü>Aë'ôßó‡o é_O±ÍõuŸ~Œz\ÌØ%‚ –@%K™a%ƒò_¬üfuKÀPv޼í”R©AQJ¥)lPµIU”h¦¥ V­$º”™õ/'½ò¥¯‰ã/ß~w9¿£~t~‰ùê~ð)÷Ÿ ×ÛŸŸiñiögÈ]òiõ.ŸMó)ôŸ6ŸFpSºqZìrS©É£¡à=^Tô`ndjM&Zlbz<û+ÅîOE9gXâwSÞ>{èÃæÏ¦>[êÃå>¨ù/­O>Ðø¯µ>Ô>3ìÓâÏ·‹>Øø“íÈø·íˆûpø¯´—âß³OŠûI~3ì£ã>ÀøóìÅøï°>;ë£ã¾Ä>Dûãϰ>;ëÃäO®>Dúðùë‘>¼¯>Ä>F~Ä>;ëÃäÏ®>N¾§¡óûzû,òíz\Õ„–)%„Q¹¨%`Kš‹•‡‡ã¿Sù\ktÎà î§^j´³DÔ¢ÍX²‹-”в•4KK4,гTÖvMM J–ÍEM’nž}r^,÷+ÙN+ÛN-vlà×vŸ®úpk¸pë¶œNêpÞú|ýwŽÐí§ î§úçÞúœñÃ{‡ºœºœñÃ{iÂí§ î§»GíWínÁÇzÑÉz•ÊêÓªœ³²Ç¶s´r;°qÞ¸r:áËžÑÇ;‡í§ ¹.áÂî/¹.áÂî3ºG´qg¸p»GíNØqNáÃ;‡ î‡ º.ÑÃ;ó\3º3¸p;‡ÏwË]cÃ~‚,$°€‚¤°Kˆ±@‹¢ÅE>7ç~ÏÇÆö³:Š î/^tU-(–…¥(–…Qib´J¢¨ÑMM ©j–‘j–Í Ñ-¤º )5i-Ñ›±ZF©¤jË›tbèKD¶™¶™ºÒ¤ZfÑ¡–„hE¦ZADQ™hE¡›`)E%¦Z‘Ed(D ”@@@±a B D–X" a%ÉbRX3`–QI`€€ –@IBTea–²«)ù>.}5U€R¾ËךËJ¶,Ð[B¢¨ª,Õ(–Ê54,Õ M 4K¨ÔгE³CSBÍÍ ¥Öt5)l¥²–‚’Û(P¡e ¢ÂP(,ª( @‚Á@”Q )ED(”@-„ `”I` "ˆ‚ `XI`—"YRX%È ˆE•HD!eDRÅjD”fj,ǧ!ùw˪X°ôÛ™E³V)e(¡iT¨ª5¥Z¥)jŠ¨Ò‹45)lÔ5)jJZSEMÍ¥³@¢¨ « *¨Š*ˆ¢Ð,¢€$ªÊ J,¢Pª¢X„E€ a–€‚X@BÉse 3D…@I`– a%JA( FUQ™Jù_[óòüLoŸD¢(€è«·5[j•QTYªš”V’PµjjhYK¬ì•Q©KT¶hY¢jRêRÕ–.³¢Ù¡©KBêRÙKeBŠ XVP¤YTPP RP(”Q(€P€PJX°@K°K°K°€„‰,$°B¤°„ ©, H±bÂ, ”¢MH’—+ ~Sõ‹ÍäÞuŽ€%€}:wäP«e¡UJ‰h*h4%¡jJ+B†ŠR–…ª[4[,[4ZÑ5)lв”¢¨²‹(Ô¢Ë Tl  ’€ €…€ !a  – r%‚YLØ%„”H‚UB ‚XEŠ”d @ DTB‰b–BQ%,”gðß²üVui€XGÓ«èå K4”¥-M4J©4R´*ŠÒ,Ñ54,¥³B¨Öt7 SDÔÑh]KR–ÊZÊR”Ê’…(MJJ((€(@)(,¡(¤¡,(@¤P„  ”e`!€ˆ !RX%‚–YRÌê ‚Q%,š„QEDY¬TI¨ID³Kò¿%úOÍã[Y€–1~‹SÕÆZ¥¤U¥TP«I¥¢…QTUQBÕ)ERšRÕšKe5e&†¥-”¶Q©E”j AB¥((…¤¥eY@Ê%  @!,"Â,$¢BÁ‹*‹JK" 3A°ÔX¢(•H R(Š–,"—š?)ñ»¸¹ïEš‹=Úc]í=ž|êQf…ZUQe«ATV‰TU&¦’jhšš%hYKf…Q µF¥š- V-”UE R‰m–Å –%@ ©AeBP°P””B@H°ÂK°B¡°  @€KBÀ’Ê„ "ÂT"Á,T €¢,*%¨‹W+±Káˆó^}, fåöV/Ñj{<ñB…¥R£@«J…Z ”¢¨ª*‹4,в–©J[4[)l¥Ô¥Öl]gBªµ¢5¤´¤ª,¡@¤ ²Ø*(P,±,  )) @@ a LØ%‚\–E¨b" `š‚Q•jX %€,E€¤¤%KO›ôþ¿–Þ7Ï­€)ôóöÍÓÈ}¹©êáš ¶*Š£RŠ´ªJÑ*¢¨²–ƒYØ(ª5(¡u)l¥²š)jKÊ[)lÒ¬Ð*,¥M‚­,@Q)` J T@ ‚X%‚ €2 U€€€D\ÙbX%‚X%‚Q”@²P–Q E,Q–¤_Ë~¯ñ_7ÓÇHÂ’\¿C7çºGØÏ¦=|"ÀªZKBÙFŠ ª-•)EQT¥EQTU-΋TU”¶Rê"ê fŠ%¶QB‹4”¶Q`¥” „©i` €R¥°¢P ,€Á@D¤¹),D°@K Á,ˆ¢¨€)r¡(²"–(…"¢ÿ?ýßóüï5s¹@°×w'_=Eg_WÏÛÏÝåª-T¶Q© j¦¥EQe)I¥RÙER”PµE YJRÙ¢ƒVQ©b”¶U©¡eŠ R‚ÙE–Ê•B°@PÂ¥ ”(J€¥€K @KJ°@@K¢,ˆ…"ÈJ%)@”„ R)aH!fŽÂþÃñøÞÄØJ‡GG—¯-†oÖÎwîòáf€Z¨QlµhRЍ¡l¥Ql¥³Dª,¥³BÍ 4RŠ¥²–ËP[*Û)B[,µ4‹*ÐR‹-‚€((Ê@PT°PTEA@A`³@ €Í‚‚B…ˆ" €@TXJ„T E€(@yô?-ùϯò1Óif€")W³MñÞŒÛÕóþ¿ÍäÔÞbÔ•JZUhš”Q-”U”¶R”P´-”jRØ5e-šhY¨YKe[e„¶Ym”YJ ,¶ ”e©@©@`¢€H±A DKD,A,Á¨@@B  ²P" ¨*R( °P‹ï_Áòïúh‹@£Ÿìbý~®x}/d¿úÿ ïý”ÖzfQJ+E&¥AR”YKeRÙKe-Ά³¡BÙ¡e-”Õ‚ÕX¶RÙVØ4–-Ί”¶[Z”¡(( Y@* *PXŠ B¢QD*æÄ@D ² ¬ÐB¤*CQ €‚  ¨* H¨* ƒH–¤4‚ ¤* ‘t’5sKËÕò%üuαÑajP!ú¯Êÿ@çªúŸ7ÇÛ/6uøÓþKõ¿_Åã7Ž˜”²jRë4·:«sKeT¶RØ-ƒV e(- \ÓIM3M¥-”ÕΆ³¨©Ke4…¶"ë4¶ X4–Ê š•bËe ”¨* ©E” ¨(X* ‚ ¨* €B¤5"[ $,€"Ë$,dÔ© $+"¤5™ L25 $4ÈÔÈ¬ŠƒS#R ¨5 ¨+(Ó4¬Ò³J‚¢Z‚ÜÐH¬CÒyá}ï,ŽÿÍ}ÌÍ|O?LìB¥€¹üoíüý:»þ&¸ïè*Ï埭ü—éþŸÛÇ^pX²–ÁlKZJi*[®4[š[š]dmšjæš¹¦®iniušj曹±«‘´¥¹¦’–æ­¹¦®i¤¹&Ù¦i®)¦i¦inF[¹AP[šTEA`T¤Hi$i‘©!¦F™‘Y‹¦di‘d•Y‰¦¦a©˜jbyd÷œù®™Ëæv¸1F|ÜŸR|œ/Ù|LÇÝŸ'èÊþ’~g1úwå²~©ù(~º~C1ûã3oí_‰Ì~áøh~â~·ÏâRþÖ~.Ÿ²Çä úÜ~U©Çæiú9ùÛ{?k? ¿W?5C<%ëÇ8öÏÖPÎv<ç¨Î}$¾oA™¸yÐ PúªüßÖòöúWäûqßkÀ¿†ýç>ïÓò}zã·,ÊÈÕÍ.³K¬ÒܬÝÈÛ4¶ \²7r7qM\SwÝÅ6ÈÝÅ7|éé¯:nãF®,kX¸ŒSlÓWÛ4Û4·4Ó4Ó#lG–O{Í“®ðäúæa~³ãäûWàæ?A9æ~ùl¬~Cû'âr¿¸~'ï_ÏñÐßΰH5Ì¿ÓÌ2PÏòüŸÔqüÀMÏó4¿ÒsüàDÏóáûß?ÂØý¾?öXüˆý^?.ÒùþzŸ{¿c?%SÏçÿc«<ñ}óä=&½xß!êò/«Ä{¼¾¹QÔås”u9[v8ÇeⳈv¸áØãs”t¹‡Lðîqï7Òú~OÑxõröåŒÌVÞP÷¼Ã®ñŽËÂ;ß>Iòáõ¯È‘ö_qð¡÷ïç¡ú+ù¸~™ùŒ¯ê¯å3­¿‡ìoã!ûYø‘ûwâ$¿·Ÿ‰‡íŸŒÁûiø¸~Ò~2Çì3ùúÜþRŸ¨ŸšÉúYù´¿ ÏÁw}Œ|¢ý<ðf;±È:sà=±„jHT-di”nùß!èó¯!êò·Äzßí<‡­ñ¯í<Ç£Ìm¶ØdjAPTAPX !@APT…AP²€B’Áb„ @ °)`²„‚ØBÁl…X]cº_ÜyúOéó­OaüáoÓò E Pi!¦ï™=cÑæ=&ÞtÛôyIŠm·724ÈÔÍ* ‚ ¨,  €¨ A`°€ˆ€°‹‚À À ””X€…‚¥Ònûõõú {yx;ÁlQüÜ}? ’¡P(,A`Á`,E@`¨XUDTET°P…@P)¤k&³`ðE`À°€ ²€TH°·Y¢XOÛþ'÷|:vÃÇ×õ†P¿ÎÔò( ,D"’Á`PPK)J° dQ™·%J E«–¬báfÝûξkéSæß§O—>Í—äþûòŸ³óïÃÓÏ>~šôÊ4æƒêy@`*¥ €R)`¨ ‚ ¨, B€”"Ó-SKOjx:)Ìê§#¯kÂî§èj>múcæO¬>KìŽû:—â¾Øø¯µO‹~Ðø×ìÃãëëhù±%ùwêDùšúùï¡…ä½CžúÓ{ðÁ×y"v¸q~c¯˜×å=?M#óÞŸo'Ëß~câ~Cú?à»gßßäk¦~“æÓ¿§˜@( (€"ˆ(`H¢(‹LÛLÏAç})ã}tx=ǃ¦¯+®Ç¸p»ÇèSç>•>céØù—é—æ_©›~…>uú6>{è Ü8¯eŽKÕmúÓÆú—Îë%ß”=ï<:uÄ;\C¹ó‡Ñ×ÊRü‚}wLJÙ|U}«ðá÷'Äiñaö_gÖ|ß#ë>8ûù*ú³å£>uNìñÊ뜣¡ÏŒøYæOI…mUÄëÄx:ðsë«Ìç<£õ“û¿¡Ÿ<7÷÷ßžÉú7æ¡úoÇûðuͱ×J«,X§Òý§åOãíé^‰O'²ßæcéù@(€¢-2´ËTÃtózSÉê<ž´ñ{E9#™Ô9]TäuÕäv#‘ÙKÔ9#žôw@ñ{+é ] `ÓÑä—Ùã‡0êrC±ÇÛÃ÷O ùÃè¾t>”ùÊú3ç¡8|áÙã“uÎQÒæÁg¾|‡«ÈzL H,B`¨-ÏØÎ~Eý?_oÏ¿}=wo-B ¨E‚ ¨ ,ÀH¢zãÑzù±é’2tgŸCÇy§O‡¬i™.؆؇£Ï5;5ŸF)¦t¶ËöêûüõíÝé|]¹½13¯D•µÍéùy4ä^“•ßsxFŸ5õ4|—ØÔ|[öô|)úSæjïYó»Z¹P¹¸yW”=ž0÷xÃÝá¡Ï©Ì^—*ºœ°ër· ꜵ:\áÎ_w=ž#ÚyG˜ÛÛLŠ‚¡‚À©A  €°X*$ÐI¼Ù?KðüsÇžfúí–†Zhe¡— ózB,0ôyKä=oŠ_g•=&i©4F´y=´s:©É;lpO¦>[êèù°_Žû9>MúÃåïé#]Pð{HózÅËP6Å­_9'¥òžœµé5Ó}ŸÁþßË×£Ç{á¾mÌÛ€x>Ÿ—ß>Pú¦üÿÓóôúŸ9ëé>l>ùCê>e>Œùãè|.ïÓ<,ßG:žÇ‹¿†#RÈõ/“^ç3ßqÊõö9»>{x°€%P ˆ¢(5¢(Š Y 24ÀÛÑæ=¦¤¥)›ªbz'½ŽwN—ÙN)ô)óŸJŸ1õQòŸX|«õ©òZ/Ì¿N:ýÁ®äpë³'5釆½G–´%ˆÝñ§£È{>·7•è_çéùD>·Ñüç·-ý·ÃKöçÄ'ÚŸ_^|õgËYô¸1šò²ï/¥ó{c±Ãé‹ïxUÙÙñ7K›uõ³ñ4}®™“ìkãCÓÆ·™@J”% CL24ÈÛ0ÜÍ* R-2Ý<Þ•|žððtÔåuxÝ´áwÝN Ü8]ÃÚŽGXå×E9Þôñ¾–_+é Ú0ôyÓÑä=§”=ï8ès#¥Ê:œ°ìœÙ:ç(êrÓ¢x+Ýà=çŠ=§•­ß1¹‘¨‘@•IDZePUI¡–„TETQE¡•QRPÆüÒûøäêž:‡—¼#ÇÚ¥²[,¢ˆ¨@5+6ÂcØrzfYÓ™¬Ýþ×ðßO–ÿ]+ÉÖù™>Ÿš žÞ:=˜²é‘¦F™†Ù†ä†6T()&‰–éæõ‡›Ôy=‡ƒ sºaÎéΡÊë‡3¤sº Ï}Ç…ö'õ/•ôºFj’²i®ïœ=g˜ôyKä=^CÕä=cÕä=^CѶ™jA©”JDQ‘DZEDTE€¤RÅH(šŠ¤°K)a PTEÊJ @ @EDYá7æv~—ò¬Ëó^GåÛ-ñ³¦’”EDš„Pjðêð³>üÞ‡¬©Sõ¿ûo'_trßó7»éù¼^ÃBR© ³ ß1»æ_Inbm¶™¥A¦EA`X€’heªa¬†iV™Qg›£'ÖH° ” % @!H¢(Š"‰@¢P”¢,Y@Á(”€%"À¢,,¢,,R(((Š"ˆ°”%°(‹IÑÏëœp_»ódæ¶tÛË^‡²¥Í¢(–ŒµÕŒ62Õ_9è³…ëå©ï®~‰_KæLßèŽñvüƒsèpËDÍ¢-2´d–‰›Æv=´ËC8ô§”÷‡‹ØyOxy_JxïTòž£ŸÒøžà—C- ç=O;é“öþÞ>–hJ¯#§£rûyç%ÄÅ›¾1:þê)(E ,ª(QÄUED¢¬  E`(”J±,¤Q( E@ Ze¡š¤he¡–†[lbÑ›aY†™Áë|²tOtNaÕxáÙ®wÎÛçÍk£×ˆu¹¢ô<)é  Š2Øóž£Åê<žÒ¼§®>¾C¦5›øÓRMBÀ/BrÞG.ó›Z¢Ä’ãJçöñö.³ ¬z^¨ä½Á=>®‚.Ÿ¨Ëò³·†¯‡¿…znz_ß>|ïÌäß—Ië¨ zÇ<ŠÍÇ¡¹Gžn‰4Lª¥‘tÍ'¯Ÿ¤ E@€EÀQ)@¤Q.m‘QE¦V‘D),ɶ!èó¯)×ÂqÓ9aØã•ÛxGkˆv8Ç\æ.j{çÌzLHTÔL·kÎz7¥<ž´ñ¾£Êú4Ö¥À55’MÓÎï:/yÌèó<݇³¥~d´æõñYí4–(‹Mù~‡Æ>3ôüîU“òÏÔþj¹›Å›÷åõ_Tfù+R¡ú?ÍöåõþÙø1O/]‰ç%1XöÎÉåëà{Ø7ÛóÑïázyGÝñùX­vð å} BPž>Þg¤óØ€HOe"Œ´LMåqnl•¥Ï­±J @DQR„ !R`mæ=CÖøw<:o(鼃©Ê:œ°ês—==ž4õ™“D–™žónžOZx_js½ÇƒÞžQæöoAçwL63uå÷6öúòoW™âìå2Õ1=<Ï/n.ʯ±Õ›ùÝ}Þcå>çÄ¥ ÅßÉf»þgq_{¯ò~Ÿo¤üϯÛé?'}¼+štqê}/ÓþCíàÎâ÷~wëø.•|ý-puòúYúÜ|&/è=?æ~æ|ÿ#~zÍN>þ]OMx{‰R¥‡oÐøV>§¯ÅGÖ|¥}n.j<=¦žA:EЉ@BÂÆhòõó‡£#RCRèPž^ÐðöóÅ{¼G³Äz¼‡«Èz0=&)š…‚²4Ä=&ÆåfÑ­ùCÝÎŽ‹ÌN‡0éœãÝâ¯kÎ^‰ã£Ñˆi,%¦[mÓÊ{µ<´ñ{¸ñz«Êú;¸K™z#T™Ð…ŽoSÚoìÇÃ}]GÉV’he¡Î™;:9ÿM—ç·õºåü«×ËQB•¿+;þßÃûù×Î÷æìN½ŸçZ€që^Vw~ŸóC{ü~ó»áuóVl W¿œ³è}_ŽÍèïøôûÿä©Kåꯟ×ËígÐsLß§ËÌ:¯%7€¼~VytñuVåJ±…ÖiåÍÝéÕ|}â*Ye²ÚàèžVtÕ—3BMB,-”š– ªðÏ·€z¼‡«ÊÓÄ{ñ­~‹ó:øêis¼‘IãÏ×ǧoW¾n}1#Ó^01w³—“»‡S·ÓŸ¢\Ò"úž7»ŒŽ¬œ<ŸCçêvïâ$©H5ÕÇÖgÊlâñìáÔïyúJ,B‘BË\ׯÎÂ˱‹¡–²kÇÓÉ×ÇëgKÂËí|G³Àt¹²uðkÎΟn w¸"ýÏB|úÓˆvÎBôÏ ž—ôåRfì¸j¦†m,PÖao‡¸zûmŒ5¢cÓÆ·¹ï/ÐøÇÄ~“ó¦E%‡ŠzW¯Ôù¨ÃåzóvKó9{xµ„/—¯zyúÒq{|Ì_µùÿÒ~jÏ!¢X x3ºèýç;2èìøK7‡™7,°sôs'·G‡´¿SdžK÷þÑ<žÞzaÐŽ|ïÏ×ÉÖ{yk1¬€ô<Þ»9ç§s®lí•IDÞ=Ìù{á<ÞþG7_§g¯¶lT¹õòÑíù¾XâíãÔöêâí"ÌÔÔ3ïä=ý9âîæÔÏ_pµ›–†üôxÊôâ÷ñ±Ûó½ËÆ;y^÷“è>t>‡ W§¿%:o(ésS£>#×>z'ŸN7¶ŽyÕNGT9§]8÷Ó£‰ßN ÝN}>}ワ™íKÅ{#®Ú÷§ƒÜx_Qçª"ä·4òzÜùG‚ªJ¾^ž ¿|}û¿+;ôŸ™û?ª]ˆ áéš÷û?³7¿ŸæýXÏÊú–\žýG§W—»·â ’Á³ŽqÏÑÌ{zãeÍ…€—Cœ`‰ÏÑÍÐzÍE€š½Þ…oÂ3ÍÑÏg¿·²¥‡¶³¸ÌÖG7IÏãéŠçëäê¯IfIDèç/´ò%‰^ôò³½.uK!ëŒDÔįN?~téèàõ^§":Ür»g;g;¹¼–^ÎÜáKÜâs—g®C9÷ö8/hãx9ï°ð4åvÓ“}tὃ‰ÛNu8uÛeåõö‘«”ºfYYhžwZ1­ kpËÈz¼aïy‡KšSš.z¾÷<dzæõÙïŸ1¹‘©!¨ FZžƒÔyÝÓ ÀR(ú¿‰Mˆ,ELjtlŒÂÉEÃÐO@ Aó> d^@Ï9]; ¡¢9²iÍÒYë ¨E ùyãÔZ"‚Î~Ü ©æ€¨f„ Ø@¶…ÑBëD)-в„‚P²…p¤yd¦D¥ #ÛÌ>A©íèKÕÄB€+˜³—°³ÐK)PJ‚j€(%¤@¿ÿÄ6 !"01P#23@pA`$CB%4D€ °ÿÚÿ{!BB¨U (P¡E*¥H ‚<¹å$’I$’I$“ûB¨U ¡B… (QJ©U!H#Ì’I$’I$’I$’Iý!U ¡D(P¡B…¢•RªBG™$’I$’I$’I$’Oëø! ¡V”Bˆi¡CLÓ4Í5(¥¢•RªA~þÊ„! ¡F”BˆQ 3LÓ(P¢”R¥H#üZ{ò$ÿþhñÿ·ÿã«´ø3†‰?ý_þ»¯í—þÙÉûeþ¿¶×öºú{*br‰Ã9M›Í£Í£Í®CjólólónóAæƒÍš/4ži<Óy¦óMåQå\UÄ) ì þíþžÆÆ+Œ\0ÜmoCJ´£ 0Óa§ŒÓa¤ÃI†“ ,4h0Û°Û°Û0Û0Ú°Ú´Ú´Ú´Ú¡µCjmM©µ6Êm”Û)·SnãAƃ.4œi8Óq¦ãMÆ›Š8£Š¸ª•R…!ׯ<¾žÄÆÙpáO}„!B„!B‚„!B‚„! Ž}¹ö;‰BP”% BP”% BP”% BP“±Øìvü˜÷ ¾¾ÅÂãÿS*J’¤©e,âÎ.âî.âî5j8ÔqªãUMU5TÕSYMe5”ÖS\×5Ís\×5Ís] t5Ð×CY '´äú½…=p6ú1¨5ä“ìÎõOaÇõcú}¹ˆ‡Ë<*®HwE|ÞÚe“¥0¹Qp¼F«–;èäÆ÷ "'D,B¯¿¢ÀŠHžÈ¾‚{ >¦}>ÝŒV¶êä—øz%jÔEFª6) r ‰ÉS¥1•[i±¶ª=¸ñ·%™÷q¢N£ç'‹ÛM2½Fµ\©ÎrcrŸ3K ˜ÑŠÌ™±½ïsUª#\áqd䈪i¾ »§…c\ºœ!ÄâkáÅ‹‡Fðø[‘!_¬áý¡ŠHŽØò/„OaO\O·3’z¯lŽbËþŸ"÷oýX…õþò¡éÅõ§ÝWwË<žªÆ÷*±>f>è?¶W°kõNìX|#UéFñ±*è²Uùò9›¾!]¥‡Üìg bÎ÷äf4^#æÛ<7='ˆFÓ!Áú7 ã&DË—_›ÁýÎvq\jü¯hg«½Z£TObÍôû~qRÎUDr ®U%aSž£„r ‹U•eªªjšÃUG+]ªÁïW*eHÕbs~YL¹åÛ£‰íDùi“3‘ÏÈäV ~7²p·…Q¬áûefDn}s•Z¯v_øÜCþIÃý®&›ø†iåË‹p5»fbíÂ;"&'ì{C}rz´hžÅ›Ù8·þf<„W ²¤º%aUËí8þ¬ŸR زýIì|/Úý‡ÕÞ¨0o±?êOcàþ×èì>‹êƒû ú(žÇÁ}¿ÑØþ؃û ×Â'±ð?OF$2$;–“‡5Z&7) ‹G©ËÕvYq¦§Ê–±íoÌÈîLdóÆÛu+~Y<š’®Hpצ«TI•^œxî+{tª*tBÿÆ'lF ì9¾dà:[ fOõÊåAÿCœˆe^Èfåë²]uœ‹©ÿe|Y¾£"¼Ä‚· ”{Õ˹‘)dÆ×¹Ž5>_Ôê¢bj£Ù|¿pgÔîÏe4ÕærEǧÛS&Ž2h°T«¹;åáDbà~:¢bJäÇCA Có5¼Ù^+)¶ÓƒlñZ¨í¾A­sœìœ3ã\cp½è¸ÜŽÛåÕÍkœ¿ß¾¿¶£öþËÀ}} åTG*'':¹U©‘ÈŽr¸L‹u”E…U—9Òº¨#Öú½žë(×Uu1 ×Â݈ŒZ«ôÔk›W9•³¯VÃUµXEWcQ^šŸ*ÏTWúŸÝý´Z©§É¬­_—î½È™|Rõ—˜Û/̶}/éLf£rxS&%søŽïÌÇ;›Q]…­á™ö[ëWSÜ·êcN á~¥“:½’m›#õ1:¹‰…®_~â;0hÁ=‡:ø“Ù8»íñÊæÇ£ïË¿)w)w4UA^õIXUMl¢*´v\ŽFªµ_‘ï›#Q^ë*ÊâÅs;ììyŒs•Î÷´õâýF Øs}iìœßý‰''õ Á‚{ Ö\„,{ ÷¿FðÉ9¸…ùˆ0hžÀï¥DöL?sªEG'ènëʾ41öÝ™ì¬úº±zcz¸¤¹qsªÕ(Ö+‡1Z+a*µå¦áZ¨šn(³¤á¶ÈÅç;½ªŠˆª+\œèó¼Ñç¡Wò«¹B§.èwQÍŒuÄÑíªÿ›á=õ4`ßaâÃì©ëýtâ1±QZ²ìmr êcS'…×fŠŠ¸£åòE”Èd½õ+—W/×–y±|YlÙÓov',ŽrôÈ÷#™Ýê÷jBkj¾Ø½lÕ0Â1êÕe©‹"Û8Us®¿æø~Ü8ÓžÃÄ/eAŸo¦U ;•͈ƒ–ÊŠ¨+œ¢9È+ܼ찪®îA^å%eUT»º"¨×+G=U99õEr¹\¶ST³­ª5Ê‹ª1õ'f¾çÙ©ç[Ûƒ0OaÍõ§³aû£2øxAƒö‹.Ofá¾ÇèÎ7¶43ØWÑ}SÙ¸?±Òªˆ/Ä7HnÝ!ºCt†å ËMËMËMË Ë Ë Ë Ë Ë Î3rÃrÃqŒÜc7ÍÆ3qŒ×Æn1šøÍ|F¾#_¯ˆ×Äkâ5ñØlF¶#[­ˆÖÄjâ5q¸\F®3S©ŒÔÆ_|eñ—afif–if’ÒPíÏ·Ds…åGDÊ=щ/þA{¡Œg°å_žÍÀý¾‡½Ž{ž®z4DÎâœI^$¯GGÈ?äòž|òsÌ[)l¥²–Ê[)|†£ÍGš¯5\j¸Õqª¦ªšªj©¬kƱ¬kƲÈk!¬†ª¨j¡ªÓU¦£MFš5j4Ôa¨Òí.Âì/Œ» ã-Œ¾2ØËc-ˆDkˆ#”Òqˆ¶2Ì,Âí,ÒÌ,ÒÍ,Òí.…кˆ\¹råË—.\¹bÊYK)e%IRÊYÄ©*K‰q.ß&Ž ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚#”~#y'>#î!ÿËüä?³ûó8ϰ߷Ëÿ Ú ‚ª*U ¡R¥H*AAA1øx•ú:3øx41ŒØ3¯ƒÚ1ýnèâ~¡ÿÚc¢9Áqׯý„û\¿ðᾞQÎ ‚9AAAAw®h²¤AAAAAAfú_èlhc'°qKÛÚ×úçÄòÍõy0AAíÊ:8ï±ÿ_/úøDùpAr‚#”AT©R¥J•*T©R¥J•*T©R¥J•*T©R¥J•*T©B¥ (PâÉêœñ¤äþI{´Æ7Øx•ñ{B öùçIi›ÑÛïÜÙÿ¯—ý\Ùòà‚‚ ‚ ‚ ‚ ‚ ‚ ‚ •*T©R¥J•*T©R¥Jœ_דռøTž#ùù­1öëãOiÅö:2cª¢&Faâ„Þðæ÷‡7¼9½áÍïo8syÛÎÞpæó‡7œ9¼áÍæy€Þ`7˜ æ{€Þ`7˜ æ{€Þ`7˜ æ{€Þà7Ø î}€ß`7¸ ö{€Þà7Ø ö}€ß`7Ø ö{€ß`7ØMö{„Þà7¸ î}€ß`7Ø ö€ß`7Ø ö}€ß`7¸ ö}€È÷ñOz¢¯&* {øgïp›Ü&÷ ½Âop›Ü&÷ ½Âop›ì&û ¾Âo°›ü&û ¿ÀoðüÄ0üÿñ Ä0Àoðüÿ¿Àoðüÿ¿Àoðüÿ¿Àoðüÿ¿Àoðüÿ¿À|C¿Àoðüÿñ Ä0À|Cñ Ä0À?ùGˆõ^|Èãs°Æ7Ør/‰=§‡ÿÛô*"ŽÀ¢Ú+ˆ¦˜Šb+ˆ¦"¸Šâ)ˆ¦"˜Šc)ˆ¦2¸Šâ+Œ®"˜Šã+Œ®2¸Êc)Œ®2¸Êã+Œ®2¬*¬*¬*Ò¬*¬*¬*ÒŒ(Ò¬(Ò(Ò(Ò(…Q¥¢BˆQ !D(‚®H‡òC‰|Q !D(…¢BˆU !B… !B…¢B¨U ¡T*…PªB¨B„!Øìv;ŽÇc±Ø”% BP”$”,…±bÅ‹.j9³—£øäññ 9˜cØÙ«êžÓÂ}ñ0„G(!C±Øìv;{§öò}Æ Ø3¯Ëö® ~_èœI¥Ál'°qKáö® ð÷‚ŸÄ‚S¹Üîw;û^ K›'ò9¨ƒ ìZû[VªÇ&V€ï˹Üïøp„!Øìvö´Erµ¬à°äȹш7Øx¥ñ§µàθáz~„ÃÃfÌG À·?üîAˆ1ûe—§¶bÌüK‰Ã”¡ øÐA) B¤) B¤) UJ©U*¥T«Š¸«Š¸«Š¸«Š¸«Š¸£Š¸«Š8£Š8£Š8£Š<£Ê<£Ê<Óy¦óMÆ›Í'O4Þi¼Òy¤óMM54Ô¢”RªUJ*…¢iF•iV•i\eq”ÄSLE0”ÂS L%0áÈáÊðå8bœ1N§ S…)ÂáàÊðEx"¼^¯W€#øò?#øâ?Ž#øÒ?Œ#øÂ?Œ#øÃÿL?ô³sü~3/òyœ*«–´cF žÀ¾õOm¼F|b%™Šd>+ø®Câ™OŠf>)˜ø¦câyωç>'œøžsâYψñĸƒâ\Añ ø|Cˆ>!Äþ ßñ&ÿ‰7üI¿âMÿo¸“}Ä›î$Þñ&÷‰7¼I½âMço8“wÄ›¾$Ýqëˆ7\Aºâ ÏnxƒqœÜg7Í|æ¾s_9¯œÖÎkf5s¹\¦¦SS)|…žYåžYä¼—ãÄw;ùAAA*T©R¥J•*T©R¥JAAAT‚ ‚ ‚ ‚*A*#°kDOaʰÅõöØ V•*AJ•*T©R¥J•*T©R¥J•*T©B… (P¡B… (P¡B… (P¡B… f™¦išf™¦išf™¦iF™¦išhišf™¦išf™¦išf™¦išf™¦išf™¦išf™¦išf™¦PÓ4Í3LÓ4Í3LÓ4Êf™¦išf™¦išf™¦išf™¦P¡¦P Œ¢'±q+òý¾*T©J• ©R¥J(P¡B¥J•*T©R…J•*T©R¥ •*T©R¥J•*T©R¥J•*T©R¥ *T¡B¥H „*…J•*T©B¥J•*P¡R¥ *T©B…J•*P¡B…J”(T©R¥J(P¡B… (P¡B… •*G±ñkÛÜ`‚ ‚¥H ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ŽpAAAG\tAAAAAAAAAà‚ ŽPAA{7¾/zÓÜBËÿkÿY\Ÿµòvbúþ×αöǾÛb÷ý±Å/Ìý±™eéû]ßKþ¯ÚùÖ1¯¯í~-|Kgñ«å7örbøü”ýŸÄ,¿öºý9Åä'ìüË××ÈOÙüZÆ?%¿³øÕò“ÑWöw¾?%²þÎâVry(+fÿYV]ä'¯ìì‹Ýëä7×’þÌâV1~C?gñ«áòYéÉ;·öRrø¼”ôå‹íþÊC‹tåò‘ª¢aS‡û²¿¬Ë/ò16ÏnˆTá½öSÖÿ_#ƒI>|Kû+‰Xľ¾Gtן÷öW¾'íùˆáÌTå‰|²Pã—¿“*Æå%;‡õû IBí51šØ|&çÅdG½<Œi9¨7/&wgêIBÈY¥Øjc5qšØ|Fã¹Än±¼Fó¼ÆoXoZoñ¾S|ã|ó}Þå7™MÖcs˜Üe5²™ <—!$ñâ…*¤y7Ý’yG, 8—ô’…š]¦£ \f¶3_¸Änq¬Fï ¼Äo1Üfù†ù¦ù ñ¾7Êoœo^o2̦ï1ºÌnsùlƦbùKd'!ó!å^QÅQÆ›54ÔÒ4Í3M !F”iVÒCO á<$¡(JI%‹,J–%|žª¢¢–T)ªœ¸o¶¿è¥ !všŒ5±šøÆs€Ýà‹À†÷¿À|Cñ'ÄqÆ|I§Äø‘ñ%>"óâ9ˆf7ÙîsyĬæç9¸Îkf53Ê[!9¨âUJ©B… !D(…ZCHa <'„ð’ÒZY !d.…к5 š†¡u.¥Ôº—Rê]K)e,I$’I$“ùÜF5!ÆS— þIBÈ]¦£ \f¾#q„Ý`7˜ î}€ßà>!„øŽâXω°øš>&§ÄÞ|K!ñÇÄs›üæûˆ7¼A¼â ×n3šù\¥òÈKÏÜ…!H ‚ ¡B‡c±Øìv;„’I$–$±bÅ‹$’Ä’I$’I=ïøˆå¸Eñ¯±Ê…š]†£ \f¶3_¸Änq¬Fï¼Äo1›ìfù†ý¦üß›õ7î7ï7ù öc}˜Þç7yÍÖsq˜×Ìjå52y/L/Göü‰C7×äp‰9ùÈ©þbRª•RªQJ)B… 2†™¦…¢B¨U ¡Bž|’Y !v—i¨ÓQ¦£MFš¨j¡ªkƲšªj©ªãUÆ£‹¸»‹¸²’¤©$“Ñ%“ñÛëH%%I¸« þxòպ溚êk¸ÖqªãUÆ£…ÈáV|ž \’„¡#—´ók•«‹"eg§%NrI"©–uU„c>Yà%…˜]¦£MD5PÕ8ÜŠ÷ŸŽ®Çn]‰$’I$’I$žŽçr…*âŽ(ãMÆ›%4”Ò4#I $4Úi´£J´«Hivé’Iå=rI$’X²BÅ‹,YK,¤’¤¯–¼‘yðÙ´ßêzrTzxrTŸ/‰ú¹±¶WãF¡ ÊŽ(âÒp¸Ü†“¡­W*âT4{&G6«þI$’I$ž}Îçr…*¥T¢šji©¦¦ššf‘¦iHi¡¦†›M6”iV•BBŽÞTôÉ$’I%‹X±bI%I%|é,YK!Ûò8<Ҝս8”% !d.†£MD5PÕCU cXÊë/<dK+’­G"&8Uʽÿ¦"²är¢#ZÖ£F½ùRÂ+X׺Ëí2I$’I$ô÷!HRª•RªQJ)CLÓ4Í3M !D(…Pª„'(óä’I,X±bÅ‹,X±$’J’¤ûˆÓ±c¢ã y'6¹ZìY#:ÑIRTŸ)z‘‹—ĹdÔìÜŠÔWªªåpT5*ª–RËÊWñd’I$’Ië…!HRªUJ©R… (PªB¨U ¡vóç”’I$’IbK,X±$’I$òU÷•ç<Õ$EüõQS’sá²é¿”u'´Á B¤*T©R¥J•B¨BAƒ<¤’I$’I$’I$ŸòëÏn÷p,T{Ç¿‚©ÕÂf³|‰$’zû¤/DAJ•*T©R¤*BA…<¤’I$’Iÿkš°¸¸Öªq9îäß_ÁrrNmr±ØÞ™AAGâO9éŸÒ«ŠíéO_ÁŽh¼ø\Ôw•>Ñ$“ä"þõ®sUˇ2.!W(Ôóc”G'up¹¬ßÊîw;ÈR‚#´Ê|‰ ‚ëDÿyAÈ!¤´³K0»K´ÔCPÔ5 E5]ÅÞYä¼—ž#¹ÜŽmü8#’uGœ½(Šª¨©ËשDè°™ BèY 'ýGc±(KK4»K´ÔCQ CQME.âî,âÎ%Çs¹ A*T©H ‚ Ž}ŽÄ¡)Éy§àGB§Lù,Æ÷ô'~…“c3^ÊýGª‰Ôá9¸‚ ‚ }Èv;„¡d,Òí.Òèj†¡u.¥œYĸ—ÎäAJAA"L¹Êsž¾åÔž½,œ‘²ªÕjµªåéEò“œÆc{̸ôÔõëþù(œöùÇáÊÄäÖ«œ¸ø|mÕzðü‘%p`n%ÌÆ3šúj¹uqàc«™ õr§5›‰,X±bÅ‹§÷ï]‰BP”,…ºB桨jŠ]K¸³‰RTîw;ó‚ ‚ ‚#£·—uHR®çUއ$*ÚŽ{±=¬{Uز±‡Ì­n ®n^­ääzß›#ñvâ¼>1ØåúinÎ~Dk•É ¼Ñ|œ-«ªç½×zô#zšóEƒsœvG¿ž7é¿[Tâ*ÖsÕŒ<¿¤óS¦!;D{L¡(J…²BÈ\¹rÅ‹)e%IRTîw;ó‚ ‚¤AG(òÕ`·J"¯R1cÔs/v75õYÓÈ+Šì9œž‚ uýuVà¶M nÜ-{š­w7'd$fAªäʪŠ9°ŸZbî=ŽbŠœ“«M7c^Ó2*kãjë1Eâ>fåLW»ÌLùܱZ½6^j'5ð=zÔEéõó Ã’P’ÈX±bÅ‹,YIRTîw;ôAAAG™(O“¤h•Ú¸LN\ŠÇ#Ÿ…ÌoC“°ßN ~cõÕ¸Sñãʌř/‡¡è Ç+›EªóÕëuo‰Êª¾äe/$1½ÃV1S#_…ëĹwB¤* {š‹‘꩟**ä}—#œO~oAÏ^h¿ƒ=*"ù’I$O“s¹Ü+±ØìJ…²,X±bÅ‹)*J’§s¹ß”AAH ‚ „:»媉ê&,ŠŽÆöŽÃ‘©Ô£yWp½1¹ÜF·ËňƌwC½[Ë*cp¹4ŸÅýmv¯ Ò£Fºâ_Ç’™cx¬ŒGž‹ÍÉÙô\Š¸Û•Ífç$.G*j¾íÌöгÍÂyONÈ'’§ žzùÓÎyGTrîw;ÈR…*T©R¤'9$”$’Å‹,X±e%IS¹Üîw;òŽPAAA£ð¬'( …ëQ¾£¸f4L vG`b³©Þ©É¶w Ùnlm˯‘´K„å…ñÂäËqž>7™#†èxœ—";ì–Ç"ãUézwN™ê^ÊÕòè‚yKÙQ|§ Ÿ›vç$’X±bʼn$’T•%Nçs¹ß¢ ‚ ©Auvòd·šÞ[\°ü/aµËÖ¼›éÁ".MLÐÌîDâßÖ£yf\qÃÕxŒNÇ“­ÂÓ^›KwGc¾E³ú]ɾzµ¹Z5ÊÕsÕÝ.ôé× >ƒÓ² éÒžºb6ªä…z å¯eo”äÊ{+yGGc±Ø–’ÒÍ.…ËJ¯)$±%‰$’Iê‚ ‚ ‚ ‚åûuI>bóFªšo䨨½J'&aÅ¢ôÁWcᱫ¢ÝmåÄ2W‡jnXÖ¹ýKè'§÷W;Õ84MW*¹z”o,™nÌy÷(näžœ§œv澃:ã²5TVÀ¾ƒzYê¬CÑ\<_A½wìª*Èäì7Êz Î#—bÍ,Âí?´t5 CPÔSQMWŽ,îrJ’O\) " ëÊ ‚ ‚NŽÝ3䪉æ(œ™Ã#±¿$nÙ3c\OêQ9bLás?ŠFðØÓò-²u(œœ¸ÛÃÙŽ{s½ù³¢&n¶Ÿß‘—‡Ë§‘®Á…z—“}U9"Ç¢u@­èw Þ–Š½Üƒ`XæÎ´úQâª*õoOö®Uå=¥y;Õ:#”˱(;Õ»K¡t.†¢†¡¨¦£…r¯9$ž¨ îw ‚‚*AR¥J•*T‚ A„'%O9yµq¡˜DUUEjõ¨ƒ[gdÚc{Yƒ+°§•zÔNY4[‰Ô·¸œþ&uº”N\CÕ˜™’2aÎÇ+Ýgu(ÞYr®Dc•Ž^)|–ôÔ©Òv^KÉ=:jBAg“„êA&K / Î”$U‘9¨ß-âuv% !d.ƒ–y]K©u.¥”²’§~HФ) UÅ\UJ©U*¥T…¢´©D*Ò­*…J•(QJ”(TªB„!?P”,…”óÔNTÊÌWˆbpîkqu¯> ¿5x¦ª³‡çDgƒbxŒº¹peÒ~¶j½×©FùÒžD$"õäÞ†×ô£½;Õ}út¯s°åæá:‘Iì¼ÿ¡žT á ¡t.…к†¡rêYIå$’I=0¥Tª”RªT©J•B¬¨JBŹw ©R¨UB:ZY¥šY ¡t.…Ë—RêK‰q.TUJ)¦Q 44ÚQ¥P©R¥J‘Ñ*„!d¡(Jœõ BÊYIqâ;È ŽHd^HŸìMÆÏ-D䊩娞b‰äú ‹É:Ûëè°/ ß!=\³É}ùkè7ÒI$’I$’I_5<¨äpA$ô+„r’§~ˆéhÿA¼š’î#ŽN´2rgäÿÄ*1@` !0APQp"q‘°ÑÿÚ?ÿ„«,ZKIi-%¤´–,X±bÅ‹Æ1Œp?ÑB„!BˆB…'}¬l±iÆãeN7,Zp™ÙFËœ#ßÅ<([%ógâN™¼ž¤É3Ø Å0ˆÔü¹åù#?Gþeî Ùˆƒò!Aêrõ?Ø÷f…&Dìþ±²ò}—À‚v1ÆàŒmÿÉv²P#±:‘©ì’…Ø]‰ÓçNS±¤ëã}&²c‘Œ>rŸ‹<}|yøÈB„!B„!B…ã÷ìck9ÇÅy±ŒcÛû'cYÎ6R/4ìOcîIØÆ×Öq®Ê|(BÎtØ¿¦1ŒcÆ1ŒcÆ1ŒcÆ1ŒcÆ=–[‡N[¶c^[‡^[‡–Ç-?lÇ-zc”aéŽQ‡¦2y4iÔ¤yTkž.U‡R',\–:pA8Oê1iÁÔŠE%d¬•’’RJII)%$¡B… )`¬‚°( ŃLñiòŠE"‘IY+%d¤”’’P¡B… AH+`¬‚°( F1î°éœéÐ…"‘IY+%d¬•’²RJIB… (P¤‚V ÁX+`¬ ýi‡^[ƒ^R„!8jŠD!©R¢„(…c±Û(BFx cÌQB„!B…v;²p1ŒcÆ1ŒcÆ?GcôàÓé1ŒcÆ1Œcðâ•$¶ˆB„/ êçV-<+æ¡B„!BßÇb%ôÆ"Ã,X´“=,c÷HB…ö°ÊåØgb‰ôaÄõ…>hì?ðàÆ?2H\5¡t¡BýÿÄ- !01@`APQa2Bpq±" ÿÚ?ð:R”¥)Kæô¥)JR—Î)KÿÚŠÓLŠ0FÁ#bŒLLLLLLLLHB„!1ôBøE·}˜ô—E¥Ñô1oØÒ5t#ã—?âG¸.¯cü‹¢?«ýzþö/ê8©þ Ñð=‘“LŸÙï§ŽÆëÿeþBnQk|Gcâ6ø ¶=QÏ€~ÆžÅôøçÇz-±}lœoÀ3Oc«ÆßQv:»wîW²» ™—ýÙ“2blÑÐ}^"coØÉ±¶dý‡¯‰Ÿ&Úÿ"¹|rêqv/¯o6Î4ˆ‹dD_D$Ù”›†“1F(ÅS~=uØ»ãKÜ_JRî^┥)JR”¥)JS"”¥)JR”¥Üöbì_NÝàì!M¿°»];‡Ødd„ï9u\øBì„!B„! ¹ì…Øêí}ö¾œšR”¥.ÔR”¥)JRîiþK±Ãþ‹O9íúbû‡Ð¥)JR”»/k£ù-ÊR”¥/m«c»×µ{˜£`Œ‚0FÁ~ÌìÁ#`Œ‚0FÁ#`Œ‚0FÁ#`Œ‚”¶Â„! Úݬ÷ø}^ î‡~ »Ó·h¼ë²•¥EEEEEEEEEE_eE_eE_eEEEFH¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¥)JRî®ÇWsB„DDDDDDDDDD!NÚ„!B„!B„ìõykò×ýÙ~Zúyn¯-Õ寯÷y“ÉßMÖ_'{º¼¡î¾»O&{Ù#OO*Öæ–ÆÛÙ£§•zŸÄjlÑä¯wÖ}µI§¯„TUödŒ‘’2FhÍ£4fŒÑ™™™™™™“2fLÉ•”¼¯[Ž­ºzüEE_e_e_fKìÉi2FhÍ£4gú3ýþŒÌÙ›2flÉ™3&dÊÊÊÎ<ÈNë[ºžÊ.»iQQ’2_fHÍ£4fŒÑš3GäGäGä?'èüŸ£ò~Éú?!›3flÍ™ê3fLÉý™?²¿²¿²¿}–ú®h~[ëÿ)¥)‘뾞QJŠŒ‘’2FHÈÈÈÈÈÈÉ•™2²³‰ÄâFjËIž¯³7ödÊzz½ˆB¿·ÉÒ”¨¥2222222eeeeg‰ÄŒŒŒŒÅ˜³bŒQ ÉõFŒƒ?4iiîúÿÏà©y„!CˆˆˆˆˆœO-‹×ÒôÔ¿×vù:õj]©ÖÛÞ¦HÉ£='äÒ-iüæŸJzºµ{>Æ¢¢¢¢”¥)KÈ[=]Õ½êuÛCÓ_úÛw***øÊTTTTR™¥)YYYYÇ‘ ²„'5ª¡«N/wVŠéøÿgã?0FZR{5t##1f,ÄÄÇ»»·Ÿ ñ>¦Œ—j¢æúº#«Çµ_cN»ÕGÎj¨= ?„!M“¼»ÜÿŸ!B„'uJQ=ýFž\ù‹½K»J]¯bÜ„äB|Õ*ÝlËuï>lùº]ê'ºúžû¬›¬œ¥¼·—6„ÙB.ë|…×u“z †ãì÷P÷áNe)JR”¼›ÈH[¬‚ÞŸ Bl!—m)JR—Ÿ9º|cß}uçÿÄB12!3‘ APq¡"04BQ`ap@r¢±²b€‚Áá#RÑð’S°ÀÿÚ?ÿå2D¿ü_þá¦Ói·ª‘"’’’•$¤‰{….ºD¥ P¥ P¤¤‘"Y¶êÿƒü †Î¯o["D‰ö^ÊÉ“&Où!]Ä›ÂÜÉ hê®´l6 H´± jƉìJo&&h@H,-­-Ci¢A]nvh–Æøw„IˆáºK4Tðàë!ˆ¾(X›*f±"P¹¬B:+š•Õv’FØŠÙ(ŽËmØ*䦃ô¶ ƒ_Þ]Ó Ì¾S\È&d,-LÎà.dà8h¥Y™° £Ì¢q̉›EÉ™8Ž_þªßül|MXXdÕg ÌÑš™HøfÊ}MŠ;Á ›£àE ?á2¹_LÙB(ª1©J(œàe—øÔ†É‰óyÍ8o9ê@]E"D‘ÉÑ,hªD£4Ó¢Ûs´lnÑZé(ÕjØ* ÄM‰™õ„£^™²–à*ìSKõz eø ₤v Ò ÄËø‹bð1‰“ÑŒ!°ÿÓÆqšV ŠeâXŠ»ù‰é䯦öwŽi®i®j—5KžÅ-p©4UsTE‚©:P!d~Ñ]âzÔ’BÒ+¿MÊ»•¾Ç3Žæ\ÑÜlãìsEÜ‹Ãs3Q/aœ¿Â.ä]ÌšêAEBÅÏ9ÑHêG2!°ÔUØXZ™ä@¤´§4³Z™­LÈ»DGLHys*¾‚îDÜéÃYKG‹s@‚ ŸMVq2d" ÝE9Û‹ê„H?¤[>ƒ–Û#­ˆÈ$Ä^«O.e×r¦çg yæžxªæ°µu`[™=–êË43¶ÂÒ9©4¶”‘,l3A­Hš:%©`—s³‡±¹4ôÜ«¹Ûìkr.èúëZ¥–”s(æP¥ P¥ R¥*R¤œRâ—RN$âN$âN;ÆÜ ¸p6àmÀÛ5ÀšàO‘>DùäUȨ¨¬¨¨¨­ Э Э Э Э1+n%mÄ­1*LJ“¤Ä©1*LJ“iä¶§¨‰¹t;ެKOR(Õ(qC°(vÀ¡Ø»—`PïüJC°)\ W•À§í)ûJ~ÒŽE<‰r%È—"I$À¥0$„’BM$Ò–’i&”´¥¥-Ä¥1)LJS”ħ™O2ŽeÊ9”ó(æP¸”®%+‰J”©J”©'q%$¤œwŽö{ÍXì*\ S°+vn+qxâñy—«Ì½^eêó/—™|¼Ëåæ_/2ùy—ëÌ¿ü—ÿ’ÿò_þKÿÉù/ÿ%ÿä¿ü—È_!|…ò¨^¡z…ëq/^4¼n%ãq+n%mÄ­¥mÄ©1*LI¡4&†Ãa³6γ'ÇɯÕášÝÇ$)B”)B”)B†à]·í¸mÀ»nÛp.ÛvÒí¥ÛK¶—M.Ú]¡v…Úh]§2ïò]”sRŽjQÌÒl¿;OD—€»‘t?†«¸æw e8 ólëÁyëü#¼™ôÕwËõòPþ¿íß*þ7âêå”^;‘wBj»2ñ_ e>Ÿ“úÿ¶wü‹¿UëëäÖj®o¯ôüù×ûgÊ|Š/Æeb–*Àz ƒ•,H­¥¶§ŽáO®ä]ÒÎ:¿LÍúyÜPgÌìù_“ûŽãñ™O˜ÓïB 1õÇUñ‰«“Oáòjj·6M}È Åqv|¯ÉýÇqøÈEaàXª‡EÊ…«ŒMV7r&êo FæÉ/§?© Ÿõ~så~TüŽù·Òj³æA©äæpÖȯ¼7ú|Èd¸;óŸ+Á¿‘~eßLM\ŸÊ»©œ5}f݇éåE\йr*äUÈ«‘W"®E\йr*äUÈ«‘W"®D×k5ÀšàmÀÛ5ÀÛ·nÜ ¸p6àmÀÛ·nÜ ¸p6àmÀÛ·½ÞÀï`w°;Øìö{n{nÜöÜ ¸ì ¸ì ¸p;؈D:2K:£¤ä‚Š×%„Ÿ'àIøvŸ'àIØìöŸ'àIøüþ¿ßÀïàIøü ?OÀ“ð$ü ?OÀ“ð$ü ?OÀ“ð$ü ?OÀ“ð$ü ?OÀ“ð$ü ?OÀ“ð)~/)yKÊ^Rò—”¼¥å/)yKÊ^t±õ+”šÉ5SÑ]È»©5m"ÒdJ‰C±.݉vüK·bPìK·b]»íØ—nÿÈ»v%Û±.݉tìK·b]»騩vìK·b¥Úâ¥Úâ¥Û±RíqRíqRíØ©v¸©v¸©v¸©v¸©v¸©v¸©v¸©v¸©v¸©v¸©v¸©v¸©v¸©v¸©v¸©v¸©v¸©v¸©t¸©t¸©t¸©t¸©t¸©t¸©t¸©t¸©t¿qt¸©t¿qt¸¸¹_¸¹\\\®..Wî.W+‹‹…æCF ðF”®+Jà¥+‚š*ȧ‚¢—¸¸_¸ìë÷~ã³þã³þã³þó³þã³þó³~ó³~ó³~ã³~ã³~ã³~ã³'ÜvdûŽÌŸqÙ“î;2}ßòvdû¿äìÉ÷™>ïù;2}ÇfO¸ìÍævfó;3y™¼ÎÌÞgfg3³3™Ù™ÌìÌævfs;39™œÎÌó0ìÙ>ggÉŸ&v|™ÙòggÉŸ&v|™q“À¸Éà\dð.2eÆOã'q“À¹Éà\äð:,bpB.]\¢ÿíÆ¾v–ÿË8wÆ»­É¾&L™2dÉ“ø)r$H¤¤¤¤¤¤‘-ßï[¹uÃÇËÒÝhÜDÉ·r&ëE|±-Þˆ‰jŠ®­ErùCÐÒo’&L™2dú™%»z-³ÄЬr„Wr®í³p_Ž‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D³ì6 †ÌM˜›16bM¸“iSJ˜TÌJ™‰S1+Éâ^dñ/reë ÖÌ/˜_äËü™’Äí3´äÎÓ“;NLí93µ0íY3µ0íL;SÖÓµ´ím;[NÖÓµ´íiÚÐíhv£µr;W#µr;W#µ.j\ÒìÐüÐüÐü?ÁÚ‡ø/ò˜‚ÿ)‡ø/²Ÿ÷è_e?ïнÊ™R¼©ÐÈiq ÄF!_*ô^¥­j—M.š]0»aC P†°¥…,$Ìîw¹ÝÀîàwp&Ü ¦IR`T˜r*äWȯ‘_"ó‘yȼä^r/ Âð½RõKÕ/T½RõÅë‹×®Ä½qzìK×b^¿õø—¯Ä¼~%ãñ/Š—ÅKÇâ¥ã±R·b¥nÅJÝŠ•;*v*TìJ—k‰5ó:ù²D‰$KÝ™y¡?›%÷e}Ø_vWÝ”÷eÝ•÷e}Ø_vWù²Dþl—Ý…÷eßÍ’'ód¾ì¯»áîÊû²‰Ö§v[îcºß¯¹ŽãÔ¢j;Ü·pê•}5‡¹kÕ9Þº‰îZ'TÄÔorÓªo ö{4*B´+B´+ Ôµ=u›ÃÚi“B¤*B¤+B¢¢dͦÒJRRRRR„Øl&TTV¥JT¤ÔÛð)¬žÂÌš!R¡Z'›i"E%%%%$’jL™5&¤ÔšõÓ&L™<Ó'ñŠºËÇÌ“*B¤+B´+B´+**&m6’R’‚‚‚‚”)BY¦L¨©J”©J”©I“&TL™2dÉ“'ÖÈ‘"[Ö>+¬ï"Ì© ­ Э Ь¨žm¤”¥J P¥ &i“*+Rµ+Rµ*R¥&¤Éùµœ5•=7,É•!R!R'›i%$RRRRRI3̨¨­JÔ©J”©I“ö5]¹æL™2dɓԑ"D‰$H–¬É•)R“R¥&¤ý¯g»+èžêKUëëîË}Ùjz{°‰ëîtµ¥ªÕõöº]Tú™%«"E%$‰# ÖëH—·³ê%™½„‰$H–¢õ,O_jdH‘"Dµ¦L™2}Tµª^¥¾žÎO©—ÃÚG=‚"êÃ^dÉõN_oašE‚Gá¬B…-šû¨Õ‡4fŒŠ‹èÈ(»ºdÉ“'ð–-ž"¶ÇÅ ¢ªÎÁÜ~*dɓՑ"DºÉ%ÕÈ‘"Dµôi/ƒ‘"D‰ ëç{7É“'«"D‰|b*ï‡g¢{-ëè³øtÔ†¬‰jD†kT±H{a ³I|TDC¢…ª*ˆ‰š:‹šÂd}”´³4ŽÄ" ‹ñQ-BÄ,#ž~Ü[%öù­,UE ¹£ñ 4öö'M-"ŸŠ‚9=»ÒgÕ>: %öî-[N’h?Çb› |fŠ­©íäÊŠ‰“'©"D‰%©2dÉ“ÞpD-óôÉêÈ‘/#tSë©f¾‡y$tíUÐóLõåÔO4úë~"‡a™EKH6®ð,CF1X|Ó°"æ*gDñQîÑWh®«52®_•©sÿU&ŽSIuQÑÉÚíªF0zkYì——®Â+¹”‰¾M‘"D‰|Dõ¤H‘-Y“'ÔèM|v £.¹¨: d³3'µÖ¯Vö§{25!ô å³z)<ÉåiëKVdõ¡ñJçZ²Må.¶>hŸ\ÔñSF1³qÿÿÄ-!1Qa 0Aqðñ@P‘Ñá`¡Á±p€ÿÚ?!ñ–†ö|âD(i[@æ9‰©K"dÉ!—/³$“D’Ý¿R‰&–ðOt÷SL<ÿÁ!dB il&J†• "dˆd2åËÒI$’vB{_h%[=‰Ø{ù.;»ˆdhF…b4ó“'C@Ò4 &L‘2 ¾Ä’MIQ:³Øñ¢$’I¤“YüÉ~bD24HÓ¬G3˜æ%¡È4:-q ÉêL‘ †A †_fI'RI¢Iüb.„BÈÐ4Í@ѧÎ!™Ìs&hò%—á´&ãò xÙ‰7*mÄìÍ&¶À‹ùý¼$’OãÓåkòä¯À?ø³ñ“ÿ…Þ8ðsãÿ-~!øØóyÿ¥pðñÿšZ5ð y;*þrɤíOü¿Âçxÿã‘ápò ¤eø8ðù¼y£ÏíÃÁNÔÿÁ-âÖù~‡á¿ã6'M™DżÆ|—ÓËæˆ[ˆð«;ðXð^ž`œ| ü‚-â[—e<•sðÑøùË~G><ºþ_ã§óhÝ<‚ÿó8üæÞ9øØÝGäñã'ÿ6?Ç_ü~6cþîV-ùCßáøfžT†·¶ñVÿ”Áo—Ž+pãàÜm/>~÷­Cñq¹ôn¢ª‘ùÔ8n£} Z|$mFñ[b6ãj6¢±µQùìx©ßC‰‹nb‘µ±1³‚7ÐFÄlG’ÆÛÝ´~2)¸"‘¸‚<’+lE ‚)ˆ ˜#n6ãÌ#vðÒ7$I³‡Ñ´FŸ°Óö^ÃEûpÇÒ>YÈ9fŠ÷4ÿgIÓë A¬;ÛO±®4/Šo†ñlg´¶cj"A·ˆÜG„·“´?’5+oP²FƒØíGd;ÚÌi 1ÓgYšïsEûfsèë„rý•(tÒTœ¯ÙË÷gT‘énz½ÉkîŽwèçœßc¬Žg±Îö: êƒP峕K@Ð4¶Z£\i3Aò!äAzFò+R)±AE ‚)R)G˜ÆÒ‡Ž%Æ*Ы_#z 6¬aç²-‘ "HÐF%#HÒ4!åÇ{fª4´"9!‘ "ŪHÔ¡¨k†±©¸€‰ÌNbs̶e‹ΰAAAADAxKÀ²6Áy”óI$¼’7Ü7™î¾6xocf6n\¾co3Qš”Ö5 CXÖÞ”/¯H‘- ˆe°ˆš'’‰1ÜÆÌR7¼tyay Ê"—•½µ¹u{œ÷‘á™iR6à“q’IÇ ¼»_"]ÏËÚM¤’ü‘àØdukKW™DÂb(ÌèÄI\&¦îCâbKóãØHb‰œ;vo$ŒàS$yÅiÝ5ÏÏ^ĪDZ±ºc"þÚ0¼¿JÓûDRÓú6ÜAF ‘·!õØÐ”Xˆi"Ü,qLXRNµ‰³üÄYñÈÌ€ ½QÅv$Gø¿ÐÜÌÂ>ÿpO/IY44¶â/m¨©Äâ-6 ŠãÍý$•uÿ#¿ô]„ªK} xÓÛA9˜R¦>¾FYò¤êT<¨ÓN!ÎD9ˆ¾CëDX’¼8ä0Ãcë¾cÀu£œÑÁÀàM§Ê|¦dÅŽ“x¶· T°x{F¬>cÊù~³HyF]®"ÂÅR£ep5F ýÁ`ÜUýáLZtæçœáj¾à²%,lßú!à$–/äN¡´žb¦„ðc,‰îg% ûý¡)œ ö›K†%¨ÊVÄgˆýÐÇ)©±¬ó zZŸ@ÓDNÜ”“bRÌj`?t‘!+ƒ£…Is%¬¿ë–ÞnDWt¶©cqZBˆdÆb²Š67ßÈCÝD¼ïÒ×ò0:`-h°òHy~/–°ufܱ+‹13I–¶ÎVYYòq Q ‹Qq”WCsáf…’¥Â9ÂÌ‚NË$E2D9¿ÙÆm>„‰à¸ ¬¨ã‰ ÏÌ »»8Š]L¨oÑ B»•!¿*?ÙíŽ6T@û¥¦}$,ân 46³–È'´M®$d´’oð–w¡¿èî Åßo[<½Ü\Kìˆ-vŸÂàÊL !à-z×KRf Î>¯ìÁgÑP¥Š0ôÆÜlÅcym§À/"X<¯šç¹j™Ö)AQàdEˆ#«[ÒA‚“r»¶ß3ô¸àfK͹"y%Šà7ºðÁpBÔkI xyܬ–'‘öÿ$?³åÏzêö#j+128SãùWÇb)H¤²ü;ß%ï%â4«mK£Ëu¡îŠoÛæYíFæu‚§=¨8Ÿ×óLŒ†A#aÕùuš#¢ ‘ãš—?$=Û8o ä¢Iò—L÷œKØù£¯ ‡GGW·¨ñí[®4 ž7Ñ?Aâ`ò7³ªÙÆg-º‰Kƒ’2„Lt®]k€k&Õ¨‰ªhrZé‚$ħ؜yÔ«‘tØ&&4­Æ"nÖ­V,¹¡ å8‚êÍ5I¤¦!gÉ#EÄ‘ ‘rpÝÜ Ci9KgCpgZîëa™ŠiLZÆá3:æ1þ©[)LÒÇB!)šXÆë3‰•~(ð3Ü=§åžåøÓN£ˆ°ò7Á¢Ù¼Yé€^¨™/cÍÕŤA¤ó0êMBf<’¹'¥¥ÈQYÉïHjÄ¥DZȘ•‘ · ÄÊ rþ€4® 9bɆI‰BP1¸VqmJA atr2 âjÄ•¼ÅH•“_àˆ…fHõ¢+#ä„í-P¤—ƒ‚«‘¦ùÈö.`câaßè´¼g1Iý„ãÕ‡ôkAðÌ+MÂÃÂÄýÈy)F¦C/ÊÉq!˜Œý .VF"“KTŽÄ>¥ÓÀµ<Fî+# d6pD4‡Éq‘êka#~æœb°â¤º“ö!¦\S‡èp8 øÙà>'?>Ã¥I¸Æö$‡²ë…<•­lÖ_á*ë‹r*ÔIÙ.+ÊmDR"ø'oÐ÷R†0¼²" A‰¶!N0£¤à&[O/Bm âÉ‘"á«›Ô²63•ŸØ‘ð†ìEÄáá’æ4³$A— ƒ+'3O|Ç6J‚£¤×™ͯðnॿD6¸ÿ•V˜}tÿf//ö9˜$ÿeñ/ C)ÊÎKÕ’¹[ëÄž ºú2aø«Ücò³:xÉz’—?Rë_2/GÄŽ¡ûðéY` ÄæCÞÿ ýžÃÌDõÖ` ó¤É$Sö8êýˆi“"ܤÖë?QsÇ6c+ÆÓ˜i§7œN¯ ?å cÙ{§Wä>€Q<7º´ZPü¿„‚6`ˆ#f7M[ $–‚Á 2­’y¢Æ #“cÅæ&“äàx¶ï«"²JÑÁR„…£«ybzfµÇBÙb,˦šá fä7ùèð?ÈêÉæB q…sa¤ I ²P-#mKØÛŽÌ2CbÒàc»¤Žæ<Ü–‹YˆýéŒ6ÇácÂ,«6†´!6c @ÑfFÃ#ü̈‚,E8œ|«ÜG‰¬Kn¹ó¦dƒZ$±i#xòž÷vø |kñ_œ¶=‹iú=ˆ=kÛÏǽӣÝûÁ’Q¨–[Hñm ÔbñÎoÒ=¥†EÑ´yVG Ž' f:|W.g œ·™)Â<+‘~H÷©Ê1'>”[áÞÄ ÍE‡’4?T=±Ë:l–%feÙ@‚pBiÅxñ9 \ÑbXÎõ6\Æ›KÑ;.(a¨,…F¹ð-Žáœ!”gU$ŠôlÄq¤I »ŠñºjB—d›cX‚æ„°|„ÁD×ìx‘êë¯o©Â³­Z3Ýq®GâEÖéÕíFÄxõ)³Z¼ nÞÅ£S‰ÃÉ-A]ú6Éò@®cüô3÷é‘ ÊÂPâ'ûH6Ãe•bêQëö8‹ýçþšý’LYqL&uø=)ÅWàÈàEéï³é^¸l[MŸªF;lu{1GàÛÝ&¨–ÅÆÝ]-|—]§i`Ú±ŠS”Æû7"ˆ’ûÈ…1`K ØE þ« ÃÄÂh·¨Áœ\I>6ÈJQoStM¦šD Î$ªÈò•üa‚)E Ršky­N<í)å yús[Ó‘ªÐîˆï‡yG^Ž £tˆèÓ£³#°#³/“²/“´^Ê6©j†¸jûÆ¿¸wÞ§Üj}ƧÜj}ÇAŽ¢gE3Aìͳ4>ÌÐû=¨ïç};™ÚT‡h´c˜ì㱎Â;í#º#™~‹f¿D¬ÿh›°ý³,F'#!äÈy?Ù Èy?Ù,Ÿì–L–TC!êC%• bb ‚ AAAAAj‘áu©Wxöˆ…â`òg·²œ½³$ èZ U¢;Ì•µÁñU xs¯ñ¯Êå´£C°cÙŽÞvÓ´’ï.çR~N›'Y¿’cù!ö?“½ï;ìM é¹Ôú©¬ëÈÖõäk:ò;gàìσ±¾ö‹àï¨Î÷QÖ£¢GLŽÆŽÒ¾NÀ¾Lk{ÉÖO“¸O‘Eòòˆd/n$2"ØD©´,Ìhâr—ôê?'Dù:ÇÉÚ>ÎÌþNØþN×ò;hí£µŽÂ;8íã¶ÐH†9Œ3Òú{N¾Ó¯°ŸO,9ÉÓ‘>÷ÁÞŸÁßÁß~Žïôwó¾ÑÈïÕà;ìB~â'ê úâ~¨Ÿ¾Ž»GQ£›ÜŽor#?¹ëÝ ½ ô#)Èk[ óûF…ÀÂ/ eŒh޾PöùlÏ®‘)YJä ÍËæ^·Ì¾eó/™råó=v ‚ "HÐGf;;B;B;J;*3=¡Ùge¶vcùénÄvc·˜í‡\έsù:çòuÏäïß#GïòKÝü¦NÈu˜²{¼j 9Ž(àZsdÄZÒS圳–r÷_ú‡"‡"„‰ò&L™<‰äH™"DªJ‰$’I$’I$73Iz‚pö$Ñbj$ÀÀ/èèèKÉÚÑ{¼\#Aæ5È“€øŸ%–6Ž?ì$¯&ÿÿ²(‚ ‚ ‚ ‚ Šñå^W°A<{£Lt1y;^µ°±Ï˜%‹äüõ§û ÚKhñÀÿýqÞØDEL%˜(È944·P‚ âÛ£Ï7Ê ¶b{ Ç)K„5d§Àe¾Œ*Œ.ðp-s¦ä\Î1ì^*'»g¸?ÿè ~õç·1zPOö,f4»¡×‡‘¼s¶dö‘RcƧH¸<¡âpCÚ‚ ŠÁ™mp¬…uÐr©²¹ËÏQê¦y38p,Ðå®ËßüªøˆsüÍ$€¼XF¸ä<"¤l¨°ÆººzÊ…¸¶^Byz¼ ÐÍ&)‰ ÌÕö#ÄÅ0‹ýQà¢^‘H ‚ ŠAŠÆÎ{=g3¥d«þÿðHô¾ˆô¾ˆô¾ˆô¾Ž¨ú:#è‡Kè‡]Žˆú:#è‡H‡H†G)9’4ĉ$HÒÄ›Nòþ"ÁYì¢Ø!ø—ú#$iŽûÿñÛÕ!‹EcGOKŒb ä(ŸSˆ¼¡º£{ sF"ÍcûÜAAAAAE ¹È ‚"ˆ Š ŠAEAE7ÑóG¡l‡ÖÐ%Þ¶!iìBÓØ|žÈHööGK#¥‘ÒȇI,ˆ]$BŽM’ôÖ[Tÿl[ØP5)Œ8“õBÉrÆÍßÅ€.Ò£ ¢1V&ÃòÅJ ä+-Îîí ‹˜Ž›ÁŠéAAn€AAAAAAAAAAEA´rh•Ÿ^ãé2— àútÈuÜæë܆}{³ÿ>Hg×¹Í×¹ ú÷9º÷!g×¹ ú÷!Ÿ^ä:îG>½ÈõÜëÔÑ "rÙ8øÿÿÿè KKé蚘-‡ã0™!xâ1.Ó±uÊž™ðiY§àR—©î'^½É}wE”N‹":’Oì‡Óû!ç×¹?ßÙ§öCéýóýýúd>ŸÙ®äºdºgDý’éýÑ#­ü ÿÿðôyÁ¤¥ë)L𑽟ŸCòUEˆÒ̓I¨2Å€Ð8Äú——¡¯÷ÿq«÷¿q«÷ÿq­÷ßq­÷ßq­÷ßq®÷ïq®÷ïq߇vìw™¯ï±÷ð5½ã[Þ5}áwa#î:n$}ÂGÜ%}Ä+xë°»ð“÷ »»Ð”IDI¾“A: u”I@”ID d–I@–Z¡­ïš”ƒüØ-Xš°^#› ¶¯–¤Þ[÷QÐ>Nòt“¡|–º³¨|käÓêæiõs:ÇÉÒ>N±òiôs4z9š}ÍŽf‡G3K£™Ð>M.ŽgPù:GÉÒ>N™òu“®|Cäë'\ù:×ÉÖ>Nµòu|{äëŸ'k|[äío“«|[äío“µ¾NÒù; ù;Jù;Jù;Jù;jù; ù; ù; ù;Jù;Zù;Jù;Jù 5ÜÆp+Q’;5H!Áä'ƒ$f´?(iÙRÂJr€ë5s¯üàéIø;ïàþûø;Ÿàþÿø;Ÿà½~õøY‡züü:ûøt÷ðé¯á×ß翇M¾þ}ü:»øuwðèïá×_濇^´þ}ü:{øuçðêÏáÖhuOðèŸá×?â‡Hÿ¡þ3ü:GøtðëŸáÕ?ê‡~øSü;×ÀëÏáß~X°þï>^¬þ íø9çD¿Ã½~ñø:ëøu÷ðWç=¿†¯ßàwïÝt¿s­÷=65=y_±Ðûéò#ÓæG§Ìê}ÈäëÌWúw/˜ú/ôé¾g|¡GWò¥ïäu# ùÌî3©ùÌé>g[ìt—Èé}Ž¿ØŸ§ò'èü‰ú¯äŸ¢þIú#±?“±?“´?“´?“´?’?Cù;Cù;“äîO’cä‡ÐþHý’?käÚù!÷¾H}ï‘b?”¸õг‘ǬèO!¼½ ü¨inoq$“I$’I$“ÔšÉ,’Y,—%—­ögcÔõÝzžµõ'SÔõÛ‚4#Oóà?Ï‚_çÁ /óàÐëØ‡gÑ ºö!“ëÐ…—^Ä2~ßF‡ëèÐý}Õ}-ôGEôGEôGEôBé}¯ë莣莣è?_DuEòýÈô=CÓŽ‡¹XåÒýBàa^>é¼EäkkÖ_êN§©êzž§¯^ç©êúõ:u$ôîOGöONäôdôdôgKýž¿¿³×özþÏorÚ{¢Ú{–é–éÒÔŽEˆ]#Å´öG§ëèéÑìú!öýìú!ö}Ñ}Ñ}Ñ}«ÛèëÔéô['סlºö-•^áøgKnf‰™w¨¸ o!À­|ª àuÆbuÙ+¦[§öJéý’ºd:îJÏ÷ö[®ä¬ú÷'®™+®ç]^“®Ç¡èF„hFˆ¶]{×Hzö%Ÿ^ijý}ôGEôGUôFO×Ñ?_D&?§ÑŠëØ|O ÷,{”ß²©ÕÀL<…qt•5.)ȲãÇ™a•=CÐôÜ_2ù—$K'§rz?²zw/Óû/Ÿïìë«’ôëÔžºdòëÔ~”õ¬AèE°ëØ:ö!e×±Ð_F‡^Ät_C]ÑÑ×Vò7¶úÍ’ÖiÆ€JJ/é#y)më(PÙ)5åùSÐô¯¦ËXXô=MŸSÔõëÜõëÜõëÜéÔ“Ñý/öt¿Ù=_ÙÒÿg¯ïbDiú#O×Ѱ‡Ùôt·ÑèzxY„c_yÀàb8aà„«§úÏË vÜX‰\ØmÁÈÕÃ{C!äCȇ“!äÈy2Y2Y2Y3AûØÐ~Æ£ØÔ{ÃXka®5F¨Õ£Tj6$š“RjMi­5´}h:ñGÓƒ£]£¯^¤Ž£GU£Iîh/s•îiûŽ£f¹Í/pÓ÷ƒ;°îgs;ÕÖþ¿ðK DØ]ÉÓýØûÉÖ/“¿¯“·>Nö¾Nú¾KÉÕ/“ª_'B¾Nƒìè~Î|2ù:Uòvß³­_'B¾NÀ‡Ú(®Âv®©š©éÈÕuätƒ¥|3àêŸ\øÙ³H íc¶ŽËôu—ÑÒìK—>a(GÆ›|X¶§ZïA¥ü€· b†8bÉßýÿï§u:FuŒè™Ò³¦gRÎÔÎÖþNÈëŠÐ- ²ƒ1èö)Ú¦‡Úh}†“ÚuÑûN²dø:iðtÓà×û/ƒSì¾ o²ø;:Ôu¨ëVÂàwZ;¿×ÚNñ;´ïÝÑMe7ÛdÓ]àwÑÝÄý†s?vs?r3?r"Tr£å9NRyÈžD²%‘,‰dK"YÈ–D‰$H‘"DÉÓ2dö<É“'VdÉ“¦dëÌ™:fJ™“$*â"!‘éAaå¬:GáÌ‚ò“'‘-ó€9(äÝyÖÌrÍìO7±Ï::U®S’ŽS‘ÌÔDs]zœëßìôûÓ5ùNC”å9NS”å9w=¼§.Âä9+ò‡!ɼ@‡%‡!ÈrÖEi§‘°zŽ"òØ¡î¢.M½ B¿)Ë_—nøœ´òîßþS”å"@»‘!S–˜õØéÔ:‚Y~¾‰åúú:ãè†D+DOìÒýý2:õêC^½Hlo¾cËᧆ} €ØkÉ O˜A¡ÊÜÿâ/ ÿøq#µøìä°!R$Håú#—èŽ_¢$i‚çX‘©‘DA ÈC"o7€#`A`Fäò4`ô‘-…ED·@‘AA AA‚)AAAŠAAA>»²ý²ý¡¡éµëû=g¯ìŠ\ô!dAQøŠ¦×åðFÅ‹mßñ¼ž°ð…àýyç¡m™=izZ‘±o8~@Òt/ Ë€^Y븶Ý뎫VK$3G|GuCGÊ4cïÓ–WÙŽ‰–å¯J`ˆÒ´óÙÙšNâIð²J5¢÷¾c¼ê½znº5ÞÔš4SÌVC¼ä×;Lt‘ öCé#[HnOò“|äÜG>„tåÀÖÀió,Ü"‘ðM‰sr ¯_“Íd’I$’I%ÌÖFƒÜïÇp¡F 8éÛÌ{®“\<Ùª¦ˆåj¶iª<Ñ«§Hàïü_¹ªÎb6,jšôµ sP•U ¢Ô¨Úõ˜¥9 LÒ#‘Êrœ„ÉÌÖ%™¬7¸z®¢gPyG…'ÁÎê|ø‰$’I¤’I¬†žsºüî¤?9ÞÆV„ŠϪ ÊÔQtØh«I '%×S»‘/ÌMò¬ÔœÁgfkbi*Q"¤4yIÊhšF‰£³–œ¨äDò$Jµ¬kšÄœMCXÔ$L™2dÉ$I$’I>#šø à7£Ùy Uø)ÞOЉ$’²ø=Îøwó¼×ùÖ“Õ9ìkGš®ôû‘Èö®úñÑ#¾íÿ˜où N3˜FvjÄó9Ç,YÍC^«9IÈhÈŽG)˱dH‘,ÉfK2dó$LžÄ•I$’D’I$“YóhvŸÙ€Î•s&1øÉ,J%ƘÒû‘|›]¡4õOUìs=«ƒ[Aô !¤§i¡é{U¨cû"`—ä5f°œÌçdg#1,ês‰•°'!9dC"9PÙÕžÎJ¤Ñ$’I$’I$’I$ùà ë¼ÖÆ‘2$¨äE õºqCËûšsFiM-tniÍ9¦¸ÕPÆ–GBhͦŽQÉö5?U_]Dïr;áÞw¹¬ÍRsœç8•œº#12d³£˜æ!™h‚BÄ¢VD¬ˆdC-à’I$’I$’I$ŸÎb±ígØ‘*)bBDÔ@‡BD‰“9Îzc™R è"…‹%dJÈÈrmTI$Ñ$’I$’I;sÿ™‹PEdP‰Ü–I$’I$’I?ó¦¹BüK ‹™`1Ø’Knü&màÕü­Uà#ž¥Žˆ·ü7Èj7p«‚‰¥ ®@ŸŒ??CsãÞ`;ÄCÈy¡"$Hr6dÂb|Î!äC[^„hK#\";¢æ¤o^É rD`K ŽÁD“DB©Èà†K#@Ò¢Ó4M#B±Ë5JÄsØ·HÓ4MH²áG˜—Qf¡ Bš’Ì—¡¦ÐX[çP|@i• Àˆæ=†'Pw¤¿ø<ÉdhF™¦hšF¥]Ô5 æ'ÌZˆæB³¢5³!f‰¢h‹(i”h$dj‚!\hžq£‰ªj"1¨bÞ¹¤hʃg®îí4ѤË¿¹rr,(¤H+RKÄÏâPC!’4DÑ4ÍOciæO2yœÄ3Ù­Ñ4ÍÞƒ{3$r AXˆ"œ Aè â<ÄOŨÖ5ÍsV•£LÑv‘šhÒgªjíH æ'RuØôøp0¢F9½‹=Kf81?d[ŽX6aF…A/?+~Q2D²¥¥´QL™Îs!VЫ,“DÒ4D,ˆ¤l-™Ôi™ÍSTÔ5Ç;<³Hrȵ†ÏƱ¬j´Úij%™ÌI$Š XÓt’i$’I$“»t²È+¦_&¬(Y.$1bF9«Ôìi®kšãP,±%îgË/ôÉV A£ó»Èd‰dO-Š$L™:yªÄŽÈF‘¤C"D"Û‰%MÉYÌÖ5¶¹±Äär¤òÙ×£®jšÆ±,Éyž§®áKp”¶H“&Òà $»!¯(•èA©Š'–Ðúv˜¶ðËø:$î’=Æ,?CöÇ¿Ž¹ÅÀ蛘cY C †C$HžDÉ“$H–Ê@iG"!‹ Û‰$’H"k¨D‰[a)¬jÌ—™/2|\î!¨d÷^£Fi&Õç‰E„Ñ;%”#†Cȇ•‘`$9-â9ÔÑh9ˆNÀ5™‘ ¹r2Óf‘,ÚÀèrã"ÌÆŒ"…™¿¹ †C&©$H‘"D@Ô¬ˆD-üÒQ$¢̉Ñ;t¥±V¡"D²Y/ÍÜ=L¹&/úq_%4‡iŽó@Ó4F‡(žhçDrìc3#=:5ËHG!ȉÒÔ6(‘Y¢ÍE`g…)fs‘ÌÖØo@µR–NA …>8GS)Ïa~C8”'±‹>"É™=ª9NS”ZNBa8TâWðµRI%Q È"@6RD‰$L ÉeËþ' †C$cè´¶F¤jBÌ…™ªH‘ iÈŽHä9 mÔÌYY ÍhštU+XLè#@Ô53B©h–RVDª’Hظ—±'©4’u1b<ÃXJž4Ь*ÀU=V¤z i§I"D[P)ià#†¢L\¸t4Í9 îK%—/øÌ2F¤jF¥³-%!‘ ·$H–K.rÑf™ hU-SXžg9 ÍM†4Í#Ap$ddJÊ“°&™$‘½I$ÌŽdUB9ÈzC’”‰P™¬jó%îY옑,„1fŽ:Úq>T¦K%—.\†ArÒØù¹“!²^1¯ŒLp•Å&–„À„±8Ü·‚$¤¤&AdËò˜dE}K,X”J%‰[A‘"D²]HÈhf‰§µ¯O2Yœäs56|4”hšFÉD¢Q$’'I&‰¤“c˜9H FŽJ³¥*Éd¼ë}ÃT&3‘&(»6¡Fâ+h"N¥Ïþ 4Ö+hBNkZ¦¹«C^¿¤@ä"«q‹ŸlX¥A&4ž‚/¡èEŸ"+âlŠJ`¾Ý&M#SnØ!ˆ¤DšÎ„‹Ar õ§©bÅ‹%ˆmD‰d²åÈ4Gc)’Ìç!™ öÑ4Í3HÐ!d@¶ÄÒI$˜¤ŽØ‰Ñ Îb9\H B£¡ÈH™2u Éd¿ µ¹ÀJ»Äb#NœN þ(´h«†œ¡õшk‰;/F±¬H–K'iR«‰á°”–%!b2¢L ÓbŒIP˜Û‹$Ö%½¨Ú±bÄ¢Q(’NM‘"D²YråÈt4¶`¦Hæ©4&Y‹ØšM$’D„¢H"D^J’$L™2D‰Æ2wHØŠÇ­»Ä‰dˆI0.þ$N€„'".[&®’?q§©bÅ‹‰D’I$’I$²I'bä2 –ÁS$H–À£‘ˆ[©¤“D¢Q ¯H‘"D²Yrþ"7 Yo'ÉÔàH¶Fá‰wBb‰æ7*'7­ÉÉä`NÒ¶ÈK$–K.\Š‚ †H‘2d‰Ù B”!RǦÜÒi(”M¨†Ð$K%’Ë’Éeö£Í'Í8Ž$8k%3R©Çƒ8 7·C ‚S™¼j¦¥ #Gb“F»®j“q'9B{nͼHY¢hæG=RD‡L•¾V¶-„‚¶ÃàB¤íO¶´®ø®Â6Io‚0®3#›þBðÈð0]<4¤°«¸ 5À·Gô±yÜoR ‚ ‚5=NdNz-sV“ H€ä%‘§°Þµç¡,Îg_“rþúJÕ%ó/™2#‚GûÊ9b[pˆ‹#†²Õ-‚†ÓXQ©¬t¹Ä{‡¸ë> Ô‡e¸q‚“ —m0¨Ðgª` In/T%êr¢Ð$áK%ªõ)q,½&©ÊLXØÛ `!«±[GŒ›¢•„äKÉÄá ÌXŠ O‹ —F}9’‹s)`Me$­sN±9¬¶i§7\DZ»TÆ.’DÊà'NÈî1ÐR‡è>EûÁ§ý"bupÍÈÑY'Ðט§ÒNá!Êâ–Aß²°â1…e[#Æ·:¸°Tc&±Cû‚2Á›šÆ£”¤Ëk¼ÈlŽÙ©ìMVŸ"®À²è›TšqÚÂâKˆš!B$!äV%CPÖÚ‚¡G!ÊOfÿT“ÌFb ‘2{­üTÂ!,X±é[³&9‰J ‚ P|”‘ ‚CR‰&1bÅ™êk/îq9 Û4qa5¦Ñ9‡ jÑ5ë6"§|ôœ–Ík‡K‚„*Ä)â‰E/tÞb‚†žÄÔ·ÒäâqDà{Ç ´DË€È7#\Eu˜ÛoA‡ù¤Obaˆ¼CƒXÜɃw ô&tÛJ‰‹V«Š½È± ‡¼¶*,1à’^âI 4­ÊJhÄ_¼C¢Fõ¡@I ]Š8ˆkcÆÄ©%™ÌC:‰Y!J$H©ÉD¶“"j.H†H‘1n?"ˆ!‹‹NÅérô½ie胓Á2%-½ˆ#m2Ùkîñ‹#!‡1yn TG˸™w ôJÝ+G&£+Ú‘±ŽŒPæ¸þ å±(5Š„ZæÆ ßJ^¨1Š6!ÊŽL7 T²T0±Œ‹·ŠãÚx1ìîÄñ-ÁÎ7X‘N"v`j„ã·jbæ¶'· …K°I«`C!¥˜Ç‹e®)΄ FiÈÝã]má°”+¢-™ 46œMsVž‘“ƒTÕ5Ms\×&âNbY‚ Òf¢ƒDÓ4v`ƒœY•ByÈä ó%™ÌGb}3@…‘ !%Kl­ŸSP×5jÒžù…ŠVîãzn/PÛm¶ñxî*ìBŸÜ|fËâ®%Vå8²B@†'ûÛÀx˜M 7…DKDDÜQŒœÐÛÊô ØÉmËÚxl ·wI{ ÚšðcÄ]¥­´7Èÿ#ƒ;>.‚wbcsœa ®G©‹Ïs ç€¯%dÓ ˜$v³jñÄlÛ3PŒØh; éÈä'Rlé,ÉæH–tKØNà=‹il$™‹Ž$peŒKÉQ<Îjè8T @àT”z8Œ’i™5 Aëš ÐuéZ(›…-BhPΘ HYÚp›¸ ‰yGg»c»ðù¿"xãg Ù–äRæ¦Üo\7[ éK2AÅõ6Q¸¬L‰´p¦â%¶âÕâ8ÒöåZœ†äx‡ŽÉÖ7^#'À–b–#½lS³ˆFÔ¶e³'19MŒ¹F5a±^­-CX–dí܆HNMM6dÈ×5@ä9IäO"Y ò!—̆sS 2Y‚؉ÔÕ¦×5éh»IvBŽ¡9©AK3™#!¤m‚¤¼Õg ²\Î;Æ*HC¾ Æ-žíÙ‹s pÅÖxvðÌAÊ€èq0n0†Qˆqß&Ù¡2dè;µIy’ó%æKÌ–_fæ5H ‚)Q!Á„B  E!¼cÁ 6ƒœÈ …\7¤Ì4^Z#˜dí²ÆbT¹ožëÿÚ ýµ]q Lø“À cç=<Ó]ö·}8ǽûÑõôªAóÈ ö°a)cßüÿMþÀq€EWÊ®5þ*$‚ðtÇ^7¢JyÜÚeK?dðÇ07/qÇÜs\wyÒéŸ1Å0즬3Ï%O¬~Ïÿötòçꆸ ÷¸Äu±ÀëO¿ÏzQÔÒQ8ËÏ%÷9­Ã=}Ì<ÃWÐéÅpËîØLâA ÁMçL+ì0Ègª³¯8hYwûµË"9Y÷þô´ÓϽÑ4X¬Ï+*³Çª¶éýÝaÀÄÓ{_úÏ rË óÇïûÏMÚ$1ã¦kSeü}æÛd“˹í´0#ߌ|ñÚWþãçý;Ã~Ö13ãÌõPâíãÈÅ´#ÂûàCÿ¡ž?_IŒ®ß`7è4ðV§¼÷šïϺóÏ>òòÂÃ|»Å0ôRCϾèê0´3ûŒÒa@!Ú›þÏ ½ÿ¯¼‚gî1âœ'ŠŽx ‹·¾Èÿß )øG<È]Â$†gþòÇï¿ü=íÁø×+œ[Æ7±O® #4÷ùEØÆ{oþ‰÷Çðþëì³IÅ<÷wÎ)t@—z}Jë`¶>!Š%®è …À,BÏyôCcÈ0 ioˆüC'>¾ëŒ€";Ì<á¨Ê_¹˜ÛüÛµxýú‚$Ýa‡Ï|ì  eÿ¿â ø-¢ˆ †C‰ë†™Ž9î¸sΤq€††©ä:/]ìBÁ „1O·ì2 þ!—Ô}µÞA—Ð|ÛêÖ*U}ö ò8°I4i4XÃ}Ú1ÇHO00„2Ë㈩¾û¨Žz ëªj‚ !„p¬±6c츃€¬ñkXƒôeZUößqÆTû÷û‘~˜âÏ áˆaÈÓ±ëo1WßüÞA4POóž³Í*{ ¢vÖ2°Ï—û¾ô˜(_”e'ãH[  0‚NÇo0ðã Ò‰,Þzá¿ûëŒ~ö9™Ïw0q×èIëEn¾ÈώÊ,¢:%‚À-6G0Ò¤I>u}Ž7ï‚ EÐÿ°å¤²Âž¹©ðÃcˆ ßö×msæ»î’* Ã÷¬Õû÷·~è_`8¾ëm{¥¼S…"ˆWÈoªHbßóF–½¿x€Ás>qÏö×MFvÅTîÁîë$ÿ, ¦½n!WÛ]¢ïžŠ Ãúæ0Ãó¥ùéÔÊr1íêòœ¬5€7[}ÇI“Î,Äß0ÃÏKìêc˜c4ÄŽcÌ0$ÿÏ?놠úSJÐQ†ß ª°ûÖg¿Ã2Ûï® NîßtóB%1uÿþ‚Œ7/¹ØËD‹ $×ÿÿ´åÄ[[ý¼;=Ô,‡ ¦àD"ˆ †Ø¤¾9¬:7›¼¡:|}0¢óøwíï<“ÈÅ0€<‘ 5<õ<ñAŒ¾Cjã2BªqR·±ÏorÍpãªû¯²LmŠwRUæ‡æp^@Òwížq# óïAŽã,[î®øaóm4pÛÝÎ<óÏ<4òEðóÿÏõÖŽþî4ø³Œ%y÷Ks–’q4Ñ]Öa‡Ül¡{ç Æ6ÑLuÔÊÐI.:ë†Zaÿ;ˆ›ƒǬ4ÓÍ4ã ð6Ç]Îb=gðm~ÊßK3ÔW 1ÎsÌsÃ|6óÚI´õû ùKŒsÜ Nû‚ùo²-ž §*z¼}Ÿ}$ 3;’‡|Ûÿßy÷zâåµ8élUø6ù²@[È T‰§Ü2@Ã2ÃBÈ`0³M {\Ò.“0Ï =„ÑAÃê÷÷µ–ijƒâþñÆ0)ŒÍcÿÎ1¤?ïïõ÷¥v·Ó‚+n·"–{¥¾øª€HðÙnØEÆ€#àÔêc¯Þ—(Uö0© Z39ú•רÞ3̵<{ü¶×ǯ d(saM(ö?ý}(#ºŒ EìyŽçF{íœÆf¾ºá²¸9ר'ƒ=öÅ'Sv#ÌrÓf92hT`&æCH§þ­œý!$1Á€!ô鿚Mô¢D}Ǹˆ·†_Dóó쪠“Ùá!qÀ#¼¸ ßµ¾°’‰©®ù¨žÌëb†KfkŒo¹ü " ~§ø‹¼¡Õ+ðÇ„ò·ÌiQIw›_Ü<ºmùتÐ@ì¹©Žæwÿ×g¼P„GÕŠÉ+Žp –)àŽÒ¢¦Qñk&4âÅ'ÑŽ¯¸xúuáÈ3ÂZINyepKN¼ÓL­gS÷0`×¢åGÀ×¼òÇ<Ïôî9ã æ®8 Žú¤€€ °zg®²Ê JÇÃÍót’»F’:ˆš~ðš× ¥es=¾Z+‡ßqAi¨Ä—‡±uù’ ½çUrÏÏE•q¤0›†Òµ¨AL‹ &ýwˆs,# ?Œ$bC½÷“ß[-¢<üªOÿ®Â (h²Ëòi# êÓŸ1ë„+¯•¢B:ÛÎ+aG¿0a;®„Ù»]”Õ µµâd7ìæáUjþ ?÷zH€îÊcÂi$OŒ3öºpó­4¦H ‚™©¾*$‘tyX5V÷>Ö–ÓŸ)èSóƒÑe—C¼sÂÝ$‰e_M×~ÓÞ“à£0Áˉ3oœÒAì^q{g HˆK}ÊP– r|¬œ²Øn‚øÌ<æ}Ú€_Hšo$¬78š,J<ßÚ”{ùnmöI;Ͻþ¥ J@h„<÷¬ÒÏR§~k2³às¯Ââ»±OT¹¼°þ;ÝîÿitI×ÓN Ë<&¶RSõ£+AQÿ½-Ž*â锈vÄÕ°E¶÷ûn,#XwIL{ÔA§Á™0/Je1='¼ó<³…$Pý÷|ﯼMf%§mµ^vB `O,™³ÎˆÝOð;Û0~Éâ„U•D*ø 1ÿÿ bf†„ð/AüAbt!O‚åsïzªl’ƒ,Îò)‹ÿ·}—ÛÁÜóÇl><øÈ°¾[âê7¨C Ù@°ÿrÂys‘ÆL2ÇÿÛ k"“}rAÀÍw + LLþBjÃE å„]´´á3Ï (ÓÏ<óKBKòã×&š˜Râ ÓžºÄ(]¿±Í!h<ó-#´µ.FǪJæÛŸhé®u+íÛ§O¶ƒSˆ‹sÒÖ I‚• OtM5ß}öÒ÷^×"y澈/²A›[©_7qÀ'0Mé ‰ G-Ðÿ†ˆ³}§¾ôÖhÙíQú®‰ŽÆ ä‘Ofœ—©1Ïöƒ À@p {ì¦(Ã:è¿ÐË=ï.´×>xórYgðÁRjŽéÉejë€Lòó‰ ˜a^p„ŒGÞXl)Eå¤öå8ñã·eƒr&‹§…#®öbÞw…¯o0Ìóöûë®ûï¾8ó;ÿï=4V#H[ÀE®þÝv[AD¼PJŸ…jF4Õeyä”&¾fžXS _îó"ÂÊÔ@žpŲh9,Ï òç?°Ãˆ¯Œ0€(Ÿã wÃ,f²0°‹ r†°kçß,•>´:Žäýóº[â¤ãoTú·ÏL³÷é'€çª¾š@ÃË &üÙß’þ°m›çƒùrßüᤑܳÓöû/«ßwÜô‘q”_u¤^asÄz=~… ÏPþãY73pK$šhðï/µó ¸¤cRÃØNà0C#F ‰8ÜJ.}"ˆ}ýVtŸi·[(QÉ’ˆ¬»0ãî7óŸ <èÎho‚k”@ˆÁ¬´ÿè/€ nljeºûû@b}qÇÏ<0Ã+™ÓÄ|3R ¡8ÁUý~Ò /‚倜ßhƒYGÄ’ (}ÞEtA7ûÁ4“C ?üˆ#¢‹Î¡¦¹ãaÕÔMwÞID< £Ž?ÿûÝè£ÝG,:†CÛÏ-÷ºß-,²Èð¶³Ùåå*¢DØeH#Ý·QçÒAÁI 0ãªif"(n’`gVqAEQQÕœ}Ïø¾jûÃQËÖ°3ãC_ïymþ×?´îÉqßéFeÔF[媄¹WŸiwRARC}Å0Ó„0Øëº™(¤¤ÛiEÐYWSß<%ÚIç£øëß-/'p]OSsÅÄX¦]´Ã²å¥_EÇÿh➆1Äa§qÔAÆðAA 3ñ”¡£jgÄ[Mÿÿ7ÎÚé–Šã,ÝÏp† ðM0pi†ÖCZ“¦ÚßJ À=4{î/|ËDÔq'·yRê¶™z–IÓyçM3ï<·‚M¼üç™ÐM8ãÍ,0E$0A†óÓÍÿþû¯²Ï=²‰?ûÏýû îÿÌl·-ÿ¶Ë__®n<ó´²ðN<ÓÔu÷ß(ðú¢îk×80È0Û½ÐÛ½¼ÿ̸{ßýqýöõ§0Á 0( R 0ÂH¨² 0‚0ƒ.‚É0ÂÍ,–Û9Òßï>ûï¾ßî´Ëk4óÌ0—Ö@Ë^4'"iŸðƒ 4‚½ÿß¿{›Ø4a%0ª –ÌüÏFi‡®8äà ‚ ‚0ư‚ 0Öˆ"†8þéûýcN®O²jÁ)H;TaŒ†'-E¾(à $ÜlêX`’Û?óÿÛÿÿ³É4ÃÞI0‚8tÓÌ4¦ÿþóßû׆“D( g³´3'ß­:öêÂÀ'‹ [Q‡šöÁå"g¿v†=±bFÿ¹Ôˆ“'pü ‚ Ãz²Â#®³Ë,Me÷–¹àqÄ It¬š³ÍÛÇ0ø0ÚÀ032ÇTòƒfsžÂH k‰A+JÃËI,ºéþåuwÝM3Ï|òë¦ú¡û«Žøç¤8çœ5 =÷•yßû¾ü Ì,8â rβªcŠ¡’ˆ""!*`’J`º´÷ß÷Ö]ï?Ǘ˲¸,² $²æ’Ë,¾¹à]´c²9ï¾êk s†0ã$AsóëŽYÆ";è†(`× g²Ià—üï‹ÿÎ{“Œ¾Ý}…Zó Ðhp=/,>Çuƒ«Ç ñ92¨°Èï(¤ëšë@Îê,L¤Ð€1¦ÐuİS'¾âI8r¢²š¬¶<ÿÃ?½ý·•sFÙ}mêêæ 9¨ÂË£)%° QÍw£GÁ^™^¨¸Œ-ln˜4 Œ0„1·Mw½Ëß/Û$ªÉç (CÑÆRL·ÀZJ$·O\tJ®¢Ít²‘>ÀÀ H/‡¿åMÞm…^¶‚ºËì0„-h™Q -)„i–ª¬ðJÃÌâ—:HÃ9V§˜ç¦$ºzƒ œžÅ2¡À‰ÆmEËßdvÃh‚ÐýGxpà4G>ç“1,<Á±28*žÊï²3ÊyÄ£C„ -Ïoßp4bS ¡*è¬;Ê&D#J甲ŽU’Šé$"A7ðÓËM„ ,`S óÊ8¨?0ϦIª#œ[Ì4“|[ĵáÀƒC\rzQgì´sY(Ûî¢ ²»`#HîcJªÆ€z¸†Éï:A„d@”tÀŒê,ºÎ¸ÉÜù}!³øl/¼à¾UÕ:PW¥C ÒJ°>¸¥ï÷AIë9]zélŽ„e²TB¸ã, €¡M¶y’ØI6‘ŠÈ€=ã <ÓÂä&sYôK´ãÒwŽ@² –ÔÕÞô}”Üÿpq§Ïìż,–/ÁؼÐ~[‚é,¾z%´VQ]ªqçR¾á•<{™²ÀÏ6ošh<÷Èô"ÿè^Ïüç`‹ð<÷×bßcú@€Aø8 çõÐ^óà€}øÃŒ(ãÿß ùϾrõÈ{ÐÜùÈ}÷ ‚(<ñωß=ÿÿÄ& 0@!1QaAP`q‘€ÁÿÚ?ã_«ˆˆ„Õ6ïénÔ'ïniy9Ëÿ Ò”¿£¥ûËøÙȺîº]T¥ú]õ³9t¸»·j—¿¥ÕJ]»Š^/à)xv—fð.‹µ~–—]ÑJR—¥)JR”¥Í/å)tR—¥)JR”¥)qKõ3è)tRë¥)xt¸¥.º]Ëötº®.›Æ¥Í/ÔÂm]˪â—U..ýÓs~âèºnÍ..o&êZ®Íâ.u)t]ê]¹¥)v..(žnå.»Ã›MßãZUY~ ð'ðG‚I ’I ‚1AQQJŠ‹Í¼ô\]+ŠÚI±¶ÞÕåR—fbYvYe•ŠŠ(¢°N+»„ü„ú%ºÒc\ ¾¥úB,|jgÎn„Ï‘X2ôZVÕÔÐÖ)wû öe.òXš–ûî&Ý×ÙÂîs!M‰µu,.‚¸4îe§ÄcLŒJ´ì±¦•ÃMB6ò“ÂMâf1¨æ Ú]0„!8K‚»±‡®éZíÕÜÌH#¸!¡.ôo“»„2ZcI&¬mW%)=hŸ‰ÜùÖ>ÏéWn Ñsà!H„¬NÉô:¥êtŒùÒ]èŠ>“t¦…ø>4Û?èÍÕÂùmî…žítºî‡ÙòÓiÕŠÊÊÖ+…b++(êÇØɾ°¶î{ßY.4~>½i[ï³;õi.Uã ãàŠüL9RF»½’"› $4é Ø‘«ìŸ–%–“ÕHMæ ¹Ä!j»ÑŸ uE£³…pY¬¯ ô…ex®ä£mâ†ÛbƒlL°T.B¤Äæ¡oöðË~ââÅûU¡nw'±€»‹\N‡B¢¬ÔTTTTT\­oç<’N)I' ‚B„&‡¿{¶.ïbÖ„å¾ÙL¸Rê*(ž)sKŠ\\R—?üï&Žþ/bÃ;·ÒYM–÷kÑJR”¥)JRàêåBe)JR”¥)JQ<®^+Á¢i™„!B u„#!M21zðôR—g­¿ª9踥)sJR•èw7¾·Žg`k©5># ˆ„!ˆˆˆˆˆDB"„3Úœ>ÜöèNP˰»=]”Yez+Ñ^Š(¢Š(¢Š(¢Š(¯Ez+%PÖ²ÞÇL©˜Ž„DDAA@’Ç“±ººR踽ð­Ô|€—^2c_qw; Çôòh›ñø!GàÁ†FFFFGà¯x+Á^ ðW‚ŠðQEx(¢ˆÊ#(¢ˆÈÈÈÈÈÈÈÈB=Q±D†à§^=(¡ÐˆètÍEEGB¢¢¢¢¢¢#@¥.¥)p‚6À $’I ‚ ÒAN _ªR”¥)JVR—EÅä‹yEÍýD¿Av––/ÚO¶OÕì´N$ü¢ôÓ?K‹¦ðnÂþ´—E¦} ú;ôiÕo^Mü<´!å)xI¥/KÀM)7êä@±¿J…¡j %…ëú%ˆ%Õ h¤!/ Ùþ"òáwM©ò"þGàÃ=¨õž“Òzôª{q~KògöA/Yè=©¢ˆ¶„­ôÑ‘ø= õ£Öztð{ òGôDyÒé=' õ£Ðz¼<…E)J\(¢²²²——7m"ç¸B22¼3ÔzQè='£Qa^JòGôGôG“Ù¨ ôž“ÐzAˆ¼q¯×¥Z먿Ivþ1E ’é„ü#ðFFVBŠ,²ŠË$“ŠÈÿT#3Ôzˆ¼ • Š(¡Û¿_üø##(¢‹×ÜA"Â`«AA:~Ë,¢Š+++.faA$ kJÎVfb„!1 ˆµRFƒe–YEQYYuzp¥Ißu]wy KÐÇv"I,M 6V ,±ÊÄ"×ViKŠR”¥++:J^{I¨ÎÕÒí½þá‚22Š+A‚fªjD! ¤Dddddde‚øó‘”QEjtADDY¨¨¨¥)JR”¥)Y^Ãͨ„¢Ð„’#Áü”V5…)QQ‚2>#Ì#+@‚DB""#¦.iJR謯Œ÷îšQsK³ ˆB„D"Ñ89Ëqbê}ìúkõ;&x7S²bV—)‹5íêÍE)þa¢½”Qe–QE„dd!M¹ºÇ½ µ6m©±bI- Á[1h‹v”º ’5Á¦À‡1Ìr"=‹fm¿„N¯ixW,«ÂzXðöÚ˜˜³M‰¥»-ÈM=ì^r±6>èö,!¸Ê{=lOü*ÿdX4Aåu7ð›¯6 ©» ˜±‘B&™²Ðô1ë„ÌÉÅÛf kn¯y- —b—ÈhcÑM•vð[­ø¬¥íNrÇ*¨¤6$þT=‰‰Z%ë kЄ©Ý,4Rá´“lM4š*³)àM4šÏó«jî1CÛ™¼ðßG»5w,Õö¸L‚kk¥¶Bu9B%Ùº¶„mÒ-Ô!²ŒFÒ&(*r”uteºVA§TŸìÿ\ëò'U;‚$Ò±Jêj܃CcQ­ÆÝ:%Яs¢»;õ+mqУ:øt_±wüÐó4b)U’S°ÎƒÔ*²î¼1L&Çjp'‚ú“D×ÝyipŠH†“Q¥“î–,":H)H š…”‚IvGòôDw˜ðÆ=Îñ<éµ7j²¯2øï xz&´«Gñ)„ú,ųœN¤Æ°ê>‚h½à¯Û«©\muC3d”ƒÝOøi#¢I*&ä]©nÅØbˆWBCô«ó»|7®k_óxcä–™¸»,´nÂ'K zŒ£­{1Ô±S¯BA#6–:Rì, QÂ%Clè7»ÕÀ»¶Rí_ øîvx=Ïrh‚Y]´GèÑ:ú##'áß#ÿ,VÃØl~ ÊÂC)K¤)K…)J\R—MÅ)x)t8ÑïWì¼[Ë(²Ë,¬”P˜¬.FüÅx Õ¾–¥Ýê)K¢ìÜÜRé¹î_rÑ4… !44B„!1ˆB ý‹ø&Ÿçì.iJR”¥)J6ã‚f×ûÄ‘JR”¥)QJRþl„Ä!B 0˜øpéacùY Ä­¼„µ<Ü)JR”¥Øì‡KÛ)J7ùr‰»õì2§ê“0„!LB !²ðvø=úg€û¬.ç{B¼/5”VVVVR”¥eeee+)YJRþ\z.AAKŠR—]Zj)QÜÄv3¹.&™µÜ³TŒogÔúŸsê}O¾rûŸSì}Ϲö>çØúSè}¹ö>‡Ðûcì}ϱöÕŽ‡Q’G¦|3០ÿ±8dúGÉ$dxêVVR•JÊÊÊÊÊÊxìx¼ßq-”蟉К:bi¥)uÝ·àè/Ü[³e*• ä½ö'ÿ£Ç2Ôr×K¢¢¢=•{#Ú9ÑÑÈŽDr#ä9Žds#™ë™cs#Œs£˜æ9ŽC˜ç9Îc˜æ9ŽmªªªI ŒR”êÄ›/c³~hšZ½ÐýDz Dz8#ˆDz8ÄŽq#ˆà8zDzDzDzDzD^‘8"ôEè‹Ñ¢/D!B„!B`÷~õø—„Ý—— âÜ]W¥)KŠR”¸¥ÕJR—¥)|G¯EÂý›¹|5¿Üÿ¢Ý¹°—Šõ¯èÓÏnûœ¿³·e¦ïÂñ&Ã×?»Knþd!<™æ=-¤ßï¿É}އ؟Ð'ã·ÓûhÚN$þÎú[©_öËÖ(õ?³5z[UÖsÓý™èø@Ó«(ßÙDôû‡sö6ÐÚf^Ì.Õ/íÕìäGå9NSœåÐÿÄO£äùÆø+5ÈrÅ{+Ù_²ì7GÒ!Ôh¢Ý¥ò*öŽDq6rç9±ýO¹ÄÎ"D‹èâGÎóœç)Ês³œ…ö+öuöuÓBAB„"ð¨ùx4A2¯d{G1Ìrásœ§)Êsçþç8ôFAB½G>yÎSŸ”sŽqÈÊý²¿lB~[ÇzCuÿË!x«Ÿ:þµ/…q$æ§ôEE^ÎB=œÇ6(ô|Ÿ'ÁEj3/3þäÄdf.rý±Í¶ÆIÁ)¯çÒ•A8¤’ƒàø(­EM÷#ûÖ~W²½ŸgÑiâ#шˆ½f¢”C"½œÇòMék–o…qQQQQQJR”¹)JR²²¼u##(¢ŠÓÐI:*à8ôDDB=3ì©S/ò©?‚BBZ)õFéÅ#œçÃͤE++ÏR2222„!B"„""""ü“Q{QJ|Þ«ÙW²¯g!É’œgÑó”R²²²³®(gªžˆAŸI²3è‚ )Á`Ypðù¶lãx&ð†ZKm²½²Û…¦q·¾L8]-²Ø‡‘8ôÈÙu¼uÎñÖqëƒ8,×â†XXXH¿tv⟡zçãšr¿3#Ô_¶¿ýéúÙœ$x0p…mwŽáý°²ßß¡–»c€– Xáƒî× Á!Ç®±Þ:³­°Å·¾AÀ„êõ¶w;½Fj¼ÉÁã[d×rì–^áX6‰ºÎôÊ î9cøœõÃì' ǽ€¶sà—à3ø8 åx_–[ÁæÜäà ùé­¶ü[]Ž3¯‚õul¤°²‰Ëm¼jÁÖp®ñ¶ÝDÁ ­Ü/9Âð¶°ðm¼õiiuÆ Ø]žã/Ã8v-zÂTž%û¬ýËû&ߢ@Øé៹?äŒÜZû ~¡å¸P¡Û"vâ@Z`nÌ& $ä&ËlôͶÀÃ`I6-ï ŒÙQ¼Âö]°Û+†(ÏoðçGõ•ãmàÜÆxÇžì•ãmש—¼_†p)=ÚoÁ‡‡„ƒÆñ¶õÎÏ Ö|:Ç86ÖÞ;µ–_ŽÃ¸H¬x> «z"ÖÖÞvzŒã¬ãÞÝÛ/þNN:^]—‚îx3KxÓ¶ö±6xÒÙ° ,ðÂYðJú^¬OÔŸHšS…Öu/DÁûsÔ«ˆCïƒv ðˆ=Hý2¹kj;E¯#®ÚŽV†1‚!Ûdì{‘äe ÍÓ¦Ù lœ ü;߆¸œ»ÃÂõÁÆplœ{Á>g^žq·…›»X··ÖÛm%-ë—xw?àWà–Ûð÷ë¯de)–ÛÆËzã[¾6ïm•ŽBðå±Áœ>a´ç>'Ǽ‹x;,‹~â7…‹nøÒN7á¬fZüFØl'"ÀŒ³Ø#Ü'Ú%Î]=™o1ô)—Ð¥õ)úUú3÷Ä:ÔŸE{]Y=ÇE&xåœ'+&W “cw+k "nnô²¡ìSgxË«dxA†ãmŽÝÛ6ÃÆÂöÚï%œl¼o Zòf]ÙÇY–ÝG:pZo ð·rÚËÇ´™^L—gáî~G;396HcÅÖYu)¶ñ¼)‡$óß"ñ¼²< Å‘à\‘ç88Ïø:áãmyξX[…Ý…Ÿ ‚/l/=m‘g ÞZó²¬o#­³¶­’­ã öZ‰dë)ë?^SÉfNžÉÛ¦?~CåM=“¯©ú’TÝÜûÍZï °°ðNpvpl2q¹Ë¹wœeÒIÃ7–%—¯ƒÁÆÍÞÏ[ÖÃ{›½àc^VÜáÙ7àfß‚c\ãxsù“–ÝC7\‘Á/ ¿Ÿ‚,䳈עGc¬Þ3ND>Ù8·|yí9Þîþ9Ÿ ‡àì˜prnجYÖ¹w±ºÇ'™òe¬È8ë-»Î6Ó>ø_/!rW!îÞËÕ¬E·%If"RrÎÚCñ–3ã[\ŒÉÉߎüa•Þ‰xV%-m»–×8YˆÎfrmá]å縶v×…•¶Ü™·æüR<Î|5äáÍçNËa,±‡ŒyG‘`‚…³‚Ëx7ã½Ì±*ºö¬p6„/Ã䷷ޏȼ<,]Y2g ÁÀpucÆÀÛÈ6Zçm™-·põlo…¼²n¬ËvÈäNÈR¹Î¹{ÒSƒ{*Ë*]Þ/Mï8ׇ ´á8Ü-x×n¶Û~ ËÃu0ÛÓg:ç ÃËÃkË2ËËÎ’ÁÔü0ÍÞørÓá·p?ðü à.EÕ‘ÀYÇ\nÅÑo:±ÎpNGGÿN^ì‚‹½°]´ÛÈ®Yì±ã-É,Æï„·µáN'nøVÙ´ŽÛ£Çsuh–ñß΋".í·d¿g p.íªÝÚddùž7ë‘øµKx-%³/RËÂë,%pÂÞw–ÈYxs··à‘ÓkºÇðm—{{·¶Þ²ÛtŽ;»^vÏŽq¶òpCi¼xå"^ »ø=opüäÎrË" -º€ãdƒbÞöEuç.㇆,Ç”™½ëÆ;:ÎúIÕµ¶Þ °z%îö–ÆÞxÂrê×Dø?8l3‘LIK«ÂðoÃuž…S83s†3èàåø-ïŽù×ùm¶“Éx߃gK§pˆ?án³Þñ¼ïðü}ͼ;°@ÉÉ! ²×\ï/),p]<L縳ãîÉ96vÞ çKX+ãß:ðì–9g%Ó%ÖqÞpp0L˜AdgpÁ'q8GŶd°ÉÙbδ8flÆÃá¼# 2»+Ÿºµãx'Ž¥8XÎ:‰xêßYÈÇ,…¼u6ÛÜãKf;`ÛSIe>J[u ßBm¤ŽÙÏ‚·YÎò¼&1Ær'*ÃŒ‹ßpü3Ž‹o|g!ÖÝq豎G8õŸ,mx#gޏ 2ÃxÈDÚE‘œo¦m–θ-.¸Î2!ø9%¥–÷gwÇ»a78ˤ’3€îsyLˆO|i–„½x—Mm“—w'…%™erW6Y».žC-à³`ºË#'‚tme7ã Y;j„ðq¬«Ã’ ¬¢KÁ›ÂL¦ó¶üý¼ž»Ûk+Ðð‘o9a_àÎ ½Ÿ Èç#9^87#!>%·vrÈb8Î0»Í~ØØ?-¶} là‰8 ààßgà #?áDH,l9îöñ“=Ýðå«#å‘…—…»Å»ÉYq•»õ²²àvJì²¼m¶‰{†Éܺ”Î:ÜxcK\‹¼¶߃Ã{ºûxIÌç®d³Ÿl@YðؽóÐG ‘’]s­¡þÁ2Þ¾CÕÝÔyøi­„YÆñ–9Á–GŽsἋwÈe¦eœgü{¶S‚66 pr Õ–6wÁæïa"÷„¼8ÇË ³^3­²írFz²È1ã:°³xxsôÎÚÝ’ŒÛ2†Úí±ø$m‡½”ªð¥èZ,åáÂÛ%œ.fBÈÛÇVðq¤l¨ºç~â-°ð˰³ž¤9ÅáË8Fmîï'†Ï†<3œ 2«¿-8Ïøú·àh]8ø­‘ÂMë‚È$ù“â3„Œ³ƒdŽqŸ”>=pnÛòppLªf¼YÁw¼!a¼p ‰u±c>lnþ¦Ë9^³…ô<‹eïm³Œm›Þ³lË+).÷‡6îÔpôËù­âÇ•³­':±ÞNsŽæîUç¿–]o®:på¨õ<*sëŽòv~8ÅÎNYyOùs¥ø‡g-–Leœä xͲ÷o¥ÜñÀx뇔ɃÎpXgËdÞ¹€!x#áŽ=ÙÀD„Ùe먶8êîÛ#c,î^0·€ˆNzÆ8îx÷–3ÇK2Áœ/ ¼2³× ìŽpnÃo œlZðñ¸Ä&Ê,/åÔ OØX[7quhœžd²Ó&Z@Ùuä9Þ­œø¼b¼?ž’Î{†<‘ªsDüuÙ'8H†ö`˜ƒlà‡ƒ#là~gÁÁmkÊs¼ÆiÁÀ)œrN]pŽG îOÇ«¸$#Œ8ÝX máá.óŒ‘Á–¬3¦CÎÎJŒr~‚[Ûu’Øv»fÛc;Á’™/&É„ u3¯1‘›2Ã?½<„æYmÔnʼl|ÍÙoõðc™8då,N=ÌÙñLJ~Dp¢,îozÙgÏÓ£¤¾Øx,ˆ8dK­ãK?ä3€{ƒƒŒã,„pàŒžy`á’BlŒälà9É#‚ÂG€»;Çsr‹„àÙ[,°à»÷3æõÆLððª²uÃ.2ža[÷Y²Šg :Y-³œu¼oÅŸšð­œ/;iÛÛbòÊØŸ­Ž{àÎ4NwÒme >záèøúàø(Ѻá8Èç¨!GG‘çxyî ßdQƒ|g¶?E¤»®ìÇ€9wŒá³xHòdEœ$`póÞCc-–;gv¹‘œgÙdïÿÃÂÚl¬lÎ)+,°Êe¼ážgã:ŽçqˤŒ”ÎsåîÙnå¶è.³½AdðKÝ–['ÁááŒbÐ!ÂYtÝñÕ“Æ,—¬ß‚Y1c ÇŒ‚àÂÞøw9 ‹¾K$3ŒàäLàè:»à4ŽE˜ àxë9H:°á¤)ÇYe¼¼bŠ|[R÷eŒAz8H,³äñ–w/o9ñÓ.øÎS‘›¡á‰ÆÎHLÝn³Æ¡…¯A )ÞC*¶œ“°»fφãݱ˼g;1os¸Å½æÎCl^í0ÖvÞG‡’ëŽòÎ\Î2¡ê;yÔd“àZå83€w€³“'“Ï9ƒ“7ày³‘Ìd7àg¼ðÇÌð &2G.DÙœh3ÓÎ1dA™Ÿ ƒYÏ gdbòm‚Øaœ1ƒ0pyà²ÎÜ8¢HCaFÆg˲ŠKœ/sæm‚ïRsbF|¼hAØpá|–ÏbN mçyÉM‰8Õ8Hga-àá2ï‘~äq‰ÆËe†ÉÆ@>R/QË ¿R»y9UŽ=ü[%á/RYÆAbÇØfdY¯ÀYÖX„ÀÉ–6s–:m“'× rp²^û²×f7Žìw‡‡—V|ðÈZÇÎÃŽl6fnãÉ’ÉÂãÚêL¥½ñ¿ µc•Œ±­œ!É›¨ÝpÝouw³ð,׎ò|ŒÙdhç:µË,°d“u<{²Î ’ «,ƒ¹0‚Î,³€ºÈ;ã,‹9 ŽR ö|z$äíÿƒy×à[l¼“×op–gÄðAÆYÆb.ñšFÞžÁ=“`ºË,áã'Œ[#‡Œ^{±Û®1—©êx$ðøøwÈÈp“</gÃã8É;჌ç?àφpFÌI«:»‰ ƒŒØŒ²K8 7\;.™q‰»à-c…sä9.ñ»Ãçá›ÂAcÈGlAÖÁYŒq†YYñëYÎ2yN3> ZÛÊ»2KƳ»„Îdp¼yx×áè»ç³¹ÎÉ1°àã;øjq‡ž70øoü\Ë©x|po$ï cmŸfÌ>a“<äÉŒ– ñ“k¶YÀq–IÕ–Ydç8È8Sœ‚÷að 9Ã9xà ²Î3‚Bé6|ƒ€’ÆH8Ë,àˆŽØ ²ËÜ}¶z°³K,ã-2ÅÆrÎ0Ë;²Å\%áž0%™ÙK'‡$xW‡Œ,FÇc5Ù ,vË[ 5ƒ€ìÅìáн[os¹ÆÛÚ7‡ž÷s²XÌàà>ÎÝððäjÝÇ9ÎuÆOÇ"È ße–2'VAÆIœ'v¼¦rl#;Yy²N¬ã8²8 ã,’ ’€øc–sÏ|gÂke—vp ÀEpYg  ’`xešÙ –,Y¼d˜Bš@ìÝËòNÌËa{呞‰°`ã'ŒY8Û ³¹xrõËòx^I[N7yïÂÛyX"ψŽeœ‡/ ÙÀ[we¶IíÈÞ^3Œƒwlã'`°ç"]ºá$å 5e“€ÄÆúRH ±l1à&Ë à ,‰ ê>gž÷Œ “#€˜‚È ²°Ë,‘`18Ë‚`²8" ÆË ƒ:l°°öYe–3°XÝ<õ3M’<3<96à džÉݱ»žú[í×/g!gZEÞäx7‚Ó·†ìÂmäŒnžGƒ‡…”àË&ïç½s‘zc€FïxlxÈɽXä‰)›Ï\ë“9ÂNCŒ³`³Œá öO›$€ã.äVÈà, œ³«ƒ$á,È lƒdØ$YÑÀpI²Çr 8 îH:à'c,à‰ †HÆcÀk{ŒÞÙ ²ílà3¦Ã€#áœäom½p‡ ÂîåÕïxÎ¥²Dãè´áÎ1Î; KÖY&Yk‘çeVw$™#X Kxï‡%à'§…ãx‘›©cŽÂ‚,³¾Nq ànøÈgà–|ò`’Ë܆d ³0²È,²F ’Ü`²êïÉ-nF›,Û ²8ųK,áœaYdLm–a¥ŒXffq^øAl€Ù‚ðÛ;‚È,Ö îël‹»0²Ë8ɰÛ,³Œ²ŒåñšØNO X¼<6%ŽIÜÚx,Qf7uá³’o^“Ú½©tÈ’]î!9’‚g£Œ¶Te»ãbzáWYg+ÕÖp=É-¿$2ïcÌ=Íß,q£Ïvé-­ÒpîsÜœdïÄ$ë9ÎCŒà8 ¥ÐO AÝŒðäoràå‘°@Ùf6Ywºm–É,²Ë,îË:à$°$଱³ŒàÑdaa6AÕ䣀² ;`ƒ€ï€@»ØáŸ ±RN l‚Cl”²Ë#s,ƒ„Î:œá^=ØZXIg 03°YdntXÏ`pÛ>Wœƒ„“¼”œÞ½°EÞAã½ãËÕí·Ná¼døÿ‡, 8îȲ0 ž}ÙÔøŒËîîwޏw‚òä.¸ÂÉ Fy#ÙÀXeÙe–q’Y ܺŽ*ÁÀÔÇ£¢`˜8€‚ "!‹í‘¥œ²NrÈ,·,á…XAg¨„g²0àV9² , !AÔ–e–XýYcÆRI:l2H@ðp'¹ÒÙ$—Þ²BÞŽ^Vê/ çL²'ÅÖòD[l{Ò3ƒ9 RUuá†pYd‡ –|2ÈŽ3 ²ÂÆË-d€ô¹c›eœad 6Ë,²À² êB ‚ ̲à “`,êyÀ`l–9#$Øå’Iß©ae;e–dk’@’ ,‘,’fw$L±‚"0÷–@Ë$ˆ0“»,»²Îà´ÆíîNláƒEàL’ζyÉ5v'[$•°– ²s2 á˜F¤^ç!œðHÉÞHlé2HÉ2çlˆo‰ÍàHfùkǤc—’XºÎs%0È8~zñ²g¶spq—²Uj²  ™ÆY–pdLx$ð ,{’Áä!ÙdɶAá°ȉæò‘Á2Îàc¬aÀa ¶œ2Nd–ÆwðLJ'eÐ)e’O Æ2ÇŒ[l4œ˜&l7†Idå„A$“b*ë½Hðaøå¿83ãœ)"w/]ª}tF­Wßû3ô¼aú£ôÇèý¦õcÜgôŸ¹û,Nÿ¡2uì¿ý„—oîO³ö¯»ô·þö)±»ãL»øŸñg·ƒ6:Ø9ËKÓÖH @@/hÄ œ0jÌ8 "6AÒåÂÈ,`¼˜:$Æ €“§‹0ôÙyB˜Ï5d‚H,`á†YdÉ -’ ã,Œ]I…’Y$;d#–N쌒IÕ€wvK9H:Ÿ2u$“k$—Y¸ð,!¤$YceŒ„e“$ÈĹóÈ[&äØç-Ýï†rÛ«8ãyÀ²êÀrxÍ‚vÁ÷Ã~Ûm”¶[_¶ÖVMœŸk"Wwô¯üû¿Jû?ZøIO0#l~ÿí*vÿi ú$ýÛãú'îý#„>âOâý-áú%õý?Õ?Wï¢áý¦hî¿éŸ¢ÕûÇÑý³÷þ©ôB^ìû¥û˜ý¿µ>±úl}ÿTáôß¹ëêvø/Æýήž ïWä_zŒ£»¸i*O»ÓR P!+¾™ vÈ¢G °È;³ ÏDFžþì`˜Dƒd7×°221ÒS0NAHÄg…‘À LôB’Ų!p°Û,l‚Ë1’ ²FBG„’Î3ŒÁË ŽH°29'\ k#g“ç¶Fɲ˼6lýa$$Ô gIŠL‘ý„3`»‘øÞ㵟žœÏÕ°“ø±1úXÙdãyÜç.ö[ma¶xߊ°ó¶õkk.[,&K„.w ݲõ ïxÙHe4–×VUrë£ ö ;hðίIÏ}Ç¢ô4³Ñ?JTèÎŽ í³Ù“µ9ƒ‘'êžÉ§7vð:ÿ™ÃQ}ÑÇ1yz8½À}J[:êË=¬z›£¸áä.Íûhh¿zGÚ7æ/Í¥àîÑ»zoÍ4ðh¿ª_A™,Ï¢Á€ÂÃìv~E#ÀÎ¥ždBYÃ:•Á‰&³$ŠA“âê8p°€å ‚`É6K1› ^Œ°È Û/ ˆ†NÝ+?WkHcƒn²í·:Îvûó‘uò²È'ºWŒãtÿmЀÁ8ÂÆÉ,à"ÎŽ2Ë'ÉÇO)g²ÙÆ9ynq“{duã;²'M,VO)ó1¶O†É ÷dÉÕÞ)zœUêM8NÙ:IöÍéÏ_ád¤†ɪCWó!/YžàÓ~ÏðÉ©%‚™$™²IÜi1BD$’Ô’X¶2Lô[Iº‚ô£Ñ_L`ù_ö¯Ì‹žÞ8öˆúÑç?ÞÇoÏCÄáã>—”ßç‚}Óï¸/Ä_„¾ÈÍÖròÉ ;’ ,`‘ŽÒpÈ]›.’]Úl¶I’°Û É8l“¥ž KÀë ã,Þ`äÙTÎ;‚ °ûà ø·‡àå¬lú®s–? ºŽBNàÂÉ9Ë ’Ë$‚G êË8É;“”²HhÙ$ÁjôL†÷öÈÁ ,–ðM&YÛ&-’M÷!Œ“àOlšAÙÂtußÿ`,í/zǹƒëú8!†NÉ«9¼dÉ< ¬œ!½Id’Y$‰$–3¡’p!k ³¸,’KK(™$²ëŒ’d8f|Hd)!cîu‘^¹;Ø$’Ae‚dzRK=–IÖe™%‘ÆH—ò¶I0°’ÇdÛ‡DìÐ’LÖüè¹ç;ư|X…ƒŒI0µç-²lø9~“‰à³àœú±“†Î2 ã&É;³„á ‚sxËÖÙ%–I¡DÎHOø8t6S¥ŒHzÃûƳ¨õêswøÞY öÃqØH,Èô>$èãõeã×Ô˜ÏnðÛµ•éIÿVbàWzèž‘!`5Qº1üÞÖŠC$·öÚ†cÒ1žY° Q¡q ¨(CgœÍÀŽ"“è°ÎNu“¬²KáÉfwm¼ºË6 ð6Yc$IØ„Au!á:†”±M9Gö7ö2¢‰5‘L;ÄGd$CcrÈ^/­°>ä–ÍÑŽ’AH¿1t8up# /‡«.¡õ,§°ˆŽðÁë?an`>I©,;x+<`h‡|fê†-g<¹¶Ø¢ »Å#4 õ1üFQ-ï»|Ћ h-†¸YUç"Æctr&Ú™k4#:ôì»Äö0’ß-ÝèÏ©y»¿Å—>aßõÙV:Nçrt—ñnÔK}^Ë$Ë$`;^ÂÈÞË,²Ë><#lÉ,®JK’ÊrÌÈCrÚL#¿ œw¿ :d,dld7,²FH\ÆëlÒ`€Ü±àl°á×¢L;·Ð^áãN3]AðÎ1öÆYue¬cãbë ™âýþS?©'>öΙ,’Ï—¹8`²K$à$‚Ë &ÌH;’dîK;Hä$M¾Øÿ?µ™iOQD²õdÇû’?Óýâ’¹ýàKR £sûH€)ÚôLYH0òNÙÙöä_±/íx?º±Ú’Uºu¤:þž}Hgú™2<¨›ð0ÜŠ?_ë”0Ÿp(žÿʤSºA‰ê¶îIùцoØ1 y?d?H§ñŸ^ì3ê ãø¤?S?2€?F}Ľ†~nð-_¯ûÂ$îþ°ŽùOÙ¶ ¬ïxþ¿÷úØû›·£Ãø¬W¨½™ø`뾟ÆlSÙþ›É$Íœ£dO™0Þmld‘³‡•ÆÑ+dËu$²ñ²LRà:„¦Y×’Îä±»Ë,±Û ´ sÖì˜Îkd—¨’„`gd²N زM°Í޳ÞÁn0ÛÇ\'9÷vpp)ÆAvrÇJ°þ̇YÊ6YgVNYÈz ²N$îH$îÎ$» &É6C8NBÎØR!wÕÙˆâäW?Ì$H:õìáÆ#Ñ”Z++:¡‚“'”zÒÍF¿˜BFàìXÖo—/¯äL?»È¢7Wd@E?lãÛúΚÆi‘g3ÐÏî†"]hd¨¦©ûÑg@˜¢]¾¿Ïcô1Wæ½+m!îýäÑ•0A;~ò óéÂGbåáä?ÎÓ¥ø(~¤+f:3^zõjˆ\ó™Vwa6CÖ¬ú??qÉÃ=öbWnÿyÁoKú^ñÀ?Ý^b j—±c¥Ž~È áßÑ:=ï $rK5™àõ26ÆXÉÃÃ%Þs ݸ’¼¬“`^ÖXWg êHÁ'ÞÈdL²$Na=¤$º±³»¯,Y'oîêN°‘•Î;Ëëc¨^ -^lƒŽ¢NS#Œ6 ,½ÙwǵŒÝo|à’,½pœ„YÆ9ÂwÀppð’pXÙÒObAeÞpäèÀí–ud†Y&± Ù$g{s„1É;lƒ$““üN…Ìw?æÌè#£×‡d-úk ?&¤0?aý 7ÐßóÇÑþ'¢Ÿs¬ZL!:Ñö’ì'öþ« Nú=†KÒGL»lñ$ÐGB× UFf¥»ÿ¾ßû.¾¶hPl"¢dÉÝw±íBĶ ú, ;°,’É, ;àåណd™œÉxW-ù©&|A%²›Õ¼‚Åg‡8w,²K$å=$˜A﬜€l$1——fdÙ ⛃&Nƒè†³×{Gc8×.ö÷kñ:8Üç-áøcÇ“¸é´þ3ûÉÆ@ðwÀwa1»g1dØsœg9d’Híœe–#VpK;ž²Ï3á°““€HbɹŸ ž¿]I:l“¼ëúýØd¯å=zFCñÖ]™: :T±d2qÓØ’v²TÅ!ª}ÿ™þˆCµa#!ëî*OÛÓÉÁíÕ˜ÈÞ Þ’@e &¤à8N2N¹IáóÃ<5'Žçg„fyd~ó-o̸ êL‘`^¸z$îË$€Ë¨—«±Ë ×Ab¶9–O¨6k3¶÷ÐO–ÀÂG!#ÜóÞFdHWEîêAóÃáÜœ{Äã#€2'VÈ%#ÈÛ~&&¶p|K?á|YÁña<' Ù%ŒYeÉÖDÝe’gêõ${“¶N›!b :dÉ;‰gŸáþ'O–EÐ$Ö4Æ^ú{þìØvƯòþãц’`‘Ã:‘02vï¯ò’Mbv!’ çXΥ̽)#åГNýÃt?æé.±wþ÷rlã'‘È;“¹Ùó¤ËÂy’K5$žÑe“3ŒvÀ,±Ÿ3ÂI>eVxfxRxfÉ·à~É,nDz A2A9³"@:8 $¶í’É!¬%l…XðûêY“e艻¿,y—®[xô^ø.²4cÌqŸ²ÎBÈ#€ãß aöÿµ’Þžpƒ‚;çÇH2ã*ÀgqsázeŒ‚>ž½Á' ¼˜É(R*]ÝéÝî"t™þ`'=ôGdt_`1ÀN°«É¿b“\kƒÊltÇï0…ìÇùÈŒý[¨|L¯å 3<™DëŽ>º.þ@õKo£mªÈ;öP³°¿‘¸È4rÏÎŽ5‰1;öÉ"Ù†”DwÀ%<‹‡·¸kÊ ßH‚Kl!ÈŽþ¡˜î‚›SݺYêa÷àçg÷'öCÿ‰—ÕüSy,ÿp]Äð¦=A÷ÛôMÈ þCü[Oyï©ßDõï©hð‡ äGHgDµ£q?ý©öi§óœ{Tü3þ¾_=>¾“uÇ« ºßîžï¼ŽÉ°N艠ÿÃe„\ïŸÎÓéBÜÇ;åÔ‹¡Ä?Í0/óÒ:IßÙ{mߨsvÑU]X£@ñ x4ÿL†¿•ú}Ètm¡IÛŸæLIèŒ4þPË 6L x&I!¹%“%É 8pœçpðð’läÉ+<33$»%lñšA6-ºˆ;6HXFL¶6M†ð$-8|kc™u=®ö%d]frIÈ&üj5 Á¼mØfp—¯€î:ã 8Î2 ²È!zH¾2Ë ýŒøž0xÎrBN¬³Œ,’K8,²Hà 2 ±bA'ö“¦BBBÇY3Hœ!.о‰LØï 7©.¡×‡¡»d{âã‡FÎÎ8¯µ]mìöê*Š¿eVWÊuÓöĆ ¤íz=°KÀ î8öêûU;U±ßNì†a–§î·s×^† ßå¡ EKt@ý'dóŒ: Ñ(tCFI±]·pw>Úk¿rmÏC¹~¡§‚EÓò`ŒåÛ÷÷Ô-uù;ßéÅ7êÃ5€žˆøOðY³Øg¿ób68K± ¯Öÿ™fLdÖG$’FÇNàm‡–IL:$³¼’É$ãºÌË\¹%t¼6HφNf{œàÉ‘á›ñò“ñH÷vF YǸ$‚F@vI$$²Ç8G2É#`ìˆu&É„Îo–G œe%úÃxÄ<ÆYÁçpünþ&ç,yà,á²üš^‰M¶É‹ ±²Ë.¬åxÉ:³“–öXXe’AÆY'|d˜ÉæÏï Œ„Èâ6Xzû°È:Ù“¦KûîÃô 6'œ»½{ÿ293[ ² À$úi™ëÙé[IùñhòƒýoÑcÚZ²v`ÿ"0ûdéÕ€ÉΗ>ˆ)]_è²îÈoI&Àï×Àz_¦Gsé“¿åþBY3÷þfIÎÙÉ ÆCL86;ð$†YÆ]³¾2Î3$æË„’33$“Ã>gŒoÌÆÕŸPÖ;–ZYig/Anó `ã¡g ]åÔæña'WGLåÇN‰A´äÊ=«‹ê/gæzàË ½Yǘ"89<ãwÀpÇæ8ž¨>'ž;ç,ø¥Ÿ#᜾øK;áäû‚w8Nî¶Î§Á>쳯ûì'4þ2ẗÿ;ù?Åäõ5 ,š³™ÚyY÷ž^'ýy,êNïKgy±0½®Hj9Ù£õyv{G;ëè%n˜cf Ýݱ}½¶ñqÌúaÓ"}ø·½vLL×`ì³@Ï_Ø ð²Ãu×ëÓcÕ×{`–ušÌH %’O ¬’I#0)%q’Yñ' ðËI’Ìä„“;32Íø‡wg:›áí¬Xqì° &d$ % ;“,’GßaÂH`Í´u‘ÙÇ©^6DäȈ>~¥xlO†vx ‹&üyýØâÉÁgnñ¸ÐHFë\l°Ã)km¥¤¤"Ä“)ËÃ:ÓS¦|y&|~L¹0gn¿ãðÎ: zö²fc¿E “¸˜å–Hï<$“Û%’I$Žã’ZìÙÇ{' =’]ä–HI#>83Ã3× ðäËô©·…Ù&Ã!ÔÈÉÖZ d‘‘.·Œ ™ 4ƒ[;ÛØÂÓ:I"Ñé=Àub89Œç,c‚93eË5±àã8$ƒ€å€õYq“à’Úð#³ªñwøM’"¾˜^æõ@.ø>ËiCò†$1øQd‡X0*3|7n§g‚'¯Á.·ŸÈ¾—_•‘€y5ë ÷ $ú|B²gvN„gâÇ/°ì2Â_°~–Šz¾µû"È`õ(Úîäèc ’Æ•z³D;Kn°x ²;}Í ï8T¶m ß=²Öj¯ú[‹wÁô—ûk?»{½»ûþÅŽ†Nâ¹ãü@îs¹;gË'ë¿Aw§^ÿÌjBãÐÿ”®ÝlWÞ ½oàez€g—Ăݧ;ðû#:ð›öHz ³°Ïû«†d¬{D®ÉÓïOLøéwݽÙß§dõÛ複³ÙýAŸ´é×û¶=ôÉÔé’wÂ$ÂK8=l’ç2fY¾äôLÆNsdá5ž™$’rrI’Cg–I’oÂðKÞÔ]˜ H&ÂÎæl°Þ0’t÷km˜õ¼;±Ú^òBÆvÇgŒ›¬x Hì¶oû$œ7Œ3Ž¢ã’Ë8#8_çÈ;ÊÑ/Ï'ŽR#rÆ“±ŸÉŒúÏ3³‹N4Œ¢Ì’uß xÙöîOKs¡Ø x#ÏðAÁg!Î<çðÄ¿cúÌ|‚È8Å’Y%qœ†A’Id†p“耪E Ë l²Nì3¹:, èýwYÜC§ø³õ¤ŸÐÿî}–f|¬–XŽôHjã!žŽˆñƒóýÐ÷oS¿ëþ’d5(ÉyŠHÿN´“®þŸcåY ‘ÏàèNéêÄÌãì-"@Õöï·Ð¤Ž/¡lu,‘áé’îÉ  ’É ,žnìdîK8I:’Ë&Cd’N $“'RLÎÈì½,>Ç;í˜y 8aÙ'`“„Ù $€Î=áƒê@åáì‚êq“©%e¿2(:²’Î<±Áœð2êφui`<¤Gˆà8ÆtÉÿgƒ~%6Yd–Yeœ ²ÆË,¼¸Ë:$‚ls„É;‚Τî 鱨,`0`ú÷þ¸!ýõ'KöȬMý2YâLXÐÍS¯nÈÚûlrL9áý¶¹˜ù}¿êNµ1Õ©TIߌÆs;Ç MÜÌëüÔuûˆh ºXcÜM8/úõèúd þ?Jºê™¬Ž‡lIͱÝwôÊè‘LVGú,Š·zmwçÄ,Üõì|aà±dîGÆX¯PWd4‚AîB$p!–I'VlîI$’K$’d,}I$ä„É<³t Ê=[G]›²`pîN¼ÌqÛ¡²XÍ–21‹™#2ðNdy—½.’%³„'%fIHÈ·RgÌ/ÃÛÇoS¬/ÁÈ'ÛÉÀpa!霺_¥>x}¡tcë¿Ùúpÿå¿Ô{Vgéñ‡þ¤7ïöÃÿ³ü«îúüϯô´{?LêŸÞÏýÏÌâ²müÏû¾òþ'ýÈp ú_â£ÿ¾»r.½°÷EöVÿÙÿµÿ{ÿÿyþÑïÿ·ø½ÿ_Åÿ¯ýWãÛñ?õí:wÿcñ÷í:gýá>ïëD}ßæþÔ>âǃûxNÌwë>‰œýHÑÍà~–ãŒUñÜûnć¢ÍôýósÚu÷§ oöŸhýùžºæ;‹Ÿîóéîø²ëéÇúBp}Céžµýs·ûÀ‘4ý àUkMÓ>||>™ÓC çMÝÐôý$t´úõþ%ì’§ru¬°=·Q!¨ÏB ³Æ1ëbLlj;^+d 8ò“´’K:dÆK$‘‘“¹$$‘‘ ºœ™úµñáÙvAÒÎHÉ2Ûw%ŒHeé˜;“t çVì4%ìžË;’ÆRõ™)#·Ð]”3á 9à-î8ï6Ø´ÿ€ ƒ¦ ˆ4‹ÛO¯RÎ ¿òý”Íï}zFØ¿2RO|Ot~ø¿ýú¿ôêû¿wýG£÷ÿվߵþ­÷ýŸõßÙÿR~wýXþïú¾ŸÝÿQÿبõþ÷úÓúÏõ>ÿÙÿR}>þßúŸq¯ªg‚g¾)ùýÛ§¸?ïê^aÿ¶õ_î<ŸÓÿkûÇíûc±`÷J}“¾Ø¾êŽÞ¬ƒû¥Ÿ<ëÁëýÔÐ>ë«÷áç~?ýŸúŒ÷úÿõ?øÏñ}_«þ®¾ÿFÿÄG»ô¦i†jš¾§}°ë^¼"ÚébJDÿ1g¬ƒ~a“Óú‹>‹õ'Ö:=áÄç´GLMØORÝov6éÏOîYÿB“l8/ÔËfÎÉa0Hld…–]X@HA œ³¾½&vdင~ÐÕŽ‹m9ÒõÆÄ(A‰Ã,oà ã²v^A^·†Ûøüð’ïWó†iô§YžÏ¸³"û–Yc–@–9éo/,?e«ÛköÚý¶ú+qÚ¿oÙoÙ·ìÛï•ÞùldÛpóg躼õ/üâÐÿ{¤ž&Xf\î.û”ê3íÿ»ø¾ÿû¿‹ª’´Í²ÿá'Ûv3ØY¬ÚøÓÆÿ^éÑÙýÚ?úÕ¯˜}.·d³ìô£ëým‹Ï9íÈêOÂ:l£×œ=ÀÁ¦^‚Æ ¿ 5c&’ÿîvëüýŸ¢ÿ¸Ÿ»ô¿4võ8zŸÁaõþ)óõ{ºŸ«w«ñOÆOÒ_„¿ßÒõá}a~Ò>…øïÀE~.%Z¿«g¨|2Ûkà?ƒdú¾ð—¿ÊdÊJ{%8 5ÒŒ†ß ¨-g§ß¿#ÙvAÃu’دf²Y=¶pÎì¶Ë%Óp]ÀIa÷`ÅçÓmÛÑ~ž!ÓìÅí® ÅYcõcecešp –6P3{ÃFiV¸.XLxJ¦1Û*Ñ.^d«Y N ›ñ5ù:N:‘=NÏ Ælœ?ï ³Q³‡^ؽ.¤‘d™ÞËS+~à™É.£Ämœg¦. ² Üà ³Žù ‹ `‚Ù;¿ŠéÄ<`_aý–{ßqü¨XIg9ÊY%–YdYÆe…–Y-±é€#«Û!vr#ÅŽ’ue–wc–wίlƒ$Éc×LŽG– ?—÷,ײ˺,oCÎ~’Ã=]7÷ ö¡K‹²]i<ØÐÌ?¯1xà“äÿ_–ö7I‹ƒÑïÖý¹.Öáz·¶ÇzvJÍüoù”­ÛIS*]Ý’­Jd)V¡Xé™ Õ¹YjÕ«V­Z—*]¢\¹rål©R¥Ê•*LV(þž ÌOqý/RèwËˤº^2xË˱á8ëŒwd›9Nîòl98²ÌúŸÜ,dÛá…‰dE·l[6ÁÁ‘Ád/cÀÄAdÞ ø—1ÅÁåã'û_éÂñ>ÿ¼œ2Ye…–Ye–Ie–YeYe’ae–Y$LBêÖôCöVzµÒ,úVT,lî ,îVO7†YÑxXLxáœïû’â@[ °ëÅå»?䃡¿¯Çëhü¿Ò¥S­“µîÑÜõþ þ€'AÖ>äߢ?ƒ³Ù#ÂI镬Râ…éR¸¯ Ï©ÑãÖþÉRÃC‘Xl„Iì£bWHÈÁ= ÁÔßdu{{B¿|øÕ?¬pØQðžëŽ&1à+X¡šÆ1¯¶r10ºG‚í©2Õ÷?kÁ°°ƒðl²N4’õǼ,Û"I2d,ÞœÜ9é$™‘÷b^ª×¦xÙŽË,´Œà. à ²^uÌàà :g €€ºà8üyG·À}ÿrì“üßïu'xIÝœ±Û,²Çl‘³Œ²Ë,²K,² ,ŽÖ]]“^¦øË·«¸É38=¦*Ö¶¶ÖØÚêÒX÷klleeŽØŒlŽc"Ìuí±þvÀ§½Âëù¿«N~md“¿gMØÞ¿»»×ø]oûGu¿Š;ºûü½ýÌÏ/í¼¿´~Çöˆýøô§ñ<ã§äö è”tÜzß?é_ükéý‰Ï?¨ÿåpNŸÛýK( ͹Û”n¯‡vI–X'ñŽ«ŠüOiG×7s~ËKï«uëPð<,$XH“Å$ÈY$H’dOªý¦ÇA#„ÙË~óûä6j—T ºžÎÉ Ýמ§,2Ë/pã+-“¥¶a$ ÜÙ˜[š—TP'Î’ñœ »&äpYÀXÆí°lE—¨çà‚Ì 4¿ VPxIŽ3û”s%ùÏ҉ܖ6Yc*8eœ<8+X|³†¸j]® ÛØ‚ËÝÛ>Í»vóŽøëó7øxªþ}uxxŸÀ·¶óŽó‘R‰xÛÛY™q~oïÙO<£ú #sèÿRyËáëé÷ZýÏí»úêú¿¡þ®?èþwqü¿ÌXaßèXó§ý¿sÉú#Ê>„ý7Ô>!?éöN›§a#<?E”À~‹>³£è³ôXï¢WÑ嵞'ðôГ-ÊÎ+ú•!~G{;îìb£ßt}ÛxËÙÞ7ÜÕPËY*T©R¥J•jT©A+mJ•’›R¶U«r­^Ž]@Ó8J@ö‡íˆÏGú#©x"†“ÎOÁ™œœø)%’#&6ë<$Éy€þfáp¶ÅãaôðYgÖ^µà…ÞL»ãµ‰‰ ëƒsà3€b6Ë5º«éž/´½¼vþiÑ–}ìÈÑø'„ÿðc†8gáòðÍžLX±Ã,X±gà3ñ<Ù³gŸ<^[ÊÏG G^ÿkÏÜû_Ùÿ7wZ×ÞLùûÇ÷ìóýù€7ýOÉý¢<FGOÑzîˆÉéþ‰ßñõäGÜýCÄ,þS%Ìþ§ÐóÞdÈÝþÐ_·ÖGÁßè™ýý„F ÿ-õÑþ/hÿ0ÿwtÝOÐÙú³õcêÆø¿©;âϲ œ±bGÔ›ÎM™2~¦çêOÕ™©Ù©ó’rDÔ}Oá?„¨ücûö} ¯‰Á›íøØ:%f`FwŒxMÔœã&²E‡ k9$’33$“»l} ë7[;uÉfðX@p È,㬃€àŒ"=)u¶kôp] Ï{þÏ$îÞÄwÕÑû‚>bÿe–Ye–å–Ç–Yg9e–Ye–q×ïŽYÇVYuue–Ye–YeÖßÿbÂsKß¶8=þß§îmïïíûýŸ‰½èO÷hû­ýG»û¢}ê:LÿPå§ðGû»/úN¾ßêOý`ÆÈí×Íø?©þíÑþíÿÔÿv¾žGƒc<”󵩗nßÛ•ÁŠšÖµs?‚\©Š…’ÂKÛĹvjOÅzþ¥‡újYN¢&bwxBÛ;“¹Ë9Cµ^»'*ù-á™áÈ0º#¡ˆ;'"YxÎ2&ø]θ2΀‚€²,Ž;Ù È]Eø«“%7ìMQ¶éúcëäò?É Ë¶Ùyÿñï¾ã;o’çÅzÏÏ‘¨,†ýñ^]ûdžÓa<Û¯Í êÇy—µ±ˆLc݆wƈgûã!b<~øO¾9Ùû"¼~访f;3öÇÓûcè—Ù#ÿV3Çî½WíŒñû¡¼~è¿¶3÷Þ³÷ͲÿèwÙû1iÿ£œ|þÙŠoþWÌd}ý‹_~þ™ký ÝÄ]$ÂüÓÆ|û?Râ'þ>ïÒžGô0öþŒ?ø9‡ŸÖŸùIö~¬ù˜óÿå'þ>ÿçoþVüíû^ÿâçþ~>ÿÓ¿ø¹ÿ‡Ÿø9ÿ—Ÿù9ÿŸŸwëÏüÿÊÇÝúsÿ ?ó3ÿ3þfàø0ùÈÝþv}ÿ©_àúøg×QõÄhϪ'ÛÅ›}¼·ÛÉ›6*¼tèÞ•'ý™*¦µ€3“zªÿ†eчpļ!7¹$›x^Þ®rΦxg$’XÌŸx¥ÁÜ—ŽþÉÁ°¤FÁíàXØÝ²x7‚È8Ë ,× BÎ?ƒvÓàÄëù ¸þ þ ³×íÓÿ£§ÿcö¢“_¶«u=OGòbU\{¨]»ð÷^x¼Úû¸³Gßt¾žº©yãG±÷Húh}p»ø¦Ô[¾ÊÄ ¯!}ÑŸ}gÁ ßTFÿ¯šo¦ŸÕÁ·?®¦Do¦[=co¢«1dí'ï“éÝîÃO¸øâ‰ßØoëú¬ËöIKª¯¶êæ1B='„þ Ö}ÿË’>#wœe¸,Ku¨}_²Ÿ¹û‡ÿd>ÿê‡Û?ÁýÑôvû|ç§õ8`ÿô÷~ÞHÿ¶µÿuÿëñ)ú·Õ×ï¿ý»+o»÷OÿFXœBòo?øïþ‚8iL¹â#oº3íæÍï¾™í=}Ò™Óë¼ËŸ\fÈû8¥^ßÓ»‡ìÁˆ^á &xmô3÷¶‡êHpcu’|†Îs†&Á³fщ¶Q‰ ·çÌNæ¢Ý {ã¢Wƒ‚9žˆË#’Î" ;b?¼áþ«#â-¿¶ßÜ/:Úý¶¾ØV¾íý²¾Û_m©UòµöÛköÚ=·ØÚûnÏ-§¶èòµû¿#oÚ×íµûmÆì¶¹æ×íµá¯Ü¿’×ìµ}-ó5Ù_:ZýþKzõkk¾¿ïóµò?ÛýËç¿ê»}³zØ ù”öÂ}'ì%= _8´öÚ}Êol£ìAt}g~0?´g¼Müö±çÑþ¬báéÎõaé¨F"„…ÇòƒÐ¿˜GÒ¿I®ãßà´ø6$|KñìŽ=¡ù‡¡ßýþ"ê25 3ïö¹?µ5ë_u‘Ú~±××ï3¸Þ=µÎßj~ûGê?h£L?ižŸ¢ÿRgªŸuû¦é,3¡Â»ÊÌðÌ’òÎp‡lÉÔ“’It&fć :M©,ßǯÔZGºì‚ œ),ðÏ>¾KÝã¹.§†Vë†~‰áîþ6ê웩Æe™ÉÀ9d™¼œgqÁuFÆïb2–ÓëÄÅø¶W¶Þ7‡á¶Ûm¸ÛmïµË]µ¶×!wË ×l­¯B¶¹å·É¦ü&úÞ¿¯û³£íÿv§·Ÿ·ýÝwíþß÷ »êñ¬WïûcÓuû†?Xrâÿqü±öþ¨°cê4ö?Hºxÿ8^´?§ûŸhw|öÿ1íâÇûœ;°Ðaý÷oÜþCýÛÛý%ÐZ¤Ó°º¡"ô~‰CÀž{IpèúÙH!ÒÒcÿOñêýãô?xþèpì~—_OÒ«ú½"7ãÒ™õü…ûþ˜¯Êßç šeüâlú‰à?æoø$ë1ÍúCõ–o_ïüô&{ÁÿD½†Ù)¤ðöˬ”áIÒfYá›%îYxI»8N 'p½³2ð“7„ÿc«^éRn,ÁÎR^[ß-ü-œ²ÄIfÓl·[8Vw&G„á>Öc¢rlàçbõg 3tGŽ:‚€820€ãu‡" ±˜lº¥áïœÿeŽ]á\³—Œ²Î2õdq’¹+¶Ý@äŽX¯úØ$lsÂÿ6{ëù°'ߟ¿öŸZ?·ýÎ{ßÛýÊ}/ðÿhûÚÿvëD…V}Æëfîjgoóg¹_¿÷éOâ“¿Êþ¦ï?”Ó_?Ñÿ0ćÞ¹àßa~A{pYô$î&GÐÿ?ôÈ»éÿiéš¾˜ L1×ûÒý!èǯ_é>¼ý'Wa ˜—>„|éý#ÜÒ Ù èAú?ÔŸQÙì³Û)$ðÍ„šÌää嬓+$ffyfg$—ƒ-Ï+èËQ™˜}ÔãuO”,Cƒ„ø;™ÂòÎYc;6Û;,’OLeü;;³âËfÅ.ùê `»ˆØ‹¾€dAwð ‚Ç—g1¯¦~B'üO/ü9ÃËñmã:ÿƒ g5ºÉq'ž–32aàßä6H>ôß¹M{m~Ø_:^ÒýŸ»KíïîQë{{í”:_ßÿc//ïýÇÕýD~ÃöM/¡üÀûí]OÜpÝýŸôµ¾¿ê,{íüâ\wöOó(=¿·ÿ²ö ƒò žô™ø$¬êÇÛ_åþ .·»ìè©›ýÏ7¿ŠºúyÄü¯äúÿqÿRކfçµ0³ôÅïÁmà$8w9v^Ùè™é•˜Ù{½·m½Ì™)33,ÏLË.D—……3Õc*–¿ðIû¯ô]åâ—8_ÎRVd‚yï…œ8ÖÃ'wm»Ð–xÙK+¾Œ‡rœó’sÞgÀ àx€pACÈ‚7€‰/¹ôÒ ƒD”Dcx9>Yð8O†qŸ ’ôÏù³„xÎ8õdp7¶þÅü‹¥„þV¦OJ¿“úxcs£þþ¤Ý7ïõcÖôßÏú±ÿßÿ$wu;¾9ný©Q_öÿ3¾¿ëô‘γûŽç_õü„õÖ¿s¢bõûÅ3Ï÷‡Ü~±_cõ™ž^‡ûa:Üû$ p±™(ÿ “¦>_îZú×âuãþ#Éûþ cõ{?Èñ9Ž_øÉzŸá$²Ìd˜ÌÊŒÊ 2̦̰ß¶Ve¶Ye–Ye–Q”–Ye‡pþý&Ñy{d½pU´N –RS…xÙ'ŶÞöY,›fåÂÓêsdw”vr.nÌo/¦s‡dOèÛÖè"ÍäÜ‚ ë€àºˆŒ²87;<õ ÿQ²i È;eœç g$ ­µôÊúoÀÇÜý7æ~›ÿ!êgÿ”Æ¿âcÿŒßøÍ¡þeÑþdÿö“îý‹ÿa>ŸÑüN!øuú=´ÜÕù7Gù1îøQÿõ éý…ù²úßî}Ë÷?#öOþ˜OõOú™ö\l~Ñ ½ð-ø?}ÿ½'/_Ïè¯Ãt¼~‚Éê'M£ímöÌ?ŸÝgÿ´‘Óú™Ã]~µþ§×ûðöcøpöÿ];ýIÿæÿܰªGlyï'ÿS¿÷Ã,=¶dŸ!g]uÿú–ût¾ßß/þ}Ó¿ùÒû?\?ü˜ÿÌG«L?ü$?øhÆÿÌßøúwóëÿgþÚ}ß½?öuÿíoþöÿìëÿØÏýœûz}Ÿµ>ïÝŸgïÏý}ÏþÍëíþMõ¨Ô~˜÷»»2ŠÛ2)ÔÝufXDÎg=ûá`g…ås‡vï†Ùv7xöJÛ+Ë!¯SXûVÞÄÝËðȈ{‡M²/qÁηAÄC»ƒ»H8x Ã÷eÕýgõ1¿œvÿëoý=ôþÉô~ëõ_ëýWÿ&ÿäsõ7×:¾?rüqÿ®¿ô—ãý—å¯Ídó÷þ>8!£úþ¾¸JïÙ·äKò$ðó~m?ó'þdÿÅþ?ÿ½ÇK ŸýÝÿ³gÃì£ìâëû¿é~o·þ—æÿµ™_?õ¿3ïÿ½ùŸoýÍ™×ýOÍ÷ÜüÞ!ÿ©ù½öÿŒûÿëþgm[§ùí~7`0Çð‚QÝÊgïlù˜W°‘ÒÆüc øVþ¿¢ßÒ~­ïÅø÷ØEöÔýÔûcô²ü/ÕÓ¸Ÿx¿Õ4{‚üWà#è,=_Šê·œ]ÓZ„Kæš+Vù&XÌuJñ/fšisN–ýŒÓGÑ:ø“C'ú±D›?Wð¶~×ð±Ü£3¨_PŸV½¡ZË~óöXû?dŽõ" #O†æ¿LeŸÔ«ÿah5ýú¿7úÿQíÒþ¿ê~ûÿ‰œt"Dã¤I<@àëÒ»»=Þ³ýË›ýBdèý ¿¿ÑþYvÙì,@=J\&üÃ~÷òŸº¼ bb{ñ$õgêOÕz‘dHˆõž,ýHú…3Âþƒ<®§ñ¿† ä5Œ_ Ÿ¡+é>‘ü%{Ò?ho¨ŸDEøeàŒ ¬˜Í“à¹Â÷œ²Îp3‰ÃϱžäÒõ ’+gwU÷U€ã~'àÒH8 $ƒ« ²86:bÆ7##€½@pl@ì@AW¸"¤%‚&Bkw&ŸR!˜Wžeü0 ²Ç}mÀȤiâ 03ÃÄ~ þ¡'‹p`æÙ‚2¾ Àg‚þþ+Nx/ij™3³8™Øódìý¿î=ïëÿ¢èë¡þ`ŸÓýÁòƒüÇ\ÂO™›êïý•ãèëÑÿÔ/Çó?ú`ÿ¯Ì}Å~Óºþƒý!½Ç>¿ù5ôtG›¯ßÿoújå«ú°_´Èëôgvwý¿Ì+³ ëé“¿í_ñ=úË­ðÏ®BèõgØAð—ð‹#âNÌ›2vÁêÍ®g‰«ÂiõdýYúšçÔöñ#1!=OVcð‚ Xdò¾ åËI « áž2Âs‡rÞ8xÍpxd‰çOsÔp1dð1A½@YpÙe›@ìAe“»:,IHüb:x0=èœN”¡öHƒõÆE@±ì³–{êYºz±Ã³Ä¸ÄibÆÄÑe81ÃV³!Ï|ÿ?é…øþ§ù´ðk#òOõ>Áýêú¿éü ñ#öÿQ¿cöÿQ/póû?ÝO÷»_³ýÙ{þçýÝzúÿ»G×õÿvìývÓˆ`Ȉz[w‹ Y³¶,|81õ'êEÑ‘õbÇÔ2fg‹SõbE›; ‘.xbE‰Yà 6vs'%'®Û[n¶m'lãg¾{à™á†–Ém¿ “‰¶Y!ÈœgÀÎïÇ€sV ×€ë`‚ ,ã ‚ð÷Æ8éÈi¹ –‹<ˆ¡c§duõc$l±!ô8bäÁ‚xxDòàCpbE°,Ybu ¹‡b&&fXÈXwþì2v0?SìÔô³Ùƒùgø ?_ÚÇÇýBÃ;˜e‡ÝŽùòZûÇù„ö?£û6Ÿh†¿ª×Ûcymï¸ô_\q,xÁ,s’IðÉ$² $8B˨ &Ë ,²l'$$'6CÌä³—VIÃæwg-›¹fXfwŸm„æAxyã'œ“I™+óÇ-y|Øï^z!FÈøû²¸-²2,àñZx#³ 6pȈ0ÈÝ€‚È }Dw°+u‘Ùã« ² àŽ3œà±Þ`Œ·XødpoìlZFZ‚¹išÕ‚tÈÝÙø,;é¿ý½`»‡ñ)/ØßÊÚž“ŠÊÚÂuÙ­‘ÓÔcêÈ$cx`>òmŒ|Ÿ’Û¼±/ÃIg…™rÞVs†ÞRlç;—«_£†xxÛ\áãÍŒò³ÆËÜð½&`Ú'ßÁg“Ádru¿€¼Á0FDš$ ZÞì‹#²Ç‹½‚ˆ$0AÁ0tFÇÀFÁ1»°poÀñYÜÚðŒÊÙnK£'­.‡cœ{±Ë«8 #lêάƒH;ˆ‚<¤ pÙAdAW¬‚!qà…‹°•°ppDAÛD¸²8Æ#àfrgÀr?îG878ê Pµ#‡oå!*Î&å„¶– dÜ…e­·\d<ç'Ã~/Ã/\wñ÷–ÎIÃ/ Ã<3³3/.ðäñ³ÃË/Ç9’XÖw8Î1±“†z…ù†¬`Nm‘+ÆÍÞÅÔ» ¼c–nFò¬¼šÆpAÇkkÁÁän²8 ‡ØñŒFAw°pÛÏ\g| ´Èàà9ˆre6AÕÝ·Sümx2ÎpØ"Ñá\ÂÔ-,$ç 9ßø2Éå–x~Ï q²Ïpïò²<é<áœ/ ç=|[C‡&W‡VŒoÅ-°“†´Îø[Ï–yÇxذËQdv,›ÀAdXAð0a¤¬fñ‘±dpA=äp-œ‡@ÆØÙÀ<p ŽC€øÿ;¿òç¾4xw޹êX›pÛ#Œ6#Œ»Ë¿ÆK˼ï+Ãðn¡›xmáfy||^'eá´´³‡–Y¶îvvë9Çgå³®4Ë 9dx63ë¨wo×>~ ätÇeœ;bÄyˆàã 阵úà²Ø8 #^³B `‚82öA °àˆÈ,ã/llYŸ>!ð3ƒŸv;cg| 9.÷޹ëoVs“u³gýåþ'yxgàò¦]gÄfmµÛÞͳ’õe¶ÊË/xK-³Âäð‹Ão'Ìã~cÎçðIgkuǢ߈qŸàñîCàrpó“/Íø÷9;ÃwÎËðCå²s„l8FÉ™f&br÷t¼d™wò 8ÃŒN3„Û3”²ñÁŠDý¶ófêêÛ8Ë»‚ "È0‹,ˆ,aÀAÁXÆìoßAÜì@Dtñ±Á×!aÉÀãø™dþ!ï9™Ýøoü|6Éø·¹áø»(],Ùðw…çxl»–xg8Ùø!<<¯ì¼¼é¼3ð`Ù'K®ììžûål,çt}OR]í†ð–QY,##`³‚ ²âU“Œ³ÈXÂÅo.`F,n¤ØFo0GpGˆ €ÈÎ8Ž3®MøÄÁÎD6$ÂÖúÈÍì­ã­á3‚æÉ#BÇxø ‚ÈéÐ’8vÄ`Œ²2|Ázƒ¹€ã ]ÄGžYì‚7"€iD0@í°6AÈòÆ<Á„{8|YÀ÷œ¸<ÝñœN lù= ãß&ÿÀüž7–Smžw†w„›yÞž^:™NpååyFN:Cá“ÃÎY៲vÅ$ çMï-8[Ü»lÔºÙC¬pcu±à"8v7839.숂.¬àØ‚""Ž˜v#€‚/|‘ð0Ž2È#þ ã%8Ö9eääù6|œáÏŠüÛeå^7——‡à²ò¸p¯²Ÿ7àüØ–FÎ Û8Î3xï„28$Ó„›#"ú©¥{áH´Ë%è8m'ŒîG dKÙu—±áòA0p À]ÀYð`‚‚€²Ž.ù‚ bÇà@rÀï9æƒ>À™ðw"=—£þù'q¿^ùÞYK¹¶V^7†[g…ë—Œ›©m•¶Yãm—ž~üïØIÜÆ¬Áœk å—\gÁãuã$–VõƒÚDªÐs¥½$®rwŽYc›ÁÀ6Á¥  Ž®2àˆÓ‚ ‚%‡¼±ã#¾˜ƒ`éä,ÀÈà%ÈHä»à~Î?ò;ÿ3¹ò88> ]ò|=À“œlË2ð¼«2·©ã~l§.q©dø‘“—gÌÈY<;k’ÛǶKf 9,îc'ŒŒ€¼¶pØ ûµ±oYÀÞàë8‡åógÁÁG“€ÞHà#ƒ2 8n·« a1¤A„@Ç0–ÁÁ FØDG†ÁÆÁê9 ‹x8xcx>gg —§â|WŒã8ß‚rü…åÙfë…åxvËeáãeãYyW†xe“àÉËœ/ †]ìóÐÏ îÄø³àa¿3°Å¯%œ0ÍY2]ÉÔ, ‚H º‚÷ÍØ‚Â÷Ü‘Ñtl ØîÝÚ±ÙÁlpÀ `ààà9>ÀG:îwgÄà;]y?ýI¦q¼Ç«¨øu<¼oÅá^:Rx^w´¶x^6xe¶^Þu>[+w;&ð88ë–0³I ädÓŒàe¶7Ä&òœzbŽ„{Ég|1ˆŒã ² v$‚Þ î àƒmŒÎuŒ±È‚2";#€67c€ˆ#ƒ³xƒ€9†q–pøÇŸw_-ø¼9Ç¿“òß“<ptp0ðq—\Él</=ÿÅŸàølÏüòÿÂü\áågâ¼/Á—–Î{-œ—àÌñÝß ÆÊpœwÆp' '%Ÿ.øÇç ÷€Ÿ¶íG„²xïm‘‡2!'wQÈ0;™u–ÝÄ@Â#nà˜„ˆó³`ºI=pDÑ“­ã#€ˆà:‚#€²Žˆ„ã ‚ ##xx8È‚6#-p±lwƒ€à9;m™ÿϾ}|=¿“þEùëòxYçgyxmáá½Kâx߃,²ò¶ÏÅž¶vïæ¶ð³lïe×e¶ÃÆM×|,Îsá–<–uÀI0-GF9ë> ʻؿF6p¤AîÆÓákœdÀì Á…ï‚ìƒà;^¾ッƒ€îî2#“yø à‘ξF[ÿßü'9ËñØàù/-²Û+6ñ¼<2Ël¶üxY†~3ÃËg>æÞ`› ‡àãÝHü{ä!n³„"w›­ÿ€cþ!áÈàñw¼?_'ÏüKÆÛðY[xxe–YáxÝø6Ûl²ÊË ¶Ë.Êpñ¼6ÛÊ[Âò¶ÙÆœ ±ƒÆ÷ ñ÷m–q‡Àe˜àóh>¡^û¼áœ}%ýè,2ÒcÑþÄ “«®B"l±‡„[À9±1¶÷'dvGn[eÔplœvðrpDC—q-Ÿçx#†ÞG¸å4ã{øwÏ3ä¼oÉ^6m™xaá–[xW…–ÙxYy×me´à›ye%´ãxmxmåa”ÈzÛxÛmFîÑçß;m½ó¶›;Ã1À÷’ÛuãÛÊX/¬´HÙi ù÷ö$ØdpFlpbHKx ¼¼:ºŒŒËX‹®s¾bà›ÁÑnðr<œ pr6‘ ê82ÞN ­àùŸ&ôÏ?7þ ç~[l¶Ë-ºËl¶Ëm¶Ël¼l²ð²Ûl³nÊq¥¤å²Û-¼,2Ël±Æ÷m§,âb6þaܰ™·{ð,X[Ü´‚.ìî7!Ýχolo§ ¼dDCiÇŸ†õÇ{øm¼m¿ ·®tØL‡—mà?1à·ä²Ûm¶ÛÆòËlÍÒbÛnÚp²Û²²Ëm²–Ì^›m%¶YeºËÂÊÚÊ–ÌT)XvÛm!áe·àmãYmêÛ[[mm#89S aÒé½Îý2>ÉØIv—ñ§ÙR˜þ¼L—ŸëÁóavõ·gŽóàg꿸°ÃÝ™½ÙÊ—¸&²žv3v>$Flu–„]pð¯Dp0Ãp÷pÛ£Ãl^8!ãxb-„!¶ÛKm†Øm‡íäcâð[m¿–÷op»ÀÛo+ÂéÛÎÛÎÛl²á,¶Ë-¤¶Ë’-–VÞ 3e–S-–Ùà[^Ò[yYu–m´¶×m¶ÄûKÏî$?Ò¼·êIùýkî²¾}ÐÔŸÿ2>Ñ€Œ}Tú£Óý³ëQô˜¿_µŸE¾‹}?®~‰}3'ÇôE¼ZÞl?½>WïOûMßîeÌY¨÷,"†FòýƒÔ‚Èpl¯AN†ÀÈ'v·³©}§Õ>¯Õ?õ)ïõ=/Ñ),Z–ó%Ý`ÿµ§ÿ¡j;¹ûK{ï[§¹ö¨SËz$}ßÖBšÝ¿cöIöw±ýNb~¬¨ƒnâ~ßécí›Óé·~]ÓÕ~Ü`“èÛÏ H×Fõê|Úœ<¯Ì‘¶÷—¾\ÒØÌë V vÁ{DGôëijÇk¼ ¬l0ð<î›v´†×!´!†ÛXx!ˆ.ò0–ʬÁ!†Ûa–íëmmz-†Þ2ÛÀÛÔ6Ûm¾¡¶m¶×-ãp˜“hX³$;ýÅå?ZKÏéÈyý¿×Ÿ2Iù ¼Ûë…3¶Õ?ü9qôß§O¯÷Ï­~çÕûÏHþç×?\>¿Ñ}¦ü×·´¹²¯ŸÙ/æÌ£ç~É_?½»1ƒh9IÄíü¨su>õ~dî;Oîgʸúw{²3òMàAÜ-ƒNÄ}ú7n—.óRrè_ƒ>iJîè@z$Wû.Ÿ+èSåÔ›µèû”må&f·çºÞÛò_’ÓÜóåÚû´Ú´¶»*ðòq¬¼å¤,ð±™ÁÁt»T#Ôê"ó`^£/Ý×t(Vw!ê0c%†{•´m2rÓ:abÃÜck w °ÃÀ؆Ö×!„àaa·^5¶Ö8m¶-îHšK pÃÙ}ŸÎKÉ~´ŸÐ’óú2~`‡ŸÙ%æÁzÉÄ¿ÕÃÈú©x‡×~¤¾—?Hú%ôÓë‡Õú§ÓO¬~‰ªŸ0½+æÉód»ýëË~åÿÙ3ä~÷ˆûk¯WtnÿpÏk÷›—V|8a/Å»Háõo†7äHN ñ¤1—ÒO¡ÇVâ?,}Óè¶(?;rü¯¶ôm©m¹÷,©Ví;·ÇV­mµm­¶¼ì¶Ãßs‹ÿ蛥âÝO×E>‹}.}×câŸ]¾¹}pú]ö©Oõ+ãú%ÂýždŽÚ>GîžãGÙûÙo?¹·;­³öa½¯µZ [v=À R6ÔO­Ž¼âÐ8^®©ÝÔé¤O _|&EÍ—jhGckÛoîÓîüÒ¦nÔ¾;à¾MÚ…iø màSãní–RÙaã^^]à˜çxõÁ’Ûðsáñüƒ°€¶Éãb88Øç½ž].¼2 ÀÃÇsðtpêlùýãC‡à-šCŽ GL½Þ+åþ„ ªº'/Z—µúo¥?ý».ßÝ}µ÷ Ô¸v/Õù¿IôLýwè†gÔ¿wÖÿsôþ»ê?¹õÃg×?_éŸ_ôÛ½0ýYø?©WÏõ662”¢½AöþÄÿ÷ÞßÅmÎÔýÉqº¿‰y}A¯,éÚ»ûVÞÓ‘±_v&=Ï´KÖ'Œ,›õ¿ ÛÔ(¹™/À°z81vå$©_|ŠçUçñòjÕ»V¶ÚÛm¶Ëm¿ÏÕß+#àü¹Àüsÿ%ødgËx'à3À¼.±Ã¼óßœñ˜E¼àéaÇ·‡» ‹¶N>.¾þ›a‰÷%…ƒ+Ž [µUãÒÀàÞ\™À³ùm:Û^ãí0û‘;ޏ6S¢ ¨0F2ië-úÛõâe4õcég؞ХZµÁ¬‡›omÊ[O ç _«km¶²»‡ m¶m·”áÿŸ8ïüÝò|‰áá^{ÞOø7ø>µ›g> ÎÝqŸóÉò߃ðΧŒù «ÊüÜ‚wxÛäÓŽùëŒêÙø/);(óóxïœ,€¸Å4’Î2'NDå“ðî%ãlscâñœ9eÞÏ:Ç¿g…ÉŽõÂ]ÝËxZ‡ ÄÐ1š.Ã{µòç®{ºá~"|tËŽùÓþ~ç-øzçaxê7åä8Sàs×/Ë> ,œ÷ð×€·ƒŒ“ øc¼/òxÏIÊÇI~ ·âã!<ëÂü÷võtŒ)ðÖÉ>!úp<õ³wœõÁ‹ÃÃÆpð½woÙl`»‡—àlå¬pcÒ–Ø*› ?¨!d6. S"“Ë/ ø½|Ãà]]CËÆñßÁ{,N:»àãO‹kÂõœ{㯙oÎå¿þ^îá—‡>/ügÌùoü‡ñî;ãÏ!ÃÇW»N:ø÷œ ñ6läáùf÷6­}1èRÆÁŒ°` <ãö…—G§oãô6–$æä&Ž(0-ë ´zEÛD™ý<,+ÓlœŽ:»õ/'ïî'ƒOðx$>{//ÀÞ"^7åìãxïþÞIÎEÏÿg¿åyÌùwÈq¹ÆYÿ¤Þ‹¸gŸ\§ÇRYcc¶0¯¨vü¨û‘÷£Í«¯wùÂ8SÜ/ÄF‘–ìêõê(ƒ³ß”:1FyBˆ[†>úÞIWÐzɸÉ<áf,ÔM€„IúàΑä~ ×oX–Yf3Æp—vòrÇ*ó¯ ðppO#Õœ<¼÷ÃwÈre¿ò¿ÿâÜÿŸ8ÈßšccÀ-Œ(oL/´zœm›¿6éÇ—qè§¾Šç ÏÉñö$¾ÄûŸ•žxÛmáBOš|=cÄP± —€^F/L$²xAtp{ ÂÄÂzl!"c^E±Ö|©¼í>0ŸnVÓœºPOJPÒ¿IÒˆ±–Ï ðdü vÀJôgÐgWbzž®©9kl6ÏØ¿îÇb¤Æ„w"Áºœç×1匜ຽ|“xyi Ÿø÷ñ-ä篇Sÿù7øf|ŽXýZΆü ¿¦ü-§´+ç?vüË«Ê!ѳcˆÓwîl2ÖcqQ÷£dW¸»û,y©†† :Hƒ¢ xHvÀ” †°3rB#GBtfIÙ2;µ!ÇlØÙB‡0¡zLçã`ôgÚ$£Ðž¸Ýk«—ŠúôËП@—쬻Œøµ+ím¢í¼©ö«»µ)òíuªº¥<‹)u&v¨+¦È¬`Z‰*8ÂÙwo^[¶ö÷8O.3ìCî x%¹bj=^ì‹xÉ´ž¡0F%“¤I‰ Ù ¥ŒU¼a=¼¼dð|5Ìøi&u“Â|;´ç/\ÀÂnÈx>ó߃ÆÇœçck"½y{_†ïPþ ðÃE_JF¶æ¹ßå|·ƒ…ß'âƒ<¯ ób}B4ð°V$tÅŽº,×S$%„!crMn¶>ôÆ:HáÄí‡yì÷ŸfxcÀGÐÝ  Ú´ŽÛ¶ê6S»<]üÛ;»ºzv¾]éÝ¿š§Qðî>Üý›^ÔþlëÛÀ±£¦W¸Ó`È…àUsªÕ¶ÊÛ ®Ýü1 amzé!a׋AN`ò@»¨ƒÅöÎoSz·øšiûfk™¹ôÂý„X8ïcƒ«¤ŒÝo¬Ä‚Bì¢MØ›Ã`FqêëbÞ^_?xÝ9Çù1uÈp_ð»ñÆÆË#Á~qp¿QÉ¥õy·÷~Spc8¯Åø©B ÂäEdCl6.Žâñ–ÒÃì°ypú–„ÓînÁÒIƒ]ÈM0å‡>¢ýG.§Ô]²Œe+Üü¼u‹ù3§n}®û”¯Úî<8êÑôK†a½«ÁD¡É\3ÀH;-¶Ûmmm†]øw›ó"Ûm‡àxã¹{øwÁÂlô'¤‰ & 1ëFÆZ#Œ½rø;bÂ>Zq¥ÐAܶ‹­¶ž-z ôZ&[üç÷ƒ3ÞÏ«v,:”!üÆÖÏ/ÁxÙσðnöÝ;ã8`•eRÆG,~¬{Âú/ÄÆ'Lýlyx"œáÜ}Û?seB©&ß‚ûaõ‘ôôJú#h¤ˆBCÆŒ&F›|i&{%Òü¤{"o™w‹»¾m†õ/Ñ/tœ¹‡)˶>ë§Íù“÷/ÊŸ½~E¯ ÚÛòêÛmøw‘ËÀð<°à]çx×,<]¯øõ$eäÇÍ(x¢܆^$çovRÏŽÛΛÇ|¿øðí9ÎAÛñ émü+/*ÓÓaE– Ô:a5ß$û‚` V¦õ‰Î•1¢Æv'.-:6þZà2DÑ1ƒÓ}1Bmü(gÔØùqäb¹³óà`…ôÎÂFêèc(é'E8HÉGbÆÆÆÇ&Óãã½²þú¤D±Éû€‘m•Ûâ8}Žíˆëì8°ó\§é$&]m¥¦pºl¸ {™úSGíã%7ƒ< @ú·/ÑÂá7Õuyœdœcöß‘Ÿ½¿#?ckwÎAÀüú¶^68ߟoü;¯øÝÉ%äÈå° GVé6B©x5Š%•\¿·Œvzìu{7kÖîÜy§ã.Ï0÷Æ÷ºìÛÂhãåú5òel}¾/Ftê‚ñ~©×Á<Ê7äoÎÊûm~í7#ZÄjðLqÓø=tÆß¾öÁÈ2&ÅôC.„l‘tÒvìZƒìD`e@—GØ}ÇOlÄ—'xîBÖœšvIèX* Laª0áuЍ7Ë÷‡êŸjôøêà™,‘ŸÃY&æ—¤gì´÷2ËÇ5™Vþ¸û2üSjœ¹‹vy¿%÷·ç·íoÚí•o\ágÀ>IÂD¶õÇ\.ŸæÙÉ®Ämßü8ÿøÉîÇêÇêÇ€£h„ÕfÀÏÒÌÏŠ< {a,Î^£é^C> DÌZõ U—gî·´û·ûµw“XëÒì¡ÜÜ9:¼JcOR>a˜{Œ=®=ª=ÊúT ú©¾ 0Ý]WK‘ô¤ë ½H'PÆØk`ójÛ;x6Vp"àÇŽYÑDŽ€€÷ì q˜/9îHM™z‹Ú_TSÓtÚ ɺ1$_g›òÄ”‡³;#ÎË/«5ÐY 7B‘#Ð_€rêÛ+ƒò<ÆÌ^7Z‹·ðí½áÍÜ/‰ß˜BC&ÏÁ#|öž÷Š£V ®ñ”1í¡Cà øÒ|¸XLŒô2;]Æ=‹¤¤ÕâÝ·t€û“±öØ8Äý$¼ˆtå¢]S¹³êRç÷*ó"Žä¹Æe/îÔÉô2WOÚȸì‹a³’ ŠZÎ-bÌBlÖLÓãæ:bl»O‹gÈÃG•KbýD WËovK¬»°6tîÜ Ô¥Ò¢æóäŒílÍ0oº7"±à!Ülƒ¤ö÷4ÒXÆ@a „`{µŒÝŠI97‡.¶ ô9ñ~GÁxÞ{×â¯À»ã¾1±±±úa-6íe–ÛbϽ;·í Nç…ü7WEŸ¢ÇÑcëŠí|¡ÝP/~uùSæÕ¾:ò ê˳¿%ö$3˜¼3©÷±…!èP¦lûzaÞ66n-òéðF›…”z‚IxD‚²AY›a;v Ö’b3#÷‘ö]½ D¹C…µBÞ¢E¯©ãºßÝŸ¶íÝJælIõlxË6È.€ËV“ž¦~HÙýPÕѰÂÈ œX¼:±»gyc®Éœ}žXm´1=¬cÞ^x¯¸£!°Dب‘gw¥>ÔžÉ˱eé9ö$=’1—n‰õ)ú£ýY’êÛ»o¤Hž;"$êu“zô²&=KVC éLìCμº<{`£TjªØƒL5ÌnFÌç9ÞqÍm·Œå?àÆÇêEPMl¾ÖbîÂ'Úñ;ã¦Ìu_‚Àø¿€·/-^^LQO@ß‘~ü¨NÅa>âÈÂ;#Ûî ;{QM ² 3í7Cƒ#ë_@@ŸuârE¯î°5­ØrÖtnŽÝq ï…å#ÐŽÇ|xLe.]>!{-Fœ ¡YÛvîó}+)~Kò°¼cce…–qYg!'{dA\‡97V>[ FU^“±æÏx“ÞtÌ FjºŠ2‹C$ÖB 5àº\n…$ ldZœ;± qèfˆ4{Bð÷ì%(¡‚–ÝgàÏÝœ;V¦«4Ö’ÖV^åHll4ä]ÁK…ƒØ ÎdÅý;oh×ÖmÁ#]a ô)ì+cbú±±±%Xýd‡´œO6¹ú3‹a:LgêÎXô/à8 Ô±ð›qúoÂß¿*ü /¨Û.Î?Éj¼Áó°þîß,gž™ø# Äc˜º°C‚4/áàæ¸„!"ïñ.Y;¬z›òĦ"©­š÷6IÚO±ƒ½Çì[ú°eãÕGYûåÃVÇÜh;'®ÂµØØØ$€lb °°¼1³â­ñ¸—K}å6Ú?fîíy ã ’Ë,°ž€&Äm XpJ'¦,[­øz²Î2eÉÇPu˜Ù¤w{lÇœ x8x÷ÉÇË “†s-•ìDÕüY?%žæÉ¡pyðÝe‰8ëg›#½’6LgFN¯J^˜^)Û`ƒa&1>âþxº —–––Û#6à¿/Æ7ÞZ¾,Ea IÚlÆÎ;õ‘Æ’pã^Þ!ô°|6 Ûš¬†) ½q¾\$C¢Ñ iƒаú@]pæÎJDRÆ[8Ed±`Ù°24$ðñžÞ-üK”r.öï‚nŒÙeY〲Π³» ã²È.²3Œl²îÉ'Œ‹,°‚ ƒ«8ÏŽZbõǽøâOïä@mŸ³I 8Íöã$™8Lä.¶ ãÎ:±É– ‘# ‘Š0øÙYa -'Ù4¬Þ|À÷c(îÀÙåà°É,ì d|¯NÀ´ãF¬w†+²/ó߰±#Ãè×Ôd„À8ŠÅáA¸sâ2Ðõ=Ë!a®Î h’"Ǥ€AÏ6*[27Šƒx”ñ]¶âÞýðf}îöFÇ °Ü³©„ÂÂ&ßSq–uekÎs–Yxƒ^3Œ .²‡|…œîÎ ;“½ñ¸üz³x3Œl‚'ŒxËRs€˜8|XZgæëb8Ì ì´_ŽruÆKeœeÔI{È á,BÈ-dÇ,³€±»tXóC¿È}ÄDDÉÓ  +qÖìœw–š­šBT – 0"ÂP³uö6BmÞìiFë† @X³&8tíYp+“ÁžÎXï™[#ž`y1'xaÖ›i¶ãw„[‹ tñ†m–p)øU݇9 "V.³ƒž¹Ûµø{dºÈ œç&8 ×>øÞ¸êÎs^CŒÛ#9ïg`<¼ŸÎ Ë]ç ø$YÆrAÜ–7¸,á'ƒnöI»²Äì³l, 8Îl,9Í,»±8É^§QšÞÑßÍê“~„9|‘áL!~Î ‰4!¾yÓEÜ„o\ã;WÅ„àÉÁ]b<ÇO9ÛwœåÐ|;Åç»ûD=rðœõÆNXG—ŽöÅù0-œúØ8 &ÎÎãYÖÖÎIÁ¶v ,"tlY{èàÖË:á-2D efÁ­“1“ñ ¡¾¥?jÈ4%†iy ^ñ´Rµ2RÈŒgîkë#®QèŸL^h/\wô/£ôJv7<Ü‹º<§Ï»m|j„Åmù0VïÄòÁ!Æi¶ÂBÆË$6ÂBF{^¤o¾æÕ]ݰîú=Û1n…˜†]oJe¯ ³¼û„ã& "Ý! 'àd-L'`[3Ó%.æ{ÉB;uË„ï€XðÏÕ+Ñ+k턦¯8GßÀ^ìV9,7Œ~#ge×ò8ÛËÂ6<' o26ZçgvE†EÞü±ç:å€ËwÀAf¼0[°Õ²î¶–xc臛’ò@v–áÂIÙ©'Þ'ïÎþïÊŸEpõ¿WéãÁo½ßŸjíÊ9¹^•MBûe¿w”/¦L¶›‚BȾqDYj;»:zƒ2cq¸ð€ìžÌ+˜Ý$*(÷fð<9Úõ/ `ëç$¨ «vÍXÆÙ«Ûd •ÆÏˆRMðˆûšGŽª` ȬDE³Œà £a0Ãø=-ðõ3ƒ~¾‚O¨ao,€ukưÅ&<"lUm¼µð Û)—¨8Î28Ë«ØÉÊDY%œgW|6{ã:Žp‚ Ž1ã$³¦È>ËadNdo άl`dºÛ$X,`àô-®°•nßxD€ê,M joŸIƒóÃÐÙ+Ý~˜¯T×£;i7z&ÔöÂÁ:Ô닱yWØí¦Ç§lAy˜3K?EÔÌ û06Т Â>† î!uÁÈz4Îô•ô…ú-˘BœÛqšœ{ k†º¼UPz)Mív ÃÑ  j¶;€½A`ÐÕ˜H1DË[‚ÎÄ){öhX<¶„»Ùw=ä8Ê¥†qÖÌQ§Óú,c{¹Û¥Nômz7Ð%=Kq.÷:k–Ý=ç^ñÙªîµmxFÇXZ¹} âa¸˜ 0…×–’á’ìƒßsCÁaï-Ä7WÃ^®]ç^BH²Fë2+ЭקïDˆéè[#ÙHD›Hön‹Óåz‰ûg§ðœùbƒ¯«Æ…ƒ¨–5ûSávðŸã?xÙ¡>áºÙgÈA¥®ÄÌ&ž–ßÙ½Mo­71ûü“ó!uÏÙ[çúÝ:°NÔ@þ[¾ ÞùxÏÉ^ÐØüzîj€¥{I…‰ þ΃àY7kr:ñç=¶J9åûžr¿”Z!7t¹G ؘºÒÚ ’¼þìÞ?Ã|p=Šþeƒ7¡ ûÙ#bïÃÂŒg»ÚÙé+mÛY›„ˆ°@ æÚÂW$2zÛl²¹,ÊHÊ:ž N“ê&Àw± Ö(L–·\l‘6uó ø%–XÙÆ6pZ}–)?a?lý ðGM™Í¶Ç⿜ñ#ÔiYoêan'É©Orïµ ö¯µoÎð’$6ßÔN¾Ž>,AiÔÇ+—&L^˜È猱,îBAØdpi5ybW:~ôÁ H¸ Ia·QÁ¸»Óó¼n™õaL ´NÀ†ól¦« lÞ½qü~¤ ›‰<åÐÅÝö¿ÆTîmAìñy}vgð#r:ìô¿„îoLGÓöì.¯ìÃv=äí$^HÔþ|¦–¬{<™ Çgß›8ÊS¿·OR.§P o¾)xfNÿs–¾Ãa!Zýæ#C'§ÅÓn†“ôö%û(}¿1Nþ 4ýäèÄ™%„‘‹¢Ë ów$ôÂîÛÊñIÞG%Á€ÒÙc¦W¼÷±²N½¾ä†N°KXø5µ”ö¶Û²­ÞÈ °g€tF0Øë¾S»²Ð‡BWB/‚ÉtuÀRt;˜Í’kÆO9ÆucÀi9i÷}°ðé?m÷Gí˜møb¾ŽY¶3[«ƒ~ÍœØXxÛ‹¼ØÜ; 02y¿¶&÷$ÄœôZzú/rcƒ‘±ÈõÜ <(¶Â׊ĨÈì§Û>öa¶2 (ö?2ÜS…ÑÿmöÄYtôg5Ùš;¸Ñ㸓¶ç¯ÙçŸnýW‘µß¿¤ú³.±ûaÇò“l5(©HÁ¿Ê"027fN§+äô;jú« u·y†ß¤5D3Þž}`n&ÍÚbÅÁB'-8ƤûØ#èXGßRÅo¸³iãcÓ+aà§¿¸ÑdÑ&ŒÛÀéHHè{Dz€F/JVß]”L`‚’fªu[·N†Ñ³c°l\õ²’£Á¯•xÖÅa%ah‹Üˆ’n$ðA<“#–º–Êî[+#ghþN°s‚³uw!ž7µåm-/æY=–À¨¤ùF]èÝ”Æp±Â à¼o2Mî8Ç–23Ø¿$Ÿ&–{$$é.7W˜ø>‚ÏÔ¿Eø£_Žƒ„,á‹Ý«ÛS]…«pÈÞ çYˆ#šˆ4Åãg“Ä—¸ #‹8—ƒ¹L@¹²Á Î=û µ, òùÏã8d3,¼62÷±B’5éÔËõAÑÞÿ<»¼ïöS½—h;÷ÙNßá/¸e€Õ–BÊ”#H‡Ž·Ü”ß]ûÏ»°{/º#XÝͽ]C.Y}—øãM‡’gáô½÷Éá7ÈôÞ›;6NˆQM"Þ<©:&Áó¢{ÒèŸ{›\hcgñšuñ{-ÇÞ™k¬zÂ4ì@0'i$w¼³°n˜ã¶ƒõ#ÑcÔ×Ñ<å¤åÇÙæÀ¬ß Q­Ÿ¨¸ú,foË n“`œ,¡) Õ¢×®¥ú¿E«Ô3[°w#I Ád’½¨&¨û' P@±¶^¤,Û[²Ø°±…ˆÏSoE¿ô—ÈOmöáHÂxHžwR»Þù˜ÈxZüÖwGÂ_Gû´’€Rm‚jXéäaF0v XÔÉ®öWø3‡š~ÏuTý>—‰-î}Þ!=šÿ}¯›´f>õtøv™X@ÙnYÀf©Ô”ánCûeØE…„À&—‰½')ÈXZGŽ;E‡ÕÔfwÂÉ!‰œ.HoM伄u°9™&3ŸeßOWv%!Å V,’H;ßÇ€¾C0´£¶Zg&löLÇ\!“Hò$ R.¾ÞfÑl}HôJñ4 {â~Zqo•ÛÝüWYÛoíšÐº7¸ƒŠÀåæílƒ›QÃe½A–Óf¯ˆ<î*[ãy…Ñ»b{±û±ûµ­–YÃ`&-&[ÇL Õ6_½:F®@X=³–ñãÛÏt_qE¥0žRîC)ÿ£ùqÖÞàQx `O²Ï£ž&¡IGËOÞùõ—@Ëhbt¢?ǸP`î–ìý1âu|¡üãez-²wÂ:—rÑ. Ãî %Òb}2¦«'Xðì1i(Ûgq€[Ýœ]~‹@8Šéû%@¹oÚüÄàŽ©Ì¼]×vJ„£_ê6!ÈAÔt/9î–‰5Œr{…¤”ë[]¨Äu`4ut $vFs8Y×Yÿaf–LËw¤¸rÈ%ˆ-­vÛÇÀä –¼:Ölg`ÆAÜž«wÎhYIÉ©BZÉyaŒ‚ÞÁ†bÛ!âÊ…½¤ÄFNèí{HÁ@f1§»~Íö¡F§#£)ó×¢ïÑ$rçqÛÀÛéQ÷nß+ò7ämGô±ë¦‡”‹ÒËnpÅÛÀ8ð …Ývòe…èy8{,Æ}2¡§ˆú§)jßâ±û€égŽ»c,pÆahËã -¸á‹ß}nCˆt:}QP{bø˜³@'×Èy–0 މ”,Ièõhëxkø#LêßyUwú‡$ä'©»q}Öwæ_ 3Ë£Y?ŽØÅÇ ¼vÊ„}ÅZXAÃR·q{!ø6ÇWÔqˆÕW›½7¤2o×€Eg/|+ 8ë[8zÌ›­t2ìF`d@RØŒ:‹8äK A$~-ñ¯Ã«ÖU€ùâ,5Å.y—7Ž·”Ò1)¨K0g- :R<ái#“äxÍ2ûvKmÒE½l.úal+[màˆÑÓcØŸ-‡Ü ´ØRÙ:)û‘÷켦ú«‡Fúéõä£Ôtµà§Ò­ý®Ì½J†XU€t,¯¨¦Ð¡…W]%›Û ¶®µ y6<Šô|SÁC¾1õ` }YBó°}¶t€|I€0³Tðw/¶W $tƒ[ w™‡öi¦¾/·b¹@Õ·ÔfΉ!H„ïxñ‰Žhfw—pE²ÇѰ:1ìœ÷«3Ð!=h|ƒ¤ÂñÖ`þ}÷gšú­öH4ŽŸ‡Rda…?‡ˆ^3ŒbîŠê<–Ó¼÷¦>É)‹wòûÀé‘/‚Ë[¼Û1¿;îS²œí%Uò±»Æ÷³–&n—nUdWeS|—Ž²â« -Ô'CéK©³#ÌO‰$°l¶x h+ %Ù©Áî“£u–Y),÷ -‹ºðag ׈Œ›Ñ j~壱×Î÷î~Ì»µ;v­ºÖÐ5 ÅBôPÇP}ËóâIyŸyt¸qk×%v ÙúÞñVÝ]9ô(õCÔK£q¢ÄšÐâªËÎÞ¦Ê=Át ÔÁuæ6ÏÓ€vGátuˆåŽ62w· ±Ù-³$šgnë®Ý™ê‰v™3ÀÆðe·vÞ•º“ˆëjð½ÃÊ08K˽ü¦ìj/Ù_‘Û’ƒoÐéüº»\¦ì¤ø×G¸Þ_µøãg#©ÉYåKFi Wû›Ô’>àÆÇÛˆ~£ÈQ*º¬p¼¬RÔŒLèðd‚ç@a2¹=%ÞðÙdÌÕ ešÀÖj?1qòYV^Öô=ƒŸo’a]jzÌûß_3Ñ›{}ÞYðs8êt¶®¸‰`vÓe69|¬!‘á‚ÐÇ£+×Í»"Çg¤]‰1EŒk$„ 4_`“JµX=Ö§ÑMFtgÒÇR¾à±T… Á_jŸ¹•<­dÖ³¶3«ÖŽ!ûŽ-{cóƒí¼µ^Zg4L§e£ºdœýL¿FÞìJy®ýà=ëtA6}.ƒBÇ:³¦XX„˜Wv èºf£´Ì` `!=‰ß+÷ÿP™×°Õ’#•éÊx±†W¢žð"÷Yö©(o*ûlŽ, p,.ýC{÷H›L¨A'snæå¦Áx7ãv¯à¿‚cÑo|L6ýóöI=0ò¸ ?%¿i[½·Ydfq‰º¡‡«ð]Þ"ƒzÁîÛ=Þ^ìub#¨ú.®¢sѳâÉ;¼@ä8÷(o\;z…»ðF}Hxš¡nM2{ƒ³ÚÈ7 “¼Ö陸ã#É{ƒ^¢^‹æ8Ü%mêVoSu{x;“&üǺyŽÛ;³¸îÞ¡õyÝòßÿÙfwupd-2.0.10/contrib/qubes/doc/img/heads_firmware_management_menu.jpg000066400000000000000000003376341501337203100257340ustar00rootroot00000000000000ÿØÿâ øICC_PROFILE èmntrRGB XYZ Ù$acspöÖÓ-)ø=Þ¯òU®xBúäʃ9 descDybXYZÀbTRCÔ dmdd àˆgXYZ hgTRCÔ lumi |meas $bkpt ´rXYZ ÈrTRCÔ tech Ü vued è‡wtpt pcprt „7chad ¼,descsRGB IEC61966-2-1 black scaledXYZ $ „¶Ïcurv #(-27;@EJOTY^chmrw|†‹•šŸ¤©®²·¼ÁÆËÐÕÛàåëðöû %+28>ELRY`gnu|ƒ‹’š¡©±¹ÁÉÑÙáéòú &/8AKT]gqz„Ž˜¢¬¶ÁËÕàëõ !-8COZfr~Š–¢®ºÇÓàìù -;HUcq~Œš¨¶ÄÓáðþ +:IXgw†–¦µÅÕåö'7HYj{Œ¯ÀÑãõ+=Oat†™¬¿Òåø 2FZn‚–ª¾Òçû  % : O d y ¤ º Ï å û  ' = T j ˜ ® Å Ü ó " 9 Q i € ˜ ° È á ù  * C \ u Ž § À Ù ó & @ Z t Ž © Ã Þ ø.Id›¶Òî %A^z–³Ïì &Ca~›¹×õ1OmŒªÉè&Ed„£Ãã#Ccƒ¤Åå'Ij‹­Îð4Vx›½à&Il²ÖúAe‰®Ò÷@eНÕú Ek‘·Ý*QwžÅì;cвÚ*R{£ÌõGp™Ãì@j”¾é>i”¿ê  A l ˜ Ä ð!!H!u!¡!Î!û"'"U"‚"¯"Ý# #8#f#”#Â#ð$$M$|$«$Ú% %8%h%—%Ç%÷&'&W&‡&·&è''I'z'«'Ü( (?(q(¢(Ô))8)k))Ð**5*h*›*Ï++6+i++Ñ,,9,n,¢,×- -A-v-«-á..L.‚.·.î/$/Z/‘/Ç/þ050l0¤0Û11J1‚1º1ò2*2c2›2Ô3 3F33¸3ñ4+4e4ž4Ø55M5‡5Â5ý676r6®6é7$7`7œ7×88P8Œ8È99B99¼9ù:6:t:²:ï;-;k;ª;è<' >`> >à?!?a?¢?â@#@d@¦@çA)AjA¬AîB0BrBµB÷C:C}CÀDDGDŠDÎEEUEšEÞF"FgF«FðG5G{GÀHHKH‘H×IIcI©IðJ7J}JÄK KSKšKâL*LrLºMMJM“MÜN%NnN·OOIO“OÝP'PqP»QQPQ›QæR1R|RÇSS_SªSöTBTTÛU(UuUÂVV\V©V÷WDW’WàX/X}XËYYiY¸ZZVZ¦Zõ[E[•[å\5\†\Ö]']x]É^^l^½__a_³``W`ª`üaOa¢aõbIbœbðcCc—cëd@d”dée=e’eçf=f’fèg=g“géh?h–hìiCišiñjHjŸj÷kOk§kÿlWl¯mm`m¹nnknÄooxoÑp+p†pàq:q•qðrKr¦ss]s¸ttptÌu(u…uáv>v›vøwVw³xxnxÌy*y‰yçzFz¥{{c{Â|!||á}A}¡~~b~Â#„å€G€¨ kÍ‚0‚’‚ôƒWƒº„„€„ã…G…«††r†×‡;‡ŸˆˆiˆÎ‰3‰™‰þŠdŠÊ‹0‹–‹üŒcŒÊ1˜ÿŽfŽÎ6žnÖ‘?‘¨’’z’ã“M“¶” ”Š”ô•_•É–4–Ÿ— —u—à˜L˜¸™$™™üšhšÕ›B›¯œœ‰œ÷dÒž@ž®ŸŸ‹Ÿú i Ø¡G¡¶¢&¢–££v£æ¤V¤Ç¥8¥©¦¦‹¦ý§n§à¨R¨Ä©7©©ªª««u«é¬\¬Ð­D­¸®-®¡¯¯‹°°u°ê±`±Ö²K²Â³8³®´%´œµµŠ¶¶y¶ð·h·à¸Y¸Ñ¹J¹Âº;ºµ».»§¼!¼›½½¾ ¾„¾ÿ¿z¿õÀpÀìÁgÁãÂ_ÂÛÃXÃÔÄQÄÎÅKÅÈÆFÆÃÇAÇ¿È=ȼÉ:ɹÊ8Ê·Ë6˶Ì5̵Í5͵Î6ζÏ7ϸÐ9кÑ<ѾÒ?ÒÁÓDÓÆÔIÔËÕNÕÑÖUÖØ×\×àØdØèÙlÙñÚvÚûÛ€ÜÜŠÝÝ–ÞÞ¢ß)߯à6à½áDáÌâSâÛãcãëäsäü儿 æ–çç©è2è¼éFéÐê[êåëpëûì†ííœî(î´ï@ïÌðXðåñrñÿòŒóó§ô4ôÂõPõÞömöû÷Šøø¨ù8ùÇúWúçûwüü˜ý)ýºþKþÜÿmÿÿdesc.IEC 61966-2-1 Default RGB Colour Space - sRGBXYZ b™·…ÚXYZ PmeasXYZ 3¤XYZ o¢8õsig CRT desc-Reference Viewing Condition in IEC 61966-2-1XYZ öÖÓ-textCopyright International Color Consortium, 2009sf32 Dßÿÿó&”ýÿÿû¡ÿÿý¢ÛÀuÿàJFIFÿÛC    ++&.%#%.&D5//5DNB>BN_UU_wqwœœÑÿÛC    ++&.%#%.&D5//5DNB>BN_UU_wqwœœÑÿÂÆ"ÿÄÿÄÿÚ ìz,%AR(!(¸Š‚…&#),&B ¨‚ÀX, B`¨,‹B¥€¨YH  ‚¥B€™Á@A@ãE‚€ P¨* ‚‚  ¡!` X*X(‚ ÀX*PX( @ °–赺kÌ«LYÈ“1ƒ1ƒ1…ÊF,†,†, `a`¥@E¤RE,¨T …A@!R€X…EA”$[** @ Æ€T…`¨,(T °PAR …À!QT,(!A`Áe‚”°X†IE€Ie,@Μîùè¼ÔèÏvçç¥ô³ò‡±ŸŠ=ܼôYüÕ§Ïåiõ™|ˆû¾6ÇÚgñûœþ }þ_ŸXýðŸy>#3픿a>G3ë'Ëæ}+æó>…àåÛÇÌõ^^qè¼üÎ×&gCNFfq…Èc3‹‹!‹!ƒ1…ÈbÈbÎÌÆ(E¤™ m‚XY J@€°P”Á”‚ ”©@°* ”`¤* q , RÀ°P!APT¢(±T…JJ1ÈT™Žù%%” ¨* q1LFLFWÖ65­DÚÔ7]{@èsŽœøÑߟš_W?{Yxv=ü¾zŸEŸÍSé²ùt¿W—ɯÏã‡Ùåñv>×?ˆsŸÂCï²ø ~ø<×îgÅæ}‹ä3¬|¶GÓ¾o(ú)àæ{wÆÎ=iæd¾…áÌësdokÎ)‘‹!ŒÙ fÈbÈbβd1´bÊ©``°@T  A``T¤¢TÅTJ),¢P²@TX,T æ’»ä"‰B(Šˆ€D °¨¤* ‚ ¨, ‚ ¨* ‚ ¨* ‚ÜFW›lº†ë »ï8és§(ër÷.zyyc×ÏÅ/¹ŸcèrùÑô™|Õ>›?—GÕeò£ë3ù}~_cì²ø¼—í3ø›m—ú¿ ‘öï‹Ìû'Çì«¿/šý#çsO}ágÓÈÌõnkÞâÈësæmcb¨Œ†,†,†,¡ YB)"ÀjP€”¥¥ A*q¡– %æã¾@(Š"ˆ¢, H"ˆ¢(Š"ÀJ"ˆ¢P…!H°©TB„) * ‚ Îb3ºÑµ¨oh7”½wŒw^ß|û÷Ïž^Pö3ñ½—ÏÙ~‹?š§ÔeòÈú­Ÿ%‘õ¹ü†G×çñÙGØçñ™gŸÄØû}Ÿ ™÷7áv/Û>33ëïÉf}Sæ3¤Ÿ=‘&?>ˆ|äúH|ÛéüÜúAóo¤‡Î>ŠGÏ>€|û߇‚÷RøOr#ۑ⽑ã_^KÖG’õbùoLyHy¯F}îG ´q;!Èë‡+ªî‚ó· ÈÓvÃ[d0f0f1d1d1dˆ¢U"ˆ¢(@ "‰@(€ €‹ˆ–Xˆ%”–EEDQEa9ú9ós–@@ d*À²Ãnxg©QeAP%‚X–J`!}©éøå£Œ”I”"–,Æ„™B*1Q@%€,”E‚XX‘`-€$X@¨EXJ€€„%Š–DXI”1X²T@@EDX±d•YˆT%T%H¹1‰Ö6µÓ66„¦5d– `XP•QD ”E<ýù»1ËB@Zƒ1@R6jÙ©E€X@€€u}_ËýW~zæS¯8°Œ¡¥ÅIŠŒT²e 2†*$ÊDX%D™EÅ”ˆ£²†,¡_/q«MØr= ÊôGœíÙs»èà¼3§|yî®Cn>®ƒÏdziæ»v/šèê5¿¥|æþˆóçVóÎz¦‡v“šöçkv „ôvCz4N½k¡µ„@©Q`X @ !b%…Te«*ØÇ+"Ê’ˆªŠ €Š|ûôfìÇ,eJ² [5çfHª„°ˆ€ Këúßúþüð™ãלQ&PŠ&9ŒfCPÅ”1e,H°”\YHÅÂÑŠŒTE¨“)QW0ǯ”½<øÛ9)ÓÑæ£Ð×Ä=<ؘúÞVRõùùÃÔËÊúøöZ9äwcżÝ/®smò·õsÅèëòééhÕ ésc.¼öiÍöqóõk>–Žxt^;.ö\ÒóŒÔ¨ŠX!(‹!,‹Q @BÄ¢°ˆc[sײň¢(Š "ˆR, $Êa§VÝX¹K%°€Ø ¶jÛeP€X(HB€ añ¿eÛ ž=¹IÅ”1eʲ†,¡&C²2̆*1T²d1™B(Æd$¢J&9 Ud1™QŠ¢L†*1T¸¨’¢J&9 T¸Ì†+"L¢ã2‘ d³áƒ,@€”°Jˆ¢J ”","‰,T±BÄ*)–9#lÛ«mYUEF,¡-€DQEÑŠŒTsêÙ¯"J#± \Àf½•l¶@D `Q”E>ÏâþÏ®6L§~X¯£/˜ìãBôª©(“«šçZ›sŽqID™tG,ݦ’ÈK(²T°š€"ÈJ1  ŒeÊX!a©q™#D•,”c2’âʦPЉ(’–,¢K²ÂD¤BÀ@ g†vcŽX›wjÛ¥Q”EDUEDQDZ`ÊeL+°A‚©D*Q·VË2P@XT—Ùü_ÙõÏN;1íËoÅôsvvÌ9Þ>îMÖqúw¨rèëÑ^ÍzÞzqú<vözysåy7^ƒÆóþ‡çúçÓô<~ÌkO©Å^‡'g$vêöX¾6¯{γíøÏÑ»×>eî~×ç=^n:óºý5áõzšÏ7vç•ôž6¼ßCÛ‹òÞ7×|žó³êþ?ë¥ù½žÖù~;§›ÝÓ››êöaò]?Gåžw­âuWMèæü†žßj¾gÌýCó ÓPÅdbÊ,Ç(I’1Ç"â¨ÅKAQJ”IQ©d°J‰,! ,°€B „Í{,ÂíÚ·iUf,¢Å”H¢(Š"ˆ£Bb‰2‘ŒË‰/: ˆ(‘-¤(&Ý[S$´`E€”EDPû/û™ô0Ù~XMÖÙr°l‘„Ù Œfc\Ø5³V †8솶Á®lñÚ5ãµLÆ»’Ìs4· 1¡Žpc†c,b.­‘pÛŒt~—_O˃£N˜ ¹sÈ»tJîóò¨Å”—PÅ”ŒfC”\VF,¢ÅF*”!)bÈ‹ 2‘%„XIb Ia%„ %$– `f½¦ toÕ¿HÉ©Šˆ´ÂÑIÆÑ¨Š$ÊDQ‹"á†ÍqÖ9s°©`T @©I³^ÄÈU`ë¾GêúgÖ™ãߎ+LŨRL¡%DQ”I”$°K¢,‰*É2†,¢É”$Ë *$ÏL±"ÀK",$ÊX³¡! `–d¸® D™`¤BX©`„¶˜ÒÉb‘%„–‚!%„– `–(#n¼ÍYa°éß§£q2•chŠ$ÈbÈE²²̆,¡˜Ê—[´GxgÎÀX($²Ô <33K`%€APPTúŸ–úmçÞÇ<{rÇÛñ=Œ^Ÿ;Ñbù®Z+ÊéÃÓÔò§¥¨äžœŽ?kέœ¾Îøò<ÿ¢ñµ9= _£ÕÝ‹ä7j³«Æõ<ºéæöyŽm>ÎØñ6zv>s_±äuÎ÷µÇsé÷{#æõûܧÛÓWÊo÷l|}è××=z>»Îå¯ wÒùGÏú:~–¼9פåãú?(Ç“é·Kó[òZ›·÷{Y¿)>³Î&Žÿ><½Øû:Ÿ=³ÝÎ_ŸÇéúãä|¿Ð~³ú_”ûCäßK”|o·â{váëyþæÓöø›ôó}>¯›§ë;2ø|þ·ÙÊ6â·í¾oØIòÞ†òñçö]y¿Ÿêý7óä³ûŽ<»öü'Ëù¿Góv¥„–C¡%XsÆš¶êÚuôjÝÒE•EDQEQ%Q‹(Iœ]z:9²âÏ ùØ”@)2 ±ÈÌYR’ÄT @(P£èþsè5Ÿ¥Ç<{ñÅÅÕÉ.zò÷%ðÞ¿ sÌ·×+|4Nœ Wµêyk—6ͲµÝÙœ;qÆÌröpμgÑë«Õñlõü\=ÃÅ׿£S‡m»²õùýÚŽl·Û9öã ˜æ¬î¤lÇ_0nÐ3ÕK—O¦ùM‚vñC;®¶.ýzÆs{sÄÇ.ÿ76˜vsInÝÁœ:tê‘»wŒ7ë.ç<ÛøK†TnhKœÅNI/w>ˆwðHt9aÙÁD–XE„• `”d°Õ»OAÙ¿^Þ“RÉ2K‹(E Å”%£F,¤I”"ÅÓ¯.L¥ç@… (–S;*¤e µ)=ßÚÔúÌsÇ¿ wé‡ÑøZY¸ýÏì_C·Á¹{yxYKîhåÔvÏ'mœ›uì韢çÕãr×»|œ+ÞâáÕZñ®™ö绞½žO3R{þKž°úï’Ú¾—£óyåêmó°—ÒùíººgÝxó/k/ª^ßÛñw)Ž¥– `– F3,E&9E‘È 1™EÅa&C ž1‹(¸Ìäa2F8a3‹ŒÊD™%ÅF+%“,IŽxÆ2Âc”\eÊ1YQ%‚YD Ì6é0éçê®í˜ìéœ Y TEDQ±`Q%Tc3†®ÿ?E—°™JQ@ÀP€PE JP¤Q*ÔRO_Éôëí1Êwã1Ë1§é¼|^SÌöÍ>o«¿7çöû;kç²÷µ'ˆö‡„»úg®¬ùkÉÕíç^&^°ð·ûZô:ÏO·ª¼~î.½FŽî¬_ sÄÞ{Xtç[|kv^7ÕøšžFz¹øýÍœõátgÇ©¥ìö+Ûéh¾£ÆÏÙÕ/™§ÙæO?)ëîy;= ñ|m¾¶é| ëË<]úsé;²ÍÇ^^ïC®¾fz>\uiõ7ò×—§èy—Ëâú¯–Övû~£/7'­Å[uoèŽIô8âüîßSòù½os³Èû—9½/è£ÍÕë¥ùË»f¦ŽíŒyvðr·&9ÊÇ’ãŽPÆe fR\̱‰2ĒŘå"c”1™EÄF3(cŽQq™HÆQŠÈ’ÅJŒA%žw¥æ›½_3ÖÔé«Ó8¨‹¨“!Љ2„Q¸¨ŠŒTIF8å%çñ½)f4X–#)eŠ©P¨,°Ë,3(©@ R…–TYE)@ « >ûw7W^rUxòÌÞ®y²·³ãëO6ZlÙÌ:}/ —_,Ës¥Ì—«NéÑ17ã†6e–¼×o£â²ÝŽZl›2ÜkÃ4jÏVêÓ³^Ó™z+V¿[n/‡—§°ñ§O6òzœRêûI§WOIæ·íN,}kÉ­çÖ†k5Ü¡†;ì.¼v Lƹ²ñ݉ªm†,¡ŒÊ2†8ç%Âd0Ç8a2‹ŒÊF3),\fPÆe#”1”c*\&PÆeXÄ™HÆe¬‰2„o™éù‡O¯å{›ÙN™“(E²¨Ålc3†,†*$Èb¥“)̆$¼~7¯äs¹K3K°¶¢*­”,¡ž”(@ @(‚Š ´(-J¶}·wét爳¿£ÊÙÏZ7jËsÒç˜ò»ïZz3›^o_G™N¬ø6'N¹õ·ùw–ûuðô¦ÿ#³“s§Ôðº%êÏ‚GoŸuê{ºtfôôðáfÎÿ'*öuyž–/ÕÌëŸ_‹;»¿Åλ¼Ýš÷=Þ> ‹ô\gT½ØpsÙ§æµ==Þ4Í÷|ìùM]3Ž6X©a I»\Ù™ÎA$,Ç"L¡Ž9ãÌâá2†8å fQq™HÂe fXÄ™b¸²Ä˜å#”\fR1™C ž+ŒÊDÇ$c)d°‹"ìòýO2»}'ØÔÜÊo¬ ¥“!ŠŒUF,¡%¦+(ÆY̤¾•éù¼í–f€ ²FÜ7 £]а("Á³^€ ª” ¢¨EieI‘J"ÕKTR}o­ãû;Æ+,N®ü_ÖZ=L9kžÿ.§“×èj.vvêxÙ{\ÒùÏS3Èž¾ƒƒn¾­KÏÕ³7f}ZyùwwføXý¹|g§½¯™êôÎé“YÆe£P“(E.,¤cl"ˆ£ÂÑŒ²\fR<Ÿ?»‡žÃ mÕ³šïŽGHÐ)`°,xfd ” ¢‹ )eAie QVÅZUedªFPúowçþƒx’’îÑ+¦IÎìÇjîÑÏlèÏvóê¦ÝütÞÑ –;k·š-Çi«?SÊÍbÇS;djË·†³Õ—IÉžTÓ–ÜŽGvƒV½yxíùiÉŸvˆæžž“ŽzFJÆg lƶpÊײHÛÏ”1™ÊÖÌkg &ÌL&ÌDHÖÎV3!Ž9à ”— ž& ±1™C”\&R1™CdLr†3(¸Ì¡Œ²&9E˜å#–+ŒÊF2£‹n®‚ðvrG©êy¾§LíÇ9¬â£Q&HÅKŠÓ”"‰2„™BL¡ŒÊKŽ9CÃäéæã°€oѽfÍ{#Z\€*À3Ã#0Q@ŠRÊJRÂQK(ª b¨¥U´ZŠ ¶&CÝúO˜ú‹œeV~Ÿ•¿N¼›Ï¥çoÙmâÙÎ{4êÅìÏ—]cèùviÓ·^祗\ïV:t×_ŸÓ£Rzæ;¼½ù½¸ù»ýíKëzžw¥¼l™MLTb°Šˆ¥’‰, $£¡ŒÊKŽ9Cçug‡¥²¬éçØc:±Í(æË¬ €ËmK@ŠPX-J ¨-(S*U¥QU l-"WêþGëìÆU‰D¢ED™ m,QEb”E,QŠ’(’ˆ£PŠ1QE¨‹9E’Ä’Â,ÊJ1– h’ÃaIpÙ‹1ËD"b%Î5FK„ÝÎ\PLP˜ú‹æI‰gw " KŒÊF(c2Æ\VD–,ô8=ó1¶=KÏôzç9–6IÆe²"Â(“(° D”I”$°Æe%Ãõ;‰Ãi`²«§—dl׿RæÐ[˜b6Ò”°²”@¥ [AjÕ¥TUl-B«³ìþ#îZËãJtÇ:Ê ¬FH("d@FíqŠæk^øóƉ·(Ñ:´OW’9]=G›7aZÞ†˜åwð’lë® èkËÙо[·‹Rusú¹yXôé­3ÛÓ/úf_3~ƒ]x/{‚Î ô¾  n'¡Õ‹åiú Ù¿3«Ýðºg|ôzq¯7›×âN[c:ð;}EjðýîHææèëÔò¼ßKÎÜ÷Ûµx‹³Ÿð=/;nç»ÓætbùÞo¡çê}——£7é7ø:ãÐèùî¥îëùœOwã½/7P-˜ÑŒË˜å%Æe‰&R1õ<¿Z¼\õoËÚïâô:å3ÆÉ2„•a,QÀŠ1™cÌ¡†ŽŽY~}/¥‚Ø\ñß.½·JírŠ.A@`ÛfZE UB­… Ê†B­Š¢­Š¢Ð«E&Ϻø?¼0•dô8:sy½?/¦9†çnz°çzvq¥ÙËÑÍ©‡£ço¯G—VŒ;wy{ëœÛ¹×‹ƒÓœxK׳f,§Iènòrå}9êëóåzϕӯZÎÜüô¾Ÿ›ÓÊaìyy¦[¸KÝ—ž2µ1èÑ ˜â6ÍC®rHÛÑÂ7sd®«ÆŒôäÔëÑ¥.ÍllÛÏœ:ùq’ãŽk0Ç8ºæÈjlÄ×êyÒ5³Æµã·^;aªl‘®g lñ\Ã\ÙŒaŽÈk™ÅÂgŒbÊÌ¡Œ±d²1™b²Y¬‰ëùÍ|÷W'noµßÅÝ× r–E„Ya F+¥€ÅD”Lr†3,eÇ‹·‚_Ç`æÇ¦9º1«Ph ‹»,3ÔRвe)*¤«K2 iTU±Y­ŠÈ•2©”É 1ûß„û‚ʲK×SÐË7ÎzšN,}Ž™sÜ׳‡;åõvi?·›¾¼ÜîýN|=)›ÂîÙ/?­Ãf¥›‰U IDX”E¨Š$Êa@ eE&2‰(’ŒfCÌ¥˜©q™D“(¸Ì¡Ž9ÈÁF(¸L±&9âc,1Ç‚|ì>œÄúL~~žö>G³•OO>ËÛ9iÑ4Óf8Œ°Êš°ê±Å‡¥WÊÃÚ§ƒ‡Ñd|ÆW‘ò}¦gÂϽÈüýúø÷£á/ÜÃâoÚÃã/ØÃä_]“ËêGËߦ5—ÑCçò÷G‡}©FŸu_o7ÒC£v¼ŽoÅ÷GfïX E‚ – aBQ– F8ç‰1Ê/?Ëý7ÌóÒV,Q+,r6‰B* xìר“£›}ÜY•’­2”U²ÙE[JQY ´ª–¨ÊZµKTU'£ç÷C†xÄPcå§©®¾Ž|þG»bý0ù¬¾Œ|íú|õúà=ñá_px·ÙG‘—ª<̽çåÜ92é2Ú1´EDTED”E²Ìáƒ1„Ìa3Û!­²Û!„Ø]ShÕ6ÃSl5MÐÕ6Sj4¶ÃTÝ W`ø>î駱÷s}¿ܳ¿<3Öd°€€€‹±RÀ",$°‹›úžçºLZBÅVxg…èéæ°(FÝ[+°t”Yl¥«K2EZYRÙKBÕ-–­RÕ¥dŠ£)³"uòï>Ÿ šòX¯+<5=KONn*E%(Š¢,"ˆ¢(Š"ØÅ³ÜLÃCYš™ŒY¥ÁsMlêête·u47 -úŒY«chÁœ1d0g fcpÅÆg+CœŒfc ˜Á˜ÖÙ m×6 MƒSd5̓[f'ç|]¼s} ‰épúbeíø¾ÝvçÖbÂJ!T*T$¢@€ò¼oÃ弆h( ðÙX”°1<^—›`P…”@3Â×}^¸–…Z¥FRÒ¨ªŠ£)jÕFS!”µj¥Qr”µI³ ¬Õ·VA^p³ÚëãïÍÔê™s3µ­»drݲµºs—»;9²½QÉ7äs:r^G]ŽGVG¯C·’ÈíÃ7L݉¯_N«uiTÙ²4ÍøÆ©·]oÂ"mÂòôJÅ%:æ9·f¬ŒšáW6U·c;¬mšá·-r6sæ³V°f5¶ lé­².¶Á­°kl›a­±æÑ©´jm[†–᥸i›Æ‰¼ho††øho0êæ?5ãëäk§¥zžO±ã¦ïoÆö«²Ë¬â°J$°KÈ,$£§…ãú~g-Ñ›(³l¸çs— õ<ŸoÊJ`ôrÃglJ¶L¥-–•QTU¦R–Ì…RÙª[2EZe2-”¶dJY¯,r ñ°Ù…ž¿£æúXÖÜ1™oš’ôM8ÆÝ56µÈè¼ÔÇn 3ÄŠƒ,°C<d¦½„“4¸6 M£ w M´ÒÜ4·£KpÒÝMpÒÜ4·-ÃKr4· M£ShÖØ5¶ w1‚àdÕ‰½É¬ïyº]âà¾óçñ>†üÞôÏ–Å>®|ž³ìª¾áðš—ôçzÒqüÓYún¿Í!ú>¿Ï!ú¿ƒ‡Ükø±ö|„_­×ò˜ŸU‡ËãO¯æ±¯£âò!p™Ù^χîøo³ãû:v.PÂ,À¨… d¢c”&S#æ<þÎ>;¢Q Ý[å]X®XíÒlkAóÿIóp•©†Ý;»d««fH(«TÈU‘2”µle)j–ÊZ£)ÊZ²ÃéØÜ(¯' º¬õ=O3×å­m–56ÓSj57 M´Óv¥Õv w`ÖÙ SÚçÆº¯ ó5ž½ñ1=Ç…+Þ¾>óøFù¸ŸJù|kêŸ%‰õóã¥}~(>£š§ÐáàÛ=~fG~|;±ã‡Vð߆¼MX˾hÀêqöpàz3ÎÀôqóq_K3ÔÃˇ¥Œz8ùðïyñ} x$wNÛz§(éœèÝ4˜ã)`bÈcl²ÖéV{_?ô_8z>Ç‘ìk="È°Š À„°K°™K#Í»O”]Ò°Ù­nwAµ¤}?Ì}_ËFP±{ºy{;cj2”Y’*Ò¨ªZ¢ÌŠZ¥KfB©jŒ¥.XÚÈ¥–Ež­¸,µæóõqYÕéøùæúØy¶^ü9jt9¥t¹GTäXó+~:I³dà6Ý輸¹yØž£É‡­<ŒO]ã`{Sć·ùMlÇÓüÇÔüÎo0ÞAEY2‡_wèõÄVਫVÌ…™ £,rKL¥FAj–•’dTȵFR–QîoæéÁqµÃÃèpÙtaá/½<ÜñiëãåSÒÆ˜óS{FFÉŠ¬´Æl¦™ÑNgM9]tã½u8ï`ã½t㽔⽃Žö7h⽃ŽõŽWU9/U9]c’õWPåuS‘Ö9]=1æ½ Ç“}=Ñ‘ÀíÀåuŽGXãuŽI×G\9'\9g^5Ë:áË:¡Í:¡Í:¡Í:KÊé‡4ê‡3¦Ó¤s:!Ï:aÎè‘ÎÞ4MãO7¡È¼5q­˜ås羃À=_WËõw«(’ˆ°‹¢P”ˆ ¥’ŒUGG"üœ8ìrݧl«Ï´Ãn:ŒÔ}gÍý'Ïæùãy w©äúÝ0•Ò(–ËV¨ª[-*¥)lÈU-–™L…Q”ÈPÊÊž·_v-rpz>uœ¾O³Ä¼×¦2è§5é©ÍziÍ{²8/FUÌêê2ûG-èʹoVÈâvô[ÔeõøŽ[íØñgÔa›ó“ê|K8ŸEÊxϤÎ_˜¿_¢>g?­ð+¯O¯§å=:ñüï«òlò{rö«ÆÃ¿œ›z&o.XÙçz|çG.<ñŸGuìópóËì]<1Õáwé鉧; s:aÍ:G+ª³¬qÎÁÅ;aÄíW ï{¼yîñçÏDyÏFsÑsÑkÒkÑkÒGšô¡ç=çùßAà/“qÏžóÇ({~»á'¯éùÞ–ó¶Q  EK&P’¡ÁèùQó)yt •Ù+^T¸e ÚÄ}g‹íyX¾(é€?_Çö:f¬éŠQ–9U2Kb¨¡iV©l¥²–¨ÊS$É-”¶QU}GÌõ2ÂÁ£Ìõ|«0Ó¿*öuõùüµÕÉîa›ãtú9×'…ïñ\úõe.vg<ÔìêòöæïèñöW¥»Æ‘ëcäæ{>4Õ©ïø“ÔËʆNŠšÛUŽ®Ï¾Øä½V¹/U9tã½c‘Ö9/Z9/U9c•Ô9oM^WPåuWN™³XkÖtN]gsÍÄôÞV)ëÏöžôç5ŸO>[Yõ¸ü~1õúþOêµüÈú-~öðñ¢ûyǯ‘‰ëcäÉ}L<ÜOC›D—Úw™õüOgÈO_Òó½ça,¢(Š"ˆXŠ"ˆ²[ãû> ¾å°@·d–êÙ™«)™¥°¿Uç÷ñó|ì³®jQ,@o±ã{=3±”ë‰hU-”¥²Ù‘*Ò†VRÙKe.R–Ì‘T¶d,ÈPëõüok7Uƒ_“ëù6N>†®»/=ßNW]N<º©Ëz©Êë·¦œ·¦œÎšs^Šs^ÏwÓCxÓvÓMÚ5]˜˜˜°:\xï0õ°ó)߇ êrâuÎ\ÉʼnÛ^Ð݆¬eÛŽ¬M®xt¸áÚàÄïÇÏÀôpó±=<üW¾pHíœPîÇ„vN2õNTtNqºjãŠ[d0›$`ÌkÛ(ݧue–;Nÿ/ÓólõýCyÉbB‘d©`P”%d ±BQ•úO——ɺX¤¨Tä’íÓŽÃõâVÑõý¹ëåæxuÀ¯cÇõ÷ž¬vcÛÚ&S$U¥ZP´-S!Te)lȶTe)lÈ©ªn÷<7@1ò="²õ<òzï5•óéß.«È‡²ñaíãâÓÖÇ͆”èÇ]2c Øbmš±7MPÙ0ÄÎc+;¨m¼ò:râÄïËÌÄõqò±=l|œcÄñ¡ìÏ'³xîÖê1”“:jn§;£#‘×NGe8¯e8Ý”âw'hãvŽ+Ú8¯a8݃‘Ö9tä½V^;Ö9påuÕÔ9C•ÕYÖ9'PåPæPæPåtŽiÒ9]#•ÓyÓyÒ9§Hæ#Ÿ‘Ìé‹Ï:q47CS8`°¾o&n¬ò’ÎÎ>ø×«)^¿on³e@¤X)B‘DRÀ Šû_‡Æ´ n€”™ìÕ-×¼ahÔØ>£,2篜äô<þ˜ ,‡ŸÛ§·¯v¿G,JJ´ÊQBжRÙKfBÊ–Ì©T¶X¥- YÀ÷³uåz¾eaâ{žMœ®¯&]tä½yyuÔä½v¹/]9/VG%ë·ªœ·ªœ—ª§-ê§+­®ªrÞªrº‡3¨s:iÍz)ÌéîŠsº‡@çtXætS™Ò9ÝÐ9ÝÐ9Ý™Ð9ÛËÎßLÝTÛ‰®lÄÁq,ÆY”Ãd׺iÅwÎxoš17ã§~:a» r3šÆXã,XÂXc2†2‰,–J‰,Yéùžùâìãï=^¾^­g)`• DQ(J@DQ°@¦? ÷ `1µb6á–2ìÕ6®áÜ>“f¼ùßËö|n™ K\»«éuíÃÓÇ ”¥ ´Y•J¨³!UYJ”¥³"e)lÈYÊRÙbÐ{ž·›ˆo¥ÃZ¼ßKŽÍ}[¾ƒ7åpöºOùÜ·eÒh»ìh»Æ‹¾š.᩺š.êinn᥼i»Q©¸jn¦–êh»†–ñ¢î¦‹¸in†¦Ñ­²3†*$±"ÀAK6X1°Æ\IŽX¬ÆÂceLlLe„ÆÂKŠÈ„–1.6Dˆ¬l$²$°D$°D•K9úÑ£gGÍË—«Ãê×oF½ºÂQ, TJ(±ŠNˆû?‹Æƒx#=˜d²éÚK† ½~Ÿ,o=px_Eó½2f¼ªÆßWk)eiU-™ 2d,¥³ ´P¶d*”±V“Ùñýih†½ƒÎåö¹¬ÕìùÙfîßÅMÍTä½—S’õS•Ô9¯@ÐÜ5]°×vC œ"ˆ¢,K!¢A Y,DA,! D$Bcq$¸ÔŒF(%¦¹¾¯,íÎ<ÙêSÉÇØG=ŒWÇž¾•­ŒyoOƒ.­F9é×/f^f±|›œ§ÐcáS×׿ŽÝZ-5m§.¾ì2z”òžµ<—¯Oûñï¯O"úÔò2õ­žMõ‡–õiåßRžV^<¼½!çeßN=ûìc• D)( @*ÄÏãÂù£ùÌme΀·U—f7¥y÷ç…ÎÍlõ4º‰ë¥áÚ|×Ó|æóÎ5¢îó}?O-3)©QV¨*[(ª(\¥RÙKe-”¶RÙ”2”tóSÜç»qtŠX(J¤Œ²5¶ÓKxÐ݉®eZð7¹q^Ëçë=[âà{ïœÖ}Eù˜hΘlÈåǺמôr<ç§‘æ_NÇ›},3/JžmôÇžôiçåè »©Å{,qeÙN;×NKÔ9¯E9Ý0Ñwr4Ý£ShÔØ0f0g fCPÅa&P’‰,"BİBȱI#)!`X…DµAPY›”‚¥U‚ %Œ¶jÚ|o™ÝÃÏ¢’„ݯzëÇ,»9áÑ6l5µ¥öÆ5—‹íyZÏ7Gé>WêûcF;0íÎÙJQfBÊ2”YJR¥-”¶RÙKe-–-”¶RÊ0ÓÕ ^–'šôG]ôá˺œYvS.¼ŽLº©Ë—E9òßM}4]ÔÓw#UÚ5¶ .TÂåL.TÂä1´K`QJ…@!P `€ˆ!I!qBÄ *ÄK D,²EÊDYdEAd†LE¸Š€‚± °”Ɇ&ÖqÖáÖ¾•ò5Ǹùýrý#åµËõŽÀû<>3~Ç_ÈÃêõüÅ¢ÕáI}¬|qu猡·.ÿ^kæ4õë16zùíÎR{¶9ï/;Ðã¯0Q`Poúÿû¸Ç šûó‹Q”¢…²•)l¢Ì‰”£)E ’–Ê\±±l£)K`É(ZFTÆÐªKB ÊQe ” ” ¨Š`¨* ‚Ä*B¤*"É !l‘*J±‰c)!d…c,Êc ˜ ³dÇÇWÉ%ú8=í^2_W_œŽÝ|©wã¦ZR횆֚mišù¤›Z©²kÈZÄdÆ€€  R„!Ñï|ÎÌëíù|Ÿkù|>Ãçõ8¡e1†í=p,êþSéºgÐÕ»W£– P ·d™ (²–ÊPT¦Lr-Æ–Á•ƒ+Œ’–ãL®4ÉW\FH2A“›dƒ+ˆÉ-ÄdÆ™11°±1†Æ¸msë;v•õÞ˜ú'Ëé­|f“îgÁi—ô)ùΨý#Î0—ô]?Ÿ—îtüb>·GÌ£è5øeõôùÒ;uóIwã©&±±®Z†Æ±±¬»&3c ˜—)B AR‚A@!PQ(€ X¤ªbB‚÷ùö_­ô>#ÜÆº]é|Ñe³#æùû8úà@ýÎûšžîÚ}×WÇÕú­_5#è5x‰}m>z;5h’íšÆl›XØÖ65Œæ#&" °%°(K ‚À ‚€ P‚YLµ÷èšæ¶\n%ôÞk7ê›r”ñ|ïWÊé°”A(¿‘êWÓiéÑêã«ð¢RÜi•Æ–ÈdÎêÄèscŽ Ò¾Ní<,#èoÍk_©|ž¸ûÆa/Úãñr>Ë[¯å’ý.:{_Š—Õ×çÝ\ò7a‚\˜#&#&#&#&É€É!“PT APR(PA@…A`PP` J¬e‘¬ Ó‘Èì/&}æ½(Í›žøùý>}N&Í{È PŽ{¹a‘çøÞç‡Ó$Y`T(îáÌû~_’qôÚ¾zî*=||ª¾Ž(ëðÁ.R" © V#)APTAPTX,° AHPP™STÁ¶®—EŽgVgº¯ ¾œväpÞØrÞˆi»4—,þ³=|†WŠüÞß{ò·uë^/–û?•Þ0ËšïñÔ6LLiíµíãÛC§IËÍèj¹ó§W?Lb+éQÏW,)§ÀúyˆÔ¨*2¸T¬iP¶€i‹*`ÛeÑz-s:²Ž7nGïÉ|ëèÓϾ€á½©xïXåËpÕ–XŒµã[¯6'Sˆ®wÎvÎ1Õ9¢tM™E0l¦©´iÐux{ùïÓy8Ëì¼Ù]1Eˆ*¥…( …‘DeLÕÖÙ‘¥ºšéÏw-ðÕvmƒ an(Êë†Û¦V÷<:\£ªsC¦sމ nš†É€ÎbJ€@ @IC×àÐåŒYN»°rcR™,e”k›²®yÕ”¼nÚpÞØreÔ9¯D5]ƒ…ËXØÕn\äÞç‡LæD7´ ÓlÇ Ta‘DRÉI&XŒ°È»µ뺾_é¼½ô·iγŠ`Ìy }¼]¼µãË:b¥@¢T,  ,*P´Å•Lwe56—Sm56We5]ƒ]ÈE…A“lk†Ö¨niš摵¨›Æså!*"ˆÀ[q©R…«‹<-ö9ÝY/¶œ7¸q^ÁËz¡¢î²Ê*ã ­PÜч1:'8èšé¨lkÌE@JE„Q&P‹±†lv&‚¬Q‚’P õ™°Ì¾ï…–wö¸óuù{èÎêŒÔ¾:½>x”ËŸ~³ÃÇ)× bÈbÌ`Î6M´Õ:,s:ò^+ß™æ½LÏ#<ñBʬFl!±®Z†Ö¨nš†Ù¬l˜6)‚•(J…!@Al¡F,ò]MÔçudqÞ±ÈìG-é/=Þ5]°ÂØ\°¡¹¦ï0èœã}çæšl˜ æ"ÀÀ…"ˆ¢(×3Á3û/ŠéŽŽ ºid6…€&9“M¸›¯OÖ|_§ÏEŽÜ|Ý´6-ñ‡£Î"ÜrÅÿGˆógFŠÕrÖ›€=>ž¯_‘¡}Ü|YÏ'¦9 ˆj± n‚R9<ã¦q(€ E ¡2 @ ¤ € ‹  ,A(ZÒ3Ä\1Q@@@@(€ H((“ €…¸†­¡ÕE›Ã¡æÕvô‹žò9|Â1ÖzEà ÃXÔ%È@Ñ`(Iˆ·1(_ÿÄ2 !0123@P"#A`p4$BCDÿÚÿò/v3r3q…§ö¶FFFFFfãç›ò™NT§.S™)Ìæ¼æ©Í9¨ss#9pœ¨Lü&ô&l-?³o¥–Ye™Z†Fâ›Ï7ä92œ™ŽTÇ.c™)Íæ¼æ¸æœÔ9­9‘œÈŽ\'.“Ȁ߄݈܌ɿÙ6Ye–Ye–YfFFFFFFFFâ›®7ä9œ™NT§.S™!Ìæ¼æ¸æ©Í9¨sZs#9q¨NL'"~v#6ŸÙVYe–YfFFFFFFFFFFfã眉L§*S—)Ë”æHssTæÄ9Œ9qœ¨ŽL'"~z#q†M-?²l²Ë,ÈÈÈÌÌÌÌÌÈÉ É ÍÅ7\oHoHr%9™NT§.C˜ó˜ã˜§0æ4æ0åÆrâ9Pœ˜MøèØÌØdÒÐî;¿°¬²Ë-L”ÉLÔÍMÅ3SpÜ7 Ã43C6™4É¥´´23Sqæì†ü§"c“1Ê”å¼å¸æ´9Œ9qœ¨ŽL'"z#r36‡wöE–¦Jd¦jfã57ÜSuMÓtÝ7Üi›LÚfÓq ÃuMç›ò‰NL§&S•!ËyËS˜rÚrã9Q˜ND&ô&äfl-?²¬²ÔÉL”ÍÆn7n©¼¦é¼n¡ºÓq†q™°Üiºo8ßäJr%93©N\‡-ç1NaÌiÌŒåÄr¡90ˆ èMÈŒØZeZ–¦Jfã7Ž7\n¸ÝSuMå7Óq Æ™°Í†êÆûŽL‡&C—)Ë”æJsd9Ò×å9§9§9‡6#™ Ë„å@r`©‹ùMv(¢Š(¢Š(¯ã6Z–¥©’™)’™)𙩗ò„ñ×ú[¸î;¾ÑÊ?µÙÕÝ…íþÿØÿ¿D^ª¾ÿd'büUûŠ(¢ºQJwçyÞwçyßÕ>ÉD£ñ?šSJiHyiR”¥)ߨ¯ÿd(¢Š(£Š1(¢Š(¢Š(¢Š(¢Š(¢¿¯ìë­WJñ(®•ÒŠ(¢Šú[,²Ë,²Ë,²ËéÅ“ùQEQEQEQF%QEQEQEWʾ–Ye–Ye–Ye–Ye–_Kú´þˆ¢Š(¢Š(¢Š1111111111111111111(¢Š(¢Š(¢¾¥?«k¥QEWÒ'öR‰ã'öR‰ý°¢l§Ü#DÓ¹N)ÅCŠqN)Å8ªqTã)ÆqÆqÆqÆyÇyÇyÇyÇØØy²óeæÓͧ›o6Ü`ã1S1S(¢”¢Š(¢¿œ'ÛÅ ˆÔoÑÒ…4Ŧ-1i‹L`Ó˜4Á¦ 0i¶Óm¦ÛM¶›hm¡¶†Úhm¡¶†Ù¶m›fÙ¶m˜˜˜˜”QEQ_ÇSéáŽ×ËùÍô²þÕ>™<âm7ú~ }4^¯ÛêÑ­Æ¢?ñœÅDkrUJ_€ˆ«Ö¨óþ¿>š_Ö%+A¨˜~(Þç9UUÛhäi¶™àˆ5­ªÈÆ·`mµF"tr\p!†R$q(ØÑWmŠ2<•"F³'$,Qªí–Œ%ÙC`HÜç,=Ì^,44{qVzµ1µ#sÇFæ‰ ÔtjÑÍVŽj·ø;¾ }4^¿¬ÿæ3ÐïC}RzåèËôä_Á¿“ÅkSÚ-ê’^%нÊßTlr=žpúÙÞDÇ#£ó‰ŽGÅëG:ãõCëÞg›^G脃Ծlõ»ó½ÖÔ‰îEŽETÞÜÞ©¿ƒ»à§ÓEêO/¶µ;úZ¡Þ½-L–íorC%U [Þm䙡’Í"¢9Z5ÊÕG**=Èå™êˆõDkÕ£\­W¿1—urÜZdªÔ|ªñÏÉOÇ|{óþï·Ôž_Á{ÿ»íÙêo§ú >s„ûfù³ÓýŸ9Â}²yÇéêšWªI‘ôDµ’'ÇÙÙ‘YÑ­W+˜æ.ÓðìRª¬35ǹ™Î›ZˆØ¢ÒÍ*qæÜàê*=$ÒµÚ=C[÷©ó—íЃÑÕ8înq2=èž“«*i#|pLŒѨÙã6´èÈ%b2H±èÅÅó#fHœ›P9»)2=šŸA¢K«¥4njîÔëšED’ÐÍ.G&+ÿbI¬÷4R"ª/™7úzBœÂoõ2FÁ<.™iÑê5±½Ë ô1Qº‡Æ÷LŠ™iUU:jZé ЪmÄ×¶Aï1²¦¢5n ®Ç^÷µÚ^ýN½]¼ü×Iÿ­d²$ú?õöûùË÷GË®Åý(ïìäòÔïÎh¶¢=íU½$3}£ÞÕèÙ$a“²Y¦rY³êdÜkœÕtó8{ÞñZ¯{Þ¬šXÇj&sž÷=ÌžV'/Q÷ÉóWîtÞèæ¯Üé}Ï×¢ óWîtžŽ´å?¯Dæ»Ëî4~žºCw/nti[´è•Åq¶ö>d‘iÜö¿Nö¤m•XÖ+Äy³&|YEE8² wCO£õ-騛rF¬W¤»QÄéÒʈÈ_ í4­N$£˜æ)/’ #¥•ÈH¯v–f¤ "¹Þ¦é¥rlËšé'DèÝ,ÎD‚Us •äò­xsÖšYuP`èÑÛ1±Ò/zŽ$Y4ÓFƒ"|Šý,ÌHôòÊœyw5šoÇEDð¬Oz/8%”“K4mK4ˆºi‘ïÑêƒé«Ò~Ã$¤°KM ’j´ëÚ­K³.äšIâi2J¯ÐêÈ´³ÌœY÷8Z‘ÍV¬Q>WËPÃ¥nC#|àjj=¢F®ƒT‡ R7I;ží¥«ÀÕ"E§šbM4ñº],ñ7á ½Ošï/¸ÑùuÓzQÙù~xÁèKŽW5Êé=zŸ(}¨»ãÓûq{ïUÜ´Í­zI7¸~¥/âõË/ÿ§T×e¤ò•®kÝþ®“Î5]ÄïcQéê¤^šu¸Qš”lÈ÷3ÿhÙ7âÿjyÎîvæm·^âw+”•$tÒ冈Kß·+ûÓ[¬ÏsIìh}æ½Ü¶úSˆh÷RÜÖ98úëÃ[¹ü¹©ÜÜ—ýøÿD/sôón,/ÜÜj«…ó5»›zOôäGpOøÿöui/'S#›2cÉs—lÿŒõéÞ÷«8òIZ…ª‘ÙÉîɽÈÆ{_ñßík§—Mþ²­'pª÷é[‘®ž]ý*¢i5»ªjÓZ‘ü4ówDæ¿Ëá'Óh»6ªZ™¾…{•ÎA\å-iPET-Å­äã!’D„ŽÉÂ9Í÷¸G½UQ$‘ ¡ds“qPES5¶9¹:höÛ#ÐsÞî–¨,²ª5ïiÕ$³»;påW+^öŽ{Ü»ó kѲHÃv[Ü“${ÑW½Ryš9ÎrïKL’F Uh²I”OMɦfÈŠæ¯*zd’FI,’ ÔLÁQ_4²7vL,‘’FWÆÖë'E/Q]¨ž1²HÅ’y¥A-Ú‰œ×M+ÅšUsµS¹£$’5lÒ±cžh‡Í+ÝÊÔu|²H2ic÷±^ç=ɨ­]LêrµsKº½Jsžèç–!ÓÌçK¨šTøiæîˆ'ÍÜè¼ú¢*‹ Ñ:mµÌ~ÛJ¨¬z/Q±=Ã⑃a‘ÈèÞÎcž©ÕÛR*¾)ŽíM™4¢µHXÔeÁ*Ë&غxÕï|(¯j5Æ¢+7µxÓRFõjF÷7‹0ö9‹28d2=ŒFA#Ññ>?^-}«|×£Dù¯û«¬nF»6ªH¶á‹QÊ܇/u÷ª®IæÌîÞˆä\..î#vDy’&Qy&¥ÎC¿¤_”,ŠÉ¤V$s«¤ÁY˜ÖlŸs£õöÑïAöˆ÷ éá%‘¥Aò¿${ÐY¤T¹$j6Õ Ù)ö‹4Š+œä芨nÉJ÷* ,¨*«”ÍêŠ÷*$²¡Ç”òf¹¹PW½Q¯s½í!V"jdk›Ó»ÂV=ü‚ôhŸ1GýΓÜì`úèÈØèö£kQŽp¨­]¹F¹Uc‘£XçcÛ×jR5á{F±Î)Ù”ÅÈ»ˆÇªº7°HeQZ­RòtñbæÄ÷#šæ©2|ñb®…˜æ.ÄÕÑ‘Hôš&£8òÔfíB"# ‘é±.Mî]3Q^æÔ‹§¸;ú#UËø¢Í¦~QÆù !’14³9 ‰Ë6®QU`Ò½];Ÿ3ôò±V FG+'šÒFhå{v%Ü~ŽV·ìØ/F‰ó_ç÷_w«V•&ó ôL™±ª›j¨¨ç"úE~ï{Kɤ~¹VDXÕã\ªôô±\±ÊåAk/Ïy¾y[sr6t\H=ÝNH½û¥ÂCîêrÉÎVé—Ò«ùj=båÇòE_Í?ÚÔÞ乬_úk¿û2û©k£[³HÏÉ–®¥ä6†dšy·M#XÈÓ*$ØËÈkB»b'$‘Âüõš¿~dtÑÇ”rÛÏßì˜(ƒDùŠ?Ïî4Þïc½:¶G4G½ ׎™ÎYd¶é%sWqæûú¤Ò!»%î>÷{ò ÷*+ܨÇ¿óÞ’’I:G¿®ì”×=£¤{ºîÉY»ßI,UÊ5Îhédr$²¡“²r«•¯{–GóVoµUrµÏhç9Ê2V6ô7æ-mÒÊäl’0r«—vZè’ʉ“­ÒHñä誮TsÚ*¹Ë¹%}›|”A‚|Åç÷{¿Ï›é`Ÿ1GyýÄ^â>O@ƒ>j‹ç÷ õ§—Û_ðþb ù®ò_?¸O6ùuÙf*Ø«²˜ô¥éNëN^˜¸¹:Vb±FŽG&.ZÅTV=Ep¬{M¹%1ubêÚ‘J[X¥A¬{‡1ìéUΞ*4|cØlJ©ÞŠ$*:7µxòÓb{“/HÙ›¦Jd(õÜÅfïI!Ä],—ÃpšG*:#×Fêò<ΚEÊM6-Ú”loÝÔ£›#tŒÁð#$ŸNØÓì?ù ó_å÷LôõîÛý<¤v(ÕÍùÓòkVòHO)^ìJLóýI½Fâbï5r"ªþ´ïT4ꤎUwÿ ?š=VFù1ÎtY“ßH½¦#›JôËò· ûÛêÔd1U4×ú*½úP÷*i•i-rbþ´‹MëÕùéÿØÕ*ç¦ö¿v¯rȉ,q¹¯™ëÂæ²[uËî5qv)$Ž÷&OÑMDȌΗYî¶ÝJF²Íq§Ø;Ú3æÉéû¨½¾­—»(‡I’"ªæúÚLƒdTU_ÉÒ+‘erÌ›Ô$®AÒ«‘'zz¹w\#•£¤W"=ÔŽsLÝ“]ù+¿Ë"{Ñæ‹4Š6I9îx’ȆOEYdTè’½ ÕrÒ=ÍUzZÕ­#Þ‡y“—­¯^õ<—öéj|¼é_bÿlhß›/—ÝAívš™*ÅkSµÞ;úÓŠq‹Ú–º+Tª˜­"&>fÜ„,Éef.DüZŠãfQ­s…ŠDér{ßF#[$ÌUd-“o¦7§tOiÇHž£cs™Æpæ9Ž™ŠÈí|aÅQìV;øžØÁ¿6o/ºÓ{=aj)#P×.Ùdù)bD¦»6£Ú9¶ëD-u䋿žo~îŽ]ƪdêc½y&M½ÙWÁíÇyånKäN®ÊD¼—y¾¨óÀ…iøþ¢"1¼‡ª;ò'kS¤nFÃ%¤–·’¤ó®ο¦žz¯4_ú™"EŽrj=ÏàSzF ù³x(WØé=ž¨ª‚½ËÑ•ec„‘¹+^­7šo–Ñ&Sy×¼/z‰3Yœn;&~N•Ö½g¡¸ûÜxsznÈ*ª®ãéä;Ås•Qß–ã0èz:ÄUAUW°Š¨Dæ5d~kûuTT+HüÜR×…¶üÕsœ×1É•‹úíGF ù³øMŠúý³ÛX|JÒú²<Åî^ÂÙJ½XÌ–FFѵF×)·!ƒÅk͹ ¹ŽDÇ8V¹½"mºhé[ÜŠ×7£Xç FØ^©¶¨ù-È•âÀä%G±ªÉç÷"kvÕ¸ôÓ#I•Uxí8ÿ“ A‰Ã"ÉîÓ±DÓdž0î&›ózb楹tðÔzfªË¦b5O‚M«ÛÉ‘±Úm+÷mÁ"#b‚ÇOŽ77ëÏP!Ÿ6?½>ÃCíõ¨ªæ5Q|ÓÍÍk‘jûM¶¹{Ò\žç`˜¦RH¨°Õ*µÍÍ*n‘/å3–áô3Ý™ê„*«Õʈ?<ÿw½Ù1JKÀ×::ûÖ}²Jc\‘åmšïRGß"²5fOw¿?¹“Ìé‘«RÓ$zÈÖ½a‰›´ÏÑ‘›MY$càõlBy²HQRv5ï’-¶KÚ*2Y] ƒ%jC©¢è•dQ&ØÝL O¬o«SêB1>lþ~ ;¦?_ ôõETGWGImÝ´ÝCw½%jõG9Íp’¨³-¹rV½Zo-$®"»¢* çd5Îi}îr¸E{ Ç›µs•ïAdyæ"ª ÷;®nÐW*õÉçzëÖÔòð«²×9¤’9þ#\æŽsŸõÌõê}hF'Í›Õà"wby}~ƒÀljáÑ+M¯ÃµßÖ—§xÖªºFcÕ‰nTGÍ×8r9¦ÜŠŒj«å®Ì‚#‘ò]¶'96^qÜ$oW,¨š›iæØÕ\XøÑ§H%?„Åîj=hF'Í—Õày&]1úý««R×m´ä¥?ù¢¾“Ú‰ËÜ5­A®Í¸·;vH¤ž¡?ÓòkýoZ’g*u‰)#[‘ªìÓÒ—¶ü‰}SeÑ<ÿ+zT³ú›ùB‰Mrþ¶I›æ¤*•V9¿¥ Èr·?üÿ‡ÜÔ{#æÉêð?eA>ÃCîv7]ú7I'⊭7PÝ[ÝAd´NåÝì”G¹£¤r‰3‡9\$Žêç&)hn¼· ÷¨Žr ª¢9ÉÕÈ-¯Fâ>F«zwøŸÂ`÷'÷F'ÍŸ€+ì4^÷óÍ7®_[F óÉÞ~ ã"þ¿IïÿDUW±ñ¨Ö«$oÝo¥Ò¹°ô|q£E´ñôÞ©=m'Ìw’ùøãÌV•õúoy~Ÿ¿ÇÙuWz¥-¡e§^ó¿¥*v)jŠE-#\©ƒ•£#t„‘IŔٓ94Ò1²A¸Í™7£Z|)$[nŽ}o­të·<]£Í] o‹§S‹¦cvc‡Q®Úé¤VïëÖ3gM up$Ob[æt&’8¤]K"ÂY±Óé‡P©.›þA©µö´zôE‹ÆÓy¿ÖÁ‚|ÇúWÁVû}Õì#PÛîð{ÊStTru_$EQTF=L\ŠøÿŽj¢m8ÙpÖ9ÊÖâԆ਩Z­ç²ÃerÚŒýÒ¯góH›k’1­]¦dý”étJ©„5›’ž÷£Ô¤“dÉêì"k³—$µbåÓNïÊEHØýÌÒ­Œs4æj9#Ô¹Qšg"Hƹ¤ŒYZéXÙ#Z¯bM¡íÜÖ*+ôJ»z—ç!¦Ô1"~©‘Ó4žxßú†9Ó¿O;Hß¶ùåÓÌÖk#TÔϺæ®.ÔêdƒPèIµ|qëi‘j Í®ÜdÚ§ÌÎSödÔ¾FxÚo'z˜0O™/£ÀBú;ìë^Ä~%rþNj¢#\¸%%¢~«ýB® $z¢ÄZ*t“ۇɈ¨–¸Ëéu¬}™Æ+Ws&䯫QQ³zú‹ñsýÉÕFÛ¶ïPŠ©$ÅQѱ›‘¸Y›nïwG¿&1Ø+–Ü÷æ1øî7ÕÒ9ËÈp²9ZoÈG.*÷dâ´Y_››$2u¬²/H$HÕÏrôZÒZv¹OÃétþ…ó`Ï›7§Àh¨X‹e}zy¯aDOß4ǤH·+­R^å•oxUµG ²¨çd"ª®èE©‚*¡¸ó' ®rˆª{Är¢½ÖµàUAÏW‡Ùýãógòð/¢ ˆ_†ŸKûx/b—¥(ÆdŠŠŠ±þ)æï4c”s\Ñ#z›o6ŸüŽ?õÈÆ|ÝG—€‰Ó"ÅO >•¾Ž­DsU­F®,Luú¨ˆ9÷Ñ<òKj&VŠD£œ¹Zíµ9„ö¯ð~BݧxŒrŠŠŸÈSýR1Ÿ7Qà¢ôTñSécöºÄ‹r¯~âRËߺ†ïctÍouÂ9Q\õpŽr ªªªªdµkLTBGY“ŽÿäKÝ¥B1¿7QçàWZñé`özä¿ÎþªùºW€³/ >—Oìÿ;›ýd#ógõxtW†ŸK¥öê}”#ófõx ã§Òé=¯çz¿mÆüÙ=_y£ô:O=g¥¤b|ÏÙþ¯ŸI£òþtßV´iŸ1|çòé4Çû‹BÐɦM3i›MÆ›7n!¸†á™š™©’™)’™)’™)e–½†zõ¾m'Ì¥|ûiðSé4~¯¿î- šfÓ6Œ7n°Þa¼Ãy¦òÆéº¦ëÇ®7\n¸ÝqºãqMÅ7 ÃpÍ ÚfÓ6™!gäT†S›:ƒcRqõ'Rqus…1Áà¸à©Á8-8,81œ(ŽGú6R¢ b÷5¾¦ æIè_> ~“Iî/ÑZ†M3i¸Ãq†ë ÖÍ7ÞCxÝ7TÝqºãuÆëÕ7TÝSuMÓtÝCu Ö›¬7Xn4ÌÉOÔ*SÌ5&Ö¨ÙÕ£ª8º“‰9Ôá<à¸àœ8,81œŽ  ‡§8šs‹ƀ؄ڌÁ¦(Q_s¬>î³ÖÁ‚|ɽv—ÏÂoÒi}ÕíZ´Í†ã Ƭ7Xn´Þi¼†ñ¼oÊo)¼¦ò›Êo)¼¦ê›¦éºn¡º†ëMÖ›¨n©r¬TæƒoRmjMAÇœãLqd88jpÎNg#‰ Å€ã@qá6!6b6ØbÒ¾âos®ŸÝÕûŒ'ÌŸÑà'iÞ~~“Mî¯aUtŠ¢¹ÍKýR¦1œÃPmê A±9ǘãJq^q\q"FœFXÎ,G DŽ؄ڈÛa‹JOâ³{tÞî§Ý`Á>f§Óà'jOW„ߤƒÝ^Ä®ïòH"Jþ\¾Rúúé½ÍGºÁ‚|ÍO—€©}^~’/q{ów¦/oêq^˜©]È—ÙD¾•Ò—¦*"YCǺ»°¦?Z¾Rzºé}sû¬'ÌÕx)Ú›Õá7ÏèÙë^ü×Ó·ãR•âPˆ#DNüW¦ Q‰~&#|ÿqªWr% ÓÍDóÿ×KïO1<œ8ÿÊ¿ŸOÛé(¢ºQE;ÊORuÒz¥÷0O™ªóðQ{3ùøIô‰æ½‡z¿m?´SPDEZ1S½ÊÕAr Û1ïçþk¹Q¨"%SJC),òF÷ªõýÓÍ<ÐO?ÝÞkåÑr÷'÷ÈY‘û'wLºYeõµí ¾-QEWJ(¢ºQEQEQEQEQEt•Q¬‘{Ó®”“ÖÁŸ7SêðT±¨ðÐO¤ÿÏWú¿m7¶ƒ”gŸíûtÿÈ便ùO'µ÷Y‘}ù!}/­øôQ](¢Š(¢Š(¢Š(¢Š(®ÅQ]+ádÓr3~•¦9šSŸ¤?ÈéòzSüžœÿ)þM§ù5?ÉÈ’˜ÿ!©9ú³¬$ÔJñËbuÓz]ë`ß›¨õø_E/¦£ÄO/£OGY=Hi½÷ˆYjZü(¢ŠéEQEQEQEWÈ´3a»¿ ÉÓœ½9ÍÓý1þGL’€ÿ#þ@ÿ ãŸ1ÎÔÝYËÖœiÈÖ›úÃwRnLfþŸP•úè—› Ä7TÝq¸óqær<·–㿲tÞÚú˜7æÏëð,TO1PB/ ¾_FÏo¬¾¦š_Ob»tQEQEWÇɦäfôG"•ËÓœÝ9Ï€çÂsÚsŽd‡*s“ª7õ†ö´ÜÖê̵%Ì~}1a„&s9Zsô „Ê36›†óÍÙ ÉLå2”¹OÔ?2”¢î?ñ?Ú[LšdÓ43C3pÜS53S52S%,²û¢‰ÖkÿLóeõx(* Ò‰¼¼6y}^×Y}M4¿&›Œ7¢7á90œ¨d: ÎaÍ993œI½«75†zÂõGýƒõŠq‚qpiÌtå@~‘q™4Ü7\nÈnÊg)”§ê┢î?ð-…°Ê3&°Í¦ãMÄ7MÓxÞSyMÕ7TÜSqLÌŒŒŒ”µ-K/ä'X}݃~bùIê㣇Ké_? ŸIµÖo4ó·DäÖ§6Ï„ç°ç×¹ŽN¤ßÕ›ºÃ=a–¬ÿ²TÆ/06˜mDm@mÀaPŸ¤~Hfn8Üy¹!“Ëyùåw‰ø–Ó&0͆m7n!º†é¼o›æú›êoÆé¸nŠf¦jf¦N2RÔµ;Îó¿¥QEQEQEQEQEQEQEQEQEQBŠ7«?×ýØ7æ;Òï?ÅBúbIéwŸ…Òiý¾³tG*Õ-¦Fâ›7fòÞ~Gyß׸î-¦M2i“LÚf†hn†é¼oÆù¾oÆéºn›Šn)¸ã7¸ÉÆN-Nó¼ï(¢Š(¢ŒLLLLLLLJ(¢Š(¢Š1(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢‡ ¢uOõ`ß™'¡|ûhWZéc¼Ÿêð£úM7£¬ÝVCxÞ7ó|ß7ÍãxÞ7Õ7TÜq¸ã7¸ÉŸµ;úÑEQE˜˜˜˜˜˜”QEQEQ‰F&&&&%QEQEQEQEQEbQ‰‰EQEQEQEQEQEQEQD‚‰ÕÖAƒ~dÞÚø ؾ”/”ž¯ ??£ÓzzÍäJ¢ª¯J(¢Š(¢ŒLJ1(¢Š(¢Š(¢Š(¢Š(£Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Ž-Çm*I,n|TªÕhÊ£b‘Øæ-QEQEQEQEQEQEQEWnºQEQ(á:Éþº ó'öüéä"Љ½^<þMØ—É Š(¢Š1(¢Š(¢Š(¢Š(¢Š(¢Š(¢Œx-«Š+¯qE j9΃L‹ÄLÖ-2«4ÍÇO|ŒFÈÖ+ÕÚG Ò¢µšks´¬®,TÍ3n];Q kÒA‰hŒDŽYúó¯xŠ’1Uû–·¨OÓ¢Š(¢Š(¢Š(®ÅQEQEQ‰‰‰‰‰‰‰‰‰‰‰‰‰‰‰‰‰:Öoa£ùšŸG‚+³:wøLóú=7šõ—È”®´QF%m¿‰êÚ(¢8VAtت³£®CmæÛé4ò¨šyTX$C‹%R šSŒ¨cN{SzH£xøðvÔ,IâcHZݽ¨äF)÷Ì®LÑ˺‘;~ÎâHUVF¸z¢Ã Ñ£pcöÍÊG³ÝŠî±Ù»–EÍîG‹"ªoX øÕ‹;p#Q®ÓÄá ‰hQ†™{“—¹­‰ùª:Õ’7½¨‚å—î×þ©7®&/zb÷ üUÎ{w%TWæÇ$¯G#d¦$”Íáävó„îåp®U7Z¡Þfú¢Š(¢Š1(£ŒJ(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(£¸É†äFô'"•Ì€æÂsXsPæ8åÊrg9ƒRoj ÉÅsÔUDèu^†ŒæjüO>—Òºj<4åôZZùô¤£Oè’G9m+gß³•3ž•asqb±ƒ$j&ë•Èâ7ànSÖkF9X$ŠŠ’9{‘Îpz?¥¸¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(®–Ó8ÍØøT3Ns`9±ÖœÅ9r™ÍýQ½«75Fz“)Ï¥0¨Š„ý"ã2aš›Ž7fó'–â׳ÝÕ:'ž¯É£ùšÏA:WKé?—†ß/¢ƒÜwŸGúI<“Éæô·ã¼¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Šðû‹iœfìfüG&• ˈå´äœ—›óº“sTeª/R~¹ù˜˜Fa Œ@~‰q´Ü7TÝy¹!œ†R™ß׸î?ñ-¦M2C4333S52S%,²Ëí7¢to«V4`Ÿ3Wêðo¢—ÒºMéðãòú(½Çyôw‘'¥©Ý‰EQEQEQEQ]+Ķ™°ÜŒÞŒßˆäÄr£9-9 o¼Ý˜ÜÔjKÔŸ®~©Š˜!·„&0 úEÆfÓpÜq¹!œ†R!ùý;Ķ–Ã&0ͦhn!ºn›¦ê›ªn)𙩑‘e—âÑEQEÒ„èëÖy°`Ÿ3WëðEBúWT%ô¯‡—ÑGëUòèg¦Š(®•Ùî-¦l7#7£7â91˜ÎKNA¾óvcsPe¨/P~¹R˜¼ÀÛa·„&0•úe´ÈÍÆãÌÞ[ÏÈïëÜ[Kil2i›LÚn!º†é¼oÆñºn›†fjd¦JZ–¥©Þwõ¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(ýÄ鹫õ0`Ÿ3Sëð+¢ ™=+çáCåôMõ;ªôJdŒä8ޔܜÏR^¤ÿ°TÆ/06ÚmÆmÄcQ¦~&Fjfã'âÜwõî;‹i“LšfÓ47ÜCu ÓxÞ7ãtÝ7 Å3S52q“‹RÔï;úQEbQ‰E”Q‰EQEQF%QEQEQEQEQEQEQEQE¿X}ÝW­ƒùšsÀ¾–X½/£½/õxP}y»ËªùˆªdâÜwö;‹BÚdÓ6™¡¸†â†é¼oÆñ¼o¦é¸¦â™¸ÍÆJd§yÞwô¢Š(£Š(¢ŒLJ1111(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢ß®ŸÝÔûŒ'Ëý¦õø(¥tE(®‹å'«ÂƒÏè_OUó%æñ¼oªn©¸ãqÆn2q’–§yßÒŠ(¢ŒLLLJ1(Ä¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Šìw…¡’!šŸ¿]7»¨÷3æ/”ž®Út®µÒúËêð ó_£ÿÇWyþÒQE”QF%QEQF&%QEQEQEQEQEQEQEQEQEQEQEQEQEQEQ]®ã¸´- ÈÈÈÈÈÉL”ÉL”µ/ÆÒ§ç2þ£üÇz]çÛAKè¥ô®³ùøPú¾=¾®õ~ÎBŠ(¢Š(¢Š(¢Š(¢Š(£Š(¢Š(¢Š(¢Š(®•ÒŠ(¢Š(¢Š(¢Š+­u¯ Ë,²Ë,²Ë/ècM¸•mÌó$öÝçà×Z±¨ðâõ}=¾¯õ'LUN<ã{J6>0¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢ŠëExTWJûø!Éu21üÉýµð®Åöu=_·ÑGíõ‘;ÐB?[Ñâ~nãÆ‰‹ZOŽ4QEQEQ^%QEQEWð„ER-14íb*ª«Pj ó5>ׂŠ/Ué}gòð›æž_E£«’Ê¥o›|Þz#"b¬ŒQÒ7=ŽoJ(¢Š1111(Ä¢Š(¯á4ãnCfc9ÅœáÊpÞq+<Ö­z$7tˆrãBML;ÔFhÔùº¿oÀAPEì×Y½> ôý>žÅix˜˜¡Š… ÅÆÜ†Ì§c)Åâ8â¡ÇŒÙӘ齚#‘¤9pæõ9òéŽdç*s~crS'Ÿ‘ÞR”bbbb`#°Füíg£ÀN•Ñ¢»úWÂB/GÑAåü”ÅÆ6¤6d6qÜl,0ú¦Z3wFrtˆs 9Ì?È)þBCŸ1ÍœåNoÌnÊe!ù¥)‰‰ˆùúß/Q/§þSÉPïêÿJùøPz>Š|þÊ”ÅL\`ãmÆÚ›fÙƒJˆ¸ Í1½¥9:c™ ÎaÏ9ï9Ҝَ\ç"svc)È¥1110000006ͳlÀÀÀÀÀÀÀÀÀÀÀÀÀÄÀÀÄÄÄÄÄÄÄ¢Š(¢¾zï?„Q´+D<„Qz»Éþ¯ Oé_¢i\ŸGJR˜¸ÁÆ `¦S „Î{Nr4ç*˜Ãš‡5Ç6C—)ʘäLnÌg!ù”¦*````m›fÙ¶m›fÙ¶`````````````bbb`bbbbQEQEQEt¯«C[ëðÑÇržF%¯Uò“Õái|—èã’ÅoÂ¥)LTÅLÁL iúfp›Ús‘§9PÈÎkNiÍyÌ”åÌr&7¦7$-çyJbbb````````m˜˜˜˜˜˜˜˜˜˜”QEQEQ_~†±SÀT/¢B8£.ÄÞ¯ J/Ñù ˜ü\bb¦*`¦(~Qús•§9ØÎrå9ÒÉŽ\ç"szS9È¥(ÄÄÄÄÄÀÀÀÀÀÀÀÀÀÀÀÄÀÄÄÄÄÄÄÄÄ¢Š(¢Š(¢Š(¢Šþ%ª÷<(K~ô]ËØŸÕái½Kô¶ä7äAuS ªœäNoJg!n;Ê(£Š(¢Š(¢Š(¯äÚs¶,±DpŠ‚¡KØÔyøZZý- ‚´ÄÀÀÀÀÀÀÄÄÄÄÄÄÄÄÄÄ¢Š(¢Š(¢Š(¯å6d†m7XoÆrc9LV‡0æ)ÌxåÉ{mj¸òêŠ*´5ÝG‡¯é袊(¢Š(¢Š(¢¿•Ùhd†ãMæˆÎKSNZÖã”ó’óyæãÌœZç¥õK^9®b•Õ ìj<¼(ý_·ô–†m7o0äFrXršrÐ圧§œ‰ ù Ç™¸·çJ+¯qÝØ²þ .ÅZñÌkÒXÂú¢ögòð›æßOô–†Hn0ÞŒäFrc9L9m9g-NSŽKÍù é Ç™8µ;û]ÇwK,²Ë,¿ªƒ$³¹ÄÚc½:* Ø—Óá!£ù•–d†m7oÆrb9QœÆ֜Ӛ§-ç*S‘)½)œ…¼ïéEtî;Žî–Ye–Yeýs^­#–Äq, xö:5ìÉé_ =µþIe¡’hÎLG.#›ºæô9ês^s%9R›ò›²¼·v;Žã¸î,²Ë2,²Ë,¿¼E¢9†¼Vµé.™Z_aÞ—yøZom‹YhfÓv3‘º¸EÖÂsãþA§ùžó)̘]TÆü¦ä†O-{vYe–YeÿŽehÉD[$Ò£—ªù?ÏÂÒúø=–d†ã øÎL'2uПä#?È´ÿ"yÏ”æÎr¦7¥7[ŽÿË,²Ëþ@׫H§±Ù“Õái<—î¬É Æñ¨EÖBs¢9ì?È!þAN|‡:c™1ɘÞÍ寃e–_ò¥NÝŒS³7«ÂÒy¯ÕÙhfÓu†üG*#™ÎŒç´çœ÷œéNdÇ*c~SrC'¿ÐhP©âOçái}Kó,É ÐÜa½ÈŒåFrã9­9§9NkÎd§*S‘)»!›Ë_èê(ÅLJê­+ÂÔxzoq|[2C6›Œ7£9œ–¦´9g-N[ÎL‡"CzCqæN-KS¿úR”ÅEEA BŠí~Ý¢§ƒ?—…ÓÕÍFÑ›ì9 9rNJœ—‡›ï7^n8ÉKS¿úRŠ1S000001CeM“`Ù6€H㸓Håj¥*;»#3"Ô²ú1?oUASÀ›ËÂò-¥è¥1S1CŠ+³e¡’¡¸G°æ~‰”(oFrrNK÷›®&õø òÈîQS¢ ­+µ'§úVŠS1SŠíZ†Hd†fffjd¦JY}¨ð¾Å–†Hn4•m{iç·Ü¬<‹éB ­+²ÿJÿDQJb¦*b````b†&&%Q];ŽâÚ[LšfÓ47 Ã53S%,¿ éI×öéé7TÜSqLÔÍL”²üÑUÉÑX‚Æ+{Ùw’ùÿ9¢ŒTÅLLL LLLJ(¢Šìw…¡’¡™™™š™©’™Zñ_$è?o |Ù †FÄ;+(±”©Ú_'ùÿ,¢Š11111111(¢»}Å¡’!š™™™©’™)e—ôHY};üJ)LTÁL zÅ"±Qm;4Q‚v¤óþAE˜˜˜˜˜˜”Q^ ¡hd†FFffFE–Y}–¢¹U®•ó(¢ºwÇw[/±JR˜©‰‰HRௗ]<µÑSÄ—Ïø¥QF&&&%áYhdddddddYeü8‰#Ñ“äDê¾5QE‡qÜwYeôï)JS0S1B¤ð¬²Ë,²Ë,²û0Kh*xs ¢Š(¢Š(¢¼,²ÌŒŒŒ‹,²þ‡M"5d_ϵ]žã¸´,³"úwå)Š˜©Š¡HWƒe–Ye–dYeü&9Z±¿$ðeûÊ(¢Š(¢Šðì²Ë,²Ë,²þÊË,¾Å)Š˜©‚¡HRx6Ye–Ye—à÷üˆdÁzyx2ù}uQEQEx–Ye–Ye—÷t¦*b¦ ````†(RàYe–Ye–Y[¦—¯—'—ÐÑEQExöYe–YyE)Š˜©‰Š¡HWƒe–YfE–_ÐÒü„!“6Ї—mþ_&Šñ¬²Ë,²ËûÚ)LTÄÄÄÄÄÄįË,²Ë,²ËúÆù·2xÑèŸ7«×#Ú*vÝä¾}º(®´bbb```m›jm)²£ûÖË,²Ë/逸(ÄÄÄÄÄÄ¢¼+,²Ë,²Ëû–ÈæŽz»ª|m<¸¯ŸJí/’ùøqÇfɲm!¶ÓÃñ2iš›„ŽW?Æ£1_«¢Š(ÄÄÄ¢Šð¬²Ë,²ËþŸûzil^Úù/™},¾Ò ^ì”ȵ-Kí(ï>¨Õk‰‰Š AßÝt•]ÍJDÍËÝ󨢊1(¢Šðì²Ë,²ÿ‹Ñ]{‹,²Ë-K_ª¨±I›k´£üü$åÒËC$3Cq ÃpÜ3ª·ì"÷½L‘ZZºÌÅr–d¿Š(¢Š(¢¼;,²Ë/øçqhZdYjZÿ&)0Tu§eGùøy™)e—áÚ—ñ(¢Š(¢¼K,¾—ü{¸´,²Ë-~öö’´HÍ¥´‡¹!J{«Ú†djv¤óú:(¢Š(¯æÅ¡e–Ykñ±üiLTF*‰…‹ñðc\’;E´2BÐÜh÷5SÅ“äQEQE}î- ,²ÔµøìcU®Fo¨ã±ÍÅz¨„n¥[DJ¤Äº={q_ ×–¥¯Áz÷ÉüË,²þ*õsb`±ª"B¦ÏvÊ/Ué ©Ïu*¹qTɲ%³µ¸ã%G)“»+ñ?ÿÄ& !@AP`01Qp ÿÚ?þÆÆ1ŒcÇÞXÆ1ŒcÆ1Œ}ÙŒcÆ1Œcï cÆ1Œcþÿáj|‘t—It—It—It—Ét——It——————Ð8úO”¨í¾| ££ÇGÐüø¤.…çÛ|þvß?”Šm&T’Ó*l~ŠIµ7®Æð*mHλ@ˆé~5¿:‡—ÎÛ?)ãö±P/‚‚H±; 1 ˆB"D"> :nmMͧÄmþðÁú~s“óÚgƒí3¢ðÎs¦fff}^k2È”1Œtc&hÆI†lLæ<ä‰'ïY˜ B¨„!˜#ív& ˜ FÂ&!`üã#ƒcÄô“޽>øà$B…D#z!@‹E•?EP[Á°¾ÅƒrH‘¹÷ƺ~ÑÓ:º<.¹™™Ñ ¹™ÑO >ø×MTÞŒp::I:::±ÑŒ™6<ïtý¢¢„!ƒsaŠ»ØB#zmŠeI ÉŸµ<ïtàÌÏÙ™™ò¹ˆUSÃGÂxÙ¤,DÑŒ’1Ò&Œc<,ÆÏ@ž6t(B¢ªZ!B¢7©½226#ŸŠíI cʬtcèÉ£#££«ÔíÈ¡U ¨DÇd}*:ôûã¯O¾:ôûã©1ŒcärfEgßn>Sr}ñÖãå7'ßBÀ„!QUQG ÜŸ||ôO&„.r}ñèž##!Ààp8qqqqqqpÆ1Y:yÅ:V1Œcð1ŒcÆ<3§œSÓ#W8§ƒBªŒ„LFFDUê#î®qNšdoF1ÕðJˆB´q÷ßå×£ï¾t[rìcÑǾ~tÈÑG¾z÷ªuq/>ÿn³§Žqè<}SŠ~ècŽcÆ1ŒzhÅ18'—Þ»1D‹ò³‹Ëï]C¤âòêjE%²[%¥¥¥°(h|ú*‘Il–IaaadÀ P- {<¾rÈR[%²Y%……Y°[Ž'×åóˆR)–Éd––AlÀ Qȱz'æ©H¤¶Kd°°°¶ `¶êÀë>õ"‘Il–ÉaaalÁl U˜Å8­-‚Ø-@£¤¡B©‘lÀ¢žXæ0ÏGB„! ™ cÆ<1#q3… ˆUžiB……ŒcǪñ˜À‹k^¤-GŒ¸èèB¸(•:!q(Bä|<¶öD Öº·‡–Þ¶\\\]#š¾µÑ‚zâÐÏjÿÄ/!10@Q AP`a"2BqRp€‘ÿÚ?ÿÛ°B!EQTUE ”(T«(ʲ¬«*Èd2Ëà‚EQTUEDP¡B… 2¬«*ʲå°AATUEB… (P©R¬«*Èd?–Á„A*T©B… *T©VUÈd21‚„B*Š¢¨ª(SݧÿðAÿУëqÂJbSÅ1ìS˜”Ħ'‡‰á£ÃÄðÑá£ÃG† áýžÙáýžážBŒ£(Q•eVU•eY ‚ i}f _Ä_Yø¶}\Æ ‚ ‚á쯬ü[{<ñ’I²>³ñzüEõŸ‹„¥¹2O–WÉ?ü2?cYÄÊÒK[1ѱìÏò¿äo#†¿r\ä~Õ™-‘lŒ^Mî<¸-1oì³gÜÿKR¢ìÆu“/éÔËDY‰¶¢Ìm¤bçá_~¦8ÀÔ¢!{ ö¼7øö¼7ážÄ´Khy=5ÕÊIþ‹dbÛ3mA|´?ÒÔ_ÓÔ—VY—˹‹”YÉv]Ã2ɨ.ÏAfË7‰–MíÿE´È³1r‡“LY6Íi¹?Ég%™‹‘ÿHL¶5•ÿê7’Fz#Õ“ÅsW9õ¸oÇF6‘* ÇèœIF‚…ê~¬ý~ )غìoé뎄¥ê~§êÅX?Xô!>»»ˆD%„A„A]dŽÂ8ÁÄúÜwṎeŒ”eW0QÈö2Š1`çƒÁú )¦eŒÀðecauãϼåÖ­øã›oc=‹4‹¾ÅÙw<N ²ùeÜ‹7&_Ë,Ö(ÅÊÜY4c“fN ´jò.Ç”¦e?®¼2Ö~‹¸.ÉvCس,àOöÿ¢ÌÅ·Õ®~[u«Ë ±UØ¢’«· ®Ä.Ä.ÃÆJ¢\!2b]¸BìBìG¢H]ˆ]ˆá ¯ÇŸ—Ç_¡?/`M븲q0_较Ê#Bÿ\/®ÅÛ’]öNK³›fN›.˹ĸZ—bÕȾZ˜6Çý"¢r¬É|‹ä'”6`ÛÜɲÙJ¾†ÝPžÚõ/ÐÇŸŸ°U$%ƒô*ˆCÇ…QˆFƒÅH’ÆÖ6?Sõú6,¿ñ,» a4ÉÄœQ*QlDÓ#ØY?-úå·‹´ÈöqÊþº}Šå«áFVŽÆX¶UÌ‘ý ©WÊ‘¬”1Mnd¤ª(¸A(‚˜•D\#Y!p…3å„ú5¹ëÏ{{ pX¸²—· ýp6ô'ö,˳·ŸqBßžööª* ªK…Q… ë$/qBçå·5|AóòÛš¾!ÜÇŸžÜÕñæ<üù«ÞdždAA{ðôf<üù«¦Žt’I>Î÷áècÏÏš½®Q+Œ’I$–,‹"zw¿C~[òÞUÓØ’È’ÅÜ,]-¸²’ä½Dزe™9A.ž¤2#R Zsd’I$’I'ƒáè.~[òÝ[#B ð‚¼#„#CCCCBI$’I,X±bʼn$’I$žšššÈd2VT©R ? ¹ï~F>e·JÞ¤’IbI$’MMHdeØ®]ŠäS"Œ£(Ï ðþÏ ðÏ ðÑDQETUˆ\tòI$’I$’O-sÞüŒ|ØíÒ¼S(Š"ˆ¢ìUv*¾ˆDuÐAAAAG‘tøù±Û¦’IêåJéßWŽþl:Ù\4%w%w%2ȸ²’ȿѢҙ,—ÜÇ-u2ÉÉ9C'#ö“)’¤lU•f*LöèžÜ•¿››ý2̳0‘§©û}š¼J±âT£&8ÁE%D ª*Š¢ „iÖI$’ODöä­üØoÑ=úø#›$ñž¯-¹øoÑ=ø.0AG²AAAG^~[sñߢ|¼>>¼ü¹Ko*ߢ|³å-ºwÁpË(-ÜYkî럗+¼Ën‡/#RT…ìò‰D¢I$’IóÇA—™y1óc²è^Ýd¢Q(’~‰ú%ö%šššššAAAÓå¿™dŸ“6Êëe‰$ÿ£^ƽMHd>ä}UˆFžà÷ò¡®Â}øã¿›ñíÑA¤2d*Š¢b>÷äG¿›ñíñ9E‘dYEËe™/Êß™oæü”Yw/‰txˆñèñvY–d¾‡$O/ñïîò»–Eñ.'Ñâ3Äeò/—rϹ/Ø&Äòpþ½¢Qeܶ=Ëâ]'Ñâ}eÙvY’û’ý¾$j ⼸Kª•ܶ=Ëâxˆñâ}'Ñâ3Äeò-—r_r}î|•\VÞU¿&Qeܺî_åñL~$“мu*Ê•#$ž[Ž>ù$ò`‚ë2O¿’Ÿã¿Àòq‹fžËùר¿&>«Î·ø"üifóïìyãWæ_'ËCQÎ’IE—ÃÚæþLg^cl×—?ü˜Æ¼º•*T‚Ç^£ÁÏ‘mñÉäG%|«ÿÄ>1!23A "Q`aq‘@Pp¡0B4r±#Rb‚ÁáC€ °ðÿÚ?ÿÄ^ôo¹½{—_W.Íò÷7³{7›¿/cø›blGòlfÙ‘ÏØÝø7£ˆŽ$}ÍËܿ՛c¹û›åîo—¹Ä‘ÄfÿÁ¹{ÇØ´M±6#‡ù8“c6È´Ž~Åß±¸ÞŽ"8‘÷7Çܺú¹wîn—¹¾^æöofówàåìr-j6/sgäØýÍŒÛ#ù{~Æãz7£|}Íȿջ›Ÿ¹Äg›ÍÅÑȲ6£a°Úͬ³9—7ѽ‘rÿV®nföog›ÍÅÑȲ6£a°ØmfÖsÂæãz7£r7"ÿV®nf÷îqÄfòç"ÈÚ†Ãk,Îxn7#z7"è¿Õ[an¦æogœSˆo.âY°ØÍŒ³9ûçãîn^ÿUlX±bØîgœYVqN"/ø›bpÑÃüœ7îl‘¶Gòö.ýçHœHû›—¹uõ6åúÖ,X±´Ús.Íò÷8³÷8²8¬âƒzö/cø›blGòpÿ&Æle¤/bïØßø8ˆÒkÿÆåËÿú4ìX±oüÓ\Ün7ÈÜ‹¢èärêØ±cick6²Ì±bßOuù%‹,‹,X±oü./–Õœþ2…>_ «åºš1‰´- e,EžDªfÚEYÙS*±le$Š)k†’(i2ˆÒzµ7jPÒUq²â*6iÔÔ×ÇËåò°DJáNdª<ˆªð€¢M£¤Ò#TM&SôÉá÷:AÓ£:B~ƒôÁ$×r#Ti@€…ãåó‹ã£êV¥MØhT¹z©*EÍ ¢¥pheQl‰y”¥Qä%ÜEwuñúú²…õa êíÒ+uß… z¹é¦4E$ŒôÓ«DUÃA¸Æ¿½¤[údº™dò±eé3E›*FQµF6ìi½&§úr׺¢¢¢;ßZתÚ|ˆ×¸Íѳ´ßتuŽ×7#N“+ÿ$žfšî2Éÿ*ô’WIUC|ì}¶V}%þ¦‡k¦$³h¹•„óy`«*.ñešZhΑ¹*¢zÑÅÐRýOÀµÌŽ"¯p³´šåÞ~¤imQì#.ádé#QAµ›]¬ÝÇg¤Q—™¿2 y“«î?ïDhùŽ¿í.]S²õ GД;6_!áÿb:H+ÊU%ý§G'ʇFá.ÏyÑgé*»Ø¦£T‘Òz•výFtS‹ì+“û5_óÆ5±8ÿ*“ýnuÁÿaÒÉ즇I–5¤¥¡ôéÚ\È%*\èë©Jÿ£ý+mÔ%6’#ýÏüè¢ù ñ¥^/ ò÷¯[—gfMf“f¬;3esº•Rudz6ŒÕ×¼Êæé…dô¡5“³È¬dÓfÅšU¡TèÄäêéC±:³ëäf•̱–‚íÿá+DÍb×Õ‡êK:ÓèèŠÖ¨¥SE/ÜoDUuïj?32’+Z•M5Üe7DËCtJ<7E 4¹xúÑyãJs±u<¹’‹_àyZ?‹4ÓԮмJIaÙ÷*íÞŠè½G Q•Ñú&[s%¥<Šé÷2eÔ®ïh¾ãŽ]Q*FÄéµÍ2‰s±eîv–Û¦9%ÙdÿÓR‡äË•¢ô©H£3J˜R(«ŽžEb´?MÇQK£®9Ê)ÇôÒº¯D¼¤¿ö<ŠÆi- Æ: uerû`£²/£…®vÛŽœkmìJHèÅ.óôòv»ŒÒŽžX5S3Š÷+éÞÏÓÉÚb㋺":t5ÓîOý ÿú2Â:•ʽÌÑŽ†ÏɰqPµÄ²\«Š÷;\5v+8øÊD¢eæRC^ed©©÷ ÔX?V}ÙÑá>îCÁV h‹®Æû‘o¸NŽ”&Jªìÿ°ŸØéUI¨ßSµr*]o4G]0¢¹W/X²?§Þ"oøÐé} ýˆ¸Ç1©T•`àûÉ×¼UµHÒŽ*£\Éd¹Òý/úŒJ]OýÊÄ*ùòÓìOÕ‘þÒq®Š'M“uXó×6Ê“ï$Ò”tލ×Ebžd3}‰ÿi,Ù©]*CÒ'KêtÎOœŽ‹ô<­ÜC"VÔ—fP~c£¯;‰þžýJt»©Ïý¬×6Lñ§qÿMôrÔŸ~DtŸé5{áÒÿj?êªí*#£É Úr: ªV« ˽þäti;Ö§Mê?ídâ¦Ò:A¿<$úÚûkÓRQSt:<±®œ»Î‰B4žm5ëI5âçÕ¹vS3ÂŽF’f¬¥M4l»*-^‚ÌÛF“r« $ÑÚu4“5f÷þJÛðS=PÖÉ£esëê79Iy¢™ë¡Ù›;N¸hèQÍ™4fœ¹$û4*¤êUÜìÉ¡7#y®™²¹ÝLÙFÔ©¨Ûæo÷*Þ£‹W™Ù—Øìºóv©tJSékÍýLÏ ÅÑ”Ïø;¡Úeôó3'F(ÉÔÉ›²vd4¥£ºýg[r!’Z®h¦Æ4Œôî+PíË £,§]Hf›¬lÅ'>ÒæesÓ ÂT$ã+ÜìM¡IÉÕXâ;SžU òÉ£4eF9JìʺGAÿ¨õ8ŒìM£ˆÇ)=YØ›B“›ª°”ç_>¦ˆ®1Ð¥ /¹Fµ,‡EîU­<Šª¥††ZjR…ZÓÈ«Z Kœ½Ê<3Ð\¥Qv*'OTd’ÐìºÇ– ,u©FŠÑ{ŽIXÌ–…—¹ÚBÏŸz+kO"©{¥ô±õ*ê\­k„}ô‘*£QU>ã'[WAö3GÈìÖÖx&f'êO?xÒî"*á”MôYXš\ÍMz:ª\ìÛ EžÜŸq¦·‚kÈÕ±z¢_Ü:÷²?§ä/·ÒÇû4‘¤MÃÌÆ«¡¤‹áÚ•Ù4e3HÜ$ÝqÑЦb× Å^r©FÛ7û™§>Ñ¥‡ÕXen¨ìÈÒGÅ×ìQ:þî|½žÿ£ÿn­iŽÑÙ‹)%CHKSX3²ªkvN#ìèvQ°ØeË©·òS.§j&Ò’0­4*—dªE$©…´+öH¸­hRQ6òˆöu+”ÕhŽÝI}ÊeCŠZ¢jK‘(¯÷Q Qg)‚]ä:?ÿ´¡ÉÚZw•¢(ôÊõLϧ<W3J–& ¨„5wGÍŠ*™¿«ÑzŸ§—´UR^ŸC—Qk‡,kz F+–åAÒ=Ÿ"±g„Häu©5Ü: 5ÈÓ¼‰OãBCìKî,З¬KÕa(,ÇjôÂ"½(U´…{л/Ô·,éÞ‡G˜¦Gê[‘%](GôÈ$©¥Óz'÷›å5­psî#ÜŠÿ §J£zõN‹ôí]N󨞹i„j7ü2"Z´ÞƒìÓîC¤çC7­ ýˆ~“ÐËÒt™ª´d»)ïæÃQýCG…ÎÕ¥(i#–7.6*6E(ÙÆÍäV,¥M$kLiZúšHÕãLÕõk¡LÚH«Ã³*“üH­]J³³*Ȧs6mFÙÙ“E^OUþKêo+]{ÊJgfm“©LîžøÑt’3fuï;SlÒMaVêÎÌš+'VeÏ*|Ñøj>?~¯ß†£ëàº}]Z¶Í%žš× ¯Û ¯"ðÚÏ#JÐuCFּ͵š#X› <3SB´ÐÚe£©´ì£´°Fh¯Qi©ÚEŠ<ÐI«œ‡¥‡lª4‚k¸–Zy¦(®ótGªTt7DºÐâ/a<ÿE:ÔÒz”(kÒj4墿f„³#‡"y¢ÙÚuÐNsdT§Ø|Å(Ê«èbê*÷Ë߆¢ŽRHŸ©÷™—Ë^ÉÑ®ñjF•·ÕOûGèMw§yWq))/Ac¯p”£/TF¿a¬¬—cOÈÚ­¬ð©WìiÜȶF‘¨´Á5þÔtm÷›~äÓîÂ?܈}Γì/ï‘O"Ü}°Q|ÑÒI»ØÓÈŒ‹Ç)"2îgGÒFjˆté)*s±,Íf¥Ñ¿ÜèÿRJ‰‹ûþŸMê™ÑÖTZÔŒ¡:¯˜¯ ÇÓ©G†R††°4FÁér¢õy®_\y2Ô92¦©3CTŠr4c’Ï”ÖH¹®3‘¸ÔÜW35xÑL}¯±E- %B­àã] UšI—cí=q¦ºc«*Š[ßÏ ¼toç‹ÃpôëÐÖlÒUëi\,ñÒ,Úͬ¯"´Çk+M ÐÖ|63U¡m •]å´Ñ†I-Î¥råe….á5 ‹¢]ë‘™—äY©ê)fü ùTÖz”~†áÔ±l³)AÈe+B5_r•ƒ£RÁõÁÇÈ” cMGKs4Cõ v“5î9Òƒõ"eå”—xóbë ®ñå9”UDÒ×·ÜBuìTTJâóÐt#ê/Rqú2º„&¥Ù^†ãשÚÙ\6pMJ†±Lª6cÞhŒÆvþÅ1ï._ìi…Ê”n¦ŒÐ­u+'÷7×$ÍÏ 5x=J6ŠÉ:÷áå\tæQ¦Ž¾Ep­4ý¿Ôþ"Š»)!ÉråÏæqðßßö5™ZÕu£XÙácH¾¥™µúš¢6M¦ÑöMbhV5KB¨Õa Ô¨Êè(ËC²—¡[!v…ÜŠcôµ/\º™s'©Ú›¨ÖmÄëNC¾]LªZw$»HR“vÔŠRyY$öÐi1/3kõ%WT¬fŠ¥9ÉÙ¥ŠRµ³|ŠS·ÞJT×]IfU쓊‚YE9F­n&°Tï_-BðÛõêÛ1D¦˜r’î(- Т1œh3îÅBUêB–æ2Zó2ØDçJ %Äðk™,×ޤ=HSíjG1 WDAùaªó(ðuc}Ó#5;r5³*ç¯xàåJ×òjê©tNj{†“×L*ïL•+úy2zv_pÒ“géN´‹ªFmTÉto¹êj®Ž“M%*ÔJ}hÔ‹¥|ŽÏFÓùjðä½gU©Ú´Ò+–ŒÕTÑ46–©lQ¡QTî¥Ê6n.TÑš¼w6jñÜÊêjÊawð‰¯îvdÑÚuùt}|9?ع[™«×³ÆØÑ£KbŠM ª,R…b±ä%ÌÕ,¨Ü‡rcU©“7+Ž5±¸ÒU5ÓgfZùø&>Ÿ§V”Çìyl) ÚQUKì[ ®â- ,[&N¶Qg#—¼ƒó+ЬkæC^dJFô#RŒk½¯y(ÿÈY‡:ÝUÔBË#YÐíI?¯¿ËÓö(ÎÊ)L6êXÚR˜k¾ºã¡£ÂåÍ$jnxé&jðÕXÝþżü7öñïØ~‡’W)%LUÙ–K¯—5ñRK›ÊƉ³UûïÐþy›Ö©n˜_«fYá\®JÓC54ï+”m+\Ζ˜<´Ð«Zy‰’šèÈR‰£'1Ó¤üU¥9—û‘Bƒ­EÚ®–gª#I¿2I7Uæ)Oü§o&*ïå„kÖÞE)Ûäȩ®ZTìÙ_òEIkäO¥È¯¢î3Ò9£ªb›Šµ‰tÙuÔÌãÈŽ„%Oâ…&µýùzãßÏ!ëÕ®oö¬XÚðÕc¶†ˆÐ±Fi|j#r;‡­J¹PU–…è\£ï5“)] ²‚©ÿ÷ Ñ se¹Ë…MQö(»Ñk#§v#¡8÷!˜¡,‘t®.=äcætymÌÓóäû'êy‘§6:óGJæîÈe—1W¸éeZæ±¥ft:ÚUdÿi.êè?,2Éêˆ4ëjNK¥ßÞC+´–‡E8ëGZ³µ%ËϸÍÚS#ú°Ö<Í,E÷2PzU>FHôt2ṫ•vÚÌ¿§AFI¤Ò £*~üýá¸úõëLhPZðÐFƒcê2^¦ÚúaÙÆˆˆ¥ÊƒC«(ž¸,­0ýH:ÿ!>D¤Ç†½Ã¯2‘bÍÈORO©fVƒ~dtÔµË!é¡ÍPãËCÍÌo ·©äi#6gSYá*ódéRœ°¥] ëR‰?“O˯Lk†ª¸m+ÕÓÔ)SBåÍY~¥M+ûVÃOÏëÓöoÔ³ÂÌu([\-CDj‹¡ËÄrðì=:žx- ÓSTz XTB>ÂlŽQ³AÛéü=:Ú£Bź›J–+MJ71PÜËý?‡§#éáØøò>žûøòŸ =~¾<ÈÏeëãÈøv~;^¤~=üú~ ¹råË—êØ±bÅ‹u.\¹~¬}EñïçÒôð Ë£r7#qrøs,Ë3k63a°ÚYÄþ&è›Ñ½CˆofæneÙÏ 3c8oÙœ'ìÎ ö8/Øá ¿’ËÜåîn‰¾'OÁÄüGìofé‘üŽfÓ³¡GÔ‡¨¾=üùú|ŽåË—._©fY›M¦Óidr.ÈÞæó{7³s.˳ÿ“ÿ’ÆÃ†ÿ' û3‚ýŽ ö8_ƒ‡þ ¿àåî]{›ãîqÄG_ÁÅüGìq$o™yŸËܳ÷6~YÃ8HáGØáÇØÙcjöø©u!ëâ[—.\¿VŦÒÅŽG"èÜÆóq¹—eßRÆÓc8oØá¿c†ÍŸàÛþ ~N^æåîo‰½Eìqûäo‘yËܳ÷6þM‡ 8œ8û#ìm_9—R>]m0ÚÍØá¿c†ÍŒÚ[òò]{›‘½ÑÄüOÁ½›än‘y{œýË?sa±8œ8û#ìm^ż+>¢ðôz´Ã7?OשöǯŸG×®¾|¾bÉzõ¡/_>¯UŒ_*¸ÿaàÄ>¢Åæ —¯R^„¼:º¬b뿆CÁõ‚ÛcêL~]F1u4Åõì¯]ãîqaîq¢q‘Å7þ ËØ´ýŽÎ Í?éäOù8 ÜáDÛø¹uf?_GӪϿ.nFøûœHûœXœTq ÿ‚ïØ´ýŽIìp&M#úoÉý:÷81÷6@ÿéœHÔDþ¨þ­ŸÕLþ¢geæZFÆpÎ8hب²êÝ—.\¿Zcø÷óøõŸ¯Ë7#|}Î$}Î$N"7þ Ç2Ò8r82?§‘ý3?§8Q6À¼,ê"TkÿVÏê¦qæoéæm‘Ã~ç òpQÁ‰Â²>Å–Ùv]—ü—.ÈÜ\¿RÅ‹-ðrÇ¿ŸÇ­?_„º7£‰sˆæïÁwìs,ͬá3€ÎÁ8h´MÑ8±?¨Gõ'õOòqäq&^ƒù›%îpÿ' {œœ({#ìr.\Ü˲ï¢èÜÆãqvs9–fÓiµG#–.\¿É_ÈsÇUÌÖ¨»ö9–‘±œ&pÀgá#lKÄâDã£úƒú–qÙÅ‘¾Gò-/sc÷8“……‡cl}ŽEË›™wÔº.ÈÜ\¹ÌæX±´²9‹¢æãs.Î=ü±×£GÆÈû^Ø\».˾¥Ë—._©bÅ‹#‘Ⱥ.\¹vsÆÝk—ñKðê,5Ñt].]œÎxX±n­Ë—ñûð¼–¦Gz‹]Ø«_”v¢ÑÃc¤NÔiòÕÕ^—É2ÓR”Ô£_±b+½‰5«ó/Ø2RÈ”e®·'™[BIX¢4•|„ÜškC³&s^nã4=Ž×ØÛO4eÊŒ±³U½çF©y,*×?ð%M;Ç¥WË—V>›.†dº—5é _Ó ¯ØÖ-²ÊåfÒŋģZ‹µ¡»Z‘Í*÷«¨³\¡ÚI‰ÅP«]äeC+¥;‰C’eDÿä$8¾í'jÔxJIý°¸èÆ™LÅsSÈ}Åb‰3´ŒÈ®]{Ê5ƒ\™»Ün,Õü¶‹«¾²ÃX¦e}àìÅTÞŽÔ’³&‹Ñ”fƒIÕT¬tfÄÎÍŠP—gô¡/!‰á[žož44:7æFy¬B^¥Wp³òDÚ9QT¹rÿ/º7#z7£y¸æY›Ãgá2Ⱥ7£ŠkÒšucñëÀÑÆŽ—ZèhÍH£cY‡êjiƒ•¦ ÷ K—Âÿ/º7£z7£y¸æm‘ÑÁ‘Àgá¢Ñ7D⣎PÎ<Ž$‹Èæm6±Qln\¹råúè_¼º† —0º7£z7›‹œÍ’8S8,àœ4m‰ü$MztRqäqdn™ü¯Üáœ$pâl‰eË—.\¹råú–,[àר¼>ºÈ_ º7#r7Ùµœ)pŽ-t,N9ÇgFù‘gîlüœ4p¢pâm]K²ì¹ræâåñ±bő˩‘ÇÔ^]uû—FänFâåÎfÖpÙÂgá›Qx›âq‘Ç8ÌâH¼åîm~ç ᣇcl}º·ÂåË—/Ô±bÈå…Ë—ù¼}~@üºÔfš£†ÎÃ6£‘½TqŽ+8ŒÝ#ù_¹°á£dK.¥ú—.\¿ìܹ¾¤~@üº÷/Ô¹rÿ±Ë —ðÛê/?¯/cð ê¿·ä?Ž~]Wã6ûþ>_¾Y°íGk⺻WÇ¿Ž_+Ge£/H:‰ÐÕx§DVfXüø!ˆ]ºŠr(W†ìÍŒá³ac‘¾''cˆË²Ç ìÃÀ+äoÂÛY±›´ä]ÑÄF½)Å7œÍ§ Ó¢8FÄ[ ›ÌÜÍÌ»ðBùð-™µ›Y´±ÈÜèâô¦òøl4èÍ:3a´ä\Üoföneþt¼þkbŰä\Üo7›ÍØm6 †ÒË ›ÍìÜË¿¯Ý§†¬[©sq¼Þn/†Ói°ÚYasq½›™wâ½>ü’á,[©sq½ÍØXÚl6QË Ææneß4øò]bæäoFônÃk63aµ‹›ÍìÜËøõøL9ìÞÍÌ¿ÐÙ|zúcsq|lX±È¯ìhkðËéË—/‹crì¿Á¼5øUôVåË—ÆÅ±¹rÿ#ÔÒß ¾‚ܹ¸¾6,[©råþeXšüñµË›Åð±bØÜ¹råüækðKÄ÷.nFâøXÚm-…ÍÆãs/àýpÔ¬|UtnFôn/…¦Üw̹Õiá½ÈÞèÝ…¦ÒØ\Ünfæ_èùåÍÈÞåð±´Ú[ ›™¹›™¡Ïå×7#q»¦ÒØ\Ünfæ_éåË£q¸¾6›q¹¸ÜËý<¹sq|,Í¦ÒØÜÜË—úv‹£r7ÂÅ‹cråþ’رbÅ‹}QO M¥±¸þ«ßëú±«4úµ_¢”E¡_åEJs\É'S裨éàJ•ú³åõk+úµçõOˆ©Uò*}ÑšüM9|‰¿§_Ë)Ô¯Ðû|Uû§®cÈí-Š;ò5±I}=®4,f_ÚÈþÃŒŠ¯ºÃF¾žU³²ÊÐÔk­NLÒèÑ{_ý?âËþÕþšÆB‘Râu/ÕõʈÌÓÔRç×¾l¿ÄÿÿÄ-!1AQaq 0@‘P¡ðñ`±ÑápÁÿÚ?!óñäçÿ\¿ú'†šÏ':Í'“òæ_!Õ?P?J/·ÌU̇¶¸!B„&šB? Òÿ9«¹^\Ä­¾a'ý_û‹ôgUð„žÔ%ÇþÂý€¿îéýd.7Ú?L´.³ä~‰ˆÔì?gû ìŸ&O! ¬ñM'þ5Ÿ>ø)JR”¥.‹àYE¢y# :GsùvùBOú½¾aû#ó‘ùÐDŸŠÏÍgïXŸu‡÷èü´~ŒE~Ð~TÏÊbgú‰û|ŸàvùÑ!M!4„&„&„ÒysIÿR”¥/ˆàöYE­ØviûÛÙÙ‹õÂç¾¹/ÕÎÆgïÞüÿCõÃñQø LýÀüé‰ä™þçè‚~ß::/“ü„! ¤!BXOü¾”¥)EQe–WŒø#‘"~o‘+ýÂ_úè½_Âå þ0¿Tü~k!ò/É‹õÂý¿äŸ‘oÃ:_¸€§ú‰»|…xt0`Æ„!B„&“øuþIJR²Š,²Ê,²´vjO€E"ÿ¤ÏÚ‹þÞ˜¿+ðOü4ëOn‹ä\o¸KÍó­Oâu_Ã? gC¦_ýOÙ”ÿC :¨¼Çv³HBY¬þ J_å4¥eQ|αÔ:çWÁµr;Â9#¦t5ޱ˓ó6~Ø/ûöø¹u>Ä7ß ÷ ©òsôñ£®þ4 …õ¿Ü§û1yŒÒk à„'þkJR²õ+™Ö:§Têoår; -@èfr>A~ûýñsàS¡øBÿŽ~$G¾/ú„÷ûÄœ~e¦þ„땿ቜrŸþB¿æ:‹I¬Òxf³ÿ8¥+(®gXês¬u‰ñÑì‰×»iM%¤ÅÂmœQ·ÊÉmó Eø=öD?Èï=ÆdwùNoåì@9Ÿ2>È\X¾‹ô@ýÐþ13oŒSo„!4„!O*ÿ1¬¬¯™Ô:ÇTê4‰N©Ô;gMÙ'”\}zrÒÙ~ì_¶?p?d/׋ôGZöª?ýbä4OÙŠÊZÇ?÷(ªUûÿ(Jè„'–Oá7Ë¥eees:‡XëC¨u¼ßÓ©JR¢¢¢¢¯©mÖY¤!B ÿˆÔT`ÁipúÖiM!4š-,! cQù|ðö¹‘™2gL™3ôø>ñ ߪxØ„!B„!B„&„'‘?ò´™hÚEŸ—ÁåÂhóá„Ò„!B„!B„! à„&°Ÿø²—¶ü͆ϣM!M!BÁƒB„!M!4šÏç°G3¸îЂ‡PœÄÑí;ðì+ä;6*zåé6œâ¥ëc®8Ø~\ÖI¤!5„ÒžOçT¥Sj£¤tN‰9ùädMõL•”¥ÿðëJ])K¥Ò”¥.—ÿ HI>w7ì²Ë,²Ê+Eh­„!4ŸE¥)JR”¥)JR”¥)JŠR—KÿŽ¡ˆDDDDDDBh„𠢄!Bš“DñB222Ð])JR”¬¥)|à]¥)JRét¿ù*„DDAI$ùŸÞË,²Ë,¢Š+Ddd!“&Lé2VR²²—Eô`D¨¥EÒÿä¨Bˆˆˆ‚ ‚I$’Iõ@ê(¢Š####2dÉ“:])JR”¥)JR—ÿ2O!´DA#À'R>d|Èù™2dÏ$g¡ž^ŽF93™Ž¦ h¼×¡è“Á5þ®Ýç¿O!»üv‡æ¡é/<…ô­æŸ©ø¡Øø;xlë¿Èìü“µòt×Ê?GáhëN¨ëôOÔ¢:߃«ø:âtΆ¡e"¹È¢¹È®D|ˆÈÈOæO<…ôt¨Üö ¢ZR—ZRéu¥)K¥Ò”¥)Yz”º`‹’"䎃àé¾—àýÐ9Ó«tΙÓ:gCRïký}>±Ö:Äó;ÎýÚ—¦Ê(¢Ë(¢Ë,²ŠÑBø;8‹êÌóHÿ˜R”¥)K¢êRý íõrÔ'ùõ)~³NU%ï è'¬Þƒ¨¦]áçó1Pn…„Jé¸Ù°„uÓoÐ6éÃêÉÃ鋚ˆ™˜i­ 4 µQ½yº,Ò°øll$tÛbÎÀ¤ÄuìËÈ#òªG¤eÆMìalIäcMDZB j±-ØÆ5±¬W¶ìÀQ¡¯„6UDà2Ä…°•ò´ôŽ ÃrlÐËt&‡°ö´®o ¹BʇØû²- º¦[g6o; SR2Uâ0HDæ_HºÏY³ÉONLMxcÝÐßÛ¢X¸¶ˆ·WÒ‘÷§±«l›eyiíÚƒ%Üé—ö/°-7Ú3iܱ„Finì-(6.9’.¥ÕîovgöáWvöxÈÍ;‰s@®ûŸr"«„ÔÚb¬|Œ+»IÊŒ¹ôû3‡Óé+×—žþ[†} &y™æelËñ|–%Ño„6Ì»£t­ãaîŠq ó:Á&ǾãzщqôJ,£{™-ä\ Ý3£´€¹·:$ûŒ òÇ¥…ØU¶fÇDŒbS\È«[ fuIHœøÒ8BHªõ''ÑôBóñçÐüõôo¾6û¼ÏëHz!zêò¼kèßx} ýDógñ¤?ZšïòOÈ_FûïâYý=Rõ»½ }h۷ϽȂ’ä´b“v᪊ð¤îÕyf+Ò[­Ñ Cmì‘suóÎCWsJø²öɶ!ã¼wÊ«Ÿð°õ/§D/¢îZ­R„¦a{1$O¿K¡‚§À_™c·Cš7(é°Ÿ O{äz^L1\ÉÅphÉ^\ÛñÌd>±û}›©Š«uûçØøî´Çÿà HØáp Z÷3ê.ª/czCMoïƒð‘ò]¾ÇØþbš» ²®ñ Â_o'¬¸±¢*Öû ‰5)(°$aó œ•Âq¨ª‹K•uù! ×ÇW‘wfWq£9å`¤³qÝËCøv48ÝBB7Bñl·ûë†'ð×Q²dpÛïÄZ•¶Â›¹ ñX³m Kl£ôQ:˜åx¹xÇW°Ü{ÑŽ cñ×õ¾Ô¼sÓobò…ô]ÈØ®ªÃâ„©)ë“p+±íÔðè”Cil9îN£d2|3Wâ;²Ýc²°Þ^Æá“Î…¼ÙŒ™‹ª‘#¬îsN,²#l¯ÅC§ŽYÑŒi*òû×\K$Uq\%Ï‚oô1P×]/dºŸSQmÜŠì5½älöÀéikŸÝk}‹èb6£e—;§M»ØhÉA'‚¸Ñ9!‡tÃÚ_Ȫ¼MÝ'ĺ—4´û·¶§Ë8Þ ‡#¼¯¹gN½‘“.öÇ(|F‹}ÔÏ(CïßÚk¤[̘ͣÕäîšÈñÈÓc í¦m^_cx×¹½¶]×Û"hB‘ÁºÞÑ_wæc‹?Nà1h^±›…ä/%}`4Mf“HB„é¤!Bh„í¤›nf§r¶|I‰ÃyÀ÷á=ˆäeSgwË3…Ëc°MSE'º®‚m$M¤÷\È7¤°-²?ûãq4M(=×Õì ŒÖÆÛâÅM"¹Z¾ã¤Þ.Lu¶Û­ºÍ„ô¸øæždØÂ±qîúë:w$ñðg#§v0þÅ‹~8xš¨Üƒ0ÏÜê蓦4é·a=Ói7¾DG‘³DÑ™:Èœ·B#:UÉG7VJ¾Ã;+—yØIoÃÑg¹Íýiì1zùî/B¾Šø¡ ¤ñOñšû„&„&šBI¤!B„!BŸ[{Qz)ålcÜáèWÑŸ Ïþ8z«Á=F×å¿!}cÀ·‹²¢:£›SXå˜ç®|ëêÛœ|ª¹¯ ^þUOÏ~múßÃW¥ž=ÏF¾ý£Öñd§û†Î¨ÖBs–ª¸Ë)씦4¦q!ð‰Sä@@ŸB0‹Ó Ü*æ·Ó Ó»vbcc{Þü¦=#(ÜLJ°øn:ö5³1bÁÅ»W¸³£+s)¥Þwì·’'@SM(ÏcÃMœ—ýXNcû «c£¦+Ù铉ͰŠ)uÌJ«¡Œ:,f î+îžkauR'µnq‡ü;˜ƒ¤†zaóAK…Ö›9€Ç\Ï!õÛªû!5Ep¸»gqx”šv¿ø¼ÊÇq×eôP°¹ÆÃÿ[-Ž÷d4¶èzfmñä‰xÓz¤áên#ÏH„Bò—!WJØ;_Aw,±pXéuœÍÉcÍ„<ênQúƪ:†V—5fâJ ©tø\‡íW›{ VöiãL6Áw˜²ÔH÷cÁôêÄø„oWJ7%^`É!n–G¼ˆí…߸™ÿØ„s†r÷蹓ÝVOþ‡Õ"-Ûa™F± .غ¤ï‹p†z¹›£6ïhxQ¶ÄtnQEÈ=yæíá =]™¢Ò‰w¾“r7ø5êß—?%}¸õoC5åC4îÓ¶üz—y¹"v56U´o ™’¬›Ö$ßÞ²ºë_̰RS6iüDÞëÁð­n²á ù K ÷æVhÓŸ·°“ݧ‡ÃERÁ³ï¸omÈÇV1Fn9ž"¥7ýw¿¡i›8>0‡»Ã\Š»Š©Š˜-ÞWÓ{ßj?&SÝ9—pN£Ý-VÃÔÝXžu´½ÅÌ"DŸöb¥L{Õ¦¾â3°V—q±l|ü“‡û‹eØvj"Ž>Ãw¦Ä×h³U“[ùig¦j ½Íð½Äs>Î[Ó¾h–¡òº[Þ™Å.,|ËñϤ?Ÿ6ñ¹·Æ2 §ØWC<píFñ)ƒ,ö×ÏMì 5‰ b;›—u±&èuÅd¢óÿвh¯Î Çã‚U"} Ié.„ÍÕ/gP‰5ï³[U²e*ã e½wP°vv>“hÝë÷õ Øîð&ÖͮΠªÚó°oÙlîN@utN<6ŸG ×&éÏtšËÙÚ·.°½ÿø6©fá‰N¥;páF¬¾; ‰¸+Ê÷9D|òWönÿú)»}ºþŒ Pû{µ}ÆöŒAuÿôd‡\Ó¦î_¸Ø5t„r·f×ô7½/yì<ü?Ç2á³»6u8Z¾ä—ç ÍÍ8P‚ûüŸ‰ãàmT¶ÿ ü²rbShŠq¯­ÄÛÓíð6kf±>µ/ÝR†Ú·–ÈmÁrÝ|1³у=ÛÌìΜÂ~Åîmû›s¿‘wŸ6Û î YŠn‚^äý†i±–*z&E±„6ì' EѧWðH\NbxNÊnŠrà5Yç Ь6JŠõKŒNí‘ÖìðÃá’{®¸‘Ò¯!ÑðÏn8É¡ÿÜÅÒ 1Å”9¾ï°ª—¡A4êhÇá#­f®¥£Éy“ w}¶üþH]{õî4ªnß[fÁ—mÐÂíæÓ°æÓÍ ªi§Ê«8MŠQcÝpeç! ]Æ,!Mºv+bà9ïÄ7gyÄz›²å‚ –RÎï68íÑb ,‹Òm½~Í¢òW’¾±ð5”1Â}äÎÚKr,‹A $…Ô!º›îa¾ÅÉ 8Öh_3R ‡µf4êÒoQ“oDÅs_Bæm`“˜9G¸èâcZ'57ÄÎ'ÝAK9Ü8^à ºŸ‚R¢½‡!ûRös^Ó4{…Óq F¸ü7'Ș(érk)‹ôµøœ Æˆ³.Ýó5‚bÓ¨t¥ž)Õ¤!4„'B„=ˆM„!B„&ˆB„Ò„!4šO ðÍ1êMþ½zW¢_J6.†eV8n"H5ÛJ®+‘7\Йc´|;žbˆ“;ì¼1“Ë\G#ÚÁnwÁ¼¤ÙP¸.a‚N\Oôctt+5^G{›îAÔ¤ÔŠþøÁؤšžãsœ¯òpA•pFl»2¯æßD¡;ÇH/”@½—qðGÿÖ²]Þˆo3^¸ø'!nc>Tò§¥ŸI·±hÛë¸Cô+èÏðyUŒ‘ìo‚\¸¬³gs{ûäKodIXÜew ühȤôÉâ(&+æò»¬›ô—.åöÁßH3®nƒ}>Ì–ý-É •ç\Ž ¬ˆn\FÓÏB–ç¶·Ü<½]Çvm0øNBèÙ´j—d7p5"ÓìÆÎª7:ÁX׳‘¸>”:¢âŒü™(É÷kñOsš…‹o‚‰¯œšlÅem—q7Të™8Ì2þ’ÊëòQ»/þrÝ|3.!®e#ÿD w¿ØÞùÑÇöEáo0£R6§‚õsAíì̹œÆ}ºÒx_ÓþïZ­ñ}a§t[yI¤ò¦hšO*iO!<äÖšBšB„!Bè-z>÷õãa¸/F¾‹‡dlòg§ž¾ÒÏ¥g®½fÖo ѯ¢ý Úú¼.#ioèRm¤•od4Ólšktý3õËv½‚õ›š=ú/ß/o² eÞ§³Ò?ø󣯯»¨]cÞ¸8tåü­?QªÞÈu8ÓO“*ªbè…)FøˆñO°ÒwA¬ßI7‰„Í7q‰«—±¿LUZ—=ÿ¡=TÓº_ØìŸ'§õœÔ%ªû¡¿+”I^=bÛUÍàÌEsÝiI¤Ów‘‡ÈLS2nÃâ²* {7‘¦Ó\‹F-TÛ:Me3x—mÈØI”iî4t:ï¢!Uoˆc”¦v`M7QA¸Ì!¶YáwÜf(O$M¦>YHücˆ$Ú­öƒƒŒŽc°©4³†Ö |­`i³eq¡&ÉmÄ…·"H~'„Ýd¶ëØýy 4ÃÙû¼ñz“ß-„½ËÛ +ËFÛQ0/Yº=ý"ú*Üϵà[Ct­aW.‘=ªÂ º·ãMr„á"àf:? ÝÁ¾y—¶¦BiI¥òT%ŽŒq!Tò|`.ëdÇj¡V¼H4ø'±ºþ6Ü(¢žæm îÁyµñÉ|‰wÌ|táõbÊHÐxìßU}ŒªY©šv~]³·„)‰UÑÁ?Ù:ÛþŠ ÉÈWÊS#òÅ=Œšø7ÑŸF…m;'ø8¥©Æ?b¨÷ÜèòoOÂs+N?)ÔƒXÈûÇô´’îìÚøÄØj?ÀzÉœ>æ$2W¹Fkv=mO< ¢Â_ÀÙƒ}u-²E¸2Ê#p å‡Ec#"Ù&M‹8O‘(¡Noþ¡qhnBs‡Ú}G Â`^³sÒ¯£}£Á±‡šHÏ c["Ê©¾hy(šqS|†êL²UÕq+ UI£€n†à²›OJd¢:“î7¨}ÄM“bÉ¥=GUág1<©ÌœŽ{QÜm+íÜkÞ?rYÌn–×TMÇ´á_CÜr†É‡'’E¬m0"™wBpˆoq‰&v×ì¡ÆJ˜¦š§Œ•˜s%¨ßÞ‹¸ÿ!é¿5º¦uIðnŠâkçûgss¹q‘lI›-‰ð¢©ÔÚkŠpy·5ѸQÅä*†F¸¡X§©]Ÿ35ë¹÷½ÝÄÎD”J\“iÎ^wÎäØ;6‰¤™Uvp„êþt„>#—ÔpÓÞmþ ¯£=ñص(ÿr±5½þgìŠ-ÄNª‰Ý«oÜqº;¦µNË‘šdå•\ùÂ=‡Ôq窋˜ Ùó ³f‡)rî$á%F¼ÿAA×5žcqeâYÛ顤+,|ZÈ·è/­gG¢ÔÛnÆQà48\µÆ™þö <øî#À–4¿½2¥;™’N¹ƒ‘‡$¸¡²Q\>‚èý†­ÔNâîÎ8ª7ÜL•&e *ÆE‡‘*2;³çàšM'×"7h_QWÑžö¼›ÉqoL;¢l_H&&WËʆ$U³(\aD~¢ ÷ ¥ÐLK~í#nîB$ÑLì*vÚŽHâˆÿäk‚HþNSn½÷' &(ÑÄNCc3[‡_ô}Àt†º÷ɱ-‹ÉÄC}S±÷sûCޏƒ(ó'ö)uîq¶ø¨g%«õ 2ØÓìÙ–¨Vå>8+†›T!p©{ ´Œ¸‡m"lû CsÂ÷ƒŠ'ðp<ŸÙ ¼ÂòŸý”×w4<]¿âß뛓GË寣?ÞðlŒY—¤¨ØG¿÷7'Ëa1.ù•ɉ ¬/#/t4âÂÆqâ<.â…±>ƒQ¾:"I¤Ÿq„þGB$œœÅÂFæ0ößL¬£‰.äJ¾CâO~Øå XbZØû¯ð¯¼ô2N¨á§N Mhú ð¥XbuÍþÂWíêš{¬ÿbS5GÔÚt½ÆõϹP›J5æW8÷\±ã*KÞ3ü·çJC‚\Ë“»®â5¥|ñMúJ(Æ8iÇÙïã~ /ìA EG…e M>${9Í(Ú\J¹‘Á¯¦­Íˆ!r¼”ô¼ò Ždo(_Fo¹ãJ´„eÀ†ÊUÌRlA:=˜³¶Lô¹Æ,ìˆn˺‚J±ÙcQòzaó8´%Ÿ3"±r=½„«™r?!êæü»åyäIVÐOU}ÐLi¹nމî.£‰.¼4šñônþïÑK¨âÛG¯ì”=:“¸˜Èø>ˆH\¨k™¹· 76eú°Ñ“ßú)r¿ì’ÄÛ¸Ù–IN i‘¬r¹éT%Å1.* Ÿ†xñ$nÍ DðS{õúS"!ü†é"›²,àÀÖwî/2× î&˜“‚Y8š*%—hÛ3w¶q¼Ç›.GQà­ MÎ`6ã >ΣI‡ì ô¤oÔËP•)0Q\ZÙ'’k€Ð¾\"®:¸?!øß­ûÃo’g¥zly1£¨›ò—Ñ[_ƒ"FQ°HÉÅM“1Xdá Žá†v:A•³¡œ¥ºTÅp°Iœ6ˆ¹ ²ÖÅCqØS›}ÈG¤ù&Ž•qÈF {¥3Fܹjð]…2(MíFD#¶ðÚCh¤“#`[6C¶Zq,Ìâ ø¢ßqrO2>NˆÍÆÊQY|ÄÛýŸØÿ¡ï$}ʼn•)%H?zJy⿦qy¨‹è›âêIŽØkIhªIu1G‡á™fnbìbµZ¨ùçû`vfæ=v*CN¨m•¹Ã,.Cnÿ;¨Ú]>|3F“ÝRMˆä´œ‘:kHÆ«bä¼SI¤ÑìÿƒêÖDò˜ýNzpž»¸/"ÁžY}ƒÛÈI\!D8`àºUÏ_g§âLZSf}Œ§iòdGw ÇRäÑÿñ¬÷ ØÉº¸µò½Î) —ÜVû±a¢<¦Jwš$ÛIºÇEKÅÊVg7\JEê5ºÈæ8¦ÝðfºI ¦ý‰âŽƒO .%q–œ MìŽQa•>ˆvY%jâHîÓ‡©O)ý>CY}k׆ÝÌn?(¾Šø:< JchÔ®.ÍV2Q”ðCã8äM†T¸ÃFrÓ‘DEæ>Í Ì` ÔÄwK‹£ mW“{ëÔRó¦J™I< ì^îé~[˱<ÒS{̳Mˆæw2BÓ•ŒÜ—Ø}«ÒSÜ å—Á^ÃEÆgì(ÓU³ÏQ +<©±ØmØ1¾Q›¹®"YM0ùw#oÞÒŸQž¦îhàõ¾gܾ4¨j¢ùKè»"õM§Q´û·–ôž ÅÐR¸qP¯_xHv°ä%§'±}ƒ45¹¾¥µÄÌM6Œ®¢Y ŠÌcDÚr#tâ,©Óà6øŒ;¯!Ôé¾(SÞÅýÚ›7özìƒÜfV6WmŰënú§”=Yð&à¶CÌ¢iG»!îßwIÃ4t•ýSk°×D˜^¶öcWò]é$Ò/!}¾èüÉüÃûZ÷­ïv7<” ÄÈ—å/¢¼ì1ÿº¾}4u6ši®ÃuCUœHÎ{DkY‰'öðG4R7¤0iœ“ÕJ™îÊ—°ÞvDØŒŒO“Qùû‘Ÿ|ßëí½äó ´å/¢´ïóè«£©Æ “l›ó›™)Ò1-'ßÌcàt„s:ÅE&ì]™FvTxm4Óäð4©<Ó³>Ó†ù ŒÔwàn–½á5§L縒'̸ ÝŸØ!j¦;7Q2Ìû ÕIE¼!‡Íƒ¨Žˆvì>y&éql FÕÖ”¾Û3ìFWÑn%žBJÕªÅu–üÊì¶– jOq¬B¯‹ ñÑ9äÿL™,{3¤ƒÂàÞ‹HŸ-ÆõŠ5Zñô4­BÑ™„ØÔÇoijiYuÈt¥É»Š?´à0¹ÅÅ8<e4lö½†B­æ–tTÁ9Á‰®Tâù5‹ôaKìK™ð# Ý>L<5¸¹m Òc(ĸŸ¹‚-’!a¸:ò³aˤظù­^ËZX…íÄÅqëôw(¡sŸÎ›Êøµ¾û!Zóø/´—teÖÍtBÍ %n>S¾ñy †eì6"ÌíÀUM1ôÅM¡Ö9„ñ‚ÑZZü ,í­ÐÑUjèUU¼ö<ÁÙ†ba–ŒH«mŽ<K„™©s÷0ƒ‰ã²ÞÕ&ù†èÍqà1*­N¢ yI>zPÉqFü!$ù$/¼8®>zeè>äÞ½pч¿…“MäèŒMùH_Dǰð·ø7"6ÉȸËq*­Ì[vZ!‡†”—Ð'lží¹!º×ÿ‰µê\®Ð’–!^k¦s‹c4Þ× ÇiÁ…hÏÂ…°t)”6]†Ûu½M›]…Ömô¤æÉŒèÏ7òBÀ°Ó[‹§ô6ÛmñÖŒEÞt'“<ˆOñÏõ˜»©ÄnÐ¾Š¨¨K ?§ÜMßÑ⨺$(˜ß_ÑiU©uBØGШ,˜m `¥è,ßè\"¹î#©=Ù“‰ÐjW.ÂþEƒº³Ž¢ôïÂØy46Fá#úŠzîŸj!=ù›Å‚ )ˆlq1\RˆŠŸ"©ÃÙŸ5À¶e^É[6=鸳T¸]!•| “fžå5whÖˆÕ§ü úü[Ü[ë/Rǧ“4ôr„êú‡>ßÁàUQ^ƒ‹¼â-½4Wo˜Þ©Ë`è,ABؙ̆ú¢L äÇ!r¨õ÷‰¦ÌV÷wùfnQzÍy¡TÄ4d_P5ðdµƳÐÏ®?¤b Ç‘ž‰øEäl0l¯¨ŸÞþ<ýv éÓÁ¥zÆÏÉKÅ)_P7Üþy‚º ç®1/#a¹<Ýßϵú槇gØÞp=gïx^BÒËBôkÐìÃþlü͸à6úǸ|…åß1>ˆØú¯áïÑOU—d6óƒ×vÿ·ô;}jñðûø‹ñM!5s™y‘Ñ9ЕÕî©ß;ÇMaÝ/Á£tÑÛ:¨èŽ€±B[‰Ýsíô{ôíõ˜vï#w¡ÛôMˆ^÷/2:o“öZ¦tZÌŽ«øð èÒ^‹§ùýs£ò7ä0ƒÏò1õ>¡½6zÿ#¤þNQg³}™žÆ_c¿{*«ì~6ŽàtÏa×/Û3ñúVtú*Aùhü¤~Èë|ŽŸÈ—ýâ¡ëÜCǪ^ÓE»ÕoÀð7y¼O—³è›N¯\ž sG@tÇBtgC¥ê3¼t^¨]Âùôý&ùÞ³¬Î—Kø‘ÃcþÔë1Òcó²x3ù/·Ä+]¾YÉ ¸þ'Gò;/iÒÏììYùìýc:OÏs½ùî~¼~¥ÛÏÛΠºO—ªŠç%·Å?Z: ãR„D!O"HBh¸‹m3bnõÞ=üæç­¯Døvfï^ètgK©C¼w΃:²9ô(t'E¢êëdèô—Ï 0ê³¹ò~tüLü²Wo–ÛçåüHêÅ¿ìtŸ¼P×ý_áûÆ~{?t?ýŸ™wÚ?5·Zgíbæ~GKòôÿ× þžßJÿ9-2gž„'¢„!B„!B„&„!B‡ßè¶Ñ>ðÇ®±|îÿÔ'ecù‚7̳“ñ2ßî?+¥?Ê/ÿ#±¡_½gä³ôÌé?ÊÕ#óûE® ÷Cñ7£þ€Jÿ¿â ÿˆ†Éñ¦y™çõI¬!B„!B„!B„ Ö÷ß_¶cåëó`qó©——¿è;¦ÿùn¬EYVâEüZiB„!B„!5„!¹ -õ_½¥Þ½tþxL¼½ÿDò7êö2spû_¤¥v:$v Kb²³Å¡\‰…7:Ztì.¤Èåݰ®c^^ ÒRßI¬ôð„!4„!B„ ¿÷¡oªåÒ8}qÀòvx_Òaxñì<Ýi>ÏÏNÙåGÈ…r2*„;Qœ˜ÔM XÚ^…¸®držœy+Œo"ȵ…Y£i X6‚ÎBm´b*lwð·*C`L°’t7–}$!B„!B„ð á/¼¼÷¾6>N|Jýþ^ñmô<{ÆÕÛÀ ôŠU¤=ñ†ÛˆiÜM1 dIK4/ B.à<7!„V‰’hÁa™cCc6N¨\¦CdFq0=Ýè#ÊàÜÐUFðòId@Ä,œ„Òo‘s!J÷7ËNÁ6ZYEz‰´:Ìø ÙB„!By'„'¤h 3›7k½úw¬½ZÉCG$­‹Ÿ/q±} n‡öW‡-%¹\ Ã5Ä;¿s– ‚Ë r‘›DT7¾t«’«*Ä{#ÃCq‘Èt ÐnÖ”Wi]¥i^°„!BÀ'£! á ¢„!B„!CÆþa¯ˆ4€hÿQ¨Z8~×ÿmønŸ„å›ÿa >¹t|¨ê| Žì°UÐß®óÁZõhl¼•„¡VäBïå£kè Uº>Éàß6ïhÁ ™ šø¡B„!<@ž„„!Byðà·ùÑò ç¿Ð˜µÿÃ:¯Å> -ÂG’=`å‚›ò±ôãå¾ÃäÈ|t^È|s|_™§»}Ù8Ûî>uîuÏrØ~Tåé'ã!°ò~#ò‡ã£:¬ýáÕü—÷О›õÜ®½Zù""`5 ÅòËÓ“ÎkÛð¼]YBh„!O,„!ByõsÿÐý@kßàºÑ?+ \OØüp|/´rÚW–Ûî‡æ³“÷—{ÿѳ„tó§ü‡9_9Fßýè×6|›·äæ¸ïýÈq}Èq=Å#ç9C-¾ø rì?Éâø)Åüâb;ßç'4t?'æzkW¼ìï7àUô$òˆì|iu¡Ö:…ëR”bÑ¿_¿cûšv‹Õì˜ß7“”5G$V‡ý/'/ o6¡„!B„'‘W2ì4ïò¢?àý‘§‡î?ݰ…’´þ–œŸ»þÙŠì…?þ;àè?=~ëü/çÿ Ä¿sŸø»Šÿfs쎦:Mî´8þgï1ø¬ŽÆ¥²G%|N›áOÁ`l?ÂΫù:ÿsò3öd'l¿¤ê¾Öhz'öGæcä>tÈÒ¢øºáóçX>c(²ŠðE™2d΄!B„!Bc~¸}uê÷O¸ò汿K¢Óny{}rñf¾ ‘â1Lë°U—q¶ôOÂG-¤ðí÷Ó÷Šþåþãý:OÁÑþ¯—þßïg7îÿNc½Ž»ìާÚ@Påb!^ŒŽÂ¾¿³MÔŸ¼)þÇsù'9\Ñô?ÐRühê>4Õ…·<´µÿ¹~5dÛ˜m×ä¾Er:Ij½cªuΫ/>¤eQ_EÿÿÿôðŽHpúÖƒ7òkLHôS#u£pÝõéÿ·Á»FÒ<æ{¢džŽÉöeñàMÕÿÐïdæ'_¹ŽC¶^^µuÙß: ëô±ÉòN†ú´îÞwHËßòv:GE.¦“¨:¬¼ì÷QE–Y~Lx¿ÛôÎÿøÿÿ)áðöŽg¬iÙ7¼Ã¢4Ä5bPs1o/¦^scëá‘„®Á+d½ô$'Ijdšs;^…h“µ§×Ñu‡XuYy™’2Š(²ÿ‰`?ôÿ§oŠv7úëÜü„¦ÅL{ KPÆL…õ =SXä8ÉEQeúÌ¿ÿ÷:Òvú ÿQ$Åæ4œ!c;ñ!Ý?ÈœŽJdê>”y¡"j/²þƨüoqý‘Žþèà!BDð…7x8vÍç ³Ö>CßÈh<£,SG(NœõÍz÷×oBgÛÏûþ|øø×¤M‹tqšMsðx–ìI=Žâ0]jªß >ÈD²ldAU¸÷þÊdê›gE:*¾èÇ~*­¹”,[$· µâ·eR|rq6æé²òOÆG”I¿ñxÁ‰øý‘ªyÉ¡)¨HÒ|Éðj>#†x&ÉU+;„Ûmÿð4Ë´Ý }5oçhžX~ˆÿ&wx£¡»Öèr÷ò%ì5L^&C‡–ÙßC|} Þ •ÛD1Íx g„©iÆ´ñ‡XýJ^õ¤‰.""£m̳i—p«dñ¾™Sv¬†‰0{U¹v™–ãB<˜šÂø±ú¨ï&=…¹ѸàMÐÆÛyrfÖ…›wf³{›QLNcœ˜b89—y2MM«ÎMÄ)l"ª¶¨xO1ãuM8à–B]Ç’3!±lMµ–°ÄÔ¤ŸºJ21%è{‹éÿK—³Eq¸„ÀoMî#Нw—Ì•#u6 ߊv7‚•¤Ôcx&V¸¡ÅT˜CєĴÛYO“æmmþ‰;Üûˆ™í°º‹€‚Q}0~ ê Ó5[‰\ ‰zöøy,Œ Ö”&|½ƒgŸ<µäý‘¿]ájšFÓª£¸Lñ‘s}Qì~´µìÏ½Ž“o.|Ÿ&…’© +0«¹n¸±!>¡î7M²á¼pÁµ•dù›b$—WÿI‰º§ƒkàd$ÅžÿÖ†d¢d:Þ$æeÀñÀiYå["ô´çÿ$™z©$ù(Š JI–¾â²°Å8\þãâe%óóD³ˆ6¤Š2™5º6å±,Ûe‰¦ÓRÊU¦WçM}/ì ÿÿÿÎÿ Ó¿Î4¸Öm\_ü \~'–Ó|¦)°|²'!%¦ßLºé·Eºîl»ýj‡Ây; ÓLE¤ô¨&þ^älUëÿ§ÁoÚ(ÅÆ¥BäädžÅ9nÆì/&3εît>gäˆä—'ã/‡Àtá©ÔwΊ½F9Ã\tÛ¦}ï^ê>Kf¡Ç§bæKso¾ŠÓ„Mº`•9²;™_Cÿè!B„! 8¢[üÃWú‘ž¸Óÿ,ŸÀú¿c’ßbø;Øä|å¶ù2Û|ÿôèç]îx8ßÙ1ó8æ4êÄx?¾žü›g #·ÄG{¦: àë}‡Ï˜æüŸ#å\¾/QÝ:n€ŽCµiu±Ô,¢Š+/¸ Ù¯pôðzÂÞJÛ]Ñô!o–FÏÐþ÷Áné÷GÇz7àh„!BHcš?t~ßZM_ò4qgE‹ló•÷ ì<·+þŸôé^çH:ÔN$wûÇ1Ç1ç]÷%Çãùi¹_ ¾ÖÉö:kàü!n/@ýùß䜟’tUÖgpë>GûÇO¤G!ì+™Ô0u™Þв‹,¢²³&L„!O$hLŒÙàŻֈl¼°LÆSkRQ¹ý;Ǻn×xâ%÷’û~š!C¹ _ì~ÛJè†Xq!Ê.‘¦ßt¶ÎS€é¿D÷:Õïÿhs?±ÕðœÆLèR÷Ë09l.Ÿ€r§ÁÔ:¬ë2ó¾N·÷=Ÿ&9KËÔî³;çEŸ•Î#¹|ú+˜¾lmÍ”_†®u ¡E} ÿÿÿ€[‡¹³D¦ÛõÁäÔ„PH6ˆž››†÷—¼?¡cÜ8uØÇ»“âùx¾Ì_oÇÉNÁN_ƒ©>ú·þç_Äs¾3žÏƒº÷:ç¸üí7‡â0Ûã:+àêvuÇQ—œÏ3Ø^ItçPïtΈùìÍáÜÏ~‹ät5þ±Ô:çT¼Ú#(²Ë+ê¶ß÷à@â-ƒãëÃ|¾B1”…BRµ§‰›ž^Æ?¡møØÜ–Ì뎸¼ìÏ3ÜÇ!t7[C¸tÎÐDô'˜Žh{yî;Jätµn¡Õ:¬¼çvˆÊ,¿;ý¿[!ý_ÿ€Ú%®^½§ß/†š9£¢4PtX·7Ååïè[—„o‰Q[GÔÏyï/‘Ò::ÇXëSªËÎôFQE—æB¾ßà¿ÿþÿô ƒËÌuN¹Õ×rçàZÎŒßÖ^¯ìŒûÞQ”`P7º>"Ï,ßC¸›ö|¦Ý¢Û…ÿîÿ?áÿüÿ† Åæð‰ävéo­u¦•s++ó;[S^¯Ù¿äo09JB/+ÀO.øŒBÏÀïÐQýþ?ßàDô!4Bh„!<0AYbË,¢Š(ýrÚn)>¢é^¯Ã7<‹èùDÚPÍ'àLùm ì?¡d¾ ÃcìJÄÂIVÏÈѺ‹¯=‘ñ£„|W¬øš„!BRh„'×-#\›^¬´iÛ7?!(²Ò&HAPž^!À?£;ƒŒZÙƒzŽ¥#ê-g&&L—6É–9‡—˜!B„! ©~Gˆ""'ðVñŒo #ˆR‰ëËêÐÀ÷ò+EL‘”Ê%+BÒ…¾[hÛзü à)$ï †Ý‚‹Ï¹Uƒ‹JH1š—ȾE”W„¤’ ‹éÏWé/R‘¾à¶ß6øDí¾3Œ\ª÷9­}Îb}βæ”wÝ„¸Þç9g{GˆÉŸ©z´1{ù PÕ°Áë.´(~VôeÚª^nhĦiÇy>£‘×^¯G¥\ʹ‘½“ø)·ÂʉP¡¿÷9ë9‰9…ŽïòŽ)ß¹Ì7¹.r+#³ðrì>>U¤7Î!°›‰¿ý ÿaÞô,¢Ë,^IOX‡Áuò­¢S^=S3sò·hŸÐûßøÞФ.T'É×<Áž%Í/þ@iw’\oÝ|)À}„E¬_ GÀQòCoËx6ÿ©oôn#ªô¨²½#ÿð]éÐJzÔ>%ÝN!taªÖ7¼¥¹’ Am‚Æú–Nƒø:³­ÓtQ×ÊG#¸ŽÿzwûˆÿÓº’ÿ¯o°|"߀|¨Îmä6–î!»”oÿr¾ÿ1~,¢Ë(¯3Ëöú¯ôõæÃÓÉ¥¨_a¦CO4)¨Ä¼5Ý/-þ‡G(ªúAqÖ¶uÑÖ‡5Hî!¿Ü5ÿÙ.bð5ìãáÂQ„úcoÀÚ·œ­¿ÌW‹Ð¢¼¼·OoƒöèìõÑà&ˆB„ú1°íä"¡%£ŽpyCG–7×&7¼¶Á¡ý Heèc: ë´]3²u‘Ô„7R®ÿ ׺ «‰Êo‚P|Åð7ÿÀÜUÿbßìgÅéQe•àkË÷êGÁô!B„ðOª2<…¾‚ƒq++"›Ž2ÇRÒœ™ùm”Ðþ„›j„ly ntž…Ä8“˜Žÿ1=ɯ÷8·ì>`>‘†£à‹ýA³þFî!·ýJ¡[v(Y^'vj/ h¸?ðB„&„'ðd7“ä‡b щV;Û¼"ygût?¡´q£ž8!úA¸›ÿÜnßç-Åó¢‹,¢Ë€;ÏVðB„!Bà5îy´pÄŒØMd< åŸHþˆõ#ðÂ4v‹Óx?B„!?RGˆ×Â5ð ï˜>CП!è‡ ö·"ôle¢ Q¦Š‘ŒR ­P›?-§tà‡ô5¤ôÀ!B„'ñè* —|_ÓÂ4×-÷FÜ£å!·ŠCy7q Ü^ÒÏ="0`Ç‘9’¬2i 3~‘1!yfÁlÑáB„þ|¥ój*#™Ö:\#_Ð_<|4ÇÀ ƒå!ƒ#g߯6ñe¸ëGÌî;ˆŒjTT]JR—ÍVV[‰$ƒòpL¨Õð'—Å{{þq|7ÁH:Äø®£€i/œwÆ¡ðåŒ[¸œExŠŽ©Þg™:‘jTG€Yz)Kꬤö’eDfÿÆe‚&5õBו½vÇü®—ÁJA<Πù«„ià8gÕ44/‚_,`Þ8Æî2Üfb>gqȉ¨¨‚|~R”¿J}†-wȶ!«1Ó"ð%ssò· {ÆÃu¥)K£¬tÃO™4ðeõÎ †'Ë œÚÃo]þB›¹ždêEÌšŠ#ÅÛ(­JR—ê¯W¶Ã4D‚Sˆ\þ ãsÊCaüMJRu‘jÜšÁ€}Sq¿øH>R-Ä`7ñäÙÆuFyžæ *ò@)JRÿ~ÌʨÄ!½—ð™ùo[Cú…ôT¥.ˆæ4ñCFÿ ÓþƒP4„¸±óCà8Û|5y …·Œoßæwù nÅæ=ôÁ‚¢—Æ)J_âoGâeSƒqŠkÁÀXÞ[áú)y×ÁJR“Ì醽þA£ià/®ÇÃq•ðÔ|¨ÙÈm-ÄÛþÃwúâ+æ{ëƒE^¥)Kü‹È& U¢Fòß@ýMôŽd¸#H5paó˜ù |&—Ê †òÛÄ6“vá~3ªgÃ)JR”¥)˜³r3DƼµòï¤ýUDDGƒäj&®a¤>°ù±¯ø(>Z¹ 忈oÿA»Œë²¾cò)JR”¥/óÊ+–… ÓBPjé8òPž[á¡È¥E ê!.\H|äÇË’‡ÎÛ‚Ü'XuΡyŒø6ÓÒ”¥/ñ›è/«Œé3p7dE<ˆ'È,¡°Õò¡ å–óq¯îÑþú£ç´_ t×CÕvuŠ+2FBdž”¥/‚ÿ?…©V¤"hà‰ f—Evk4†I’¥ ޱe•ˆi¡”5ªÍn—ÌáftšCcÁJR—ÿ„e- (Z¥Ò#‘:Æ•s#™ÖÔ&ŽÉé1ÙmÁÁ¸•â.žB) ´ DÕžŽ^uJ_ü&2¹-J‹Ö‚H#‘:! ¥GPëFô¡Ö:…ó+™|8C©Jˆæu´ÍEÕ Æµ‡°ôG¡ TÌØÜÿðhFW#¥©^”`)äG-¦$âwçSJG@é…rÖ:…ó;ŠR”¥Õ(·Ñ$8T)´"U:›-o˜uΦ…†ëò q61ÁƒJÐòJ3|†÷ó˜Ê+–¡eêI$I4Ç2óëb¹z R—Ðî2ÀAòE¦<§ µ/!¼hP¨˜ïkð‘qZC‘§áÜ?å‘”Ye^¼A$hˆž s/1Ôð<´V…Ž¡|Ë;ŠR”¥õÈ®Cl£(÷1¥)JS&JðQGqXA-Hq…¤Ñ£ì׊{ ?áE”WÁ’A áÁW€‰'EQ|ËæW2ŠR”Œk˜eÁàMhÂßÖ£#+R.dæÔ«‘tQY“'CZM¢HÖ ÇóÀª˜7GOZ5L¯‰¡E_€A$ž*ˆÒ‚<Ê,²ŠRé|ç&ÉïPè|âßHó!"Š,ï#™ÔÓÚ^B9kÑ^އ‚ Ñ:·KB/ Ö¢ 'Á좊¯pÍžQ¸Å°ž ¥'‰3üŒ¢ü_’'ŽA$øBË(¢—×,Þâ›v7­7!D!2-§@žZh¢½éjÕÏDx*IÈ‹ÇH$’tß"Ê,¢ŠËèQŠZ#F¼k%ýBŒ¢¼/$DO"­I>e”QKôxBÁ…ÄÁ‚¯¢³$~«LøèÃJAø~Š++)u¥z+ô‰ë¾îÜN¤ÖÌcUgI|KôÔ!ExN5'‘JRñ>Š+)~‚¢”¥)“$gKÀE醔ÁJˆ ’|?E¾{¥.OÐ8=ˆâÝ´yúK„+Æ„DDòiJˆð¥R—éø0TR”ºÆW‚ Ô$"Œkt¥ ŸYE¾­ø•kÑÏ Fš'³}(VÆþ%/Mˆ""ò©W’YJþ¡ƒ)JS&HÊÖ/^H ‚H‰â¨‚ ð{,¢ŠËô¦‹EJ$ÓC‚Ù£ô&´OU„8šQ Ĺ›žBÀ‚M–V¿aØ_!7#¤.QѰ)J¼`Q_Õ0R”¥Ó$eiY^A¾¢fÏDÔOˆš–`•;—Â^sÕiÿÚ ‚Mrÿó‚øÇ–pa¾‹é–Púò4ÃÎ4à$ù¯Ëj¿Ïý{Ë>ïå’ª¤¢ «Ž{á¼ûý´°Á´PAvÒA½{C_M 4Ë Îw}Ý<óœ÷ÿÿÿ}È>{.š©Š/†ë„± AW¬?ÏþÏ|ñƒ -¦¹¨²˜ †z(²¸àÇ^wý×ÛYqöÕàÇs*Ž{ï¾óy„k‚B¨ÖcMP˜AϰëêEîµ¾»Ì$‰ c ðs›œ°×ÿµÿ½þÿî÷÷Ë-ª¨‚9gªëjÿÿÛmÇ\q5XE% CŽ0C„"è uë®ðà 'Žÿóÿ¨‚@aDÔ3úaK! –]ÀCf5)ºß_{óÍx÷ìpÆ»kþ ’(Ž{$ŠŒsÏ U¦u&”*Í0€ |óûŸûßÿÿÿ¼÷ì4ÃL8Ã0ARAPAQ5ÎñÊ^¶ ¢'Äúè2@–Ê€Ûë¾8¢˜ãØMsO=ça<òza$ý÷ß}öÓC 3ÏwÿyÄðA 3Áuçß}çyÇ_aAQÃO=»ïª94â+]„[yŽüé7–Ò¡€ I÷ß]‡ß}öWc=ç†óÏ}Çå@Góœ´óŸa¨ƒ8æ$A<qA6à 5Ž8 Áà ƒ8 0A”A' 1€@ƒN$%_]öÜNø 1„PRÎ7·Œ8à 0óáƒ.±Ï<ã†)ÈíóÏ0Ë$‚ÛãÂÆ³Ï1Ê˯³Êb,G,°Ï¬ãà 0@=ltÐAÀÁ0¤—ó¸/¡ŒrA$@/;ïÏL°Óœ0ÃhÂ.Ï¿¾sÀq×Ës@ò„  08ÂŒ §¼RI‚8éˆñ/¾+-¶LóšgÓÃBC ÇQ€0A »÷ïÏ<ñà 0Ã'Ï8 ²ë`²‰%¶øÃ ò‹=çË\‘Š4G0ó<³ï¼ìÐ0À óT>xÂLøƒ)wêI<ðÃ<Ñ$H„)4A¼}÷}Äãœð®üòà 0à 8ÿó¯<òË=Ç 0ÃL9ÓÇ<Á!Ì6ð 8°ï1ã|B†“OƒNo‡>±ÿ°E÷Ë)ç$PÏ ϬsûßþóÌ0ó¿ÿ=óÿ÷ÓE~òË ó’Éâ‚î¦*­´°@ ð‚%gÎ4óË/­WÎA l£sÿÏ?Ëøî²`ÓÈäîú„—0ׯÐU÷ßOÏ~ó wÓÏ›žÍ<üµŽkë¸ÓŒpvów—d sˆ ÁO°÷ý;É÷E¶EG°ÃÞiÿÏúÃ8áÓ óÛßÿãœ×ADVïчiwÛž(wh^ó²”·±Ï43Ì H¬ÕçßAÀó=óÃ0Ñü¼Éǰÿ¿{ñÃÞAßû?þñ·ÜMæAW™]ç]÷ÞÿþøûÀ$)OpqÜm´Ø/Õÿz;ÚÑÍ óÏŒÒEå^†ËzDa{Ï> _¶GÛ˜„òA(Á˜´³ŠËÉ?ß@Cèã0QÍ8ÃÏÿç4Üsœóà ?í¾aOuPq4ÞóϼžÄ óï¾(.¶ËO!óÎÀ<¾eA5€Îøn ÷Ð}òÜ”<à 3<2â‹Âwüò0›ó†«‹-—•á«8Gëf oÏ5Å4ç¹î3¤W"­¢;ï²Ê+ºFDÀ‡ýJ3€¾éoáwÞA„à ˜>[ ,…󦜀ãË ¼Ø³Á)_žZ3w¤RÚ·´ì–$OUÓîsù„Ž5*9ï®óÌ ,²Éó@rèRþèc›7ØÓAÍ„âHïªkT.”4~‚wEºÆtä-©‹Ú`ŒoEІAžÁ¡L×.\GJ¦"kZôo·ÿÿÿ¿²+ô\u£$÷æŸ}ïï óK+˜Ë>z¤–±Ã03Œ®dÐGÂzêÓt“Bê6@¿@þ„LØl!M-cŸUØ÷™“K”Ói¬0Ã&ƒ?×\ô0ÿþ3ÿÿóÅ3„„•ˆ!Oº¸$ßû(šd\00޶ûš6¾üÈa¶÷–(#µ $¨hÛnóëO=õR A%µº¸wó?<ÿ׆ÌXW÷Ï¿¢qæ5ÿÏ3Ïÿÿ}5ÿ°¶«¡IÇÛ“dèᕞt¾µl”ÅüâÛÚˆÉÖÛ•/ãò–Â5µþ{%Ý:ÑTIÖýóÇ,óËž ýðŸ’AA~ÿþµ&óßÿ÷ÿÿÿ}ÿ±Üà°Çzl†&êÜê{H5ÕëϽ˜.VøÞkí b=¾0ü¥ÀÒ;&š•eì3AD´æS4ð ñǃA1ÿüêÏÿÿÿÿÏïý}œþq9­—|9jÄ­Ô±úë?•A¾ø+4õ‡&.þÒñŸìȼü="jsÔ!‰Œ@Ó÷4õW<ÿ,2@„ïX iúéhÓÏ8ó³q¿8Ê~ ¾Ëp'ýA'˜J🳪çYHÀqHâv14ÜU¿NKHG‚®â²DKèI´¼õÖÍLûïÿÎ=`eNçk(?þÿ½n0ÿ?[]îw"/PwG玀#X# p…3>;#IQ~ƒf]ZE“\S0úÙl‘ÏûEì»H–D÷’O«FS PSÍMÜ…C?;ƒûïãßó}îÞN™_Õ.zºçª8ážJ ‚>$4á<£K0H[ ƒ-ÎHÿNà‚Kèyò¤á/õÇgÙGø÷Í ô@úr`aX )¤Ö-ŽKï½ÿ^f…[£á*Ui«lkÚ.J6J”ôZŽg@¸€–vˆ`t WçÑhàB­ól|]Š™ŸÂ®1‘°0ÃMûÉÞý%ë|Ò…b )žYáž)o½í9ZÓÅ$Ã]•h†³Æ!US.2è'Ö›ALêi®ù'‚&”«GÄ›¢Êá$«À‘=8‰kNëE,âÞóÌQO“—×J´üã0Çz#¼ ÞÀî â­ FÀòŒqߨ\3z[‘6+ÞQR úx¯¶ªŠ9”_Ë7ìOÕDƒÿ"N[(À‹ð‰M6„ì=Û°ƒjd@^l®x=ëá¼3ß@6HŠ@Àòe,+0?Ú„¤íuèÐp—yLª¾NÍùt„#æ ýÕÌ)ãè®{ÆN½¶zB&u([%yWÜUck»˜IŽ AqúçžMâœõÝÅ'‚6»ºB'í…l áF:€*Í/,â¹&QžÃ­f29$@mý䢘Ø/”÷xdÕ_¦¬DžML®žû|qÅÙ©FQ ñÿúúo’~RàS:T5DnAm(›AàgÔJqûîyÞ+;F»ýò°°K4ëk!%•Jù/ÔÚ?GóÀù(Bì=œ}ü°‚ ugæi„ 8ïßûጱæÙ£v€S4Qû¤’ŽºÓˆ,‚÷ üߎpÿœøã´Ÿ{c‘=ZS‹j§¿à]¿É54ˆ©û§ÏÑô¹ûXæ9VÜ-¹h®;ûï®ÚÝ$lI–æ7€w¹]ˆJæ -ºÈà-»<¥ç^9ûò’sXîêî¨V­PÕÇÖdÉlæPò™)C±ö±RßïÈ6ÛØPM„¤0}/‚{ßp Q¶å6šWb]˜Ç{‚G0•ÅJ´<ùÎूZž‰¾»ÈpÂjæùÿ›=Ó`ó0y¿¥ªwÛï¼`#Î]Ø…} Le¾­e`FÒ³ô¤·¿yéÊÇ+L ®3Ž8q)³œ¡Ë b8ÓÏŠ?Ï0“ëø¬^²¾HíºåºžcþhžÕ¥:‘²ûOÒq/¯^M… 3£Ë‡þã¢ÅVºT#°K›Íåñ.0úéúÿRÞ@ò„ cÐx€Ó̰ ß³Ï˜ç¨r˦«‘a»fU²»jGF·óÏ?MÔ€‚\(giÅŒ7Ëè’èÚq3:uYCîìÁ‡ûO?xuð…(rÂ0Óˆ0²/{Œ½î½Óˆ)‰“ÍŠíµ¶¦ùm…ÝI9[qÿîUUŠŠP<}Ú}ƒXë킞hFêïIÉõ8ßðÃAAD (@‚40! 1óÍ„`†wÈ»‚íK7¡ ¤‰ÐØd ^SA ºJt2ø}óë>É© 9Èœ6¾ÉåMíZIG°Ã öÏ<“í4ЀÌ< K8¾9ÐóøÿÞ~’H©¾+‚„a¨šÈ–§ñ–ô*†‹&ª%mtO>H!­¦ÄÇÏhßþ‚±ìèÀÄ‘…l ¼ÖMó—Ï=9ÿ,òãÛ}ÐÀ )l¶.7ÿ “ïtÏÄà˜EÝô‡ƒÄ]EUM'[WF PëQ„@Û£†M ç-óóñŒ<Š0ÇOá0ØèÒ<äd ExÂÌ7$·sçc/M‚XTù.‚[©Y&–÷ò·2ÃÏ<€ K(óΰ]¶ÚSù botÕÖÖW¼3†æ’A„ð Œ +‚ ÍÇ<,#*–^Óz ”|\ÙûÉ zO~ËdŠScޱÌQÿóß4ÚAgz ×Ö@žk!‹©Aˆ,òþÃÔÐ}ϸÂiˆ_$ÒRCÏKÿÃìOÜ¥ýC/‘¶3ãªA'RýÉ}ÙúÈ}ÙA¯Ë/õYî[“iáö0E8bÅÆ†øýŒ"O¬À8ÿï\ATÑǰÀcxA9HMÔ Ã¯ì,€¾@h']µÃºÅ]ÏX?J(ÊýçÂ4€Ã,rG³c4éôâ)Ï :¼r㦞Y>oŠãÀYg:;ùAûÞñ]õÓSÏÓšwI9%ö ƒ ¬‹ŸI)+¬ŽxÅaÀ B3ø8Ïo:Ä`Ì ˜I¦XrÊ"¶JgºÀ$ ýwËíøç× ¯¶‰¡ŽóÔÁ*ˆÙ>áÛkÌ0þ‡}4Ë-ÄÑ&Ê­AÉKa=\ýÝ¡J$ØÍåæ_øÙÌÑ:å%q“r©€…ë&°ótÖêG&%Œ ¾€ # ƒ³äU£ÿ¥Pi<àCôÝóÏl'k ¾ý%Ͻè_ÜÞMK%þ³sUÈÙ=”ìKÆQSƒ ÁD´Š,5c­1øÃñ3ú0hxÁñk~ìl¯®™ÝyמñÄÑ®Wß=ÂL¿â—¯ehc¿¯ÝEpɋ̴ïdGóhîÿ­>ëN<ë|»ã]U{Ïò’:)Šûmž‡˜Šè°ãäJµ6í…¬ÜÿÌKH!çO5EîÁËÑF$4]öèB ·…Êj÷ÕW×ê)šöÛ­0:\{A÷SÞã‚Z)´ «®Ij’/!¢IÙèJ$AŸ} (sL 0å£ÿóÖà|%4GóÊÞlÜ¥3ó9° G(ÖJpOä?¦©ªïGsÿ ùÐE ëŒ0ï&–Ig¦¸Ž°Ì4B(×7Ú¢ÙK<—Y+š¯ñÏo÷âZ!Íᇺ„_ϼp4°œeÅÐM¬ †üœËv?ž8ê¥RߟµÿŽ´Û¨Å€… ÒW~5UfY4úÈ,²¬?ʽ¡¾Ø•uÊ©%EºÜ>y¹€áÁC=€³ûÙB 7ïlsðäZYFÖpœWÌP޲Ì´ãþQu ?ÿèg¾ò F;‰só³ëþ[{‡Jµo-?RºÞ…›ô©(‹0ùÆÏ¾ÄKE cyÁÙ"Çú÷¬1É!„‰yS{ï÷tó¡Ž"lsŒ0Á_ëÿîú °Q§öÊWïüàŠl0à 0à 0à 0à 0à pà 0à pÿm0Ó °ã 0ÃóÃWØ£³šB|iãÍÊ(0M}^·ïD4M;ÛÙ뵸Ýëö¾ëoŽ8 ‚0à 0à 0à 0à 0à 0Ó 0à 2Çc°wü¾<þt!ĶŒéaì=AQE$<ã¡¿?¤¶g,iXm†4ü$ÓËïúX"‚ Ã0à 0à 8Ç rÏ,¢ˆãå CRã„ÓEq ²O<²Œ0A C&SM6µm'xýŸ’š÷ÁuÜ-õÔâ©(„ <ÂN~ðÔ}o4,7¾ÊwÃbƒ3ŠøÆxê °Ä óL.è8òN8à 0Ç 0à 0Û ñÍwJMŸæ:ç—gC’ÛM& ¢è,ºÛ,£¿»Ëk,Çmwç?$¿ûÓdc¾Ûæ]Oè_E×@A0ÓËòÃ<°Ãpà 0ÃóË ~÷Œ~ŽÆUÊ'1GSßœ€‚Ë‚ ažˆ'¾úãªë#žˆºá]㚠p…ÌÅÆ¾?IõßaVüò²â ËÊrÏÏÜù÷ì<Ç>ºÓÜsÖ¹Íö¼ºyéÓM0ó°¢sÞè$‚9Ï6úü²‹!y‚ í¼9-»­8ök9Ž»·÷z,û+ïûØ¢“lå~Š ê‚4´oˆ A)QQJ_¦^Ôþ¾øçÙsZoN┥L¥ ¨‚¢ ‚œ „Ñ€øÁJ]WÃL,Bf„dEIÕõ¯§ÅuoŽé¥éÞÂãÂú|XHi¢u'Ù}çŸDWšñJ&+Íbò/¿åòMsëßÕódi1$œD8p(*bQÏÉC’4{6D$Š…4éá®Ä'?N‡ÃÝ"à’ÐDJ1rmþR!ìÏþ͈Š"ø°ÜÝ¿X¥tn?+ú¾l*F„˜iFG(›£oànˆ)S’ýŽü‡ ¸HV±Y_±6R‰Áºè›)^(ÝE¥ÍÂbx¾å¥Õ{×<i¦7”JD& Ô4¨‘ E„± èɤD%*¡*g± äÕá¾ô_3º]KºøÂUˆI4ÅMØ—Ù>ØÖ V„”–É“%QH›cQ›Oú0Å!S!±ÅÿbD‰¬ÅâüiKÿ1rE±ãJ~ˆ¨ùð¢—ΆFš ‚9¦ŠõÒöWAýÉâ6Ar†Öê&©ðDb N³„D.èiQUbØš„‹Ü|Ô;„¶ŽD‹ÃMkŽ‘ö¸xwEÏ S´'já¡þÉLCbäãháR*¤TE¹‰Z#Ù"IG±BTDÙI„HŸý‘DPÀ¹{. rŠ‘| ¨eüþ ¨¥!¾¿‡‡n”åŠñCb²´ÆÛ ¶„B¯a¤.BM‘‘ŠôQÀÓO[mýq\w¹á¡ãoeU {>X’2l$" ѱ$Ç“ÐÝc-_Ò>Át 0;ßO èÏ  Zê\]HØ“~$ßIÌFQenGcM PÔQÒlŽRŸèÿdYHcrO£DE°IiT$›bIk‡³’¬R‰îU¹TJøÀüËžûÃ$'ÆÈT@Èi*>Dˆi^ 0ù¡“‚lë…ev⢕pÚB²²¢ZS}7ÆŸ*BåiBÖúO R‹Û–BbKrlGÙ‡Àøó®t®•.·„ã+(nâ²²¼R¸Wõ«Cà|ÍËÊüÅïêWUôµÍO£|ù]Õô—ÙÌFB2™õå_j}%ôsˆ„Zj*)J\”QEd(j±ò»«úB˜„X„Ó;œXøtÕ¥é}%ÊìÆFM0ŒŒ„#Za11Bž .Òó|ë”>pˆB22<##Ò G„CHiÁÄÊ„Ð÷XmT&°™J\=0„!Bi„!4pÂáØ\µ>“Ñw)qKšÊÊÍÇqÈÈôB™˜˜Ûbø¦@صLsÔúO)lˆB„!DDlQt‚¤‚=d¢Š,²Š(¬¬Üß‘‘‘‘„! ˜‰­rúÂðsÓXo ò™"Ê(¢Š+öVR½‘‘„!Bˆ‹])JR”¸R—ÄO+—Ð.4-usé?Œ¥HˆˆˆBiºiJR—Á»#77Å.šR”¹¥)JRá·Êå̹ZV…ÄÑÏUò<§Š\Óv4Ö7#ôFFV 4G)Xìjž„ˆJ {…ED ¡ºüÔºn+%ò?2åiZ8v/DCd)¶  šÂ¥HV6ØÛ…ecl¬¬ß„!:„Âa$Â!ápüü«‡KãÁ ç¥)JR”¥)K¢<ÆB„!ˆm›‡…ÀüË„ÖÖ®=%•ÕJR—ÅLB„DXˆÛÁJR”¥)JRè åp?7=w/—§“¤¸ÃÓBˆˆ‹M)JR”¸¥)sKŠ\R”¥)J_+ãÏÏT&_:x>ª.iKôÓÌö]2ÕËK㤾tQ\O¶K ùjoÄ>_QÊÊÙBi„îA&$ÈB„!B/xؾ™ lL¾7G—v„!ˆˆˆ³QK¨’ŠÊÊÊËÑã¡ Q" «“¢œòFBbclÒ”¥.ŠR¿ú9 ±¼=ÐÕî&8ê^‘2çb¢”¥)J^„!5ÂèøhCH‚pŸX᫚êÞ”!Nó-I3ð?,/Ùþ¼"ËY|jM—b}.¸B?LüÈüpW²½Ÿë%ù‘ˆº5™‡÷r¥z?#ðÎW±/±{²kÒ~D^´]¥ð_"cQ³U h}2øé|©?Gà~G嘿býd?ð ‹MÍúU„Ú=§“±£ð?,•{+Ùþ³øz"ûº&/<´¾‚?Dgà~G嚯gé‘ï1ø‘ø"/á!›$ÙakO-Sï1ø‘ø E„aD D*•¥eQ ai“<ñKüde¡É # ÂÊ(º8ÁU¡*Ñn44wB”QZ Fk,¢²²”¾U¢222 4šßذÒ•ž?YôA±¶ Š+)pÄ‹t†&Ïì‹ä„Ä!V„&iJ7™­ñô‘„&&j*)K…e++Åó Ÿ9„!G’"f”¥ÅÅð¦ÓMkgÑš!Bj¸¥ÍìLFB²ADÍ)JR—ÅKáCÉ4Òz^´ˆFQEx.‹Ø„dded„ÎÆÅE.)qzÉ¡ù ëÀˆA†÷xi‘‘“EÑŒŒ„!ˆˆ±JR”¬¸¿E4ÒëEº¤©d¬ ˜°¥ee)_†E.‹Ð¥ï_##&ÑŠH„!ËÃK¢}dóß%Ô–Je.›ô5ô+º²üŸÿÄ' !01Q@AaqP`‘¡±ÑÿÚ?þ\ÿúèˆ>GÈø I„vOgè®ËÉÌøŸà|ö¯ñfÌ!BAÈø#ä|†!?EvWyJ>ÈøôGþ‚a2Ba ‚IľGÀ‚2…V@>GÀ¢>ˆÿ¡¯cRI$I$e ,¼¡_àWL¢2y÷%.ÝØ¿Ï„!ˆˆˆˆÂù#å–ªþ­ÁJ_õ/Æj\”¥Â”¥)K…/û ‘e¥/úzRç¾ÿ?w(¢Š(Œ„ddd##Ù„&„!BŒBÂm͈Bf„Ø„ò¡1„!Lžjø äB„!B„Ø^]þÌÆLИµ°…±|(O c6gL«4Ù<è[Ô¹[©è%.1à|‘òÌ'ý±ö'²qUöWev_gØû²D}Ñe`Œ„&åÚKÈ/^ëù3ƒD&õ(…„Þå²¶éKá ’gƒD& #£XQE6 ¨Ö¥ØWEt4ÿš[mµxP›7ÉJRáR(˜Ð¼È2]øB`–ä!0åæpòSL&„'‘°[ü¶Vå.ol0$J˜Ñ4›Uâšv4ÆÒ圭qm&“já âÌf†‡žf„Ȱ[ï”/ ›Q6Ÿ>†Ù.â¥Ñ½®z²)¥6Ũ%MuMóÈí«e·!ÿ¡yjOØœµpJEº £ÝŒáN>•ÙòBk§ƒê†hn Z£n¼È3cF“á’Q29Mð‡­Ç±éXë¸Bc1cCCÌñ˜Âa Š!LÓ+åÿ6SDDý"'ÊD]Ò Ò|¤È¶"‚”‚EÂDVŸ¥$K…ŒÌƆˆMˆM”!2Bgcóº(ÿŠü®¬|?=Jâ®hÆ•·ÃÐ’r5ߣQ®”OúBW/Ðñ¦é¢áÀMÙòC´¶˜¹¤ÓM¯ÑÀĶk5Q>;ÁJG ’W(z|ÐÝànœŽTœ ‹ªÔUzdùž†Q÷ Ø¢-XmõXèâ'Èj;|±çÕ…XÞ‹…ê!ºèpeŸVW„âtZi4DÚG îʘðàäÔO‚täOÔ’D#¡ÒT*ʨÔkí‹ SâØŒ–ˆv=șؙ&­].D ÊÒd z«”jé4JÚPmé¸I¤°Ö°|bÕ"“DGHjÔ$7:Ò!;ðB!Ý•0H¸CIò‘Á¢:DU !£t4DB",&HLè[Sq~o $iˆvé*T¼p9)T"*ô„Uè{?ø{t¢ˆÜ—ZSD|•vLn= p— S@ýô×eEÍR•íL Ñ1¢˜Bmú(KMÖpóx°n&1&¤k±Xå¶9¨”×áÖPn&Æy¢*9JE þ&š%%ÉKйZ£9èÑö•´ÒŠ{I(&~¨þc¶ŽIˆõéú'1¹PÑšDë·Di˜ÆÉ…6ôü;µ0˜Ba6}!kÆ™ù,&£½  u±q_ —è.&‚‚¨›©k‡Í $¢#äI&4š#NAiÃ^À— ($’ˆOè.¡¡u‡ÁI""¶!$¸[S ’m>Ëš¹BÞ¾tðø[þ¹®òØX.6l4ÜLäƒà¹!¿ I—W@’Ñèzê5.틲IDjp¡ÒƒU5ØŸ‘éÄ…:ŠQÇØkô¨í,Ôº M •¶+k¢kò5'³ ûåð¬j8…È . ¤F7mù<7›ÿËÀò^Ï0˜.ôŒt0Ñ„´Ô-BÚR!D’¦½‚E‰ .H)ð†¹¹XÂ:Š[J4Tm¦‰’1ÌNZº†Ò6ÇÈ*§;ÀÉÑ[D¼‡à œê6ˆŽ‘D&ËÆmMŽkð_åS €Jȧ¨(ä6®CvƒüÓ†°}?ôt^¿ú+Z¹cÄѦ„'ÿ¡}Œ{¯ƒP¿†‹òÎ X¦ÄÉ·y)=ª¬oò{QÆ­$.$4ÝQ Õ6žÓY¹Ï≄!„‰–Ñ_"º(7KF¬LÐ"5ËC=!2Vß²$YD*pÌKzV6Œm_F‚ûàuQ¾NPЯ¾ F…³j“¤ws :i2U9Ó0 Û5V¢Î¶62Ñ(Òu=K}C[*î¾ÄNO"á¿ÉšîL“„²ñÁ±¦Öj5`’š4`žM ®Á)蕇d„^†‘Rá t*%qj'(àÃ|î ³KF5òT½ÔhГ¯ 5œÚ‹4Ȱi4ÓBI(³ElðÞIª9Îüù^Ì!3¤%›† aX©ÖºùKÃ| Lk‚£œ ƒæ íûÉÁkH2 â.YÀHj =Zƒ éiŒM¯Ðf”áÈš4ØÖ¬HõäQu5zÿÊ9Î;ü›«,È–Ç.ø•Yuв§ÃÁ¤ð» ”rþœûåä{ l$%²°m.XÒ.Z‘ôEU*®/  Ôõ[Ž]âÔ£J‹†%¤!¹4f áà °I¢ös.‘uÁ~‚g_jª˜5<$ê 8­-­¯ǥ葯҅Ôr&ÞÂVï¶ÃR$Èÿpÿ·’t#I•áÀrâáo>2·°³Á-¥‚·£±Í§›d›œš­ôK©èr)ëÐô­ ¤¬cÒöÁ+±3Ðõ#Ö¬Mú4¥®¨æ¯DBàk® ÜéôbQ$4m7Ê"Óò€çÀ‘& æÔI$„ºÚŸ$$—Öˆº& ±™&„&~j÷&W°¶fÊ û'Òt:7ëC{¹ ^¡|5øRÃñ„ÓI¯6ï±àÎKd»O«+Ø[ :ÂÖ àc>ÒID)ºàŽ‘‘éx«Ëg3žÿ&ñf[oÌ^CÚöü9oéÊ{5’m,_ñ/€Ç²¸ü·ø7yâ„·Ÿ™|ZTTTTTR”¨¥Ã×ãÀp[¼ð^áøsš-.¥R²²¼5#5ÂA|áKwž xžˆ ŠŠŠŠ\‚ŠÊÊû)Y^5—ÉJR”¹)JR”¥)Nl#†ÿ%‘äYy! xk·JR”¥Â”¥)JR”¥(™cE=¢«)HH‚ŠVGdÄv3è%|aJR”¥)JR”¸Ò”¢å˜>Gy¦^BDØ›+Cr‰%ŒŽÐйÊN‡ÔÓBl'¦|Ñ KM#p§è·/FªŽ=^ ‚eŠo„27}‘ÓÙÂÆ8)ªTJ“ÙTAJR”¥)JRítåƒåúqßåÈòqf\xKG°¢K¢a)EÃR;N ¤Xpè¨MEB$œxÊYe–YE—¡ø'FNŒøŸžQ]•ÙûÇ‚""!4‹ZÁÿú8ïódy8fâñ˜œ»e–QDêÏ‹>Ãè>GÃî±]”?DvÈú}Ïâ|QñXÄF˜*** Œ‰X(¬¬¹W+Â8ïòou/…Þ Œ3÷YóÃ|ÇËÿÉB„DDXTTR”¥)JjWޏG„!< {Âð—‘í_Â{x.ñ(¬¬¯$d!B„!1˜Bm$Ù¨$pzš¡Fªx„!B„!LSL_¡q¼øy^F™£Ø›OœÑ ]¢ ¤« Ù \¡ó ÈÇÞ~= õŒNr1ªmî½ ½ð4pÆ¥°ŠÎ B±Ã–';D×TOÔ^©†˜Ú©)QV5d¥)QK““è\-îL¯'o|ëmó‚Â7ÿôt£~Œ­&Ü(MF¦PÔÑÑ ¬nh-HÛÑM£eR{ÔHtÕ­ ¤\Bš#à†ºaJRãJ\.ÍÆ”¥È(l.p|¡q½Í•äàÍÍ„É7„3JiÒ4ð5èÔŒŒŒŒ„&!‰EE*#)E++Í3ÚßäÙ\¯!àÃØhŒ¢²!ˆDBd„F™éJË’22„! º %‡§“.WÓásY—]¨B›Ð„!B-Íhƒß>™^V©•âã7ù¬O‚aBg¾4Þ¯es¿ÁoÉê…àñY d‰Z´E)K–ìß9åll]þk2M2l ™bv2.±)q¬¬¯Í¨«³è}0¤žÉ}QYY©©œ›üò1B9¿òð¥åUÚ>ˆúa~YúÛðEù)½ÏÑ;2¾àüçÞI$‚"",4/Š3K°kàQTæÕ]£è}ú~_¦.ÿ$î®ûÛÀù ^ˆ""Ø« …)Kž”¾76TByÐÆ§ØøVŒ´L'бûdvðIò :F˜ÔTR”¥Â”¥)p¥ÏKüNL©²&W{ê…rTRãJ]‹äOµØûØ>Ü?‘ð†ýû"(™KŽ”Ìü‹û3 ±Wh}Èb}˜ät>ƒà3}çÔû÷à1ê„ÈM<. ë™õ¡V®Ï˜úŸC÷>ê>Gâ2`}¡÷2¾üô3V¸®WÃè‹Áýƃà|þgÔ}ãïC\“<þ@܇• /"®ÇÖÂ}žÏ‹‹$~#îxmÛþÚBâéÆÄñEš•v>Ä>– “ì~çÈø ù,Ü0}Gد¿åÒù4¸1¸ðSÚ“2ÇÉ`}†Ú>ì®ßøZа¥)JRšŽÓ$®±á-êðj2—ü)JŠŠ\‚²³SR„!L¬l £÷—4Lº%,†þÅ*)Q)JÊSR22± „"ñVÓkþ‚lLGóÊR”¥.:áB‹²9R­!ºL—”-Û±J\ИúïÜ^/àÒ”¥.I„!DBd›ËsÕ$ب¥)Kš™fÃI¦˜Æt,¼¼ZR—b„'•QV¥.ã0„! ä"C<«”\)q¥D…ôÚ„ò©JR”¹f„!<†1(·"ŽVT,Ô±¯d}ˆKE’¢R¿ ”¥)YKü+äÕ<«‚elÙDv$$XЄ!Õ)JËþ¢¢¬Q$bL§ˆRÿj›4«#x.i-Ž?åž 2ÜÿÄ*!1 AQqa0‘ÁÑ@¡±áðPñÿÚ?…µ†Ûm²Igà‡RhZ<(>-8ÇÈFmGƒ2À”-•²eŽ0´¶zã`ã"Òcº’Îëg%ô$±™Æ–Ï+iÈ'l…–hi #xnóÑ¥¶Þòz·y¶ÎMœ›xvî {‹¯Ao;16z ÞRÞ ‡‘&N:Û¼Ž3ŒêÂË9̆ÆÔµ·†X;µz‹x÷äã" äé–îåîëѼ ðäw³‰1P³»mÛÀaÁÓ#‘h[Þ—N/8ÀÁtBc‘°#ÆŒ½B<‘™iñ9Ç¿éË==Ãae˜ñ×;Ü·´n]í¶I,p–öBíœäÇ"ú882ï !ÆÛ[cŽì³©0åKxë$}! 0L2÷i<9À¹o& klq¬rÛm³fÄðua¼í¶—¨m·½ÙÞ¨· ¦ë‚êmáÏFÉ8î,rxË{ºyá ãc‡vNUËL´¶d±œ2Äví äÚ6@X²Åì‹VxÛ]–-…²ÇE¬¸ZM­«yÞFñ­Ý¤Oq§ íȱ¶Ûlñ¼eÝœ¼lÆdÉ„Ã+°¹ÂG [xÛxÓn¸y7´›#w8s—/VÃݤÛkÉ–eÕ°ñ˜]ó’r[o=M§¨gÙ ÙãyxË6ÃÓ‡-–HÛ ¯æÌ)lqå²FAÆÃ¼²¼58rÛKmî_ë›AÿMÔ} .ÚÏ …@$FØØ2 «]¶e‘’²81±±aYiá–]d‡IcÁÂ6Y!&I¼¬ëckèGœ…äÀ‡…tµ›A¶Äçm†KÞÌ–gœàH{»axÆ 8Þ5±dÇÑ–ðq±»;¶—Yg¥màÙôo)l± Á¼œo%Öòôìs¬¿ íe¼¾,…·‚|[¤KÜ ÃÆÚ’ºÆ6[j[¤ÚØ1l§ æ1%¥Ô¦Û¶q­¿8YwÐ ü#éINŸÓ¼!}@~Øó@*òI÷¿ûç”ê{•žçõ8éßoÅ÷_©§øßÚì~ÿÙyýèÞGîÏ/÷JìgùþÁïÓAÞ[ø ?"xm|6H6a!œÖØÈðËØ¹à©,²êÀ†ìÒÕã8˼°ÍÛ66÷ᇄFH ô$œÆ6¼cDzËmàvêÛvëym¶e3ƒ¢:”ã.²<œc¹{ž±8aëœ-2Î1ô Ã<°¶uu$2ìú<ú á¶[g€ïŽ™m8Iu…™u 2á¨,|ù˜ùïËô-]¨¾oè øþ”^ýGúë¤ý:íëöÃÿ :ÿ½‡Ëý˜~ÿºÿqï·è˜÷º÷ë¯wú©ï~¡þ£Ýþú=ÿè‡Æ~·ºÏÛ…òt7ÇîÏcú'„ý±lˆt®¼‚ú¡^1²W&,äËR¸bDR˵%ÞW µ–q–wcg|xìY1L¶sŽÞw…ô Àò¯ qŒì g"M­;ܼw Þz^ËxrÖmäe3чÇVÆE§ ‡ yîX{9Ðú6eŒË®Èxîv×8Ôa“Œ˜ ‡»½µ”„^¾xÇ= ݶYßt>námm…¶ÛR5%¥Û…o“V­EjËošüŒ|íù-[±ððŒºlxõøýV‹jàÿB½Ïì±ùo¸=ÿÞ#ãtÿãÃݰ÷?RÿQòþŸïwôAïÿl=ñý´|´=óû¡ü»ðQá¿}¢ >¨|Çú (è6 d„‹@ƒéG`Qâ¿·ãûˆ.ÙÌ~ëÜ§ÝøOíǽýp÷ÿº{ŸÓ~Wú¾ïû§»þ™îà|ÿ½=ïÙC÷ÿTW‹<Õ|šú‡ˆ¿IoàCó,àH@àp‘áòžsKVXØÙe–Ycg.Ég>üdO9Ç|ëh¶÷èuãaá…µƒ‚­l׌ŒÎ²Ïfn¬´¶a·8鳄ދ¾X’cwc¸KÔnm¬e–·v·y ŒúFÙÃç9}ÆÿSȼm¿Êd »€3p–lÚü$˜†;˜Ã÷uÝ¿pë5€qìØ—Év¿|ò?òµçý?éÒ}Á^Ûî*úPìL÷¥Gøò?B{þ˜÷ì{Ÿ±G”ºí¿ë†ÌþÉn¾ˆ×èpé#8e…’M–Ig ±lYfXYa"ÁdœbÈÎóØpæqJÂgMœü`ñ‘g¥9Æ9Î3Œãs³Ÿ~{ã9ƒ ô6YÇËנཿÏV––ñ¶Ûo;oñ§¨_KÎú¶7KmpüÌ…Яɛ&ÁÇQ¡õÔõ€:cv¢WÛ`Ɖ»AÈDίtgþü'÷GÅýǰû°× ù>¥þãÞxv¿±°?_ïsõÿKÉý+±÷SÝ?¹Wœ}ÙþG°þ?i–êŸ"þ¨ò‡Ñm§†ÙìðÆÈr¼%ŽrŒIf–7—©íeœçe–?Ye ÆYaÊqœœgïeY%œçež¬²Ë,³€d³œ²s=Yd9èöôç¯^{õí¶Ûm¬z¶ïÓŸñµàå}ìªÿ·wv¥¯ËÁík~eñ(ù^ö§µÓ€0qïÀ÷EöF]׃cXfl-QgÄên î>ÿÒC7é¿Ý)þ?÷ǵõYì~Ú3ÿAAï¿ßú ô®ÂýÐ>W÷ÿÜéþcßýuî[ÿP~ª{ÿ×…óý°÷OõþÑðŸ±þá|Ýá÷"xúÀ”ßõ{ú àÃÓŸÂÙÆXAeœ'eœe’XL‡ðå’Yggg½–Yežœã,á±Û,²ÃŒ³Œôež­ ‹?rE‹$qyZµ(þLõg¡ÿÔôÝzžC{¿èêÛ®HzöÖÖÖ×ÏæoÌÀö(ùœ̾vt¯Ênï1ÑÖ«8&Þåã®;ç=iÿ!~,ÙM@#cÎYd–Yeœe–Ye–YÆYeYd–I–Ie–Yg9d –C4Ø@d'ÀÏȰ°áœÿ"„qœçðgÿÁêëÑíwê›=;éöã}FúºþoNpr[éc—=;Àzóêuä6ì(L¬‰Š°Á=Ï^Œ²Ë8Ë,²K,²ÂÉ ²Ë,“†YÁ=Ë8$ìz7Ôš$˜¬êË,³«,²É,²Ë,²Ë=èÏFzrOO~Œá?ã¼ï¬çÞ?.ø.½cü@q¶Æ]YèÓŒøÇ¹÷gãâÏ‹gqÀ Ïä_âÂ=!ÎDg9a–Hq–YdO!Æ@ÍOŠEÈäk¬€å0à#è‘3Nk€>˜É,²Ë,ឌ²Ë,‘²É,²FN1ç8ÏBzø2ß[¹Ãèï’C}G¤?€eïáëÉéJÃm¶ÛéÞ7áÿŠðż¼ž½ëƒè ¡5K³»à•õŒTY%Yag #im–YeŽ9À€WŒ‰âÖYéfx6XÙͲÈàË,‚ÂË ²Ë8Ë,³”ƒ8OVpY<¿ÉºÃÆœä3ëÞ;‹§Œçy8r-õw³—–ÞOIÆzsÑž¬vÇÔðž­ã-õ<¾‡ÏIÆz3ŒåKg>Üo&q¼·Ä°ò𾎭àçK¾6ÙÎpô'8\kÁ3grZç;Âq£Ãè~øën÷=hm·e–dÞKaËxyy-¶Ûmµ¶Ùm¶[Km–ÛMãn­¶Óãmž=½¤ywÔzµáõm¿Í×§OVq¼ì<Ûè=ÛÁÆÿñ¶›Á×£nùë“ÓžŒ—ùq=ž1à=ÆÇAÏv°zBW†8Ë»a‘»‚ÙxØçy±àáHäµô%œw¼FÝï;ÂsïÂ<“yÞ7ydàmôop§*ÈÊú;²NHn³ÒFò8ÏZÌIg£,°²BBBÅ‚ü‹ðz‚¬Ù±fÏñ ÷nݸvíÚµnÕ»V­Zá«víó[ZµXØÙcÆ67w|ïjÛV¡[·o‹váÛµjÕ¢!ˆ‹ÁlI³fÍ›$6Ø´ )¶ñ§[0ÛÉ ¶­®rúK=:Î_G¡ÞRbgƒ}²á–wŽý{<“(ôfú;ááÏCœ{úŸãßSü#lñ¼küGñçXqœaue–gƒñð?ƒôP~FÅŸ™7Þ>;ù-K·oãÐWâá~ÔøkšÝ¿àž> “&NïÚÖØÆ¸²-ÛËVíZ•*Ô._;0rL ,刯1˜€q7L±)uugݱÂñ³Âœn²é¼¼Ýï£x8Û"e ¾õ6s¾­ôo¡ôïú^wÑ×§‰=[Áü£ãg9ëË,,õae…‡ Xƒ~"üEø‹ðð߇ÓKÓè;,ÙãúñM™ãßÁ}müKømüKøm|6­ÛÞ*µónüÅùŽeáÿbß…³àÉ2lج+YVíÛµiµÀ\ÄïÇxÎ5þ ve"ßKÎÏéöÿ€ÞßÿΞ­ô§}':zCþ'\Åž‘ëù3œôäeœçVå–YadÌá I ‹à$_Š~~ <‘kå—ò¿2møRnËŸÌÇÒ'Ãcð“#“í¾Ù¶{ã™«Ìæ[³œ7Í–O Î –úúõí·Ró׫gô¹m¬¼¯üÛÿòƒÐü9Æü¹ÎqŸÃŸÌðI%œ9³Ã33<–Yeœ&Ye–H²Ë 0Ç™ãzžH™GK Ç\güuÃo/¥xß[ëÞ{àÿ˜zŸæàú^Ãü˜Ä.zsƒg‡˜Í—ø{ã'}X©¼$‘e“ám¶Þ§ÊKéòDñ_Áïo -ßèØŸK>þ7øvßVÿüÙÁê?Ÿ=YüÇMõ’-p²ÏXzsÑœŸÍž¯n™ô>Œ9ÏO|“uœ!–K´8/ Ï ™wü ·”îy_âÛ}[Âò¾¬þÓŸÁœ¾·?âuuÉëV )g9ü…œçÙëË$õeÖsž†_KêëÒ>—xfïŒîñ–ñœãÉ8pöÆñ¶ñ„ú_FðÛüoñ²€íœâ`ïúÇØþñöŒ~_·ïSæÿ¡ŸcþÉþóíÿs>ï÷3ìÁùé ~èà¤ÿÒÿHÿñ>aôYùý“:>ô“óý‰÷ÿ¡¿.}äq¿ø/Ã~Fïò¿ ÂüˆùWæ_¿¯‡†Xåòç£9ƒ=9ÎG£¾Ë8ÏFqœg«9ÏVp¦|ñ¼7¼½cžŒÞ0ã¾ÛÉt6_ã'ÃêWÒž—ÖŸÂò¾¤X!$:;Išá¶¤¥™«n¥¶Ï ;ÛÃlD× oKár[îáçi$S¶žýOÒŸý)oô§ÿˆµÿIþ ´8÷{d|ׯͿ7ûŸ™8ùGç”w‡gK?;Ä~f}µ>Ô(Û  ?3Žñ}­|—æ% °Ël¼kÆcè#VíÛ·jË,}“#=-×§¯ãxÉ} ÂÎÌÉ×§f}ag&rxOs9/w|õ¼1wÎß1.¡yÛ¹Î}Âó¼ï¨þ¸ :# X^%a™µáxs-K®%µ%‰»ÎV%áàÞw¾FÞ%îÛbÝxÞí–2m¶Ø^5¶×-—[z¶VÞ'9Îxlô<¥œg¡NsîÛním¶Ûmmm­Z·*Ü»RíBŽ-Ûm8m—ŽøsÒó‘g9dyîÃdž{‹ªŸ0`'þ„ÓŒ³»ÛëÉóÆúG¿ pçðl3èÄA¼Ÿ grsœg!'vIg²XÙÔg£9d³¾s€ã8}Æ›!ÀO l¼)ÂÄñ¼hð¤ú÷¼,ðéÃÂs’YèË#ÐðŽk?ðw‡…6Ö ˆ›ÂÊð ð–YÂqœ9uÏ¿9ÆHIÂAyoC8êÖcёǭ–Þ^OVðÿÆóŒ„މ$ã»,îÉ ä3«!YbØwe–X2q–6M“–LY;g>&ÏxàåvPÑ;—=²Û.u ôCeÌÓT–ð¤n÷/ œ/ u/ <(\GRv¶Q*¬§¯-yË$’n¿‰þ…mão¿0Ëe·†4}ðÙw¾œÙá׌[ß©°ç;²éKxwèIñg;.&J9Xmç¯Bú_RáëßKº’HíŒM–IÆq–Y#e„ŽðIdqœç8Ie–Iß Œ`hìŒ!ж@Ðc‚^çEÓ°BÏaÖ=ŽGA+!»`H ×A Ž»$‚xù¢3,Éð!µ T•Q1½ê’dh…ƒØþdÖy¼Ñ×ê#wšTˆÉÜé_CnA tø$Í À†0th›ür&É¢ú³û;æ;4Ð/pnå×IÀ7ÌØÂ,2HføsÓ– o+/2Ê»m²élA˜H³0^þ¼ƒÐÇ|g<,†¬ú7‡yÇ{ÆðË€·‡Õß­ã¿â};Ëϰƒ¬šÉÀM‰Ùe’6XXô!'*ÙÆYe–k¼;"páÇV@ɬÙ67r²YÁ²™Û·tôbxÛ lұ̃҉ —JÍV3ø£˜@0f»Ù=Ü/ª™ÀaÔaú›M†)7@KÂFÖ? Þ%È´Þa†Û½%Ò½Á€Ûò‘; /C®ÒNª0a…¨Š££øÊ»¸5D“8D&ñÙikª3á~#9ÆAé}kÒuÈò¼³ -¤²Ï0Ë—9 =Æq–qœg9$eï&Ï Êz;Á·|$¡é9Õ§ü÷Ÿt•?;d%“°I°–IgÏk'€`$6 ,,žØဓ„áÞAã$ddÉ;–Tb©.Y*HBšƒ;…úGS‚²ÝÕ~·¦,(„„‡Þìj7câ¦Ú‰I³øC“¦íà:›’rf KyKQ?aÝ@ ‹ü0¼²Ø×gn¨˜‰÷ ‡$$’‡ˆ™PKÔÚ ¥OŸÁDñ!Fp4ƒóN·2G§8Ï['©Im¶ÙKe–Ûx^[¹À{—&H&:ô''K.†Ã7cg,^xf8yú†ÄåÓÎuÁÛ)?æV㬷uùIï, –A%–YbÉ%™'Vu#–HA$qíe’IÇAw%–I`Ia>Y$l‘’I,²É“Œ°²N¬ã$á%Œ[íRÁ²K$“œ’ð6I$“È·ÐŽú_KÂÛ6ð¼ì*Ë9[ßeœåœåœd ‡fÈÉÆu’x¡ºþ ôo ÿ ÃêYõ§´—y$„ ’kgSÃÆXðf¤–Yd“¦Ë,²K:vË,$à…’IdÙe“¶Ia%“dÇ %’XáÊ„áx^r`“¹ ˆ.²I$‚N3ÕŒËêmeá›yx[x[Æ|i`ä,²Î4Þ2É,²ÆÎ¸yÎ2Ɉ’k.%xßT¼¾–qèïÔúvgõ.ŸÆñ£VüHëe–œ±žŒ³¼“޲Î3”Ÿ!ÙÂMš2@ÈÝðtu…„§‰$‚ œå O{à$là {²w‡“‡8w—Œ8Il‘—r¾,fË8Î ³”ã$åá³Ó³2Ë-²ËjO«ÆQç‰ÒɲÉ,²K ²Ë$²ÎsŒã = =|ž½¬åߥèàzVÓŸFÛü/ z}½o>û¿× à–ëÛQ÷ºÏÙwe&»åcPu'ï‡yZdPé¹vÁ˜'Ûø~™°äüÏ< gaYÀô ý^R&‡/GIuÂõl§ Î<® ¬¶ðK€¬êRÄ`ÂyD‡ÚvÝZp²7ׇ-¶[fÙÀl¤!)i)Â-“Ü’Ý÷$‹%œ¥]qe’I$–zô,ðç ÆõÉ3ÂâÂpÖË9N2K8G} –u @ñ’I˳íxKÞq߇'­— xÒßFú·þËÊ[@Ù'NÍ}ë^'é"c¶9ø$ò}¦³Rí?KOÁmtáîÅb|7ûÙnŸ ¾Vuö϶v—3 nÌnë{âuŸ fw / 8:÷˜Œ›š7âàtD‡TïtÕ=Î!LÏsÀÆ÷S=ç;ØbWúö9këWqïwÏÆJU„¿nM‡+K={~*oš`I»´¼ùøv&îlã,Y›:’OBD–Yg|cœ'§=+,̲̲ñ¶Ú³ÎÛ3‡^"É ç8ðl})¶Adœd€’sHô]‰þð»þ'|ï«¿Küéï×Ù ´ÿÈ//Þ1Á‰áŸ%JX1¿v¬`ø6Ýžaí<À—IP“°ðxÁºgŠàë[TÔ/ÒÉ‹Ùý‰eeQæMފ߈¡ø‰ôZýdm*=!vtL SÝùø|B= ¦Ÿý›ª.ô=ŒÊ+燲:(¡jû°!›8:½›~>;;ŒÑzéÍ—¿û“$ý½ÜÌ€?kíwò]R¶­&ÆÆâVÈrü媴/°X6ŒÆFÇþB°ßøÂ6ïç1fcíŒ}ÿÃ/ªøÚ&=¬u´ ¾<Ú~¼¯‘â«§2{X[Š{‹¿ä H+¢Ù´:Cö¤ y×7â[‡nÇÎ#'|ô{‡Ø^x?›²±÷TÆH">ÎÞï¸û‹Â±ðô±$™#Ã<>dá;µÛÅ9{ñžŒºÞŒååáfvvfff^Ôôxyqð‚ÎLl‰5,nóŒ²FË ›,à,fÉ8=w×Êð°ãfAèu¼<o_ÊúÏNsäOv*FG8cnG-ʇgwíÖÁÁô“É|ƒ#Æ —ÈY:#ˆ`£d¾DH«ªÀ€]C¡IŸ›VX ƒÆ ßsw®ÝŸ|XÑ´(¨Ìe‘BÊ@ÂVÄ" @ªî!1ì _dq1ÂP kw½;FShèz,zPk òCpA£ DêêGøÄÄÛ´Õ–>éÕ7¯3!‹ž 7$¼A>U‚1ýƒ³©’£¨[÷o2È<620`3ÖßìêA­ö?™÷×¼¿sY:gÛ‹ÞŸ–»=žÑÚi>Q6ŒÎÝŽàËQÇÉ÷žØ¾i"b†Au½å×o5ëý ÿ4˜ðƒ€3¨ÜÀø.ïåk[Ÿ|­ÕO+®jëcÂwg ’HÌ’z2Nº8Î@ž:ô$Ï¡áá–x]”ô2úäaáç ,²Â $‚FÆÏ6AÉgS9–Y²A Ó;t,}OM¾‡€þ‹Ç—ѿ޺ô¾ŒôôIï×Cdd²Î {ÈAâNï) 8“šXDK ²É$Y`Ú»œw© Ÿ$àö4ŸvXüqf&%á…‰02BÀI+Ð<$càÈ$²Cp²K,Ë:›$Ÿ6K× ‘aÎHÉ!dÙ' ÊÌúVeº“—Ððeâ±ÀÞw%’0p¶cÆYd–XÊ᳦Kgñæ]¡Ôxî×Ð<»i{ÝǦ]ú³ù³ø³œ‹KI$êN2s„ ‰4á̲ÂCŒ±`‰:‘K$³© lw¡àŒ ‘Â]`˜ ËžŒÖAî2K$VÃ''êÂK$²FN2l7‚BÉ‘žÞÞRÉB °·ÑÞÛéK,“ÐòË,Ï¡fxfÉ8<—‰.øà6YÝ…¬±’È$³„‚Çgr'^rà ³Ä…Üku§Ë&ßK‡\pyžÂM ›óOËbØóäzvÎBí¬ èUÜGO‘íw„Ææ>qKé´ {á©ÉBêÒÃfÂÐéág$ã»,ìÐ[ò…¯È’å·¥®ZË ö¸(ïrN(p[©Œ—…—€ j…¢ZxÑa–Ùxrez”³†¹n¶ñ¶ò¼+l a¶Ò^“†e™áyxx» Êxˆqœ;’Yï²HA¼Ie—vpÈ ’öÌœžF÷ÎNpúQ9¼{ãxѾ­äÏød÷錈M‘hôºÑù vG®·ú/•þ~¥ÑÞŒLC5ùJÔ²ö!=þ™RÛÁ¨þ'Íâ5âTnvfú¨}ìKÀh&ÿÕÜÉûuø$—¦;VÿÞC =ŒßäŠ<3BŒªËÛ£ò"ŸÝoýäkwDQ"8‰ j°sì¯úô†úÌÝé(>#ȇM`ÇÃìD†B áûSNMå5ÜÃr\pa ÿIÝìÆC-Hn5{%<}Wá‘òïœ4ab¼}Œß¾ ­(øÎãê:ÛQ¯pK>HÏ{~h«»<¹,ÚÍ·~«×~xš hvšÿ>[ Ò½‚ýVØUZ\ Dö¬ÞŽ¡š¾žüÿÛ?üù>Ø×Äu².º~zêÄóì––9l…C ‚|þ;ñ)orp8;xt’P÷h"üA]W”û´ƒã=¼õgÍ£«a}Ëp*×$ŽÛfñÒ¿ŽúÛF”µð˜÷A½íÇ€zÛ+Áµ×Óî­66-ýØ©fxsM»¢—/ öÈ©€ä9s¿ƒ…áç¬ô?À6²c€l dºXdYe q„†YÎ6p»a²p’gÇb3ÆÙÆœ7„£®^#êßGµ¶ÿÈ ï㡱‘³ :мñ§Z§°]=…ê:Ÿj$–æ1›!{ÇÒÌÁÞ_lŽ1àE/ÇøHKO'þçG}¤ˆ|½Û¯ý¶a󚓘r׺A{ [úËBG¦^ê¿¢¡tèñw›§DÉ4/+Ê_–ƒ[é6~¤•ÁxíÿA$Ƈ/ÔŠ„ܶsZ·²ÉwÅÙ6kò’ûøœ›é==õvg×uÇ¡ó" mPe|¬É“:Xk£ÚðÛa>þB]á ¼¬ߦƒgGá?CÚ ]“ —} l(êÀü¤­ØßGÆ­tvT?+±v^ÇÇwæ0éÝkœqG¡—@ò>nß‹ÿ2×ç–["|¹˜y_s4öh÷Ê>/»À¿qyZ½éújõõ>'N™PКÞÓÇ÷×Í“Ã=ìdænŒüν_õºþ/ü$™ýHy\étÌOÝÛ¹¿„Õ“3jÿ_QAö¾ÛFÑ:LnÝóKÛ/€Þ‹çÎj¥ }´nåŒr3á£wºveeX÷ ¶€Úùem¶€]§_33«ÍŽ”©|¥Õ½í•:`íÔ¥àLKÜoê–I1ìÿ11r@æ‘ÏaŸú½þ'þÐ&Dsnöž«Îð¯èÓ~@ª½ÿž&ûAvtˆc:ÓV³/º‡ï>¶l“‡—‡¼‰ìQ¶Y&ØAÜ› sœdœûÉÂY$šXI$Œ;¶C̾§‡W8:ÏJþ_ø¯¾sÇàI!!%¨yÔ]}$å§ä%ý‰2À3@±M9‰ÿfM>P‹û‚y,ß·Y5—à_úYÁ$Â^„íö±ç!^kó¬´ðÒÐù/•à! ÊPë_.‰Q+U>MlÆ‘¼ÑôÆ{_]oÕÿíâVÂ? ‹û)Dcæ þ‚lwȲC¢àÔ"O€Ð?bCÌ“ÈþÐJú•Ãé\…?!ÜþÑøº6~RÏ­‡~« u ë?$>áˆ~µ2‘xhú˜Ý^\Ô³ãK¡cáûL@¿ÈÓò´±úõ>OÄ躎ºmzf½,r€p†.ŒÃ¸;ý“æ=ìWÉ2v?’uJÖQ`üT4ð<ä|öOAÙXx3âÁ§³û)oA`°l;0Á{/&§á-÷h÷Vj󪽌ÇxÐá¿’ñí…üØ-©O¨`|GÄ:zg 8Ía¼iµ§êh!àXà!G@|[ &˦ùÛ3DõÉô-ç6úo£o1ËWÄoî=’DW½U×f±A`!˜U{_²ø’`¾½Èb$=B Ïa‘„S63¬—:˜g@GÂ/ žh1ãÐà©>al´õþ†_ã¡üü$ Їóï÷ֈᥛ<‹¸»Ÿp&ÕêØÊÙçß>ß…æ]=¾%Ämµtö!¾/H"¼Ü†pKGdŸ†ŽÜ§8t:ú:Ÿá '“±å½ã¬zƒ©$,,² °`’êÆÇok.²èxHÀÞë†SypÝM¼¬=~óÄÛÛ×óéÏù>”ÉÜ’Å Ü!§ä¦ÚP…Ù8§¶VúŒý¬ ã°Qÿ«Â\³³èÏOáÞÒÙ\6%*ÇÜ|îäæŸ¨£¹ÐoºÎ ]$&CÑ ³+ܺEÏÔk'Xv;›ó;øžù^Wûܱý¶éöI>ð»Âz t‘þ Äòu4®äz Ôê`ÐL;‹Ù€Ý/’ïµèu¿—ÕµçÎñºcrš¼ÔvžB¯ÊÅŸtžðÑVnˆÔ8†½ÉOœZÿâ¾F¢¿fп;ý B!t¯ðY‡EØŸ‘y—dü _)ümçRObbaسìJÜ îäñ¢cÞkQ1ÏsX ˜F1áÃ2j81€’ÀÉÁ,²N dœÈçKÃ<-Üðï¬lóÄ;Va%Xq’26YÀId—³ÆNX¯Dlœ$¦¸^SÆÏ¡ƒÓ×+ÄËxöçmþ7ùrÇŒàž}g ’;ÿ÷HxÚÖ-Qôhü6]+¹NÆ0ù¿ú@2¸‡þæ7ÏAC‹bçnvÙâE2Lü“Ã@£p›ã*5h=þ쎃 =ÝþIÍz_’uà~aêòµÔ€g؈Ԍcê]¿–K)N¤#ª`ŠŒß™¤jH-{™’¥‡ñõ°–'Ë|é÷ò‚ì·G)ï BÝ6ôÈË¡†þ?ýæèöÿÄ“j+õÔÍÑÃ2þFÐCvGdw¹,ld˜é#$„’ {»d†XY,“» ;Ù8K$d’É$“¹$ç‚e–Y%’YÔ“2pÙ' $ÎðÌòó½sïéúéhT¢ßUæòÛlþÙºM¥î×þ?2A´‡Áa.&kξXÉ>SCô•ÀÝ·_Eº:›Û}Ò’ó:~ä68æâOé’‹Å1%>Éc_:œ÷¿§ˆx)ðõË\ú&@û$içÀÃ_˜#…5ý áÚü²6.Ö¸„Š–†ã¶fbúÅó÷•Þby˜¼;|@ѳñ!ÜGÏî†pÑš>¶$Á{>"ç3bž~³¨·X =Ÿ,°É•Ðím«„gUOl ÐßiÏ„÷ŸËâžÒ]û¡¦Î‹æL+?—à˜f:B@¼;7òØö‡w l’ÎÉ:²`™'$ãÎ]åΩàæL“èÉáå°Éã9xóúðp\g eŽð™geŽYg\dñœg †L‘â«•õžõca½£9F60œç1åçåQÙ™[¡{DÇ3Ý#æjoHÄ}aFÿ/âö‰&Zñ¥ÍºéàY×€u“ÚËuº¥{Kâ:¤wUýw‰ Bˆt%« ƒ¢rzÒ"*cÇGAú6˜± c#ç¯õ4ÁÜ ùn¦2ìÇQxÇfžð@”ž£Aà>áPÉË ôýÌ̤Í?{é÷¢Î'ÂËþaŒL²f›^ˆæ*Ôolh|¬žï€~&’Æ7¬rÇY›Œ„ÇõÄó¾Y¬¸r…´ëÜÌ{—•ÏE_d)§Ñ×Eä›÷Õ‘Ó‹ß>.¿èÿý@}`žŸ“AÁ¼gÌà ðÂûçæ£}íH=‘óél›Œ)‡è·î0ü£Zì­àwup2ïBÆZ{>묑pÁ/τƆæ*ÃN…®É!LöïÔy&üOçìÁø2íø?ó³¦5ã‚ý¯M$'>Ãî—¢*¤€ÄH$vɲL$²dg„ÒËdîl™3‡„²fxÆË$³–pòÀÅÏiv†±ì†’IÉ{à$’8e…–v–g Ó’I%ï í;ôg!ëÎN˜õ著Vs‘ÁdYÆYÂÏÏÀ$ïk¯ÂŒ¨M`v(uXÆ%ðpx+\~Ã`¥ù!à%í·ýß›™á¿¸OqÆOŸ€é—A¿&“©TÕ±°# _öc7ñb1`ÏÄÐ'šv舨'Õæå éâ<¬e'R§Bþ 'xgíÆfu¿ŒjÿÀÇùd´„DDlj5ÿ̾Su¯Ó Sáf:.މÒ$ ” NÁòÆdð`fk¶3è>‰[ÙnÙ{PR_Î|O–ÛAðBñþ(¢Sgº­ßtw`€×Ùcã¾P™Üèì>f\q† Û™Ø ß˜Äø‚‚Æ|îëj~tk¼1dxî—ŸÝ@·Î·ã1á¿lÎ>¹2òÀåeO¾zf©>ÏGÒ:É4*Míƒþ ä$Ð> ©Qô“g‚2SðÙû']¬ìëÕŒÝ&Y6AÒɳÞI ’I&Éå$žI$’d’l²É’C8lê} ə˱„XÙ'Áe–pH¯¶Yd’p…†Èdê,÷‚ùô6pOñ ×üœç=9I«¼#¶2A'ˆ—Á ²BK $ÂÅœ41ˆ˜rÉ,’Τ²G†LÅÑË‹‚:v¶Ü’uÚȰޛ "wgð’c<*°Lj,’a%’IdØL’Id—rY× ÍŒðí’LŒ–Iƒ–O-“èOYÄ»«ºFY6 # qœ<6NÙÔe’q“æt†T?uŒŸ<拾øW;§ ôçñç«9,žŸˆíS€ã8É8–Ad‘ÊXz2lã:“[$W‚Y#cÂYg}å’V]l„‡i/\äøáÛ$x!³ $žrl’K$²l’É& ²p“Ù'9Ôí„‹<#–?'S$“+Æp“ÇSÂÛÂzŽ'æò„##g#$;$sœad2XLŽp‹;a'sþ©l.½>•‹ËÆòrz;Èóœíߣ89Î23ÐG,ˆìžL&Ë®7Ñ×pd†ðÄ£:‘¼>œYºœ°º³—"SÑ£ãÓpK[máeã[{™YvbìÝÊp ]OöÌÏl²¼#dv³zlè' Â-’ –IgR$Œ’I$Ä8I0áñÂH,äÌ’wdÉdú–Î0ŽqÈç !Ãd–IgVuœg²AÆu!óuijˆz›Ú­=9ËÿzƒÓœêhÄ ¯äêæÆO\ݶ9 |Yx¯ü‰ŸÔoÔgÀ?¢WñÄ(–è ¯cjê{•ÿa>lïÃݼ!:|B øÃ<ëÚ1CïXÕåGç?ÒÇZÍCtxöYôµÎËB˜±½þKGåâÇÊq£=÷³Ë(Ò ÿ©1í}G ,ˆñ€2“‰½,_l;» T(xsÿ“{›NØÞ{63ÎVH¶–GxŒ¢€ù ÝG}΃þàðŽ€cÍdõŸ£5ñr|=yòÄÆóÙ‰> ÓÇc2cÞ=ŒÄFK¯fQ$„–p’Y $“©’Æ@2ɳ„’d’ á 8Éç,“–yyL%ÖØ ŠYe›6_öVšKPuò°L(½V1(wý| G”ÍG3့’+Ûu@Ns&É$,É$™,“,žd“ $“„™8FNSK8K=OùÌYgå’YdëÀYÆYLJ¸Ù9ÂIä=­íÎËÃ1¾®¹'œžŒþ3ޏÎÎ0² 9…«óI'Ožyÿ]0z·R´Îö]`C¦îë1×dò¦ÉšMÛÈ(qw²1s¨p†éàýŒÔ{´€ftqwØ»?RtÂi)‰n-Ö¦`Éê[û#/âJÒ #Ø™Rx»A¦k:‰²Û~ 6Š·,n¸=£kÁhBo°n¨„ì‚!ö7C¶:,µ-ÅÃxOûŸìÉOã1ž!|eá/Ô‘¿õµ°¿ŒÔj w–('²è@jù¬/ Fà;Ÿ©T¢ƒ¯‘|1¸ Ê•PìO,ƒál,Z)*ô)Ù!xäˆF°‘/•]a%£SØI€dŒ³àÔ§`”ZèöùctFpqÓ÷  º™ÍéÖ#½IŒ´ – IDùDý¢HÏö«ý²ü~…'ô²MÅÐ!‚èË ,d²FI™ $7Œ›$“„³”žFNГ¶Ig£"‡ñc¢ õÃ9NrK,I6ÂN1ôg=f<'s>oޝNtxßQÉüXrpÀz‚ÎíŸÈ’Ë8ÒÈ×Á= ¿Éþ£Çožßú„Áï>›Gg ûEª(‡Ê!ýA¥½ÀÚH÷ðML1w_?ö‰ÂBà~`Dqù$€Õ%¼=á‚HgQŽ~…f&;¢QóÉ3ì]©„Òø1÷ÿœ\ø}$ëe×°~'ì¥ÕÊ>%·(x¼æ,¢k­|2–Çooøã²\!òP|ϱýE°wî‡ÅÞÁCÎË«N…óÔ×Ïb˜‚ëþ§ÍŒtX~kÑ®ÁÆùÿì`XÇÈýH¡Aâ_t¼PÇÿ¹CÕÉÖ ì—ÇXs©­ éNbŸ6Ba~6fKƒÿVî}ìOÈÙ'AÁ8$IJ͓,™$E$›$ožͲK$ã$“¹²lížäxI ™&ÆNäÂÉ’BÉ$³„’K'&}×è¼£¢:êËØ’’L,êÉ8 d2K$²ÂFÂdždÙœ–OKÈz]çye' ñé`ô àã#€Û à8 ,›ð”’m ÔHô=¼ÉÜ¢`¾Ñÿ&;„^D‡êŽ íŒàsdÇ ÿ 'VÓ¯•à)Þ€€”=P"ä…<€b"gcùŽÃ‘ôöƒla®Ž£#r/ñ˜Ö[Ö !‹š2À{Ä»d÷çÊÂ_оödG¯&¿§±³µ§wÂeðùûw/ˆéõæ<ßd4”DÚEêøNëí”Ì÷rw¶ž³Dê°öÖEÍÓg:º ›öFœÙ|4±Û¡~çà°ÿ`OùùÙV|/Oe!‹‡Ì)¬>ˆ¨`ú}„:¿ÆÁsüðîuOÞ“êrÊÇ*I×=NY6I' YÇSg “d‰2 „šÉ#&2N;$ÝI<2O™$›'d‘Ù2IáÙ,ã$ä; ">#Ü4G¢$ $Éä&vÃâd²Gƒ8À’I³¹áAÜÃÇ\kmÙ“Lb#‰Î¼gqé8ȳŒbOIÈÿÆpPq–A–ßuI;éár~ÂÉÖ¶6”–7_Alʈ<Œ­ E|§>ÎàDºG}lihžjGRf)ñ Hì>iC¨ÝZÌí;’1°!{âÎ0ß&äÊÙÓþ×D8¨†(Å”ùÀà ˜EþBI©‡gÃà-Ð~/GÚ'`nÚûC¡í»WUf\Áñ(?™Ô?DÄÖ‚]õíŽJíù~ "ùO>›½ÇO¶“1šá°cÛÞ{°i½u"Ã/–,zäQö8Ýù~÷Úºó—`‰ëvkøiòÍV éá$“Y$’É<’I„ŠÈ„Ù$’HÉÂIÜÎIe®2yNS€÷§ÓdXÙ'26Nð–q’#'!%’L–2-Ü·‰óéÎ1ò Çp‡e_Ydy»ÿ&Fz3ƒŒ³€² #`ƒ€‚’Óá ²ÀÖ.Í>ÍQEoÆoµº6†(£­¯MŠgãi/aðC < ˆÚîHþýÍÈ®ž&sÛ£$q•âv ü;7 ýìFó àš =öéLÖ dYŠ/–,{Ê.x.ÊiÜè‰fþq^™ðëí{º xüBêé˜i+vèÏ™ÍEËqïûGlüPJ剎±]DùÊýï ð§Ò†_ Zã¸)ÌÔ­¨BÙMt,§`3xZ@=ˆù@T­ä7z0Õp$%ޏ|«#=ɤ { Àðø±tøl²Wq7.pïA²@>h$[±dàœ‚KWüt?kH/ÏôLfÉ’BHkgS2pœ%“$,’nöI8I$$’I-“Y8|Ùg)„·ôŒê‰„Xl–Hì!$Id –I–Y$–Y¬“w=¡$Å…ß„²î速?ü/Яó9A°Ad¼d“ëòD³„†'8Ìÿ=€èÅ‚n ´·äO±%}“èañ>ÄlÇX|ŒHdŽŽO°YÓà1Jö"°ïÙ…¶åçÜá’ûAþˆ½cY:9ÓòA¯Þ\%Ç¿â1˜ø0`Ɖêõ#èqÈÿˆÔ-A3øMÿÈPõ: “ÕÛ:$-hëñ¿÷êwúK¼>_òe€:œÁ|LBö:`ñ 047§Äýhè×Ü€Pv¿jŠÙ†z¿˜¹wŸAdÉ#d†É$…’A%šH6X2šÉ u"K,dd³©ÂÎàløÆfI²d$’ÆBÂrÉ’Nä’d‘ž’NxÂxØoo¢âê$ë ±Þ„ã$±² ,îËH-²ðDœ2ŒôAÁ>ƒ±"²!c\𠳦ÎwÓïÃDã=9Áié888 à‚Ë ²„²ùé3À~ãñCî$±ðñƒ~ÉÑLï{,~MÌ‘É÷ë]¼Ÿ ï¯ÄK”<D1кpI>àîñ#AëÃgP¾äÒépA;s€¨$ > .1=lp.y«Ö É iÓ×qôBm„êĶëhGXî? r|¿ñ¶…Œø6±²ëôÚZñ·Êû‹·ÿž¯`7|6h`cTÜéqŒ϶ù=ÄQK ½­ÎÏ{íÕ«¿y'^ªû‘Èœ¤ð €»†F=º“ì`Ã¥{ò¶]É6HYÆ2k2w=9dÀNpöÈðŒŒ’I$šØI²]Ë'DÌ’Y3’I1’xI’l‘Æd$Ô³‡8ÎÚÙÀÈ:±á8E°‚É8îÂlí»ã ²÷‘™8Fdê=iá:3Ž®£Ï"¦’Ö’Ð-ô7|,ùνYüÀqœAAeAÎO>G¢,ˆÀ6ìWRÙj®ÿ¢ e30ÈÏžJñŒƒ&¼Á.ß’1©¾ã6x!#Ãöd›AÁùŸ‚ êìŸÃ‚LÃ0X@ \ðqN+/Jdª&}kîJ„ÀXAàéþŒŽØ!žþ„|«%ÒýOé¿" •p ¹.>745:aÿ7wªŠ€üCH  @Ⱦ1õ²€ì|5ÈÃ$—È€¶öøƒ–βK$w„Û&lêù äÉׄÎÉ%’pö³Õ“–L’p“$ù™³ g,™á$Ù’I$“МükËS$“9ÃÂweœ{YÆq‡ f1a%ï3‹™oÌxôœ%»/%ª9bòÆ7¿©õB ô‘ÈGArp–Xùå6YIÀ$ƒ€“³Ðœe–Yeœåœe–YÕ’%–2O=NYèI'GÒž—‡Æo=<9–œ8c) ,Ï ­ÔÊL³,¦[/Ï ÂLÏ_âýÑìP"³»Ã6a­’YÇ\<%œäð¤Ì“$ÏäÏ[åŽzà;à;.±ìŠlHï©Þ'«8듌²Îr62"‚ 8ï"u¾Á&låŽ2l’ܲHõ¶O9%œ³{¼ååþô/ü6ðï pûpÉÊ’á2œ/v’Ë#pfÛBÓt&Ÿ&—pé ÞÊÌYd†<«!^?¯ÓÁö%`Ïzí‹m–G¶ ¡=€$´UsŒ3e×ý‰uûa]ìæKü«úfI$²I’fgƒ¿¨HôFθB˶K$›½›ßŒ³^2ÂxÉ$’sfgŸW³xîbùµ”r½Ò4z˜˜}-‡øÎ3ÐrðAdYdAÆCùëþàwŸAüO£l-åãyeãmqA±i!g†Ñüæ¶Ë ¼è¼iœ;À `CtGA!¤9¾ Á-]¦c`{d¨ Ãsd»»›#­d 7R3í#É!¢¡šDE@þÞ¸-˜6ˆ'-BËáäÛº#2÷Áý‰õ&÷‚¢÷¸{çÅ’ÌèÒÓÍòYsïdö‡c^¿¹í÷Y Û´JÁ=uÎÙK]ûöé]x±a¼»sðHGxwüâOŒ2¾N›×±aÕ<ôøÌên)l„ÓºÀGOnß§Ä0Nû²˜Ý=V$®û—ljǹùàò;5Í‚®]3}üÏÔæq-ñ&ë7”Dý)’û}ˆ Qvý²ÏÍí€ãïÉl|5òŸi ûçd ØFA Îøîxfrn¤žôªÙŽÀc„ídš­– šò†ÈXYež€‘ÞRI'faõË}'–EîD¼²ZËú7†QâSƒƒ€ŽMàB"2H bÎ?AíÆp±x.ä€)tA¶ÜdI0«€ÛÀœüHÙ@ ô Å`!ð1¬Ëþ,"[Ì@lO´6€y³î‡Œ ÈÞ¼:ÿÔ%>p#l¾'›žI¨¹ÿÖ1\ö]Ÿ½l§ÛÙ(~ì *·Ø ÿÙc—ÀÝ™@h|(K9‡eƒä¸vÛ{£ÿ(kåùäí:¬Ø Þñ°ìûüZD¯M$üs‚UÈ-ò4b¤Hûº¶ÂvéõÍÌ› ]/Ù a2BíÖÀ Ù‚=AÂé0ÓCÙöø´ˆ/áÉ1¸…ùD W }¹agN÷:¼®A w(OQˆˆ˜‰0;=ß`Æ0ÂÇñ­‰í{f˜€¨ ŒáøKÀß«üîÏFfu=€øf¥bûéb¡:‰ ù<„'MþÞðg°:!OM£§œì0‡þòƒ!”’¿¿÷y$Ô7û½ä̃Yú>ûœ£>ž.­ÝaŒö1I¼ºe·ÐÃ8—vAm¤^â$«ÁÍ ÷wó’x¼;æ0¡º ÙŸ§±·n£XÊLgÓ,ñàøÇçg—W“ŸL}¸ÎÓõ÷ãqg‹°× wÔÀ™'Ï #Œ“;ÊIð¡ë|¨@è€É9’Lœ<¤œ°M׬É39Ä^Ï')xCäqrœé–9ÞF›xCÎÙÀvlAfAYõ5•ßGo±‡Tuw€vÆQH`³+=‡‚ Y„bIÓ ?7¸F\ÃÿRh<‘Š~\Ùs½ÓüÆ-„?ˆ¿:o8úk_À³ü¢ŽYë…° v I:´DÕ’>0¿¦IÁ>XÅL"TO‰‡Ø?²'z„‚& ¦ ìü l@W~‚@¡{ddú«Ü~¤»ïx:Ïf¥Ô¹ím{Ÿäˆ´kŽöa€°ˆ¦Ap±ÞäøÏéd¹(SŸ‘&wásË9ÀüƒýNp©Çkð“ð#~sáx,ð~/Äkí(wý yoúVû‚þ1—3ëÐŽþã@(¤IÏâAú±ãì=ŸH'ˆ`?R2Ns|1ù?6Ç`õ³uð>§xä)úψvÐ1>¤c3Ú&˜>müø.œ» !*†J"u|fÛP(Nî÷‚[J•|ª«àRÈ® ø á,:ÁøùXùÙ'’JÉ$žÖ1 ’VLI&3‘ã&dîNÙ™˜çÒžþx6SÄ’GyNõá8ÃáßC¼¥„œ;°“¹cžãuœàÆæ<Ñ;7-è},lúçläÞrË98Ø îÈ,‚È, ‚ ‚ Û®ƒi“œh–¶BðΦ0Ó겎¦C¨ðÙƒóp~ºðlù¼3#ĵÑ'ý‘ZÀwËšŽƒÐ-QÃ:·ëК]§RÓ1¾áþ.ˆ­ÜVˆ,’:æyI`u^R÷rä‡vGÀ€³£~k'Ü Ë ¹ 0toɱÕ-±˜GÔ=ÙÖñ’XHl–Y$’Y<I!d–d’0‘I$’Éžœɱ!$‹$’:å!$’¹$ÈÉ2HÉ#“ØÀÐv±ƒí_(Bˆ ÀqžÅFêêIÀ™<’M–w6Yg6Id†Ù×¥VYaÎIÃ'' 'RIÃ%’Y6+%˜Hq“¤“$Œ“»$„ƒ$Ù„ÌË' ’É$œ' #$’qöË)4‡G6s‡‡Å‡)$ÉdíeŒrðIÉ%ï,$ó<—\Æaàt}'®Yè3Žà²Î ² ²ìˆ‚޼‚b ÆÛ=ÿó’Îð– Ìéš:Ø„›‘ëD°îÝ04ðĸmÒó1€sðÑ몯WÚm6óX#¾¦•Wm#ÿti;rf%§ ûbcn“³Ýv™|Gsþ¥46EéñéÎRgÏ>Üuœç¥ô¯¡,ã$’É9ÎPË,“ $°™‘gÁ$ÈòÌäñ½§$’xFI$²GrÎ%°ÉÉá™™ÙÙ&vß Èug„ã&ÉðëcÂËw²LÝáég€á÷O3œ¦°¼ŸÉ é86,1Ãêa@Á@œAp@YÃóÌWŽmñâqKÝæU{w¡ÐëÞ2>QX“Ì6ž5g 8@&°nÑ/V§‡Êc‰2D½˜¬m+Ï"õ{$÷‹²iöȨáe:‹'Çü<äÏž>9É=rœ¦¼ç ˜É9ÃeÖðçÉ2Y=²d’õŒ†L“3$É' Ü;Ã' “<<<²LÏíÀÙØE„:™å›Y&xÎ7‡—¶dºÉ“®¸Bfþ‹Ë†ÆN< BÆ.Á³ÐQ%–YÏW¼²î=9AaÀ@äAe<ÀYÙA±âÈ 7€ãèQ9èÖ"ªö¶pË,ã,²Ë,ã,ã "Yè…àÏKÃdúÙ$’OG¼É9Ãg “²L“ $°›™Ù’xrdfmž‹g$’FxÉÎfyxdƒùÐÃ7‘ãCþ6Y>„l’ë'v'ƒd’Ü~8>¦y1M"O~–óŽsƒ‚,t,y°±‚ ˆ8 Ž‹" öxIïßÏFz[,ôœg£= ?Å“æ8<aÆMÔÙ6rÌÏ Ë$ðÌω™Éxfxg„à“&]OpîÈI,Ì„Ï 2pÎÍ›'/òSäG¨:sœdØb͈D¸¼ìÙÆð‡L“, µžƒÐɱPUÝfVz2} ­ŽNCŒà9,‚" Û ‚"È8I“ø„à}¿‹,à˜ôe¼u¾AÆ^Þ“ÕÔðù›8Éô#Â^Ç ÂrÉ<3'33âîd’Iá&ÇŒ™áá&xN™å±“„™ž>à !pÈ:ÙYã'$²lœ˜fqdë–|HYm÷zFpÆlXG¾Û–™èØ},¡Á<z‚<Äyˆ#`à‚#l‚$e¡ñ/ðçü"y=]ú“”åóéNrcvNY&I’̺ž{ffx$ÉßsËÂÏ,“$–uÃ%ÖÌ“ÂXïR)&FÍnŸÂ&rŒdc8YŸaw¸LÛÃuÃbtp“œ2ðƒÃ„˜Ï@ÂØJþ†êÞ' Î ‚#DAÀ@Ù$þ°¼¸÷±þN ü9éö½ø9ÏGVYË6O žƒÁËÆNñ–$Í“<<-“33$äöÌ’IdÙ3=®Ì’I$’O)Æ™$àhžâëõ¢¢MÏi$™Y]ãIÏBcË,l[ug S¼7Tü6‹ùy6Yp`Ng-G~§ÇÐ#,,Ž à ‚8 ‚È€à#¢Î ƒ^ú¤$ðÿÿŸÇ—´ú°ã½›'}o g©ºxKÁÃ᳌å›8fIÜ’3™$’ŠÉ6Ã$–$Ìdd’I0‘‘d’BI$‘dl’fl™œŠ0óÿ¡t‚¡Ë<3¶J’ðúž3fï;™'ŸD÷ìc‡äÁáàçÛœ=Ý᳂ ,l€ˆ"8,x"ƒƒt»‚!EŒßõ?‚6xØÿ‡–s–Iü>´³Œ±ÞrÃN2l±NÛ$îK,’M’ÆI É!$’'R¤—+dá|—ÜHyþÂOý³´ ê~>Ü~_õ:y„³ =Cð‡äÊxsÛÔœôBµ¥î‡ÁOÇz£2ÑáŠòÌÆ=#àôŽÎT»Î6ö}ñ“²ÈÌÉÃÑ𥩎¦Û´=/›3ÛíÆ@ØÙ{ÁAæ€ï€±‚`ƒ¨ `‚Ež|wlá= =ñ–Yc«êÎ2lã8Î2Ë$õg%–AÂI$œå“–†¨ý¡?î’ÇÏõ§Ýþ¤‹ßöO¹ {ÔüÒñãþ“ìCìÛìJ÷ªvê_aŸ¹óá÷?Û ùÿ°»ÿ^^»µ/¿ÿuîõýPÿñîZ÷Rû¿ØL_ð%õþÒÂ×éÏ#‚ŒÇïƒÂýðÿ×Ok?s¿Ù#yû7›æo›û¨¾Tïêîù¿­ùÿPúaî¼=ßéG»Þýô÷‡íÉöûÒ»%†hKoÁ XLœ¶¼o¥‘ç]ô<†Nâ_âìýgðQ=;ÎpøœÀaœ…‘ÈpA)gqœæ8²8Yò+Éåç9Ë,ç8É,’ÏFI–<1²r_|cçû ??ÞO¿ý¤ŸŸïœiöèÉûß©ù—èŸb_fº¤gVû4û?ö•ÿ#;æ?¹ùØŸ|ÿ©øïÙÂß{o©ù¿Ô¿tú¹úýÞâGåþÓýÎ}þã?ütû1ÛÂvþ‘qäªÕJ8ÝþÜ÷{ßwó~èüöû`<~èx–ú|ÔaúÁfGɚ̰H±!%„–I&Ê”ÈË·=XuWI‡Òš}qÑu¼wo©åãfÒlç&syú žŸ;ÛŽ¹9öñïêxze×£"b #ˆ8cGAÆA’uaùë¢ã®;²ð¢;?r Hû$ýßÓ??úŸ“úOÅ\:·Ø—Ù§à¯wÛ=?ö3ìËð×zNÚŸr–÷~Ž ~)÷)÷!ï_ÚÏÏeøõ0½/ÑüBÏüMwûküÛëÿd#ÑþÿÚ?ØV½ÿeWÚ‡œ?â‚y„_?×Fóú(÷ÿ§û@ûþŠ„:CÎO IÂOàø?s ä>èžh=Ïꇽú(÷¿WûÃÞª{Ÿ®sú!ïù?ï/Ü>;=æû ?Ù໣Øýæð“Á}QãÐGˆ~…ü–ãËûnþ[,°°€ƒÔòq…ž®½y;éN3†Yè/ª Í{ÌD»3‰åáô2Ô£Lx&ø#i0µž–ßFðÏ=Ç M“³¶21`^–éÖzÍçOIÈØD1gbðvÙÀY E„@å}Êt¯Hã‚1‡ØT€Ye…ž¾·øûµôå–p]Xq–z³œ±Þ2Ë,‚Ë,²È2Ë,‚Ë;ã,²K,²Ë,’Ë,²ÉVzÌϦxXÇ’O bü¹ ±3þ´¶„ Yu,ç !8œ·\7VôÛÃdíÜšÉ'ÞÙ„yõìØáäÆ=éÉ–PƒÐEÞéAÆAÀDD@Äpl„Æ’MWÄK”¾v]×äH«,³‚ÏAêÎ2È,²Î¤°‚ ,‚ÏB,žþá°´Œ§¨EöYŽAÊ6PåÓç%ij¡W¹“Žß³#ÌxÃ"ÏŒAc$n`4"²+„ppË8e…–IgÆYÆYge–Yg¤œ¬cÀL'vùà‡©´Ùâ-û#Ü k$xxM'•ã}!Æð’Ž“uÀÁÜûƒÏð`‰É(ôo|ìð“Èr@plE——€ƒ“€cɰp‰$£ãƒ¶IÚPmw’IggÂXØp©tn<çeŒ c!åB|¤treàC“l $(CÈI“*$a5 g•¡^ˆ`¿øš ‰Ó±|mCÝŸ-¡ÅжÈOY†ˆõØ'ͼÆÉƒ¹ö¿0à”Ed¾ÁzÉ dí΢ì Ye–Y%–Ye–p–Yg pf­Z๛ôþíúXð?©ŽIá½§aßñµ‘èà³{MÔòœuÂO Á–q—Y9 $y´‚øßS€KLG…ë-lןàuÃ,6ød@Dpu„@AÜaÀA¶D™9“Ð{ìÿ<Ÿt›¿ÓkAÀˆ Ë@ì°KhØú»ÄćoI»±ìÆùŽdj—Ìgp¬CS:øÄºž­€&KÆÇBÐXè0y|È ðÉÊŠ‹`?räx!´ð‹#ÙÙdÏL‰Dy‘² Ø÷jccÊ4l}»±:é̺Y›Æ»xwÛ%1×!í¬ÏÍ{ƒ$×[QšÆå‡dmGíÛ·Îú¸W¨Zá¯LkƒÁXÇ£¾>6ø ´šC®/K‡.Ýlù×–Ó…“†6ׇ…yÔôõÊ‚ǫۋcÁõ#Ãt< ²3€ˆÈà,ˆ €‚ ˆ² Ò ˆ€²3ž´µ~[‚öˆ@ØK÷?ûÂo²¶#ÎK6]SWyºn¡˜ƒÐOeó²IÉ5”ù%0 >/›Ø£t“§L"¾áb>¬ÀYø– ÍØí³®ì,ÈÄEVŵ¶­qÜ;vó›P¿†V¸_ð@ásÃÙ³g“8kâjŸ ´±}‹ºÜyZ¼„|¼ù™ýÂý[ì'Ôñ¾‚¾5ý÷õþBÌû?Þ¯Æûªõý¸¯SÀÀ37áã'n,rÏ_òdz9¶Oó>‡'ÑÖp<Ã9“ÀêÒOGYèQ¬‹D"YéRÌQjX‘›ÁÀ+ÀAßQw n®õ~aä%oõ[_â|J…‘é$¢öÂâÇÏ½ÆæZ¦26J…kŽíg)éÀ¸n×ñ±É‹6lÙ³bÅ‹Ye–Yg¥óƒí/>éóǑþ»?ýEì_Õ¥þ)¿ôïëk}§Ÿó÷'Øýñ_ýWØÏ¸ùr?^+ÏÒ¥wõÐßü³<ÿªê>ú•ë÷ÛÜ}ÈûŸb'á¾æ‡OïÏìgú‘:ÛèÿV 6b>¸öÀúÏýGÌRÎÓ•éý‘÷ßî.ûÿ¼¿"rÐ$ð¼t“ÖùqLDºã}, Ù×Îòœ!¯Ø"ëžž"KFÈñ°8Õ·£¾µÝÜq’YÁœDÙ`XàD6@Ùä+ñäŽ+ÀüÃõ£¿9NYêð‡è Y³fň2Ë,þ¸GÈ/*?fDÔ¾ãÁºCÌPîÓŒú/uúžèþ£9µÇùñýŸé)×öÏa÷çü«”óûçú¾kŠÌÇ/ü„׿ÿ'óbeóq?'Ø#ÎûýÏ…÷¿ÝuýÕ—þö,wûþ§©ûGýYÿÑL?识gê®Óô wv|*wè¾¢êýŠûÿ²!çú3òþ£>ä'’“ó2—ËôIù§å´¿lŸ…ûgÊ¥š:{\öaú ùèœÂ+î”÷ÊyR¾ì)VåZŒc%Õþc²|¸tE€åá Ù»‰·ƒ$x[aººäeáí~-žú‚7Û׸$–ŽE¤¾_àN™ìÈ,ƒ‚".¢ ; x8@ˆÞ27l`á~‡ÁÁõ gÉcçšä 8ôx,ذ°°ô£äO˜_bO÷K1€>LòßÒ ù©KÞú‡ä—Ú_×ûO¶ÿÔ–åÅÿþsÛÛ)ñƒúÿ5_ò?Þ\ïökÿ’EO#ûÿQÿnÝþ¡þSËúÊòoòO½ú“/ý%á~éÔîÀö~ãþ`<~Á^`>éÿP¯Æ¾ŒûAõK¿ù ó§Êk*òŸq_?²|ÇúÏȾÄíÝ~óÝúF|iý) ¾ë} ù¿ IøOÙ8õûû+÷}ŸîR^?bÙj_ÃúKåþ„ûEõ×ýIné¿ûd¹•ò¬ÃkÊL?$ü·ågçe›óá‘R­|Z·oø¿l8/5„°Óò±«CŽ ð’<¬Þí™ÆÁ¼gg)!1t]öd)¤Û.¢Â€ žŒ"i;‚ÑŸcÎo$úæ ¨Gƒl †2 b;‚5x82,ˆ76!u¡ãû· É㎖n^Ñ_…òGü0/ú¨§ý§ý5 åÏI¿þ|Eöë{ýOd_¯÷ñÿäùŠùýš§o쬬Òý¤|ú…¿ó÷-Ýÿ_åŠkî}HKÊþÿÖ ö‘á~Íla:Aý`3ôÆ>è—Ÿ+k½ÉwÚ•ù´Ïv¯Û|Ÿ¹'Üþ„‡vÚ!÷ÇèlÿÕOÈ}OÏý}ŠMÿ9!ÞÿsÞ?dKÄ>Õ¿˜ýO‚Ç}GæO±¯þ’?ÌÝŽÿxÁóæ~ û!?Š~Y‡Oùóï)W»'!d¦µjÕ©vý8ŽKé„û/ô°·ŸìgÎý‘ÙÛ}¬öoömžI÷3ßý²û¿ºùë§Âøõþe÷ÿWÇ}½~Æwér?æµû´~Ñ>ßè'Ù¿ê{ZX•û–î”´óý³á´f‡àÈK»÷Zps•j÷ô8€Ý¨šŽ@8gâGã}f·ž!ü[ø•ÅpåñndÈ‘¶$|pÈx±ñ;öäúÊ—/e˵1SÈMžl|XH°Ø ðsn«ò®ôõF8èá&M,ôäñ¶qœ&Á–Yd6‚=‚g€³„ լɳ]ð=Ð#¾ á”

Ç%ºøµjÔ©m¹ríÌîÌFÉ“bE›6’Éà8$ç9J?õ[Æ©s¬KeUŒMޤü'Ò¼²É˜AÀwd@dfÁY¼´@pÀpGžC€’ìF8ìcÜh< ”ØÁQD;p¢lÁ³ñ̈êu܇ñâß}C—›n>'xñgâü&~/+éhàÁe«p¸hŽI0È8ƒùAŸïÖ{,AìôðŽ‹Ïé‹,œSüAàe¿L…¾ü ÜçF_†>7;q z‡ < Þ[—)w‘ãÇ H±–lØÉbjGŸ9™ÌÖ*U¨iÄÝ)ë†FjtEÉÒÏFLñ 9÷ãlåuá‚Ê{/O|ŠMci å‚|þ1Ùñ–YèÎàƒI`ê1GdaÁwu êÈÌà‚#€ˆ‚,Ÿ_EÕpGSóÃûpK™ Ä?ˆv¡ÅÌ)Øà7Äèˆ~¾-nB!Ú͵ pXSxf1>Ï›3S%oã| †Ô?H’!Ûç~’éÖ”¶§iŸ º'½†&5ß_],Cǧ_Ùºô1Ô"ìsµ°éˆ|GCI«à&bÞè^ì5Jõ¶Áøíý=Ÿ‰dþBÌS?ø¡ÈݹÁã¯ÉŸ#íª‡ãá†Að6ðW´B>ò.6{ßîÏñ‹Í§º¢ÍîÐú.Ÿ,yAšÂÖ9`7 M/ÚBxž] ÷"÷ð|Îü8&E›6$Éàð"k^b¥Jµ.Õ©\Zá»våÌ×ñúKÏøO~É?½Åá3ã â„: žvgŒ˜›,Ž7ŒÖÆ÷žY!cûK}L8ØR¨D(è8,„: ýÍŸÂ’Hí ˆ,‚Ç€³‚ `ˆ €‚NBÎìƒ`‚É:²“£äoï0oÝÃ®É ráaÚ›ž°5eæ&#r_Ü%Fìë6 gaöxC‡á?’Ù½á Ý­€JJö`û•Üáʈ:!pñ¹¯ì„ÂÍ‘;ðÅ‘èë7Þüì=£??èɘk]Ìí¬èAÓþÓ»ŽöE=Ãþ tB«á~§Ãó Ý"3PMö}ß×Í´ì´Kå>F‚G$;ôd†¢î,ÚŸªÐ1 ‰ù£¾Ó÷$ëºË„=×~ÙNÓìHÄÀû¼Ô|EjˆnÀó¾áb‚~Ió..¢œ6ÀHõdn¨²üˆüÝBðv{ð#³šúênSÀ|·-sö}^í‘'ü¡J…*Õ»|u7~Œþîû"Ø;<ºý‘|OèO°BqÃ3=„±›—ó y¢Ž½¾çÝÿµ«ì”ñ7ßu>sà8 c7DÜÿ{‹„8ïx3Žá˜pç›,°»á-°·Eü,`=è`IÁ—ŽÇvëø^ ¼'œ±bÇÁlXdÑY›YFÄ<³œ †ÁÿÌêþ ‘‚ʪ¬¦ÿŽPºó£<,ÒöN×aüZ‡ÀPãžPÁƒleŽ 0`Yõ=6xç€3‰Ä¾Bžð¾Ä‡ÓŸ;õ0ÿ_ú^B„ßSÙDÿ U¿ï1>ßøxƒûÿH¿Š-S»þ¬¯HOžƒ±~°e;þ²C·ú¼ûÞì*̳¬"Ëþ‚¾ÙýW>ƒèJøGÔO™Go÷Ëóû#ót×o»Èi<“?5/Í?<~%>ÓŸdO± x…{C Ÿ)åOÎÍ0¥´©[[¹uP‰#½~Éõe¼‡®:ô«ÉΜäq³¹e†HÈÆ'ðr×2 ÞB!(Â&’«A(`î{ÄHŠI¶èÏi à ƒ€v ˆ2 l‚#7€01g$qäwë0Öøš¾y˜â |Gá󌇨q !ƒÍœù´“óûŒ—ŸèÉù¤üŸé¿ÓUá›õ>Ü»ß×CtÏ×ú_ýÃýKv/¹ñ ìEÞ¾ôWÏî’¯úIö÷Ðÿ»¶ýcý¥:úwúOõ!çí üÍ ÷F¯Ý݃ÿdA0Pÿ¤‘öÃêmáþ£åmôã…¼Á?ý÷?`‘åþÙ=ð¹ìÈûßÔûüô x‘ÖŸrPûûÀþ§ÚÔ×Ɇ'äŒ2Ël¼;+‹"MZ·nÛoŽ¿ˆE“·°Š&G|¬FéˆõÃwu²z0Ë9È,7œôeœ›RFsÔ/LþIvÍÞHÞ&^ß ±óvz_AÓ-‰fÞÐuÁ1¤pe…ž0‚ cƒÙ6 ‚ÏC-FŸá“‡ˆV3#ÀLz)>¼Ty/´/%ýI/?Ò“óýÒ>vú`ÿó?üDûiýO¾møÿµþ¯}¿_éÀ¾À¾å]ý†+çíõ"ÿ¤ÿHûGëe¼Ÿ£ý¤¾wÒÿvßô™|ÿ¸AyµeçîWÁû¨K¯ÞlxCôŒø_T€ú¾?êJ5¿ŸïeüþÅ=¿õˆû‡Ü{÷ý’i÷ ‡¹ú$½ÙÅ~KíŸcûGÛýñ'¨~*¾ð~£ìÔV¯ ù¿’~çóÏÇ)à—Ÿž~f~D¯Z”÷¾Ï=œ •jÔ¾ XàþøZø…ê8bÅ‹Ÿ&¢ŽÓbøiDzñ!œ'§9C•ÞËß8îvÉç¹kGzCeò¢Ãpªˆcàx}}dy–ƒ;²È €² ŽÛ ‚ȳŒˆ ‚Óâ8-ˆž¿{ܬã*0ØkŸÒŸê&öÏõþáà~ä}÷îŽÿé‡ú—ò¶ÒâßüJqÄýCýD‡ì¡øû”û ü~í`<*‚úÄ?Bø£rÞaO-öÊyvû„á䯒ò'Ý¿—#:‡Ùrßîµ?aq «a’½¢¾“§B[\&VŽÞSóå9ùV|ŸD¢rµÎÌÙ±›–>,Oã}müKø·.ÓéfbÇ?•Žøµ*]»v½KGY³c† @FLŒÜÆû6ÐaÑ—Cœ²Î}ẲNrË=/'kg}Âi#ÔË€º Zƒ¡?b(u0‰ÏáKÞz16H2ì8"Ȳ í`c¦`ƒ¾xñ q˜]òYS‘®öpK²ÜÏûÙo?ØÛyýͯÏ÷mù?ݧÌcäHû/™Ï̳§ˆû*Gr¿ý©ÏÄGÄ1!í‘ä¨ïh>køE{GۯƔø—ð’žg·ó[yVûÔ‹ç‰B$Ûµk†`A±ñgâñ܃ç/­¹­ñkâÜ+P¯|–lØø±ñdŸÂ?RåpWÍxñ<ËÄ„,G#ìËM­á®*´ÚṌ),AÆÅ׆'íËhtޤ¦ó“Àz³”½ì³,g¤ø [óŽN޹+{ )š¦.¡¸‹ÔKÇ¿{ÉÂ2we‘ÀYgPpxÆÁ`ƒ€ îˆóp[­î®ëø˜Ÿ Ð-° «ÏÜþuü#ì?Û-âüB¿„—¯¾í}òžiNØÄö¼¡bEb  ‹9âÄ|aøÃø‡o¨|M‘CfÅñbÅŽúpÕ»P¡ÃäM™,H“"ÍŸ‹‰˜ž*VKÎ;àÇ¥©™àÖfÌŸŽ!˜å ÷%ø¤zÄüI& Æë6(8Y“á#´ˆtDË8Ë9ßJAw±–qœñ³ÏVøRÓùG¯ÊA.Í „ë¦Å‹€öÁC2Í"rÁDqœ@Eœ€‚ŽÈŽ…º¾G>ÐϾU‘H‚ pãˆ6`ÀÈ$~1øÅ8váD 0`Øñ<@Þˆã?Û0à+{Ä|íòƒâÃfÏųë¾ÙíÉŒ±3< É5©ÉfDŸ"OzKá‰ø—ç™~ öã·Qø‰yâødÓóM‘>Me|©ùÙo;k+;ò<²Lß q>'ÅórÆòlòdÏ.LLWž¿ÊÇ!ÇV;If@$l°`RÈr:.]¾Œž°“6€à ²<@DÀYAÀ@pXÄ@E„y%úVr2QiÞÅ(¸ˆ?G ¨P¡C³±0Àb ?~ p9ÜpùZµÀ„ya³É‹xž†o[3p–(fñ³ ²ûìÃòIòÊ“2Ë/ 2L’<9Ç–xd’IΞ[¬2Ù“÷ž(9éf¼àsß Õ°pœ¶ó³ÀfKE-£}XBí Æ÷¢© ®í8òzaHN§ðuÃ$†nÙÔ ÀGŒ`Ýx€¼AÀAÙÀ@Ad -6òr0ŸÌ\9t`¬'ïJgÁù/ܪjÏYyÑÜ·‚Ç@±€à!5 aBŽ drرg`ìSÑû·ÈóS"BÂÂë…&Ôµµ–Yvvee™’eáá™åBB_K“o ÌÎKÆËÃ-¼<),IÀޤÌ胨=c±ÁŸÀÁ'V;–O2EÇ´Fz„;(™À™u ã t˜ðϬ%sÐsí7ìF°’Ψ ƒŒ²6,"¢È<d‘OyŒÀ˜Aø€qNÍ ~'ÑQ•ýº·ÇØð&Ähâôøc !p°±6~8œ³„rÏ7Á a«\™³–äŒX]s¶ìðñ¼-íèyxY™xY™x^åeÙeœ–e™e¶Y•–veŸ,³)³ÛÂË-²œ+)kü]´»"uVe/™u“üÇ{oQ6_1çÍûð ÷+s¤Ü"¬êlCâ ² f8V<ÁèîÈž0Èôg/ Ã>£/ŽÙ ‘2"½£sr.Öðg*]o>%=ìœ#p “à‚Ĉ,"í³‘D=?ÌôúêüÑÁ~eøˆø ‹«­çmçmåyÛy_^ú^áYáyY™Ùe™Ye3)“)“ñ.Àí}&:Hú£²@ñ(„ó¾äÿJ%’ìÿ©ÿçgÿ†Å|¯¹_ ~Ë=_ÒNßí‰ñç„{ØÙ¾3xéäëË×”…1þÔ·ŸîH¼·Úßwñ±_Š>9nÃH>"<‘ˆñ<' ·€‚ "Nì²!ž2ïlãÊËåGvLäa•t„½›Ám'Y'íÁ’?T0¾X=l°O`»$ïd0uÕpqí  AÁ‘c’І.t·ôw¶ñ³ÿ þÖïÊú2Y¤¢½–Œ/†úVÞ?µ$ ëû$/ùiïí¾%ÿ€ñÕÛÙ<Н—÷+ÂÿÜÏCXõÿè¿Ó3¢ÌðW‰àKËÇ™‚Žÿ±y%ûYO<”Q…j ºÂÅøuñçâ? é|2í§_Ãé€G3Œž7œãÈñwÃçŽÒøÃr_O?µ>eåô`”OI;=a [zè‚§>ÿO·§¬DŒ9Æ8³¾#xØ"cb³Žÿ’nAêyØÿ‚ó‡ü^ÓÅñýíÁ§—í#ÞÝòt{óÿÍOŒÑ>!¯žø_í>¯¸”~{Âÿ¾öïx¤þ¯›ÉÏ—_SÏbDèûSäpŠüQñ1ñBߟ$8{â×ÄO¬~wÄ~6~ {ÀüXÏìÈ~9óñÍŸˆ üYƒ€ üY³l’1ëÛ}‘’Ç ‘»é/àC6Î2Ë8ïg±3ÒHhàÀ6¶¥ï!LV<`ŒÎ¼Úì@Çå°³?€ómŸÌ$±Þ àä,‚Æ#7€†!ˆà¶8çuÿ=ŸÇß«®Þ…„ðߦêñEÇöþ×Í’|Dý“à¿Òògõ›ŸQñ È 7Ä+ÚøH{÷ú¼§ößåÒwëûRîÙûe¼¬,P˜p¡|Bö!GáâĈ>ð?`~,|_RˆˆüW„~<ñgâÏÅŸ‹æYƒfϱfÍ‹6x‡,Øáãb ‘ÇÃ8Âë,¶xN7¶Ó—ƒ‡ƒƒ`á7Œxv9ÞÆÛ—Ç&=Dxàö[u ƒ&}¼]âA/?‹_Ì„$l2Ç€±‚È5à.¸ ²!Ö68ˆˆà9v"ÆÎ,'¬“Ñ–p|Âx£ÚƒÛQqþ»³¸"zÑÆKüš¼¦¿»Ü/êG¥}GÆXSÙ·²))¾êyL}O!O•g´ÿrÆÖÅ÷c(¡ÂâŸ~øÇÄñâ > ßX?à±§V> Ôˆ?=‹~,üY÷,üXø±Å,X³ñÃ3Á‹6x±çõ°±e× ðÛèXx^-mxce‡“ë[màeç¿AÆ“ÀÏž6-Z}~Œ·‘ÒO$ýH¹Œ$#Áð=€ 6¬aÎõÈC¾}˜`ƒ[¨; ,äà³XñG ²"ˆã „F"°´6íæ$ûpïÜO™^}‚Áyÿ5^T¿©à¾•ÒƒwÛþÓÆþì¾'Æ~§€'×|Ý:ÿj÷gí¹™8_áü0¾!Áûþ$øÂ?ü üA üXøƒñ∡‚ üYƒÄ,X±"ŽLIã›<ˆH™Ž$œ¡Ë,²ðËÂËÊð²Ë-²ú”xÛmëmãBX¶^5çq‰xÞ7ѼgQæêî\ã{º–ösߥ=‘ $2 ›aºD#ˆÝV²z™ÐŽˆñÙeÀõŒù²GÌ{¼LpAAÀY< ''¤¶I!Ú¤xåûÒ©? ¼¤y¯Ò¯*>ÜùKö§~W„‘رX´Îâ`À€øÂ âĈ?wÄ60 ç @ƒ,BY³¶lEÇ&Ye–rç=zê[fÙxÙve–YyÛx^VÙm”árØm´Ûem–Ûe¶Ùa†Þ‡†ÙF-–ÖØxÞ:(ÛlÛ12äC“÷¬zNˆn i¢|—uÆ ˜qÏCÆtòcù §Ç,€ƒ€à9"#c‚8 ‚²"Èà‹,^!IS”0`> ÁÝHØ~ üAÄ€\ÁŽ@c<1† ²Ë,ƒŽ¬þ ¶Þ¶Ymå—…–Ùmá–Yy^e—»rY{e¶Ó8ÛmžÛeeÛm6Ûeï#…¶xvǃ’g «bõŸ Év?¸äØÞú½œié^Ðϳ„3Rá´`µˆŒÊì=x0@n·Pž“d?Á³³¨‰©à V2 ä;Û /v¶®b ¸Ü²8!ƒY#$w² `Ä b ,E q€áŽl‚Ë ç?ƒx=;Îú^Þ[xx^6Ye¶Ùe¶ÖÙxÙe–Ye–[e–ÛxÞÛm¶fËï#m¼t{Ì#å'È3ç~o%ýÐÝ׌¦ðÍÝQ<iì)/GxHñNòŸÝyOí¼´)åe½í÷, ¦±OÍdf“'6ïӽϱ;2^ñÓ!,t„3Úx H†€gËlþ'k=ÀpgÀ60AÀAÉ"[a䃒 ,³† ðDL,‚ ºŽzYœž–=;ú8ßF÷éÙe¶[xYe—©@–Ùe–[e•”–^6^ m³á+/,¶ð¶ÛݧÍÑÄü+-Ñ!æ"<”uX‹á^/ÈJÜ„ÊñÁ)ý·’þæ|…*÷l÷-ó©PÜüпA<jլ୶ÚúAk À±èXâgØdZ·CºÉpǘ;€ô3Ò{@QÍ‘dß !‚lƒ‚#-ˆˆ½¹88!àmˆNLàa†ØxßIÆÿè9ß^ÿñ¶ð¼,²Ú[-²ÊK)-²Ël²ÚK-³¶VY‹))"~I-Ñ>ty s¸Cö&>•ì/•ðeûÝåòù»µ+åÛŽÕÌ×¼ùór†?ø Ñ~nx2ø®Ö\ôo£ç==ñž¡Nô`p» Tc#»IX$1xØžw†YKGÈŒÀeŒ†Gm‘²ÞÐEñÁgQâ:ˆºÈà‹¼Ž 9#‚89¶=Gè}êXßàÞ6Ûm¶Ùm–Øm–^Ûmáe¶Yé3m”˜‰ù "Gɇç÷$wÞ^s‡øñ=¤{kâJžŒŽ ä«É~öYÜ>âŸyÏÉÝÌc¼¿ 2í’™r¥í½µ¼µ¶Ö_G^Ž—…·’=FY§§}oˆm’C<~$¼’Xœù…/ŠR3Ë ¶ê‰å?w!1ÑÀq±œƒ‘ÈzMôÄpl<>‚ØcËm†Øä¶yÞvÛyÞßF¼­¶ó¶ÛlM¶Ùf-³"ndIj‰-ÐýÃký÷‘þËÌÏ‹…÷à—¶Œð'ø¯:yÕyoížÄ˜~Öuåݼ«ä\6~+ã,q]¢\ÃOÍ*ÖKµjÕ­­Ý­¼>“³= Ï£xÛINð]ñ½õw—À¾†ÚznþcÉéBL»NZEïu…a¿L0§“—ž­¤ ;±Û¬ÞãvЉØyD…Ò,²ØcaãÌCÆÂpCÃÀó¶˜Ã,B[ ¼w°ñ°ñ¼-¶ñ°ÛÆÛm°Ûl¶ó¶ÈäLü’^ió¿s™cÞ±üÞ‡•ø9ó ŸH¼ÜùDýÞQÿl¯jmù;”pík9×nÕ©víÊ´Ûko\/Ûêcø4õ¹éíäeãyêË}ÃÏ|oð';87„r ÛãGpE¤mØŸ‹#üÅ×;o>Ñæ@~ad#N7Þ²88V±ÈÄpCkÆÃÀà 0ÛÀÛða¶ÞvÛa-¶Ûa‡“q&ö/ _h]§ìO'/?_=¼ÅðüiíÙ³ñÁ¼N/?ʼnÿ®£³T§k»÷˜|Á7‹6&*Õ»RÙv­Z¶Ûmþ'Ñߨô>Ÿárï×¼ §«wþ}=ñíèerð‚z"« 8p,‰ž©ãé8@“¹¶SsŒ‹a´„‡†_ %¼m°ð< ¡l=C ½Û(a!¶m¶Þ­¶Ø{ˆ0ÛÀdÈù>Kö_ú*vqy™ð´œ]ûoûñ#¼ ãÅüyûvOö%Ú£ò¬¾ëm^ÕÎÍÔðˆäU»vø*ÖÖÛm¶×þËÇÖï©þ™=[ëòż¾“Ï=ˆKƒ"r6™¶ô[“÷ÊFÀBò3ð³ž6Þ:HAádU¶2ØÈˆ†^F8amÒêÙCm°Úäm¶²» k [bGÈŸ8ÞKö7•þË·xø ÃñG³µ·ø+Ä뉼í•Sä~ÕŸqÚü¬ºÙòØ][4±Êð®Õ¾M¶ÛníÿžüŽ×ÒúOIüüžüëcÇ´ %VFÙÎñ½f1e¯I¶ÛÀñ›?nŒ½ã-½C¬9Ãl*Ë#äA‡å‡óii¬‡Ÿì%]ƒù”þëÈ[£G³…â| ñ#íoÅ3Á_%~VžAÿl¯šÕZ^–Í÷°,°´´Û}+¾ ž¶ÖWÑ¿Á¿ð·×¾—þúßâxÏA*ÿüAÆð3œõo§ÁùJ[Ú•€Œä£2³†îRÃ-!bœí¶ñ¼Vc›žKD´‘–[3óGfGÏ÷nÿ¾QÜÆ¿SóÿYáCàþÓÚß°•øªòï1ý“çY?ÚËyr“æWÙ»²Â)ÃKI¤¶ÚÚÛÆÿüsþ=Küülž³Oünqá?ÄއўŒ²Æ0a1/‡(a˜öN,ûÄ`àY - ,¡!ŠØC!#Îð¤í‰ŸAsàg|Œ&LÁìEÝýMæýC÷?SÛ“Vü?í{*}™}è—zFóý’þi_*kòݽí=íj-ŽÞ4´ž€›-¯ ãmµ¶ßáÙá?þ6ó¿ðA™éï,äáåXmç= ËüO¡àÛ$`mGÁkàcàb aÙ÷b’(*[ð1ûBg°R¼9>+˜ìM ÄaÁøÎ‡—üŒ2nßTËÜ‹‘Má;HüðÏ ˜o .¶ßJ1X[mãmŽ7޽ Ï|7_Ã×üçø_ø»g­õãÿ9ŵØñŠ,Fì5âm°Qð _ðadˆ'R…ò‹±~y„€½±ðÙí’îƒ"Øiq*ðwU+±£î¿A’Ô'gh2 ]‰Ù#;$cƒFl) uOKgê"ØŠ…¶Ûm¶Û,¼kÎðú÷Õ×ü3þIÉ%ßüƒÓŸÊ¼oòã àoΆ‡ÅY <{aü èã˜6ÑäŸpO¸8M7ÓÔi†ÏÈ•÷Lµl©m³!'}ØI›¢Ä4Xûd\D‚o䳌ËóDܶ¬N900Üà{YÖà & Ӿ޸} ´Æßà÷å³Öœç©àä܃øºþSÖú3ùNsþ#ü˜¶¡½›ó¡!ðD'½äÌ{#â‡Ä›„@yáD‹ÌQä3Þ˜³íGÚ2Ã=˜%%}å¼Å|«KÚ·¾YïÁšµk+ÉLzÈuÐXÃ=hðæð3_*_…(ùJùS‡–WÞªÍÈó݇@3£d`jåx’ÉŸ‚ÂÃŒE£…λÀœz\õïÁÞw—ž¿ˆàåçßøÿ›–7âm}˜ùО×â‰(P(øãã‰ËÚ ÚÃ0$4!&bËt_ ŸšHâFôMaIGV}Èü¨^æêƒPôUmm~mµãxy÷ôc÷JŠËRb‰ÖËd¶v|s°ÝXp&Be°ð»¿a‹§³Í§fÛ°él¿PwDÞO˜st-èqc¡!;Æzv}yÿ3„ßá=%×óŸñû±¿ ìÇÃID A†5ðqÊņ@@Z|‰/dŸ²~[ºtŸb=ú&Ø}äÑ÷ÕùY_.-f­ÊµÛm¶^uôo¯¯NÞóÆ ¨ÃžòX…<˾k:ÒHà¸M¿Gí ,øcˆ=ÔÖÃç¡’2HÏjü“qÒ@`‘·´èÇŒ ‚VVñÈ:Ëz˜“޽[üï¨ô¿Ã×>üž–ùõ<õž®çã¯äË\GÁ à‰,qù@â ‚ö±.Ž‚Íˆ 8ÂÝOÎ_’èÝž%ö‰_ L¶WÝ?"tójÔ ڞ>‹Êš…PÙ¸Ecm,ˆÿÇËHn=p2ì_'ÇÚ™Û\ (”|pþN!BáÊ Ë°- xÓ%,Œà[1‡$ÑêëBÃD\6Ž˜ÃjvCÔ¬=0Ê3Gã]ã#/Ÿçyêy^ÿ—_ð“ø»çP×áŠ$¢ÄA¾øŽÎ2ÈËIù¯Ë~I×&“Å\ ¯ï?+)º²Ûríµm²ÊÚËêxêå-LÍv?윎ƒ÷³®!ï!>ÍùWÂpùC߇Ȳî; dOàJ»-l^Ì'Zº|0ð‘æXù±î1„q£à"‹H”±i)t*’ 2Æ=É?Ôœ'Úgßg Yma,fBBRœ4‚L„ÈĆÒÈ …Þ0‰'òó¾¼õeßêwùwÐæpqß¡}9êÈV¹!CbæpfÃn­!”±1ÝÛ#4‰­\Ó/ÎÏÈËá­¯ÚúÙþõ¥hÂ,cÌß÷SLmÊ«I@ìüœCå²è:V‡ÅI½`Ä7ºÝ äâ2îgåŽ( êüP}–YÔàÝ|ÂJZ|É&(ÈËxy™vlQIòíùž ¸ww/;lò37žËÚUOÇÙJÔ¶€Ûàpî|fšó±:ç²8<Àò1çðo éßøO«yÆÕ®`ÚˆI, 9ÛmšG0Òfï‡ä´÷µó)¶ÖÛmyï—‡Žÿâãcc ÔTö{˜| Ûµø¬|Z·<øÛOf>¡¼¤|˜ùbë#c¢ÃÀºú$s¤2,p>Ã`ɲJe[Zõ³ópkÐsvvîÐq¨E¼ãœd–pž‹eÃZd.ÐDÆxn+¸@MÕˆpëŠm»ÛM»õo[ü›ìúRÏáFÆÕ¾‚88±Áae‡:’ ÉàBÉc‚­dÓÁ+†ÿ2ÿ+ü8ضYce…‡Í‡Ì< ñÜ»V̧ÆÂ{£áˆ(€|Å€Æ^ÕÑà"±ÇP™bI‘÷àBx,Ý©Fü’ía+¶ú=Ç’d‰CÙ¶²Ù,‡’828×ÑÙÇ|w$0dÓ…¤IzùÕê8FÕ&išå–‚};,˜z´±ÀmµàØ\l(6-³`]CÕ XIKC®uB•µµåà$---ç9½äàÏÿ Ï¿/oü<ôõÁÇyg£,õdqž¬l[ ¾Å§Èâ%|jO´êê)~KòÛòd• bg ”ôkÆXl…¸ËHwG‚ /`ŸÌ#ÝûK!cíÕÊH; Î;Î5åx€oµY jvXµ5>‘tÅÒÚNNþÛAÙÜHRô,¨E'Ý’,Ö©j™J·¹Ü§ÏŒãîÅŸ¹0»‰:€-±îÍyyÎB‰˜‚ÆØÉ,ºº˜½£Ñ³5jÖKwvñß9ÖÿVÛß>’,™þ=ëÏIèÎwÒrz}¿ˆóÊs¡~ #Ñü¶“¾û½¼Ùd xxά³¾2ð“²è˜ð™=_Aho‰Y¦Íºd""þ&xϹ ›Ü,‹­èa…ŒÉ|„l¹Åü>Öüþ¯¤=ÌÞ]Ÿ‰sAÖÍ——séµ+-Ò:˜w.3Òq½ò8 iÇÿÙfwupd-2.0.10/contrib/qubes/doc/img/heads_flash_rom.jpg000066400000000000000000002361331501337203100226420ustar00rootroot00000000000000ÿØÿâ øICC_PROFILE èmntrRGB XYZ Ù$acspöÖÓ-)ø=Þ¯òU®xBúäʃ9 descDybXYZÀbTRCÔ dmdd àˆgXYZ hgTRCÔ lumi |meas $bkpt ´rXYZ ÈrTRCÔ tech Ü vued è‡wtpt pcprt „7chad ¼,descsRGB IEC61966-2-1 black scaledXYZ $ „¶Ïcurv #(-27;@EJOTY^chmrw|†‹•šŸ¤©®²·¼ÁÆËÐÕÛàåëðöû %+28>ELRY`gnu|ƒ‹’š¡©±¹ÁÉÑÙáéòú &/8AKT]gqz„Ž˜¢¬¶ÁËÕàëõ !-8COZfr~Š–¢®ºÇÓàìù -;HUcq~Œš¨¶ÄÓáðþ +:IXgw†–¦µÅÕåö'7HYj{Œ¯ÀÑãõ+=Oat†™¬¿Òåø 2FZn‚–ª¾Òçû  % : O d y ¤ º Ï å û  ' = T j ˜ ® Å Ü ó " 9 Q i € ˜ ° È á ù  * C \ u Ž § À Ù ó & @ Z t Ž © Ã Þ ø.Id›¶Òî %A^z–³Ïì &Ca~›¹×õ1OmŒªÉè&Ed„£Ãã#Ccƒ¤Åå'Ij‹­Îð4Vx›½à&Il²ÖúAe‰®Ò÷@eНÕú Ek‘·Ý*QwžÅì;cвÚ*R{£ÌõGp™Ãì@j”¾é>i”¿ê  A l ˜ Ä ð!!H!u!¡!Î!û"'"U"‚"¯"Ý# #8#f#”#Â#ð$$M$|$«$Ú% %8%h%—%Ç%÷&'&W&‡&·&è''I'z'«'Ü( (?(q(¢(Ô))8)k))Ð**5*h*›*Ï++6+i++Ñ,,9,n,¢,×- -A-v-«-á..L.‚.·.î/$/Z/‘/Ç/þ050l0¤0Û11J1‚1º1ò2*2c2›2Ô3 3F33¸3ñ4+4e4ž4Ø55M5‡5Â5ý676r6®6é7$7`7œ7×88P8Œ8È99B99¼9ù:6:t:²:ï;-;k;ª;è<' >`> >à?!?a?¢?â@#@d@¦@çA)AjA¬AîB0BrBµB÷C:C}CÀDDGDŠDÎEEUEšEÞF"FgF«FðG5G{GÀHHKH‘H×IIcI©IðJ7J}JÄK KSKšKâL*LrLºMMJM“MÜN%NnN·OOIO“OÝP'PqP»QQPQ›QæR1R|RÇSS_SªSöTBTTÛU(UuUÂVV\V©V÷WDW’WàX/X}XËYYiY¸ZZVZ¦Zõ[E[•[å\5\†\Ö]']x]É^^l^½__a_³``W`ª`üaOa¢aõbIbœbðcCc—cëd@d”dée=e’eçf=f’fèg=g“géh?h–hìiCišiñjHjŸj÷kOk§kÿlWl¯mm`m¹nnknÄooxoÑp+p†pàq:q•qðrKr¦ss]s¸ttptÌu(u…uáv>v›vøwVw³xxnxÌy*y‰yçzFz¥{{c{Â|!||á}A}¡~~b~Â#„å€G€¨ kÍ‚0‚’‚ôƒWƒº„„€„ã…G…«††r†×‡;‡ŸˆˆiˆÎ‰3‰™‰þŠdŠÊ‹0‹–‹üŒcŒÊ1˜ÿŽfŽÎ6žnÖ‘?‘¨’’z’ã“M“¶” ”Š”ô•_•É–4–Ÿ— —u—à˜L˜¸™$™™üšhšÕ›B›¯œœ‰œ÷dÒž@ž®ŸŸ‹Ÿú i Ø¡G¡¶¢&¢–££v£æ¤V¤Ç¥8¥©¦¦‹¦ý§n§à¨R¨Ä©7©©ªª««u«é¬\¬Ð­D­¸®-®¡¯¯‹°°u°ê±`±Ö²K²Â³8³®´%´œµµŠ¶¶y¶ð·h·à¸Y¸Ñ¹J¹Âº;ºµ».»§¼!¼›½½¾ ¾„¾ÿ¿z¿õÀpÀìÁgÁãÂ_ÂÛÃXÃÔÄQÄÎÅKÅÈÆFÆÃÇAÇ¿È=ȼÉ:ɹÊ8Ê·Ë6˶Ì5̵Í5͵Î6ζÏ7ϸÐ9кÑ<ѾÒ?ÒÁÓDÓÆÔIÔËÕNÕÑÖUÖØ×\×àØdØèÙlÙñÚvÚûÛ€ÜÜŠÝÝ–ÞÞ¢ß)߯à6à½áDáÌâSâÛãcãëäsäü儿 æ–çç©è2è¼éFéÐê[êåëpëûì†ííœî(î´ï@ïÌðXðåñrñÿòŒóó§ô4ôÂõPõÞömöû÷Šøø¨ù8ùÇúWúçûwüü˜ý)ýºþKþÜÿmÿÿdesc.IEC 61966-2-1 Default RGB Colour Space - sRGBXYZ b™·…ÚXYZ PmeasXYZ 3¤XYZ o¢8õsig CRT desc-Reference Viewing Condition in IEC 61966-2-1XYZ öÖÓ-textCopyright International Color Consortium, 2009sf32 Dßÿÿó&”ýÿÿû¡ÿÿý¢ÛÀuÿàJFIFÿÛC    ++&.%#%.&D5//5DNB>BN_UU_wqwœœÑÿÛC    ++&.%#%.&D5//5DNB>BN_UU_wqwœœÑÿÂÆ"ÿÄÿÄÿÚ ø¹YÔQ% *QK(”E`)BaEEDP”EDQ‘KE$QE,QEªB™he¢EK–†mhfÑ–¢E¦mZ¹h™j®‰–sGc À,U”"«Šo)he¸e¸¹nj˜n&[§c Œ60Ü\¶0ÐËDËTÃpËTÃC- ´2ÐËEÅÐÅÐËC- ] ´2¢5MÃ- · Ú©4\´Œ´2ÐËC*"ª(Š"Â(ä•)(Ê1ÉÆeAPT.¢G$°X("Á@@ P€‹T#›—¯Ï›-â8U©,¥Í€¤¡1ÉÅZ TEi–†m¨MAF†Zhe¡–†ZhI±‹¡–†fÆla±†éÆänAÆämŒ9!‡ âr6m•jåi™¡–†ZÜq^AÆÝ8ç,8ÜŠãnr7 ârÃÈ0€ "À,@PŽ7!8܃Ȯ7 ƪP@@ ‚À¤‹D”›Ä]Þ4r^!Íž1ÈãŽ1ÈãŽ1Êã¼c““’aHXR(‘aPPn@¤Á`, ET NlãNwçpŽgæÏ¡ÏBP”E€Tš€ @b€YD @ (‹Â(% X¢ X(€…Í[¥”  P±A J ° "Áe% (Š% BR²5s D…*… «A` ã,*€¢H( D¢T-€ ‚¬¨ b’ËJ°K(°PYaPP,!B‘@@°T(–  Áe€J‚€ @DÊ@E”X ²ÀB€¢À– Y@ @( D¢P Š‰eJA* X* " ¨(* @ˆ P€* ””, °e– @ ¥€(ÂË”K ¢(°* ,TÔ$« †¦[‘¦F™.™di‘¦F™di‘©¤b•’i‘¦jÖI¦K¦F™&™dºdi‘¦F™™b¦˜bšdi¶Ø`m¶ØŽ;˜`»`nàmÆ9däq—‘¶Ø¼cwŒr8Ç#Œr8Ç#wŒr8Ç#Œr8Ç$Àä¼T䘼cwŒnñŽFØŽ1Éx‘¶$ÀÛÜÈÛlSLGä`m¶ئ™djâšedišTdTAPXT…`R…EÈXYv7¬õË]'v§EÞxt]âôg|tñÐw‡EÞ} Þvz'tt]áÑw‡EÝ'tt¯ptÝÁÓØtÝÁÓwMÜ7rGmGlu±ÔváÔvÇQÛGnYÛ‡UÚ[ÕvGZv‡UÙgdu×v]Î8ãÎ8Ôàs¥às“ÏW®ì8ë»ë;#®ìDàsŽ8àsŽ»°:îÀëÞqÁ;àsŽØw`p9ìu¯`uÝŠu‘ÖvGYÙgdu¡ÖvGZv‡YÙgdu‘Õ½”u•½gdu¨gfWd½gdu”ušgdu’õ˜uÝ‚pNÀàs—Î8Ã…Ì8\Äás0ás0ás48œ¥ârŽ'(ârTârŽ',·‰Í‰Ê99xy·›eÔYHH°,°€[%@ ,  €%‚ÁP*,P* ˆ¨] ¨* " ¨* ° ‚ °*P* `©@€* Z„©T€ ‚ˆ`  ”P*PB€RPR( "ˆ¨(•ʃ«Ï×ìuŲê,X` `X%d±`* R!`T …€€AP"€AQ( Š`ÂPTA@°T  J Q(€²¨À’‚€  …°P J‹e$PX) b€G³Õíuήn³@P@ ‘`€"X¤TE€!P!` @ ‹`AQ PiA@@°TJPT  (J°[°T(”…J²¬¤D …ŠJ¤P €R(!a@P BY@yý¾Ÿosw7y €X [)‹B¡l °,‚Ä) ‚  ¨Zˆ¨*P ”%‰@ ”XPPe€€ %UH (P¢P(,€’P,U¤” "ˆ¢, H*K<î×Sµ¹ÉeÞV X, BÀ°KE° @°,Ë ¨, %,PB€@BP "ÀPX((PP RP@(”QR(Š ±e”‰H‹HUŠ"‚ÀR‚€B’‰5*Íìõ{:œºæ€ R!,‰@B ¤€* ¨€€KÀ@,@°*P€jP@*P„‹ ( @P"””% 5 °Š(•H¬Ô!@ %€‹@€"ˆ°("ˆ°, ¨@ƒÎçáä³±s­åbªP”€@  @"ÀE°@P€X ,E@°@ ¤((,°YPP D¤ePATRP*% Š"I@H¢P(RU %R(”"‚‰e"€e(Š"Ê,$°D*+ÎåãÑÙ²ïPA¡X,*Q”@!`¤¢X@  ` ¡PBÁBPR %’€  (Ñš¤P(D )Q*Ä(¤ )J( J¤Q()()%‚P@ ˆ¬ë6%‘ѹ­vµ›¼[T@-€BªBX b€@ ¥€@ *À°P€PªHP …,¤ªJ (Ë‹R’@€¢’ˆ()E)@@¤Zf¬EÑ(’€¤RÆ¡*’‚À¤RË$¢€‚Ie%„–YQUÐ&uÚÖ7¬T´ ¢ aH¡ D€ Q% ab¬ %‚R V)( ™R‚E€UJP¤P)J () PQ‘@‘D TE‘¡%¤QJ‘ADPši%DPX&„QCFVID”ID”HVV$ÑÆñö·Ç½bØ«,, P„€D,"À°°JY,Á, @°¡H¤Š%R()T (”%¢P”UIhR-"¢P(” "‰@¡@¢•IHBZ"Ó6ˆ ¢(Í¢(Š"‰B(Š"ˆ°J$²¡l²Jz8Ö1®Ï'&³E€@!P!`(@%…‚€”%d@¥DÔ€RX%ˆš•E@ UE P¤ Q( ²•(()B€…  (‚€ ÅP@) EÊ(±( J¤ªCFmD[´B¤)RRB eB Ù@eU•~7Ž{ìrqrk:K`T@…!R’Ä,(EAP X@("U(RE„–DQQIQAD´‘U(E¨PQUJ¤PX €(KD  P‚„ªJJ (P²……´ E±±Bhf„ªfÑI¨J¤”E$X%„Q%”ÍYHa%Tš†TyÙÖyï“›ƒžæØ²¢¬  @ PŠ…°¨‹@¤Š¨¨Rª“Y"ÊJH UЍ¤Š"ª(•H«%R(‹LÚ"Ò-3h•ITŠ"’-"ˆ¢-"ª-"HЋH¤‹H´‹H¢-!H´ÊÓ+VU"ˆ¢P"”QTU‰T–Ø”"Ó-¢*"ˆ¢(ËC*"ˆ°K*," ’Á,¨°K¢gC*<Ég=Þ~¿bÊ.VZ€H¤* ˆ ”@¢* I5’,¨¤”¥ˆ£-J–ˆª–’U"ˆª5 U"‚‰hŠ %¢(ŠHÔ%º0ÐÍԨЊ#B4%T‹Ò4"ˆÐ ´"ˆÔXÐËB(¢*X ´Í ©UEj"‚ˆÐŠˆ¢(Š"ˆ¢’Ó*$¢J$ªÈ ¬¨ÊŒ¬"Â5 (Š<©\÷žÇ_5cY¨* € @ Z€J"ˆR(Š"¬Š"Â*³)"ª(š”(Ѝ¢*È´ ­¨ µ3h‹LÛLÚ²4#C6Ó- Ý ­L´%R-¬Ú"Ó6Ó-S Œ´3m3m2ÖŒ5L] Ú3m2ÐËC6ˆÐËC-6£7C- ÝE‹¨ÊÒiI©©e¢Z#HÍ£- µ¢5¢,#P’‰5 5*MC*L¬$Ô©*$ÐÌ¢M ªØ¨ò=㟃™9ë P€@-€ U‘DQU&¥‘¨EQD´EYhfèfÛYh‘DªKDiRê$´Eµ›IF„he¡Vn„ih‘¡–ále¡hF­eªe¡–†n†fÆ[h¹he¡&„›FZ›‹Ähe¡-²ª‘¤J«h™hejåla¡•QYQ&f¡VZXI¨I¨F¡–²%j,QâŽ[Ï7ÓjP€€°X@  RÊ @ JJTQ•¶eT) dªJÔZE¤RJÔªE©*‘VKB©›i›U›i›DZ™¶™¶™¶ÆmTZfÚfÚe¡–©‹ªbèfèféXººlbîF.©Çv1v0Øã»wc ÅËhÃcC7EÍÒ3ueÍЖŒ·#-Œ60ÐËC3C3qrÔ2¤’ªJ3435M Í3C-C+ 42¢(ðË˦98ör‹š(P Š€PP ›ï}§Ìóýv·¿]O}xù×Óãßa¿^>AõÊù×ä_‘}t>EõÊùÖ“äŸ[”}\>UõJùGÕÃå_T>VýB¾]ôãåßQ˜}8ù—ÒæßF>rý>yô#çŸ@<½ îÃÃ{ñ/µëÓÇzÐòž¶O1éÃÍz0óÝñÑw²tÝÌg`uÜãË MÂ.MI•äœc•Ãw^—VGmÓ‡uч}çæ_IæMåÃÕyPõžD—ØžD=‡%ögiâmâmâ%÷/‡cÛx¨öÞ)}§Œiâfø°öž*=§ˆ¯jøƒÞ׉ڳМwSQj5 ÍB(Ê“*©5+¢MD“EÊ¢gPŠ<ˤ°sYnB€¥PP% éô¾ÃYçæ³¯8¢PJ%R(‹Lµ(“R¤ÐÊŒª¤ÔH¥’’,¨°MBJÂM ÍÃ+t2´ÌÔ2Ô&y2ei†¡†¡™¡–¡™¨I¨fjQ™¨²jFf¡&¥fnjF'&LNH¸›É™«MÃpÄÜ—pÄÜŒMÅÎw%ÌÜ0¥ÃR$Ô$¨ŠX¶%‰t!eJ–, ,îóù½­Nç'ŸÚÞ{W’ȪËPÊŒµ µ ´2£*2Ô\¬ÐËC燒Ès¥²¢Ê‚¢(Ye, OOì>kéúc2·‰TŠ3h•H¢(MB)$Ò²Ô$Ô$Ô¨£- ªÌ´34$£+c3y$ܬÍ#3PЬµ 40´ÃPËCäÉݼý!ÕïCÎzýc¡}ž©ÅÒ÷ú‡—èùþ̼=G†ÊÅ8z^ÿJ1=ïø¾ÿ‡Xj.fäbnjhqÎH¸š‘‰¸a¨g;’ân›’ânF‹™¨fRæiQ&¤@«,T²„Q,–$–ÏW±¦;>Dõ;çLòªÌ¨’‰4$£*L©r£-C*2Ô\¨ùÑǤ–ÉlXJ*ˆ€ Y@>ƒé¾_êzóÅ5€ "ˆ ¢5Œ´2¤“JÌÐʌڬ¨“C- [ 7 µM ¨ÊŒÍÃ3pËP‹+- Mä“C9Ü=3—Ž;|ý ¦zõêpõøW·zDëúÞg$wz¼¯AÒľ‡sÁ·‡ÍÇg/W›ŒÌÒ\MÃ3y37“3pÄÜ17y35#9Ü17%ÄÞLÍ%ÄÔ‰EÃPÌÞe’ˆ²U–Z„°KA,©,'k«Ü³§sN÷¡æú[Ï>åÔ“K2¢MH“C BMC*Vu ,ˆ›z³¬œšÍ²„X*U¨*X ” CÛúÏû˜Â·ˆ¢-"ˆ R(Š"’(Š2Ô¨¢J¬Ú2ÐÃC*$ÐËPŠ2£-d -0Ñ35 5 ÍJËC9äɖᆡ&ቱ‰¸fo&Z†fᙸbnšF†fòfh¸šj›†s¸bh¸š‘‰¸bn.&äada¨¸›ÌLê.Vk1&‹•J,P©dE„—"–½Ñïêy÷:Žï§æz›ÏcMo8RI¨E.Z†fá•Q™¤¹š„š„QóVu“zÆì¨K`©VÀ*€”R(T”=_²ø¯µé‰+xˆ  R MB(ͪ“PŠ2ÐÊŒµ£-B,"Â(“Q2£+ 5 5 ,²Ô$Ô&w ‚MdJ342°ÊŒÍH™Ü35 ÍÃQfw Mà Cqq5 Í#Qq7#äÉœîK‰¨fo1™¨¹›É™©bÅDQ*æÅd©a™¬’X‘r%•=;Ò³ÊäÆåïú~w§Ó<ú]c3BJ2ÐÃEÌÔ$Ô2Ô$ÔŒ©rÔ2£å’ñë%†·ÇÉbÂTª°T ° € ­ßû…û­à7‡s§ÛÍqs÷s|NÇ=³ åô«ÉsðXw{2ùØîu‹Åètðz¾RY®]N¥Õ—¬õøÏ3“¿ÆtsÏØ®Œôñuïv+Évº¶'o’_=ÝìG•= ]«Ó:òÛ1=lfùŽçdò^³Ìw¶y¯^Ëãsw{Çη痳Ùçç¿ ±éqž\ú.#æÎn˜ë¾‡Ïκ¼>…N޽>y~mïuõ<—&,“JÄÜ3Ã3CpÌÞLÍÈ—9äɉ¸bnš†KŒòdÎwŽnF&ቼ˙¼˜jK‰¸g;†f’åQJD–YS:„ˆE„ôüïKO#—‹š=OÏô·Žkn³†òEjj.-:TfiY”²jFZ$³[›/-€¤,  ¤(  K- AÙûÏûíçRµÎsðßcÌfúZòÇ'±âC¹ÔkSŸÕð¦osuwº˜'¥æ­NÇ»²=®/#·›ÚëôÖr÷¼ÅzÙò’öýΕjoÖófoc—ˇ­¯/§æ[¬ÎLJõx¼ìâûZñ"çÕó™÷¼!êñù—/S—ÅV¸·7ŸW“Ås׳~¸‹4Š2£+u 5°ËC3PÌÞLÍC3JÎw’MC3PÊÈÊŒMC3PÄÜŒçpÄÞW3PÄÐÄÖeÌÞLÍHÌÔ\ÍIbŒª\ª35 5˜ÌÔ3dÌÖIC3QfucËö¼+8ý;Ó=nïS¹Ó”™Q•¶a–¢Àe¨ea%DÎòE8ö¬hرeQe U% •U©Bª¥ öõõë,Öȹµ"ÕÍ¢4ÈŠMeH¢4¬´2Ð΃- ¨Š¬´LZ.42¢(ÊŒÍÃ- 42ÐÌÜ342Ô1t3BMBMÃ*34¬ÍA5 ¬$Ô2£3y35 ÍC3PÌÔ$Ô0Ô0£y34ŒMC9ÞLÍåsÃR\M MHÌÔY4—3pÌÒ\ÍHÊÂgR35 gy3C9Ô35•’Ãè~{é~cY¾§™ë/©Üëv·ÏYÐÌÐʉ435hÌÔ"—3R$£ C-âG²ÊK(±e*QAe*QEQP –…¡H«)Iõ)õ»55‰@ÖÙJ*PZ°°° €@±I-2R,‰(€dJ!L¨’‰(Š1C5LÍBM ¨ÌÔ©BMC3PÌÖI5 Í ,$ÖIä™ÞLÍHÌÔ35PÌÖLÍIs5 çPÊÉdÔˆ©r£ Is5 ÍdÌÖLÍdÌÖLçPÌÖII~£äþ»ãõž_ÊõÏO·×ìï:„XE„Råa&¡&¤I`ÍXE=–R%9Rز•)AA@²” 4’” «RªJµ- é>s褚ΰåâäC‡|œµÕëú;>~Φ]Þ<^ öÇS‹Ðóõ7ÍÃê¯›ÉØÎ]{ÞêV¸uÇgÜô|îïSçë÷8“­ÍÃÜÓ<÷­z¾Ž§W“±™z}‹Ú<¾Ïcqåú3›£­N»ÌX%€ a@E‚Q%he`”fØI¡•„š„”fhfjVf¡™¨fjš„š†e–š„Îäbj”g;Êç;†&¤a¨¸š„Τ²jDT¹”eKœêFf²LêβfkÉ3¨g:QñÿcñÉÛö<Ÿf½.Ç>ñ&¡ eaeFT¹XI¬ÄXI¨Id±GÃ×U”ÉNJYAAAJ ,¢Ê eJPRШ- ,ÕJ¤÷³œw*ÜQ¼r“µÖŽ7p7…IÉ€äã’“yTU\ÔkNÏ^SPgpgyÜÜ¥€E‚PΡ¡ ЖÉf²&¥EQ%XID–YRjš†f¡3¨fj:†f²I¬’k1&¡œê.f²g;ÌI¬®f¡™©T²j.VD–,Îóš†e†s¬™Î¡1¡œØK9£øÿ­ù;;ÞÏ“ìW£ÍÅͬ&²EQ%Räd@eD–K• 9uƒšËbÊ,  ´²Â” ¢RÒÊ*‹-‹(¥*Š$õ<Ï@ûN>^6B” ¥)%¤‹j(ŠKjP(…$¡(ŠH*(Š$¢(’‰4Œ”ʉ5‘7 µ*M µ 5 ª$Ô2Ô¬Í 5 5 Í ,¬µ ÍC MÃPÌÔ37˜ÌÔ0Ô\MäÎw#pÄÑxç&ce£9Ü\ÊŒÍEÂÈËY3ÃŽo&q¼™Î¡™a;~Ùêü¿Ò|åž³äû6w¹xùu™5’,"À°“Y–MB,$¢gQd¨Ê„ºÔ aÍs«((*ZP  ´-––R•ZYJQV…ERw:ƒï8¹x™YEuìì߬}EùRýUùQõO•§Ôß•'Õ>T}Så•õ7åGÕ>X}KåGÕ>X}Kå‡Ô¾X}Kå•õ3åÉõ3åáõ/–RùQõ/–Rùaõ/–‹õ/–Rùaõ—Nùôï—Nùˆ}<ù‘ôÓæGÓ>fO>d}3æ6}çò} çQôSçéïÏE>zBùè} çV} ùè} ç¡ôçâý ùø}ùñï¾~AŸþ|({¹ñ!îO ÓÄ/µŸG±xpsÎ sg‡š¤ÖLú>w¬kÁö|‹=?gÉö,îrqòÜÉD€–`”¹XE„–Bk$–,Qðc—PÖN^N.[¨E”YE” YlYKe¥•-–­”QZœ¸±úðÊÊq|ß§åêp÷ºÿW/‰¯fÇ}šxר§ŽöUãß^ž=õéãëÖK×KÖ'”õ‡•}Aå½Aå½EyNž]ôǘôǘôÇšô‡›}ç=ç½çÞøè=tñнêtñÐwÇEÝ'vtt¸=R|uæÅajß Øñ¥ãnç\s–'åS–NXqç”q9aÄ凖NQÂå/•NQÂæ.h¼NTq9‡ ™.bð¹‘ÂåNQÄåG–/q9åS–NHqÎHq¶³“;á³µ Ç·â}y¾ww©^§±åzöv¹1»˜h’ˆ¥È!°‚$°“YX£àÇ. 3¨o—‡žÉJYEJP¶PRP¶[AEš¡R”Y ¶–TPûÙ,5'æúÞN¥úï’ûxÛ±‹ªb꘺¶™hfÑ›DZe¡–†ZYEDQE¦Zhe¡–†Zj¡–†Zhe¡–†tÑò\|ü:/ŸépGUÜ7ptÈu'v9Ý)Ý)Ý•ÓwIÜ9ÜGMÜ9Ý/IÜGMÜÉÔwñ/MÝÁÕv¬½GnGnWjWjKÕv¡Õv‡QÚ‘Õv¡Ö¨u§fiÚ‡UÙ‡[=™]lvúㇱÁ‰¬éŸ¤ù¿¨“ç8ukÖõü¿VÎÆó«˜°’ˆ!JY(’Å’‰K ¨ø1˨ rk±ÖìØ-J¶X¢Í‚­J%(²Š´¡lÒMJ(ZYh}Æøy¡eO7Èö|mKöŸö¹ÖZFZ¦ZºhfÚe¡–‰–„šhfÕE¦ZG$f\»âÇkˆâi¬å¡–†Zhe¡–†Zhe±†ÆlbêŸ'ÁØàÜÊÁ©Ü:®Ô:ÎÔ:®ÐêÎÜ:®Ô®«´:ŽÜ:³¶:ŽÐê»Pê»HêÎÔ:ÎÐêg¹™~‹—žð÷íu¬ø÷s=gZv‡Vv¤u]¡ÔvGUÙ‹Ö¨u]˜u˜ugk'^vbõçb|öbuóÙÉÁÒõ<õáëö8syæ³XúÏ“úù>;—ƒ´zþ¯™êo=‚¤–BXE„”@±`ˆ%‹%‚Q™DÁ]@=®¯fÍXª,YJ e° RŠ¥QT¥¥T¶QZ$Ô>dzÓîDÔ©Ññ=ïSkñkY¹›–†n†[&Zj™heªaµa±†ÆféÆäœƒ ‰6—9änEœnAÆåNQÄånAÆänAÆäGØãr;±Æäw|‡_·ÕíŒË z~w¸½IÜ’u±ÔvÇRwÓÑÒwM܇MÜ7ptçtt§zGI݇MÜ/O=áÓÉIܑѽÕt§zÞ‡Jw¡Ñøtètg'FwáÑøtâô'xt'~ Þ‡G>„:_ÑøŸÇ¼ã|ÙÞ+?añÿe'Ãwº>‚úþŸŸèïñd’Œ, X°@„!,Ö@>\ºÅ³ÖìYÈ5P¡eªTšE–¨•-”YF‚ÙjÙ¤U«(úŸCÌôò–ƒç¾§çlÇÛ|oÑç^£nwC-XÅÐËC6ª(Š¢ˆ¥Š"€)“L24ÎNGäpŽgçpw;¸ì:ã°ëÃÏ<Χ/~E‡/Ñx}¹}Žg 9ÜçpC±8ó¸"vt½‰À®Ã¬ŽÌë²ë²ëˬ;.ª;S­Ó«Ó¨;s©¶éŽÛ¦;nœ;“©äéøéã¥ë£ôèüèC¿:½:=“ïùgAžlé.j}‡È}l|W¥æú‡­èô{ÚÏ*Ä‹¹Yd¢K±YÖAƒàÇ. ž~k9–j,¡T²‹*QTÊTÒ(*‹-[4,Ò,вճ@Cìx~î\vT×.{üã½[/•}Zy7Õ§“}aå_VžMõiå=Qå=aå½Qå=ZyOTžCØ;Ø;اŒöUã=˜xï`xï`xóÙ3Û‡ŒöIã=šxlxlxlxhx¯n+ÚWŠö‡ŠöG¯^7¼¯ïÄçôáäOb;؇‘=ãÏ`xï^DõÇ“=hy/Ty/ZSÔG—=HyN/šôdyÏ@yÓчŸ;ã¡;Ò^”îC§;™:“·ªìC®ç‡æ‡ å‘Å9!Ç92¸›†&¡œê2k«Í“Žë&f²oé¾{Þ’õ<ÏV½n÷O»sÈ„@€` !°K ‹ ¨ø!˨—Gm[̲Ш «BŠ ¶*’¨ªŠ­- ªzÿGòŸ]›Ö°”¥JT.jæ–ÁniPPT(APTP !HX%€` XE„š„–`– a%„šÉÈFHIr3a3¨¹Î¤fXLê––.e„™d¹–6βg:É%†srw½lj­åúõêw:½«¬HA(„T¹Tu ,ø!˨Kxo1V,¢‚­*€•m *•4J¨­T³E(Ô£RÙJ_µøŸO7è'o« Ál-²•)R•(Ê,(P„ BU@E€` X‰F@–XE‚”B‚X²Y\ŒØHȈIs*2%ÈŒˆ‹"!#+sdLØHÈÍÈÊ7&{\ŸW_Úë/gÖèz¶wû\÷6X‰a a* –(‚\‚|^]H,õÎ÷˜«ZjQD¥¥BÑ-Qeª«”´)JRã’¯Òüó~×ÉïÇa×›ÔGnõnôÇuÒçDwwïž=ž=œ=œ=œ=œ=š='š='šOIæÊôÞ`ôÞXõ'˜=7˜=+åQåQåÃÕy#ÕyCÕy#ÖžHõž@õç’=g’=iä“ÖžT=g’=W”=G”=W•UåVyJõ'˜=9æCÔžd=G—#ÓyƒÓy…ô§š=)çCÑyÈô'ŸBt"úχ¡žŒ;ù製žžNìéû:y;ÙéŽÛ¦^Üéù:híΠí:ƒ·:¹;s«Ó§“¹:’½Gæ:'Øü‡—›ß7êp÷µžN\é‘BÀ„T°@@K,!‚W.°͇s“‹—yɨª¡U(ªQT(ji%ZUQ©«”¶RÕ¦¦£æ>¿£Çn=9zNì—©{c©{š:Nõ:7¿N…ïŽïÓ¡{õ:Ð ßÕy×ÑuôiæßJžeôªy—Ó§—}=SÕ§”õmy7Õ±äßVžKÖ§’õÇ’õêx÷×µã½ä_\y`y^ž;Ø;ÙYã=‘ãÏhx¯hx·Ù3Ú/Šö‰â½¡â=±â=±â=±âOq$÷ âOpxopxoqÛ/ˆödžöÑâOr$÷!âOrÜ‹âOrÛ‘â=¡â=¥xjGŠö‡ˆö¡âÏhx³Û‡‹=¨x¯f4öG‹=™^<öG‘Ëéò'G¹Ùæ¬v‰5‘,A¢’Á,H,Â,X°K£àG. \Øvyúýä,YªY¡U%ZU% BÒÍ–Æ¥)EQT¥ªSEŠh“táœôë^ÈཊuÝ]v4uµØ§_]kÙ±Ö½¡Ö×cGVöi×½škÙkÙ{اZöiÖ½Šu¯bkا]ا]اZö)Ö½Áyé×v)×v°:÷œp^qÀìCØqÁy‡ œp9Çž§^ó—¯yÇœp9Çœp9ÑÀç˜pÎqÀçGœu݈pNÀë»ë»àˆpNÄ8'`½yÙ‡Y؇^vIÖ‚õçfyÙ‰×Öv"õçdu³Ú‡ZvIÕ¡Õ¨uµÌ8õi$XE– `$©`– ` %h|¹u€°æíô»ÛÌV¡Bª*Т¨*RÒ¨«J¨²J[)lÕ*J5)u)jªZSJªZ¥³H­B¨ª*­(ª(,¨ªM‚‚¨(‚€ Pb’€¤PAR€€*Y e%‚ @ @@ B"– eE„X%’¥aÅ„Qð—Q ‚Ë ú>o§¼åZ‚‚¥²ÒƒJ“@¥Z ¶- ¢­*”Ъ*”гIi M ¥Ô¥³JÔ©j–˪ڢ¨ª Š¢Ê*‚’‚Ê(,   ( T(P–€  BP‚!`I`@€–‚X X EYQaJX%Š–|9u°*PBú¾O­¬¦¦âZ b¨**–ÍTª Y(ZµR¨ª5)jŠ¢©j¢¨ª54[,hС«)j©l«l¥²Š¨²Š ¤¡J((,¡al€ ʰTT° @©` €€ˆ%ˆ€–*¶– `–#6RX %€DÅ Ρ~}c—['­äúºœ¹ÔÞ"‰jš•EZšQDZ¢ÑTU¥QTU)¡f’jhU)F¥-š–5e-Q©KV-”´ZR„¶R‚”YEš%PP P,€‹€JHA,QDK°@D.h’Á,T° ¤°J$²¢ˆ,ˆ¥€Š?>'.¥€ )ŸOÌôlíçy鈪UEQV•I¥²U&©F¦©BжRÕR”¶RÙKTUšbÙ¢ÙKf¥jQTY¡TYQB¨P P¥  ((%”@P‚€€¢,"ˆ°@@„ÁK $°„Dˆ@°‹X¹Rˆχ.±`ÊgÐèw+ÒÇ&:sËR”KTUª¢ª•QAT[hQf’”jRÙ¡BêQTU- T¥‹©Ke- H¶iVhš”UEPUQR@R(€€€b,„ÁK°@@$,°@„À*"‹! ,%¥"Á,RÂóÙ\ºÅ`íu¹o—¯9*”F¥-š¡EQKEQT(U¦‚ÙRжhUKe)¡f…Qf¢ë:”¶R•Z–-”P¶h´UU@*P(T X*P€ ¹¢K@@€,„Á €KBÀJˆ,¢ˆH,€€ („%óÔrë@ °98ô}&zó¬•EZUiZP«AJQTU¥šEšhP¶hjQ©Ke-”¶SE‹BÙ¡UjhYbÙJ”µE”·4¥RZ A@%(X* ‚   €@KA,"$²,‚À€D€’Á(‚"À (Š$¢,,°¥Š?:/.¢} ˜ã•¬Å gB­(4XÔ¢… ¥Z¢ª –ÊR”¥M AjJ5)l±hZÊ(¶‰¤Ô,¥M攪°[*T¥ÁAR‹(ˆ¤T¤X„%‚X" a%” €ˆX…€€Q%„Q•4J @%¦j*Q Dš‹$ ùÈçÔÀÃÐö_}à ¿<>óŠú;ódúGÍ¥¿5§¿.>¥òãê/˪¿(>²üúëò úûñôû ñÃìŸ>Ññƒí#í¯Åæ¾Þü8û—ÃXû‹ðªû§Â»|6O¼¿“ï/À¿¿ŸÐ_ŸÐ_ŸÃôçÃôçÔýùü?@~~?A~|?A~|?A~Ð/çÔýùøû÷çãôçãô ðïŸ>ú|û·Âîç£î_ >áðãî'Ä·Ÿ>ÕñCíO³Ÿ>Ï>Â|…>º|ˆúéòCë&«Ÿ,>¡òãê'̦ÏÎCèçΡ|ðúàCßž}¹âÓÙžD^y4õsæNy£Ñyôï:¿:5{¹êÜêÈíºƒ²ëS±83Š®ò)%išÏ ârÓ†sSÌ8gc' œp9DZ Ê8\ã…Ê8\Ã…Í'%®',8œÃc Ž÷“îxy¡¨ Ø`m¶$ÀÛl °6ÀÛs$Ó#L2]$M1M26ÀÕÁvÅ-Ì6ÀÛl Ì3Kr4ÈÔÕÅ4É4ÈÓ#L—L24É4ÈÓ#L2­24ÈÓ#LÒ¤4Ò ‚ ¨*" ¨* ‚ ¨* ‚ ¨* ‚ ¨* ‚ ¨* ‚ ¨* ‚¡j „¨* ‚ ¨* ‚ ¡P@*P* ”@¨=ïžúošÆ Þ@@A €°b€X, ‚¥B ° ‚€‚ ¨  ‚ ¨( @((P,°¶Z‚Å>¯ä~Ãäyë)zä"PŠTª”J BÀ¥‰@% (€* ”T) @ ” P,*P) P‚ ®ùOªùžZêŽÙ” !h€"€P°€(€¬”–P (Q,¢P€Ô° P·:"PHB D(@`±D ú¿ÞñxkÍòQK€XP@,@,JAA J( @-@ Ò%PY`P²€*€¹,X(‘@}O—êùÜ7âø¨ PHˆ¢QAB  P@%(ŠH¢(JKL´2ГC-S*X´ËDÌØËCC-— Ž;ºœnAÆä/q¹)ÄåNQÄæ' š¯œp;®ìS¬ì»³N­ì£¬í¥í«´:³¸:ŽÝ:nå:Nðè»åè;äè;åè=ç=yÏGG˜ô’ù¯JžcÒÑå½Aå½JyOPywÓ[ÓcÔ[Ô[Õ‡—}8yPyOG’ïq×YÙ§UÛ7y×WµÔã¿éè狨EhE,áç‘Å}íâüûèóï §Ï>†Ÿ:ú!ó¯¢ä>cvôÌUEDPQ–¡EDQ™´fÑQQDQ(` B’¡Qf‹APjâ­Hm˜m˜nñŽGä¼C•Æ9\C’ñ#™ÃNWçpSšðyÂ9ÜÀ;¸ì8=ëXì^­;.±{7ª;W©äêC»zTîÞˆîº#¿|úwÝzóG¥|Êz4zW͓͑éß-^£Ë¥òlzóȧ­|ˆzï!^µñéë¼Ìž­ñG¸ðÇ´ñaîO{ZðÇ·ºy }\†Œ»}@!@‚¥B ‚… ¨* ”B€D °U€P@¡@°(D°¨ZB€¨,” ¨-ϯœùO«ó¸òñÞn¶ë“ZàoŒ¨* €*  ñMk·Ó'!œÎsŠnaSê¾WÑç¯{>3–ý—má[‡Ÿ üÈõa, %s§s}7A›ßtßwÞ!Ýáà–qj5/c¯ÙŽß7šœ;®^NŸ<&KØuyZãâ^~n¬N®MÀ@©H,)P² XPP¨PJBÁh”ÉÆO¢êyóÏÏ5ß®õÄ·“Šˆ¤Š#EËTÃq¹”qÎQÆÝ8¯ ãrŽ'(ârŽ'(ârŽ'(ârŽ'(ârÓ…Ê8œ¹1É4nñ¥çàÖMÞ8rÎ2qµ*sñî*%± ƒè’ðßÍqý•ß'&u1PXKMktÊ4Í*CL“R+&M3J‚²4ÈÓ#L24È·$Ó#LH4ÈÓ#L24ÈÓ#R 24ÈÓ*Ó4© "* ‚ ¨* ‚€¨,e"ˆ¢-\´2ÐËDËC-Œ] ´37W ÓÉN' ãrS‰Ê8§0ásŽbw=:îÅ:Ó²“­{UzŽØê;c¨íÓ¦íÓ¦ï¡{Ôè;ã¡=ç=ç=y¯NÇ–õ2y¯JžcÓcÔ‡˜õ™=:yoRcÖ‡”õG”õ–õG”õKå=jžCÕ‡–õG”õ–õaåëÓÑç_A/Ÿ{ù:W¸:nÐõù=S¶8Æå"†„ (@(J#%@ P (ˆ (% „€Q €( ¡ @¡@F P( @„ aP (@(@ ((P¹ ```)@¤°$¡B‚(J„ €ÿÄ3!123P`p "0@#$4ABC€D ÿÚ÷jp_Ê¿#'â©ùWôcÞðAAzºqoüÉéÑí‰$’I$’Iõ¤â«ìÕQÊCˆq!Ä8‡â× ‚ ‚ [ŸÎ½ýgý£Œ†C!‘LŠd/Q^'¿:N§S©Ôêu:N¤©+ÂWS©×Ø ÒÕ-RÕ-RÕ-RÕ-RÅ"?þ'è’I$’I'„’I$’I$’I$’I$’I$’I$’I$’I$’I$’I<$’I'Ó`‚ ‚‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚=ùYoË-ùe>YO–Så”ùi>YO–Så”ùe>YoË)òÊ|²Ÿ,§Ë)ÿÖiòÊ|²Ÿ*ûU>YO–Så”ùe>YO–Så”ÿë4ùe>YO–Så”ô¤E‹”Û<Ú¼Ú¼Ú¼Ú¼Ú¼Ú¼Ú¼Ú¼Ú¼Ú¼Ú¼Ú¼Ú¼Ú¼Û<Û<Û<Û<Û<Û<Û<Û<Û<Û<Û<Û<Û<Û¼Û¼Û¼Û¼Û¼Û¼Û¼Û¸ÀãŒ08ÀãŒ0¸Âã Œ.0¸ÂãŒN18ÄãŒN1©LjcS˜ÔƦ51©b˜Ô±K±K±KT±KT´´´‚ ‚?,’„¡(J…È\…È\…È\…È\…È\…è\…è^…è^Ó"ȆD24ÈÓ#L24ȆD24ÈÓ#L24ȆF™di‘¥èJzzM:*áštB> GñÐÒ(Ò¸c©ð{\I?¼žŽÄ•¢ÈO„x"ˆ¾ÄÓ6\½y´Ùm”F±®sº/÷UˆÎ4é1[Ž‚•b¶ƒQ® ÕJ4‘â¤+(Í:tQ[‚›“×›ÚDQØzNþ¾‰4ŸNÒ‡v±äªæ&Um4*5­uLhTkl)¤ÑnžÎGÖÔøôË÷RHuv#’)äsÚÆ¥¯]`½Ðj‰ì-_ÿġ݃)¹ôþeZˆµQ.Ô ä ò(ºRÛ*Ö¦¯)S°¢²6.º›Vª½]ëÔÅh‚{Gß×îXªÑPÊòUW5A\ª®r¹U΂ç[Øs•Ê•ÑÏ{„sš—¹÷Z‹ ç+—×™áàѾÂÑø¾o– Ñ¿¸ž¤ñü$žPƒFþâz6—ÌøKÿ! ýÄôm/™ð“¼¡ öŸÌÿ\RŠª-AÔÕ¼,[~¦¶áR¬WpF*§àkUÊ© û˜ÝgSs×jñåWiê519ÚNr}¦¬V£Š”œ¬ö=O,Aƒ}…CÌNÜi/ÝWÆëaôÚÆÛ’¸éj5JmkŒMÈôµÔGx©xx1%Îm&j9ØX6“g7z>§˜Ö6Õ§MX”ØÖ¾›D¢Ø«NÞšŽT£÷¶š9Øé91"Re6«1SF¿I\tÂËшµ’‹ÎM:é1Ì~Jte*Qm«J‹J´˜ÑØñ,I¥ìƽƒ\˜ô¯WµÍºš6Ú#s¶Ôæ­&±uц™~¥i­Û-.§Fc­»Ø•ü#F ì*^bvãOÅWÇR1¹‰¾[;•¢â’ýÅ5þJ¾:#üT¼<)ø«xé*#ºËZÜe%š•<ćӋ)¹«-e1eJÊœ°ê޵šuHkŸÉJÛ)Uò¸1aõ)^´ñ¶¢SŠ­T¼oøå5šuª5 o æÚ¦›µ'ÞÚVÚˆÝ;Z¿kÕ,aÒ…Uº¾¯½•kÑ•ÿñÓTM5-2=ŽDG{ ;ê84`žÂ§ãooØ‚›Ñ¢õ^1Â>†X…GßÂ8G¹ÙãÔ÷0Oa7Äß¼ÍWˆhÁ=„éø~¡æê|c ì$)yi|ÍO˜ƒ‰ì:_ÑI QÿV4°bJÕ^¿C)Á¬Eiׄ/ IÕ#ìc“‚1¶½ˆ‰ÁIGÛu$—/ßUícQ¶Ÿï© BŸë„/^ Šœax"*ðµÞÓÒy•üÄ0Oaéü¯£ÁNfºñ©j$To‹k]-W+ŠIÒšËÿöO_rµ¨…7Þž"ÜÊn¹²¯^츩ãðR£àÓ‹âŽyUÜXûJè“à¥Eak9f‡wxœëz½ÜUÏmIzwZÎrQj#_ ñÔ©`ßä©’*#Q%´t}ëxÐ`Ñ=‡¥òx±%k)MèÓ#PeH£aµQ!„[U_ö±‹jä§,uÎZj²¤ƒj™ßh¶™©“îÉN|u+,º›í259­·*TcQËLJŒDj­Jg˜úË*ŵÏ[MÈÑz«ÞŽjwÌ’µ¾Üè%_¿-9JÈ'fUF¢ÔHcØÆ¢ýõzµÖªÖ§-­ ™ ޽ÞÑÒv«ãAƒDöÊ⊨/_ª®âŠ©øÑU?*§º´¾]O '°ô^_Â~”âhÁ¾ÄÐø>¥þ:÷iLo±4¾ˆ_Hî**{:i¿ÛF ö&ƒ¿xœëJ“ŠŠœ’#Db¨´Õb¢X°"JâQX¦%1©‰Jh=!x[öG\J**(ÖÜ-%NQÌ´m9G±Z%!ÌV®×]„Fª«i{Z‹åû‰Ý4à ö&‡ÅŽê"¨«¢ÝW…5…rÚÖx(×ubøîOß¿ýÒã{Vî ëM©kœ×_W¹H¦×4Ÿã—SìÊr¨åU?ï+-ïI®hõUw¸S½~šqƒö&‡Ì_£#ÅUS#¸±¨éUs”G9 œ¼nqÖoys„”ás—Œª *\î4Õ¹ÊJÁ*‚Ê—8ê¥Ï?ÝÏP¹ât÷|Z¯ `Á=‰¢óWàöxõ¾Z '±4žrüGÌ×öAƒö&—Î_ƒôþoü‚õAƒö&ŸÍw°ú>ž‡C¡Ðèt:‡C¡Ðèt: BP”% BP”% BZKIi-%¤´–’ÒZKIi-%¥Í.isIi-%¥Í%¤´”% BP”% BP”% BP”% BP”%>­/¯ñ Á‚~úz-/1~—ÔkÔ=LÏ38̦e3)™LÊfS2™ŒÆc1˜Ìe2™L¦S)• ˆeC*PȆD2!‘ ˆdC#L24ÈÓ#L/i{KØ^Òö°½…ì/a{ Ø^ÂöÓ/a{ Ø^ÂöÓ/a{ Ø^ÁÅ>ˆÆ—R.¦]Hº‘}2úEô‹éÒ.¤]H¾™}"úeôË©Ó/¦_H¾‘}2úEô‹éÒ.¦_L¾™}2úeôËé—Ó/¦_L¾™}2öS/as ˜\Âæ0¹…Ì.as š\Òæ—4¹¥Í.isKš\…È\‡E;}:$þ]w˜ÑƒýôôVx¿×ºÔUW+”fŽSeLÙS6TÍ•#eLÙR6T•#eHÙR6TM•eDÙÑ6tMgDÙÑ6tMgDÙÐ6tMg@ÚP6t ¥i@ÚP6” ¥i@ÚP6” ¥i@ÚÐ6´ ­k@ÚÐ6´ ­k@ÛP6Ô µm@ÛP6Ô µm@ÛP6ô µo@ÛÐ6ô ½o@~’’£%ÅËkâç8¹ÅÎ.qs‹œ\âç8¹ÅÎ%IRT—¤©*J’¤©*J’¤©*u:N§S©Ôêuýµ~™­óZ0hŸ¾žŠÿéÇR¼)§óúý_ò—¿ Ý•:ÁAAAAAAAAAAAAG#ê{}ûµ~kF ôôTîÏ+ާƒ<ÿ_«þZ÷á[´AAAAAAAAAAAAAp‚ãpQݾmGšÑƒ}=—“ÇSÁ<ï_©þZ÷á_´AAAAAAA ¦÷ˆ×9\Ç5`‚ ‚ ‚ ‚ ‚#„p‚ ‚èÐùu¼Æ è èÔ<Ž:žÇþ¿»/D‚ z§ùkß…~ÈÞAA¤A¥¥¤ZAZZZZA¤H¢Ú(ʵ4î­YÚŤAZAAAAATîñ;ñÒyšEîÁ‚z z.‹Ëã_À8o‡öã„A¼ïóW¿ Þ$NAiAiZAiiiZZZAiiiiiiiiiiii¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥_5ýÓè^š_öÁ‚z z.‡ÁƪKxYàõçš½øVñAAAAAAAAAAA¥¤AiA¥¤ZZAWÎwtú+tÓ¶ Ð[èº~еÑstÕQÍôI% BP”.Bä.i{ Œ20ÉLÉLÉLËLËLËLËLËLËHÍHÍHÍHÍHËH««¦Ä¥(œ\Ü”éjY¨™è™è™èŠ⸠n(Šâ¸Ó›Š⸠ntæçNntæçNntæçNntæëNntæëNn´æëNn´æëNn´æïNnôÆïLnôÆóLo4ÆóLo4ÆóLo4ÆóLo4Æ÷Lo4Æ÷LotÆ÷LotÆ÷LotÆûLo´ÆûLo´æûNo´æÿNoôæÿNoôæÿNT×"‘jw^)ßUÓNƒ è-ô]uïÅìG¢µÔÖÄj uº‚ÝAn ¶¹mrÊå•‹+U,ªYT²¡eBʆ7˜ÞcqÆ7ÔÄb0˜P†˜Zai…†i˜i˜i˜iih˜h˜h˜h˜¨˜¨¨¨¨¨ôæ::=9NcÓ˜ôæ=9NY§,Ó–iË4åšr( ç9ËÅ®VŠ´ÞY¦-Óé‹tźbÝ1n˜·L[¦-Ó¦#LF˜)R4¤iHÒ‘¥#JTþ©ýSúÇõêŸÖ?¬Xþ±ýcúÇõëŸÖ?¬Nœ9:rtäéÉ NœºuêÔ ¨—Q.¢]Hº‘}"úEôËé—Ó/¦_LÈÃ, sˆúã×yH0hž‚ž‹¡ñ»¿Ò´˜¦ f f f f f f fffffffff*f&˜b¦ba‰†&Øca†6Øca†6Øca†60±…,icKZÒÖ–´µ¥­!B„!B„!B„!B„öU$þM… ô$ô]˜îÿi›uoù0o¡'¢éÝðn‚ŸM[î¨ÑƒDôôTXZNÉOàº4Uú‡¶…%YV Á¾„ž¦­c•.O‚héêV_âÒS¯Yj¹ A= ;z- R°K*"µSÙÝxu:HR…!HRCˆq-qkË^Zò×–<±å,¨YPÇPÇPÅTÅTÅTÅXÃXÃXÃXÃXÃ\Á\Á\Û×6õͽsm¨6Úƒm¨6ºƒk©6º“i©6š“i©6z“g©6z“e©6Z“c©6:‘?ãë¨Ý E]}6%J¯¨£Pj A= ;z3j=ƒ?ä î™MÞŒÝèÍæŒÞhÍæŒÞèÍîŒÞèÍîŒÞèÍößhÍößi ößi ö”ßéMþ”ßéMþ”ßéMþ˜ßéþ˜ßéþœßéÍþœßéÎa@æ ý˜Q9…˜Q9…#˜R9…#˜S9…3˜0æ,9‹NbÓ˜¡ÌPæ'19‰ÌNb§1S˜©ÌTæ.9‹Žb㘸æ.9‹Žb㘼æ/9‹Îbó˜¼æ/9‹ÎcPæ5cPæ5cPæ5NcTæ5NcTæ5NcTæ5NcXæ5ŽcXæ5ŽcXæ5ŽcXæ5Îc\æ5Îc\æÎc\惘×9…s˜ja¨9† æƒ˜ja¨9†¤æ“˜jNa©9†¤æ“˜jŽaª9†¨æ£˜jŽaª9†¨ßêþ¨ßêþ¨ßêþ¨ßêþ¨ßjö¨ßjö¨ßjî¨ßjî¨]V¡EsœA4kF´DôFöôhB#ðAAAAZZZZZZZZZZZZZZZZZXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXc1˜Ìf3ŒÆc1˜Ìf3ŒÆc1˜Ìf3Œ°Æc,1˜Ìe……………………………………………………………………‚0FÑDg¤Aiiiiiiiiiiaiiiiiiiiiiiaaaaaaaaaaaaaiaaaaaiiaaaaaaaaaaaaaaaaaaiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`#Ñ™éPAAZZZZZZAA¥¥¥¥¥¤A––AAAAAAAAAAAiAAAAAAAAiAAiZZZZAiiiiG£³Ô ‚ ‚>˜#éŽ0Gú ‚úà€YòË;ü°Þÿ,'–Nß+ ¾Xg‡åŠ}¾X¥ÛåŠ?,Ñùf—–)x¾X§ßå†÷ùa;§o•Ðo‡åŠ~–)x>X¡áùcOòΟ¿Ëü±GÄ¿,Sñ/Ë ïþ¾WAßsÁ ‚‚ …!H!HR…!HRÕ-RÕ-RÕ-RÕ-RÕ-RÕ-RÕ-RÕ-RÅ,RÅ,RÅ,S˜ÔƦ51©b˜ÔƦ51©LjcSˆÆb1˜ŒF#ˆÄb1ŒF#‰ F#Ć$1! HcCÐĆ41´Æ†41´Ä†41¡ m1! Hci¦6˜ÚcCu%œ/1<Âó Ì/0?…oAp‚ ޤ) B–© Z¥ªB–©j–©AAAAAAAAAAAAAAAAA}é±øcóÇãŽ1Â8t:p”% BP”%„¡(J„¡(KIBP”% BP”% AŽeÛclm±µ6Ü+x?ŒmÊ” 0 €À`LO_ÜžôI$’I$’I$’I$’I$’I$’I$’I$’\I%ÅÅÅÅÅÅÅÅÅÅÅÅÅÅÅåÅåååååååååååååæBó!yÈd2 †C!ʦE2)ȦE2)‘LŠdS"™ȦE2)‘LŠdS#ŒŽ28Èâ÷¸½Åî/q{‹Ü\â÷8êu%Ä©*J’¤©*J’§S¯í"õewÙæáæw™Þf þÇC¾E2)•L¦U2™TÊeS*™T©YÈÏ|¢*¯aWÔ“¨© ©¢´´VÀ±ôR_ãú«ø?ÉWÃÅE§ ïMnI•RƒQ¨Œ¢p_NN¨ªuS²w³ÏúqmEDʦU2©•L‹Â¯ƒñ±ðd2 †S)”Êe2>S4êþ¥¨ÔDE©.„-EFÓAW«š‘jZÆ¢«¡¤$/s5Ö®‰UÕµ?muYàœ}5¥ÝUUZ*Â*\/DàŸþÛb¢ þªôRPcÑ:W"BT.lº¤©ÑjôkàW5E©î–=Ìårþ8 …-RÕ-RµKTµKTµKKKKKKKKKKKKKKKKKKKKKKH- ‚ H ‚ Ž)ørBü$’I$’I$’I$’I$’I$’I$’I$’I$’¤©*J’¤©*u:þ>§S¯ë¤)j–©j–©bñ{áíaЂ ‚R!HR…!H … …!HRÕ!HRÕ!KTµKTµKTµKTµKTµK±K°±KT±K °°°°°°°°Æc1˜Ìf3ŒÆc1˜Ìf3ÐÆ†41¡ hcBÄ1¡cK±¥¨Z…­-ijÒBÒB‡C¡Ðè}§C¡Ðèt:‡C¡Ðèt:‡Cí>Ó¡ö‡C¡Ðètã$’I$ñ¨åW/ ‚ ‚ý#ðÇÆ_ÿÄ*`1P A!0@aQ"B€p ÿÚ?ÿ_lcÆ1ŒcÆ1ŒcÆ1Œcÿ¥s–Î[9lÿÌüÊ/%¤´–’ÒZKIi-%¤´–’ÒZKIi-%¤´–Æ1ŒcÆ?Š„!B„!B„.ee½yoVø\ð]X\ð]X\ð]]ˆZ-gD!h…¢…¢„TZ!ÑH½P· <Wc¹F= ÆyÑŒ‚% zX±m,?QïÃOÕ„ùÒx.­µ^âì\·'‚|öG|žt üžLŽA;žÄÿ"x9Â|“ÁøÂcrxu¢-¢ˆÏQn½Xö ‚D!%+Äë…¿®ž÷0¹Iþv5(£(϶ÅêjJ"ˆª*Š"«&jjjjC5!ò«é?½oãÂaÅ=I$”I$’I$’I$’I$’I$’I$–$’I$’I$œ°ì»gÿÄ7123!‘ 4A`¡"Bpq0@PQa€¢±R‚ÀÁÐÿÚ?ÿæ#7ùœœC‹ö6SôFJHœjNb ±)ßr< ê* â‘M‰Hî’îÈEˆï"a"}â6#\EO² 8¢õHÓJd" ‚ÕHX¢#Rc´Qñ$¾¾¾ƒK ‰á<&¦b' ;¼ ™Žf¤‘ILîÏ"hM~¾¾¬¯óá=NFM—÷À–3øJŸ;ßáŽC¡)fO#¾*ðÙIñàI /ØXø"rD<Ò"hO L°Yà°’‹ßg3&YæxHˆïFM Æ'tÏ1%†býˆ¿â‡„‰Uä*ˆªx ‘9)ð"ƒà^*MO è‰ÃâdâgH²l#"XûI¡Úvñšè/yf*~ ~0†;4GUrºº“UÏ‚ ã‘âi,ÅŠýèÕî·"ÃÈÐí&²§èR!qCõ…YH‰UH”ˆî™¨’Rù'‚(ª*,òó1QT‡õŠ(‹2Pýˆ£›¦~ðI/(³ü{ˆ$3à„§<#"…:vsoý‘*«þP‹Û_Îþ%þÈ„ï÷¥‘ÿU3(”Y.\ˆ‚r2|ÚŠ¿ Ä~9®~GA=‡’Ñy"oD—‘áÙ˜¿[9àÛ-„Å^‡ÄM1ÈTL$1ž,2“Q†\3EÅ— “ W”ÿBò<;k£a9&LUÁH’BàªOžÌËÎ Eï„§¦=Þè‹÷'Ä‘Ýá!HýÄYÏï°‰öB(dÄGؘ³ÂqGˆîH‰pýr”^Âò:lÈÌð¡™(P”D¡$§… a1E’ff2™¡$E3<(xL²BrYá/¶ ÝLÌÉœE”9’‘2rÌB_bdÅI2˜ÇzDä³,$¤ †C,Î÷äbdû¹‘,\DËÜŸ)D/*çñ²øysTBò:ú#¼¢+íÉl·Ò23OJâÙBh>ÜÉì ø>ž2$>ÆJL|s\î•1{¯1{Þ•®Ê‚ Šáâ2Ù‘>Eî(«,TIˆ¢`¢Ì‰Sì¸xOÒôFH~ŠdBÌn&~™>3ž.8ø¹1Ç2Áö3QñQÉpÃ%3Qð« ŒŠ¹‘õbq9!=„NH‡ÑDäˆ}ù]ÇqÐqÇqÇAÐtAÐtAÐttqÇqÇqÇqðqþ2rL>ûY™d\.©uKª]Râ—¸¥Å.)qKŠ\Râ—¸¥Å.)Z•©Z•©Z•©Z•©Z•©R•)R•)R•)RŽ¥J:Ž£¨ê:Ž£¨êq8œN'‰Äâq8œN'‰Äâq8íf§˜ó Ñ Ñ Ñ Ñ Ñ Ñ Ñ Ñ Ñ Ñ ÑÄSJ”©J”©J”©J”©J”©BêRº”©JêR¥*R¥%%%%%%%%%%%%%%%%#r‚ ±2jI Ä¥JUTCÄ-…Á}¾¿è_|{EOnJ‹nJéôgr¤B¤*MJ“R¸u+MJÓR¸u+‡R¸u+‡R¸u.C©rKê\‡Rä:—!Ô¹§…f¤}¬\[c'BQøT¹ rä%ÈKˆ\B⸅Ô.!q ˆ\BêPº…Ä.!q ˆ\B⸅Ä.!p¸\. úô+èVWЯ¡_B¾…} úô+èUЫ¡RèTº.ƒ®ƒ®ƒ®‡›CÍ¡æÐóhy´<Úm1(!Qb‰×eöä¥ÛšE%/ÿbÿö7ìoØÞ?±¼cxþÆñÔÞ:›ÏSyêo=M穼õ7ž¦óÔÞz›ÇSx7ަðoñþÍãý—Ëåâñx½Ð½Ð½Ð»Ð»Ð»Ð»Ð»ÐººBâè\] ‹¡\ZÅ¡\ZÅ¡\ZÆUTeQññžsÎyÏ9ç<æP*û“]Ÿ#F4e1êS¥êS¥êQÚjQ¥êQ¥êQ¥êQ¥¸õ-Ç©n=KqêZRÔz–£Ôµ¥¨µ-E©f-K1jY‹RÄZ–"Ô°º–RÂêX]K ©au,.¦îº–Sw]Mß©»õ7~¦ïÔÝú›¿SvênÝMÝ57tÔÝÓSwMMÝ Ý Ý Ý Ý X„± áìÑ öa÷’—m±a†a†a†a†a†b’‘ŠJJJJJJP¤¤¤¥ Fa†a†a†nRƒÜOV!9) úh¾¬$(wS“3bièW…2ûŸ’|›(˜šrc)ÄeFQ”eF]] WB•Ð¥t)] WB˜´)] "Т- "Т- "Т- "Т- "Т"ˆŠ"-©n"Ú–â-©n"ÜEµ-©mKj[RÚ–Ô¶[-©l¶[-õBßRßRŽ¥J:”u(êQÔ£©œŸkÎïd„â^Oð©ã„ÍdV\+++*èUЩttttt8èqÐã¡ÇCއ›CŽƒE Ñh4Z ƒE¡LE1ÄQDED[ˆ·mKj[RÚ–”¶¥¥-)h´ZêZêZêZêZêZêZêZêZê[ê[ê[MKi©m h[BÚж…P…P…P…P…”BQ D%”BQ D%”BQ L%0”ÂS L%0”ÂS L#B4#B4#B4#Byt<º].‡—CË¡åÐòèyt<ºššš&…I¡RhTš&…I¡_B¾…} úô+èWи\è\. …Å.¸¦kÿ‚@ž¬§ø‹_ñž¬§òÍ=YOVSùN¿ }N‹ßᯥÎ8ã èT…HT…HT…hV…hV…hV…hV…hV…eeeEeEEEEEED_ pqÇÁÇqÇqÇqÇqÇqÇqÇqÇAÇ ¨¨¨© Š© ŠŠŠŠŠŠŠŠŠŠŠŠŠŠŠŠŠŠŠŠŠŠŠ‡qÇqÇqþE°a†a†a†b’‘†)a†a†a¾;àãŽ8ãŽ:Ž£¨ê:Ž£¨ê:Ž£©ÄâqæçøMôõfå’ú²¿Ë8½ýY‹üE§ò«Â0Ãz‚ÿ ñqöœLý2qÇØqð|„ÿ$™ðqÇãOâE/·=I ̾«™3<3هјr;|Ê«5!O2FE/¿Ô§ŒŒÄÚ–Ü_;,3Ll%,sÂIÍ3Bd"—Õ&e†D°—Ã_¢O°njÈšüóŽ:ƒŽ8㎃Ž8åH8ãŽ8ãŽT8ãŽ?¬¹žJXm¦ø 6 6 ƒbà 6-ƒzÉÿÄ.!a 1AQqð0@P‘ñ`p¡Ñ±áÁ€ÿÚ?!ú¥oÅÉã8~ÖO-67±ZÍÂwÆ—,aL&˜Ba „!B„!B„!B„! „!B„„ؘBcBa1„&Ì&3Ó£æQÔV ç‚‹|LÓž´5DÍ—à=Þ c „Á»³BB„!B„!B`„!B„!B„!L!B„!B„!BÒPß‚¤n¿£=ÂÝ‹F[ä5½úÜÆÜn žýLÒ4DM™· Œ'¥5|/êõSá³JõQ—0ÆÄJ,¬B°˜××–¶À¸Qqx•ò/!E·é y{:¶NôÍDÓ"¦^.òýÁ¸R”¥.Öwì•úkÓ‹Ô¯ÿ”ÔŸI_ ’úc¿Á¸Ò—À¥)vnÕ)p¥ÂãK….ÅÙ¸\i|+·q¥Â”¥.¸R”¥)JR”¥)JR”¥)JRà¥/‚}ÁVÐ)JR”¥)JRã‘L¶©p»4¥)|,¾Eò/‘|"ËäY|‹(¾Eò/‘EQEQEQEQEQEQEQe–Yea¢Ê(²Ë(¢Š(¢Š(¢ˆÈÈÈÈÈfGäæ„!Ba æóÃ33=ŒðÏðÌÌÏ ðÏc<33òWR”¥)K‚²à¥)v)J. . R²”^<Ú˜Ì"!–Ò²22Â"ËfBˆˆ„DDD!ˆÁ`‚" ‚"" ˆ‚ ‚ ‚ ’ $‚I$’I$’I$‚ñ@~Ë,²Ë,²Ê,²ËÃe—†Ë/‘\‹äYe–YE–_/¹YŒÙžïÝßüpõUé ÄJ±åôFÿØt¯‚œ7ý¾/»âúEì6ð¾ì-æçÙyôwîÎïÔËÕ²ÿèÍùô{Áý£/+¢I)="zg«ü>ªãö޼IéOìyÛëo úbzòÓè—ôžzž^uyIµ<ãúù=Fy~>F`Íï¦8O.—”ŸJ¼ wÕžzlOAžyì1 éâOyI„Åøsba6&3°¼´ôõmL'0˜O70ž4Ø›S „ؘO7>‹%æáÃÆžòn µ0˜OU|0F'¥L&ÄØ›3fmž ð&0„Ù[3„úá—¦Ì& „˜L!B„!B„!B„!B„!LaBžnc=8Öäp!?ƒ4Y¢ÍQ¢Íh³Eš,Ñ~Æ‹ö4_±¢ýìh¿c¯ìuý7ìi¿cMûc¯ìuýŽ·±Ôö§± ýìwC­ìwCºì‡d;aß=Un‚¨?ÿ\è"²ŠÃeãõ„&s22*æTTF%©…­¶ ¦¯ž÷^U>b5Äþ>‚ý$•&do±nÉaK…)J_>Î~#Øã‡ïôìý±¨¸‡Ä¥ò3à¿I9–F²óÜö^?Ì;ý‹àM¦qüìrÙþüyŒ&a“žŒœ("ŠwÂ^<ÅúB:·=5aÃÅþÿèŽ ÿxñÃø>>VxÓÉ!¿+†ò¸²~m‹Ò"/)<´Ã—ƒ6?§ 8ã5ÌÕû‹Ü! pÁŸÁ*‹™’[Ϲ•c-½Fæ¯!{ÃpÓ¸þLÂjyÁ‰szC/€óRõ¤Žc=m«­‡/ lO‹Ñ×ɧ< áòÙxsÅãÃ8p3â§/CõF¶àhëFq3‰š.7¸hšæH}&¸V©D)ò¡Y™ñžN ®„F5µ ƒ‰(6gŸfg͓Û3ž‡ÄoÀÜÌòŒBômðýlÂx<ðc&ołОã"LÈ:ËÝ‚HÞäs"ØpGì¯ð[‚Ãx€Õ˜²gÞ÷ äЇjÅì(—ºA·O«Ø{‰Yû .øÿùfí~L¤ÞÄëI¹¥óCs¤þ!Â2Çúfgù )/:­ÃÜŽ,öãÅï!î §-ÂŽIY,?a w¢ç!ÄÙþ‚ ¼„ýš‹'åûù8Šxâlß#’B‹ÿʕқÜ$q?`Q´í²h¹ˆÊ²{`ªœM šãÉ·¿—Ћz2£Mƒt^,Mµè߸Öä¤þÙ3å=á•çÀœãs2k˜Ì‰~p‚jTQÜÙ¿è1› Ï«ÇCAžžñ¬ìœ…ÍsÞ%²VÛ÷5€Ïø?ÁsîŠÄ‡’to³vˆ“JüQǘk¼ó.¢TçEL'–ƉҊ Üó7n8Ôëu®rŸe{îïtàÄ7Êÿ}_ú~¦›TˆÎIÚ˨Œ¼DÇ2ÒŒ:0d¹Uèçî3ŽÔ×ú,æ²_ø/7z ûÊ•9šT†é]ââu2þC§ÅfÇM·7ÿ…¹¢ŒÑU«*ä£i“<âÈEÙÏ¿EÏÖ7zq&9æX½öMÎÔ!0„Æa1›€®“5#má0ŽCFN$Á˜Ýh5–Q8`ÐÌ‚qã±›âýÍÛ²?/Ü™N&­Í¯É ž¤âgÍûƒåÑžy±e».„ãÄÎÚì›É¹p勼߸”!\­¾®“q7hp¹ÊäO#<‹ô%¨Ðn–ø«Í¡z/î©æ§‰< äç™~A“fyWâ-ê`£Ê3ÁbômÙú~õ>iúâÐß‹nÛS ±6 Ñw–ÐoÝáf·¯5øõWêi— Þ‚ôU¼kÓÙÏaÝcMoMuorkzhI½Éà™M;„Rȼ3Â>LuoO¶gŒÖöö´ÏŠŒ¹7ø)¨öÂúYnä\¬UB®òÁJ®ãt¯¹ Ò·õ3N·&å—¶ó{móŸù\G½”Ïq9ѽ—TWw´:¨š\á•wÈrs!1Tå¢#²¯(eÕ.s À:¬k&r«!¦ži®¢gF´Fo$«3&óvCMdÓO“Ç—‡ÃËONLÛ ðyâô½Ä8…W¸-¤³ÌlÀÊÉXn:Žç†AœÖotD~4Pwõç£Q¯yQÊ4‘dIFu¨Íï(iL‘ï?Ö±þ{ƒ hx-æ\ÈdY«bKs~…ÄÿÑûžw>%~Sò'²Þ Ý–áä”œŠ¬Ðª6ÖæD>ÜéÔáè*¦¦ZI¸ƒÆÇ̸%OÑd#mRúJGà(ÚFâz›½Ÿ/‡œž;Øž#ðS8=ëá\)çW¥¦ç”LÕ gJÌÂΘ“0d äÍ!$"”CÅ8äf¤²è!”²ˆ¤¼Ãó é™ ˆÖQ7öAÖàùn†3ØvÂ<ÂN0[鑺›ƒÜ5·–°Ó7 ˜—A‚èA£Ë‘gi¡_ ‰<¯ôBvfNYD†¬îáõ&,*| ŽäÑ¿dA“×èÍÞeîšJh>o‚Iî·’"$w‘ºáñ³#$Êo±Ii¢S=L¬o)®ì‰·- µ¯ü 5”Ïg–ßyì¿^ßô?lß¹yÒ£6ÅíC6¯€œu“lwhó­øLhu¶Þ÷á]{í¶÷¿ mÌž™ÄáëÅXÝû8TÝò)â¯F{èžô*Q þá¼)º/:½òôW1£žÅžª±Ã;õØ‹Î1z3 Ø×ûy$›Ý±³/"“{•m‘³x Ëÿv¸Çô'ê{|òôfŽà»•™$feî cX9[L¹Àrô3íæqä4gá)ÜОõ¦”Wj¡ðÌëü!›ÝŠJ½èq'E á’O öpQo*¡%öŒ³pÌ-C°û¸MÆYÝpõt÷fñøOæ½ ù·ã­E›Â‹çËÑœwýoúwúØâpü{æ.ôáèóhù£æš>hù£æš>hÒàZqò&”ÒšS@iM!¦4ƘÓcM†×F› ®kšãçšæ¹¯¶-ˇ‡Ê—ÔO8vTæÏ‘ÊÁ¯{0|áó§Î³ç™óìùö|Û/å+þì¯û²¿îÎÍ;4î¶w[;­“ó“ó‘ó›;¶vììYÚ³½gzÎýàýû;wàˆ‰‰ßüIñ§ÄŸ|I©ì5=¦¯´Ôöš¾Ó¶ŽÊ;(í£¶j4SCõ4SGõ4ÿSGõ2*HÎOs!+‰#tÌä/b4ÿC¸GpŽÑ‚;vìÜ#¸GhŽÁ²;dwHí‘Û#²GdŽÙ’>I$GÎ#åQò¨ùt|ú>u&“>Lù3äñ>k|ñóÇÎßs[îk}Í_¹¨÷5æ¯ÜÕ{¶v™Ûga†vØgiâšáäÊØNìSñ ‡èbОoÓa/cžAùoØë5¢;”v‹ ù$|±òGÈ(|¡«î5=Ƨ»ŽãÙŽ®Ñ-ˆJÈš$>h|ÐùóQî5ÞãWî5>ãWî;MæÎóg}³¾Í¹£÷4~æÜÓáZ}¼ÁQ_>øããŽ>8ø"]Ë(kq¯ÈÖxæë585 CPÖ5 CPÔ5 CPÔ5Ís\Ö5ÍsTÕ5MSTÔ5 CP¼Åæ/1y‹ÌQq™™™™™™™™™™™™™áŸ„œ3¤É|0øañÃãÄ>|8øðñ =Âï/Þlã‹YÕUy<|±óÇÍxÆïÿ·ÿâ#ݳ¿gtð¾ÙÓ;f|3>9Ÿ Í7³4ÞÌÑ{3AìÎâguŽÛ¶;)–;lwØ×ûŽóq¬÷y©÷ßqóSæ09å®:û&"&i{Ey£æ6a˜æêHß8~ˆO’o¶&¹™4´Îû åŽÃ†4ãMî4Þã²æÞvï?ôï9ÜéÞoéÜoéÜoéÚoéÙoéÛoúv[úv[þöÿ¥|°HìͳþôÎêg}3¾ÇqŽólW~´ë¶àáUú|pøað£áGÄ#ã‘ñˆø|;4v(íQ«úèwæDè”Ýo¯#ÛBàÁ4euF|ûÍ,Ï“>þëúvïéÚ¿¸µNÑý;Wôí_Ó¿Nýý;÷ôîßÓºNáý;ôì¿Ó²ÿNÃý'ý_é;Oý'mÿ¤ÿ½ý'ýïéÙoéÞoéÞs°çaËÿ\¿õðkÿ|£ßúçËO”Ÿ->Rk=æ»Ük07mŽËq¦÷<>h|ðùñòlùV|«>UŸ*Ï—gÉ<·BÊÂE‹§HÑzç¡ÏŸ¢·°ovZOzÁšOÜÐ~æ³ÜÕ{šsQîj=Íg¹ÜgqÖvYÙge—±î½–ßÓcFi¤4FˆÐ`Hi0Í£4fœÒšBhM1¦4ƘÓC@hh#AÐF‚4 ˆ¹r""äEÈ‹‘".D\ˆ¹".H‹’"䈹r2ädDEÈ‹‘".DDDD\ˆˆŒ¹r2'¤äAòð®ì+ÐwýÎÿë'á¿ ä×LÜè;ëÑmÄÝö4Åøþ˜x} ÞôW%òžpj?®“K_$$òàǼ%À½x[—¢äÀ…úΗÊÀê[ 7€¢ôn<²òHÔ¨à™ùÃòTU̺™s**æTTTTR¢¢¢—³î~±Ÿ'ìGÿgËØgÿOøü?àdÿ‰“þ&|˜ùqò£åEÿ¨ù‘ó#ç‡Í” >`|ÀùÆËÂ|QñÇÇ|;ŠÄ‹¿GvŽÍÚ;tvèùåý5>ëúký×ôùåý5~ëúj}×ôÖû¯é©ìÔÔûâ”;(iý†˜i†ˆhƒãû4îX\ç°%°úzôy¡ýj'—äBOüLï¦w“5~Ì×{3]ìÍ_¸Õû˜4>i‹…«³UWh††ÔŒøÃâŽGÆ#ãñød|>!ª;4vhíÑØ£»GxŽáïtj=Ѫö¼MS%ò Ó>,æ”>PùCå”>Lù3äÏ—>|ùü?»gdÎÙ³;Fv ìÜ3¸gpÎåë;vvìíÙÛ³·gjÏgÀ³àYðgß|)ñ'ßixvfDk†¿€kƘá?†Ÿ 4Þü‡mÚ´;hvÐí$wï$w’;ɯd|q«ý`Kó=ô²„ÿGEPx•—B„+å'ül (¾E–YEr(¢Š(²üaôãôA|‹ä^=ᢹV%•‰X+– /NÓ‡¤é:NÒtøÿô FÉzôFç£Á†~#ûô“‡£côø tF“£Ã :ŒNƒ£Áéð@:ƒ£hGÓÑá’=AÒtAÐtø ÝF¿ÐCDE/£~/ü#Á~[@ôù.?üýÿÿÿûÿÿ÷þôùÜ(Ùu…={ÐW0„! à„!$! ‚`„! ²„! ‰B„! ±B˜B„! „!Bb„Æ„!L!BB˜L!B„!B"„! èûÞ•6¦ÌÙ„ÿça›¢z2ûeº&^€¾Üo‚x«Àž ûs¼g_ }Îâná/°/èåøÑ—–ßöSz=™äÜ>!ýØÞ¯¯³ ¯»ø+ц¾Ï³r=•é+é—ôŽìà‡ç—¤Ï²£2ô§/µ;ÈÏÒÂû3ú)o3ôÕö­èv7ƒó+íûd݆ÍÐcû®Þˆ/¶¯6âû¤Óþë¼S€u÷ǽý×[Ìýøîƒß´rú³öNùçZ¿tüåoÓWbø¥Â—R”¾Jø—Ϥë¼<è Çå—©]‹ã_ø4¸ßáp¥òw ŽXR”¨¨¨Žds5 dk#Lh)òÇË |óÇÉŸ$|éò§Ì!¶¹€¤4†ÒšCJvQ¡ö;)„ÎÒ;ˆIø¼<«š6h|ùosLititi1¤4FˆÑšHi !§4¦”Òš_sJiM/¹¥÷4¦”ÒšsJiM £÷4~æŸßÒš_sæ°  4˜F› ùCDh!¤4ˆÑCLh1§4€Ò#Di°Ú,›ÇÂÚm&À5 z‡ô#@ÓÁhì2ü¶ÿì™Ù_^Ç¿¥4¸]. Li tÍ$t¶eú>ØÆ·èÔ5ð»MI­5&§Àª&毛¾HùCPk A®ÍVu nÇNR''™Ym÷UQIøxVÄмÅÀ¢ŠÊ(¢Š(¦Šh¢Š(¢ŠuMSTÕ5MSTÕ5MsTÕ5ÍS\Ô5Ís\×5 s\Õ5G̦©®jš¦©ªj—œë/9yËÎuGQÔu`ê:Œù™ó3ægÌ™ù?$Ô™5&¤Ô„! ®¬Ïˆˆˆˆˆˆˆˆˆˆ‚""""" ˜e‚l‚ø€•dQE7”£@Ð4ÍDÑ4#HÐ'‘ˆäO"9Èé'‘ˆåŒÈ‘Ðt'IÒt(é:QÒ:QÒt(éEòEòErErEòErEòEè_$V…ò^ÅrErErGB+’Ã|‘|‘|‹ò(²Š?Gá&EØ¥ÁvB—Áq©JR”¥)JR”¥)JR”¥)JR”¥)JR—)JR”¥)JR”¥)JR”¥)JR”¥.¥)JR”¥)JR”¥)J\.¥ÂáJR”¥.¸R”¥)JR”¥)p¸R— \/¥.M8ýÙ¥k÷fÙ{—Ý•©ÑýÙߨo—ÙÇëp³¨û3<åñÔô„kðlçŽÿ°7È_åêÈ\úø µ»íñSŒnúÅé÷eÙçÝXÈFZ–gµNÂŒŒŒŒŒŒŒŒ®E•Ȭ äW"¹ËH¾F–5¤iX‰¢hXÖ™¥äýÕÜ«[¾es/™\Ë+™ÔuGYÔu™xÖñ!Äó#ÂrÓø“iãÐÇtñ3G €¹C^ =Úf¯4M,Lï „I$ŒNÂÒ4°4± , @Mc$’ $Œ2H”jI$AAIAAAI‘…AAAAA ˆˆˆ‚""""""""ˆÈˆ„D!°Ë „! „ „FFDÆ`ˆA‹!ÄCXB`‘K†C-‡šÞEÌË™2.d\Í e¼á¼Uº·Ÿ’äfâ„ófT¸š†®Pà¶p)Ô‰qCŽó…Qªj£Dkˆ|…¨QsÊñF¹¦3ó…ãÞ*ä·†ÑPÔŠå\ŠäQe”QcE›üÙû¥)JR•¥)QQQJŠR¢—X***#Al ‚Ás ‚ ’ ’I$’ $‚ ‚ ‚I$’IÁ$AA# øš‚Š(½‚«ÊÕ?»èk†±¨jÆ ˜—#X×5ñÛ_ ©€¡yŠù™ó3æg̯™_2¾e|ÊÅ|Êù™ó3æ~Lù™ó3æWÌÏ™Ÿ3ògÌÏ™Ÿ3>c#1¶çÙaÖ¼»ŽW2”¥e)K…)—HÖÆ+°¯`ÛJëß üÅú ·CMšHÖô1I+{–Æ~ . RáJWæ•4ll¸šIBYZ[Q‘Zå«bêdTRœ7›ßq„!B‚FüXÑ âø‘ýS¾7ÏÈ-qsˆÈùEåõ Ÿ§&if‡7‚Þ¨Nó ·ÐY·4¯šk—xž×‰,ð|(²Ì¯b‡*8Žä¬Q‡Þn2ñ Ò á‰R´+¸/6‡É¶‰M<éóÑç‚Ä31õþrÝn#š¼j¯N=ì¸ä8‰¹™Ý“lKu”š2 1ñ vÿÁ/Mø‰øc~‹†¸ ·Ž;Ûº&ü”D”gÌn^T4+p4´¬=¨²ú¥åx52¼!™™Ê(Ð4 ¢«¥Z¡\Ñ|Ñ|Ñ|ÑÒ:XÉÐ:@è2QtHå#”ŽRByIå#”sÀgà:gDé-‰:@‹™5Ãp¥)ṗ5kâ'â<'¬e†FFXdddeŽFFFFFFFXddde†FFFFEFFEEEFEEEEEE*)|ˆl +QE”QEY­klE••™••™™™™ì ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌù”/ÀÓ„ÈÙrªeða33+33333Ã333?;™™ž™á™™™™™™™™™™™™EQE`Q[EU—…{hy* ª¶¬ (¾e–Y|Ë(¢¹•Ìê:γ¨ê:΢9O3XÖðDWt3@Ó4Í@Ñ4#G¢h“”œ¤å!t”é')9’r›Áé IÒ:GH´‰Ê)à:QÇ‘Ð:H€—*'*:GJÀè77 r*äNi,°`\,$Œh ˆˆˆÈÈËÀ„ ‚"ˆ†DÁ‘ˆE„"2"Â"ˆˆ˜ÂlL!L&fzxWñ¸q.Å)J\Æ]…ðVÅØá‹.Å(°l¹cËk-¥.<(yÿÚ k¬—9ìªK-ºˆ¢†,‚ËîžË,£K Ç "‚ ¢²Ì0²‹(·.>³Ãí`.ãÚ‹ƒó4ã\sÏ=°sÖClzÁ(¦¹h‚8uÿÞ×> žƒó!žÛï–+çÎJuŠ ‚©o}÷Í–¸-¢bX0’I ÎQ3Þ}÷F;óޏÀñoºË-®8nºÉížJ ’À0Ã|Ô ­Ã|ê"–€LÓŽií¾ël¶ð꾬1º ³ìB‚…= @aDEä—|ó!„*§¾û঺mÆh0ÐÐRA<ö:'ËÿçýÿãÉïÿ³Àénß¼”¼ä/²Kéžùí¾ø<ð ’å¶Ð‚óy†Q yçßqä]ç“}›Îy¯¾úM¤$`Uûï°Ó|×ÏzýµþÂx¯Ÿ¶þË q2Œˆ$ŽúçžÛþ}ö ‚J ¦‚6B M0 Ät\¤1Æ* o¾;*®èÕMÇœÈÂyÏ-|Ë|ÿY‡Ó[Ëh‚dt­ûÏ$*Ë(®lÃÛþ¢eŠH+  ‚(à€J!É"AD=0Ê uüóçfZç¼À4óm}ËhÃ9£Àïþßÿßbm-¿ãÎl³4ÑêÂFwù﾿óŽy〜Ar’3ÈG~„2ç¾ î®ˆëòdm  Õ_Q—Ôþ8ï¬Ë"¾€'8Àfåçl‡L0æ÷¿¹ÞÀ„æ[ ‰4ÓÎ8 ×ï¼óÛß2’ˆAµ{Ï;çÿܳH o³éïŽæ²H¯ªóÞÿPÂK8ÓC',„L0·üϰ’€ßÜÄ!%f[P5UMÄÓ9¬0ÂÂæ^}×¼¾*êY2}ÿÿÏÿ1ó„×(ÏÛî‚èçŽ{ö¹ûÒË-Ë,?‚Ì ±±Ù(–k%8Îþó»ÍýÛ½wÆJpM7rÛïšC@=„Npg·µÑ}—ßî1ãŸñÏîbóÜÓF¹ EŸ¾o<Ç,¡ (Æ0ÀÉRÉ 8öõ¿è‚pc=ýÏrC³½ÿßþ¶)Ûœ³ÿÿæ¾à‚÷™‡0óË8ú(•^}„œYÆ)5ûÏÿpãEÛ0z0Œ=Ö• È`ˆ³ÉÛ‚}æ^ã´¦‚ÔOù›ç°ïüì¶µÿ¿ÿqÇÿ†x%£OÒ@ÃWxBE¿/4óorãŸmÔÒE7ŠQð“úÙ+ª sκ#¾Ë Óàg]MYŒÞá)ž°Cû^Î<©¾óâšÚ~޳º‰h×>€¦üÃï³ívç,y⌠pÍ+¸âƒ JóŠQ°Â$@B4àÅ,Îa9ÞÿrMM7¸ÎB˜€_û)ÛßìÃSˆ §ÑCøþÛï¡D|ºàõÒÃQËÇÚiD‘o|øç öÎúÞAGDòŽ*Û+ƒ¼Ÿ4àKsœ ·â“D^þ‡ÊÊgýÁº5Æ-<ïþ°ˆÀõVu+ªzÆ ‡ò ÿöôüßÜg²OròjŠÛ°ÃÞq4Q‡>ÿÏvÑÅ[iõWq…ýÇL<û×}ô2+„5 1ï²€e³ÿ øÓ÷” AVÑ}CÏ–3ÚO?ÿÿ³{Î5TƒCκÝ;ÛþÐrÁ+’™-ïô]¤wßÿIFSy÷]ÍØ½ €‚3)ŒöñßÓüÃÕÈÃ0}!)¼P‚/<òù,6üÞlN†äÞQÁ‡$âI$óÏ04[£RqÈ Ÿ'¼˜Uî2ÏL²ßþïˆÄñop®}ÑF0ÂîT›îãsÏ÷Î5÷ÐÃ8ï OžÃRCO<ó,qÝö£ 2Aháߘ¾çA†0úúÀû÷‚HpŠ8, $q3·PKÖ !ã 4Ð9è‚=zÓ‘8X³ÛÌýÓ¼ù¾È¬¡Kæ¡KÊ×®¾ûÃ,°šKŒã.ˆ š“¿ÿ×¾ó»ÃÎ0™9Ëœè²k,±„pO÷{ÇaI ÇŸé…÷wˆ½Ï&W(Ã-º ®²›¬³œ\¿=äi hÂAªÃO LúÀ5Ûòÿ:àSÞ<ÒÆÃ9ï-&¸Þ9à‚9ì1÷½¼TÿQÊœs‹ŒÏ;í;ŠŒ}÷€/Ïßüãÿy„qÄC q0T8­b{g^Ëï¼àÌÀL4€BpÃÑü‡j 4Ï AS3ÑIàZËO(Œqú¼sÍ×þHó}¤0Æk(š›`´ð]M÷õ1(Ž\v«lg½5C}ó0eíØG ¾ª²T°À@ àýœa,òBI` 6Ð5—¬¦z­ºœ°QúÆl_iôÔqÖO•~‚Ë-M7Þt q\ofÁ<^Bˆ,ÄŸI{î‚.K¤?÷³„ Æ0Æ÷ 4ôõ¨ aQÂççª «¾xëÑ•? ÁÚMwµñ]óEà™bë”y¶%ñù½ç§ñ•¯7Î'\èܤe 0g’ÚkŽIïúë>ôñ”¿Ì `Á„AWÓ|±æ.9ïó@² ‚Ʋ™-ß¼¼óšN<óÿT…ovd•´­†ÖÓ?;’ž,,ȵ[Бù+÷ür nç¹Ä3ÿ¹áùî‚"„w:æ3ÿûßO~<áúB˜üWÚì3×¾QÀxM¥<¸³¹Ö$¥Að0I|½]gs9oi7=Pß]¥«x{Ñ ºY>4ÿ½Ï>ë¯ÏS(k-&<°ó=±ÎÏ;Iev¿UgÐ$ÝýÙ³?4î à!ÊÝwúð̘ʷ¸ÏG:í¼ŒV7 %Æ ^¨=ËÙú½OLúÕRȲOÐrŸ[Ë í|{[~¬Ç äû~4î ³coòÓîsËÇŒ¾ÓuVEOè€Ò ’Ú@Y@SÆQAuùÕzR@qŠTçc[¸SI“]e§sæîpÝŸçsþp½à‘ýϼë ñ» <QûµÅUœµ§;ð×)õc‡ÁRà}ã|¾Q“uçðÑÄXq?í,Â1¯kµó/ç' ú°ö @Xg5JóÏu¼‘?°äÓïßÏ.û×wß|É—ƒÃã磩л2>¯€+jkÍáx1›fÛ|,F,36¹…Bê,wvæ“·<[€ ­wíÙ]D0뾜{€B×7ݦ¿iç•uÇÀ+Áv qž$Àé K#ŒÆÜ}$ßy÷M· ;(À<¡‚ `¤£J÷wVjïzC‹©n½Ûmêv²í5Éôž7*Y?÷vZMÂ=œH Òzâ{OrÓ”tûm‚Ê]”y—m•’q%€.©îËAÉ<0.®I?ýOª%~ÿ8b¡]m…Ó^_Ú9ë♈Î-Ü\¡ã0×ÿ5Ön_°ïFšB×ǨJÔ5 ë8¢#”fWM’uuÜmfTeõÖ2Ä2¡¼óTæ8»q<¡í2DÃV0ÁT¹ó†ûS²r¯·]óÓH¶²dñÀoßh°•4›ëûÏ¿ß?=sH„ÒP´IVÓs]}×¾ýyy9À$‘-™×Qá OÎÇòÄŶ(îšüWtwXyÆÖ,˜ªJËÏžò×m¤\DÈ$cnwÍÆ_íùOóØ2Ã>iæ³4ßitS<5ó‡™Mü}qõj¬€ tÙï‚G¿j~ @¹>ª­šzñ|L}ôVúJ\ûï‰W‚"ó}ß68ƒþ¡GË.§C8;Î!M7ÛLðË80 <±KÉç–*ßmا ðKÁ(îý¡ Š[ÞÒçœóbÓoÈÓ Oì¶Ù`S ¢¨j•<y°˜û¹š¶ÞFÄ,tÃ?ÒaG<óÚD´¬01'e… snp÷ÏÊŠ„C ùÇ0á<¹Êÿ¯=Ÿmâ‹­½²Ó,™ítv7¸u2°4^"ÑI†ZU¶ÒþëSMóË 5ñ®üÒË,²éb?Ì=Çoµ)ôòPÏ\±èo˜L>Ï3óÓwöbœ°Ýìò‹:"¤Ë ޝ„¹v°°Õ³üк’Ø‰Ò ¡H¡ÿûH ‹r 0I&M5q‡­dPãJJ0@wm¼?Œ±¶_X1•Üj ÓUÃÿ¿?ïªà(‚ s…üðÅõ<ç%œÜ~×R(C(ï³ß±ËÇ_ZùËPØÓÃ)²‹ì¬4…ÿ=l³ÂQßÔþQôÔeÝhc’¦»Fû¼z¢1î‘_rËàm¥9•°¾7¦ ´Õf£—[Ï9Oô²ØÃ 4‘MçÝqÇœ÷ÿÿö¢6¦81ÐÖì ì±-pùRÊ(QO(1ØIóíxý³Žuúú­òX„= ±”°l½Ó.|<Œ#°Çéão-ž¨aÂ(s`º{ßC sCy5ßÜØHRÈ}÷Tq•I”µd<¸Y,½ò0+m"1EQæAr<ºþè©ÿ30𠎘2|~ ˜ºÌr2[ýã?0ÞòË3ÏãÏ=ï!ÿÓx ° <ã@º¾ºã=ÿó¯uÇZ*º2P/üû®’j¨ª¾,ü­ûùÓèãcãÁ$õ§ÿ"_:+ü*hª]_ç'XX¯ÇÿzÊñ„ƒ9à»Å¾,U$úðÎÆQuTBì°hŒcžËï³YÆœA'Ü9è~úÿ¾³qEQÿ €CitÇnÎóóž‘Lhêãþ¹þÎÿÈ =Ë㊿»PüU"Ú¼pºl9 ¸ný„ál¶èüÁ—h¿®°Üª¯–aX©U_eI4TÜsü#ÿ}G6ꮹäß¼'> ¶|tîi?·n! Ž9Óü¨>ûe†Fj©Ejê$žlÁ(ªø9ÿI–y´\ážÈ+4ó €êc_/~uÕUXBD|òlábûOúÇü|ïöÜF]õÊKþz¦`ÃþzKóF€k=€,£Ï¿\Š4w±ÿðD÷u•E–^}7Ð}cF0 ¾ûïŽ8‘û­þE䀞ÃÓÈ %ŸabrãݼÏmXuÛn£¸cœà­°ÇÀ:8íûœ{‡Öá‡G¿yÚ¥Wö ï cšiÄ}”–]ÇØA…G<0€Y/¾ûÿíav&ª@d‰PÎ1âûßþÃ>=}Ø#®]å²=ò†^Æ ÿÀ¸W…T,åWMqwÝ'€„•qÿ˜a¥QEgÞDr…,0K¾ûïý,ápƒ5uB-&Ä÷üÿï8 ‚ùᎠCžêós]F´ÿ0Múºöëíªx$÷/~ÏøâýÃÇÿú‡8*H×ì=—]oŸüýå×ûe*´× "Ç<·ã>¼²8 ‚[k®9 €ÿL0kóîûÛÌQÞJd𫬆kæ¾ø,†l<ÂÚ‚ùL2uëPpÁþ_­q|÷—}~•Ãô_c™lëäÊþï»­;ÿN2Ö¸ ‚ Žøà 0M4ûqóí±Oª(–ù邨.”ëæ¾Û{óÎ 15Œ2ÿú("×¾-µ¥qôCÞ]µTI—˜q/ePìøã®}÷Ý~Ïî0‚s¤0à 1ÃÓ÷¼7çE©ºh¶Hóï?þã rÏtã–ÿX]q uí§œÁ‡-%и‰˜ÇdK⮨2ëßTqÌwM¦ÒQ„qֺ眷ëo'ž{íŽ{,¾¤:Ïßçÿxmÿ/‚¹¡X5Òãlxï?·úˆÁÏ ò ~`0‚Ü!J4؄ϾsßeðžxϼÿÎ?ã 9Ë,pÊ sæ 0ñÕ=ý÷‡<‘„Ì3_<Ó ‚`‚0à 0à 0Óœ°ñ 8ÃÍCÜw}”ÒA4aYC ƒ$CLs 4ÏøóÏ_0ë 0Ú0Ó ?ÅæUy÷_\óŒÏ<”që-8 ªòƒ °ÃO8ƒ 1Ï 0à 0à sÎ?þÂË<÷|öó;Î(4ϼ £ (AÀPñN<‚ æ9¡ËL0fË)(Âï¼ãÙ}ûàe7‘&T2ËßÿÍVÛQWß=vˆ$1ÔƒO,3Ž%8¨ßü¡ÒÏ, ’¸¡““A¬± °Ì,0À2”ËA´l XÜt£`BÙ~0Å®u¾—ÇÚ ø Š8³ß3Ï<°ÂØ!ÓÏ0Ã4¦ï?»Î=÷üïž s† ² <²Èb»êðóÈÃ0Â8ÛÃÃÌóLyv6cŸL`r¯÷ÞÕ™2Èo€R¯ïÿþ¿ÿÿÿÿ~?eL0Ó 0à <ç9 Ã->¦ûê²ßó¿࢈0È”Ã}öã 4@Ë0Ã0ÖÐ sÏ Ã L”uÿÑç¿.À9©Dn¢8ã8ëÏ=óÏ<óÏ|³Íg\óOÿÿ<óÏc/=úËâÿÎsØ Í80‡â òÏ ó‚–ù!“|Ë2‰$ºÉ«¢;ؼò}{ؾ‚7\ò yÈ]õßAv|ô÷Ðþÿø£ü<Ç ?ü8ÃÀyÏ ø‚7ÿ_ô ÷ÿ^?¿ ?bûÿ¾ü/ÿÄ% 0@1PQq!`aAp±ÁÿÚ?î‹3BÝœ‰Ã›“²-ÈM„âLMÉÈ–—±Ò—ü²3üzk»×z”¥..)J·iJR”¥)w){Ik〔¹¥)vg.f0™‰2þÙF>LÞÒ÷dï÷ïåáO¹¦Â){])x¬}Ž›v/ck´BqïmcáN=ÑyW‹wèÇ¿7æÅÛ¼;Ú^âç^%ÑsJRæ——t±öªR”¥)JRîÜÒ—MÓvitR—uõ×7¦‹¹v®Š]7U)JR”¥.š^-Ú|™µx4¥ÑJR”¥)JR”¥.n‹Í}Ò”¹¥)JR┺iKØßt¥Ú»—E/1±Rí\]›À¸¹¥/9~pnÕØ¥Ø¥á®k ©U³ùØßUºº磑;2Û}uñŠÌbT <4óCM 64ÓŒT$é! ®v§Ò>™Ró©Óš…Tª•Súbj ¦Ú! ㊈¯QªÛ¤y,¢–Ð|ˆýy_’Pœ °|›ç-¿ü`÷ç¡ë»µÎê¿çØŸWfœ5•»Ô箟ãÂê4¾FˆI42‡Ñ†º’6@’t Iva ”3“[(œcmºV:VVWšÊÄáYJÊÊÊû2çöýì…ôž—ï„|7Ê„ÄÙŒ„dddeQXVéÊ„&‹».ý.Â\­‰ÈN­oƒà¨¨¨«EÚ»O£:|#áõetD';£ ”¥)JR”¥)J&R”¥Å)JR”¥._Ft8GÃêÇôXB„Úˆ4K}p| —¥)JR”¥ºc¡‹ |¢—„! ™ª2 7 VØÑ®˜œR”£|ç¨>ƒà½wsøÌlM„Ä&¿ƒàø>µ8˜„Äx˜ŒŒŒŒŒ"M2äcྛ”ºiб1YJÏ“äù>JÊÊÊVVR²²²²²²²²²²²²²²²¿'Îý)JR”¥)J]®…ìcì%ôGÕ0}‘1¯¢Ô'õÁ¼+¢„!B˜Œ¢22™=°ö! ‡±ìEä‹É’~‘ä$y#ÉHòDDE°VH×!MI!†ö®Ëâ&&UšŠRðR”¬¥)JR”¬¥)YYJÊVVJ(¢Šß†Ûæ/ —v„!4ÍùÁŸKMøNì¿G¿q^OOÛz>½wÓáâ—µ_ >²NÕ{ÐþÃ15u>Rì³·u9ý êûjtû4×ÑöÞŸ­Ý×¥+}Zb222?exex+Á^ ðW‚¼ø࡯,÷=cØö=cØŸ'¹î{žç¹î{žç¹íƒØö=cÛ!ì{ç¹î{ä'Éî{žç¹î{“ä$ù Ÿ$ù'…AU~'â~'à~'æ~gâ~'â~gä~ü‘øiˆˆˆˆø""ÇÆ>ƒãSëù»ÐœJRîÜÒ—b—föíïk±îÕÔì³7è½<Ë•Ý'+£4ÌÌÂnM3\ÄÌÄ!‚222221&FFGà¯x(¢²VbËÑU–Ye–Yeæ,¢Ê+Áù3ò:14JQEQZ"씫ø****òTTTTAQAAI$’I$Œc8zéÿ\þºZ…E(új[©ÐóÍÞ½ñÄRÇø$?;WˆÉ2I'Zqä¯4ݬ)Gƒà¨¢Š·nÝÔøT¥ÃI¦˜¦I|¢I{ð„ÄÙZXªbLd#ðGˆÈÈÈÈÈFFFFFFQEQEQXYE±êµí L“ƒ¥Æ\õºù¯_ÿÄ( 0!1@PQaq`Ap¡±Á‘ÿÚ?í×a±<ÜÒìÒ”»”¹¿‰{4¸¥ÓJ]›ŠRé¥Å)J6R”¸¥/OKׄì|ˆ á¥~nÿŠçëæ˜B„! ·B„!BbD!1 ®bb3T $‚:EðB„!1Bk„éîiJR”¥)JR”¥*****ÅEEEEEEEEEEEEEE9?BuЄ9wŸî×òUøØMO?O{t!:9Ñó~>vn<Âé/[5ÂoÂv°ºÊ_ÁÂDÓ0…Û!35Â3nv$„‰…»uÞ’jšæÔ! ˆBLÌ!6¦Ü&•ªöhBf˜„! ˜L„&¸Bk„!7 ‘L®Ó ¦tð„!LL„×6!‰ˆL®×Bb™˜„'G ˜LBj„&aLB¤>‰k™šáBi„!B„!B„!31Bb›33´A3Bh„&! ˆLB„!4BT!Bf„Ä&Êìsfj„Äy„Ü„!335ÍS333e ¬Z&ä&f!6¡ ± ¢TÌ&Ì&¤.±aoÍ\ÕBLÑ7ºG³/ê,šI$FAAb&šR”§Æ·ïÁ^Þ> (¿ø/ÁgÈsit”m.ÖöÞÔža6×H“|º‹–Ë•¶öš&!4s Wa‡#uh¥Úº45†Mç²Æ†¶ŽŒÜ_’žÚqÙÍkjmr÷w´Ç‰§üè(]+Å¥Q/¢"!4ÐÐ੉­ mâµcÁlá HM%CC™„­‹1´È—å‹ ùâf½ÒÅX˜p¡9ŠV1qK§xcØþ¾ žÆë]'ôQª™Åð}'DŠQM mù©¢yƒd…À뤯"üñ¾a"GMÆWž©¢ääªBqLâTQñB±QÛn—eà.4èÕP…ãü— Ÿ#6.K¥šY5òϱ!ýöEÒ½,x{AÉØ¹žZ(ËD6¥¬6‘P‘¼¹bBD¨OðÆ4sHUä«ÉJŠŽx¨«Î«ÒÂeŒcØEÌ.ÅφâluÖ(3é09ÈÐùä0ô MÀT¦Ü)»þ¶BÀÏØ3\¼‡ I—¶¸ž±xÏ@åùG.uàª=e¤¥Ù¥ë¿õŠì\Øq銬$´ÄõElè)zv<=SþÝ‚´,Õ©oUeì²,ÌÂZùœ“˜àgÄŽ1G‰8MâÍ6;pÈ\OåØâ’càŒg+ÿ½ì zɵ/à’OÕD%ð$‘¼x"¸ŠØ$‘’Rx"ðEáàŠöF÷YËóÔæÙˆg:zG'cyZB„!Btt¨¨¨¨¨¨¨‚ Œd‘®2=_Î0F/L½$Ñ ­òÛ)YYJR••”¥eeez+Íex¥eee.!Gq8œN9âqÑÇLÄyŒ„&!–sÅËfõk –UÅÍ)JR”¥)JR”¥)J<5ˆB„!BhàB„&Ь¾i{8º‰¦bm,.G>itRæ—l,ce)JR”¥)JRŸÅ¯ÃeÛô+,L„!B gþÓúBºT7”¥)JR”¥ÉVDoš_0ÙJR”¥)JRâœ<<¸¥Ù‚p<ËŸD…ÐB ›Ã åE)J^’?d!™±a­ÊW¦”¥)JR”¥*P˜\¾“é \Ûþ¼Ò ‹Â"ðˆ¼"/^¼3À‹DDD!Bc†)K¦”«LÌ&!B3atusÚš&Q§áËÿ’´Í©”ü+w‚= Ñ®K£c‚ä¾™ð|ôσàø>^ ðÊÅxexe ŠÅb±| à_ä…*T©R¥CÐ_ÒRôžƒÒzQè= õž…±@WÃ` ÊðºG&ÔÚhhŒ„#(¢aB$! ‰˜BˆˆŽDD!DDBˆˆ‚ˆ !9tœ›SrjõWEèiKÔòì/òÍ?7?P_ÞÜËNs_®\–Wâ!;#õ¼¯òÐü'ëyÞ™úŽoò¬KKðtt¥Ý„è&ôêªòTUåyE^J¼áQQQJІ®õ¥8|Ÿ'Éð|Á^ ð_ƒäù>O’|àŸø>IðO‚|àŸø'Á#äŸÉ#ä‘#äùÕQX±^ à¯xG¡„zèG¡„zèG©#äz–º¯÷žóÞ{Oií=§´öžÓÞ{qî=™ äÊ(¬¬¬¬¬ãŠÊÊÊÊôÖ5OÆB0„'{gâß‚~µø—¯Öð~¶ó¹Ô¸¥ÅÅEE)JŠŠŠŠ¼¢¯(«Ê*òˆò{aì=‡°öÃÜ{qî=Ý-¸õXôùšªI¶=_ý_`âq##Ñ######(¢ŠÁEtEUT_’¼—ä¯%ù+Éô}GÑô}GÑô}GÑö6‰•œ|áÿ8š!Í ‚3ïY©÷¯Y¨Ý?‡·V™ªh›”MáêKG­Ô„™_Yë="CQcP1wXÒj1'YüBI$’Ø¥)sJ]¤´Í/%íô¹¨¨¥)JTTTR¢¢¢¢¢¢¢#AI$ïþ|à’IÕú+ñî=õ·ÿÄ*! A1Qa0q‘ñ±áð@ÁÑ¡PÿÚ?fÎ'–9Ãó'±;ÜrN²sÛÞ=z-ÿ›<^²wgžîïÃè>GÄÞ;eŽç>¾;oÀW¯Ç×O‚^ïÄ#mó˜óÛo®~Î/e’íu'ñÖÈ,æEÑ9‘ácðößQ±ã·½gyèñã¼>ÍÙŽ{˜Ûû}qß¶<:ürw/ovÖzoãølvÄØOƒß®'[ËRs…ƒbÙy$’Y=K/~XÙåžég ²Îd™ÌG™Ð//8™grË ²á²Ä`’È,æY2ÂL²xË,²Ë,²É9Æ ,òK$öÅ–ARpôe {ĽçÙd| YçÂ0"'Ø8ÖYWFø#‡…/Ê…ùÀåùÓôB²ÄBž5ÓYø_…­ÞÉC Ibüb38O6Ï#½È,‘¼eœe–[#¹Óµ½8Y{ðÓâ±6LHØ6tμ÷;–qÕŠ`¾Â˜~OÜþ ¯Ýü‘à'ç8÷=˜xžØ$œ 5²úç›—–u:qæsë©%œË8^YAÌ ²Â/2ýl Ãx[=2FLN÷xuCK,æYådŒ²Ëac`Æ[–p‰Ñ0,^ìX<,sÕ¹“×62+9b¼J™ç®w<ñ"ÖDkÇ&bP_™³Dò¸vm“L;×$,×ßG9ì[-¾ÛeîqÛÉ4ë/>¡œâsË9–ß\[x¬œˆ)„Õèã["ÙèüOvm²=tç»,FØ‚ô³¹ós™ÄãøøyÂÏrx6Nu¼¨d8 Á¬H’7ßÃ>dIy+ËÕ`{’}ALL2ƒmæ^A)¾^Cd,î< 9¼È,¼$XAie’Yai>1ac– L²Å£‹ø¯¯/£/7ÉüÅú/ŒÏÖ9̆Y!°zHœ°Î9¬±âôØN“Ëò™,ïáæ7œõðÕ}æìË?‹8ÛÂNïÃm22Ӻÿ›_¾‚µû…n¡1ñ3á±Ä·ç÷ç ïÕ²w2úøyù¾ áùîa>œ'‹ÑÝàÉCž Ï€–F§ ’&Øy’uõ’O-òûœÎãÆ%7¦lðáz‘Šl”ľ[÷ée§‡ÉœæÄ 4܄ΠÄ,=Np½&N#ÛI’¶-…°Lú:pÏl™…Ìž1ûÉw"òo³žÛòÆ$%°w9–s>Žgàð8Þt5Ëï!› ùçRGyí¼xô´ø1-„žÝæÏKgÍ‹&/}| ï¿|x–È>Cƒ?ˆ,ƒPd²ÞÙÌø²óëœX”ÛxK¶ss¹^cf»Ñçâ Ë@KU´Ë̳ÍÛÅ& ,æƒo9y³‘Ãñ¹1i ¶Ëaœú'8Jó×£ÒvöÈøùÏ/&v3äÚ[ß;®±¼1:¼d-–V$YðÙçÔ°ô Ýy3»ðöX;¿Ñɾ¸oÀÞyðFTñ…àó>¤D¹Àã±Üò|!² l±ËËHb÷žL-f×u‰‰ã‘¶3»kóÍó K¯C¯ŒYdO>мT‚-‰{l8ö#s{­îÚlóe‰pàyg®4gÿ °u µ-ŽÛíë}aÏ,‰æÙ̃Î9¶q8sÛxßM¼,`³áõ2Ü'NŽ2|ŽzYóâO=óá³Ö?mõ{ö;÷g7¿˜1ǺÏÃq:~e‡Îû/‘¹¼ó‚di/IÀ½XÌæÇRuâñµØ|I}0½¾±Œ;œ'-·XÉ>Nl>Ô - VCo2|r=±I$±Kài¬¦Y-êIä<9å ÞØ3¶œN)ðMàYÇ׉Ó#9“¬/uê¶^L|uëoAgâ—ݶp~!=8Mï æG{í†÷Ë>!܆#âd» ùpâð> Ä; ³œÓ%ÓÈ—ª>F„ØvrËËx/3ƒÅè±8¤ÛÏÄE·¶ç—œX>™Ìµ‹IB&&3P˜7‹nB,§‘´•w8úCfߢ޶œmòÛ? ËÍî_\=xzßs¬fËmžw,•°ã{Ì8e÷k¿ aI~~”þ“Ë/ÇvúŸ†ü~cœÖYŸ·—Ÿ<÷™ d}Ïû‘‹lp-›x˜NçÃ̼ÎS‹È#/Ám7‚Xdó/V!a•„˜”–6GdãÇ#6sƒ¥’6¶¡/Ùxû}óp±ØHŠ‘-÷Ä`goüóL¶ØKem‡ƒi ¼Ð·Ïe·ªÛÁ%6Þ 6™m¼ÙL·Ùa´Ùp¼È´!--æÃÝ-´Ÿ-5„¶ØM¶ÛKy¥¦qm‡¯€y¤%¶–Ûm¼Øyõ™m²ÛðצsNm±Í¾þ„~ešÂCiÁ6|àÆs|¾£ƒ?žm¶òmÎiyóî8_ öv#wàÚÙÓVôesm¶†}µ¶3ƒå³,Žk!}ð|¾¯¹²ú-8¼ÂW„Ä$ŽŒºÊæp¶XvûiÁ‡„Û\ÆÙm„èÌe°ÚCÆÊJKoBiÓxÒÒÄͱ3aëm”¶fØ–Ó8f––Û-¥¤¶œm¶Ûm±5–8oMZ‰¼j\kxÔ)KjÕ¿Ü¢Õ¾û·ÀíÛí»d;V­tÖµ.®4y¡[½O†­H±ÊcÔ…³Í‹E›$@‘b0±bE†˜6dnÙ³–`À³! e‰HO¶Q"4´´´€“ãmà )l¤±a2Ý´ü–ÞK–ŸIÁ¾œÛñxÇí_ʱ3SûÑû—çñ¿oôßʼ ùüoä_ȽwWêøÛ÷üMü q¼^ úÆþ&þ&Ï鿟ÖÇëg•ú‡ŸŸáýøžÆßÇØþàñJ¿‰ŸÐòþ'†?‰²š;'…éÐ~OVpcce–?«ÛÛ>Ìà66¬mZµ–­ZµjǦ9bXÙccc{ce–6XÙ{#{c#{c{dIJdÉŒ “ŽØ¶bÆÉŠÉš‘²cccd6ÏÓƒyì­­ìl¬í­­¯ê×›kkjVÛy÷À¾›VáË·î°Ë\— Ý»váZýñøž¶òÝ„¼´Ä÷kØ{ËK· Ì€€ËÈ ,ó\°²AÝK °°°±aú€°°±a!côXe¥ü~a¶ KèÙ òó˜~¬,2ÃôXX~‹ÂÃ/,$,/"OØXXXeåå…„…‡è°°°°¼°³ô$™úœ¿€ð>ÅÀá€QÈ üXXX±abE‹,|ΰ^Iìc¡‹ƶÅðƒÃ±ézGø‹øŸñÅœƒñ—ðt°½8àð:ÖÖóÎ͘1:FÐRÍ™;füìÏÙãV­Î[œŽ_ÇnÆŸˆöò?E»öWè-þ¯àO8¼ýŸ¶{þ'WËø/âÕ{~,OÇT¼Àg©ñ±øãœ§À·«ÏaŸ{žs>_V›}[o5—ƒŒó~)¿ —>>qw›;yÖ#ànÛm¿ÒÞÍêÛo-øoô5·á°ï6Øyæ[lgÞú÷^NfL>cÃÞ}^9œõ,'9–e„ ’–$°‘a¼È8Ø%àÉa°Ælsàag%¾yÍž2ł͜ÖrznÛyðüóg}óÙ·á¶ü [ß‹¶ÊüÑey¶ô ží·²å¶Û/ÇzzÚ–ð·ànÏõ^ ßNŸ=è÷>'úCÏ~g>â ¼"ó'‡Œsz^$añIÝ“}<öÞ!º[ôÆeù¶}ãn œ-yôw/ó`÷É çÕ£Àá)l¼mê@Ïæ6bÛ œ³™pÉ&ÃÖó÷~Qؘ̈́üÍŸ¹¼¼Ûfry“ß,ˆ[ïžq> pÛäsèøìü·ÏrÞ6Û)¼x¶ß|x=Þ®œÛmæÌ[ {õÀá¼Óàï4þ‡œXèsØ_‡Ûcyžü{ðB)‡ûá–GÁ`Ö`ç³g3™ðÈÒð'xm± o‘ñöËèl±á2Çc%“‹$Ýå„Hp›â6yÑ”è[‡ /¹‰Üàt~ ófÛy¼zç5ž¼ß‚ÞoŸ’Ë,÷zÿAøí¼ÞqÏ=Þý[l8Þ6÷ÕÏ€|vÜŽ zI×ÀøïÁV’«_–|2Æ÷"òO‡ßÈæpN-õAc ’ó-×f6ωdYÏ«|b×9!“am¤òf…ê|]ü3ÛHz¼Ó?£¸smÛÞýGÓàç6Õ›mã6Ûý —›ç߈œ~;óßm¿â1Ý·áœÛNbÇs…íïÄÞb–{Ò>(üƒƒoçÖìóË;ôÇæop¿|Â÷yøaáœ6Ë"M#ge©{žȽÖg]óžÏ ÄP÷ ã/ÅüsXüõøoµ½æïm¼m¶o¿‘-¶ÊHõT´âóMŸƒýeãÖÙc²Cú;ñ#ä6ˤÞ' eù¾7ÑÑç×À#†ÙyðÔîô‚ÏGà“Ìp^gÇ%àmœöÈ?Ž ÀîFYy™ñ‰ÇfØîI ¾~xOçBO"e‘éšgqOƒðÙy¤ÇÃo9³ðÞoÉ—8lì¾§}ù/~;.Ͷñ·‹y?f? fËÝþ–¹Ô7È~||½#˜üËëâíîüËl³l³›ð Lø ²pÏ€NÂÁ¤Y<ž¬}ð#6r[ 9ŒÀó[ì‹êó ²ËÌàÙå®q¶Ï/1l -7à¼x¶ï6÷ÙËI´¶ÛÛï¾s z¡Å/^ï7¦Zl¯7+±3øŽ<Þ6Ä̶ùcœÓã°oôÛmáçÏ~'â88ååæp èÿ@ËÝç¼N¿%ñüp:Á9¿ ³¸ÇÃ`’9ç2òC>ðÛËѓϨ¶üú[c›}Úlžó8Þôµâ3¹â? `#[ò|›Ù¼·äñ›Î§À~ÎoVÛ~l½rÙîÏã››Á—†e¼z¼o_è1·åæpË9sâ³ÉG™㲋ßs~gùä ·ÄÞ?å4ù…÷ij…Iåõc¬op‹æE’Þ@3¶3àÌoÁmê[ÍÂ>Í—¹ýX›ÒÞý¼w†qø=ÙLÎ}ófr_Šñâó~{ñ_†ð¶Û]¼çŸ xlŸ"KÎlmùŸ¹ãß¶#¿Yyñ†!ñô³à7Ü?óà?-ámæpÙ™üŒúÛðGy¬~IÅ3Šñæó>ó|ø¶ÙÝæy²ÿA8ÿC~ò /wàÿC~cðß’' á¤Xg‰øøa‘œö߉Ÿ¤ôË>c{Üø e÷ÜÓß’aÓˆBo®}p‡Ÿ|`WÉ…æaÏ $ü;æYí–ÏVy™»–1“x+ð`dô$›_Ô³«7¶2xðé,Þä=Þ3»ï7Žß^_D®ÏÃËÞoÇÅÑÙêÏãv×8¼Þê÷ÝËÌO‹Æôîü[mö߆üOÇôýøùŸ$øgÀƒáŸ0à_Ps,ƒÀàq8 Çýß\$Ç¡gY$X9œÎÉeœ`6Cavbe8c³g‰dä2ÈË!ÂÂÈ$á>¥--6wYBu™•¶$—'Ãg»Í—»?˜ÁÅ‹òä>9d ±œgžs^}a·Áã×çäñ~/úmí‡ó§/À=³à~?¡,‹;ŒʡųÎ=‚Ë,"É ±Žæ7›}pñ‚ÎdFÙ‡26À $“˜ð,xdˆ°ß›$r3/¨‚úl²9–{g“ã#]LâœdljÂNð‡›Ás«ñÙîðž,9{Ýœ›Ýìóèáiððù}^÷Î{Çá¼Säó<þ Ý÷ážs:Ï–uÎ}<Î0Î6s8pÌâ­p> ¶s<±æ|CàgNa°9g2Ëܲ &CâŽl›$Eõ¦½xt/x™ÇÆ}3xIãû']ñ’a~wá±/¯ÃËÙΩ;;¶Ûz÷̳xðg¨|´³žüï™ñóâü}¼Ïèí¼xüvq/~'>)g1îYÌøÀ8Aг˜f÷΄Žó: ¶{Ààw,²È#™Ã,ƒ[ÌÈÞ œ1°Û/½2?–À·»ÇÖÆË8äœHßÃjØÙ;¼d†Ulñg&\ ²Çˆñç½coÃÆ7¯6û8³½ú³àÙÆÂÙ˜ÉNoÃcÇšçŒæ÷ÙŸ/Áxñë׌¶Ûó3dÇáŸ~@¯~øs8Yí çœËÛ87¼È@ø2Î {]³…’XÞäÈüYä9%åçÄ`ît<ãŽó&,²È>Ií©bÚdpæw½€‚FFÍ“È/o'¨çT›Òû%&™¶aÄx§Áxþ&'£6óm8»d3øãg9ðó-Oƒ2z’alãφüáŒd<‹³'ÀùÈ,ƒ‡î<Ž&0pAd‘ͧÃï€s/67™À²ö ;†ó9œÍy–b'7Ódd†E¶@@ðYÌæ^H6O$ŸM¿,…’ßv3xm7̇›¼ÛqÎlóͽ•ùgrw®D—˜çÁø07¿ÑOŽ[Ç™}qços¹ð}{çÃ>’|1ˆ½áe‰ðú`²C,³Íêsy’D,â¿,…ÈÒöÃo„¯ÞÚs;œÂË:Þ—çá dîA%–sêÆûd÷›ä†Áì@ÀÞpñƒ×f_Y$³N6Äžcqì¡­²ñîpͱ–ÂZ¿ÑÏm%¶Wã²óiiy3ÏŶØÑŽl¥²ñøy/'8ñø_ç¿Ö9ž–YðÎ0YdGÄü÷8GË,às8pîdp,6 ³™Àæs ¾ûõÀœ‚8›–…/Ç<à3'šor3âdɬçœÙîE¸ËoŒÛÏ«øeäZw½Þ<{öJd¶õø›×úºqÙ9öñ“©ÍÐÏè³ÀøÀàÊtçœÎ¤|LlàpÉÏ«9Ž@..YñË þ™À1VÈ"ðùH¿lÏŽ|2ÎdœB6vÂ08b÷,¼Û;­’Y+$²Ë zŒY…’¿ “‡áç–FË9#«/“àI/“kÆfsŸD—Ñ6üËoV^/½s|æ=Ïy¿,ø$,æ×_ŽsÎoÅ™“‹ýLøåœ ’ ,² ø™ðó8È,ü¢ú;œ>dY-²ÒdôÉ€–Î»Üø¬p$‚K8gŒI'Gñ–s뙞ÞåœÉ‚Ç,ç¼Î!e’{Æs™#·‚¼÷н™,é>G{kñðy¼f6ÛvÞìoËÞoÁ>ÇëáóÀï°qãÏS‰‡Ã,,²D³™Ì²,‚ ê ,Žç ² às;‘ð /2Ë;,ƒ~`²8g‰žiyy¼;‘yaa»ÀƒNœÉ9ôAÛ5`Û ²'™À%| ²Àd ²Ä²qæYÄ^3Ä€ÙI²ûcg¤Ëä›,=™}ó8¼Þ/ÏDá·„ÛݶÛ>Kï63ãïËó!gŸ>GI8ÙÏl³™Ì’ÎçB,vÈ,²ù¿âÛñsúAÜîq#$Í $fóá’éSœ 8Yœ$cÜÈ,àAÌ‹$±‚È8É,æ{e’AÌp,‚È8L`êAÀ“àö ²Ý‘Ãó,‘Â"È#±‚Ï,²ÎcÂ2K'™gæË5ø°m¶M³“œòL°1³cÊ÷„[<Ü•–Ûm¶\éŸàgÁá×:ÄÎC?£ ~÷,²É,¼ždÁe—¢ 9–YeŽÙdAÀ‚B˹ÌÛ,ƒ€ÙeœÉ$øa'1Ül³™d`Ë/2 ³Û|ƒK,t³™3™fÝø¾àÖŲ7xEŒ2H,²È$ƒ[,³6É,²"HYÏ82OÒXñouž#zÙ0Ï‘—‘ãËÆÞ,0²ü|ï¿!âÛÝç¹ËÁÙþy—ŸåœÎ³$Ïã¡Ðîp,ƒ™dĩ™eŽkÀ’ 7Û=‚ €àIdÁdXXHkdY–XY9 d,`æYA$³‰Ád@Ͼ'ñ}J¶ym’AÀ¼€ÎÀ±²ÍÄýuYfHÙf¶6NX?W„Ù;–êfgxŽä˜ñ½³$²=’}³áCñÇ›5ø—–üÛ~,OÁmëgË8÷ï§Áçœzñ³™ds>9e’GÃ<,‚ \LãèYe–AÌæYÄæYg¹ 6úîI„Yg“©2Â&@-Yg0°Ë,€°°à3ëdÙe›a ó û ƒíˆ9œ ,±æ3"IâXÈe–„†ÉôI…—»;²qm†óg MžIeý¬I%x<–3Qdl.ß}Þ?%ù<ó{¼û³›/#™Íá“òxYÌâq<Ë8o È$²NçÀ$²,àp5’ àYeHÁc1ƒ™e–@–DÏ ² ² ,ÆÆ =`²Á€àA$X‡3€²Å,à,²,2H,²Åˆ òK" ¼²Ë$Â$æYìœFE–i!Äþð0˜$dækalœdóo¤’Fx û&ìô#c«°ËÍâü>­æÙÃàìq¼ëß,ez ËφYù=MyœÎe–A'2Ë,²Î‚Ë ³xYežÀ˲à 6 ,Ø,b`à K,’Ëe–AdžYaðdD€ØIeYÉ6BÛ 2_„¤ƒ¬æ˜< ga;À²€ó Û9–@Ìí›'$¶XÙÖrrÉÈÎc»9%“Çlg¡ÌŸÄ1/Âßo4·¾ñ±†Ôã³ -­ëß¾çÄ®ó\á¿÷˜KÞe–pÀ‰lîIijdYÀ² ³ŽÄo2 "xÁg±@²ÂmdŒD‚È,‚N3È™,² Õ³ “ l`ÎeYeIe…–XY“1"eXXIRÍx& #ïp\:e’p„!X/¹,æÉ æ'5›®–9Ãg,òxÃõ˜Xã2ÌŽq‡Ù9ðØ·›Ømç—ßeá?§Ã/®Ïróeî|ó©{Ç8 Œ–Aä–Yd’@e’ke–AY2Ë8%”ðxÁ¼w‡Æ êÉ -gè²H,ö  ’&Aă!ŽÁe…,³`² ðˆ‘2 $ˆ–²BÈúG×þ$ƒ­Ÿùkþ`Ö|üÿĘšIêû×þ`üIçþ?à$’Lÿ <‰}m’O, ³Áã¢É=f+L"$˜ÙñÂk!d…–IdŠÌÂK$’É'Éã܈=ØÈ øûÇ/YVŒã7ÙwÖדDŒIæIå‹¥äŠÂEd±‘ÊI‡0³‘²Éò»ÆÞAÓãÃ#eøm¿xgr :Fo™=7žøDÑ#"¯sÎ#fAd %–YÆÂÃy–xq$±³Û,² $vdK$,ây'’s=,ö.0F@N#Ÿ_ù$ñþ$“óàbýŸòÉ៟$“Ø'ŸÜŸÀýù?œþð‚wþQŸÄ?Ùÿ‚rg¡f¿½“dËÅ·Œç $ÂG$l‘˜“ešëny²Y³ÌY-2{e“$“d̳lžÇޤ ¡ð æM–­!?¢‘‘úýÛ1û% دÜÙòO¢úJEù'Òbk$’Yd’He’I2Y6Y#’I%„†Éc$‡èl-¶òU1øl²³m¶fÒòÖMîIÌÁgY%¡$l’G1›$±œ>Œ.t^ûß20ã+Óy÷Üø!Ÿ€|·Rad–YœËLxÉ5’Ë3Y0€’ =³„³K,²Ílã<²Fl’Ë,°ÂEfAz pŽÉìO!2~?óÿú¿»ÿ$`ÿß«Çbлä?ùîÆ ïø€9ëûÆl¹+µã<ËxÉìÌHðGâK8:>ÆeOÔäGí,=ü ò;ì~o¨2åãÃíŒCø¿ú¤³‚ŒhÛ,‘’I0²l”Id“1$$“‰xI1É,Þ$ÙgÃ<øì[ovÏmòzŒÎM§åžçë$£Éï ŸÍ’t–$0I¼É Ù߆¯ /;ç3åœóôùœË9–Åœ8ÏðÃË 8 žA1ÄyÏÁŒ’xž·€Ù Á$–OäX ͰÛ/Ë$dÈYe’äÙ‘ XÁÖ=$6`çþ,ÿËl|>öMCìÿ‚TʾWóÿ1È£¯þ–Öýˆ7rÀËSqç2ñý¨k7ÿÈÌèuÃãÙžÇÆeûu–-©cpðL!Øw÷/à}ü¬$aŽØÙ%œG$’M˜–I!$ØÉ22p’ã2Yd–Y<N6t‹bÏ€K/Y–ú©O‰aÁ,,³Î!’Nl‚LœÀ“™cb Ù#÷gÀ>úÌÎ}=sú?RüèiÃàÎ,–N…C,,±³™a–_mžY̲K$‚É!²xX2Yõ%žÉÉ’AêÉg›'„–6xŸ¦O!'¬’AETØ¢!¸ˆ}œÿ±îÄcø>à–úç'nHÁdåýÖhpE´‚7 >½BÃÐ`K e­«rŸØs5-cIÿt$’Y…’6Y!aÌ“Y l°“ˆId–qË/LœK$²O’»ðË#ጼz·²ñe°iú™¶w‚$÷KM–269#˜Z‘$z ²dl²v÷‡‡8lsËërò9¼Ø½þ¨ÆpþŽVŽM’Xd æp: Þdl’}@e’;"߆Æb$ï2L$$ö|=“‘“ˆmž<c¶lM6O`“ÍŸ $̃ÐYâHeÿßÒ#÷ë/Oü08H c>§ÁûÉŽ›ûrFH$}’Ïd›$²MdâAa!a 26Y$’Hó$²Nb{$“ùž'Îk—¯„‡šñüqd–f)ë~[{a0×%–s"Ùc¼N¯¬}Ù9fó$20eÓ«‡Ë>ãxtÞbÇžðîYgV|ú ,² 8€óèâkd@ñ8…–Ig²+’aeÉ„˜Yã#ŒžÙ ‰ö6I&lnIÄs$SÉ8I=,Ç÷ódþ‡ñgþ¿†g?ÂHšl#d‹þ=‚úg2'²y%÷%“d›>–Iì’L(è{ ’I$„Ù'P’I !2Y=K$²gŸ|Xó:—¼o®3-Œt•ùEäúI!9¥¦Ì <’ËaÄ“I ’Åœ â-ò0ž/xp;“ð>8üÀæ^÷,ö·r?̇€å‘ÍxYe–A#Ü‚K$lâYÂ$7™%–_VIa{"˜Y¤Ùæ?ÞIȃÉ'ðÙ2K,öócÂ>ßlIc¬˜ä0~òÎgâOdsÙ$³‰Ì±“™#’IežÉelòË$…„„“BÆK$’É6BI=²K $²I$æ6A–YeõðO‚ð&þoL}# 4‘±Y,³$ú±Ù<Ù5²K63’XÈ[a¸#éâ2Nke‰Ô>¤'$Vw/ludtI—Ìâ7™?ReldçNä|Žû²Y¶YÌï–X|rÉç÷ãq=$ÓõzƒþH.ÅŸÂo¬L?lLÀ§Ó6³OÝä’#(Ïq§x÷ð —3Oÿ*q޳Ê÷“?™x¾ñŸ®«.Çð±Ây2< ¿Ÿe.~o¡z†:?I.m£òÇÚÑ»ô  àL ʸæµxüãím~"àéª^hôP>¤cv5’ɲ˲BÄ$d²O,³ÒFÉ=Ù<²dÒCd$À’BIÉ$‘’@°Ù$$$9’A¤–<Ë,ƒ[8Í’g§©î¼ ‘“ÝÂAdlfÉÌÏx–sè‘ÙˆNIê3Ð!ÝÙûÏß¼-7à~yŸ¿-îô³¤q`o`yçŰSºL¸M–+#–¿…¹#v›Ðåéü["˜6B=‡”3qF#FØüÿï’e—ïîÞ¿ò¿@÷í-þôpÏå& õI‚Ñ¡êË3~~þrEê9øF3crÒ%~‘°ÿ ¿ïåþ3”Ìï¾f=ô¶üú¯‚ÞÏ 5£°â_ÉŠú²O|?k§¡Ÿzþ/Á_Ã}§êÄm<×{L+ô!½[ð~¡ã¾ Ÿ¿©!Ý@†A~yL/à õÅÈ?#~Þ”ú#ïäÿîyß쉀:x¨Œ?9OÏ‚‡i/ªZ?ýÂC’oYS=Àá¶]?°2óF{¾†FÓoWÔ—›€î: =m•\g÷äÿ‰7'éûûõ‡÷D#|âçGT»êÛÑ-Së~¯¿Üøc}=Ÿ“À§p^`ldæ}¯$~¾ör(‚ŠýiØ÷¾ÇþPÏ;¬²“a䀒Od‚N¥–Xä–YžÉa!$’I$’I$Œ’qI²É,2K$’É,æ9cðLã<ÆxG—ó~ôçäF>#öÈqæYd’0Yì“âi¶x Àd™«ÂǤþW/xsê:@Ž„¼8ô/y‘ל –J¬ôiäaû+ü¢ˆ?ë¼ †D„ ûS€h0,÷ÔvE<5ƒÛÄ‘áõ‡ÚA0ˆÿÄA§îqøÀªsÍ~b)ëüØ1g‘¸7ü̪BkÓÿÙþ0Ùß3.k?Üÿ‘?bGì2ö5Ì/ƈóîf3Ú!þ>üú,ë¿n jfíôÂɈÜÿh|ÂÇóG“7ì›÷`Ÿë¿V>ßKbY¿à{!C‰Aý’4¸ü$M5ùÝo’åÐ>´ÿ•êxOÇñƒÿ¥¡ÿ Ôbjg¾)†¨hkc™?ð0†K›óßÝÉ#A Ïã&·žN~ð±¥??ä¶úÃ5'™3>oüEìØtgÿÈà–' ü?¶Æii÷8x$m~Ómnp­}ä«õ›#Lš4,Wñ_ü–€Ñ± €?æÍ´í¡`T=síÔì#=|ü°3ÿ&læÙ,ò f $‚I€’D8˜Ya²Yì–O2I6BI$vÈY$œFË$’IâYd–A’YYıœ<â«<;<8ÿbÈ$"iƒŒx–H&ðüæYåš–H¤ÆÞØçRuÙ_‰#“ñ:s 3{œòÃy‘d0Acñq‚þ…êæ0pÄ!¥ŸR_àYd”G1±‰–ä„Ä‚’CQ ûð€ˆLÏ zcî8àÀI|lŒ $™€[Ò‰Sìÿx/žÆ? {ô¥ûJ‹ÐªÄ’y²8~3PÿøH®­æz›>¤ÓP`a“ ÏÈ*ƒ$`𠏋òÓû¬ú:«UYJ¥WYó†è"8^ˆ¨ú×®œRÁ€7ǃí¾0RNkŒÕVüž¦´?3±—šplqd •vL]ZŒÓ/É}ÕÕUœAì‹rJpª-P‘Â_Ìÿøµgå’3l“ñ9!’Y0YäžHÉÍHÁ$ÌÉì“™%’I!'™½>²LœK,?RYܲÁæq<Ù‘âMì–?Sî"!áá8ñ,—Žäq9èÉÆfK/:ÃÛð½Ø½†Ó›ÃñÏcññ9œeËݘ$ø²slÛḬ́“É7 AíöÀYìžHßiy–9‘›a¬„^cä†Ù&{d–Yì$³Ý'¬“&2I?™$öIG,±É,’FI™'Ë,ŸË%’Ỉ5³$rI?I™d˜6|rIŽ =/(YIõ!¸Y2ìžóòÉ’É Ñg ĵ½ücÒ~táòÏÍ86t8D~,ƒžÁÏ÷ûýD ÀÎŽ"1Âm:+"ñæÛyoÀ·cT ÑÇm½·ƒ?GÔ[}OçžwÅ÷7ŽñÀll6ú·üqû“5â9±»…ŽÙ¥–Ie„–I¬HŽq<œ×‰í’NL’Aì,²I‘’N$’HÉ$’2IêqÍÉÉÝ™ã9–Y;g“‹93+³$œmô­ CR.ã²2ÈïáÎ#6<ó$°Ë9²³³ð-“òÞí½3ä~>&ïNå÷Ïb"=lxN[µ]"_-·L½2O?I„’ÄY Cô‰kþÂÈŽ"32Çéû²ÖÔjp„Pgøgˆý¢‹„›õ Ñ,P Šxk'â'íD-þL´Ÿ`˜ŒDý‰ËÈ ãûÔ"ii£Á!ß„'ݰö>ÇlGFÏ'Äß|ª4žsBñ¯[oÔ~}™Eü?’öš7‘tõú?$éñ|Œ¿ŒUŸ5ŸbÑ ƒÓ~´#"¡;¼ÙÈàp„|f^°YÂ-´?Ý’lµÉ.ÕL Ae–LP`<’ÉõaihU²Â IadáË@jka'–I'3'¬ÚAyé²@TöBK$2É$°’"XH/Ÿ» ,)éù,ƒÿ°ßü—àϬ ±Ù<òG ,üGáÿÏü_§í!äOÿ¦MðýÈŠO^2Éåž»$–{2<|,›'Žõ²vdòÉ › Îdžñ$âId…’HAg²3É„’I'$™:ßÎ1§ûˆë&‘H,“ÎdAlâHÉ`27á!a{'Œ˜IÜö2KËœ,rö87„|2ö,9åȇçœlæú¯<îY|3™a– œ,âYe–Ye–{dœólƒ˜Y0 œFÎgØÏ†Y’Y劋'’Y¥žÙäÙ;çï $ül–YäŸÞMOãÿ©¾Û¿§þ$õ’Od’LÖÉ,’N$É ØežÈñ d„“Ä&Ed$òI&I,’BÉ,±æy!'¯&v =ŸÆÌë3¼BgÒI™ÿaYÚˆ°†”o,³ÉÛÝž%õ²Î&Í ±¼É†ó™Qæpàü³˜Ç}6ÇÃ8Y}p¼²ü@ÉiüˆYç3;‡3¹Ó¥…C™Ï®‡rOdãÑÏsàÞ—ÙÕæ=}gñg½5“SûFç§»Çssí³ÛܲÌÐÁ#¶94ßüÿÄžÀPþÇüJÖGIdÿßüñ<‘'¼I,6b62I²I$’,í–I'å’lâq5’É,‘øg2BÂOdâ{!;6NÌ’u‹÷é?h{Ä\!,ÁìÙò 23ŽËcÄ2Æm–÷™ÓôþÓ¹3‡âȲπAÀèÙdX_›ÐW “5ÿ*ØrûæÛ ¶ÚwmöÛx†g©ú?”0õ”¶ß‚™m²Ëlìl²˜þ%ów÷o/²`8ñ?sÏ2l’ñ ò·×þ?äŸÏß“0ŸÅŽåžN«ÿ¿rDß_«Òß_øÿëŸeî7ß_Ëg'1œ²I²dã²Iì–I2HÌ’{dŒ†I#²Y2p’É8ÉÄ’I&xÉìÈÌ“°õûÅëû‘t‹åá²O'§RH$f8Ù’Î}t:„Ð#¿qyÏ®–G§C!ÓŒXÿõî'5š™÷ 7 \Šø6tÄp8$X£|wwù‹UøÁö°ã8wòþ|ƒ =5ŒÂÅg2;–Nì“$YŸ‹=’O ‘öK$ÖólŸ» /£û¿óa'……‡0’Ï$–HXlæñ 8ÉdŒÌÈ 2NÍ“6OS$8–Y3!&IdŒœgd›&xÃDÍD°þáì–AHæ’0;eêì–M“e–Iacl› l²DÙgA#Ò"÷ lƒ8sx^e¹Ò#™—Ÿ àGBÆgˆýÁ”¼D ñ4ºÿú)áXG顳ÿû3ŠüQ~€6üYaœêÏÃbkãïâñóÿá“ÜÞ×RÊ^·`1Ñü˜29}¶¨+ÀÈ€SìžFç…5óîýG˜zDÚ½¼ÍƒƒÆc*¼O³];jýd¯æÒjŸ´œªý°$¶f õ¿¿ùdq“7÷'tgÕîÞ¿úý_q¹¿Çü ÅÉ]ÀüEMùD—ÆGR_RO<ý6;g#™™€I9Ì’ûx’IÄ’g$’ÉÙË'ó’I$ “$‹& þ8„…„õβy{–I$ØL–HIì’Y$ŒÈÙ#'¤‘âHOædã$ÌÂÑi¡ÁàGÈ™x.õÎ9;ÇFweæY$Y’s=êÛ2×#äY«ÓÖËï˜ð‰aÐ~À$²Ÿ Ç¥‘'À9ïË>Fð^åœ êidó$›îy–6I±³?†ÏK/×>³ïÿžûûÌŠ?Ûþ"Ë~Eúügÿ–:Àëð@d>ŸøP3_¯þ™õüùÿ蜿þÙù>·ÿÉ9Œúíé–IïwÎ0IcÆI]™ü’Ù…›ì’%“Ì‘æKÂY02y#¸O’I²I̽=$’E“‰ìÁ$)2 îy;3$0ýL®}‡¥àC /¶'>6Ƕ¿gß Ü±½ÐÆw,ëßsÁdp8^D6ô³¦ì3‡‡@ð8™üœgÄ,`±ýY܃™gO†Y6I~>–Y'HØm’AÜ}s>¬~ÿOüHˆ¥€²xÿoøOY<ßÖ¡ù?Ÿø`0¯þ™=ßålrÏ,üÉc„–#dŽÉç¶{;–XÈÀí“c'²yŒY&Iì¶2q’Y“1&1<Ë’S%–¦1$’H™$²„’LBI$’30m&|_‚.ë}3!;̶ټÉKgñWk²ðü÷êx6AÝ::Ó…œ"#àÙ@I ôxt,²Â mAe–YeŸ«_¦ÇôÚ‡úeBµ+ßV¿V¿M¹_¦Ü/ÆJý0ÿM¿Â6¿V°ñ•­{ã/ôÃ\Õ¯ÓhqÝ´}2¿8ÚÏÃkuaÆ_Iaª1Iõø"΄…öÃåÑo¿IüY¥áŸ»ì±EüŸ³˜¿ªÿlKŒÿû§ÿa?÷DÿÙ§ÿq?ƒÿîO§üÄ2où‰ÿ¢X?ýKÃÿ¸—þÑ~ÏóÿPŸúÔû¿ÿÿЯôIÿ¡XÿÏ;ùþ5—çü)ÿ¥IþÆ¿Ð'þ?gøúDÿÐ/ô ÛÏð'þ?ÿäŸú„ÿÐ'îÿïÉÿ¶/ôëþÊ_ïIÿ½'ëÿ!zÿ÷M_¯üÄØØØó3ÍÁÎ~¸RVtügôoÒÏêNß„åû%’ɜ٫±ÇXù‡IdŸreâÛÕ˜Û–Ëg]ã !:1òØç¶mæçB8g8Èá„FMü@-¢á£O õœÌÓ8?üÅÃþõ÷ŽA1þÛ¨ ,5÷þi¯û¦iÄ”ÿ²¿í©÷ÿš²Ÿî8ÿmÁßCî3ýsô“ügê?ôMû¿ÀÇýC~\ÿ¤zØÿ)ý¥0ië°~Î<ó»;Cweüôä~ßÿ„ýQ†éìšxÿ†yõÇo?ÃÿêþÑŸ¨ÿ%¿¨äÿÇ4_°&¯Ÿ‰JÑ FFžYƒ3ÐV ëûŽß«ÿcù¿×Gþ·ÿÝþ¿ÿÕþ–?oøïúYúÃÿêèýq¿üuÿ­¯ý-~ž6?÷söÿ–?öñÿ·Ÿ£üó×ÏóÇOϹ¡¤oÆùîÃÙ~ßðgþ»Â¿G(¿Ò§•ṠÓÎÕ­›åìÿUZ­—ë«õ¯óŸ©×ïWùõýÿó¿ŸþWñcõŸòŸÔÿ,þù` €É#7þm±ò Ÿ‡ærCˆm–K,ääæÛ’ÿ™-¶ózËÎ…õ0àwÖ0†W`xŸ³Ý²Ȳ##",öÉ’xŒÿ”°La2Ga]P3[øüœWûÉþÄ¿uŸëúþ¿£ð·¼ãþJþNüÑüUü5ü¾ßö[ý¶ÿeþ©S=QÉ¡À`Gò+ù­ü*þù_î‡ûÅþáñ¦¼:ÍXÿFé;”!8Iì‹÷¿ülÙaŸ±l€ØÁñû ‡ù#Î~þûóûü/ìß¾¿³?»?³>~þ}üÙý¹ý¹ýÛùû7ò/ä_Ê¿•*k›ù—ó/ä_È¿‘¿‘·öµ}¶»÷ÖÙýÍŸ³mþÇåñ„ŽïåÛg³»{c#Äl‘‘iâ3±›Ý…¿ñ­yÂFàÈó$€É6ÈXJIÆsg8óaN,{¼#{ìlYÀÉŽ8ß<È;‘p lž/òLý¡é~¨¥øœ™Îç GÀ2!3¦°Ø 8Áds ³™e†Y–YÌ7Ÿmôs8YÏ6,’l,Žoãýo»,ÿ›þ$ªý,+YkõjT¯Õ¯Ô¹D«_©«¨ú oÕ¶k«V–ÿV­Zà+Pí[µúµöZ´Zµ _ÁnÕ¿Õ¬·ú—hµk-q©PsÒU©d¼·5pBˆ0Áþ/0ü6—Ò2>k6>²N22X’Y6^HdæFuã‘’—–ÏÁ¿,g`²ç–¼Ë8OæÓDæ@ÁÇ>ìØà_KyIyœ¶?ùÛó?Ƀ¸â| v¸Ë,a,IÎ2É,vË,³‰%–YcYe–Ye–YåžÙáÆIíYgL²Ë,‚Ï…Ðu~¤xpf?¯øð8ßê_ž_Ù5oÕ¿£…ïâþ$ÝÛýZµo;çL?²?/gâÌðå|ïþ¹ØßÛ¿ έÚýZµ*Ô¢T«\+ÂÜ©R¦®T©{‚°Á>‚#ðpí•ÿµácø‰ƒ}Édœx !%“á!9–ažeôq=øÏÄDt:g3»Ð~…÷™±yþÀð„ƒÿ?:_î5‡MrLoÊË,²ÆË,²ÆË,²Ë,²Ë<æ69dŽØÙdXØížXØÈãc¶9e’XÉceŒ ‘€ ,²&ã pÙ~?ô›!?ÁÂ/DÌ#á#ôHýS1ïØ^˜×Ñ“É×ä8zsÃW8¿Qø”§fGgAùÊ!\öx±Ïƒ73ò‘½žZž fppÐK(üNBÿʲ3~ðô¿ #63$–2/êYÔò_3¬¦FKoÀÎcoÎ?r8^_{À?LYÂÈ"È2@ÆâþÕˆï “ÔüËÓ÷4IeœË,‚Ë,²È,’Ë e–s,ŒHüÃÌõ/zL½%z³™ÌƒÒÏ–{e–ygS×̲K"cÿÿÓÔNãþ_þuGŸí?‹ØßœýòZ¼7óåø×ÏÔâÏ·öÙ±&zCf@¤çß­‘¡ÿ³üØÿå5~Â.NþÞ "1s&L™â‰3Ë31c£‘"|³ẩÒì`! ŸÇù{f­ìˆÞmçÄuIJBBK$8ää¶|2ü%o ‚,÷„½8lE¬Gã¡ØôàsÏm¥üôhþó¼êøLË,㠲ȄÇcŒ€c‡‚Ye½HêŒZÌ~táÙ–Yaa°Y g3=/ÂË °±gLYe–_âaíýfngå3ðŸánúùc?ö^Å÷RþÞÅý¶í¿RÈ«^92GêþØ_dŸÔ¿£ŸË3gõ-þ2WêRþ-¤úÿšyøt2š‚ŸµŸ@Ÿ¦À‹‡1þ2åÛ˜¹šáŒkÁ­wø R·Ê2§þ|H„k3 óÖýÏb‹NNî;lïv&Âx“}Úfm¾ë93–y·œÇžô±–Ü"=ÛÎ)Ïr dž Žä6_d þòkdGy}‚Ø“¡À²Îç2Ë,læXÙ̲Ë,²ÒrÒ^#)+­œeøLË8ÏèÇ ž…p| áÿ„ ?úk$žAcþSè>“øye‰©˜ý6?LÿÇé±ùŒwž<#ϦþG8ýOðlþ¬þ›øŒÕŸÔ²Íý’¢ìÈÃl¬`H2*Ï¥BÂð?bþ/ÇðÛûšÝŠüd½üKýK‹ú'õ-e£ê‹+õ/õ?Æóõkõ+õkõkõ/õoõ+õ+†V~!þ¡/„0³ñ ÂfcŸó¯ÛB ~:ÞNlÞdí®;;²ÐÎe’¥èúÌÞ}ñæD‘ƒz©ÜÁð6g3È8ÂȲ8Y»±²Iéÿ3ºð´¢ñúÕíÿpüM¾Ðèß/÷ý>„ø~lÙ³fÍ›6lÙ³gŒñ‹6,Xã^ÿô¼…õñÿì`÷dñþü ž?I=“–yÍŽsgàc?WößÄ¿³ì¿¶„~‡™ý¤ŸÕŸÕŸÕÔÑbOêGêÇêCdú‘ú²ýgöÙýHýr”üYýI“úá­þÙ?©þ3ûOíŸã õbRÔ‰Yãû½  )ùßS“{}îñœâöNFL…†È,ó'šç6NXóÜçÙÃzpüAgÏ,€à˾pÏüøA c†²DY3п%êÄ€þ#ÆöÏО§îÎûëä ²Ë,²Ëe–|òË,²Ë,²Îe–|rÎe–Ye–Ye÷ýŸÿ˜?úžkc;“oÿò"Èý$Xé›8z0Ùç<~=Þm‡íød~¸f,p™=Û™àßížOñå\µ¯¯ÄƿŒÿ û$Kdyð–`Ä 'µ…Ίqfv^#ÏdvË$¾²òYøfXÞÉ›ØÙdnŒØ|N>3Ÿˆµƒ¹Ì`€DežäIãyW‚ϸá±#§ÈÒ`½™gÿ7OÙcö_Äÿ7ûî;ýÿNúõþ~Ïð'íÿýÜj³˜HäAeó×öˆø%—Q™«SóÁèþííÃ?pþ-T·ÔιÿÔžÜÃóþÎ~T{³wújnî"'¿÷›ýÿ_?Ñqò,n¿à_ñåý¥ÿóïù—üŽqÿ7ú}«{èµþãº_ì“ÿg¿Û8ßæ¿äøæt*ŸÞ ©ú‘Ò190Ź œ°Aà 5’Iâq›$ '™§6S9–ñ»À–Oæ ,`á¬= €Î=ˆ 6ûȈŽA̳þ4ϱ»Ïé´‹iÐO‹ýºWçüë÷š?k÷ÿâçßîŒûŸlgæÄúûþRû?iýá/çØû•¿wþ>Å#ìv>íûÔ¹Øû•¿zÿÓùÛü?í+ùßþÿ˜¹§ÿ¯óÿoæ~áÿ·óêÿô¿ƒÿsÿ;ù¿ç*2ß»¤´?ê¼×úï"öãÖÐþ#‡ßº~íöÛÛÓþ’¦¦¤ÿE?7µú ýäû?gùÿcïÿ-úßáúLôXý+ö ¯Úvø~# øpËÈûÿÉÿöï³ýâ½Oö>?äÿËŸíÿµÏöøÿÞcÿq‡ýö²ÇþÏ?ÙãÿfýÚ¿CþÉÄ÷™þÏa4%ÆCAå!7þÎ%û¤ÔŸ¦B!ú qÈÏêÿ6~ÕþwÝšýfšoÑ3PýP?] †?ˆŸ²§ëãxÊ1ÅC1±ýøIë𙵰~ÒS÷B:!â‹…øDÎü2Ì´ê0/[ ÈÎa–É·ƒmñg±ðo¸èFgàp‚ÄAí–FÆÄ/Ü!&px(®µûà\ý¿ä_îûEþÑ?ß/ä”~Ïù_¥”~çù_Îÿ+ÿB¿‘þQþëê[ø_å¿ô«øÿå¿÷­üòÇêÿ—àD®I}·ŒºûÅü|YÔßÓþ;ý,?ðß«ü7ú’ÿS®ŸúëýE‡‡øÿ /ô„ÿæ?DûÀ^ŸýgH˜¿É~‰û?ÄOÑÈ¿?­y~ øŸֿÿH^ïüôÿü ÿ¤/ÂBOÑgèNÿ‚þþ„úx_¿H¿€¿‚SèŸÐ¿‚QgèŸÒ“ô,!!d„„„„†Y!ú$7ÙOÁa!²HH/âBBBrrC$$$™™fmþæ,ÿµ¥á?·œxü@‚x¦Îso2S/:¤fLæYÏvy¡Æ~o«$ƒÎ}DdæÙ‘ŸƒØXáÀ`à<’6Päl11¶ÃÛ[x0ðm·›Íøko7Ë}µï»ð¢­—¹ðûù}¿'>Nq^¯t ¾Ëêûe9äå³×¯6lî3¼ffW'fÇ=ÆeÖo2Weœœ”É.KÅã;/Ó9¼se6z³ Î %ò9¤29ôôáÀþ¡ÿÀßþ|Óà¼vvÄø/àç^<{ŒÍ¶¼xŒçÉüÌ6:—ל[4êÙdçÎ7»'¼ÉI·8eöôüü„$ö ã1È8YaÌW„I)¢BÆ ²zAcdaÀˆ0‚ Œˆ ˆ‚ 8YäA¶AÂÏ"Î`ÀXÙÃàpüÍ?§Ÿü|Ÿƒóyç¹ÍâñxË–ÎJÏ/Áe•µâÉíœß^¥±¤ÚqØâOQ²É6‚lç‹&L–“3Ì´areà„‚ó&?°AÒ!#€p,ƒe¼/³‡1È=ƒo8 AÃà{ÓdtUˆŽ0yÀàs €Œ°éÃ;¿ü¯?§¿ÑÎçÅø¿5–Ùz¼%îÏ6^?c8³oÇN7™Äfò[X‘œ–ÃgólœïGÞ%ÎlíõÄá³,gì‰a9"{áë}Ç ±‚ ‚Ιd^ÁÂÀ‚ç¹p,l8‘¤AAÀàp[ð éd|=æAð/¹áýæ|Ÿ–üþ²¼Yxü×á¼^/6x¼^Ñx¶“Ϲ½â|æúœÆC#ŒgË4ã²¼K虄'BË9ù`ގ˲¼7 `ƒ‡3Û"Í,8 =áœcÑv,‚§‚""Â8?÷‡3¤Yðxþ þ‚á?ø'õ÷ã²ËÆwäÏ߆üŠqë½Kë‰ÌŸ“]‹ÞüD³ûð“Ù‚_1%À÷„s‡æÈ¼ØÎäk`FY°d lG ƒàfADfp¾£àc±gÈá#à&Ç3åž|þŽðßžŸ:°‡«~Þ–ÿS¯¿&> ÝŸ‚máÊÝ#§8tŒày° š™dnD[ Aa°½ÈŽ ±/àAaÀ–ȈŒÞoMø,ïÓ}§3ú‡õÏ–ÿAŸ†ñêËi JdÄï5μ̼·žpqâxÞäÏØl‰# 7Ã6Â#„1ð3†2ÁaÈ!‹ƒbØPˆ8¡æÂîsaŒ‡„ ÞGçšôéù'ññ?ÿ?¬ÛðYŸƒÅ•ãÇäðg«m¥½^<ß‚k3¿ y–s#{²G¬ßo>¦I°â6ü :IæÇæAƒ < #Xf6G ê X^{ÍîAÇ èDA`FÇæ8‘˜éÂØ¶:wé¾þ_ø‡õsÿ…¿7›×ffYIe—ÖÙã}q©ï8_| ŒŒƒÇƒ ÙCÂöû} €Î¼Èàa‚l'b5ÈàpyöAÀèó~qϲúáø¿'ÿ~g xìOÞŸdø6ñ~Y”âñx¿¯W»Ìe™Éâù×-®sfwº[>¬;íî· °Û/9¿ø“Fr|eˆ`±ƒíˆ ÷…aúŒàE‡H,2Ë#„xAÀád±°|—‘ÂŽFÞÇÀ7ú§ôOèüuëÇšÿQã=mãlÚqããúCÿÀ?¨¨˜l=ó»Íø<^,¿ø}uÉzüÖßzïTø/Î;‘ͽ‘߇ÝõyñÝægsóðcvv {½Þ¿‹"l‰fK/¨ƒ€AMá}Fäs8[ ˆà¼ØÈàt[{½8p ále±Ž áøx`[òßþ¿Ó~oÉ–ÙS‹=Ù–[om:÷m:Û3ÖÙç»mø-æt[\ï¹çâÙ<3ŽNÍœËIσÕGf2ac°08­Œsdo ¾´Æ¹¾°Ãå£2×~%šA±‘o¼ˆ}‹ÈàÃ_}"†>ÀùÃÿŸ#ÿ—²üN,¾ñ~+/_†²ña´&ûù¬_y3ÖËg2×cŽXõéáf»g>â÷¸G<áÂóã¼ü‘ŒÛ.·¼̈ „/{Àáa‘À8XÄ1°ÞxÅìt€ˆˆ†!æðŽí¼>Oý ƒä?ü>[Å™–^hÌü–g«/^ï=öÑ·ž^KÌFs/%¶[xÙg6É“‡60xóÑø$ŸýÉÎãÌ€ŸÄéý0*~të5˜#læFå¤G„G÷9ï Ûo¨Œ²/Äñ`ŒXƒ "#×âFpáˆáò>[͇Ôñ_†ôîü~kòYeâË,¼z²óm–[[ežm¶Ëå¾sa–Üe¼×;²ËÍ%ø/Ç~-“Õ†}˜´ëÂolã¶÷c"d…±g9‘ÀèÞÃça-2ú‹c„1æ"îÂôèùœ!¶,ÈÎl2„=Øw‡ bz1Ó„|¶ÈøïÀø{ý]¶^l²Ël²ÛÅ–Ùx¶Û/7ŸL¼ÙKmâÛ%¶Û/œÖÞm¶ä¬wN1œ6X·…±l|Nj‘ןsÆüÚôá{0úrß$àù 1{ÞFlä7–ÁÂ"7ƒ ë!6ñ›ÞÄD0‹l~IHà¶ð‹a·ƒÁ†Œow›kV}ð·ãçÇx6ü7›ñ߆ü‡á¥²Ëo²ËÅ”–Ym–Ùeâñm–^ì®óy­²Û–¼Ö[fm¼ò[[e–[}âù ¾Úä6ÛÍ%„ãÍ9²Ä»Ñ]Ù‰[êÔeÖÒx¶Û=̇؟ì¿9Kaç»k °Ë°ù ²[¶ŒZCl0¼†ÛaÖa¶!à¶Ãä$n[oXxZÃÂmˆa¶_!‡ƒ ²ÃÃ2Þí¶ü¶oÃy¶ÛͶm9¶ËoW›m®Ã,¶Ë,¶Ël¤¤¶ùl²Û)om—‹oKm¥²ã ²»o4ɶÛfïòrýíû¿Ï“ÿgEŸö÷î¿ÛoÉôÙåñ¸ŸÆâ þõÉ´Éÿå¿›ä<ÿ *þ}ü›ù÷ò¯æÍÿŸ6_0Zûú¾ÆfyuÅýFFþ$ò£øéâÏz¹ýsÇkø'ð—ö/ì_Ù¿–?’mü)úÿþy^FþåüÛù7úC[Ã&¦§ë§ì·þÞ~ßòߪ þÉ0›ÿšý?æoöL~ñ…Ù»îÞ¾ì?¶Dý[ÍÕ üËö ül~žoáF- òxÞŽOêˆgø¤}ÎÉØ4ú³àÿÝ6pŽð–Ž(Jú«ÃÕ·÷cö7Õ­ü¬–zÅe7üÌoÀŒ/æoäü^?ÎßÎßÊÇîgö,7T~M_¡_ÌÜ¿‘¹?R¡Gî_Ì¿‘?½zî£]úU¯¢ŸÛ¿•2nþ]üèýø'‘ü¹ñW?·>þ]ëùßÉ¿“mæå'®ðüç÷cìrþû=Ûû-ý–þÛkû–}•™üì°ýÖ~ëù§Š±_Ê^n¯æ[ý¡Åþ×÷ßß?Ÿ³û¬L~ìþæ¸ø’\oØÇÔÎ[·Ÿæuœ_óO÷Íß fÌ`ÃÖ)a³ƒ&L™2Y³z7ô¶j¶s!{ä=\/·'/Ä~H¿¡`þ®â`cì(Q¦gämþú׸†þÒöÜ@}oãÏèOê@;™ÇÁ'|6?[™ùh'Kô€f|~+ñH>•Ò_é 5ž0ïÿÈ•ÿð8Á5ú‡ácÛrl«ü~n¬Ê`ÓŽþÁŸ°þµåø${‹Bò×&_çA’í˜ó^2{ þ’¡s›dFêAÍU¹Ríc¹ÆíËçpíÚýò;vöÜÕÛ-ó¨VåöÚçVËe¹v¸ÖÚãm½•n8îümü®¾5Ã7kâ$Ý®5ßPø×ñù|)ã[ãòµð sð eø[jÞš¶Õ¾BãYÁ5¼*Þ7kƶݫ\jq«[Æ­e®…µV¶¸ÖõnzBÛkko²ÚȘóm…¶Wšïuµmorex[oÃey¼N9ÍÙø2VÛ¹ooXæ¹m¿çÇá÷}óc›3ÀæÛÏm‡š¼^ýpží¶ÛÍøôàÿDæÞ÷«ïÇëáäüÔ,øoÁïÙ1Íøåæüµœ¼Ì÷x‘ðûù2ñé»Íî³yœÞéݶRÛgŽ_›ôÍøƒiðÓŸq*¿Ó~[Íþ³{ïäÛÃ-þ¡×óÔïÜ|Ø>|›ýwâOÿî³ù·¼xgqæóe;½Ø³{Á³HÀ—xoÇ0ŒÏ½ÎaÁâñŠzÓžï1ËÌæóÝæÏõüÞé¶Ûñß’{ŸÞç}Žù½ß›z|ωyð'åïô~þG6{÷ð߆÷,ù¿aøæw;åä áÌø7±óη»Àéñ"ú^ïÅæð‡bxk¼Ó†3—æöÈ<Øþѹ÷HŠp߆sgsàüμcú,Gôwàðæó>^ÞgÇËÏê=y9ýÞ}ÛÞGwâ¿ îœlŸ ïÕ±oŸ ãòߘüN,útæ|´ŸÜG<Ž0¶ðx±k³ë{“Ͼotèo=ÙæóÛâúL3Ÿ'á¼o¯––{ý¿‘Â÷âgÁù?ž%·ã¿úÇõ~ø0Kóûëðx@8ÙðÝ?¢u¼áÂÞ1àÀúæ÷ÞŸ™ù›{ŸsÓ‡ËÒØæ_\qõ½µë¯œé+ —’ØÞsÐ,>fW½Ø}æÏ0áýg8_|™Ág¾_\éí¿æÚÞÞüο1îw;¶qøïÄù¿»~]úéÍáÀx #Ãá¼÷à|ðîË,\òGom²c¤[Áö{äZÞóêÔmx¼ÎN¾ðæ÷}³è½Ëo7à]E´oßü%`vŲ"?€¼Ç®gÁ>y§3™ðÇgçïVÏþ×7…Î{<«¿üq–Îl6óîrÏê9Ϥ­œø¿,“à<ÆñsåœÑ‹Î°Ît¶]›W„ñ9ï=ç½mæy¿ æËÏ/cmñ-‰8ñөij_±·‰Å9¼oQ|2σý-ø¿v:qçŸ îñ3ä6÷mù¿ {¯ÅøçÈøïôŒù¿-fóyøïÑÏ¿˜ü>ø|7Èxußè¯HcÍž)y±a ѸOÃáð%-ƒÞ{ðÈôë¼ÒG–º&fŠi÷Þ7Ã9ž ÇŽe·×\Þ}Kdg2ŸÜlŸ-¾¾OË5 3™òÎýð~Fóy½Ñþ‚ó,ùyý|ù9,þ¦üþ¸YyoÄ~|óȳÞ{ðÈ´Ùè A‰¡çÞ“kðúžçs‹ÍöÞm„íì[!®›™ ?ÛÛ‹4á;-Ÿ¸çô7¨ÀÛðÄž¼öö/yïÃî ±bÌkÐM‹ú'~I{ÌæÅŸÑφÙg~™eÒ/~ õmŸ`ï×ÃoyqøçÃ"ÆÆÇ˜Ù#'=±±Ë,ll²ÆÆ×ÐÚýBáåzg¶ßQ_ÆÇêoå_ÀÏêo§Wð·ð_|þUüëù×ñ7ò/å_È¿‚þb?¹öOà„6¢þuüÎC™¦þ;îã?ªHô¿Šqü^üZ nǧ!'ˆ} üHg—žœ'ëƒÏlØ€óÅr?¨º³ûY?1CU‚àþx³Laëçe± 9E ò3 å¹l<{ŸŸ©x>3œ;ä?THÁ¢ûE~ ãé)ÅOâ6Ám’i¼8‚ü 5¹½ý6I‚]IšNÙ¾y×öhI¯GÙ¢0½~f/äñ0C½0š¢?.FQÀØ(…ÈõÁ¾6þ@Bv½ φ(ß*Q€í"õÁyH †1à±»Xž3Ö˜`¿ó?ÅnßÖ_Ú·ÊO@’_î€r› ¶›ã9––’xÎ(þ©ÏÉûÕü|Ï´¿Š(? ±ÿY†Å‰³fÁöX'ê`¾Ëê-£•ݘ-ÜVLFr³fþbÍ݈ÊÇ쿘¼/ÜÀX¼/OÏÀH‘û¿“¨ÇóXûIà±Ý4?Ùñ h ÚxƒJ^qؽ¸;Må“ynÆLó‹öÉä4Áм¬Í 8gõ"D‰™l ×Ô§ê?ŒÇè8HË0oÀ Ùâ<×x¿müÙ•cOÑÝc]ÖþHÓu#}׋ïÔ㊃ðŽ9¹ýéý˜FÜÌ–þç?Ÿwíwó¯çGìÇЦÿΜèß½Û{‹¤Û®rü}qöWÇcÂGó£‡j¹å}{vWÃö%ü®Æ·gGµe`h-˜KBÂ~Ù±äw€ DÜ¿H¶WíµÃVÿm«O»kû…û”<ú‹/1ȦÚ-àûg"„Î¥þ^S›}ðÎ?!ïáø¼ÏŽ¹Ï¶D`/#YÜ"dêlò?™á„ý==c¶¹¶y=ü†ÿBÔÚLêé%,–ã›?«ÏÔ ÇÕ¯Ô­µ?¿¤Ó"1"µ(0H¼xÀ£DƒÉŠZX]’ô  Ș–' Ç©?¶Ù¼øîsÞm§}æÏÃmŸ›ù‰€|bOKcŠÍ ùgõ7¥¼÷úe–^}ï—’Os€?ºÿŸüðÕùPñŒüË}? â|7Ï—×uŽ#ÂÛy«Íè‡Ámvg4$Øð3(“$Ò"§bÇ~=Ø1cÌ{Ãó×Y þä-ŸpþqæOŽ1kl/võï·»+ïž,¢Åê$(cà€dêx+GQXò‚±„°µiepJ¯ôs8/ÈÛÞ{ͼîñçŸ ‡Šód†Þz–|³á¯Ìƒç——™ðÏ–Ï2Øç½÷™ðÇŒŽAbÈÐ’^IöØ– ÔµO¦þþ6ü7ð6Ÿ«ø¡ßÅëøªz{kõàÑd>ç·Ôþÿò½ÿ0³ì¤š*XkqŸÏ+óæÆS–ã ßeômàŽiž°}–˜FØ/ „Î 9äº}1ãð‘ú^s2’¹ }$~›`Y›G <€ ‰¬§3:Ëîóàg>ù(ôfÅ/láƒl¾ÀÛ!g‰eŒ2Ë,æI`}ÌœË,Ï»Ýÿ› Ùd–Yg3ŽYaû°,°l€l²Àl¼€°°ËË,/, ?pydɳ͇VÍ„ÓIKfÍVðÞ4†¬3ËfÍ›6z[7 þ xƒ?µåøåŸ× ÙóÂÅÔ‡è–?GƵ W[ݵo–Öþ5Zpýox×u•ûgö/äf?‘möÚOJ߉;Ë­·iî÷ Uü œðÙs—ç™>læ—1# åîö6\úŒé³÷1ããQl^CîÉñÉóq¿›uÍ‘€D?L«³O« zÈlÞþ˜X9 ]ÇF‹kB#dÊœ5¶W ÕÞû{{²69{d­Œ Œ íŒ Ÿ£géb±ý6~˜±²gèÙ2dÏѳ6@™2#áüð<¿‚þ⟭¿‡©ü-üoâoá‹?_Áz~'ô3ú"u¿ŠõÌúä&ùÁüWñ|Jœ¹IÊ(§â Fp…üóûâB©·­ƒ›où!>ïN³H(þ‘úÀþÐ"B~©üŒ)°û'èSÊqާ?Ò^™ ÄÍü'qÄçèwïá$âgê+þ3ú×è§é3“æ<ÐËŸŒ~¬¸ƒ™!Ó}7ƒ†ôYúä4Äh“r Ó2œ7ëùfju|Ȳ{^±˜1ì2°¨Ãú–)Ór,Òx@Zø,ýøR³ý3õˆ’Ù†[p…#­µ mÔž÷+÷hþlæ`á',GB4šÄYÅù~#?Q–Ÿ¢óôFlæþ$ïà“8B·âOêDÕü,ý,X³ô‘ú˜Äþ‹«L²¢CõÀ?PÓ¥Œ²Gê õa¶gðX°b‹ 3?RoÕ…ˆ/# 6rÎg·™ð¼œ¼¼Þùy›yÂcáäþoÏÁŸ Ç7¥†9¶þmöÞ*\KÒ[oÂÛXM…-ñµ¤¨E«q6²V×eµ°û+3WñÅ‚ÛLüq“.±/±æ÷-ÿ7Õ¹7É“±ïØm2Vþ_ˆˆ„ØÁ n0ûo±gÈtòm3ñ*Ï=ËaÛ'ð{,?2úJÿÄÃð?Wå“, ÏÿÙfwupd-2.0.10/contrib/qubes/doc/img/heads_options.jpg000066400000000000000000003764151501337203100223730ustar00rootroot00000000000000ÿØÿâ øICC_PROFILE èmntrRGB XYZ Ù$acspöÖÓ-)ø=Þ¯òU®xBúäʃ9 descDybXYZÀbTRCÔ dmdd àˆgXYZ hgTRCÔ lumi |meas $bkpt ´rXYZ ÈrTRCÔ tech Ü vued è‡wtpt pcprt „7chad ¼,descsRGB IEC61966-2-1 black scaledXYZ $ „¶Ïcurv #(-27;@EJOTY^chmrw|†‹•šŸ¤©®²·¼ÁÆËÐÕÛàåëðöû %+28>ELRY`gnu|ƒ‹’š¡©±¹ÁÉÑÙáéòú &/8AKT]gqz„Ž˜¢¬¶ÁËÕàëõ !-8COZfr~Š–¢®ºÇÓàìù -;HUcq~Œš¨¶ÄÓáðþ +:IXgw†–¦µÅÕåö'7HYj{Œ¯ÀÑãõ+=Oat†™¬¿Òåø 2FZn‚–ª¾Òçû  % : O d y ¤ º Ï å û  ' = T j ˜ ® Å Ü ó " 9 Q i € ˜ ° È á ù  * C \ u Ž § À Ù ó & @ Z t Ž © Ã Þ ø.Id›¶Òî %A^z–³Ïì &Ca~›¹×õ1OmŒªÉè&Ed„£Ãã#Ccƒ¤Åå'Ij‹­Îð4Vx›½à&Il²ÖúAe‰®Ò÷@eНÕú Ek‘·Ý*QwžÅì;cвÚ*R{£ÌõGp™Ãì@j”¾é>i”¿ê  A l ˜ Ä ð!!H!u!¡!Î!û"'"U"‚"¯"Ý# #8#f#”#Â#ð$$M$|$«$Ú% %8%h%—%Ç%÷&'&W&‡&·&è''I'z'«'Ü( (?(q(¢(Ô))8)k))Ð**5*h*›*Ï++6+i++Ñ,,9,n,¢,×- -A-v-«-á..L.‚.·.î/$/Z/‘/Ç/þ050l0¤0Û11J1‚1º1ò2*2c2›2Ô3 3F33¸3ñ4+4e4ž4Ø55M5‡5Â5ý676r6®6é7$7`7œ7×88P8Œ8È99B99¼9ù:6:t:²:ï;-;k;ª;è<' >`> >à?!?a?¢?â@#@d@¦@çA)AjA¬AîB0BrBµB÷C:C}CÀDDGDŠDÎEEUEšEÞF"FgF«FðG5G{GÀHHKH‘H×IIcI©IðJ7J}JÄK KSKšKâL*LrLºMMJM“MÜN%NnN·OOIO“OÝP'PqP»QQPQ›QæR1R|RÇSS_SªSöTBTTÛU(UuUÂVV\V©V÷WDW’WàX/X}XËYYiY¸ZZVZ¦Zõ[E[•[å\5\†\Ö]']x]É^^l^½__a_³``W`ª`üaOa¢aõbIbœbðcCc—cëd@d”dée=e’eçf=f’fèg=g“géh?h–hìiCišiñjHjŸj÷kOk§kÿlWl¯mm`m¹nnknÄooxoÑp+p†pàq:q•qðrKr¦ss]s¸ttptÌu(u…uáv>v›vøwVw³xxnxÌy*y‰yçzFz¥{{c{Â|!||á}A}¡~~b~Â#„å€G€¨ kÍ‚0‚’‚ôƒWƒº„„€„ã…G…«††r†×‡;‡ŸˆˆiˆÎ‰3‰™‰þŠdŠÊ‹0‹–‹üŒcŒÊ1˜ÿŽfŽÎ6žnÖ‘?‘¨’’z’ã“M“¶” ”Š”ô•_•É–4–Ÿ— —u—à˜L˜¸™$™™üšhšÕ›B›¯œœ‰œ÷dÒž@ž®ŸŸ‹Ÿú i Ø¡G¡¶¢&¢–££v£æ¤V¤Ç¥8¥©¦¦‹¦ý§n§à¨R¨Ä©7©©ªª««u«é¬\¬Ð­D­¸®-®¡¯¯‹°°u°ê±`±Ö²K²Â³8³®´%´œµµŠ¶¶y¶ð·h·à¸Y¸Ñ¹J¹Âº;ºµ».»§¼!¼›½½¾ ¾„¾ÿ¿z¿õÀpÀìÁgÁãÂ_ÂÛÃXÃÔÄQÄÎÅKÅÈÆFÆÃÇAÇ¿È=ȼÉ:ɹÊ8Ê·Ë6˶Ì5̵Í5͵Î6ζÏ7ϸÐ9кÑ<ѾÒ?ÒÁÓDÓÆÔIÔËÕNÕÑÖUÖØ×\×àØdØèÙlÙñÚvÚûÛ€ÜÜŠÝÝ–ÞÞ¢ß)߯à6à½áDáÌâSâÛãcãëäsäü儿 æ–çç©è2è¼éFéÐê[êåëpëûì†ííœî(î´ï@ïÌðXðåñrñÿòŒóó§ô4ôÂõPõÞömöû÷Šøø¨ù8ùÇúWúçûwüü˜ý)ýºþKþÜÿmÿÿdesc.IEC 61966-2-1 Default RGB Colour Space - sRGBXYZ b™·…ÚXYZ PmeasXYZ 3¤XYZ o¢8õsig CRT desc-Reference Viewing Condition in IEC 61966-2-1XYZ öÖÓ-textCopyright International Color Consortium, 2009sf32 Dßÿÿó&”ýÿÿû¡ÿÿý¢ÛÀuÿàJFIFÿÛC    ++&.%#%.&D5//5DNB>BN_UU_wqwœœÑÿÛC    ++&.%#%.&D5//5DNB>BN_UU_wqwœœÑÿÂÆ"ÿÄÿÄÿÚ ö_(寪¿+O¨¿1’ý#ç3>…óùóÃÊ=§‘ëO7#Ðpg—“3¡¯#)r0¹  Y£p“!chÆÑ%DQDQ E‘DX Q`@ J-@(€€ÁHT¨À,JÀ°T , )JAPT¡@ E¢À°(–~^ÅÞepFLFWfÀl˜ hÚÔ7]{@èš ¿.aןô3óG«—kg…è>>ŽüÝ>›/—U—É£ë²ùö9|hûL¾&ÇÛåðÃî³ø1÷Ù~~?AËóÄ~‹—ç#ôwçY¡¿?ÈûÛð™pø«/Ú>7#ì!™õ”Èú—ÌgI>w#èG¶ñ²=w—”zO>ó“#©Ïœma‘e¦,¡B2²²„d"—B(ÆÑ EÊEDRJ°X(X’”¨TAPT ±UAPT¢ÁPT©(-P~\;ÀDX[ ‚Á± ‚ ¨*B¨( BXª ‚ ¨-ÄdÄ™1ÌRæÀfÀlkZ†æ‘ºèbu^Rõ9GV\c³>zùƒ×¾@ösðÇ¿—Ï#é2ù¢ýF_-OªËäì}nO°Ëã¬}–_>×/ŠmŸÃºÏàì}æ_O¿¿Ÿä¿~ø,¼|6Qöï‹Èû7Çf}sä²>­òÔú‡Íd}ùí‘ïO#Øy9ž›ÎÌîrdt´çäQE„QETP”E€ @DQ(P ( ŠJ…!P P~^³¶@(Š[$QE”I Q €ˆT°¶ €€‚@°TPT¸Œ‘!•Äep˶é˜u^AÚâGmáƒÏóG¦ó¬òG³—ŠogO¡ËçÓgòÕ~²ü˜úì¾C(úü¾?#ëóøÜϱËâò´Ëâéö¹|U>ß/ˆGÝeðtûÛðtû¹ñ¯Ú>7aõÏ“Ìú™ó4úWÎf} ç²}àæ{ožËÇÈõž]OMçdz Ž×gKFfÆÈÂå m¨‹LT¿˜+¶qQEDZb¢(ÆÑ‹!‹!ŠÓDQE²„e´ÁÅÅÅÅÅ”"ˆª“!£CCCPŠ"’)bŒY 2„QB(ŠH¥Š @"ˆ¢(Šä1ZbÊDÁXÒ±%€ ¢"Â¥Ái ””%`¨* ƒkPßyÇF\£±Æ;o ;o;òó‡£|ÕzSΫè<äz7Í'«—‘Oco‚>ƒ/I—̧|ÀÜ( @¤*ˆR‹H¢(Š"ŒY¢(‘ ¤  cr„™S“!‹1ƒ!‹!‹1ƒ!‹(bÈbÈbÈbÈarÌÆ á‹!‹!‹!ŒÌc32²…ÚÈÆg¶9Ý:L« 5¶ M£TÝN,²‰EDQEDQ&EŠ"ˆH¢,H¥–RPJ€J @*Q(%*PB  ©B ° €¢,(¤ÊJ‚Ê%Á` …€ÊkË M­PÜÒ6µ øëÈÛ( Š"ˆ¢(Å‘qd1dLY Y fx™,—og/\Nn1ËcJl5€MXíÆÌ.tÔÙMM£ShÔÚ56Møš[†–Ñ©´jm†¶Á®mÛ¦Êjm†»•5¶CccccpÂä0f0d1d1d1d1d1d1QEDQclŠ"À¢·¢) ÉE€ Q€²ÄÀ‚¥Z”) ”P QD* ”På­6±uñ%ïäÖ2BTªÁR{5@!A,Á””€X, €¢(Š"’(Œªá2ˆ™£jàÌ`Ì`ÊÖ É„Ø56ë1³q¥¸i›†–ì i¥´jm†¦Ñ©²š¦Úim›¶Á®m›F¦Á­˜×r26CpÅØ @QHQaÀPYH‚‚ %,RP @@ ¡*  ( b2©%¥“"1ÈT-BT«*(–À!@aH,(AR“(‚d@°Ául®«g%ꮡÊéÈÄJ"XJ `QKQ€–¤  €,T¡`"аXÀ¨T )@(b‘DT Ê”%…ãJB¤) BÅ%À° €, ²ÄX@²Ê* À¨%€°,R‰H¥„, ¨- eâVÎb2¸Ã9¸RÜl)“››“› e$2cL˜RÜ)P[€ÍŒ3aL˜Œ¤1111 ¨* ‚ ¨-Äe¤*Q` !) ‚Üi`T *€J`)J,˜ÐJe, ‚(¥ ( J‚„°,µd¢,JB¥²ŒVP¤—À”ØZC)b3Ç«SÒÔæC•ÕN7b8Ýãv9Ú8ƒ‰Ú8ƒ‰Û%ãv7XäuÖuŽGPåuWL9çL9ÝÐÞ47ÃKt56Ã[b5¶CceÁÆÑchŠ"‰2DQ˜¨”‚À,EɈɈ̀ͬlº†Ö¡µ¨nºö¾iÚ÷=7´‡=7Í#}çE74ÙsÓkHÜÓM­Pß5 ÓXÚÔ6µ“±®™µÓ60Ù0±–ZÆllFLFH-ÄXª–‚‰()I”1Y@!PT‚ JB ±J”\½[ÎIuX* B ¨P°‚ BÁD*(‹`¨*$E°TAQ AB€…,Q‹!‹!ŒÌar¦¶Á­°k›†–᥸šfñ¡ÑWštÓ•Ô9]DåuS’vØ8Ý£‰Ú8§páwS‚zÏzÏzÏzÏzÏzàwŽu<÷xàwŽpávŽ'hâv;Ô9]ºÛ!ƒ!ŠÀ‚±´APT"äÄe1]YÆRŒTg, qÈÇ«“«S;.òQ@%`‘` €\@*°P(B‚¥*"Ø* ”¥Y@ŠÆ€«(", ,ª DPD `)( €¢”‚’‚PDPPXBT%•˜Ì¡&JÁœ1› ­²MÁSv'.ý³" È ¸äaÓÍÑ©²›ÈJ@AQ¢J €"Ø…APR@²À (R‚€Š‘@ `R€¢ J (( ‰@ E‘`(UJ`(€ %1²ÄBJ©°J"ÀZŠ$ÊÊLs×/7G?F.È2Ç# ú7YºÇLÒ `!R¬ !`€$X!` ¶gÕgÕW•=XyoRcÒ‡=ç=p»‘ÂîÄã‘yHåtÃCxÐÝ mˆÖÙ Y YÊi@°P¶A@ T €(’©E©( QD QEDQ @%X %–LlÊ’ˆ²ˆˆ%"€&½ºeçß§v.(Šƒ2¬X[)ŽÍyÙÑq½1`T(¤•%€ d€!`° `ПIÓ7'nF*ÊA”ÊAR$*B¤2b2c ® rb-Àe1c@$%RÉ’1™ sb]s`×6ÈÔÚ4¶—LÜ3xÑ7z]xÑ7£C|47Õç›Ñ¢tvùâ雯†á¡ºF¦Økl’àÌ`ʘª"ˆ*"±1\!±¬lkÆÆ±µ¤ni†ö˜ohÚíg%±,´ce1°“(AQ`”E‘iŠˆ¢hèæ—^ÝyâÂEA² ËS(N”»Ê–@€%‚ÉX bˆ[Iô›óÃÓÃjbÊI‹(clXÊa&CCÅD™E“"c2±ŒÊ.*"‰2‘I”"‰2Š•¨ÅDYv¯:ï9úvv5N¬Î&]‡n©S¯™fÌMº‹[J$ÊT²e (‚€ˆ%(‹ˆ°YP°,„Ë-jÝuç –EÄc`•RXEU TK(Šc2„[òvñJ˹أ8(PKŠtÜn¥±e€Ae€…€€RÀ XDX;üþí>¯ ˜z|øÛ+DÅiЉŽBL†,‹ŒÈIiŒÊd1e 2†6ÂL¤FQqQ‹(FPŠŒm†,†*\U¨Å”1ëæê&Îã/3Ñä÷hÓÏÕˆº‹£_¡çÇnYÃMΚôlÐlß¿ˆÃ«w‡V{#Úq T#T°€d•,)IQ(@ À%€ d…b3ÆÊËo>Ý6¢Æ5X¬À*L„QQ@,¤™ JNCÎÅÎÙ‹Y‰@,rÂÎŒ±ËR‹€% D, €@ 'gU}–ãêóã2TÇ2b¢L‹‹(bȘ©qd1e YCCPÆØEQ&Cª1Q&E’¢(’‰(Å”–J$£cb°«†íAŽR3ÆC¿‹6ênç’Ù{ù`•,Y¬T¨ÅqÅ•"€ ¨R¢(‚"Â,ÀA"‘ ¸Ùf5l®†9iŠÄ…$ÊDQ cB(•LfEŠ1ó}?t³,(¬¥(Fxe›óמ¥K` € KB’`…%Á:4n¯µ•êóâÊY Y —cCc]Èc3LÆ1„ÌkfL&c ™p›!ŒÎÌÆ † ‘ŒÎÌäbÊ.,†38a3ÌáŒÎKƒ!ŒÎÌáŒÉÌ‹ŒÈc2F3!Œ¢JY*$´ÁRÉa%K&PÆY ,¶QE‘*T@ AXX€ II`ˆX•»…˜mÕ¶º*é%&+k`QchÅÅDQ&@S‚Tºx{¸1wc–8¡@d‰@˱³nÍ[l @ °B Kˆ± Â¢È l×™÷cŸ¯ÏЬM?/{ø|Þ¾{ú‡Ë¥ú—˨|¸ú‹òôú‡ËÃê_,>¥òðú—ÊÛ>¥ò£ê_,>¥òÃê_*>©ò£êŸ*>©ò£êrùAõO•TùQõO•§Ô>^Rùaõå‡Ô>]Pùqõ—‡Ô¾X}CåÇÓ¾`}<ù’ý+æ‡ÒÏœDùÑô3çÑôÀôð‡»<:{sÅ‘íO/³+lù?n<{qË9l XÐ *(‹€À¨Š€‚ÀX* ‚ ¶ ‚¡j ‚ ¨-ÄdÄl˜Š‚ØŒ˜Œeq«•Ç#+ŽQ’RØ-‚¥ © €cU!bYdS ð Tƒ~Ú,\ÝU¾š‘UbÈcTÆØ%¤) ÅD)BÆ3(phߣ–¶cflYA‹B¦ý¬¢€K @ €BÀ(Š ,ûŽžNÏW mk?Žì8õó9tÅõÜ à2²âˤävsšßUáLº‘Û•ž{1„Ì`Ì`Ì`ÏiÏ6K0›¶Á­°`ΚÛ!ƒ:jlÛ¶Á­5¶ mƒ[4`Ì`Ⳇ,†,¢ÆPÅÆÑ˜2QJ¤Q*‹,[*Üñ±’RÜiPRH* ÄT…H–IUŒ2bªÄ™1´J mÑ»Ee×ÉÛ[Õ¼â¢(J1´EDdY(Š1d1e ma%G™¯)ÇV¥”` •boѺ² @€ %TRE/Ùz>o§éá†K¼üž®®^]<ü³ËžþóTÎ:2µÕÍ¿Ì]¾W­óÉë{º0^¯3Ûù¤êìãë2Çg—±óÞÑñ½úüú÷y|ÜO{ƒ­}ï™úO—g™ºÛ¡¼hoV†òho‹¥¼hoÑ¡¼š‹¥¼ho††ñÎÞ47 ã¼ho'D] èÐÞ47ŽvøinF–᥸º[†–äjm›F»²Ë…ÎÆ1…Í33LÆ áƒ(`ÊÌå`ʲ†,¥c3‰(c–9 °ÙWFýÎþ.íMÓ9©ŒÎV,†6ÈL†,„Qi‹!‹(@±F6Œe‘ãÜsã¨%d( a[´oJ,X, À¡R(Š_­õü_oÑÃtÏÍðzž_=á»/V_ëË<—®<—¬<—­O#/V¼~±<™ìK|‡®O"úÃÉzÔò¸ò¸ò¸òž²¼‡®<™ë%ë"úÃÉzÃÉzÉ<—¬<™ë—ÉzÔñÞ¸ò/¬<‡®|}çÇÐ_Bùáô7çGÑ>tŸDùÑôOG>t}çѾp}çUôoœDùÑôWçÑ_œGѾp}çÑß›FùÁôwæÇÒ>l}$ùÁôoœ/Ѿp}çÑÏDùÑôOGоt}ùáô/žBùáô3çÇÐ>x} ç‡Ð>|}ç’ýçÇÐO{ï{Ï/¼ðiî<1î<ç/•Ó/O_Îo>Ÿó¿§ðõŸ_Ÿ†¯©‡›N¯cÌà92êºÏ#¬¼“°r:Ç$ìnÈr;°q»°q»¶nÁÄìnÄqÎØ¼nÁÇ;aÆìGì/¶NÁÆë“²KÈë“®®¨rαÈê‘Ë:áË:áË:‡,ê³®/+©Žªpiß§7rªz£g‘ÓÍÕ›êösuõÃå˜Ú1e 2²„Q±bÓ aI’1”c3†(cà{ÿ=ÏPsØ€!U âm² ‚ Q(…P¤«R¨¢*ÔQDüëô=â«x|oÙ|‰ž½ûf±žÄÖ|{ë%ëÓÊÇ×KÖ§öõéä=qåcëÓǾ¸ò½¯"ú£ÊzÉ<©ë%ê-ë%êäy×\õ¡åeéäy/TyOZ]ôéå½Aå½XyoV[ÕUõ–õ)å=EyoTyOTyOUSÕTõ‡’õ‹ä½aä½aäÏ\y^G’õ‡“=qäO\¾;×CÖ‡“=qäO\y×Dö!㽈y؇=™<öaã=˜xÏdx¯f/ŒöG=¯8ù~nŽ~=:Tô8»,ò{x»åõ{9ººó“!ŠŒYCCœ1d1Q&CEÅd’d1Rã2†8g‰‡Î}ÎóÞRÎz ¬ € –&ÛŽJP, P !T²‚Ш «BSôÏþ÷XÞ³x|§Ö|½YÏ×5ô×ä õïbøÈ}£âÉö—â¡öψo~ }¼ø…}»áéöωk>.ÇÙOŽcȯÇäG×O”YʪŸ.>¢|ÀúkóéŸ2¥|ÐúWͤŸ7O¢|èú,~~C>~žóÁ‡¼ðG»‰Üxjö§Œ]ä[,zs͇¥<Ñè8vd=<|èz3Ïó€½Ú¹†ZÖ:e•³~»gŸèùþ”zÝ::z㲄e YBL†4"ŒV˜Z1™ÈÅD ŒËåùïÀå¼å˜ÐdPâmË Š‚`©@*‚¥, ¢ª‚‘V”hTÇî~íu;åo›æ¾›çk‹)‚îxS:÷¯€=ùà“Þx%÷§„=éáD÷ž÷ž÷ž÷ž÷§…yà£Þž_yàzxD÷^÷'†=dž=ljmárx…öÞ"=¹âmânx£ÙxÐögeãØžD]äZy#ÔycÒy°ôžj=ç—½À;œ#¶qÈíœe뜣¦s£|Ò6ÍCdÀg1EAPT…€(…A@"[ŽFj®ŒsƺutrÙËêù~¹êtißÓÌ¥‘D™Iqd1Q&PŠ"Â,"ØÁ”$£ ”\qÏàðý¯žóÆÌh å,QE‚XlÏ^À @¢‚¬•jUEZ•‘+*•RZ¬>Çä>²ÏZe7Íà}‰^“ëù“^MÛyoK}9ÝžôwHæt«™Ò9¯E9$ætÓ™Ò9C•Ô9JåuWPåu[Ô9]C•Õc‘×gPåugU9c‘Ö9'f'+¨r:Ç#¯•ÕNGXäuYÖ9c‘Ö8Ýc‘Ö^GPåu#‘Ô9]C‘Ô9]#•ÔŽWL9ÝžtÅçtvñ¢tHÐÞ4MðÒÜ4¶-ÃKt56âUñÏ;¸=?*°öCÀæû/ñz~ÃÌš}ž ò¥ã¯Ñúnlß™êëõ«æ^ÇZ|…ýÊ—ä7ýO¿gäž·Ëéž_›õ²>W´§ÆzžÀøüº³é8Ý£…Ü8o`âvŽ'`âvŽ'l8¨âv9Û9Ø^9Ø8ˆãuŽ7\9eãuŽIÔ9påpäuHæPætÅæPåuCšuC›W4º%e¿ ñ¯OÅ÷¼gµãûg¡·úsÅ”"Â)qe 2„Q&C²„•XI”1™EÂeÏìãáÒ²ÂP fdb°nÓ¸RÀRP*Š@¥P¤U©’Š ¶-S%¢’{Þµ_I2šæó}. ðõnÀÃé<››ìíñ»ñwåâÛ>ŸÃÈôù¸®§ÖüŽR_WŸŒz}ÿ;cè¼ÿ6×g¥àG£Ç§µ§ËFýœOwç7ŽÝ¾u—~Î5ž~ÑêðjKìùùqà ÏC‹ ›«fVÍ{Qztc!¯r´·w—Ü2Óº.Îm‚LÉçÞû©ç½ç½ç=ç=y¯Hy¯F¯˜ô‡šô‡˜ôÑæOPysÕ‡—=RùSÖ‡’õ¡ä½hy/Xy^CׇõÇŽö1<‰ì{Æ{0ñç²eLùôϘ‡Ô>VTù,O¯|~+ö/ŒÄûYñ8Ÿs>ï‰÷Óà!÷Øüû¼~qÄÅû\~0}–?>¾|‚>·“/ÓøúM[ùº³¬ñÏ Ooæ>Ÿæ#¿Þñ=ÝNܦZÆ3(I”"ÅJ1Q%¨“(E‘I”&9bIbá2×-à,9cMºFXÒÀnÕ´ÉZ2ÄT * *Ò̉’’­Š´²Œ–ÅQVÊRwqtŸiŽxk õŸ5×Ë–›;tiŽÙÈ:¦»tˆ ƒ\Ú5]ƒ]Ê$dÅYI ,&9£[`ÒÜ®yÐŽyÓgL9îòènF¦Øk¹Ã!XÃ9®Z!Ðæ‡SŽWmà‘èÏ?ÒyƒÒy˜ž¤òñ—ÖÇÉÄõž<=||‰/¯<ˆzÓÊ‹êÏ.G©›Ny’=7˜=,|辌ó‡¡8!ß8QÝ^ÙÇ#±Æ:ç êœÃ¦síߟ6âuòõÕÇ<5=¯˜ú™OÜñ½Ýgª®³Š‰(“(@²d1TID™bI”1Ç(¸hßË2—‡T¢€ƒ»“Òó"eŽU™Mºó6AjPYPª”-,´-”¥ªTU«e²ÙÊQT›µf}Ö¼ðÖÓå¶á³S >/5ôÏ™/ÒÏ›§ÑOžA<ïO {sŧ°ñǯ–=9æÕô' ;g윴ésí#sM6µXØÀ¹E!Hɲ¦¶Á©¶š᡼sN˜scÖ9'Z¹päuŽIØ8݃Ø8݈ãv7`ãuŽ7b8݅䃒v9Ø8çd9päuÑÔ9]HåtÙÒ9o@ætgL^wD47Žvôho6eM}œ}¶0Ï Ooæ~›æ£×÷<_oS¦šÌQŠŒVbÉ”‰(ЬV Q%Ê0™b¸ðwù±óÙ៱(A({^?½àË2‹ ‘LðÈÞ­@¢l(UU*زŒ¥lUlZ-ZUEd@}Ö%‹*|ÕÉgÍkíÇ;å½IyU9]jåuÓ’õŽKÖ®KÖ9ƒ’ôÓ–õS’õXåuS’õdr:©Êë·¦œ®­§¾ÇêW-ê‘Íz©Èé§,ëÚp:iÈë¶qÎÑÄì§ ²œS¹\.ÑÄíNÑÄp»iÂíG´q;G´p»RñNìN)Ú8]°â£ŠvÉÛ'l8çd8ÝãuŽIÖ^GT9g\9gXäuC•ÔŽ]>‡ ¼Ýü=ÒÌrYë|çÐx1í{>G³¼ïU˜¬"‰(“,VL¤%„Q%e (˜ØLrÆ\|_Ä—ÆÏǤ`X}/ÎýOÊæØº’‚,GRe¹)J ¢‚Õ)eZJ¨«K2EZe2&KJ¨ÉER2ÄûLõmIVÏž¹eg‡–ÎÉ®í=üœï7öÛ‹}n£åwýf³å3úüOŒìÛíê|¿WÐã/…}iù½Ñãòûø¯‹ÓêùÖrmï«8=$Ó«Ð/Íß«S¯G«É‹ÇÕ§f¦Ï;¯núã^]^|¼Ó?LîÓFÞ|úß³šô]NWNG-è§$êYÌê/#¬œŽ±ÈëޱÈëŽÈ¼Ž±ÈëG$ìnÂñÎÙs¶SºW î3º3¾òpOBú#ΞŒ<ç£Îz0ó§¥:zCÍž˜ó'¦<»é/ÈúŸ˜Î¹;ø= kðßfïÕó%÷=+×Þ6‹"Â,ÈKL¡` – F2Ãr’ãóÿCóy¾u——H )~¿ä>Ûâp Å€¸Ycžó)E"¨-b¨«dÊQU-*­)• …TµFS*“(}nþ^¹%Ç*ñ-–yüþ©·MfÌñ©¿“hÃÚò¦vÎ.¼­rìÝNWXäv£Íê¹qÎÖœnÑÄí§ í©Ã—`ãvŽÚ8ï`ã½v9õvŽG]®;ÖŽ;Ö9ƒ.¥r^¹™u+‘×N;Ö9c‘×N7`ãvGZ9c•Ô9]Uyv8݃‰ÙGV'3vâY†&ÖŒN™Ë…v¸1=æ`zÓÇÄöž'½<ûÀüðbûïè?ôo›Ö{Ÿ!ÓÃ.®î.ìÝ}œõÍÇÓÏï©æú›ÎÉIeL¡%a1Ë’â³åþ§ä±y©Ï¢X,Á/Ûü7ÝüFn«Í€ rÅzvjݼʶJ U Ê,ªªT¥E™Rª+*YÊd2”d¶+!ŽxŸMÙÃÝ -y÷òØöüOf\‰5ÃmÑ+¡Í#­Ä;ž|¯Fù˜ž«ÈÆ=—Š=§‰OmâmâmâmáÃÞž>…ó±>ŽüÈúgÌCê'ËÓé±ù´}??kÝžjxªö'“VyƒÒÃÏøñ¬yÆé¬f× ­PÜч6'V<Ðèš$»¦¨mà M˜ã Š*1.36¬ óDމ oš­lÇYlc6mÅÑ:G.=ƒ‹Fž^½w#ççÑd|Óéêü¾¿_Ë•ÛÇÙXz>wªžV £ÝôüïGyÌ$X%‚  aIF2ÈÆX³ã¾¿ãq¨9í(Æ‚Q¹Rý¯Çý‡Êbùã¦@(KŠônçêÞq¥ŠU²Š¶,´ÊÕ²Ñ-Z¥-RÙKfUj£)X{þ™éIJכÃßÁgG>zÉqÝx›ÚC˜tÞ\£¦ódteÌ:o8è¼ôÜÓMtÊJE¦ ”Òè§3®œNúyÓÓ§•}\{YG„÷éó“é²>Yõƒä¯Ö“}`ùWÔ—}@ù‹ôÃæoÒÓæŸJ>j}1~fý*>rýùûï îì"úÃʾ˜ósïG]Šä˦nÑ®ç YC˜ÖÌk›!„Ù &Èa6C ²M×6 s`ÖÌkl§“ñŸkñSW³“®\=o#Ú< ÜÝrû¾‡~ñI(’Â,T¢, $¢,$Ë’Éa§ã¾·äyêŒlQ,:¿ióGàâøc¦@J% 2ÄÛ×Åݹ[&QV–-Rª*ØÊQU-–­™ ¦B©jÙhefAG±êøþÌce8|ïOͳ_§ç},¼Yu{¼jË1(,E€¤±DQFP•DÈbÈEÚ"ŒmDQ Y Y Y Y Y Y#C˜Áœ1d0g fcpÁ˜Áœ1™« ˜Á˜Âfmƒ\Ø]mÖØ56 mƒÃø¿´øÅ½\ýëöüOvϘïóý<ëÜîâîÞ,K,@X°‚ÊK .1%/Ê}?ËóÞdÆ€•N”g_cäzþv.:äŰ/Ÿèë1[’­Š´³$•iUAhZYjŒ¦E)jØÊfL¦Eªz>ß…ïFS—Ëõü‹1ú™úŒÜ1Þ—EÙ…FTÁ»\cvìŽfÕjlÛäit`jo×X:™rº)Í–{ŽiÙ3yñèÊ´kìÐjn¶ho¦œz3ŽFv°lY®æ0gMmƒc[`Á˜Ámƒ[`ÖØ5¶ mƒ[`ÖÍX3†,‹‹!‹!ŒÌ`ÈbÊd1d1™ Y fpÅÁ˜ÖÌ`Ìkf0gMM²5¶SShù¿Œû?Œ¬ú4nkÀ÷ìù?cÈö3¯o¯—«|è"Å„ÄK¥€€cQË ŽX«ä|ßÐx·”³: •N}/V¬>2W\ˆTÑó}-L–tÉC)lR©’,¢¨ÊZe*2™ ¥«fC)’[2L‹T(ëúú9pJš|gÆ©ôÿ3ôÙ»µç‡;·,2Í· j¹TÇ,l»p²Ö(Ɉ°­šrËJAR‘†c]Ìšîuq™WbÍmƒ[hÖØ5¶#[`×s33. Æ Æ Æ Æ Æ ƒ[1„Ø5¶ mƒ[`ÔÚ³TÝ M£ShÕ6«[`ÔÚMM¥ÔÛ lñ"`lhÖu8°=—ë¼Mg¾ùÍgÓ¾W/Œõ|º6ëØaïøñò¾×íËìõstï `– a¥€‚X%‚X%„–§Ïx¾·“Ë¥ÐLðÙ„××eŽ|ï¶k툡(€CÏô56«®e•-”R­•- Jd%2iY ¥³4e)r”¹L‚¿MóO›ªËf>'»á¦ù¯¨Æ²m¼î¦Ñ­±\ªàÌbɲÚ"ˆ¢(ŠX€+^&ç>)ÔãÆ»§ =—‰ë<|+Ûx8Ÿ@ùè}çIôO‡Ñ¾kúwËb}[ä±>½ñÑ>ÉñxŸk>.WÙãñÔúÜ~P}N?3¤ÇçGÐaàoz˜yÊô''£<ÌOS/ZzØù×­‡—‰éëóñ;ðá‡f¸h†ìub»Z¡ºh‡DçÅz§,ާ.'c‹ïÕÇ#)¿I×–7s{Á÷¤ùsÆöåõú4oÞÁ,T°@€€K¢K 2†9J¿)çvrqèY(6êß-kKöyaŸ;ò½ür•IH€)o^§\ϸ”ª¢ÙQTU±e-QV™JZ£)’2Z¶d[2-™ °úŸ–ú|ÜR¥ð½Ï¶ú>^É~Ÿ’býf?/O¤Çç²=ì|;^Î>E=L|êvcÍ’nšÕ±¨mi‰»jÎa ñŽX˜²Y­™pd ®¸liÄ鼘§n^|¯Jy˜Çªò0¯fx¸žÞ>,=¹âCÛxp÷qñ!í¼1îcâ%öÞ"=—‹jx¥ö'‘_)¦>hô1áKÛ‡$:µé†Ì1…Ä1Ç8a3¦Ñ¦n&òóΔsN‘Í:¡Ìé‡3¤s: Ìé‡<éΘb˲Ʀ¿{Ãö¤ùïsÅ÷Õß§v°€%X %€‘e‚%XL¡~/FÍ|z 7Æl“_U–ó¿7åû~'\…€%€«—¢½sÇ·8[&AlµT•EQKV¨³*e2FXæ*—)K”¥ªRÃé~ké%–$ËÆö|zÇ'“^•ù¹/Òß™LùôÓæ‡ÒOC‚=ÙâSØÇɧ§ž;ñã©Òç¦é¬l˜Ñ-1›)¥¸hǨr:éÄí§¶œ7¶œ.ñÀô½gï§žô¼p»©Àï§îG í«ÄíWµNÑÅ;¡ÄíNÁÆìs°q;ì‹Ç;í‰Äës²nÈqºÇ®³®/$뎡Èê‡4ê‡3¦®‘Ìé~ú3®ÙeWÏì#Ýñ=ÓÒÛ¯n¤”’QÉ` P– Qˆ\h˜ç¥~)/©DXEè–4¥ûL°Ëò>é~k¦CP¢mו{g~p¨ª,Ê–d(–ËL‚Õ«TUKY ¥³"ÙÊQ”±>‹çýés|¯W̳è°ó=.;0ô4úؾ^Μ_+~Õž{³n˜ðõz™|ÿ£Ñ¬âÃÐÈó÷z\ËÇ¯ÖæO;nÌ÷9óÊŽž}ù¼méMÃKu4· -ãKpÓw -ÃMÜ4·SCxÒÝMpÒÞ47ÓÑ }9ÝÐŽwHæ½çHåuS‘Ö9C–uŽIØ8çl8çl8çh⣊wC…Ý+‰Ú8]ÐáwpàwâqNÜNGV‰³I±Ï¬ì×ÇÆ¾_/O>5׎XÙßÏ×Ãûž/¹]ùáË $¢JÀ¢,$ÊR¼¯[Ä—æ²Ç.=%mä5%úëçoÄ}ÏÄî`7š”J â{4ïïÎÉT¦Z‹)lÈR­T¥-RÙ‘lÈe2³!”¥²Ã×ò}Iz„^î*ÑÏ»7öêfÜr§7f5Mö™à—K}³Ò9§Pæ½4ç»éÎé×¢œ× ho¦†ñ¡¾šžï[¬h»†–᩸i»F¦È`˰†Ë£¥É‰Üóð=7•]ä]ä]ããÕñ1=׉ôSç1>–|Ä>Ÿ™‹ô¸|ä>‹÷õøjöuyCÒÄvN,Nùç`zXù˜žžn'£¯ƒî×ɉÓ<7c¡¦ˆ»ÜðèsIz\Øzùò0ÊŽ¬2ÆÏSÌõ|ˆÝîx¾æ§fXçf+ˆ¢‹Š"¢,$¢L¡~{è¾c:ñl¼º€Ú.Ö)~¯)y³ù®ù}O=gLÔaqËÔêãîíÏo3 µj”¶eC"d¨ª2dL¥.R–©lÈY‘l¥«Òó½ {fXÅãì峋<6WªæÙ›¶óéǂw¼é^›Ì£Ìòá꼑ë_ËÅÔñ³<‚zÓË”óÇv<”èÇU2ÆÓ‚ã ¦0ÊHLr•TÄÉŽ&ÖœW¡ËÛçÃÒ¾V¯‘Œ{øñ}‰ãÄö'ŠûÇ‘ìO¼ñâúøù2=Yåâz˜yÐôqóÇf<ƒ§^œM¸b."ã2ñÜ4ÍÃDÞ9çL9çLŽyÒ^gHåtŽyÓwF1”ÊWF9cg¯âû¾w{^?³©×”¶ $X¥DT@I”"Ã/’úÿŠÆ¸,¼ú‚„mÏkÒæGØŽwgÎ}…©âÊ阠Q‡w¥åúý¹êdéœr fC)ihµBÔU-™ ¦DÊd2 TµJ™°íâë—ÓÃf¹:4W·^u–bo¼ÐëˇÒ¾f¬ò1Obxد´ñ%{“Ã’{¾ìð¡ïO '½<({ÓÁìðÇ·Ž=||ºz8ðÜyGN:FɈ²Ó ²Ö™¼sΫnÁÆíNáÂﺜºËÁ{‡ íNÑÄí§ ´q;)Å;aÆì‘Ç;aÆì‡ì‡®“²W®ޏrΡÈêލrθrºG,ê®’óc×WHætßOw·Ÿ_2:࡟sçþ‹¶43Ǧ"ª™ÉL‰”Èd©TR™e)lÈP¹KcÙÕ·T];p¯3µ×…«¯#’ö;ÛN'u8¯nUÃ{‡í©Å{G]£ŠöŽ;×NKÖ9oUŽKÕNWU9]Dæ½4ætÕætŽwHætÓ™Ò9$çtŽwB¹ï@çtSšôwE—Ð9ÝÐ9ÝžtŽg@ætÙÓyÐ9ç@æ#štÞtCžtWL9ç19çL®iÓgL^gL9§T9§T9§T9§PäuUääõüèò̱­¸e{ß7ô¿7—­ìy>¾óÑe À "ˆ¥Šˆ¢(Š$ÊDø»øí>+)q©E)M¹á¢^„/ÕYy®Xl>³Ù”@}7Ìý/Lç†Ý}ùÅ ¢²I’“ µjÙJRÙ”,È™ãª2™)KfBË溳l«<•TôxöÇG›†.îï;Øðö«mÕlíÕÌ—-üöÍí]^‡-³FÜ•-F%5Mֵ㺚[éÎè÷xÒÞ4·SKpÒÞ47Óžï[†›¶š[†›º›a®åŒànsâu9!Úá•Þó¡é<¼OYäb{/ÝxC>vCÏCßÃÃο$zxù²Îü8±;0å‡Fa· pË ‰Ž½¸®¬7âiÇ¢íÐÒÛ M’\&pÆe ,ŒqËd¾ÏÏû¾!ìúžg§¬îXDT @(`DQ*žGÇýWÊóéFt* ãeÏžS"ý`ælÃ3ä¹{øzæ,  ô_=ïo=º÷êôr’…Q‘V…2J*Œ¥-”¶X¶R”¶R”¶QHöõlך²ÙæN®zßÑ«¶9îúsÝãEÝMpÓvC `c‰µ£ªò·ì¼#¹Â;¯ŸEçCÒy£ÑžxïœTêÇFFxÚc6C vSt5MË4MãžtÙÒ90ÐÝM7`ÁÄe¸‚XIa1¸‰qÜiŠD–V8Ø’\I$¸¬—’â¬l!‚@˜åŒ©qâH‘>ƒV帽EôýNíg %…DEQ T%DÊd|ßÍ}ÏòëU5)#q¯v6].‘ôéyÖxd|ÿ•íxÝr ‹)=¿ØÔö5oÕéã„´•i[)l¥¡l¥ª,ÈP¶RÙb¨e2@=Œ2ÇbË«háÛÒ94åuYÕNGXå½#½ZsÌch ‚ °¨,@@@@€’âX‚ —‘)‰1²Y‹ã%—²]8ôe¸öÓƒIdõ)äãëÃÈÇ׋äO[Éž´#f/õ08sߨ¹óaÙyx×GÍj_WÅÝÔjõñôu3è™%€@b•A ñÎ>;ÆôüÎ}hšHmÕ´Ç~%ØÌ}HçYá™å|ÿÓ|ÏLØj,ôüÞêú]=}\5Ìñj”YÊR”UE e-”¶X¥cK” §«4ôâà, ¨*P‚¥*R*¢Â ¨Š ©B¢2¦¹ºšÇ<èíðÓ6ãK‰f8™Ì16ÞlÛçk=g‹îeóØI>gê1ù|O©Ãæaô¸|è÷ç„=¼|aëáåÓ»_0Ù®ÓN6¸°ô)åãëSÇžÕ<[ìíSžͯûTñ^ÕgÚhèÓëá®e*UdPUÈYE™ ¥,*¢ª¬¥¡J_cÆË/K¹K¥eФd1ge×7TÑw -°ÖË­x¦ç>5ÒäÄípbz9yXÍð±>‚üÞ'Ó>cê-©Ãæiô˜|õ=ì|J{8ù9ž;0ç¦x­a†úrãÛO>zCÌz”ò¯ª<§¬<«êÓʾ¥<«êSÊËÓ§—}:yLy¯Hy·ÑsÑ{Ї¼p;‡ íNØq»!Éz‡+¤s:aÎÞ4]ÃKpÓvÃShÖÙ ŒŒ-Ú H @" ¨Z" )­?×–<º¬ª”M˜ô¯7D§c}-—e)—Åý¯Çîj•¼Ê `²ŸkŽ9z¸kÇÇ>µ, wóæ¸nÏ™:ÓN_7Ye2ùo©ùÍOJÇÔëù¢ý¿ DZ¯Ê²ú:ø‡N­inbÊDˆ¹1g¨fÀfÀg€»ë|ÿÒó¿"û?–·’¾¡.¦^o£ÅgÍYzÁ ‚À°=Ÿ£ùoªíËF9áÛ,ÈYRÜrRÙV‚‚ÙKq±lÊ–ã‘l¢Ë-²–㲕)An4¶ q¥°d‚ÜiR•¸Ò ·T(XPTEAR1I ¤Œ2a “VDäÖwÏ3\¾¼ñ5GÐO›Ö}>?+¨úùñº—íçÂ뼟÷Ø|2ýÞ¿‰‹öš¾AW¯æôXx/³‡O8½¸r#£wc®FÆ¡¶èFæ…osí{@ÞÐ7Í0ß4—v8DÙ11JµJ°« ô¼ÉißðÿCÏZÞÀóåeϾ;` B îû‡ûž¸Ñ¯fùÀ2Ä™\il\irÆ™ ·[.XÕÉ“$¥¸Ó+-Æ–ãL®3cW$\FW›•ÄeqÜ)•Äd‚ÜFH* q1†ÆœN‡&³ÐyzãÙx:Ï¢|Æ¥úÇÇkµ|.¨ûùùæµýͰҰüÛý#_ç(ý WÁI~çWÅÓëµ|°ú]>_o_KW^½z$nÇ\65%ÚÔ6µCuÐ7´ ÍÝ5CkPÙ0101$2T„ ,ÁPRÀAPT¤E€H-Ù¨{ï‹õã4 ²Ã#ã±ß£¶¨?»øO¹éŒ0Û«¿ö¯O^~Ôðp_¡|Ô§Ëäñ>½ñ¸¯Ùãñ¨û >I/ÕÏ•Q‡ÍXú|û˜xÈõðòÇ£‡ :°Ð6ã­.R qEBå ɀ̀Í3`\Ø ˜Œ¤‰2cJ€  @PJ•AŨ* € ,PUŠ  e%‚ËÀRT*PI@QH¤”RP ”°”¸m—·‡ÓÇžüy×ÏÓ úë…å«q†ykÈñ¼¯gÆë‘,¨* `݆#&0̀̀Í3`3`3˜—&#&" ¨* €`X,Š l„²‚K@P@,U‚À`‚¡@U€H € ‚¥-1e‘­ºÆ‡NG#³#…ß’ùÏG…ègcÖÍ|g½œ|ö?Q’ü§Vk;®Ü%™L ¹sï|WÒÕÛÏÇxqö[<‡kyö,™e Ë-y>ÐüÿL‘¨²‘b(¤ %€(%€  X `•*€J!*ˆPPAHT€°€®,ò57XÐéÈävÓ…èÕó^¥<·«cɾ¶g}z¾N^¥<¬½1ç^øreÑ#FË ×+¢ò`w81='•‰ëãäÃÖžHõñò‡¥öž}>/Ñ<ŒyïÙž0ö'=l|ºr|ÇÒüÏn{¬Ôª<:#ÚÎsyûnçÛhd:l»çÊ >kê~[yƒpYH %@¡@P@ HPJ"„¤Š @@ Xʘ6SSxÐé±ÊìÈáwf¾sÒÈó¥<·©”yoZ¯•}HyÙz‡.ÉÍåÕ–x-Pè¼Ø®,C/.YërÏCÙ‡(èÃPÎbJ‚± °Ûs.6Cf9 yú9Éëy=ëaæÌëÑÇgdäD/)©š[HÊã°Ç»/_žý-盿Ëîa©â=5œ£§ S/˜úœÜçYÓ+ÊYaPP(€”J€ؤ (E¦,é­¶š›r9Ý·ªËÇ{)Å{‡ î§ ¶œw°reÒ—žï†«°c–0Ø× îhu^!Üóâz/2›Ì‡§<ÑèÏ=]øqÇëÇ”tM6ã€Ïô¸ÇÍÞì£Ï˜íé¼&ý BTUPPAPT ¤eL¹°km‘ƒ1ƒ)Yå®GFbôMnµñØa7%Ó:aÎê§=ö¼§«SÉËÕ^^ŽG—{yOGÛù=¼÷öº2Ýæí£,6F,‡¯G)e3ð=ïS€tÊÊ@L±‘aA‘J ¢$²€¢…FTÁ5¶SSu4:;¢œÎ¡Ìê§-éÙo.Û®eÂ嘛Z†Û¢xtÞHv8‡cŒvN1Ù8Õ×9IÓ9ÇF:FÙ®™01APT°1ÈSËËúßÅÇìÆú;lÓÁ²V Æ Æ ÄeŒ\µSc]3c‘J±²šæúrÎêpcéä¾SÕÍ<Œ½AæeèIx¯l9rè›!2ÇÙuboÏ‘“±Æ;''výGÏÄí…‚À°zþG¥z3›[ëqâw<ükÓy2ÏcÚðòѬË.òö¼S7–ïç.ͳV:ÝZVyú×ÕçáYÝç%…DQ*‹ PTˆÉ‰3˜Ò€Ê˜Í•uMôçtÓ–vGe8ï]^KÓN\º$i»F(X†yiÜã}æ.hu9GMå3œtMsHÝŽºe ¨‹LY mDP)fPE"‰@°…"ˆ°, %‚Ê"„¢(–R,(ˆ°J¶±=ïM2Ä.Ý{HÁCK±#(&BkÚ9òƒbR¡}Òr¹@± ¥§ÌË;` `d7ÉeÁqaq¬n$ɈɈÉ*R2³M´ÒÞ4:)ÌéîŠs^¡¼i»aƒ4c,†wQvµ Í174Í#kQvÍcd¦L)”…¨L‰¤ DQ @%€ @€¤ )¤X)Ud1e BPL©…Èa6 &p˜ç WvS&ÛtC}æ.aÑ4SkU3aJ€°ÁžÙ®™©}µœ´¸ÐXäx\ßCäuÏ+f6bÊšÛ)©´jËe5Ý©u¶ ŒY@‚L¢,–åpµt̀ͮ™0ÌIn4°Uˆ"Ó•“ d ¢-\vnqÛ¬Áq²¨Š€ € R’‰@€ˆ¢P(€(”%”Љ@ÀªŠ#+Ì•‹"Āͮ&Û¦ï0éœÃ¦sÓtÖ3˜Òˉq¢c5·àklV&VÀIsÀFƒ¡…2b2H/µ¨òžÆóÁÃêó¢ùúÔÊ&l¼ÆrÞlFLFYaÏ\:òtÎ)ÑÍÕÌcP• ‹(E¦+nc€ ±æ­mƒ[dŒY m¦3!†;0³+ŽduYy]<¥LÑkÌc–½¢Á7òS±Æ^z‰–Ô¡™b((B(”Z "€¢(”"ÓB(Š"âV#&0Îê†ëÎ:\£®rŽ©ÍMóPÛ0†lE”`ÌklÀ—*`Ìbݬ–Œrº‹toC#Îwn-èî<…W>Ìbzéõ—ÃËÙè“Þðj ÇOO9¿Úð=»wº^ç'G‹¿ ׫i4ìÓµ(YPÛêxÔ÷5øÈõbßKƒ Œ3†(Ojœö¡|óMCY@Ì€ë €ÖTÌ22;x€Ža„Ø—q›àé4Ûd T5î"ZP‰A@P”((T @Æ€ BJ ‚…    –„ ¡9Cw¨b9w“  äÜ'»™5ž$cÔIãq9ÊÝ胔—(cJ@çÜ%‚åDÕfÑ(P(€OÿÄ3!01 23@P"#A`p$4CBDEÿÚÿYÞÝ7·MíÃ{tÞÜ7Õêò›å7Æù õöÙ½´o,›»îÁ¹°n,¬“¶=%”K©µ¨ÚÖmk6µ›ZÍ­fÖᶸm®k†Þá·¸mî{†Þá‚á‚á†á†³f*Ìuê1ÔB¢ AH©"¤Te"£(Ê2Œ£(Ê2Œ£/÷Šº©ñì­E6é§ày†Ad‘©#IHÒF’4¤…$)!I HRB’˜é1Òc¤ÇIŽ“ÐÆ†41¡ hcC11˜È @$H‘"Da†a¿¥T'Q>͹*"'÷÷øZ„騟 íRÔÿ’UÝ:‰ðÖ‘êùz)’‹C©Bª”’×Lj¢Ü“ˆª¸ ©ZxSijJí­ ]T•UUBU5WUB*¢Õ]UTµ®T©ÕÖº–¬÷>Màžý{ ÕO…³çÿAx'¿«·Y>×;€ ¼ß×Û¬Ÿ oΞ_ðáx žú¾ÝdøZ<Ôù| ¨‚ê­!»¶ní›»FîÑ»´n­«Fêɺ²nl››&æÉ¹²nl››&æÉ¸²n,›‹&âɞɞɞɞɞɞɚɚɚњіііђْْْْÙ;dí“¶N‚t •¨%I*G¤zG¤t9ŽG#—ô?áx'¿¸'Y>žôyxܸ–鮺®*¨êII)%$¤”’’RJH‘"D‰$H‘"D‰$H‘"D‰$H㎄tAÐtAÐtAÐtCèrC è:‡!ÐtqÇqÇqÇqÔqÔuGQÔuGQÔuqÇQ×?vžþàUáS½¿'Mosøþ˜Œÿ¼SßÜîdøk^N7=OãûâðA=ýÎéÖO†³äãwÕ§ûêðA=ýÎéÖO†Óú|oúÔwàŸLH_ÐWjŽ6må¹zÍVn"*ª}.ã^±rÍE«y.]³U«·ìá«£MºëþÄêà‚{úû§Y>MéñÔúÖüÿÉ]ni!n Q¦Òéïi,ÜK›5ÓnÞ—[õJ­äÑ¢n~¡]{ª©KÖ.¦‚›¶íÓ¥×}Mmf¹¡²º¨ý>íËm=oZÒݰºM5)©·E»öto[ûV £é÷««íZƒ÷èo"iuÆ£OUŠØn 7a†a†a†ƒ Á†n 0à 0à àa¸7ǯ;ÕÜBŸWtë'Ãi=.:¯Zß|ìSwIsKŸKbŴ׬_ÕZ¦Š®hoÕ©Ô¥ÍNµt׊Uiªº´:’榊®j)Ñ¥ûúœš­jé®—5–Òõ+ ·uu6ëÓÑzÚhi»¥}et]Ô#QÍ TX¦ÕW.k¨´•-UÔà 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 7ܯ‚Žõw)ߨŸ)£ôøêýJ<Õ'ïˆÄH‘‰#(Ž‹~åw«ˆÊDe"De"D‰$H‘"D‰$H‘"D‰$H‘"¤H‘"D‰$H‘"D‰"D‰$H‘"D‰ˆ‘"D‰$H‘"DˆÃ 0à 0à 0à 0à 0à 0à 0à 0¢ø(ïWr‘=òöù]“޳ÎÕ?ëŒÆ@Æ@ŒÆ@Æc11˜Ìf3ŒŒÆc c @ @ @32Ìd111˜Ìf3ŒÆc1˜Ìf3Œ2 @ @ @ \FEðQÞ®å"{êü¿+¡òñÖwBâÇRÄ @‰H"D‰ @‰"D D$ @‰ @ @ @ @ @ @ @ @ _T’‰ÆØ½ÊD÷×<¿+¡íÇYØH\¡*ÕiÍæ¤ÞjMæ¤ÞjMæ¤ÞjMæ¤ÞjMæ¤ÞjMæ¤ÞjMî¤ÞjMî¨Þjî¨Þjæ¨Þjæ¬ÞjÍÞ¬ÞjÍæ¬ÞjÍæ¬ÞjÍæ¬ÞjÍæ¬ÞjÍæ¬ÞjÍæ¬Ýë Þ°Ýë Þ°Ýë Þ°Ýë Þ°Ýë Þ°Ýë Þ°Ýë Þ°Ýë Þ°ÝëMÞ´ÝëMÞ´ÝëMÞ´ÝkÖ¸ÜëθÜëÍμÜkÍÆ¼Ïõ?Ô ÿP3}DÍõ7ÔLßQ2ýDËõ/ÔŒ¿R2}HÉõ#'ÔŒŸR2}HŸÔIýDŸÔIýDŸÔIýD—Ô ý@ŸÔ ëÉkÉkÉk‰ë‰ëIëIëIë ë k jÉêÉê‰ê‰ê‰ê‰êIêIêIêIj j j j[ÊŸ<±xR'¾¹åö ðºüužN ]t™oooon™oooooooooo.™///.“ºdºdºdºNá’é;¤î“ºNé;¤î“ºNá;„î¸Ná;„î¸Ná;„î¸Ná;¤î“¸Né;¤îºNé;¤®’ºJà÷¸=Áîp{‡æ~gæ~gæ~gæ~gæ5CT5CT5C(Õ £(Ê2‘Q”eFQ†a†a†a†n 0ÝàœmùTB=õÞÞÁ>Cçã«ô¸-Å2Ôd¨ÉQ’£-FZŒµj2Ôd¨ËQ–£-FZŒµj2Ôe¨ËQ–£-FZŒµj2Ôe¨ËQ–£-FZŒµj2Ôe¨ËQ–£-FZŒµj2Ôd¨ÉQ’£%FJŒ•*2Td¨ÉQ’£%FJŒ•*2Td¨È¤Ôš“RjMI©%$¤”’’RD‡qÇqÇ„N4y(ß^ùm«ÇUè—;/÷tãO¦!@žúðžÁ>GëqÔú%ß/ºa¼ 7àÜÀà Á†ð·Q¾U8'þB ï¯wO`Ÿ ¤õøßô‹¾ŸQ¼,0܃ 7a†a†a†a†a†a†a†a†a†a†a†a†a†a†àS‚q_DB=õß2{ø]7­Æï¦\ôK4Û[› ÑoI¦¹g¡Z­ý=î[±UWµšzl]±enÜÖmó&CU bÕw×E¢E]=uM+M-Û´é´K–ÖÝz.㵬Ôém%ÛÖô)^£k¢¼X£L‹~Æ‚ÉjÆ‘4Õét×lÓL•tÚ+ ªÒcUÓßDÒYËzõŒZ‹Ô&jì] M=ê…µZUVžõà 0ÆÂª­-+J°Ã Da†a†a†a†a†a†aˆŒ0à 0à 0à 0à 0à 0à 7o 0ÞàœkôŠJ÷×|Éìálú¼jò¡_ ˜ÓóÐ};×TÐQwM6¡4ôî~¦Ÿ·IM6,T«UZoü?N娿M[Ÿ©ÿWôÏ=7tVåKvíÏùtŸLõ,^Ò­ÖuÑR»_þ¯¨w´¶“CsUf›vy\×йٴõê.¦±?V¿Q]Uk[þ½5Û—ÊïWkIR®T®šÖ”'ôòíZ8%z®½ t¶õÖÕg 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0Âxnúe%{ëždö ð¶üþô(NLY¾–ìé«ÃrâÎæšö “X¸j]ÝÝuÇV-^Çj‡¥w´®Uz¼Êš{–ÍLh¬¥W53]±ul×mVŠó×uÕ ëzêÝ2U‚#k.SJÝ®ííFª»we^KºŠîîf¯Yz¤ªºª·¸ºU­¼©ˆ‘Ï{ 0à 0à 0à 0à 0à 0à 0à 0à 1†"0ÄFaˆŒ0ÄFaˆŒ1$H‘"D‰$H‘"GÅÊPR'¾«¿Ê§óÿÆÂ=0 @)U+UŒŒÆ@ŒŠ¡ŒÆ@ŒÈÈÈåœr @ @ @ @ @ @Æ@ c1 @ c1 @Æ@Z9xu<()÷ËÙ~];q¯ÏO§¤G¢$H D$H‘"D‰H‘"@‰$H‘"D‰<£Î$H‘"D‰Q"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘ @‰$/×MºWËÅ;ê{”ûêü¿/oÓãwÕµÍtµ¥ªØ‰$H‘"D‰$HŒ0à ‡#ñ‘è%A* Û'hÉdÉdÉdÍ`Ë`Í`Í`ϧ7sq§7cs¦7:cu¦7Zcu¦7Zcw¦7šcy¥7šSy¥7ºc{¦7Úc}¦7Úc}¦7úc§>á§>á`û…ƒî6¸Ù>ãdû•“îV¹Ú>çlû³îvϹÐ}Ò“îˆ}Ѻt>è§Üê>çY÷+§Ü/›ýA¿Ô›íY½Ö½q»×y¸ú¸ú‰Ÿê&o©¾¤eú‘“êFO©>¢Oê$þ¢KêµäµÃë‡Ö¬V>¤}IÿAûÏÜ~Óó?!”cõÒURÔ¼hóê|È[)÷×<Ÿ/gÑãÕ*Çy1_¤† …óãÓà fÌ5À`CoI·¤ÛÐmè6öͽ³ohÛÚ6öLLL   6L6L6 V V V V v v v v vX!`…‚Ø#`‚6F°5¬dk'ê?IúÔ~¡í@ôI*I¡2fE2)’£-F[†k¦{¦{¦{æâù¸ÔA¸Ôõ}Ašù–ù’ù;ä¯xýÇíè×Ü#Y ÈVB¢ AH D‰# 0à ÑN6½MO e>ú÷“åôþ‡G¬0à ƒ!Èär?ñ?ð?ðèØöǶ=±íl•²VÉ[%h•²VÉÛ'l²vÉÛ'A;fKfKfJ ”(2Pd ÉA’ƒ%JL”™)2RdC"ȆD2¡”Êe2™L¦S)”̦e3)™LÕª3Tf¨ÍQš³5fjÌÕ™«3Vf¬ËY–£%FJ‰ÔMI)%$8ãî“WQê!l§ß_òü¾“Ðã©õP¹\E¸dC!Èd2 †C!•LŠdS"™ȦE2Td¨ÉQ’¢u¨Dê'Q:‰ÔN¢u¨•Dª%Q*‰TJ¢U¨z‡QêGS™ÌçýøN:WQê!l§ßj;|¾Ðãªõ)/yº¬0ÜXa†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†olÞ5)ïÃMê^õ)-”ûíO³O…Ðú\u^jKþq†a†a†a†a†a†a†a†a†a†a†a†a†a†a†a†‰#"0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 7…JŠxé|÷}JKb{íO—Ðy8ê„/§ía†a†‹Œ0à - J°ÃÒµ*Ò´«qa¸°Ã 0¶+ÄÃYªša†M-KEËXîXÓ-ã /vÆ4bš(Z[ƒ[tÓJØL!S0´UIµÿ™†"D‰ˆ‘"D‰$H‘"D‰$H‘"D‰$F"0à 0à 0à 0à 0à 0ÅEED5£dž›Ifå¥e¹¥­íé5ë¼\T¦í6.&ªßã­Z*³©YËQÎõ´KºZ¬â£Qÿ«%4ê*ªªh¥)½§[v­U]ÛuÕa) ½U8Õ,­êë¡’ªv‰]½²\¶õµWÕËUܵ*+¡54ܵR\ªÎ +M5%ë4ZŠRÕW.VˆI0Z©mÕMÝ=5%ÄKwVuÑ\m[ª)•_=/EĦ‹µÌ¢¥¦›•L¹VJìµ»l[½ .\ªâînÜ®Šî^®å6ïWn–a†a†a†a†b#"D‰$H‘"D‰$H @ @ ^F+ïG7¦½è(ß^ó§IDøÍ©ÇSéš3èµWr¤¦ìtõJ¡´ÔzÆTe"1$H‘"D‰ˆ‘"2‘‰$H‘"dºÑ"Dˆ´‹I"SÊ"Ò-"RD‰ @‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰ @ @ @Õ'å_z8Øô?š ÷×<ÞÍ>EëqÔzF (‘"D‰$H‘"D‰$H‘"D‰$H‘"Daˆ‘"D‰$y±$F"D‰†a†ˆÃ D‰$H‘ˆÄHŒ1†"Db$FˆÄF"D‰$H‘"D‰'#ñ‚VÉÚ2Y2Ø3iÌúsq¦7:cs¦7ZSw¦*Öi‘¥»R«­mÿå-” ïWµ]ýš|&×ãÒ/ù)ò°Ã 0à 0à 0à 0à 0à 7ð0à 0à 0Ç#ô’ ³%£%£5“5“=ƒq`ÜiÍΜÝiÍÞœÝéÍæœÞéÍíƒ}`ßY7öO¸Y>áhû³îVϹP}ʃîtŸr>ä§Üj>átßß7Ú“y¬7zÓs¯3ý@Íõ/ÔŒŸR'õ"_R%õõãëGÖõŸôŸ¼k£VEH!Ž“³“œÇ¦1éˆiˆéFÒ¦?å?æ?æN>œ•‚VIÚ2[+¹U|)âŸù e{Ú¼«ßÙ§Âi½uãwÓåï×`ßZ7Öõ³}A¾ ßÒoé7è}Àß›õ7õë†úé¼¼nõëRn5f}a—Zd××××®\ÚØÔ6¤mAÄ.¬ÇQˆÂa¤ÁI‚ƒ³ £ ££££“’ˆY!d¢6HÙÐÖ†¶~³ð?ñ &¦Z̵™®™î™î™¯¯o/¼~Óö‘¸c¬ÇYŠ£˜ÔÆ@(2 ƒR5'â~à~³õŸ¬ýcÛØöǶ=Ð=¨%I*IRI !"Cp§·å¤BÙ@žöç‘z79\øëªñ¹äá’¦üÄä8ã’¨dî.™.“ºNá;„®¬zÏÈjˆ© ¦51 DŠ ‡#‘ÈüOÀz z PJÙ+d­’¶JÙ;dí“¶d è2Rd¤È†D2!Êe2™LÆc1LêgS=Fâ£qQ¸¨ÜTn+3Ög¨ÍQ•LŠd&H’AÇ‘"D‡QÔuGQÔiO {p»ÿ‘ eú÷“ĉÆÿ­ÒO„·ê/ìœ*®šLªeS*™”Φu3©MÁ¸7àÜ›“rn æs:ÍIš“-&ZL´™PʆS)”ÊfS*™j2Ôd¨ÉQ’£%fJÉÖN²u’¬•Dª%XõPõQÌçņa†a†a†a†a†a†a†a†a†a†a†a†a†o<)íÃSËN…²}ÉÑÕúÝ$øJ|ËàAySr¥àà 0à 0à 0à 0à 0à 0à 0à 0à 0à Da†‰$H‘"D‰$H‘"D‰$H‘"DˆÃ 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 1OíÃYÊÝ%²Ÿ}©òôu‰û:Ið‰ßøðUéÖœâDb$H‘"D‰$H‘a†a†a†a†a†a†a†a†‰# 1$H‘"D‰$H‘"D‰ˆŒ0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 'íÃ]Ú’ÙO¾Ôöèëzið©åâ¾eôÕa†a†a†a†ƒ 0à DŠŒ0ÃpaŠ­WAЍ%ª––a¸0à SjåHà 0Ãa†"1$H‘"D‰$H‘"D‰ˆ‘a†a†a†a†a†a†a†a†bžÜSͯ)-‰ïµ]ú:ß'I>O~ÿ(0š;TÓwGÔi±ÖÆ+‚Z¸¢ÑU*–.©Ž§ªÍÊMyQ-Ô«V–õ)N–íIµ¸•êi…›ÔV¦Æé·©(M=KJèÖ”µ¦[‰sKÔÛ§Vë[: zW[š{inݩֺKE55QvÅ×WÛÅ·¢Ý+mmé¨M½©§Or›6­­›ôSMÊ-%I‚‚‹T)ŠÉMKEUQMÊa†a†a†a†a†a†a†a†ˆÃ"Db$F"0à 0à 0à 0à 0à 0Ü“·~¦¿ÍIlO}©ótu~M>ߥÆç©Il¡?-j~IMØÝ¼´\…9ô÷«¸-Ê­é•&d»¹¡²ÕRUnÒ$®çËnY¬f•ÅTÓ^ôõ|ëtE¦ÝÔÔFtEh³¨çy)ÉbõÓÕFKNÖj·MWºj)TÛZd®t¡j´Åyi©.c®‹kDi¦›Š•S54Û¦Š*·ŽíS©†bÝP*Z«a†a†a†a†a†a†a†b#"D‰$H‘"@‰ @ @*§ñÿòøYõµÞz b{íOŸ †£Ñé§ÂYôxÝõ-ù¬ S}crêÖWqk©oW=Íb×Rз+T¦¥j®.Z®\­Ëtæë]Ê“›sQ]Nj%ËtœåÌa†"1†ea†"0ˆ¨*×Q$HŒD‰ˆŒDa†‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰}RŠÈøi½}o©AlO}ÏФ¸o¦Ÿ §ôxÞõ-ù¬'å$H‘"D‰$H‘"DˆÄH‘a†a†a†a†a†a†a†a†a†a†‹ 0Ýa†‡!é'lËdÏ`ÜiÍΘÝiÞ˜ÞiMî˜ßiþ˜û†œûŽœû•ƒîVO¹Ú>çA÷:O¹q¨ßÞ7ºƒw«7ZÓq¯3ý@Íõ/Ô šó&´Tç]rTïÃKëêýZ b{ë¾~…'ñÓ§á4Þþz<ÚY†a†a†a†a¸·]†n„©'A’ٚɚɞɸ²nl›êÉ»²o,›ËFöÙ½ ßRoé7èoÍú›Û†òñºÔQŸXeÖ™5ÄõÄõĵ¤µƒêúOÞ5â5SŠƒ³“ƒœÇ¦!§#§NÎÎ=ì’´Nƒ%&T3)ž£qpÜÞ77ÍÆ Ï¨3j š‚WÏÜ~Áª"¤T‰$xS߆“ÖÕzÖËb{ßâ盡O Ñ«éSðšOOÿ5=ê{WRí¥L–̶LÖLöMŃs`ÜØ76MÕ“uhÝÛ7vÍåòƒ{I½¤Þ›ÓyQ»¸noA›RdÕ“ÖÖ¬VÔÒ~ñ¯¸B£‰ 4h1[1Û1Ú!l…¢ˆÚ#lkc[ƒñ?䉩’£%f[†K¤îpýƒ\#Y ˆ)# ‡#ñ?ð?èèÙ+dí™----™)&„‡¨ýƒ^!¨1êLZ³°Á¬6úÃm¬6º³iª6Z“c|Ø]>ßYöú·Ÿn>ßIöú …£adØØ/i± S߆ÔÔzÖËb{ÕòÕß¡O þ¯JŸ„Òyxßî%\–Ý““£’ˆY#h¢6†¶5³ð?‘"Jd¬ÉY’²u’¬zÏÈe"¤T†9ŽCÒ=#ÐJ‚T •è'A’ƒ-Z Ô™PÊdRu’¸~áµjHêˆjÌz³¬Ã«0jͶ¤Úß6w•ÃcQ°S`lØÒlh66Í•£ehÙY6V žœÚiÍ®œÛi͵ƒƒ “²A½æ«ÒRžü4}ïz¶Ê÷µù¢œ5^·JŸ„Ñø/ñ•d ÉA’“-ZLÔ™©3!”ÉQ:É]?yûÆÔ‘ÔÕÕµ&IƒPmï›k¦Öá´¬Ú)´6†Ñ ¥&ÒƒkAµ¶m­kFÚɶ°mì˜,˜l˜­˜í ?Ñu~Š”wá£ísÔ¶Qï¯z}á¬OÙÒ§á4øßì\U(Ó[DÁdÃhÅhÇl…PdþÊÞo£QG'§_žÙG¾Ôz}=o~•=þIçãÊ÷þã®ôª(ã¦ôÍl£ßj¼=j~=$øM/¨¼oyõ=Ó £Qˆ/‰¿¦kü•w£üßͲŸ}«òôõ~Iø=7ª½ø]òùˆ¹¼ œêÜÕ9°ÅHEDASœx"rD"DTa;p~ȤH‘" ß>Þa†a¨ykïG|´‰ÞÙO¾ÕôSŽ£Ñé§o‚±ê/~<…ΊÇñüÿ<*;%#sð?Ïò(‚ ÁDíðMÐa†a†a†n 0à 7a†ƒ 0à 0à 0à 7¨©Wz8§-Ê}ö¯¿A8ÝG·Ó§·ÁZõ*ï¿)sÊ“ƒŽ8üWƒ‰ýUЕ&Kf[FâÁºÓÍ)½Ò›ý!÷!÷-!÷M1÷]9÷kÝí õG.WRª”váW-%²Ÿ}«óôŠöéÑÛà­ùêïÁ{=:<¬0à 0Ü[å Rd¶e´g²n,9»Ó›ËòÁ¾°oìp°}ÆÉ÷+gÜ©>âoë7·Íæ¨Ýk θϯ3ëÌÚã.´É«'©%xzø5$m°GN6˜ÿ˜ÿœ{¬“´d ËI˜Î¦{†{¦{âܼsáOn¹ii-”ûíO©Ð¤~5r«¥o·ÁSæ«ð\ômú~ÿ‘*IÐe¶f´g²n,››&îÉ»´o-ÛFúÙ¿ ß¡¾7•›»ÆçPgÕµf]a“XOTKR> ýÇì#Q&;f;F;$,‘²5‘­¬ýgà=$™’£5Ã-Ó-ã%ñk¤k!Q DŠ IøúÏÖ=±í’¶JÙ* ÐN“"Èd2)•Lµ*2TN¢j<§· O¡Il§ß_õ:ŠœoúÝ+×·§FšúEЕ$è2Û3Z7M̓u`ÝY7vMå£{lÞÛ7Ôä7¦ö³wtÜß7“6¨Ë«2jÉê‡Ô¼ýÃ\ ¦3&: vÌvˆ[#lkc[Ùø‰ÈrJN²u’¸=Ãó¢*DˆÈr?ñ‘è‚T •¨'A:IÒdC"PÊf3ÔΦzŒõê3ÔfS!™2d‰RJ:Ž£¨ês9û*)íÃ[éÐ[ß]óôSŽ¥?wJ߃ÿóÇùAqÖa¶b´b´c´BÑ Dm lkcP~'"Dê'Y:ǬüÏÈj†Q†áÈä~#Ò=¨%A*IÒJ’hL™Êe3Ìæs9œÎf3L¦S)•LŠd¨ÉQ’¢u“¬•dª¡ê9œø0à 0à 0à 0à 0à 0Ã"Db$HŒ0à 0à 0à 0Å<· j b{Õí_› ˆž g©Ò·ßàÓÓâ¾n $é'I4&LÈd2™ŒÆs9¸CpgC:Ð̆d3ŒÊf¨ËQ–³%fJÌ•“¬d«£™Ìa†a†a†a†"D‰$H‘"D‰$H‘"D‰$H‘a†a†a†a†a†a†a†a†a†§‚p§Í¯ïAlO{_‘{ô„«Ž·¿Jß›àéô¸ÕæBíÃ%dî¬zÈçÁ†a†aˆŒ0à 1ˆ‘"D‰$H‘"D‰$H‘a†a†a†aˆŒ0à 0à DˆÃ 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à àµêk¼ÖËb{ÛÞš÷è3£/g—¥Gƒ·éq¯Î…Ä"D‰$H‘"D‰$HŒ0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à Á†a†a†a†a†’x,zÚß=²Øž÷Qéô]‡~:¿O¥Oƒ³éq¹æ¤Q†a†a†a†a†a†âà 0à 0à 0à 0à 0à 0à 0à 0à 7n,0à 0à 0à 0à 0à 0à 0à 0à 0à D‰$H‘ž_Ç7¯¬õ-” ïu^ŸEQÎiÇRŸ§¤‡ñðV=>7|Ô÷þX£LôÜÓEMb’æž)‚Íë8ÖÕ¬•%j…±\ê³r”¢ËÙ·EMEŠ«M½sÚ©FÒå¨ EJb¸b­ð])•ª¡jéU©†n 0Þ&a¸0à 0à 0à 0à án,0à 0à 0à 0à DaˆŒDaˆ‘"D‰$H‘"D‰$H‘"D­Ÿá;ðÒzú¯RÙ@ž÷WäèÒ7ÜítéíðZ'¾j{§B= m­ÝÄdEªíºõºi4ÝèµZ\E{–æ”Yu³f•¦•¥k¶ì¶ýj7ÙDª¤'XõQJRëpa†a†é0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0à D‰†9I;f[&kãNn´æïNotæúÁ^¡n¥l'~?[Sê[(Þëz)ÁU¸ÕÍ:ty~ MÛÞô÷ÿêÅ7Y+¸µ%u-c«D©V¥NFJËJ”еT#¡Ìæ0à 0à 0à 0à 0à 0à 0à 0à 0à 0à 0Ãu›ÀèJ‚v̶ŒÖLöMÍ“u`ÞX7Ö ý“lß¡½SwxÜêMƬͬ2ëLšÂz²Z“÷Äi#hk#X?ç?@ö Z'A’“)š¡nV¼¿ ©Õ¶QﵫϢ• à«•]+~_‚Óx/÷a†a†a†a†a†a†a†a†Ø=$¨'A’ٖٚўٸ¶nm›«fîƒyA»Ct¦âášù—RdÕÕÕ©?èñÈé1Û!h…’6°~ƒôh•D2©šášñ–ù;ãÞ?`ÕPÊ0È5'à~à~Ð=¨%I4&†C!‘LŠd¨JÅBžü4~kÞ­²Ÿ}¬óô)’Ÿäý½+]¾ MßÑÿÐà 0à 0à Óä=$©'A’Ù–ÙšÙšÙžƒsAº ÝÐnÜ©ž³5ã%òz‚Z‘õï?p×Tc1Òc …²ÈÚ#hkC[?é$ML•“¸JáûÌj†Q¸r?ñèÙ+d¨'A:L”™ʆc1™LÕª3TdS"“RjII(ê:ާ3™ÌçÖ^Ô÷á£.z–Ê}ö¯ÔèRKéõzV~Oæ^ü.ö*E)¿B¦kF{fâÙ¹¶n¨7tºMѸ¨ÍtË|ž –¤}AÿAûƺF² c1Òc …²ÈÛ#lkcP~#¡"u¬•gæ~C(ËÇ‘ÈüG¤z PJ’T“¤šÊe3Œæu3™Œ¦S!ÈdRjMI)*‡¨zŽg3™Ìa†a†a†a†a†a†a†a†a†íO'’¿RÙO¾Ôú EA*ðjÓöt¬wø+uïÂï”ETDm lüÄrJN¢uYùÎ~G!é‘é%I*I!4&†D2LÆc9œÎf3Œ¦S*™ÉQ’¢u“¬•CÔ~G3˜Ã 0à 0Ã"D‰$H‘"D‰$H‘"Da†a†a†a†a†a†a†a†¯µEO­ÒÓyWà“Óã]'ÿ›>^!é%I: ”­™í™í™é3”ËY’á;¤¯xýÃ]#p…F3‰ T˜è1ÐB’(7ôËcU¨¶…õ rþ} E¤DñjýN–”_‚§ÒðDT¬ï¾=ó÷xÃF#©1Pb ÇAŽ‚4ŒŸÕN ¤.o}A´Ô›=A²¼&ˆÿšÉ{VµœÔ¦’ŠJSÞ¡­_ÛÐNÒ©<:Îý-'™~ ß§ýYÐtàÕ¸bº`¼mï›kÆÖ鵬ڛZM½£˜†ÿ‰ hLº#q£7Zc{lߟq¨û…ÓxÞß7wÍã=ã-ÒWR4ÐRžÿWêôE¤æ‚/‡VŸKKê/ÁZòÿBtáÌD+1Ü1\1Va¬Â¦&;dlóÒ4fm!¹Ò›ËúƒîÜ+7÷Mõãy|Ý_7Ì׌—I\?3òH‘ @Ìf23ŒÆc1)ø G©Ðn”o«Òéi×ö/ÁY^u'?e¢5¬…D‚ „h?Xö é̺s6œÜiÍÕ£yA½7Êok7— ÝãsxÏxÍxÉt•Ãó¢*AH c1˜Ìf3ŒÆc1˜Ìf3ŒŒ @ D‰$Føâ•pæƒøoú]+^uø$V)T­ݲ‘R5¨‚R$PjOÀ{Dì™l¬ì››Fî“xoÝÖnâéšé–é’á+ƒÖsH @ŒÆ@ @ @$H‘"D‰$H‘"DˆÃ 0à 0à 7á×Ë_›¡Žäio|èéSßàÒ¥¥i¹MBÐ2øFR*F¢R$F¤ü´d²e°g°nl«&îƒx†ôÞTo+7wMÍÓ=Ó5Ó%Âw¬ü†Qˆ‘"D‰ @"D‰$H‘"D‰$H‘"Db# 0à 0à 0à ó÷=5ïÑd^©ì§Ê¿—«¤Ý©½7Êo«7· íãwxÜÞ3Ý2Ý'pzŽc(ÄH‘"D‰$‘"D‰$H‘"D‰$H‘"Da†a†a†a†a¸0ßÕuŠ÷ñ £°ï㯕}+^šü# H´ @"D‰$H‘"Db$Fa†a†a†n-ý³V¿§£JŠŽ2 ž+þ¯KOé|+ 0ÄH‘a†a†a†ÿÖú]qÑ|zŸW¥¥ô×ü¹Ð&[f{FæÉº´o-›Ú òã}Y¾º]Ô×q:íÕY͸¨ÂUâÕùúZ?*ÿŽº¤ÉAšÙ¸´n­»FòÙ½¤ßå7µ›Ë¦î鹺f¸N²U§3™Ìa†àÈr9Ž] {Úzn%tWm{‹O‹WÓÑ÷_ð·BT“ ËlÏhÜÚ7Vå³yA½Czo”ÞÖn¦âéšé’àõŸÕ ¤T‰# È2‰Èä:8ãŽ?²ÓÖ””ÖWE7öž«bUâÕù:Z?Q¼9$'I–ƒ=£shÝZ7–í³}A¾C|oª7· ÝÓuxÜ^2Þ't{‡æ2Œ¤H‘B4IøŸ‰ÈtqÉ$HãŽ8ÿjû\9)wFëáÔú]-/¬¿Ù Òd ÍlÜZ7VMå“}hßÛ>áI÷î oë7×Måãu|ÏxËtÁë?!Pb(2 IøŸ‰øŽƒ¡"D‰$HqÇqÇù;wV’ÝÔQ+ñ^çk¥ajÿSrHN“-kfæÑ»²ol›û'Ü-q ûŠqSî ýÓ{x]]ãqxÍxÑë?#ŸG#è:8ã’$8ãŽ8ÿÒR¥¤·¨FðÕåé[ó/ô'BT™(3[7MÝ“{`ßÙ>áhûÜû’Ÿq¬û…Ó}xÞ_77Œ×Œ—IV~G> 1Èär9pqÇqÇqÿ²¯~ŠwþãÝ Rd ÍlÜZ7VMå“}dßÛ>ãA÷î*}Âῼo¯›»ææñšé’á*ÇSŸƒ—ÇqÇûÅÏS¢…¾v×Þº¤ÉAšÙ¸´nl›»&òѾ ßRoÍýFúὺo/«Æ{ÊfºNá*Îg> 0É×^qÿ¿(‚§Nÿ«Ò±è/IÐKfkf{FæÑº¶ní›ÊMâÓz¦ò³ypÝÝ77L÷L· ÖJ¡ÔæsâÝ'ÿ§ºÒ-=-O©Òµ«Kv×Zo*7u››†âážéšá’²u§3™Ìæ0à 1Ë¢ÿäŒPŸ¨åTŠ W›£É¿Î\¦—1˜Ð‚ ƒpa™WƒR7Uþ^Ê2‘¨…F51©‰LF#[H`¨À¦ÜÛ¡ `C Òåµ¢«kÎHM ”™Èd2©5Uˆ©Åiñê|ŸãÌ2‘R cS˜ÔÄb1Ɔ:H¡a†àÜyBt™)êQfª1Ù#`ý¬™-¨3¡œÖ¬Ó¡Ot¨ä¢Òvè_ôÿÃXe"¤T‚˜ÔƦ%1ŒHcC&4 „P‰†a”n‡¤$é2ReC2Œªd¨ÉQ:‰(ëãÓ¯ë~†¡±ô-#×U1ÎJ7Žç“ûë £) ¦51©ˆÄb1! hA!"ƒ 0à ÃèJ’t ËI• èg3©ž£=FjÌ•“¨u©MÉ‘£òS²UÛ…›±§9˜ÌfS2™j2TN¢º—£§õE¡ ­‘^‚öþÚÃ) ¤‚˜Ìf3ЂB(EA¸0Ãx9„©'I’“"Pʆc*™j2Td¨D”u)1ý­}ØîR¥]—•=5ñ¢-Kµ¾iôøÕGਊ-¾…]ÿ®0ÊEH© ¤ f2‚B(2 Ñä:BT“BhdC!ʦU2Td¨’’QÔŒr\¤:ºª‰$è8üQ¸sH©"¼)Õ\¥,ÞKˆ3tîyÿ¦°Ã)"¤ DŠAn—!Ðt$„КÈd2RjMI(ê:ìiÒÛªÂYT]U´¢à‡ñîeH©†A©?ñ‘%G^ÆR*% cS˜Ìf3B4IÈåÁǹƊ֊­Ö•Òvé^õlú¼[å cÆ1ŒcÆ1ŒcÆ1ŒcÆ1ŒcÆ1ŒcÆ1ŒcÆ1ŒcÓ>¯ùFÆâ„!B„!B„!B„!B„TEJ•„!B„!B™õx÷Ê2BR¥J•*TB*TB„!B„!B„!\‰õx÷Ê÷óߎf< Œ˜òcócí1ŒcÆ1ŒcÆ1ÓG3Q“é#ÿÇA¨’6‘íúü‰‰é{>Gý¿ÉŠYâ Ÿ?¨öQŒ™éÚcLzòcÆ1ŒcøñÌÅ”||s1m”zÔ!B„!Z|“ÀŽf-´T©R¥J•*T©R¢R¥J•*T©R¥J•QB„! äžs'lã²Æ1ŒcÍSlcÆ1ŒcÆ1äžm̳ƒó£ñÜò³ò²9þrðNS´&1&>ûÉG’x· cÿ¡Œü~„õa–ÆL²ÃVZIŸÀúŽrc |Øß8Üž|z„!B³äŸB„!B„!Bn<“ì" •*TB@ P( ƒ¡Ðèt:;ŒcLcÐòcïA=øôOœÆ>;Ñ÷ã}SÈsGƒÀކó?î"zeùËÉÑ“äÿæ†O¡Œàžüoªy8[út!BˆŒãnõOÈ1ÀǣǩùL9øàaÕ:§ã£? =Éã-hY!B&’ôÑÃ÷rx±ÄB„!B„!B„.g<Û¹=óÆ1ŒcÆ1‡žsëÆ?k¯‡ñœêþŽtN©ßæP¤RVE< {üBþ ÉY+%$¤”(R AX+G·R)+%d¤”’… AH)`¬ £J¤R)+%d¤”(P¡H+`P(ö6ìãÛ‚…"‘H¤¬•’’P¡BV ÁX >3Åd¤”’’P¡B…`¬‚°(|¢Õ?4¤BŠJÉY+­ DüR„!B ètÉŒc†tâß·>éB„!eÐèt:1ŒcÇÜŒz°Ä¼´O­BHZXÆ1ŒcŒxãu#•ä!eµÏ©y1Œ|\Xc)?¥‚±×¾„!ppbññB„!sðËî!B1zt!Bí1úX”>Ôn1Ààe‰œ£Kî<þìÏÕÿÄ01 !0@QAP`a"2qBR¡±p€‘ÿÚ?¬•’²VJȤR)üÉ `¬‚°V ÁX(P¡B… ’²VJÉY+%dR!H§åÈBŠÁX+ ¤‚… (P¡R¥J•’²)ŠE"ùZ R¥J•(T¡B… •*T©QBˆR)ü½lB…Gþ#Ÿýï©R¥JˆB„!B„/‹ÇËcå±òØíT•Ÿ2ðW/rðW/gÁYðV|ŸgÁYð)øŠD!{ü÷x`õíÐ…@ P( @ P(…íÝÇħº}¡GµåÝF¿˺‰åÞÎH¼›É7’o$ÞI¼“xo á¼7†ðÞÃxo á¼7…Ë—.\¹råË—.\¹råË—,X±bņ1Œ}üëÝÆ‘²uøvÝã¦É×… bè-‹bض¯gŽÂuîñÒ6N¢!òÚ¹Oèçÿ9ˆB"$B„!B„!B„!B„!Bì#ÚðÒ6N¤@„!B„!B„!B„!B„!B*T©R¥J•*T©R¥J•*T©<û^lHÆ1ŒcÆ1ŒcÆ2ņX±bÃ,1–,X±bÅ‹,X±bÅ‹,X±bÅ‹,X™{#]‘×;Ì4ÙÑh- ´‚ÐZ Ah- ´‚ÐZ Ah- ´‚Å ±bÅ‹,X±bÃÆ1Œ}œk²:ó§y†›$Ë_}‘×ËNó ’e¯Fvúô¶!B„!B„!B„!BÅÇ#¯žæç%%òeä¬þ¥yÈœÿ‚š•”‘Y´‚0ò)n úÿ¢’RLq˜e~ä¯Ü•æú¨B„!B„!B„!mR; ôï0÷éÙI†}æ>àÆ1ŒcÆ1Œ{}$ŽÃ>ò8-༂Ð\¹råË–,X±bÅ‹,X±bÅ‹,X´–‘Èär1ÈÆ1öž’cØg¯yí!B„!B„!B„!B„!B„!B„!BãÿIa–½äm’yÃ"bt”‚ÐËC- ‰‰- ‰äËAo¡òdäN>e¹Œ‰f²†[èsÿ"áB…µB„!B„L»'ø˜ökì‹'Ô¤ÿe%jWþŒqE~Ê}‘‡ÙO²œÛ7eìÇSì¤äŠAy$RȬzˆ‚±ÞN‡®ÉÒ { ×ÚØÆ1ŒcÆ>6>‹ Æ1Œe‹,XvNcî(B…ô/¡Bé}[yÀàc,X±bÅ‹Æ1†6eèGkws‘bÅ‹-%¤r9ŽG#‘Èäæs9œÎg3Ÿ„!B„!B„!B…¶6eèG^tâ;´!B„!{ „!B„!l’:ùiÅŽç4Ùù/:"ò_ž„Ê”Nr[!ÌD’2™G/SÓûG5#ÉÿY–sç®§ìsÍ@§È§ü˜Âö#dêG_-8°ï+‰X+ã(9H£lcÖ}wÀÆ1ŒcÆ1ŒdÉ'ùviŇwœüüÎg>þt„!B„! ±R)ˆ˜#gú» ¸±×Š;¥³‘Èär9mcÆ1cö,ˆÙëØeÅŽ¼Qðéô#dkØgÅñcÙzí’Ð=Œ±a–‚Ã,Z ȹi#"Å‹ cÆ1ŒcÆ1öÓ³Á†zñG=—®Ù=véØ1ŒcÆ1ŒcÆ1ŒcÆ1ŒcÇÅ;=L{ µêc¯e;sÆ1õ”Š| |ŸrðS"²RJII)%$¡B… (P¤‚T¬@ QÑ|ö¯iÃŽ½”íËSìÝ›¸7pnà¤#ÁHðR ÁXú+Bàå³–×±ŒcÆ1õ„! ‚; ×8c^Ê}8_~…Ù! Š;|tíçß¼‘Ûá§o<HçÉÏÔ Ö’9E¹–ÈŒ¦Çíô~ßDÄÿ~Âö=‰o’:ó§C(Ó±8§bŽÉŒ|û·ÁyÓ¡:v3§3ŸÄ!BØ„!pr9p=°B…Á|´èc¯v3îÈ[…Á|´èF¼_NÊ4÷¯NÏ>„q~?^Ê8/ô[趤d^ s‚徉™«-1ËKF:/ìbÿ®ù÷™õ¿½”mžpVJ²1B䊈¬–Åf1ôP„! Ør×£páü»(ì„! Ù£°Ë^Ž:pãü£²óí¬p81èc vÑÇNײõî\<–‚Ð1ýHþ‡>>~ØýÛÉÏȧȾÅö!B„!B…ÚOÛo1Âàp8Çôsðsðsð~Çì,¼Š|Š|‹ì¯ÙR°V÷ªeþí¸qF`¬ < >ëÅŽ‘ñ§‚ÐZ:XëÅøÿÂãÉhò_ð^ Á¼/ô\¼—’Ò9õ2žcàx¿žîà´y-Kâ^ äÈðo>äø7’o$¾Eò-—‘Ï‘Ïy0É„>ãõöW£Éhò_ð^ äÏ£yôo$¼—’ùŸ#Ÿ'?p¤t½“Ç’Ñä¶>Kâ_yò çѼú7’o$¾Eò->KO‘ÏÁ£N%I|Mæ&ò äØðo~ïѽ“y&ó"ùËÉ|¼–Ÿ#‘üFHÈ|8éñÆ1Áh/£Ž$·|)ŒcÆ1Œæs9íBŒã—pOôÆ1ŒcÇ'3™ÌB„!BêÉX+GYBž{,8àÃÛÆ1Œs·™ÌB„!BÛŸäÇñÄND~Lfb<ûXDÇ:ü<#(çàüS†?¶½‹ì¿&¼Xëð&1Œ{ûüñSÃñL¨/%¤´ŽG&ûCÇì±=L¡Á8ÌOkÅ–‚‘IY+% alœÎb¾¾òWóHî'_‰ÿÄG 123‘¡ !Aq’0BP¢"4@Q`ap±rá#CR‚²Áñbðòs“ €ÑÒâÿÚ?ú³ÀáúA¤JÒD)æȘ¤Ž%qì®ùªÒ£q'n$Sê̆ˆ®$ÎĨüJ¯Äªâª•2C{²"˜Ì0•™’4¦˜”ó)fS\Jn$y+ÎöWl‰ÊˆTB£q*7vâNÜH§ÕÈ©2âNìI݉QÅE''&È™08`wH4•¤ˆH˜”ó)®$ŠJâ;Ø\ ²''B£q'n$é‰2ú·e'\JŠTR¢““‘CÝ%BD$Ì“2E$RU §ŽIÉÐ Ð ‰«Q&'R¢âT\JªUR¡92`E08BT$Bžd„ŠJâ qÀ‰9:¡:bL„~¤Ä‰¸ @$ÊNìJŽÄª¥U*• ЊÒ $Bšó)©MI\AØp&Èœ¨…F“·dÄŠ}J‰$H‘ØRRR$TJ®+;º•ÊåTÈ0;¸i+ SB–e,ÊK‰MÄŽÈ•äÞÀŠà¤ù)PªÒ«q*3vâL„~£D‰e&&"DŽŽ ¡R¤¤¤4EI݉QØ•\UR©>DÉÝÿã‹$H‘"Gèìú!ÿËn$H‘"GÿƒÔ¡80 @RU%RU ¤ @‡Ö( „ @h‡ÿetñ¦üÉÎ ‹ï7-åÄÚoع4'Ðäñ…äLY}‹ïÐç{†¸xÞz/k‹¡òÕ5•KاÌÜ£Q8—+·ˆÓ{Bí}â§Ð„ñ…ä)eö/CxÔß‘h…ëÀ]æá¾û´#›îç|Ç«½ÇØû–|…ÜY¡÷CÑ.Vçô!¬ ›;Ôâ¤AÄw°;Ç{Ž8p"¸\®r&È›"l”›%'ü““““¡:¡:¡:¡:¡:¡;q'n$íÄ1'LIÐ1'Bd&Bd&Bd&Bd"„PŠB$~Ž ›©¿Û @ @ @{$H‘R$H‘"D‰$H‘"D‰$Hün›{¾ª¦Ãù‹õQ6ü…ú¨Ý‡óØkn‰y®×£ÛòØk/ºñX¼ ßhÔSU顬N*-›¢#uší×Þ’êµW—ÐUñVì;ì&›Ý~ªnÞ[%Šþâ\¼K7ZYë¹Å­¢u×sÅloÝ[Ñéèü]OOrëWûÇ%ëèÝqú7Z%묈%‹¬·¯æë_*ˆÝOOuîùMD¹º—¨¶ grûËU¶mú]â[Ù·SÒ¹F¢~ŸY=ãÚÅôD{\Û‰˜Z6öÞÅÞLÃ…÷b„º–¦£¾ßNcì7˜îk¢ÎÎÕ÷\=¶.W*Œ³·UEgÊÎÃ~ªß!¶¶Š­qföÁ™Ÿ¸ÛONèäŠ(´UkÓÙÙH× µ}Î÷ µH6ëÝm§§þÁí[ýG l–›ãqú¤]Îz­È>ÍWÒÖ‡Ük›ú…gýœÈn"EH©2âlûšj~™Ÿæ5œª«ôá6øˆ?ù/fŠk¹8]»èŠø·ßaœ´/òú:¾,¼ö÷Ðá¹~6ÕM•øaû ç£Qër¤®.»v(H*H*H*H*H*H*H*H*H*H*H*H*SN•)§J”Ó¥JiÒ¥4éRšt©M:T¦*Sò©OÊ¥?*”Ó¥J~E)ùT§äRŸ‘JiÐ¥?"”üªSN•)§B”Ó¡JiÐ¥?"”üŠSò)OÈ¥/"”¼ŠRò)OÈ¥/"”¼ŠRò)KÈ¥/"”¼ŠRò)KÈ¥/"”¼ŠRò)KÈ¥/"”¼ŠRò)KÈ¥/"”¼ŠRò¼…?!KÈS^‚šô× ¦½5è)¯AMz kÒIå$^’O)'”—ÊKå%ò’ùHd„¹! †HC$?¢Ñè‡ôCú!ÿüðGðGðMø&ü~ ÿÿ‚Á?à©ø*f…LЩš3B¦hTÌ«šs*æUÍ ¹›í37o]•øaû þZw9P¨ìJŽÄ¨ìJŽÄ¨ìJŽÄ¨ìJ®Ä¨ìJŽÄ¨ìJŽÄ¨ìJĨüJ¯Ä¨üJĨüJĨìJŽÄ¨ìJŽÄ¨üJĨìT¨üT¨ìT¨üT¨üT¨üT¨üT¨üT¨üTØ“¿'~*NüT¨ìTØ©Qø©Qø©Qø©Qø©;ñRwâ¤ïÅIߊ“¿*?'~*NüTø©;ñRwâ¤ïÅIߊ“?&~*LüT™Ø©3±R.ÅH»2.Ì‹ˆ»2.Ìïfw³;ljÄâq8œ|-~w-¾‹ÍÄÄÄÄÄÄÄÄÄÄÄÄäÄÄÄääääääÄêN¤êN¤êN¤êN¤êN¤êN¤êN¤êN¤êN¤êN¤êN¤êN¤êN¤êL¤êN¤ÊL¤ÊN¤ÊL¤ÊL¤ÊL¤ÊEH©"¤TŠ‘"GÄ—á…å°¿m õM9l?C>©·aü´7ù|vŸ 3žÃÿŠèÿ6„KG\Ñ^–®ÕN&«-U[«y¨–ˬZ±î„.Éc­rˆÔ]ÊÛÆ°±±]Ílnè÷j§Ìc,ª‹ÏÛ[oO™©n«©våC÷u©ï¼´¹Ê¬K[;EÖÅŠ¬­-¯î¥È1®‚–¬¿ÐbŠÛOH{P‹¬Š&².ôÝpÛ[TX´ý:­íˆÔ¶_IF­žö:ëdü¢¥íâj«]©­»æƒšÆ9‚,OIŠ‚]få¼ÕV­þâ÷Y¹i-,ÜŽ÷¡r¥Ëðs| |Yœö–‡sM6éòwàÿ(ç+½=kjäMÚ¨~÷½¥šÿУíÞ+–*~§“¿r-/޹ú~J?Ÿû–œf5u¾ãŸtÊ#wþ#Q–/?\ÆÌ¿þ£.àZóOÁúà¢~ê^ÛÇ2ÂÎëË5ù¡MÇéÑýÄÏ»¸Õduâ5Ws\—!nû¯r1.-[j›®,58î¼ý+õo]E«izÿ…xQxïÛ²r?¾Åß÷÷VÅ×ðÿ»Ëì·|ø®¹mnáðJì§/‹7žÕ§Ûó¦ÑŠÕô„u×î뢢®­û…f®þ Y&­×&ñ,’ ÑjÍ[õ„TŠ®°½ÉÄÖwÙXêLj®Õ¾ôѬ°h®á5®¿p×û–ñmSuüÕ³j/¼[âYÞ’¥ÇìݺûïÓª­G]ï,Üî ZMÜOܿҾñ/kw)û»‘n¸TôRÿpË=×4³‡ ›ˆ56’Ír'º?.Ê|0›+̶þ"íÞ‹p«Ø­Ë´½¢é_ƒÝ²Ÿ §-‡ÿ%-¿‚ŽçíI´½Ší'À#¿ÜsØO_gñM‡ó¾ôTfýÞË&BdÄ™¸“·vâTf%Vb/ö¬Å ¬Å ¬ÄªÌJ¬Ä¬ÌJ¬*´ªÒ«J­*´ª…T̪™•S2®JTÉJ™)S%*d¤ù)2àL¸v]`w°;ø~+ð%y#ò$~DȦüŠnȤìŠNĤ¸”—˜£æ(ùŠ>bŠuS‹q(¦eæQneÁJ ‚”¥JU(y¡äR‡‘J>B—¥å)ùJ~RL—$!’ðâGý$ÿé*fÒ¢bÒªbÒ²u!]:õ„ëCÖ[Ö‡¬·¬õ¤ë=i:ÏZN³Ö“©OZN¥=i1SÖS=gò^®Ö/Øo?_³å°ýŸ¢ÿñ6Ù.þwÓÿpõ†ÿîÛÖWoYë êSÖÔ§¬3©OXf*WfevfWfevà¥và¥fô©Y½*VN…+'B•¼ŠVò|…_!Qz Žè*; Ýïé'JZ`„Ö˜!5¦FÓ"6¹Þä{‘ýî(BÓ!iÔ„-:–Ó©?üÚuBKN¯èHþ¯èHþ¯èS_ô)»¯ú—¬¢½eë(ùÔ£æR‚u)A:” Þ§Š”™êöy”,ó(Y`P²À£eÒQ²é(Ùt¬º v]–}IgЄЇw¥ïJN”"(O’ ŠUqUÅWâU~%WâT´ÅJ–˜©=¦*NüT™ø©æwó;Çxâq ¤ @àp8ÀàE‘"D‰$v™Ïá«>[ûlp"„SdÄ™1&n$ÍÄ™¸“·vâNÒv“´ Ð Ð ÿ$ùäM‘6DÙ\®W+ÇŽ8ã¼AÄAÄJâU%RU%RUÄ‘q$\IEÄ“2LÉ3)æI™"bSLI2FæHÜÉZJÒVi`A¸Üîw"˜!Á¦M’dLLNN¤êN¤î'q3‰”™H©"¾߆›±öÑrhR @4pÑô‰$H‘"D‰*D‰"GàÄÀSÆ>û üt?ŸÇ?a|Ö·٥ɬŸ‘èþËWæ~šä¹]ö¿jõ÷Ÿ¨Öb\×±ûŒ³Õ¹Â5`+Å7q¸²ÿÖÿrÓíø-=s?RªÔ”ý",5j£nuÚ×ñ,×W½ÔÿÆæïV íÞáœFضVĽ?ÄXZ+nUr"‰dôw¨s[z¢nB×÷.Ü‘×ï¾ëüa6mðñ‡òØo= þ)¢ÖÏŽüÇ+ÒíÑœUnĺÒ%›¬×qaÍZ_è–¿3]Ò«â&®®¯¼zßyûH·8mö›õ¡À²_ûˆäUŠ å}¢|®g­sšYzw»[xë'KtKFþíË~åYèçk$¶ýÔ?Sé&ôÝ€öß¿Ü#wßÿßô}ÃÜW kÖåiaªíÍQϿѿý‹F>Urª)ûluûÓò5\— ªzü‹HµwˆýG#•Ô·üKþÂÙñÖ¹ëÔknþó[1ï÷–¬ºbÑ.™.,•D[eé/Zök%÷DmÈÒÑ.™.ºVÜk]pë^0Cy«¨Š‚_ºè¶ÿx®¿zÄÕ܉ò.K®ù‹âíÙ´ÀWÆËcîš,ÿŽ‹Ð¹^ª+QW[½Uy›œ¨oU^dÎÇÛnÖø=›/çàKãm…ÑcÉDø’(L˜“7£1B£1B«1*³³1+3³JÍ*¡U ™Ø[Gno •æ¿ 7aÚ,W˜Þ^lÅ ™¸“·£q*3«1*³«q*´ªÒ¢2R¦JO’“.]ÞÀïàAø¼‘äŽ)¸‘q)æSóüÅ/1K2ŠbQLÊ’ŽJPò©CÈ¥!GÈR^‚O*ä‡þ$ÒN˜´¬M=a½hzËzÏZoYëë=q½jzêu©ë©Ôã×·þ£Öòqë9)ë ÒVwAUýGô“Zt¡Lþ÷#û܈Zâ„–˜”ßÔRwQEzÊ+ÖPó)A:”Ý`Ó~Î>¾2͇òжkö?nѤ¯$y#²$qMq)®%<Ê~bšu&$ĦÒFæHÜÊi‚”Ó)ùT¥äRšô/I"ô’®ApC”ãå#þ’|ÚOæB¯ Þrºu•Ó¬®Þ¥+·©JíÅÅfùŠÉæ+& UN•*yT©ä*/A;ºIŸÒ„ÏÁÚdFÓÊy‘ýæ(BÓ%Q#ºŠnë)/YKÌ¥êR‹qqEžb‹3(ÙæQ²À¥gÒS³è$³èBVt¡ÝéC†O’þ ‹‘UJ®*¿£ñ'~*EùüÈ8•IT””Èîâ„[Š7fâNÒv•¨„é™?äŸ"|‰²&\®Tïã¼AÄ•q%R\ÉIIIIP†Óyx öOþKáìç°îZn[•>e+< V}%;>’ŸI#:IYÒ‡w¥(G$'Ru'q;±'v$îÄØ©3±R*q8R¤¤!ÀàE¸‘n$ÍÄ1'Bt'Bt'LÉÿ$Ù)6DÙ\®{¼wˆ8ƒ‰\J¸’®$‹‰&d™”ó)¦$ˆJÒVaùÜÀ‹pBdÁ ò''*)QÅG‰;±&v$\EN: @< ŸÇÀW²?g=…ÓtIP•¤wNá7"väN™¨T*¨¥G¸Ä\EÇxâAH))))))))*BA1"D‰$TŠ‘R*EHüÏâž¼“ÃÓžÒ©wÆk²Ôåámþ> œ¶]ñ£¶SÀS²³ûøƒyl;˜þ^Ö›‹îÝØ&²FŸÁ*wcÙ*µ·Ýð#¶ÌONɼüAœ¶Ìñ](¶ÏºñŠ×^Ç-Â#Ê—sÑMØ sç"¡MM]U¿ÜzLTÑz0Õ»¸½Z^—\£Z·o.kâÅ5R®"ÑîÝè­Ã]zo[…U´i}÷ ‹¯z_pÏJX|Ë-ýè-¯¥îŽ[µE{«p\Šk;!«¿Ub7w¥ÝznïëtpÍfoTÛ¯«gºˆå»ysRíÆûDië ûTMåquÇZÕº«ïø Û þH'…ýÓÄËaÂò] æ1x\1{—ûË6§]ÇT~²]pÕHÀ°U÷ÿ±©Ý-—ŽïÀû‘W˜ËÄÔ€ý{µµxýËÆê­ÛË,ù)ú{ÿïpç,§ê‘Ö‹þ$’ˆ»ÐkWüHYÜ©è–?ÌýÍt¸·ßÝÜ=Þ-ö¨©îºÚªƒQû¸ô®TAì~ëËd¾)¸³»ƒKÑ/K®-5QS˜Är;rp/»†Ê®®ò÷-ÿ(¼ö,ù‰áoåâ Ø]W7XDºäN]t bVˆÍ×^5=Â^öÜ9íRå]Ú&/¿y½Ê"_¸Mð7­ú;Ü…X^qö+•~ [öYàKÙ=>Kâ ±öwÂs!;q*³«1*´ªÒª2R¦JL¸v{¿’¿"GdSv%%Ä£™C2êÿ“ÕòSÕ¼ªz¿•J“DÈáå&o”¬ÜPÖ¶µÖù&Ë~þ¾5÷ØûZs_Š&$íĨÜJ*!Q ɲ&ÈŠàw° ì ^HâE$̧æ)ùŠi‰M _’–JQò©KÊ¥/*”ü¤™øŸø“ùšUN¶•“­XN³ÖS¬õ¦õ)ëI‹YO1ë9)_Ê¥eè*¯A;úIŸ‚Þdy‘-§R?¨¤î²Šõ”<Êz»qSÕÙ™êöe , 6]%;.‚[>”;½(L˜!9QJ*<žÓ&~d^q8èàp8M¯ò¨¾½›¹ø{¶–Üî¸ Ä¨ÜJ*!Q ò&ÈŠàw°;Øq+‰‘q$Ì“2DÄ‘1$BŸä§’”üªSò’/I*ô’®qÈŽm'ó!SÎVN²²u•Ó©JÉŠ•¿%TÁJžU'ò¯I3ºP‹ðC¿‘ßÅ _ÔH™J~e)&*Rn*Rfe;< ,úIÒ„Ò‡w¥ “&üÄïÄ‹ó;ùìÈ8•H2;¸¡â„Íĸ“´¤é™:fO‘6DËÀ‹°;Ç{#Ž(Aq%Ì“2žjRü”% ” ½%é(yJ9!OðIø?©Ãdê'LJ‰Š•2ªfUB®E\йW£‰ÜEç{Y«»eßÄw€/hþ~í†è¹Rô7k!LˆÚdwñBÄ‘ÝD‹ÔSó)O5)72“s)3›0$gIàpÁ“©:“8™ØœN$ IH‘Bd&Bt'BblˆäEp8àq8RÌ—2TÄ•3$LÊy)OÊ¥%é)yJy!&HJ¾Sþê„|ÄÞbtêRtÅIÛ™;s*7¢`Uò•¢àNâw“<‹ñ;Øv$«‰&jSü”¤Ò“0)3›p%L'¶®Í§ñà Ú/$ð÷ì7LN' Aq!™ És%BOÉ&JSò©OÊ¥5é$^’EÁ W" Šq?þˆùˆù‰“fæNÒtÀ0'È©‘:àN¤Î&qbw±8âK™&jH…4)·›p$n©øvmy î~½¢w-„硜OI/RšÛM¸7TÀ‚|aköÏÀ´gßÃ×–ÇßC>7´æ/? g?³øÉ9ì»ùx vŸM…Ñg¦>È»Ið«yìý×ÀS´‡·atYóØBï‹XŸ=”ûø våáíØ^Zü„åñ I1*7«1+7³JÈU'ÈŠàw° âGÜz¼×´]þí–‡at7ù åðH¡:bTn%FâUB¢ ²"¸p8â J¥5(©»ôî=Ys=YpSÕò(”Ó#¹ŠÙâ…{Í<6B|bí‹>~½·ßé*‹Ïažï^ÃYîÕBöºôYê"±oE-¼K—rŸ#U¿Þ+Syz´vä½KD¹9—Á>b72t¼½Vá7ÞŠnBE.ÕÞK™½8·(©ðº‹°ß¿€§lþ]¢xì&‡s]~⢪ ×KýÉsQKÝ*ºñDK¾ãî÷ýÁÉÄr*o¸f¢Àcx–‚Üî#}æå&R*EOH¹©r|&LIÚTiQ ˆNL§‰sw¼»cü«à)Û/‡»a4;ž‹•/.ºäå¦õÑo7¯ÀBd'Bt'Brb'‰%Rš”TõuÁOW\ %4Èîâ„ÌĪÜOXië)™ë?“êW»°*¼žÓ#¿‰bH¸”³R’fRiM˜7 ˜m;ø‹à Ûª|ü9Û ¡Þ!&Bd&&&"q8è†d3$)”—). SȇàŠb„éÔUn%t̯ù+äV\ ŽÀùyßđؔ³RŠZQf6`„­ÃG."㎈æG2d&Bd&&"EN' IP•!ÃLMû6ŸÄ_?]„ö˜¡2!11‰ÇL3%$)ä¥5À¦¸üÍ Ó¢bULÊ©™S"¢àNì™ùìH;LÔ§ù)´¦Ì +pC†H©Äã±&Bd&&&"§‰%%%BT ÒpÑe"¤}©6-yðí׳_ G·îD‰‰ÇD3!™)!MpRšàHKø?ªLIÓ¢b¥D̪˜r*.îÈ‹Žö$\Ô¦…6”Û+p8h‰8ìDŠ!11‰Äâ@””‚Cºp"LL¤ÊEN:`CÄbÛì/?wnœ¼=v·± ’›JmÀ‘¸¦"EH®ÌH‘"DŽÄ ‡"LLEH©ÄlD‰?ÚsÀ·g‡'a8B(L„ÄÄTâqÑlĉ?»ùx öíçÙ§¦Âüd›?uðö¿fž› ¡>0nÊ}üÞÀþÍ< 6BlÇbG`oK¶þ;Jç:ãÐvór$SÑAR"*]¹7ìJ¸Ú·½7üÝ–r_w°?’ö‰ài³÷Ñq©¨/-Å%Bï¹­ïz7\oSr©¬ñ·/ØMÛø®KÕFª ­F Û¸$:ø ¶h!f·q8žñœ7ŒTÒš.=êoø$ÈL„èN„älj*›šk.ÍŸññÄð6ìýôî…âüßùòáî»BúF«„F§ÜKâ…ÎA»· «ï/] ß~‹‹ôn7—xœH¡2!:“8’©"”ÊdˆJ‡$ÄåE'Ru&R>ÀÏ➞¼û4ð6쮘è‰ÄŠ!111‰*’)MJd„¨AÈNT*):“)8èÖü˼¾Âþ~›.ð(‘B$H‘Ñ%RE$$%Bp&Br¡:“)8é1ûŽão°»³_M‹ÇrìâE¡bb: ¤ŠSRš”ÉPàE œœI”Šœt@!ðn½¤=Æ«|íì)˳w„ÞÒD ‡d'*I”㎈¡*O…㦠Hì oÀ¤â™*bw1=+Dûõ=_agf¼¾;މW›°)¸¦¹fA1"ÌIØUi]0+äV\ŠŽ"¸Ì§ù(¡E¸¬“s Hi‰bb>ïao?Œà¸©*’’æpÄ‹IÚTB®ERªäN¸“fRBš`RL d„¤4Ę™I”™H¯Œ;Ÿ°ýû6øÞ#%RR&Brrrr9ŸÔ)&D„¤¤4ĉ12“)2ü¼ý…ý›yühŠBbbrbb: JJJ@†ÄH“)2‘R?//br|¼GvÌ!¦(L„èNNNM¦RBBRRbLLL¤TƯåíÍåàÐ%%!¦$ÄÄÊL¤WèSýÜû6ý;_c>Í>ýýíÙýþ˜L„èN„ÄN$ªJH„¨p.wbº¥ËØ·—fï£ñBd&&"q JJJ‡ )2‘ögŸ2åìÙ»—ÐÈ‘&Bd&&#¢Ó*EH¯ao]ËÒiϳû|u$ÈLLGL IHÓ$H‘"D‰¹tÞÝ¥ìÛñ‡p/ð{mÄ®ˆh‚þš¯/¦ûÍËÙD‰:`@‡Ô‘"GDðúG¨صï.DúK;Ù°&í»¶nT앨Öë§Ê&ª¢5ü߯zû¸ ­Í ш۠·´¹=ßqZåM~'ÑhìÃÛ—ÒD¹ ¨¤ Ȫ'¹vÙhî;Ìu›QQÉ󈺌䨣?³d=+÷-ãÚÍUTüõ6=’9 }}_•㨣uoU¿‰¾Îôà*Ü—/‡ë¿njö sÑT,ÜÄŽåO™Á}÷¶óÑr.ýæª?ÒÙG£½7Žkånÿ°®MÊä¹ycÑu› ·"Ê©¼b÷›Äßf/¢š«ÁFjµTÖMÅÿ=×Ê—ì+ï‚ݵr ©»â‹½ã“uÃÙ«vëÆµ7\Y»Š¦ý»F¢ÃzGsO°×·ŠšÉ:á…¢oNeÈõj¤G]³r.á7Š—îRíuðÿÄ,!1Qaq 0A‘Ñðñ@¡áP±Á`pÿÚ?!"°AR ŠÁn ‚ÿž 2“wâ_S¶}NÙž}™Nú*…ÄøõuÒ®¦˜y¡Úa? è 0w Mw8³ aDlÆÌA‚¬H¤ɤš“DîÏx“Ä–$¨´íHÐdà$\p—r½âWSD¯±;Tw:BwØa{ç¸Ç„}Da}ëyaï “‹rž,yÄ/xÅÖ›äf˜^3;ÆK×£%è<`N»œ‹1ÝÁŠFÔlÅ#þ1$ÒI$’I$´’w@9“'‰,IbKX“Ä™:=†‚R¾é‰QRyaäNcàí$dù3K¸¾ÅåzÓ<Ë|þÛ£²]EõË­#ÎϰÈòg“ &è„Þ¡–'Å›ˆ ˆÿ†Ï­’I$’]Ä’$H™2dÉ$N‰“ÙT"k 7sæÄWsE÷âx¨î$EÒ5~ÆWÈíÚ3&$Kî‘Ø£µ]Oer™œÃ7É‹Ât>O×BTóbo –3 ÄJݰ³þo$’I4H‘"fq;`©D°%€¡6`e ŒŒ²NîT•°ÂnfGîË5Ñƃ«HÏ|Ô©D9x"ò¤wüç’-- ^bãüb} ßc;ɉü'“%ë¡6îHó(Ës'(°°³ü nÔ®’I%’%‰#8Í3LБÄf…ŠN¾Ó<Ð,â‡Ou(‰ÂL¾(¹µ10?»0•î<°Qr ¹Ä~"ÁòBú´x4;Ób+æ÷1ù¬òw÷{æ/ cüB²#=Ìe>I…;¨&t„÷rÄýbÌi)Fæ?æK$Hž&qš3OÙ{‘… ñ$¹”™3- ÌÞ2.; ÅbK °²Ó®.9]Î ïˆ1rè. {ÇBÏø‹Ekì ³Ñõ#û:ŠâÛ܇®Ìnÿc7ßC7 [Ú¯tw}G†]NÕÔï¹ÛßÃEÙ’äé“‹ÅOú¨És¤Ù_ågò’ý’ñ%‰œg‹gøÏ(fŒÅo¼ uñÈÈä.,äí…Ê‘'Àh|Òå…ÅFeDa.¸°p‘Ô\Æ!sœ‘—ä®ÊÿU"D‰!È{…þ.\z9$ŸÁFö6 ¸Ù¸¬zÙ§l Š Šþ‚!B¬IBÞEb°E ‚¤R"‘H ‚ ‚ ÀÐÛw[ÄÀ¸À€….¯‚¬V"°A ¼F98›´¹AAAAAAAŠGúô6TZZZZK-%‰7¾¿ðNôªð"fD‚æÃ¸Û¾-")AA jH ‚"ˆ ‚ ‚(Š ‚"ˆ ‚ ‚ ‚ ŠE ‚6¢±Hÿ+^ªÆ+ˆ€æþ7¶R7pE ‚ ‚"ˆ"¤DDAAFÈAAAR ˆÿd©"D‰ÄQ(”YŽÒ¿ø‡ŠÈ£ÅK–Ì­ eØFÄR È"AAT‚©AGã¢K¿9fŒ 0,ÀŒa#ÆÙÄLã8Ž&pŠs>‚}‰%X&DÌã8Î'‰ŠÌ hš{›ȽLÖ?ÞaøKýZ­Ôý›¨ÿ´ÙÿKü|í\I4™Ù’k$’I4šÉ4šI$“I¤’I$’I$’I$’I$’I$’M$°’v&“øWþ!VͧL„$&N–QdFAO(ž žð&OhDAA ›w2M&’I4’i$Òk4’i$’I$’I$“D“Dú @ %D¢I$’ÂIÙ‡?ÆY±a;kþ¢ÃØ„Y … C„(Gb¨‘"D‰$}ÿd·¥2tL™"dÉ$H‘" †C¥»ï$’I$’D’¢TN‰Užÿÿø‘¦Bˆ% J%‰%Rv§ÑNíïnÅù/oÈB!ˆXÀ†A”eFVÔHâG^ãü‰’&L™"dÉ“&L‘"D‰&L™2dÉ$H‘"D2 †ZZZZZK-%’Éd²Y"D‰ôkÀÑ$’I$îÅáÅ!ˆXˆ`@†`e[ˆ€¢DŽ&³VÚ:M&’x ’8fC23˜ŒDb#@C h‘©ÅćI'o€…wüò „BÀ… `C"G$H h¶qÀEÅøW?á¯Õ\¥Æë‰wþ™»þ ^ÝõÛ®%ßõ6z'6Ó‹½<~NáÄáºãþ:ã‡üG Ó¿{—w­N¹ØO2gs¦v™£ÌÐæisFWÊ; î:×™3[0ÙÓ2gùã4g “$˧’d™FA”eFQdACÀ†C!ÿ ‚=%tÿ¼·Â;»RM'bI&’I$’I9“™9’I4’Â`D`FK‘’äxã(eL‰‘2¦PËl$2(²Ì³(Èß*  D†;×–ßAV6gbI&’I(”I(”J%‰%JünwǺˆ·,C tÿ;;Sù'r÷Wúåñþ{ǰ’¼G¼\釿lõ’'ø+ê^åñ \—¢]ŽÜò˜²Gdœ‡änC*ÎKF¤â1ÂI¡x•…´¼Ð‘ì:p¡f&›ÕPošeÆ`>*wW~9‰‰þò—~ð¿8¢¯f7˜× qÜä_°Œ¬*Ä$6° à°KÛ íW¢æI7wQË‘/J¼BJ,Z’p’t•‹=”4Èw9¥é·o1ÎRÑ‘í“.÷y!+Wr‘yi Táù+蘩gúÛî—ºáBü+ú¯IŽÄR.ß#‹.i÷…2……&ݬ‘Nàò*ÙL+C‹‘9ƒûC"±§€Ä·ÙÆÔ"4V‹ “„Z†VÓ‡ýŽ”ØF#g.Ã>iZLËÂI Êsz 6¿&byÜ;Ë£8n•âü„Áâ=ÊmZœQ¶„Ý×@°ZËÁ6@Ì’#*‘5¢á%D†m-.œ*8„:X‰Âhbtšá’mjQÈJQ xŠÔb‘º{æKSú Ë—{vþX!ïÛõáĹ¼_‡ ӺǠ¸ìcèoÜG嬌és`[ˆ#tïÝØz+·ŠÿÃÀâ`tƸQ~þ"æÂ·-ú%[ÚpÞqõg7$ø›PAAAAAAAAEÄbAÀF]H¤R)H¤HôÑH«ü*Ù(ôËq­âõo}çË>.Ê9BY—W°ºž9uösˆ³©KÌ03¼Æk˜íµyÎòè;Iô2Ü™”äö°˜ïÍž\òçŸ<ÙäÑ㇎ÿ xQáG-¿ˆ_rytyôytyôyΞUx¼ýÇ’<¡8\ÉÂæN$jF¤XXY‰aaae"‘±ŠE#n)Çb=[Ýé£qq‡»C¿ðÏ•²™=…‰,nˆÎÛ @–D²%‘,,,,,<,,¡¡Á¡  ’@@DF¨;¯y$’DÉ2L’p™$áfC'¨jš†¡9ŒÆf2²²³S5 jMsXÔ'Œž2xÉã3¤ñ™Ó:g æÜ,IbK0Ì%âKÄœD¼Ix’ñ%âK%–âZZZ[[Kii;\ÅJÛ‚7‘ºá¡î×áË­çJÃÄÇ¶ã‡øUdMÂÄq..ÝE#b!B ‚ ‚ ‚"‘±µq UÇ­º¡îÐÿ W­‘/r%Èÿ;ùÛ‰&²I$’I$’I#ØWÒê¯Àg¼‡ÛÓ[ Í >Ç·ÈØÌV ŠEcf)~Rw³;a^©uUÛP?O{CÞ;¿·¤‡T¦àè£ÀÆQ¾ †§Fx’'Ûý‰mÂCêÜ®Eƒx> Q®Ðâ–D„·'ÄkI˜ AA°ô¥Âl ‚ ‚ ‚"E"E ‚ ‚ ‚¬FÄV6#j? ö. Ãßqèþvúî¡~ãl'+ô>i‘ßs)E°ZŽb/ ÈK’ìO ,XÎiüCÅöi>&±=ïx†«‚WIr¤a.3ô „{\aŠ(›Ùj‘¦eD$It& â¼7’F£Ý¦¦óºbŠ„î6Ã!.7 ¨'mñš$âvK.*¤TøR6B7@ Š‘DAAAAA®æÅÞÁMünîÙ¼×Ð? û#¢֒΀K.é¡‹_ ð)‡"›"eÉwMêÑ1ÛØM²FÉI„¶­F?dDqK#’æÔ‡i s$#=Jð“‹7ªòdÅÄm¬VìóYÉÈѦ)8Âne£<“šîÏ*-ýòCzKˆ|mñà`Ð)À@"¤AAA‚¤RØànvæô7Ñí»™x¹¾WŠïÂüæ:©¾Id“rd‰&H‘*J»zi¯b‡°J”©H•)$L‘"D‰$H‘"D‰&H‘*$H‘"D‰$H‘"T$H‘"D‰$H•$H‘"D‰$H‘"DÉ“&L™2dÉ“&N™’¢D‰“&H–À™"D¨H¥Wõ—žõ!Þ+·ÊñD~ÿ^Âó%ÙdâŸ(gI£`è§I Ði4TÒh4TÑFš4 &e8iÒqØOýÅû&’vS¤ÓN“NÇÐi8l4.§A¦é4š(Ó_FÅÑFƒE h4m‡¦®šúv<Í4J‰6°4Q_o´/*K+ÝÛ¶é`8œ7ÜEøV³.«j,4MW¹QZM4Óµ?TM¸àÙçk™Å°¯aYï#ƒÚ¬6£À^£ƒÚ‹Ÿ€ @…¢@(T @…£& ûÐ_ ›¾Š7 qßñá^Ã4:­¾oÕ ¹HdË*χùG¥ñÏ<óÏ<óÏ>Saçi¶È@²”‚ÉH²A“ N@2[1d²ÎL2!‘ ˆe `ʆPr–  e,`ËnÐÀ$­k°æcœÏó™®m̈AÎ|ø‰˜å3œ†wÎòŽC1ÈgËaù™VwäìIÜ“¹5›)™lËf[2Œƒ(Ê2Œ¢xÀžð'< &št#EÍ ‚#l7¨M”z«¡|êŸÁÝö+cHˆI6¹b;w î$w ìÁÙƒ2v`Ì™Ó2f É™3&`̃0f Á™ÞæÏ‘<‰åO"yÊžTò§™<ÉæO2y“Ìždó'›<ÉæO6y¶y¶y“ɳɳʞtó§<¡å,y£Í@òc™,D±œœœŞóSÚ’I$’O¢R”‚ ‚­Êü±ÔAî#Òð{æ/¿3°œßØCÂñ/oãn#yâï‰$T¹[¨ý\«è¼=óü3ç6¿bÓ65iAAAA@¬ ‚©R(Š"ˆ  ‚¢ ‚ ‚"ˆ ‚ ‚ ‚ ‚Û\eÊX/ìt§¬8᳞ùþÑîþ¶Ÿzí`ÑAADA(› ‚ ‚ ŠAX ŠAmÅ/±—+b…ñ6Xõô=óü1£c¤¨¿è¸¸Í ª¯$¨)Î%ø‰W“Ê¢›qI`WÅ Tï âíyCÚb®yxÂó-®g‘qpU2 UL8^7´JÎÙ{köYÿê&yE‹eÜLéÈДU|â\iV©ˆÍ"dMÍ#e«&ll¸Pe§<5‰l÷«?á%ßÍ`5 JûBIO#ö »ÏCÀ#•øµ`±”\qyìem£î45²ó½«5 Œ"ôÔ:TÚ¯+ýÿÿþë'§ÿЄl ‚8Ž%ÊØÐªç­5˜CÃòì,¯7èàr h-k7æ`!à¡/f(ËwÈIŠýˆ[µf…æ«oܸ¸¸†(kFìÄï´øöØ^QÒbò‹W ‘:¯ ”-JÑbK†÷·AT뱞Âã.[îX^ímA—/Ü+=ÈNê+iNæ2[l,ÍO–É R_Ùä<«²%⟈Z‘ÒFi 8Vî²NÑõTÓàùÖ°ÍÙó‘u¤ÿ¡s« ˺}?ûý’õÔ‚ÊrUÜ{Qè£f֧冇dïZ»˜‹~ëàLYzŨŒÐ·m8’{†LìŽÏNm.L´ò;Ù`¬_=i·\8ÄY¿‹DÉì“úeÚ0¸Œñ\)×$Loß;Fÿ¨îä¡É-¡H”Ø‹'MÄ¡d¼_CfY¶Þ-–S¤qŸºÔš/ˆ<aº¤‘ r¿å™|b™–mÀåLŽu¢WB mÉ»ÃÄ­mcÄ´u “c¨Ð”î*E 28ö ²FŽö¿Ç±Ñ»¸ÿ¦4é4šM&“I¤Òi4šM4i'…Yš àL™< À™2X&L™"dÉ$H‘"D‰$H•*¶¥Wj‚)¡Ü[}EèálëÛ°Ô/7ìVûŽMêØó®Ó.(l [rÞ;šeÅ*Øq;ƒ^ UµÞßt~¨¸q˜e±ðj_±wØðÇ`£é}»â_ ß}âqzŠÈÓ„Ðp— ³N’ýÔé4škéÝÿúM&“NÇÒi§NÇÒi4M&“I¤Òi­¢¾ÿó°1ÃUÔW¢âŠ…¸K˜Ù°Åá]èâü"¼¶ü› ì‘eÊÓÚjtúA?Ÿ xQÄèb!m—`âq‘µ/?|Gt»¼vžå×ŸØøGi½K¬i;\ÎÎteä õÄGbáÙ,š â¦òÓÃø¿ÿÿÿñÙøšM&“A Òh4š °úšNî"Jo0¯­½t]ZW­± ;΃ˆ¿ ÒÍ!9ȶqó¨¿4âpj¶‚XÀžð', `KX ’$df#ZæOÚG•G‰ŸÈxAÀµÒx‰âeøçä®x¹ã´Çž<¹Û³»gdÎé›ЇõÃÇ °dêYÂÍ—žžVyéç»=ð#Çêx5³«LyQà‡‰ž;TþÿÔŽúR<¬òƒÊŒ ™]WW%±ÁïµI{¨v®Û#ÛæÂsM±ª­¾Ì<’¤iûM•dlæNf!ò±{É: ú§ ]lw¬Š^>‡ˆ¿ òÌ»-~…d@«J%=O€oÕWóÏO7¦]³ü;'øMÙüœóÜúž+n*dõbÉs—3Ýõ;ÿS°õ;Róõ:ž3ÔíOéÙ_ÒϺæv§ôðfÓøñï3“ÙÈíoáٟú?”‰á£ÂG¨;·C̺qÐφD œvßÙÞê¾¹Ú}gaÔxþ£Í:žwÔîÝhxv¿§u CcÉîôÌîÞGoÿÙþΚ亚¤y:SÉÏ/Ùjª¤œIûB1ó Îvw¤Èù™tb±e àø%‡À–ºÃºÃ¾ÂH×Ìkæ5óPC BÀBÀBÀBÀB„gKÕI¤¯ ê1·xqôKð¯;9 …#ÚkæGGƒôD}QXvï!ÜJý¢~áå¨ý›;—ÐñO¡ãC)Èw˜í1Ûböß0ä½G#'kêv®¥Õ½ú—¶÷j\aÉÔï§S¸—RâË.¥·ð]Nñ?Èí‘æçQäQæQäéïèw‡uèwÞ†¾‡êwþ§qÌ9¸vXí±Øc¹ö'çêvg©?¥õ'õýÔ‚í¡ÚS;aIenÌŒ§'Cº‘ÝHÎr¬“<Ã<£<ó<Û<Ë<»<Ñæ bùšÞÄ’I$’I$‰Ý«‰~©Í/‹âP½k|éÃЯ·=­„øD0³âÄ7lºŽÙ;dIù“ÂO ‘2†XËFXËFXÈF‘¡ÈÐäg#<Î3 ÿƒ0ÌøÛ M3æ|Í™“ÉÓd‚ ‚ ‚ oÀ–íÿŸãÿÿî€ ‚ ‚ ‚ ‚ ‚(‚ —Eµ®–÷©#eÑŠ¼~úòÛiVÅmó¦K| °F×g^{ßûž×ßæ¿øŸÿì ‚."ðè–…ù¥ãƒÖÝ΂ô,_…k<› k‹è°tš4Q¢‰§jŠ^–‹`-´’]ÀvlÙÌá#À* ŠFÐFK˜¢CÐÔ]‹Ñ:—¨RÖÊœ;[ý¤žãïQ©yYðÇê·mã‚´´[…ÅDSg:„x-‚!rRÉ]1b“;ˆS'|OØoÀz&J¤¿%¿ùXƒ„¾:%­”µ®_G­º?Ƈ³vgvè^¹l²âi°¼çè¾…æ}-Ù¦>%¥Ñ½vmfÈG‚«]NJK5úéB׎µ$ã븛¸œCE‚,|]œŸ½°'Á$¡¥º¿xž@[‘ÝÀT6–àp\­ÆÁCç ¼ÉCÈŒÏØOIãÙøs r$ÙåqÒÔˆN¥5¨gbµ™/È(ÚÍØ•CbûäAUT/ÇôÆB³ŸCD¶­"†Ñ­–µËzdbÑ©qhCÇ/”–~À[” ¬,-ÊcvYÉÖpÕ—³ùxɕȴ!"iBûî‚Ð2±ÎÂ×µû.”ã·Èk‚q„Ûa„ä(¿¦¿¨×¸ÔMÃ˜× DÈV†M¶ÒÛ–ó{&ܱßap¸ IZ"½x¹É—®d™¿É6MØÛ>-¾{¿ÿI¤ÓN“I¤ž‰àKX$H‘"D‰&L™2D‰$H‘"[°v­”–ù¯Ñó‹è_^ù;G²‰»E.ÛBüËb†Eÿ½¥ÑcO@•„Ø%lÂXŒÉºR)…Y6‰wV&ÿe„Xa"S6Î#¼6õrXD¸ÂMË×  (Ì‹ a‚‰¸%a(%edÉ$H™!0’©fë‘u+¾Ô>±vD{^Â2Ó±µÞÒã/U¸;B¹NŸOÿÿþš4úïÀ!#2þ‡K ï`ïÑw×8µ…²ïÙC»mzò¢Ø{‡Tæ¯ØŽ>WȹE³úw?šw86ÀãÜ@àÁ¡Ût+ºoþ'Ü6MCA Ðh4Àžƒ@ÒuÝCÁk—^x¹âcH<ùóÀ3'ÌJø&ÄÃ. e–‡×,0òâÙ㲎Ú_…4{ƒ¯Á¢û¥ý .Å}WÿÀ(‹È°ƒˆ–°dí´O"x‰â'…G™s'ïîQHEÓžxèþ’xÆÅ’;çÐËPVx¼Î}ÿZ/Žu<;whóˆðÓ?ÊwR…¥ÛC"|Â”Ê ®vËÇÎg¨†d³fðãØey®Å{`þšHý—¦“Ï2üèŒggå=L˶gˆu÷ð‡Óѱ.ÍöÔîOêwÇôíÿéá©–g9ïôNÊþÐïýÉÐò”óíŸÙÛ_Ò>×QßûµÔOÚõ;wS¶„̳ø;}?½‘ÜèµÑFyÔy#ÊÆëÄ·Õä ü^gjéö‘—æ‰ýµ@£ô¨O8yæv,í_B>·A§Ë ï‘Ù#THw…Ga#¹WÀïçÕ5ä Ð쳲̿6d¹‘a Ô­‰œè^ÕëÇñÃe_U‡ì§ÅìJ? cN^uI^Щ$|2ɯôíý©ðú'uI… ÎÆFsC°T?4yñåç™ÑfÄ}Öw['à-K?Èì"^ŸÁØ‚8s¢ãûEªýB>°ï%+ºÉûgbËn¡ú: —.аÄ\ô§rêv“©ÝK©•ð;D?¾GœG‡t<¡Þz¾ú–èisûm³¾ßS»=Iwõ%ôý’ñ;fri×s ‡'C²‘›åEò ó,ò¡ýøŸ®<Ó%ö’GXbc„Ò¡,, a·T%’Éd²KKKKw|hwì¾pнkA»ö¦w¤q¢Ç¾çm8ÓâÖt%úÛÒeÅ ”Œx‡vÎñõ;ϳ'–ƒÀ#Ä%õ‰ý(ÓïØï¢Xù}úÙ/9Ýlï¶Gí}NÏìó¿tõf ÿ3¾ÈùòO2x9žA׫À|†Ÿ#G‘¡ÈÌøØg Ù›¡y£ÍHòDãs5>e¤~/N@¶½}K/œ'½kÜv¢©î3ãcŽßá>8àÓa,CeËöLƒÔÀ€ÿ鯦¼ÉS?M€Qnÿý4éüÿÿü ÁßIŒö§éBÝJõ­g]Õ±õGN?‹]j~–ÄZõgÇþ“.›Ò…"F˜ìx‘¯£xþ'^dÉìü½)Àh!§Ö?ÿÿý;ÿÿÖàdL"î^8Kž¥ì½ˆ·*¥´U=·øØW¢ÞŸayŸÙòdÏu¿1#³ð˜â$¶>šÒ¼ÖÜÚ¿B™åbqcØDŽ"鑚ŠÇ²…Ÿ¥ ¢ h¡©ñòW¡QÒp—G²ùzȧyÿžþÆ|ƒˆ®TYF_Ø× ç wÕ=†°Ç ”—÷²°ˆÚt/N¶ã‰ð{.”«²’x5ð'#̤¼]n°ÉÝÝwódÉ”îjñ8. @™4ÔÄþ1€®Tcd|S.ª_².{qˆs²+áȶ¼Ã ^bvYx–ð›ÊÒ^MµÈ¼‘u†Ñw‚Æ0H E€FDö¸÷–b\ˆ‘›¬>+‹Á§dHˆN®A.í ¸É §lÃ-¢/5b”ö«Ÿò¡8â#Û¬&`ž¡Û¼;w^ÑlXó0â¬dm!dEÑÊÒÇ•¶¸I:‚w?(ñ‹¨ÊÏAf±’-8V‚¨8­ŠÜõ,^üÿûÿþ“M}4é4ågç ñ\†$¬oœ%ß\ÿm -–îEÏX·œ‘ª¿BÊ\P²„-(ìô½Éå¸,.U(¾Ø¬²±ý±ÊZ`@í¥ò‘ÝïvÆYlùq@KØÎä~Ì&”:Ú¬Ù„æe]îÌF’«/‰¹Êý دCcÀ–›gà•ä “Þ3ʲ@$ê†ø¹Œ‡½óÎ;eÆŽùD1ç!÷“‰•KŒZy w®;ÃN ·)Á×Ϩª-ÊÞ5–{4ÐŒtÑK>]è$êãöxÑâÇž£ö ì±™æ<öƒ—Ý©á×S¹]NÕ v”Êö„¾çCË39 øyÆ óÜô¯‹ÓnäF:r‡G‚ݺ…ü1K—¦žÏXÍöÔñ>§rNÁþ‹ÏC#½‘˜vÈî= ÕoGÚ?Ò>¯QÞû“õ¤—gú0!ÛÞûØó§š<ÀŸ¬-ñçEÜ1ñ½Ã+ðKj"'xvØûÏ¡=·ðŸ¥Ðv˜Íž÷ÔîÝGô§i!ù(ê= ç}ó˜`—8/Èëž;Nç¹++Ës9){ ÑóãÎè'—¡^™^bg gÄÏ…ÆøQæ•)ÖÃÌc¬ÅµÏH–¯^YÐ/E¸K(‘ïùÛëër¶wÏc÷)(ÛÓµ²”?بLvzŒô;Bßû¨Ì~gžê;ž¢>˩ۙ?LI\¯m5ÕŸŠ'<ó<€·Ç™?"æÙœäHi|ÒˆcÊ;mÏõŸ÷-õ;ÇÐÊòeÏô3tc;•Ô´é]L¯‰æPñ<Ýô2]ô0ù‚$šîPÊ·~ô“‹Ì”Üc” §Ìê–|wìæÆÔ§””³ÁO;õÐì‘æCÇ(þRy œîÌËsê¤øsÅ!»“'G‚ú(<à§r:)-À“Œ$NòP ~µ Êäš¼” 1æý<;Æyßz“ÃßR=½EõÃÅ£²]õt<;¥#/ž,g9Øê³#èÒËéG‚]Ë °¤¼Y/KÅ–þ* ‚ ‚ ‚ ‚ ‚ #ÃAŒ¼…³pÒ½mÞ÷7Z}­ÝÞÚ[Õ¹[‹¶Ó± tÕñÃÄ<ðÈH¹.E¥¥¾“ð0AAADAõÇKÎhZÖ4/Rö¸îUÂ×$Ý2ãð+`î·~„cø.ž)‚ ŠE ‚¤ë`‚ ‚ ýÅòøÆYg{”8h^µ¢9œwH´»ÙºeýµëYyÖóUKö_Ò=R ‚ÎáªôIRœÄP‚. ‚ ßR ‚ ‚ ŠAV= ±FÔmÇ¡øê\c¥‡æýz×^µ¬7*ú¥¬—tè¹è–ÂÛKjÀÞWô©yÖˆ"TÖb- ™´EpZ¡ ×;¹IE+w$"dE Ðvl6J¥lZ@l$d> ”¹„kbŒÕƒFAAAAAAAAŠG¢ŠAAEA‹¡uŒu©ÃJõ.¨{¢Ü^ªÎ’ýîÑuEéÕʯÎ/«ñéÅÌ+–‡œ$IJIÇpÜ@–ªW…ELZ[è8ȽKÅÁ ¹±.$…y6–!Úö V!ÉAAAAAQA¾m`G fƒ¹FŽBÿ%ó†•ë_”-Åú"Ÿv‹kê•Ëó6–Ãø>8h¥È#z꘬´Åa*l´rK--e¤ AAAAA‚"°A‚à GÔ ¿’ïäÇô‚`3rœ™™æ­Ñý}ÉÉÁ .h:v%ㆅë×râýRYb‹¬ÞoÒ­í]‚Þ®Eûý1'I¸QAÇäúÇ x1ÍYL~XóÃ0ŸŽe|:Åž]TÉÍ'ê:' Á~>c'ÈË{f<$°^íè>7{AýÛ¡Žƒ=ȹãØf>+}ýGˆwÌðË©›òïf g¬3aö…ä: @dE%0’jä%ÕÄm¯u˜Ë(/œ%Ï\â·D$TXX:ùôÄnVåmXÔ84«½Kʾq#Él‡¤<ý/)òwS ãÈéÞ îÑÑÔÌòQ2w{yt%¹èï¼ÝIû¿¤œ+¶cû8þ3Ð}ÿøOvd[¿æ$¿àsvY™lÏëX“ÏÐgÞ”†l# »3´ú‰úO­5âur=¦”»)ÎJƒ!Íö^cã|Éø>h|Hä39TýÇlúËèGÒdyƧmI À#½”G’3ðٖæÎÕÒû¨ï£+”f®TYó2xá™Ý¡“ÊèvÞ‡Þ1ÞëfÛþ‘ôæ‡?BY¸®òSùBn£<“#¹™c'䟒;e$GÝ;lìI’äÎËáÚº™¼¨ËøeEt;gC82ÜÇŸg`ú‘=§t ÎQ—ä?™åO.6õÉãÌ{‡–†‚xÀÊÙ9Ì'¨´´´‚ ‚ ‚ˆ ‚óм\ e„ä/zÿƒuw2ÐÒ¨ˆ³Ò»ï^¨¨¯8Ùl;5Í9BÚ¿Ø3ÃzÒ× ìºSþ^fK/Ù™Ù3ÃÅ'èÉp_·¡˜—²3´iþÒq|ËaædÍK™ ìGaÒN’äwQ˜äY_ÈF2h¯(CÌŽlí·Qv ì¤hrú;¢²v¯œ†8ùóNXL3)<FwÅEÍ™ó1ÌÖÈÄC%éGÿþdÉoüvÿÑRðwÒg\"÷¯~ÒÚ‹pâ"!!øÿ{Íþàì|×K–IÜ•QæŒç2>Gu´k„FI3òBœæÖã9Ì‘?##BÊ\†—#;àí$gèžHÌó5²1ߟÿ •$H‘2{¿ò¢8v>“NËÏÒÿÿÓɲw—$«ý`_8=vƉ}¹“jˆK|·F\Ò/G>y˜m¯fè`óÄâˆd‰ï¿M: $°4$Osÿ$H"D:ké4škÌ2ßÏO¦ìüýÿ…ÊEr’¾É˜ËÞ¹CF•ˆ j¢ß>éÕ=úôP[Ù­¤ô$¡-ÁØ!D"G&štî ðþGÿÿEßÿþ;¼ÔG£ }±sQÄW B½^ÐnýË hBLð]ÝϦT^¿Ð¼$Á=ßÿ u@ˆOð -¿€l#Ñ€0ß‹2dÉîr*eyÀe¤æÿCXô*7È‘Çp¯"AdÃùÝÞÏäø—dZYº"¤·vÈ«§Ý…‚íæ-šÂPó-Àß6„0çñ$Â^6vEžBilZ­‹p–)Ì “á¾.ö“M¿‚êG [ì?”–~\øá|ïC¾]DÖ– bQ‘ä%â’ÚÓ= N6B6Al„z ‚(‚?khÇó2âŒNGè?Çפ=†{§w1£¢}Ú¼´Ot·ëz~!p%+Hl®ÞVêQÃÐÅ”`ÄÁÍ"õ‘4´x¹ýŒ’q&BѤ®Eîe:¶mÔ· ŸÂ)Ü Å—M„9[ù߬ÎýIZß– ¦%6íöžxyÛ¸ýî· ‚ ‚#ó ÿ÷ÿÿÿú `É`i!Š,õ /æ A¨ÚøCúÁ¯‰g~uq°è’—[òþ¼!¬]Íñ%À{YD$,S[Ç•ôê‹~¥À—?C¬`Så/° (RHË$Ò†ÕÂOø„VÏ!®›G®ÛÅ ™Š‰~$)@ ‚ ‚ ‚(‚ 1<¡T‹ª?³‹-ò? xÞCÄäðìÃå²k„Âæ z 확ÝÌxw°—‡Û!ñy ß{A¯ð}ŒWöÌÌ}‘ç: vþ΄bÏ}ÎÇP‡¯ÔGqÅw(Y¹%‡ÈëØ™yÏIÃBõˆ°²Ýp˜Š¸ ‘2ü,·?£e¾„¬3þÿî ‚ ‚ ‚,W´EÖD]tyã*eù3¸™žäwÓàe¿ƒ¼”)®ù7ü'¹¹3 ,"³=µ4º™eïÖe'/Ø<—¿¡‰Ícy qùz‰ÛÔİÏ;dyuÐÈíÒ‘ŒB³ý[Ò.‰âd\Žä{ (öFä%û‰þÁºö\ø¾ga ˆóy˜ÿ1“äG„×åI%—Ìg¹–wZ;h–†#>‚[-¢*—ŽöŸ4à¥zÄ=Ü…¸I“”l@Œÿ;»áúU½nPïtEÝEä%¯oÖð€AAE'<Ñ姆;¨Ar½„¸.Dfš„g£¡Ì‡ƒ™j /’|o‘Èv ËÞcó ·3Í\¨Óä6ð¡H&8÷] ƒ/aÌã8œf³Q‰%»?ô×3ÝÿñÚäHø…áÒñ µU¯X†an.W ¢lãúî™x²Þ/WïÙKµ—‡/ÑçÕNƒ§éË7'ØaÂѪáï=äx®ddæY‡™¢i”w‘žù¦S3Çe–y8 ËåCRŠ! \éJ–2‘£ÈΠÍâq lŒDÉîÿ̓Fø"F¾š4lüðÞøîp\EñŒ¾åýõNW¬à<ëîSi—ß°!-2{»½² [Í{[ñ ܤ€@ÇÂCÇX1ÅJÌ GÌÖævÍHÊF,ê Áž'Ö@‘=ïÞh4nÀˆ"DÓ±ôš6<ÿÿý‚;ý…„8Kôeç7è½µ8}rü†mʨ±ÕŸ{7N‘¯F·ÿ,¿ØH‰XJl$Kzÿš o„"Gó¿ÿÿ€þQðÒÍEêXË,Êôá.z׆ä_îS0‘6/"î™c\{K|·j¿'aø?t¼ê@‰$HÓ¤Óè@š4m¸;DAA= Ѳó¦[`Cî/l,ë×nz×"ûr懭’N„òݼkö#Ò-ÍBòÒ¿Gɯ¢¾ÃH„i£ÏpÖÁì J¨‚ÌQ2,>ÆÅµ¦6¡J‘Ù6)"pÙ<ÑÆ AÚ«pJú'j¢5%ƒäy±!5¥Å@ÓøÉBÏt‚ ‚ Š à‚6~ ýô,`JB«³°´¼pzÒ óp‰ Õ w^E½$=¨ÜFÒÞÝŸª·‚.‚’E¶»‰jEÂcúYü«Rà oÜ+©Næ¢2´^e¿ƒâτܸ̀m÷)éa6Þ,;m2ÀEüM¥‹R|8?7Ç¿“K¢=ØËRd‚­Mâ<ç§ÍBrêü2”¹Ž6— \â„%$ä¶N.2 }ÁÍ–|EÂÕ$LEAAAAøà ÿÿÿ该̆þi\j8ÁøW"><ƒÂ¦cÚÈÁì¢-‹ ï%ÿ^´>£½îâE®%uBK,·jòÛòÛ[Èß\®çOÒÈé“ĆÂܤJ ¸J$Da31µb–A’ÆX\,Í>"Øl>Œ«¹Ëáð KçA (pÍd’OöQ-Þíbu0™¤E®íCb‰/ñ&ZY*µaDl„zàAFØ.g’zÃI3øñÃÀ/†,|Œ?6”ð“ 䉋im ¯ç–¯æá'Äku¬ƒ(Ž0 A e– ^¢ï­k ÷Râ!‰¶°pmݪ(Ý­òÛWì'pïj|”$¡ ;õ8WÜD;2!îËæ›¿Ó$6AAIÄŒ—3ËP"à2T£+ñ¤8rhædyžz“œ|A/ÔÍr!…·ªy3¶Ù›ÌeQe ¡–e„B#Õ¹'‘ þ»CÙnl: Ä›9þwo>á)fÄQl@½J°úŸúY}8AARq#-Î… —­¹ŒÁg±Â,'¥ò†BfÒf ZG¼Ìæ5¹³$È™~FHÊ B Pý\åòr¸!Eõ¤5©–æ¯! ÙUḢñ»¶œýAoÔÖ Zç ‚ ‚Âp9žj‘2ÕO9˜¢?K094ŽóGž2ëQÛ_f#ò1JG¼Íæ2_:\‰àŒ‘ üKÞ?M,&æâ^°IeÈ‚õ«"ܸɮ‰„$;.íîi´µ¶½% ”?O(y33¿µW‘Žü¾è²â8ÜÓ8²Ÿ6vÛ¦xR.‘”ˆ¤€onws™æŠ²ax(yÐoú’qå/X–êqu &H‘"[’ h4쾓MiÐCA †ÙÈ#¾æÐy×n`°½A;[):_ÝÛC² ø&2tKßK÷¤<#8fé; Ñæy ,#²Œã?Íýˆ>Çu§}\¼ˆnøŽ£¹Ó3Ü$ã% ‚BŒjR$K}Ÿý&’87tÓ§ÔxÄŠAø'‡æ[røÑb)6W6}Û¼Î{1ëfCó$ªûŽFxÍRd|š<Éý„³zÒÃö;̇‹ àa|ò0Ä‹3Á ÏCyOÄIÔ<Ñk‹™˜MìþFÄÿ ‚ ‚ É!áÙËí¼”É`®›.ÔÖ[»ãK²l#ð«ŒÀ‚òуxhy”‰xÉ n¡?X·Åó5 ’$Ooí?†ÿøüT‚ ‚6 ‚?Ä´i›‹ÈYV œ[rÁÁ÷Hy~_„@ê´hI¤Óêô'ð"ˆ ‚ ôˆ€Ýû–Ì1!²4­”$#´î•ãϾ?º¸úüAAR ‚ÿE>… \wÈÌŽ¾ aXmvòëðäAAþþ+Ú!ê óFh9þC1ð_˜Û‡0`är–<…‚ܱDµ)& „ƒvJÍÝìsT:ÇûéõRAÅuˆÁ¯„Œ4ñòƒÆ ŒX™aã.C3i7ƒ™W1ÄÖkP­3AàÜ:Nx$I•œ ¾âi ”-ýVíìö…þÒi;ùX™NcÀóºƒYdFñƒÍ8|3¦<ðQ„7ñ å!KÔ'½³]O0›âk#€Î3èÅÂd‘ÀÓ^d‰dúÝæpÊ’FîÐâl! o»µÜÔ=…þži;M$’Hâ<×Â4ƒGÓÀ5—˜<6®nX!ÃLM0Å\;IÍ_½ÛÅü„þ:I$’I%B¨øž 4—š<63¼%‰áÇ‚7…¼6ñ£>G›õ†}¡8 “(†0­2D‰“Ú„’Oãä-PÅFÒîÊ&d¡ÿ‚ìÒHbd¹žHi²iáK„ÆW Œ¯ ¶¼l·ñ €Ü ÷±ª@Fd1¡A 5äKgLªI$’I?ö«¾®a‹X{K+ÍúÝ´;:¢ŸÇIœˆºÃ_Pk®¤´ŒGŒ1<8Å´6þ!¸›‚[ùƒx¼ÍB35Ä€• 6^dö’I$“þQß±;(à$2Á½Õ”—b›)ÝNîi>†i&r"ë ÐM<OÖ^pñÃÄ CÁ <¡¸6ñüd¡7š5ñ=ÈDE……„¢V-*’I$“IÿEe‡~ía÷WËC- ~¯1s"ëõˆA§„júqr¨fG–> ‡”6þ#n2N2qšˆxš¨X„"”XJÀ’vBI$’I'ý½âò!#r„yÝ+Ç–dЃã-Ìhê‘_Í!¡áãäw•#Êö<žtGHÀø†þ¹?P“™'šk5‘™¨±I„XXYa$Ñ$’I$’I$ÿŒ»óJ®òì˜rÜ!~-Ò]š]²dÃòcúS?ï¤(ʼ‰Ù+~ãåÞIOqm´$ûðØ%”KÀË2ÄÚy†£VÁлŒ5I ˜RÒO>Q"â†ðŒ¥}'ÁT2àŒ‘¸x£eJKÇi÷Ãbs=ö,,Ä™­â+÷˜7'DÜ á°²¬ßïa’ÀÊ&àeìi,MD6 XF@ƒ KNƒIda™¢;%ˆÌô_ B$\†Pè†FÊt±$šI$’I5µ¹fnòÒ–Ø$·r8‰)¢ÑTVÕS‡’™×s‰¸Œá¥ ‹p³ïš®o¤$«/õC%”déˆcPË2E‡Ap!a°`ˆ,Å cfQg’e!•I“Æ&©Iâj$’HI I>ŽÌ·p¼ ¼ƒ›¼ MX$ÄX8©ÄJâ᥊’MW-ðGÝc“ŒBà$QyÁ„m]D…`ÿÎC%”dì•nKO»Å’äMÚ\‚ÊÎC3Æ@HììÉ’%’ËOËA‚ÂÂÂÂQ l)ËhŒY“±¤ñ5‘Æ´ Ë2ŒYšI¨†$1¡4h¢DÌÃ8Ì%‰,·ÓYn˜ÞË\Q&ý6®£TUµï© Á¬¥MrÜ †<)¯šßvMEÒeÆÞ@ïûß·ÿ¿üûÿóÏé‹ûþóŒsÍFqÇ_ U÷O=òM÷ÞqGqö’mß}ôþ÷,4ãÎwÞ Š;î£Ï,¢9ïžt×}6“Ïw«lóÏô0ç,<Ç;è¿,±Ë-<à Ã}ýÿnfž{+¾ !¾8óªJoŽ9$¾:Gƒ$“}¢óß,1ËŒ4¢Ëê²Û¼ª‰$Š "’¤Š9­®Ën¾$~ãºÀÀ <ÿ‹¯o®ZQ¼e'ÞŠ1Žù6ô—Àà EÁM4Ÿ¬{à–û¨mçAuÇS]€G x¶þ¬Ã!Š ,‚šâƒç¿þ /\°—Í»ÿýn’ñ=ý AOxû‰E$@Ÿuö6 paqÀÍÊÚ↠n®+®Hìº[üò]2æû,AvÒIðË<„+ï?-Ï8Ç 3úKm bÑGM4I‡_m&A6y˜a·Ï<0M€ 4Sïº ðŸ|ãÊ™­‚o¶⊢Lš È2‰-§l<óóßÛÔ³M,0—ŸûO)ÓtÓE 0PD@WØQSyÆ_AÎ8ÃÍ 12øk®¸n¶ëï¾òå¶aš ëŠq̲ÑPFŠ~3ÍõóûÛ-®°œ ß<ë_µçn»Ë<óÿ-þ˯;×-7Ë=÷ã4ARÕUöx4SmtSy &ùï¾Ùâ¢ËçŽú§–8l†Ëï¢8àAMBËqË?ÐÇ.Ðc}ö_ÿ¬îv±t˜ƒ­0q§,ž¢‚óË[üž¸éŒ1ÃsÃeJIuÜéÑì¾k;‚ÈO9ï¸ûÜ@"M½äg¾TƒO_Z€ÆeŒðSžÄò€ºµ òëÀß·¬¯{Ñä8nZS Qóó¼Ñ$"„ Ï:!•üÈï0Òƒó¬7¸Aw®8å¤v7Ãg‘:Û³á9ä×q`$Þ/<Ñ¡¹óîñÅÎÇ\Hâ*_a¦r墄ßÖîÜ_Œ7dã?õä –3ƒ08!†)&6ù?þÊiv ( ,ÍàO[·ÿÃŽá´DÔ4ÆŒs$@À ŽÖèôV öõm§{R÷‡Ù­Ð˜ÂAÆÄ¯f8ÿï1÷ÇZÛ\¬å-}×<üϽþ†s7Á@‘1Ü=tvEEûÿÉF1Ô|Â,€ ¥ß$Š+•ý®‹jù¤{ë‚Ã<Ã͉_•|ö¤ÌôzòküøÏ·7Ø/PæÙAûÿ²ÙªN¨ÃÛ¾†¯yŒ]|o×3Û'zStS8o¶à‰L9ã¬sÂ$" £ÜsísÄaÍ~ûŸo{ºqèŒËWÓM7õïä×U{ ‚óË?³´ÿß<þ.?˜RèZUÇðLj)<$3?Þ ÷ÉfÿFÁ„U]ÆT´âd²KnóÆ/{Ã80Å2ÄË|ÿßñÏ~ãž3œh¬D|ß9ûý/=¿îxI/ ྺ¯®î¿ïÚq äÅkÜI÷!ï2üÑ$šú+³—Ñì×PÐpT~õ°2 2 oßÃüí²ÿ=éFãwœgà 2ˆ0ÓNûùS<óݼ÷þ°„ ï¾ù¯M„ë÷êqŸ8ïÔ_Q×Ö"É0p¬0Ã?õçå5Ã?3º$ó‡IŒ€éû¡ïÍC.a='?ö2óé›—Óm&¿9°‡5ˆx×½úU`c;<÷×}_èóÿ:×{àÃ43Á$€M¯Í5ß}Ø0ÃM?jŠ;ï®8ï‚!Žû© Wß}ûË=üóAµö˾^­ÑBTb„Eð„<Œ§v?]Nß=ðËŸ·¾g¿wááYa³%"½ÄÕjwˆâÑ P”8‘]÷YoƒÎôuúIOè;ÿstÇç9ûl5õ{Û“WI6 8ØQÏ î,óÏkì—ÿ¯¾’üE"côÿ?HŒîëÐo Q‹ü`!„¹«®;ãžØ ‚(­é´ó¯Tó°ô•€4Vo|2Y@ ‹æ‘Â\‘Q”8³„›Æ—ÜÓL‘Ï$ÿðóÛÿ¢Ã)Ñj‡íÂ.)m(C²i ªÅϼ4óXþú¨ Ç\öÌÃÏvÃCrÓÊ u7õçï²8Ø,µ«”ë~MGÿ:—XóÐ}¶lô¤¶û+ê ÁŽGã! ®éïãú0I…¼ÑPBî¹ß¯<Ö²ê’ÉWëÏ(_¾ð䆋UÞ\ÁR†"¶[fí˜S¼á±š Aq†QoÎþÿµc=Ì3¨j;ì @@SMµkÚâÃÿ}ò'M_h+0–ö¢s  #}o9ÁWyÀC’r{Û‘n®Re.sü0wÔqÇ_A,ùßì±>® ›ì°RÔu<|¨>ÃpI3Èkg¶#Û)º$£<«à½÷9ÉS‚©ãžçPkÎ §2¢X/t0ÂáMTS_{,è9]õê„<·&.ƒãÝ24$ÇÞ ÉYĦ=¸… ªíùÒþwm3eØ,¢ˆÎEî´Ø÷²Ï4CñÌSQ@<’þ1âˆâSÏÿ“ß8R]¿í7ß7–a»JÔÌ #³}åHvñ6—÷—_†w<ŒlõÐŽ0Çï¸ÚúÏ‹%¶˜Ì$€VÕßvÑþ'³ï€, ÇÌ_ŽZyÄ p:ëÊ÷ é ÙÌ’ø Ó€£Ê,»,²CS¬5xë£\qÕQY>ã¯?²÷+m‘çœCI+¹Û$ò×~Â(C&Í5X¹ø}„g]»ÃÂ?E·°* Ìzù.âBQí†xk nå› ™Öâ<æÊ£Šp1ßâmÿÝ"Qð@rLУÆY@‹Žÿý½êÀà ‰õác•¹´Ã'Ï2CÞe-õ{À_aY Þñ…]A'ÙhÉâ×ÛÎCͪ¿™aù£ñÏ0 Œÿç˜AMò!‹¬½<#lº\’N¼ðIÀšKÙ¦DZ¨ Qv笾ÇÛÝBb u®~Þ} àå˜÷SŒˆBË­¡Ž'¼.äÂõšãCO<Ó?¼~ë¾{)Ï¿0 AsaQWqdO{²Uåј›Ïe6ãn»ÙÒWöÈL«F± XKš¬»?n>¬ìõ7û‰¢:ÆÄMÖQ»l‚öÎsûÿô°ç¨Xe>;î×MùŽxÀI‡Ï“Ùšq”ð²«oWZ o£$Í}¿‘6Œ ûc_‘£ñaײÆìø\ ÕØédòÇ)¼=ƒn7¿¸7€+Mc>üÿ øûgúœNaN5¶òYW®ë<ýôA,³G"9†=`´Ãœóðk´q:7eÖÃ…µõ£:Žš²’ècé€éD•ɬـp 6Û.°v„ ,Á5} ®0¤1¢F¾ô×3È SÊIþ…ŠAóëNdgíLµ¡GÅÜFá_EJCˆ¿·ŽÚÛíÎkyáõžì42 $’˜éÕœE„5¶Xa]ïûË Žš¼Õ³È€Ç5ÕâýE5ÀÓN½ØjÅð¥}dí_KhNˆ’–+´ývßqÖ×}÷› l²ËO’È(‚˜ç0ÃL=40ÓœýÛŸ¹Ùdóø(„5H‚íWÇh‚?Õø°ÀAÄëÃ5ŠOo`oÚDXkXuIêm޵lhųĿ¶²O­}çR|0£Š 4óÇS!$` TÜp‚Òw­wîgi€G}bÜ×ñ S¥DSÛ¤v„FEÚJ¡ŠË¼o´fƒ;‰·½´9ô Ýß“/¨(]ÄÓŒ0¶ùï¾Êç¾((U¬' ÷Ï0°\q÷[Ùn°ùõ|ÀCÏw÷mðm$ððÒ-.à4I[ƒ›žê²Z˜)•è4ã0òó8ñQü9S_8Ï+T:1ˆAò†Y`F,,õÓ:šø J Z’LðýÇß‹×æê¯‡ØY.òÞ“Ús'É3G!Ñ‘á¯û¥*™øÓ§ž‘J^<|œ†Sã¶;¨ˆÈôÇ´('JAϽ(qÜ´à×ÏA¿Ç` 5óé&g5QÓt?)åZLÁpoÉ« –Š-‹vî $ë츲ÉÜu0Œ#èÈ’[ÿi&“Îöóî1º»qeöÓAÎ>ä,ÀÌ ÿÓ¨uÆ,˜0ó.}ŒÊ~oö\`_áÊËÝx-¸†ãt,ÛÏ£Gž:ìßdò¦€îðÑDÊorr,a‘4ÃÓfô— =hÒÅ$1"ë$ö‚ã~µÎÅ/+·¢=]afßCtÃ~tü¿zœ¼¶ú•ÿ}ÄXMµ¿Ë¾>õ„W÷±î³5”Uu—VЩ²Ëg‚q‘\ØV-xÃÿÇð„çÍ;„W?-=ïÐm™5·_²Ã§iDPð¿Ds³ÓŠöÏÿKžðÏw.¦x$3éÛN²†Í—QþøÛQI{ê›¶´’sçc=0,0"zYÀ# JÜó÷?gð^øït}~¾äªœP˳}Ö–y–Õêp‘Ucà ]èôÜ».ÝrqUÒ @1¨¿ú)µm$}]tÅÖ”/,>y´Ÿ}„š@¦¹é†{b]Û¬Š /²è‚Ye£k»aòj"ËÙ`?Tœ0æñÏ ˆ“¶óæ/69»Í'vBè0Ãí¿a0Ã, yjW¼°ÛL7aôÏ|ƒË À cêI¹¢,¶¢˜§¦ ÆZhÒ8`vOgØÒcØãÁض0vÿÏÓ.·0\ÃkEÍØwþ|ÓJµÕWÌAE2d¾£ƒƒüûË<³ËÇC óÃ<³<ÐŒb0Ké†ú"$GÅ…[wìþH'ˆ¡Ç»Nù쫎>÷Ÿóˆ„‚´ÚŠ­0òãƒ<÷¿q%4`F$Á.ò¸G/áØE9¼ûø°ubÆцC€G„ m°#Ê8Cïºߢ[hžÛã‚›%®JÈ ŸŸ>ú_ϸL2ÏÆš)†ã\ôÇPË< ÓΧš 8i‚çͼÆ^M€Iešî&«\+ã?òÓÓï¬1!F&ªj©uqæ,E"?4˜ö_¯Øj¾R†yÅòÇ^Æ þÃÀK,ë#C šz”C¨’¸¬’8ªªz¬¦ï¬ò„<0ÃÃ>û, 2Â8Êå²´Qæß4IÓ:( E $âŽ"íÓo ?Ëš˜ê¾Ê87îÀž?r®y¤¾ø«¾Ú ¶r…< ->ú䂨î¾ÁPåS(;ªO»ªiA¿¦eiÔ.'LÔ‡öRD7Ž¸ä‚ c¯— q6!Ži+ÔC¨Iô0­¾jN~É«ÞÚ€˜jš8¥ªÃÇñÌž¸%’¯]é ÑÁûVÁãèË„HÝ7AÖÅQÇPp°Þë²›î¾,pUõpÜsÚQ‰Ë”ûAçÙ Ê" ðþ¾ûà†Ø¡‚™,¦È¤ƒ$ÃO!qY¯\ïñº=ë°aÇ}Öñà5ÃŽQEÛM„sÌ4ó9Ÿÿ¤±ÇÑ×üô·ÏêAÖ:UA8$ýò2;ð=3Å¥OZ™°t˳}8Ã_sÏúçŽ9gþùä‚J1£ïÜóï=s뼠뜫x‹ =‹O}þýÿ¾ü<Óïÿÿÿÿÿzï®yò‚ £(hŽ Ï8c¿ÿûÿì3þûîî <އ˜Aûﺯ Â7û¼±ÎúàÃï¿ûÿöûï¾ûîþûê‚$²O?þÿï¿ÿ(¦WøÐDÐÒDÜ!À†´˜@Häq¸oû<5ó“ ¾jc¤g w‰þû›{®¡Ô8+0ÃÍ<㬰ökïÿo?þû¯¾ÿü²Ï2ùŠè–ÿºÈÏè3ç~»´Ô˜÷¯õÛ}7ßM4‘E…ÝÏs‚Ê–:Ç¢æPº¬<ø¡ÃjÈ.ö <‚ 0†°† Ž6ª m<À[K­Šë|º‹#-µÖï;ãŽ?ë®ÿÞ}sß|öþÏ÷Ÿ}^´ÃCuafyáˆÀaû9+ BÞð°ãD þó]˜}{Å%[Wøóßî3Åß}¼=ÿ¸Î {ò%WÐ ¾éU5—ˆŸÇ~M/8I\b‚  ãË -¾ë¬òÛÿç)àóênš(º6šgÏ%ã—ÿFËA×JqÇ^}å]4¨‚ÊÝ÷ÿôó^!©Êq9;Ï_>/‹._Ž}œÈpL²Ëoî).¿~}ÂNÕQþùꈰhãGÚúÂ!ÌóÍ8ã ÑÈMwÔQ%QY÷ßA¨•”§Ý÷:ÓÖ5ÿn¯™ßs¼þ’}}óåØY…xÓ{ãûŸë»¬c®M|ûŒðÓŒ1Žú#1Í<¡8ã m÷×iÞדEG[mFÖ]%ô–¥šî`Ž|³¿¼´ì¼Ï·'–8á³Ã‹9µË›oœO/¢•ÁûÃ(3ÏN0†xç¾ÈN0òÀ0aÍ-öœïqM½Þ!\pÁ„0I&ƒn)£(¥Ž|+HŠÃŸ<õ»î¢Šf–éª(á„ãgºùžt[°ûιþ=Ö{Ì5ßìøóŒ`ŽK# $ã _0Â0Q>Km²É£®øÅ‚t TñÝ‚.,é°‚b< arÿ 7ž{÷žõÏà ïžûøãu硊/ßü0A?¾ˆA„"ˆ<ñÏð¿àÿŒ ¾ðwÈ€\ð烎(¢Ç{×â/ßü?ÿÄ( !10@QaAPq`±p‘¡ÑÿÚ?¨¨¥)J\_çïB—£JR•••”¢Š+'ã@‚)JŠR¢¢¯áogKÜR”¥Qeê)JU¦¢¯ê++++++++(¬)K…Ò R”¥)JR—§t\š§ñô¥Å)KŠR”¥ëO©„! üì!4LÌ%­¯ø"„xŸE:­?–[ý×z·ø¶6QeéÏÁøÒ@¥)Jµ^¥èVVR”¥.nnj)JR”¥X¿Ø.šRë¥.º^‡âo^—½…¬þÚ—è'Rák|´m#Ú{Oyï=‡¸÷ãÜ{aQW’¢¯%^J¼•vô½½/Yk}¤r6ßIt++òVWä¯Ë++òWä¯%yeyey=‡°¯%ù(¢ŠÊÊRâö“]/It¸N“Ù _zšÅ.*Åî©u^ªÖû7Ã\¬ü•••‰‹Uìßn}›ã²¸ºoNb-ìO²cåöº®iqKÒ¥úåí¶6ÂM@â’I$‚08F¬?gìýŸ£ö~ÏÙú?Gèý£ôW’¼ç¼”VB¡K¡õnxw¢t“îÞ»Ñ\.ƒìù0û4\å4ÈãR”¹¥)JR”¥)JR”¥Õ{§›®ê\hº©Ò™åÇŸ&òY_µâW¿úOô‰\•ß.þ —°OeænyDF×£ä¥ë€t…)JR”¸¥/B”º–W¬´Âv¼øâM_Ÿ§þýþI'HI$’I$ áö%н¿5–Ye–Ye—ŸôÒ?h½?ûEùGí´~Ñe—åYxß”~ÏÚ/É^OÙE_’Ëò_’Ë/Ée–Y~KòYe–%0ô¯Qh½½ÉÄJ–YeQEQe•è¬öYeèöYeõB„! ¢³RëZot8ê¥.›ÚR÷o°S³3;_†O±O‰ ¥äNŸ1l§ |6LM¹ïàm¢ì™FäöR”¥`R”¥)JR—¤ðùëóï>äÄÈöÁìŽP‘ì=ƒ–ö%z?–{½Šìð4~L›â”ÂMÅNpÊš/‰`–?†|ÞžÂMï”4ˬ"”¥)JRè%)r^ ‚”«/ãêüV9JÊR”¥ey¥++/m1ÊÊÊôîR”¬¬TX|¡ð>ÕóÓ{“‘HBi‹0š"""6"""ÎÙ‹f""ˆH„ ŒA„DÏüof.E¹E”YE…V^° ‚4$‚I$‚ ‚"!""ì>>¿½ëß'"”¥)J^€ \”¥)rRô€AJR”¥)JR—+¸hBtßC“-\o ›íí ¶WXÜ~ÃpIÔGHèÓ‰‘Á&4dz#'ý¸„÷"àÞ":-ãÀ⟰nœzŸ‚?ÁD.¹)JR”¥)JR— ‘:ò¿×±ðwŒ¡r¼1¬ô„¶M³ä±y0ô½lÜMÿ´wðT“aJƒy!uþ$2='J€¼A6ìUïì­êW»òö¶Ðٕ䯰¥)YYJR²²”l)ÉØø-KC×zTo+„!B„!B„!5„#!1B„!:!sŠ/ŽÌ ¤ô¶R”º)qt<7‹ŠR”¥Õ¹¹¾#!ÈÈÈÈÈÈÈÈÊ(¢ŠÂËèÜA„"!>0ß]r»;¢âéx©A=*¨ ‚66666****** ‚)AJ\.JŠŠR”¥Å(ñóýì–§Ï^—EÐù‹ R”¥)JR”¥)qJR┥)K R”¥)JR”¥)JR”¥. _ï`âé!ÙrÊÝ¡JS†Mö&ÒÝ¥9A¨ìQ£llÿ‹ÑAì÷¸©ÒIú·èŠ Oˆ†¶Uäm7_6ñɲjøø¥^—ÀÕè½ Ò¬¥.)JR”qã‡h—Búw[çE¶†#qTW–ßHëu蘙„ÕBf„! ¤/ öeÒ¹}“ÇÏF¬Ý4¥.)KšR”¥ìv6***#&˜ð¸¯ËJ×cÒÆúo9Üß1‘‘‘‘‘‘â„!B„&b""ì!M!Œ\×ùjáÓ^«áa‰}'mÍáðÇÀ}©ñÒ>·Æ"ã± •Dd~ ( ‚Da˜„Äddd!N´!1Bg›ÃáŽÐX|>•õ¾0Î ¢h¨xQ/SL„devÇÿø˜„Ä! ˜BL>;Tº1púßbÔô„„!4ÌllUä¾G°÷ãÚ{t…z ðO‚|a^ (¢ËÆŠ)YY¹¹ÈB˜}“ƒB䣔۲\aè  =‡¸÷æ{YYq¾c##7#!ZB"i¥EEEE)JŠ\3ÇïbKe¡g–—Ãì. ZˆˆˆDDEšŠŠŠR¢¢— R”¥)JR”¥.iKšR”¥)J,xì\- ã––>¼ÂÃ%)KŠR¼ÞÆ´¹¥(±ò» çJÏ=O—ج£á²!ÎW‘¡T"WolWÁ"@¤t„éB„Ä!1B„!15B„de +°pbet|ŠçBm ±6°ž¨B„Õ «b¢¬TRõf!B2+®#‹J]>Åaˆ¥X«ÉQQsJR²²²¼ÖRâ댌„d!B‹FØØ¨¨¥))JQsØé,ðíÄ=PŒ„#&!1N•.)JR”¥)JR”¥)JRè¥.W=ªY|jæº×+‘ê¥)qK¦ö×Eè\Òå=ò¾G׿š&Mp^;'”«©#[Âɸ¶*b*ÐÕµ< <’qH„SÙBtc!3LÌ„!B„yò>s:‹ÏV›û9[14‹Ýi•Ú_E(N ‰²³|B„'R¢¢¢¢”¥Â”¥)J\^Šùì8t_:y»ÕEEEEÓYY_Ó!¨»A-/+[²â'ÒÂ222ˆÈÈÈB…HkØqh]#U¾®222„"òDDlllllTTR wÙeaEe}Šl´-dÐùìSÕLLmäØØØ¨¾‹è¥Â²VVWŠÍÍôFFFGˆBbTî8hH°Sߣ$nΔ¥.)quÌLÂj„úÔ·BÐ…pÇEÏÛOà£ðW†zP[JÆÙz’u3¡>¦ô£ðWŒOH¼9Åä/ȗȼÙ?Iê=]4±^¥Ý}´~èg¨ôç=Åùù=ÙŸYé=g¤ˆÛ¢ñsJ\^Šbi“/ê¨ÏAè=Z¯'è$yÉúOQê"'Z÷·)´=)³«3G õ³×¢Šò~ˆó’ôž“Ôz…ö·¢ˆõ>^”lzòžÂ¼Ÿ²|‘ç7ë='¨ô"/‹øׂ[¡£i“G.¥Íï¡:·½ŒƒÔ, G±Pš¨ƒF´ùÿÈÊ(¢ËÑäŒQQ ¸¥. •ÊÌн/ÒÂ22Š,½H" ÿ‹粊(¢”ºVXÍ2Š(¬n½"$½{ú]s1Œ¬‘aDFÆÄ¡ÙeVR±1ù‰ ·µ¥ÊL3@ƒ“W5‰ß!B„FÆÙ¨‚䢊W›ØO‰ §®-1‘“$B"#clTR”¥)s¹ZŸ(FB„D"ÓQsKôpŒŒ¢‹Ñ ‚"#cb­¹¯Mê"/z^š%ZÍ$$]µ/^2ŠÉú „DFÅX¨¥ÉKŠËôQ#W¤ðGœ”]e±±Qbôã##!B‹¡JR”¿KzµùÐõ&Rè})¢""-T¹¥.»ÙNêk¥eËÔš!5Τì.©ßÞÞa®„êNŠÌ!3J]s½¥ìRÕ4-»<®5=+è•—•ÆW=ÏÿÄ) !1Qa0q@AP‘`¡±pÁñÿÚ?ă ê:Ž£ ‚>2B§„""àŽÔuAÐuäê#“Øö+’³UAÔWAÐGÁê!DANAÕΔYYBº‚¸#àŒýˆŒ…I$ANAXl¢Š(²Ë+Œ*:ƒ ‚>ú9„D!DDAIÐu_—ÇK–ùhØž[þ ž¾ R”¥)JR”¥)JR”¿*þY¤$—̹îk–áÐT6¼×ÿ‚!2ÂcBDÉ' ‚ÈB„ÉLaB„! „ø‰xa1Ø<Ëu…ø[‚I)ð¢"à‹„EÁÂ:„uGQÔuGV¨êÉDABx)JR”¥)EšaLû™n/„•i _.i„cD!<-à¡1 yöø–”¸¾ ðZÂBŒ„Í<„°™¦§|'Ñðç†x.JñkBy&3$$!c ãܳ¡x¯ƒrËÁKð¦I„&3°ƒYÆy& `°„!Oïy©KηBÙ` ó@t_EôWEôQEÁEz§©êzž§©êzž§©êz“ÁèGpG¡Á<Gpz I'—4!0‚X!/òKá,eÐÞKùT²³wÀÞü á¶‚‹¨h”„À¨˜!0„!0LÁ ‚„!B„! ŒÁ¡¢yÐÅøž/<Åð·ÇTHH÷{Gu¤r$ö×ÙM=ØME¨•mçþˆÒQÒÍ ìlÒ…ôRÝ#ìmõø/þ7Ú„!B„& t÷É™mØP˜L!B„!B„##!B„! „!0„!1ž}çØº~,Øðú CŠFM)¢Iò88¡ ‘SHú4$i¥êÁèà’–ŠV‰±®"Iìþ7ÿ¯Ã0B˜ï~…º6/ij¶ h{оÊv©1¥£A3ÿ¸×¡lÄ•¨ÓêõºØÍ ú ›~v"Ú•#QúPojÜh‚é MEq•v¶)¢´ã^…uR4ºh'vÒpDB„!Bä€ï<6z…²óìdŸÜñûDDXE—LÕaQ¦:aQpШ« ŠR¢¢ãJв钢¬444™x¼ï·ÃžnÆc¯&¼šáB>Mq²2¶GË#ì׳^Í{!Ɇ¦¼šòkɯ8k…x3SöÊ6Rf{3ö(ög³+²Š/ µà‰uŸrù †ü[IÊA$“—ì²ðÙeV`ª°¨¢Š+++++ò­ñ· ÊþX…á¡e—øø»,¿4ø!A,þß'™Òð¤$Cv)PÚ 6ŸÐÒ“¢šmÊ&2à¡C]WQ•¼4m?B 04ò.Â̧°èf¥–ÌNŒžBFÐ3ˆ;N[¸8†M8]!Í·ÐÔ?¡=ÕiY_ÅF¢^Œ‚„È ‚ÀÐŒàAÇ¿‚m~yáK2ÂMßÙ¶m:·ÓˆÚ2îê8eíÚ^¿Fé­%Â\­ØÖëÔ; Z«7ä%Û­ÒyA³íì¿à‘=7OpêÛz5œi0¸lÓ„Í  ùÝš›º2|hˆˆˆ±EX¿îø-ï4ð¥ŒÍJR”¥)J\àO$ó”)YYDÊz1 WÃ+á—¦^Šø+++¯éO‚¾ VW‰Ed·‚Šëf²gÀz·á‚&IŒÂ ‘„D‡§ý=?éëÿOGô®ÓÑ¢=ðôD] ëøO_ÂzÂvNÙûgìבûe콚rÍ9f†œ³ÙÿM ‹Åf¤’Îâ^%+Á3v=EÓÎöyš¦Uža1K:ØXA—Á|Q^0ª€¼Š(¹22222Š(¢Š+bWŒ¼4QX!ÃÿiKåÜÍ·• 2BÆ ,Ál±tÈÄŒàB„!¥HÓ•¹0„[²„!B„!Bž0#&-ëѱyÚ6f¹‹*DÈ„³­±z&Äá=~†$£èÕÐ¥¶J‰á¡'¶t4H§Ô&]íwOb[a¥\¤i&­1Äßóa5×u1U¸•N¡ZöÅeVž©=CSWŠDÊæžY„Â.Ø¿úÍžvÍØó-–H/ Ȳ*Xt‰2#lÐKšj†·Ј¸DCÅZŠŠ9ˆ¥. R”¥)JR”¥ìŽJŠ_IÌÃÀ_ÜÉŒÃf(^e»Å”¯†kÃ?Lý3^ì'!8¼¯éû_Óöy´NÉßü'ðý¿ã!=Ÿ±žqÿOOúðz/é`ý0?B:"'f††ž pŒ 1`cÕ½‹Ïµy-¸¡/ Ê·x-°ý“¶NÙgý"'Cô'Cõ(¨¨‚'†G ŽBž…|"²²²²¾ŠÊù5åšæž*U)pÔ‘´àûûø¶mŒÛr/Íöño<”¥)K–”¥.4¥)J\Ô¥)JR”¹6‡Úön| Ë6”ÎH‚^w ©(‘ìÊ%c†E&Z"K`•ši¥¹¡QXNdrIWàÅøR”¥)J\Ô¥)JSjô-±¿à6cj³nÁ|ZÔÆ™Bº…³åŽ‘“£TÊR¬¥Yb°QEQeYe–YynÊ+‚²²²¾xD-¡¹ü ÜÈYv0™×‡rÁ &+— \ú‘ðΆwŽñÚÂta׃Òw,jù/’ù/ìIœƒ¯ˆˆÐШ¨¨«>ÿOà)­²2a±•¢x¡3îX"yåŒöÜ%ÿî:ßÜJ:‘Ñü 䂈ˆ‰Áp„“é`¹BŠ(¢ŠÊÊËŽ¤!V%‚û~¾ µgÔ™v°Y§‹¶ ‚”¥)J_"áJŠRŸ£^ x5àŒŒŒ’cÙ‘‚ˆ¸""àŸ€lÔþ òìe„-ý¿‡z;°§ ñ…|QYY©àoyŠx¿ gù-Ú;‘Ö;aK…\£±« {=Y\ËÊSõIÁ’:>é|Ïa¿éÔΓ­„—Ь•¥)JRáK…)J_ˆ÷y£¾˜¾Ë3éõð¢gIÒu‚. Š‹)q¥ñÒ—ñg™ÒeX¾cßó*åÇqØ6exk‹hÌ×ói„'†®Håc¤vø}LŽ¢øÞw…|¿HW‹ÄÌõ½þZ®QÜŽ–³¥á:à+¸|˜ñØkÏËB ~²Õg—àêåÈéa»yï:‚‡B=x}˜'ÎäkÎ0„øSã´šŒtȶY_P¼W5\‡Xéa;^Ó©áX|Hôáva»Ga_?Ÿ¹›FTUØó­â ëXØÁÚ;Ù\¿ò-˜‘¬»?á¦àTTTR ;²%j«Æœe™ã Šˆ ‚Hã'²‹ˆŒ ‚Iù&}´eX\¿æ** œË²°.Qy©""ñ¢kpGP’K"ª º,©£‰¨¥ ŒJ+³\QY4`‚""6€n«±ò^7 RàÑ£M6ŸÏ©JR”¥Â— qŒ„!ˆB|(jMP¢¢ÇJŠR—e++Æ222„!²ŸÞm á¥**”¥)rB„!BçÔTAb\VVFFFB„"&Iç}–Ï.Öm@èÉ¿hÏsÏQPš2¢”‚1+)Y©©©B„ø­Ä!ù䯓c2¶:޼J¹*ÂÅH¡8˜J/-)JR”¥Ç\aBð©òÅ‘n³Ï-)JR”¬¥ÿ"æµY)~Uüt!BàR¬ æJ±h¾=BjgŸ€„!B"/EEX¶éDñc¦¢ÊÑ„’Í¿â~ŽÇþ%áö,^ [`þÿÄ)!1AQa q‘±Ñð¡Áá0ñ@ÿÚ?ËÃ,³½rèðTbNcƒ$XP­YaapB$d«WFB@Éx†H,àfìwèvÛßã\ãlˆººú2Î]ã~›¾ ‡è!“¹$GŽøÞ{ãaîqçxÞ7N4ã9Û~á';ÂG ÉÿŽFOÓ·_Ný;ÏYÂDÏÑ·\íݶ}$ÙׯiÉ’YÈXï ‘»mÂJ‰Ãôœ!ç¿G±ùã×€ú/G®ÿ­=HSû¾aê8zñy#ý\zŸŽ•ä¨{•Òûо;ð®ÿVð(Š~(òذ¿<ðo‘hò3FÑ,,8cd3ðç pÈ{LÔÌ’Ë’Ë ã,îÈ‚O¯¿¥ã¸xŽ{áä›X_£87¶^ê'’ëŒãð?ðÞ=9êÎ2CêÜ·‘åñéêN6Î=~ž¸7‘vȲìafX‡K¤‰ Âvxïì½8<*‹¨P­÷ËnÖ&Ç[«çEy|öß8Ïqº ¯–û‹þò)Ÿf çôið…ãò¡xÿ>ÇãQî`CøZ|ÿe¹ý_1êNõ¡=Wâ¯SùÄz¿Ûó<è¾'Õˆõ%õ_ÿ¸õ‚‡´g¹!.ŸIg¥ø .¤'¡|WæÓú7”?f…øÙ,°Ù « ²K#€,˜–‹,rÆxe’pf<ŒM–q—¯ýI¶},pL,±ËÈÿø¥Ëaž7‡èGè-úWœã¾à£{çm‘ËXeµ>më¸y,±–Î4Ÿ3m¶–ÛÀ¥°ÛÂÛm°÷oZZ¬²¯ Z¶¸êÔ¸énðÓmÛµjÕ¸R¡Ý–Ðt·tûÌmš€éQÞ^ú³îlù7xç|Lû3ìG·¿ÂžƒíKuýÿ˜ð¿£óÜ‹›Çä½Çü#.¿;}*ÏV |µø<õ?ŠWòÃÔ»Ôþ\=cZ/Uê~õ#CÜ“Ö:ò ûÀßÐÿž(¿ õqàIÑ ö1é§Ùñm$.¬ˆ3,,±²NEXÙe«,“Œã$ƒœ²È>„å>œÿóõÆÏÖñ¼úðÃÁ°ð¼%×:Lo'›xc°‰†u!»º³7èYzcl‰í™ÞN¯~§xÕ&Ø`µaAô„bõãyxm2Ó†ÞvS“žþå·ŒŸ£séÞ;ŸøïѼk¶ÚÚñ­¨\7nÖôß#Ä{×¥±ÔE]¨? 1ͺ %Â,{KÁ¢Ä1­é±.×U܃×3é'è]ÝÃôFô¢pý‘ú“öŸÏËCº©_‚Ãhzõ¾âÓöOõÿ7¨xv?&{ùx{¯å~Dõ?ˆ±ö4÷ä?ö¿ÅØ‚X„Ãô¨sDûoŽK}ä>âø'Yd–;%–Ye–Ye%œbÉgÑÐï¼)ôeŒ]ÛÆÛ8óœ¬Yo’ιÎs…‡FK;á8.¶îVÈ·ž´úwèvë8Û¸³ë~ã~­ç{ xÈãmäËgã×èëÿ'éߣmåú6~mmm·†­oKóëêùÛä]žqï`L#´ ðPžØ¢8?Ÿü@³÷rGv½f€zƒÓ ·<3wX1ÛûÞ£HÀò uú _ìñà"CýqÚGÞ13_Ñø´õYèÝ”}‚xïþžñ†¯í%úÿ‰ë~?çàÊóùaáü8ƒ±ûÏu÷©á˜ðŠxoǼ%ÇAh¢Ï¥óeœW{ɰ¼úý'…¼o;oþ çþûÿ‡Rð(Œªë;Æý;μëkkkj=%¾Fùå@z¸€Ì¤„è“ÙÈ$§lìŒ~䪱¾.Å<Éf?0@':´ØfòßðCÿS¨>}™ÚñXCOÄ4Òc³:§èTެG…Fçæ!hù|ÁXÔ¿}zßGóŸÊ ãø¯_¢ 1rzáô#Öþÿˆ{ÚǨþëüúúŒx â“Сn›íBx_°cÚÌË^éZtÙÇR"K$²Âλ»lVH ²,Agpw'vHl–i&¶q’rË,º²Ë&D²ël‘É,,³†GŒvM`ïé7rK,‚ÃŒ²Î2Ë8Ńÿøôåç,ãñÞ[z¶Û[[má¶Ûm¶ézýcigþ#ôoÔmœíÕ¶ÙÆÛ–¹À’Ãwm¿NÛ °¶¶ÚÛ÷M {«ÇmÂðGÂÆÛ:üŒù>`]t¯¼é®·o}ÞÛÝ^d5™]Ñ… »s,/ñˆñ?ä”kþh:?䞪cë 2†¿¿ØXô럠Qœ>Óù&:þÿÌs‹âÌ_7³òÓëÿTG§)w[ÆE°öXçÀìAÇRk¼%’IÆY#ôçÆud–YuíÈY%–Ae–AcaÆI’Ag ÆYdœg9ÆAeYgÔ–XóŸFYÎb¸p?B‡ÄÝ“’;cÎ7|7¿¯~Ÿ«xîïÿ—޹~£8Ô»ØãTµØÎíÃéYߣn­>‹v]K-äM<™Çyl6ÚâZÍ®J M®:Þ:0»K-…Y °ŽS¾rK'œØ3Œ²ÎÆugôaÈ$²ÆÄÈ$²H8ce’w=$²Ë džYbM,á dã,ã,²K$³Œ²FË,á$[bàMZ•j]¹º—¯R7rû,{gˆ1d²Ë9Î3‚v6Ï£8 »>„·Œ²ÎO¯¹‚ï†ÈxqŒß¨’8ï>¤¶Þ ÉðKuàÙ‹½x8vÞ2×I~ƒ„`’ï—yêx2H]óh»ÃâfȺ³Œ›ÒxÆ0³ŽòË “‹ÄÇW á¼g†Ùd˜pÈÙ%Ù ¼¶Ë LÎ*ˆDµQÀÌ&gp"–zo#̲I }m¶W†Œˆãuo [i±œua.‚É ,³Œd°˜YÆqY¶YgÂIwg,ÝØØÝØp¼»ÆðnEŸùwÏVñÜLóÛÔzýÔqœ­…ëm¶pºëa³cÃô iô>Rm-‰ñwÎñ·{ÆY;g ëß-šq¼œ§k#¸Ü-ön°“Ø]“haôw€ŒÙ8ÎrË8Þì7`’ÂÂÂÂÜ›$äÖÁÕ†rIà@Ødõ¼ pÁ0iÛõõ„ÁÆ uc›Æòe¦–Ðp5âsj·áEã/‰ 7hxg\3‚Ye’w%@eœdÆYÆwdYe‡8ñŒ qÃu­œOÑ–7¡¶q¢Ùb} z!ΜjNðøo³gÃÊDpð½< È<÷¼ú¼¸Ølœ s²[<žs²ÏPÙílö6Ýà>M˜~޾wIUßo ×›ã6îmåì·¨Ë'a-RºØã[ ɺÎÞ;ñ³HP÷$;L°õÞCaØÁc™Y añËgŽ Τ³Ã•yNÝÁœ‘|yÛ<×…§9R³†³†íð©âU®Îðµ#(!6 K;’ éce¢N %˜YgPpp…–XìHYŽX3ÂXå˜Ù½M˜Bý=ì,%—V=&[Á=f+xß@´ï¨†ÒH8뉔[Ó83O<Ú˜;‘[@´±ÙÐHÛ/ “0EÑwÆ]eÝÈäo @ã.¬m™w+,ñ£oª°–÷l&ð<6ïm¼g,OË.¡ÖÔ:2ð‘—\¼’›föp¥“0ÈÆhì³·ZZ“¹Öe¥áž†ÑQ”É“;7ƒ/ž„Þð)Ǭl»; öÄâ>…kÕç>»ŸYñÏ™S3~ͯfT+Víe¬‡Ë»vø5w¶62NøÖÖÖÛmá«eBÛ•ÃyÔMõ‰²ðÞ Ù‡W†½ížËr¶WÊm¶­Z”°Ë\žÎDì7TQDu §V|i²YMÒBðzË0}ìB;‘´àì”c‚õÔÇX¢-·%º/[¿eØÚd¶Ãcdd½Ú¶FïRD=Ú…ÖÂç(Y¥á1àÂd·$²Î; › »]"%•pd-d^Ù³=[{—Û¢ÖÒoh#÷šy¥ç‘['>ááoão…¾7gͬë÷ŽãŠcäàÙàÜ(p­Âº6v ÈëË[8 vRÞÄË{6ÎY“$f¶2fÖïÇwñÚ.šÙ±à´Þ7†nä6z!Œáú‰1ÆQ»'àKI”&Yáú IyÙóΤ½Û¼ –÷{GÑÛoÐ[ɶÇ) zñ·v¶œm§,ãzá‰Î6ïë>¥ m'„C_¤Ž3‡uWŒÔámïã¿£³€«f×®ÎzàðÆðóßrx² ½‡éÆÆÇèÎãCè3“‘áçzŒã.¬³"Þ6ïœëèlêNå/'\&pÛŒàñÉ :}§ÓÖ=]Àe‡A 6]ìe Ùa®AcÄ9$ –^·yaÞØïqÆp ì[uaœ¶Ý} ’¼OVuœ+;3œ3+?@F̼ïÀ6ñœ3ÈÎç§eî["Îø<ÄfLF!yêÒxÞ^Y9'žwYyÉ8xxÞ}YmãN¬àqµxÞ[¢%mxØúËÖÝzúœŽ_£a¶W’6ǃœçÃÎ@²ë á9Éìäºàx;{$à‚È,›²FFÅc•‚=À‚ØólœzéÆX@Îq³»8ΠׂÄíŒÞƒx~ÇxmºY™ÛÖÙ°·ç©à–K Îs¨.´f7œ7ƒ2}œä}ör2/^›µ×áÓëN['èmW…¶g'†>…´á[e”ä–ô´¶Þ“ú=yÞH^Kdñà°ïÀÛn};oÒg;‡Ð˜ qœoÁvp}YdyàñŽ 8,,°÷ã,޽ l³ 9vœàqË0Ù ã'ŒN5Žn¶YrrìxGxxIË#‡'èxõ˜ä1ºáp.øZØX<î6gíÞ>áú ³Gc—!?Oy=œõËÎ[wwo) í¼o â0€YØ}çÁCïCïC_Áøç}>Ëð¾_á>ëüO›sêñ éþ)ÓÁ>·àŸ[ñOüÙÿ›ÆWñOFþOÏàgßÝ;»ç_£çUò¯ù_"=Åó#Þ_"ùß_|wÀØàãccíccwÇp²¶¶Ú¶[m×ѱ¼çm°ñŸG¤Cß=e–qÞav¶w—§eœœgc!%ÞÏNYÇv7|<;wÏVÛ ²Xñ¶ÌŒDg›-f;x$ç96ëxI͘GR>/6ÝòôD{ž1gΫ9uÃg9¥Ý¿BZý^·žxYyXe‚{ãü¥2P¶ÛcÁ•né-½HlX F@]›A†Úñ­¯vÞÃ÷K}VüjWªÌDç±#|{ öW½ø‰ÿ”FážÉÿŠ{Ã}–NŒ½ÿÀOüÒ^ »¯S3ì/~7êÆbw{6¾êíÍ^®D|,é6SMy™xæ;ü“Ã>ÊDŸ"X÷$sÏ„‹Ër¸5hm­Zµ.U¤áK × · K„  “Àcé\y.›ð¤Æwh–Öò2ý^²Ý³Çv}-’}’Ž:ä'7®3¡õ8@°cÍœ™ Mº1¼©,äçq¼œlälýd“9ÊÆ4ù@@.îçw‡VÉñÆÆËÃŒž×rV-׸”‹[»XQ•1–ç Ëj]ìË6ºÂÂË0Û­½GykyFÞ5¶ÞøÙmêVx^ØR×mµãm¶xl®¶¼3mµåxxÙOüUxNøÉ'¦EŸ¡7-’8Æî×èvÔ—¾’ –ÇdNJðReån­˜gqÓe–aÉ`È9gs=‚#KN;²Çµx:^Dg#â×—¯­àux뀃ô¼/Œ·Pxxb ­ñXlÞ=,³ŒI™œ0(gܽÀûÉbÙÆuc`XÉŒ1·wl–L’igi<°-‚N³„2 :áµ álê^-·¨·…†y Ým·‡¾Clã¹á$›$~“ŒÉ;­¯eŸ@9gZe¥ÖÄ¢Êñ¦rÜ6ð.rêõÒx2xNùBÈ8!lî$v`W©1FÉ;È,$êÈ šÁW"{²±Î÷¼+:œº…àg‚vDáxÙ9ç-Éç\Îwèß§'„°YÖÀ ›,îI5$º’BK$²C"ae‰a²¶¯Æ2;'r:ØÝêMšL2!gEé±éxO´ €FC6 ‚mw©M!ì¶ào”jöq—XJ'¹ÙfÉYl ·©…¸Ÿ²7ìˆÞö²›ùõZî{…¹Á¤h=¦h`¶S…ãl’6I—¬Œ¤šï)wïÂZ}³Óadx#µ±áÕáٺ埡Þ7™ôI“,Ñw*39¼<2Lœœw¼aÆòLgјÏ9=Ô¢qå„Èn²|pdâK8 BOзSu,Ç ô¬øåË·Žå–>žÿrN¤›:’ä³$€’Î3Œˆƒ¬“]Ë&;l²cd‚Gc¡vJmœgwPYÑd˜w²ê¸ñÿ ëVt³„NÛ'Œc ¼Ox¹|íßaøƒ²½ #¤ÒÝc0E÷ýìáb?i™¿‚@ŒjŠ Z“˜'íÓ 1ÒЭ=>RÖúçÁeYïcÎ}Ù(EéaàêŽ+>G|)4FŸÊ’LŒÆcïǬĘó–k³dXpŒøá:Þ8ÆîάLm¼ÏÐñ·Y',f˳iià·*6nO«­ÃaÊêàI6GC”ãxë-6~žž7áñ,3çèÙ~–l¾ñ$ÙÆO eœe–#'–NØù&δ›,Ž2 ²I$ÅŸ"lÈ-„Í`&ÄìvV6u%IÞIÝ®ÄÀY§VX>eh!Ù˜§´ ‚'€Y'ƒìÄÞ“‹` Áœ³¿¹€Á žþƒxõRм¡ƒL lnÁgJÐÆBˆ^øÙ4€&Ò~±7ÃüŒ°†êG| ü`—Øš5÷avxÂmF $³¹ƒ¹ ÛÙáµ–3ê`—$·plç ëôŒ¯ Ö.¼l¶ÊAx¤à–Y‘»Þd±„ÈIvæYeÑdèÝÙxvlá’­±BÚÞ yc`ƒÄ8Þµ»G‡¢Ë:áË®=á”»ç-—éõxvÞ;ÎW†yOd–ugvM—\aÁ²ÆÉ‘ã&K$°óÊ^^ ˜2BÌYƒ‚zãI5½H;>ׂE“²tŽÛ¼É:$²N†gRìÑFE„eEè’:€ÀÓdPÂÑ} d­ˆÕÒŒ0a,Daˆr\S‘ûÇÜ$’1x´ÂÞà„£Ù†—²FâÊû–•ô–]Ÿp†fEc î÷ž]²sÉóÕÐY¤ž8É霜³žç…‘Ée8-åµÇŽç—w†^åá À”‹V«¼ ½Ì2¼+1#<$8S: à˜,›¾ê¤ìéuÎHgèL°²è„ãxØ&;gèN“ežIäS޳ëYGmñg0]XÉqœƒ¶6Yßm’@A‚N ½ðÚïËÐÉÆNY2Þ“Æ9,Žð„Ûl’Þ“mŽ[Æ÷9-¶SóXËfØÂK äWŒ-Ì$€²2q0 R@€ê:Û¸N¢ó#šeñ¦Iàø.û,n±ã'„àì†O~‡“§ŽøxÞ|›Ë“ËÙuÏ\idÈgKÂϽaù„,ã‰I$²È,‘ì ’ :èvI$°d’$l²I,C÷°`ï€o “7=’|ýéûÙ=î{Àn¾#T>ßêx}¬l`˜$í᱘ ’Ë-‚xNøË¬›ÄOg ÑÂ>8O êÄÀÞ²7sœ' .“6úYlÛÊ]l¸K/ÈË-²ð­ëJÙkvųٵºÛ 9Uí´.ØË w$XìíŽÈÏI–wµd7¯ w$£aMF{·©9œú¬ñ¼–3© Ãõ<g­¿F“ÙÆ²ñ¦;-ÒYvó¼™8x ôl™ÙkÏ¿ <û ¿Å8ÎäùE1|¿ò}\=K¡øsâü_Ìû6{×óYëñcëñÍ3ÝÙÊþÿï§ÄIþ§ÙPö–ö¿µñì}¯CüOÕ×ùbGþ×};y;õøw·1`ëÔö¡Ã,|Œž-üxOé:±wøw¨ò\y®2Þüô§Åþ,ùÁ—܉c­óÛ;c'{Ìú_juø’Zÿ…-¿°½Cóûԗ“N¥ÔòG^ƒ'¸¾âL2G2ņØIÉ ,‡2Ć@g,.¤$2D= ,"w…’og8/m’c+-Øðä²÷orIÙxЕÖ]Œȇ¹,eººã,l1VòaÀib@œ¼gHkLoY¦He’}rËmå–Þ2WIçÕ¶¹o ý:p¼FO ’< rʇÑß/Ø]þÄ™e˜mæ+3ÔS&õà$üí ä#Ü?òÎ_kð]Ÿ‚</ÿÇ>9Õ‘×ÓE^lÂûÈßn·[ößnûV}–w±ùlû?-œÏÜ¿¡oìYÿ½m~oéxŽ£ýï—ùŸþ™Ãµù¾Oæù_›åß7ò_*Ý_"÷û˜÷7Wnö0æW¼‡±1ðp½C‚1ñi¬Î=dYÕ/Òu#Ü·fíGDû|ò€F”{˜ÓÎ=Ä{È÷Èwîùwɺ3wϾW%?BÙí¶6ïº\y—ÜÚÊË+*•m¼TÝàX86dòÞO 6AÂa“² ƒŽñÈ'‡ [8Yo<㜠²±²ïãyÛ¼åóÉjÛÜ÷²³>~‡­áú\ábüËa;îÈ®¯Õƒ1x-oÐÇŽÍ9‡˜8Ï©á-¶6Üã¾z¶×myÞ5·…x×FVÖWaxVÖÛ[¾6Øm¶×2ÝúvÛcáÎpmUÏh\‰³8Àã à ,a `bX,@bųbNnÀ“bBÅ– 2&ÖyéNó†ä2G„$ë g`ëc&îÎ’†€6KSxË&oèO¹Žo8ÚÛ,ð²Ë¹É꣫¾q‡——Ïôžfß O£²Ig‰=ûRYÜC°öáß?XXìýY<;'ôeœeŸFsYgðýýOÓ²&oôm¶ÛÛm¶ñ¼¶Ûm!†Xa†z‰¶ÛÂα´¶ÞÙxxMaÎÄÆ#8 l‰x ªÎÈ2a †[…œ|䆖 –YvËgv‰™!À8é¡;2:|^sÖÛo-‡9¿@Þ2Â23fY›½ç©I´çyߥ>‡èœà–¯x@’̰?iØ=ZH,êË,à–iÆ6I–Y%–uqd²Ç8e–XÉeŒ+,äÕØÙ"#««’Ë-YVYeœd‘g9eœ•ã{e™Ie¶Ö[xo;gƒ£<â`r Û:Þ0,ëŒë$´YmŽY–Á#!0I8;¶I-Ëá %uáÖÇX8]ãLŒœ'€ú5·¼ú7¦Û9Ù›eyq70àË8CêÂ2{W€³ùSó ›¦˜Â=Ó‰~~#¾¼œåòã8Æ#ù7w¿XÄö.=Öß ëÛ;=ÁòØ‘ç)ïîRw«‘.¤¦h¥‘ú›?,³K„ôû¼ñ–ó·™RÜéð€Ýšéã]Ï®þ 7îÞ¾Ìçîµ—¥€KÛèùó¬¹ZwÜìÏÒ¥½êìݼ󽳌½Ï{z„¢í#w‰£Ñ»zÏ‹ððD˜àñ#>€ ±~Ž“ÉŸi Ž#é€$Xàθ‘SÃUóŠ~ž6sœ ¢Yd–YcvÙ&§9ÊHóÐCÜvðvzÆ„’$ë„‚ÎÛ:$Â,ãW[$’MŽÝIÁ‘;ɰá {« ËÂÅŸI—­¡:–¶ Ķð<9ôoÈOpÝýŸA&8œaïd³ß³û XBÃäÿ|´{F=>ƒð­Y–½·¯ Ý]—«­T!„ïy×Y³ ³¸¬ÙÓè÷Á—ÊM½yk ó<“û…÷‹§ôx·ü;y²÷ÄSÆHýùÙspd’0=#I¯ùë½ üÆÚ]ºòæ.Ýõåtާjù:ÃÁz¶{„zé"¬^‹Ü?dŸêß¼Âaò€Ï«ý?˜ž k]§¦÷%êv^}cb¶óަn×M]µ†Ä^ÚÛrØ\›—X[·ínÖ,;ríõíj×µ­„2â®]©v¡›V²Õ¯nµÈAD×\ákxøÛ¼38lvf‚n¥LFb²bLË^8e’9xM,ïk:6]aÜD ÂÇr *ðÂÀƒŒƒŒ±Û½‘É›,aDz•Zð,²Î“ ízN¸Î;‡†>ƒR9ýÁ—^6n¹Éãmá•ÌxÈ^1&2ÈúèÉoÅ#X2,ÛÜþìñ½„Ÿ7ü½ó6A™=1»ø{â´xö`®‡ºÌKë÷âx—ÅÝ÷n˜/Û·Å.re<_Á×Á6û|kÝv¥a¸\#x’»ãˆøâV(öéx7¼ã)àŠ{ºú=¸Û²Ðt<}oÐßÐò;3>S8J8¢È â;§€xO¹¯ŠtžBÚ]¹QÓÅjQyöxöÇÁÑ#Õ–HñœŒueÙcºZ»– ’džÃ5³«$ã¬å(õµ“>.à·¨ƒ†ëbÕ]ò®o.}ô§ÐPpI6C“æ[ñBXAb¾ÿ»uû¶ëõ…¾{îƒkÚµ®¶/Ú=1õ ¬ù©Ù=b½×C¸-ñ–½¡A{Jöµíaéj,Þ§Ú—ìËÜ· é.-|Jö—¾%Ư|J•+ÚW´¨9ÑS‚kì®ðF`@äœ1³€wdXêXÁÆ@ÃÁ=…Ù8Åz’ÎäÂQ0èg } gÒG\e‘ç„Âëéî[9xëúÒÇŒà9ëd-=”é£ûHCy¯šû€ î0‰3wÁbÄ›33fÄ!±b,mˆ fÄŒÄ, ŒƒÇZ1:€]Ó#À;ÿ¶ÀϹ4ê×xy€Þ§_ÂÝH'c°eŒ³°àag6:þõ=7ùŸú@鱃=<deÐÄ zŸ8AÛì‘<s Ÿ„ÔlÐo‹)âÏ}Ys¨=¬d‹>ÒrNIÂG±g6EñYO¥ðßR=Ž,q|S·ŽSÁ0ðM"DÃM4¤“ÀåÅB1Iw“Ú9"ücà3‡b3«$²Î0ä8bqŽð‹%’k©Æ„%í-’s¼6ǹ–Ûk¼,ó¿F'ï>>žùî7—…8wé†-ý€þx-4÷Ðן‚/ضkò ž?§ó{ÿw»ý?˜þ‡ûßÒÿÜÿ[ýïöÿ[ûßû¿µÿ»ØþßÌúÿÛùï?¼ÿSýï>Gæþþïoû7÷Ÿ÷qÿw·ýŸ˜êÏíüß×ÿÝýýÏ÷ÿ߆ÿ¦ÿ»úûâßê¿î=ì|ß×Üú×ûu{´Vröyë:òóÑœõ¹qÿ8Éb3‰Óö«:òòûâ×Á¿o±¿~ñ½<~<4ùÐ?‡2OO£÷¼÷§iÍ “þz"[”š÷Ô”ú¥å˜#ßæ*ïá‰í2|²ûsùÛ=ù¾§ç­þ3ßÿ §Qîf¾¯ùV‡Û7ïWîÖØ·o·¯_¾ û‚Ÿ%õ̾Úin4{ŠKx bUvYàÙmkbÈ$–8Aô##3xYa– 2%ŸXóšÈpޤ“Ì|ÚºgÆóù‹Y9ÞI† ]Àq²ýò…ªOÐqßÕ˜6cgÔ œ6~ÉIñd·²ýÊ¢f¾œ˜Ã&ëîGÒ±¬ö¸KÇ› 1‰0î“-…õ©ö©þÓþåÿ§þoû¿3ý?÷Ÿëÿ¼a×÷þc×þÏÌÿyýçû÷ïBÿqè[æþ•þãú×ïÞ¿{ú×ûŸï_¼ÿrýïWûÿxðÿkï>/í}ãû×ïß¿yÅýwÞ©~óêÿKæöÿ¥óÔ¿yôÿµóxŸí}ïí_îéÚžçö¾oê_îöµóÇô¾gÒþ×ÌÿJýïîŸîþáþåÿ¡þg=þ×ÞþµþçËý/½ýýßÑ?ÝýsýÚu»ãú5¾©Ò3/<*Î+ûÁ¯X}ÆgéùÏS?(ú¾óàü™ö&õŸÊÞü{ÑÕ°ûÛåß6êÍ_7ŠXÎ÷e(y!äZöJöJö!BŸWŒ/ɯ‹~Ä£G%üZøµ(·ñoâß¹nß´¬QÓ“œ _vN¸x @‹d#¹8Éê2ÂS ²Ë$K,ã8 6K `I’ßêÚÛ¡1’ð‹ÆýÅà6£Y,˜~£.íÿÀbÎ[.¤ã8Kgºÿi’ Töþ8ÂÇÔô¯GöÿõßãøO¤ãæþ?„z+ñ:s_á?ó÷ÿ‰ÿÆ/·g±ð‰ð¿àŸþwñÃ÷]½IF¾ÁüO£>ÿ¹‡>c™?ÞÿÜ»7ü“ÿNéGýi™îÃÒÿ#Œw鬧ÿizÔsïò/_üˆÿ¼½?ò%ûÿ"î/þâê/ó#Óü‰ðÿ“?õ¼=îÜú•0̇6×ÏæÏýLÿÔ_ýÔÿÛJyüéÿ¸Ÿû ÿ´šRþUÿÝoþ£÷™ä5ï7÷e/Kxgv­{¶íû°ýØ~í«p­aaW-vVÓ0·Œ0ÍXÒK,± °É XȽ¬G€×¬÷Á䙋§¼}±Ö>"H™0XÙeƒÙÒ 7€6Îìူ²$&ά,îDáõX²º—†Þá²õàx?§zïècxÖï…ãÓè9Ç8z²t‚õã÷§ê-þÿÉáITe’6q–q–Y#Ë,²ÆÉàVm–YÀ)d–Ieœ2Ë,²Ë,²Ë $²Ë,²Ë,²Ë,ç$²NrÏ£,ƒœú^—X†4¶Ûm–ÖÞÛp–Ö ìÒXKmDìÉà‘×î­¬5#2Ë.Û‘‚$ã±,wd³®1²`òLK1í»IÚƒxKÂ$–q¶ÛÆ[;Âwî­ãn‚6s²zg3“-úcèÈ,ˆ²ÉçÌ7“x->üC~ŒîÇÒÌÇÿ;þ횢ŒbWÓ™³f4æ’¸ö,Øâ Î2é73è~PNtïïñO ý<¬ƒ¿Ofy¹žCQÉ–Ye–b xÞ†~^;á[¿£nÍäÝÌæÈXaûðoI2Ic²YeŽI,–I¤Iaa$–`3d„ŽY6wåÃ-?kÈá®^œi?Osw¼qgÏXÛÃômœá¶IÆGM²wÁ%÷?0:& n¯øE¶û/ÝW˜ä½ã¾.vï¢(qãàÁK\5Ç<±aòرÅ•îÌÉ6#€6d}6o.|1Å'É8g‡‚å‹ßH½n†g?IŸCû/¶(6x žƒ,e’¹1’ÆË,áÈòaœX~†|ÇDXG À!tLÀ²Ë;°³„Æ$p8l³œ’G,›údž©,&Oôgzýq½ñêÝÞŸGq˼çÔYsYÜopI7 ‘æù[ö/Ð>ßäPÙV­Ål‚’ÈÉÉÒYä~ù(‡®Ûb5h­ÑõØ{ÜOUŠ}|Ó¼þ¶ö™v½àtA;ké²VäAé…"ûïÇWd²Š_uL7.ï}lÄýÿ…†~9ö?Ú1Û‡Ïè"\•Ÿe¢íaîÏOºŒÛæ´ÐÉ×vÃ6ñ3püOJÛ´ñû›¾ø2¼/Øíâk~áamwï}~sÓ^ûòÄìIê”X;h™¯¼õ!Cò5L¯³¿0—× ð¾†—\{^>#P¡Þ© Õ«WdšÍúDö&´ Pt*K‡ #Á V¶øíî\¸Víå»rÉví°íñܾ;—)'¿ñÜ!Ü{ËŸªcxâ9 ðžs1<Àð¿F0Õå¤VŒjÁŽ:¯s=±ì„XIÝ’@ìÙgÄYfAÃŒŒ6Hì’wÈ[´%™è²tµã׎÷ƒ8fèÇg_<þ˜ðÁÆAÀ}ñÒx,[¾´˜ƒÜò‹Á'Y½wo¬™×ÿô~÷;}TºÖø» 7òE\ö÷ÅéßâWjOŸ_‰÷“=û×û?óLGÈ«[÷7§þPþðØ÷ÊH@×]=,Ä>Ðñ.€é~þ_Ò*¨`w‚îþ—÷‘gëŸaçfðŠÞÿ]¨¹¬I½øÎÅd`6áøò;õýöé,iâ·së³æïŧß§·@¿ Xì£)G×Μswu·x󆽥Q1,×Ï[†ƒÿëJd‹{Ÿ&ÞÂsß~°´ië|G£×n¿„R¢Ãî÷Ÿc€ÄTñÎ23†³Ž›6³‹ÎT°-Þ\u–¥z}­2 ³Ã? [jW´½ñkÚWµ¿i^rVxµèZµkÚW ÊÛAÅ|·nÖÚYs†Y.Ü»s5/-e»píJÍ‚ª|ñ6 ûWÖ8ÙgV' 2NÛ8BH,´ñ‹g¶u+d›0á3 Ýñ;'Òï9ÇèpæAØ: ã'~ŽùÖëé'„ºÞŽà³’$¾ ýøgÜOIð‘Ã"û/ñGå¸sηϜ;ØMŠ^^ h·Èl}q»ìÖèÿܳÐóQÔë.–Ìð8ë P@4ûKPƒäà“ ×ìÄ@‰â² Þ{#âó0:©Pò†u0‡î¯tÑš1å^­óܬ^aî®IvÚÁø¶fyçÿh­”*Å‚Ú%F7c–¾÷¨Î‰ÒÁÕjFpÅíîÎîÂó-Eã£ì…Ü’~ GÐoÝØ¾]iu‡¹0ˆ´þgFRCp™Ÿ„—™4„Á‹9î5“¶;‚+€; vZ_*ê½êÁØa ö±¶T‰ÿÝ“gNØ6íŒ`䜓íg6=¯j61¾*×¢¾ _º\Öy[÷K ¿H<í[ȹ¯k~œ&½­û^n¡k±·Å-ÞMb|Lø§Ó'Û¾;/NŽrñ>\½ã…f{'˜}¾ ³‚Ǻõ¼ ²È^Ðó H`!Ôœ1c%BË6Iƒg l’Î Ü8E Ålll’9¿uxÙ$ŽrǧИq“¼MÉx7m³èë>6Aôw¼aXìA$ô{ À÷œ§´â^Â-oµT­‡í|VíléÆPU‘å=¬;^Öý­ZÝÎ?oò³‘Є{·|¸=ÏÞ|GÄkV³}Ð?$k>‡û†Áêÿ¢@ï XÃú¬‹Ñðþc1z7q‡_åeoÀÓ¯/ûˆþƒf´[ûGµê~öNvtþìè³ÓýL滀­€éâÁ÷¿ì‘ŒCö'ѯǺ@Å߀$º4;ßîÀÍF› cxÌ`D`êG¤1Hß–@{Xï«Ý—Ù#|]=,kÕk>Ö}‹“>Ë3ËÅö–}£Ü,fGã.}³ðºúOIá_¤\\DÜ}³S~Óðµí3s~ͬñm%o‹r½¥Zö–’½¡{Cß§< ˜…<ÈÇKÝžp²Ø…%“u#alŽY… ‘²Ë8dà’LÏO] mkôHð} ÿ˜ß£~¯Xú}l³“yÈ8Èú|ùgüœeï|þXžð¿„ûCö¦çÙ?ù:è¾Ù;âÆtAñ}§¿Ž9öŽ4oÙË9&tüÿ¸ Ç‘'0öýÈ!îý çG_ôj: 8%ï$ô˜Ã=ð÷?æÙƒÜ#ãÕüCÔ :™ÇOji0÷oêœ~FHO•ýÖPÐÿ¸×«=á’-=ÏÞ¯‡÷`HLí™ë$OÇì]–8Ŧ:ÌvÇ ƒ@ÀÈÐdG´ é#ÚG¡g^ {AöƒícÚ´k>RǵöÙö±íbÏ pĉfÅk2 =¬X³`‘E™kÖ} >Ò}¯ŠLLÏz¬O¹gÝ#Ñ[÷C|;{粿àͨä€ÞTÎÐ\$b‹ÞŸMçvl…–HA%„Xg%Žwc$9À26a$Èx$’I+öÔxKÚÆKtù±·~Žùac˜ÎåúHmåϤ‘ã9ô‰ä8 ;³’Ozý‰à,+ÇïÝ7>r™.¦ž:pÐÂß³oÄù—;L{˜÷7Í»»w‰Ýën¾û°}gF¹a‹…ÛÛ43ü„˜tþàž½0{Ú}x¼·ÅûO½–oË´¢öÖ{ó¨þýÏîOýîäø?[æPþ©´S¹÷™°ÿ›š n÷ûWÓüÑË¿Í0{üñïýã9øŒ³¯ì|_Öß´îÿ[ôŸWú¿oÚø¼ßø·Óâþãþ¦Ñ¿{ÞY©ƒñùsïCíÇô¯÷RÿsèO}ïüßü(ÿΟü)ÿÒ£gµÉGkéWí/Æö!{B_N…ý‹J¿f娻øüPôçöéSÄ£ þ‹àÈþ?öèúî>’Ÿ"½—/¬©÷±îwð¶{¡úOQþu^c$ýú}ùŸ^½üÃïJúµ{ô¾µß_ò'Ö‡}uo¯òRzá=í|BÌþ«âö ?qDý‹9±I¶?ÞŸ\ƒ¤XdMd‹$’H ³« 3«:ÒI‚8G;Û© ¼—ë<ö2QQ¸KìÛwbqÝ×l[œ‡ˆ³89röà,ã82Âlã3‚02wÓÀÏ™¿"jI¶à}ÔÇøêÿ•ï~´ÿîgÖ¹õi}hOvcçX|_•©÷XúÜ^íÇ¯Ç îÊ™ÜÞü„‰êþÿßOó'½ýŸ3ûo÷;YëЦz³O%ð7ïø±éÞöîu÷ø¡ÑßgúuÆô°Írùå°Tš³éNþýûOkñ?×OòùÄmýý]Ìò[$3ÓO}_›ýïNuß³CÚ¹íTCÇçÏèŸ÷{ x .‡g§7µ¶oáŸhý­ìS=gžŸwBÞÄïþGðŸúÄÁ}o͘…å^êîô³÷—îwÈO¼)Û3êPÈ©çócê~X³ÛýÖdêî\]RÄtïüSêþYï~hÿÒÇóŸoç¿Ê Ã?ñHõ$É#þ…ÿÓ‘ëùçg†ï‡|+>‘±›ÒoÒ iû^"˜tp‡!' ÙÂrœ' !‘ŽÙ “&W1Y'Ý'5*Â[/-±›ÆZs³çŽ:6qºd …’uÂq×;9¿ølAuu $ßhâct²ùóƒØã‰5{× ‡wwvãý/˜÷¾mõÕ¿Í¥Ç4ÿíÞ܇ÿUþ/þÏðžÏÚ‡ÊÂ÷©9™ý¯‹ÖOõøºñÜâ¥Íx«Â~¬ýù¡ºG?½ÛúÏ0ètTï1×öhÃøa™Ýãû3¿_ìù†Ÿó£ˆy :ÿó‚íù?”m¿æ†8Á#ÿ4‰ï0þ/ñ?ñŸÄï³:zþïˆÿAûN¼ƒ§¯ìöŽ]~j;uïõGÒ?Óâô,èÊós©“í}ÿ’_òÇÁRó³>ÕíÀôÿ·ï7ú?Yô? ø¥ó×úGú¿§©þ¥û_Ö¿Ôøæ5'þOâ¾Oéüÿ3?ô§Ñ’žjsî&©ž}k–óWÿe)ÓùÿM-óùõpxsígÚû~’fÛo ¶ÖÛaãÓI+HˆužÉ->5û[~ÅÚm‘êI%’6Y'¬pœa9!’HÍŽÙîÁ3»#!˜ñ—ß<²N¸xêëè-Ÿ¡óÅŽ]ËÒÕ8 ±àxëy¾œK¨ä$M€°à½,gvÓâ“e÷ÿ *ºÇ ö%(ùV#ýíò³ñû@ûgÙšöþSí'ÿa½¯Èßý[Á~oì·µþ{úÏìþæ·ÂOµñÓîÇÿ2ø‹ÑÇâ}·à@~÷…ðÿ ^ÿaiÖ¿‡«ð_;ð^Óü¾T@²äm?ó[SÏåoþ»{‘ôVžSî³ÿI·þ¦ßrß{}é=ö?VÄ‘´Úµk‹ÇYλrçë@ÿæø3†pÕ–¸e–¬xe–Y#ôce–#Â¢ÆÆ6!=Œ“mñ$¶§ê &È>$²N¬›,°’Eë ±›8BNä3„Í%ׄɲù§äžŒbÒåÙ£ÓŒ2,`à8 ‚ à;x Iî}­8,ßßöAµ°=ƒüÄßÑÈqœ{·ôM[ö·—Ù ×´-—¹nÔ.·íoÚݨö­ûZµjÖµ T-ˆÖZ…–íÛ·nß|Åå±Ð Ç•‹ÖlYúEŽàðý¿Fnÿ𯯕Í̾Ô3ìØ™´ø–kCD:!eIÝ„ç’Æp,Ñ“èıó2Ö}¬û_ǵkÒ/‰1}Žwê}ž\œ5<;´ð@€ s‰Kí'i#¢=CÑ9e…–IÕ˜Y!’%’a!ô %ÑÆ²é}§™9ÂÝ ‹#tpó·\ç©¶1;&-…à99êˆñp$dw’Z{)|frp?k÷±ù ƒÈGM€ö³ë>á"éÄFÇ´Ö @@9 *j*6DZc‚¨8¬ôUšh0°š9žVæ“M¯fלa8ci >^KœL¼3s’•à7¸ÃÃo{Yk]3åeö|n÷m4LÝ\87÷‹öÕLÎä2'àÙãÓüÁq…ßrî06˜Ø.WÞåΣn˜ÊÄÂË=J í¯bcHG#ä/Ê\"@ª¶w°;´S²8·íÈd Z»6ß Ûrí÷qwøµóêʼné–3›&L…š›6,Y±<±ó “|‹ëši6fý±‰æí}£þIé†OŽÃ,d›:’K$áó,pØÙ;I²Œ6²Îí3íô –ÒÆzºã—èc7Œd¤f%žq8êÌMúpã.¡žÝäà8Ò3nò NáŒxNn4>Ñ­Ó •À°$-Æù<ãítÂúÂüzÆ&ÞõÎ…>_KqB²u¸ÈßAÐa|£ß±½ 6ûÎ óöŠMPtègv ¡‡¼~*üô]š)C~?]ÏÚúÀUìôv`3ß½øcŸž{dIõ› n‰ìSFTE=ÒUŸzp/>æ1€vÖâ÷ûÉn;ÜB‡´w>˜8ÖýÐŒˆæý®”jaºP$î–$Y±½ŸfÎøoµ¾Ëíx¾÷ñmúͤߘÐõ ið#Ø_ëwÝÞ'Ï…ñ^è1¶áxGxÍÚU¼ñnÜ¿k{Ãr÷².+ÞwšÖîFCÞŠ-ox62ÈgV;œ>8ÂrÂÆH$µðs–{¼<%ŽäÈɳâ&0l¬ò à'2Ô@Û¥Þð'S‘œœ ôAŸ@EAÀ6@dÁ ½ã›;ã@ûqz{ïÚ죦,oÂciò°¿mרò½àëµØÇØ•*;Ÿ¶L˾, >qkZ¥V@r ýËvðû#¯BÙÜ#:ÄPšC„#¡'„ Ñ‚ÁH‡€ ‡‘±×iþ¡•ìê]ŸêâìAw³nžÖu“6hÐò÷¾–-ï°ýË?c–íT]uUYzÞäV?k¼âÜÿíœOpþÍ›v@ûÿ#g§ãöš»`}œ`ëË#vöÿs´§y©ü¿ÜLiú¨ ÁÖÿ¶ é=|AÙ›6ùñígÚÎø³íf áâǵö_lŸbûgá|Iø_mö[Áâûo¶Ï´›>¤ücá? í¾eösç,ÉöŸŒ‹>ÜSí'ÚOœl{]ß1&Ï´Þ –Ðéx6}ùˆµ>XÁ1 ÉÌÉ…–;;a1!&u!' ]¥–e›!Â6bšŸuxÝìœöÅèAmŒ@†ê xgÝádeÓ6ÁÆr]òdö·yÁÑÆƒ,ƒ9¶5Žùgà_Äß™øCùèóûDñgw/¶µ™Ÿkí³ž âÇ´!zƒí}°1cðãöÙƒ×P7r P|äu4³í(%OøïýÏUÖ›û2ÏC]ÿsµýÜŒ\Œ®’²O±g|Çá&uì¹­:ž“¥ÛMü?³xCÕ¡ œõÿD,å¿å¾ãþÈ!eÙ/çù‚4“ˆvÏ´Œ±bé}¼™±fÅŽs– pr¦E±Ç,_c}ë^ë~è~ë~ûäϸ—ŠòÀüÒú‡ïüò~»ó"÷ý/™õ?­ó%çû2>x<§ãIy“êHŽ%[ýÇö•{ _½áÂOº(<Œ!Þ$ÖË º‡àþsé~jѾ•¯¡´©ôOã>ÏïøžÇçÿéÿKâÂ~¿Âö? S¿Ïaü}ÆÏßkóåüT^ÿÂM:<¿Ñ^ñQõŠŸX>ñîßêå[ÍwÕúäõûó)æ²þ{¿¾_Õ9õ“í$<ý§ó¯©úQ÷oûôÏQþÖ}÷ô/—ýh·éÿ«}1½¨£ýÿ$>Œ/hÃèGä{ƒü<@ÏX8›õMÿ˜ÎÈH!aô%œwe–“™ÖDù²NùáffB\éOUÒt»dîsBrDûü1kzw‚p<ƒ¼23xÚYŸKÉ XAÈAb¶uÀu°Y„ êÆl¾Kª#ÊÈ3Ïúø®žÄÛÌgä|û‰ô?>‡à§Þÿ¯™ôÿ=öå};ýêSéšûmíñ>„~×ãžÇã»øüüzø Õ€z3ëâþ×þ½S¥nÿ;î ¯®$Çn_y>ö/›ŸWòe<×ë¤{ßý¢?˜«ù#É·_òóëÅÐi£ÕüÚCûÓÔªzõ`õ?¿ó§à/ÁwJþFë÷7ÝüúŸ÷ŸA9ô¿R/§w?ë½ô~KŽ3Òþ™Kµÿ¨ñ‡íD>Ïö±×€û[åÛ§ þ¾'Òýà—‹ÿciOf½/šžØ Ý.ºþ·˜\×üP|“îήÿWùgÕ¹¹#Ú>ÿÍ"Ç׉íÚïõOÚ÷/ð¿ø”ú°q=CúüKîFú¿6­ýÛûCýÏþ·ÍóÿgÍðþ1ô¿4ÿêO™ûÃé~ñ>‡êâqëõÿâùß«ŸL~lÜ>—ä_ýFÝîg½ŠÄ…¿–ßæ {xË-Þl²ÎÝ›xl“$™’N¤Ù$™Õ‘?)-:€]ñ®Zbw0ã Û³µšA‹wÉl èGƒÏÑ9È^±°0]§&puä#6# Þ  îIîöàÎE¡zæ ž‰®ƒOGû¾a߯òfã¯Äþ`|Y¿IMô°ð¾Ëøøåâûí6ìüÂ<_±Äzz¨õléïû?1ªÿcïvGþÞòëê¯Möw¡÷Kmù–é†û1ë~t7—úÎÝGÝúsû‚Óã<¢:·õÆAiÏb¹û@OÚÇþYìÏÌ[¾ýÐwø÷a¿£=WóD{øßë©nÔ;ˆè©þŸ)Þ¶kGI¿›³>õ¾$ð~ßòŽ—óÅx‘Û¨W€tqëñ'G÷~·Útzk—p=úý_æ–y¤¿OË.š›ýÇÏý_y׸>ÇùŸ~ÍËèÿsâBƒ.=Ãø¯ùcø™ÙÇÖ‰´Wõ?Ìùߨù»þ×Ì·oêĽº~xG½anøO§ô§Ø8§}Åñ¦ž £è…¾ûç{Y^‹o¹·ÞÛwÜÎÝÈÝóœe“v2"(rxûê;@ìÏü°Ë8îîlÂrËÁÃ<"Þœ8Êè©jã¨nÖÈœH¬‡BË;ïþ!;0m~‚ð¨õ5KÕ½NNNí à#x‚,`ˆ8 Ó¨ûpY2Õíû¼9î_’&QÐZ{¡à½¿Ïü¡ºÆ?ùìCÏô|¶?ð3é:f0å’ß?iþ³ûZ?ˆŠ÷ðqM–ö3üÍÛþįþà•óù†•jÝüÉ=K.Ó+†­ZµÉ«OÕœÞHîûmßd¿kí—íkÚß·íʵoԵݻanU©v­Z•Áà qÉ4OŽy!⥯b‘p!Èpó„ÝͲÎíŒìIdÁ)Ó³’ÜKô48ÆÎ¿üéÆoÒx]ÆÈ.àã$‹ ƒ€ƒ^à챂ÌcFë`°»‚È,oFX¾ß¸^KÞ' e ŸÀô…¡©ªRëv/°Vui…jÕ®ç!Ny<¸nÕöGÂû-z½­e­0…íofµê´2 îîYL/Š»V­Ú•nݾ-Ýž9f ›Ÿƒœ6,^£8/ŠÍñYÏ}¤Dz¾¬cՑɞ𑈓8×'§‰ì+л87ÑßN®à V¯‚ñ0¼ð[·¶ó.ÉP–]¹ç¢Çå³fÍBϵk>2ǵŽ3ì³ç$ï‚ÌЃÀÁŽ&1ŒS*b%-«±âÕ¯k^Ò­g‰™^Ö“Ák-B–ñ\¼·+-äË tÍP=ì.Îкý¥â‡p¸‰™ã8bvÎ|’XÏdÅŒð¦dgÎq-œÎOXÛLjx‘Nˆr^þaxtoK9K,€»±à³l‹4‚,ŽÌ[:»l"ˆXÁ×a$2–¯Ù±ÞË"öö¤6jôÄ8ÁÄAŠÑáœ+–ÃcÜy¥¯SÙ4° –Çn<˜YÍÂDB  ò©&+¥ ì02zt´'BÂNJò/û˜CË¿j>¶a¢íö܃$ŽÒ²X¡ÇÃaocagŠ$¢ÛÁ7ì˜ò¤—ΠBÏ(³°H.‡D>¥å»z9:x—°­$ÕK·ÅãÌSdŽrE…I6,Ù“%’dÉ™ö“í#ÚG´iøX‘1^Š–Zx1P³;˜£ÒfåЇ‡&÷ÁüÎÿš#$“—Ç«tNöØî»sœ9e½É%Ô³jfʼnø\T΀Óô±þþ;@ãvn§‚ë/=øõ.¬ê#ÏÐtGlAÔD8´½3!e–z½‘Ðg¬¾aãÑ#ƒ¾¯ò ò·øæ=¶pޝÀ ³ØO£ò[KÄ´ÏHÕK¶ˆû#=¬^vÓÝ ýPL§NЂüh74ÇcØsºü“3´÷Gâ)ϤŒ{±¢ÇNqÆ 7|ÛŽÓÇnGα§»w‚½}š€:~7¥–Söµ#¸o;iá±u÷™)7[;e¯nsõî³’Vþüg‹x b1܇Œäwz—8;oq¶1(GÀFîh—5ë4ß?ìÉßÎÊËDeóOK³TöznÀ–å}zw’°1ÊÑäO‰Õ5{o” wý2}¤Ò&.=:IÀå3…>bº€~‡ÂÔŒuë׸N ,øgÞ7ª‘±ô&hO‹_ƒwüm¿R{(8)=??îÉß³gzd¬ùÿ$kAÁœ+´¼‘À´õá\^Ð÷ÅÓÒ×´¬ñkÔã»{™nÜ¥·l—1wvTð'ðFM^ªJ¦¼®}ãàaåogŸŒª6ªÄ‘iæ;~ñýŽÿ¬´Y§ßÌtiG²ôcâqGR~RçXmç1ßzjž½^0îcѰáðÞ*OšëžÔ/˜>Ì‘óíÆ véÌÛùAtE´» íƒV£Òúk\0ëþçK†ïó8 Uë÷žDN‚ù1ž>]Z»7£;f_OzÔe<ÉŽîüÛz¦tkÒ'3zCÊâG‡>«t0 !Ò~ä€e¸Ù=eëÝ¡ôxû3ÓП…çâ~6}¤Dû^Ì–…²ùFŸ, ƒÃ_}]¿Z:!Ñ å;Â[<;$ÊðùêÂîN± ’dW€xkÊîO Ûìÿó6`áxïO§eEG$yàà œˆ“82x€ ²È ,àožF}¿Û¢~ÉÕ$tÎÙ¹îcg´n¾ÓÎ1iGñÙõÔ ÞÙ \|» 0ƒº(ÅÔ4´šwækï°Ùž b¥à¨Ñgê?1Þë¾æ= 4}ãh9èzŸr©ž£!C4Ü"„âF9¯‡·à.Iš)1#¢ª•¨*µUXC'uØ4[ª8Ù +!í¬=Ô!÷!†Oë#OWöA[C7²rF^þìÊ1íÑV ÊGÒsL üJIŸxí<¬„u‘ãïþá 8$íÓöfäI)=ïLŒºX±–vxƒÅ6lÙ€±¾¨ &ßɆÆuÃO‰¿ Ôý¡¥1耞 žÛ"w–:yÛxH,ÁYl–`,ém¾ô'$áÆÎCà¾$_âÛvS[ÒÙí c“€èÌŽ‡€¼ ‚#,3“`€à,‚à 8d‚ x-ø¡Ž˜‚|’³ï[{Tþ+ÞÌâÍŸn+ù wƒ°}¬Yö±b~30lŒo8Î'í]>òŸÅ…æˆyÿ<—ŸîüIùþÇÄûÏéñ> ñîCý«÷ŸBô|4ÉFÑ¡ôfû5oáüößëS¸–WÁ•ücÞ+à¿y·€ƒçÃûûÞÊ‹óú.òG>dÀ~òhD-µØ6Ûâ_ÄÒˆ™œõ;¶ÝÎI¤o ÕáãÓ‡²K$e’MŸ úÏÅ™ºŸ3-¹¤øOÈ‘1y/N7èwŽÂðÈqg&F¬HÁ‡˜Á–Dc±Ùß!¶DxOW±Nì`¾ãü)çK>Íëæ‡$Ùl@€¼bÏ´#xb^Ïyüׇs'lF±¦K¡0º1°7“'F³²ë Ü~k×{æ}ïüκ‰õ?ÇÏç½ÿÁ=ñ`w·ídº·Ð§Ú_{òO ¿¢>”/@ŸŒús¾¯¤¿§Åè?øŸKò‹_ö«o†ëþå{"Ï Ç±QãþGû²[_9ý!™÷þøoz ]ýçú'Õü{6Ç÷ÿYzˆÏRÑõ?›DúŽOª¿i>»ý¿”÷Óí'Ô_Òƒóø Ÿ£ý)„û× ÷«ÿStxO¼çãõ¡ô¿ºzÞßê¿ìŸOòÿ¼ÃÇöü̼ØHxûI}/°¬zÃígбÃÄ¿§ñ̪ÿ ŽÖ3†Úö•²:' õzÌM’;Ç\wÊ¿NÊL¤~î¢ääóœP˜6çñSc9ôç.ò|Þ²c€àÎ À7aÁôÈ@A…ŽÆA `,ƒ€È ³]´ö•à‡ºýáöæW:Š}È=ñÃgéó·q>¥ß_òÉùüÂ|8þ¿éVúoö‡ßhÿÊÇѨú?ŠŸOòÔà}/ÊCð‰ô?·âkâ&Þ-¯Ë#Ä…§†ñzþívð_ÿÄoŒ~•U¬>ê}ö«ä>÷ÔV}ÊÅ\ü·ýC÷ùnSÏäïp~‰ûÌÿ*šKïî‰ë\ Òÿ«Ñœº“‡OÞõ=¯ÍŸ?÷þ)ϯÎqñú¿Í9uùˆ(Ÿ½{uùï °³Ù´àt_iëôHX§Ø´qêÞ·ùö”¾Tþ–Ò¯ŸÏWóÔþÀÿ»ÿ¤$<ŸÞ3ïøÿîH×ú7¹/“gòüŸâ_õǹø—í'ÿ ¾ãö_ÍÛßô|ήÿ̇Üþߘ°÷?¥ñz(ýoÄ}ý>!<¬mü¤'•úö/iÔcÔ»Õš=VþÞÓïN«d‚;P†žÒð@p’NÌ“ÊLœ¼)œn¹0@ò‰9&ðÏ_²ž®ùa§ ÆcÎpKî¿Á‹×—Œž›{ÔŽÄp@dq‘ÇGaÎÈxBÁÔSßl%äêüP,Î\í g{Ÿóqwø¯Ê~?êø(ÿ§C.ÿ¯â­>ÿÅöý×à&Çëk÷§@ÏÁýÑéÛøàYö/ãÄÏÛø íüv¾ðìÊy¥Á©÷m_ï}«÷RØÕÑì[»¿¸Ãû.´Ž¼¢Ï[¿û[Ïì|Ù…˜>§¦ÞêÕõ>þÄ “øG—üÛ}“ýIcp¿ŸÒsý?îÉø_éë¦~˜u÷ùïN?­!&y{üc_Ññž?4ÑŸÿ»Ô?·ólÕü—·¢þ·½íÞôŸCñÿ?…öÁêDõ?8ÍMüþSýJyüô_?O­þƒî=߉þcÕ¤µ=OÅG­ù?ŒzÍCÕþ‡Äzðz‰÷ƒÖ¤÷èâáÿÝFøˆ/žûH/V}cà`±—ÜËî[êY,{H’Mc¹ä?H1"D›1^—X;œñÌËjLC¢=LðÝÝg96XYÀ¯9;ÆL¡b(ÄŸh–zËyNáÖV·ÿŸV÷œ–§—8õ²##€ÍX'rŽÇ`²€×mÎÈã ‚.àÖ1`²yñ àÈô½—í>§±Oc±ìþ"ÇýØ»^ï¼?Ú?kÑÿøŸã„ú‡äÞä´kГÎdð¢=(øõG±øèðŠ?¨»Kûûß-´óž®ÞlÌ/wò_êSÏâqêsæ=OʇFÿoÌ6í~ÔTz³ž¯éÑë·ëGq /W÷¾ý´ÿ¦‡ý/å2Çü 3ª‚Ì”ñŸÚCôè}€ãÿ¼ßýkÿ¥/¹³Œ°²Ë9ÂÈ;ç:È ã®zúw޹yË,l²N •<,T¢_¿36lYàÀÙû8“\3ð?Û?Õ#Ü=Œ¼<9%pÌÙÜýÛλ˛lð’1ǰx ã~‚æÀ–^è…ÇqÖAÃö³>¶rK× 6"Ì9,Ž…`ÁÑÀrBð,È 4“²užjû¤H>q ÁÎ÷¡ö‚ñ#ÑâP¿½ñؾÀ·Ýo¹¾æwxNs—9I;l,ç# !MÖÞÄD¾e· |K¨Rè"™ Ce>1óé$ÒÈÐ •Mˆ>´ÁÆ—t™©Y/èç×øÿÇÜ/ü'á–XÙd–Ye–9e–6X¶¤mZúCvåÄß™pí}` {Çj!;+hû¥Û^ìM‡“œ<¬Ç=NNp»cÂlÄìòx!>@ýÉ>ûÇl(šçð¡yüðüÊ¡?:Nƒá"^}ý2—sãYí‹<jFWHíu-û®ðõjñ»MÔ½ðm¼lÃ{¬ÛÕ¿CœïÐì«uìág:ƒ©ºÛ3 Ì1½˜CòXn“ÀVu!üf0YzEŒf]YycØŒà8 ´Æ,‚#gqp,1%¥û¿èä ÷àƒÏùŠ÷p!ôCc`ðÏ&pË£,ç,ÿñgþygÓ¡å“ò©>A}̇™ò4‡˜³ý¨…þžæ>ö”½o´'`yÿÿʯGùÿœúM¦ß¦ïçÏR­ôï©gÑÞ_DªÞ>ë½õ?ÒûÏ­I>f½þý¯¬/·únÛŸì¹_?§ ùh§ºŸ©ûoå_Y(ú¿Ö|Ï¥÷}5ûÍð7ÞŸOõ(/¬Ý×oÞ>—ä@øü6÷}’œz/·ñG‹ìÅõúÅ>é΄ü§u›,Ül›íD]‡qß,¼=·|,ç o Ϭه 21oø¢I9œaÆÝìuVHÙlvNð_Iá½9fä8 à-crÒ ^.ì€à4‚ DA¶# ÜˆŒ²Ÿay¿xCâ42žÏûÝÓÜpËÿg^á{Å÷I/?‰'çñgÈ?%æóø¢.vûXÙôÊGƾÊG¨¾‡áþw¼þí!âÑ×é–ÿ:zeùpòwÙ?Õ¯…KxÏöõžˆŸ ýîþ¿¯ï=QÓ韼«ä?¿¤õ•þ«†ÿ_ísýBI5³Ù{üƒ÷«ö_ÊûÏöÿlííþÃ?ò…ó?§ëõö¿?øñú´zk÷—Æ8~?]xúžþìãÕÿá“øäükø.žéüq®Ÿ¡üýCà'Ûg±¤Gó˜ô~¸³°ûÿ5ÓׯóÏ©sî¼÷?/øŸP~ÏúDM>´ƒü#GŸëù¯øÌ«öþ±ði÷þ ®—‹eà~´—ê|Ï£ù?”úå“àûö }?ЕnŠlFïä‚é”41H¹ƒ>›íÚÀv@'A Ã9o È–Dä—IuÁÂÛË6lÝZÅ·°õ9"ý8KÚ#6/–›÷YÆÉôm‹ %„v-­ŽYppAÀà#AÂ<^¤AÀ9±( f¾óÔ}fœ9—gÜcÐÝýÂïä×LŸÈIyü9óïHù¡??ƒ'f'Ãþ?Å>ÿö—ܾÐúP÷èO¥ùç±éÿ†ßm¾É†ÎߊéÌO¼½S=½8~÷ïWï!ò½ßÊSþïp­õÿ¿ìJù?×þ¦›²²h~IÓSóþóÐûÃnÓû÷†]¿Ûù ¼«ðrKúŸÚ=gø¿Úž5¾ÿÆù¾bú]]RóèýWþÖz Áý¬KAöþ(7L}¿Ž<>ÁŸô¨Ý e_Î/æW{_¨XýPŸyàvsÿÁÈ;)·ø:ý‰×? ÿ©õ}U~óz?âÿ¸ÿTýïþd¾~×wQ=…û˜úŸ“üOòÿ7ü×·ùaÿ‹¿îIâ«ô³éTï×é_J>”>ò¦7T·Þ/ï|Å?õ|‰öÅWÒ3é™öí’Óó3'ÜJ½Sí[*]Ë·nܹ¡Î>P¯\í)í‘jà;¸ËâB:!ã‰ÃǬç)Æï¼&Âjp„óÔÌä‚åÐÏ€m¼¸xÎ:»‡‰6é !¾+ù.Ñ·½ë½Ê›ÎÈ!‹`.ƒc8ø³æÌ0‚ .ð$±‡8wÕc‚^¢‹ÚFp­¿'¼é¬>ì=uü!ãLz)÷þx6ûÑüåœOûÊÿq®ŸçÙè ?öþ8¤ú@]>À¿Ôzt§_æ†kù¢ç’cw³îÈò_&¨”>é+×õŃÙÖk¹={?Õœûü+8öße:fß´{‰Óñ?›ñ#þ™zÇç'Ól÷þÿÒçùˆÏö?y?õ›>ÿã'þq_‡ø$æ‡7~1öþ/þŒûÿ+ùŸ}•þIòŸ©1èâ³óŸûSèæsëð2¾ô´ì!ÿâO°>ÂíߨJyR¾aO?•µéüéWoægÔSï^âÛ·o‘èðO¯Â~½­å½ñ._´¸ö§Ú˜U·É?DæÌ›6f1bʼn2ÄFtNOzý­nè8†ygŸ8ŠŒŽz#'•xë-°›BÞRFê[¹ºú1—.á03î‰>óY<é%×F0ÂÀèÿÂú3”8'¦a—Û†61À¼œH‚ ‚0¼±°0°‹DA¶1¶KWzG‚GD'Ößiñþ%Òc~ èOÖ³a¿ªO¡üÇÚvs×ù¿ìü‡ê2?ùÏÿ}_ÐÄûoëàœ;ü1׿óO¯-n›‹ñ:õù¸G_”}?úmêùœÜüLáÔ=´}¢®ãð”ÿ§:²SÏæZùü©WŸÍmÞ¸4Ã…kŽÞ8,ñ)í/¶ß´)ðCö¾£Ú‹8a÷nûìɃ&O5ÙÁŒšf>Âp:Xö˜}‘o‚×´¯BÓèΖí[•Îך–D‚NxáfÅ›™{<ù7Ä΃v¸4Ü?™Ôúj “ÂÎ¥£%yrW.ÒGdœÜº‘ç.¬ŽÙ}#Ú7:™‚ÈPž [¼C îãnìc†Ø KFp€ à 5ƒ¥‹ ‚7! ÀAeŠiÛÁàÈ mžµ±·¼P¾M_ˆùu‘ß w’WÏäKùüËg¿ÕY”ϽÀF­d;P¬ÀÛÊõõf>ÌÁ‡êZ× ócî3!úOŠê¼FJ·°íÅ)œhŒeÀQµÝgر1íÛ€M{Zö»¯Š´ÀáÛâ>|û6dXö³gÚÄÏZ2=¤cd;,r ,Ëãò&jT¯iCâWœ%J󒽦–qÜ¿3$ðò‘xI )"ÆÉ5ßÌ;ˆ•ŒY=ûüSx< /„áá¶ô—cg‡dµÏ¡É-°ã$Y ƒk'upx5l‘³'¨w’¸ ßÙäúNÙáy<ûÓË.ˆó–@ð0 mŒØ‚Œ`‹ lbl‰áœ û„û© E †Â…jÌ Œ ¸ƒ@GŽ",u¤A¿Hîuk08‹¡l-‡ª‡n)sf -›âx±íDLý §+äZö·íµµí}œ5·— y9<,ðÍ‚ ‹6&1"I'ÚÏ´Ÿið$X‡gƒ2¦¿òg´ÇãÁr‰V—†µ—=cDn-}A`NÌ0p'à6‡÷‡pQ!ÆËoSô6ÎØÝó“&=íd\ºµÈp_HbÅ‹áú.ý­g‹ìµíj«ví[´Ì×¼lnx'•-Žö¾Éø^\}“S&ûlϼãöð÷ä ÁÏ)æ^­vC†ß~&ÇâH`^‰ã«fÇgÅéÊl›<#² -›d³†^ât¼1Á›i½pŠ[ø'x|¡;Ǥg,ÏEàš¡ð‡Î‰úÐþ"_÷ßÊ¡-ûGðOúD‹:±é'¾Ó÷Éíƒa0ŸÇW\uÎ]sœq™Ï´ì¯c°ÆÛÙ@îõç ›Ñá\êq# ¶iÿÎeã6ÖërêõžÔâïx€x;‹ΰ³x2à= èà$„FÙ±±³.Á,q­Á½²kox?ɵ¹Âítô±Ãì±yD9çÀC€˜¼ X³f žS’}ö^Àû¡:¾ôù‘'çñ§×”üÀÿÉýl¬)ãñþRÏñùÃézËíût¿¥ø•“¿r¶ñý¿3oôÞ?ìRWÌÈþãÒÓÏö~‘L²Wý³ýÏaä+û÷í'ó7½øÈûÏé~Ñ?-÷þ1פû»ÛO¿òZFóõãèÀøiüPLûWñÏ ùD§Äüò?^vŸªdZ·ÜÊù?Óæv쟑>­_wç3ü Hyý*åßáÿv/öƒýçÒþè_ü7ñ9ud¼?Õ3éî¯oò-ÇcÓñ'M ì.¯ô'ÝLŒ'§‹¼ÖMŸû¢ÜáîòðÏÑïÆpæpÀ팙e˜–¶N{Iœd-ǰ@^ú;‰jqÙŸ{q„Øñ#ÞçéÚR8Ó’ØÍšŸ²ìB#rÈ‚àÛ8 8AY±ÀA8è}çìÛv6.ÞÜ01þq¹ÞÎ'‚qlìOOûDÀ'Ôüćý~ÉîHy·þhGÃ}«ïB~ŸüßÉÿ¦ŸCð/cò‹o'á«ùÞ¶‡«¯êù—ð?§?ïE|ýÔ>_è}¢ðe¿ÛÿÕª”HÞÏå_[ô©î~}×ìÏ¿Öþß_¼ÿUß»}ÿšîêãèþ±ü ?ýíFëídt?oà½oá°çÙñ?Ce¿<^GòªWÉû‚vì¾åv÷ú†IçòYõ:“Õúc#çñMîþ‡ýßÀó?Å‚|ä$ÿƒüO¾ýÜÿßO£ý™ñ[§_‰>ɽ‡è­ØX³sŽC™–ßÝ™”îaC‰KIn'Ümõ)ù2DŒkŽùÁ-éiÓ·Øë6È1è Þ¥½O OÒsŒ·dÈÙ9³ÆuÁìˆó½Ù˶A2Âd&lfÃ}ö¿ž]3iÁ>g¸à,2ˆ8‚žà±ÈuÀ0¹À'ße˜FD ’Â÷˜>ìD7ዱ-úžàF5gþ6ŸrÿŒóÇÐüÅìÑ9Ñü«×žSÅÞ‹üÞØëñ¿îÛ­~ªWú?æ|™C—p;?Ôý§˜/Ê}L>ýϬ¿¿™×Üžïàþ¹úf®þÿÆê}ã×⽿Ô_Ìêãü`?õgؾÔzyû{¢Ûî'æ[Ë4Ñþ(6GŸÄN8~rÏÇëÀxO¾ÇÒýE9uúËùŸGü¿Ìú/á'Óôéòÿþ!?Ò42±û‡É_Où¥û¿3Û‚}>Ò_Л}ûçO¹ŸåF®Äy[yR§K—Áà~ÀŸ„ü-yÉQ^Öø—|VåÛ˜¸2lØ1 bòt™ž¸»m3±Ï›HNú u8/Vðó¬Áœ')Ç|SÂHM’IdKQíÁ¼gœ‘D†ê!%¥‡µ?<ˆÞ‡/¼‰$pXlFÆÀp&:³H ³":ƒ¸ˆ]ƒ«&X>Ì?' ûr oÝEŸÖÿtÓñ?Ì×ã@xëí¡‡Ø Úp?#=OÝ—ÍjzÙî~åïiò äQ?ûW_2¾¿ÒGÉü§Nßé:ÿÖ½½Þ;Ÿæ±õþ±ö‡ÝH±3éÂ}éü3äÿ¥‘ã‰>¶Š}iuŒ‡Úÿ6^*ÞúqÿÁg^ŸÁA)ê—cÿ®Ëÿ#+qËï>e^ëÁ¨vøæÉéf>7Ù}‘‚šö·í/ÚÛoÚß8½lX“Á“;x“ç$H‰ö³aCâ×·cÙjÖÊòZµ8l?@0±#x0;<š€YìÙàsÜâ-îAçµá¥ã,á2Û°™²xÍ7Ýôã2É> rwõu8þxþÓÁ¼§ÐÏ(2AiÅaÔn#“¶!hAdXD<ƒœ! û‡ %3ï/þÄû‘8ïùgÂÇÖüS—gñ>>÷3¾Ä};#Óõ¦ý?(úCòÏ¡Ïâ·ôü ÇÉø.í»î+jUïó6ý‘‘«¸í¾V#P¡Åóܱ°gãxø·ç/²ø·7éž-ûCÌKpïăftˆø¯Š¤Ï]3Ånlµ¯kWÁ+Ú2U®,"$X±êXö±Þ’dç‰&æføjb—Äñk%qðž­LxqbJ%œƒÆbî¤]}÷ø„:ÏU ‡¨ús‡b`rGÅŒ±Æ¼‡’ðHAkØ£+ÝGÒ–Ù thHÑ÷1Û;â_žLHñ“ïĘØÅ‘½GD Qdi¬3bà8HàωêyÙ Þ ࣢™ a‹p¡q)J úGÆ×´'Ò‚|Pý¡¶áÇġƒg8€F2†€ z;Bǵhø_d/k^Ñ5ÎAÇk6,Á¾ÈÐ í›>Ä0—…áÃ\|­Ë¶Z•¼Ö1‰dXÉébG”“03#D&bkž ß±rxw5M¶Õ©v1Ø™Ÿ¼é<ÖrŸÏ_dùaÔ=:å2Ï£ :²zg îN¯I1.ÖÉàðýŒÉu–ñ{Èìpí¤,›8é v»+VûŸõgÊï ÃrBz>Ãd²HMˆAnqÞçA—\²,î2 à8ÝL±=„?Àäï,Bß1JkeÀsœ;{jÕ®Ðcxç,nßg+ð•Á^Ò¶Ü;s3LÖµøðxÛãâûl{qÅöp#ÍŸc›síÂí<)¶<íBè“ëÃç¿ÚãÐå9m%rØÒÕï¾Ë Õà4±ØDD±° ¦akÂä5àÖÂ8h¾»g¼ÈèüB:þÆeY‡·ã«lÔV¼¿u’•ÛO|ç{{qCûpgfXà;ÿY«à;öÅÛ7ÔR/΢¿dŠÖ_Õ³j#¿Ž² Nƒ­·óâ .ºrØQ‚C&úº`€|B>æAÁ Ç·«·Pî5V8Èpò=†<”Ýx<ø[²gpò^t3ìo`cÒSÔ :6y›ÿñØcGž 6yqÀã¾#•–­Zäðâpoèþ@F§ò1ÃÙtžsÈǾ׺Xx‘ê‡Ý$#÷ṟ2žJ#üмÊðIöODÓèÒÐxæw™ìY\\g6[ôAü£´øDxá·‚.¬²ß£Öõ’-²%›»:à,O¼[íéÉg+$5/¾º6áD gqÓwf2ý=kª[dYÕ‘#ÌÝÙ—©°G²#¡‚ `à‚}Ë»{Ì»ÐbÇuí,+¶*;ük³ã½ºË" à̰ñº°ë[àÿì˜âG¾·O„ܳÜRœùòO¨;+$´¤í22äKõ[¯wX^"o§}œ%Gu4~£ëé¼¹‰GÐô 4ÏÚ-'¦C¢O GjÝ«Qé8lMpÔ`ŽŸ\ à³bĈ^R_PýÄŽïâ^R^CóÂó^B<µô ú^…¿KÂ]èáôÏÑþQôa^(¿‚ý'NôŸù·–W”ü’F‰õWÝgÌüì‡Ïú¡|k# O²ã|6÷‚Ç8rn‘øQ…pE„ðó¶pï<ÎpððÄ›eî¯KËm†^“ÞmÑØ[ܽÆÃÜ:I_éᘶNvE£Õ>ÂOÄ>‚}£è'Ú2Ï¡/¢£èQôl¯•z _Cí3ðŠù’,cÈ~œ÷“ïá‰õ“î™Ç´ýÕ¾$ÿÆ/þA%é¾èí„íÐHÇ©$$'&gvI9xl’xK8y ¼ò“ìyo°â!™h"y$ä²Îq²‚y~£ÙcC¶GèÎYª‰¤ÅÖa/p™#&sÜýßK o™ö½’I¨,î1‰rà,ƒ¨8éx/\È{Ø#ÇDGBlyu~_U„}€?‰·ßú¾s|9×$5,G`ÙàÀðè]I:þi¿ÆŸ_òO‘Hy¤=23o ö%ÿÁOm¿I‚´×ð?'ó>}æþE§¥ùþ%<—ؽŸÒoçðKÇo?˜´òÏÌëí¯º/¼{ÿæÆ›½ì~4‚ûì@w Œ‘aìXIÃdÎÌð¼2ÌÊLÉÞ’“)f[z–Y“&Vxre•áÏ¡–ReêËÙ„û{¥ì.ɚǨž7èw"Ë!áã>…’xnä-=¤<œuÛ×Ú]2 ÜŽ‘5× ‰Ìgæå­Ýßô7Ù $.ä‚̓¼‚#xÚCÌ ö'ОÂ}‹ß?eü^×õ>-MýB½Š[Çåü—°_q)à¿YöÏñkóø™NÑùMÿÚ2Ä{oÅàÖûÏwòãMQG¬#äþÄŠŸâ |aa!a9ÆuÀOŸ©úžeåeÙÓvxY”–]8z̳›Ô¹ÂðôË,°·©[Yu™[e—©\·I”Ùeáe—e[{$õò¾è3á>Óo ,ºœgÒs„œïÇYÁ9uu‹eíËOm 8Îí¶×Þ¼CÞŒŽŠGMÖÃb/rýç‡9Îs†mø ; BÆF!Æ|Yg#`È,‡Wˆ ˆÎ€ˆ àÙèµµã¦KÄe^îž—ÜM·ûÿÕñþbx!ù”óúKù–ý‚ŠyüVÙû8o&ž·æF]¯ßù¯Iü£>¿ÅüXé  ‹ì ,{WPvËÆõiÃô-ë/$} ì˜òÛø£ÂüïþþãÆ/¼?_½ïçî§«÷D}’÷?%cßé#ûI› ý¿Ry­ÿw[ãî“ÓOëúµwðRdÃ蔯|GË_ø %fâÉ]K3.Š_ ² œ“è8Ë8$ä7éÏ£-xË0ýüyäÞ0,f#Q˜™äëÐJÛÛþî Öp¼“»`^êBÖÈd¨ "È‚ë€#2 ƒ“8 à䃇;å!-£fÖØm»ËxÞ;áç~…å¶|ðîòý ôç9õ<ïÔ ]}þbtÊDzý“ 0MúP¾?iÑýÏ›ì¾ð/’ûÃ×/Öÿ«É‡òßý,Ç¿Ð&{ߦÿh?ùïOž¿x}ëõo¿¢Ÿïúq> u~ƒ3ÑÌïOóŸãü×ümêÅä+É~i‘>GçK=·Ým›¿ÃpBömï‹~Ðý˜sì…b}° =¯/‚ïS2ƒ»~³èv'êζX¼!VÑÁ¹pï*Œz8ì~†¯Ù8~ÒvÈ™þùà+(!XDN2 "x2Èð@XÁÁ·q†]ðkÁ ±Ñdl¼1³éÿƒ-ž_SÆç–AÆ$–YÆ22“ê‹×ņ1¾ÉŽÄO³¶ñÒü…ð?öϽ}rýÅï_Êσú¿,‡ŸÀOûNváÏ‘_-¨OÀ`<-O¨øROWÅôgó?Å#Ûz ŒŸ%ùçΓ«"|‡û«.ö³Á|7Á|Å Úß©}‘ñ³½—ľÛì…ë„ ˜|,{Aö‡ä#§„GÛÙ|Ià1ë3ÖC؃èXï¨6} ÁÎÈ}HÓ§ÑÔòOÄä}>ŸNF[¥ôV·Þ+lfpdæõÔã…°Å[tãcÉ=·øàÛwwlðµGF2;%…ŽÁ|w°zìyxŒˆ8Èã®1Á™‘Æ@Fȯyb8KÆ6NÁ–Ae–D@ºO$§Œ@¹ƒw˜ßdÇ^~=='ù#Ù~¢éï?yê#÷WŸw‡™'äÇ>OðIy¡ó|ýr>Ÿæ%࿪d/ú@ƒÑO§_J)â—Šÿ¶þÙó©Gpù’zJ}Ö_>ë/åxôŠÔ8€ÎÈúuˆÐ=¬d êâfxƒícÚÇ©cÚÎx³3í'ÙÔƒíc|Y³e; ûI³ž {Xƒ'lÁ±b /§X³–,A³²lÀ°,ú³¾¹,³[¼ãysèm—èvßxH²øª6>êǽdŒ°äÝ!¬``Jº#Ïs‘'©òpÈrÄœ%‘z UŸaŒçÏ[¯(Ç‘ÎoAE„AdyŒ€ÞXŽÉ#æÄ1"y=D|/Ãuùë~Ϫ×ù#ØÏ±ž’xùúáüÉ>Gâ'Öl|fýYð›ùíb>5 >ŒôBô4J¯ò^X~'Õ%XÛåSO?™/™þéŸ" P¡ÚöoŠAq=äP{AöƒíÐ=¬{AöƒgÚµŸn³ç-Åñ k6,û@±gÚÅ€ñ&ޱbÆðE‹$Ù³&nxbyسŒ6ÂÎ0úéÙyyÞ yòxÇëè97¶CÚ¬àà{Ë,3É'€ëc³–ätASÕgLHËñ,…ïÉ:‘ÜlX¦AdyºŒ² èà"89,²VÔÐl:5âLoè­~G­l_­Ïo™ù/QlÚþD=O뫳Ì(¡Ã!ÅtñwðCŠS¿‹6} ï‹>Ä„ñÚÏŒƒíˆ™ƒN£‹„û XããÇ68 fM˜<˜XHX¼¸%œg &rÏ-²Ëi¼ë?FÚý3ÊÚsëo"ÌNqÕ¶Ýï9Âý:Ë9iiÀÛgg½[÷>ƒàm9Ìc¸@87ɰ›}¢DýÏМæ&xÁöïâI#dd@ðE—{‘±‘e–@ÁAÆã#‚_¡ ñp ôK3d8€ˆŒ°l ;x"}¬ûF}9㜃Î@±íñ%!Ç13̓1auae–YgÓ—\çÖ¼-²ËÂÛo;’ÊKl­¼-½Ë)<ì¶Ê}m¶Ûo+o ÆÛœ­¶ñ¼í²Ûm¼m¼­¶åç}À„õò’–ØÁ±¤!-°›k“­‡¹–ûâcu?N]å®ÒüTFRr UÒìÖ2R,`î,ä[x"˨‡ƒÏÐJF¦¤$Œž11À& 6Ä6 ðó8<€³€ã,Ž0˜áyÞ6Ûmçòx~—èÞÙe—†ÙxÙxÞÞ4¶We¶Øe¶Ûm„xm–Ùam·êXxÛeáy ÆÃlo:ÚðÅœÃ{ÆwÂN[Ï{ÒÄGn§2Ç­Ù<][Ý|­g$"ô½I}¡X ‘º!î84³¨Ì‚¢b #¢è7ƒŒÂ!ˆ _£:²f[+fˆ:°€‚Ë,6ÂË 8Î3€º·8ôúw†müÏüŸ¥çxYe–Þ6Y—Ÿñ¶ËÂÛ//rÛod¶‰ÆÌ6Ê[m¶ð¶ÊÛ,½Ûm­¶¼Æ¥ß=FY®ßa—$/ºÚCî%àïÿ„‘ýþêŸí…zö.YŸ¡„jý/ˆ¶Òƽ¤€œBiK”lG•½Óö6pÙgÐG’×Ù$;¾/™ó°6 ,0½,Ã3€à†889àˆä‚!‡³è8.¡‹aú…çonèvÞ7ÿ7ÿW9m–ÛxÛm›f[m¶Ùm¶Yeåe–YIq˜¶’Û ²ËÂÛ,­²í²ñêsœ¬ƒË>E>wàO‘ù.é·È<ð…â}*‹Ð^†B½ý£}Gè^^<ä¼¢þ²¯jdŸ%»Œ÷T)áÜÄbv¶~Àøfo\­½w¨õ’eå OØú9»°AÈ;Ùv¼®¿Ÿ™8x"|ß|ëðÀ‘îÞ aª÷‘œÆÄse¢FòCo!ÁoœdGÐr±à—ƒƒ ÿï¡à>‚ÇëÞGã~¶Þ6Ùm–[e¶Ye–ÛIe¶Yd[k2[e–[e¶[m¶ÙL;CôBò?qÁ!Iù®‚ÄÒzjñeöס—x¥§In+òO‘E»-n)•‡;[£Uï¯Üƒêz÷gÖxǯ²Yx˜7¥ :2> ÌF we£åšÒZÚð©l¨ÛjIvn‘³Ã±0G†Á«jýïo¼\2wƒÆ×ØþâsŽ÷xÎ3‚l׺¼8!°}"†~¢:,€Þ ÎF9ˆ]žÃ>ƒaˆààxÞ6-·’[Ál6õ6‘ÁÆÇ>¯Öý;ô¼-¶Ëm¬­¶Ûm¶Û-¼7‚Ë,Æ,ÄI&ïä--ü÷’¯!À¯"úÀxXž#ÑGÒKÐÅ}9™ù¯-EÝþfõ˜|§oåÃÎÔ!ßùðOZ õ«(5ÜÚöLû+áIÞö¡Í>í«;½³‡˜^ò­K•²­ZÛÊLjžãcœ2IìçS¸Ö^0–P¡˜I÷E‚ŒŸKk›{,ºÎKo‰q‰áyÛYÛõÂp€gq ¸oQoÐ<œ»p/ F[m×9 ±ããB8Ûmãc€ä$Gǵ°ñ¶ÂÛ °áÈËÀñ×ó¼m¶Ûm°Ûm¶Ìʼn%å'~ÿ!>@þ·’†;¿9^pðƒú¨^+Ò¸ô¦OŠoƒz…ÒO(¯- ù…k¸`¨|¤uÕÝ;©ó³ ®mÓåÇÑÊ}!{0š©{Mò_4£åµ/ÞÜ«VåÚ•ça%yLN= >‚zôòxÞ–ùSZ•λV­[m¼wÆÛÆ/wÇrýg;Xs–kÆrÛuƼe²#ÏQÂÚ¬ç=rpïD‡Àg)»`€$ÒëmêHýè¿3×ÅöãP/µ ®O¼•X[²-ÕŒKÔ-µØQ€Xåa[B ÂŰ‘8Û0Ûkj0Ãiâ¶!1•‡m-¶Üm¶xmàxÛ{´†Ó¶LúŸ˜žã!O'R|Éá>½á#ø\gÐVx¦øñXà¾n¼¤ßæžïê \Z"{ýE·Ø¹Y‰2òÁ¾Î¦bNtÀ—»nX´ÞíÚµmá¨\›m¯Ž6×éï~³Žíààç¸ú|KÆÉßÕ¼-­ŸAôégvýżûÆÛÀ›Û9 i+ÁƯ-f¾ÖðM1·vKdHªF¼ƒðÝÚïåDÞÀáµz ƒov¯Lh´È\6[Xa¶Ñ‡YƒÄmàxeˆm…Ka¶&Ù$;Kî„ùvä’î¼´ùJð>®?‰ôµèg§¯EhðDx¾e^f<„¤×óH»±^þÖΞÕÕÝÚ³@Ù˜smø§n„bU©lÖåKm˶ڕµm¶¶¶ý:OÑÒäòý{gêÏUùo%}·øGÀièÊGÂ^‰ÅéÀ¼¹ã§‡È¿!ù¤=þ»$Æòäíª=ÅÙvÞG˜ßdE§Òêð’²f*× ó}8>ƒžÅF½KúÒ_:kò”|‰>wåOùUÝKêP/+,^­¬î Ôàí²Sжg: 17–íìÝ帚y5¶Wåú¿ýü»ÿ×Óží#“~ƒ~²wé`ÖA[ôïÓœ–ÄIž£o'ôaȱ#Ç|KÀ¹Â·sl»ƒÆpx¶Ö3m•>é7³aìNH0^­£u–¹«æÇ6žéwêqÔœ)uF ‹!Ÿ6Þ õNñùâ7 ì¿™ô¿_À>Ü |«Ü<“>ê–ò­º±gºù§Ý„;Ê æ/uº®û .û,{xêÜ+|žV÷,Ím¶¶ñ¶Ûÿžý;<ï&ãëÿ–›ÿ¦òðÎq¤±ÆDø!ú­KÙçx ;½yÈã¡x>·á&-äÎ2Ün¸ÞºãNµžp´À¯†uBi»p&h›·p’ãÍ¡%7Ifù¡ø˜[Iàá»ÇY{¾kÁ–¨øðd“ìM^qg[´"ò˜Í¶Ù\·“†¬]ó¿AÆðò?F[ýô'ÿ‡¼úzÿÀúãxÞ4ã¹n®÷’Ì™ °7\ï\ä"‹¬=Ë=[ηr(ÞPIÜ¢¬}-ß:ñÕœwô+wœƒ"÷ÉažKVêý ueãm€qP ÙÒŽ¦"=”0˜Â:øçmŠ5w]96ðã .ž!Â}ZûPž©­­¼l­¼;Ç{wÆý)ëwÇN¼w6p¼oÕ¿FĶóßÖý }/Õß/²ï׎ÿàïÕ¿QÆGVëô¿FËmmŒavÀíáí‡áž¡2ëŒéa»{àóg8ê-ç\àxnŽG…x?ð×ÿnþ¬tµúNÎMç×èêÞ¾áÎ;ÿËüGGôwcbß âvtåÝ(o\a¡3‘z¨÷ØÆ ŽÍ >7XIt.7°$nJD.É=)¡2¡I8Ö¬".‚Àu`Zwt2Äa$ŸF!î ŠŸYÊf¹ÐöÈYNø)õ¶ÔŽí©Ð’kÄ …3†¶lŒ:‡SÃXÞ³Ú_ }"ý¦_B}Ùß·>t9íQnìäpBl¼7ÂøŒ„-TÖÇ_Aàá#2Nå¤Q'ioÁ»Á¶wœ?GGg q¼!ô‹Éô×ÑÑeÞEŸJrrp–rxám8|Ûþ};õg;õuœå£ÒôÔ7zÈ~Ŭ0í#݇½ÀœTä¼'1bÒeèLÓ,6é¾,w×I8;%î¦õÂÏ´Dn#¾=פ™ ½"„õt3‘€Œóê‹ÇÞJyr¼é±îÈàI´]eZ•mmnímú7“Œú6ýCKWˆ1WN£ÝÀ ‡€J¦OKÜñ›‚,F;dŒîBSƒ® H¶Ó ’ôˆ§³÷Ò¼x`e Fé=„È[9 FdÞ¬±³‘zra)‘˼áôi°÷¬Ÿ÷·Y=‘–ÚØ£ µ‚ÝpçÒM„gÓ¼'ÐymSŒëdá áÞ‡x<›b 9Üú;üe«»1uø£(¢G¸°=ã-ذ¼‘í„õaº\€>5`õ’w&×t“Y !bš£Ô>“ØQŸPכؗÁ KúÊÞ­¤{ˆwT?p,=¤oÚG¬b÷wìQålÄ ƒ±ºÁ•ÀZw!Ü BD+pmio»8ŠÀF Ø‹ÓDøS¥Ý[憭`´\î`’rñFze³»RútLD"ÉC ç¦è”ð! Ð+×Q¹(¿0^ñNu!ÀG#ôuo\g_H–ÄÊ“Æa½]äåíô¦Ùcñ7¡Œ/YB÷ƒZw§‘àÅñDÂ@Ü€°ËHeeî],év›Ù|¼@¼nš3/X8Ÿr}Õ¡åuyá¯6ñ¼lÇÀýOý:Bï?QœÓ>ÍœˆÚö„ZÞÐAaÕ3,³\÷ŸBé°ðgÒýˆ>VÓÞõµxmÝRž¨w7ܸ۱ndÕÙX“1h¼@C46áÀ–„mw q%Ü…á‡pÉÛǬ.ógæ"î²Jõ×|f…ƒ]˾7ز¢#»«Úœõ[\ã5œŒÛ{èÃŒ±Úùc4Ï¿¾ç¼ÖêÃ¢Ž¯ÙŒ‘“CKH>äò¬Ol<¾.§bÎþ¦Þ±¶úF-y’„ÊrÅÝ ćw˜ÂØd$2ácƒ>tü“ïÎg$Ëö†x'ÓÖ`§«lym{¶¾à'Ìœœø¾dï•üO”'¤Yĸ/ª H,9YaIDô|†D‡{Á½m‘Üx^ÊÌñxp0¬È¾Yq˳Ë+kîÊð <p¼Q ä¶Ï¬ã3Œ‹y×x2C‡Ž¾œçhA ÕÕí,{z£ˆ €¾=!ð¥r]YÝ·OF{ _&Ò›ä-ë˜÷M¬Y­›Ä&ò“ô!®ˆCpXiÇYda½DHaF@&ºö’Ëñ(«Э|©ÓËk%^‘àŽ3,2òg%špwÓ<C߉J'WFŒžHö}&í„“ƒ'¼ÂÂs‡Ž–Ð`ÈI÷ŒƒÇdÞ4@ëíÁæõƒE3€»忼YÝŒÉÆøb1ðâÒÒû‘À!§J=% £\Üu4ðX{ŽúäzÓofÎ÷íI“»*¼¬­½ó[Ø[Uò‘÷’Ë Ë =:»ú3ãnÞ^sœã¸>£ÌöýÜ<—{6o£¶¡@Ø÷$×™ÓÓmÝ˰bÃÀO´•/ƒªØü_ éC¹¨×²È,K~°÷‘£Ü aðFIˆ#'Þ.®ˆ ¨€°€ fÌ×= I;DåгŽÞdÏ3Œı®MiÑ>ˆ+E±gw”ú*ù­ŠÀÝÙ¯'dˆs9u‹fðg9 Ðv‰$c9ÉíkM‘Â!rÖQ€Ýpo#‹ÆhØÛÔc!>©4É —Qxßbâ×%c¨Ä&`Gý4­ƒ­&0ÞËÑÙŒ$Xö°+Du}Ø6æÇÀžÜè™~É.‚hû²ÎÕºÛö¥WúF]Kkœ6£ôž2l“Q:‚-Mg{W#‘¸!¹eƒ9~‘¯rÆpñ“ôuŸùuÂ062¬µa½·—l·wÅÕÑ`ðNåZ·mj³= ±ái†ð¡ŸEùîïÀFèFÝ /EРƒFpzÍ—Y LÈAøÉ‚ ˜àzS-m”禬hõ|DƒÁ,C‹¿E¾E¯ZÊwÁ®ØëÏfiÀŒ2Ü–;ÁÛÇC<÷)¶Y™x[[qï„ç,ãyÛyxBK - -Þø‰oçË=¤dAŽ =ˆç7œ˜é›¾¦8nL’ÑàZ%é:—ºUàåå5-vsÁ»ýÂ]»[\·äºû,IŽOKhÝ‚Ð]Rf Õ$=á…êÄgLML"âNB³¦ªî*J6Mx@$–,dmö)î/ï5ð;Ç[Œé»¬o|NsßÇ­ŒŽÈ°¬Âõ´Lôqg†¶Õ»Hj;£Ã u(‡l,í…¾l8„f³«3qƒ` p-H³ò“’¬I½3Ë mÌI9t˜Kw8_¥·f¬íÒÝ<nîîçw‡‚YÀYÛdKøfȲ8A³ÂHqÞë ÚY`½s–;9czìÇD½ñÐìòxxê`/g%£âE‘ áú>ä†jjÇX)šÂ™bptÝYkŽ»«&·{’;–u6ì.ìã¬À,f8ÝmûÅšGN‰cwãö¾½}g 2lÏÔŸ*Á]Hxw±Ï 'Þ%½r¯œ±ÛO¼¾FÞì<^…‡ôm”¾Âä‰ïN ìU/BÛ@Íî×¹µ³‰vÚÛÜq˜m½0§EÜŸR8ŒðÛÊÆ.ñëoVðhÀd>ކÓ;ÉsŽlm¹ *ÓêÂí­‘ãÙ‰8a·LÖ {–qxnsêd&0ï.¥6RǼÖϼ,HÙ™l ËQèŸbH¶3r¦.åÊz­ïj}Û#AÖ:ˆ™`¼< ÆdÃ[ºë$ι 0ÒGaëlÞ:õ&‘uà…„|ÊÛZ§V¤dmmµÙ[[[!€@‚Áyqam­¶›‘| 13íÙ±êðBeê:&’Ç©9†q& ÄôñÌ1ÑÛa1ݪëeŽXðfÙ;f1Ǥ‹„d,ˆ€Ï—–ôú:—Žù`áÿǾ—”nìxï>“³Œ~ެFxx:9n¸ŒR2K’yÅ~„²9 ÃÂŽH¶AxµCÅŸ) ÍŒ<›²ÙùWÄÉ:Wв.ý–¬¹Ý\Õ^åß;k<¬$Š÷Ü‚Æ ÀˆyØÇL²)žPGd½X:Ü÷Ø|0žñô§ëØ–òåÐÕž€(óÔÇ;P²i¤œ&Zy•n2|ÚFF=I‹ÎÓsí’º ÂQ<0½ ×HСàÀ@<:8kèBËÁäE³’lH“ç%÷Õ®–Ö½¶¾îNÀʲϣI¸õ›Ö ”r^‚DžÙR)v·vpÈ©w à“©$²Í³èÃ’9õž‹9_ürï8Ï£#Žì“‡Ž¶,푱&v6 ;åÞ1lll$êàÖ 4±áŽCŒAÀžØ¡'¬Ëaí"ù^óè×£à zBk»¸·ªpöåz¨û¬Xö. ~$‰±â5e„NpŽ-3Œõ#M÷‘“æèBx(*" $áÆš¡ÚŸ‘¤0ÃR@ãdí¾9±byW=Sï3í^ø#kt‡{¤î‰›®Þ¿O­ÖÛ#_,Ã2ëѯ¼0%{Zzû‘ ˆ„°xØËEÙàzàe)@‘Y¨ …ǹDšÅD©[ 2êïݵ±`Æl‚s<ŘØYdd% —ÛÖN¬äz´u±Š÷¢¶$;%!Ò×µƒ[¬ ^ç\8Èvpo¬„“{G ^Õ{W™Oƒ¿^Ygr$Á;ÃãŒÿÇ$³’Ë «úAå‘ËyÎG„`ÒÇ![öaKDôì]Ž‹|†ÏV{ªv°ð¬t*½ÝÑúÉ3çËù¨ÍS ½YV"bÌCl A!É+„m²Ìll€ë9„èK=FÕ6WDQ¿Žóó.™Fô›Ѫ–×ÄÇä~Öð¨‘eá4AÛ ÷ïÅ>,è¿ÛÚÍ»c~Õah°àú±´OœÏ#÷C![ÃÌÛ/IÅ×n—bÒó3놀³Ã+ÕîG…Xã¸Ñœ>{桜º1?ƒÉfè]¢'~ Å£öI©X錘ð'[ÊNDæbÀ>W —KÔHgªXYdO*Û„ B5ÖÙA)àÁÛK¨dl |½ W`–-¼ °÷Á›Â7Ié’Æ`=ƒÞß®HêæÈAÖÛ-¬² ×2È0ᡱ¡BÛS¼8Ãç¼ã¿üsGŸ§³ì½yëéï8È5°Þ md.‚€åƒÈ·“»Íµƒ`>ÛH—Á Œm>(!‘o*ÕÓ;<¶¤Ç—EŒâ³° HX/Eç:\HpÝØOdÍ@·Ù?¯ÈeŠ(w11 N]xº˜™‡¦)Öõ/Z,ô¡h9èþ‚ð÷·×ø!,lÁO§âƨï }ç,„HE»ô.Á.×w£ÓÔ&²4 ‘&+¿b;'èî}‰Ñ°çßS©-aï…Ó…Þÿ$>Óøa?Ø7‘.¯;È6W†½"ÛÕl׫l»g ÐyCy± tN¶Q¤²£ze½tšCµØFB³Zî–ÓnÎ>÷?†]†G¾ÊM80KÜ¢­°^#Á'=ìÙg îÎÛË/dÙl¢ €ÖÓïÖÀ7ôv¸ú=ÀjýA•£7]‰wTtꋜ—jê·ÕÀÝ|BgÚx²ÂSÚG´‰ƒÒû a0ŽCÏ©õQôzÏLJƒ†9Ë;ž’1±i0Â$ôËöHú”î÷6i®$oÞÁM`ÙÉ&Å›.¶Ç·Ú”éÐ èÆž¢v¢ÅsY3Ok8 x‰—QéãwÑ™ÿæx~׬ß7·ÃÒ ~†jñl»ˆ‘êë,ÌîK«‘îÿËûM -7¸:J”|û>Ìš‰z.'f6B뮑évv†Jyp©ù„.`0c²XXrñ‡«4#àÝ¥/kgW´µv“Òco?C ½"%Ÿ‹±Ë„ú RKÿÙfwupd-2.0.10/contrib/qubes/doc/img/heads_selecting_rom.jpg000066400000000000000000002402101501337203100235110ustar00rootroot00000000000000ÿØÿâ øICC_PROFILE èmntrRGB XYZ Ù$acspöÖÓ-)ø=Þ¯òU®xBúäʃ9 descDybXYZÀbTRCÔ dmdd àˆgXYZ hgTRCÔ lumi |meas $bkpt ´rXYZ ÈrTRCÔ tech Ü vued è‡wtpt pcprt „7chad ¼,descsRGB IEC61966-2-1 black scaledXYZ $ „¶Ïcurv #(-27;@EJOTY^chmrw|†‹•šŸ¤©®²·¼ÁÆËÐÕÛàåëðöû %+28>ELRY`gnu|ƒ‹’š¡©±¹ÁÉÑÙáéòú &/8AKT]gqz„Ž˜¢¬¶ÁËÕàëõ !-8COZfr~Š–¢®ºÇÓàìù -;HUcq~Œš¨¶ÄÓáðþ +:IXgw†–¦µÅÕåö'7HYj{Œ¯ÀÑãõ+=Oat†™¬¿Òåø 2FZn‚–ª¾Òçû  % : O d y ¤ º Ï å û  ' = T j ˜ ® Å Ü ó " 9 Q i € ˜ ° È á ù  * C \ u Ž § À Ù ó & @ Z t Ž © Ã Þ ø.Id›¶Òî %A^z–³Ïì &Ca~›¹×õ1OmŒªÉè&Ed„£Ãã#Ccƒ¤Åå'Ij‹­Îð4Vx›½à&Il²ÖúAe‰®Ò÷@eНÕú Ek‘·Ý*QwžÅì;cвÚ*R{£ÌõGp™Ãì@j”¾é>i”¿ê  A l ˜ Ä ð!!H!u!¡!Î!û"'"U"‚"¯"Ý# #8#f#”#Â#ð$$M$|$«$Ú% %8%h%—%Ç%÷&'&W&‡&·&è''I'z'«'Ü( (?(q(¢(Ô))8)k))Ð**5*h*›*Ï++6+i++Ñ,,9,n,¢,×- -A-v-«-á..L.‚.·.î/$/Z/‘/Ç/þ050l0¤0Û11J1‚1º1ò2*2c2›2Ô3 3F33¸3ñ4+4e4ž4Ø55M5‡5Â5ý676r6®6é7$7`7œ7×88P8Œ8È99B99¼9ù:6:t:²:ï;-;k;ª;è<' >`> >à?!?a?¢?â@#@d@¦@çA)AjA¬AîB0BrBµB÷C:C}CÀDDGDŠDÎEEUEšEÞF"FgF«FðG5G{GÀHHKH‘H×IIcI©IðJ7J}JÄK KSKšKâL*LrLºMMJM“MÜN%NnN·OOIO“OÝP'PqP»QQPQ›QæR1R|RÇSS_SªSöTBTTÛU(UuUÂVV\V©V÷WDW’WàX/X}XËYYiY¸ZZVZ¦Zõ[E[•[å\5\†\Ö]']x]É^^l^½__a_³``W`ª`üaOa¢aõbIbœbðcCc—cëd@d”dée=e’eçf=f’fèg=g“géh?h–hìiCišiñjHjŸj÷kOk§kÿlWl¯mm`m¹nnknÄooxoÑp+p†pàq:q•qðrKr¦ss]s¸ttptÌu(u…uáv>v›vøwVw³xxnxÌy*y‰yçzFz¥{{c{Â|!||á}A}¡~~b~Â#„å€G€¨ kÍ‚0‚’‚ôƒWƒº„„€„ã…G…«††r†×‡;‡ŸˆˆiˆÎ‰3‰™‰þŠdŠÊ‹0‹–‹üŒcŒÊ1˜ÿŽfŽÎ6žnÖ‘?‘¨’’z’ã“M“¶” ”Š”ô•_•É–4–Ÿ— —u—à˜L˜¸™$™™üšhšÕ›B›¯œœ‰œ÷dÒž@ž®ŸŸ‹Ÿú i Ø¡G¡¶¢&¢–££v£æ¤V¤Ç¥8¥©¦¦‹¦ý§n§à¨R¨Ä©7©©ªª««u«é¬\¬Ð­D­¸®-®¡¯¯‹°°u°ê±`±Ö²K²Â³8³®´%´œµµŠ¶¶y¶ð·h·à¸Y¸Ñ¹J¹Âº;ºµ».»§¼!¼›½½¾ ¾„¾ÿ¿z¿õÀpÀìÁgÁãÂ_ÂÛÃXÃÔÄQÄÎÅKÅÈÆFÆÃÇAÇ¿È=ȼÉ:ɹÊ8Ê·Ë6˶Ì5̵Í5͵Î6ζÏ7ϸÐ9кÑ<ѾÒ?ÒÁÓDÓÆÔIÔËÕNÕÑÖUÖØ×\×àØdØèÙlÙñÚvÚûÛ€ÜÜŠÝÝ–ÞÞ¢ß)߯à6à½áDáÌâSâÛãcãëäsäü儿 æ–çç©è2è¼éFéÐê[êåëpëûì†ííœî(î´ï@ïÌðXðåñrñÿòŒóó§ô4ôÂõPõÞömöû÷Šøø¨ù8ùÇúWúçûwüü˜ý)ýºþKþÜÿmÿÿdesc.IEC 61966-2-1 Default RGB Colour Space - sRGBXYZ b™·…ÚXYZ PmeasXYZ 3¤XYZ o¢8õsig CRT desc-Reference Viewing Condition in IEC 61966-2-1XYZ öÖÓ-textCopyright International Color Consortium, 2009sf32 Dßÿÿó&”ýÿÿû¡ÿÿý¢ÛÀuÿàJFIFÿÛC    ++&.%#%.&D5//5DNB>BN_UU_wqwœœÑÿÛC    ++&.%#%.&D5//5DNB>BN_UU_wqwœœÑÿÂÆ"ÿÄÿÄÿÚ øÑ(((P–H °X„Q reH ¢QR‰` °QR eT°ÉQ;9:bgYº4ìÃP(° §~ ÍX3Mƒ[d0›F¦Á­¶ÛF¦Âk›¶Âë›¶È`̘3Mƒ[d0f0f0g ÃKqdLYDÅ”"ˆ¢(Š"– p@YH€*£T¦ Y@% ËÁ`±RX2€ , ŠEQ`XJ –³kZ]·HêÇEŒ˜«$AAulÀÉ( ¤))ÔXFCCCpŘÁ˜Á²Û¹´jl¦©¸im[¡©´j›†–Ñ©µZ¦Ñ©°kl›¶pÅ•5¶CpÆf0f0d0g*Lá‹"I”"–(Š"ˆ¢(Š ]¢@ €J B€RJ"‰B,P‚¥ )`°R, K,[¨Z” ¡€ e( @J– QH¢(Æf1™ŒYSc ²’€‘` \KqÈ–PP`©@,2Ç,@°Q, "Œmg !„ÛƒWEa6CpÁF3zÎz óÝñ8]ÐâvC’ôþ.›²Z%ɀɂ³a ­PÜÓ íuÐ7´DväAž AR‰JJT@¡(JÁ( X…J@%€…ˆ XJ-ŠD¡(°µ’ `@$¡jP”©A !e€Y@6khH %P e”°* ` Y`”X) ‚ ¨*Q(€ÅÖØ­-ÃDè‹Ï:aË:ÕÆë«¿(Š‚À€ Uˆ B ¨,BÄ[P€E¤Š”¨*ˆ¨-Æ•„ J P¹% ”X*X`¶ E "  °©@P” @@*R(RP„,¡”ˆ  ¿2P”Q" BË‚ €¸­DTXTEA` ¢!P¹H ¨*  ¨– ¥ @, B„`…Ѝ(€( RTŠ‚¥P ”¢("€(AAe©T BÜhX JJ€q*R,“)PTvÀ K `!DT(E@€YÀA`¤ !P¤T¡("ÔE¡””,J`©D° ©A ”e…‚‚P¢€Ä)e,¥XRPe„ €%X( E(”"ˆ¢(”Š"ÀJK(‚£¶,¡ RT± @ BÀ²¨E± €B KR ˆP¥) @( PX¥!e P`¶P!*¥€(PK@($    BZJ(¤PˆQ( X”AR YP‰%„G\’ˆE@X² J€K BXT ‚X(EŠ–%‚‚-DPP,‰@@P”…¤¤‹*¬¢Ê @@ @”X¤¡ E€‘DP–%”H–±Ê1#®l °²À"ÁaE„°@  K EK ¨@ ((€@(X RYP I@QeR€…””R(”%%H¨¢*¥`P(Š"€…Eˆ¢,°bX…‰V%"y©@€ˆ¨[ , @TX„°BÁPDª„Q! €V„ÜE€ @Ê,¢Ë$ U¢ ±@"Ë ƒ)@U”PËe(Š%ˆ HR(Š%„ ¢(¡@ (€*%¬€¥Ç¤«±* ÊÈ„@ `XX¨"X€–K°BˆÂÀU$ª((Š”YT¤ŠPRP(”PT” %HRP(”RÁ@R(”‹(Š%Á@RPŠP ŠH PÁ,¤A „K „ -‹’P% ¤, – @!(€ @¢, !(I,  IUe RPPR(€ A@”  RYE™P)E€ @¤ZEi ED ,J‘D¶)DR @%U”ˆ%–!P`QIIØ ` ²€ BEDX E„%%€`TQ@J¡ADP(D ˆ(– BÁ@ P)(JJTE ¡(D QE¤¡E©Š„´Æ`Qe"Á2Ĩ ¨BK,’Â*±X&PÅ@A ¬@¢‘`XQaeE„E$Q‚­J©J ©-,´J%)E ¤ (Q*‚‘@¢ÁR…@€JÂK*ÄP)J©¥Š"‰l "ˆ¢(Š"ˆ¢(€(‚ȱbâRXIU%””@ E„P@DT@J@… bÔ” D”EV2ˆ*TŠ$¢-¬TET*E¤ªJ¤Q*‰@ X(% ¤QE‚²1PÁ@  ÁAPPA@ ª²Ái AD´E± B‘@¤QE,™¢‹ $¢‹*•TXE‚XEUA`)€b`"ÊP,€J¨¡(€ cF*²(…"ª(Š"ª2‰ª™XEˆQ˜ÛLj‘DQ(‚˜²V6¢(” "Ò•H R(”%¢PU–2BÚBQEiEDQEQ„T– eI`%%`–TRE¦ Y(Šˆ£PÈ”% YeV,°ˆR(“(E•&R¢“TÊQ(Ѝ¤–ˆ¶±Zcm1¶˜Õ"ˆ¶ÌV‘Dª‰@‘`U‘“1‹!FB-1d"ˆÈEÅŒ„Qq´Š"ˆ´Š"‰@©eR[bQiaUJ%XŠ"ÒŠ"¢-1QEDX%D”I”$¢K*,$ËŒ±"ÄJ¨¢J"ŒYB.!a°¡€ ‹a(¤XX, €ReTUIiQjIÅmFCP‹ID³(%T´Em˜Ú"’[LY+ÅŒ†6’2Q˜Ú#!ÊÕcm1d"ÓCi‹!¢2†7!…Ècm0¹ Yȸڂ¥•Im–U%X-1Z±DP–ÄQEF,¡ Tb°J$Êa&R¦9D€€‹+e¢,"‰2„QŒÌc2„X±G<0¡BX `P@€RQK/BÏ6ý'V§È¾Â'ÈO¯Æ¾IõùGÔÃåßM‰óO¤ÄùÇÐã^ÝÄðï³ëbyoNmï•Â퇬r^ÏvM°ÁÆÒcTŠ¡IDR…"Ò(‹Lmi‘DZE¦6ˆÈbʘ²V,„\ŒÈbÈbÈbʘ\†,‘ÈbÈcm\mDe%PZ•@¨–ˆ¢2ä².,†,†3"E. J1QŠŒV$XI`–TQ&P“(%„XFCe‰ed1!¼µ9ÚB%* ÀU”»_ÑÙ}lÝø¡@”TYbX%V38˜3’á6 3t4ãÐ9]R¹'dŽ)Ú^ ÞO>z0ó^”1é*zƒÊž´_&zÃÉž°òª<—«-êEòÞœ<×¢<ëÜ8]°ãuÙÒ9®á¥¶3†6‰( ¨* …"ÕŒ‘´ÅšXʘ³K‹*ar¦dŒ.T×r33† Æd0™Œ&c•&P’ŒfC„X‘KЉ2• DQc2‘bÊQŠ—Œs TE € T ‡¡õžÑvç‚ÍàŒY Y(ÅiŠ«PŠ1REV*1e 2„™,ÅÅKŒÎ̉ŒÎ.3!ŒÈc3‘ŒÈc3‰ƒ(E„™ fX’QD–,XIU%F*\fx’QŒÈc2‹ŒÊÌ¡ŠŒ&r0™Ã vC\Ìa3Æ\&c[$a2. 1Y(‘R.s»ÆÛ¥òæ²ôÞk9rX꼃®ñŽËÂ;ïž=œ=š=;åÃÕyCÕÏÆÊ½©çujn˜ÝK(Æd¬fCF+S”$ÈbÊD)ŒÊ,Q‹(IÅdIÆe^ /: P!`K(!@(‚€ JOíø¾ß~X,ÖR‚ˆ¢*"ˆ«1e*(“(E²‰‹)Q”$Êg YBL†*LTbÈc2.38c2Låc3‰…£Æe#Ú¶Z:xŽiëå/«¤àžß˜s=Ž*Ç“èŸÛðýÞü°V³J¤QE"UE‚e¢(ÅI&JÆd$Èb£F,¢Ĕ32àÊÌ¡‹!„ÎYŒÈ`°Ç~©[¹5/©<Úuìó¡êrëå:utò'“Ù¬æö<¯Z]ùÛLû5ñÐǘÃÎéÒuvðé]^߉ê›{<Ž´åò{¸—œ\qÎ2‘ƒ(cŽpÇä¸2† ¤¸ÌñŒV,™IqQ%KR欫”¨–(X%ÄKƺuåªÎί7»sªãž³‹)X²²Å"–L¡&PŠ$ÊQ&P‹£Ì‹ŒÊDš—žÖ ‚…*¥”é}ÿè:òÁZÌ)FP(‹(£RȰ(ÅDQ‹(Ee*±QD”EQb£”1™Ã‘0e YJÆd0e ÈÆg fCCPg„— ²LƦÈkg &e×3†¶pÁœ0™ÅÁF8cŽr1™Cr. ¤a3‹ƒ)㜗”$©d¨‹!b[e€•(‹ Bãb¥ÅÁ‘°èçêåÔË·ƒ¶ÏCf­Ý3&RȨÅD•X²„™ YH“!ŒÈb£IqQ%¨Æeyv^[(R€¢PªPPé}ÿè:òÆV³z2ùKDr\rÔ%€Œó4·èE„të-¸Ö ©‚ŒYà‰DXE”T@E˲’“”"ŒJ˜ç fPŠ\ fR&9DŠ\VD–e 2† ¡ƒ(c2)p™âc3ÄÆg œ0™ã̆$¸Lñ1™ã.3(c2K‚—F* ”"¢ZX‹ •d¢BÙH‚ ÆÀc]Ü}ÜÍíáï;÷jßӲƢ‰2„™ fBL¡Qb£PÅK‹$`£”\ ¼·2ˆ  ° ”  Q(ú?¡ùï¢ëËk;=/'ÕÆ¹½ ZñrÏÓNÙ‘£§^㋦n¯#›ÐàéžÎß3ÑÍÇ,qÍÑ¿«#ÀmÕÛåÇ^inâüÞÌ2ôrö¼ocÎå¼¹=Ž{<Ξ~Ï[Éìäç¬tt÷k>f¿g,ëÃïîÂ<<}¯¦wzÞfÌÞOGÊ÷«ç¯O}ϓۯїÁ§L±ÊÏN5ã.ýIËõ^oè<1Ç/Nß#w¯èó¿#;ûõ<éuž õz£ÄtúÑó/Gƒs”¬fPÆe fC œ\&PÆd\&x˜Ì‘„ÊÌñ\fr0™C œ— œ1•.+ 2’É”"¥”…—6PKŒ¢ a"Yf$Gwèùº™ú<vî×·x“)RRE‹&CabÊQ¨ÅKŠÈ“(c2‹ˆ<‹/-RÀ¤(” BÔP‰H ªECôŸ7ô9â­f-1eÊt¼ñè_>ÙpS¯>!×y-tìã§T籿g-2¸ÝMØë²íjGC@ÁÔݯ ›ÙÉe56kHËf„zzxR÷èæž~Æ¥ôü©›==f¬³šÎ¯WÏÆ^îv™uÍ“YöõøóÁ²n{Ü~~¾wßðrÕ©‡¿áì_nxÛyë××àuÙï|ælêêòûŽwƒRõøÞ§›f,¦æ8ìÄÇÆ8a3ÄÆg œ0™â¸Ìñ1™HÆeR1Ç8a3‹„ÊF3.}¬¯‹}¤>1öq>5ö#ã¯×ÃäoÖJùKõ#åßO›}>{/{ľÌ<›êbyÙwCŽõHæ»á­œgtÎxu91Nç¯Bù°ôÞT=weâÃܾ =÷ÏÃè_;¤Ÿ7¥¿3#éïËÚúw̧Ÿ2>•óCé_4>–|àú'Π|ø÷ç…Onx°ö§Œ_byõ±òÇ©š_Fyã¾púqI{gëÇš/LæGDçæ¬ctÔ]­V3`\Ø#6)kT‘q ¤°@˜ØI–5:y» üîþ :½O7ÔÔìÏ÷ŒfpÅaIDXEeL¡ÙaTc2Åd¥ÁG‰N;YE”–…%B¨ %ZJ(÷>§å~¯\ðV²ù¯¥ù£~ž™¯¨›¥Î¦Ñ­´jmÛ)©µZÛ)ªìÛ»5ÜÆ Æ Æ Æ Æ1ƒ1ƒ1ƒ1ƒ8bβ¦dLŒŒŒŒŒŒ&Á©´i›Ç4铱8ú óg¦<©ë%ñç³{cÂÇÞúóØýù¼~”|ÌúaòóêGÊO«%>´|„úô|tû(¿>ÒÛCâ'ÜI~º‡ÂÏ»‡Âc÷ø9÷²> ÷…ø9÷’>÷PøWÜÅøyöò>!ö°ø¹öpøÌ~Ïã§×ã_"úÏ0ñ¬˜ÖK*wðzU£‹¯’ÎßWÎõ.zs™o8Ì¡&P“!ŠŒYBL†2–(ÅD•e YBcœ1™c.+ "^]* QB’©( €@ ,¶E¤ Y^×Ö|—×\`­eóŸGóÇO7d×Ö6D×»=Ùp=JÕwáZÛá¥ÛÆ2ß$æ¹åZ¦Üko^Í|ºl™clú/ú;ŸœÙ§¢_WÒóý.˜Ø«œT¸¨’‰2Tb£QЉ2‹&C–0–Ì¡„ÏÖ¨ð,¼ú,¥ÊP,¶ bP()b¨QèýŸÅý¢b–Ëâ{ž-žWwRý¯OÂïå~Íñ´ûÈ#ëïÈUúçÉ­|•>±òƒêß(>®ü_«|¥ª|°ú—ÌSéŸ3O¥Ÿ;O¢|îGÐ< uáÓÛxãØxûQåÓÓy•}'ž=Gk‹$ërÕésänj¦Æ4DQEDQ‘DQ‹!‹!‹!ƒ5`ÌšîCc °klÛkf5¶ mƒShÒÝKpÓ7 ðÒÜ47—½Z&òho††òèn†¯œú—³ápËtÙŽxÕúÛÖ~o«“»:õ}þ˜ÍeÎ4X£”"‰2‘%á°“(E‹`Ç)ã”\qÊÄx)qÒ¥(-”X-•ZXEZ”ÉAV–TP îûoˆûtÀY—ìyxù㶦\:eõo•ORùtõ/—OQæSÓ¾^Ié¼Áê_.צóié¼ÊzW̛ͧ¢ó©èNÞá‡|á±Û8ÇcuNQÕ9ÇDÑMÍ0ß5CkPÙ0°FLFLdfÄf×M—T]í¡ÎŽ›ËË®÷BùÐôžd=W•^øãØxÃÚx¥öž%máuáyà}àCè_=#è¯Îé6>–üÀú‹ò¨ú»òPúì¾<}øÑöy|T—íòøa÷Y|ûëùü?C¿Ãôkù¼?I¿šÃôÛù„?Q~[Õ•«ôŸŠòtY©ŽsYã–5ÕêyýšÏƒèyþ”׫ÝÇÛ¾y’È,‰(“(E– D ‹(b¢,1™HÇñ\qÊF°|ý—Al%AAJU(-€)J"ª)UQ×öÿ÷¹¾W«æW‹†ÍGƒ«~8驸jn[†¦Ñ©¹Z[†¦á¦íÛ)®m&»˜Á˜Âä1¶˜Ú % ƒ$â2b2bL®ØÖ65ÓcZ6µ ·Hß4í{@ÞçaÒæ‡TæÄërC­ÉÇí¼#ºpŽçîp÷=ž^÷;Þxô'=ž;Üîp¹ÃcµÆ;/—±Éc©Ëc¥Ì:åÞÑ ítÕ ÓL7cª&ÙªVÜ1ÝZvëÚYq³ÓØÃYò=O3Õ—Õëæê錥‰%`”°D™B,‰2„XIK%F3(cŽX¬Ç,cR—çòŒnÐÀ)E––TX-”*ÉB–¥T™JФ¶Ð§GÜ|?ÜF"æùÞŸ^9t3­S\¼[‚Í}®wFã†÷j9ÝJå½C–õWPåu“•Ô9]C•Ö9]C•Ö9]Uyc’õWPåuWR9]C•Ô9g\9§Xåt“™ÕgQy]#™Ò9(åuWHåpåuC‘×IÖ³’u—×YÖ8ÝcÙ#Ö9'XäeåpäuÈäuKÒ^iÒŽgHæt—™Ó#žô#EÞ—CpÒÜ-ÐÒÝ ShՎ躦ؚféZféZvÌÍ[uí±†Ìk—¿ÍÖx}o/×—ÒéÑÓÓRb¥ÅbE,™H‹ 2„RâÊ$”¸ª1X1Êã”\pÙ®0Rüñs¶XÒÊE––QfI()U*PR‚ØPU–…)A¿î>î" /xg§Ÿž~ŽuõÝ{8ià{äñ±õ÷בž<š/õŸ9¼é½nkÑNgHæugHætÓ™Ò9¯HætŽyÓNgHætŽgPåu[Ò9#•Ô9]C•Ò9#™Ò9#™ÓNKÔ9C•Ô9gXäuŽIÖ9'd9'd9'd9'd8݃‰Ø8ç`ã°ã°ã°ã£‰ØŽ)Ø^7d8çb8ç`ãu¥ãuŽG\—•Ô9])y™ÐŽiÒ9§L9yçD9ñé‡6=R¹§D9±ß¤Ñ»Nø˜å}îx:}'Ù=-úwï2eL¡RÅ„XEe T@E’Å„”Lr‘Ž9b³ ñ5¨ùáÕVÊ*%°[-((Š´**¤µK2BŸqðÿq V9;9Sç¶kÛ¦žœºã‡oM5è뱫Šr;«Ÿºœ7¶×ìnÑÅ{´q^ÁÆì§°q»#°q»°q»°qÞ±ÈënÁÆëޱÈëޱÈëޱÈë'#¨¼³¬r:áÊꮢrθrαÉ;!É;%rNÁÆë‡#®G#¬rN±Æë“¬r:‡$ìÅyg\9r9]Päu%åpåtÙÓiÒŽiÒ9§L^iÒ9gT9gL9§L9±ê‡,é‡4é7w×>ýåÇð>¯æ~«äÙÙìy>ÍwtjÛ¬¥$–,X@”¸¨Å`%F+ fXË1Ï3ÄÔŸ²Í¬¥H²”´*,È,ªT(¥±BÙ¥Uý·Äý®YK,ºw—ÛÓ¢ÎÏCÇöe£ÊkmÛji»†–òén¦†ñ¡Ð47ÓгžïÐ9ÝÐ9ÝÐ9ÝÐ9ÝÐ9ÝÐ9ÝÐ9ÝÐ9#™Ò9#™Ò9(åuJç#™Ò9]C–uŽIÙIØ8Ýc’v9Ú8ƒ‰Û)ÜŽßÞ8'xóç¡ èC‚w>zàž„_>zó§£>zó磉çÏFWšôaçOFtôaæÏJeôm|ç“ô?iß§tÖ8f>¿ãþ¿ã™ìö<Ÿf»¶kÛ¬Éaa `XE‹BQ%X%‘%‹Œ°Ç¢òµ£È²´°RزŠ)BRÒ‚ˆª)e ÊdL¥¦RŠ©~ÓâþÏ7bÄYk/×§ƒ£é1>vý>zýù÷Ð+ÁžøðoºO ûeñ/´zDùÁôošÄúģ|¼>¦|¬>©òú½?3¯+v¶¦wqÛ«ª>‹ä>³äî}cÉö+³fk2X%Q%€`– K@B D™ceŠÌr† )©r j[QeQe¥QeJ e²”P¶Z¶R©-”¿cñ¿S›èa»JPd”¨-”‚¢¬¢P l@RR@ €@@@K,KAK°’Á,$¨’ÂK ,V6Y±XBK#†3,LV.8ç &R\&x˜Lñ1™Ip™âaŽÌMxìÄØ˜Ì±©ŽX˜÷ðzùw|ÇÐüýž§¯åzÚYL¬Å`Ç(@E„XŠÅ€@@‹",YŽPÇ¡ŠŸicrÆ¥T,©Al´¡hŠRª*‹-2R„µEZž¯™eûÎn_Ok-‚‹(²‹Á@P„¢!@E€%DQID”ID$”IaX1Èc(ÅF+#Ê1™C’ã2„Ç(c,Y."YÊ1Ç<9bIbÌrÆ&9EÆXc2Ę匳±©ŽX˜ã–$Ç,IŽXê¼_¥“ç|ÆÅõ=_;ÓÖwå%„XE„X% €D %†3,H¸,ÁÏ{ÕÒT¢¨¥… ©KC [)Aª RÓ)*S"ý?ÊÌß¿ÑòþÎ]–fFB2¦,Æ ƒÁ­´jm&¦á¥¹Z[†–ñ¡¼hoƆñ¡Ð9#™Ò9#™ÓgHætŽgI9]C•Ô9r¹]C•Ô9'`ãuŽ7`ドÚ8°âvÊwCŠwC…Ú8gpàwÃÚ8ñ8'¡ƒGß Þ<ç¡#Îz0ó±ôâù“Ò‡›¤<ÉéCÍžó'©ŒysÔ‡”ôÇ•=H¾\õ!åcëI|™ê&z°òg­ÈÇׇ=xxøûžF>ÎgƒßôŒþS*2ô4zUÛèsvk‚K²¢Â,„¢JX°‹°K!,ÂKc–#VÞc嘹ôí/L”Rس"U±TR©P¢”e(«S%EUU&JŠ£Æ=¸GŸ«ÑÕ›ÂëÆ^gAt:G=è§;¦œ×¦œ×¨œ·ªœ™tÓ–õ+™Ô9C–õ[ÔN[Ô9]C–õgT®wHæuWPåuTäuŽG]9TäuŽG`äuŽGXäuŽWPåuWU9C–ôŽk¼šƆøjm›dk¹—cPȹ1FWXØÔ]­(ÜÓ óL7¹áÒæ‡Tæ‹Ôän4v8‡cŠÎÜà‡ ó‡£<èz/6‹Í‡¤ó%zšü᳇ª×þ¾“W¤ìÖnù’%‚X!QdKŠ Q ¤–e‰%ƒ‡»Í—æ†:wå/L¨RتLŠ¥E”U±TUEZYEQY)ª,ÌU#*j›ÑÌéîªs:©Ë—M9oNG-ê§-ê§-ë§%ê§+ªœ®ºr^ºœŽÁÇ{#®œw®œw¯#Šö7`ävZãvSŠöŽ+Ø8݃ŽöS‰ÛNÚ8£‰Ü8om8]Ã…Ü8]ÔáwßSßu<÷ 8' <÷ =è=Þ^ <÷ 9è>z#Ξ”<ç¢5è5è6zCÍž”_:zPóg§6zpó^”<Ùé2zcÌž˜ó'§2zcËž¬<©êÃÊž¬<©êÃÊž´¯+/LpoéÌÕ·+¬$Êe@@%”°¤XIQ%„™bI`ò=ŸkÅúz#¯;fT²Š´ª‚Ñj RŠ¢”´2”ZQT«ERеKe†KJ¢ÔZ ¨ZÒ[R[V[Lr¢[SŒ„d#!ÈKDd"Ò-1eLYSTÆÑ‘Dd"ˆ´ÆQETQ@¨Š"ˆ¢(Š"ˆ£HÅ”$Èc3’ã32LÆ1ƒ!ƒ!ƒ%`É2†,¡‹(c3† †8c3†9X̆3(E‚\AB ˆ°@ˆ bÃ/™úo”ÍàV:z+{s•I–­J¢­’Ú *ƒ+%U¥QTUFS!fB¨ÊdJÈUJ[2eÊZ£)R”UZR•PQe `Y@À¢(‚ –,„°J$¢K°K°€€’Á,$°B¤°@„ÂJ¨ (€€‚•(‹¢K!(ÆX²e ñÿ]ñ¹Ö¥c~‘{s•E[)E[)J¢e(ÊZU KT--QTU³!TU-™ £),\ µJR”U(PQB@Qe e¢P¢(€€J–,‰,Á,! $°@’ÂK¸ÒX%‚%%Q%QA @%TLhÅa&Qu|wÖüŽ5úK{s”KfT«dÊQTU%«E¡”È(e(ª2™ 2d*Š£)FR–̆R–¨Êd“)”L¥[e-¡T•AITP´ ª % P @e%€€K°@‹ˆ– ` –…ƈA’Â@@caq¢K*,ˆ!*€‹ ¢K"‹¥8>Oé¾gÉY×¥W·)JµJ¶Á‘*…¶J¢¨P«J¢¨ªR¨ª(Z£)JRÙKC+2-”¶TµEYjd([)Jª‚‚€R‚‚’¨Ê, ±)@)@T @ „%€€€X%„ V+ ,ÂK± ¨,‹ °,"–JŒV,³#ÃùßwÃç¼Ñ7ëüUE«J*‹2©iEQTUeR²&R¢¨ÊdL¥VRбlÈPµKe-T¹AlÊUBÙJQAADUY@¡DP ”€I@ ¨( @ @BK°K ,ÊKˆ €–RIae%ŠªIi%DX QŠÅ™ã™ò¾O>™¢_]o~Bس ´Y‘*Ò¨ª’¨ª*Т¨¡V‹)l©L…™ ¢¨TZ¥³!”¥¡lÊU™ )l¥((*,ª²Š (²Š ¤ ²€²€  K( %€BXB ` V%‚XI`€, $¤“(b¢ ’À±P"€%@¨J•*$°cÇ(_‹æÛ«—\{JïÊ-¥ÐU¥QeE BÕ%ZU)FR¢…ª,ÈPµE VVR¥³!”¥²–Ë-¡lX-” U*Q@Te `  B€²ÈX* ‚€ "„ …@%‚ – `!`eŒ%T!q¢. *Q,‘`”EBQ K%„X­{9£ã!Ï­´®üбTY-- ¨2 AE™Ò…²Š¨ª*‹2Ue2E”¶e¥³!”¢©l²Û(ª,ÈÙE  RQ(PYD P€D É@€@cQ!PXB  ”BRX‰`–E„XI”Á@¢PJˆ¢©b‰(Ægçú>D|¦xåϬQîLžŽ2©2”¥«eKe)JQJe(²Š¢ª,¢¨ª([*Û*Z¨«ÊZ…³)jRÙJ”©ª,¡BÊT¥‚Ë`B€,(€, `,*X%€‚ – lÁAQ "âX `X -”EQ @ -€”b¡á{ÿ3x9áž:E/¼=RÙj•d,È-©‹@¢¨ª(*Š¢¨¸ä2”e)l¥²ÅÊRÙEUe,([)JT¥²–ÁAAAB)T€  JÁ@@¤²ˆ@¨€„€%‚YH" R!P %€€€ `XEŠ@EDX B’Teò__ñ3^vx厀¾üÊz8J…2¡QTPªde(-Qe-” ´)JQfB¨ÊR–-”¶U¶RÙE™BÊ[)lÊP[(²‚‚Ù2‚HJ, Y@`RÅKA¤@A,! ,ÀBÀKˆ°@%JJ%¢(‚J"–#/…ûŸÎõeÎÀúžŽ% ´Z‹(ÊQfBËK2AFR¢Ê[(Ë…”U-”´-”´Œ’–…²«)Kb2²”ÊR‹²‹(¥€Q ((°P’ˆ "…ÆË$°K‰b@X…€@ ` XYd(*X”K,kø/¹ø\îYs°_¢™OG AJµPdl¥-R‚¥)E”¶R¥-”P¶R‚Õ-”¥Š™2*RÙKe-ÆË’RÙJ”¶Q–9 °R‚¢ËKKPPP€€@‹`1AÂA,!À€BÀ ‚  ,¡(…!H…¢ʲÈ,²f¾wÅ}Çãy  _£•èá()eÊQe*R‚Ò™JR ¢…°ZÊ,¥²–Ál¥²–Ê2–-ƒ+(ª,¥¸Õ¶X¶RÙJ”¶R¥(-‚ÙE” ( !e€¥ @"  °‹RâX‚ D…ˆ\P± ±°± ĨK¨, …¨…€ ”KD, xl_律çqÓ!4ÒL±ôp -”e²•)jÕA•Ç!eAJQe)FXÕ¶ a2J\±¦W‹qÊ-Ç"Üil¥¸ÒÙVÜl\±¦W[)lÊ[(*R¡2E[("Ø*P e¨* ¨* @$,E²BÉ,©BÄ ² ± °,B¤+PT…b*RÈ* ‚Ä-Å PT1±PTAb PXÄÉŒ¶jÜ¿#âz~g>™%h¤Yèá,’T·eq¥Ë[¬®4·[2J[ ’ ®4É)l%[q¦H2Ë™¥2¸Ó+ˆÍ2A“fÇ)rc‘n4ÊãLeqVW™1¥¸Œ®4·’ [HŠ‚¥* ‚ ¨* q1¥b‹qD* ‚ ¨+XŒ˜¥¬FRBÉ ¤IVH™La”¬E’ˆ²C&0Êb+“RH,be$2X…Ad¡`APT…BÔEHdưFƵfÂFÖˆ½aÒåÆ;Xξ„à‡ ó侞ß"Ÿ3Ç·V:e,(_¦ÃfŽ¡r¸Û.XÓ$¥J[IL’•¸äd–­Æ¥¸Ó+.XdP[*²Æ—,jeq¦WÁn#+2¸Ó+ˆÎâ3¸ÕɈ͈ͅ3ºêgu—PÜÒ7´SuÐ7ÞaÒç7–nAÖã‡kŠî ƒÏ‡¢óa鼡꼑ë<ˆ{ÛÃÄ÷ž+ôçáô/žÂ>‘ó0úwËÃêgËCê_)Œ}cä±_¯||>ÁñØŸe>;û9ñpûGÄÃí§Åb}¶?µŸŠý´ø¼OµŸµŸ‰ö“ã$}œøÑö3ã©öä!õóäKõ³ä‘õ³äÇÕO–/ÓOšKΣèqùñïãáÃÜž ö§Œ—מHõ'™Os£Vý͗Й¼áÈêGˆN°$U„°"ˆ”J"ÀTV  (Š"ˆ¢)"ˆ¢(Œ†,†,‹‹1ƒ2`Ì`Ø\Œ&Ñ­´jm›F¦êho§;¢œÎ‘Ìê®´r:éÆì§ºœ¸p»‡ ¿#Îz0àz<ëèϾ€óÞ€óï 8…<ëßO>÷ÓϾ‚<÷¡†÷ÓϾ€óòì±ÆìÈ⽘œ×¦œ—¨rÞ„hËm­74arZ«q0ZK¹¢3žXveÅ×;\Cºù£Òžf)꼘{^Åñ¡ìóðúÏÓÒð6êë’5-ƒ&#wÑx>ÇõÞ'=v¸GuàƒÎ‹ÎG¥Ÿ•xYz¹fÀg11†lH*B ¨* ‚ ¨* €¨P@°P,€…° T¨• B¥@PXJ¿]ò<8ý/£òÞ^ð>‡·~Z–ã…€ @%DQ nCTÂl6Ã[`ÖÙ‘¥º[©¥¸in[°1߯(Ùž‚å–‘·.z];1Lz4m60K›œÀfÀgŽV³6#9£âtóߣ|Öo¢ó‡£|Èzwˇ«<¸zÚ|ülå²õÀÌõ¸}=\õç]ùjsgèqFýX¼6ôœÜþ•9W|hÑ×Çg*]ÀÀ* DTªB ©T€° € € X¢*¢’(Š (Š €A¨,BÜFLFLK•À¼ú’fÁnw]\æ#;®–ãKq¥²•,¥g‘©¶œ÷¤s:r8݃ŽwÓÏžŽG˜ôr<·«2zèòÄ<‡¯O"úÃÉzÔòo­)êÃÌzpórôaçåÝWgLt4ݔӖc `(HfÂxû´ç¹™` `M«&Ptc¦/N­c~¬FÉ€Û®C6-Àe ¨* ɈɈɈɉ2b2b2b\˜Œ˜«&$ɈɌ¤-E”EÂæ0gMsjµ6ØÒÜ4·Õçtw@çtÓ•ÕNG]8Ý£‰Ü8]ãÞ8]Ö8]ÅáwŽpâw+Ú8¯e8¨â˪œ—¬rºÇ+¤ioF†á„Ø\‰`ÊcL¤†lyi¦Ö¨»šI¶é†ö½Î7´Ã}æqÐç‡Kš›ÜÔès+¢ó#¢ó‰¢ï=­÷ž3D:èès«¢hÆ:†Ö¹[f±³-ÛtŒØÃ9ˆ¨J¤¬ØZU¡A( XBŽ `P@Q ( 2  @ € @ A@$”(”‹  PB--°E(%° ( ˆ,P°€ ˆP( `¡@‚€€jï1 ÌÓ¤ÝÚ.€™…ÿÄ3!12 "3P`p#0@A$B4 C°ÿÚúDA}]}ÄîïÊÉÝß»ýþôøÕ=Ù'÷—è‘ø’I$’I'öÔO¥Ávr;Çáh ‚#ßQ>±ù Wøê$©­ÅUå\CˆñïôÚ=È÷`Ço`º›ئÕ6)±M‚äüh#´~7¡¬ÖkSZ”qGRŠ"Gó#ñÌ“ô!B¨U ¡D(…¡B… (¥ª•R ‚?,„!T*…P¢(P¡B… *T©R¥JAô)üm„!J•*T©R¥J•*AAÎ’I$’I$’AAT©R¥J• ‚ ‚#øÒI$’I$’I$öžóøÖ ‚ ‚¥H*T©AGð'´’I$’I$’I$ö’{Oãx ‚ ©R¥J•*T©R ‚ Ý’I$’I$’I$’I'ñäAT©R¥J•*T©AûRI$’I$’I$’I?`‚*T©R…J•*T©Aµ$’I$’X’I$’Ií$“ÚÁB…Pª(P¡B… *¥T©R Ë„!T*…¢B… (P¢ÿûá'þ‚Kÿ´Ü~VD‘˜FôÈqÚqØqØq˜q˜qšqZqZqZqâ!Ä8‡âE8ŠqTâ©ÅqÆqÆqÇqÇq¡Æ‡^iy©Æ§ÜkqGqU*¤) Gâö¶LXDDOæx„!BiT*…Q¦¶šÚji©¦–šZii¥¦„4!¡ h4MƒI¤Òi5)©MjkSZ”RŠQJ©U*¤) BA‚ÑLvÇú˜ó¬&?“‚×oÏ…ÞïŸës§ù™3ÕÞnÉæäEα=L}cúDû¥òúîÇŠª¢9Z+Üá¨#•¢åzöÞáÊ®VdVu– mÖŽ[.<”: È­~ühmM™Î1*5ÙÎþÝ‘ªçeMË¥ÆgµËõtòoØ/ÙIà®{ö:y(ƒDû¥ü$ž•hŸaô¿„¿åD'Ø}/¸ˆ7 …ãª~Ò$Žb´F+½äÄåÇ4FªŠÕAQP…ŽÑ÷‚ù(ƒû¤÷ç“å±îQÌkˆkQØÚCP\Mš4\Mm22½±DäFד±1Fâñ\D(ž–Ú<ƪ=¯m˜‰ð’ªÏX¨º**)–UDDĈ¯Ïá‘®DŽp­sL˜¾Eoli/v†­ÙÓü=‘W&3ö‰ÌOyÌsÝTT‹#ÇbÈϰ]ä Á>Ãé=ÄðæªYä­’¿ Ñ^Ô!9S³•LÝ™êÈÛ·€ß˜‹ñx™=bz=xâø±¶¨‚÷ñ׋Öå}úŽØ•QÙÞæ˜}}GÍÃèÑr&5C'ËvMxòüXL^¾§æ\ªÌXî¼TÕÄüë8=Äf,mê1±1˜ë|Úë‹Ç/O…EEE0ô÷L3™q7*eÀ¸Õ:?3¶ðŽ›äêñI©~6õK=>>™ïj¢µ~ºžoìƒû¤ý¨ïãÚWܳ‰q*K¦ïïwC\­îE\SkÎîܪÑÏU]ãܯQ#î1j¹]wcÉS{fhìÍ®G£±®Fê°ç.­°½­j½²9 mq™­nNÎks³©ðÃÛ.=­é¾ãíï[d+³¦ª³¦ê1?!•Q.<®ÍÿÍ™ïÛÓ|î­¹.˜Þäê¾FVµ›}už¬š0O°úO}s1§)‡)‡+ÉÆrqœœG#ÈÄr1ññ±q›1›1›]…šY¤¡(x½Aä{°5îaâ¾ü¹;+ž¤vkÞÁÙr99‡+œ©ÔfDnGµË*©(®êr9¸ú—cf\îÊŒêœÆ,ªýw¯'fì>“ÝUƒ.eRIrŸ¨~¡ñ’ò^YÅœYK)råË—CbBè] ¡± ¡°Ø†Ãa´Ú¦Õ78ÜãsÍÏ7¼ßßßäd9FC‘ä<ä<ä¼ä¸ä¸ä©ÉS’rNIÉC’Ó’Ó’ÃÃьߌߌ݌݌݌یٌٌ¾2ì,ÂÍ%¤¡àx_ÅëÉÙ£Dû¤ó÷:‡ø K9˜Ñ©B…PªiFšØkÆjÆiÆiÄhÄqñl' ÅÂqpœL' ÃÂp°œ,GÁÆpXpXppà!À8NŽ Î ÎC…”áf8yŽs‰Ô^ ãu'©8ýI£©4õ&®¤×Ôê ç#1úÇê“—–qe.l6؆Ɨiv—iv—if–id,…¹råË—Rê]ÅÜ]ÆÇly±æÇ›ly±æÇlq±ÆÇئÅ6 †ÂåË—.] !d,…²„’I$þîV^Í'Ø}'Ÿ¹ŸÖaùŸ¿L‚„*…ka­†¬fœFŒ' ÆÀqpLÃÀp°,ÀÀp0 ' ìüG³ñÏÆ{?ìæÎiìä=œ‡³gÏSÙî=žó€óàe89Nc…œáç8™Î&s‹Ôn ãg8ùÍÍ99YMyJd)«Ê¸‡§‰ãüQû¥ó÷:Y‡æw» ‚!B¨U ´£J4ÖÃ[ L5c4°ÓŒÑŒÑŒã°Ë‚÷pye`Ñ>Ãé|ýΣֆ™Ú ~ÒµSïl©ð¿ÏÜÁèËæƒ‰öKçîu¤0üÎÍATVÊ+U (­T(¥VU¾w‰ UJ©4¨ÖŠž0½¡{5AÍJ ©!D!PTDj0¯ÙÑÞ#¼Aý/ó÷1|¬žh0oØ/«Üê=Haù„Iý¬ŸÒÏe‘|äˆ/‚0ÿ¡;"öE‘7ؾ~Hß7y»Éâ£Trxµ_é¨B °„!T*…|a¥PTBˆ9 jH¨‚4VBˆ*GÕ ‚ ‚ ‚ ‚ ‚:KüýÆ|¬žm7ìN—Õîu>haù‚^ÖUK¥…Y, á ¢¸G,YRK!bPO7(„¡'à|'àyªùòÑ|š×ýb/ŒvNËâ"@¾~h/šù»ìާÒï?q>SýM7ìN›ÕýwêD0üÎÑÚ ‚#¼Þ ‚ r?a?Ç·ÙpAUäï?q}óhÁ>Äé½_×~¤CÌý¨ ‚ ‚ ‚;AAAAAAAAAAAAAAÚ;ÇxúoWäá=ÌžKæÑ‚}‰Óz¿®ýHƒV«èäû^;Av‚=Ø ‚ ‚ ‚#´ûAAYä¢wo«1ý°`Ÿbtþ¯ùïÔökˆ{M½y·¯7uæþ¼äuç'®9]iËë_Vs:³›Ôœî çç9ùŽ~cÚOhä=¢ãÚG´i¡í6žÓÆ{Oí˜ö—J{K¥=£ÒžÑéOht§?¥9Ý)ÎéNoJs:c—Ó¾œåtç'ÈÀr0›ñ±q›1›]†N£S©Ï°Q;àù¹û0`Ÿb`õ§§¿SåÙÙMÆãq¸Ün7ÆÓj›MªmSj›TÚmSi´Úm6 †Ãb؆Ä6!± 64»K´»K´» 0³ 0³ 0³ a,%‡À|Â|'Â|'ÂB„!BT©R¥J•RªUJ©U*âCˆq>3ã>2r¶BÙ å/”ٔٔٔ۔ݔݔݔßßäd9FCóóóãã¦õ7›Íæä6¡µ¦Æ›la|eñ–Æ[lDâ'8Ò?Hý#ôÒ?Lý2X+û'~›æçòAƒû­=úJÁËûþ'‰âxž'‰*J’¤©*J’âÊYÅœYÅœ]ÅÜ]ÅÜ]ÅÜ]ÆÇlq±ÆÅ6)±MŠlSb›ئÅ6 †Ãa°Øl6 †Ãa°Øl6 †Ãa°Úm6›TÚ¦Õ6©µMªmSj›TÚ¦Õ6©µMÊmSj›TÚ¦Ói´Úl6 ˆlBè] ¡t,…–’„´–’„´ð<„øOÀð<Àð<ìúO™Ôy Á¢}‰‡ÔžŽýG¥ žJž0AAAAAAAAAAAAAAAAAAAAõDïÑùõF û©¾Žùý(dôªAAAAAAAAAAAAAAAAAAJT‚¤T©R • ‚¤AAAAAA£ø)ä'~“˨0oظýLôwÏéC'¤ƒM“ ì/cÝÒåk=Æcs̘Ÿ`‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ©H ‚ ‚ ‚ ‚ ‚ ‚#ø×¹Ò|¾£Í£ý‹ÔÏG|Þ”éD²½;*Χª±¯Ç— lcèqÿǰààDéñbkºæcVÁAT‚ T‚ ‚ ‚*T‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚;AvŽðGhî¨^çMò:6ŒìVz±ú;æô ÿCPÀÙ{ò·QÉ‘–ÇáŽnÜið·­ù]'ŸY…ï ‚ ‚ ‚ ‚ ‚íGx ŽÐAw‚ ŽñÚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ îáÿÇÏê`Á>Åg«£¾_Jô1<PYr£ž‚fʃ•ÎVåÈÑ\õW+œ$´vLŽH ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚#¼@ñ}Öx`Íë`Á>„¿Co«£¾OJ éÄž*T©R¥J•*T©R¥J•*T©R¥J•*T©R¥J•*T©R¥J•*T©R¥J•*T©R¥J•*T©R¥JT©R¥J•*T©R¥J•*AJ•*T‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ƒ ïwËO[ ô'} ¾x}ßåä¿ó>•*T©R •*T‚*AAAAAAAAAAAT‚ ©R AT©R¥J•*AR¥J•*T©R¥J•*T©R¥J•*T©R¥J•*T©R¥J•*T©R¥J•*T̃„÷2z_ë`Á>…“ËèIçƒÓîfgŠ ~·&\jlÆ_vafafaf’ÒZKIi(Jàx}V ‚ ‚ ‚ ‚ ‚ ‚ ‚*T©R¥J•*T©R¥J•*T©R¥J•*T©ŸÍÂwO<ÞKê`Á>…›èiçƒÓî*Hì*µã5c5b5c5c5c50ÔÃSMM54ÔÓRÔj5JjSRšœkq­æ·šòòÊS)L¥3ÌW1\ås•ÎW9AAIIIIþÑþÑþÑþÑþÙþÙþÙþÙ=Y=Y=an°·X[¬-Öë õ…úÃgXlêÍa·«6õfÞ¬ÝÕú£Rr:“‘Ôœž¤äõ'9ÊÎró¼Ç/)ÌÈss^s\sTçãœsÎzöœöœööüg?ÏÄsñüG?ÏÄsñìG; ÎÂs°œÜ'7 ÍÂspœÜ'3 ÌÂs0œÌ'/ ËÂr°œ¬'+ ÉÂrpœœ'' ÉÂr0œŒ'# ÈÄ;©ÆƒßaDïæg?¼cDúQô4óéý+ïBiF”iF”iF”i­¦¶Øka­†¶Øka­†¶Øka©†¶Øja©†¦˜ja©¦¦ššji©¦¦ššji©¦¦šÔ†¤5!© HjCRÔ†£Q¨Ôj5F£Q¨Ö¦µ5©­Mjkq­ÅQåW!\…rÈFR2‘”Œ§êŸª~©ú§ë¬Nbs˜œÅ³Ì[)l¥ò—Ê_!|…òy±æÇqwRê\¹rÅ‹!d,…³K4³K0–Âq’Âq“Œœdã'úg韦~™,A\ªG¹Ó§ëu_Û‰ô.£Õô>›Ò¿ƒz_ŸÔy'›ý?¯è}*ø;ðoDŸ©Ô¯ƒF úo_ÐúwÂú“ðgKŽ˜º— 7è_ÖOWÐÑaz|²ŽOÁ}>Êü®F¦GYÌ7èKäÿ?¢5êÕÃÔ"Ž«ø#Nü«ðagQšÂ Aˆ'вzçô_Õ9£zÖ(™ñ)³ ³ |%ñÄ[lE±Ä[8ÉÆN2q“Œý3ôÏÓ>à>à>à>à>à!„0†ÂCHi !¥ZU¥ZU¥ZU¥ZU¥ZU¥ZQ¥Q¥Q¥Q¥¢ÐÖ†´5¡­ hkCZÐÔ†¤5!© F£I¤Òi4šMƒA¡M qÔã©ÇSާN;Ž;Ž;Î;Î3Î3Î3Î3Î+Î.C‹âä8¹.S‰”âe8™N&S‡”áå8yŽc‡˜áæ8yŽc…œág8YÎs…œág8YÎPpzƒƒÔ àõ¨8=AÁêNRpz“Ô‰þ?0Î C™º•f´b A>…›Ð¾FPñ‰Ôù}& ‚ T‚ ‚¥J•*T©R¥JT©R¥J•*T©R¥J•*T©R¥J•*T©R¥J•*T©R¥J•*T©R¥J•(P¡R… ”*P©B… (P¡B… (P¡B… (P¡B… (P¡B… (P¡B… (P¡B… (#DB>‰Õ}2=È ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ©J•*T©}ªõ~Xêãü±ŸÕù_úËêü®¾—ùþWÉèwŸå|ÿ-,u>ËRø~Xê×òÏWçùcª_òÇQëü¯ýeõ~Ww¥þ•òü·yþWÏò×òÇT¿§ùc¬ôþXë,õž¯ÊèukúŸ–:•ýOÊÿÖo_åuôäõ~WÉòÝçù_?Ê_?Êý_Êü®‡Z¿åt:åü°‡\¿åt:Õøÿ+¡Õ/êþXÏó? J„¡f–BÍ.Òí60ØÃk Ì7c7ã7ã9ÎF3‘Œäc98ÎN3“Œåc9xã"ʧàY$’I% BÈY !d.…»K´»M64ØÓcM­6´ÚÓkMÌ70ÞÃ{ í7°ä4ä4ä4ä´ä´ä¡ÉC”‡)R£”rÎQË9G)NRœ¥9N9N9.9.9/997¼Üósͯ6¼Øóc˼»‹8³‰q.>#â>#Äñ'oÇ·Ç´v‚ ‚#ò$“ÚI$’I'ÿC9ÿíFýÉÕSZš”Ôk5§á¨ ‚…*¥Tª”RŠQJ)E5©­MjkSY­MJj5F£Q¨Ô†¤5¡­ hkCZBˆkBˆQ ¡T*…PªB¨h“ŒqŽ1Æ8èqã¡ÇCŽƒºt‡6«Ð¿xAA) B•RªUJ©U(¥T¢”(¥ (k5šÍf³Y¬Ök5¡¬Ö†´5¡BˆQ BˆTªB¥J•*T©AH ‚#´~ÇàJ„¡d,…š]¥Ú]¥Úli±¦Ä6´Ú†ä:5cÍxÍxÍxÊc)Œ®"1ˆý#ô޹¬í±K©e,¤ýr ‚ ‚ ‚ ‚ •*T©R¥J•*T©R¥J•*T©R • ©JAAAGhþ(J„¡(J…²Bè] ¡t6!± ˆlCa°Úm6›ئÓi¹MÆÕ6©µMŠlSb—RêYK)e,¥”•$’I'÷:WCÕÊYK)e%IRT•ïÔú‹AAR*AAAAAA{±ô™$’P’I,X±bÅ‹,\¹råË—.\¹råË—.\¹råË—,X±bʼn,X±$’I$’I$’I$’I=¤±$ÿ"<"EOð,>ÈY !d.…ÐØ†Æ›mC6DVûÊ#$Ök5šÍf³Y¬Ök5š‡$/ó$’I$’I$’I$’I$’I$’I$±%‹$±bÄ–,X’I$’I$’I$’I$’I$’{OÐÓ¦s™U•¨ÎÐGÓ1$UŽïów—f–RÊJ–RT’I'³—߯–rbi­ m5´ÖÓ[Mm(ÂŒ*ÒCÇ éY$’I$’I$’I$žÓ÷c•É.š×Ϻý-T´¤ °KEq-³Ý?iWö0ùرe,¥”²’¤©*I$_ƒïÖ¸W"µËãÞ‡) B¥Tª•RªT©R¥JB„!CO„øOÀøO„–’„¡(J„’"’I$’I$’/ìaýÜ‹ð{˜Ú•Éâíj.5\j ƪkh¸DÃàŒ•\H†¤DkZã#+÷^ÅØð<Þ(J„¡bÅ‹,X±bÅ‹,I$’§‰âxž'‰ B¥\UÅ\QÆ·q­Æ·Ükq©Æ§”L&£Q¨Ök5¡­ hQ !F”iF•i !§à1ÐlSb›ئÅ6)u.¥Ôº–Rê+–;§Ÿ’0w©_àßO«#–"*·{Ñ‹ðü(_â|9 L·ÛRO¹$“üÄ…!HRªUJ©E(¥Ö¦µ5©¬Ôj5Ö†´5¡­ hkCZBˆU ¡T!ð<zI'´ö’}É$Ÿ ¢À¹Q¨lQ\ª]ÄŠåRT²ö•'´¯Ù°B¤) B•RªUJ©U*¥¡B… (P¡¬¡¬¢ÐÖ…¢B¨Q ¡T!B„#ù2Oyí$’I$’I(IbÅ‹,X±bÅ‹,X±bÄ’J’¤’½çµ»'Ø0AAA*T©R¥J•*T©R¥J•*T©R¤AAAäI$’I$’I$’I$±bI,X±bI$’I$’IþL–%«ÇhïG¹]Ÿr{I=¤’I$’I$žè½¤Ÿ®ù«±#¨­h¬âÿÿÄ(!P1` 0@ApBQ€°ÿÚ?ÿZR”¥)KæÔ¥)JR”¿ø>Ó4fŒÑš2FHÉ#$dŠŠŠŠŠ¼u¸7ñéJÊÊÊR”¥)Ká\ü!B„ẻùoW~5|×ÁõwòÞ®þø>®ú/_Ñýk÷«î†…ÜþBU²n>çn‘MÄ›Gñzµ4kxv!‹##&Ô›^ ð}]þôV=.¿L}–Õ¥ÞèLÉ Óêb?®ƒêôR—äB„!B„!B„!B|$>«‘¬¬¬¬¬¬¥2222222223222Z±ƒ~ì'¢i ¤ãÖŒCàú½wÞZ-v6Ñ}"ô|Õ{0„!B„!Bq¯GÁ¿f¢¢¢¢¢¢¢¢¢¢¢£ccccccov„!B„! ðžƒ~˜N”¥)YYYYYYYYKêšB„!Bjüz¡B„!B„'Â}Çà´¥)JR”¥)JR”¥ø¿câ^¿gÛä/¿ö?)ZýÊV¿còB„!=~X‡åoGú~”¥ôÒ”¥)JR”¥)JR”¥)J].´¥6666666666666666Òþ€]üµwòÞŸ-éòÞŸ-éíå«·úÍwòÞžþ[Óå½>[Óå½=¼µvò˜ÈÈÈùØFFFFFFbÌY‹1f,Á˜³`Ì ‚0FÁ#bŒQŠ1F(ˆˆžö&&&&(Å£bŒQŠ1F(Å£bŒQŠ1F(Å£bŒQŠ1F(Å£bŒQŠ1DDDDDDDDDDD"ÿˆðŒŒŒ„!CÄÄŒÄŘ³¤'!5„!B„!B„!=úR”¥)JR”¥)Dý]^Ôã)JR”¥)JR—ÕÕÖº{³5ýüêS##!¿Bð_ÍøóGáé˿יJŠR—ØœÄKÛÜHB„!B„çáB„!BŸ£—·ÿÄ-!P1Q` a0@AbB€"2 ÿÚ?òoåÓýìvéÛö2dÉFB|Ò„!B„!OôJ|”¥úûõØ“fƒ Ã0Ì3 Ë2̳,ŒŒŒ„øçOOÚ„!B„! ðŽžv²²²³LÓ4Í3LÓ4'|/O„/ ÇÑ|)x>>Ÿ ^§Fá¥úUyÆ/ÇÓ§.ÎåegC—©üšgñôñmS'Gè+–jqFÙ¶qu¿9NU% $>N®«š£ô8óíÜ\“HüÎæ‘¤kþPÓÔ*ð ^§×Û苤DFWH…Åt¡†„%ǽ'bÐIë¢áߦ;CrãOËfÇ„f^©äqO¿€bð|}>¨OÕï×¹ÜïÒ”¥)JSE4hÑ£F¥)M¥EEEEEýƒƒ^ž6tˆˆˆˆˆˆˆˆˆ„2dÉ“& 0`Ã2ú¯Qú‹Á¯¥±2¢‰Ò÷)JTR”ÑEÉ1ò…4_ ½Gê/¾¨B;Œƒ!ëØbÙüAÿ{••Ž—±Ü¬MøEרýEà×WÖ”¥)JR”¥)JR¢”¥)JRŠŠR”¥)Kûµ×¯Eáã#ö#ö#####;ü)JR”¥)JR”¥/Kô®¼}z/5±±±•ìe{^ÆW±•ìe{^Æ„aF„aF„a}Ì/ssssØÏö3ýŒÿc?rb}ÏüýÎþçs¿º;þÇs¿ÑÇùÀûôîwý*R”¥+)JR”¥ýeèÅâ{~ö„!B„!BŸTýeèÅâ¡ÜM•óN>žN„! õˆ¼TD^yõÿxGð^]Å Â?)ßôéJR—«¢„cùgò>®”¥)JR”¥)JR”¥)JR”¥F‘Q¤iF‘¤iF‘¤iF‘¤iF‘¤TTTTTTTTTTTR”¥)JRõ^"„!B„!B†L™2dÉ“$!“&L™2dÉ“&L™2dÉ“&Hd„!B2ŒŒŒŒŒ„'IðûÒø÷éò×éòÞ_-åòÞ_-~¿-í›ôùkùkùkùkùkùkù]/œìv÷;{•{•{•{•{•{šF‘¥îiFÑ´mFѳfþÆþÆþÆþÆÍ³lÛ6ͳlÛ6Í3LÓ+÷+++ým›6ͳlÛ6ͳlÛ6ͳlÛ6ͳlÛ6Íò6ͳ|ò7Èß#lÛ6ͳ|³Lß#L×#OÜÓ÷4ýÍ?sOܯܬ¯Ü¬¬¬¥ÿÒ*****4#HÒ44hÑ£HßhßhüÅÒ”¯ÈR”¥)JR”ÑJR•••••••••ú÷úaFFFe™2dÉ“&LÉr]ŸÕøõ'ÑQQQ¤i^„!B„!B„!BtåËVk¿ï_¡ùf –~YÇŒ_GâZw;úA/?øÿ…ùœWØü/Ÿgü?Ë‹lÃ0Ì3 ô¬¬¬¬¬¬Vùd’ý:R”¥)JR”¥)JRþÎÃR”¥)JR”¥)JRôïûÉòî_ÇEúÿÄ7123!‘ Ap4P`q"0@BQa¡€r‚ R°ÐÿÚ?ÿ§» ÿƆa†a¿è«‘šb¼KWÅa°×§ï†jƒ¨ÇdÉpü`Š‚Ô¸h§dì‰HæB)ìûBa¯O547².eBŒ(¢f¢ã” ¹aW¡ý”™gŽYžÌ¤=sSC\4àÍpÏUpÑ Æ;Fxf¢® R¥S*Æy3SC?”l5L3éú冦xf†¦h1 Æ½Ž¦†Xh{Â¯Û ëOÁÙÃ%Űÿ‰ Æˆj‚vSSTÁÉQì1ïc’ç¯ÁE^|iÚGâÔ÷iÌ÷©Ë¡Î>.!–c‰‚kކE~¢à¸FŠ*~ xW'È÷œ§$Ч×)Ëžñ33TÕJ½S<§ý¸je™5S?Níäv©O¶ Úcß?¢8¨¼°íTº R*‰˜™jŠÆµjvyg™3<â§ú™²" ‹÷Èíg’ ‹ãÉåWÁø|3Ì–9| ò#Že?ƒ3?Æ ¢æ†T‰O¡Ùç‚)ª ½–5Á×Ô\ÐÉ>ØÓ©—å1§%ü•Ò¥uvœ®¯Î M+Èì«äQÙVSØ¢ÿäQU+¢ þÓÚ'ir̤Ï^ÆG»J©J~P÷2Ë–bæùëãÉä÷9œÎ|8ä‰$„’BH8ãüþ†kÇ¢®Ô¸ûªdµ3UÔË1jÏUTEC-§²šŠˆ‚SÙ|y<’a¢b$Fa†a†ø9%$¤Ôš“RdÉ“&H‘!Ðär9†Aˆ‘"D‰$H¨Ê2œÎ|8ãŽHã è8ãô;,2‰†¡"„P‚BA‘"DˆÃÎg1Ôu$¤”™2dÉ“&L’BH:œ D‰ ¤‚]ˆ.Äb ±Ñ$HŒ0Ã|QÉ$L™2dÉ“$HãðXa†a†ùåèk 2B(A! …´-¡ @‰†S™ÌæsGRJII©5&L™2dÉ“$Ht†‰$H‘"D‰# 0Ã|zºŠÃ 0à 0à 0ÃqUäôèRù50N ¾\mð2óo÷äÔÁ8ôù¬þùqxSÉ©‚|Þ*¿ òRðÓéä”àL)èU>žINž…^INÁ*èJy)8óöUåøQé9 „¶…´-!d²X, ‚Á`°¥•,©eKUª-ÔB¢5¨j†¨ú¶>­Ž{»»H™p¸\BêPº…ÚK´—iܹNåÊw.S¹:w'NäÐ’AÐ&/ >¢ù%>+Ž8ãŽ8ä‰8ãðrB(BˆS±n‹tìZ§bÕ;iسIf’Í%”,¡d´Z-þËk¹Ü‚îF­Æ¨j¬úǨ•Dë.T\¨»QyKêw…;ÊéNô§{;ÙÞÎöw¿ÙÞ¿gzMÎò›á7/¡z’õ%ÚDéý£ô=£è‚4 µIf’Å%„;ºÜîÇv;²ÙNíQݪ;µGw¬±Yb²ÍeªËu¯b5ì5{^ÇÕ°õl:ìIv'ú&\.Pº…Ô.Ò]¤»NåÚw.Ó¹rË”î\§rtîM7$„’CNEòB Ç ä‡$HqÇqþ 0à 0à 0à 7ÃqÇqÇ$HãŽ8ãŽ8ÿ™Ìæs9Ž£¨ê:Ž£¨ê:Ž£Ž8ãñrAŠB(A! „"DˆÃ 7 ñ§šÜ|qü)ÇqÇqÇqÇø¬0à 0à 0à 7Ì/§Vkôòrô/Úy%8W‡ÝC±RjvÕ4áÐʤËÏÕúù%8WÅgí>Ê-52™û%þŒëS:*SU]Ê»f‚TµeR7çÏßß’S…p\Só‚Ó™ï)’.F¥域©òrà¸æ£Žfªh¦j¦ªf†J¾~£ÓÉ)¸/Bhôò‚ô'úò~xj9$$„’BHI è:8ãçtõðtð­ ª¤zDê'Q:‹•*.T]¨»QuKª^]‹Ë±yv/~‹ß¢÷è½ú/~‹ß¢÷è½ú/~‹È^Bò¼…Ô.Ò]¤º…ÚK´—i.R\¤¹NåÊw'NäéÜ;“§rTîJÉ&ã¦ã¦ã¦ã¦ç-Î[œ·9n2n2n2B$H [-–‹E¢Ê–T²¥…,)aK*Y]‹*Y]‹*YRÊ–”µQiKj[RÚR EH©†oŽãàãŽ8ãŽ8ã’qÇ3á£ÕðdðÆa†dB(Da†a†a†a†ùc¨ê:Ž£¨ê:Ž£©%$¤”’’RJII)"D‰$H™"D‰$H‘"D‰8ãüb$PŠB(A! „b[B[M‹i±m!m hA hA! „$H‘"iO³õ«‹Õ…_²x:ôË?¿ƒ¯ƒf ü#ø:ø>¦ ûS÷;(eàËᚎ8ãŽ8ãŽ8ãŽ8ãŽ? àø¸ãŽ8ãŽ8ãŽ8ãŽ8ãŽ8ãŽ8ãŽ8ãŽH‘!É$H‘"D‰$H™2dÉ“&M ¡4&„КBHI RJ’T’¤•$©%I*IRJ’THôHôHôHôHôIôŸIôŸNçÓ¹ôî};ŸNçÓ¹ôî};ŸNãS¸Ôî2n2nE7"›‘MÆMÍV”3­{F†ž¾"ø>8ãŽ8ê:Ž£¨ê:’RJ:ޤ”’’RJII)%$¤”’’RJII)%$¤”’’RJII)%$¤”’’RJII)5$¤Ôš“RjMI©5&¤Ôš“RjMI©5&¤Ôš“RjMI©5&¤É“&L‘"D‰qÇG#–ŽG#‘Èär9ŽC È2 ƒ à ƒ 0à 0à 0Økþc«+Õ•êÂõez²¿â-?ÄZõez°½Y^¬¯òÉ:²ŸË%êÊõez°¢õaz²¿Ë$êÊue?–KÕšº†ãŽ8ãŽ8ãŽ8ãŽ8ãð)ŸCÜqñqñ~†8ãŽ8ãŽ8ãŽ8þ.þOãŽ8ãŽ?“_ÿӔà eÔåÏáçOò{_ ËϨ¨ÿc#µây!¨ž¾‚¯ŸSÐJ¹‰åš¼ü˜/‚8ãâþ ¼Zp>/ŽXkÑÖo—a¾[<2ˇ\Wü–·ËÿÿÄ.a !1Q0APqñ@`‘ðpÑ¡á±Á€ÿÚ?!è™P(AˆˆêÚ0h'R0ÓàÄf!B„!B„!BU~!ç'‘4“œ<ÀB„!B JB„!B„!B¢ªÄ¸(_ƒuN™Šf|m~B„!B„!P„!B„!BPª……Uuh'ËØZGÀÆ1ŒcÁ¤Ñé×TB„!` „!P…B„!BVUUïµ@‚a"‘H¤ÌÌiƒGD˜á¡QB°!B„!B„*¡TB¢àÑdŸO–O©r½d²*žpBš„Ä…QP„*„*ˆB…øƒ—¢uÑ‘(¾]/Õ­¤‘-kü‹°­è…QP…F1‚êÿëè§Ag™Üw`À¾Dm‹àŽx4ø,! D#3ÀÆ1ŽŒcÆ1Œc cèý©Æ1Œc£ö2Ɖ¤kÁ‚jŒ¨ý’ P ‚ (¢ñ`=ƆŠEÀxÆ1Œ|Pt1à>­—*éQúçìuE@‚ -+ÄÈî8ã )ˆSLñ1Œt>0<G1Ñú•L9'Ä~ÿB@‚(´§PãŽ8ã 0¤SÑŽxâ¾ ƒÀñkŽi"‹:¼3/]ûµ AE€-¶aÇ–aH¤Fxæ†qøÔQƒ!‘#¦¦DêN=1rüJ….Š&.¸ãì0ÃŽ8à D)333Œt0ãŽ? ðèJ ~Ø~ÝþlXÃD‚`.6Ãl6Å¢ÀÛ µ/QH¤R)33ÀÆ<3ùF‹E²ÆÜï;Ç™==Qprˆö üý¦Æ?EB?È“îXèÓÿÀë«ÏÿGµ§Þs>Î^¦*Ïäêgò£ÿâµÆn¯S3$À¹u(õ“GÖÖ_…gÛy{Õ~vOà™.Dôÿã,tUÿÌÐüà¿'êWU^­p¤^ÔT½®ý#É{ \‰qRô ÝK؈^ªeô5EèW _€×ªT× , Ø«ÙpeÀ`®¢ô ¢kÅ]'1…"‘I˜ºÊá.à/@½Ù(ÔÎhˆKÕ.ÇÁØ;gg"×zÏÝp2w²Z,`Ë]V e‚ÑhaH§aHºj¢À¨±/L°®œº<ó!Ë" ô¹le°¶ E²Ù`¶[,‹¬Ë%Œt¨‚ €¸Ãpbm–Ë¢Õ'Øa…"„! Š‹P„! ˆBª°!Q`X¶&‘•ˆˆ‚öA–Â@ P( ( LŒŒŒŒ‡ÀàAEQi^¥x½Z½„ÀàqFFFXr„!B„**!U{'cö;>`œ?É™˜çqî. åòét¾_.ð=ZUl>¢‰Œ¯baÀú+.ˆòéËÓÏ«BpWb‰Å&Œ~­ } 5#ÑKÇ<=T˜Œ±¼©$¡'9Ûšq"SQÒ@9DÌj'EÖ80"}B鑯^³þDç!&L¹äA™'©f‘†“%è„’y™nÌŽ%9I¥H„'ÈæC(ldÒØ”„'T•Ë$OÈC¨Å»QI¢9Á;!ähG„O(pç¦K1¢)"}‡О²ª¥"`œÆX¤9Ô‚&sN"‹*`œ—5=dš¨ÖMX™]É”kDgX>~ØÉëq2DI ÿHwˆ¤2v.IžòHdܧ”2{T׬žt¥ì8¡==qt‘1‘­å">b9“ND"Љ˜—AD“ƒA"%2 {Ñ>èK“X’N¹0Ÿ=FM“4S(¼)m„0„e 3ç(#hÜAù"eк‹:“$G° ÑìQT”Å1ÈËfr„!XP„/P½$ñ}D¿+^ê×GÓøLÕÐ\Òz=’‰p5ÒC‡,Ô ^’G8%ÄÌN±† Á ç!§Á7$=Î59fU™D5–%;{«G±«×Ó$¦$¥‘‘ŒÄŽ \¡xˆA@S<ÜŒ”‰Ô§=©71A«›$g”ÎÄí„ØÍ1$ÊIkj=ä3.†·“ª¼Ü¿‚8ÑBˆ ê íFÃp/*iØL†öƒC屆"7dÖJ¦ ÈtCÔú¨þÉå4w¤3Ä,ÍáikXÖ¡²Ì3JfX&³Â#F) ‰¹åå‡[œ1¬ð'ZaÇ®²“D‰s0rPdØänTÈ”„“Ó2F±½LçI'£VÔš<1.lŒÁµ>ňvEL&gJ™‚ï™GòMº4A¢iÌûW¸Ó­ÜG‹3ÉRç’ˆ.dägÍýRÐûÔ„B£3$T[V2•é ¹›–Ïâ¿ûOûLìhÄ<@¼}‰çÿ¬2öt︌c=šå Ã*ywÃQ!d¨m&£ÒTÍDrA“©ž¬„g0¥äErendz¾ÂÓø¡|Æk²ŽuæHc–ŽG6k•§¯xÐM<žÄêÆ„*3÷>Dd¦ù„.Lˆ³b!–ó9Á“?0„D(’HLs–e­Eˆ†"25tˆ¤œµB"k6žä2g( B¡‡7ö%CœÓÌd/O€Ö™Ÿ‘ÜS¢ÚäÛ—Á:ÑŽsBn&l÷!žò‘ QÌh<´—’*ˆ<‡w JŒÔÓ2\È¥åÊHD¿WXáM܉óè×7÷,ŽIÿˆB#&³“ùÔ°’ÐŽJ3'2^³C|M™38”üÉ; !å Ñ N¯þ‘OÉ/˜M2‚(̆wÝüºô>sDú‚°®•«ˆ5ãdYDÿƒ¿ð\ŸƒìŠÓhX¸;£Ê@·ù,Kbìp÷neL¨ P!B ! „! ‡O©,™ȉLh&!B‹Jĸ#T¨ÙЉ ™`dS?Ä :FHg¼fC\’™“˜ —$å%1.$œN3í’P—‘a*5ÌÌÏò,B¢ã,UYõÓLûjÃ3%Ù‘)·¾ïÀ÷ñ¨Ïí_-Áh¶[“¾]’ô‰¸¹¬Íþ‡<ñsò]üá½Ú…v¡-Ñ–%‘`}óK÷gÝ‹¼¾À…±Üø;ßéó¶¥hYÖš×ä²Ð÷ÁÝ[™ndeB„!B„!B„*!B¢õáØÒE1©zÉéB‘ΉÄa6- ²ø,þxÃÀ(ñG„Á–%­^ú¤½ù/>O¢OºK˜9Ñዯ‚óàïüt}0Xü…¶€óÇ”;_'oäµýŸ\ŸT—uDñ烯œ„b\‹ÏƒéƒîèR•õIcû-$îÂä—¦‘~^‚o;‡pmÃnàM.Fİ.ArÁÚ£n e¬3háê…Øíavn@J(¢81ãÁDt==šÎ„ôiB„!B„*„!B„!B„!pÕP„!Bl&Ţвø<àxÓÄž,ñø&:Ìû¤úæKï’ÿäò‘÷ÉöÉtù¼_&©r&ü/¦Ø¶,Kj5zÙÙÇ+SÜ—eé~\—¥Ñx_‹t[¦{äð22£Ãålq×U¦³¡ÿä…B„!B„!B„! ×***!B„!B„!B„!B„!B„!B„#°M„Ø]‹E¢È²- 2ܱ-Ë2Ô³-KÔ‘AœAI’9ƺ!§®'¤s^F—j"K!Pé‰ràD2ñÔÇJTTB„!B„!B„!B„!B„!B„!B„!FpÕà ûû$ŽkËe•&t ¢‰s$)D‰–œÂl†r§1ò"S¡¾LÚ") ˜Î "PZj“4Ähò*/LçF$sH:ˆæ „!B„! ‹¢ô(B„!B„!BX @êÒ+Ý*zÈh@Ž=#DàÍ$"‰ndd\e@噤¨6‘’Hµ;RS3.ˆ‰’IäG9О½Èær¦]ÔŽræyi´–Äÿ€D î9ã­ÆkK!De ä¨9jÎ¥1ËbÑ“•Ô„–„,ÈðBœš …ÁïVªª„!B„!B„/J1pèb¤c ÎQD‰Á32Qt#Ì“’JEØ`œ‘'1HÍ4–É‹qA,Áœ‰)L&b |‡¸P‘ÆEy”M$QÇÀœ"pu‘ª‡œt(I™72LM"Ñ`Ô&4 ""¾òÕUTB„!QB„*¡QbUXШ„!B„!B„!Fµ4`ж½‘¨Ä¢4 €1T‡à€"&`T!BŒ„ç"„!TF{™ˆî3;„C äÎLÉs­WUTB„!B®j®¨-5ý]\ '¡éã¬B#(„!BYT!P…B„!B„!B„.á!B¬jœÖ ‘¯L=‹ÑÃÚš‰Ì‡ÌO²P„!B„!T,TB¸à*ˆB„. CŸÁ¢IÔrè9'¡éá4ÇrD‘ÿˆ¯â÷ªìøƒÇ²-ËOïnü½.jƘ@úQVÏxŠ ]¬"F÷à×ÄŸlI ý$·ÄŸJp˜<åUO<`ðCÃ*y“ÊX´.@›©¡Bô‰Gñ`¤‹†Ó@æèrIèzd`4{‘HÙ'tt—$¹%ù/‹’ð».˲ä½. ¢ü»«×©vþ _Àð0?øª‰xÕ]/x³Çvç“è™/þG’mQsËÿ‡ÿªÂÿôýn?GÑÑ÷ ð0x(<àÏxóÁÏì_'ìúSû/¿¿Ùqó?²9Þg×2}’-þGœQï®ÃËGèó°yœQR>”N­þâpG«ZKŸ“ÊlòUµrZË>,ñ¸-BX«IósË ï‘õÄŸT‚üñæ°8ÚñcÃ<ð£ÍÁå`óMŸÈv]RE÷Éä#_R} zp¿êA”g"#Ú6Ó²6Ñöƒïè;Ghí ´ m ìƒ±_R¸÷þÕÁp[’Ü–dîüw‹Ò]’æ(bèý£´-¡l–e™fZ–¢ìaØ; }àmàkc¿Nùß.ÐñÞþeü誯KšÙõAwú.ÇÁÙø;oàµðàGRB €î bȲ-8 3‹fÉí?™üŽù;Ÿ"¯Üšp$Å$óU‡BOCÒ4±I Í2…‡33333333=Îáî;†㼺]/—ä¿4×…Ù.*%âáp¾_/—¸P­’Ùh´Xa¶a¶;Nðì;ØM„â\KŸÌþg|òwÉÞ;Çtmão.K’ç¿í|¾_/ÁrÙÚ;E¨Ãmm`í¡v–<#kYMh´Y, ²ÃÂþñoñoá'I%0$†eä³tN’}BİèšX5è¥ÇoþÃõß“è@B„!B„!B¢‘S>.|X”7àC8é¢4Ç@’zŽ35Ìκ"ßëÕ?ò±7þˆ¡B„!TBIÀŽwbyš©ÐG²ñš¢£3÷¢M²1B%LÏÓ_Üà/ÿÿÿÿÿÿõÿü!B„!B„"`2+ êähŽ$ôF’4„AÂ23Tö“N‹33½Æj'ú2¨¶“<þ¨þL!`„¨tÿµÿÏÿÿÿÿý{À™€¨B¨¨B$G!ÌŠI Ó¼êãLt '£Æ(µÐ2a|‰Îy” »šFp™:Ök)}ˆeHyìä™g¬G¤€„°„± …ì wÿÿÿù‚H?ðs"±]Éš:ˆéLzùá'êôø`þ¥Õ¤eÁž2@(‘¢c™¹…¦ÌÜ‘‰šV„æÐ‘¨«{ÎR¤ÿÿÿÿÿÿ~â@BÀ#•1©ŠcRˆõÓYèîòÅ×í ÿ ÆÃUz\|úõÀt9ˆÖ²L!´Mngˆèz8H9“ ‚sìüÿÿÿÿÿï/€@í0j“Uu2FitY<ý^ƒLÿ‰5˜pKa«D‘c†‰‘ÝkòX–‘d=ð=ÐwAÜe¹‘‘‘‘„*!B„!B„!B„!B„!B„!u ÿÀ€aNc]bãÞ·cZ˜t4×CiSpGaüIÓ1&ù$úð}8>ün³܃íAã ñG‹üŸJO½'ß“íIæ$ò’yi<Ôžq†bY·þ8ÿÇùcÿ(»ñ|¶€ðÅ5‚Óæ _˜,>`¶þµ`N_—?Œ<1âx™Ø‘?ÆO‚:¯‹Òø¼/ R[“»Àz*ñp¸^®83zí¶8ŒµÄdÃó#<¤‘q}™ sÓ S‚yô6ÿT♹ÁcðYüÿ¿Áã x#À‰jX–%©kKkÀ‚Égû-ÿgäïüÿ“½òw¾N÷Éz~NÿÉßù/ÏÉ~~Kóò_cE).+lTŠ¿/˲ä¹/ËÞµ»@@«ST‹ÅìöÎÙj "ص-xCÅAâ ðxüàúâ®ÿœ þ0x¸µÌÍš«GB“è$U\~g„Æzœäk¦tÊDŸs¡¡Á 27ðY÷ÂÜi4’(Žƒ—´K7DCªDÂr r÷‹ôùj´J˜IPBÉÙètVH™\M5¬F-~K“í“ê“ê“ì“í“ì“è“ê“îš/ªh>‰aÜwç}.úKp·.‹âø¾/‹¢èº.Ë¢ø¾/‹âð½. ’乡¹/Kòü».‹¢è».‹âð¾.¨>ˆ>ØÄ_Èû#µðv¾ÇÁØøgÁâxÂð‡Œ‰~®_£î—èû¥ú>ïðZúv>ðXúvÂ9Ó€R™¦Oþá«—õð ¤A%1 ‡3€Ht).H˜&†cq7IÜwH÷H÷H÷H÷‡¾G½ò=ò=ï‘ï|—%ÇÉsò\HòGž<ñçˆ>È;_ÙÝÕÕÕV¡`ZÄŒÌËBÒ‘jY–e™fZ–%¯Ékò}³K sS20ù£¡Hjôh^‰‘&zXßÿ3ìÁûií;}o¿ìv`]‡aÙW³Ñ„ž­ÿøb’)Dt=2uè‘zW¯„N €NˆÆŸÿû}' ì©Øv`”vp'‡aØv‡iØvðûiíj{hŠ‘ÐÐKHé(B¡\!„{ì 0\??ÿßÿÿÿÿþ.‰½=b5E…B¢¢¢¢P…T!B„!B„!B 4! „!B„!B„!B„!B„! £Ç娓ït8Ƴ¨ŸÈGå|²s#ò¾YmB0/ÊrA¬ÊS‚ ÖHü­VdpÖù '¢O² –ŸËfÃÉü‰ ~WDßxޣēSX›™£òŒ—f„q#ò\Ñk# ~ŸeÀ¡:Éöü,Âr?bF¤³‡åˆ'ýHôÑù~Zh3ºGå~nÄß|Êù{DŸpŽ{Ò}³5-ŒÓ’?+A5Ù5ü­”‰×ÐÇävUùe•ʱH£(ü²ÐàׂB~´áGÔ?O)&û„tøös«Àø xÑœŽ7. ¶, "г-ËÔ³,ø4ô&ksü ßtú“ÂýCõï×:±ácÆ1ŒMÄÜMÄÜMËÅâéwÊùr²\.P_.—Kåòáx¸\.ñ'ÃÐL‡Å|_/— ÅÉ.IzK’\’ä—¤¹%é.É~KµÇTŠ*·‡á]É8Hu’zÛàq¸›‰¹p¼^/E‘mÄÀ Ù\/ Åâáp¼^.V.a÷퓈)o—Ë¥á| äÌ| 7p°]§t¾]/—h®‹¥òé~Kåòáp¼].x÷qÜg¹žæ{ŸÈ®wâî.åâõ åþª€‚£Ø=ƒØ=ƒÚY,Ke²Á`M„ØJ¯KUq†qÆrgÌNܵÇŠ\.RîaX±FÑl¶Y, ¡vavc°ì£¶«ì8þª W÷ÿÿ‚¼_¡~«|¾^. Åâát¼\.‹ÔWËô×Kåâáp¼].—Ë¥âñx¸6ãn9ÜmÇ;Žã¸î1ÑŽŽŒcÇ¡Ž†:A ûÿ~÷qdž1ÑŒc«ÂýLèÌR ‚‘…#n5Náê!BÄ!F …†25¦TÈË‚g€èÆ1ŒcÆ1ŒcÆ1ŒcÆ1ŒcÆ1ŒcÆ1ŒcÆ1ŒcÆ1Œc«£7ìÑÔj£Æ1ŒcÆ1ŒttcÆ1Œc£Æ1Ž£'îùüûWù2?¯b/¯ҮŸ-ø !ú' 2ÇL„´üðÿ%æ! E"…# ‡#*eW4h´® ¹þP¤R)°ÃlZ- e²ÉdËÁìKPzÃaŠ ªqµJÁl¶Y3´,–Ë%‚v FF‡=ŽwP—¥`µ\¥Mf$–FQƒ(2÷„)Œ6Ãl0Ûµ+\( !¨8ãð D°.ÅŠ‚ìZ, ±`°Dig .ˆv%y´íÃsm=£G!2&FF[™p÷ äï—ËåÁËÅìpw'Ή¡Ù.Óì— ¼\;èj %It¾_.ÔÐ…"„)Œ0à 0à 0ãŽ8ãŽ0ÃpÁµ¡AQADQEQD„„¡P„(ª„( PB2ÜÉje¹ÜFõ ¤îÒ¾_.ðÝ\)EJ, ä8ôËãy“¾]/—ËÅÂáx}ÇÜ}Îâ¸Æ:¼HˆžFìº_.‹ÅÂàçr&LØóøÌ=.>>åq~"-uJTAA*  P!P„!Pr„!^…ÕÀÆ1Ààp ‚PAEAAEA(N݇a†a‡}Çq·q÷q·qǗ†Dà—¡ŒcèÆ1ÑÑ1—‚3‚9%ä@¢Ï25Î\ÆdzÅ$BÉ+åòùt¾].‹¦U“ž)¹& 68ûií;ã¸34ףǎ8„AAEQE¥k¯¡ýa†®õÜqÇ›†1ŒcÆ1ŒcèÇGÆžlÐÇ/Sr)òŒ N èwhk$ì €D©Èe3:9#W‰eÂáz…áÆÜjÈŒI ïø*ÇPÂwî8ãŽ8à 9r9ŽG#‘ŽG#£Âý¥³;EA2ÁÏ™­]`‡Js2T ˜dÄ Ðf°h®C³˜ÏÒN7E@Ùâ6ãn\¨\¥z“¸ÄŸÁ…Lò'.^ø–'6„W±$Arh≞2“1H§a¶,‹E¢Î˜mÎ㺂…‹ô×h ¨²öah²[-ËBlfÁì$Ń1H¤Žre###!Å.5À‚d†T ÉD8fmISœœM§0ˆq$̘ÑYjf4è!ÑÁk*!QB„ˆÈ2s–!Aü™ Œ° †°X-RL^qÇq†qǠè[K%’Ù`µ%¡•-`´X"lɽÇp»‹„Þk*ùVÒ-$ž0½Nép¾\&#Áþ$!n“<›%€E@‰Ÿä"”šTò"vÐÄg™ê#4ÈŒ¼èIÎfËOa!qr1ŒcF1à cÆ1ŽG&fffb‘lÂÉh¶Yàúí>çqÜ&äb+Ø%’Él¶Y- i'aØvle°é¡™äcA˜j9¤Hæ’Hèð:DñyÕ†&|“8É•©‰f¥$E̾9‘\ÆÜtôlŒ‡Æ1ŒcÆ1ŒcÆ1Œc ÌÌÌÅ"‘¸¡p 8ã⺠&+EŠÄŠÕ¢À› @£QŒcèÇGQÀèp ¤ÂD&$DQi(‚ ‚ìv ¶?{iìaǬ4Pr9ÜaÎäIR\Y222®FCŒcÆ1ŒcÆ1‹™™™™™™˜¤R)ŠE# 0ÃŽ8ãŽ? Ê(¢!AZˆ ‹ (B"EŽ*Æ1LaJn%uáÌ/]††c‘ÈÆ31úOhí`²E…R!òªá.ftšÌpUP„!B¡P P ‚(  @ P( P… ôé˜Æ:ÇC¨èja†a‡$Ì’2fFcÆ>>†pÉ(d49dFW$.Ù4åEXœŽuŠªªhÿÚ §²û­¾{µ‚ˆÏ7úï®:ç¸û˾óÊ}°Q„Ö`ÄÓa°±°oUÛnÄÃÍÚ0òÓÏ8ÇßÏ|åSMœ ´ÃÄ @ÒH >ª¯®û3渣¾8/¨f ñÏ–ÒÆmE0ÈóM0„N5¼sßSuõ[}%˜É^M÷^ñÊ‚B’û,Êæ²kE=UÏ,p)4¼ëºúí¿žàºÊ-Œ0)ôKòƶÃÕ1‹ ³Ç,0î‚;¯šïý}ÝþßÞºÃ,sïn5÷1Ç-ÿû¼ÿÒ8ãŽùL"ûÌûl°)ìµ¥^ ç%¾¨#¾ª ¶k!Žk”óΖÖA–^AF|‘â‚\÷†©×aý¾çþ½ƒ½8ïÂ@AeqTÕM´Ø}䢖(Ž}$yÎ0}·ZA:Ð6^*d€âI×Í<¤y·“hÿ奄öÇ ²ÖxÀ D‚ jË„u~»Çܼ’ÎwÓÏ#üòˆÀÀ v•l¦YÓ0Ö8 pç}7Ï>Zá¼ÒH‚°,ѤD¶aÿ/8@Ûîºüÿq…ÙAFûÛýÁƒƒ;}÷½òl°“<4Óæ€ m²ÌËy÷MGh(!ŽüÃD×¼ €þ{Ïpʃœö`x-¶9=§ Î1M4ë‰?¨ AXQáM0Óì4ó7Ói2ûÀã¾:Û-K=¥5÷Üi4×}7â€3óß»O¾û£œóÃ;™lCh!_¼â²È삌Lö—ÿÿvïú‚PÇmpÓicR(`€ÅšAE}^´O2ßt#’£"’ û(ï~†dZF8'¶÷óÛêŒa~ǼÂwa=<ªû#²2îK{ûßZJ ŒºÂ w <0KOÚà¬ãM?¼<÷ðL0Ãï®;ãŽÒÊ&»"œöæšãÚY‡ ª®pC,#€0œ­8`K/;­Á5ÿ¾ö›žsŽH죒D;)´±SmcO cM(â­õÆTæíòÏÿP‰ )"‚  ¾ù£‚Køó8ûO<“!ÅsÏŒÄ ê"¥ôW²ËÌ·~8Wüsæ9üç-xÒg°ëM,q’(–ëìÈÃN À É ‚pHM´PÑÆÿö•£ž7 3À}Ï8‚È!‡ÞwÿüqÊ%œ’ 5¿ÐøïùÖœóȇ¬{ˆ/¶ôñe!“m4`U qÇC†Jåóøì’ZÏ0 #úªÓ•AÐ]öóì<Ç0óÕI‡q”Æ0ðϳÊês^Ì{e9D=ìé=ÿü^ÂÜ}õÙAd $Ü2ÀQmü·ã¡¢K®‚®ªyìªDSÁ'X •„óú¸át±ò—Û׌=ú›4çSsÿ=¿‚Z ï׌,>ÃøÞ Ï<ñÎËn¢ 1hã0”qÀǰÄ<Ûà„38‘mQ-­ùÓï¼Ø~8à‡SÂm‚Kì×ä„O,ó‰0óË«ßû¡Û­9ÿßê%•à >ûŒ8>[ﶈ ¾2@qÛËn C^QD¼ó^]›­›¬dˆÄe6òóAöÞë=y‚N<Å<óË­°¢SÖd@ ƒ¬² ×ãÌ* óÕë_pã×÷µÀíçÿ<ªà€)û¸­½ÇEÄœ ÂÍ<ÀG<÷ÑE·ëã˜ÿ FI5ê¾øòÇ~²ÓxÀ„Ú÷iÚuÒ<>ÚÇ.#ó[}äÐaÆŽ{CkNSô º¼w2I›­ 4TG,üç•IE”aiç œ€¼ñ¡œ0 0ǃLO¾^<úǯ5d1ðìËH ç_y€RI(4idÙõº£º¡N()gƒ¯§—Á]æð÷ŸóÍøë²È°4â ·×V÷!ˆôaà 9À<óðΡ‚l|òƒ7ÉÆ(ã™GœD O[ =×þ Ò#\¢¥ˆÆ`¯>îÿØã¦˜ït¶êâž*·Ï=°Ã%Ü]óâh”X¨ì…E>ÓµF2|ªÂC,ðûŒ¼7üð‚°vÐô“ÃÂCÃ{óB¯–w‡Y­ç‘}뤭Ž|B8ÕdNkjÛÕ 8ê†XAltª;P°ÃîßÏrËûÍ/ü­ ³¾å 7±ûˆ€‚ m"-º¾üÚK­¥Õßá’ Êþ a‹vÖ5÷R¤=ÌŒ=”l7Þr ªžÔ—¾í4ÕÆpºL<º¢ÀŠÏÏûóq -8Â8Ão')ó< ×DRâ¿ÇÇ®¸iËo$Ž«ͼÍ(ó^Ù9÷±Qê¡AñuèBW¹{[Ã)ÉR $Ó5a©º%”Ã[Ûºx˜þmî³Þãÿ¼ ÑœÓkϬt@5¶:إĠož¹ë(Cnïž¹w óEI"0õ‰ ÇJ0\ µ)±º±˜ÞQüß"ñµ=ì÷ug4ó·è‹7(…ûÿûóŒ6ñŽ&‹]›¡¬0Ÿ½èÒm: ®ÓΪËé‹þ?Qô ~ÃŒ m¦uÑ ùïÙñȈ¡ óÔŽ;tS^[’ÑYåQo ¸ð޵0fp¾=ÿÃðá-²KxLÛ4ï'­bÚ—;ž¬¶hvì  È(ÏJ|/R.¼Š±DpÂ`u4 }‹…k_-ª¬硼3e˜GLãm…Ôë"ÿü2×Oë¥Ãçi7eï;ñ%]M7{8:%|yf~øA[ËRÈvþɃ’ ¬·Fdövƈ,vÊ3}uî¹~k›]_L Ðsß’XïeθšN“œú_ÔNTS í=oùíådàK íieritß}çWo$e5mfWm$Û °o+àØ)²û ý^YQö×M$4žqKÜ£7þ±×±©Ô×óä= o[”oF,rˆ„óï …}÷×M4A6öa‹h¾+&,¢›kÓUfZ1C +𪢒ó¡ÈÐQTI” ÷éD#óå眺wü@èœFƒômhõ°ÃÝM¡hH'¶û/<óO<óÏ ®8%¾óÄ ¢r×pK`aÏÚüxP d`Sjƒ~°?±ùL2¼S9€Føa½iî¶–`žã²Ä£Äé‡ÇÇ‹°:çŽ( ’ûã‚Ë{IGoNü†9ä·­>óÌâŠ÷ŸYÀGmDò€ÂÃ*_ΰ¿–V­<µ¹(·7jÿ<Ã<öýÿ°AÓ ïúó8çŠÛÏ,‹ßE4ýîsZÛâ—¬~óO0箲ëRu}ßpåœC޳ûK Îûýo†÷¾‰L[ˆ‹ÝÊ|¥tûŸæ8¬± c‚nIÓM0 ‚ 0ó-¾<ðÿ,²Ç¢ÓAûœ¸°æ\(ñãÑe©5²p ½ïþÍM¢uÍ0°G†˜Ú¦:û”…Êqbß·,ó‡$xàžÉ¨D3‚6ä3­Œ mõ48¡Tÿà ÷ÕŠ8P̶y ÷Üó?&…]÷QÏNí<y:Ï­¦Uv R¹:É¿·1»BkQÐÓ Lÿÿ¾ðæ{ä ]å?ï4Þi4GßøÏÏ9ÌËå¯÷ʵ±Á“K48qŠòoÐVa@¼ÿý4Aá¨ü?ë‚%TÀÓ‘Ø€oÏR¼ßß(z×ùÉ SÏßÿØÂ¹â–HñB0ú»á¢õ‹â;]`]ù±êÐS |]xüé¬å‹F`—„Ãß,mãïñ|8±ûxæy Ø,BÖWž>íhÎß²(AGxS?ÛÔq¤A0à "-g4ñÅ0áÅÕØ¿zçb ¦Ã9æÎù#”1Ö¶§¾4y¶àQmøý3òÿçïVø¤7˜C,X8±K‘€< 0Â0ð2ÊLóË  BKº{ÿÉM…©l³ÆßYÿ6 ‚uî,o¿þò0𧡆pÁö˜BÞðn1X™ÊÄÕQ}¶9îçX¯²+[Aq×}ôÓA4â˜CÐÀ,1 9`ÙõRU¬ŽK 8.6£ÃÚhi·Õ{å“f47¯óâNfX ]¨·ÙQj²ÎW”Kïë–ÈäU¶ÓE§^ew !„ AÀÏ<Î<ÓK¶´7ðÂs÷Ï-=µ›}%Úó²+£8OÅRuT¾·öOÓÏ»M¯8–öðEK Ý[¿Ò ‚ˆ"]Æy¥ŸDø$‚ æšxb½”YÐml0ÐVµ®®¹lVm-ºf¶[U{¬Ü$Bu‡È‚0ÌÂI,L=ܯxë‡û´Í“ãôJn°çðà ‚(£wq×ÒQ³ ‚¨&²j£ˆ‡‘a5®¹Ÿ²õ›öþ.Q‡ðóWœE%µ„sHiUzŠÃ?ø(„G:'Ái b/œÒh[í a†7=}Å[L0,²O¼8õôÞ}„ÞABj+§ÙHž´¶lÖC–›IÚƒ`ã4²eC)ê„Çtú‹K~:’Þ2¯¹ÌÂ\z¤) Ytœå1Kx~ïtç¶žÿâPçÞHs,¾è!ˆòqÔÕîxóåJ¢-ýÃ‹Šˆ±K ?á\Šìuƒ:Æ€;°çý?nT \îAópMçšEäcTAö[a%å‡_4ï.w Á <Òƒ<àÆE§sÏÀñC»Î{%¤"‚'}­I ¼ó†ÞE=ê³Ëý¾>“¸ÎY:¡0£_MY>WZ%˜wŠ çuwEÕ—yã 3G8¸-¼ó@‚!½4Ü÷ífªë(¢#®½zû öŽyh†¼šMïrÃû¬é“ȧ ®q?&|BõÓ\²äƒˆ,sá°µ]dq]¶Ûa]4Pz/¾(!» ƒ áQÇùÃK%¾Èa’k¥–ʨóÏvvë‚»‚¦Ÿþj$^7”p$ºµèEx+¬¡qÓþ?úê†4#Š»G ç™E—›y$ó{ï« üã ±ïÿ<Á0Ãý*–éD:îû>`Û ¼ÿÎCqÿhŒ·çаúœ€e×,uz׿þÛ),®±ˆ¦¿ŒS (C‡]EûüûøÃÿËŒ0ÉÐÃà®KOfþu†‰2l7í_?šY-4O{D]ïEÁï²GA®Ý‘N$i}}·Âˆm¦úÅ"ÜbcBSqqÅ[In_c ðÃóÃL0ADÓC '¶+êcN:Ê(讼ëŠ?‚Xwý°C×ßãß\îF7Ï‘¶¢%dëLž)æš$ù¥²sQ{Êc“MaÍ=„yôy…Xà ?Ç<0ÁŸy|!¨xj‡þ5ÏÜ$º º÷ñbš 1ÿ—ò–_þö6ý± ;þ…µ2¼‚_ë…ÝDRÃY'WÝ™E¨£ŒCÏκK,€7YÖ“]¤Aÿî°Ã 0}÷â–ç²½0† *¶ØÄblª«°ûõÒ`3%%4ÜiòÙàãÂàQøaß÷Ö rQuÈ6e¶áŽDzãŽ>".õ¥PU°]Au{Ï<0ÑG˜Âx)º 8Ùé&”³­Žnª»ÒwçVU¡‹8•Ó”÷Ÿòè1ûÎó@ûMn˜QËmÄ›!6µé‡QŽË¥žÑE6{ Ï÷ZQä@tA_í0ÃA,Œx¦¢KäŠ;$’Û©TÛA†E0ÁBñœŸ£G@!Åf{OK,°1ˆEåè5P{/YZ:#¾ÊÍ.8l©þEöÂQGAm½ûà Av3–È"’x`ŽË|ÂÎa„]ÔqUÙDC «þpF<œn§Âiů¼!…ÞAta'YU~p™fšx!œñäå‘y_¸‚ ÛAAWþë<ðÁâi²i¯´d ãÄÒYDPeäAµÕyÒÃ<ÜsX"_–°4°f®pºEÛaô—etqòl€¶Xi–¤™µYß…ýáéASÿ¿þÿÎkY"‚x2E…mdœqDTQ°A%W}ùà’¼¼oÿ¼ aÃñ0Œ€‹Õ[QU‚7å5Zëà‚’mçßUõùß}öÒO½óÎ3}‹å¾ÈMÆ×êØâ® †yí²ëKIÝ|8e÷77½œ{ŒD3Äöö×:Ù †éCM¶•|²K×ÞAUTqŒ}„RaµIW×Ó|þûÄT8ˆâ ®¡$»Çll¼óùŽÁG=Û«p}þauŠÇðÀBcåGq%Ü Øuó=9Ã/¾Ý·l;‚WÌ1UÞ,ÅV#N<‚O0ôÓÿø$£ü0¢oõëXãiôÅ 0Ø  0‡¼Ò ¢ G‡“Ì ßO'º0Æ |óK8ûÛ$¾ë+‚)"‚lPaA}çßM4MtÐq×ßuÇ¿ÿÿÿïoÿþ?ßßÿÿÿÿïßÿ¿¿¾§_ûž{c‚ ]³ˆ  À•A}ÿþú ‚ ÃË-¿Þ`Ž8I4A„D0µ4´›}4ÞAD|”Ó+”UÆöþ÷Í>óßüÓ¿ú¾ý:–µ|ú¯¿îÿÍã pK.kÿÿï²( ‚+iÊ +°ÀQ4ÓIcijaEZuð}gÞ}÷ß}÷ß}¼ÿK̶æl±Š´ç}¦ßêŠüà¶ù-†5ÐϽA“ƒË?»üñÊ#Žûï¾Z¤-<Ï9µË,ûÛ 4AAG]aÆIQGÛ}=ÿÿÿþÞê#ãÞ°·ýôÓݼþºbÃÌçôÛ>HÌ,0¸lÞ÷ÿï¾È ’ûæ’Ï_¾k¼´[ÄRÂ4‘IEAAAgûŽûïÿ¿üò;d¦<óúÒ!:ëC$UFsšÀ_uÜU£"ó¸u3½÷û÷ºÛê’ûï¾›®x䲋n÷ÿñÿû,óÈå‚âˆAû®âwSš£'Å™CÚM* `íÎßd_X³Ç;¨« ‘@ÇBYgëfIQP®ù»ÇÕdc]ŒQÂaÏo®Dc$³Ýsß™cL1ûÃM)Ÿç<Æ `Ì Ñì¾´6ãÛâ ò·°ÛÏ~­”pŒ4°F8ƒ€ #c¿6ï$ª\°ÆÈr S]œÜvû*Šå‚Ël‚<Ó©,ºû<óß¼Ó{çŸÏyž{ؘbÃÌ0ÂÛjŸïÿéQ·^ø$€!o<ÒCRÆ ÀF ŒÃ}üãN"Û_U‡uÐß’ôsL›( ‚ ‚ááÒˆA4C ?]÷ßþéqÇßÿÓÏú¼ÃN0ÒÄbX«íFå°³B49|ÇQI„Ïé*' {®}"H£OBAN^˜ †8ã}óŠC{G=3ÇŒµk<£Œ ‚AO;¤žŸ® 5ÃÍä›,8«?ó'8ï6¬›+¬±H€í¨-%yý÷çA€€ ß]ßâÿøþ‰ðÀÇ}ò<ƒÇ#(‚ú ƒß}ýÏ~yÿb¡õØÿŽ(Ÿcð}ô]ö}t<Ïð_pžƒÀ‚÷ÿÿÄ$ 0@!1PQa`qApÿÚ?á–¶a ˜M¨LMpŸˆ¸¼™·7®åðw7ÇÌÂk›7fùØM3nx¾ ‚ U¦”¥.iKǼk¦í•J\ÜR”¬¬¥QEi)QQJRí]›øà.˜Bh¥Å.Íe)K‹ä…¼XM0„#×JR”¾ñzæâíOÁÂp®iqqyÓ“|åæÎ.Íò÷Dð0Ÿ«^Bìß#5NUÕxÄLÏ 6fíã_K¢îN5Ù»·Uظº/"ð':—7vê¹¼ ±w¯‹º.n»É¥âÝfðf©µsviqJRîR┥)K·Kªèºï‘¥)J\R”¥.)JR”¥)JR”¥Íâ^ñ¥)JR—E)K³K›øKµJ\R”¥)K›¦—b— ë¥.Åòbè¥.ª\]êRæââùVˆ{ ÇÐûcì²>…^ÑW´TU¿KÃ¥ÅðSƒÆøuû+öVVQ^Ϧj‹,²ô…)JRïÒì]Û‹¢ñßË-A’I$âÓÍÙ¾d›T¬»k~b{—ÀDÕ7iuSвîÑ zë\Ü^Å/,„Û„âÑ= qnîÝÁ&4õA¬ÆuÜ| Ròîqq.‚#,DXPˆ}ػΒ–J+üA¥ø!S$$Äè:9ô|d¹%BH/R^¢·P¢È«þ¬A¨Ý¬oHiÅ3 ýŸ#¥`ÚCKôjh[kR.«¡`÷Õ.šRì箊ñY\”®KŠâ5&íiÜLÝWE A9`º(^‡±”‚X§WÜN2{Ñ“è] Ÿf6Öû§MÓïT7DŸVKpÿÎMÔûd~ »)!¡üÁJ½¥ÅEEGB¬t:ˆˆˆˆˆˆˆAo€ÁE‚bŠ(¢22‘‘‰=§à‹žì«Ã\VVVVVVW¬ ¬²Š(¯Ez?ƒù'Ñ>ˆôO¢DÌöšwmnÒè¥ÏýBÂDhŒŒjŒŒ¢ˆð“lº4éIQQ…sg ±aºžqqþ2±J°¨q´^¢h¨¨ª¡A4(«ƒŒHÅ+cw:A50‚/D6ê $›}€ŠmM›³3Ke‡è.ËÁiNÅžâ„!BŠ(¢Š,²Ê*QEFFFFFFFFBƒ ¶ÅÙaúâðžÅ†&TTt**Ùú`úBûؾÅöEöEöEöEGC¡Ðˆˆ„DDDDDA$I:ü¢„!B„!.Øÿ#íÀ. ÒxŒ¢ˆÈÈÈÈÆ™Ôêu:JÎ¥egR²²²²²²²½•ì¯e}¡ö>Úb(¢²²} ´8ëyîC=½¶„âZ.Ž™¨¨éŽ˜ˆˆˆˆˆˆˆˆˆˆˆ‚""""" ‚ ‚ ‚ ‚B„!B„ÃÇc¾¸¶ŠV†ÙJÊR”¥)JR”¥)JR”¥)v@+HV R”¥)JR”¥)JRåöàûï­ç©öЈcHˆO´ëÿ¦.)JR”¥ÓJR”¥)JR”¥.n)JR”¥)J\)JR”¥)J]3Þ›¯cüÃÍ++:Úuçu:uuÚ¥)YJR”¥(Ùÿ|4Û~ bj„&ä! »u×w5K­fÀݺR­=3E I$’H”Ïsá—bìQì.Ãñw™þ¸dºm]„ãó‹l”Cp—m—µGB22222„!FFB222222‘‘‘‘‘‘‘‘‘‘‘‘‘ŒŒŒŒŒŒŒŒ„d~ˆMë¥)JQ*ë Š™JR¢¢¢¢¢¢¢# TRAFà‚0‚2AQEDAEQt]@T4àÑwØ| R”¥)KÉ„!Bfnu.šVVVVV^j^ ãO;<å<§w„'›NäüTð]Ÿ§[%ýWkõ©EúÅÝ õ‹ºýokõ©…ý_wëS¿ê”éÿH&ÄÚí~’22?DdgÀø Ð|¢^b22?D~ŠôÏð>GÈù#à|†º"¿eû/ÙýŸÙýŸÙ>϶ºƒù#ä|òÅDDD[SìŸdû'›EX‡øxð>Àø#ä|‘ð>GÈù#à|ô|ˆ½"/D^ˆ±6®Å.)JR”¥Å)uÒí]ÀRî\ÞðÓ™J]ùÍ›ÎÒìÞD!5O­JÃâR—vl_ ·VÌ'…„dddddeh*Ë,²ËÂñ¡±ò+@I„^ „Ì&#! „&…l±AADDDDDDt:b£¡QШ‚ ’IÕû(²ÆØ]= ¬¬¬ŒŒŒŒŒŒ¢0ˆœ:ŠR”¥EEE)JR—@¢Š,²Š(¢°¥e++!t†™ïØ¥eÅÕtÜR”¸¢ÒœdéŠfŠ¡"g³KÃ¥ÅåÒæ}†£Lú…Ùpá2³p‹ŠRéRDâ’I¬Î„!CIm½Ûªk»×)Ê–˜LB22`FQE”VÖxöAD$ˆˆˆ¸“BŒÑB„!5 ‚ ‚""""": Š… ŠŠŠŠŠR”¥)JR”¥e.ivWæÃî†ÙÙ·ÿÄ% 0a!1@QqP`Ap€ÿÚ?þ«:™™™™™™™™™ážÃñùLü‘îž¶fg–ffffffff‚†xffffffffgû¯ÍÏM˜ÏHšlÏ ô¥9ñ)}DlrG|ˆàˆè;|ƒúFFBŒxt)r‘ÁÐ8þA ˆˆˆˆˆˆŽàùGò_œðÌÌÌääçW¤dG"#b#¸|W¸ÌÿMžÙÞ€&frrs‡¥éÐD|güi ììììì÷_òÿxàøiŸæÇ?Ä îŸÃg†~QÖwHˆù9Ùy’|:NÑÔpGhˆˆí¿áˆë#¸DGu3? íœðï‘Ú;DGIÁø¤pDDDrrDppGIÉ0ˆˆˆˆˆŽ_€ðü2"# Ž‚888#‚"""#¤ìˆˆäë~q’#¤ˆˆˆŽ‚""""8"""#¶DDpp ê:Hˆˆˆˆˆˆˆì‘ÁÚ""9""8;OÇΣ°GAÜ""""""#‚:¢"#¤ˆˆàíïÌ#ƒ²DDtŸˆˆˆˆˆé:Ž£ƒƒ“ø‡hŽˆˆˆˆŽˆˆŽHˆˆˆˆŽŽHíþÁ‚"""# ˆàˆˆˆˆˆè""9"?¨e”þï5æé‡ñ^ Áx¯à—Ô¾£ccyÎùÉÉÉÁ‘DDDrw_‚«31ðLŒŒŒ„!B„8Bˆˆù'I<ãþÓ¼k;;3³·’ò^kÍy:€,Õ‡"8#’>W³ð–g°ñÿ;“²[okFÙ¶rD-3ß#±ÿxûãïøÄDEœk²|ƒcà3=/Qz¶#²p—¾Èˆë>!ÑìqŒÍ?ïVî%›ˆðÞ°äâ99ÒÏKÐÌò|C ãbÞ׳/}ŸÀ"ö8ƹ°ï´ÄÞ¾Þ½­ ìÚ£o-š¨õÛþy³wÓ9¬6Í^ ËEâ“s†~ [°Ý½¾›2Z¶lŽ2^ë¶àgü½<ÖóÂ}³ÞÍ~)‘Á3g·ð½ŽKÓ fÆ^ ãÁxï²X5-Ç ‡‚üŽ Ã€Þ¥¬B׺ú–×Òs¯¦[ŽnI¯Ëw»¾öÙÃß/ÆÒÃêÛFßB„ÝÏL°jÃâm½g9ž¹üްû¿RûˆØˆØØØØØØØÞ³³Çe)K·èB1ŒcǪˆ““““9nö3ß/Éœœœ‘Ó3Ã3333Ã?Óê22ôŒúŒúŒú„g×h*1„/Õû¿wêýKî_rû¼–—>ÇΤDt{yÙµL±Û>èýŸ¢y»2Åvã1Õ˜ÜmÀY³f&f~ÖÌÏCÚl·ßxö¯wå󤈋ÛÎÛ¦YžŒ¤Ù¦Çfz[‘^ éc}=%j'£RÑkÕ$À“Tµ}÷ÚýmŸe䵃þ­ÜfzÞi˜ïiž^–ffffg–fou¾ûÇ«»ð{ÉÒpvÉžAB18yÌoQŒcB„ð a#€àäÌÎLÌòÌÌÌÌÌϷߟþÿœî9k{^Uå^ ¼‚ð^úGÒ6666õëxfg†g†ffvvgeÐ"í fm{þsï~Yï{>)ÁÜÎ3ÜœœœœœœçÓ£ÒôŒŒŒúÈϬàðeàËÁ—‰x—‹/ðºÒÛo á¼7†ýïÞýï.Þ}¼Ûy§™´>ðûߥûs ¸©¿Xú/Âü8¬¿Sõ‘áfîg>ü^ë>!ÝÏ~3ߎçÖõ½z=o^=gggoYÙÙÙÙÙÙŽÌc³³³³³³³³²”¥)p)JRá±[»3ØË9¾û;OpàŽ££=øÏ~ Ca±Xlt‘‘‘‘ñl""""">:Ÿ}øÎ4j=ÖkÏz½NÖDDDDDDDDDDDDDDDDpD[‘€Eœg¿÷ø½÷ã=ó“>¯àŒÃ=²fffffffffffffffffg§Ó°DGI`Þãøs©½LÌÏÄfffffxffg–fffg«[íoËdvÌÍézwHˆàˆˆè"""#cb6666666667‡­ëzÎÎÎÎÎÎÎËļKħâvëdzx>Ï…»ëðÙð_šüfßñgeìîúïQÜÜrOðÙê-ÖgÂ߆‘¹Ø@cõÇëoÎÇëcÆ1Œcǽy/%ä¼—’òÞKÍy¯5æ¼×šó^kÉy/$>áB„!;¾Òa–¹oÉ‘?à®0ú‡Ô>¡õ¨}CêPú„!Ä9ÎsŸ—P¥f£l¹‡Âßiç>Ûffffffxffvfffx<™á™™™™íúqéÃÔw5êø™ùOøéþ÷³?Ò{­÷s–0ù'ñ¢Ïõ^íþIÛ#çòw]ã?ÆÙ#áï·øcøûí¿äO†ÿ`">añ×·ùLÿòÿüÿž{þÿõ¾ïë¿Ö÷4þ9ð™Ï¹ÉϹϸ}Ãîß}ê~IÙ87ƒ ØˆäãÓîôû½>çí“ôOÕx])må¼¼//;úß­ãÛŨýG•8þ¯â¿+òèWËyï-åây/&ÞMûÙÙÞÚú—Õâ¼WŽü¯ÏâÿÿÿÞùo-å¼—–óÞKÏyº²Iyךò/5æ¼›/¹}ÎÎÎõâ"?¦Gÿ¸ÉÉÉϸ}ÞKÉy/%æê!¨}_‹?éy/-æçWœ¿†333993399GëNsŸUQä¼—’òLvvÿÕë)K¤JR¿Wêý_®"1³ô¯[Ö9{de»…ä¼Ü^K˸ô¿ÞØØØØØØØØØíŒcÆ1Œc}FŒ³órÆÜ¾Ñ‘ÒGdŽ6xÆË²ý_»÷~¡÷zWF½ý_¨ûFýËîÑÏX3#OíœIºïü¬ÏLø/,òDq±L:€ çÑ»½!÷x|3øE¨æt³““““„!#‰¿<ï£)Êvvggg~ç~ï[ÖÞƒ ‡Ô#Äddd|G—¡É™™í—"”¥)NÌvvwï²GDDDDdddp"2####8"#ü&üÝîhÙß»ÛÛÿÄ*! 1AQa0‘qñ@áðP¡Ñ±ÁÿÚ?gÞטæÁ"_ æKã/s®s:LYèónBKÀæs]’"~©¿¨‚ß0šÊo3Œß}8õ9™ÏÀ$ùôg›8{pò[æÎa–x› °² ÃaÀœ‚B¦°7»å…eæË,ñg‹Á£ÐÇC=–¼Xžÿ@N1²Ë8Ë$±%—²Ë ²Íæ<Ë <É#du³¸ÁçYH:^6mßFmž—l·ÏU¾!2ùæ6Þ"w™Â-µ¶ 1ç˜Ü±‹Î÷x†Øì–ø/x<’`‘™kÆ[Íòó=&e—˜aÆsb8sâó›yΣÝî£yàÎó<ÉmLJ¯ 燃Xö‹å·¤Å“ˆ^"-Á„T³¨å³Ë=†A'D˜âLto¶Ñ…ãælžŸu—0YgÓ¬G³{¬ã,ã$l’Ë,²I,,ˆÌI$ñd ϰINeòóÄó ³™ëó·Å±¹Ìâsž,Žæ2@å„áÃÞó6DähY;eœ ±’=ÆæðÛáîù¼ÃÏ‘âxã­°z„‘ HÛ/:ÚOÌìÞ2ó½ÉÔ‹|ÙoS„YmœlPo;ÃÉ!Nñžfp³¸ãe–YaxàÀDZÀc¹ÏÄ’H•]`ƒÏô,n2÷ÿ.É{5;½ d¬Ÿ.Ùå­öÞPÓŒ$1å$’³Œø²K q½–X’Ycds[=}Æóo3Äd†LgÜ´|ÎÁxlô1j6™Ïf [Ù†÷a2Ã!#3&ðÈ^9—)c+ÃÛžLîu²Éã–câøXyŽô—ÄõÎ ²Ý8wyå <äzONú XÇHÆVø‰ al@ä0yˆiÔO*qÆ3,ɇƒ0Å“xŒæëÃÚñÙ–^ æ<–p‰æWêxÇ:—c–ù©–3"ßEÙgžsIÖ‰dÒÈ1±Ö}Í%ˆ‰f‹$0#DßÚÍ`,¼½ØK^0ažŒd¦Ú6Ã*–¶¶±5e½Œ¼ÙL´µXOI–U†vß6’ØÙÒókÍm…ÜÛmæz|o3Ðú™o<ç3«˜Ùo<81yÞsÔï£9¼mðA ¾gÞbÉ÷áÍóÝxL8[³Ì½‡-îƒ|ÛÏŸ œ×o•âØð²FHË^ -¾eãc5&Ï6^ËaŒ%Ž››yb8u%Kô<''ÝËÉåO„©'”A—…M²teËGà ¤žDÉ_‚vÜ0¶¸âÂlþ'Ĉ¦_iºmå8¦X'MÃå«RjÁ#bû5÷ÖÖÞ[ÆùÞ°ÛK«l0œo˜›ÀÛ ±ÁmÚ@°Ú[l&Ï`–RG$H¤Àe¢² v×ÇÉ:3âBI#ÂIîÈÁgš_äЕwÐ%lûy¾ˆ|ÑxY:ÏÚÙ;â+é<$üªI÷ÎØ¹úÛñyªù˜Ü…¬ÉV;Ç›Q…¶Ø\™ª[."ÓŰÊpÆkÓ[ÆÛ{¯g  ›i".޶–Úd6› °ø:°ø‡…·ˆ/o9±àæð‰bLïžñ5,H<ÉÁ{ðL>/ aÔ'F9¶ä°”¾XzpŸ<Þ®X‡ÉM†Ë1„„ÙM‰|a'HæÛ…åoO´M†Vø¶%–1#ñn°kºäHó|&¤ê@Jdº[ E¨ãì@|–¶}ÉeBXd7Ä&ó˜Ùò °È]Ži9j¶ômðóg›h$åáÝœ÷²~ Þ0”øJyÄÛí'â· 9{ðgìåRç”Ó¿kçÉ—€ðCƒÏiVO1£kyµ¶×-ZÛmîŸ@ åγ6Ý»ÎÛÀȇŒÌí˜0vÌ$3 ƒ‘üç‹|ó"Ñ‹Ìc ä<ÈØ^y±?\%-KÊKy滽m¾8­£ £klùme„´ 'φJ{ý)ò¨_I<&Ûkfa2xVLo“ <ŸË—†ß&1±±±²Ö×o6¶¶¶ÚÛkœV£–áÛ‡jÞËÝc¡åffEˆ6d@´Ý,–Æ"Ël¬Ç Þi'1ždîØn¼ƒâ29óxŒß6¶,˜B`ñ³–NìUmv,ð‹bù$‰%øXàr<xýŒöŽÆ$mŒÒp˜y·›=­îwm׋ã8o7Šsâ÷|6ÂÎKļà^aµží±¨ÛÅ·Ö†Ùx¿"~Ra–¼¯  Ø‘èB•.»c!Ë)SÒµHØÞy©m«m¶ ¸Ñ®ð;v­Zä\ªð‚r…çÍ ’œ -¸Ù6á1;mñÆ[$rò“ˆÄdèÆ·Ì#;Â"KsÁÍs'Œå|i/^j|Ìø#†·‚äÛÅï)·± « ––»³í99<ßnco4¶mæ!ǵ»mç\œyðY®YŽu}m²Îuá,ðçÏIÉö·Å¶Ãl¶óeôj6ùœêÛ69¯6ÛowÏ6Û{âs`,,,_g&YüËOƒŽo%‹6fŽ\³–¾¦žW‚#Í>€ŒróœòÚñ¶²Ø„Zµ* ¢â`$}’„ŠùópM¥¢ÂHl¡¡¬ˆØ'sp 0`Zæ0Â|°O‚Ö`X¨2UW<’I³<$ ¡…G/6>å¯ÀnpxËä°¼>wH~{Ò/%wÐäÇ&3íÇŠ%¢Dùo„ô¾ûÍ´ŒCk’¹æx¿Á¼máœÛHM–xúWН6Ûx¶­¯6Ü3» 9Ãožo^m¾†'‹ã‹ÛfÛa›x{ÅãƒÆa·» ²óÆ/‹L´æa!c5#ƒ¼ú4ñ"͉r¥dýÉÛÙÞ{7йOHÔŠvÛ|µ‘öÁÂÞÛy6[)(ûô„=Ëf`[MÉe¸!œÎ™_f-𬶥¢B™*àŸpYp±}‰öȆRøcÚ"׌L¯‹}<}ùœËæY÷™üe•Û[ÌNzŒÙ@æJsåâ²–óÌÞg[fÛe†{¼mÕ|g§ÆsxsÎCâ|&ˤe8Æå­¼{ðsañÇmè±ëžFóy¶ØÞ/ÛÈÛo7›Íµy¶Ûm­¶ÃÌñ-¤0œEì8H[/ƒ‰%þ'ž“šIµ.kÍ™Áó›~,ׂÁœx5½àù%¼¶¦ •â‡)3!Œî~Eå–,AÛ;ºXÛèl&)¹Ĩ7å¾|œÞc¶qódŒ—…·Ò’0Ì£o›M‘ž6»Ì玲™-§W›Åî³y}d¾t­³x—šíãÕ·žo›m‡§Eµ¼ø¶Ùyðóeâ÷b[\·›Œ2ïlsX÷@wÅây¿yô ¥¾•^8‘<¯m¬y-îËm¶Ç5¶ß=ù´ K >›b§?7’Ë8“õ›Á4Ëð2~V¸}q³ö§/ û•—OʭΖòÑì–d‘x5/‰³Q¿L®ÂÌ`‰c†ìŒ¼à÷]¶ÛLy¶ñc-Öò§^)¼óÖcÛmΞµ¶W¬Âç],¶ß ç=Jóxú-9¼×{¦_'ã͆Ó0¼õ·³¶õNøËg:Þaâ¹nNðžè/Ÿ@¶Ç¤Þxçôg =>9ðzuÆÙôd†ÿË/@›+i[mçǛͬ°œÃ›7‹ËCÙ?8Ÿ¡y±fžQ_.–LVxŸ$û¸_L¾¡Ö#1ìñ–_ñÛ8çÃÁáÖy¤íæñ¼Lµà2Ãë^o¡xÚ[|D¼ÖÛm–Þí¶ÊÃÁðÞ3z6|$ÛoKx[ o6»Íôì[k½3e·‚wmyä'ÚG¢N ãŽz©§//ÙK:¼}íæsÆ$ékd<Î䯩<}õëí9èÞì6¹ã7øá'žaŸÊCm½Ó¼Þ(¯£Ng|CoÞ;m·Ï5‹Èñç•ôùáéKÄXJgVÏáמ,ƒžyç`8Ç1 ²e玛±ï?eñ8Ç£æ'e:_pܱ†'ŽÈy³â''†ß]Û ãň‚|¼/œá¥â-' 'Ç6øô¹Å âß‚ÔøáÿœÓž }×Ûž=:ìéo™¶Óg»Ïžhžžm½¶Ó¾mž‡á·¤9oðm¶Í²ìsͶðÎ wæ lîÛÃfSÄZG¤¼ZXY;°ibÁÁæóxNAnô8mkyî7Ï #'ÙÞgÊñí›_6Ï Ó‡> 0½Ïi:ÞÝò¶™Ãr[Ç0²3y±.l·Çû6ÚìO6Örн8ÞN,k gtN¼Û|ú6IžkÄálË2Û;Ã';²Ç¡´Sl>m~+ÏÇx>d2ùàÎzu—¾{–ÉêÒ&—c}xw ¼ï£=§^‡ô%Œp:_%XçLæãˆ¼ï§Ä[è0²86p/+¥ŒõÿYåÙ %â0b¢H0°øo:LÎNÉÆ_ÞÎ9€çÁxËå¶Ûy³¹m¯y§£[Ï›xĵâÞlc{>†ÎKÅïÇ6ùãÝþ4õÓ`ƒCSÖMæØßFóÍç¾6}%œó‘1Ìô|ÙÀu9–ú±¾yâož="ñ©xôC‡2 Î8ú„^#¹âØ÷Ü㙑¹Ìl 0l·/5Þ$xq¾gÞ×ä•“Äá9kÌ,à¸ÞIwŒKäóÀ7ž ÈÞ“Æ<2½ñyçÊñx/6ßAÍe'žvSëÂ^ì–ó{ðú|Þ9±ÍæóOâó>­Þj3bsdôùô œÏV> 8'Ôž,ŽiŸÁ ñ'žç²69‘á¾D‡¢q<óä‚ÈË8ó;–Bs|eã`r؃͓gÎ÷|óÀá;l ó¦1€ÙòΖ»<ø%—‘¿óx·Ì¦Ëß¶Ðçͱ2»—†Hã)Å–óÅá;ÍŽ¶yàóm·›àµ›9³Œï¡}=[môèñKy¶ú6Ü}[y¶ÙÈç‰õy¶•áíd^-´„‘cè ÏpôždÎø9†Øp9–YÐŒËç3Ìùxp8wÇNìOâV×=Ç"÷,àc›xK Eõo•ëy€Þ¼,,!g–Ó%Éãf‹²ùm^üÒ2-î:z âwÊXç2s»=6}ÓÑá—ã¥} Ý’gsÒ¼`™ßàØŸFñឃ›ë8sy¼}z÷9æÓx^-þî6°Î^v=Ý8Yܳ¹e‰yØðóN^|œy–YÌæ‘,áoŒîZÇ‚Îäœ8ž£yœÞg6Ø÷-ÇÔ ^÷‹;ž;åxYg|úFÈ/›Ì^Ia[ͦBÛ™'™†móÆ[܄͒'~2ó(F,¥RfÛÇ£YšéÝ!¼6õ8±™æùð–ÁæÙËÏ êÞo‹ Ç›y¶xú¶^/¡{¾¥õ ±üqŸ{Ï£lô«sžlæpÍàúÌd^l²!—XD¼o2Âód¶s 9ŽLk3ÅœøƒH=#ó,r89œ<O§‡³Hƒ¤ûñ¾<7Ãp°OF™œYÔÀÆ)°L¹oÕ´–YòÎXwyƒ ˜ÊóÈçüÉ—ŒÛyŸ<ùÞ“ïèw¡°–×®^vH²IãìÁ3íã­¼{¼ÒÙô0[ê^o¤ßC¡á§3™èÇ=YÈàÚž€ç‹: ð±²Nd/“£,ôäA®õÛYT‚[aañ’’²Ëãðy8g©•OW’SÑñ<Þ›ëñy„“Îzøôczð@pèw9–p#ßÑŒmŽ@ú>;YÀñ¶A%ñË#™è"Ξ€Žç±’ ’uñÀ7ŽoŸ1íÀo6¼Á̵ :K¶ZÚÄl7—FIeÆßÁâÖP0„dvVGo!6WxÉRÆlàËëoÌæËÇlõosÐ÷Ï^>”fñ>†2màñ8úImžÉê̼Æb>‚l‡Nz1çœæw‹7‚ç¨àp„„‘p²;æ3-è_9̼6w"G7žl qÛ•xKk¶ÞwŒá‡g/˜M¼¬‘:¼PoäÍßhò-ådâ2Êñ^á·Ž|Ùm¶ðÝæ·‹%–GwÑã'o>·láˆÞ|áÂ^<ÛÆz2Â’xñ9¼Dgy£Å3ƒæWž%ë¼wÕ¸úOPzüYÜæs$‚O^…–O0x†9—³ÀÙeœ ²Â áwóÄvy†A>=<=Ï/6ñ‘܃ŒÛm¬¶“ÍöIY‹Q¾w=9Ų8zן$óâÙ]âL7îuÉõüž¢|ÇÂ=$XÙfžœç˜X%¼ío2YßêNøâMœg™%ž9ŽÎä’HÙÆ&$ƒœ$8XláÞžaˆxYÄÁ's¡¬ŒôÇdÐ=1|Æeï'΄d#¶=ñ™e–YÀVÏFz‰–M‘ç¿>òp=Î6i&!ñÆYlw˜k»!‹3#Äñ³œÆßðÉ%ËaÇ̼ æØ˜}ß›|ó9„óff žh·Ž6I;Ä%æY%ç8Xe†YÌãyæs<Þ9d 8ûó ²Îåšð=9èu²gŠó6ñ—ÏQïùçÇ’>Œ_K‡OfޖƼÉÝ‚BÞ€’Ë#Ú6Ï6Xi%–xƒÍ„Yy’Ï;?q žgc;>Ò6óÏ3xïÌ‘“˜[ÂÞ6KÅc…à"Ìœô+Ó,ã^w8ç¡ËÄæÌå¦ÃèÓ™Ï<ñ=É$’N¸Ìo0Ë=ìÙeK8YYíÈ<Øp,æA̘ö³¹e’1ágˆ9……ñÏž€¼ÁÌ8Μù‚F=¸mœ3:ÃÔsæ7d à‚·æ6sCÐó,âYg„#šmž6|JÛã®Û/Ž$xœÞb6Y2J /¾I Ì_6Nôa¶ØæóÆùm²ÉKÆóߘ<,¾Á¼|1‘«Ç‹èfòÍñkÏ6I>Ö¤õ³­Ñ–Ae…ž,‚ÁdæÙÜ,‰‚Í0ƒ¬ ,ผՖY­–YÌd²K,ƒ™Ìá2e‘äŸf=69g,à06@äy’K,²Ã $€°ƒ€XY'1È$¾=ËÄ|_6Nã §ø™™EŸ$¹‘¡Äz“xO@|2Ñ1ú²D’Èp²Éô›¼ ‰¼ôq¼$Øí·Œé×<õ[tc­–óİy²Î'£8É3–Ỷ±×†x„áÃ,€³Œ²ÆÇÐtÉ]a¶Y|pÎg3™̱àHm…–;&@Ie–XÙeœ1³Õa%ñd w,€¾#``±Û ‚È ö²Ìlæ<É,³,óc­®ÁÌF¢ó “YÛÎN`Y1á¼%’_ ÇrÉ÷æ-ãVÎg2t&óaðO–·lY,gH Ì¥¾yçšÇ6ÂX¶Ð‰^|Ú°>¶ ­áHg¤^,æyœÎãÌã›Iâ 'x'1°æFy³™–ó&ùæHe„AÃ3dòcd ² ÖD²l,Ë9ŒLlA$ ‘&Ae–YdXI #ÐadžÈ žl² $‚É5ø²ÂȲO7’ÃÞȵ°ï‹'3¹Ì{ò“d–pŸg{™yƒÏ]³Äp3Õ¶ø¼l¶Iœø¼ÙÖÛeÒÜ7ËèÉ-Ó˜ñòE±°^,Žp³¹6I%›œ 1€‚I<ÙeœÇ™ÂÂ.Ég8œÄ‚ÎYæÈ/99«%Q¶%’Ys,½Û,³Å–YæK,±³Ä–A0IeYKÆÚÌÛ|[{ùxêI%– `¶6Xlñ ÃO9Ì”²N+a'ƒäœŽ|ß0p“™¯mœx<ùô¾ž¦Ã™ãѼß29&NgŸAe—Ž`8s2' rÁl²È/0YeœÃ€Y’ACÄŒI–YIžl² ² ²H–YcdA%,…–AÆYæË,áë ƒÐ ³æË8 ,² Ë ,lã!Ìl±±Éã66O¼œó¼dO{,BÅIŒ’“¹‚ÚHYa›g›ÉŸ ÜA9â߆UîÛ>×Ìž“›â%æFóÝŒ÷"rÓxñ””|ø2}_MŽû7Ÿ®6¶ÌËè=û¼ñm°?sxxx£j¼Ã4ÎY™²È,9Yg3ed¹ðAæÂÉ–p<Ée—ºÈ<ÈYb@¶aí‘峌AdAÀrùH,‚ÎqÄl’Óc–NeÄÂI“Ó‡£,²I ™$ñ’x³™##²Y8Þ ó13d!dær!2s͇MȼpNe¡xµÝ¶ßá'†B¨` mmm˜6Ë ­¶Ë-²Ûm²“m£aõgÈYô_q¿ ý;ì3õïË}&~¿#fËüMŸOЛ?A0Ë!–Úb¥Ì6ñmâ:vš}Šý×î¹~ }«Éíi}Ä?¦Û VÄTL‰ŒL‚HŒË:èLášãÉ‚ÉE–A&^My²"0YæÔ‹ÆY!xË0²K,VÆË'K/އ™Ùž0p>É<Ù²%ãŒYf9eƒ',lÒ}—‰òÙg>W}=$ø%8¯sVzÒ,‘Rlâ!è`™æõçÅžfN;d¯5çŽyÎ/ã»nxmeêóerÙXmvÛey­³“lä NHH}Dýø'Ù8p{SõpCñ4C>,> ÅŸIü[61áàÑè•ûÒ~âü—ÔÇÔàón‘÷à¶ñ¤}…ú„»îF>åú–ýˆ~öÑWˆÉ‹,òFŠU¦Î2x$Œxlãï9ÆÉ'l²Âd³ˆÁ̲H gÉ™;âÇl³Íž;¼ÛaæÂqÜâİË×§<ñbFú2¢O§,82ÂH,óe’YœK$lâ@IÌóÌ“,²Ë5€’K$ƒˆØìžl±žc³ÆÉôg¡ŸA˜$’ɲ|²6L–N6L’Iã„,‡,Éc#yûKömû3ö¯Ñ?bú•úoÙbú¡ ¼B@üß¼}ŒQE–‹‘Î+QÄ!¯4sŽÊ[;’HÈd–I`Ì%–ø $óa–YežfÉ'‰$ó$6IÔ¼Tîg¼Ya“lg9á:æ^=!>Ðp ‘H -¼ä䌉,ãd–IæÈ > ’BK$ƒ‰eHó$Û 0²H,ñÀHë²Y%›#cgÌ–I%’ó ö‚fªY$šA2q ±’É&g‰'ˆ$’I$$žé,²I$’I9–IbÉ,Ë,‚ã'l³á²È,± ,lw8Fó³³¹j|³¿007=@ˆ2ÆÃ‰dX$ÖÀ³†÷0’lñœ8lCl’IxX¼Zp|Ã7Œ¶_a8ØYœ°³ºg ¼ï3ˆûO–Ë:ì«$²zž,Û,’Ë,“æN%Y„f‹$‚Ë$XÙÎgrË,’½x°ýPƒ¾o‰ÉðL¼|²É쉳ǿY\˜ë”Ìâ'êËκÎe’Id„’I…žVMy’Yc%†Y¬„–Xs;YfaÜã»ÄâÞωaF`œ™ÇW‚ì9é|Y'ã›ÄÖN§›2Âw,8…œò¼|¦Î2s$±¼ñƒ„Âe焼zÿ†Ãžy—ËÓq,8E’w “¹èNgRË$ðñƒÊq ›$óFső̎_bK$1jf­ùòJ­uĆÇ-x6óa 9§åì<Ê1µ…†Å°a+Ý!÷{^C>‹lœ=äƒT‰‚´k7МDÕ©·ž&x·Ëœ2I¬–IãÏS‰×‹9dÉ3ÆIY,™’K,‘²I,&I’É$âM“%’IfË$,g8–u`‘·ì‹•böÄÏ\žÁ²,ÎÔ‡œÝ?úY…`Î)]-C’6Ëãý/0Ô¯·Û….¦ ÓiX3ÿ?ù´óGŒølQm… L{Þ†Ÿc™âN‰„ïãÿE–r<°–]žm)l¾dT Áj |ÝÐÿûy¡®'ÿQ̲`’l“Ì–IÔ²É$’É5’É$’BKæIg2N»Äëĵž3Õ”áö=Cö ¨õc H gˆ$d²À:Ù'‰,’„–Ic#$ä&FÏ3³'ÌŸ‰'Î#Ðc¼ÞçOoOç‹ÎÇ ƒÅ‘¼,‚÷ÙÕ÷–̓ðXpö!RÂÀâ1bŒÆ÷q–hj.ŠÑ¹DL Ï/[2°¼üJü!|Mó¿Ÿé}ƒnßcïmºã Ir|&VùK`øòŸ‰ŸÉµŒ¿$Ŷhˆ£7¥k>$GØ7§÷mÛf7²øþ„²2^‚OwåÁüó¬´D!V/Æôß;ßx‹^ðµó0KxýÂkÿkôã5ÂKó7ú‹÷J}ÝéÚäÄß=_fyÁ?ÏÜ3‘Þ÷|l‹m¥_œø½”MuGô¼l1Låì|–ÈÃÝ„lºŒ’§Ôƒ;fú ÅÐϹgëToÿ±r|¸ñ¿|vDN¢¸ŒRþ݇—<ìT7Ñ–I³d’q³Y,dž$–I'É&É&I<Ï3_Bx²È;àæXq'ggg8°aÇd$±†ólŽf¼Ö/9$XXÙjÔ+ÝŒûbž­à03èD>†Ÿæu|ª²²ôê°_ù naþ&¤~ c„³ŠGè <-5šöãÆÑì>O·‹ü1 0" žq›C³é>d1ß'çÎË·7Ê/g¶hÏ-e^?VxñŽEä¯`½‡ =Á‚ûè ^Z/º"‡´ÈªŠ¿*«,ÒÎiº1gἃ…Ÿ²N |%¤ë#îöŒÏ° $ò68¹œÃ¨1¼Û¥h¼ÌÏ‘y‘ñ¿ÀöÉSøã·é}›s³ËûÆb6KÁÿ–ÆyñÇ}óæâóüH¨p<§ÞLµcà å&‹^ Á˜– Ǻ÷ÿâË}ÃæœN$œI,d“‰%›'‰,™ <$„‚É$œÉ$ãd–XYܾ,™gŽÉÖfó,È„ð‡ÊÃÉ x‚ɆÂFFHÇ’Ë,d°‘,ë ¾XˆÉñ55o›ã8váÇy–p÷Ï@9è 4ƒ¿7³Äy–O”¡m¿žd¼õ_wõ_”5m³ý¯ýCOúx„ÇÁ$‡ýÄ'“û¸þ¡©Gý©}?Ùx(ú÷Ð-öâÿ+ ²!ÈŒp‹2g’/g µ|/«X!Ããm÷Ë"2×cÇ„% 4ÑFNïáM/fÀÙ×É ÿQ쿽2Rù`CΓg>²R×'¶¬¼†_ÀÅMˆW`»xdŸ§RðYá9%½Æ 5DI,™îŸ)8FK$²É$³Y $“„’ÆI1,ÖY!e’6FÎÙēޒqI&mÉÙŸy™lkól’É °Å8äs,“™6i6=Î%œI Éãgž ó`²ÞüÏB|@ÉèÏFsuèI•1Å÷ˆ)…eãïý‹ÿR±ðÿS~é°ð¯é¾Çý7›Ëþ›ìwí¾õ’ǶïÛ}Šý7ï¿o0¡Îr|cþæ?ìû¤\&·ýÇ#>¬T@qû§áÇ')û¿ºüÿKkï1÷‡ý§ù¸qòUöÿEµd?ô‘÷?©ÿFü¿¸ø ÿ¹¿Ýßï¯÷„¿·ô¯öÄ»Ÿý…àßþâÒüD~R{´“ìH}’d†{>RyfL‚w5ŒÝí˜ðc^Lc©„lðbq†Id“™䳈u™ŸCdû²Äü x¯t/d2òY¥–YO] „&HÉñd’H‡² >==Ù/1c’AãžyŽAÀ²Ë6Í`ô’"Ià—cÌ þ < ð¾Á}³ðÿDüŸÔ_gô'çþ—8ÿÒ'åæ–¾ôûeþ¢~Hûz~OÙÖ5ùGééûžóFÄ5öŸ›Ô…»û–ÿxäKü¥ú·ý û#JPÿÕL‡ç«òÑßÍ0<Ýì_ö§çþÍŸ…ÿ{/+þÓò &ü_Üý_îûGúÿ»ýä}séiŲ¯«Žœ±ÇŒþÆ×Çö_î›ýô|.y/Ï~+ðÏöOÂ?YÄ~Ccî{/Ý<žc/±ZùW—Ê~g'ïlf¯Ñ:¡äÑÚâý²¬ý²³ÄÍ"ÌÅ%-4–I|Û'Õw¾>Ðp‚I'‹®æÌ6lòs=^j×ã,±²Ë,±²Î2Ë>¤X±õcêÇÕ—âhý)ú7ßõ'åþ¤§¿ô%ýä·¼5ðѬÔhWß«/çþÜcÿ}¿ÙúÛþSûÏûÿR¾Ûô־ܬ¿79•ô)N¥?ý úq/zýÙ™zûk¾ønÿ[x|×ßÞc!Œ;÷·õ)ö”úm%-‹…ŸGø^x&œŽ‘%‡îI#c³d‡ â 8ü@¾Òq&I’udŸp<3¡±íè,îKe¶z|G ÊÇ=¾Â÷!›û‚†YçyÏ£†Ïñþ¤d–YcdYe–Y̲Ë,²Ë,²Ë,³Œã8Ë,z=`'ÓM}-;÷o•K辊ã\.ÜÇ‚ÚOÕŸ©ð_Š~”ýos?ôóÿA7?õü¿ÒOÿꟛúçæ¶§å¦' ¦Dût'ÎQ6 ð¥´ù¼Ð˜CMfËù¬ ‰oÝ*÷ãŸ/„ñ&çÌà220Ûˉ ñ6Ye’Ye–YÓ,²Ë,þµÑ^–¬²ÇŒ•eœÈ,l³™e–Ye“Áˆá3Þú~ÜIç9Ù­c1&LÄLDLø¼#ç¤6ò?0ø£³ÍÌ\Œl“9ă8ž‡zç6I8×±ŒŒ %o-˜ðØ, ‚3‡Fø‡‡´GJ}±û­¾ÿéo³ú'Þ,½çÙ~ßþî_ß8{ÏÛ_lýÓþоÞ+ø<ú²ÿR‰~¼=þÉ»Ã0þýóîýÖ,Áío×ýÿeâýÛ}<Ÿÿ@„öþ´'´‚û\O íÿþoÿ,7·õããþ¤ß—ÅÿÝ~ ?e£éÛ}iix´²Îø³¹gsמœ²Ë ,²Ë,,,X³ß=@„ˆŸ’¼Öß÷в½ày‰‡Y!¶sÅ–'¡6kÔ:œld²d™ó@ Œ8ÂÃfYÌóê!¶ðy’ {ãf“§ =Èx‹á£á£…Ň IÎQö±AÿeŸì_quúü«çú³ò\|”gægæ«ï©ùlû¿¥??ô/º?tb|éÙ>Úßy‡îÓòv2ÿ½Í¯õØÿÐcôÿD?í£óaúXúÿª}?ÇÃýxøgõßë|p1ö>£âþÊú) Ê[é™ð™ú¹a¿ÙÛéøiõFJ~¼ýH}Ô ïbþÜ>Ëúô3Ãzû¿±¾ßïo³ÿ»}ŸÚ¾ÚýŸÜ¾ú}Ü7î¾~Øû‚PmÓÞ=E§é¯ý”¤xÿGþˆ}ЃúqUôD|3kí0ÞÒÃ{UõÌA·Ê¡Ç„§ú8C\s"tÉg^%ŒFËo ŸCQ“U$™ãÆóyâñ^l6pæóÝ6ùàÌú3Ç^šò¦ ƒý\€0è`>áQMQ}#è_E ÔáöC{øBÓ⵸j³ò,ý9¼þO|kú!üª_磉›üGÜÉûÏÖ|,k˜ïÕÞ¿õ›é6ývò_§Ä}«òoÊy·#ñGÃ3sŸ\î5=&DWé¿TýÏ@÷ðßQ¿,röžXüÇùϺ˜µŒWÙ¡ywâ”óˆúwç·$絟«rÌŸ©~ Xú³õbÇÍ‹bÅ‹,سfL™6lØ‘&D™lä‰1¸ávíwÕ¤çvåZÎ7ÆíÛ…,äVáBµjßç|n]¾ÆdÆ*G!˜93eþ€xC1‰ œÇ2É6l,$ñ=v3Ñ—™žä¿ ɬìÆA¥ãbùA;Ž„d¬ AdpØÆ ¼Íaìø¯tO뛨þÓl¾ã<ú‰^]öî|ÿ°…è|äqëߟG ŽHGðzGÈŸáÜ0ÃÓ̱cêÍ‹Vbã,ì‹cdXç`³Æ&¦y¼)—ÎûîÖvU©r¸U¦\«R¾¥JµjÕ«vþ¥Û·kx×Õ¹ïÞÅ\ùڷʦ33^³ÚL–?r#¾ö!ã!㯩’O6ÆËã‰âɼ̖lÙ>ó&¼KD¾ìƒšXÁâ #8<}øYÒàqÉØ îYA%ãOñœŒ‚'üg i4IŸ‰†^3têX|ŸkÏãd’qËÄ—ÇŽ'M”ã¶aÖxžg„FdCؼς,³yàÞvÍ‚Ãcxp"ÈŽø|^o?ü$`”¢ö">ÍqÎBí|9;ÁƒÚs÷ÿ-bøúà/(ö!çÊØD[xŸéH¨³·óç=¤Š/©å¿D=CÖŇgÑ1è ñÓ‡ŸL{ºˆà„GÕþdI›æÙú“‹~¬31æÅ«,}X,}HdX‘#,|’,É“Â,س’,X‘Æ:æc3õ&L×’dä™)<‹Œ =³†‚M’ÿ6‚ØI“Ú^É‚l$žyâ¼É<¶I%„’Y!d“áµ™'$a8^," b"B,†ßY`sâçÁt:yÈæYòýÞ]‚:SÜG~O)Ø£"bm‰â1øyM¤,ìÙB>б™™á°üñöÈ^†sõb ‹6gŽ,õϳfÌ6,X±bÅ‹,m«6E‹,68Åì˜LO¤ x>\3^€©‡zCR­o*µ*TÍ[N3vïÉi™¹Ynku.Ñ*b¦*W å|0VqöLÁ5-%â$È$dË;ø²x–XÉÏ;¯É‘²d±™,ð<±o›ÂCf«ÑŽaäDu‚È,²2ÏGŠG§ï¡Žÿ‚ðHïìï;—›í΋ÅåññøƒõêÔ«üyÿ ülýzüy?à_ÿ´ôywüwo¹Çv­r»ríd)v­Ëmò¬•nVʵÃVåÚ•k†¶_%òv—*\Öþ¥ÚµõkêU¯©r¥Úµ*\¿©üÚ•?ŽU+%OænåÎGÄ¿©s¶8½óΉ– ÁRÙKÚCÄ6_3¼})>–øÉâYg›,À³du'$vyþNé‘y=Zlp²,s„AÃxn^ c,2$žœžñ>8ê|6Ÿ2(Æ{@ú¿Â1ñ…ˆ?P8 $ó›{æÌ›6~ zk,X±`#êÅ‹V8ÅŽõbÇ cëÖú,سgœ¶,Y±gÓøôÐþ9?7øOâÿ ügóŒþoñžoœß´þ{ÿŒßñçügòOæj~¦¥dýrö“ðù¼¾&5ÛËçËô&§êF{OÇ B’&OdOмŸ¸F>#¬îu çŽç¡'Ò“<É,'ˆ¶e–0 ŽgMèYÌbË/;e‚#ÁÀzAÀàyŒÈ"_’[CËÒBÁ_æhS þ+QÈED‡ýÅþâ>?쾯ìû‹ý±÷þ•÷¥þî>ì/§û ýÑ}?ÜC-ø ò/¹gÞÉ“1Å‹ ,X±3lÁ“ÆlXÛbÅ›,Ù³bͱÆxÍ›=Ã&Í›6$X±bÄ@Í›2}õ›?\§êL›>€Æ.j§ñ3üdyñ5øçVÿŒþfÿŒþeÍn>¬ÃŒî!> ¿,Û,ø"×ûcíx Yc“ï³<Ì8¼OG„ë–=o:ñ–ß+2JO3yüÆm3¯rØx/>Ny…³‡2"ù±‚ ³Ï<-éÿ½Ìs0¶—ù/§æÿí÷ÿe?îaôÜsèþ¨|_×úÞ8>?è_ëþßêÿþß_ô§ÑýXf#ëý™ô?ï>§ýçþÕ>‘ýáðÔúj}úŸ±ÿ¡ýŠ/¢oª«ú'Óýwëª^ÿø)îOûÛôÚüSß_þ·åÿ¶þ_Göïö~_éçNY’oýw¤Å,ˆ¯ÆoÕÖn/«Ó~P_ûkù¹ÿ¯#ÿa}ßÿºß$~Ïîßgö¯³…—ÙwÙgÙwßgÛ7ß7ß7ÛwÝÛwÝv,û`ÇßûúöÿNÿOÝKç?ËžN‡îß¿ñ¥øÏqOÏ~~šü7àŸ£~úóÜ”žÀøŸ€Œ&ïTyO–~ï$.¾O¦³×Ã[¿ ’ŸÊü7䟡?R~­—Æ~â+üwå? ï‡ä¯³úËýI?ôËý3œ:y¡¦pós$¨±ÿ±ÿòj¾øš§åäAöÄÝ}ÿÑŸú\ÿÑï7™ºG¤àú‰LßëšÙŸÞÿÍ¿Lý úßÛØ+yµ³&[ü¨ˆóÿ5íðBø“‰o¡Û8–uïËnqãïxŸyÉ^%&6Äú-#Ú83Íôs9ŽÁ½ë¶@A‘z'†×à¡í>Ä{oÎgø2OèËÎØñ,ãÌ;gYôd‡2N¥“$’lñÝ™6I$™êHI)9$Ï™BI–Y˜q’I™›Þ`øûÿˆß,tA¼m› KflæyS¾¼´ãÝž+&’ZGãl¡ÀŽxzd3½ b2ÈBˆÂ ²#b:nÀ&1³cxÍÏçË&FÏçx“éù“¯¬ñž3œz†Ë2Ëlj!²™i)Å–Rs&x̤ÌûKæd6e™‰hí«ÿäñ˜’Xë?ˆCÆñ&3¼lï†Klç6mñ8Þ‹c.™{žzó$’wŲóä²ocÔê«!­²ÐFk¤^c6Ë,ày‡ÑœÆƒX"8@ì¾€µ dƒÈŠ /-?o©?<>ŒüÐ8Ÿú_–~(}h¹~ ÿ©¿ô‹?÷6ÿ¶ß÷Ûþûß?û6N²ýwë¿uú¯Õ~«ô_³Óˆˆÿ,TQWc=BÙë€N`x™þªê/õúËýEþˆŸ¿ýÞÏú_£ú_ª~éúcR>Ÿ×ÿÙüÿ¯ÿ³º¶ê««ý·ÿÛý÷ÿÛý·ÿØÿ³ÿûºÿûÙ)ÿ¶_ì×ú†ÿXßë'ú þ²ì·þû?îçýþæ¸§Ž€ÿ„ $‰S§é&}<ˆ4£ñ£óþéþç?îŸO÷ÿ„¯éÍ…Çþf^áñl_0hUf_kÏíd‡Î<ó,Û93Ï<^óŵék+³¯Y‘`b§šæ1œÃb#¤pôðG†Ædnp‚ †o<¬G I|e(ñ«G˜?ì›ýó¶o»ûÿ²cþÁ}ŸÜÇýÊ=óûQðzÿfûÕþýûûÄØ#þÉ÷/öÈÿ¼_ìÿtå>íGÃ/÷ëýÚÿfïôÿz9˼_ï\û×¢’7Ñè>‹=ó¸\8Ã1MÌ`öÿeÊ}Tÿe÷×û¨ÿ²¿ÚÇý•þË×Þ·lzN\~Ïé~¿é~Ïéë òOÅü$öÚ?Hý+ôÖ?ü#òËÐÄ¿³øáDJÿéWþý¿ß¼—þÝ¿Ý8ÏöKñOÃýÄøéã>ª™–IlÁÎ=vÓ‹èR[o†RYÞm¥àg7®“2óÝ›TAóÀé™ÀÇ™¤1–ð"*,/Ѓ,Ö ‚0Li±Á³æá.& …õÖÅßÔ~aÃŽe Ÿ¨0w2Éñ~Æ<½­}CÏ¿¨[íkêÕ»vË}SÓ6l}Xú³õ?ˆüX˜Ïk03?WøOâþ¡'µ¯¨û…¿«\ëê?7쟡?‰ü_@˜~/ñ¼>'ñþµõ?‹\³<ß0ñö¿Ã—è•‹Wè•õ{s9ÏÕþ¾­}_á?‰òöµõ)ó’¾™_R¢¾­ýZúŸ–[ú—õ?‰Dù{Zú•õ*WŸ¾¥}Kú–|K—ð2åË•)ß²ûKõ/Ìo¨x=O3b3ÌžcÜãÏ;aÇŠ2ÌÆK ÷!ȶŒ ÄpéÁ" ²È9„{ÁæÈ,„`²jb æç!ƒžÐ'Ä9e¿Æ!Fîs€Á¿?R~¬}_\üGæß'0ÅgÇ‹0~ ülýAúe‚Xà0lýq³ò@ú²|ãœg‚ÿ¹ø Ùú‚üI³õgê Ÿ‚LÏÔþy׳ðXú€Yú±õgêÇÕŸ«6>¬gµ¨?V"wÚÏÑcæfbÏÔþ D}X™ú“õ'êOÉ?RÏÕ—âü$ýOæFYú坿ýÄý$ÅGå11õc‡ñ„òHÏkÝí"?’D}â?B>Ð:aÔ¯[ey¾'»Ò[z¾0ô9Ïü$ó –Õ‚6%>◎)oVò[6wxï~3ž'&ó2¼ÛÎÎΗ› •Ÿ™ÐáàVÕì,³[ÆýA°±‹#cy‚È8s ‚²<Á@@Áæ `l‚ù‚m‹AÀˆ8^=ð‚#8‘ÂÎdt=YǨð÷?áó½zú“¯[ž–ÏC6ñÌz¼} Ä{³÷RÎ<}÷Ѽs›>Y’ñ,ÓêGÌyà|ø³„Ïo+`07ÆÁd² ÎÇ’Ïm¼s,ˆ‚È2Àxo x ‚<°tˆ9–AЃ…ç§ë:pÿƒ–yî)ügó§ÔÛ†fl}99½d³ž¯£Ìž/;'O&qƒÌ´Zããg†A™aÂ8@ÙEšÀ%yÞdAYA…pàAAÐ àpàz‡ðœ88p'Ó‘ÿ?þg³oClñmô>†[N&zËÇÐñèß3ï!éñ|¶>Ÿ,¶ñËÇõyãK|ùèk³Ñk„³5fÁ Ò ƒÌ–0AÃ#"1°áB67„@@ð/0Y,Ž£Ð[¹eæøƒÔ.ðwÓ¸Üó3Å—«m³Æóüeó׌ÎÏ¥ô$ž9¾=IèË9–wfÉ ÅœˆA%ž·#260e–)bË#„Cd¤,îAldpÀáÃ88YDg@ÇŽÁÀƒ§2"ÈÎç£à¼ïüSþCëz¾{¶Ûl¼}+=fÞ2[×Ððô¶ÖVm‚Kàž›6rÞ|'©Ëgl›ÎltCÊNðCXÌ0^V8p:GLÎaà ô AdC°z~‘è&=­ÈáyoIIÿǽٖ}¡“«ÕæË/¥xñ—Òóz,÷Ç¥ë3èSÒ¼o2Ë/,,ó9¤ó?‡bÉËOFpîÞ`²Þ‘9±î±{xý¢µûðÛ'“†%–ú1ƒÎYç¡À!"`Ž ;ƒéŽAlAcþR8{páÂ=üÿñï¥ô¼} Å›më<ÙæÛÝ—ø7Òõ÷þ,ôœG‰hÙyˆÎ!èÓ‰9Ü`&<“ÚüM×Ú“Ìž„Aç8Ãac6y‚6¼E€G"ùŒÏ3À‹`Ž Ó‘›ÂÌcc„= `²›:t:p—?„þþ üc…zñOBñmãÍãÅæÚz«<Ü™xôç›{âîÞ6Ë=MâØÞlß39c±íÍ/àEôoØ<03̾8‘±Ã¢ eá|ä^ÌFmG7†_/аÀˆàœ" ôzâ=Gÿ(ñxͳo^>…—‹Å–^ìñoßBúð3õéÑr$æNYÌãeŒÁdÞï+ïÒÔ¶Ù:=Åà9v2ÆóYÌ8g@CÝ`Ò6,æA' â8<8Dzˆ}tõñNoðŸÀÿ×о•ôí³Æeãݾz¼ÞíóÕôú†I#=gVÛ JY)o‰Æ=íê)d|ÃEª9“¹lp]¾o1º¬fÞÎsÌŒˆWa²Ë°xŒàp8sHc¸ÇvˆòÁÒ#§¤ôò·ÿ€gмS¶Í¼_K/£}Ýôm¼ÛyæÕ·îú7™Ác·ĵ¶Íë{ž”—˜s,-‡è—V7ïžîqKÌ@ì48Lg[o˜8Á&œ#˜Ä^9–FDA͈h"8DCÈá?€=ñoñïüÅæóeâñâͲÛ-¼^-²ñ[Læóx°ËèÞï^¾K}9i‘ü £Åâs,çž›Ä¶Ãæ5xÊäy¾d†&‰„ÌcÁç·qØ#v³ÀáÂ8t#¤Ds ˆ¶ß6‰xݶVÒ[x&sÏ,DóÁxα)/ /vZ~NÏn%ŽÞv6vX´ç˜‹ßöA <"¦c°Dt"!ñ ù"øŽÁl1è!‡¤táè?œæïño£e–Ùm–Ye–^-¼^m²óe·Ï‹|¶Ûl<^ïWŧ4æ“m¼_<ù—¾!ùÒÛã‹ÂÛy¶ÎÛ3Íe–ØÌàÚ±jÑþ †oIJðÛbñØ_|ØÆ[Ýôlsv-‡£è!†ØxDFFd{ô·Ó¸Bå°Ûl0ÃoFóÁîÇóoðïwѼÛ} ovÛe–[m–Ye¶Yz²Û-¶Ûl¶ä‰eê÷{½[m—›ow› n[k¼[aãm·‹m°Ûm¾%m¶Ye!ñoÀ̼øx¼mþ*ð­¸Ûâd9ä6!—[B!0·S»f’mx6øÞÍv "sbñÁàÛa‡¦ÛÁ†y°ÛÛãm°óa¶ yœ;½Øþ-¶Ûm·Ñ¼ß^Ëm²Ûm²Ëm¤²ÛÍ%–[m–Te¶Ym·…–[y¶Ú<Ùm6z/vÞíâÛy¶ÛæÙÞ ­¶õ¥¼%m2K^iêYóxÉ8ï†ú%p¹A²ºÌL岚sb †a‹|-›HË[maæÃÁÁ„·› <Ù[ 6ù¶a¶2Ò d>aæÃ 6Û °Û[oÛa¶Ûaôí¶Û-¶ÿ&óa¶Ûm¶Ùe¶[fm³̲ÌÙe–Ùm˜Í–^Ù|["Ùe–Vl½^m¾•·‡>Þ§ž%-¿NÆ'ëH8‚ÿ{YýZûÿ¸ç?7öÍ S~9“òV-Ü|ó*}ñöÉò;ï<$ŽÚ䄸ËÁ8$-â"xm°»lJÃ{-Ò¾ a¶K[^lCâÛOØàí¶¹ -¬¯ ¼m¶am‡màÃl<Øa¶Øm¶Øx–Ûo‹mîÛn¶Ûm¾m¶l¶Ëm¶ÛooÛe–b8Û"M™?eø/Á?JOÜOÖøÏן£:foÉ?B~Q?^~ø'éOÓŸ¯?Z~¬Í»6ì~ˆÉƒgî‘ùž;m¼÷è¿.ÓÉS ½¥û¯ÞlËèsò9‰øéøéJ2žT.ÖÞ2Ÿ—†?*¿v~Ö^XÝgVûÖåàÀ+jÁo73cwŒBãmÞîh|0°Hq´Œˆ‰|ÙÂØÞdB–p9œ,`x¹ç¤FÇ¿O ´¶80Âl$$'Ù ö@û,o”²ù#ê_ŠÃÜFÞØö3~óö!“?qBü÷ç¿çð/Á~iúWæ¿7obõÒòO¯‰ú_¥ù/Ö~òýËõ'îŸdŸ¼¿BýÉûIûø?%âÚÓõŸ¶aþr~ç÷•û7Ù¿[öíþ–žcöß´ý÷Þa§Ï&í×}ÓˆŠý·íŸ¿?~~ü¶5§ÎÛÙÏίSí.ý“ö/¹Z¾Uâò¥û-çæ6'ÄÜE|ffžk·¶ó{ß#ñ<ì}Ö Ëgγ©›Áƒ\FúFCñn|S\o»õçÌ“í¾lÇÕ¾£h{l!³ó‰tq{^ ;ìZ”p ^J—¼<²Ñ»ŒýÓîìãî±æ6Ö×-¶ÿ´Ä§ÞcòÀùLýH/qÈ+‘û|GÞߤ[?†>¶üYø§çë’ ý®?5ü-|Íý_áÁùGÑ;fw!à~Ü¿y?›÷¿[éoÚý勵VýïÖý/Þ~ë÷â~Kûðiólü¼¯¢0ý3÷o×~›Ã›¿\ýûôß¶ý³÷§ïß®~Õö;æÝ῾ý|£¾\ï›¶}ñðW?×Þ©ó¿l}øùåï¿E÷+h_kgKù^‘Ó9Ã!<1‘Ã,ɱfM‹0&xX ÛxxyWŽá cÆ|û“v­÷;&—nÞnÕ®‡Ð¨d¨RáK Vô•Ï,<|ÛP¶Vݵj²ÛkØy¬.m¶ãÍâù¼äóx3„d>6| ™CKe{ƒ÷Æ>÷ØÇ¸Í{Cïk7e}¡…÷{=æývO°Åÿ;>óg “qŸw‰ °ÙDªÙ&'#ÎYâñxˆe-7šZØWa$ä‚Ç 0ÄÏîNÿ6iÓ»w¶öòážñÏ£ÎeÞ÷W…n]»s4ZµkZØR¸U­«.ÛYYV¹jÔµ¶ÖÖÞ5K[à·Í¾m†^6ÖWca¶Õgy­½óo‹Ìl<Üg¹ãšç<TƒÓ²úNÄ{LïøŒ†Þ©è[âз­Â-ái„Äç˜Î/‰y³—Ák)Ós×®í¥¼$¼smŒàðm–3‹¼ó°Ã>¼0½ømãÀ”çžg fló3‡3‹yëÄs†_3½xõáüz63¿ ÷îôégðo<[Ó¥¬îCÁ·ø7œÏFúwѶwK9³.–sÊp,ële¼%raño6ØÛN9¾9…§^|õ~ Lál ãlâéÀÖóB/;oH÷îúöù_–ׇ±Çšå»-ïàã™ÏÎ3NyãéÉ÷ôœßCÃ/éÞoñgwã¡îðm–Û}[ãÄ‘êw»èûñÎd>æsxÇ67›kÏ0³yçç•Æ!Ku¶Ûf-·k·ËÝsâ7›ç-Þ_’ß÷$ÊóÎay±-ðNp猋ÆF’XóÇ£Î[…âxx„Nç£æùèFwÃyáǹ28fùy¾cׯ4No ¶y­°Yè}ûçç›ÄæñâËl§àƒ}>mîó}ê}½xðôe¬Ù†Ù ðÏ£Ï6 8ämÿ†Í7a¼÷æzNm›ß›ÁäôwežyÈ´#[ÏÝøeB`´â› »Áp:™àx·Õ¾KÅãÕ¿ÁìEâ‰g3«óÅàYe‚zN1Ç=o“º€ÛÆÞø³yç™^='þ>úKÅãø<^yã{ã§Eôo‹^ 2X}[×¹=Ymg l•¼ä)Ñž7Ç7›…ñaͼâÚJÌ>2xðͳ án[‚[ÏŸP;tæÊ/I5ž@yâñ’6ðxìC)7™²–¹ @ú&Þg‹/3kóÆÇ›k%æóÇ&#Ë' ¶Æ9¤±Ü¼ä–[ÏŽçž“ž|Cnð8ø$¶ó’œó—îp‹1õ'NzÌôièžäzBÙßVzFóI²=~/;Ó‡£82™»Ñô|Û0qBÏ<%oŽäÙÁ—»cbK,ìK̂޲Lf™y°Èé¼ùÜæ™xàF%œËz¶$yU’Âɼš[âÕ -¾I¶v%²méèßáñ¼xG‡Žì¤Ï w^o{gÚ¥à“­¦BDæÞF,ƒ×šÈó9ž§¹ã»Ýœéño£xy¥óÏùþsø³øÞx˜m†{½m—Nr6ð²6wovx¥§£Æe‘ï%¹Ç„ûÚs ݼÀïçŸËæ—Š—ƒyàÛ!k¼xí¨ß;/vЇ¢ú|[{½m¾V'¶^zÛyc¼Ó¤„åñÔ{œOož+–÷|Ïwy–ãÌî’óÏrÇ=9ü;ÓѼV9¼ñ±yô‰Þo7«&xzFU°ô1¾­êqgl‹&}'‡Ð›,rÄ,¼ô,l[ÍŽ{Y cfú>×?/µáÜKÙ¼0œÈ̶ þHBÙâ¼FÁ¼Aß0ƒo‹z<,Œâq;æÇrÏ7˜ðz|¶ô²Îùâï7ã ŒÙ1ã|Ûg¡çŽyz¯<úX0½­e¾8®z|ìœ-æwÉòièùæ¾µç‹ÎúMçÁóÿ=>ll±±Û[í~/É¿6<¸¯Å¿Uô+âÕç÷ZxÜiš¿Eäó0!¯Ê8&vÜO‰r7‡žð¢‚)ÑÉÛ!OðDŸi-ØTú—Ø#Ù °õ`¾bÕàäoàÎÐÚ'èÇÙ@ß›ì†_1Žª>ä@9ã‚ Œ›ø5ð˦i”YÚ5òwÇé9cxôy²ÌõdÛyãÑ|X|Àg£9¼ó¼Û/Üî3χmâóI‰fvY8…àé’ózlúË8Y# ÈßÓ éà}WãûUø½Èø§›Ú=Œxln1LGšðGŸBµÆ¢zïâ HðàF9 •äL_–×ím4<Þ'aCfGÌYÝâ!ù#ä ƒy> ƒb¸Iž`óÚZÂEÆù&äBÂCƒRv„¿U§ä¼>D›™¿äåñ ytÇ×¼õñˆÅ×ÄuÍ€ù“"Ø5²ø!užÊ~òý£ô/Ü“ùŒŸ’á'ÀKŸˆf{ûÌ)nc4wRϾ[ßæÙlÄÞ,çÆ‘ÌâèkÃÐâÈg’N—ƒ™Æø²wÇãÌæ6;cbØÚËQ•kàa}6ÑDx- ËðŽÇ™èìö3‰e Ñ¥»váòAòˆ É£êï~QõÅæ^àÎþehøð:Yx¼^H²°ñ’d·Áy ÐÄi&È’‡‚N@7p,… ¯ ¬åÄiv{ ÷„9ò%ú/,~HøcÀŒß1–O¸YýãædcI'Iƒ¤M‡2ÁLìÞ˜†Z!Îú¸ÏÊZ=òÞîý¼¿|ý“‡—?#—sSöïÓ+íhžYóð­ç••¹±–dÚZZXO —ÜO†ã-Ýà÷ÏÛ¿Tý‰ù”ûš·öVïv,û0õo0`6!9°¶ù–Æ×/2Ãæ„„½¡ÃaâijElY³,å˜Æ,lp E„!;Ly ,€D<¬Ù±ƒaø³òHcŒ@bÄaXlÈXXHF‹ÅâÒ22e¤¤acdA_,ò¶ÌKÁ›xýÆ(û$Ñ2>‹"ÉÎsnsG<Óxƒgi9"Ùà«V§°´ó¶óûß¿Þúçéâ;Ϻ;n£ìO¸+âTýÖO•¼Þëo†åÛÛP¸Rd+P†U¬· øyO–íÚÛ\n\… × ÊðÞéY ­®M¶Âì*}Ëܲ,LOÏÂÁâ9k"OÒÄ;?Rü—Ô#êOÒŸ”_šü×â™q³oÒ9b’µo“ÏÈÚ…ôÂGfc~ê<ýÑÓßæøˆêãx€Ùa2D·Ì¨´ÛKl%¼cîÄAùcOrNøz±‡ÂkÉG.Àc)6DšNAò’vkÌô¬v¶ÞC–ôݼš]¹à5úp×Ü8V¥rÕ¶Ý«V­Ú·o%Û·nU¾w7v¸ÑjÖÕ«YVÚʶ¹o7›æ^ž Uþ/ó{‹Õ2¿ü8l,×gpm¾mà’ÂîËxâü^V›ÍñyæËÌ<[ç9œ2ß‹€ ¶ ?F¹ÀBÖOµîOã“çIJ—|©ûñ)Cݾå~¬ó"ß,”èÔµË|ó|^-Ý bf‘÷—Þ'èG·¤¢G»¥„Éyþ#ˆ«r!òhHïºX$UË·nVd“ ³åârråú<)WƒÞûÔ­©X[[^•·Å®6¼ÖÛâe¼óm…¶Ø•¾<½Ö#ÞßóÃlµ‡Ô÷môø—¯¼zž{¥âñÁ,ËÍ–õ87ÄO‚Sg |p²Å¹ÇÙp5¶ð$„jÈ›ö›|óÍà9âÞøY[gaÎx´là‘ ·Ãܵáî—Ìo6-wƒ¶3¯<$Š{Èv²u¶±‚ÑàOP¶/‰ç7Œ'Ü}o!:kexXYoü侤ܛ¾ååYyU7[cÝ{±÷/‹YûYöµ*Ãm|ÓuîÇ´÷+KÍççwÕœØc3n½óÑã<ßFú¶[|ôôo èJpŸBÍ›9–¹œToŸV÷-Ž“6úüïy±Íç•âØ6cã¬'<ôeXˆh}¦¨PR×mĆ$gNмÇ/#Äç›O Éô¶øc,Ô}‹æ}‹÷G4òù9~Ä›ç/É/C1ðÍŸ¥äC8ÌŸ4ÓË<©û÷êÞï—6ga&þVŸÂŸ‚<Á×Ìú„ýyúS†ã#0±cdØ]ãQ³ko#ky‚š`6 [YÍâBà ±7~eûFpa(Ùˆ6Uc–a„L ÀQn‰à‘™D§¢ÌiøE›\Ÿ!ÑèÝ&äæçW¯7ÔúOžð)ψ}!lszBNEãÑñãÑ…—ŽkdzŒï‹_VwÆÖXÊ™–³Ð5À¼´€Ë&~[|›aí~¬™š˜Œ0Í?‡±{ô |&üÐG‹ÌŽ]¼™p{^;Ã<Çõ& ù3òŒ¾ìdß³xW轇Q¯¾ý·ÕOßl¤}ÕæÍF¾|·îƒhMœœ:ÅýÙLË |ðù¼ Dâqö,T¶#LÕ¹­cãïñ”?+2óÙWç™O™û—ÚïÙ}*T”{çP1†â=x©CD`Ìó>ÂÌS¶1ñy6Pì,#Vñ²¨5t† ç};m–¹Íâúæx·¿H;âyñÆÓÐôæ¼O¶HíçlFÆÆÆÆH$d‘Û,ÂÎ<½Ù,°°ÞxŒ”Œ´ÛIKw0N5±fMŒ³éåí•ÑvíÛ·m´Ûáàµ[3ÃÃü[éRÓö_ªó{¡½•åö‡ø…y0A&‘E|ÀOÙ(÷gâYù˜äx /’N¾ /‘|á²pô§ë^O2ÌÀ]21À[b,òÉŸd´U •0¯âN²‡!æ†i*QG µKÂ2²yo“!Io™(G‘›—¨Î¼ß"Æ #ò9–øœep’Ø /ÃFgrGg\²‘ô Iñ«`£ ‘í¨”rÏ,.{¡Þ5–ù}ج–$ Œ7o+c÷6Ig0Ï —ç‹Åáœ-!ñhÍàD l…™,ñyûíÛž{ˆ«ròÕ«PíZûÚÚÛ1Yôlú·Î?Æü'êoÆ>¦û‰úû^-Šy<<ÂÖÛ!ûläap4Œ/Âü#O"ðî"¼œÂ7ÜMŸQ:x‡Ô^sÅâ<@‡!”¾}º”±“‘q—› ø![(cQ¶-™œ€}ãC„}Ü?%Ÿ7›ôµsxŒã7ãÖ}䌙ùQ÷/ž‘õ#äíÔ¶Ð,—°Ì·ñ8 gâÕö•ûÏ{Ùïï<Ôíçïƒ<=>é,Ž ½Ü§y ³b6šöè~¿;Ïíùòü#Ïâü!óÄW‚De‘ÛHd†@HÛ ÆÁ$ÎxË7¬{4–ÿÙfwupd-2.0.10/contrib/qubes/doc/img/heads_success.jpg000066400000000000000000002172211501337203100223350ustar00rootroot00000000000000ÿØÿâ øICC_PROFILE èmntrRGB XYZ Ù$acspöÖÓ-)ø=Þ¯òU®xBúäʃ9 descDybXYZÀbTRCÔ dmdd àˆgXYZ hgTRCÔ lumi |meas $bkpt ´rXYZ ÈrTRCÔ tech Ü vued è‡wtpt pcprt „7chad ¼,descsRGB IEC61966-2-1 black scaledXYZ $ „¶Ïcurv #(-27;@EJOTY^chmrw|†‹•šŸ¤©®²·¼ÁÆËÐÕÛàåëðöû %+28>ELRY`gnu|ƒ‹’š¡©±¹ÁÉÑÙáéòú &/8AKT]gqz„Ž˜¢¬¶ÁËÕàëõ !-8COZfr~Š–¢®ºÇÓàìù -;HUcq~Œš¨¶ÄÓáðþ +:IXgw†–¦µÅÕåö'7HYj{Œ¯ÀÑãõ+=Oat†™¬¿Òåø 2FZn‚–ª¾Òçû  % : O d y ¤ º Ï å û  ' = T j ˜ ® Å Ü ó " 9 Q i € ˜ ° È á ù  * C \ u Ž § À Ù ó & @ Z t Ž © Ã Þ ø.Id›¶Òî %A^z–³Ïì &Ca~›¹×õ1OmŒªÉè&Ed„£Ãã#Ccƒ¤Åå'Ij‹­Îð4Vx›½à&Il²ÖúAe‰®Ò÷@eНÕú Ek‘·Ý*QwžÅì;cвÚ*R{£ÌõGp™Ãì@j”¾é>i”¿ê  A l ˜ Ä ð!!H!u!¡!Î!û"'"U"‚"¯"Ý# #8#f#”#Â#ð$$M$|$«$Ú% %8%h%—%Ç%÷&'&W&‡&·&è''I'z'«'Ü( (?(q(¢(Ô))8)k))Ð**5*h*›*Ï++6+i++Ñ,,9,n,¢,×- -A-v-«-á..L.‚.·.î/$/Z/‘/Ç/þ050l0¤0Û11J1‚1º1ò2*2c2›2Ô3 3F33¸3ñ4+4e4ž4Ø55M5‡5Â5ý676r6®6é7$7`7œ7×88P8Œ8È99B99¼9ù:6:t:²:ï;-;k;ª;è<' >`> >à?!?a?¢?â@#@d@¦@çA)AjA¬AîB0BrBµB÷C:C}CÀDDGDŠDÎEEUEšEÞF"FgF«FðG5G{GÀHHKH‘H×IIcI©IðJ7J}JÄK KSKšKâL*LrLºMMJM“MÜN%NnN·OOIO“OÝP'PqP»QQPQ›QæR1R|RÇSS_SªSöTBTTÛU(UuUÂVV\V©V÷WDW’WàX/X}XËYYiY¸ZZVZ¦Zõ[E[•[å\5\†\Ö]']x]É^^l^½__a_³``W`ª`üaOa¢aõbIbœbðcCc—cëd@d”dée=e’eçf=f’fèg=g“géh?h–hìiCišiñjHjŸj÷kOk§kÿlWl¯mm`m¹nnknÄooxoÑp+p†pàq:q•qðrKr¦ss]s¸ttptÌu(u…uáv>v›vøwVw³xxnxÌy*y‰yçzFz¥{{c{Â|!||á}A}¡~~b~Â#„å€G€¨ kÍ‚0‚’‚ôƒWƒº„„€„ã…G…«††r†×‡;‡ŸˆˆiˆÎ‰3‰™‰þŠdŠÊ‹0‹–‹üŒcŒÊ1˜ÿŽfŽÎ6žnÖ‘?‘¨’’z’ã“M“¶” ”Š”ô•_•É–4–Ÿ— —u—à˜L˜¸™$™™üšhšÕ›B›¯œœ‰œ÷dÒž@ž®ŸŸ‹Ÿú i Ø¡G¡¶¢&¢–££v£æ¤V¤Ç¥8¥©¦¦‹¦ý§n§à¨R¨Ä©7©©ªª««u«é¬\¬Ð­D­¸®-®¡¯¯‹°°u°ê±`±Ö²K²Â³8³®´%´œµµŠ¶¶y¶ð·h·à¸Y¸Ñ¹J¹Âº;ºµ».»§¼!¼›½½¾ ¾„¾ÿ¿z¿õÀpÀìÁgÁãÂ_ÂÛÃXÃÔÄQÄÎÅKÅÈÆFÆÃÇAÇ¿È=ȼÉ:ɹÊ8Ê·Ë6˶Ì5̵Í5͵Î6ζÏ7ϸÐ9кÑ<ѾÒ?ÒÁÓDÓÆÔIÔËÕNÕÑÖUÖØ×\×àØdØèÙlÙñÚvÚûÛ€ÜÜŠÝÝ–ÞÞ¢ß)߯à6à½áDáÌâSâÛãcãëäsäü儿 æ–çç©è2è¼éFéÐê[êåëpëûì†ííœî(î´ï@ïÌðXðåñrñÿòŒóó§ô4ôÂõPõÞömöû÷Šøø¨ù8ùÇúWúçûwüü˜ý)ýºþKþÜÿmÿÿdesc.IEC 61966-2-1 Default RGB Colour Space - sRGBXYZ b™·…ÚXYZ PmeasXYZ 3¤XYZ o¢8õsig CRT desc-Reference Viewing Condition in IEC 61966-2-1XYZ öÖÓ-textCopyright International Color Consortium, 2009sf32 Dßÿÿó&”ýÿÿû¡ÿÿý¢ÛÀuÿàJFIFÿÛC    ++&.%#%.&D5//5DNB>BN_UU_wqwœœÑÿÛC    ++&.%#%.&D5//5DNB>BN_UU_wqwœœÑÿÂÆ"ÿÄÿÄÿÚ ø±((%€h(€(” XX¡ E (  E‰PT   ‚¬ ¤Á`,–Y ‰PPªQ(P€¶) P @…Ae`eAPT¹APTAPTäTd[‘¦F™di‘©¤äi‘©™¥di‘nF’ˆ[APY¢iAPTAPP(() K!PP, €(”!PT  `,*‚©T¢DP `P@°YDXP”%W+xâœÃ…ÏÎ8ã®ì¼ì£¬ìŽ«´:®Ðê»C¨íêí«´:®Èê»Pë;#¬ìî컜p9Ç]Øw`u݉qÀç‡ ˜p¹‡ ”q9KÄänBq¹"᱆ᖆZ„Q¡APTA©¹di‘¦F™`m¶ئØ`m¶ïäqŽGåœc‘ÇNG9`nâdi‘»©¤diAP[…@AR’‚P@(ÁeXP@@%€€㦀‹B‚!(–Q@‚À¨P¨,, BÀ K–¢(€Jˆ¢( Ú$ÐËB4362ÐÌØÃq¹œnBñ¹˜p¹‡ ˜p¹‡ ž.aÀçtp9Çœp9ǘp¹‡ ˜p¹‡ ”q¹ä†heDP!PTdiŠjàmˆr^!Êâ/+Ž®"r¸©Éx‡+ˆ¼®!Êâ®!ÊãŽ*œŽ1»Æ6ÁvÀÛ#LŽÒ^˜RP@( , °@€BÀ ,¡  ‚€  ¨(Jˆ X(P*  ‚€R( ”$ÐËC- ¶0ØÃc”p¹‡ ˜p9Çœuï8ë»ë;#­;CªìÕê»#­;Dê»CªìŽ´í«´®«²^«²6Ž˜¨* ‚ € ,, `BÀ‚À@¡ °*YH ”X*TXJ•B@*PP” X( P!H "ÀXJe%”‹ ,,±J ¬€#®lB,* °@ˆ €Q ˆBÀBÀ,@Tˆ€* X*P‚ ©@@VP$ajPhÍ”5 ”!@H(”`PJ%  €(BÊ ë°,`"ËPH¨,BÀ€!@ ¨„5" %¥¥X ©@Š”P ”@°((€@€%(HR( ,,°!(”¨°@"%í’ ,,B¢, b  € `‚À€€‚À   @(eP P(”` RPePbP¢(¢‹€ ޹K R€B¥ˆ@BP@ ¢"¬PYH @* T( %UR(Š*"€Š%,H € H bR¢¡(ˆˆ°@*¨ é‘ ‚¢* ¨‚À¨€ ¨ K@R( `°€P"À%PIe”…P ”¥XX, B€‚€ (””%P,€‹*À€€ #¦l) •`  €BÁV ”€(€(€ %BÀ(J%@¡eY@PX”"€"€€I@,` «€ % %”"ˆ¢,Í ,,@A,°€7(‰` `@P H@D,K ˆJ"À°, €P””PPH €PRP°ÈU”b€H¢°E‚E P*%H RŠ%Š!H°‹¡,A(”’ÀA, Ô° €H „%,À,ˆ(€K(¢(”"’(¥€¤”( ”%VP€(²€(K @([HP( ¢(”¤´‚‚ˆ¡€,ˆJK°K*(È"[ °XP"À°"ÀRªR‹J  ¢,¤ ¢(Ѝ¢(R((” ”(”%¢(€‚¬Š•E!H¢Y@ˆ (‹TJ‰@ R(‹Hˆ°(Šˆ,¦l‹ªÊˆ°  P,°@P M@¢(KR(Ѝ°Š"ˆªŠ"€À(¢(”"€(Š% €PR(‚"‰@P%"¦…š‰T)E„Њ"’(UX°J@"ÂJ"Á›(BMe“P‚ذBÂP„R,°J¨°J"ˆªŠ"‚ª)"ˆ¢*…"ˆ ¢((Í¢(”"€X¡) X ¢(ŠH¢(Š%"‰@¢(Š¥²‰TÒ`  ¨…"ˆ¢"‰(ÊÊ€‹‘,D²¢‰*(â‚„!R„@(„‚€ ED) QBÔ” QjEDQQ@©J‘me¤EDQ‘DjADQ-TQ( EDZf©EDQKDQ´E, ¤*аPTJ¤PQ@E D–™”@%%‰F@Î¥IDQ%€pʼnP°%°bˆJI`©@"€BˆªŠ°J¨¢(Š©T•H¤Š"ª(ŠH´Š¨´Š%R(Š"Ó-¢-2ÐËB-2ЋI4#C- 4#C- Ú"ŒÚ"ˆÐÃB(ŠX´ÊÒ(Š"‚¢U%RjjYT•`¢*–‰,%£4$Ô"Ê“P$ªÊÂ,"Â(Ͱ“P‹:âP@P€B*%€ € ”@  P”ˆ¤”ÑH¤Š‹Ê(‹(R-"‚©(Š#RÉhŠ%£6ˆÐÍ¢4"ˆ¢-2Ñ#Tʈҳh#- Ý+6Ó-#- Ûk-#- ´2ГUr¢M 43h“BM¥Š"ˆ¢REX•eŠHµr¢,“- ¨’ÂKk $£-JËC C* "Œ¨êØÍ¨ ²ˆ€@R,PŠ @PH¢R€Š¢ˆ¢*È¢(•j-"‚ˆ¢4³6ˆ´ËB43m2ÒÌÚ #C- ÛL´3t3h Ú3m$ÐËB-2Њ#C-£6ˆÔ#C-IcC-C7PËC6ŒµLZX£6‰5L­"Ø%j!H¢(ŠŒ¨‹ª’‰,"«+¢K°‹ 4$£-dMB(飥¡"€BØ–P%”… Je@ "€"j-2 ²…H¢*¢Ó-´‹HªM(–’*—P‹LÚ3h‹S6Ó-´ËB-\´H¶²ÐËB41t2Ð Ú3¥2Õ0Ò2Ñsm2ÐÅ´ËC3eÌГTÃC-B(Љ4"ˆÔ%¥-‰h(#-B(ÊŒ­2°“R²Ô"ÂMB(ÌÐÊÁ(‹£*342£ 1@HUQ(”"Â¥I@PZŠ%‰@¢P*¢Ò*¢’(Š¡H¢4%¢Z²43m3hͶȴ͢-I5k-S-´ËB43h–ŒÝ ÛL´3uÐ͵2ГUs637LÍŒ´2Õ360ØËC--ÈËpËpËCPËQsu ´1t2Ðͨ“C6Ó6‰ZX¶3hŠ2¢J$¢MC*2¤€“C-EËC C+Lª²¢JŒ­29., (*ÁD@P%D²€ ¥(¢¤ ()(QA@XQ(M´E¶fÚfÚIªIªe¥‘i›D´F噺º¶™´fÚe¡VZ©–†m¤›hF†Z¦Z‘VZnº„la±‹¢å¡›FZi‹¤ej塆‘–†f†fˆ†Zhe¡–†Z±–„Phe¤¹jFf¢ID”e¨ARhe¨I¡…hfn;‹&‰™¤¹šhy#  ”@!e%¡,(ÀT”R… P(( P¥TŠ%ZŠ%Z‹Q4#BZ%ª6Ó6ˆÕ1t¨Ñ"Ó6Ò-²MÂ4#B(Ã-Œ´3t3t3t3m2ÐËTÅÐËTÃTËTÃTËV°ÕŒÍŒÍŒÍŒ´Y42ÐÌÚ0Ü\µ · · 7 ´2ÜŒ´2Ô%¥ÍÐËhËC-BMB,2¢M 41hÅÔ2£6Œµ ´0Ô2Ô$Ô$Ô"Â*\¨ò5HT*€Ä(€’€TR E´uµô^®§Å_¼¶| ï¡ðo¼ûÁðo»û©_ûˆ|Cí¢|Síañ²•ñ×ëÇÈ_­$úÈ|«êaó¦•óWé!󯡉à=èxor+Ú‡Žö!ä½Qå½Hy—Ñ–y÷¾:»펣¶:›ì­ì8à¼Ã…Ì8œ£È8îÆ¦¦›V[heº˜l¸mnºœÅØÃcŽìqÝ—ŽínHa±ÆØãr7$^7 ã»mÓŽrS»laº¼m£ Œ43žHg;›››jjD–˜Za¸eD”e¨eDšRÅGŠ1 %…‚€(hì}Tô:b * € „° ¤°XÁ,š2°“PÅÔ340ÐÃPÌÞI5 ÍC+ ,2°„,™+2]LääœpåœY—žp#šõ¢ö]\·NÜôáÝtßyÅô^d=G—Vù×xò=§Š=·ˆ_qá£Ü¾÷^÷^÷ž÷¯Ï~üðúÏÃègÏ |õ>âw¬îN=V¢’o$Q•jjjnšš‹xV^z*P ” (Š% @¡I@Ñ|ÿÚjz“SxŠH¢U"‰@ª‹À¢(Š"ˆ¤Êª(Š$Ô"‰42£*2ÖHÔ$Ô¬¹¶u\£…Ú뙜ÜDœ¼f]‘Õrñ™XI¨IÍÄ@fjrñ“:å8'&LÊ2°ÊÂgy&t17˜ÌÔ3åfu’K"J\ÍHÄÞW3r8æââo&f¡‰¸bjFf‹†¡%DQ E–jQ%‚X@@%ƒ“ŒzœÞggs½ËÑìÙÍfŒÛ£+£-C*0Ô2Ô\´< õ@ Q(-€²€ JV¾ëá~æÏC6o™D´EAD-EDQEDQ•YEjDQ•FZ„š†Zš‡?,‹”êö:Ý£}~Þ·g®w:Þ™ÏÕíÓƒ|„áî`ß_›'G³ÍÉ^_/6ÎO#ÕóÏ/K¸qõ»Ôó±ßä:nïÂôºÑçö;ãæù»~‘óÓÖæx²sstá¾×DwüÝNÏG'w]z½.¨ôú]zzœ~hîv|œGgÑñ²zÏ {¾gPsz>4=î/ ïy=f9þg¹Ö¬µ™s7“9Ü—pÆw#y\NL˜jFf¡™¸¸i›†T²YjD–Q™`&¡%‚XE€Ï[·]NÇ[˜ôûþóÊ«$ÔY*"Á(ÌÐÌÔ$£-›÷@D ²ÀE ’€ ’”(P~Ããþ¶ÏvY¬J!JIJUDQE ”%RE,”E–”I¨I¡•jQ&¡–†f¡&†f†f†Ta©Rj›†šœ™37“3Cä†&äbn›Êá¨qÎL˜jÎó.s±Ç415%Æy3›‹‰¡‰¸fjFfâå¤bha¨fjK™¨fjXea%‚XE‘a{\õÒæáæ=C£èo<¶¬ÌÜŒ¬#PÊ—*2 40Ðù”×=€ÊD ,P P(( B“ê¾[é¬ú5šÀ³k¬UBT¬ƒ°uÀ)Xå8P ‚™X”@% Dš„XE‚Q&²%jQ3¨I¡…„šš†VVf¡•„Ρ3©,͆f¢IK‰©Ρ†²¹Ï&Lçy\ÍHÄÔ35%Îw µ#- HÌÔ35 Ê3Is5 ¬$±$°“P’ÅK¢K}kŽºÝŽÌz‡O½¼òM,ËY3È“PÊ—*3(Ê—Öo=ԠД()B‚’…KJ"ƒè~{Þ¯«•¬Eÿ_X½ž§bâõÝ…u’^§/^³ß뫱¸àáíhëv±£«yíryÝÏ9;\^Ÿ õø»Üvu{yëÞ¸éÙ:OK«ÉÖÅÁw=…äÅãß7B½/7ÐóŽ\vGZv©ÔÏ©É;Üéž|ôógœìö+;׆k:ôO2v{ž÷z_^§•c·ÖõeòµÇÚNœæô+Èz]¨òº¾—4¾LïwìðyûóÀK¹™ìqâùs½ªó³ìðlöºç–ú.‰äϤèG‘>› ¼>wÔù)äïÑç—Çw;‡‡ÅÜô5<9íøkaY𑆲¹š†’á¨fla©.f‘…š†f²fXea™¨IrIDK$ %î§γ=®¯y}ïW¹¬njTš„•X%.TfZaaò»Æùl)@‰AeAI@RU%)@PPZ”ERU©íxÞ±öRË€;½89tÕÀÖ¸èåâÞG%âþ–F÷­ï„já\³øã‡{ÏäÉØããWkª'o­”vº–Wo‡ŠÇg§¹\¼\®$gŸˆk“þ–Goƒˆgµ×iÙââF·Ä1ÜêŽ^nš^~®×9ìpÅÇk‚îùÙ—½ÚñóþºÎÿcÇ’÷»ž.LÝΙõøü©Ï^Æ|ˆ{~NW·Òèàú¯;ÇÄ}g›ãâ>»çú|g§Ûðm}'KÉãÍæôü=j_¢ùŸ Íêñy]N¶lÍd™ÔŒÍes7#qs7 ÍÉpÖI5˜ÌÔ3äÎw“2Ã3Y$£3Y$Ô$£+²X°õ|Ÿgų]þ—¢¾kƒµ¬Y©YYFZ†V,XI¨e¨eGÈòg\¶nh%Ê, R‚‚Ê ` ´(PôüÞùöóRâP”P@ RZ€(Š"Â(Š ˆ°(€J"’J"–,"ˆ£*2¢MC-C*2£-C3PËPÃPÎw,ÄÜ168æâñ· MÃä†3´qg—'pÃYYCyŒÍår¨Â—3Cr\ÍC9ÔŒÍdÌÞLçPİÌÔ35’Md“Y"Â,$Ô–= Ýð¬æôüïPïöºýfÀʉ ¨’‹MBä98÷Ït(J‹((²€,´(²‚ UU(*“»Ôì{ MkŸ“7ϽìVùëªäæN³|ÇXU½¾,Þ»—VpÎÈë7ºárÓ‰nY‘G/e¬‡k¦i®3SX+µÔ( "ˆ°J "ˆ¢(‹¢J"Œ¬"‰ ÍBMC-C3PÌÔIJÌÔ&u çqs5 Íd“Y35#9Ô353Y‰Ã3Qs5#3EÂÈ’ŒçQs5#9Ü15“9ÞLMCPÌÖL¬$Ô3(’‰,‡7q{>7«åÙÙõ|ßZ»Üü\×3:š„š„š„”LèfQ™KñÜœ{ç½€‚eZYAAAA@±TYh´U©ÍÅÈŸ}f’³®¼ÍìqàœÝ¿7š^}ô9c——£ÊcŽ·žçVòfòN,¸ãæ®}ô9eçérqk6jjvº›Þo=ë¥ëèÞ{½=ñfú|äe2o‹ÇèWѾRY~NŸXù:}cå)õo•VùQõWåiõ7åiõ/—Yõ—§Ô>`}=ùqõ˜Nùõ–§Ô>^ŸNùôï™Lù‘ôÏ™Lù¡ô¯š§Ò>h}#æéôoœ§Ð¾zE>|}ð¾ð¿—澜ù^~Ìz~§ŸéêsU³3P’‰,¢K ,X°“Y >,c|¶Z)%––P¤((¨­ÐP´«eJRÙCPûÿ™éÉ O×ù´ãáìòW¯ÍÌ—…ÍN8àv ×v)ÖvGUÚ§QÛWMÜ9Þxtg|tñО€óÞ€óžˆó^‘<Ùé1é-ê)ë!ëìì{CÅžØñØðçº<¼<¼<¾< ôçŸB>uôCçsôƒæçÒ£æ§Ó+æN>^}HùiõCäçÖ“}b>EõÃãçØ}‘~6}œ>1öcã'Ú#âŸk‹}¡~)öƒâ§Û‰}¨ø™öÃâgÛ‰}°ø‰öé~÷øgÜ„Ÿt>}ä> }ì> ?|>? Jüú~ƒϧèPüö~…Ï_ äüú}/ÎC\<ª–žÿ‘èŸ=Üêw£Óôº=ýÎZ$šÈ”eaÌE†f¡%.V,ç¾[U(JP[(**‚‹)l¢ËVÍ"¨³Bª&‹õ·íFl¶O›ú_³—‹”úV™¹j™he¡™he¢e¡–†mme¡–†Zhe¡–†Zhe¡–†Zhe¢e¡–†Zhe¡–†m´I¡›DZe¡–‹–¢EKe¢e¢å¡–†ZFZ„Q&†Zšha¡†á™±‰±††††'$8ç,^9ÉŽrÊ✣ŠrÊrŽ' ò¾3ì~:^n.lêô÷ÉÖ¯3ÐèzG©Þé÷užIa&¡%DVX©a gåß" R‚…*(,¢Ê)BŠ‚­(Z¨³CRJ–Rý½ó¿G_Ÿú :üœ|«õ3s7-42ÐËB-2ÐËC-+6Œ´2Њ"Ó-- ´2ÐËPŠ"ˆÐËC- ´2ÐËC- ´2ÐËC- ´3¼ó\Ù½|özöIµe¡–†fÆhe¡–†Zhe¡–¤e¡–‹™±††fÆVZšjh˜h¹›mŽ6Çc²qÎAÇ9’nAó¿#õ_(¸åâæÎ¤Ö+è|ŸoçÓ‹Óó½Eô»^Þ³¼êX!`–X°…ˆE„Qñ2Î{×7=’”-, ¤ª‚€ZRÊ 5(²Òª*Š©h–Í•ëýGÉýfoføžß]^>UúÙ§;•i–„QDUE¦Zhe¢e¡–†[Vhe¡–†Zh™´EhE¦Zhe¡–†ZiYiºäÜÊ]bÔÅÒ²Ô2ÐËC- ´2ÐÌØÅÔ\´Œ´2ÐËC*2ÐÌÐËC*$ÐÊŒ´2ÐÌÐÌØÃC3pÌÜ37 ÍŒMŒM« Ããþcè~yg/"Éaõ_-õŸ"œ¾¯™ëž‡gƒŸRË ¬Á, $±d£ ø‘ÏnÇ_±`º E "Ê*Ô¡J, ¢­MJ[*5(Ô£RÙlгAG{ëþ7ìóx…—ÊõzÉásðsax{w‡ ãr;È8܃ŽîœnAÇwN7 ãr7 ã»Y†Æ§q¹q¹q¹‘\nAÆänAÆänAÆäwq9G’œ„㻜ƒ —ÉN9È8܃ÈN7 ãr#È^9Ë7 ãr9Ë7 ãr9Ë7"8܃Žr7 ãrJãr7$8܃Žr7$8ÜƒÉ 68ÛœÃcÉ NAÇ9å㯂ð}ŸMo:Yg9ô¿õ(w=o/×N÷?. D–…¨‚BüJ¹îv:ýŠªÔ”E*”•I©ERYªE ¶”KT¥-”²ŽÇÚ|GÛe„Åy½O{®žW76æº×±c¬íS¬ìÓªíTë;Eê»Tê;cªíªí³²:îÀàv]Øgdu݉\.aÂæ‡”q¹!†ÆRJ…dnñWæpw^§V·Lw/JçDwÞ|=æÈôï”=W•YäÃ×xåö0öo†=Ç…#ß|øúÏ£|Ú>•ó#éŸ._¨|¾cêß&>²üŒ>½ñåûÇCìŸ>ÍñpûWÄÃíß¹|6O»|$>ñðy>ýùü?B~{ÐßäýùÄ?H~mÒŸšÒߙәCôïãe^žî‰a;ÝV;Ÿ7îx¶wýo3Ö®ç&7b@°„XI`–,š„”@|E9îsõùë‘f¢Hh(PRŠQIl¥²‹4,Õ(]J–Í”¦¾Ûâ>ÒR¤X*R ÒQ`¶ ”X*P[@X,‚Á* €*À!Š€ €@¤°Bl°@’ÈD$±Y¹&u Ë3Y35 M#Ô3äÎwÜ1ÃÜ1ÃÜ8æòbo&3É•Æy2bn–‘=ß écÌó{jô}_7Ô³µ¬é$°K’æ‰, ‰K (€ø‹/=Î^.JçY¨²¢”(²‹(²ÙJT¢¨¡TRÒ©J”¥²Õ²–ƒéþc·/Öc±×ÌYie"Üèe ¥( I`(…%@`@‰` ,K €K°’ÂMBJ35’MBK#+ ,Yd’Œ.I,Y,35’fÂgY‰.I.Fif,2BD$¹&l3,&lYd’Ã2ÂK!öOhùÍõ»‡¥éô=ž{ êX%YU%D–ŠÄXç¶ñk¶5ªPZUE”U)j ¶i&¥-”¶Z¶RÕ#Tõ>—àûÙ¿O1ÚŽ ¸™´J¤PQ™´EDQEUEDRE€A@A`‚ˆT†™!©›U‘©‘¦Edi¦a¦æ¦Ef™˜VdjfHk9-̆¦afa¬Ì®¦a¬Ìš™‚H\ÌÆ¤ÉY‹sL–H2ÉrȈ$Šˆ#"9ο¥ëóåÉñZë›ô:¾vûýnæ³¢6aa a%DšÊñžÄ;·:Ü Qe(³EBÚjQe)Rдª¢ÕA¢ÙjqóHës^} äqKî< ÇÑ_«ô7ç©ô/Ÿ§Ð¾zŸ@ðÐ_Ÿ'Ð_ŸBùê} çÇÐߧо|}ùñôžõð)ï<{·Á§ºð‰î¼1îh}+æ‹ô¯šIô¯™/Ó>`}<ùôï—Pùh}Tùi/ÕO–‡Õ>VTùX}[å!õo”‡ÕÏ”Vù1õo“W>P}\ùH}cä¡õ¯’‡×>FW×O‘Zùõ¯‘_ÇòcÐó95\<ü½³>–{š›æšK D,!, Xˆö"öù8¹w™KQBªÅ BÒ•E B•F¥¥šQVš”¶hU&yÁŽÔ:s»%éëµN¥íÓ©{š::îèèk½£¡{Ôèk¼:Nù:7½£¡{öºý:¾:.öŽ…ïS¡{ã ïÔè;ôè;ã¢ïhè;ã ôAßz Þ§Aß§žôAß <÷¡O>z#Îz#Îz#Îz#Îz4ó^<×¥#ÍzCÍž˜ó˜ó'¨<·¨<§¨<·¨)ê.z±|§«.z°òž¬<©ëEò^¨ò^¬*zÐòg®<ŒúãÈzÐòg¯"{ò'¯&zÃÈž¼¯"zðò3ìCÈž¼<‰ëÃÈž¸ò'¯!ëCÉz£Ëߥ³£Úìræº"Ä@‹±bÂ(’¢’À‡ú%‡cŸ­ÙÞBÅ”U²ÙJQT¢­”¦ BÙª•JZ¶TU- hU-š*Ø–Ò]Õ3uHÝ3uLÝS7c7c-Ó7C6Ò62ØÍÒ3uL]+7DËc-£-S- ÛL´"Ó-¢42Ð ´2ÐËC7C-#- ´2ÐËC- ´2ÐËC- 6\6ŒMŒ68ÛmŒNHc<ƒŽrÉË9Ë9È8§,8ç(ârCÉ+Žr)Í)Ê8\°áœÐâœÐásC†sÃÍ+ºbÑX `‚Å‚XE‚Q±`AðãŸ@9;]>沦¢‚­*¢Í ª,¥²£RŠ¢ÍT´–­MR–…Ô¥¡u)j–ËR–Í J]gE²–Ê]f–¨(²”Ê,°²Š U²€T°°´  ,¨ R€°, „€€…ˆXX%‚ !b `…B `–€X%‚ ê K•€ d%‚RÅ 9ô½î‡{RÓQTš–©QT¥¥ eKe-”U¥TU¥RÙ¢UKe-š-šh¶SI¢Ù¢¦¢ÙM¥”¶QB‚ÕR¥Š)@T@–P€ e(€•°°PD°@€€@„, ,µšHB’ÅK°K"Á,ˆ(“P‹",R„öïô;ú›jj)J©54(Z KJ¤´J)VÊ5-–Ê[(Ò“R–Í ÍÍ £SCR–Í ±l¥³CR–Ê[(¡l¢ƒP[((²‚’ˆ)(())’€()J„”@ !I`%X%„š„–"RÉD–a(ETY`–,Ze`–(9ìS=îv» çyŠ©UJ¡V©ITUAJZ©¤jZY¡TV…›"ŠT¦…Q©¡Um”µ¡VhU*‹e)EX¨(()A@B€… PIe@ H"ŰKJ @@„À@@A,"Á(„D±Rˆ¢,ˆ°JY¨€JXƒöíõyëÑÏ&:f)% TU© iITQiBÐj[-¥”U4Th-•n¥- ©K¨-XjQ©Ke-”jQe (P@ !@(¥H¢,‚Q€ˆ°J  š©,äK°@KK¹‘)dÔ ’,T°,"ˆ(@Jˆ°b‚ö—‹uìcyé‰(%hššZ¥BŠQV…QZ¡F¦…”¥ TµEQUZ”¶RêhYKf¡T¥”YKehYAA@TeB€”X* R(KBˆ°B’Á, K°@€’ˆDK"Á ,bÁ, ʈ¢,‰A,—*>sØ µs®™Ê¬•F¥EZQh•F¥¥QVÅ ¢Õ&‚ë4µE TZF¥ZRÐÑKe‹TYKBÙKe‹e¥ PT$Š€…!BP %R @)J$°€’ÀA,Ä @K°K¢,! (’ˆÔ"Å ”±¥€J\ÐÊ€ö ¯ÏÕíôÆ%Y*‹4 RÒ‰J*ÒÊ*”¶TÐÔ¥(ª*Š£YÑJ[4,¥²–Ê[45ËÊ[)AlÔ,«R¥²ÒÊT¢Ê( ”JH P@J@ B ` €%ˆ–€€€ ` `–‘b€X @%KE„šRÆ‘ùåLt J=CÌõwŽ%š… )l¢•h–…-M T55BJ*Š¢©l¢ÑAtÊ*”¥*[)u›-Öt,¥²K*ª,…@KJ € ,²€…ÊP”  X@ @B `@‚" `€€€–PDX%€%FTB¨DQX©C:X¥ç£³íx>þóÇ5œ¨ššQ©ieKAf©f‘f¨Q©JQTjQf†³¡TUJ EµQ¨- ©F¥–ÜÓ@¶RØ.³Ke-”X*PP!b¨ RP(T¤±H”‚€@QÁ,ÁÁ.@!°€@@@,¤ `(‹J"–(Š R(ËQbÁ-\ËbóÁŽ€¿ ùß¡ÖYÞ7˜¢h-”U¥©EZQ-–©E BÙKBжR¨¶Q¼Ò”¶T¶RØ4 Ya©V¥5sJQe4” ¥X*Qe¥€ ”°²*P €T¡,€€„Á,A.A°%$°¨,°(€Š ,P "¢‹À*"‰(:¦:Þð}½NÆwá(YªYRÜ袭”¥EZY¢jQ©Ke-”¶R”¶QTk4¶RÙKeKe-”·:Š %[e-” ¥QAA@ ”X(*PP*Pˆ€J ,BÀfˆB !! (€K J"‰eKIe„¢PJ"À±B"ˆ…ó¡Ž€õ¼ŸJÏO¼}1•‚…²Õ²£RŠSPi*[«®ihZJ]Al¥)l¥°[)h[*[)l«l±SBÊR–çBÊ5 (²‹(ÊHP, BÊ,)@(…€@ @ˆ  `!`QDXXHQ(JDRÂÄXE.Te¡ùÀÇ@{£Ù=î.n.¼àªTY¡eRÙl·6­”¶Q©F ºÎ…”¶RÙKs¢Üè¥Í. ¶VQ¨[e‹e-” ºÍ-‚ÙJ `¶R¥Y@*P¢P¥ €T@@@KK’Ä,A.K°%,‰BPU‚(%ˆ¢,” A,T¢óŽ@9xµMǬuçKe°i¥TÒTÑE[)nt[²šJ[¬¥°i)¤¥)¦ti(ÔMH²–Áuš¶Ê[²–Ê,¥°[‚ÁR‹°RÁR€°”-BX( €@@ˆ–@ˆX… %”ˆTBP%‚Ê J‚(a@¢€(D$¢ç=`ú=qrôç˜P´²¦™¦™ÕP[)nt—Xµ«š]f–Ê[(Öint]f–Ê[šjäkY¥Öi«’–Âi¤¥°º¹¦®i«š[(²–Á¤æ–æ• R¨ ,²€°T–B€A`@!b%"!d…ˆYAb%@@$T!©H[š,!PT,XJ,BÊPT°)(K* çto·Ðô:c‰¬Ø²V™Bjå[A´¹¦’–ãVi)«Šmšjæš¹¦®)»ŠjãF®)»¸Ñ«‘»‘­e&Ù.®4[škX¦î)«‘»Šjäjæšfšdmššf•(°µ)R EARªÁPTAHTDTAPTAR!©dY"Y!YHjIU‘Y‘R!RådjAYä[¦F™ARdi‘¤[› nF™AP[”i’ê!PjBÔB!P~t'@ôýoÞÞ:ÙÞ52Š©M%-•-Í4ʵsM\Òë$Ò+wÝÅ5¬ ÜSw‘šjæ—X¦®i»fš¸¦Ùfš¸Ôjâ¦î)¶jíš[ššf®®##WÕÅM±M\°7p6Í-ÈÓ#L£LÓL2]3 °6ÀÛlC‘Ç‘Æ^G9C•ÂŽgæœ0çœçuáØu‡fu‡buò™Ö•ÚXvçR·NÇJ×@wç@wçB÷ŸAçCÒžhôža}7™#Ôypõ/•Zy0õžFOeâ£Úž,_rxp÷^ =ûóðú'ÎCé3¦¿/#ê_+¬|‘~µò9>ÁñÈûñûYñQ~Ùñ0ûwÃÃî¯ÂCï/ÁHûÙðcï_¼|ûÙðCï:âhÈrzþ½~?1g¢ó‡ óÇ£|Ñé<Ñé¼Áé¼ÊzO4zo2«Ê©ê¼‘ë_!^Åñ‡²ñ‡µ|Aí¼Qí¼H{šð‡ºð‡»|{Ï@ùñôŸ@ùñôŸ§Ð>~BùáôOFùÁôoœFùÑôSç‡Ð¾zDùáôž@ùú{ï{Ï{ÓÂG½îs×Ã_¸ûrþ~=€[ô%èëè5Îüëè’üíúóÏ£8ú!ó¯£ÑòîÇ_®**¤4Í*CWrDÓ%Ó$Ó#L25p4ÈÜÈÛl °6ÀÛl °9c‘Æ9c‘Æ9c’ñWåqWæœqyœ#™ÃN[ÀŽwçpwì:ã°ëŽÃ®;¸ì^°ìÞ¨í:£´êŽÛ¨;n í^˜î:c¸éŽã¦;®îºEîºTî^”;Ó¤Nìéÿ|ôz/:¯}Ð÷A]ùÑG}Ð'~ùë{ΈîÞ€ïÎ;®îΜ;®˜îΘîN¡;n í^;“ª^Ë«·ztíN´;S¬;½9§æœC•Ä9gäqŽGäq³JˆS+šºãGÚ¼ûåíÞÏHw!ó#×ÄBP­åzxßiÂå¾WäqŽKÄ9påqÃɇ£ˆgðª¢Ó-e Ã-B60äã   Š©A ‚ ©@B¥Ò|߱×OŸÙðùóèJõúuTã›ÀT€AR„ìðr.ZÜc-)8¹8ê&Žåë1{°ì:êáÈ%Tìõµ/eÀÍçpyÂ9œ#™Â9g°¹¹¥;øá™¼®,[ÜâÆÛ«c“—­„îδ^ÎzÛ®ÏQÆ`X   ÄQQDP”EDP”J¤QE%ÄXQEDQ@Q5 ìøéŒÅokjDP)F†hT‘¦F™¸`m·y‰ÈãŽ1ÈãŽ1Èã“äqŽG725p4Íê2Ý8Ü•xœƒ¬îõnpÔ * --ÍZ”B¥$ÔH”(‹ "ÀR(Š"ˆ¢(Š"ˆ¢(Š#C-B42ÐËTÃc Œ60ØÃcAÆä§”q9G”q9iÂæ.aÂæ.xp¹Çžw`uÝŠu’õ‘ÖviÕv‡UÚgdu¡Õv‡UÛGlu]¤u]¡ÕvÇUÚgfWjwdu¯bÀàsÃ…Ì8œ£ÉN7!xÛÕDQ(,äjAHiŠi˜mÈã¼c‘¶!Èã®*r8Ç,ã`mÆ9c’ñS’qŽYÆ4Å4Â8³¹©ÞzëMd ¨-ÈÓ#L24Í+#I !#LŠ‚³Kr4ÉtÉ-ÈÓ%Ó"¡* 25 Ó#L‹r]24ÈÓ#L3JÈÓ#L±M24ÈÓ#L24ÈÕÀÛl ²06¶ÀÛ#L£s#WlIÉx‡#Œr06ÀÛlGÝãŽ1¶Ø“l ²4ÈÓ#WH* ‚ ¨*P‚¥ @QR)bˆ¤Š"ˆU‹H¢(Šˆ°J%D®.CFV³¢Z3@°43ò8„ R( @”T€Q@    (‰@(,´$(Z (€±AÀ @n…ÐN7¡nB‚ƒÿÄ1!12P "#3@p0`ABC°ÀÿÚÿÛ%¥¥¥¥¥¥¤A¤AAxGî ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚?¡OÃ>úª ‚ ‚ ‚ ‚ ‚# ‚=®|$’I$’IýcAAAAAAAAÅ?þ³‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚?jÁAGþ’SÿÁÒ¿ý¹!K\ZâÕ-R…!HRÙ¬¦®)ñDã± L1S1S1S0Ó0Ó0Ó0Ó0Ó00ÁLÁLÀÃ^™®Ã]†³ fÌ5˜j°ÕaªÃU†« Vš5j!¨†¢ˆj!¨j†©ªj𦩬kÊk)®¦ºšê`S˜¦0©‰LN1©Æ7Ücqc‹XâÅ,RÅ-RÕ-RÕ!HR…!HR#òcù àë©\S¢‰ýÇoÁ’I$’ââââòòó!Èd2L¦S)”Êe3ŒÈfC2̆d3!™ ÈfC+L¨eC*PÊÓ+L­24ÈÓ#L24ÈÓ#L24ÈÓ#KÚ^Òö—7ú6\´i¢'é RT•%K”Jƒ_ýŠÏÓ°#‰'ßPã'Ëîl˜ÅlÙ19±àæÇ†%1(ŒUø­XøQª¾8Þ+U?Þ¤ˆ¢/¾'Gº3ÊÇôù2TxôíM;Õó1ó!HDW.!Í…Ä£˜­"êkIQL. m78kmaGÔ÷ÔEZ¨­±ÖµªåT¨¨5Žpêoo†‚¤ J¢£˜æˆÇ8ZU¤ËßUÊÔµÊwUã7çb&G¢«ÅEOqADQª'½¡Çô{£<šîîNë݃÷¥O, åR˜ÏKUn©ëUc‡Ê5V)Òò¤|³RõUPjªÓ;‰X{Z­ÿÁCΚ}E¬ûžëÖ£±µ+|¼v³ZÜ‹*VU²ŸÛ[‘´·ò*:iýŽ/ܧ÷i”JT¢¥wÜÿqÿ‘ª ž÷Æô{¢,x*Ê# ½^êîîtŽYðr¢ø1ȃ]ÔÇ;溚rCœŠÆ*"1пH{¥ncÒX)ª"«i(÷5Õk˜Ümø~:sQÈ«òUEÇM´^ˆad¹Xµ’8¬¨Œm69Ÿm”>í]/š•*)L¥ë§é¤è­]Ö3Ü¿äA¢{ßÑýf<#âî„©ÜîS§N’U¨¯w¹;Ò Ñ=ï‡èþËæ…O'½ðý?¦çSÁ ï|//ÓýU|Ä'½ð¾&²QZ©ñBø£%?žÅàí⨩ý¶ª§˜Ñ¢{ß ÏàA~VĵÔÕ²GSTNGST)§g¶ówf«!,XJCšæ¦ª*CMTZnAùUªÑ)¹G5Í1½EEhÖ§’¹)•Q<W5¥V¢/ƒŽ**^Ä—UUsíkZ>'¹ áþ9'ÆP”ñe$s\ˆŠæÃ> l½~ tÕÿ‚œJªŠ£纣ŠÊo¨®j±}¾ˆÿ1£D÷¾«àbW¼KŠˆØÄTO62Q)y½M;Ô^îïMätãO¶éÆ“Óÿ/J ¨zËé}4U—×óÙð¦Áî¹Ç˜Ž{V¿?•ˆ®šr!X£êZΚ¬¿O¶KPl_Jª½iª5®¬®J H{njx#‘”ŸRñ´™V×R–¢ºÇ7Áj&/÷Sì±.uÔo±«È±“ÇŒÁQhÓG; 5)Sj¥%¤©áN븈”µZÚ^ÛKÐï44O{áúþD/uG9s”ETïQPW¹Dì+Ü£{4G¹ %Rš½Îê‚Ôr{šdrr´sÕÂTT÷8J®A^®å¦%NÎz¸ËØÊƒ•ªeìŒV£ªöAá޵Û[“ÔL™Òi:Á® Òä°cÑêMmçÀˆªXïã…!CSI'Á£÷Ê^¿õãL—ÜæÊã-ïˆF,â«8—ÆŸ’ÔzÕp´œSB£Qª¢ÓrÅQXäñ¦ÑÍVˆÕQZ©ãc…EAå!Qj1dµËáköæ-ŸÚ¨'Ôäù'¾Sõ'§Æ˜çª+U_ê¦O~÷¾÷öUw¨¦9ÏRŸ•5Q=u}Tü‘È'¦U|i_™URš|ÌO¹ZÙU%­mEEmG«JÛQêÕqïVº¨ç;ö®/¯‘êAƒD÷Æz›èñEVDX&|28•œ%g#üQʆW¨Š¨JÎGª†GåXû†½Z+Õ|\éA*B9êã(竌œõq‘¹\¨ÿ—´ÿi╽H0hžøžtþßén7Û©êAƒDüDö„ó¥öÿKRíAÞh0o¿Pû_½¨f¦f¤f¤f¤f¦e¦e¦e¦e¦d¦d¦d¦d¦da‘…ì/a{ Ø^Âæ°¹¥Í% BP”;sò㯛F ÷î7Úø`©Y\*Á”Êe2™L¦S)”ʆT2¡• ¨eC*Zei•¦V™di‘†Fda‘†FÓ/¦_L¾™}2úeôËé—Ó/¦_L¾™{ é—0½…Ì/a{ Ú^Òö—¡z †Bó!ȦC"™ȦE28Èã#Œ2<Èó%C%C%C%C%S%S%S%S%S%c%c%c%búåõËë—×/®_\¾¹}rúåõ̕̕̕Œ•Œ•Œ•L• • • ¯2¼Êã+Œ®2¸Ê¦U2©•LŠdS"™ †C!Èd2—————I$üu»S0O~â}¿‚»ÄÌm5,icKcac tÌtÌtÌTÌTÌTŒTŒ4Œ4ŒLLLMz&½^‰¯@× kÐ5¨Ô jµZ«@Õ jÐ5(š”MJ&¥R‰©DÔ¢jQ4ét:F#N‘¥LÒ¤iS4©šL4˜i0Òi¢ÓE¦‹M4PÑCE DÑ4MDÐ4TÑSAM4h8Ðy óAæƒÍš †…C§Õ:}SB©Óêš æ…sB¹¡È4y Ñä<“K’irM.I§Ê59F§,Õåš¼³[˜ks nY¯Ë5ù†¿0×åšü³_–`å˜9fY‹”bå¹F.I’cäù:æ:å•‹kÕ-¨[P¶¡y>cæøÙÝÜ¥ùFŒ߸~ëóœO_èx94Rá “W–¢ 'ã'´p½?ýhq~ïèŽO¡~"}nZ÷AƒüdöŽ¿O¸‡ï~ˆåúWáá'ÏÉõ Á¢~2{G Í~ ¾´(ýÿèhÙ,Iú3É~—!~t4OÆOház—ÏÆ·­ úü·ÍòþƒÍáâö¡WÖуDüdöŽ­ßR ûÿÐÑNÈ"öEþ…Ì_™Dø)vã?Ôу=£‹÷àäy Ÿwð#ûÇ/Ö¢| Ú‚ù´`ŸŽžÑÇû‹ðrCÿ"y~†_.W­DñO:ý©ÿ¶ ßè}Åòñ¨ÙhîÅ7#›úÞ\ŸPž4’jr×å0OÈOg¥ëÿŸ‚­!ª&jFï Þ®oW7«Õê¦õSz©¿PߨoÔ7êõ ÷›ï7Þo¸ê7Üo¸ßq¾¦ú›êu:‚›êu:Ô u¨@êP:Ô u¨@êP:Ô u¨A ‡PC¨!Ôêu:ƒN Ó}§PiÔuA‡Pa¾Ã~™Ô)›ôÍúFý#~‘¿HߢoÑ7è›ôMú&ý~¿@ß oÐ7øæÿßã›üsŒoqî1½Æ7xÆïÝã¼cwŠnñÞ1»Æ78ÆçÜãœcsŒnq¾1·Æ6¸Æ×Úã\ckŒmqÍž9³Ç6xæÏØã› Šz&z&j&j&j&jFjFjFjFZFZFZfZfJfJg#˜ÄJ‹ðñ’ksAƒü„övz¿ãáu&¸Âä,¬Y\¶¹mrÚåµÈ®Er+‘\ŠäW>¹õÏ®}sëŸ\úÇÖ>¹õ¬}cëXúÇÖ>±õ¬}cêŸTú§Õ>©õO¨}CêPú‡Ô>¡y!ä<‡âCˆq B¤)i¥¥¥¤–¡j¡j´µ¤4µ…¬!„0ŠdS"™ˆ¤E"(‘DŠP#ŽGŽ9r8äqÈã‘Ç#ŒGŽ1b8ÄqˆãÅ#ŠGŽ)R8¤qHâ‘Å-â–ñKx¥¼RÞ1o·Š[Æ-ãñ‹xÅœbÞ1o·Ž[Ç-ã–Ð- [@¶‰mæ¡çððÓër׺ 'ä'³·Ôß·ý»±„!B„!B„!B„!B„!B„BAñpSæå/̃‰ù ötó¥ÞŸé>E>BËÚ0oä·Ú8ì©úM¬¹£þJ{EØä‡·ôîåÕðhÁ?%=§^Ó³“ôŽê«V£)1ïW* A¢~J{U.C˜3‘N¡ij¤) B¤)rïîJ„¡(J„¡(J„¡(J„¡(J„¡(J„¡(J„¡(J„¡(J„¡(J„’„’I$’OŒ’I?˜ÊuRà"¹L¦•*¹ê5 ÔúÄäVaÔ+!ÔêN©ÔêN©ÔêN¡ÔêN¡ÔêN¡ÔêN¡ÔêN¡ÔÞu:‡S¨u7IçRyÔžu'IçRyÔžu'IçRyÔžu'IçQyÔ^uEçRyÔ^uEçQyÔ^uEçQyÔjF¡ÔjF¡ÔjF¡ÔjF¡ÔjJ¡ÔªJ¡ÔªIçRyÔžu'IçRyÔžu'IÇRqÔœu%:’IN¤u#©HêGRC©!ÔêM:“N¢Ã¨°ê4ΣLê4ΣHê4N¡Dê¡@ê þ9¿Ç7øæÿßãÜc{ŒoqMî!»Å7x†ïÝâ¼Cw†nñ Þ¹Ã78fçÜá›|#o„mðM¾ µÁ6¸&×Ùà<g€lð ž³À6xÇØàãÍñæÇøó?øó?øó?øócüyŸüyµÂAßä©Ë¨óº" hÔòÓÚ ZZAZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZXZZZZZZZZZZZZZZZZXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXZZZZZZZZZZZZZZZZZZZZZZAAAAA@£Z"~c|½ª ´´‚ÒÒ ´´´´´´°°°°°´´´´´´´´´´´´´°´´´°´´°°°´´°°´´´´°°°°°°°´´°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°ÆXXXXXXXXXXXXXXXXXXXXXXXXXXXXX#Dh‰ù­öØ ‚ ‚ ‚ ‚ ‚ÒÒÒ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚- ‚ ‚ ‚ H ‚ H- ´´´´´´´´´´´´´´´´´´´´´´´´´´´´´´´´´´´´´´´´´´´´´°°°´°´°°´°°°´´´‚?9¾íƒxGô ‚ð‚ ‚ð‚< Š ‚# ‚#Ø[ûe¿¶SöÊ~ÚOÛ-òý°Ï/ÛÿlÓý³OÏöÅ??Û óý°ß?õû]òý°ÏOíŠ^_¶(þÙ£çûbŸíŠ^¯Û óý°ß4òý®ƒ=?¶)z?lPôþØ¡ûf‡ŸíŠ+ó/íŠ~¥ý°ß?Û'§öÃ=¶(ú?lqý+ûcŽ/íŽ?©lqýkýŠI$’P”% BP”.Bä.Bä.Bä.Bæ—´½¥í/i{L20ÈÃ# Œ20ÈÃ+ ¬2°ÌÃ3 ÔÌÔÌÔÌôÌôÌôÌôÌôÍŠfÅ3b™±LئlÓ6i›4Í–4Í–L6˜m0Úa´Ãi†Û ¶›m6Úm¡¶†Úˆn†á¸nŠn)¸¦ãÇ7n<ܨnT7*uMº¦ÝSj©µTÚªlÕ6j›MЦz¦z¦z†z†j†j†j†j†j†W™j^ey‘æG™ü4Ýk—’Ã;LÈfC2Ìf3Ìæs9œÎg3™Íƒ`Ø6 ƒ`Ø6M“dÙ6M“dÙ6M£hÚ6£hÚ6ͳlÛ7 ÃpÛ7 ÃqMÅ7ÜSuMÕ7TÝSuMå7TÞSuMÕ7TÞSyÆóçÎ7\n¸ÝqºãyæëÍ×›¯7^n¼ÝyºóuæëÍ×›7n<ܨn<ܨmÔ6êu º†ÕCj¡µPÚ¨mT6j5 š†ÅCb¡±Pبg¨g¨gyæg™žfy•æW™^dy‘æG—¸½Åî/q{‹œ\âç8•%IRT•%Nçs¹Üîw;Îÿ)AAAZZZZ…¨Z„!j„!B„!B„!BÓå;)òŸ)òŸ)òŸ)ØùNÇc±Øìv;ŽÇc±ÛøcÂ#Æ ‚ ‚ #Æ?¬O„’I$øI$’I>OŒ“ã$’I$’I$’I$’I$’I$’O„’I$’I$’O„ÿñ4ð‚ …!HR…!KTµKTµKTµKT±K±K±K±K±K±K f3aŒÆc1˜Ìf3ŒÆc1˜Ìf3ŒÆcCŒÆc1 ´”ÄóÌO1<Âÿ|†ø ‚ ‚ ‚ ‚ ‚ ‚ ‚ ´´‚------------------------------------ ´´´´´‚ ‚-- ‚ ‚ ‚ ‚ ‚ ´´‚ÒÕ ‚3á$øI$’I>I$’I$’OƒÕ§oÇ‚?Ž ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚>#úœÿ’O„øÉ>0[ýÊIþ)ñ’I$’Iñ’I$’|$’IñŸ÷Çb?™dÚZB’¿È‚þÇQ>$øÿÿÄ)` 1AP!@pQ€"0B°ÿÚ?üÁWþ;UÉÉÉÉÉÉÉÉÉ\•UUUUWµ·~r"""""""|÷núwn÷nú~_ˆßG²']öFçMôÞ›Ó}£Ýï¬DD¾N==ŸëQшýQë«1Ÿ^ÿaW§§’ôö{E^—éÖ¯Jª¿\fú¯ÕŸFz~σˆ‰Ö"v7á*¯ZöVùwÕ|›òÙèßÁ»Ù{ð[óUUUUUUU~zçbçÄgbç_…ÎÅξÿ ‹ŸUUUUUUUUUUUUUUUUUUUUUUUWí³ø¾Jªª¿e½Û½Û½Û¿Á÷n÷n÷n÷n÷nþÒ7»w»w²âjjjjjj&¸ëŽ¸ëŽ¸ëŽ¸ëŽ¸ëŽ¸¸¸¸¸¸¸¸¢'š"""b"bbbbbbbbbbbbcŽ8âbcŽ8ãŽ8ãŽ8㋎&8¸âcŠcŽ8ãŠbbbbbbbbbbbbbbb\\\\\\\\\\\\\Q1:DLpÇ pÏ4®.....?UUUW¥UUUUUUUU]]UUÕÕUUûObÿWœþׇÅãñýýUóÕUUì‡<>½?Ç?`ÿÄ,!1P` @A0aQp€"2q°ÿÚ?þíB„!Br„ÿÇW&L™2dÉ“&HB„!BÕÒòÔ¥)JR”¥4hÑ£F4h¾{o¶¯m^Ú¹ßJüx7ÜNf˜¿Ü>晦VV.ìÓ§v/Ç%ß’îS‡±ÓKÐÙ£_Ázv£p¦ÄéÄÙz¤>#Fº³H|\¶.†‘¤mCãQ³]8¸âpâ⊕!;öÿˆˆˆB!p‘ ˆ„èBt!”e¤èNÃTÊ„'TÇÂ>=̙厦{üèc¡‡ÔÃËCà©M' HãV">“î¶1x'Äé¤iF—-u*5Ô¨«•ë Ê¢‰óNùgÜ^ u'QðÖe™|£¤}Hé—NÊu#䑼³î/k]ÏÏ«_®—÷—\„!B„!Bs^Ñ_5ùƒCðL¢ð/Ÿà^ ·¢¾xGÛ)JR”¿c~”¥)JR—í ¼,FQ”eFQ”eFQ”eFQ”eFQ”eFQ”e2dÉ“(Ê2dÉ“& ™2dɃ&L2`Ã2̳,Ë2̳,Ë#######2Èűø'éOÁ¿JüøF½)xXB„!B„!B†HB„!“&L™!B„!B„!BŸ³ ôϰ^Ú½µ{jöÕí«üµ]½µ{o¶ðûjïí«¿ø»{oomáôúŠŠŠŠŠŠŠŠŠ#H¨Ò4#FŠhÑJ_®ÃLÓ++4Í3LÓ4Í3LÛ4Í3lÓ4Í3LÛ6Í3LÓ6ͳLÓ6ͳlÛ6ͳlÛ6ͳlÓ4Í3LÓ4Í3LÓ4ÊÊÊÍ2²²²²²²²³©×þ ©JR”¥)JR”¥)M4hÑ£F4hÑ£F4ÊÊÊÊÊÊÊÊÊÊÊÊÊÊÏ”ùO—ên#å>Så>Så>QvñP„!B„!B„!Bˆˆˆˆˆˆˆˆ„DDD'ïBêãìB„'Ÿý’,§úœ¨¿áýþQ”EõÄDDDDEõNPž_ƒôøx,õê_Ø¥)KýþÿÄA 123‘!¡A 4`pq"0BPQa’¢@±C€#Râb‚°ÀÁÐÑðÿÚ?ÿÅH0à 7Ь0à 7ä9aËG#—®æsþÏý‹õ ÖÏ­¦2šþA»ðž÷ËWÁ2Áps!ðDÅñ^¦JšœF†Cš!ªa.š¡¢ &šs2†<Œ…Í9"äE–÷Å=ꦽHL°L"ÇSä!™ñ" ¸hj‡a‘àšŠ‘ ±ˆz1 œ*‚ñjeÉNÈŒéáù‘D¬òO|¶&)‚ãš™ ˆE†fg¤*"á©2\á„áUP_‚™æz#Ÿ1QLÕtÑ3†,¼Žõ‡Ax¢UóÇ!uÔé~bwÍ;Ţޏ8‘ĺûÑ?»îø2À_pgë5O WÔf‹ž¦¾Žfcâ½Mp× pHDL„‰1DÈL¹ã(¹aœ‰34ÑË TЕFÅ—Õ¿S‰bÉ 2¾=ULùuWò9è†XpÂæJe /¼¸Ù j.F¢äk‚õt545!zš œz –¨ff@¥†Ã‰z™/ÄX”Ï™\ðèÏÐ\…ò<¹žp‰ò:X¾*üË#1x±…T”…rÕOÄDÈèáÊc¤DIqH¸AW,Žƒÿ¹‡ÇZpæd½y&K¡0ÀždYû*/¡‘Q´"þeŠEg™Å p¢j¢G–¢~+¬ÄüEÕHRh×5RQtS,—1`H´DÌË%ÌÉbeôL¢‰|ˆ¢\ÜW‘Éò:_ÐŒâH•W#Ž8µËÝÑwE5SAÍǾ¸±ª!ª ˜6 dCèëÞ´î ÕË©.® ½EOûb¸²‘uWÌÐ×ÈÔL×#õ È…HQæ"! D+Þ¿Ó¸('ªÏ ú¼±ÌåŽ]Vê¢eƒf6HK†°æ!¬9á’Ù¢wª5î> Ä¢÷’’X¤–)%Š)bŠX¤–(¥Š)b’X¤–)%ŠPØ¢–)%Š)bŠXÓ¢KŒ#ÿÑÑ'ퟴZþÑiýÙ§‹)ýÙ/‹+û¸ãŽ8ãŽ8ãŽ8ãŽ8ãŽ8ãŽ8ãŽ8ãŽ8ãŽ8ÿ‘a†a†a†a½CŽ911112“©:“©:“©:“©2“©2“)2“)2“)2“)2޾¥a†la†a†a†a†a†a†a†a†a½äø8ãŽ8ãŽ8ãŽ8ãŽ8ãŽ8ãŽ8ãŽ>/ÿ)ãA†ÅhSÅ”qÇó1'ÃÃÄðjçÞbfÂ/ŠèŸ,ˆ~Hpø±—¹ßqÇ|qÇ|îïDý ÆOÿÄ/!a 01Qqñ@AP‘ð`p¡Ñ±€áÁ ÿÚ?!ÿ¬+¾¥û4Ê,²Ê,²Ë(¢ŠÁB„!B˜M‰„õzR”¥)JR”¥)JR”¥.4¾}Ar ‚ ‚H ’ ’pÏŠÿÿ B„&Äôë)JR—)K‚à¥)JR”¥)~É„!BÄ›@„!BÃÜ)JR”¥)p]À)JR”¨¥)J_³Ð„!0AF ñ_ÿ€ÈFFFFFMõ)JR”¥)K‚”¸)JR”¿jaˆ ‚ ’IÃ;¿ÿ¤³§côîð +Ÿs!Br#AdŸúIK÷Aý OÿË-Ÿõj¿ã{ÿ‚sþ/YOþ{qú ÿÂÔ=|'ýcÏ´î,ÆlO6&ìÜñ“ÁÏ6gª¯®föxIêÉW >¹›Snz4ÞO>°›ÂÌ&öx9¸ž‹>®›¨MÄÙ›ˆLfêc0›¹±6fÌ'ØåáfâmLfja6!6&ÄØ›SÃO>¯‡–úlÍÌÛ›SflÂlÏA˜L&ê})7w7w6&ÜÂa6¡ „ÝL&3fa6瀞%ý‘6á7“ja6fêa6fòc1›s „ÜO°s ¸›MäÞÂcLfÌ ±0›jmBc µ7SÁMÔÂlO¤¦öx„ؘL&ác0˜M‰„Ú›™„ÂlÂ}‰ž&xBa0˜ÌfÜÂa0› „Âa7›bx÷êiÞF°Ö{£Tk6i3@Ð+‘ „ô%¶æñn¦ÌÜÌfB›ˆBmLfÆÍ‰³1žžÛ$sQLJ¿+>F| Ô{š Ðf“4YÔÂÓý`|áó‡Æ8týŽŸ±Ñö>t=‘/lrø)Ñ:'ÆsáHøÈøÏŒÏÌüÌ­JÔ±c©ìu=Ž¿±Ôö:¾Ç_ØÕý§Î¦ª5Ñ¨Ž™Ò:X‘¤h&‰¢iš&‰¢išf‘¤hr+‘\ˆÄ™ Œ!La0„!B`„!FB„Ä„!B`„!B„„!Lè²bœÑÃÑ®ÝÜ]Î\ˆ¹r"äEÈË‘".BŽ9“œ„D\ˆˆˆ‰¸¥ex(²ËätȾEò:ƒ¤èÁÓ±£I$¶Lº¾ [ô ªÔ5MSW®kà­æ&¹ã–s7SÒœ”!z{ðÏĽ–B „&kbá³++æW̯™_3PÔ5 A ró*ZT\fÖo˜”SÇ?[{aíÏPÖB('¹›—é\DÓЃxq4‚î‘Ü%ù N ‘+ÇXWͨdœ¶š×Ë´‰´ÉqÆüái¿eM°í8`ýG61ŵ=ZýOQ{l ¨ÜF妅 «–ÌN07Æm`´¤_(Ì圈ÕÝpij2¸´hIx-FªðEÇË!¯,†1<ƒ"1s “ò=ˆy½®FâK3.†4k2é4<š8ÖÅ5¸Ô#gB¥Â оDxúó°™d‰¢Çå‡OQhΈ0‰ëˆòO€¹`pæº+…ŸqM'(rV¼ÉÓRüQÎæ:kQ–¹ZÈÊüÈ-‘—ÞqzÎ/Dq¾¢W¬$®OR”à¶`÷Oм‚xëeÇÕ´Ía1LÇt‘ᘢ6Y«ÈKEÈS©Ë±å‚/wƒóÓÈoˆmxb ó/‰^ðgeyNDJs’Ã%²ckZS(‰© ›¢ìÚ¤ãÄUŒ‘‘°Ð®%~K:R5|Òä'5ÈÌ/à/fGä»jq\F…¬ÿƒ¾‚©Þ~e^vFa:Óɹî%‰q{SÓ_‡p›SÓW½J›™½„&3L!EË ¡G"`„!sÑØÉcyò,ÊEUq– //"m¿Kɲyë_ëô¬!Bfc°ORâGÚófz£þÿJ=ûÅ¢n!=%/XãÙ©a=aþ§‹žž”˜¬)»ÍÂôý†aåÏi;‚Æã»M9fX̬Ûz9Kqm/=‹Ì‹„~/¦½åâzRqhqÅv³z½%ò¶R±…dÛâ$È ëbJTKlÃ1Y©Kifd/ÍŽ]äRLcIž;šæ„vÂmòܶÔÑÀ¹š#¬ÙqB’$Ÿ”%._‘òKÇÌX!â~|¯8D”¼pŠŽ#(ÔàÁÀÈ„"pâNB,£AY¼ò¥M¶‘y#2U‚MÁ7ÐËòz ¾úFoC<óeÇ.,ž| åú3_É•¯¶bÓ™ƒK‰SàÍ ˜ø¬@ʦ]×K °§‹v2S‘o^‚ÑÑ|G9¼ž ȽG›ø8ÔÒ¬qã-ìÂz ñè>q .í7 Ò_õÙ³2‚d—!„dœ^C¨Í?!N¥>†qDŒ$›^Ù`ÕÊ#÷ ˆ¹Ÿ¢I߉¼½†ª‰J†ƒ_²"“ü”g;%y RÁwWäH²)©šY®kÃ2ü!¸þåitM9cà.Å&ànÀ§Áy´¿Ñ_|Dœ“Î^aôÑ~GT Žt«#„¡y¾u¢“­d8ÌÉíÆýVPX8Êé`ßayp%‚7Øâ9IÞ¥Ãæ7“¤—R·8rœQ"^hª‰ ô„ªîgDÜ3Ž}OöN>ÈHs$¿ü8U¡"Éqâ\Å,¸õô‡œõ‡3±Äœ±˜?I\ÃxxIzKìÙ.Fd€š~“Œþ§£ýmP’6¡.¶Ø¶)ù²¡j £“C…þÄÍ@‚)ùÍ<Ã/àÈŸQ$É-½2I2âƨe'(—¥3?"qØ7(é~{ Ó¤ƒ6M ÑÙóÈ˼ðÿ=LŸdÁY)ñqWxyg6~ìzW;žÿTz„F™¢¦YR¼Aërº zÜjãI#¯›oý"²?'è¥Ò)¢¼ø_=añc“%¾Âæõ²Xd;'+®  Å€[/þœ%µ“‰Ÿº“¾t×’3=&àKè]ˆÏbrd—øXòÛü3=Ñþ²ÃÇà•éæ21%çø“Ê/b-Fj¯ð¨áú+o5ò‚3¹íU’¢£xsäsoæáædkÉjÑÏ+¤1’©q±N†ÚägÑ?Ö5ü_áÇ|d5ëE§ÌB'‘›é7Ž]ŽÌÙž„±ß3ˆ›¤Ü¯I|¾n& ´­¹µ6Y²kˆÌq7„ßM˜CAOc ŽD%Áàjñ4DÁ§"‘>S!:û® ?-«ÃßaÞ¢«4Ú|ÖLmÉϬW34ךÈt¬o›Í–³h6ñìÃŒcOÌ\ÒçÅ&&Ao6ò-.ÞNûŽ"·Än¤ÏMýñ‚ Ãåèôß0„ÂB˜B„ÜODÈÇžð#ôÆú“ƒÂaMìôªÔȨóÀœ­äêžKc‚Ý“8*kýnz4&ÓÞ?$ÈX€¾zGï‹ôl³ þO 3Ì+¬f…y-n¤i´ÔxœB:B ¦ž…Ý 2$Urb¸‚[“XäV6mðo#‡£…±å'0Q£Rmrtu8ÔbZ§c©Ç“ÕNÅ^F|Œ¡Õžµ= ïìlP»–oW¤d럫±Ä?¤,î³8†Ê+©xhM8h|Å-b©ŠÓWqÔ,£Œ8Ýf^±Íá8埙 Ó–Rža¼YÖ1¥¾ ‘CÆ=8Ž.'˜fàÜUG”€›3È:F•?ЉŸ¿g …HÞ0½ÉÔ”¦YdìAæÚFm×ÇÓgˆ˜OCLïlØQ|"ôØ?Waœ±œf¬M®L¨žfgKØ\s3¥ì:Ý|pá&ˆÏo!íGR {PÞ¦HIòfL^õ HÔÕò$±äaÜ 5ŽVQ"aÆH‘d³ÈJmD¸àÁ×½qúrûf‚øRôŽ›¡á¦æmB„'¬?E~&¸|*‹àÓ\#?Õ|&ÒW73¾X?½!qF}·ƒ§Ì™ò&|‰šfhý™§ýî9’ª‹»Øïç;ñÜÎäw³¹ôîG~;ñݹyå\ÑW4eÌËjŒ‘">D|™">D|ˆÈÈB„ñp„!BaB„!BBa6!7Àðƒ;`\ Â/H\G©²•¬lòPŒÌ>AÓ:gDéû“ËîO/¹<žäò{šOsIî|Œùó3çf»ÜÖûšsQîw¾ðïz;þ ñ‘ñò¥ú^Ä|é*>4ÞÓWÚv—Dø;ìC²Ü5žÃ]ì5‚9Äs{…ÅXB_xøXÑûË™¯{WuÓÃm§–HgwÔ÷Ýq9ÚØòö#µ¨Ò{cKì|ˆøøñ&| Ÿ"gÆ™ó&|ÉŸ2gÄ™ò]çþ¤–Ò÷Ð:IäO"9ÈžDÈ‚ $’6òáæ.:ú uŸÈƒ9b#ê©ÙÇo¬vAÛj¨váÙ‡d v†8çb;qÛŽß°û¿!Ÿ!ášvi}Ùò¶|Œ|Œj}Æ·Þk=çvÚwYßf‡¾|ŒÎ®»Søw¥ü;ÂþÑ࿇|_Ãà£æ£µ£µ#¶#²#±ŽÆ;<ìéð?ÓQíþ“Ìg˜õ[ ±¡Ò|õ4ÇEµVów¡ß‡~µµ¹Ÿ=â@|æ|vwæw¦w¡Ý‡ræw™ßf ë¾3'LhbÌÖ{ g´ìÓ¶ÎÒ;ì㳎êŽú±jí§i;ùÀêR)¯ö5Äç{™ìqâ¡ÀOa"æ„ óÃäðÔ/H|ãÅð-ÕÀ„úŽa1„!B„!L!0˜Í˜A£âdÔÀ·Bzøàñ|ÙÁþª! „!L!B„!B„!B†LU:š ëãŽ,X¸½ »ž…<\Âc6&þlMÌ!6¦„!B„„ÁãÁcÒ…ð×è áų¸Ì»HM‰„Ú˜B„ÝMÄÚžðT?DœõhL!B„„ãÂ…õõ³í_‘B<7Q—•é³b „&„!BlæÇa)æÇ¸áôcűÃèq«è ¾˜Âa6æÄ!HÄøG%“W-ˆL&Õa ·$ëk‚ Ãð¯ÒMãÙ|Gé ‚&„&Ä&0˜¡B„!BB„!B„!B „!1˜B„!BaLg¨Í·ÀlÛBO¤Îç:ðÜ^‰6g—cˆ8;©À!B„! „!B„!B„!B„!B„!B„!B„!B„!B„!B„!B„!1„!B„!B„! „!B„!BLÃÁ*.lx Â(¼2A[M²ËG6‰†{ˆBa0›˜B„!B„!Bjxº„!B„!B„!B„!B„!B„!B„!B„! Ž7¾=„5!ã¼7|ŽGÈ/dgÿ1Y§'Å ™þPÐ{3Gî4>ãOìÍ?³4þÌíLÒû3â3â3á³ã³å³ç³ç³å³ç³º³¾³¼äï»z€Plµ…Äêôp¯DôODôOLÏ!“‘žÉ=’{$vOM_´ÕûÌ; ìÃ³ŽæŽìŽüŽè€€ŠŽž–Šš¿§hGjGlGlGh;Fæœæ«»5Ë»Žî; îQî>f>6>v>f>f>f>TÏ3Gìͳ;#>#Úgwþìw£½èïy;©ßNþw3¹ïwÝÎôwƒ¸Øî‡t;¡ÞäwlÛ‡yÀœku±+±ù<Œ'„½žGx[GgÌ»ýÍìÐû³ålÒ{³àlÐ{³IîÍ7»4žìÐû³Cî͹9ää{“‘îNO¹9>ääû/s¥ît=εî~>çÊà|)Ð:?C£ô:?Cà‡Gè|°ùáñÃãHøÒ>‰ÿ˜Oü¢æÿ&Bn)5Q5Ù§`‚v)Ù§m¶v¹_ùKÿDzj«%‚¯»æ;˜íÌìlììù¯ |‡‡4cKî>wƒ:íÂÛm 3Éwa߇uÜw!Ü›$:ßñ?Ãá‡ÎÿÙþ³ü;gøvðíÏá۟÷?†¹¯áð5ü>¿‡Æ×ðøÚþ _Ããkø|sø|õü>Rþ)ÐŽØŽÚŽÒŽÚŽÚŽÒŽØŽÀŒßºóa-‹t}•ʾ§22"!4"äDEÈœ„r#‘ hÐ4 HÐ4 HÐ4 #@Ð4#@Ð4 äG#@‚9A]ŠWèÀ]>©¾—¬,¡0-‡ë„2“û÷ V’ç2ó× `^#Ò—ä2 ë7ã²Gû JN,Q0¯Çé-jäqÀÓ\~ª~‹VòN,á5%æÂà/âG—¤47ÐU›T—›’i3Aš Ðfƒ4\™9 ÉÈÈù2>L“#äÈù2>L“#äÈù2>L“#äÈù3>F|ˆù2ò!4ß]‹ŽE)QQQW29š†¡¨jˆÓcLi1¦÷4žæ›ÜÓûšos¿ðïÇ~;ñߎüwã¿øïÇ~4žæƒÜÐ{šOsAîh=͹ ÷4æƒÜÐ{‘Í{šr9¯r9¯r9¢9¢9¢9¢9¢®h«š#™ÑÔG2”¥)K…”¸ÝãYøì½Ì„ôŸåR‰„¼k„.’ÆLÓ‡‡Ä.dÎË«ª¹¹›ÿÿïÞ»»¿ÿÏqØÝÞúwÓ¾ôï§y;ÉÞNðwì;¼àïq;†î'w;¹ÝÎï·cKÆi†œiÅòì6Ó°gÀωŸ<>x|°ùáóÃᆣôk½‘¬öF¯Ù¿dk}†·Øk}†¼>>uÁ_Ÿ Ÿ2Ÿ*Ÿ:Ÿ:Ÿ Ÿ:Ÿ 2"22a ¤ð”•¯V·ïg[û¿Ë2»Á.6â1l9Qx”púLxGãBË,²Ë,²Ë(²Ë,½ÿÿÈ¢¹Qe—È®EQE–Ye–Y{_¼6Ye–_,=:6eî¿8(®X+‘EȲË,²Ë/ —¹ƒXß„¼WŸ¥“/Ó¸9ѹàZqúpô'IÓ†÷{·AÐt'G€x у£AÐt:ƒ§AÐtï6ÿIÓµþ’yaé:N“¤éÝÿ½'IÒt'NÆèÇ{^t`è:6€´lz–òxF[0ž$ÿÿ§ÃÏûÿÓ¼ÿý;I$œyômêG€ÿÒ=>)( —ÑXB„! ‚„!B„!LHB„& ‚„!B˜Â„&Ì!L!B„!B„!B„!B„!66&$Ä„! ¸7a<íz"ñsêy·=‹Ñ×€_h¸ý)}¯â Âû ŽBKí²ñÄûÒ-•öŸ€Ÿv<ãܯ ¼ûRÿv àÄ/¸È{kíµñ\pzÙ_h¦ûˆÎ¾x…ö±q8Dû®¸à¿» “t¼ñ«ìïį·ì^ý}Éh uò)ÅÇ/¶sÀ3.Êû§Äféz2ûd†¨=•÷KÌjÃÝþ6—Üwà݇δׯ鼯ݓΡÁà—ÜgŠqH{kÑ×Úž4gèî}Ü.#Þˆü5û…™zÑ®ÍûZÙŒ¿K_§_‡Ä¯ÖwÄÒîï¤>n› ânuK‚Æì]ÝÂì\.¾øú\)J]Š_~…l¢Œ~?N¸Ý›…ÜÒ”¥)W2®e\ÈæG29¢9£QÈÖF²4†˜Ñ#Dh1§4æ”КBiÍ9£4&”ÒšSJiM§Ãè÷¬ÚL¬k†¦äíW@Ò4 ½7â az%ršLi$tΑÑ:&¢56•"jÍ^<ðïx;ÉÝNêwó¾üîgs;¹ÜÎêwC¾Àî[•°9€ùÍy @7 (ƒ¥¼  «¥³ÒW"¹ȾGA\HÒÂÐÀÐÆ´<<þ€•7ꚦ©®kšæ¡ªjØMM¡"£Xj A¬5†¤ÔšÓXjÇÍãTjy®5°Öƒ\kqªÍVk fj†¹®kš¦©ª^ró—œê:Ž£¨œÇQ2>d|ÍB¹•̬GQÕ‰ÔuÌŽd“ÌŽds#™¨j#PÖ5Ms\Õ5Ms\×Ã×5MSTÔ †s˜œÄæØŸ¯sÿEÜLblMèLHL„!M™´÷Ou‘–ë#/JR”¥)JR”¥)JR”¸.Ð.Ø——Ä€@]à¸)JR”¥)JR”¥Â—ÄÒ— RáJR—bãp¸]õÂìßKÿŸÿ€‹êóa©öÇ/ù£ þÉB22>Er#äW">EÈ¢ùF‘¤iF‘ hxè@*«,¢°Y[¨ždsÁdŽdA$’jA(žxTd»M-’xɱ0˜L!B„!BŒ„dx!FQD#!EV Ä¢°QX+ËðÿùÀFÈFÈA$AAAAAA$’I’Ad‘/,0AR ’I$ã,ÇeYF|Ž’¹ h%¦ ÖÖFFG2¡Ìde̼ÆE2Ã.eE ®eæìY¹†6!6¢ ‰ K+QE‚±E¸ŸHÒ•aQQJŠŠŠŠŠŠŠˆ ‚#b©ðAI$FØöYxWµSÇ XÕ5McPÖ5cP×5MS\Õ5MsX×5ÍsPÔ/1y‹Ìj†¡y‹Ì^bóó+æ,ëÁ“q-®5Å.¥)KƒSŠòjšë_l¢›5ÔпD"]ïû¬{,¢ðQEQX(¬J+d+t¸)JR”¥)JR— …)J\iJRáJRáJR”¬ áÑ£†yÓ@µÐ¼ôsÞÀáù­ãrpµ|ST×5ÍsT×^g¯×®‚Y·ä .¼ÑÌLóçŒ'§qŠò$pC> lèE%¿SGc##,22Ã!²8›j± \Šä$ß‘¤CÈŒÒ4 !&Ư#CåõR‘ð óóÅny½‡éŠ<ùã&Y45<Æ4ÇTjçFÊxCaE²¬k˜ƒã™”%¶Ùí…¥T¶e‚Ö²• 3 ŸÕF=GXqûï¼ò€Œ=ÿÎA÷ôëm¸ÍtÎ E01=ßÇ]pðŒa5ÿÎÊ$¼ÃL3 <0S|ë.³ÿïºø©» ´Ç/4ÃŒ{Λþq²Ç @mãì0('ºˆÍ °P};D÷úÇÿ1ßöA'à{þeg ÄØB ûÃCÃCÏ=?{Î8À°°ÄÅß&8o¾,³ï)-ÏQÆÏ¿Q¨çÿ½ø@ðçŽÏ ÎÀ wØß»ÿò$€F2 ôçÿáÖA7Üó¯òÂpÃKxÏòûKj‚OO<°F<ò?þè ¹9ÄiWµßþº÷]lžÇОøŒA·_=ôûÿSy€C¢¬·ýÿŒ~Âj M¼G<4÷¼¤ 0„qò„ÿ^çŒR 0b€0óT<ò…>ˆrÿˆ!‚‰g­D ÂsÏ ÿýñ/ÀJ!8Eô\²B<B ¿ 7q•ÜU—|}„}·Ú]¥8ÓÌ~óÀ bB1O>#r‰äl B ÃÉ43mM÷Üïÿ¸I þ°Œ0ûŒ0 óñ„<ð„‚pÕõß}ÿ¢ƒÎuã 窱žÈi]¥Þ}vwúˆsúÈ8î.uôòîkâûªCºóN(S$å§ÑGÿwÆG묦7ã ·L <óŠ5óË CÏ "„7<¢Ï0ïö@Ìe7]%¹Ú›à¿ü×œ× Cî4×R1AÚÆi,¯üvÁgí/öã=ßáê ÷¸Ã >0 8óÏCÎ8ÑÃóùÛ•ÓÜœp¬‡N}æ×q—‰sÏðÿ¼;H„(eEu|ØKí(Cƒ0Ó ‡×}$×yÍçµ×tÃ-Î!aßCñ4ÉQ„¦WQuwÐVûá¿<]¾r‚<ÿúŒä1'2“Ã<ë@üšI%4uôò‰€½¶[q‡d£L!¯û œÓÎ`øâ–‹ßl¸`‹Ïh¢«†ƒÃì®XçºúmŠK­¤4YQð ²Û¸þð†ûþ øG ûÌsÃßÿzª ¾Õoü>ËwH,cE bB¬`b®j-5d˜U¦d›Š;Y¬£€÷ÜzÞyæšfKàλf¾ ßq¥* o‰ßúÃ}<¢ 0yùùë?ï^|eᆰ ù¼·ãL;Lj0 @ ãÎ";ä’ãÀ%TÛAXþ,ÈàPŽãRÁ:ÑßϵC¾ÑCBQ¢â£ç]<4'ùýÇÿ8Â$k?½ýùýóÏðÙÇ2Ë,½÷ïðãi‰Jí±Ê/‘…UD<²é&1¸·ìÈêä5îu;[øëN|×&î¼(¢ØãPÃRפ_cÏ1ЬVçÌÔu¾<§>3Ãpki²Æ‰q…w×Íi4V!G8“Xf‘ã&üŸµêÆÂðÐÏ[Ì:®)5ßþ·ÆvÚ8n„7[j[ q8߸zHÂJÿƒN>}A¾ú,®x/â{[Ë]0ã;ì_o§ª€Í(fQID²æANl’“ËŒð&eJ,Š©ãº­ò_Sþvª9YNû}_°ñ‘ß?÷˜Ç)ˆaˆ=ÿýß}óë ŠèogÔi\õç½ÕQ•Óÿ1íäR+Z'~ìÄÖœž¥·’²ˆ°óy䕳,°Ô–Óʯ=¿Äå~ ÐßžVš˜Égw¬y,/Ú+™õ!&4YÓ¦ z!J µ®HºY›\^»mråÏKqÖ•Œ>£"‘:¦Ñ}§y&(îo–?å Ï*,ò¡ãvd%™{Ž š){æÑÎF×y†<Гç kOSçvüÓó];ú?Ûs®å ¥4·Óñ R¢"ÕæOOPKUTÝyWezïxiOºÌ㘃ׯÑPô[ï؆ AgI3+ßì1€-þï$‘Q„N0ùéŽHæ£!ºËî†›â¢øi™ç³, ”÷†ÄwP»À…Å›qœue‡.âªj²ïð-ݯ–,´ËÞhÍßq—o¢84rk’Ó æ²Øð»Ä{I]óN;©žû,®Ëg¾ÒÊÙ(¢=_ÑA Ø/lÁؼ“"Heªêkñå‘Â×Ú[%ûEß<•4¿\zÉ'”Òi_Û'°÷¦0-˜üòUá z£wéq3Ž Òç4# ˜QbF* kç6npV>Ÿ.ùY*†¶›ÙAnÙr¸æ¶Ó6•åÝG‹j ¶ZýÄÇ€¤d)wß3ßçŒþãŒÿq…\D5Ã1ŒÀN4³Ì‹¯’‰%‚ËqÄíU¦5Ð& ªh§¦é0qwöoœ Ð èuMúçœ|Gyzï Wt†ËÝl¤QûŒp=ŒºäóD´Ñ&†½#¼Kf_Iï,óÃ0Ãó‚ ‚á†ûëm4Ž /¾¸`Aþ리 Á ±Ž8â–I—Û[•Ñ©D󫊃ëzì·ÁóȌײ±jÜù‹8¬ŽÈcN÷ÿ<óÏ?¼ÿË 0à £ÖàáÞm$dûÌ1'}5“y…”isÛmzÞSÓè¶ZxæÊÏ*4ßüub¦°ðz¾•QéÅë5 ó(C/­4B!²ÓÏóï¢ÀGotÎóÚC kŒSÃÿ7óId^e óË·¬3š|}Ë£}÷ïjLÉ/8ŠuQâà¨4‘°PGý÷¬‹0CóO<0óÐÁ9¬”ÓQ%ÒMÓoëi@<‹‡“e~SÎÌo×]׿1ùœî8þê ±Mç6ûï°óŒ1ÔÐ ÿüªOzלÿû¼û¾+ŠÊfeŽþØÄTóA(~yšeäê "š§Ç$ÑÏ5Ž0Kó À¯üò£ŸJMƒì×~Ó_,ÀŒ1•ÑLB (£×ßíº€ÃA÷Úq”üÇoõàξ- ò!ž}Ûí°Ÿo¾+¨Ž81ë‘f„0­2êóû¨„€¶G|„óá›Ü޳Tq|Q¯ý;Š:e߉yšnÛü±yõÛIôÝiF_mà‰®{þñÔ3ÖÐPÀã²jä²è¤S£ûóÚ˜­ÛŒ<¡ÂxϾ÷Û2TVºÄÇ8ŒÑyㄯ\?Ú Œ$ä ò*¬7û<·Ë ðÿ 1Ý„óϾóÏ4Ó|Ïþé¤wó 2ÿÌ1Iźçjñî-r„RFa:C©xBÂÃÓ­"¿_u¥I~"¦(H"¯¢PZ]>å®[ †­wo,?ÿÿÿï´ÏúG¤µÇn2÷Žõ×¼}ÙµP¿®?þú4ÍBN]t°K¹.ŠÅÿFB§>ì¿’YÔF àFif¢|€‚"E‡Ú¾‹þÃL1Û¬Ðÿÿ¹ÿýß|ûàÓ\'÷¿ûoðßq†q„NÍã’˜cËLvèA]þ}áØˆküjÎ1´×aô­8cBac¾• #Ë`Æ%çØ]ævãøÓï8ýÿÿÿÿ|ú¤ÿ¸ë.ÿÞó8Ô”aôÜ?ô’Koð8ÖÂQ_óÛ»!-Ùdúwž,Zq¨%FT!€ŠÉ]‘1Ä^AôGëºÇþ1ÏßÿïÿáÓê‚/¿ç^tŒ„Ymu™,C€ h’½l¾ËvÃËSýÏ_Hj$ƒàøXkMš*8QùÜHÊ«AWØ“È0t^ö_ãðü³ßÿ¼õì¶ÿ÷ÎÉ0Ïþv}Å[u»yâ—(g¾ì,ªO-Ϭ8{< pEu.ª3×€nùÉøçÊë ’ì‘‚ÛyfÆ2” ÷Ya °Ç~±ßÿûÍ >ó_ß2H¥uRA4ÝIM¾úûïŽ0ÿ,4î‰o¿¼<¡HÀ4ç»Î¦Y@Kþ ‰Ðd»/’Þ¦„P¬¿‚C'Kr(x<Ïü1÷¾û$4ÿ|¨`¨3(ÅŒ6›®“=µ×M¸ÃüüËO9Ž4t*ƒÀ¨B‚Iã#n¹²uÛQÓ,®Z¡Ž“Ѝ½{¹ð("„8S= ìÃ|0Ãý¿ÿÌë2ýЄMeÍCL<ë 1Çž³÷ŽóÃtƈI5®³Ç̦ ¬ìKç¼~6÷N—aà ŠèŠéÆI¸¯©«3¢PO<+ÉLä¢_0ÇÿÿþCô\ðÇA…œ,ÒFÏ̶ûÏ6Þ:ã5ë}<ó¬Ax–GÈ‚è(½gn®¢øø¯]mfH¨m×Ú=u†ã–¸Ú@Ðàkðb ð“?ÿoúc ðÅ÷(AMy`_w4ïn0ž›)¾ùêLzóG5ôpÁÂÒ8 ¶e_²[!öê0ywK P펹"ÁµPâxl- D0ÂhSÂ;ËOÿÿÿw,3Í÷Ì$áGlçmr×ÛãÃ{ïÓÿ·Ùµ]<Â7åÔÓŒê‚ûÉ’kƒËS¤á¦HE‚Jï’t–[]¬€4Ó±OŠH4÷,ÿÿüýÿûÿ÷„8‘}u'½¼çŽs¦ì¢H`¾9ì÷ÔÓ\⤡vÑ Œ£’äòùc [‹4ÓtÐÉ,h#¾Óšh‰©’£Ê8ãŠ<óE< (“¼<÷ïÿÿÿÿùòG €,{ß®óòhî¦ã‚Ë,·ßE8粇 <+¢È,¤>§7Êa–8Â’²OQ“ªŠÚÃ.øÜº¤±àíµ<´ßó<9C±=×»­¾ƒ B ±)†ˆ –Œã 0Î=,Af^<°Mÿsûì´T­¨’È-²Z§¿þóM÷PC€È|f8ížûéÞã“É[&É6AÓˆ0±Ê$_ïë¾*  Ô:ú ¶¸ì2Íq²ˆàóÃy0SQÍwݤ†êͪ!ªˆc‚ν4›ëCK~"kˆodJ.¡ÈC·µEª •Mw˜M±‡ ôA'9¬¦+c€ÃÏ '8gwúˆ óO, 7ÿíûuDyœ÷Áºâ¢ê-®¸òŽ®ø‡Ls}Åò¸ÑY¼çýr'q`*f®0I0ÕÆ ÑÙM1iæ—lsB %«JaŠå˜Í·]°Âg¢-æóŸn:Š_]®ûY€aåPÚK(67̹Üà Ol,÷-ãìåmÜÿ¾«-›~Û}´qö]µÞE÷ß}…a4q|ýÿ0²Hd(A§|»Ï¶ÝÆ0Öˆ#ýeYyÞIQË+aÄ\´xÀŒ ‡ ãÇóÏïßþóûÿ|ãÜðÓ!Óß|ã1ÇœaEpÇqÇM%_|8à 5M4óÏ8ûÏ<óË^òƱâl4óø(ÏÏ4óïÿÊ₾„s 0q|íNç¾ïg¼0hƒÏÏ,Ï}v A@A‡cß}÷ß}÷ß}÷ß}÷Ï<óÿ¾úõžÿþ®xøÂ°ËØ`‚©þºÏ:û@ 8ýI”£…üÿÿ=¿×Ú 5ól òØLãÏ 8„Q7q§Uã 0ã„<ûïþûíÿÿûÊÛàŽy램eª šËï¾Ëo¾vŸÿüsß¼0ß;óÿÿéÿ÷á}§|»îUå_-sË@$DAAEq0AÓÏÿû/¿ëoŸ;ÿ¾ùäà gŽ8rÃçªo÷ÿ}¿,ó¬0Ç 3ž8$›ß­o¼ òßl÷]i× v“Q€A@ ŽûÆ; ¾éþ¿ÿïºËï®;ïºã‚ <žùw}¿ÿû÷O2Î3ÿ,3Ë=óϬ5ÿÿpÃ.4ëŒsÿÿ÷Ïù¨ÇýÿóÌ1ËO2ãŒ0Ã$PA4 8¡†ùî°Ã[Æc-Žjâ´òÛn0¸ü0óÏ9o¶ $®ÿ{þsÎ0ÓÈ^°4 8ÒÅ|çŸ}·×M4ßyÇ‘m×m4×T2‹Ž{¡ŽË<ã 0ÁôÓM4†$gSÊ—³(GD>xÖ•n©óŠ{ëžšñËÁøÑÎ(“¬8  >Ê”âûiÿ=qâK$‚ ,²K<ã<¶ãË}çÞï$ 4 ‚4ã1ŠH­ª'7ûï{ãŸüòþó # 0ÑÇŽßn‹A4x:çŽ;ÁÓ,À*,¦"„0ƒ $@A$3[{pCqF5ב\(sà ’ã¢6_¯Ûï‹O:É4œa%÷ß}ôÏ8 Êå,3ÏŽ0Òê<÷<1Ï b‚ 8à sÏ<1¸ ² ãà 0Ç,0ÏÒÇ4 3BS ë, TqÓY#ËŒòÊɭ<=¨³^·óÎðËòO>’0à ò‡4Æq¦S-´Ã=ô×E6à sÎ<óÂâÃ<Ëï,Ã5âI † ² rï¿Ï1ôÓARM$ÒI^pÃq Bzñ|‚ „×#ò0žÿÀ Ï<ñÏ €sÈ@óÀóÏ ð}ÃqÏ"ˆ'¾{ï‚ v}÷ß}‡ßp<ø@òGs¼%5Q¶äæï Ž *lœ_ª…SéP—ŠT§#)ÄÝÿÀ¡‹EX. ;‘F“ñEâø„›T“¥ýbl¬¯ÉYFÊÆÌ¬¯•ÖW VÆÙ_èÛ¥v•ÖÄç"ÊnÕ>A1~ƒ^Dâ,|]/ò~‚‰ðP‹Å? ‡Âðû ”›‡7J­Ÿ´„ê_´š‰©‘oé0þAø¾¡Òj)æ.™scè/¥.Œû ƒQBŠ”Œ 4“#ÃL€ÓDdcMvEÑrvÅà7ÀA!m4ÁPˆ6-H¸jÄ ®%C|9þÝî¯vÏ7Ò¹ªº¹ºÈMp„Ä&ˆB„!BhŒŒ„yg“±¶¦g`¥8”¥e+(¢Š(¢´ýiôÓËüÃz×AvÏMTꦩ¢t¿¼þ¡°<3›n*BHit³TÕË ÷_Hó̆¶&&™™¾ŠÇµ4LM¸M<¹ýh›O~é|°Îd4B„!4BšaB„!B„!B„! ˜B„!B„!Lò ëé?B ²”VQZBÊ(¢‹,¢ËÉe–Yz5–Yeìœ`‚****8 гÀàp8lruô‹Ò×=ÖºDƽ%³’ìT¤î‹¤Hcìií\p81sQÀ«MEEE)JŠŠŠŠŠŠŠŠŠŠŠ±QqÀàpÏ(ÞìÃé©J\R”¥ez.xïB„! ™®•é¥++)p¥)ZqKÖ/¡ÂzZ—øBöÓ}um¾^¿wŸ'±}*ææÿ~•Kع½µ8ú…þ ¾ääöÞGí¯—·5ßévïbæöÞ}‰ÑÞí7¦÷?]{4dd~†GàÁ^ð>ÁŸ#ä| ð|ð>ÃXQE…y+Φ¥ÃIAF('¨ªªª‚€2Q’F/‘ò>GÈù#ä|‘ð>Àø<x""""""ðEà‹<4ÜR”¥EEEE)KŠR”¥)JR”¿Å¯³]…Ó\ßo½ª¦„èæ«ØgdLÙ™š!ÈB¢Š(¢Š(¢‹/G²ö¸AAI$â‚ ‚" 0úCí©S_ímRíÒ”¹¥)vª)K²uËÏxÙe–YY«,¼B¼•ä¯%aJR²²éM‰ðE.®l\Ò›ÄÕ1uÒöÎìX¾>"é–ª(¯8ºh¢ŠòW’†Ù¨¥EEEECjwXVòØ„!Bˆ„&fÌxƒ]<Û™„&‰³B„&„!B„„!LDEˆp8tð„Þš'd½ÑK׿gZ?ÿÄ& !01@QaP`qAp¡‘ÿÚ?ú£Ü‘°“";’#·„!¸¡¸ÔÔÔäò""#a‚"#<³±>i™Ñ™™žaš‘Xufffffffffg”ýÍŸ ì~D숎Èå½»? hsÀù³áNIÛ¿M5#»~æ‘Û¿DrMy´æœÃüA©°ˆÔˆˆˆˆŽàøÒ45"4#B4""""6‘‘¡‘òFÃB"7šÍ"7¸ÔˆØ|É7š‘©°ÐçrñØ‘‚"#iÍÔÔˆkð'dDhsĈøsSžhsÍNa´å‘‚~ÐØjsM†„DDGfFÃCa©Ñß©ò6MHØ|¡‘´ÇjhDF¤sˆø³B5#a´Þn"#q¸ä‘¼ør#qÈ6ŸDD|†y‡˜y¿Pó~¯ÕúÐô¹ÎzŠR”f"">(ŽIÞcK1Ìgs=û3™hR”¥9ë©JR–ÅøÑŒsð8ñîïyŒg„÷ÅÕðÕ™œëãc:g¥ã›Ïìw8±žäÐÙר3Øâ"9œhp×:çšhDnÅ‹©ÉêøFf{;f:fÅŽì׫\á‹Á³9'{Œw:3Ù3µßžÙ‹ö5ãÃDgûqùq¤¬œqp?ÝŠÎn>ì6?lók YÎ ,¹ù™ÿO™¸=s.rñ%œÜ8Lç…œKÅùž&sÂÌ3pXx8qÍœ£‡ž«K„b1ˆŒF#‘Þ3:Lâq3ˆBq38ÑÄãBþü4řՙ]\ÎttgFfg3™ÌÎfhDlοïGêýí ¬ú±Ø ÌÌÌìfffffy¬ÌÌÌÌÌÎæfg˜ßÍ:>thάÌÌÌÌÌÌóò–,³™™™™™™™™™™™™™™™™Óù±¦:¿yÌrYtêÒfffffffffffffffflÅŒ“3£333333£«£:³37V¿Ïï9ŽÓ=uâÊg’ÌÌÌÌÌÌÌΆffffffy`333333363ÇtÍžœãN~zêÙÍz{uU@1Œc­Æ1Úÿ»÷~¯Üüß©yŸ™y½•UDôfþ?,tåšcáÏÆ¡¡ëc–éŽÓ øv~¯âëžv,vž œLâq«3Žc:;ßsÍ{gV^½v tÇdhYÅ !Æ0ñÇÄ|GÄuÌ#ŒcB„cîÿûòÿy”¥(ÌDFc1Ô#61ÑŒsú;r"""#C¦gVffffffffugÈ#R1ˆˆˆˆìzyçÄ3ß¿AYõ^¯¶õý·qôò5#S¶ÇØžf2ããñ8ïéúë<þŒÿ‹Fóü<¼ø>ÛŽ¸î^Åä?9Œý(ï]Üx}‡1ïáI(íèÞήç»#yŒÆc<×q8õ8ó‰Ç™ÇœN<ÞËÙ{¯uì½×º÷^ëݼ 5†ÈÎÜw€PÍAP–‡áÊU}÷¾÷Þûß{omí½·¶÷Þûß{o}ï½÷¶ö^ËÙ{'Ê|§>s9ó9œÎ|Ün<Ò3ô#çº?gt>Ô}­Ÿ‹uffffffgp!B©Œ/Åø¿Ÿ‹ñ°)JR”¹¥T@Í â¿3ñ/°6aB²ÎpÏjhhjn3ˆˆˆÜËxLc"1Žê+Ñz¯D|^«Ñz!â##ˆÄb1â1§×8³ƒ˜Gvˆx‡ˆx‡ˆx±Ó8tá3‰ÄâqÝ›MÏ ˜eã`9ÇPÏ轫vpÞ«Õz¯Eê½™ŒÆe”›ù\á¸<†fuyìâq£‹qñLÌÌÌÌÌÌÌÌê333:ŠR™™œÎg33™ÌæsÏ6Ÿ@"""9ç<æcíÿÄ* !A01Qa‘ñq@Ñáð¡ÁP±ÿÚ?'ž¤±ãy|˜ì®gsÃÔ§WÀ¾^ïwÏ<³›ô·™ãñÅñøò|6õ9<·ßé›Ï[!Óšíõg¹½Þûï3ÀboGœÃž÷›Üž‡¸—N{ðÎgq³žøødíí-½câZå¶Þ¯´êÌͳ)Æ3/{{½pÐèó:½ÞgÃ}ólê½/^ùåëÏme×èkô3Ç<ŒúšøÖÙxÓ‡IðÔëÝó=7» 9·¼-wy±œ-÷?hòw×öóKÜ÷Ý–KÄŒç½zg1Μodg‡ÏS‹a²Yà2oiÍy¤æ^»ÜÉÖÓžòÜ-´ÛHy¶ÛÍ·»m¤¼ÛmôÚ[ §5æóaõÍœð»oÐÏ ¶O ·éeîw<_!¾_=³×V÷|õç¾|Ïyï¿bÉ/}m÷âç=YÕ³NïÆC9 gÝë¸uX雯l^øflå¼õ±9'¿DFlϬûôž›z·Ã0¶sŸs‰g†ym¯=loL½ÛíµâÛ{È|vÛy¡im²–óm·Õ²Ûîí©n‡6Ûa–Û_Í«m¶Ûmv[a¶Ùmé¶Û -¶–¥¥¶Ã)£Ý---´¶[Ky¾G¯£“½Â=Ü86Yô>;ž>¸pž„Cig2;·¾zž?gÏWºo2yœ÷¾—ºña>Žk¸Ë1{Ôœ…§=3›Ïá\+m=‡ÂµüsøïÓ?)~¦Ê~†ÖrU»vænÝ©V­ZéŽq«E‰dŽXÙc—¾û½óÜm­©m¶ÚÂÛo6Ûm¶^ ­¶Ão7»m¶¶û¶Û}[m¶Ú•jü|¸8ãO bˆ,í¥¶8Ûx6Úsm¶Xæ¶Ûâ{MîËß=x™míé9l¢÷|wž¬Žo¾%¬~ÝßW¬ËÞGŽ,¤qç=l†sÕ¶ÌYg ¾,v ‘ß¡ï§2Ë,°ù,$,>Ø_9?ˆ¿A~ž'O±z>ÖþOryAޱcóbÇæÇŒ*×€Õ®ujU«V­[·nÕ¾1±±-XØÙ{ï¿ßw‡Ð÷Çx¬m½Ùm–Ûm¶Ûmµ†Ûm¶¶¶‡q»z[µnyž5 \ðlX„¶Ka#-÷)ÏY ÒÑá×6Îo ´Ë{¶Þ™8¢qð,îLXd¼fÓñdÞ»ñ<^»ÇÉ|N—¬—|}Zu{î~ÜõßyÞ6÷y¾;?rõÇÔ÷Õ……„…†Þ¬,$,X±&Í›09 ’ã,Xòº+Z•jÕ»Vºkñnß#·á2FÆÆÆÆÇñc"óß=Þù¯µµ¶ÙP¯®5jÕ»d%·o[èDG€3gœ¶,rÍÍ‹bÂZ[iÅø9£m©oÞí¤ýãÁ³‰Ìo}Âsm·Àñ|Y·w¯¤_/Ñ_6Ùψò{¼Ù|7͵·‡~<=xï‚ý?VÂÂË,X‘g†lyYá‹@×µj¯Å¿ÃjݯÃ0QÃV­5mã+Þë¼VÖÙmµµé¸M­ãVíZµÊíð¾[¸¿cÊIë{“Ã;–9qáå½ÞŠðgÏc›å°õy½Lñz¾o€ùíòócÏmñÛz}OqÍØy¶÷yóôŸ 9–Oã“ø/Ó~«õ_®v8‹ö»¶Ê/Yež¬I,³Ÿ<ø¶mŽ<Þ<`îÏž¯{žÇ2%g|ŽØÞël¦^ü™æÛáœ|×ÅñÎï–™äùë<ÛÔ¶Û8φñûùèyÎ_ÛÓ›þ‡máœNûȇêœ3ÄÏñÎo órNgžo ñoyeî^e€x§‰Å½K{Œ‘½åó?~g=ÏßÛgTã='Éï½½qyë:Çß›×ëoŽs|ôÿHýS9ž'Ÿ¾O owšg|—;Íê¬'7›à!á¹ pð=÷=y|ÃÓrËc6ú°‰Î©†œËtÞ{âw×7£+mò׺1™-ëž¼ ½÷-y–Úó|½ó;­¿Oߗφ6ø{ñ÷{îý ðß sÅæùovÛ_-æÛá½·»o›ÓÌq˜¾¼Kz)þƒx>^ûï™­†ðûœ,#ìs$<2Ë,õe½3|ȳ%9¶qoYÃÇýå¶8Û+^í½ß´¶ß:¾>þ‡Åóͽ[o>:Ûß ó^¡ÍúÙôGÇ: ùzóõž&^¼><’>Ï›és§ÐÏ îA}ü¶¼Gu^-–+Y={°ï®/3ž³VÇŸNxo7žý“õý[ô}ùlý Ÿ3é{âð·¾ãËçÅ¡‘ä[àÒ>óÝ‹:7½³ÍòÈÛ‘y‡6ܰ:¬Îô8–ËÇï‡vùñ 9³Ïž'޶–;œ~ýzç¯%æøï>';®K׋6úòÞ½$Ç»³ž_Äœ9¾aͽíõôýtq—Ë|Hÿ@'Ãß7ÃÜwßw|½x»áî<{®ß'áИ,LœÝ™ØlùØŸE­è#ëx[0óM<-ðØœ·¿<Î?M³Ã=iã¾~¼½xo†óeë™å¾o7Ï8}ÄîAäw< ܘDºúæx{è} çÁôîñ†8o3©toˆÏ}öžáz½Ïƒ$ÙeòÄóïÓ‰ÍÞ~ñ×è7²öÌ}þ û-¶G†sz¾)ô÷Ë|7¹×èzú'qðǹdIúGrßçÏ3èú²õÇ:Ùf<Ï/D>¸9‡†YÜúp̉0âÙàõ{³eçÃc–«ß˜=Èpæø³âMîÞ/†wâ׆Z’óC»‡«Ýó Åðm·Áç¯ó~¡þߎžcãïè¾A#ô‡ÈæA¾ÒÌàx{à+grÏPxdw,³†xúg1¶ "N6[eë,½qÈ"CžÆzφ¾¹§ÐÛ>§Çwý×Í8[zðõ9ž»²ø>>ù½|Î…ŸGƒÌ<÷~g†Yó;“âyqðßÏ ë×6Ë ù;–XY/=äg{ámŒðã|ÏÚ^{ðÛ[y¯v|_>Yï¬ògËo^y€¸ÉôÃÔ‡‹þŸÇÐ9¾g3¦ßt¡Ë< Û< æp<3è{سÀláÀó;“£ œ÷ßžÏvqzŽ¼Î¶Íï‰û›z}[Åò3…î~Åñߎ–óm·ßÑõ1âÃõ}øËÍñwýqŽýée‘ljã–x–s}d<3§‡®±g»,ÞxKãž¹ƒe’ ïž³Ÿ~èpyïrpá$¶ÚZÙã¼Þ=ù“Í&_§ï¦NñN ¼ØæÛÖx«ÜvËxùàø¿G,ðϤ}<ÿHyg‡¿ ޤ^¬á1æºpÈ·˜p=oû}÷…™(Áû1ÄŽ—˶KÛöd‘Éã³)½õô¾:óN –óÖÛôö>÷¿¨}ë>Äñ|ÓÀ;¾} ³ÈñÈ8ùg¯Gs©e‘õ½ägN<œÇyŒΣ{è;ÃÛø þïyc…ë#í,ûl’bsm½¯=K;'ïÌ÷ß>z÷m½ïåêßô¾¾®y¹Ÿ@Ϥù™<'ëœ÷8àç p-õ݇Äc„ž;ÆË& ݃™o6ÂõÏg§ÃÖLý¥÷:6x¾Ë ãœYfg­ëy§š|»½Ï·Éîy>>¹“ô_¦žYôsÀñ#¹ÀðÈú9Çç™âw:†s,î¼I&÷÷œÞÈèo̽ÀÁ̱Îg3¿?A¼õ“8°ï«ÖÛ6Ë{Îe²ã2Ù{Ù-†øéœßúkÑÉ”Ï^IÏ|Ùó  ðÏ ÿ@ð>†yçéAîygdöýœáãž랺õö÷"qƒ%óg gf2õ™c¼Å&ÇÁîE·ÄÙ>× ÑÞ<ÙçÅ„Kgx¾Zç}ùo6ÛÒ}åðÜdÀÏ~ àÛÁ–ß_GÑd;ŸQ^§™ôsÀ<³x=ÏàëzêYë‡3ÀŸ’Γ͋c˜ð³g2~ý2xXôÎg“W- xý¦}3!o¾hõËz¦q_]Þ¾l¹o6xí¼Ûl“>¼¶÷â(ý|ëgÓI³ÁòȰò ð,‹Žs9œ ,á¿G;žys8É/Ÿ~ìå“3é¶Îfs#‡™fsiežÉ×™#Ìž ÆYͲ}“n¾ d‹/Ú/s/‹ãïÃ<Öמï|Wžºógªgƒà?éÒ:Ï¡žÌ‹,æIâ>ï~Dåœ <Ç ’÷À€’Ïáİìag3Ä$ó ,³˜–Hp Ù8ÁpAélæ,,ólË,w˜­˜Ageù#x“¹Ä'oxs8–^áõ(ñàÃ>EïÁðß/^Ï»'Ñϧšý#Ä£žg€pN[ÍãžÍœ<}ß:ÙèðõôÏ >CÌó;–s9–Yij©Ygƒ– ,²ÎdI–w,³Ã8–^² 8YdiÀ3dp–Ye–0HÙ̽Àð=,Á#–Ë" ,Ȉ ² $² ÷­–{,· xÏŗϽä—ÎM‹6s’FC¡+#'·›oÏÑØ{ž¼¾>–pû[å½Ë;ñç†yå<2Îg™Ô²Ë,°ñÎ ³™%’YÀ²ežì³Œ,ã $b%œÃe–Xì‰eaÀõ1 !sØ“,²È,²É‚9žà˜ =Aa+Ìî6XAİ™$³™ßY82ÏÝÉ› ²~у>ØxØfÀiÌy¶õ›Õ¸[>׿'<÷¤Ÿé²>Ñg–YgŽs9œÉ,ƒÃ8K yžNYgC%–Ad ,ã ‚ ,æYe’zK$²ÆȈÙe–YîÈ9–XYÒAd)‚K,,Þ%îø‚ =@p/‚û°YbØY)azûp8š[!%’{³Ð’9z3¼r[cÂE˜ &2g†ygo»æ~‚^ü ½Càwaâ6Yä /©grË,³¬Ùd¹e—ÆYÀàYÌ‚ ,8—ÆdAd^¡d˜+e–XÀYe‡âG§rÈ,²É ÂË/v{²Ë#X팖;ÀdlÖøîI%ñgÀX c3XÌlî2YÔFDl™€½Le“+– '«4âHBt²OwÌÙ&ØIÍ‹gÌòùú;ÜŸõáë§ÐÏ¡–s,æw æYg‚w,ò ²±${ƒÙYep8B²lõ‘íÜ,,l‰|É›°!вÈ,æ;g2H$±^¶Y&Hï1`’F$¾É,ë÷½á¬Yù€ ,,õ„z±‘³`±gfdlËædwÜ“¦–O¶lrÏ]&<Ôö,Ùe÷`Ÿ²–3ë¼gÏ<7ç8sçÄîÙÃÇm<™ÖÎå™eŽt<Ì<²Îg´ –xe0²H™Øæh¼vXìI07l`±‚Ïvuœe–`@{‚G`Ë “Œ²K,²Ë ’I æIpH=É,’, : ÷aÆó6F9‹Ä6Ü/Ÿzó8cÌ›,ê+'3Þ°{‘‚Ìæ¶ÚÏ=í¶OÓøñõ׿> –wÖwlàO³‰;g»,l‚ ²H,ƒØœÈ$²Ë0±ƒ„CàîÁežä²Ë,²ÏRŒ+ d‚f{’l²Ë,xÎeŒ"@É–¬°’ ã$Û$µbXY{Ø [=zlà_öÆÉ ŸÌß<ËlôÉ–,ááæ<27ÂÉ-e–Ûg3Äž-îùÄf>׸ç¼áô#™g3™ÌàhÙÀ²Ë™dË,lv#6 ² ,³[î²Ë,² ²HÀ²Y¢ I© 2LÔ ’ $wŒ÷d,ƒ[=Øs=È౓Œƒ=³°IxžøË$‚L,w ²É™3 ™e–L™Œ–Xd–Aб˜9ÈË/m–;›ì‘Û=É9– !’Y'»Õ–s8’{“gŽóo|Ï/OãÜáõ‹/PxˆYÈ,÷ãœÎg¾&6A&8ÎYeŒCFYe‹eŽÙ@„‘ ,vF4Èežˆ ²}¬ƒK ƒì`,ÖÈ' …–G(A$AdÉî"g$+$d²É"$LM²K=ì …–ZõÀf ’Æ²È l°Ø ’2s 9Œý¬›ÚIÅÓ’kÄ~9³16-„ÌÉè'l'W,z[>:ù½ÞýE<Ë<õt±Žo2È<2Ï ’ÎfYÀs˜É ’:>YìàI‘1€‚É Â l,‚È!G£0ãÀY6Y‰jû¢$D€ÖRDBÆÁ#KdÉîqá $•>ÐXOµ÷HY8AdgË,‚ȉ¹’XõÝ’ÂÌæXY…Šo»æõ¶dž¢bÈ}9}ý:Ú$’ÇlÀúd}À,>ŽF{ÇÀæxË0àYe–tá¼ ² ‚ËîXÙdLƒd{²Ë> ²OÚ ² ²pXlXAah²&@É 6¨™ž¤²È Èœû,ˆžØ‰¸Žˆ@ØYc:`Ã$²ÍÒÇ é"aÆA%“Á,,‚Ë,` vÍl2Îe˜w=[9±!g±²Ë,$â Y%œA²ù‚ÂÉ1œÙÞa|ý3éøz}9ž¼,áÅ ÛÅîÆ×cÀ±¶2ض%Œ—âÖÄäÒ%Ö>Ó³¹ÎdhfÏAó°LI¯d·¨;À“ àIî lxŒ€€°‰HAêÏP?™=Ì #Re†K=Ú7¯ ,‚Ïvdœ0Y#iÏH,/³Óe« ‘ƒÜØu,²GdƒÓa&ÉÌsï#¾¤l ÀéØ4“Üê32Ë,÷a‚Ë gÁe–ß?èrzú‡‘z˜¥¾4aƒø‚dÿÁŸÈæÿ+öx÷W÷WàNGèàþBýïá?ðz/üâ~ǯãƒøÏéþ3ø?†1zëôÃø)üý_£ÿ©ü?Ê~¿äß§ù7ø§è‡ñ—õßÔÏðgëÿÕ¿‰þôLkñÏÛü'ó×ì§óCñ°þH?4?Ÿé6?'ò¿[øoëðÿ4~øþyú#—ÅùGò_Œ7ë3ø‘gð§á¯ç߉ØüïÙ¿v~ef}÷ìÇæ_³“a÷„?UøNߪý¥@úRÍxÐCMN\ @­ë3#ŠáÊå÷Ù~áöÂf—®ûáQ —’Lg©1[“^X$2Rì™ÆËí<ÃuÞ$HXÙf¾› #$,’<^—«Õñï…Ÿ\ñ¡>>Ðt §5Øâ¶¶ÚÛ{½Êçm¶}ž; ©ó¥¾­”¶Ûm–Ùo¹%÷¼ññk£¿$n[îŲ¿¯»jKkk/©› îË¥²—û›ì?‚Oþ'ð§ñ¥~â~æ,ù ô'Þb˜[-ö—¡9}‰ûx¶Dm?y$l…„Ék+ /\ÉÁLœ¥|dÃé=Ii²½¾,NˆM}¦÷bþ&¿)ý[_*FOá±øgñ2 îö3Ã2Åx2ÁÆÓäÕ~„ ñ‡ùŒ~?™õÌ[Í,ÛÆ)³ò¼ÿk;ç nKÈgñcñ!g È^‰ŸZÛ& Q“ˆXmŒæMšY!í°ëaŸ?~/7Ë^{³Ç<Yˆ­’ñß ²È½Y|A!e’9Ïöyžì²~ÖIïl“¹ñcŸiØ °Ì,×ïø’ÏV{µdûžd†a»}²È²'‰îø±œâ{æ@½s,—c¯¶Iÿü’I$²H É,d’GllcdžÕ‘È Ñ˜ÏE‰ŒC%€à$“Žcd€’I0³‰d–Než ÚÚÛù7ìŸÈæ:ýËö/ØŸÍ¿bù  T?|@̰ 6§`ùx͆ÞþÇ ²w$/Af­öáæ6ó/v^¢Nc{òË<3À½A©3 ³Ä& 8YÔxØs8ž»–;Àã4Û=Ø%ŸâBù6{?Éež ²É0°°Ù=Y>Ë}„ð7/ŒÔž|ý‰M “ÜœÍy™'­’I$™ œâNd¯^ ³d÷bd“xÏSîfFɉé²I$’Ë$°‘É $‚lãÆNú°“ÅAlGÞŒ¼îʧvöñ’L’{²I$’L“K&Elld²NgCÃ,“¯ƒ¼ÉãÌõà†’(V±i™î6û÷ …’0!g2K$ÛIßçÏž§Ë;Îå“Ü;ŒDÔQØ l˜8A{,ðõežg’O ½x»’Y'¦:züØã$çy}ù‰Ÿ÷1ahÀ‡å-<=…íÇ©¯öXû˜õ󗮨üßkX@ùoV ÆðHLGâ$TG6ÄB`41ö%;>cNlžÙ'îDÂP…ÁÈTÿ„3æ1Cd‹P†Š?¢$(kP8¬ 7ù„ ¤ /a/þIObëp‚Õáã÷$S3 OÃ'úN?IBöƒ ¸lQ}±ð‘A…¡a7ð±O{’&¿†ÌŒŒ’IdÙêI$‘±$,÷²;e–YÀ±°ÉãÄñsÅâõ’Àg­>ÇÒÀ\³E3ž› GÙ#HH’Agû‹NûñÎFX—ûÛÓGd³Üm:$ù…ù8Þ‡÷ ·ãÿÔƒ=úÂ(0¯æ×Þ09zm=¿&G·Œ„ƒ¦£Ö;àA~fùó/Ÿv1ü&"±$Éc³ûQ߯ÿí{â—ù×嘡ãi¿ºø„l“܆p$™/~äd'd°„’IgÆG,âx#3Æ~ó3’õãÏW¨•iöM/\´°BÀ'm€²MN#“éÇ ³ÜŽÏ“ô#¹Ìó0úïwÛžÛßQŸm˜àYÜ,æA&<|Îç>~’zŒ³˜Èß3Í› dîYïM!QbØcí!Ñ“tN g½z‡¦4ÖÏ÷ÿìßvXkX_åñiÒLOà‹{ ›¤i’…£wØNÍðìSÖÃc«×ÚÏK(tý’…î~ ŸîØðå¢ I¦·ä´Å£hO–\ðXŽÜ6|)!†qŸ¹s؈bîQ!òØÃõßî˜T}·5 _J÷þ"ù‚8úž‹ƒ‘Yßà˜sò^Æ1™‡µÇ¬ÛÅUîIÂI$›$’HÉÌ’CîÉ%’Hs Ë$÷ôFϯ †Zw‘IH d sdvË6‡£²ûË>Yk²' mñߥŸ_ Û,²7{ö­)àp,óÉ‚õzýZ=O\læs,¾,ç­âY6N÷$²y—ÜØí–HÙcgHXY!’ag½$1±‘³„Z²ÖÉ}Ì…Y gÚa¦YÜHä‡t¸7¡3ÙènÚÉ$ßWM•÷¬ÁECò„ª¯òaô—Øé,Š/b2üuñûLއЉ2IË$Ì’;%’I#$–É “&Õœ’fxç3ö²O ìƒíêr÷3€X̆²@s=ÎìˆHï€ÙÝ“Õê<™>Òß éœ̳™Ìbz?Q8r`³ÀñÇóg2Îg™a–Yg1³¹Ì²Îå–q‚ÆÈ ²ÍÂÉ3™#ãŽó$`$’É 6K,±™ˆœ#f1˜]IÄ–Hž¸E“€EK$ÖBOÔ–IîN'¾ ¹d’Y$Œ†ÈìŽñ±$K$Öx›'^ë!922 ‰îFw…s‡po~^¼3À³‰g®slÿQáÐñÉ/Ž{ëÆ9rÎ%–c×z–He–Ydž¬ŸÆ1¹à–9 É%Žó ðxÉîKâOMžä²É³Ôd$‡2L„„Ÿd„–)c"YÆIIdÌä—Ù1,÷ q$’OVIg$ŸDîYÔž2O^<Î2> Ì(óH`@àð„Àæx²aÌ9¿¨\½?WÜ/¤Èá¼8sÞøž!7®zç®gC…ŸAûqëăž»žìG6 2~\lrs™Ô/S6MŽÙ${lã©d†I!¼ÉƒIßr^¸„ŠÉ…’'?ÜI0‘ËÛá‘’Iæ32ùdvÙ%ùË ØŽ^‰³ç‰gïŒ÷ܳÄy¾eœs§Ñ T€ðÉý“Üã§Dc…ÞxÛl « ³0‰1Í-ðH×ߪ֙â¶ÎÆwEé4²I/M4FËa‡Ød|¡AÝæHCßy?ke·‹ï8Îw÷zÒL8³ŸfùÙ½$ó œŒ™Ü°ÃÔ $'Œ†ÌÃ9½.8Ùd„’+3&l™,$rÉ,²d“Ü’2X„ñ‘²N;;âýü&õjþ[ydØCÔw‰¢²qœC~Ül“½ß‰<\ÞzŒ[/vs ç¯289ÀçÁÌîYœËô&Ë8¢A'뀙ˆ>ä¾€LÆG²MX>þ )¦i¤YáÌJ ’KʹB0ºW·ƒ>müO½ŠÐSN> ,ÃÜ,}ý¾Å™gô¬°Ÿ`tœþæCþñ´ü~¬>ùˆ \ÇŰÓCòAn†GìáíBasÔGæv¾ßvCßòÿÂWDF覹{S÷;òZ£€¹>èôb‡­@ûd² }èôÐÍ}½}>öwB#Š1ûHÿšô,ö{?(-„p}þ$¾ ¸˜R Àj&‚X¸ÿ¸–gñ¢ñ¶çÛšål¿,\lgDZfAÅѱÇâ‚=#í8¶».Dí쟺¸îÊâ^ÂyïYÉ–Ux÷: '\/_ìõìˆ`¨Kj7Ø/Gý¨V¢YÞRÚ³˜Ü’Ig©$á,HIgÉ ,™²SâÌφqLâsÙøæu½„XC›$–`ñ\g2E“…¸Üñ>¦YâøˆèsÞÅžúÁþæÞøÏ«j?´ûÁ8 mž¾m%öR¯a‡[=|Ìü¤,NÌ9Èe¯ª¤:Ò Âû>è‚ù´>þÚ?›L_sï0ží7å‘÷¦îh?Rc¼`3žäûŸË-?JL€¹šgó52¡WÛj—Åû%þ›ü‚ý¨ˆz}ÆM¯ÚÖ^ÜHÜp/MûL¡{Óý’ œ…'È{b|)ø‡ÙÙK¢~X¼¯òÏ’üªßÌÿüKý¦§—ìoµSêîÿ/wØ ô!G gùÙÿ0.zcþÃ$P`1ú b97¼ößàìsö1z9oê¾yÿÁÖD&)f_ üêϧKuÓ%ñLø]6A÷òMÄlâ[özûÍœ…¾Æ`Ã&©ÍD…€Yˆ0…þË‘ >!zV('؆ ‡á¬A“í^#³èåÿ$CôCýlò \“~=2‹©Ëû·&ø!òÛYð§Ts½7åVT¾ÀÔ›ðÇô#ßý˜~‹_Îê!¯÷B[ñ«›ú¹þ ? ‘ïò._ìqóúû½Ïþ}£i­}ßÀX_5Æ |¿vLþâÕnQü˜ßyjZ}§Þµ™ÂhÞIê{‡ÊOñ9>ÿDÑþ'Éóþœ%ßéñ›+,¾Ë6FN%¯C‰[Ù—êúâ$?k`™OÄZ{ÏܳAðj6g¥™ÜçÉ ŽŒÏðCˆÕ¨–þ}rÛ >ÊÌõ™çó XÿŸÜ¦÷¶7Û4FÂÞ_è½.Óü'6½íìËÒ6ôŸØ ?˜‰ÈäÙÿÃðAýîgÝ2:Æn™.4z`Ù4_cþ="DÒz6}}ž¾3 OµÜûå𞇺öÊFönÞƒòm—cÙöÅ›~î›ó逗ÿìL¿L`ýì™t?äX‚‡±özlý‡î#ûc¿m'»ç—ÞO¿¬ƒô,°àdô™'ÜáÛ[Cúƒ•C£Ú-4IýŸ¸†".Ÿï E¾·ÛØzÞÅÊ:÷–'ÿ¦ÖíŸlz…ìnz0Gñk%°cìп&|GÕ]èGßôŒ†uû´qêV·gû í¾Ïwûi»¯ÇÄ['®¾Í~-£jƯoÄ^Ï=ÿö1T«  '?°?¦Fì®=û´Ÿ¡þîŸäd|á½~V¿}úä)WøÙÕûõ‚Âüó=[øòƒÿ1÷Œþ/Ū½cþŸÿ‰L"É““g d’pIcgFI1’d“Üžä’I6Iã$ä–O‰ÏÝCÜ5'Âq½íè½õ„OÜ‘g2ÉË=ÈY-c,éÓÅò æxÀá¶XeØXq,æxds$°µLV0²À²Ë,˜Æ>È$°æXÛ,õd„¥`dCV|g ç¹$‰,,X¿,°Ë ,‚K1‘l ‘dc!¶C…[ßµÀ/fÿñHsC)éî ”Ì!:‡ä!Ù:¿ƒ„“«iŽ¿È,ÃW-nˆßÂþX– ` ôOàI'¨ÌÀ 33Ò"#9ûÿÛû’žØÄC  ¾ùmÇØ¢(øô‰4±}ÕþEdÒøÝ5‹r‰1ž’ÈU*ûUU*Êù÷aµ‰öõî¦ßÊ-£ŠUÊUûɬ’IÆvÃgÛa–I$–{°“܀͒LŒÈÌÌœdæLäñE¤XDË0d,l±“,õc'£&ÆÆL±ËvÇcÀeQ|pmò9ñoC¹ÌéeG}"<³dîYÌç«ÕÎç†^¼0âXx'Îç·R÷dø£$ œBÈ$öß{$æc%’Idd–6I‚Éé’NVW 95‘0]OI:dý‚ÌFYød²Ož!6,<2I,õdÉÄ'wd‘ãá÷{²D[ :ˆ÷dé>ÞÜ4ãø9ïÇ :`Þ»7¾¸fÏÝ‚8GÜ‹: ,?Ë| æxd°ôñxÛôž=cÏß,õXÏŽeòßx;Çw5æ2q $ÆrÏRöûYêN¥„ïÔÈI¼Ç 1=pÍXä’$’XI;®OÞ’I&ÉI$&Bg6ÉŸ¼“)=d™</Ú±úÈ ÖõMˆ¡lÙ$öBCã&Dâ{ÒMU‘™',?1G{“Î[˜½p‹,àE¿ÉióœÎ)†I«º"#Â[Ô0ø{Ž~Ä9¤ án(Ú[zñÓÀ˜eà–>Üõd‘äæÉùgðIûYÌê2' –q°’ɳÖI Y%'ŒØñã“Çs ’É $÷#„›$l8gˆä˜IÄ‘’HHI3×Áãàß°…î Ö.—¦z BC5âžÑX$$e÷·ät†p†?$ñê;˜Œè~Ü’bÜbÐ2ÊŒ½¿†3ü†ÿ6™¡ðÁ„0`^à¯I6ÈýÙ ä88¾KÆG,²Ï—„øÏâIdó&~}IîBÙ3™dð„–L’*Í’2Y$Øä–L’{œ’BOr²Ie’HÉ÷9ðœÇ©'2gl’K=Mþ/­¦cî÷8 $²B"Y{ã9–I!ÂÒxg‰Ìázð8æAÌ›þöGðž$’zàJÙ&¬fÊ“ñl>ÒïÓg?˜æŸ?q€¿»‰·²GæeêÆ?`âžÂ;>Ù"&ÍèBf…‹*}Ì#cZI5Ñ6d.úIø²ûŒÜø&×ÂItüØ”ý4Ìl‚HÌïÓ*!ϼ@€X!ßÙë#s?ú Õ¡û|ûËkz…ü-ï_çü„Të¶wß‹ÌîûŸ 8M…†îuñd’}3²k!Id“1lâYéK=ØÉ9³$·¹öàÌšHÙd–#$“VHÍŒ…îOl“ö²d$’I’ù™!×ÅâÏÖFµ÷‰èÒR{àHd‹;œÏ€ :ϳ#Ÿg7˜øoÄ΃è„s8IêõÛâ%_ÒsŒ–©"ŠËj6a‚ÈjÆà`ÏWÞ÷ ~þÒrj²Õô¿¥`ðm,–‘èÆ|Î’t à ÛW`3‡äþ`³;Ý}ñØ8íú¶ËÐzäû›«’N9û-‡îÁ/ëùÆNùP²:O§÷ ,ë- z½Gs‰Ì²ÉæužÆõ99|Ï›$bzœ~ìñ’I,žz’Kåã)'Ë8…’I,‘â3e’äÛ"psIöó.FW[g%ö³½7<cdÀ÷œ3†p‚pAÃÛÍí=ÏâNä<ÓÅ~qãÿãõ‡ÿÔ|\úømêy‡:ûxÿ»ç†ß×/è—ôhÿ¬_øwõú ø?‹L¿®Gýþ¡ öþ±ëøö÷öÒþ "Ãã$ƒOXØÙ*×âÖ3ù“ù“ùgî_½?~¦Iû6¿ ­Ü•kñ*×ã‰ežì²Ë/[z°üÏ2K$âIdƒ!aa$‰ôèúp©Yß³&0ŠšÖ1S’%J“Öð’I+„âHÙd„ž¦Ip‘âd–I%ïgžç¹û(¤}ßf©j—ÆXï3‹Ädœï¾%„ñð #¹g —«:GÚ„p,ƒ€Á°Á#/X&ø5sH.A®?ï O‡ÿVõÿ•³ó"1ó:,=Þøú<¹ìRoô?ø¿«ÿÅùÿ·×ñâ2÷üOøŸø?ñ7ñ?â?é×ôKûü?/ñÃþ¦ô|9ú!ÿ°¾ihÿ ~_áÿÌÕæôáü?ù¿7ð?æ>?ý?ÌÞ?æìóz€þ¡áF¾ü+¤Õe}ßþôÿ´¿ùÿ,~_ûýYÿÇÿÉ_óÿ¢þçüQø¿úý_¥gáò¨ÿµñ$çõ¥!ÉqhÿŽkn[þÖ˜ßÁ+ÿg´¯ÀüSðŸÃþsÿˆÿý¿¦ÌÿÓ?æéóèÿÍý·þgñ¿ŸüߣÿÝúÏþoÉü)üJšOüxî÷·Ê£òâ?û³ÿ‹?ƒÿÛÛïÿõ7>üÜÆæOæ_¿oåÊùR¿)”úýóZ‘ùHùsù3ì×?›?‘6~e-ðÊü3ÎZ+KÔ+$ÀàO¶Û0ôDÂ%ñ ±9Üd±ËçˆììÇwŽÃŒ[ôgOñÈà@ô/EÃc5ÂÎ-¯87î»Y ~Ä}¯ßœqÒ®æ×l7ŸÑ§þµC”ûøIOí¯üÍýÜþãÁŸ‰ÕŸþÏ¢Æ,®»È¾~¸~/þ×î§¾¿o tÈØ3/Ößúo&¾»à°Äþ¡ÄÏéÿóOÿ™ü?‹ÿ7þÇþm~ÇþoýOüߣåªgÖ¿½_߯îþiH§póýmý„ý§þ~¯ï¿ãyÿÿÿåÿ‹ÿòÿÌÿùêÿü¿þ/Õÿ¤ÿòÿÙùà¿ü¿³ÿÅýöþÕ~ßä¿|þ»üþçñ°û/ãôì;¬ûÏ6±…æ:Ñc<èÞlÆIÿ¾OýöþÛ`¿²HÿŸ?öyÿ¾ßŸù‹4H­-:ýçü*þ["Ô}‘0‰ÚÌÊõ ã½ùN:ƒ2Æö8g}ÅœD1Àáâ}¯˜"8.Do"Î>›Oè™üC ?óeœ,{–XÙáÏ!ð:7ží·è,Î_<<0½X[̽wÖx…êØXXXY!̲ÂË<2Ë,°°°°²Ë,²ËH°²ÅŽ1bʼn9G˜“’¤d-¯Ë##ùl,þL¨ü~Í à®0é7»%ø×ñ|÷KÔA€IÝÁœÉ=d’93—¬xç&N9d‹»ØèxÈ[]ák8DFÁ¶AêÈÌ’Ð/½ïÞ!‡ñ¸{KÐÌË ²È,,³¹ae–w æeœÎg†Y„XYe–AÀƒ™ï‰ã’w9‡0æYaÌ,³Ã,æYe…–Yg–Ye–YeË,³Àgˆ3<¦ë…J•jT©sU*\©s5kz7ͤžê6ÓÖ÷_i‡é„Ë $âñxÉdÛ9»Å¶ož³­œ"<‹Ü FìY­laˆ åŽÇ/P½ aþX{†íže–Ye–Y̳™dÙe–Ye–Ye–e–Y„„—« ,,‰†YîË=Ùe’Ie–X6Y!a̲Ëež¬à8Ë&e–q–YeœË,²Ë,³Œllã,’Ë$’Ï–Yd–¬˜ÇƒÉSÃ|c²¦.Ò¸}á„Éix5½$},:–ÍœNlæ0KeóÄ€Wv~ÜSÓÙCÔt'ϱޅ„¨8Fd{`² $,‚8úæG(z^Ì’Ye2Ë8Î2Ë,³Œñ Ad–2s9–¬‚d–q„Y38sv6@ÈÙcce–LþË‘U÷#r¢OïöŠB6s9–Ye–Ye–;%–Ye–Ye–Ye–xg2ϲÎe–Ye–Ye‡ƒÁàL„ûÐtãÌÿÆ9ì˜ ²ë.DìNÈdîÙïæ_,¬ý­xËÀ²,áâYÓl²Èñ8tˆmXà2X„=¯Q$Sý’aÿ,‰$ 9–p,²ÂË,³™ôrË ,³g3€…œË,æ{ƒe–XYad›e‡2É/@¨~fþ$8–XYe–Aá–Ye–Ya'†Y!!e–Yaae–Y%“2Ë'†LΉÑ1`#öÞð™/ß|ìýÃSú< e¥òÏå&vÞ|õŸM³œÞ}ÜÎzáäp¾HéÞ¶9‘ ²pXÙ„‰aÒ<ÿ™è/_Ø8–s,²È,²Ë,³†.Y'Ë ,²È,“Œ¾ÆK ‚É,±²Ë ƒŒ³Ý­…e–Yg e¥öàWÝ€ÑíVýˆ‹áe–tÎÕ–Yce–s,±²ÆÎe–Ye–s,²Ë,êYe–Ycd–q$’Ë,²I$ž3²ÖÆä¯‡òZ/Ýî/T CÕšIý¥´Œ±ÆNc²¶Êdç0Ìw…œ?~ÏžoË#Àƒ¤dÁ‡…ØŽáÅ¥–s¼?® s÷Ø€7æ¦gˆe–tË:WæÎšðè+V¥q’¼À:÷G³¦¢™Æ­Yc ËV­Zðé–ºccÆYe–Y̲Ë,±æYccc{Þãcg2Ë;ÎåœK$²Ë,ã$’Ë,¦Ú߸{ë?ÌŸìzœˆ> 2GÁ>òKïŽÞÆlç©É‘“ ß–C¤Ƚøbp,é¼Ïq yšÁ’Éå}¿¹Ö'OÊ!ÕŸ¦t_×è¨ôåá®çSZø1 »çjûãÚtkbñ)9ï¿ û×ÄøáúŽøƒ,ÿ@ò<ÀÇ^;ˆÿ kgÛ“Ç;î_‚žÉ½Ãz °o³fIã,²·©Î{²^áÆÖË|d6Ξd^£7xö"‚ôp#Ù9`Ÿ»Ù:FCï¥ôŒãÞû¡…·² ,°²Ë,YËYeYcŒqŽ1bÅ‹,Xã1Ó,‚Ë8Ë,²Ë,²Ë,²Ë,³Œ²Ë,²Ë,é‹,Xé2sÝèÐsÎçêM§Á7ä ­ü@ç}Øaßò þ`_ÒÒØû áðVôqùg‚Bõ¹Çx¥¥ítêq†å’Î|A,È8pË=@ežà€½@n@XDFA±¸85_¹êÇ œ£ïæÖIœ¿™÷™}ÿ›úÚøþãïõ×úkýw†¡öû‰ý§DQýâþóy¿¸}J³…÷8wÿFßý{ÿCâoüMŸúöéØÿ«A¿Gøß­üoÒðï~ïÓ%ß7UEpawû»ýÝþÃþwé¿÷÷ïÿýoüïèÿç__ûÿoýÇÿ·þëÿÛÿ5ÿíýGþoê?ó\ðÆwúoôq_ÛÙü¢I¢³ý»ý‹þï>þ¼þœþ…þ·†›ü_?ò·þÿÈßùXúækZÂGW´°iÿPáDûMŽø_o{ÃçñrôßãsO_¯ãͲ/amYÂá,ÿ(k1–€Â$°ÉãÅv@v`3Ï•’u™!aÇ#Ê8Ãàgá–G„ÁÀ,‹,v‘…=’Øá·¤Ëݹü¸¡ÿyÁÌ80~=ÁÂ9ÒßÚïï÷÷ßÜ¢Ê2œágÝ÷þ+îûÿÿ­#?Á/Ëÿ„þÉ7åü‘*oËùfüž™C>ÿŠäœøÛŸ)€8’ÉÊxÖ~J˜çŽÐ<Â×›¾Ç³Ïþ#ÿ÷•?ú/ÿØ.üÑ?—ðoüOo°}ä£ó2ü³6_Ž'ç‰ùù¬þ?࿟øçô•˜üÿÏ»øÓùÜ?r'çäü‘œ-v=áR¾*¦Ð?öéæRþüWþ»?¯Ïè\|PsòLeŒsõÙúîÜþzÍSùê?ò§ã ë©ÿ ÿŒÿÕSÿ¡ÿü¿þ×m¡²lŸ‚Çá¹üßù„á>Ù¦§±„&e›oÎÛc÷dw¾Ò¤Ù —ŒñÕ³ž™mç¾c‰Ç¾ïsÇ,xg8‘d9Î0d,†KÕaæZ8Þí½æóaîÛm¼ÒÛm;¶óx¿ANo^>Ü-¶Û|›Í›ß=çÐg,œ½NYzã—©ËÕƒ X~ Ô„†ý‰Áaø$‰tø$ý±ø_¥:>‰ü)üIü)ÙŸÁŸÁ˜”_Yüõgðçðgðgð'ðçñ'ðgàøø3øsø3ø³ø“ø“øsøÓø“øÉ³øçìåƒöY â[~iO¾í œ”²Ó I¾e²lŸDÛŸr2M›^lßcg1‚"Hœ¼"8nṏ8G¦ ÷{‚"s'‰û!Ljîý-ï¼¶ß-¶Þov8îÍ·½Ýë¼[yñ93âø½xý]ðɶÞ;{ðÝõ{ÉepêË,ñgîõÅ—ÔÊqI~ I”Î3,§™ÙâÊ“öexÌñ™âÈx,Ð̻ȸ^‡˜Î'2w­¦ÛÌŒËßžòxœ ² ü>ðpád錉á’b6¯Ë yp³„>KÏ]Øñß:ø¼g‰=ȱÎ6<|}øûâ} ós—©êñ—Á¶Ix³œgN3¹dì¼fQxËÇq™™Y—[æ^’BYâUp¾Ëý³ö4*“½–Ð™Ðæ³‹i²±2²ÆÁ•8gL°Øï¿X/– ‚‹#ïdz²øb¾¬2 #‡ o`¢”.JcÌ·ƒäÞí¶ôúCãë<_ ñï¢ÒùŸ /_Eózï^ü6"ÃN†ÎdDø Ds ~ì AÄct?ƒ£äp9ŸT3é»õ=Î}\õ×Éæø7¹eãÅ%ãY–SŠKl²Ú²–už+²³,¬³3’’Î,²Ë)+,¾¥™vÔ–e–VÞ<=£¿Ïþ#B`ÌI}Ÿ=!„6M·­²Gmelëœ^n³<÷ Ë#ïâ8^à³fÄp 9–1Àá¹ÂÇ x1¶Ad‘!væEø2|p¸/è›ñôMù?‰Ÿú&?é˜ÿªoè˜ÿªoë˜ÿ®oëúÆþº?éúëúÆþ¡¿¤oèúFüÀÙù?‹?'ðßÐ0¿ ûÃkðÿ ¯ËølO‡ødðØþá“ôÿ áþ?Oñ³ü_ìÞ¯VÊo1i¶ÚX´ùßá´ýÿ ¿·ðسùÿãËöOÌÞÿbGÞ#ð' D7Øßp½žÿ¿±'þÀ¿²¾þ…ýZþ‘?$¿©^­ÿæŸú…ýrþ¡?õ‹úÅýBêŸÿ‚ê—õ úÿÐ'þ?õéÿ­OÉü)ûŸüS®ÿñOýJèSÿPŸúƒÿàŸ»ÿÉ?ð'þ¹LŸûbWOþÄÿØöDÿØ÷DÿÜéü“¥#à$üKöI?’O´K™9&ÆúIxYŒG·e•˜²²Ëk²²Ë,¤±ýoÉÿ ',÷ð»zg á‡FmÞXrmãÍ⇢Ä7›êÙKåêY„¤žôîÌÁ'éÏžg–XY‘¼ÆÈ Ál÷‘À"ÎHºäq/Yþê÷ïÿ€ÔMÏnLÅPº˜€„UFçýÕýÛ}}d¿¹oî[û–þér¿µ_ßïïW÷+ûµýÚþót¿ºoï·÷Ûûíý¾þËb¿³ßÛïïwö»ûÝý²þùi¿µßÙoì¾G|çÝþ7öü7ô/êOÔà?Wøß¿üoÜþgþMý¯æþßókù¿½þ×úŸ£ô{ÿ×—Hý‹ý¹ýèý™ýŸïé_èßéßéOëOéÇèÏéÏêÇôËúµý þ¿\¿¨_Ö/é·ôûú=ýFþ£Öïúχíç¼Óÿ¡ßÖ§ÿà ˜ÚB„â£ÁWíÏÕb¡zIÍ”½Þ;j–ù)Íáë¿÷K{^|pàC°0X°idAdmXÇcÖ 8p ½„ÏTPo%–áÃŽ…j+[ å¾7o–­[…jš~)ËÙÚýQáUëäVöÛ‡løçŽwø·ÈŸ‹[öµøš~žc~‹ôÏà"Ï?U·Åúïƒ/Ñàƒ¬ËLjÕ«Vòk‡°Ç¢#ö-~_”½û7ì­~-~%þ†Üþ'’áïÚ_âÖý¥þ-~%þ-ËüJ›Ï´ò›üOÁøŸÁ?ýSø§”å=]K·.]»PÙf[?‹²Ï d8æOÖ^ÂÉÉ–Ålxñ’_\Î"<Âö1g=ÁÃíÀ-½¬pÏYÛ Aa°Yë¼±‚#b˲C"Ûâšú{ ÂÄþ–³ÑÃúßã/ï2ü_®¤Äûõ…þ6Â5ð’LsÔüAëž³Šin¤õ{æ“10æMªÎÀ>›9êÞ®<Ðl¶""3è¾È#,€²ÆÌá¼,/œ`Ž D olp7ƒæìéŠÃ‘¯ŽCøƒØÎÐ…0`ؘüA³¥OÛ‚Çà±ø“ø¿Ä_a ì^sfcñr,·¿o›,Y³Ÿh‚ÅŸÁãUŸ‚DVl¶,ÙüY“bÍşņ͉ ›g¡?ŽSݧ~ܹ‘fNY±&ýRdçÚÏâOâÌþ’l6d·Ýö“øŸÀ“øŸŒ—âFH}dŒ“#ä“ðHüH$ãêFû$g¢Çââ {>Ñ™ ‰ì²Þc7»z™Ìë 9/VvÏzYcgvt°³ÐÙ`p2È ‚ c˜dÇÞK1ˆ/c¸=1±Ò ˆ"/‹Ô@Ì80y…oðŠ÷Ø…TFÑ5œaÉÄöä§ÀlÇ€d ›08Îež@Ë>ñ>‹†<ñÌžéž)³"Ã"Að_–^±"t‘dð8‘ÊdÙ“:f"DÄž#º$M&s€–Ùîo¶xØNlzeÖp½²IÌ™6w$sž¬¶fzøEŒ=a̳„AÇ·˜ðØ=ØÁÂl ^áls ÁE‚ÙÌ3€A@YdXx2Ã,, $ƒ™Ì‚ÂÎçs™z²ËÒØXIÜ;„Iaz²ÂÎgžs,,æq8ä–Y%™d–s$²K $“ˆHYeœÉ $“ˆqK 6Æó8¿2ƒèâX’›žõd‘ë¡kcÏŽc³œ`%é±,ó“…‘œàFÙ¼,½g†GÞplA¬ò!˜Ìˆ‡‚d÷X}dDáÀ÷ÃíXÆÞಉ‹X߇ yÿÅÏ5žz·­¼mõk-½x¼RÃó/}q™ðÙ·ÔËÅ·Œ¶rÂ|Uµû§2vF©Í%8Ùg¶õÆ6Iå39ï¦ÀÄE„p ˆMÈÈæYÀŒ€à@DEër ààA 2#Û¼èk0½AzàØ<g™¥êõ€w×sË,éŸS#è/ú :õ·ÇxÍñÏV“‡}M§vx¶’’Ëi/‚ñ[á˜âq=oŸ„–Í'žö{³6{YÂÉÛ °Å ÜaÀ² ‚!g­l‚$²3>Ð{ˆË õc‘Ü2"d°FsÑaìá|±c8wÔw Ž ¹ä>‡¿ ó9ódgrÏ1…ñÞçÐß5â–ý›Yx·¾3ͶeæÏ6mŸgY_â“{ËÕ³Ç/—‹ÆG›n6ÚåŽ0JI§™Þ97¯ï)—«=/†šx¢Ã<@àcH8e–,dp8cz½Á¿x=FÁfô ˆ‚²",Žgßeë†D}R9–G‡däqNgÔßôZOveê÷Noy½wxϹ=fosâóc£.ÎD儜g¡a?y, Øâaí÷°³ ²ÂÆ7,ˆ ÙcàØAî áˆ89‹ÀÖ ‹Ü@ÃÁwᜂ#sÂŒˆáìð<>9ïÀáõÏ#‡¥œõât«·¯¥§^>M½Ùñz¯‚û½ñ–^3ÅãÄæ±¥¾Þd–Ëùâóe’É -žgS lcÓ1™…–Xô8nAͲ  z²÷ÞàÃ,}Azà o{Dk ±Ó~ÇFÁÁ·¸‚÷,oƒ†ø ÜoÀï¸áÌr7§Ð‡Žy·¿ ú-ï¯w›-¼zõ|6Ùz³/ØJ[m³Æõ’O7Ù³™%“Öû›Ü÷¬Û‚0qD›VÏYïdæ^¡-¾FÚ‰R61‘wA¤,{yî „·ÜpÍàD@Dø|e‘œ Ð@À 6Ë‚ €bÂȃ×Á‡ddt"8s;œÏ¡žå‡–x{áâ£y¾ ²õYY^ïw‹Í›[gxødí·¹ñd|rõºÌû·¬æqgYܽÆNñfØcg2e‘ç¸ã/sScÀ‚p7¨2sF/– ç€@o ‚³Ümê ^ö -øcÖ1Ã`,ÈxsY‡…ï×Ézq‚÷»Ô,c:C=ª6:}@FlAzcƒÂ5lˆ<ÛÜ1¯ X žGÐÎgЇô^»œEoO‹Æß'‹ô¤í¼xñË×vNdÉ/=ydíºÉÇ™g¹² °ë’x#`ûH‘ÈYfqlrI,< Ÿ¢[\ŽñÈ"8jðîw~Ô@Ø €Žt àAcdàNlZx"Øá¼<GÐ {ôæøçú —É|VgÁ|Ámð~׿|g;ñ/6Yãd[Ì÷aã­¹o7!Ûo÷,³L`É °½±½°s=Þ§ì^±-*Ï^ö²½AØF^ ç¦ ‹Ôsƒ È ÛXΜ^ãí@pˆˆ~^>ÐAˆ8GÚ8t úG3Ã<3™õ=u~¦}w¯v_–xÌ2ø$ÙÇ›Çpã½gÁâYf3™ÌoLç “«·®zœëö½Úfc3ò¾ŽeéðÙà×:CÞ<ÀT˜a€‹#/{}ÈöÙÃc#†Aê`´‹ÑaÃ-€8@ÂDAÀ½ÄDp6øˆÖ8o#™ÓË×ô‰ï¿êïŽõç½Îl³ÍîÌ’ø>/’Ï·ŽÞž1Ñ‘’B6Þ‚/VÇFp ‚H# ÈãAÒ6‚#ƒCÀˆx7¦ý#‡úT7¯ƒ>¾‚¶žvxñë/‹õÏ]^ç2õzž{ó÷ÕvÉf 8ó;„„ŒØÛbÇxœÉ=ÀÎïÎ:sàEœFÄ$,-68&pð»Àád™x=ÀæAêQÂ7€ÄC‘lËbƒ¾»¶t:YÓÌç¯ýiá¿QãÇÁâÌñe—Íy¦Û6’óÖñ8Ù<÷Ïw¼µÉm¼É½x!À`gžùœÉcÙ$Xç3„YÌ U‘ÛmË á³à á}5‰»dfGß#½Ä«c/yÀ‚÷6‚8px[ÏQä}µ‡†t<óÏ'èÏõ{Ç‹-¾áŽõb|6[eÞ¾Þ¹à½Ý½ød–ñ™ÁæádœG»­‘“œ5â;¼Ë9²Êa¶^ÿ߸øº#¡Se/^ð†¹Â=ÂK ƒ½È àY@@l Àá±±Ìc`½¬oc‡›ÁÞŸoý{Û>‘öú{ÿ£_®qû[Í—›àñâÞ¦x¼Ûfw›ÆÙ·ƒzƒƒâÀæIÇ‹zœ,öý‚ &#c'Åéß™xÂHl´ËÝø²‘ í‘o¨Ø`÷,p€ƒx!à,A°èGNðæAJÄA$DèLó|-úž/Ðß'Åâù­³Õ‡Omâ¼s¯‹œgíg=x{½ó¶¥¼9—¾–=Ãçz^·Œá&³ÃyIF?c!ad&3lƒlμÃ`ù½®pöðæ¼bxpØá/¾ ¼ , ³‚p€éÏ—†ï æøxñ3Ë9ë‡ÖÏô[ô_²ñž=[x½e—Áf|t·6Blî+̃'˜Ày,A² 6w/Ìø#z¾¼¬Su°/¸ Œæ€D1›ëx°6® È,‚66 ÖÉÑÀ†8}øDLEóÂ8Dyú¸Ùàuóõþ|7ŽÎì¦KÕ¶m¶Þ-¼lzÎOÛ¼É$ã{ðz½[:îÁÏŸ ²%æ{ŸK$–Ao‡«ÐÙ¶,åñz&~ÑÙ¸ ï-±,dazûœQNd0C‘O¸:Ǧõ±xÞÄ^ø<6/|#„p‡Ÿ1ÇÒ>¹àw~¶›ŸK|S‹×ÅžÑíˆöǶûëD^¸FðŽpáà9o‘Ýú§ÿÄÙâËÅâË/6ÙxÏ›ÅëäŸG-'ÁÎ|ÙË,÷;̦ÛâË$ðoS„[Í—ÃbúàÎÉïì[×Ü laˆÕ†vX±Òùàð`àD<88g8‘ K=Ä[xdo Xe:}¸s|-él>ú¼ãâÛÆÞ=Yžlñy¡Ç«o=o_«çž'uæe–Xñ7˜ÙgP€·ß%’s9œ9¸ÙþD3zÇßc|Œdo7Þ¶A¯ áÃÝïXàl}àop íàœ,…³ ðál"/˜x@ÅòDG2 y¾'ú"Ï3ë‹ÍâÛ͆Þ,Ãm—«l¾ m²ÎÛôŠs|4·è’ó8Û¾{y¯Ž!zá}Ïp' 걌ñƒÌ¶^|Η2|³…ßQÀoPÃó¶ {b1áœ=G=dsDàóbcbØâpNEŽŒp<ǧ†Ç¦smúÏÓ|7‹Õî÷xñ}Ëׯ^2Ê[œÞ¼RÙñ×ÁðÒyêõÅgí{Û 8óalxìg1Î8x,}ü1±îðzÞ·Äv[/c¤A ±62-ÖõÎÄDél¤G܈²"õsÜ`CàEœé8<؈ú†÷çÇ×ú ò>¾³ÅãÇ‹á¼YæÞÉfßsœt-æÏƒ½mç¿àó âz²õ/Y܉ç«y…路ÇpÛ9‡ÆXgÓÁ ê1~‹.‹62ZÃ|pBp0„†öÅë#ì‘÷Œà¶ŒŒ>ú¶ó;ïcx0ð}dYÀ™ø[ßc#Àð-áÇžócézñ^ïŽññzøï5™x²Û,¼e•Îz™èÊqãÁœµ¶Ó™ÌŸ ã|7Ä–Ûǃdx1aÌæsµ„€æ+ܶÉËÙð#‹)hõìGݹ+ÂÅ/yœXÙHûD$p/Q ±¼Øáè†f8o‡ß=aÃ8>fŰÃl6Ƕ-8?Té|xŸHï­ãâýæøovYy³,¼^o”êñ¶[m¶YRÞo¾dŸÃ-¾#m½sè2Ç›àx{ámïe°Î¾Èœç§Ìœž о!–ΰg63m6Ž=pášðšz†-õ GÞ}ÛÛ °b݆!…áÒ#ï͇„C Ô>ä>'Ï|NoŽÛõ7‹l¾+â²Ë/Ry²Ûo­¶ËlÛ6¶Ëæ%ë»m²ÚÞ§-ðabÎéÑ÷ÃÈY¾|2ql5âúÛçxŒå†üGpá’GÜeƒñÅ9ïdca‡HK!ˆH<6m‹Ha‚ü#ÉÑbËaŽÞN^x0ñÂ9°ÛÍæômð-àÛå½ß?žm²Ûô7Ëm¶{¼Ùmz²õm—Ãeð[LËx6Úe¶Œì¥²÷y¶ß ½^/ ·›ãêݲ%áÔmx½õá¼\„Kc§ Ûy¯îqêØØHx+ö…ØØ`aˆtˆ[P‹[}CxHµØˆM‹-†Ò/[nBoŽ Øm…¾!-2Ø}óaÒbÛa„†Ûx6ÛÕúÂ"{¶ómú;ÍðÛN/7›o7»Å¶Þ6Ye–ݶY¶Ùm–Ùm-–^-¶Í¶Û-¶÷xd6Û-®[Í·›ë-—¥°Ûo®Œ; ¼y°Ë {–m7ŠCÅo‚ÙŸócå|ý÷%‰.2¬°‹vô0úæÛÃ7ƒ„$>Œ†Kmõ -!ÂpûÒØwƒ 6ÚÞ­-†ôCÁ·=C 6¼aaàÛÁàÂC 0ÛÃm¶óy¶Ão7ÃmðÞí¶Ûm½ÞïvÛm·‹o6Ùm¶Ye–[m%–[m–Y›Í–Ûm¶Ù[m·›-¶Ëkm¶Û<6Õx2Ûm²å±5ÞkÁ†Øa†RØGÔ°–û–x¶½ÙoPÊájÎÛ̇¨ì~`–WgL$&l*Ûz2u…a-"ØXx2¶Â¾ö}CúØq…·[RÖaŒ6\›ol&[ñ [ °ÃoÀÛ¼a†m†Ûm¶Ûe‡¥¶y¶Ãm¶Ûm¶ómæÛm¶Ûl²ÛÃ7À3e–xl¶ÊËm²Ëm¼Ùm—­ã¼Þm¾½[k3kͰâqVØX`½÷ܶðÁç«â^zœãÃcx229c?ã›)’»k¾>áŒ@þxz<,Ûll+kc=û <-Km…xk#ací£ê:m¬A±cÁ¶]é 0½7À†Û[w› 6ÚÛxm¶Ûm-±,Xü–?%É~¤þ$~5úWé_Û÷ÄüßÌ_ÙöEý‘xOýÁ`Oý9ÿúM_ÛO3?¼ŸûI±Ïa‘ùá™ù;7?=751?ƒ3:æy4`üQ‹ÏŸŸ¥™¼¯òOÝæÐ«»þ ŸâŸƒƒøgêYü%ÇãÅ?‹Ž?~˜þ+û§íâ?OÁËùUûÞ0u$§º†ûÿ"þÙh¿¶ß—ù²¿æßŸùÿp¿º_Ù§æþuýŠþÉl°¶ô›ài’zŸÄ­~Sù“ù¼Ç*<üø¦f 0ügø¬~`? ü6 þ6 Öð¿Qcð¿ÁŠoð¿aðÀ[üzOéÇâÚü|}#ôD?W/è¿Wë¿GÔýUï÷ðý·ï¿mû¯Û~ËöO€Ë? ü¾:ýü³ñßÞ¿zýë÷'ó¼K9ž6y ,[´ø36±?ö—æ‡å¢ï9Ÿ–±f…?Ÿ~Íûóùóò»ãÕûwìß¹~Åû¡Ù¿lÏš³ó_±gäÊêÛ{Ï~Ë_›öšŠŸåcó~ Ì>HCÑ÷/Ö¿^?~s?œïÊ/Ö¿àý ó õ#ä/NÃSÜÁ!Ä‘Iü±¯¶ö¦£çQó©üë> bù`ßÝ‘sSð³û¯·ÒϽºÛ&Ñ Ñ,|Ýœ#Û1îÉõV¤O=G¤«îèÌXlHá0&fNرg€ôæË °üØX€üÙd{>ì°üÙ!e†Ya°½XXXXXX@Y›&« ÕêÓzz½ZZZZZJZBZJ[i)Æ,Xãd¾AÏÐÿöÚðFíoÑms¾šµÆ¸×å·nÕ©p¡~mÚ— ݶÖJµÎ¸Õ¦Õ»P­[µ¸víËçVíË·h—¶­ZµµjÖÖÖÛRÛm¶[\•¶WšÛgð6&ž6ÖtÖÔMBeZÚ¶ñ®ÛÆÚµµ-¾åµÎm«m®ZÛo»^7÷m¨ÛjÛm¶ÛmÂÛm¶Ûm[mmæÚÛkm¶¡[k+kl¼Ûy¶¶¶Ûkm¶ÚÛkko7›Íµ¶Þí°¶¶ÚÛo÷Ý·›,6ÛݶÛy­¼ßðÛov¶Å¿C|w¹îy¾?<õÍ·êמ÷¡z~‡§ËÝêÞ7ż¯‹ ‘õ³ÇÞy/¿ ñ÷ô7ßúÌÿùNøï™/®ütó|Ï¥½Ó|ç»â'‰6?ž­­½÷œÆôð†9Ç©`w¾öO ðÎïÔÿúðóÏ é|ÿ¨÷þœú^þŸÃm¼õã¼Û×€x>^üwÏ,çÇ€ÎFyûðõ=B9î#:ù{Þo=yüs|7Äú;æ?èß­½ÞïwÁãàø{³Ç|wë?è3éûò;±öò‹äç é-ïŸrs:gŽÇT|Sºx ³ž¼=pæwпO×Yú¿/Ðõß]~†¦ØŸų¦pîyïN…‰äø{Î>^ìõgrÏ=éÜî—͸ý/XÞøøûîHß³o}ù<ËÞs|2Ùûsov<ùç»×Žøúî¿Iðõd¨o‚D½o™Çëo=ýmñôY+äß3¾gw¡¤=ôéá¾uî’ï†}ÉVÏ-çÅë†^ù³·¾ú•‹|ØòÞ=õô7ègwýZýLòyëÃ߉áï›âx 9>ôôxy¡%îœrsyî÷׋oÒ|½óxd½z[à6Âó,õÆ^ ƒž~¯yß~'O$·WèG‡®–ùçúó¥¾côÈúgÓø·™áñÍž¯™æ°ô=äì} |WHÍñ<ÕãÒNƒàí¼ø9}ÁÇíÕXëâœYç¿ àL°q™›LæyúòsÃçêö½sþF}Sè±ô4žûðs=IÍñÔ…ë-9¾OVù>NZØ,¶óXÈ'ïeœ9‰âù¼|½Ùîøá“âM¾/}g}sÑ›âpÿUïëƒây?AáßW҇¯§óáŸIéô}ý úNx#ømX9ïÃ!&< žzœãß<^cbÏ~<Þz›âÞŸ¯3í᳟K3ýþ‡<³Á³™g1²ÆÆÆÇðÀØÚÀß©¿röäýËö¯W¥~†ì¯Öõ =jýÎOÞ¿{«Š?ðd~x²!6hê_»°Z‡ˆ¿<˜Gí~ÍŸÊ(üÊå¿2ÈŸÉ`ûéöŸ…cÓí]glÕíufÑj¹iöƒ¶ô ²3Ö•Ä\ƒù±ËÈV6gê÷< îY'½[kfëc#c¶6¬K„KV2·,­Z-Z„Û•*U­·ømZÅkñ­K¶Ûðƒq[Þ@× Zˆ#}!òÔ7¡«V­Zµ¹Ý½ãWùÇsÀ™‘g–lä1YÍÈ2FÌmÀÂ3ÛçÎgàÀ_¨Œ>Åúyf¿YÈÎ'6Lˆ74Ã'O°GÈѪ2½D)ý§ Ö8AŒ¯¹|!#bÖy|õGA8Åb{Ëoà¿ÜvtŽª3ù a±[•¨dUÉ÷}œIÌ ÌølÖOdšûÍZ &¦‡~Ä h´ë$n™/É4€Ý¾çA{í¾é}Ž#ï‚Óm߃'_%þÈâ ,@Yeš!?XÛá°Þ‘›±ZzNײÒÜVìF°3˜X/ÚÂÂÂÈ9®XH@m„…Ì, ,7˜s ,‚Ë ,°³˜@XHs Ãl,,,ŒœŽÃ,²ÂÛ×=^¬ç«ÓÇ<4‹m´Þi––’–2ÆDi÷Þ6còÉ?x¯›Iø’OæûÇæÉŸÉû`ÝÛ÷œ>M„9û£KÙ³œ¹<-±94ez£ñ È gÐHƒ™œº¾Ï´p¶•'…íØy–|³`= iñÕÝžœ¹î9ü‹cï—ÍW¯Ó}OÜ¿s‹âÜþU‡Ûû3óðNZòØé›4è‘ÏXHZ Ž"2Õ[—äÅ_ƒ—‚ÇæÒbFí‹!"X_zÀ_håùgî üb/ÜX¿øìþHßîIÂ!øÆÉ"Å‹,d ,$ؑ͛bM›,@³¹bÅ›6lXÎC5ß›K´#Áj×´Z…*Õ¢Õ«VíZüòÕ®ƒD/µøž—î•ùínßæýÍû­~[_›P§ò6ËV­~[V¶åãVí[aËçvøÝ»~Rñ¯u®›lãÄ6&¸U²³¦¡Þ©I*ÛV¶Û Í­¸[l,²ÛÇw›k–¹m°jÄ׺@%f¡—5"JšƒtI3šÌ/ÚØ¾åCì%'o¹m²¸–¥îÖÞl,n—Þ _–}&¯FjHÝ¿4Ûì¯a?ŠS·¥5wæ7óÌ;ë#;ê <Ï^ï€Ûá¶Ûá¾ÍðÝî¼õã¶ÿ£ß}ߢZÛo6Û|–ñaî(¶Ïèá½Ï’ø?cü ˆãÀdœÙö{ñÄ!œ`Åóf, Oƒ9ù…@)#;²ØÙ²o ¿hÝJzWïOÞ"Ð,gû©b˜Bk÷èNZV>yæÄøÏ?HûX÷|˜îøgƒ·¿ ÿM¾'ƒ{Þ1c!u€øCú…~˱=9ì„ É“÷Ÿ¢¯úM·ÄðÙl…EK}„\@Jµ,¦Ë x‘oÞm¥ë,2[82M¾ æ¿uêÍEdýÙÉÕ¾âñ ØqàÒ@ ׾ؚ‡‰öKHi”‹A Ë7Ô{™êNuLÿDx^£<·ƒçë,áþœ{¹ä÷á¾jcì ‹ýçÞ[‡am–Þc{±±±¿SmöQù×í_Oä_•p~Ç䟫µêø¿Á~Ùñia~x^{çÞn‡ÈŸzV?žúè_©?‹~1~S~½¿¬AöGðïÕ¿,~ŒôÌ"'„XàýX寞íÖÏ=X³Zp­JÉð0lpdˆAÃËì‰c‚–Á¬ºýÌ8ÛÓèçÓÎã{³™#eï˜Þà`l±² ±,lrË,²Ë,²Îe…‡æÃwl>[¾ØXXXe…„L&dÎ4¶iihZZ[À_ ³fÏà³fÏ·Èå>ÿ·ÅàŸO?}î¿ñÚ³f~ š²ý úö/wµ?™~åû‘ùäQùWäWéa3òë°2ä>e#Ü(ù¡§Ú7ösÝï¿3 ¥ïŠ :B^­àßV––ÈHÈ™=^­'-% Õ³m7ž­ž­KLÌæÚsKKf› im¥êRÒÛKK7ŒDÆñŽš[oÑû<çÌíZãV­Zµµ¶Õµµµæ¶¶·»ßâÙ3ðl™ø7éoÒÚü7écð·ëoÖÇ߯ôn_¢öf^Ï·#ñè¿M£öð ñª6¿~è_»{½Íþm|¶öååoÅ2ù`Ÿkè|(ü\ŸÅ~¨ü#ØÀˆ^¶û/П°õ‹?p,¢>ço2˃R2,ê`H±|ø¿QcM-ð0Fá²ü_†ÈÂÆÈÌDb5÷eæ!1hôÉq† ½“ò7§-ú´›¬AâYeìlõØàV¶ÔÅZ·¢ÙRämö·Ëráò9 µ Ô¨\jÝ®5nÕ¼µj.šµÁuåÜX±4ç‰0,YËvÆÙ س" žŒ,e‡@ð8e~‚8Oážè8Ÿ†cóøˆ)FÅ™ꀱaaÁ6=ú $,6ÂBõazœ½^¯VÚe¦ÚZl¼o?KîŸk^m·Kì…óÜ%³À€l^ÎË 2›À2À<Œ a±—ºÎã’rÇk""R_xI‘½ðÖJö_•#ØâKìpo€§ÏG³$ú÷:„¥’²#à°Ù%…‡7ƒ oÒÕ¶Ùeµ•Ë|tæ‘l&Y.!ØûÛ²…¼Øæ›l½˜Îo7š3l%±/4–ÒÓ-Õ¶–™io½sy¶Ûm²ú·› m§LJm‰˜°–6Ä “̃Æ,A‘°dÙ³dû0xÎÙ,X‘Gt³ÇøxÖZµj.ås¼´¶¡ÛŽÁêŽ\ˆ÷C5µ™þ\÷ÌW˜…Ÿ—™eœÂ ,9–A®XXád–X–,:ö¶3Ò Ê$Ã/²Ë,°²~Ü2á`Êå«{-mmx,-­© Ô¶jÊí­«×[Ýî÷cÏv69cù½–¼7˜ï3Ýódný ³ÌzMïªç ›Üo­ð÷{—Çß3žüöØï½ñß|}ù{çÍ­›Ï}ÇègÐÂË<2Ï¡’uâpÉçÇÑ7…¼ùï¯"9ž@ˆ!ýˆsÒÙ^ãÔ‚{’ΰ„–D£>zq˜ð,ˆŽ|x‡¯‹owß7Ï<7À-Ž|qûùëäÇS×ߘq“ÇâO\9ñõóÌæñò>üÏ¿çœ,ñ2zò:ñ,ñÞ?iâsž_ÙÍáÆNÌ‹Øv»¨ØOdWê^ÃÿÙfwupd-2.0.10/contrib/qubes/doc/img/heads_update_process.jpg000066400000000000000000003504261501337203100237120ustar00rootroot00000000000000ÿØÿâ øICC_PROFILE èmntrRGB XYZ Ù$acspöÖÓ-)ø=Þ¯òU®xBúäʃ9 descDybXYZÀbTRCÔ dmdd àˆgXYZ hgTRCÔ lumi |meas $bkpt ´rXYZ ÈrTRCÔ tech Ü vued è‡wtpt pcprt „7chad ¼,descsRGB IEC61966-2-1 black scaledXYZ $ „¶Ïcurv #(-27;@EJOTY^chmrw|†‹•šŸ¤©®²·¼ÁÆËÐÕÛàåëðöû %+28>ELRY`gnu|ƒ‹’š¡©±¹ÁÉÑÙáéòú &/8AKT]gqz„Ž˜¢¬¶ÁËÕàëõ !-8COZfr~Š–¢®ºÇÓàìù -;HUcq~Œš¨¶ÄÓáðþ +:IXgw†–¦µÅÕåö'7HYj{Œ¯ÀÑãõ+=Oat†™¬¿Òåø 2FZn‚–ª¾Òçû  % : O d y ¤ º Ï å û  ' = T j ˜ ® Å Ü ó " 9 Q i € ˜ ° È á ù  * C \ u Ž § À Ù ó & @ Z t Ž © Ã Þ ø.Id›¶Òî %A^z–³Ïì &Ca~›¹×õ1OmŒªÉè&Ed„£Ãã#Ccƒ¤Åå'Ij‹­Îð4Vx›½à&Il²ÖúAe‰®Ò÷@eНÕú Ek‘·Ý*QwžÅì;cвÚ*R{£ÌõGp™Ãì@j”¾é>i”¿ê  A l ˜ Ä ð!!H!u!¡!Î!û"'"U"‚"¯"Ý# #8#f#”#Â#ð$$M$|$«$Ú% %8%h%—%Ç%÷&'&W&‡&·&è''I'z'«'Ü( (?(q(¢(Ô))8)k))Ð**5*h*›*Ï++6+i++Ñ,,9,n,¢,×- -A-v-«-á..L.‚.·.î/$/Z/‘/Ç/þ050l0¤0Û11J1‚1º1ò2*2c2›2Ô3 3F33¸3ñ4+4e4ž4Ø55M5‡5Â5ý676r6®6é7$7`7œ7×88P8Œ8È99B99¼9ù:6:t:²:ï;-;k;ª;è<' >`> >à?!?a?¢?â@#@d@¦@çA)AjA¬AîB0BrBµB÷C:C}CÀDDGDŠDÎEEUEšEÞF"FgF«FðG5G{GÀHHKH‘H×IIcI©IðJ7J}JÄK KSKšKâL*LrLºMMJM“MÜN%NnN·OOIO“OÝP'PqP»QQPQ›QæR1R|RÇSS_SªSöTBTTÛU(UuUÂVV\V©V÷WDW’WàX/X}XËYYiY¸ZZVZ¦Zõ[E[•[å\5\†\Ö]']x]É^^l^½__a_³``W`ª`üaOa¢aõbIbœbðcCc—cëd@d”dée=e’eçf=f’fèg=g“géh?h–hìiCišiñjHjŸj÷kOk§kÿlWl¯mm`m¹nnknÄooxoÑp+p†pàq:q•qðrKr¦ss]s¸ttptÌu(u…uáv>v›vøwVw³xxnxÌy*y‰yçzFz¥{{c{Â|!||á}A}¡~~b~Â#„å€G€¨ kÍ‚0‚’‚ôƒWƒº„„€„ã…G…«††r†×‡;‡ŸˆˆiˆÎ‰3‰™‰þŠdŠÊ‹0‹–‹üŒcŒÊ1˜ÿŽfŽÎ6žnÖ‘?‘¨’’z’ã“M“¶” ”Š”ô•_•É–4–Ÿ— —u—à˜L˜¸™$™™üšhšÕ›B›¯œœ‰œ÷dÒž@ž®ŸŸ‹Ÿú i Ø¡G¡¶¢&¢–££v£æ¤V¤Ç¥8¥©¦¦‹¦ý§n§à¨R¨Ä©7©©ªª««u«é¬\¬Ð­D­¸®-®¡¯¯‹°°u°ê±`±Ö²K²Â³8³®´%´œµµŠ¶¶y¶ð·h·à¸Y¸Ñ¹J¹Âº;ºµ».»§¼!¼›½½¾ ¾„¾ÿ¿z¿õÀpÀìÁgÁãÂ_ÂÛÃXÃÔÄQÄÎÅKÅÈÆFÆÃÇAÇ¿È=ȼÉ:ɹÊ8Ê·Ë6˶Ì5̵Í5͵Î6ζÏ7ϸÐ9кÑ<ѾÒ?ÒÁÓDÓÆÔIÔËÕNÕÑÖUÖØ×\×àØdØèÙlÙñÚvÚûÛ€ÜÜŠÝÝ–ÞÞ¢ß)߯à6à½áDáÌâSâÛãcãëäsäü儿 æ–çç©è2è¼éFéÐê[êåëpëûì†ííœî(î´ï@ïÌðXðåñrñÿòŒóó§ô4ôÂõPõÞömöû÷Šøø¨ù8ùÇúWúçûwüü˜ý)ýºþKþÜÿmÿÿdesc.IEC 61966-2-1 Default RGB Colour Space - sRGBXYZ b™·…ÚXYZ PmeasXYZ 3¤XYZ o¢8õsig CRT desc-Reference Viewing Condition in IEC 61966-2-1XYZ öÖÓ-textCopyright International Color Consortium, 2009sf32 Dßÿÿó&”ýÿÿû¡ÿÿý¢ÛÀuÿàJFIFÿÛC    ++&.%#%.&D5//5DNB>BN_UU_wqwœœÑÿÛC    ++&.%#%.&D5//5DNB>BN_UU_wqwœœÑÿÂÆ"ÿÄÿÄÿÚ å|Ñ£,†LFL›èó5z¼‡½ùÇÓ~Qõßsá ¯†ÊëF×-E79èÑÐeÎӤ˚F\±zÌùvqÔìïNÛ."Û…‘ßeÀCô;ùæGèOϳ;ëÁzˈÎ;KÆävK3ªs/Jçs7ífå©ÌÙµùÇÛ~L¢øÓÑ…2AlY@@ˆ¢PŠ”*BPX) ¥ ””`¢ÀP€@ P?)ŸWËÓ(T€€ÄTPTAPP{yÉb2HdÄep°1êò×À{ßœ}æ[ä]øÇÙ~!÷宦Ë=R79èÆûÓž&\Õ:ŒùKn|‰{ øÑÚåÅXíóá²;¬ø*wùþOÐ2à=òðž‘ÛÞ'3³r¯Xå½–ó™Ç@Ñz–£3h×f}Ï“Ð÷ydfÆ•DQAA(‰`°T(A@ÁPT ÁE‹@PX!Q6‡²ãúI2š@%F,¥EX€ XXP•@ZWΕAP[ˆÉˆÍ‚_G˜õ¾#è¿0ú²øÇÛ~÷å®FÉ®6²›<õ#wžŠ®ÿ>zÇEéÍS§Ï—§SŸ'‘×gÇӲϋÎ;?N.ǧ ™ÝgÁçÕáó;gèvNC#­r”ê¯1™Ò9Ü×~Ñ妧ÐÙ5þ‡×~|ÏF4©@P¡h ¨*B"¬ ˆÕð?£pŸ:´ÅD”chÅaE¦,†*"ˆ¢L•Šˆ¢(Å’1eªŠ"ˆ¢(Š"Â(Š BÀ °–RP¨*  ¨¨*…бbT¥ˆ¨* ‚¡j –(A“ó·Ä{ßœ}YüCîÏ\6™jQ¹š¼šA»ÏD7Í ö|ý:<ù¼Ž—>g#¦¼Ý:kÎd½/§1”u9r”ì=8ìŽÏ.+#µË‹Ì윇¤unb;œÌè*o|³W™²|GÚù²=Þy% €€ùønëŒÓQMȰJ ‰2Ú1d1¡E¦,¡I”"ˆ£F6ŒY V˜¨“!‹(Ed1e¢L„™ Y V˜©"ˆ¢L†,¡Q”"ˆ² !H¶±dŒhEDQETQ@ P–ãJ‚ÀX* …©@ ¨* ”± T‡®8R ÉˆÎá\¼GÐùÇ×—Ä6\6™êÞéé¹ËF7¹hFþóö:,¹±ÓçËSªôär:ïN:†\ˆìr㳎¿>;#²ËÈìÜj^ß‘ëù#FÀ‹° (Š"‰2„QEDQ%¦,¡I“!ŠŒY Y YB2„Zb´Æd1d1ZbÈb´ÅDQbÈbÈar²¨ÆÑ¦3!‹!‹!‹!ƒ!‹!¥ÅÅjbÊKIÅÅ”"’(Š"ˆ´ó}ð{¼<^Ðòz7¤¬BR*S   aHe…JA@ÀÂT‹ž Aú+Õrüï<³¦A@2Ä,¢P‚ ¨e,(,€*Q((aéâ.ccp“! L±Ì“1‹(E¦31…Èar33ÌÆ31„ôoAæôoAæÌa=›Ðy½›Ðy½!„ôoAæô† Æ ÆÒ=!„ôOHaíçösÕOsà}~Ôy\Ç›Ò=!æôsÔ|¸}?5VCF6ˆ¢(Š1 Q°KH¢,  BP%%‚X* ” ©I`¨?Qæº^w˜€%€TAR€"‰`…(–J¤°R(”_›è‡Îú"x=¡æÌah”[é !@°XT (Ï¡,¡`”22TQE¦,†,¡ V˜ÌÆ3:yÏQæôsÖF ò<=ü½I^ÆÅ—Ë‹ôë¶-|Iu,dIô|å (ø~ï˜Âú+ꯨòz' ózC è1ÇÐy½›Ò33†H`Ì`Èc332²²²²¨ÆÑE„d1ªbʨèwÚÃÓ˦hP çôx˜ß«ÓSá}Dù_T>gÒ_š}0ùßD ë9ë6cCCP‹H ‚ÜFS“››››é0—Èz¼‹êó4z¼‡«Êž³Ìz_*z<©èó1èÀfÀfÃ"±±11¥¸Ð q¢Ê%‚ãJˆ°«qFR„¢P°% ‚Å-Æ­"dÆ€¥€ J©·,I,¢ãKÀ,‰P– ` q¢ãV T€ I2èXg=p??ÕòõÈ€——¯™¸Ùêö¾ŽZï=—¶ó«óß|gÃØË¯ø6:˜úŸWÖ|Ùµu´™ýöj5ûîw×e6uÌΓã_—àé>SKz #CôûíNa¼ö4¾}´×Lî5å>½¡ ›µš6ïÌÔN¯B|3àºi¶ólÚæi¦ÓÆ_±ò>9÷#à}Ñ~)öÃãŸb>7Ö>Kô•ôÃç¾ðñ¾£Åë#Íè<ÞƒEÅÅ”IBP– q1_1èó³Ìz¼‡«Êž¯ï¿Kæ§ÐùÇÒù©ô_˜}/šŸKæKæ§Ó>z}žž÷çG½ùí{¼G³ÉÓΙß1ó¦W“e%*R ¤Z¤T(B À¤(%e(c”%B¬( °P@~…zá~-†»¦h @ËϦF¯[„Žá/Õ¹çLæ.îGK¡óó—}ôó²Î×ãæ}u'Õ¯Ï:é½ù}žóíËõ|öoÍrûùïXõúO…ˆTH*¢* q¥An4¨-Æ”Dd0zÍê<§°ñ{ôš}cä}–>Ü>°¦µ²¦®mFªm˨›¦nF™¹F–n¡¦nF™¸†¡¶–Ö¶Èk› O…÷Câ}˜ÇË~™^iW8cP·z_"ú¼G»À}æSæKæKæ'Õ~Qô¾býWåMù©îñG»À{Ï!êó'¥ó«œÂ™È-ÄTâ2ÆŠqQ~…Zã5{7\Ù@–â};Þc×o=f]¹ý;ž ~‹Œ³íûtÞ§¿¾³Ú¾¿‘„gô|þG¯Ñ®Øší¾·Þ[±æv•²ø½~ 6??–Rá–«u?ÍöýçɨÙ|±ºÕ}Ú‹6º®—]gץݯŸ7Ö{žËk™«Öô>•©¾?fZß_½yÛÔyK¯ø{=}š›{÷W/w?Lh>-ߺèü:ý\i2êþZæçS”rs¯ÒK«¿nì囹È:?œÒΞÍè±9ûÐ`h®ãæÍø½$«Š\™úÙàØü)ƒ½~6~Q›Ó„\˜ÒÜaóÈÎáL’–â2¸XÊ⬮#6õy[å‘ó¦lFWÀfÄdÄfÀfÀgp°°°¦W c=œõé,ûÃ>ñ¯›!«›Q¨›‹.’oŠo†‚tžtç&†o††î¡¦má©mqce uûáð¾È|—臃Ö,+úHå¾OI¿ÐtÈT²‘DÊ\2Äôú¾~Ÿ¦9çÝ÷ôΉ»ø‹,zCÏ¢ÇS›÷ØìŽNý¾²ênûÒ7ZikëÝ×/6ÐÕ6Ÿ$|î·Ÿ³âËêùóp¿_ÛZ©ëåxî~*ÃÒüiëéñoW_–¿ÔÙùk¡µôÒ ¦¨l~íètß=:,ù•›ý—­—Ý¢ó—£Ùñ$ì}8¦§m‡šìv\眽†¿Mæv3•õÖ7_G/ïuƒÎΣÃ[份ωŸ·‡Óü[OÛµ¾šŸoÍìŸÙðý9ÖÓÛ¬ë÷?¬ø½÷~&|÷Óì}:]ï©Í}»ÿœç7¾Ÿ5Ÿfƒm°9? æ‡LìcSèÚa¹ñü[΃xàï_ârÙ}Ÿn7¤Ù}ß.§†sug;çÕk ;ËÏZVÞÙ§Ëoðž ó¥” ‚€@ ‚ ©@ ƒ) “äÄd„·’ ˜Œ®9€ôyÓÑæ=^cÑæ=cÑçWœ=cÑæ3˜—&­nœß9Ós3IUJKa–&{ ×Ó3×ɼûyâ>5½ú¹‰sÓeËŸ®™/Uõñ_u#¸ÔgKÈI{mo8ÔìñäG_ðh†ó7†n}7ö›¿-*Íþ«zîvšL®v?!µ5ßw†§:»ÿì_³¯MÓž×羫ðzûÔñÓuÚ…ù³Õt˜×Ãã¾Ëx×j¶þË©Öôü¦5½ÖîðÔÐûv ñôå×{åÔèºóçü;_(ãñÙêùôÙ|=GÁ©£v\Ô|FëÓûî²éžiÛ|‰Ê^«šÍòtÒMŽÆÎ{íú¬â§]ÈgYO¿è\ÙcgÉ>Í|¾Þ¿-¢xSíÇäµõ|Ó(zù%õ˜ ¼ÔÍNŸÃOã©õß‹»÷ç[›¿M:M7ÉO»mÍÈÞý\ÌÓ±Õi*_\3ç° ”X¢P‚‚  ‚€"ˆA€B”* ° ëì¾^º.W®äzdªÄ\rƳè9ýÆóã÷k>î˜ó—äÝiz1áö{ôΣižIÍ}>?~uðùtiË|=o/Õ¸ÑöÈåÙêlÐxö^gÁ£íu•ÍΗæÎ´ÿwŽí9WW‰©×ôúÃâøúo¦¹ žÓjpmæo;ïóíeù~.›ËyçÝœ¿'Á³ÌÏM·×ŸÿãÜž{ ãZú¼W/·ÇÏs­¯n—SòoÏ£á¼úm.®&ã PØ|5²û5Ÿ=&ƒÁÿv©/¿AÌúÙÓçÈ}{Ï[§Õ|‡Ñ×q,ÞÇY –ußo ,ë´šïLëyç©ó®·uÁá¬wžûüù¹\o=Û-Æ•)’ ’•(²‹IE‚‚`¥%P ª–X(°*€ (ˆ¤Š °*€‚ ¸Ñ W‰PˆòöÕñ½§¸&  \l2ú¾n£¦y÷ß´éžuîÛöÖ=Êxg}#Ã+‰—Û¬§OË_¾Ïéô×Ë÷YöŸ+_‰·ÇS ÏÇñg+w­Í=óÕzVßSò£¤Ë™j}Ý—¢çðô6?g?ö/[¬øt;çßzþeì>¾Y©ÛéóÐÓÍúsé·Üòg¬ñæ}¬ê´ÿ¸ê½´0Ûn8ÿ¶ÍF3îçÓ«øvºNÜþÌ>M¥Ï7£Ýèøuí5n§×ÍôœÉóì¾-¿.ŸSEÒõÆ?.ëG=ßhåóðø;Yy½Ôë;ŒfÇSUËvÇ=\öéu}NëYüóÇô?Îq}ܱ1¦Li“eq\FljÛŠL®*É2cKq¥A“ep¥¸ŒdÄd‚¥¨)BP ‚¥X%** ¨J”¨‘ ²±WecÉÛáâ;®pMJ‚À õ|žÃ¦|ºNgéÞw¾œæZÏGå¦Àêpæ½kgïÌýçSåËÔø7Z_N{é>/?yûô{)|6žƃeðûç]%ù<ºcîäº>s7aôüÛ³EöùúŸ[?·yÒaºøÍN«}¡ç½æß™ê÷žéöùùïªÕîtÝ1ôó9›´Öôúš¯ ž”Õu?ß:ã}{¯=gƒvv9ÏÏ]\„ïøjmõŠñ΃îO oS±ëŽlù®{ôûv_QÊ{õ^'7áØr²âÆòcb쵋6Zè¯Gš>‡Î³ïùü)ííñÓïù¼iºñÕÔÙëÉ“[ˆÉV%·›–XS&#+‰2¸dV4ÉŠ³b2cKq¦LFlFl)•™%2Ä-Æ”,„,@)…J%¤ "%ƬbÚ%òvùø.ÿÜÔJD°¾Ðõ[ÏÁõéúØænÿì9O³ ùÓMñwgêó—‘w8×ÏzØjúÝ~¦.‡‹÷ùtõÆ:Zs·Àæ²éy©f?fÞ9ɽ´¸vߜðКæÃÚ]V_VgÂû>s?;/Òù½ËíòcfÓäû´¦;}FÆ_O«[õo9úsìºL9åu<¼¹³¤æ†÷ëåÕÙgÅ,ë¹Ï“h»--ð&ßoÈ—®äØæÔf䯭D[ˆ¨J‚¥ ˜Ó+…¬˜XÉ!›epµ›åp±¦l)“•Ä™\)•ÀgpÜl)•Ädƒ;‚³¸Xô¸d[ R¥JX )R’…–(JJ–•R"!Hv¶<}ðàûÞSÀn”¤ H7«S×c¬tÇÝë¬VÛXÛúhǶãB7øè‰žt?ªYÑs”¾ÛÎv&§à'Z䦧OÌ̳¯§°á¡Úyqíg³ã²ùåí~Þj~—çŸBtÿ›ä^ïù~CìÕî´œ÷°í¸ÐwŸáÔc¼ï¸ï³ kYØñ»ßBj=~¾i­ÏŸß”|¼‡aÇbì6óÒßÇ£ûõŸK·ù,äõ=o#Ëg®ûíÓäå;.J>^¿î%äwk¼èu½^¦]6¥| Â{ôZétÞžû|ëžõÝ{Ÿ?‡Ýôo›m¶—“û:o#˜òìþtå_œ¼Íë5ªc¿šÑ]þzœëqóGÂÅ››› epVl)› dÄfÄg|Æwó§«ë—•i…3ËΙ±È©E‚¥RÉ@ %e,”±H*À„6X‚‚Qn<]ñáûž#Sâ&å@Pëù~OKÞmÆ• P¶D‘`"(„–Y ´ !%‚!PT!PTß1íšÏª|Ãèðˆû=uÊûþovÚ²ût2¶[.r§èöƒ7Ÿ.µn>˜l½´ãmµå-?¿"³¹ñãÙÞ.GÝÄXý KˬÜô\.Rþ¤æ¥Ÿ¡s¿fƒS㸸ôÊà3a‘“dÆÅ¸«6#&4Ɉͅ3aLîÎáOK…=.Fya‘’R` ¨(°* ”ÁjRX@  "¬@D²J"ÂAd« Ɉíìx»¸¾×ÔÕÓq,‚ÆF;ƒ­ÔÔ|^Ó®4_Mßǧ×õŸ.ÇLš†÷Â]Kiº®EöleÑ]·”k›MPÆõ§#v^'Ç: Qò>¿ |篌'¾ò¹ÌsÀ‹a(M©«Çw†³§Ÿ~¿:B.6TEX@(€°TA@An#)H* –$â2cJƒ&4¨¬®ÉL˜—+ˆÉ2A•Äz1¦yyäg–—£,°±•‚Üm, @(Å(T¨ XAPXQ!dXˆˆQ Ü%ð÷r]o)©§†â(@£¡ç~NƒeÇθÜmy1³û¹èwxð¶Íöó†ÈèþÞ7#c¶æ,½¯Ûùïß©Öpg%^Ãóϳ7³ÖsÙjm}t‰{“™Ùë?_ÚñxÖ}‡#·]ßÛË7Ž‡ÛŸÙj}Ÿuš¬kPöÜóÞ÷—áî8n¢Í†ÛžöíËI¥èpåÓCsÔ)Œ°KËP%€…APX ¤€ÁHT(APe`¨* C+ˆ¨-‚ÜU•™1¥JT¥Ë eq¦Wz0ÈË<)é–KžXXÍŽFH-‚Á(Y@ `Ë(ˆ,’¬‹ ’Ê’¬BÀJ¨£·÷—éùÍN|n%`ÊM¾ŸªÔùÙm{sÔg¼ù¬Óç¼øÏ«˜ì>ƒ‘òé½Wyík޹yòßMÏõzýçI½ßüqûýŸÉ/.ë³8«•徇CÙüÝqÄåßç/ç³¹úÎìé¾K8ÿŸ¡ç¾{íé¶Ïço§æÆ¾ß>ç-çó¿·¢úeâ}:mœ¦ë§?;âß/çþ§üÉùÌýÑ?5Ÿ£ó†xsÜÌóÊ ‹,A ¥@H‚ ©K ¶ ŠÎcL[2A•Æ™XŒ²Ã%Îá‘épÈôËÏ(Ë,i•Æ–Á“–J’ÊK-%…€E‚!bR%„X1,(@а¸÷žè4Vs:@@ ~¯—}©­Ë¬çúãä}8ËâõÏaš|SæôšËÓÍš [ˆø³ø178j¡´ËQ¸'¾ŽŒº, >|ôÕ¿öåþÓkïËëù5ù:Noß:ê>MWϼýz¯·,ÝüçpÜí¾>{çNãËfýý?šýàãeŸ§üßËŸÑ~̽Ò0ü×Ðúþiáâ3bˆ B¢€€ ¸¥A`X€ €TT…JV"  ·R€TUn*ÉH2¸ØÊãL²Ã%Ë/<Œòž™yäz\2Œ²Ç#&(ÍP™ ¨*RÀª ‚  "‘,©²¬(@ ƒºG‡½Ónu5ÈØé¨&óG÷Ù½û¹_nØìrã}®z³Wa¡ø|O´â¶œ÷ïÓr¹tÆÃêç~³w놖ºtžfÓêçdk<6:î[ú1ñKÙký¾>¹Ýý:Ï’ç Þº.¿éø~ܹžÛ‰êñ½¦çÃGÛŽÃàóM|­Ël´úܱ¬[Ýjeˆ°@–X% ¤À DX@ B„„¨T  D¨PAPR’Ál¥J[ŽE¸Œî5sËÏ#<°ÈÏ?<£Òá‘•Æ™\r…ЍL,AP"UH—„BJ"Ë© а !;¡áô]fÏà®(tÊX Ki¬ÙÖÿGÑáß—&îó^Oˤû“‘Ãcå.n×å9;ú8AÏæì}¶§Ž£²ÖûµÂ9|ú ½~xû'=|“«÷Ôã}z߈ç.÷¡³óÉÑ{ËËcXÞÇX–\{¯^™à'êܪr¸þƒùöt›ŽàÛŸY6Øš·}¤³›}yKñ>‚üÏ£Î<Þ›ÓžzílÒÌ’â¢.á4À‹öŸû¾*‹™æØëÈ¿Aó=|ƒÛÍQ%DP)H,¤Q(J%‚Ê)R‹°eq¥°eq¦VS,üê矞fya”gq¦VXÉ”K@ˆ—ª‘¢B¢Ë %,* @¨;±áîøþÏš¸i–=2A`J…J >ïƒí­ÏÓ¬íÏìõÐYw~úœ“Ú|ù®Ïçùþ£ÞþyõÙ¸çk7yñ>¥¼¸ì¼y?K:¿~C¶>3ž¿Cñàfó¿º .ûÁCô _$­¾·Ëk¯øGÝ®¸nïóæóúç>ÞúçwÅ/KïÊììípâ1ÖG×ñÔÚ}|×ÑtÛo¡ß9óó^‹ÕüÜ—Ñ.ûíä½ÍGYÌlfº/§Ÿùîz_ÌúÎM]ß ×æû꾟—Yç6Ú¾:o>“ïß/ŸY¼Ñg=¾ò]oÙ¬èŽO»Óç]·†ý¬rÿOº8¼:_SòüvŸ&wó6[‰@X‚%„P…–T  Jµ)R–ãRÙL®9ªÁžXdgŸžQëp±Æ™%*TX** ± ’ªJ±â…’U…–EY QVE!PucÃè¾þIÂyûyuÊ Díø¾ÊìõøãßŽß w­zl´i©ú¾?y~ï?%ÛáŽÃYçù箿?—îÓæù÷W??ŸÍ·\ô»Ïµ9|zO#Må¾Ôˇ7Üðù³}¥ý ^?.³âÞyÛÚHá¼»ÿ#‡Ï¹?>¿¥p‡®Ÿ¤ó—Aãúf~iòvœ^7õt»-޳ù¯Ÿê~6~iáúç³^›Â?:~‡àœúd׿OÖ~;ŸÎ¶>=5¼Î³ôïÎãíÔwYW¸Øìã‚wºã“ôóì¥æÛÝž³À7ŸbóýgÜ~]ÉDn>ÓÃô®.µW´×ÝÞzœÿô—Éú'(šfïnq·gç/ÀÚîIõûšÖßØÐ¨‚YHKHâZ” ©ieRƒ9)rÆ™e†Fyyå™a”gqȶ q¥AP•EXÄ©*ÈDJ± ’¬Š((H‚ »ì3Äá|~Ÿ®bÀ, ïᱯ³ÖÓ¿.Gáïø ÜF4 Ûj‰ô|ÂØD™B(“"É,.5×Ê\1³wôs’Ï®|¸¯Ñíð+c5ñ6xk˶ëÿ:–u¹Ÿm :\9Õ:KŸQÊS¨øt²¯MÌXë~Ž)fßïæSªÕÕì6ÿœnSèÚq*í4šh޳“g[.¯‚º¿¯OÕ5ŸŽ÷¥íõœàýWȽ÷ü÷­:Îs>~Ãö—ô?«óŒîwÛ^ ê·ô_ϼ¤ÞmyOëþž3ÜìyŸ“æOŸæÙë3µ”ÆåˆDX, , ((K (°eV `¶Rå2Ë “Ñ3Ë ¥ÎãS64ÉAQU"T•c±в ²ãVP”%…ETGv{ŽPâ~?»áé¤}_/µu^Ü÷Ÿ^}N“SeØëã6Ç´¾I‘‹ÓÊ©"¢* ”%‚(,$°’ËÂ(‹*, !H’€Ò(…"“TQÊA(Œ¢E”XµÂ(,¸Ò¥ `YJ‡§˜– D ªcEÁe€*R¥ [)n4Ë,22Ë ŒòóÊ3Ë Œ®4ÉARUIeF+,BÄ* ‹,Z„) –€À%* ƒ»±âïen»i«é¨R'·©Ýüú~ü¶ —¶¦ƒÛi™¢é²Â¿:Úë6nu;ôüËíêåiù_×8Ý¿ÑÚ/;ðõ[ûŸÎ¾.¨¼^?¯þi/ÇÛð?RõùqùÅýGÎ_ÌŸ¥CógéÞçе«Çûøõ“ZɶØ\ð öÜâ¾ÞËñýó¢6Û£›ø¿Gæ'‡y©®sg÷ý'1ãõ|ÄvÚO;O¤ànÜüíú~QùsôϨê>3IFæc¡èà›ä}Ø#ìÀù§Õæx½ªø=œÎF*"ˆ¢K¹4·ëÚœós§"ï#F "ý‡Å~œÆ/_*¦Qõò¦Q,meq±óÈôËÏ)}^cÖyä™È2˜¬¬UdÄÎbª’Ê‚±ⲡl¸™A+Vâ*ZË@ @®èx½rº}Þ“x  –>‡¼í=zc„½ÌiÛŠòn§î¹âîÓ)­N[o‚>OLz9u›Í6[λúæ#çÃç÷Î¥í9gO/y_¾rüŸdñ—ÛßïÑë?_Ó¬ÝWÏ4ôÜêü|ãÓ¡æGFçnþî[5ý+ó^›g }¯1lýS›æ~;žÏíà>“¾ø¸ÿ”ì´úR΋ŸKµÞñ”ëýø‹sßxððýóû¢úù!Üüüt?@ÂéO¿ŠÏ%Uî¹Xý ùöIÞ|˜ý+ìü·Ê;ïÏ=ük«Ýp’_Ñù]gÆl´ßwÂu:¿¾õâ|×eÀöéñü^Ÿ§|ü~9~‰ïù’?Açtþ6v|·Í¡]ïuùgS£åÒ§Uði°³æÞsÿk^Úë­ŽÃæùâ|5õ/=Ðó»ìë°×xz֩çžÇXcösûåãµ{ 7ÓÏ¿I¬ü\ÿGÎKÕl½ýìðÐôwYÑyu³ËUº÷—ŸÏëéWŽöú>HÐí5Ÿ¤c|/W÷ºóÐxõ?4r9o>™yYÔc\ßÁÚñY»M–{ N+mÒøš†ÿÆÍ=9ë¿Àç©ÍaÑCštùÏ—F9Œº¿3šñéÇ-Ÿ[ó¯7ñváðX—(¤°v¢¥NoCÐó½%EA?<Îãïüÿ¸ïÜ$ÔýùÆGQµà}NÃmù·±ú/¿åäê¹_¯ŸNÛ[ËûtÇkëÂDø{~'ìçÓôL8lºséöÐv¼Æ³Ðíy=VuÜýüaså8Ÿ¾ºo~C#±à>ÍntšÑߣts›>ÉLÓ|>ç·û9_¥7úÿ YÌ]Þ©ÓÆD ™+F*H£R¢„¢Q%´ÅDP”EKEDQQŠ¡)rÂäžu%2ÄÈïòùFÛR$RÑE Ä™oùñïã)n5jd2”¹c”2fL°£ÓÎYé0•ï󇧧Íê¿"½½>X}¾0ú½µë>ì>A÷zë!÷¾´º¥ln»#kލlòÕ+sñüCèòÄT(°÷cÅÙe4<×SËtr …€ÏÏ3¶ÉöwåÌüý†„û4}d®SÓ¹Gç>?oÍÏ_yóôÏ#áÚýçç{ï¯í8/~ßÐàüJÑÆƒUÐsøßÛÚþ}‘Üq½nÛxä4Ý·¬p8þ+óÞÓ_˯٬1©2„™+DЍ¢(‹‘‚‚ˆª•H«"‰@¢,"ˆ¢,±2²iŒ¢*"ª(Šˆ¢(Š$Èb¢(ÆÒÉ”"È”$¢*1e©` VL¤X¡ clB‰T–‰TUKfPÊQ`’Ë A,)`°°¥‚ÜhAae)(Pwv_qSQÉöHÈûfëwÓ‡Ùåmòú<ãÛ«×|»Î›ËÛ,__·¢â3:Mf»¦³OŸÁÔo¤¾Ÿ?e«4MÆ£7kï Ê·Y}\µDæá¸Óý;#]öêöF¡(J»i@_¬øË@ dQ/×ò‘i&CR¢’(Š"ˆ¢L†,†*"’)c(E¦,†,¡EK‹!ŠŒY fpÆdŒY BJ–(Š"È‹€”* ITVD¶“&RËADXŒr• b”YPT)a@,UJPQ@v<]Ö‡íxä5’À e:Ž§Ë®;¯“”ÇS¿ðã±:.o#¼à=ŽçœÚh¬ç?BüósÏ}WËÎzo;í§ìœ÷ßñýºw\—׬Þzÿôë;_³šÈêtŸÉOÐ>éó¼úþ3ßXËèÒ}'MÎuœj»Î °›öûôß]ÎÇçø<Ž“FÌú=9ý¡ôùkþÃÓžè4• o>Fµï·cf‡×Ïï—½Ñî¼35ù})ñe¾æ+“ÏgðkX(Š$Éf,„Q&BL‰&EÅÅ‘1d1d1d1d\Y#CCIqd¬ÃR$ÊQ%dŒTb¥“)DT±D”@ ¤(-¥³)VÒ-ˆ°K,„!,K),ª”„*,²Â ©@ª‚¥ÀP¢ Ѝ;ÁâîóqϬã+R S{Ðh:N¸å¾^ãä³”ñý+\q{—á9Ÿ.·|~_—éÚcˆËËã_¤zï?˜u=7º~T»üo~Ÿí¬þU¹èñ8ß·o³³óÞýRñ¯Ô|̯u¯®ÿÏvšŽ¯ù¬å¥ü Æ}Fý?'»/ø¶]GÒpOuð§‡U·8“V(-1d¬“ óz .TÅKH¢L–IÆÑbÈbÈbÈbÈbÈbÉÚ"ÕÅÅ’$ÈbȸOHžláƒ(c3‹ŠŒfHÅDT¸Ì†,¤IÅD™HŠ$¢(Š–U% X¶d2™KK  F++”LW$ÊT%°Ô°P€TP KKJ€@wˆñw©kˇîxOk6m¶:MMÖ¿å¶n±Ó«qçª_£EÐ>C&ïK3®‹}gK—.¯O«áK×ùr«>½¿:6;ÎNÜùñØn¿5úìí>öó¯Ó /Iõr+;ÏYÛ{pƒïÃä/hâ‰Ý|<˜ìw<. ïò|öÝçÁñ»âZ-1´E ADUE$PQDQEDQEÄQªTĔ8`ÊÌ¥I”‰2‹&PЉ*Y(Š$¨“(X„TchŠˆµe´•Ee*²‰j "ˆ¸‰eIqI,± ’Á, ¨@P(‚Ø* Š „F.G°ä:fP”$Ëî÷ðú;cš‚ЋJP J¢ËB‹(²‚‚‚’­* ¢­(PŠ¢…-J€…ˆÔQBXEQ@E,•e°ÅF*$Ê.+"(˜å%Šˆ±b¢!KT¢K(€‹ ¨ÅbÉqLVT–e 2Ä‹£”"–Jˆ*(‚±H ñ€¡àyzAóqýŸ¹7’ÀPBá˜ú¾¯—ëíŒj* -J¤¡@ª RŠXÊQe-–©BÒUB…P*””I@¤”¡H¡(Š%"ˆ¤€Š"Â(‹¢L¡,Tb£„•,”I”Y*"È‹%€°•,K ±@BÀ…@H°ÅcbIa%„– iŒ£„XE„Xb¢…„%,¤n<½€¨<øÞÏŽÞ|¬Á@T\O¯íø~þØó•¼ÅP²‚Ô P²‹(¡RŠ E´©l¢¨«B¢…²ƒ*”•RU"’*¥ "ˆ¢(ŠH²€(Š1e°J"£Q%,™BJ‰2†*\U²‹Š£’Àb %,T°,€T°’Á„–TX%¦3,D£” €Š"ÉRÀ¢L ”FP… ;qåì<‡aÈî|cy%‚ã–'ѱÖìºç –=0” ¥, €PR‚U(³ TUªQfDªR¢ËJ  ¤ª‚B‘I(E@©Q`QE€ D™B(Å”ˆ±bÈ‹ 2‹D™E’£„–D™I`Á,` ,Pˆ‘B‰(’Â,ÂK°–Â’ŒV+ Š$¡X°(ı”B€v£ËØ`r}g+©ðC¤°@"ˆËã–'¶ÓU¶ë9gLÊY@R€   RÊ ‚’©2”¶ZUERe*QL¥QAU¡I@¤H¢*¢’(Š"‰(”"ˆ"ˆ°‹¨’ˆ°‹©d¢JY,ˆ±`1Y¬‰)q€–*Q„%€ b„‚ˆ‰b¥Ä“(ID•dX&R\A%‘Q%D\×Í– Q@Ú+ËÚQsG5©©YÓ ETQíôû~™Æ\zäP*€PRX*‚¨ •d,µl¢©J Š´ ªl¥*J¤¥‘ADQ‰DQ EE€$R¥‚Q”E„™#T±aT²QŒÊ,Ç)¡ŠÉd°@B aÁ* RÈ%‚*@1Ê,X@ED QÀB @˜òö¨(G=Ñh4Ñ“¦h j,,±2Ûj6ý3%q ¥ J ((¢(TÌ©fBÊ2”Yl¶QTYE ZPPQ@"ˆª”D¢(Š"‰(Š"’(‹¢J"Á(“)bÅF+²$±©2Ä“,bJ1™Cd±a d%‚RÀ%‚T@¶…„©–(AB ™HJ ˆ,€ˆ¢³^À 4»­Eœàë€PB‚ËãQ¶Þn9ãÛ Ð`°)J!@ («L¥JRØ ª*‚Š´² ¢Ê(…€)QIEDX‘DQEŠ”IDX%DX©d%„–J–X¸K"c”1XIa ° d%,BXT€² Ka”EŠ– QU e*$¢ ŽÌž^Õ-]^Ó]g,:àP ,mNÛs<}0íÎ(”” @¥QATP¶Z¶TY²Š´²Š¢Â[(ª’€¤ P)QH”E€I EE€ DX±D•X%d²eˆÇ(²Y.8ç‰Ë ,‰. eK °J X°K¢@,QL@•XˆX –"À–˜ˆK Rˆ¢#±^Õ~»ä³‘Yת•"YEDÛj6ûžÞ~˜vçYTÔ ²’©(‚ŠR…¡J*Ò…*(PZ´¢(R‹(¢(³"(¢Pв(Š"Á(Š"’¢,JX²‰(ÅD”²YX%‹%‘1ÊÌ¢ã2Ä’ÂJŒe€‚Q*Y(` `”A*X&Q T²J"‰)bÂ,„Ê•0 •DQØ%òv ©Qóý5Ç.=p!`PM¶§k©ôùúùwç*J"ʰPRÒ¨(«JÊ(–…)*‹2­TPPRPP¢R’’(… ˆ¡) J"ŒTE€,X%‘Ia,–B IQŒËK fX«¡Œ°“,`@ , –,"À´Å`„ ”%ŠY F*"ˆÀ‹Ë,Pˆ°(ëåž^„EóôÁ8Ü=|zæ¢Ê@”ÇkªÚk?o—¯—£œXP)B ¤P)*ÒÊRŠÊ*Ø(ª )T¢Ê* ¢Ê(”    (V@ E`QEˆ¨JRÀ@I”$Ê,„Ia%,– ,$²Y,1–XE‚Q%DXE,Tb¢,.5`”±`à2Ĉ°"Å¢J‰abÁ2‘u´òv ”Ãðú~n˜€Cž³e¦ÃËÛÇÑË€  -,  ´¡l¡B•JŠ¢­,¢…R(R¥Ê(,¡BÊ% DI%*Q`%X²Q!¬$±q”c,$°˜åK#”$°J ˆ¢‹¢,ˆ°X "À $ÊË rŠ”E‚Q*XEXEŠQÖÃÇÚÀ±CŽKåû>>¸,@ U ÷üu›O/o/O,’” ¨²Ò…-Š-P**Š ´²Š ¨( ÊTX-”PP(ŠYHª”@"ˆ¢ Š$¥‹$¢,ˆ¢KJ$¸‰dH\UŽXˆ’ÂK ,–, "ȱR¢P€…1YE€ D ”Jaa ”"–(“(A( D”EÆ[±>ÕAl§/ðluÝp–P,¦?oÇõVëÇÛËÓË ”©T…%EZU&R…´°Z"…²‚•(«K(¢P[(²Š¨((ÁE Q E€*X– d%‚\U,$ ,$ ŽPÅd²\I,ÃId°i€ À¥Š$¨Š "åj ˆ"–PJ "¥“!BÁ`€1Xu‰|}àXÓÕíõpYE$ÌŒ~ŸÜßyzùz¹a,©@J¥dJ¢ÌªQ-”¥%Qe¥”YJTPP ¶QAAeEAI@R "ˆ¢(–R(‹À°Š1Q%€d1¥’ÂK ,&9BcF2Â,$²&4³1XIK%‘ D)%d`Y@%€¤ EDXª1¢Ê€d%«‰Hƒ«‡‹¸,¦“I½ÑuÈ–YBY,µ=¼}NƒÏ×ËÓÇÎUEP¤R(RÒ‚Ê[’QBÙEV¢…QeJ ™ (¢P¥%JP,¥¡@ "T `Q%K%a\l•ŽXÊ$¸‰aŒË“,I*1X¬l!°K 2Ä” ˆ²QH!(‹ "Èzy‹ˆ€Xd%Š”@E‹@J<}ì…Ç#UÏô|ç\E–X¥‚Ê2Ç#¢Ã<=^[«åwœF¡-‘`ª)¿ÏÏÓÑËÏÔÆ”(«`¢ÊPRÒÊ,¨²ÑP¥,¢ÊR¢Ál¢Ê[Q@²•(°TU(J``YaH `@@BX"Ì¡%‹%„”b"Kˆˆ%‚ %‘@ŠÏ @@ ±X²e,¤¢€Š €¢,–%é ãïb‰d,r]w%¼à—rX– C}íáïèåçŽXêRÒÊ•)KJ)l´°–Ê,¥ª q¥ l¢ÊTÂP[°T¥PQ@•`…€‚X1°c`%ŠÇ(HD–e‰" `–Y*X%„X%K a`¨´DXY`•,™J‹, "‰2dE¤) ° DXÅD F<^HX—]Éï>7,Ë ßÓò}~Ž~xÙ¼€*,ÊRÒÊ,¥°RÔ¢PT¥¸ÚÉ2J,¢Â[)RŠ%J[* ` YJŠ©E” BÀ"À¸€H aI`%„–X²YX1°KK!,Á)b¢T,Z‚Á(’ˆ°K @R(‹¡* d¢*X¢¡KâôQV\·SÌê|Cy[.&çìø~þüü±Ë åb¬R‚ØKeª”¶ ’Ó,iR‹*, ¶+$ É)R¥,in4¶Ù@ `·TH«a* B€‚€ `D!6 BB—ÄT²Á,ÁT°(– l"À,€@K°¤"È P:x}2 .\Ù¼üy€ 6»¿?Mä( h”AJPPQ ´¥@‰hP(€( ¡€@‚˜‚€„HT„@H, €@•" BR% TÀb%ÀPc@PH\AÿÄ2!012p "#34P`$@ABC%5ÿÚòµÍs\×5Ís\×7féÍó›ç7ÎrNrNrNrg.rÎrÎsu Î¥¹[[s¬nuε™ÖžuÓvuÑhgZ¼ëbs«Nu)Îuç:³™yȼstf¹¯Èæ¹®kšæ¹®kšæì×7fìÝ›³voÍùÉ9¿7fèÍÑ›³~rNr–su Î¥¹Õ7:¦çXÜëiç[9×g[Ö†u‹Î­YÕ+:•gP¬æ^r†oݺ3XÍs\×ãX#^ís\×5Ís\×5ÍÙ»7fìÝ›ó~oÍù¿7æüäÎLåœæ,ç<êYS3ªfulΰó¬,ëg:Üëc:ØÎ°s¬ ê×Ró©Vu Îeç(fñÍÑ›£5în§ýC\×5Ís\Ý›³|æüß›ó~oÍùÉ›ó~oŒßœ™É9ÊYÌÌêKsªnumα™Öu³lgZ9Ö/:ÅgVœêSB³™yȼsXÍc5þ½pt/ëSåëšæ¹®nœß9¾s’s’s’s“93“9#7ÆoŒÝ¨æ±›³|ç!ç+3¹Ô;:—gTÜêÙig[lgZÖujΩYÔ«9ל¡œƒ›£5Œ×5þ“p÷}s\×5ÍÓ›§7Îoœä,äœäœåœåÎ\åÎXÎQÍã›Ã7/?~ Ö3YÍìÎVç+³™ùÔ;:§gVÜëag[hçZ¼êÕRs©Ns«9C7Žk¬f¿Êº5 €ãï×5ÍsYÍÓ›§7ÎrNrsCË9Ë:‰Î£:Œçæ å^o^nV~V~ Ò3Ç?37;9œ¶3žÆu.Ω¹Öu™Öu‹Î­YÔ§9Õœ›Ç5áKѱøþ×5œÝ9¾s³³˜³œ³œ³¨,ê':Œê#9Ç9C9W›Õ›—Ÿ—šŽk›§9œÍÎwgPìê[QçW\gVÕ¯:¥gR¬êœËÎEæñÍÑšÇú3–có>,>Ýs\×7Noœä,ä,å,æ,ç<ê :‚Φs¨ÎxÎxÎ`ÎUç*ózóxfðÍñœ™ ,åfs;9ÝœîÎ¥¹Õ3:¢Î¯:¸Î¬sª^u+Îuw[Ìù\×5Íg7Nnœß9ÈYÊYÌYÌYÎYÎYÏ9ÔgÍ?’»ìø}Óº'IöOÙ3ä——§~™§pÄwiݧ~™¦iæéþµÏgÀ¥öÁÌg$d²|òÁÿ{O·O·LÓ¿Ný<ÛŸ§üîŸÒ ×vnÍÙº3tf±šÆk®kæO ÿ§ÿ?ˆµúøg†~ÐsAÍ£›G6lÙ³6ÎmœÚY´³BÍ 4<üYø²w`ÄÇðò3êÙý?…5û5ÍstæòÍœ±ìù?Ów³å†û'×åv{KÝüþ¹¯Âçí?š!%œS³6glœÙ9²sdæÉÍ“›g6Îm͹¦išf™¦išf™¦išf™¦išfŸÁëÝ®kšæ¹®kšæ¹®k𿹝‘¯÷2ôoêyµ§4!3¡*G8KAYJ1"ZÙð-siL@–nV(dx™˜Œ5FÞî•÷J†xf‘šFvŽ‘šFwá+lìŒÙ³6fÜÙ›3flœÙ›'6æÜÛ›snmÍ3LÓ4Í3OàõÍs\×5Ís\×5Ís\×5Ís\×5Ís\×5î×úÜú;õ<ÚÑã1,á¤lñÕq<ÆÃÿÍ®êúî±¶" ™AγíõNV™×p -V’‘\`°¸t.5.X\CÂ… ¡r*T[ý^àÅ(XäVÔºa˜ŠÞX¢X¨â\jËQãÓ38¯&ñù¼LÈ™Øv™¤f‘šFmŒÛ·6æÜÛ›snm͹·6æÜÛ›snmÍ3LÓ4Í3LÓ4Í3LÓ4Í?Ú×»\×5Ís\×5Ís\Ý›³vnÍÙ»7fì×5Ís\×5Ís_ôb5þ2},~¯š9ãE39Èy ddÌ–CY,2Œ†Ä9‘IäÙf½Q`¼¢Xâgr›+ž¦2,ŽŒhÌ€ #É^3™F $ä9$°r´ÿ3•S–v‘×Å ®RLbƒbQcöä•à$[z<'öÿô#ðÕךޱ`æ`´ˆ™)écYîÒÒ3LÒ3LÒ3nm͹·6æÜÛ›3flÍ™³6fÌÙ›38óflÍ™²sdæÉÍ“›g6ÎmœÒsIÍ'4ÿ[\×5Ís\×5ÍÙ»5ÍÙ»7fìÝšæ¹®kšæ¿ÃÙý_2r¸A`ŠÎd'X ÜKHË•°´,áTðâT,Á®$L„°ß'_Bé£8‹yÖ!Žî™šEvNdÆ“¯t’ÉK#8Y“ÚOtîžÿ陜Öt™™È3ˆ‚‘’k9Y·¨n½S°@Ly· ÉÌ¥ÆÂ°QÐÞ"3Þ\ãÀ¹ˆ; ózf‘šFi¤fØÍ±›c6FlŒãŒãŒãŒâŒâŒâŒáŒáÎáÎáÎâœâœâœâœãœãœãœØY²sl據Ni9ãž?èëšæ¹®kšæ¹®kšæ¹®kšæ¹®xf½ÚùöÿSÌœYHâ ‹"2Ç€ƒä‹HÝ6'©ðàYˆè2¿Ü 0*‹~µ¸µÜY)‚Zi¬AÏ-‚/XÄq–íÌæ°|yR5ÆX$±Æmk™Z@jJôl~yh°ÃÐGUnˆF„ ‚¥’ä g…¹^¼Hð%‹®¸2šê†‚+2F´“¬+ˆò5Ô«3‰AÈSX¡ŽQ*VƒdvEWL 6ŒII ì="&rbcú—†i¤f‘›c6ÆmŒÙ°sŒsŒsŠ3Š3†3†3‚3ƒ83ƒ8'8g8g8‹8‹8Ë8Ë6m,ÒsIÍ'<|Ís^ýs\×5Ís\×5Í{î{üÉÀ$œzˆ žEý°/8.¢2M¬ê‡"ÀÃ&ÐdY ƒzË!•âEÃ-{S5Ø5,°ž ë8–C_sIŠqàáѳ;jÓÓk5$Ó ‰p:Ô¡vL`çðÚƒÔŠÈsdÂgòâ ¡®€5–°¢å\D+/~¨ÄURrûCºËØ %Mbc#zLYýÓ”¶Šý•Q½²ô˞؈«QÅ‹-Ù ”FÚΉšˆ:Ç÷6³]5UÈð€au¨m `YP˜¨¨Î,T®žò¨¸dVS+.Â8r)NÙðžá2r‰EÒ; d':gaw&$HsIÓí×ý —ðÏ Ò3Hͱ›G6lãâááŒàŒàŒàÎ àÎ àœáœáœáœâ,â,ã,ØY°³ifÒÍ'¾ï»Ìœ¼FJL$G„ÀÍ¢2RBC?d ÎHÌ}º}ž1›‹ Î3yÎK9/tÄXtg)ï; 8]ƒX•––u‡i‘3l¶ÅÉÚçòãìòа–ÚQI>³1Æ&pôB:”9Ê6sWß =¬jÁc];≚ #Á2=@ʼn†±Ï.:º2hÐÑ»Š­’l Ò•ÍF8¦J%6‹ü[=YÛSw%*І\sÞ")@€›ùy›! \ ІPjû…oPc›\Xî:Ò/ÜÉ$²^ê g'¹7ÙÙÑ¢¼¶ tLĨɶ$ÍœÐRòS>În°®"ñÓÇ»\ñÓ\ ÆC5Ù¾k21jc0¢Fs\×5Í›×î×ÈðÏ Ò3Hï»ëæN¨¬8Ä{Ô(HÎ=p%ˆkÜ P¯™7h0ºñÞ1ËÓ/"§ŒÖ 13£,&Y1Põ*…5H¢Tp`½ª ì<4@Œ”’d À /…š`­|(T²^ž2Ú}ÁX y¶tó  ~ïÜYC“33ÌÝÌ'Ø6\1ÊÉc,¹‚§š±Œ6Ü3Ø• m„XÒ½k˜‹I+H8k’enœ"ô’!èY”Ó&'†[p’ÌJëªUd[„,"¨kQ<âf´U9 ߈ڒéV;?ºßc–wt×ä¢j²Á³xœâ4w‚Å^Á‹RFò€×`õ-ß‚Õ1ÍZ늄Ó Ó+8‘‹ÚDàÖ\„Ôü}2ç:)ÚäñN½æÎo-0ÌÏ„339Jܾed9{÷¯DDrÙÓyë¼u!Ñ8˜ÔËõuÏò&#Ã4üØ×DòD9e#ø+â#µú ™éìx×N³Z™–ûdÍûö e6 QB%‡aÀ#”Ö*âL§¦TÈVT± S"j¨Âj&&È1Hª³Š&bž¬£®:© ô'ŒI¬íÄŠ"£dN»BzGa ‰šÜH@UDI½r ‘(ÉŽêõåÄq´ü~Í'VªU?p,Ï L áni:ÈÈý›Š3Y‰äfAœök/tÀ5Æ—­ì\1ÌoxÎÒa“ e°ÎÞ¸W$޳y¬¹âçK1µÌس3Z²|‰BùѰîcp,â­FLÈD Á»1¬M½éæNG‘¤Æišf“ö)¼;Q#ž9¬ëQ“%9¼ôä=y›‚æŽK›93%1:IZi@¼Ä æ@6ÈBPgp¦ ÜJÒÙQu£“jvbZ  e8ztáaq[¬TDXD=ºË”¡ü$Â…E—¼œU‡uNE†ƒ*FÓŸÁ^ÄVcXâ¸%Ò«öè½y4™6(×?‡JÐÞ³õb¯ç¦Û Û”%‘«íòGWd`•ënÀ²£±Úº:ûN²!ÑUB²6ë†t1¼̰áP ì4öG}lJ ØTš CÒ;|Õt ,`Dë3ݦišf™¦išf™¦iýBï³Ìœ úÇî:»EH#–¦Brº£F/”+$¢m Ë PêÛ0R]+3§n½+rcI\Æë(Ö8™¬¥±¶LLJPgt‰GtF1 *î]}à#$NTª{‚©˜úOp4׌°ÖGzmJ„ŽHâö‘–µ‚¿î³E$S’ÆLDé$÷r³bžRsa`"r&o3aÛi”\g)Ýa Ûf4åwð”Ýü}]}ì9a¯nû6eزØÇ¾©cn®2\¶Y3R[Î Z—ÊÎT¶¸1UR –ÉqñV¯^]Ì×¹l¶&SÿÔ \ÿ¿Úî~ŸvžTä`>Dw–æY’¾BZÙdåyÑIfáŸÿé:€Ì>ÜNOéäDìÀ÷ÎxkÏ3ƒKlLê“Ò_¿b-|°ßãZ¾„—ˆ’+m$T8†^8‰§„ë(ä6I5‚¤++"="fª} LÅ-Xº›ð©NÞ„¶½<2ŠäÜ­Tuj›¢~tÎÒTÈ\Vq@¡’Ëuxò°A±ÕÒ¹^]Ú}ÊQ4Š6—Ú`AÜ—p“..ý'¦¾à¸<ƒmzÚÒfuL†U6nÊMËÓ[õ^ÄJë.àD÷-îw$Ö¯^]’¢äjŽj.Óâ{Cë1ƒFÇVö#oH¨Ü²ê*e)ü€üÅ:ÊÖÅFæZeqDœ¬o2f}rçì©p´HZ] ê#ß4Á±š1.ubyԫǨ%”\²÷|ßlúù‘‰H”8T¦"g4˜Í'±2z¡e÷Åi ïÓ:Fééå@Ì÷iöÉäDÈÏ;°H†MÍ80Anbðí<£®±Ž°Çw Æ ºÆÀ}Bu ̉›ó#õ´yfúàfuœ ‹$×zP%m&$úŒ`ßQºµuEwÌR܆.Pƒ[Ë¥"P‰ ˆ×Yøý¥îó#+OàdÌš¿oU~Û+‰ K›ÔÇnoK0¦¡k×SOD‘T0áHŽ'NK°•¾ ¢‹:%dQÓIzLi*_!M5àҙ§øj†Ô–Ë ¶ãš¬…t¬ãè WiáTxˆÕyÃkžþ&é°õ8ÊÕ9F|'+×'*!öªÂ#Íëlã,½±ß¤ü^Ó÷y‘‹|„ILZ‘²C“nf"ÜiÕþ.³ñÁÎMÕ*f/FÈÂÅ€ôŠ’ÄD²Âvbôßi‰5Ö5Š…fÖ☘“ ðÄÆ§¦/`G zÚôã ²™x€Ä¨Æ6Ç" ©e³[^Þ0¸m0é‡*ÄKüyÕú“qR4ÚLS†yÙíÝ–Û$öìÔj¢Ø©*Ê`£cXb]5ú4 4ËëUKpé$€ªU\…6—gÜ®‚yô œ % êf¸¤“R×UŽ–Ójƒ“q6³Uˆ¥¢L e’”.o³O‰têKÈÖP{"&sIÁS 2gº¤›A‹S7zRn“ Yÿ¯ôYôg¿ÌŒ³O³Ní3LÓÉÓ»LÓ4ïÖr5Œ™™ÍìîäfØ3&0³©~E«†ÃdÇŒ1Ìi2æåb¿Pu‰›’+Ó¶µ©FX³.î¬þeŲòÉŸP\‘6˜/*’E“n°=wW›¨ ‡W†Õ”®wì±e+²U ºäÖ`¼ìˆiQg!‡5(‘X€”9[­‰·Ógg¬x­î*UdÊ‹RÅN’û6ó qÚí;5Œœ>mnÿ-2¡+ä{Ë2n; R—^¸ÀVl:õ÷H·²ý´“ÊÛu• µ¡UáCy•RÙŠu¸§³£˜jWl}4v[­œ qÇ4&éÅHä‰OõÉôwêy‘‘üf³þÆéÈ3ŒÜZó7Qk ŒgrÜÕdÚ|Šlµ8fFXËbÝeŽÏ¨?e{&‚¶Õ‹¶%«ÛBÍÉt – FÔ…uH ,Ù76)M¸Õµ´Ÿ]*«a+†>¬ÖSª„)Õº¥YNÖ’b—g: ×ÅUn° ŽÓ!™upR,.l$æÝƵOÓ{K÷?Öç,~¯™Õ"f2fgý]g5˜î]ºÂt¹Ÿ×,þ§™ "ÂCF3×%-ˆ’ÉYÄ0~ßátøj×êy‘ŠðL=‘ ª-_ûjž÷j/r¡‚4õ™£à7GM±Xp×YlNK¢<èÛ»¢fO„åTþA›Š«Æ)@8?>k¼c’[T ²ˆl¬mêe¼»V]')aþ>Q¬´á§¨NœЧŸlY\Æ“÷xiðeßO6&c$¦r ã7¤DX&c„fYtd0÷Kß0hAµŒÀ°Ð‰")ދɴù.¹Ø2Ûm`ªÙ(BãæñdÌÌŒé'zoˆfüý@4›«–Åðå‹ê!³i.^T¶µÛ¬J¥i`J´ÎW²³K¬(„mÖ©À§“’§´k)ävœD©+ÙP+IÿÚ­-øNŠÊ-ÍÈ´’vâH%e®\©Ÿ ]öùµvÉ.2TL)®èÎkÂÝeˆª¸2¸ \äÎ æ õV+ˆ\”‚ü{¼{ÕP˜?÷UF³¨¡Bè“£ib«±¹é8¤“e¨bgLl•;#‡ôÖ3¥³;òPèɤñ‡!‰Ÿ³YîÖr&c&ffȈ™‰6à5Š™™™þ£¯•ü½Ïg›Sßm‡·Wéÿô×öEüŒOá¯Q»@¹àV?m^uAí•ë¬(Gb¶Ô3zšÉ6`µƒÖÑý½E©…ÓW(­+Ø U‹J¶(Ew;Oß›úT¦J&BãåJ¤o %–¬u ø‚ß³Í[H0Úga¡aÅ[óªvè¶èȸè†Xc#l×u“…xçh™%zHjëmÓƒú€`v„êV×^ß=ðϱk©uì%nÉʶÖ86ª-ŒšFh”õ7Í,îì÷,`a@\¹MÖ.kÞ0é;2šño³‘Íšp…ü?gôüÔ‘Mjú¹p'¡f“•Ò ›*…tƒf¡F€—ö¤Üm]"0Ÿ îR‰²ä1?p˜ÃÛö·€wøý’S??Ùæ×÷½›b¬Æ›Ä4ˆ²q( 7ÿR¾’ŠzLñgÂ/hÔ6å×LÑB7¹´Ñ±UëÊö ä©“aP¦¤`Í´tЯ»éèÊAë[Å=-z0Ø>Έº š ¯ü_…-÷f®S¯ÎÂe@tÕ&°éÙ èmjšòVV-¸a«ºwä©‘L‰ãf@Ìæ“ÑiPcq\¬´}ÇLÆ·zPo' Ð}Ñ2ê¯Lw˜É0 ,#’þìïgš£Ù=qkOXºÈ޾s­üsÚ«Nňt®âÁi´ a~0&J;GÝ•^•e¦-§Zµ\H—Y[E>¤ Y¬G7Ëm€Ç$ ÕVœ‹(6Þ8ßÙñwöKuSëW4ñ´UV¬? `XÚP´WD¼ì£™R¨’öÏKgš*Fã;ÀeO;0„]j››kd®”IEN’e1Ú¬EËÙYDcª.þÞÙHܳhk¶¶ÑÒ˜; 苨1&òç+T[¥Ñ–¡eÙÔÑX‘~+uÝ_W5þšŒXúrC4¸lÖ› M<ÝÙ‹P­(žÐýæv}rTجvoý8H_DÒK=~–Ý?¹7Ù>jÃtô#O÷)\²ôJqJ–Ëë’qU°Q´²,º"fJp*¸à¢D»ÄHäÒÕùÓ391Þ,`䱓$ç ÈÉÙyÆDém­iê†Ù{rnX’‹¯†>ËŸˆºÔ 7›lXx.ºûEÀ¹íIZ¸vaÉK}ór“Ú2µu¬Y¿Í“Ú)<Ñž¥}¢#9cnUšµìÔÞ¶·÷R%ßn¿W.¥ÔÅä< TDNòVúc…À¡&¢]Úz˜õ2Y˜ ÊáѪ F `äÌuuë•–´8ÙåNŸÙ™íŸ_1^æWüÂâ\âë€Á¨ö€¸¬öŽS€%%)'t•ç"šm¥* ìùü,¦£ÅÑQ RT7éèÊôøÊÒaØ]Ÿù³@!¡°c³Fb{4·OfÆ0%f&_MœŽÏt™vs"#³#ô×ë8ªOd ŸN³‘NęҲÙé‰CîŽÖYì‚®àHoeêiR¸[®ÃÝ+dwIMº]8}ÊF[§5±UXÕ㩱*îm©]ârÚÏLE[¾ðS&&'þF{ü{µœ!þ4óõþxý§îóQ6Ñ9Ö†ñ¼:õʘ;j"›µòûoCb³Ð ¤^7Q¼Y ÑöeF%bѧP‰ŽD“®´w-Ë4“Óœ‰Á°“&hJ(ÒµFcfMŸ¨˜™d bv†›“ø³],Lf¦ÌíìgºpG´8û'v•÷GhGë·Á]¯ÝLuâ3û´òÍÔ˜eWš™ÚmhÏIC´" žRô†„ðªo7ÂçkâJX$ÞÒé…5:NѬ¥CÿþMnÏOú¨Pgdr_´YJf{:•Q|Ûìþ,žÍ@gg‡Ðü9V¡X¡¶Ù úŒK»HHQôÆé=b]˜øŒ’’¯  ª6ž[ÔxÖªv%YàeJÐŘIhœÔ²1M0×_JÐïì…èϘ1®E6Ìtn×£v¤2÷…Æ„5Ìlù}ºÏ#0H‡̲£öÛ*auÔl80ÜÓ™²ù(·b –¬4Umê´îX»f ³c‹´ì-¥‰¼äŒö‰©¿#´ÎÑaDé3Ú¬Ø=£ªí]'Æhñ×ú¡q}M;ɨ;W®&ÀTº¥¥ÌS[nåV¡;~™zØØ–Ù§ÑŊΩg¤ÎÌ4¨®qÍŠ¤€¥I©émv×Ê|àjúì²"£*[ãeNÖüv;L ¦›;5¤GnÂ÷£;"—cz1 ›gf[ÚHk‰Ì$×A¸Ü%“³ „®ÎKEýaDpjþêTÈìsèß™_ÞüŸs?Ió³~Ößô5œÖuÖgv¿KC$Æ`ó»Í€‚""+ –@z‡rË,½©ž­ü¬¿e€7ìŠße¯–ÚkWßëý‚qÿ©æ(¶”¹Zdº´Éê–>ÇO¯ôH?ß×ÃûÔó#"»¦ -ûxÙ¦DLæÆ|ƒgõ<ÀÄë*fæÏB~ž:M€èI p{tWýªS Øê‚ögGÓO§»wÓ™‘ÙÍœe}=Ú “Ùöb>k&•˜)ìû9¬Ì6³ÕQ0ÖXE1 CA«ÿ «Ø¹šó »LC&&'8›”ê­¥iN¦€q<]¤÷,`”+o>Ìavtqy ²a:…„‚¨½¡Þªít±f³ïŠïÎ6HäF¸T¬ 1f£Í?¸i>}¯˜8¹Mcoà䩘Ò\¯xBù_ 4*£2Ûñ<]ž3o`=ƒ,Í(RœÂlz†±‘:äÎÄË<ˆäØû­‘ÎÎ)åíY=ý•‰Nëž5™<.æÔËñö”if´Dºõƒ¯•šÖÙÄ}:XÝ·´õüëƒg9›­Tc0¸QÓ¢Âu⺮ž¿:ê¢fµJã^ú銳²vóv’ìŸÐ£\\Û”PŠaUÁ+¼ÚH{íJÝC:Jª~:èŠU!ÿåÖ K©ÂW=¤$÷4œÜPÅj4øIý­¶VË\÷\{f±„ƒ:züÄ"tó³fÆÖ2g8;EKSû»9hk ¥S_Ig8[ÈuÞ t‡õ•2Ò,ÚQ4df3IòSRX”¤ÜËžÄé™×î*ù*CŒY¬»¢&d€Â{¢&p€ÇÍ×5ÍÖ|û~ÿ23if“F½Û‹»‘™YÀ} ˜wdxg3²\éˆkF ­)ê¬Û²Qì LÌÎ*ðçb­¹CÖXßõ8¡`bo¼Š{Eònq¸Ä¤gêgƒÚMƒÒlM{’‰KŽÍ˶J¼“ º­î+ñÇ?†¨ö·æ}IuoËošö#´ªÄÅꦋ ­³(5);ÌK_FÅd"ªë†Y¦USb˜X©6Óv¿/ýìלÄS–-‰U ÎTÒˆìîËk\b×O½[,Ñ©]Œ}úÄæÙªéÔ!3ÑØ3x¼¬î(å Ù<÷ƒ¨Gç nÖÛÔÚ§5‡;&?É»nıÎb©U±ÔE23¨zõuÌç´Y{\V‰:º œ OŽÅ}LCìð³ÜÓút@–à¦\¬ U˜Úˆ!ŠÂ›ä£ivgcìå¼ò–Œ,»1IBiYuCÍ™èç׳P·; §9^‚vôuºÎ†¤ÇCOgÒÓ,ggVÙN¨×›¿¸ˆÖc³²‚¿ÅÉŠ ¦%58j§96‡}2pè˜HÔkˆèí R4¢FÍÿ~.«]°ÂP‚)לuf•K"­‚ɬøž–ÄDøezü˜u—#*lJ©ÆÒ¥="´AÐêELtŠA4°©Î³N"”éHêgG1PË:fo:ÄÒ3hVa䡛޹„EfÈÌLOŸoÝæU™•¸7/éåïe#¤“Œ¿œ eÒi‰Q|g@üjI3”—Q•§ÙÊÈ6ÕpwÉÕL *Ø8$<'¤µ‚—•{=ŒØ[í×J•Äìánëx“s’%´¾ø™ŒE¤ 6—|N’OyÇrÚj–9­ÿKL¯~™ñ/³Æ3YÍÅ›Ï$Ì£•¸$a†l>àsB!­ë-`œÃö<±V˜6›.Y`¦ÃQ,¹a¸6R9×°ƱŒú•'´lÌû~Á2;NÆæ^aÅ‹ìzðJD¾«:öd´ñר]¤AÚS±ö÷¬/)u©Û]i°Èkzä `¼™¯aê8;‚)ÊB¹j}J¼â§Þ³muóP£Úh)ä ©ëªrY5“j4Uc¬«ºµ„.͹§ÝR?ÁêkËÕ|)[ɺC-MyU-¡Y$•ÖJ›¿*‡ùwýñ금õ/OV%‹{:ƒ+:‡4ÄÄÌgazÓ˜á2‘ ð gnèÁŸ Jf4qÌiᦓ§!ÌmAÛ«6ë:m^›co#6éÿ·o‡-~ºy—<Ñʾ ­çœ_„β>±|fpFzjZÀ·I’¿ûŒìøœ˜²ñ·/ª,/çgˆìñ•±œiç±9:–~8BßtÅÎáK-ò¿´?l²žšÉHÒl ?&œNêÏp n9nl‰Fi3öS¯röŒ­*Zv©•‰iŒŠ5¡ŽŽÏãþ{q ›.N˜fwl˜ù?Qgγj7¶cL.í<ÖsqçŽofC9½šË[=ÂdÌíy‚÷râ{"àEÌ74æm:`,¹qÎÝÇe§[¶…––ɶ ¶ “‚;L)›… [(ˆ´{ŽÑNu~Á‚2‚Ÿ:盬泛‹"f3qfóÒH§%Œ˜ƒ8€dä\­÷s3íæné·fr,¾#¨vÌ[Ú¬ëlàÞ²17,IýJÎç†ûŒí˜¬¶öƒÙ üºÖ-5§¦èö“F ´™8þÐ'µt¬ Kq_Éc{z2µ‰C>ª½G´—µ½ R_UÀí=1íæek‹@^]¦;kZ.¿êaÈ—“‡´té2£¦^´›FÊ+Àv‚EÿR«‰·H@ïÖ2žÒ¯-¶á{ë²×¾ƒå©hWÙÙﮪK+ﲚå_€Üî.RCZ¸¨™gƒ—ábôg¿Ì+YÿLàÃ3dùºf˜‹,F>Ã_? Ï£=þd|—8ïÔó#äË©æGÉ–S¿Iòcù¯éJµïî‰É/&>L·ïó#äË~¿ëü Ï6>L¹éæFGÉvý¾dzGÉvýždd|—kôüØù.Ïéù±ò]g›%¿ÙæÇÉmöy±ò[=³ëæÉgí/_2>K/C÷yƒòYz3ßæÉsèïÔóä¹Çþ§™ëüÔó#äË>ÿ2>Lµîó#ã­|‰<«~¾dd|—oÍŒO’­ùÑéòU¿O4}>JµíóGÓä«>ß4=>J±ìóCÓä§û<Ðøó_=ÞÙóWò[}³æ¯ä¶{K×Ì’ÏÐýÞbþK/F{üÅúü”^÷ùëòTú;ßæ/×ã©ÿB}ïó×ä»ÿ0=~8Óý;>ï0}~K³ëæG¯üù*×›±éòU¯62=>JµéæF§ÉV½<Ðôù*Ï·ÍO’¬{|Õú|”ÿoš¿O’íóUéñÌÿ¢ßošŸIù)žÙõóé?%¡zù‰Éø‹Ã4þ½Ýæ''ä¢ôg»ÌNOÉSèϘŸYù+þ;ßæ'Ö~K¿ÌOº~K±ïóì{¼Å{¾˜þJÏ»Ì_»ä»^¾`zü—kÍ_‡Ær‹µéæ¯üù*×§˜9ÿ>J³íó##ÛòUg™>ß’Ÿìó#Ù?%;Ûæ¯ÙðFŸÐ›íŸ5^Ï’™íŸ_1>ß’‹Ò}|Äûgä©ô/w˜oÉSèÏw˜O’Ûïóéò[½þb2~K¿ÌFOÈ_ÿÄ,!1 `A"PQ2@B0aqRp€ÿÚ?ýÌ’I$“ïRI$ÿ÷›ÿÉy$’}§>É$‘s",IbÅ‹o‰,Y "Å‹Oª>5ΣD"©Q *$WEY 3ŸTÈ‚6C7ÇÁð†…2.™ðt#ò&l˜,Ë´ÑbÅ‹]–,[eò‚è².‹¢È•̯³É$’I$’I?²ËÂIÑ$¢dl•Äó®48!EQ‰!ER¥ +ÙFWE6UìX¸cOðnz!Ã6>Ð×ã‰gÉ=–cÊ`Ú1mñ%‹’X–[|X•ö÷áH*b†¤©R¤ ä²Ó!ŒÙõr9‰,Ér&Ù:e؉í = Ê%ØYIÑg¢èY&Y ²Ðbä”J4B!¼*ŠU*4ÐûFÉf=ð¿‘€ÞÅØÞ‹—-³¤É–,(hnE¶‰Üx%AÂ!}û.‚I,X·'#e‹!9$”Hš%hÑôÈÈĪ*„’’¨¢":+=•èúK ‚‚«ˆ¢ ‰@Ô”(Q‹bäRfU‘¢-‘ðlvÉ,ÂL»,É}˜¶ûPàOêf]zÿ$»þÂr‰á7VËtXYE‘0Y¾Þþ×BæX¢1¨*Š•CÄ Ô•Ùßo~o¢I$’Q$’‰‰Dî9oÎËÔ›Ú T©]TÅljJ²?&[!Ìgœºf=z{ó}K%’Y¾'ê,N‹q.Yb嶸¶´.‡”2ȶ‹8D²å˯S“FI£\h„h„J4UEPú!~E äi¿%E_ä¡Bž§YVUÈú!àù!àÇ®#lÙ½ñ2ë˜]½fYbÄí²Âr>ÉÙdXE‘d[E—ƒËbËbr7¢P²['™'ÓôB!Ñ J‰AòD•EP´TXÂ+¨)àñ–cŒ¨‘§)•e\‰}#‚¬®ˆuñ!˜¦eÑ»q½ MìÙC{%“½³fô|›02ì–lÙ,–bÙ“,Ë2̳.Éú¿Ñvbåe²æ.L¾ l}'ôŸÊ ìIe1ĺÉ(”"Q󯏒xž'ïvbsýz¢„@ñ‚T$Ñ Š¢:#r5¦Fˆß¨ #䟒8†ADçïpŠûc[d²XÛB%–e™fY–bÉ·Âé±e¤\Yln–ÉÚ[³kä±rŸS ’ÈLdïÃçö’¾æÜ2Q(±(•Ƙš!ãC©„4™T@—Tª*G0Wdsùñß/Ê:â hŽˆgñcïÏÔ|QõB>£ê0Ö(—,Ãô¹6‹1·oôK‚w€Ü&Ñ$²ÌNFI$ó$’I$“û¶T©]Øú*ʳà«*ÈbCQ%\ļ!Èn…$û( é¿7ÛdöY˜¹D²Å´X°†¤n¬¶ËŸ«Õß›hÑ£Bˆ4A ‚(Li*!Ç«?<”•eYVUŸ#è†C–Ù †C‘ôC!èÝŒº6oñ¿N~yX±bÜXyËh!½¢Ârdàù$°¶I$ú{þ„ˆD"¼U\B „B JAq×§¿m~ÚýµûkÿÝgí¯Û_›ögæý™ù¾ý™ù¿f~y{3óËÙŸž^Ìüòögç—³?7×¶¾½µûkö××¶¿ûQûkö×í¯Í÷ìÏÍû3ó}û3óËÙŸž^Ìüòö/ÿÄ,!1 `AQ"Pa02@Bq€¡ÿÚ?ÿ ‚ T©R¥H ‚ 1‚ ‚¥J•*T‚ ‚?.¸üÌAT©R¤AGá1𸠂 ‚ ‚ ‚ ð1ò8 ‚ ‚ \ùn<ùjçÁ×á—> ‡\ÿê|2 †AQôÿ©ª*¤Ë‘.‰#JEŠ(QAÆJ2Œx@ðÙFSRQ• ‚ ‚ ÁhÙ³}6lØçÜMû‘,–Kø,ËhlN þ‹$Ë).‹(bÉûÿ ‚ ‚ ‚ ‚ Â.ü(’ID£PhԚؘÚ(ÿb?ÔÖ‡Šbã%Tjx!4à§ìxp<DQF‡‡L¦…ƒzl£(Ê2ŒxµÒVC}!ÿAAAAĸïÀ‚²¥HÑV5RC!•fÈfÈfÅ,–Y–ȳM&]ž£,ËþSôzŸ¢åø=D_g¨ º”<±”&¾vj9,¥lœBã!4ù?á©©LEŠRk$f’ã¤|”ÙéþÊ•ù# ¤£*ÿ¸ïǪe‹XÉÈœ.‹¡ä„þK(,¡…’^Æ<£GÚhx¨LĪƒ%Š!JÿÁé¡­•ZcÁI’‡UG„!)(¡ìôØðh£* ôçƒ,j$ÙÈ—òK,þIo­ò.à¶ÏQ—ý6Å”1f_ñ÷àhÔ ’>J£%;*Š"Ke4F‘]‹ •ä\•ȦDe´¦FRFDdFR}È’r-ØžI&ÉM ê3/©ð, ¿è¿èyKpQf²_—Ç¿`ŽÈfÍ’ÆÛ6¸³,Ë1Ig™v,ØÛbcÌõ?E´<”Ùu±}EÍľ&›é©>§öU3¬ \¦W,qÅ›ìúžÐ,TK=4g„~_ü4ÄöYE‘bÈ÷,Œ¡±AîN&qìcà ŒHÄË“ qö™ò%öƒ¤ôÑé”Ñ–50R,d©”¦,e6SLkSÖF礲Y$²Ïä–ÿ/~ØÕŠ•(P§Jð5)2¥XÓBM•È®‡‹!Èd›>ãîO±9—Ëä¾Cm‰´,ÙvZy- EÞÆõàX÷ãÉ%™fY–eŸK Èœ.<¤Ç(.<‹¢èy%ƒéû–Äy*˜½–SÉ8‹“íC© àÎ=ŒR‚¸ÁUf’z1Á45ö˜rUU=?Øñ†AXÊE6ÏMò< 2ŒJJdUþ?õøI}%’É,þK2ïØ–I%·%Ùê3Ôz.,£Ø¾‹*ÿÏÇãߊ–GÀ–Ä´<~ ²¬‚¬†„¤«+©!ôJ{¹+—ÁáëžütËØ™Ñrâȹ›1Ê ¡d Y&gì`Ò-‰dg£I_&\øzç¿HEQh¢(eg¥vS¥VŠ~Ê~ÊÂbä¦ö=1a¢…6UO%WÉCÓÙO \÷®HfÍ›‰%ŠÄ2Y9 äË2¯’r-‘|‹eÓ'/Ëö'/‚_ÁrÅ×ÁråõákžõÉm–EòPböYRJ‚Ø–ReÉ,•œMÙóÖ_#ɱ¯\ŠÑetŠ(bŠ•”Š2„n 2¯Ü®Ê¾“Ñc(¨Ô *Èex †AxÜY“‘. p,¾Lœ²\Af‹²ì{r\yl¾Ëþºk¢ËCcsz,Xlý–D“ÇV.MGMðÑì41-ÃRF†¶üQ8“‰¨ÈQûLù1ˆÙŸhà®$bf’F >J¢¨ª*Š¢4U H¨ÔŽFú|_ðûxQ’ŽÉ'ød³%’Éd“ÒK2zOI'§ÇIë$ôO§Á1ÒIèÜ“ùË?å#òEÇ|éˆÄI#ÜQ\JâUEñBä÷EvQ hJH t‚;}Èìöëí'·øqøÅÇzÆQ †Wd2¶Í2Y³î6C6}ľ’Oó.‘®¾ÇÇHéï"èˆ×d ¯ÏO|EþÂö=Ðú{ôÑŸöp8¯ý>§&1SLXâb“ÅOÉ\dJÔ1RÅŠLª‚¨¢2Å$b”D"„B!Aü+Žõ–‹–-²æOBÓ.‹¡rYEÙ$’¿+. ’{§¤õ–OId²Y,–K%’OðãÇ~Z»×“.õå«ËW–¯-^N¼µq嫎õäØñÞ¸òl{ñãɱïÇ…äØ÷áÇ“c߇M~y6=øy2ç¿&\÷áäËžü9òeÏ~ù2ïß-Ãûynùn<¯-Ç•äë¹r¼qܼq嫎õÇ“cÇzãɱïÇ…äØ÷áÂòl{ðãɱïÃ&Ç¿<‹ÿÄA!1"2AQaq #03p‘BR`¡@Pbr±‚€ÁÑ4C áS°ðÿÚ?þU¯¯©û®g}×;¾ëâ9|B¾"æZ…øV\ƒî¹?uȹ å+Bº­OÙs.eÌ8\ásª×æÖ¥sÎWÄ+s®u¨_…h(\‹‘r•¡]vj¹—0\Áj¿6õ+˜®r¹×2ÕjE \«•r®R´;u\˘.`µ _ÖÇõ&«™s•ι×2ÕtZʹ)Z×g2æ œ.`¹‚×õ þ¹Õs•ÎW9\ë™t](\‹•r•¡]V«™sÌ2ÕkóSOWUξ!û¯ˆWÄ+s.‹@¹Bä\‹®R´+U̹Âç ˜-œ’šúº-‹OSUªæXî¾!û¯ˆW:æ]r/†¹ å+ªÕs.p¹‚æ _äÎùKª×ÕÑh´õu\ëâ/ˆ¾"æZ… \¡r.EÊV…uZ®e̹‚æ Pµþ ü­ÏºÕjµZ­}M‹E¢Ón¥s•ñ ø¥|Uñ:æ]r…ȹ)ZÕj¹¾hêµõ´Ú>l›#æÈù²?速Ÿùùe¤ÏÍ“ò'E§­ªÕjµZúú-?”gøSódüÙ?6OÍ’ŸÍ‚çüF‹E¢Óåó¼ýû®RL)™Zº!²BÓ e €҉vŠæí»fŠå¢Ñi²í¸P~V;߸•ÔÐå§ v °›¶®‰r’ ´¶Gd§)B2UЋŠ2Z²P!ªm¸… Ôú€/‚Žp¯S(g ¢„Ü ‘ ØÑ l.U¹~L»ø ³˜®b²T('dªÑ«¢å NWa·‘C𡬅k‚<0Ôa¤”ð¡ƒê€r-Ð)ºâšëÆ!Êzì s.Ê©)É©  ßÄÄ<•_4J¥âžc0œî¿&O¾2ˆ± A¶™XZs¥pÊÉN"Jñ«V»eh¤©B›V*å+#Ô“>®IQ8Q*Šr¸œƒnÂåª.Ä¡0™Œ$g¦%+-Ê襭”XFFt¦„´)Nê ÓAÀB°äàWr‚þ¥•q5Ñ* ‚†e8L@§9D RNâW+ņwáBѰޜ 8N†¨!rKƉÎd…ÑZN!×”æ ‰Ù…¼qV ÉÕT…+Ek‚ºÜ( !M¸X #õfŸËãFª Ä÷D•Š`ÄÙBÖ®Eu¨ÚÓ+ˆ8J¸j¯vsP%ɦ嬸Î$t&Æ!„J+ =ÓÕJ»Å7 – IN½<Žé„…’žëcÁ;¦õMWžb†u9TÂkwb~‰Ù ®vITU§š¨¹ÐQƒt&½Õ~ˆ¤jžœ!QÆü¨†òvF߈‰xâAÖä¯gÛ*z! ½ÅàŽ°žÍÔBuJ™„*³D×?ñ*q–…{1”vW462œi¿DÝåH'¢npP%Ù=¦Ð2ƒNËNÎT.Ù6•¦1ú›E¢Ñh´Ú?…€²° «¡@PG«…‘îpµ*Šâ ¼©QzæW“%A8VˆC1 "NT5¨K$¦ðèšÛT¸AÖ‚àÊál»ž‰·…Ió¡Wï{‹­Qažå1íxÂsAèNà©C„4ÿ„ÇȈ„÷“Á ¦è˜”)¼ñ•ýD'»ª8âì©nþª‘o2vð@ÝS'RtMtjžíuÂ{¢ÝQô‡èÝè?ÂÜPÓ©[úút ®~ÙS#Tí]Ôª`>Ó,ˆê¡ƒ1*™:Çï~‰ïqáhW5ÝSwua5µ>(¼1H7Ç ¶B&au:-B‹tÕPf“ªÝŠXîª5ƒE.«[*AªÓÔǃG]†ŠéAïúy¨½ €¥ÐV£Ëd·UŪÌv¨IZ•®uÆ62ŽT‡¦Bµq°¨ £ºÌ÷Sjâj’ ¯…Êv]&vL{ü[UÌW ˆRJ‹Êá0Ê€äL•i8ðFÏʲƢÛd+ÐÑÕnmú§‹fS­£”Ðö*pØhÕ A›N§EOvÜ J5éðW¸†€páùè¨ê¹iĬԓÑSs_⟆Â{ ÁwTÒʇî©SnJ§Lk…LSg‰1Ý,N ¦#ºáæs³ “›ÑqŽUã9è³ N§ÔÞÕ×ðµf8ŠÝÇ ‰\£ Û «Ã†á^óÆ¥E¸Ñ¨º%ÎÓÁo«<Ø?tm6ƒ„û]r¦` ”Í×ãº#k¦âÊ×ôp÷ñ;2V‘9Y(:ð¹µN…Ì ™DJvÀš®2M´aJ|£pÂá+»Ê¸œ ¥NËg ˆ>:!àªÑx-Õ?©Øç¢uK,Luª®4OÆ…K0šÏÄU­”jUÑ^„Û]‚žÛ´U[Ø«Jæƒ{è˜-š{ #U¢-(NŠÉ(²«2ÃÃ8@²6x'Tµ‰õøBâj› ˆÊÈSR¦r¹Ê´8Âí4QyP×&‚pmØP»AÓh1¢.v¨8! Œñ!ˆj{â¨ÓkyŽPhÁ W»$h´Õ4à(è}¦K“Kݧ Ñ0 ¿ªh¸HO¹Ù!T.w2ž‰£õ.6iêd+XݘS+¬’T\aMÅs•‡!/RJ•VgEm_ÕaªÍÚ”x¬m³±×6JáYÕSÊ&ýUkN%[t™LtôUP迤h*L.ª:y•pSÃŽ¥2ÝD “1Ùª¥ä«Ÿ²=åà™:Æ÷šÂ¬9JöÑœ ØæÀ€k‡q[›'´lÞTÃo³ðj¶Þ(Õ4B§ýZúc˜+“)”K¸§ˆ¢ë¢ž²·^Ž1:¦SþmÊ2¦‹ +Ekج›VZ¹ ä+”ìÀZl³.€•%}h $( î&1³IVµ–Ï]º”ë¦J‘1â›’#¢qd´ ëò9Ë„¢.CE{ŽU¸óA¶ƒâS%£NèJœªnkb4@–æ5UˆÑ=¦nwUD½†WÓc®êšXÙÎ]æ·ûÜöO|jš Ä­Í3¿tÇ[׿À‡m^ŒèUz®8!_ÍUúx!ž3Õ>‹ÅQ{[u^Š÷œ¸¦1ÆXV² ·¸{ΉţDþПMÍ’_û* aàŸý©Øü3ú´ûò³²c(2…Ã+&BrqA¶ :"Š(§¢SvîéóuÙn÷cDE;ƒM5 L™@ç(ä§:Ns”8¸ûl½Ú.°_¢àW¿•S°Ôv|šÆR»Å6ÜÛ ­ÊÒ¹ ˆY UƒÔ/qµ»!A­Ñn™©SUà’ =w%\á‚öÄ«íáïëÞàOesA0a` Tró…ñ –•.2S˜Óêˆd+ÉÊj©‘.êž5»¯êÓïâ㳜¬9s•̉œ£•WNVF³²BÃ~ªKd¯† §ÝQ;³(˜…*ìÊ.8 è©q„ x¸Hz¥Ä^4NeáKž2·¡Ãe­r׊Ì"ÍL«#0›JÁŒ!4\ÔîéûÑ•-mÏ+yéË®_útòÑSs•¦ï¢©Z-ºì©ShæÔ£xé„ó`s|›)ý¤YÅÝR`mÏ'UH½Ü}yðO}:·Žž?ÛÉü½–ñâ]Ð*nèžÇäï?GßË”µ]Õ`,…¢‡(î.‘êʃî°=~b°HR øŽW•y(µ®€¸J‚õø~È^vnÚÑæ˜ÁˆSºl§\.”Fìx!ìø‡U~ëÿ)ÖR3â‰; + NpÔ:Épö2£ªe©óДûy*we8Ôi…Z®øº! wè™è‹ÜxŸ£Q? Š>ø£; ¥LJá4ýTBv§^ÉŒ ”#À4èÙ(Ë#`@ʽڮÕk„bQ ÄF]Žêæ>UÑ”„j„84¼Â¿§ÀÑ]jÕ§!Qi•–”\ì °·wbuM7LûïÇ©§È2Ÿ¾€’­ È\eŠKp¦Ôe…^-2 7NËeB‰.Ø%FñeásËe4ï­ú¢Ö¾S‹ý#‡Ía(PèNc!j€=Ó-èšÜTˆU»'´µ0†\›4­jÞÈŒ”øaVqèœ!ÎOžŠ_ôÙe£[•æÌ§ºÔé,úlãû']Ju½x.•Ià@*³z4*ƒ2 š.ê˜Ê7è©À-s§Çe¡86¼‘Ñ^÷†×p=”>;§¸`©WÈ#Ãe­BáöUöñ‘®×eì§ÅÓº °Üz*v‚]øŠ—r"-:ár•…¢qkI]†šìk@ÉÑ\ jpp¡ ´ê?T;ùV¥`¬’¹ÎËo0 8Âã…ÎW:—@J—™B“csRHˆÕT-ËJ!”Ãgª8PÚÑÓdô@ xž#ÕR!¦SäQÝÓ¹Çólö©Üwx!Mî±RcMÀuOvôqªÇ|Þ"‡´–Êc·ÍUiÓÕ<Öô¬v'Pª=Õ¬¦2‘£SyáWqÑÑ íŽNÞZÁþvT«©è6ñ…W$»(o59Ø5ÿíP,0J¦Ês‘˜OcŽ4L{´ºÐ…>…’Uv5¡¦íSZæîUÆp~"­hGÑèçó¹oëáƒAùyÇå ìi×™Wú#,íe0Ø_=TR8µW3ÆOdÊ…ÆßòƒC¸bJ~éæZ›ÇÄS`È;êÔ²tMàõG=•ïphéâ›s¸OÓ®þu¯ñ•‡õS&Tï(Úò'T.q;8 À¿]Q°¢ç;7x ðMº8U¸Ó›ª$fUÐ+šÆÉ(µì»(46Ö£DDº¦ žg&—ŽÊ“ÐvWÔœh®dø§\î'*ÍÑ®[«Œ ;”ËjÛªÜ5éfîwa6žøK{*—»$uU‰¨w@˜.“dB¤Ð*n¿ÝÂ0¨5¿ûb]àŸk¹Âô[si¿Ûútþ¢ÁY?ÂêV ʦ ‹z_ú¯dm’Ü,(-Ê—4šR ª…ôZ¦ñ¢ –•—å]…Ìî°S!sºÔ(ÙÄÀœÕ6¢ÉNkB›Á†Ö˜Ø.Ñ^Õ†•´>æFAòAØMº3ò²Â¦é(Ä C‚¶ñ¢Õ4Ür‹ÁM°¦ºP„)¾ ¨'£æ„¯ON ¢Ÿ)Òœ!=·`Cèbap•ºü0ª6}͹ò(ýU9ì9°Ÿ-µ+%EÅ`ÂˉCˆ©¨âŒ:ã²BçSz‚õà ë—EuÍú«p\z ’­Ý…kéÜ› £¢»sÅÝT{º§Sm8ž©×2QtD¦Úyî¨0¦›`l4Þanéºe1¬ze†cR…¦j|ÿ’à0\Áò®–•ˆÑ[ ðýÖ‹@Ž8”’à¢Þ¸Ri•Ë”e°' HÊphê²Ã¶\â£_T6atÎÈkÈ ID·oPñ²XÙDWeCÛ]»0¥¬%q4…6˜XY&¾(’àæ…©5OTfÖ„ØØòtFpV5…Ä6(¹Óª"O4ë‚rªcªsaålx ®ŽˆÔ‰r—©‹B °h‹,d_l¦yì$²B/ ;.ÀFÆZ6ÎBö†Gt×C:ª¤M:¥3ÅÔ/©SƒªiÅ«wE¡¹L î'uF¾ÎZnz/j!Ê*tñÔ«þ°žNÞÈ_Çäh÷ØY+˜©¸Êâq+…Ä.'¢ò®¸Êç+\O*å.2vp«®ú.ˆèe[ ðPt$m¤Ð{©(fëî­sd(¥Jšfäר|Óa„CÚBˆtì,~™&˜ó (·ÍÛ7u &ú=7põr¶ÔªPº—  Ö¸-S Ãn`=ÃT+T«% »ßg £Yª³åĪn¤ñoTæfWÖpœæœÃ¨ùCŒÓšmÂåQb"Å6–Êm­˜i(^Ç®`2 …2nZz·L ¥ÅÉ•uá3³…FÈjÁ*wh8°ÁCÙœ¯„åðœ³IßdÓn¨W]‘%`©&U¡æ;) ‰Ä©c¡IùEoEŽÈN»:'J¦l–ê¢ÉñMTÎÐç \0œâÈ)Òéú)€Œ€6@r .ÀET`9N±„àÖà'Ë@ý×'î­k¤J§±ê¬™âBôÄê•]‹(i:¯jxÊ¥-ÓUt@éò¿…q®u¨S(çUÑAÙ*@p€e¸ ÝÐY Û|$of<ÝÑÉêQ ËG©™òOv`÷USñ&W×W#QÕ~ŠîFx¦–Ô“±Íq„ç7öMfFpœa1wÑg²wì5ûÍz| >ú´* - ÑCŠ0 ¹Z ½Õ#Öµ¯(›W„FØjÆ}hh•UÖ˜õlk°³ë䟔߆ÉD9:Pö_DZQjjÐ'‹F©À´ cZÐ6豤¢Ÿ,‘(à%Ëze µÝ“Cò‹B )–OŠ‹î¢ã)âü…íbL1\C:wDµçëë›"õíÙÅÓ Á•H€'¨W6O†È:-ɤÜu)ÿéÛ,U´)ÿÀ«yìmG¾Û´otð4IO4«‡–ô…IÓ%Ýcw­—ïhÛ‡áýf}ö«âþÈqNØ—J€P’ ¬B-=6@z’dì–€‹N£Ô†…ÆÈ÷Ù+o ÈB^L(uBB¡Õ $(Þ(kнú&8»—DjH.Bò­`jsŸR1¢©{Ä!ÃO‰L%0€,h[²Àà¬4Ûçÿ„æ]!V¨»Á6)dJa©@—5o^Ü]Å™~›w <ÀÂk\ ]ÔŽ©­ctüGc_Qð•J³I1ª‘¿ÈB¬Ç:Ë» ÖÔ½î<Úª4ÛPÔªôé¼6zôSSÒCv*•mèÀˆóO-x2‚ôzÛÆØÙýÕn1/’®ê¯o©›Æ¢°²éû/L3ùQcLuOdÌŸwÔÇß%æùJp±¿e–·ë”ÂЭ-6SÂ{Kµ–ʳ'aOp›–C•¤•:QsŸ€˜ÙA­&:«or¹¯?T½Â¿ ⟲-=¡llˆî‰kÚUÄæ€¹©Ü¸DˆÅ ’Ñ¿tYfB.-Çš{ŸJåPS§¡Ó²ÑRÊd«L±¢ òAÔØn%»tù+m3ÙIc¾Û *y’uõÚÙz¦ñÝ> õ4kFÆÔq]6ïçêÑ%Q·‚™·¿©ÀÂï  ú“ÕÔ®úÈ£ïr²Â¹ ÆÒ¸M0Tñ+߀„JâvQt˜NÊœ€›°ÝP'ÓNð íëP,«ôqüjç Ö¼'6ñ*Û„Â"£ç²qß±;Œ»Åt Å=¡ò{˜ÆªvåµTñ\-—‰¥áŒlýUÿ$øß'öUBUAǪ̀†£Gôr0MþÅéŒÊxÞÿ ‚ØÊ`gâÕ5ôÚ'™PŽ,la°!ìÆ¹ÃìþEA=û '…_»¿”*N êkîûTßLDôLú&¾ ¸»:Âk™ƒÚgcÝ#MJ<_ÿZOæNºìŠŽæ1Ä™}gIð­HT¹ J­YµA!Ä‘°æ5)Õi×kíT²0Jm2G…P6ô„Ó¼dÙ ¿DHsÀ’Sï¤ÀðÕ{Z#ÄÂÝÙÿ„ÍÛD4й”ä ÂÃqè‰4Œ"ú­6ôÐc˜n:I¢è@9®sz ¦1oêRï§ 0¢-:ûˆ€¥çßk·ßuÂâÉ^Ò¡´tMu"ë‡U;Ò¸jsÉ„y‹·™*×? Ö:öî$ç_’›íóâšÖ·S²Ñx¨Çš-EÖ¶;"Ö±¬žÛ ]ÊÊ´¯A¶°tØÚmfGR€²_9*ýӮЕ¼°†Î{”ažå:•@cÁ`e1ÛT)¶âF‰·ÌGDÆ3‘½Væ÷)T©dGìšÊ ¸õyØ÷Ô¨Öô„òÇ\ ”æ:»eÓô•R–öÓù´TÆü`ýÐö–Gâ9Uê1ÑOEé\C‰Ï®Êìž"œjÖòµ¹’½™‹®f&·&/éÕPkuÇùLªÄW;Ù¹×eB{aWi÷aQÝR¿º *ÚßéoýÓ,å 3œ*Q¿Á{ZmgåWÝΧ^Ÿu^ç Ϭ2Y‰UOщ"|—¢¶ž7x¯ö„Xm=RŸz<ÓSœžÈ—küúÛàxª™Y%nXŽŠA2¸žO×dµÄïjìë”Ú®\âIî¬5 ½•­ª@[ÝῺµÕL)¦è[Ûø‘cŸ…`ת)º-nŸ©O¾mÏ„3Õ,)8j¯˜çßa…K˜G­6˜ üÏÕ Cî­½ÒW^Tï œ".œlg–<Š’{!u@ƒâ¾#VÕ¨+ˆè<×E û ÛVû©³÷Rö샢p- ƨÝNJ{)·®ˆ—R )e2SEzn¦niŸò Œìønû"Ú·ÛDæ°:Јy€œÊd¸¡ØÐ{ª 7S”ëkÕ”«_è5¢J¸€G‚½ GŸ¨wm”Xñz—Šn·¾ÂðÃh뵌³.ÑJm0¹öQVAZ{—Ö¼5­AŒÕ_xpN¯p w¸>ÝÛ%Zá°‡66À¸šGŸèÞR²ºÃŽÎw}Ô½%|B¤/ˆäFðåZ@@—“;xT…@z’vpBãrµ¯¿ÿ ð®‡ÍÇDcïAÃT&“J.-ôðO6‰r{·`¹ÝS`ƒšÆŸ\ã$í,s$-Õ VÊdÓ.€0‰s8Ø):&fäÇÛÃlŸ |;%Uàß)Ð¥þã®ÂúŽòWÓŸD:§•VI '£Gxèr™mk;Q«ph'ó/Iy~ GŒ#ææ$B©Uþ’9†ªÞ tÀë•Eâî}éÕZ *þöLô{î~4õ7Ðæ„-Ž *‰Åºªe ]SÝ!ïo*ßúC€ÎŠƒ©Ÿeø²«Øxí…[ýAâ3®ÇéNÝ»á“pò^Š ËnòB#—*™/›¶ìO¦L49P~g†å^­‘­½$äê¨3ðXp½!³…^ƒ›$¼µ½‚§èòâlÕziG§‚Ñ…¦‘ûªÕmÌd§[MïŽãPyüQqO{ïo—ùRÝëdÍÌ@‹ÒZê-o‚i¨ÑÏÓÍ8µ°4µVôštçÍî@q„ócq)úÝj©LmÊëÕ î¦ãÝ7uJ×uè«IТxŒ3¢ö”µ=JÝ“#•P–õ”ÇXs⎶ö ‡û¥eļ¢zO´Ÿþ"^<4F.»`©¾?dÂט*oÓ¢o´’Jø¿².çé å®b{Ë€lê®AR*´ù+!²C™÷A‘%eŒ¬"ꔄt•ÀÑ •rºÃ[Ÿ–BÊ.yà^ͪ,*êÒ<³dŸížÁk ^Ï+/—"^ëB¿u‡ÜWàöZÃ{®%Ï•$†«GÝk>JIHôóVŒ•&Ñü‚ªÍAöVÜ<Ô‡\® GŠbák4RZ´ù-Z¡ÄNÉ,”ZÚ`x£~êHJs¬á€Ê'r&¶™„©œ¯‚Q ¦I 4œ¾ kšBmuËá»ìƒltŸ¹ÏXiû,±ßeÊ~Þ¾ ó_þéñõ$(uG¶Xè(^òƒÝŠ3ß:¢`ëj¹Šæ* ‰ â?î¸\G’ây>ga yJÐó0¾;yâÌå\óå°î߯q’­uL#ckêbz&Ÿõ"Àݪ«©/tä-ãFǶAâ’J'OÃÑ8›LôA¶46r%XXÐ6 AWnwu^µOÅÿdƵ‡¢¿ý+oîžÚÔï“ä·té lì·"„ù”÷d¸§<6Ù[EÚu)´jÓ$ɬ¥H1ƒî·>ŽÒÖõ'S²£9„û™ä¦\sz+˜ÊuU.wÙPuIÀ  t´wW6ê™Ê¿y¨s )wDó¾ÕU7aÝJ%„½çí±¹„=«xUS½j¥mVj:¯ŠWÙ> Òö‰3ªsÛ3Ý4Ô¬×Ê/t÷â!jM°ØOî›(§˜V:!{:lŽå{@‹V ŽÒ¤HG+ÆPïÙR’£¬"H€„ E7¢]p¸…ÄòT•q9Pjhq„Ð÷›WÅú':…©iQzW>(1¦ãÝZ!)óQtˆŠ×<Ǩa²‹@ žÊÑL"ëDvÙE­=Ô:ÊØµ½¶)¼0ʣ︙(û&¢uŒîÊÒTŠpQsØJpe3®‹á9Ki’¤Ó láa+Š›‡ÑI™E¾(»Šc` æQ¦çý“CI3Ýhvš‚¡8õn5Û˧ÁŽ©¾Ðz{—Vˆhõ &Þ"ïsìé’‹HÈù$}ù –|šv5E©î(ì¨\–žM:R•”½©EYeôYe–P—èBîgÞ?ÈÝ7köLH…Âû‰¿ìî>âè™å;ïì'žÐGØ?Ì[aß; Š¿XÇüºüe/JR•õ,²Êä¡Q}x’w 剹Ñþçýü!¸_ò—Û£A®?Ü_ô$:âûNãû þ'x§é÷‹¦Ã½Ð‚¢¯Ó“Mz©JR²‹,³¸w:]΂ö(OÞ;âîýÅ·÷·÷„²ü ‰ö%»} íÓ ÷wî-Þ!"'„ÒO+ìvôÝâ**ýV Òý=JR”QEõéß;ǤQ¯CV‡`Qèûey‰¡X2_QfŸp-¯º-ž©<ÏØK݅ʈhj¿qo?Üî/ø"à~‚ç}…ÒBfŸtÿª&mûã¸AWÍ';eßö–ãôR”W%ô»Ä÷;§tJGqXÊw •-"cÈÎ;Þ+ЛLôü…£ þ¸ŸbqÝŽdû‹{ò2çÂ÷/ ¸"gù:b~Ÿp특ù"×!æÅOÓOä÷¥+(ïúw:d¹D½Ä]´.h*äò¼…ÃÑÑŒ¸œš¾çk}Î7û‹bˆídÁ/t[¥&WxG./¢î¾ÝቤÒjì;ˆ«à42µ³)JQEúa•Ð#%î;¢W5Hñ¡Ûô·=‘s1tÀÑŸp­¡ç¢£ð •±K£!¨vßrº9ui»AèKä©ú‘”¿8½)J(£¼wNù=Îç\»Âê]p…¼ èòÙíÏLùDù}ôÑ|á<^“àôùLö§¦za=„õΓ¤ôNžôé=™í*d’G«Y]+(¢2u_=Õ{÷åðƒŸD’zÄT"¬ôN„'¢ž‰Ö„'XB„!OD'± èÁHB'Yê„'I|÷ù¾Dþ5è)èsÓxŽD¬ddz¾zgHN„!B„!Bu„!B„!B EÕÏŽ‡¢„'D!=!Œ^ü'¢|§G—Á¯ŒYø:Ÿ¶µš~¨—†„!:B„!B¢tN…¥uJÒäZHš5H¿zF!B!Bd!:OLö'Æ.®ë¦Ÿ,_ãתIæyõì²ÎFcäøõ/au„!ŸQ¤1ïÒÓèJ±I}•0IÔ„'D!B„!B¤!B„! íODöô#Onãâï´½kÛÁØ“ªC%'JÂô'¿gZ„'HB„è„êA”è¢Ð!B„!NˆN„d! Ò¤!Nºÿ>_4Ûá2ž´NYï®ÇYè~ˆh)Ñ¢z!:Bt„"'D!º!NŠ!:!:!B„! Ñ>nO^‹òð+àµô#g‰úït ‚£Ö±Ÿ©{ú˜,N:cÓ=§Ð'YÒ¤é:A"„è¿cæÈ¾ºôùœô_nú¾†8`ôˆ €ít;ìî3¸ôžnñ—^»Øééhìmû^ ÛÖ¢õø µù&žÜ÷ôùnþ½ý+ÚÏM½/®»>Cuhj{´¥/¢—­)JR”¥)Ké¿&m¿Uùô®šz/©zuøL/])¯½J½7ѱ}8ãÕ¡“CèÌzBO+ÜcKÒ—Ü¥/JR”¥)JR”¹êR”¥éKñ™ùo]¯¡zôöêšgáo«>Æ}W¦Ý/Hd]Gæ=µÒ õIý2ù+“Åè·³èRP® ãÞðB22?jô½)JR”¥)z^”¥)z+ÚÇE)JR”uè¥ôßoO€˜ô®«Ú×ÕñK®}8ô,ïëÏTGèÕ0÷Fã«Geì=;Š–Ý¤îÅéª\x½±©,)F󡨚ÒíÈæhàW4ØùÌMiœ14\èPÉ1x74¤®ô–X}^Ømà}á‘Éæy] E1£TY|Á|W^ÊêBûT½)JR”¥)JRô¥éKÑEz{üA ½izßE[é“Õ>7>Êö³Òú ̳êcÑzk îHz–fi)äbVk°¤ÿF&’¡¢ÙP‰°÷cS+\ÁrÝãc‚[Ìš;ÔßaÙ0¸È‰.n6™J.†fؼœW-²<´h.]_ÁT+àX¥òVƂձ¾_Ô©›C L¤nCêÆÄÆ‘H¶Ö¨kH˜’ÛrjMÜŽär\¼:dïq8+ÞMJ„å)Úª0#Õ± [pEÁ£´vº„A'Ÿ^Ë,¢Š(¢ËëÙe–YEQ“>œô¥)JR”¥)KÑ{”QEYe—èƒÒI$’:• úR‹Ó}5`j?c>Ã÷<{ ¯¦ uÔ#ÜÄ4cm«b’ìÆm¶h… ˜}hÊ ¡ƒÑÕò }Æ8ÝôGpŒ.%ò;ê‘¥Àº4b¦k¥ƒî1¦ðŒ?9b‹Í!$èàh³—(o“„²×€ÆäØúSéˆ5`w1’æÚÐ\d°×£…¥!tƒ…+aŠØ§‘Œž­Ÿe?æ"þM¤Ðe8Þ»ƒzKZ°ÊžñŒ‘¬B…nŒjd ©tò.‚§c˜é¡}Jfü,DD]ã¥=( ‚ $’=·òË/ÕAö=%FdÉ“&zR”¥)JR”¥4ߢŠ,¢Ë+«áé ’H<ŠR”¾¬ü% Ôé:½žàC¦ØbzÊÝå »[zLY§±Íö‰oï† Ý÷:'رX2-‚’Ð2û~…ÜLìɵämiã¢Ë…¥>¥ÙDS¶M>Fª,m±’Mèÿó‡ñ‰÷袦†’äþæSƦ,Ý› “;Kbycœ…Ñ­p6h ‡Y§mŒ–ƒ¶€£†Á$)¤9²—ƒIÏ«­G‘6"SÏÊoÀ`ˆ‹‚8ê¯n(Þ;]b=ŠõÄÿdí–vÙÛ}rt2dÉž”¥)JR”¥è¢‹ö¿òOEäAz*ä¥)¯¯Ÿ¼l"0Ù ÜÍ7¢"å·Ë’m, &¢à—ž^MV| šiMLHI¡MÊ=KM¬hÊ]æb{¬qc_,´£VÞvŠ|Š‹‚l= †¦ ìdó-–#>Ÿ%”E1ÂZŽÂVx)ß!6þ⺡ŽVÃHâTÄJ­*cGɪ «"Ûé³nì!.Iñ‹ò²dicCÀɨµ-ÔduÊÓìgž$ÔpŸ•-ÊwÙ8[—ÅUØ@{ÇÓ¢rkà²-Üh»wèü¸¼‰7¢ôßjü’õ¥)KÒ”¥)JR”¥/KèÁ‚"pèv:]ŸC½®‡i“·ê`®NáÞê݃´vðí3¶ÎÉØ É“=)JR”½J^¥u,²ýÇǸD— õˆÃI;¹ Ån2µuC]o;—Ñ‚RFdêò8Z~DPäKW.öÏèÎÅØ ûDØfÿD’Ú _µÁwÈÕ©kØ´¬joÀÕîV I§ƒP§]Káx—–7²Á2NDdÝX®9 ´e¡—Q˜ÑàüÇðe[”ZÑ=ÚìdÇpúnÀ…}ybý£V(´qÈC=O޵16x$u“ÊØËk‹¡ñÕ{Ù¨ãXÊŸ#oOýÁ{4™S¦WÁÛÓ¹¦jfBôš& «V¼‹µ2Ï’uRÜÚˆïl\¬Ø)}*Ï%¼ÀCª2.ÿ°¥Î†¬»˜’ßΤûžèlæþÁØ“®eC¥P¯]4B”°F5qjàRU%PÊœ¢˜SÜ7D—B‰B9=*Ê›‹»ÑKÑ}T¾ŠR—¢”¥öï³JRô¾šR—¥)JR”¥)J^˜1ÇDàvNÙÙ;G`í£·è7¼O,óg™\üwÑÝGˆì¾¸w‡pwÝS/¼ÕëNEdBaÅz&CÉ'éÐTÔÅç¬!†`º;#xfM>Ʀö¹Ô_ vci˜kJÅ´Í’<¢ÈÓ¶‰$ÇcÈ[T·’˸ûhI¯“WÞÛ*7&î3b÷nA!-ØÎû`‹·CaãÊv)’½?ôR«ðb¡6VølþÂî9v†¹éŠOíøK&M¦Ð£-áÙ™yoµ¹o`³–:gVg.ÜÇ¿l9¦ Чê`2z«„‰G-³Éºäy+ ÞÂCëÁ•$"qô,ûéËb;4Øcl?°Ï×>¶ã¬Pðœ2¶ö ³ÎÁGLôü‰»nòET%ÄßÜkÍ©7Hèv½Õ÷C+=Ƶ¥ö!äüŒx4´I¾ŸýÔz!A Ñ8»’m]Ú%—zôÁ3C7×ñ¯î-`‹«bn—§;>MFi¸Þ¨j‘Ò¾:0HÃÑô4¤šMXHÝNõ>/¤(¾Q}t¥)JR”½¥)z)JS pxØ;u¾âè–´mµ>Áü¹ ?ä_NÀÄÖ]4B÷exO°É”æåÜZŽgb ñS2Óþá% 1¶ƒû–E‘Aä4In9Âp)¢¼–U‘TO%‚–ÄšìÅdïaåTÑ2u.¾ŒB/êd”ì5?äN5“Ñ=©½JR—ØFÛZéÒÞ33W<•¦XÓ: é¦ôû±sT8eo–7¶’Ná½+ÿfÂníG)½.„GMªBZZ{2ªðÚ¨R©û3#¯@צ­ÃøØDÔ™n?…{ßÙŸ&“b¿­"xƻڋ*¥›¨ÜÄPÓc‘ /r½¶ݼ"'Ã]Å'!ÿ&ã`àdÓzìb.ÈvDåô-a–¿‘{{+Üz‚žb&}çvM¤:ˆ8a¶ä&¹oÜö%Ø“ÓQ}wýôDî~ÈX\ZÒ›¼#.Y¦ÅŠÓZ%¼'qOFSÿwcÏzqî@ÿt6˜ŽËÉž\ÖÑ5ò tQ§îrð=ËEmðIž ƧØc-B:½o[Ò”¥)JRõLyûÍDfé ´êÔ‰˜rùŽL­ô³ø Üãcr&Â-O÷"B ¹ð*Î"Ü«ã°âf•Ql%[ˆ||3› àdòü%w -–Øb÷­zصtžÜ˜M­‚'ÙE7dÊÜXîÆFjð&¤—NÔ;ä¨0­krˆT†NSŸJ¤¸'ç. 5 €xÆÛjñÎ윯±P66Y1×8B£ª²71¶ì£Ã^$ê/µ£XšŽ.w9#Y™ýšöA­¥Ê\¶³0‡øÂ«Á4e¨±Iè#M‰=:Jè«,ט+Øœ¢ˆ,½|«×W»šˆbLLI ƪjHF¯—M åWD„J^EË–ãbiÇÉPÜÓ_´3´ž£|Ȫ=Ï‘DÚÅ Okº&-Âê¶õVr?:ßeÙ ×ÈÁLÕ¶í!õ¬W–ÌT†ÎZ—Ã?Ý/äu¾‡äïçAlEÄ‘…$µ¯J%«í‹\6¼ddJbA¬Ñ^ÁƒBS~x.óR^ÂgeƒZ–‰-ëcÜeí4ý˜ù÷š×°õ ¡3ÑFÉÆ¡‹T!&ݼôqa]Ø„Ó,׃t^hÑ´›ÕÒj™Ë¢\U©”´Éž•Jç[‘»1ÍÃãͽØå&ʼn¢ìŒ4‚ãD‚’Âh«e‚9)½]¬ì£’h°õFxtž¥žpú ,iƒ²dUUÈõÚ¹Ñ"6›Åì'ÊßÚnãj'õ>fE£Mhò*DTÒä¶xúçó¨Ò⦢'‹D5±eÝyå&%õІµEàrXI !SI˜Î÷3ûwÀKVb¾„"Ë?òC»j£ °û‚dæ1Ž ÑAdôu¥=Æá^.Óõú · ~z-ûíÉåÛBšöOÐlË8®p·‹ ø¼…öÜoš£þÔJ׈ÞgähIy4Åtê\_)ð#9$ƒƒ5R¡¶‰¢”9h™´ÿ¼{ðx7Xͽ¶õd'´ò#¶ç’6ëu‰|+ò¤÷„­âíÈÕŸ‘•¡cM4äf‰“yѶMKD!"Óp’ÐG&Mr¦ƒ¢q…“Gïmßa­É÷ sj…3 ×°›ŸØ/°hga>ÞÀwS™¯"¯BÊ î'kö2QO¬Í±¬VÕ)LN;!ªs«›÷´üRRõÀÄâêÆ˜±!©`hµ£­Ò7¢<²)œ+a<‹GYF ¬cfmzvMW ŒS#Zo“Ø„ÙpB-ý‹‚LŠ:ªT®æ…±†T–â,ìz[#vŠh˜Û5¿‘îÌ5Ýô~pŒ™•½y3ó`Õ¥´¶ ÿÜÑnÕV†$²"à•Ëcyêû Ù*/O?¬£Ë‘ýüˆ+ëÂ:œ± gÈÄž§©šxôî&tˆlë‡beª ¨-VƒBÝ—õcüâgXOŒn¿™¦]£mŒ#‹{‘Ê¢jåÓ&mÁù\:%» ÈýX¢yàAF½„+mÅèN#øFëÀ…{›ù1Ôj·0lØÉÏ“92^ƒÐÄX…º ¯á Øs%ÿCb'83­C˪k* Y0™­¶Ï^6µ §% 93Õ!^èËí]ÿ$>“Ó£Á‚*zg ðT·¼l¾‚$³z‚î³­Œè‘‰ÆÊÎ ­ƒqYŠVulØÒ[ ;‰¬òä~sn*³Œì·x ¹ÄìJ=ÚO©­äÑÜmõl³ž¥}Õö(o,_oSƒt—nŽl‚+°½àž„Íú &©ýOôXǬÓå ¶¬Ûåˆ*ÔKq‘¼ÁÑæÛ±ØRXKöÈÊÜßào³}Œ|lùr}¿uûä®pX•Îh\R7eÜÍ«ò_•È`ÊG-¨í1œ ŽÖ-mˆ:ˆ{L ™¾ÛÏ¢…uGËkÜÊ©‰ì°8’PFÃuÂs£ë‚à„p³¬ÄÄk§°ÏÝ KîQŽ1‚j]y¼±ÉôažÛÑ¿c6“‘|”“•ÜÄc•ž…Ñ} îרTÖ'Ê£©­ë2¼ Læí‰ÛÛÀÛ$99+FÞ¥;Ì6\:=˜æÐ¶,é)n€{ïèÍZ>å,*m×íAuà«ÍL‰o¶œÈ›ì7¥uBéõ Vlt[JK/"h¶«ö"6”ü"O2—ƒ'Ùka“H@_Fã°Q?4w¶Ð¡,a k–2ºæá¦Q+¢gþl«É ¢K 6—iÞfÕ‰|>ë5ŒùØE’fj£Ð_ÊüôiÚ«ÚùÉ=ºEDêY™t‹bzZ <ÌžïGŽtñðO­ L2Ë‚·‘W@Š$¦æ`•H¹2áŽ7ôŒe]‹™“×7Jƒ‡k¬ü!øŠ—›£°%“1Z‰½–Üp1±¨ÖZW¦Nv)à>3õmw«.ÝP¥ÁküŠ™K§sÃàRß=«ÈìâÆÃ{[QjNª®¡7XåqÜê$Ù&¬²/ l¹áÜMï?av£o¶ÛY{¶`¼#m%«32]«Ö¤âZoweºŽ«Û¶ ¼jT/$KD™šÚªòchñËŸ¡“îs"ty¨|ÕVx‚Ĥ[¡ûS]¶ðk=½²› ôí÷×í¿qô%"H«nDQÀC ½Oò H“ìgm·c&;D§‚^|E6÷hn²Ü̶ÛÜ]aZ|†÷ÜËÖíYT°Þ^Z,äJF¶[»ò^VÈGÉbsÑ %t1 ˜ì¢XÜAEË—¡šOÛØG !˜w1õÉVVÄÔhû þXƒŽ¯¸œqp"MM¸Þ G&ˆ% NeeÜZ÷jŸ‘oÿ ŒðãØ˜ðJÑ6jÂ㦔¦G2']Ý 8$ÐÜLô“X¾Ã­ïþz3=µ*¯pN½ü#˜0×ê`=ÿ"$…¢d¾={W­Ô¾õù.¿#Zž}ã–‚*™%O„c}‡ªW•ëìYÍk.—¥*GY’[ú7›4¾£Mš#Eö0jKð5jLЙ¦õÃl±œÖ8^$ùGýb….D38•ºÂŽ;ÎM;)zF E²] •}ÂVRoÝšƒ¹1û&Ý!ýEˆÔ»Þ< ë #l'¨}¬¯£‚ÂÁN ¸žGÃmÑ9´-òÐ÷:ôÕN æ½WŒñäz¿È-ûÑ€jÛô!õºüôÒõ^Õë}‹ÒôÁKú~K÷¨{TGpÆNAü"þl³ÚiAÓ8µÌLÆBËAØIݪàk‰À¤Z'ŽŽ¥.Á]®ˆÉnd—öŽ´­vWh„NŸfWÏùR¬cÍSŒb‹EI%J3S“b¹ääÏ1#rÈàiÿÔ4g§K“wï”ðR0òiûÁ:§]FªÇàiú jKÁéüˆØšk;ôƒiÈÞð“B››Ç«½?\ÇÃOE4gñÛ½/¢”½/J^´¾Íè‹òk·ÇOÏ{t^‘'/¢1%ÓÙ`q_à ek¹OIh—ö,¯ŒØEd“ ̓|t)fë„ecÂY0œ¯N–—³†3^¦ÿš2ö>„šÑ]ÄDJ¸ÈÌ•ojPÒk±b ¸zü²5lƒÉå6º‹åºŠÕM^¶5ÜJå†&¶«“+YVYS¶dÆÉæsNòŠÄ­FöC%­È«™Éh€J›gt\ÚõF‹²› ÓùÄcê2‰!ªÛ¬"º=œ‹S“Ém¯Ôl x®ˆŠZhä!Ÿ}!_Ç!â”­$àtç·æÂrÅäN÷_è¶\ª+Ñ’CÄRž¼ÔÂÑѲ]ß¡ 8×"¹¥™à×’Ö“õ¨\kÑ«+o Ùqò£R-žÆ…Å×~›f2ò$Âô v›(¤ZòI’m3Uv56õÃ4~Ñ«/(Öh%·DNÖ’ÆË£°áŽiCÁ·"º÷éJðif’ŸAΗÑ}7­ôR”¥éKÒô¥)J:ì'Ñ2ûÓæzçç{ŒÔhë:! Ñ=t„'YÑèŽ:ìþf6¢¼8jjrÛ”I\VA»aÉGÕͲ³Û; ókÀõµ«²,{ â´¹UŠXíÈvòÞ¯£´Õ}Ãrc"¸§þƤküá8M}4~K<’ôÔ›x£cbžl¾¬@»ì*> —½$Ž=¼÷Ro…§:Nåj,d ›žBL+pÒÏ»ú77/·Dþ‡_ÈEI°¨$KÑcícjï¼iél›ô`(ãè p£4ØymISêúiÐitmŽkš7ô†¸ åÂMì6HË;ÎÁdh}vÿG,Gå¼çÂîsG§´ž˜±÷ñ_ð~8Fëãê/¼­ÙqKÜ\ÖÕmDýüwµŸ!Ù/8RÏ µì>‡nô‹û1GYêßÂ|µ¾ A,†‹ë½i}¥/Z^”¥/è ag“Üf£GËuo¿ÁãЛN§Õ— ‹bî!±]ܛΠ<‰Ó^Ææ˜Ý/Kš¨ì­?‘ ˜öjîÝ[èŠýºñ„‡‹—€` ©=ÉPí)«™@v;;ë‚= W$}9×¾LÌvÿ¡0”œš~˜¼°à–ìòY1*ýÜ<r¦òÈíù†QeÞDZÍ]]ˆ2ª“»Áˆìžì•Þ(ûmM!²&µàIVóŒgíG+½¶7èÎä?Ô¡æ!¾=þzô½/Kí¯žèb{‹èÑó»ðË®Q‰öp˱ùwàqÇ£þ𮢺Æ:Ýfß,}?Ô2?ù0—oU÷o¢ú/³{|ÖõzþãF£B‹’Šàµ,opú%b3 ¶ÂrÑJR˜+0*ôZ¦½M—­ô>ºÛnú«çuúŸÂâtE]±~MKñ›ÐÖeàsRá·v+6¿¡%63²‰í¸+ú_Àÿ2ð:%ÇqI´[A[*6AÖï5‚R[éG!ÙàG¼$iìeþ¢^“›­[ˆfyœ˜í}üÉt'PÝÉ#®.; äsBˆw$‹K+pW[OÙcºB;"ò5ˆÎ±Ž­W­Å‹‘ôÝͦÒÁôÕôòýü~î¡‹RBÎü$²iù9cÝ Y;²kâ"‹êé»)V)¸þÖÒFöRÒyàwbá#Iò'IKRxKËCKR£t*Z¶‚yç ÊBÇÿô"­rÙß…ˆÔÉ^ìL‘Yh?8J&.¤â2|×%ž„µ}è•T‘’ÓÕ—LK ‡ŸÏwnŽmú5A ØIú¯Ì—ukÕ=K6Žæ‹Š3J9¥d …Å3nnį¬: FÄ´È&0ÛеLصœÕŒ#<ñÙ=›|éö1ØŸ¼¡µ_%¶ÇûPêÑoý(]Úï‘,Ø'ò,)üx,ÝA7ý [>ïôZ[0.})Lú’P©eì2âþ/HRòfu·ÛÉq­5¢Ñ! 2Ž¥§÷ènéòzä¸ôÏ{¿Ç®ˆ¢ô'>eLô'»ê3ö.Gv‰ØtµÕG'¾†ö1]„–âÅm‘‰J†÷â!~Lç\›¯ T%l·ו®h“n—àxgU‚|#{wÊ‹fQ‘t_Ø>ÑÆHj8~‚­!Ô¼øU´Õ>Æš—nŠ&N†©U¸éÛF!é†ÞìJ[¾®çÝÐB†ïªèÅ .#£±W8˜ Í!á/²¤÷*ŸÓ6I+ì2ȼ£IÎ5íãÚ›úWËvè“zzo°º®‹ã¯Æ&¿uj@\‰uª†¨ôÁ–¯5/7é×Xâ0šZ8(±ÖŒ“w{êØ5d‰Ûny”›îi#*C~H~ZX¯Œ·f3êKÑ]2G(ˆ“›+ú6ÕTC‘x!¨Ý›Ë´4ð)¨uÄW—AÌæ©éK£ïÀ toJÜÃ\ÚÖÑ© $r¯öZúÈöß#ÇЩN¬xÖ' ×]Åh"“ŸŠ.¦²8õ®üBÀYD:¯µÁø5è•î±>¨ñfn¼nŒÍ£/·®í¼µø™óè]…ó¿Ê÷w5ƒ^ ZîÄ)vdãÜ\›rÏÀ€a9DÔ¯#Òsíà„|ûÿ$ë~xÝfï¥_Q%¿ uÒ| ˜îÍ v¢ï{˜!½ŠP<†6ÞìKÅcЙ&›°=¾ }Ë&iÔûBµÿÁyLc¹–¢È’¼UÇL98ï´ËðC¢êÅåäéÕ)¬3•¢]Œ„¥FTÔp¶šSI;z–X†#üøþÐ\t(Åsõ7¨’šÛÍ+«âƒÿîÃ? /4]òG“Çò!ËËŠ74á§ßãëø„cÕ¿ z'= úk_Œ¾åöi} Ÿwq`KCIká÷F¯ù¦5äWàOAªÕ``i#.ÐGftü #¹‡Þ¡$°¼‰kvÄæO(kƒ<q`hˇѬ»äÄ3Ü[UùeÊŠ‹1…»¥e9ÒTR!]3zg43O(Bv“,™OpÊOÂÁØOP]䙤²!²mc4Ç£¸oVØÞ©ËŠ5¬O”á|–é¿ØÚ%Á„¥nã·]Ô8°Ž˜Ïm&ý!ëd.̳Ù!´’S£I1€¸º„j5¹–I5§Þî)jBlŸã¬Õ¹“j?¢w%6ªQûÌwÂ5@acS¦é»c̹ÐÝÂ|÷ÓKò mð7放æÚû©_ÝÜÔÑêÈ…ç5Ë¿_? ÇË6¥µŸ“gÙüv’1)ŒÂ‚àQD˰ÃëÛ[JP…±£Dcâ-qMFÂF-v5îÕÈ/ £ót-̾¡ Áü=>·Ú&´¬%Ÿ9iÜB ¶C°Æ¨o˜dFúr•~¬ã Á1\Ôÿø|5K=ápÕÇÖò}_&¡ëƒhvÎ8¸Û¤]´š+Fýz&XÛ9d–Fì7†ÁÈtÕ3"¿t;㉠W™ 3›†é«+°Öã·‘ }½s)Õ8TnF'ÃP·GúòmE÷Ð4qê†wçÑŽÖ\á!{¶õ[EmÄŠ ž2ŸW¦„²’Œ¯TõéØ%ge‘üÒû(_~)Aëî2lóªû/à[d=E‹MÛ*•ÇÜÖkòbß®ïBUßrKmÌ­þMÃL*6²ð™Ž–[×dyJ¶xb ¦Í[ÿèUöÒwï©Ã]ËA˜à–­'·Ñ 1fTÚäò¼}†¶¶YþpÝØÜ, ¨mŠlR‹×ŸªZJ'…È­®Q#PŒ7Sì²®•„Ï&„-Uš‘å±¢ÚWÛ¤HW½œvÙÌêlw-ÃÔbIá7¾(þ͇Ôb f GÛÀ„øH‡^Cà›3Û‹-œ JÄ8ÛÊ]j=9bgg’7å,ÿjeÙ¯à0z6‰WU¾LŠª¨XoÂÈÖ»sø _ì—.” MFêcÖQ; ˆMW<”49]”4›Ÿé "49Û¢í\Ÿ€þ¹²^|ì1ℱM´E‚ߪì5Ooœ! ô_7šŸ¸Å)b8&Á jý Š>/0zˆ®ÄÉ^F>›¢\Ô Kb—Ðmi›¾•`¼Á(ÌÑ”Á5±^ì^ˆºÄNµóÒ5åë“å8<¶ÛËég3ÃõóU^ƒË“fÄׯžÆüÛ£25£³ä˜ˆ~Õ=Õ2¥¦©lvøÛ«+EØÂ¸¶Kã-Z,Lþ €•iw˜#tè ìEù/'ÂEñ¯âÆqˆíì)Iô…™ µÚBG–¶FŒ·ö9C]1Z-D•Úú"Eµ´bî—Ý”—Ex£t&uÏG²„^D®›Ò´¶¹‹‹J³ïQ/8%d“ÖêYïJ(kq¯ýîèh-5Ü2Ö['·^âeoÈòA3°GZÝ-ŒO"K×°­ßj2±”¬6;àLTV±Ù«q]»ýcW€úš§^o»E>¦Ÿ‘Ûqà±ûÏ& c=’“Ë\eb-%«ú™;èÒ-ÔʈÄãþ‚µ¡is³&ßž4?ùÎåÀ'ûŸyÓQe+p£Z.§ÀËdIøÏ;ò&Θü™3ÈV¡¢§%öÂ)o÷èÙùüEV"þE£½ TðѤw@Zu2î‚T¹m†¾Œ¬ÄIjÙzg•—÷(tÔ Io%ngo,§Üµ/äÚ>5 ¯MHp©W׿X¢ÔYmh·P×Á:zߊ³ã/¹}›ñ_Œ~GºùHDÃypÑ—•Á»¯ÉcYŸa}[äÉÏ hºBv""é!ÜU<86(Ûï×þ°`Û«Nâ.¢Õ»‘ôD±Ê0VÕ'_Ÿ¨ÇF‹°Ó»G°¥·{%Ã|t¤PO4¾ýçQ{xwŸ±r:Àü݆/»Ýµr´zú?\'Q€\ЋõQ:noüA-Ø3C0<ë—"a~››·Êš?¦9b˜“Ä\Nûv¢dkq¸Â,¶_ÔlîÈ:w0%†×ø;±)«mB —yrÐwà¼êãÔvŸ‘?£ñ—;™ú0/e}:e€©œìs‹»6Þ] ”õ?Ø"­çŸ;Týz#KÓtI~âöpå–H7‘%˜2È5œ±‰5™ÀÛ”mŽÂ3™y™2¦ÕØ=Û诱«73dç' GÙ¥ž˜¿ŒIô}Të ýŠ6߯OJùž°¿Û½*øºÓ/_ãÓ”c†jk „öâáãÑ_%&XÓ&à¼ÜŽtòîé &ÿ"Ÿtó’)±³éÝù'ß“fdÊFh“™:³È’õTúd,õãê¢C]¼¡'7øŠÁzÅ ?Ó#Xa%¢B¡v[úm—ð¸ø;ŒÇ?§¹¡ûÆ#4ÐGŒ›y¸ßÀ®0­÷/3Ë‚ ¹»Û݉sÜÈïíí§¦{ÉZF{³æWÓ~9è'¸1nˆ®i}Âwõi ]—L;ÑþLô3ëöß] vôÎ ¬v&û-îbhcÀ·gÓŸ‰ ÿüÈJô÷öÇ4¦:óY}o§lþýä»cN¸ôË jjÛš::¦Š 7í™7‘sH.K,y>¦ éÔäD¨Õ¯Yèúø†£Àýß5_ƒj@±Ü0ü™hQ²Q¹ˆÆ;½åü³»¬:s=ÐÚ%Z›PÅŒ‘Ø;ëg6d’ºÈ‘ßÀz~âöÉ$ű†)­=äoÒú:ÿOh¥mžËaº´8ÀÇ&š:ÕÉ–ËÖ`|-~YCôZÆÈ— 7ßš–Û4é‘î,5"“E­¼jÍRSÍ£e¶!ö˜î†=•h1×/³V¤JÓ Uü!Å ´›¼ÿbV“x>¨c’LMye¦2O9ËŽ´¡™.åg„¿•¶êÀ·cCe\p-0a½ÎòÌŠL¶p'X7…¾Õ$ú€Œc`Óòj\. ¤Ej®¬yÓ‘o£ÔÎÝJ—°°ÈRvÁ—Jg(4'º;5°<ÿd$I*–!H× AøÊócΑV·öQ‡·Bzíkhô»Èç#zÞ—ðLügß¿N˜ öÓËw*a ¼e¦2;IîÂ(@?¼îÐ…;WrÑùÑ}Ùôô –'•¨u:/ »<þÈ\nàKÂAìç¯Yü[“(ß Ök< ëgš’²¶ÙÄžPâd^(œjËÖ/Ë3’øJ³é¯ù>°9ΦìE­h‰þäšuÈ‚2-x–6ç¸3[Á ‚·üJ¼’Ÿ!Ýjø!4&×V[òZ!sñ49ÜÌR"ßU ñ¼à—w’iõžêáî´f:1™MÄ´¢|d5Ò°7+ࡎ€“›ÿÂjü‰jìhÆ›EÚäU~Û'•Ò¢ÞcðA‡ø%(ýj£ñ'øèâ¨y¿Yo_`°£sáÙ % ‚¬¼ÿ¤JvÛU5‘Ñ’×0Äž7¦BkJädgøy…šæ±™¡‹Ë#,Ïäiªž¬¿Œp~¸ãa’mؽZ†F´cËãfúÒéJ=º_„†µëaØD¼z(žƒkÃ4óÇsþû,^ú³°m´%诬hÛ—«hDRôÉ:'õM)0ûƬÿº!4Êšëîlõ¡hº,y,#SþÖ?çuR‰U›¡…® ®þNk\·7Ù'VÓƒ²–Ùs’‚MÍ-< PØ™'³ÉßXF»Üœyž4÷M'U]̦Y5YŠCF½s¥=©£³›+]n\™Ýlâ®$„ÃFÓ_«&´_PÏâ/} LÅ^€a<¶Õ¤þã#ácAkf)yï@*Á®u*ÁXÔI^²ÂÂî ô>T4$b2qáàÒâCܨ-•W9~M?¤Õ¹ö)l®;šq5? Ö4&ÒOî}.˜©­HšØ5’¼|–¼p™TнŠº£­ÄÅý†CCx¹ûþ5Kšj³&329Rªs–Ë0n\1Yœ3OäY|㦃îÛëEjÔ%*4l ‡‡Ñ {ܲhb‹¦¸$Xò5M¼ó¢T•ì2oZGñd©V˜Ðkƒ¾¥òSÔ¸!MD3hî좩º¡ÚF‹p»Uq“åàql`MDs¦vx èÔ÷>ëPÍ’^G¤Ë«x-Û'†úÜñM6Î9É<ŽÆ¢šœžIú„U5 ëÕ)’zŠ Å®Â+1—©n¼¼ˆQ¡Þ]@È#JX”æÜב™R£ÐyȦR!-/G& mc¸\”ÿV;øZˆqu/k’ä#Ô–êÍ'þ¦˜_„· ¤¹hZøW£OXçLdëY¦ôÉú|h»¨C×Êé±ÜØ%þˆÉèæÄ…ùø´…—fþ rØBKÓWà¾5ÂKÙÜ‚§ÔCÒ±‘ŽIår/é¨g¹ºDxtžŠùqysÉ÷?ê„>äYd¼ÜÊ7¯1TYÂa*=åQLQݺ^'”~mQ)¶’Þ¥)(â¤èÅ…öþ xGÎ|çù*ûôž$õWù?ó…ñ .Je™2Éä!³ÊÙh`ökkFÔ~W³xl‡]•ìu’Je£l£ŒÄDÍ‘¯Ë6iÐá2‚+}p½ë”mƒ,¿ùÍM^óÜÚäI‚øQ‚lM+°¬=qF5²b­Õ^á¶©h“-ãl¿ÏKä†õv&ìI/ð_ÔŠÿ¿¯M³ó‘¾ÿRd»Yµ¶ÃŒKð<~øó{5õ\!ëÖ6gÚð†Ã¼qéõ_FÎF0ÝWq÷ùAYuXL>¢]XMÊÑ¥ ”¤˜3褔·"ù=Yy@«›š°OïVÇ«;ì¼àÊÛ²ˆqa.ÞÏÓæ‹UL•ã«ÁEþ>·¿­MÆ—ÆT¿ ˜ûº¦Â¬ØVã7ºµ¦÷Ta+™f¨¯)÷ɯ£ò£RO[°ÂõÁ4Ʊ+)&¾èGk8Ïð<Ëû ÔÁá"oƒE4cNö[„™&m0%?½Rލ'ž‰]áJ¡ádqÞ;i‘­~¢_ ‚ˆ¥‘¯gê"°™xl_3ð>¤àïqD¥´³Ÿ‚Ÿ3ƒž–¤ýIOwOe}Ö´2n·(vM䂲‡Žs¿à†MF‘¦šF‡‘&Û):IòE“³mX÷tÕŸ¾7¯äÃ’…F£¡à×^¥Eg’ Õ&–µÎtÐw¹¼ô—ã?Üŏ”Zï4’B׬4a~$6^¥ØƒMjš÷š‰gôÄ'Îoµ«ï?È1[¢ù3°Ì‰¶C2sÑN2%,&$j#C˲†¨"½Cê&2Ç2š íU8¶V/é¹$Ó°"AÀBcÛoBTUgêÜQ7zgžíÔ¡]q3]ôߤgÓÐj“§çàŸé:_Ž¿&[8¹»š aÔ>IŠé£t/¡\Bå!íax„9·M܇ô»Âì‡D¦íts‘쨾 å²FG| ’v)åšE‚Ðb][G_Œ¿Åüºfcs†Qbž;´>l¯A>”Ì[»7pMƒŸh%ßÅE^œt¦h‰o¹JQ‘[ØÃèT¯²èЯ÷rRáüøô¯ÉçYót¾ìµ,ìjèó¢åÜÅ‘ W‘ZïRù0oLà|ŒIlb‰iFsZSZ"è“‚Ü”×&žŒj>Ý §Y!ƒ$õg=Âö戒1-6h³  _Ø1§5“&‘=¸#+‡bKì+3Ò« +È·»1¨îÃsi0™³ ^èàÚi®³pÛ4S£M^¦›[ ²#&E†ÜWR·ìö àcÏxCf_Áç¸xûú¬ñ‘!u»h¶H^è£!¿ Æ-+cR˜œ!zmù$ù÷§Iò;ðñûˆ€RÌšîOªÕbÓ­¶;oxÞMò†׺F˜>ó4EFmÍH{›2»2Ö‚ÁޝsÍ­®½ŒËo‚SF÷*ÞÊ%M‡¥ó2_*ZÈòÍ/4}™–ÚýÆ}e0üÖÃÓÄ!k‹&87Ò`µòûâ9„µfKæϱnšŽ5*Üip—vjH7¾=¸Lì„]a¨‡ˆ42ÍÉ»§I°QL2ô¢#Ä£_V•ŠÊrãìO#z×8ÁÎÊÍ^P–úýFvbÂMáô¢T7߃ùÔç”­­’—÷xȲ¬å¬ Ðzå1w ‘My—\KH‹J?îÅ\kZÆduLî虣? D¾ñ™›kèk;.„pÚéQQÝ@øˆ$ܱFú"Š·c’&¢®H*éT <,•æ$Üè§ø·ËEé…w—¢3a·&"ta_ÉÂjÞ8èù-9q ¾6>š'HN³¢zBz%ý—Ï—æ{‹^½šàÛüh3mDi»†thC¬âGÆžq /#Êlƒ~h÷‚½ÊÕî!wt_ÈýaÏÐ62V)Ø\v\™¬’F0ÿ[&&10œaA5ÈÎÈÈw„®¾¬Ðωõ ðGÜh^šípräQowÊpÑT»$5P^¼ YpÜo< :溑F´\>¥ÂãÇÍ:&ÔÑÕÖÆ1ÿ èfæNç¯&Ÿ.ÃæcÅ%PöîæD´WÃ/³ òYÒ~¡¾Çà›î-Fñ^ç`¶)ŸbùÁ´¦$ͰeQ'ØovU<PYþÉw-´Â#³8„h>ÿã1Îá°ýÙYÆy5Sl‘ ©ôPÙ¿ b¡ù~z}»ñ~¯Èæ ‡ýÂ2É3¹Ø=õég…rÇTlª ´ôƒÌ)oÛ#Rå·ø! N¬ˆü¿¹ºcµŸ ÂÓ=†(K;‰¤Ì}®ÂDò$ÎN“’(°~†èO€ÜÖã:±ž’Øe e»á›Æç°ŠÊ¥w×ëòí¾986ßÎñïþ1ù?žŠúüƒ±ß±7é„!NG.·¹¡™‰h–„'IñÓõÖ±ùß;Â~Šž¹ïÏ™_k@¿’£_1Ÿ¯ÈŸ>o}LN£wæ:.’z'Ë>¾¼{ËÒÉBý‹Ö·ù6>G¥ÑŠ|©=3õ ô,~^“°µö¾M?^·ëQ쿈Lõ/l´ùý”¿½EÓe£Ð†ÿeÌ´º®›û ¾4ÇìeŸ#Òêºoì´|‘~Ó¯RצþÂ4þÏ7ìcäºÝwèõö¿°UéÇÊ£ø´ëîQû ö~þ[—Œzôeböý ¿–-«÷í¿ü*ß­ç㋤ý‡2Ÿñ|þÐÏ„·oc\üÿ›'ìf8ømA~ïîÃXIå÷Ù÷§íLõR—ÜÐaî,×ûE)è³A×Èž†¿¸ÍköeÏÇ¿=OÝÙ¬Ûö¯ \}ÆjŸ¶Ùu=Œ\kGÜf£G®~É¿˜¦}Ö¯hŸ´Sâ/°žãìݘ§¹bý¬¢{sÑ}øaz)}¦ÿo—?/ìÃø ñöÜíq>>úÐ~î‘þÌè?‹žªE÷“3W¼~ôý¼ÉÍOÞ¿Ù–þM}½Cò½Ä«5÷-üŸwPÿrµÏÉ÷‡ûI>.{ŠþÐSø'Wɵ…÷'Õ´T˜ÝêËeð†¯¸Í?Ü·£Ü3DØ´ÓžæŸôf‡¸Í#eûq~ q÷¤-ýÊL¯qš_­Gò•Ò|³G»¨Óìó~Ëù~^ã5?» 4GúÉéúas÷ºo÷)}Ùt_íÇÀÏWã{«¢ÿk¤ø[ìg¯¸º/öŽ{ÙÁið9ø‡«ý›â¿=í}W¦±­înj~Ï'Ä>³Ú¸KÙ+ÝßÐÿV¿šNDr9ñï×®~O»¿öÀþ/TO½îë}õÛùdê×·„Ô?3ßÏö­üDõ=_³ý^ÿMl&^î—íjâ| ÷vf§¸Í.‡û`þ'gïzìß ²È|Ép÷\ÿf/Ê'µ¡ïf?ܤ÷ZFȳÉV‘~%ü—¸Í#`ÿhtùB4}ÝBÓà«_éº!=Ë5ô÷+[ÞçâÿoçÅ@?Û›ïQtÛûˆzö#Gè,‹S÷ ?Ü[éüSÜ]?Ú9ðáµüþâýÌÚçæ{ˆÔÿru„ûÞâ5Gûq=Ìúž‡äûÑÝ_·› =Ñú÷û:žîþ‡ì¯ÓôùÿÚ TM?µñÔÓo=}ø´t™$´Ùb¸ã¸ÇÓÍ8ê>ÚSó08<ãÌ4÷ón¢0Ë ¾;¦¶ÛË ï²Ë-¾ûï¾( ¶* ’˜Ï_}´xÀ6}r ÷ßx  1Ë Ìy+¦úïžÉk¼²Èó½A6Ù©)®|¸¸£ŠjâŽãŽû,²-¾ûら¥®:£²úãŠ#ž¸A aÞa>Õ5ýuvÒAqˆ° 1Ï0ðÓ}÷ß}×ýùÇsÓI_mt%¾J.M=À*Ž8©¬vÓÆ (è²ûï¾è#²ûó‚ ~à ðû?ðÍÐúŠ¢ë¿îÃ<0ó@"KÀÌ0Œ=ÖÛm4ßAߺáÓ}µÓMôÓ}÷ßµ»o<ÿþû¬‚ ŽË/ÈðO?ñ÷B¦„ ¶P}8l²øàƒŒ;ÿ )›>4¢Æ_{Î5ýØqì÷þ¸©ÊI,º<0ÓßÝi2Ïç;ÿïüáÇûßüÒw?ûÓ?î;zÃNsÎi/¾8çºX!Yá‚DEÅØ I&]Ô¾ÑÈŸ¾Ú,¢ØÇ= a\mµ—ÖQ7̶˜òžÊêª8ã®(ޏ¶ó <ó«"ç8ç²ÛüÿÏ=ÿÿ?õö³×;ï¾úí†Ùì¾9å‹ ì°Ã ²FCÆ“è0Â#Æ€6ÏiÞt¥Q&ÒP,)gž¡ b8ñËT G_Ú†8AIvêû®3ë¦b® f‚oºûk¢ˆ«†¸Â "ÃM)%¼0ÃI&Ò³]ž0L1‡lB”d rïºÁ µMäØaÇ¿qÇ@”q%2ÿäá•eTu§« ²ƒqÌ0]@pÌ<2bÊ8Ž=|45ÿ˜âÈC °h@²ÔAM'ßo5Açwÿõ{c<0÷« ólÑžrÚAm·\xòA 0H­¾û⪠J<¡N0Á6¶û(¤GÛA ,1 ÖRÑA×mÄiöÖu se”AtÒó-4Ëý}ÿÎ0ó7 E´ÔQ”’E$ Ó€LF_,ƒ–C$ŠäC ¢4“Q €<“Œ®Ce†  Æ_a7ÖYÇÓeç×ï½|E/z}wã±Ï\õÛï›uÕ™M[EõXAÃÌ<`H 0°(@j ;z¼ßûÛž*/@Ul°€†0Û<ㇲqQI4IDÓAduçÞ}¿ßÿóû (“<Ó&;øÇq4ž]vq„Ýq1PP,cÈâM1 ÃEò‚Œwf1@ó»ÓüÐEeyß}öW0Èf–(µM^} !ÄÚU¤õOM<ÓÌ4ײÜÿì9Çqöæ”4ÒÌuç4€qËeÐ4û%&€ãœ1ˆËq/wPRê±EMœ¹=WïAÏù§YAß<ÊZq¤mõßyìcbN ð/¤Uóí:Uõ“TÓÙudÞ]ÓŒ,0 8Â,`K,sÊ¿h0Û¢£_ʽ†ºS¥ D!qJ/‡1ç»5&|¸ëÇ$ÛUÿïŽÎE•Xývœtãþ릳ìPG5}´80GA Èdr„±/>‡°×Þø®§dKÐáa™„ H(ë/¸žñIë§¼DÖ¶®YÖÐÛ÷ðx´Ó‹82é<Ê,BK0ÀÏ CÎ"{QÐqÚ°À¶êâ7î6ëìðÃûºëÌò_¾W“¶­/ÊG14ó—*µ¬ ¥Idzll\aš»Jÿáõ²¦ë* òÆïe‰@x4{pãÍ8±Íà sŒ4ñØŠÏÞß­ï–D¶›ÃD1d}<8ÏNLp<ÖÒe…ÃùéÏëI‚Ü® Éj5¶éáràÐFñÓ;çý0óÙ¦þ¾Ðô2Œw%«jŠÌ,@ÐÀ³Î< Tÿìホ[$°£Ê±?óÏýXµ-‡ã[² ö5Ñë=Y3©0lLô]™Ó° ®¡šÐœÞ›é·•QfA—M_ûWÿ‚Ó­® $¤#4Ãaô×OûÞ{ £’‘Ï oëÆW•Á9(*AAåy¨¸'Gærþñòrú…\4b=2ëkTßzU‰˜Ñ•–M^'•“iwÒcTÑË]Î8øâ‚;¤ŠÃIq÷X˜èóæK|OãïïÛVW ™hÙYu&Ü}†æÇ|k{ë`F4´Cðùiלô™uëgªzzË4VqDSËe[]öÜ01/® <ÃÛ}FRP#"0ޥɭMc•ÿx0O5œÅ©ÔËr'l²NŸzתèÿ”í™¶ @OxË`ÃO,ˆ(`ùBÆC}´ÖqÎsÝî˜uÞIÅ@›¢¦[¬DCq<¼Pÿ®Š·ÆÄ4Ž¥|‘Ѭ5Ôä£ýh˜õxîÌ2îuËqXc¬J“»d§ ¨r§hHKÑ,7Ðôã ó.ÚÑ ³QÑ—’m÷–|°²° ‚EG!ØD¨~œbóÌ0õö“q¼¤´Äw}뱑5ü|ñÇ•Ë o‚ØêH iP[ÞÄN&0¦Õ>6û 2À i¤‚E8QVÜMG crÿqF»ܵ©:¿ZÓÈ#Ûª@ ­uP^gGWâVÔ¹6„Vìû5ÓM·ÚD'0Æa4SqÅ]×Û0¹­ŠÒ„L„,ÉQ¼vyÛö¶² H M'_ø•ý̦º[,£ÿØc°biQG Û ò‘4içΤq0×7ëŽa@C wœUæ]5AÜeG߆ú+ cŒUmörª Ý­0ÀN¢{ŸãÝé7wþ³<]õ¢Õñ°_ç˜$OhWžUd"ýÈo 8ñÄA uçÛOuþðÈRê}Pç’uo÷ž*ªZ]BλѼ`SnÚÃ.0í$H ɧÕ]e¯ÚH#6¯äõô¾VMÿ¼×À MuÓ}÷Ûaד}´Z}uÛÍL¬1Jûk €4Ó[OóMB;Ñ£ÍT@„‚:«‚8!ŸÏÁÑ•ªXòÿf ìï²h¯âõ‡þ÷^´ã^Ô-C øÙ×}’Qçe4VeÇã®h“Ìw Bd¼.GÁì°`%j9­_xvjLÔ>ß®®ÿᴊƯTK“ÏϽŸ?wYÖE þóOñéÆÓAvY”sݳïœ{ÇÇÎ5×±Óöѹ€ 7r;~½x9”ÐýLõç-óawÑ[î‹Ånž9ØŽø6Ò ”©ÇìD]ä#Ï 2$WY.qË ±Ïq„Vu$[U3†)Å…,}óO1_^É)Iî úQÈN4³fM#ÏoÿаðÐdïK¹žÌ9ˆóªØ¯AKn¨“ëCyH4çÏ"8¡‘̲ÍI%\Ìva¥VeäÝûÞ÷¿î¤@„îïó„£}ÇÃM °s ýßÅž…0ˆL/{Ôp@¦Ÿ1¬5éQ´Ÿ '/:†—R qËÓ¹å3»ø‰i ôañ (TúïHc¶Š²~Ýñþ€8‡‚kžò°ÜC|y<õ×íþŽb÷±õ”ö¢žíí0‘ß0e,³Š1Ã@ ýÅ÷ÿ¹‡U}q…6»l1´ãFP@Ûÿ,»ÏÍúù~¼ÓˆOz€ˆ5c€D2ÇûÓ~¿ã1ÿ7 ‘-YE‚ÝÛ˜±ƒ‹Zëù¢§{¦-îh‚6øn¯€ó­ jà„cÏ{±ÃÇNÉíMߊo#è[È`–OœéŒsß°Lñ~ãu×YŒUÝ’\¢ùkádwÁA{°·,‡®æRÁ±w‡ sjªjˆ–Ët–’ï„ Ë|5—¢y„çOƒ°‡¶eúŒ¼¿×ø÷ ‹àó9¿¾÷9Ï||:€Ò6ƒ=ñ@Ï£¯m"KÏÎæ,ûÓ篿¿÷wÜû¡Ž~^ò¾a½óÍÿ;%ÿ$Üó¬ÖE®¢…ËX“$QŸ÷ZJO³¨³Ëøü® Ãl4×f7¥òÍ[¸[ÓÃ0q÷ÿ¦C A´NT0À#ƒ M¡ ®túhÞ?üóÆe˜X}9É{½G:—›c©a¦g³;hPç<9\°C¶÷â øgœL¿6þÞ?³y–uó9ÙZÏG! ÕOê‚Gÿóÿ³AëOûðãm×÷~l ª:¥~^Èáì°rM‚ÑEgÞ‚EZAÇy×ûÈ%LÇ[p#ëBsfé\×qÏÜ[çÅë:8Lòî,ßóñ@b&¤á4æ œ8ÃwQBÙz4§-ñÙñ³œTqDЖñ¨–x#9ü1Q…#5=,^'§œc0qäç€-{…ÿ³7†21œ;RQOÎ2Óë×Û¹!0Ø0ïhб´W<4qõ€Â)Þ€rx©ßãM%1Î8Ò_:æ‹}¢êÜùmvÕH.EûŠÇÇs}ßÛÄ/|v7‰WÞ¨ûg0í}í5ö!Ì–€l‡.0PÇϦEÑ‘kC=r¡ ¼×¨±Ô×ÏZo M?‚˜o0…ÞÉwßi¼s×W ^0.S>ñÁÛnrýï Öd{ß³ÿ¼ãçO2I,±ËøG„³œ¿æ‘ ü¼vèOˆõË-' œô¾Ë^yÎðÁvÿÈxÒ¿ç7<ôýèë÷%ðü€:,²_ïon¨g]’s<ÏcËÊòÁösóʱ4å7îæ4Jä0äh”õŒ±©@g?÷4c¹ËÌ[_Ïþ^ûïÏ÷úˆ1ƒGý§0Ÿ #Ñã‰8Ë} “þ¹ÜV³IOýe¤3®ùͯ¬?ÿ|?KŠQZö×>`ñ4è(e”I„øg=?Ö3™MDÐUo¬ŒKjÑ] °ŽŠygòÇç^tèrùÕ=˜Ó-7ŽÄ=‡|õÓþÆÂâ‚O6*sݦ0u.”ì…êÇJ c$9áŽ- ŽÉïýä-ŽÉ,òÎÓ@òÉuÖÆ =DûßDþ€ëzZÎ[ñ×tíÄ‘Uö5é3Ç6ûçÃï%AæÙL?æl4‹¤=gg}þ!Œ_½sÌç¨Nvå7œ0×Ól3Œ0üãºÈc“8BƒGæs#.²îcíß•žlu~AVA%^y˳q°j¶ûi~ÛD΀í&Üh{î…(jíiAÆS[ÿÿÎ ‚Ë=Ï>Y˜E>$:ÓÌ÷=,ç‰Hå;úqvåÓQõ÷óÿÿeÒËš¨áÿ17y ’RBr (Ï}„oø A”`Û±ÿpñ„SÔ0{ÏßðË…{ÇîyIŒðÈ–€ \WýLâPá€}§÷Î4ÿkÿ|"㳚 <ñr0ŒèÞ&òI׃ÍX(h½±"€ Iø@“ð×Ê0óß+ŽÚÃÃjf’WÖ±³;ƒ #4Œ¢GsÒ˱9a‚ýró_÷ÿ½û|ôD¾¾ûÙÖUvÂTê“Bò¢„±Çušb¤°…†7U}þß íÓÏ1Œ1÷y÷tÕÀC.8ÔÅ2Í+<¾ã;˜Œ]§ÔY´§¶i)¦/ÿÿýÕÿ÷cÏ<ËWØP~·Âläƒ7)D_¾2c¬™óíŽdâ_Ã|3¬òÃ84€„T}øìÿŒàG-ÜÊBÛB”0ÑE§ýˆb"€jÌWÿ?ï=›I‡óËIS@wöëscxtÛx'š*f–òïa>:T´½åb8s pÓïmVÓ Ñ‘ÛeQ²û­öòVô¬L¼Ð1·Ñ}e ³ Å-Dq·ßyôÜCNzƒ4=ÿþÌÓdøþ’L0øežÚ)ñ|ó <¸–€ÖQÿûŒ1éÔdóä¹éËf÷Ç=ã|Fë?‰I;7 %²Ëc¸3˘QôÓM ´Ð}Ç~°Ápå÷Û {Úéà‚¨éý¦0Ž+QóÃ=¥Z0í.¦L=ßWµ.3Æ<‚U;†ÀÈ(:ûê"‚Ëæ4T¦5çÚu4ûÃdqÿà'0Ç}óÌuqâ–Ú,á<ì8ìýþóÃr zË,6Ò×8‘¦2O7AÓ×<óÄíQl@‚nñëí’¯ºuµûËQ¶;?õ!aGßû ‘}ö˜cGhFÿÃÊ U5Úé.#„69Ù“ƒ0¼÷éG0<ßÌþ ù)ºä ‰z°1¤79½ZŒ,€ŠïSz]ûn‘Ç„¬ãï×ïóiußý9…מEǘŒ;·÷ø&_¶Æz`cþÄ0ïôÜcÏîLÒAc Ê2óß9Û§lÊ)^sOe Ó^ÓÕóÀBÇ:¥.<áÊ;äš:­ãa´)?ÿ<Öqûç¾ÏD4˜}wÿ©™â멎Š÷? ðž¾U# BÏ à ºßSMDEd8ÃIF“¢ø#€¡vê…7¥8{žú¦®~ï4Hc¶°aIn/]<º4 ÷ƒßiÕîÕòÃ{yæ’Î ÐJØ3uÁŒÛ3Ï<‹lŽïLûMCÜù&IÔjWr `èbQMóä²ú@›bÓ9«‹ “ »ßÿý¿þ÷ Oì8ÃMÙÛím,Á>ó¾\8JsiâÈÁ"@LsŒ°Çsúì¾»($ ùõ(ưŠÿ¢Õóˆ›ßtøP@Í.˯u£ñ4Û¸fÏóþÏ|uôüq†â5ìP{ ì°Q`¯¬Ñ8 üE¡êƒïL1вK¢¶ÿåž°Ásíëeø^¢÷ȲìZ ×°9…23ã ª%µÏøàôñˆÓTéìqzº®œÎ3ÿþŠ Óy»o“]$„&²ÒTqŒ&²Œ°²›¦%E8"öw$ß¼~Û®!4¼Ó>Ä=~«ÍÄÝL “??6» / ¯ª» 6Õc̱ýÿ{¼7Ç¿˜¹™ií® 3Î-Ç“ÎëLy÷9‚Î0ËÎ`œ°³ÀƒjÂÛ̓꫄Óë:=sý6È,?qAÚÐÉïéˆ3I$tÚ4Æro ‰±ãö>ØÃy$ €{•Ÿrë YÇùŠß¼Ï>óÓÏf-Pzå °õI€–òvólÍ}ä7óP˜C_ÃPoUëˆÂƒ¢-õ¡5qÇÇë,€’¨ƒnßfÛù`”(ž%ªËl¢Çæk,ÿ¿½ÏH ‚O¶ô‚ßÃrÜqc Ìh°’Ï•\”ÄÇûþ” Û|ìK®°(âóWuõ[Î2F³ï}ÖðC'?ªp8€L÷šú#’Ê)¬“<4ãîp„7à}ÇܽsqÃŒ—g¨PCï-46_ vT¯±àÉ7¿N¶9RbEóÎ=í’°óÏ“ˆ¨[Í=¢(µ7ú(rŽÞqX¦²œùÇ>>®2ëó@çì×f+Í<@2ÓÎ ó÷klÐ÷´ÏîHE;¼Û.°ƒ@¿÷ße¢IŽ*¡û?™J[E~“.lq«Þ¬‚Óo|ä’øïï-ÿšËBËmN œ }7øъ>Ωš`m/ýÿðƒMÔpUb0AƒÚ šI¢¾û{|Ã8Fj翲dwÁ?î1œ£:ú³ç¾p’ûê°„<¢?òü<úØ&wÌJMñ–*í]Ú€8Î>ß…F¡'ÿþ°øB Ol|Öè†tg¾8ãŽku}÷Ù^Â"z@+Þ·Îàiߪè×L!žàóÿß¹n·¼½ÊXî~ÿDÄIv!¼‘ˆÃÚûþn6oïüË k©FÐQ Lø <¾®ºÃ”ÇúÖ7."[ÿAë"=58ÉÀÃ覊¸¢Ë¯ºöóʬ¶)oè°Ô˜Ú0cØCiœpKD-'ßÎ3se&pÿœ²†«ì5\âÕÑü$×û‹bAo˜r´ëpÑöãðÀñÃ'Yª£[7’ùiŽòK %ª k’ý<’'NMyj¸ ×þ¶UèÒßQÛƒ½”wnó÷¼óŽ\Qd-£àÍs®ßÝ4¡ÖsƒÛB€f]WšúÚY5ËÛ$²ùR󾸧ºËëˆ il¼ªzèŠp^oJRn©C]ö´»ÍØq7Ús-÷ψ%ýÆQÎ3O8’©F=ò”ôá‹ÂBŸRÒ o.]~h²»©wóÜ(¾ðßmöpÒȧ¿ o‚ZÇ)ôÑE_¢Ë¯-ñuó¬†GÓÏ0Tw¯0Y7ÿÇü°ÚXÂAt¢‚Ë„mG~EÁ‰¹<­¾Oà'Ð'H8iÝè#ôi-ªa¢IÚ%¦ØQ±:«vJ!iþ…y4ÐÆåPÛMÃÞ Hþ0êN"fHwÕ)JR— ƒß¡ˆŠÑ¤Æ¤rnC VàrW% ÊH2& )0I’ŒãQÿE¹YdÔ>£R5Àêø(s€Ê!=(›Õ>!bSq´•Žš4dœÅJ”C´hÕè4¸áõBo´>Gš)Ä›ðI ËúpHŠ©J #¾¡ç© ô(=HmP¤¡R›L{ $+| íbIšš¢šlÜIò9¼ Jx 1D_‘=ª ²liÈIa®$”CÕ7˜¸£)ªí4…¦˜HãÔr60c»í)p{çyCЙPÚMb4‚¥ðSX|„‚4LtZ@ш Sí>ÛFÒ‘¡^ Aƒ' ‘Å(A ·^±=`Í¥¥i=5³ÈÑ´ÚÍm˹%:VvŽÓç€JP|éíÞt°%DU/ ¾zT¢Ú¨Öê:XS‚¿"å)„zl;±ÈÚ;É2ÖêÒ‡Žƒ}»%õŽ+àhœH„׉ª!"CI‘Â#‚"+O‘ãØ¼n/$Å£•릑¸¡Áü‰Ós¤,ˆûd§Ø~F&ÂE²"øÂZˆ££Š|šû3d¸lx Y†Í[á< ?>šwW…Ê• $#jðj耇†yÃ4W$XÛ‘ò ªˆQ¨ïøx ‘Æ'¤Ò Y ŠAYZ1þÆð¥SW° õc5¬f•X¡Ýó°¬ù!CJ …¢Ø›mƒU!äAÿÑu)á§Q¤&k„œàÑ+>¢s—ÔÒõ } Õ‹<&ƒHˆŽ’„[Â]Ç) Kµ„DDüæ%¶Ÿa ÿ‚DÓ£_4K ZÉôÍö”RUBcÔ„ºÓì5ƒÚ.A# ¥¹W%LQP±nQ4×} ØONMøÑíQÓ_èz!ࣹ¨ØbŒj7ÜGYˆÑC]ŒÕ++‰Â» ­¹VÃE«ÙBv4½D&FŒ6˜L#@ˆ[!²ÄÍ sª¤¸ >àuÈEFÃ6ƒaàÒpksR›’ðNµ,>Bu_U:S¶yÞÂF^Hµàº ©RHŒ|ˆ²L“‚NÚM-› i¯!’ »E}XÕÜ¢K°èؘë'‘­ÜI©Ç¶½ Ò¸<ìo1²’ˆÛ}z¨¨¨M ¬“mô¥xG ”MO0vP%F'/4b¡üPßGfitf‘"Ô¾0¦¨5Tj(œ6Ü!‚H„"ˆ)š›2䦦’¬\ƒI¸DTT4Jû‹‹&ao¡ãÍFo‡ÈfÕà§ÐÓÁ$M‘«Sè.AA«ò"ï­wq'®¢C©MHÆúcˆx›tJ¡©ºP«°žŸ“X¥tGáªc^¼’;Ì6nò5êÙQ‹ÀßñF¹„Õ+Ǭ}®£p…¯±­ÞFžµÀçz3iÞDz– ‘4¦×F£S#L“Ðlƒà2Ï’¼Ç' f«éàž`Òˆàmybµð(UᎡÌÄ™‘™¬ÐÉ !4à»ÞKË ¯áDB¨†‰¥ÏVd„!=;¶F¨’J,aB""!±Óà| k—aà…ƒs°ø›åãS ,N6?Ðh É Õƒ·h¦ê!:5¸[ŸGgÓ£±ì7/D¾ëdÛa³‚‘õg§{¼îÒ‰4E*}¥ükÏGbl(lõ|cH›’¨CÁZ᫚üpN¢ÜjF[ãh¤Aac^ðG^Á™ÏÆÄ©MHŠF‡€òþKfÅ)cHoä)Jº ÄØÑ)Sp±d¥»`škÓî'Aá “D‚ä4‘¸.APq!­FQ€ªÑ>E;B¡6M“ SD@§Ä[MÝðhص;„¹ JM±"¡CpiZ7ÿbXXÛ5.½5BkƒÝŒUñäŽ²Š¢ÔÆA²0Ñ|‘Æn"n›!i’ÞB8GX”0Ÿf„;]¦¸,ù¤ÄÔk#¨(¤ÄÕ¡õf„ê›!]L”û€ötwFårW%eeYE`X¡2½ñ¹+T®JäºWq°Ä‚7‚nz0˜"׬b}2½±éÖWÁ\°º Xê‰ãz4¥Æ”¥.KÖ‰»®]ç‘£BE¢ÆaÒ""CH‹‚ABK%ê…I ®™E6/=vèÏ!€Ù/Ô¶i¦‚Qv´½Ó½£êÂdRa<T‚`kƒÔƒ‚¤ÆbNÍ…²6‹–¬/Vte{ßšÔXBx 8þ–á Ls…$‡&Á-mç LËz ‡äKɳ+BÜšuÔ‚9þ›–J"••—Ê^’y©z”¥ï!:‰ƒµ œ® I‰¡§ÀÜTþJ-÷|#‚›87© hdO%йDDLNãr_Ǥp|ˆ"i‡ÀøI…IÊ|,ø4d’%0B„à‰LiK…/oKêæ;:¥J\ô¥Æõ/Nä½´Êû}¹Þ3%)z4½ÅêßS:‡ìi}=ïgWg^ö7°½•êR—½Ùšg¹ï ½KÔ]œë=³Qô)Kнí/¶{>Âôiz™irR—·BèRå½yÒyØßJ—Ù/Pó½º7-ìéK.4¥)KÛß[{9øç»ìizw«{…Ô¥ë^Áõ/a;JR”¹©}“ß;ôô½õì¯JôÏ~òå¹.j\Ô½{êS….iÝ^Æõ/¦ßš›²_OsÜËÚ·w÷µL¥/£¥íSêÞ•ïPºwÕwáKøѾ‡¹~zõÛ‰Õ¾¥uîZ^õ Û/¬¸ßI³>ÅÞ_[p¥Æöw&ÂáJR›¡½[ÚÞ/o³¡OÂ^Í{³ï«èV[ÚRölû}zν‹Í±þ”½;Úܯ|Û«ü}w¾3·¾Ž÷ÔO¬÷Ép{?U=…Ëz ®'‡ùÅÓÝ–`úÓ×ÞÂõî ¡¸¹.wío§]sð·½™ó&çïïe{Æöyù«„!Ï»¯r?Æ^µ5É;ù¹÷fˆ]§ÿÄ) !1Q0@AaPq`‘¡±ÁÑpÿÚ?éÌ! ‚I$’ # ¢Ë,²Ê(Œi‘þŸÀBˆˆ˜ ’I ‚2 ,²Ê(ŒŒåüv0„D!bOJEBŒŒóvW ^­/ÂÄDB ‚rüfý”QDdd#'Owi{;ò„D"""" ‚ Çž¿¯eK…)q¿KøÙJR”¥)J\·‹–”¹oÂЭÃO‚x6uÒ¥í6¿xíîJ\ô¸l~Z·]¤üenº³,èÏÄ–ùМ¬=G¨õÃ+‚> è„!B„'JcB~ž‹:Ý\䀉µI¦ÂZÙ Mj´4äzPûQZÒ NÚÔº‘ê=F‹ØÕ SÐÄÔ= l·ËÖYE`¢>ÈBB„Í1„d'Á-–v3‹DƒM² ¯ nÀàLõ‰.¡%¸…$Fº¡hR†óЉ!±¦‘-ê%=H¦ýXB„#FÎ=”QE‚Œ„!L„ÂFN¶ÅžKqR9Òê]$ËÈ­©‰£ƒ”€m"º×D@Ù?Ð⨠² lj!­‚y‚Ž5äÔAB=Êþ¡ï-õ$w^ª òE4>d=½•D µ‚ei ´-‚#Z3BÂ>ΘÂbA$’N_²Ë,¢ˆø#à„! Ò’¡ ”J7. σV‘Hë=C‚dF(?D&DZ˕š¤™á§´÷ ŒAÀ–’ÐôÖ ÖÂü‰nj4{v=,ZÎâj/¥ÜkÐ4›rÃkÕQš†KYzIÄ-«5FÑ [àÖ°KÂ2I«X«q«hz…ÉÁKò IÑ`µ«0¥/yBaµà›+ƒ"ø4¦ ¹¡Q0ÑÁèÀÜ%¨µFÃÐþ€Wnžx…åäE¸9› ·‘âQ}ظX|‰"ÜnC`ÓdÆLÓÀ™~ia݆4B`è–W]X‘+-DÕ#ÔÄ…¨÷sÁ65xWOp¾àÙиKÞÔ Ä¤ò;ML×à¶ge]ì tI¨‰¢ !ª©î#‘¤HePa¡Z¨ÔèLò6*ðB8ƒvøaw¶)¨v†6J˜ÐŠŠ/ÈØ÷‘Œn³DíDщ•¸T“…Ø>Ü!¢§c:­E-éµ£ØP‹¾½ÎÌÈJÚ¤bcDYûŸ°”1J°M–ç´ö‹q‘6…ô‡ CRhZ‘\ºt&–CE¬i”VM‹˜mÇïaíü-×”ENä¶·52] »˜„b Jhx’"ØTÔA~AÛ!¢µb4ú7⦢uQ ˜±$¾pÓZÇàäj)¾¢ƒØ†„"Mˆ’…¢-C^ŠÜ]GÀ•gÚ µžU3Ssì%Ò÷ä™:!àÝ&4‹V-H¾ Pø*ýb™45«bl÷˜­é¡.©«îïy±çTÐJŠ …ÍÊŸb¹Œt q Ù~0âQ&‡¨¿-DÙë 4ver&ü²§#zf5 ÍvÀ{ÔØD6>±¦Ÿð%’ q­ Æ0„!; š³¥é̾YÙ šÑrcIm¶è™A¥¨ÑQÁÅ!™°–¸…Ä&h5šŸ±´ WQ鼄 SoTDÑ»[¢wE¸ôô ²µu4šjÍ mP˜,]-/"ÙÛÒèÄÆæ’ä J¢W¾ÂæE—Ùn‰ ¶tjþ*1¢=C™gBwpHK1v›’”¥.jR”¢l¯–{†Ä¥cb”L¶–Å…A¹ÚYE_ѱHŠàbeìnÔ‚Zð…èž¡o^ºvó„Âa»>’KpU“ÙÐzÄIÚlôJÐö*…@|M ¢22LRm³G…)~6ôgE`»µ Ǧ¦Õ±ºÁ =œiF"Í&€ùÀhE )!5ÄÆd^¤j£E§û¯®Z_Š}¢yÒ•5¶DIx ‘ !*d/VC®›88"ßKVâ6‰ B…¢ýÄÉêÊRö5«at•©à¢ß¨Ôm|òOk>ÐÕäýÏÜKxSfÄjÊQ%n{† nNª& Г´À³i í Éøê’A­:ÚЕ]7Þœ¶áâƒi×çä/Y.¦Æ}(Ę4L²BQ%îjµð$+cä4ZŒ­§Qà¢ÔÚ57G›ÐªHu4m1¹#ðI0’ _&û9Ò[¬ûrû BždC äЄflhÒ=ÂXù,M ¥KáE5J5JÓHîç0v‰¡ºû Ój¡·fS¤ •x“òNíô–uÔ[¬éÆ‹öŠU ­Ç’)Æ !Ä%B&ÐÌ ×›T4¦Sb³Z7¢‘Bð‘&LH”\Ò!F½!Fß²!çŸôô‹è'«ÔM%ú5¡¥ 89zÿEËq^x|›ÿHE{ÿ°Ò?•ÉõÒHáüZÁuCAj/@¶*,”h›nIࢦ>‚VÂ1à ¦&„6¡< [}‹F¢è5ZªBâœyHŽpDMÇ¢c‰"l*7Æ V%¬öi«Ü#&ÐÆ¤öTM/ Z-pòB„!B”Ü##yšk|Ž2 4ðša2¥‚ív³RC§¬V/E=cJ¥A15ÑÝÏ`Ò¥/°ï‘!ì4¬M°k‚dP™à‡ˆ¥Q’[î–Ý^¸h±±K}Ѷ…ÍÆH¢SrÐ×WQ©%v9û¢RLHÙ4\‰lþ”{·ÍÐÒ·Ê~ˆã¯|(“Ô[{ÃwŠi¿Š7¨¶Liö6+»±ëé57öhªLÒ¿F‘{#p:n Å`dí1\Zðy<&…1ð@V‰¹b{~¸„ѬAPô£Ðz1© ‚“5éREàž Õœ µGƒÄ+.,#„TÑ5rùÔ"" ’ÿQÿ¬^ÛXLg}FË…)æ4!³u²”¥…|•ËbnXØ›G±•È›äÔÜöÃØ{f »(!Ì%¢E®Gè$äD´U÷"8‰÷O£×ž˜Ìfiše˜Ba0™çB÷»3Ð&5îo[‹A ÌËîQÉH®ª=ª7¤ØÄÄö–±«ÔV2 „È—Na2Bu&„êÜ«§{Bá¶ •ý –¢”+ !èe<ᴌߨÚpº® µ[rnCÖµv¿Â4š\œF÷ušbÝÚx±œÆ-Ûø?mÊnçèòÖ³„ÏBt®T,E>Ì©µ]ÈäÐÈôî%ƒìAò5³Ù÷´nhHÖ¡¼¶‹UPê˜kLØÆÒp™¡0I‹Ï5ša3¾‚ê§Øh{aì=ƒÓ ÑžÃØy¢c‘B à„´¤D6JÉŒwèMI£U‰„Ï;f²ÌìcußY‘,“ˆBa:„ÆvS$&k¤ÞI‘tïW~u– %•!!"LfYß fcé.—.Öu‘"a3Á!.¤É:I–`Ñ ‹è!vs.Öt%„èÁ.œÉ;©ÕcÂd˜LWeKÓ‘1˜L&áa Õ˜<_Án]TÂa>aL! Ó}UØÜûº’c;m:ëÉ2Né=W@–0™¦ àá3>Ùw+t,Û…‘NªéB„!L³°…²Í»%ÝNÒt˜ò±¢|Û6á`²Ï‘x¿‹Ø³-аBë\oâ[Y¶DÁf9‚éBw#ëÎéó±tæâÞ¸„Âv;ö³Ï‘cí's³>ÇRö3« Õ})–º™6vjw0“NeŸ ÕñxLa5„ÈÆ>Æ¥ „êù|c;7מʄË>^c2Ì! –uæMÙßF_ŒÔ&xLÓ4!:ö×ó „!>*CêûÉÜ>ƒ'a>Þo>"e„'a:û]=|ü&YÚΆåÜðB|ô'IoŸc"ü eÚÜ[,Û?73NŒî&U²ï´êO–„Æ ¶Yšw5/ÄNÚuö,ñw0Ÿ;hLÛY. ì'೩2íg[/Âm:öÇå¦×â3µÙ¯á3èÏþM¾}ŸÃgFv³CoòmÙöÿ}‡ÿÄ)!1A Qaq0¡‘@±PÁáÑðÿÚ?\Ü ’lc¹vÛÞ#·²öä­„2ØV¡6†6óŸ1l“µaí9=àùš¤lè‡×XÌÀué™ú€È/ÍÄG§õÃÓCÛBúã3êþ´?Qî7õgð‡·øMÿ•=ßð½ë=ò>¯ÝOoôRÝÄRÿúÌülzðÞùˆ}Ïâ!³?‚BûXãMº„ºº´ã®3Œá,²Ë,³¸>[Â|ÞþVü¶Ìÿàõð?øÚÿ†ÿðtw¿ý²‘ám†Ûoe¶¯Û)¶ðm¶¶ÚÚŒ­©aZËRíZ!C†=¶îøyšË–z;‚ð¡žïT`»üÀºGMÔé mÎÍæë ÐΫ©ý`tô ]?›½ ¤øÛ÷ÑfŸ³ÿÞ€žT}¥ìÿ5{ÿÊŸeü‡º½šO“þG·ÿ9?[Ä@Ñà›û á?°Þ}‘öGße­ù­ûŸd'Ë,l‘³Œÿñ›Î¶ü7ýO÷Høoÿø #¶³?.¶ê[ovƼó±$<í“ÁÕ¿ µ!¶Ùa‘’2ªÙV¢‡.Öuhæœ#O7Vêa„´QG©»:Yݘ!1ï6Ð¥<~–¿ÊíÕÌ: ]G¤ï?ÿiî$_ÙkÚþßÿÅèïõOkù$X{?æ=ßÖuPJOý#ÿô'€þPð“ú0þ /¤7²>òÓÙ i÷i÷Ɔðóÿ‰¿ÿðCþÛþ*ÞŸÙÌV^»™,ál,²cmù?ï†RqŸn[Á¶ñ–ÛË:1‰w‡mxÛxÞ7«yXÛ»m¶Þ6Ø[VÖ×aC·«p'èc_(Þ£2XnÌ"˜–‰“,T`õNúËCÃô£s ìÇÿ8>ŸÑ/ýàu{i{ÊûKù}ªöëÛÝ=¿ÝÊÿýp%à?öºãã`ÈÀ¸¿)mì´÷um×Ã8Ïþ6qŸ¿Èøï%±ðφü‡ã¶ðü3ø¿ÚÝË>Gáß pæÇñëà.øw—žvφð/[e¯ñï“9îá¼¼oÁw»Þ;Ùg‘áƒ&ìØYa…àMCûx¿;~V7òñ™t£O(p¦NŒØD$\ .°"5#îbÏ0#.¯´,úP_øÝ³'ð^/Ä Æÿ }Ÿ±7÷'Gh/?û7³ü+ûþQÝ =ÓôÇòW”ÿæÂW‚ÿÚ§þ·‚Oét!ý#í-}—ä84û8ëüßðßžð?-çmøŸçúìçž;•°à,»Nî± $¼NoÇ',’cv6<eî~+m·§â[»Ì»ó1÷0ê1Þí¬»ÜÝâ 3 HS‡«.˜8 &ï¹VQ2™¹ÝØ’Ã6ð~ƒ»…jÏÍñ?â#]ý@Êþ†ó_Évܦ”þ±Ðþ ƒþh{Äöÿ)íþõwX~ÔH6.ÿÃõfðßÄ0þ õô¥ù!->YƉóßžÿSËÆñ²—C D°DA–wu·VXHÙ!e„“³§ÀYeHœ#d–IÆ? ,á8Ë,áãd’ËÃÎò|ô7Žþ s³iðÛßx÷À¿ãm‡mÆÛm–ØJ‚]Ýð,+\‘}-—¶r#‚tWµW†P&(xRieµ„'¸g+Byø†Ó’vù°Ñ­¯pJ˜0,_ã‘ïÓÁ0˜ÿ¶éýðŸöd™½Èú€é—Ûû!Ý­åç¿j)§ºþ0|ÏÍ…£ñˆOÁ!¼K~yÏ»9ß‹Êü3“4€üø¯ÙgÂIÎqœ%퓌°xË8'PIÂ6YÂi'HðcÆwËÂ^¤³Œã€XY'Ã8õþ]‡ø–| ø.ñ¼w¿‡¯‹p~[Æñ¶ð<î­øí¶ü6mî„ZÛô°~Ølz”…`ã£}ë¢Z–Âý1·uݨ•±…ì-û)òá P&°ªc›>­Ô%~ }ÿté>&Ç…ü‘á?ò@ØyJœGþç÷Hó~Í`?‘î€þq#Åx*ð‹ý„ñÿ´x_É/B{KO»K¯ôDÿ¸°Èé8H,/VY#eÔÙ`–`e–;g\çM†É'HE‘%’HìYÞÈI–ad–YÀI–YÈHe–AÎwg¯†OÇ.àá,²Ë½³Œç>Žä9.íÿž¸î×ã°ó×|7ã¿7xߙɯò2ÚÛ°Û7_€—v¡CBäzÖܼC0 úÁøPzÕÙ7UÉê&Àá ³k\ÚߤGSEõ+^ÿ-½ðüG¬Xãú‘à=&?#ñz?÷xŽÝÃØ—#ßü!íQâ§ù£Úä{Wô†ñXT竬1á ጻž ’õ ’$ d‡df’YÙ¿ ²É$mœá%šÙ!–YÂXlË,8K,ã,ž23ŒîË,’ ,±’ÇŒlç;ã,’N3„²È,²ÆÏðφqœçùëáßÀºäb ¸m¥°Ûm°––ó³’s¿~=^ñ›×!ƧÍ[níã½ç»emµµk 6ÚH=-ô­ùÛëqêq÷àH2 ÞÍF9¨ðï†QG·¡ö[%ùâî¿jGq·¬á]˜Í‡¸è‚HzË1”l ²6Ðã¼’ÆÈðY!d’69e‰Ær–XÙaeœ“«;É ³Œ² áŠÁ$œH,Ë,IJI2Ë,²Æ 8CŒ²H,’ $²Ë8Ë,²BÕY%ŒYd’AÆYgÃ>)g“ðz%~Ç|í¼/ ¶Ûo žC3oà< Þç‚RÂ[e„Nvõǯ†Ê[ÀÛl@SaD‡2넜èÎqÚqÙ'¬® xvÙÛN3©ñÆCnqŽåÔð…“–I…ÖY`Á%–2|sx!a#ÎpŽY= ²ËµgV šü °²òá«,²É=ÌÎ °³€H‘vYd –6Y¶A…ŽY"Î gL ,î ,³–!ãïà ˜í.U¤Ûôσ'·¦ËÕ‡©÷V7>sŸy'ƒŽž7åÜÀÌvLs§™ã~®íã9Á`uç¹Ü°AXp“³7 ,xd’Ýp¸uœ²pq³wloÁ 9Ô.µÙóÜHÂÎï‹`$‚BÇŒb1!,,à„~1„ÒGÔp"GO瑳4DËVLã¨PYdàËÊNØìŽÙt²Ç-qéYÆób3_MBÛ$²U¨8ÆÃl²Ë´a`±¶Hž=ÏBÛX¶6wbÌ”ðXv|XÙgòã€êNrÆÎîçâÇ wðÒïm²Ë.ò|÷t<ä§ÆY;l|6u!å4ÐRðåö$œa<Yó7s‡R!·¹x|ÂÝ–KƒoV;à^7‡w^…‡®¨øzËKcYŒ8 ØX~¤¿K5¸p9poìàëØl²&86XIa»$!dvå’IßDL€Ë8f&ËônŒ½g¥¬´Úâð‹†¶^AÍ#ÀÌÌ`¶EŒ·»È¬ ¡Ë !çm®‚Î\b¦níe»vípc6HEã$²Ë,ƒY,±,g¢¶Ä’ÔÙcd–<$©<-“Îñ·|wƶ<wð/™xØwuزêVL!†&ÎNÛ ‹µ’:J®·S!+p–ZåÔØÚñºr¼'W»Ý¦Ãd ˆñ¼tͼ ’¹èÔ&;‰m­Zûá«|uÀ[lsº-Ö³ê{mw=¼{œÛIY£…Ž€œ{'&ëNdÂÍÛ °€Îàƒ‚o¹à!0$G.n¬p…±¼Yçi€ eáMò£Bí@"ÛÇ.ñ›#66Öï„1' ;x™¯Åâe½‡Ã"ê'bíi#…"%…–Z²H$±‰M–@ÈÉae–qÞ s¼<õÂËj~‰–v3$à'.¸Æë"β΂ÞÔccxêÃà¼í‹;eÖqÖci¼‚ÒÔ[KY²ÑœdÍnÂÜm‡âPÒ+ëlú‰õ÷+îü×g›óé49,ë[&Æ^€žÆ¹ ,œÛ{ÆÎË‚òZŽétZmK†Ac+yZw6ºov›BrH{l í•°9 „ŽXDo)è6€$BáËa c&óÜB@àÇ~É)lšˆHTq{Ío—倉Þë:Ö«D[i%f]"™Òqf2pž`‚È™d–N l6IÛ-¶þŽ1l`vqgÑ3³ÆgŒ.ZÝlY ðË%ãaä›@—yc“exîbõf8Ùk‘©f<+oÚ`C–Ûv±•-¶q°‡¼Yéºa‹2Ý/X¯ÃØq·¢ëg8ÒÓ—BN¢òÚÙ¢±löK{„xÛ¤³2Ö¦ÆÊoQéLÈwënB°£nÚd0’ôX3¶hI˜•Ø/0 œ"âñŒãÔ[ G&R<Ô¼¶6Ï´¯6Ï{‘—Hi'g‡vxŒaM'fM^ÄñÛ3ÄÐÊYl‰'¤5ÂÂðËVܵ»cw-ÁšÍá¬Ô³‚ñèã.ƒ…øá;vðì½vs¸HdÝDñÜÊ<1æ\¶ËÖCuǶìã®ãyÎ0º2«¼#±Â)$nÁ{”|[Á—VǙٹcà $x ’:4·c!ŽÞØóacÁÆuáãSLÝJl½dmŽYÒ@Æ­¦ÂFæã"—¨p—£!ž¾ä`Â÷ÏYê3ðÄŽƒ:êìY$›ŒÇqeµ(ÆÊÂm¨{d00eŽÌÀa NÀN;ÅîÈ5À•,²ü¸¢À\g¾ƒfTÑð냛[gޤgYu[I¸†Þrp•6ñ·ž!˓ǹÅM­•ƱÁ°ËÒ]ZË ]ð&v]nì&ÌGgÀº¶8{'5–8& #'.¸Õž[ÎÝl¼¹’Ànìœ[6ߎØÜ„Øœ¼1¶‹ÃNY/R’;â##ØmÂÞ†±lñ±{²lâé¼õ¶m‰K VÆ$ƒ¹z…‰ÄàF¾ÙY†Kw3VÔ ÌTÑ%d7zODüÄ}·§IGt‡M¦ÂNdvq¼)Á¶!'VXÐØ< ¸é;±Æ)¦]u¶–:{‹§4l2û`/yÂè°Ë¡£°œ‘Ù>‹:èf‚™™¸‰¹œzÏ$8ã ÐÉ б’†ç†`ðOㆪX„¶a~ÕÑiõ)˜[Ê“l&DdðìXÝñœce±¼g.[Æq½“Øš¼²Ùòvïymq°ƒáöíaázÉ8ѰƒG³ °É€ds[3 H²rH²7 ˆHqãd˜Ò5ÒèxvÕ¸ ë^1ËÂXš^Ût :0à;$F7/ ý²5$pèàé‹#gY¬¢B`¢Öêa­M 1>i£ºãÓŽ•¤Xw0 G¨v3J†I=ë2&†Üã \,½d;·CHÜèŒØÅÝ-Â×võ¹6P5‚õ mv똶ïLˆH íº6,w‘]³×»É»Ì±-ÃŦú,:[-‰uzÜij ‘r'@§œœ:[Ú  µh$¥í»’ïYê#ÞZù¶ k’-Õ¶+ñÉø:Û»®RÞ ,‚Þ6x#xÈ9w‡vÑá»ãnât¶ê^£€>%Ö'ÝÎg/Ã82Òé8 ºÎ d8ðZ ]í†Jg!v8°q®ÎÃjjq—|jCv?HËmêÒda™TÉdsv9Á¼BÚl«­½dì[BÀ^ÒÖsmî÷+²‘Ùc å·iÛÀÔ¨U•–ÔeB…\•|¥¼¶î•·lY»2®a„]¬`öÇir£í-è"“Æ;›¬(ÛßÞK °»®Ç|¥‘«»u5HÑa¹²ÆíÛµ‘²ãYKIÈSI–63±·Ñ*9oNN² –ž6S0lè" Gµ;«c–§m®q¼v–q±xæ[=¶Ýð³Ç|Ç«¼—…f7œ·®V;,²<ñ¸ÚlYu(ÇD æèp¬XHñ©Àñb@2DlbIm¤ÆáRÖv^´ föL<ôp ,:ŠC1`F^øÛK¼‡¨L½ém‡¤Ha83“™c3gKvËt€'xF0„Ûf¶]XÚÝu“u ›¯Bã…ä Þpmì`Ç^x2Å@Œ8s! ÒÀAÕÚ« ¼õŒjÝ­²*ô=B!ËEã½»eǶYÉ;±È:¸ÍÛ'~¥ëz¶VïêëÌnîÇ…ôÚ3¶VÏBOŒ½–àÇtOnNAíè $wHÍÖ~û¶ÐŸ«ƒ "ÛmxÓ§¢Øã©Xöl¬qÖËÊ6Ù>m»Û»#-´ãtø;—©3–8-#ÀY¤®A/Y#Ó*^ˆÝÛ´¶-ê4’îØ›Ó{À‘Ç®fQààn®í6I ¼á5Ø”œ»:%ÁN\½d.‡'R®.F7c'w²ërëN¶Ó»bÈ2ð *¶X¾E ðÎfp2ÓdíŒÖVD#[Ìèc eÙêlhIǶI‡QÉS¦ ¼AìàÄ`Š•¹ã„Í164öÏ©OÚ¾ˆ’£Ñµê+섇` 9’ýdæðûÏ‹úg©s jT¹|ØØåÊÚÚÚà2¡[·j{pÅ‹lX[l^9 jÞÚÎ5âÐd)¡BËP¡l‹,nöÙØÁ¶q·cK""XeOl@B,ä À;Ó¬†K—½Êc°£¾ºXÂrïÁh‘Àà±³#z4´´<®É–;c+ÂöZlçafvÉÇV£ÕáÆX A»–û².7yÆ»-‹¶ÃÎØÖéweoÔMKúˆVV1úàéþüÁÞ½˜Ù؉Ž1–=çd N7‚aÑ,&ðP³Ž HŸOPÈkÖþN `J‹ÆFÞÙ÷÷“ ¥, >¢?z>'èDb$‹«-œ°B¾ýd@'© Û¸èBß·Ò}o¦d~·9t޿н€ ]0Ä«ÎH(ô4«oVZýµ ÂpÞÔT¼8@oÙ=.«/‹GwÔitíþ',ǧ¼“[zßiÛ&Pï»04³>­ ©(ð>ßů@Kùæ\q¯P–}K^‘oèœw‡BôGÕ§ƒ¾~Ö}KmîO©çæW¤¾„˜a¿~O×~>‡šÖÚø€<uø¸ Î1“#¶¶¶°êÞ´ð9D;|ázmîÍ–Âð¿lŒ9ÄP­>£·c–W¨ +‹Éèo©0m}™Ä·zd ˆñJn’Ðl<;½FCËFÍ-1Ù$Âìue­Œ)Á¾mƒ†[¤¦ ²·W²èX³²án–Xœ!¥ÐuæsaUËÎ hÏgœ$rÕêKbprØ=›p­Ó t>Æ5Ù·‘º› »µ™KC@Å¥RíÅxBÁðùgäH-K¿vÇ}Œr!a0Hµ' …£ß˜°À}Åн!=3Õ-?!v‚@”IlW“'ÖB“ÙÚÞ¸{†ˆyO”V7Ö§? ýLHðc×8¶lÖ²×&Ř˜ŒðêyÊãfŸ|Ù~[óÞÝ›#[n=fàf±1ù¯·€ zdÀû°ØËFSÛi+¹ozd©óã·¾öÈì‘äÙQé!”Ã#3Hm%7£IeObÒ$÷\Iã*`Û"‡Ã-eã[¤›»,¾&‹¼³CôM;`@"S ܇¯$‚zz¯O-†?OÁlˆ|p„T TÜ=>ü‚_Tb"6h%GdîØ$Ö cøYƒà”µ§bŸ—R·…Ĉn£ßô@Zž ÷ÿ”ü) » GHŒô„My "oÒƒrO°£„ö"G½ÙáõiP•}¢NEÜoÝÑD˜ƒN`ŸÖº^ÆŽ'¢¹Ó¨ûn²WW²üûíïÆ@U¿7´2_vˆÓÞðõÎôMëá½|Ä%-»Û«HàmîÒ^7a¶a´……¶t[ 0Û m¶ËΙ!g±~õ¢qè]^/Åtä>¾wkVÓÑ8z¿?Y?I>‘5ø8Q%=ʘa¦ž.Â~™ûsöxäi.£Mø/M‚Ã~¶ý-šÂµÏŽ&]g‹§¸û¶þãﻢmËpºz.¾¯<È! ¬ç’2TlXVÂ`ΰg»4ìÈp¦»Âú$·Om°rÇñ¹Á«Àó¦Í–Bá m«þCÂxÙÂ#ˆô·„(ó6œ¯P×,ú@çöŒB¶¼@ô;`ÖØm=9,Ï\½LPAÜT?Vf({–~Ý”(Ôþã¨{Ù³Ž“ÿBMp„kÏ™X‹ÞÂż¨Z¡ÏÉ[âÝo°a Ðf.çOVŠy[á1Ý`;Áá1ݶ÷<¬r>хãî!¿T;‡f¨?„PÛM÷. Øö÷޼Ô~Ò‘ëî^Ö´¯­zÌï9{'§cTt)‹ksU’˜¤ó>Y#PèÌÿ7kÄëíú ™Û?˜~r É7Eã[Û2?†ó{*8ñ¶m†Hm!„àÅ¦Û Å°Û[m°¶ÛÀÃld0ÃÆëm§Ô-°ðq®ðqœx^E°þ„Ìr±Ä‹aVÓm2ëØ_¡~„ý'ORÞ¦`Jyõï‚~²SÒ~£Š^9¹-ÂÇÛ=¼³è¢"õa?IñÔzß—šÏª{»ÞNÖž7ékk‰´Ûû‡kîVôß‘µ÷ ,#ʱ¥«eé²|ó X²6èn¬gƒ`xôŠ ’À}QR+ôzŒàìg‹}ãØxoahKë«Ú5Sߥ¬~Ôû^}eôÐ'øC`CÂÆÄ&2‡ë©»Üö™íÓƒ÷³èïÜòÙDÕ½L XöúŒ2tº¡ÜÁã|ÌR?ÌMý=oÝ•~gÛ#ž;ÀDWE•¿ aÈ$´®€}[~üÏ6Ðe:ýLãÁèþ̼×D¾™wêxñã3õì‰9îâätÅ­ËíÁI&CßFÐñrã ;؈bøàwõ#Ydð@ƒ7Í¡¿1…§õuñ•ކ'´éìÅ9¼ú›Y¢C¿²0qzåó¸ƒÜì¡uˆ ~´zO­©ô‰ bÄÿ»û1®Ý3/³¤pÉ¿‚+v@ètºG]~_„‡?WxyîôÿÛÙ²¢ë缚|¶¢(¿^ݶìýéŽGÕ¦ ê??|Цüª@=HíS§ÔGëÛ¿wth¯¯0¢¿‰Èú¬Ã¾oãпŽV!®ìù‘sàŸˆuÜ’x{]KVØ~þ‹Ï"6ýSpÿsÔ`vÅøØ9ù‚·Õü¦ζ“¸è´ Ol!Cg§Qµ±¯­™ÀÖKM‰xÌôö},Cü¡+¨zm–7#RptDCžÛM|Äû›W»ê7z¶ilõoÔÀñŠ‹1læp„0ãÆ³–™<äyµ´Ý-ølyÎ6\xÖÛe{m"Ó-á¶ÛÜ0Ûk€â7q׉^!œ^7D+Ѭ¿RÇÖ$è¦~µÚõãm±L^Ø€½|r,ø †öÏ£‹´º›éo="…¿Æ€3­Ør.½P©î?j?£)§Ðõc QÖ1)™‰ãö„Þ>YíƒÇî2O>&[Þ}K¤K¦É0lY~׋? ¿I´SÔQ£}g¹Çã8kï¾ *Åy]ÖÆ:˜&`z–È^u„w Ò6Yäü¯«Á§i6ÃØL˜ 5h)¾Ò ¡jN eˆ$ZqÔÙa–—Þ [GP±:Ž>ÍÔÕ¡:ÊXqChð«Q‰3žj,ùg¤2t3©X=ÿÿž#ßZeÓáÑü™ö c|’ô­!ëÄ} OxK_l°h/7emïäde¿ ù,D¶:0´Zäa}`™ë" `7Ǿ­ é¿žç؇¼žŽpLŠqîúiÝz—ÃÁœ‡_¿öÊáZ»‡_õâò :õÛÝØ±r‰ŽÞWä‹äcôмÇýf½\OÙ2ÍŽ+è”»kò÷ÚÑÀ]·¬ŽÈ[˜ é XÈ'ùõxð=¯~Åû…çZ<ð~"߃8û}³ï F½¡þ¯à°$Â[ÉÙ:éê ?J€÷öA¸ß5Ξ„Ìx¾ÿË7 ûµŽ§xÿ­F'Ygk#ë_P'Ó#§|œõ5ÛþbÑP}ýÌÃdÂð‡ [dfñïƒ[ev8êv΂ï~$t÷¼<< –ÝùXÛRl÷O\yJµk2ÑÃAo¶×8~ fñ½ðÆ’<+l^[tøÐ`ìŸA$³E¢å«dñš0ÆÇøùl4Žý¥¡ÿƒû-¿#!]~Œ»°f|Þk=Ïvö^0™q¢ó ÿ"Q¹hÈt§E³¹Æ–—ìx<ëÔºtÞEŒ2ýÛêh[±k »ßï¨þ™m.¿H—ï*9½FÈÝŽÞ­±™¨~}ÊÈ'¯ †P!ìÆïõ ?Gýp±ÍÆØ÷Â/»ƒôYµižã²~û÷i4ñ¹ÚߢþGO{·l]­ã¿©K IîNE?@·çYɱQRxŸ É«T lÅ3Ó n.»‚<úïz´ùx»ÏþçîÉSã- 襃ñxü@²~OåÁ,7F;mZOaC0{Ó‚@{UŸàX=B[Æð¤¦Tn%h™¶$FHYôìðqñŒ‚Á·=Ñ®,PcЂî<îW³%Z1“*E°Æu>¤aÃÏI(?~™w°SÂ=ý®‚BKGÔ½½SlïH=Ÿp[Â:½"õåƒÔwh¿ñ{0ßP@^ïïÜä zÊd¾·™ý³ž ‡ãÜ)û/sîÚpüÄô­`½nB„ªh£ßŒë?…öØ5¬|Nçꛞ‘ ?IÐäó¯Y*!æ ÁÞõÈtpp%¤[ .øÛ­Þ6êÏgÃ]‘㹎Ž¥ƒ†zd‡8í"Û½;m9x`örBùfÞ°Î;φ·o¸l7‚Óñˆ’Ôm-80ã²vȵ<…-„m!'$²;ÃØ“ø©ômšúFÓBРÂzÉ]6 ¶ÀŸc6÷r$tiȳ˜i²Ý ,8"ðA€‰'é×Bq/é“RÕìþÛr»àžnM «Ýù<OÛ;‰Ï5Öì…§|Þå†õ±V³¯unÞˆ»o¹¬ìÕ.ýçâ%æl$hïOϨ(nÑŠ!_µäô>qŸ˜-ãæ^]Vë]¼DHH–w¿*1¦ï¢h>ÝkL BÏ‘6ï«Gá9éWr»×Q"è$}` Ñ á1„ÁßýªnŸÜŽF¯Ç¥j±àT"B“·¨‡¶së4ñaQÊþú…©‡^ßkx/Æ}Ãc¯ñÁü ƒ3ßSL³óÃÈI¯K£y©ãò1%Õ‹×`w÷~Þ¡)3ÂÓ½eC{õòĪ_»›´ úú¤&úRnåÒû™¶U|0å×ê {'±æV­é÷ÚzI?½=²h•2µÞŸ<û… zʽy;óøƒ8| 3!Ì 4¾¤-‰Œ;_P JÑSŽšª1ŸnÃÙÛAꪯl #ÀÄÝðø²ê e,xË à—Þż2œ¼e¶®ÙÁÎ;áädç4òp=ìñ±¶¼7|+Ç…é õÚ SÜÂa`’Eìv×Ͳ‡÷îDz™tاø}[özíd<:Þ`AÚy"I6µ ²×MaDHÞÐ’HP«ÈŸ§™3¾«¯è‡à‰ùac¯å‹CöÙé±ÊEôDNMóóì¼çsë]sÛ‹nò æÍltÇx&, ïE¥™oÐ{˜¾ÉùŒtQ莼´Ÿ^»CEöÜÒ1ï׋ 4|œzÆøµß^æóôfÝQ‡—[êÄò¾ÛÏÝKv§óîB&¹ô‡´Z°$Wkt?5;Ñ9Ô ›àx‚Á‰~’hõètÿìXçxÖñݦ±ÀÛÁ LàÈH°XDDÆäÃl*¼¬@Ýä“·\w`±mÝõ¹·´"m'fÉÎYب›{åõ‘Á¼o|uã¼ÙÙÜÂ[myW9ɺ•àK«iÀ´ÌÁ´ÙùÖZDToÃ|!‚M}·G³÷˜a¯OvFƒæêéå†5Õ÷©Ž£À;þ'=/nxš®ýÛC¸Öv<Ö C3Þ„¨€Qè&[V·5Bú/Ppx, V0„œýõb¾ñ.'¸cä~pXõƒÿöˇí=ì« OQ${«Ó·îlîÄzüÏÃqoÌÏÿ‘Öt[p±äŽà¦ÿ!<ò_QQÞgéÈ6²lŒêÑm8u'QóFëÏÙïõ÷(a›±ù#peìtoï6,øu÷8áéuÐν} îŸcä¾ô#ýV|ä^È!Ó¸ÈWk¼ÔA·Ží¡aOBAÇR{R[w ÿ)žølE—Æg<õì¿l=NH]ôl ¸õ„…:Ap¨^‚"K^@t[)³Ní¥°T šÀâ3Ÿ:ÉÑ ^yƒ{Î6Ø^7‹m66Óæ2ØxÞ ·«P›)¼ŒpÓã»°ŽN:àùûxs‡yÇ\õ²;Æ9g&ÆB„DÙ¹'|]íêÎ9xÙbÞåºÙRYyL½LÛ^6Ö|ËðÇ˲×!”Ü´„áëû…&Iy Œb Ô •uàU¥ÑLÕÉ›<û^!>–] 1èq¬LHÛ C[ôÒú·£ ÑÖÆ!]<´Á ÅœƳ¹éï¾ÙÇÄ ÿ‡î÷½]ë'„lÕZ ;c´e*PuÚb§Ò s£Š€VgoË¡Ò￲õ;êi·È~d¤>GöÆF}ÎÁû„û„{tñcÓá~`­ÿvdÙ†›ÈJ²aÍyدÜ1éXû¬˜2Sû4‹æRN.’Gß,fí›Òvu²)PŒÖ¢ü1óà­Ôiâ)è‰òdö=®¿^,ÙD¿S¢ ŸÓg~¥“GŒó¯Srëã§PÕ&n‰Ó=‹¯ˆµ¡ŠKˆó(3öeî05õñ¶æÏÔ{æwZ›¤;®ó3”0ñƒóÁ~¿6>R÷$1¿ ë½êçg±Y¿÷ªÛƒs£¬Ç|­×ùú‰…ΰõN‹º|cÞÎ;úã?’ëgú]qœM'‰Áøl8 @ðH 1ºAãz‡¶ÞF8RÛ ¿ ¶Û^ž6Ûc-màÚ]^N ·YaêÛ~ËF/¸Ûa7ƒ,íàÈÜçx×eŸm±™ÁÇPk0ÝÆÎ Ç×ðÚì¸L¬îÊé6.¸Ó÷¦Æ÷ñY8Ø ´2P—=~FÝœ//¯¨’Ǥ@S#¶ë¥@IŒ!?3¢àŸù%‚Ju&7Þ®ÀþÏ4 ´•ŠyUÊ"E¢c’í<ˆ‹ŸhìóHz˜ ôÆeå%£´´„ØDˆ¶ŸC{ˆ‚ôc`–œ!a XA^¼$ß%ßßòeÞjíþ@‘v;$`÷ZB_Zоëÿ»ô¼MC#L¿°ñ ýÝG¹öý_¦ð×ï"Vz:`ݳÁ3n»â~8õ ‡•åz:,"§Ù}>§3‡¼…¾‡Þü_’6óbˆa>Ù/zæ|zšÚ+×êŒ\ÄWO=Ù_~]s¾¸—=x~GûãõT?DSºó1[wÇó&^ޤ½H`÷‡¢ÉrH|пìﺭñï³±%³áîg… ö}È4R|PLGß~ÛÏÐÃ)ºÝÃ×öÖ'ùmœnñJO׫kïÐ!ÂÎd éi’}Ù]úÝ%’ê8Ûm†Ü!aa•…¶àe†{¶”ãxØC¨fc·Ù)³œâD=仨q¼vËÂÁœ¼™³³woCnÛ6ÃoWyjppΓòó®p«+<3k+uÀĵÀ¤éc²o:± º8DxnPêóH¯,Ìù?LTOáü“ý‰„«vœØö­¥‰u1<$:Áú$Õ7÷Yúɤ`ѽA“ßí?s×D)ub7c~t0¦›÷;:@ö†[¢Rùï'™íÓîÜö¬~z¾V@ÏF÷3QYϨÜýžâ{Cõ ^µÅÓ1ty§ö°YLGÑøŸ›é%¤0íêÕØò]—å]c“­'ÇÆÏ»7c®ãSæN €67cƒÐ:K ~ |ŽO䨮GØÛl^Z& @ÐzBU ó~0z¼òL½ ÿa†…iSIÚ!`ê÷ßp"¨[vÜ2í„ÛI{¶À²[‡òó°o$'Ü)*WW\ f šÕ+d$ÆéXþ}:Z¼EQÝú™9ýôÿÆé}ô?“^à?3§¢}ŒÝV‡¦aø´W#SYÇÑAëè[Té>pÆ[k )º[oô¹ l6Û 6à ¶¶ÃÜ0÷¶Û &Ë °Û-²Ý™Æ» Æ’&°Û¯e©î^·sàñƸCÞ<>%ØÎ6Yx9õwucw“Ños¬.p¸Ûð ’6Ú̳“6pÛ©èßgñÓ4c`fÞÐëudlG‰ãÿYÛÃ|LSúaîÏXÎ}‘âíùM@ï«÷ècÞK®÷ê,Bþ+ù*©YÃl÷ç’]éå™Åx¯c°uc üzÂ÷ˆÀ†CêÚð÷sÔr;x=ÿÂÂÊÞžÙ†Ž#ß¡o²àGÜð<Àõxÿa¶ñwe›>!¤ç€=Í3ß©ýÖV³øàw¿Lo›tcÖ$ƒÚ²¿²{DÛCMž=‡5Û#Û}îD6c8[DÃÝWYÔBSÅ_1Ý¿ŒÃÕ_%Ùå÷´8W‹­Óá<þnÏ×z;²–Ò¾wIëjIî'ƒ‘ØU2EP/D]IáƒiÜy?Ϭ²Ïç ²ž¤\Îy;·Éûþ`Ï”ÛpµHkÁÐà¼PÃbé ðX~ 5Ÿ@Âñ­¶ÛÛÀÛoÀð=ñ± ²Ãm¶¤%Ù“¼¿­°–ÛÆ™Æ!¶2 á°ÃÛ ¦Û-¶Ãcua†la†îÑ`‡-ãxô]m¦…Çw¹[´²2Éy Û%Ñ.8Vï8ËM•cr½ <®Ñ—…rQ´—îÕê¿i[l2ëŸ|h<,‘Âl—Ðu&1ßeƒ[@ë¶ggѤØÑ±6ºG˜†îÔ/,—¼lç˜Ø‘9ö ö†Èn°0¥“xR#a'. , ÌY "¨Æx²Qé'°|iƒÃÎÅ”±òÇFÔ`ϳÉýˆ,%QâïöûD¾ä_ÆmfôÀ‡dßawÓÆÌb<8 «§Íߔμïž $鵉4.aüügvš?XÞIü·Û_¢!Y¸• °Õ»è,97~¿Rf¡Ìé â)mÝi³QŸY`ýtÂ#¡öê@g˜]Ë\Aqó&6_0ƒè»ŽMª2i:¬Ÿ[7øfw€øº1»×ÔÏ<»ã4µçžóñØú.MŠ;­b@ÀˆünuÁ±Ýþ-ð>Ý·x`Gš?f¡,‘¢ZÉ?+Eh_qg‚3€^¼ê¼@pOUg `‰ÃÈ!ÏÇ»^ÂÝ/OÑ~ÇÚCPŒ²€ïaè\øïl¼;ÆÛ ¶ð&Èà bÛxÖ݆Ò6öÛSâ7«VÇÃ.[m¶ÛèdD ¶ðs½ÚnäçÃxÐ^=‘–’‹ÆÛÀ–™×ÇoÈÁÎNl=pÛ¼Ÿä ÀÃv¸"Q!Üfß î»ÞŽÅÝDûFô¼—½Áoß¶ºäz‡z·Û d£¤Õ蓚^€lϹ¹îlF‚Ø ûÕÖ;/j- ä°ñÙçuóºøŠ¡Ýú˜ iauí²ƒŸ`œŽ‚ùØ'Ân}„#×Z)ÈÃtÿÑЃé6yiÉóV<©—æa)žwG™ôˆ#£ŸüCüÿî¸-ÛÜ1µ¾®1æ@>x³¯½@ê_ãdý®çMÃÉ ªõ°ÞAKKõ0غRëÑlúá×ÈÐ:Éñ=ø™åûñ ¥À÷ý˦÷àݘ »÷ÂÙj!ƒ÷?Cà…?Œ'f¡|=?Ê^¬§i1²O–§lû&;<úçÑÆè´:hd,ÆÄ‚(ˆ¡g÷^Ð…©o…Û´€§ØÃ/t;…NýýÑ2ïG—Ù°Ò–Æ!¤–¬½ÛÃm¶¶ÚÃmã¶Úð8;<6:0­ˆ²’òà@tÍè »v ›n—Ol6ÛÏ Ö^1-àÎ6[,2ë#ƒ9Þ€'mäÙß/,âBl»m…¤Û;Ãáž–Òï%9Øb¡<Ë۽ͬ¬/Û-°ƒk%4`H‘á$mž0„sY0l„„œ±#èàBfb'ìÑ' bžë_OÞÚ?‰ø6–à Y(2íîŠd­ñðþ °äô2vÍöh§®~Ô!£Ícô¿¢xE‡li9ÔaÁ¬B[|_N·fyvÀ8MÍKö™í?|žñg¸ÐKü@ q¼ÇY¼|ÿ §eèëò¶=§=þŸOgùÝ™×ß8À‚4å˜ y¶0vå4êûuêöð»Æû)>ûˆŒñµí„ZÄÏíÙT è ÇX¼Ï¤ïqÕÉ5|ä>×~}¥WôñüEÔ}þ§Z·µw˜²ªí(ÍÝŠ~ẹÕi¹¸šOˆÍN­‡çï Ë'Ã΃†>G)Z:ë±}F.™×¿DŽ—¿W껬žÀwvºóéû¦Ik¯s=e{Þõt{?ÅžâÙ5?~¦7ú¤$w¿Ôø;ô¤vÀŒx”î±×iöc¡ó?œ á™Ç{~Œ%t|“ùWoˆÌW5߃h¨yu gãÝ~£:Ðkê¾»b×é³4Am–KbjÛc†ËlF!À, ¶Ä&Ä)À0ÃiÀ–™¼kõÉæxÛ8 bð¤ü8Hå9Ù¶4-–ØMIe–ÖßDŽå®Ë/V°]XC}6KÓ7ÆrÞH,s„æŒåÄ«ÆgË,“†s”².Ա៳>$3…Û®C­²êNDH¤n蜕ž4›n­·…ã»^Ù5à·‘IG±D›ï¡.Ïö‚?ä)Š#þL¹Ij ³wAv¿2‰Ã›îpëwçâk¶ÞŸ¤lt³ÿÀC„ïÑm:¾çgÂ~P>ëB9?ÔKRLÊïƒúú„Yvé¯}Åû½¦¯ó-i™k.¨ûRÏP“Žz°_†~ 8=[Ñܘ¼¾Ûø ûñÆ Ñ>oÇèÌΧ/‹ }¨ÇG?{ö1)“Çö™ôNô×õ¾`«¤÷„H£ÕëónŒæì¸»Œùsënµ‘¤”pÇ„èË­^:üfÞ–Ï‹{²Ô g€8u‹ ç¨ï'8ïž¹-m'%ãݶÃ(ÈžÒ¤ç#/.]q¼ ø‹§ñ=Žñ­§Eï®QÁÎ{MŠÙÏ!{ßÖ‚f¿o±$——·þ]/OÌÐb÷™úIÌò>SÖÎõíº—x-.‹ñeP;~¹'KF?qJàÎì z¾þ¢ŸVx ±úË«SzÅu$ ±Ç·x–õ#·c”u§¸gDžºM\3ïaÄo΋b'ްäœëí×lcÚ8óè$¤gµÆâƒÃC~ĵäeàç6èøÖÈÆ ]÷3®5ú,ÂðÛÎñ;ö±Ã>8~ÿžðlˆ¢q²r§o;ÎÃm¼oÀà ¼km¬0¼m¶[m°ËÌ,0ÂC,aa†!·ž­#nãVíx\#l´à̵Èç¹6x×â¶ðæNr Ëܶ¶¼i(²³¹*í»-®Zd±²KËá†î9°™t@q¼mížø\0–DŸm=DG|m‘½2{ÓÞ *1¯ 2>Æñ*ÇmŒ :,áà:ÏL@ÿV,Nÿ°ÂznÄÎÏÕ§`øºö#±ôõ Hî=ˆÒ>˜ c×à÷†<“žMÛ¯Ke&Ac oܨ KuémB7zÈÎbD¢Þã~Ü9ÚSƒpycuŠhÂV®×ÚÝúƒÜ¤ÆD‡t†ê1;vz™rLYÕO#ÇÆ“<¯ßÓý‡]éè…1۵Šȷ•ü†@ý!ådž„h_ÜêdíŠ|¼¨ß â~%bž{fRÂMY#íÈKö„K…ÑpŽñ{®ˆ­­0í†NDd{¶`f%ØþÁºu'æÕ"о+G4S¯²õ&ÑžúCåè ïüÎÂoýð:—ýY dš}²=؆{Z:4Xm³¡lHô³ ¾„Ñ4á D~‘,—üTœlñ¦[ȳàd²ÿ¨£¦Œªï.r9< +ºüwᶬ 8q¶òKu6|±£œ£i{×!ˆ!ŽÛØ„”$<–DîÛþ¶¬ðÛãm¡38'9Y[m´ÉzfKN5–RÖYYô­¶óÿµðBç8ÙŽy•øZ_T¢¬ÎöY  c€']x™lI òùËO¯AcÌNÛ›uiŠjATô!D~‡mTË´Q{†$'ßÛ>ážÙ1­ó#m—˜¤1ü»P€¬î?|j$p5ŒÈÀ4š“4eн“>Û°&£±ã®¤Š !ϲÓôóý]þðˆ‡~3GkÚo‡“5† ¤·Ïâò6ýO½ÐŸÑbdî“ÿe'§dýÏ3ZžãžuŽhkâ~¿‹- ™¢˜"3Μo®ß;÷³×Ò¿Ïñ¯:úÜ{î?,8ÛÑ€óù„‰6=¢A™ã«»'7›X*ð–ÛÆ™ð.Ç…ùm°l¢HxßðÞ4Ïñ888pPÛ[x-µ·“6vm„†q… ¶Ú‡‚“ ÙÀÀú.äÒÂØÈz›\8Ë¡ãm÷ñÛxΕãvÓê%·¹ÒYBÙ‰3޳t—NÛ¼–Ø”Ýä6ÆÞ¶,êéù8k¶Ýo\6ü ^Õ~ÒDv÷…aN)£œÊ˜ ˜ÔÐó] ?€—”öN߯Tåëõ0]ùgýL¡/ø€`ôÝe刲Îè®Åá×àuUáý¹û†Ÿ;Yd¿tl¦O8¿îu÷„\mª-,S„ã4þ²q>³¬¤º{\(…ûÍGMN{¿DÕ#Ÿv 9èS/#¾#öbEß'¼3|=3¿¸0™g×x¥õüB–+Oyû’uŸð#$ž¼?]•«=¯Æû–ä]…GÑ~ä-Nuö#wã{üa„¶P7u¯i°×?²e”·î}ä$|w–oœô@Økìþ7Ô_»ï¥ï?Qè×Ѿçë]~ª/qÓáØ| ðÞ¹WáØr ƒ)uι9lIn“ ‹¿fþ4¾]±uÿˆÒNö»þžh‘ëÅé‚ý(Ã"Ò0 KøÂ½…œ Ø4®äWBn_Ã_­ëm-74ÁÖe¸/¼Þ AžoI >ÆRl§å4c¾Š€MŒ;q¬\û!x1ͦéùçÚÉdDïݨIdLÁ|-ô},(nj°°Ä’/™ „H—@T°õ¬‡u'iU¿|Ÿnõíeµ~>ìŠ[Qª¼ç=eßË> ÖrK¼oû²ñïãY×Çm¶ïâ)ÑàüN7àóÄXlú@)Ÿp*ñ~cYÛçÜŲ{ŸR¼ââc=‘WÀ¦_Ÿ<µü—¿¿hÖñgéâòñöœæöò²ÔutÁ‚õÇ¿úú° µéBnRvÁ6E4öè1ÉšžJN„O!,fìlgÆt3×GccÓ¾þ¸:¬ñ±ÊÝ]ìŽòqßË8Ä5‚™˜ã8Îw‡>g9%ŒL#¸D›¹czŽ7•78߉ð߆õc ¶— z©¼Æ™™Á (µ†zÉ˾â 0õi(ð6Â9ÆÂ³Á¡ÆðZ2÷i)6¼l¸Ël°Þퟙ°p½pÉm¶h¥ë¹q‘mú§¶íÔŽãIœ $p]ö¦›G$íç0?’-ûK‰¼y%|<»Ï? òm]ósa3¡âx8=plȆ“ ä|÷#z{ nj\6¸žÑ´±k‹'S=A8Ó‡!¬ü2lïu†Ë}K¬åVuäúÊVÂÂ&aÜté‘<¨î# M0Y÷m%rlã0»’ÇxË8ï”n‰µá@‡§"æ|E¿Ï8î88ÉäÅ¿8ÎR:a˦~;¼üÚ°CÀÃÛ ‘Ž!‡XØXHÞÍpÇ/M¶¹oÁBÙ}Úå¶³-¯‹m‚|ÎËÕ·{w¼<+&uÂmeSŽç-»þ%ÛÖÍú§¤Þ©ö„y‹ßEÿ˜É¡·PEîÃÜ4N«êć‡%¶Ò~‹¾¾³O”½e¾¸=Fcòü^º#¢†ÏQ#Õr?ä )˜€[ùÍËUÖ';¯êÔõ[¢Ž>º‘xÿÆ"ƒf¥+èú ïýà…ôûNáܰxné>Bã×a}D'ÿdÙõ@üù«¯ì«.íð,2BÈ5¼Ë+8N2þi.0cÂd–J´`{[¥¡.ãü³ Q J™éˆ.¹†ŒêüÒƒ¶ñ€ã>`½|P†FÛcÛœçøe­¬ó¶òq§o#Àä§Ê°œ ÀÄ&C)BÃÑð.q¥¤C/O/+“jJä2¹¬¬¸’õ/\,;fÖÛgoL3#±¬ŽËÃ’¹/‘¶Ç†aàm¿SÒc[{N7^ ; =.A{'Äð@*v½ ëãÝI|'òÕt–gž«êBã`_-Ø9,gitüMŸöjÒâ9ªO#À¶-Úîα€õÒ7Yà¿ÉùÈvPcånþ"J›kï¿VÎçŽÔÄ×›€¾íÕMø:ÌÛT¨®LØ2AíÍ~>е½ xûYã̦¼à=î}¢ã3ÍånˆDÅá–þ0‰šI0"G-3Q´1#·df~ÀýCœ•SÑíéV·ï„Tº"»€w…ÊîŒúçÜàÊ0ÔoNÿ¶Qµ0}Ås±¾¥xÀçîíÊ",Àš¶úÙ}­ð2 Öo»R‚辰FŸµGšráû2ƒ‰:bä/W×»\¯Ø~eN0dø{‰)ØJŽ9|gÄÉþÄ^q<H2iuÙþÔ·ÍEú9ê^[¤y·ßù6OÛ´¬åf¶ßÿ½'Æù‡¡ ]q˜1ïF›ëwÝûŸBZË*:—±ó{þh‘TLhËÌw])¹Èó§@Fñ×½6rñðž»@"\÷õý60I'Ã_V|1ä]ø"Kðît•]xÞ2ë~:½1ÉiÉÉi°ðBc CÀ7oaÀÃoyÆÚ¼9œïVÚð«l¶Ëi¶’ÊÛkn§rªóݼo <7kÇS3ÀÝÿL,ñÊ[Îü1³D?Ì Nú†¬½ð…"Ýóî/ŸêˆS?¸'¶ïpð¹h± H9¯â ð팛~x}#¡K©ÈãÀ‹J:ÂEÔ䇤“A m€x…Ñ®MJKš«ª²D€¼¶Ôi¤ºjò®£*èóT.†Êfd” âNHú”˨–˜UòLZûÈPïlò¯ÑÍëú£]æm}Ïs]*Ö$9Á0Ĩ:”.8]O©sŸ+!îPs®ŽFD×~yúzŠé×nx…Üè( ð?Èü¦¦}eØq³˜þ;…û½Lt^ŒÇl»ì%†$š;j› ­Pu>Ñ’½ÞÔáL?+ê“Ûƒ¦{w Ðñ–V¬>…Öæ]ó8 ÍØ%´wá4z2»9Ú=ØóAª¸ók@cv`ҺΊ íMA»¤3°o|õ±uï„L>5ֽ׌½^//ÄŒÆ%ÆŸ €j™­îP»IYÙ{·Œd~a\ž=ÛÈ›³wqœÇÖ`xã7¹CXX† wÃ8[KxØN«eÓHu‡l;ÆËio¬·…RÝ8W•ÒÖÄ»³çeà^2È2q}:ýN¤^0?ÀMýbôËѱç9Òçãmãõ !F 0} †!éêjtþiCîn$— îÚþ„8[qÄu¨õt9-à4îúß×¹áýÁ¨^t]¿°ºv‡téö…ó™ ëV¡žúíu‘Àê'Ò§/³¾xÚ‹‡©‡ï©õa«E‚îžìì‡^¬ô_Ó=¿Â³yçpÎ?5é¸f¬#'6[®Ng‹Þ2|~ÅsùŒ×åÙ›¨:Ù¤¥ýßêIí¦÷é& ÷â.mÇÕê ìKM#Š|Á˜õËôBÏŸû±_‘R’mälï¨Ü 4Ãð):t „1²LÈ”ñ‹9,hFeBÙÞì' à{VUÑúk\¨«%Yðbÿû‹²öã=¡äv{YkÀb†0IAôB`ÿlžÃ¹¶&nb™vâ 0 xb }ƒÅmË$³,‘²6À²Î‘¼Yðy_–“ð8Þf8®p8a–Çœ‘½0°°ÁHzxq $0°÷b=Œs¶ìˆOvì(õkÞ³Û)l·[ ìyÛx^5åuºµ¼¬áÓoÄ{äa˸~"ƒés„„Û×:[K¹ŸÜôx õy»³¯s{‹îY¶úšÌ…Ðf;¿­*Vüv~öhœ×œÀœçáõ½¢YÝoe¾æ ‹o–÷'P±t¿—|Ö @–Ô&w÷¥=~ág(jü¢—h¤ú4ñoÌH ì]Ÿ´ákÈÃËtóéü‚ÅØz²ïç÷«mo¦äÞ³˜sÔ^»j-=3¸ÌÚ¤È}A5ĆÞ-ú†V0<³á@®~©‚7_þè èa Gôþ!3ãèeѽWÄ¥ê Þº@z{x¶+›éóÌ™Äì}Èw£ûHߌ?²ªfµÓÖFÆÕhÅ áîy€ª;¯ÞÚ~³ëïcf¦ïTºì¬ÃÅ“y0¾ò=¯@7è‘^ÃRÖñÇÂ{­òûgõVR¾~\ Û³½¯1bó½wxmÝ=ß.j%›Ìû´õ³[¦ó«—úÏpZ0‰×ÀRò7]ÝþfžÁ¨ÒŒÖ Qüþ©Îã0<õìpOÑn]öÄg«ígQ’9£Jdèo]ýdOî¾ÓÎJ‹b½ ›óž‰!Ìê¹Lr5DÖQõx "òÐB~åùÙeÑùB40·b'§õû<³{ú…êêqpÛõ¦ÉB¸n[bWZ ;O§Õ®÷_uxêîMÒUc‡8Û’Drd%ä㬆cŽãƒ‚ØãX¶!ˆÈ{àma!"/\i¶ÞI}l²¶ám¦ðÛeÖÖVÜ-—»VÊñîÞvÆrS|ü=÷#›ts¥×Ãþ¤Sî܇ðl6B ÷kð÷œLYÀÝ:ÇÆœÓHXdœdbKöX™±Ñ² •‚<±Ó £Å„£Ù~"ü›!– R2ˆúÑ’&{ÖˆŠ#>/Ç”QÝ‹E‰÷¯à÷ü@ÀAê½[§ˆ­ìßÚv÷áfó#qù\F^Ü8?l–ê»ã²tø‡¸~›¿8oéúóiæe+hï{d'éry²õŽå†]½Ò’zÏz3Ã傇Œþ5Ü}¡8ÿÝX6w‡§´ûŽ™|鈻¼7ùÉ5DWŸù61å•“ïñ«ë0ªÜ½»'ÎÜÆ:ú÷·]¨f³ÅGÎ_Uó¯_¤ýÇ«€æ¼Ý¿ÆÅM¯m¨öYÀôdÿ³†!<ùãת»ÄxŽzòÑÊ#~ôMù]®ú h íÞÎD¶Êýu»äø§®òÈq~µÑƒ“v|ÛY±³õéo ù9Ö’Ïi«ëyžß´vÎð˜1œßº¦Âë¶[þ¢sø[¡½œ‡³Á¯ø¡ÓÅÌýfã`¼Ëõ(&õëvú¯ zæW¯€óÞ¯Ò~—lÒ|ºÜìíkè&~‰;Û%ÙBrtHÝ6õ/ÇmnÈ*ëÂpfÛóÎ=q¼o#ÀŤB,°a†E¶Þú¶ÛmžVYe¶^å¶FÙD¶P-ãmR^íIm· zᇵgƒnž5ãp‡¸hý‡å›Ëæ8sžå=¬­‡í˜q{ßo§fŒîÂÞ”º˜$yɰåÞÎË!É–|Ï–@-œ™Y‡¦2Dê,2Fó¼ìÎp¼vÝGÂÙr>ƒñaøçzà#Œöã€Ïp³–Bš (Š””%ÉJ@®Ç½&;ÙJ [ÐüÏlhí‚ü˜ëýà&cƒ»"À®…Ö7I'éIö¦¤Ãîâg…àwf)˜Uåwu®RÑ>ÜBl_}ãë>ˆ`º|éõL¿ÌÊ ,áƒÎòÞ÷“oºÏycÈZ ¤¿@ê:;¾qA[¹_Õ”–ïV;'a¶Ë-2rb ^íIøm°œj2Ì„“2OdgxØáÞI>Ùñ !‚ïã‹e–Y€`â]“ãœäa§sÈJû$ãã༼cß·•ã¹à~"E 8C9WyK[¸c987ƒƒa†HaŽã¦!†²-‡…–2RÛGÌ¥¶Ë)Um–V× i›)Á°œ9§.ð±¶ÝZëLJ¾5žKª­IÛ»eaêIl&w‡T€Ÿ¾#O¼qñ²ƒ¸JŒ7uXd àŒ×û»´èbbC2¶ð½ÛjO¡´¶ë^5••’`3eRxYCEÒË,³vYggaœe–@l„†p‡9e–Y“£•­'„‡3ŒàN¼å3´ymgŒxÎ=ÿ‹&™©©ÎYÈúg"ÓàYm°ðy¶àˆx 0°ñ°Û,¬3hRÛk¶ÊÂKì–Þ7ž =Ì}d•…da·¾€yÉÈá¯)ag s‰^ Î7„ãy"‰+Øòmê¦úŸÉ>1=S|‹ úË…ï2‹o'©xˆZâì{ÜrLôgwøîÁ¸É?ä_BŒ[ Í‹)‹¼Gk:Z?Õìxq7Õ¨ñÙØ€\±dÑ¿à½=wë´µ>ÎßÓkïñ¯gA$>çÿ3¬¶Î4e#Æ(wfZÌw.æÙé˜}»@}z`ƒì[ràYdè ë*¶?ÌŸû¼³P'Kþ‚¸xt¸ÛPÏÒ“yÇ%öé:{ÏÎÃYÒX}–Ÿv’–dÚ€²yW=TfJöFë&L¶ìötÎç—~aeÈÊ~k¥¼ôj\¾æHAR«>ôÛÇúfl—æ(Aè ÊìDM;1Vâ#žÄÓž¸N¸Ë‹!(bA¬–p…–YÆYdŸå,³Ž‡Ûe–d’Ï"]lî^°á·²8Ç8 ãn¡††QšÃÜæn 㤶¥½Ë2õl¤²Ë8»e­²Âp¥¦p_pËmy@$¼-®Ù§ÊÙ0|ñ²Ï†ÌÞ7…³žŽ}f–³?·‰à’æ\_ØlÅv½Í\B|z¯Hî°hlG\´Åo€¸ ÛHúıÔ2åCúŸ« g´d.CI8`%-ÃE›ý“^žfxè  )„¦½vC^]«„e¯,¥^*lèö×â ú]3û6vÌÚNóÓwãú„ݨé½YÏ ë #  Ø.ήtßE—3D ÄêÀÑà½ÛÚ™¾ß»óbÉ›ÕÜP"ý®À=›’EÑÚ0ŽÒ@~btHŒ]ñüL,Z¯lÂùÂ{"Ÿ¨¨Þ:ŒüpÚša³Bå^ù»“íÛ°ÕÜž$98OB‘¾»ÄÑîê—Jã¯e×:ÓmîÏŠNYâ»ï¶tÞ¢Œö›÷ŸAêm´Qþßs/?[u•‡¶µXpûoú[ÿŒ±@´ÐÈ3!çÙé,áï>¶÷;€Zú; 3R³ß¾ fâAìþ TôÁÌŒN˜{žœ\Yõ»>kIÒõô¢g½çëb——¿^Âû…úËùA(s߸˔=·»ÓÐ- ï×–í´¹0fO@µ¼Ý¾ä4v·Ùcäúñʲgï°cI S½l¾íÉõïg|vórŒpÂÁ²{Øi™Ùh}áë†0Ÿ ž}üÑ D?ÈóÂT”údŒ©!?WcÌ#·cÖXBý¯ý6H:¦æµ% #žÒt%7vý‘™®%©èJl=¬×_j¾ Û#cc<‚Ævá™85FˆêYe–YÆYuýxL¬Fn’.`OAÁ÷xîèÕ'øMOCˆ•èeJ ÄDHa’¡ ìrPÄAÄLï‚Û*35–M³%–´Þ%6Ì[-¤Ûi°–Àd¦BHÄnÈe¼ã–¼ 唜˜›u-àçn÷N2`ÁXý+<« o” R8-Š3§±´ê³'®}¤Ÿï=j·áÆ)"‰)íuYÆ1èÑç¼o€0ÎëÐ!¨5¿m–l;Bß”ÿéYˆxïâ/ÔoY„Õ~LÈ×¾” ÇÔ*ñî=X¶ZNþÐ{Îþ¦Gþ"É|]2Į빞ÅÜXÜ73,zY00>‰Ù¤Tc/ãÞÈ{©÷ãÞƒ8[t{A“y¾¸TLOl_űqBwd \ê÷0¬U÷’ŸK§CÍEŽýLˆÜßoÏ~¬zxùþ=ÎþÅ4n¢rhŸ+ë÷°|ÿxóaä¿à‚(ßwó™G— À2>UQÝËøšˆË æ6p×h2÷ºÐ~‘ô@÷» êz&8ž–9Bèó½Ý6ËýÛÄØw¼|÷¦*êšë_V÷êñÉmþá0C·p– ¹ ýîjzúÛ=,“»é,4£|«ùºëñ–9¾Ówns °‰#;² Ó£³øÄ™qÙ;2F÷å ˆð'gl6ðÇl|àÌéuó¿h‰Ñ“!Æø^¥7àã¶ëYAû4eø`‹r3þ ˜g_pêüu?à=y·Ð@Ï_æ 3«=TE£Yû™ƒo¥«”>ˆéŒõÕßJ¡!‰3á£û€‡áîøŸïs¨G{`Qù€K€—ã²P2:Ü â1òSÖ OEãÃ~©æ}eð¶^»¯ß”t;ÝÔ¿#hçÿBR©ä>c´ÿg!-íÕöÞ{ÆÕŸŸOÚvó‚8x ÿbÈe׃âfMäü]Ï=áH„÷Ô™ ô ·žA&‹X³¯ÕøÞOžÆÓf†ýH 8ÔBNî–ðfròÛÆb^›byw"[Üã0¶:A@Üßµ¯ÇÈtøLèŸø°ízXñ÷Ÿ•‘}Ëq>Ãÿ`sÞ)ßÞ[VN?ˆc+{Áùà\“ǤHS1'÷'²þ¤%õâÛ=pÐgs}tÛ\ƒÜ#>c›}åÔëºG­?É-¸ÿ&#/hP/µ˜†Ÿ‰†^¶5ŠŒgÃGtÍs¢Ìl ”,ô öÀÏCÿÙ4èî2E"/¤FY9ÎÙõf²¤uyðÅÂÅöÿ½@´<¶’kPƒÙyÞ÷Uã©â_DÂ/yð,ã5²ÇŽà³Œ9BAu {CG輺5ñ¦ëã´½L±±´ ©nzÅЪÓWP=ŸÊƒʰµots¶=tÁ€‡¢L­b þýÂ~왯ê"0bÜtûX–DZZwÚÌÕéÒ?Åè—1{”Ü•-ÂÅ:õŸS¿zÄîðÀl±*çýëÎFB«ÿlñ2M¬‚ð&bö6~‹m©zö£êôZ/ltϯŽÅöUg­¿ ñƒì‡g^¾·Çî)€Õ£Œô¹‰m¹mï¾ÝwN°\íæòIW± µøüªîù¯}rÝQHN}5ƒ»Ûܛ΀?jå…} a“ɇ0k?ôx›pó"*—¯y×ÜeûÄ=m¾ãz†ï¸œ'<*@@k>âŸáx¬ÖÆ„S>l äþEsù½l?9ëʳÑ ¥}ÝI[,Zmüî0ìûÞ?P =»ÇÑ;`Ù—.ðÒž~ØÙ¸FzS*0‡÷7®;y>o8Ç]äkõB£©¶yÅç¡X„¸Å¿ôÝý÷cÆ}HQbÊhBÃ@oÝÓæº”$_¡æ:äš8…ÝùŽyÝ ëB VÝ{ޱq~Ã"`Î)C†gÒÚG ¬(  1 ÒúBXŽ=·Œì$$ i¤±ýi+f ɽýÁúõƒBUDvÖ[‹`ÅùY*Mˆj›@®Ã…ÖƒÑìÁŸ +LžST¡Gg g»y`ÒÓa-øõ¶Û6Ó¼’‘,[ÂÛI~e«×ü‹ù»¯âÌ÷غhõÁ¼„FhËqÖÝòBéÇ´‡qÕQ [0OØã ”Àa ÎC¥ë¦=:›¡hMÞAÒW7DÖ'OJΡã@Öx÷i" 4PwÜ7ö:»Åà0+Þ°·v%±G.òÇ¡€NñFá³C&ÉãÞÇ’;¦äÄŒ× ˜Þ'—.=КDsÂDE1†‘ºˆ$&ÍÊ@XC¡1_nÐë ÒÓ:HÖ}=Ã?©›ÕqÞŸY7LÖ;ïNCŒºáxË8Ë8Ë$³á—vH cƬà[»,sã’dªeü8ÎMÈß¹á8^Ž€&5éÀVòÆÈŽ[oÀËy A[rOk«ù±èàU•w‚5Or#/@DØTî0û”ÃÌO«îzý@X}6³Xà±×DÔÆ‘ô ´:ö!·¶¯Þ¸ôÀ&ðüÙºðϾˆd¯$ ç3AßN*Ù2‰Šã:<~{ :·¶uûÿ‰jtÙ7ü*$q„7S̃þ å ºdÃ÷ãDßdÏÓ÷þ]ÿDÓ»ãÄ]îÃ…îllDë¢c^ÿp06vÅäÌ9 <ð>;ÏÎ.Øxèâ>v°ÅIÂd„†KXû’2à„Ëg°¼q°Ý]ó¥ëd…ÞõT §xZF ,úD?W=Ù÷ ‚ ^(e ^9èñ¼›,"öõû8h%ø OïÌóŽ¿P/·nžCãÿ™¯ó8Ët;ºžX¹?i»˜ÍïÏÆÊÂzk‘7­°kÎŽqa¨ñz{Š…']²«Ë|wzõºgÛâM0B†ÎyItÅw/SÞÒVlT ýæ³ YÄôý«z·½Hú‘öß=}"¼‰Ý-§ÊAüûÙÇ!‡5}+¨3‚N2S0Aœežàã,³Œç»l°Ë8È>ÙÆ6kÆYd’ugÇ øe‘±ƒ€[ƒV0é±ç,gxÞ®øîDíáÙ_Ž9¼–ü'ÈLG˜†3nÿ+Ÿ˯†ñ¼éÆwËòÆÎãâq¯-·vòƼ·ôç 98.‹z%" Ù#3µÚC£{:‚Íæ¢ì´ ´^§cI ïÁ¹ƒ´|ØkSè½O õ»&?ê¯á!1~¯-ñÔBf铟E0î0ö‹!¨ó»\šJã§{ñ‘»í³Oyu¦.‡w…ùÙúO@û®DM“ML{hYü*ù•»¨ZZarª\{ð“ü:ã> bð‰Ë"êÞ¾9–qŸ²Ë,ƒŒ“œ³ŒøgÇ«,ã8C,»ѶY$ì½™ÙÂsœ¤z9Îsƒ&l9ÎùW€8Ë8 Ž‚Ìx-gá§>ãâïñ¤örÝï —>v³v¸I¬K~×VaÀÚ&ÖxÈàVX xg•·7!ž¦3º*sJä‡îsºý‹ioÃ6'®">H‰…Îå²”µ¤Z±Þt„Bú1>€x-¼©!£¿®Ò"=æ3úîæŒ8¿‡á‘"ŒÀøë¼ñt?DvF¡´c4RË yög¸aîî+{YRäŸY?ôk©k¡oßðÄ_Auõ#L¸)ô‰Æq‘Ó/ñ–¶BŒªvë.™ðË9,`³¿‡{ÆIdæu ,²Îsáœä–Ye–YÆ@pØ6qœ%ˆqšÙe–s„¿GÇvs¼ãd&~,.¸ ² `€€Ï‚œ>yw”äme¶Ã‘ã¢ïâDvÈÌBl$¼+ã9ï²Ï‡‡‡mxõ·¾:–0¹ö°àÔx ÐÉïݶx³°ic'9ÆX¶AÆYga¶Yg9c%œgÇ$dø=¶?Žì"FÎzðpF-p@ÀÉdEAAudïÁfÙ^5àó­Ô¶ÛÁÀ³Æ¾>m“Á;ÆYÜ6’Ú’ñݼAµœ¶ïœƒ–jï'в¢(ÖA#Ì=¿È o™³û¾òîôpAÕÑâ˜ÑüÂF?H˜YíŸ)xè–Xï¢aô˜QŒãnÁáaäƒi°Ö¹WñÞâ!Ó·¸¡F©à"½ôuÃ2>É%<üF=k,ûº”nØêÛðDZÝc½»2ýðInWZ}2ý÷ë>¤7= Ì!á‹6´>_voQªÇ¾Øóã"Õ×ÚÿåÓ6j#ö\1ðhFAÿkýìú.¬ uëS·¯=a‚y¿êÞÿ¸aÐK¹¬e—|dœdIe–Ye–Yd–YÆYgÎAÃ,²Ë8É,ã,8K9Ë$å/XÙÆq‹8ìvφXÙœ6BØÁ ‰„zåíågáþÃoY eçë8n–ppÚ7¨a~[kÆp©wÏRòJg\6¹yÉ?%ÍowÙåýGƒþxW£<²<û›gêÂß·b&ìöbrÞŽÿÍà~­°ã'×ÚÝR´Ÿ ©–®‹æù"~øƒ)ØtzNÙÀkô£+a¿“6Ù—×Þ^û)–Ç>^Ø"†Ýþã¶ÇËw%p¾$ xõ]\º"Gû½: ðq×aÄ?ɶäÏÚC@vgò@:þãAMw~÷öÎÚЙÖÑö&9Úê®?C“\­ë¿É*¨ñ¯æÂØ,î>‡sr[_Ñ9NÊÑÀ*É¢æ5'Ô €7]t2Û§¼þ—Óÿ»ÄÀFþ:›!`×·²:–Jû磢<:ÞG/#MŸƒLX96y¿O´/cÃçÝó“Ôñ—¡æ«zó¾óõ¿õßïÌôû½%±lgÊר‰,²U–<$²Åµ–YeŸ,9É,±’Ë l² 8NI,á9K8I>ðrÎ^3œà ˆ‚‚Neád»•·ãIuãaãglãÔnðq­¿mà®.Æðí§o~¼†e÷¨1Hý¢nó¼#–è"ý p3„–÷:懾‚øõÏülL1ñ)4wYÂõÿ<¼ÀOdäÝŸÑ{½ö¿'?\ð,2‘Aû—!4Fà "p˜ì{=ÄpÀ€…–}¡)SÑ6êE!ÄMô°a { ‚ÈgМqHØqkg%–Ieœå–HYîÎ÷ 8FË$’I$dá,ã9K,,“œ°³Œã.‹:l²Œã8ï`‚T"6Î5†Î™xyÃë8êÛ¶ÇŒàx-µñ *ñ¶ÛƶÚËÆÿ‰èÛ„’êO’3‰0N—ëòÈ1¯ ?­»é|zÛ`ÿ »B[dÙbð&IYÆF’YÎsÕœdY=gYÆAe–Yg 3†IÂ$œ6YdÉ$–wga’Y%îË,³Œ“œ“€`ˆ ‚6ƒŒvË8=rðÞä´8φù¶[¿ƒ…ÖD¡ÑÈ+%î6ÎâÈY͵ø € ظ~#n[k+,ÙûÇ׿ÞA ü–j£ø™'‚þ²zc!ð(îYæc³×Á ‡XÕ^Ùhô¢ªßÅ„ –gÂqÑ7rq–YÆ#\†ÿ|$ ÀÍ»Â7B3ЄN@{YMçÓÖ-“¶q(:y"B×’ÒXãËÜð âó¼!ÜI'8l°Xgcd Œ’ÎK†YÆYeœuÆ@Yv¬v «6N2Éž÷2I!dð’YÖIaðFË'ÈçÁ'À,²È º@qŠ»;Ã#3ã†yëå¼íÞÏóÛ,ëŽÉcƒBáÈCÁœ‡”zá9ÒÙ±„p!—v[3,²ü5Ø Áà#xŒàá;€°Í±Ž—Ÿ¦ÁsŒ°;²Ã\g|ÀoÁøc··Œã;ÖÈàÍîÆÅœãœ–9e„Yge™3%ÖØ}p„ØYd“%’I$tÊ®¼å’2A6Ya²p@A\^Ž1IááÍ™yw–^©g¶[~.|zÞ v8o=¯dÛ<xFÈõeÄ¡Á°q²ðäæJÝËÑÀñ·viÂÌòóœidÚÅ–? á²8ÎrÈáó¶ð3697Œ³' †X]Y'6ig9‘d#¶XÝ­—{ÆH}E¶í°ñ“Zì²’³¬P›;µ·I {ˆ^¶’<6ÎÉÇ\zÛygaäóÇv6YÉÆq‚ÊoF¬…zøaD—»,Îpøãeœù´ŽK8Îì²2“Ç|wÇ “á¯9a2IòïŸS9c¶Yd–Y%’I%‰°YœgY7S»m¬¶È¤ÊË/!;Â{ã ÎzgRóÔvq°0ÏcÈ:’$w»c‘gœ™ÀEŽqç¬,³Œ³œå,±°Ý³€ŽÎ2Î2³åí»ƒàñ…Y;|Ÿ ã$8y~Lqœ§$–pœe“dpAsÖ˶wg9gë…é–VIÉÉ^3áÎ{àœ·¨µ‘É€9À³6×’Æfߓà †X§æîN­OdgÀq¦M¬±À^tOð–ñ±·¾ àŽ3³x ã>&-…‘"ͱ“Ž®¸>%³âdñ–7rgÇdag8|3‡-’I8îß‚1díp–Y/ ’Ie„’AeAÝ'ÌÖn£âÙÞÉÊ1á ß –uÇSÊqœ„!^øØ#6=êÆ­6’4‚±˜ðÖgͳN@ÈÂ<§|oÃ"NC¸ë>!cwa¼÷È<'œäl„Y‡'AðÆÎpd‚x8Ë ¯†YÂq†s›Üœul'gÅNnøß‡Y|pˆeeº™³†snƒ,¶xg€dËzáázåäÉxÎ ¶ÙorÇ]N[f7[Ƽ"šym¶Øq²ˆ©n¸ò|}™ÂL‘’«owClüsã–Dq„ÆpdFü:‚,ºßÖœe†IšÙAgñË,àà‚ ,³8N2Îç.ì‚I:‚N3ü3’wŒ’ÉóaœäÃcd›d–2YdYd¸ëàDñÕŒwÃÂww$ŒÌ—©á'wŒã-ã.ƒá¼›ÁuŒ˜ë†9ÈÛðF°› *Ùö›,ÈÙde€õ:~÷Çm·O¹cΙó$É2Ç–¥ƒßÃyîÆî=ä'zƒ¸=FqÞpl]AËÆIg9gÆuÆYg8eœe–Id–q–YÁÆYÃÎL¼'Ç>Âag8lÙɰqœ;®ñ¿¸ËN:’ ™ðpMYgYÉ,±žÆ÷?³-ãm—’Ç'rXàá.ÃŒ½ØÈ]dç €ùZÝ.ñ¼åŸv&Ïðp–ig(FÁ1ƒ¸ñœd9Ë»ÆD AgpqðóG!gÆXÉeeœçÇ6Ìà,²Ë,á9N1Ë ³Œ³Œá>)ÎIc;7|ãÎYß]ÙÀuß ˆzø/|cÉlšGe’Ìððøxu™$ÙÙåx`‘áxÉË,”YÏvq¶°vk„d[ËÛñ  RVÛg“¢ý0Mko ò³x›½œ÷9×ølä $±9È,±‹‰'s€l²N4ƒ”ƒ€…°<Hq‘ðÉ à,°² ,² °²,îÆÃxË"Ë,²>¶w'e’YÂYc6Yeœt¶w'û8.㜰°úຽ¶q–93Æ)6wc—hYÃ;²ÆL¼%“'²d8ï†÷Âs׎“Þ«?!Æ"Î5²Î¬³‚Þú´Y4Ûn®®­Ÿ‡ÎX~÷Ë]˜Î fÚðÇ9ùfs‘+gÇŽ3‚êË,7âw‘? ÝáŒøu¼†Áe„pñœ'cÁÎqü²Â÷ÆAœ¤Ÿ,’O†dë–ψdœgp˜ÉÇ©Œç;Ž2ï8:eµøgÎH$ç¦I7•dï—Ž®²×‚Þ HË ç¾Æ64߆¼l¯$¶Ëpô_°Æ|q×dœ7|$q†Y–¼agÁ«º€²NçÔ1<±Ã¶wdraœguae†È/Ï ’”á½Í—Vä–IaîΑ²„°ø'Ã'ov<ã>=çíxeg•ç—$ï„™ñ3<9$¼2¤ó¼e–p—Sðï|ƒß®sÝ­•{C8Îp—Œ»8ËI¶xÜe•ž ž# ›Æü …à‚88C¾€[‹,°ƒá¼ˆIdp…YÀqœ–qœŸ îÅà.î윲L³œã,³Œ‘²É,à,á,á$ø¶2Xíݼo|u9Ξ$åã“K֙㌺Ëd†Q·eá庒\á’NBxÂØŸIÅ-á ÉÛ¼–Ù¶eãLåàx<ÐÉηlNvÎ ƒ¢N5xxÍx ½’³·1Òë‚Î0ŽcÁ»Ÿ 3Œr ä,ã8À‹:²À‚ BîË1³Œ³Œ8êË8wœç8Ë8ÆK89KÛÊqŒŒ Ÿ»øwgNÞ¸Î3Îz$å•㫽žz™œÛ'Ãðuäs†ežC«Y^7¢l6s—œ8γ <Œœw`Ÿê35°ËÆõÈÒ}²ó¿ ãÐÇ|°Î 8H²ë„Î=G AÈYÈpIÔ[erZÄYÆA–@l–YÆ9ee’A;ÎY–XÉÆ|s„’Ë,ø' #2pœ?#xë†Â8ÃfÛ –-ïà¿ê^;Ë8vîÜ”·Ì ÌÉ$ïÁ>“'&Yþ,øe“0ÍÖÏwvÝ]|OØØKÆÏžê¼|–q“;dœgð¼–AÀYÀ¼aDAÀD¿·àXò0|K8ÂÃŒ³>YÆYggÂXÙœäŸ6ɲIá1á"΄äÈááçq°ÛÔü\™bÖß‚–[ØN¶§=óß ËÃ#Ãi$ÉÁ?–÷gÃn¬ÓŸ€OÃY»à´Â_̦ɳᓜ9‘ŽÝpFÆ'8ë“ËÇ[Àd¿á–Æ[cÇ®¢7ˆ88ê#Ä猳¾3Œä–|3Žþ9òyË'œ²vyËÜðäœ#°tÙg9ÂOÎü7† xxÙÛ´²Û®rË[å[aëŒ3xfg/s=°™6p–u%ÔŸ'6é'a°“á˜ïpœõ‘i—¸MO†‘±/sxï…ø)u·«½ÄÆç ç#À±Êe‘1u°É ÀAÁÀwppÈÈ8>ÁÎpü3€øgò—¿ŠY?ø6I$Ž|‘å:žï×97¯ƒðO›Ç»¹Ó9w—-`žŒ$±'ᇒÃd$Ë,º‰áNRÎñ뇌á¶s>9,f<1¤FŠz>-×—"_‡F؃%ï[,²  V<7voÕœwðbλºÎK"È,΋ƒ€ç9 ,ƒ9÷d îσeœ6ªp’|2ΰã,á8FI²Î$åø&kÊñ¼,—IÀõþÆIcc­6~È]ñÕœå’sœä’YñÏ’Iðl8NrI$²Nlw|6òÄü7^ŽÎ÷ßÈe›:ãxYÙ8RÃ9Çb—sœ{˜eÙ0ç øpOÄ åç8L‡‚íxFYð†Øåà’O ܇:E뇻9Î0Û,8çà‹½½ÙEœcÈFg¼À†Að> ÙËÆ|3–xnì²NË>O)dð–q“#%“›Ë'ï;3agÇyw—vdfN6òÝo.ÝoÍÁ— êv:`Kl™²l’N3œì›¾;°D FÈBIWŒåÞIîvÔ–8ÖÈGl2s‡cá©d;’7\lÞÁÆA‹u£e¼u„m u𠈄à²ȳ“8^HωœoÈø'sâÇâ¿ÀãÓ9ÉyܲO–L’LíÓÂI<ÅãRÖ=Ͷ¡løµÌãm”½Èllñ¼b¸<3Æòìîòä£uÎ<÷ÆÙ62!Æ/ÇV1»,ó±…½…eÉ%ø3™–<¶F˜»8ñÕÞñ¼ VYÆYagÈøovsŸ,²Iáá,²Ns²É œáó'Á<Ìüsޏse†g,xy3eÑžàz‰áÛÔŸs¶adlã ƒÞñ¶æg· $Îs¹ÞK¥äɽ7aÂsñÀáÖ»p;µƒ’öü2 ¹,‡‡àø›¼¶D/là ³]ã¨8Vs9geœÏ¯†YðË8Ë8Î]±²Î2xI$Îrl9ÎfyL’d’mã9x~ AiÆ·\d¹ÂÛÁ2<«6÷2püµÜY˾I5Þ71ËÜÎ]–œ'ÀµàÒî82[¨‡ã¶K¸N^f݂ϙß$–AÏrÙn¸Î àÞ È8,1œ–E„YddCßùœ°Yò~GÏ> œ7ŽrëŽóާ²“„áã;àÌŒÎLå©ðs8îÖYMãnçåï…á·ƒç$ŸÉ(à™_ƒÎ027YÆr ä‚le·—,ã %!l“ƒ/\j<玸õ—VÇÀ]É—Vz„8F6 €9cÍ…‘z‰ðCÉñ,cÄžÎLÛb²3œˆèೌã8dóŸ/|¼¼g9òsŒø¥œ¼»3;ÃæI'$žYžW…’ή¸ôr][Ã’ÎÙ ²rsÅÞl’H˜Íœ9êIìÁ)'d"Û&Î:ÏŽp–nsîî2öðä@ä­¥¥±múG6~ G†õos±Áu”2w³]—|#V^¸x×lã ä‚#‚²ಠˆ€‚ 882ÐäøŸ,ç,ç>òl²É³‚ɳŒ“œ9Û,½O‚pÎpÙ3³ üxYù<°O,ñ³ÏIÍ”——„xvÌá8lëlg„'² l²5}Yd%l§‚Å’Î;½æE‘07Á¼i/À–8%ÛäŒpOæÁXì±pq‡„ø‡Ç9˼²Îåá9lã,ã8̳ä–psœiÆ^¸g}s—sælÍêfxY^rÉß‚ñœwmß.¤ ¦#c;Ïv;{˜Þs>Fð¼n=KÕœ¼!2YóëüR}|»ðÉóg 3’ g.M“s¡8œ;Ã,³l²ñîÙçn¥º·®·xÎ5ãgÀàvfÖ$‹»#6NøIá ¾2çf$êÂ^‰]‘ѲVxe°Ïƒ¡tËȦŽDZ[~†Þî¹l†mŽIE¿†YŒqœgÇ1ã #á–A YuÀA{ˆŽødAŸî¢ ã.·Œá:™†p’YÆIðÎ2ÎRK×=üÓ¾2g—…ž\ž,åz匱Þ’[xS“à·V“¼³wNï‡ÏHHr]nÎYg˯V6ñ‡—ckâÎuN0œ—©@æq¼Å áç"n¯sÞŒDlÏ€O$x³†ïbx7tˆ˜x3‚Ã,ˆóÇYÈÎAÀGË8,äà9ÎŽ3ޏFÈžðÎS”³Œá&dø? žÙã™$že´”ylœãd~/ÊKÆpÝM¡w%®Ëw<+—SÃgÁ8Âè% á,’ï„,ÂxG8vpº»äŒZKýPí`ÍꌱºÓx'2ÖÚfvwyq9ï.ï}p|‚ÎÂÃ#r 8 ƒŒ0±È,ç:½ ³€²à#Œà,ƒ“€ã,㨎q‘ÀpÇe¿7àï+òmÙ'f{$œºœœžËá²lã'²÷%–ØÊLàrüžØ°Ï†âN¢ZÈNÏ]óžoPwsÌFÆÙYps—YgÇ ‹;øÄà8>Ye–Gld¤^ìx3‚È8,ùgYòÎ2ÎÉ,½p‘ÎHsŸ-å—”îΡÊ?,“Žæw‡‡â˜Êóêu`åóÇ®ÐdlYðpÛo„Ý…ŒùN¢=彟 Ö7BG“gx'„àÞ̱‘Û¡à8fË%™ðd¥¼ ýMÔ¾—ÉxÞ¹½Ï¾3’9Þ7qFð¶pI88µ&¼$ÉÏY{™¯ƒî8^7…ŽÎ xÎ=œ`¶AÆ6reAðÎ3ä8ïlã,3>gÃ8õÆIÎ[Â|Þ3åœ3¼¦IÃÃ’¼g(ÎLÌËÔäŒÎìÝL’OÁå^3‡…CF¦^¦Æ2HPål› Û;²V8链¸ÒιIYÙ®ù~lÌÉÜë)'+=œ3ÃòÏ=ÞâxɇE“)'Ë—|8·¹^> <0q½àOÚÚJoËpñ†ÝhY½™¶²Ìg8Î0Îðæww°M©„nÎÇm"d¶ÎqxÌãV8`ŒÜäF ã;qŒq–wÃdgm'!ó™È|†xy,ø$dðürbõç/Å›¹á9YIež^œ ™·àù†ÁLÄñÞZñ¸ðó“Ï[)Äð°<’d êı“¹`‡m‘n¸I%ʼn  K,Âë0³]d²F\#s"Nê{8Ôµ•‡{ºˆ:l§Ôºpæ›zxHbl?tz9é»>¸>nððY'=òïÁøç Îu?'‡„žã0L—BfI6Þw–dãÞä—PÏ’K †Rv¶]ðs—‹gŒ-ïÍ‘àÛ[{áØPàe^SdfËYݡݦb;–JÛÇ›XRÎÝ'…†g'+Á)²Æü݈²Î–qÕŸ àà ŽÂ ƒK"8G'ÏÊñrü‘³üNsâü:›Ô¼<,ÎK‡g~ m²Û3ËÃÆ6IOÁ¶x~9eé²ÉÞ6ÎÛfG”2N¶n²Ë¬ž n³N1° lY½Ç™²É 㹜ƒ¹,ãIF^ó…v{÷<¡wÆ»Îé÷<7dü'uçMŽHȳ ã 냶Èñ›ò3ޏ/\̃à< Ç9ÁÆü½?øo Ïr’rÇÉÙø³“2̤޸ï'„¼LœlM²L¼©zËß/ÉÎ<ÆdÊm˜YfˆžÞ2ïݰJp¶jpðd¶9ÎGHHÚM–DÛ åÖD¦ë-±šGemΫyîc„l´ì<­µ~'Á‘ÙÀ‘άA`Çp^£vHƒb½ñ°ÇñÖÈÞIx<üðë>OÃnùxf_ƒ3<38,ÌððððÏ/Åä™Ü—„³†ËÝÔxïwƒ•,ìgy×8÷3œ¢—¬ø%’Ø6¼m¥¯ò¦Ï»u¹¬ç4xgvéÀ›7{±¶ûÈcµí“‚c8-àgƒÕüJíì²<ÚÁ;Û­èˆàñüòrr^ö7b3Ÿ_>^¸Îwà»à·¾KÃxyë”áááÏ‹œ³<ªò“2Ì›ÃhN|=ó¿•ÞŒø&ñ¶{8ïK>ÆpųÃ6 ádà;²ÆI}e€Û!’MËÜñ·\lNî’qÖ£²Òá <õ>Cð'9Ó‚^å&£×Bs98!Ûa‹:ˆØã&2 x ¸àÞãHà×ä1ÁþûÿÀƒÉ¿á¼9ǦfÙá·5™áfe™‘™eáf×êxoE¶?Ý“Ÿ\:[Çs»dfMÉÆÁ—k˶91k—v“¹#l·v<G ppªé`Ýñ½YÒÎóñß ¥ôð\_P-Ó– 3ž¬¶K)“ɶCÃÎw]‘ðõΗ»oQ¿âp< /;×Ãcàqë–Þá¿7Ž¥ŒÎ6/SÊðËÃ)lÎÛ{åóz~/Áá,áÎ1–z%–÷ YÎ9À–Úìçļ;zg<²;cœcŸ ’Ëmr`—¾0ø‡LŒ!<˜YËÙ¶Dç[¥m[í²c<Œ4¼Î¼…¼w’l’Í ™ÁË»«°àŽB,²#‚ — Ž7¾ ‚& ޾ #`†9#-ï¾Dä—ᄦaü_ŽÉ%Ÿâù·ŽæN‡–~ æY6Ï,åÝ“$Úü2Ñøw¹{aç#ŽùVÛÕ„ärÌIf<&²‰[xÌxëfé›YÑÞsXMíxx\p•½ü‡á²„º¹/q‘ß\8'ܤ¥ÕßÁmÑ–ÿr•›¤:ä¶"ÝÈ xѵŒ<|u±ÁlDpxˆmøkÀÛÇ\-ç~,<Ÿ"ÛxØÇ’xSàôp¿~ øøy&fõ;Êq³32s³dïñÐKËÙgMï¾;áÛn²Èã_ƒ}ïjÙxnç„°ÈÝødŒr‡‚õe³$œ8ï/-«È’KÕ½rºØZi(B-ßô1ÏÀ¸3á±Æ÷oRß݆;þ/;<¹3Êp²²÷3u<3.â-‹c€ˆnàÈç~coÁàÿ -½ñ¯ ¤Z’݇Ãxß““NOÁx^ç…á–YfN7Ž·‡—x8d²~¶ò69b2G p ó¼cfX6wa ·{7LœêØçO‘eˆïeÖÉ '.âL,»Ù Î=ÛÜÄ<—xNÛ®ÝÜ0¶<;zütäéŸ'ßz°-´Hmîì¹ÈêY oÇÙuœÃim°Ûo;oÀñ¼ïqÉoéÛÀå°‘þ;ÂòÏ‹xë>+lÏÃe—»x[©Ý—…ååg> ¯Êñ¯¼f³oÃ8\yÞŽu:Y»Ç–Õ1Ù·„ÖÌ2lá$n¹q±[½ÎØ3…Û}pØäy²sgÌ^R9Æ)*0Þs*‰Ël9îÛ,†^¡VØ£0ÌG[ ½ÞåÂ7²Ü!…çxX‡¨àXà¶·!K~]ð6ð6Ê?"3‚y8Þ=[ñ<òñÖ˯-žVY’Y™yfÙóÃm¼³Â̼u¼oi6>yÒÉ2G–Îrv måÈøä|à gƒ%³¾¶ÇxÁ˜ä~NXÉvp·D³‡¦ Ä+y­Ÿáñ{-íò/,rõ#ƒ—¯‚a×Ìñ'ƒÜ7³“Ïø÷ÆÇŽàO™Ï¾7‚<|=üƒãüßÁóÃò~ áóg‡Ï/‚~¹Üü<<<ìÌÞy8f÷9cþ ®| åø‹Óz´Øøé{·«ßgSão<3œ>8|O‚cÄø¶ÿÙfwupd-2.0.10/contrib/qubes/doc/img/qubes_manager.png000066400000000000000000006330331501337203100223410ustar00rootroot00000000000000‰PNG  IHDR_÷©¢JsBITÛáOà IDATxœìw|\Õ±øçœ[¶J«UYõnÉ–dYîM6¶1Õ¦cLL ©’_^ $@Þäï%!ÉK^’ÇK#ô ãŽ{·\´ê½­VÛËm¿?Žu½–Õ-c óýø#ß={eïîÌ™™Czð¡ï}ÿ{-m-š¦i Q „ @ %„J@0ðÚK¯Ývçm€ ‚Äðâó/ªŠú…;¾ÀQÞxí`0”£F£1%%¥tjibbâ„È’déÀÞÍMÍ’,%%%Íž;[ùàƒÇŸ9{fIiÉ€½šššN?éîu«š_8©pò”É„áe½ðì š¦Ý´æ&³Ù<Uõó°âúv»:::6nضÛu7\7Ž1AO†ž}á;÷§µ£•½¤„RJ™iÀþ§„ÆZãîO`ð/#‹Ù2 ÿ}óç¶÷ö°þ„0„•$°ŒHøµêZÖ?9)YQ•¾¾¾éÓßà(À‹!ÿg÷n{4ú—,ÑùáζH„ €öoL ?¸â äá­[m‘È„ü©Š|fà !ÇQŽjšF€hš€Šª€Ål<ÏSJQ¸ØÚ"‚|º ”‚‚ pÇ^RJ]¶(+;«µµu÷Ç»[[Z—^¾4+'ëüeíÚ¹«¾¶~Á¢iéiëÿ¹~Ó‡›Vß²Z4ˆÀs<¥”ã¸êãÇŽï۳ϖ`»þÆëƒ°kÇ®ƒûz=ÞÊË*G<.MÓAß“Ÿ¨qÖ,\´jµ¬…£•DA>UPJyžçy(PB‰ªªhÑÀL}BH¬upnæÐ]Cõõ…WUà+_ÿÊÀþÇs\æÊë í·yŽ£„De9ç†ÀûÑF^æôþ<Ï•0}8€PB€R@ø[¶Ð~¯=Û;@'öL¢Âˆ:ì§P(ò™á´w€ã8ä…g^`­š¦±¯Ýó5J)æ(¥¿çA΂£h "3})GÙ‹ÅR\\¬*êÎ;wïÚ——G( ø{÷ìíèè%9)9i欙©i©ð·?ÿíÌ€g2›DQôz½™™¹dÙŽãþ@SC“ ¥%¥@ /?ÏYí¬­©V1 8ž£”òû ŽF¢‡¦”.¨\œ’ ‹—,~åÅWêëê§–O·Å?ÿÌópç—îäy~÷ÎÝÇ«Ž—W”Ïž3ú½ÿxõ BAaÁü…óÙ1}{öu´wȲœ’š2Áüøøø§…“ÉÔXß8Á|Y’[[Z-VK(¢Üéo“—ž) €Édr¤:æÍŸg±Zàé¿<­q<—žž¾ôò¥‚0Ü..—kÇÖ½½½úŽSJ§,X¸`mc¥˜-æ[×Ý:q7‚ È¥ û"ã8ŽYøŠ¢¼öÒkpëí·ZÌæÿµô?í ”¹ †ëO7ÆEQØŸã8ŽcΫZßþ—¢i97®:ÝŸ8.ª÷õÐþ@á8€-[„<²c‡=ùÁòå@){WP”Én÷—Ž1ÊrH¸ürxòÃEEy}ʔ͹¹WÔ×ßX]½7#ã™òò ŸïÁ;]&Ó M{èã“C¡BB€(þjÞ¼³yÕ©SËúŒÆ·Š‹‰‰QJó<ž5'N8‚AøÎUWiýÁ}¶pøg[¶Ä¶ŠRÜÛËÔ€¡Ñ…òt? Ÿ+xö‰å9žò¥¯}‰¡”9óa&@8Ž#„ðÞc‚ gA!„p<§Ç°—ìYPX°ëã]áPØÝçNLJ|ÿÝ÷½^ïµ×]o‹ÿ×›ÿÚðÞ†U7¯²'ÚYœYyÙŒ™3^}éÕ€?Ÿýªë?xƦºÚº’²ÇV«•yˆ‹#„xú¨[[[UE%„deg1õâmñv»Ýívwtt$&'2¹¼À³_iÌYÌF`o-X´`çöÕ§ªM&ÓœùsEÙðÞ¯Ç{ÍÊk¬Vë?ßøç{ëß»åÖ[„°óPV^¶ïþgM$€²©eû÷î§ô´’w|éPUµ±¡q㆒$­¼a¥.ºxrñüÊù¼÷AkK«³Ú9mú´¡v‘eyã†Á`©ô¯7ÿ‰DØ £-“’—ŸwÙ²ËDçWAÎÀ¾^`®W^x…=3_yá•uw®³ÅÛ(¡±ÖAlÿ º³`ÐþC¥¹ ¼0°?¥åºß]ŸvÝõ ûÀ¿q£Àq¥zŽã€À逵þ DÓ4Ž–J„ðB¿-=¯­mÍ©Sš1ã¸Ã±3;ûªúz¹ÿûHXd!,ô`a[[•Ãq -í‚“II2Ïß~ìXú9®è<*­¨pY,«Oºª¡A¢ô¿çÌé6›¿½R0ø üvîÜß¾Ý$ˬEgç]ÇŽ™$Ia~kë-'OþOŒz#ÂŽAÆÊ騽óôŸŸf­úŒÊ}ß¹8ÊQJE?!‚œ›c9‘ã9èO¶ä9ž=0ù8žý:‘%¹³­Óï÷;Žü¼|˜4iÒ±£Çœ§œ‹/b}¦NoK°õt÷Lž<Ùj¶:ŽÎŽÎ€? ò¢•ج™E+„Ãaö’=¥Nˆ}PË’ÌægL“Þh2›<O48É5ðæÝ ”r”c#°ãšV1Íj¶N«˜V[S[SSS¹¨²¾©Þçõ¥¥§±£ÈÍË­qÖ454 ¨wÀÎCNNNc}ãÉã'%YÊÈÌHKK‹¡3¹xòÖÍ[;;:‰FA`¢+¦WXÍÖÌÌLý µKg{g8NNN.È/8UÁ¤ £-“2gΫÙ:q·‚ Èg–ªÆ¬ƒžy!v†ÿåç_¾÷Û÷Bb­ƒØþ± ºk`˜þ_úê—ôÿãÿ‘çùý)å9ŽRÒûþ{)+Vêúø?üPà8B G©ÞŸy˜[œ¹ôØÚoç!§=„!Ë›šâ%©¸¯¯6)Ée6³ƒÓÞMã86@óÁßYUUg·¿ST„LëêZÚÒ2è™dƒÿiÆŒ.«õæS§®©¯€£))ÝVk¡Û]ÞÓå=={32¥¥-niaý¯¯«‹“¤Ø®llŒ—¤¢¾¾š~õF„ïWAÆÏÜ{‚  ºOßàxŽB9Jáîâé‰ òiätì€Àö°*IeÌ?ħÖ8kG{!Äl1³·¬qVBH0äŽõaoñ<¯wcÛ„Nà¬ñVBˆÛ]Ç^²§´.—a¶˜ !Š¢=ý# $I"„X¬].S~ÀºÚœÀ1Uá0áH  „tuv=õǧô3àóû|Aè13*6´*fTœA)'pš¦íܱ³®¦.(ŠÂÆ GÂF³1V4/ð#î‡ !fëék±ZôFÛÓRâ­øÕ† 2€Ósû‚À ûï²\¶Xë ¶ÿSŸ9ýb°þ„Aö'„øAúó%4"Ëú7øÂ¡Ä¸8JNW)dýc½\ÿ?v\L¼=§i|¿ù ‚ªB¥‚VÐ;Àõ;Ø„¼]’–´´ü«¨®­¯g»ÓÓŸš1ƒ þÍætt°Á»ãâÌ’´°½uë3› !u‰‰÷¬Xqú„ôX,z Cb$¢Oû³{4*ˆ1ê8Æ ããô/žò„’oÜû§þð”þÞw¾÷=ˆÍJ]DEA>…°)h ýÕø^`Ì–ÆJ©ÅbÉHψ†£”ÒH$ÂÞŠF¢”R›Í¦Ïዼȶõôù|ÒRÓ8Ž …B<å™­N)MMMe£±ª„<ÇÇ>¨óróDQ”e¹§«'++ €§ÏC)ÍËË3&—çxžãYl‚>;.9* œ GevÁhO¤”fdfܼææáO mêÔ©{víá~rñ䯯FöëMà„S§N9t$%%å¶Ûo âþûªª2é§Ã1xQà6wÄqÜ0»Øl6J)‹†ÐO,;iÃh+e"oAKŸÓ^^ ”~ûþo{}^ý©n49ž`èý#Ñ(ƒ èHv ÚŸRÊ ¼žŒ ûØ·êYý)8>"˹«Ïzžç®¾¹åÍ7¶;ÀúÇzôº§ãÎÞfÞM/^H˜Ožp‚¦1ï¡”SU¿Á ¿Ýfó‚ªÊ÷JiéOvìà4‹aÕ£©lnÞ‘ýë¹sÞ¹Ó:‚ABH±Ëõ£]»œ(ݰçk‰UoLƒ Èè韖á9¶xá}ß½ï÷¿ý=Üÿýûõè }èbk‹ òé‚ÅP޲'äé—”ÊŠÜØÐ¸kç.Žã®¸ê Aò òì ®Wkkk‚=¡¶¦–㸊,D (OÙO®ÓñýóùÇq<—`O˜R2å䉓'OžÌÎÉnhh0lwè`{麙yóÂE ·mÙ¶}Ûö•ׯMãeeg€ÙbC---v»½±¡qÀÀ±cÇ“;F))+áx.¿0?11±£½£¾¾>7/·ÏÝwäð‘Ò²ÒôŒôsO Çq¢Aüƽß`,‰”p„ã9MÓ!<ÏŽ9r„½äxnÀÙ8½ n—üü|«ÕÚëêmoo·X,ì@ØIFÛX)ŸÀ}‚ r qÚ:à8BÉ~÷‡ï~ÿß`èýŸÿûóƒøƒ0hBH8>W:K:8«?!DZ À·a%Är啳úfiófÚÿeÌÿ ý¶}¿w@ëßÖú\Ì6¯—' <€U–ã"ŸÁp"))=<âpèoi„ü¹¢"Âó_?räHJÊîôô7‹‹o=uª²½½òwÎ=ü[N tGfæ“sæüx×®==é~¿31ñPZZyww»Õº1'gqkkqo¯®Ì€¤¾å]‡q ‚ £á´w€rgßûþ÷bW"Ñ?ÿ¸f‚ Șí*ð«Ì^nxoÏóf³9??öÜÙiii ðÂÚ[×nÛ²íƒ÷?$)Å‘rÝ¢ëÒRÓ ßg+èÏ[8ʱG4{ü^}õÕƒáãíK’”–ž¶lÙ2«åtÎ<ë9`͘7o^RbÒ¾}û^|þE)*ÀôÓ—.[Ê´½æšk6}´é½£ G96Sé䉓'OœEqÚ´i‹/æy^à…µ_X»më¶­›·F¢‘ä¤äŠéYYYôìºÓì< Ї%Jp„x¡¼¼¼½­Ýétþéþtæ P^/RÅöÕk%³‹ÉdZ}óê 6¼ñú¬h޾Ë0ÚÆJ™à{Aä‡ô©¥„~ûþoŸ•-0˜u ÷?7 ÁjÊ Úÿ¹§Ÿ;Wúàýû ûÞŸÕ6o2.¿´~oë? ³€ÆdИ)}Ý@!ÌAÀ¾½úíê{Ž{¦´ô¿fφ~G«;ðÏÂÂꤤiÝÝË››çvvžLJzgÒ¤9]]Ån÷¹Ç„pšöÍ#Gìîs÷Ðùð7·6ÿý/ÿÁƒ?¸ØÚ"‚|ºxòO*ªÅÅÅ«nZ5bÿ §«³ëé§ŸfÛK—.;oîP=ÕÎ7Þx#//ï–µ· õî3Àûï½øðáË.»lþ‚ù[AK’_üç/Æd|ý´d±Çïg])!„zúÏé’­=ݯ:Éú EUz{{S©Û„þ‰ôØ\Œã€‹I@ ý~ù|ÂBZÚ/³©£‡Î~2:!‚\*üà‡Ùmš–‘öÃýp4='—LžÑ0ãÐÁCÛ·o_²tÉ…Vì“dûÖíYÙY™Y™]]Õ§ª^(ž\ŒßY‚ ãc¬ÖÁ'пÍÕ3¦þý³ìg™ý\ŒýOÎv pè@ Q)úŸÿçˆýüуŸ€6‚ 2VúÜ}~ðasS3ÏóŽTGå¢JVXAcµ>Uý^`±ŽÇŽ˜Ø¡?j@àÑ;€ gCdE¾Ø: ‚ ‚ ‚LåÎõœ›_€Þ9ž£X®AAAÏ$æß€ât°vô ŸsxYÆØAAA.yØ¢<:òÙÐÎþËþ©gW%@Ï-¼×ï½Ø: ‚ ‚ ‚L ¶xÛHç¤4¦ú®Y€ ‚ ‚ ‚ ŸAdEîêîºØZ È¥ïv»/¶‚ ‚ ‚ €Ýn× «•””Á€þ…ÙÆ€¿ò¹—ƒFAAAÏ;è@AAAÏ;è@AAAÏ;ŸwïÀ3Ïm ç8pàÀÊ•+³²²òòò®ºêªmÛ¶ièiÓ¦íÛ·ïüÔ3GŽY³fMVVVVVÖ-·ÜrâĉOXÆÚµk{ì±Ø– 6Lš4I’¤[n¹eÙ²eŸ˜&¿üå/ívûž={&vØ‹rqAAA ÄÞh4ºvíÚeË–?~üСC<ðÏóŸ¤fãàøñã+V¬˜5kÖþýû÷íÛ7mÚ´k®¹¦¶¶ö“×ä¶Ûn{å•WTUÕ[^~ùå5kÖ‚°xñ⊊ŠOL“—^zÉn·¿øâ‹Ÿ˜DAAAä’cHï@KK‹Ëåúæ7¿ŸpÅW,X°n»í¶?üáz·Ë.»ì­·Þ ƒ_ýêW óóó—-[æ÷ûxà¶¶¶Ûo¿}Ú´iÏ=÷>|øÚk¯ÍËË[¸pá¦M›Øî¥¥¥þóŸçΛ‘‘ñÐCµ··_ýõ™™™k×®õûýc=˜Ç{ìšk®y衇RSSÓÒÒyä‘Ë.»ìñÇ€}ûöÍœ9Sï¹lÙ²7²mI’î½÷ÞÜÜÜùóçïØ±ƒ5vwwõ«_-***//ÿÝï~7VM®½öÚ@ °}ûvöÒçó­_¿~ݺupvfÁk¯½6þüÜÜÜU«V544@¿½;wîÜ;3mO›6íàÁƒcUcÏž=---?ÿùÏÿñ„Ãa½½­­íÚk¯ÍÍͽñÆ[[[Yc}}ýêÕ«óòòæÎûú믳Æ5kÖ¼òÊ+lûý÷ß¿òÊ+àÜ‹‹ ‚ ‚ ‚\Ò éÈÊÊÊÌ̼ï¾û6nÜèv»õöuëÖéæâÉ“'›šš®¹æšgžy& UUUÕÖÖþú׿áç?ÿyFFÆóÏ?äÈ‘;î¸Ãår­^½úë_ÿzmmí¯~õ«¯}íklþóŸëׯߵk×Ë/¿üÅ/~ññÇw:~¿ÿé§ŸÓ‘hš¶eË–›nº)¶ñæ›oÖ=CñÑGUVVÖÕÕ=ðÀ·ß~»Ç〻îº+55õèÑ£ï½÷ÞsÏ=·~ýú1)c0nºé¦—^z‰½|óÍ7óòò„ lÙ²åá‡~ê©§êêꮾúê/}éKš¦UVVîÞ½[–åîîî`0¸gÏMÓZZZÜn÷´iÓÆ¤¼øâ‹W^yåªU«DQ|÷Ýwõö^xá?þã?jjj¦L™rÏ=÷€ªª·ß~ûŒ3ª««÷»ßÝÿýÃ8#\ܱj… ‚ ‚ ‚|ÚÒ; Šâû￟’’òàƒ¯Y³¦¹¹®¾úêæææêêjxùå—W­Ze0Aèîîv:„éÓ§ †£½ñÆÓ§O_½z5ÇqóçÏ_²dɆ Ø[ßýîw“““srr*++-ZT^^n6›o¼ñÆ#GŽŒéH‚Á`8NMMmLKKs»Ý±þçRTTtÛm·q·zõêÂÂÂ?üðäÉ“‡~ì±ÇŒFcffæ=÷Üóæ›oŽI¸í¶ÛþùφB!x饗Xà@,ûÛßî¹çžŠŠ Žã¾ùÍo¶··×××gee%&&>|øã?^ºtinnîÉ“'wìØ1þ|ŽãƤ@8~ã7Ö¬YÃóü7Þ›\°zõꊊ Ayä‘;wvvv;v¬¥¥å‡?ü¡(ŠóæÍ»å–[t‚ ‚ ‚ ò™g¸R™™™?ÿùÏ ³³ó¾ûî»ï¾ûÞzë-QW¯^ýòË/?üðï¾úêŸþô'¸ãŽ;:::îºë®@ pÛm·=üðÃLÙæææýû÷Ï;—½ úL¸Ãá`&“)v;ŒéHÌf³Á`èììŒmìèè°Ûí”W|133SßÎÎÎnoo—eyáÂ…¬1–””ŒI˜={vFFÆ¿þõ¯ ìÙ³ç/ùË€ÍÍÍ»wïŽ5Ú»ºº *++·oßÞÖÖVYYép8vìØqäÈ‘ÊÊʱ*°~ýzUU¯¾újX»víŠ+ºººØÖÙjµÚíöööö®®®´´4AX{NNΡC‡Æ*AAA¹DU¡ÁÔÔÔ»îºëþûïg/×­[÷å/yÉ’%<ÏÏŸ? ÃüãÿøÇ555k×®­¨¨XµjU¬Mž™™¹xñâgŸ}öBƒ²dÉ’7ÞxcåÊ•zã믿Îìj³Ù‰DôöÞÞ^}[O¼€––VøÀjµîÚµkx·Âˆ|á _x饗𛛗,Y’––6à]V^öÇRYYùæ›ovttÜ{gã¹çž;räÈ¿øÅ±JñÅÃᰞΠ(Ê«¯¾zï½÷BÌ!·ÛžžN)íèè$‰9šššÒÓÓáìó{ÒÎóÌ ‚ ‚ ‚ Ÿ*†«JøøãWWW‡Ãᆆ†¿þõ¯³fÍboÍœ9Óh4>ôÐCk×®%„À¶mÛjjj4MKLLY˜)))uuul—U«VíÚµëõ×_—$)‰ìܹ“å)L,?üð»ï¾ûÄOtvvvttüô§?ýðÃÿíßþ òóóý~?[àðwÞijjÒ÷r:/¿ü²ªªo¾ù¦Óé\¾|yIIIQQÑ#<â÷ûUU­®®ßê}·Þzë¶mÛþú׿ž›V_þò—ûÛß8p@Ó4ŸÏ÷Ö[o± ˆÊÊÊ?þØëõæääÌŸ?Ë–-c]æ ««kÓ¦MÏ?ÿü¶~¾ÿýïëq o¼ñÆÑ£G%IúÙÏ~6oÞ¼ÔÔÔ²²²ÌÌÌ_üâÑhtïÞ½¯½ö+ŽX^^Î 7„B¡ØJ±AAA¹ÔÒ;`µZ[[[o¹å–¼¼¼«¯¾:%%%¶tÿºuëŽ?¾víZö²©©iíÚµ¬vÀŠ+V¬X÷ßÿc=–››û׿þÕáp¼þúëÏ<óLqqqYYÙ“O>©(Ê„LyyùÛo¿½wïÞ™3g–””<÷ÜsÿøÇ?˜]m2™ž|òÉ;ï¼óÚk¯Ýºukyy¹¾×å—_¾uëÖ¼¼¼'žxâÙgŸMHH „<ûì³.—kÖ¬YùùùßúÖ·X©Â±’™™YYYéóùbÃt–-[ö³Ÿýì»ßýnNNμyóÖ¯_Ï\-yyy ,(Ãb±Î;Wù%/¿üò”)S®¼òJG?wß}·Óé ìv»¦iÝ=ÝŽGII  ˆÝðA>· YwÀn·ŸÛÈ\ ápøOúÓ]wÝuõ;ƒ*|çwB¿Ú]™OR“‹®‚ ‚ ‚ r 1¤w`(3rË–-ëÖ­[¼xñ­·ÞzÁ´Ÿ*»÷¢+sÑ@AAA.!FµfA,K–,ikk»ª ‚ ‚ ‚ rQÀuéAAAäóßÞÞ~±u@AAAäbÂOš4ébë€ ‚ ‚ ‚ ÈÅ„WUõbë€ ‚ ‚ ‚ ÈÅë ‚ ‚ ‚ Èçô ‚ ‚ ‚ Èç^Ó´‹­‚ ‚ ‚ ‚ Œ@AAAÏ;üÅVAAAdâ‘e™Äçn³žzËEÔA.:Ð;Pë©ÝÒ±Åu%ЉËÒ—Ä\8Yˆp8ÜÙÙÙÛÛ #‘ ³Ùœ˜˜˜ššj4QÄ'&b4Móz½²,Çq6› ŸìŸE9Ÿ¼'BÇq¥Œª kÐP:}%$-Ž·›9G`¢î¶.¥«!Z¯¿ÌrÒøô {H4КÂMÙ†lJ0” Aù¬áõzcÍþsÿº Ÿ[H0œðA»B]ßÞömgĹ<¹Ãìè t}Øða‰±ä¿ýWŠ)eÂÅ]"‘ˆÓéliiQEQUUÙÒ”RJ)ÇqÇeee qAEŒHoo¯Á`°Z­ÀÜv»}ãhšÆ\ C}7Ķe¸N Q‡:dµZËÊÊÎóë*"©Û«\µ­> àŒæ)6ã5sÒ̆ ³¢' Y–»»» !š¦ ðñÃ`Þ}MÓØÑÿjš–œœÌQŠM IDATóçëU55p…µ·Nø¶4E{" ŒÈ¡°”cëŠÍ7OKˆO'ÀGðhçð›]áÉŒ_Ÿ÷¨#pÔtodï‡->UúTœw¡Å!‚ È…Æl6kšÖÝÓíHq¤¦¦¢wAFÏØ¼ªªvuu1 j(ܲ{ÝÞu…¹…7®¢œM°y$,*¯Ô¼ÖØÜøò¼—'gN¦tÈI*¯×ëóùÎsÎ0...>>~Ü#tuu:t(G£QY–m6[\\œ(Š”Òp8ìóù<Ïó¢(ÆéÓ§;qD …ËåJJJbÛ ºwÀëõ¦¦¦žÛmD‚Áàþýûõ—ú]{»²mæ‰Ý`o©ªš•••››;¾ƒŠ¥¯¯ïĉÌá3f̈‹¿åö¯-¥Á¨Òÿ¹:ý¿Uä¾rjêø†õx<¡Pˆã8ý<0cž}~c7€R*˲Ñhï&wttȲLÏúO„½…ù¤˜sG÷O€Ãá8ϸUƒˆFv4‡Ÿ:à§ññ q†¨ ²¤ô¤`X#²?B¢ÿ÷šô™&‘=¿WìXþÍä;õ—ì~öÃEÏkÄQða÷‡Ï¹ž+4n¬Ýø—Ù)Œ+¼ÐAä‚‚Þ7c›XëîîNMM•¤!½š¦Ý¿ù~%Y™šQÒ=^®Îº9{í«Î¿”ÎÈ*sœ?©úÉþ{+Ñëõž¿qÕØØ8”w ¹¹¹¸¸8 µo}}ý±cÇ$IŠF£yyy‹…Y¼Ìðe9 544´´´È²¼{÷î©S§æçç^=1 #^ EQêêê²²²œNgfffOOÇqF£‘9­ŒFcoooCCƒÃáèééIOOommMKKÍ|¾¢(---üeºEªª*û gû ônªªò<Ÿ˜˜8¢ BÛÚÚ²²²b¿ÚÚÚjkk !º¡»iÓ¦ùóçÛ½ÒÔæ s—™0 ½¾µ»]ÑÆçp¹\§¨¨hÐw™æêÙPJ?>úÏø† ôÑt³À6s@ĺiÀd2­^½z˜‘G¼Í@iäÃúÐ_«¢Ù¹©f#Ï«J4*ûU4*kD$r¼É€û^©ÿsç[ÜyE„".Ñó2 ‹Îc¸Q-Þ–p œš’zKÉ-wl¹ã§?½"ûŠá÷ÍÙCAA.9ÎxvíÚUYY©(оqnoEQ"‘h4µZ-n·G’¤NõZß»~féôÎh´@( Ž õx)Iö·«Þþ7ÿ¿ cäB¼^o0äy^UÕa¦þxž7Á`R:`cÜž¿ŽŽŽcÇŽ…B!Jé¬Y³ØÜ# NŽ¥4›Í‹%##ãСC¡PèØ±c&“)-- EL ˆ¡¨««+((ày¾  ÀårÅÚ™ÌDÌÈÈPUÕåreff€ÃáhooÏÊÊq丸¸¢¢¢wß}L&“nü¨ªj2™<ÞÓjµžkŽ›ÍVRR2úc‰D"UUUÐÛÛ;uêTæÂp:±÷p hooïéé·w@‘Âþ`Ôëè" úý"?ž´MÓzzzL&Ó0}¸dYæyžçù¶¶¶œœœ?¤ªªZ,–sÇaÒÕsÐ÷€óO(€¨ Ç]ò+µRéä$3Ï›‰jÔ4¡QÖBh*M‘Õ(þè5Ï~mja/Ž1y_’$]ùpsÔ•Þ§¿n‰°*@)a|+‚ÑîÜëÞû|ýóm‘¶x!¾<­Üñxóïxl×c'ºOÜ7ã>œ?AAù¼Áë1ü••• iš¾qnoö£¼££# [,VA¼»©õ#°B_}«,d'O€lc‰§³/Ním/‚>v}<'ÎP 1+BÓ4QY\úP=ƒÁ ÇãA–åúŒâP ún8Þ·o³xgÏžšš*Š"Ïózs¬†ÌΙ?þÇ …öíÛ·|ùòØ?Û" êvkŸˆ¡._OOÍfcÖ (ŠéééÐo=2Ñ¢(²»ÝÎŽ‘5vww'''/ CNN °X,qqq^¯—½eµZãããcçK™+2mFczzúè“bü~ÿñãÇÙí û÷ï/--­©©ñûýzJi(êêê²Z­ ãθQäpÀôzÎ|šØ@Á€?Þbǰ¬Ì„~Gé2/Î…Rªiš¢(ÃGs £Ò×€,Ëzþ…ŽîDÇs€!kÐ%¯÷çæØ³í¼¨Âœ8Rí_’D¢ˆ™ÐOk#MÕÀQÿ磦ŸÜX`môùš¦­ßT·¯>„h„F;%·¿ïtøR—òÿ^ldqŽ«—%—g§Ü¦ªjÛ7n9Ðpú%•ãJš‚qíÆDkbrB²ÙhÑ/ù%U"@VÎZ¹þè;G6ùýòß ÜpþˆóÉÿBAA>…œ±ôj×P…RŽR€ ˆÐ4­+Ô"ä‡'=²ä?õöåSV-‡U°éå]5†æ®p×ð:©ªêñx\.—Éd¾”,ËÌÆˆÝ÷ ÛñãǃÁ`47o«´ÏŒêcÇŽM™2…™ÐŸö¬3{öìíÛ·³ÝgΜ9>"Õ/9”Ò`0xâĉãdz¨ŽãJKKKJJÌfó8D ErrrCCƒÉdÒ]áp¸¹¹90ë4šÍ挌 Q™éF½^oaᨲ© ƒÅbÑ}ñññºw@UÕøøøØ˜_ÀüÌR}µ —Ëåt:9Žcþ žçƒÁàæÍ›ÎÿSJ#‘Hww·Õj5kÖùÄ\PN Ý^¯?¶QÓ Š’ó¨Ð1 4CìË¡ç&e û\Ÿkü˲ÌNsœ+eň¸Ýîž@ ^ŠëÓ )¼(k`æ Ú¯q4вæhÝ~UU´Dç–e™€ pëvܽ<ÏO £6áeYÞ²·«9*¡„Å«zåÓ¥•5M#~RÓ%„%5n¥$m<7I–÷;]Ç껈-ß§ø­&«À a5Q#a%,)RTŽ D0ƒM´žô>Ñ{bZÊ´±ŠCAA.]ÎxvïÞ]YYÉòÃÙÆ ;¨ªª( Ïóç¦hš&(Ô+Οnù•ƒŽË–_¿ùÔ[Gz¶ûhTꀀ™7¯«oÇæ!Mp8£=ϳ±Ã{7†" 644D"‘ììì´´´X£º½½½½½½¬¬,'''V4ˆ¢h·Ûóòòêëë¦L™b6ytƒŠðz½n·’’’XÊzGGGWW—ªª‡#11qBDx<}"]ÉœOJJJHH8O”ÒÆÆÆ­[·ú|>`qÁ`p÷îÝUUUK–,ÉÍÍ“ˆáq8]]]‡# ʲ\SSc±X ƒn%ƒÁÇ›L¦h4ÚÝÝ=¦ñ“““SSS»»»€üv»½©©‰RÊ®QJJ ;™ÐŸún³Ù\.×(-ÒÖÖÖºº:ý^¥”vww«ªª»tÚ`0dee9ŽÑ„â'XBÙãñ hD)'ZÆ=l¬©ßÛÛk6›GŒ ½w€MM[­Ö¹sçž:uª¦¦FQÝ)À>Ä`”‚Î¥ÛÕsÄ=5uKqf‰È*D4*"$ Q-×Î…h“M<—šd¬„»du_½'«Ü.Rm”—*–N6†¬YV „ˆªÆƒ_õRJ@U5Žç©‚FŽ€••qx"QùT›/ª(зW18ÑÒG)­÷ÖGi”yNàGhª¢òJ´½UýéÂ_¡kAAäóÆïK(ˆÝ‹Åìp ¾*¡¦iK¼—ýoßSM¶æg”_N2‚èU—ÂõU¾6~[„æ€(”såÃ뤛yã›ýëä$£­­eçèÓÝl(I’B¡Ð¦M›²²²,XÀÞ…~ªªÅÅŵµµ’$µµµMš4iL"<ω'ÂápYY³<»»»wíÚ .\ÈòÌÏ_Duuu$‰D"l}½’ü¬Y³X¼ýùˆƒ[¶lñûýV«uêÔ©¥¥¥PUUUUUå÷û·lÙ²víZ£Ñ8zÃÓÞÞž””¤ªj0¬­­5 ¡Pˆ·gæ"›ù?|øpEE3¹G/NUÕžžUU­VkW×™ Ji[[ÄTdn²Ø¤÷ÎÎN=ÀdœN'Jw °~=L†R*˲>”(Š.—«¥¥e@åÂaPõÉ—jìQ5B8ž®£O¶ñ¯É„è\×4-¢ßÞ۽ϹUSM•9 ×/*¼qqÁh¤ÀÙŸµh4ºgÏžk®¹fô»Œ·Û½qãÆ%K–dgg¯_¿þÜgÂùÄ E¼#£»Ïålì ¦¥¸Ã` B<£H8Rƒ ÒpD …倬*QµO’Ì¢× ÔtC¥v+?ÚڄǪ{5c¨Eúƒ·×­ZT¾(.Dì ©²¬‡¥gIŒŸþ~»GœdÛ @uKŸ/¤H²­†Ýæ ·Ë×E8à‚„D5…D5 ì&ÉnUUÕ"…HSüÿ®|~^æ¼±ž4AAäRçLÝ=àÜ MÓâââ†YYm9¿|æž™”íà%&ÔöBK³¹Ù“,“†ŸÖwj€N$k¾q{{»,Ëv»Ýb±ÄºXìz8ŽD"§Njhh¸ì²ËŠŠŠôwyž7 ‰‰‰§½½}˜ö¡DD£ÑH$¢GIhš‡u‹wBD(ŠFÙ°ÌxfoMˆˆêêjæX½zubb"“¸`Á‚’’’×_ÝçóUUUÍ™3gô"½@š¦y<žh4ÊîOUUÛÚÚ˜m0&Mšäv»;::Xˆsè444dee±)úžžQ­Vëð¶(ŠEEE.—+ &''§§§···³X\\œ––¦×&då Âáp eÙï÷ÇÇǧ¦¦ªª:ŒUU<¨iZJJЦi,lÞ`0ÄÞo‘HÄf³ùý~½†‚¦immm¡P¨  `4‹/ÈŠ²ïˆ3ÎäûÉ2QIÖ¢QJ9Ú¸ª¢‚æÏ2” Â:ZÌ–“Í©7ŒbÖ]w0éÌfsrrrooobbâ0»6?âÜ>ë`0-Z …öîÝ;¢V±è¥F‹¢ÈÑh¤Å/5×ÔôERyJüQh÷ƒ@@  H5š8âWzý²+¨QŽFAMóD Ýnöx!IUGUÕC'Ýa",“ŠΤQ1.¨ú8ŽBŒ‚¥hz‚Çhhm#¡LOÀÓÔ—”Ÿr¦ÖÃhˆHʱF·tæ¼…pÂóë@4 †dkKÚ´Ô;2í3¬ÆÔNÿ±ãÒ#Ô,&øÜ|cÁÓk^ȵg&òë ‚ ‚|Æ8;°gÏž… ʲ¬oŒc¸¤¤¤Gí~%ð•Þ@o þî}mgϧ÷T a#$™“µ>:ü²í„ŒŒŒqˆŽÅétŽu¯×«( ÓMŸ¹Õc"‘H0T% ½ûî»uuu‹/fAõÌ´¶Ûí½½½zŽúèE¨ª*IR8Ž-º‰DB¡óLˆfT³Ø6óO)Õ—©?O”R³Ù·Û­gmŒf9CBH0ìììdˆ‹‹cWÜb±„B¡´´´¸¸¸ÄÄD–`‹…åÚÔÔÔäåå _ŒD_\÷ì[RRR^EƒAD^ šÊJ@SUEÕY #ÑHD’¢žÑlu#D÷èÄ^*ƒÁðÒK/ÝtÓMéééç_w@¯>øñdz€ŽœœV|¡¥¥¥®®n”㌉mÛ·fggËÆ”ÎÆZÙjŽÊª Ñ ‘‡q°ÍÍA-DB*•4@Â2@T%Q€x{ºëŒY¥#/²,vö„B¯k©û»–í2Z“…çDªjJ“·Áç …}J|OnfÊÍ!½±OŠ­9J‡êz¨bTT…ë¡×Nþs¼!@U6ØÍÆh»Kv.[Wñ³dã8×Å@AA.uÎx.\8`c<Ãñüå—ÿfÇož0=q"þD•¯©ÊÛ<@"”EË‚‡–N[ÚÙÙ9â8zñó' ¿ß/˲Édb¦r¬¬`0 cÃ×O:ÕÕÕµpáÂüü|Ößl6³ 䱊ÝÝÝ’$éîUUÃá°Ïçcâ ßz·ˆP(äv»EaG‡€Ù꺱q>"¦OŸ^ZZÊܱ=)¥eeeGusŒFÄ „ÃaVhçyæèì쌋‹cÐo±§§§çææîܹÓãñÔÕÕ-X°4M‹D"Ìí2¼I’@\\\^^^ggg4ÍÊÊRUÕn··´´0G°ÔV #..Ž5º\.Ý•0„«Õ‰DXŒ€$I ”R“ɤªªÏç+((ÐW1HNNöx<¢(Š¢¨GLŸOŸ£öù|ḭ̀:ƒÖ#Ôõ_ÆÓYÓÕÝQ¾d%(rn’å€/l0]ò~+ê… Q4¢Q ÕT  iMó…Áª&Û‡ó~ÆÒë 7tÍ™–”¬Eþã͡η=Ö‰D‘€`£Ê'td™èui ýáh;Q½ÁH’m ¡Už@äx³'Þ–àÈ`ƒ)>õ%h‰¬ |ª½{rO׬Yé_líX¶˜Fp?!‚ ‚ ŸIƼfÁˆX­ÖÕ‹W-ÚÔ°i?·ßÏù㔸ٮÙKs—N:Õl6èp:(---/?=‡©ªê¡C‡jjjæÍ›»¾ý'Css³Ïçóûý”R¶Êݸms ƒ--->ŸO/ãÏ\̆ŸÂápWWW ƒ, Þl6›L&‹Å2!GÁ p¶ûf€‡åüQû—`dUUmhh`ÁEEEz(Ø‘RJY);}zD).—‹Ršššôv£Ñ¨G hšÆÊ(°YõJ©ÛíqVŸ²`Á‚={öø|>æðù|F£Ñb±˜L&“Éäv»cW^Œ‹‹c60«Æçv»NgII‰Íf^¥ôwOüŸ•w>hŽK¤œÀñƒÁl2›LQB hª"ËR4êîó~|°ÁïóHîÚwžû…(ŽÁøÔ¯²×ë}ÿý÷ËÊÊôõ&‡ÙeôãÀår±ò¢Ö#?¯àÑG}}Ù5ÓËJøôÔcN0l ¼‘‡î0ñKT’5¢@T" ª¢j²¬©²‰(YFâøÝ½^š1ÂÕa>Ù<5؈¤È…Å7ÜÒ˜’N• ·ÑÊåKGJomÙÔ%7˪ŒhFšÜ‘D›u”Ε¨¬©ë F$“FaRÈ’:ò XÕß¡$qFÇt“ôŸ9ê +„Ðú®=Î$òÊ‹ ‚ ‚|j9Sw`4™£I4%„X,–9s攕•õõõE"ƒÁÀÒÔa¤”ãÖÖÖêêêiÓ¦íÛ·ÊËËUU=pà@UUÕäÉ“7lØpÕUWè Ç:ç&“)è«Ó 8"ŽãX1v¶AQQÑW\ÁLDµ¿N>d¹ƒŠ ”Š¢[é=¶%¶èÝùˆ`y<Ï3A`&=³îÎ_Ä_˹€cÇŽƒÁXÊhDÀ`H}Y{vs¦¤¤¸\.½Ó§»»»««‹çyARRRt×CÅá劢Èî[Çc6›G[[¥Ôç󥤤$''¦ƒ¢(,/  1ë=33Ób±Œø‰¯¬¬<|ø°ÇãÑ—çÐÏ›Ùlöx<Ìøg®æ$Éï÷×ÖÖʲ<š¤}ÈHMþé¾ò£'_LÍ-žRÓ"4¢J5MQ”¨$EBáP0j4Y½ ÞÿÅ‚ÜÌQ&“³nº©ßÙÙép8¦L™R]]ÍÊ( jÀ«ý/Fÿ =Ÿäöq<æÍ›÷øã9ºlvÙÑ^nz ·³»Ï–ï4€‰‚¤¯©QY …e*)DÓŒ!DͶɢ/êq»{zŒÉIɃJÔ‘dù`u‚ +š¬—Þ^õΓEßäH¼L½öê·íS+oÔçähÔÌku=¡ò,y4µ'  m?\ôõ™ãâT^$„ˆêщo<éþ{Y(Ÿ3˜X hšì÷U3=XœœnâF^Rë ‚ ‚|Ƙà̎ㆯ_8ÌÏÏ÷ù|999{öìI’>œ——Çâ®ßÿý»ï¾ûü5€Õjíééq»Ý±Ëþ1DQdø”R›Ívà 7”••AŒm,Ëroo/d¬"X.½ÉdbÓLˆf³Ywf<‹¢ã=nÁ`ðõ×_gþ(,,\²d Ïó}}}UUUÀò˜•;ƒ’žžîv»%IêëëKHHe¹¨¨¨««KQ”ªªª¸¸8¿ßßÓÓÃÍd2)ŠRXXÈ”d»˜L¦¤¤¤±Rl›MæC¿›Æ`0°U™Ÿ‚E 0s˜L¦Ñ‹Édš={vuuuoo¯,ËV«Õ`0ˆ¢È.ŠÅbñx<±‘¬hBKK Ïó6›mÄ뮬ühû¾£­AÑ*-&“If[*ŠJ<ÏJ¤°{AYúº›GXn`z%EÈÎÎv:o¿ý¶ÍfþâŽ>v@¿9ÕÁ¸@¦©Ùb^ºl©ò`_T«È±ù‚®xQØÞáAÈ*á8¨ÈSUVM‘eESÔî¾ðôƒj*Nëë¨;i¿rùUà Re墜Ëç«ÛOÖîîPAÑ@– '­©ûý³ÓžààÈïùIÅ7ƒ$IáááÚRÇ삳ÈÉòh½ Ê+g¦^15éým{7Õ-ÅQ_¯=rykÑßÜü¡îÚ“fG!g´hZ íÔMÓL‹gMyÔó CAA.Q&>³à<™;wîÞ½{ÓÓÓÙÜï®]» %%…ÅuwuuÝpà BnzzzSSSwww$`Z³i|AJJJV¬XÁŒµ?×]–åh4ÚÙÙÉóüPéÖÈàyÞl6ó<¯ÿâÐ2!"ôÀ‡h4 ýá ,‚`BD„B!™™9cÆŒp8|ìØ±ªª*ŸÏgµZ™3eô"…ã8r YrAqqñ©S§|>_ww÷æÍ›ãââ,‹Õje‡ÉqܤI“˜KBÅQJd‹X,–ŒŒ —ˇ‡¦i6›­¥¥¥¯¯¯Ýn×3 ³T].Wkkë(‡çù)S¦´´´°¶ „înHIIѳ XÎBSS¥411qÁ‚±‘&#ò³Þs×dÌ-6[Ì&“Ñh0ðú1u˜©Ï.ÇUW±‡Ù3$ÖËÆn9aLu†áBä¸z\ii©mím]9EQÿÔ<ûG[ý}O5YQEQÐxQ6ˆ¥ÃÑVÙ”I8Rš,.ž‘Ö´óàìIù/ýå…•#-맪«#á0ÌM%!WÇ¡dÉ ö%õ¿Ü„8ì‹ ¢I•£„ÀÊ"ûœTðuÔݦ©S§ŽæXjë8%bæ`åÂ’€o׿zY´g"•Yûs ˆ’91¥D‘´pgõí³L7,.óù|ªµu ³fL;ßS‰ ‚ ‚\jLðšçOJJÊM7ÝôÚk¯%&&2;"‘!¤££cÍš5ç¿¢Á dee±P§Ó9}útVcÏ,EÉ’%%%%¬³ßÎ,Þ'N¨ªJÉÊ®Vù "ÇâÅ‹UUÍÉÉaÝÒÒÒXKVVÖ„ˆHNNž;w.³6õžìèÒÒÒÎ_–Uèìì|íµ× ?ƒÀjµ.Y²„U"½ˆáIMMmnn¶Z­ªª.[¶lÓ¦Ml>E‰D"l…Èk¯½–Z0ÌÌÌåપ²ÑØÐ­ C0Ôg­ ƒÚ_q@?«@`ô³ú@)ÍÎÎ6›Í}}}l=æÂ°Ùl´±IèèèhiiaβY³fÉ5f“ñ—㾟þiÞåëÌ&“Ùdd %Iâ(Õ@«Ú½ù?þF|œeLÃ2DQìííÕ­tæ%ÑgjL­vÞdYNKK}øÀ¸‹Œ{GBȃ=¨iJ$]²ty^Ee›—Ì-Í4l>VÛ©ªªd4ª¢Y1=¢ð†¸hxn¾mÖÔLco]é¬É$صhþ‚å—/QÉhúƒƒ®žiÔö9õŠåmŽ\Ùµ(±%å©”M]Yb»¬$= 2¿äè|5šLT;Ýù‹7.Uߨô^½—šì %€ðC jÈýÕÅ)k¯ž …N»> ?̰‚ ‚ Èg•3uXB¦iúƹ½'$šwÄdàÜÜÜ5kÖ¼ð v»ÍŸ‡B¡@ p×]wÞ50Ö|c³Ù\XXxòäÉêêêììl–“<ÏOžÅìÀõº‰ºG`@ íGUUæéMEMÓÂáp¬SήÄ1Lì@4Õú^Ê€»Ý>Þ‚çž{Îb¶ìضÕ`2ÎÌ›¹½UJMMËHMêîìêr‚rd°(?.;#'Îæ¾Æ4ê˳ F>eÖôrVKrøh2›ù´én6›o¬4,‚Éš Ú2B€Žh©ñÆH$ž?Àóü(/–Ùl¢š¢‹¸ç ׬ìr âÀäƒÀæ¤E£Q]„J¸ÑˆÀº‚ ‚ Ÿ1ÎÌé¥Ýô‹…ªªl]¶nôÛ´===(p€QQQÑØØvîܹ|ùr=zñâÅz•µ ^¯÷£>“ÉTQQ1n±)Ü—œ“É´páÂÜÜ\žçõe&t¹ã1===úbŠú1w€^B_ŸÏ×Wìéé‰] `(t‹”mƒAvê!¬!;u¢(²e t Ùn·ã âããAhoo§”=ztΜ9F£1 ²¼ Qsrr ϧÚÿ·¾ò…íwßœfO(4™L@Q”PÔiÙ÷½G=¾1 !,QeLø|¾ÑGX¬Y³& ±,EQX. Kd ”†ÃavNØ ‘ ë#Šb__ßXuJé˯˜1}†ÍfknnÞ²móyyWç&ìˆî ÀÇ'gØŽRÐTÅ †£­5<ïÏJÔŒŠ§·Kv8R:»Úmñ # xÞ ŠúK³Ùœ:tgÝn‡±ØäQ¸3If³95uH!±UN$Í~AAÏ#g~îÝ»wáÂ…’$éK§={ölÛ¶¥sëQЄ>ø@UÕéÓ§_ ¹f³¹²²rÓ¦M~¿ãÆ•••lY{fŠÄκ3‹×ívoÞ¼™Y;•••±¿à?‡"¢Ñ¨.ú]ã1‰‰‰555ñññìe0Ôc:Ì$‡B!=ßï÷Ò¯d4£Ñ¨¼í÷ûív;³Ç4Mc*ó0kÊh4 6NkØ¿ŽÉdÊÌÌdå_g{{{JJÊhz†Ã¡Ǩz £«{{!‚ ‚ —:jÍ‚qãt:N'ËëÖÈEQ ‡Ã<ÏoݺÕ`0èùÿNvvö¼yóvïÞ ?øàƒÉ“'—–– Ýè…~£úĉ¬ ¿Á`˜7o^vv6Š˜XƒB)ÍÏϯ««³Ùln·;--­¯¯OQ”ØI{·ÛÍqœÍfëìì´Ùlgôïiii‡CQfðsÇÌ~ö—-ÅÛ?6è8Ž·o0 Ng8nkk‹F£&“©  Àn·OH¿ŒôÔ¾ºöÖÿùêwƒ›7?ûÛ®ÏÊg0!$tww³‰}QõˆF èK3²Ú£9"EQ8ŽÓ‹€êÅA™ ¶Î¨$IÌSÃü,Ä€½Ë6ÎBˆÅböy½™™™‡þøýð‚™•™“›Ÿ–š&<°·Ö»xáeS§:tÈnKÊÎÎqdMÓššš&Mš4­óóóG<š¦Õ×Õä°øë Ô×ÕåådOTñHAAäR°µF‰Ïç“$é|J…s'Â0kž½øâ‹¢(ú|>‹Å–-[‰DvíÚÅ‚º“’’dY^±bÅðR\.×PórÅÅÅ^¯w˜Ý›››·o߉DXÌBzzzbb"›£ƒ½½½ííí,®Á`0,Z´h/ŠŠÑ\ EQºººRSS™ÍßÞÞn4Ù¸$I’$±Â ªªvtt¤¦¦Ž~z<Öø×­£ÑXbúÆyÚT²,744H’¤(Jþÿgï®Ã£¸º€Ÿ™Õldã!žBn…àNÄn%P @‹Š”)îÚR kÑRàE PŠ$hÜÝ=YùþØo#Ä6dÏïyà™ÌÎÎ=÷Žìî+¶¶µ.¡ö¾Z¾ºØ²§Á‹½½}óÆúì*777;;["‘0Õ@Ì•“³X,’$™6›ÍårŒŒ>8µ¤L&KLL,..®[çv‚ ´µµ™Þ.UnP›Ó òòò~þõHÛvmoÞ¼;sÚ4‡“™™YPPð6ük‡ö.íÚQ”\KShllBÓ´T*­ýŒ ‘‘‘?þØ&Z§{÷îNNNµOâc{p¹Ü&QËÒC!„TB(Ò4“mldlbbÂ|4W÷• ©­«€ZŒ)øô¢æ /77÷ÆB¡077·ÿþÌ3ᬬ¬ƒÂÂÂAƒ}°i:—Ëå*õé­ƒ²²²/^ÄÄÄ0¿yχ™È̯{{û:Ô¹<&ÑPrrrX,–¢v€¦éÚ 1ÐlÉåò¼¼<š¡–ÊÊÊg}¿£8%ù졨«EÏ ¨ªW|•õ!—Ëëóü_qJÔMÓ¹¹¹Lgggg¦6ЦéW¯^EÇEúúú:v …uØyݪY?jø‰:×­Ôá]!„P3µÕÙG×4üüü;wîôéÓÇØØX±2--ííÛ·Ý»w×ÖÖn²HJKK“““ÓÒÒŠ‹‹KJJ@KKK[[ÛÌÌÌÒÒ²Ažëbõ'—ËSSS™:MÓæææõÿYØ‚åËe2Cýº  ˆb±øÍ›×`mmóÁv!„jJX;€P5ÇÚ„B!„ª¬@¨Îj;u6B!„B!„ZªºÏ ŽB!„B¡–kB!„B!u‡µ!„B!„ºÃqB!„B!uÇþçŸTB!„B!„T‰Ý·o_UÇ€B!„B!UbK$UÇP[ÑÑÑEEEùùùª!„B!ÔŒèéé …B{{{U‚Ð'Œ­êj+::šÅbuìØQÕ „B!„šøøø˜˜UGÐ'ì“©(**8p ª£@!„B5Gúúú·oßVu}Â>™ ±CB!„B¨ø“¡úødjB!„B!ÔH°v!„B!„Rw*¨ˆ‹‹³¶¶nútB!„B!T¥ŠµÏŸ?9r¤………Í!C‚ƒƒUB!„B!„šÌæ,H$¾¾¾sæÌ ¤(êéÓ§lvÝ'5 iš¢(‹Uï B!„B!ÔˆþÓv %%%77wîܹ:::ºººƒ êÙ³'óÒùóç{ôèammíéé™2™lòäÉ666ÞÞÞIIIÌ–³gÏ^¶lÙ˜1c:vìøæÍ±X¼aÆ:XZZ<8//ÙìøñãÎÎÎû÷ïgÖ¼}ûvëÖ­M“m„B!„B)ü§vÀÂÂÂÜÜ|þüù·oßVžäÞ½{«W¯þé§Ÿâââ†:mÚ4š¦iš1bDXXXDD„¹¹ù×_­ØþêÕ« kß¾ý¦M›‚ƒƒ¯^½š˜˜¸uëV.— %%%¯_¿ =þüÆ ::úçŸnªŒ#„B!„BèÿÔp¹Ü7n-_¾ÜÑÑÑÛÛ;99~ýõ×€€€:°X¬¹s禧§ÇÇÇs8MMM>Ÿ¿|ùòG)öãããcffA?~|ãÆ–––$IvìØQKK hš^·n††F‡ÜÜÜ^¿~ cÆŒ‰ŒŒlÒÜ#„B!„B¨Â¸`nnÎ4ïÏÌÌœ?þüùó/]º”œœüøñãÀÀ@ÅfYYYÖÖÖ?üðÃÕ«WKJJH’,--‹Å<LLL˜ÍÊËËóóómmm+¤¢­­-˜eMMÍÒÒÒFÊB!„B!„>¨ÚAMLL¦NºhÑ"077÷õõ PÞàÔ©SwïÞ½zõª‘‘QFFF›6mhšf^"‚YÐÐÐÐÓÓ‹766n´, „B!„B¨^*ŽJøÃ?DEE‰D¢„„„_~ù¥sçÎ0}úô={ö<þœ¦éâââK—.QUTTdbbbdd?ýôSu L™2åÛo¿MII¡(*,,¬¤¤¤º-qTB„B!„BH%þS; ¥¥•ššêããccc3tèP##£½{÷@ÿþý7nܸpáB++«îÝ»_»v ˆI“&QÕ·o_ooo ‹êX¹reÏž=‡ fmm½téR©TZÝ–8*!B!„B!¤„òÜÍÙ­[·¼½½UB!„B¨™:þ¼MÓÙ9ÙÆFÆ&&&L—çêþ¯r!µE~x„B!„BµhX;€B!„B©;¬@!„B!„ÔÝ'S; §§§êB!„BÍþd@¨>>™Ú¡PøìÙ3UGB!„jŽž>}* UBŸ0¶ª¨-{{û˜˜˜Û·o*“, „B!„š†žžžP(´··Wu }ÂØ?Vu G[[[Õ! „B!„š™L–›››››«ê@ú„±ØÄIFFF:995q¢*LWÝ´¼rny9ª',•ÃC  VE™Eu†åYOX€*wïÞ½P4MÓ4Í,PÅ,3/@HHˆ ãDèS÷Éô,@!„B!Eí€rMMÓ$ùÉ ©†Pó„—B!„B!¤îØL}[“!BQɧ骛–WÎ-/Gõ„¢rxÔª(0³¨Î°<ë !¤>°íB¡Úzüø1—Ë­yDhmmm.—›˜˜ØdQ!ÔÄFÅår?^û·¬]»–ËåΛ7¯ñ¢B-ÏÇ;vì(¸\nFFÆÇ¾½6wì¦ÜOú|ùòåË—UB- Ö „ú‰‰‰\.—Ëåòx<]]]—¯¾ú*==yU[[»[·nnnnª ¡ú›9s&—Ë9s¦ªA-_Í÷Õ,Z´èÍ›7ãÇß¶m›ŽŽN}b‰DL a3¼ó_~ïÊ•+ªŽ¡¥©{€Ò@£j’®ºiyåÜòrTOX *ר‡@±çN:ÅÆÆîÛ·ïìÙ³÷îÝkݺu›6m‚ƒƒ•7SIMœJ3™m¼´êö– R±%~úÕ³k¾¯ÖðÆèèhX¹r%óÜþccP>ëª\®e„µ¿ó7K—.)Z \¹r…¦éÑ£G«6$„Z l;€B¨ ¯_¿~þü¹……EVVÖ²eËàñãÇ<ÏÁÁŠŠŠ&Ožlnn®­­mkk;uêÔ {ÈÎÎvqqáñx?þø£ 2€P­ýøã<ÏÛÛ{êÔ©¦¦¦mÚ´¹sçó’H$úþûïÛ·o/ mll>\á½={öäñxÌÌk×®ñx¼ž={2/…††öèÑCWW×ËË«ÂÛ“'OvíÚU__¿]»v»víRLƆZ¶*ï«ðøñã!C†´jÕÊÆÆfÞ¼yEEEÀãñJKK ]»vfff0iÒ$mmm ‹1cÆDFF2o¯á$däää…BfÙÄÄ„Çã=xð öÖæÎ¯££Ããñ¶mÛæææfbb2{öìòòræ¥ãÇwéÒE__ßÙÙyÅŠL¦ªÜOff¦»»»™™™–––Ýüùó +D¨\5À¸rå ¶ @¨¡àŒ†!„ªåìì¼råÊyóæÝ¼yS&“)¿´wïÞ³gÏ>¼gÏžIIIaaaʯx{{GGGoذaéÒ¥M5BuqåÊ•±cÇöìÙó¯¿þúüóÏccc`áÂ…ÇŽ …¾¾¾yyy̳ÜÊ‚¨°¦¼¼ÜÓÓ3##£[·n4M+W+œ8qbÖ¬Y­ZµZ´hѵk×–/_NÓôâÅ‹/k¨Y©p_‹‹2dEQsæÌIJJ:zôhjjêÅ‹çÏŸðàA¹\>eʦv ""bäÈ‘ÁÁÁׯ_OJJ Uì¶òI¨ ¡¡1wî܃ÀìÙ³¹\®¹¹yí#T~©æ;ÿ±cÇ&Ož|êÔ©ß~ûMWWwëÖ­ÇŽ ÐÒÒòööÞ¹sgrròï¿ÿ^å~JJJòòò&L˜ §§wúôé#GްX¬Ý»w+'1f̦B¦iŠ¢˜ XņPƒÀž¨µ¼rny9ª',•kšžŠTììì@,+?áišyþ£««Û¾}{KKKå¨&L˜³nݺeË–5j´ês6bf< EB̲««k```II‰¡¡aJJJNN—Ëýí·ßàòåËÝ»w©Tª˜b¹Â°ö4Mß¹s'##ÃØØøöíÛÇÃÃãæÍ›Ì6û÷ï€ 6Œ9ràÀ ؽ{÷¢E‹5³êsò4††êYPå}õèÑ£"‘hÚ´iË—/§(*88øï¿ÿŽŽŽÞ¶mÛ‘#GärùòåËíììhš áñxR©4 ÀÚÚúíÛ·YYYFFFÌÎ+Ÿ„ŠÁ¦M›˜Ú 6èêêVØ ¡îü»ví8p`¯^½ tòäÉ-[¶0‰nÞ¼yÖ¬Y ÎÎÎçÏŸß¹sg•û±²² #IR$uéÒÅÃÃ#((O]„šö,@!T“˜˜àñxÆÄ èØ±c``àØ±c†*‹•ߥ££3a„¦¡ºj×®hii‘$ %%%‰‰‰4MѵkWf‡SÃär¹b9-- ˜·¸¸¸(^bfô˜={¶¹¹ù€ ==½¤¤¤Ás„š-åû*s>;vÌÜÜÜÒÒ277Þ8 L&“­\¹ÒÂÂB[[ÛÔÔT"‘@vvv…Í”O†ŠPy}Íw~+++Åÿ¹¹¹b±˜É³³3ØØØðù|HLL¬r?999>>>ºººUf!Ô¨°v¡O›P(äóù8{j$o߾ݼy3 2„ÍþOg4KKËG%$$\½zÕÎÎîÞ½{÷ïßW¼:qâÄ¢¢¢‘#Gæää4uП¼~›‹Å,(Zh[[[3ÏcŸ>}ʬ©ÐÊ0ÏWß¼y£XÏ4OMMeþLJJR¼dmm ëÖ­»öÞ… x<^#ä©9=z4ŸÏ?qâD=÷óé^>î«Ìùàåå¥8.^¼Ø¥K— ï:þü„BáŸþÉ4H÷Ïü«; •)NïÚ´À¯óÿõë׊ x<“;fˆ„ÄÄD‘HÖÖÖUîgÆ ýõWß¾}¯_¿¾wï^h6ã "¤>H5‰ãÇóù|>ŸÿóÏ?3k|||ø|þÎ;UXóqçÎwww######77·€€fý7ß|Ãçó—,YòÁ=Ô~ËFòúõk~UZ·nÝx‰2WrÍÛ¨¼dVBBS°S§NeÖìÝ»—Ïç;¶æ7öéÓ‡Ïç_¾|™¦éÒÒRCCC>ŸûömÅ{öìáóù]»v¥iÚÑÑ‘I%,,ŒyõÑ£GÌWWׯÈWóÁœT>>>mÛ¶íÒ¥KJJб±ñ–-[”_¥iú‡~ðôôÜ¿ÿ?ÿüÃŒ¸f`` Ø`Íš5“&MŠŽŽöðð(..Ve~hšV:mjØìÆ|>Ÿé(Þjsý6± 5kÖ,Å­ÌÐаwïÞÌá-€â›P…eZé¸hiiM™2F={öìñãǯZµŠþïc¦yÛ¼yó²e˘8™õýúõ311IHH˜9sæš5k.^¼¨xל9sàèÑ£÷ïß¿wïÞþýûwïÞÍf³›¾*c¾®8::Ög'5_G uÚ7Ã˧5ÜW§M›Æãñ®]»võêÕG>þÙ³gãÇ¿wï^çÎUTÙµk—••ÕéÓ§Ïœ9£¯¯¯¸](,]ºôõë×OžpàÀ¹s猌Œ,X°fÍšêöóÍ7ßÄÆÆÞ¿?))ÉÛÛ;$$D±çòòò‡Ò•FU¨¼€ªB1ÝH=U¨ ¨®j€ ˆˆˆ''§I´öT•®Â‰'fÏžÍ4PܱcÇܹs}}}¯\¹²iÓ¦E‹ݾ}{ÅŠIII̧Œ3˜Ñ‰>|8pà@++«Y³fmß¾]WWwÿþý±±±7n$bÕªULlyyù–-[Ο?Ÿ‘‘áèè¸víÚ¡C‡ª$›u.ç‹/úûû÷ìÙS1‰TDD„³³óâÅ‹•ëú÷ïíڵɓ'?xð 77WGG§K—.›7ovrrªrË´´´o¾ùæÞ½{eeeÝ»wß¶m3OcçèÎ;#GŽd±XÊýHÿý÷ßo¿ýöÅ‹#FŒøá‡˜¾|ZZZr¹|÷îÝÛ·o/,,\±bE÷îÝçΛžž>nܸ}ûö±X¬mÛ¶­Y³fÔ¨Q\.÷Î;­ZµÚ¾};ó%UWWW,GDDX[[ïܹóèÑ£,ËÑÑqõêÕÆ kØ’Qù¥‰‰‰LF‚ ÆŒ¸oß¾¥K—>üÏ?ÿ„jŠºsçÎoß¾UìdíÚµ;wöðð …III\.766ÖÅÅ…$Éèèh333''§¤¤$]]]‰D+“Éìíí544 _¼x¡ªì7‡CÐL|TQ(Næba.œÍ›7Ÿ8q"99yĈ?ýôÓ_ý¥räH·nÝ—@ 8p`ttôwß}7gΦF>##C(®X±b÷îÝ .ܼys Ÿ¤ X€j«öw³ºyöìY¯^½SÐïG­0a›ÍîÔ©Ó­[·–,Y2jÔ(33³… ž8q¢K—.~~~GŽ7n\PPPåkM–Ó:”3óYòäÉ“Y³fuïÞÝÝÝÝÉɉ¦é¾}ûþûï¿ÏŸ?wuuíÓ§ÓÖ1""bøðá<¸~ýzbbâ³gÏ*o)‰FŽ1nÜ833³C‡ 2$,,¬Âø:‘£Ê5ÙQQQC‡¥(* ))éçŸNMMe~Ê2Ö¯_ߥK—›7o®ZµJ(öèÑ#99ùرc ðööfösõêÕ™3gúùù>|ØÇÇçíÛ·L+AEnnnmÛ¶MIIùí·ßüýý_¾|Ùà%£ÚK ”JÕËËë?þ`Zþ+«®¨ýýý÷íÛ—™™9|øðÖ­[wéÒ…i÷›™™yãÆQ£F€»»»©©©"‰)S¦ìÙ³çĉåååb±xΜ9ÌÔJ*/|H¨}QTh£Ë,ïÚµËÏÏïĉ§OŸîÛ·ÓÝà IDAToçÎG}åʃñãdzX,𦫻Á2;¹pá‚£££‡‡‡¾¾>3–Ø•+W<=={öìyíÚµ€€€¨¨(¨ò®õ±¹hšã^eA1ÆÅÅ1yÔÖÖnìHð$oXu»R”×Wùùbll |||¢££×­[÷õ×_+×DT8?øIÚ ¨æš¬ ”?÷•—±sBõÄ®<¾Nyyy1õv^^^Õí–$I¹\Þ€‰Ö’ªÒU`J†Ãá,_¾|þüù‡RÔtÊd2ŸI“&@yy9I’Û¶m»{÷îèÑ£™g ‚¸qãAöööEEE§N5jTçΣ££_¾|©¡¡qâÄ ‚ :Ä ³víÚ½{÷þüóÏMŸÍ:—³‹‹Ë÷ß¿eË–ßÿý÷߀Aƒ19}òäÉóçÏ{÷î½iÓ&ÉdwïÞefô™5k–½½}xxxzzzå-ÿþûGGÇíÛ·@llìßÿ}öìÙiÓ¦5vŽ#+ÞÈÌW4eÊ”¯¾úŠ¢¨ëׯGFF23ÀÎ;ÇŽÛ¥K—¨¨(ÿ-[¶,Z´è—_~ õôôdΟîÝ»ïܹÂÃÃïß¿þüùÙ³g+R”ÉdÛ·oçñxE‰D¢¨¨¨‡>|øpܸq X2*¿”@©xW®\yáÂ…õë×÷ë×hš–ÉdÕõÂ… /]º”™™9yòäQ£F1Û{yy8pàÌ™3Æ ;{ö,øùù)ú=ÀàÁƒ/\¸pøða±Xlooß·oßÝ»wÓJý?›^s8ÍÄG…â´Q~ËŽ;FEQÔÞ½{ÃÂÂ&Mš4}úô+W®˜™™1K~~~u7Xæª477fFáÞ±c´oßþøñ㥥¥¦¦¦)))™™™UÞµ}Žj“‹&;î ŠÉæÉ“'Ož<ɬ744ôôôlÔHð$oXUžŠg³¶¯òóeذab±X ˜™™µiÓfÙ²eÌØŠ®£ »bî®3gÎܲeKaa¡ƒƒChhè«W¯Ú´iSå•òìÙ³ððpƒàà`mmmJ¥Š}Nœ8166vÍš5_}õ•L&SœÀL˜ì0ßµ>øIÚ ¨æ° úÔ5XÛ†··7ÔøTM¹’¯)©*]å˜ÿ;wîܹ“™9‰‰êÙ³g«V­zýú53? dgg+611166f†Ÿ777š¦…B!¼Ÿo‰Ùrc˜˜•d¶>å<þü€€€§OŸíÝ»÷Ö­[Lw5ÅÎ@&“­^½ú?þP=;;ÛÐа–LÉDEEÙÚÚ*¶üØ’©ç™£x#ÌñãÇ?®x5::ºuëÖÌ23Оž¼?ÄÌ“ü’’ÅN³ [ZZ€òÃ&ÈS§NíÚµ+))Iñõ(''G9þú—ŒÊ/%P*U{{ûñãÇÿþûïšššŠ—>XÔÊñûùù8pàÚµk!!!ÑÑÑ£GVÎI’3gÎd:O28Wˆ¡é5‡CÐL|TQT¸X˜å6mÚT¸Ö”ŸAÁû‹¥†l·nÝx<žr Ì>I’E•””èèèTwתRƒd¶>ª,(33³öíÛkii9::NŸ>ÝÄĤQ#Á“¼aÕùJQ^_å狆†Æºuë6oÞIk(5u…r777''§ÈÈÈùóçÀÈ‘#µ´´*$1yòäM›6‘$9a„'Ož4r†P“R¾îÌI¢¸ðË´¨¼OåÝÖp×úTôë×ïàÁƒªŽâ#ìܹsݺu&L¨}ØãÆ»}ûö&NœXŸ¤Åbñ«W¯˜“§e¨áóeáÂ…óæÍ ¿téÒ¶mÛöìÙãááQá:ª3®AQQó«ÒÌ̬º+…¹«GDD”––2—¡L&S\hþþþžžž7nÜ044äñxLÅ\aa¡¦¦¦òp3Œš?IQCi‘Bêàû Ù2x{{ïØ±CñùïÝÉåò”””›7o~ÔÞ´µµ™Æ;v 6¬°°ðñãÇîîîŸ}öYÇݘbbbüüüLLLJJJ‚`²`aa—/_€~ýú)Z¬•••]¿~]Ѥ¢ò– pppˆŽŽ>v옋‹KzzúíÛ·wíÚÕô¹€)S¦9räÆ&&&qqq7n܈‰‰ù¨„‡‡5ŠÅb…††jhhxxx(¿Êüè%B"‘<{öìÕ«WŠ—šsÉÔ“••ÕäÉ“ùåÅšŠšù¸cÇŽ‡N›6zÍ××÷»ï¾‹‹‹€*ÇR544¼~ý:I’LƒÔ²1'ITTÔÂ… æÏŸ_ÿl w-ÔHLMM»té¢Ü< }”ÌÌÌ2˶¶¶Lû©ÊŸ/2™ÌÍÍmðàÁ̯}¦ELåë¨rGÍÌÌ ‰D:tpvv c^ªp¥ôïßßÙÙ9""¢OŸ>Ÿ}öYlll@@€âpÕªUAœ:uÊÛÛû¯¿þÒÔÔlÛ¶íëׯçÏŸollT!Ýš?I[¤·oß23(U`ff¦ü]´é­]»v×®]sçÎUnš‡R-’FM‚)nf™™n@q hšÞ½{·ƒƒÃÏ?ÿüÛo¿ >\±^yºªÖžÌòŽ;–,Y’ŸŸÿã?ž>}Z(öëׯ©sX?...Ÿþ¹žž^XXXtt´««ëáÇ™q¿¼¼¼úõë—ŸŸÿÓO?=xðÀËËküøñYYYkÖ¬133S ¤WyKwñâE__ßþùgë֭ׯ_ïÖ­›]ÓäHùÑ4íèèxùòånݺ={v×®]¡¡¡~~~$IÒÕÖÊGßÓÓÓÜÜüñãÇ'Ož466V~‹¹¹ù?ü  ×­[÷âŋ޽{+ÞÛ¬J¦A(—ÌÒ¥KÏoé‹zÑ¢EÎÎÎ/_¾|xLLÌÚµkgÏžÍpÚ„BU|%:pàÀüÑ¥K—åË—:466JKKóóóýüü–,Y"~ýõ×ï¾ûD"‘¯¯oXX˜««+I’Ê}=çÌ™“““³`Á--­Õ«Wïß¿¿)³†\»vM,wïÞ=>>žB¯J;vì033³¶¶~ðàÁ”)S ús^,{zzž:uŠÃáøùù¤§§+öSXX8vìØ˜˜˜5kÖ,^¼X±¾ºßÿý·žžž3mMVV–D" HKK[´hÑçŸnhh˜ššÚ`ÅDGG=úñãÇ&LèÚµë±cÇfÍš¥¼Á÷ßïìì\XXøí·ßŽ?ÞÖÖV&“8qâÒ¥KŠm*Ÿ½R‰ŒŒìСâE‹Æ:eÊ”ÔÔÔ>}ú0Cµoß~Μ9Ì5uöìÙ=zL˜0!((hÔ¨QÅÅÅMP!…ž³àƒèÿŽ(ÛâÓU7-¯œ›[ŽTIs+5ÔH‡€ ú¿«ËÊÊ@GGÇÅÅÅ××ׂ¦iKKËþù‡$I‘HÔ¹sgooï{÷îÑ4”žžnll|õêUæÇ3„ MÓ‡€Õ«W6¬_¿~Çß¿•ý?–Z˜Ùúì \\\~ûí·ÒÒR ‹ÔÔÔœœƒÊÛϘ1cÓ¦M………NNNaaaáááöööUžówïÞˆˆ000¸wïžbš=ErÓ¦M‹]½zõW_}¥Ü.ºò…Æ,tëÖ™3"""88øÒ¥K&L`& 477wvvf& ¬[™¨ÕÉS[-¦•ó3 ÇŽ‰D“'Ofæ~øð!3RbºåíÛ·{zzvëÖ-::züøñ›6múꫯ~ýõ×—/_Ž;–Ù¦òÙ«¨b`N~åé0£££=zôøñã±cÇ>þ<44T1Ý23ý°ƒƒÃ¶mÛ >>þúõëüñÇÔ©S›°Rw8*!BŸ€E‹-Z´HÕQ –O1ûÌœ93((èܹsç΀>}úœ;w®  `Ñ¢E÷ïßgê€i¡š––vvvÌŒNNNŠV“’’@¹: ##C1ö8BMƒéq­©©É h_ZZZe퀓“…Bfš===½*Ïyf¶K'''¦jþ;Ý 3ÍÓé½2å ÁÌ®JÓì ‚o¿ývëÖ­ ,--­ýû÷«Ãz?I+cîÏ'Nœ8qâ„bell¬¢vÀÕÕÓ-€bFmÅö•ÏÞ ©îÙ³Gy:Lå® ‚‰ŽŽVô&`‚©WB kBH­ (,,¥¹¾ÀÂÂ"(((33óÍ›7K—.  ¹råÊõëׇ ÂÌÃÊ<…÷C”ÇÅÅ13)ïÇÊÊ*??õêÕ;wfÖH$.—Û”yD¨òL“UŠŠŠ€¢¢¢ÌÌL077ß´iS•ç<3Í^ddd•Óì?þôéÓãÆûûï¿+O³Wy xÅf¹ “^¾|yûöí{÷îÅÚÔ°˜é===Ïç¥R)S Àø¨é–•Ï^…ôôôÅ‹s8œ}ûöÙØØ|óÍ7Šé0™h*O·üã?*ÞnllÜEÕ– z¨dèQò´i´¼rny9ª',•kðCàêêúðáÃmÛ¶½xñâØ±cŠT¶nÝúìÙ3.—ËÔèëë3“óÑ4ŸŸþüyÅÆ}ûö555MOO:t¨ÁÝ»w¡Îš5kÁ‚¿þú«H$¢iúÍ›7eeeƒ®ÔêlÄÌ~ì”wUae…õʯþüóÏÌ4{b±¸C‡ŽŽŽÕóîîîNNN‘‘‘îîî½{÷Ž‹‹ûüóÏGÍl°bÅ ‚ }}}/_¾¬˜foÁ‚Šiö”³áááA’$3ÍÞèÑ£¥RiçÎ dnn®˜,°Î= Ôçäi -¯y™>¾W¯^¿þú+A5œó.\`¦Ù;sæLzzº©©©ò®víÚåîî:iÒ$‰D²}ûv‡'OžúøøTHw̘1æææÿþû¯½½ý‰'ŒI’lÛ¶íµk×¶lÙ2tèPœ58GGÇK—.uïÞýܹs{öìaæf±Xµ“Êg¯ò«ßÿ½P(ܰaË/zõê¥xÉËËËÝÝ=??ÿðáÃ>d®)ŸÇoÛ¶íÆ]»vUôq@5 "//¯)Ó#I’q¤)Uaºê¦å•sËËQ=a¨\³=EEEŠùÉ=<ÑüÃ[!„B!„*ì3¥TBjg4D!„B!„ÔÖ „B!„BêŽ]Ý ­8+-šËáÄ»Ù?ÿ½ÿATž5”¸%³TQÄj²l „B!„B¨Îª­°Ñ 7–±¹r›Åb1ÿ‘,‹Åb‘É"I‚$H‚`ÞEÓ´D"!ý8MQÔsŸ’r1·(­<7],0âµ²#Hlõ€B!„B5ŒjkH’Ì-*ù—Ô1ְج)-KJŠŠJ‹ÊËKD ,‹E4  i €ÅbÉ¥Òä„´^vV_¹[×ùíè£p ª­¾¨¯uYó2#žŒ€†œÛ¢ù˜ÙÝü›Á­k¹±G;£Ýc5„B5¸~‡ìa-¼:«Óýù]Ûšh}Ôž¯ÌìØÞTûƒ›ðjÓÑüÝfÓºšÝžÛåÖÜZ=-ëe£{j’ëG…Ô4šm`©ƒÆ X@”Ƨ…¼Õâp³K¢S€ZNÑRI»VÚ©9d©HÖÓÁÐÒDOGG»‹‹‹AØ«üî.f¦úò:÷,‰ËÊÊ>|Ñ©S'CCÃÚ¾Q.­cŠ ÄÝNoI?c-îóÔâµ×c²J$ª§y2àJGºæõvaijp)ŠNÈ(¹ô 4S“j .š Ig§ºòÙ¬QGŸW¹AkÁË´âZî-.·<åIZu¯î÷jó0¡à÷géŠ5žígv7÷kØ‚>Ö“;›~÷¿¸?_f2/-ío3¡“éŽ{‰'žV»Ã&³jP똜²³aªD}á!PO-ì¸ïã”S*ýþVóg' ]žNÞÇ^àç,ú$ÌÿÌjǽ„;ÑyuÞÆaöñyå¿þ›Zù¥N:Vhj1hrYszYŽ8ò<¯L5_‰ylr·§³£±@Àa%ä•ï I‰Ï?·VcÛ[ëi”Kÿ|•uôŸ”–ù\¡¡†qH’ ¥E…÷ÃâmÍô¥¹ù4EASTW{ƒõÓz—KØ,R_›ÏãqÀ±µÅŸ¾|ñ:Ú¡µ¹T* ~[ÏÈòòònݺeccãêê*ê¹·ÆÖJ‡·e”ãÚë1 —ö·ùa¤Ã¬3oTT³Ã!¨ÞŽÉÎm©’[".M}z`·‚ì»–I%ªŽîãx¹jruølM.«T"¯¼Æï±Ð댒^É)³3øÿK€Ï!çö²Üz'A*§Œ yåöïJÏR—?ÐÁ D,Ì*­uVQo[ÝcU}›AMzjaÇ}ã­¸?¦¹]Èy–RÄe“ß±Ûy/«Ч¢•/.·¼‘vîíjò×ÛlfÙH‹["‘7FÕ‹$äÔ‡ÑË)ú—SãóÊ%rª‡•p»‡ãÃÏ Ëe†ZÜÝÁIQY¥ÖúÛ<³K$_W=è8BHåjw€`³Yò²²=_Žt°3k?~A@I²|úسY„ž$)I²H’¤ä—ÇmßÖx<žŽŽ6¤W·ïšôî> ÉÉÉŽŽŽíÚµc³«¶lô56 ³om –ZœQ,.{ÿorgS¿Ž¦Z\Ö“äÂïoÅ”Ë,uù§&¹î NœÓË’$ˆmw2ŠÅ«·6Ôäþñ2s×ýDædð&£äFd.ì IºÐÙL‡—V$®)õ£Ï“p cSK5•Ç”±¥œäÛO¨"™©žßz'~í;#AXj1S»šù¹µ"IbOp’¾Ftvlf_*‘›hs 9¥ÒU×¢gv7wi¥¥Áa-ºŸW×>ï´ärÔÛÌ’ïG8ä–IÍtx¶úrš^x!"½H“Sæåj¢H}rg³ÔBñíè\p2Ò<ñ,­›•yia_ë?^f~ÑÛ22»éjnt>š1ÀA_‡Ç~•^¼îF¬Œ¢ µ†XF¥‰mô5Ö ní`$Êé»1y[ï&HduµU†‡@=©ÏqÏ-•n»›°n¨Ïñ³{Xd‰/¼Ê6IÌée9²!CÞ‹Éßz7¾\JÙèk¬bgg¨AÑp/6íõU‡Z ê¾Coc8«»…±÷mfÉw7ãR ELsÓÓàöi[ ’ùþö" §Åc¡;¥@´=(ñߤB˜ÞÍÜJ—¿þf,8¬_vë¾û±âjÑÆp£Œ¢|:˜bÐtqqqFfƒU Êåòðððk×®×¶ö‘ìã—ï¾ÿɉgi£Û1ë‡8Lìl¶àÏðaGžIäôúaöÌz—e¥§1üÈóu7bV²ÔÙtæ™7~Ç_x¶7fz‘Ù ¢²Ë˜³K$yeR;ÃæÞÞ¡é"J— IDAT±Iћ—³Ÿ>Ïyõ:/êM^ThÎë'9abH'‰O¨rfõ°È,–üž“_îðþ@Ïêaán§?ùÔkÏ_Bǵ7Î/—‹eà`$p4|{=fÌ/a:|ö¯6'Ÿ¥?ñòߤB_·V Åckqcs˘]Zim¸ëu,,&»lŒ‹1Äæ”·~ß:@_À™ÚÕl{P´ÒáióÙ×Âs˜–næÚ¶úo2J2Š%…岦,fÚÓ_{ ³Ôås~×è3[½à¸| =Lpà©ßñ—mL4'tlÕ”±© <êI}ŽûÕ·Ù ùå›G:zw0Ùp3–YÐËÒÍ\{Ò©W#Ž<pYóz[À—}¬îÇå»ï{2øÐÓs-¥ojVªûÙÍJø•»õê¿£Ý÷?¹—¿u´#àu,¬X$›}î­ïo/ !_4ùÔ«>{Ÿœzž±e”#ŸSÓ÷pƵðœ[Q¹¿þ›6âÈsåª0ÑáióÙIù"ˆÎ)ûâðür)³™®û WÛ3aýö?ýáVÜæ‘ŽFZ\Xìn­Á!‡þôlá…Ocf?­tx;Æ8xÜoÿ“ká9ûƵÑxXKi¯= ­}ð?ù´ š×õôd×à¸ü˜÷ß@{S­èœ²Êïª2°¿#rw6„PÓ¨ö®DÓ4\I«OM\úï#Øì!Ýl¦)ŠÒÒÒ$€ išyÌb±(Š’H¥\.WS YLjèª%–••EDD|üûªæd¬i¨ÉùåßT9E?N,|”ø®¦sD£Ó¡éñyå")µã^BßÖzÚ<6x$‘Q÷bóå}îEf^™4­Hü,¹ÈÉXV‰Róò±\Àe}D@ê¡L*È̇d>¼•qçZÚߥýý¿ŒÛÿd<Ë/Ö¡èOfÜ3Þ„N­v'Ò±9åŽFš ä³§u3ûözLN©¤\JÇåGg—‹$Zhl¾_"–S4]P.=–‘WùåRI€£‘fRH,£Ø$a«¯±ùv|‘HEâw¿ðãòÊ4¹læs= §EPlÞ›Œp2$å—§‰i u5Ø_¹Ûì¼—è`$hún§ž§Ë(ZFÑÿ$¶1ywí÷i­WñyåO’ eS*9õ,££…N‡§ð¨'µ:îÿ×ÓZxäŸTE»L.—RiEbÅÐ$ƒôŸ§݋͗Qô™°Œb±¬‡µ.óRàó ÉûNµ ~ÙÕ¨I¿¿Úy/ñߤ ‘ÍímIüùªŠ'ˆU“gg ¨\Œ¡ÆSm[}š¦)š&XÜè”\’Ía~ÿ»Ú÷u³èÛÞš–JeMüù(9.«dPÓ~nV‰¤¬¬œÍfwu0¥uä§#ª¨¬EWÛîQ$ªí4‡d¤ÉÍ(VÜî ¥àÝž´8éï¿vä–J%rÊX‹+‘S¥by¹ô]`"•û¾O—HF1µåR¹–Ru€UVU_t5W aµÉZ¨ŸTÄÉ€RÎr(ïQP`ó µXØ×úYrÑãÄBˆÍ-ëÛZºZ “óEÉïO$MnLNØêk‹åŠjr;Áw‰ï*þ[h¯¶“¼¹·jPë§ß0.Íx÷»º\*ç°ß}ÅVõ³ÊÖY$.ɬôøL›DeÅâ'IE__ެ°¾T"×p˜ŠEƒ÷¹È.‘( €™O1ê§"á*ƒO.uÜþ¨ºüXèò™ÚÿN¦~n­fžy­¸/¼Êº Ôˆ ÊÀàzD΂>V ŠªK!Ô€jêY`afòŸN³=:ÌÖnÊ §‰ìÛXê±H’y™Ïçêëë“$Éç²xËb³9:::IA,—Ëmº||¤È¬Òb‘lˆ“Xù}íÞý°ÿ;"ÇÏ­•¾ŸC.v·ŽË/תÿöÈÜv&ZôÖüÞV¡©E8$aeAˆØš¡Ù…1ãuc¾dÅ~‘äž,3 YŸHÝÀ’~Ö¾ÌóK¨ßñ~Ç_|~ö&—e.ä'æ‰\Í´-uùkÅ[s!i;àh¤©ÂÁP3&§ŒùŒçsH ]^ô»JóYëhôÿ£W˜ yl’T\1…an©´X,s0ÜÉ»ž8Ù5·Tz+*W*§™Š|G#ÁÕ÷S 9ÿÿ;AF±¤X,# °70Õ†JiÆä”*¾ÖÆä–Íèn¾þfl™ô]ÒÚ<¶©©È(–0  5K%r•×L½lt÷…$3|¼~˜ýƒ‰ìirao[=Õ†§ð¨'u;î‡&Ïìn~ÔÏEOÀÎ*–\x•õ ¾ ³…Ί¶<6™[&]w#¶‰‡hEê ºïÿ$ðíÖæB~©Dþ$¹ðvÔšòEe—^y“}z²kZ‘8:»LÑ‘ðyJÑíè¼Ó“;d—J‚bò*§xéuÖÖÑŽ÷çw ‰+Xõßo˜ç_dÎû̪ò ʹeÒ/·/v·^9Ж¢ámfɦ[ñ°ã^ÂÚ!v§&¹ŠdA±yýìô ½HüõåÈEîÖ›F:Äå–-ø3\ñeãƒÁ+“S0¥‹Ù†aЦcsÊ_ŒLÌÀÔ.f†š\E7‡à¸ü//TJ¬ÊÀ@FÑ·¢r½\M¤Tu0B pÛö°VÒMÉi™Œ*+%²àË/%‰P(”ÉdEEÅ………ºB!Ç+‰Ä äêÛi "^¢óS£vN-/«¢Y¾¥¥eïÞ½+¯§(êÅë·¬qßqu[ೄ>9mL4¿bÇôt@*‡@=áqGH ôn{øQrhjƒMìÕ<íëAõêÕ‹¢(`GSZ`FRgH’ 6lMÓÙ9ÙÆFÆ&&&ÌóÅêþ¯r!µõáÝ4EÑR1)*l-¶ÒIK“ x<›Í.))‘ÉärJ®Á×Êdb‰„¢i‚ Ê$²|iGì'ôÌVþYxu{ÒÓ;rù‡öËÌÌŒ*¤…žë°j¡f‚âÀƒdUG¡Öð¨'<î©¡¹çß~x#„ªk˜éd"­Ò´%½[ 58gΜÎËÎ^ºlI’@’¤HT*“ÉhM$ÉT¹ ¸¬–ì u JCGè³¾ûÔœ?7¥Å¼Q¬®PŸW\\ž ='9÷¬kJ¡†÷6gS1<ê ;B!„꣦Ú‚ €ÏçiqŒFgXjÈÇx޳µ±âr8ÅÅÅ¥%%EAÌ0„‘S"º_ÒÕB;µP|)^D½†š#[Ï9¢÷êFòå½Eùÿé…%“É"cbó-{LYN|:Ú!„B!„BÍSM?­i@lJË(SÛŒ«Ë—s >¡”Kë xFB­r±$3¯ÐÕDƒÇå¼MÉÝZRBs.Ç–ËI.°ê5ƒ«§ýÐÖ.ƒKoN :K’$MÓ‰‰‰q-£QßbW„B!„B¨!|àÁ; ¥è¼2i‰D–V$Ñâ±´yl-S ’ÄœòâQa.ÑÎDWS#*%Sš™Ast$\mRÀ"Ivƒ DOšƒæ8÷›A–录SÐ^ÇŒ¯Õ0{F!„B!„PmF%ЦERZ$¥ D2KÂ! I°à±õù&7ó€]@–RVÚvæ„”"d4 ?à'ÁæÒ:­ê8Ô!B!„B!„ª÷qöå-§h‘ÒŠª¶Â¹@B!„B¡O ©êB!„B!¤bX;€B!„B©;ö£yUB!„BUíÙ³gª!µ€mB!„B!u‡µ!„B!„ºÃÚ„B!„BHÝ}ÜŒ†*TVVœœleeÕ®];‹¥êˆB!„B¡â¨Éd·nÝ:þ¼\.€ââ⬬¬E‹7NÕ¡!„B!„þ½ó‹âxø»{*½*Š"M+Av v!Ø¢bIÔ¨ _;–XcWԘػA좢ػF‘~ÇÂq\Ýòûcâþ.p "ƘÌÇçñÙ›òî²»3óÎû¾ƒÁ`þ ÔD;ðèñ“†Çù<’$H’ H’$‚‚$H–eµJ™K}‡:uê|¤|wïÞݹsgYY—bjjjjjº}ûöÝ»wÿôÓOMš4ùÈ&0 ƒÁ`0 ƒùSíCÓW  ­“ñiI™ºL­©(•†¡FK1ZšÖÒY™’Fek~ò1’åæænß¾===]ïY;;;–e'OžÜ°aÃeË–ÙØØ|L[ ƒÁ`0 ƒÁü—©‰v@ à‹‹Jî©ä¯U"­š¦ÊÔ”BŨ5´Z[&SJ‹5ry=†ek&Siii\\Üõë׫ÎFDƒ Ôjuß¾}ÃÂÂ&NœHDÍZÄ`0 ƒÁ`0 濌~íÃ0,Ër“í¿Ìº ÂÀÀà­² ‰…iq£eYŠaµjª°D)SR*-£RÓ*«å1 SŠ‹‹çÎ+—Ë«™_ ¸»»Ÿ8q¢I“&;w®A‹ ƒÁ`0 ƒÁüÇÑ¿£a^ž8éÞã+OÓ®¥¼¹õ"ãÆóŒ›Ï3n§dÝI;—š-cŒ¦·uó¶4Žpä¤K Ó%EYùòB­¦X E+´RÎP¶FÚ­[·îß¿?33óƒJ988¤¤¤Ô 9Äœ9s¢¢¢*;›––foo_1=66¶OŸ>5nƒ©š-[¶ˆÅâÏ-ŇQõ«‰‰‰žžžööö÷ïßÿ š½¼¼nß¾ýÞl!!!ï5;ªš . <øcjÀ|øCŠÁ`¾hþQßêÕ«ëׯߠAƒêÔîܹ¶mÛ~HÿXÁ0˜ÿúµ|>ïV^)Ë3|PJÏPÅ>-Úq'oûµ¬­—Ó6M]wòÙìß®MXv¼´ð­2[¢)xK—*XŠ‚ ’ H–Ö2”–ajâY R© Åõë×Ï;WPPPý‚¦Í} ­[·?~üGVÓ¦MSSÓ &覿xñ¢sçÎæææ­ZµJNNþÈV0µÅ“'O–W`ÅŠZ­¶vÒh42™ÌÊʪ I>\E ¹¹¹k×®ÕMafëÖ­?~õêÕòåËOœ8ÁzùòåòåË«®°V˜?þòåËÅbqË–-kVÃØ±cW­Z¥÷Ôµk×ÊÊʪ3¤hÑ¢ÅóçÏõžêÚµkFFƽ{÷j&ÞgaÆŒnnnææækÖ¬A‰‹/6ÔÁÈÈ}Q·lÙâççgeeåææ¶téR–e@©T;99YZZúùù9sFoC÷ïßïÒ¥‹………³³ó/¿üW®\1ü+ÇŽ+WjæÌ™†††»víâRlèùÔýúùù¡J¬¬¬ºtéòøñãZ¾S˜/“Áƒ÷ÝwÜÏk×®ÙÛÛçææ~F‘0˜êó·u|2™lñâÅ÷îÝËÈȨ¹¸Ae]‰Þ~ƒÁü3ÑïY@’¼2•ô4]*4‘–Ñ”Š¢äJZ©¦Õu™¦PZ¢*•J%­Ñ² 0,KÓ¬VËPZ–¡Y–eØšØp]¸pÁÙÙÙÇÇÇÈÈècªúD¸ººººº~d%ŽŽŽsæÌ9~ü¸n"Ã0C‡íÖ­Ûï¿ÿ7pàÀ””ssól óñx{{{zzÀ;w233 Aðx¼ÚmH*•ZXX‚Ê2äççWŒÓÚÚZ­V—––ššš¢”û÷ï …Â¦M›Þ¸qÃÊÊJ*•¢t†a._¾laaaggW‹— —¬¬,ww÷OTù/¿ü2|øð÷fËÌÌT(è益aÆmß¾½U«Vµ*Ý'$44tòäÉæææ/_¾2dˆ§§gÏž=£¢¢f̘2¬^½úÒ¥KÖÖÖ ‹—,YâããóâÅ‹aÆ9::Ž1‚ÏçϘ1ÃÃÃC$]¼xqذaiii–––º­H¥ÒàààY³fíÙ³G¡P”””@@@ÀÛ·oQ†Û·o‡††vëÖ­¢„®®®û÷ï1búÛ¸qcî”î‡tË–-aaar¹|áÂ…£Fú²Ô4˜OĆ Z´h1hРöíÛ«Tª &,]ºÔÑÑñsË…ÁT‹¿­ãËËË355ýº)Šâóߪ¬²®Do¿SëBb0˜ZA¿íG–ji3ã…º˜aŠ)ªXC””抋ò$Ej…’Q«­†aX‚à‘@ÃÒ,£eY†eÙšÅ(WêÍ›7'Ož|ôèEQ5¹¾ÊIMMíСƒ­­m¿~ýŠ‹‹¹ô´lÙÒÞÞ>((HwÇ„7:;;»¸¸lܸ¥p±E 2ÄÉÉÉÞÞ¾OŸ>œÊvÔ¨Qßÿ}pppË–-³³³+ŠÑ·oßÞ½{[XXè&Þ¿?==}Þ¼yuêÔ7nœî2/æ3BŸÏçóùR©ÔÎÎ#Õ@vvö¾}ûÖ®]ûÛo¿q‹Z111·oߎݴiÓÙ³g 9²mÛ¶¸¸8ôH+ŠŸþ9))iÛ¶mk×®½ví*¨;ù¿páBLLÌêÕ«wîÜ)‘HàâÅ‹¤ë×a½}}›²5ò,¨MÓÏŸ??uêTiii­Tï烂‚rss¿ûî»}ûö¡ôK—.EEEýöÛo999AAAÇGcÓÒÒÒ”””ÔÔÔøøøeË–]¹rE·6–eCBBž?žžž^¯^½)S¦p§N:µcÇŽû÷ïûùù-Z´¨šâ={ö¬qãÆÆÆÆè§Ï³gÏjá²1µG~~¾î\:;;ûèÑ£íÚµ›2eŠŸŸ_||<¨Õj™L&—ˇ:bĈ‡&%%õîÝ{ìØ±eee¯_¿FõPEĨQ£†zãÆ¢¢"J¥œvÀÎÎnôèÑS§NurrB³ÜÀÀ@‘H4tèÐ &øúúêm¬­­9÷œ7n8::"_D‰DâèèhffVTT¤V«oݺծ]»ÂÂB[[Û½ô*Ý¿ßÜÜüôéÓH£áæævåʱX®W<’$ ûöí «úräryll¬¯¯/Þ‰ƒ sss1bÄöíÛ7oÞŒ—,YrãÆ«W¯¦¦¦–––.X°æÎ”——÷æÍ¬À| þ™Ÿ··÷‰'¬­­Q¶ÂÂÂàààÈÈÈœœœõë×GDDäååÀ¬Y³ EZZÚ‘#G8‡¯¬¬¬!C†Ì›7/;;{èС}ûöEº6¸råÊåË—‘ŸW5…¯¢+©ØïpèlÈ!xï-Â`0µˆ~íI’ P¸õJ_¬Ê–¨ó‹¨9«Ö À@,Ͱ”¶¡ñÔ÷1ùhj46Ðil7çÑ]êìÖ0 Y=‚¬É¨®²•"…Bñ1qËñøñã¼¼¼3fðùüÀÀÀ®]»¢ô_~ùeâĉ͛7çñx“'OÎÍÍES8†a¢££E"‘——WDDÄÁƒukááá&&&†††³gÏæ–`ذahÆÕ³gχVS<¹\ÎYƒ€™™Yõ7qÀü hµÚ¢¢"ݹôÅ‹œ ‚ðòòR(J¥R*•uêÔ‰ ###‚ Ú·o/‰‚000@f2R©ÔÁÁ¡C‡ÀÞÞÞÒÒÙiK¥R®þ¦M›ŠD"’$ÝÜÜÐz‚\.×h4\T½­€ ÒÈd² M=ÔjuII‰­­-:{ãÆ&Mšh4¡PXï•}•t4h²Yˆˆˆ033KMMýÐÖuyûö­‘‘‘®‹‡ŸŸ_›6m {÷î™™™hyG¥Rݼy³S§NèééI’dË–-û÷ïÏEuBõè.ýó™6mÚ7vîܤûõ€={öôëׯ\",Z´ˆeÙÑ£Gs){÷îMNN^ºt)zhËåÏÉÉÙ·oß¶mÛ^½zemm¡{6>>ÞÌ̬cÇŽ•I¶ÿ~•J•PN¡Ë¸qã mllvïÞ½dÉ’÷^8æ¿ÃÆ/\¸ð¿ÿȶ IDATý‹¸¶}ûöE‹ÙÙÙÍ™3çèÑ£ ^¿~- [·nýYEÆü;ù'w|‡jÙ²å Aƒx<^Û¶m;wîŒlâ:4wî\ccã p1_Ž?,"##ÍÌÌ.^¼ˆNMœ8[ª/|]IÅ~GWàŠ‚õîÝûùóço#ƒùtTw€ Êÿ…÷˜óë¥Ì2%<øÓ‰€a(Š¥´,£%X–Ç#eûíט˜¸v횃ƒšÆÔX *ütí¬>’ÜÜ\'''·ÊÙÙdffÞ¼ysïÞ½\N‰Dbggg``ÀÍÄêׯþüyÝÚhš^¸pa|||ii)I’r¹\­V‹D"àJ¢þ¨Q£âââàÇD 111Ñ5”())©W¯ÞÇ_5¦¶J¥|>ŸóÊV(yyy*•Šó‘ ??¿Aƒèu(,,422âÌ ѳ!•JuÝ …¡¡!èØH¥ÒëׯK$š¦µZmÆ  ??ßÚÚuº•µÖÖÖ<€+W®x{{£óóómmmÓÒÒÞ¼y3f̘çÏŸ×Àp>ðUrqqÑ-·qãF±XÌãñÄbñE!­ˆ………B¡ iš'qoÇeeeæææIII¾¾¾è&ß½{7::úÅ‹,Ë–””pËŒ¨ž/+Ò‡©©©©©éÀ¯_¿¾zõêÙ³g£t´¤¾9º¬[·îðáÃçÏŸ700à------§NÚºuëfÍšqnCCÃ^½z!ûÒ9s渻»—””˜™™¡³{öì «b©ßÓÓÓÐÐ0::Úßß¿œ/•.Û¶mûúë¯Õjõ¹sçú÷ï÷î]''§¼˜'uëÖ­S§1D¡PŽ3½ò,Ëj4†aV®\9þ|[[Û™3g6ì³JùòOîø8233ïܹӬY3ô³¬¬¬E‹r¹\&“qs¹¹¹õë×çÊ6hЀs.spp¨Bø´´4Î: ´´ݓʺ’rýÎŽ;&Nœþþþñññz366>tèÐ?þø17 ƒÁTýÚ†aù|áйq €à<h–e(X¶° ðZòµÇ»ºº2 ³yóæáaa666¶66hn\ªÐÔ¢‰©££c~~>÷3??}þêÕ«7lذrn]iii*•Š›Îeeeé~+`ÿþý.\8wîœM^^^£Fªð•ݱcÇŽ;ª¯I“&iiiJ¥Íažô*éòæÍ›)S¦œ?ÞÇÇš6mŠÞ###µZò '‹rT¦|¬W¯ž™™YZZš››[sn6wîÜaÆñx¼I“&q¯mjjªÝ§ìô7À²¬nÀ”øøø:uê”[Òß´iÓ–-[.\¸PÙURN;àíí]î#ÌݱÜÜÜK—.•Û#£"aaa?üðCuÌDE"QHHˆ©©irr2žÝaôbdddaaÛ´iSÝtGGÇ_~ù…eÙK—.õë×/00ðoˆ·ŠùOñEt|õêÕëØ±cÅïm:u$ RÑrWáèèÈ&€ÌÌ̺uë¢cWxd«¨Ý®¤b¿3jÔ(@¤ Á`ðàÁóæÍã 40̧Fÿ熦™Î~M†µÚn\hÛqýÛŒÒaÜ vcúù}ê?.¼k䘾##ú2 Û©Sçyóæ­ZµjíÚµ'M266 …µ¿½Öñññ±°°8r䤧§Ÿ:u ¥;vÍš5÷îÝcYV&“;v i+H’ŒŽŽÖh4Ïž=Û½{÷Àuk+))qpp@s‰r.aï…¢(•JEÓ4MÓ*• EªkÙ²¥³³ó’%K ÅÎ;ÅbqHHH­\8¦VÐꀙ™AOžôUâÉd(4ÝÙ³g_½z…Ò›5kvýúuäñ¸aÆŠ-ÚØØ ¨{å ¢{÷îW¯^­Zà3gÎpÚ™LæííÍãñ²²²¸` põêÕ=zTï|~ÊÊÊ6nܘžžþöíÛøøø={ö DÅ%ý_~ùeÕªUÇ·²²R©Th3·;vL,K$’eË–¥¥¥ùûû@NNNTTú.EDD8pàÉ“'J¥réÒ¥mÛ¶åÌ+öíÛçççWn‰¬"#FŒHHHàî¿^†¡(J¡P y<<Éóôô‚ Ð\…eÙ 1]Ñ9VGªJmhš®¢`EH’Œ0aÂÆíììBCCQz—.]–/_>qâÄôôtSSÓŽ;öë×IåæææêêÊãñ~øáä´Ì1bĈ‹/úùù¡(µÕ–-[ÆùÖîÙ³gÊ”)Ë–-Câ?ÞÑѱQ£F‡þ²ŒœÿõäççëÚáñxýû÷¿xñâ… x<žƒƒCpp0˲å4\´ß¼ótppظq£H$jÓ¦ 2ä´(œþÖ­[mmm‘¡*èééyüøñµk×vèÐùÌ—k5„b¿«ÕjÝÍùòóó5jB¡Õ‰D­ÙòÚ‡¾J>>>ááámÚ´iР··7Z‹€víÚõíÛ·M›6z•b#FŒ@a{öì¹sçNÝSß|óÍ‚ ÆŒS™´©©©ygÀ† FŽéàà`gg§«Ø¿¿ÞñÙ?wáÂ…Ÿ~úI©T6hÐ`Á‚\Ì?½Kúk×®‹ÅH+½zõ:zô(I’kÖ¬;v,ÇóôôL–––±±±Ož<?~<ЏŽ(§Œ(--ÍÏÏŸ={öä!ŒÁè"•J‘‘?æï¡ÿþŸ[ ƒÁ`0 æJ¥Ú‚ ø|~ýúNAèNË?>²@5iÚ´éõë×÷îÝû¿ÿý¯ÜŽ/E¥§§0`òäÉ8àæËOV1 ƒÁ`0Ì?ý{è‚\z8þÕGxxø›7o¦M›f``€b¼¿yó†eÙ¸¸¸iÓ¦aÕƒÁ`0 ƒÁ`0OUqþ!ðx¼Ÿ~úiÞ¼yb±˜a+++33³Ï-ƒÁ`0 ƒÁ`0ÿ¾íÂÀÀí¾†Á`0 ƒÁ`0 ¦vy¿gƒÁ`0 ƒÁ`0˜7X;€Á`0 ƒÁ`0̬À`0 ƒÁ`0 æ¿ßÀÀàsË€Á`0 ƒÁ`0 æs‚m0 ƒÁ`0 ƒù¯ƒµ ƒÁ`0 ƒÁü×ÁÚ ƒÁ`0 ƒÁ`þëð?·ÕE¡P¤¦¦fff6hРiÓ¦<ïsK„Á`0=°,û¹EÀ`0Ì¿ öܱÞÄŠy0Ìñh(Š:sæL\\MÓPZZ*•Jg̘1hРÏ-ƒÁü‡Ð;Òªbø…Gf ƒ©X–e†aî˜;@} wÀýDy0ÌQíÀ»÷ÞR_ äóH’  I‚ I’ ’ Y–Õ*e.õ-,,>R¾[·nmß¾].—s)¦¦¦¦¦¦111;vìX¹r¥——×G6Á`0˜Ê(7Ã×ýYÅ©ÊR0 ƒ©Ì;à}Útª2íî˜0ÿ‚¨A©šhš¾*%Ú:Ÿ–”©ËÔ ™ŠRiŠa´£¥i-•)i`T¶qNx *çÈÉɉ‰‰IKKÓ{ÖÎÎŽeÙ±cÇ6nÜxÕªU¶¶¶ÓƒÁ`Ê¡WP½?õ¦`0 S†¡iú½Úî”nfÝzj6_Â`¾DªwUñ"ÔD; ðÅEoï©ä¯U"­š¦ÊÔ”BŨ5´Z[&SJ‹5ry=¦¦A™L¶wïÞ«W¯V ˆ (Šž={Ž1â»ï¾Ã/<ƒÁ|<ïUèþ_ÙÏŠÇ ƒÁÔŒêØÀ_-°gSºÃ³r3hýÚôjqYÿR† Þ*µM,L‹Ë-ËR «UkT…%J™’Ri•šV©X-¯fïäÛ·oüñÇÒÒÒjæîîîÇŽóööîÒ¥K ZÄ`0 GŠ€êÿÕ0%À`0 ¦šTa;Àe`Y–$IÎv,û pW…ùÒ©Áb9÷Ø£²úµÙÙ9¼É& Mx<Ÿ$X† H‚$‚$’0˜ÞÖM.“Y¨–_ΡåJªLÎ(ÕÀ¬†¢ Z)g([#íÀæÍ›÷íÛ×¼yóúõëW¿”ƒƒÃóçÏ?H;™™9pàÀ—/_Ο?êÔ©UgV©T†††¥¥¥&&&ÕoƒùÖ­[7hÐ GGÇÏ-ÈEQÔªU«*ËpþüùñãÇ¿}ûöüùó­[·®~Í7Þ»wo›6mªÎÖ£G¹sçT¿ærœ;wnóæÍǯq _4•©ªs · nÍ-[¶Ü¶mÛýÝ1 æNtt4EQ‹-ª,Ã¥K—¦M›V\\|ìØ±-ZT¿æj~3 0sæLÔ?®[·nÓ¦MA¤¦¦¾·þÄÄÄÅ‹_ºt©ú"ý=è¬2í× qžH) «¨þœ›!c¾t*{Ú«ól£²úµ|>ïV^iW›¥TN±R!SªjVË0”–ÑÐ ÍJ%o3_¥M óSfK€¢YJ @<>Ò°´–¡´ Sõ›R©T(ׯ_OIIiÙ²¥µµu5 ªÕêjhãÆþþþ·oßþpk‡)S¦?~\"‘888Lœ8qæÌ™žžuåÊ•²²2__ßuëÖy{{. ¿8îä_ãñh––-E[ ìÝm=k·‰‡þþûïå ‚øßÿþ'j±!FSRRbccS…$Ïž=>|xerrröìÙÅ¥0 ³qãÆöíÛÇÆÆ6mÚ444JII9pà€››Û°aÃjñ**2{öìŸþ¹ÿþ5®aäÈ‘º×ÅqõêÕ²²²ê¨š4irøðá&MšT<Õ½{÷üñÎ;¾¾¾5òo¦_¿~ÜcillŒÂ¸>|øPwºråÊ3f ã»wïN›6íîÝ»æææóçÏŒŒT*•}úôyôè‘\.www_¼xq¯^½Ê©JKK§L™ræÌ­VÛ¡C‡uëÖ¡ø/\†’’’úõë;99=zô5TVVæááÁ²lff&üðÃõêÕ£izöìÙ[·n>Ÿß°a訨¾}ûþ}÷ ó¥ñõ×_ÛÙÙqjÇ›7o†……%'';88|^Á0xgÁ^Å2õ¢E‹.\ ´š&º4MOž<ÙÕÕuÊ”)óܼyiš.--]¶lÙÇ­­­«ÓMÓÜ\ºVP©TaaaüñGYYYãÆç̙ӵkWøõ×_÷ìÙ“––fee>}úôª'*zC·¢2“Ðw€þ33žóc0U¸”C¿v€$ye**éiºTh"-£)EÉ•´RM«5ê2M¡´DU*'”JZ£e* `X –¦Y­–¡´,C³,˰åíSTTtáÂggg##£©J/™™™:uªõj€¢(>ÿý 4uêTssó/^„††zyy‰Åâ-ZDGG›™™EGG¿yóÔªÉá;<ìˆWò4CHÞ²¾#j];ЬY3¤¯¹qãÆ›7oÐ\š W» I$KKË*4b±ØÎήŠlllÔjuii©©©)J¹sçŽP(lÞ¼ùÕ«W­­­óóóQ:Ã0.\°´´´··¯ÅKÐKff¦§g-ÿQ8¶lÙòõ×_¿7[FF†B¡Ð«@„……mݺõ Ò@LLÌÈ‘#á¯_üºuë¾zõ sÏR~~~÷îÝçΧP(Š‹‹€ÏçGEEyxxˆD¢ . 80##ÃÒÒtVf–,YòäÉ“›7o;væÌ™;wîÔ]·A#9¡P˜œœìïïϲìï¿ÿnooŸ——‡†zH+DQÃ0ƒ^½zµV«=}útddd«V­þ†Çó…²|ùò:ôíÛ×ßß_­VO™2eÞ¼y666E}nÑ0˜?'¥U<ÙÙÙ...5{\išFßL†aôÖðÛo¿ 0ÊÉÉ155577¯f[èË\ÌÕÙ²,‹"‘èÊ•+#FŒxøð¡……E^^Þœ9s¼¼¼ÒÒÒÆŒcggWõR„^Á¸[5[+§ IuFEQô{. û`þÅT1,çJPRo,ÕÒfÆ u1ÃST±†*()ÍåIŠÔ %£V1Z Ã0°,Á#†¥YF˲Œ®Vïƒ(WêÍ›7'Ož|ôèQíÆŒsâĉٳg;;;?{ö,??èС¶¶¶ 4àV'X–]°`]ýúõ÷îÝË•MKKëÞ½»………‡‡Gll,Jܾ}{=FŒááásìØ1SSSggçõë×ë•!  aÆ...&&&hïïïÆè .ÌÌÌÌË˫ŠÿwCkO ”L™”J­‚V×þGŸ >ŸÏçóóóóÐ1R deeíØ±cùòå111ÙÙÙ(ÿÚµk¯_¿¾k×®Ÿþ9!!¡   ..nÆ »wïFtYYÙ’%K7lذ|ùò¤¤$TP"‘p;qœ9sfíÚµ?ýôÓÖ­[Åb1œ={öîÝ»´˜# ûöí+‰ÒÒÒh ¦¬¬¬¢££§N*—ËW¬Xáèè8tèPš¦ÕjõO?ýÔªU+//¯iÓ¦•––Ò4ššâæææááñÝwß}nÙ1ÿNRSS{õêÕ¸qãáÇsßÀÇwèÐÁÕÕuàÀ¯_¿¦iºC‡……… èÔ©MÓ+W®lݺµ‹‹K``à•+WP©uëÖM›6 Ëd2{{{…BAÓ4K:t(!!aÆ ­[·ž>}º®E%&&úùùÑ4ýôéÓrÙ>|اO77·N:]ºt )..;v,J|üø1¼[“¿téR`` ««kppð£GPbóæÍ×­[×­[·öíÛW&¼.$I¶mÛÖÆÆ¦N: 6Ôjµ9994MÏœ9³}ûö–––¾¾¾ÁÁÁwïÞ­XV¯`aaa[¶lA¸›ü^*‹;ÀVàoè1˜ÏEužv½§ô+y<ž¢@^×ÅYñôuA©–Qj¨²2Z¡hµ†V–š2k³a½ZA|X–¡h–¢š¦TJ+A[#Ï‚ŠÐ4ýüù󌌌Î;s« ɯ¿þZRRÒµk×ÈÈHèСC«V­222 zôèáêêÚ·o߸¸¸]»vݺuËÆÆ&,, d¦_¿~}úôIHH¸sçN¯^½\]]‘KØ… Ο?¿k×.–eãââ<<<î߿߭[7?????¿ŠbÌ›7oË–-EEE 62dH¹³7nܰ±±ÁëiÕG]ÊhPÑËj-ÁÖòrþ_‹ÅîîîÜϬ¬¬¸¸¸4lØðÉ“'GŽ™2eŠZ­.)))--ˆˆËå«W¯–Ëåýû÷ …111/_¾ôôô”H$EYPPð믿z{{[YYI$dÀ²¬½½}çÎÁ¹sç.\¸Þ£G'OžŒ1¹èmlllòóó5jW¯^urrjذ!äååµjÕÊÜܼ°°ÐÔÔ4999""bóæÍ5x؆ >|xrròåË—CBB&Mš‰‰‰3fÌHHHhÖ¬Ù†  t÷îݧOŸZ[['&&zxx€»»ûÍ›7mllvïÞ=dÈŒŒŒ÷Z ………?^¯gAVVVqq±««+—’˜˜xåÊ•¶mÛž>>YYYíÚµÛ¼y3 ò²l·nÝ}~ûí7dÇuP¹í:#­VKi)xß2)'Ø>Ì?½z/:Å¥WæY@‚BuâÖ+u¾LU(gi–¥´À0O,ÁÒ PÚ†vÆmÊJd¥r9Ã0–,ËjµZFóöm1AÖľ²÷S¡P¤¤¤| [ß§OŸÞ¿ÿâÅ‹|>ßÉÉéÛo¿=tèPß¾}ccc'Ožììì óçÏGn½=ÊÌÌ\°`@ h×®ÝðáÃ÷îÝ‹´­Zµ ‚ Ÿ´nÝzÀ€ÉÉÉzµ3gÎ;vìõë×oݺU§NÝSYYY“&MZ·nê0U^”v#3™Gò3%y^î•Á¡R³wÓïýÆû XbpÀ ƒÚ '©Õj uçÒgΜéÔ©š‡ûøø$$$(•J©TjllÜ­[7‚ Œ ‚ DK»h4ùùùuëÖEOŽƒƒƒµµuaa¡••U~~¾‹‹ ѼysÔŠ§§ç¹sç@.—«Õj.*‡ÞÖ #Ù IDAT mmm‘í€L&»sçÎØ±c@­VÛÛÛÛÙÙI¥Ò4mÚT£Ñ [ƒ½>ÌËË‹ŠŠâóù]»víÞ½;Jß²eË·ß~Û²eK˜:uê²eËÒÒÒ7n¬[vèСè`Ô¨QK–,yþüy«V­>TŽ·oßéºxøûû·mÛzö왑‘Q\\lnn®R©nܸqèÐ!¨ì=Eõ)ÚñãÇÛÚÚŸ8q"((èÖ­[Í›7···‹‹óòòÊÍÍ1cƘ1c<ÙÙÙÏŸ??s挻»û·ß~;lذk×®¡zâââÞ¾}{ìØ1䆠«`YÖÛÛÙL@Ë–-7oÞŒF´ºÙÀÄÄÄÏÏïÌ™3YYY½{÷F5#íÒ ÓÐ#GŽ9rx<Þ¬Y³LMM±•8¦j-ZÔ¥K—ï¿ÿÞÞÞ=-ûöíÛ¶m›……|ûí·³fÍâñxéééYYYŽŽŽ^^^ø¹ÂÔ:OŸ>ÍÏÏGökmÚ´ @fÿ{ö쉈ˆ@ÁVÎýõë× 4@FòèQìÕ« O« 6¤¦¦z{{£ån”~gÏãñ¸o&wPN’ÂÂBΟ+ ñññ^^^(‚L‹-üýý¿wï^¡Phooöûï¿SuòäI___äo;|øð;w&%%uëÖ ]g«¯WøŠ÷gݺu2™ìܹs|>¿ÜêýÚµk†4hPÅkÑ+X```TTÔëׯëׯOëxÀ;]€®võDïõ,Ð;ÑÀ>¼˜åòrwejö]„ŽÊ´¥Pþ/¼Çœ_/e–)äÁŸN CQ,¥e-Á²_$îÚµsãÆp÷î]GGG‘Hô1Ú*üt Œk‘ŒŒ ­VË}à4 :ÎÍÍE ­€æ](ÑÑÑ‘óàuvv¾wï:Ö tûöí¹s禦¦²,[\\Ÿoee…~*ŠÜÜ\•JuëÖ-”"X,vvvF¯CAA‘‘7Û”J¥hn/‘Htýðår9ÒÖççç#Û‰DråʱXLÓ´F£Al±XlccƒÞáÊZ[[Û»wï@bbbóæÍQ‹b±ØÄÄÄØØØÎÎîÅ‹iii“&Múã?ìííkÐ;æææ:99qîˆÜ›’‘‘\*¸œb±¸œv`ß¾}ëÖ­ËËËãñxyyyÚº.È“Sp 'är¹¹¹ù¥K—¾úê+´Ü¡÷=TO t%Ÿ 4bäää#GŽ í@¿~ýÀÕÕuÛ¶m_}õ•Z­‰D†††ÁÁÁH³`Ággç’’333°´´´°°øþûï›7oÞ´iÓöíÛs/–e### ‚xñâ…ÁÂ… ÃÂÂŽ=ª«>@£=Š¢úõë·oß¾ìììµk×j4VÇ}”Þõë×oÙ²e4M¿zõ i7ºuëöynæ ÁÚÚÚÄĤQ£FèqR©Toß¾>}:zå¹Å‰¨¨¨5kÖôíÛ×ÊÊjܸq}úôùÜ‚cþmäååÙÛÛs_¶zõ꡹}NNÎýû÷‘ÞX–‹ÅuëÖ…wŸ>8qâÄîÝ»¥R)I’R©´  }u?¡ðN;ï‹;`bb¢R©Ôj5—™«'++ëñãÇÜwU©TzzzÊd2¹\îàà€ò8::¢üyyy\"JÏÍÍE?---¹ôŠÂ¿~ýš[xúô)ÃÄÄÄÄÄdäÈ‘!!!îîîÜÂÞŽ;öîÝËãñ(Š:tèÐܹs eË–Û·o×+˜@  ŒŒŒ¬B;À¥”×P±@T=ÔÁF˜/šª­Êe`+ìD)úµ Ãòù¡sãÁ x$Ð,ËP,°l´ ùjòƒ\]]†Y³fMXx¸ƒƒƒƒ½}9ß×êS…và)öœœœLMMŸ={VN©áèè(‘HÐ1w€¾˜Z­;޼yƒ¾ûåÄÅVVV/^¼àêÏÉɱ¶¶ÖjµÈf¥ëÞÁÁ}äáÝÞ ºm±,ûæÍdÿ¸oß¾½{÷îÚµËÌÌ Uد_?¤Â®B0èÙ³çºuëüüüЭÐu(};’$ÉÝäY —ʴ؈óESñÁÖ;äãËýD)úµ4Ítökâæ $ŸGÀŸp‘h­V[&c¶S§Îþþþ*•Š$IK++Š¢DBa­Çoÿ¤x{{{xx̘1#::ÚØØ855U&“ùùù 2dÍš5#GŽ444ä>¯>>>NNN‹-š3gν{÷bccÏŸ?_±N™LÖ´iS—™™yüøñÑ£G—ËPVV¶}ûöss󤤤;wnÞ¼ »té<}útd+!‰ð§ê½ˆøËú¯ü_ì,VkcîÀŠË¤Òçl˜Ëø‰='|ŠæÊÍ¥ÍÍÍ ‚xøða³f͆ÉÍÍ­S§Ž™™™X,FfØð×ù¼D"±±±AAAAJJJÓ¦Me2Ù‰'I’D! ‚@±ŽÜÜÜH’LIIùã?Ð\Z¥Rqïe­€µµµZ­þý÷ßÛ·oÏ9¢Epvvþúë¯ÑB,×̪¿yóæA=ztÆŒnnnJ¥ÒÝÝ=::†Ž"888p,wìØ±‰'ÚÚÚÚÙÙmڴ髯¾ªXgLLLXX˜£££½½½ÞiÇ;{öìÂ…  …³³óâÅ‹#""àôéÓüñÇüÁmpçή“ÀT…±Å꯿u­0{“÷úëÆfôŸþ‰Ú‹Å\,àñxƒ>wîÜéÓ§y<^ݺuûõëDzl~~>§DH$hb\zQQ‘±±qݺuþùg‘H€õ9·›&Mš¬_¿ÞÞÞí£‚ÞÞÞ\¶lY``àW_}U±uÔŸÏ·°°P«ÕºO©X,F¡û„B!ª“eY]3‡‚$É#GŽ|óÍ7ëÖ­³··ç†ݺuûùçŸÇŽûúõë:uêtîÜ¹Ü ¾Y³f#FŒhÑ¢Úµ”»9íÛ· mÑ¢…££c¹bôèу¶°° B®:ãÇŸ3gN¹½ tIIIœûCeïéÞ½{cbbjp7>¿üòËäÉ“5»»û¾}ûÐü<##cܸqÙÙÙVVV=zôX±bÊܽ{÷ùóç÷êÕK.—wèÐaÿþýÀãñ~þùçQ£Fñx¼&Mš>|¸qãÆ ÃˆÅâõë×Ïš5K(.]º4** -àxyymÞ¼™¦i¤]¦˜ ƒÁ`0_hNíèè¨WA@T=ª£(J©T>zô¨OŸ>$IΚ5k̘1FFFUlÃþ‘¼|ù²E‹eeeO999µk×®b:Ã0)))'Nœ@‘·1ÿA´Z-EÓAË‹§lŸâêÐxfÿ„=áx$)Ör‚óüùóû÷ï—³BÄ| Ý»wŸ7o^@@€Þ³«W¯ÎÎÎ^½zu5œ?~Æ ñññŸFÀ(ºœ§€î·m!24E?¹äÙËgŸ÷0 ƒÁ`0UÓĵ : BWAï´UiäryÚë×/_¼<{îìãGÀÉÉiÀÀ-Z´´·³533ûDï………ãÆûý÷ßËyÏêÕH$SSÓµk×êš¶b0_—/_Öh4\ḐæðáÞžžœ•;†C¯j@÷˜Óp›N7lØuéYé>øÜW€Á`0 ƒy?¡!¡ ¢T†Ê ‚Jµ(JvvެT&U*%²F††à“úÃ?zôhôèÑ÷ïßçRêׯÏy´@ii©T*]°`Áä!ŒÁèrðàÁÆ#o æ3RÕ@¹ÿ]\\X–MÏJö[ `0 ƒÁ|1÷ æ´ ã¿_•vàÂÎ;gΜ‰¶:ã´E¥§§2dêÔ©ŸÎǃÁ`þ#èª*zèþ@ª8sá WI¯nïWÔž>ú^ƒÁ`0Ì¿‚V}Pþž]{ª(Õý;÷ÛµmWNA ?*á?‡‘#G~ýõ×QQQ7nD1Þ322œœœŽ;æääô¹¥Ã`0˜l*ª ¸Ì\t.º°,››—ûçvSÜ?î@´ UËV‰—?ÃUa0ÿ%zvë‰vƒëбÃüèù¿¯^½ÿÍxt<~ÂøÁC×JµÝ»1 w(m ü7Pk Õß|!ÿ}Ã`0ɇ«ºtê’ö¬ùõë ËÒ Cÿù?£{\¨PµêÖ«²a˜ðŸ®·råÊE‹ååå1 cmmvkÃ`0L­Àí ]îg9A9í@zV:ŸÿÿHn^îÍ;7ùºðþr|ôØQÐ-‚Á`>ç/žGQxôP -@`aaáÝÔ{èð¡UoþZ}”Jå¦ ›’’’TJ•‡§Çäï&»»»{xx$%'mÙºß~’$+¾õW’®#:¬2¿~ýÄËÔ_ï?€)m¾â“$Ÿ ù$¹äj2,íx4å9TcöÅ|& pÜA ƒù¤ EèS è£ÌR Ûǰ,‹:¬ëÉ×ËU;xð`.'žb0è}˜·`^»vínÞ¼¹rÅÊ+IW~ZöS»=û@}(«W­>wö\Ô¬¨V­Z}3æ›éS§>zØÄÔÐl¿¢và@ìõëÖ;7tÞþÛvcãËVlX·áÕ«WsæÎ©N‹<>¯?#gÏœ]¼p±iÓ£~líÛº¤¸äƉD·‰ê´X©*™7gÞË—/7nÞèîáþ&ýÍ¡ƒ‡´”¶êjGš’ºbÕ î¯Y1åêõ«$ƒù'À «/\€>}ûpj”;`X–O’Ç?€~>>?¶k‹T(åå÷ÓÊå×mY  }òô!CêׯØ)®$_144üyÕÏq±q#FŽ˜üíd®†þ}ú€‰‰I×®]gþ8ÙGH$’ ë7Ü¿w_¥R5õi:}úôú êÀ¶­Ûâ/..viì2pàÀÞ!½u#j—••­^µša˜ï¿ÿ¾[·n`anáìì\î~ ùB¡@XÙªBªÝz€¥¥e³æÍ¦}?ÍÁÁ¡\µÜÏâââÔÔT¡PؤIcccî¬Þ :0##~˜ñLúvRB|B¹”‘#Gúùú1 sòÔI[;[tŒê‰D¾¾¾‹Zlll )))K-yñâ—aÐàA?üøÃ{o#ƒùD誸ÁZq©8¬¢†ÿÎëøãÇý||ÐÁŸU½;õÞaÖ`0 æ/Š€Š)œíÀ_â@òÿߘ…?•ÜIÉIz­àP6Ý" æÓAòHôº!Û‚G Ÿ;-_º¼   õEª»‡û„È 9Ù9c6:Õw1zbäÄ}ö¹¸¸ J†:fì˜}äåæÙ·´?yöäŒi3.]¼”0hÈ ´×i ÃØØÚð…|°³·€—/^þÙ.I ÿußú‡ªÕjðýÊ¥Û9Ø9;;¿~ýúÖ­[ΜQ6ž€GòI½5÷žñãŒUËW?~ÜÌÂ컩ßiÔšI&eeemܼÑÞÑ~D؈ñãÆùýHjJê/Û~éݧ÷¬9³2ÞdìÙµ§õW­ëÖ«ËUõàá¹\»v®âÓDð -­­úFU” Î_:E]¾xùÇ™?oß±]·ZÝF I’Ôh4Æ ëÜ¥³_¿¯¾úJ(@ex4þhø°ðçÏž¯Ý°¶}‡ö0ú›ÑåRô6×»Oïé3§Ï˜6#99ùèÑ£#FP©TS§L-,(ܰyƒƒƒÃ¨ˆQ¥¥¥èÎß»{¯êÛˆÁ`>ܰJW5€"""*«h–å“äÕÈqí·l½¤Ï˜Îiô~ëtͰvƒÁ`0åÑ›°œí€ˆ/ D:e©:t,W[Zz×ý¥ƒùdÂ?_7@À ŸŽöú±ËKå·oÜÎÎÊöððèØ¡#ôèÙcßÞ}¿ùý³ÿ‡ò 4ØÁΡnݺ2™,¤Oˆ•M“&Mîݽ——›'ˆdÅ2݆  ¨¨ýä‘Úè¾õe¥e````jbÊ%ZXX€¬XÆå4ˆ"Täé^HXx˜•MXxØÉ„“'Oœœ9sfrRrFFFóÍ;tèíNŸ:}9ñ²¥•%¤AÞ™4ÑwÓf.?R pÚ÷Ãø*•ªÖ®ƒÁ`0_ ·0ÔÝ¿¦iŠ¢(ŠâP)ˆò¶•D½BÝ:ƶÌßÉÿ«íÀ»xi¡e°¶±~ðàXY[¡S6¶6+ÎåÞS + ’O „°´´$ù`X†ä“Ö¶Ö ÑjP~-¥›*l,¬,@¥RQ …Ìï ¸¤,­-IÞÿ¯nU´@bmkÍ5]PPÀln^.<|ðл‰7×PfVfŸþ}Ú´»–|-êÇ(¨[·îo»~sªÿÿ;^YÙX€Z­V¨&&&?üï‡þú÷íÝÞ}©P‹$ÌçVv£*“ Öü¼æìé³R©T£Ñ  87àª-÷=œ›”””š’*‹·ÿ²}åê••] É'‘.€3 wÚ¿¤è4‡ŽmlmH>‰ ПRZ K+KTÊÚÆUEòÉ€UßF ó)øÐaÍ2\Âã:™§;Ô« Ûd8ð%íY€Á`0˜OçJPî*Ù¶àÿO` 0ÐÉý§ƒÜ½‡÷Z5oÅ%gdeèvo)‚Á`>´Fôçb2_€Þ¾+¯€­­m«­Š ‹àmÑ[tª¸¨ê׫Ͻ§"È@`@$WßÑH¾ÀÀÇÛ‡$Éi'$I² ¿šz7EÅù$ø<¾î[ßοJ¥zúø©¿¿?Hþ½û‹âø>{^8ª4iÒ‹Š("öÞE»b!+öØP¬1‰[Ð(**ö "vEEEéwGçúîí¾Ì›ý]h"jP3ŸÇÇg™Û™]޹ÛïNáóssrjp76ƒ­ÎV¯ª¬¢Ë¢sU‰ÔmÕEU"ÇÓRײ±¶øúú&M¨u<(^½|õûï¿?~üøÜéssçÍ¥_ ðÐÖÖ‰D©É©C†*OÕTKTc«ÙZÙ6~¡êÖêôéÓq{ã\]]Ïž;«««ëÜÆ™ Õs×¶V…½=½½=½-ZýÇÞ?¤©:[½‘„¿‹CgU7¥nqpþ‚˜ ¦:[½•y+@Ueܧ¢¢Àd2ÕÙêêlõÆ/#‚ _Äß_«^g½~›ýV]]]]M]]]ÝÈШޯU°ï= ! ÎA°oð Zû7EAP€ Þ!p7úÉäÿõ÷9“õ2㥫³+P  yä_Vë/c`b©øæ›[¶la±XëbÖqÔ9]{tµ²²ÊÈÈH½“jcksñâE‹5&lÌÿ;ÃGÐØÿçð¿‡Õ ŒÁbXZ[2øô©ÓÇwòïtýúu]]ÝÐñ¡ô002˜¿pþÚÕk£££ßñ»¦–æÒ¥K•Jeß~}Ûuh004(/+O½“jÛÚöæ›usˆ·s°‹ 1œÁbtëÑͶµí£G®\½Ò%°Kö»ìøƒñA#ƒp¿yãfð¨`ÿ.þW®^yüø±!ÏP5+]}ÝU«WÍŸ7môZ]}ÝŽ:ò|úêýïÒ1±^¨ºµ"”€ÍacLìÀ°×•jÕÇû€šššð©á'Oôöö†3>:tnäÛµoGO¾/¤³ª›R·8¸ýÿ¿ c°Ýy<ÞëׯïÝ¿glb|ãú ú¥»wî6~ùè¯UNµ å øu¿V)UÖ,(°Ü¸ púùóšËkíß@Aø»ã@­Y A}¸‡É©gÍ‚¿cp®º³¢5 äßQkÍ‚¹‘sÕÔÔŒŒŒgFÌôôô„û?q|íêµóæÌ“J¥Î.α»bÝÝÜkeG³X,›û0 ˜ù¦Í›´µµ7¬ß ‘H¼¼½V¯Ymdh…£Ó™ŒÚ‹›Ìœ9ÓÆÚf÷îÝû‹Å€°qa«×¬†»mÙ²eÅòÓ§M§›º,è䉓'OœÔÑÑ2:jQ¬áñÄãë¢×­üieMu““Sظ0___Š¢rrr~ˆøáÝÛwú\ý‰“&Nš4‰Íf«V&dLˆµõŽí;æF΋Å:::.®.íÛ·W-‘Ãæèéê5~¡êÖjÔ¨Qî?¸tñ’§½›Í¦sþß/€–¦–ºšúª«ÊÊÊH’4·0_°pÁ´ðiðôë=A‹ñCD^^ÞšUkV®XyèÈ¡nݺÕM©[ÜVýUrØœƒ‡Ο7?tLèÿ¦a09lN§N>xùè'.Lö\ïšô\Â%‹yëÖƒ­Y :¸“J¥_üÌ>‰D’••UPP`eeåêê [4AäÕšqN4@oÿ¤T*qïܹ3EQR¹N÷ 062ÎÍÏ­ªªb©Rù c³ØÑë"È¡Ü IDAT££×D J-{¾òÝkeÞ t8h`ܾ¸¿/^¼€ V®Z9sÖ̆ö¼xáâ„ñºvëšp4Íu÷õ˜7gÞÁƒ—,]9'²¥ë‚ ÿEûµÊØÈ8wnd•\ÁÂ0¨õÍ`FßN‰¾™ ÷çòd„ìÉÃ'þüMMM=e †}}‚HJJJLLT*•€šš@9|øð–®‚ Èw¥Ö 1€;`LLu²œ²Ò²“§N~°ˆzç×Aä3*”´l¼¼¼Ê+Ë›²çà!ƒ'Lœ°ßþ 1–ÿÝñi1ëcüüü|;ø¾HqîÜ9uuõƒ¢AZÊÇ~­*“ÈNff4}ÿ†4§ïÀ³çéU$“Åæ°˜ †a€ÁÀ0ƒa0Æ ( —VÛY™éêê~l浟ÏápÖ­[çââò‰E ‚ü—©öPí5P«ïŽãtß.]ºPER¤ê'ˆ¶–öˉE_òTAfÊÉÎ‰ŠŠJIIÑÐÐðððˆŠŠêØ©cKW Aþ»>ökÕGí¯¥©õ9ûJeJ)èd©u‰/–‹å’j!SIâ‰+•¸² Ÿo­)þ9jT32§íÝ»7''§ÞWMLL(ŠŠˆˆ°µµ‰‰áñxŸR‚ RWCÝà« ŒÁdÿo—TñmŒSCAê²w²?qúDK×Aÿ÷±_«šý5ì3¬hÈf³JÊ«ËDÙ25\®$ÄrB"#å ¥WKË„• ‘¨•:ùÏ•±š®¦¦&!!áÎ;ï†a˜µµµ\.2dÈØ±cgΜ O Ai6ÕY 릫F€Êê_‚ ‚ È·®þèI’tüüÝÓüýƒººz…´Ô…«S)&qŠ"H —+deUÒj)!ÃI™\)“Q8óËb7YeeåòåËé™®>ˆÍf;99;wÎÅÅ…ž—AùXT}!Ýz»Ð¯Ê 9…73Œ ‚ ‚üË444z‰¢¨ú£ÅÅ%¯ßóÚL&“ÅÀ(Ã`` ÃÆÀ4çurUW¨Ë6Ü|¯I ±ˆ”ʉQ B)‘(¥"’`SÍŠìÚµëðáÃ^^^VVVM?ÊÌÌ,33ósEÞ½{çïï_RòI“ú̘1ÃÎÎnþüùŸ¥JÈÐÎ;‡ Ç}+–-[FDLLLC;\»v-""¢¢¢ââÅ‹>>> íV—««ë¾}û|}}ßmРA‹/îÔ©SÓs®%))i÷îÝÇŽkvÿ99•-] AAäã¸8×?y_ýÑ‹y¿¸ÆÏ—VC¼¯”Jª¥r‰œÂI’ÀI…’TRB~EþÛw‘c;H ù€PR†1Y0z@)q’ÀI²9”d2™D"¹sçNff¦‘‘Q„‹÷|=F­§§÷¯7sæÌ[·n½{÷....$$&nß¾}Á‚ô>©©©u?öÍy\z—Å")P$À ¥>ÓØ‘׿ó‘žž~ñâÅZ‰†Í™3çó®ý«P(ª«« ©Éëׯƒ‚‚Ú¡¨¨èرc‘‘ÿ[‹ˆ$É={ötìØQSSóĉ...ƒ ‚/eee|ø°³³sÝ—zöì¹téÒÇ·mÛ¶Ù•ü7±pá¿þú«¤¤ÄÎÎnùòåôå=þ|TTTQQ‘¿¿ÿîÝ»ÍÍÍaúúõëwìØ!—ËGµuëV‹•’’Ò¯_?ÕlwïÞݧOŸZeeddlݺ533óï²A[oã*!‚ ‚ 4)^ÿ<õG ¦XF$¿Ìr´…b%!#‘T)•+å ¹XQ&¬’Õˆ0©T©À)‰ ` )€a”RIá8Ià©„ÓYJËËË“’’lll<<<455?%«øoçéé9jÔ(Õ[A(88x×®]p[Mí;!|:û ‹ó­(MIbü Ê›5ö³GÜÜÜà-åÇóóóGŒÀ0ŒÉd~èÐ# ¹\n#@ÐødœFFFr¹¼¦¦FGG¦ã)Ô«  ÀÉÉé e¾gÏž1cÆ|p·üü|‰DRoh Ù»wï·Àqœ$ɃZYY?>,,ìáÃ‡ŽŽŽãÆÛ½{wÏž=,X0a„+W®wîÜyñâEƒ¡C‡nذaéÒ¥þþþ.[ðàÁƒ1cÆÔmÁ***~øá‡ &,_¾|äÈ‘ªéÿê #‚ ‚ Ÿ€Ë媳Ôë}‰Qo*“ɨÁ•fzZ¥y%IVD¥‚(­ª)*)/æ—Ë%RR.#qI’`€¢†aL`’RR$NQ$\"«u­uTnnî… ž={FD3rkÈáLJ ·á}5Üvrrzòä ÜÞ·oŸ­­­¥¥å¯¿þ SD"ѬY³¬­­íììV¬X«´oß¾AƒM:µ}ûö^^^ÉÉÉpç3flÞ¼0wîÜàà`˜øÓO?õîÝ»î•ÉÈÈ011:thDDÄ¢E‹...>?~\CC#==pðàÁáÇ×=£ðððÀÀ@‡S+Á`¨ÿí»ŸµQ‰ƒ-%År •â¥üó‡Æ0ŒÅb±X,¡Phbb·ah °°ðСCÛ¶m‹‹‹+**‚ûÇÆÆ>xðàÈ‘#¿ÿþûåË—ËÊÊNœ8±{÷î„„øþ‘H$[¶lINNÞ½{÷¶mÛRSSáª7ÿIII±±±[·nÝ¿?ŸÏ\¿~=---===66¾Iê–ÎápôôôÊÊÊ`&r¹üÎ;Ý»wÇ0ŒÏç;;;WVVÂäiii&&&L&³yÑׯ_wéÒÅØØxèС•••túÑ£G}||LMMû÷ïñññ)--íׯ_ûöíÑÑÑNNN<¯C‡7oÞ„GmÞ¼yÆŒp[$ihhÈd2:Ï„„„S§NmÞ¼ÙÉÉiÖ¬YªÕ (êòåËðdžþ0—.]êÛ·/àìÙ³íÛ·çñxNNN¿ÿþ;½C``à_ýÕŒKÑ"444¶mÛÖ¶m[7qâÄÖ­[§¥¥Ž?îã㤯¯¿zõêÛ·oçååâãã'Ožìîînaa±pჂ¿ 55555µ“'O0@[»öº8ìÔ©Ó¨Q£šÞ¥ AAù†Ôßw€ÉdJJEv6’—Ù¥58)Ub±R"J€‘@)W(¥bL!63Ò é×`ÆbŠ" %E¤RIȤ†l1Õ¬‘u)•ÊŒŒŒ¼¼¼nݺÑOA?Q@@@dd$A‰äþýûEVTTxzzæææÖÔÔ¤§§¿|ùòÍ›7Ý»w—={öÜ»wÁøGP†$ÉÑ£G7.22òîÝ»C† ™:u*¬ä­[·Ú·oûöm[[Û””ww÷Û·oÓ÷?MqåÊ;;;SSÓ°°°ðððï;@ ¯!qÈ”Eark`VÏC 888Ð?ž|æÖ¡C???@ïÞ½óóóUŸ444öíÛ·dÉ’ñãÇoÞ¼¹îl‹ÏŸ?/--ŒŒd2™;wîÝ»7LHII¤¦¦.X°n§¤¤4=:àïïŸpåÊ•yóæEGGïÞ½»ÙWìk–[‘}4=>ñeB DA2¥&Ub29õäuÚŸ7ÿü󯑬©K`4Žãåååª÷Òׯ_ïܹ³ †a®®®‰D*• …BMMÍ®]»b¦©©‰aX@@€šš†aêêê° ‰P(433ëÒ¥ ›Í655500€ý´…B!¿»»»ššƒÁptt„ÒE"‘B¡ g%¨·tÇ+--TWW§¥¥Ái;åryUU•±±1|õîÝ»... …‚Ãáèëëì¥xþüyqqñüùóY,V÷îÝ{öì Ó÷ìÙ3sæL///&“QTT”]ëØ‘#GÂ> ãÆÓÓÓ{ýúõÇ–®ª¢¢BSSSuˆG½˜2™ìÞ½{]»vtïÞÝÙÙ™Á`øøø 6Œ^EæSëùë§P(Ææææ‰DªáT===¸Œj:œE,Ó»]¸pAWW×ßß¿nþ|>ÿâÅ‹K–,AS6"‚ ‚|—šw#$ÒÅ¡}–ýq#_, &øÿA$IS$ŽQ“É÷Gll, 55ÕÌÌ ÞÆ4»BŒGPí`üéà“ù÷ïßÃ.ý)))Ïž=£o¼uuuµ´´à¶–––H$‹ÅUUUô½½µµõû÷ïá6}“Æd2Ùl¶X,®u—åíííààPXXHa˜8qbBB **Ê×××ÌÌŒ¾«iÕª]ùsç ±X¼fÍšÂÂB¡Pèåå°³³ƒÈ/\¸Ð½{÷zÏÑÛÛnØÙÙ½ÿþرcð¹îw†«að¨à¾ˆ*ute¥U>Ä• ’Z†,>xòûýG£Ú„qXµ\|"¡PÈb± à‰¤¸¸X&“=~ü¦°Ùl6›-¬­­áŸCYY™¦¦&ý,º¬¬ ¾m„B¡ê8|‰DW¡\ …Â;wîðù|¥R‰ã¸­­-@ Áž •022‚ÌoÝºåææKZZZZZZÆÆÆïÞ½ËÍÍ͈•´ ‚ ÂÂÂttt~þùg˜¢­­]SSCïPUU ¨¦WUUè¶pøðáQ£FÕÛÉH]]ÝßßßÉÉ Çñ/w"‚ ‚ HK©?:@’‹Å½þÜÁÁ$É;vŒ;–ÇãóxÍž¯‘èÀçípòäÉâââüÑÄÄdÿþýÏž=£;ÜÖ¥¥¥¥««›ŸŸo¢òòò,,,šXÖÞ½{kjj¬­­7nܸxñbÀ¾}ûöíÛ_MKK+..¦ïj a÷okkk}}ýíÛ·wîÜYKKËÒÒr÷îÝ;v„wbïÞ½û¨óe³ÙJ¥ò£ùVè©ë¯í½iþɹŠ2’c&+WŠ) ”æáaÓ~èñÙKäóù<~CÊd2555úÞ’&è[nÕmÕ'ÿB¡ÐÒÒ¦RejjZ]]M’¤¾¾¾B¡8räHß¾} Ä`0Ο?3QíôÞPéàï‘àÝ»wô|>Î/Àãñ’““{õꥦ¦&š7逹¹¹@ P=e333@«V­BBBjM  *77wöìÙW¯^õð𸻻Ã~Cšššr¹îS^^^÷À†‚­ZµÒÓÓ{÷c#¾|ù2œt0vìØåË—‡„„0™ÌY³fÑ—^¿~mbbò +P*•ãÇW(G¥#5...ôì |>¿´´ÔÅŦ§§§Ã‰WÒÓÓ­¬¬´µµaÃ[\\|ëÖ­†¤´··ÿ¾‡&!‚ ‚üÇÕÿ=[©$»up éï7u¸ÿ´á¦ ó›6ªË´‘þ“‡v˜2¼ã´ÐžÓ'™0nIR]»v[±bÅæÍ›·mÛ6sÖ,---5ç³Ïßþ%ܾ}»ªªÊÚÚºcÇŽ7nÜ())¡¶×+88xõêÕ•••EEE›6m¢lÜ»wï–-[÷믿Âg¹ª<<< ·mÛ¦T*SSS¯^½ªZÉ;w ]ºt¡·ëR(2™Œ¢(Çe2ü®Ÿ˜˜øîÝ»ªªªäääÍ›7ÓË×}ÔXêk®“•¨)òyúc9J^R£¬'Oë9õK§z«ÐÓÓÃ0,==¢(¥RùþýûêêjðÏ™UG CCCƒAQTYYYVV–R©¬¬¬¼|ùr—.] }ó_UUE’¤ƒÁÈÊÊ¢ŸðÃßu㥠 ÅÅ‹;v쟖ø|>ÌÄÒÒrÔ¨Q°+ ø±<<<¸\î‰'999ôrS§NýùçŸ?~LQTuuõ©S§jþª««ÕÕÕáÜ —/_~ûö-L÷ôô¼sçìëþÛo¿Õ-‘ÇãÕÃ0¬wïÞpN#þúë/::P]]íææÆd2 Ξ=Kï“’’Rw1¿¯I’“'Oûöí#IR&“Á8`PPÐãÇÏž=+‰V®\éïïÃŽcÇŽýã?233ù|þæÍ›ÃÂÂè¬Ú·oû§@%%%6l€<þ¦èð ‚ ‚ ò=©¿ï†þì˜L&IR &•ÊrssH’244ÐÕÕSà ‚ jjjX ¦³s†að^…¢¨†&h¢Fæl䱕R©üØ mmmõõõáÒèÚÚÚööö:::/Y¿nݺ xxx°X¬ÆŸ‹ªVlÒ¤I‘‘‘0î=iÒ¤»w磌ÿo &“yäÈ‘ððð 6tìØqĈtÏ‹€€€Ã‡wîÜnoÞ¼n×5pà@x_~âĉþýû§¦¦Î›7¯²²ÒÂÂbòäÉu×;üžèkêÇŒZÿþٔD/·8;Äfjä Ù_¨,@‡vCL&sذaׯ_OJJb2™fff  (ªVD€>®wþ*off¶}ûv555???8€ÓéïÚµËØØvÚ‡:;;Ÿ>}zÛ¶m]ºtcæk• b±Xúúúr¹\uq>@кuk‡Ãyª6¯ïƒÁ8räÈŒ3¶oßnbbB/¨Ñ£G 6Ìœ93''GGG'00pèСªzxx„††úùùY[[»¹¹Áÿ!C†øùù™™™ÕÏ?~|hh¨©©iß¾}÷ï߯úÒ”)SV®\9yòä†jûúõk6›MßýþöÛo&L033311Q >|¸ÞÀÄש¸¸øèÑ£Øk°~ýúÈÈH++«?ÿüsáÂ…EEEþþþtg¥‘#GfeeõèÑÇñààਨ(:«#GŽDDü£¯P(Œ‹‹‹ˆˆ`2™~~~S¦L™?>œÕAAùÎ`Ï#B*•¦§§1Œ… Ž?^SS³ñ»èOñöí[???ÕY²h–––õΕE’dfffbb"ÝCû›6jÔ¨=zÔÛQiŽãJ’Ä0¬RT9ÿÏùö¦vsÍå°ë™n€É`|¹wïÇzýúõóçÏU—ŽG>ÑÀ—,Y£~uýú믅……7nl$‡k׮ůÆ&&&~™ ~à´$I*•JÕÿ ‚€³]AŽãðGÇ»uëFQTNNNëÖ­á<š‚ ‚ È7ËåRuçîÿNþ&&&ð<ü¿±5ßD"Q~AÁ»·ï®\½;?xð •¥¥§§—‘¡®®î—ƒjooÿæÍ›Y³f;w®)ãäù|¾––ÖÑ£G¿éÐÀÝ»wíì쌓’’®]»¶yóæ–®Ñ7†ý÷¿‰ñÁ9Z¸6M& é™óÏâüùó¼jiiI¯ªÐ=zôèÑã³V AAA¾ F(ŠRWW×ÖÒ¶w°wuu•ɤ ’¢ÔÕ8\–üS9r$===<<\u”~­`DMM@ Xºté74B¸!YYY!!!2™ÌÜÜ<..t M' a'äß1lذ–®‚ ‚ ‚|¥Œ`Æb±¬¬,1 S½-ÿô™šÈÝÝýÎ;ñññ‹/®µÔA999#FŒˆˆˆøzz‰ŠqãÆ7®¥küÛÐÍ*‚ ‚ ‚ _‰ú×,PEI…þÐ-44477wΜ9êêêpŽ÷ÜÜ\Š¢æÌ™ó}„AAA¤e56ïÀW‚Éd®[·nÅŠ%%%$Iêééµt¥AAAäûñá¾_ uuu›Ö­[£Ð‚ ò¹Èd2.—[ïB9´_~ùÅÑÑÑÉÉé£r>vìXPPЧÕAäKñññyôèQK×â“üøãÛ¶mkéZ Èw囉 ‚ ߨ'Ož 0 U«V666½{÷NIIiú±ùùù¦¦¦_®nTSS“ššúúõëfgâááñ­ GÀårsrrè”r¹\ÕY´[Ê… ¸\.—Ë500pvvŽŽŽné5è£>TÛÏZŸ#GŽìÖ­Û—­+‚üÇ è‚ ò)Šààànݺ½zõêéÓ§ .d±¾Am´’’]]]×ÒAä«`ooìØ1¸]PP——§¡¡ñ¥ %¢)»YZZ …Â’’’#GŽìÛ·ïÌ™3_ºbMAQ”ê"åŸñC! ÀÓÓó3UAPtAù¢ ËÊÊf̘¡«««¯¯ß³gÏŽ;â8nooÿüùs¸Oyy¹©©©@ ÈÊÊêÛ·/G6kÖ,@hh¨\.÷ððððð€Oïýüü¬­­‡š›› spqqÙ»w¯¯¯¯¹¹ùâÅ‹‹‹‹ daa,‰jU‰¢¨˜˜GGG777ú[>@(Nž<ÙÁÁÁÝÝý·ß~¼zõjðàÁ¥¥¥‘‘‘YYYýû÷·±±qttœ3gŽL&ƒšššÁíåË—¯ZµJµ¸… ;ÖÃÃ#>>¾î9"ò ¦Û£GÑk{á8¾fÍwww‡üQ"‘80zôèÈÈÈV­Zùúú¾xñbÏž=NNNNNN§OŸ†ŠÅâÈÈH'''—Õ«WÃXÀFŒ1cÆ __ß={öÔÛfÖ­‹Åâp8^^^íÛ·‡m&Aaaa666AAAùùùµ9þ¼¿¿«V­<<üÈ·‚|ÏXêêê-]A¤ÅÐ Ó(•Jø?Äd2•J%ƒÁ`2™L&“Á`QkÛ¦hÕª•……EDDDhh¨—˰Ùì#F;vÌÃÃpòäÉÎ;Ï;·oß¾—.]Âq<==ïëëK'NNN^¶lÙÑ£GÝÜÜvïÞ=a„7nÀ*={öâÅ‹‰¤k×®=Úºu«]PPÐþýû#""T«tòäÉ#GŽ$%%M:•N?~¼——WzzzYYÙðáÃíììú÷ïòäÉ¡C‡Â ¼yófñâÅ~~~ååå!!!»víš={ö¯ÀÆÿú미¸¸víÚBCCk#‚ ß;;;ƒøúú=zôСC»wï†/mذáþýû×®]ÓÑÑ™5kÖÚµk×­[¸~ýúÎ;7nÜ=f̘<{öìæÍ›3gÎìß¿?‡ÃY±bE^^Þýû÷%IPPlµnÞ¼yêÔ©ØØX¸hWÝ6³Þ’$™™™ùøñcؾQÕ¿ÿ;w2™Ì¨¨¨yóæ?~\ummí¸¸8‡gÏž 6¬mÛ¶°±ª®®†uàóù¾¾¾´¶¶^¿~}jjêùóç-,,ž={Æáp`ƒ¹iÓ¦!C†<|øpìØ±©©©°ÿÿùóç“’’ÌÍø_"R IDATÍU—<«÷C4м×j?»té¢ú‰ ªÞÚ.[¶Œ ˆŒŒŒòòòáÇkjj^¼xqàÀ””—ŸŸÏd2?åý€ ßÔwAù‚8ÎåË—y<^TT”££cPPPAA $$$11‘$IÀÑ£GG `³Ù999ïß¿çp8mÛ¶­›Û¾}ûÂÃÃ===™LæŒ3Š‹‹éÀ³gÏ622²²²ò÷÷ïܹ³»»»¦¦æ!Cê~LLLœ:uª•••¦¦fTTLÌÌÌ|öìÙêÕ«ÕÕÕ-,,ÂÃÃéÇz4GGÇ€€6›mbb2}úô»wï6ã‚|ðùÊ=:!!áÑ£G:::ŽŽŽtúþýûúé'ccc E‹Ñmˆ§§çðáÃ9NpppaaáòåËÕÕÕûöíKQTaa! 11qÙ²eúúúæææóçÏ?zô(<ÐËË«K—. Ãêm3k)((àr¹†††þþþ6›¢¥¥¥®®U·áêÚµ«““ƒÁðöö}úˆÅb¹\®zÔãÇGŒááááîî~úôiºeÓÒÒâp8ªU•J¥¶¶¶ª‡<~ü˜n–ïß¿‡Ôª¿ªz?iÞ›¢nmÅb±H$¢oþé6mÚ¬Y³&::ÚÞÞ~Ê”)U ‚|÷¾¥©¡Aoš‰‰Éøñã###á!!!ÇŽ³²²4hœÖËÌÌlÇŽE%''wíÚ•ÁøGN%þ)Õ055 …p›þ^haa¡­­}ïÞ½Z%ªZ¹r¥––Vjjª††Fbb"=@WSS“þ¶]^^^·»¯jžuϱ¡îÁ‚| ÚµkwôèÑŸ~ú‰NÔÐÐàr¹puuý¨Ü455utt `SŸŸoff_ª5’«n›Ù33³¡C‡þüóÏ6l8zôè7Ο?ÏãñJJJœUûù&Nœ¸xñâàà`&“I·Ïõ‚瘓“£ÚjYXX×¥¥¥¥­­ýþý{Àû÷ïé—BBBBBBÊËËøá‡7nÞ¼¹éÙ"È÷í›é; ‘HÒÒÒΜ9óôéSÕ‰OA¯YaaáºuëÞ¼y#“Érssãââèîô£FºtéRBBýüíÌ™3B¡Ã0Ç`0X,–¡¡!Žãôœ'Nüõ×_Ÿ>hèС6lJ¥ï߿߻w/L|óæÍýû÷ ‚ÐÖÖÖÖÖf³ÙMÌ Aþ ¾èAçÏŸŸ6mÚÆ–-[Ö©S§Zó© ‚ _'øèfäÈ‘666}úôáñxp9€‰‰I»ví‚èܹ3L¹sçNçέ­­'L˜°}ûv ¹sçvéÒÅÚÚúÕ«Wݺu[»víìÙ³­¬¬:tèpñâÅ%Ô³gÏnݺ 6 ~{`vðàÁ²²²¶mÛÚÚÚΜ9³ªªªÖK–,9~üxß¾}ÇG×}àÀ=z,[¶¬wïÞuKŒŒŒ\½zµµµu\\\ÝsüØú#ÒâZµjçÉSµhÑ¢N: 0 U«VƒÎÈÈhbn«W¯633kß¾}÷îÝ{öì9}úôzw«ÛfÖRPPÀãñx<^`` ®®î–-[¡¡¡$IvéÒ%((¨U«VuÚ²eËÔ©S‡ ²jÕªž={6^ÕÅ‹wìØ±oß¾ÖÖÖ ,ÀqÜØØøÄ‰ptttuuݲeKãñúPh¨yWm?k}"4^UÀÚµk™L¦³³óÈ‘#ƒ‚‚àбX¼páB[[[©Tº`Á‚æƒ ÿX­ÎEMñðÑã c±9,&ƒa`00ŒÁ``†Œ1(ŠÂ¥Õmì¬èiH›íþýû{÷î­»ŸÏWWWß´iÓÇvßBAT5´fAJ¥Çqø$ nà8Žã¸¿¿?EQ999­[·®¨¨ø”Ò###¹\®jï\A¤!¨Íl¶]»vÝ¿?..®¥+‚ -ËåRuçîÿNþ&&&0ÿoN‡FR©Lb,µ.ñÅr±\R-#d ’ Iœ q¥Wäó­5ÅÛ—…~J¥ß¿ûîÝ»z_511¡(jêÔ©ööö›7oFƒ6A¾9ïß¿?}ú4ì“ ‚4µ™+;;[*•ººº¾{÷nçÎË—/oé!È×®9Ñ6›UR^ñX&Ê–©ár%!–)W(常ZZ&¬TˆD­ìÔÉï•UWWÇÇǧ¤¤4¾†aÖÖÖ‰¤oß¾ãÇÿñǛѿAi6løõ×_üñÇÖ­[·t]A¾v¨Íl‘H4qâÄÒÒR]]ÝÐÐСC‡¶täkWt€$IŠ¢è›íÜuc˜ººz…wáêTŠIœ¢’Âå YY•´ZJÈpR&WÊdÎlÆLQ€ŠŠŠ¨¨¨ºëH5„Íf;99:uÊÍÍ­GÍ(Aù÷EEEEEEµt-A¾ ¨ÍlÇ·t-ä[Rÿ¬„……ï/§>¸òøåµ§™ÉÏ_ßH{}3íõ­§Y)ÏÞ¦>{[I¨Ïëäèf 5ΜUš#,Ëá—DeÕJ9A)¥D¢”ŠHBA5+:°cÇŽC‡åççÔQfffMŸúåƒÞ¾}«¯¯ÿ‰™L™2%&&æ³Ôùoúå—_š>+ïWbÑ¢EóçÏod‡«W¯¶nÝšËåÖ ¾qööö÷îÝûàn}úô¹}ûöGå\Ë•+WгAAä?¨þ¾,ó~qŸ/­†x_)•TKå9…“$“ %©¤„üŠü·ï"Çvò¡¤ c²0Œa J‰“N’ÍY •J%É;w233}||ŒŒŒšx ½ÖôWbìØ±zzzÿBA7nÜX¹rå“'OLLLÞ¾}K§:ôÌ™3p[KK«î̎ߟ‡‚T&SI‘€"N( ئNÆÎŸ·ˆ§OŸÒW•†aØâÅ‹?ïŠ8 …¢ªª ®?ÔPM^½z5f̘†vxÿþýÁƒ-ZD§$¹}ûö€€--­#Gޏ»»>¾”™™yôèQGGÇÏxu-]ºtË–-Æ kv&LhÓ¦êyÑRRRÄbqC39«rqqILLtqq©ûRïÞ½£¢¢>|ؾ}ûfWòßDÄœ9s.^¼X\\loo¿jÕ*úòÆÄÄìØ±£¬¬ÌÍÍí—_~ñóó£R*•íÚµKOO'““³xñbxÛ¶m»~ýzGGÇz‹#Ir„ _þ´AA[ýу)–É/s„m¡XIÈB$UJåJ¹B.V” «d5"L*U*pJ"H `¥TR8N8E*)Š"©æô •——'%%ÙØØxxxhjj~JV-¢[·nÿNAZZZÓ¦M+--¥ £ÅÆÆÂïñÿ‘ßìkc‚½¥)IŒ_AµWÿÙ£žžžnnn€»wïæææÂ{i ØLæç-ˆÏç4q())111i$'—Ëkjjttt`ÊÇ9Ž——WJJŠ‘‘‘@ €é$I&%%˜šš~ÆS¨W~~¾³ógþ¥ÐvîÜöÁÝòòò$I½¡hìØ±»víúV¢8Ž“$™`mm}öìÙÑ£G?þÜÉÉéâÅ‹ÑÑÑ×®]óôôܼyóСC‹‹‹é¦`ûöíôÀçó½¼¼–-[¦££œ––VoqÇŽkFƒ,‘HJKK™LægÿKAù¼à¢-FFFõ¶u¨5Cä«ÕxóÕDõG˜LF ®t1ÖÏ(“W’ ‚ !¯ª‘VŠ%Sr†+H’0 c2(’")%EâEÂ%²šQ¡ZGåææÀTY¬æÌ¡X¯ƒ>|øÒ¥K€6mÚ¸¸¸œ¿n×-¡P˜˜˜8þ|úZ5|jhh`ö V"ò-¢(Š¢(™L& ­­­ëî€Z3A¾Nl¾š¨¡èSR*²°³‘¼Ì.­ÁI©‚‹•)PŒJ¹B)c ±™‘^H¿vÃ0PI()‚ •JB&5d‹©f,¨K©TfddäååuëÖ­Öq³Κ5‹ ˆòòr‰Dr÷î]Š¢ ÊË˽½½srrjjjž={ööíÛÌÌÌÎ;6ÌÖÖváÂ…999b±¸_¿~†††óæÍ\»víÖ­[:uºpáÂäÉ“³³³UËÚ¸q£ÏÞ½{]]]wîÜùäÉÕÐÀ+³}ûv©TZXXÈápÒÒÒ8NÓOsÕªU«V­rppXºtéaÊFy ‰“@¦Ä( “ãõ%Ãú%%%NNNô #FŒ°µµMOO?qâÄìÙ³åryUUUMM͸qãD"ÑÖ­[E"ѰaÃ8NlllVV–³³3ŸÏ'ðéÓ§—––þñÇnnn†††|>v  (ÊÔÔ´[·nl6ûÊ•+III¡¡¡}úôIOO?~<zPoé'àäÆ)))–––¶¶¶€âââ¶mÛêëë—••éèèܾ}{ܸq;vìhFt€$ÉáÇ3æöíÛ7oÞ4hЬY³×®]›?þùóç===ûí·‘#G>zôèåË—FFF×®]kÓ¦ ÀÉÉéÞ½{<ïÀ£FÊËËû`¤sìØ±W¯^­wdAAAAee¥ƒƒÒÐæÅ‹g̘ÐÑÑIHHhӦ͓'OzõêÕ¡C‡:œ‹‹‹Á7·Tª@ ÈÎÎvww ><66öÁƒ^^^{÷îõõõ¥ïöçÌ™³lÙ2]]Ýz3yðà‘‘‘±±qÝïæÍ›'M𤥥õ±c±XšššŸwô ‚ È‚a˜D"©÷%Ôš!ò5k¤ùj¢†F0€Dvîþ[¹ ZV&¢”Eà€$1&PÀ0JI·5Ñêîg[]U]#‘$É5àR…ã¸B¡¨¨¨ÄÍ ©R ¬ƒ(‘H233?W__+++CCÃ'Ožäçç÷ìÙ333óåË—iii;w†]Å(ŠŠ‰‰ÑÔÔôññiÛ¶í³gÏlmm>|åÊ.—Ëår—.]£;vìÔ© oß¾yyy•••ª“jhhÄÇÇ÷êÕKOOoÛ¶muc9W†Íf ‚ÌÌLOO϶mÛ6ýÃÃõ´´Î;׿ÿû÷ï{yy}†k÷õÉ)w7ÿ6“ÁÊç»:ÙK Œ˜LN=ÊyÇŒÜy¤¶ºög,Çñ²²2Õ{é¿þú«k×®ð>ÜÃÃãüùóR©T(jiiõêÕ Ã0--- úwﮦ¦ÐÐЀ·^ÀÂÂ>6733322*++344vvv Ãè_œ³³ó•+W"‘H.—Ó7{õ–®¡¡all, ÕÕÕ>œ:u*@.—WVVšššš˜˜…´´4www…Bû|ì¥xúôiqqñ¢E‹X,VÏž=égÑ;wîüá‡|||‘‘‘111ïÞ½³··W=vôèÑpcâĉÑÑÑõ¯¥¢¢BSSSµ«g½˜2™ìîÝ»ÇôìÙîÙ®]»#Fܾ}F`>•••ßVt@¡PŒ3f„ 0:`llŸÏb±èß»D")**’Éd÷ï߇)l6›Íf—””ØØØÀ?‡ÒÒRMMMúnS(Â{{>Ÿ¯:_$Áççöàóù·nÝ*))Q*• …Þ`—””ðx<ا±¡ÒÆÆÆððÚµk^^^°Ä’’mmm---“7oÞ¼{÷nÖ¬Y/^¼055mF'É¢¢"KKKz„½s^^Þ;wþüóOzÏ’’’ZÑC‡ýòË/ÅÅÅL&³¸¸¸´´ôcKWÅår%‰R©¤ÛÄzÿ0oܸáëë«¡¡xðàÁòåË_¿~MQTeeåÌ™3áþ0ŸO_¸äßDÄèÑ£uuuak ضmÛ±cÇ^¾|igg—˜˜Ø³gÏŒŒ ]]ÝÙ³g'$$Ô›IIIÉÀ#""FŒ¡T*U_Âq<&&æSÖaA]pùV4Þ^¡Ö A¯Ö§7PõGH’b±8£—'H0&0À’¢H‚T©°ôvÊí´´4’$þù籡¡ffff¦¦ði34ø¼Íq``àñãÇ‹ŠŠæÎkjjúǤ¥¥M™2¥¡ýµ´´tuuóòòà=[nn®……EËÚµkWuuµÍºuëà¸èøøøøøø¦TFMMmÍš5kÖ¬yóæMÿþý}||¦L™ÒH=ëÅápj}×ÿžèkp7üåÇ„EÁ1“•+ÅJóð‰>3ç Šüì%–””ÓoH80þ‡~¨µŸÏ§û”””ÐÛ555 … àóùtÈ©  `nn^UUïN ÅŸþ9hР#F0ŒS§NÑ!zJ†JÀ¾%%%YYYôtMŒ¯]»Ö¯_?555Õê}ssszvCX1sss€¥¥ehhh#£ÓsrrfΜyëÖ-OOO€££#ì7¤¥¥E¯?RVVV÷À†‚–––úúúoß¾UñQ×Å‹ ·ƒƒƒW­ZÊd2§M›Fw\ÊÈÈ055ý†:(•Ê1cÆÈåòS§NÑ‘šôôôAƒÁØÓ˜1cæÎûèÑ#{{û7oÞÀ.A(•JSSÓ3gδk×N ôë×o̘1u›‹âââÜÜÜ3fPõ7&‚ ‚ ÿeõG”J²[GgÀ`11 @`€žY@‰ã¸¸š$©®]»uìØQ&“1 CC‚ Ô8œo¢ÛU``à?üÀãñlllŒŒŒàwëÆ;6‡„„¬X±"!!A,¯_¿¾)³£Þ¾}»hÑ¢¤¤$.—Û®]»þýû×-¥‘ÊܸqÃÂÂÂÁÁÁÐÐÃáÔêF’¤B¡ÀqNDÁ`08ŽT*=qâìÇ~öìÙ³gÏÞ¸qãc.Ï7F¥3lÓâ#K(œ§oF•ˆ… j¬]øÌ¾3¾Dqµî¥õõõ1 {úô©§§'I’EEEºººzzz%%%p€=øçý<ŸÏçñx ƒ¢¨ÒÒÒÌÌLww÷êêêsçÎuïÞÁ`À) 1 «¬¬$IÒÑÑ‘Á`dff¾xñÞKËd2úV¶¡ÒFFFr¹üÌ™3ði9 ¸¸VÞÆÆ&,,ÌÆÆžQózõ{yyq¹ÜcÇŽgggŸ;w.<<0}úôI“&uìØ±]»v555W®\>|¸ê}uuµ††\6ïÒ¥KYYYt†[·n‹ÅZZZÛ¶m«[¢±±±êÊ4 Ãúö훜œüÁèÀœ9sè:¸»»3™ÌüüüÓ§OOš4 ¦'''Ó]o¾~$IŽ7ŽÏçŸ9s†$I™LÆf³™Lf‡¶nÝ:kÖ¬Ö­['&&VTT8;;·jÕª°°øìÙ³<}úÔÀÀ ¬¬¬OŸ>}ûöýñÇe2I’°/..Þµk×üùó-,,®]»ÛþŒŒ 8Q+‚ ‚ ò=©?:€a`€¿=“É$IŠÁÀ¤RYNN6IRFF†ººz \ADMM ‡Åöðpÿ¼jdÞÁFú(•ʆfØjHëÖ­¹\.\][[ÛÁÁAGG§ñif6mÚéääÄb±BCCáÄoS*•aaaóæÍƒ·^6l {òäI­¥©LnnîÔ©SŽŽNXXØ!CjqëÖ-z2v ¶mÛÂÎä{ö쉈ˆP(NNN‡‚°¿c\-îÖ°-á»f(%z¹ÅÙaö3æ›÷…Ê*))Q¦Ád2ƒƒƒ¯\¹réÒ%&“iaa1tèPŠ¢TWàóùðÆ@§———kiiYXXlÙ²EMM­sçÎp >=¬€Ç㹸¸üú믦¦¦°Ó><ÐÍÍíØ±c111Ý»w÷õõ­[:,ˆÅbq¹\¹\'®§+§îãp80OŠ¢T»9|ƒqâĉ)S¦üòË/¦¦¦#GŽ„é½zõÚ²eËÔ©S³³³uuu»uë6bÄÕ===Çïíí W-¥/N@@Àðáý½½ÍÍÍë¾Û“&M ær¹ýû÷‡ÃsháááË–-›6mZCµÍÌÌd³Ùôð‡ØØØ±cÇš›››ššª†âããccc›q5ZDQQÑáÇ\.¦lÚ´iþüùáááyyyÝ»w///·µµ‡½Tè_4 ˜ššRõ×_½|ùòåË—tD&))ÉÍÍM ìÙ³'22’Édã8Œ=!‚ ‚ ߬¡Y!‚ ¤Ré³gÏÌ`0–,Y2yòä/:YkVV–···X,®û’¥¥¥¿¿Ýt’$333Ï;geeõ…j…|åp'”J Ã*E•³÷Îv0³_0l!‡]ÏtLã£Ö}ø¢222žXDII —ËýŒ«Ò"‚|9ATTTÔ4G­‚ _³Fš/U\.—¢¨;ÕZÛ— IDATwïøwò711Ïàáÿµn"‘è]vvÖ›¬ËW.ÇwîÜ137÷öö151ÖÓÓûó²888äååM›6íÌ™3MÝÊçóuttNŸ>Bÿeô<|¦j&G£êŸqí+‡´t-¾+pM‡†XYYõéÓ§ñzõêÕ«W¯ÏZ)AAù4 (JMMMWG×ÁÑÁÕÕU&“444(4ÔÕ>¸,ù§044~üxÿþý ,¨µÔA999£FŠŒŒürcäK né* ‚ü'dgg‹D¢zWBAþ; uuuát¼‚ H]_û¸© &„……-Z´hûöípŽ÷¼¼wïÞíÛ·ï—«ÃG)++suu‹Å-]‘ÚZú͈ òõbÉd²–®‚ Òb(Š"I’$I¥R©ú?AJ¥Çq‚ ‚Àqþˆãø—¨F``àË—/1 ÓÖÖvpp>|ø”)SX¬Oíà¶yóæ={öÈåò#FÄÄÄÔZwçÎ/^<{ö,Òºuëƒúûûb¹··÷ž={Úµk×УGvss[¶lrõêÕˆˆˆ/^¬\¹r×®]¿üòËØ±cáK7nܸqãªU«fÍšõYª‡|º+VlܸqÈ!€åË—·øI+V¬X´hQ VÀÁÁ!>>¾C‡ƒ>}úìܹsîܹ-X%A¤éPßAä«ðË/¿§¦¦FDDìÙ³gòäÉŸ˜á©S§þøã“'OÞ¹sçáÇ?ÿüóg©çgrüøq’$é”cÇŽ1F1ìììŽ;¦úRëÖ­[ –ß…/ô:??ßÉÉ n‡„„ðx<ÕWI’$â3×ø‰deeeee6;ÿZÆqücs¨uµCBBöìÙÓìú|!-úNDùª¡è‚ òU`0,ËÜÜ|ðàÁÿÇޙǟýüLÓ¾iÓLí¥•Ð†ˆ+$%©YJ¶«¬E”%n—k‰\EH{J×E‰Úwiß´7MS3Ó<¿?νÏoîT#kø>ï׋×3ÏsÎy>Ï9gžæ|Îg¹~ýúýû÷³³³ÕÕÕË–-SVV622Š‹‹ƒ…ÃÃÃW¬Xáéé©  `ddTXXxåÊMMM--­;wîÀ2‘‘‘kÖ¬™4i’¬¬¬§§gDDÄè…qwwßµk—­­­¹¹ùÒ¥KàùŠŠŠùóç+**:::vuu¡'­¬¬”••544vìØ100ðòòjll\½zµÁÍ›7±±±¦¦¦JJJK—.­©©,X°€B¡dddÀvÈdrJJŠ££#ühbbRSSS__ÈÌÌ”’’RVV†—¨Tªz}ô°­ýýý ‚¶¶vfffnn.‰D¢Ñhðjbbâäɓᎎ@PTT fkÄÀÀ ­­ÍÒÒÒÐÐA--­¬¬,A\\\<==---555 h4šªªê„ ÜÝÝ{{{Ù¸qã©S§iiiáçç?wî‚ 555ÒÒÒ £´´Ô‚@ ÈÉÉmÙ²…J¥¢ËZÖõ-›<)))&&&8~ljjrvv–““#‰ëׯG$00ÐÝÝ^%“Éüüü°e6•””ŒŒôõõyÿþ½³³ó„ TUUƒ‚‚`uXeáÂ…³gÏ®««CÄÓÓ³¡¡ÁÞÞ^MM-,, í¢²²²O]È–¶¶¶ÄáhkkZxLg"Æw ¦ÀÀÀÀÀøî˜4i’’’Rff&“É\µj•AIIÉ™3gvìØ‘›› Ë<~üxæÌ™–––«V­ªªªÊÉÉ ÚµkF”––jkkÃÂÚÚÚuuu eô2Ü¿ÿÒ¥KOž<™3gΦM›L&sÍš5¿üòËÛ·o7mÚ K"âååUZZúôéÓüüüË—/Ž?.++{ýúõœœœ•+W¦¥¥ùøø\¼x±¢¢â—_~Y»v-‚ ¼¼¼666h;wîÜQTTÔÕÕ…¹¸¸–.] ˆŒŒtpp@eëëë»páBGGÇgôñÿ¬‹ÃüüüÐÐЬ¬¬¦¦¦¤¤$YYY]]]"‘øàÁX ""ÂÉÉ A77·Ë—/755åä䘙™±-2_¿~-&&vïÞ½—/_"ÿ]ºß¹s'$$¤¸¸XGGÇßßÿÅ‹iii¥¥¥d2ùСC‚˜™™=}úA´´´‰'¦§§Ãcccc...&“yàÀÚÚÚ¬¬¬œœœóçÏ£-³°RPP ¦¦™L¦ƒƒƒ¨¨hAAAuuµ‹‹ËЊ# xúôéãÇóòòqrr"‰eee=ºvíZRR¬rïÞ½«W¯¾~ýzúôéGŽAäôéÓ$)22²´´tõêÕ°˜ªªj~~>Ç¥ý@BBA6Õzž±š„ß??Œv€I&“_d¼‹‹i(*b޵8_"‘ØÕÕUTTT__¿gÏ^^ÞéÓ§ÛÙÙÁÕ2@WW×ÖÖ–——×ÞÞ¾¾¾ÞÛÛ›ŸŸþüù‚À­~ …""" ‹ŠŠÂ3£`éÒ¥ÒÒÒ77·—/_¶´´¶´´lß¾››{Ö¬Y³gφ%ÕÔÔÌÌÌxxxÂÆ_¼x1´µ°°0WWW]]]<ïîîÞÔÔT]] pttLJJ¢R©€ÈÈHÔpâàà500pÿþ}ô¼¤¤d[[›ªªêèçÖÅ!§R©ùùùòòò$ Agg爈Aº»»SSSáææÎÏÏïêêÕÔÔi‰£ç—/_.++ O^¹råðáÃÒÒÒÞÞÞqqq‚˜™™½xñ‚Á`<{ölûöíYYYL&3==}ÆŒ‚¨««Ïœ9“›››@ lÙ²%##cè-ÀAgg§°°0<.***.. ááá111Ze$›7oæååçææúûûóóó“H$ww÷ØØXXÅÑÑzRÌŸ??''g$©„……»ºº†vÝÇÊÊÊÊÊj¤¬|ûé‡ñ£0bÀ§Úæ6p‚ àŸòO: ôåŠ8àp8ôÓhx..!~Æ s€FC„()&.*üù""tz÷í›íqÑé8Þþ%(ª{ÀÇÀjñç7Žñ}ÒÜÜ,..ÞÜÜL$Ñh‚òòòyyyð.Ý‚‚‚"""‚‚‚𣀀Ô ‘Édx²§§ža½ƒÁ`=C§ÓÑPˆãLJ¼¼¼ãÆknnnii!‘HhEEExÐÚÚêãã“M§ÓÐó¬ÔÕÕ½|ùòöíÛè™–––‰'ÊÈÈܽ{×ÈÈ(;;;$$„µ–ººº€€@@@ÀôéÓÅÅÅGÕqBSSóرc‡.++[°`Á‰'¤¥¥üýýÉdr\\ÜÔ©SI$ **êØ±cÞÞÞ“'OVUUmllܽ{U …H$ƒ¾¾¾öövWWW.®öch4“É”———ÈÉÉyö왇‡GDDDiié³gÏÖ­[xÿþý¾}û^¾|I£ÑFìPLL ðõõõ222üüü£ìT`¶µµµt:¬I§Óµ´´à±„„<àççç wëíí7nÜ(ÅøL/^ HJJZ¼x1<ÆÀÀÀÀø(F´`"@\DHTHPˆ_@€——›Ï…€kpÐéHÿ³¿Fg2€É8ÀÕÑÙÛÕC©ªoilioiëììî¥P>_>Ê_ª,çtþv’ëß(ÙbxüÌJÏÞ×—.i*+ûü[`````|o¿{÷ÎÈȈH$677£‰jkkeddFÙˆ††Fqq1<.,,”““cÓL˜0¡¶¶ÝK|ÿþ=•J•““ƒah@OOOgg'‘H$‰­­­hõ÷ïßÃÇ ¦§§çååùûû£ ¢ B‰Dúõ×_3ÿ¥¤¤ÄÈÈ^rppˆŒŒŒŽŽž9s&Û" °|ùò .°º`|l»Ç+V¬xòäIQQQ___@@‚ ÒÒÒ¦¦¦ñññ·nÝ‚n‚L›6->>¾¦¦föìÙ®®®‚”——÷öööööš››##XøPçqqñ›7oæüKuu5¼jff–˜˜H¡PäääÌÌÌ"##[ZZ «ÿ„„„233KKKOœ8ÁÚ2Û+ÚÚÚð˜D"566ö÷÷³€Ç0hÅP‘ÿDH$aaáׯ_Cá £¢¢ØÊ°ÂÅÅÅú‘ÉdVTTèèè -ù•°²²Ú¹sçHV±˜€ß ááá«V­eáèèèåË—‘ûzxxœ9sæsZø‚ÂŒžGYXX =ÿäÉ“)S¦(++£žnŸÆwžwôâ566ÊÊÊÂcCCÃW¯^}Î}ÝÝ݃ƒƒ?§…‘ÐÑÑ),,üÌF>êKô#2¢và}KsEyYÕÛòºêʦúê¶æÚ®ÖÆÞΖrÛ µGëÆ3È8Z2Ð=Hí¢Q:¥E¸%¸dÆñDy'H‰ò0Gjy”ÐÞUÖ¬u~¿Û×1LÔ%y^³ªŠÛE·7mìmoûÌ{a````Œ90^zSSÓÝ»wW¯^ ƒ½iii‘H¤Ó§OÓh´ìì츸8;;»Q6èààpíÚµòòò÷ïߟ={ÖÉɉ­€©©)“É êîî~ÿþýÁƒ§OŸŽþĹsçN^^N?zôè´iÓ`;11±„„@MMMjj*,ÙÓÓ£¬¬, Ðßßýúu´}))©ªª*xìâârîÜ9h€M&“ÑTË—/öìYXX›[ÄÙÙ9&&fîܹ¬'±¨„ ëâ°´´ôÅ‹t:]HHHXX˜‡‡žwrrúã?rrr–,Y‚ …B‰§P(¼¼¼âââh1¶u&Ûš²t_¿~ýÞ½{aÜ¾ÆÆÆÔÔTxÞÌÌìÒ¥KÐæßÌÌìâÅ‹FFFx<Aîîn8£úúúBCCGº›0óæÍ{þü9“ÉDFèܳg™L€¾ :::ÏŸ?§P(‚œ?Œ k`ý¨©©©¦¦¶ÿ~2™<88XZZš=Òƒ#"%%UYY‰^ÊÍÍ•PQQÚu_UUUξöd›5k–””t‚X[[KII}æJò‡fÙ²elmFÃäÉ“??‘Í—ÂßßßÏϯ²²R__¬e÷ïß744$‘HË–-kjjkq¾/""">Y©d``ð™jŽ”µx<Þ@_ÏÀ@Ï@_O_OWOOW_OGOW[SSCAIe’–¦ö$­I“4'iihijhj¨k¨«©«©ª©ªªªª¨ª(Kÿkù 0;;[|½ë—Y3sßp(†@“WÿEZ¢ùŒ´+—¿Áëãë±}ûv"‘hllüû￯_¿þÏ?ÿàñøëׯgggkhhlݺõäÉ““'Oeƒ¶¶¶...VVVÓ¦M300ðôôd+ ,,•žžn``0sæÌÁÁÁ+W® Wœœ¼½½ÕÔÔÊËËá>WXXØ¥K—.\èëë ³Ü¼¼¼bbb-ZäââbjjÊúDGURR 577÷óóóðð˜8q¢‰‰IJJ ‡ƒÅH$’‰‰ ™L^¸páЧž5kê[Á¢~èâB¡ìÚµKNNNMM­¯¯oÏž=ðüÂ… -Z$$$Ï\¹rEMMMNN."" ȶÎüàñ¾}ûLLL,--‰Dâ¢E‹JJJàySSÓÞÞ^¨022êïï‡Ç‚x{{GFFΛ7ÏÙÙÙÌÌ YƃáöíUTT”••Ÿç}lƒ‰†^ÍÛlÖ¬Y‹/ŽŒŒ„9YOŸ>Ý××’˜˜¨¯¯O§ÓOž<M¥R,X ((ž’’B âââddd®\¹’™™yúôiwìØ1¨ˆ¤P(¸ÿ>77·ƒƒƒ——77wxxxbb¢´´ôëׯ]\\‚‚‚bbb`≎Žœœ4D <<<99yüøñÉÉÉ222&&&€“'OFDDttt(**=ztÆŒ€èèèèè訨(ƒ±~ýúÌÌLƒ1eÊ”S§NÉËËttt6lØœœÜÕÕehhxæÌ¨ÇÌÉÉ9xð`II ??¿‡‡‡««+TŒnß¾ L òóóétz`` Ôõôôxxx<}úTVVÖÎÎ.))éÑ£G¬½Š  £R©RRRçÏŸŸ:u*kIYYÙW¯^A+0nnnŸŠŠ ÒÒR...KKË?þøŸŸ¿ÿþ’’YYÙ#GŽ˜››Èd²§§çãÇG’ÄÔÔ´¢¢BZZZBB"--íÉ“'¾¾¾µµµ'Ož„ïîî.&&VVVV[[OB***¶nÝZVVfdd$+++,,ìçç¸páŸþI&“ÍÌÌ%%%«ªª,,,<Ý‹Ž=J"‘vîÜÙÒÒ²zõêC‡þøãÔÔÔ»wïÂÉ©££óúõk8:(Ã.”°¢¢¢¹¹YLLìòåË0ØÊHâ¡$''Ÿ8q¢¦¦FBBbÓ¦Mnnn€ÆÆFCCC“ÅÐÐ088 V¶W_½zåîîŽîÏ[XXìß¿ßÂÂÂÝÝ]ZZúÍ›7ÅÅÅzzzç΃²µµµíÛ·/--M@@ÀÕÕuË–-€”””C‡µ¶¶ ìܹ†nTWWÿòË/ EZZZDD$--MGGgçÎ!!! ,€3vØéíåå*))ÉÇÇ·k×®•+W²~‰RRRäää"##¥¤¤~ÿýw8‡‡ö ÔÓ'uvvjkk¿yó†@ ÄÆÆþöÛoMMMúúúAAA à ÁáõÅŠ¸¸8‚ Ï_<751%p»þ?ªœ‚ôÓèdJ?/7ž‹‹ a2ùxyxy¸û¨ý4úÖ‡½¼|¹²ª¬ŸöQÚ]E^ž–ò/²²r~~þç´ammý¥äÁøL*++?ømü ›6m:uêÔ‘ç§!44ôó½ ±ŽÅÀÀàLrròÀÀÀœ9sÆZOÇÈÈè;Q ÄÅÅ_¿~ý]©¾ÊÊÊ/_¾DEE±Æ ÌÊÊzðàAnnnooo@@<ÿÁ­‡ª­­}ñâEjjjjj*Ìœ xúô©££cffæÆÑ¨€øøx333VÕz#“ŠŠŠ]»v­Zµª»» ªªšššúîÝ»7nذmïA ¼y󦨨ˆD"íÙ³½”œœœ‘‘QUUhmmµ³³srr*))yþü9ÛÒ––¶zõê´´4??¿í۷Ó>>> £°°ðöíÛ¨ÙŰÞ¸qãéÓ§ïÞ½‹‰‰A}Á8säȘŒ¶¨¨híÚµ€öööeË–­_¿¾¼¼<00ÐÍÍ­¹¹àëëK¡P nÞ¼yóæÍ¡Meddˆ‰‰ÅÇǧ¥¥Õ×ׯ^½zÿþýåååË–-sppèëëƒÅ’““ÏŸ?ÿúõk4±.9-nBBBpppDDD^^//ï¶mÛàùÞÞÞwïÞååå={v÷îÝ/^LJJzòäÉÍ›7¡£Jii)ªz ‰RRReC‚²4¸<ÎÈÈ044§NÒU™(WÝÐ:QFbh•þ:íÓâþû ja0¢:Éü|3„„¹F¥ÅøDUUËwý“1kÖ¬os#!!¡õë×·µµ]¼x‘íÒÙ³ga8VÖ?i?:߬c¿=K—.ݺu+4tppÐÔÔ´´´ŒŠŠ:{öìÝ»wååå·mÛæîîJ¼|ùRPPÐÕÕuçÎááᬭIKK;vLVVv``àÒ¥K»wïfMÂû?²ø•&cŒ\Â`“àß°‚?+ßl|mll «á•Jmooß¼y3‡ÂÀ —àC)ZûúúzzzÐŒ*rrrp£@ ÐöõôôÄÅÅÓÒÒÊÊʆ eºœFÛ‰‰‰¹téRKK oiia‹x:88xüøñäääÞÞ^... …B£ÑxyyÁ[Âx ÃfuEA“³âñxnnn …ÂÍÍÝÛÛ‹Zp^ðkhh>|8  ¢¢bÞ¼yþþþãGììèÑ£þþþRRRöööuuuoÞ¼AsÇôõõéêêR(2™ŒZæs~@ss3 á‘——G㲎 kùaÓâ²¶#--ÍËËÛÔÔÄÇÇÇ6Ð6‡MÜ èééfO'?Òà¢&%I} £ð;vì@µþ¾¾¾¿üò ›Ó,ga(ʪU«H$’ŒŒ xÆö,†††ŽŽŽÃz:qssóóóóóóóñ X„•€€€‰'‰D¬¬¬¼¼¼ & “9))iÊ”)ð@WW—H$Nœ8ñÒ¥Kl¸»»Ÿ>}ðþý{L»¶¶–@ Àp>ƒƒƒžžžÒÒÒ“&MBûê+u쇉‰‰¢¢¢˜˜˜’’’°°pee%àîÝ»+V¬ÐÖÖõöö¾{÷.|™TWW/Z´HVVVLLÌÁÁ¡¤¤„­5"‘¨¯¯/)))--=aÂ4« µµ5>>~Íš5ßòé00>qqq&“‰`àp8&“‰.о*†††QQQK—.EO —aaa0³iVVVYY×(öÀ Ê ®®~¬««C×'lz|GGGèŸoee5l(Dh‰©¯¯'‰555»wï ÊÏÏÏÉÉ™0aòß_ÝQQQ?NLLÌÏÏ¿ÿ>à¨d!‘H» ³‡@¯u6 ‡ÅÑÑñÁƒ¯_¿¦R©C[ƒ¾ÔnNFFæÜ¹s¥¥¥¿þúëû÷ïI$’™™še6??ÿ×_A3ײ¦°"‘Èúû¿®®͹;ìþÊHiqYÛimm¥Ñh£ÌÝ«¡¡QTT„¶ÖÞÞ®®®ÎZàƒƒ;ñXY¿~½Ý«W¯rssmll8´†{_ôIÑ‘,ƒXæ…B^ô0ÂóçÏáx½~ý®´§NYQQann#°2š/×HÓ{4uY¶g¸¸¸lmmãâââââæÎ+** 8f8þñ±©ÿü¸g0{(}t:cp9QAÎunnny9R{w__?­¾¥mèÌÀ}êžãÐZ dSú¯utw~É_ðjjjiiiÍÍÍ[¶lqvvFÝ„îÝ»úæÍ›éÓ§9rPWWçàààããS__ïèè¸dɨ`¶0+3fÌxñâƒÁhmmíëëËÊÊB¤®®®³³SOO–‰ŠŠJNN.**ÊÉÉ~V±±±gÏž¯¨¨àããƒÑ>påÿâÅ‹¿ÿþ; €íÍkcc3sæL®®®7ž?~¨§ÆçóÁ1%“ÉEEEIII‡‚Ãäíí]]]———––vïÞ=¸ô<~üxýúõÙÙÙÇŽüÌßß¿¼¼<444+++$$äÊ•+lï)΄††ö÷÷WVV644œ?ž³tè­UTT,--?~̹d~~~hhèË—/›››ïÞ½K"‘ôôôdddÒôfÍI<Fê;;»;wîDGG£ù˜9d8þѳJ‰ $È/ΪÚûÕå¥û¨ýPÀd2¹ùûÌö®caì¸p¸OTŒ ‚"2_÷Q犌ø«ôcï@V¯^}âĉ²²2€““\W[ZZ¸ fff‹-¸»»_¼xñï¿ÿ^¼xñ°…Y‘“““ÈÍÍ­««›3gNYYYqqq^^ºàìܹj—,Y’››»råÊÛ·ooÞ¼ZË?~\QQ±«« €Çãa¸I[PPÀfðóÛo¿M:uîܹóæÍƒâa|q>8¦‚9rDPPP__ßÀÀSddäÝ»wÅÄÄÄÄÄöîÝ{êÔ)ÂdúôéPYøË/¿ÔÖÖvuu‰‰‰¡÷ ]¸pá¸qãÐøÀ£†‡‡çýû÷ååå:::pn’õë×ïÛ·OPPðÞ½{666ééé²qssS©Ôüüü3f fÎÎ΋-êîîNMM…z}nnîüü|uuuØlí̘1ÃÓÓsppðÙ³gÇŽC$==úRtuu¡–aíÚµP &&&ö•:öGÄÓÓsݺu™™™/_¾„.ˆ .}:77·½½=º{Ä@˜2eJii)kvUVfÏžžž¾wï^™k×®Á¿¡ŽŽŽæææòòòZZZlш+W®|ò䉹¹9@ø`ZÁñãÇGGGœìU8¾ŽŽŽIIIÀÀÀ5kÖHKKëèè°†o„Y9ŠŠŠôôôàCáp¸k×®ùøøL›6­¿¿_EEeïÞ½‚„††zzzâp8UUUÔhþ:ÕÔÔ†q‡2ÒôÞ¾}û¾}ûöîÝ{ðàAÉ’3#õŒžžž˜˜Xmmí/¿üÏ ŽkjjDDDÌÌ̾«…Ûˆ Ÿeç[˜ýeô]Ck859é¾þ2u §VÛÜŽãââS&—ÄqáÀ¿KûšºúA  óaG 6žzí‰ ¿>ì%e>ž%ãD†½¨qÿÕ|ãG¤žº}ûö¹sçš››ñx|sssLL̼yóÖ®];eÊ”­[·^¾|¹víÚ¢¢¢}ûöQ(tÂY[[/\¸ÐÝÝ}ØÂk×®…n${÷î=tè«««¦¦fCCƒAyy¹¬¬l^^žŠŠ |—)++ÇÇÇåשS§*++/^¼hjjêéé¹lÙ2x;11±çÏŸóññ™šš¢>fÖÖÖNNNNNN‰‰‰ð¼··÷o¿ý–““ƒþ ÇøâpÓÊÊÊ¡Ãdmm-%%USSI/_¾\ºti}}}hhhJJ ˜WLL ÆJÝ´i“²²ò®]»àù™3gÖ××WTT@íÃè'ØÀÀÀñãÇ£¢¢ ‹Á¡C‡®_¿¾yóf€±±1úw7)) ¾‡}X{{{mmm___rãÆK—.•——[ZZJKK·´´hkkWVVÆÆÆFEEAÍ}VVV@@ÀóçÏ Ž?>yòdeeehf–œœ‘Ë—/¿|ùòÊ•+c-ÆWáÑ£Gly"1¾_1£!Žëÿ·âe$EÉíïË«²KjúhŒúÖîrymsG7EˆŸGŽ ç‘©íÝ”ÿ÷ìúÔxfLæÈ)9¶‡€LP]]½}ûö .¼}û¶¬¬L^^žƒÛŒ¬¬,«­Kmm-‡p)¡¡¡T*•J¥Â4¤Ð^úÙ³g3f̘1cFzz:ëvèo÷þýûQ&k),,üóÏ?vîÜ9/ ŒOãcÇTHHHTTÓšššQÆ×\¹r…L&+((@}6ø˜ ÆÇÇçëë[TTôðáø¸¸;wî¬]»Öå ’gƒ‡‡gpðYKÓÓÓKJJ¨T*ÌÌD ÌÌÌnݺ7üÓ§O¿sçN}}½…… W[YY åùÃf̘qçÎÞÞ^yyù3fDFF¶´´p°zø²ûÓí·wïÞ’’’šššY³fñññA{ÂÂÂ5kÖHII »¹¹¥§§÷÷÷shB¡ttt477×ÔÔüú믋/j'…ñÐÐИ˜C ÿ@TUUÁŠwïÞ]ºt Zìb``|{FÔðñü¿ZT€ŸÏxŠž’¬$}€ú #·¼¶åUnspŸ‘'ò÷ã´ûÿ&÷Ñúúúþ£øò²~±&{zzøùùa´ÿÔÔÔ·oßr(¼dÉ’ôôô””ƒqùòåÎÎÎa3| ËŒ3ž={ÖÝÝ­  `llüøñãææfÎfÞË—/.//ïëëÛ·oß‚ †ša…F£­[·ÎÇÇçòåËmmm.\¥„˧©ŸŸ_WWWccc`` ““ÓhnTYYyàÀ«W¯^½zõìÙ³999%ÌÓ§O+**‘””äååê˜Çd2ûûûét:‚ ýýý00 •Jˆˆhjjêèè¸~ýzrrò°QJKKaì6÷¿•+WB™mll}}}ñññ}}}|||âââC…ÏréÒ%h 9sæÌ‹/sÞŸùzûA¡PÎ;WUUÕÙÙ™˜˜_P]]]¯^½êêêÊÎÎÞ¼yóîÝ»axª©S§†‡‡wuuQ©Ô+W®¨ªªòóóüüü***ÉÉÉ/^¼èêꪪªÚ¿?ÌE"‘RRRnܸvàÀ±}d Œa9yò¤±±ñÆ¿Ÿøç£¤··×ÅÅEIIiéÒ¥ÃÚ´c``|FüåÍËË ƒ»þó4e::»d$ÊkšfšO¡Rû:û1Óh2¯oíb0e¤Äÿ©‚ûDÛ¾!y8P84ÇŸðˆNCÑÕÕuvv622RPPÐÖÖæàV ——ˆˆðöövqqÑÐÐHHHš,d$&Nœ(&&fbbVQQvi„²lÙ²úúúÅ‹“ÉäY³f î>,¾¾¾ãÇß´i‡»zõª……ÅܹsÙ–b|>aLvïÞ­««ËÍÍíää44¤êP×­[çáá—úþþþëÖ­{ñâ\ÈF˜šššÍ›7·¶¶ ¯X±ÂÊÊŠíÏž=CÝ«ÄÅÅ ž?¸zõª§§'FSUU ãFµ¯¯ÏÓÓóÝ»w¼¼¼fff0)ÀÊÊjëÖ­VVVÐAË—/Ãù©®®>l®¸3fÉd°ÐØØ˜J¥ ^ÈÆ×ëØ<ÿ×_P©T…C‡­\¹ÐÛÛëææVYY)%%åæææéé ËÿöÛo;vì˜4iƒÁÐÓÓ»qã<ñâÅéÓ§+++ ìÙ³§¦¦FXXxÊ”)—/_Æáp\\\’’’ƒƒƒt:ý›ôÂÀÀÀÀø(öìÙóƒšwéèèdggµß ‹±–cDFŒ;PTQm0I ‡ÃA°Øí#‚8€Åo«+êÛ”åIýtÆ»Ú&Ei‘)Új°LSS3 áš@¶qtV”œgÑKg ½¤ÊÇ»xÜ0Ër&‚d "‹ï?µ]1Æ×FWW÷Ô©ShŒïAFŠ;Õ ƒÁ`Ðétø‘N§Ïž=‹;€ñ³ò£ÇàLUU•……çØõ(†††h.ºÏ!::¦Züü¦>www}}}¶|Fõ˜$©¦¦fh   &“¹sçÎê䑸 þàà ››[ZZš™™Yhhèè[öðð˜8q" äüQ ƒøE&ưcñÓðÇœ?‡Ã MýUùü¸#¾Ý™ƒƒƒƒÿ8"‡Cp€‹‰ A4”äéL\U]cOoIZU €ŒðYUÕ½ÎKÜäö<ã9ã¿ k;PK£×ëè/:yS ``|?$%% Œ>˜-ÆOÌäÉ“nÓ³ÞÞÞ+W®¼xñâ›ÝññãÇÕÕÕ%%%Ÿ¬®ª­­522ú"ÚŸÿM BBB ÙΓÉä'NäææÂD‰?#N&ÚÀ@gg×?^ÿõÀápL8@' Ì-ÁÏ/ÃÇËÛÝ݃&$¤Púx?ÂÔŸ>IIû¨ØÙ¹¹±›Ý kªG*Ö58˜/(:õôñYs0ë Œïˆ… æçç_¹r…‹kÄÈ&ß’wïÞõöö¶··µ c‰¤¤¤¨¨([BèŸ ƒñÝÚ5(++Ãà¸?+ñññ&&&ßRR__¯¤¤ôÝŽøÿ2ÍÍÍ"""£T @Îï'…ÖˆóIHX¤î}G_•Éd2™A˜L&‚ ÿ#\8‡ãBp\8‡ãâ ðóó ~–dRúúŸg…]ó?ò¾ @utÉe ã7ÿêìê†çèïñí¹wïÞX‹€ñÿ¼{÷Ž‹‹kÖ¬Yc-ÆØóæÍ›ªªª¯´OGGÇÃÃ#$$¤¡¡aõêÕ[·nuwwÏÉÉ111¹rå Œ]ûÛo¿555éëë)((Œ²â“'O|}}kkk544Nž<©££+nذæýY»vmFFSæÀt:ýĉlB^ºtéÌ™3x<~ëÖ­7nTTTxzz–””ððð,\¸ðرc|||¬UNž<ÑÑÑ¡¨¨xôèQ˜ )<<<11‘@ äççÓéôÀÀ@3¨¥¥åÀééé cþüùçÏŸgõ,€'''wuuž9s†‡‡AãLJ……ñóóïÚµËÓÓs¨õ~[[Û¾}ûÒÒÒ\]]a¼!www11±ŠŠŠææf11±Ë—/ÃFà hll´²²*..ÖÓÓ;wî[Â#:~òäÉèèh*•º`Á‚€€AAAA¯^½ÊÇLJ¦FfãÁƒlîôl\PP°lÙ²ÂÂB*%%ÅßßÿÙ³g¬UÈd²§§çãÇeeeíììÐóùùùû÷ï/))‘••=r䈹¹yxx¸¿¿¿Á®]»¦M›6tø^½zåîîþêÕ+؈……ÅþýûY…\½z5Fƒ‘˜n߾Ͱ,99ùĉ555›6mrssö©Ù¨¨¨Øºukyyù´iÓdeeEDDüüüΜ9SUUõûï¿(Š‚‚BCCœ]ÃŽÅЇ…ÝuèСÖÖV;w®[·n$†$^^^ áááÿ¬¬¬„„..®èèè3gÎÔ×׉ÄßÿÝÈÈhØêááá ’’’oß¾e2™&&&^^^«W¯†³F}/_¾¼½½ÝÀÀÀÜÜ<((hد-œ·eeeµµµaaaðä÷ÀˆÚ‰ ‹é§åÆãææþ"ª¬I.ë´œW?ñöº+?(éè4™±èH€øèòüa``````ü/ÓÛÛ‹©0 “'O~úôé×¾KRRRrr2…B±°°xõêÕéÓ§•””–/_~ýúõÍ›7§¥¥ùøøDDDLš4)$$díÚµ=‚†ºœ+Ö×ׯ^½:$$dΜ9ׯ_wppxõê•   ##ãþýû<<<íí탃ƒqqqpEÄJoooYYYnnnee¥¶¶¶©©)‚ ^^^Ó§OïèèX¹rååË—·mÛÆZKUU555URR222rÆ ¹¹¹0NZZZRRÒ´iÓ>|¸}ûöׯ_#²fÍ--­ììl>>¾7oÞ í¢ìììääd€µµu\\œƒƒCLLLllì£G$%%Ùnââ⢯¯Ÿ——×ÞÞnoo¯¤¤´`ÁÀƒRSSÇïëë{âĉ³gÏrøöíÛ111ššš>>>›6mJLLd½E```VVÖƒDDD¶mÛpôèÑøøøÛ·o?xð@JJ *S†R\\ÌziØN&ÿý7Œ ½|ùr¶F|}})JAAA[[›´Dhoo_¶lÙ‰'¬­­³³³W¯^––¶jÕª¬¬¬@yy9çá–ëׯ ›¼IXX8$$DUU5??ßÎÎnÊ”)S¦LáÜ“É\³fÍòåËïÝ»÷üùsGGÇõë×s®2t,†}X"‘¸mÛ¶ˆˆˆ©S§vwwsv…v’:thöìÙáááaaaÿý7×_ýuàÀëׯO:µ¾¾æðiŽ¥¥¥Ý¹sÇÄÄäÕ«WNNN¯^½:~üxjjêPÏ--­èèè¥K—ÂŽåðµMNNNMM•••ý®RÑh÷‹Ãḹ¹? ^^Þ/eTŒãæž}â”ÅÛ5©Äb“ædå8ÿ†©0000000FC{{;‚ñ/ßÀÁdÛ¶m’’’òòò&&&¦¦¦ÚÚÚ‚‚‚ÖÖÖùùù€°°0WWW]]]<ïîîÞÔÔT]]=šŠIII&&&óçÏçááY¿~½¨¨è“'O`E777^^^'%%5cÆŒ;wîž]BB† &L˜7‡‡V)--ÍÏÏ?tè???‰Druu… °··?~<`Þ¼yyyyœ¶µµÕÕÕåááñööÎÌÌliia½Ëµk×<(--- °gÏx‹ØØØ 6ÈËË Ž”¢««‹5©Ù°ìèè èééyøðá²eËØ‰÷òò”——G·Çôôôlmmñx¼‘‘ÑÌ™3>|ÈVñƒÃ÷±Ìš5K]]‹‹K__ñâÅYYY¬RXXØÚÚºmÛ6<?cƌџ:#=,/**êîî7nœ¦¦æH Ž4Iøùùƒƒƒ>¼qãF999À76nÜ8}út...yyù‰'r˜c:::0G˜¡¡¡ºº:‡h‘lpøÚÚÙÙÉÊÊ‚!^ücËã©ÂÍ/ ¬¢:ÖR```````üx ßÓÎ ÆO@€ÒÒÒè1…BÔÕÕ½|ùòöíÛhù––èìÀ¹bssó„ ÐZòòòMMMlw8::¯]»vØÝiŸ¤ä?iÅäääà:§µµÕÇÇ';;›N§ ÎséÒ¥––<ßÒÒ‚*YÄÅÅáçææ¦P(222lŽ lHHHÀ~~~øÍÍͨ‘?i¸Xãõõõ ÍvL§Ó544Ødàçç‡}ÅA`´qaaaqqñææf¨YP©ÔöööÍ›7C'pAh4“Élnn–——‡e ÈPÄÄÄz{{ÑÃv2Ü'“ÉwîÜ144”ýïf'…B!“ÉèÐ!¨««{óæ ªdéëëš‹ýƒÃ÷±¼y󿨱coß¾E¤»»›ƒ%?Jss3‘HDýçá œ3CÇb¤‡½qãÆ©S§|}}õôôüüüôõõ‡mÃ$ÑÓÓSVVnhh@]6–,Y2Êêè÷777ðé £üÚ~?ü0Ú ŒOS`€ïcöz‰ IDATŽD"ÙÛÛÒ‘›"‘ÈêQWW'##YŸËÒÒrçÎÅÅÅ)))iiiCÛhoo‡kWèq 8|ø°  `zzº€€@llìåË—Y«ÔÔÔìÞ½;))I[[0mÚ4_(YYÙ¦¦&ÆËË;ú§# ð=`kVXXøùóç£1Oæ 0Ú8…B¹ß I9@@@@\\<,,lÒ¤I¬­‰ÄÖÖVxŒ°¡¥¥UYY‰.,‡ídiiicc㤤¤¨¨¨+V°µ $$$""ÒÚÚ •èH$’™™Ùµk×8<ï°Ã'((H£ÑÐ2C3sèÉõë×ïÝ»×ÞÞÇ{zzŽæýI$›››¡‚ ¾¾j:`™ŽŽÖ*CÇb¤‡:ujdd$F;wîÜ–-[222†•Ã$¹víT¾Áà$©ªªj”ÕkjjÐãêêj+++À±Y»e4_Ûï,¢8Æ÷‹‡‡Ç™3g>ªJxxøªU«¾’<?(chÇ>^¼xaii9ÖRüC{{û¤I“(ÊX òµëÉ\\\Î;—““ƒ ™LNLLd2™£©hee•‘‘ñðáCƒÚÕÕ5l@ ^^Þ%K–¸»»ëèè »ÑÍÅÅuìØ1VZZzëÖ-[[[@OO²²²€€@ÿõë×ÙªÉd~~~À_ýõîÝ;rjjjjjjîß¿¿··—F£effŽæélllþüóÏúúz*•úÛo¿ Û¬ªªª¯¯ooo/“É,//ýúõH­q8!!¡°°N§L›6mÿÖÅÅåÀpÕÚÜÜü×_lmmoܸÑßßöŽóæÍc]²Ûɇ‹/æåå-^¼xØN€q¨T*ºB¶±±ÉÌÌŒ‹‹£Óé°?ëêêØ*;|'Nìíí-))Ü»w¯¶¶–­–¤¤$ƒÁÖŸL&kiiáñøúúúQF›ÖÖÖ–’’:þüàà`FFì:€ŽŽNVVV__àÒ¥K¬U†ŽÅ°K¥R©T*//¯¸¸8Œm×ÞÞ.%%UQQÁÚàH“¤ªªÊÏÏïâÅ‹/^ †î'«V­º|ùrvv6‚ uuuUUUæØ»wïnÞ¼ cyTWWÏ™3 %%Ŧ_Ê(¿¶ß˜vã»àÞ½{–––&LPWWwtt—ã(ÉÉÉ!‘H===¬'çÎÜÝÝ-%%e``€®úúúPÛN Œ/BMM ‡Tg>>>»wïþ–ò°¡ªªŠ~ã$$$æÏŸ?Òãó177÷óóóðð˜8q¢‰‰IJJÊ(·åääÂÂÂŽ9¢ªªyûöm¶þ(ŽŽŽÅÅÅÃ^VUUÕÓÓ[¶l™‡‡ æïåå³hÑ"SSS¶*ÚÚÚŽŽŽæææË—/öìÛÖ:8îÚµk“'Ož4iš@3Ë–-³±±±°°022‚Þ<ÿML›mooŸ6mšŠŠÊÖ­[»ººFjƒÀ^^^jjjEEECçùž={Œ­­­lllÊÊÊvvvsçÎ;w®k`V–.]š‘‘A&“áÇa;°páÂÆÆÆ °)@9|øp{{ûœ9sV­Zeii OŽ?>&&&<<\SSSGG'((h¨:iØá \³f••Uzz:4£`E@@ÀÓÓsöìÙJJJÅÅŬ—7nÜhkk{äÈ‘ÑDpqq………ݽ{WYYùìÙ³ööö𼑑‘•••¹¹¹µµ5›ÏÈбöa ÕÖÖVQQ‰ŠŠ‚Q' Ûëa'Éààà¦M›6oÞ¬§§§  àëë»iÓ¦ ‹C‡mß¾]QQÑÞÞ¾¥¥…Û3gNff¦ŠŠÊ©S§®]»&&&ؾ}ûÑ£G•””BCCGê–Ñm¿p߃c¬@„Éd2™ÌÁÁAø?„Á`ÀÌ5 ƒÁ`À:N§Óat몪*%%¥¡–ŠCinnF•ý#qãÆ ooïC‡ÍŸ?_HH(==ýÑ£GAAAðôöíÛGÿDááá<` ÓmjjêîîŽÚTTT̘1f–‚Y¸ƒ‚‚àïÑÈÈH˜ilè6 ÆÊãÇ­­­ÇöOMM®®nww÷ÐKoß¾ýå—_Þ¾}ûÉᜑÿ¦Ëþ„¤÷jjj7nÜ€±å¯_¿^µjÛjáç‡Ã%&&Ξ=›Cƒœ‡^ÍÛì{ ¹¹yÊ”)ÅÅÅãÆkY>…¢¢";;»ÒÒÒ±äã€KÙaC*²2}úô€€¶ô‡?~~~ ÃÏÏï+µÌd27oÞü•ÚgeØßß'^_¬ˆ‹‹#òüÅsSS”ðÌvcŒ¡Ñh~~~¾¾¾k×®•••7nœ••ÕPËÒ .L™2EMMmݺuh|©––WWW ˜—epppûö펎ŽýýýŽŽŽ‘‘‘è¥ÈÈH )))øÑÞÞ½ɺávïÞ½˜˜˜/þÈß6ÛrEEE ­­™™™››K"‘h4¼š˜˜8yòdx ££C ƒƒƒÙyûö-@8uꔑ‘‘¦¦æ™3gÐK·oß600  ,x÷î‚ öööjjjjjjÅÅÅ¬í¤¤¤˜˜˜àp8ø±©©ÉÙÙYNNŽH$®_¿AÀÀ@wwwxÚKS©TA\\\<==---555 ”””ŒŒôõõyÿþ½³³ó„ TUUƒ‚‚`uXeáÂ…³gÏ®««CÄÓÓ³¡¡ÁÞÞ^MM-,, Aƒ¶¶¶²²²O3Ýÿö´µµ%G[[ÛÐÂc=¿:L&óÂ… K–,ù±T ãáÇL&³§§ÇßßÑ¢Ec-ÑGãééùAÕ@JJ F㬟 îîîßF5ð¿Æ÷®ûÄÀÀÀÀøéÉÏÏïèè@Ý2!l¦¶ ÁÁÁ111rrržžžÛ¶m»uë2rVmæîîŽÃáÂÃÃyxxìííýýýkjj‰‰‰aÝÐX²dÉâÅ‹ûûû;;;ß¾}ëííýûï¿ÃKOŸ>íèèšw ãÇ‚uYXPPš••5~üøšš<?aÂ"‘øàÁƒ… """œœœqss‹Ÿ>}zwwwCCÛÂAîînAž?ÞÜÜ ó~)**>~üØËË+..NGGçâÅ‹+V¬ÈÈȈŒŒÔ××G÷BY›*((PSSƒgqppÐÖÖ.((àç燱è%ÖÿáÁ;wž>}J"‘àǧOŸ>~ü˜——A'''ƒ²²²öövkkkeeeFëÞ½{OŸ>•––Þ¿ÿ‘#G‚ƒƒOŸ>œœ|ýúuh;›‚yÎUUŒtQ‚$%%±ž\¼x1,//¿víÚ÷ïßRSSÉdr\\ÜÔ©Sa謨¨¨»w睊©YZZBËUUU!!!!!¡Ç„„„Љ„&Z¯­­ Öÿ@KK yÄÄÄÐHfõõõ222P50ØœKѵµµt:ÝÐÐÊðûï¿£·`Í3f†Jooïe—X¼x1ŒŒ9c›B¥±±QVVvèy5æ óèÑ£Oð´ÿ„´5?óçÏðàÁWjËòÃ)))ïÉŸ’mÞÕ7Ø…øç‚ü£²Fݶp‡Ã‡Ã~ ÏÅ%$ÀÏdÐh‚ÈŽ—1<ïèAèôö×Z‚¹èt@ýà`ŠÐ¸É‡ü —Ø|~ãcŽ®®®¸¸xBBÂÚµkÑ“‚°:‰ÄúúzxÜÚÚJ£Ñddd˜Læ°Yµ§M›6{öì¥K—&&&¢’.\¸sçδ´´¤¤¤èèh6æÌ™³}ûv"‘¨¥¥•››ûUžcì`Û_±bÅŠ+:::6oÞ$--mjjëÖ­U«VÁÂÓ¦M‹8s挫«ë«W¯ÊËËÑF`Ž4Öfá]H$’££ã¦M›X€ȆÝÊÖÖÖ†‰ÔØØØßßϪó‚ÙÂaq}­€íIa;¯_¿fU±š k!RQQ¡££óÃí½[YYÁàc(¹AHHˆ¡¡áX €"&&?V*ooo˜|NHHHGG' @WWÐÞÞ~ìØ±û÷ïwttÈËË/]ºtëÖ­t:]YYùÍ›7_<Û˲eË8$ûø©­­5226‰àÿ±wžM%]ž› ½wQP‘.* "ŠéXV׆ }•eÅ‚«ˆõS×ºê® EÀŠÛZPAWD¤Xé(¡&H¹ßÙÍfSQÀ2ÏÌ;sfîä’93çÄ'òå|õ¾j¤žàpyTЬ¼¬Œ,‰L& €€óq.—ßÖÆe³9LV;“ÙÖÂnomå¶·sy\þ»÷ µu¯J*J+j*ªß¿g40[Z?]¾¦k©Ã]ÞoÞUe"Ñ­•ÉXºðÐXïʯ͗)@ Ä‘‘‘Y³fÍÚµk;V]]ÝÔÔtéÒ¥¥K— —ñóóûý÷ß_¿~Íf³×¬YãååÕ§OŸ¢jϘ1cÖ¬Y&L¨ªª‚9rrr&LX¼x±ºººø¯2™|úôéC‡‰ä#¯„ßÂNé 2228Ž‚‚‚¢¢"™L†ùÁÁÁ»víÊÊÊ?~<Žã,ëܹs, š½Š þ»Æ†‰™3gîØ±#33úW;wîÇSQQáp8åååâ•xyy¥§§Ã¨]–––VVV‘‘‘ÍÍÍmmm<ÀqÜÆÆ&==Åbá8¾gÏð_C‰ÂXZZš™™­X±¢¹¹™ÇãÂåétú›7o—²³³UTTLLLÄ¥ýò155í¸@oMžG^^~úôé]¾=00°ºº:++ËØØªnY,Ö¸q㊊Šâãã_½z___CÇw®®®P+@ z©Úw5Õ¯^¿~YVò¦ª¼¤¶º´á}%³¾¦­¹–Ç®ÃÚ‰Üf¬½ okä±ÚYõêT’ … ÙGVCIFGEA‰,‡óci{óúUh@EÄ|À¨¿ª'Cv~SøpŒçñg4×¾ÿĶÑ»„……íÙ³çĉƒíçç'\ÕÍ›7W®\yìØ1{{ûòòrزeË£G®_¿N¥R.\¸aÆõë׋ãë‘ø8ÐmÈ!7nÜ<;ÀãÇ/_¾ ðõõ={öl`` ÄNihhˆGÕ9v옣£cVV¬jÆ â](++›3gN||¼‹‹Ë¯¿þÚ±±[nnn||üÝ»wétzii©°W]Þ¿ïçç·nÝ:6›]\\ ó%NQ8Ž{{{ïÝ»—H$þüóÏ‘‘‘§N,\¸0!!ÁÞÞ¾±±QÜPBä*™Lž4iRrr² àܹs®®®êêêË–-ëàaA®_¿~íÚ555µ5kÖlÚ´é×_•8Ú"“¼3]ø(Ï;wêÔ©ëׯÓéô9sætfÀµµµ544nݺµ0ÉÉÉ¢/©g0€Q(r …B‘£Pä(rru¬¶wÍ­->'°Ú¹Í­ÈŠ"#+####ÓÁÔìÎûw¯ÇŽl>wºcÕ€Y s"am[â î¥}¸4@ „$._¾ÜÖÖ6bĈžoÚÑÑñÂ… =ß®Dh4Ú“'Oäåå{[o„~ýúÉä°°°ÒÒÒÆÆFÀ™3g¢££•••µ´´–.]š˜˜ “É%%%222œ?~À€0Š££ãСCoܸ!\¹ŽŽŽŠŠJNNNzzº»»»¾¾~AAAzzº££cÇ?Åq_½z5…Béß¿¿­­í‹/ÄËœ;wÎÒÒR__?%%eß¾}€ºº:G˜sîܹ¨¨(yyy==½3fÀLiòóóƒ*¶–––k×®‰Ä‘Ÿ3gŽƒƒ@ÐÓÓ344=ztÕªUêêê %22~‰Ä‡ñƒõH|{{{æsĈeeeðÙfÏžM&“aäÅœœiÊÏÏÏÏÏ‹‹£R©222YHìÂÅ‹ÝÜÜÜÝÝI$Ò¢E‹:ö¼@"‘Z[[sssÛÛÛõôô®vÄ9þ¼]hh¨ŒŒLŸ>} ÏT eŠ “ÉAAA rrrË—/ÏÈÈ€ùD"ñÅ‹ÐÊO¤-ñ«AAAgΜáóù€¤¤$¸~îøaAüýýÕÔÔ^^^ÐŒåƒß ¤uHz”gΜùñÇõôôäåå###;9àAAAð°FSSÓ7àìíxˆ¾X:³ÇñÖvN3«U†D$8Ÿ/+C–!“ZØ­íœÏ> cßžß^¶¶”j×@†\]øq>JKK‡ B£ÑSÐÚÚŠa“Éü¨&z€/V°o˜ocætQQQË–-ë À7ŒŒŒh4ZffæGÕlbb"lR.Q£F‰ŠûD6oÞ¬¡¡]âuÞ‘x׈‡aö„Ûruu…½ÈÈ‘#á_Mâ+åƒÎ}}}.\¸mÛ6èçñ ÓÃsO°œ†‹‹ÕÒÒÒÔÔ¤«« óuuu«««ëׯçp8...p¥QVVöôéSÇøë¯¿Äc»8;;?xð ##ÃÙÙÙÅÅ%=====Ëï*•*PÉËËKôÊîíí}÷îÝ‚‚‚¿þúËÍÍ  ¢¢Eí ,«¹¹Yà§ÐÀÀ&¤uÊÏÏïÊ•+­­­W®\±²²9ËPQQWòØl6ƒÁ‡õL™2¥½½Ïç‹cÇõH{Aœ]"‘H"‘£$øCb§¤EÕù`ª««݇AI;g ‹µk×nذÁÜÜ|öìÙïßK5¸®¨¨<aħ¨ðUëììÜ¿ÿ1cưX¬ööv@|||jjjÿþý}}}ÅO7ˆ_0`FKKK+...,,3f 4ççÎK§ÓétºÀ¡¦à)t<Ú ‘´.I²ººZ0i%¨‘8à“'O¾qãFssó…  -zÄÁÆÆöîîݻܻtJ;PÅhz]U/#Cf·s_½­zü,¯ò}™DÄDVk[sKÛgˆËb7óø—›˜'ëš*þ±ßèÔmíÕÐîÝ»œœêëë#"">RÆÏÃâÅ‹õõõåää ·lÙ3‹‹‹555•””<<<¤±Aô"hæ| ÑÑÑÛ¶m«¯¯ï²GÙéÓ§oܸQâ¥{÷î±X,hëõ¹hjjЉ‰yþüyuuu'UBŸeË–ulvÈd2CBB´´´ Ç2qÖ­[§®®®¤¤4wî\ŽØ«µ¡¡AFFÆÜÜ\𻹥¥Ú ,€¿NîÞ½ëââò©]B zœ®ÏŸ?ÿúõk//¯î_œ"z™îžlÂq"$"//O¥Ra @YY\žijjîÞ½»  `Æ ‹-z÷î¶¶¶««ëÃÈÉÉY´h‘HmP;5ÎÎÎééé<×|P*qäääÔÕÕ…O‘¸»»§¦¦ VViCª  @¥R‹UABZ§ŒŒŒ®_¿.nV‡P(väÈXÏ£G ‚ø0v\´ÇñQHì”––Œª#\RøAHëBß¾}…ùâæo"]¿~ýÉ“'l6þ2„NàÕºº:%%%Ûµ¤¤¤Û·o§¤¤äää\½züóÄííí_½z5lØ0èRA‰Wá{RRÒØ±c) 4ç÷ïß_[[[[[»bÅ i"I›BÒ&¹´.HDxð¥©ZÄ\]]ÝÉÉéâÅ‹‚cZÙÔÖÖB;²/“N½)¸ml.—‹,;çE}ùkU¹ì¿Ò ß¼¥È‘Ëkê0\ÂñÖ6N;‡Û‰þ±]ªár“ê›S›XLþ§:8”Hiii7ñ¸Téÿ;wîTUU:ujÇŽW®\TWWÛÙÙݾ}»°°ÐÔÔÔÇǧþŒ!> 4s>…î=Àþýû?{ÌÞÊÊJ%%%uuu€‚‚ÂܹsE tò©} >>>=zûö­´Ë—/¯¨¨())INNþùçŸÅÝV%&&îÙ³çæÍ›yyyéééëedee'/ •©à’¯¯¯ªª*LwágÑ»Ðh4>Ÿ 0 ãóù‚­Èn‚N§‹¬?Å™4iÒÆ«ªªvìØ—)))µµµ†Ñétèp„ ><{ö,‡Ã¥µeß IDATaY‹Xð¼Ü¥wppHKK«©©0`@¤ú Ó§O——— ÍÉÉa±X¯_¿þùçŸ;8Ù7aÂèê‚Íf ¼itЩɓ':tèÞ½{&ˆ†-Ÿ2eÊ c…²²2Ø—éÓ§¯\¹zª«®®¾yó&4Œ¬Gâãø($vJbTUUU.—+°B—Ø…±cÇÞ¹sþÝ?þ¼à,CJJŠø¨—/_>zôˆËå*** ,€~ýúÁl6ûرc!³²²N:Åáp;l¢©©ICCƒN§83ÙlvJJ ›Í†!]DYÚUÿ«W¯&&& \ivü°>j´ôI.± Ò˜8qb|||kk+`ÿþýâ$8 00pß¾}Ïž=7n܇èKæÃÚÖ¶öÏÇÛ9\UòïQvvv“'úÖÔÔ°ZÛ¹8V]/á0Àè’@>ï_]@~kÛŒ†{¬–öϺԙ9sæÅ‹£££ òòòÞ½{¤®®®¯¯¿uëÖ¿[Çñ˜˜ ===a?ØoÞ¼9r$F³°°HHH€™‡5jÔ´iÓ,,,öíÛwîܹþýûS©Tƒ_ýU¢ ®®®†††4ÍØØXQQñõë×''§¨¨( MMÍ_~ù¥´´T‹ëßa‘$Xff¦ššš`¡rñâE+++@XXØO?ý4|øpyyyOOÏ÷ïßÏš5‹J¥ÚØØäåå}¶ýnøªgN7QPPàè訤¤4fÌá“]'Ož´¶¶VVVöôô„QÁ­­­ß¿ïáᣭ]»ÖÀÀ€J¥ÚÚÚÞºu ÞµqãÆü¦™L&†að 9qâÄéÓ§7mÚd``0{öla1pOMMèb \]]i4šªª*ŒÃ4~üøíÛ· ÊÛÙÙÁ0u¿üò‹–––²²²™™™°)àùóçïß¿‡Í Ÿö×ÑÑÙ¸qã AƒàM|&”••Q©Tø•œ;w®ªª*Žã€éӧû/2™ìääµÝâðx¼'N¬Y³FMMÍÅÅ%$$äÈ‘#"eŽ=:{öìþýûëèè¬X±B¼$88øäÉ“0˜˜èïï/¸„, _5JJJyyyPA€øžáóùyyy[q:‹/^¿~½‘‘ÑáÇ¥•Y»v­¦¦¦ƒƒƒ——׈#à_´ŒŒŒ¡C‡͘1c×®]***jjj§OŸ>~ü8t§¿}ûv¾Øž™¾¾¾²²²ƒƒ@AAÁÈÈÈÞÞ^°bù(©>ˆ‚‚ÂÅ‹õôô‚ƒƒMMMÃÂÂh4Z×®]Ë`0FŒ1eÊh7è S'N|øð¡³³3´3ÆÃÃ#&&fñâÅþþþÐábdd$Œè¡¯¯?a„ÂÂB‰ÃøÁz$>ŽBb§0IQu(Ê’%K†ndd”——'± zzzÛ·oŸ2eÊøñã?~,pPQQý ÓÒÒebbbccÃf³ÿ÷¿ÿfÍšÅd2ÝÜÜBCC'þÔÔÔ’““;fffæììÜIÏÐÐP>Ÿ?lذÀÀ@3€ãøáÇa Ÿ¤¤$‘Ÿ¬Ò®jhh 4ˆËå Dêøa}Ôhé“\b¤áçççééééééççgnn.^@â€ÆŒSYYéíí­¨¨øÁ!ú’Áp)«î[2G¸ 0Yì¼ÒÇåå”ȸ®öß»Fy¯Kp¥¡¹…ÓÞê>Ðû¯. ¨¤”O ›èi~¬@.ÿß©?~ϧ “••hD©êŒòùK¼Hˆ„!É“'{zzÂÀ¡C‡4hÆ µµµ£FŠ‹‹?~|BBŠ+nß¾­¦¦záÂ…ææfyyùøúú®Y³æñãÇÞÞÞ·nÝ>ÞÑÑqúôéQQQ"’”––p8è‡iâĉ?ýô‡ÃÉÎÎ2dÈÙ³gccc¡'Þ/^¸ººVWWŒ7.33S]]½¤¤„H$ ,!¹¹¹žžžPm_^^nbbµ:::ýúõ»pႌŒ †ag‚‘‘QBB‚ƒƒƒµµ5›Í¾pá‚‘‘Qbb¢½½½ÄîÇÇÇÇÇÇ_½zU¸-@dd$“É”tª¸¸ØÈȈÁ`À¿©û÷ïÿý÷ßa@#úúúÛ·oŸ4i   ÀÒÒ¾4àUÇëêêètzvv¶——Wnn.ƒÁðôô™   øøxÇOž< =X[[oݺuåÊ•jjj!!!555"_·Z<8i3ÁÝÝýÎ;iiinnnÆ »{÷.téWÝÕýææfeee‰—à"¶hll¤R©âe„ î*O:%° ù@ ñQlÞ¼ÙÉÉiΜ9"1#¾ &Mš±eË–žÙœëV¤žÀÿ.î5U•²s ª›¹L¶¡6ÑÈâá ¨â] »]OSEWC…HÀšÙm.O…ú·_S¬«›ù|¾ô‰Ö‡ƒ.:&ÐÕÕ¥R©yyy"SKK # --­ÊÊJ‡—y%%%‚(#Âý X»vmXX‘Hœ={6TyÀ3Ã’…Çqh˜ x÷ÇÔ©S—.] sLLL„•&˜™™™šš^¾|ùäÉ“!!!] DçùògN ¥¥%ì ¸¦¦qÑÕÕ w­, ¸¸8<<<-- zN233ƒUPPxÙe0â7J{íêêê*++¿~ý‰ikk9rkŒ3ÆÓÓ³oß¾Ó¦M ððð ‘H‚ÐÓ¦M›6mƒÁ˜9sæ/¿ü²gÏžt”œ´™àîî¾hÑ"cccwww—3fôéÓzFÖ}iäçç;Vâ%===*•š““3lØ0À³gϬ­­EÊX[[çää@Ë‚gÏžéëëKÓxyy…‡‡÷íÛ×ÊÊ Za @|"‘‘‘‘‘‘½-EwqöìÙÞá³!U½!KþWq@‘“u4ÀHK•ÓÆ¾þ ûeiMfö >'GÂé}¨·n§]½q«¹¥½¥¥å_¯°XËwMÖÏ^%À¿eË–577óùüüüüGÿýw6› „1ëß¿¿®®îºuëÚÛÛ322$®Ã›ššlllˆDbiiéùóçÅ °X¬;wÕÕÕ;wîÈ‘#ÐF…Á`xxxøøøüïÿkmmmmm_3H ²{÷î[·nuÁÛ*âcùêfNw`kkK£Ñ’’’EEEOsçÎݲe ô ÜÔÔtúôiGJMMM ÅÌÌ ššúêÕ+A…÷ï߇!g%†TWW‡ŽEÀ0lôèÑ‚ø±§OŸ~÷î†aêêê*eìíí)JDD<ŽÈÏÏðà—Ë¥R©T*UÜSg6 •••=êîî®   ««›””µÒº/‡óðáÃQ£FI¼J$CBBÖ­[WWW÷øñã„„„éÓ§ÊËË—-[?L:õÀùùùÕÕÕ›6m‚$B&“ÏŸ?ÿû â3]·ˆç¯_¿^¢§tè¤jdddþ³ºÀ€…¦™Ý} ©"™?~Ø !¦¦úZíîPÇCìËß7”¾gòù|!Ë‚®,æe¥ìhÕ \€ËQ»èð³gÏÖÖÖš™™©¨¨LŸ>z- ñöö¶··÷òò‚nÉD"ñܹsêêêÓ§Oß³g¸ïPÀ¾}ûBCC=<<~þùgè&P"‘xíÚ5{{{mmí+V¬_¿~êÔ©€ÔÔÔÜÜÜ­[·RþA|ûN¢`   {÷î :TÄM¢;øêfNw@ Μ9³cÇ—åË— ¼Ü{yymÛ¶mÖ¬Y}úô±²²ºpá‚È aÀ€Ó¦M³³³=zôíÛ·±—ÜÜÜ&Mšdgg7lØ0===ñg̘‘™™I£ÑBCCE.Í™3çøñã0 ·å•••ýýýÿøãA4¾©S§>þ<,, ~d±X .TQQÑÖÖniiYµjUAÚL¸»»÷éÓ¢swwçñxÐÙ„´îK$55ÕÞÞ¾·[·nUWW×ÕÕõõõ…‡ª««·mÛµAAAsçÎuss333³··ŽŽî 9SSÓ ¿Ñ}Ì›7o×®]Âñz¢Ç³àin¡­•)†a®õ…Îíã8Ž ` ïuÉ«òZc=íV·¨´Ê@:¨Ÿ,SUU“dõµÔ?V º—놺29Ì}MeeÆõ‘ ;àãø>ùö}•Ũ@ ß6#GŽ\½zµÀj@œ“'Oþúë¯gþqssÛ±cǧ¸lìÇù|>ŸÏçñxð_—Ëåñx‡Ëår¹\˜àp8ÇÅÅÅ,@ ß*=³ ¸¸ØÃÃC`(Lppð¤I“„ÃÊ"DgèÆ˜<>Çãý]`8†c€ÀÇq€ãFz>V\VÙÄlÑVW¨À'xP1³ˆË-8óã÷ïÝçþ·‰gJÛ9•NÚþ+R ˆëׯwp•ÍfïÙ³§ qŒ{—{÷îõ¶ÄWOQQ“É”èÍñý ªªª¤¤ô±j>--­ÌÌLè…gõêÕ$iõêÕ¯^½Šˆˆ((( £GÞµk,üÛo¿íܹ“H$.X°`Μ90ÓÍÍíúõëH;€@ z©Úö¶¶úúxXäH0†a|8èÛ‡¢HR‘“Ó”•‘ill$d±ZúÈuM&9:=ôüE¯§OOÍšù¼¤XZ±ï¹Bç]ÛFyzu­!ñýðçŸúúú>F+@ ßEEEzý@|ç<}ú´¸¸¸ç½¦¯[·näÈ‘—.]âp8¹¹¹0“Édfgg¿yóf„ ýúõsqq˜™™IóFŒ@ ÝŠTíU©Oùûú–6ŸÏçóq>ŽCŸÿ¤q†0 #Ã0 #0ŠœœTÿC}àÀEO²ž:´vÍ»6ÿ†Häàx6×\´tæ¼pb—¼ˆ!ˆï èì@|o0™L¤@@(ð_Û“Éä’’’ŠŠ ÂL>Ÿ-##cii|æÌ¨PTT„Ñg¢‡‘ª°13„§]ó|N"‘>‹QÖ€g÷Ÿ>ãFäÿ.‡Îò[Û]Ý'lÜ¢òO@8@ i0Œž ã‚ø*è“õë×ÇÆÆzxxÐéôˆˆh5 +++pš«««ûçŸÂ4“ÉìÓ§OÏ ‰@ Rð†}®þ'‚‘H#ÿoçˆ Ùeeí‚¡š:E©‹á @|‡ í¢ÇPPPhoo‡éúúz555€¦¦æîÝ»qOKK ‚‡YÚÚÚ T”—— ‰½|ùÒÚÚº—ÄG ß5R#~iä(TS3Uc¤@ ñ±àD(‰úõëwëÖ-ÀÛ·oSSSafJJJmm-†at:îÀB\\\{{{AAÁÉ“''Nœ ?xðÀÓÓ¦Oœ8qóæÍ@ ÀW¤@ B˜]»vYXXXZZ~Ô]•••З8⻢›–š7oÞ433SWWÏÌÌ´°°xøða75„ãxyy9•J…é9sælÞ¼Y¼LFFÆèÑ£»O†‚Á`X[[³X¬ÞD”îžlëÖ­;~ü¸——×êÕ«½¼þöœ‘‘1tèP##£3fìÚµKEE ¨¨hjj:`À€É“'GDD¸¹¹ FVVÖøñãá—.]JOOïn™‚´è}ÜÝÝétºššš¡¡áÈ‘#÷ïßÏår;(ßÜܼiÓ¦û÷ïçççw¹Q;;»ÌÌÌ.ßIHH€éÝ»w{{{ _mhhÐÒÒÊÍÍŽŽ¦Óé'Nœ\Ú¼y3Nß³gÏ' €èEV¯^½yóæšššAƒ­Zµªç=á‹Ë³|ùò^ÀÔÔôÑ£G0­¢¢2jÔ¨ýû÷÷¢<½B¿~ýnß¾}ãÆ£GîÛ·oõêÕ€¸¸¸¼¼¼¢¢¢Gùùù ‹ŠŠæÌ™“ŸŸŸ››;oÞŸÏçóy<žð¿\.F®ár¹\.—ÃáÀ§›$!$IKKË××רØxĈ?¶··¯­­ýùçŸÓÒÒ(ʬY³æÏŸŸ——À`0ììì† ¶eË–™3g>|øËå4hëÖ­zzz--­ÌÌLhG°zõj‰$¼UYY9uêTYYÙeË–…†† .?~<%%ECC#''‡ÃálÙ²ÅÕÕ .IIIÉš5kX,–•JMKK6lXbbbTT¬*11100¦oß¾]^^®££óðáC:N£ÑîÝ»wÈ!ƒî¦áýÎYnذáàÁƒl6[MMíСCrrr>>>ÅÅÅd2pñâŘ˜˜'Ož\¼x1::úÝ»w %**jΜ9Âu8°¶¶vôèѪªª?¶¶¶>|øð!C~øá–ŸŸ_RRrêÔ)++«õë×'$$°Ùì±cÇnݺUAAaîܹ¦¦¦ÿûßÿÞ½{§¯¯¿uëÖùóç—––ÚÛÛWVV¾~ýzþüù¹¹¹222¾¾¾[·n•““ÃÿÑwJXžÔÔTggg Ã`~uuõòåËïÞ½Ëáp|||:´uëÖ7oÞìÛ·Àd2ÕÔÔêëëåääDž4iÒ¼yóΜ9Ãd2Ÿ?þþýû¥K—Þ¹s‡B¡„‡‡GDDà-………UUUÊÊÊÇŽÓÑÑYºtiEE…¿¿¿¬¬ìŠ+¦M›fggW[[[XXhffÖOƒ!ñø½³³³ (@ >:;€@ ˆ/kkk##£‡¦OŸ®¡¡ñìٳ˗/Ÿ8q"55ÕÊÊ*99YUU5++kûöí8Ž{{{?}úôÅ‹ÚÚÚ‘‘‘ibãÆZZZÇŽËÊÊV @ÒÒÒ¦Nš––öË/¿,^¼fŠKb``°víZ—¬¬¬´´4@```RR\ž•””deeMž<ÞN &Mš”œœ þ«5€œ?00PIIéùóç%%%Ó§O¿QšÀ€»wïÞ¾}ûÙ³g8Ž÷íÛ·°°ðÏ?ÿ{ö,Žã®®®\.÷þýû‹/~ôèŸÏ¿wïž››ŽãæææC‡%‘HóçÏðàx@LAP__¯¨¨Ó/^¼ÈËËÛ²e •J%“ÉÎÎÎâ·H.##ÈËËËÎÎŽ…Ó~îܹgΜ·AKŠQ£FeeeI“JQQ±¡¡A|è>;cÇŽ;v¬´ÂôüôC ˆ¯Ro €@ „ª««i4Zyy9—Ë…ûÇÂÂB¤$ÇÛ¸qãåË—™L&@`±XííípmÓegþ‰D"‰Db±X‘ ++;~üøÄÄD''§¤¤¤U«V _577§P(6lppp6+@ô$–––qqqk×®-,,ôööÞ´i“ººz```lllssóÙ³gíííµµµIIIqqqÑÑÑvvv6l8p ©©iee%àÒ¥KÇ—Ö„ p}KK ƒÁ˜5kð÷~L{{;ŸÏ×ÓÓSQQÉÊʺÿ~DDDBBBAAÁýû÷g̘x÷îÝÏ?ÿü×_µ···µµuÒÙ¡²²rss3L———kjj <Û}À"KKK9ŽÀà…ÃáXYYÁ4ôº““c±XÒjf2™}úô餟ȸqã/^7nL#â£øj´üæfVnÎûª*YsKM ‘ØÛ!¢»€Î½åååÓÓÓ‹+q’’’nß¾’’B§Ó«««ûõë·ÚÛÛa™úúz555‘;¨S---‰’ˆW8qâÄúúz‘€€€€U«V;v¬óM#>‘M㺺ºððð 6lß¾]]]ÝÅÅåܹs'Ožœ2e ,|(//Ÿ””´oß>áV$öЯ_¿ÇÃLmmíÊÊÊÖÖVYYYAyyù¶¶6X€Á`W"X¤wÚÚÚŠŠŠOž<ÎãÖô IDATžçÂ2ˆtœ@  †ãø«W¯lllzlÇ~ìØ±fffТgZD ˆo ©?ŒJ«kK«o«jKªÞW¾/ªx÷¦üÝ›²š×e5¯J«_¾­*|[õòmõ«Òê×e5oÊk^•”T”Õ0Š+ß”Tä—×71?‹ˆ8‡ÓpüH±‡kmø,lÝêwÁ“Ž»8f]ºøY*G Äô—^UUuéÒ¥©S§Ž=zðàÁ–––¦¦¦kÖ¬a2™|>ÿåË—Ož<¹±©©ICCÆ/8pà€ ¿_¿~·nݼ}û655U¼E:._ c¤IB§ÓËÊÊ„52DMM-""b„ Âk3HXXØéÓ§===Eò÷îÝûéáÒ>X^PP‘‘ÁápÉd2ÌÞµk 5ã8‹Å:wî‹Å’‘‘¡Ñh‚b"gÔEΫãb§ëgΜùÓO?•••á8^YYyíÚ5˜ïêêúÛo¿Á3ÿ®®®ûöístt$‰8Ž766S(”–––ÇKkND//¯ôôt>Ÿã¸¥¥¥••Udddsss[[´M°±±IOOg±X8ŽÃPš«þhiiiff¶bÅŠææfWPP P@ˆwÇq:þæÍÁ¥ììlñ¡ë>LMM;.ÐóÓ@ ¾¤jø8 Q”ää(Y’ ‘@€Àãomã·¶òÚ9|.ðù„ºzfC«¸¼¦²†QS[_ßÈd±Û>]>ÖÍëÅ£GÔÿßfÂ??¼”‰Ä¡m¬¦Ÿ–›4¾ª°ðÓ›@ Ä—ÀâÅ‹ûöíëää´cÇŽ™3gþþûï ÃŽ=Ê`0† bbb²`Á‚††‘CCCù|þ°aÃuttùëÖ­;~ü¸——×êÕ«½¼¼$¶¸~ýz##£Ã‡P|'Ñ_fÏS__ïééyïÞ=yyùÞ–å_ÂÕ«W;ðàr¹õõõ"¾ èm†@ ¾d:x} C£ÑpOÏHwqvÑÐÐ,ïA~ˆD¢]¿~à`€Áÿ‡Ëc·såd‚ S fVW×´wU?˯¯¿c+ëây¬C/€¥¬L[FZÊ07ÅKÜf΂]B øX._¾ÜÖÖ6bĈÞ¤ë8::~!ªF·B Ä—ŒTí0 å?®n+\>.K"rq« &'C¦Ê‹ZTBˆD"àvE=À­}_î?o¨ïäB_ÜH dû–—–æ®n]h@ Ä·ÍO•?>77wß¾}âþùßèá"DtÊ]3Žã­íœfV« ‰H p>_V†,C&µ°[Û9¼Ï+Ð_öxS\Øúq' dÈ5/¿ ‡èm)ÝBYY™««kß¾}wïÞýÁ­­­ …Éü<:ˆÞbìØ±È$ñUóAsôóçÏ¿~ýÚËË«Û Þ_ ½=âË¥SÚ*FÓëªz2»ûêmÕãgy•ïÈ$"F ²ZÛš[>ƒ÷A¼–Öfÿród]S… èߨþ…?Š·oß*++wS刯èVºººzÁ‚½"À²eËÌÌÌ”••-,,¶oß3KJJ ÔÕÕ½½½_¼xÑ+²‰°xñâß~û­·¥øöa2™Ó¦MSUU544Ü¿¿Ä2qqqºººêêê .䈽Z”””ú÷ï/øÝÌb±¬¬¬1ØæÍ›§¥¥¸wï^ÇöºÄFãóùðPâ{Ã0>ŸO£Ñz{J"ÄJ§´Ü66—ËÅ–ó¢¾üµª\ö_é…oÞRäÈå5u.áø@k§ÃíŠDÿXÔp¹IõÍ©M,f'Ü!=FYY™¹¹ywÔÌåvê+3iÒ¤ëׯ—””?~|×®]W¯^À(Ù×®]ËÉÉ111™8q"þl\»vmôèѽ-Å·ÏŠ+*++ Ož<¹zõê´´4‘§OŸÞ¿ÿ•+W²²²>|¸iÓ&‰õÈÊÊfddÀô•+W444—|||TTT`Z8ø*PRRÊË˃ Ä÷ ŸÏÏËËSRRêí)‰@ _(ö¹ÚÚÖÞÂ#ðù¼vWUìæ4 Ã0››÷ijjpq¬ºže¢ æ K>ù¼u 8ù­m¯ÚÚìäåä)2ŸÕé ±±ñ¼yó.\¸ÐÐÐ0dÈýû÷ÃøR‰‰‰›6mª¬¬8pàž={ ÚÚÚà‚ðüùó–––"õ̘1ãúõëL&ÓÁÁaûöí0º5Ç[²dɉ'ÔÔÔöîÝëîî(**Z´hQff¦††Ftt4´>8|øðÙ³gûöí›ÍápvîÜ ¿ÿ~éÒ¥wîÜ¡P(áá៱ûˆ®1wîÜ+W®üùçŸ[·n=þíLˆ Ä7 FSRR222êmAâ Eªv€Ýö÷ÑS.—×ÄjÁqŒG&êëc0Ô‰¤§«Íhliimolj2Ö¦cÿ]ºcX—òâwqqð˜ÕZÐÚ>YY‰FìÔa‡NòèÑ£[·n¼¼¼’’’BCCoß¾uîÜ9›}ûö…„„¤§§'%% 0 °Pª_ƒ‡Þ¼y“@ nÙ²þì¾qãÆþýû7oÞ|øðá¹sçæççóùü€€Ÿ³gÏ>yòdüøñ&&&ܾ}ûæÍ›ŽŽŽ©©©°0 88ØÎÎîåË— cܸqÆÆÆãÆûŒÝGtýû÷766Ž1bÖ¬YOOOñg”œœïÞ=:þÃ?À;xú·nݺ|ùò¡C‡p¿}ûö±cÇÌÍͳ³³ÇŒ3dÈ{{{q1~ùå—ƒÖ××øûû‹\}øð!NïÉ ^ñ¯:8€ãøªU«œ †ŸŸßÞ½{—.]Úc²}ó”••5770~ìß¿ÿ‘#GDÊäåå ¦bÿþýKKK™L¦¢¢¢H±ñãÇûøø´¶¶2Œ7oÞ,_¾\ @ ¾vŒ{[@ ¾t¤.¶aX`y9š,hdµjÓ•ˆ2²Ðp‹Çã‘dåy8Îhh²ÖS“P/ÖUý€”ãÐÍ<þ“v÷u¡¹ððp2™L&“=<<²³³ ·µµ%‰ ,¨¬¬,**ú`=sçÎ%“ÉD"qþüùIII0³ÿþ!!!d2ù‡~xûömCCÃóçÏËÊÊV®\)##ãää˜ ;888::FŽYZZÚÐЗ——G¡PtttæÍ›wöìÙí ¢[‘öŒ’’’æÍ›§¯¯¯   wðôíìì† À0lĈ–––aàÀ'NLOO—Øô’%K222Ž92fÌ*•*|©¼¼<""bÛ¶mÂçT¥uŒøW šš µîîîd2¹oß¾ ,xðàA ö=}^ ¦AŸ>}Ľ`2™Lá‹%^•²²²‹‹Ëµk×NŸ>=~üxiS¨¦¦æs @ ørº~À`Ddä5U©åïèÊJLv“]Áhjçáå5udYÙ7ïškY|>r °®ti†õ¹¨ªªÂ„À«|iié¾}ûüô;ØØØ˜B¡P(¸Y „¬p544ªªª`zðÉd‹UYY©©© ]ôõõ+**D$!‰d2™Åb•––r8œAƒAI¶oßÞÜÜüy»øD¤=£ÊÊJXFèàé ŸÿÏÌÌ7nœ¹¹¹™™ÙÙ³gkkk?üðœr111°•JÕÑÑ™ƒ6àà`cï²²2áÐÌ[˜’’’ÁƒŠ‹‹;°ôÖÒÒªªªâp8p‰øöí[mmí$QTTÌÊÊêÉ`ÄG!íiii ”JïÞ½dJ{úÂß”ÐÐÐU«Vã(PYvøðáÇK”Çñââb˜~ÿþ½··whhè¢E‹>g?»Ä­[·† ;­  ðøñcyyùÄÄÄ={öô¶tߺººT*õùóçÐ×ÀóçÏ­¬¬DÊXYY=þ|üøñ°€žžž¸âéé¹xñb KK˧OŸv·ð@ ˆ/©+OYò¿ŠŠœ¬Ó FZªœ6öõÙ/Kk2³_ðy<9NïC½u;íê[Í-í---ÿÆŒùÀZ¾k²~ö*E™5kÖöíÛŸŸ_PPðøñãOíâ³"íMž<ùÈ‘#l6°{÷nX¸“O¿©©©_¿~D"±¬¬,%%E¼‹ÅÚ½{wqqq}}}JJÊñãÇax¹ºº:ooïÑ£G/^¼¸µµµµµµwcÌ ¦¦¦òòòl6û?þèE©¾IˆDb```\\\}}ý“'O’’’¦L™¨¨¨ˆŠŠ‚0BCCÿý÷‚‚‚ššš­[·Â!‘HÉÉÉ(%@ ÄwˆÔ³22200ìߟ1`a YWߨ¯©òòmÕÐaƒØì–úV¼Ãê8H$–¿oàryštÚß·`]<; +eG t¨nà\V‘*ýzgñððØ´iSxxxqq1•JuwwŸ0a‚¼¼|dd¤ƒƒ—˽yó¦ ¸€   #FÔÖÖŽ?>22RZåD"111qñâÅzzzêêê;vì€'$‚aXbbbTT”Mkk«©©éªU«>½ƒˆÏˆ´gXXXèêêÚ·oß±cÇÂÂ|ú»víš>}º¦¦¦††Æ¨Q£Ä ‰Ä›7onذÍfëëëÇÄÄ\¾xñâÅ‹;vì€%}ú :ôÚµk½"Õ7L\\\xx¸‰‰ •J‰‰‡jjjvîÜC"‘üýý_½zåááÁáp~úé§j³¶¶æóù<žÔc\Èï@ ÝAQQ“Éd0½-â‹CUUUIII`³Ü}`p‡Sœ¯Jì¬Í0 Ћ€Ð¹}Ç1€ ä½.yU^k¬§ÝÊá•V¨Sõ3ƒeªªªÛq‚ކêÇ Tÿªp³—“#!𻩬̸>t|ÀÃÇ]ý“&ý ~÷all|îܹþýû÷|ÓÄ—Ivvöܹs>|ØÛ‚ :Žã|>j„ÿår¹<Ãáp¹\.—ËápàG‡3|øphÕbddÔ™qÕÕÕ4Dúp ]¢›@¿¹ Ó?¯¹\n}}}ß¾}Å/¡·¢û(**"‰=°üC|iܺuËÏÏïƒÅžOŸ>-..644ìmAQ˜L&4E|‡tÆ"xàÀiiiÝ-‰Tí@{[[}}ÃßVÿµÀ0Œã}ûPI*rrš²22M‚€„,V‹Œ|ú˪ªú'ž}&|nîÛiÅx¼y%ûmÝGxt­!Ñô–E@H„Éd"Õ2pàÀ»wïö¶„Ðá¦ï™Nú ëÈ_Rµ ŠÔ²wu--l>ŸÏçã|çóù8Žÿ“ÆqFÀ0Œ H` Ã0Œ@À(rr²òŸ$ÝÖvNúÃGþ8»î] €C$rp<›‹«…/ ›5›øOˆ¸ÞB<„@ ˆ/ƒÑ»ŽZ_h †@ Òª°0Ô&¦]ûƒJ"‘>‹Q–õôVaSïDG]=™ä·¶Õ;»ù¬Û@ÓÒúôú@|ó í@ ¾XDþH]¸pcQ÷0Rð†}®þ'‚‘HÃ7mu[»®µ¼¼@AW£()õ¶P@ ¾&‚Ĭe{žˆˆCCÃÅ‹w¹ÀINNNNNNJJêr ÅÅÅEEE]®ñ5bbbïèèØÛ‚|ÿ…JIIÚ€¯¯¯ ¿g^_„hã³@’£(š˜ª#Õ@ »ví²°°°´´ü¨»*++µzõèY¯ ð}‚ÿ—ÒÒRggguuõ_ýÿl6[FF¦¹¹ùƒ%{†7nذ¡·¥ø›7nøùùõ¶GwO6www:^RR"Èñõõ¥ÓéÙÙـɓ'wìyîƒß»wï8p ™LþñÇ…ó7oÞ¬¯¯¯  0iÒ¤ºº:AþºuëÔÕÕ•””æÎËápDj;þ<†aS§NäܹsÃ0Aøí˜˜è°3""Ã0 ÃÈd²¥¥errrwõñ‚Ô… ª˜îÉ×øŠ´ø†¿§ÕÔÔ G޹ÿ~.WBh[ÍÍÍ›6mºÿ~~~~—µ³³ËÌÌÏOHHèrµŸHï¶þ°wï^‡ššš… öŠK—.511QRR233û¿ÿû?˜YRRª§§G§ÓGõâÅ ñ›››÷îÝÞ³òþËÛ·o•„öi<==KKK%~¾gŒ[÷eee¥¥¥ ~tuuí8 ö ¤Ññ_¯--­˜˜˜áÌ„„„íÛ·_ºt©ªªJNNN 8HLLܳgÏÍ›7óòòÒÓÓcccÅ+ÔÖÖ¾ví‹Å‚;fföoȹ°°0 ˜ž:u*‡Ãill\µjUhhheee·ôñªau€HNH‚´ø"عsgUUÕƒ,XpðàÁ™3gvP¸ººšJ¥ÒéôñU#²{\VVfaañQ»Í, 6}??¿7n”””ÄÇÇÿú믩©©8ŽWWW0àúõëÏŸ?711?~L&›™™=~ü^âãã³aÃ{{{“iÓ¦UTT (êææÖÝÝ­­­ [EQ++«ÄÄÄ~Ï͇„úúú;ܨ¯¯ïyú›†††´´ô›7o±±±îîî\³={öÌÛÛûÙ³g\ $%%M:µ¨¨hÖ¬Y^^^%%%™™™GŽ ¦Óé‰ôÛo¿•””œ?~ÿþý¼Dª¨¨ð÷÷?räȇddd²³³û¾…ööö>deeÅÆÆ>|øåË—PàÐÐÐÓ§OÙÚÚ.]º´w{¶··ggg?~|Ó¦M§OŸŽONN¾|ù2ô­ˆŒŒLKK{øðaVVV{{ûž={úyá­[·Îœ9sõêÕììlÌð§µµ•F£=þüÌ™3˜²£°°°¢¢bÖ¬Y€+W®äååõ}Ë? l6»ÇOhg”ŸŸoll ËÊÊÚÛÛ{_îååuéÒ%ÀÍ›7­­­%$$ú¨‹Á`ÄÅÅôõõK~œï>!!¡‘–gÄ@Q”Íf³Ùl‹ÿ…‰D‹E ˆD"‘H$L&:"ƒTúúúêêê¯_¿7nœ±±qvvvCCƒ›››ººúìÙ³¯_¿îì윙™ `0³gÏ>uê‘HܶmÛæÍ›¯]»öÕ*öíÛ÷àÁƒß~ûÍÜÜœ3]UU5<<üæÍ›˜a0ö-+&&°gÏž]»v’’’N:µÿþ={öxyyÙÛÛgff¦¤¤¬]»vöìÙ€ÄÄħOŸÊÉÉ8qbÕªUp ~Ùkiiåä丸¸˜™™™™™õ§öÎÎÎS§Nyyy5j0Úûßç´ðôéÓ---Ó§O‡¹666&&&>|hhh˜;w®†††££clllttô³gÏddd–.] K`±XnnnŽŽŽ7nÜÈÈȘ?¾†††©©)àéÓ§wïÞ…KèIIIQQQcÆŒÉÊÊrpp7nܸqãz‹´sçÎß~û­©©IUUÕÕÕµÇ,ëõë×222rrr=ÒsssW¯^ kkkííí÷ïß¿páBöùóg®s`ì8%%%))I@@ ¸¸¸¥¥¥³³3==N­Ž9âì윖–¶`Á‚ôôtyyyÀýû÷SRRäää¶oß¾sçÎ3gÎÄÄÄb…kkkS©ÔÚÚZYYÙÁyZߎ´´4Š¢ñññœ‰sæÌé#"’»»{ll,@ã4ÞædܸqãÇLŸ>½¢¢¢¥¥¥ÇTÍÈÈÈÉÉ àæævòäÉ!!!;;;E¿|ù¢¦¦feesÏ™3'--ÍÌÌŒk]ñññVVVS¦Lp.°s…Íf‡„„èêê.\w!V IDAT¸š \¸paÅŠÐñÁßßÿرc¥¥¥ÐSEÑmÛ¶ ÚÙÙñññùøø@#¯I“&åææGEEEGGËÉÉ6oÞìââGÔ¯^·bŠؘzzz---°R¨)@ÄÖÖ6((èãÇÚÚÚ±±±sçÎ…C1fšñ/ÀÑÑqëÖ­K—.=z4l:¨5nooǼ~`Gjoo'‘H=.·±±Y¾|yUUÕÅ‹xyçEEEEEEˆDâáÇñ¿5CÍHT½Ámppppp¾G(JsssaaaNNÎÏ?ÿ,$$¤¨¨¸bÅ leƒŸŸßÃÃCTTTHHhÓ¦M¯^½\I¢¢¢vìØ!''',,¼yófLøá. àææVYYÙãÃæqvv†Á+W®|óæMMM gÉVVVcÆŒ!Ø—}?k5jT}}½––ÖàÞ鿘ޫÇð    ++k÷îݰƒùûû߸qEÑØØXÿÑ£G‹ˆˆlß¾æÏÉÉ©¨¨Ø¾};??ÿĉ,XpõêUXމ‰‰••,yÚ´i:::‚˜˜˜ÌŸ??55•ërw``àË—/ÿøã{{{‰Äyª¢¢"00022AW555a™ãââÌÌ̼½½ùùùÅÅÅMLL°ïËÞw X½z5œ)ÁŸðàúõë&&&®®®ÁÂÂbÚ´i˜-€‡‡‡¬¬,Š¢vvv™™™=Ê„ ‰Ä¦¦&®·9l8:::::òúÉÉðt¹ùóçß»w/::š—á³'‰|||˜78="""bbb"""ð§°°0ÌüîÝ;777ccãÛ·o÷aOD¥R±ê ö§± ¡²²2•JTTTüþûïÿÐcLôóc‡2Óh´†††Õ«Wü¼¼èt:\ ïûBx JJJXËTWW÷¸P@@ÀÉÉéúõë(ŠÞ¸q£ÆÿqY¼xñêÕ«444tuu………a³H¤ÖÖV˜êMz«D"ÑÝÝýСC¹¹¹œŽ$=X²d Š¢L&óÝ»w‘‘‘Ðgèèç@7 ’Œü†…888888½¡R©RRR•••L&ÓÒÒ&2 9Y,Ö¾}ûîÝ»×ÞÞN :::èt:œýs°oY"‘@Qû–ýê‡;[M R©œë«ïÞ½Û»w/´*oiiY¶lY?k'påþàP^^Î`00ûƒ¡§§¨®®VQQ‰ØÒhuuµ¼¼& Óœ²Ùl8¯ˆˆˆtwwÃcÎè÷ÝÝÝ PAPYY ;¢¢¢››ìÒCXXXJJêÂ… °T§P(•••𸮮ŽN§ËËË766ö°hóðððõõ:u*ß„ ,êw ‚ !!!!!!€W¯^\IIéÞ½{=²‰ˆˆtuuÁD¸’Œró2àXIIÉÊÊ †Wèý_c8ã,³°°L&ËÈÈ Û²|8::˜߃0{÷î¥R©Cj’ÝÖÖ¦§§G$+++ïß¿ïééÉ+§££ã¾}û^¾|9qâÄ“'O677ÃtƒäääÅ‹Óh´‹/bù ÂÞ½{÷ìÙS\\|åÊ•ßÿàãã³nݺqãÆ···'%%9::~«²ÒÇÇç§Ÿ~:qâ„¢¢"•JÍËË›9sf.tqq Ÿ={¶¢¢bXX˜„„§Fbbb"$$âææ†)N:5~üø~dß?L&“ÉdBw¿®®.>>>>>¾¦¦¦¢¢"mmí>¬X±bëÖ­pG ooï   www))©ýû÷ûøøð*ÖØØøéÓ§PÊ h8Àb±òóóŸ>}<èw‡Ã ²´´´îܹÃÙ«¥¥¥7nÜ8œÚ³øÀnkkyù¼èzLEn.›ãÓ çß›Íf2™ÕÕÕwïÞõööž5k–¹¹¹®®®––VXXX{{;›Íþøñcï°[­­­pràŒÚm``ðôéS@YYYBBBïeddJJJ¸¦WTT`›EÃoYè,@¥R?~Üÿ›º}ûvvv6ƒÁصk×øñã1[YH/ûþ׎G%üV¸g¢(ª«««­­½}ûö¶¶6‹UXX]ñ£¢¢:;;Q=qâ̬¯¯¯¤¤´oß¾îîî´´´ØØØ  ½,í[[[õôôByyy|||oÓÐŽŽŽ“'O–””466Þ¹s'::z%444888Ìš5+ €F£Ñh´Þ{ØÚÚ¾xñ;99eddDGGÓéôæææwïÞ¡(jddôäÉXËüÁyË=Z;vrrJMM¥Óé]]]/_¾,++ë‘k1iiiƒQYY‰zþü¹MïÛ)´´´úÎ0l½NIIi¨§£‘‘‘~~~NNN;wîì{Ž=zôè“'O®[·n̘1µµµcÇŽ…é+V¬hooŸ2eЧ§çäÉ“±ü$IKKkìØ±®®®P!emm¨¦¦6iÒ¤„„„D¢Ù¼y³……ÅܹsUTTæÏŸÿáÇ~^8þüåË—»¹¹Ñh´_~ù…WNww÷‚‚777,åJ¸k×.aaá'N\¸pAXXxëÖ­€ööö¥K—’Éd—… nÚ´ föððð÷÷Ÿ2eж¶ö¸qã q/¬­­1ó7®\¼x‘ŸŸ_DDdîܹ#µùë8:IKKIIIÁŸRRRAAA0~ʰ]¯šŠ+©ð, þúEÿRk`ƒ+0FUN$D……˜,v7Ž¢¨‚¬ô(Iq®…(ƒÑUs$’À`šY¬\Q ÓŸ#ÌçÍÿç…ãàààüÇAyD%„‹ ®]ÀƒÁ`0&OžŒ¢(ÜGª©©é«U@¾-­¬¬`Ôe‰¤©©éââ²bÅ xI}}}hhhrrrWW—¦¦æ–-[f̘ñþý{gggN©½½}ÅŠÕÕÕd2ÙÎÎnóæÍ_¾|ÌËË €û ˆˆˆÈˡ††VUU™››Ã½ îß¿¿mÛ¶¶¶¶;vÀÈs:îííýöí[‰”••Å`0Ž9S__///ïååµfÍšK—.=|øF~.))™1cFqq1¼ÜÐÐðêÕ«þþþrrrïÞ½ËÏÏ;vì‰'”””8¸yóæ(ŠœœŒTߟÚÆŒóêÕ+<ô@HJJš={6g o///kkk¸kf]]]HHHRRFÓÒÒ ™9s&Š¢»wP(ÁÁÁÕÕÕ¢¢¢Ÿ? ÊÈÈ““Û²e ôjŽŠŠJLL¼zõ*,üÆ{÷î•——‡•B¡„‡‡sÊÓÕÕåééùöí[6zôèeË–­^½³bŠΜ)))&&&œ)ííí¦¦¦bbb€·oßnÛ¶­  @TT488xåÊ•---¾¾¾•••rrræææÉÉÉPG¦££ ãÉO:3ÒdggÿôÓOÙÙÙD"ÑÄÄäðáꪪ˗/755…²¥§§/_¾ºßµk×¹sç ƃôõõ§L™räÈ‘eU–@ $$$L›6­ÕŸÑì‡ÀÎÎnãÆ¶¶¶#-ÈpãÆ_ýõÁƒ#-È·‘””äêê:ÒRàŒqqqœ1 :ظq#§ñÑ?¾8 ˆÔW©“'M&“ÉPÍ÷׿¼´J¿H‹‰2Yl&‹Íb±™,6“ÅbqüDQ”H$ "á#¾ÔÔóóé &‘@`²˜ü||£TúRJõ‡Ö _v††žåtF…®ýÁ# ½PqppppúÏw¢øWâïïollìïï?Ò‚ü×IJJš5kVmÀ~\:Äf³±Ñ%))éìÙ³ýÙ%ä;@ $&&âÚ±v ««ËÙÙÙÓÓ³?‹ï\;ðŸ%..®ÇËm{ø%ýÃá‹“>´AAADF£}¥¾F÷çOå¡Û™ïÞr=;Z€_ùó‡×ö3;¦ÛÎß·_LfÄ6ÑÁÁÁÁÁÁÁÁ66nÜ8Ò"ü?Ó¦MëûSg8yöì™§§§¥¥%× ž88ß-=ìaD؉Ãã\ÀS;@$MŒô@@ÿ &‹Fg’„–ȩԚ‹Ïjl¬>¸¯õÖ ¤Ï&@Ðè~‘tÝb¼êÆMÓüV Ï.Ü8888888?ÃìpŽóÝ‚wÈgußO¦NZQQ1ÒR ! `‰óóÕ¡‰@ °Ùll™¡ƒ§vˆ°°gJUC “ ò™(¡ƒÎàäz9‘HdH F]mñ<{vSc?'ú‚bÁJ#÷êéëNµP888888ƒÏ™3gFZœ¿Àµ8¼à|·ˆ‹‹gdd˜™™´ 8#ÀW¹Ùlv~~¾„„ÄPKÒ/¿)E»̶Ž. 1€2™‚‚( :i]‚ü|üÄAèÕé“·>OÑè¿%€ª?õCáÐi”””îÞ½‹mÿûOøô铹¹9¶‘LßTVVjjjvuu–/_®©© £•btuu ·µµñÚÑô;„Á`4µÕ1P!Š Ž!ýXFååå®®®EEEaaa}gþÐ?aëÖ­L&óàÁƒ¼2yòdÏž=üüüöööG…Ä.\8pà@ii©˜˜˜££ãñãÇEDD°¢nݺåéé²aØòèÑ£;wÖÔÔ˜ššþüóÏøW5Έ¸¸xAA®®î¥ŒÆtØlöû÷ïÅÅaG-œ¡@MMm¤EÀù¯Ó/í³›Æd²QAþìœ|F»ª<9ëM*MßHSM¹ôKš<ÿ‡®n{`&ÿá®a2c›Út„§„I?ÎÔqpÄ`×L&sd£ì2™ÌÌÚ—buülÐ €ˆR;^ ´ˆ¥¾Zóû¡¼¼ÜÚÚz(Jîçrss ”””üøñ£³³³¾¾¾½½=•J511 —wpp(--ý¿ƒËËËuuu‡¨ð3gÎxyy n™UUUâââpO`®ïã0¼VË—//++SQQášÁÛÛ[__ÿÎ;T*ÕÖÖVKKkÕªUeee>>>çÏŸïìì\¶lÙúõë/^¼033‹‹‹£P( þþþáááû÷ï‡å´··‡‡‡Ã Ï _¾| Ø¿¿……ÅîÝ»·oß~úôé!½Yœ¡@]]ýóçÏïÞ½ëÏ8ÿb¤¤¤ÄÅÅÕÕÕGZœï”¯ÔvuÓ;Y6›Eg0G‰òO±°CÄÐÐðñó4yy2E¨Mš¢"=/CÐÌ„ÍbaÇ(ﻺ‹º»MD„&ˆ êT§°°ÐÇÇçýû÷“&MRVV?xðà¾}û>}úôûï¿ÚÛÛÅÄÄh4š 99yÁ‚ ÎÎÎ'Nœ¼{÷nýúõyyyJJJ‡¶±±DDDœ9s¦³³SNN.**Ê¢wíÇŽÛ·o‘HÜ´iÓúõë³fÍZ¼xñâÅ‹wïÞݵk×ëׯ9­‹ÿ¿YP4<<üôéÓ‚‚‚¡¡¡Xzmmíºuëž>}*,, øý÷߯_¿N¡PÒÒÒÖ¬Y0ˆmø­´v´dÓÞ !‚œ}ƒÉb ä$ÉßáT¶7¾¾¾ñññ=Ú·oßýû÷eddz·9¯ôùóçU«V¥§§“Éä°°°… ‚^HII),,¬¤¤dÔ¨QAAAëÖ­ë-¶6®¡¡A"‘>}ú°°°ÀzZDDÄÙ³g«««†ºAÀ߯RAA¥¥¥²²2Ü‚påʕݻwùòÅÜÜü×_UWW××ׯ««›1cƨQ£rrrÂÃÃÏŸ?ßÐР¡¡qøðáéÓ§úx—/_Ž‹‹ãçç?s挭­í¯¿þЉ¢hBBÂŽ;0©–/_žŸŸO æÎ{þüùyóæY[[cKâ&&&!!!®®®}¼°¹¹¹³fͪ««SUUµµµ ÅÞG%%¥µk×^¿~½­­íãÇ½ß¾ŠŠ ==½¦¦&>>>ÿëׯ×××#âããc``Ìõö¹ÂÏÏoaa‘˜˜èççÇ5CIIɾ}ûDEE544lmmóóóË–-Ã2þôÓOðØÐÐH$2™ ûd×®]ÞÞÞÏŸ?ÇRnß¾mddäààÀd2×­[7sæÌêêjÞÝç;ECCc¤EÀÁÁÁÁÁùÞá¹&OëfÀ&“ÕÚÑÉ`0Y,¶šŠÊ_!òñVVlhéìì¢WÖÔ÷ñ‚ tª×û*& Ò;º¢[šXƒæ.Èf³çÍ›×ØØ¸iÓ¦K—.}õ’k×®½|ù²¨¨(??ïÞ½€úúz[[Û5kÖÔ×ן>}záÂ…UUUÙÙÙ¿ÿþ{VVVssóÇ•””zÕÖÖVPPPZZš˜˜¸k×®äääoþÚµkQQQiiiïß¿¿{÷.–îêê*//_VVöâÅ‹?þøãöíÛ0ýñãÇK–,),,\»ví7U4è´u·¥×e¥Ö½}WŸ›×ø1¿ñcf}^z}VIKé%èܹs³gÏÞ½{wii©žž×6çú€ØlöüùóÇWSSsîÜ9??¿·oÿÚ­“󉉉]»v­¥¥%...,,,--«¡¡¡rrr …Íf»»»÷8ûêÕ+YYÙ¯ît:(ÀWÉÑѱ±±1((®NžAŠ64·ê–åR.2PýYb‹ÑIëãºoª.++«¦¦fóæÍD"qúôé½?¦{,+++--ýÓO?]½zcffæááA$---g̘qÿþ}>>>–M§ÓUUU•••{Åf³wïÞ-((h``°téRXZÿ¹zõêÚµkUUUEEEÃÂÂ`b~~þ»wï"##………•••®_¿O™™™ÁeÉ_œ—–ꢱ^Ô¤>¦>½_•p¯*áõI5CVˆòÅÀàÕæ\PvvvyyùÏ?ÿ, 0yòäE‹a3UÎ4sæL===`nnîââòâÅ ®UoÚ´)##ãòåËsæÌéá?YQQ±fÍšcÇŽ O“feeUWWoݺ•oæÌ™¶¶¶0ýÌ™3¦¦¦D"100ðË—/Ÿ?îq­‡‡…B!‰K—.•xÿþý?‘¤©©IDD„HüË£‰ŸŸÿóçÏãÇ8::–••ÁZ.]ºäææ&((ØŸ–ëÖ­D„WO°²²JII©¯¯ïîîöððHII©¨¨¨¯¯‡ñ¿éöÅÄÄúe:kÖ¬´´4‰¤©©ikkëààÀyöáÇ/^ܳg–2mÚ´ÌÌÌxzzBoE8Àÿ¿¡^:;;9j’H¤ŽŽŽþ7ÎÏù‚ (ŠÂ¹:A@D~”Xem³Œ¤xK;­¹ö¥¡•ÎB+kù?×¶Õ·t°Ùlô¯ ƒPE20Ç‚¾všé¼PÃUUU ØD‚—7/'Ø´AEE¥ªª PVVöæÍ¿IMMmhhÐ××?xðàO?ý$++»hÑ¢šššOŸ>!Ãd2BBB˜i.VÚ7 Å,Á|çÊÊÊ †&22²µµž’——ÿ¦ò‡!WIw º$ÊBQEÙ( L&L5•ý!÷náÕæ\ìrØÔKUUõË—/ð˜ó½yóÆÎÎNUUUEEÚ¢/^ ûf.&&¦¬¬ìîîÎ`08€]^]]=sæÌ   è¶0 TUU)++cŽ÷œòĉØÛ R©=®½|ùòøñã•••UUUá´ùŸH"%%ÕÙÙÉúÛ5éÈ‘# ÃÌÌL__*b<<<¢££Q½rå ŒPП–؃ãÕ¬¬¬’““Ÿ={6eÊkkë””””””É“'Ñç›n¿­­MRR’ë©®®®Y³fyxxtvvVVVæææþüóÏØÙgÏž-^¼øöíÛcÆŒÁååå­¬¬lll BTT…BéíÝ ""ÒÞÞŽýlooíCNœžqÂÿOîåG‰gåRÛ˜-í45E™†– Š¿ÔvÒè£å¥•ÉÒDÒFëf0YÒb @ºRÍf³xžë³<|ƒiº‚‚BUU‹Å‚Ÿéåååp:'**ÚÝ Ã廉TTTL˜0f†îÜÊÊÊÓ¦MûóÏ?{¾dÉ’%K–444øúúFDDœQPP¨­­Å~ÖÔÔ`oÇâÅ‹¹ÆM€”””¬^½úÙ³gcÇŽhkkÛýê‹ÀË&BYYYRRòÓ§Op¬¨¨xáÂEŸìììüå—_LLL¾[$œÁ…§í€€€Š¢ÿ¿ª‰UùƦyéeÕS­Íh´Î¦.”Î`NhJ$+ëš™L–¼ŒÔ_— \²äðqíAÅ1*$ö »×„7n,]ºt÷îÝ“'O†;¦L™âììlbb¢  0oÞ<ÎK,X0yò䆆†ùóçoß¾@&“ N:•ëôFLLLGGgôèÑD"qÛ¶mК7 ÀÓÓÓÐÐL&[[[?yò„—ð‹-*,,7nœ¼¼<&$‚ þùgpp°¶¶6F3fLxxxÿdx@DHHˆŸŸ_RR’Åb>>>"‘øýX7|¼Úœë"‰7oÞ\½zµœœ™L>yò$t†ïÁéÓ§===( ×pD"ñÁƒªªª0È< !!!/////ïàÁƒ0gzzº¹¹ùPÝüßÀWiùòåÇŽ£P(nnn0ÝÆÆæÐ¡C+V¬(..Ÿ6mš‹‹ ç…cÇŽ]²d‰‰‰‰ªªª‘‘œÐ‚>ßAȲeË,X %%eooùòeÎS~~~?ýôÓÊ•+Ïž=  Ñhd2ù?þ5jÌãíí½mÛ¶¸¸8ø³?/ìWéãíƒÎÐ4ÉÊÊ*''>^·Ï•„„„qãÆ©ªªr=K$oß¾´oß>AAA[[Û}ûöΞ=ÛØØˆ5»¨¨(ô(--Ý»womm-™Lž7oŒG !!!&&Æb±Øl¶€€€¨¨¨¤¤$“ÉTRR:~üxDDDMM©©éîÝ»Ð>88ßÅÅÅííí}›æáüë5j”¸¸8¯ágÄÁG*^ Ûð…ðZ`|—÷ÁXO A£pØí£(Š  àSiQe½ÆhÅ.³¸¼ZUNÌÌ@æ©®¦¢|‚* rß*PãÇÂS-Û\Ü}µæHpѰQô% uMz!Ímƒ€þ°uëV&“‰M«ppp~Pྃ¼ éW®\9~üøëׯ‡SªÈ”)SŽ=jf6Tá9Pe³Ùl6jXÃd2Y,ƒÁ`2™L&0 ƒ1yòdEKJJÔÕÕû³<•J•’’ÂâSàà 3ÅÅÅÁÄÄd¤ÁyÞ½{ÀÂq…Éd655qÝúÍp†Žââb"‘ˆk¯þƒ<}ú´Ç*W222黇ô1|q"%%…¢hê«ÔÉ“&“Éí+ÿ×Ö„¼®a±Y,믬@(@6ŠÕQÍ`#%U­íŠrÒ˜j|cN¤µuöæÞX¾ôÅóÌÿ-„ëúr9Q5ÖÔùÈñ«pppþ5@÷^Ðh´“'OBよçÏŸ´88?6íííЩÇÔÔóAÃÁù®hooŸ6mÚHK32ôÇ#ØÔÔôÙ³gC- Oí½»»©©ù//ÿ5üF„¢ aŸ´¼ €@KK+¶!aGG§¨„ÐÀd’‘ñ¼oóîݵ¾¹¥%¼²5³X¹¢“~9d7Óf`áààüwxòäÉܹs§M›w+ÀÁÁùïÐÐÐ0<¡Xp~p³mœï¼gþ—é穸ÃWà©—¨¬kê줱Ùl6e£(›ÍFQôïc€€ AØB@ADXHˆgü€þ!gjº.#3û÷_cÃÃj;iüÿ‰ Íb¢òë‚|W­&(ŠX ›.ο˜3fÀ`‡888ÿApíÎWá©0ÔVƒ§ûƒÊÇÇ7(NYc—¯4òYöhóÆ{—¢aðƒ÷]Ý-–Vó÷EJÿ½!Nßà ð=m󌃃ƒƒÑã/ÔíÛ·\ãs5|˜››«©©9oÞ<òiPë¿ýöÛ¢E‹þI =n¿Ÿ­ÑãaA…ÔÍ›7ÿ‰H¼jR¾|ùòâÅ >>¾û÷ï¬KKK##£þ§·°Ùì%K–˜››øðÁ××wñâÅ---#-Ôÿƒ¢(‹Åi)xâææ–œœ\]]}íÚµ£GÂîD¡PNœ8ñéÓ§ÂÂBƒÅ‹ÃÌfffqqq•••/^¼(-- ï] ¢¢âƒ0ÅÁÅ‹µµÿ˹ŋ“ÉdxìííÍ`0ZZZvìØáééYUU5´·úŸŽN½Õ=R†A\;€ƒƒƒƒó]@ øøøæÎ{ñâÅÄÄÄôôt@}}ýŠ+ÆŒcll ×· œœœLLL6lØÀd2—,Y2fÌ  ”——ðšÐÐЈˆÎê¶nÝZUUåíímbb¹ŽÁÇÇ'**ª§§wèÐ!;;»½{÷‚ÿõ,ˆŒŒÔ××WWW?~<Õßß?88ØÉÉÉÚÚÚÙÙùË—/0gii©«««††ÆÄ‰ÿüóO˜˜0aÂuuu}}ý?þøPTTäè訡¡¡££ÔÝÝ söìY===CCógÏö5''^8eÊ”ääd™L¶¶¶Ž‰‰ÁòÄÄĸ»»ÃãI“&•••UVV^¿~-##£¡¡ÑûöùøøÇ\«ƒ÷>oÞ<%%%gg熆† 6¨¨¨L™2åÇ0¡¡áfÍšeii¹aÃ:ÎY#×çwçÎãÇÃg͵?À.-¬q…ócËÏÏïÞ½{¡¡¡ÚÚÚµµµ‹/VRRÒÒÒ:räÌÃf³wîÜ©¬¬¬©©yåʬ„ÏŸ?;88Éd##£˜˜˜øÇ8::úúú={ööíÛæææ222ÚÚÚ'Nœà:Aµ°°PQQ‘””TSS#‘HŸ>}BQtüøñÁÁÁÚÚÚd2944´¢¢¢ºººÇ…\ËÈÈPTTĦâwïÞ566FQÔÇÇgûöí¶¶¶RRR³gÏ®««[µj•ŒŒŒ™™YAAAï’---±”+W®˜ššÊÊÊAw ‰ÊÊJxvË–-!!!(Š~úô‰L&|àô,HNN¶²²RSS›={vnn.L444deeÅÆÆ>|øåË—œ’444¸ººúúú~üø122råÊ•T*àîî ?&JKK333]]]á%ÁÙÙ‹rj ú¯êqqq»wïþøñ#N·µµ:ujQQÑŒ38—°ÒÓÓãããSRRjkk=ÊY2×çèêê:wîÜuëÖÁg ¸õÀ§OŸ †…œ“ÃÓ§OÛÚÚ†‡‡êèè,\¸B¡|øðáÉ“'QQQñññ(ŠÆÆÆFGG?{ö,33NfPe±Xnnnfffeee§OŸ^»vmFFlá§OŸzzzfeeùûû‹ŠŠFEEQ©Ô+W®ìܹóÍ›7\'´ÊÊÊ***l6ÛÕÕµÇY¨µ‘““ë‘ÎU0iié'OžÀ<111 ,€‚]»víÀåååt:}Ê”)Ó¦M«¬¬´µµ…{N***ZZZ444àÏlÞ¼ù—_~©®®¾}û6™LFÿ×T³a[ZZ:;;ÓÓÓáSàl§OŸnݺõܹs•••³gÏ^´h4ˆhiiAQ455õÉ“'»wï.))QSSÛ»w¯¥¥eaa!ÖhcÆŒÉÍÍåÚ€œHKK£(ÚC5€¥÷ ÿ½}ÀÄÄĸººººº¦¤¤À.¡ÛÚÚ~úôiÕªU˜æ®¢¢ÂÏÏïСC………RRRppàäâÅ‹™™™™™™cÆŒÁÒ+++½½½·oßþñãGWWWww÷ÎÎNx*==ýÞ½{/_¾,))ó@”ÇØÒ^röQ]||<¬.66ÖÇÇg÷îÝ………ííí/^jhhˆˆˆÀ̆††………½ëí{ôð°ÃkøÜ»wïäÉ“NNNd2ùéÓ§0‹„òÕQex •““£P(l6«[[[)Š˜˜Øúõë·mÛ†eNLL¤P(âââñññëÖ­ãZ ——×¥K—7oÞ´¶¶–è£vƒÐ××´[Âù¾á°CοEÙl6›Íf±Xœÿ2™L¸s “Éd2™ þÄX† …ÒÜÜ\XX˜““sëÖ->>>EEÅ+Vܾ}»‡G=??¿‡‡<Þ´iœi.òòò]]]œkÝ|||]]]yyy“&M=z4–îìì,''X¹rå®]»jjjjkk+++7oÞÌÏÏ?a—ëׯ‰Äüü|mmmè> ÐÖÖ†Fžd2ÙÏÏïæÍ›Ð+žÍf‡„„èêê.\¸ðÆœžá·nÝ;v¬““`âĉS§N}ôè‘———½½}ppðëׯ-,,bbb¬­­¡`wwwŸÕ«W'&&†††Â@hýWu777è¹0wîÜ7nÀ>>¯_¿~ûö-‰D⯲²200022Abs EQ77·˜˜›ŽŽŽû÷…ÁtCCCÀüùóccc]\\NNN=Jnjj!0ýüùókÖ¬™8q"VÇ©ÿ«p·ƒ¥s¶Æï¿ÿ¾jÕª±cÇÖ¬YsðàÁÏŸ?ˆDâÚµkQ…†999***Ø£Á¤mnnîÏ”ÞÑÑEÑøøxøsΜ90å«:oß¾-))qrr’••ÕÒÒŠ‹‹[»vm^^^MMÍúõëùøø¬¬¬°N?eÊ+++ÀºuëNŸ>ÝÏZâãã'Mšdggðõõýí·ß’““ííí+W®„o™µµuNNŽ»»;¯±¥7}ÈÉ«º€€€Q£F5jÒ¤IØ ½±:::ÄÄİòÅÄÄ MAú=<ìäççs~...P«‹ ˆ‡‡Çõë×íììZ[[=z´k×.X`ߣÊð°iÓ¦+V¤¦¦¦¥¥‰ÿ|MLL,++«¶¶öÂ… ðv Ó¦MËÌÌüøñãÕ«W±ª666Ë—/¯ªªºxñb@@¯Ø=QQQQQQ"‘xøðáQ£F öáü#2Xqe6ÄÁÁÁÁÁt¨Tª””Tee%“É„vžƒ¡££Ó#'‹ÅÚ·oß½{÷ÚÛÛ BGGN‡6ðƒEuuµ°°0g™:::ááá{öì)**²±±Ù½{·¬¬,þ  R©µµµ ~¬F ˆŽŽ>xð`XXØØ±c#""ŒëêêBCCÓÓÓ Fww·ªª*¼DPPû2SVV~òä §lïÞ½ƒ³8@gg'tKœ7o^LLŒ……Ell,§Ñ2`̘1ÂÂÂ{öì™0a‚””Tÿ›‚WuLû ,,Œ‹ˆˆpÇÂÚGNN[ýƒôç9ö§?|ååå ›½3 ===@uu5öm ÃtÁDyyyìQª¨¨dffÂc …‚•ùöíÛˆˆˆ¢¢"E›››W®\ X¶l\†Ý¼ysXX€D"‘H$WW×W¯^9rdûöíðr*•êèè0?þŒµpKK WÁ ,°´´¤ÑhwïÞÕ××ÇNa^Ä0V<î=C“””ìììd±XD"PYY 'ZýALLLTTûÉÙååå¯_¿ŽŽŽÆRjjjÈd²¨¨(öp…„„zGPƒ´··÷½¶ÉÉœ9sñññsæÌÇ#µkצN »º‹‹Ëµk×Ö®]K¥R±Ý¾±·›J¥bª¨úY •JURRÂ~Ž=ºººKKKÃ!!!˜ÈkláZ,/9yUÇÙ¯8øLEEEÛÚÚ° ÛÚÚ¸nûÒ÷è1àa‡J¥r~9Ÿººîß¿¿­­ º óÆ4}#&&&&&æîîþâÅ‹ÀÑA …B¡P¶oß®®®^UUE"‘‚‚‚òòòòòòõõõË–-KJJê] ‘Htww?tèPnn®/íÀ’%K.\¸Àb±òóó ïÆÞé\;€ƒƒƒƒƒÃ“‚‚‚âââ‰'ŠˆˆH¤ÔÔÔ>vÉMJJºs玌Œ •J500€eEEE±Õþ¦¦&l^ŠÑÿwïÞ½Û{)ÛÃÃÃÃã±±qýúõ‘‘‘”••Á³­­­MMM …@ P©Tƒ¿PËËËåååãÆ‹‰‰¡Óé'NœX³fÍË—/ÃÃÃEDDž?.,,|ãÆ Ìú·»»»¡¡**++9§^EEEKKK¸ÈÓ[Bwww''§¦¦¦€ ìØ±ZÿöŸ>ªëeee¦¦¦ð ÇðzŽœIAAá«ý+=l°EoEEE‰”‘‘ÁY Š¢òòòµµµð’šš˜N¡Pª««ét:|”eeeòòò°ÎE~//¯"‘3œ;wîܹsXùœ²±Ùì’’˜XWWgoo¿páBx!@]]s&ÏU0E5555559Ý 8«ãü—«ŠŠŠŸ?ÖÒÒ‚?aŒÎ<"""ÝÝÝ0±±±:>8o¿Gk(**zxx@/ŒâââÞÂÀ {¤öÿ»ÙÑÑ®“Ô§6N¿uëN‡›­2Œæææœœ …RWW‡eƒJC…Bkìúúúòêê %%%ûYQQ®ð[¸ËKÎþWÇ‰ŽŽNqqqWW— //oúôéý¹“; …ëð € "''gaa»hÑ¢o­ex@Q¾8=hkk«««ƒÚ¯f†xyy™ššB ‘¾+%‰FFFÖÖÖ÷ïßǵC ²^½zÕ{‘‘ £H^àqppppp¾ Øl6“ɬ®®¾{÷®··÷¬Y³ÌÍÍuuuµ´´ÂÂÂÚÛÛÙlöÇ322z\ØÚÚJ&“áþœ_½Е´¬¬ z§÷@FF†3ª˜Lfggçû÷ï·lÙ’˜˜¸uëVγ?~LKKc2™p[›º}ûvvv6ƒÁصk×øñãÉd²žžž¢¢â¡C‡ètzzzúŸþéââB£Ñîܹ÷«—’’‚Ÿh­­­ÂÂÂ]]]œ“v°wï^:^XXxåÊ•+ºóçÏýúõŸþÉ`0ètúëׯ+**à©ñãÇËÊÊΟ?_PP°Ç .^¼8..næÌ™¼Z€+}T×N:U__ßÔÔtøðá7Âë9r>&^ýᛢrÎcuuuµµµ·oßÞÖÖÆb± ÓÓÓQuvvŽŠŠêììDQôĉ0³¾¾¾’’Ò¾}ûº»»ÓÒÒbcc±y8gá­­­zzz¡¼¼F1èAGGÇÉ“'KJJïܹ íðfÍš@£Ñh4Zï= ¸ qss;sæLJJ ôè!èå лMlllž?úøøœ>}:--Íf—••}þüEQ##£G¡(ZZZŠ95ô]‘¯¯ïÑ£Gß¾}Ëf³[[[oÞ¼Éb±x]"##C$`§ž?nccÓ» û@KK«ï ýï® !!Íf¿|ù2%%%%%%55ÕÒÒòÚµk’’’·nÝ”••=xðæwttLNN†ZÅ[·nõ0¨Œ5ŠÉdöŽïèèøòåËG1™ÌóçÏ777C÷®ð[zÓ‡œý¯Žccc•Ðh´èèèÚÚÚޚʯ2àa‡ëðË5§»»ûéÓ§³³³1«“JØÑÑqìØ±âââÆÆÆ›7o^¸paÆŒ€?ÿüóÙ³gEEE~~~ÐÄãÔ©S¹¹¹ÍÍÍiiiaaa03WŒŸ>}ʰ 7(Š2™ÌîîîwïÞ=}ú;0Ô`ÃWjjê]RSS9Ç´aä‡Ñ°ÛÚÚ^½,þ3îK~>û;Þzg`¬_¿žB¡XXX=zÔ××.´"ÕÐÐ0~üxMM͵k×rÆÐ†xzz²ÙlkkkwwwNÛ×;w^ºtÉÆÆ&44ÔÆÆ†k»víRWW?þ|S ECCÃÝݽ©©éáÇpÅ£³³sëÖ­ššš†††4mãÆ0}áÂ…!!!ÚÚÚ?~ e¹î‘#G|}}ÃÂÂ`Çë1;EäñãÇS§NÕÖÖþùçŸCCC.\ˆ¢èÇ Ž;&û7™™™=®å*ÄÅÅ%55uÒ¤I²²²½ï´cŒ¥K—^½zÏœ9sçΫW¯–——Ÿ?>•JEQtÏž=QQQVVVÛ¶mƒ.è\Kãü9mÚ´Ý»w(((˜››ß½{üoÎK¦Nªªªª®®®««‹¢h}}ý»wïæÏŸß[ÔÓÿ¾:0®]»æáᡬ¬,÷7Ë—/¿qã‹ÅºpáÂÙ³gíííÃÂÂæÍ›ó=úÈ‘#^^^óæÍKOOçô!‡ oذaÚ´iêêêXº²²ò… vîÜ©¥¥síÚ5NçŽð[zC ¸ÊùMÕõ.ðÕ«W§OŸŽŽŽ–””ìÏ…œ xØá:ürÍioo_UU5{öll~Ä£‰ÄŒ7NQQqûöí»víòöötuuùùù)((Lž<™Ífß¿ÚA”––ÚÛÛ“Éd77·éÓ§c{[p¥G$šÞ\¼x‘ŸŸ_DDdîܹ¼Uà p€’–– ’’’‚?¥¤¤‚‚‚°èªÃ# B£Ñ¸ž(§Ö€ (Š‚¿þCÑ¿†sl|E‚ A]t:‘@b²ØÝt:Š¢”Q’Râ$®…(ƒÑrírÃ/G  ™ÅÊ7ú)ÔÄqÄ<Êppppþ5 ß•pÚ´i(Š–””¨««755}µ AૌÿüýýýýýGZïCCëW¯ÂbÃIRRÒ¬Y³¾I‹ñŸeþüù[¶lN+Ö>ˆˆˆ––^»ví –I ±`{\a2™Ð-¨÷©ÿÎhöd„ {öìécÉ}¨IJJÂv–ÁùOõ­†††C‡6nÜÈ’@ $$$ xøâ* R_¥Nž4™L&Cü—çèÆFÁ(qQ&‹Íd±Y,6“Åf²X,›Åb³X(“ÅFQ”H$ "ð MíüüÄÚ†"Àd1ùùøÄD„ÿ¹v ãñÚ½;‘ÆÌÈA’HœÚÝQ¾%èâ¿Ûì= ϱ¹ ΀&åß ¡¡¡#-Î…„„:Þ÷¼ gèà4 €ð€3}xÌxjjk¨T@Qhð·Bá¯c AXŠ ,°DNŒADDAäe’ÐèÅŸ«w†±³Þ!ÜÎŽàW.)ÊtrH±šá¸k7i”Ì?¬çßÊpšeâ|Ïàݧ7ÎÎÎùùù'OžüÖp§88ƒE¡ î<Ò#q„µD"ÑÄÀø?À`²ht&IH€@ `‰ 0‘J­¡T~vSSÝуñ·>›@WP ûÕ³;ÖS”Öo˜â»‚3ú(Î0 àð"77w¤ªÆµ8¼àôæÏ?ÿi€Ó†ç¿ÆW‡&Àf³±ýJ‡žÚ ÂÂBœ)U -L6*ÈGd¢„:DHàÿØ»ï¸(Ž·à³W9àèpG/JQĆ‚(Ö׈Š5jb!¶5±klQc‰QcŒ%úÓØbG‰F“  ¢4¥©ôŽôã w»;ï«Aªåù~ÜÛ}öêîT®P»æÈ 6›ÈÆ|ÿ’E/³?…ËJx£Ï'oJ߹홋«³ï¡¡!MÓAÀÐ8æòÚÐа­ zzz?öòòjë@@øÏJnš¦ãããõõõ[:’ª‚1®R‘©B_¨Íb±0Iò|Œ‘L®às9<.»zxðÀÕ”´>:ÚNZ¼†·°ãq³ž%CéÀ;A¥R•K‹I¤`l®¡¶@çÝjô‘••5a„/^¬\¹ò?‡JR(†††µç¡Ðjôôô\]]ß­ŸÐìhšNLLÔÓÓkë@¨ƒ]ZZÚ;wŠ‹‹Û:ÐÚnÞ¼Y###===föÊÕ Ò¼âŠi•Ÿ+W’yÅ…/]ímÄÆU,¶TQU¥bkjAДL!¡èk•dœ~BeƒgZ¢”ªæŠ¡…dddtêÔ©ö\\©¬2Cú„¥'ãr9˜ÆE2dPj/6°~‡:zíß¿¿gÏžwïÞm«–,YrùòåÂÂB±XüÙgŸ1S‚¥§§¯\¹òîÝ»2™ÌËËkûöíoÃÌ´ ,pssûì³ÏÚ:÷\o‰¸¸¸=z¨ÓlÞ¼yáÂ…¡ãÇ÷ÝwºººC‡ݱc‡¶¶v oܸ±fÍšüüünݺmÚ´‰ië(“ɶlÙrïÞ= æ”””¨¨¨†L±Þc†††zzzmu³··oëÀ‡®A¥d•œ$iÌçÆÆÅóT•v梘‡áòíí­Ós^Ú›×ÑÿAQ¥R6®ùÞëbý’Ý¥K±XBCC™lŒñªU«²²²>|½oß¾V ìÃQç[¢¸¸ØÍÍ­K—.Ë–-“H$êÄ¿ÿþ»­­­™™YHHȼyójd•””äîîÎ,‹D"##£/^ „ÆŽùòåËêY€÷†ÆÒf\_ŒŽ¶–!•K–&zlŸ™ˆ¢(_›Â¸¸¬¢ƒiù-Ðp—(¡èÇ2y=û½Ñá8Ž\.‹‹S*•¶¶¶VVV¡I“&15½ååå¡¡¡ãÇgRÆÅÅ•••ÔÙ‘›Ífþùç!¦3]ÓO?ý4wîÜÎ;³ÙìàààÜÜÜÔÔÔ†‡÷¾Ò×2ËÈ»á·òo‡ä^¿–{ýfþòóÌÞ¡qÔbbb6oÞ,¬¬¬æÌ™Ã̈sæÌ™9sæØÚÚêèè¬X±‚IüäÉ“¬¬¬•+Wòx¥sçÎår¹\.wàÀ111ÌÊëׯ3¥...}ûöår¹b±888øÞ½{­؇£ö[B$?~üòåË»wï¾{÷îìÙ³Õ‰ûöíûàÁƒ+W®Œ?ÞÆÆ¦FVR©´ú›J(Êd2„“““žžÞèÑ£¡[À{IãýAcæ^ÅÓ67f–™è•WÊË*å9ÅJ g”pùü”BIQ¹”¦iüj:aŒ1FDã:Ô7Ù£´ù&"rssÛ²eËÚµk­­­§NZXXˆ?~|hhhEEÅo¿ýÖ½{wKKK„йsç®^½êèè8xð`¦²·]»v@ ܾ}!¤««Ëãñ˜lAee%B(33sÿþý^C4Wðï._0Fïc}¥¦0ÆÓ˜P±z¾=Û:´ÆÈÌÌT©T^^^Ì«¼sçN¦N577W= ¨z!77×ÜÜ\Ý÷ÛÖÖ–i­277WçùèÑ£áÇ;;;;99ýöÛoEEE¡éÓ§3o¹µk×2É„B¡••ÕØ±cI’üî»ïÔ»ççç:tÁ‚ãÆkÁ3¯E=C¯ú#PQQñôéÓ^½z!„ ?ùä“:8::.[¶Œ9)мj¿%D"ÑðáÃÛ·oïëë»o߾˗/WU1£} >Ÿ/‹}}}X{ÌHê­$ 3láÚµkY,ÖÕ«WCBBZë´@ëÑ8ÁúçæÞÜX/æiR¾„,¯”Û[š—K)ŒRs er¥¹‘µÈˆÍ"$ò*I _}M¼ae¾Mk »Þü0z³¦é“&Mš4iRIIÉìÙ³7mÚ´k×.‘HäããsñâÅ“'ON™2…IÖ£GK—.UUUíÚµkæÌ™LÛiu&µ»ì2¬¬¬&L˜P£ÉnVVÖEøþa±X=,z~.]*¹žƒ³XÐßs„õH¡Î;ÙÙÊÊJWW7::ºF-½………º0ˆ)xbVæåå©T*¦€ ##ƒ)~Bÿþ¤­Zµj„ l6{Þ¼yLaÙ‘#G˜aájÃ3£Z „^¾|éïï4þüæ<ÏF¹}ûvŸ>}˜“]±b…ŽŽNdd¤¶¶öéÓ§5§šEõ·„—Ë¥iºöLïu&vqqQO«óòåËÒÒÒöíÛ#„^¼x±hÑ"õÐà}¢±íŸûOÁ@‹ïíÕÉÁÂXU%ÿý^̳̂G1ñ4Eiq°‰¾ðö¿oܼ-‘)e2™ºíñ÷ò‹µÙ²LJJŠˆˆ IRWWWWWW]£´gÏžèèèQ£F!„d2Ù… d2ŸÏ744ä6xzÅY³fíܹóñãÇãŠŠŠ .Ð4mll¬R©Ô5Æ ‹ehhØÏ¥ÿ—í–o²Ú¶±Ý·“]§Ú[;¨_¼[:tèàìì¼|ùr‰DBÓtRR3LÀرc=*—ËB{÷îe»»»[YYmÞ¼Y©T>xðàÌ™3µó¬¨¨pwwg³ÙYYY—/_®@*•îÝ»7--­´´ôòåËÇïß¿?B¨¤¤ÄßßÈ! ,P( …¢m‡rPw+@•——;::jkkËåòÇ·aTï%Mo‰;wîÄÄÄ”••%$$,Z´ÈÏÏO  „~üñǧOŸ–••EFFnذIŒZ¿~ýóçÏB£Fb†ÌJ¥;vìèÚµ+S’åîî~íÚ5‰D¢nƒÞ'Ûðx<Œñ?µšr±3/)-·57z–‘×§Ÿ—\.+U`¥ŠìÓ³ ›ÍÎ~YF’”¹‰á«]ˆF¶àëêjÚTOv$Â|Ý7¨–Éd‹-JMMåñx>>>Û·ogÖ0Ýn1Æœ3gAÎÎÎh`þܺuëܹsÓÒÒ„Baß¾}G¥­­½téÒ=z$yëÖ-õ¸_‚ ´´´¸\®¾¾>EQ,‹Ãá°Ùì6Ÿx¯q‚8}úôòåË;vì¨P(W­Z… LNNöññ‹ÅLb6›}úôé 0ÅïÚµ«k×®µóüþûï§M›fnn.‰>úè£Ú Ølö­[·6mÚ$—Ëmmm×®]„ WOpïÞ½.]º´ÔÉ× c|óæÍuëÖ1W¯^=kÖ¬‹/êëë÷éÓ'44´M¢z_izKdffΛ7/''ÇÈÈhРA›6mbÒgddlÛ¶íåË—fffÇW¿Lû÷ïïÑ£G»ví¬¬¬öïß¿fÍš¼¼¼nݺmÛ¶Iðå—_nݺ500¢4·ðï,‚©á¬-þyºg'‚ 3Š@µvûcˆ@ /ÒŸgµ³±T¨ÈÔÌ<;3¡—»“&//_‰YV"ã7 ¨ôyò·ƒVªÈÚ›ù¼áúu”Ðߣðð¾nªÝÛ·oe7_ã.˜¦ Z‚©©iQQ‘L&c³Ùm Ô‡i8kjjZçVø6¼µêÿúj ¥.öVLÓÆ|Îáp8™7\‡iŸ¸MšòçŠå7Nžb?HTT•öò¶a“¡…EÓóÐÒ´µµmllÚ: h*ø6¼ß4ÞÀÑ\wøMDp8ý·n÷]·A‘­d±˜˜ ôôÚ:(àýÑö7ÿ ÄÑè¶wlë(€÷ ¿|è tøÐAéð¡ãhiiµu Ú Æ˜¦iš¦)Šbþ2Øl6EQ,‹Íf³Ùl‹E’$Lñ 𾂶À‡J€”xOtéÒåÑ£GmÀ;é) %’ò{aÏÏžÎzò„¦¨¶@CùúúžeÊ„ÐgŸ}ÆçóÕ)?ûì3.—‹êß¿llìøñãëÙ4ŽÆÒVõ5¹Åå$ù6‰YR%‰¡Åã µùuîÎf³ÉF¤zY˜:r(]ZÒÀ}>AxsPú¶ÍIn\ûômÔ1´¬Õ«WŸ;wnÒ¤I'Ožddee=~ü¸{÷îL©TêááÁ,‹D¢êû1  //¯þ}@ã4hÎŒ±B©’H<›Åbašæó¸<.G&W(UÍ<}@Äþ~|‘š¬P¾QÓ;7?ù­ƒàÅ‹mÅ[A¥R–俧䕤WJ%¸Þ"o¡ÌÌÌîÝ»îÚµë?+ ‚ *++[!0Þf:::çÎ+))™6m3v€¥¥¥¯¯ïÃ×âãã.\È$þφ`õì §A¥yÅ/òJy<®\I>ÏÈ‹ŒMÈ}YÆå° [ª¨’Ȫš1 R*—PôµŠÊ“%9¯GŸjÐŽUÊf ´J©$áeD.;ºL𢘗ô\ž]”FÓt[ÇõöîÝëíí]ZZÚVw# ,°µµÕÒÒ²··ß¶m³2---00ÐÜÜ\OOoàÀOŸ>m“Øj˜;wî¾}ûÚ:Š÷ßÞ½{»téÂårgΜY}ý–-[llltttzôèqÿþ}få;wúöí+ Û·o_gnþùçÈ‘#ÜÜÜ,XP^^ά?~ü¸§§g÷îÝ{÷îݸ8…Báo¿ý–=kÖ,Š¢FuÿþýóçÏ«Tªªªªˆˆˆ¬¬¬fÕ”}@T:@VÉI’Ĉˆ‰‹/Í~ag¬ó0<9%C ÅÍ.(!pÍU*¥ªQ} ^×ä™RÉõ iå;uëêA’dtá½|^F ]YPUV¨*/e—>UD¼,/lëÐÞ@ff¦««kKäL’ úÈ|üñÇþùg^^Þ¯¿þºk×®„P~~¾§§ç;w’““‡ ö64ʸ~ýúСCÛ:Š÷Ÿ……ÅÚµk'NœX}eHHÈÆ™êú#FŒ5ŠyKèèè|úé§ß|ó¦Ü²²²&Nœø÷ß_¾|9--mݺuêMC† ¹{÷nS&000¸páBrròœ9sLLLΟ?ÿË/¿899uèÐaÇŽÕÐÆhfffÞÔfjjúߥŠ*¥ŒbÑ4VªHcîPÿ<==ÇŽQPP U(ILä—JëØ@¨QcÒÕ®ð0B‰ŠªÃÅeaR™²¹ouÖ¯_oaaa``àäämjjªž+ëÒ¥KîîîÌ‚³³³……EíZÐû÷ïW¯ëÚµë7BIII>>>†††ÆÆÆÓ§OW'ؽ{·¹¹¹••ÕîÝ»›÷tÞ ÒòXyTVeajEΫÿ$¹ªìg% oílC̘1ãÊ•++V¬°³³KHH(,,?~¼™™™­­íöíÛ™4ãµk׊D"›'N¨÷MIIûì3…¢æ:Õ3fĈ†††ÕW¦¥¥1Uý|>æÌ™ÅÅÅ¡îÝ»ÙÚÚjÊmòäÉVVVíÛ·Ÿ5kVTT”z‹Åâñxê©.,,L]xallqðàA‹åááqéÒ¥´´w , IDAT´/^œ;wÎÎÎ!”бcGõ¾Õ.\¸Pý¹¨sߨ¨¨®]»¾ix ²²R㨄òªW—ø$IUHe—cokËtåp86Ö–Åå2™BY^QÑÎÒ¤F7Q¢±óÔÞ‹Ä(RªHR(Çè²ÔØá?ÅÆÆ:t(&&ÆÌÌ,==Íf[[[›››ß¸qcøðá¡'NLž<!4}úôk×®y{{—••egg70ÿ¯¾ú* ,,L¥RÅÄÄ0+%IBBBzzúóçÏû÷ïß©S§~ýú5Ëé¼+$U’È—1lm¶K‹Çâ)i¥‚’ksL{cüNL9ñóÏ?———ûùùÍž=!Ô§O//¯ŒŒŒ¢¢¢>úÈÑÑqäÈ‘¿þúë±cÇ}ººöøÃa 0TÈ©»á·òo‡ä^¿–{ýfþò›j‰Y¬æ)÷iMñññQQQÛ¶mÖÖÖŸþùÙ³gB§N ¶³³ÓÑÑY³f “866633síÚµ<¯wïÞ'NT7+ðòò0`Bˆ ???777‹Õµk×ÿû¿ÿcn¹kûòË/?~ü¿ÿýoøðázzzÕ7eeeÍ›7o÷îÝ­ù”Ο?ŸËår¹ÜAƒ©«š¯]»Æt+puuíß¿?—Ë577_°`AXXX«ö!333>|xÏž=ù|þ÷ßðàÁ7ÍáÏ?ÿ<{ö¬º(ÇËËkÇŽP—y€ws˰yóæÓ§OŸ?^ãýAcæ^ÅÓ67f–™è•WÊË*å9ÅJ g”pùü”BIQ¹”¦iŒ°"×±ÕÓÂ\Ú|tèÐaûöí+W®4558qbAAB(((($$¤¢¢âìÙ³={ö´²²B]ºtéÒ¥KÖÖÖýúõ{ôèBÈÊÊŠ ‚ nݺ¥)ÿ;wªT*//¯:¨oµ´´Ôuw¶¶¶¹¹¹Íu:ï m-í±úJLaŒ1¦1¡bõfõébêÕÖ¡5FFF†J¥rwwwqqqqqÙ¶m[EEB(77×ÞÞžIãààÀ,äææZXX0s¶#„ìììrrr˜esssuž>üè£ìììlmmÏž=[TT„š4ió–[¹r%“L(Z[[ªTªo¿ýV½{^^žŸŸßâÅ‹'L˜Ð²'ÿoêÊd@ÀLÐPQQñäÉ„PAAÁäɓ۷ooccóÅ_0'ZÚ®]»Îœ9_UUõÝwßùùù½Ñ31oÞ¼#GލßÃnnn^^^ÖÖÖLax×1m¾úê«ÀÀÀÿû¿ÿÓ\:ÀúçæÞÜXOR\ø,='21C¦$³_–—HÏ2óKÊ¥:Z\k‘›EHäUÅåRuéñ†•ùÕâÓ<²T½ùaôf×§NúàÁƒÔÔT™L¶~ýz„X,îÓ§Ïùóç?Ît+@y{{_¿~½¨¨hРAS§NEegg3çèçç§££SUõÏ” L·^„¥¥åÑ£G wïÞýÉ'Ÿäçç#„ …úê<33ÓÂÂâ~°X¬Þ6> õ¿ì®êe©°n¯pÏž4×>X_¨ßÖ¡5†µµµP(LHHHJJJJJJMM½|ù2BÈ‚)oB©,,,rssÕ}òÓÓÓ---™å꟔qãÆMœ81%%%###00),;qâó–«=’Æ855•Y.,,8pà”)S/^ÜRçÜ`7oÞìׯS²lÙ2¸¸¸ÌÌÌ;w¾+cL¼ëžŸodd¤N¦Ö®];‰DÂÌwñâÅôôtfý¹sç ‚033c±XÌŽ,kÕªUJ¥2>>þÈ‘#µ»‹¿÷X,–‘‘‘Ÿû ®«·ÙïúÖeçŒN³ÚÛ96b³·Ój`É’%‰„¦éÄÄÄ „þùg¹\ŽÚµk“ØÃÃÃÚÚzÆ J¥2""âÔ©S5F˜gTTTtìØ‘Ífgff^¼x±v©Tº{÷îÔÔÔ’’’ .=ztàÀ¡âââ6ì‹/¾P( …¢moÂÕÝ BåååNNNÚÚÚr¹ü§Ÿ~jèÞW$I* Š¢(ŠR(Ìä=zô¸víÚóçÏ)Š:}útii)3×MÓ …B¥RaŒ …Rùj.ØÕ«W?{ö !5~üø;vtïÞ½ªªJàÆYYY•••ÕÇ)ï £òx<\} 8¹Ø™—”–Ûš=ËÈëÓÏK.—•*°REöéÙ…Ífg¿,#IÊÜÄðÕ.D#Ûðuu5mª';a-¡žæí5I¥ÒÏ?ÿüÅ‹|>¿OŸ>ê[¸‘#GΞ={äÈ‘B¡!„1Þ¿ÿ¬Y³‚pqqùùçŸk䣭­½oß¾1cƈD"OOÏÎ;3ëÿþûïÏ?ÿ\.—‹D¢Ã‡—–– …B6›ýÕW_}€­s ‚ÐÒÒâr¹E±X,‡Ãf³ß‰ñk#â·ß~[²d‰“““\.wvvfæ~›8qbRRR·nÝÌÍÍGŽÉ$f³Ù.\˜;w®™™™H$úá‡ꬕݿPP………X,ö÷÷¯€Íf‡††®_¿^&“ÙÙÙ}óÍ7̀ׯ_úôéÓ§OÕS'DFF¶ÕàíãÐÐÐ72ׯ_?mÚ´ß~ûM__¿_¿~ׯ_o“¨Þcß|ózÞÁ£G~ñÅÛ·oÿì³Ï222 ÀŒ^yâÄ fž‚¿ÿþ»ÿþLb@àåå‰úþûï½½½Û·oøðáÒÒÒiÓ¦1i´µµãââBQQQ[¶l©¨¨05­c¬™úÉd²¢¢"6›Íf³›ãŒÀ¿0E&&&ÚÚÚ΄ÐTÁõ4¹³›#AˆE Z»}Œ1D „éϳ‹ÚÙX*Tdjfž™ÐË݉I“——9|[ ³7 ¨äYÒ†>>•ª:&~wäó†ë×Qv@c|ÂcïÜ5²²zÓÃÕæìì¼{÷î!C†4=+>LQQQ3g΄æwƘ¦iš¦)Šbþ2H’¤(J¥R‘$I’$³ R©T*UïÞ½1Æiii¥¥¥ÿyˆŒŒ ¡P(ˆFX 4`Úï+ŠŠŠŠz&®fbŒÃ#Â{÷êmnnμššPÓ>MQÅ$bÊ0B#‚ÀbÑ#Œ]lT4‘–•[Q)³43R  7@ÍÈÉeóÓ¤ó3§ß »Kþ;“:¯(3•ªÜN]ÆìÜÓ,E—.]ªªª}GmÙfôzB84…Æx‚ šë¿‰gðw»lÚ"ÏÊR²Xö¦f½7˜žõkû›ÿâh „ŽNÿo†ß>tP:|è tøÐq E[Ç Í`Œiš¦iš¢¨êI’df®!I’$I•JÅ«,.jâ±´­¾}ûš˜˜˜ššÚÛÛ<øÀ$IÖ“^"‘lݺõîÝ»oT4Pƒ§§ç£G½{;v,ÿ5###MÉ(ŠêÞ½»¶¶v=Y]¸pÏçoÞ¼¹Æú… òùü{÷îÕÞ寿þòóó366vuuÕ”íÝ»we2Y üòË/ê‡óçÏß¾}»\.o«x€·ŸÆ¶l6ÛÓÝ!ŒBóR‘”\IêjñX,–ze5³2?¿@ÙØJº´ôå®íÒ+‰zëI„\ù¼ªˆ¿/÷óµZ°ÈwƬÖin %ìÞ½;00°°°ðÑ£GëÖ­‹ˆˆ8v옦ÄùùùB¡ÐÄĤ5#¬I’NKæS»íÀ÷ß?yòd„AššìÛ·OWWiî•PYY¹fÍšN:ÕÈ?:::22’)V¨½¯@ øä“OŠ‹‹÷íÛ§)çŸ~úiâĉMiïPãùlÜÓ«ÀÄÄÄÓÓóÂ… &LhtHÀûMcÛ–@ ´-–V‰´ªP¢©hfI•¤D¡RQˆ_Ïãñx}:ó;]QQ!—ËÃÂÂ8ÀbĈf‰´¡ýû÷÷ìÙ3???88¸MX²d‰“““‹‹ËÎ;™•ééé“&M²³³333ó÷÷o“ØšbÍš5[·nÍÏÏïÒ¥ËÊ•+íììÚ:¢wFeeåÔ©Sííí™ÔÚ6oÞlmm͌ʦªõÕZVV¦§§çáá¡þá‘J¥nnnîîîÌÃ9sæ0ÕËaaaýû÷o±Sù±X\VV–””·víZ---KKËY³f]ºt©FJ.—;~üx--­/¿ü2""¢éG—Ëå“&M …?ýô—ËE=ztÖ¬Yl6{öìÙyyyéééLbf¬;¦w³!$‰:tèÀ|;ÆŒŠ'–.]Zû¶nÝúÓO?ÕOëƒ3fœ>}úêÕ«ƒ=ztlllüñG~~þ„ °†{xš¦,Xðí·ßª›ë3ë wíÚµ~ýzM;Ö¸L©s}ii©¶¶6‹Åb9rdÞ¼y={ö$ÂÆÆÆÁÁ¡ž}B .T'ðôôìÛ·/óðСCsæÌéÔ©‹Åš7o^nnnJJ ƘÍf3e"‘ÈÃÃ#..ÿ»Ä¡££SVVVÿ­, @ÓÃêš~yóŸÔ…¬’“$ùÜØ¸xžªÒÎ\ó0\ÞÁ£½½uzÎK{ó:ÆCRT©”—ðõÐ$y¦Tâ¢Å÷Õè²TŠÑúZ´—)x;eeeõéÓ§%rnàÛi̘1ÁÁÁÏŸ? tuu2dHAAAçÎW­Z¥¯¯¿qãÆÑ£G'''¿[#qdee9¿ž‚¤vÇ`Œ1MÓî²ô~ûúë¯sss“““Ÿ?>räH77·oÑsçÎ8p $$ÄÈÈhÔ¨Q[·neZ›×Àçó#""zôè ‰Dùùù̦aÆ©T*Š¢B"‘¨   åO åççfgg“$éããìT©T...5RRµeË–k×®UVV²X,©TªT*y<^SŽþäɦEëõPVVÖÇýõWuš‚‚‘H$ «û§£££>´@ J¥r¹¼¸¸xîܹÌc¬T*iºIc÷<˜Yprr ¿xñ¢‡‡‡z«R©\²dÉ/¿üRO'Nœ‹ÅµËz¾þúëùóç×ÐáèÑ£óæÍCy{{ߺuë?Ã300ÉdE1§œ=zôè† …:::ê‡b±X½œ™™yÿþý'N¨×0/Aõç\KKK*•Ö™see¥¾¾~ÃZÍðáÃBW®\>|8³ÜVþû®[Q¥”Q,šÆJi¬Ãêÿ‘§§çØÑ# ¤ %‰‰üÒº~† „ugBSÿtUÀ%*ª—…IeÊæ.,100ÈÉÉa–—/_Î\+9rdøðá³fÍêÖ­[çÎÿúë/„ÐâÅ‹srrÆçìì|ìØ±””±X¼cÇŽ®]»Îœ9óáÇ:tPgÛ«W¯ßÿ!tåʱXlooÿã?¦¦¦._¾<,,ÌÙÙ™i®©R©Ö¬Yãäädmm=gÎæjcüÍ7ßØØØ8::ž:uªyO4ÝìÙ³CBBÖ¬Yãì윘˜øòåËÉ“'[[[;99íÚµ‹I£éELMM ‹Å:u:sæ ³’yËÍœ9³S§N¼|ùr·nÝLMM™þÕµõêÕËÎÎÎÀÀÀÁÁAWW7%%!Ô£G%K–8;;‹ÅâÕ«Wgee©ïëZÚ¦M›ìííÅbqÇŽ22²zš;wîܺuËÞÞÞÞÞþÎ;;vì˜4iÆøÏ?ÿܸq#³^&“7nïÞ½S§N­¬¬¬¬¬¼yófCªA,--õõõ™Š}æ!3F@uÚÚÚUUUÌrII‰úU«1ØAõ‡–––‹/Ž~-55ÕÛÛ»Æó£^f^—êY%%%uìØ±!ñ­, à‹/¾ÐÔj€ñFW£ñêD^õêâž$© ©L¥")ж·µe~n9޵eq¹L¦PfÕŽ•hl­eí½HŒ"¥Šc%å¥TSgIlˆ;wî̘1#22róæÍLëÐï¾ûÎÒÒòÌ™3ÉÉÉS§NE1½"=ztäÈMù|úé§?ýôS~~~tt´ƒƒÃ–-[|}}“““™q¶6nÜ–œœ,‘HÖ®]‹:{öì‰'ÂÂÂbbb®_¿Þ ç ÞȼnݺäädWW× &ˆÅâgϞݾ}ûرcW®\A^Dš¦Ççåå•™™yàÀààਨ(fÓíÛ·'Mš;gÎ]]Ý_~ù¥  àÔ©S6l`Þ*µ­_¿ÞÚÚÚÖÖ–¦é?þ¸ÆÖû÷˜ˆD¢{þwäÈ‘‡æçç_½zÕÒÒ²S§NæææLBèÔ©S'NDµ>5ò‰ŠŠ200¸~ýzíS¾téÒ¡C‡=<<êüÔøúúþý÷ß¡°°0{{û°°0f¹W¯^l6c¼jÕ*¦Ö7::Z=JÜ{#++K"‘têÔ‰yèáá‘P#MBBBõ™™™uŽ…1räÈÐÐP…B‘———’’âëëÛ¢‘×FÓ4I’yyyW¯^2eÊ!Cºvíêêêêèè¸fÍšÊÊJš¦Ÿ={öøñã;VTTˆD"¦ºûàÁƒêõîîî·oßFeddÔùjbb’––¦)žO>ùdÖ¬Y£FÊËËCM›6mïÞ½ÑÑÑc‰Drùòå†×ÿO›6måÊ•LAC~~~íê÷7•P&“:u*77·¸¸øøñã!!!C† Ágggýõ×*•ÊÆÆ&999<<<<<üçŸf³ÙáááLÏf¸GŒñwß}ŤéÕ«×§Ÿ~ºgÏŒñ½{÷î߿Ϭ˜6mZ«Š¢är¹J¥ÂËårõM~õË—Aƒ………1§M›¶ÿþÐ4‘‘Á”xxx0e éééW¯^­¾oËã3fìÚµëÑ£G4MWTT\¸p)©s“ÌÌL¥R©Þ6hРz®º€6äèèX‚^u4…ÆÒæã9£´ IDAT¢#¤£­eÈGåR…¥‰›Çg"£(ŠÃצ0..«è`cZG¾DcË4œ¶„¢Ëꛦ¸¹Q÷èу©¹¼îÙw€éÉU'“Ú>\]]™e]]Ýê½x>ùäfÁÝÝýÎ;5v¬>ÍáСC‡Zûˆ“'Ož55UÓCu".—»téÒ“)TdëÖ­uœy5Õ¯ ´´´BBBjlEuîÜ™y}«ÿ’vîܹ¤¤D½† ©ÆOm+BL£‰Úë}||˜Õ ºÞ½{kii…‡‡3ÿ'L˜P}<Œ±»»;Óê§úJ{{û¬¬,unS¦L™2eJõÌG]cƒ»0…†c.—{öìYuÎ{÷î?¾––V«]`ͨu~¿4Ö.¬nîÍõ$Å…ÏÒs"3dJ2ûey‰Dñ,3¿¤\ª£Åµ±Y„D^U\.U—m­Ì§iÍS$Ö›Foö|1=™e¦Çc=jÔ¡U?µêùTϪG—.]ÊÎÎ8pàÌ™3kd¢­­mhhxêÔ©ØØØØØØ¸¸¸¬¬,‹eaa¡¾ì.,,|£3­ÌÊÊJWW7::šy™†Áu¾ˆyyyêÞøÌ=3ú÷Û)(((000!!áÙ³gcÇŽe¾Ž9"—Ëår9S4PÆXÝ.úåË—þþþAAAóçÏo¡S®Ó¤I“ÂÂÂår93O›H$òññ¹xñâÉ“'™n¨®ODJJ s^õ  jϦO BÈ××÷Ò¥K•••666¾¾¾§OŸ.((ðôôD­X±BGG'22òùóçÛ¶m{ÿn ¬­­…Bá“'O˜‡OžŸohhÈÔ›šš2µÍÌŽ³fÍZºtivv6B(//iS:vìØ£G2Uv{÷îm®“-¡C‡ÎÎÎË—/—H$4M'%%1}æë|ÝÝÝ­¬¬6oÞ¬T*}öïßïííÍLQ^^îè設­-—Ë™éÓß3l6;00póæÍ¥¥¥?>sæ Sé““³|ùr’$BAAA?ÿüsRRRAAÁöíÛÕµâµq8œ³gÏÖ6´¹j¦€wE+\ohìYÀãñðë!Bˆ@.væ%¥å¶æFÏ2òúôó’Ëe¥ ¬T‘}zva³ÙÙ/ËH’271|µ Ñȶ| 5Z¨Þâa¾®Póö:lݺõÓO?=~ü¸¥¥¥¿¿ý‰¿üòËÅ‹/Z´hýúõ¬¾I[[{÷îÝãÇ733ëܹ33§ÆøàÁƒsæÌ!ÂÙÙ™™¼_¿~ööö666B¡ðÙ³g+W®üöÛo TTTdaa1}úô>ú(00099ÙÇÇG,¼ÑVFÄéÓ§—/_Þ±cG…Báèè¸jÕ*„P/"›Í>}úô‚ lllÌÌÌvíÚÕµk×Úy~ÿý÷Ó¦M377‰DLGÜØlö­[·6mÚ$—Ëmmm×®]„ WOpïÞ½ýYZ‚L&[´hQjj*ÇóññÙ¾};³> 888 €éî^ç'¢êüÔ „|}}% 3Ø¡···\.W|¸zõêY³f]¼xQ__¿OŸ>ÿÙµû]´yóæ¹sç¶oß^(®]»–™Î°  `÷îÝk×®åp8üñóçϨR©Æ·lÙ²zrëСMÓ¥±WëLgjhµË¼…Zç2€ÐÔ³4þyºg'‚ ƨZ»}Œ1D „éϳ‹ÚÙX*Tdjfž™ÐË݉I“——¯Ä,+‘ñ›Tú<ùÛA+UdíMŽ|Þpý:ÊhŒïQxø? _7Õ´9íÛ·«geo-Œ1MÓL‰@õ¿$IR¥R©H’$IR¥R1U*Uÿþý1ÆiiiuÎXC~~¾¡¡!Óš4BLLŒ——A Ÿ%ï ‹…1~üøqçÎëIF’dii©X,®?7CCCŒqxDxï^½E"S©ÿjjBMûP4EQÔ«¤ˆ@Ì€ƒ&‹Æaìâ`£¢‰´¬ÜŠJ™¥™‘ºh½ù(ÿÄêè¼öqìå9Ÿ†ß 'ÿIm2•ªìއ}»Šx{\¹r¥ªªÊÏϯ­à} §§—àêêÚ\³óàBÓtbb¢žž^KHc進ªª´´ìU/_ŽAcŒ0Bë t9FZZæ|¯¼¼B=!¡T*ãi¿YS5¾±ñÇgÎ÷‰9?wöÓŒtMÉÊ(*N[¯ÛŽ-} Ô”Ðú†wèС֙U€÷žƒƒCJJJTTTCZjà=chh¨§§çààÐÒÒX: £+Ì*,‘Éä4MÓ4¦1¦icüzc„XÁ"‚¥^ XA‹E´´øÚMŠÌ¤sçÏÂïÇ=üÛÆ …29BÿL‘¨Â8†Ä¦sçOšõ)»±Ã›ZH¹ÖM×®]»¶ï9¥.öVLÓÆÀápš¥‹i‡iŸ¸MšòçŠå7Nžb?HTT•öò¶a“¡…EÓó€Æx‚ šë¿‰§ÿÖí¾ë6(²³•,ÖSAË÷¸>móß@-n{ǶŽxÁ˜aÀ‡J€”ü?{÷Åñ6|æöå¨Â½IE"bb‰=¶5Q£ÑDc4ÖhŒ1jÔ$¶D£þ4¢AcPƒšhŒ%bEÀ* £×+Üí¹ðÒDjÐçûIpoovöÙeÏͳ3³ÀëŽ/‹[;­†²,˲ Ãp?9E1 Ããñ(Š¢(ŠÇãÑ41ƺ§Ë€Wô^w^w^wüÖ ¾Ø²²²¸˜Ü¬,±G›xÕÚ¯ˆZ³Ér„0!„ çÿ‚Ð??¸WaŒ7G•Z£¡x<=1Ͱ !ÄÚÂÌÜĨñ!­¶àðÁœ­›xZ-B(ƒaÎw]³¶Û¨Ñ¯µf´4c&1 –fX†ai†¥Y†©ô’BQ<ŠÇ£(̧x¹yÅ•¥- x<š¡|¾‘~ã³¥çÏf®[ òuC L(*@]žöÑü½?ì¶y«µ‡G#w¼æjÍäæÈ ²iD×7c„þYÂa„ycŒŒXŒ-%|Œ1ÖaŒ1©TªFFV‘”˜¶êS:úNïÚ vIo ¤ðÆè/7JÚY4rwÀk«ÖìEQ>Þ^„0Ba•†6 y<žne%˜[)—ç†ÆÄfoþ²ôטÔUFÈS$¬¸v)ܯ‡ã¢û¿7žÂ 4@­ÙŒ°žž¸òš¬‚š%">EžBC#„ÅBD_TãæEÑ H›—›JÙ´!¡ƒ—gßÀíx­Õ뉆„µF[¦P ùÇ#,+ „¾R¥Öh™¦ (j׎=‰ÉÕš—êzà(È'4U ‰‰‰&&&¬dæÌ™_~ùe“ÄÓ2–.]ºxñâÖŽ¢m{=¯œ°wïÞÑ£; )œØÆxã7âââZ; ÐŒê•È.(MÌ. * ý45ûv죬¼bŸÂ}ú´L&Ó½õæ›oš››s˕׀WÆ‹³ê ’á±,ÑhisÁ° !>>>ãǼ™““£Pkh‚åEŠ6Ã5hŽ@–ù7×@ŠWWüXP|U¡ÔÔ9CáË:tèPPP·ìáá1vìXnÙÑÑñÎçOIØ»w¯µµµ¥¥å×_Í­)//÷Ýwe2™­­í§Ÿ~JÓ4WlÈ!o¿ý¶···§§ç¥K—¸ÂºnÌóçÏ×uŠ^¾|y¿~ýØÿŸï¨;…B1qâÄvíÚ™™™uëÖ­¬¬¬Ê±ôèÑcÒ¤IÕ“Ïç‹Åb±X,Õ<=DBBB¯^½ŒŒŒ† VTT„Òjµæææ111\ÂÂB==½ììl®Ïü®]»¤R©……ÅáÇ/_¾ìééibb²dÉ’žðW\9Õ­]»ÖÚÚÚÄÄÄÍÍ-**êÞ½{º»Ó;väÜÝÝMLL¬­­wîÜY¥’3flܸ!”““ƒ1þæ›oB©©©ÆÆÆ à „†™7ož‘‘Qûöíu窙NlÛÂ0ÌO?ý´zõj ÿÐÐÐT)sðàÁwß}×ÛÛ›;QÕ pBBBŽ9Â-;vl„ º·`dÀ+¯Ö쀪âù—{šfJJ­–fÖÉÁ{.ŸÏ··³)(Q*ÕšŒœ|R­éŽúü€ê[ÑÝV¨–1Mö >00ðï¿ÿ¦i:77W©TFEEBÒÒÒ }||Beee±±±‰‰‰çÎ[±bųgÏBK–,yöìY||ü7NŸ>½}ûv®¶‹/¾ûî»qqq›7ož1cF•}}õÕW?Þ»woTTÔîÝ»<ÈãñêÌÞ½{U*UFFF~~þž={„Baýó³Ï>³³³0`ÀÅ‹«¿Ë²ìرcGŒQXXøÑGýïÿC ‚qãÆ………qeÂÃÃýýý­¬¬¸sòôéÓÔÔÔüqîܹ[·n½|ùrLL̾}ût ãW\9UÄÆÆîÝ»7&&¦¸¸ø÷ß·µµõññ±²²:wîWàðáÃS¦LAMŸ>ýÀÅÅÅ=êÛ·oõËÝñ¾|ù²³³óåË—¹å>}úP…:þ|Ïž= -Z¤;WÍtbÛ–´´´²²².]ºp/»téòðáÃ*e>|X¹@jjjyyyõªÆŽ{öìY•JÅ%«ÿšÀ+¬ÖïÄÜÍ4‚¾ØT„Jj›vF”PD!„0 Ãé3„—zÙ[ÔP/nh~ –>e {W©ªc»—Ú½½½¹¹yttô•+W äääôðáÃÊMBÈ—_~©¯¯ßµkW__ߨØX„Б#GÖ¯_ojjjkk»|ùòC‡qµùùùõîÝ!4tèÐÔÔÔâââÊûÒÓÓ;|øð’%KBCC·mÛVýVmÝÁ‚ÜÜÜ„„Œ±¯¯o}îårÞ{ï½_ýõ?þ 6l˜®;€NLLLvvöÒ¥Kù|þ AƒtC²CCC=Êe|Ž9Ê­'„¬[·N,9’ÏçÏž=ÛÒÒÒÑÑ100°zå¯*¸rªàóù*•*66V£Ñ8::ÚÙÙ!„¦M›vøða„PIIIddä¤I“¸’±±±ÅÅÅ&&&\o‚ʸLÃ0W®\Y¼x1—é¸|ùr`àó‘téÒeÊ”)`æÌ™)))ܹj¦Û¶pí|###±qõ~"ååå• è¶ªÂÄÄ$ 22òرccÆŒá.éêärySþ;jÍ`Œ !\['Ô·2—dä·31*)W—«2 J5 ÉÈ)ˆDI¹eù% –e ":7l`ªÞ AGѤ½¹{•W®\ ìׯßåË—+7EŒŒŒ ¸eƒòòr…BQRR¢kH8::fffr˺á¸\“¬ú×n___777F£kiOž<™›õmÅŠu3cÆŒAƒ?ÞÚÚzÙ²e ÃìÝ»—Û¶OŸ>u`PP¯¯¯‡‡ÇÇ;JLL\ºté¡C‡:´uëÖ»wï¾T0—.]zòä !ÄÜÜ\( ‚*Û²,«V«µZ-!D­Vk4„J¥:|øpVVVAAÁþýûO:5bĈ*véÒÅÔÔôçŸF%''Ÿ>}Z÷VhhhxxøáÇ_;«M®œÊâãã¹¹$‰D"ÑíeêÔ©_ýõÝ»w¹¾'J¥òøñãJ¥R$™™™U†;–;v „úõë÷í·ßúûûëú¶Ô¨ùNlBQThhèºuë oß¾ööÛo#„222/^ÌÍÔ8uêÔï¿ÿ>>>^.—oܸ‘+P#@ð믿îÛ·¯¥Âÿµ~ó …„ïÿcäáhUXTâ`eö$5»o?_•JY¤&-Ý·WWŠ¢2òŠiš±jgú|ÜÀ¾¢ÚŸ¯^Gu4"b‰ÑKíÈÙÙÙÔÔ”ëbmhhèêêZ¹mS£M›6-\¸ÐÝÝÏçOž|ø ñuWNe …bþüù‰‰‰"‘¨oß¾Û¶mãÖ5jöìÙ£Fâz¹BvíÚ5kÖ,Œ±‡‡GÏÀÀÀ²²2n&<•JõÂYñšïĶ-›7ož5k–‘‘Ñúõëûõ뇒Ëå[¶lùüóÏù|þĉŸ#0ïÀ+ ×6Î?úÁã.\1ƈ›E R¿}BFaô(1åiF¾‹½ZK'§e;ZJ|;ºqe²³å„/r°¶|Ù€ Ÿ$¬ëÛ§\KWËU$i\Cî€%äo†Œ¿tÍÌÖöewh&îîîÛ·o:thk^€²,˲ Ãp?94M3 £Õjiš¦iš[ÐjµZ­ÖßߟòìÙ3gggîa¨u“Ë妦¦u÷AÓtQQÑ §ˆ255%„\ºîßÛ_*•r7õ¹ŸµŽ,`t_Y–°,!„%Ïÿų„°,ëálïhmñ,=ëqbŠ¥‰.5€^~37 úð«õ¨±ï@šF{«ƒ÷Ø —!5ÀGDDDEE…îA€ÿ¸Zïäh**ŠŠŠŸøÿc0Æ,!ˆ „‘ÌXÏo&[‰„Â’’RÝ  ¥q{êŠÛµ›ôëéÁÑÑGg͸Ÿò¬¶bÅ sßÀ¸÷·[† ܰšÃ Aƒbcc<ÈãÕšü§Ôšgä)•*–eY–ë8ÀBþY&!Æ<Œ1O·€ycŒy<¬'×:@ýXvíúÁÝ{±{¿ÿù³Õ¹JBÿ>"QKH M¬>øhÆœ¹Tã½-ïÂ… ­àåÔšèäæÄ8­mb‚ÔËç7ÉÓÎ3ßõ~û?–,úíÐanòƒxuEIŸÀÑ_n2«÷ÓËP‡Zðã¦já7æóßøzû€/¾T¥§kx<' K=£—{<êÐúÿzâ‹õ$®n/.€—s†¯;ȯ;ȯ;¾Z­ní´B˲,Ë: ~ë IDAT2 Sù'MÓÜ“khš¦iZ«Õr/µZmk‡ šô^w^w^wüÖ ¾]J—Ü))S†Md^˜GµvDÀ+¢Öì@š<!L!èùÿ„ ôÏîFc„1Æ©5ŠÇ3ÐÓ [¡ÑBdæ&¦F†M#«¡å{5©ëxXcˆPi*ŠŠlgÕm£S—ÑMP9ðÚ«5;Àdnd@3,Ͱ ÃÒ K3 ð Ã2 ¡–BQ<ŠÇ£(ħx…Eå•[PBñx4C ø|‰¾^ã³lá)UâŠÍãáçkŒ$¨kÇüì”éWcvtò©•g#w¼æjÍäæÈ å "„ë€1Bÿ,aŒ0Â<Œ1ƒ Æ F,Æ–>Æë‹0Æ‹T*U##cUOTORª›5!°’ñdÒèÔËþ d¤ÏÈÍbC‹FîxmÕš (ʧcG„B!„0÷ÒÒŒJCŠ…<O·²Ì­”Ës4¤¡AÑÏÖ°aª« Œ‘£#¶ÑžŽ;ö‡Äu¹gÀ\„«…€©5;€ÖÓW^“UPB³DħhÂShh„°X(è‹jÜœ¢(D7$=@´9ê˜ÌÔ³¡/`o¯Š¬¬YO½¬Ýú5`Àk®^O4$„¨5Ú2…Zȧx<aY‘P ð•*µFË4m@IQßDœ”§¦‘:û TemÍ+ÉMhÚHZØœ9s6oÞÜÚQ€KOOïÓ§L&ûî»ï^XX­Vëéé•——·@`mÝÅ‹===e2Ytt´——×­[·šo_™™™&&&Ü2|ô@õÌd”&f …•†~šš};öQV^±€Oa¥PW”)+š0 –Q)”èÚuúÜ:/ÿ%2,Ý”aP›]»võêÕK.—Ï›7¯UX¼x±›››‰‰‰‡‡ÇÖ­[¹•)))“'Ovtt´´´ zøða«ÄÖ«W¯Þ¸q£\.ïÚµëŠ+[;¢6#11qøðá?ÿü3·òÒ¥KAAA2™ÌÖÖvæÌ™ÅÅźò6l°³³³´´œ?¾V«­RÛ¦M›d2™ƒƒƒ³³³‡‡GQQBH©T~öÙgÇ9rd‹h1õÊÐ*š¦ Â1q‹2ÍÅ1·®?NJÕ 2r 1©¡û€ºB«ÑÒ H7   €ü~‘¾~ƒQ*PMs¡é†x•¤§§»»»7GÍõ¼ºÆŽûûï¿§¤¤:tèÛo¿=wîB(''§K—.çÏŸ‹‹kß¾ý˜1ciðÌ­£ò‰ ±´´¬ü.!„aš¸§Ò«a˜ &x{{§¦¦îÛ·oÞ¼y±±±¡´´´)S¦Ü¾}ûâÅ‹‰‰‰‹-âÊ?~|÷îÝ‘‘‘÷îÝ»qãÆÆ«TøÑG¥¤¤$%%=~üxá…ݺu355EíÝ»÷éÓ§8räH #h/Ψ+4J†Ç²D£¥Í †øøøŒófNNŽB­¡ –)jØ £†ÍȲ•=KaOý¦‰eim7u\\\Ö¯_èëë;wîÜŠŠç]Ž;ÖµkW™L6lذgÏž!„’’’d2Ù–-[ºuë6sæL…B1eÊ++«Þ½{—••!„’““GŒ!“É:ww7}úô>úhøðá]»v0`@FF·~ýúõîîî={öü믿šö¸@³š={vddäêÕ«ÝÝÝãããóòò¦L™bggçææ¶mÛ6® !äóÏ?···wuu Óm[ãE²ÿþ‘#GΜ9³sçÎßÿý©S§ºwïnaaáîî¾cÇŽcèÝ»·£££‰‰‰³³³¡¡aRRB¨gÏž‹/vww—Éd«V­JOO—ËåÍ|2žûâ‹/œœœd2Y§NnÞ¼kkk«»#}úôi___nÁÛÛ[&“999íÙ³§J%]»vÍÏÏ êÞ½;BH7²€ûuèÐáÁƒZ­võêÕnnnvvvsæÌQ(¡Ù³goÙ²!”››«§§Ç·´´4©TÊ0ÌãÇ $“ÉìííçÍ›§V«[æ´´˜¤¤¤„„„+Vèëëûûû:ôСC¡iÓ¦…††ÚÙÙ¹»»ÏŸ?_7RãðáÃ3fÌèÔ©“Í’%K¸Â•Q%þÇ©S§ÆŒíÏÌÌ °°°H$-y€ eÔšPU<ÿrOÓL©B©ÕÒ Ã:98pÏ6äóùöv6%J¥Z“‘“_ý.åóg 6†Aã™Ógé²:Çn7 ypãÆ .ܺu+77wÓ¦M¡K—.-]ºôÇÌÌÌ6lXhh(wh%%%J¥òÎ;û÷ïß¿¿Z­NJJÊÌÌܱc‡P(dYö­·ÞòõõMKKÛ½{÷¼yó¢££¹]DFFîß¿?::ºgÏžëÖ­ãVº¹¹]¹rE.—¿ÿþû“'OVþ§zG€:íÞ½û7Þøì³Ï?~ìéé"“ÉžŸíìì¼dÉ]2…síÚµéÓ§?~ÜÍÍ[chh¨»ù_RR‚Òýq«L­VŸ9sfܸqº5kÖ¬áñxgΜ‰ŒŒlÆã­„_Û˜÷oãÞÊÜ(æA‚¼Œ.)W9Ù´+(Q0%gæ*U{+3;©ÅÃeª -͘IôŸoŽPÃú°-8ñXJJJ·nÝBÏž=ãZ ¶¶¶!!!ï¿ÿ~åbIII•E$­^½zõêÕOŸ>=z´‹‹Kvv¶V«åÚ~©©©•¿Wßé‚ þøãooo„P§NÚÜìq€ckkkhhxïÞ½*wé­­­¹¤Bˆk-s+k»H*_]“&MZ¹reHH×¥…»6¸ñ,5Æ@áfÇ@åååMš4éƒ>hÊã|‘É“'Ož<¹°°pöìÙ_|ñŶmÛ¤RiŸ>}~ýõ×#GŽL:•+Ö³gψˆˆŠŠŠmÛ¶Íœ93::š›1á…tçG__ßÔÔ4,,¬S§NUÊDDD”——ÛÛÛ;v,''ÇÇÇ!´|ùrƒÛ·oëëë;v¬¶ÙÚ4]‹}äÈ‘\W„Ð7BBBþ÷¿ÿõîÝ[W¸C‡÷ïß5jBèþýûööö†††\g±ÊÎ;ghhèçç§›,311ñÃ?411©þ˜𠨵ï€Hðoâ@O,òóíìlm®­PýþwÌ“´œ;1Y†óI;cÉŸ—®œûãÏ2¥F©Têú`ôòýëèE³6 …½mÛ¶¼¼¼¢¢¢7Ž?!4kÖ¬­[·Þ½{—RZZzòäÉê_š/_¾üôéSBˆ¹¹¹P(;v´µµÝ°aƒF£¹yóæÏ?ÿ\ÛNKKKÅb±««+Bèüù󉉉/8øOðòòrww_ºtiYY˲ ܘùñãÇ8p@¥R!„¾ûî;®p=/’ÒÒÒŽ;R•žž~êÔ©ê Åwß}÷ìÙ³¢¢¢S§N:t¨ÿþ¡Â   ¡C‡.X°@­V«Õê–É:%$$DEEÑ4mhhhhh¨ë1iÒ¤o¾ùæÞ½{£GF)•Ê“'O*•J‘Hdjjª+ö²fÍšµdÉn‚ÏìììóçÏsëöìÙãïïêÛ·ï®]»üüüø|>B¨¤¤ÄÕÕU___¥Rýøã?äÿ Û·ogggçää|õÕWqqqï¾û.BèîÝ»cÇŽýî»ïüüüÔjµnâÕI“&íÛ·/!!!''góæÍS¦LáÖ¯]»öéÓ§º:þùçqãÆUN]uìØñ·ß~+++ÓU^%µ6Æ…Báÿk]`äáhåfÛ.°««¡€ÕÏ·‡«ÔÕÁZ£¥ûöêÚׯ{F^qZ^9˲•F4$?ÀÕöVÕ1 ˆjݰ6'N0`@‡:tè°dÉ„ÐÀ7nÜ8wî\©TêããsúôéêG‘šš:zôh©TÚ­[·áÇ1‚¢¨cǎݼyÓÞÞ~Ö¬YÛ¶mãº$ÔÈÛÛ{òäɽzõzóÍ7/_¾Ìõ mÆøØ±c:u²¶¶ž5k÷Xøààà!C†ôéÓgøðá\áz^$ß~ûíÛo¿´råÊ!C†T/@QÔ… üýýW­ZµfÍšI“&!„Ο?ÿðáÃmÛ¶™þãÞ½{ÍyôÏ)•Ê?üÐÚÚÚÙÙY©T.[¶Œ[?bĈŒŒŒ#FpCÜ !ßÿ½³³³••Õ‘#GvïÞݰݭX±¢OŸ>ƒ¶°°:th||<·>  ¬¬Œ›ìÐÏÏO¥Ré&>\µjUXXØ€BCCûöíÛØþOúý÷ß»víêááqéÒ¥³gÏrç|ïÞ½EEE'Nä®]_• &Ìš5kàÀ:uêÖ­Û'Ÿ|­ߵkWjj*·œ}íÚ5.gªóñÇóùüàààÊà À+sw8«{ø4ÅÇË cŒ¸¶~¥{ó„Œ0ÂèQbÊÓŒ|{µ–NNËv´”øv|>´5;[®!<[©ùˤ,ŒØÜ[©¬a|½ð¯a(Ë¢˜Bï · Mmë¿#—“'OBã€fâíí½yóæ7Þx£µ/@aY–eY†a*ÿ¤iša­VKÓ4MÓZ­–{©Õjû÷ïÏjqvvæ’bu“Ë妦¦\oÐhš.**’Édu355%„\ºîßÛ_*•r·Ã¹Ÿµö`Xæ9–%,KaÉó1Â,!,Ëz8Û;Z[¿I†˜Z{ÏÓqVÂ…Ñþ„1"¥¤²ôŸÑ[ Lj}j ê¯Ö<Ƹ©Zø„y|Ï7v¸جU¤24Ï¥‹ÔSϸµƒ^­ßø¯'_OdìÑÚQ¯ ˜3 xÝAv@³kØ6 žÿu ²šÃ0­À«¯‘_ºøb±¸©BÐæBX–eY–aî'‡¢(†ax<EQEñx<š¦1Æã—ÝMÓ*•ŠÒ°Í@!„•JEÓtcêi3³h£,,,òóó•J%EQ­ À+ˆ»»caaјJ ; yéëëÛÛÛ·v .0ïðºk3}”´2±è©<'ÇAâàjçÊãA^hµf’3äaBAÏÿ'äùC¸9BaŒ7É”Z£¡x<=1Ͱ !ÄÚÂÌÜĨñ!Ò,ý{úù£OÃL#„4O5ègô^ÀœA½7¾rÔšÐÒŒ™Ä€fXša†¥–f¦ÒKBEñ(¢0Ÿâåæ T–¶€âñh†ðùFúÏÜʽ¹ÿá¾R¶ý3˵ÐHˆº¢ïž|{üFøÇã?q±uiä.€×\­ÙÜyA6áú`ŒÐ?K#Œ0cÌ`‚1ƒ‹±¥„1Æú"Œ1Æ"•JÕÈÈ2™?<Øó´üIïØè—Ú”,üíoì½hâÇfFfÜðÚª5;@Q”·Bqwíñó›÷ZšQihC±ÇãéVV‚¹•ryihL¥šÒ°'?]ɹ\w1Œ°Q{I²&iÚSB¼&… Çh Pkv#¬§'®¼&« „f‰ˆOÑ„§ÐÐa±P ÑÕ¸9EQtƒ*ª(ZvcI]VÏòxð Ubk)))º4@µ\¡¯¾úÊÁÁÁÀÀ`ìØ±………ÜÊtèÐA___*•Θ1C¡PT¯ðìٳݺuspp ‘ËåÜÊ[·n͘1£_¿~o½õV ”ÎÅ‹û÷ïß’{x=Õ+;@W¨hš&ÇÄ=,ÊHt4Çܺþ8)UO,ÈÈ)Ĥ†îê ­FÛ ±ÿL ÊS§žIËú+[«Ð6¤ž–’––æééÉ-Ož«R[FFÆ;ï¼³|ùò¸¸8©Tº`Án½žžÞ¸qãfϞ݀®]»¦{¹k×®#F4äPk2wîÜútäu{qv@]¡Q2<–%-mn 4ÄÇÇgü˜7srrj M°¼¨†[O#Ô 9ÿßmd‚JKŸ…?Ë»ÏjšøöòÚµk­­­MLLÜÜÜ¢¢¢îÝ»gaa¡Õ>ÏDDDDtìØ‘[pww711±¶¶Þ¹sg•J¼¼¼òòòèííÒ,˜j+MÙ3fœ>}zùò厎Ž=ÊÍÍ8q¢¥¥¥ƒƒÃæÍ›¹2„5kÖH¥R{{ûÇë¶MJJzã7LMM=<<¸•{÷î2dÈ´iÓ<<J‰‰‰&&&7nôõõuqqùúë¯uo9rÄËËËÄÄdРAÉÉÉ¡1cÆTTT8::r¿…–9Ìÿ²/‰S§NM™2¥S§NFFF«W¯ŽˆˆÈÏÏGuêÔ©C‡fffR©”+\ÙñãÇ}||ÆŒcll¼lÙ²[·neddp6ÌÊʪå´€Z³ªŠç_îiš)U(µZšaX'î¹|>ßÞΦ D©Tk2rò«7ípÓ=?€¥IAlAò‰mI“u"ˆÝ»woLLLqqñï¿ÿnkkëããceeuîÜ9®ÀáǧL™‚š>}úŠ‹‹=zÔ·oß*õ<|øÐÔÔôâÅ‹qqqUÞ:yòäÁƒ“““»téòÙgŸýý÷ß7oÞLII)++[±bB(00ð¯¿þB]¾|ÙÙÙùòåËÜrŸ>}(Š"„|öÙg¹¹¹111ÑÑѵ5Û–²Š²Ûy1×óîDçßPøäaá“{ùnçÇ<+Ii+Ù}ûö­_¿>%%¥C‡ãÇ·²²JMM½víÚ?þ:zôèÁƒoÞ¼æÌnC–eGݽ{÷œœœ}ûö½÷Þ{wîÜáÞºpá´iÓæÍ›'‘HŽ=ZRRrüøñÕ«Wß¼y³Æ0V­Zeii)“ÉX– ®ònTT”………L&k¶Óð¯¦ú(!„JJJBwïÞ½víÚÚµkŸ={†ºxñââÅ‹:TPP0bĈ &BNž<)‰RRR¸ßB æ_õK¢Ê€–e>|È-Ÿ;wN&“>}úƒ>¨RU|||§N¸e©TjnnþôéÓf ;99ÙÁÁaãÆƒ êÙ³çîÝ»¹õeeeï¼óŽ££cïÞ½+ÿuݸq£···­­m@@À•+WBáááÛ¶móööæûäåå͘1ÃÕÕµS§Nß~û-·addd÷îÝ<<<öíÛ×L‡Ц՚à¾Y„ ôŦ"T¢PÛ´3¢„"B×[ž/Òg)(.õ²·¨¡^ÜÀü©e6BZ¡-¸_Pdž/µ3>Ÿ¯R©bcc5£££BhÚ´iÜÞ’’’ÈÈÈI“&q%ccc‹‹‹MLL¸[ õjkk‹ÂïÙ³gÆ 2™L__Íš5ááá¡ÀÀÀ¿ÿþ›a˜+W®,^¼8**ŠrùòåÀÀ@„§§gÿþý••Õ‚ ®^½ú2Ç÷e¢gªV1×r®_ÿ™uö·¬³È/ޔߵËx¼z rùOyøðattô¦M›ôôôìììæÏŸÏýfÃÂÂæÍ›çèèh``°zõj®plllZ x IDATZÚš5k„B¡¿¿hh¨®[¯¯ï€BãAƒuèÐÇãuëÖmܸq•;cWöñÇß½{÷§Ÿ~9r¤‘‘Qå·ÒÓÓßÿýíÛ··Ì)mÂEQ~ø!BÈÊʪsçα±±¡Ý»wÏŸ?¿k×®E-\¸0333))©Ž«Í©~IŒ1â§Ÿ~ŠW(Ÿþ9BH©Tr…û÷ïïÞ½óçÏOš4ÉÁÁ¡JU …B"‘è^J$’ç&h*¥¥¥ã .DFFîܹ“»æW­Z¥P(=ztôèÑÊp\]]/\¸šš:{öìwÞyG¥RM˜0aÔ¨Q .Œ‹‹ãÆL›6M*•Þ¿ÿܹs‡æÆY¼ÿþû;wîLMM½yófïÞ½›ïpÚ®ZÛcB"!Äê[™K2r‹Û™•”«ŠËU™¥†dä D¢¤Ü²ü˲„6°!RëZU×þ—ºõìååµyóæ+VXXX„††æää „&MšYZZÞ«W/®maggׯ_?îf¯­­-Ƙû:[Ç.týo•Je~~þÔ©S=<<<<úè£æ=ø4áGÉÐÐP(rÕêééqzHMMýî»ï<þÒM’*«~ILžS¶¬¬ÌÀÀ YƒŸ3gBÈÜÜ<88øÄ‰¡'N,[¶L__ßÞÞ~ÆŒº’cÇŽµ´´¤(jÒ¤IFFF?®RUBBBllìÚµkÅb±Í{ï½÷믿"„ø|þƒJJJŒ›iÞ€¶®öìï߯½•¹QYAî“”ÌÛñ©J ‘WRX¦~’&/,QˆvR3ЇËT% ]v#Ô°¾4ÛB=̧M›vóæÍääd¥R¹víZ„L&ëÛ·ï‰':Äõ…Fùùù={6??ðàÁÓ¦MCeddpÇ8hР:ê×¾¾¾¾™™Ù‰'?~œ››ËÝ× 100ˆ‹‹KKKÛºuk[éx_7çoßg¡ñÇ=´½mÔvíÕn©ÉsæKŒ[;´†°³³“H$=â~³ÉÉɧNBY[[sd„nÁÚÚ:++K7?%%ÅÆÆ†[®üIyë­·BCC“’’RSSƒƒƒ¹ßûáǹKŽ»\!„ŠÊÍÍ8pàÔ©S?úè£æ:æš4ëGÉÎÎî“O>Iø‡\.ïÓ§O[ìiÒbt—ÆxùòåÉÉÉr¹¼ÿþb±¸}ûöµ®ÌÓÓS7!77·°°ÐÕÕµ‘Q ÝÅÒjµºL™H$ÒuUJ¥ÙÙÙ …¢¬¬LשÁÑÑQ·axxøÀ½¼¼¼½½333 ªv(KOO§iºwïÞ=zôèÑ£Ç7ß|Ãe:Ž9òÛo¿yyy1âÞ½{<€WR­_²E¾nYO,òóíìlm®­PýþwÌ“´œ;1Y†óI;cÉŸ—®œûãÏ2¥F©Tþ¿ì@Ã"ª}3ÜÐ*«‹ÿûï¿iš–H$‰D÷=uêÔ©_ýõÝ»wLJR*•ÇW*•"‘ÈÌÌLWìeÍž=ûÃ?LOOGeee={–[¸cÇŽ€€„P¿~ý¾ýö[>Ÿ*))qssÓ××W©T?üðCãù¿€Çã™™™ ê8x¹çªMNÛ¾òØ:£ó¬öŽ®º;Æm ×k`ñâÅeee,ËÆÇÇsÓïÛ·O¥R!„tS©{{{ÛÙÙ­[·N£ÑDEE…………††V¯³´´´S§NE¥¥¥q÷<«P(Û·oONN.,,xð`ÝœyEþøc®|JJʰaäRé„  °}ûvný·ß~›’’‚²³³Û·oߪU«<==322t®»wïöìÙsÙ²eYYY/䨱cwîÜùàÁ•JuéÒ¥ððð1cÆpoñx¼õë×WTTxðà~ø!//oåÊ•—.]R«Õ®®®Ë–-óóó ‰Å»ºº~õÕW]ºtiî³ ðßdjjJ¹uÝ¿·¿T*ånê?ÿY[vàvÜ£.ž®Ï !Œ0"!‚0&ñXB¸|ÁýÄô´ìüÒr¥¥Y@WÝæYÙÙˆ/n@v!T¤,ZuhEÔÕ(Âü¿Ø$N›ÖU +²”vÅvKƒ?µ±´iÀ¾ªˆˆˆX°`Arrr½› À|”ÚÈÔ†ËTtšUÙ~mÛh**ŠŠŠ¹ÁUÆ`ŒYBA#™±ž!ßL,¶ …%%¥=O6(JcqÃÕ7ýö½ #âWî_žüðY­–jP"úxð'þ]ü¶£* {ðàAhÏÐðQ Í©5; 12ÎÈ+R*U,˲,a áæøg™„xó0Æ<ÝæaŒ1æñ°žøe¦¨‰‡gøŠ_~¹y|ûáíåùå¨ÒcX-[_>Áí­IMæSµÂ˪û …€z‚mN­MëNnN\ŸÒ†Í|Îçó¹¹÷ilÏñ£ºÙrjÓ‰S'F‘Ò§eŒçÇS?–šË_?€Öâìì à þ#jmÀcŒ›ª…ßHZ2zé‚áæ”ç`™ú™ê7²_þÕúÿz Dö¦ö­ð j3Ùm”R©ÌÏϧ(Š¢¨ÖŽàÄ=sª]»vúúú ®²šW^^žD"ÑÓÓÃWyh$B!D­Vçåå9884¸Èh^|>____ ´v ¯,Œ±R©lL |µZÝTÑhs!,˲,Ë0LåŸ4MsO®¡iš¦i­V˽Ôjµ/» PÐù¥‹×Tqµͪñ_· ;¼î ;¼îÚ̬„Š 6!«"3³ÀÅBàájIñ “*Ð4jͤÉó„‚žÿOBÿüà^a„1FÜªÔ Åãè‰i†­Ðh!2sS#ÃÆ‡H3äL¬âàµ2š`„øê’bí³¿Mt9Ä«ñ• ÖìK¹‘Ͱ4Ã2 K3,Í0 Ã2 Ë0„fXBEñ(¢Ÿâ• TnA ÅãÑ -àó%úzÏü¨ÞùGa©†‡ÐóÎbccq—Þ›/f<óËç úx´·lä.€×\­ÙÜy¡œA„p}0FèŸ%ŒF˜‡1f0Á˜ÁˆÅØRÂÇc}Æc‘J¥jddé…ô7ç òH“#ÙØª‰Í;Ûw6¾¹nÑ€vfÜðÚª5;@Q”OÇŽ‘ç7íñó{÷ZšQihC±ÇãéVV‚¹•ryކ40¦Rûã•’‹ñjTç#0Ææ®®©͈EÎ$Úž˜4@­ÙŒ°žž¸òš¬‚š%">EžBC#„ÅBD_TãæE!º!éB3ÿp~©šÔøwGB¡E—î‡î¥¹»<ëÛ˹{^sõz¢!!D­Ñ–)ÔB>ÅãñËŠ„¡€¯T©5Z¦iÚûsì•ÿW˜”ˆÈK$ŒíìŸ&4m$/äââ×Â;­.==½OŸ>2™ì»ï¾{aaµZ­§§W^^Þµu/^ôôô”ÉdÑÑÑ^^^·nÝj¾}effš˜˜pËsæÌÙ¼ysóíëÕ0bĈ´v Õ뉆Ù¥…Š ‘@¥¡³s rró<]ìeæuêTnn®L&{ï½÷>üðC„PJJÊŠ+®]»¦T*}}}7oÞìåÕÆê±zõê7¾ù曡+V8::¶vDmFyyùûï¿æÌ##£O>ùdöìÙÕËlذaçÎÁÁÁ_ýµ@ ¨ünqq±­­­Ý½{÷¸5 …ÂÇLJ­™3g޵µ5BèêÕ«Ë–-kòCHLL¬OÍÐа}ûöM¾w¨ü¼&ê• +T4Í‘ 6î¡P[îh%¹u]ååÝÞÉ.%3ÏÉʬú&ê ­†mH@º© yyñ§"Úµoo×ÃO`ðßšt¦i>¿^§¼zÒÓÓûöíÛ5×óº;vì¼yóLLLž>}ìéé9tèМœœ.]º¬\¹ÒØØxýúõcÆŒyüøqÛš‰#==ÝÝÝ[ ©ò.!„eYŠ¢Z<®6àÓO?ÍÊÊzüøñÓ§OGÕ¡C‡*—èñãÇwïÞiff6zôè7®X±¢z="‘(**ªgÏž¡ÈÈH©T*—˹·†®Õj†AI¥Òœœœ¦=„òòònݺ½°Ø;wšv¿Pð7 ðšxñÈu…FÉðX–h´´¹`XПñcÞÌÉÉQ¨54Áò"E ›aTωª`˜JIBòŸ>=v$ýæ V«}Ѧ/1áÈ‘#£Fâ–;wîÌ-»»»GGG#„.^¼Ø³gO©TÚ¯_¿ØØXî]—Í›7ûùùùøøT®-::ÚÕÕõüùóõ´Q³gÏŽŒŒ\½zµ»»{|||^^Þ”)SìììÜÜܶmÛÆ•!„|þùçööö®®®aaaºm“““GŒ!“É:wîüóÏ?s+÷ïß?räÈ™3gvîÜùûï¿?uêT÷îÝ-,,ÜÝÝwìØQc ½{÷vtt411qvv644LJJBõìÙsñâÅîîî2™lÕªUéééºv]sûâ‹/œœœd2Y§NnÞ¼kkk«ýç{úôi___nÁÛÛ[&“999íÙ³§J%]»vÍÏÏ êÞ½;BH7²`úôéý{çÅÎ5ð°»ô.eAš¢4Q°Š(RT¼X{GA±bAåÚë± Uª(TЂР*`.,Ò¤H]`˼òÞyön“«Kþö—ÍdNN23'™ä$³~½Í°aÃÞ¼yC£Ñ´µµÕÔÔ¼½½ÛÚÚ^^^GŽÔÖÖŠŠŠÂz+//'“É ãÝ»wS§NURRRWW_¹reGGGßTKŸÁ`0ÂÃÃýüüäååÍÌÌ\\\®^½Ê–æÚµkžžž***›7oæLqvv‡áèèè¹sçâ‡ú`e4ÊÊʘL& ÿ'^^^gÏžýf}JKK55¿²—͉'tuuõôô¾9ˆ••ÕÝ»w¿SHï±{÷î;vô·D?ó;Û¨¯b``ÐÇ«ÏúÀ.}ç‚|Õ¼?zôÈÄÄdÈ!yyyÝ[YY úˆ‡çèµóÿ;÷t:£¹­F£3ÌÁp6’D"©«©Ô7µ·wtUÔÔa{ôܬ%“N¯z•›ÞÙÔÔC"¹¹ùÓ§OétúçÏŸÛÛÛ³³³1 £P(††† ÅÅÅeÇŽ®®®³gφ/!€´´´G±n7žžîääbeeÕSê!~XΞ=;}úô]»v½{÷NOOoÞ¼yJJJïß¿OMM MLLDEE]»v-===//ïöíÛðD&“éììlbbR^^~öìÙ•+WÂq(@jjª»»û«W¯¼½½%$$®\¹RSSsãÆ={ö<þœ«»wïVSSÓÐÐ`2™NNNlG³²²äååÉdr¯UÃÿÈÏÏ yöìYuuuRR’ŠŠŠ¡¡¡²²2ÞÞ¸qcþüù€eË–]¸p¡ºº:77w„ lr^¾|)##sûömÎ"ÇÇÇ_¼x±°°pĈûöí{úôizzú»wïZZZvîÜ 077OKK¤§§<8==†ÇG$1 Û¾};…ByöìYnnîéÓ§{¹Jú …ÒÒÒbhhÿŽ1¢  €-MAAk‚òòr®.²³gÏNIIéè訪ª*..677ïUÍ»ƒÁpuu6lXgg'ŸdÉÉÉÖÖÖªªª:::®®®ÙÙÙ} [KKËÁƒ322 û »¯rçÎQ£F©¨¨8::VUUõeÖÆÆÆhÊñ{‚lÔËÏb—öíÛ·{÷îââb##£o“°råÊcÇŽõ¬Vˆßž£L&€ .&"+ šÚ:T䥈B†aÆ`0HÂb «ÿÒ<\]‹\oÀ0îguµµV¾â7¨Æå늼QSS0`@^^^FFÆäÉ“ TPP€¿NÄÅÅM˜0aæÌ™‚‚‚^^^ÒÒÒ©©©ðDaaa¼h·oß^¼xqTTçÛâ—§   //oÿþý¢¢¢ªªªÞÞÞ7oÞDFFz{{khhˆ‹‹ûùùÁį_¿¦P(þþþBBBp‚w+066ž8q"@@@`òäÉzzzaäÈ‘öööOž<ášõºuëž>}zùòå3fHJJ²ª¨¨X»ví‘#G„ní9úH$*•šŸŸßÕÕ¥¡¡¡ªª pww‡¥kjjJIIquu…)óóó¿|ù"##óŸöDpqqQQQ\¼xqÏž=d2YLLÌßßV8ìc0k×®…ƒ}éééðåVWW×ÒÒRPPPIIiåÊ•™™™½Qý|ÏÇoiiiÎ7ÿÖÖVÖ|Ä“™ñãǧ¤¤DGGÏž=›×-ÔãË øUXXèææ&,Ìsw›k×®y{{»¸¸<{öìÙ³gîîî¸D¯R]]-)))//ßÄt:½W•©¨¨X²d‰ŸŸ_AAÜ‘¤W³C d£ßIEE…¶¶vk@üžï†Á„Ä”å$+j¿ÈËH5µR¿´R?Õ7w1°ŠšAaáâÚ–º¦6&“‰ |ÛÂÀçSt*õ›$rÎ7fdd˜››[XX¤§§ã¯•••êêêxJ OŸ>Á°²ò¿vI:::))IKKkúôéÐobÈ!°\øèWðúioo¯¯¯÷ôô„îììÜÕÕÅd2ÕÕÕeeesss322¬­­544 ñǹ¶¶ö?þ>|¸––Ö–-[`•þJHHHà½hjj‚1liXÄylæâììåèèÈ+ÇÞvKÁÑ ÆîÝ»EEE7mÚÄ+qWW×îÝ»/^}Z^^ΖãÀ+++axÇŽ»wï\½zÕÁÁaÙ²e“'Ož8q">"ùáÃ++«Aƒ¹ºº~ùòFÒéôE‹éèè 2ÄÙÙfáëë[YY¹páBccãëׯóªáÛ·o›ššjjj>üÒ¥K¼.(ñãó»Ù(ÿéÓ§C—ÀcÇŽ­]»&hkk“——Ç&222ÆŒ£¥¥µnݺ®®.>eÁyñâënS¦LyðàèC»4|øpMMÍ1cÆpõÖ¬¬¬´µµÕÔÔ´··‡ÈÝÝýÌ™3x‚‰'&$$ðª:VóN£ÑöíÛgdd¤££³víÚöövÀøñãëëëííí-,,¸àììÃwïÞesUŽŽŽNHH8~ü8~¡ˆï„÷èá/÷ÊrR-õµïÿþô¼°¬½‹^ñ¹©¡¥ã}yuCS›¸ˆ y‘ ÐBí¬ojÃGø6ßí;ðoxx|#¬£æææ¬£díW•——Ã×ÀQ¨ÌÌÌýû÷÷¤fˆŸUUU ‰ÜÜÜW¯^½zõª°°0::0pà@|r¾-ÃȪª*|5~YY×›ÊÍÍÍÅÅ¥  àýû÷ŽŽŽ° B¥R©T*þ&€ƒaXii) þüÙÆÆÆÍÍmõêÕ½Td®¸»»§§§R©ÔÀÀ@™Lž0aB\\\XX\V055¯¨¨˜2eÊ’%KÅÅŰ\“'Oæ#¯111YYÙ7nÀ ÏÏϧP(p~ÛÜÜ<>>¾µµU]]ÝÜÜ<""¢¦¦îâçç'..þüùó>q.ƒúÙQSS“””Ä»z¯_¿6l[šaƱ&PWWçA€L:5//OPP°Ö©rRSScoow‘ÿLÊùøøðÈÏÏohh°··gÄï™ððpÿââb55µƒêëë===ß¿´lÙ2¸CÇþýû322âããKJJ<($$„K;}úôÙ³g544ðÈaÆEEEÉÉÉåææWTT,\¸pÛ¶mðÉuqq=?@ffæ;w؉)ÊòåË9RTT$++‹/7‹‹;{ö,¼É…„„V­Zã[[[KJJ^½zuüøñM›69s&11ñÑ£Gׯ_‡çÀÄJJJòòòïÞ½ëfµ§¥¥yxx¤¦¦>|xÑ¢EMMML&sÑ¢EÓ§Oÿøñ£··wDDL‰a˜ÍË—/ß¾} ÷°8p`àÀW®\ÉÍÍussãUëV­:yòdII Úè¦nÄÅoe£¼¼¼‚ƒƒß½{þÂ7 IDAT'//ïÀÅ‡ØØØÛ·o?{ö¬¨¨îÄÄ«,Ý¡ìÒ›7o®]»öøñã’’’èèh®ËøÃÃÃß½{§««ëíí puuÅßÕ‹ŠŠ(еµ5[Õq5ïAAAÙÙÙwïÞÍËËkmm…ý¥ÌÌL™ØØØ´´4®ù*ŽŽŽvvv«W¯†º›Õ‹@ðç耰àÿöN631Ô(Gë¤ÞÍÌ{_^ó"ï-“Á!aòÒ’©ÓîÜKmiïjooÿ×è@Oëúµá†ÿÖé777ÏÈÈhjjÒÐÐ033{øðauu5|˜={vzzúíÛ·étúùóç'MšÄUˆ‚‚ÂíÛ·#""ð¦ñû0|øp__ß––&“YTTž/_¾L¥R'Ož„‰õõõUUU÷ïßßÕÕ•‰ï…ÉJss³¾¾>‘H¤P(œ£Ñ€¶¶¶“'O–––666&$$\½zÞœ 666ÖÖÖkÖ¬éèèèèèè›×ࢢ"¸…‡„„„„„îáæævüøñÜÜÜ9sæÚÛÛcccÛÛÛ………eeeÙ>§×}–.]ºyóæŠŠ @UU¾¨¹¹ù¹sçÆ°°°8s挙™üDSS“–––˜˜•Jý%g,‰D¢‹‹Ëþýûsrr"##,Xøô铯¯/tussûë¯¿ŠŠŠjjj> p…D"EEEqnÙ7HHHdggïÙ³çóçÏÝ™”466 ÉÊÊr=êääddd$$$4þ|Ø»‹‹344´··'‰cÇŽµ°°¸wïàÚµk»víRSS#FFFøèÉbbbâãã•””ø¨‘˜˜8nÜ8+++AAAOOO)))|njÙ²eBBBlíWbb¢¹¹¹¥¥%‰DZ½zµ””ŒŽŽ^ºt©¶¶¶¨¨èîÝ»ïÞ½ }=0 Ûºu«°°°••‰Dòðð——WWW7n÷ikkc]d$%%Õ¯¯A à´á¨Q£ttteé}`—H$RGGÇ›7oºººÔÕÕñ9VìííGŒ!((èçç—••USS3}úôŠŠŠ÷ïß"##íììXGjoóº}ûvEEEQQÑÍ›7ÇÇdzåÕ‚ }‰Ï*\ÜÝ—J¥B»&€–€6%%Q¨€ÆŠšÊÞ¿PJ>°J¨ýTÆ.ôkÌ/8w¼ïãŸxÊÉÉùOyÁW xÜOßn:P½zõÊÄÄäæÍ›p&22’F£á¹À¿ 寿þÂå ~y6nÜþ¹ÜAAAØHBNNŽ––Ö‰'àÎp .\¸p!¼Ο?xýú5‰DJNN†BFŒ1bÄüæIJJb0ðo\\àv_™™™544444¨¨¨°ÞÃðîíÛ·½^ÿFHHöiV¬X+ƒ˜¯uuu¸1’‰‰‰‰‰ g¹Z[[aü•+W ¨•+W–J°³³³³³«©©Þòòòð¾¾>Ü2''gÀ€x°eË< ‡êaøÁƒ0~êç×ÃÃPRR¸uëø§8NNNðÒhjjFGG·µµµµµÁo:°–é>zôèÓ§O …ÉdvvvæååaöúõkƒqðàA:^PP //¿sçN8…Ò㈋‹.^¼8 À¢°°pÆ ü2ÈÊÊvuu566rí|ãÝe111¸Õ…Byùò娱ca|{{ûˆ#¨TjccãàÁƒÙNïèè8þü‘#Gäääøk^]] 7Ý€¨««ãûrÕ¿ººŸå#‘Hx¯”UŽ¢¢¢PUU•°°°¤¤¤˜˜ŒÅeŠŠŠÂr‰‹‹ã‹GÍÍͼÜC8QTTd WWWKHH¨¨¨àXÅWH1Œܺu«µµ•@ ´µµuuu±õŒ¹Ö0àÚµk‡044ܽ{÷7ï¿…@ô#¿•Âí‘Hd[WË<½ªª*ôàe ºCØ%]]Ý]»v~øðaÚ´iûöíSP`ßI /”„„„¬¬luu5™L¶··ŠŠÚ¶m[LL ç`:WóN¥Rëëë}||à‡™1 ƒK#Y÷÷éNAˆ>€Äß§òäÉÎõHøÀ0ŒÉd2™Lƒ!t:Á`Ðh4:N§Óa€F£Ñ¾þ}ÙogáÂ…'Nœ8sæÌ™3g¾:)1b„¬¬l\\ÜâÅ‹YKÄË×LEEe„ ¡¡¡lñ²²²¥¥¥¬ýQ€ˆˆHTT”›››¬¬,Ü:”JJJ?ÆÿR(¼3ÍU%%%ü«%|; %%%èøüùsWW—²²2Ûc®èêêâ“rµµµõõõ:::liÄÅÅñÅÀx?˜u­òßÿ ?¼úùóg<²¶¶¾ÃDFF>|ø0!!A^^¾ººZ__ú(±vpyÕðèÑ£#""ºººNž<¹bÅŠ_oPÄoÂïc£ð¹k&“‰%ˆ‰‰á °™¦OŸ>ÁWTT@‹Á«,8bbb¸Q466âá¾±K®®®®®® kÖ¬ :tè[|3ˆ¶¶¶ÆÆF˜£«««§§§……‰D255å¬:Nó.***++{ùòe>»2ó*Ÿ:‡ôÍ.Ԉ߆@ Ä¡Û$.Xð_Í  صkו+Wª««›››“’’Ö¯_Ï+ýœ9s²²²nÞ¼I£Ñººº²²²( ÀÝÝ}×®]L&óÕ«W¸[¾‰‰IhhèòåË322ø¨akk›™™yïÞ=:òåËKKKþé=z»¿qqqøZ\‡¿þúëãÇT*5 `Ú´iÝü‚úœ9srssoݺÕÖÖ8vìXÖ½u!úúúÐ%°¬¬ ÿØ* ¤¤äúõë ãæÍ›ÿý÷äÉ“õõõedd ûRYY¾„§¹¹™L&Ã-С3D^^ß…k S©Ô„„*• ]¬ñÙ?â§ã÷±Q?ÎÌÌd0'OžÄ·400ÈÎΆ{°M›Ÿ>}º¾¾þË—/ÁÁÁpŸ^eÁqâDwj*páÂ…;vhiiQ(”³gÏr¦Ù³gÏÕ«W§M›¶cÇŽiÓ¦áñ“'OÎÊÊ:tèáÇCCCeddÂåË—Ï;7cÆŒ€€€Ù³gÔnnnL&sâĉ...¬^ÊkÖ¬Ù»w/ü°×Æ0,$$D__èС‘‘‘Çïf¹ˆßÄF:ujõêÕ:::µµµ†††0~ìØ±¶¶¶'N´³³c[¨?{öl›Ñ£G:nžÏ«,8¢¢¢AAA‹-²µµMOO×××Çõ]joo÷õõ:t¨•Jݰag=¸¸¸øúújkk¿}û–ծ ¤œœ¸VWó¾yóf333;;; 9sæpnË« K—.mmm577wssƒû+qž˜››;dÈô-[D Àæ#ŸÒãdeeákuñ+}me ³®,°°°À0 ~iÕ”ÕÕÕügŒóòò¦L™òU9<@ëÕ{–«W¯Þ½{÷êÕ«ý­ñCó›Û(++« 6LŸ>½o²ûñíRLLÌùóçqÿâÇN§ã«`ø ++‹aØ“§OÆO&“¡ ü%±Î%$$À-4Y#qŸ™¾œ;B DßðU>ð#’’‚Ÿ¹þj²Ï@ ¾ ²QœŽŽŽ¿þúkáÂ…ý­Ñ+üot !!ÿ‚[ç¯ï=KÑ7ðè³Uššš=.@ z d£´´477· &8;;÷·.D¯ð¿Ñ¶™"¶tht@ ~Iú}hÑ,X°`Á‚ý­ø¡écúÙ.YXX°í­ˆ@übüot`Ö¬Yè>€Fâ7¡ûCxú~Õ@ Ñ+ükß[[[ Ã1´²@ ~xùð2èo•@ =‰­Ÿï@ü&tÇq€-²¿UF @ô<ì£àŸ¶HÔ#D ˆ_’ÿä5€@ ˆ_.£œ N!@üª|ut€ëx@ âƒ$ ÐͤÝO‰@ ˆŸÿHë‹@ èyHÝI×Ídø¹€îL&“ÉdƒÁ`ý…Ðét<Ðß*#@ z’µµuë€@ @ ¢?éÖ¾\AÓGñ €;pu Ñh0L£Ñà/F³°°èq5JJJZ[[ëëë{\2@t999))©Aƒq=ŠlèGø¨„ô©òSoç@ ˆ8:€aÛ‚:Îd2ñÑ:NgÐé4:NëqJJJ‚¥¥eKF ˆîóòåËÒÒÒÁƒ³Å#…@ ú^ªg!©ª¨öj@ð§µµu»D¿3räÈÇsÆ#…@ ú^ªg!)++Ã×%èËUñ›Àjðñ0 °ýmhhèÙ¬ëëëQsƒ@ ~¸®@6 @üôÁâ&’€€´wøGªXÍúr@ün`ÆÕøóŠï©L{I2@|?ÈF!ˆßøg·z¬?d Ñ7 @ô/ü@‘B ýHßLÛ“8óC¾<œz©ŒaXH¾téRrrrtt4[¼——×!C6mÚôýYðÁÜÜ|ëÖ­3fÌèÕ\DßÓS6êçLåàÁƒãââ Y#?}ú¤§§×ÜÜümj›™™ÕÖÖ~Ûé€G[Ð# Áƒ|||¾|ù’œœlbbò=¢ºÏÝ»wž>}Ú{Y0Œ <|øÐÒÒ2<<¼Gdêéé]¾|ÙÔÔôûkžWãŽèqœ=?ø‹––Þ³gO\\Ü•+WæÌ™Ókª"¿>†††½ñ­8â§ãÅ‹S§N•““#“É–––pž+W®ØÚÚö·jßNYY™””Tk@ ~wF-,,,""¢  0a„'Nàß)Ÿ7oÞÔ©SûW½Þ£GJ·cÇŽC‡ÕÔÔôÙÐ@wÐÒÒÊÎÎþ ÷îÝ+--¥P(=54€øIùßÊ×aQþ‹/&‘H‹-¢R©"""ÈÝà«HHH899;VXXØÃÃ3Áºuë¦M›gŸ¤¥¥###ñCD"Íœ9ð”””óçÏ_»v °xñâÅ‹O™2…Fûoã%¿½½—žC† ñòòÒÖÖ¦R©aaa 0þÀÙÙÙ±±±€1cÆlÞ¼yùòåß°yÆU?ÿUO99¹€€¤¤¤””À!CÅÅÅÝ‘¹páÂ7n ‡Ñ÷ðÙS W·`Ío€ºººìììV®\Íd2Ÿ={F$ÙvFüª4®ò{{ê.l[<"ˆŸˆ_Ìwàܹsnnn555ÙÙÙÛ¶mKOOˆˆÀ)ŠÿZRÎÊùNs×SÖ’M·•Žòòrn¶DL&“H$~Ov¸(Ð Í¿§håååC† ammù@§ÓI$ÒW“áZõTü+=ƒ?,¶ÿÿÉk¢««{ÿþýææfÖÒÒÒ Jþ‚P(”œœ®Ï•©©éàÁƒñCMMMV,ÄÇÇ_½zµ½½ÐÖÖ†€Nž<¹³³ózð|äsÕ“@ ,[¶,<<|îܹ«W¯>|8>~Ô¨QòòòÚÚÚ'OžÄO§ÑhK—.%“ÉÆÆÆiii¬=766&“É666%%%Ø¿ÉÎÎ6lþ×ÌÌ,%%Ã0U«VY[[3ÆÆÆ†B¡À Æ SRR‚>“0²¨¨hÊ”)d2YMMmÅŠT*Ã0''§ÎÎNmmmmmí‚‚^šìÛ·oРAd2Y__?++ C ßG]]]7êêê8wÇFýìˆDâÀííí###“’’ µñòò Â0¬¨¨hÒ¤Id2YYYyéÒ¥†Ñh4gggUUU2™Û·o×ÑÑQPP077Ç¿qéÒ¥Aƒ©ªª;v OÌÕl¾xñšÜAƒ:uŠU:¾|ùòÙ³g···ã¥ûøñ#™L>|øðرcõôôpù_¾|™7o™L6119tè™™›ªÆÆÆuuuÖÖÖ°*..ž9s&™L1bDDDLãáá±nÝ:kkk==½×¯_³ž.--]QQÃ[¶lñóóÃ0ìÒ¥K3fÌX¸páØ±cÇŒƒ7[MMMnnnP™¼¼<Éõ*¬[·îÓ§ONNNÚÚÚ—/_Æ0,77wòäÉðÜû÷ïÃs `-={–U±¿þúkÇŽ·nÝÂ%;vLWWwàÀóæÍƒO ^i&&&žžž¬§smòXï®kYk›k-a<÷®®®;vhii©ªªzyyµ¶¶bÜîÞ_†^í‰A©©©< ‰FFF'NœNJJ255 ˆˆ’’¸páÂÌ™3OŸ>}ïÞ½ .¨««Ã±ƒÕ«W'$$ùûûß¹sçÎ;$I@@@VV6((()))..îèÑ£zzzœ«~gÚÚÚlò¹ê‰a˜¯¯ïË—/™Læëׯsrr à!‰D $$$Ž=zöìÙ‚‚‚oS㇭Ÿîè©  PSSÃd2»ºº„„„¶nÝzþüùwïÞuG ­­íÝ»wq%%%%ãââÄÄÄ|}}oݺ#*** ðôéSeeeü”ãÇÃð… ŒŒŒvîÜy÷îÝèèh}}ý,,þñ#è?ÖÆOUUUUUuùòåwïÞmhh€‘ƒÞ¿ÿ„ ŠŠŠž={†a˜¸¸xhhhuuuXXØž={`$àÞ½{&L¨¨¨Øºu«““Ó—/_0–>Jjjª¯¯ï_ýUQQacc3þ|&“ÉÙú²µÄ0””’••5uêÔ?þøð²²2ãÇ———0ŽîaÆd2ýýýËË˳³³sssao5""BXX¸¨¨¨¨¨HWW—«&ùùù!!!ÙÙÙUUU‰‰‰ìîñ[1`À ÃØ†ðx6ºc£~1ôõõ‡š‘‘±X¿íÛ·ÛØØTVV–––Â÷+&“ikk[PPPRR¢¢¢²zõj˜™ššúæÍ›‚‚øúÍ*jÞ¼yJJJïÞ½{ðàAhhhbb"›»wï~üøqJJJUUUpp0‰DÂ0¬¥¥%??ÿÍ›7 ;wî,--Åxðššš3f¸»»—••åææšššâ¹wvv.X° ¥¥%22RDD„õ:655aöäÉ“ìÛ·Ê÷õõ¥ÓéÅÅű±±aaaœ×=''GFF&99ùÙ³g ÃÉÉÉÄĤ¬¬ìÌ™3+W®ÌÉÉYÇÇÇ_¸p¡  ÀÀÀ€õt^ËÇ===Ÿ}š––VTTÔÒÒ²sçNŒÛÝûËГ/¦L™2uêT&“) 0nܸ˗/§§§ûûû¯Zµ 0a‘Htvv>xðàŒ3>|ø°gÏxò‰'fÏžýþýû€€ƒðôôììì´··Ÿ;wî7ðµLV8}|}}###kkk¹º!­X±âÒ¥K°’÷ïߟ6mšµµujjª„„ÄwöÚÙäóÑG]]½¬¬ † ‚  àþýû©Tê­[·¾G^ùöoýtGÏÂÂÂiÓ¦éêê2™L ‰´´´îo!3kÖ¬äädÖYYÙ|øðaáÂ…k×®íèè‰D¼ úkÀøíÛ·'%%9::¾~ýzݺu=P<¢¯`müµ—ŸŸO¡P¶mÛ&((8vìXggç7nÀ4ÎÎÎpl—M¯ÉÐо®=ZOO6[QQQþþþbbbêêê¸2|®.-**ÊØØØÑÑ‘@ ˜™™Mš4 $‘Hùùù_¾|‘’’ÒÓÓãZ4÷ööÖÑÑ ¼}û6>ξvíZ΂t§Éã,;^@55µåË—¾C ÆýâÅ‹»víRTTõóó»yó&ÆíîýeèÉŽþõÍ‚ÚÚÚüü|]]]!!¡ºººŠŠ iiiøòsíÚ5 …ˆuww—‘‘ijjú$ð¯Ý ëëë---G•›››““з¸+–µf¬¬¬äååcbbàt4kzmmm•'OžàÓwbbbwîÜSÇkÖ¬þíß\Õlòùè‰cccxþü9$ IDAT©µµ•×;|7á”ÏUO]]ÝuëÖyyyáï½0Ù¦M›lll6oÞìîîþÍÊpæÛïõÓM= Æ¥K—LMMBBBüüüœœœìììâââøË±··OLLd‹$‰>äŒäš;@ÀýtêëëÅÄľ­DD¿ÀkPœL&ÃÿøhžlÁ‚~~~®®®D"&€ÀõŠ0 …B±³³ÃXæ"TTT\]]½½½ÙrÇÃbbbx ¾´Wxš››•”””••Ÿ?Ž'þüù3Lìïï/..ž••%&&yæÌNýyi2þüùóç744øøø_Õ"ˆÿÇÖÖnüñ Sp}6q×7°çí۷𠂬¦RYYùܹs†=zôhîܹ'N¼wïÞƒîܹ£  PUU¥¥¥—e***àûyy¹’’.š\ ‰œœÖÎkMŠŠŠÊÊÊ–””pŽ„²&ãcÀ_½zuëÖ-¶«ƒa˜™™Ù”)SfΜyçÎüÅ’µàlòÅÅÅ%$$(Ь¬,,[6e”””ªªªºººàAYY™²²2ö϶\Odm_ 3†axã(--µ³³—””¬©©‘‘‘ÔÔÔÀ|ø^€MUUÕÒÒ®Œ`Õy̘1±±±ÇŽ[ºté‹/¸–  ¬¬\^^õµµÊÊÊõõõ¼ÊŵÉÜðS`ñÚ†“Ð0¯ZâlÜáÍsýúu}}}V}8ï^2™Ì©6‚+ÿzëÃ×—Â~üµŸõµJUU•H$VVVþOÇè“ÉŒŽŽ^¼xñÔ©S?~üxúôéÞ/ÈÏÛ+î´iÓ„„„ÂÂÂRSS£¢¢¤¥¥SSSY?;zôèׯ_sžøçŸò÷ÿïlòyé ÑÒÒ:uꔟŸ_II I ÂÂÂššš¢££¬¬¬¾GŸ­~º©'`„ ŽŽŽÐ~½ÿþþýû#FŒà/DTTÔÒÒòÞ½{ü%C˜L¦¼¼< ³®,`ìë¥ò"½ÆByyùž={ŠŠŠ¨TjiiéÅ‹áþOòòòååå]]]0Yssó°aÃByy9¾ðþýû°°0ƒ×LN:•µwâééù矾xñ‚Éd677ÇÆÆ2 ÖÜ ÔÒÒòöí[ Ãá¢xzLLÌË—/»ººÆŽK&“íììRSSá‚Õ˜˜8i†aXSSÓ!CDEEÛÛÛCBB „Ðh4|³%®š=}ú”F£Á~ª   †@ z---þ ºc£~vL&“F£}úô)>>ÞÕÕuÆŒ£GÆb[[[ ——‡„jjj"“Éòòò†Á¾=žþرcŸ?nll|x̘1ÂÂÂ’’’uuu+++*•Š' “É·nÝÂ×hArss]]]ß½{ÿЉ‰½xñ_»%&&–——§¯¯ßÕÕõmZ±Éç¥'F{ö와€•J…뵨Tª¹¹9àܹsiiiׯ_DGG'&&^¾|ù¿jòcÖO7õ$“É]]]йÆÁÁÁÑÑ‘Á`¬^½º¡¡¨«W¯;vŒÍÅKRR2''‡SçÑ£GJJJ>|ø0..néÒ¥K–,ܸqãòåËp“uuõøøxccãž*,â—‡µádípvž0 «­­Å0¬´´TSS³±±ñ«Â«««eeeù|ùáÇÖÖÖ_Ýž£Yºt©±±±O+‚@ z pçÎI“&±Åÿ6 ѳœ={6++ëú±ÿ•ÐÐД”¶…¿÷ïßß³gÏãÇû[‘^Š:W>òGLž<}2~Üx2™ŒO.¸nÁ6¾qéÒ%øÖ@ zUUÕÐÐPè @ôhtà« Ñâ—üæ”””P©ÔáÇÏ;wÇŽ½)ht {ôÍè÷¾›ß\k€¼”ˆÞÀÁÁ!66=_ˆÎ¥x$×C=˜ïOáø÷S(‰@ ¾ >øÏb£ßCKK‹‡‡G]]”””»»ûœ9sú梣»‹Ï²NßÔÏ™VÐÆDïqüøñþVè~ü¾¾gR+‚@ z 4:ð›c``¿ÅŽÓ}Á‚ ,øÍï®)S¦L™2å7¯„¯ò899õ¶øm‘••…{š"Ç]Ñ_&“ ¿¯Æ²Q¢ác z–n Ñ{HIIèéé!?5Ñ_0™ÌÂÂB)))ÎCÈF!ˆþ…êYÐè@ úMMÍâââ—/_vgCè dee¥¤¤4559!…@ ú>ªgA£è† Òß* OB ¿„þV@ @ D?C055µ±±á<0kÖ¬ˆˆˆïÏ€—üÿ «>ÑÑÑ="øÑè©ç@ @ þ„+V477÷^½-ÿWBNNîìÙ³OŸ>MIIa}Eä˜5kVBBÂÛ·oãââ¦M›ÖƒÊÈÊʰðîÝ»’’ ‰þÒ §§š“““žž>þ|<^JJjíÚµ wïÞe;¥Gô!þþþ™™™OŸ>=zô¨¨¨(¿qãÆŒŒŒ´´´5kÖÀŠ#""¢¢¢pµõôôº™z^@ D¿@ÒÔÔ|òäI/IWVVîUù¿GŽyùò¥——×Àê««sssùÄ»»»ûøølÚ´)''GGGÇÔÔ´•ill6lþwÛ¶mmmm­­­ý¥@ذaùsç²³³¯\¹RXXóÅ0¬¸¸˜D"MŸ>õ”žÒÇÑÑÑÈÈhÚ´i†:thÙ²eÇŽxxxŒ1bÚ´iáâÅ‹Ÿ? [¸paDDD||ü£Gbbb$$$Ž9²eË–îd„ž@ Ñ_nÞ¼‰aü#..ôòåË;wîŒ3扄åË—§¦¦fddœ?žL&444^½z%$$ÓH¤œœUUUVé¬ò=º{÷nÞ¶m[PP9¼ô¨««_¿~=77722RCC£§«¥7nÜéÓ§•••'Ož\¶lŸx!!¡Í›7¯_¿>33³££ãÕ«WçÏŸï%ÝŒG}òäÉ~Ô‡Éd.Y²äÉ“' ãÅ‹?622‚‡ZZZŸ>}Êš¾õ‘““{öìY{{;•JMIIQRR‚ñnnn‡¦R©mmmGŽqssÈËËS(ƒÑÖÖ&$$xðàÁׯ_w'#¶çERR2==]LLlß¾}ÙÙÙ=MNN¶°° ‹ˆˆF}óæÍo+@ @@HÑÑÑøŸÝ»wKKK[XX„àñÞÞÞÖÖÖK—.=zô¨››[YYYaaáÔ©S“““'N,**ª¨¨`•îàà°hÑ"üïÎ;SRR’’’Z[[mmm­­­|äðÒ`cc³nÝ: …âïï„@ H$iiéúúzÀÛ·o½½½ùÄkjj feeõn›6m:vìƒÁøAôhhh„……ñIЃú$$$„‡‡WUUÅÇÇ;99?~  ¡¡ñþýû%K–tuuݼysðàÁ€W¯^Íœ9“J¥Òh4 ‰»wï>~ü¸›±=/™S§NÝ»wïèÑ£T*5??_OO/---((HJJÊÃÃCOOïÍ›7ß_L¢)))imm…†@ ú999))©Aƒq=ŠlèGø¨„T^^C"""sçÎ7nô¿}û¶»»;<äéé¹víZø‰×‹/®^½Z\\¼­­íÆð­ÞÁÁ!22’Uô¨Q£jkkqù€æææ­[·ô£QWW—••%ÇÏÉÉù¯…B ~(JJJ‚¥¥e+‚@ ~k^¾|YZZ ‡ûYA6 @ô;¼ TÏò¿/ª©©uttTUU±¥’’’““Û»wïãÇ?~üèÑ£††iiiÀ;wŒåä䤥¥G}çÎÖ£¢¢Ø¤¥¥¥aaáÔÔT<’«^ú°ññãGƒ!++ûßËþñjÕ*yyùëׯ;::þù矕••|â+**äååI$`Ë–-£F†ãõ,S¦La{ï_}†¾eË–õë×óßîA}.^¼˜‘‘áììlaaA¥R÷ïßèêêjmm•‘‘Y·n¯¯ï€êêê˜L&ƒÁ8}útbbb]]Ý©S§ÂÂÂ455ñ ùÀõy¤¤¤°þ…¾***õõõÕÕÕºººzzzÝ\¹€@ü°´¶¶÷·âwgäÈ‘øÔ+ÈF!ˆ~‡—êYHx¨¾¾^LLLJJŠmËôæææ¦¦¦¥K—¾{÷ŽíäÎÎÎÄÄD;;;*•zûöíÎÎNü¨¨èĉwîÜÉvÊ’%K^½z¥©©9gΜ¸¸8>rxéÃÜ¡€mEÃOJuuõ† `xÑ¢E………|â+++«ªªlllàÄrï1f̘øøø¯êÙ7úhkk_¼xqíÚµ?~䟲§ô‘055]¼x1 ººÚßß?;;{Û¶m £°°ÐÄÄäþýû€Q£Fá×ËÒÒÒÁÁ!//Éd%''Ïœ9“ë›?¯ç…“¢¢"55533³/^@ß >|Oˆ~§¾¾ßq@ ú®>†ÈF!ˆ>XÜô¿Ñ†††gÏžmÚ´i×®] ‹-‚ëÌ¡¡¡ûöí[±bEMM´´ô°aÃðàÂÃÃ÷ïßßÖÖ'Tqlll?~ÜÞÞΩ¥¥õÇÌœ9SNNîÆÏŸ?ÿôé/9|ôò333»ººüüübbbh4ZO×L?0~üø¢¢¢úúzccc¸Ë¯x&“yèС]»v566fgg>îQ×㨫«³ypô—>çÏŸ_¿~}vvöW÷”>­­­µµµqqqL&súôé•••ðV¼zõêš5k²²²ˆDâªU«Ž9033[·nÝüùó©TªŒŒ @4hPmm-ÿ\¸>/\¡Óé?~´³³;}ú´ˆˆˆ——WYYNÿ†¢!?¨ç@ ~dB ¿$Ö?k×®=xðà³gÏ>|ø‰»C;v¬µµ5<<\HH¨¹¹ùòåËøè@aa¡€€€¬¬,Û¾hÎÎÎGýWN$Rppp```}}}}}ýÉ“'ÿüóOèÎU/}ùùù7nÜPRRzøðawf\ TUU¤¥¥«ªªV­Z…OóЧÑh¾¾¾šššÿý·ŸŸ_]]]k¥¨¨ØÐÐÐ={U‰. pâÄ aaa@{{;üHáŸþ9vìXaaa)))¸ á¤I“¨TjOé³xñbÿõë×Óéôââbø@RRÒÀ“’’—/_†NÕÕÕ+V¬€ïùÇŒŒd0>>>ü³à|^øŸŸ?oÞÈU‡Aâ—ç{”`Yòóóq'bAAÁo( ;ü=ê}gm³>tèСC‡~§Ìòòrn ¡Óé|<æºOØùÊÊJIIIyyùîùOåâtKü¾Zˆž¥W|øO`ÿƉD8©E$0,$$„'àÊ7bcc>¬­­íããƒaXmm­»»»ªªª––Vpp0Læáá±mÛ¶éÓ§ËÊÊÚØØ|þüÙÛÛ[^^ÞÄĤ  ¦ÑÔÔÜ»w¯……ÅÈ‘#½½½;::XÕÃ0lïÞ½ÚÚÚòòòcÆŒyøða÷sgeÖ¬Yaaa0|ëÖ-sss Ã>~üH&“÷îÝ;aÂ##£'NÀMMMnnnd2ÙÄÄ$//òÍšMš4‰L&+++/]º”W­"?/uuu ܨ««ãLÜMõ Â}†aëÖ­›1c†±±ñ¤I“( LÌia “'O&“ÉÖÖÖxb¼ººººvìØ¡¥¥¥ªªêååÕÚÚÊ©FXXØÈ‘#FŒ‘‘‘OÏÎÎ666VTTüã?ººº`Êððpccc2™lccSRR#«ªªÜÝÝÕÔÔ”””<==Y ÓéË—/Ÿ={v{{{XXجY³°LëáÇǎ«§§wìØ1x “Éܹs§ªªêСC/]º$""ÒÒÒª§±±q]]µµõ¨Q£0 +..ž9s&™L1bDDDLséÒ%[[[OOÏ#Fœ;wŽõôÖο~ýÚÚÚº®®—pÿþý1cÆ(**ZZZâ¹kjj;ÖÈȈõtæì쬪ªJ&“gÍšõ÷߳ݷöšÉdîÙ³GMMmèСýõ^Û\k J8q℆††¦¦&^K\ï®ÖÖVww÷*))™™™577c¿½Þë%ß@ þlÍ׆ó/×–ÒÅÅåÁƒÚÚÚ7n„ÉæÍ›gllüîÝ»úúz;;»!C†ØÚÚÂÃÃoÞ¼9dÈ9s昛›ïÞ½;88x×®]ðk8PZVVÖ½{÷‚‹‹KPPŸŸ«nZZZ?–——¿~ýº»»û»wﺟ;ŸÂ¿MMM€´´´úúúñãÇXXXlݺµ­­íãÇuuu¶¶¶RRRß©ÉöíÛmllîß¿O£Ñòóóû¦ó@ô% À0Œí dzfÍ‚ñÝÒg]ó¾„k¡’““?~¬¨¨¸mÛ¶={öœ={p³0bbb€«W¯&%% >Ü××÷?þHIIa•¼oß¾§OŸ¦¥¥IJJ._¾|ç·bÍëîÝ»›7oŽˆˆ055¥P(t:ê™””$((heeáææöðáC__ß›7oœ9sfþüù™™™}}ýׯ_‹ˆˆ<þûç]´³³sñâÅ‘‘‘Ð-§©© ð'OžTWWÍš5kРA7n܈ŒŒÌÌÌ”““[¾|9gåää䨪ª&''ëèè0 '''[[Û˜˜˜œœœ9sæ 2däÈ‘€ÔÔÔ¤¤¤ .|µQûAìüðáÃg̘QTT(//wqq 6mÚ¥K—fÏžýæÍqqqÀãÇ>|ˆÐC˜L¦­­íÅ‹‰D↠V¯^±žÏkmdddXXXzzºœœœ§§'ŸÄðoKKKaaaQQQqq±••¬%®wWHH•Jýøñ£Ð«W¯¾Í#æ7ù ¢ÿáÚ3`‹áó—??ŸB¡lÛ¶MPPpìØ±ÎÎÎ7nÜ€iŒ---1nmÖlçq ±±±ãÇ·±±!‘HË–-“’’zðàLàãã#$$ÄV.‰äææ&...""²mÛ¶'Ožp³2###½¼¼ÔÔÔÄÄÄüüüx%ÆÃгCHHHOOoÁ‚°–¸Þ]$éóçÏïÞ½Á±Œ_‰oè_ýWï@ ~eÊËËi4Ú¨Q£à_6lØ0&“åò IDATÉ0 &&†‡EEE[[[ñÓñÄUUUlÂ#""N:U]]M$«««ëë뻟ûW‘””ÄsÏÏÿ?öî;ª‰¬møMèÅ@èEWPEA)‚u#`‘U,»ŠØXEq‘WD±!ê²–EAE¤ˆuUpÅBÄ®¨Az'”P’Ì÷Ç|;' Iõùg¸¹óÌsï$C¦ÝyÕÐÐP__?tèP¼pذa=ÏäСCÞÞÞ¦¦¦ŠŠŠÛ¶mstt0=¾.¶¶¶¡[·nÙÚÚâÓàÚµkêêêø´¼¼<>!''‡Oˆ‹‹766âÓ¼¶0ø„´´4•J-))!Kojjªªªrvv&“ÿÿddkk+›Í&þD.X° cbªªªø„””Tii)B¨   55õòåËD²²²úúzUUUqqñv³¿|ù²¹¹ùéÓ§œË"HIIá{¹œm,))!ÚBLðRRR¢ªª*""‚ÿ©­­ýüùs|ºÓ±âÛlÛù’’ÎçÙikkói‹ÅÚ»wïÍ›7ÈdrCCCKK þèq>8{[Ç牋‹ïO--­û÷ïózw­X±¢´´téÒ¥NNN»wïÆïL‚ƒ£^ǃâœ%Ø%Â:œ—h¿U]]]ZZúÙ³gœ_¹FæŒFLþüž:77WEE…‹aØçÏŸ7mÚ7fÌ„ÐØ±cÙl6†a‚/'))‰h€¿íáÓÍÍÍeeeøá‰¼¼ÝÚÚúÞ½{ø^(×"œËRQQ),,$Õ±Z»Ê%%%­­­ø‚üü|UUUü%‰Äõ}2˜·óœ£ªªO4¡  o¯ {ðàÁ½{÷KJJ~øá<«v§»ÛͨªªJôvAAœO/UVVâ¾|ù¢ªªÊëÝ%**êåååå啽`Á‚qãÆq=üøè…; ¨Tê™3gž?þðáÃåË—÷< ß-cccFüI"‘ð{½øæuü2ÁYȵBKK ã_øÝª…œœ|ZWWwäÈ‘;w¯g±XYYYœ÷¦r8¾›”——WWW>|ØÎÎŽ³N—>|8†a±±±ÄY:'üÒÍÆÆÆsçÎåd2yÏž=ÍÍÍoÞ¼¹|ùòÂ… 1 [¸páéÓ§ñÊçÏŸïy&×®]ß#­  @&“ñ«pøVýðÃü+¸ú´Ûжµµa6Œü·0¡«W¯¾|ù²µµuÏž=&&&ÊÊÊœ3®Zµjûöíø„âââØØØv9¬\¹òôéÓiiil6;??Ìuã¼jÕªãÇgdd°Ùl:~íÚ5‹¥«««§§÷ûï¿×××·´´$%%³¬^½zíÚµsæÌ)**jŠkü 466:tˆë' õõõ544<ØÒÒ’––éàà€qûWEäÛy"s[[ÛÄÄÄ{÷îµµµ={¶¶¶ÖÊÊŠOÓêêꔕ•ñçœ:uг“¹v8náÂ…Doïß¿Ÿ¨À§—|||ZZZÞ½{wéÒ%¼—¸¾»?~üéÓ'6›M¥REEE¿½ÿh=ý²%€^8:°yóf‰dee5{öì7nô<à7B¡lÞ¼ùæÍ›qqq‚Ô'“Éîî‰Ož<Ù´i‰D"^²µµ½yóæÛ·o¯_¿>sæÌ~ËÓØØøúõë™™™±±±öööxá¹sçˆÃCS§NMII!®Èí­år5¨úGII)"""**ÊÎÎ/ÑÕÕÕÕÕ0æ† èt:ñ§ªªêµk×z1g³vÿÿ:þGl÷'>Ì5îÒ¥Kœ¯.[¶ìùóç¿þú+B(44´²²rܸqššš...555crF-Z´hæÌ™ººº[·nå|uôèÑK–,155?þ“'Oðó9‚/àââÒÐÐ`ll¼xñbsss"¾´´´žžžžžÞ‚ ~ÿýw Ã|}}+++ÍÌÌi4ZÏ3ILLœOH~øðáO?ýÄf³{ÜŽ^¦««‰ ëg¹¹¹S§N%.pô2™|ïÞ=++«våƒvúÂÛ·oçÍ›—Ý× ‚í>žˆ³qãÆ¦¦&sss‹ebbÒÚÚÚÿ ûfH$mmí?®^½ºµµ5&&¼TGGGDD$55µÿSÒÓÓ õññÑÓÓûôéÓÑ£G+**ð—„„„Nœ8ÑÐÐÝ?É ¶þyùò¥µµ5~ÿž´´t\\ÜãÇœ×ÎÎnÅŠøtMM•••©©éîÝ»Û];ÐÕÏ)_…þ¼jNpý|9_»E#ŽK9ý€Ï'npn£@oa³Ùpvvµ²²Ú½{w?¬nØÎ#†„þé¢ÿ\çYTT”™™©§§'""R^^ž——‡Ÿ“Dæåå!„ÂÂÂ~ùåYYÙÚÚZ^AËÊʦM›fbb’žž.ø`À•ˆˆ™LniiÑ××oii '“ɲ²²õõõxC‡á7íÌž=»ãÓMzŠŠÊ¶mÛöíÛççç·eË–€€€ŸþÉÙÙ9??_]]}üøñ™™™} |ýéâââääôÛo¿ÉÈÈLš4IQQ1$$¤ÓËŒŒÊËˉ[ùè­Ï)ƒÊàüfðîÝ;4@_݆ –ŸŸ?û€oøn sžéDý²å‡í|@&''‹‰‰µ´´ „X,ÖºuëæÎëååÕ?'®[ÿ°X¬S§N™ššVVVFDDlÙ²eéÒ¥vvvQQQügtppè´®·>§ T*•Íf“H$¸­0PÈd2>ÔyÇ—`X|6P½¼ ®Î ©©‰*,,äS‡Íf‡‡‡/^¼ØØØøýû÷çÎë~‚ß999ü§„÷ïßãOÛF½ÿ!T\\\RRÂùô»~óñãÇQ£FáÓÕÕÕ,‹xéܹsµµµaaaòòòsæÌé‹¥þþ™:uêâÅ‹Ùl6›ÍÎÊʺsçÎØ±cùÏ"!!aiiyçÎvå---rrrÝÈAÏ)ƒ…By÷îþå›Í~÷î…Bm`°á³ê]Â!EEÅêêjþõ¬­­“’’ÚÚÚ<==¯^½ÚÖÖÆ§²‡‡Ç;w^½zÕØØ˜ŸŸÇY',,üÏ?ÿ0™L¢ß.]º´iÓ¦ÔÔT!!!WWW„›Í>|øðž={jjjÒÒÒôõõÅÅÅû'É   __ß”””ŠŠŠuëÖ%&&â „ð#L&ÓÏÏoÇŽ÷ïßçÿVéªÁß?“'Ovss[²d ƒÁ••%“ÉC‡ÅŸ1ËF{üøqSSS»ò¼¼lž••ƒÁ@1™ÌìììÖÖVÎC*·oßVSSßìzáÂ…þù/¿qãF[[ÛŽ;ttt>þìééYYYÙy¦¦¦„††JHH¼yóÆÝݽ㼷oß^³fÍòå˃ƒƒ{k¹èkèŸÒÒÒ 6àûù‘‘‘,«Ó€ƒƒÃÑ£G;–WUUíܹóÏ?ÿDõúç€AeøðáðÛ(À÷€¤¥¥Õi¥èèè   {÷îõCB|o444._¾liiÙÃ8ð9=A `Ñn‚ëï²²2 Ãòòòttt9“VZZJ¥R………;­ º‡ÉdÖÔÔ¨¨¨ð¯F¥R1 KNI6bª¬¬L"‘BøoAÇ õËpw|‡ìíí¯^½Ú+¡às è8“À;~üø@§øÞ ttÀÞÞ¾¯óô|NÝÖå'àwx¹¹¹ UUUà;%//O¡P†:ЉÀ€£Xnn.™Lž:uê@'ø®effæåå 6l €w` †††à{7~üøúúúÎL/\;@¥R:4qâÄÚÚÚóçÏ_¼x±ç1ø>ËÉÉݽ{÷+?Ø– ¾UUU† t€àþ&À÷¬®ؼy3‰D²²²š={ö7zð{fll|ýúõÌÌÌØØXbzyyù¿þú+%%%66–F£á…’’’ùùù6lÀÿtssËÏÏíÅdtuuCBBž={–°dÉ¢œL&»»»'&&>yòdÓ¦M$ /?wîÜòåËñé©S§¦¤¤())õb>hõ…BÙ¼yóÍ›7ãââˆB%%¥ˆˆˆ¨¨(;;;¼DWWWWWWÀ˜6l Ó齘d?Çd¹\û ñ~_uµ|¥0z[©®Ð××?þ|mm-B¨µµµç¿[£F ظqczzºššÚˆ#ðrÿÌÌ̵kת©©………•––>þ!TSScmm}òäI„ÐO?ýԻבÉä­[·¦¥¥^¼xñýû÷ørW®\i``0sæL2™TQQÆ9¯ŽŽŽ¿¿¿³³syyy/¦4¨ú!„aXNNް°ð¬Y³ˆÂåË—GDDܸqãÑ£GW¯^•––ö÷÷ß¾}» UUUutt’““{7Ï~‹/àr¹öâý¾êj9øzÁ÷rÀÀ‚Í€ï9#####CHHÈÈÈ($$$888%%ÅÜÜ<..îþýû222¡èèh;;»°°°çÏŸGEE£¹zxx$%%¯8¼>¿d2ÙÅÅ%>>>11ñÌ™3ÊÊÊüóäµ\®ý†x¿¯ºZ¾RÏà>>ÑÑÑPTTD‘Édaaaü0 BèíÛ·#GŽÄ§%%%oݺemm=wîÜ¿ÿþ[RR²ïrÓÖÖÎÎÎF‘H$mmí?®^½zùòåYYYœCû 8q¢¡¡!::º×sÌýCxùò¥µµõèÑ£ÛÚÚ¤¥¥ãââ?~,à¼vvvíúMVVöäÉ“oß¾3gΊ+ ÿ‡ŠŠŠ²´´ÌÈÈðôôì4>W¼>¿ëÖ­³±±±³³333KOO?zô¨€y ²\^ï«®–ƒoF}}ý©S§Ö¯_ÿYYYéêê:tèP …2f̘}ûö566l†œdee9‚O¯^½ÚÏÏOÀóóó)JŸå5`KŸ8qâºuëú"²€f̘QPP‘‘1€9_ÿŒ;PTT”™™ùéÓ§ÄÄÄòòò¼¼ŒŒŒÊËË 8 )JLLLXXXUUU~~~§ARSSétzdd$qçŸø\ñúü®ZµÊÏϯ¦¦!d`` %%Õiž.—×ûª«å¶ ZíNßEFFZXXP( Ãêëë§OŸþéÓ§èè袢¢èè說ªÌÌÌ8­È¤¤¤³³s÷æíØöþÔ»K'Våˆ#æÌ™Ó½y,£ãÙ³g»1#øÎ Äö‹ÿ|™Æ8¾( ·€fgg³X,*•Ê'èÙ³g]]]Ÿ={vöìÙü±WsþfUVVzxxdffÖ×׋‰‰!„\]]BCCííí?^\\LÌRQQQSS“››ÛwàÑ××ß¾}û–-[Øl6B¨µµµ¡¡AVVÖÍÍmÇŽrrr•••øK!‹µnݺ?þøÃËË«×3œýÓ‹Å:uêÔ­[·*++Ož<¦££CŒPȇƒƒCTTTÇòØØXÁ—Nœç¯¨¨À;§ÓøqýüR(yyù}ûö=~üøñãÇ=ª®®&ŽòÉSÀåòz_uµ\‚Á©Ýô»wïš™™áÓgÏžmhhˆ‰‰?~¼””Ô?üàïï?eÊ Ãrrr¬­­••• """ðú+W®Ü¹sç¬Y³¨T*F«¨¨X·n‚‚„ Þ½{‡×ÑÑÑ9}ú´œœœ»»{QQѬY³äååçÏŸO§Ó1 KKKÓÓÓ#ò™æ¬JiäÈ‘AAAnnnøm¸÷ïßO˜0Ÿ622âÌçܹsµµµaaaòòòsæÌéÝdaÿp5uêÔÅ‹³Ùl6›••uçαcÇòŸEBBÂÒÒòÎ;‚ÄommUPPÀ§| ×ø¼âpýüÒéôºº:ggç©8Äô¼]¼ÞW]-_©v_Ð_¿~ýÃ?àÓ<°µµk÷ žÅb-Z´h„ ùùù§OŸÞ¸qã³gÏð—®\¹røðá‚‚‚ÖÖVsss++«ÂÂÂY³fyzz³_¿~ýŸþyöìYhh¨££ãáÇ?þ\__LÔi·Ã€OܹsçܹsÏž=366Þ»w/gåÅ‹/X°`ëÖ­YYY'NœXºtiXX^áÝ»wùùù4³bbbYYYYYY?þøc||üŽ;‚ƒƒ i4Ú’%KØl6†aõõõÙÙÙ>| Ü´iS@@@\\\jjê… ˆ&?|øpÕªUÉÉÉ‹/®­­ål›ÍÞµkWAAAZZÚóçÏOž<)àÒ_½zuþüù´´´’’’[·n©©©qæÏb±i4ZQQ‘««khhh»UÉuv))©ÒÒÒ°°°½{÷>}úOõþýûfff………‹-j×®éåçç¯\¹2    @NN.33“¨?räÈÒÒÒòòòŽ»à{#''‡aX»CDy;ý¿õ€ÁƒŒRTTìtÀ6kkk …"!!áééyõêÕ¶¶6>•=<<ÆŽK"‘óóóᄞ€‚‚‚6mÚ„ßN¿nݺÄÄÄ––„©©©¼¼‹Gæ:»••Õ?þH"‘ çÏŸŸœœŒÏ5räH'''2™lgg7bļ D†\Ó»víÚÔ©S­¬¬„„„¶lقߊ‚“".gß9^r ÂÉÉÉ%%%®®®üë•——GDD())ÅÇÇ{{{󯜘˜èææ6tèPaaá’’bX)À_jjj@@@hh¨„„Ä›7oÜÝÝñr Ý»wËÈÈàkŠó4~ß¾rå ‰DúóÏ?ñkÔ›ššŒB·oßVSS»}û6BèÂ… \oé¿}ûöš5k–/_Ü[) ª T5l IDATþA?~ÜÄÄDLLŒB¡¤¦¦"„¬¬¬JKK7lØ€ïDFF²X¬N?œƒüñ·wïÞÇ[[[?xð`çÎk×®ít®ñyÅáõùýã?®\¹"**J§Ó/\¸Ðñ¹‚,—k¿1 ^ï«®–ƒo•JmhhÀ§åääJJJ:Ö)))QUU%ŽBjkkJ!ž©!))ILKHH19ëHHHpÖït¼C999|B\\œeQQÑE‹…‡‡ïÞ½;22²Ó+é RSS/_¾L””••)++2„æƒW‹ˆ!W𦕖–rF.//÷ððxúôikkkKK ×Q<¹.}Ê”)سgχh4Ú¡C‡8TRR¢¡¡!,üÿOGîVWW·ãì>>>Ÿ>}Â0¬¶¶–xÐŒ††1£––V»&pM¯¤¤„xŽ’°°0~y#®©©‰Åbñ& |WlmmB·nݲµµÅ§´#õññábþüùíJz?ÙïÀ7nܸѮ0"""""¢]aSSÓðáÃ9ÿÔÖÖîÅL˜L¦¾¾>¯WÏœ9sæÌ™v…¿þú+1aX_üë<ýƒÚ¼ysǼ¼mW^__Ï5ç´´4ü£Š#vÑííí‰Âüü|===þñyÅáõùe2™æÉk¹\û Çõ}Õrð5jwÖnôèÑ?~477GYYYýõ×_ûöíãMÃ0•’’’ÖÖVüA~~>~‰Q¡ÝoÎòŽKlW_RR²¥¥…¨P]]ÍYûïYwÎyÉd2gÍ%K–,_¾ÜÒÒRXXxòäÉíÎLâW' ÕÕÕÛöŸ››Û± ÿüüù3Q˜——7wî\Î$wíÚ%%%•šš*))yúôi ÃY:†aK–,Y²dIuuõúõë÷ïßO<·!¤¢¢RQQAD(++#®ò "wœ ’£££««+ч………D¨/_¾àM pM/###==˜ Oÿ3++KYYYAA΂ >м+€+AÇÀ¿Cz½½ýÕ«W¿Þøƒm¹à+ÕîâÞY³f%&&âÓ«V­’””tppxþüyCCÃÇÝÝÝ“’’ôõõ544<ØÒÒ’––éààÐn¿qÛ‡çS‡˜:th}}ýÛ·o1 »uëV~~>×Ê*((·`6~üxqqñßÿ}ñâÅ›)''×ÖÖ†ïã-=~üxFF›Í¦Óé×®]c±X‚d‹ÊÉɹxñ"“ÉŒŒŒÌÍÍ1cgºººáÇKHH455?^ð¥gee¥¤¤´µµIIIIKK‹ˆˆpæ?fÌYYÙ«W¯b–——‡2¹\®³Óét===2™\PPpëÖ-¢þÇÃÂÂX,VLLÌÇñ&Ì5½¹sçÆÇÇçååavõêUü<`BBÂÌ™31þ‹Є—þÚæÀ`$<Ð ð½;~üøW°-|½8¿—ÛÛÛïß¿ŸN§2DRRòÞ½{{÷î]´hQMM–––½½=~ÿyXXØ–-[† ¦¤¤tôèÑñãÇßïùÿFÐÓî·„„ıcÇ–,Y¢¤¤d`````ÀgÎ?—-[¶bÅ Y³fá·VýüóÏÞÞÞ—.]ê¸ã!!!áîînjjÚÖÖkeeåëëëêêš——G¡PÌÍÍ9/à쨎 ̘1#))iûöíjjj¡¡¡ø3ˆW===×®]{óæM …bffvÿþ}¼.½±±ÑÝÝ=''GLLÌÔÔôàÁƒœ™H¤Ë—/oܸñÔ©SÊÊÊ ,àÌ Ã0®³;vlÕªUªªªJJJ3gÎ$*OŸ>ýñãÇ[·nåÚ®éijjþùçŸŽŽŽrrr£G644$ˆˆˆ8vììì.“a€ïIKKk s0À8÷™9'¸þ.++Ã0,//OGG§¦¦¦Ó॥¥T*•¸;½£‡þôÓO톰õ÷÷g³Ùøcê¾jQQQýõ׃ún!!!±±±aaa}·ˆ¯Ëǯ\¹2Љ€¯ ™L¾wïž••Õ@'ÝÁd2kjjTTTøW£R©†%§$›N1UVVÆŒâ¿áÚƒÑÖ­[:…^À`0Ξ=»råÊNäûbee;x@WÁÑã÷ñãÇŽŽŽ‹/îëÖ}“@?ƒà;G ¼orçÖ¢¸¸ŸîÓÖ-[¶lÙ²eß^ÐÏàCøÎÁÑŒJ¥²Ùl‰Ônèè7d2™ÍfS©ÔN ùÈ‘#Ë—/è4º&::šF£ ¶øø£ÝgÍš…ÿ)$$”‘‘‘‘‘‘Íù úvlmm#""ºŸko€þÄ­ZµêÈ‘#GŽñóóëø*Fk÷œíÞâëë»yóæ¾ˆ<àH$’””Ô@g¾ åÝ»wøl6ûÝ»w e ·ˆ0`„ÝÝÝûsyòòòK—.ýñdz³³ÿú믦¦¦þ\zŸ¢Óé?.((Àÿd±XFFF¡ÍëkÕÿý‰?{ !”””ÔG‹øÞ¨ªª^¸p8Ä/:::999™™™‚<ú•J¥P(::: ˜~½³@GGçĉþþþçÏŸ733;uêÔ·4ŒsSSÓ7ðä­Ácðô§Ð¦M›ÅÅÅÇçíí]RR2ÐI}ddd:ðÕ>|ø@§ð]#sþ‘““£®®ŽOÛÛÛ'iåääΜ9“’’’dÈ„„III__ß´´´GIHH „¶oßîããóàÁƒúúú»wïJIIá‹“––>räHffæ½{÷\\\ˆ+ÃyåƒÒÒÒ }þüydd¤¶¶6ÿ|xåÏ×ø¼ò Ã/zÏÉÉ™6mZ§Á¥¤¤üüüðöNš4‰(ïjòýÉ?ÿ®öçܹsi4Ú¬Y³ÆKÄáÊÈÈ($$$888%%ÅÜÜ<..îþýûøNò°aÃ>Ÿ™™yöìYyyù޳¯\¹òúõëøÕø¼òç…W»¸Æá“§¤¤äž={RRRRRR¼¼¼ÄÅÅñø¼ÖWtt´‘‘‘¿¿fff||ü¸qãBT*õáÇ¡¡¡Ã‡Ïø—ÿ& ¹ó*mܸ±©©ÉÜÜÜÂÂ"((¨µµ/_·n™™YzzúÑ£G‰YdeeOž<ùöíÛ9sæ¬X±‚Á`ÈËË«««§¦¦"„H$Òˆ#jjj455Bûöí“••µ°°ppp;v¬ )Ñh´]»vMœ8ñÝ»wǎ㟯ü»Ÿ—%K–½{÷Nä}||¨T*Þ^Î]Ä.õ'ŸøÐŸüóG]ìO2™ÜÚÚÊb±˜LfLLÌ‹/øçcffvòäɸ¸8___'''6›mii‰ÒÑÑIJJ²¶¶666FÜžåîàààèè¸bÅŠÆÆFþùóµ]¼âðÊsÏž=JJJÓ¦M›1c†¶¶¶——W§Ë=tèPTT”¥¥eFF†§§'B¨¦¦ÆÊÊÊÕÕ5''Çè_,«ÓP€!ÐѲ²²qãÆ™˜˜ˆŠŠ¦¤¤|øð/_µj•ŸŸ~›hPP1…B‰‰‰ «ªªÊÏÏGiii½ÿ!4yò䨨¨eË–ÉÉÉ IJJΟ?ßËË«¡¡N§ß½{W”óòò˜LfXXØ„ deeùäÃ+ÿ .Üå@ÜvÎ+~¯_¸p!×öv©?yþ$^ê•þDݸq####!!áÿû×þíeff~úô)11±¼¼¬]»–ÿÑ^Ž;ö÷ßãÃ@Íœ9“xIGGgÑ¢EšššÇŽ£ÑhMMM½•?¯8jjj\뉋‹+((TVV"„ÔÕÕI$Rqq1êÖújii‘““ëIþ€þÑ~TB{{{)))ggg¢ÜÃÃcìØ±$©±±1??ŸÍfãå!!!¾¾¾ø g222“'O泤‚‚ü!1®®®[¶l¡Ñhû÷ï///¯­­MMMݶm›°°°ªªêŠ+:Í!D£Ñ(Џ¸¸§§çÕ«WÛÚÚøäÃ+>¸Æç“ÿ†[YY ã¥WWW?}ú”k{»ÔŸ¼@5{¥?B»víúå—_ÔÕÕUTTLLLrss»GSSSXX!¤¦¦¶aÃΗ._¾œ““óèÑ£ÔÔÔ]»võnþ]ŠÃ`0®]»ö¿ÿýOLLL\\|×®]QQQø¥ÝX_yyy²²²&L@)**¨„ Zÿ9:°wï^ ‹„„;;»;w剉‰nnn>LLLttt\¿~=^þÇÜ¿ÿÊ•+IIIW®\ÑÒÒⳤ²²2!!!--­ÔÔT—ÇÇÅÅ}úô !äææ¦££óôéÓãÇ'$$tšBèÕ«WáááIIIõõõÞÞÞüóá•?\ãóɇÀÀ@UUÕ´´´Ó§Oãg\7oÞ?:`kkÑ×Ké6‰$%%Õi4>MLíùúê-Ý^ïðMúÞ¯PUU½víZ7fܰaNïõ|@ÿèöz€oÒ÷þÌ™nÌ¥ªªª££“œœÜëù€þѽõÀW';;»¾¾¾ªªj éCòòò222:::\_=ñÏÛ¬Òºôœ²~Ϊ?M®¬¯F]7M·'A:: ''wðàÁ1cÆ0™Ì??~üõëסœœ ‹¢¢"„½½½­­íŠ+BRRRÞÞÞÓ§O///OOO'âHJJnß¾}Ö¬Y¡;wîøùù577óZ¨‘‘‘««+“ÉÔÓÓûý÷ß½¼¼H$’½½}]]¯8¼ò‰ŽŽ>xðàÏ?ÿleeU[[»eË–/^P©Ô˜˜™ŒŒ |¡ÆÆÆ,«Ó±³³‹‰‰!ž Ž>räÈ´iÓˆø¼Ú˧]d2ÙÙÙyñâÅ¢¢¢ïÞ½óòò*+ëÎ;¸KýŒx¯/®ùhkkß¼ysâĉ­­­xÃÓÒÒæÍ›WXXÈ+þ!Cîܹ3{ölOOÏ3f0 Æ`0¸ÆÌ뀯Kvv6‰Dš:uê@'Òç233srr†Þ®üÏÞ6³PÈúŸ$«þä™|òÁ» ÓõºA ; 6nÜØÔÔdnnnaa„ïòáããC¥R-,,äåå‰ò={ö())M›6mÆŒÚÚÚ^^^ü㘙™ùùùïß¿Ÿ1c^ÇÒÒ2++‹Ï¡œ¬¬ìÉ“'ß¾};gΜ+V0 >í´ë€¯K}}½¡¡á@gÑÆÏõÖï%uÞv&l6öÍÿ챟ü¾¸¶'}H¾{÷npp0ÿJeeeãÆ311MIIùðáŸÊâââ .ôòòjhh ÓéwïÞ%Êííí½½½ Fcc££££ˆˆŸPEEE™™™Ÿ>}JLL,//ÏËË“‘‘éF„P@@@jj*NŒŒ1bÿÊü•——pþñÇíâóÉ“k»B«V­òóó«©©Að9ïÂ… w9à뱫ýÃk}ñÉ'<<ÜÎίcggÙi§Q(”˜˜˜°°°ªªªüü|þñçzà«SUU…}7¸Þ=‘ž[6ÐyõŸôÜÝ=!,È`ûgÏž¥Óé®®®úúú)))þþþYYY¼*kjj677—””´+×ÐÐhnn&.•ÿòå BH]]ýóçϼBa†ÿ&&º!D\ïPQQ!&&Æ·¹pppˆŠŠê4>¯¬ë€¯ÆqCt/b2™k×;‘‘FÿÆÇb4µž:Û‹ë LàNHMI161!‘H}R_èyÚ;Àf³ÃÃÃÃÃÃ¥¤¤\\\Î;7eÊ„Pkk«‚‚~¿·¨¨(^¹ªªJRR’B¡´»®£¨¨H\\\AA¡²²!¤®®N"‘Š‹‹»š1Ÿ8\óᯥ¥ENNNð¥KHHXZZz{{w;O®õétz]]³³3ÿK3º½\^õy­/>ù´´´Üºukîܹ ãîÝ»---ÝÈ“W|55µ®¶«Ö;_¯¾8@PTT¤9LªþŸ=Êô'ïûè`D§øì³ÿ=ïÈ߇¬,¿›De†x;:ÚÀ€ó¥ŠŠŠ†††aÆõB¢}¦ç‡4wÀÃÃcìØ±$©±±1??ŸÍfãå999öööRRR&&&ÎÎÎxauuõÓ§O·mÛ&,,¬ªªŠ‡b0×®]ûßÿþ'&&&..¾k×®¨¨¨N‡0èˆO®ùð———'++;a„¢¢¢ÿú4íñãÇMMM=É“—___eee„ŒŒÌäÉ“iB—Ëk}ñÏçÊ•+óæÍ³µµˆˆhðܹs111ÂÂxêR{v½8a†PÇ]î94ÀÆÆùyÿ浆֎áz6»Õ×щrÿ½{wÚ;ìpXœñô©€¡꧇%ÐÑÄÄD77·‡&&&:::®_¿/ß»w¯……EBB‚ÝÎ;‰ú›7o6lØÓ§O?ÎySº——W]]Ý£G>|XZZ*Èx®xÅá•UUU;wîüóÏ?“““ÿúë/|O•®·t5O^þøãû÷ï_¹r%))éÊ•+ZZZ.¨‡Ëåµ¾øäóþý{‰D¥Rß¼yÓ.šˆˆÈ„ ~üñÇNóìj{p½ð•êÝ;Û›››CÃ.±Ùl ÃZZZ««ë8ZšÛzwq]§Ù¹;~§–&BË•5÷ïØzoÛÖô8yž¼Š”lá—ÂÚñ_¿ní‘#~üëð鑺½ úÒÐи|ù2>~>à#))ÉÉÉ)//o ŠøwÒn‚ëï²²2 ÃòòòtttðE08ýóÏ?sçÎíù~#®¥¥ÅyÝ/,¬åØ¡Ó ãäÙãL‘ÿÜžüù}YÌ•Ûøttt´ŸŸ_VVÖ!ClllöïßßÛëîî^[[„*,,œ={öœ9süüüz˜‰Dºyó&ñl5‚ñžk¾?³ë{{ª²"EÉ]àã}54TîSžÁÙ’†ú›u•—ïljKH•ÿ¾uëø1ÿׯ_KIJŽŸ0aëïÛMLºsõ· \7¬×ÑÑqÛêΧ™D2ò OÛ½€(*•ŠaXrJ²éSeeeüŽ ü·@ל½½ýÕ«W:‹ÁnÙ²e………ph€Á ·ÎÌ777;¯ýe†­I”Åf³ËÊʪ««êkœ?m­L¼òùóç7lØàææöùóçþù§´´töìÙ ƒó‹Í¦)©ïpqVÎÉ×—–y]Yv½ºÔûÔI1qq¢æÅ kV¯rüyɳç/Ÿ½|½lÅÊðË—{¥\!ŽÛùUë8:ÐÇÿóÏ?:‹AmüøñS§N]»ví@'è5­­­¿®Y1ÝfJ£\IUá×µNÞvˆÉ 1 ÎCµ´´xzzúúú.^¼XVVvÔ¨Qááá•••!!!DÀììì3f,[¶ÌÇǧ¯“gc›-ÐÏhƒ±•TJ] !ä¡k¨#._÷°®z÷éÓºú£‰j F³·×®Ý>{WþºZEUmÈеÍ\ÿãl6vëÆ S“I*Jzº:…×9~á¼¹ë\Ö˜šLš4ÁðÉãÇDœ=»wÕ×ÓTUžaeYYYÅfc/ž?§Íš1TCmŠñÄø𚘠­èñÑžY€€233W¯^=ÐYþ_ÏÏ*·¶¶.[¹tæÜ)¥5!e 9e .ÏÿŠ¿•ú¿0 {ñâEuuµ±\111[[Û¬Y³𜜜™3gnܸqË–-=?ãÝ)Á{Ã01 ñ¢Êz­!C0 ÝÌùXHbÖ=†3«—/«««,´kÃ0)ié³Á~9òÕËvó玟`4ÁÈCèÉãG·îÄN26¾ûÛ†uÏ^¾AðÝ›’œtãï;êê¯^¾©¬¬´›?÷ŸÿÜù ÒÓŸ._âø$9UEEaÂ:yöDÏ;ŽÀ7«çGÖ¬ýuÊ Ã‚Ò>uÒ¾Þ¹u‰± †a•••bbb …s¹***oÞ¼ÁK^¿~-,,¼`Á‚~84€bcHóêl6{ûºµcªèÚCd˜,‹Í§¨¤=ÉPwô˜v³WUW‰Š‰ÉÈÊv k>ÕŸ0gh3w^JJ²á„ †Mœd4iÃ,§OÿòåKMm­ŒŒÌå‹!—Â#Ô54ñú¡+á¡ãÆÍ[h‡šdlb>Õ2.6Öiù ! uvuÖÓgÂÑøfõðè›ÍÆHØ—"~ʽ|šµ}“—¹™¾ 99¹–––ÚÚZ¢NII‰¼¼<žÌ¼yó( F‹ÕÔÔìvnÂ0®ºßõ›ëØjúCdê[›¿Ôѵ†P4¥)É)i-­­"""œ5e©ÔÖ––êêjY*µ]ç™ÏùîËÎþ„a½®nåªÕl ÃF•“Ãs ‘ÉÂ"" Â""555ZC‡ræVPðåù³g“'NÀÿljjc0–ýïÈü[A‚qðÒƒ±ð0 ÃH$’ɤ)^ç53Z¸þd¦¼s[¿ÃÊj:1‹•J½zõ*QÂ`0nß¾=uêTâ8ÅáǧOŸnmm]\\Üà <{@€gžô;¬^X:rˆLESã݂ϡ…9ôZ&‹=#0šÍ ³‰W=zŒ,•z=æ*g6‹±1ç_V,°[”šñ"ãÅ›¹óþû꫈_ .&.K¥~ÎÉå ¢¦¦6ÅÌ<15ÿÉ|õnƒë&Œýÿjì¬=|«ÀÑæA§IDAT<­Y½v¶¥m·ü–ÖÎŸæ–æ×Ï>l^·mö¬Ÿ8닉‰íÙ³g×®]QQQt:ýÓ§ONNN²²²Ë—/ç¬öÇLž<ÙÆÆ¦¢¢¢Oóïôà@aaaö?&Ê)¾(+ Ë~Ïõƒ×!¿ÄÆ:›­!.•xófuu5É„×ݵ{Ͼ=»/†œ/))©««»}ëæ¶-›ÙV__?JW—D&ùòåÞ¿ñÑñG‹#RZâ´ÜÇû_¾|a²X/^<§××ÛÌ›ÿ455æjtkkksKsJJrAAçµüzØQä#GŽ´[Iƒ_tt4FlñUTTž>}:kÖ,üO!!¡ŒŒŒŒŒŒììl{{{^sÙÚÚFDDt?×Þý‰[µjÕ‘#GŽ9Âõi«4mݺu݋̟¯¯ïæÍ›{¤Óþiןý WÚÕU¾O¾+=?-¿vٖ͆óòs ÛØ-ÙYŸkŠZ›*Ps•§»ÏO³ç°ÙìvõW®\yüøñcÇŽ 6læÌ™ŠŠŠwîÜçx" ~ªÿäÉ“cÇŽµ±±©¬¬ìa†|šßéN5½žÞÐØø2ýzaÞ$‡E‹W­SÐý±°Îb³t1¡G÷ãDDÅ8÷ð/q:vâÔ•ÐËS&N62 »|Ñn±#Ãö߸ÖÙaá¼ýûöXMŸaXÇ}{"¥­ÛwL46™oó“îpmÏíÛZ[[å/GF‡]¾h 7rÂý€cþLóßã _ÑÃ÷‰°»»{Ct‰¼¼üÒ¥KüñÇììì¿þú«©©©?—Þ§ètúãÇ ð?Y,–‘‘Bˆó¹@pýߟÁÁÁøDRRR-¢ïtÚ?íúó[Ÿ;€v:Ýyî‹Åjmm]²Ø©®®6áY‹Íüm›‚‚™L*))å¼?ßÎÎÎÎή]&¡ƒÓ$)((ˆóÕnã3;†:éFŽj¥´6mí ûáf[›¢’òÒÕ·Û4ODTØLfdziÖ³iÖír°™7ßfÞüv…ŽK—:²¿ãå""¢^»=¼vsVÖ=&<úZ»‡þÁ¿x3ù¼*ˆ~•PGGçĉþþþçÏŸ733;uêÔÊ•+û3>ÕÔÔ´mÛ¶ÎâÛ1xúSHHhÓ¦MŽŽŽâââãÆóöö.))褺lðô'èO½ut€Á`üìàÔÄ`ܸƒb0d2YXX˜Åb±Ùl„þgï$ÝuüŽp^}Ä,•J¥ÊQåäÈd2Bˆ¶rÅÝóçëÉ¤ÝÆÆ=?3ß×Ƚ;*aNNŽºº:>mooOœ|“““;sæLJJJBBBppð˜1cþf2ÙÅÅ%>>>11ñÌ™3ÊÊÊxù!C$%%}}}ÓÒÒ=z$!!Ú¾}»ÏƒêëëïÞ½+%%…/NZZúÈ‘#™™™÷îÝsqq!® ç•BHKK+44ôùóç‘‘‘ÚÚÚüóá•?\ãóÊ',, ¿˜9''gÚ´i—’’òóóÃÛ;iÒ$¢¼«ýÉ ô'ÿü»ÚŸsçÎ¥Ñh³fÍ?~|ll,‡+##£ààà””ss󸸸û÷ïヵ6ìðáÃñññ™™™gÏž•——ï8ûÊ•+¯_¿.%%Å'ÿ®âÕŸÑÑÑFFFþþþ™™™ñññãÆÃËy­—£GúøøàÓ;wîäzÿ/‚´‹×zá•goõàODDdÈ!ŠŠŠ***Û·zÄÇ& 6LYYYUUUMMMMMMYY™J¥Š‹‹t¦Ü 2*á£IŒ& þƒœœ< ‘ðÂEN˽/‡»®£3B þÓÃŽhTÂ7655™››[XXµ¶¶âåëÖ­³±±±³³333KOO?zô(1‹¬¬ìÉ“'ß¾};gΜ+V0 yyyuuõÔÔT„‰D1bDMM þøŠ}ûöÉÊÊZXX888Œ;V”h4Ú®]»&NœøîÝ»cÇŽñχWþ]ÏË’%KŒŒŒŒŒŒÞ½{'Hò>>>T*o/ç.b—ú“O|èOþù£.ö'™Lnmme±XL&3&&æÅ‹üó133;yòd\\œ¯¯¯““›Í¶´´Déèè$%%Y[[#„¶nÝÚnFGGÇ+V466òÏ¿Køôç¡C‡¢¢¢,--322<==ùÇñööž9sæ¤I“ôôôlllöîÝ+`‚·‹×zášgoõÀ·­‡÷ó— ‰‰‰IIIÉËË+)))**R©Tiii111!!¡ÞZP“äª'{ÜJ*ª JJd!¡Þñ觇o.ü(++›6mš‰‰IzzzJJ Q¾jժ͛7×ÔÔ „‚‚‚~ûí7)))|€B¡ÄÄÄܺu !TUU…ÒÒÒzÿþ=BhòäÉ[·n}ûö­œœœ¤¤äüùó'OžÜÐЀº{÷®““S§)æåå!„ÂÂÂ~ùåYYÙÚÚZ^ùðÊÿÂ… œç‹‹‹W­ZÅ'¾ }Õ)qqñ… N™2¥c{»ÔŸ¼@ön"„nܸa``sòäÉNëeffêé鉈ˆ”——çååá×õñ%Ç/‰äååwïÞ-''÷æÍ!!!ÎÛÌÌÌìíí1 ÓÔÔÄèÒiþ½‚¸î£¢¢BLL¬ÓúOžÁà["##óîÝ;]]]‰4йô!6›ýþý{üœ\;£ÕåŽÝ}¹ù'!!¾ÑN al6ûؽWšr= óŸ£­­­ EEE!QQQ¢œÍf‡‡‡‡‡‡KII¹¸¸œ;wnÊ”)t:½®®ÎÙÙùÇ‚,©²²RVV!$))©¨¨¨®®njjzçÎŠŠ III …BœŠì4N¡Â¶¶6^ùpÍ_œ9ã ˜OG†áWÚપª¸¶·«ýÉ ô'®·ú“PPP°k×®>¬]»–ÿÑ^Ž;ö÷ßãÃ@Íœ9“xIGGgÑ¢EšššÇŽ£ÑhMMMÝο]ÿtŸõ²zõê—/_êèèÌŸ?ÿúõë†êÅvµÓiœž÷Àq,À§¿íïÀ7fĈŸ>}ÊÌÌį¸üVQ©T™#Ft|iãLý?âÞxF¦=ϯìÿÄú¡¶‚¦Üúéú]š ÿ^G|»ûÏÑœœ{{ûììì1cÆ8;;?óðð¸sçΫW¯óóó‰‹RBBB|}}7lØPVV&##£§§Çyy;:::!WW×-[¶äääìß¿¿¼¼¼¶¶655uÛ¶m{öìQTT\±b‹ÅâŸBˆF£%%%µ¶¶zzz^½zµ­­O>¼òçƒk|>ùðQPP`eeuõêU™ªªªêêê§OŸrmo—ú“èÏÞíO„Ю]»ŠŠŠâââX,–‰‰Innn7‚ „455ñA\Õþ¯½;‹ij[ã¾, UJ‹p"œ€!^_îÁ@ "“1<!F bQ¹ À€ÕD¥hPBp(¢H1F§@Cá\ÄTDeÅ–Þ‡ôÖ¶{S ‚Âÿ÷`6«‹µ¾½–¤»_÷Zû?>¬ùÒ­[·Z[[[[[«««“’’ŒŽ_k|Œˆ“n^ìíí8°sçN ‰D"—Ë© ƒY}ÚÝݽ¢¢" €ºŽ§TVVÆÆÆ>yò¤²²rÏž=QQQTùÅ‹ËËË e2Yaa¡ COÝÝÝ&&&666ÕÕÕ‘‘‘éééeeeoÞ¼!„ÄÆÆÚÙÙÕÖÖfffVTTL!¤¡¡A"‘Èd²¡¡¡””æxèâg ·}†xäääXYYÕÔÔdggSßÄÆÄÄØÚÚRç[\\¬®9­ñd€ñdŽºÄb±ƒƒƒT*---mjj¢VÄ!%%Åßß¿¢¢B(Þ¸qCó%u†E(zxxPO0.~Ýñ™.½óÂf³E"QjjjOOOsssVVVff&õ ³x^º˜Û™ù8îXT–}in„M›6ÅÅÅ¥RéääDÉËË›•.Ÿ={ÖÑÑ1“Ftã¤Cÿ¬Ÿ×b3+óºŒ{r!ÇûÁÀc ‚   6›­P(æ1.—û³»Ÿa#s'0›•yé³ZéóÕæFdà—¢¾ƒ€ÝÚÚšššJýÐÚÚêîîþáÃBH`` ŸŸß¾}û!«V­ºÿþöíÛ}||ÆÆÆ|}}ÇÆÆèÊY,VDDDPP©©é‹/Nž<ÙÝÝM‘J¥iiiÁÁÁžžžýýýÇŽ{þü9Ç+))ár¹\.·®®ŽŠÄÙÙY©Tê ÝÎÎN*•º¸¸LLLPg"“ÉBBBÞ¾}Kw¶„ñøñcª\o<„>ŸŸ––¶qãF…BÑÜÜœ™™ÙØØÈ§­­­@ prr277¯¯¯?qâDOO³B7n'%%ÅÛÛûóçÏr¹|Êvèê3Ì‹D" pttlii‰§sZóh\¿zÛ!„x{{GGG¯]»¶§§'//ïöíÛ óH×¾ÞyTGUTT4>>Ný]š¹ÝÁ?ÿýg|||>ãcµ··Sš‹ Øþ²¹¹ùåË—ËËË322V®\966ÆP.vìØÐ×ב‘‘±wï^ªþÙ³gO:•”””˜˜¸{÷î¾¾>OOO77·äädCîØokkkllôõõ½sç!ÄÙÙ¹««‹!5@ ¡îÞ½«õ’n<„#GŽŒŽŽnÞ¼Y©TªÓ qÚÙÙÉd²ääd…BqåÊ•ãÇ'$$Ly"ºèÆM(r¹\www‹•––6e;tõæ%,,,66¶³³3))I$íÚµ‹¹¾Þq3¢_½íxxx¤§§GEEÕÖÖZYY©G›néÚ×;6›½~ýz…BÁb±&''§1I‹õò׿ÿšï@`F´ö“24;`ffVRRB}*Óüb\oyxxxLLL__!äêÕ«ÑÑÑgdd„réÒ¥êêjBHqqqnn®qç ‹•J¥ÆµCOww·———‹‹‹\.¯ªªš²‘GB–.]ú矶µµmٲŸ`ôŽ›R©ô÷÷wuu&„4¤ÜÌÌÌÂÂâÌ™3êËÁÞÞ^.—K}TûåË—¥K—Þ»¦§OŸ&''ÛÛÛwttxyy …BãÚ¡‹'77wppðèÑ£6l¨ªªºpá«W¯±°°HNNæóùMMM&&&Ë—/7"ºqãp8ããã]]]¶cmm­·>󼨵´´(•J7999­y4®_½ãokkûòåKÏ—¡}æyüúõ«]….Y@½¨—P…ZY†9 `ÑÓºcøñÿûhÖ˜˜˜X½z5µï€©©©qA  DDD¼~ýÚðßúöíŸÏ×û’J¥211Ñ*ÉËË –Ëå2™lhhȸPéLNNJ$‰DÂáp"##¯_¿îêêʧH$ºwïõ?''§­[·2ǯ·œnÜø|þŠ+ÌÌÌ ¾§§Go}çÅÚÚšòþýûïß¿Okgد¦ÎÎN‡úúzC*3´Ï0„>Ÿ?99Ùßßo`T‹“wsäÇ·Ü;0¦¼w@·Dóøxw†„:@öIEND®B`‚fwupd-2.0.10/contrib/qubes/doc/img/uefi_ME.jpg000066400000000000000000002464221501337203100210370ustar00rootroot00000000000000ÿØÿâ øICC_PROFILE èmntrRGB XYZ Ù$acspöÖÓ-)ø=Þ¯òU®xBúäʃ9 descDybXYZÀbTRCÔ dmdd àˆgXYZ hgTRCÔ lumi |meas $bkpt ´rXYZ ÈrTRCÔ tech Ü vued è‡wtpt pcprt „7chad ¼,descsRGB IEC61966-2-1 black scaledXYZ $ „¶Ïcurv #(-27;@EJOTY^chmrw|†‹•šŸ¤©®²·¼ÁÆËÐÕÛàåëðöû %+28>ELRY`gnu|ƒ‹’š¡©±¹ÁÉÑÙáéòú &/8AKT]gqz„Ž˜¢¬¶ÁËÕàëõ !-8COZfr~Š–¢®ºÇÓàìù -;HUcq~Œš¨¶ÄÓáðþ +:IXgw†–¦µÅÕåö'7HYj{Œ¯ÀÑãõ+=Oat†™¬¿Òåø 2FZn‚–ª¾Òçû  % : O d y ¤ º Ï å û  ' = T j ˜ ® Å Ü ó " 9 Q i € ˜ ° È á ù  * C \ u Ž § À Ù ó & @ Z t Ž © Ã Þ ø.Id›¶Òî %A^z–³Ïì &Ca~›¹×õ1OmŒªÉè&Ed„£Ãã#Ccƒ¤Åå'Ij‹­Îð4Vx›½à&Il²ÖúAe‰®Ò÷@eНÕú Ek‘·Ý*QwžÅì;cвÚ*R{£ÌõGp™Ãì@j”¾é>i”¿ê  A l ˜ Ä ð!!H!u!¡!Î!û"'"U"‚"¯"Ý# #8#f#”#Â#ð$$M$|$«$Ú% %8%h%—%Ç%÷&'&W&‡&·&è''I'z'«'Ü( (?(q(¢(Ô))8)k))Ð**5*h*›*Ï++6+i++Ñ,,9,n,¢,×- -A-v-«-á..L.‚.·.î/$/Z/‘/Ç/þ050l0¤0Û11J1‚1º1ò2*2c2›2Ô3 3F33¸3ñ4+4e4ž4Ø55M5‡5Â5ý676r6®6é7$7`7œ7×88P8Œ8È99B99¼9ù:6:t:²:ï;-;k;ª;è<' >`> >à?!?a?¢?â@#@d@¦@çA)AjA¬AîB0BrBµB÷C:C}CÀDDGDŠDÎEEUEšEÞF"FgF«FðG5G{GÀHHKH‘H×IIcI©IðJ7J}JÄK KSKšKâL*LrLºMMJM“MÜN%NnN·OOIO“OÝP'PqP»QQPQ›QæR1R|RÇSS_SªSöTBTTÛU(UuUÂVV\V©V÷WDW’WàX/X}XËYYiY¸ZZVZ¦Zõ[E[•[å\5\†\Ö]']x]É^^l^½__a_³``W`ª`üaOa¢aõbIbœbðcCc—cëd@d”dée=e’eçf=f’fèg=g“géh?h–hìiCišiñjHjŸj÷kOk§kÿlWl¯mm`m¹nnknÄooxoÑp+p†pàq:q•qðrKr¦ss]s¸ttptÌu(u…uáv>v›vøwVw³xxnxÌy*y‰yçzFz¥{{c{Â|!||á}A}¡~~b~Â#„å€G€¨ kÍ‚0‚’‚ôƒWƒº„„€„ã…G…«††r†×‡;‡ŸˆˆiˆÎ‰3‰™‰þŠdŠÊ‹0‹–‹üŒcŒÊ1˜ÿŽfŽÎ6žnÖ‘?‘¨’’z’ã“M“¶” ”Š”ô•_•É–4–Ÿ— —u—à˜L˜¸™$™™üšhšÕ›B›¯œœ‰œ÷dÒž@ž®ŸŸ‹Ÿú i Ø¡G¡¶¢&¢–££v£æ¤V¤Ç¥8¥©¦¦‹¦ý§n§à¨R¨Ä©7©©ªª««u«é¬\¬Ð­D­¸®-®¡¯¯‹°°u°ê±`±Ö²K²Â³8³®´%´œµµŠ¶¶y¶ð·h·à¸Y¸Ñ¹J¹Âº;ºµ».»§¼!¼›½½¾ ¾„¾ÿ¿z¿õÀpÀìÁgÁãÂ_ÂÛÃXÃÔÄQÄÎÅKÅÈÆFÆÃÇAÇ¿È=ȼÉ:ɹÊ8Ê·Ë6˶Ì5̵Í5͵Î6ζÏ7ϸÐ9кÑ<ѾÒ?ÒÁÓDÓÆÔIÔËÕNÕÑÖUÖØ×\×àØdØèÙlÙñÚvÚûÛ€ÜÜŠÝÝ–ÞÞ¢ß)߯à6à½áDáÌâSâÛãcãëäsäü儿 æ–çç©è2è¼éFéÐê[êåëpëûì†ííœî(î´ï@ïÌðXðåñrñÿòŒóó§ô4ôÂõPõÞömöû÷Šøø¨ù8ùÇúWúçûwüü˜ý)ýºþKþÜÿmÿÿdesc.IEC 61966-2-1 Default RGB Colour Space - sRGBXYZ b™·…ÚXYZ PmeasXYZ 3¤XYZ o¢8õsig CRT desc-Reference Viewing Condition in IEC 61966-2-1XYZ öÖÓ-textCopyright International Color Consortium, 2009sf32 Dßÿÿó&”ýÿÿû¡ÿÿý¢ÛÀuÿàJFIFÿÛC    ++&.%#%.&D5//5DNB>BN_UU_wqwœœÑÿÛC    ++&.%#%.&D5//5DNB>BN_UU_wqwœœÑÿÂÆ"ÿÄÿÄÿÚ ìÓ4‚‚Â,²¬R ”RTŠ• ´€X!H¢Š"ÂÁ@J.mHÂ,°X* P(–*Å B" ¨* ABX“DP,!LÐJ °) lÌ(„ª ”%b 3f‰BQe%’´¸*È, "Ê(%3¸ "ˆR¸4 ¬ÒXJE$°¬í3s¢X% ¬c“ ¨.6@E¡‘K¨†¤°DD²€e P,€&„€Q`!¬Ú² ÉAa4…DViPT¡R !`&¡&àXK4fÊj\‹)*…2i K²äjX.tB¢(–›UKË èK`-D©šÆAbÔ²‘aA5‘¨‚À¨T¤šo"È“;È¡I,¤4qéFinlMfÂËeÉj\‚ÒJ"€Ô±BPP¢‹„PB ‚Ë,‚¡l”YØ," ( š°, R   ©H¢(–CRÁe%€°\‹(%5(”%”`X,RgAC4"®u’Ù#Q¹,Ò¤ `±Q›V%IPP–isP²„ÖQUrXK*[LÔ-€Î“;™5r5. ÄZCY°° žL•R†¢Ê‘D X @ JPM@XPX J i%*RP¢E±W4Ä A@€APX…@²ŒÑ.²–™ MMdR(!¬Ñ,¢P”UÉe³DJ¹±`P Z„% UdQ*Ì–Ü”†³¨\Ò+©ro6%si*ÈX‹7!f†ušfÐ:…’”Â7‘¬…‚—1b’ÜÖ™h,Eˆ¥Q*-AR¢Q*-2š”J†²¤ °‰@¥‚Q* ¡,%P”%…A@怊`¹¢ÊHÑ•XЊ+!¬……!@Ae DQÉJK(\•`e‘5QY ¤ZB(!¨•e a©¤XXX)¼Á¬ê‚ÈU„Öt¬©T«&„)&¡( H!V³‘Zd) I…€ A`…!nEA©)`X $T  ‘R‹•jAl†™F™)ddi@f–â”…–!PX† (‘aP*äjAl† .hJR„¤PŠEÑA¨TgRÑ, ,UŠK,EÜ…gBU"‚%’‚ÀJE ’Ù©D(€¨ cr ‘-¤²PX I`*KbÔ$X!@!PTJ· APd¢Èi‘l…@°T†(RJ€¶M†’5HP.F¤¥’Å€²¡š’BÁÁPX !e…”J…PTPTKU”•P(°k0i‘©( ¬5´J™Ò gV„¨T P´·‰9/ŸEÐ÷CGvô‡vtéÛ½1Ûunu¤vQÚaÙqÙuGa×—ZGju‡iׇ֙fõUÚuGiÕGiÕ—U]§TvQÚuQÚuGjõiÕWiÕ±ÚuGjuiÙ½aÙrö]jsÞµNwì:ã°ëŽÅëŽw9§ 9œæœTä¼#™ÃNW9œEåpŽgå¼#•Ä9o9\C•Ä9\häqŽG6â·†×#Œr8Ñ»Çgå¼#–ðŽ|ñiÅN\ñŽ|ñgå¼ çœC™À9œ)9çìNs¸;€vqÎà‰ÃwæpŽgçœPæpŽkÂ9\Pæ¼æœC•ÅMï‚Î!Í8‡5ëÓšqCÓ³:ã±:ã³:Ã²âæ‰ËLèJ% ²6[Ï!ãüÏk§¸”€ ) E€R¡@(!@¤-EDZET)E@¤QE¡–†Zhe¡–†ZhEi‹¡–†Znhe¤e¡–†Zhe¡–¡–†[la±†ÆfÆfÆZj˜œƒÉ#CTÃcAÆänAÆänAÇwN'(ârÓ…Ì8\´ásÊŽ'1x\Ã…ÎN8àsÚàsŽ:8ã…Î8ã¯yÇžw`½nÔëŸYÜùO«ÍÁ3tqM0­®±¤ø^>N=å560ä'q¹tãr7$0äwcŽìa±ÆänA†Õ‰È8܃È8܃È8܃ÉN',8܃È0䆧’nרÃc Œ] µL6L61t¬´$ÐÍ¢(Š"ˆ¢(Š%¢P ¨–È5 © 24ÈÓ#LŠÊ6ÀÛl °6㌠Þ1É1F#Œr8Ç#Œr¸©»ÆŽGä¼C•Ä9\c’ñWåqW9/åqW9\Eåq[ÂNkÀ9ï^œ×€s¸)ÎàÎs8G+ˆs8¸ÎÖu¶ø¶šÅ‹H€M5¬Óáq¼o:‹²Ä €X@QDX(P (T(´dE€”E€A ` €@,@(TÁeA(¨R€ TJJ …¹¢Ê,¹f–Áz=î‰Ü͉Ãößö“X—8¶jU•µu§Âñòcy,7e€@°€X €  RJ RR”Š"À‹ @ ¥€( @©@ %X¤(-‚‚RP PPÁ@””€¦³¡ÓíõÖ98“_aòŸYf&m”I¨éuYðÜ|œ{Í»,PX‹@Ê @)T¡( RÁBPÔ A`PT,¨”°BXY` `X–*„@JÊ@ÄP,  Áa( J ,(¤¤XÑ5:½ž±Ùâåâ;?WóM›šÎmJg@dDi½cuðÜ|œ{ÁQ»,²€%€XP , U(eT `)°XPµ(– ,–%’€†¤¤ DX (!AR€Q‚’P,, (T   4JPN·?sðv:ç¡ôŸ=ôYÖs¾9uX‰H£ŽÆ®µÇÉǼÉbJ€€ €©@€¡– , ‚ŠRT,¢YD¤€RÀEPŠ @IK¨*PH« ‚‚‚ˆYE‚ÜÒÀ ©T€ ” ‚`°#Ô®ÇW±ÖOgÛñý¬ë-g75D°+ŠÅºßÓâ8ùx·€9À …•`,¤ ÊQ(@, @…– @Q@ Q™b€T@%€ D€@ŠÁ` ²‹²­J -ÐP¢X¤ J«J@J«)´äëvº‡½ìù>¾5Ť•© E2ƒ unñ£â¸¹x·€7I( B b¬X‚¥%(‚ ¨, (@R€((° @ ‚‚•  *"Á,A,@XPŠ`¨*"‚ ¶ eŠ ”¥ (” ”… R(ÉsK898¹õ{]Cèý_;ÐÆ¦w%J"Š…áFšÞ4|_/ñ¦FìI`PP²,¢PK (@`P,()&¡•Š€±P– `¢R€T(²ÂÁAlEMXK(²‹,R‹J (,-!¨Á@aÖìõ{&º¾¡õ=î§sÂųY‹`È8¬jëXÙñ|\Ü;Å–²È P ),¡PT@QU,YPT UJ( E•BX¤  @% aR‹*%‹%„ ¤  ±…²‹,5(ÊÛ€EJ€T  BŠ‹BT¥¹ÑÓìõ»&º}¾¡õý®lk2ÅÔ"T*‚ÆšÖ6|g/ñA ‹,,¤ J°,T PÁHP,UAlÀ±U*P¤DQ(EDQEQ(™©,@ ¥‘R€,JT¥±A ²”BËKDPT  …JPT¢Ê,J5štû]Nå:¾©ö<Øäç¬k%YC(Ð:éu«¼é>3‹—‡x¡4PB¥%*PB€*À¨* ”*P,€Q`¢€©E•¢P‹IhS—¶yóÖÙã=N¤äÉ•š„š„P)3¬,X%D X*P P (J[,,-J‹@°P,J`(,(²>ßS¶^¯g¬¿m¦q¢ ‚¥Œƒ¯SZÞ¸ùãxy¸wŠ„Ø(R  (  ¨*P, ”(„X) B¥ X*RŠeT`¥%º3}¬—á=ß¾²üç«Þe( Åæúäù ôÅ~7ŸÖ~cYøÉÚëÖ„¡&²Ia,Y ` €€ ±€À€P[AAR•¹ °€  ,¢ÁR’€ AÓíõ;cƒ›Úñë8Ük%¹ë0h½hjµ4|w7ùØJJD(T °Q@J‹…, …JPi-,¢Ê Ñ5¯¯_íý»Š8c™ò_5gè¾GçóSëú_8³ÞžúÇ˺õ?2²þÈü‹èeûÇ•êæð|Û«ñÎ?Ôÿ>Ö|Ù©Rk)™b ©`–BX”EŠ%D ,EJ(ADP(,  EA@²€²€X†™(– (AÔíõ;fny°ãÞ9îMBÆŒ7*N¸Ö®±³ã¸yx·Î¢¶$ ¥ €©B ”¥ X* ””E€( ‚  ÜêQe(¡Ùßéõ}ÃÇåüŸIò|3¦l€…°* ¨5qNOsÀ§êžãß]›ö|{¹¿üïì¿ ¼ü¤ÔÔãš’åbÉD–@ …K ,EÊ, ÁDT¥AARÀ (RÀP%€ ®tJ,nÇW´c±Öî/Ôg|xÕfĪ1ªrëWXÙñ¼\¼[æ  XÁR€¨T´(²€,µB,¥J[4^×é2ö=ò}OÞw‰5,%@‚¥ ·4Ò+ZÂ=ßÑô¥ýMÁÏ|Ì~ÇùÆñàã“™¬¬XID%d€BÁPP²À¢ÁniR‚Ál((ÊÁe€ °iXu{=~ÉÅèyÞœ¿I‹1¬è.u£«¬Ýjï>7‹—‹|Á7, €   J‚   ©D Ae¥šIA©KÉy}ï«9ׯzžêH›ÊY( U @,T@lUuŠz¿§~=ïKú?_°Åü—£ú_æÝ3Ç5“+€K²’ˆ  ”ÜØ©Ua)a` ©J‚‚¥¤Š ” ”€M3 wÆpözý…áõ|¯b=ìV5±bÐuYjòk‹gÈqrño™) €@¤X(, P €²€•l¥²¥³KËú§ÉýÖ+¥ÝüÙ<ަbB ¨@²ˆ°©h ˆÖøô}ßÕþAú¶5Ùüçôo9?(Ï/æ&²±a@‚YA` ,KÁD,ª²f•(°” ( ,¢XR…¡lã1ÏÃÌp{~'½/±5ÇM¡stFjåUÒ*·Ÿ#ÅÍÃӚʚP@Q,%P‚X°PT@Š€PËZ*^^?¢—îûF5áþkìxÝ9¥”–J-Ja@”E²A f›úo—å?buûõù×ÍþŸùŽó‰e²X%‚X%€€KREJ, (²Å°T¢ÁDj)EÀQ,Ð),¥âäâ¼{^¡ùߤW7Õ–®(–°µGBźÖt|Ÿ7NbšT°T¢XÔ  µR€°P°T ,£Yµ»¦¿Hü÷õÌë“Ëõ>>FYÓi.VDUJTEU%D²ŠµEÞ5_eöŸ“þ±ÏSò_ÖþϑΦ™•X B `D  (²€µ(°i,,&’€P(@-TÔK(D~zΗ‡é~gêc¿g¦M5:+®KÇO—âäãéÍe5 È(  % ¥‚¥Y@ BQ`©J–µÉÇÈ{ߤügÙâ¿(ý7ò+oS»¯Ó;9¿”gõ‘ùì¾Yùl÷|=H´Î¯dëýÑû¹¿p~­ù}œ5i}?½—òÉúæÖp”j}2üÔý¡[_Óÿ.û¹~«çþƒ­›øþwÌË! ,P€¨"…,-•"Ù@*Q`¶ ”¨(*XX(* ‹×ìuÎMgKÁõ)õ²öó¬çQl™X°d«u`ùŽ>N>œÖ  Y@(*Q(P   ` X*P‚€‚ÙMrqòŸ£}—êc^æŸuð»Ã“’ßÖ»<üõòºÏìË>ò_[óÿÐp~={žÖóæþ…ÛÖ4ÏÀÿ™ï}ö§çØ?>1úGç¢f¿ý‡ñÛž¦þóá>ökéúÿ/Êæ§\Ë)¯¨ùn_ÒÇ=~;Öõ¼ž˜Ì±`$°KD*  ” ¨((Š‚‚– ‚¨”% @ ”/[³Õ9µ)Áõß#ö\˜Ո[ ÐÌZM-ã§9e5,JJYD ‚ ¨,¢X” * Q@)` € ”‚€Qe5ÉÇÊ~¿Îc_ ò_SòÝ0åâå_×yxù9ëá~Wê>[¦/s§µý…äúÜõçúòz¬òú^ŸÖ×3ÍÅíüS±¹Úûî¯k5øçìë8ïô?Ï?G—Ýò=/Ì¥3¯GÎîKúØç¯Ì¼£ùΘ̱d°@€ ,P ¨…€* "‚¥*X¥%”P‚¥P *P ”X(!IP½n×PçÖt¼gñŸk-³8µab€Da.µ¬Óæ8ù1Óœ…€°`È‚T Ô°P@R‹(ùx¹ön·gà~[ë~K¦Ü<Ëû篂ù¦ù¾˜œ¸÷î;Ç='¶>—² üez~ÓsÌûM1Nig㟱~;¼eU¯Ò?7ý2__Ã÷< ¿8ÎóÒea{ÝJ_Õ‡=~móžÿלÊM"…D)€¬€K)R–Ä[)PT¢Ê,  (JP²‹¢PA®§o¨së#‹íþ'í³¨LÕB˺0¥ò’Û«)òøÞ:sX4R’€B  ‚(€À o ePP‚¬Ó\¼[?Wô¾w豯’ø_Ò¿5Þý~Å¿°zùŸ'ï|Õò¡çòþig[è>fî~Ç5ýäù°w¢àŽo‘ò|-Ï®ûO‰ûlÞÉ`â?"~¹‹?&ý+­Ý=Ožú)"ÏéùÔüè|­Ÿ7îø_\¿rNwò/±×ëŒË™¤°¨, ˆ°P€À¶"‚¥-Í5s¨Y@Q@ ‚`©@…”IBôû3° }·Åý¦u‚fÚÔØ y)mÐ>gÇNu¥–(€XQe¢UP ¤P BPT(¦·Ç³î>Ëó_Ò±®ŸäŸ³þK¬ôy8æ§é\Ÿ™ÙKäüÄ~•â||9ø"Ń~—WõÎßä_Q›ô_žu8u0¯±ûÇ{Ø¿ª?.Ñú{ó*}®ÿ?å¯Óßî_Ð_Ÿìûß‹ëø÷>wè?úª÷¼ïGåó?âäÇLã:Ì ° (,E!@,,Ê[EJ,¢ÊJ(ÊT-J@(^Ÿo¨s®NO²øÿ¯Î³,Í.K`,P¯æ¿=ýŸâµŸ‹›Î¤Î¡%(DX@(ÔÝkëz£fèòððþ—ƒ®fnB¹$²T°ˆYT°°*R¢ ”X*R¢-”©M%‹e*R ©E‚¦‚ ”¨Š* ¶!e @'_Ÿy¸ù8oÞñ=¬k1c:BËÀ¥ñ,i¤;gx, ‚ ©D ”€`¨*€ ”b€A`Y@@€‚€‚‚ÜÓ[ãÑÙý3òÞâþºèwðøý£äµ>rãS ,,PU&®Éïvþó7<ޱùwgÅé™”«„, ,–²@*P”¶Q`¶X¶Qe ‚¥*Q`©E‚¢4”`©Lqrq/7'ôÇ“ëcXYÔ(gKx·4¢¾{:Îð’À,– lE€ ,Õf¡@²€@€ Ê5šoXµßý3ònÌ¿°<{Èüÿõ|YøÞ~ãäw:äËC3C- ´2ÐÍÕ3®_§>wî}î|QçGgó~/¦Yf™¹[,lÀ$°€°€P€,‚¥Yae-Í-ƒIcRR ·#H* `¨-Í* ÒQb(S‹qwÅÉÆ}/§çz8Ô„XVÅ4ŸÍ›À‰@XTD(¤UJ,T%A`T¥A¤¥ÖU½`s}wÆê_Ùµù?Üeô eùOšýAgãYý{ÊÔüÕöÝcäŸJ¯š}GpøËú±/æ¿Kö,º²SÉø{>›á8x÷.Y« ‚! €B  @%P”¥‹`¶Q`¶" Ò ” ©J”¨-‚ÜSW6-ÈÒ$«®>N3껽^Þ5’İ U*Á4ÚY¼±eJAb€(JR‚‘`Q(²ÐPŠÁlµnG&¸©ëýoçºÙuøïµ›úCå=8öêɧ—æÙôÙø¾ÿå| êo…Ë"X€–  °@EJT¢Ë *ÐP€Tl(JTJTJ[)Á¬ê]qòqŸ_ÙàçÆ±BÄ*ŒÑbž¹µ¶UáË5*‰Y@¢P”`Y@%X* ‚¥ , P( ( ©E‚ØK` X5q£W ä×9uÀ;w¦;|C“8†¤…’@ˆT”±Q%`T@@,E²‹ l©P, TAR” gEJuùx¹aá~Ï“:Æ™¨ –hÊåtƒç®ÈŠñ3ZÄBT´APX¤@€²€Á@ A@²Ð²Ð"ÊT¢ÀJT¹ˆX…\€@%€«(–@%E€`%„ ,EJT¥A@DT(!¤,¥°jæ~niXÞ·YIJJ’Ê üÛ+9$•äK5 @°…‚  ° J°(–P‚€I@¡ ” ØJ P¤(* K°@B¢ª ,¢‹°‹©a`¶P ‚ ¨(©`‚Ø*PP sK¬Óƒ›ƒœJµšÆ75 ¡,¤)¿3skQ›<¥šÈ€ U„TPR‰` Ê(PP  - -²‚¢(@ˆX `– €*XX¡%€ D– ÄT¥J²€,²ÐEJR–çG7498ù—ìx÷œi(–" Ó+~^æé¨ÉæËbee…"ˆ °€¨(P@I@(€E`¤  ‰,KR!,À°„°¥¨(*P X*Xe,Qe89ø9¥½ž¯púÌkÐ f’„Rü¢57–N†lÖT(PT‹PZ…ˆ R…%  % *  X-€²‚ J‹),Á(K‚  J €"À°Š  ahJ… ¡* !` Y@R,5sF¥89ø9Éßóý)~›;ãÆ„( "—ç':kƒ¬'„:ó€ ¬ÐD¢PR(”Y@  ég§šß¡7æ]ñëróMuáÓsð\…È K%‚X `@T )E– `Q(, …$QK¨ )AÃÍÃ̹õ|¯b>‡Æ4)›D²‘Ká\ÜïXÞÀ'nt%Á@X  °(°PJR€ö|nLw¿MåNG³ÉÁæKêóõxæý×âÎûüYê\Nçm~gÒó}nþ=ÎÇ?OBöûgG£ô)®>ÿW“\ý;ÛºÇK«ë÷—ŸMÖ³Äáú¾4ù‡½Û³äç7£æ…ÊÁR€”@€°P K)P,EA@J °P,¢Á¨‡7!ŸsÃ÷£ÛγPE*QáØÎµàð Û’Í(,)( @¤ P¤°(E”( 5š©@ Gg}9žÏC ¸²,k#X‘Æšä¼%ç½bö§T¾Ç™3¾Ç¡ã¬îtñ Ì (€‹, @  b(Y@«‚e ¹ÖLòqòF~‡ç¾ˆõó¬ãIBP–xDÏMãXOYÛ“YР,@@*K) @,¤PQ(/7§Ôq÷ñõ}é7á=.ž^£èú8ëåsú]ƾm}­ðñÞuòï¥Æt]ÂtÜÝ›ž‚vµŽ«Ôég§MsÉS$°,@K • @@ ¨*RB¢È ‚   ˆ¤(P”ePFuÉDú_™ú˜ôq¼g@P/…+;Ö.SÁ–väÖt[,T¤UEDQEE€!` Á@‘DQQ B‘DY@ E/cÑñ§?O{µã¦»Ž£\ýmxìvö÷àÖßGóÆ{œ¾æý®ßÌjoè·ó|‹ëñù¸žÇ‡Þ×?[­çv9ú}WÎöf½åêñuòwõÍÒçëçâÞÎ}uYòý¿é÷ÏËéöý ôó±Û³§/ì³~o³ëðï‡çý¾K/ D  ,(H€D°PT TPT(°T¢Ê,ã5¼j'Õ|§ÖÇrYG…¨ÏKe< gnMM ,¡((X¤PI)@ P€KE€…`”QEK%—‰.øµ,’Á¬Åírôë»ÄÖ{ê%æßTÍôJâÀ°!`,E‚€²„,Á@°T (…ãßɬê1õß#öRóçS6ÀÅFw®>N3Á–vâÖvYP€Š€¤ )( E•(”¡(*Z@ ED”IDš„– êa%€(@BY@D ¤°P,,¥Ax÷ÆrÙc?gñŸm.ófjXRQ d ÏMqò`ðaÛ‹xÙDT €)Á`Tæ•(°”¤­´@-JDUE@K‘DQ²€P’„YEDP@”EQIaI`–$D!R€% X±,E²€, (®.N3”î>î³s,Í@B¨_YÝâæâ<)gn-ã*%  H ` ¶]ΗÞ'Îô¾Ÿ½ÓŸ>·Îšðß]ì=¾”½ úVçáÞ·Zk¤×b^³—VQ±DQ»³®çW šN\®ÈA@@AŠUea&¢fQI`–"QT %€JT EDXPPP[ ÁR­âåâNk(ûˆûŒÜK™lX€JW ñ‘듈ð–vâäãä(€PRPKH¢, ,*P e/Ùü]³Ÿí>Üþ›4šÏê„Á÷)ÔÌ×ß|çÆ~ƒÃñþŸWÒùÐnwãt¾z_Ñ<¯VÏo“ÍæN4æËö¿ö2âk9¥€6 ‹+;¼|œG‡,íÉÉÇÈQ– (”K@‚€ E([²…i›DÔ¶fÒɪbèbn˜ºhe¨k|Jåœk9jä¼K9µ×ºvuÔYß×›lõ5ä¬öoŒÖ}«â,÷&µ=™äjÏVùK=käMgÚ׈³Ûx—YöÞ5¹õÞVî}'Ÿn} кǡ¯6³éëË\ú*YêÏ,zS¡NìêSµz¶^Ï›Úó<þ¯Yó¾ ;w©ú7E>G‡ì{G缟kôä/Ö?=›ªž·ôgÈB¨*"À°*Q(@,P`¨) (Š$¢((Š"ˆ£6ˆÐËC+N>I¡,>Ç›“ˆ, ”…$X éõû]\íÛ>s=þ‡\•AR•€)5X*EÊ~‡óGÏ®œý­]Ž/K¤½ŸÙá“æ>ëå¾”üÍìø³( ÁPT @°T¥k¼±¢ ÍX3làëó±Ó©xºÏwÂçàÞpÔ¹Š"À‚*PX* `¨* ‚ ¨* ‚ ©@ ,…B€‚€›“ª·L“è¼>ú½ÿ›ç¥ò%úÏ’±@œz2X%”  %* `²€ˆ¢(” ¶Î³D JP½o ¯ÃEšÈ DX%a&°°T`A`ÁP, APTA¤AY[‘P[‘¦FT* @°T ¡²’((“˜û–=<\ÍX…XK`BüpÞ@JJ%”–R(‹¢)"Â(“C- ÍŒNAÆämŒ68î†nhe¨E APTAR‹AP[‘¦F™`m»ÇM°6ÀÛ‘¶ÙdišThe¡–†Zha±†Çbla±†Æ'$0ØÃpËC- µ¢(Šˆ¢(”E ‚ ÒBÜC‘APi‘¦F¤¥€°zSäúÒâ\ËeÊE!¨€7PT,° K € ˆ¢(“C- ÍŒ60ØÃc±†Æhe¡•„(– APT¹f•¹djàm¶#Œr07xÇ#l Ü ±M\ ±M24ÈÒ ‚( ´360ØÃc Œ68ÜÃ’nAÆänAÆä†' ÄänAÆØÃc Œg5ÇÈš…”F†.†fÆíû_5eû<üïÐË.°k4\Ù P|pÞ@(ˆ¥%Ie$(”"ˆ¢€"„´ÌØÃC- ÍÓÉ 619’la¡@‹APT™dm¶#Œr^!Ëx‡-áÎsN!Ìâ®1Êâ§&¸G+Ž›`m‘¤ la±ÆänI.Nä@ŠXÈ5¢îtáöyò½|Ü `¾8j,ï'[Ðä<—{¦e¡™¬^ùçÎÖJD!Ydi(!eDQ%DQE¬€a@ €¢Q)–¡la±Æäsa±‰È8ÛlbÑ–†TJTPTäjàmÈãŽ1ËxG;€v'äÄÉ¡,-h%”¹hK5õ%ëKígy–@¤(’(…!s`¡«ÉÆî~?{ÂN<´RºáŽ»”âspŒk&ˆoÒóaÛ—¨@3¬—P;nÙÀìðEaSDNñÑw9=D¬›gA!£&™di(€¤QI¡E` D ¡,¤Q(%DQe¡–†œƒÈ8܃È8Ûnhe¡™¸fØo||‚ ª$ÐAl `R-°}n¼on^5€ AÇÓY”P79NKÄ9c—ØðÇ©ææ“V*⦙÷¼ßédfÐJ gG±ævz&"šÍXòç¿ç Øj\—·ÕÎO?ãräÄßÔCÏå;ϲu ãc.c±ÅÜí/¢EXÑLwvyïSÌ%ÈÓ#@3ÊbÈiŠi‘¦²TEDheDQEDQ, (!Pi(` J P‰\ `@×Ö|—¯/¯žN<Ñ(R²>Dj% @¤RE€r[(fá–†ºQÆÜ2ЋE ÖM26ÀäqFM wúéŸ3N^¾á¬jž¯K¹Ö8úݾ˜ÔYÐåáîvOš}Ïœ{Ç!˜¾1Ýé ë&ñ¬š÷<>C×ÇO'U}Ξ±u›¯é¹õwzBQ70ièSÎz{<¤‘²˜å2Ö L26Í* ‚  I43hËTÃpËC-Ã- ´0ÐËC- ¨‹5%äj P&Ž_±ø¯µèø©èt0\j,h@TÁ~¹åú¹d,)ò#P€vLjÆ¢¨@L†€€ „6â„Pä5 –ƒˆ3Èz Bä4=Na{Ÿ2B–ca9åá èVDíqo¾Kâd±ÞB{ŽB^/4° °Ìü ì: ›ƒÐQAP(, R 5Æ‚÷ ¾»áÃ0ÃT_©%Ì%´2ÿÄ21` 023A!@Pp"4#B$5CDÿÚúÀæ'Ç.I>¸˜ñÆbxó«å ¼{ƒàËé¿<ï?ãà.ÓÓ~¼póÃëË x7ÓíܽÁüQ{/_­¼òÚLO™ãŠ~…¬ù/£Æ¥|è3Ðeôx\ï'Ž?%gì'¯'óÆÀð/N5g}ŠæHÈ/8L±ž™O‰Yà_b¸WT/NgÉ^xØ$dª£8ùàV`„¥ö+€²× ÛàËôá^xØ®fX¸~щúx!ãyà_b¹žéŽÏŒ=½xןØëêÀº™ìË cç‰yà_b««êo·>3Ï}XاׂsGG õÀ°<…yà_bŸ^ éõõÇÔú°-6I3³ãs‰ŒP88’ÃÄ%©?Þ<ðx)ñ/«ÒèinlÇŒ#fC¤&‹‰HB‚àa–²ˆ9Ø– ÿx7ÂBXñ/«Ò¬A<èkg2€”‘5Èvl³ mmž. y3ŸF‰}X“b׈šá\C-…íFíUƒÚq#ù¡ü„PþJ(Ôx'k$":@”“àq´8Q4È›,ÁäÇWñ/«Ò$BgLR’Án!²wj$9ûœ´8´¶›É Æ0ïD+o“ðÎ0zi9…eÖ<<•õ`ZA¶Ôâ¡`Î*RROí0·³çL1óAˆ¶Ÿ,„­1*gM'0¬¡ºø'Ž\JÏÑí4·W †„Lcl¢xý‘„.Ñ##!˜£LIÁyBõã‘àxK…}XŽi¥8¸ht0Œ##éfgíacÁ´êFÐvô¹äŒ”/_5YàZ5)388RaGGOÜÃÄ-…´ú@2˜…²½*y#å ×Áàù êÀ´nÏ…‘aîá¢ÂÚq :Ú]Cí)§4¢²F ʬ#1>%õ`Z2 ó˜GÅZO¼‚в¼ÈGC^oJ+$`± žqÈóÀ´ZHÌá™&Z¼–[qjZ½î͉ÇhCÛwI«$d!ð%YàZ/f1Rðˆºï¾B*aòy‘ÍæL¥¤×’2 œåg£FgÝ–D{ö™ø œýá´Y¡í$¼‘^p™p%}XŠÙÍVþÏ]àÄ+×™ì܇ÒKÉ9g Ópf\KÏÑ[9º!Äc¶˜ø-–ïæ D7mí"¼“WT/lø}\kÏÑ(I©HM(UÏËà˜rÛ…êCj·'4‹€²Õ ÛÆ|¥g¢öz*‰Â%w"0L3ê-Ö nÏ‹meÈ" ìLJà—4„•Ã¤Š¡ô‹€²êµÌð¬ð-²“ê] `B±À수'ámµ8¨X$3„L2JÑBð„m.?¸BÙðÂ$¯D>_ÆCˆšËmpì¥þ"!µ¤\ÿLv¹ªÏÑ;12‡QR…Àƒ‘12ÄÊb:Ñá ·ˆvØH3"(¸óPJ Fì3Ìáû8;ÜÇeöÄwêðìÅIüM+Ò Çý1ÛæxVz(„¤ ÚÊü0 ×hmCþɆâ^lBÆä„­.4¦Ü…‚7”$:êLLRß6X[ª‡…C¤¥ElŸû89× —Úß«ÃrŠÂ0¥¤žÔ×o˜y+< D‡üaÆÖ?ìÀƒ}j÷piÃmhQ)p­-Ü"bÐÁ8êÝT4 ¶†Òâ[LTbŸ<¿¿u`CeöêðÜžÃh¢´‚óÀº›èæ«=A¾ØÚ½ì#¤m^ö Oþlc"Í sQÂÀ Kâ[`ž}ÇÕ ·¦PÊAƒÄ†Ëýq´?W…®æOöt‚º°NhéæxVz(ƒ}±µ{øOHÚ½üB"Ü>/0‡“†±Š&Áš–p°&°DDX+,HlßÕGõxYîa´¿kH+«朼òü+=BS‡X¿³‚ÈmOØÂë˜rþx™‘Tq¨%&£…$bN ÜtâCfþ¨Ú_­Ã S{ ¢õiêÁ–\ÃÈþDÔgîˆ@œá†×/Ãg„\Þsø¸€ÖËB‘ù0ѨÌá# •¥ID¢{g*¶!Éa´Íõt9ÑÁ³ÿXm/×— jŠÂ,êˆÒÕƒyóO#Ñ›1S`m4Nuq>úLDBß^Ñk`Úy#CiŠSØl¾æ Zh;1†IˆÕ6‡¹C ÆnƒhC4Æ)3xYÌô‡úÁž®"ãðz3d¯ÔD7qœr2ÚÒþJ! 7øQü”)v¢‚ÜRψ…²¸x†ßHˆŠm‚~!o+ •ÜáýX/ÕáÚà†ÊD›k¢Hÿ¬ëã.ÈôfÏ]1D·mþsn­µÕ;jZ–x¡jAïq|ˆìHߢFÿ‹}ÔµûiþJ %?“|(øþUḃ! ݶÔrMiô(~¾iäz1µT…Tµ[’þ »a´œ©ý"]@ò†îc>Qäz0†Ìv¶kWaþ f5" U)qfµéÌPÝ|ÓÈônÎv‡ðfÓÿ„š”Ê ¦ÆÒv–t’s”/sšyIÈá¼Ðbë?²Øü°zëÚI9…e ×Íê}ä%G†Ò‰‘i3É8(BgÍÝÁÂÊ""!L6µ•¤Ï$à¡Μ"`Û|Ÿ†u…{&Ú[ІÙÉo]KIŠ‹SêÒËÉ8pÕdzM) —qRR¢ˆÙm­³çLÍšâƒL¶Êpˆ‰m„ÄD­år SÑËÉ9ζ|ÓÈô¡…Ú&…¡IÁm!iwe ÃQ ‰rd†yÀÖÊ 0ÓEŒNÑJÜRÏL/"ÃÌ7kšyifbdØÚM¬Ïl´°½™ a[$Áì§Çñ‘#øÈ[2 É0˜ÁCC·Âüc,ˆˆ×^ÓKóÚæžZ^a˜§Z íDmÖÖ\ÃQ;´X@z=çôâÁaåŽ×4òÓ3³ ÞЈ@ok$&>A/2beÀn4¨Ød…mVˆ/i¾ao-g=<¼|³ÛæžZvbb£V/8+11=D¼|·ÑÇ>#ËìeçfŽžiåö2³À³,¹§—ØÊÏçÎV_cx#>qåö1õ`ßW8òûý`Ï_8òûý`Çsœ¬¾Æó„?sœ¬¾Æ,ð†îó•—ØÅ˜1 Ýç+/±‹0yBw9t¤RŸO±‹|åe£áÛKŠòû$‘J¤™›ÍÚX²vœA$õi`b«œ®Õ#¬ûë}w·to)|ÍÅÓ½Fáÿä6Ѽ$š[²ÒXtü­¤YK(&I„ –Á¤Ýf‡·_îq²@‚I’í*i-ÑÀPJË’&Ôi¡T›NHŒÁ¤ÈHËKN óç/§G¥F“ÞLC¨Ôúâ$²quožñý¥ÚEÔXÞ}—R—Ú3¶Â[@<á•1¿Éd˜€·%¢·Èÿê<á2?Ô‹?ì‹3½g¼–ô’"%~ )Üd‰1ý«šDKÒ©ÀÄ=}:i·T€ãë^z„™Ìç…F&xŒ…j’–¥œR”§¥›ªRÍåŠuJ^ô`¢ ”‡ƒ8‚ JD´¡äXƒËœ¾Ÿ±O"ÀÄO9}:! ©FÃ$n4hã&ÔiÖf ¾rútD?v'¾¯Xe7ÛJI’%9K7SùÐË¡ª ɰÒIÆM!,ŠëC RÐÊ–haJ mH3†p°C.,žm)i-­DI33ÂG¨Œa{\åô膕BÔä:ÇBëK']7&Iq.'y'“YLÌ”Áfûw Å¥(qÃŽ$‰Å‘Ê"B•4<„aHê‰5Þõ8´»“ÈA$H›i÷MŤ’ô5* 2U’PeJI¶ÖÒ7wÚa E36@L:Tm4n,¡È¡ä"Û* Éj5îÎÈá^"ÑÆ 0½®rºt‘]5àFe™˜33 qHšÖ¥˜KëI-å,&%DU¨Ô·ÍJqêÔãÕºû„ãU\J¤†*.6ÒP¤-—ݘµœLª%¸…ÒÑ1U·T9èµ™×9Å%F¨Ä(Õ÷wF˜,<ÃvyÊéÔ¨ZjQ¨øª0Ñ ”ó·(Ò F`ÜY‘<á%+RTN¨–—T—/Ž0XycµÎ_Oا–{\åôýŠxùk·Î_OاÀŽŽrú~Å>ôó—Óö)çç:>¦xn‚ÃßNyÁ9óÜèø}“Ðë1ä ÿE9²FàýãÙ¨ÌÿÄü ÚD2IożÌ:e—á7 Ü;î‘‘¤Ò˪M·¥EÍJMGl[1mBÚŵŠ)P‘ëŸ8#«žçOÃì×›m¥­JT3ˆL$Õ·ÍòÑY)€_þ* ö£cWâ£^}qçùÇ-æöYqoÄ:ÔYþÏíEZ„3Pȉ?ê†i‹Ö«ãh6pöÿå P‚Ä(ÝáÆìÈÝ’7Qº¬nŽ Ñѹ¼7G†êèÝœ,‹b„ŠP)H …ض¡mbÚÅ· V¢óƒ]|÷:>*¥ œÔëª*×I¸á¥ 4)Çâ’t›ñ¾mm›CN]‰õ îÒA>QN%õm„,u r5„4Ü\2ÚŽŠCçÆÐJ™¦¦ÙÔ$I  ,Rà“£ûDݺ.¸/¸7‡ò±½,oF7‘}ë"¨qÿ(¦Zƒ!ï 7fFêºÕcttno Ññº<7WFì±»¨YˆPBÈZŒYX²à²à´à¡b“Ñ~pc¹Ïs£åf|ò3!qBµ ÌV*©"¤ŠˆVB±pÅÕ‹Î Î Î Î «T.‹©[Y°*`P’$(¡b‡Ú*p\p^p_po. éczPÞŒo$7†ÅÖP¢Pbܳ7xA»CÕ¡ºüÿaS 4ãª~öL<ðZÚ’Ë«L¦ Œ¾p‹~ï=Î131R…k.(\1X¨…I@©¤ŠÈ\L]X¼à¼à¼à¼±uBèºBëbë"ä8®Mý"–Å  f-¬÷žÔIZÞ'vt)ÚÙ›5Õ¾#8XuJ'g$ÎDÃ(v4ÝSß6Yá Ýç¹Ñ£ ÀÌÅF+P¸±ubòÅÓEÂ+@­âĤø›y¯âàâS “9 §h²úš…‚Ù×Ö´>Ù½“8/›,a{¼÷:4a‚ù‚3 ffµ ŒØ‡ùÂÆ»Ïs£P‘êBƹÏs£ìRƯžçFr’Ë-ª8_úwfí’„ˆÏU–0yóÜèäLLLLLLOA)_Ú”Øcÿ¼Ñ fê[Iä–òB£e¥ M<dÖPÉh”Ó¤4ª=?ݺª”©f¸wPT*ŠUJRµaë.!N)ÆÔÚ¥DZT±ƒçÌ;Ñî&&&&&&&&'ò>¥©ç®(¢KyÿŒÃ–¦Êí¸ÚáÐû›(· l­&Ë “% ”›Q?¬é“qÓr·Õ[еÕ*Ò¯ÐpŒ ¢l¸”шˆš¡™A¶ââaIº" "Û)Dím6á6Á*+õ4¡‚Æ.{½131P¨T*ŠÈVB¢¨„ÈL‡§¿KªKx‘™`j5 ‹¤%F“r%× §ÖØ[ëqˆ4‘º³sz" =A)ÖÒM-£m÷[4TÊÍ·YßYZ™6ÜQ–ï¥K>ž{½1<&bf*1Z…f+1pÅà \Áp…d+!ZED&Bd&ZÜÁcÑÏw£GÌÄÌTb¥ ÌV¡YŠÌV. Åd+!Q ˆTBd&Be¨Ï‚·Ïw£RLÄÌTb£¨ÅF*ÒçÁ ÚæÈz‡z>¤/€>n×=³Ä\G‹ÝTËÓß1Ùæti©Œôç¨õæ³ÚæÏº4ÖÏýc·8˜)0ÐN< áá[\3ª((Vžn: )†î¼æÍJQ©íó ±YM:jõ¶™D²ghòjFäR–pÍYFÔ?Ê öbþ}HŽŽa9âê$zf ÿæ6›Râ"Òa£ aÔ“¨53 ˆgÔDÂ_6¡ìÅ<“SKy ÔeÓÍ2ÅÂütÉ8²œsr Y6¥©fÌRÚOò fïò)G¡MñRté²çÈ-Ä$Æ j=W3–›N|Õ,NLù--($Å"†ŸjKUKã8EÐÛfµ©ºQÄKQrPå ¨Ì8²—à©Zéˆíñm¬ÚHÝÔ ¤zM&i8ÔìrÒr4IÑdâƒnR]\ˆFÒ¢C ÜÝÚºyà|Õ(Ôhv”¸é¬‹Ôn®…´´pÃJòœI‰Vq %=Ç’RÖG%6ƒE¶|iB” /žó†Íp–‡Ú6ÜåÃ/ÙJ4© QÈ–¢µ´QÐM®¡ˆ.•.×@™‚\šMž|„9M+ü ãõˆõ.Á¦‰i[DAlš Ößþ(ñaÃmͦÙrЪUÅÿÄ6PR !1Q`"A0@Sa#Cbp2Bc‘ qÿÚ?ÿ¥áþ0£Š<žT•Ïl•;#ÈÉøS° 'âOñ‡þÌŒ¸#òA¬eçd(QÁ#Ó+¾ÙážÔlôáPËûñÊ€õ=8FT3Ž»O]œ#*¾ØØO ì…"£`Ø2¡—’¥Oí•*Wº•( L¬eçä‚9Xòј‘9`ÌNx3(ÎÆg_ 8çü˜ÂŒ¢3Q×BCˆöEŽA[§Ý½“i=ýsÓsâº|ŠƒÃ*z*O¢ÒC]7Š©Zˆ0ã<ÿÒÞÒ‘/i[Ê7¿‰©Ï§Ý¼Ð¤Û¾– ª‹XêaP§Fn†‚Z°ôŒÃ}ù«C)1œ™Ìª-ˆÂÜPÒ°´]Ý`X:¸§ØX#ÔP°µÀòžÒןo"Š¡—orLªæoUxÌÏ4áД\ORP¨ñÈ8­íMEoªj[úº‘µÖ>é–ª¬žòøŸ¢Úb…÷2y§ØÁtƒu¨Ðp«»”lOÛ“xòû&YßRnÇ%…­zíÕ…­¡a«h+qWŸ¤òMcœ` O öºìJ¸îÅ]wc™Œ¶¥ÔÛvt-•A3Î}–ùÆ yê…±ãØuXßÑéT+Sc_{ý,`$‡3Ò…¿¯£ÿ·6÷6˜Umm{];+5f0âGÝbiIõGÝ U2¦—òƒ†*GB¿ùt[IÎEøUL›­˜V*tÜ\ÜQw8‹§šÃÐ,¼°Ôª:\‹I™VŠÚÉdõÊFvZd"âã$©RƒˆèP®ñNç²Þ¾"ñ…¿«3|­õMKWWì°òÑó‡’¼Uà¯*ò‘ð§ÇO4[É\Wl+ªéG’‚ ¨roN X$Ÿì®ÔÔõø£ó¯Vú…o+ýE¾´k[ûF ±6Ò±v!c+h Sé¬s¾’ÇÿL¬{t¥Ø¬}ºÇPî±”5¬U ab(ë }OX[Æj ó{© FT2ÈPÖö[¶i uOH[šzBÃÒÒ°Ô»,-.Ë Oî°ŒîV j+ú– êX7w ï²Á¿³Q±¿HFÆý ý ýaߥËuPkWjêzüaù®WëýR¬¯ªçß"8%J•<òàP ( ã{ Ð:P¡AÈìô©;/)W”©Sž¦Ý—UÕu]A°ˆæ ¨* (0¹ÆnryR¥OŸ-=<´å×T(P£Ê}¶û!ž¾T•%Lç%ßò9ÿÄ31PQ!` 0@aAR"¡ð#2bp€ ÿÚ?þzË_–¿-~Züµùkò×å¯Ë_üz’â}7¼’pµ–¢„Z‹p‘? n0T™{.œñæã½Æ°OÇ©^óXgãlJ~BzcÔ_ e¥½:¡gðª)ÒÞœÊp¸¸»Ñpœá"jÈYéoOYchО ‰IÙ •fSùÁä,Ö/-1éë,V^…HêÛ }ÊrxU‘NkÓ¶”ôõ–?D‰IÙ Îäð¨§5éÛJ«OYcs$K„ÚÁR<Äà¸y 2ä&<´Ê´æS…¨µ½‰F –‘Ú Yk)PÊ…ž–ôæ,þY”o¥½A|‚ËKz{ø ”­1ê)ûÍá–˜õ°OÜo£Mz‹X'í:°J4ç©Æ2äJ;nJ.EØ*dËÈà‚¢i ùcN‘S…,½v$¹L¤„ÓÖ¡Zš`ªššË"šjüv-«bÚã&%WØê–ä­µJeÕç.}gª›Ì©µ[9•îskG9ì.;ïØçÇàNTéV²™˜)A«bÕ±bزŽU‡K1éµ:¯‰·Ø«VÜ.-#âì:Ò9”Æg2ËéܾÆÒi©$MéµP›“—Ij¶R9_et¶éƒ•÷ÜäýœžÅ<&š+¥¸ƒ—VÇ*³—_ä‡Ë?©?oRêu.ìâÔÔC/­Ù}s.%IÚÿ¡Ã®¦áé/[‰"1‚Å2Z¶,§bÕ±e;i/Ë_–¿˜‹KYiíC#ÀÌD—’'c°ý¤·½Åœ#•Â93§áîtÔnt´ý"ú:C¤þäéÒTt•}-gMQÓÔrj9UªŽ]G.½Ž][U±k؆F’ô¹%—=ËêÝœÚÿs9ÜOÞÎ÷Gsªâîu|C¬âe{#¬{oúN±~Ó¬§c¬ êøgSÂ9ü£™ú¢ïÓýàl·¿õ9|ÎO s‘ÃÜãðh¢‰OG~ÂÖd’ç¹}[œÊ÷câVóz;ò×ì,ô˜!aF¬ý…ž“%Ä’I%Ãr'ØìIØí'iÓß°³òWì,ü•û ?-YéÑþÖ@–¡$ëJŸüÃõ?'§!¨×–Gù‘WãÚÿÄ> 2!1 "03AQ`pq‘@aBPbr¡#R±4‚s’ ¢ÁáÿÚ?ÿÊ4yÚ9¼ö‰G›óTó²÷ŠÅL¦S)”êu:œ)œ)œ)œ)œ)œ)œ)œ)œ)œ)œ)œ)œ)‚œ)œ)‚˜)œ)‚˜)œ)œ)˜)‚˜)‚˜)œ)‚œ)˜)‚˜)‚˜)‚˜)‚œ)‚˜)˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)‚˜)œ)œ)‚œ)œ)œ)œ)œ)œ)œ)œ)œ)œ)œ)Çu8S…8î§j½Ôíî§ou;T㺽Ôíîµî§ou¬ou¬ou¬ou;V±ªv©Âœ)ÂÖ8S…8ÓÙòó†›4¯]+úó.! )ëÍ“ÊìV5b±X¬V+ŠÅb±X¬V+ŠÅb±X¬V+ŠÇÈžlže”2á”y–SzéO3×Jy˜Ý)æcrcæAÈ4§™žÚS̰M)æXNé¥<Ë ú rO^e„í+ºó,'eœ§uæaët.ëÌÏ}+ºó0i]×™e †Ýy–St®ë̲™Ò¡£w^e”Þš ²]×™ƒ¦Dt.ëÃ×V¥ý–©ËTV¥ËTþÜ`2׆³JÎ!¿•œ\å›DܼæƒÕjáÒå™IÝIœdî¸@o+;<«†›=€¨Ñ;ب=¤!tÎëÂÙ‚íê'9Þ¹9ô€+ƒŠºˆw_e8쵟…0ì¯kUôgºžnWˆ=± Õ ÿJ[{7+l1¬Ò0fí¸li1_QƳGDzŸ3i¾á6¸‰5í”4„ã‘IÖª_·*Û[Û¸ð«tǃ(ê¢o¬röЍúUšò nx¨µØæŠÓîoö€h€ªÓÊÜÝÊ êÞ‹\" ´ÛÙý&{Öî§!ÿuT½2¨º×K÷p«ziQ¤UGÓ!TmmpØšíâ¯× ]¹ZqЉ¹»Õ–ˆ ­8À+"æ!ÐÖzä;望ÛûÊ¢?P­þÜ*ÞšcÁŒûELû2Ò¦}™ ÷Ȱɷ¨•j—µwã¹EÇÙzoPhË?q©þßÞS>áYûG ŽšcÁŒûELû2J™ödQÇw÷‘wQ7º»,½ßÒ$˜•j’á¹@ ÎOûÔe3î» áQÓ†¨Ò*¢>™Cì¯éÖhÎò/Vh®ÔV©&ݺ»¾;¦Hêjÿa•E÷ŠßíÊú.•Q«*ÓH àYï쬴@T]·b‰*˯gôƒšb Drý¹NýŠìw×b‹þÉßmOèrYïýÕþÃ*½t§ê<¯†ãQ?ăXË´â­;Øn¯éÚ¦äZq€P3û©ÿmr«oeªgd÷ 6‚7*?z¬¸D-RÕ-_å2ÆØÔã¹µ“냯 ½žõR3x¬ñvSÊÂÖ~$û/Ûd:¨¸ÄäÅ¥fã´U~;‹»WIöå?ÛûTYT>õ=ÛÏõU!ôáVõá¶úÝ]#=táÍ0*ægþ\br"×V¹ýÖµËZVµk?YsîVZë–#²øV R±HÄ ¡vê¨Ûé}Mgò<*Þ¼6×n)®Þ*e&ñò67dk³üG ·‡,ímOý¾FêOaS¸E9ÇiáVðàunÜo!bSX6U`ÝÂÇZþõDLÛþCâ˜Vã°\8[Û‡|3ƒ«ˆ‘Øyö±»SZ01uÜ/íà„·mE…¸^<õ· ÷~*½lÙÂç§_)Æ»lœ~|ïˆñš0¯Áo¿ ð^zVihÇÜ?ûóq2¨ü['o ;‡â gšJ!vÖù˜üÐkD¨½Ø"ãÃ;ˆšo^£Y};BòÖßs´ ‹œn é ÜBÓzÜí¢¸àíêû<œØ”I{·V\ãrÜ݃†‚=xˆ˜e÷;û®Q¡>ÊiO£Iš?* l+‹±Ø]ÛC~'ïÄ´·êÓH"»/h!~Û¡Õ^ÎÚ<Öª4®ö 1°È³Eyß±ãã‰bÇ(?4þ2s¨Ú}•Ö‡E›KÜ)ؾëáî¾}(ì¯sŠÍ¢É„bíÊ7páñÄù®»rýÆÁEŽKUÙÇÑB6G§·Lxnâ¦UŸGÙO«XÞù9Ïh÷ZÁì³XãøY°j‹œO³§Î{©ÝÝcÅ é¦<ËoNl1ÿÃàëÍ–õæË:óeœÙo6G6}¹³í§#xV™Cû»Óü[íbQe&;ÓòYgrh`¹»tûP‰*e:Ö¬ p¦ {VܼªAÙj›ÙjšµAjÔ¿•ñwS;ºÖ½åþIZñÙkYÙcGÙIFµLZÝŽ{­CÖª‘KIÙbîÊsÙk? XÕ;Íî¶wR© ©JÀðs:ùólNžåx(R©VÕñ)œ§rÖ­ZÀ§jÅ‹)µAjV¨­[–[TÊu¬ZÀ§ `±Ò`¥ @µMZ µjEX¹k®§r»õxeö„2Í8…¬±±+=·o öÙeíEÍ£q»€×ÈÅb±XÕ±JH«Ý|]ÔÏZÇ-qZïÂÖ…;À¤bÕ©Z’µNR¹mS:Ö-`ZÀ§ `±F;ŽSK›hGJö¶É'R6kÕ3)M¡ê¨Å âÒο¢ý?ƒ aº ’œ7Ø]HÂÝ×|ý¾@ñ†+¦+°”­R5HŸ•î¾.êÐŒ}rÞËBÖäKItprqu%§901Á¯nÂé­ÅÐVòñ'Q€Tž0„aóñä!®*õ¸…hÖ{Éùÿ·<Éöò™.éä±Í$¾!CfÓ¹x!þëüÙ<;*á.O xFŸÎŽ ’7Úu™ÿ?ºìVcí&4áb¶ŠýM¨Á˜(PÜèwEÄÁƒ©]Fë¬m^#ße˜&Äæ2m›Íœ͵üjƒB‰Úõn±„U¨]½-&¨ÀÃ(5¢ò‹]@`pá—ùÂ-vDºl^)B w_·z¦»rñ £ú/Õ—`JµnÛ¡ru ºÑŒU=§ˆØX[wñ8*6ÄǺ[+RÈ-‰æ)öa-éî¡‹`/j£ÿ‘P>4Gs`£³vµÌ‹ sBu)Ø`"œ_HÈq àÓIõa«°ÿÄ[Câ·~Ô,ßuÞ‹õt—曕5,on ô8eþÞ@ðØ0v9UyVcuQ «“ZíŠhvè«q½Zk?pâQi½‡‹h[¯ ’ëã›FÛàf(=´žÒƒÆk`¿QECqTìñ"øÞ¿Kðð˺ùóìtØñëºñ+6=y³ï͖͑òè>ΜW†A€â¦tâ±Ô«&ºŒAÊ'5ª»zu+hàßâœ^>(&XÚ˜Íå9ÂÜ8•<á¶{«¿ˆTv±³z6q^%1´ÿÀ^éÄNýÊÌc|J¢èUUKöž%oAäcÃL÷VËDWÕ°+.™•Â.?É4Ú„µÅ=£hE×]ă§Üâ§5ÂïQqV@Rª7Ù•HžÛ8Œ»[8³rÇŠáÅW¯Ne7®š'™lê8J<‡g^l³¯6YÍ–ógÛ›6·ó`GlŽm_w6·…ÍŸNmX>Þog~_Eæc¹j VÂ…‹Jºä@ÐaÊ«;üÞ*ó ½Ñ;t!¡æåÝï@ Š™ ñäôP>s9F(è._ „‚õè£û«°Ð8‘ ç8ß»’¶|ä«7C„.DïÑg9]††9a£zÞƒ@ë .8µ‘P£vB"B0Wò(˜ès±Vw(h ÁB¨’Qƒ@Ð4z§fúEQ†²Ò»uú Ä@ Un yÐÊx„4bpE®#H[Àn;v#t Àœípܬàšèã—qпy òƒ‡÷¡¢áš©]è £8úÝëX";” ê[ºý ( ºäó´ˆ&€Ð<¸]‚01ƒ½‘‡‰VH 0eX,á’Ø”sk‹ nèžF€Ýð•}©–6º ÃŽýÁ^Ò~ÛSšt–|”nàXE\QòŒD £t‚`õËÆ¸åPELô|Wï (ô ·Úê¨^F-TN†-Ð8“‚)£z¼š5ÃaL¥Þ4€åÿÿÄ,!1 AQaq0`¡ñ@P‘±ðpÁáÑÿÚ?!ðRë¥é/Àµq¯j=;øKÀ^ëŵ'EîI¹¹»TâœpdTÚˆÚ‰Ö.Ū)±q5"°ÌÓceH†@¨ªéØØL摚X‰íœ›QÚ–!Á•K2‘©ˆ:S 8cìsØ‹(c[‰§À¼æ›Òç)À¡; R˜½Ñai¼PЕbòFâV7t‘€ €¥Gc-Fî/Ž\jÍ#TªƒÔ¼>¤ÓŠÅ#JÞ«N iNô{ ×q™§J3}9¦ é&È“eBÉ FÕÛGâÜGfœUŠ®N éÀëЭMé‘6ŒÁD>Nš"ªôThéµf­¤Å62$êðŽÄ c(…èÆŽ$ì^Y0ÉYè¸Ôß`—Ô-ˤDŽ(Ä¡5tËÅì;ØX¹Ã2СxÐb±+Àº¬cÁ±m1à*ô#NhÄ`šÁ"¸#ts¡ÞŠQŠnfu&:ln޵äGx780dÜäÚ»“h7»,䌰E7ѽl-Ó7I¦ µ«ˆ¥¦rl£DµKVlˉ›Ò걑ZÇ7"ÑDaäÊ”)§ÐY"ä5H¤m*ÆùÇCÆ”££Dw³™‘3ØÄ˜`˜Ûƒv8fþ…Éå Ø& e ›@î¤ÜsáÍ0N™¥¼uÇšçNôuΉד²dZ&«ãÒ‘ªp^EÔÚ›4"9EšZ6666$Ý1òlb(¯$è‚n< dºDø `‚.lNŽ(Õw¢ÎL6D—CÀëܱÁÉ0¦™7ToÔ¹sr;ªÀO(± ‘oHváö$`– ˜—µ‰P5k aÎÃ>´7(Œ£ðFT2ãW’Ç"°®Ð¹ëÇ*ÇC1baào÷mLO4·ƒŸ Äxøfú™ÀÖõÚ›QèàÅ37®ÔÀÍŽ4=Œ1eެ[S ·Ñ“ç"vÑÅ9"Š™¦ÔLjèMÉ‘ –¬&DïNåŒRrrM©}„Ù9-&+ÚæÂ/#p8e°X\cÙŒÃre99ðÖh‹3 NìÜdþƒDïa^lI t‰}Œø–¦Å‹xÒˆ3LøÑG©hµ»‹L®õƒj_ Ò"®˜8§YîmEH¦F^†n«“"ÜŒÓc¹ÅmL Cc “v:õtNÇA FÃÁ–©‘oW£Œ$µbO©°Òæœ ªoF‡tfipGC|м Êk‚ð^Ž'è+‚{Á¾b»¹qâ“‚ÙB”Ç,Ãà‰.§ÃvŸo«¯‡XzcÄF+4ÁmlØšªÉ4v3XµXž‰63NiƒzDª;4<ëÜW CÚ›éÞŽÕÚ™¤ ›±æã».eŠb–Œª1zAÒ­©6z‹1DÜV>¤\r™6Cv'©ô7±ÃalU¨ÜDÔnVt¹B¾ý ×Y®¢1If%¹¹»¹&ü£ øoN«×mãXUOLhÚ™ø&f°^Á®jŒ362-ÕÅ2'ΨéÅ/MƒÍ7×j,éÙUÖG}14ZXŒ;Uª­é#:ÖèX.If1×DQYMèœIš,Ò.E‡,NÆhÑ1’*EÈI·S{¢ÓPÆ^~ƒÑ*›È­=Kœ±^(J[’" Ñ9'> ôcÂ…Iª­´Î¸#ÀšcÅÚ»ø “ªÉG:6«°†*l^Lo] Ocqb)µH½&»Ö*Ä:Ŭh³FÚÒ'$êQ$W‘ìMyÓÍ¥32\[›‘lº›¹Wc\ Ù܉ÎQ±xhœ¨3àd«# qEÓÅ>ƒQ72¬Å¹xcÚK;Éõ›¹ øWð§ÁtZ^ˆÓ”/£,rE•1Y:é“}""’\nÌx$™˜é}Q]ôA±Ò)½92•«±•¡ÓE"™BKh‹›MpZf³z¬›n7'ÌŽ›Œ´ªu’)ŒÁÁhfâ>ä; < ÜúRz’>£ìOBÖ”@“¹s-Å?CwL·É”Ç6œÜÜ{˜/5càšÏ>)4[Š“øJ–¬Wz«UQnaVä“ELŽ­Rhö79«8È£VÆÃ£3[ª9jeÅI顎*[‹ CW¬dFä&HÕÇM©õ&rdäÚ³³7b]HÜëÅ6RêèoYÈæUΧ¾µŒïC‰ÔÁ6À²†Å§¹˜"~zhSLVt-MV™¤Ó ©7'DÒMé:qVmY¹7 ÀêÉÜÉÁ7¦FIi96Ñ97¢†´§hº%Std“þ“Hµ$“}3 z‡5“ccŠoKRêdÆ‹’mÂzvÚ-rÌ‹‰XœØìƒh@M.¾Å¥"ƒ’Šr$¹HèÖŸPòÚ :)ðòý{€d>äl¬Ôw#Ì®LĬ ?R÷¹»õ ;ŒŸ.BQÙiôÿ€Ã „¡(Õ謓,¢w2è²]œ?XÙ6šÊóÊ YZEžç'- kƒàØyS§¬8OÇÁs¿ýn2áxÊ£¨o÷¦_~Åòi´|ã-2v=ädæŠ!ŽÉ\”ŒÀá¹ÜE)à›æ°lzç•IQ'Â"ÓÖÙÛNh¸›ˆ~&¶ÃjùÓõ 5rþèÚnÒ9{¤)•5ÑΆÕ:ij!Ëú˜Me?.g¥½¡>Üy"ö3bn²0á¦òÌÚI…¥ú¸¤²0•zå&à^ßl#ñø 'À‘¬÷Ô_¸Kè}eµrEW¡ÃòÊæZe=˜ÈòfZ±ˆÛ“eÈš%TmXïåÝ…+»ÿJ½ªŒ¶fý†È‘îfO‡$ÕkvcŸ5@e1‡öß!—Ã<“Cš=õ+Iº"fä–n[¥aÝ$¦*ìzÿ)/¦~„>çúµKOý“û;&öM6šÃCaq~®-¤ÓÝQ¤=‰ù1x9 ØŒ´V‰"æÅ°'rW¾ 5qÁÕyHf ¨©TÍÉMLm»¶é?™ïÈ¿ý(îN»h•r…E~Øø:1«—6>›Žn.£OFǯùb'äˆBYÂEÁ|ïê»ýÒø„&7û·Ýpè„i©Lgü•ÒÊ™Íôü¦Ñ—ÁÁ‰HsÉÝd°«±ê_“ÉÖ~Ýt:|Tˆ¸~â$Ð*$ËÊž¾PZr˜:bîd9¡&D ÙI¾ ‚ѱê^N#ßÿ$’K —~ÏÙ üZõ]DÒv¥¡üŽƒò¦S b»™áØmÚãŽÛZô0áկɤ%"[p‘¾ü÷SiÜ.X×¥’þ52{ÿ­«bw~¾TÏSŸe`q‰É)>£ºI&ǯòi¨]½µÕóñðæd¦-vètFãžä9šjpוÓÄ;’ÓR8nè·¡\ŽH0š-£c×Õy))Û„…£Ì}Î’Éþ@»Ýî>¿8›GÇéü##R·¢I6Dàkbb¨Øõ>L"1ñëÖÊ!¨šk'!cºigè?”+ümü_Ž©~é˹c£³¥ç#ÍÕPðz¿&x«³ô§.GÔùþ½QP÷-×g _ÉŒ5”jŠêì‹„,öåU«òc3ˆ_r ¾ÊL¶ #d¯/mÆHiÙÞú<¥‚1S7$;]1õ"•‘»º«ÉèÊëE‡oeT׆QéûÓÖy4F¨ΤáýŒnHQ¬§÷YÑ?ÆþƒùMü»aŠŒÛÜdØ‹˜¦G”*I"õÞJ!HPêjqT¿gDÑ.ÁŸæÃ¾”ÇY±úM©‡‰ÝC˜£:K*®_ì{“ÿc I)èꋼ4‡½M}(íïΉçÜ~QØ`£1ûŸ’ÐnaŒº7ä(Wªë¼–EÇš‡éø¤\ET#MY–ªæ9$C×Т—ݲèôÄ“-’íåÞû ÌÇ !dï‘ û8¯î¹ª=&–èDÿ9}¯V<Ý_gåf……Mâ}¡ä™7¥Ñ–+È£}y¢«É •4îOKCöœSï\¦¿M¾Ç[ÁsEŸ)†Œè9ýÈDDáQ¾ üŽÏýŸWG²îX—ÜVÁh{“ð®Oí:Æï:ûß:™Æß€X¦JSÈ¡³—F)"húhÈz% @¬}z~Õ©û=kš¶‘ ÂÞ‹)›cjýì½ÓñØwÿeØ@ýfôuMÚ“žãçâƒÁëZýÛWOíëÞrôøÛüÓ±LÂÇl6Z™ƒqʹ´Ñ8ÐðÏUUäuGï8§îuÑôÚ~÷]¹¶†‹5†[¬m·vòÌ ¶ßö$D’„¶¥ã—Æóé±å·Ÿ«3ÝÓfûèXúÞzkÏ•3ˆTËÜHí‹3x’ô½.+ÈåhcÔUyQ™úE?c­s,ì©új·"Y]¡»nGÝŠÏw݃Ë3-'¼‚K Wѹ¥äz¶ÿYù Ül£æxLÝÏF¡«‰17.œŠU&¨y;Õ|ŽgÃi¨•ñ(Èëâ§Ô\‘ƒµ= óD†ªPR…³jÛ¼M$§j±6„²É'rïb{ͼ$Aªvl©‘mÉÆÔo¼Ö¾Az`_¬ÖOÝùOü†ÆF-éf`E´ä3}ê¾\&ñ‰Fh[\FŸP—¥pwª¿[ާÜ2gé¤ÑGÇ¡zŒòrßZ^ƒÒzÍ”ÄPþo¥´•Ìïÿ¶ÿwª£X#ª Y,ïV¦ Š#°_¢¿©Qå5àÅÜCî}ih,‹2#±ÐºÆŒ†o½W‘вJòéûàZ¾±kú=-Ù°°¢e»v6Ùè{†ŸnþÄŸ¿Ö³ÿÄ›þqö¤v¡ŠóoÞÔ¯ ô*vèâiÉ6ÔFC7Þ«ÉŸø![õ¬’I,’uHòhÜõ’]f[ÑÕ‰'þš:ï²¥.Ç OµâTÑ_±Ú{C?I‰16ªSΟ¦1ùAeL‚ð@åSš2l@´™¾õ^I[2‡öƒaýé /Ì-SðRN‰$’k?åwe[s‹ëWäüÔÈ'ݤ’Ð’­b—7ª2™º1y(”»»r­ùžôóû ‰Lþ¾Qfzg¦ØÞ–ÎÇaÜ{º¯žÃψ‰ÆªþÿȽå—S )Hë‡ÐoÊ,ÉLç¥tÍ,в;–-#°â¨Îfê¾>>T†!¬§(êúû©?Àÿàþ="F_ÿÇ)yIàÍSpËÉ,L´œ’7cê'£)›ªøôþV‰‘Þ³ˆÜÿŽBлëb1a‰òŽ ß 7LQ34ßB2:¯%!ïÓ”ÎØÔú|¾ÇCøÄ$Ÿã¡G&ЕÆöÇo”òôìYÕÁcµ>¦ú2™¾õ^KDGÿbiNS¥¢³ö |ZD_rëz.ý< ùO=x#ÐÑÑ—’øèÈgU䯂¸ýÅoöGÅ[ÅHÀœž‚ЄP•9±Q±Kil~.~goƒÎcMÄüqÕSzï£!“ªù„|b‘ÃXgx/^µcþvð¢|d‰ZÈïý QÚØ}×n“39ÛÊYLj~¸é.“jXß:sö2~J‹N¤AñV­ZD_æöÓN{|$EKøMd‹*GX$ ¿*ç0¦cÖ Ú±V¨•F~ÆN«ÉÈŒ2ÕWèæ¿Ù€]“">„™²,Gí`¬‡¿GƸhˆë’:×zcF^Ú å“l|…UÅQ†ˆW*ç•Qå3/Ö£¾b#Ä‚¤6Þ»#{@Ž$õu¥“bÛlDø ë8Ýå(Æ.bêXˆ4µ:3ö©‹Éʰ—õ÷ÈFeb+ÜÁ"ÿÛÜŒÞ×7aAEó×оçè¹e=[ÖR'¿*ÍÙ>QŸÆŒ§Üc¯}]ôgÉ•‡—&X£ðȳÈDÓ•£Ôä0õ/ûD cÔGLú&ÖZ}Oèãÿ:Ì%å—¦Qº 0Ïô®?˜’Ÿ”âŒt–-®¨ÏØt~T‘@ÉýK¢Í'•t}Wâ»$—[+ñŸr°–ìŸ-í0UúÕ›‘gK:È©Ÿ–&‡rÄùDf3¸~³úä]ÓÂfèõTC-ë)büj»&Ìê9óÿ¯Çm7˜]¢ÃOmûä+æóY$•)Xmvq÷Cf~àoÝ÷¦d“I$Ÿ.d…Š,ÏOm£š!1“DzaùŠtO–çNS(‘Ú „9>”¶Ìú–diÏØÂ6ðãåxkÒ¤¬^k:M=0èÿ‘0×ò YkÅÇŠM$Lkûù'¹{—×~ÃþEZN6¤R+}yÇü’ôslÖÂ,cVqü¹¦¿…ÕO¢â™´gVQÒ‘7Ñីv¨L“ÎŒ¿ÈùhðÅõuFã$yÒ†¸É ô¬t˜Î·Y$È?”8ù¿žž Õ¿(rHîbŒŠ:&t‘/ÇüŠðgLLôý4f‘Ö‰ÑRL?'¾,!‘T&7?%õ‚ËŠ={¤ÞHÖX¦ÇÙ2UM¶ókÁ¾˜‰ûy¦å§WrÖ,ôzAù?ÔL¯°Àà£ÌWQ*÷†RȉÐ$dcÝ6Œf…ùRat¤nÌœ&´.ã“·Ir\"P.DCFäØ%JÍnî^®2’,—ÐR>#³vûß&×ÑÔ…Ý®I¶1m;¸84~sÙ^.$BêR^G \r)ÄdÛ/ž£º˜…EéÞ™Ò@?'¢0qzuËÁ:˜¨4t‰o„˜aqì„l°õÒHIâÀæÔ0ô—îWݾÉ;ZBv$„êˆÉTbÍeHNî›ä\ǪSÁ ‡`»Vû×6”÷­‰ÝÆžVXÇ|5¼rŒˆ8îðFøœ¶ìN„,Ÿ*¼:¶ øUõ£ÐÊ)µ‡à³íÒŸmãœ×mË,:uEöç4FÔ’Âh,6„I\» “= ©ì&ýÅê‰H–Õ/’0ìdÍJûÛ¸0ü«Æ›4²ÔºÒ+èÇücM‡êô¬øžˆ~HÛ™ÞeпåÂk“NGçLLi’¤¿#¢ìè^EõýÁa°ŒÕÚå“,’T|¯Å¹¢1 r"î;!Õà>*Îaî%2%Ý‚ÏD˜s,²†¸.`îÈa€6;$¥,‘’F./ "rò]·KÏ%ŒL\K* ‘g :Ü¿a $ܱd>N’¹9WÑ)ͱýð¯‰"¢á@eÑ™%8—ck¶6Æ8šsˆ|&MG¸äÛÚ>X¿w#l:$–ì‰ÉäìLhð=PftÞW¤”šf¨šÎ’ðGÈãjC•9d¡—HÉ—³6“k ì9€Ø¬(H6̂±‹H¢Ä lÓïôL’UÞƒ¹bÃcw¼dضñõ2‰ÐSq'7|±sš' Ðã!ë,_*åùõ8ֲƞ',;$*¦˜bxaYò “ÔÕµgW§™eL1™’õÙ‰Kg0[J šA¿‚Û­"5xÀ‹Ù„yúŠ%lÝo'ðªÞßö= ’ÆôŠ@F?äjÅ^“MÆlAÓŽ«ù¨²,ìÇ©dšzqÕ!f*,³Ò¥DZ#ÓŽ«ù !Qe‰öUq¡œÄ¬|”ÈX–¦ÔÛCÑè>PɦÄjÿ#úÃr¹_®¬ô=-µz”ç2V$Û2Êñ¸¶q6†E¿úÈ¿ ëuçÿ´QÝúPâfݰ8ð0(–]—œûIwŽª8>ùÆröÒuO¡×N®{€øßJ]ïä†; 5p?ì›î=•‘ÿvHO¶t—Ø~æb ÿ›ùL†¶jI>a'ÛþhôM6Óz#Ð|­!B.$¶’y:(ÆÏÍ…ˆØ˜Ø6ÀÉ0ê ËšWÐèñqbÀ©ÌÀåV›§†ó-Nä”i,ÐÌ#½#þc§î7Ûì:“N_ÁÈI2’¹ÖÕÎ0÷ã§ïK÷?s;ä\–„\V.S©M(ëO±Ìi² ÿä\rí:¿¸—€éã‡ï!-ƒý ùÔGYúCÿH4ÿYäûãéGÂdþúÓ½/ÕÞgêgÿJŸ©£ÙFŸø±«þl‡?h†¶Õ¶|ˆªOµ% j’oY"ˆô?6oPââiãSùhªº ì~¤@:3ô6t>ã¯÷ ý eþx½ ¿Â=²¸cêŽo¸~’rW뾇´p‡Ü'·Ü:¹×õ;§x³’HwgWF=3˜ûq&~Ù.~Á&FM¤ŸèŸþ‡¿kúÔ#½¸_åêIž¨K{ñw!]÷Ql~ž¬xq{15žP&d‘¶ðÂi­ŸÏÛªH¶Ñ“íDz"Ƹ¤X ‚)r_'P뎼ëh:§Cì?J9þÍö·Cvã£}EÀý”÷£ÚY×üêŸT{ú'¿ÛGäUV‡ïEÛNàû§OÝ8~áú˜¨¼—à•TMϹö0`1žìýLêÝBþÁ-rpýá‚0•­Âê9]Fóè[çÃÀ²n®ôc3H¤þ“˜¢)+ ‚4É,ë3ª#ÿµsò5öG?Ù_jîÌýKv!]\ZÝ|£WÜ6øÏêcrÊuà6_{ˆþ_‹ÖW¾Lþ¯Ýo³*<3Ó½]):äôcòošÉ$%‰ò†³o–>”ÄJp/°‘Ê{É mñí~D¼<¨ÏÍÕr© y8<ü#$ó­àÈYmW抲zqù24É4Ÿ&4»y ÇÆ:ÝÀš_O~P|ÛÅÀÑ.b›K‚BY6Ú²I#¸s%6àŸ/:ü²ñ[÷@éqkÇ…¥’Éd³°ì;(¢QoÏÇAÿ ÏôVæ×§Û)–^T¶ZñåønÂ牗-„…ÄàÄÇì`AÔdf¨Ã2=,j¹u*(£+ógF‰ÌlŸÂëRËêÞnÉ0¡Ì)ŠGÞ±$Ösΰ®á~¿Š/ë)«?*¼TÅ¿Ð=£ÓÉøbä²Y"D‰q @(”J-ñ1>ð8œ1ÜÏYqÙ¸T]—UÄ·S!ÖŸÔ^#4’m,_‘›„“>4°&Y+¿bäß'ö!Ôú‹­oä[¾U^® á\¸úž¸FÁ¶b¶ÂFæä öœ7À8õÙgù²r0×ë¿kPz óÅhòî¸sq¬Œ¯–%3ò¦t‡ªt2Õ”‹’ÉV¤Hí;HhŸ®uN¹Ô'‘nimq¦<2AA „·¹A-ÚpEH㑱%è¢ûa˜ µ£ Š({È:ÐK' ¬ ÀFá“y.‘TɾÜì)M¥‚µˆíq=Çn.%?BS‚ï*³ =Jõ²ú$MùÉ"YÔ:çVƒ¯£~‘Úv‘ãAýC¯M×:„£bÂø"E /a?oð^‰¼Ÿ/“¨È÷ªZßHŸaÚ&T×9×:èëmnãöùÙb›žº=FæÔ”I4Éæ9d¾N±×:§_Gâ¼’Io*7?4zg[Gp†oàÒi>5¾ |äŠnz÷ù”õ²ÄAA¹rõ’I$’~TX¦çä~Gj¦Ñ¸Æ®Õn‚»üÎ<ÓjAA2råèÝZ^’MgáX¨³Hš^‰’¤Q›¿Ë[(Èóòtõ)£˜Ð–f4²ÒÇ’çU‹ ‚êGR:ÈdP¹~ $’I$Þ«'¦êtÜuLk7¢XÞ¯™I?(UŒ¤ÈðàèÉV÷EÝ–ŸîUS•ºD›.Âבl\,ÅÿÝy{éKª²zm¬1隊C¼µo{ò%l ;: ^% úÞžg´o©ÈêZƒ÷: ó#,Ÿ¦àz²qLQ–²@IbÙÃòÊ#¹údÙù±®é\±ù—‚2…Ñ–(è¹!ØÙ^ÁCSSÑM$ù öCð1VJŠ"CËòÊAw,Ǫ1¥¹c5’u–Ðâãd(DȸƒÚ™z‹q‰X–­{eÎ<ºVHzfDCrÉsètc>äAy›p·\ÁÝhßš0þ¼TR?ŽïWOçµ1I)ó™øy¤éŸ:~‹,År¾.+|©1·Õ$’I? $ù~Üf*Ìi‚äüÍ<¿ˆŸ8søÆ/’E ŠÁy‡_½ýLè‹|ð'Dü¢)X¤h‘Ï:[jŸ~wàuÅ6#•66ñŸÄÍ&“òèðcåÒI$’I(±mPA †_åSv5tíðQX¤|¶~UA‚¬R>C> —.I$’J,X±m[üLë+þ#tfþ7ø(ÓAAA|¦i4Ÿ‘ÁV ‚ ‚ ‚).’]$&Jà•ÁbÅ‹]×ûÙ›ÁÅgå0AAA|Žk$’I$’I$’I:gá£DAAAAA|Ž Y$Ó3 Ç¦Þ@‚ ‚ ‚ù’I$’I$’I$’I$ê’Iø8¤R ‚ ‚ ‚ ‚ ‚>ŠA‚ ƒ uÔ´ßô#UüŸAAGÈä’I$’I$’I$’I$šI$ÒIø8 ‚ ‚¢ ‚ ‚ ‚0ëèKCÕd}Ÿø/ºSј®ã;O-AAAA ’I$’I$’I$’I$š$’I$’I'‚ ‚ ‚ ‚ ÄÅI¬Ò|7_Ï(”ѧ)êš:_àdŸ,ÁAAò)$’I$’I'àI:ãDdžüb³[™ÀÙÿ„¶žÄA=2I$’O— ‚"ˆ¢ I$’I$“âù«>U‚hå&S—u©Uü­'(ä™yƤ…᨜÷9§W:X©’\ëjl'¦‡[Âc}*© LÐÐâ —.K'ãÚ· ‚ ‚*AGÈÑ#¾„=8×*øÃ½ádê ¹o¹+îèÇÒœHÆ&1U‘i) šwêtAm,CÉ,± / Ò?¬ 9i_“ÐÅXZº#bháËC¦ƒX5¥VI$’I'ËÐA EH ‚>´­X¬ÖhÆI”-.ùЊ¬|-ÞQ·RK¹[p6'*F-Qr¤{D¾„4= iNä²1v•KI[qÒ#b¶ÄE›Èhx,´1Ujõ0-Û1NqqØÅ¡­Ø×”Ë—.I$èŸ'ÊpAEH"ˆ!‰xsLk…ß{Œƒd­(YSÁV¹šµ ìYÔI&[Èimͤ›$²ÇæÁ2Ɏý¡Ç%‰‹htÉiÜØÒZ–%íØ€[™îÐÅMÉI ?©̨%PË\Fúlm¤á¾Gð&–\Ø} ë¢tI$’I+ã#çÑð–Òå&S‘wø6´Ë$’LéI$¢I¤kc&²];Œ%¿2{˜e%ÇV)C»‘ëßçüCßbï éÖÃÄ$*ìòGÔ›PÚWÖòuQ´[VK‚êû';ÜqBf±\È^³¼;“G H8—Ы‘(i‹”ˆ\4ä¹rY$”u¬¢IøèÕ˜[‘Ý÷-&çd¼KßÕ¢ ‚ð$’IÐd“Y%3$Ë–ô-vj?±±3_š:Ðäß^F;š_»¤‚¸]·´¦†£:†""°—¨á ÙÇÕÎô/ÅÖ1ÉnHªcNnÝ M½èתÞB3ŽŒMJRÊÌïq¬D5•¢jÙ,’I$’~" ‚ ‚) ¼FàE²r…˜‰«¢ÒÕ™LJÐíWð@þ ‚«#Ád’N›@µ45%šßBî»rï‘1iýŒC…„ê [åpÕy‘¶´¸ñ ›Ñ9›%ôDä—0ù ÂA$’ÝÝÓjLb¢Èèô$¤B·º#oë¦èÜÁèb¢&w„7”@lÊnغq­’5 §\RIl’Æ`Äîèi¢åË’Md’Q(·ÂE ‚ ‚ ‚>Õ‹U”+ §Lðs?t?€Djàd#[Ë$NJMäp)iŠr9kEhóG­S}+Dù¼D&¸F:³êHñC`Q(À™ŠŒCÀ‰–Ϫ Šì„,¸"A?Þ4³ëWš‘àîI‰ô!* ʆVƒ»…š0†LVàJÒÏ*Q«l'MÉã[I$ü¥—ù|VŒhr ˜œ¡­ ÿÚ …Û_UŸÓ˽E´<ûœòÃÜ0@¤èHh‚zlŒÀ€š¸á£¾qÿ^¤¦HZ„O¼DXcŸ¨¾­h´m®9-<‚(#ƒpÇDáBQ¶ØHÓƒö•}oøÿ~¹Ã7ÿ¾1óÝ?ÛO´ïõ‰<óM0KŽ8§¢Xò–Ø:în´Ï\ò[T£NYGðd«+²Ô ‹¢ P ²€8PÌ4ÃÞyæÁmÛŠ<Yvß}ÎýóosǸÇ=zÂ8x\ÑŒÁ{®òïŽ+,¦Xª÷ü1ÇÀ‚Œ} tK•I†¹0%)–4aZ:B€$ÙMµV QÐŒ4çS{ß[ïαÿ8Ç5ïôÚQAH ‹ïÿ{ãÆècªzb“Ϭ‚¹j4C”4V]÷Ðo|°´ §ÃK$QO|PW]”ARD,3‰ÖL)$ñ-ín4þ8”æ=ÝöÛÛœÄ0 ¦€s-É羈"Ó< ª ®ªèm¸ÓŘp€€MQ\÷ëϦž(%B„ЀuXEæ†=# <ã‹<ñ@”…#ϱÿ¼÷ÿÿÙm÷Ôq’«°ã¾ûµŽJïÏ¿uÊñžûâÈyç5Úy°É=ö’q'ÏO÷"€¦¼Àç¦9Ë8ÅÛM'Üb@8²ƒ0È C­Àm›C4÷^SL5ß?ö–øcš{n¶È®¢é-‚h`²Ê„šÛé)pE9WÓAN:û;Óo~#¼ö˜,ÚËë¦ÂÏ<ûÇA×ß]eÝtK'7ÇØvöâ4± ó„e4‘}÷}µÓ}ÄžBK Ž ,þÊ5óïIÏóßÎuÎ5ó‰oÿ¾òÇí=×=Ê–«ù¾‚:áIuãÑEäsïû÷øÁKøà¬ò ÍwßL8à†ûàˆÏ<òȶó”Y”3 s  (‚š 0Ãÿßùs.~ÒÉd4–˜$óÓm´&[ ½9kUç³~ä¿¿ûùæ0Ãþê¬sϾø †à@Ï÷ÞzIo<ãÎ 2Œ4³Ï ûŠAt“ËÉïŒ0#‚ °HÃà¾[î¼÷X4â@ý¤íÔè.Ž*Ua­ï {úÇ̰uÅãÞ·è³Ï[((†B@×üU9î 1Ï 3Ë0Aâå´Õu,5ªÂ ‚¤²¤E¿~ý…YŽìû… !_wL›¾Ðkª?ðuî6ù ÿ¼žu¥~qÍvÂHß ¤¢¹ËÿÒt»-Í4ãÝïÏLqÛ:SÜAUVè€çªrÕ?<ó÷0ÖÛoeC…ñÏO¨9ž«ëËöÿì0Áð¶ 0 –:§¿ÛsZã€$B„¾ó¯¾ú%¤éç¿ÄY„l²ÒK[s×:Ç%ßÓ)ôõÕœmHÀGwXµ:~ðd®7ßþp×0Åv˜AF0Â`Ò€‚,°Â?}û`€Bh†ûd²ûí¢óÂ2)BQ´_öúç¼óO—´ùŸéíŠûþóô™Hl!¹âê(ó`b>Œ|Ã,ðíÖ×m¾òÃ, „àCŽJþø}÷è4ðÃ(‚ˆ/ŽK©’ø< Ê%ŠM÷Û>:ÃíÛk ¿YÔÇÎûÿ÷^yÛVãë¥øÉ€ò÷ÓÏz]( ÿη<0[ãA0ñÓoª÷ÐŽ8ㆠ®( ¾û$ä2ä²K‘u„Ÿtñâ¼3ÚïFv×ÎÚK}ûÛHLÁßkmá6 ør Î×ìi bï~cžÊ&øÃ ->´ÒÃ5ɾÓÖ¢û«¾x º9è’ø ÆÛ΂xì¯<¿i@‚KžVšëÏ|âÿïÛE|°WMÏÿÄús¯~ô½îû¯î»âÁ :¸`‚CßËΗnºÏB d¾Ë+‚9à‚ˆ ²~:È#Ûhoû¤0M{÷G_?ýNGœµç=óÉÄsY”c@ÃÓ9ƒOü×ç8à "Žko°PÃ( -ŠÃ{û¬¦²‰ï¢J¦ºÚã–ëå¬ñƒ»­ÿþ u¤?Õt9Öp’Ã<9ÏÜ·ÝÆpÕe}ÆòGÿÇþ>1ÿÿþÿùãòÃÇ 1Œ>x ´òÛOAá⡆®ÖRÿ<ñû»Ë¬£KZcŽÿÐàI÷½û÷ßóIíxÛ¼>×þI,1ÇMÎ,çŽýå÷O?ÚË ªÎÓÏ ‹/®ÚE;#Œö ¶™¨=÷þ¿¿½ÙöÅÖÿ÷o¾Ooª*ƒïÿõG_zù 0ÃOzÿh~×ΰßt“£Èë°#?òÒ'¾óÍ<óʾûâ²Ò㊌¸y0ÐÒãÞ~:¡½ŸøÀ‚k¤mf¤ahª}û:,oºÅrÍô¼ã¸ÿÞîÿ/4ïn=8k6ÿâìóÿþÿÿ¾ÓÞC 4˜ ¾˜„ÿü°AWߪ jzñ¤üºˆæ¢9ç&°Kw’ûÞwø8ÚóÁ×¾Í%½ïœCìrÃÞóÛ¼3Õü¼ïj¤×Î « ûÏßÈ$ ‚Kï¾ M|IEMÚ 9Êú©]‚C‚Kí¤Jk5Âí.í}Yþ  åËozÔÇ_³Í%Ñ÷-ßûû,=âñç['> ªÇŒ8À@#O> –ËÏmõôñÖ×j / R·'ô³$0ÁHq 2cúkø¼Ãa½¯z¶òA y;WÿŒª‰+ÿÆ•}0Çï:ßÎ{ÇŽ}ãN¬«Zï„0J@òÍ<ûïŽ8 <ñ÷ ¾ FöqñB€4£@ˆ/>¹ýo „tÚCË ( Ó7ïtßÓnßÓÜ}û£ì"ÿ?gÇ‹ù‚›¥rÌ0u%\!¯Ž{IWÛUöâ‚{¯ÓÅaÈî °ˆ’ñ+ î:ñˆ‚_y ƒ ëy« ¼½u€@WäÛªû/Ž+%üøÓÍ…½¨ÕyW2À† €4ôÓöÓ}¥ ‹<²˜¡Ñª¶Ó† ÿÏ®©Kúî¶òˆ=u×ݽø‚ =q¿í)¼ÿ¾Æ †Á¹ã‚,ð`ÈÇëí–æ`Q0@1(à‚ECËŸmöÒ_¼%®+'7óD}ápèoÿ~J¸æ¯* ß}¤ÞÂÀH³ÍE7uì5ßßö€>;íª#‹Ao<Âj¾—”M<Ô,ãÏ*;1ö ‚[Mõ_I5ÛÂJ$ÜsÇ™Ã'Wß•„ä,¡N¼ºYä˜Ã[é)ÃÔÓeôûtÊ©O,«ž8.¦ë'ýðÃÛìþG e½<¯È.¦Ë-†,1E\uô,5¦Ò\H 9Á-ÐBݽMjñR< ‰OÎ0ŽsÏóUl6gìwê«Z!䶺î’:;Î.1ûûà·¼û쟔(Ï>,’¸5½ôÏ}÷ÿþúçŽûÀ€ –,½ÐEg[E½¸ÃúzúªdÒM Ù`½m›¸*„ÐmÿïŒóϼÜmä4gMù¬Óa¬Û¤ï¢}Ìè¾­<úþÿÿ¶ûï¾Ú®²¨ ƒa5}{×Á2ˆãžë à†71¼:D‡}§?¶Âþ·mT±Ñvðÿú+‚{¦¾Ë£ßö:Š ¶'³WÿÿA šÉj6;€"À Š|óÏAGÓQ>µj{òÈM0³Œ¢øÌGõáµÁ}¦q¾ <ûÿÁVÝÃoÇVŸã4 ¾º£¢¤ô÷ø.ÅÞãq0Ç=c‹Là²Ûª ,¢ÈîÇÿŸ}´}öqÔr¡×ð â<£$ö•ËE}´Ûê Oôÿýô›}þ–[¹û´ƒ;.ëï‚gï|¡þ=tóÉ|Ÿ|°Ã²Ko¾h(²(üQ÷ÛaÖÔ˽ ZÁ ƒO,¸©ŽdZ}f]=…ÿ¾ÿï¼óξÿÒu§pmï>ÿÌöÉ_m+ž¸ ·gÐ<Â÷ß}îÿ}þðç¾è!‚J‰¨,–ËO}ö}®=Òæ¶ 2Ë'ÿöT ¦ÄWþûïª wí'Úë5WË ó׌ö}q “ͧä=½^ \Ïyýÿÿ}ÿßÿÞð‚KíºK¤ À ´óÓ}%Öô’Fly›‘ ü¨iêb‚kÓ~ûïº"ÃGR}>|}ñûÍ~Ò+‚¹hºK¸ýóì­+I—ßìÖÓ× <Ó¾¾+­²‰j ï¼óqžÇþðóÍt¦áó‰ zÒ|ûïŽóÏ‹ }vº–gS¼¿û_3ûwD2X|]Ç€>2±Sþ¼I$ï¬[ßÿŽzà–x%ª d¶û¯²_óË«i‡ÏÄ}%A×ß\ëK8Óÿü0AWúæŒzñÇ3ÿÿü×%ZÙ®¡[¶— ¸ó=ßM5Ÿ}÷Ñ߬Ÿ}³/€c‚€‚[î’£ßY§AVÞU\öóì½sWß([ÏòÍ<°ÀCN8Ó[W<³qÅsËßs÷ÿôU6«¹rÞ]0ýc€]õûÏ÷ÝMdÓuΗ,N J ‚ "z¤<´EÄœq']wÌsÍ5_t-<ðG(×ÛK 0ÃU,úÓ4ÑAô¼ï¼¼ó†RU²^yÇÊ¡<²ÏWÿÿÿÏ÷ßM%x}£BIC@!¢€<0ï¨óŽ}ô“òÇ TP–ó×lx,”ó@ŸþóóþñÃWÞAÆ÷[þÅ>”ÃÏýÃL¶÷‰”wßÿ pê/û¼ÿ-ÿß÷ÓSöa±ˆ!’ ”óï¾ î®»ï¾èa(à"[­ÿû  cÌ–ÿÿã_ý5}$ÿÿ½½KN·ûï8׸}…\pôÇçoº½ã"‡ÿžóøß„‚F 8Æê¾ñÿï?Oºú¤ž‹DALûë  ¢Ûÿ¼<}÷ÒEõÐCÿïÌÃMÏpãνçÖ%tØICë#¢«`Žþîÿÿ´ã?¼ÓŒrÃ>óÕôCCÿ}÷ó•=½óÁ óŽ’_zø®¼ó_ÿÿÿAwßiWÜum®úAŒ3¬³?Ï¿9Õq‡ S¹sïϾ»ý>Ð1Ï>øÃœ4ãÿÿß|w?ý%UP™ß•Ÿ`"ËóÏ<­ÿÿÿôeGßmMë«‚â g;/šÏ(–ªå¤·RQÀÌ/N~߈¯<ò ÿðÏÎ|Ã_÷ÿîøõ÷ÿë –þ½þ‰=ƒqÇKâ ÅÏtAú½ÿýÓm}çÐuÅ-º;oO8—ü+‚«o[Ô‡QAÓý㳿ñà ±Ë¼çœE„ÜC~ßOnÿ<ó_·Äm3;Pæ‹7î ²€uß ÃÆS.0ßôa4Ò}÷Øâcž<ð‘ôùžëz׬µþ²f×÷ÓE ßþ¿Ïÿò°Ë<òCU„PEßyÇ_÷ï¼õ÷œ&><ÀüdXd¿;y¸µXp*« 8Çÿ˜AA³ÿ(H ²»£KDÏ/}Ï,7úÚÛËìUIGÚƒß:ÿü8â 0Âæ²9]÷ÜM÷ßÿü÷ÿ÷ßrËo÷ sá]i†“%ÕKòûÿï¿ó]´ÐMýï¾È*Oÿ¬Â×þüïðÃßCk¶I <µIÿß2Œ>ïžüÁG‘MNj¦ÿõ§Íݳ€K,8ß8⇺ÏñÅPtï8òˆS‹Ž\óßO<òÃŽ»é0_Kÿ=ÿîRIGßþûìº8 AEÿý¼0×ÿ¿íô]gB® 1í:Ô“w9}•}¼ÛËë¦ÛSäcEwãƒ=6Ë¿‡ËnŽÝýÏ÷ýŽòË!æƒGòÓ…_}÷ߢ ,ƒ$à 0Çl0÷ÏpçsÇÇØe1R °ÃkîFPݤ!W¼÷ôOj(ú ÿ}ÿ­ïïø#‚à¢6ÿ}ãÌNÖúG ;Ó ½fçmDEló !‚Jîa˜Çquú”ÿœF ÿø "!¢I; q—n¹ÿÀ€8Cšˆ0ËN½÷ÿ¿÷ÿêªpÈ,iÊ›C‘½Ï,þJh yÏ~ïO¼Ãìá’Ê$ŽÞÐì%ÒAgO¬n³Mö`ýWù]$3—.]¹ëükôð;¤‚ í’wCŸ¤ßù§šË!ÁDXÒÑM°êaÔ Ï^ÿ‰®·×Ü÷ÿ<à 4óÏ8ê|óÜ5Y&]´â"—ƒ ›Ó*ý)idÄÎT­´°0 J ƒ õúKzA$ržzÿÜþñ†PqUIWß]_Ö³ößÿäXó 4ó 1Ç,1Ë 2ï+³Ëí<òî°óÎ0ÃO?sŠØL8ˆ~gŠˆøàBÁ  ‚Zì¢"íd°Ã$‚È,ó„ÓIDq÷¸ñ_òyå\I6æsŒ0Ç<=ûÎ?ûî0ÿ~ó˳à 0ûÿÜ}ÿGaOQmGÃþÖ0N8]€ fQE%]tÂÃÞiúÓ èÃA÷Ó[ésÐ÷“qœÜÏì8Ë1Ó<0ã 8Ç 9ÃLýó÷ÏŒ0óÎ×ÿ°q]¤A,0C8CÿÿÍ9ß­<ïüðó°Ó p ]¥Û]×…ÛˆÓ@qÜ-:¿÷<0Ç,0à 1à 0à 0ó½¾ÃÎøÃ 0ÃL4ýuñ7×ʈò±‡QM-4óÌ?ÿÿÿÏþ´Ï-[ C 1¼ÑQÍE4Óí×{_§}^ÏÉÿÿϸà 0óÌ0à 0Ãùíç <à 2Ã|óŽ0â7†À8¿ÿý÷Ë 1Ï,=ßÿ¼÷Ï<÷ÿ°Ã,0A0ï8ðÓ <ï/´ý6“,°Ñ¯8ܽÿÿßÿÿÿïxà sÏ<óËÿïwyà 0à 0cÐÃ#‹oÿl0à 0ÃÏ<ó8ÇÞ0ÿ ßÿºÏ »Ç÷þ¹0ƒO<ÿWôúüñÛÅ5w!luß¼óóß4ûÌõóþóï¼óÿÿðÃôãþóÃ<ðã 0à ?ÿ<0à 0ç 1à 0à 0Ó =Ï~óï,0à µÿxã,0ÇO°Õa<“ÏÓ\ú¦=öQÎ5ß­?ÿN>ÏŒ÷Ï0Ë_»Ïϰóï0æsÇ<0ËM{ð“ýÿñÀHŠ™h™ë 0à ÿßÿ|ÿÌ4ó¸ÓŒ0þþóÃ~¾×¼3Žþòó8Æø÷ïøóŒ±ï>ÓQÇU<°eûÃöóï<ÿ~ÝQÆvyhIó?zÕÿ0åÅõãœ5ÿÐÃ>{ÌóËN÷ÿþ²ÃMñóÎqÏÿó<óË 1Ï>ðÃóϼÿëüûÏsÃL2}ÏÞÍ=ïÛÎ ƒ´Ãl Ç<2ëß¼÷OÿÇî9ûN>Ïö{l1É¿þϬy׌0×lwO~ðÅ 8ÃÌ’óÕœqÜÞq×~ý%ÛyœñÍ7þÃ,wÙÖÖ”=3µ’QÒT8ÃÀ_Š'¢'ƒð(ƒÿøÃ ï£ýðÃ~0ãýàÿ}÷ý}øÿŒ7ã0ßò Ãü7â~?ŸýøB÷ðãöá‡ßãþ ãŒ0A×߃àÁýÐþßCŽ|„ÿÄ)1P !0@AQ`aq‘¡ÑpÁáÿÚ?ØïÁï ì/Ëgýì3þi;¤ùEEEEEEEEÒ¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢•«JŠRðR¢¢”¥EEEE)QQQQQQQQQJŠŠŠR¢¢¢¢¢•Ž•¨¨¨½þKö+Ù])JRéKÕ¥)K­ïÈçInw¡w%¼ÝVÞô]Í/ÁïáDÛ®”¥Þ˜q=¦n׆ê»[ÚN+Ö»‚Ýnð¾hþZúS㋠푱!D 5žèòøîÁuHIhÔa}ŽboØ(˜ÑMÁq¼í)i¶Ä‰ÃP†HmË ö„´ôu†¦—iš,®ƒÙÒ<K‚i ÄÚHæØ²ºgJ!¢é°µ k]™hÝw°YÒFö¼ºg4ZBuØL=žè´Ë Ç³f„µ¤´5$ƤÑ&4×9„ê[BÓ.ƒÌÝai˜ÕbC 65ƒúŒZ2j²´Áu/mx—HöU§ŸÈYZybÕúG£m±ÒÕ›U•§—åí ¤cÙž_äZy‘iH†Òbiñ«6«O/ËØ/Aq§ïéu|˜YÒ´bcI˜1`téFM TÐÔFCÁ‚ÚHønŘ™EªzoE¨Y=5Z^0…³®Í{Ô-]IÅ€ø]ôé­S öU£Qö :7_}{á{2¡uÒÁ6Ö^¼ï‰9®²Cp˽•ì)zÉÀúW½OFM"ÞÞÎ ¼ÑŽeãHKFÛÚ¡5X[’bzFˆÈA!%£Ü s¢e/.Õ: å0š¬/™_—¾Þ|9.;ð{߬®þu' ! ¤à›¢âºNút&ˆ™X‡-ÆIŠû9÷rá§¶—1îd!B‹!4„ÛVWAáí ߟò4«¹âA Ä19\2¼>QZŒiٺɶòÅŒãõ ëöÛE7< päGèf—$¢yoß±T”49¾ƒÌ%g×)ÈÈ£M±etÍ5M§Q[)J@\;º2ƒºÕ–1m&ý14hN‘/TÉæÏ3?>iÊ—šDªç,"µS÷àä‚«Ï—¶.ƒÃáú!‹¹#»UçàZd­òfi¥"\òmäqhi["óàiòÌZ–g19ÙÍŽfW4$åQFFB„k“Dا`xÙ+¹y'‘rCšš3 æ ¬H£ ™ _ò8|Фë“ì?Ê'“(ÇÈ2êÝJ&¡;.|™/*„p£õìØâö†â/>”`ng<¦$ÖwÈïUFBU}ät#å9çfU=ÄñÇ6 ­.”¥)J*64:4ÛòQ^Ìø¯¡e:ß‘&kõ¼„Œœ™$|ÑI¬l{Bè­ãH‚\%¯#¨I么Ù!;Ç“(‘r+\Üúº% 8ùþ„ŸЖ(ɼ¤¼¥y^Æ^oÖ&ä!§„§û‰¸ý¥;}ÈûQVܺ<>î"i5š4™‹xCõÏð¿Èg6x ¯Ü°???G«úÿ8~òô¥¡â4yþÆõ">“ñ”¯ýÓè-[+ôr!”íïŸHðöÈB„Òh„DDz>¡¿Áô×€æoì¿bÊŸwc5¡áëw7³N7¢ÇAáìSk|×èN•‰ò¬‚E‘5 Ñ2¢”¥*Û‹äû ÜÁRÑèmϘŸÞ‘­ÿ°AÌA%‡ÈÈí¿Emÿ=ºí‹ ËK©¥ZUÆÇ·.)£r{•.••ô&“bz¬.(Q¹¾)K¥èÞµÁíÊ•*ÑR‰š»%í˜ÐÔ{oVXB8ǹ.(1´2om¬¥ Bmµ)zÌzÎòü¶t/Í'F—ŽìëŠo„ÛZ²…ßã8.ð‹òäú`ŒÝz·hí.ÉwI±Þ$úSä‹Yº.’àCë¾Åñ®ÿÄ+1@PQ !0A`aq‘¡páѱÁñÿÚ?ÛfÏ57ü J_Š]<'MømÓ\oÅ&®í7lsMw%ÑupŒ„! „|ðÈÈÈøddddd|2>ød|2>|2> †GÃ#á‘ðGÁ|ðGÁ|ðGÁǹ1‘‘„|ÈGÁ|ðGÁ|‘‘‘ðFB>ÈÈFFB2‘‘‘‘‘‘‘‘‘ü vóBa<ÓBcBa ÓB˜Â„'‚Á4½…­aJR”¥)ziJ_/Šha Ó1˜BªŸΩ¶ßÉ3ülË ýK¿MŸ?Ãf¹aŸá„ÒÏ'¬3u­døG¬3j¦ë4³Ã›© !êÒØ¦ºxž}Ká7\ó}7–0˜Í<ÑUÎå_ÍlW®a Ó‰•"D ÑæÚ¯˜ÞëJz$¿¡ÜOD˜Ý&_Ím7|ËàY¬U.ºa5”¾*R—TY­"V4äú •‹b2>N”›É6} ®É„!La0›Ñf°z&‚﮲m1 ú‰¨M:Ê—¿E5á-öèWjÅ}ÿKgñôý!ñ¿Er£8Ÿ¡ýCâþƒø=¥þ˜ÓÿÆ3ücO³ë>èùcä”} ®àY¬¥6^´Ë&.f%äÂ9'þá¼>è”Wô…Ê…À„R:oasÍ  ý þ™þ¥½ßC¿@¶L[Ø¿ú¡Æ‡PñJÏ·«˜K´3/,óß=ê½U••Ë/–%{ Kþá k_ž‹Ù~n¡UÚ+5Ñ5WQK·fðda:a °Oa«ÐÒ¢b°ÜcMŒøivLÞ !B„!B<#àžj+ú™òÔ(#±‰ÿBÒ¶_bI/øzi§ÍàÈ×Â"xiQK…^{ÑvŽlL27:\)z®¥)K….»7UÃ+{º+¨y¾«†F×K‚k »Þ„ãBi­Á¡jf¾b™Ž[ucl¢†÷”/ŠM\ùÓÏòÐÒü^þ)|wÇqi1©à¥ù+Q—ÉJ^›ðèOXÌ.®`ž3ÄïK/ÂßDñ5D§‰^†Ëá7¦—IK…ÔÞžÅõÔžMT^+â…]ôÒèéK nï¯ü{,k©­ã&‘jÆN“GÿÄ*! 01AQaq‘@¡±ÁáÑðñPÿÚ?ÙK äÒÜœlÖ2ë`Ô™ƒcww¢^á{q±'³Hu‰¢odãðZ‘·HÜKbq`PeSg@GB2‚d•´lÒÁl=ç9tÅ“äŒNX^Ð(‘§KHÀâC¤±I¤˜ŒËAºÓ4Ø[Ôêlo¹$8İdèé!5´,‡ihHB‰ñš8’:°°ôF˜Ã ˆ.$â …ÐÈìc3 š`â©"èdL$¶ØYÓXLÀ o$³½±D¾,ïY"f ¡YÀ¢2Øè’1°nüIÀ°w¦43 Éî/‚CÞÀ¯»8žâqÀrÌKÂ4-.Ùò¬ è ‰è-í>ÙçDdY%(š{~¥€mýYÐ×v& Hq ³— ùC4{ ±x³x& \E.–A‚P‰ßuß°€ÀhD‘b /aŽÌ;¦ˆ+"½âX`[²hb6´‘îŠ9GL‡¡sE0Ó!F˜ êè# "|èÈÕL±4=ÆDð¶ $HLRÙ.˜ƒÜ,`ÞÁ€YpHq1`>“ À‚ƒvÕGcFE  jƒ+Ö“5îÑ ^:ÄŽ›'äK9Œ&ûtÆpÈ@`0|,qplM-²g0HÁ„ Ò±K´ºHÑ“1‰ÁÓ`.Õ”Alï„ÈëA°Ý©Á€Œ[ݸØd{Z‰&­¹’"Ñ&“1ÈÌxøÆGÒÝdD[I; %ð˜Ä¸ë ÒKÞέî@qšá hË„,0KF½Ä°Ñ!Ð è,ì/bÜŠÇR‘uãH\}Šh¬)£i4ÖFëw QÐéµÓr)¢ °½“&™¸Ã:¦1 Rù€€Í` ,t¡d”ôèÊnÂZ¶`%Ú À$.AÈ$€‹íè"ä$ˆˆ—b’hc-.9ðÇH`éGd€c=cô2\÷ ö!÷#A£€Îh&#¨ýI A.È-@„©‚&„ ¦bJ!=jH$KKÜ©n4é4°ìhJº$”ÃØB5F-sì·¨Í'Su˦ka‘¦€àÀâ ǰÆÀtF}ŠbfHU1-2¸tŒ© pUî6Œè,tR`‚L‰uQ©g@Ñ`Ö„z‘¡4n¯£d’qg÷,uØÑ!°9FÄCHY n‰†h—½dh@ å– ÁŠ1Ò¡6N¦Â0ª^ás»1œ!DºÈVŒ¨!G‡HL•ºCÞ2À"{Ú%›¨Ù£Á1t‹`†NîžË´à‡j0€Æ§,Ô‚–±¼’ ÙÛßI1È ø(,£h¤à‘‚!ö/³ä!p`$˜‘‰D–"‘îNn»bHâ-Ù›£ t²C@qRWpÊŒ´¶$C Œ-F~e†dö–#’:F0—¸Û¡,!4o°OAÁF~³/¸õ„˜ÑdÃŒ¦Ç°$ÑH:‘`³@l ‡²HBDd!Â-;‹"t‰0é‹":=-º ÙhÀ;‡B¾Ä"ÁŒÀÓ9ðèØû{‚( 0›a@}Çzì0n9n?fØÉ'ÇätŽÎ€À .ãæpc öI(šBôʈƒ1Y FÃu¬ ¯ÊÛëë&¤TYLéBkl"冱Ų:üÙI7Üi»iÀ :d;ÀŸ:.›T ;k/ÁôŒ4ø™Ó¸ërC¦ˆÉ ìl„f²H8¨ÚhDÖ]^Â@éðÁ€Ú’ß‘àùÆ.·'64§­5‘*„âoN- H¤%€ÈÆŒ³ßcj9?#`™df‚ÉŒŒTV#£ ±$‰¡ ²d4ŽF£§dìì("Fc;jCŽœN{!p'HÝ,†OIaTeƒ- €ÒÀØOÌ®”aÔÁчD`æ«uf ½$ˆÃØ“Iý—H¼µ"Jdc8!‡U@ %Þ‘ÇH{ã¨~À$HÅVt Iî#ØK®.Õ¤vâIi^Ë=‚]ìé‹=4€À)œï#»‡Ñp6èOlbIÝó¡v¡©¤î]÷Zi=¬ÐšG°}²ˆÝäP„RQŸrA(2Û£'±#`„L!’¢3XÍ:´ H³V¤pE ‡ö–&¦È{MX îb,ªÀ¢£©cD}†ã QAÐEŸ±mhBÇ D%´c!ù'‚pèÄ`žÍ§§K,a´ 5·Ôd°‚YÚ1B!в4¥£ßp‚À¬Û@$Öý¶²(O£b1´û"NÉhÑÄuj‚b„öèBNÀu›2Ì]aoŽ¢6ÞLIÅ“†à‹*‚Hí˜Ø‘Ù:lzVí—Ã{O²‘‘Y=„û7 º!ødÑ‘Ý$Ë_ftÄÞÇR²%4mÈ#G÷3M q ˆÆ½FÉÒ/c‰ †ûŒÀ™ÊãàÈ Ý$i8Ïv$âdئ–†2×µ˜ƒu øgvøºÖ A)"H"Ñ„CiÃDÍT„ÍIĤÓe êHá … ùV;Ig{¶é㢆B‹ÑO„ËsMÒ@–0rzCác°‚а0èGN¤ HLFdS Ò"BàlEh…CÒB%ÞŽ ï+­Û]TÏ@ÝØ8&‘3H5‹j+°*gVîØ8Œ ?)%Y았Så—Z@– Ài®ò×Ók܉ÏbÌèô0öH5SÐH]qrEHïç6sVö AŽb‘±ÇKÞBˆ ``gÜ á£nGlÀû(±¡V¼1ˆn´]ÒqLdv¬1éF6ìFzc±htRÛ AŽ™kÈÆ a¬® (Æ‹ñììHÃì“IØMB=¥•›(5=µ´ÄÄme~Qºâ F¾ö¾ÈOxŒˆìƒØÂlÂ6ÆY`ÞË,55¸¸Æ#&@Úš6 2($!4b ã4Q„˜"BMm]-PÅ1HzÆÜAºÔr³5 Ãl·ßr å ®Ýî’™©†KZÍð%¯i'bD6‡d:1Ó‚XÆËÛFÈ¥¦l¢“´_rcJ@©‡:ŒLlØÓFDq¦Lzì„”„èãv (ô1{>ËP!q ªs»qIˆÃ=0Db‰V%¬F3‚½6ŒX`ûªØ"6l½‡ÆÚÑŒþ&îVê±c„:iº]'fÚ±½¡Œ`J 8ƒ:´hLµÂK ØéñQŽ´Ýé„¡(¸ ± Ú²[ °Ÿ$‰-G²íFQ¸Ð05#kT¦‘¤ÄÆ'iHÁj6g@Rèѳ­Ž V@g²–mc~AP9Ó„¨èLân¤L“°Á;Bн¨ÚuV0÷1èȱ˜Œ(gI5ÇчÉ$Øèš[¡MÒ¢VXlÑÞÊ£BÊ#<(‰Ùé0¤ég[†¥¥€‰$„¦!=4 2€c5„€"GmnØd&vXX©‚3Œ$û™gjX’ ) 8ì°)¥†ì‘£h(ÍÐtÊtΤ‚Ý3ÐE-D,IÉ ìv7‘1p åÒZ‰:¢C=" qÒR4´ÔmöŽîÄ‘MÁ Ü‹‹U •$"–Ž$üƒ¦}–îÂ0¤eÔ[Ædk°Zf# âZÔ˜’P;±±Ø # ›òXh2šl‹”Ô``&2 Igưv1-ö$;‹_çm*FQ7 :„n£{è½¬ŽŒêŒÎGgßejË`hŽ›ÛñIqÉ@ƒ¾4€K|ª–ñ ‚¨^­¦§à‘@ƒ°Ø$=¥6¾C#H£Ý$®ÕH“€ËTc¢­°Ñ•±Z}BÂô„"tl ÎÝÄxA©p‘âЖ€0-P0['G´"†dfïÊXt„ø uŽÂ0žÌ.ጠ-e;#°†¸ÍOŠ]ÒΠ´3#8h8½Æ5ö› O}„Ì–éÚ‹Ôp†ëÛ!z;h »n3I˜c'XÀÓá¦Î˜ŒH5—dººƒsu²Ž¢BS@@?Ɖ«Hv…§ •q€bÌx•#“ -™ pIvÑofÎ,š‘ì¶DŒœ8ؤª CˆÎ3+‚qÚÛ,9£hÉ‹VGÈX(–f$ý‘‰Â-GRM…$ÝB¤„Dïq9î-étDK°—Y!^‘éeÖ†ÁFq’öA€Ì,ÃÂF@îìP~†cmÚÁ 2A02ŠtXô‰Õ„‡B@$‘‚šÃç!ÃCº­ÞOB$äù‹TI Ctee‡a™ \lf¡|ÿ FöYrì%žc± ìÝav%‹kÔÂ3Câtr}®c™ Ò tã" öì€*ÉL[¥†%€;%Á"/Œ©cIÁFÍtdqGcZ:7ÂN‰'̨Òë îÜH1Ôf£ ÐØmò3­cd-2†Œ€swv …EÁÊ‘€v wGÉí(LTv ´Ñƒ, ¤—uÝ€``ÈzK‹t ¦ÝÃsRåèÖLìÁ.à8Ë 4F2PšE¤Ø0tÈ齑‘(Š$Ø ‚ê7·C³wr[º%ØÛ;Ϧø†#n¸¸›ÚótØ:60³,ºÌKG³7Xõ¦ÃQ¶/q¦3£-£ŸdèéÀ“‰Ñ£‰)m¸F—]‘¤¨„´\lR^Ì–¢ àýJ8Âl’Bc ¬´YÑ”=ã,bÀ$8ŽF$†L8ØHýé{Fßf]„+á.ûHTvNÆ÷Õ¤Z ŠH°b$éÄ–¸Ê°,†]l€ü¤#rÉŽÂê/IéÓŠ0%î'²I\Â,µ@41î#ælöÂhËš0èJ$pR 8ëØÝ–âý$Âzð‡mðO¼*JÁF6TaÄ ÉÞ¥qÒØ", Q•iª÷«C|º ˆI.CܵÉN´›]I Q.ÅlS „0‡Ø`hôˆm…ª$®Žlcˆ;Ñ`hËŠ‘ 8"B“¢NâH |’Q3BFäz Ú°§D‘Ðë04?(˃ÓöilP`Ü»Û CÙ‰w[àžÉ 2¦3Ü!ºãdÍ:ÁŽÃÛ[{0E‚TöÛ ]$Õ¨žÛ° J±@a@0ì,¸1I¦â2€0n#aˆ‡@m«ÐÈ»Ÿs‡\m‡©~IM%‚&‘‰"ñ¢È˜Œ£ÚB@lÆ4Qx66ötÒ×-€ÁÚBÄcÙÜ€nÒ!‰ ¦À #ÜÙK~@À$c&tÚâB^Ž’Bu‡CXL´P‘°ÒDrÖô¾I^˜÷^ì&i±ö$…ÀØYÔ¬”p2âã¢B¥Ð¬ô±ÚDA*ôÝ#i`9ÁJ¥‰Év„,ê'd„ƺéµ ôÁBD˜ šíŸ,ˆ©{ÚH¥˜Î($ v¤‹q% a°‘8-%è•.¢„ºF1”ËÝ%˜02ŽìtLOe‘F#trÖèɤhHÇv;L䤆8Û„á3Œp#ÞÕkز­,BìÃUe EÙﱚ¡'Ìq0w">Ä»FpÁ·Q‡v27³«QÆ£`,è‚¶Ñ0ŒAÙPP‰'`Áe° vMѰ°Ð ®’3£ÜÊD‡Dp‹ØI«°€»Ó ù,.™¨¤UÔŒÁ,˜Ѐh‹áÄ(ô³ôDz*‹hŒ€ˆ). VdЛ+Y˜pbf‹Ü‹‹ÝRO\,/C)#¢Æ—°D´—Hï†]h“‹ÇP]­Ý¶ÛÇÊ’|aYs²·´mëTÄœCöp-ïw$GKXwFí I´HG$HÏd•LÎ˶3“˜KíwÝšF‘ƒ.¥qH,8…¡ÂÂhìöC‚2ή–¡‡ØÏÅ ¼t÷\âagD[¥‚2‚ÚJZêŽh[¬‹4Iê4!éDbfö0…¨ÆÄ-c~Æ#±¢Æ(b#=gZ[[t©4Tƒ¢w±=°Q~BuˆAcAÒzT»cP!FC‚Òtˤ£2CØ04“I"³Ð–¢«ašpÜÐKS¹‹ØÚjlbc1!HÚö0‚ ¢ˆqŠtdê¨lk#Ý¡÷æI!Õ¦1š«Œ ´ƒP»!$®1¢pžÁ{AΡÑ,ìf2j¸Ë¶ÀH …£‡¤´:÷Ü³ì¤ I6Aƒ7bƒQ î *¶)¦Yì ½€Œq¨ìÆKGRØp"3UNµ QÅ. \@ÝØîÙ3—KŽ’Ç  ËüÈT°{G#u­mvp‹Üöx],¶Ât‰†ÇÝFÄ€ûÅæÔöxU¶ƒÈŒ¢Ò[¥¨cjîÑ8C9+¬©ØÈÑ‘(˜ ÜXqm-7…(Ý„²>UôaE%‹Ö“‹9 '5ŽÙn¬mC‡K9°‚g­ã]Š…ì´ÄàÄFv„—²3‰¤,‘žà'0“˜¡I!´ü‹^¤Œ.‹P.”…°º$;@ãŒÒ6Šæ„c°̱‘£=+>Ú²¤¦Ú$é­.Çs¢„Æ oµ¦ Š$€›¬—}–ê'`’ÜKIìK@F»H 7E¹ºX… :$+ð…Ð3FÅ{œÍMBÃ$Û±5tÛB$­,¦^á¡=ô, Í`† ¤2¢ u¤ tîûÈ&–#Ò¶»ïÓ=¢†MU!qc¦6>ù,„7±ßcîI@û“@9°s:—K<Á¢¬{ úRpi-$]J'XQ`HtÂîá:Ó°pÐÏ%Ø\ë"…ì5Ÿv¢ô0©w±ù ^ kƒhЬ-ýÄš ,ÑËRtþr^1„±”c%àˆÈô–gcic‘a¹`Z3a'¹ïÆA¯L“Œê[æÒìd·'-„G’$ËÞÎZ—BqŸe 80ö/œd.Œèؽê’'i²´Táî-Çñ)¦·C&) Ù.´R@D´º:$¨‘4Ô´JZHãÛÙdtÇønñ-p‡¼HDF]ºa”Ã]ŒCIV£:J€%@dÏÓ:2î¡a;T°Q G DmÅ-°°˜¯²$ŒiؤxÑM•("`˜ ŒéŸ"J„?”œbtd—ÙÁ±mGPa ¢«!„‹öʹ&nºJ¸KmfÆ>–Üa"FofØ’IØ–è´:6¸ÎHÛ’.¶¤D% :„@!‘Ó €¼zBÀšÝCÑ6A„˜‚ÀDëruB]c `n,0a9‘Ê„;:$C’P>é4“³à@t6˜X £b]kX"˱B“­0îf$ A$:æZ -V1'³©€71‚¥óØÚšNthÈ al+¬B ½Ùt²v: &Aƒ~Vé.¶rËå$& >ÅûT‡I5•÷&“ ˜dºêYó²Bú½í[ƒ´e2;0áB!(» i*2˜$¦ÔeîÃ`Ì£îXˆYÉt·K¨W>E!ÖéWf‡g ²¹b“K“/pHOÑ•N’¸! Ž–¦½J) ‘¤‚¿ 'XDZ“˜–ªSVD”Dp)|1¹HÝš·BÆ Sx¾á 1a÷EÑ*jLbp·»çKé“BPݸ<] Â`’C¸T@@±ì «l¦k%Ej`ȆÈqY §ÁšØgãíl¹ŽÆË€‡˜’jȆ°@ªlblïf±T Ÿ|gl(#+.„•ÅËÜžÄ ˆÈ°A9¸Ø`n®a %;àG ZžÁm1³L!teÔ'2ƒ PÖ‹*v#  0p„q¶`ávQkIÕ ff­ÛÛ‰]8£@š ˆ 4„B‘pdì°ÀàÀ»‚GàRX‚˜Î}Cš%í² ŒT±Q‚,Ä$Ä8,°Áã !$Æf¶ÑˆŒÝ11ÙP–ÊXD &%î•"–$ ²·B ac-1š™cd¥¬'@} ;!Yè,èE,Dlô².æÙ˜–hØ&¬R Ðß)HÀ"pˆ÷;R–k¦ì £¾¦:bHŽÀ€ SÜé$Àžák, Ñ•CðÈE…:@ HËc¤š™Ž9¤b‚@aˆH:AÇoyèÒlÔ’ u–0–Ü‚h6:}$ôÕ#lBzmî"ÁA`H"FDб[Jºö1°…dGobG$LK„²/´a`GbHjˆ±¨Ød2€Z,.¥Ö‰l›lHiÕ‚íÑ`è2^™(u»›¸Â‚Ý»Œ`nÇ £¥¡ÉÂ-†0‘;0„ x « ÔŠèÉ>IĽ˛dÒ*3‘ ma»¬m°[Ù¤zRvû6 û$s¤Š¬'‘Ž2E±M88X8Fé𶪠”Ô’€ÌEáy’#CH€‡eÀd4;Uh{.éóˆÀÚ A»Ы1fè°` ­ÒK{d V~””O÷aÿ·~e}¾ÏöGþï2|6|U¶§,¿ 4Š´Ûëƒp¸ü?г4–ºË?i(÷ý’nYûŒZúåôÿmÿæ§Û–kŸÙ,;ýÖYÿiÿÜGÔ2NQóÑñd´}Ý/”d‘õÿd?ìƒû'ÿqÓý×ÿ­eÙý×â Ñÿt¢¤¾ßì¿ý¨`Àý±óÿBûìÿšÿÓ“ÿÃ%«¿ì±4þ¥ðÿ¸†êŸþ ×lÚtÒB©þ…Õ§õ/“ýhÉÿ’1õ.”ÿRÉOõ ûRÿÄ»„þµ¸ÿÍ éýI7OêGÍýméY{ZU7úSÿ¨A‰©úBñ¬ÿJXúRëŸÐ—?àHÿR2Ïô¡Ä?­jô%±?­$¿ðCB~oêK[ÿ èYÿRIÓú’üHz?Ô·PÂ7ú’ŸÐ†tîü-Iý Àÿ©$ú’W?¡(?ôƒëýIF¡§úÐÍ?©(ÿâNäþ¤c§ô¬·þÿ’ÿ’OêK:ÿBw?äƒ#ü9¿éYþµî¿ÔŸ{ýigþ4à?ëY8·è2o_Ö¿üõŒÕsýI÷Ö±ú—Fÿ©.ÅÿÀ³_ø`×þÿÒ «þ¥õÿB~êßúÿ¦Â?øÿÃMÐýVú`º'õï¿ú÷Óý{4ßëË™þœÿˉý97?Ó¾»’ 7ÿŸŸŽäÞ¬+Ÿý> $™a² úà>h7u¸tÉk¸ÐÿñД(;ž‡]C¬þØH_êÇÃýY©ß‹  Ϙ¹±4¶Iµ,môp·×ê@ÁN‘Ñ rÝÆA Š„ø@YwD æ•=³a `>H"9$@0·Þ¹©úd#ªº¶­mRÒpžxgu¥—œ_‚RxðQ® Z·nTðÅs]ž@¹6hµkj×!¯­plÙ® šµàÖ×eúZµjVÖÕ«f¾­sÚÖ×­m}Zµ ø´Úµ~/Ákê×Õø/ÅjTÔ»vþ­Û}ÆÕ«_Pþ­}6þ­ý[ú·õoêÙÞKú„|[››››ù›ú²øoÅEøøŸŠü‰÷²üëò't‡Êüëñ7äZ{«ò#ì_‘júýˆûŸ.6(ûЯ̾ [û[øv¾Ö¾ö¾ÉiùBûBûJûÚúe|¨øjüŸ½ûì}ìïnÇÞÇÞÇñ½;o½÷ÙûÇÙ¿2Ï™Ý^òó°2ÞήïÛ~ÙoÎtfíæ µíùX_zd-uŸÕð#¹fAÆŒ¸`âu’€z$Âv$8RÓØÊÁµÃddi±3*‚à8W˜²ëx8ÂNK|>³Œçá·¾ çÃ3Ÿ‡ÇcÁà`Öùç Ž3ŒàÏ<,ðË&Ë<œ²sŒÉ$ðË8Ï,$àöäá²Â ,ã«8,<2: 7ŒÛ«¬ç48HÆœØ#D—AÃ’AuX@jHø,3°°l÷ö¢Ìrþ ‹¢0E‘Ÿp,|—Û 8t –À Høc,h±‚7v é‚‚³O…—]£~⊔ãÝ#7Õ €}ílCdNŒÕ_0+„†E‰m›,سf†Ì,¡f͘,س&Å™¬Ùƒ'†xcëŽ> Ï)à"`ɱfÍ›6,Ù,ØlÙƒfÌÅ›0,M†Äˆ6YüÜ;%´´‘5Ž»±Ž q~hä ¸’¿4™û%½Óƒ2ÝÐìü—ä¿!?q~kó_˜³ù-»ÙwWIô%ˆû®ëºüÅù¯Ì_˜¾äº½ÈËܶù8°öKóßžËä’ù/¹ ð#N’ìé,»ÒäâüÅ•õ%ö%ô7æ°ø_ˆÇ݇än$3O}%6RÓ£KHHM÷'-8Qz`ZZe¥¤%§İœtžünôFnlö‰Æu«u7[¶¿ ~Æ×Ê6†#ÛhˆƒK¼I Í!Ñî°a Õͳw j QÆÀ ;pº ,ƒzßE«%K :,X~°Hl}@Âè¾ ,»Ã‰ú8pcè“~"~±ôX mž 72Å‹ ‚ÃêÇÉiñ`øí–tè°×¢ünë!cƒ0| 2º= q.Î{+6~¯ÅÄžCÀ~.ÍŒ´"Ѐ†Ô»&?ÌbÅ›³“bÍ…‹6¬È,H±bÄ `رÍ‹,ÙâclØ–Ĉ+ï}Ù;šØø[µÛgín¯Ý³ö³öØ~PsÝáYûߺr½¯ß~ûs_h_{_hLíÄ÷FèöÖ¯ƒqñ+ìpÛÛ/’{à¬w½×º½ ¿.1Nø¨w7öÍF¸É©`k kk¿åƒÔ$©‘ØHíÜBrÍÔƒ‚ ·Q™°¶­÷7oÙ‚rÃ<âDï=ï+ÆóñÁÊG9à2Ì]ì¶p2ñ¼‡ŽóËÁà†s‘âñ³³|<Dò[§ÊÆÛ,ͦz9Ðà $xÃ-²Ë ½,ôž[8N4ãm,ô ×RMÆišî^ò /tî­ýz¢`«eT0·«¤³œêqàð럖ØÎ¾BO<á㡟ó|w|NòÁž3Hòx3œ°Ë8 Œ¢ÃúàH^ñv-œ{ó‘ÇOœç,Kç¾f3Ó ½ñ¶ò,òø¾,æ¸ñ¼Àòïku¼Ñ-¼g›œuoŽ‹ž?>žòø…Þíòò3ÆEß–ìÉåÙÙ$0Fß=FøÝðÓÁÇcŽÃeØ“·ÄsÃymñØb_28Àyå ®|3‚Î:È,¿cÆ‹Â)a|[ˆHË ñÓ¬ÄcÙ#¬Œ~`Ö8à[©q³®ÆÄC5d":ÎoDAh`„µÓmBÿk ðð/׌µ8ÛxF=ç.ˆãx 8Ø&< b÷xËâ׆|:H œxLyø<ÆÎ:Ë<7ÄðvC‚ï,åÞr }<á}NsY%‚T'Áß ó'8ØäËnü àààxê1" 0²ÒÕŽŽr=¸È±-àm•lq„a] ‰ø[L–‡Ä߇ÿK¼H„ß0MB—A¨ C ŠÜi ›ðù6ä# ×÷ãxpÃß òrôDwä<9Çv““ã¾ ¾%²g=gŽñœ|Cƾ]¡$s¼—ÌKŠ6ÛÏyàp“<ë¼+Èb%UW…9ÃãæÛg!y8Yxx'…#|4È8Øx Ví¾,ŽË-!º“¡Âë89øaÖÛ»3Ç……ob#4²Ønµ³’3q˜öfh’·¬½æ8¯€pébC F £QÑÕ5ÖØ†ìܼ±Ðî,t/oL¿—ÃÇ--ôud6ß'ÄÃŽ­O—‚øºà7Œ·NX<;|•œóËyxÎàø‚ÞLá,G„åÌ8Ã<_nò9=¸uðFWx_ǯ ëãl™9WŽ:ôwž²Ï y}¬ç Õ¼èK¯;é|ó¶øõ¼ºœc¶DDZñºñºx|mœœõk±lm‘Á “ ð°*‰-)ˆ$»7å‚ {´þÓ€D…€D‘æ">çD…÷8°ÓÌ:‹p6ɾ,”bΣc'_ žÙmymއxØž6 8íáÞ3Ë=Û-¾&ø>’ñ¾‚®q¼üú9Á¼g®AÎAgPYe’NI%œá“Ã8?áyã Ç['!'9vroˆ¤ët–ÃÖϸ9Þ]¿.Û:Å…¬ìú ¤§¥˜.Z¦ö¥H+è `Ð`ÝŒ0–‰¤{T‚N˜éÀ‡$<õƼjÛÆÇ9À x‡g;iæ»Èñ¾#èïŽøž÷QÂø–FFØYÀ[Ä7À/èñþÄŸý•÷/á ì3÷i(Cˆ-H’I#ÂYѱ$óž.røgdò³þ)»lAw¾óœÆòì{Ùl1¼ñîvŦd¼ 1ÑwÃ9ûâÞµð0¾Ëˆ=ÝHQT ɨ„AQÁsHU/E šZ«œ~àì#5€Q ¿´ž7¦Æ>¶Ã˯&z»Æ»Æð[|ð>yÆù=q™À>gžX„˜ûoÅCöÇïûT ª}¶â±ö•±Ì‡—·Æ8þæµ/´Å”:Ê5tšQˆb6YÂ]Œ[$Ù3à²ṗ†ú€¼œ‘¼o–pp[ánÄ…‰Áñ°Ãu¯¯Úc6±u¼dk“m½À$l&] z&`b[« ÈÌÆ±‚½q$Û:€¦t\H”›¬%¬véàÑåOµìµà÷²ȲÛ|ÖÞOò9sŒÎK}=îVfùo/ oCò_',DpY™ °÷½öƒú# ¿ƒAû³uŸŽT ÚGÖˆüdl<&mð_ÁT<_LÊéÚ+ÆywÉ‘ÁÈœw–ððÏIâäàMœ Bº¤ Èp  p7°Þâù/⊠_ìeì3`‚–"1¢XÜ`&¤ÓŒN i`k`ÄÁE=Nѽ–rÄo]Iá³ã׎ñ¼÷ËÃâ>'¡¼í¼ì¼½Ûnøí¶ðG޶—Ä}Z;ñœgêñw>Ì×ô&ÑóêÏÓ÷s«ÖßH°Ë~ðÿ¹—ú•`ÙWÀ-ð“󻀯i> këîÿ9w:$áŸflœ“†Î~|B_[y3ÓÃ’â=ùwž>#x\`çm»M‡­yÒÙà^6Î{ÅŽ…’¹’aõä#… +VŽ,tõ\I‡Ùˆ!™&û´BVN0…Å ¼;ËBî †~äx çgð9Ë9Ë8Ë-¶ÙôO6C|·‚<WÀ‚Ä`ŽC‚D ¸ª¿E÷ÁîÒ28,Rüô-@¯žôƒ#•ªÊmm¶[maHe­ûî¬Ï>Z™+‡Ùð]Õ˜|{­ÈiÆ$îI˼ëàøæ>žxcžâppf]AÃˇ ’Ü1‚ʱ0Çä‚—æ†ßL}Œ0 ¢›{(ÁtŽ”F³€Ñ.±Á]ÍÈ€æ½aœÕ®ãKô°º $ç¼ïžø/Î[ÕŒó°ÇgÎÃã¶òø˜O€žcò>X@Á)Ob,Ëù>?‡1Yú¼ þ:ºrèû³«x[mãe–ÛZlA„Hî+§éƒÒ8!ÄyÚ9v”³®lç$åÓ‡Ú]çy9 |~|ˆmîøÖ8<>-/Ž>^³[f—ÆÂK¶p[ ó1»cZá–&²l ÷‚Ó­0I…è‡Qp#Qï;À€‚NÐÕ»,„är@Ì.’ìÿ{àœ19,“_-“‘}aŽ7Ó,ñ8ß#È#€‚ܼü¶a»½×-v9Ó¿¡wj;ÜNËÊËÎí¼ov²±¹–Ã%p"$‹þ3äSÚ&‰Á‚""&ì©'zGö,’ìÙÓ<<3<¾&‚øg;ÁCï{à9< u¼t¼8TÖmŠõj€3°b@:L•mowI'h¿È‹ã±:Øï9ê|]x™œ-¾–Gøœ>ŸpÄh³ò¿A3»ó>V7Ùøa, ª½ª²ðÕ–ÖÙÞF'ˆLãXe QïÝ =/¥}'‚7»•¥ód„’x§šg¡Öùg xìGÉÎpç‡ËÁuã½Y"ðgoPñÑÀ[<ý,Ë¢jÿe¿Ô)§ÑaÄ $ˆ3 B¡£€¨‡°/ˆ÷¿/°­‰;þÿ´¼3“2X8ß'Œó9߇ÌzCÌôw…»à8O|" »”µ[NA~¿«—´­`è?v²ó¹ã®Ý¯¡¤ÈØ­]WËù¨Ááå ðÙ·†øô2K2Øßäð8>xF3ƒ_@ñËmÅðw" °çu”3ÓIt¾"5î5,Ÿ¥µ”fÿA áJºüNbšHŠaÖ0ƒªAÀ(2^ÑA^ˆ‹å”1„îòà’Û9`äÏOàðÏLd| |_EàÏ5\-àÎCœà-˜Ïõ~}¹é¡ÿ³,³oÏŽpï;ÁkÇpÄ_שñ^0ÙüÒq¼¬ÏÙù!Û·£ðd’g-µÙõÈ,8l›#If8<7““ƒbL¶é㬈ñ¬[äŽ2å Æ'2FxXò<°ìAh¸UqJ«3´%…u¡!H¤´N)ôØ«D;@Ïç¡~"Ñ eìÝE÷ ¾Æð3ƣ᜶zøu缞éÉ‘‘aÀASvÇóýQààúŸþÿ"ªªº«ª²Ï9àñ¼o:ñ—ÌFï"p!,†º¶‡õŽ2Ñ=Ï‚þ˜$xO$²gÉò Ëc8Çdy,Çãr|7‚Þ €ˆÎ:³«c‚ß"Þñ’&f#Ù‡ƒŽ¿¢E%¶9aaÐC‹lÖ IWNîƒuePÜ“ç>· ¿Û²\9>)éo¡ž™wo¡³Æó¶Ûè§$rp'~0=ÕŒ^ð¶wÇl`Ï‘}‹_Œ¹„¸K2ð§¤3â1à,37bß®5$ý2I&xvypõ‰a–XmKc³’u|œçeÓƒÒ":»SŒm¶Ó€·ƒ!.¶ÈÂ]HxqáŒOm€ýÁŸHN#pÏbÂ-(Ú 6¦†SÜ4rB8J©z‡Îøxüòñ™É>¾œ¾¡c>ž© {Ù°Mëüø¶Ž–\tùêñÑ>-·{dñ¾x6r0È0|%ÔÁôûñõï¿×y%äb#ˆ“32ù<>Ü0ðOÎñ¼‡Är<uÁÁoq‘øõmñ¼uÁÆÄ:Èð‘wȇ0oúQ/6YÃdœƒàœá¼g)™p8'…` D‘ä û÷G&rs“  GÄLSœô_| àñ!äËxÛ-ÂØÏ,|ÔºàËåã8#$™ {&ÇX5KIY?ˈm IÕÌ$a FS!ÌK é.t¼ ’xÛ³áè»ogƒ·Éâ>' Îð寡†GÁéïxœ‘¹¦h YÑ¿C8öjqü½ŠêðX%’HÏ&@Yg\g|$œág œ‹ OÆOäÜÆH ì;ÖÿÝI™Æs—ÉÿO"8"<_cرäðX\3ƒ‚ìžt°Ž™G]}²4KH1äÞİö“-¡ÆaèÎì@Ä:B°’‡LäØc±v&+ùpš%–xÛWÏâéôXÏYõGÃx·_KçŽ×ßW ÃkG÷Òp@®ôz$—¿ñÔ}äoòpf²ÆCˆˆ«€¬”ìiõkƒ^Çþ‰ÒYÆBgè”{“ÁŸ˜–2Ú—ägòãþ—qdžl›?Í1CÐwÞpååaãxÞX#‡bÈy6Û8E‡ØvOOè°Œ÷Œ+q,{˜§Þ¤¹–#( t^Λ7Ü`ìÅ7Y²á8(Û‡ƒÁhœçÎóž=zû߆Ùè‚ô ðp§¼Ã+Ýv³â¼þs¨ÐÑ ,Þÿô_ñ–*ÏÑ1ýrY{3€Fû>?9K¼—»3ˆ¨³K µzÜ}âm Cõ"¦ÉnZ½{‘ ƒÀ0ƒÞÿó6ÈüìG$Åä—eªþ`þ9ÁwâæÖ ²ð¯;/kkç¼­¾€£¤p>?ÁÀå±>9Æ[À|¼KÎ|qñÀD6»d2Ưх‹d…¨š‚ Hûc+=Â'tI –ÊüÛœ #6¿&¸dz–«Áœ¬Y§oŽú£žæðò9o–p)‰²¯¢<Hzf¯ŽÙ½ˆk þÙáP½µ„zþîA’²BŸ8ЈÝ„Òû~ÛŒ¤ª£>VОÀú™P ­¿—C¿JB×ùßÓá»~n»%uˆ?ß?mÙ†]ÃðÜvö0ñÝâz/§¶zÇàpLsÔXÅÜl2ç‡G[Ü{ðDøc>ÜjpÕõíì?"_å`˜%~ ¢G;`Çä°×°cçŒA.žÇ8pâs_¥g­ð«Iö_¼o‚G‡YèçŽñ¾[wÆyï&>߆çç¼0ÑkžéýÞ3û¿ôÌq?_óqüXŒÅ"æ½×ûR\É¿ û8ÐŽý±²=Ê¿bhØñþ¸è pA ù_A?î.ÿɧÖü·C÷:¬cw$O²OöWò߆Möe¿y¿µÀBß|~‘ý·¹'8?-ŸÚ8}˜¿%?Ø333Ç^¾ô)œoŽøl< ÆœïšoDuÛ‘²!Á—Ï| _ –‹"5ãÕó‹Q¤€êS1Xt‚ØjN’ ×F%°&uk…àøá¾gžúG/‘ç€ðFòzg¥ÉoëÔß‚÷ÇñÄö_—û» uÇçì u>gøoT„Xm=–á‡4÷ô'Ëíà¿5¯Ïÿ ‡ëåû\{Ú‰d’MëçùÞŸéògôõ~ÿèóÙ UûÿÏúÿáÁ$ˆŸé?Ü]ÿmffß ôC®v((zE±áÇÄnp¾ÏxÄYv|ËÄúWwlàÆ~ø~oùï{>Eò_Ùåïã¦Rfß]òÞ6É´p`bú#o0Är±gdp1kéÎ>DwË/JÐ ÁîûŒùŽè'H’£7L€U‘M4GÙÄæðhÀÝ—ò¶|ûm¾?>Ë ÇSž~ïÝà<íºðFh4ÓÀÞ¼KzqË¢8Sà¼rïû‚éú\vüïpH·½>*fð( (Ñ=‘ä‰S  g}ƒÓôýi~’«qwîómñÅØ;òýñíßó‘CôHØØÒù8žß8$û?ôsýå7ÚY*}ø~úxaø\?¶O ø>gêkêÉ';q×pø|Y,eóÆxj6:ÐCKá'C`SÝ::—}0¦ŒáŽÂN"¡íE›|8º~¶§\8žŒYðøãmÿ5ã0ããƒ×Þ6.¹8uâuÁî/÷PÖ%¾lô˜~ïÐOÛ…í(ˆÎM\Ó?¹ö?O€÷+i¸®|¤K­þýñIÑ©û.OUþXDZI‘3XâËOtÿgÀ@ì:Ó²_Þu¾·Ïøµ·ø+N‚7ÖËþöÀ‹Ø5—ÿk™žñß ôÜð8OàaŽ‘Þ3‚S‚Î3ƒ²ùácæÝaÂíI CjÍ$81ÐN˜ÊÅ$ˆôÇÝ q/`÷»¶À¿Úˆr^$q²ÛÃüÁÃ1|ø«žÀùo¦êŒ"Ðdžî³²u bc±S†‹òm„?ÜŸEû³ëÉêßâW™ýÕ+ÿmµ†'àÊþÏæëv5E¶¬ÜûMìJh[ŸâSáòÖ/ÿ]$áð·ûù!MÇÓôÇÛLo”Ï¢‰ëkè‘ ÁÉáñÎð<Àpf…–)Bs·Ëdp:¸FŸ†" >Ú Êú#0A¡ u1 ±€¡8šÊAùHµV£ý÷ Õ‰ybyØ»8Õœão'y}2^K,ôODðíàžFûƒc]9ý{“l(væ—*VÞF"-=ÀbŒZfî¹hÕ–ÛaùjXüiíý†?ùæÿ)ÃüþÍïbƒ¿ÞD³ážùßøñöàýå>oì£í ùÑ€_|ÿvðfûëüU)ž_'WÅË}'Õ5䈆7“x9G8ΓÈ9Ë¢v67‡ÜqÑwâÐ~s`èÎ\V&²ëÙ!ð”ÃBÒkd´ÛuU¾áö½³dppñœlú#Æð¶Ëo¥ªx÷Èø†ð0ÊÎý«0ÿá¼{?£}÷“À…¶¥µ‡ãa†ÛKxa–ÙV¥µ†!À«]‚+£õÕ€8-]ùûe—eÃÆ¾‰åß§ñ輜‡;ÆÁààD¨Åß|pg)ÓvXœí$pÉH¸@·VL èÊʨÉtº O»§amÑ”ÿ¥‡ö0Êöφò{ÿü^Iðøõ |ˆeˆÚsk®>3ËÞG'ÍmóÞV8Ûe¶Xa¶a†³ ãûµÓô6÷Å”YfmŸðŸ\Žwb8ðããsÇe‰à†þþ9bÌWõ>Éã_1ðÖØI[mµ-–ÛmàKa¶ØNœQ©¾Ÿí÷^$ÈŸ½³,Ï/;æ8>Vðñ¾'–øœí¾ 凈ó¶Yãaº^ÆËŸâJ«3cG¾ÞànKY± Ñ B³Koƒ†±ü_í<>Íìðz-ã}$õûgæž°xm¬/?>CÁ)Æ@!ðŽ ›üxÈû§Ú‡¼<±Îñ¾KËïÆø¼Äпçò—Ýá@[ø¨>#àdpÏÌlìÄeœŒc<np('ž]vHc”ïâÐ2 錆‰À¢Ø«7KN¬Þ€„ˆVÛ —o“ÿi‰ºñ¹ ¸xîzã»áŽq¼©.«á§‰ÀycëìùŒ2Æ6¡Ù¿¯+«øßšø'“è¯/ l¬šSô|°í…?npH°ŸÆ|¶«hDfx|·‘mÿñÓñ9HO ãç9ÒÓ‚ù9/™ðN˜/›$S7ƒJu?N{â1%ښݤt½áq´ ¶à1=˜M›Ý&?ÞáÌ—¢ï¦gùƒÎ>¨øàoC)àŒqþë>x/9û2ÎhgŒãnâNDµÑ%ãn‡ÃxÓŒs‚ ΂ȉßÂpGE¥pÝný„ËèË<¬ä¹/¤Žz¦xœg‰ GlxjrðF\ (Go'; òÝ`žâ2}?)cñÅ;'³¢U-ÙEÁ¥‡J”ÂÄì /b^‹ù˜Øþw 'yÎáñÞO,óÞu—×Ûçü!‡!ì–##¡A,‡=½£ë’I8|Ï'Ñ'P›¹oçôsìŒÅý|nʲÚK,ÌðäòÌx‹æ‡™ÆÃÁg‰ G ÐðÞ;èo¦sœoŠcÐ"O[<Hx 2tq€_Ö3‘îþž¹ÇÛõüç¿PœßÈüÌÄá9Ë<20ÂY¤–Gè ÿ}ßhCåO•ç¹–8Jü§Û[tÛ,²Ï$*`Ô'5ÍÏñ·üñHgYã{„çxØám†ÛaàŽHqa‘Ô‡NH ä¸ËWIi&IÐÂ(ÝnÃØcØžü:¯sÆ3ÈÿЇ'mžèÁªøéâr%½Ã"ØdB,:ú!yœ¶Hí ´ÿ5î?/êÊ*%ÚàðKæÑì‡ûSÿ¯¾Â}÷¹þהР#O›ß>çA÷™Ô²Ë/Æ[âúC螃‡§¼¯g ¼ ¶ÛÆó°éom¦ëa¶Ü"%€ºžŸÔã}ÓH\‘ÕÛ\÷X&“Š ‰ÒÁ‡rCñ!>83oô[Þþî¬Ò<Ï4ðëÅxÛxçxZÙip-òè³ô×ÅP·’Øa4°mÎniî_ISì^ÐCDtO õ²Yâ?Iþ×u,ú³u–†âCæ²ùü­º’±V?a‹>ÌÏö0ÿ›‘Ÿ©^Î÷þ_3*¬²Ën–ú ðø÷›éöù¨ž 2<m‡Ày4ãKNv0†FÓ‘‡‘‰u]x>ß‚x»ÈÙ q•ÀIA•`θʡ“{‘¨Æ¤ ŸiÛ.£ÛÏ'õ ôLÇÇxëÐ88'Ìÿð†ˆ§€_.æ}þTß)>¨eçkû[Y¥ñ#ý’/í½á˜²Ë-¶ð-¸øïø/ŠúÂ’GÆð0øñà/މð6Æ‘—²*>Èu°þ~--ë€ìL£±²Ü{¶®ß¡³¯d6ø o/¬œ<ï‘dNyœï<ŠwœìÀÛ 11/dD–?€?ì½€?"Ý—Ó—ñm §ú<¨F‹?$q߀ÿÈÀ§Ôª”ömrËÙ=ÌÅ–YyÞøÞM=ŸY=òÈ"`QÆúñó¹Áõ“ÁÀÅ„p¶õ¡ï"3ª@’ RWE•» yc#4[h‘1_¥{ÞF9Î;Ûyë–Ùy`çyÞ\x olœlrÙžxï¦Âúg'#o0ÄaC‚ömÏÿxKÛè#»ºÐÔ)–»„¯Ë.b²&3Ye—ÍóxëË,³Œ@SüÌäàð^7Ô‡ƒËm!¶Xcr8aàŽÝ¨ñ˜Ã4QmNõ¶0…Ðî@clø°üÁÿrq* ÖúÎzìg:ψ§™o†x㛞Ãéžx ¶Ûm°Ël²­–YŠË-¶Ë/«¼g Âx¥žæÿ‚g&øið< Œ¾{à±|q¼l$?2¡†€Û¤¸í9,:«Àx%£ƒ×5 úËš0ÐB4²‘’ChÇí{øs<~oŸIòßÏóuÏ_y9ùñÖ-†ØxØaÉm¶Y^ÙeÓ…óÛeô^Yÿ7¢yƒ“Ï}!ã\Èß"58"!‰Áígù ²øJm@3#w>ÂU7 ë¨ ü7bZÆaÒoó'ÈðÛoŸ#<³à|ñzôsË9ÏDφIaàÛaRÞ6Þ6fßÇ<Œÿ Í³Ž³ÑUðÛ|NwS<ö93"ØSác0ç¾BKFÄ7ø Ë3‘‹uÅ—ij;ÑFÃCt(å¦A´7íÍLa|ØÙgñŽÓƒŒƒÌãlõñ=ÇÕÎü^sƒÄçãðØyÙxÙx[x}Lò<ÿ„z;ÆÛàrvˆòÂÇ; ±ív1ìppý˜Bmóƒ`®™„*w÷€Ü‘ "3“é7Ì!ïY¿Îã³ÙžÁêgdødðCËá>/®yožÛËËËêï‘àðÿ—º†ùo–ñ¾{Êøël™‰Ý‰l öЉóHÇ&o°S¦t°Ë@$®° —z™|wýo&:ôLô4ñÙçL‰¾#À°áß3×DÏCy×ôßñ²<ðœ³Ð<ˆ¾|7·È^wŸŽÒÒœx¯Qsï¤Deögp:ÊÚ7CE€ ¤}Ýgúy'QèœwÁŸàê0*)ˆ¢ù<ï‘ÙÆú[äúÛâygøyÆYéïŽ6øê|øwÎñ±â[ÁÎÛiw(íGéüâÓk ±gÚHÙÙÐ,gE'àICæ63eŸ¥åÐòK"N}'Œ†Î¬ºå·ÐÎ2t•]x É;¡,#(u@÷cc$?±ÉÌc3Ç#Ç'§Á<^DxÎH|3€,»ÎsÄÿñÆšþÓÝF>ñ¬Æø‡_´âèÃ?-’Ä2-`'ÄÚô¦Ÿ®’ø£±õ—Û!`Æî ü ¡’ŒWg ǘ€ K•uòÈ]4\ÏŠØJ!Ð~YDiñ (~5Œü ìp#à~lÊX öÂ=–µ ð%bо̘ŒˆÀ±UÓ"– iY»,,ÞBG0b(YŒê1›Ð7 ØF0c좓ÿ¼N!¨lŠEcŽ äã¤ã8#—aµIä}?q7~+¸Ø`Èh’ÇP‡ =$è.µƒ­€t„ûXÞÈç=ôSÁý[Õ‡g—\gÀq³è™dú®ˆFÅDH:ýÛÒª‡ç"´Ö÷ WZ„ òX>B2!¶»´ûŒÞú>þ6Õ@žDk>^áŽ76½3»Ðà£=®ËùE«T°_›¿á=¡¾-€Ö >I î‘›¶û¼Új¸Á÷¿‚jcÏOÌàl~ b¿n]©HCöiã×Ù!ý5}ÓÞ‰–Q|áz?;¶Ÿ#¸Y,}i„µ0À›ü¤kë½=І8>†áþN†o‰˜ïƒ¼lLyŒiØÄrC¶Śzÿòú‹(#Oñ|×£Ç}fïÇxù'ÃfÞÄØàöä|„ÎaÏ!ÏÎ'qKXÌ’Fn-ÐãmÞ½H¤±Á–ØÖö‰ÿs’(AèmÔ >¯?< ‰¼{羓™Âÿ„¾‹âpvÊz›Áäpl>BOl2í<¢ÈÚ þÿâ)&ºBIFIÍ’ùàín„7ƒ ?ÏäèD>ø3êäÐY­žណ>9Â%ßèAóâ5óz¿øÏÓæÕ1å˜xž+æòòÿ–ú/¨œ?à>^Yv9ljò[ƒ*ßjÎÌ$‚éi½Ø¬®ã¶Fµ¥« Yÿ›ú(Íü#<ñÞ3À±ÿçxå⃘Þ#µ¯Ù(b}[¦%¨i/¸Q„tέˆ^ÒÆKˆ¤ŸÍ›f=‹¨,á‚Þtœñë"} ä÷àõÉ l}G–0”RžF® `&%ètôiøbxöÌÚ~žÆì–±ýS›°×aCèÁ©þ`üxXˆ8†¾ÂR¿dX€$‚áóßxìsóVaÖÏÀP‚ÒYjÇ”,Ùߺb¿ÂÇ„7ñ6Æe—XïÅšºËóÈšc–¾-¡4+÷²D"¶ŽÛ.÷SÜ~eÎUðk9ý…ýE ‰ŒÓöXTb'îÕðk2Š^ÑuZKHá¯è‘Ÿ£ÙIÄ}Pò|wŒ3Ï9x} ñß…ž§`Cn >BqU3â“ÛúBÈN4`+·¿ÉUýÏXR XûÒ_“Ð3ÓÛ| ÞCÏ®VxëÁçeãx|2%ãlãaà%áyQ!Ž>ÜF·EOö¡¦¹.³Þ%Ðì”;0SŒüœžÄÛÙ!Ѓ¶ÿ0ž{„&q¼e‘ÆÇ gÁ¾YÇW[Æk`9yâqœ§Ž]ÿüs<ž7“xÔ¶à"#ƒØãcãHš7gH{ßê +$ŽktšIŸI+± –Œã¨ÊGvat Ëã\›ÙÈZg9æáä†ÙÆÃãŒ6A–AÎññÇY%œäqq‘' ÆygÁ(¶]ÙÎx³œcu¶x%œdœ!ÂIËÇ^š„§žù<;þN¾r,(ðpiÀòÛ¥±{v‚Þ.EjH‘ ć`g2 é% ›cì¤lpÇsòå„{ù'Žq–s‚úxIgžs9&{ì‰g Ây6qœæó—y™ÆYÇIÎYg¯Îç “ÂOùýúXó¾O·pxpm³ÁƒäñóáÝóã–Ç€,9À¼ ØGQöä>€Š<=ú235 ”:VAg®Ë°K`«©¡×å<9ÔqÞø7ϼï Ãá¾*prO|÷¢ü·¾¯áäR«£ 9¾ùÝû!‘ôOyÕóxË,³’ù\˜²Î2Î3€Í8Ë9ÎïžRN3‡$òNIÿ|E-wòx}1òÇÃHxßñ],äxØyî.Ðàifé,@ ]—`|1¡ŒÎä(ÚÀ ÄeÀÇŽ†’é±áB,3› byëœà[^œä7ØWŒäˆb6.ÿ†F\Œ¡ü’~Û³¬r¨ÈÃÕ´Î>∛„Ò&ýíðôí(Ã4? /6­-ÒZ0ߌŇá{·ž”ТAJ#ýVFË ¤±vÒÌ2^ØrP-f¡nƒÀø¤{2V@zoåÿ’úôm ú¯¨¿¤±ÿÀŒÿíLŸ¹~Ñ"ì?h{‰`,=éi ¶Ûm¤%¶ð3lrIÂHälœ$ð“ÎzG¬C£ÖEž:ç™àø'¥ÛÉÏ\žÜg‡9â3 :‚™îåûÙLd×uxÁ#EïKe¸NBn‰ £kd”}ù3Þ>áç{ÆÇÏyìRí}%Ý=ch‡±3OÓm²÷¿.Í£PGél¾E¸ïá1˜ŒCï¯fgåÄm×÷¡¿„[½Îì^ßæïæUóàÜ ž÷²2¦¯à²×·»¸À¸Â“£˜n7æî{ÉôÞîÎW øÁ“n D)¬/Wõ¤ !}¯“QÄÌþ#Ã á «7O÷eÿPVÏz‰~”³{?¥kíúøÂíÑ¿sã<·³þcð§îëíûÕøÿd˜ß~¨}ìÃßøª'úˆû—ð?ø“ÿi~[öW\ÇößX~Êú_öПÁÿH]Çþ3ñ¿ê_ÿKÿáÒ¿Pýßê*ì¨ogû§Ý풋ÓÃ$ó“þ/~³ž§Pu¼œ'˜¼±‘ÁÆðøó§>ð:Õà‘~Þ êÆÂÊãô2Ã$ÒZ)!²;*üK¬,FXf3ÕÈðÇ·§tpñ±†ÙƇ+àùg Ƽ*ØÂÖáh¢%Q!¿v” ¦Ñ\1Ñ‹õÚ¬n]ÐÓV©rä0,ŽT³öÒ• LÂèpôgyZi¸*ìÀ‘q‰™ãgÅûñmÊÆÓ…Œˆóhè™Î[Áe‘\ S‹ä§H_ƒöB{ö@½¨¢>dµï í¯õ@öþ‘(u÷nO‘|àû\ÝòDGù ŸÓÚߨ”šb}ïôm.å÷ãúYOõý ×Ú‡Û4ù‰toó WFý½¿lOÃûbiýÿôÈ}-ú­íþB@pþ‡¿õŸúC÷ýã&?ÉøOÛÿÃmíúAjáü¸€ô7íŸô…q?¡}#ýK¶ý$‹Ú~µ{…Ávº}’ý¢}Â9Ç”2âÎsÃ?Ë×'žýð_ë”à9%ŸÛVÐë‰ gžðd-GÈN¬*$`,I»AxNÈ%áXñ<ºðΣ“Ãgb.ø3ŒN~2,±;-`c’™e9%‘ÃáË$eJI?+¾ôa}ïû÷oá}éüGÏü*Gáþ 'Úý8øCõ@ô?ÁŒ=ð7À/Ø€˜ÿ›£Fý¨ø[ö£ì§Ü[ôÇÏúÐ|ƒô#î¿>Ñý ¯Iÿòì¾Áý–žÙþ!^Ö?¹†M?Ê}ÅN¾®|_Ô6/‹±öÃ>Tò€÷X¶>amìRžíû¨û߯KÜþ.¼¬Ü¼>ÌÂú/õ ³MŒœAÛ’ö—¡IÆ BÐË&G€Ž ãmãyÞåá÷´áa‡ÑÎ2N c“œê2 ±cu³Œž08Ë$³†Yddð@€œpÜ‘<¬nŸ,²ùÇÃýŒ·ö°ßÞßšÇÌØ7Úß±~Ó”÷sìþ%#ßÙú(`ý8ø_ÈP}¿€`: Ñþéñÿb>ßöíËÿØ#çý(û—õO¹ý«îÖßYþn¿dB{§öCü_Ä+Ó?p=¯åXëî _qú1ó¯Ñèß./{¼|rdûSòKãñ É1Ã>¿ÈÂ!ÍüöèÌw%>‘ˆ¡-èœxÃi\0¡íü¥µ´!f™õ3ÿ’œ‰·NwÐ8s'«l ö·`%s VÈ €æ% 2–Èö‘¦†BÌpGߎÔÜ8éK8Å&ÎN^SÃ'Œôy"8‚Ë ‚ ,‚Î1Ë$ 9ÁY,Ò ,²ÆÎƒ²Ù‰ Ü,€»"³¸†`Éd Ž3†Y'3$‘%|-õ-ôsìËËû^ßÌßko…¶·1ºÓ}ŸÄGÉaóÿZö¿N>ÏÕT:Ѱ —_3bdˆíZòÌwll34=€š²kkì‡ËaÛ]F Ÿ(Kàóþ¤dnÜSÉéð<——Ó?É7gžƒÄÅÂ3c‘ŒŽ:€³¹`i°vÏÊD˳:É Ž“`ÃôÄÛøî亰áá[Xܳ•×Áðß¶I·ƒ‚ˆÈx 1x#„‚ ,³Œ²ÇÃ@k|1ß© 2ämc$eDÙ0Ya¼d$Ï%†É$ŒìÉÃ3<<røì(‹a@^ȉ#QT”‘ƒ™àèÁý£’úK½œ0‘·»ãcÁñÓ—Ò8é|6ð6øé6a.›Ù~¥ýK¨Ý*³@÷-A/dÒÃz%`$ I:WR(ïÁ>ü|_6ñÚżÓ¼–ðxäx¾a 0ÃÁ0pA6Ydœe–0;>ÆÇD`s‡‡žq¸Ë'ZÚo5Ö "„ŽpÌž.x¼éàúoÆÏ¢¼cäÏXs½Œ<£$ïŠaÇh;þŸý% Ý{…V{ èáéñˆ"»>ðº°>à€Ow'';ÆÏ)—χ; o†Nògƒ kÈÆ,Gœ›ÃÏRAÎJ]2ã-†YÜœ:`¾g-94”ñ®ç°‹i²œ „ x$Ž ÌÌÌÎyEH°N úÙaáQ–Ó¥Bùtq„[+€(Úrú»äÿˆ¾=gŽqž†œà>?;ÎÛèó'£Ú_›Œð°æÑÅü¥أ`™¦åºä„ÈÈ;LÖD$`²ü¬°‚« ¥ÿ 9$ñÞ;ð{,²ÈÛ[~͇²Àüß“€ñCù‡Ì1ô1Þð°g£æèCf ~1ù/ذ,#$$ouƒcg‚È4±Ë , '¨“¢#Ïè8è²TªtUIŽ9£ M ²I‘‚¤¦W¨eϸ|ÅRЂ̲{p9:ˆŸÙ,`þO£ GŸ¾D½¿ž–uÇÇë~# ×8Üvˆv|ÅpMÅS?Ɉ³3 Hý`†•õp,O¤ Îkž[+¬8˯'’cãž[ß ÛÆuÆxõñ“Ç[g9½Ðgç…ˆŒ…£‡0 ã2ö‰tFF1ÔŒ:œ>ëI9[<ßU»·‘ÏO8à <³[ó·ÔìÝùö'º&Èß®>‚>BÂù¡Õiò24x•oÀŒÎ°ùÑ„øß@o¡.œl8Á‚ó’ XgAHAdI¨€g$Æ$“,²©ë|Ï¡œíÒIêuàfÏ‹íäIÁÈÃg–øouÈ[ÆNÀ Kvb ðÿÌvì]$¡n‰nÁ áY2v ^7Ça¾yÞO<ÏAßG9{p×?ÃNGcîpB4pnê åp ;>a1¿'åÊb Û¹šw˜m¡=„° ¦« !8êI&SjL—†yÙxÛ|¾8ß=s•„ОO†Àª9ÆúЬo“­‘Æå¼@øoëÁôã:C?'3¼l H;bB,“´Á¬à‹XÛ[»}üå3ŸˆgŽ›/›y}S“ޱôO7ÿàž/ޱ†kì >qñ8ûü7î/¼%½ÂSá-ø¿I?ÔÚ”´´´aŒàÆÏ%ð»ñ<Œ‡ž ää‰ã[¾’xãà瞌C÷œl»¸K¤h ¸Ê evÖÜQ'l>ˆVp™0qÄ&¢Œò¼móäxgøm½Û#›éï˜øä’Áþ\7^ ÆYaÆaaeAÙ²]Ýå©ÃS†Ø¶P´´´´´œð/´p1ç¼åÓï.*z*q±ÀçC᜹o&g>ûÚã:ÄíØ µ%dÙ^ÞÁKuY4µ“L[ñU‡oÙ>ÿð5ÌôwüLó}|Må÷³üœñ ,,,X“c†¡|¶6FtµÛgƒG†!-´|ÞFÞzÎ;É9Î1çâ<6ÛcxÈ·œÂJ—±wJ/ûšVi¶HO~ÈžÍÓ£!€,8¶2ù´ud®3 0}øùñÏœ9y} ô“Ó Gø§£œï;?â¼>ˆëÆÿŸ„…“EŸÍ›÷·÷oìàÆË‚÷bZûd‰QñKlpÑ„ Z‹¢Ï¬"6>Ù?sË䆰‚!ø·Í}ZHÚÁ—a¼cpÁêÝqã±»;¶f޶`±‡q.‚‘¶@oIqž3“…<^6<úð?ÅÈD¼ÃücÇv}ƒÞ^þùÐЃv8þù§L¤ohÏÔF#¸ߪûã÷ BÁŸJ"Ñ.$úòÚ; ƒŽ^çúø›âx碇¶y±atYÃÉÆÛjÛ lãÚ,>AvhGiÃúÿ™2eFÁÒЛ°b#*éîçLPF^ÑI…8ÜŸ=Ÿ'Ñ_ áÎsü ãy'Ë=cÐp䘻ƒú #. ØšÀì¯ÎÏY„68IûMY2Nˆ°QFD{ƒ r Rx8íŒíßl<éÈÛl6‹m¶òx< ¼uã°Ï‰èçyãø&/€)ÿA$Z:ª«!Ä›Ìiì·UªÞXý@bd‡Ý~6 Ÿ4‹íK¿!þ#Ãé>)èœ>€u¾“ÁÉ˱Ãeš@If»i=fp5϶±0 Š<6|  Ì`ªÏD9¥¡’ÃÐÒa¾#7‚LL¥[Ríœ1±±±»»µµÙal[•åËq±çm¶ ¶ÛmðÞwÃ= ôÏŽ_¾ñ¯o†òm¶°¶¶¶ñ«KK«««®p±±²îîÖÖÛXyÞÃm¶Þm¥¶Ûo£ž’ÿ¾®ÿŽ§ÇžqìBiÂóšz}å²ïÇQÑû£ífÀwRÌ5d¨©,/xÊ. ÀnFœL2ÝOƒ„ªádœg ²xÆÆË8ÆË,ôwÃyÞÛ}8ÅlÜaƶ¼kÉ‹ N4º°³Ã»6Ûmm¶ÞM¶yü}ô8ùà·ÐïÐ9ÞN7ÅÓ†w•·XyÍàã=‡9ßkÑí³¡…I¤F{Kg³‚K\11<1¤îGŠlðÁ<ä–!e–XIe–Ye–6Ye–pz¢ŽØB>yæežZ–¶¼6ÒÒÒêÒêsœ²ËvÖÛm†ÛxÛm¶ß ð8sü ôÀ°Ž·ÄàË®µäã8vÀDåß Ã‚?E]pâc+ÒY¥ÙÓ/±j[ò]&un· Hðöñ³¶¶/¶L¸¬6ñ¶Ûm¶Û9m¾‹Æ Âqœe’Y1#ƒ,äË$²Ë,l²Ë,õœÿðîÖÔµµ•¶Þ4Û«®sŒ²ÆÆîÇêD<‹mã}ñÓ8Î@áç=2A¯£¾àß þ¦'G qÃí%Çb‰a½²B8êÞþÜÇÇIÆÏ*a¶Ûm·†Ûo ¶Øa¶Ûm¶ÞVÛmãm´-¶sÐÎ3œ’Ër3,áœË8e–YÆŠøm¶øm¶ÛÃxo ¶m¶Òm¶ÛxÞ¢[a¶Ò_çxÂ,³Œ³Œ8Ë$³†Ye–Yeœ2Ë,l²Î2Ë8È<_TP·ÀO"ÖÖ;sñË/Á*a®@|2ã`²NCïi$ïêÈm8Á³ÄÎzN0°²É,vH:„@j Q‘±²Ë89ÉZY<0à2Íœ3'‰ì}Cl/çÝ_¦p ö=Yœdâ{ « ú#6é#ê7’bËxÎS‡ßÐ'9ÏS8Ïðs”SË« ló¼ýÛµjÆÆË,±³ü-¶Ûmð_0P÷Ào¥ºñ¤'XXXXA%–r3^"ž?ï“ÊÔ®Og *qªd&PÇwd»# ±'¦ ²Ãgäù‡Ü1p6Rd[@’ aQ{à±:á-8Û®&$‰"Î¥xÖ×xÛIÏE,lõ7üÿl°`,@á‹3áûxë-Z±²Î2Îvß-·¶Ûm¶ÛxÛmµ‰¯ x9ÆróͶ™ia¶gDy,cWÎðc‚> ÖPã=²e„²16Ú'd˜‘˜‰&›GÔ¨€;»Ù×ò îÃî$°‰q܆šF9V Ïê83$n–6}äÉÞ;áðÎ2Ë,“Ë=MõFÏóÞWy<°²ÂÃbÌž9šóÕjÆÆË,³ÕÛxÛm¶Ûm¶ÖmðøŽ)ÌêrËX9:*ìïÅ`ÈÍYW¥üN”¬Qa¶Rd´åF¡Š Z Ž ˜=‘G´a-ºñÏ=-ò $²P66ƒ¼>"㾩oYÁ› –XXH³bÏñÕ«v­Ú•cgø{ÆÛm¶Ûƶ²¼ Ä# !q¢YÀc 0I;M`ÒÔ±Y]µvþ0;jZ À[Ò_KÆOÆppHÚÝKÏ–é )G¥J0À3á²òSl"јV&yˆÜÔ˜ƒ§A#»°~—Ý•UðJà @v ü–cæèÌ;Ç Û4sA#àKÙmªÀÐèÈ, ó€$yPÈXY*KN \õÃ,[ÁÆÝdpM¯‡ÄM“–‹åÙdŽÙgÉšz_9ê½0ùo&n·Ktp¶ùçŽqƒaa"Ì‹6lHá¼µjÝ«V¬ll}SGáH`‘f? ’=†U ‚põn²!ÀÙD$°qÐøHc+ˆ³ÔÑ#I™¡³ñcÉ™À8©un‘è‹JÂXáãH'Èàà;‡îQˆ’ bÝ` ´X)!€‰j¾ ¬–(ðÙ¯È=(pK âè9ûVt:úrpƒ­ÄÑ”d0!!³ÐÁݲшWØî6@»]¸(ê1\AÃçñÐ!ÕYØè¥¸tÅAöæÅ2 u#îœá %’€­¬xÐÑ’É_!8é ·Ç¶žÁ$ž81bÓîÝxÞv[aá| °²Áã$$²Ë,me–2XبK{Ôœáð<Ã7ÒÕàŸ‚”‚NmFêÆRƒó\ whÈ‚Q"ÙÆ218C¤±k®ß¼aÓøaYìxýºyq8îv6H¶Y¶ÒÒê˜0,Ý»yjÕŒ"6¥­«vø‹¤\BZB<Êd*ê¶ ±>Ëâ´†šC³¬À:_Ä4àÕCì|âÍÇ q–@Dq‰]­=Y’<±Ç„öCƒµaN€~%R¾è9<#ÔœJ9€Ø!ê¦?*»ð ൞Õ;ìcNÞ˶ÃK><PƦ÷dЈPä)¡‚Æ´â “©ÄGbM DéÉ1ë1n–¾KwëfÍ›v¾e·ž°ô\ã®p°‰‰еjUŒ«:²NìxNsçã“—Œ†¼&œ³ke©Âa¤Àv9ØÉôŽŒ[Ž1HDBCC db3¦œ!Ã%€ol"`¨Wq:.€_­±Âp±Ãm»oXXXHx$‡|vÛKKK8BÂŖ̉³*ÆÇmHXT  ش´„–_eÕ¶+Àš;Á†‚)¡õÞ€EWÆû2¨Ëò½÷BHC m-áÖ@Ft&ô ±zй0m‚aô^ì˜Ö†:Á·€ú‘T~vB9p—ÝÀµŸÿTðѶí`¥Q1§E‰eo¸@dŽÏéOÁp^"$‚³2fšŒFðÊ;[ùT1’0Ú×QøIb(É$*Æ$°ˆV^# á@d±°[6n[ <Àã¶Ò7›má·Ž€ÆS“$,6'-ž]Ûá»V¬llxƒGx$H[µ-ÔãIàào{¿8&Y<C ““Âa*}f{!=,æl°¡ÄdžrOÆ…GþZ[]§JûXrpËXpwÉÇÄL†6ãÎM–Km¼ç$Yuaà†²x6Ǽ|>&+[m†xn¢-nz|’ ßÃÍîGgÁzçåãâx±Ü88×Öˆª³4ÆdCÝ?¢÷-Ë«ßvwÁ‘/¦ÚYT˜Ë}×â8{8½°¯Á3e?,\«ü ÏaNf¾Sϵ³½UGðÉ7L@ÿl-˜¬`¢ÇÔòI8 tdžÝ™öáîý,Õè(Þ‡á…Eö¿ù 8ïaë€ÃY6Ú[ãúÛ?€ÆÃ,x³ËÀ@Nl–<û‘ Ä{Mî#„›¡üKKªðu·ÊKcÝL½Í½KW±8šcDûß ±¿ÿÙfwupd-2.0.10/contrib/qubes/doc/img/uefi_capsule_found.jpg000066400000000000000000003465741501337203100233760ustar00rootroot00000000000000ÿØÿâ øICC_PROFILE èmntrRGB XYZ Ù$acspöÖÓ-)ø=Þ¯òU®xBúäʃ9 descDybXYZÀbTRCÔ dmdd àˆgXYZ hgTRCÔ lumi |meas $bkpt ´rXYZ ÈrTRCÔ tech Ü vued è‡wtpt pcprt „7chad ¼,descsRGB IEC61966-2-1 black scaledXYZ $ „¶Ïcurv #(-27;@EJOTY^chmrw|†‹•šŸ¤©®²·¼ÁÆËÐÕÛàåëðöû %+28>ELRY`gnu|ƒ‹’š¡©±¹ÁÉÑÙáéòú &/8AKT]gqz„Ž˜¢¬¶ÁËÕàëõ !-8COZfr~Š–¢®ºÇÓàìù -;HUcq~Œš¨¶ÄÓáðþ +:IXgw†–¦µÅÕåö'7HYj{Œ¯ÀÑãõ+=Oat†™¬¿Òåø 2FZn‚–ª¾Òçû  % : O d y ¤ º Ï å û  ' = T j ˜ ® Å Ü ó " 9 Q i € ˜ ° È á ù  * C \ u Ž § À Ù ó & @ Z t Ž © Ã Þ ø.Id›¶Òî %A^z–³Ïì &Ca~›¹×õ1OmŒªÉè&Ed„£Ãã#Ccƒ¤Åå'Ij‹­Îð4Vx›½à&Il²ÖúAe‰®Ò÷@eНÕú Ek‘·Ý*QwžÅì;cвÚ*R{£ÌõGp™Ãì@j”¾é>i”¿ê  A l ˜ Ä ð!!H!u!¡!Î!û"'"U"‚"¯"Ý# #8#f#”#Â#ð$$M$|$«$Ú% %8%h%—%Ç%÷&'&W&‡&·&è''I'z'«'Ü( (?(q(¢(Ô))8)k))Ð**5*h*›*Ï++6+i++Ñ,,9,n,¢,×- -A-v-«-á..L.‚.·.î/$/Z/‘/Ç/þ050l0¤0Û11J1‚1º1ò2*2c2›2Ô3 3F33¸3ñ4+4e4ž4Ø55M5‡5Â5ý676r6®6é7$7`7œ7×88P8Œ8È99B99¼9ù:6:t:²:ï;-;k;ª;è<' >`> >à?!?a?¢?â@#@d@¦@çA)AjA¬AîB0BrBµB÷C:C}CÀDDGDŠDÎEEUEšEÞF"FgF«FðG5G{GÀHHKH‘H×IIcI©IðJ7J}JÄK KSKšKâL*LrLºMMJM“MÜN%NnN·OOIO“OÝP'PqP»QQPQ›QæR1R|RÇSS_SªSöTBTTÛU(UuUÂVV\V©V÷WDW’WàX/X}XËYYiY¸ZZVZ¦Zõ[E[•[å\5\†\Ö]']x]É^^l^½__a_³``W`ª`üaOa¢aõbIbœbðcCc—cëd@d”dée=e’eçf=f’fèg=g“géh?h–hìiCišiñjHjŸj÷kOk§kÿlWl¯mm`m¹nnknÄooxoÑp+p†pàq:q•qðrKr¦ss]s¸ttptÌu(u…uáv>v›vøwVw³xxnxÌy*y‰yçzFz¥{{c{Â|!||á}A}¡~~b~Â#„å€G€¨ kÍ‚0‚’‚ôƒWƒº„„€„ã…G…«††r†×‡;‡ŸˆˆiˆÎ‰3‰™‰þŠdŠÊ‹0‹–‹üŒcŒÊ1˜ÿŽfŽÎ6žnÖ‘?‘¨’’z’ã“M“¶” ”Š”ô•_•É–4–Ÿ— —u—à˜L˜¸™$™™üšhšÕ›B›¯œœ‰œ÷dÒž@ž®ŸŸ‹Ÿú i Ø¡G¡¶¢&¢–££v£æ¤V¤Ç¥8¥©¦¦‹¦ý§n§à¨R¨Ä©7©©ªª««u«é¬\¬Ð­D­¸®-®¡¯¯‹°°u°ê±`±Ö²K²Â³8³®´%´œµµŠ¶¶y¶ð·h·à¸Y¸Ñ¹J¹Âº;ºµ».»§¼!¼›½½¾ ¾„¾ÿ¿z¿õÀpÀìÁgÁãÂ_ÂÛÃXÃÔÄQÄÎÅKÅÈÆFÆÃÇAÇ¿È=ȼÉ:ɹÊ8Ê·Ë6˶Ì5̵Í5͵Î6ζÏ7ϸÐ9кÑ<ѾÒ?ÒÁÓDÓÆÔIÔËÕNÕÑÖUÖØ×\×àØdØèÙlÙñÚvÚûÛ€ÜÜŠÝÝ–ÞÞ¢ß)߯à6à½áDáÌâSâÛãcãëäsäü儿 æ–çç©è2è¼éFéÐê[êåëpëûì†ííœî(î´ï@ïÌðXðåñrñÿòŒóó§ô4ôÂõPõÞömöû÷Šøø¨ù8ùÇúWúçûwüü˜ý)ýºþKþÜÿmÿÿdesc.IEC 61966-2-1 Default RGB Colour Space - sRGBXYZ b™·…ÚXYZ PmeasXYZ 3¤XYZ o¢8õsig CRT desc-Reference Viewing Condition in IEC 61966-2-1XYZ öÖÓ-textCopyright International Color Consortium, 2009sf32 Dßÿÿó&”ýÿÿû¡ÿÿý¢ÛÀuÿàJFIFÿÛC    ++&.%#%.&D5//5DNB>BN_UU_wqwœœÑÿÛC    ++&.%#%.&D5//5DNB>BN_UU_wqwœœÑÿÂÆ"ÿÄÿÄÿÚ ý$3j)d‹d* d* 25 ¨*CLþöß„³Ä]J‚ °°hÍ€ ‚Ê"ˆ°¨*Qe"Ó4%°Š"‰TR³h“TʉTŠˆÐ ´2ÐËC-SC- µLµ´ËC6Ó+L´%S*%R(•D¢(Y@„ ¢(” X¨5q+£éyS«êåOé¿ êß @, %€ š ¨<Â[ ¨* ‚ ¨* Ô± Óý‡ækàváîÖ|oÛvéÂ?ùóàOè½#ù¶¿{ùÓá?¡Xþxýî+ð¯é<çoÜ{¬þrý7Õ—ð—úÇË?¡z+ù«öcñèñ¯è%þnþ—ÌþrþƒÜþnýO¨ümÎ劈ªŠ$ØÃC-3TÌÐËC-)shʈ  @BÙHR(–Á@°*"ª(H(•LÛLÚ#B43uL]S Œ¶0ØËtæéNW¤Œ7k›¥9:ŽW­8».˸àôãƒÐ<úïäŽÇjýÚÌÐ,°J"¢Tò’Z‚ °%*BÄ+#_ìüúün¥Ö~ïG§·/›çú½Oõ})5êíõâïîr¯ƒóÿ]ùÝ:¬‡^c§ ÞT½x2;ß8õÏ-=ñO>Bh• ((€  @” R* K¥5qJ‚”• PˆÓ4·6ª X\Ò@”(³EQe4(RÕÍ5ãöxÎßKå~‚?M @€,°”"ˆ°¤(<„– ± €€ €!E²C\zTüÖ7š´Í[!H¢(–Áe…€%(‚€BÀ±  @`X¤°*LŠBØ* H²€ "‰@ R   PU”©ae€ Ò(‹I¡VPPÔ% y;ò9þÃñ¿½p”€P¢ ò%•BÀ  €@E@fÂ@„-Í?áúÿ#YÕ–Å”%K‚¨*P ”Š©e H ²À°BÀ¤ª°X! ‚¡j ‚ ¨*"¤7¬ZÓ(¬è4ÈÜÛ4© \¥-Í-‚ÜØÕ”¶QeP(-”eH¥”ER(óï—sÅý#ùçôx @ ©@T* R,ñ¢U€B ,€A,Y!ͯ‰ùïÔ~cY·6Í XE  @ `XŠ)(–X ”¢(€@°Kæ©H±D5”) *‚ Ò ˆÝÀÖ± H* ‚ Ò `Ö³¡BÜÓIKe-–-”€ %"–¥•jPSÉ߇c¾üWírBZ”(  ÂÊ"À÷ê¾Ý€” ‚ʨ¢T* ¢P ¤,¢%@°*¿Êé1,Œµ™@h((-–-š.³¡¬ÓVRÙbë4Õ”ÕÍ-”YJ–@) Í-Uƒ_?è|ã§èÿ1û3ëË3@°©BRR€H¢€,š ˆ(<š•RÀ²!"KK‘› ,°ºÆÏÌ|ÿ³ñwë¹ `T(  B B¨ª‚XTª€’ÀB”ЧL÷¯OôîO™ð|¿ƒÔïäÄÊS"À¨5qS§o6«÷¿­þ1úÌ_Úÿ1þ©åÍþ=Ÿ‡¦q7%Í P¨—H-‚”k:³MÜÕ·:Æ‹¬hÕÍ“VSI¢Ùae*Qe@.7š”.³bÁjRy;s8þûðÒdÊ¥,X)* ¨Š(BË,Z€‚Ê<š¨@.A# EBÂK’K š!³O•ùïÔ~cy»çÒä,”%P”%*P ”@‚ €DØB  ºÎë_½üÇõ¿ŸúÊÓ–k„!(”%-ŠÞùnÏÚ~×øÏô¬]ÿ/þÓüøü¦zcI5"–P(*j-¥ª-Í.¥-š‹e.³¤´‹BÙE• (CLضUóÉLFüï%%‰@ JR H*TJ…X‹>xšB ¨*Q›¹€YËY&u ’Œk4ãùÚþ/YšÆõ € ”X*P ¨¢&l"ÊAH*Qf’ôÏÑÕý×ߟ+›ñß¹ë”I`„¢(Š"‰@´š”×»Á½?±ùÿ7úþWøÏ/Ôþc¤Æw#*—*"Š1¨é3£l—RRë:-–.¥.¥’ë:)E”€ KRMA¬ìño–§û?Ì~žX R¥ €Ë( (P@°Š>pš‚P€.A%@’Âf„‰`ßä]ùO¾{Ö4,©@e €¢ ¤@BKJKRë:³´ügõûÏ¿ü~NM³6ŒMÃ+L¶342Õ2Ò3¥%¢h¯gõã¿Ò³süÏû7ñôá7›s7#3P‹ˆ·:(]\è\è·:‹¬èºÎÆód´-”¶QeP ËLÐMBtÆžýÛð} Ø”AS4, ,e°K >xš ¨* °$¹Š‘lA(™°’ä@„PM|/¹òëóºóè(T”` X-Ͱ@JbÁ*İÈY,¤*Rë;®¿Ù¿’[‘ü·÷ÿÔïú—ýV_çÙþ†ËùWÌþÑùmOÀ7tÇè=ÿ·Åüwâ¬9³æû8þO7ÆþÃü×/ƒ©ºúÏÝz3’Yu5ûÇýɤ3þ™ø\¿'5+3P‹ ÍHΠ•AKeZQe‹¬èº”´‹©RØ- \ÒØ-‚ÙF5 f…×óóí“úc5 ‚À©hH ²¨”¨, @€>pš‚ D–ĉ ›l"äK°E/×Ìüx饹¨4‚€P ”²±A @’ÕÊ( Öñºû?Ô¿˜ÿNÍü_âÿ_ù“èÿWþMýgó—ûÿ‡Þ¢~‡ùõžzüo¿ôúÇðu8~⦅ùŽ_ Óô_Í¥5ÅøšÎ÷?©þUfºG¿Áé?­þ?ö”ç 4“Y$ÔŒ¬2¢j Y¥RE”º–.³MjPÙKe(*hX(&¥*P æõxË×—¸ý¬³5`¨) P,€ ”%©P°J%• Å#憉`K ,”’Â`"äÙ£ñ¼½~=óÞ±»%,¥UD ±IeQeÁ,Ê‹’™Ô$°ª”Ö±»>×õ/äŸÖóøÏÜþs×ýwùõüßÊ~÷߃ÞoõÂÿEƇìËÃ÷5ÈóþµéŸ'ÚÏÇ篵üÛúOózøz—¦¨Íc–¿–Ó¦^¯/Ñ_ê’ýoã1b¤°K"J%”X]\ØÛ:4–-šF³¢ë:)RP·4´-Í)IAB  @׃è|Ó§Ûø?©_¼2Is@) ”Ô°« T”JÀ>`šX.J‚K%±A›@EJ! Š»ÆÎü¯»ðwo¹¨³V€ „ X-ˆ¶ ›-°H¢7€°;q¶Mf$ª’Á, To®ßÙŒQ—óOì?Çìíý‹øÏôô~CÑy3ùZ×?.ºOésøçíùßÖpóÿ9=¾¯Ì}ž“úg“æ9kéx¹ùkì{?3ï=3ÎÉk‡ÜÜý‡óßè_Ês~z(‚ÈŠ€CV%݃w4ÕÍY¢¥5e5s¢„U f‰eP ” |Ï£ó‰ûÃC—Ñ*B€ ‚(  ) BÀƒæ"j ¢°TBÄ„A€@’Å ¬Øùß›ýgä÷†¥Öj %ªˆ©j¥‰hЋ› T@$¢’ŒŠ„ Q¬èéû_Ä}Jþ±üÃúæ2þ}rÜÓ#L2\S¦"´ÈÛ4ÈÓ6­ƒ_ÓÿýK//ò?Ýþ± ‚Ù ,”X—w4Ö±¨Õ΋s£W:-•4QBePP háåíÌÇôçÿÐeB(¤((¥*e¢°‚  ùhš¨(€*ÁKEK˜‚ÜÒ~7ö¿Þx¥Ö-Í*R Ò \RÙJ”YJ” ±(@€DTª™¢K° X-”×^;³úÙþgý?ùú/óžH ·#H* ‚Ø-ƒZÏÖ?WúGÀÅüWÌ7"%  ©B E*Ym”ÕΣZÆ‹CZ΋¬é&¦…–¶R‚¥ k:«°ÿ;ý·«ãSí|m̬ ”¡AF§CÓü Íçü§îþRÄE– Q(•”YKf‹e–ë:‹¬ÓZ΋¬é-–(*PQAB¥¬èù¬«õ¢ù?W4 Ñ (” I`°P€øì³½3M ·5,B²[,° „.lQ ßÈúÞ?0³¦)IEƒH-‚¥.³Ke( H¤¹,Á5 ,$ªÌÔ"ˆ¢R–S[çlúÿÔ?Ž}i§9þ‡ß/âóößÜä´Šˆ ´–ý#ÅýÙõ1_•×àlÊ4K%@Y@"‰@”¥bÙK¬ê-š.³¢êRÙJ!B‚e"Á¬Ã浚ýÿ¯:Í  `¤ª”@,@bÀЉº”¨4ˆÖP¨,B¢*°@P, qëSñsYé€KsJ”©AKÜèºÍ(H«€°ŒÍC+*(ÊÀ‚­ƒzç«>ôoå]ìÞ?ÇþÓñ?™þÉŠþ4þ›ó,ü+õ¸_ÊߨûÁ}èÞøüÇé¯Â·øŸ…ó5,ŠBP%Ö"€ˆ¢P(”*Ø–‹Th‹s²êRÒ.³J šT ÓR+\zùŽ=1í—÷$€ Q¢€ÉQ,P>&ê e"Ä) €H¨,BÁP*"ï¯Éù¾‡ÎÞËa-‚‚ ÕÍ*RÙMR€T#Y ‰JE gPÌÔ‰5*J r­3Mk:vóZý_èšØþË¿ãÈþ¶þ_³úkùgˆþ¥ð? +êü¼Ê¨‚ÈY@% RU%RiEh–Ø•I¶…”´† ¥% `©Jƒ%©T¾/wÏ‡×øß¤_ÒB ¢ª"¢¨ @*°°øšÕÈÓ4Ó4²H‚ ‚Â,‚ÀD,E°J‚ë>'Æýç÷›Ê•)J‚Ø-‚Ük4ÝÍ €K$¢.@!(‚¢­‚ÜÓLÓWÝÀÓ#L– ¨  @()IB””)af‰©¢iaTZae((-” ¶R”ãY&¥h¿3éü¢þÇñ¿½_h€B©@€‚ ¨ %@, øš«‚Ø5 %•`¨ŠÈ© ¨* ¬hòþcõŸ“Öd³R¥J” X(Rë:5e* @€RB€ªgPÊŒ¬"Œ¬¨ @‚¥ ”¨²¡h°…‚   ©Kb-”U´b”¶QT¶X¥% e%P”  ¸Ð‹š¶hŸ3ßâ3ýù÷ôIRÈ`¨`° * * ?;bkP* sJ©`‚¡j ¨,B¤5 ¨,‚ÜÓýå¬óK7•ƒIE•¶ šÆËeˆ(° ‹Q, )‘k2ˆ°fÂ,  D* ƒRR ¶  ”X-‚ÙbØ4”´-”j"êQ¬h¶XÕ”YD £@©E”egP¥<¼:ò=¿ºüwìVJ   `¤ B‚@)9sf­ÈÓ4±“H5$4Ê4ÈÓ"¤—R ² ÈÔ‚ ¬2:þï|‹>1w˜ ©R¥)eQ.³¢€ ),…–€¬Ò&ˆ°°‹ 5 *¦uA,¡H ‚ÀX) ‚Ø((PE”Ya¬ÒÙKe-–-”´”¥-”R,¢jR¥(k4 P¢k:>~?Iú_ö¥€‚ ¤ а((ÀͲšÛ Û5™“wÔ̰]±M23 ÌŠÈÔÓ"²-È© 24ÈéàöùÏÍ—¦!@F ¶%¶R¦’Ù¢‚P "Á`¢(“PÊÃ"™°•%@° X(* \Ò¥(…”((ºÈ¶RÜèºÍ¶RÙKeŠ ¬ì`  X*QeSä¡{îåÖX ‚ ¨,°¨°ƒó ¦µp6ȹ‘ur,Ê4ÈÛÓ(Ó#LLŠ‚¤5 ¨* €ƒrlü¬éÏxYE”YJ”-΋¬è¨*P"Áb¬° ©@J 37“$¤°,X P ”YJ–-Í4 eI©Ks¨¶SV ¬è¶"ÙJš ePã~sÅÓÅýé ¤)*¥%‚€‚¥R ÀÊ9šèæ:^Iw9Ž®DÛœ^®hèæ:¹®c£˜è掎cl °6æ6ÀÛl›áÐø>oÏÞmÍKeQ@RëJ”ÒP ”Š¢‰(‹Lµ¢(Š$°K++‘B,  ”¢ `ÒRØ5s¢ƒIJ¥ eŠQ©M%‹e((* ‚‚  ¾O_„åõ~OèWõÈ ¤(* „X*… üõÕçÛV1wNn£›¥^.ÄäëN.ÔáŸM<—¼8Nò8ºŽ3¼8»C”í3¼8ºŽ.ÐäëÝ>7Åýç:rÓ:ÖhRØM%( ŠÒ ”‚,  ‰A,),$°’Á,@¥`°*"¥¥ Ò \ÓW4ÕÍ5sM\ÓB5e-–5sKe(* ” ©E‚°Å7ó¾ÊWë?#û£èË (ª‚¨* ‚¥ ¨** øF¹vŠ%´Í¥© BX–[&vŽspÃY¢J$¢gP’Àe<ÿ•ýãúsγw‹`¶ @¨5r5r­°M³KsKsV ©EƒP %ÁfÊK"KL€‚ ¨* ¥ X(*R¥-Í.³M\ÓZÆ‹¬ØÖ³K¬Òë4ºÎ¡¬‹¬ÓH* %PB ¨h×ÈúŸ-_Ñ?ŸÿFȨ* ( @,ÀBÀ øµyvÕÅ6Éu`¨-ˆ©J‚æ’gCœÖId,°K˜²R²iuùÖþkxðœí‚AsJƒH«sR‚Ø](KI@*¥"À*(’‰5 Íä’ÂK* "À€Y` ”¥*Qe e-”·:5sMÜê5sM\ÓVX¨5sK` U€”)3¨9ø}>eõþ÷ñ´ €*  ‚ ¨¨@¨>=ιwU"¥@  š Íe2°°"H¨Á(Ê|½òlø·* ‚€*¥* ”·4¶hk4Õ”PRØ¢U$ÔI5šd(̹$± Ôˆ¬H-È·4¨*P”¨* sJ‚¥(*Q`Ò \\ÓW5sMÜê5¬hÕÍ5¬j*†¥QA`¡hPHšOœOÐ~§à}é@ ‚ ¨** ¨¨* ‚ , ¨>XãÝ`Ò % L´& ¨*R(ÃpÄé çpç:`ÌÖ` (ÊŒ5’T7áöñ³ò+;q°(*Ø* \ÓIMYM\ÕÕΊ–€(’‰5 ŠK’gYLË”  ¨* ‚ÕȨ-Ê´Í24ƒl³KsKsJ `ÕÍ6ÎkÞ±czÅ7¬j5`ÑUe `©J”X*ØV³£æs¹?oôüÞ˜€   ¨ª€‚ ¨* €€"À—sx÷ºÍ()£ Ae "Œ´1u¢K 5S¤ŽMŽmäËPÃCc:Sñ¹ôù»ñ—5(,JÒCH-Í*R¥5q£ZÍ]YKe-–¥ ¢@’ˆ)3Y3á&l$± "ÀBÀ¨*" ¨ª‚ ¨-ÈÓ4·4·(ÕÅ5d5®tÝÀÝÆ\Sw7¬Sw랎—:5q¸¶ÙJ” ©J‚€P s“åë>¤ýõIj€ ‚ ° °X* ‚À%,,>ešãÞ5L¶ -Í5r,¢5L41l$²“P“Y&zC–zàÁ˜¹¢J¬ÍA¬èüßÏû·"ææÂ€©JÈÑ sKsMk·yê7®uz^téqWRW£š´ÎN“rJÔÌ52&YFl‰,H €ˆy!HD °P¨ CLÓW#W5qMë˜ëp:ÞV;ë…=᣾¸n:ë^·–Í^c¥æ:^TèÀÝçMÞcw:ùºyNKæý³õ‘ ¨* `¨« ¨,* ,*<7{TM  M#-@j$ MÅÃPÌØæÐÄÜ9ç§2K .I5’jXùŸžýGåûs¨Þ* ‚%J"ˆ°Š‰TŠ"ˆ¢(J%‚€ (”YJ‚€ ‚` ((”… &¥"ÁeP ¥"ˆRP    PÏé¿1û#êÂP B ¨ª(,* ƒçÙxú-Ù¢Vˆ¨*¥¢ ’.hD.h’ÂMC2ÃéÌÉY4Í9þGöÞ9Ž˜ÂÁ`¶ K ”X° BúZå” @X( :ãÕ£É}] Ð<­`©@€ ¦‰@*Q`©J‚Üè%P‚²4”°¨*Q,-‚ƒŸï¿ýÔ" *P (‚ ©@ A ‚€ € áè”.³JPT¬Ò‚ËS4"Ó*#PËCqdÔŒÍÜè®yé>=ŽSpÂÉuùOÕþsxù²Î¼È²”–P‚¥(J%°¨*P!`íÏ*ˆ©jR(‹=˜óeñ£¯™O€:s*P‚ Ò ”` X( ‚©E‚‚Ä-‚¥¢ˆÒ2°ëýðÿ·€P@ (š‚ ¨°,×èó©`Š‚ ¤*P”BÀBÀí^ŒÐKLÝS: )%R-3h¨JCFcLÚ¨•59Ñ(ÆwœÔ9ã¨ãäéùâðîëÏÎôdâì8ÎÑ9NÜÎ:լ͌͌:C ŽnƒÝ9Σ•è9ºnˆæéN.˰âí+› ¼{.ー9:ŽN˵8;ްâíNヺ8;Žヸã;«ƒÐ<ï@ó½ó½ÎôÐ8=„ô;Ð<ï@á=Ï{ÓÏ=#Îô<ôÄô;Ð<ïE<ÏJ¸sö/£>ˆãz—›­8»ÔãÓQE÷Ÿ~ ûɨ[ „¨** ¨*P  òÎ÷~¸ãzÔã;>½Ï=#Ï®Ãê9^”äíN©8Îõ|ÏLŽÑÔàê8ºÃ”í.¹9òïÂ¹Î˜ÍøŽÜì« "ÂqíÁ;ß"½WÈ=o õ¼ƒÖò[ÉOUòCØñcÇO[È=o$=ö<ƒÖò]ñcÆ=öƒÀ=÷ç¡<÷_÷ßž>ƒç¢ùãè¾húOœ>ùƒé¾e>•ùˆú“æ+ê>Xú–>¥ùcê¾T>³å¬ù#ë>Húï>½øÐõôùF:,[ABêòäýüáÚ@PA`¨ª‚    ‚ BÀ ³yu¤6Ä:1M²4Í4‚¤4ÕÅ4稩 3K%D™4Él *¸ðôrŒü¿©ù«<5zâ, "‰Ë°ñ½dòOfO+Ô¯+Ò<ïE<ÏT3ÓO+Ó;ÑkÌô3Ò<ïE<·Ó;Ò<ÏHó=ó=#Ìô3Ò<ÏHó=CË}ó½ó=#Îô3Ò<ÏF+Ò<ÏHó=#Ìô3Ò<ÏHó½Îô3Ò<ÏHó=CÊõCÌôÓÊõ+Ô<¯Pó=4ò½CÊõ#Ö<¯T<¯T<×Ó£Ë=Õ|>®€çö¬ùZáÖ]UÔ?Eö¿#úø€( ¨*P*€ƒ8ö¢ÊRP*¨¢*"ˆ°3K ³4··<ë­øŸ6ÏÖø?#ç³õ?u>ß?>ž~t=Þ,i5,¥‚,€lçž¹LNƒ› æè0é+.ÃcÕ0è9ºHÅÕ9ºnÃc Ó Œ60éNN°æè9ºS“¨æé :S“¨æé ÍÓ›¥9:Û çzS“¤0Õ2ÞŽ.Ø0è0Ý9ºäÃtÄëN.Ðäê9ºhâï7¥8»hóOVÖºE‹ `²7Ôù«=þ+W¥š€1û¯Ã~˜ú¤ŠB€ °¨ª‚¥* °,9«i@´ÊÓ- S4$Ô.P-34HUξwçn~¯çðé›=~óá}_¥'ÄýÊO„ûƾýdø¥Ô‹ €MBKHS5ƒÛÓͽÎ^oO²>Uú?.;{<>‡ËéôÏ–õø+·£Í“êø³ÈξïçΚιåùýÏ.ÿGùØÕ毬ùZ1¿µâ‡Ðø4Ëìw>Lèïëù4éÊ£=¸ô®Ÿ_àlöx}¿¦?Þ`Çè?9Ö=þׯׯ—Øøû?Að§Þ?=¿Gœ}o‰Ôû_ô¿oÏ[âu¯±ãñ{#ÇÛî~t×ÙøŠû=~>ÇÉûßž¿'3ïoàî|\íoó?SŸÎÃô~?‘buåÒ:Q@gcРU1ô~pýÞH B€( ‚  ‚À°@,Íçx÷Û$Û¶BØ* ˆ²*X)IPW•;þsÅϦy÷úÿj>g«¶3¶,Žyéαž˜9ðíÈÏ—Ùòlò—§9B, "À¢k8³›½ÓÏõþV£ÓåÞ>”åèÀϻ˛5ê^s®NšçKËtå;f;N[«1£›´jæCꌨÖPä%ÐíÏ“´9:ŒôçvŸ-3´8k´Ž>œ«= œú=- ŽÙ9t `¹º96% óº®n¹D­sÕ1ŽÒ3©Fð¨ÐËHβ=,êUEA`¼÷“õ?G󥉀€* ‚  B °‚À ¸÷šTCTÅÐÌ¢gB,Q,3æ3ùyß®3ú>ýs¬â³y:ÞzÚáŸD<ó¾O{Èãùß·ðw%Þ@”%R vóÛ>ŸÌíÄ–åÏõùjgx4Rúüz;ù¬Œo©©O­åלãQÏy?Iñ½>#‰¼hƳ³éõùëÅHß=dßnPú¿('Nz1¾}·äw¯t‡>¼ŽÝx¿ð–håÛ—Aôþg óçBñ냠>Íôp3C—n=ƒ~ÃÁ ÷*íÓš>×ÅÖj5#‡§ÍègׯÎ>Þσ~ÏSàóûÿïÓiE(Š#P~ãð®=‚b€¨* ‚¥‚ X¨ç£|Ε1t0ØÎ¬Œ5 Û£Y"Èçù/gÏéžÿ§áïŒÍ3©,J3ß.ÜNwPø_?Xëά° `åîðêÈ#¯^ž"ˆ¨ú;ßàT‰Iö>WØ>”5ÌÐ>§‡è|ãhÌhúßî|CÑíñúO™hƦ£Ž=£3X7îñzGËúCÎÍý/—Þ<ʺó:wãÐû_Ÿú_8T9õåÔíö¿?ôkæ[c9ß*ë`ýç¾¿É C^]¯Ìˆªœûñ:F¡àôð9é³ÉßžZ#Bâ„Õ9ú<¾˜ÒUTDPŒ}ŸÜý”²@P)`¨J‚ ¨(*(‹Ì·iT€YHRUX3æõ~^ÏŸõþgìwž¸ÛžåIåyµƒŸ¼ÇÙñ,ùv^¼Ö ÖO<ï,æè9ô>ƒ“¥9:Œè1ž£“z9t¡›N7®Nw¥9v‚rì8ºlâëHNÐåzßJºŽ.ÐæëLm g¨æÝ8»XsØâíS¼9w óì8ºláw²MC“¶NnØ1Öªrì9NË}Ljõ<³­8»`çÖÒðõñ9;#‹­8½#ÌôCüý΋BÅ @'>˜?cßâ}È* ‚ ¨ª‚ ¨* ‚ ©@P, €4ãÖÜÚÜÊ5. LSlÃ|æã>ÏÉÞ>Ÿé|>ÉZÎ¥ 3FdTs9ÌÒþSôšÞ-7K `ðíÆµ~ݳáºñÊ¥ 0›¬îPû=O‚ƒ´ÑöŒý7„øüú ë;‹›úªü«ÕåŽ{ÎŒtΪ_½³óÐŒÍäéÏ=ý7æ‰Ï¦ ×—R5ú3óNü (Õ”_«käcRY6l“ê|£:”åÛaÛélø“¯3<úò;™=žOÕü ñçC‡§C_cãèÅ”œúŒtåÔö<’‰Ž¼D¿¢?8úz>?FN³eY@’Õ"Qdôþ»ð¿µ6H¨* Ѝ, ¨* ‚¥ ‚ ©@* €1£u•+#WlÃs0ÜÀÛçç?3¿7ÛéÏô8Ž}6Î2MINz.f r¼JÈøß'¿¼è°‚ E S8Të¿: €¬kú~P“Z0¹5ìòCÙåÈgtÍõ>XëÉLÛ e;ëÌ„¡äÒŽÜŽ’±Ó'£‚"‰ž¸¥Î£ÙLjcy.:r:ƒ¯ Ÿ^]*û¼©IϤ­3c¯9F³Ò¹Íð;ûa¨9tçÐ}ê<ºš'üNó;"ÂòëÈôï—Yl P ²PC? øÓõX*"°,* ¨J`±I@ ”@E¬¸öÒ[ ”T°± dÇÅû•¹òþ·òß²ÔÖ¹tÆ·e4çMá ‹|zr/ß"ϹzóJ!@,°±I¬èóòôâÎ:è9kcê9ºÀƒê9NÐåÔ/-lãzÛ š‚cz9Σ› ©“£“¨åÒ‹Ïc“¦Ž3¼1°˜éNS°ã®€d˦NnㆺX¼ºC•Ö«“°åÕ"óÖ«œÞŽN¨k óc: bŒZ#}=o#|µÐå'S› ¬Ó–}cÖ©g.±üž¥Ô B‚Å%B˜éƒöŸôHˆ¨*( €`¨-Í* ‚ ¨* ‚¤*B¡nóxõ·4·œ:¸ŽÎZ$±k\Ù'ã?IùÍãéþ›ã}|Ýo–åÜ€Ô…‹ ›ÈÎqM~kïþWXº—¦J×2_GKX¿Cç•Çp»Î¿~¿>%åÒ[3¹`û¼+äÄ1w“gµ|OµäO>™.tŠ~€üûêü¡ uçÐOoÒ>§3 ÙHûðøXÔé“¡L¾¿Æ7Ïc—n]úƒòï€Æó³n]K>ŸÎ2 sëŠï,_“xÒ°ß3¯é?9Ôý7ä}^jo6<Þ¯7¤íìù·4œ=<ÎÓŸRáëów^¤(¨)†5¡úOÅþÌ̲€ a*  „°U€B ¨@^£—Z‚³cHÄÞW dc\‰ò}úsý_¢^{Þæ„´ÌÐÌÞNXí<ûñ^Øù_×åéÌ5•BÊ Ï§œ÷úþCSîù¾R¾–¾[/£ó€XåÓr¾‡·á¢]â¥#ß~x“A. ûö7€²4‚ ¤*ØZ‚À¨* @º™å×v °€ÌÖU–K‡?/ïùÿo¦>útç¹­SXŠÌ]ÌJÞs ç"æüäøzÍíÊØ… %Lù}|ŽM¬Ã¬9Σœî8;#—h9:Êæì8ÕéN7°ãÖ qê8Þ´àôdÇHúnš8;Ã¥8»S‹¬.ãƒÓ#†ö5ËjäëNï“d5ô9ºÓŒôS‡`Ç>Ô÷r<δ›Èã{.ãÏ®š5ÎŽ7°âôó9w æô.˰çß0Ç>ôá=ÏÚàôïŸYbˆ´‹À ›Lêr?kŽƒ(*P‚À¨*P‚  %¡@€° ë©yv!-È*"Œ.UçñþÇçn~géÿ3û-MöçÓÑ’°6fÃé•óã çð¾ÿå5‚^˜, „R ¸8Üi-‚ ¹¹\îrÃÓæúŸ.œúsŽ™ÖMËé<¯¯ò)ϧ8éš4z+Õä/¼ÍoŸRLùO˜ÉLuçÐOW3‹§3žå3Óvž”瞘:€ú^sËϦ¾~܎ǼùïGCÉÏr±Ó:4zŒù½¾8ÍŸ7¤žßÒ>kY2¶¹wáÜ×Óù>ÃÇeŒpôp¯S:€YT±­4ùœ>ÏÈŽ¼Þ™l D¢gy>·Ùü¿ê Md¨(,J”Š"Â¥ÊP B²:9Îû8Ó£žI F#s0Ö`~SôÿÞ:þÃó¦/^=1¾ŒÄéœdë9 ÎQwÎó¦lOÀúœÃY`©D¢ 5Èßêÿ!u?AÓó@2séʺ(©ØÏ?¡óÆu ”}ë=_+ÛâÔh}ÿÏô>§ÇÞSyô5÷>qÂÈÆ³ºÆó£ÑúËÒ4&¥?AÛóBYc2æºJÒñüý¦u˜×.¼Žß_äJú÷ã†wƒyÖ’È_gŒ¼ýøöXäéÌ¥®=¸w'§Íõ•eúC¼Þ¢J‰B‘h\ò,õù³N¿'¨í’[%-ÈT, ”°9~·òŸpúYé‚`©A B¡(X‚Z¨,‚ ·ÏèÓ4Ô‚äJÌàñþwìüŽœ¾§Ûù¿Kß^:ÍÜ‚Ø$ ›+λq ” (@¨¼{ù¬èóKÏLóSËë¬YÂ=ˆú^ïxv%p;¸×€îçÔÍœNôg:1×—3Ó8CÑ|㮳£åÎPì㣶49ïžN·éxèé42ÈӤƎžO8®]ª3K1Ô×@æ°²SW=|ý<‚ (×N[<ûëÌ,Öó³Ãê¸4ÈÓ#w·éœ3 Þc>Ž[BiaIP«  ƒ>¿.Ùs£!±hRŠ "Á<þ™T5£ž‚N™0¹¼Óâü߃¯/Ò{ü~®};Þw7y”$5™ƒRb³‰Kñ~ÏæuŽv^¼À¢Àf‡8L´¬´2Õ9ôáØI RMBtáèŒbt1ª©Œo—Hft1¢¤Ðιt"t1tŒ´362´ËC- µL] ´2ÐÌØËC- ΃ Œ60ØÃ|Žüºp;4¬Í jó:sß#¦µ ŽÍ¹ØZ•œ{ù+ÓþŠ¿4ûOÏ¿CðŽ}9ô@„°æ°ôk—e€ š«´…3äûßCà~€Ä±€  ± ‚Ä, +%Ó3Íèê窶R¥I,2¹3üÿÓåëËõ^ž]¹uÕ S9és® ã|Î-“Ëù¿­òús£yX ‚ʈñ£Ï¼}+<^O2"”óú8u¦¢§Æ¼VXåЬîR?yî¯æÓÝáŽ}3Eèô>RXå¨7©@%”(%,R(Kú`é˧*ïxøµÚ¿?Ÿ‚7Ë·œôæý3å>ΫâK˜êdÒCL ÜC§=â¯o ë×Í“Ðóìï8õ˜6™73bcX;wòúV¥%‚%"Œþ»ñÿ¤OTÖh€ ¥¢(„ f¢(ÍÜóú3©ÐÇH6ÆN™Ì5– ™ƒóœ7žÜa¾WoC6ÀÞ`LÁÇ|Jr> ï·¥A`X,Q ¼Lý“lû^Š„¸¬n*=ÚùÊK’ç¯3~<Þýæ3R‰säé,§œ¥ƒ3|ÎÂ"ÁeJ‚¥Š&w‘Ç®+ÐdÛÏc^O¶¤71ôkËäûÿ>Ï/›ÑÊ;O§è_ˆúÞ£à>çùzS‡³ÇôÎïyÏ™ÇôcäãV8îó®îß`ø¿ðŒÏÒþtáêóö—¬Ô€%¡E1ôþnÓõ|úb²( `¬LÃYÌ5"ˆ°3“L#m^‰Të#\õ’gy1ŽœÏÊôóú»qý^¹ôãÛZLÍóFfk<Ô|¿©ùÛŸ5—¯5AB,©D7ÿ;^»Î;¸üµŠêq=:=8ï8ê½ïÒó#ÐóF9Zí'3¼ó£Ðó«Ñž4íÏ\ãÑ|ƒÕ|ƒÖòSÊ=o,=o(õ<”õ_=o õ<£Õ|ƒÖò[È=o õ¼”õ<°ö<ƒÖòcÇ£®zdêò]òC×|tíÊìñÓÕÛÀ>·›Â¯D”·Ï#Ó<ãÒòèõc—SÏéÆ+»„=/:=å³§=Êé8#µà;¸dôï‡s»6Z‚µ (I Í3ž˜?Mßäý{9¨”À© I 2*HÖs*š%°¬ÃyÌ,’5"€îºóú \ °5™n séÄü¯»ÃîíËôûçÓ]ÙR417+Ÿ>¼ŽiL~WíüMóÐÞU¡Jgy9»Däì9ç±yN´äê9:Õê8Þƒ“­8ºÓ‹¨äíœì8»‹°âì8»‹¸àî^°âï7´9Nã‹°âì8Þ”âïS¼9;3¸â룅ì8^Ó¨äê9:ŽW¥8Þ°åzŽN£œê9:Ó“¥8Þ£•ën£“¨ç:Ó“¥9^ƒ›¨äê9ºS SØåz[Ô,¢Å EBjR,.m&uÿ¦üwë¬Md–P@’È*X71 f†²52,(€ ;Û¯?¢(ÕÈÛ"æBã\ë>~Þtü¿»ÃíëÏõ=8^=}µ=džoG#å>–;q¶)P E%@ ` ±BP”%*”åBR © ¬‹e,”© `Y $4ƒLÒ³J‚Ù d5%53JƒLè•Ó4, %`PQ(RP°(Š%š  °(‘D }¿‹ë³ôYÜ0ÐËPÌÔ2¢,ˆd¹£-S*2ÐÊÁ(’ˆ¢*¡’ˆô3xz4Í5¬èËTÆzdçžÙ8yý~{?+ìñúúóýGLï]M äÎ68sëÍ¡ðu<:N¼­  % PB€ET*PT‰D¢Á@X%@¡E @E@°g¦Ï>}ƒ¸å@á=Ï{ŽD8ÞÕè9ºmŒ:nƒ› Ãcƒ Œ60Ú°ØÃv9ºG£PP % EÍB€P”²Q`Y P¤5%% lÂçBcT÷ß••ûŽ_±><>Ëâì¾(ûOŒ>Óâµ~ ûšø#ï>ý~ >õøý üèýüáHüàýüÐý-üÉ?LüÁh^ªCH-ÍÞLgpåçôp³ò¾¯/§¯?Õi¾]c¥Ž9ï·*àÑ3ù½ùýâê7ŠTE[d¶Kš,UATe ,¡…"‰Q‚Ë’ë  ,(¬ P(bP” ’*Qe8uãÜKU2RU"À°,(,P A` (J”%*R@°Š$ÐÄßF¼ÓÕ%óç×ÂöÚ‡S·+Ë%€InJ¬ Q%IE•EAP,, ¥%‹P.KJeX‹( X,°P”   , H¶SÍ߇ ”U¢Å* APP Š  %E Å%AP,YÖÎsÑÊ\G5ï¯7Hë9Ž¹ç“¼óF|ÊõO=;LC®1ï'CÓ<ö=óSÑ<û:Ä* (ýV——XÕ2Жˆ¹&5ŠœwÅ?.Þ:ñý_§ÁíåÛ­åØÆG85äõ|[Ÿ™©zó©@ I@Ä,RT P J.mRÄ*™dMs:T¡I@ ‚‚€ °) ”óz|ýÂ¥” @ …(KDh‘IPX& Ô ‰¢:HVCX9ëÑ©¯=í‚[ gërK)yØèáMç[9U¦w’g¿ëŽâïŽkÑŒ#¥C ÓÖytº-”K+2äÇÜÎqôyúòýÒøÿSŸN÷™u†h~[ïþoxÑw€,&XD”±D , ABj lK‹UAPšBgpÒʰP”(I@²€ ÍŽyt6•e• aKıK&–&‚P”Y PP²ÂçA›DRYRjQ*Ų‹›H‰§i®R¥Í´ÃH•’’¢È¬ô3n²é{šDÎÌí“<÷£–; oHý5·ŸLêÓ‚ØÃR±ž˜9yý~[>è|ýãëý¯ƒ÷3½ë75ïU>/Ìëǯ;RÅA¨…Fm.D°,APTeh€°,”°PX)*Ê,, ”²‰A` %(e9[“¬j$º3f–B,Á)DU±Ie%‚€ @¢ŒïšÆµË:Ò \ãRÁ'J–s‹¨,¼ÍçböóÌÑn qLë5Œ(ýMŒoE"¢,J¹¹1ÃÑÀøß'í|^œþ‡è?5úLë¶¹I«Í˜¼;ü‹>>ã¯+`YBh‘¡E"ÂÀ¤)¹EA`P%±H”€%°X4)P©H²¬P€©@X,P©LñôyL¨²iRlÉE`T€ÅTTjU‹R(•A¬(ŠD¢jEôÉ©¾®#:Îã1ÔãÑ sº1nMgZ9ky3ˤ4š16¬çÓÎ9î š;¸Ö nØMf‰E”ÄǼ—åüÐ~§?Oéÿ-ú™®–g:ç‡æAù]ãq­áE.hJ@ AP[‘PajRYIeDP”&‰(° @PT  ¢¨€°`€Y@€ %—×ä=K"Ê ¥F†fÑšXBP¥,QPHU‚¡gEëynkVn_>¦Ž]¹ôŠ›9Ë Ž™0›3Ó cX/ID¸7%1b· æ?U¸Æõ¬ÄÞeK“Yxß+~wç¿AùíóïúËþ¦^Ü·ËΨù_×åë΋) ¹)Ø…X P&‰(…%d(&™-AP,&³ºŠ%BÊ ,°X %”%@U‚€€”(/“×ä=ZTŠ,RÉ@*P””P U$¤*Ê(ETY J–€šb%Aéóuš×-Ù§>œË›¨Þq ÛIÞ·‚æàšš9ô æè‹¢X¨”ˆÕ1¬or *¦MqéÌùŸýçºsëúÍ~š]àÆµÏ_6¾.³zòi@ @HQDɤ…, ”E€« nF™di@}yu   X(,  €X/—Õæ=ņ™@°¤(, ”Q,PPRP”¥QT€, !BU3Ózsôñα¬ ;S“R|μt’® ©Øå©L®I¾PëËX6ÔÖ±¸° J Kúr>OÁû_§>ÿ¤üïßκ\\ë_û¿˜Þ&³­àR% @&’QP”@°°¤)BÙ™di‘¤&A(€ÏLj€R(K„©@U‚ @D )e¾G}9î T ¢(°,   Dh%RQe%©¹*ÄT*À šRh™´é¾}fùÝs•f“¦ 7ˆ1ÐXš&¥1t3CäͰès2Øý]ÅÆ÷3cVZD&v37 ñíÀø+ÝáëÏÛ÷>GÕÆû°ËÁðý¾>¼ê[, ¨) À± X ¹APPš¶!BRåK€k:  @**P…(” ”‚óé“—£Í鉨%)`T( ˆ¤*RËÊ¢ •E 5›HP• BZYbPX¦™Š¹Z¹/^=f±Óæ¸;rI¬ê1‹JÍ35N}²5¬ä‘ ÌÃQy3¼h¬Õ³yôÕÅM05%3çïç¯ÍùzrëËëýOÐçÓyx‰¬ºòµ ”„T)¬T@PX•e„ ”Å"hI¡"ha¢âÂo¡€@(B¥*U–P¥SÉëò{H”%PBØ‹@ BŠX… A`©h°²¡(&ªUŒé"QD3Û^Ûç¬ïúòœRï4äë#7pÅ´–Sx6häÜ Kš2Èý[­Æ¸o¦$º9ÝÃ2Ž^?wɯÏgW§?»îóú¹ôŸîþ[YÍšÞ"h–P”&ˆRYDRQ@@HP”% K•¡&†[$š.-.T¸´cY­YH`©T% ”””@@Z”%SÇìò{Ff„P–,P” €PRPE JQm„T Q[‚æ„Rä.nŽ^Ÿ‘ɯ½~O¾];q—7=¯3žò‹¼ mEÅ+0½1 gNt¢¿]Ljä5‚&‚Þ_¹ùÍçï÷9ïÅùóxÔ5d5À°,  ¡@ €‚·D€@P ¨”%P'¨^œÃx äà5 ¡T%$Qe*PQ%)E”% Dš ÑT‰DP”P@Pãà+žC¿Ð3®Ðšçé s s"Q u Y¦Jò ?ÿÄ0 !01P"2@A`3#B4p$CÿÚ÷ø·+«víÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÉRTÜ—â\K1*J’¤¸—âç8—â\\âç8¹K”¹ÅÊ\¥î/q{‹Ü^â÷¸½Åî/q{‹Ü^ãˆãˆãˆãˆãˆãˆãˆãˆãˆãˆãˆãŠãŠãŠãЧN*œU8®8Î8Î8Î8Î8î8î8î8î8î8î8î8î?!Çä8ü‡ãò~CÏÈyù?!çä-?—þW&%Õþ‘ŒMÏ©NSÝßë0‰8Ÿ¤cø×¯ü1iáÉ5>‘ˆI¢¢Ãö;§†¦ßHrKTOøe2§§‘Gé59þ½)újDŠIÄ$Uÿ…Ôô7ÒÿU$ŠIŧò[Ü“êõ½uª>“ŒL¾?á5z˜t»ô¬Wêùú ,5z£<%Ã|3 Ñ0˜d84Ž !p˜eá˜Uá.*ákÒúÿaI¯ôªÉ4„ïô0U«p)i¹ ›¦¶…R¾µ.ýÿÕO Mþ”»¢äñŒs݆ðö3:•éR*x¢Çâ-g¸¸¸J®A¸êí)ø«ŠXº3Äà)Ö*Ñ©IÝí¾§ú|9±OéuR|÷ªYø|5: *Ö§I+ø“Ü+•y2PÇU¦PÅR¯•jë7…~ÝêŸJžœE¥âSù{Ý/¬úE†+Ú%J¯¨îc\¨aï[¡M.¯ôÌJ÷œ%5n…zÍ£N¥GTw²CŠâ°ñ<=îÕ}F 'ôÊ©4×¼øuÄ1J¾Ò…WR¨Çµì¯I+RsU«ÎNÎïÚxr'Ó¢äÛK‹U61•¸4}·…×ËÄé[[ºÿô^žž_¦ÕHwðš{ž'Vê¾ÚE§Q¡]"§uíµO!OÖx§é0xGUqŒÄ­‰evºÞ#Z†”Zªˆ”±iV¹âíe„ÿ[þ®xTœAâë¿sþîéá龟ŠOäî¨1Èæx“nÂê9rp(eV«)2½gV¨ÊŽc°˜ÖÖLªTe6ⱎ¬¾ûÇP¢õ\&¯…Ã¥,/úîj9? ~&ñl¥[Û8“Å8ŽæÑý0‰>Ÿ‹Oy´kr7Ù!€}øj¬¾šì¨'ŠP?ÉáÏòxqþ(…Zõ*»$X0ž!%|E: ¯ˆ}w*í£Wü–ÿ#†?ÉaŠÞ!‡u,>>ƒi‘Ÿ䰧ù,)ެÊÕ¼)™bß~#žÞ–tqI"ŸÓñIü}Ûªncéðñ<¥U^n Ÿ^§—s«úÓ¢$Ôú…tšBw\-^cÅ(ÝKÛaiqk)VÜëzL:]ˆúƒ’Z½ßÃëqh9¨ä¯EhÕö¾BÊf.·¿s«ÔÀ¤×úMݰUø5ÃqiûL%5TØñ ü*]Ò§ì<97úŽ!"§vCÃq7°ñ ' ÞÉW; A(Ss‘ÄVZÕ{¢þÅéáÉüQÅ'ŸÜmïéÔs‡®ÚôÜÔ{qxGPw°D“„à¡â8«×º§WtÁ$PúŽ-;Îê¥U•Xö5íÅàŸAyÍj¹px£–?gvøgG)}GŸÇÞpØ—ÐuÔ«1QÅxh­V¯.†­uÃáiÐCŽJb¬ç=Íþ†ô_RlŸQª“M{Õõ(» ާ[*øj5’·…Õhæ9‹®–µR†1¢""*£Sâ3Þ*þ²’M©/Eïh¦ÄÞ•j5sä†až;ÁÅðšçøœ@žPo„ÒA˜<5<ëãèÒ+âª×äJÇp­ÐÁ¤â>§U!ýñ¯V­/®Áž%‡xÊ´©ø¬=2§Š°­Œ¯W½Õõž©âSù=ÚöIˆ¬Ñ1ؤ?Èb…ÆâTuG¸žúÿا‡'“êx´óÀ¿»º`R(}Ož_øGtŠ?SÄ'ñÿÀWdoEêÔ†öøì•7bÿÀjz¥4º¿p•ìŸëÿ­é0‰8ŸªÖHü¶^ŸËõ\J'¿Dû OØxry~«ŠMÿàý‹Ó‘C긟Oüû8Ã$Pú­méýÿᢌHgÕ]»Wïïô'F%Õ~®î¿~­è0©8Ÿ«ÕO?ß«t0 5»¤ç$ûçWõ}ú¯¨ðäî„!ZÒÖ–¡kKZZÒÆ–4±…Œ,acNKXÒÆ–4±¥,icKXÒÆ–4±¥­-BÔ,BÆ–¡j¡j–ýþ§ì^˜Š?Wħ“ïËëS‘CºïØ«$Óûò SHgÕÝÑ~ú¾–ôjKþ³Q!ÿ|©è0É8Žé$ò7÷˜„ŠŸ|­é0)5ûÆÝ‡žo¾UËÓ~í¾ˆËs~\{Zm÷Êžµ<=?‹½É<Øö”þ?¾;ö)ƒH¡õš©4þùý”¢‘Kèóì¾¯ÞЉrtúds«$TûÓ—Ê($×îQÌ‚=æ%?“ïU} &¿Öñi¿³’P”$”% BP¹ isKš\…ÈJ…È\„¡(J„¡$“Ϻã(ö’I$—!r…È\Òö—´½¥íQKÚ^Âö—°½…ì/i{ Ø^Âö°½…ì/aUȹxzyþ·ŠO/ÞÔðôòwÈË{ˆOãûÚ˜$Šï~ÁQ%î×ˆŠ¢¢µU®AW½©A"Ñ6ö{›èw_x‰:)RG£Ûjûˆ¾_„…Ñ,w«¼ü¦Éôx#TF˜×Y"§`kÜÑUU}Åâ¼kŽ&÷´¾˜ç±[›Q»RI«õÜJ'Þ°‰5ûžæþÏs|§qLûÞ—uß—G2 ‚#Lh]ʵݿos9ìllJ†ÆÄ¡&ÆÙìmžÄ¡(lm”’I(I(I$’I)”’I$’I%&_Qêñ”FU*µ…E½m----- ‚3Ÿ|®”î3ìÔnå……¨XXXXX1_MhãÝÚum–ææü­µI<ôW©czò”‚ ‚ H ‚ ‚Ò ´‚ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KK KK µÜ¬[™Ü·Õ$©¿+nDFpBF‰',C¦§)Ú¤’uI$’NrI$’I$’I$’I9I<ÉÕ$é’I$’I'Þm–ÆÆÆÅWrðï²·tŽÈ«“kzòÜI$’I$’I$’I$’I$’I$’I$’I$’I$’I$’\\I$’I$—\¤’¥ÅÅÅÅÅÅÅÄ’\\\\\\\\¥Ê\¥Ê\¥Ê\¥Ê\¥Ê\¥Ê\¥ê\¥Ê\¥ê\¥ê^¥ê^¥ê^¥ê^¥êqâ)z’7§*“¯§ô=¹³”èRrÅ:Ë^A B¤) Z¤)j–©j–©j–©j–©j–©j–©j–©j–©j–©j–©j–©j–©j–©j–©j–©j–©b–)b–©j–)b–)b–)b–)b–)b–)aab–)b–)b–)b–)b–)b–)aaaab–)ab–)b–)b–)b–)b–)b–)abœ58jpÔá¨ÖÇ/ï/pßTû˜Ïm ž!ÓSíé®L-6ÖMõa]mn÷¾˜ Ž{ñTX.=‚ãÔü矛Püº‡åTTëôÈÊÎ Œã™FQ”BB¤.¶˜Z¼*ø”gOA«s~ƒ$’Ns¥ÏcãXƒ±•”}j¯×=úM´I(N‰$”Ñ$¡$ç$¡$ç$’NrJ„¦rJI9Iqr!q:• ä`ß4ûŽüèäïž'§I•qê ¡_JsÕJ4xËøª¥JJÌÑr¡GŠ%*’¥ÙRgªÒ§}V±¯.ËÔµhÐmLM&SQ2™Eœ˜ee÷HªR£Mipi:–IÕV…6?‚ …¢…zé·å5Ai°ŽŠ½¾IBIÊtÉ<ú¸–S*V©TRFѨá0ˆ~=´R›ñNü“ñ˜~=3HàR84“Ø(ʯ¦~EQõ_PÃáTÄÑà½.XG*&ZºT ö3&ª´eG°UW*øuf³¨‡E•EcZ¢ørµ\áSyQ²§øêшÃ?!©s¡Ø:£Sæ)%ÎBž Ï£WÂ7pš“* GÔeNj*9ÿ Ε/ᬔø‘È‘-Á=íªÄcº9aØÇéµ™.t׿B‹Q2m·Y…á ©"tÍE¥Dç"ÚäYNã ¦9pA¹ÍjUÅ9ä" ¥R©OM…­œ¶ Û%$’«¼¼õT²P”% Þ *Uã¼^’HÊÕ\ª9¨Æ&Äå"Õ©jtN«2I}fAz¿¬“ sâåQGl’,¡sœ^«’ôß$UŞL·BK‰$ùÝ ÆVsb+–è\\=õ"Š*)¹*^øI&EI%I\+Ü«2uO:¢*Š÷©"­'dY[êZŽÉQQeI7AXª,¡(…ÈIpœü#®¥Þ'•¹¾Uj¶š=瘟,&¸ ÒAeósÔGA{²•RåÎEYD[Dr¡3œ®RN©ÊI6$¸’IÎs“b4O²žvÚå5Ψʃt?èFu«6’yž­kª-*,¤™nI&úW%eyÊ¥:n¨«„¬Íz熤ڵ2\“&Q{ÙVšÒx¹&XZRµ· ’ZT\Ê­¶¦J'DË Áš´,`¢.ÉÔcï®Ê‹Ðù0ÍGVÄÑl ’u)£Uøº4‚ô®X¶£*‹ÐN¹b2^‚uй'Q¨ŠµèRkEɽy)Îc¬o’yrd«U´Ûæ¨æµÕ\Êm¦Ý;@¦ÆÅu†sרr7]7£S/œåuÒ¯ÂG¹¤,SÄ«÷9îЙ1îaÇ©ÃÍ5§Q•M]ˆªææu'\ŸQ^™üåR¢¿GÈÕµWõLÿ¶Rº?¹‡kYØ6¨¸KphŽü=—áÿ‰ÏÃ:ê]·ssssss|·äI9H®F£ÞµŸ å§M)¶I'’ªNX—y½‚QU£“çb,ãh~YGU,=*ºèPmR£,~Š8[ЧMõ]V*Y¦Xz UkÒá®i–‡õè%4Ïäm³VÑ¢tN¥&£êUÃ1)çòRj>¢áQÉ’"$¯‚ÖÍzä”^¬ÍÙ·sU!rvr¤©*^ëeI\“Ÿ‚výÿl±unw¥0ô’›tÁ ¦ŒÜ·;žš0õN¯]/¨‹C(ŒÒ%ÕèpòëR¦=×¼D©•*¯¤úÕYW4É‹M µoB2L©Uá«ê·‡ŸÈjÔuW‹Ñ:|Œs˜õ¯åÉ:|丅·5ë—å?(ÉÂo©DÜ*Öj-ʰ¹?£wHR…!HR…!DIÖUî{jŒãEjœ:m0´ï&Ér‚ºÛOž¦ä¸ócÌ$ä²y1æ<ÂNJyˆSÌnFp¤8‡¤.p¤)!H\à…!HR;HRµKT·%I----RÜ•$´µKKT·%I,,,,-ÎÒÂÂÂÌí,,,,#%aaaa`Œ¸e…………………‚l'>‹ï§Û777×¾™$’ržú…6pÙ¦ÔªNX§oî‘>õ]RO!7Ö‹:ç}(+³m7¹3rÆm¦÷èU„né¡vŸ‚vÝú£¬¦†—TäO"£®?çî‰ÓçJ «åu;$Ô>t ½tü¨šT^é’¢¢‹Ðnn§Q©’tþÙp+想£æÃeòþèaÜ­¯ˆk[_'tgMèœü;­­Ûàƒ}1”Aã^tL;,¥”r#%γ­§ìmNgâ5£Ûcô®†£)PEJô2Lé%øU¤ÄÃâ"•3asWÙB«Ü÷‹’e„GpkÊaóùQôÖ¶'Ú9 ½DUENVÔ¨ê¯>TN†'ôÖ”Âd£z êÆÿ¶.IÔÁ~Ü3ñ ]èˆñë•xüŒCÚúÙ8N‚9Ì\ÔnHªÕ™\ÛÔL> Ô8XcêJÁ¢sØëÙÛ¶åošåQÜJ´Û}ND’I9.x§{É*=5]9³­eLB>†š5ßAÔ«Ô¥QU\¢hü§ÃÝ{³L’£’ŸÜ,Ó®N¨ç¦H.iQÉO5¡N½jiR£ê(¹77½ÏvÛ%ÄâÜvns¥½ 2Ûˆ¯?¶IAîf_+щ©NŸ&ô^{Äç.XGvÍòØØØ;å‹YxĆ”è”Ô»"¬¯°Mkѽ>rO.%É2§Ij ‚¬¹ü¨™p_Âà¹)dÑsn¢¶­'Ñpî‚u)RuU¦ÇT{Ûk•Ð¥Gˆ×aâžH.u免¥m!ÝåCµÕ”ª>¥z\§ÊèQ¬ê/¬´š êÊfQG9\¹³:®G&HT§-Øf£?ÉSñ¥Ѫ'>ŠÛW´I$’N{”å9UYªÄ—éœö<¤¡"¹ “è§ìSIr¢J6š%N¹»MÊ­ÕGê¥gºÜ“Fø[ŒGñ2CäHœe*ϯR›é¸\“*UJ¶aÛ—È "9h=˜\\ëÖá×Å1¬Ã‹ÐoS;å§Žÿ`ù¦§rÕ•:w¦oèΜ¶WV'¥ÜwÊî¬Ø1×3´înnA!llm›aÒjë.[—K½‚ò®tÝkñL³’hezÌGb+¹2M ç8¹Ð._96­F¦K’kQ:iAu.IêÓò7:´V–‡fÆÞ÷6×gý²jÛƒÍzSöÙ=†Þ^×:w77ϱI *o®y[ì>u¹®jè«Uj¯'ã“ó¥zˆ¼„T% ‰BP”¢ô”% BP”%2˜t¡(J„ ±p‹ r ®E.A¹/Dz4¹¥Í.i-«‹Ú\Òæ—4¹¤¦H¶ºö—4¹¥Í.isIisIi)Òæ—4–’ÑU.Lúr:iíµ{VÆÜ¬_§ã žM„Õ$æ²BåYm§ìU—ãÌy1æ7\ì¡Hq C‹TµrêZ¤)j–©jˆÜ•$´´´´´DŒ•$°°°°°DŒ•¥…ˆXX…©¨XX…ˆXX™ØÒÔ,BÄ,©’¤–!k PV´±le±b¡b!bf­BÔ-BÔ-BÔÎÄ,BÄ,BĨ™*"–!b!ÃC†‡ ±§ ¥8m8m8m8m8m"ö¨"Êw( ÅõøÃ~¬á6Ô¹â]¿±’uO"â2Üß=ÉÏs|·7sÜÜÜÜÜÜÜÜßVú÷Ë|¡HRR‚‚ è¤Î˜rEU#8 NR¹g(B‚žJ{ +¦ŸhÜß8ÎtI‹õüPýyïËzÜÿ`åä;$Ô½DÔ½FêþÊ7§dvMõiþÊ3Rz¹.êVvØUZ7P¦ŒJ4Õ«²¯)=†b§jžV'ö“øõíš“•W[Oد«Zú„Õý„Ѻeý„ÔW¢tìŽèm+“zéþÜ—'•:^ësÍO`޵ÝÇs|±´g£9Ö¹â—Ù Ü:ª- ­Iíf„ëŸã¥ŽÃÕkrhììÂ1Œ}¦)÷âL©á輦k™’éÙ]Óãû0È”±©µ¯¥+]”é½ro¨¤ŸÃÃc©­:I—É9I$å9;£f%ĸ—’ò\]QÊI%I\·7Oa‡tÒì±¢sŒä’r­û„M·77Õ$å9T[ª{èR…Aq-¶¥[Ùšôh¹¾»œ?®Í¢åIZ•W†z%JgU.iŒ¬‹ùÍ£»2ôNŸØ•›Þ^øYU^‚zÄsš¼J’—¹x5‡dž­_+Ò˜Ö«k®s\ÂÊ–äjtoAQSKDöGoÚv66Ñ$“•OÛóÊR2¨ëYì!ÝùÒ½ùÓð/]+ÑëÙW£z/]?õÏ «/»…ŠOç7¾›+b†âµô’š¦Wÿ"˜Ž ä›:Š1jbRS+5Î}JH÷W¦æä»¥2›¬¨ú”nGáá±8j˜YXº7O`Ç[S³AìllBe.Këo¯‘.x—mìW®·uÖ¼…è|é^‡öì¿ èí_;E:‹MüZeG-EéEEÕ¶K³¥5:2_*í–ÆÆÆÂª Í颓®§Ùw'FùÆj(…/Ù§~Eeº§±U…¼¼¼¼¼¼·U%P¹K”¹K”¹M×%™¹ÅÎ.q.%Æë’É.%ĸ—ã̹*ò^KÏ9ç<çœóžsÎyÏ9ç<眇òCÈy!Ä8‡âCˆqkˆq-RÕ-RÕ-RÕ,Éw,--RÅ,RÌ¢K KK s´°°°°°FeXXXpË fJ‡ áœ2ÆpÄl ¥5ühÂ;²ÂÈŒÔwF”i:àTLܶ·ÙÂBBG¹Œ¡H7!MÈR È ƒr‚‚RRR!H!H ‚ ‚ ‚ ´‚ H- ‚Ò H---- ´‚Ò ‚Ò ‚Ò ‚- ‚!3ÛEÖÕì²I$—¢…èB‡ìËmkž%ÞO¿NSîë›ì'ÚÁ66Ð¥OÖ†öÉ9É$ç$å]f§dºa]äìsÌR§¡ ´Ø3¡aç²O¼“nD’I$ç$“¦r’I')'¼áÖÚ½ŽI$¥Jž„(~Ý„]k–æ!aÝfmyk‹\ZâÅ-qk‹\Zâ׸µKT±K °± ± ±!b4±¥,icKXÒÆ–4±…Œ,ac XÂÆ(×wn‹ù*~RŸ”~QùGå”~QùGå”~Iù(~J’‡ä¡ù(~KOÉiù-?!‡ä0ü†vvjg™Æ¦q©œjg™Å¦q)—°½šwÊ ‚ ©èB‡í!BÏaT’LC¥ýßdNÖô½Þ;\’n,ô!KöJ®dU•îÉÛ›ÜgNç˜ócÌyÍÍÍÍÏ1¹æ77777777$œ¤’I$’uArw¥ ~²Ò±ž!ÑO»§nN½ÚtN©'•šv ƒn\¦J/DÕ".…Óˆ_7wNµ=]ÚK‹‰'‘%È\\…è\…è\…ÅÅÄ“”¦KÉÛ9$¸œ T ùùNAAdå¹ÝÝ:vßïÝ›¬8Hp”±Ä8ÜÜÜß8ÊRÒ!KTµHS}&Sª#(BÊ2\—ÖSôg$Ž\ëº)ý™}}ÞÜ•/RòZlCHBCO)å% ’âT¹K”¸¼“cÊCKP‚ERuí©r©µE(þ²-ÍôbW~î½½Ý{šk„ µÊ9;åŠÒÒ2’âDËmpA!.U¿j˜eþ<ä’s{®Ùª ïSÝô'Dä«”èAI7Q’IÝW8‚ÒAPRN­ò¯ûÂú#8‚ «k>ÏS¢tîÉ×8#(Õg¶P„AI*.˜Ëb5Âe¾œO¯ã ÓBçŠpŸg¥½;Òrg’¤èžtèS|a‘Q×Tû;º3§sé¡¢èÛ8ÉÊ4í”ç0Lé”'\hØÛ(ÅzS¦Õš®uk;̯o^ŒéÛzrš¹¨‰ÈŒ¶Ñ ¤g9ì&©BIÖ¦'õ¡‡ý™FŒRíÞS¸Sï)šå9O!4È‚‘”(\£DF¥1­¥Ûª²ÝS¼§po^ð‹´ˆF„AP;è\‘ ÊTÜS“<•+þ¶”nJ¤äõµ½é:vôõ÷„ÎIÓ:IÉ3ùØØ„Îìç(ç)[õ´§û%Ïï/Ú¿¿yœ·Êrß5'%äF˜ Tö Vô4§ûÝuNó1ÜW×ÞaWZ®¨!t †úg)ÎI×:T¯úÚSýˆ.nXN½é{‹½IÞPTÎrß5]霔’r\£)æFjWýhRý‰£èoz^âñ;Êu×&Ú`Œ‘ÍP\’H'Ø©ˆýhQýš+¬Ôï_=§Dï]H4)”L§9'Dë‚9Jb} ÓëÍÛ'Uï_=§Fôï-ÎIyˆAFjFp¼õ1K·Æ®x—C;ßÏpFtï(FJ„d™Á<‰ÉU:Š ¤û1+æ^˜Jg]eýí;Šô§šöôåFâÊ9ÈŒ”APŒ•9ÊVY¨â¥2U„U•ïiÜTg{B%‘DФ›‰¥u®sÎQw{Ši¶X…†wÆôî,ëÍžÚ›d¨FŽ™):vÑÓ%ËlàPA_³PþÌÏépŽbw$õw´¨‚FPAi¦˜ÊHÊ3ŽLA¾µ1 »›’슲½ízw/ïÞê'™¨%aŠl.PB &wdº)rùç)Š]¾(õL«¬3¾/N便b½ª§HÍ*9©9H£zL*[’ ×àŒ£uBÉ^ÿÄ%`1 0@!AQp€P"ÿÚ?ŸkZ–´*Z–µ­kZ–¥­jZ–¤ëRÔ#§Rÿ_ ÏÂóð¼ü ™2dÉ‚`™0L™7ãc-2Ó-?I´ •å2ag)ÓA}øƒñøâÁ ´÷L@ã»îÍÆd!{A2%2ö½\ÈBö…ócoW0CÞpžÎžþ…âÆ{‡AL´ËOÑ­.téþ¸2Ó-?¬™xLY–Õ-ªZqù"¬Ëj̶©mX–Ÿ2ÚŒaÓ§³§OÆû °âPéÓ§)ääÙŠÒP¤­%i2bP§qÄ”—@o«û/ü²PFPà2JàªIï„ÈÎ<IRàªHsÂsÄÑ¡™aTðU$©SÁVd•eS‰eYCêĔ炬ôÞ%ï¶Ñ/{êÄ›Þú¤Ã;̘gqÄ=“&MvL™2dÉ“&MÂ7U-9–JPÆÊ¥(caÌ@vU8¹”Õ•Mê”Ô©¹ÌHuêTË*ʧªTr†,sQaت,éúgXÆ'ÿÄ)1` 0@P!A2QBp"a€ÿÚ?ßhÃ'ö²,«*…•dYE•eY|¨Æ[¢ŒB*J’¥J’¤©R¥OêSvž”Y¥h»EGH 1RW•%f(nbäJÊÜ' ­E¤ ëSœJk}‚ߤ sN)ÍqMöœ=¦Ÿ\ÓÓ·›S¹Cb7˜h™¸äÚXb¼ÇzM¢q…˜ ì “f¤ á>p4M·Ü‡Ä'úMø ÂošÏnNtø âÃ)˜—û‹ko¹ˆOô›ñ åA‰ROµáŸÙ^]å3Þ—û‹kb[“(œ9à[ô€•ꂆdA’¿’üS}Øæ6»Ž(RÄo0ÕWn¦Åo2¨xÚ%bŠóH@®²P$J‚e!HReä ,Ï€²¬«*Ê,¡K´]¢íÿ¬%J•*Siv²íenÖÖíý¶Ú]¬¥ÚÎ ·[´x7k[7h»^ò:È+*ÊP \ᥠB 0Y‚Ì6|[¡¿h»ëP®À^4ȵÀ„]­½Ä(QÕ4'KŒ(ì ÜF» ÙQ¢:xQÔÐlŠlΩµTýr5:» Ù…ÑÍž¸SdSfVb§LÚ†—cj°ÞŠ9‘Ó1?a´ÚžDìOzfÑ:¼OÃhu¶½Ô(ê…6Ns#DvBœS¦tOCB…ƨé=km{YSØkmÌi¬\Æš›[µ—h¥Ø+t #] º®K¡´N®"·Kh‹n–§b)tµ:ìm«ƒn¦Ñ:¸6—PF·XŶ´qQ¦ ¶çÿÄ@ 1 !"02@APQ`aqp‘3Br¡±#RbÁ’Ñá4ðs‚¢²ñÿÚ?×ÝÓ{Ouí[VÕµm[VÕµm[VÕµL©•2¦TÊ™S*eL©•2¦V‘ZEi¤V‘ZEi•¦V™Zei•ñ ø…i•ñ Ó+âñ ø…|B¾!_¯ˆWÅ+N×uÀðäzO$%S}¹ö¨sÔ«’’’’’’’’’’’’’’‘R5B¡ß‘ØjaB‰cm#ûci”aŠy#À¯…÷DB 0ÅYƒ\Ñ'A l´V[X]$­ÑÐãÓg¢ø-"0AÖã6ˆN¢Ñ¤Ã²‚cÒk*”;ú‘ÿÙnNŽ ¾S2ؤü„ÒÚ2zœGª§€‰ÁR6Ûi;L&`Ø6eø8øMT¶0N…ˆ< £á TËz»El.Ê9°vä–žµ¶#”$²2°VÜ0LpnÌWÃE¹A|4mYÅ‘Á[†Ú¦¦¦V¨íG,­7{ÔE³ŠÁçÝD "Û±ž+HÊ -q‘‘Ð=VŽ÷C÷]‡UÐN…#±šÅweÔŽ!‡ ù±Õ8ôäwVWÁû <-œQi95–#¾Ý`Ì È¢6{¬H_û§¶Ã±ò Ǫ)Ç‘Ü:!äsÉuò8 ›ÝQöäŠO$héÉÛäsGZ€ê9&Œùʇ~Iì|Žô¨žœ’þÞG=óÛ’HéäsZ½y)ã­@ƒäQ¨wLä§ùäSNJE]|Šõ¨r[_#:Ô;ò_¯!ä0÷Ytì±÷+à·Ù|6û/†ßeð[ì°v+"“ÝeQžã@oj£ß’ßÛc7‰R´x›³ b2›ÓÕ8ôä¸oðÖˆ•j“)ßjòÞC=Öœ;,\kÀ•ñ®+-¾ËcÀ×ä½Y{a¿z¢œy1Ý÷í–¨6{MQyPfHûæáŽ« ¬¼,tvú91Ûð5ªË}O¬Œ_øQq‰Îà¬ÒûÔZáT>S#¾MCºgnLomö• »MVr¶žˆcô:H±ÛàÔÔ&4õß~+¦eU–韶¦(žpÙVb[àµ4ušwÐ&j/(¹ÇªYv«Åtûïv ‡&»¶úµµøÕd–ê­x؃„ŠsÔA˜ÞÞ•8ôòŒãSŽÓ€ÕÍî*·±ÿìäSÏ'?¾ø}'¥V?Ž®×ð(ýqÞÎ=Q^¼œ{o†uÅSœv`qn„æð0ÞúgnNié¾ÞR{{ÔÖ •6{©³Ý|žë*ÔcvÓ°gåZ ûk²É­û¢ÇŒkkDT\Z :ܤgHÔþ¸ïGvA54täàzïz!ý…L]UÕt¾ˆAü8ÖJ0Ø*‹½q Ê;-pý;°®‹²¦ú Ê?j¨ÏõÞÍžw½z¨Gz¨¾±U—B+âaúU F)÷Aôš\8U ¿‚¹PhõYXó#ôÔþº.Ê›è7(¾¡U®ôhëS{ò{»oz*¨¾š¨¾±Uz Žâ+±G¥øV£Å´@"çeº<8«nÒ…Oô®‹éTßMÆ}Bª?«z6¢{òィ»ÕEëS>¡UÕ]þµÚŽOñP .öPö 'ñ­þ•Ñ}*›é¸Ï¨UGßzzTóÊÞÔGû ¨Z›Þ¦}UFGæ¦Y™+ ð¨ÙìF1ŽØìPlö•¬7D Ÿé]Ò©{\¢ú…TC¾ôr(÷åMî×q Ý5  æƒÝ|&{UiŔӬ»þk.q€P3‚?MQuI_¾ÊøMÑ*‹éD8D/‚ßeðZšØd¡ÐT87zÖ¦rƒNä1ÃSgLÛÄ^‹”ž¤õÏu˜Ü ¥8ñQw Qw ªÓ¥/›Ù|ÞËo²x‰iؘ×F )Ÿe3ì¦}s%f ‘þ•RŸí¼ÍA0tå]îú?ZŸ×Þ':ÁÇt÷ðÐÔÑÊ.Þì½AûYøÕØÝ›jmè;ÔÞü¢wÄ Û‚ ȧ0ìÕ¼C7~*s¶HoFŠ£ÊNï½ÁùN«mÒoãUåÛU‘¤íê;TóÊNߎ#ñWˆÝö:˜bU»QqEÞÛÕÕ;¿)zo€æœB¡‘_ÐÈêVݦ~ÕxL8 ïgµ7”šwÍ¡ê{J-pˆ*#q០U·âÿÅ^ÊÛÓ|°tò2"[B´ÓUªôÿ² ˆæHÎņ.ãQeŸã{”¦îÛîÓ „¬¶¨Ñ›Cî æzæ2Y‡Š4†Ñû(¢N Å ¿–ú`ê<…&PãµEPsAî´löY4Þái±i1cJÕ•Hâ°£ëpN貎6f!¼Z:Ô:r£»ï؃ ì ±Éî²^ ìª@¿m‘î±vûmO=9Q܃“JïuñOÙ|_°_Ë)ÄïïJžyPvò Õü¨ÞþA“Öª>ܨ|‚5€éÊŽíäª`ê<×õ©¼¬îþ@¶§œ¬|ªyåfùjîyXwòÝê£íÊÇÈ6Žœ¬|€50uóeœ°îoz‰éæ»j¤;ÂJJJJJJJJJJJJJJJJJJJJJJJJJJJYÖžô¨÷å^uLå‡sù©£§,~54uå§wçÓS;òÑçßZ£Ó–éÏ­©ç–›Ï£µN=|×53–ÛŸ]SO6åÇwç³S;ù³Û—®ÍN©î ÇsMMMMMMMMMMMMMMi- ¦´‚Ò Ii ©ç§.üúó×—>ާ—Û~h•ŠÁ@ŒV-+¿(þž~$¨k>‹[ÁL´}QïQß`rë»îN»?µN”{\1t0ÞÌ|Ùo/sÙ¤>œ½% ó=RuÍMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMN©ÝhPåîª0ÝM@†Þ<Ÿ•fC¯-J¾«|²fÝÿ=w·976ßn['ŸÚ<þæð<µÛðTÌ:VbÛ㮥¥ËðþëA«EªM[ÅxcöGI`űˆ½Ç“rœÉ¬ SÏ;MMO1Íž“hßÞëXù+-Ñ":³¢è‹ CxÿÄV$G‡ ¯‰€hþER圆Çÿ!uŒ”J m³Ø‚ˆk¢.†ˆåBf?„Ë;[M{Ž$ŸšEØÕ¤ksÞ~h `‹›VK¿ó ´…ÿ+c÷@ÿ'1áéut‰Å@5¤B$ÅØ#`dì¹F"áàÀZE¢PDUŠ4a²&V¾é°ÚÜG ´x–4™¬–‹b# Bp×F`-ñL0}F27,¢çÂ6øGü…Gb±‰'îˆh€¯F^óZœUŒ£8ŒDà´0™ÿõ>6àÊ«eÛA÷‚‡ˆï–8$ì³Ú²Ó‰±+ŽÉcYb!ÀâŽ\Æhœ2q@µÄã¨Cˆä®'‚Äá¹,§{-×°À‚›)•µIh­DÙtÑÊšÊtQ0Àu„J‹AãYÀÍ æ­6¼JÂÉ•xl¸ŽÊ&jÓ‹Gr1A±Äí) Òl‰‚pàk ¥<Úl±še¢"Ჸ(Z0à›Mi°"(>ÔìýÄn`J¶Ú1„ ¤3M–Æy¸ -²!(9<&M%†’]lÀA?ÃѹŒ-¢ÀÃÁâÑÂé.ÆavFÀöº"0Š4¾‘®‡p*<Bƒ0 „¸©Dñ*0Ö‡z¦¦¬ØiEÝ0îCè±%'ðÚ©¬:ªŽ U7La‚•^ŠU•>«ž ¤±m0û,cQRRX¤TzU%%%$0¯E;Qhè¥\”“mpÁhÕ¤¤§÷XÃÕ‚ÑRQ•cT‚RX©UÅIKî¬ü¼"´jˆÄ)}ÖŠÑû­­7Ùb´V‚Ñ*uÓF‰XÉA£j“Ã^ØŒ0«°ŠÀÁaͼw|µY®»´åe‹¯ñ:„SçA°Êˆ8äÀ“é}ïƒÎÅaÄGl/RØÙl£•E`‰åZ»>À˜' ½7MDɧ ’HõDÀ I§ˆÞÄÔ.ÉK1*€á®µÜ ¥5›m#€îˆu+ˆïJ„L/Á¯pëÉͲ'6Ô8_ ´v”[`f/Òq{Àöºu¨–ðÞ²F§@g(C ¡Çøí®ǽùÝšSÛ\ÔÔÔÔÐ…D5555;“SSSSD¨Õ5555…sSSSSZJkIi(Ǥ´”ÖZAj#®Îu·½G¾|ûkX˜æ¦¦¦¦§rjjj{–U`!rUK{E½YPÍJèn§,Ô·dÔÔÔÔÔÔÔÔÔÔÔÔÔÔÍpSSªjjι•<Ü7 8oVö©¹©ÝqÖF`To Þ;Ôo ÁÍF¦Ñ™j³`‘ãè|Ñ ‰ŒˆÄÚƒ¡Du¸q¢YÑÚ¦öÏ8îŒsGwŠŽ j±²1¨EÄë¡Ü7©íS{gšß]H KÚØÊÒAZp†0̱ÎyÊÁ±EŸW†ŽHAìT†;o9ñúG ëpý»xî£pa”ÈuŒý“Èl¦#ÑR1¬`h&‘Æ*Ëma8Öj{Å{ƒ„Ä`Üê2Ç[l¸ú¦4–Ìô5 ²RªUÏí\”ª•Ù))),u&ôÃsIIK8ê†yÚ˜sšûVa%“j>oeFÜrc\ËfhЦÃN¼ÂéGL(‡·ÝFóa˱BËabÄ7Q¹˜£”qŸUfч¬nŤ„M³ŒñF$¯„ÿlШ Ñ2U˜eFY@ŽêÐa‡VnbÕœÝèþègœw ÜÆ¡˜)@ÛDï¶(ËLû-2´ÊÓ+L­"´ŠÒ+H­"´œ¦å7)¹MÊnSr›—̾eó/™|Þëæ÷[}ÖÕ’å½£š’•rRÎMMMMMMMMO<îÕ79ß þìŽÞgwj›œ'{Ÿ"様57¾lõæcÍ6f<æO“¸Õ=Üîõ7¶dïƒÍÓS¹%+’RRRRRÖ]ަݕƷ™Ç3œÛ3ŽiõÌ8ùž*va­õó=µ;0ãÍ™Çz÷6ZÝô|–5 çÛš3…ây¬ó3ªƒxù¢ê…ãÓÍPºJ5Žf5 °ã¾ÆñÌoößcÉÒQ>i ÈqæÓÌÀ^‡4Íâ|Ó5r<ÈMèpæó¿1Ýñ>Vã¹…Óæ?ÿÄ,!1AQa q0@P‘¡±ðñ`ÁÑápÿÚ?!÷zî…­Dˆ[¶!ˆD"„A„Aˆ ‚+ÉžHäŽO$rG$rG$rG%÷!î_rÔ‡¹}Ëî_s¸î;Žã¸î;©Nó¼äï'z$É“5¬ôÀ$pœtxŽ#ˆà8*\GÀp' ÄÎ6q³œ îúw*—ó:n±üŽ“øÉ÷9>ìþrýÙý«?±g÷,þåŸÜ³û–bÏìÝûv=ŸÇg÷¬þåŸØºÿ~bÏìYý«?³gðÙý˦ÿè«*%W|zh p´9ôÙ;šSj±ÑÓ&Õ}p_ß\ž˜»ÿê˜$’I$’I$š3I$’I$’ʼn$’ID’M$’ID¢Q4’I,y%|4|X ‚T±…± b¶!:Àq#ˆâG8‘ÄŽ òÄv¯C^Ŧ¹7ôµtØ‚+µ\R:"þöIà’NûQà]ì9VjḲÔл/D¬$ßoaãѱmˆ[ˆ[¶!lB؆Ä6!± ˆ @4¢㸎Hä¾ä=Ëï[î_rû—Üî;…Èî;Îñò;‰ÞNâLî'y;ÉÞNòL™2g¢$É‘&D™3ן°í£´í;Ó´í;Ã°ì¡ØvPš„±;„½Ä½Ä·÷ÜKÜKq-Õ±‰vúãÔÁ­c«n©Ó4ÇL£4ðiXõçÓíÓ'q&¨ò¬¯7¦¥Wînœ»‘e&À;'„> –Äaö¼öÛû #6›DÿQî‰-—– ÿtû‚LGÝùÙ—&DÅ>‡—5itÐ]¾¹ý Ü6ÚÓ¿b<«‰šþ‚%ò&+ÓRßË$m¶†Öç+“D‰w<Ÿ¡ñà%Øo ¶¶œˆ‘öÀrE•ÆkHƒhlbÄnɈË|‘±ÁÞ¿¡eï?õ uóp´ô"Ggþ}8ø ôg׿€<âÓÑ~»®õœ5èB}9ô¼ôö!yfŒ¾6žt7r$`—l@îûŽÁõƒôhÜx¦àÔý‘KrŒ7gxì=|]ÉÈ„Ãb »2w3&]lß&vf î™ÔîÝÜÄðr.i‡%.ýÇ2­¢ÍŽCÂpÆJ,R¿rI×’úmؘ/vÈK Ú;|Po^úrXË;²$Ü)jÐbÚÌ>Í¿ÊÏËܶI Oü¦{Z`‘õç¯Jk[—÷ÓרPÿ42,(ɇJÁ‰ ì:¿s—Å:™—(î`+ýˆ/ oücSŸñĤz­ÍW­vè„#™òº—ÁN}›÷³òw6±zöG*üz} ¦.ôíMQ–Ñ­uTÛ¦åÍ‹-ðþ#A ãétI· \Ä®¹'¢~*õ¹OiŸÍY·¡¡ÇLõ%jn#4|WÉ¿EþDy DD¯r·Â&cÓŸM@Éd5Iö6„½]Øì}óõéZ¼ÍmYkKtiњə8ø9böL¦Œ_)3èÌúKÐ’~ZîÐðwÝ]}¾¥Õ¨ºfcá>Ðú¹’íBùY$ŸBÕšODz8$šÍÈDѳÀð¿£=VêŠ=ÑssD\ßàg«š™>Œ_Áiì÷ô&FC#ЖºDõÜ’Iõ—²Y8¢.äqáÉÇ¿£·^Þ†¦õƒRÒYôk_&¿ÄŽ#s ëgñŽ4öH—ì5èLnÑk§©4_ Ðî f)¸ü—ïÔÓÑÕÓ4ç¦:“]ÜßÑï(tÛØGÆ;ú¶×Ó~ÅzëÛ4v…ŽÁz_o~·\/„¸‹‹7xù<=(ê‚ N=9õ§ßjhn†ˆý—Ø‹ÔÐÓ¯=z›úÍî¬m25ùCôUb±HéE¿f…ðhü‚žø-kµuöÒI$’I$ô¦$ËdBºbÇøQ-ÖF–áyš?áƒùBFà7ç뙿¡¹¤ßù4iØz³ë/„EÊØ3µ½ƒj[Òx•MM:äæþ‡”d/“u](ˆ# ŸIcÿ ¸•Ñ6߇“ýë/¯C¿ "Äú~×Ô±Ž…Ó&•¦þÖ}²I¹EÕðùô§ÒHn6èˆXv4UuƒÛQ yþ$3W»l™#»81oþ˜+|ØXáìg»ö¹q§ï·«ÛÒCÖ(¾ÑÙ9تk]:±EÑ‘ô¯ƒ€íJËä'ÑTF_w±/îºXwöɾø¶Ûm’Mf’HýŒ*ÿš^eiºìkËS~–kßI>†‘îÏÈÎéoÛkìýŒû4AȦš®Ý å…•Ýì„5ÿ±Iïã5©I«éšHÕ6i­P˜?&MJé…ó4é1⥱ßkBkjmÑ )¡uðH¹oFÏâ]dŽ™êD½™ Šþj_š&òèåz°[âÂi%9B͸˂ÃÝzË¡| éhä-BÁÜXÚ%]=Šõ4¹’(º#ÙA>ÁŒ5fŸ0‹5úê— ýìÛt’}TÄàÍ=¸¢ìµäi¦ÓW^é{™èóÇC´‡±·£c\Ò:$Ö—®åŒ|&ài™3Gèéí’œ{„;Ký"IXF“¸[²c öHh/·ûÕ,íu¯VH¤½ƒôw™NÎsõì_£~·KtêcàÑ.ÔXùT"âÿ’œ2<û\ þCß³&œËpÄW ‡Ý{4Ï …îzpY'%½Xê·Â$²Ý ÙÀ³òÄsbý„H’Áÿ¹csíQ›ÐŽ\‡®3F/F_´!ã°}3ßÏDtøøh–ÏE—ò¨Áñm#W ù~”zQU;ødăRŽygÇ©š¬š¾¥ëÛØ¯Ý0áußÞ.x¯ƒSÏF*¾ySUòˆGß¿"ñ¤§èÌóŸLVéE‰ œa¨Ç/úuGJ~šª4ö úXfk¦ãíï¿k8¦=]~Å mí’÷ÈVÚK[I„"Cº¡ 2“ûñýèä²Èì)<_Åh`ÃYÝØi¦ÓPÖU!’pœÅ3óš#A¢?2ú=ÁŠx/ïGМi= ß½‡r;s‡•~=ö´e<üR;¼³O´zNÞŠ9“÷ÒáÆôîd4É´Ô4$Ü׸’J ­#/mtG!3óÝT•äÛ£¸&ŽçeõíÔ^W³z-ËHXGr’ü‹ÝÞš|‚;FãÈ”Ê:'áÒÏ …š7 ÿ;½¿ø !KCÞgD"è¥âCþ†Ös»„ynÔÇ7-°·¿§ò;Qg©ã(¦·ùù2>ÅËqíµ'ØZž~mØÐ!u¿q”Ú=$~cýSóhþMéùQbrŽ ßØÑĬRu“­†§;®úö"Ä ÄH|Ô­¯¸Þ~SðÿZ,п;ÐÑüKÓùœzÊÏݯI»&éØ/hŸGZÇ£Ÿ†wb³Qkòhx§[¼Qüfôþ¢MÙ\(Ô’X&÷mL_DÞΟþCµõ¢?~S¢'ü&˜<ý„Õ;;|/nve¾ßÁÇCøŸ š,üCôŽ4ýôí—TxgÝøö¤ ×Z$?Qn4º=N6<6%ÝŒXäeŽLI,¶9&åËÖŸúÑdüMÃ’§ƒ¿³NÀ#/æŽÑ+ßàøèÐßâ î¡gät¢ì z/î©ÎTƒ'd‘+TP‡¨ËöxE[!ÈQ¨•ü¦•¸_÷äw?+I­µ'þ&‹i’‘ø”éïëÄÂW7Éí2gùö ü3ýgoc´üZ/x©·\û<–¿ ¦¼Ú!íìUÉö?»ðFÄÕÓ†44@RŸ£ú£ú7ç=¥\Q\k&™â³rcþÆOKx.vO`„çRyñÜYòVI(V:ÈGáZw¾­ë^~ÐÞ„Íy8ñ}{?IWZjM7øäL`Í>Mï«i„ZßšÉ4’k$$Ž7è’I¤’M¹É0î‘¶Ýý‚ø6[;Pä¡/„‘ü_€"'fý´{älRF'*HÇ“»Ûòê{"ýrû/“CØÝ _À´ë¿Ä"#ºf ñèL…v¸Arˆ~NÐǺö¨‘ãhˆ¿à_½¯vÒâIf¿»¿ªýDO_èIMX²ïö¦é+öˆ‘$¬°_M(‹•°gŽúO¦ßà‘Ó_Œn}1£ÜÑ*›·öd¹™ éßv>XD¶?xѲ£sí—¼CÏŒÄ.ßãóIøZ½ý “샷«bˆž2Qš¿¡“ÊD4LÕÿ ö-d’»ÁeÔÚŸõe~Õ{ ö»œ3Ñ/å§Ý¢Ç7µ–½îÓpýˆšÒbú¥t]ØþBñÛ­P¦¥= §:²2šèJHêƒÈ3>E­N¦XÍ·-庶i-ºÒ“Œ|Cx,$‘, eò’O½‰Œ½KzSè[ªÑzíBWéLY‚ÿ F¿µ S=¨áXq$‚:¡™»’ÃÇqå’Z!ñ$™llÙ·vù)'¡îæ°¾#Š üZK-бO>ÑG¦½[×CS”îEFGíH°6I0 ߥ'}£ûVÇÀ¥ŒŒÝsóY–qÐãÇ_A2éM¿¿aýåîOüR³ûeé§í1´a§ Ä?S"¦Élö·=2cÚeþ,ÎV"Ãh‰ø½›êÃÚô—ùSþ)2N>Ï=h”CŸdãÖšI4`ÈÿPêƒòìÛ¢~Yô¢þу<¥Š\h95Ÿð7/äÈw[?’ŸEâ‡m?ÂjþÍÕzh»™È÷ÌgÝ¢n(cɧÄ[ü.=éy|3q¸ ÈJ&=)÷(X\Œ¿ø Çh±æUþE/ ŸƒweÀËfоi)Ÿð'ÐÖ­ÐÐíËÿŠ¢F¯âßø¦kÍ'Ùþÿ9>œõO ‹†ét¯v‡7N+¶Ÿõ(Ø<v—ÏI5Ÿf‹‡ Qèþ‰¯M•SsÒ¿ÉŸ¨ÐÞ´ç_?$Öz$ž™ôQ °ëgìçØÏøÛCóÂ;3ä$’i$Öi=I>¼õ¢.窣ü=|&ôq³ü„’I$Ñ$Ô’I$’I$’I$’I&‰$’I$’I$’DÅÏuòÿCÚàgãWÈqŽ5xà#‹_øt|ì/ ] IÀpÆqœ'Æqœg¥jOøœ“ì½´ˆi~Bq¿Ê|uØ·SDéÛÐFnÕ/òˆ÷(i&'wßäà¾å÷õìbˆ‰ñ#÷í£Ÿsµ½â÷w¶ÝšN_‚ꞈ"úZéŸBåËž $BJ·E®b‹âÚçâø†pXy9?fŸ!råËìxé…éE#¦åýXÍíWøJ÷ïeŒo}|¤½ŽÆyO5û<—Þ‰dÖôš9%ìJØ”J§ŠI<O’·-Ñô7èO¥>Ó5oã<úkÞHÖ­èœØÿ#’åËÖÝ Ž‹ôÊ#é¹ã¢IEˆ[Bhîoñ„ôM'ßé ðô¾ZûÑsÁî!ïGa}º#¢¥Ë—é¿R.\ÓodŸÙæŽßOµT™÷W£aƒ!Ü|ž9#’û—/µ._bx Crz|V)ö@ùÉ‘ÉzÁbÄODÃô'ПA|bô§Ø¯} ‰ÝRþF"“O4’kæz ¼†<†ÏÆ¢eœšÍ~KÉ<’X±VI¥º%ìK$’I¢jÓ#§Éä¾ä²Yàð(ØpÙ@°ËoJ}5ë?u/ÛI>ºw÷M˜‹•4‹qòA±tÁjÁp@ùé„X„B"°@y{Õ³øyPúg¦z׸‹¶X„7ýù(TC.\žO=0ˆU!úp\¸ú`‚#’9¬ Ù{9&’I=RÉèóÐéý„üÛ¢k4B¢èŒ_ÙXÜ‹OþJi=KR+=1éX¢‹Öý¥ÃŠ?FO’I%B¡˜ç9Îs–°rQs“êsœ§)­'1ÎsÄ'¢ûŠž+àŽ f{ö!ì[îAÀCÙù!ìC؇±b[ö%\‡±°ö¥öèSµgމ¦Ô9ŽqtÄšo 1'‡†pN Å8gàR85Äáa§—ËÅ.^±éA×<“Ϧ‹ÞÞ¢}Ä;=ÿÆ]û’’I¬tÁ-ËîwRåö/·¡ÒшDsHäŽi'ýœ‘ÇVuå$¶Å¶é¹"û$òG'šJ%nZ²ö'ŠßcÁ÷Y[–cU¹zArÈ~ƒö÷rO]´ìvLî=„ÿ_±õä™LA(-ÚhÉ/²ÍzóÑ=ïg­áò^ A~»’é¶"–¢ b:/Y&G\VNê%ì&;¡!ÖÏÞ±¡t<à‘—±AÎûØ’gý–ÌKð ô‹„(¿)®uì,[ÊCäüŸ^¬J!xE'…Kî_zAPG$rG$ÁtÉA‡ƒÅt…ÑøTN‹û–³3¡8œÌ45ŽßÌpŽd^qlY(,$˜Õ=úÀ‹;êÖ–ø•êö;ó6Þ— º'šÏE'šÜ¹räðϲô’Ý-"äsÑ=Òÿ,’} ‚(ÎÆ—òqRåö'‚¤ðO¹%·è¾Åɰ—±ØO.zc¦†Ä '‘#K1úrÿÇ`‚d2+º|“FÇ/äÒËÑ/b^ÄñI&¶!t„KÈdrArûRH¢!—/°×m‹ìIàD;†}š­éråËõù§“ÉäóO5òy<žiäòy<–Üò‹ny<–ܶç“ÉmËn[rÛÅ·-¹mËn[rۖܶ具-¹mËn[rÛ’·-¹+rÛ–Ü•¹mÉ[’·!¹+rVäî$'qÈ'®…!4„„­¤­„ŒŒ¡(NÅRägA¶Òl!°àØA ŽÂ;ì#°†`9¡ âviw£¼ï;ÎáLpؤ¢„–>R ÒId½‰à…±ŽH{Ë—.K%ôX±bĪÊ&žKî\¹rÂÄßD6çbŒôú“D“I&“Iö«á0¯Ýï%Æ}´·è–\–‰ôåÀ’(… @“˜ñHlÇÉÅ'ƒ°žàŽ lËÇ“ºŽÃÅ<JÜ•HªÔ¾Ôž*6‰]/°ø Gr šÊÈ»6ÒÙŠO[Ø_ã¿»ÿÿæb^˜âr"v;ˆ™v%ÔË sÿáòW;‹Ò —$qhG¶e·¢ä2ûm¨„BäûeÉ{Ry#Å qÚŠ8e¶$––zy!2ID¢Q ’I$¢H!D D¢:tt„ I$’J$”J%‰D D¢Q(•D D’I @ID¢Q(”J%‰D¢Q(”J,J%‹,X±bŨ·H"Qh,(,w; ’øÐvÃ-©rC£»ÏÉ[bä–ä> ìx<!$ˆDr_£ÁOƒÅ._cÁ<ÁgFù.eLz&ÆäÛ×£ÏNâd‰“&L™2dÉ“&L™2~Ÿÿ>>:fL™"DÉ“&L™2DèN”‰$H‘"DÉ’$H‘"D·ëc˜æ9©sÔ9Žcš‡1Êrœ§)Îrœ§9Îrœ£ß$ÝØò†sL"Æ0#½ÊI÷òRMn^X±bÇš[bI-G—×äòy$š%ìvÁ< ½† #º_®‡ÖÄØkˆâ¥ÀqQUPrtQÈKz„·%¹-ÎCä9Cä%¹-ÎCä%¹ÈrÜä9Cä9B[’Ü–ä·9 nsÜä9Žc˜æ9dsÇ1ÌsÜäè œ‰LX’!I“ƒ2Oü£ùôðx'¿BäpE!,y<ÖQ+rz ŽHæQàž(±ç¥6»?À£ØOºž•‹,Ó&3Kßì.íw”&‚rXÁ8oõ|ŒG4¹rû)%è¹àñÐAkO¹r8­‹VÃ(¹äÐÍç:oüŽ©ý(ÛfÙu}yCö°GDAC¤E"C؆AC!CØ‚8 ‚ †GÉÉQ Ž lJ¤ÉA ‡±¤A2b –Ä=‰ö¢"[¶¡Ä$27/©Ù–;ƒWq°ú„[$.Gkˆ¹Ø¶¤¶I”åx» ?%bÔšO$ÒÔ’IT•TÄ“G’Y,¸–{ã?ãJ?JÑ`am ©$’M Ë1Lú‰&?JTcÛIbhÛn[—YD­ÉU’îÜM$†ç"!º%hHÜÜŽèäDòI1’;£™ÝzÖ;›Ì†ä7&‘ÜŽä7!¹%Þns#´s"há–s"<ÝQ#Uª!°à¦èƒ80d½0ò„Œ†ô‹j$b™ÈÚžI{Ñ/b^Ä’\ñÑ2ä=Îâ9$s±}ˆâ—.@‡±0oÂíËr÷¤Žæmi؈0ŠRI$—^:ßT¹ ãýE)½ÁeÓÚ¹}Ä" Lvì „J—æ9²‰Xf0Ò|ÑSðŽL¾ž°Fú×ö­°›Z’Y¿q993qöb{$bn 3Wêg÷cJ›ÄP+½I4‡KGÁOIÑÿBmaÁý¡w0$E’Þ×;3&%¸Kö-FÇC‘#–FQ˜r-Ž\ ¡w†Õ„äh7­)A{NLM]ÉÅ+ÜQá“MØØÈäü/À‚]ÅùDð%ÃXeÉI—¸€§-ŒACk’õÞhÉl‘b¨o"Î?9É$„Œ“ÝRdžÒ[…ž«>ÂÀ+‚³/;Øìò$8Ý­h6Þ|w˜‹ÿcŠE¼AghÜEω©Ú ÊÙ!©’HsªÏýWúäŸwbz Ä*P+ì?ÀK‰XÂhnäxÔì[õónî54dy× e­¢Grºµ¦ª)¦ø2ŒcÖbA–z ÈÓTB5ûlqLÑÚžŸÇÃjŽÂVÔI(•±;Q"i䞉èòO$½Yn]±¡[,£énì"ý‰+'Ëc*In„gúÕ?ù™Ìû?±¤Ûü™iqŽ™ô’{ –%–³Ë*£ªY"¦†ægq¸,ìX$dÙp’»X†ñšX†«&D˜j§¹Å{ ¥‰ÆW&]ù”HÚÆÙݽG÷ÍÏ&ˆÔKä‘F=Ó¸¸.ú –Åú#—2)Þ‹'a±1¸X[ö,»i.Òjuà±y‡ÀK6<bëÏÝœ}DzaþG^ºt®Ùy(‰°i$ò“‰å›àýì@‹¢A„ ô&¯B&Æ&w.cÒµË|ÆŒ¢8L¸&ÉÀÕ[“ñj #RÚ÷¸ð CѺšSªµÂ?$žå’åü'R ‡e¬$Y<7ÒäâÙXýñb! ‡Íw±õ¶Nø‡ék j$£H*#kt4¤Þ%³&^ÔGŠÈ.±d¢Ŷ!l‹l$ßaŒà·“¹Ù–òw;S½,Ìf–d´ÃR¾>Ä"D"!±ˆ¯‚ÛˆDtH[žF•‰jÏ¿ýY“‘ŒÆö èÏÏè¹aÆÃà4Ù‘°°Ê(¸ŠÖ:3è«víçýÕÂýn_#à/eÔA2K܉G6ÖŒ¯¼†þf4y¦—›Œ¼7afÂØ™¨fÞE\ }ÌC\j'‘ÚÆÃ99qÁ*¼"÷ ø´­ø;„æºhßßt6EDž  ^Ç0w†‚âQ ^ð,Ô«fü Þ´ ™.&p3I„Úá›ÜK$_73Ø›w'0nÐ8FK"% ´Æ–CÕä}7 í‡[¾sIk°¤ãö ’óJ7Ì$6ë!¥ŽEƲ‰ÅþcS‡ùÐÑ´ÿD­¸;‡1¤F®­?dgŽÆ>ªMÍÞ廨¹}ÊÏ#RO'#wH£˜!np’RôZ ÜØŒ‡¶BWGîã’.ðuýÇ+AfX¬Æè1;lÚ_°æjÚ_¡ì¬WÞF9MÌ=}ÐØ`óí ‰µùSÇÛX”d¹ØWÍ1S¤îv!_x¤Sî—.KZCrSצIäò_sÀž’öú-Y?µ0´Fªsl-Ä•†4÷d1©;ÆÛ“£eéGRn7CwRkfQ¹-/“aÅl†hXCÚ\ÜXÈ l×sDãe.i)™¸Ýå–£K7z²hä=Ä9$9Õh²¢I0ƒ, ÌZz'ˆ ²ÅYm‰´A‚OƒÁm‰¦C½'‚I0JgƒÅ;•¬DãóI¬Y:·9¦³ÿ¤§-ÕàÐÕFŒÐÛKTžÛã©á4x43}/Á’˜‹#Ã.Äm%G†,º^°dªñeSZM°*ìòI Z%Uƒµ*R·I>™ÙÿUX0•oLf°SŽÕF(Có±¼^‘B¥¡=1xäbG ìqLäÍ•`Y4-̱]îÙø åðD)FKàÁ’YŒ ôÁ‘’íR—v„rè—¢]$’y<žUIhói%,šñ£d%®ìWWw»qÚÎÂVŶⲄ#!ìgqÜwæÒy#£;ŽêÂP i1© Ù Î㸅D78#»;Îó½K ÓFÎñ=ôûä÷ÜLw1,.&-EÌ“‚"æLXƒ&4¦t†Ç‹øøÞãÀTI<–ܶè½D­‰D’%±Ø2O‚ÊÿІá\…äRe…ù%ìx%Ø…± ¨‚>Æøg|mÎÄSìR>Åø.\¹råÆhNWEË—«rõ›Å3H"Å&irû—ܺ5$Þ)rå÷.4È„Kܾå÷<žEt»<—Üòy&Cp†„‘ÉÓÈÜE#ɳÉ}Øå\™DÝHäòyPÄÒFu<ŒèÉZO,O#V¼]‘Ë1ÛK؈å‘Ëû&áÌhf†Ý>鸶tw/½‹hèòZ2w;;‹Ǝ˰jiriâžK‹‰æŸD rk–/i~t&Û¸ÝÙ2øý†û—ÝŠwdÓÂ<*v‰«rɧ&»uªg¯-é’Ë«6hêw8 ’‰¦ šé0M%K±+zäúçú˜˜*CQ)©ÁiÃÃŒÓ!› `CMe5¶†ur‚˜•4f#þÆ…/íÖÆû™§TŽ]6-Uù!JÓÿi@¥ŒÑå„–ŸOà1“I<DI/Ñ“‚è‰ñ,îyøØG‚(–ç•<‘ɲA£Èó¢Á¦$X󨽽Ï4ŽNá² ‚‘ `9Eö$Ä}‹×vF9xù Ümâ=ûM¾KŽ«ÚF‚¢º<ž`]“~Qá‰Ö‹Í­æÌ˜«W‘  òŸéMz NEšY<ÓbU4*cM–Œdj²m•w^‡†, :LDQ(IÛ£pÛm·—MUL†œ¦hBCà¸ûBáU‰‚’}óå1¤9o Ñè`ê<7dýŸÊâ˜Wi#V2°±t·LŽ’U]“jaÑ™vP_Bï¦T,`¤à;§ÍVLZæ‹£ YÜÒ7w3Lª£ äbÁä=Ç,’¾Ša(¹Üì[Z>¢‹<éc£ã'ã<Z¹'qäžIäÎÔŽH{žKÕ<’ x;é ²6~oÙ¾O5“´í©<Í%ºëÇN1Flœ!CÂØÓo®¿ZRÜt^ JÑ|@¹Qä»ÑâšÑÖ9ÔDÉ ûå·Ë¦¡ÖxÁ»!²xˆO¥M|S $‘·Þ o3ØèñÒ}±:trU^"fýÔÄÁG¦§öIÇ54w ]XŸ)ÑàÐÿJ&ÓMe‚«Q£½eM€“¢ZVmî–½æÇRÅ-]Xïò{ÑQÙz“‡.к´2KG(É&NÁ)29DI/óLr7.AØ—º—ä—¹,ž ïGñbe:Å‹Éb’v»(ðxöO,žä’Xp\}É2Q¤ÇÕsé:½‹ÑI¼ºÖå£tZôÅŸ%°â~¨‡F.†èš—Ld´.Ä,ùê…Éf<>šÉbÛ–<˜Þ³Š)Jd­Ñ+t[tJÝÀÆæà ܋n‰[¢VèPõ_b‹TäÈ4²NÁ;‘;ì Çc•§z)‚v‰¦’W$`Ìa<–ÝJ! Ò؈%´}–ÿÐβårÛ¯±/ìD¦Ü2Øb,:mp…ÉÙ–f2kbÐ.N̵;ÖЀ[ð?‹òþÈ{²î/¹‘à\mlwC:÷Dp!ŽE'pŒ|^%ù<ä¹ ŽÅºàŽEܽ˜°^ŽIè Ê£'¼»Z›RêüÈFá €jQÌrt’…—4dÊtÜÑoú8¥Ù¾ ’E÷ sún¥’…LôÜ6Ñ…¹Œ²+VûÑ!ìdwdw2=]•wfûd7#¸£»”(£qgY;Žâˆ¨!¹±B;‰7¤'•BÝj<,Q¸[2p fÄ䄆J9q9_Ø‹YMÓY‹0Q‘¨Ë¸ÀÍBÀóf”|ðÉL½'a[ÖÐÞM#`¨åÄÆá¶hŽM1~h¢]¨Ú쓺GSzJéS¿µðßa"ÓWvyT˜£Viž«[WV$C.goæü’í1E†`6ôi{:fbŒ“£F “hKS&‰“Ms¯Ð?‰žà¶Ä¯â!³$\ì%ìI$–,A}ŽÌ±Ï' ð.’I$–#’û’2nG´°Šæ¹èÅ1D;#SçÓ½W »OCûEÉñÕtQeÔ–{Qb—ؾÕúuEÆcKÊÔ¹¦­= Y‹¥«7GµWêˆäÈ]J£6Ê‘ˆKm¶ûÒ,ÞÆ„ÂÒÔfá…6RE1¸¶{HÈ¥EÞí|] ±'Ôhdäõ¤Qω d\‹—Xb¾K‘©œŽV¦„½G+R_Qðg$[µú3ðÉ Îäx/±,KØ–yQ%Ëð>Á¸M½ìrF½’5Ãp“Td¹¥Éà|¤„!–Ȥ–Ͳën·Ö(!¢%SØ“‘éѶkú$‹ÀÎP"MD”p‹ê¢+' Æp)P+ Jb°M´C†Äð$­ ¢‹cx; UŒ&;¢ ‘"h)Ù­I®ên8¤ uÛwcD@«o¾'e!×EÍy#–\²$rÜœ³–sD̓A9ãó9'<çáÊíËcR7™gÙÌ9Ÿg7ìÕ´w§q±ùŸ³øbM2V81”FÅŒd¬¸ïƒ w6Ù]æâ!âU\šy£\÷!î9Ü—¹7â>Ńñ葸«Jžò7äî©ÁO³¸¿¡Q¢¬GN(†9`¦¨¶ÇabÔ#(’Û«<2xdðÉá“÷’X–%‰bxN“Á/byö%ìNê·Ø¾Ä½‰{ØN“愽‰bvö£iö'a!;Á] MŠäìDìGb;;¤6–DŸ¡Éàv"üàvpËq AÔÏÄñ<IÃÈÜdMr„½ñ Ø™¹l¢YÞ¾Žôw£½èï_GxïGܾŽï¡Ýô;‡wТKPddº"ÒL—BR_ÕÑ’êÄ9.ˆ£ÇÇàmÈäðÀñ!Rb; ìw¢Ü‘©¨Ð”näÈMxGRôeˆnegU“eìg§>ƒ”t]o ÀomŒ”ûËŠà,!eÕ„f·V¢x]Y ^§€ìŸY‹2ê`Ÿ#N‰[C?@lTL$6ÞRÆ«Ên–»ÓðtèÙªè^Î÷3WyÊÜ®“†Ñ?6Gu] ‹=jI,|ÑG“¹Ø´QðÎôµ ]ÈhËj~¬?R}äÁbhFö˜lBÚ·$K?‹Eí[¤M%P’Ü“|¢ÌdÆÂRIùm±u>–*áÙ+~¹:â•á+šTÕGpÐŒ‘˜YW“bTâ ÷ kÙZÿјUÎØ†*êSNSU*„Œ^Í…±~c6Ãí^¦{< ȵšT÷ªB3UTÅÕšpÆCa–6£zgJ¡Á'Ru(iþÆÜË÷SH‰îbÌ4Í´EÆÌÖw›˜T»Ø; ¶XÛ|º=ƒB·JÊNé\‚Y«ôÙm†d_î9sVu7ÕÜO¨ü ø‹ô1¡—‚t#RdÁN`Ë&,@îOžíñ0Ú•]$­‰ØN§DŽ“ÎÏ«””Bd­Éæ†É[´‘ÀGamèEè!ÁR}LŠÆ zìm“Á,V+Rí ÊD]:k‚.ñ¿CB—Vòºm6ÕX½ŠN7óöíùö«&ÇTœBàs„g <3ãH¶Äö"óYPZÑV&bi¦™Q“Y„ÿ°¶«Xðb¥ùÖÙ—aŒ–÷³\ úa^¦íçîèXŽ$½Â ÷‚õ"\.»EÙ'ÏRSKµ[o¼›ÄWQ%Ç·Ô†Þ ”•‚ÿ_Ñ»ÖgŸÕâh°‹-±4d–bâ»%«búŽV¤¾)—qØÍ)Ä^¤P…nFNÆNÆOlàÄÙ2Ûäí™e…ž¾Ä¶¥„‘à"ذàˆGî¬]+¡Õ§¡u+àåý C‹×kº'jð>oI”èÂ1¬e±B—0‰Ùt¦Ê˜ ¯ƒ™ ¶å¹tÎu–â^1U–Ì TM¬2ï5Äœ®ýXyô#ǰø0­£´–­DC”-……L„Q »™º Aö¢ÐjZ£ T\àÜ]V®šX{'§§N9®XÎŨàP²v-GQrAhøoŸ±ü<§KDÒû´KSØ_aÌŽñ±ÛÊ = èx%DÕ¦CܾæS}:¤ï×JX’š×_gŽ”¥w4nh‡n¦®£p—~¨Å%upö¢5jJÜ•¹mÉ[–Ý Æ®ÃÃ#+œˆÈäG"Û ÐxòDúœèáCp&‘½»“ŒqŽÄ8Ã4‰µè’#„4—7Ë8D¬Pšjؼ ÛA&áœC€p¯Ï‚wͲֶ šÒpN$ðšXÉ)ÇKìñÎ0œá)Â&Ð~Ð8Çá×1Ù3 ìc“,ì ™0gBbÐF¤Î„Œ“¡¤Žä#Oêø$ò(< Ø‹l«,—¹æ%§,kA –âB"Û±FìòKÜ–Ka¶Åö¢Ã'! ç.Ï"éÁšÉƒ=/ÉUI{¨WÌ’WÙh5(„ä9Nc”ZŽBÝî*”ni ‰î&;Ä÷ªº[Ä·;ŽLMl†çt…¯%¡*N@z£Ðp ]Ua¸…•%Fw–pŒaûºHÂ6Qœ#7 >(£GÄqœbSšðœ=à)”TøÎ6pœ,ág8¨qtÀp pT&àL互ø;3¹tAr"èEÑ„™NPµ˜jGð×ÓÁâ°\¾ç’*bw£òY ¶8 ±/a±àÏ›mwÓƒ5TÏCÆ4ZIÞ'‚x'‚x'‚3²öbûR^º‰ɸ©$’nûšÏÐtdcJMŽFä˜"IQ.8b±Or[ùþÅÁ4Mn^’÷<Ó½’ݑ܂a§±àÅ©j!¸Ÿ&ðD}‹¡cÐÁš@‹»= †‚Bê‰ µU_d#Oš™ZÌbø_Þ¬»ÆaÏK÷¥'z:ëæ”.E æÚM'g•½½…Öê“K¢5&K¡\—‚ ²èW.5°íؘši5†?‡PXž)â—£Lh4šÂAGIàžæIråÇØ±‘à…ÖŒ˜¢ëF ›ž½f¤g$‰QÛ³•~›ÝÅRP:¦±ÛªgÐ…‚/¾kHB‹4!–¾Ô;G„`¨Ë5Ãü‘m™P«­ÓX_ –‘‘jëàRÖ5ÏÛ^Æq³É n4G~€¤±Dw B†Èï—•é:8£ Ñk}‰àìd·ÜO· ¦eî;Ggê»YÿÁ0œ<¡åIm\!O ͍ÿ°ßö ¦¢åFŒäòd¸ÖÌG’NÌ\¸ÑÍûÙ$ž© Ð¶Ä [–Þˆän!÷&ÃÏŽñ"8‡a;žI%’Caì%º§']9è~ƒÝBØLÐNXýˆ›Í[tdÆIUò' vXhûnÊ¥Q§Ú¯P¨—s[R„^È7&ç&æžõ‘(á@Zw_^¸;ü7ãûcœ—÷I—7r6_ߟØ_’ä !›{»Ò≑wN D¤4¯Üä‰m÷?·’ÕaÓ-g¡Û˜Ì6]Ùw$r†÷„ Ò¦h•½, •m¤”½Í¢ƒÁ':é¡ô.-G Á–&–H1XwÁmH,ÌäÕ>G} ¦"æð¥÷PÃl—w"RIؾärAB ‚@ƒ€U»‹Ñ]8«Ü]l1DO\HCVõ<„¡'VAa|=ü·º cÊÇç¡Fàζ þTÛ“åLp÷,I[-ÇËphZduÍÁ¹ðr­ÀûB¤x_S°ÔǼDÅ,Úu¹.%`@ øÉ1Öõ « ´ƒ7eåG  ÖkfpôfÔ©”!ðsi³NÌus$ºN$]§NkÂjh=‡{Ñ“™0gC“&Y0%#v‹›B®óí¤šÍcªI¤=ÆÅš l@š*1©(<,ä,§•HàIA}©wEìx-±kjçê“\tM3LÕRåôÄ!uZdצKS–Lš:³±;ᆮl™ŒüÁ4ðéÄÌ!¥Kf'*ÕRQÙ¨xi¨?‘ÿGl!‹(I`a€e·E·<–ܶå‘uwÀðÌ!mÑåµRÛ¢Û¡ÒÔnp‰"— –Õ}–Ý}È‚v ÿÔ8’NF93’éŠåÈØ¹t%%ðE‹²á\r5c{">¬?c$ôI4Ž™'¦IàHKZéˆGp¾ä½É böØŸZ‰1'±~Oº.šõ ˆ®Å\èÁ“^˜$d¤v;Ó°ì;í¬¶’e)1ÛªL”¾DŒ²ÀqœDÇ¡Ròt|}àMa]g  BOyÞwçqˆÜFïnm½´ÚÛÿ¦ÕÅj;‰îE%s"482[Žã¼î2 Mè;èï'¹Ü6Ëd1”+Ù­1ÜJ!“«ŠFy1È©w tFÇ'K5GÀ‡j ‘¢ÁŸ«$õI4ŠÉ$úH¯‚8! &wq†û”~`Ÿ ¹}ÅÇKn4 ·#’[xH¥ÝåÕô¾—b£†pØà!°µHlBAApE ‚8 ¹‚Äp<ÙC!ö%±j"‚($@‘"D‰P™*¤Þ„ÉR)“&M“TÌ™2kRdëÊ™îwÔ–çy5©q=êÏrâ[—êwœ¹Æ9;Ç:x1S‹T¾B—[t:j@†¨Ë3Z²MiŠMpf³K3I¤˜2`É'$Étd˜Ð‰$r®L—FIg$Étd–©,Å˲éŠä²I2`‰?ðL~´Ö=”©Ë‹î\’Y$òy‡åë°¶#`¶£kbVÄñHàšØ¤–Ù¼ºàÉ‚+“k#¦ ˜dAšfª®ÆMLÖO,tÁšE ¯ÑjJ%‰D¢QD¢Q(’Q$¢QQ+ I(”M@T MIDŒšI„¬fIƒ&),kZ]1I¦K£%È$ºÁ’èÊ.Av; ù.ˆ±,fL IsBg#àW.­$IqªF-±H ‚+„tÇ¡7t…‡cà’hoš29%¹/2‡ûIØ#e[Œs½6ܽ/tW ÏF ˜fiuÏCµsÐÕr©¥[éµPÉ0 ½ˆÞOæÃ8ŸG éœ>¢I?Ì©¹ÞßD“ýÒ‘ÿÄþ¤þ§¯~ßwù •üä|ïýâ?¼G÷‡?ÙÛíÿ‡7ØþGÿ”äÿÁü¿ø?•ÿàÕ‘ð˰CZ¤RèÉuÑ’ê—¤ïD\ÍÔ–D`W0dò1fŠ–j*5aQÉhªæŠ)h.„ÊtÖÁØv‡aØv” #±ÅY8Î3„á8zªõ½XAtðNÇ/±,–gR7÷«$71„û4~@¶)wÇÐÚÔOq’õ#ô'½bôíÐèÅEKiÒºg©Mµ0önlù}ý†=äê$dÀЈ41X¸ˆ2`ɨІ…u\QA¢–5"⤨½3Š))˜3‰´6Œ°¬Ah¢ Ž˜ ‚)tÇT2 Š_¦+‚{²[QÜsÍT¼wŠ<‰óIà‘±â‰hI›ã‘U{kh£1E0gÒTšdÁšæÞ>5àÖ©“ULÐT°ËeSQÑaY™$‚Æ ˜,ÉÒ’cA¹f “j7& “0Å“G°íM Øv l;HÚFÒ6¤l;HØ4º§aØv“´‡a-‰lKØì%±-‰lKj³Á(”IŽúQDñHE"½Åú]1ñ—s¸ú¤¢Û,IÖÁ Ct;Õuã«5“4ƒ4Ƀ&)5Á“ È­EL˜2b¸¾ÿjù­Œ5,ëfaôM&’`³ŠM&Lƒ$Òf f)'$Étd–„KؾFäº2KTšMéšMI$C€„I ‹–*Úˆà…±ÁA¥± b[ lØ\‚ûÄ™SÁ:WE¢Ž¯J:1­éßãÚïÈÖ"dÁ].™Í3]hˆSJ3 Š ´RÐ`p+…•p`yDŠ8p`q6ƒ4»À›¡&É¢Ô‚d—d¡â-ÉQ„pbFÄ« 7{*V 4‚H¼AD#ì¸É¢“_±¦1}…üQ€× ®ÉH™jFžáó—ˆû"ÂsLÕŒTW¦(ÅÑ4Ҍס>‹QÑÜTž‰šÚWÇàî`0DÒ5Å0XDƒ& ˜èDŠY˜!1Q5IP`m3 pØœT•Gf'I±É)˜Ç)‘عQDLÅ4Y© èQ¸ØC°î‹D É-ÈÑ5r% ”2b nB8¢^ÄðJ¢ÝÝPÖ9dñO$ˆ/Ð"¹­mxÅÕ¥†.G‘z*ÔM¬˜&™¦EI¤˜3LÑ+øøüÃXf‘Hìdv¦aY˜ˆ)¡ŠjY‹4NÔ”`³0Y±8"I¤ªdÁ“šM¨Ü³КM¨ÞlŽI¡ÉË%5cAœº5‚ÙÉHÛL‘2çFÄ`È¡bfD2I$’QbÄÕ#hŒàÔïEĤN´nØ':mÇÕCÅEEtbŠ":YÜTN“jL˜2`Ƀ$'Ñ“LQ'3|zýU`Á³!x ¦L2$D ¢:0Ë–b¤ÒLSIƒ$Ú’`™0ðdN)4ž ¢eŠì%àƒà\‰Üv&«XC3$Œ!Ù&æE‹5$‘-D X‘LG –èŠ.\ïR‹v¾º&JŠñbX†!ŠŠšQˆb¢¤Òz3Ó4“L0dÁ’H$j ɆdÁš&JmÂÓãóK®ia C&Ã,a–Œ‰ÒÃ&’jZEc"p©6£eÑ‘Y™&,hO’èÉq’Y”I&K£& …ØÐŒa%obð"YfMj]dMˆC®r4 Y ªkZXjX„¹ÅQ%ÉcdŸÀÌGüDAÜ>ô‚ÈÕÍX¨ºp:;ˆÈ©4šâ˜®EDÈ&¸2`Ƀ5’ 0L‘i“ ÉŠ~ïx5.F!–5¦‘2m\2ÌÃ,ÅbÂtšM$Á “Iµ$Á“IkJIÉ29FY\—‚ t™˜Î KCºLSQ§"ÈÞ‚ê7µh¹ç‚R Õ•„ž…ÅÝ‘ÈB¢ ®ÔËrý Q*‚BLxÅ1Ko ô´RÔtw$‰b}M 0Çsjˆ&“&)ƒ4É‚$’ £2;0dÁH£Êø÷ƒš„¢ QDé5Àܘ2L¬ILÓ4ɆD˜u‚H.D0dv2`F¤«TmrÉŸÅØX¥˜¬dVÉ4“˜3Yƒ&IÉ$`WŒ¢ðe ®\W.f‘GGf"ÔHàҌ̑Û%‡ràÃ&è5á½DùÉX¡…ÒÉ‘ÙXÈÃ&†àÈË'RI$’I¢Ä¢Ã! ü?(î+>h¤»6ÔT“† ˜2`Ƀ"d I‘L0f™0D’A,ŠA4ÈíFdu]ZQј.ÿ#mš™“P†Hë&´Áše2^)5†©tdº2‹‘%È%‘F#UräJ£[hÔy¸ \ÂiÈ¥ØÁàsÐS#¶¤\”%½N춆¹¥744C,e„mCPZ(w鎈D A°îXŽHÊ`Ƀ$Å‘:'DÈ$v&Gc& ˜2`»1I LŠ5j2dhÈìdb¹_¢)Š:;S.ï‘ÍùƤ£Z+0;’d‘îHÉ5&’`Ƀ$³JË9¤C2aŠåÑ’è‰.E5©Q)¤ZŽRÀâ.<¤‰¸Ò‡2e 2'hj&ZHn…„jH&›²Ç>Ä5 ”ñDI%Ë“Í%£C¥Ã±Ñnì"j&’E©ƒ& ˜2a™&’HÔ’: Èìdv2jdÁ“ŒÒ)£ThCÇa\f~DÁÜÄ“RMI“RÌÁ“IŠI—É4’"¹1\Ñ­K^H½"] M"Qr-FH2¨Õ/‰M ‘°’àq.Ý„ÔQ¡,Y#q’ª(£’2] ͆X„@‡Ož‡M¾T\¹ iÍÈjè$Å0L˜2`Ƀ&š]Ò ‚ ‘’3#±‘ØÉ†dvfh”˜3zE"ÔjÔv£21 Wªý>G(ö šM&™‰“Qƒ$ÒHèÔEЮ`Éte"ÅÆ‹úËŠô‰T‰TŠ5F**e L|™ \K·FÓ°¬ñKnHû‘#6A‚‰åÂBärY‘Ñ=A ¹,—°å§šà®ëܪÜÓ4w¦Iƒ4É$I Lv%±¨£2;0dfI\Ô‰è‚H¤QªbŒW®;yÅ—É&²BFHòM$V&LŒ˜2LRYœ“\.…t].D¢äSš4"!ˆQ^™TjÔj Ó.¼Þ$±a™ ¹”‰V—3“oArL+:ê&Œ 5¡¢©.\¹$Òi$ÕÓâ…….|ô;iDÇ{Šˆä’È!‰ ÒGbL0dv3L˜3L“‚k£QF­Gƒ#Ça\vb¸ò%&¢UÉüŽs‰ÔÉ#W$Ô™0dÁ’èɃ$ÅZ&¸$t+¢æU"Qª4"!‰¥ziM+2„ö¤ÅØñ‘§¸¥!£A©¡_A–…¸ò%{˜P@Ù$o™"aè·]Éì#X´È¦7 Çš±AÒH&’`™1L0L˜2`Ƀ$ÁšË ’ &ȤQ«Q£#2;1dvÍEz# JE’ÔŠbûüŽ ÛGŠ:> &¤Òi%Ñ2+ å̘2©šE —$R&z+—3Hµ"Qª@²E0+Ó#/ 1¨Ò‚D²K/"&‰"c0^]dKZ£xdA{!eÊ%7Fd.åËÓÍ%’O4š3Xòy2Hú¡.E›{ ÐV˜ÌSL& “ É0@™‘©4’ " #PLQØÈìdv2;˜Œ1)5&°OÉŒüª!àLfM©#É&´EɹŠ`žœ¤ZŒ“’Yˆ5"`TEÌÒ%R-F¨èÔQª4…1]ŽÒ„íiX´Xi¤‘%ƒ’‰,p$cR`L —‘fĽŒ²¡Õ†¹%޺ñ6ep…H£RtN’@$2 ’ ‘˜Tj ŽÆL0dv2`ɆDÑ*%K¨•"šR-KA¹‚ù>‹ ˆw#¹’2i$jMÆIÉ–b˜é’â¹teâ— ¸Ñ~„@Œ1^‹ʤZ‘H£EÌ[ì¥3±†ihA\“(m "U„4U¸›–w%Ô\˜CTÅ"†»UÕ`ž‚–„Æ#$Í]5$ä•I¤ÒH$Á& “L0dÁ“Gc& ˜3LÒ&‘4‚`Ji‹ I$‹þU”\êH,pÛÌsE4Ò]ðÍ4ð䎌"ÐAôT4ÖªaÏ6ë=ò®€CC9‘ AOãÇ<ÒL4CÏ}­½Û¿˜ECtkk‹ûǸÿÃÆ’ERຎuó¬wÅ…u&Çóȶ€#óUÑIW]Iú⯼,çÎ%„4 ÀÇ0bÇqc E<œË,ÚH5õç 4S¨²Á€  ‡U÷ßqÆÒ}Îg<B&SÊ@ Ñ÷Š AI4€83Ä0gÓìØM²4ÓÏËzi˜-Q´Ó]Ì9ã=òõã,¿¯;ê–в°áã¨(DûÌ]ÆXA”ó¸±È<Û¯y’à ¯~¸ä0ÐÀ4ÂÁà$ËÏ À‡0$žËˆ#©ËÿËqÖ–qÌrPRŠY<±Íè'ŠÌr1Ͼ{`qÌqÒÃÏ]xuPÇÓŒ “-xÌ€[,°ûgd‚(¨ Æ q€ BÀ,ܳ3Í}ÞuA{¨š8~œaÇÿÿÇl8à 㰀Vî½LsÞwIÓyœãÏð'[åŒû]¤»ó¶Šë0Ã^0†™ÔŽK⊂B°ó^Ï2öðQ¯­»Iñß™øq¾l¯O8Çv÷ûA¬tà SRÇÚ,¯SþJ|©—å„Â?@‰?Eª(¬ÃWÙÊz1ë)¾(AàRL2D®pÓÝO󠎰ۙ†¡Ï?Ë^¿Ë”–ã<á¥ì>ðã‚Û©lyû‚ €e“t$’”COÆðÍ[ ™÷Ê/ïŒðA=å²* ‚Y.‚€HŠP2 174ÖY”†ú®û.ý¯æ\þÿý´”‚þò‡6Ú߯€ |ÿÛ„$/j" ÁÒ¼ô—{Ä>LïI.²ë횉gŸ,¾( ‚ ïØÌÓq„2ϯ_®*"Ç=¹l/E÷ÊÔqÍÿßþ>]¿á2Á|Õ'G13¤³y0I$ ú#Oº«ï–¡í¦/¾óûêš,¢ÀŒ  CÏAÓSMÑÆ[¦U´Pó41×ßmFSm¯Û.‘þŒsÃ8·‹‘ ž±šØójaïíþ¸GŒ}Κjžê9ë øÛ4²¹â d(Ã(3ÓMC=øn2‹v]QdĉTÓqö]Dß?n°í&vê+†Gpƒ'8¥îºË„:@ÃÏÙYžc¾/çÉf,2’_¼ñ‚+-ž»âˆ“̧ß<Ëó÷ÜYÈã!†Y8ÐÁ «LÕy¶•_ÿ¿Ãœxëé ±Õ«µú®v+àà/_·šD\¸.ºõôÈ,BEñÏçû¿ñŠg¾Ñ HÎãaþÛu'a5ßd@+ '`ÓCU·{Øf<ó\8ÜÛœóžjü˜Ó/ôQù2мüúAw-Íý·×`TM"Š#Å)»´ãÿºÚà’û¨€`Ì r}÷¼Ü±ç®†SiO8úçÎáîâQ²È\pú°Ó»ªsÊ2*A¼$>ýR£˜‘þžñU¦V¶û#Ô`Û8@õÇÿÌqú¿ï¾(ãé3O 2… Üñ(˜Ã‘U‘jº8« )§–:$¾(ã(œkfÛo,JüìÓMSaÖþb 3Ã\“Q ÷ïÚŠa¯ÿÖ)$´¤—½ÿ»î¸î’Ë+¼óË<ò€0ðgŠ^ߪ!ÃyåzÊ#½/ꓨh‚{羊^D*¶@Á:óÄY-}ü¨áÔöÇäó ú²ïj‘¬pÃL1â;kžøF<Öœ aáüðB¤'yà†,r?K°óÿ¨¾Ûï”â ¬kŽóOq—„rÑjMt .p´[Ý‘ûsÓ >ß®(¨^ïÿº8hÃŒ4õM7¿|+î«sÏgŸ¡˜ªÅšK÷t󡲨a‹cЬSÌiéšÉj³¥ŸÎ™ã·ðÏ1Ï*!ŠI$¿b±ÃÏ0„À0 O<#AâLpîò÷Dý7æž³ÓŒÕiÇA½7(ïMñW„ƒ¢Œ0Šž,¸Xï8ðÇ,S)’Ècœâ‹Ž –ÚßMÝ9ßûÃx˯0Ã<ñî1ÀàO4ðó{=Ä¢!‚û ê€sMˆ”5´"›ÌxÝôRPÈñÿk$¢ñ튚n<ãí®Éâj¬ŸÜrH»6ɧ0Ã}{×÷ëN&_þÿÿÿŒ0ÃK 1C8€àÀü òƒ‚ ì®{íÓó¡Ã2›n°Ã~ïQûÞ<ëÎĆ)n¦ÚÈú€#ƒ]Ûqå½Ç¬Ãm5ã_½ãÿs§Ï8×B,à Ç@1Ï<ãL öæ ‚#¾Þ2ì-òãè¿Ë ¾ÛåüÏ 0óY]‹k¶€‘<Úé’™µÍÙkTÿpÏ}{Crú ñÿÿŽ4ÃÏ<0ÛM³ÊŒðL êÏ#Vu›ÃÁ9öQÿûŸ ×N7MwçÛçLÚcß8ÚÛo,±ÃŠ[㎠ÏQÇ“wÿG?°×ý7ï»æe|÷Þ1 à 0ÓË00¿áЃêßÙ•=ëWü¢Rš÷ý7æÓI Ž¢[¨ {&ÑÌwÇØãŽ:)º»ï‚&Ûn?aŽ8íŽ{Ç4séÔóû Uï=ÿóÏ ÃŽ8Ã$ Œ$CÛM$ (Ë “åûMvág‚c¿üêøÊuY,Û<Ó%W]Ô}Ȭ‚ˆ*I©ŠØ¢ƒ]ìãíVÇ´4ï½øDúÏ#½¢a sßß<°Ã , €4Œ0Ûóèçº #¾)&±ôÏÅS 3ÃÃ>+kÇœG(ZyeqôÏþî‚Ë ¢ 2*0Å%÷[­ÒÓ®¼mó 4ãÙÞÆ<Çã³ÏœhóÎOÁ8ОÉo…:®žßÛÍ´]M'_I5ÐqFÓn3]ßAÔZóìpÛýþû¬6óï´ç,?÷ìqÓûî’mŒ4ûÿÿÿÿí÷ÛM’Bü§lØ“@W(Ýg¬’¿ªŽÎ‚=vq}åØm<¼óÐÇqôß8ÂÂN8Òs 2ËwÃO?þ«ì–|þôC¶íiÌg²{£Ï4ÓIG\uó€ßŒþÏñÎA‡4³ `KbŠ›ìÙÍä<úçÉ…AfPaSqÆMwÞAÆÖawEVÖq„ÔMÐsDq×qÆ›Æ ÎŠ¿-³¾{í´ÜuÇß}Öóÿ™¨ßÍÜM0Â$ÃÃ0è 3žïˆýðñÎM<}4rÏ&ßI”IÇ}æòó2Ïÿóüðß¼1Ë=Ë<×þ÷/9¿&QÉ¿Ûc±umú˜Àa€»“ÕöóO?G6ºÃUüùÔÕuÅo 1Óõg<Ñ@’36ÖÑô_{®Ùhy°Š)ÿ¾²ÃM7ÜAÇsß¼óÞ°Ñùÿg9a§Ï‹>°%D†h0pdGO°MŽzã®m·”]Æ”QTœeF7ËíþËxË7ÞøÅ“a42ËÞ½$@—[?¬Ï<óÉß}÷ßu¬<çóþ?œ_÷wQ¢m†ÿÎÞ#Á!Ý(¿ñĶYß|;ñ”¸Ë{ïÿpí,>Ç}ýߟv=´Zi„I×ym§Q`±Ì.zëdSAT_=¼÷¼üÓÏjÿŸ]Ç…Ùñœ­·°kB7÷ÿÉî¸÷œSÒkóËŒÿm 4Ç~÷}¾=˼gC½úó¬YåRE‘Ò°`’@I…˜e,4ó üÓ‹(…þ³×Ž=g 8ÿ¯ù͇óý7We?P_ÿûqÖ×mAܺQÇÄ;ÝóÖ[1Â0äºË#ö¨#ÓKè1—þö‚ý©ôÙMÁÔÐ Yõ7¬öƒ^—ÃO×Ü÷ÛÿñëoýÓŸ·û^6ËìÖUL:ïïAÌó~¡“á°Žó«JA40½›o=ËFiSDshä±£tßù…RIB.Åé„™{¾¹ïm±Ç…QõÕ—R×*Ãý“ëD5q^3w¼ÿÓWÚùnñ‚ 7é9ê’ûdºÃË cÓ 9ód0ßÍëŽC°KX÷ïî7 ŽˆBCþ¤ÓY°ÿ?}ïO?ÿ~waººHÞy5=÷žµí<þÇŒ÷×ß<Á5ÓqÅiŸn8ûë†è¯óï=÷dY·žsŒúË …²F …•²Íí<~ÛÏͦB5mdIÖ~ñÇ|í5g=2ÿÿ¸ÃÝ8ÏL4óß2qÇÞUnïÏïn–œC_·¦Jé£ïퟷÞq÷Ãÿ3×=ïë¬Óßi¤`¬sLç¨ øà”Ãìñ0O¦{ÏüpË,<ãûûÌ=ß;ÇüÔüüÿߟG‚ë\3Ç~0Ë(ùÿßÿeM Ú ‰˜0€áŽ!açg’s 0sOžó\Öûošzë<8TH0#ÓKE$@ÁÀ ˆ²Ê`ûã²baˆ‘—KnŸ½%¾ ùë /‘¾¹ïÜÇO*šè 'ÖÒ„K¢)æ‰Íûzš)†A‡@’ê¥ ðpŽ c„<²… )Œ²ŠJFÝõ8ðΆ2%ºNü$sN¾R·ÇN6–T¿æˆÐ÷L£ºú&¾æŸÙÆ‘àY>QF—ó^0oï ËýÚi®„¨AÓ׎4Ó_½Ã~;÷O÷ó¬óMoðößÉ èÁŽ»&*ÝýžXùëÎ<ÞH©ê»ÒGwþʬÎàÈ¥¯¨tA¿à0²–x¿ìuŽù.ªËY÷óÿõÃE~ó¬SŽ4ÅôrãO<ée¶$÷ü|¶n¿80ÂÉlÌxÜQBÂ8Ó…¬ˆäºûïˆì±ËÍ*µè”±DweÌ–Œûߊ÷¿ÿÇÚì¿þ½}¥ÓPAäÅäÓ7TG¯; Õ&#žk.–ø¿žèi—šºÖ,u2¥R´„,áÛ?ö¸ÒŠ4L6ýwææ2F8áV_¿u÷üþûnýõMñï–{äâ¿´ÕAMôQWßG6Xx9`º˜í’ùt²É&§+/›f›{ñK”L°ÄqÅœ9 ‡–í•¡Ï‘M‚ ’éïëÔ_Öû¿zÁA¡Ë~¹ÿÀmè°±ÜMǘÁçßÓÚß*»Â 2ʨfžÃ¥¾è«êf‚žrÜ;Gó°æœá›y×M” ‚NZí%©§šYé×~uO9¾³ï×óoÿÎ]öóÝ”ÐI_}-¼î8%’ÿùþ î®Jo¾é.šÛ*Ãçþs<ɦ»)¦*ž AyB:9Rg<â(Ûâ Lsqg4ï¯]?=÷÷ý/$#o?|w—ý$Æ[î·ß0"éwßú&ž+‚¢ÈûVˆ) `Ŧ‘ ÷Í÷×Û »ÿë¯õûý¶ã=zÎ/è0ð«Êoü1ÿˬ›\wÿÿúÿü°ö¬ðò çžov¾Šó ¾»­–¯ôEM~Ç[<&(GI4Ï=¿ÿe÷Òwþsó,öï<ùÊd¾Z̪, ’Ú ûj¶ûà‚†öï}ñÃOyëý»ÜœõA#ýí’ÿ Õ[mzsK$³iËJ¢ÓÀ˜Ôµèæ‚É-<Õ¿‹ôÿû}½çÞxÛ÷íÓÇ‚ú"†¹ ‚å¼Í¿®øøãLxþp×­?ëzY&Žz9ÆÈ ÅxÛ]8úM³ï?K,SÃ"쪱½1²š ©ÐSÏ.÷ÿ¼ðÇŸ?ã½øÓŒDžë%‡¬ Žú!ÿË'þàO¾ú‘ÿ­3Ú<Ïý»Û/³¾ˆé¶M¼ëz¨ÌûWÈ~i8ç¼<_‚ Œ „8öŒóÇûî¿Í¾éç9Óï»Ë_¾Ï]÷<óãÿ̰’ËïÿK<®{â¶úà«ue×ýÿ[¼â¾K ×”4ïdv‚[¢SòÇ?]ÇÍóÒ0K/ÇërtÃèæ×'‚ðGû§þ¿þçN³ãñê4ÿøl٥ʩȂI<ö(lº A—Óoý÷vûÿ¼‘Cž›ÎòÙöjN®ÄÛ– çª 8ý}z}: ûˆbçiÏ_-—?ÿœsßì>ë\ôñ ûÊ닌!²($¶ûè’OâŠêU=2ÑDmÇ/ÃÄÑ+Wˆð‰i‚øáÈ'ju¬ûߘ¢Ãô 0Š ãü?¢ˆ þñÏâþ žˆ ƒýøãÿðÿ†?þú0ú ¿ 7B‰èÿ0Âù×=' ÷È# ß ûø| ÷â £Ø`€ŽƒðÿÄ" 0@P!1AQ`aqÿÚ?ß{t¹¥æÍS0„&Ä!B+ˆÿW\cO¡,Ÿ¢>ðGÑ"R>†¢Bc5.o>”¹¥.ŠRâ—› °ñäð„à0ª¾Ñý!§Ð—èþGð þ}ôGlº8Bf„ìa8+¡šâKíúÔGJ]4¥ýFþ(U‰ÑðÃö¡3Óno®©”—ƒö«’ø<ÿ‘EàôX~ÂéÊ]RF>R±9ƒø ƒ ÙQp(^φûÛ\{¦o(õÎ2„"Ä! ü¾Rü5=»1r!0·—Ç1 ]¤ Ý…ô!9hjžV¸7D¿ ùÕ¢¦1TŠØ—v sa^Oá¼N-ÍÐ…É¥)wß¾‚œyªa «^òéBý"ö‰ã¯¸¹½T! ¸ø3 œ øùˆ>Dü DDD_D_D_D_D_D_D_DD_DD_DD_D_D'èWq|®ýr¡ À¸Z–äÓ4Âu ¦„&‰…­¿ ™®~n5Sî û6¼¾|Ì&gáÓËi»o‰3NÊ—ñ]zúíÌR——KÍ„&fô‚›°„üj$cQÂó.Åë§&#­ÖBvЈ„"ç  R”¥)JRáJQ«éîiJRí­½sMiÚ·™Äº¦ˆBffe Í⯱¶ûÕ΄&!MCm‰ƒœ„ØwEÍÙºf›®ºa1 ÑM¦3Òí‰)‰—›G¢<ͩ„Ä&!32º¦àƱJ\·[·ãRųx÷fÎ" ì5|+«Íϛ΄PÑ YK©¢Ûš!5MÙ³ ˜B±jà‘p º®‹®íÝwBx»7Ÿ1B˜n!+¥45{1LÖ÷ouK¥àO-æèhžìÙ„&Ä!BrW"lBCy‚Í ¡¾6j*ÅËíÞFß¶‡Œ&íá¢bJdƬ¾E÷ÏÛ!ü1µ‘þBÛBÙ Ôü}ŒÇ³M/½‘K„¬>í;w¼ø“Ýt— <ÑÿTò·[T7j-ÙDD÷kdy?óü¼xÿÆ>ÓëÞ’Ý!tW?î@#dm#aΘۻ ÞcËüax†·d‰³Çþ1ô/ñØë´…Ù1«!ÎD/“k6üÁþ^„|&%/%_’Csû‡æþ\é¾ËÍ/nÛU4<¥.iqKŠx³ý´ß©ü»HLXŠ> ¼N´ý,ÐÛò^tÆ’ ÜÙî¸m²$¯ä½·ê Ñ;hLª«ÈÆHM> ìÎÆÿE^P»”h~DãúÐÜ#»)u_g9[nz^;„Ê7å—õ þ‰ID‹ÇzOÇb¯M«JR”¥)xn/E÷á8ß’ð^ýç~¿t^ÍàŸ°WÓ7Þ¼©Çÿ#NªñÙœ7Žôßf¿¥Jþ••ý+úÊþ•ý+úWô¯é_Ò¿¥Jþ•ô¯¥}+é_G¸^xMwÓ¿Zû5Ë8&‰ªzëZG£½k¦áõoa¢ðNœô¯Ï­mÛßItRó'RõSôQªòÒ—¥)JR•¥)K®—7ÑÞÄÃxu©JR—‚ößjõ_ÏE¢½kéLÍw]×tQº*ä¾µögYÚši?zÝöl¢/ZúiÚ„!B„!B„!$ZáB„à„Ì'ã„Ñ ˜A¯WxéR”¥.)JRò]7†”º)sJÆð¨h˜}Û›¯Èœ/±|à!$ºó§NI¡2âµyƒGä ^ H[I\DÄxp˜«á3a335LBfEY«áHUð¿ÏSJQVâ·[ Þ•¼$ú***ô· ¢› ¡´R—7 ¢¢¬\Ü&ŠR—¹¥)KŠ&6Їê’mÄ)kÿÍkåðÞ=¦[SŽíÕH¡9°~¦ ²®i¥ÂN"DÄ&DÌ&S[Ãà\+CÅeecõD&¥©ÃqJR”¥)JR”¥)J\ÜRë¥ÍÅ)YJRê|šô’*5Ò„Âùz&›Õáˆ% Öf†µÎ ˜¦ÊX¸‚£g¨J´‡ð,NÊÍ ¬K›ðNG¢a-- ÔÌ¥ ²DÅ.„­qR—¥)JR”¥)JR”¥)J\Ü\Ò—”¬¥Ã¥e/«YX×€¿œBi‚a·Â1§¡,LMd5¢j„ᙘhÄ2 ò¾ò@Ýmèšp“¡¸±·tPˆ2¹¢êeš.•¥çóÀÕB”xzæ_u*ÐѸ­p­;i¸¥.*)ssVªR—¥)JR”¥)JR랉(}±5&÷žf2LÍëšS2“C— fS††¸_<ÕJR”¥?&6è¥Í)Dœ&Óæ…„°¥ʃuቬ±>Ê|f%˜¯MÐú“e _ZBQ$´Í„Ç÷…Ä&‰™¢"ff"",$ˆ…1 ‰ŠŠ±JlBz æÖ¶µÞ \Òæ—†æñÜÝi °iðÞNz-~1ùyšW COL! ‹DÄ륄ÂXM+„'Ntö Þ¤›ˆœ Ú‹(Üר¬¹U±©4Jð‹a!¡†½Q.ZäEÕ8!9n˜¡JR—Mmtn«Æ–èò„&V+®f—E)tÒâæ”¥Í)J\Òó^’ìQ=ÑäÍБ/K é—eyG“RTÄÑJ]3–ÙÒâ²”¥e)YJR”ºÐ–è~4§—¢k™¾ºô©JR”¥)JR”¥)x?ZV&‹¦· ÑxBi„&šR— nÉ•©ÇDx¥Å)qJ]PšüÆÐ¾_¡¥õÏŽ”¹¼-2ÄÓ}{î1ôækÒ SÕN ßcë#Èyew¿©1®iÁàyef¤ý1ð^÷XJµÍ Õž½­ »Ð½ú.¤‰‘' kž†z7Ï4£bCn­s^õä}ÈôÎ(B)ZË _,'é GÉ5ø&©+h¬¤Ðϵ±£† Û2GËcí±žìÛ1òÙüݽ·Žë~ uíY«Nºµö±=­}¥}¡¾Ö—ÎŽË_y>ËÙ¸>ØöéÝž¯,¯ç°5>Ûùláp—¯Š>XŸ£º1ô,zßœŽ÷Kóœ?ÛíÉßD»=Oàá:/Å?Jò˜ƒŸ¥Z{˜ÉûWC°.±»üãîOܽڿ~uù;3 do×éAÿ“õþÛóþÌ}ÿ³~_è£ÖþÍîÿj=¯ôPîß—÷_ðŠd??ìUß¹¯_鯿ûkïþX>ÿû'ßýñ÷ý0Zó ¾/öÃÿ+?ïáøpø:wèŒ{þØ<§ìgþQºõ«þÓõ„GßíƒÇþÎ ò ï”È7ßûÛèýÿcò×ý¥¾çì¿î£Õ£ú+ ‹èû!ŒYÿdî#‚Œî t—vˆŒ°G4ßE"cC;³ÑԆꤻ–%CKqnI´Èdù\ºBw ÆY]U³° Ñ™7²;˜å·Kx´zãm´œ†Ôµ•µžÓ»­Kxa#Ñ·È.6© ½¤.2ú¿±ô’6M†Íœ³Eôä"ÃvÒÁ¤“qƒ’RÆî–<=$>É.`-’ŃÜéºA³cîÒEˆæè¿7´úø‰aéFQìpG×°ñDMDZŸþ» ûŒ=»ÃJ·a=”–ôнézÕ*ѽ¼ÛpƒI G¦°Öß¹2C¥ÙÿZdf²ZÞ$æì‚GÛ±t"CPÚfž”ޔĒ-:2:·fhÉ8PÛo"–òã.Ëki+m·«mmVÑÑé›e·òžÒC˶Œwd‘ Õ›è2vs`¸`šôˆ–£V 8P]Íl@À€ ¨j­LDqµœÔbàWYQmDIÞÖØgn­ a6RRѺŠ–„&tŸD§Ñ =…ºðYì!}.¯}iúÑíô£é_…}‚tÀ_†ü$ýp»ƒ0FPEŽ ,†ÛYÒ¢¿’W·mzv„ß‘îk«j¨Ùè¬òÕ›]ɱ¹öYѪuÍûbœƒí¡ÆXÍ N¦^Ѹ¿~¥ÙÒC ¤³Š_’Ðí-)Ù/¢Zx0áè»b ìõj·êKˆÐ˜X½ü ô³ØA¾‚Ï¢ÏF|‹~˜>‹øcñÞFU‘ÅÓåEï–íßI,^”>Z¿&~Õ÷7) mÜtiþ‹èý,/ø_ð0¿ãe…ªh"]H†} rëRDBE4µPmŽÐd‡cwc3P±Ð8Ê»*—ž”“¢UmÞÁ!9ÐJ"Ýâ“r’h 9{ŽË»°vû2 ÃmAU]”İ –‰d»Â/"Æ–Üd·…¶×´ãxLƒ¶Ò-3–-8PºqLèø»%OfN$+( b‘¯@ôÉô­ 0½jbTtÒˆ€’‡º{ ¡[p]ðųSDT}ý’5F?ÒõLKªŒÊüX+6?ʾàö?a$ŸMQŽ«wŠ}|‡û‘™nŽ*ápM5Vè$èNþ.¿ŒÂ Ðbß¡d÷ŸßûÑ`5#¢*}³´±—„ß?SjØ•¡tD}Ï%íIŸoúFÎW^ °Ïh){M%W^ž—¦åñü“AE绪f– ¿DhƒÑ#Eöl¼BŠ@4øŸƒod^È[\çIxÛx,vÆG.ÒavÞ27exg†¹,vFlgh¼ ñÞ àyf#aáá x8Þ7³‘Ž e„àqˆýº>»ý•îp˜ÁÛv%¸pô^± nõj’Ä6Ã7^˜éœ:KÒ\LPœ˜Ø.eŒ½˜X;¨½„*v»"¿.°º(½$èfÐÙhðjφeãnÎ1µ¶N¡ÒÓm†ÕdM-NÙe©Ñ–œ ó¿õ'þ±ðˆ„¤ ½@ý0IÎ:(‰o±=ø éÜì%“QW¨Æ×ìýu "e@û‚4³qê­CÏàY”xv|OMPHq–YöHAt°Y­†2p`H)!¶YÑÈ~͕DŽÖ0ÑŸæÜ .Á/XÈ,©)®ô[è8eÚµ$žÄd@ÍŽÜo,&&O}eèBC!Tb€1ofy 42Mpð:9 ž¨dô“oÛ[IN–_‚<–›<­®JJ%Wµ >Ù@ã_¹ÈtF~¿¼Èg©Âe3#iœ80⤠³Œ<ÎÂò8ôL«w<ÁgÃ&<<*ˆ¬O2Û9¤pÆ@²u·«lɉ᷻eàÄ^7a—-†[µˆažž’Âq¶«¹ ²­¯Ód,0ãoLE°òø 9 ²Ç,,‘Ž„ƒ[6N1°?±!Ð/æ &?Ý–ÃvvêPàì5$ÇîÃáMľÃ<ÎA©v‘®57˜¸^Ýpˆ¦ä†#}±ŽÅŒ|˜„` Ð(Ú=ÎîÃé‡mnŽŒèƒ­A¦59:ãG†–“ñYmº•/¼e¤zQÅ|º³ß¦~Pÿxù½ã&ÂÉa`¸ê †¤ãÄ4,„ ±vZfbXD6¡±A@h  ˆö2 #0ÂŒ¾É4q˪;Œ‘Ä—SqH;ðq²Ûʲ¼++(–YY\µmØ¿ú‹½a¢pÛùT˜ãavÓ­¬ ­ÜtÆðÚpÂM¡nÏoÁ ´8t“@ 8[wI;φÛz„GX[L–u- ïg…±Î1‘ƒŒ»ÈrabxÛ\“¬b›m°¹êOMî˜xÝ!¶ˆÉCÙCŸÆCÈYàŽ»^x8,øÇYíEÏIckÚ߈ÿè-´lÏz[ÆN)g “·™•ÌÛXŠ1i–¶Û‹ ¶!ÖT½Ã :Ǽ6 ‡¬‚B,t·Œ†9 2Ñ“àC†X>’@CëûpŠI—¨a»“…Ì wŒw387c³ îîqOIäFàÛçI-O]Gòë ¸ Ž‘˜ ÄB{ …Éé±±»ç¨Ã yxYø9,ËŒ¬³!Éýð³Aª:Ʋ3™Ã+'..¼©,¼-¤Ì¼e³¥¼’ƒ )1t ±2]yù?ƒÜ«ÿ~ý‡(v³åò/.¿€Â"[ÆcØø›sõuþbNbpå²ÎqŽ-ÒlœçÏ&{ÅÑh½êo¥ÏѼF×1‡¼çÞœbÄŒl) »90˜gbhθæ¥ìHt—`ÞDH\ÄÄoÀ“ª{,n7¸MM Iû“Ѥ=Œæ£—¶œl….ÈexX~š’ÌÉ,«Â™m±ƒ¶v‘h N†Û4bN1•ˆÎsPvzÕáˆ^3à| ã{N7‡-±ô3-½Ýq±²’w„È@µ¶Èó/¤û‡í±%c·†!>gÐ]•þòœ˜jª¬¹ŠJ‘ˆ2zU—ß?é³V½<›ÌŽŸíYP½=/áúd†¶¯,§ ÁBÈͱä6$$e°Æðl:Gòx-†ìç;žƒàJ£vû£`àx~¨… â‘Ã!š ¨Z>¤?«ìð?¾æÓrË;»ŽƒÃyÇxêHD³4}’®çA”Ѽ'jX0„…Ftc6İ¡?€Ã˜D'{îpX…º-àßžsœ)$él²Ûó%Ñîd^.Ë@¯>ê~¡ºM ©óÁ“nÙÆ¤+Μeêo\ ·SÏ…ø­¶Ë ¬­×JͶö`IŒ62`"A PÍ„¯Œµ‰ ÆýŽ¡"â5WWeŽ&’á²òE°Ã "›kX\)<1“GûIûÞ&ì’C,9ôs®!Îð]t!“ 1°¶ƒ 1ÎHçbNIíã8ZKðUÁ@Cõ>¾è´Û¼‡an6—^–aãÂNŒÆ7{-¤Ý$0V>ÂÒûN Ô´¼*CÕš—Ó¤®€î _dš'qœÀa2CÆü{½CÎð²’Or†&“k+$„ƒ$ퟙžFM·Úݘá“:ÀvüC4wN7Œ²웸Ôù$ð2Æp¯ʉ/ lñ»<<0*ˆÝìiDu ÃᇌœøÿÉ'UUÕ^ÕYdi²–Y~  0q°É³¡;è÷á¯0Vÿkˆ‰ˆŽ#!!'^øÈЀä mÂaRÔ„q°à è³€FÄrxãc 8ÝŽ xc‡!F8 ]¾± í“w–ÚdŽ^÷,·@xN}q¨…Œ°ãq»,aÇu8‰¨»›8²L`@6Rƒ"+½-˜„è—zHtHôÜB7Nhüv·“xf^Kp”ežÎ™ÓÂ1²ÈL¬*È´Ôt¡0cÀóÃäáË7c±,1ÎÂŽ’«­¡Ã¶rÛl2 ÎŽ6á–›0Í­«-ÛÀ’ÈÊŒéÓn2ëm¥±æ ãÅücŒ»ÀÆ}Ï‚[Œ+,²¹<6Ë­9%¤¬E-ýãýñàâtü"pcÆÉdY!© XàÀ©¡:aÁ°Û 0ràäàÎ6Â0‡9£Œé?¦†Óíþ‚:'§ Ù{¥ˆÎ ²5]åz‰à(cnHÚäªFÒ.³¹‚²€RYu4,M´œÉÎÑŒ)°s±·Ó “Ø"2ö‡OŒËz4¶Þpùm¿N³gdœ™—„ö6?sÌ‚û?« }ˆÃvzlÓ[`›¯IÂÆÅîHb93Ãð&Þ5ÉãmÏ2ëèË(J/*oC/ ðÄ7 ÈëM±Ç_S­ŽŸÏ»n²Û+Yp¼e<‘#Ù©ôö[: `Çèfx¡¾“IË8ÎN28`]Z0‘¥Va¤…€Þcy"÷ Áø_жIÙÈÞdó}Oö²›ë?ÞñÒÙj“¡ Âá°gN^ÄÒÇÉÁ¸‹À =7¹aµHÝ“­˜Æ1á[¿¨r`À«ˆÀž£ø%bæ¬~'tÞm:6]·|oÇm·‡†g…áOd¤²"O ²·{±et„¯Gi-e‰àx0† xÇ‚-ŽqŸ &txHe3Iážmç´ Öa?§×øÍ`(}¹Íÿ2UœŸ-ë—á’Y;$ –#dAt`N7ǾÏÔ¤’Iu8ÙeIdÌ1{(ˆ”‘à`ëƒq…`ˆMàã85äå²8G‚É,"6þ8ËéYäúÇgL6èØÊè[§/[<ZÈmäºÉ^ìú‰ÜvDLÖ}`Ú ¤‰ØÂdÓH÷‰.“¤`Ñ‘ÂmÓ¸âo‡ãGäð¼9/)33’2óÜÌÞI>Í~Ømñœì±úŒÈ#x‘à°ÈøwÃãàékÆÙ%¦r¥ƒ€pò¼ešr&D.Ù ŸÛÛÃ<÷ÿ7ׯYÎKƒœl»KÆîé?Éà~§kLŸ„ÒÖ†¹|’I$äèáàØ€ðtX; =1ÙÀÞà‡‚-ààQÒL»Á§9Ccq$ ïÛ5ÿ×V÷¼–xáÍ|Œò¦ÈÛÀ‘=>'tL q“ 80t‰ogIÍðJldÃÀI"a)ˆÝáv,ÝFtÔÉ8¸XúK'á·YÀü§Á¶^-$‰œuž‹õM‡<š’ïÃo ¿`[$Œ±»ágá²ÃÂöp¼ ð¼:p£3œaŒ>‹¯íͦ=?­»ÚíP‚Ë,îBtAŒœ%šÉÆwâÔn°/I8ÙÉ4FÑ{޽r>Æþid’%˜ÌLfnÂÂÝu†d8«k޾$ØGŽ æ 2/DDßΞwDÄ,45耭ùy26'ÿo“’;‡—íÿ–>_ü›ßa?ÑÝýœaÿ»î ó`I%—|㸜l[‘ ™+8y8 > ü‚&,  è',Õý 0Âcìmž .“oîí9<Îd‚^úlQŽ÷FдœÞ7é·¦3lÂnÚ&"XÅ“íŒÑÎä73¦4ØP¿"AÁÑ}|7ü*|çFe–^œÀËKMàdí£ü?N<Úœò§!†ï³‚8ØãÔ]±ËÎc*?à%á^7…™hadøˆ›!îD&OÆßJ'œsÓr.ïüêÿ¹tqÛ¬Â7k'ù½\ cðšÎ6ž· î9¶k5ÉÞ±£Ë1þÎa:4ŽE…ônŸýq‘~ýKÆ¿\âK&I^M»`в à ‚1àˆ!!«k¸&">––z}BÿPKøƒôW—ØÎýñ†ðaÆChæÍ¥Ó Ðî¼ -Jl0Ên¶Î[ÓÜù/ÒHçLâ[–ëe¯œqÙŒeatÓáŸçgyxÞfY™”™œ¶ÙÞM EôbÌŒ-´àÈÌ–ºÝ6@ðgi„³Àp "wÆŒÙlŒå9e¥[Ž:<%²ÛÜkÇßòïRBß®ŸõÆtã â#±þÉ U¦ž/Åç)ñìîWì…T}û[ÏÔÔ |Ú}â\ÄWB=/ôŸ„Êö?èpn£<$Ï=IcV7„Æ0‚"BÍx,à†öà :2­‘ð`Ç€økžÔQ<4øåÍ„CŽåÒmàŵ²Ä“èT8Œõ'ßà ±œ°‹_YÁlÚ.r“++2jå·ð`Èœ8KÃÂ[ ^PÿSûý7‰b¤C¿ëÿ×€ì.#¿ÙÉÒ“²0X 0À Ë¡v¿DËE|q„àÆ>…õÏÿµõ‚=œ åYÜðÃû?ߟ}àÛP™áxqáNÍ€„![ !ÜÇÙ‘ñ!†23d€ààñœdœ§ú,›èXŠeå½çÝñ1°ªin­ŽÝârœ9l2vJë¤xF^…8ï á9v)Ædþ-éÉó ÆœïÍ–Ù—•ftáNuË«g'-à[aõý¸]‹Ñ³uw¯ g|5Žrp†dmá“åÐÆNHîòë’OÇÃeÑ#Ž£áß×}ßÝ‘Ó/èq¿âÿ÷‚[ÍøÇ¢0'+¶Æ•8±g³Á޼΀»}Aˆ±öÊ€íXä=ÆÖ!¡õÁT?ú07ƒ»‘àoæþñŸÑ¨Í³(ˆ1ËÇ)à´Iÿq±»â8.í¼Òw×áþÚÃ(Œk*éâ‘´óœ)…ïxQñÇ©Ä5‡´cÉ…Ýý2YÑÊv¶t…ˤÎ4LÉNÄáÑl¦Hçm®ó²ñ¼<³Â£‡Ê’ÊKðYuá‰ø¿¨#1¼[h¥±Ñ8cXSäÎ >#Ó ,™W¦?…{€"’òþýÄ„Ýå*ó¼kðï""KÑìÀ®qOÙîˬÛ.ñ¿ s^7‡œˆàÈØr`³¡ødtsœŸ ‰8.÷‚ìËê)`¶ËúŸ¶fîÞ: 1ÒY‹hÄŸG»ï_¼¹‰À¹Â벜-±edîju²)jZì«o¤$-­³k.ÚB²Ê<»ÃJÊXNK`Ÿ ø‰„‚Ï)ûÎŽVo$EÑÀ‡·èáó<û™Æí!sqIÓŒåÙá0“†E¨@p^Þ ‹e$>¸Íë÷<4œË!HsD´J½{~$‹Ôă^ €‚ ÁPJ±Æ=>Î>o'_“*¶*ªÈŠ<çD,U#wàHq“oÁÁ™dDÓÁð#“’Ø&WCзvý“ýÏ»þæ->ŽìÚÛeï ã-ÓœÓ~ ŒæÎlL‡ o6’}Lͼk6ÄÄ#i ·dÍï… ~+/ ,¬¹Âž‹YX†’vÒúlp$í>†ï'i'vG–ºÃ±l·Y#:Gwf™ðÙÞ0͉1Ë d»Îs,áã,a†BwhzÌK+Ât¿Ipb({Wÿ©óž E–ZA\ÕÂP¶X¶, ¦(`\Ú bt0Ý¿ƒèà§ø½q?!Pª½ª³.L,À†Îð|4dŠGäXp$ƒƒHŽ#Ì Xä[ÎÉÑÁ¼$7ÝÃŽ/Á!ÛÛŸ´,Ìè 凂C~Z“fð.âI—Ai9ZGËoÁÞ’XSmCƒw$Ds“àmà¤[a%7NÕ±ÙžvRÛfS‡m ©[^/¾´þ¬v#1-0x·â:põ÷¼lD@üÑÉ &ì¢e†-šÝxÖlœ„¡Jæ#œ2xu’UáÛ3Žç…\A„aˆaàFžãÓô“;í¿ï‚××÷ÙÙ÷²›ýÁ¡Äl²È c&@WÿÎ[/ùV”0€}oQ€Zqã÷~WUÛ¬ÙxßRÆOoü3“ÇÖ‘[®ÁÆÃ°D=gÙ< ôËcö‚ô}ð¸•á…:xÜfqã;ËI½Z„»v0£âÄ^ Ë»ÙùæÉ9La8`ãIÝ»µãK]am¶YXa¶ÞvKI”œeF[\់–¡ ¥!h¬R0 rq¼lAukozÄ1 ÊÚ&¨X ’ò?l-ÜpI6‘{ÆK®3Œíã­à³PÉk@ÄH¿«¼Fzƒƒù;ùý2Z×Þ'éÒÕçŒoó¥àkü׿ømÙ3è%Ï~»aýÀ= _Ûè3[†·X%[aÞ‡g2  nÈã8`à °ƒœ n©b@ÅšA^âˆä8<ÅŸIàmÖ9ô ý0¡Èñ¼[jdí©À¢È%—k;o;ßðQ¶máÍáÒÖqàÔ•·Ÿ\ Âû!´Øeàá'966Ùw•žvwyφZd0Ä.Å,Ž˜mØ0ØØÝ²mï‡"v¼wœ0Ý3:UW€‹®=K7¾YÆë,8Ž˜q¦Ú€É6hÔƒøH°:ÎgyëöAtŸ_ăµÀ¶¦ÒÓ§ûè%é÷Ú­—‚Þ1ÈbU8Î ˆ,Û8IJ 2 ² 2ñjðXË plp4ృX'¿DÇ¢üþê2|—k6ÎHq© œÀÎ1ÊujqœnZ7e²¼m¬3ñƒ$ÎXIÇRÆB°¼¤¨ÛhÝ3ÆÛÉl¤ð kl¼i<.ØÁ‘ ¬[àNyçeЃ`yyÁd°"Iž2DxÆØxØX‚Ä Dq.°¡åáùIû_ÔyéOüIÕöë¹ûµ*ÙmáxaçlÆÞ3®L‹,ˆ ±‚ ,ƒ€€‚7cŒs屯ðp€åaƒ²Ôú‰c} ŸÜ?ÑÏ¿‰wÈñº&Þ2Ä‘‚Y ·\<<-á[x[nÉxÙRÕ›aèO@'aVÎØA!mààt„xÁb H‹xØxÙºã2tN2Æ '3âÎOd¼»$ñƒt|=€`½¶™;l¶òàCÆõ °Û ¥¼,p0Úñ±»>'Á/DKÀð üµ-xÍx'exxBå¬%œm¶ð&üžž6Æ$°ãz”ƒ'Ï!ôË"]ä¶ËÊÈZØÝØñ¶ñ‡ 2¾„û†WBFž3ƒ!'‚ ¼$è|_ƒ'¢Ùø¹Á³$±å$>[o&]|Kc³æ|×îÝÂ#Ì/ÀAK ÁdPEœo:ppØç/’Óý™zƒþþ_âÔ·“à|2ôòÚü]8ÙÎ^r7àBCÜ ¡ŸE¥¥¦ï –Ô—›Õ­¤ÍxvÓîÛV»-¶°ð2û „·KÙyn Ðì+˜q„oÉàEO‚/XàQGã;’ɲ'Od+&xvE™ø¬&0œB87¾vÔ„œcbÞ2ξÄ9y ƒƒ€pY¶˜øÊAbú‹OÜñ6_{•×J[Á¸ð¶Ýü{ÙxãXDƒâÂÃÂqœ­­¯:'# /Û(Ã-¶ëim¯&ÚȶÐm6ÙÄ”ž¶_ l¶¿|© ˆ2ûà 86`†î ‡l„àãxSbLfl7Hm>ïmÆí²êÛ3á¦p³Ï\j–圹˜A8ÓƒÃ#R38]àà·àÈb!8x#ƒ6"88X8#‚mk s»ñÎ_ "H5 S<ìs·Qñï#”ywç GŽÎGŽÎá1l6ðÙ‹ #4mm…Ëm¶ÙšJZÚÂKl¶ÛÆñ²èZ–­ÓÄÙ_®²tRs ïŒN2uDçnd–ü‘ç.ã„‹£tã,á,œ±²NQ€ö²râÙ!¿'?À< °Ê-¿Ã€6‚:l‰aÑ`ˆ'Ä˸ˆå"7’^ù Þåh%ÐûI–ößsðØááµöC <äaÀ Œtq¶ƒ¼ îl~;ð^—‚o ”ÈŒÔLLa¢ÞípÛËÃeá¢Ûm°ã) ¶Øça _•ƒôº¸Ài™Àr!(ª$.Dm›u–cÀ'Ï]ÒRW‡&LJæñ¡/y+Û3?àÞq~eÜæ2s‚a‹£îŒàße¼ØV»l?ž™£ i)m¥²Ûm¶––“Óí´%¶ÖYoXbzX[lS~‰dÒð/Àº›-b^’Ä‘ÛwâÊÚå­¬íìž6Uø?ãg§ã‡§ç°ð< 0Û;°9¼'’p!aµ"7aÿ|ƒË*ž‹ÿ£Š 8ÞGwñi³œ1Ãtøí½rñÞü;yÀ̬—ÄS“áÚ]í»-ž8Ùâëï†nÿÏ O–7ôb|±ÚdJ=§AÉþ¢%Ž}ÈéÆI„g,ŒI6+’aê]| §¼ï.f‹vœ¤p º[Æp£ÆÙ½ >ζÖ؆6>#<¤ ñÖl$$$0ëw¶ÇÀÿ ò-½a2Án§-ßÿƒφĶñØÚÎs¿ 8qø¿©ƒï#ëdy³Ä/;¯/¢:ÏÃí ôvdãfz˜ãmwƒ93m¶é#p-·åœïÆ‹ñ`/³ì³Ö~I ‘ü63È-¯¬—Û[.ö[©•9rqrΠ²ÂÁ!}¡ûnŒæ0à ªÆm‹¨hCoØí„…m¶6à‹ À¼®³Ñ˜Ø[jòïM¤íÛ#³0ƒ©'>æÕ3†ìø‚ pÝï8p¢£#ÉÀk‚-ˆãa†Hx83ŒA³± [ÀÑŒÈbÎ]eáàpYþ@oQædäÃݲCÆÚ|6ÞŒø ³Çd¿àYàú XkôÃiaöBXÆH}pÅzx1ú‡®ÆH2I-3Â2-'‡Œ'd‰2Ô{P>’Gbp`–0 m†FéòZd’ÛXm†Ø[c8HaáÝ·¨“‚éø§ Ã3o 3³ÃÁp87G ÃÎÃÉ:?ç"Þá 1Á ƒ°«¬0C 1Á6Â|8Ø—€›c7ÅM²}{ýˆ¼o;ΟåN6rì¶Þ7弿 çxDƒsøÙö­ Ý´€°¢É¨É-8$, ïÓËi™¶–M“#¶¾ÆWêõ6¯Á±´ËêCŒ-°Æ¼ãÀr6¥¶­¥Þ™ ÂHYø-†pö|1Yß{>8G†NYç–ðªÇü=ðgÆðD“vbÕˆbCG¶ÛjÄDpÌsŽÉ†A¥Í 1LX‹èÐoÊóÒÏÆÃ(Ÿ øëÇOòÞ7øm¿.íq’öa{€{B#­ßÔ»Ò HƒÖlûKÈ<²©ÒCÌÆC§dýáŽ!K_j×Î2³Ã+ígØ–Œ¸ùI¾È¾Œ# —óŸöDv¼ bs¤ãaáx6VÖfÂÅî ,…° ¨åÉ;,³…Ö¼i™’9ªáÂI/ ¸èåÒðœ<î?åNt@Žwã±ÈXÄ€™Hm†!‰ìDKˆCwg‡8Øs¦DèGI¬‚ü‰x¯É㯑 òÛ~—\uŸ=›6iÒ ÄɰLFÆÑ¬,,0^8q¦Á dá–6&$K[\Ì•ŸÀµá¤¤ŒgT¿¤ Ú’’À9°üVÝà!„ã¸xØlab6SBÈ&Ç9He×Yå–ÖI’YxÛxaÙN6Þ6Ñ8Û{5œÛxë7]àK~;Éœ°÷lA„†cÀ2a¶D„‡¬,à#““ùìOvM_βÂÛéò˜»ç®uŸðm¼ïÃmãmç~;ðYN4àl±²ÆÅâ;F3ù›ZûYø¡øIôKìíû’TæÏ¶Á7e%¬à`d`NÒ¶‡²ÑöYô’‰†é°”`@ŸÓ?|!«‡Â‘¬°§ˆäËnþ' ‡Hb238ÎpÎ^0Ÿƒw.2T‘¶§8-¶ð¥¶Ûm¶ó¶ÂÚpØÅgí,b¸ d6CgЗò$}Hú³ ßp8iþ¦zYf8Ö6Ãlò.g„(y Ø„”AdfYÔYÂpÙ<¶Ž–`Ý ·…·»ËeËe¶vÖ…¸Ûoo%Xíe‹y-‡‘‚¤Cä0ÇÈy>%°A1¿@¼…ÿÖºÈ$èÛÆÛñߎÂ|wºãxÓü ¼9Æðü5²ÑiÛi÷ àž<'Ú$c"Ã뻵µžÆw„Yg…~ø~ ‹HBì:-Ú£¤±èg‚NŒ½Ú±ÇdldpfZÛÀ|RÉ8ɳI]™”Éfmát xÞ6Ô¶Ûy%= ±÷.hÿ)½àê×eã¾wã¿àT·ã¯ÿËœvž¡†g¢YµöN–¿E£‚–bIX÷)Øé¶Â˜ÜXgRÆDãûoí¿´‰ÔÑ ¾D§Ö^Ðøß¼¤„Æ88ÂSbÞæ.ñ§iÚë]Ü#xÙÜ….øN HOdÅŽÏ ¢Lºc,Ï Æü7‡2ܼÇßp='ÆÛ¶Û 6Û <b=Æ®âŒ0Â[ 0„[ pq:MUXcƒžÂØààºùìŸX,;?j³çöˆ@(èÆÃ6ÛñÛzÿ¼)ÆÊÈzI0»Z‘»µº’Ãé±#Ò’}¬z2pæHlÆD&YbúOÜƶKg|þ6#ж$cÁ³GÁÅèÂɳ©ápáÄ™ze••%ÖY~>‡à"’Ÿ ãmùm¼Úðs±ˆŽúL†0ð;á!ƃ 0ÂC,a"F߆Û ãy#HŸôã‚o¯OÓ=ü—ç¿–’ÛÆü7ü›uÆÐ»Eô„úYù ÷¥ÔõLÑì°z ÑRXJ}Z2– –¼Œ‘`ggN0ȱ–mjQá@˜Ü™þ”‰éN5½ÚðŸ”}D¶üÇå¿ m´Œÿÿƒgá³Ão–²Húl> ˜@ã_¨_P¼†ÌD•µã[¾°]ÖI‰eÈN.Âu¶*Êä«Üû¨ãj,¤·ôÚ¸ÚÛÁÐZBHƒbÀ`ìidt " zaï¾òŸ^|)¿ ßyƒzôçÆ©>7âÏÕ´0·ñ6cƉ*à³ YGrIà–¦6?LDqéHý0/@Øý¡}¤CLY÷»ò¯Ê³3É/àH=¾#ðA|+ò/È¿"ö‰¿"éẼ¤DlpÁ»zÆÁXøÆB £} ý69á‚\MÞö7ôÀ<¨¢ƒf; Æ‘ @F‡p@°/0XÀ·Gép©Fh¢%®`ºÉ½Öb$N&&<§xX·9€ ¶D—ÂöBt `~±?oÇxðÛkœo;uòÔøœm¿áÞD°°€úYd ’¬`±ã 1µ7©²¬le–RCÈ–#ìUY‘,2ÃÑ9õfÈœ&Óþˆð2¶¯ +œl®{8vÖÖߎ¼1ÆYe ™3ƒü#m­­Ü,¯ð߆ÚÚÂÚÛ Æò–q—yv60YcdŸL ï&aø&Íl’Ll³ mzîVoíøçC¶@K%‡èOÇyÛx7á§ ¼ªr6ñ¼oø·9ߘ`Àl †$-¥„è< 3¥Üþ#wö¿’ÏÃwkfÉ#c*T©ÙIA x$ž‰ý6¯´“Û¥~jͶü"bÍ/ø”²Uå…š’]pqœ<È—¤ƒŒåÿü3.¹È”q³äG9ÆÆTñ£Üy|VÛl‰‹ñml±!–ÒÞ6Ûm¶Þ7¶ÞwâJ®rq¥¡.#Ft{‹ûTåPÍ|#Fp¿BŠí" G1m†Ûma¶mm†Ûx ¶Ý---ḶÚBBqÛ–ÃÐð8Z’ëi °‰%åmº½¾‰¼±´{%Stµ÷–=¤‡Ù8øgèZà‘—¯Ð2eøÒýOQ/ÉAÎðÙ§ÿÅð×íDs‘â‰,~Î`^³äfÝàP‰§¤Wô˜ájU2îÃØâŸ¶(1D2úƒØ†z,Àˆà5Šv8mŽw¦2é; 8ÂÎ@àÇaÇPrÄäÃ8$±Y"9 .[‡ ·E¶ð<,¢ü¬ctôðiŸ ãyÛm·™¶ÛxÛxm‡Ž§>þ%oh± `ØYb ±auéƒ}É}¶ý({sÁ“èYÃù2¶KáY<©›9Û)0DÄŽûV,O‘˜\HZH ñô`¿O ,<ƒÆŸäß‘›ÛðÈäÌh\—OŸ{ðÓÿ«®D’ `Dé“€[A¤3Ì  }•Q¥ÜÿHÈ#a!B*é ìÀpo…Á¡ÃÛÉmÛÁð8Ø…àwá°üw!-´Ømme»±»Ø^ ùH1Î<œìégÁm¶ü·ÿæÙbü»ÎqÕý@BZ¼(SØÚ¡KzgMÐÀ´° RuÔÇÔì±û,û Ÿ­S¯¡a÷#bU!"JÊ:%½qŸ°XûÎn,‚Àd|¥øiÊÚí¶|K­›~gù_’¯Ã8ÎSà ~9ÈAÀ]ÝÄ="km®|2Þ{ÎB9Ë.ø×0·¨m„!•·„oÔ°–ݧµÁ¹)~™ýD¤¼¿ åãÅ¿àÈåÿ9õY=´t…¿XNjí@MËp'©_k´vÒ[îí~ˆOpí¾‘fˆÛÂ-¨èíZ{€HNd¤Ý'.ótŸä“ ”N¾Tÿ"µlÒp[Œ¤OÀ £ÏK’'=rüÝàÆ N3ƒsÉm´ã«mµç>.±ÎqqÃ,ã,±ä'v Ë,‚É,@[ Ï $ŸdöÄ`¤ž!È„:f0#–Çóͦ$7YİGóý³ÆÚ|7å³þÿ>ÿ‰²þsê ˜{Ôý”jZ,Û –°Å×ߊÆ#:|ÏÁ’hóNÉä£mž¦¬úd>Æ~âDÅÄtr·?Ê ätŒëƒÆmlF¼qš…úãi`‡eŸIf8‹,ßI0ð³ð³ðƒ\ÒOÂÃ<›gÒXý ü,}‰3ÒÏÉaô°ú]}l>’};tæý §£9õƒØÉèl>—_Hüý7Mëo†o/ÉÃ=L;vë<:ƒ©oÓ.–O±,uä$¾‰£ELF1ŠÜîûëvËù'ùfì}A¶û‘$-]<²Évý3X5ö…¢uä/âYú-ïpµÃe¾a¾È[¶÷Èå½Kk ¶ÚZdñ‡ÑÇôK¶™TQPÏߌÂÂÃëá¼÷dbIgPp˜…ݬ.îÙ#XÆye…8ÔµxÖµˆ_JZÆÝ½m¯Ý­¶¥®íÓ‚µµ€uX!Tê«AÁ”à²ëÂݰ6+ B¬mžËFèØ §2†p4#c 1GÈŒ&a¿“Ô…ÑòÏv·|?üzü/ ´Mf@L>ŒsDl5æ!éÝ<(U°/·j>-f=Œ¼Ñév Ñ”<ªa’_Éû…Ÿº³ãS">Y—ÐÜì°w·k…¯k cº/ûmJ#UuXq˜2JLá1“N ûY@—n«kj]»víÛ·jݹv­Ûµiµkˆ­Û·n8víÚ—nÜÜó©íg〼ïÇ•j×à{øpå¼ã¹\ûŠ«[v˜Pó«\Ü9pËknÜ"Ë» Ж½.HzNÂMª’ƒè—Lv݆vIhÆ’ïröüs¼í¼¼m¼oÿø ½¤öêûÆ'BÚŠ˜G„»äV³i ÆýÏí³L/?á`û¬×jÈ_ä‰×OfÌ-<-§¡dcá˜$&î%ø³é]Gí+íJz^`³ä  Ò)Ÿíï“ [rL$y!ƒgãP‹`Aº¬—g!Ž’`âŽlr‘‹,l <‚lÙ ñgå]éñ¢+ ÐcÄ 6x;,l ˜8 ,¼]WtJœ‘ÆœAÂã-xBuÃ6ŒÑ»F É"‚NF¥ž¢°n¸Æ¹ÚB`=¬a[­„FÌ:|d;8ÿ×^Á›møïÏmãmøïùÞ6~y÷À~É~¿Ðcéþ¯Ê®¾¯³iíÇÐ%¬=¥‡©­{úHK”ƒÃvB6X{`Iiaé-XIô$}M}¢G|“Ï^ÏôƇsù$žQ>AeÇ5¾Â~ sF,Vx¬U/ö±ƒ9æÑ-S`]p@œeËHý?ÃŒ|úZ•jÔ+V­ZàÕ«\·o‹â¬.šüÜÇç¿%ùïÍ~[òß–ü·ä¿ÔŸ¦üWàæOÅž·ã¿ øïÇcêüà¿øcé9ñÿÁ¿©öä/Ê| >Ëóæ/Ì_¿!BüÅù‹òâ_…~Bü ð¯ÆŸH¿ü ñ/Ä¿üKñ#éß~ }Iú·ãÇÕ¿>½ø·âß‹VÁñ¿üKñïÇ¿ü{ñ#ëíø7ã_‹FtÌ߃~ øœàØzß‹ toÀ¿üHCDŠìí »Ä¸ìªc#4c²˜2¬!CŒ \p2$vfYì“ø©8ñ·¯ñíÕ¿-ÿÿƒ~{ôVÍRÏÆWítúGO< ]¼–^Ñtºxr~M‚l^—RžÈ÷ 9`/.›Lð¶ià$É_p¾ÖÌQ…€gb€€«G§ôÚ½¤t%3É#3dî`ô$†ŒžÃ”lµ'wK»“Ùn„“˜ÏoÀrªð>;ñ-”ãCᜠLæq¿!µãzã@:p%¿_ŠÚÚZ4!·!†Þ £á¶ñ¶ðHnC7[(pXKd¶¬ p4=¦¤§CK­±:éP3OÞžRAQÄmþQúµ`ÝÃJ–a.ÿóÁùøwÿäÀüqÇ¥d,î÷›’ÃØÚº@€}JÍ~Ü"ÏÝööHú ’/€dúo€à¢㦪OšúÍð•ü’ÿõJ§ÙÿÔÎ(?™âÿÒ½:а ‰xrÄcÒ^Bé!“$Ä™1Œ{aÄÈácc#ccc–@ð–qŸ‹?-Œ )CccôÀÚmZµjÖFC¨R…Ô ¨g·ØBµôÚeà–mz»»RßL7u6þ­_PÝy—àm}­ædýOW‡€Vw^л‹ƒP–ß×ÒÛú—Å·´ôÝ—® }Mø›ÄêüMøûæF ŒýÉ_LýL®ôØ{[>VþÐ^›èQ Ðøv-G4$CŽ6=jÖâÓ!€|Šcs'àx³fŸ0êIÀq˜ÁNÍÐDÃýÄŸ•¤ò˜GöJœõoòØÇ¿äÞ7༶ØÝC÷‡Õ¶Õ­ü¯eŸaaõ bÇñiöÈ&Àl>¨sÛ5´|+€LÐ.h¿Õ®'ö…<è?“ý±Oäæ~„ÈH6,Æ ÃŒÚ7‡$´FÜrHFÐdˆÖ ñºÝ|2.µŽ‚7wg EÖÓÀ›ihœïìFïÑΈ°Ë„&BI%ÙwK@V„uWV%´!‘©Ø„¥²Ü'ì#c@]¼9ëR ãFO´~”7INÈ4mØn„T'éÄÝ”À,ô’5™œDyL¡Ø4%é¿À½ ‡sbÜ'WD¤xÁ" t’]RÝìvé!^‚€$—þâ3éW¡ ¦$îcOJ_ŒþËþÀ€Oü¤7Í!…ï#DŽÆàÊ4A$@D†ÇV0ÇØôàÏÛ‹é7Á6j°DŒí{…²ÙN¬OsÓ/ùÅøïËþEú‡¥ZðxŽ=%‡Ü°{³èH÷bÛ°>-=‰O•îh-<†ÁEQ°¶)ôwÿ¦ÉT@±ï×øgs“ÊUfëi)ÊïdÔ4'KªÊå©R¦Š‹(ÙÒ1#¦Gd>HDΖÔ{‘ítƒÌ”9ϳ€l @V¡®5ÄwëW×¥”ˆÍdbc"UP Aw[?b d‰Ì gr:`……F=Ø(9\mÈ­ÌcEÔÝ::÷xDA×; BÌSƒù« ²£ëÁæ“õDUB°&'¤‡`éðKÀ|ËÑe«;Î! º­ˆz¯³+¨¾Å.Í5?&B©N,âŒ[c%[÷¬h™ÉNúR`°í‚JiäHŠìÆP(¬¨B­‚«ržO·æJ¢šXHB0C¨tWÑÀXÇÖ?i±êm†öà>Û )tPZƒ´-y}Ð{gF¨€#«ù§I†kDÔGt̓ô„m%~ãHÃ1'š¥–Ÿm/ƒJ£TÃ#ŒxD±Št†‰"%5¨á¦ã`ýOC ŒÏeÓÝø˜}Š ‚3èa[ÂêÛyùTmpÄS Õ!y~Õ,&½ƒ, l/AÏÉŒdB¨í0!—Y¸°¸B{~%\Ü5¥Dî Г×á‚„0 ‹nYb’Y w¤Êè‚ðÄr'nâ5–À"ÔÜP´¡,6èf©.6=¹P‰>œ4Eô‡BIÔ¹‚ÀˆC¨‘FfiGn곚þ ·±‚Ø×‡lzÑ-kñ"+éD·ç«By!@80 M.‚\‚j1 “ØÛ‚Á¬qìÔ@Kšˆ0ЃC>A ö-oV?³³àa˜ßño/Àÿüôãmøí°ê Y÷ö‘$íPOµXΉÃO$hð…¯¦ÓÚ°{•K'U€ô,,îN‡–ÔQ4-G?Iü³Èþ‰œd'lwéN#bïØ3ýÇF™‡îDša·Î¨ú€òŸ«ì,7·þ\Î#8 ìÉšd G¦ì2r"BŽ$‰o@ëw'²Á‘;™Ã6 ¹Àvf“6£ce&’Íã0tUÿveY)‰¤5AC¡z[SüG-$°Ã¶Î¯6 õõLè ”}/ôÀLÐ"8¼[`6¤*›CHk쯠ÄhÅDq»Â«°Óù-¾‡¶ «;úñ àØ Hþ¡q˜û ¤[Œ©§\œštØAå6qî¶@±Ï¼¬çñÚÃYDlŠÞ æÍsdÖbcüFrR×”ô ç_©(>„AtÒ!«ŒôƒjÁ·Õ훩/Ñ´“Öw1aØ*ÅÔ@PµdÇ_å9݆"£ ¥ÉTŠ8-ÿÇu‰0XëA!æŸ2¶ã°ÀÕÛ´dHHê PÁì„ɈDÍ…‘ÓcÚ[6Ìn[û™€úk Óý¡ý ½#e„› PÏÈthk œä­Uò´Þ÷„¥füd$ Ãô‚Ø0KHÆj…4Bð ,%@Ýîh–ôQ°¾˜UÅè•ÆˆÏ;Œ4Á?ŒÚ»ª_õÙ&3ñßžü6Û~;þG•ž6yïrbù#j=í¢x7bϬ{I÷Dà}pì“ÊÎ50˜†ÛöþÃ;fâ&ü+£v¸§£µ#0«6!ä<‡ób@}Þ…ðo±,R·+ÈgM2 i€$¢B#¥ºB2N0£fº^KÄ«4YÝ…gAPPD›ë™¸AÀA!DEÑѶ3ìÇhDQ”g’Q•Wò„ÕEJ· Q6™*í8aaAŒ4GD):Õf͉%ŠŸ ÇaÖ àhK·Pû, È·j+ر¬Ø$h0P–úâma4G´ßD­§Ó D2b`ØXD!4èí–ZY,5³©å$ÐQaô”°Â}­ XÐ=ÉZÉø@€Í—ìƒï©4’äg>–>„«ä[SÄßM½;Êy_ÀµÌE¬‚™fèÚB.ŠFÒ^î¿vý(?i“$H¥é°û~ÈY}¿Ã™(ÔÀš™:0ø¦‘¥i“D«ÇCI/]-B KÏÄõ¸Ò†¦}ëSù¢~{oùvÛmã~;ócà¨0>•ÛØÄ!¸0¿i?Ea,Mf¼‰(í|ìŸlŸ8)o¾Á8Œ O³úahþî ‹ .{çèò«ªôüc5>ÖÄŽþ–p¦ö–d ªŸ}σ¬ÖûY„ [Wµ‰ Yò ‚) ˜È$N§tmF@L°Qô’L ÷m,RÅý²nnyý6Î%†ÚÂËf¦1 ì ‘L´‚ð†e¢ì†õPj4Õ¿O|<µ·­d…m,¢6Ž ÐšÈÀtxT\IKl‡‚‘ûûõ­`í<˜q哹%g™à†EBwê6p€!¨ ç>´Û8LÉKÁ^0~ʆêÛ3¨®+¢33…±àÖ>rbAHºHØmxåÄ~>‡è˜êKw¿É" *¯Žá –iÂñ°X& P-ýªð6KléüUéàìnׂó¦Û ¥¥mð[›mȳLƒÁ»”žå–Q6²ð>Xe„´ 0A´±¹E€˜ª—½­"n1 )Ií¥¬ÇF¶’òºmFx`Çt– Ù´>›í…ÆQÑIŒÅ¿ ÿ>ü¶Û~Ëÿ /²ïûÙ=¼ØD6˘íþ/ÒY­]ù$ÈÏô ”!Oj™&~ ô³µÄ×è=|aGÒ#îjí^Õà}QË¿÷€“Ýgñ!…t obÈÊÊo´x ´¥šhÂf,î“að-ÙbÊÔRD°‡NmÐpaz ÏP{“L`à±lf ½ä®;þM2 à @:=2B*ª«ª¾Õ±†A1N³× ¿V& `P1„ê맇2 §”Rc‚ÆM¨¸~²àÚ\Ïò˜:¼zOi=“ÃÁÙî÷ǹf°Ôiä# C> ÷Cy2¬ Z>„D`¢ˆ ßz(éË¡ñ²Åã^;¡òúƒ!…T?iÀІQ‹Ðœovô8)¢7aü§ÚºÞJÂŽÇòréOXéÆÆÊ‘’Ô @=0Á$É=ÏFJ­ vŽvM1ô »"8mièuìµ*öÔ ¤Ä1˜\qiÊÑJ «d§q妡¬½èS@–CRA–,båA`V 3Ø9b5 uª ©£–`D“Jä^É^u_ÉÖNßñôó¿àߟ'åß;ª6ýÁè ‡ÚJ¾ík¯µ—ž[¯¦Hv¿Pnª„3+¢iìoéú€fúÉ;ëI²$íôjÏ=ÿd×Bû >òæ0}`¾¢£â_¸žƒõ~"Ìèr¨ îwܳÿ?˜H./M©0Ú“ÙѦá;8Èî‰ÐÓ€¸r1Àºp3‘´z Ù)d§˜£Ù:௹‡Yý-›3Œ/;œÈàiý¹þ|¤qáàüUÑ1èÝü9á"ª¿8TÍ?Úö«¾ÉFþÜH(“ ¿—†PÎäx¨¢ÏßGè ¶6‹H †¶ƤþºœyzìØ“/ëOêhd…×WP>¸]Œ:ÁgºgnÜ4 ÔN¾` ãÞ ÈÆ]‚VñqlÿÏC É¢KMƒíÄd°Àˆ_ZŽ •QˆD'½á›cÒ‰äüÑà.È ÆÀû‰êK)ÒQTþЃ« áS¤qaÐÆˆŽ#(ø*RÕNƒZ’Z%ö(ÏÅé±e;)ŽË¨ÑŒZÖÐõ „ Æ3"#ïV/¥vP•1Œš1¥ILN¬ v_ Hã1þÃúºgÿü·üÆó¿àëåÔѯо×ÚýØû1¦¹h{oºý‘¿ruêLH|„ƒÈ¼ÁÍC=úŸÔt¹JóOWÒ¥v÷‡ó”öìÿ¾Ðò„`&¿»yÛfÍxÛ}0b€¬¿h¤˜ÝAE‡±‡Á·DÆzlaÒLé`!°‰k7í[\N[Àý¨ˆ• HXölàÁÔÎb°Ïà‚Ç$@³³ŒX 2Á{ˆ30YßÛZ8@ ÆÞ$py%gûB§qÄ[°….& ûO¢2ƒ.þ¼¹¦ÿ$ 'J"N¹Arì̈º`h~™»5V¡ö¬3çeVž˜ÈµKjá˜ÿ„oGúþ¹€?ü› p?†Řª˜ÜÁ,#Þ6¬Šö Ð6¤!ì`ÀŒ@Âø]OI(WÊzÈA•zm€I'ž¿Ä§Àçm¶Þ7mÿGÍQ~'öA3]­¿iù| 'Il?å+<Go¸˜Ëì"gé ô¬N¢áí)7Sý"UTùWµdaÖÏί_°ŒÞßÐ!éägÒ²=¨ÚÎà^Àâß¼€K@­kÚÔ"uþTCÞ3£=ÄŽì+ „Šè@'eŒ}$6$‹vPK… #Ð[ô‚Ç3#¶n{uˆ `f°Ssàu @©ŸIt¬€Ò$fìHô$랆&ÌúþåÜ63jü‹­í1LŠåöÅVK{a jáší‘Ú mœÔ‚ƒeÕ°Ágê$ôÒ}m*è.P@ %è3K7¦—FÑ÷ õ+Y&$FzŸv=ê}T)´€6¶ìáonÚùÙœ`8$—@¿o{rÇ¥ ÄvPÆ[Ö—O²=Ñ#ÔOX¢ÀV— LQŠ#,úŒ*³‚)B‚Õiâö…À€¿"ØvÄä¨1‹§¡* bqôc*4 Ò{–öhZ3bìe¨Ûk¨ŸË£<éþ¶Þ6ÛxÛ~ ð~ò~ݰ}/òO Ñw%O¹ŸÕù s0}‹=RÌz”§Ùû}7Ò FPyĨ<êQ}<þ_ld{VKÏÜ»Yû³ W¥¹ãÂÇ­?¶íf9ð³§¥b÷¡Ÿ¤þOÓ”S¤\GýÆm„OL‚nŽl€Ý0Ζ‰) šB"7†óL•èYõ~~0}‹>åŸrÇìÜ€Ýl~Ë>Äb~‰±$ICUl™qû O ,ÚX饭µé±'"ãi›!ä±³ìYÀ(,Ž9©#÷f·löoôé¼°Ø:j²®ÎÞö}–?k´úga,"+cö¿’ïά~ÖÔwHQeÐákíkícç[´Ú*Š™)ŽÀ,~Ö¿äûIé P®vŒMÄ…C€Xû[¶×ÚGíwépØ0jkwÙ_¶ÏËöÀs…]6íûlc|{CÊjëgþFoU’•Žþck3ýèFÞÍ"RJ*’·ößßìÙüþÙ;ûmy*Isc7ÓA1pA›ÚÌ\I+é†u,ùìå¨éÆ0u)'Ú:`ãuå)}MºÞÑý˜0ÿÛÆó¿àðí¼o|wÒ½ÓÈ•”}ÆNöÙb:ô ®æ§òZ¤£T0­åÍ€}ý¤*ÕíÔ/ }««Í.¿.ØÁ0{ÒÃZBAÔ‡–0ƒ>¿L€í†©eIÓ¦#²hè¿ÐèƒHRHnÉ%:6c¨S¦Do$hã#äàuzKg‚ä-©o#lj~ÞÐábè“Å£„P`:ˆÚ²Òûc ›í•ÒG’ÄxÿQ‚k~ŽÙBükmbëù°áÂÛh»¿Ò–L€‘û$È´Ñ:qxÐ?¢ÌXUpÕ bý#ˆð{^fÀ²7@\Õ`Í_º›dÐma ª©;Up ø€j·± /Ù†`}|Œ0OfÅÝÑ}Ìdð?¨/nˆýH(ûµ¶}E,ºOhN2¶Ãiå ´£ §re¤ÄYÚ0ö€2‰LF ,¯Xbck'´NnN þ*ó¶–Ú[m¿âR×å¿-·á²ÛÆ–ÔïîC±H<œ0_ý£¯”õÊúöt:°Ÿ€v±obÚK‹Tf]BLIªŸú#“À/ï™®¬éÿLcc~à²ãŒÍÝ nXHZ&£…˜‰I1!üÌå ÃéÙ´z^#Ûq°E†÷¡t‘ôÄDeöÅ:MVâ‹Ð ͯ]øÄ±Õv ±ùH0GàÝ~höÃ÷)éPÇ«O‚x'•„J ð]¸ŸàŠesÐ?F$Z6 28-î9.ð©:X±òtC-a//X——°–@7B›DŽ­d6X[¿‚ü&‹<•EWÊ®«7hõ ‘äGD“8ÎKð0€)èÀú=í¼²ì$2ªõ¤¥sâb…o7v¯7òñ)¯ô‡åìðŒdêÀ\°‡õÍóï±h`ÇÙD8n.§bˆìÈg(O€êMx"®ËémÞÔû80B&çûmã5“Æ>,î¿Vƒ2åÑ^ÕxL CqöE¡Ø* Ü#è {ÿzª€5ìˆ"6üˆ’0£@RÎ.¶2"FuT€£X8¬ÅuIêíuu'AŽ1‹":⤀ˆOü“ðÞ2>[Æü5¶[yß‚Ëñr7=ˆ| ìæBzry?Oìºÿ‚ÏZZôçÑQÞÍž¨ÏØS´“èɘÑÑuOá&ÆT_DZµŽaÁ‰aû$„aíœúnÞð>Ò>™I/´RàEë±a pþWþ  Œªu 8ØÛ¦]ÄØdÃc¹"€Ò×uuD À­a(YÞqÉ8¯…¤çö‰Hû”Æ\»¡¡g¢ªðδ$ †ÄÚAÑñ¢ùNÕ¶ò¤0žUX(>¹K’e¸j•ç³€Åo’. ʼnÃW5œù†³ú†Ë5Ì載׃*¼€bwŒ¼F~öf@AÚ «pò:†ìïô %_šäMþ)U}'žpv/ òðh‰ätŸ€ €¸¹ÀU‚¹†_SFG<#(W1ìü³\},ñ„N3ÈÈV¢Â xëFa¡äIhÙË>ò_ÅÀ hk¸§¨• žXˆ.‡8ƽmO6†Õ%õöIã ePTÐūԻ J³@µiØHѦZ`–½‰`YPXL^œ·Ÿ:Çv¥¶ÛÀÛo;oøùëo o+oÃe¶üèÿXºì%¯þ”„§ûœ>± •NÆôþöwÝK=§ê~ÎÏ×=÷©lãôÂÔþ~°ÒÏGúÖ˜@ýÿ¦Å€A$ã>2bˆã rb®ˆ2‘Ž$xiFCKeh\n˜¦Au²Ì"ã&ªN/hFöY¬emÁ¿±ÜìM¶Ÿ ùïÇmã>[òÞ~ øþ¨z+òä<’ÿVzˆƒ7Ñ]…í).Óôm  á~Ÿì,# §ê-]'š•ýtMo'Kù¡¢°WÒïýOØd§ËúB}?®ƒë%õ˜(Eþ–@÷?¸ÓœÈ~KÑyk"¢X³Ù8ذõ&28HR&¤ªÓiìdc5ÒÃÓ &3 ¿ ÐÓf1œ  ®Æš  †ä”cÛ!\Ö[ZˆÅ^àú“uljì­f‹´7Û¢RVêŒaÙ6ã‡ø—½1=zXÂzÎà'€»®Áú»Âv§ôˆ}PÔȈ=ðpuµ'?t{œ#Ò€à0…”ä% n<ì†0ƒáeİìÂ5ÈjtQÏj«ašÊ/cꥼrÏØ†Š  ]ê[#þcOi:yBí1€@²ÉåÌ×°b½¿ì“·KìD‹_¸¢Ï°l(Ù²H-Ï+ ˜ë¢À`Æj6-"T:G¶8e*H`¢Œž(vYdaƒÈ7`Œ)^„aiÓ°±Ê0-lq ÞÀ]$1£@•SµšGB¿”Èþÿ xÞ4Ëm¶Ûx׎®í»-~äïó¿>gÓÆ1­Ÿ“måÝ^wäÈ…eÂ}ñÀo¾¥}çóZûH ãÄ <²½«i{xô(bÃSDŽ1wååkícéÀvPaÐHj=_—ÔtöÚô¬h€¤e¬$_Íý¶~ß¶óóû3ïþÆÓ Ÿ°—a8R€t²Ñ÷ûxdR °œÊQíéb C7gC?Ôb>ѽàðD@ªYú”šD=úh>¿Ž%m®(2­„à:l0(àªúS J‚ OÓ#a©=~¦ ýØeùhI¡1ì )h‹¼‚ÆuTCC ­lwÉ ‚€&™tIgìíT½ˆÁØ-Õÿ«Üj6ÛÆÿƒzáç–Û-¶ü8m—âÓBúÅŸJÊö¯Œ³é:úLˆìI©_h?/õ?dfÌRÕ'ð$Ÿ¨¥ûavhöá8±¯pz(9ºÈe‰ô‹ð¥ô2ƒ¬×¨G¸qòŸÈÌ)©hâ¿VvHkôGñ$ÒdíaÌgP»“K±S ™„åè8Ö Ð–8ÛmrÕ ùNÿ¬Ê6ˆ.¡èXj£¤s¤2ZQìûU›_€Ía§ùX•m·#`gaÞA©¿ÃÂÛÆÝ¿w Ç™m7-îïM#œ;(G?–/¦†[pµÁÇÇe‡kêw@ýpÜ9ôÛk&ˆÞ$t[7FnuCÓÊ!ûa¶)l69½‡{úP$® ðQ+‹%éB@G{aù®JU¡…lñV0ó$sÔ]°GÈË@Žœ'QâÇÕ¤ž¬5 ]nCHÁ „¬ŽÓ@H!C`S¡¥Ú²1"->ÉXzÿuìOžtΆÞÉ—·´ùÛÁæxmž4NÙxÒ1wö-©ÚI~„qXvÅú¥WBæØ¯Ô ÐDŠ}2›Ò$E”:KïÕUèíÐ?ù1bt?µ ó0øÁ]?Åù©-÷Tº@ &Ý:esA˜V§UÕûX4·P!ÐLÄžã!š1Ù`¬ ÃÂpè¶Ð=¥Û¸;¶Á…‘vvÈ‚H¦'Ì …-­–5 ñIÈIè{'Ʊ8 œðò† ^HÀ5K€D½j‰üsM %RÀ Žã8܃ÛÜhh@)Ãhƒ’m¬oHÙºÅ*ˆ3šz\*êA»a¡ú öºBÂg숗QÒÉvúkpÚÀu~‡…ÐÚÏ *œ¾þÊŽØl·¡ý?³ªþ µþ×cÛ3î…ÀÌéb*‚†> oñd}Ú!ˆU½ž—p ¤Ïx1÷`së'GB˜Yèu Ï÷cù÷„Ê Çå$D бϤè< A@žG”:Xk'z\gÁ‘ú0åMvï÷Iw$?4ðÊà ‘ üͲ ‹ØÕÔe©Ž1›ÚPâeØÓ]5:9ŒgzjÉ1nƒ£d¨MŽî&ºy‹«¿ÌMIéà·âñ«wëñÞGâ¶ÎJÝðð¶ÛÎÞ?î%ô%ùû[ —ê“í¼_z%½e—®Ä9ícJ£'пï©2(ŒžÌ3¥þ–ô`.ߨÃöHýáúìŸÁ1𱇢È`Œ³†# 1©»¿ÄÚ0É &xŽ%¹ÒHÂf]ǰ†GvŽÑÚ:±“6é¨ Pf¦ ªÄ "Ô >ªÏßÏ•T~ ‘<+¯xGÔ@5C/œ~„rêðºwŸßÁá÷1 q¦Á”‚—eFzêšMaM}YÜû -ÖÁÑ›ÝџܪÝiU}×…á&(:O`ï‡aM樑á×cìhš¾òðk^Ä ¡× §è6b 1è8òØt/I×ã;®ß’F`¯½ñm†ð3`^Õè8K€ªë®À`µk¢,–(tžžˬÙÿ¬’…HÄcç5C`  áëâþp?I 4õ-þ"HNÒ@šÎÅê|?³Ø‰@#@^¶]p,†8šNºš«*ò&ƒ¤[$z¿*4 (#ÀkƒÃ–äö@™$ ½ºÚuˆ‚¼іñq{úc…š3¾Û⫼>ˆj,i7y(}dº·,–¨ê ¨c!)Fq¨] % J>˽‰£j(@&%Û³JÑSxº þ`—à-²ÎÛÆÛoÃÀÛm¶’œ‹m²²ð²ð5Ì¡>ÒoèºsÚ}¿¡„èKðÖSð„kÛý’Ï"I~ÿ±“ùý6©Ó(ÆR¿ø23î~¤ÆšýÀ°ßz‰:D,ŠÌ'è„¥Ò ò)!ïIN$‚xNB3¢_µºd™d:ÏEØ·‘M…Ìy‡HH"Úaµ·ò)$xÈ,ã,²ßÒHA†,ÍœAGá ½s+dK$³».‡î‘5àð¡’‡é€(´ Îe–A9wtÂ;èú$d‚êH „´téÈ—/Áš¥ŽÈƒwHx²K$}C‰&‘^Þ7[,±x&ÃÓˆ$6öHñôÔA˜"ÁÚ©U²ZA@+è61 –3mU’”-@É £/ŒÖ.zõPHz™@r¹j»#~¬èfYéXi)@[V(–N(‹2Ã軃jA“­whÑg Õ4aѰR0˜- † ƒà´†b¤‹ù›ÂT·‡8$xïóׇ㰖ͼ»+ˬRf,{€>â¯C gЧîß`Ø?àÈüþ˜-ºÎ¤õ`äb]ëYKº·öYŸ4H @˜„ÚØNÑ„;/ÂÿK¸þË'ÐJÂ{ÀûY`(8OUÆý @j+ü®°9¦]‹rA#Æ2c¡¥PžÎ:E뤿‚juÁ:OÚ±÷¾úžþVý«~5~DÑôÀQ ¤RR˜M’Í{˜w·’%Ѩ¶„(DîVG¤Ù#¼ 4`HÝw#äB÷‹ð¤‘D‘€ Œ[! €ÔÌY !&$胸¸Ã8RŒK¯ôFKq!N *¿„eâz>ÂB¦«7›ù_ÔÏL-͆[v(xS†bdGÆ¡\]Ô¹CB04ÔÕ]U–*„¿(‘pMÕµNˆ$âÝj+C‚¢xBÄf—êì"ô6Ä÷&Aª#ªº¬€‰¤¡=ì‰?–ÔÚÛÏìlá]n*8àlL=j÷mý°]ÿ¹Ÿù† ëö%¦¾ÂªÀH=±îþ¥;÷ûØÔ£ésò´9¤c&wpCã‡áýÆq5~Ò©oÌFa`(FÐ((HÀ#Õ#zë Q™Ùè£"h]J à &ˆkR`•®€Œòß‚ðÙl¼°ÛÆÚ…­¥¶ó‡ü·ä÷¤­Ý²8’ãš…m ©T‘èèÕ>Äý›¦áæ ‚, íÏùëWcŸL¥ÌrLÍZ¢ð£ÀÈôd#r%â */qR»q‘éSü7é1Ü?¨EeIcïHÄ•‘A‡l`Ó'±&É4»±nòñ`OÐ@.…•Ðì/høH!Îý2[›c=Ûö?Ôš a8kÐOÕ¡ÐèuºH ~›I'Xû—æ_™~d³µÀèéŸ /¥ÊC4Õo˽‹ºæÃÑ­ü¶~åÛb©á&t (‡ÓÝÿÍ¿ù1ÿf϶ˆ‚@jáj û·àþí<Ýø?ÚÛôþà ”¨(¾ØÏü³m†¤EqÄaUHY…Å&ú`'Ô1êbôæ³úp‘2  š:Þý£ ` ° bñô6þ¥ÓÿL>ïé?gébTDDµ‡p ü×ò³þ‹w ÿbF;I«Ë1î1DOâ_°ýCv?¤ô^L¬À~ìCBIÞätæBÑk?«À%ûÎhÂÈ2+Ú™‰NËî#÷2FJ¨ºÒpö‰ÎèÏíí°A—D1tlW °¸à²o¦Ì°mF4‘´Ç[ðêÉ´ÕL$òÆãƧ‰ºÈq­œý©-þJ[Ðu–ÛÀ– mÓÊ¿á÷tcßÃ×±õ%ßÀiÀÃÓêÚ ÐcÓ6xoàÖÒ~¶õñ/YÐÀ ·ùl--ŽV-–f,"úU‹'è‘ú ¹l4>ÔÔ*õç¢t`¤qR9ÿ à tTÿ ·Å-H¦Ø~ð›™ÍÏå ’%å$•GaÆ ¼ Áoç$ ?”D8¤íÓ B›v"¬˜0ÑX›Š’3GÏpƒk­IŒ êb3¸ŸÙÓÁùï Ñ#8mµø½6Û…²³ÁðÙo:#¨k¤‰…Òí·z·bbÓ5BSE‡9x¡ÞK oþS³žÆ˜êß¾Ó%KõX>¶°!l]«•ʼnƒ‰«úM–Å­>ÔÂ\ƒŠ[ï‘ôúöÿ2*&ŸPF3vδ†œñí„O³k¸:µþ ÌuèòðMÑxÁ0ŒzŽñϽ-x)<ðžÉd)x(4hˆ§œú)²_KÁèbÆw˜¹ÓËô ,¼™ BÚhq¬DÃIlÂ">¥Àk”¾—v 4cu}¤äí©I ÏYÛt06 HެŽÌÔÎI1Å '˜c`öíØ„Ïd‡/Áã–ÓYÁ7¡Ñ“ –-”ª/‚f)XÀ­”Ý":il]Ñ,:ˆ¨.†Á 46ƒÈÁŒ;ÄŽÄ’šV9RAœBÍ „2Ö˜ªþ+Ù3‰Æ¼¶pcóÎwŒ°·—8Óáœl±é2Ù#aNžäOîvéÂÏÒþÙõŸI Smâ˳ ‡ñѸ-BpÒ=óËÂÇŒ?%öS¶¡'Ð'ím=Ú ³E‡ÿ,p‘}GD‘5µHѱA…ì‘5…nÆÍ†DÆÖvO)â'@ˆŽ¡eíE¯JOIw')ú½Äâ-Rç/RÖ‚=ŒçmûRÆÑΖýLÚö1_肬³ÓÕŠûø#¿ab£„;ë.ü±ô]AÚ&ü)4hdí,Ð:ý„ØyËPˆat+–Â,!üÁ…·IËd²#ÙýÇ)‡’ Ïly¸ŒˆÜ仿™ŒŸR±¶œ05#¸Ë$é´LsðàÓòA8~âºl]€ý#°¼Y‘Š‚¨î:#‹I×&ˆ2³»øZê5žgˆ3èR἟@ìÁ;ì3-™Ú¥7HàŒ6û&|r÷¯-†ÌFºue`-gÜWò1s‡öz×4ÓÐ*„»#´’ð$g¤d@ ¸2§BF´2¤ aš”jì AH:7qÄ™GØè‘ûÐ_Ãá‡%œwËþ´x[måxߎNK+/¢g¢Ûá¬)¶ó‘ƒ0´1Ÿ3¯ð·òK°K¢|ƒAz?S>ú‚NªdWÓôR^„(Áèbú kë/Û)埔”C´þ™R—¶¡‘äõn^ÀƒúZ?Ì 6€6Ѽâ°ä€]œd˦ DàÀ‹EÆ®«­Žü1Ùb>¥Âk¤áRl{  ٟŲŰ©VxÒª8‰¯âèÁ’èÉÈ’ï\4Ý$le JQò…“z’ÙÛ)ÐO‰'ßG3ôLЊªê¼$ m+N÷”Á`UÎúBFÉgBTÔ°cj±°Òc–΀;A–$’İ –ÙƒÑ?Ö··€H–DFðx66)žÑ&JxªLiŒÓççWƒrsUÞÃQ¾P‘ägR½"XÙyÌ„ÜèpŽ›¢Y?±»§áœœ$ƒK#g h$5]Qæ0`=¥©Ò ³¢Œ‚¢…1î"{ “¿,aŽ»{C¶¼ë¹üq¬ÃþMøåpém½ãðÎw4¶^Kaì´Xœxm²©iÝ\°Uè•î럙‡QgîÆ3õ_ÚQägäP'0±è'g„¶Ç%Jýjè!OôÊ«©7Œè*–ùîAôDšFHëÎæ!ô“çHzÐ1I è´Ÿ*BGV¥,±,åH Ô°@‹Ø£_‡ ;$ú^ zaÜŒ&cÁÆI„Z&á¡&B ä‡1)Þ¶@!}B6…‡dôr3áÔ})úýWàBËò¡cd+ð‚Ýd7ý‘Q1 I½²QŸMßöV]Çý¤†iˆ$¸^ #`Ã÷¤ú#þòåð°ˆG‡VÇIÌo®5ó W€j.2€“m€{/àªFp™ µïáUSÄ®ˆªß÷Æ1KïP‚æ}§Œ.;LɶUÐiaC¨s°?Hßúcêþ›­AÇbïúb èmäŠ8Í$¼Ç¦þ‹'E,Ÿ+€…áØÐ¤¡a ‘Ñì$àDc’ LFŠÑ20ÐÒ)hX˜Kf¸à+ÿ1$¶Ã…^ÞXgügr6Ûm¼õ¦¢FitòŸÛ`ûþ"H\‡ARTÅ$4º§Y÷ÖèfÖ'ößÑ„'EÿÔ£šÁwRrõ+”(ôâ áÕ…UÞø³5rðŸÐFü'ÓŒ1þH2Å„Rï"q³¥#i"æA˜Èguß…áÜ¿3.NÒïË5eŠ ¥@À/¢Ð=¤»º{wOù3†®Ý³ØBŠrN*„Œ»SÕ‰´R÷+ëQî õ&>á~f] •àVŒÓ‡±î³§ ÛÓªr¤_+«Îò ír} 0ŸÂ`’ÅD_¦7¾ÈþXò+:y` $DÄgЇùR>è³·ZW½‡ÍI+§ðÕµS}êRïÿ+ûìÃ>[NzNÈDêËpiÜ%ÜÈ'2<$º†¬é›t&`¼–Ë d$UÞ—â]»Œ¥öŒ—K¨@¯N3¯JÀr>Õ<"fŒ}j~–XÍ_‡„ú[ò§íZDc,‚XaµQÈ"ô^D5X÷£ •&öJ]—mÐ%¦Œ£:_È:L×L N2Í‚Ìåÿ<‡osðιMyêö6¦cb½ŒÚÓpRò@d)=ÿðH=3>¬7S;=X:`lÓðÜè÷+ûa!úLýF>æHi­¢~ÃýËйO $8‡€K ðØ×@™‚Ñ“ [tÆw…ÔÅe&@O ¬="@tP%ø„®Àý»y{hc´v5aXÛÎVa!Æ • 0Ê]âd;'N¹hw!ßPµ®Tè' Cd4I;ƒW“ÝbNáÑ_š¹Èm‡JÁ¯1>ߣdþ#}‘Ÿñ/úK)øø_«ð¿SŸëŠý?Õø¿¨ã×Ï›ÿ˜[ÿÆë—ܬß+ó/½_ôÿ@GÁ?òÛWß'¦³š ìméy '¯Ûl÷àŒ#Ù@ÂTÑÌlŠÀº€Zÿ–O$ÉäÆß¿Ù}&þYA1ãb[X·†^Û.÷±ZƲ“T ,6õ òÏó6b“ëFËè:Mt‰¦¯BŽî*¾‰¤½c¥‚cÝ…± ‘í˜ÝWú=?þ çNs™øm²üVW¤ô’`Á:H{<$ŸùŒ°Î†æÃ ~¤¦¡'v :¶EsBD¾ŸûŠâhóÙý«,˜‹¬Ý%Þ‡hùH‰’:'¤T•Ü€F7\t–¨èüéÈÆLtØD‰Ñ¤?wdÈÄÆn²+¤0íI4\Ô'³€³‘*ðläp@,ž]$ º¼¯tôADûWG+ »4sä<<í§ä1à?Ú˨@ìs v¨»‘OÓý¸(ê.4mdv,躳¤“€¢Ãùƒ ÐrGt%1%Ò0q“Xp‘/&ŒF ©u’Äô9NÈ´gBJ˜0ƒ¯"E§G¢N0À”˜+¾ƒYÀFh"jnMÆGOÀÖ'–ÿÜg_ã,ÿqÜp¯7â @t¾bËm¬0Ö=(‰ÔöUÀ%¶ØdëÖq’Þd°‡˜•}„áH £8áp>dúb†4²;ŒE´'€~6C‰ ÉcÞ¤!f4‘tðÃN¸ØKY£j›:õ½‡$ÍPÈìÀ÷“p # ,"ÀçyêÞ{ç ËI´Î,‚e¶v O:!ëes0 ‘Ý1a&£¨Lë'¤èÊæ…¨‘0׃­%5O÷ ޲zG©òÑnÊ[7&Ï)cèM¤‚è—Ø°:ÄGz^VF{°„Cò¿ðA CŽI €O«°Æ¤,“™%äÆÔ@»HS܆,ÖzKòŒ±¡Qû˜.ƒâ™4ÆA¸šSáèá Àã_øAÁöŒÉƒ] ‰ …‘'•I¤0‘öBwQX«(@8pc`ˆB `jûZ ¤ŸÄÓßÔþ¤`9c„>ź"êç¶}#ñÎAuŸ‚ü^N3€wŒeO°µIô%§W <«t}R:{*U—gÇ@D´Ü â¹›åî˜@ ôDRHS6v¹¬jj„ÉEIÔ‡DJìp#$Ø¥à]ƒ±+‰ù_¶ß|j„^–ÉÃøTEÀ]ÄÃüCeÇêüÔv4¢™>„•¶‘Bêê= ôµ¢ùÙ0ÜìqNhé% aظ„éšØb0éˆà  ov ûdE#v1¤¡ Ç.êÿ¨ð¼­¿ãËy8$‰ém ’I—{øþ²ÞðB<‚À#é?~Œ-é2Q>H^Âm¨æÊÖ ß†þ‚{P ƒGÂf%òT è–ãµ%ëýÂ{Æ}ŠTJ5ÂÊ#fŽàÿ!IFÝîFÛÛèÙØ`ŒèÀb½0ã9œ£!?‘ QàF”²1—. !ì0Ó’ÔÅx ld‹d–Ó-Ã"×=â ãû‘€Ï™u¨–'ÐÚÉ1~æÄœ6·ŠÂó)¹ûÇ…hè/,¿•¼ è[É=Ü: °YÀ ·¹e îŒ2à±ïxŸ±øÏƒg/‡¾wøçlt&ŸáK0€ S ` P|»P!+ðÆFºê~²†¬ÔS†Vç±Oñ±‘˜)~Ä”BîZ¨ 9Ê?†©l`ƒŠ&"t‰(ƒ4ÛØÄè¢"(‰ˆÆ–Þ =މj'¦LMpS ²X€{ *h3q$ÑL€év -±ÁévècäÄŽUeÁÇî×ì!QÄá®íq„о°áÒ$Á¯"Œ‚B„lË$DGxУÁ"Gd¤cÝÞV"rwžˆÓƒ»Èü=ÃÉÎñ¼o¹YN [Àsœ(MFR±aˆÈtÚɧ`g å.‡IþäeyA÷"9¥ä;9-G¸È}¡.MHRv…¨>u'kÛh;Ô€ú˜G¡’&ÇsEŸ¬‚@ç•íö躲i ùŒNÈÍž ÚæI.˜C¬rMx$Ê!€q–p“dO´†V´€²É$ˆ=¡.–ÐÉ–ØácPœ†IzÎË%ŸÃ!ÏKóÉ…Ÿº~!g)þ.÷€'â T~Ô÷øì½íM$àÓ V`¨Z,eWfûdÖgŸº #ˆB4~ÛKë¹ÿ âI÷d›—œÿ|÷’ÅÔL­;¡—[¤ ™Ù¢¬94Ð%‡²òdTËQ:ï M4†Ìƒµ(–èH ˆDÇ¢sÚÆóŒ4hc®½„ôò ‚ "6\‘é´ã„ µAý)8[þšR'"!¤,h þ ©î°eÉåè°¡©è,dtDŽ–CbŠ#:sV‰ÎŒK€o+muBÀ¥ L7úü:3™!²Ÿø±lÛ-¤æZg Û)Åm…Ãʃ5KloÕCV3rãÄ zVìý ß¡?…$x× ðÔ‚LmûŸ“ö¬ß|P)ê°Ú!–õ=ÄðØ>Òb »0ˆCòžÒ_Ä=Úì&'Rð¤‹$MaÀ]—sÃ"»&&ë‚Näã/vŠXXÕ³¸9ÆZ Òë-#Ùn¦È@JI¨ðC¼¦Ÿ²ß¢ßaý¥ €+²Ñ–½àéí í¿î,Ã?)øvÿÎ!õî~ÈMjjÛMz×OHÁÉ÷‰}ÿRߥý]´»:ý¤jïèDi ƒbQêu/ŠƒT)Uê`Ú6k°öJ†Øš#; "ÆêvÅ=‘‘äøí¼l¯)jÕ¼nI™»--IBcUmµÞ7ê³Gä…q°Èg²_BG¡ˆ¥±ì³«(sª DEnšD¶û Txb5MžYqûŒÂ^ãà‚MzᶪÁðRûFõ&B2EYXzÍ“³2¶£Óg[œM¥Ðw³÷±éYO;ww+‘°° Â'£x›Ðê90õˆ H8/âÚ¾–æUìZ¶ú@Èv ¿áØ´ôXD衇BXÈLkð_x~ì}¿eù+Ò³í²ÿ&7¯Ý¿"ü«¥ÕûoÈŸc¿6ü›÷{W>Åkæá_™rh9š¿*|ê¿:ûÔ?C¿6À,=ez²ÝpA”(™ÑÕ ŠØC¬PÍhXšÀý%eU¥éB=¨`HnûûØ‘º@k£09³q®‰PêÈ]-96_v°+,º¬„—/ qŒƒ ‚¤˜I [$l,`ÃÌ‚âB¯láÕ‚@DIAƒè¤ö°Äa5Ö qq°`ª„GôÀð®ävzAýôÂvß–œ "ÈZÛil!5ÓÁÓ$pjëÂʶͱé=ƒ ­´û6(b<"l¿H¶=¦Ó€GDÄý@»§ÕpI²WDHM7åVÑäl>ÒEâ Ö†ö'Ôq "ÆŠjk&wo1!‹ úg¦öÛ›ƒÓS´]+ˆfg¸sô@z]Ɉ ìn’0B¹Øè‚QŽ0Ué–=O°03×±÷B… ®ƒ->¬sKo{")n¢‹é/RZE ÚÞ! ^Ž="°jZ° úP‰ZÒ4’å•m³]Ií–#\eûDyË–ÑE°1eñF\¤ØrÅ–6¼,‹Ó ÑlÍìBN”kŠp/imÚC¶Ë#- aî,ƒ¢Ñ[ 59†Ù‚™ï«vã.(.3ÊK0 eÅdÝRö˜Ú3XL9 âÝUK6n0p¬qÐÀ‰¡¥„7T„wŒié ¤B¨²¬øK3^c=È%#@ß`БÃŒ¢J!Óq Â°¿ÑSI$…‡´ÙcYgâ(MÕ‚,LU«ÊÛ/,CÝbyÒVwþHß™§ÀÚ›‰¬—nÊ*bNÊ&‹úVñŠÐ´Ñ°æ0$4&Dõ0€‚Kè‰ÈYmBõ™àö!Œ·³ßâDePŒM‰Éˆø‘á‘Õ´xa8ÇQ$ê¹Á!ƒÙ&¢'bYŠÝ`e’a&’ˆK-L°Àx6 ¥¶7’ÄiyL,ÆÉ¾³ŽèK£-5H°± ´Éd5O± 3 ²Ö#„c»T AÁ8*£, B½)“Ö# 7WnÄ„Ld='PŒØqÉÀ¤´vp] qH70âΖ‰"&N0°Y7RC8FîÜv$ ã8Š@LlFQ‡Lšd‹#4µb{OV`ÂXÈ! ¦%°:SKQH6 Ô„w Œ$ œÓ1Æ$€Úƒ’Ž4D`F’Ìi!_ežD0ò Ö¨úa¤F:(HA è¢Ê’ì`:BM ˆèÀw¡á€IŽðlñ–JKg$ÊŽÈq"Þ­·ƒ…¶Þ_'Là f°àÀ b2§¸ÙôW^ŒèöÅÝ#ÃA°\õþÆÖ3SÜìŠRP{2½"pSÄ{Øm’Øâº#ÿ–2Æ IÄ2<É»cå!ÌIÜd(ã"¶@"HŠ±Ó ê|éÉ6IVìbHâX¸Ù¨QI\ÉÓË!T Y"–FHI’‚6:®ô¬)&¨tHØê€EŒ]…p„n×Ll)¦å˜h½2dj‘ Q 9¤>–‰ºÊ„c`#–Æ7B’ $LcW…ÙÒ1€b–dÂ,’ЄÌl‘KG$HHxK ˆ²1aí£&š3G¤„pmWD'¤)ÒN=’Dt¶}Œ’"IЖS5](W‰bHà, K²40´3C@- $0—d+ø6!c˜–Í'Mt݃,RX,z-Oà¶@İ0—F:0“½-³Kn{Ðþ8›> +jüdÛ²Xƒ‡Œw-8e²È%³[YI¢,ûHéý_À„z–Ùû™åÓXÀÔ/J6޳;þáóD´@)³p9é!¸Ä Ýb-š» ¯Kûöpü$á–¼#g ² ™#²06q«$rFmL 8^:80ƒUv;. ­”>X0§¥´nÂbXHO€Ã^i¼240z!×pƒ«f¾É}–kØÛ3OB–.ô„mþ¬–mª˜Ö‘;…DPEè,`Î XTM†«:6b[†8ŒL“ `› 8‘Œ1g¥€F$4Æìs22HF×`7€ËôÎCj#I@°66ŇٰÜþeÅé|j¿ÐßõÌúœÏC_¿õ4ø¿ÔÇõ¿QçÏêÌÌþ¯þ¥ÿ^?wõšßÔˆ¡ÿÊG¿ô¢ú_ûoêÿî1|›ô¿èÿ{ÖO¯õ—äR½ãüy×ý_÷°?Ü]8þùô/óÿ¢ìú€ÿÄo¿ýŸÅùWëþ+þ—ÿ«þZ‡»øó?êÿúŸ{úgØ¿Æ?õwôþˆ;£ú§VëfÛþÃþÉ´Ž’¤p—b ­ØWH·  "ã* ÈŒy šc–‰±…¹%½-Hj&ÓRX¼Ä&Ø S6GE“(‰î̈š°hÆp °"6&Á†êÎnŒEœhLM[*ä‚ `R!®6‚)'],‹ZBÈ€FHìï„Ý]]ß¾>j.=òT⢠Oc±~L{W ô°¼_•”ãP†ùæ ú¸†üyCh©0tÚG‚mº!lS´'Ò„¹š‚ý_úIÙY¦ºD#Òœ 0QP:Š€¨G‰’hïûP6YÙIˆÔ—0ij6 ²H¤½l˜Æ2}X$i ɉ¨²dÖ L‹@(XÙ èÈ%AŽÍ„Y6™x&q€æ‚ŒŒ Ä‘ Fì“aÍÛ$ ßNÀó7ù³í[l¶ë-­¶¶ÚÛk¥°°ÚͶËn™a¶m–׋K¸™m·a ìWC *5Û³E–³F1Ã;‡Yé’èYnЇÉÓ¶@ 8 ƒD¼ÑØ ²""„‚n‘ˆUö’®1˜È¾¶Àˆz#`ÑXTLÌU–ƒÁÒ¸È(–¤#É„1ÈGÜér”{OƒB6DBt€®¬6ý–‚ܾ,È$’Õ«V¤ed+,dxf-œ#Çd5”ðU¥¿“kâ1ºº—Nã1•¾D_› øJ|bó¡EŸô–6'5€½:%ª»¦Ê RN1ïˆÂkÓ4*úeî´É!¨ð !¥†WXļ-†D䳿»!…Á6HIïD&F‹(à4‡XÎ.„¢;¤"g»P`] d背›Ù(²éb(˜^äYfЬt̳%Æj6UC’&¤œ^\þ¥«ÀÛkÆ÷ÆÚñîÛ!Iuáãm ¶#7»>GžPXÓiŒ0Ö@Y*-ºâB + –@ ² 8ƒREš%Š2¤(Å€eßÊN £ Ž] Z!8$ï2} !¬ 7†’ ³CŒ¢Ýâ%Š À Ži%Ì{ô :Ä€N ébB6Š;ÅXv b héØV3Ù 4ªsEÚM½,gFÃç-ŽiÁžáànïºãÀ6òLlFÎs}j„¸w²ÅQUr:eú®¼Qº€‰À}ê=Q±½ ßÂç"ÅiÝI×PgŒ²¾¡÷ÔQŸ*H<)#àèOc,—AõÉ£D ö¦á*;-¬1ž,KqCx$» ‹b–u:¦ Ná2tu„L¼N¸êAć X̺”£Œ‹q“[Z·bqYRj¤`K4XTÉ6¢ì4³ZKN’,¤@eFX¡¸*±ez.Éö¯\¼Ÿv3!ã{áãy-ä³^ua>[ÑꢄÄd×Löê)H@D°é-ÈFLQaÆA€hÉÐ'DqçÜ*ì» ÐŒ)g1'¬K1ïÒdê˜RÖc"„ô#VäâZèH4·ˆ„‰‚R#AÈwD ½$°ˆ€ P J£š0)`(¤¤Ä'@aåI ™eðZW=•Ì„ú6ض H3ÁŒœö DƒaÒC|@7d=€Iúlv…;…Huä@B¸ˆi #*·µ;[ƒ} ¾†E¯b³¦ð¤¸nÙ>W)!ÐÆ@hÀн‰z00H´ ÆÙ…QFAÓaú Š`Uòš?™ŒÉXQ—ƒ³‰§¡Î¬" "7kRTayXÀI á& ™9ª'¶Y5Y1$CmFImD[°äš,™(#¦H«¼ Ú±ZÛ; ’‚d¨DCI1\¼o8ðId Èœ†–cƒ§!uŸ3SÁÎóœ$%ö2 Ô!'B@èg¶…Ò©pØŽŒÂR‚+˜‚#Œ °Ð /D`"2ÖF°nÖ–éÈ^™ZL[qD!ÕìÈ0쀎6“ILbB¬ À†Ä„è´Á°bBë‰jhÕ eMŽ ­‘à‰@ÌÄbâ̧cðÙÞ™01 MèÆeãÕƒ3¥]v½ÄÔƒ4e‹»aÎ(+h`ï›cx0GV-ö¢S°¬»–ÍÒÅR~ŽH®¶^P&£N¦%st6ƒ¥•úev±„È}@ûd Ä+º°õbãÅI‹Ë²M@±U„˜Ln±c;7Y#Úl1R2fH˜·bíXÍFÃp¾Ì*a&£1»ŒŸ!°‹l€ŽðdÄlL°¢„ŒØÛø,dL`ŒêX/ÐÝ ú87àpDðò)k?C~Fpq‡Ä,ã¡ç ëò×ÖÎhBžI40Nè…°*3DjI!¤;ÕA XH*³ ŒŠ tA…@˜Ú¸èì M.ÆŒ(.•PˆÀi4#±h•‚8–©²–¨ë¹Ù’Ðd˜FÖƒ"†˜ˆ„2ulÁ« †í¨H…$ ¦Á`˜~™BÁ·4®’£v6[D‹v&nþ &Ç€ 5¤gµÞá—¼ž‡&k'±»Îà°^×Nv0îŽ À50ÛUTaGFX¸¬~ÈÂ@™wwð8Òc¤“Ü8ÏÔF HcÚÂ7?¥¶ S|À¼¡zbÛiÐ¥,ÁGMõ+Ä?ÌÌê¼ÛŠdš 8$Šì‰!D‘Ô ª‘E гfJÍXTÆ 1UwÙ„xla *cjñ‚0k¬¨À:±èH\€F(-2DzAlm²C„×/WKœohE Ümí½D|xõÇKðÈ5» Ùó«ÛÏ{ξså–p@}á=0!BO!µe’/r*~ À ð h ÒÁP讈ĵ²"у#ÒZ˜ŽR1º0Œä;“A!GYFZºÈ¤"#"6hTat:E‡ È®”&2:jb°0–‡V9|Qdì ×€ ØÓb{%–ë›éa8P“Î-zY¸ƒ;a'ëÙ úHS¡ àu±7¼+·BRŒÔ[Þê|0NÂ÷àVV’# îìG!Ótˆ™MŠ6j~L`t$~Iæ“÷ » —àǽ!ID€4e=Í„G[tÒ]š§ìF›`Ù“Š ÓWù%ÝÄÅTa™3mý¿ïB H¥®È¢Éİ7$G¤ÆÅ@ŒŠkƒXW 6¸- ¼Û‹»]BNµ€À,Ƹá bÀGœfİÇhXH°±›Œà£\†3™¥¼hXç?í³»ò3ÛÃÃiÝp"ÞrÈE>ò@HLƒ Q¶«EÄ…eL/FÄ`Ù‹X€Q]eÞ7MÈ<f ¤‡Q1ÆâÏl1‘Â1ÓFDó2r>™ã X˜°Hí ‰8Ïgc${èH3 YXÈÕ.„`EPé…#²4²N !€`_iaÔA H8ÄtK)ØÎ€v@ô„«¬]Ó°†Éúc°Áˆç@…zP,4e&Ï€XAܺdKº6›kå-‡”°¡Û,øòYœºWÛ F ]$ ‡½‡ Bw:yÖTnÙÞ.+¦‡A$’\!„ˆ,ƹ.Xf²‡ÔÄ;‚À!”ÄÖÔT°ƒa‘œ"Hë [`0cÈ0ìÈAEž×N ÍÞq”20q¿ íäa bB$ ¢amÙšg†>±ån¡xÀø{àøoø0³àÏz8…->¥ÐN•UE´j,8Ä@ ÞNŽÂ‚ê©èȇK1‘ À9*6 1¤Ldª,¯ΆÝ:c¤v‰‘àdXìlk¬,ˆÈÅä-¬„Œ£>ÃaŒ,ÀFiEXëK†Z²ÁdC‰…q#`œ!1Œ3Vൕ¥„É^Ëf·b „#ˆHQ’€gêÀ•àIЩbºÇnÆMn˜J„ÆðF޲€“oÂ4dt¤G¸g®¯ާr:@DéENÞºí‘ÍÆ~’z¡& ØÂ_p4²¿Z –_°H+7$æ‰kìFG¬?˜k¡` OP+R`ÂË'Np Fh,ÃÒÈl l‘QŸ@0‹Ì‘"±À3…:e„1Ž„šõ(ɬc°‰b¢DM,$8Ýš¹5c"@›h! 1(ÀµQÕ%׌í,øàäç8>6gp]õ‘Ç¥àÎ=Y1>ƒHT÷À,äQd¦,€K‘B2h)n¤X’Åå„Åœ] ÒM €ÊAŽœY Rg†Fi à #&Î"T"YY†0¬dÀ…v ä°M”#¬dÂtp`t³êFÆÏYc¤" H·|2¤Á”„•„4ÉK{¶Yƒ&†%+„¤`°<†ÖÆ]b4ÌêJOÐ^‰ÀÀ‘Û*VgbÁÙ ®iƒ‘ä+ xÂ+¸ZÌYôE°IÙ¤±5ÙÚ%ºÄe5 OÂL±} Žã é‚ ƒáÓt#ê íè€ÏL1=CcBò°Èqn)Å5ŒÆöÆ$’„±˜Bc‰draé!xÖS,ÉÄÂ0uœl,dW¢I"T!±€‰wBl¾:“ªA"±‰¸ bTeØŒ¬TÛB‰Ò·…ã'…äž »0³˜ø? rC”máãc8+ÄY­‡±9#$&0‘ƒ´†„lTeC¥”°9#Â’1I LQ–XÁH$Á aÞt“”ÍJ„b•D°4ƒ˜Èf…  ŽJ »zŒ£‡XŒQ,ÓBJ,D`Z3tΉŠG‹V`†:©),Y·HñÒK„¤ÄÅ¢Þc·B˜í‚Éu"iEÖX*Ã@N4°&‰ ä[¯g@XšHšd–rhU€Q|ɳa  õ¦0îaüKC[£Hbk0–©ï@Ä’h¥îÁ‡ñÓ„k –8Àl2ë"wYpS:‰ícb}™î’ 2k!µi‘('©n¬c6X£"«ÀŽìtdá–1|ÍHÑR]¨Î§¹;2Ä·¬ƒ f0&ª{ŒÒ c˜3)ÎsÚqŸàØœã§âxɃà&|‰àÂ!£øn˜Ã»¤;ž˜tضE„™‘èHâ!k霕K!]j¦A!CÂ1¡MJZÄK¥¨#šR$‰ˆD¤‹n¤¤b†Hº'@3w†Iƒn¡HTõb"Y¢ZÌIÈ*$A-QÑ*û,ʸX2T…«=Û¢( vÁ«z©,X¤°­ÛˆdãúH'¸YñÍ› wY4Átl tX@ïc$@H‡l©cvŸDhC™˜ÀãB3ô/ÉqÒO`ÀˆË]ŠïF:Ÿ¦fÈOä  ÓkÔÞø6ö1ÑÿW€ÆÃwÄ‘ «#&.ÇQ”Kl„ÂM¼`QŒ“{Ýî°$m.¦‰…ºÆU…dVèÍ‘U€1%DÌ6U‘D°TZÍUŒRMÖ$t[HvÐÉÂî"#µµ€Ó 3Úõéäü=LÙÁŸp[ÀC<ìñÓðφÛ<ÆAÉíy{$f,8 ºa& A˜¨XBFCD´pŸ$¡0¦ô‰b€4И’f$ë Ѱ±©o- Ò(KÄI4Òz„±.ÉÐN­ tõv² @€¤´±ÌJš…ÒU!0I£(Ž )3É Å°'J?"“fZ@Œi ¶DaÒDzG<"Ä¢ Þ€ËÔ×Ö$%¸XêX®¦²fâ¶`Çž€2Õ†«Ð°ƒXhhãtl–ƒ%n«ˆb—ÐY$›#ºÌ oê»Ï?º/¨™Û’ű 'IÖ¨?ˆ ZÇ€³…(¹Q%Ø´Âë` ¤ÈC$Ua1 `$ˆ[ ,pÊ#cPE&¡Ç$– +6eˆ7PŠÉ"‰\£# Q¦Â,f°€, !S –¸0‚£cÚÆ*²0V¼6ÕIÃUøõ3œv¼<¬¼7DãggÃ9Ë5Ž5;ÀÕuÚÜÆ0Œ3ÃRI8Yc8$ ÄRZq,$é)cX™ol$ÄÂM%à$‰‰-AµVVf0ª:0¦ˆ0vAz‘KX Ä„$]é.æÝ¶ Ðdh”ÐÈàUNÒÔ;¸Ãz[A!$1"S £³º 8 )ì]hˆu´@[¤‚À ØDÒE @×$QÝgŒ¬×XÐ"i:~V9šÚîH0’RDA8‘žN» í—ÄÙçHØ3‹XÍ?Ð}Ï#&žI}Žçu_+BˆÜâ`ɬ k¸ë!:ت“Å1VÐÂL°™Ù%HàÂÝ$;N1!Å“]€…ÛY(4a&H˜ÚLfv»V»Â«]¡E#aA‚‚pm¬v˜É¢í˨ŽÒ -¨È…@‚oð± 8ì/Q~Y0ÈŸ ä>ð2wy.¦é0ŽI:n§ò6 3C ˆ HøI‰Œ1µ]Šd ˆ‰&`9Hé@“Q,€$ˆéH; Ô ‰fœö,p€RÕA±K„*´…I˜0:Zæ6°Œà £Õqa¨t’n0Ö\ ÂV2  ˆÊ“gB®¬èÆÍé²T@ö0;v’=¼EǦXâ°*Á· Øu$ Â1t­ªÃM`È„’°OT²¼Ãšuc(ã h ¥ºÒ?+8mCÁ''bÏ©¢XúHÔvÁí”°&íÕjH: 4% ƒ}'1ïGø€.‚^‘®’k¤É;X8’&°3ň ™b%BO„½rÐ4Z¤uA:,UP-ZÌ“ ´’%¨E™vN¢u+[ʰh° ’„® 0Ñ…é"×—Fk…aÅ …,èÂà ªyHÀXF?ËÃ6ñ»Ç¾ÞH9pyöðpÚ8p®éøX”eMXŠY£ÀõÛfÈ K NîÀ˜! ÉDÅX£„†™Õ¥¨²,3LH°¼e€ØY˜HÂgK\d™;a¸ ­,*A« u$äµt³–ÆàY(ÆêÂk F͆` B¬ â%¯gàÛ/'Ç#vIlK g/#3Îÿƒpã!xõW;ˆA±¢ÚãÆXì` ##D±á$tH:‰"¶Â¨H(ºK¥¡Ù`4µ†^‰nDÁq% ‰iÒK´ÝYµ£(’€8FÒ‹#¢B¬ŠÒ¶ öƒ-BZÂÇct×V„IB ÔK°BÀ1Ü;aЉÂ=ãA厰R<$ÞemÙ–°CÁa΄Y²Ú1a3$iˆÁ  ‚3!ˆË Úô|°¦­c¡‘‰ RÁ £`;€DÊ`Í ?F ÒtY¡gKc;x€ ØO6*$ê!Qpñ ã*b©Y#a2ÄN°Šô%Ö2ÍÕŒHé°Ñ¶ë 6ØëŒæZ£:$›€Fí+;`T*cÉP„÷àÑ… Ù 1“ !a‚¬A!Ô [¡H-áq€Hd@Ôzk`ÄÛö$½ð,†ð¶Ïž2Î0Ía]»ÈfË¡ˆc>;ð%BÞ3äHˆÆ:!€-DÂNŽ¥ aGbÊth1€Š¢ºƒb6vŠè‡  —’"HPÇ4mH˜ª‚ZaÕ²L0 à´´)gIGc ½Á$¯v( #",0’p»k` µ¦¬‚#$`&3& áv”ˆ`Hô`Û Ý2³`DÀ¸H¨F• ‹f4 lb"i (ì!¥¯XÉ‚ Ú"Ć"Ӫ̆rПxa |„ äðÆ õ0x30:îi'¦Ûü²"OjÄvÆ}›]ÿQ‘Û[A…Ò' ƒ,d†(È®–H¦ÈJóCbBz’hÆV*®æªÀ!Rk°… ˆ>Y° –Vl,k‰a¢íFÔuCÉ…Ô XRÆ -ÛW¦@0ª Ä…a ‹žãäà¶Éã§ïr×5àp»ß€¼d`A1§Ëx,rëàsØòGêIAãb-!-+ ÂÎŽ¡¤˜êÐbApBÜF.‰dA,Mwq1 E(/Ä9HÐÁ‰0³tJ˜2($ƒˆHˆ!×ìéÛ‹Ì7 œm,Û£¥%Á¬èµÓVCHß%‚´c·Å8±ÄSX2!ÒÈà:Ùx`QÝB§`W¨Ñ‰`øÁ´b#¦0£qYÀhÂÕ2ÞÛQb—{`Œ¯Ø\1´ È.CA4˜D‚Ã: ’H%¨{ìK}•48MÙôÈðF6Aö‘ưVþYú'Jì] 2k¶ LIß>a X–Ð.©…L“n2M™kf]»¤ 1¥¨!cZ¡ñarL!YÁ¶ !‹Pév$À‘ØDMR1Õ•Ù… ¢Âä0°¨A/LB!­c€3°Ymçwàdq«Ã8pÛ%Õ¤qàãn¬àá º·…Žv<dz< I€ìžAÒYÛ±07b‡HÕ¬!r4:OL! IÔЄŒ¤Z&ç’ HŽ‘§‡TKB Bë,T$@„­¨šÊVYGEÜa¥ô퀂Ø;mA6"àwl4…Ž»"0±4Ø ’@'lèZNˆØT“Ô,p8¬D¶!„°©3c½ À"²¡ØÂjZ&á 8+Õ:YèRz*[²ðN4ò)k0IQ Ç¥02iÀiAblòœae¶t0‘g+/ œ ü:ÈØuO§è°©aÑÙN˜@6Œ3‚0zrƒ"h#.$ŽéalFÂÀ‘Öã,"@6 &ã0ÆËRH¨^2³íFPé ©(Ñd«V”i ÁØ7Ž’2Úá:6ÖÀ»ÑÄcDV@ð8P‚GDb&›t8á)55• é ËðÈÀ1ugå18ìéH!°@]`wTšôCÆâYMMKS4d@„CdÇrAp\—õ’L2fÃWD‚ÅC§KªÊv`pšÚ…^‹« °}­4À¾¡î•‹ƒÀŒ%!”ÁÂËL#2êè'¶NÁŠØI,{‹“º¬0„·¶B<•†… ‚“¥…’±„’³!†Â@‚0*q±ŽˆÞ‹F¨(HËk‚îË5µ0éYQ`M„(@$.ac!f ˜ÁÆÏÀ¼‡/‰‡à|󌃆żÎòs¼t>”âu$í¤ˆÕ$ âÒØ0É©u-ö #ر¡“x ‡X°ÑrD… f-Bu5©ƒ:’ÇFqvZÒ[)h¤ÕF ¶ÝK ÓA°·y,d·± Ýlt‡b1Ó"1ƒ»±µ«d:IOi âD`ùæù‘•€”`¶Š……°ú l °Çdê#bmŒÍ…(–¡Ž¤'+.…¼;º-ø)"Ü!¯bBŽƒg¬üV&!`²D#šßŒm¦&üÇåØJ+ ~䥊Ќj26{a'U‡dURPK7[$]„i–±[X@A—'HlÛY’¦™0…pº l-F)$=ˆÛvqì‹§°#I‰5— EaÅŒ…4PmCl2YHY4Ÿ sÙ?´˜ãb-/SpðpÇÏx8îÕØÞSƒƒá³ØÝCóÅÐϤp–:ÀO ¤ LYˆ$ Ä`¸–ÙGL¥?"éQ´‹‘£Hà‡tHØ$hH@F.Nvî$„E ”³à’è—D¼µ‘òFî-ˆôÚVP¨ÚSY,„[Ab¨4 FÇ,:áê2®jÎ,Ô`“°G\‚ÀT5ŒŸ!P‚"!V9ž€k–MCgyRGswpa  ŒÑÒʘÀ“83êU„:Mg6Uy‚Ca1 ¢€µ-ƒtÎh[¤:B'I.¼ßζ6edpd é&³¾ÕÍ‚ÅL 6 $ ÞAÈRX°!­Ø‘c 1.'M%˜%@3Ø•Á=‹ö…V‘jåPÜqmFKj0´¶£kj(1¡a,.ÂÚŠAM… 13,ä)ƈR;B†MQ W†IÊÕàã-Ç„9Ó’='ü5ãqmfôÛÎ;g%ÑòëaãglŽôžHR(Œ“ÈݵL––:`@M%—@Ã&3¬d`ô &lÉ ""ZY7di‘Vº `¡ŽŒ4hY‰Ab„‘‘A- #¢-¬pŒn•”hm*2‡FØNõf()K1ƒ'kfëX‚teµ© Áþðy)v–º¤& Hn ³¥6Yƒ¦Údž„î2C€F†h²DµÐA˜éœHv55“¦™®u/ÚQ]“O0¡1çÛ=BØe5¤OWBcìÅŽ±ê,ÛÜîÝB lÇ%ë!‰ ˜m±ÔöB Æä·tCE¥ e©5‡'‘,Y AŸ6££;pÁu…a“°±(XņÚO´ pÃÎ;ÊçǼä88xËÀ.£ò]ˆËÔFœXuÂ:x¦G—‹`1ëc%ÐŽ¡—Úð‡eê]¾2Èì^vÄXu·V®Â‰8";… —[)ÖH Œ£$pvß´0;k¥ìмº·çGÌ2> b)Æ+cìÇìˆ1†ÔŠyNF0!'+a4¶‹*SAEÚHwÔ.%ÕÝ‘DI5 Ï—mc³ aÒp!=œYú /ÿÙfwupd-2.0.10/contrib/qubes/doc/img/uefi_success.jpg000066400000000000000000003215211501337203100222000ustar00rootroot00000000000000ÿØÿâ øICC_PROFILE èmntrRGB XYZ Ù$acspöÖÓ-)ø=Þ¯òU®xBúäʃ9 descDybXYZÀbTRCÔ dmdd àˆgXYZ hgTRCÔ lumi |meas $bkpt ´rXYZ ÈrTRCÔ tech Ü vued è‡wtpt pcprt „7chad ¼,descsRGB IEC61966-2-1 black scaledXYZ $ „¶Ïcurv #(-27;@EJOTY^chmrw|†‹•šŸ¤©®²·¼ÁÆËÐÕÛàåëðöû %+28>ELRY`gnu|ƒ‹’š¡©±¹ÁÉÑÙáéòú &/8AKT]gqz„Ž˜¢¬¶ÁËÕàëõ !-8COZfr~Š–¢®ºÇÓàìù -;HUcq~Œš¨¶ÄÓáðþ +:IXgw†–¦µÅÕåö'7HYj{Œ¯ÀÑãõ+=Oat†™¬¿Òåø 2FZn‚–ª¾Òçû  % : O d y ¤ º Ï å û  ' = T j ˜ ® Å Ü ó " 9 Q i € ˜ ° È á ù  * C \ u Ž § À Ù ó & @ Z t Ž © Ã Þ ø.Id›¶Òî %A^z–³Ïì &Ca~›¹×õ1OmŒªÉè&Ed„£Ãã#Ccƒ¤Åå'Ij‹­Îð4Vx›½à&Il²ÖúAe‰®Ò÷@eНÕú Ek‘·Ý*QwžÅì;cвÚ*R{£ÌõGp™Ãì@j”¾é>i”¿ê  A l ˜ Ä ð!!H!u!¡!Î!û"'"U"‚"¯"Ý# #8#f#”#Â#ð$$M$|$«$Ú% %8%h%—%Ç%÷&'&W&‡&·&è''I'z'«'Ü( (?(q(¢(Ô))8)k))Ð**5*h*›*Ï++6+i++Ñ,,9,n,¢,×- -A-v-«-á..L.‚.·.î/$/Z/‘/Ç/þ050l0¤0Û11J1‚1º1ò2*2c2›2Ô3 3F33¸3ñ4+4e4ž4Ø55M5‡5Â5ý676r6®6é7$7`7œ7×88P8Œ8È99B99¼9ù:6:t:²:ï;-;k;ª;è<' >`> >à?!?a?¢?â@#@d@¦@çA)AjA¬AîB0BrBµB÷C:C}CÀDDGDŠDÎEEUEšEÞF"FgF«FðG5G{GÀHHKH‘H×IIcI©IðJ7J}JÄK KSKšKâL*LrLºMMJM“MÜN%NnN·OOIO“OÝP'PqP»QQPQ›QæR1R|RÇSS_SªSöTBTTÛU(UuUÂVV\V©V÷WDW’WàX/X}XËYYiY¸ZZVZ¦Zõ[E[•[å\5\†\Ö]']x]É^^l^½__a_³``W`ª`üaOa¢aõbIbœbðcCc—cëd@d”dée=e’eçf=f’fèg=g“géh?h–hìiCišiñjHjŸj÷kOk§kÿlWl¯mm`m¹nnknÄooxoÑp+p†pàq:q•qðrKr¦ss]s¸ttptÌu(u…uáv>v›vøwVw³xxnxÌy*y‰yçzFz¥{{c{Â|!||á}A}¡~~b~Â#„å€G€¨ kÍ‚0‚’‚ôƒWƒº„„€„ã…G…«††r†×‡;‡ŸˆˆiˆÎ‰3‰™‰þŠdŠÊ‹0‹–‹üŒcŒÊ1˜ÿŽfŽÎ6žnÖ‘?‘¨’’z’ã“M“¶” ”Š”ô•_•É–4–Ÿ— —u—à˜L˜¸™$™™üšhšÕ›B›¯œœ‰œ÷dÒž@ž®ŸŸ‹Ÿú i Ø¡G¡¶¢&¢–££v£æ¤V¤Ç¥8¥©¦¦‹¦ý§n§à¨R¨Ä©7©©ªª««u«é¬\¬Ð­D­¸®-®¡¯¯‹°°u°ê±`±Ö²K²Â³8³®´%´œµµŠ¶¶y¶ð·h·à¸Y¸Ñ¹J¹Âº;ºµ».»§¼!¼›½½¾ ¾„¾ÿ¿z¿õÀpÀìÁgÁãÂ_ÂÛÃXÃÔÄQÄÎÅKÅÈÆFÆÃÇAÇ¿È=ȼÉ:ɹÊ8Ê·Ë6˶Ì5̵Í5͵Î6ζÏ7ϸÐ9кÑ<ѾÒ?ÒÁÓDÓÆÔIÔËÕNÕÑÖUÖØ×\×àØdØèÙlÙñÚvÚûÛ€ÜÜŠÝÝ–ÞÞ¢ß)߯à6à½áDáÌâSâÛãcãëäsäü儿 æ–çç©è2è¼éFéÐê[êåëpëûì†ííœî(î´ï@ïÌðXðåñrñÿòŒóó§ô4ôÂõPõÞömöû÷Šøø¨ù8ùÇúWúçûwüü˜ý)ýºþKþÜÿmÿÿdesc.IEC 61966-2-1 Default RGB Colour Space - sRGBXYZ b™·…ÚXYZ PmeasXYZ 3¤XYZ o¢8õsig CRT desc-Reference Viewing Condition in IEC 61966-2-1XYZ öÖÓ-textCopyright International Color Consortium, 2009sf32 Dßÿÿó&”ýÿÿû¡ÿÿý¢ÛÀuÿàJFIFÿÛC    ++&.%#%.&D5//5DNB>BN_UU_wqwœœÑÿÛC    ++&.%#%.&D5//5DNB>BN_UU_wqwœœÑÿÂÆ"ÿÄÿÄÿÚ îQ›@€Q °X’„¨X¢X,¤XT¢Q,X¤–‚ÁR„  @%@PE…@°,\”(¨XT¡b)€¥’’©€€J 4f¡e¨E…€Ô%°X*e&âR ‚ Q,¢QJ„°\ê%–™¥EŠ©’À²Uš„5¹¤Ô¤K@X¤°P°X¤Q E…”%À°R ))(JBP–¢PQ % BPJJ‚¡eDXY)`*)(””J‹@•¡(!H,¤Š"„XRXX¨!eJ!B€XR¡b’¡`X¢X[š%…XKe,¨•ÎóVRçLÉj[H-ÈX&²U‘sTXD²€KŠ%‚ÀXTE…€²(`)K(Ab’„PJ’¤X*²T "ÔBÊJ…`,€Q ae…ŠJÀ`@XPJ"he@ i&„›•*™š¦ZhfÚb©&ÆZ&.–âè™hfÑ‹¨²o!,‘b¬”)J¤€–™¢¦¡@JPX…Q* (@°²›UäP UAHT¡HRCYUJ(D[š%€ ”%ØJhE,,e¡&©™ªfl™j,hfle¡&†fÆ-¦VÊo\99¯Í“ë|Y; ×f»GQâtØ;ËÐäô3Îæ½ó9=CÊDõ#“×ëÆ`ö·Ãä÷Oƒßσô,þ}+ôxïžã>${\øÁìsäIëqå¡ê±æG¥Ï‡¢ÏŸ§{ž•]Æzšvyë©öçå§×Ùt\è¤ï3fm• ÖZ2ÖQ¹* ,ƒ“ PR‚‘DXJ¨E`XF¤E¸fÑ–„š¦ºle¡†Æ´EDX&ÜY9ïËšûNÇ=~S³uY;wO“»Núùüž…ç2zg˜Åz§“‡­xüžÉâò{gˆÁîž'¼x({éà%{üø!îóáj{Œø‘ìÞ0{ ä!ë§’ªyaéóæ‡£Ï§žˆw˜é•Ûg¬Ž~öO}âÈLò9Ê®'*8œ´ásC‰Ê8œ£ŠòŽ'(âsCÊ8œ£ŽòCc £6™­\ °ŽKÂ9o 9ïÎ>‡Ï¢üÃê|£ê|£ë|°úïÈ>·È>·ÉO­òäû¸9ñ\þ»Æû<Þ2泡4B%"Òæˆ%–-ˆY¬–XP¾ûïŽà¯pð£Ý<=ëÀCôçÙ?C~wÑÐóùé?@ž¾òxQîsáí{\øÂ{,øñë§’¯>Xzlù±èóç•ßg£Ôé‡ož¨vxëé÷cäLùÕËœhgQ"Œ¶2Õ0Ú°ØÃ’ºle¡–†n•DPàm‘¶%r07x‡+Œm¹„mÆ9c‘Ä9\påqSn1É09tÓ0Û#PŠ–MŒ60ØÄänJq9G”q9iÃ9‡ åSšÇæ.hq¹)ÅyjÆm¦ujçJ,,µR•®i¤ãû>3íÆð›ö^;ÙK‰fiDP—&•JJÉ©`šÊë4\©&€K)|·ðvfÖ¤¨, ,f•šTAR•€)RÑ–ªa±Æä§–¯ ˜p¹©Àç'žŸ=ç/žÙóÞqÀú!Âæ§œp^aÁy­p¹G æ' ˜q9G䜃È8ÛiQD¤!¡e¨!DPP–T¢X(E¹(QQÑ ‘R­²¢Š± ( [YÔ4_ìøÏ»‹“‰9ý‡‘õÒáfmŠE„¡©šP‹"Õa(EaY³GËá}ß„ÔX³Wv3n£Žèe±†ÆZhE¨%¤UE¤Xµ(AR‰`YT‰@¥ ’€-P€(”F PXEŠ•D))%, Q@%@) „%€  ªC R”²À¢ÁlÄU†¥ ªÒø¾ÏŽÏ·‹“‰>¿[å=^n .¤dTYD-"‘H‚€”E nt|ÞÞx=K 9êä(± D¡lRPT”(–-J(Y@(EDQ@²PJ–`¤Î¡²TE€ÁH Q¨X¤¡,«,@QR‰DÔË`P[ÁlÑ(RÂÁJK(!¯íø«îáçùÓìõž[ÔfÉd¶\Å-³eX†B„Q&ƒ:€Jfœ> ßxK>gP¤‚ A@ Q(%€µ¡*`¨…Š¥ RÀ¡eT¢RJ¤”E”T R‰bÅ„–@ `”A@€ ÄT æÀ M @²Š‚“Pµ)AR‹)D ù>¿’¾îœì½7šôÙc6æØ”—1lÒJ$P¤DPAª³IÅà}ÿçú”–}:ÆðQlQ)%€¥`B  (@H²€P @PBªP’¨-¬¬* ‚‚eRšX°°…È€”¢K `ˆ¤(€·"ٵȊš* ¢j e!J°k:€¿'×ñÙ÷üßGη£óž‹J–PÅ Š"ʈJ«2Ò3cU¬Ò~úç÷1eŸF³¸±` T*YD¡(”¨¢Š¢€Šª% EB„(BÀÂ(”‚Ë*Jˆ±H "T–YD” ‚Ê*XA@¥•bPk4·#W4©JˆºÍ-…·#YQñýŸ%ŸoÍôüÉÜz/?è3fk5 " í¢ÂP%"È%Â][sGçÿ þ}s§Ñ¬ë6¥ ‚€B,ª`°PB¨¥AB,ª‚¥ *¥ X ŠA (K€ ,¢gR$°YegBKˆ‚¡b€Š%@H%RÀE ‹e AF³Vˆ¥   e-‚ ¿Ùñ×Ýó}?2w^ƒ ïóp¬Ø¢X\Ú\ÒjÁ,€*æ£*\XÕ·55ù÷è?ŸÜäj}Íʈ©BÁe¡"¢ª¨YH*À RªQ,*’¨Bj¡ ƒH¢€E€°T( @J"‰eÀÔ  (Ã+ ,¢±@€ ŠÀE B* e‰¬ÒY@ŠBÙUb*R¥*PP s ”©b‚¥-ÈßÃö|uö|ßWÊï}Òwx¼{’], ©£4 dPKÙx‘m±[üÿôÏîqËÅ.~‹ÀEJ[•P,€X(•HRP @ ¢ÔBTT¢”*D¢•( J¤)EBQ•`«IF&¤E‹ P€( (@J ©Qa¤‘©Q(°µÙF’•(²•(±æ–Ê,Á`_“ìø«îù>¯˜ôçKÜâÄABPMA)BE”KæF™±uR›ðÿÁk¿Ž¾ß—êùOIÜu¶*°P U„jAU`,K„[l¦ü½ðzÏny®7 ’– (@…P APT´!¨e€Y`X T„©E²‹B % ){c¨×·î³>ì=Â<·7£Kçøý òŸ¸›õÿ¬pê~Y=Ç›³ªYdQ)YšÌIaÁD‚€!`% €À€¤A`T¥@²Å¹«ninQ»‘l†ÙJ([,±WäúþSìù~Ÿ˜ôÝ·UÚâåP@¤‚QaDEAPTKÅ&–Pß„÷~Yùå\òï-ˆ©@ –€€(€X¨J•B€À¨(J(€(X-‹(*QbÊ”(WмÇ£ï±zÞÈÍ:¾‚½Ÿç_§èü?ž¬ý_žÓôo¿ò­GêïÍûÉ}cáû³z¿ú2ÏÉ^ÛÇtÏ ªc“„IbË””B„ B@ÅaX@€PT¢Äi*Ô¢¤j暀²”¥MBÅ[,,´ùþû>_«å=OgÖvx°‘¤ JЖØ š¹Ñ”¡ Ã5-Ó7á½Ï†ÔùóeÇ6±°X %*P(H*P- ¨5 `Ô²‹HRT( M%RÊR[ ]âüþóé¼ôpx´ô^K«3aU‘PjAPjär÷] Ò¾ÿÊ}V5ë>³Y¿™ü?©þÓ=fu515#+K YH P @ H," ¨ ‚¥…”X-…Õ‚ˆYM%* \è  |¿OÎ}/ÓóGªìúÞÇ5%”¢e PC|v‹‘HP *ªÂ”MøŸiâµ>RÜoYرK@J PT (e€QR€€,¥¹¥*JV®ik°_«ßg“žwçÖs|‡L @P°jæÕ¹§sîÿ,ìqHãÏ65ù¯_úç1òçR³,$ÖeJ%J J„°@°Äˆ PXš‚e-”j ¢„¥Ye6΂Ĝü'Õòý?2úÎÇ®ì1P” ”@[ ¨D¡(à±kY¦üO¶ñ:ÏÈMg—XÜT (°T¡ÊX%BQKBT( °”P,´€°T @SEJ‹-]M//é/§ç]oÙù¥pñ2$TP¤PY`±VÁuŠv¿¢þOèñ¯mÑ÷Œ_Éqèüï\fX¹šÌQ "K(YX ²X€°@«(©(²‹)AlF€²¢¨¹«8¹x“êù~Ÿ–__÷ü?nlÊ šP”E «›DQ,¡‹usMx¿kâµ>8k›ÆàBƒR DT ¤ Ѝ*P”¨Z–„J”% ¥–TX5¬èYKØõþú^òºŒ_7玼Ém‹ *(‹€«”¤* ëQï»ÏÌÿIÆø?0ýgÅYå󩬿Y*Q`"‚¥@a%‚X¶X€,EAAR­„PlJ[ušPJ¢€T©8ù8Λéù¥ö?gÉõæÉd¶(XabŒè’‚))º·:Mø¿iã5Ÿ€k<šÆà°² @°**©B (@ X*Z ©JЍ* ‚¥ ‚*É@ k*i>ïÓ|§¬Æßœ{?͵š­È"P (€• £+*„¡`äö¾'°Íý'áûœõù{n§®$²YR¢ @BÀ’‰,V [`¢ f‹ؒжPP ãx9¾§æ_eõü¿V.lKHP%ÈjBT`"Õ€1n–S“Æ{/¬õãXäÖ7°R P !@!A`XUAe‚-,TP@Ê]gFµŸ´ý î9ëÃù¾~¸ª"ˆ°’‚‚Ó¼ë™5K5Îð 5“;À²—“‹kúWßå}W=y?úwæ[Îe„– ¡ `X3D²*Q(KX ,¢ÄPPPPi)D—Y¥M (Á1¾3Ÿçú~i}§ÓóóçY·1¦tJR ,‘@Å3 ­Þ=¾CÖù+ž½.±½ãEA¨(XAP@ÁR€TJ…ÁJJ@‹ÁwšôžoÚËê:þÃÎfø[/LwþËÊû^{ùZ>.«ÑÌ>?Õ<éž·NzâõŸw{Šù>{‹³úºN“ÚxÏs›Þù/aäãÆÙw5í|W¹Íô^gÓy¼ß W¦ÆŽëôËÿPƳù7ë˜|³R@K@RJÂ,X@¡(–X° Y`Ô”YaAH¶R¥*hYRÜÒe1dçù¾Ÿš_mÍÅË! J%…²JfØX %jXXE”A_©¥”ß’õ¾OYëÆñ½gBÁa@P… P!`QUJX(²€ J… ÖtoßøÒ3{_ì<1æõo>£ÙøÏgÏ}_„÷?ï=Ÿ³üïê?Nø>Ýs׿¾ëìä§Ïâ,çåû½]c‘çsz^÷É{ÍNÃÉúÏ':›Î½Ï‡÷9× ó~“Íåá©×+)Ëú—忦c_Wç?£xóòÍÉ,‰P€DB,‚5’,Xˆ°*lÈ*R,-–Z”·4¢-”©Ke*R„Üèã–/?Ëôüñî9x¹3¨ BØ*PHƒY”.MÀgPãƒV朞OÕy]g®Æ÷ˆ@TP€R€T P(…ÖiÉúO濤föÞ ÞøCÎë7yõ^ËÆû.{ê?<ýóÍæòcš¿Cûø¹ym(üÿÑw·YN?/ÙÔý^Ósƒî9éäýg“Ö|…7›îü/»Î»ß5é|Ö^%]s ¿¤þmú^uõx}ùütÖd!l%ˆK € -‰m‚ÙK`ÔXYF³KeTX-ƒŽÊrüßGÏ/ºÞu@X (³D¤²ËFu²‰4 ðLš·4äòþŸÌ\õãxß'è‚€@,( Z2–€ œÇ* J–€¤*Q`¶ \èßèŸ{™}7‹öžZ<]ηŸQí¸X­ ± ·(Ó6«" Ý㦤ر¤åâî×Úý'-øÏ+ö|}yÌoD°)"T-ARˆ„X¨ DX°´ ,[šPj²“P]dh ¨ÊpòqòK¾nÞfÌj–h€\Ò ¤) ‚ ‹ˆ£«¹­[/™ô¾jç­Î÷Y,¡€X°TU@Q°[°T"Á@JÁAni­ãGúä§ç__埪ùdñD隀BÉ@…” ¨* ” s¢Ù³~ûË~ƒº^ëóÉ:lÙ×3:ÌabÁ)€(Š%€@ @ !`ETE ÊRÂØ[(,,¢ª,¢Êªprqr.ø÷˜÷3YƉIP¨-B ©@‚P‚À%êÒµRœžoÒyËž±/Loxв¢MTk€¹ P‹T¥Š‹µž“ÌòŸ®gáì9ëó¿ô¿Íºã’È@ ‚À¨* `²†³µ×6=t½Ï`œïSù×eÕõÊ ˆ“7+bKPAnEJT %€€°µ)©°i,mš[,R’ª,¥JP²œœ|«eG¸—8ÔQ,¡`X,¢X%¢(“Fu˜jÈTQºÆÍùßCç®zĽ1½qò "PŠU(–@±UÁ@J,±@[ °T%A@²„ )ntkXÑÛ~•ù¨ÍöÞkÒ³"žÇÇõÄ”fQÀ[)M,äw’ò{œëä»_Ïu1”Ôfäfä"Q ‚ ¨* ‚ ˆ¤, @(Š‚Ø[b-”jRÙbØ-e( Sçäâå]\rG¶Î³E‚çD³D )Ðͤ!A*M†–_?ßùûž®ËÓß!,%€AQA” UÖER’Á¨¤"Ñ@T¨ °,¨ ÁPi¨kY¦ù8w^ãÓþIíñ}'—õ ßÉ3ú?†ë޽¨e¨E‚P”KD´+kžNoa/Uí6Å|üúÌ|ÌôÍÂ*DŒX¤ATäi‘©ARˆTEAP¶ˆjA`ÄT¥J,«l±l¥Öi¤°¢TйAH[)óópòÊäãæ=žwœjÊ% R• ‹“RQ Š:pÒÁËÐwÝ ÏUc¦7s CQ*€€B¥– sª€¥AA Q` ‚”·55¬Zääâ«ì}wä]Þ/è\|^o“ò¿«qê~L÷=çHå–q´ŒµW{8uÚwÒù/Cë>œß—êc7}'Qå·ž_™çXEe"²Å’æ*ŠB¢*  ‚ ¨,%©¢ ²Á` "‚¥-”ºÍYb”©J `¬Òꟛ‡–WÑÁôžÂkÐÁ@J²ˆ €Ñ”ÑQ];6kIMô=÷CgV—§=k:!l ¨* ‚À( (RT(( d¥”YhQ •A* %-ÍMk*Þ¸éõzß©¯×/å~7غÞÇ'[ÙŽ‹Ð«Îòwêû¹Ð>3ìž_Ìêzÿ!ðçræA+!’È!**  …B©E`¶Š”ÙK¬ØÝ”ÒXYERYE››‹•_WÉöG¯šÎ5), …æ€,¢5ÍjÜÓ“¢ïz-Nª›ç­c@P !@TJ E¡PP‹),ª`T¡(,%¹¥JjäšJ]`r^=W/ÓðÕôŽG»æüü~Çàdzø<Ðì~9ZÌɼÈY!dAb A`3DXT’ÔÑ* ÁlE!niS@xÔjçF’Å)eȲ”…¡nióóps,û~/¾=nnq , @ÂP¨Ì*Â4:+VçG'IÝtºÏT7–ñ´ €%‚€   P‚ °(, –‹‚ ¥¡*RÜÔ ¨]\"­Ê6ÀÒJÓ0Ôˆ²BÄ,Š ,! ¨å ¨%*  ‚€‚¡B B‚€ %Š”ºÆ\ê5¬XÒRÅ (*RÐù¹¸y–vwe¯Æ4!@ AAX °KFhÐ\ÕÕƒ—¥îºMN®ÆñušT´% ²ÒÄ( ÀJ‚€‹*€`JPT´ÊPPi™¥APT!Ql!PX `` ˆ*€‰¡Á@(²‹l¹±«XѤ±lÖ) |ÜÜ<«;>¯µQg  ©H° J, P± è2£ÏÜÛnøôrô½ÇQgU¬]æëJŠÒ€”,,*€ ‚¢¨A @Â`  eZRPXK`¤ ”‚ ¨,, ¤”–À% B ¨*U"(X, b-”·:-΢ë%ÝÆ‹sd¶5%4”ù¹xyåÏoÔv禗8Ô ” "ˆ ™°(B£6’æîjëYÝ?mÔÙÕxšÍ«sMIJÀ¨, X( hYKÀY@* 5¢(”¥– B€*UKA,,Z„,Â,Ae‚€ – X*Pˆ©E” j \Ùu`Ð-‰4–ª"‚ÜÓçæáç—×IÞ/£Î±Š J¡e…•H¤±I¨ ›¸¶ï\tæê»>²Îª›Ä²U)R–怔¤ AHT ¢U – ÊJ (…(JAP,-‚X(X a Q`€ d’À ¢Ê]Bé«°šA¤EAu‘ÁÏóý Ž÷¢ï³} ¸ÍX(,²€,°±*"À.…ÕÆŽN·±ëu:Úoç@R¡JJ H” ‚À° °T J` E‚Ë@%‚ÁR€ @€B ` eK…%Q%( ÖBÊ[š[(ÖjÛ,T¦™¥JTI¨ -¼üñŸAç½ ½þuŒZ A`šÑ"ÂË ,¢XJ©5’ƒÊ\éZÈäëþþ¾ÎºË¼Ë*, (,,@JD¥R€ ¤PT( !`X a ` `Q)•DQJEDX–€°) J l¢Ê[Öl[š¶Å•,TÈ[”q}?:çÑyÏI›ÞçXÍË Q` ¨ ˜Ô°K*U %q-ä¼tæø>ϊξÆò¹©A@²€²¥€J P( )(@XT  (¥ÊA` `€–(E`( P”ED-I¨EQ•€ D)E¤¡,lÊ,K«šT©PºBT,q}?м~›Ìú|ÞëÎlPŠJ¥€XT¢`KIDY@Û=8óÎN.¯¼ê,éÇ^k-…¤,*P*ÀÁ`)( R€,P ,X¡ PE€–TX@%XX`QJ@¤Q DQ•DQ I@±,ÐJT¹ª²•nj[‘@²ËÅôüßAQåýFosc5`¨*PB€RX€%"À£6Ã*މ7Ž°Ñ®£·êµž”väÖt‹)‹ «% ATQ@,   ET¢PR€@XE„–(²X¢’ÁR‚DP€²‘`¤JT)%@…²€´ l™¥DcèùùÌz¿)ë3{Y¬æÀ, @@€·6€GH—5Õv½U(íÉ©«J–%)BP–"‰@ªPTJ†³Í3ßõž‹­çæócËuâ^÷ç<[Üp9ë±uå/¼ðÖò»Ô¾fú_™:>nÿî¯Ëè~øño]åõx]Øé/uÄ×Tíþ–|û²ú×¢w9ÖOAÚ§‹Ÿfך>Ë~'´êæ<û¶çºè§aóêÕ|s²ìcÎO·å·ìùŒ9%¸»À”K >“çrñ @Z (T”B‘D BQ)UTÁR­¹¥„[J°[›çàç1ë<Ÿ­Ííq¼f`* )("€jKR¡L–ÈI3yõ äê»>²ç¥¹98ö @J@PJ¤ PP*ÞR}|Ÿg°ßX™î7ÒíÒôV_Içr®ë“¡Ï¥ë8cÓrùDϹø¼šßiä¸ú/§Ê&}ÏÁåKè;/OWöx‹×ãòÑ=÷Áä¡ï¾?!›¾O³¯k^¿—Åo8ý#®ñ˜“Öqy¼Ÿ“ÊsW°ß…ûW¼ì¢Ï·äø{,Ï»¬²vœ½>-úþž†µÉÐúO9¾o ¡Á`€k!R@`–Y-JTBÊT>Ÿè8ýõù½–uœÔ¢ ,* ”ªEÁd(À£¢W>²†º¾×«¹é~MçB¬€( D©@%),¢Ï¸øoÕóT`*(5šHUf•¹¥AaB €!` ’X%‚X– ( ɘ BÁ@ @• ",‚ ‹€ U`©` (&„ !Ïóó™ö>7ÙföÖs@X …¢P s¢‘Lµ£¤¸së¶)ËÕv}eÏH;òrgP ² JUBD¡Uü«ØÙÌé}UÏì{Oµ:Ÿ7Ûñ¯¤óöýGl:‡yöoƒßt‡AÁú/Xx¿«Óö§äÕ3jUw=?k×—m1~—ÈMSŽrÙ~yõUø§Ü޾vEêçl—¨ÏuW¢Ï ±çg¥Kæ3êìy'®/‘ž½/zä¾BúÔ¾Fú²ù7«’ùYꇔϬKäç­²øüû+/ŠžÞ¯…{¤xG»MxMzž›=~¼ú”Y¾Õ:w¦ë¬z?Š:‡kó×ÄBˆ‚€€  €X”P rpsÆ=§‹ö™¿v7œÐ% ,¡DQË  #¡µÏ­A¾³³ë5žväåâä(ZX * ,« ”X*RÀÛ$×µñ´ëb¾ütæúúéJ¯äYõO™Ó?[äYö_‰gÝ®¼v7­žº¥½éÉÝ^‘]íèIß:ž‚ùòzçezKæ—>™æG¦¾bÙé§š¶z9çmžÐ“½tvÎîôwS¼×@;ûçÖzçIèï›ÕÃŽ/·„¾@‡®ð}éØ{_óIÍØü^b¿Tñœú,´YDj!  !hK– ‚ÜŠ sK×/,cÛøq›õMc6À¨4¢€¢ P€¨::œºÛ)®³³ë5:AÛäãäR ¨‚€,”€ )HX(*€¤°¨ZŠ%¢(Š"Á(f‰,"ÂJ¨T‚Ø* ‚¤²Á`°…€°  Ó+ @‚¥‹ÍÃË÷^ÝËôKœÑD°·4Y (RX‚ˆƒ#¥[Ë®Tk¬í:Íg¢§nN^=”%A@€P (JR‚’Á@ ²€ @eP,ETXQ%„YPD( AAPX T°!R„ …€QHJ ¡ r›åâåŒ{Ï ï%æÎ³š) EJP…‚¡e¤J"‘JgŸ]²9zÎˬ¹énWxذ•))DT,PT¥AJR‚€ @YJ@T  *JDUIDXIFf¡%   Y`°R€EA`€!¼ °° !*Ê©I(JIaɼm3_Íäα›@KD K) ±r,¡‘Ôc“‹Ÿ[¬Ó}ggÖÜô6;s¨M3K¬3 °6ÀÝãŽ1ÈãW%Ⳏ®1Èâ§%â®!ËxG3…'5á×€s¸jó8iËxG-â¬C’`›×ä`ºdj⥱UJ@J( ‘BQ+FejTX%‘%•™¨eb@%…аQ€ )ˤEJÂÔQIbU(U3¬Ç&ñ´ÏèŸþƒ›e™ ÁbÔX…(%¤”EWž.]t¹:ÞÇ®³¤•ÛŒXT KB%DP) E@JÊE”KHi¹a—ˆr¸‡-áW6¸;ç÷çîÎàÓˆrÎ1¹‘©!¦F™dišiŠi„nñÚÕÀÜÓ#LŠ‚ ¨5 ¨*" ² ‚ ¤91(J-!PT¡APT¤¢.hšÁËÉÇ´Ÿ ~úk:ÎiAD‚Ø‚A×c›ççÛT½weÖÙÑŽü`J 5•* ,€" ¤ª‚©@¨* €€Š"ˆH€‹ˆ¢(ËC- ¶0ØÃ^=-£ºq¹3YlaÈ8ܨâr7 ã»laÉ+’&·ô/ÏBÍ̳5`©h”@ TÀ `¨>n>]~y¬Vþ¿¯¹é%ù%‚ÁA@ @ ˜ Ëk…PT °TJ ?@_Ïß øõëž¿á<û´ú“¡Ÿ_ty €JT¡A@HP €`T‚¡PP, ,,•´JPkôÏÿ@͘Ԕ ©B B¥  ’r9ôù8¹øMõ}Ÿ[¬ô1:á * ‚Ë‚¥" @K A@L€¥ ‚ ©A ”€XX* ú·å_ª7ã=×_ÔµØã³ë×çù~¯©D¿/Ñ£Çg²ë;sD²ÜÑeP‚ˆX©e%”%Á`X °²‰@ @€H¢}_-_§æCï|êì:QÙý¾~·P€sðQÀ…JAPT€PTP°Y@¤!lHPRW7¼ðžï72Ée ¥A (T( ,R²^cwçúzTûx:-g´éy[ÏÎçYó¹áÅ9iÄÚ°Ð̹5e€¡ ‚©bX"¬ ˆ©B ª‚ ¨-ÈÓ#RCW#L24ÈÓ#LÒ ¬Ò ¨* ¨* ‚ ° (…”KP‚¥) . ƒ@¨(TÐOºðÞã6gY–Ê @¤P”—4°*Rx¥ò˜ÕøS®),¨°‹ ŠL¹q¹åW–Gq¹!†Õ†ÆF†ZVmDP±æà¡QUA¦EÖEHTi‘¦F™™¦™PXHQeZDT  ‚©( X°²¬REšÉ¦t,šTA¤E±VÙîqô_˜}:ù×~1ö_ˆ}Ï„}<z>fÄ¡b¨ “0i š'¬ò}ë±¼ËYK`` à—:ÍDP%4UÌÙôqö¹NŸÏPe¨¹2œ²Ó3¼â^¬ †¯(Žäé‡ÄbàjñŽI`}Ügʹ*CIA Í* sAHˆ¢ãP ´2ÐËC- ´2ÐÅÐËC- Ú3h“C6Àˆ¢(Š"ˆ¢(Š"‰`¨(, (Š"ˆ£- ÍŒNAÆänJpÎaÂæ.aÃ>˜|óé.¾˜M¬¨¤*­AV)`{O§Ízip²*P*À²*ÀƒÁ‹˜RPX *qšªBíø½böwA¤“»ùβZg“"wýÎßáÏÌdXrD>íÝ'Í÷| ¦9xy…Çh|¸ÙÐ ©“•˜ô'Bì¾cæ×6ã„+Ÿù/ÕðœŽ:mÇÎ`ɥɦæi¦iLšT ¦j™he¡–†Z›la±†ÆfÆla¡–‘–†Z„ªIDjTÁR–²¨£6ˆUL'' óÞÇO~¿7WŠQ^ãÂú»Ï'm °YT”JÀ"Å šÌ !@\£‹”|ï¢rð^QÇË)1ɉÈ8î °}TqÖM07¾ô㋜Á’¸Ç.þqÞðuC‘ôðθͳMú)O]Òuš8ù3£‚Å;–ñG[õ|¤ÔƒP^^¼ùÞ‚oy9x¹¾s|ÜPú>p•7ż—ÓÓû¢vçYÇìºC¥äãäÎÏI¯5Ì¿O]®Dâv]a98yLý?Ôv3ªûN~§´êÄ£[áÙ>¶81¯´êóöü%’¬SYâôI{=¯Tì:³•Çi§tk—_o]³Œr³j¸Ç#Œ ³K,…"ª(…Œ©EE’¦9xŽO³áÕž×Å}:†[J-ÈÔnïs×vràJÀ …‚€eÊjI4}||ßAÕe'¨ÝìΞ„ÎøÒë:/:üÍå"Àãå èã¿oÆk œƒŠìqòÁ©!Œó)ÏÊ1Ë‘q¡Åw ÛŽ\Á¿Ÿ—â—Š›× 5¼èƧÌáo¤ñÚ;Þ›‹fyxöqòqhÓ#^‡ÎsW™Þtqof}ÿ_]Ú¯Å×}?:gY¦98ùH‚öwvu7kÕ„¦>Ÿ›v=q~œq{Hñ3¿è,Ó:8ûÞ‡”í]T_GÇÐv‘ôôÝÏE[cIÅÜtßAÙsu?AõN,œÝO×óÃ\[^=póYÜü¼¿D|]Ï9Ðqz.uòno7sMÅY@£È3©K¶”²hf·Øx/b¿TÖe•P‚åH²RQ`< Öe”Π³ã,PŸqñNOœni%•@û>H"à í,èûz¢X=ähz?ß]ô|â\™äÆÈŸIÞõçBuöR,ÞlÜææïÌc²ëQ,5®.RLÐîúAd5X*À}çÀ ©P”cpÆ®Ž6ÇÝ%AÄåNQÂæ|8¹‡ØÆ¬,Bë¼™¦µEJ * rRˆÐÈ]L—“95fÓ¼ïüG·—2ä¨*RYB ”@ÀÂ@'6L)%BÜ1ÈU†oWÀ>‰ÀN{óS“JMqeyœ#¶ßN9t߉5pmš^îÑé<Í€[Ç) w¯–úÏ‚XvßwSõÇ]òàP}?d}ý£ó$Ŧ³Eéú»ÃÉãïø rñò{ÆÀËê:01ÉÆnkÔfÀÍÁÉÏÀ>ï„Îøù \ìÝô¼çÎ3¬èãäãä>ÿ¨éœ¼C:ɸäâå>œv¿Aç@ΰoÁu•\Þ5¨dË@…ÓRÐÈUÈôÞkê=Žw™d”.JA@”gP¨QYhÊ—Â¥Ö@Ó- 42ÐËBM §Ì¤ËP”$ÖHl㜰ã»Õ=Ž)Ì8\Ã…Í+ȃŠï$¹†Ø˜lg|fÜ#›“å§ÓÇÄ9o '.9 klŽÏìè)ôpAs¬HMg@×YŽ0&Mñòq›¿è;S½ò=ßBKny äÐÿCꎧªõ\ãäãä8÷–êú>“¤w} 98öqòqòX.vqrðò•JΡbœ\Ü<ÂQöü~—q垗㮙ݕÝñKµâŽ¿ÁMñìØ•AQ©’é‘UlÈ”TÔ=oÝå½Rñ®bÊ"Á`YH”¹¢È,¹6É|-—Y’å+éíŽv\GÅ`¨* •aƒ:ºAVéÀì:â…qï 9  ¨*Sš4¢(Š"‰5’kE"ˆ¢(† ÍÃ*3hgcÉN0ás-l%3˜pÞQÅy!†„ÓC‹—&nF®!Èã—†›Þ9 ç|<áßg\>ŒqŽKÅ¢rcgð6Èú»¯9NÇ®£‹xä8¹8öU…îzQ`ãg7!e‡ÓØõ}´u?×òUJgXÙÃÍÅÊs8GÓ¯’g/^;ŸŸ®¶z±ÙuªqrñrÔÔ3t¬¨¬²–42«HA Yb–iâû“Ðã“ JŠK€"Ôs¬õ}Híþ˜…€ °L4–€?уŸ íúAd1qÊT£ïø;ìì7õGMóö]=%ÁžI¢QVwiÖñ÷}Qñ¨œ|œFí‚€í­Ùu -ääΰlT¦d¦â€`XMqrgADQqì²P‚ 2Ô3h¤/Çq9‡œp^a8¾ˆpÞA†á›`Ô8ÜœfÜC–ðŽg 9ïÏ£{ãä8µxO¡ÅNF)¦iPTºÁ½cÖV&™U°E%”@¤EUI¤k9=Íéû™q©D dÑ£ÃLÝf¥[R(‹ß ^I  rðCy•''Él,°v=påí:rʼn8Û5(ê|·Ð}ýGÑó€qÌò–(?}ðpGoæ{~¢€ã×1BQõ÷¾_¾›©Z•>N>C:ÎÂnGƒ£ÇÙñäÇ!ÇÉÇÈ/i×}ß!žN=˜äãä’ˆ rqìÆøù@(Æøù k(Š"ˆ¢\l3 €R9x‹r5%M%J3619qÎjpO |óè§ÍyáÅÉF™Ñ­I ()5)"”ÈÛ$­Œ(¬£lœþÏÂz•ûæñ, ’„ šk<.b𹇠å'’j†¬Õ1¼E¼PæpÃÀ>‹óS\“K/w 9œ4åq—s1',¤¼{*P×9±¢Y@q“4š””7ÏòÁ`¸ß­!5(fø0@1¾3Zƒ;·';Ï“·ã:Ÿ‚Á`º”Ç'Ë}×]õH.u’k:8ùx¹@>ÞϛΛàôžhœ|‡'!Any‡¢óF7˜!ÈeÖý§/[Ûu0,¥àçà9ÀiÕöß1ñ`Þ7‘¾.P“P›âä7e؆BêdilfQPY@oÅuŸ›ê— ©DRÒ«Y“C-ä µÔHÐÁƒ”*Q™Q(WÍfŒÍÃ3c’.5Il§yÇÎçaÃyµ¡=f¼PæpŽkó£…È\ïˆåpŽgåqŽF¸· çX6 \¼g&uƒ`|œg$¹”›<`u‘©N>^=‹£yÍÇJc“âäãäì{=£¹óPcyÑ3¾3”}Ÿ/:ÉÉÁÍÄr\líþO% ë&¥<ÿ?Ð}_!c³:Ô§&4ky°·%¬’Š‚‚)sD¡PR©$ÙÙúOìWyÖe% I@RTðîLêe¼R|cÐùëÆcxåEÀñ§.9΂RX*R^>^>DŠ2ÑrÐÌÜIx¹BR(‹B¦Œ7 NHa±Æå|€ÆÇ š.aÄ凨Æí|ÍÈÓåqWú8³ÈkÉoŽW9c‘ÇMâää98y ‚€N>N>C‹“γ¡ÇÉÄsƒÒyÎû¡cx98ù2g||€ûúÞøè@)càåÅ9>ÏŽÿ¡è$z«ÉÒ°ßËËÇVš†B”– B&TœŽ2Ù¡•IÞôœ«ì±¬Êf€Y`¹¥€gƒŒ·èÀp|Eà]@íúB@©Ä€´P9¸BÄЊ €8Åœ‚J ÇÊЀ  €¹Ð€J(`X @@@6ABA ÖÃ!°h(2áh^ $p€z ó PÇ qä9‚¡"€2¹4-¤¾0º ì3Y ±pÀ }/bK˜+!A@ ÿÄ2 !0123@PA`p"#4B$C€5ÿÚÿä´üÿèéñþ#|ñ¾ GŸ‹d…ôŒFÆ+C¡ŽḚ̀3, Ó6ÀÎ23 ëc¸®°®T}Ï*Dixá® Ù"m;Æñ¼oÆñ¼ob @t]EÑt]EÑt@º @ø ]a]4}Î,pO…QÙ; @@‚ @YGâu…tÑ÷<ž3ý›ð¡ùÇÜ é¢îzû6ã#îôÑ÷,ù⟎ݯƟúôÑuðùñ¾>4½Úüiÿ s¦‹®ÎCv¢ãÞßÇã?ý4]\~Z¾xNvì?´Nïi>/þΚ.~q—Ñð ãý4\ø_õ/£âÂüZZ ¸åEÏA‰ãnâ«§à/Æ%Ür¢ðK†ZO§à/Æ%ÖåEäù|X_ŒSÖåEÄ.ëJÍǤù‡øÀ„õ‡9QXcâÏžpwpÃüdž°ç*.@Áïwìý·§¨8(ùpK‹ò\#°Á~1OPpQôùÁW1º ñŠ:ƒ‚§Ð¯˜0_ŒQÔ}ËÅ)·änÔ¾ `¾ šJ…ìׇéCôÖ‡é¬ÓZ³+f¼KPŸ¬£˜pQôz:ò/¦µFû½œÒBB8Kiµ‡6kf¤y¿«7Ì8):= @Á}-Š7] R2Ö•:ÚA×SýFœ‡êTà¶8*Ês Z¥ú&\Ò:×Õrab“·ãîÓ:]ë é-¶·OB†ô;VËaͤ³ ¨yzä"©äö‘†êsEEV… þ¢Ý‹çIÛô/w‚úE=2Þ6™C)±úÖšUºïCU® 3RÓÖ¼Â'Ø[*úÂ,_:Nß“ñ­îà0_G¥¤7)$[ˆm5ËsÁ#M´ ‘q´¸šše0¯§·bùÒö„ðãIp'€÷p/£RR›ÆDI!QR†ëËu^5RÙ6ÜC‰6•¦¡…2¿¦ü7bú©{^…îàø/¢Ó°o-J*ª’ÂVµ-^-=BÙ[.¡Ô™KÈqµ6¯¦&ì_U7oÀùá§»–Ñ“QÓ°L¶*jµ-^=5B™Z•$VÓâ£é‡É»ÕMÚô/÷l/¢lö ƒ‹Ki}å<¿&†§ VW±†çÒÏ“|‚ºéû^^íO÷A‚ú;8®A ú‹ëòè_Ä@y¢u¥¤þ”|›ä×OÚãrâÆ§û Á}gµq±Xþ^c.›kI’ˆm&n¯éGɾ@úéû^†£º ИoÒÝecدy»9éH©k¯xg¶;^6îGzÂúÍnÊÇp˜ó©ÜÃvÊÆðßúJ¹#>¶;~†£¼> èTÍá°6“’çœBËì ¦Þ€ýJ¹7È[=¿CSÞ°¾‚Â/»c«ÄsŠÍ Ž“­-¥x1{Åj/S}%\‘ȶ{~†§½a}f¦jRîSÙ@ÚîZœeØ ¦`û5£0ã'i¥¢±æPêV›«³g%8wR6¢RZ6aÁ´Ëø´Q*ëáE$eì <…rG!þÚíð‹Á=U=Ñð_AÙi³i¯ølÙí+B\MM9°°”šŽšŒ›°Ìˆª« ÀÅ2Þ:Š´Cgv†ÔånÍèO³¡³…ÙTQQô…rG!þÛèò£]OvÂúÌÜÀÚ§nÌï Ã2§Æp"¥ä j¢|…K8Í6ÊÜU=2 ã‰m5+xééèB„ŠæšC›?´6§+volm>Ö‚9H®(©òwzõòG!þÑÑã§»a}gXm>õ›3¼+¿«cK4-*¼”%6?P†IÇVꩨäë*k.¥N*™¬&ÆÔѳ»Ciö´§ì£ýW>jù#ÿmôYñçT÷l/ ÑTm/ìY³;¿ú¶¤þ½Š˜q·q)© À©¬¼en©–ÑYµ4lÞØÚ}½)»h÷þ¾Hä ­>†§»a}‡úÃiÿbÍ™ÞÿÕ±fm¦ãvÅ‹ZPšŠ¥<)é”é¡´¶›vž›ÚK· ƒ‘´±ô…òO ]iéô5=Û è;8ÿçP¿}›7º+¿¬¤QÒluÂm¶+…6â‹+[tΚŽð""!SVMŠ%^lm-?´6DhHIA Óš¤/’yõ§—¡©îØ^~èàªï‘³øÆÔ/ÙfÍî%*NY€–›Eœ…eN*ƒ/­¥0ûo'EMp‘³ûHJ†#.ÈU;³»AIIŒ† @ÙdÂÊa7±ó—~¾IäÖ\½ OvÂúÌ?äéšk6gwC•,¶*kö„8¤5Z] µ¥UX§mÙÝËgðé_r2ðtî¶I_$òë.^†§¹a}‰WjÒJI‘¤ÂT¤žeñ™|f_뇬Ž;FýJÞU¨uh—†aá˜xf }ÕZaá˜xf‡¾ñÙ³Ñ êºÇÒWÉ<‚:ËÝè*{–ÐR{ÛY-±^ÝÊ8ƒ¸ÐÚkýÿIs’yúüðª{–жk’ÈÚ-^kΣjû¶<æ#ŸIs’yúü]ÜZžå…ô*n>’R^lÚsÌ!DÕÆ…s·úSœ‹k¯Ìߪ§¹b~„G¾™ìVÒbSæR³ˆå•¯b;ô§9 Ï_™:ª{–Ðö{÷DeRÁ²ï”’‘LÎb±ì&énäîy£žªžå…æO§#oã4*©ñÛ24ŸB†žÃ8*§ñ\ú[€¹;žŠ§¹a}™óeÄ©*H®¤¿äÒSb•õ?Mp”ýÏESܰ¾‹CUpì­¢ñˆ…51ºiI%"®§&siý)ËiûžŠ§®Âú) ¹¶®†ð2 ˆSQšÂRI!QP–PãŠZ¾šå´ÝÏES×a}ŒQÖ͵ˆx<ÃŒŸ€†ÍGOBI¶¢¡ ¥×Tâµòú;–Ów=OrÂú9¥¯€FFAIJ‰ýšÚÐ|BI˜fÅ™m¢²¦±-¸¥«?G]´½ÏES×a}&ž±l†ŸmÒ±HJÉÝšƒP¾€i28Ó)^Xof†Øm»T¤¤ª+ÌÁŸÔWm'sÑTõØ_JJÍ&ÆÒ q¦”™9ƒÙÌÒÛ¦6?Nd1¶„è3"W¶€íCŽŸÔ—Ì|Rw=O]…ôÄ8¤›[Id«§_ u  9´ƒµ8'ê‹æ•sÑTõØ_N‡œ@NÑ|‚v˜ýI±ú‹õê,ƒÚd´^0º—–'êëæ>(ûžŠ§®Âú¤ú³ö+æ>(ûžŠ§ªÂü`® |¨ûžŠ§ªÂü`® |¨ºýOU…øÁ]@ùQuú*ž¯ÆJêÊ‹¯ÑTõX_ŒÔ•W¢©êüd}@ùQuz*ޝPE?‚¨+•?BB£«ñ‘õr¡ôuÿ |¨|£àÔsüeþò¡ôoóüeþò¡åèH?Ïñ—úʇ—¡ ÿ?Æ_è*^‰îŒ¿Ð1CÓèžçøÈºŠ.Dïã2æ(º<ØBé ¤.Š‚ƒüd\Â…G¢©çøÈ¹ƒ]¿ESÏñ’yƒ]¿BB«ñšyƒ}¯ESi{)5P6€l·‹"HIp0œÂm§;V…¢ÃJˆ¾Ôž`ÅkÑTûFû•¹L]žH:›ÄÕ‹&ZqiCë§iirµ‚&Mש}æY]IÑ3ueukÿò¶g2f•ºW©’hÀ¥aUm%ڵѶ¶êÐ…Óþœˆqµ4ºZT¼NÓ5|é 8šE)×é’†ÑBHòÝz…IK4WÛU"ÉåÐADžt«*”2H¥é%¾óÉmÊæÐI*“n…„:nR>ÒBÜS¬ºÉ´ÃÏRê¥Kn&õ$Òx]223Jˆ‰&`’¥oà2Ù¼âÐh_½M†(û^ x5V—±IŸxßróaK¨5S»SˆK«¿RŠÓ'›­ZE=^.¹ ªViUl…¨Ù¨F¾Ë)þ‹µEí+áʦVºÆ.*¥¢dêZ¯Ê%7r¡æ ÀuH}¥©e„)-T9KPÒûêmª[Ø &Zò%EG‚g\£ÿm­“ûN׸ÝS¤J|Ö¬Zu_ª£'ÚS2t'|¨*±p”péÌÔ:ãTO! ­iêœõgö©ÿè¢Y,T<ºg‰–Ñ´( þ¤»ƒ³LÌÃg 2h[ôÌ ¥Ã6ZRJ¸©ÑQSQHÒÛB)›¢s&Ëuì6ÒýÊl1IÙôU6—Ð kR|3;/(IÁ™˜#21yWq\¸•­Çœp!Ŷ§[Šj¡Ö8²qu¨8êÜKu7ê¥ Wˆåì/c<Š÷R„Ô:—RPUAªœ&½…ÔTUm'ƒUlág¦«<”¼º¦—*ª:ª„:ÍCôõœO¸M†);>ЦÒûq§èI°ùÒö|½úêm/BTÕ %0òA‘‘Ù¾ÝÿÓaó¥ìú*«KкUMF}Ê4»RþÎRT{-¢&J¨*è°²Hâz³ú~§ ³v¢…ÖRÅÏ%[9òS´O4nlú†ÐÅÏ“´´µlê¢N¶S(ʸ2Ž ›ƒ&àɸ2n ›ƒ&àɸ2Ž £ƒ(±•PʨeŒeŒeÆÁ!‚‘„‘†††††‘†C†Â*  £.ðÀx`¼0^/ 'F‚êÄÒRe曳èHTÚ^†¾¤®lõÅSS­ê—ظ¥µuN5úmrÓ’Ù0N®¾¦öÌT"„Ðå!žR’¥•Ô6®í;‰Ï›—«,ê «#Zu¶_ÇtÅÕ «V.¸!Ñü£ù„¼/[èôU<¿˜O!òßGšzjyp$H‘"D‰$H‘"D‰ô?y0\‡Ê:|Ýúj¹z™bD‰/ Âð‘"D‰$H’BD‰/&7}dÁr)åèªy}+x“bLI‹Æ/¼/ Âð¼/ âð¼/ Âñ ÂD’'Ùy¦ •‰åçè©åø}i‚ä ™róL–Šž_ƒ @ @I$@Þ7öoðÌ \üãÓSÓø¯p‚BA!§«Î›Úž_Õç™!x©åé÷F„…zF¨i”Ò¶m1•E2Ø].ÏM6ãÔ´äóµ”i§K â»Q³ðZúÒ:üíÀÈEµ]>²DúF;4•OcÁ¨n¡Õ)tôh¦iÜM«Û¢þÍyÿÉõ¦úüøVÕtý@ƒ=–éXiOm¡ÖÝC‰Áa•SÕã½QLUšRb¦¡œVê(°úË]Ï@skɼ¨ã:@ÞqV1P¶TýC›n­£ÏÔ«t×ú‹áê·F”Ü»õ6{žaØv+_jéþ3g»çÀ€iº‚¼…ã:~ïŸ"`;TAKR¸qø®Ÿ½ç™’IçÍÏz„)ZæÙû7{Î3"'Þ7Œ~ yÓ÷º^÷œû÷ÏÜÆ˜²4ǹ›$H›wY¸náG§¡Fÿm?~‘"D‰$H‘"JÍÚ·x(Mãi$–ýaØ_dY,qé¾,.ûdM”ýiåæ— @ @z™d‰&ɲD‰$H›dOª @ @yQ¢-n©ÄžC…ôx @ @zy$H‘"D‰$O­‹bÈ @Y@¼3I²é:°À @ @I"D‰$H‘"D‰$H‘"D‰$H‘>$bºKÁ§s ϾÀ @ G"D‰$H‘"D‰$H¼$H‘"Eáx^„‰ Vâð©\¾ßáx @ @<éâS9qßV†MDmœÝ1íߢDúXÝôø @ @8Eá0¼FýID­ÔA- mŦ-3a¨Ô¦Û&Œœ¥j˜B ¶Ð³Y*Ò·DÊ”HõÍ’$OÜ G‡F¸_­e“p&ÙXV’p–Ö¡iXÚî(—½o‘§AXÕÂY¼§ +KHÐV’R†ÉƒYÔ\Å´¹ØÛ7ÐT÷”ãw4M¤F£4,…Õų®DýxéPt\"Q‘¡WÓêÈ!暇¯ƒ¥D§+m-Ô›zéiÒè¨i–ËVó2¡peŽÒµJ5q ­7¡ ­0û„µiù§„%£mn:Ái‚åfR†ˆ–§°ïÚGd†Û¾0dÖÂ’vlº±¼H›'ÐÆˆôefËY}³iÞ÷zc+7öobNÓ$H‘"D["m%Q¨þt¥F“UcŠmÇÍI¥%xÓ•S‘JãµJA»ªš”ÞJ˜R]u”¡:ḢïY›k%k%A‡bÖkV!áè+[w ¶^þGÝ,qv g¸<´!MÞ6ê®ãÛò Ã)¾ëˆBHØA§ •¤vHmµ8 •6Ü%¨”“‘"tH›'Ì‹ @8eca;´Û#àÅ®âù—«Ýª"Ó!¶l=R ‰l‰£RjJê’µ8¥•‡¢™hmÌòT¼ñs:È-†ƒOÞ:Õ’Ÿ;Ã33'V’ J–£¦8·åµá®L%ãH7I´¬ ÂÉ S¡÷3Sý‹ „)£ JA-ÛÔÔ%)Õ!Tí¥Fˤ£mw2® âDÛ$$H‘"Døçiº³MŸ< EÞoÒ`;Ó‰-& Lx±¢ @"¶5¦$H‘"DŽ`‰¨ì=  dëb¥IRÈùÐËX)%4ÑÔ‚Låߺ´¦AXZSNnå„ %$&¥d·¾…(Á‚È‚ê·ãž W4É;ÄW’w G&\‘ H7pÙKEJ™M,Œ®ô0Ú Ã=ã‚mÆ ˜]ü5É6á”+‹9p©œ¸ç¤'ÖA÷ï£AˆÒDj7̧ dfPv–’#1—{ÈÜ @4@i;$H‘"EàVÈ‘"BÜ5œ‰(-M:m+õƒ®©Õ˜+ BhWñ¬<åâah(št¶¡T”XV”´•3•I(0A*4™­Fœw'2åìs–Ã3‰- q£dªP€ãè4KSŒ›í¸Ù%Ⱦ|Xâ°åöýIjlÐI=öž¦ÔHQT™•¸mŸƒÖÝ2<¦U«çA™­¥ ®œZB8„qÁ0 @[8R$H‘xH5n+dI Ö!õ%Fáš~ Z7ª¨Èì.È0\òiu~„ÙQ7¡–¯š¢|²†M Z›I)H%¶ÝJ’§t–šw ·.‘¢£m-LRßN„ê*gŒ´“¶-³u…a•’$ĉÁ*XbD’´0`¼ÿ‹9×}€ÝV©I¨Ü;„f JHŒÞCMµiêC Q:Êšãœi5¨õ¦–M›Uwì0ZRdJÌ«G*°ÁXV´Ù¸µ,”—›&ÎÒÔÅ$­Öß m©¿6²4@Õ&/¼/‹âð¼/ùÓ>%ý+Ndg­„ Ôë†âì=D•(É£H¨wV´% 7‰)w[l¸à=Ça%J…'È;Ca)5(lßY8å† ÂÐÝNo¸N9iYóm:o½R£Sö† B‡)´Ÿ<9ðà@òУBÈä¼âH¼/ Âð‘xs¶D‰'„ÓæÑ.¤ÔZ Klšˆ–m¢Ók4äe+CVS0N‡Ì›JÜRÁ‚ÒÓfâÖË) »zÒÔM:¢4šOIðNÃÁ-MªâêMö“ÓOûiô ì0ZE÷›‹Ò`½M’2DD¶pLH‘"D‰&Ùl˜žå[êZ,aãiKS °õ0‡WÑ õR4NºkB—Sx´¬ùµkµ ´ÛZOA¨Î™ò-ap~Aé7‡Âk.хݽaÚZ«RESåI‚õ,®ãž¢xP @´ÈGD‰$O”¢¥ ™ì%’©Pn,Ö» …¡–ÒÚqN+Iè¡-ì'ù½V+ AµNÈy”-ù§èÐV|ðJÂçÀ; Œ»Ðdd|òéܾߦ0\#~, @-$H‘ ¬1"D‰Òv-¥+IYók%(KÔíh; …!U&šßz)K¨r­ÜZä•ûKˆ\B°¹ÛÔÊÛ%¼tí(Ò¤%çiÒ‚u„-†Ih¦‘–ƒCÖã‚0\#ð)WuÏN\#Â0^L#D @i‘"D‰$²$H'a‚ÐOºAJ5¿ ô4³mÅdÜUKØ®ZZó é+>mi)R²Ì‡HU¥gͤâÒ1&ó† ÷ ±ÜœÊîI]MAÁÕ¨Ÿ$®¥iãΆ—};sxÉ*Vò8ÜpR[ZÁÓ;sÂ0^Ž#L F‚-R$H‘"ð+ H‘:ÊÃᇡ ¸¬á\%ªÒ°üŸtÑ®ÇC—HžJK&—{„ZXkNT 5­JVõXziKyÂþW)ɶdÜRîÞ×Ì`º Nƒã/G#D F³²D‰$H‘x†Ä–¸ ÁqÏÅI;ªIÞO¨¤4ÝM"MO,›+U &æ h[­<êI< 6åg™•º¥p“Lf…R¬ŠÓÃ;K†~² @ Aˆ;7Û¼I‚Ñ"ð¼$„’CpݨËqytkÝèL¢3 —“33; – Q¨×Tfß h`ÓPEi‚ÒÚ/®¥)h–´ÁiiHI‘S›K:{ºOEÅ]•…óÁ.!X~‚D ¢b4oãO¥Ü_„~Qñ ¬UŒE™piT„_Y.°ÈÜáTÿ: U*‰ÆÛ§KAçÕØ`¬ùµöRÚWŽ¡¢iËJÏe¤¬ù០ä-*B‘®}9pé—y ê‘"D‰$H™¶D‰$O–`¸f RNó¦êí0VŠB3~©Ã[Ö˜+ Ô”ª¿½H›Õ*¼ý† ÂÐE'_ÝÐV|ÚËØ`´!f…)FµZ`¬- :Ûdšš§’¢´¬=¿Ø}ªsun2Ó† JßZÓ ¬=4i—Ÿ2S֥σóåÒ®ç.ø Fˆ Gĉ$H‘"D‚³ç‚V äÁXZdô•‡¡¥Üq×1Ðv–›‹a‚à˜+>mÁp‘cL’Ù°üÂ86Õy>Rb^q²h.!xóÆ @‹OT‰$†$H‘"uŸí.X|#´´©GÑò ‚)7éÍ¢ÐV+p›^3N·‰F/P†Ý¤@; | EƒãÝ1’gÃ0\3Ã>)q'ÂѤ¬;$H‘"D‰9èžÁX|#­?ºƒGÈ0ZO÷Pè.`Áp‹ÌmW[Ë;œÜ“ý’ êšHKiQºEÅII¼Ê[g‡pô˜.‚⼸ @4ŸD‚°Á $N•n­š…4“Ðv-,'þM Âx_3¤Ä/A[&¦ -Urq89«Š!œÕ"µO"›JjmÚ&$ãG2˜“ŽŸSŽ™ÌæRòg"¯¬AÄŽ$q#‰I$üN¤òî¢Nö9TŽÆ–; !¡YR—ú(²¦ŒŽWÍ2ÿÀ¬ýNN3”(’‹%C™9?7œ·ä;'Èå9þ]K—Ô¦ÉÒ*j:ÀèaQR8*°èª:AÂÇ+¬Ng¥É?C«ûЍP)]«ˆù¿'R‡¨U–³£Ë²wKî›çൕEQ™ˆöŸ#í>F+Ú=Q‹öÁŒ·üÛoDc­|QŽ´ñF:ÓÄÇÕâcêð1ïÄÇþ¦?õ1ôø˜ê= u™Ž³1Öf6ÈÆYË/Seêb¬½J-)­M:ìž]Ä|q'(åÑâŽUŸŠ96~(äÙø£‘eàŽE—‰‡²ñFËÄÂØø˜K câ`ì} ¡‚²06_“eù0~¬ÀQêÏwÑäÌ>L÷zò=ßûžïýË .U<3¡®Ê¬¶ÚìªËm®Ê¬¶Úìž[mvUe¶×dòÛk²yi)I ‚Ó—dòÒi¿ì%ÓuÉ,—§.ÊvÚìÛ«þÿÿÄ11@P`!R0Qa ‘¡2Aqbr€ ðÿÚ?ÿ"¤H“$É2L‘&H‘I"D‰$H‘"D·Yd™&I”²–RÊYK)e%%%¥¢”RŠQJ$‰$Kaz)$H‘"D‰$RH‘"H’$‰"H’$K‚½ ãB¸ãЮ8ô+Ž= ãŽúÇô+Ž= ãB¸ãЮ8ô+i¨©•2l©•“[kЭń™AJ)E%/Å×kz³71)‰%è8pN{SЭ•¹ŠM¬žÒô+dl…OÕ‰ ¦ô¶6ô"ûo+cˆƒ×vÞ–È´â¶ð¶'a_|&ÄçƒbO¬C|"± ñˆ‡÷ÞÄÈoƒ¹ †°nb†crè‹‘X‡õaˆoŒD;Ä;D7Á܆ßCˆJcja ðŠÄ7Æ"álNÄ7ÁÜO â˜á§) LjHNL¨}Pº2¡9‘oßbb¾”Á©”àú”””ô))îCmâ‘[B­¼C±ÄBýxˆW]æì©ÏÕž Io0ßdhNEýFÈWï½C}•¡9 §èÎCs;Ú¾ÌÖ "ké™V }òíã6M“–I-õ_j’)E(’öà –®6ôK½ãoD¯ÆÞ‰_½¿z%ÆÞŠ×½7ãoE õNÄÙQYYX¢&TTT&T‘R*EH©"¢eDÑ2eDÉìïE Ö®E(¥"”(R))(ù)ù2((~å'–PRðp²–RÊY&I‹ 2Ls:N§QkžŠ­æD¶¢†ëD½_'FZøL´ð™X{ 2°ûÀeW¼SûL›þ“'Á“~Ëîe"íü™H»&R>ÏÉ”³òecìfV>Æe£ìˆËEÛ—‹¶/±—‹¶/±äEíØò_³ûWóö<¯“Ëù"‚[ÑC}LÉ“újÇ›âw3Îñ;™çøÌóü^æyþ/qçøžçŸ¹çø†cÄ3üŸàÌÆf£3Qÿæfã3q™¸Ìäfr/“9Éœfsù3†pñüUâ4ö7¢‡õq·¢†üm衺ãoE øÛÑCú¸ÛÑC~6ôPß½¿z5×= )N4ô OŽ¿õ¹jcÿ’ÿÿÄB !12A"03PQ`q‘ @aprB¡±#4Rb’‚¢ÁCáSÑ€ ÂÿÚ?ÿèegÿ!/|+á_ øW¾¯}§¢Ç¢ù•ŽW®£ú­Õ\ú«ƒªº_*µ½áÑ_ú-"Ò•}eŽŠ ú7j´+Á_ øW¾¯+U«aWJ¸U®+ŸUp+¡X hW•õ}i ¾Uò¯iXÏb±X¬V+ëUªÕjµZ­Væ<‘nüZ­ Я|+á_ ò¼­V¬V*¬*éWJ¸®+ŠèV`X+Bµ^WÕò¯•|«ÅZU§pº¶«Á^ øW¾ð¯«ÊÕŠÅXUÒ®•q\WÅt+¡X j¼¯+å_*ùWʼUâ­;njÅb±X¬V+ŠÅgvÔ+ºÂÕj´+Á_ HÕ¤jÒ¤ ú¼¯+JÅb¬*éWJ¸®+ŠàWB°, ЭW•õ|«å_*ñV•nð[¨ÄíÔ^{P HV­!ZG-#º«î꯻ª¾Uâ­*Þåž’ Šn¢.Ûé ˜¦êðôÔLSu §&™ ІS„ãÓ!1C†¡”öÎ=4(pÔ2žÓ8ôЮZ†SÚPôß–¡µ ‡¦¼¼43oàPôÜðÔ.à„ÃÓSÃP»‚›Bx!é¹á¨O¦¦wz¬gwªÆwz¬f Ú‹œÆ<½23íDxúhf ÚˆñôÐÌÔNã願#¨ÇtôgJ²Ð«•ú+ÎV¹_rªWèªsJ®Lò¯vLÁDÿvè]€óYf’Éh¬¦‚² Ø-Ö3 Fþ;µT"vžío_Xô_7EE}Tà{»Ь‹vÔ3 FÿvæQh‰Q~S¾ËÑ;Èlr‡1SÊËoE’êöw)IÔv` áºFaá}üw/öíPhž±[°g«Êj£^ÉàîªäwDÌßnrSŽåDÜ@TÔœ`’߯‚£+Õ Å®/ÛÝ0Ôrœw&&à@ &®Ü‹„ÚÝŠ“MSºÅftF£”ã¹ÃÚ€›÷.q‰ðѤ&¢Qk­œfŽSŽã€1@uš8àsgÄG Psqš¼7@!¨¥8î?jîS:À‹/AÇ$ÏLXï¾ç„5§Æ Ã7f,o߯P&±3šQs‚n¢”ã¸ÔñwÚj¯ãøÌÏwzOÚ&þ‘¹­á¨¸’|æÁ;½³Jrû÷$ç0µAñ¤©>ÙèÉÙµ@U»g’çÜw™îïIð›úF掉ۈÎs7Ù;½³JrûÎMnÁÞ‹Œ…^J‹GrKŸqÜf“÷w¤ý¢cÀnhÔNÜOêšHùÝí™ü¾óöà&sÎ ºÁµRiž•­û*O±@MEµ»ìœOêšKŸqÞé¤ýÝá4§Í‰Ú°QñƒÎi3û§¶h¢oE’Ð8OE·Öh‚ª·»FHÿTÇÝ6SAZ6ôZ&ôGð›Ñ;ŒÕˆ­z-z-:'ÜS˜žPþã½çq6¶gùW;ý½Ü§×±Q7ï܈0*‹ªtÑq€T[S>óŸwt§{»Ï÷Á3ÎÆð;‰'Ƶ@á4ZHZWõZWuZWuU½Ýs”‰ó »0Ì—‘Ái]Õi]Õi_Õi_Õ@Ê:VKˆZWuZWuZWuZWuZGu™ÎÛ1ó0Ý uÜV;h™ßº½@ÖÌÖl›ÏQÅ-ý3S·íãÆÁ\îvÓ¹¼õÜQ°Õ1ÂœÃ‡ŽŒ+tÄbíÎç¨Ï ÆiÆÃ7j0·Æ˜Îv ÎuÜj&ÇLAE¸aã!‰¶o3PÜñÇQòÜzï f†"ÄAø®Ñܦ‰Dá†çŽ:–ã‡uAÀÔfí2±óñ17Dý“«sDíã¨ùnE]?I̤˜â[™µ»Ižh!d?Uv<b¦øŽè²[<\`:†ÝÒGËsb „¯Uº#¹X F9+\¯¹i¯9\ŠÉhØ3(ýS·LN5-ψ0Yb*ü8æòžCz¬§n¨œj>[¥’â ñYR}¾n‹æè®¹U'õU@*Þw`z²'å¨ùzf'åê°Ÿ–£åé·-GËÓnZŒzlxj1é±á¨Ç¦ÎÔcÓgj1é³µôÙÚŒzlíF=6v£›;QM¨Ç¦ÎãêÉã¨Ç¦ÇŽ£o¦ÇŽ£o¦Üõ}6ç¨Û­PwÄÀðT¸„#J¾Ö‚4÷6Â"34–˜÷sÔmÖŒ÷;PêTpO€ªES Ó?uðä1¥ÎÅIÈ 6åZT¬›[Fƒ­RmQ0â¥hÈöä†Â®*Eð…'€Z¿Û±¸—*ÕŠscS=ÿåKûTŒ¬£"Jl¬×A6NW)ç“tg_#I°´9|3+7º«æ:ЋÜM€µ0I¼Öê06¡ aµJ¶”Ã[Šl¤“é4˜ %%h¼ØÔé:²m8*Rn¦1]£å(f)²m!Ô«2í. Ö–Åð¯lWaHGjøÚ@6¨Í$ÓarsÙ0R2­eûB§G–*X=‘!¿UIͫ쨱±*l È¦É9¦Ü® âdâbÊìM„“²¬E®!vN²¥ P%¦Ä` …¨ÒaäªÌ5´§0Ú 7ž£n´aTÈòEÀF¨!# c"(ÔÄÉj7pR"§`¥)VMžIÍx¶¸â¤@a¯Š2ÍTÛ'ø¿D\m+±• Íøpbë\T€•°¦IÉ7!†5ℬ­NmM}­ ‡5,;H’¸:/f µíÝìN|!ñNƒö© Þ×\6!ñ°¬TÄÉ8Õ(ó¤ÛN›ÃãÉ7â­/)fƒ NÉ%=²²€¸ŠƒTŸfàTS(>ê‚o„Z¾ ?ùBñ!WEþ£ï)´„)…#îOmTRÃû*¤A¿ñe¤X T£¥ÎD1O7éal§oi¹Tû{Ÿµ|t¿2ÿQéÿò¾ƒ¡ÿ…ðôÿñ•AÑ£W’–÷)Iå6Î áþæ”"”6É)&dlÚ™DB2d¯‹ç÷ROF5"S#˜]pµKHÊH†a´©yYfˆ²¦Çj¤Æ²›­¤1@v\[_ý)iGŠ˜èCoI aµIK>H8¦J»áÇâ`™B ákѨ۸mi56ÏY˜e”#R¬¨ƒ5˜lT)š;¢èFÔ)¾0Tš`U'º%BR9QŒSâëÂ1®54T˜ù{‚¤eƒ¼…©ïýE:TË´TFÔçÆ¯•B‘a+µ¥'Q’hŠd„*i™½¤Œ^1Aí¯É6HµÉŒ–“&…Rr…¹-Õ<µŸ†ûB-øfB•¤©)ƒZøv7ä©(’Z0³ x5߉ŠÝF5$D‹¡Á ROòP"½g¡MÔmÔ’}h>i–sI 6)yIGÁ€ÿ„ÎÌÅ®0­W*ê_Dé:b9\Ó^×ÒaOòj¢~"7”§fàØ%vlxt/;©M»B¥Í1±nT~‰¢¢ç€Tê;@Qh«iA¥±Q¢8^a B.}¥_“è¯Iô_ñtVI+²Jä—Õh¤ú­ ?’Ðä¿/ýËòÿÞ¿.ïä´þKA)Ô- ¯Ñh¥¾‹G-Ñ]–謕þ+þOâ¯?ø«îþ+Ký«Kô+L:¦oÕiYõZV}V’OªÒIõW™ÕZÏä¾^¡aÔ+¿P®}•Å£+Få£wE£wEqÝÓÑXw&ÉÙ¨Û©$›%)^0AÎvÕ,iWIIFP_y"\ööpRµMÍ|3c]_e)П J£Rø—8óO§˜ýSä¥ "d]r_Ž_IÞA=Ï­¨Þ‰0×Ö¿nIÁJ–~ìÄŸ½^U¿UoÕ[õV¬V+ó,V*Ò­V«U½ÛŠèWB¸Œ-Z´ è´ è´ è´¢Ð¡ D´jïÝcÕZîªûú­+ú­<§Uù™Oä¿4þ«ónê¿6W澋ó_Ú>Õ®¯ô«>QÝ’¤âèÒQ{¨l ²¡”©´K`Ò„•“gš â# 5Ó8j6êØ(ƒ¢LJØi ŠœhÂÈ(¡MäÁd> ™y¥µQsêÌQ-µsê®}J»õVªÇu_7UóõV¿ª½(´’‹Jõ¦E§wE§=ŸûVœtZfôZVtZI>Šü’¶EĬ’ê®IõZ6%¡oòZî_—ú¯Ë»ªü»Ö‚QheŠQ\”è¬EóôV»¢¾z-"Ò­(ZP´­ZV­#UöõWÛÕ^U¸¡ìoÛº\Ð#V¾Ü™_Èý©â—Ë/ ÷Ñ=£¼±]£vd”ùnÖž.×Lá¨ÆîÚ­*ñWʾî«Hå¤*ùWÕ¿EhèËÑ]gñZ6tZ9>‹DÅ¡oÕhGR´?R¢{Òe¢Píä©9¶*FïéN2yn´ ecF9I¤Ã)FIWN1ç®™ÃQM™ÃQ·ÓfpÔmôÙœ5}6o FßM›ÃQ·ÓfðÔcÓaÃQ·v#»ÃQ·ÑSB5}YŒå7z²ßMGH7I„²²ÕP#š¢k…S—þ?ûPøfÄüб8–À&âbqLda)ÚÆ[¶8êFñÝ)/`MatA0B8 Àê2xœPkE{1)Òòµ9Âê’÷).?á?–í·Ž¤wJOÚ&¶½¨ÖÑj¤ÒŸ,ëN%8’š „„ 1'ì‹#ªtã^í7Ý©é_r­æh·¢‹%•y1ñ­¶,Þ|mÃuîÔ±ÃÓF{µ%ªÓF{µ VB¬úlÎ:‚%ykÓ ûgW—§,ñð}:/åêÄ@õdz³´*½Yˆ*8ãêÈÙ«^cÕ‘°Õ«#‚€­Xu”w躪µl°(b²~PË|Êt«ÅAG¿”+…kS€8÷ëp ¢=-Û«¶÷UD7¿ÔªiïRYU¨4C¾ ì ³jvTL*Ì0UHÖPq`)Á¢¡W|¼“ÁA¦Á…}ø¢Z£DÃz›.Ë~lØ!mÕ *-³¾ „BÉ´ Ð3“•à+ïÀ(¼€¤p?i£Ü¤mQ0ƲÁoè¸Ä¡ ‡~RPì¢9¯&ˆóN$@Ç0ÁÆŒL|јÿÒ=˜Éﺸ"P tI(º [Ý0žéVnt¬°ˆ§°àse5ÄO~¢¢O|¨•½ðòÔ *±A€@UßqŠuD§:?=˾¥Q¢b¨Ñ­Cº\m*€³Ãáx‘ÑRy±µ*¢K£5ÓÓ½F™Z“¢aúªµ>}ÚS¿ uR4 nt"Ÿ@˜´G¾a ‚ðT(ålDF¹cöÉfã›k”w&1¯ÀPi€TñLoéúNÀ(–ÔhØ*ãà™ød˜ÈÕmòU`;µ¨0kmDµàú׌&‘‡ÈOÕ>ƒ \ {òÞТÑG‚%§`R¾îü±ªÖÖ…88y'&À™T ¡ß ík‚shšŒ8Ukc³S— ä1n¥¸¢Zs‘†áŠv+á7*°s&RTÂ-€Ö@’â!Ã0ÈDº6'7²µÑNŽL* ̺¸Š5qM‰®lî–àfsà"U@ #Þe*hµK(ÊE9”ë¥ÚNa⥉§Tv)rê4à ‰îQÚÒ›J.ÿ àºÂŸ\ `Ÿˆ4F*W.ïzMÎs²‚m ëZD"aàj)Æ«V>,l5j[SX9æÀ ²-ÂñóÍ4Q¬Ø s0 á×1qÌÒ Hæ† ‰qɉZª†UgË0#bqux6?¥ Mº3!Ñ­9´¢ê=ÐE¨4؉N'ã[h£Tb UCºÖ>5˜cjihÑv1‚”Lbž)ˆ9–ù¦49¹)ÐÛâÁÇ`ãóaš")î†YÇ4ùHˆÂBœs ¦#Ï’‹DnÕaVà´ÛƒcTs-ljŠkë¬k;u”û]Ñj0³Æ†Ð©dˆøÁVñÿH–Ù›¤BkHªô¥f¨º¨Œ‘›qf°ŠÔ=žî-FHkïÖ…YNÍF #„¬ç#œéüÈUPHŠl©ÃOžd4'¾J·ÑóC+4ÂèâL+6fÛKÒ,æ5+͆b/°V‹Žf ¿ð…€@fƒ{ðÛ#˜É Ôb8¢ "N¸ sMkqÊD‹3RmýÊPùæå*Ëe|·9G[TˆÍR ôÜ …YŠn5lRŽ„*ƒDæ(PB=ö·j9ѳ5 D5 ]°§XsÿÛGñ±´§7aÖvn ¹›S[€îF¼ÁsEÒ¤Q…™ªì•Ñ8; Ф+M"4ŽjD«ž3å³ÚG’¾ôhÙšc`!R”eŸ=`ëÔUg4caT¡œãŽk·”þ–¢çf¥…T{Z^IÜs@JD¾JIœ˜æãÙºŽ4¥FöÖsn“xÉ(ºM¹Y¶™I˜Z›'lÍ0«¨>vÇQHº‰5B¯$×k,>JHØ AL„‘lZqýý%‘A”g4it¥7@4¢Öº #K ¾‚^Q9¦»b¦] ¡DX*½sps RêÑóÍ@9F•hEÊN1¶Ô[URY ¹£C€11N‹j"°² ªxÐï  ¢O™T…„ø ‚Ã~ƒ•rMQ X–mñ”Ú¬Gi5¡°gü…ªucd>y†‚œ1Tè¯,J4lÌÜ*¶ŸES(ÓŠ¾Œ‹9æIØ#¤?E Þd®ÊNÌNÜÉ~ F  sMu(E>†ê¨ÅDæ]Uª% ™¦µ‰¬¨Ç4Öí(XÑ(eY ÖPŠt¡`‚4[^j”*š°wÐÀ¼¡ÓÞm…H⢠£7%%Ìæß îK´–ÂÀ£š‘Úá€E€á›¬j\¡ s ›¬ ¥›bvÁVh4ƦuR‡Ï4hØÁ›Éê´¢*‹‡‡d†)õØjÎSk«Ù«GžñDgh+G±m{src÷üÜoÍj¨©9Cz9¦µ9½¨ìN”’¨¶ÜØ#šuUä!z¤[êÑ¿!Ã\m9¾ÆS’‰–©6M—š“âIæ(ÉÉÖ]np4᜔(§‘dum¾ˆµÛ söçcDÃÄS-ªyWbß‹­Qhô Ûœ4Òˆ9ªE±FË,Z%q:ŒkÔžɪô1ÞOÎÚí\€1CÍ{'Ë?6Ç~ˆ†v_®¯áŸÿÄ,!1 AQ0aq@P`‘¡ðñ±ÁÑpáÿÚ?!ôQ×ÏÀö7ÕÇO~šè­×Fõ}ëŠ'ɵ{Órú^‰8®FFˆÈŒI6Ñ‹ sŒä‚ü'’i7«¼‹¹²7¦Ù¢ÜÉ;ª-éÛœÒÔx‘©¶{Õ1è'Fuoè&»kçÓîG]kßOUn>†õ\SmoEµqÒpoVˆb'jwÑ7¤f’pr1âQÁcjE'ï¢KuSs Åh;S2äE♢paœ‘³Á°w‚éžÿ~—=$vÖº[ŠÓÓUÏCzdYbÔÐÖú#sc}6®iÍ‘:gcr ‘z:nN—[S³òp*;ïI'=ŽÑ›2 dŠpLÉ´\à|›Él¢ðf ¡1ܵÇpv=É–<[ÑGSn®'Öfœ×7¥t9èÅZÕ¹{šn©É±±4ÜŤßFÄÓ¹0X›œŠiS¢Á±$fc%é°Þ+bnEˆ¹y£Ái£“MŒÁ†Z]{Òn80qq÷;Itm'r.<£±±…–š2EE͇”rn™¸çÓO«Ç§ÃѶ¥Y¤hãBé>†V­‰¶œIµw¢ÓÅ9¤o®né±Ú·Ê7U•® ˜$ÙÕ<ÑÅšÅ^ÔɲïK %cØÈ¸bjÒZ÷-bU‰eRêI´FYx™‘’ãtðõGAugDhj=&ÞŸ&Ç"úðgZ1EÅ-\ik¡6ÓÆˆ6¦Õ{‘&Å•3K¤S;ˆ DR*ˆ¢äA[T] $Iî:\Éf)ÀÔª;‰ìè‹5l·ÅsK›¼–7êG§z&³ƒmSé¬hƧI&¼hDd·B4AÁÁš=ŒÒ)‚ ÐÕ"#G±DGH ‚ ‚)m2¹;èý¡ú¡ÿì£÷õó|_¹Þ}U!vþìRÝ嵢߷¡þD~„v¿Aßý‡ú'gF2~èMI œRÆhrpA{Ú–cM1©Ât…aŠFíàÊ7"ø§&æßz¶ø$cDXÚ‘1¦)ŠÁADm1KRÕ±byÁñ†Ÿöþá~¨ÓF;!þ“;Ÿ¡Þ}È´®ª5ýÉ—ì4 ·ÍôŸ¢;o¡ù’;ÿ°Ú ·ÿ®~ý“ÿ±6ãÞxjS«) /Äà;ú ¯ÈËrÎó<§œì‚Þèc™¢ÜœÎÕÊÍ$ïHCPAÀ¦.FÃEÝ÷"pd¹24’æå©ÜŸ‡AíÕ‹éAŽ„¢Q+“¸]ˆWÞ?t?ý±¯ýOØÑ;ZÊŽçèw~Ql±øIÛ}t¡éø‡oô¥ï†Ú 7úŸ·d¿î~àòR—¾ àOƼ0{‡pŸ$ù%Éäyä5ÑÚ;D8;8D#¡ŸCâ™\j ÝîaŽ·Ø—Èä‰Ï¥ùlÉ8¹¹kÑEïTæN(§ƒ'&WKÚ±H"°^AH ‚)AR+V4«RÄ7;#ö‡ïOÚŸ¹ª‡d~$>oÐüHì}:6K¸úý†ÿ°ÿhýµÐgâGoô¤…hDÞŸ³gìYûfwßÔóÉBÈä—'—@TH!D""AxÕ5’Ä®I\äï˜ïÊ.ùß;º C†B‚BZþ³Ôð'1ÉRCœ¤Ù¼{T³‡H؃’¢Y6Ý#(š,l¹éMÙ°ì]@ß'‚¸›à¶¨¥‹ÓbʼnGpíŽÀûGíÞ¡¯ýûŠê>Üüi÷Ðý!øQÚútz“æ}OÎOÊÏ܃oŒþ~€|_¡ùÒ;ϵpÿz~ü—ýŽøóÉeË—©rüÈd>IÜ‚5…HAYjX’Q(Ü C@'‰âxž¸%ÁØ;glíÖ&‡™ŽáÝ;ôâ\žG™Hé¢íš}q£´Cƒ°Bà·¨ºŸc7>¨Æw²2H÷¡¾å™Áqp=ÍŒ05Ò7ffaD{eNÄ q`) ïrþ$ޱãûCö¿ý‡ïïYûÖ~ðî>´xÔ{#Ø—ÁråË—.\½!Gr ‚"‘Hõö$±(žô’Q‘¦<'‰âOƒ°I±Ø%ðv ÄðÑâŸvxïâ|žG•H @…(‘ C‚ˆ ‚=TúH‚°}–‰cŠÛOrqcÁr.Ï&H‘䱃º*¶=ÍÌiCpÍá!†ÜAV)i‚=,ÒI$•D“Rh’I|É|ËêB iL™2tùkÎ$x"Fµ!B"¾I¾”º?ìª'“zw62L^]‡š2yîfÃ"åœ0‹¦;?bæ;Ó"¸ƒÐ‰Ÿxð«bQ4”J MI$ð“؞ijÀð£Âˆ"¢Ÿ$ÉrKP @‰<¯!A ¶×4Ú‹Dt´™7%œœRôíØÁ6W%\h]1>LI—ËL‚ca1YJå¥6}/Vû‘"Dí;Dx"@ E¨‚)b)ŸT®×ËðeU±ö‡Ú±˜¼™ÃÅòBÍ'&Pæ,^ÔÆÅ¹8’GÐ!hÿKãctbEä‡uµ&6Õ/j0UZò/G?-GªCi^uF¥©Y—Ó: ûj+î+^iÜÛÝØ’mƒdAÁf4fÄ!í(´ 8NLª$+<’Om?pÕ¨~ŠÞŽ:ÏæT`¬ûö‚ÜF ½è˜™û'“° N Ì™"E2ÒŒ[bW‚{i2×ׇ…G½}ºqðëO©V¾­¾-¶‡Ž¨#2;GR转[Â8wYC'Dœ Ƀ%ŒØwØØìam ˆx¡¼!z=½>ß Ç¢~zI—ZG2P¾ ‚í‘Ðó¡¬L>Ãäö-bÍ í"¶HW†`Š7†l$NkЈ{Ïàt_}8Ÿ™ãÕ[G!³Ý›‘(ÞI÷89¤IÉ$ $Ix!ù%"Ù#ÄöÁ9°FF M§ªÿ°ð°^µu#ãv®õQDßÖá‘iÝî:Þœ“‰=†ˆ¸²Æ¤ÄœØ²ÄCyðn†®5,Ï z÷ÿ™€¾+·¦‡¡QüÔ輞ä–,K7H³Üˆ³9jmi–gq]½Í”‰Îø7raª3¶„}Æ£ÛÖž›zlü1ü”ÐÐîÙ2SùϵjdN x6ÁÉ"jØØ¼&'ŠÝ2Ó&å¹»2M®yà‰.7Ént#îU°Fž¼t3é6õ+â‹Î!1åhÊFãÍ;žÅ®n¨™;I‡¸ç#¾Ä4)¹| Ì^HDhGÚ³m0^¥>™|'Ÿ—6?'ð ÜT½SÈŽs+#ƒ*`rJ±ðaJø†6%˜Ü½nôäð<<Ñá|ʨóT?‰>šé(0y1¡ÔIfòL@Ú¬äÜ Í™®ðqMÆqcü†JÃðFÆœ†ôÁ ©·üztÁ²Tbòaì¥ÛÁ}ÒNa1'µ1 ©¢ÈÓÎÄXq½Y“c'2ìmWƒ-ìLm:vfï4Á +ü%8¿ÁŸÆR: ­>ò‰Ðƒf‰ÅÒ68Ä›Óqd¹2Ïs¼RÇ›28eÕű7t¶Âï§cxBp%<¢Eñu¢~ZšO¡Æ­‡–îJtÙ‹Áº—NÃV°‘t9Ê%™MjcÀ­c±ºÆÈÚ˜z³òSa­|Yú¸è¿‘#¤ô||«.dSСæÄe ûÜܹ{—3e.¾´Ý™‹é|‘3rV¹nÚÅõ}î~flað,õÏøÔ?œû±Í#ƒx9q¸jÃjªEt`À©º#ƒ| ‹‘©N vÜkè-_u¦ò…ÓÎ}ÈΫÑEÈî7‚i¸×Ã$Lƒù%¿&åÓw°Ò#& Æ-#ïÍ00оKJ\EÌO›ý?Ý Y?ÙŸ’õîÿ$üÔ{ 4áÊ|?Hý ˆz÷Ì!;¢‰C#7g½%a–! ¹–š\{l&2ÎâØJgm_tþjaòTR ¯•oغ5¾ˆG1t“GžDËÞ‰vû›þ7>“j-zŒŽ&ä\Ê£Á±7Ch´äpÐÌ*_4]×Ù«Å1ƒdÖ11¡Sï¿Í00ôkÖqë!=ÓT­÷ gù”ÎQ. FÚë͵ã72E¾ˆ_ÄÕý#pN")™ÉätÁ•jl_‘b‘bEìdsh¹,¹f/$oKk'Üqrï ÂiîCس:~íüÓÐ_ ¤6Ô´K'îÄž÷sûK’M$–&DW²wBöKî1ïrα$Ä?\]FÏCø<ú9ôHxgòV·Ü1“Šì]!ĦlÆÒ¹a34W ^ “‹F&'5äjEm _óL >H¶í¸Ù›—»­…àÉ”ž¢dG¦ìÿ¢Ñ6k/¶äC‹{Sù kcù«¿fÔjÇLžhüžH!ʼnäÛN‹¶®/b_¹Ün]:àTû­00Ñæ³ð ×Sˆi÷ ì˜Tz9ýÃе“Nã!q~n%OI)1É££ùixzüÔ„\Š¢úva²9¥žãlËpf ‘l¸¨Èb¿}¦ ÃälÈùì,L%I%î>ÞËeé>ª?À‡)¨¾¥²e×Ô|exô3¡äo©Áäd£ƒ.¾ÔáÉ> ר“¹»"|¢I£äÙ '³Õ÷ÚKMÜÇäUUîn¤ Zœƒò*úY6õÛŽôpöß Sß&™ j¸üŒ‚ôiðm_z+QXÁì's $5½îZç,µ™ØˆØÝ«ïôxfšmÖ€(¹m ÙÝˤ‡w÷‰82þ¡3Àc’’idxû®½]ºË?šd1uÉ"˜¥íD^sK›® î`›(2rOaZ‘´Žé&译^Vçarµ}ÄCÁ‡Á$o×Û ºZt"Yìáz”ãÖØÖ?ˆ+CŽ´t³´NË«ÊPXpcbi&v¤!n\på–qzû–A÷š`aò¿\ B,¬©~o_¿ª!8?õV©¼–;1eCN£øRi&îýFs=>›Ø÷"Ét•¹jÞ¹v%RΨ¹Øö|úé-ÈH»©§å·ë(Úkî4©e*"·Þõvé%/àÙŒôÇàûʤÞ)adc½.™’ZØÊ<›Waø£¹c”Ѹ×Ð÷è& ãê¾ýž7"IBJ-[ÈGt¼(­ú'ÉÇÅW¥*ãð}ˆðOm^ ð'(ò?$173±åQÒg|Gf‹«eX“Û¡?!g• ý/\Ž2ËÃ3HVÖhz×NýϨË_ƒì‡Š+éS]©¸ßj.Éaà’_ÐPÆ‘´±;R4*}íÃãʨVïs÷¤zì—òý{·›¾ÔÄ»¨ýz%‡>©uòê^¼–~t5Y¢Óz"Q ·%Ð˨¤^L2u—ƒ ¿Ž*pS¯ç’Ó‹ÏÂÚÉg§üé0F”{BˆÚWJ~ƒøBSXõ¨Ï]ÒÎIí¦*ËÖI‚ÂFäö§Eͪ°fi$’H  WÁç¨ºŠ—nwH«8ûÕL´LácèÐÔ…wDúˆö+Øô1˜¸” ö=×P)‹º*ÓÉý`‘ 5 ˜¸4_üéwo­Óu˜ÙN>ŸK¥Ç¨Ï]ÖK%Éfhb ’¥¶¤èÞŠw,af¢¸“¡ ©Œ~%5禋tÑ~ytFzA¹oI=æÛÖÐôØL³Ïî ,-øw–تò>ßOÀí¥óGXowßDzKzIõ¹Ì4Þ[ã´Aµ&¸$–6õÄXä\‹'¥ «fañ,u‘q査ôû܇kûŸ¶§Ü¹1Åvæ‰òFî,^]óCt$@|èqïœ‰Ñ `ǰêÒª·âöÕïb:yŒ?·ª\ºï¦ºK§”ÃMçÚ+q7M´dΉ‡rVObÐv1'¹‚íÍ„)Þåžz àÇÑGÃP„ŽëQí~/ÐØe‘…%ÂM,mJ_šn‡µ¢ÙJìä"HJ¢ä'pnÖ†Í\åÓ:Õoðûi̾˜¾¬5BÒ4!úteª²>À3‘Q|ÒÔMU™T$ÉØŠc&c˜ObÓ}Uo?BOÊïOÉsÐd6N+,Ô¡iä˜C3ú—ö®(„–á!³toä{é>Ý۪º,§ü~ÚsGØþKì¦eé‡$Õd¹<¢mWrOrk‚2#&Ôqa îZMñY¢u? ^•>åO°ÿ= X–Ü$x ÐÑ´áJ£ˆGÃÏ’Xí½Š¾‹(ÿ7¶œÐŸÊ(Ç?’\tÌ}ªRÓ¢ûVOB‚ÒZ˜7¦ÄÊ.DM²X’I.*‡ƒÏÇQ,8jyÊ+ù}ëÈÉ+¾š!.,‹‰IÖ퉵Ǒ)–¤±©—.… Gæv¢ÏäÅ1 .9 \(§Ñï§Å½ºK¥ã¦C…°é¹¹sÚ+Tîn-ë=© n‚?Gƒ^®sÒç1> ÷:xòZe¡${QYìŠ6‘¶ì‹ówê§¾’ä•Z™O:I\Ïç‚çRƒ°r¤qÿX[ÿF\_EUG ‡*GÝ|¿ñâH0¼&wŠ·w¿Ÿ×Ûá~fòcðZ™ª2E5D2KTâ±ÁsÈü—BðIm*¥àÇäÏ™§±ßGB&àì]ŸD=ü´(Æïôh¢2Ù+'à½$‡»Óö‚¡{—àÎòÿc!äÐðÓ'“b–Þ›iTsIíW^ ’6¼WhB>ÍQàÇäÌrŒ>¡áPÚ•IÑ9N4Bί.N¦²iàT ¸M­_dšÉ)¾Q¥ZĶÒ>Íî)¿ÚÏÚÏÚÄ0þ¼Hï2µ#Kh1ãK­a¦o& Kѽ.mWLV`u”ÜE,(.ObQ‡ähÎÆúöâ-1ù#YX]‡IÚ¹ô‹JË%­•é*ÏyûÕüz:F…£Ú³IU]•|é~t{éGÙˆx1ùÞïæ‘h»ëˆ½ÖêÂmžg=¨ÇòPc Ë Ã‹::ûˆÃ£/¢a£‘§2¨´ý°‡†cò¥Ú÷t@2ˆhÝ3oj~™ %ïºö¤PöóGòIŠ˜BÀô*Hú78ѹ‚)†F’öÊ<|ÞšÛt‘2åÿaR 7·ÇÕ¡ÇÂP­KYí­/âhkÑ`0Q °ô©%ÖôØ\¯MŒQÈ™cÒ¶¦Æ>²> ‹™µïE¡)¨g‘íê’É"¹¨œÌ¸Òô?‡Ú¬‘‡`­íEr"“Tõ{Vó«ÈîY<HÒFz±õ˜GÁâg¶Þéû ‘ÃOmÒ!$¯Îô[™ )c†E›Ò;W¢Þ éGñ)±ÈhP1³”N ©‡¤[¨‘ïýß°’I%…Mãñ°ßɘ .Åêô+ScßG¹riUy.Lìf“ÊÒ´LjþAEóþå~ºzôäõÙ2Äç …EJqvî=›nî­/Ûä±a™±ïKäÍ3ÑÁšIPr(f Y‹­Ë=KFÇáXõHh>¹?ÕlÑLfš†ÓÛ¡.#n”êˆñŒ*M™{„ˆ›­?ØN=J °3æã ½n‰=ŒˆrOjm¡Í‰,é‡_Jѱø#×.#Ò¸ÛL‘=ÿcþÏ`—³ëAB+ ¼A waÅ7QÂ~ “­¦ÍÖWN~+=a3ï¨Ç®ÕÞ–#Fîi’tpÑ2…éZ&¥FïáЦv„J{ÑíVlÏí“ʲ#£Hx’RDÿhB<ïz«Ô¡É›N©jãfÛnïä)$šæ6)-©¦)ì'£ÉŽô´tY!Ε£aðtÒϯTM 3o:ÝWµ0eÝýŽÃ‚ýäH‡Ã³$ABv8,5^ǧÎõ{Q²sù@ÖOÂæR·¬¿G:6t{kº=õ=õÅlæÂàŽä9þËðI5Z6?$I"ó ­Ðë*{HÏìh8»£ÿ<7ÜüôDÿâr6#´ m´—z"&q³ôo£Z}ôZ¸-Å2*]hG½=È’8;HÒ«¹0øi¥oO>‰Q2\Û±¼Ü3 '‰¦¥9ébWl±i5ûmô©?&ÛTàÈ/D±èW¯š`h¹/‚ÂÎGâlxô!*Ã䩬‰Ï²33~(fǸ[µRר?Ùë9ý§…ö“Yôû|9i}4ò0ø£]8.ŒÕ.gOå‹^×'hÒ„6ù':&²I$“D’M&“ðD‡g=(ôSéÓÈÇä>”Öú%ŽE&ÔÝÒúQ×*>)= 'ªþM™).2ǹz_G±b8%Ò)èÅК‹¢ ÕU@„aññR·Á§ådã›:*krtÊ×Þk÷<èFøðº±ÐÅLæz¬ë<èÉ}8¤Ó:®^G|ŽUPÖÔ¿æé˜Ì.&õ.ÚÑmÏzªOj^–z‘öTçäþR·QëÃLôçKÖÚ¯®4^‘ÐB>ˆ_ó tÏZÆ‹Vôte©Gf{ÓÔdiU‘3í*¾ Ì?á*4gÙ ’è—§Ü][ î`cEáj}¥ÌÏ䥡2ñ£Ñî_¡‚®ú,mGDn!oEÿ"ZM×eíLišoбì$pg5’D}•WÈ);öùE[­>Md¶…7cbG[W;jLû*.§oM?*¦ãàshõ˜ûh±b¶‹èdÓØF¯s4`µ§ÿŸQ4U_Á/M«ïK—$U„ÈC¶Œè‹ä¿DüuV„Þ‹[éšÉ —3ѹ•©|ëª1>à}§µrm«©î[D=*¡ÌrS1= ¥ú ª» ÈÓà¿LÍU̼)ÖÕZ&“Oj÷­©ì`pˆ=‰¶¨%ªWü’Ýe9ô,{õ"¸f -©-|Žíñ<ôÛ@z¢±§4¹/~Oz@‰Ð´Eÿ0ÉM¦Pë:±¢â7è:A·Aý•_ó ”Ú8zò´Ý £jË­ëgÒ#áLQ–Ò^ã¥fàKAæþøJ$•§ë fHšµF< â‰èÏaÇZW4šÊç匔Ú4zõ²ôW,è®Z] “UY >$ÎÃsÞ7t¤­äoÉç&ö¦.D¼‡7!œ…÷…¸N†WŸöݶ0È)]]Ñ@}¬u/¹1\|¯‚èaðãó}E„™ò#ÈÅv|<‘ÌÛƒÚ+ïrsb7B6klnâ»Õå‰.€ë¶;ÌöÛ½¨£å±aó„2 ~Ex]Ëyx“­^C‹“;9,‡í*.ÙBP¢þ1Á³â¾ ]ìà0i„^ù}Âôo&rØyLÛ/ î'cHÁõœYFqÎL• Ù¸~yiÙ¡MYø²D–I¸ò.“LÒ²HÛ{%$=“~Ä7ŸÒ²¹¤®Hbå}ˆ–ÑÛã8¤×'LÑü¡žÄ —“"Ó:{.M.,%[óH"‰|TY¹ý QZÉ—wX:Òû¤‰mˆ;¿åBöLþ,)´Ó¶L™"a‹¹h|Dæ78 ¶Y/É™Æc±>ç¸-|ªnd:™²%˜ù_æ=öDJûaÄEÇvn¹À…Ýv}Â!)M°ÞÙF~RÒÏk²ÔÀëi¼!RR¹l{±ñÙ4Ë'À‡ÞjxS†ÿѱy÷ ’Oú6JSršn;N÷'ƒôHBßéZ0BIno–+Ööo „¶7v_¿ØzN“mÎj`%-Ih Q5%ùdp‹-¬üîÇ‘)­˜0’kȸ¼j*!¼cßZî\#‰g5¶ÒÝÆ wm¶û±Y¦Ø1~$L; À´Å;ÎÂòÖ ÕØvn1Ü`÷$f~­ŠÈ\m†2ûê&\Ñu-é3tÍxôyÑí¢ú°f“«sRKÑZ²è#ã/ñxô µ†é #ò˸—Œl/ƒpIË[ åÄ]ì-0šÃCm¶Û–îÙÜlÝi£’É‘ÄhÄÀPl$À—¤)û’Öàö;[ne2r°Š°»‡½QrL43å{>øâ<ìdŽÀO¿$vÜO4eªÅ/o¶[>›NSDÆ%Z%¸W¸¯sÉùü´ úíÀ@…ÃÊfá¹Ý˜ïñœÝ3GÞ¾®ôuLÅ"›Óй ßB1øµh}o†¾—œ ÏÈ;é€þ`ô­{hÆ‹QžÆi ‘7ÉI¿à…×­† Ò ršFSPënsŽO§GnŒ’¹'¯?2o¦ -Ò+5ÏKºÑzgFLþÏ–PÕ€)í§õ#E¾¼ )ònRlü‚šDì‹Íò4¢ö~£›7l"äöüÂnØvHEÝØ- eï.¿úAóqŒ²z·Ü¡ÙdY'³ß%Ýza¶…¢³?ÖŸœÏßÞÛ(¾Ø÷ƒôõíXÿÄGÿ-!ݧŸÊÿ§uõ!¿Iþ(ÏÃÿâoð)ÿD »fíƒñ1ßýQù*´o؆Œýaû±úâ’¹%rJçW¿ÇYiËš<¶U/[ßEb¯VÄWܹqŒ}Ô^¡=”cáº#»@PÒvN!(EÕðK‡–*Ǹ‘ÞŸô Í£ÿG8¡Ûu¶¢5°äÆolaò¥´¼žÅœKýdT¾d}c‘ÅÌ-vˆÈ°3c7›çli\ÿƒõƒ¶ú×è?‰Ï¨ŽQä{£»÷ï¾ßhëw‘à4ÍÝÌ} =“°MÎy [ D-:Dè;‡°o ®vU9ÞýgîµÏ÷(;÷Èêé“ÿÁþÑ?ŸÿtFSø¿ðüwø~OþP?;£¿C?N?Z;oÇš¨?3"_£?G«ÎÎCºú™ª¥Íý½=Kÿ0)îÕD»»ðe|2û™ÌÀmZ·™º.A Ü¼¢¹be·Ãv«ó©á˜:<+Ö£ìýªèõ¥òåËäî3¾úŸ²?`~ÅÒŸ¸?cJî>ÇoôQ.ßÑ?D'F¿ŽÿÓòÕ›*]½G²Ò‰éþó•d‚Ç-l>QIÃ$øÄáý‰Ü÷õaZþ~ OZõ]Æ+`š*XF?>sñl hÇÙG­ªï£:q«jY•YdÿÆÑ6N«_i£Ñ3Yëm¢(Œ|Ÿü? x0«ì½7”oT@ë?JO:Q÷Ÿó&ÚÇÕè\šÉïH÷cåfV…ÀÆ›·ÑMv¬Ò㪬Bßâ1ñ÷9õóð *n>Á×CZÙ4ì1!’GÞè–K%’Éd‰i¿ ð"@¢Q(”J%½TáHùÞ ¡d}›ÐÙéµ™CIbgÝ|ä²Y/’T¦L™2G… ¾$H‘#X‰EÈJä•É+’W¥—b~ œ|WË#ìǦýI¤“®ÜRY>ûâê/É/’_$ò;‡pïh‚PH™> ðx“ˆÚ<¡5¨%¨ìO­óªz[ãÅ£ŸZö¬²Ï²¡¶‰¬Òô³!— d¾Ͼø„Ú>1£B­Éd’É$–K'±$’I$“D’I$’OJ}\éÎ W]7"u±aà’Õös4©ëÿ ‚ ‚„B§•TK’\’'9’”G$ˆ# ð.Ob{Ô’I$’Q%‰'ZþqcT“ÐÛFkj*F…p4àÁ¸“äû߀Ú>K_EËéŽÄ. pBàŽ`ìŠÔy©ƒÊëöèæ³ILq[¢FërÁ#^ŠO¾7øn>rbèI=?¶W~­Ïj+ª’ô’à °…NbHOßøGbúl0ø$ÿmË.«æBµŽ6æ5x,£ð·Ék?,‘ÇŠÖ|·öºn^“_:Þ™ZíWbâÌa |8rø0¿$@óy‘6Ò›ØZxmÛZäîl8;÷¯àûàŸñçå#¥ö_挞“é·Š:AÒâoå*ÉPÌJ IûÏø!K_Á.¾Áಥ§,}9fÇ%¥ÎÅmŽ~ZûóG¦Î˜ªš­}ÍèõI,‹bnК-,«¯/ä$´(¼Š¡õæ’bÛŒ%m±›GâC±þÀƒÔLêݶ¯÷ò§Ù µ:ÅQ¬ÒÔ¹sØÜù$‚EAJ ±2êÏüJÞ—íÔE4ºTÓ4t…¼3¹›_ØŒ Œ‚Õt£þ?öoA¸ôÜTܹ䋗 ¸ƒTlá!+]Üo,/H"A"(‚)$šX¶¥=‹RÅ‹RÔ±ï®åË—èΉ¤’I4’~hûJuæÅëµndtÚ°>¾Æ8ÿ´}VæÄA¾Œ(’Wl¹,’I&‰&‰D¢Å‹,{žç½"—Ñ’ Ž”èž»ßFKéßDWØÜc.5ÀK'aðºì%ª ‚ ‚¢ ‚ ‚ ‚ô –þªõ–K%’Éd²Y/‚I$’M°J,X±bÕ÷=éQ)!‘ðÖ*É/¬ß¬wÑ¿Y¡y0ô;j?¯MècȵÍ$ž­µE`‚ ‚"ˆ ‚(Š ‚¢ ‚ ‚)um)qoz\–&K%’ÉpOaCj J%QbhX±ï¢,A ‘ ¹zÊÑ+§‰ ‚+ ]€ú—ÒèÄE'Dú&äd’I$’MI$’I$’I4’I$’I¬’O®‚ ‚ ‚ ‚ ‚ ‚ ‚ ‚#Dz™ìOm[d’_'¹=É$xžàC‚¡(‘nKr{žç¹0È¥¢(¿bý‹—.Kà¿’I$¢KŒZ³`:ßDú%DlG]½"I¤’I$’I$’I$’I$’I$’I$’I$’I$“ð¬ADEDTŠ ‚ Š ‚ ‚)qè¦Ñ¢Å‰VAb踂¤2åË’%’ÉQ-úD¦Ú‘³Ô·\G£ÿ2d‰$C!ý4ÒI$’I$’I$’I©:Bhž€ &¤’I$“Ð’i:'¥AAé@ ‚ ŠÁ Vb­¨ªÄj Æ C}†\[è"ôCTZvø´Á„B!D"GG™"D‰!È~¦I$’I$’I'ªI$’I$’I$’I$’I4’I'¯AQQÒC0¤ÕˆpébEµÚMHú…N>‰ü> „B @$kùèÓ&H‘"D2 ‡é$’I$’I$’Iõ@^ $’I$”J­´ZA u83X'BhW’‡†Gê{òüˆD DCĉ$C!_ÑI$’I$“êÁ¾—zC$‚i08f’¨ša ‰MìõX³qÂ/\hïMéqÑ‚5GÃbËÖA"×üÉ$C!ý’I$’I5®KŠ`™ W ˜«ŠLÒ ùCèo£Íp=8¤zFyÄ¢c,ZG|'a=´™íDË’:Ý„ØåeA$ÑtϬ‚Û…"‘¦= QðøÑ„B @$5û"Dˆd2 ‡§ :A4—L¤ÉÕœ¯$=6Órz“DXÛЬ…7ƒH'IK_ÝŠÃe{½Æåè3¢Š.ð"¿9Y[þ]’öËuftËIJD»8|ëÁ.쟢POš@ÐÓF8“œq#øÊ·ql§ –I"sY¢jCÖÁAAAuñ;B 5HUÅfH Å0LO?¹V¬R t¤ß¥¶¦--Ýû†rÄ*;±"ŒÕÓ£4ÀÊ~‡ S/°ƒ Ñ&Ö'‘ŽlÒe®O¡?CÀü…G=ˆâ”îüçåé\Õè±ñÎyò$Oø•Š’FQ0nØ©ŽA¬éËÚ ƒÜ¿$“ª¤¦ö¶[IÜA,’t¤’hºï£AjAAAA­wø ‘¡Ö/F׸™2±Ä¸®K¦ ¤Ò`ÌÒr„¦Ù#éo¯jo§Õ=îJ¬Ð¯l¿fêþåÂi‹Àå®M©¶û"M[Øf-ªWXCê/#o;*/»‡TD·d^"Ô±\@¤úJ‹ X7°¬7¶XÙXs—½[µhm¬Ÿs…™xì_WÕ´oGx þÊñ“º`{fz¤§<é n†ûþù βÞÈ>0–óßEÈ£…·;üÌŽ/Ä%$í¸m4¹~Iº—a.N6¤ð&)Qó Þ¥+µI¢f²J$ž„õ ‚¢"ˆ ‚ ‚=Óµ-WÉuéø¥.ãI¬LVëÚáÖ:ÞùÒãÐêK&¢ÕHžŽ¯Áj‚ï Å%‡©ÀÐï)·vÈÃcq¸Ð®œ£tSvC»†ž7bCŽ4ÝH–”8œ¯›(L#}V‹/$³gl%Žþ§s¦æÂ%Ä %8dݬpžò\$"»§p—#ÙN ig —„Îa—çC2š,¶)?é¡[ó˜Î&YÚŸ³ 4ᦌ ¸•„Œ@/7{»ä“d^$ù Ô\„%žŸö•xE(öü݈ìûvñ¢DHö¡´éqË„;K3vM,ºnÅÄ· †‰Ðm* IIDôç¡bˆD]M&®+zR_´Þ©ì*Á"•Æ?°šBaªgF:zÕcÐÅ"ˆ«! F„Ò$Cc²Š!rä²Y,M%Ñ2ù1(•G‘,܉^‰ËÒò\©YÏqÌÃ÷Ë9| J·$y!abò6ö$'.[C±–"Ss©àS ‰™¯îwÅ„¼u•’¢Áš£iorhR–Ø´%‰˜ÒZSEšœ§Ý–ÅdyŸ¼Ç »:òn%¡.nLÒl“ò‘ ­9¡Ójå\ FíNl‘p0á–,ú— cßE^ÐíÅ]®'$IJnF…“£Ç$àš1a «ËÝ¥*†áÁµ¹.t$xQàx J'¥5šçBª±oÀt‚iŠûlöêgKZ"Åã«‘5hw%©¥˜iNdhYz„ç£&kbÄ"h…yÓ:[ŽíÈØ¹rY,º/ËÄ$«–¬)HˆE’Ì¢W$¢Krgab•ŠÈ½ÿàÚmö!²¥´á‘5=%^¼’å¬ìy6­gDõx|¦8«Ëd[·¶?‘â‡,‰~PVñ%& À„ÉÝR¼RV‹‰,ÛSå+±¹¶×bÔÀ†ã€RÍÐß9yþ î!øÌ§rfn/’dîâ%ð`2R’_ùÄ˸VØ ÿ6£tÆLÝ<áY¬Žõ†.qiÆÃUû)¤6vßQ%ÙÇrý'„–'K²Ii2æ)$Ói$,À!–ÈW—®-KŸ>ŒéÇ£š¦€œ'/ËÒÄ+*[pŽü]ËÔmÙnÆ9ÖjÌ´±H–öC‚t À˜‰%’É$’I,X±m#£XKçFd‰ ‘Fh…Ñ"DÉ“!/4vtÄX lóK‚Um^bÉ´¹„¬$/4š×%‹R#M”ªœ‰!ÈeË—dÈ™2zE‚¢¸M'Y.‹K.ŽBÀð-æîçFB00«4ÓYCÀ8œ» ËêüxT/¶Œ (´-“¼J˜™Ûë£'L…ЧÉAÅ0¶ *a§¼£@¸ÚµE¢"ä,x¿ŒzGŠ$JÍ­š¢(†¨¢ˆ‚i¹"²ÍðǦu^»ÒAÈeË—/IOE‹,X¶˜ DŽŽ•X†\¹rY"d„©ˆŒH J&› îuÒYê7¦ÇG*`a£3ã8„½S-ȃý–*d-çAß#WÜ4l¦®öÚÜÅ n™ òŸFÊ5bÞ“!ÅC΄54CTŠf“LIRþÅYè=/«2Õ‚$…ŠÜ¹rä²{Hšz¡ˆ!§2Dˆd2å˱(ªJ™éøŽØ©±+ ðÙ•2]FÇI€õ,D¿®œ©«bæáÞÆ³Á–”Èû˜¶œxèÆœ );÷¬¹{J¹¡U™F5D5‰I1HdÖjé(b“)ˆ_º1ê¬vH­F‘:nì羈 ‚eËÀ¥Ö†¯Ì–‚.\–K$H‘'a"™W¢Q(QâÍk*Àú®bèíÌ´§ÔÑÞÈ—-é¸Ó1­DðrY11“ÜCifN1L (†©^‘\L ÍŠ`Ú’:!ªg=º¥XîØ”`›‡’6#¢âOMº†M™t ‚.‚uÁ"išò$H†C¤1"ˆL'«égr£Àœ%µEμŎŽ&YÓ`Óº1éßÉ^5–ºh%ÖH0X8-Qª!¢D3ƒc%µ¸úÒ†é@\‚2’ßaK”…œ«Xت’ÅØƒo¾¦1i‡î;²Òò!Š©HânØÒÕ.¤º/V])éÆ˜DÒ%MRÉd‰Ft-¤'BØŒ)¾”ŽI.yÑ’¦zXæÆÚ]8õÔí\Ñ8èªZæ˜&¬ll:Í æ‹¿ÿÚ =ïŸÏ£}5Þa••(×IGØA5ŸUÒiALÐCóï¬w®íÿû ®8â²8¢²›ë¶BM¶ûê¯?õç?ÝDóI"ú¤²š/—U&øc4«m¸1¿‚ø¦ óWQ¶ÑmÕ˜aÅYáÏ<ôßqÖue•½ÿ~ö»©Ù†B8mš¸©Žjîž{c†lôòóóœc&jžH-†{JS ýxÞñM.S ¾SÏœÓ ±…m×Ü}÷0ÓÊ4¸åžsF)sÉM¥ÏŸ“ËN11Ñϼï:_" ê‚ÈÍÁ¼sïÎ8ÂY×^]­Ž¼0¬†û9Y‰ ©£N{»9tͪLJg´àGì'mºÁÞðçý:òë¯p¾@*Q°èY©ÝòŽ1 ŽZç ï<âQês6Ú:Â4œ¥×}7‹=ýÃO½Lo>+£«ÇŸ(ˆ ÉÔ>Ñ\¼9‚GN£™#«¬5ìènÿ ¹®û+œÞ4Î0%»*EÒƒ¾û¥>¸áˆL>K[Çß:-ª“¸ÓLföë>pcó‚eàëã²ùÏ,p¢¢ Ò³Èí†)”AÌ ŸñôÒ±ìÁÕt÷vÍëAñ%îÈî$ó. Ðú€Gpc ûènWî\¯ýãÇi¯Ÿ[§5‰¢¦Šk£|ã² R<ƒÎ¦Ë^M&p¨Øð¶A?kgd,¼ðß=¾û®óË %Yqö L)šÃƒbƒCŽSÏmý]8âd1é-ž y÷ƒ=uÁÎÞ¸¤ ¶oª†Ÿ2neϨ°íVëþè~ø¥²ÛGÁ¬¾j2Û¼8à o†Îð©â4ÃL2¬0ôòËwïä0ï}$tó,A`Ì-üüã£íÿVø¢–÷Ì<%Ÿ%Ä €‚2‰ßqŸ Â-ñbÐ àºX*£Í7Ž>·uHÚ$Ê|n €ÓßúrA4èjï¿Oöüóq`§7ÓÎ@(«õhÛâ²ßŒ!%(ƒ?[=Ã>ø-üil–בnôh@Ùg¦ŠðöØ(ª:óàwÏ01Šmñï4ÒûAO;<¢,‚Ë Q«êŸŒœáÃÛüpFñ}<äæžùïüö†.ã E(1óÅ'p÷Å®>$x‘ê®»©÷¾öî«&‹GÖTKg–<óu¤ÔßÞòMlRXqˆ ]Ô§Ö”BÿÈÄ(“-QÙÁ„žˆè’[Û8ãÏ$–ˆ^óð—ÓHxŸzÛ>zäö šûkŠ 2ïY#²I€F+ë†{¿y—w}S_Ï™<1 ‚Î?AGÞ¾ÄÅïðÃå‚ÿηÃ+ÚbXﲨ/¹0¥¸Ýœ°_ /U#{Û ;å‹ÏŽ Š 2M‡i¾ò€¯]ç¾kêM7_ÿöwßüŠóL¾*%”*«ž}úzì˜Gï •[ꃽ{R©,£–*¢xàÔ2àíð ×Öã4M©ˆÂ Ho¿÷ÑF}á½!Û´ýY׬ݩܫî *çƒ`rbÏþ˜3Ècÿú<¢¨… | wf­&@%‚¨¢ª(.ó„¥ uò«ðÑk €Š–Xæ¾ÊÆ1À:å7ò;ÃÄ8ûe~ëïçg-¯<›CÍ'ìs“Ë-ý£3 ©ß3ΪE °1ã!ž˜$ºÚá¢Ä‹"M€5XTТ?ãˆsŒþ4ð ×ÿ±ÚÇt8†…Uö%È! ¬Áš()¢ú_iª (¢6Óaÿ¼0Í>ºË Tu%_l2‰<8Cag<ÿ,6·ÿ¿ÚýÛàˆæ²¡…>‹{ž"ÁÇÞOÄiN0S[2Ó÷3>À<`† ¬ŠêïÈA6F­<ÏÏË,¸Ä 6ùãóq @FÛx ?ÿ=óÏ:’çÊ“Ubº²Î43¼öÀÇ$÷[AN挑iC úË]Xþu€’‰ šiàÃÒ‹ÉjûͳKÜc¬3Àº§¬ð”,pSA Å<#eö<2Ϭ0×üò¢—žÈ_¶y'œ°Œo0p_Q„Tǂﴩíë8Q‘°Af”Ž ’¸©¾: ¤¨Ã8LýïûþðàÎ!T²aµ›L2HˆÎào ÇpŠ×›Ùî·‚ ®9ì„SûÂH… E\wÌ>Úç,ýÝæoñõÒê8šø­‚˜,4¿}ÁQ…`–¿Ÿ4a:¸‚%A¢ÐAöÜv ýîÊO¸ãPS²øÚë0“î²Ëè)Ž1KA0Ɇqß~=Ãü{±—o ¢Š"ºëïKcRäÓÏ=¿¼ûÎm#Âð ‚z€Òyô…Üb9EÉOtßùܶ» â ¯Í,aŒ1¶"n „É×ÁO»î¼½tæYx®H"‚ø ¾“¯5 @q‡LŸ~ÃÃë ¦{<êÁC ,öß—îvZÈ^K™ôÇpã“=ÞpÀ|,!‘Šû¯ña ²ÂÐ×½“k~×xÃ2ÑQÞzó ªë†Í,©rèßë»,‚]ï&{ï¾÷ß>»ï®C”öíî{ÿ¸q¤ÕKØ,ƒ:*¾züé§‹i fÚ#<òÀ0³d¦*ªK¬B æÂÿûßi,˜óÊ €"×}ûè†ÿHû,€vt2K™†÷ý°òœ}Í·Â ãºé%±5Žt×ÿ™dÿ¼2@ÓÊ,óÇ,0ÄÊ,†(ÏüqÃÛuKZf,!φׯ<ø¡†{ßy÷_ޱ.~°eyýdüÿHÃeoÿçY*óA¾ È Ô)r†An#Ço|Ê@y@P (ˆ‚ ñ¿±×<ð@ k¢“ño¾ûë¢ý$Hkì 7_óD Õï‹©´<÷û žØÃ:›õª‘pÐÛ%Yoì3Ψ¤]W.û4m !ž +‹) @O)¢Ê­¸Ç¾úç®;öuöÐA4’Ò)ÿ<*xï¿O H´ã/¢Ù$>APx‚£ <ðeöXñ”Ó{ kóÇREàçHâ´Â0Gc0à ?ŠÏý´ßqÕm$ †óŽ0¾´eî {ö[äi_Çÿó°e²éL²Èe=ùŽÛêÃ~Í Ïþóþ¹ÿ_rðá4ŸN  €0Ï8… Oñ×þ0ãÏ~@ÁHˆSˆ¶y®°ÓŒ4'·ÅÇâ‹XsS¬ón?Ï4¶áʆ SÜ|Èi.à Â<â'ÛÓWÐAãKÆ ä7â mžè#‘–×Þ3}–]ÁG®{É‹î a åû=Ð8:³ùꟼòçŒ8øÂŒ!Çþ8åEiÙ¬,§® f‚KÀØ(WŸ‚8  {l1â Û:,VLð‡=n® Cã윣C/–”÷ÞšožÀõfŽûÇLwúÒ|sLÑ£Ù] Ó¬ ‚,¢9ëœ!ïC¶‚ÖAê2%p@ qöJQ= `®!€C! A0PkûØ!`Šß\µyÛž¸ûÏrtÏ}c÷?³LB 1È€%®oŽ{.×Î(ÒÆ0pËò† jò®"”Å´‡Ú¼pî‹ ±€ÎU'0÷·Áó‡2Ï_vmß/sÛŒ$YRÇ=?×Lë.àŽ9(A :(!3Óç,4Ç2Jz ˆCËÓ‘A$òÙ ·‰ãÔQ\<%1@5é=9ËܳÓË ½2ÓyÓ·žûÛ¤„]øÉÑM$aÔ’A̸Ã<óM¿8ê<¢[*qGß~ØM·Û]µŒÃ 4úˆ€0Á 3­ó‚”rÏÁ ±Ï<;{ö5§ûõ,0?ûš¨¡ÍþNf”Qõ“UÄGæÏ4ÑIóÄË 4ÃïÿM,0ãí8ÃÏ/ÿÿÿÿD×o<óϼÿÿú÷÷ÿÿq°.´ñD®ê ’žÒû ÁyîÈ&£ìUoÐQÄÄ,4Çï_ìòÑ…Ï<°× øC¿¾=,iÍ·ËÐsÏ?óßýÿÞµÿ=ÿÖØU÷×MFSQ§ÿuõZ#DuäÆŽ8)²Œsk9Ò™WÛS„\E³Ã QácÏÿ,s×<òÇ/Úñ”ßßÎnó}÷A 4Ã<²×̱ËÿÜ}ÙYçß_¥´Q†q Ò]ãçU óÀ±­vÚû´;ᦒU¶ËÕCæA‡Yïÿ¼çoóúói _Q>ñ×±Ë óïL2ó}ö×uõÛuµuÿåRŠ眞é8†érÓ;Ûß=Ûg•kOeÿ»5R‹;,8A4ãþóo}ý3Ï5ßU|×G­ÏÏ÷ÿÿë<°Á&ûý÷ÓwŸßUM“1S'×L©×°ïxȃÌêļfȧÿÌM4idBæE^2ÏO_N0Ìu³ÊÌôe78„þ6sß¼[ÿ}ÿïýÿ.vÇï7ã|ðÃÏ?óúÏvÜ_¬¿÷®5ïß½áO2y½Š×ÿÑ´ ÙqÆÒEt`Gÿz0ÃNsó4ãÍ8Ó ;ž7<¼ÃÎpÓ!,zï5üâÓ<úónóÏ/5×>Y‡Ímo¤¶¹ªÜçˆ ¦ŠlèñÍ )šÁÍGôQ  ³ÿ¬=óÿõË qóîtÇ<û<8ƒ;ÎN2Â<ó‚_|®~÷ãþÿÍòÊ;³ë,0óÞóËM;ï?óÛŽ0žêñÊAÝsöß]eaçþ>Û¾F!wjßÙ¿ëíºÿ_|ó÷žó¿óßüóÿûÏ>±Ë¼¶ç<3ÛÏ<ëþ<ï0ÃVÓAqÐÅ÷ûÌ1Ï—!·ëïÇ-3#<ý]æŸýsR yÅAAºÊè_~~Ëo°Þ*ßî5Yîþù„»Ã4ûÇ\:ßo:ûÿÇlñE$qÄÐ]÷Óño<{ß×ßMdPs‰v¬%¦É§‰Ÿ{ñw_=öÐD¬ÃÞê¾9ÏŒ´Ó^üÓÍ´ïîñãl8çC\¸Ã/zÅFòÄœ÷ïõǧÚÍט GY}0}ðÓ/°U/ ¢FX ¡¾Û ¹U’yÄd âqžøx"Êh(îÿ³ÂÎ>ÿ?pÓï|ó}¸÷®ýól{ãž8Ǥ9ëܳÃ|V.Ð}ô3À«VùÑ£ÞSÑ×ýÂI'ó®7Ë9ï0טAwÜM&õ›(Õ“Jîà¢ê½Þ?±ã­ò×·kÏ9Ïòw÷úÏÝyÇùó>°×í3ÿ÷ï~¿×µc­{ãí2÷µÉÔ”E!£/fšéª³BÔY÷Ui˜0ÚG¥ø”¾= ¡¾ã‹b£Ü²Ã2ç/yÃÍòÓ^ûÇß4û£=Ç/õ{z.÷LþïÞ³Ówç²¢Ûe²zé‡n/E&AWqÄ8k»¼ãïšNûêÉN‡¼µÛ>¼ÑO¼ÿßCݸÃ,pïìÓ4à »ßï_ÛoýóŸðýæwá$YAT_Ïß}ôßÃ(cº¼k;e8g]i%Z5wÂ[¬’Ó¢¸ÿïÜ}ûÍñÏðÃî4ÃÏþᄌçìóë<ºÇº$~Ö+X[Ž´ë¬{ÃÄwìsÉãEuI÷zíH-ºlë¾<¶€Du¶ŸQ=p3ÿ·ë<òºmeÚ8ÛßòÿÏ>ãO5ÿ1Ã>ó0š úç=°û:Ó]ùãŽrÿ¬vósÓ<0ÕMR}üõÎS·|¢htÞ¿ÖØ.•Å]}¼øã¯ßó®Þ0ÉÖ÷Ï®õN,uÏ´ÿï²Ë0Ç-ùã}Ël Ϭöÿ¼0Û°ß>þã;×¼=Ãn9ûóÃ,ûMõ›c+íªê£›éðóÏþýÛü~’ë-γˆP½×þü7y?Ç®°Ql4{f1ë=Sûn°ã³ÃŒtë½°á:ïÏ4ñÄ_ûÞ5û®òÅ^ôÃnÐÕ?óÿ«ã¦È ç³-’Y©ïï8ÖÚ<×ýZÇßøÂK>âÍ7ëã~²Ã.¸ã\pÓOpÇþÿíåã=µû]ÙÃd]k¼<ÅÌ}]ºYm:é×^ÃMSg¶4Šz`èÿ¾¹j«|h²Ìü¾ûh¦´þx×ößùì1Ã,àëæ–仜åƒËÃø‹Ìœð7\؇ßâz?þü7èÿþ0þŠ0|/}ÿø}ü7ߌ7Cþ0ã8ߎ?ßÿã~7at8Á÷ÿýÿà~0ÁçŸýøß?ßï¡ï‚ÏÿÄ) 01@P!QA`aqp‘¡ñ±áÿÚ?ä_éíçϼ]+¨¥EEEDA¹B”¥)p¥ÇÈŸYJ½•{*** ‚É`Œ½e–QEQYYY_¶Wï¢\()HÌ (¥++(¬¬¥++ãRãq¸Ò— R—•2®2a8sIátnJR—¤x-¤º“èPš/–ê>Âè^ àyÅd„Ô}ÉJ^‚h­5¶¼á/ ^õ·Fšô¸Òötx-¾Öx-¹k¡KÔÞsè—«½Ø~÷«¬ÝH%‚ ƒòÆšÕË ƒ¼„… ñRq. Ó]ZÒe*%!³Î›X11ê—kHJ#áDÒºœ¹¬¶Òè!-W«Jý;qx ÜFîë5BüéNÔ[hL.KÑÞ)àm–»F5V°™ö®¼Áºð™ ÓÑLIÓ’ÂkíèÛ‚FÂ!£ $ Ê"C{‡ÇBsÖȼ BÙѪ„¡3Üm,ö!eÛO)s¶àúÔ#fÅ*cD$Ú¼E‡{ ‡bþzuÁÛÒ…²1Aˆ’¤5bU…(ƪÂÔx>=:àìÁô„­‚?Àmèuà gËÄŸGÈ} H[Å–±ÛƒãR觃Qµ •k¸±àíé ó¬°DÓBFØÌ#`ý4©yÈ\-¸>•6„˜B‰ƒA¶Äº”-yŽÌOYE”WSr!h̓éà–¬éµ. &ÞLåN¡p¶`þ¥3®Φuw2Ô™ö}mp¶}:ê­+¡³ ÒBs“Bi=°Y\‘K¯.ª`¸[ÝE)q¥.J\/J´fŽç)‰Ët…6I~Dx‡eXÕ¢z©… ì>DšžÏ1þ’Gò|Ï¡GÜùí WàÕ!§ä{ î“rÜ&%°p©_ ý„Åj<¨5 }‰Ì‚ Rà†&'Î[ð·¹M¦6øÂL‹ßùF¾ Üú"<µà-æÉ¥“òëÀÚÛÿÀ¡'‰û7D`£cVþʧ|½Á„Kùx•׆ÂCcÈ¡«¸‹®¡îxm—éY¹D‡ñ^ˆ)?HHID"ä­âGZ[n"Ûç¹7y%켋Ìv Zˆ5WÉàŽó–úw>ç®D!L„! ¶„'9oÂÞáÚi¡ƒ<¢†ÊP±fMÞû“û ;°J‰B¬6¿ °a{”§ô Ü¡Ÿ‹ê/k?q óá슸“_w {§sBc!24HòįþùFwçĦfÐ~lýÃ÷`W‡9û#Ooì_úß<ÞLÒÝÂÜÕš0DÓœÜ-Î|ׄ' p!2Bq[ ì§)½’„!BJƒBd„'6–˜np¦”Æqá4! „'q›Ú·­œ{…)K…Ô-¹K”Mžõé ÙFòêw±9»Á¤Ù!”4ÖYÔ.U G«9i²²„™û ¶ûãi ¶ß—RöÛ ¶ó]i’r.[ÐLûºù<Ï4æ•è’é®ï“êaN%Ñ¥ãÍTþ‘L“ZðVÿe„á¬×¯~”žœín²Æ—2^&yЧȥ!Qñ–œúÅèjž•Íäó£J_¬NšðeÕ§×^%è)K¢¸W¨º÷¥Zw§Úîojž¬û×kê3Uo’gbÅ믣¬ï™áÿÄ* 01@PQa!Aq‘`ñ¡ðÁ±áÿÚ?ï¡?Ÿ®ë©KÏœ8L÷ŸKš”¥)JR—Zð#ðÈü௠ôc…e”_“óË2I$DDDD4†£ë!^Aê=G¬ôeö_’üŸ‘ùäŒÄ ô¢<ã˜Î± ÌÏpÁ$’,+ÔzAè#ÁD!Bd„Ï3Âa1„&„!L°„! ÁߣtZ¢¢”¥)uá0„&3–uô¦7 “JVVR”¬¹)sÜð%†þÌñ…†ü“BåØðzW¢bÃ~K’ã —`ô§ v“Aq+Q;¢iŸðˆLWvjRéžYÂZ÷¬¹–øF<'e]u×D7pŒ|™‚ïV¸V>š¥»á{py!;­Ý©4°˜Í9¢°Yf?B+bwмž4×éŒ#CPËgq·tØMj6’‰.V“¶i‹ø=ú­ØM}ø5ÂZ‹#i bß,øJ-+ü£å2o|»­»ª­;8UëVªŒÚ¢Ôšé.G¿+9PBU¤$’™.Iš Þê%àç=«ïIåZŽóç+G»$²Ò‰¦åÑHÃÔÏy´¼Qþz'j wöe~X‘ö'á÷‡Ò‡ÀÚ²”›³Šü¡öt¯R—6òpæ¶Ü'„«÷ЛL»Ðï“ØJM‚*mblãõ7¿ÇNø;º-˜O ÿ®´’PxA7 Ólx8íF÷øéßGmŒÞ ¢ˆ°SØ1…Ìጚƃ'ÁòØÜÇ–qf£ìÄ©àÆ©•ä^LI$X$'±‰$¢!~Kò&N©~Kò|Áë òcÉ{=Ùg9¨Ç©žè7l݉wº4Ÿ$œ{<)K¦ßE=G2>Ôi4:™&(Ñ#wÍŠ]>ز£‰¤Óm%XÆÏµ‘åœ7¨ùBæ#ïC­£@Æ-òöÔ„æ>ÚX}¨ùCVâwÞV‹ì~Ë>ïÓ§˜½»‘¤ÆßCM µ³=ç¼§ö$Ø¿À¶Âô·¶iÛMŸXª6Û"—©ƒÜ-ýmМi®Ç¶i©¿Ft²ÙÁ†ü{Ã¥à^ æìáoÁkÞŽóžŽÍz'’XNëï#fœåT¿Á™³ld¥äRôŒ³3g”ÝÐ^ÉèìàRŒk¥JR—©½Ãf¤&]¾]EÆä¥Âãs^©á°¥àlëÝ6ŒÇ†_ª6ð~àQìY4’X–?¹î=‡¿ ·„”‘¢¿² ¼¢¯(«ÉJTR— ‘ŒÛ’iL&mp¼Fˆ“-6ÚMg¹±¡pyƒ»ò|nå~o"eö/IÙˆþ#âˆô Æ›m!ZÁxú HüÿÞ­à3oK¦Hô!B˜B„!B„!5‰%Ç™žt&…ËŸ£Eµž óþE»Â+ÿ¢'Ã_ŮٟÒÔäBˆ»<ìÏWîðcñ ?ÜýOcôÿ°ìQÿB?§ÈáûÐßûÉúgýF1S·¸UÎy×$ÐÙâVVR”¬‘åhÄ `Åìàý/мËô{èÿ²ôÃùcþk=í‹ÄÿlüŸ±yÇý ½åÿÄ™>?ÑèF?Ꮄ½¶Äš|+…ÐfÞÇEq¥)p¥ÉK…)J^%ËrÝeÓÙç^†”¼;ääRð®¥ÉKÍÛÀ¸læ˜BaB„&„Âa0œ+£y«3à‘ Ž¢å¥)JRãJR”¥)JR”¥)JR— ¥xíŸà´¥)J][Ã,6xéjßàù†ßð–¬Ö¼”à'c$]­éwiÌ—m‹Í‰Æð©r^=äÝ*]uÊæIiÝ ª±¿À“:IhÂs)p¹iJ^±êB-ËJRèÍ9žréJ\n½)J\ZeáNeÆ”¥Âå¼XB”ÔxOã„! ¤ÕX\!2Ý[–õt¥ËKžáKÏK¤ÔzšP„&´.EÞ¤"ùÊÑ87‡25˜'ŒÌ³NU ÈúÌó^Õ¡)¢ú‡Ô-ó7Òµ¹hÓEÂèAèÒ—©5&0™`”É3¬·¬¥Ó„Áª¸3­YX¸”× ,í¦gt!M•òÙE‘±7šôRsf”&Á® ÍyÔ¥.•Âë= Á™f™¦YÉX\i“LfyÐ>uÆuék^GÙõ¦³¿âO—ÿÄ)!1 AQaq‘0¡±ÁÑðáñ@ÿÚ?’txÃbË5»8ÇsË,ÆÇad4Kqmš0ñ¢ î¡ Áš“€f/F1I°bÆì{%3ô—Ñeš›ÒXŠ’¤£Ü€Ck¹ohÎ(ñ݋Ɍ—äx ‚ÃRÅ Á#ÄžÄ%vôe;B#ŒÂœ èÃíÙ“5ÂïGxR×ÇmìoÍöo‰9˜ÀLc¥Ž$Á?d°eЌlj-Rãè'gÓcuƒ‰@° Ÿ¨.Ľ $t)ÁšÂhon²ë­‡Rf‰ßð¡¢ÚLÛ"ú'0'ÀŽÀHŒ8%,f²†7s!Gø‘ØÒBc(èô XOePADlDg‚Úßaw6ë?—eàeôš$ìD¨.hËâ],¬.K4Mv’|ÄdŠ…„€‚m¯IC¤YÝFFÝ¢ƒÓv=³!f7¨Gjt»¡¡`j$Ña4 åˆî¶®¢(l„êéTQ„¸©vi#¢š,¨—Šn ‹‚(KK=CÁ—x™¨BñIN;,‚EËÑ$mãrY•C[d! ÚþSÛ=I}ô_n$¼}²vBŽ0$AŒž%è„)ÓÆÄwT‡±ƒ`è—X2ºC÷Æ c,½Üe!ÃKE±•KYºN[nãÄxÌ€ÙôÈÆÃÒñáÝãK¯$'@4S„u‘؆è>–£(Ëh’(¯Œè¬œŸ²Ñ4µ4Îb’&ãFh“¦Ž ¤æÎˆY Á‰bÆì·©êÐÑÂ:$ ØÀ4ƒ@`IR2"[»Ô;£h=þ' q†„"#(Í©¦ÏæÜ‚GdÑÞÈbZ%Ñ'»°¯VIz`2Â0-ÒÆP1P=à!¸ÝØNf»6̹ù‘B2…#AVQ ódE •PfŠF®o­FtîOmc’SŸfBõÖA}c & Œ”Dì±(΄š3¥`MúFÀH+?µ„W©#í=MÍÉ_¿g’¡Hº,b@;&—R+¨(Œ‚'©ôIùö M-ïa#G‡ËEäo¬‘A‰6îÝŒÙ%Á E´ÄaN# b”w†Uá%à !ŒTlÆôeîzÆñŽž} 3–‰œzAÚ0¦lb3ž’ÊnJ ž ìô‰kÞÃ+’àaw:F #Ä}'°KtÙ,,&0fÃdFerÇ–B2Ý%‰ ’f /‹"1ŽŒÐôaÉ /IÜ„Á‹ëe‡ Æhd«Ì´õŒDQ2z™¸22Τ¤s–;§hËy¸HFRà›.ô0YÀŠ» Âîh"é *\ŒxšéèÃ~ÒôÆí%r]=œAµCn;Æñ½ÿ%â„b]ŠYÜj^¶ã/ád‡˜YtŽ‘»„N4ÜaÛ¦ a–y¤;Ó$8pˆfä鑇`ISLšÂ‰ ˆË d,‡¦Y p%û%!²Ý,ˆÊôÚå±ä8£‘»Œ«&ŽzŠm½µÆA$H‘è‘‚0‚Gû³õã-Ô?ˆ–äßÁ}ÿÙÝýDô¡~ÿS}òå”ÔÚzËÿiô7ò¬·óŸ¦í=F¼ÔøîÅ à¯ß¾‰Ýóð[}/ÜûqunßÓuöûæ*ÔôðÓSj÷_lvÇw4–]™ÄÇ+ö ú’fKG †X1®Ý6–ál<¤±˜ŒéL-7u%œru¤w¸¤‚.’Œ›¡{a#ЬKUŠ@S< ©R'K@°™M(݇]Ô'‘D0“3„€ \D±»mML¾cË£´¾œH¦’Ä"_Ù¢'½$}¿N>Œ7x'á"küX’Ø|ZA🡟Íþ”ý_Á«óÿ“?YþQƒŸ§û™ú×÷V†î¤!ýµú?ÌÏÓJ:GbïøJð?ˆ®uoÿª7èR¡¥ü2¥S_Ê[Öþi÷2?”£ª?µ9ûúYü¨šžqÛàÅÀýÉ;z[³ßÂ}£òv}Ülj»½^w«ú,ýÔÑfnR4\GŽ/V:p³Æ€XØÇ€› ˜@¦61¶(1î6¾J‘¥ÞjK9…€Lj[>×$© ýÉ£""flFˆ•43º 6@øÍH@€vȾ٠7X°4Ò(¢êì)´iШq—IÒ­õÒ ÏŽŒÀh>Å„_C ’ÓN #ƒ¶Ë"B#øKIe¤ˆK`ô…¦†øÈß…ÑÄ ¸7ÂÔ,Ëf‰¥ØB0àeË%‡$0 Y‰2.‰ ÀêpB#K `ºi=Ìz!ü¤Ÿ¿£Dý„þÉ"âÉ“ý¬Š=_̈4’Œ¡öþ•ôõ¹#ÙEÿ‚>íõÃôÿrO‘ý±eÇýÒø?»õ÷Uóû#àšõýí¿uªçøbŸOðJwÑZSvß/÷™WU”¦­Ê¦´¿奻Wö¦Õö/š3ƒ’øƒø[2„Òêí8›îü³òM¦ê_ºÂÓ¶ý–èûÄ$”º¸{¬þ $è˜`q&Ã`°ú$ $,bûyÂdÃ1°Z~à @²{è×ç›K× Ì~ô»såÂ8±é¬¥ŸƒøËî£~kó°:v9ŠÙÑOu0QõA¼âßÃè Žmõ´/‚E ]'§ûI?}Ð À#u…¬`‚h¥·uÖI³¹’ KŽä`0BR‰ÐJŒ—GD†Ä{PiÞŠèÈ&à‘¸LK5•2Î !;g@zBqìȽ n¸ÅX#NÁ16OîÓw K¨fƺŒ‡H$Mì?”é~Ña®?¼ß“ùé,²“qþŠƒô³öÈ¿ê'à³ï£úsÿìgòJý?ѧü$ýíϹþrúOü‰ü~¡ý×éþÇ'È~˜ú?áe|¥~ï辬¿ƒ-÷Ô/ÿYUµÿô•uÿ>[¥MÜ\û”ý©^ÕO5YGU[vÇà–i"èysïû‡û»²~Ègª]¿µˆ‰˜X HŒ§À ÀbžÀˆDXYšXXAc8Ù%èHø²Y…Žå’HÉ q‰8c}˜Lö}bX-½IÉÔ»}²fxíùãá¶vJq–³vxÈ|S¿´µíËüøá‰_®ŸT¹ÛЀt´lmÍ‹É½ÅÆ÷ãD{d—ªýÛ÷®£ªÂ†¬ßLJÂD4/Y:,:wxG’nÙ›>³aˆc_6lþ¶Hè°ÑÂê Ñe©¡Á ?‚6PŽ(¨ˆoùHþŠ$÷ xŒâ`6J²î# ž†Ë|ë6`ǬƒÓÚ˨„„ºBˆI„‹•× a¯tØ¡, œÜ°«¾‚I`i½Œn½^‚é°`hŠÛÜ2R÷Õí1‚t¶ m W4òÈ:À×ü² ?ÄqßþyôÏ–,ßí¥ÄX]HX™óS)|_ÕÚ[Õëí2±QmxÅÓÎ6þ’ùNû›ðnÑ‹s>ÉùÙ3ò±Éi{[_jÜÂæÌ ,˜XIÓdatƒb„c ÍÒnHXDèé#`Æ] "|D—ò$:)(0`‹³L%fsZHu:òÕú(ª”š+ö£'¼hÔ!ÓÜŠ`»n±Ÿ±Z÷fýí—³Ûöq’"Ř -`ÃLÁü®¿ÏõÓ?}&0¡-©õ²n©D2‹Ñ‚c¨ªé(Ä Á‹ŒâO´ÉdÔ"w:l5B¼Ç5 T:ý$± /zL–aîwNÉ#^Çղ»„iÖÍÐÛÃ`e†\‹˜¥@dÇ£e–AÉ«ÈY#‚±Œ¤e¦2-=[%ÜØX2lX“û ÛüXwÁÀ?kúmNdöÖJòÑÙ`ù#!€ËÙ Ê+°Æëo1Xû0ÛëfÍ‹0-xYÙ0Wä.Ÿ /¢³ø/¸ ;Ùlt© È ICR2mã$±±º]^vTpãFÛYee F!ÖVï5‡¼aºÂÇNÉ#sÀ¶€a%»,H•̵Ù¦Ç` 61“ Ì`Ôºñ³X•Ùµ¥c!áˆüZ¬Ã§¡8N6¬ pìžfohß¹Ó„(Aˆ‘§D&vAô”Âh‘p»2Nœc—Aa€8ŸMZ)š0ÕXwFÅ_d¤ Ó°¥m (*R€áŒŠb@V8$‘Á À ŠÓCv;l„kcd òÀYf:²ù $‘í´èbéņLþ%¯µ6€m+A„:\aQבyÐYøÛBÅŒ~NB“nõµo$vy'\à1¹ €XÛ²Ó¬À‘Èû YâÎİüXŒÈÄpÄ{q’é$l88]¼[mxq[K1ã Í8-Öݾ‘àmÛXz¶Áq,îzW ËYt¶×赜îxxÎÔ'¢ ã-Á™Í‚:x}ãÆlàBDã0ÜaÑäÖKQÅã Ÿ =!’Ã,‚À’†wÑ9úÔã¸ÒdÙ• €L7Rc °³¤!ŽI™a­’áî1Ü2  "‡ :À&™„f€é6q©®v$‰N·cQ˜\X·¢6!§cb?‹tŒ“Ø£رÔÃHA´h¾K@ènÖeŽæ™x@ƒ.À5 íRé2ðu!t³ùÍÚÈ0t~DºD5†5#d“á›vh/Ë;«°°B%™ Ù@Ï, ƒ²|’<$˨ð¶Ù¬áÆÊåÞY«g®¶¤3‹:JçvN1IJ‹‘§ wÃgèÛŒ––{0– ãxw‡z8 Ü0 ;ÂÌxÆGÂ˽¼xìxz&ë22ÙE°^bÎNs®>ÎÞpãVÛìž7 åX,$l–a°ìt©œdàì6;'Ö»lØâäl·“–Ý>œFî†ÚXß¼2Û !C¢Cbg7B]ÒØãz¹bŽ¡Ï¦ý1ÚwB- ŒO‘—šÑÈpV (´ “W,Cˆ1p^cw$†J"[o[ùC Æ :RD=ë4v¶îlHÐødñ0IUæ¼ wùóßÄÈI4Æ|ë$î…©© ²Z©±™c¡ ¬,-¦Êi¦N.,äb Y&Á1xÌô,VR8ûê[½´:³•\X·”r,•^=˜ fÜ',ƒ êÂ-MºK'W³߃dXíœ>YÖœ:Èé'Å·"H#$ƒŒXxèÕ-ç#¢Vj ÒFœhF§"KÎîƒdíº7€À;)‘÷nL€RÕF#€"έs,rlWƒ¸—†¥ÞÀVÁd0…•@l0É N•xõˆqa7pœT"É€šµ­#ÅIU"J¤rØh*+Œgà®–´ê{nàHæ õpĘSê )Eºˆ½‚XŽ4 +ì;]V  ‹¶Â·í1Þ Ñ §éžåv8k&piÓÆ:¢A¦­ÞÄf$DlA(Ýx–jnbp”ÆrÍ`cx>c¶cưa¬ðÛ%:èô'[@\·ê ôã­·õ[tuE³ƒ=]bqŸ xÖz ‹¡F,-³$âÉœc’­ÚÁÂÂÇb¬q†Cd ŒMÖKúá³™÷9ôð‘®;’;iãÃá ¼…®sùÂÂ&ÞFÞSzH蔇èáÈm„‹ŒÆKTºNÒðNLû‡¹‚éëÁ|&ßѲAdØàÀÝ–º6ëÁŒ‰¦—lî¿¢ÑL‘A,Äá©-B’Ç4‘&h@sÔ¿CÙ‡´aè3I0é1LGÐKça&œze4Ñ;û N‚H[‚IÑ!± °7 ôɘ®›XëÑ͇ŒãGXƒw†6äé ‘á;}K i‰›&1üÆjY"ðHô~ô±>Ë 31¼1°u,N,•[#,鴕щ!RîT:j1w`)i·ÜàݦGAá^Ùнä ×dÄ`€K$ã ŒÖÓmÂ}Rƒ|”—Å!ë‚Þ“ Ý—…˯qÆN a¶p³‘Ý㧇׀å-ÐŽ¶ú’è’a3‰8øÇŒKa³Y¶4´-2Ò Ï†ã;–8Á¡®v¤çÆZzI ¸JÇ¡knÁtE˜3àYÆ÷mš–k@ÙiÆ–ÃÑ𷦳èŒ/#öGŒœ—1ô5& ÖÑ‹)cÄ_…Ð>¸±®,.¶à“11ö ƒu 0 ¬ÇDµch‘¢ѳ“ˆÁމŠ:„èLzaXu¥Ø 5€vˆ0ƒÓC²ƒw¢x“] ]1€C°„~̶3}m6LDÝì?Èΰ<œ:÷-6Ïm;„cµ…ðŒÉþ# E ]Œ´ÖwFpÛ3å‚p¥©£-°,YÐúÙ(¬¹i—yxH ©ÉÙ˜òzÝ ë9œd,}Ø&XœbÃÉ1qïYÉ»ÉNzƒ [e’Q˜Ì’FŽ0œ,6k&âF‡ÔñٌͳÙv³é³èàÛ á goKPðá8É´n¯Ï ¥…7€8È-Ç‚ˆŒÎ5moFÄÆ72`´Äƒ=ôÜfÌ$èHXàÉaŒÒÛ@À†Ëg· ±c€ü"ßÐK¼Hudž›Fã*ïºJ SG@Ä B;ÙØÁ£ @"#jÖB)ÀÐBÕÖÊÈ*¦t·Ã‚ lOT\!£c£ÆÇ"Ú×鋯1™%Öð" ÒLõú[¼“ŠÈÆÒÔ<b3a× 4¼I^×myÐ/6'K¬f"Y)Î Œø< ›–Bä塲™¤NCYËÇF\x=5’ #bÔ.ˆºzµxé' Û@r ÓN3Rη•ÈÛ9ËmÖϦÈ@é$®Îúpï šO ŽñÖÂ¼æº õ ¶Â;:p³ÃÆ 0išpûð d°€÷îÆFÍ~DÃNï ð£añ«-¸Fñ`hG ÀKùRØÆë³ƒ:lß!‡u×m¼Dé3Aþ. ¸ud¢–: ½„Œ  $ Ð%κwØÚ‚øYPê@@Í .}¬—DHk©d°ºtÒJ‚) üý‰UA  ìfºÈQ7VuéÝŒÀ^úáöÈ”Õä'NÔß0Y‘†}6ÀmˆOfìq¶Œs[¸o¼É]A'[ápøi&2À€–Æø¼8r=dèÛgdXo.Ï:à Û9i õ'ä1Ʊ¸I,<l˜£Àr‡Yn3¶wg¤Æ=ìªìNI ®pìŒX®*gÀÄñÐ˧d³ªO/£de“öÉ_¢Ž™2ó‡6%•‚5VË,Àƒ±˜eÒísŽ®Ì((Ã`6£ÀEŸLð"Åu#ìw#Im˜¬bçáhOÑ(U`Q´weIZ@qzlÉ;6‰¦Ðh8Èjš62˜1õ‰§~ÄÄa׬{d†)DéTaÅèÍÑdè„rJ¦ÌúDe”¢® ¾“í(Öt¼nÀ‚Y÷$þKÈŒ~ãÒØB]8qs°µ –:# H¦l½-ª2á<)hIǬt…’é1`Z$f%ÖpÝéÇÒô0’vœ¶¹w·Y‡ ²‹ Âä";À§&Ú Žñ¬„–]›.8X¿³‚>ã€zrØžFpN¥6Y³áa\Û.²îxóBqxlDxþË%7€Û,øg[ °°+–cg'Ú`Gqˆ¯ÃY‹½ÈVÛL‚ÐŒ]‹Éprì:މŒ ¢« 9 `,´±£Ýÿ…`þ(ÁGu¿L*åÖF•ƒÓ m* ²˜=‰è‘KE]Œæ¡Z·ˆìí‘öZŠzHÀ2ÇI5Œé‘@9ÖwMÜHM+1!l†ª(:%Ѥ»o¡Dé¬l,ÇPg܈j}ŒÌZ‰¬·ÜFÙÑ–Þ/¼`gr18Ð1†{4“vÙœ‚Tu¾µŽ^0ÛèŽãEÒS8Á2ä*3úry8X'we¶2sPaàM‚ÔmËt´ÈÈŽ5ÕmãIg´c°!^7Wj¬,87n5§Ñ„þqeŽ ô1…T,G]9h†Ø˜Ì.’9£c`9#DÇau¦ŒzB}:¦0™GätlTcIhhØJ,çX–‰i#€ì>„〺k¢< ±¨ÆðÐÜÿˆÖ5-c7ÛHà— Rì1ºÂÛv:m™$•˜‚wa2RÛUÎ6rs#F3edÂÉ`ëQŸ1»TÀ‰FNËÒmmlµy$·¹XàN6Ø3wÆúŽÕ‚È8Å,“ rɒIJu’I á Ä–píø¬ö›0u£g6ð1)œ6uòfñºâNeÙf§/oæ–ZËi¶›¬öìÎçe ð)kÆÃ –°Ï?ÝÖKk}dfHƒ½8ë5˜Ýìƒ|m û„“Ÿ²‹ý9¡&Š´›jƒua½ºE+–h)fÀKwAdÑ¢"Xª^ˆ:’PÑé„}‰d8˜²6£ ‰Ð*TG,`Òúb!a\ââ’ˆ/`"Ê éA€:¶Ý{“¹œ:jëÿz æÒXF"Ncy Ù6Ú˦…V£¤¤²¼¥±ùXu¾ñ¾Ä¶ÛBîìéfÒÙbÜ18GÕáp‡á£ô],É€–ä7¤f$ðe—bYÆk)§_À q7FúÏíÄ¿Ñ/watÓŸ¬?µz£1ëúnÜ~´ü‚ßA†IgGœ‘û™»e…ÆÃθì'4lMŠáŒ8Ú6Hæ¼owci;zk+c&¶:AjâHGÁyË8RÙxLyîØn¸D 6†Û\Ž-‚Ñ‚2Ü·~¢T‹c…ê†v/Y>å…õ{)jÐt€qHcŠ @(¡‘ÒRàD¸H)ŠíÐ`2®:Ÿsà:X­ÚKˆªžÄšº&ØÐ_µÉ¹c<:^6¡Ç9qbœe‰FÀØÃ)¶?kü£•ïcñë¿)ˆO³`ß±E0¦LÓûT7NýZ>•÷—ÄÚö÷?ìíò}HÉg ‡*0FHŒ¸ãmºÙ'OÄÅç¦Þ2Î;Åã¹93mŽYÇ0•¶ÙðKmÎwàd6ÇWJí½°ØñöÚ§ ‰ÎæpD:ÎÆl/lƒöÒï²þGÝ2hÍÓ¹è3€L5±ŽèHQ"ö„¿O«@#H¾€éFR}7](8å˜iŒ¥ÔHt V šÑ'V °!ð"Ñ >ŒKëI#n£ÿΡl^‰öɺ([À!‡eÔ„-˜eúm8u4»Hµ¶XbXĈH œ–m%ŒgE·à$:¶b¤³lØ À–ðZM°Æ AcÀÆD€Ñà~V37ûèçlw{v ¯§Üš´~„?Á:w mlÍaìR8Þž+þ&Ç9ãÖÙfÁ¼ª'Pßq¾'Ðñp'É鑾ì—8èè5ž5LྸÂÈa¥¢p9 Øý~Ë:ëßüßß zãÕìͳ™ÏßG '+§Á‰Ã„Øå½È<lÌšp¼i.]Áu¼ l]ð*ñ¶Ã ‘lD¨ÄC« ×r™ (Û,È-XpÙ¥v!«k;Éϼ¯Ñ "4—@1 ‰3$F#FLDBÝZŒ†•atI ˆ²a±1ìE+¡‘HKhAtXL ¦½ÊvE4érAÞ?„IÅ‚Úh¾2ÀL‡K }ÿßAXaˆP· ›zN7s`•X ´¶ ¬SƒmÖF&W:†c°›am‡“ “ »Æ’tØÂÈYÒÛi 7Wd ¶æÃÝ/ä:@dŸ–™þ0×(t8Ðz½f{‘Ãñý)ÖKnðÝ2&Òm†Ùé†È ¯¿ùaÿ0~Äà—ì”×ñø}œã&R,äRÙdwŒ½[êëIÓrí9FÖ5a·VÓ ÖÓ tåxÙF'µ>ð6ÂØfwºxØ^62<˜5±†Ë àÈKÀ ²õ­é–ûoŸ\Cö9k!#ãÓ ¹" "(#Pg@„f˜ŽÊ'І™¡—[¤bX+‘¸x}ºH_,4ƒ 0DŒ÷ ¤Ä…t5!7ì¾ìÇI… móûPÂz=¦8ÃÖ±i¶·¡‡Á\D›8}…Ùœ‚mŒË#vI[4ácvXFÎY,pg•åË``†l»»ØL€ãAbú œ6DÆÃ?ûþ“oÖVΛnÙtåÍÉà„˜ä‡¦×8Ø€WIáµ~ýûƒÁ[ûûþ¤°¦ŸÒ}$ÝnI9!ÖJMŸ'~{hœ,Fœv³ÆÉ<+}ðœ}[m²¬_¥ã>‡Œ¾£‚í„Î Ía…³‡avUˆq‘°a1atHVMg«²þHº`›0Ïã˜ýh£°±ÍÕœìlfëQ´/UÅ!Áˆæ ¯[`ú‰/ÀMƒø,šè4 0 º5ˆ’Ínhàl°”\ô1Ù,8«,@ì…qs±¦ùÂ,nŒèmîÎ’9ú¿æ†;^vž-ªÊÄ<›Ëú†×c7?Óo:³CËÓÆÏƒÁÆáz]opG„µ-މMáõe¶ÙÖûŽÁ´—­ã]Ž €à'ú >Ö öñãÕc´ûUºüTbqÐñÞp=Ì™ÀNúpæó¤n 60 ˆ.ç#§œN>¯kù}Ó&D›­Ÿœ±%—,8ûãîC“†Þµ…X`^³’%ŒîFØìÚ²³ï(6šü2Òñ·Qàç¬Q‡88=„ÙÈ[\Œ‹ÀºÃ :!N‰›ª²OàcßûœYã ÝŽ0cPögU½[ˆ³Tr1ôE»k;%ÎÉBh–tšº&@¸HìNCIHà;¥¬>ðPé €ô£&„Á:Œ?$B,G,Æ3 ºC"˜Ùn:ÂE·v» 6ö¼²Ús¹ÎÛ¼;Ã:®ð±Dq´í’Kfõ±ÈÝ–è%g^uø“Ƭ™¶tpa/Õþ(²^>yêYé<(; ë:ZäÉf†¼9hË—[Ç| ±œQ§ßœÿh7Q^IË–9Á:qØò «)’Ï ÛÂOŒÉ>LéñOƒ}D;£0P¬¬Œm냂#s°ƒ3ÆØìö0!¼*œzበüù.b~"ëûeôÝÖƒ~’Ƀ0ú “UÜÆ$ˆ°€a\ÖäéÒЂ4°ˆ‰ÙèhÛJhli»¡a]ME=Xz0šYŒp=],<±½™ž V7L’7e2Á‡À’s-™T™oqÛ90åšð¯' ).ëoÀòûŒœŽu ¥gn‚4- Ç…yûm…Ç,`Œ»¿§_àÁ  á—üm²g¶ða½g¶ÇŒG»>〗Œlèø)”¬¬ÏÛô¼tÍžßàe´² DžMÞË<,Ý÷uœ1Á«œ²'Á$’wá°¡o§ð²è¯cð>’dbb«-–‘Á°o‘Æñ‰¶iÔCv±o]õ"P.„ú@ŸÀÊfª ÀšÄmßI£ Ë"$®H`'Mž¬¨žÃ¦ÔL$Þ’×°TÂ!ˆ¶ƒ°"¢÷¦Ä4Ò ‚ëÛ…ñ6]Ó¦˜\j8²4¦hDn¬:F8?·0§#ÑIÈnœl±£l–aÆÈM÷›gG¿ ;º{8l‡Y÷à-ǵÂõ8vq’ï6——¶Gy8Øjº'B?ŽP5ÿ¹•]mã†vF7€µW†8Ò&‰b6Stëìü>Ë>ÆŸ‘ãÏ‚¨pØ$c3%†nüȶg$i†3™<¥öpòçÀºJÒ¼ Æ[+î ­†KnÄÈ]{ƒdgZÁÖŒlIÝ0†þk®íà µ™n*—ŽbÆ"0‚èBŠ9#ŽB3;%Ù¨ ÑgˆÌ`à‹´%†=ŽÈ»…F™8é²€:ÐÀfgJÀÛ€½væõ‘ÏßEÂ]¥¦K{4xճƻ:[ÁÓÀÚp°ÎHðnE²’“^؃³»Æ|6D6:Ä•UlÌ—ÐøoðJ0»}Ìæîñ÷æMƒ˜Àå–6' XùdÞ·W‡À4‚Æ!€H¼×üœE…çNŽËbZÌñÙ3ñK±FÙÙfaÉå8S€6hp$ø?「 ñÞå–cÈÁ‹d1™Âlž4Ö"߈„ËèÈhýøÒÎPÈ™¹Lg¤!4ÑaSÐ{”q èZZÕ‡\KMGFWDÖ{ 8 `vuq„Ê#—‚BÚ¬Dœ´¸[ê’´:$ìÙPå‘ —ƒ÷¡³Ðƪ bÛ¥²Þ6pxÒÛzëq¶ -g2ÕÝ"Û%$f78×a,s…%økvÚ„n[+o¤<ŠJ |_áX3À”g\ÍSô!p dlxc+ÆpH¸[iC¾_âNô§ƒòp@„¬Ó%%†’çG¶!IH½é$à6àŸåyÞ¸>IÉ˸ÛKœ? ÖOˆõ>˜Ú¶á:±vDÌz,yÔ‚Þ¬äS²·Ž¡ŒÜåaÕ$‰·œBí­G?‰8>6Œáa a>Ò™%_¤$Œ—RZhÙ‹$gS¤òíSD±T3KT:—¦†0L8‹tî ‚`ÇLÒÐtU¿b:½‰8ÆØ…á°~ô‹jÂìd-6Òu¾í7mµ·‡-ã´Î‹a˜^|ãm-‹l³áõÀ<Y¿h'G"{‡# ¨7~¸§â±þ ðM¿?ù>@4x¼.õ>ÃÑýO!p°å=CÞú]G´Cáã]6¹·”’sve¢ûùd#;ÜöùÙ߬l ÂsøDãêL7ÿ-´’`/8%¥±“μ7ÔØÊ³©oXÉg yRdã>ÁHX–ªÊe‘©F‘±&#±h:1ŠHp gd­Óݬ&Ï, ,[´a¾BŽjÆjãaÛ¹¸x¶ét3›£#*,£¤##žJ¡¾€ ÁÚã(T°Bb@ ŠNø€$8“3tI¹éh‚ßlž™0NÃø‚TÉÆËzc¢4át°É.­lÒcŒåÁˆàÙÀƒ9‹¯æNü7RØ’ËC!ÈÛÁµ…ZJl<±Áòk î"?¿ºv-ÿk…ÞFGI¦Qr`¨þ˜ÿ†ÀA{ð~ç @ñŒÝ^о(ˆÇûýƒƒâå~€‘wï†t¿¿ðˆç€$2Õæ”üýãŸüü8Ï—±žpÿúÿyàað6Ðì#ºž7ÓßùÌì³™ÃÃ}s±9—§Ã xpà'Œ— Ó;dÙ<çÁã``‹ÀÝ6–™ô[uö–‹dŒdq„&ÆGžCÀ7Û¶3 ÀCÍÈqðµÔkIXˆÆ›¥°‹¨H ¢8'âwtBrÄœzH˜#L`ÇNŒ¤f€±,Ñ£t³nÒk-Q† hnìn–Ûl0ôåŸ/¢/A†Þ5gµ gÍ#¶û–@fIVÆð¬x°¶õ »¡¼i> 6œ! s¦p%¼`¦N1™ŽÙu¹¼ý[dœ Eö-¤/Ø[ÏÉã_Å×ù¦ a¼qþÏþa™ªH_OüÎïnfjûx4¬>·wI yþ¦ë˜÷ùCL Àààü½‰‡ ÇÚ¯Ú­Ž¯Nðs3ƒQì¤f³ù¿÷È‹>k^XñbßÊNäÌÎZ–Ik}G;}ðÝÏEˆ™>s‘6‡A~-úy~=ðfHB@³âc!qº,ŠIþxgøþ¬x{ýiÂðìÛÜkÃìfßo €NŠ3oÃ^·øàVvÙHÄŸ‚„kÆOðK‰ÇÓ/ì±$vQyØÝ¶ÝbÞëxÞHˆ›¥šüøà??ì¸bÿ{ÿ“þ?Ò2TÕV 0Ï€œ$Ls³x÷ž ¾ù¦‰7·þ¢?å/ÃÇò™?ø8ÿèýì’èb%mŸîðô3eº1>Á€¼å½³»¥ÑÂX„ù/óÁÝã,çë€ç¬ã"ÀˆxÈà^ ÈÉõÈŒ[`P àÉ€lŽ"SÐGd€T‡MîÒôÍHÁYGïDHCÛ1ÛIE>’ôÉчÑ`÷³O$”Fî¢Z„êpF ’¹ˆ„ô‘afZ"Û¹iÒRá¦É?„…‘»Å·¬-•-¿’Ûø8d#ŒŸ%aY{Î>£}ðr!Ái=s»‡WŒí®ÇÄç·ä´ŽFwv.­µøÿòøÝ‹ÿ«õà)þÿâ¶(ÁŽÕ•ÂûÞŸ·û‘ ÐL–uýÏ©óðó¹3ð<©…÷<´½›Âì<_²+[·_W?”\¸"G?øxáÅûÿÎQ(ëHçjK”Ç«þ¼Çôpªüñ„ÌÏo Ç Àe£u’ÛkgÀ•œáVxdLÓùx|LHe>¡›ɺe„bp¡…[qÖÍI-a.Šì2+/òÝÈj G¹ˆÀ™t5ÉÂô6C¨÷ ˜–ë¥Ø*F ã²kÍK÷’—xN'e®jzÁË]](l¨ý‰/žL2BºÈĺ,)º‰mö;Fmààe²rÔuÉtá[NmãHÉéºÛ±¶É`G‚sš£f6¼ ³jΡ< œ›–ñ]G BrgnÃ9Ÿhä2O\Agÿ§ÙÇ·b]’ýx,Uî<€ «ÐFhì–)}ÃõÝ£›ºpšp@¬îàjÕu¢ÍíðpQ?ÆXÿ0;oXb84#á1þ~4·¨lºEwÝŠááõÿ¥¼‘>Pɉe¼©$üvÝvXãm–ïfï-ç°îïo ¯ cȼgJXìñ–pðnÚFq‘­°¼ À-‹™¸3–L…Ös >ìgo˜“² º.# Ñ“[´E–X‡mg¤ˆj b($$} D`NXg¨qFïÄ#ín€Ê€é!ßKVÍÐ$~F7ðQeûÉØ'£j—Œ/ö¤h^¶ÁdǽðÂËy'ÇÔÛÃ}q°MÕ¸¿òÉ›eˆã,ùœå¼u‘ÙÃ`¼nà›¬0°pÿƒ†ô5#ü¹{Hzf üfô¯¿ý²!ïÞÅU¶dÿàK= óëøðO`Ÿê/»X‰2ùÿñ@ËÐ¥Áð|Y‰\OSãuÿWÆãý­„™Þ7'-¶F]Kt2îÂu·‡7àpæ]' ÓŸíỺºãíøÀ‹läX"ÅànؼÒˆ€×» 'ábxé,»RÈ+Ðýd–¤‡Ñ4A!ú$¨ì ˆÁj  nv1ºÖÑÒ5у¡„ÙÝëÐ&–›¤èhȺuÓ ¥-z C¦tgÛú–XZËíP°×i¤£ã¼v²Äq¿KRíã§Œ»HK àç¼€–θ-q ·‚Æ=xÌÈ8³‡eÒ4ÌàÆÞr3cÕ®k8÷¿Ù’PßDÄx>V ¤ }ŸæGÿ§Ÿÿ_ÿ¤ªå¦[ÀÏÀPˆ¢'‰#9tBͰ³CÏý<6×óDµ1ÿëçÿ×Ïÿ½¿þÖ ÉŠa¶MLQÃü„}¿åO2 ã„Äx&|è[õÅßãýZë-YB^É5x쟉ƙ9ðÛm]áåçeŸ“–üwŒõ>BrhòpcÅÈKbí€CSÛK¤jÃù3tÁú$–˜ÓÒÀDìNúÈ 2HoŠ] ¶“¬mÐdÀ)¡kÃ1´…sHe(Ò{"Á¨6–Bã¬oA°_¢>=‰²cYÞ‰{HÂØK³Œ’…ÒZ26atœ îÇG âüÛW7á‘6Hê¿€q¼gv,dÇÇ+ú"Z fdˆ(ôIÐOå8rüƒÿ­¹o ·àa…Ie¶Xm·¡”µ–J CÀY9}þßù.¼þ(þë$­³¾]u㬾‘²8y>K¬²KÆð¼+9Æ+&ioòœlXœ#³‚1`08w‡\‹1a¼a;áo¡´Ø W‡hqƒ»m×g3ã¦' iÊÚ[ÂðþÂ-Üy8ê8ênÓ<¨˜Ä¹ÂüKD3‘àámvÍ[XQŽT$Esý÷Îj û›Ò,Wåv¾Ëz6߆Ûolü+œ‘»×iäŒSö}N7[{£JÊÆÛÁ·v|õøf1‹oÅáËmgž³ŒøwÈ’¼tYh„B1v0ðpk‰Öœk}#a@~æ`pWò¦ÔAŠØ(“·I$ÒAì° ŒÓeÀ‡0“ÉóBQÍMNΉ7¤dSd茨¦ûE2È Å±Š”Oì ¨S‰`ÝwðƆ[,;zFtÉþhôˆÇÕv^:ß‹g$2Û Ü¶ó‡:ØŒñéón&>:pÚñÓhü5º bÒ3MžÏlŸƒmÃ)ˆQé Ñdá;ž?æv;ì¥°ÙÆÊlð²üGŒ¶!àf5ãüd@ 0{Ÿþì¸Kª³$,Af^u†ÛxÛyûg¶ÙÎ1ÉYå·øåÃîXáš c¼lÁÆ…®!ÙÁ-‚E¯ìŠ,6Ÿ°ÀbAõ£ëwŠ^…šûŒ»œK ¤‚ÈFÔŒÀi7UzS Aì$t-u4±~¬ÑX4žÎ”µ5"YŽ0DÉBÒ(âÀ6Fi–ò·ãÚž‰8rLx0ñfSŒ u»Ü8û!ã#¡ãvmtÒK@(Ì7yl¬ÙÆAÁ7¯ÃXI²ÎÒa†6ØçÓ‘†Q™‡ø²Àý‰‰>õKùÜ{Û&}œà·% àNv:؈8…T7V"G£ûüp'xoéüÄF\–Õ”¤,ú¼3ÎðNOÃ'xKHÓ·…ž× ÒC4œËxÃ…[> ð1""Ó‡âJ]älpBtajhF1¯¼d] ÷3 x;üHvA"0»‰ÝÑÝ'²I]ÒÓKÉÙ3ÀÖ|Ü (NæÚ4JîÆø,v&Úì> 1.†´}Jš#Ö Öw™ b„úÁ€ 'Çd5p^—%‡ùm°é6Þ‘Ÿsx%xï°a7ItC—\l¥¶œoÀ éî83x_¾ a¶Þøë†Ûx!†bEŒ…þñûà÷†ß2†ÅYaœÇàüpãy4ƒ*ÂÍϲ?Ai§:Šï ¤¬Í®Êd³"¥ô[<ÛÈðêpKÂ[6üeàà XxÆÞ‚8!µ{x ÁÂÁ`±hF,u ¡ÂÄlÚ?–K„„‘Ûð‰uœèÔ…F¢N&‰ ½n0ãÑcÅA…©Æè’š20ÊJŽÛ"c <º;gd#±%ˆY•ßéÇPó÷£ÉE[FL8Û2pœj¼e†@ç(ÝìYk¶Ì8d„œg9¬Á&6M°œpZœko²öËuœu¶›/;ñ!‰cvÓãò±:!¾Ô‰Ÿ× 2L–tÙ¼wBRŒD¦2ÂÄ Dé.'Ê@ßÏÅE[Ðp–Ýw…áÐç{3'-ÉTàmçëâð¶‡&¼lÞpÌò–ÆAÁÆðá"eë¯]úBưdœg][–!°aÀÒO€NcPé1Ô€ îèfÚ"{±Í¾3ˆHxèÆïBÍ haÔÐ$u«'1ij$ê"¡z$‡¡Ý“¥ô”mÄHꇰd1’’e¿G‡UÎ5´áÛ\ȆXÙw‘×àNæœ÷o°¬ìY·Pƒv³ÈŽ| x-àl$‰4»ø1°É >ûÒø¼°ï.‡ýí',M³†rÒR8ä‚F8oÿË‘E>Ƨ×ñûS0QUíWµe•…ÐÐxZËÂÏ;ÀÎd¼ŠÛk-³ ñ¤Ûnϳ9œo"Hq²&6ÂE®p$le¶ÆÛÂÇ tdaa 8±ï&ØÊ1 b¬×"¢ d ¢é3‚ IB 0mAP·¼‘’L“”áA­ ‡Ð¶|2 l‚*#éûþ0JœSO¹Ííº²Ë.êɲÛ*KÂýñ©·XIãmãY¶ÛxÛyÛí¶Û áçLåáçKĺÀtÇN:ãb.²""ÍWŽò6î5Œ€Yú\ƒQm|/Z¤……Äœb;WI¦ë²}í¡Û¡'"&å Ë£Œƒã³ ý0‰Pé Òf¶„/¸û'‹ì-ÄéÅ,öÚí,'¡qƒ7OÀg`þbQ%´ãxlá›8˜-„´øoÁåø aÇK-…P±>…¥‚ú,|‹± l<‹qÛº!†ˆH¤gð7ûý<¼óñ5°Ï¿ãd²d²HÈ,X²ì )Y7¶H`t j%Y8«ú 2̬¶Ý¯ŠÏAH†"[<=ð²¹Æ¶¼Ý!¶ÒÞ6Ûe¶YyÞ[m Imm~b²›Îó¶D0Æð"ÁÁwoÀäé†û8×bÂô?w‘dTˆýÝ¢Ú΀‘' `ÝIÒeÒÝ›{Æ=`ŽÈ]rÍ4@c@ 1‡FÝÀlæ(°a"¨¢ÃHwÉ\cp˜@ qü"Î’ „–Þ5ãLmç¢í÷ã¼f–ͼ[ˆ6üá:…øcÄ㤆1eÖ8Þã¸ÒXz!I“Ãôþb-£À¯üDFsOÿßú¨4ýLÎ2 8Æ&JË.*°‡iÞÄA}{ËÊŒg翵½£ªÌ02Ê–ËÀ€(ˆá&Ø¢«Ú¯jð³lð³¹ÁÆé ’ô[/pˆ +lªÚó¼<+òÇ ò[ÀÃÉ1wÂq²ÃÀ  éÆfH¯÷Í„O²D„£f(Îb…£:Hl@…ÒBã|³Ô»NãL9À@Žlzê6½)ˆú쩈–¾d nŒŠLF!Ó[4ØÀ³g˜Kƒ=‹i¼ÖðŽY& šð rá)ãÁdÅÕÒ“™–³›ufc‚g ôÚgQ1ÓIÍN{8Àyé&²Ø×‡Œw¶N3áp7ˆc"1]¯êâ/± ~¿˜åÿà$Ki·ì•ß·ëj¥›ˆ¡ý3ü<°¡> (l™×üœÿ¼Qýßå‚?fšÿ•äv†© ³ÉòæÕU]VbÊË2Ìê’¯ ?#TÎuàÞWä¦Ëm®ü7åÀ<<ïÇpã­ã3#’8¶Îp¢™Ó}¤&À°°3ÂzöÌ»ÿ|›YèœHÎÅ 5ĵ;Dô†‚Zˆ]#Œ[Œ;lFÑ×Iß@–ãà2 ÖºrzdWó! ÊT€å7ð!ûÌÄÖÑáü!“¤8ûxRÙÉ\Ž68~+Æò{Ûb]ßM±ÛuœcÂë«hðl7YÜbš¢#:[Æœ6Û 6ÛÀÂÃOmøŸþ¥üÐF¿G€Å:_´ÿÆGŸÁ3ô7oô{›û1¿ÙÖxߤ~Ü«9Ûà¶»àµe–[e—‡&ÙNO†Ûlñ§6Û2ò¬ñ¼ïËaø¤ le¦À¦‰kÀ,h±À7š<gàQñ‚ÓˆëÈ’ŒHdÖÙ²„îÎÚÀcÛ#“íaé°¹‰kÑ4c4Zq¾ôÙ@ž' pÒÂzHý)Ùw²8)mF¨ŒBu5¶ŽË àÈ8žŸº¡ÆÙ~_²Å葇~l<dðKÀQ„ø‰’;8æpA§wÉjY}ð"]wŒÂÈKysàA°Ê$kö’R}l "€Dt»ÿDWkñØ-?4ž‡õàh³ tãee[ReãBf^xmž7ƒá~*e°·Ûo ñ×3äcÂb|NGHa…ž¶m—uÆ!!z.òkÁ&:?Õ#A#WÙƒÒB¹¡>÷›…ú28NˆJ£¡¡""[uvûHçš3¨ÍFûK`º!Z!A†!5EâJQ`®ˆ#Hä93.ÿ¢(Ã0O­ãif§ ãðM84—[mçmÖ#FÞŽ—ÙÁÀ| KxÛ­ã--ˆ8- ÝÄánãaº@mmC 83£úEËÿ®Ù–nÌ»Ïü#€ÿN8ïgúýë©ǧýí½Ãô/ñDzËeîY›-¶ë9˰¹6ëiÊ“ ºòœuá·® e‘·†ß€ò?ä²àxã$€à†Å$¢ð81‰„ñõ‚I,X[FÊ`^ßÓlý.Ù ‘‡¼¾Ýà]‡fäa±¬¸CÂk¤AäpötC†;HÔ´?鈟Ҝ ‘ÜrTdjuÀ;.§ì{%üÃd~d°ëFÅÆ·F2FBî;$´ qÙX!ƒ81M‡DÒ\€j #± K>Ÿ»àÀ#d—œ ±8Öc-d7ø1´¾¹!ç¨p‰">&D¯ Zú'ÅmøŒG+jç; ­¶Å¶ðÖY[mãmçë‡yÛyYyÙ~;ÆñªÛ×úi˧ŸáÐÄrfp BCôìC˦6AgddœÉÞÏÒ…Û~†U޵³A!,9¤¨¡ 欠Ò Ü®ñ¨Â]„¦ÇjhH°(ˆmÙö‰( b籘 ‡eéº Ô:Á,V ±ïC$rÛÆZq¼d[e¼*‡DƬ±'ÁàÂÛ¥Þ›¾;Î;øo­©ñYão©z$ãfU·ƒýø›ÎpHÁ&|L»~Ǭ‰#ç:†Ó¶87ÓŒ¬„Œ^EÔã{8'³,è·¿ê$4ƒò.ÇrCm4XÎò?gŽ“ØrÒL]™8Þ‚èð(d¸ë¡&êB‰C¸Ãš’! ÅÝ…Dr^ˆPcRÛXW‡ŠÕú© ¹ADä`ˆ·—,Œ¶Î}ëoYòÌxÇ8‡}rfDNÄeŸ ñáywbPŒÁ®¡jüƒaàq㬌>[ÃÃÇs™Â¤éË!ƒo/;óyz8 èùgÃÿ2uœ°ƒÞ§MÕÞB–ƒ¼l ϨØç`à@cmmWbfÞÙþqÜÝ2󸤪$ÓXÏQd AfÛ!îÀß`¥ôº`‚±˜#0pÁ”ÝÌ[DµÌHØdÓ ½;‘õØ+€ ÙðבäpQon6 lœñÈ´åR'ÈlŽ{#ƒ_“˼M–Oú^³©ãc8<ƒàÍã0ŽŒàmûYnöÊH[(ÎhþXQ-Bñ4’ú»=šé!õ;̇¸Ùt¶ÍÅ·OÝ£°%Ld€žÁ' \2#v‰¦Êävà,×c£ xËI,}±¼6,àY±™hÛõjÇ/g ðÃ…3´à‡c„Àåƒlø– áÔ›íù'îXí·Kl§á÷òÐ5xC,ø:ððœåœ’pü3$-‡uù™²ëÁÉùH—ßÃ÷kl:'ewWqüÃü¥œ{‡HWL‰Ô]75Ld>Ë¡¶é è²;ÒðD“{1)è/è»ç1°|Xus,ÃŽ­¢(ˆ@¾9,:0º¶˜Nô~›XëxË'/Nlê3I¼o¯‚wÀ¹<†Ç²cÆð[,Ý/##2ÅØ‘ÁòÑ,áç&m g•ºÉááãxgãœe†›lŒ8$_†s‡¶Ye“­–pÈü³“¬–Ë ø‘ Æ–ñ¸]ä86¬ChZF™ÇT[?ÍŽ±ÜØI41…&38ÔN¤ˆ[ ‰qÌo €²Íз¥Hì…šž4LÓƒu®%šôÎØxÛߤ"Zi¶šHÂÄ/lµŽ–Ûc³‘8 -8Ë4[-ž àI~}[ÏÓÆì6¬|EøáÁwœ—‡ }œ³Ç×Ëc‚>XÁãg2SåßÇC‡~­¼“ð>dp–FÉÎqéÎ÷*ó¼g\ððH}À”E'@Ht“­Œ|nÁ¡{'=†"šNnƒO]ÉRép¤ŽŽ‰$ Rð-4](4 Yq!àO 0x—ÛX0/¸`ëœägÖËîÈß‚ÙoÚrOÁuÀ<K-ãx†Æ¥×B[Ü{zò¶ü5¶8ΙÆy»¼ƒœ¾ùɳ©ùg/gà 8xCá“Â%:‚plÏ)ðN“²F³§ÄˆË«»Þ[–¶Úͽq²áwl Þ1ù°~î$ A7 ÑÙìLÉØQ=C#u2×±µ2Þ ÔÆ4ñ‡NLm$@£oa,’£KÕ#ÙH7rÁȰˆéŸRfI2+auœk ó“ÇAœ±þŠgÄ–Óœecm†ŒçxF]W‚ÉðÛ\àÎ Nâ=“¼bŸ"YmgàÈ“ÊÝç|€ã>'¯!'t²Éƒ‡~ Ê–݆q–rï+@÷S F’gWItmVÒEôÏ«á2&ÎD·«îôxÎ àŸ‰¹oo|Nç’paþƒeÕú8îï‚%ç· #‡Këý'$ølå¯bXq¼l<)ñ–rq¼‰¶KÀã<¯ ##‡ÅãR>¼˜Æ|;Þž±ŽzÈmX;í¼?¸gív†élïK:—Ö‘ ¹·V.N«D‡,}`vBØiŒ¿L€‰†¹n“©Ù˜ØBjf!Ä EË5„!vý€?Â}¤ygŒïc¡Ûpà çè…Ö|ƒâs˜ï.'&ðr| ~ sœ“›9Ç'‚Æ[læKñt䵕fÛa3å¼xððÏ)03çÚçÇ,ãIÎ2Èäøäñ–X&0 –YgÁ0à’ï9Ë^~âƒ6ãbëeço¨´ºÞ;΢cË ý8Ž/eôÈ}Ù–£(—P™dêp&Üg²ÓèFÑ,xÄÄ·õ£$$H4m±E-Éh™#DE[?ä˜åœãŽ2:~ÃÀÅÞÙÈq§|áþ†u;ftðO8ëjÂ͵ÈJRüîÅL¸¿­AÄÝÙ°g%Ò4áxCp2vj¿©Ò4:¶''ð&_CÕ£o]pgÚÆÞ6Ѷ!‡9yI[ ÁAxÛméÂÃkðyOŽDðbqŸ,³áœîü^;^RÎrûºùç;ÇÓÉší×"ou†õá;.£Î8°÷/]—·mÕû£cöH’Àšœ âÁAºÛa„:lû$H ´B¸’vFë[«‹AËRT,§ÐnJ{æ{\,xÇN*G§Yk–ðºò|rG8fúçxÎ ø¶ÇÃïŽ\¶ÿó÷,HÔ?}3£Ð [M„ß’!ùƒäHÌ+Îý™hJU8?Žã´ÏJ 2[:¾¶=ƒùßK º‚:²± wý¹eH0+Ûü‘ÿ9•¯Jð^î ÊVlÿO†­µBìµMË ¡æÛø´¥MÕ-p+@Þ zÈ Vˆú»o­ÝW\ný Hn…‡Òèƒ}8þ®,¡F?æö û}ªaܹóì—² ;'å‘Ê sùÕØaç÷³W…ÓO4‰úÁ”0ò>Ïø¯¶"f›Ä5—{€}‰ú¸ðJL /“ãö ˆ´uaüK—öëñ%$Þžû^'/~’ºŽõOãÙ‰KØÞ8Ý“ÙeÄ&å|H .»¼u„aÕÊà'º°¿!‡òÿvr$™S¥'øXEA(Ø pÄëñØ” PÍ·„CÀ¬ë% |Fsá–|Þ0ç9Í7ý,’ÇgáŸè’pÆñ¶¡v›u¼' Ò3x¼1ÃúÄ´tcÁ˜èJ]EšN%õô’#ì‚Ãm îK^Çc$ˆ„(Ã$(tajˆÝƒã7j"ý„%ü¢c'I´‘쬀cl>G·ÛÉ…“ui,ÝZ伇9Æœ ðËK828Bôüèd¤y€D·8.” /ÎUp?¹¤ }êæÇ ÑÚ.×n_°Êqbjk¢ ´u«.X?a·…üÅüÊMw~ÓeŒ±UŸ3¡•êžGÖ %¦N%«-ÖL㺲Sä‚ öÙD Ò„– ¹ûS´‹}º‹,Ñîå\À&áÖóŽHB«tRÊæ;;гù8 qRöÌêf ²\Ø~Ónz› ‰2Ä"j›+Syë±TÄú¦fÍ­1ô~Öu0¿…°ãA¥ôžQÙà- €Ýá&­Ì™œµ̦)Óý¶• ó?”I ¢Ð$â[ïOÎ@H/BËÖÆNÝúþÿU#«Ü0÷ú[g¿À¥‚¾íÃPÅã<¦YìbÉ·;?üþ³Ù ¨b’l+"få+ŒÇï: ð󡟃§ò¦ÅØDGs\i«ªŸžÃ¿:4H@A ’W½?Sõgy–þCw¶tƒÛKñìF¢ɬ¿å±Ôs› »ÚCÆ'œñô»ëò_†ü‡–ˆ7ŒN „<³fÉ„Ùå8üüIðtca¶Ñ!´´Ë¢ Á°?°ŠžÂ,w¤ .äÔ„L[è”fƶj÷G±lÛ¬Ða,F‹aú%¨HÎôe‡é‘>¡{  Eâê²áûnJ¬ð£-ó<+qÙl’f à3²LÒ8^DDàã€CÉ™hq±œ#g9ÊYÆD| ‘uÈYf¶XX@fY&«hþ‚ v„¤PŒ‡GŽóGÕÛx .“Å"À( ð$ÜúŒFFˆPꫪ¯«9ÿk‡FnK¯ÛoÔ Ûš^8àDŒ}“Œð?E†KâýƒgØöê¿»8`Ñ — WŽfaø¶üÛ›ÙàºÃí®N†=&Sé ƒÊs¯F6DÏmÎÒ(þãõ8Éþ·ëÄç÷ZÈ"BÕp—:;A—y}„ý 6'Ÿ~ˆÊÔã‘ë$·o½~ÕÖCÙS~‚·tmma¡…a8ˆëôDš]æEôþ\ëιœlñ¶kòmx~*¿¹Ë†°ð&pgÀnœK}<øÙhàáý’¨³îËd¶’uÆê,yŠ2f±f"@]Y 6ž$—ж+GŒ%‰tþÁ}¸“»Ú\Am×`Fïf©1¸LþÖXoOÁÓwƒv±$6$%» ²ãl2ômÔ<¶6Ùtøl¼lËÎÛ—|ïóÈôôÉã~9ðF{tŽzç8Ë Ÿô·6Ëc;S‚'q»ò'?ÿ™ÜHBi ¯IÇÑaÎ.ÀÔxAòDZãDÆ\ŽÍ!îìÖtŽÌ‰ävÓ ÕÒSqBqêÖ÷xþQÍN>üõ·qf¶I gëÃñÂÖÎr7ƒœ`iÏÇ?и?¾“à*[v«Ø-{ÀÜ3Ÿó¿q²Rh\›EúúšåÑú¬M+?<Ø›ãºBå'Z¯¿Ù…}g1þf½Z§YßÜ$s0p VAúuX}¢LÀ†©NYÁ_°ÁŸcqa¨IúÓ]ÓDDQ:F-ãxg,±&vqÿ“kã1ý¯æ?þ†}uþ„ÿãÃùOâßüÉ~Çþþgø¯þŸ·ú!ÿÈ—íþŠüÿåâÿ©ù¿ªŸ´ÿšOÑþL‡¡ü‰ûOù¨úˉyý’~ýñ`ýßAü#öú¿ùíÃû‹þgï_ÕÔѵñ¿ˆuÁÅ?Køÿ¡¿î~Çý7ýÈÿZÿÜÿæ1ÿÅ$=ÿ2~Ú°0Ÿ²’ôÿ‘q'öä}/äg|D²ôqÆ„A8ÇFë$ Íhq–pœ¾Û–ñ“Æ1'Ã~ ¼?ùÂ~YaœãÆšÓ¯Ã&šA§KF@ ¥uæcök†ã¥¢H`ÖäºÆ“¢àØDîè€$ûUÉQ™nHdG1u O±!±µüˆ"äÑ©ÙnÿÉY|­™6Îð–|98x?‚»Ò×ç«ÀÀÊ’êÒKê×1ôL1O¥«ÔÕìK‰ü«±à,Ž(Hp`ëŒ,{H=Cʢɭq!üïßÓ·ôÒ+rõÃÈJæÂø?’Û Uüf‹½¾ÿ†Æ6bXf±ŽðÚ˜h"'²খpr³` îàÑÏ_¨Çþ §ü ·øÇ×ýÏé‚|ÿ#dçü–_øÀ}ÿËþѱÿÎíßï#ñ?²>Ïî#òŒüqø©öÉ÷ÙöWæ¯Íjû{{þ"[ßï}…ûç×ßé½ýþ¸uïô™o`·¥þe¿ý]¾¿áŸ„¿‰ücŒ?ˆ×Àþ}'OpCëþñOö‰_åVÞ7ò·â_å.ʸÔâ_o´ŽeG€DdMC=Krÿ¿ÿɦAõÆ¿_Ùè-pŽºH€Mz YZ©™zøPÖ@yL$™?4ÆÒx4ã¼½ááòûx%ŽË|à‰à±!ŽBÆÛFþø̲Iö‹ —Véb:<.¢q£¤‚2¦Â™ÃˆŒh'ÕÞ1-6Q€äƒ ¬©jq¡[ OßL˜ÅÚpi$i¼i}Ê,? Pç¬c‡>oH„‚©iT ÐY\j¸a®á æIa‚†<Й j ª¦v÷/Ñm¾(ôË ½Âî°Þ+¢%£Û¿ê:ZIœ¯@hý¤AoÄA *DSé-Ïø¼ÉGÛýSø úhûÁý+õŸâƒþ }ãGáŸÔ!ÿ»ñŸóÿT>¯ñŸû¿ýJ>—ýÏ«_åßþüýÿò¦þ?òù§ÿ‰>¡~Ÿë1÷ÿQ…ñÿŠÆþ$üÃúÿÒ>ßë÷¿ ÿÜ'¿Õ7÷ý)¿?û“öðŸú¿?ùù?a5Où¸ûŸæ¿iÿ5ÿןþ¥×ÃýÝ|þçOøØ_'ŸåH'¿óü;ð¯ñI‰áaŸ§á‰Ls¿Òß¿ùÛàsÉ{ ËûMgõ?]M´ó/f‡}0ám¡õé?JD9"þÓ'G›V]m‡‡FyÞ _†…·Wß LòÙ½¼+žÃ0T—ƒ™Ñä–'°¡™FÑÐN„ap¾öHYá:é´û»}¶„žž’ì}1”ØBÉÉ_1‹Ò6h¶÷ ³Û™iƒã0dSWí˜ÝÐÃÈÚ¼=±ÉÑšò›Â?ÆÓx×.ó価ã;ààäß‹ÓgI$È29$ðòóѯÃ,‡9! w‘à[emµ•2q·×;iœ3'Æðd‡gðíø7yÝðñÆœ«–ü;cH‹ÚYvOoÔrMlª@ ÝébGóÈÔsÔ$•Í!Æ]Œh³$Œ4M ¨S§²p˜`_”·¡<À€x ‡IŸØËÚ=ÛxÛV.’"n€†wãêà°”À–],1‰Ðá‰àäxr9HàÂ<#à3¬ã:ü·€ãy ç°ãKmd³„sá‡ÃVgàóœìŸ1cÄÎ:xØø+¿,åÿ@øáÀœ¼¼`øf‘vCºÌ¿XœàÎ p^—{Ù;­üD,E´Ih@0þl09‰³„€€ÄaG' %ÒIÆ)"¨—gLèÊ[¥¸ihªH·`2ì:YˆL‹ú¡õ‹ìGoÁ—ëcq¾ï©±çfÞ¸%Æ^{O‰¸—v˜gÇ/¾ô]b^4çnãŒë8ìŽ>¾YÀYÊÁ3’Ïl™ó„ù*ÛoEà–uðúàQÓâå³lòÝd¬v%¶ÖU±ÈçKlˆÝœa-ŽX~3‹Z¶ÆM˜‘™¡ÃÆIðõtã»êvû¾™Y;…ØßèœC'F#vS|œNÁ€u!C4‘Á,eÄ”uL‰º&FbHHgwIíÙ.ذ>à P€rM%Ž+¼o¶ÍUgÁ œ2|†ÖÞ1ˆÞ2L1†_†6'tǀȎ6ó»®6 ¶[ׄrËu¾ÿÐbÞŽu"Åy$çY8I“‡‡–~ ÁÀÂlBtçÆÙáá—àÏ,/Ãz 8&Rñ~Çê1ƒ®3Ié絎àn„¦e–p èËöCú„}.Å‘HÙ¸GE@t[r w©?Ib—I‰fAØ–†Ú˜9t°¶ ŠFš²Œ%}EÛØáÏò¿0…®ìý ¡ì£í·LnÈËBÓ ~k|ŸÈ½Gò,o“@oOÃSRlw6Iöýg²Ïº]$¤$bú]bŒc_|–YÎA#dÙ¬-Ýpì`1[Y¼tÛÞð ¼­†nA«Æü“:ÑŒIØKsá–Yn™w'Lû<3Â[kuò>+Û' 3œnüuºÙÌáe‹m—à·é’ØxF㽞ì0Α¶Ö08êÜV[T÷xsƒ'¦Žœ -c7ê;‘…\fÖ]„aAec  Œso_²@l5öI£‚1ñK¤¶ÞÙÆÑÎïÈÏ –'ÃqÐãlž0†ë~&ïkkæ°¶ük÷ú¿ ÷Âj]SXþŽò^€$ï’m¾¬­ØÒxC-ð¤h‚2‚n yû-?$ –vqÓÁ‹d@¼‚ÝØEЋ8ÁÂqƒw¼$ÙÜ’pq§±ð8Ÿk¶ðºýŸDeÖÞD1Á´yxo `T çca„Ý—m“ìãe3–LMl‰à#3Œ‡u°ëw„ÂGÙ³:Ú² Y‘ f,@‚ޏâNn¨Æ],UÛHŒ"q|µ-ÄV SˆqëèâÀÔÅs!f({?)õ.–Ç`ØËA·^>㇄ŽMàôÇä[Ë9^@‹xN5šÙÎIðxÎÂb6ÌmaO9×òÛôPbj?: Ýß‚}Ðcù±ƒ ‘†Åë¢ xb>àÚü.ŽT³ù_F¬~›îÆü#ÆÖŸs‡¶«Ü’›?š@ul,ÒŤ¥±Œ¶Û [o%¶–ÛÆË¤ñ¶áÀ…²b‚ÜeäoÆÛ/ÊA˜©â×ßO+{ÀË¿ –Þ ŒÎ6ٷ৆ÁÀÞ8T0£Á]x×݇ dHpŒnÈ[uáã 4IO°Ž›c+˜Ä|×'`$ˆ–ý¼´vCAÐ@½Ãtd%÷©õ‰ÀcÅáåKsøNØ’ÏŠ|²É -댲 ‰Á×D’Ь“ýáøræó|±Û8ëàgú½e¯­…€Ù"¾Ç$²ÂðbÁªÝþYc«k¾Âïm³±…Ñ 6Õ÷eú­™Š_¸Êü7ñeo¬Û6d*­›6 i#a-ÒÙ@2Ûgã8ØŸ‰¼HúÌq“…±˜Â:$eƒ‚\fêÁ_¢]=4‘†Á³D! FÙöBN‘Œá°Ã¶–(ctZt—[y=:bcA˜°3²Mihu²$¢b†Á1¾„; ;æ‘Ì>RS-ê#…˜fP½`Ë~ÃÉ}¼w÷Yñxo¯ô—[ð1y3á§Ãyl=´’ êûã²ÃŒ8~øÎOô“xyÎ>¦Ãyx>Yg9ûxI9Ë^5Ëxà °°²?ÑFŠP þXk#òÙ”&Mg¼ð/ÚLp¨¯Ð[èËêË»¹€!ä¿$e~VSkùZçŽ0YWY9'€Ì~d~m% e¥¬ÃN{ˆi“bDñ-:ÖÞö A…òÃ|œø8˜Ý8 Â\/Q æFœdÜm¬¾)õtLíÖÔŒ`üF~`üã~~ðÏ€,ŠÄN7äqßÁ™ Îò±ÉÃäøXÛñ[¼ønœm¼éÊÎCm¶ÛÀü‰ãO–Æ_è}üǰøäp±7k}ÛÆqÛ—|Ã.Û@Úò¼ìŽÛ »ˆ'.f«Kéÿ˜’Ñ$8Êö°¿e‚q¢šAc:]c,T|N½ij×.ÆB—ƒ6¾ÈSÁ–$`6OB :ƒôß§v]³ï0óÖ[g;uºðñ×¼íŸè?”ãz=#‚-b:ã“§Ãu6%š|7Â"Ó…Ð,x³‚;l¤çž©5>¶Rïõ9ø§Ó:äžØ:Á§b%(†;59˜ ÅdÄ€å©ô‡Áã¼>NM«ñÛM·„áºÞ~§#ä²96ëý ø;ì´–Ùml9xÞ.¬$cb:ãaáŽçþRÄ'H³Uˆíï§ÒósQ-ÒQà–Ö@Âåv™ÀŽCŒ§å5“ lrH°æk¡"k‰¦$!0¬G¼xv~å>É>¶Ûȼ6ÛÇÜœa‘¿œ³ç®YÜg'ƒ‘¶ÒØxXyÜŒxs>¼g#kÆñêAöˆÀÐÌåB “4Y}û?«rËï¿‘›‡ïÈg¬oÇüˆ¿“ÿ;‡gÃítå~ø9Ã'€yÎ2‡Ãxá×CœlZB[ÁÀÚ|7áÓÎ÷ÃÅ€’¶Öû¾”‹¼Ñ³e‚€uþÞ-q”t„ÍàrÞ aOY:Ð’C h’7C`®6ä„‚iǦvôÆÐ”Å‚ô-êIå‚"I›aèLµxZ_<Œ¦Î/½-® ì­¼,o3ÎÇ·ŸfêyË8…øs×Áœ–ðúDKʪ¯`Ú/ýs ã©ó w!ý¿ˆFœþ×îAñ—]þ5\SÚè @å ?Oð€äRƒòd£ÓA×-åçysœáøv|qã~ÆqÔûÊbç9gûðW„àÞ YxYp·ƒž‹Ky-¶ën¬‚Ô0ã°ãd†wá´ÿ#ýbÃ%+–˜ ð;²°4ÒÅf{,Õž‰)+š—Ø„)ºZ ää¨Z“C¤¨èB|ÐesBÁº‚ˆ1‘ˆ3ìÅìH4²CìfRÞäuù<çQ–¤ÛÁðúøooÃíùïú5Þ0³N~¬,ø 1€;k7Òø"Õ&ž3\ÍÈëo‹ƒ¥út9*¿£ê?¢žKÏ´ù¥e"€¯n ºôX>jñ¶Ú[ÃðÝ‹¿‡llì|Ic¶Ç‚l÷‡YàYxv"LJ_ŽŒ<0Z¿xíÿßÙDŒìaýZý‰Œ1ѵÁ¶9wj2FQ:HC¤&ÉôH‘bZdnL6K«È¢, òqH$Ý5)wÆ`ÀEgcw©I¶Üx- eÍ´Ï€·´²ÙÅ8m6Ùã`y-µ‹>ò‘²ê¼§Ì»žwâËÆ7{aÏcãîD¶~ND.ò[Îó¯6|óŒ·-´ŽA‰ááèàa·‡à?3rä½ó™a`M¹ãwìCÇDYïþ{¬”:d¤&ãc0öØlŸdyÜÝŒ÷w¸Á’ (IöZqžñecM,[Ÿ”K6uµîÍc?µ@á±Ø¡eÕziµ DQóßÈI ØB˘ÎAd…‡îÇæ L}åÇîÇd ‰Ùwv|5!»-ãBÛm¶[m m-¶Q´ËHm%Œ”´´Im¶Ó†xÖîׄø? µ_‚ÄüR-žw®7ž¿ÐÉçqÒ_VË_–'Àmàò^N‚s·yÃÉÙ#%¼Š;„ð³ä,:öç q¶oY&ðÛ ã1!vunÛ Ýœ½%€GLm%4„0ŒìmíB2ûEœ}é!#vê’2Ð˦Ö‚Eû‰Up H…¨¹jÌè5²°$ AKBe–@ÉJ D +áhx[øK4mÃm8XuYµÀÒt=…MmBÕ•%ý_Â{‹®‹I`â'V~V™1퇑$ÀFì [̰I‹ DE8ï-¶ß[m•iná Xàa7ƒ4„-!----?6‘iiaÂpï&|B²Ö2Ì™o¾ øZæXÙwÆœm¿xvÈž1³ƒƒùfYŽòù+0ãš°ñ§À’5äx[{ž ðÈ¢D‘–c ±c]%"ÁrDv E„ŒhÂ¥†Ê©c@ÖѰvöÂiHbbìŠNTdpÆ„Z«-o_÷‚KU-vGƒ¥$²ìtãµå9õ*FÆÄ,l’îÄáÞ^E ¹Þ…–ÛYyë8ΗŒç¸×{† úʋݮ, L”d¡˜¼òâëõ:ô²ýYO$g‘ø!„ M·\vØ@&?-øl‹f¬l``C–6)¸ÈÚÝè›k ­¶Ão-°Ãn6„'"Z[Ù)9Ám·O"ÙYgsdü r×á¿ 7áœ'CÈc,fÏ)Ã7BÃÊÙð °œÒÓv-»Kè™Ó%BÁD-ÛIÇc Ú,– e!v“·áŒ"Hý[vtcsµ•—q†ëGcÃA*lY¢çýìdÙø†èt·\ „O\‘‡Lcˆ-6C,³Ù Yx1˜±³ž@1ÂÃ`á†éÁs"¿͈ß6çvøªÞ“³±±±‚Fű,`¾ô#3±Ûg£n³8Î mŽè²:e Ntã¨Íõ¼ ½ég Éßæ®È §4VM› Êyy õ"¶¢ÊyÙo[°gØ>¦ŠYû$:‘ŒMâ: Y÷~,´„-f:--# cx×Iaž0’ά‰ž8,‘K]ã óõ2×Û-,#8ÑœeZ`Ð 6ô]Œø$NÝʘ¦’÷ 1¨éÃt¼1É8||À ˜bÍž€6[m%8ÒgƒNh¶ØáNzà,,,<ذ¶lEËgˆ8?'fÅ® f¹3E«{ÄE¬ËV¥ZË-Z± YÙ‰k)H&éu¸Ø„2Ìd=¡Æ ±hƒsH£`B‘±÷"r>¾œ Ø q'0ºÈìͱ!“ï·TEåRmv&B"È# jß‚³ã7¨“¤(–0‰ÈÄ&²x[4ÞzŒÖ}H, ~] ÄúsáÓ\œã ÑÀÖÜž ­ ŒZËSð(c®aŒll~:Ês½Ûo ȶÛkm¯ Bä«VíÛ…šá®$Äpcãáâ^öR ±u`Á±˜K"a„·«N4㨠°°°Ë0 ,'‹3Á‹ “À#åžœ:<0ØgØ8 a Iôa‰Á jÔS‰i`À »»Iö6ˆÂ‰ÏÙkBTM$}-QK]ÆlÒÆÒÙ7X#a|m„Ü»œBä»WcÍ6Çt…$F,vⱆ!¨{˜B]„8‚a²H&g#83¶[&‹³¶Ý$3»k pkÃtÄÝcÈZÚ¼kϤfÿ¦lð`¶,Hû.‰­&;&„$z6?0cø6¾›÷iüMúô6?†Ä7>{ð6ÛRÖ rj×Û–[‰¨Qo‰7¹‹·—˜™ƒb –`¦Á³¶tˆÒƒa‘™b³»<‚òi¶„ËU’iB4%˜L„ñd˜G06¤,Œî‘Oâ|mͶqèäcãŒ(Êi³¾„;.8@êFŒ04 ,Ƚ»ªÝãcd$53˜ÛŒP¢§öjB¦ëÕˆÚ:5n¸+k\AUÀ;U³Ùˆ0OAú‡pÒ>¥«.üab@Ñ"ð7ecÕA¶Ê¨ ŒE :hãÁ¼5%Œ|I6,N÷–‹(¬<6ÛÎ9Ëפ§!'YŒ…†F0W[+Ã)ËšE©V6$ëÛ#Îs6ª{qp‘ 'âligo' HŠ='ÁN ³­yÎ7.ì€Þ g–'$Œ¡fÞ XF–ƒ ¡±’©Æ‘-`rYöhIäMTºÈÌ…’ù!§èÈDI{4çA4µH}2Î mÆC?´u+ºË³º–é–$©‰(’Þ.¢!£¸““&'&}¬Çlœ,I¯=³©.P_ü8ª¦(I ²¢éIUÙMÒ °?×ÑÛtPûqÆ`rÞž°’<'ÒM=0E&5ð4d¼cmÀšP14¢³ê<Œµ/è!° û-uWW—ɵ]°ë¬­Žä Y–0@ýàs@(ÅãD[}‚À‡ j ´'ˆz°´Oe°GM;On”`¶]²Ý€ÕÂô!蜩õ¤E»~d62ÓgRÍ‹#FBдü™ÆÂó¹ ŽáHnóµÎ2:Ë $"1b͘ø~ pòÕ½!6¶Ž@Ùe÷0\X:íœogÁ4‚xc†Ù>ÎðXÌHƒ‰ ®1Õ„Â4ŒLXPÍ`~ÿ WÂdâº~—Âi2$¤8L°9¤·Ð ØZE0IyZ1æÛ)2Â$fÎZ#¦b^E3qáLãÆ!ÙFIêÕÑ”!„ÀYØÓ[4à©“êÆo ¤ÃÓaÒÒ[PÔš$sߪî(ªî‹IË[fì'l*„™»÷Óogî·Ìíÿ’\#n²á¾³‚°=5T[ê× š-m» U†/%õˆxþóŠM_ÀKÌäz'ö‘XµöΤSØà VPKòÊŸ_«”°HŒqú…V× dRÂM!>¸ƒó»Š~Â4â,úds¢G…ûÍ·†`[&KI¬ŠH©^ý9‡DžÉ"Ì÷ƒ ɨ„ Çi-´pA‡¸¥âªë„4€&ŠX{ýÏ—[`AX¢»zSzXÖNäÓ4 %šükO䵑¸ˆ†{ ’7DÌÉh¸$&Ú6ÚÛ*36p¶|d,6Á°HË2rÃg,ÛÛE¹vÙe¢Ô¬EŠHɢɯ]ÉÈ?޹*ÆAF:µ f0ÌüCŒÃg¢õˆ¾ëÁþÔJíC÷èd…âÚ„+ÛB2*U|,mÕíPÑKê32¨ â@lŒ,Âö%ÞôK‚d§‰}O²u0^*ÉÐ’­¢æ^·\HÂØÁn›0Ê[.ȺHÉ7ÄkÆÀÖLM#жa7DàèµiˆAõ)„Æv³ ²¼šßdÝÑ¢JÿZ ptv²z²‰£afØÉÁEü¡õ+ N3×õ1$ñÜv«pƒ„bp5‹¡Žzÿl†pR ÿ1|vPöî'P¶9"‘bþcP ¾¹ÜVÍøõ 9‘ŠIq7CXöØÃìq€¤W©Vá öÒIÚ`t¸Ž$5#€’Ãå—àÊôü"üqôÁ,öš]Ù?ˆ1úÕf²?CžŒ‚  ®Ú¬–ˆ§¤o‰øDo"H6ÈqŸÝ€ªlDÒ‹3YkÜX™Œ€ átÛJ[&h}–=1‚q\dÇ»À=¡(›vô>ã‡AFÀRÓƒv0 ;¹:ÒM,KÕÃXšMýÅfŒCSa†Ò7Y¨BA€Î.ßfÆ€8ÖÅtËcYCC€¡Iiia¥ƒ'Ø´²b˜ÑšE‹ `8&N6 ¶ ³Ö“Ф8ׄb]^•ý¥ƒéš>Ì“4dÕktcÐ))àjhÄEÃÓò½œ'ˆš2âl‡†4Ãd`Íi'Ù‰it, ²ãthâ]…Ò¥ã/L’H„d# ÃÀçlXšZ<ï¨Xêå¯ÄŸ©>d㤀p0iiâ3–Ⱥ«d .Üá1ÕÐÚµÜÒÙ¦ÚX%å–E›ƒ ú†½µ[U…£1ŒL0U²“í…<'’Ùµ\ Vs€÷Ä%”‰ö­OÛ~Ž„0 ` X¼1‚³!_*ùV5Iäý$ˆUUY€oÅˆê¾ ðÒ+`PÂL8ÅJÎØ¢”¿Z„ƒ¥qý±9'ÓóëaA3¦KB: «Tê¬raXUUWÕcóÑ Ua9ÐáÃÔXš¶Øu2À:‡ì$Z(ÝÅRéGû´V’¨® /*FM—#ô»ïmr–WÔ`„üpÃI¹q!8䤲iiV!‹Î:±ˆñ :À ÑúD²ê+N$šØÀOV—P‹˜±(æ4ºý èޟǬèt±Æ ] !ÀÞc äa³øÁü¬}¨ýÑ› ÈÛLô·z6v6µ 2@ Bq”WahŒHÔd_4‡½  ØÂÑ.«À2Ò,¤ ÈuajÈŒÝOå¯"Q“dmFÆÕ»Ì2öÓgcK[±hÁ€2_Y2µµI K0Ã1ø$p!9²ád@é¨Þð™N‹c l—c"€ºl$=e˜`Á|Ál £Fɬ³­æË»Qã##Ôa5Nä½V µ”Le.挳ˆR6 3¦Ìq’Å`ÝÕDFAàZJñE±½êÙ ^$ >åˆ ¬4Kv[8O=ç8~Èh tZD"P$ $I#¥í<‚æøC©@<”%âΰ%°ã®¯Ót}¥ÛJ€x eÜa ÃUŸëmƒP™ªÈ 5a k¬7D‚áêýÃT\[[g‹ÆF½—k=Vß9à¬~猲 @Åjcxl$.ûËtcó;Îá­õ;*€‡Ô8±êmÞxšCTÜÏT7‘'UBè.x³üßûDº3|utœÅIŠíʾ¨%óWQÝ–N×Td±‘ŸÝ´U¨ã,_%î(p"v3± }Œ¦U0ìÿCé6[ð_ºC¥A‡¨!oÝÓŠ'–˜šU=ñ˜)0ä’¬åAˆ|#ޝÒSèH]þXú;Ä6UK ¢$Q#tdë] øÀÇX µ‹¢,'F$NÆ,v-BÕ,Î|I¬/BbÌ$ã„®[Úå¢QΛЗY^ƒkÓ;Ûn3ÚÃÞ°†šÂfÚ°7fë ÚðrÏ\‡¢ÃÁhο L=d†ª¢õåà —(¿qÚZQC@†cÁ@¨?«x 2³ÂÑ©yÂB.À5XåúE‘ìl°dšÁúíÊÛ\³vúàÑàÚe¤ã-ˆÐB4¿;ImQ%:“†ŒVŒæd¢gÑlÑ ˆeÙÂc±' r)mXü‹Dêsñ€ƒlKÐ }®w–Ó}!,z‹ ûº’xJZ›µlp`1d!ˆ. ™¬wb%¹oV5œ–B@HB5*NhIWó…¤úGõw ë•mvÖèƒ×öÐk*±1øÃÃqhK ©ß¡è¤…_ÇÅÕ‚et£…Ò‡Yƒ ÍRŽh¨°ŽV‰‡T*Ÿ¤¯LÖèaõ»€èõ6pâo²J„T`¨ ÜøKes)Sù»ûú/ÕàÙHQ—}-â%¥µD…e„É!BFêï‰#C,i", ¶.‹} 8Ðþèûä´µcd‡-K{†ÜrœŒµ,!ÓKO-7@MF6E& ãzµ”ô@ y .¶!¶•ÕÚGošÀ"€§²ÔSªò–ƒzØÁ‘çCuÏÂU‚~©Š­ß HGIc“A»nÄÒ¨À&Ã2Ó’è”r ŠžÌ‰^2š3ã¿ID4v\Ћ%þЪÀ¬L 7?PÍ2!‚,‚cTÝNG^u´2f£mºp‰}„xo1âƒÃÕæ±uu¢L_¥¬DÖDþóûËé}"ë0‘:©Â3 Ùõ‘ÀÕc€H¢NÇNEðBü%´2‘'§¬p‚Œ´cvñœ%-ái_ë!êÆèªæzK ÆÁ<,¿®duéÜ5gÓÃ0Ãu—yÁ1Ȱøl­¯ ¢ILcF’Ú]%Q!@˜Œ±ØDm¬  zà…$ Áˆ}†­ºÜކ!v4-nÃ¥ÖD6 vÇèÒ-$ºµRQ퀭ÈDnŽÎÁa³$82ZNM—®Œ ³[­ü1ã£öÏI©Óp˜ƒ¢2YÆp=d—Ñ –ªƒ 5>9#eØý˜ÃàƒÑ¡$ªw#ÿWQX2ÎÖΈíXlÖK… ¯ks£Á³ðB7mpv^Ê_Pj-¹#Ó³˜-ò1•±º?N^NÑÉ ½ÎðjÿA–}ÈŽ='ÔŽÖœ1‚‰¢êxGx°3õ¨˜Š2pôhò‘¹ qÙhmBÖØ %½c,¯ù!©\è[±ºÝl¤Á±;õ†C°°I Ηd„Bìˆ"·±NH*B—YÛ¬µ‰·Ž²;l‘º$pÙF(èËò+ò$ü#„¡„͈çhŸ»Tƒ÷äÁ.i?a4ÈàØ˜ŸÁs¤àO”„yX”ðfÏ3×yègHê"%KQ5Ó·œ3Áx¬µ­ázvq–›ÓE/žæÊ|Ĉ†[ÂËýÑÃK 8PZ"0d¬a¯ÀÒÎÇåÆ9)c˜2«1Þ âd§Ñ?†<.Ö"UÝRÀÏw"DãÆaãÁ÷)b„—Òöƒô >–¾ÅÛšSª»µàYXYá Âé,¸ÂŒ‚±ˆêÀ‚BIŒA‰š[e% ‹ÀKVÍgßþ–Éf·qÙо›z{hÈu‚ܽd¡µÙ4k– …Ø·­a˜’ùÞÉÒÂ…Ém²‹fadj³mWã6"ȈªªïªÞ„A„Ęðäçë<žå.ŸAäíãA´`/ iÉðFÜMÕa§ëœInC;Ì]pÕc˜¿¥U”W"‡ÁA°¦EIv(ÚðæŒôc{Ì[†°ðªŸŠü’î ½ðç/@G#Q/ ?¦xúú‘±ÂþÛ1³[£ºE‚ǧ‡ƒy¯Ø>, ÎG ³Ço¶r Dwö ô"_Òáðô8òºM¶Ø$(_vô~ £%Cl§äÉ"Ðð VBí§ÚÏ _‚н zAÙË>öË,²ú+ôq¿[È&ÁI»#F%ŽA]ŽÝ,ŒH,! Ç^ʲeØä~ ¥©\lii &u_ÙâJnˆ?¤“¬®ð[ŠZ÷‹n£ t—4Ø.’9pØ H0=pcÐÃër6xrVHYYi„OâAÌË<¬ Çè–Äã>ÖÖÀ`/m,7ßYqÂÀº³]Ž:,\ÅŽ›V 8;O¨© ¤÷Ø«vlØ.‹ÿRƒü mm@غ«Ã€˜òbA'‡­¶3ÉdöJ§fÄ_ìY>¦_ÜÖßUW•Ь¶ðði/Á‚mˆî o$‚`‹Aøo?ÇŠè>{=&ûñÝüÞ“xßÅM~ BÝúG¤r@ám<¾/¹×pÙ.ñ h7v|"b56U´ÿ3¤‰ãqœ$–çÒÈ àÌ›9F”‡€C8–tIA ‚È…¦ D@ãa Ò H‚@¾°^¤"J­¥GËöã'rÝŽ­Ÿ»W.‘ìHq&"_’ ˆ«¦µ³‡1ЄF˜Í‰ c  ˜Éu²$‰ç ²Xe‚Tý$þ6 ‚`˜S $·aÉŽÞ]_ Ì`0`0€Ä!*˜ÂGá_¥.F°ÒDãž>„G#¯AU¾­œÚ´!ûš7i)¢Î0‰.Ãé ‘/Ô…S•ØG"àÇÞmY~CÂdÒô€þ“lá⊪pv™ä„ñ«(®.¹÷XxÓ§­Ý»Sm`AÓ—8 YcIúW?rİtÊ^™~yyAÃÂw‡eé‡ßØó²Dº8ú›dÜ.ÎäÂÎþÑJÆMN{T³ººòfÕ×T40e{'Œ¶%¶òZ'ÇIÜàxêïàöšä+E¹b@L…° •Ž¡h±D{# DDb.×`#¡Œ]dÀIIv™·ý04Ñ&=¬!Õ‚·^D“–Ï­Š{‹vIOÓI5¢F>η £Ø]>Ú]&åÓ”dˆ©L lš±»µœµal”²",Ʋ (nZÌæEˆhòxm›Á(Z 1Ô‰ H<z³¸5Y·^01´çG¸ü§Î3¡Uµ9?.÷€ËSHû{äÈòx3RÂmœbMýßRµªßÐú>ÛR¾ƒ‚Ëâíxã¡"θIG‚¥" æèn¢ÞlP;TÂÙ²†å=½Uu¼¹À]GÀ“(ÐWEÄLF8{/I}OJ-ãayoIl$³–ĪUUy6ÞCUé8xé9~·ô²ÁlCá@E|^~˜Ncè°³³ ,- 2 ±ÐÂб„H$,%Svgf–À‘Ak ]ÑÙÑŸdCÂÞÁ— ¶{Â6,šì¤‡ç«D´u$}‰i¥‚Ùv¼i’ÃÀE¾Ûw¬ƒ €ï@%‡¡gêÄÒ#"Æèã<]¶·|,bi`Èl‘~¢¶!ˆ°Ê%XL›¬Í«eÛœppæ²n¦N…¡0b à¿|BP@LÄŽ‚X¤`q’Går±8 [‚«&]¿Î9Jé¤5Pßædª­õu>‹¨“¨‘àQñ ØÁ}?lÇx \æÞøz.Ã‘Ì Çï+Ðoèi»ûåò]ðéKg—²ë„Õv‰tæZÝò±ÖKDŠ2Ï FVé¬1D±1ÁãêÊuâwB‘QHBÏF›|»ì2ÑÊCêD"oŸ^æX‚Ÿ¶M$š²8Η—²K›êN û#t·~*Á´hÃ!&D‘0Áî w¶ã ؾ†í´Zèaä!̲A¤‡bÃ:aŠ""'¢Aïý|<µd=[m!ÙÝ,r0B4e[O¤"Æ —0'Gt·S Ò[5ØÙ£–8¡ØžiБá`/èœ[^^Txe€OTµ ž ƒW@ qPñk&(ž6ñ¹uö+3¶Ç;2ê`œá·ŽÎA¼~8l…‹²±É3ð·yr·we¬@›ºY§ùC©)KLA·‘OB×…åà¡-µ¯,çõÄRn¯ § ÄaðD «€¸“ Hq‹_ï«ì·ðZ{¤héÀHÄ1‘ãÅgá 8²52qÔè±ý„ èx Hdp êl¢ñ³’G%áÇ{ÀÔ<`$ñ,°Ë’b[XÙÌf!ðl¤u’„Ú^« а˜l,R9­ººÿ$’LKt!é´¼H[Qà† +¤YÙé¦Èå¢ÞxB‘÷OrcŒ÷GXâŠð°–™9téÆÛ¼xWlÚg¬î¿Ñh }@ø·ao[õ˜ý.‘i–G]I "\´§è’VÿNNà‹úŠ…Ï­ä*ê„6FƒF¯u(=UÀ’ÇÑ ü”?‚\#à[ðìnòV `]˜ñ©osŽîRÂÃl8a.,¶Ë½7wr¶Ú§!ÞÑûºaã¥Ä&ŸÄp²ó Ö dÓðÆÂÉN'„ “^Þ#FíëbN;vÕг ê]ÕlÓbD7ñ]¼_iÃ:±&±‘„`~-åùŒaatãv1†¢ï=€Èž—löí£® k£i”UK&ØÎ$±¶Œ­`âöãkrr `‹t¶‰ÇN(Èö˜ÝÄ®=™b·ïHѱ]Á4ÉN=0BQáml•‰7Y~~‚e”xÚžÀšEÀ.dãŸlX`F*Á„lù¸wv«/ÖW³u¬ð6¹Ö«xGîP#¢Sa‹v=þkƒ&-œdw¯ ²@a— á‚HPޤD©¡‚ uà5mÏF7½/~Ë-3"žÌt€"1²K K*ÃE,1e’Ð!_dHBÈ ½BMC FÐŒA^·oùF:CÖ7^XZ€é#t·ô•;„ÈSA+’#^Æ^´‰tË‹»láÆXíÞ[× “Ã`Û8ybGa·Ñö‰—Ò8ÌÓY*ªñ™ÀàlúJR%r­n¢ðÓP³YíãêÄÜ¢,.•»ÁxzÛwVxöœàõeÜLÌø»v»9[lä0 `¤PãX×€‚Ð@ˆV;2hÎ|Õ—° Ž€˜lÅàu¥RG ƒiÛÃÐŽ6ߊàÝÓÇpG ZY;6ÏÑxjO†? é’GFe^,°,,³ŽîáÛc;Xúu·¾¦þ¥-›Àëói朅ƒálôî]æîFÍY{;aoçkÒ~î$[T iûg¤U,¹¬:HÁ!HP%¬ke4‡«¥ßPi1 TaSxŒHIQŽ­S5qŽ’5± /†Ú䶈ìv3×b1ººŠÎ°P H®Œ6ˆØ# ²Ä3'Ýø–Ø@C$1 ÖUIBì,¤d–b1”ƒ1 Iº²ÁˆLà Á¥®DYÔ*âYvgÉGÓRÖ0wdsÄ…1ŒvûBðŒ»#è¼i ‹mN³Ü°mž >‹¥Šì˜L=<xgRiðç6Hµƒ„ ð”(„ °"³¸J¾ä9½tœdÁ-[],Y9ß‚HÈðŽ»9ð7e¹ X¢‡Ãhá(•UUû_‚Üà d9xV±fCî"Áãªò£Ò]7…áÞòâ‹ðÛÜ1.ÿ:¬'û6 Pî‹CŸIáàeÐÝ8 FÕiýJ…“Žoj<ì{D=d52ÆÞG-°œ`Ù>$½,F‰»àÃ:k÷é’ϯÃ8¡ÒÙ޲Óá¼}1`„vI’À€à Áf0ÉÀƒU)·c…ª‘!à$ ;;‰!6KHÝ3Çô’ * ‚PQ³0.¡ßeAû6r4à=AÆ6·†7^:/µ‚xìgRÅ-…Ë âÀl$, †îI³Å'³Ð¼:H2ÍñÛ‡Õ­;[ÏüÌ%0çd°Ièr_蟤`6A•<1½úlˆ(ÇØBàß@´"r#†¼`!Ž1à£!¼;˜qœos°ºN£àe¼pTÒ𺠽~*ÇáIjúð º7·Â€ Xß[cn¹…]_ò­°jW‡ó1íÔƒØËyjÂøRØaûã³»x= xj6»ÆFòÍHË4‚d1`H ,U… Å… .²½m2v!EØÃB»®£Ü„»XC¦blàInñxÖ{:HžF1ú„ñ±™Ó ¤aÂt,Æw؇ ?LxIJ™ÃÎw°Ú 숰“o d7¤Eb ›0˹Àü2sï¹–Ée‘6ßæéݳÖðA ”´”ººº°ŒžII.ÇRåÑ@9p[7V7îcNÞÁÀJt4°Ç¥û¯Ù~D±D]øý$3\ã¸8ðbÐ…áygÆp¦ôÛÀ:>;ÏpÂx!£œ»‡`°_ñjÌccÒ—­# Ï¤³NzÕ‹O†¶¡b'œ›&ŒC.‚b„C&…N© ¿†—1au€ôðV­Ôİn›Ka¬ ÒWŽíT¡„­„ #'#T£ÃÄ&FD:]°š?tjO®ìÙŠôÃ/Sà:]/°ïÐmqºÂD€FçR}(XÃYÑ!´äq—l ¼w²±»ÂÃBÒ ul!µ•¾ŽF`›jŠËÆÚÁ0s ²BÂEˆ žYWêÔú…üZyŒ KO6-N0±cðNÙ–¼-fÈÅZ› ðX60ŽXµãPF(Õ¬â~¶céQ›"É?´´‚Î#À‡oaœ=üˆ Ÿ-œY†¤Î7Õõct+¯´ã8ÙìûSø °$z{Ã#F8¥ªñ©Îi’Ç!4!VZ ÐÆ²!yÕŠ•m¦,ÅÒH¼b H†Ý"HsŠ*0‡cÄ`1rï'@ ÌãVúgeR$pnñrìÝsõ›7ÛF"OÀÇ—uÄèÈNÀ ïVK€Øpuèdq½ð°`¬p8Èf}¼\BÓI³,árÓÚ̳3‡Üï³,-z‡Ö›Å¶l{ÀçëáõÂè8ýÁï¦f&xö½p¨ü³» $k‰áÜãÍÝ­¶ðR9Ûä‘t<$Kýp4ÖdðxLs“ö¹kk~ë÷ƾÚáY¸2ãÕŸÄ~‘úÚp\G°ƒ¶\§œ äë#¿Rǧí†õ—Æx…˜~ûÂ^{t ±xm˜mäž¡Œ¡a0ƒAÙdŠê ³Û,$˜±Û!—qܘBH5Ñ+IWu!.Ù\—²AÛ 2çÓjŒ.]ÅÿÙfwupd-2.0.10/contrib/qubes/doc/uefi_capsule_update.md000066400000000000000000000017371501337203100225760ustar00rootroot00000000000000# UEFI capsule update The qubes-fwupd handle the UEFI capsule update under several conditions. The fwupd uses ESRT tables to read GUID, and that causes trouble when the OS is running under a hypervisor. The Xen does not pass the ESRT tables to paravirtualized dom0, so the Qubes is not able to provide sysfs information. More information you can find it this thread: ## Requirements ### Qubes OS You need Qubes R4.1 to use the UEFI capsule update. ### Hardware Make sure that your hardware has available firmware updates in the [LVFS](https://fwupd.org/) ## UEFI capsule update - downgrade UEFI capsule updates and downgrades were tested on DELL XPS 15 9560. ```shell sudo qubes-fwupdmgr downgrade ``` ## UEFI capsule update - update ```shell sudo qubes-fwupdmgr update ``` ## Update process ### Capsule found ![img](img/uefi_capsule_found.jpg) ### ME updated ![img](img/uefi_ME.jpg) ### Success ![img](img/uefi_success.jpg) fwupd-2.0.10/contrib/qubes/doc/whonix.md000066400000000000000000000007441501337203100201010ustar00rootroot00000000000000# Whonix support The qubes-fwupd uses the sys-whonix VM as the update VM to handle downloading updates and metadata via Tor. The tests detect if sys-whonix is running, but do not check if you are connected with Tor. So before running the test make sure that sys-whonix has access to the network. ## Refresh ```shell sudo qubes-fwupdmgr refresh --whonix ``` ## Update ```shell sudo qubes-fwupdmgr update --whonix ``` ## Downgrade ```shell sudo qubes-fwupdmgr downgrade --whonix fwupd-2.0.10/contrib/qubes/meson.build000066400000000000000000000022171501337203100176350ustar00rootroot00000000000000install_data([ 'src/__init__.py', 'src/fwupd_receive_updates.py', 'src/qubes_fwupd_common.py', 'src/qubes_fwupd_heads.py', 'src/qubes_fwupd_update.py', 'src/qubes_fwupdmgr.py', ], install_dir: 'share/qubes-fwupd/src', ) install_data([ 'test/__init__.py', 'test/fwupd_logs.py', 'test/test_qubes_fwupd_heads.py', 'test/test_qubes_fwupdmgr.py', ], install_dir: 'share/qubes-fwupd/test', ) install_data([ 'test/logs/firmware.metainfo.xml', 'test/logs/get_devices.log', 'test/logs/get_updates.log', 'test/logs/help.log', ], install_dir: 'share/qubes-fwupd/test/logs', ) install_data([ 'src/vms/fwupd_common_vm.py', 'src/vms/fwupd_download_updates.py', ], install_dir: 'libexec/qubes-fwupd', install_mode: 'rwxrwxr-x', ) install_data([ 'test/logs/metainfo_name/firmware.metainfo.xml', ], install_dir: 'share/qubes-fwupd/test/logs/metainfo_name', ) install_data( 'test/logs/metainfo_version/firmware.metainfo.xml', install_dir: 'share/qubes-fwupd/test/logs/metainfo_version', ) install_symlink( 'qubes-fwupdmgr', pointing_to: '/usr/share/qubes-fwupd/src/qubes_fwupdmgr.py', install_dir: '/usr/sbin', ) fwupd-2.0.10/contrib/qubes/src/000077500000000000000000000000001501337203100162605ustar00rootroot00000000000000fwupd-2.0.10/contrib/qubes/src/__init__.py000066400000000000000000000000001501337203100203570ustar00rootroot00000000000000fwupd-2.0.10/contrib/qubes/src/fwupd_receive_updates.py000066400000000000000000000271421501337203100232140ustar00rootroot00000000000000#!/usr/bin/env python3 # # The Qubes OS Project, http://www.qubes-os.org # # Copyright 2010 Rafal Wojtczuk # Copyright 2020 Norbert KamiÅ„ski # # SPDX-License-Identifier: LGPL-2.1-or-later # import base64 import hashlib import itertools import os import shutil import struct import subprocess import tempfile from qubes_fwupd_common import create_dirs FWUPD_DOM0_DIR = "/var/cache/fwupd/qubes" FWUPD_DOM0_UPDATES_DIR = os.path.join(FWUPD_DOM0_DIR, "updates") FWUPD_DOM0_UNTRUSTED_DIR = os.path.join(FWUPD_DOM0_UPDATES_DIR, "untrusted") FWUPD_DOM0_METADATA_DIR = os.path.join(FWUPD_DOM0_DIR, "metadata") FWUPD_VM_DIR = "/home/user/.cache/fwupd" FWUPD_VM_UPDATES_DIR = os.path.join(FWUPD_VM_DIR, "updates") FWUPD_VM_METADATA_DIR = os.path.join(FWUPD_VM_DIR, "metadata") FWUPD_PKI = "/etc/pki/fwupd" FWUPD_PKI_PGP = "/etc/pki/fwupd/GPG-KEY-Linux-Vendor-Firmware-Service" FWUPD_DOWNLOAD_PREFIX = "https://fwupd.org/downloads/" HEADS_UPDATES_DIR = "/boot/updates" class FwupdReceiveUpdates: def _check_shasum(self, file_path, sha): """Compares computed SHA256 checksum with `sha` parameter. Keyword arguments: file_path -- absolute path to the file sha -- SHA256 checksum of the file """ with open(file_path, "rb") as f: c_sha = hashlib.sha256(f.read()).hexdigest() if c_sha != sha: self.clean_cache() raise ValueError(f"Computed checksum {c_sha} did NOT match {sha}.") def _jcat_verification(self, file_path, file_directory): """Verifies sha1 and sha256 checksum, GPG signature, and PKCS#7 signature. Keyword argument: file_path -- absolute path to jcat file file_directory -- absolute path to the directory to jcat file location """ assert file_path.startswith("/"), "bad file path {file_path!r}" cmd_jcat = ["jcat-tool", "verify", file_path, "--public-keys", FWUPD_PKI] p = subprocess.Popen( cmd_jcat, cwd=file_directory, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout, __ = p.communicate() verification = stdout.decode("utf-8") print(verification) if p.returncode != 0: self.clean_cache() raise Exception("jcat-tool: Verification failed") def _crc24(self, data): """Calculate CRC-24 Checksum calculation for PGP armored signature. This algorithm isn't available in Python standard library, but it's simple enough to implement here. It doesn't need to be fast, nor side-channel resistant. """ crc = 0xB704CE # crc24_init for b in data: crc ^= b << 16 for i in range(8): crc <<= 1 if crc & 0x1000000: crc ^= 0x1864CFB # crc24_poly return crc def _pgp_parse(self, signature_path): """Verifies if GPG signature is correctly formatted Verify if signature is well formed - sqv will verify if the signature itself correct, but may accept extra data in the signature packets that could later confuse GnuPG used by fwupd. Verify also armor checksum, as sqv doesn't do that. """ # sigparse expects binary format, so decode base64 first with open(signature_path, "rb") as sig: data = sig.read(4096) # arbitrary size limit if sig.read(1) != b"": raise Exception("pgp: signature too big") lines = data.splitlines() # format described in RFC-4880 ch 6 if lines[0:2] != [b"-----BEGIN PGP SIGNATURE-----", b""]: raise Exception("pgp: invalid header") if lines[-1] != b"-----END PGP SIGNATURE-----": raise Exception("pgp: invalid footer") checksum = lines[-2] if checksum[0] != ord("=") or len(checksum) != 5: raise Exception("pgp: invalid checksum format") base64_data = b"".join(lines[2:-2]) data = base64.b64decode(base64_data, validate=True) crc = base64.b64decode(checksum[1:], validate=True) crc = struct.unpack(">I", b"\0" + crc)[0] if crc != self._crc24(data): raise Exception("pgp: invalid checksum") with tempfile.NamedTemporaryFile() as tmp: tmp.write(data) tmp.flush() p = subprocess.Popen(["sigparse", tmp.name], stderr=subprocess.PIPE) _, stderr = p.communicate() if p.returncode != 0: raise Exception("pgp: invalid signature format: " + stderr.decode()) # reconstruct armored data to ensure its canonical form with open(signature_path, "wb") as sig: sig.write(b"-----BEGIN PGP SIGNATURE-----\n") sig.write(b"\n") encoded = iter(base64.b64encode(data)) while line := bytes(itertools.islice(encoded, 76)): sig.write(line + b"\n") sig.write(checksum + b"\n") sig.write(b"-----END PGP SIGNATURE-----\n") def _pgp_verification(self, signature_path, file_path): """Verifies GPG signature. Keyword argument: signature_path -- absolute path to signature file file_path -- absolute path to the signed file location """ assert file_path.startswith("/"), "bad file path {file_path!r}" try: self._pgp_parse(signature_path) except Exception: self.clean_cache() raise cmd_verify = [ "sqv", "--keyring", FWUPD_PKI_PGP, "--", signature_path, file_path, ] p = subprocess.Popen(cmd_verify, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, __ = p.communicate() signature_key = stdout.decode("utf-8") if p.returncode != 0 or not signature_key: self.clean_cache() raise Exception("pgp: Verification failed") def _reconstruct_jcat(self, jcat_path, file_path): """Reconstruct jcat file from verified parts Currently included parts: .asc, .sha256 Hashes are generated locally if missing Arguments: jcat_path - absolute path to the output jcat file file_path - absolute path to the signed file """ # generate missing hashes hashes = ("sha256",) for hash_ext in hashes: hash_fname = f"{file_path}.{hash_ext}" if not os.path.exists(hash_fname): # TODO: switch to hashlib.file_digest (py3.11) with open(file_path, "rb") as f_data: hash_val = hashlib.new(hash_ext, f_data.read()).hexdigest() with open(hash_fname, "w") as f_hash: f_hash.write(hash_val) signatures = ("asc",) file_id = os.path.basename(file_path) for sign_type in signatures + hashes: sign_path = f"{file_path}.{sign_type}" if not os.path.exists(sign_path): raise Exception(f"Missing signature: {sign_path}") jcat_cmd = ["jcat-tool", "import", jcat_path, file_id, sign_path] subprocess.check_call(jcat_cmd) def handle_fw_update(self, updatevm, sha, filename): """Copies firmware update archives from the updateVM. Keyword arguments: updatevm -- update VM name sha -- SHA256 checksum of the firmware update archive filename -- name of the firmware update archive """ create_dirs(FWUPD_DOM0_UPDATES_DIR, FWUPD_DOM0_UNTRUSTED_DIR) with tempfile.TemporaryDirectory(dir=FWUPD_DOM0_UNTRUSTED_DIR) as tmpdir: dom0_firmware_untrusted_path = os.path.join(tmpdir, filename) updatevm_firmware_file_path = os.path.join(FWUPD_VM_UPDATES_DIR, filename) cmd_copy = [ "qvm-run", "--pass-io", "--no-gui", "-q", "-a", "--no-shell", "--", updatevm, "cat", "--", updatevm_firmware_file_path, ] with open(dom0_firmware_untrusted_path, "bx") as untrusted_file: p = subprocess.Popen(cmd_copy, stdout=untrusted_file, shell=False) p.wait() if p.returncode != 0: raise Exception("qvm-run: Copying firmware file failed!!") self._check_shasum(dom0_firmware_untrusted_path, sha) # jcat verification will be done by fwupd itself self.arch_name = filename self.arch_path = os.path.join(FWUPD_DOM0_UPDATES_DIR, filename) shutil.move(dom0_firmware_untrusted_path, self.arch_path) def handle_metadata_update(self, updatevm, metadata_url): """Copies metadata files from the updateVM. Keyword argument: updatevm -- update VM name """ metadata_name = os.path.basename(metadata_url) self.metadata_file = os.path.join(FWUPD_DOM0_METADATA_DIR, metadata_name) self.metadata_file_jcat = self.metadata_file + ".jcat" self.metadata_file_updatevm = os.path.join(FWUPD_VM_METADATA_DIR, metadata_name) create_dirs(FWUPD_DOM0_METADATA_DIR, FWUPD_DOM0_UNTRUSTED_DIR) with tempfile.TemporaryDirectory(dir=FWUPD_DOM0_UNTRUSTED_DIR) as tmpdir: cmd_copy_metadata_file = [ "qvm-run", "--pass-io", "--no-gui", "--no-shell", "--", updatevm, "cat", "--", self.metadata_file_updatevm, ] # TODO: switch to ed25519 once firmware.xml.gz.jcat will have it cmd_copy_metadata_file_signature = [ "qvm-run", "--pass-io", "--no-gui", "--no-shell", "--", updatevm, "cat", "--", self.metadata_file_updatevm + ".asc", ] untrusted_metadata_file = os.path.join(tmpdir, metadata_name) with open(untrusted_metadata_file, "bx") as untrusted_file_1, open( untrusted_metadata_file + ".asc", "bx" ) as untrusted_file_2, subprocess.Popen( cmd_copy_metadata_file, stdout=untrusted_file_1 ) as p, subprocess.Popen( cmd_copy_metadata_file_signature, stdout=untrusted_file_2 ) as q: p.wait() q.wait() if p.returncode != 0: raise Exception("qvm-run: Copying metadata file failed!!") if q.returncode != 0: raise Exception("qvm-run: Copying metadata signature failed!!") self._pgp_verification( untrusted_metadata_file + ".asc", untrusted_metadata_file ) self._reconstruct_jcat( untrusted_metadata_file + ".jcat", untrusted_metadata_file ) # verified, move into trusted dir shutil.move(untrusted_metadata_file, self.metadata_file) shutil.move(untrusted_metadata_file + ".jcat", self.metadata_file_jcat) def clean_cache(self): """Removes updates data""" print("Cleaning dom0 cache directories") if os.path.exists(FWUPD_DOM0_METADATA_DIR): shutil.rmtree(FWUPD_DOM0_METADATA_DIR) if os.path.exists(FWUPD_DOM0_UPDATES_DIR): shutil.rmtree(FWUPD_DOM0_UPDATES_DIR) if os.path.exists(HEADS_UPDATES_DIR): shutil.rmtree(HEADS_UPDATES_DIR) fwupd-2.0.10/contrib/qubes/src/qubes_fwupd_common.py000066400000000000000000000073661501337203100225420ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2023 Marek Marczykowski-Górecki # # SPDX-License-Identifier: LGPL-2.1-or-later import grp import re import os EXIT_CODES = {"ERROR": 1, "SUCCESS": 0, "NOTHING_TO_DO": 2} WARNING_COLOR = "\033[93m" def create_dirs(*args): """Method creates directories. Keyword arguments: *args -- paths to be created """ qubes_gid = grp.getgrnam("qubes").gr_gid old_umask = os.umask(0o002) if args is None: raise Exception("Creating directories failed, no paths given.") for file_path in args: if not os.path.exists(file_path): os.makedirs(file_path) os.chown(file_path, -1, qubes_gid) elif os.stat(file_path).st_gid != qubes_gid: print( f"{WARNING_COLOR}Warning: You should move a personal files" f" from {file_path}. Cleaning cache will cause lose of " f"the personal data!!{WARNING_COLOR}" ) os.umask(old_umask) class LooseVersion: """Version numbering for anarchists and software realists. Implements the standard interface for version number classes as described above. A version number consists of a series of numbers, separated by either periods or strings of letters. When comparing version numbers, the numeric components will be compared numerically, and the alphabetic components lexically. The following are all valid version numbers, in no particular order: 1.5.1 1.5.2b2 161 3.10a 8.02 3.4j 1996.07.12 3.2.pl0 3.1.1.6 2g6 11g 0.960923 2.2beta29 1.13++ 5.5.kw 2.0b1pl0 In fact, there is no such thing as an invalid version number under this scheme; the rules for comparison are simple and predictable, but may not always give the results you want (for some definition of "want"). """ component_re = re.compile(r"(\d+ | [a-z]+ | \.)", re.VERBOSE) def __init__(self, vstring=None): if vstring: self.parse(vstring) def parse(self, vstring): # I've given up on thinking I can reconstruct the version string # from the parsed tuple -- so I just store the string here for # use by __str__ self.vstring = vstring components = [x for x in self.component_re.split(vstring) if x and x != "."] for i, obj in enumerate(components): try: components[i] = int(obj) except ValueError: pass self.version = components def __str__(self): return self.vstring def __repr__(self): return f"LooseVersion ('{str(self)}')" def _cmp(self, other): if isinstance(other, str): other = LooseVersion(other) elif not isinstance(other, LooseVersion): return NotImplemented if self.version == other.version: return 0 if self.version < other.version: return -1 if self.version > other.version: return 1 def __eq__(self, other): c = self._cmp(other) if c is NotImplemented: return c return c == 0 def __lt__(self, other): c = self._cmp(other) if c is NotImplemented: return c return c < 0 def __le__(self, other): c = self._cmp(other) if c is NotImplemented: return c return c <= 0 def __gt__(self, other): c = self._cmp(other) if c is NotImplemented: return c return c > 0 def __ge__(self, other): c = self._cmp(other) if c is NotImplemented: return c return c >= 0 fwupd-2.0.10/contrib/qubes/src/qubes_fwupd_heads.py000066400000000000000000000122361501337203100223260ustar00rootroot00000000000000#!/usr/bin/env python3 # # The Qubes OS Project, http://www.qubes-os.org # # Copyright 2021 Norbert KamiÅ„ski # # SPDX-License-Identifier: LGPL-2.1-or-later # import subprocess import tempfile import os import shutil import xml.etree.ElementTree as ET from qubes_fwupd_common import EXIT_CODES, create_dirs, LooseVersion FWUPDTOOL = "/bin/fwupdtool" BOOT = "/boot" HEADS_UPDATES_DIR = os.path.join(BOOT, "updates") class FwupdHeads: def _get_hwids(self): cmd_hwids = [FWUPDTOOL, "hwids"] p = subprocess.Popen(cmd_hwids, stdout=subprocess.PIPE) self.dom0_hwids_info = p.communicate()[0].decode() if p.returncode != 0: raise Exception("fwupd-qubes: Getting hwids info failed") def _gather_firmware_version(self): """ Checks if Qubes works under heads """ if "Heads" in self.dom0_hwids_info: self.heads_version = None hwids = self.dom0_hwids_info.split("\n") for line in hwids: if "Heads" in line: self.heads_version = line.split("Heads-v")[1] else: print("Device is not running under the heads firmware!!") print("Exiting...") return EXIT_CODES["NOTHING_TO_DO"] def _get_hwid_device(self): """ Device model for Heads update, currently supports ThinkPad only. """ for line in self.dom0_hwids_info.splitlines(): if line.startswith("Family: ThinkPad"): return line.split(":", 1)[1].split(" ", 1)[1].lower() return None def _parse_metadata(self, metadata_file): """ Parse metadata info. """ metadata_ext = os.path.splitext(metadata_file)[-1] if metadata_ext == ".xz": cmd_metadata = ["xzcat", metadata_file] elif metadata_ext == ".gz": cmd_metadata = ["zcat", metadata_file] else: raise NotImplementedError( "Unsupported metadata compression " + metadata_ext ) p = subprocess.Popen(cmd_metadata, stdout=subprocess.PIPE) self.metadata_info = p.communicate()[0].decode() if p.returncode != 0: raise Exception("fwupd-qubes: Parsing metadata failed") def _parse_heads_updates(self, device): """ Parses heads updates info. Keyword arguments: device -- Model of the updated device """ self.heads_update_url = None self.heads_update_sha = None self.heads_update_version = None heads_metadata_info = None root = ET.fromstring(self.metadata_info) for component in root.findall("component"): if f"heads.{device}" in component.find("id").text: heads_metadata_info = component if not heads_metadata_info: print("No metadata info for chosen board") return EXIT_CODES["NOTHING_TO_DO"] for release in heads_metadata_info.find("releases").findall("release"): release_ver = release.get("version") if self.heads_version == "heads" or LooseVersion( release_ver ) > LooseVersion(self.heads_version): if not self.heads_update_version or LooseVersion( release_ver ) > LooseVersion(self.heads_update_version): self.heads_update_url = release.find("location").text for sha in release.findall("checksum"): if ( ".cab" in sha.attrib["filename"] and sha.attrib["type"] == "sha256" ): self.heads_update_sha = sha.text self.heads_update_version = release_ver if self.heads_update_url: return EXIT_CODES["SUCCESS"] else: print("Heads firmware is up to date.") return EXIT_CODES["NOTHING_TO_DO"] def _copy_heads_firmware(self, arch_path): """ Copies heads update to the boot path """ heads_boot_path = os.path.join(HEADS_UPDATES_DIR, self.heads_update_version) heads_update_path = os.path.join(heads_boot_path, "firmware.rom") create_dirs(HEADS_UPDATES_DIR) if os.path.exists(heads_update_path): print(f"Heads Update == {self.heads_update_version} " "already downloaded.") return EXIT_CODES["NOTHING_TO_DO"] else: os.mkdir(heads_boot_path) with tempfile.TemporaryDirectory() as tmpdir: cmd_extract = ["gcab", "-x", f"--directory={tmpdir}", "--", arch_path] p = subprocess.Popen(cmd_extract, stdout=subprocess.PIPE) p.communicate() if p.returncode != 0: raise Exception(f"gcab: Error while extracting {arch_path}.") update_path = os.path.join(tmpdir, "firmware.rom") shutil.copyfile(update_path, heads_update_path) print( f"Heads Update == {self.heads_update_version} " f"available at {heads_boot_path}" ) return EXIT_CODES["SUCCESS"] fwupd-2.0.10/contrib/qubes/src/qubes_fwupd_update.py000066400000000000000000000101641501337203100225220ustar00rootroot00000000000000#!/usr/bin/env python3 # # The Qubes OS Project, http://www.qubes-os.org # # Copyright 2021 Norbert Kaminski # # SPDX-License-Identifier: LGPL-2.1-or-later # import os import re import subprocess from qubes_fwupd_common import create_dirs FWUPD_DOM0_DIR = "/var/cache/fwupd/qubes" FWUPD_VM_DOWNLOAD = "/usr/libexec/qubes-fwupd/fwupd_download_updates.py" FWUPD_DOM0_UPDATES_DIR = os.path.join(FWUPD_DOM0_DIR, "updates") FWUPD_DOWNLOAD_PREFIX = "https://fwupd.org/downloads/" SPECIAL_CHAR_REGEX = re.compile(r"%20|&|\||#") UPDATEVM_REGEX = re.compile(r"^sys-") run_cmd = ( "qvm-run", "--pass-io", "--no-gui", "--no-shell", "-q", "-a", "--filter-escape-chars", "--color-output=31", "--color-stderr=31", "--", ) def run_in_tty(updatevm, args, **kwargs): return subprocess.check_call( ( *run_cmd, updatevm, *args, ), stdin=subprocess.DEVNULL, **kwargs, ) class FwupdUpdate: def _specify_updatevm(self): cmd_updatevm = ["qubes-prefs", "--force-root", "updatevm"] p = subprocess.Popen(cmd_updatevm, stdout=subprocess.PIPE) self.updatevm = p.communicate()[0].decode().split("\n")[0] if p.returncode != 0 and not UPDATEVM_REGEX.match(self.updatevm): self.updatevm = None raise Exception("Specifying updatevm failed") def _check_updatevm(self): """Checks if updatevm is running""" cmd_xl_list = ["xl", "list", "--", self.updatevm] p = subprocess.Popen( cmd_xl_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) output = p.communicate()[0].decode() return p.returncode == 0 def download_metadata(self, whonix=False, metadata_url=None): """Initialize downloading metadata files. Keywords arguments: whonix -- Flag enforces downloading the metadata updates via Tor metadata_url -- Download metadata from the custom url """ if not whonix: self._specify_updatevm() else: self.updatevm = "sys-whonix" if not self._check_updatevm(): raise Exception(f"{self.updatevm} is not running!!") if not os.path.exists(FWUPD_DOM0_DIR): create_dirs(FWUPD_DOM0_DIR) cmd_metadata = [FWUPD_VM_DOWNLOAD, "--metadata"] if metadata_url: cmd_metadata.append("--url=" + metadata_url) try: run_in_tty(self.updatevm, cmd_metadata) except subprocess.CalledProcessError: raise Exception("Metadata download failed.") def download_firmware_updates(self, url, sha, whonix=False): """Initializes downloading firmware update archive. Keywords arguments: url -- url path to the firmware update archive sha -- SHA256 checksum of the firmware update archive whonix -- Flag enforces downloading the updates via Tor """ if not whonix: self._specify_updatevm() else: self.updatevm = "sys-whonix" if not self._check_updatevm(): raise Exception(f"{self.updatevm} is not running!!") if not os.path.exists(FWUPD_DOM0_DIR): create_dirs(FWUPD_DOM0_DIR) self.arch_name = os.path.basename(url) self.arch_path = os.path.join(FWUPD_DOM0_UPDATES_DIR, self.arch_name) if not os.path.exists(self.arch_path): cmd_firmware_download = [ "qvm-run", "--pass-io", "--quiet", "--autostart", "--no-shell", "--color-output=31", "--color-stderr=31", "--", self.updatevm, FWUPD_VM_DOWNLOAD, f"--url={url}", f"--sha={sha}", ] p = subprocess.Popen(cmd_firmware_download, stdin=subprocess.DEVNULL) p.wait() if p.returncode != 0: raise Exception("Firmware download failed.") else: self.cached = True print("Firmware already downloaded. Using cached files.") fwupd-2.0.10/contrib/qubes/src/qubes_fwupdmgr.py000077500000000000000000000660411501337203100216760ustar00rootroot00000000000000#!/usr/bin/env python3 # # The Qubes OS Project, http://www.qubes-os.org # # Copyright 2021 Norbert Kaminski # # SPDX-License-Identifier: LGPL-2.1-or-later # import json import os import shutil import subprocess import tempfile import sys import xml.etree.ElementTree as ET from pathlib import Path from packaging import version as pversion FWUPD_QUBES_DIR = "/usr/share/qubes-fwupd" # Check if script is run by tests and append sys path properly if __name__ == "__main__": sys.path.append(os.path.join(FWUPD_QUBES_DIR, "src")) else: sys.path.append("./src") try: from qubes_fwupd_heads import FwupdHeads from qubes_fwupd_update import FwupdUpdate, run_in_tty from fwupd_receive_updates import FwupdReceiveUpdates from qubes_fwupd_common import EXIT_CODES, create_dirs except ModuleNotFoundError: raise ModuleNotFoundError( "qubes-fwupd modules not found. You may need to reinstall package." ) FWUPD_DOM0_DIR = "/var/cache/fwupd/qubes" FWUPD_DOM0_METADATA_DIR = os.path.join(FWUPD_DOM0_DIR, "metadata") FWUPD_DOM0_UPDATES_DIR = os.path.join(FWUPD_DOM0_DIR, "updates") FWUPD_DOWNLOAD_PREFIX = "https://fwupd.org/downloads/" METADATA_URL = "https://fwupd.org/downloads/firmware.xml.xz" METADATA_URL_JCAT = "https://fwupd.org/downloads/firmware.xml.xz.jcat" FWUPDMGR = "/bin/fwupdmgr" BIOS_UPDATE_FLAG = os.path.join(FWUPD_DOM0_DIR, "bios_update") LVFS_TESTING_DOM0_FLAG = os.path.join(FWUPD_DOM0_DIR, "lvfs_testing") HELP = { "Usage": [ { "Command": "qubes-fwupdmgr [OPTION…][FLAG..]", "Example": "qubes-fwupdmgr refresh --whonix --url=\n", } ], "Options": [ { "get-devices": "Get all devices that support firmware updates", "get-updates": "Get the list of updates for connected hardware", "refresh": "Refresh metadata from remote server", "update": "Update chosen device to latest firmware version", "update-heads": "Updates heads firmware to the latest version (EXPERIMENTAL, not fully supported yet)", "downgrade": "Downgrade chosen device to chosen firmware version", "clean": "Delete all cached update files\n", } ], "Flags": [ { "--whonix": "Download firmware updates via Tor", "--device": "Specify device for heads update (default - x230)", "--url": "Address of the custom metadata remote server\n", } ], "Help": [{"-h --help": "Show help options\n"}], } class QubesFwupdmgr(FwupdHeads, FwupdUpdate, FwupdReceiveUpdates): def _download_metadata(self, whonix=False, metadata_url=None): """Initialize downloading metadata files. Keywords arguments: whonix -- Flag enforces downloading the metadata updates via Tor metadata_url -- Download metadata from the custom url """ if not metadata_url: raise Exception("missing metadata URL") self.download_metadata(whonix=whonix, metadata_url=metadata_url) self.handle_metadata_update(self.updatevm, metadata_url=metadata_url) if not os.path.exists(self.metadata_file): raise FileNotFoundError("Metadata file does not exist") def get_remotes(self): """Get metadata URLs for all enabled remotes""" if hasattr(self, "_remotes_cache"): return self._remotes_cache remotes_json = subprocess.check_output( [ FWUPDMGR, "get-remotes", "--json", ] ).decode() remotes_list = json.loads(remotes_json)["Remotes"] remotes = {} for remote in remotes_list: name = remote["Id"] # skip disabled if remote.get("Enabled", "true") != "true": continue # skip local - for metadata refresh, we only care about those # actually needing refreshing if remote.get("Kind") != "download": continue assert "MetadataUri" in remote remotes[name] = remote["MetadataUri"] self._remotes_cache = remotes return self._remotes_cache def refresh_metadata(self, whonix=False, metadata_url=None, remote_name=None): """Updates metadata with downloaded files. Keyword arguments: whonix -- Flag enforces downloading the metadata updates via Tor metadata_url -- Use custom metadata from the url remote_name -- Set refreshed metadata to this remote """ if not metadata_url: if remote_name: metadata_url = self.get_remotes()[remote_name] else: raise Exception("missing metadata URL") metadata_name = os.path.basename(metadata_url) self.metadata_file = os.path.join(FWUPD_DOM0_METADATA_DIR, metadata_name) self.metadata_file_jcat = self.metadata_file + ".jcat" if not remote_name: if "testing" in metadata_url: remote_name = "lvfs-testing" else: remote_name = "lvfs" self._download_metadata(whonix=whonix, metadata_url=metadata_url) cmd_refresh = [ FWUPDMGR, "refresh", self.metadata_file, self.metadata_file_jcat, remote_name, ] p = subprocess.Popen(cmd_refresh, stdout=subprocess.PIPE) output = p.communicate()[0].decode() print(output) if p.returncode != 0: raise Exception("fwupd-qubes: Refresh failed") if not output != "Successfully refreshed metadata manually": raise Exception("Manual metadata refresh failed!!!") def refresh_metadata_all(self, whonix=False): """Refresh metadata for all 'download' remotes Keyword arguments: whonix -- Flag enforces downloading the metadata updates via Tor """ for name, url in self.get_remotes().items(): try: self.refresh_metadata(whonix=whonix, remote_name=name, metadata_url=url) except Exception as e: print(f"Failed to refresh remote '{name}': {e}") def _get_dom0_updates(self): """Gathers information about available updates.""" cmd_get_dom0_updates = [FWUPDMGR, "--json", "get-updates"] p = subprocess.Popen(cmd_get_dom0_updates, stdout=subprocess.PIPE) self.dom0_updates_info = p.communicate()[0].decode() if p.returncode != 0 and p.returncode != 2: raise Exception("fwupd-qubes: Getting available updates failed") def _parse_dom0_updates_info(self, updates_info): """Creates dictionary and list with information about updates. Keywords argument: updates_info - gathered update information """ self.dom0_updates_info_dict = json.loads(updates_info) self.dom0_updates_list = [ { "Name": device["Name"], "Version": device["Version"], "Releases": [ { "Version": update["Version"], "Url": update["Uri"], "Checksum": update["Checksum"][-1], "Description": update["Description"], } for update in device["Releases"] ], } for device in self.dom0_updates_info_dict["Devices"] ] def _download_firmware_updates(self, url, sha, whonix=False): """Initializes downloading firmware update archive. Keywords arguments: url -- url path to the firmware update archive sha -- SHA256 checksum of the firmware update archive whonix -- Flag enforces downloading the updates via Tor """ self.cached = False self.download_firmware_updates(url, sha, whonix=whonix) if not self.cached: self.handle_fw_update(self.updatevm, sha, self.arch_name) if not os.path.exists(self.arch_path): raise FileNotFoundError("Firmware update files do not exist") def _user_input(self, updates_list, downgrade=False): """UI for update process. Keywords arguments: updates_dict - list of updates for specified device downgrade -- downgrade flag """ decorator = "======================================================" if len(updates_list) == 0: print("No updates available.") return -EXIT_CODES["NOTHING_TO_DO"] if downgrade: print("Available downgrades:") else: print("Available updates:") self._updates_crawler(updates_list) while True: try: print("If you want to abandon process press 'N'.") choice = input("Otherwise choose a device number: ") if choice == "N" or choice == "n": return -EXIT_CODES["NOTHING_TO_DO"] device_num = int(choice) - 1 if 0 <= device_num < len(updates_list): if not downgrade: return device_num break else: raise ValueError() except ValueError: print("Invalid choice.") if downgrade: while True: try: releases = updates_list[device_num]["Releases"] for i, fw_dngd in enumerate(releases): print(decorator) print( f" {i+1}. Firmware downgrade version:" f"\t {fw_dngd['Version']}" ) description = fw_dngd["Description"].replace("

", "") description = description.replace("

  • ", "") description = description.replace("
      ", "") description = description.replace("
    ", "") description = description.replace("

    ", "\n ") description = description.replace("
  • ", "\n ") print(f" Description:{description}") print("If you want to abandon downgrade process press N.") choice = input("Otherwise choose downgrade number: ") if choice == "N" or choice == "n": return -EXIT_CODES["NOTHING_TO_DO"] downgrade_num = int(choice) - 1 if 0 <= downgrade_num < len(releases): return device_num, downgrade_num else: raise ValueError() except ValueError: print("Invalid choice.") def _parse_parameters(self, updates_list, choice): """Parses device name, url, version and SHA256 checksum of the file list. Keywords arguments: updates_list - list of updates for dom0 choice -- number of device to be updated """ self.name = updates_list[choice]["Name"] self.version = updates_list[choice]["Releases"][0]["Version"] for ver_check in updates_list[choice]["Releases"]: if pversion.parse(ver_check["Version"]) >= pversion.parse(self.version): self.version = ver_check["Version"] self.url = ver_check["Url"] self.sha = ver_check["Checksum"] def _install_dom0_firmware_update(self, arch_path): """Installs firmware update for specified device in dom0. Keywords arguments: arch_path - absolute path to firmware update archive """ cmd_install = [FWUPDMGR, "install", arch_path] p = subprocess.Popen(cmd_install) p.wait() if p.returncode != 0: raise Exception("fwupd-qubes: Firmware update failed") def _read_dmi(self): """Reads BIOS information from DMI.""" cmd_dmidecode_version = ["dmidecode", "-s", "bios-version"] p = subprocess.Popen(cmd_dmidecode_version, stdout=subprocess.PIPE) p.wait() self.dmi_version = p.communicate()[0].decode() cmd_dmidecode = ["dmidecode", "-t", "bios"] p = subprocess.Popen(cmd_dmidecode, stdout=subprocess.PIPE) p.wait() if p.returncode != 0: raise Exception("dmidecode: Reading DMI failed") return p.communicate()[0].decode() def _verify_dmi(self, arch_path, version, downgrade=False): """Verifies DMI tables for BIOS updates. Keywords arguments: arch_path -- absolute path of the update archive version -- version of the update downgrade -- downgrade flag """ dmi_info = self._read_dmi() with tempfile.TemporaryDirectory() as tmpdir: cmd_extract = ["gcab", "-x", f"--directory={tmpdir}", "--", arch_path] p = subprocess.Popen(cmd_extract, stdout=subprocess.PIPE) p.communicate() if p.returncode != 0: raise Exception(f"gcab: Error while extracting {arch_path}.") path_metainfo = os.path.join(tmpdir, "firmware.metainfo.xml") tree = ET.parse(path_metainfo) root = tree.getroot() vendor = root.find("developer_name").text if vendor is None: raise ValueError("No vendor information in firmware metainfo.") if vendor not in dmi_info: raise ValueError("Wrong firmware provider.") if not downgrade and pversion.parse(version) <= pversion.parse( self.dmi_version ): raise ValueError(f"{version} < {self.dmi_version} Downgrade not allowed") def _get_dom0_devices(self): """Gathers information about devices connected in dom0.""" cmd_get_dom0_devices = [FWUPDMGR, "--json", "get-devices"] p = subprocess.Popen(cmd_get_dom0_devices, stdout=subprocess.PIPE) self.dom0_devices_info = p.communicate()[0].decode() if p.returncode != 0: raise Exception("fwupd-qubes: Getting devices info failed") def update_firmware(self, whonix=False): """Updates firmware of the specified device. Keyword arguments: whonix -- Flag enforces downloading the metadata updates via Tor """ self._get_dom0_updates() self._parse_dom0_updates_info(self.dom0_updates_info) updates_list = self.dom0_updates_list ret_input = self._user_input(updates_list) if ret_input == -EXIT_CODES["NOTHING_TO_DO"]: exit(EXIT_CODES["NOTHING_TO_DO"]) choice = ret_input self._parse_parameters(updates_list, choice) self._download_firmware_updates(self.url, self.sha, whonix=whonix) if self.name == "System Firmware": Path(BIOS_UPDATE_FLAG).touch(mode=0o644, exist_ok=True) self._verify_dmi(self.arch_path, self.version) self._install_dom0_firmware_update(self.arch_path) def _parse_downgrades(self, device_list): """Parses information about possible downgrades. Keywords argument: device_list -- list of connected devices """ downgrades = [] if "No detected devices" in device_list: return downgrades dom0_devices_info_dict = json.loads(device_list) for device in dom0_devices_info_dict["Devices"]: if "Releases" in device: try: version = device["Version"] except KeyError: continue downgrades.append( { "Name": device["Name"], "Version": device["Version"], "Releases": [ { "Version": downgrade["Version"], "Description": downgrade["Description"], "Url": downgrade["Uri"], "Checksum": downgrade["Checksum"][-1], } for downgrade in device["Releases"] if pversion.parse(downgrade["Version"]) < pversion.parse(version) ], } ) return downgrades def _install_dom0_firmware_downgrade(self, arch_path): """Installs firmware downgrade for specified device. Keywords arguments: arch_path - absolute path to firmware downgrade archive """ cmd_install = [FWUPDMGR, "--allow-older", "install", arch_path] p = subprocess.Popen(cmd_install) p.wait() if p.returncode != 0: raise Exception("fwupd-qubes: Firmware downgrade failed") def downgrade_firmware(self, whonix=False): """Downgrades firmware of the specified device. Keyword arguments: whonix -- Flag enforces downloading the metadata updates via Tor """ self._get_dom0_devices() dom0_downgrades = self._parse_downgrades(self.dom0_devices_info) ret_input = self._user_input(dom0_downgrades, downgrade=True) if ret_input == -EXIT_CODES["NOTHING_TO_DO"]: exit(EXIT_CODES["NOTHING_TO_DO"]) device_choice, downgrade_choice = ret_input downgrade = dom0_downgrades[device_choice] releases = downgrade["Releases"] downgrade_url = releases[downgrade_choice]["Url"] downgrade_sha = releases[downgrade_choice]["Checksum"] self._download_firmware_updates(downgrade_url, downgrade_sha, whonix=whonix) if downgrade["Name"] == "System Firmware": Path(BIOS_UPDATE_FLAG).touch(mode=0o644, exist_ok=True) self._verify_dmi( self.arch_path, downgrade["Version"], downgrade=True, ) self._install_dom0_firmware_downgrade(self.arch_path) def _output_crawler(self, updev_dict, level, help_f=False): """Prints device and updates information as a tree. Keywords arguments: updev_dict -- update/device information dictionary level -- level of the tree """ def _tabs(key_word): return key_word + "\t" * (4 - int(len(key_word) / 8)) decorator = "===================================" print(2 * decorator) for updev_key in updev_dict: style = "\t" * level output = style + _tabs(updev_key + ":") if len(updev_key) > 12: continue if updev_key == "Icons": continue if updev_key == "Releases": continue if updev_key == "Name": print(style + updev_dict["Name"]) print(2 * decorator) continue if isinstance(updev_dict[updev_key], str): print(output + updev_dict[updev_key]) elif isinstance(updev_dict[updev_key], int): print(output + str(updev_dict[updev_key])) elif isinstance(updev_dict[updev_key][0], str): for i, data in enumerate(updev_dict[updev_key]): if i == 0: print(output + "\u00B7" + data) continue print(style + _tabs(" ") + "\u00B7" + data) elif isinstance(updev_dict[updev_key][0], dict): if level == 0 and help_f is True: print(output) else: if level == 0: print(f"Dom0 {output}") for nested_dict in updev_dict[updev_key]: self._output_crawler(nested_dict, level + 1) def _updates_crawler(self, updates_list, prefix=0): """Prints updates information for dom0 Keywords arguments: updates_list -- list of devices updates prefix -- device number prefix """ available_updates = False decorator = "======================================================" print(decorator) print("Dom0 updates:") print(decorator) if len(updates_list) == 0: print("No updates available.") return EXIT_CODES["NOTHING_TO_DO"] else: for i, device in enumerate(updates_list): if len(device["Releases"]) == 0: continue if not available_updates: print("Available updates:") print(decorator) print("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^") print(f"{i+1+prefix}. Device: {device['Name']}") print(f" Current firmware version:\t {device['Version']}") for update in device["Releases"]: print(decorator) print(" Firmware update " f"version:\t {update['Version']}") print(f" URL:\t {update['Url']}") print(f" SHA256 checksum:\t {update['Checksum']}") description = update["Description"].replace("

    ", "") description = description.replace("

  • ", "") description = description.replace("
      ", "") description = description.replace("
    ", "") description = description.replace("

    ", "\n\t") description = description.replace("
  • ", "\n\t") print(f" Description: {description}") print(decorator) available_updates = True if not available_updates: print("No updates available.") return EXIT_CODES["NOTHING_TO_DO"] def get_devices_qubes(self): """Gathers and prints devices information.""" self._get_dom0_devices() dom0_devices_info_dict = json.loads(self.dom0_devices_info) self._output_crawler(dom0_devices_info_dict, 0) def get_updates_qubes(self): """Gathers and prints updates information.""" self._get_dom0_updates() self._parse_dom0_updates_info(self.dom0_updates_info) self._updates_crawler(self.dom0_updates_list) def help(self): """Prints help information""" self._output_crawler(HELP, 0, help_f=True) def trusted_cleanup(self): """Deletes trusted directory.""" trusted_path = os.path.join(FWUPD_DOM0_UPDATES_DIR, "trusted.cab") if os.path.exists(trusted_path): os.remove(trusted_path) shutil.rmtree(trusted_path.replace(".cab", "")) def refresh_metadata_after_bios_update(self): """Refreshes metadata after bios update""" if os.path.exists(BIOS_UPDATE_FLAG): print("BIOS was updated. Refreshing metadata...") if "--whonix" in sys.argv: self.refresh_metadata_all(whonix=True) else: self.refresh_metadata_all() os.remove(BIOS_UPDATE_FLAG) def heads_update(self, device=None, whonix=False, metadata_url=None): """ Updates heads firmware Keyword arguments: device -- Model of the updated device whonix -- Flag enforces downloading the metadata updates via Tor metadata_url -- Use custom metadata from the url """ if not metadata_url: metadata_url = METADATA_URL metadata_name = os.path.basename(metadata_url) self.metadata_file = os.path.join(FWUPD_DOM0_METADATA_DIR, metadata_name) self._get_hwids() if device is None: device = self._get_hwid_device() self._download_metadata(whonix=whonix, metadata_url=metadata_url) self._parse_metadata(self.metadata_file) if self._gather_firmware_version() == EXIT_CODES["NOTHING_TO_DO"]: return EXIT_CODES["NOTHING_TO_DO"] if self._parse_heads_updates(device) == EXIT_CODES["NOTHING_TO_DO"]: return EXIT_CODES["NOTHING_TO_DO"] self._download_firmware_updates(self.heads_update_url, self.heads_update_sha) return_code = self._copy_heads_firmware(self.arch_path) if return_code == EXIT_CODES["NOTHING_TO_DO"]: exit(EXIT_CODES["NOTHING_TO_DO"]) elif return_code == EXIT_CODES["SUCCESS"]: print() while True: try: print("An update requires a reboot to complete.") choice = input("Do you want to restart now? (Y|N)\n") if choice == "N" or choice == "n": return EXIT_CODES["SUCCESS"] elif choice == "Y" or choice == "y": print("Rebooting...") os.system("reboot") else: raise ValueError() except ValueError: print("Invalid choice.") else: raise Exception("Copying heads update failed!!") def validate_dom0_dirs(self): """Validates and creates directories""" if not os.path.exists(FWUPD_DOM0_DIR): create_dirs(FWUPD_DOM0_DIR) if os.path.exists(FWUPD_DOM0_METADATA_DIR): shutil.rmtree(FWUPD_DOM0_METADATA_DIR) create_dirs(FWUPD_DOM0_METADATA_DIR) else: create_dirs(FWUPD_DOM0_METADATA_DIR) if not os.path.exists(FWUPD_DOM0_UPDATES_DIR): create_dirs(FWUPD_DOM0_UPDATES_DIR) def main(): if os.geteuid() != 0: print("You need to have root privileges to run this script.\n") exit(EXIT_CODES["ERROR"]) q = QubesFwupdmgr() if len(sys.argv) < 2: q.help() exit(1) metadata_url = None device_override = None whonix = False for arg in sys.argv: if "--url=" in arg: metadata_url = arg.replace("--url=", "") if FWUPD_DOWNLOAD_PREFIX not in metadata_url: print( "Metadata must be stored in the Linux" " Vendor Firmware Service (https://fwupd.org/)" ) print("Exiting...") exit(1) if "--device=" in arg: device_override = arg.replace("--device=", "") if "--whonix" == arg: whonix = True q.validate_dom0_dirs() q.trusted_cleanup() q.refresh_metadata_after_bios_update() if not os.path.exists(FWUPD_DOM0_DIR): if metadata_url: q.refresh_metadata(whonix=whonix, metadata_url=metadata_url) else: q.refresh_metadata_all(whonix=whonix) if sys.argv[1] == "get-updates": q.get_updates_qubes() elif sys.argv[1] == "get-devices": q.get_devices_qubes() elif sys.argv[1] == "update": q.update_firmware(whonix=whonix) elif sys.argv[1] == "downgrade": q.downgrade_firmware(whonix=whonix) elif sys.argv[1] == "clean": q.clean_cache() elif sys.argv[1] == "refresh": if metadata_url: q.refresh_metadata(whonix=whonix, metadata_url=metadata_url) else: q.refresh_metadata_all(whonix=whonix) elif sys.argv[1] == "update-heads": q.heads_update(device=device_override, metadata_url=metadata_url, whonix=whonix) else: q.help() exit(1) if __name__ == "__main__": main() fwupd-2.0.10/contrib/qubes/src/vms/000077500000000000000000000000001501337203100170655ustar00rootroot00000000000000fwupd-2.0.10/contrib/qubes/src/vms/fwupd_common_vm.py000066400000000000000000000065711501337203100226470ustar00rootroot00000000000000#!/usr/bin/env python3 # # The Qubes OS Project, http://www.qubes-os.org # # Copyright 2021 Norbert KamiÅ„ski # # SPDX-License-Identifier: LGPL-2.1-or-later # import grp import hashlib import os import shutil import subprocess FWUPD_VM_DIR = "/home/user/.cache/fwupd" FWUPD_VM_UPDATES_DIR = os.path.join(FWUPD_VM_DIR, "updates") FWUPD_VM_METADATA_DIR = os.path.join(FWUPD_VM_DIR, "metadata") WARNING_COLOR = "\033[93m" FWUPD_PKI = "/etc/pki/fwupd" class FwupdVmCommon: def _create_dirs(self, *args): """Method creates directories. Keyword arguments: *args -- paths to be created """ qubes_gid = grp.getgrnam("qubes").gr_gid self.old_umask = os.umask(0o002) if args is None: raise Exception("Creating directories failed, no paths given.") for file_path in args: if not os.path.exists(file_path): os.makedirs(file_path) os.chown(file_path, -1, qubes_gid) elif os.stat(file_path).st_gid != qubes_gid: print( f"{WARNING_COLOR}Warning: You should move a personal files" f" from {file_path}. Cleaning cache will cause lose of " f"the personal data!!{WARNING_COLOR}" ) def check_shasum(self, file_path, sha): """Compares computed SHA256 checksum with `sha` parameter. Keyword arguments: file_path -- absolute path to the file sha -- SHA256 checksum of the file """ with open(file_path, "rb") as f: c_sha = hashlib.sha256(f.read()).hexdigest() if c_sha != sha: self.clean_vm_cache() raise ValueError(f"Computed checksum {c_sha} did NOT match {sha}. ") def validate_vm_dirs(self): """Validates and creates directories""" print("Validating directories") if not os.path.exists(FWUPD_VM_DIR): self._create_dirs(FWUPD_VM_DIR) if os.path.exists(FWUPD_VM_METADATA_DIR): shutil.rmtree(FWUPD_VM_METADATA_DIR) self._create_dirs(FWUPD_VM_METADATA_DIR) else: self._create_dirs(FWUPD_VM_METADATA_DIR) if not os.path.exists(FWUPD_VM_UPDATES_DIR): self._create_dirs(FWUPD_VM_UPDATES_DIR) os.umask(self.old_umask) def _jcat_verification(self, file_path, file_directory): """Verifies sha1 and sha256 checksum, GPG signature, and PKCS#7 signature. Keyword argument: file_path -- absolute path to jcat file file_directory -- absolute path to the directory to jcat file location """ cmd_jcat = ["jcat-tool", "verify", f"{file_path}", "--public-keys", FWUPD_PKI] p = subprocess.Popen( cmd_jcat, cwd=file_directory, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout, __ = p.communicate() verification = stdout.decode("utf-8") print(verification) if p.returncode != 0: self.clean_vm_cache() raise Exception("jcat-tool: Verification failed") def clean_vm_cache(self): """Removes updates data""" print("Cleaning cache directories") if os.path.exists(FWUPD_VM_METADATA_DIR): shutil.rmtree(FWUPD_VM_METADATA_DIR) if os.path.exists(FWUPD_VM_UPDATES_DIR): shutil.rmtree(FWUPD_VM_UPDATES_DIR) fwupd-2.0.10/contrib/qubes/src/vms/fwupd_download_updates.py000066400000000000000000000125631501337203100242070ustar00rootroot00000000000000#!/usr/bin/env python3 # # The Qubes OS Project, http://www.qubes-os.org # # Copyright 2021 Norbert KamiÅ„ski # # SPDX-License-Identifier: LGPL-2.1-or-later # import sys import subprocess import os from fwupd_common_vm import FwupdVmCommon FWUPD_VM_DIR = "/home/user/.cache/fwupd" FWUPD_VM_UPDATES_DIR = os.path.join(FWUPD_VM_DIR, "updates") FWUPD_VM_METADATA_DIR = os.path.join(FWUPD_VM_DIR, "metadata") FWUPD_DOWNLOAD_PREFIX = "https://fwupd.org/downloads/" METADATA_URL = "https://fwupd.org/downloads/firmware.xml.gz" METADATA_URL_JCAT = "https://fwupd.org/downloads/firmware.xml.gz.jcat" class DownloadData(FwupdVmCommon): def _download_metadata_file(self): """Download metadata file""" if self.custom_url is None: metadata_url = METADATA_URL else: metadata_url = self.custom_url cmd_metadata = ["curl", "-fL", "-o", self.metadata_file, "--", metadata_url] p = subprocess.Popen(cmd_metadata) p.wait() if p.returncode != 0: raise Exception("fwupd-qubes: Downloading metadata file failed") if not os.path.exists(self.metadata_file): raise FileNotFoundError( "fwupd-qubes: Downloaded metadata file does not exist" ) def _download_metadata_jcat(self): """Download metadata jcat signature""" if self.custom_url is None: metadata_url = METADATA_URL else: metadata_url = self.custom_url cmd_metadata = [ "curl", "-fL", "-o", f"{self.metadata_file}.jcat", "--", f"{metadata_url}.jcat", ] p = subprocess.Popen(cmd_metadata) p.wait() if p.returncode != 0: raise Exception("fwupd-qubes: Downloading metadata file failed") if not os.path.exists(f"{self.metadata_file}.jcat"): raise FileNotFoundError( "fwupd-qubes: Downloaded metadata file does not exist" ) environ = os.environ.copy() environ["LC_ALL"] = "C" cmd_info = ["jcat-tool", "info", f"{self.metadata_file}.jcat"] info_stdout = subprocess.check_output(cmd_info, env=environ).decode() info_id_line = [line for line in info_stdout.splitlines() if "ID:" in line] if info_id_line: info_id = info_id_line[0].split(":", 1)[1].strip() else: info_id = None if info_id and info_id != os.path.basename(metadata_url): # fetch the file referenced in jcat, to workaround CDN being few # hours out of sync self.custom_url = os.path.dirname(metadata_url) + "/" + info_id cmd_export = [ "jcat-tool", f"--prefix={FWUPD_VM_METADATA_DIR}/", "--", "export", f"{self.metadata_file}.jcat", ] p = subprocess.Popen(cmd_export, stdout=subprocess.PIPE, env=environ) stdout, _ = p.communicate() if p.returncode != 0: raise Exception("fwupd-qubes: Extracting jcat file failed") # rename extracted files to match jcat base name, instead of "ID" # inside jcat for line in stdout.decode("ascii").splitlines(): if not line.startswith("Wrote "): continue path = line.split(" ", 1)[1] base_path, ext = os.path.splitext(path) if base_path == self.metadata_file: continue new_path = f"{self.metadata_file}{ext}" os.rename(path, new_path) def download_metadata(self, url=None): """Downloads default metadata and its signatures""" if url is not None: self.custom_url = url custom_metadata_name = os.path.basename(url) self.metadata_file = os.path.join( FWUPD_VM_METADATA_DIR, custom_metadata_name ) else: self.custom_url = None self.metadata_file = os.path.join(FWUPD_VM_METADATA_DIR, "firmware.xml.gz") self.validate_vm_dirs() self._download_metadata_jcat() self._download_metadata_file() def download_updates(self, url, sha): """ Downloads update form given url Keyword argument: url - url address of the update """ self.validate_vm_dirs() self.arch_name = os.path.basename(url) update_path = os.path.join(FWUPD_VM_UPDATES_DIR, self.arch_name) cmd_update = ["curl", "-fL", "-o", update_path, "--", url] p = subprocess.Popen(cmd_update) p.wait() if p.returncode != 0: raise Exception("fwupd-qubes: Downloading update file failed") if not os.path.exists(update_path): raise FileNotFoundError( "fwupd-qubes: Downloaded update file does not exist" ) self.check_shasum(update_path, sha) print("Update file downloaded successfully") def main(): url = None sha = None dn = DownloadData() for arg in sys.argv: if "--url=" in arg: url = arg.replace("--url=", "") if "--sha=" in arg: sha = arg.replace("--sha=", "") if "--metadata" in sys.argv: dn.download_metadata(url=url) elif url and sha: dn.download_updates(url, sha) else: raise Exception("Invalid command!!!") if __name__ == "__main__": main() fwupd-2.0.10/contrib/qubes/test/000077500000000000000000000000001501337203100164505ustar00rootroot00000000000000fwupd-2.0.10/contrib/qubes/test/__init__.py000066400000000000000000000000001501337203100205470ustar00rootroot00000000000000fwupd-2.0.10/contrib/qubes/test/fwupd_logs.py000066400000000000000000001127041501337203100212000ustar00rootroot00000000000000#!/usr/bin/env python3 # # The Qubes OS Project, http://www.qubes-os.org # # Copyright 2021 Norbert KamiÅ„ski # # SPDX-License-Identifier: LGPL-2.1-or-later # UPDATE_INFO = """{ "Devices" : [ { "Name" : "ColorHug2", "DeviceId" : "b0a78eb71f4eeea7df8fb114522556ba8ce22074", "Guid" : [ "2082b5e0-7a64-478a-b1b2-e3404fab6dad", "aa4b4156-9732-55db-9500-bf6388508ee3", "101ee86a-7bea-59fb-9f89-6b6297ceed3b", "2fa8891f-3ece-53a4-adc4-0dd875685f30" ], "Summary" : "An open source display colorimeter", "Plugin" : "hughski_colorhug", "Protocol" : "com.hughski.colorhug", "Flags" : [ "updatable", "supported", "registered", "self-recovery", "add-counterpart-guids" ], "Vendor" : "Hughski Ltd.", "VendorId" : "USB:0x273F", "Version" : "2.0.6", "VersionFormat" : "triplet", "Icons" : [ "colorimeter-colorhug" ], "InstallDuration" : 8, "Created" : 1614224175, "Releases" : [ { "AppstreamId" : "com.hughski.ColorHug2.firmware", "RemoteId" : "lvfs", "Summary" : "Firmware for the Hughski ColorHug2 Colorimeter", "Description" : "

    This release fixes prevents the firmware returning an error when the remote SHA1 hash was never sent.

    ", "Version" : "2.0.7", "Filename" : "hughski-colorhug2-2.0.7.cab", "Protocol" : "com.hughski.colorhug", "Checksum" : [ "80bddeb898cda5b87d9837e13a9ace19846053bf", "32c4a2c9be787cdf1d757c489d6455bd7bb14053425180b6d331c37e1ccc1cda" ], "License" : "GPL-2.0+", "Size" : 16384, "Created" : 1482901200, "Locations" : [ "https://fwupd.org/downloads/0a29848de74d26348bc5a6e24fc9f03778eddf0e-hughski-colorhug2-2.0.7.cab", "ipfs://QmUByRuHG9Gb2s8gKKVqDcjhUrn8vy62B4WqjbpWDD42cf" ], "Uri" : "https://fwupd.org/downloads/0a29848de74d26348bc5a6e24fc9f03778eddf0e-hughski-colorhug2-2.0.7.cab", "Homepage" : "http://www.hughski.com/", "SourceUrl" : "https://github.com/hughski/colorhug2-firmware", "Vendor" : "Hughski Limited", "Flags" : [ "is-upgrade" ], "InstallDuration" : 8 } ] } ] } """ DMI_DECODE = """# dmidecode 3.1 Getting SMBIOS data from sysfs. SMBIOS 3.1.1 present. Handle 0x0000, DMI type 0, 26 bytes BIOS Information Vendor: Dell Inc. Version: P1.00 Release Date: 02/09/2018 Address: 0xF0000 Runtime Size: 64 kB ROM Size: 16 MB Characteristics: PCI is supported BIOS is upgradeable BIOS shadowing is allowed Boot from CD is supported Selectable boot is supported BIOS ROM is socketed EDD is supported 5.25"/1.2 MB floppy services are supported (int 13h) 3.5"/720 kB floppy services are supported (int 13h) 3.5"/2.88 MB floppy services are supported (int 13h) Print screen service is supported (int 5h) 8042 keyboard services are supported (int 9h) Serial services are supported (int 14h) Printer services are supported (int 17h) ACPI is supportedUSB legacy is supported BIOS boot specification is supported Targeted content distribution is supported UEFI is supported BIOS Revision: 5.13 """ GET_DEVICES = """{ "Devices" : [ { "Name" : "ColorHug2", "DeviceId" : "cf294bf55b333004beb7c41f952c1838c23e1f4a", "Guid" : [ "2082b5e0-7a64-478a-b1b2-e3404fab6dad", "aa4b4156-9732-55db-9500-bf6388508ee3", "101ee86a-7bea-59fb-9f89-6b6297ceed3b", "2fa8891f-3ece-53a4-adc4-0dd875685f30" ], "Summary" : "An open source display colorimeter", "Plugin" : "hughski_colorhug", "Protocol" : "com.hughski.colorhug", "Flags" : [ "updatable", "supported", "registered", "self-recovery", "add-counterpart-guids" ], "Vendor" : "Hughski Ltd.", "VendorId" : "USB:0x273F", "Version" : "2.0.6", "VersionFormat" : "triplet", "Icons" : [ "colorimeter-colorhug" ], "InstallDuration" : 8, "Created" : 1614246373, "Releases" : [ { "AppstreamId" : "com.hughski.ColorHug2.firmware", "RemoteId" : "lvfs", "Summary" : "Firmware for the Hughski ColorHug2 Colorimeter", "Description" : "

    This release fixes prevents the firmware returning an error when the remote SHA1 hash was never sent.

    ", "Version" : "2.0.7", "Filename" : "hughski-colorhug2-2.0.7.cab", "Protocol" : "com.hughski.colorhug", "Checksum" : [ "80bddeb898cda5b87d9837e13a9ace19846053bf", "32c4a2c9be787cdf1d757c489d6455bd7bb14053425180b6d331c37e1ccc1cda" ], "License" : "GPL-2.0+", "Size" : 16384, "Created" : 1482901200, "Locations" : [ "https://fwupd.org/downloads/0a29848de74d26348bc5a6e24fc9f03778eddf0e-hughski-colorhug2-2.0.7.cab", "ipfs://QmUByRuHG9Gb2s8gKKVqDcjhUrn8vy62B4WqjbpWDD42cf" ], "Uri" : "https://fwupd.org/downloads/0a29848de74d26348bc5a6e24fc9f03778eddf0e-hughski-colorhug2-2.0.7.cab", "Homepage" : "http://www.hughski.com/", "SourceUrl" : "https://github.com/hughski/colorhug2-firmware", "Vendor" : "Hughski Limited", "Flags" : [ "is-upgrade" ], "InstallDuration" : 8 }, { "AppstreamId" : "com.hughski.ColorHug2.firmware", "RemoteId" : "lvfs", "Summary" : "Firmware for the Hughski ColorHug2 Colorimeter", "Description" : "

    This stable release fixes the following problems:

    • Fix the swapped LEDs on the second half of batch 16
    • Fix the firmware upgrade process using new versions of fwupd
    ", "Version" : "2.0.6", "Filename" : "hughski-colorhug2-2.0.6.cab", "Protocol" : "com.hughski.colorhug", "Checksum" : [ "60e28bb402b427dbce19e150d63987f5e18c1880", "a646b1798ce7f5ac26229aa85c35cc4f44a5bd8bfc9e5332a8ec815aef075566" ], "License" : "GPL-2.0+", "Size" : 16384, "Created" : 1450792062, "Locations" : [ "https://fwupd.org/downloads/170f2c19f17b7819644d3fcc7617621cc3350a04-hughski-colorhug2-2.0.6.cab", "ipfs://QmdWFrYo1YJxgGU37Qy7LkwPQM26vPMVxLRANUga6TzSjW" ], "Uri" : "https://fwupd.org/downloads/170f2c19f17b7819644d3fcc7617621cc3350a04-hughski-colorhug2-2.0.6.cab", "Homepage" : "http://www.hughski.com/", "SourceUrl" : "https://github.com/hughski/colorhug2-firmware", "Vendor" : "Hughski Limited", "InstallDuration" : 8 }, { "AppstreamId" : "com.hughski.ColorHug2.firmware", "RemoteId" : "lvfs", "Summary" : "Firmware for the Hughski ColorHug2 Colorimeter", "Description" : "

    This stable release fixes the following problems:

    • Fix the swapped LEDs on batch 16
    • Make the self test more sensitive to detect floating pins
    ", "Version" : "2.0.5", "Filename" : "hughski-colorhug2-2.0.5.cab", "Protocol" : "com.hughski.colorhug", "Checksum" : [ "e37b9d360d61157657335d80585a005ff2593108", "8cd379eb2e1467e4fda92c20650306dc7e598b1d421841bbe19d9ed6ea01e3ee" ], "License" : "GPL-2.0+", "Size" : 16384, "Created" : 1444059405, "Locations" : [ "https://fwupd.org/downloads/f7dd4ab29fa610438571b8b62b26b0b0e57bb35b-hughski-colorhug2-2.0.5.cab", "ipfs://QmQ648kwvv52wuqPoKjm5zLGXngQnmuJzp1xtJmTEbzgz5" ], "Uri" : "https://fwupd.org/downloads/f7dd4ab29fa610438571b8b62b26b0b0e57bb35b-hughski-colorhug2-2.0.5.cab", "Homepage" : "http://www.hughski.com/", "SourceUrl" : "https://github.com/hughski/colorhug2-firmware", "Vendor" : "Hughski Limited", "Flags" : [ "is-downgrade" ], "InstallDuration" : 8 }, { "AppstreamId" : "com.hughski.ColorHug2.firmware", "RemoteId" : "lvfs", "Summary" : "Firmware for the Hughski ColorHug2 Colorimeter", "Description" : "

    This unstable release adds the following features:

    • Add TakeReadingArray to enable panel latency measurements
    • Speed up the auto-scaled measurements considerably, using 256ms as the smallest sample duration
    ", "Version" : "2.0.2", "Filename" : "hughski-colorhug2-2.0.2.cab", "Protocol" : "com.hughski.colorhug", "Checksum" : [ "1b43bd71bbed2cf0e9c9efcca79799f07b3d0dd2", "c09674fb818d4a1033dbde2fab5885716aed1d8b751b428f16687a78f2a4d61f" ], "License" : "GPL-2.0+", "Size" : 15680, "Created" : 1416675439, "Locations" : [ "https://fwupd.org/downloads/30a121f26c039745aeb5585252d4a9b5386d71cb-hughski-colorhug2-2.0.2.cab", "ipfs://QmZ1DKKsWZQuvnff2DJTDJESMaXTpsc5zfNGX7Sb2HibAn" ], "Uri" : "https://fwupd.org/downloads/30a121f26c039745aeb5585252d4a9b5386d71cb-hughski-colorhug2-2.0.2.cab", "Homepage" : "http://www.hughski.com/", "SourceUrl" : "https://github.com/hughski/colorhug2-firmware", "Vendor" : "Hughski Limited", "Flags" : [ "is-downgrade" ], "InstallDuration" : 8 } ] }, { "Name" : "Display controller", "DeviceId" : "ecf0d22adf39a244a723466378a8884aa22b7e78", "Guid" : [ "e358a53d-98bc-5565-b55e-7df8e0d06c5e", "7365091f-756a-5c83-878c-edd1120ca718", "06208e9f-1dd0-5857-b700-3d77525793aa", "af9ff5a0-c613-5da3-bab8-5d411adebbca" ], "Plugin" : "optionrom", "Flags" : [ "internal", "registered", "can-verify", "can-verify-image" ], "VendorId" : "PCI:0x1234", "Version" : "02", "VersionFormat" : "plain", "Created" : 1614209932 }, { "Name" : "Intel(R) Coreâ„¢ i7-7700HQ CPU @ 2.80GHz", "DeviceId" : "4bde70ba4e39b28f9eab1628f9dd6e6244c03027", "Guid" : [ "30249f37-d140-5d3e-9319-186b1bd5cac3", "809a0b93-8a12-5338-a571-ad5583acf896", "d0f754d5-1395-5573-bc83-85ba955da70a" ], "Plugin" : "cpu", "Flags" : [ "internal", "registered" ], "Vendor" : "Intel", "Version" : "0x000000de", "VersionFormat" : "hex", "VersionRaw" : 222, "Icons" : [ "computer" ], "Created" : 1614209932 } ] } """ GET_DEVICES_NO_UPDATES = """{ "Devices" : [ { "Name" : "ColorHug2", "DeviceId" : "203f56e4e186d078ce76725e708400aafc253aac", "Guid" : [ "2082b5e0-7a64-478a-b1b2-e3404fab6dad", "aa4b4156-9732-55db-9500-bf6388508ee3", "101ee86a-7bea-59fb-9f89-6b6297ceed3b", "2fa8891f-3ece-53a4-adc4-0dd875685f30" ], "Summary" : "An open source display colorimeter", "Plugin" : "hughski_colorhug", "Protocol" : "com.hughski.colorhug", "Flags" : [ "updatable", "supported", "registered", "self-recovery", "add-counterpart-guids" ], "Vendor" : "Hughski Ltd.", "VendorId" : "USB:0x273F", "Version" : "2.0.7", "VersionFormat" : "triplet", "Icons" : [ "colorimeter-colorhug" ], "InstallDuration" : 8, "Created" : 1592916092, "Releases" : [ { "AppstreamId" : "com.hughski.ColorHug2.firmware", "RemoteId" : "lvfs", "Summary" : "Firmware for the Hughski ColorHug2 Colorimeter", "Description" : "

    This release fixes prevents the firmware returning an error when the remote SHA1 hash was never sent.

    ", "Version" : "2.0.7", "Filename" : "658851e6f27c4d87de19cd66b97b610d100efe09", "Protocol" : "com.hughski.colorhug", "Checksum" : [ "490be5c0b13ca4a3f169bf8bc682ba127b8f7b96" ], "License" : "GPL-2.0+", "Size" : 16384, "Created" : 1482901200, "Uri" : "https://fwupd.org/downloads/0a29848de74d26348bc5a6e24fc9f03778eddf0e-hughski-colorhug2-2.0.7.cab", "Homepage" : "http://www.hughski.com/", "SourceUrl" : "https://github.com/hughski/colorhug2-firmware", "Vendor" : "Hughski Limited", "InstallDuration" : 8 }, { "AppstreamId" : "com.hughski.ColorHug2.firmware", "RemoteId" : "lvfs", "Summary" : "Firmware for the Hughski ColorHug2 Colorimeter", "Description" : "

    This stable release fixes the following problems:

    • Fix the swapped LEDs on the second half of batch 16
    • Fix the firmware upgrade process using new versions of fwupd
    ", "Version" : "2.0.6", "Filename" : "f038b5ca40e6d7c1c0299a9e1dcc129d5f6371b6", "Protocol" : "com.hughski.colorhug", "Checksum" : [ "03c9c14db1894a00035ececcfae192865a710e52" ], "License" : "GPL-2.0+", "Size" : 16384, "Created" : 1450792062, "Uri" : "https://fwupd.org/downloads/170f2c19f17b7819644d3fcc7617621cc3350a04-hughski-colorhug2-2.0.6.cab", "Homepage" : "http://www.hughski.com/", "SourceUrl" : "https://github.com/hughski/colorhug2-firmware", "Vendor" : "Hughski Limited", "Flags" : [ "is-downgrade" ], "InstallDuration" : 8 }, { "AppstreamId" : "com.hughski.ColorHug2.firmware", "RemoteId" : "lvfs", "Summary" : "Firmware for the Hughski ColorHug2 Colorimeter", "Description" : "

    This stable release fixes the following problems:

    • Fix the swapped LEDs on batch 16
    • Make the self test more sensitive to detect floating pins
    ", "Version" : "2.0.5", "Filename" : "ae76c6b704b60f9d1d88dc2c8ec8a62d7b2331dc", "Protocol" : "com.hughski.colorhug", "Checksum" : [ "4ee9dfa38df3b810f739d8a19d13da1b3175fb87" ], "License" : "GPL-2.0+", "Size" : 16384, "Created" : 1444059405, "Uri" : "https://fwupd.org/downloads/f7dd4ab29fa610438571b8b62b26b0b0e57bb35b-hughski-colorhug2-2.0.5.cab", "Homepage" : "http://www.hughski.com/", "SourceUrl" : "https://github.com/hughski/colorhug2-firmware", "Vendor" : "Hughski Limited", "Flags" : [ "is-downgrade" ], "InstallDuration" : 8 }, { "AppstreamId" : "com.hughski.ColorHug2.firmware", "RemoteId" : "lvfs", "Summary" : "Firmware for the Hughski ColorHug2 Colorimeter", "Description" : "

    This unstable release adds the following features:

    • Add TakeReadingArray to enable panel latency measurements
    • Speed up the auto-scaled measurements considerably, using 256ms as the smallest sample duration
    ", "Version" : "2.0.2", "Filename" : "d4b3144daeb2418634f9d464d88d55590bcd9ac7", "Protocol" : "com.hughski.colorhug", "Checksum" : [ "448527af3ce019d03dbb77aaebaa7eb893f1ea20" ], "License" : "GPL-2.0+", "Size" : 15680, "Created" : 1416675439, "Uri" : "https://fwupd.org/downloads/30a121f26c039745aeb5585252d4a9b5386d71cb-hughski-colorhug2-2.0.2.cab", "Homepage" : "http://www.hughski.com/", "SourceUrl" : "https://github.com/hughski/colorhug2-firmware", "Vendor" : "Hughski Limited", "Flags" : [ "is-downgrade" ], "InstallDuration" : 8 } ] }, { "Name" : "GP106 [GeForce GTX 1060 6GB]", "DeviceId" : "71b677ca0f1bc2c5b804fa1d59e52064ce589293", "Guid" : [ "b080a9ba-fff8-5de0-b641-26f782949f94", "f95bfce3-18e4-58b0-bd81-136457521383" ], "Plugin" : "optionrom", "Flags" : [ "internal", "registered", "can-verify", "can-verify-image" ], "Vendor" : "NVIDIA Corporation", "VendorId" : "PCI:0x10DE", "Version" : "a1", "VersionFormat" : "plain", "Created" : 1592899254 }, { "Name" : "Intel(R) Coreâ„¢ i5-8400 CPU @ 2.80GHz", "DeviceId" : "4bde70ba4e39b28f9eab1628f9dd6e6244c03027", "Plugin" : "cpu", "Flags" : [ "internal", "registered" ], "Vendor" : "GenuineIntel", "Version" : "0xd6", "VersionFormat" : "hex", "Icons" : [ "computer" ], "Created" : 1592899249 }, { "Name" : "SSDPR-CX400-256", "DeviceId" : "948241a24320627284597ec95079cc1341c90518", "Guid" : [ "09fa3842-45bc-5226-a8ec-1668fc61f88f", "57d6b2ff-710d-5cd2-98be-4f6b8b7c5287", "36bebd37-b680-5d56-83a1-6693033d4098" ], "Summary" : "ATA Drive", "Plugin" : "ata", "Protocol" : "org.t13.ata", "Flags" : [ "internal", "updatable", "require-ac", "registered", "needs-reboot", "usable-during-update" ], "Vendor" : "Phison", "VendorId" : "ATA:0x1987", "Version" : "SBFM61.3", "VersionFormat" : "plain", "Icons" : [ "drive-harddisk" ], "Created" : 1592899254 } ] } """ GET_DEVICES_NO_VERSION = """{ "Devices" : [ { "Name" : "ColorHug2", "DeviceId" : "203f56e4e186d078ce76725e708400aafc253aac", "Guid" : [ "2082b5e0-7a64-478a-b1b2-e3404fab6dad", "aa4b4156-9732-55db-9500-bf6388508ee3", "101ee86a-7bea-59fb-9f89-6b6297ceed3b", "2fa8891f-3ece-53a4-adc4-0dd875685f30" ], "Summary" : "An open source display colorimeter", "Plugin" : "hughski_colorhug", "Protocol" : "com.hughski.colorhug", "Flags" : [ "updatable", "supported", "registered", "self-recovery", "add-counterpart-guids" ], "Vendor" : "Hughski Ltd.", "VendorId" : "USB:0x273F", "VersionFormat" : "triplet", "Icons" : [ "colorimeter-colorhug" ], "InstallDuration" : 8, "Created" : 1592916092, "Releases" : [ { "AppstreamId" : "com.hughski.ColorHug2.firmware", "RemoteId" : "lvfs", "Summary" : "Firmware for the Hughski ColorHug2 Colorimeter", "Description" : "

    This release fixes prevents the firmware returning an error when the remote SHA1 hash was never sent.

    ", "Version" : "2.0.7", "Filename" : "658851e6f27c4d87de19cd66b97b610d100efe09", "Protocol" : "com.hughski.colorhug", "Checksum" : [ "490be5c0b13ca4a3f169bf8bc682ba127b8f7b96" ], "License" : "GPL-2.0+", "Size" : 16384, "Created" : 1482901200, "Uri" : "https://fwupd.org/downloads/0a29848de74d26348bc5a6e24fc9f03778eddf0e-hughski-colorhug2-2.0.7.cab", "Homepage" : "http://www.hughski.com/", "SourceUrl" : "https://github.com/hughski/colorhug2-firmware", "Vendor" : "Hughski Limited", "InstallDuration" : 8 }, { "AppstreamId" : "com.hughski.ColorHug2.firmware", "RemoteId" : "lvfs", "Summary" : "Firmware for the Hughski ColorHug2 Colorimeter", "Description" : "

    This stable release fixes the following problems:

    • Fix the swapped LEDs on the second half of batch 16
    • Fix the firmware upgrade process using new versions of fwupd
    ", "Filename" : "f038b5ca40e6d7c1c0299a9e1dcc129d5f6371b6", "Protocol" : "com.hughski.colorhug", "Checksum" : [ "03c9c14db1894a00035ececcfae192865a710e52" ], "License" : "GPL-2.0+", "Size" : 16384, "Created" : 1450792062, "Uri" : "https://fwupd.org/downloads/170f2c19f17b7819644d3fcc7617621cc3350a04-hughski-colorhug2-2.0.6.cab", "Homepage" : "http://www.hughski.com/", "SourceUrl" : "https://github.com/hughski/colorhug2-firmware", "Vendor" : "Hughski Limited", "Flags" : [ "is-downgrade" ], "InstallDuration" : 8 }, { "AppstreamId" : "com.hughski.ColorHug2.firmware", "RemoteId" : "lvfs", "Summary" : "Firmware for the Hughski ColorHug2 Colorimeter", "Description" : "

    This stable release fixes the following problems:

    • Fix the swapped LEDs on batch 16
    • Make the self test more sensitive to detect floating pins
    ", "Version" : "2.0.5", "Filename" : "ae76c6b704b60f9d1d88dc2c8ec8a62d7b2331dc", "Protocol" : "com.hughski.colorhug", "Checksum" : [ "4ee9dfa38df3b810f739d8a19d13da1b3175fb87" ], "License" : "GPL-2.0+", "Size" : 16384, "Created" : 1444059405, "Uri" : "https://fwupd.org/downloads/f7dd4ab29fa610438571b8b62b26b0b0e57bb35b-hughski-colorhug2-2.0.5.cab", "Homepage" : "http://www.hughski.com/", "SourceUrl" : "https://github.com/hughski/colorhug2-firmware", "Vendor" : "Hughski Limited", "Flags" : [ "is-downgrade" ], "InstallDuration" : 8 }, { "AppstreamId" : "com.hughski.ColorHug2.firmware", "RemoteId" : "lvfs", "Summary" : "Firmware for the Hughski ColorHug2 Colorimeter", "Description" : "

    This unstable release adds the following features:

    • Add TakeReadingArray to enable panel latency measurements
    • Speed up the auto-scaled measurements considerably, using 256ms as the smallest sample duration
    ", "Version" : "2.0.2", "Filename" : "d4b3144daeb2418634f9d464d88d55590bcd9ac7", "Protocol" : "com.hughski.colorhug", "Checksum" : [ "448527af3ce019d03dbb77aaebaa7eb893f1ea20" ], "License" : "GPL-2.0+", "Size" : 15680, "Created" : 1416675439, "Uri" : "https://fwupd.org/downloads/30a121f26c039745aeb5585252d4a9b5386d71cb-hughski-colorhug2-2.0.2.cab", "Homepage" : "http://www.hughski.com/", "SourceUrl" : "https://github.com/hughski/colorhug2-firmware", "Vendor" : "Hughski Limited", "Flags" : [ "is-downgrade" ], "InstallDuration" : 8 } ] }, { "Name" : "ColorHug2", "DeviceId" : "203f56e4e186d078ce76725e708400aafc253aac", "Guid" : [ "2082b5e0-7a64-478a-b1b2-e3404fab6dad", "aa4b4156-9732-55db-9500-bf6388508ee3", "101ee86a-7bea-59fb-9f89-6b6297ceed3b", "2fa8891f-3ece-53a4-adc4-0dd875685f30" ], "Summary" : "An open source display colorimeter", "Plugin" : "hughski_colorhug", "Protocol" : "com.hughski.colorhug", "Flags" : [ "updatable", "supported", "registered", "self-recovery", "add-counterpart-guids" ], "Vendor" : "Hughski Ltd.", "Version" : "2.0.6", "VendorId" : "USB:0x273F", "VersionFormat" : "triplet", "Icons" : [ "colorimeter-colorhug" ], "InstallDuration" : 8, "Created" : 1592916092, "Releases" : [ { "AppstreamId" : "com.hughski.ColorHug2.firmware", "RemoteId" : "lvfs", "Summary" : "Firmware for the Hughski ColorHug2 Colorimeter", "Description" : "

    This release fixes prevents the firmware returning an error when the remote SHA1 hash was never sent.

    ", "Version" : "2.0.7", "Filename" : "658851e6f27c4d87de19cd66b97b610d100efe09", "Protocol" : "com.hughski.colorhug", "Checksum" : [ "490be5c0b13ca4a3f169bf8bc682ba127b8f7b96" ], "License" : "GPL-2.0+", "Size" : 16384, "Created" : 1482901200, "Uri" : "https://fwupd.org/downloads/0a29848de74d26348bc5a6e24fc9f03778eddf0e-hughski-colorhug2-2.0.7.cab", "Homepage" : "http://www.hughski.com/", "SourceUrl" : "https://github.com/hughski/colorhug2-firmware", "Vendor" : "Hughski Limited", "InstallDuration" : 8 }, { "AppstreamId" : "com.hughski.ColorHug2.firmware", "RemoteId" : "lvfs", "Summary" : "Firmware for the Hughski ColorHug2 Colorimeter", "Description" : "

    This stable release fixes the following problems:

    • Fix the swapped LEDs on the second half of batch 16
    • Fix the firmware upgrade process using new versions of fwupd
    ", "Version" : "2.0.6", "Filename" : "f038b5ca40e6d7c1c0299a9e1dcc129d5f6371b6", "Protocol" : "com.hughski.colorhug", "Checksum" : [ "03c9c14db1894a00035ececcfae192865a710e52" ], "License" : "GPL-2.0+", "Size" : 16384, "Created" : 1450792062, "Uri" : "https://fwupd.org/downloads/170f2c19f17b7819644d3fcc7617621cc3350a04-hughski-colorhug2-2.0.6.cab", "Homepage" : "http://www.hughski.com/", "SourceUrl" : "https://github.com/hughski/colorhug2-firmware", "Vendor" : "Hughski Limited", "Flags" : [ "is-downgrade" ], "InstallDuration" : 8 }, { "AppstreamId" : "com.hughski.ColorHug2.firmware", "RemoteId" : "lvfs", "Summary" : "Firmware for the Hughski ColorHug2 Colorimeter", "Description" : "

    This stable release fixes the following problems:

    • Fix the swapped LEDs on batch 16
    • Make the self test more sensitive to detect floating pins
    ", "Version" : "2.0.5", "Filename" : "ae76c6b704b60f9d1d88dc2c8ec8a62d7b2331dc", "Protocol" : "com.hughski.colorhug", "Checksum" : [ "4ee9dfa38df3b810f739d8a19d13da1b3175fb87" ], "License" : "GPL-2.0+", "Size" : 16384, "Created" : 1444059405, "Uri" : "https://fwupd.org/downloads/f7dd4ab29fa610438571b8b62b26b0b0e57bb35b-hughski-colorhug2-2.0.5.cab", "Homepage" : "http://www.hughski.com/", "SourceUrl" : "https://github.com/hughski/colorhug2-firmware", "Vendor" : "Hughski Limited", "Flags" : [ "is-downgrade" ], "InstallDuration" : 8 }, { "AppstreamId" : "com.hughski.ColorHug2.firmware", "RemoteId" : "lvfs", "Summary" : "Firmware for the Hughski ColorHug2 Colorimeter", "Description" : "

    This unstable release adds the following features:

    • Add TakeReadingArray to enable panel latency measurements
    • Speed up the auto-scaled measurements considerably, using 256ms as the smallest sample duration
    ", "Version" : "2.0.2", "Filename" : "d4b3144daeb2418634f9d464d88d55590bcd9ac7", "Protocol" : "com.hughski.colorhug", "Checksum" : [ "448527af3ce019d03dbb77aaebaa7eb893f1ea20" ], "License" : "GPL-2.0+", "Size" : 15680, "Created" : 1416675439, "Uri" : "https://fwupd.org/downloads/30a121f26c039745aeb5585252d4a9b5386d71cb-hughski-colorhug2-2.0.2.cab", "Homepage" : "http://www.hughski.com/", "SourceUrl" : "https://github.com/hughski/colorhug2-firmware", "Vendor" : "Hughski Limited", "Flags" : [ "is-downgrade" ], "InstallDuration" : 8 } ] }, { "Name" : "GP106 [GeForce GTX 1060 6GB]", "DeviceId" : "71b677ca0f1bc2c5b804fa1d59e52064ce589293", "Guid" : [ "b080a9ba-fff8-5de0-b641-26f782949f94", "f95bfce3-18e4-58b0-bd81-136457521383" ], "Plugin" : "optionrom", "Flags" : [ "internal", "registered", "can-verify", "can-verify-image" ], "Vendor" : "NVIDIA Corporation", "VendorId" : "PCI:0x10DE", "VersionFormat" : "plain", "Created" : 1592899254 }, { "Name" : "Intel(R) Coreâ„¢ i5-8400 CPU @ 2.80GHz", "DeviceId" : "4bde70ba4e39b28f9eab1628f9dd6e6244c03027", "Plugin" : "cpu", "Flags" : [ "internal", "registered" ], "Vendor" : "GenuineIntel", "Version" : "0xd6", "VersionFormat" : "hex", "Icons" : [ "computer" ], "Created" : 1592899249 }, { "Name" : "SSDPR-CX400-256", "DeviceId" : "948241a24320627284597ec95079cc1341c90518", "Guid" : [ "09fa3842-45bc-5226-a8ec-1668fc61f88f", "57d6b2ff-710d-5cd2-98be-4f6b8b7c5287", "36bebd37-b680-5d56-83a1-6693033d4098" ], "Summary" : "ATA Drive", "Plugin" : "ata", "Protocol" : "org.t13.ata", "Flags" : [ "internal", "updatable", "require-ac", "registered", "needs-reboot", "usable-during-update" ], "Vendor" : "Phison", "VendorId" : "ATA:0x1987", "Version" : "SBFM61.3", "VersionFormat" : "plain", "Icons" : [ "drive-harddisk" ], "Created" : 1592899254 } ] } """ HEADS_XML = """ com.3mdeb.heads.x230.firmware Heads x230 System Update x230 heads system firmware

    x230 heads system firmware

    596c3466-0506-5ca5-a68f-dc34532a93d3 http://osresearch.net/ CC0-1.0 GPLv2 coreboot X-System https://fwupd.org/downloads/e747a435bf24fd6081b77b6704b39cec5fa2dcf62e0ca6b86d8a6460121a1d07-heads_coreboot_x230-v0_2_3.cab 1a54e69ca2b58d1218035115d481480eaf4c66e4 ba519a7a5d8136c8ade0cf0c775c58f3165f42798ff631c3f57f075897ef1586 76373f1b5a157b6563d3605271472901b03f57f3 9a9c5dbd3faf90ff7a1f4c9be8d71c4db93dd69fa690f8722fec19c5a51aed9e

    Fixes flash-gui issue.

    12582912 12591670
    https://fwupd.org/downloads/1a0f0ad487a40bb27a49db55e256a207a33dac92c5c53761501c9fb89e4fd115-heads_coreboot_x230-v0_2_2.cab 58e85d012ad1d5c6f98e8fe65202b4d6c8a6ec03 94430160d35cf74adf29c7fc1490b44497e1a3f0fff72733efe2982c61c9a772 8e97ce38396e281fcf9a5a248819925a2fa04265 a6774661407622f345bf0ac2f113540507f0288bb97bf5dba586059c0653f659

    Lenovo x230 heads system firmware

    12582912 12591680
    """ fwupd-2.0.10/contrib/qubes/test/logs/000077500000000000000000000000001501337203100174145ustar00rootroot00000000000000fwupd-2.0.10/contrib/qubes/test/logs/firmware.metainfo.xml000066400000000000000000000036041501337203100235560ustar00rootroot00000000000000 com.dell.uefi6180aaaa.firmware Latitude 7390 2-in-1 Firmware for the Dell Latitude 7390 2-in-1

    Updating the system firmware improves performance.

    6180aaaa-5529-4bbf-b4fd-65b4f788de5b http://support.dell.com/ CC0-1.0 proprietary Dell Inc. X-System quad dell-bios org.uefi.capsule b03252481573f600c7f530d7a4c24bd62c810412 bd866bcd2b5964da4b20d3f57128746efb7afefe648cf7284f2fae399225f1ac

    This stable release fixes the following issues:

    • Firmware updates to address the Intel Security Advisories.
    • Enhanced the system firmware auto recovery function when system firmware does not work.
    • Updated the BIOS warning message that is displayed when an AC adapter with low wattage is connected to the system.
    • Updated the Intel CPU Microcode.
    14699068 CVE-2019-14607 CVE-2019-11157
    fwupd-2.0.10/contrib/qubes/test/logs/get_devices.log000066400000000000000000000014271501337203100224040ustar00rootroot00000000000000====================================================================== Dom0 Devices: ====================================================================== ColorHug2 ====================================================================== DeviceId: b0a78eb71f4eeea7df8fb114522556ba8ce22074 Guid: ·2082b5e0-7a64-478a-b1b2-e3404fab6dad ·aa4b4156-9732-55db-9500-bf6388508ee3 ·101ee86a-7bea-59fb-9f89-6b6297ceed3b ·2fa8891f-3ece-53a4-adc4-0dd875685f30 Summary: An open source display colorimeter Plugin: hughski_colorhug Protocol: com.hughski.colorhug Flags: ·updatable ·supported ·registered ·self-recovery ·add-counterpart-guids Vendor: Hughski Ltd. VendorId: USB:0x273F Version: 2.0.6 Created: 1614224175 fwupd-2.0.10/contrib/qubes/test/logs/get_updates.log000066400000000000000000000014021501337203100224200ustar00rootroot00000000000000====================================================== sys-usb updates: ====================================================== Available updates: ====================================================== ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 1. Device: ColorHug2 Current firmware version: 2.0.6 ====================================================== Firmware update version: 2.0.7 URL: https://fwupd.org/downloads/0a29848de74d26348bc5a6e24fc9f03778eddf0e-hughski-colorhug2-2.0.7.cab SHA256 checksum: 32c4a2c9be787cdf1d757c489d6455bd7bb14053425180b6d331c37e1ccc1cda Description: This release fixes prevents the firmware returning an error when the remote SHA1 hash was never sent. ====================================================== fwupd-2.0.10/contrib/qubes/test/logs/help.log000066400000000000000000000021701501337203100210470ustar00rootroot00000000000000====================================================================== Usage: ====================================================================== Command: qubes-fwupdmgr [OPTION…][FLAG..] Example: qubes-fwupdmgr refresh --whonix --url= Options: ====================================================================== get-devices: Get all devices that support firmware updates get-updates: Get the list of updates for connected hardware refresh: Refresh metadata from remote server update: Update chosen device to latest firmware version update-heads: Updates heads firmware to the latest version (EXPERIMENTAL, not fully supported yet) downgrade: Downgrade chosen device to chosen firmware version clean: Delete all cached update files Flags: ====================================================================== --whonix: Download firmware updates via Tor --device: Specify device for heads update (default - x230) --url: Address of the custom metadata remote server Help: ====================================================================== -h --help: Show help options fwupd-2.0.10/contrib/qubes/test/logs/metainfo_name/000077500000000000000000000000001501337203100222165ustar00rootroot00000000000000fwupd-2.0.10/contrib/qubes/test/logs/metainfo_name/firmware.metainfo.xml000066400000000000000000000036051501337203100263610ustar00rootroot00000000000000 com.dell.uefi6180aaaa.firmware Latitude 7390 2-in-1 Firmware for the Dell Latitude 7390 2-in-1

    Updating the system firmware improves performance.

    6180aaaa-5529-4bbf-b4fd-65b4f788de5b http://support.dell.com/ CC0-1.0 proprietary Wrong name X-System quad dell-bios org.uefi.capsule b03252481573f600c7f530d7a4c24bd62c810412 bd866bcd2b5964da4b20d3f57128746efb7afefe648cf7284f2fae399225f1ac

    This stable release fixes the following issues:

    • Firmware updates to address the Intel Security Advisories.
    • Enhanced the system firmware auto recovery function when system firmware does not work.
    • Updated the BIOS warning message that is displayed when an AC adapter with low wattage is connected to the system.
    • Updated the Intel CPU Microcode.
    14699068 CVE-2019-14607 CVE-2019-11157
    fwupd-2.0.10/contrib/qubes/test/logs/metainfo_version/000077500000000000000000000000001501337203100227635ustar00rootroot00000000000000fwupd-2.0.10/contrib/qubes/test/logs/metainfo_version/firmware.metainfo.xml000066400000000000000000000036041501337203100271250ustar00rootroot00000000000000 com.dell.uefi6180aaaa.firmware Latitude 7390 2-in-1 Firmware for the Dell Latitude 7390 2-in-1

    Updating the system firmware improves performance.

    6180aaaa-5529-4bbf-b4fd-65b4f788de5b http://support.dell.com/ CC0-1.0 proprietary Dell Inc. X-System quad dell-bios org.uefi.capsule b03252481573f600c7f530d7a4c24bd62c810412 bd866bcd2b5964da4b20d3f57128746efb7afefe648cf7284f2fae399225f1ac

    This stable release fixes the following issues:

    • Firmware updates to address the Intel Security Advisories.
    • Enhanced the system firmware auto recovery function when system firmware does not work.
    • Updated the BIOS warning message that is displayed when an AC adapter with low wattage is connected to the system.
    • Updated the Intel CPU Microcode.
    14699068 CVE-2019-14607 CVE-2019-11157
    fwupd-2.0.10/contrib/qubes/test/test_qubes_fwupd_heads.py000066400000000000000000000115011501337203100235470ustar00rootroot00000000000000#!/usr/bin/env python3 # # The Qubes OS Project, http://www.qubes-os.org # # Copyright 2021 Norbert KamiÅ„ski # # SPDX-License-Identifier: LGPL-2.1-or-later # import io import os import platform import shutil import imp import src.qubes_fwupd_heads as qf_heads import sys import unittest from test.fwupd_logs import HEADS_XML CUSTOM_METADATA = "https://fwupd.org/downloads/firmware-3c81bfdc9db5c8a42c09d38091944bc1a05b27b0.xml.gz" QUBES_FWUPDMGR_REPO = "./src/qubes_fwupdmgr.py" QUBES_FWUPDMGR_BINDIR = "/usr/sbin/qubes-fwupdmgr" class TestQubesFwupdHeads(unittest.TestCase): def setUp(self): if os.path.exists(QUBES_FWUPDMGR_REPO): self.qfwupd = imp.load_source("qubes_fwupdmgr", QUBES_FWUPDMGR_REPO) elif os.path.exists(QUBES_FWUPDMGR_BINDIR): self.qfwupd = imp.load_source("qubes_fwupdmgr", QUBES_FWUPDMGR_BINDIR) self.q = qf_heads.FwupdHeads() self.maxDiff = 2000 self.captured_output = io.StringIO() sys.stdout = self.captured_output @unittest.skipUnless("qubes" in platform.release(), "Requires Qubes OS") def test_get_hwids(self): self.q._get_hwids() self.assertNotEqual(self.q.dom0_hwids_info, "") def test_gather_firmware_version_empty(self): self.q.dom0_hwids_info = "" return_code = self.q._gather_firmware_version() self.assertEqual(return_code, self.qfwupd.EXIT_CODES["NOTHING_TO_DO"]) def test_gather_firmware_version(self): self.q.dom0_hwids_info = "CBET4000 Heads-v0.2.2-917-g19f0e65" self.q._gather_firmware_version() self.assertEqual(self.q.heads_version, "0.2.2-917-g19f0e65") @unittest.skipUnless("qubes" in platform.release(), "Requires Qubes OS") def test_parse_metadata(self): qmgr = self.qfwupd.QubesFwupdmgr() qmgr.metadata_file = CUSTOM_METADATA.replace( "https://fwupd.org/downloads", self.qfwupd.FWUPD_DOM0_METADATA_DIR ) qmgr._download_metadata(metadata_url=CUSTOM_METADATA) self.q._parse_metadata(qmgr.metadata_file) self.assertTrue(self.q.metadata_info) def test_check_heads_updates_default_heads(self): self.q.metadata_info = HEADS_XML self.q.heads_version = "heads" return_code = self.q._parse_heads_updates("x230") self.assertEqual(return_code, self.qfwupd.EXIT_CODES["SUCCESS"]) self.assertEqual( self.q.heads_update_url, "https://fwupd.org/downloads/e747a435bf24fd6081b77b6704b39cec5fa2dcf62e0ca6b86d8a6460121a1d07-heads_coreboot_x230-v0_2_3.cab", ) self.assertEqual( self.q.heads_update_sha, "ba519a7a5d8136c8ade0cf0c775c58f3165f42798ff631c3f57f075897ef1586", ) self.assertEqual(self.q.heads_update_version, "0.2.3") def test_check_heads_updates_no_updates(self): self.q.metadata_info = HEADS_XML self.q.heads_version = "0.2.3" return_code = self.q._parse_heads_updates("x230") self.assertEqual(return_code, self.qfwupd.EXIT_CODES["NOTHING_TO_DO"]) def test_check_heads_updates_lower_version(self): self.q.metadata_info = HEADS_XML self.q.heads_version = "0.2.2" return_code = self.q._parse_heads_updates("x230") self.assertEqual(return_code, self.qfwupd.EXIT_CODES["SUCCESS"]) self.assertEqual( self.q.heads_update_url, "https://fwupd.org/downloads/e747a435bf24fd6081b77b6704b39cec5fa2dcf62e0ca6b86d8a6460121a1d07-heads_coreboot_x230-v0_2_3.cab", ) self.assertEqual( self.q.heads_update_sha, "ba519a7a5d8136c8ade0cf0c775c58f3165f42798ff631c3f57f075897ef1586", ) self.assertEqual(self.q.heads_update_version, "0.2.3") @unittest.skipUnless("qubes" in platform.release(), "Requires Qubes OS") def test_copy_heads_firmware(self): qmgr = self.qfwupd.QubesFwupdmgr() self.q.heads_update_url = "https://fwupd.org/downloads/e747a435bf24fd6081b77b6704b39cec5fa2dcf62e0ca6b86d8a6460121a1d07-heads_coreboot_x230-v0_2_3.cab" self.q.heads_update_sha = ( "ba519a7a5d8136c8ade0cf0c775c58f3165f42798ff631c3f57f075897ef1586" ) self.q.heads_update_version = "0.2.3" qmgr._download_firmware_updates( self.q.heads_update_url, self.q.heads_update_sha ) heads_boot_path = os.path.join( qf_heads.HEADS_UPDATES_DIR, self.q.heads_update_version ) if os.path.exists(heads_boot_path): shutil.rmtree(heads_boot_path) ret_code = self.q._copy_heads_firmware(qmgr.arch_path) self.assertNotEqual(ret_code, self.qfwupd.EXIT_CODES["NOTHING_TO_DO"]) firmware_path = os.path.join(heads_boot_path, "firmware.rom") self.assertTrue(os.path.exists(firmware_path)) if __name__ == "__main__": unittest.main() fwupd-2.0.10/contrib/qubes/test/test_qubes_fwupdmgr.py000077500000000000000000000417061501337203100231260ustar00rootroot00000000000000#!/usr/bin/env python3 # # The Qubes OS Project, http://www.qubes-os.org # # Copyright 2021 Norbert KamiÅ„ski # # SPDX-License-Identifier: LGPL-2.1-or-later # import json import unittest import os import subprocess import sys import imp import io import platform import tempfile from packaging.version import Version from .fwupd_logs import UPDATE_INFO, GET_DEVICES, DMI_DECODE from .fwupd_logs import GET_DEVICES_NO_VERSION from unittest.mock import patch QUBES_FWUPDMGR_REPO = "./src/qubes_fwupdmgr.py" QUBES_FWUPDMGR_BINDIR = "/usr/sbin/qubes-fwupdmgr" if os.path.exists(QUBES_FWUPDMGR_REPO): qfwupd = imp.load_source("qubes_fwupdmgr", QUBES_FWUPDMGR_REPO) elif os.path.exists(QUBES_FWUPDMGR_BINDIR): qfwupd = imp.load_source("qubes_fwupdmgr", QUBES_FWUPDMGR_BINDIR) FWUPD_DOM0_DIR = "/var/cache/fwupd/qubes" FWUPD_DOM0_UPDATES_DIR = os.path.join(FWUPD_DOM0_DIR, "updates") FWUPD_DOM0_UNTRUSTED_DIR = os.path.join(FWUPD_DOM0_UPDATES_DIR, "untrusted") FWUPD_DOM0_METADATA_DIR = os.path.join(FWUPD_DOM0_DIR, "metadata") FWUPD_DOM0_METADATA_FILE = os.path.join(FWUPD_DOM0_METADATA_DIR, "firmware.xml.xz") FWUPD_DOM0_METADATA_FILE_JCAT = os.path.join(FWUPD_DOM0_METADATA_DIR, "firmware.xml.xz") REQUIRED_DEV = "Requires device not connected" XL_LIST_LOG = "Name ID Mem VCPUs State Time(s)" FWUPDMGR = "/bin/fwupdmgr" BIOS_UPDATE_FLAG = os.path.join(FWUPD_DOM0_DIR, "bios_update") LVFS_TESTING_DOM0_FLAG = os.path.join(FWUPD_DOM0_DIR, "lvfs_testing") CUSTOM_METADATA = "https://fwupd.org/downloads/firmware-3c81bfdc9db5c8a42c09d38091944bc1a05b27b0.xml.gz" def device_connected_dom0(): """Checks if the testing device is connected in dom0""" if "qubes" not in platform.release(): return False q = qfwupd.QubesFwupdmgr() q._get_dom0_devices() return "ColorHug2" in q.dom0_devices_info def check_whonix_updatevm(): """Checks if the sys-whonix is running""" if "qubes" not in platform.release(): return False q = qfwupd.QubesFwupdmgr() return "sys-whonix" in q.output class TestQubesFwupdmgr(unittest.TestCase): def setUp(self): self.q = qfwupd.QubesFwupdmgr() self.maxDiff = 2000 self.captured_output = io.StringIO() self.orig_stdout = sys.stdout sys.stdout = self.captured_output def tearDown(self): sys.stdout = self.orig_stdout @unittest.skipUnless("qubes" in platform.release(), "Requires Qubes OS") def test_download_metadata(self): self.q.metadata_file = FWUPD_DOM0_METADATA_FILE self.q._download_metadata(metadata_url=qfwupd.METADATA_URL) self.assertTrue( os.path.exists(FWUPD_DOM0_METADATA_FILE), msg="Metadata update file does not exist", ) self.assertTrue( os.path.exists(FWUPD_DOM0_METADATA_FILE_JCAT), msg="Metadata signature does not exist", ) @unittest.skipUnless(check_whonix_updatevm(), "Requires sys-whonix") def test_download_metadata_whonix(self): self.q.metadata_file = FWUPD_DOM0_METADATA_FILE self.q._download_metadata(whonix=True) self.assertTrue( os.path.exists(FWUPD_DOM0_METADATA_FILE), msg="Metadata update file does not exist", ) self.assertTrue( os.path.exists(FWUPD_DOM0_METADATA_FILE_JCAT), msg="Metadata signature does not exist", ) @unittest.skipUnless("qubes" in platform.release(), "Requires Qubes OS") def test_download_custom_metadata(self): self.q.metadata_file = CUSTOM_METADATA.replace( "https://fwupd.org/downloads", FWUPD_DOM0_METADATA_DIR ) self.q.metadata_file_jcat = self.q.metadata_file + ".jcat" self.q._download_metadata(metadata_url=CUSTOM_METADATA) self.assertTrue( os.path.exists(self.q.metadata_file), msg="Metadata update file does not exist", ) self.assertTrue( os.path.exists(self.q.metadata_file_jcat), msg="Metadata signature does not exist", ) @unittest.skipUnless("qubes" in platform.release(), "Requires Qubes OS") def test_refresh_metadata_dom0(self): self.q.refresh_metadata(metadata_url=qfwupd.METADATA_URL) self.assertEqual( self.captured_output.getvalue().strip(), "Successfully refreshed metadata manually", msg="Metadata refresh failed.", ) @unittest.skipUnless("qubes" in platform.release(), "Requires Qubes OS") @unittest.expectedFailure # fwupd refuses metadata downgrade def test_refresh_metadata_dom0_custom(self): self.q.refresh_metadata(metadata_url=CUSTOM_METADATA) self.assertEqual( self.captured_output.getvalue().strip(), "Successfully refreshed metadata manually", msg="Metadata refresh failed.", ) @unittest.skipUnless(check_whonix_updatevm(), "Requires sys-whonix") def test_refresh_metadata_whonix(self): self.q.refresh_metadata(whonix=True, metadata_url=qfwupd.METADATA_URL) self.assertEqual( self.captured_output.getvalue().strip(), "Successfully refreshed metadata manually", msg="Metadata refresh failed.", ) @unittest.skipUnless("qubes" in platform.release(), "Requires Qubes OS") def test_get_dom0_updates(self): self.q._get_dom0_updates() self.assertIn( "Devices", self.q.dom0_updates_info, msg="Getting available updates failed" ) def test_parse_updates_info(self): self.q._parse_dom0_updates_info(UPDATE_INFO) self.assertEqual( self.q.dom0_updates_list[0]["Name"], "ColorHug2", msg="Wrong device name" ) self.assertEqual( self.q.dom0_updates_list[0]["Version"], "2.0.6", msg="Wrong update version" ) self.assertEqual( self.q.dom0_updates_list[0]["Releases"][0]["Url"], "https://fwupd.org/downloads/0a29848de74d26348bc5a6e24fc9f03778eddf0e-hughski-colorhug2-2.0.7.cab", msg="Wrong update URL", ) self.assertEqual( self.q.dom0_updates_list[0]["Releases"][0]["Checksum"], "32c4a2c9be787cdf1d757c489d6455bd7bb14053425180b6d331c37e1ccc1cda", msg="Wrong checksum", ) @unittest.skipUnless("qubes" in platform.release(), "Requires Qubes OS") def test_download_firmware_updates(self): self.q._download_firmware_updates( "https://fwupd.org/downloads/e5ad222bdbd3d3d48d8613e67c7e0a0e194f" "8cd828e33c554d9f05d933e482c7-hughski-colorhug2-2.0.7.cab", "e5ad222bdbd3d3d48d8613e67c7e0a0e194f8cd828e33c554d9f05d933e482c7", ) update_path = os.path.join( FWUPD_DOM0_UPDATES_DIR, "e5ad222bdbd3d3d48d8613e67c7e0a0e194f8cd828e33c554d9f05d933e482c7" "-hughski-colorhug2-2.0.7.cab", ) self.assertTrue(os.path.exists(update_path)) @unittest.skipUnless(check_whonix_updatevm(), "Requires sys-whonix") def test_download_firmware_updates_whonix(self): self.q._download_firmware_updates( "https://fwupd.org/downloads/e5ad222bdbd3d3d48d8613e67c7e0a0e194f" "8cd828e33c554d9f05d933e482c7-hughski-colorhug2-2.0.7.cab", "e5ad222bdbd3d3d48d8613e67c7e0a0e194f8cd828e33c554d9f05d933e482c7", whonix=True, ) update_path = os.path.join( FWUPD_DOM0_UPDATES_DIR, "e5ad222bdbd3d3d48d8613e67c7e0a0e194f8cd828e33c554d9f05d933e482c7" "-hughski-colorhug2-2.0.7.cab", ) self.assertTrue(os.path.exists(update_path)) def test_user_input_empty_dict(self): self.assertEqual(self.q._user_input([]), -2) def test_user_input_n(self): user_input = ["sth", "n"] with patch("builtins.input", side_effect=user_input): self.q._parse_dom0_updates_info(UPDATE_INFO) choice = self.q._user_input(self.q.dom0_updates_list) self.assertEqual(choice, -2) user_input = ["sth", "N"] with patch("builtins.input", side_effect=user_input): self.q._parse_dom0_updates_info(UPDATE_INFO) choice = self.q._user_input(self.q.dom0_updates_list) self.assertEqual(choice, -2) def test_user_input_choice(self): user_input = ["6", "1"] with patch("builtins.input", side_effect=user_input): self.q._parse_dom0_updates_info(UPDATE_INFO) choice = self.q._user_input(self.q.dom0_updates_list) self.assertEqual(choice, 0) def test_parse_parameters(self): self.q._parse_dom0_updates_info(UPDATE_INFO) self.q._parse_parameters(self.q.dom0_updates_list, 0) self.assertEqual( self.q.url, "https://fwupd.org/downloads/0a29848de74d26348bc5a6e24fc9f03778eddf0e-hughski-colorhug2-2.0.7.cab", ) self.assertEqual( self.q.sha, "32c4a2c9be787cdf1d757c489d6455bd7bb14053425180b6d331c37e1ccc1cda", ) self.assertEqual(self.q.version, "2.0.7") @unittest.skipUnless("qubes" in platform.release(), "Requires Qubes OS") def test_clean_cache_dom0(self): self.q.clean_cache() self.assertFalse(os.path.exists(FWUPD_DOM0_METADATA_DIR)) self.assertFalse(os.path.exists(FWUPD_DOM0_UNTRUSTED_DIR)) def test_output_crawler(self): crawler_output = io.StringIO() sys.stdout = crawler_output self.q._output_crawler(json.loads(UPDATE_INFO), 0) with open("test/logs/get_devices.log") as get_devices: self.assertEqual( get_devices.read(), crawler_output.getvalue().strip() + "\n" ) sys.stdout = self.captured_output @unittest.skipUnless("qubes" in platform.release(), "Requires Qubes OS") def test_get_dom0_devices(self): self.q._get_dom0_devices() self.assertIsNotNone(self.q.dom0_devices_info) @unittest.skipUnless("qubes" in platform.release(), "Requires Qubes OS") def test_get_devices_qubes_dom0(self): get_devices_output = io.StringIO() sys.stdout = get_devices_output self.q.get_devices_qubes() self.assertNotEqual(get_devices_output.getvalue().strip(), "") sys.stdout = self.captured_output @unittest.skipUnless(device_connected_dom0(), REQUIRED_DEV) def test_get_updates_qubes_dom0(self): get_updates_output = io.StringIO() sys.stdout = get_updates_output self.q.get_updates_qubes() self.assertNotEqual(get_updates_output.getvalue().strip(), "") sys.stdout = self.captured_output def test_help(self): help_output = io.StringIO() sys.stdout = help_output self.q.help() with open("test/logs/help.log") as help_log: self.assertEqual(help_log.read(), help_output.getvalue().strip() + "\n") sys.stdout = self.captured_output @patch( "test.test_qubes_fwupdmgr.qfwupd.QubesFwupdmgr._read_dmi", return_value=DMI_DECODE, ) def test_verify_dmi(self, output): self.q.dmi_version = "P.1.0" with tempfile.TemporaryDirectory() as tmpdir: arch_name = tmpdir + "/firmware.cab" subprocess.check_call( ["fwupdtool", "build-cabinet", arch_name, "firmware.metainfo.xml"], cwd="test/logs", ) self.q._verify_dmi(arch_name, "P1.1") @patch( "test.test_qubes_fwupdmgr.qfwupd.QubesFwupdmgr._read_dmi", return_value=DMI_DECODE, ) def test_verify_dmi_wrong_vendor(self, output): with self.assertRaises(ValueError) as wrong_vendor: self.q.dmi_version = "P.1.0" with tempfile.TemporaryDirectory() as tmpdir: arch_name = tmpdir + "/firmware.cab" subprocess.check_call( ["fwupdtool", "build-cabinet", arch_name, "firmware.metainfo.xml"], cwd="test/logs/metainfo_name", ) self.q._verify_dmi(arch_name, "P1.1") self.assertIn("Wrong firmware provider.", str(wrong_vendor.exception)) @patch( "test.test_qubes_fwupdmgr.qfwupd.QubesFwupdmgr._read_dmi", return_value=DMI_DECODE, ) def test_verify_dmi_version(self, output): self.q.dmi_version = "P1.0" with self.assertRaises(ValueError) as downgrade: with tempfile.TemporaryDirectory() as tmpdir: arch_name = tmpdir + "/firmware.cab" subprocess.check_call( ["fwupdtool", "build-cabinet", arch_name, "firmware.metainfo.xml"], cwd="test/logs/metainfo_version", ) self.q._verify_dmi(arch_name, "P0.1") self.assertIn("P0.1 < P1.0 Downgrade not allowed", str(downgrade.exception)) @unittest.skipUnless(device_connected_dom0(), REQUIRED_DEV) def test_downgrade_firmware_dom0(self): old_version = None self.q._get_dom0_devices() downgrades = self.q._parse_downgrades(self.q.dom0_devices_info) for number, device in enumerate(downgrades): if "Name" not in device: continue if device["Name"] == "ColorHug2": old_version = device["Version"] break if old_version is None: self.fail("Test device not found") user_input = [str(number + 1), "1"] with patch("builtins.input", side_effect=user_input): self.q.downgrade_firmware() self.q._get_dom0_devices() downgrades = self.q._parse_downgrades(self.q.dom0_devices_info) new_version = downgrades[number]["Version"] self.assertGreater(Version(old_version), Version(new_version)) def test_parse_downgrades(self): downgrades = self.q._parse_downgrades(GET_DEVICES) self.assertEqual(downgrades[0]["Name"], "ColorHug2") self.assertEqual(downgrades[0]["Version"], "2.0.6") self.assertEqual(downgrades[0]["Releases"][0]["Version"], "2.0.5") self.assertEqual( downgrades[0]["Releases"][0]["Url"], "https://fwupd.org/downloads/f7dd4ab29fa610438571b8b62b26b0b0e57bb35b-hughski-colorhug2-2.0.5.cab", ) self.assertEqual( downgrades[0]["Releases"][0]["Checksum"], "8cd379eb2e1467e4fda92c20650306dc7e598b1d421841bbe19d9ed6ea01e3ee", ) def test_parse_downgrades_no_version(self): downgrades = self.q._parse_downgrades(GET_DEVICES_NO_VERSION) self.assertEqual(downgrades[0]["Name"], "ColorHug2") self.assertEqual(downgrades[0]["Version"], "2.0.6") self.assertEqual(downgrades[0]["Releases"][0]["Version"], "2.0.5") self.assertEqual( downgrades[0]["Releases"][0]["Url"], "https://fwupd.org/downloads/f7dd4ab29fa610438571b8b62b26b0b0e57bb35b-hughski-colorhug2-2.0.5.cab", ) self.assertEqual( downgrades[0]["Releases"][0]["Checksum"], "4ee9dfa38df3b810f739d8a19d13da1b3175fb87", ) def test_user_input_downgrade_dom0(self): user_input = ["1", "6", "sth", "2.2.1", "", " ", "\0", "2"] with patch("builtins.input", side_effect=user_input): downgrade_list = self.q._parse_downgrades(GET_DEVICES) downgrade_dict = downgrade_list device_choice, downgrade_choice = self.q._user_input( downgrade_dict, downgrade=True ) self.assertEqual(device_choice, 0) self.assertEqual(downgrade_choice, 1) def test_user_input_downgrade_N(self): user_input = ["N"] with patch("builtins.input", side_effect=user_input): downgrade_list = self.q._parse_downgrades(GET_DEVICES) N_choice = self.q._user_input(downgrade_list, downgrade=True) self.assertEqual(N_choice, -2) @unittest.skipUnless(device_connected_dom0(), REQUIRED_DEV) def test_update_firmware_dom0(self): old_version = None new_version = None self.q._get_dom0_updates() self.q._parse_dom0_updates_info(self.q.dom0_updates_info) for number, device in enumerate(self.q.dom0_updates_list): if "Name" not in device: continue if device["Name"] == "ColorHug2": old_version = device["Version"] break if old_version is None: self.fail("Test device not found") user_input = [str(number + 1)] with patch("builtins.input", side_effect=user_input): self.q.update_firmware() self.q._get_dom0_devices() dom0_devices_info_dict = json.loads(self.q.dom0_devices_info) for device in dom0_devices_info_dict["Devices"]: if "Name" not in device: continue if device["Name"] == "ColorHug2": new_version = device["Version"] break if new_version is None: self.fail("Test device not found") self.assertLess(Version(old_version), Version(new_version)) if __name__ == "__main__": unittest.main() fwupd-2.0.10/contrib/reformat-code.py000077500000000000000000000043771501337203100174710ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2017 Dell Inc. # # SPDX-License-Identifier: LGPL-2.1-or-later # import os import sys import subprocess import argparse CLANG_DIFF_FORMATTERS = [ "clang-format-diff-11", "clang-format-diff-13", "clang-format-diff", "/usr/share/clang/clang-format-diff.py", ] def parse_args(): parser = argparse.ArgumentParser( description="Reformat C code to match project style", epilog="Call with no argument to reformat uncommitted code.", ) parser.add_argument( "commit", nargs="*", default="", help="Reformat all changes since this commit" ) parser.add_argument( "--debug", action="store_true", help="Display all launched commands" ) return parser.parse_args() def select_clang_version(formatters): for formatter in formatters: try: ret = subprocess.check_call( [formatter, "--help"], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) if ret == 0: return formatter except FileNotFoundError: continue print("No clang formatter installed") sys.exit(1) ## Entry Point ## if __name__ == "__main__": args = parse_args() base = os.getenv("GITHUB_BASE_REF") if base: base = f"origin/{base}" else: if args.commit: base = args.commit[0] else: base = "HEAD" cmd = ["git", "describe", base] if args.debug: print(cmd) ret = subprocess.run(cmd, capture_output=True) if ret.returncode: if args.debug: print(ret.stderr) base = "HEAD" print(f"Reformatting code against {base}") formatter = select_clang_version(CLANG_DIFF_FORMATTERS) cmd = ["git", "diff", "-U0", base] if args.debug: print(cmd) ret = subprocess.run(cmd, capture_output=True, text=True) if ret.returncode: print(f"Failed to run {cmd}\n{ret.stderr.strip()}") sys.exit(1) cmd = [formatter, "-i", "-regex", "^.*\\.(c|h|proto)$", "-p1"] if args.debug: print(cmd) ret = subprocess.run(cmd, input=ret.stdout, capture_output=True, text=True) if ret.returncode: print(f"Failed to run {cmd}\n{ret.stderr.strip()}") sys.exit(1) sys.exit(0) fwupd-2.0.10/contrib/sbom.cdx.json000066400000000000000000000020051501337203100167570ustar00rootroot00000000000000{ "bomFormat": "CycloneDX", "specVersion": "1.6", "version": 1, "metadata": { "authors": [ { "name": "@VCS_SBOM_AUTHORS@" } ] }, "components": [ { "type": "library", "bom-ref": "pkg:github/fwupd/fwupd@@VCS_TAG@", "cpe": "cpe:2.3:a:fwupd:fwupd:@VCS_TAG@:*:*:*:*:*:*:*", "name": "fwupd", "version": "@VCS_VERSION@", "description": "Firmware update daemon", "supplier": { "name": "fwupd developers", "url": [ "https://github.com/fwupd/fwupd/blob/main/MAINTAINERS" ] }, "authors": [ { "name": "@VCS_AUTHORS@" } ], "licenses": [ { "license": { "id": "LGPL-2.1-or-later" } } ], "externalReferences": [ { "type": "website", "url": "https://fwupd.org/" }, { "type": "vcs", "url": "https://github.com/fwupd/fwupd" } ] } ] } fwupd-2.0.10/contrib/setup000077500000000000000000000114161501337203100154430ustar00rootroot00000000000000#!/usr/bin/env -S bash -e # Setup the repository and local system for development cd "$(dirname "$0")/.." HELPER=./contrib/ci/fwupd_setup_helpers.py HELPER_ARGS="-y" rename_branch() { OLD=master NEW=main if git log $OLD >/dev/null 2>&1 && git remote get-url origin 2>&1 | grep fwupd/fwupd.git >/dev/null 2>&1; then echo "" read -p "Rename existing $OLD branch to $NEW? (y/N) " question if [ "$question" = "y" ]; then git branch -m $OLD $NEW git fetch origin git branch -u origin/$NEW $NEW git remote set-head origin -a fi fi } setup_deps() { read -p "Install build dependencies? (y/N) " question if [ "$question" = "y" ]; then python3 $HELPER install-dependencies $HELPER_ARGS -y fi } setup_run_wrappers() { BASE=../../contrib/ BIN=venv/bin/ TEMPLATE=${BASE}/launch-venv.sh # launch wrappers for F in fwupdtool fwupdmgr fwupd; do rm -f ${BIN}/${F} ln -s $TEMPLATE ${BIN}/${F} done # build wrapper rm -f ${BIN}/build-fwupd ln -s ${BASE}/build-venv.sh ${BIN}/build-fwupd rm -f ${BIN}/test-fwupd ln -s ${BASE}/test-venv.sh ${BIN}/test-fwupd } setup_vscode() { # Add default vscode settings and debug launcher SOURCED=./contrib/vscode TARGETD=./.vscode SETTINGS_F=settings.json LAUNCH_F=launch.json TASK_F=tasks.json for f in $SETTINGS_F $LAUNCH_F $TASK_F; do TEMPLATE=${SOURCED}/${f} TARGETF=${TARGETD}/${f} mkdir -p ${TARGETD} echo "Copy ${TEMPLATE} to ${TARGETF}." cp "${TEMPLATE}" "${TARGETF}" done } setup_git() { echo "Configuring git environment" git config include.path ../.gitconfig } install_pip() { package=$1 args=$2 if ! python3 -m pip install $package $args; then python3 $HELPER install-pip $HELPER_ARGS -y fi #try once more python3 -m pip install $package } setup_virtualenv() { echo "Setting up virtualenv" if ! which virtualenv >/dev/null 2>&1; then install_pip virtualenv fi virtualenv --system-site-packages venv --prompt fwupd source venv/bin/activate cat >> venv/bin/activate << EOF echo "To build or rebuild fwupd within development environment run:" echo "" echo "# build-fwupd" echo "" echo "To run the test suite run:" echo "" echo "# test-fwupd" echo "" echo "To run any tool under gdbserver add DEBUG=1 to env, for example:" echo "" echo "# DEBUG=1 fwupdtool get-devices" echo "" echo "To leave fwupd development environment run:" echo "" echo "# deactivate" . data/bash-completion/fwupdtool . data/bash-completion/fwupdmgr export MANPATH=./venv/dist/share/man: EOF } setup_precommit() { echo "Configuring pre-commit hooks" install_pip pre-commit pre-commit install } setup_prepush() { echo "" read -p "Run tests locally before pushing to remote branches? THIS WILL SLOW DOWN EVERY PUSH but reduce the risk of failing CI. (y/N) " question if [ "$question" = "y" ]; then pre-commit install -t pre-push else pre-commit uninstall -t pre-push fi } check_markdown() { python3 $HELPER test-markdown } check_jinja2() { python3 $HELPER test-jinja2 } check_meson() { python3 $HELPER test-meson } detect_os() { for i in "$@"; do case $i in --os=*) OS="${i#*=}" shift ;; --debug) DEBUG=1 shift ;; *) ;; esac done if [ -z $OS ]; then OS=$(python3 $HELPER detect-profile) if [ -z "$OS" ]; then install_pip distro OS=$(python3 $HELPER detect-profile) fi echo "Using OS profile $OS to setup" fi if [ -n "$OS" ];then HELPER_ARGS="$HELPER_ARGS --os $OS" fi if [ -n "$DEBUG" ]; then set -x HELPER_ARGS="$HELPER_ARGS --debug" fi } howto() { echo "" echo "To enter fwupd development environment run:" echo "" echo "# source venv/bin/activate" echo "" } #already setup if [ -f venv/bin/build-fwupd ]; then echo "$0 has already been run" howto exit 0 fi PYTHON_VERSION=$(python3 --version 2>/dev/null) if [ -z "$PYTHON_VERSION" ]; then echo "Install python3 to run this script" >&2 exit 1 fi #needed for arguments for some commands detect_os "$@" #if interactive install build deps and prepare environment if [ -t 2 ]; then case $OS in debian|ubuntu|arch|fedora|darwin) setup_deps ;; esac rename_branch fi setup_virtualenv setup_run_wrappers check_markdown check_jinja2 setup_vscode setup_git check_meson setup_precommit #needs to be after pre-commit is sourced if [ -t 2 ]; then setup_prepush fi howto fwupd-2.0.10/contrib/snap/000077500000000000000000000000001501337203100153135ustar00rootroot00000000000000fwupd-2.0.10/contrib/snap/README.md000066400000000000000000000015631501337203100165770ustar00rootroot00000000000000# Snap support Snaps are containerised software packages that are simple to create and install. They auto-update and are safe to run. And because they bundle their dependencies, they work on all major Linux systems without modification. ## stable vs unstable Two yaml files are distributed: * snapcraft.yaml This uses tarball releases for all dependencies and what is currently in tree for fwupd. * snapcraft-master.yaml This uses git for most dependencies and may be considered unstable. ## Building Builds can be performed using snapcraft: ```shell # snapcraft cleanbuild ``` ## Installing A "classic" snap is produced, and locally built snaps can be installed like this: ```shell # snap install fwupd_daily_amd64.snap --dangerous --classic ``` The `--dangerous` flag is because snaps built locally are not signed. Snaps distributed by a store will not need this flag. fwupd-2.0.10/contrib/snap/fwupd-command000077500000000000000000000042051501337203100200030ustar00rootroot00000000000000#!/bin/sh export XDG_CACHE_HOME=$SNAP_USER_COMMON/.cache mkdir -p $XDG_CACHE_HOME export GIO_MODULE_DIR=$XDG_CACHE_HOME/gio-modules export XDG_DATA_DIRS="$SNAP/usr/share" export FWUPD_LOCKDIR=/run/lock/snap.fwupd export FWUPD_POLKIT_NOCHECK=1 export FWUPD_HOSTFS_ROOT=/var/lib/snapd/hostfs #determine architecture if [ "$SNAP_ARCH" = "amd64" ]; then ARCH="x86_64-linux-gnu" elif [ "$SNAP_ARCH" = "armhf" ]; then ARCH="arm-linux-gnueabihf" elif [ "$SNAP_ARCH" = "arm64" ]; then ARCH="aarch64-linux-gnu" else ARCH="$SNAP_ARCH-linux-gnu" fi # re-generate gio modules in local cache needs_update=true if [ -f $SNAP_USER_DATA/.last_revision ]; then # shellcheck source=/dev/null . $SNAP_USER_DATA/.last_revision 2>/dev/null fi if [ "$SNAP_DESKTOP_LAST_REVISION" = "$SNAP_REVISION" ]; then needs_update=false fi if [ $needs_update = true ]; then if [ -f $SNAP/usr/lib/$ARCH/glib-2.0/gio-querymodules ]; then rm -rf $GIO_MODULE_DIR mkdir -p $GIO_MODULE_DIR ln -s $SNAP/usr/lib/$ARCH/gio/modules/*.so $GIO_MODULE_DIR $SNAP/usr/lib/$ARCH/glib-2.0/gio-querymodules $GIO_MODULE_DIR fi echo "SNAP_DESKTOP_LAST_REVISION=$SNAP_REVISION" > $SNAP_USER_DATA/.last_revision fi # Setup directory structures if command was run as root if [ "$(id -u)" = "0" ]; then CONF_DIR="${SNAP_COMMON}/var/etc/fwupd" REMOTE_DIR="${SNAP_COMMON}/var/lib/fwupd/remotes.d" SHARE_DIR="${SNAP_COMMON}/share/fwupd/remotes.d/vendor/firmware" mkdir -p ${CONF_DIR} ${REMOTE_DIR} ${SHARE_DIR} # copy a writable fwupd.conf for users to use if [ ! -f "${CONF_DIR}/fwupd.conf" ]; then cp "$SNAP/etc/fwupd/fwupd.conf" "${CONF_DIR}/fwupd.conf" fi # Migrate remotes from "old" snap guidance and from immutable directory for BASE in "${SNAP}/etc/fwupd/remotes.d" "${SNAP_USER_DATA}/etc/fwupd/remotes.d/"; do if [ ! -d "${BASE}" ]; then continue fi for P in ${BASE}/*.conf; do REMOTE=$(basename $P) # vendor.conf doesn't make sense in snap if [ "${REMOTE}" = "vendor.conf" ]; then continue fi if [ ! -f "${REMOTE_DIR}/${REMOTE}" ]; then cp ${P} "${REMOTE_DIR}" fi done done fi exec "$@" fwupd-2.0.10/contrib/snap/fwupd.wrapper000077500000000000000000000001041501337203100200400ustar00rootroot00000000000000#!/bin/sh exec "$SNAP/fwupd-command" $SNAP/libexec/fwupd/fwupd "$@" fwupd-2.0.10/contrib/snap/fwupdmgr.wrapper000077500000000000000000000000751501337203100205550ustar00rootroot00000000000000#!/bin/sh exec "$SNAP/fwupd-command" $SNAP/bin/fwupdmgr "$@" fwupd-2.0.10/contrib/snap/fwupdtool.wrapper000077500000000000000000000000761501337203100207460ustar00rootroot00000000000000#!/bin/sh exec "$SNAP/fwupd-command" $SNAP/bin/fwupdtool "$@" fwupd-2.0.10/contrib/snap/snapcraft.yaml000066400000000000000000000215361501337203100201670ustar00rootroot00000000000000name: fwupd adopt-info: fwupd summary: A standalone version of fwupd to install newer firmware updates description: | This is a tool that can be used to install firmware updates on devices not yet supported by the version of fwupd distributed with the OS. grade: stable confinement: strict base: core24 license: LGPL-2.1-or-later platforms: amd64: slots: fwupd: interface: fwupd fwupd-dbus: interface: dbus bus: system name: org.freedesktop.fwupd plugs: fwupdmgr: interface: fwupd polkit: interface: polkit action-prefix: org.freedesktop.fwupd apps: fwupdtool: command: fwupdtool.wrapper plugs: [bluez, udisks2, modem-manager, upower-observe, network, hardware-observe, home, opengl, raw-usb] slots: [fwupd] completer: share/bash-completion/completions/fwupdtool environment: FWUPD_HOSTDIR: /var/lib/snapd/hostfs fwupd: command: fwupd.wrapper daemon: dbus slots: [fwupd] plugs: [bluez, udisks2, modem-manager, upower-observe, polkit, network, hardware-observe, home, opengl, raw-usb] daemon-scope: system activates-on: - fwupd-dbus environment: FWUPD_HOSTDIR: /var/lib/snapd/hostfs fwupdmgr: command: fwupdmgr.wrapper plugs: [fwupdmgr, home, network, polkit, shutdown] completer: share/bash-completion/completions/fwupdmgr refresh: command: fwupdmgr.wrapper refresh daemon: oneshot plugs: [fwupdmgr, network] timer: "00:00-24:00/24" parts: #needed for UEFI plugin to build UX labels build-introspection: plugin: nil stage-packages: - python3-gi-cairo override-stage: | craftctl default prime: - -etc - -usr - -var pkttyagent: plugin: nil stage-packages: - polkitd - libpolkit-agent-1-0 prime: - usr/bin/pkttyagent - usr/lib/*/libpolkit-agent-1.so* fwupd: plugin: meson meson-parameters: [--prefix=/, -Defi_binary=false, -Ddocs=disabled, -Dbuild=all, -Dintrospection=disabled, -Dman=false, -Dpython=/usr/bin/python3, -Dplugin_flashrom=disabled, -Defi_os_dir=fwupd, "-Dlibxmlb:gtkdoc=false", "-Dlibxmlb:introspection=false", "-Dlibjcat:man=false", "-Dlibjcat:gtkdoc=false", "-Dlibjcat:introspection=false", "-Dlibjcat:tests=false"] source: . source-type: git override-build: | ${CRAFT_PART_SRC}/contrib/ci/fwupd_setup_helpers.py test-meson craftctl default for name in ${CRAFT_PART_INSTALL}/lib/*/fwupd-*/libfwupd*; do mv ${name} `dirname ${name}`/.. ;\ done # fixes up commands like fwupdtool -> fwupd.fwupdtool sed -i "s,\(complete -F _fwupd[a-z]*\) \(fwupd.*\),\1 fwupd.\2,; \ s,\(command.*\)\(fwupdtool\),\1fwupd.\2,; \ s,\(command.*\)\(fwupdmgr\),\1fwupd.\2," \ ${CRAFT_PART_INSTALL}/share/bash-completion/completions/* # make installed tests runnable sed -i "s,\(fwupdmgr\),fwupd.\1,; \ s,\(fwupdtool\),fwupd.\1," \ ${CRAFT_PART_INSTALL}/share/installed-tests/fwupd/*.sh sed -i "s,\(/share\),/snap/fwupd/current/\1," \ ${CRAFT_PART_INSTALL}/share/installed-tests/fwupd/fwupdtool.sh sed -i "s,\(/libexec\),/snap/fwupd/current/\1," \ ${CRAFT_PART_INSTALL}/share/installed-tests/fwupd/fwupd.sh sed -i 's,^MetadataURI=.*$,MetadataURI=file:///snap/fwupd/current/share/installed-tests/fwupd/fwupd-tests.xml,' "${CRAFT_PART_INSTALL}/share/fwupd/remotes.d/fwupd-tests.conf" # fixes up dbus service for classic snap sed -i 's!SystemdService=\(.*\)!SystemdService=snap.fwupd.fwupd.service!' \ ${CRAFT_PART_INSTALL}/share/dbus-1/system-services/org.freedesktop.fwupd.service cp -R /usr/libexec/fwupd/efi ${CRAFT_PART_INSTALL}/libexec/fwupd cp /usr/lib/shim/shimx64.efi.signed ${CRAFT_PART_INSTALL}/libexec/fwupd/efi/ sed -i 's,^MetadataURI=.*$,MetadataURI=file:///var/snap/fwupd/common/share/fwupd/remotes.d/vendor/firmware,' "${CRAFT_PART_INSTALL}/etc/fwupd/remotes.d/vendor-directory.conf" rm -rf ${CRAFT_PART_INSTALL}/share/installed-tests/fwupd/tests/bios-attrs override-pull: | craftctl default craftctl set version=$(git describe HEAD --always) build-packages: - bash-completion - curl - fwupd-signed - fwupd-unsigned - git - gettext - gnu-efi - fwupd-unsigned-dev - hwdata - libcurl4-openssl-dev - libarchive-dev - libcbor-dev - libcairo-dev - libglib2.0-dev - libgnutls28-dev - libgpgme11-dev - libusb-1.0-0-dev - libjson-glib-dev - libjcat-dev - liblzma-dev - libpango1.0-dev - libpolkit-gobject-1-dev - libprotobuf-c-dev - libsqlite3-dev - libsystemd-dev - libtss2-dev - libmm-glib-dev - libqmi-glib-dev - libmbim-glib-dev - libreadline-dev - locales - meson - modemmanager - pkg-config - python3-pip - python3-cairo - python3-gi - python3-jinja2 - python3-dbusmock - protobuf-c-compiler - shim-signed - systemd - umockdev - uuid-dev stage-packages: - libnghttp2-14 - libpsl5 - librtmp1 - libarchive13 - libcurl4 - libcbor0.10 - libassuan0 - libffi8 - libjcat1 - liblzma5 - libusb-1.0-0 - libgpgme11 - libprotobuf-c1 - libpolkit-gobject-1-0 - libreadline8t64 - libtss2-esys-3.0.2-0 - libtss2-fapi1 - libtss2-mu-4.0.1-0t64 - libtss2-rc0 - libtss2-sys1 - libtss2-tcti-cmd0 - libtss2-tcti-device0 - libtss2-tcti-mssim0 - libtss2-tcti-swtpm0 - libtss2-tctildr0 - glib-networking - libglib2.0-bin - libglib2.0-0 - libmm-glib0 - libqmi-glib5 - libmbim-glib4 - libduktape207 prime: # we explicitly don't want /usr/bin/gpgconf # this will cause gpgme to error finding it # but that also avoids trying to use nonexistent # /usr/bin/gpg2 - -etc/dconf/db - -etc/grub.d - -etc/systemd/system - -libexec/installed-tests - -share/fwupd/*.py - -share/fish - -root - -usr/bin - -usr/sbin - -usr/libexec - -usr/share/man - -usr/share/GConf - -etc/X11 - -etc/ldap - -etc/logcheck - -usr/lib/dconf - -usr/lib/gcc - -usr/lib/glib-networking - -usr/lib/gnupg - -usr/lib/gnupg2 - -usr/lib/sasl2 - -usr/lib/systemd - -usr/lib/*/audit - -usr/share/bash-completion - -usr/share/dbus-1 - -usr/share/gnupg - -usr/share/glib-2.0/schemas - -usr/share/session-migration - -usr/share/upstart - -usr/share/X11 - -include - -lib/systemd - -lib/udev - -lib/*/pkgconfig - -usr/share/lintian - -usr/share/pkgconfig - -usr/share/polkit-1 - -usr/share/vala - -usr/share/doc - -usr/share/gnupg2 - -usr/share/info - -usr/share/gir-1.0 - -usr/share/upstart - -usr/lib/*/libicu* - -usr/lib/*/pkgconfig - -usr/lib/*/libnpth* - -usr/lib/*/libgthread* - -usr/lib/*/libksba* - -usr/lib/*/gio/modules/libdconfsettings.so - -usr/lib/systemd/user/dconf.service - -usr/share/dbus-1/services/ca.desrt.dconf.service - -usr/lib/*/libdconf.so.1.0.0 after: [build-introspection] build-environment: - PYTHONPATH: "${CRAFT_STAGE}/usr/lib/python3/dist-packages" update-mime: plugin: make source: contrib/snap/update-mime stage-packages: - shared-mime-info - gsettings-desktop-schemas - libxml2 prime: - -usr/bin - -usr/share/doc - -usr/share/doc-base - -usr/share/man - -usr/share/lintian - -usr/share/pkgconfig - -usr/share/GConf - -usr/lib/dconf - -usr/lib/*/gio/modules/libdconfsettings.so - -usr/lib/systemd/user/dconf.service - -usr/share/dbus-1/services/ca.desrt.dconf.service - -usr/lib/*/libdconf.so.1.0.0 after: [fwupd] fwupd-wrappers: plugin: dump source: contrib/snap stage: - fwupd-command - fwupdtool.wrapper - fwupd.wrapper - fwupdmgr.wrapper policy: plugin: nil after: - fwupd override-build: | mkdir -p "${CRAFT_PART_INSTALL}/meta/polkit/polkit.fwupd/" cp "${CRAFT_STAGE}/share/polkit-1/actions/org.freedesktop.fwupd.policy" \ "${CRAFT_PART_INSTALL}/meta/polkit/polkit.org.freedesktop.fwupd.policy" fwupd-2.0.10/contrib/snap/update-mime/000077500000000000000000000000001501337203100175225ustar00rootroot00000000000000fwupd-2.0.10/contrib/snap/update-mime/Makefile000066400000000000000000000002021501337203100211540ustar00rootroot00000000000000build: true install: update-mime-database ../install/usr/share/mime glib-compile-schemas ../install/usr/share/glib-2.0/schemas fwupd-2.0.10/contrib/standalone-installer/000077500000000000000000000000001501337203100204755ustar00rootroot00000000000000fwupd-2.0.10/contrib/standalone-installer/README.md000066400000000000000000000004361501337203100217570ustar00rootroot00000000000000# Standalone installer This is a script that will build a standalone installer around the fwupd snap or flatpak. This can be used for distributing updates that use fwupd on machines without networking and the needed tools. For usage instructions, view: ```shell ./make.py --help ``` fwupd-2.0.10/contrib/standalone-installer/assets/000077500000000000000000000000001501337203100217775ustar00rootroot00000000000000fwupd-2.0.10/contrib/standalone-installer/assets/header.py000066400000000000000000000246621501337203100236130ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2017 Dell, Inc. # # SPDX-License-Identifier: LGPL-2.1-or-later # from base64 import b64decode import io import os import subprocess import sys import shutil import tempfile import zipfile TAG = b"#\x00" def parse_args(): import argparse parser = argparse.ArgumentParser(description="Self extracting firmware updater") parser.add_argument("--directory", help="Directory to extract to") parser.add_argument( "--cleanup", action="store_true", help="Remove tools when done with installation", ) parser.add_argument( "--verbose", action="store_true", help="Run the tool in verbose mode" ) parser.add_argument( "--allow-reinstall", action="store_true", help="Allow re-installing existing firmware versions", ) parser.add_argument( "--allow-older", action="store_true", help="Allow downgrading firmware versions" ) parser.add_argument( "command", choices=["install", "extract"], help="Command to run" ) args = parser.parse_args() return args def error(msg): print(msg) sys.exit(1) def bytes_slicer(length, source): start = 0 stop = length while start < len(source): yield source[start:stop] start = stop stop += length def get_zip(): script = os.path.realpath(__file__) bytes_out = io.BytesIO() with open(script, "rb") as source: for line in source: if not line.startswith(TAG): continue bytes_out.write(b64decode(line[len(TAG) : -1])) return bytes_out def unzip(destination): zipf = get_zip() source = zipfile.ZipFile(zipf, "r") for item in source.namelist(): # extract handles the sanitization source.extract(item, destination) def copy_cabs(source, target): if not os.path.exists(target): os.makedirs(target) cabs = [] for root, dirs, files in os.walk(source): for f in files: if f.endswith(".cab"): origf = os.path.join(root, f) shutil.copy(origf, target) cabs.append(os.path.join(target, f)) return cabs def install_snap(directory, verbose, allow_reinstall, allow_older, uninstall): app = "fwupd" common = f"/root/snap/{app}/common" # check if snap is installed with open(os.devnull, "w") as devnull: subprocess.run(["snap"], check=True, stdout=devnull, stderr=devnull) # check existing installed cmd = ["snap", "list", app] with open(os.devnull, "w") as devnull: if verbose: print(cmd) ret = subprocess.run(cmd, stdout=devnull, stderr=devnull) if ret.returncode == 0: cmd = ["snap", "remove", app] if verbose: print(cmd) subprocess.run(cmd, check=True) # install the snap cmd = ["snap", "ack", os.path.join(directory, "fwupd.assert")] if verbose: print(cmd) subprocess.run(cmd, check=True) cmd = ["snap", "install", "--classic", os.path.join(directory, "fwupd.snap")] if verbose: print(cmd) subprocess.run(cmd, check=True) # copy the CAB files cabs = copy_cabs(directory, common) # run the snap for cab in cabs: cmd = [f"{app}.fwupdmgr", "install", cab] if allow_reinstall: cmd += ["--allow-reinstall"] if allow_older: cmd += ["--allow-older"] if verbose: cmd += ["--verbose"] print(cmd) subprocess.run(cmd) # remove copied cabs for f in cabs: os.remove(f) # cleanup if uninstall: cmd = ["snap", "remove", app] if verbose: print(cmd) subprocess.run(cmd) def install_flatpak(directory, verbose, allow_reinstall, allow_older, uninstall): app = "org.freedesktop.fwupd" common = f"{os.getenv('HOME')}/.var/app/{app}" with open(os.devnull, "w") as devnull: if not verbose: output = devnull else: output = None # look for dependencies dep = "org.gnome.Platform/x86_64/3.30" repo = "flathub" repo_url = "https://flathub.org/repo/flathub.flatpakrepo" cmd = ["flatpak", "info", dep] if verbose: print(cmd) ret = subprocess.run(cmd, stdout=output, stderr=output) # not installed if ret.returncode != 0: # look for remotes cmd = ["flatpak", "remote-info", repo, dep] if verbose: print(cmd) ret = subprocess.run(cmd, stdout=output, stderr=output) # not enabled, enable it if ret.returncode != 0: cmd = ["flatpak", "remote-add", repo, repo_url] if verbose: print(cmd) ret = subprocess.run(cmd, stderr=output) # install dep cmd = ["flatpak", "install", repo, dep] if verbose: print(cmd) ret = subprocess.run(cmd) # check existing installed cmd = ["flatpak", "info", app] if verbose: print(cmd) ret = subprocess.run(cmd, stdout=output, stderr=output) if ret.returncode == 0: cmd = ["flatpak", "remove", app] if verbose: print(cmd) subprocess.run(cmd, check=True) # install the flatpak cmd = ["flatpak", "install", os.path.join(directory, "fwupd.flatpak")] if verbose: print(cmd) subprocess.run(cmd, check=True) # copy the CAB files cabs = copy_cabs(directory, common) # run command for cab in cabs: cmd = ["flatpak", "run", app, "install", cab] if allow_reinstall: cmd += ["--allow-reinstall"] if allow_older: cmd += ["--allow-older"] if verbose: cmd += ["--verbose"] print(cmd) subprocess.run(cmd) # remove copied cabs for f in cabs: os.remove(f) # cleanup if uninstall: cmd = ["flatpak", "remove", app] if verbose: print(cmd) subprocess.run(cmd) # Check which package to use # - return False to use packaged version # - return True for snap/flatpak def use_included_version(minimum_version): try: import apt except ModuleNotFoundError: return True cache = apt.Cache() pkg = cache.get("fwupd") version = pkg.installed if not version: return True if minimum_version: if minimum_version > version: print( "fwupd %s is already installed but this package requires %s" % (version.version, minimum_version) ) else: print( "Using existing fwupd version %s already installed on system." % version.version ) return False else: print(f"fwupd {version.version} is installed and must be removed") return remove_packaged_version(pkg, cache) def remove_packaged_version(pkg, cache): res = False while True: res = input("Remove now (Y/N)? ") if res.lower() == "n": res = False break if res.lower() == "y": res = True break if res: pkg.mark_delete() res = cache.commit() if not res: raise Exception("Need to remove packaged version") return True def install_builtin(directory, verbose, allow_reinstall, allow_older): cabs = [] for root, dirs, files in os.walk(directory): for f in files: if f.endswith(".cab"): cabs.append(os.path.join(root, f)) # run command for cab in cabs: cmd = ["fwupdmgr", "install", cab] if allow_reinstall: cmd += ["--allow-reinstall"] if allow_older: cmd += ["--allow-older"] if verbose: cmd += ["--verbose"] print(cmd) subprocess.run(cmd) def run_installation(directory, verbose, allow_reinstall, allow_older, uninstall): try_snap = False try_flatpak = False # determine if a minimum version was specified minimum_path = os.path.join(directory, "minimum") minimum = None if os.path.exists(minimum_path): with open(minimum_path) as rfd: minimum = rfd.read() if not use_included_version(minimum): install_builtin(directory, verbose, allow_reinstall, allow_older) return # determine what self extracting binary has if os.path.exists(os.path.join(directory, "fwupd.snap")) and os.path.exists( os.path.join(directory, "fwupd.assert") ): try_snap = True if os.path.exists(os.path.join(directory, "fwupd.flatpak")): try_flatpak = True if try_snap: try: install_snap(directory, verbose, allow_reinstall, allow_older, uninstall) return True except Exception: if verbose: print("Snap installation failed") if not try_flatpak: error("Snap installation failed") if try_flatpak: install_flatpak(directory, verbose, allow_reinstall, allow_older, uninstall) if __name__ == "__main__": args = parse_args() if "extract" in args.command: if args.allow_reinstall: error( "allow-reinstall argument doesn't make sense with command %s" % args.command ) if args.allow_older: error( f"allow-older argument doesn't make sense with command {args.command}" ) if args.cleanup: error(f"Cleanup argument doesn't make sense with command {args.command}") if args.directory is None: error("No directory specified") if not os.path.exists(args.directory): print(f"Creating {args.directory}") os.makedirs(args.directory) unzip(args.directory) else: if args.directory: error( "Directory argument %s doesn't make sense with command %s" % (args.directory, args.command) ) if os.getuid() != 0: error("This tool must be run as root") with tempfile.TemporaryDirectory(prefix="fwupd") as target: unzip(target) run_installation( target, args.verbose, args.allow_reinstall, args.allow_older, args.cleanup, ) fwupd-2.0.10/contrib/standalone-installer/make.py000077500000000000000000000115571501337203100220000ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2017 Dell, Inc. # # SPDX-License-Identifier: LGPL-2.1-or-later # from base64 import b64encode import io import os import subprocess import shutil import sys import tempfile import zipfile from assets.header import TAG def error(msg): print(msg) sys.exit(1) def parse_args(): import argparse parser = argparse.ArgumentParser( description="Generate a standalone firmware updater" ) parser.add_argument( "--disable-snap-download", action="store_true", help="Don't download support for snap", ) parser.add_argument( "--disable-flatpak-download", action="store_true", help="Don't download support for flatpak", ) parser.add_argument( "--snap-channel", help="Channel to download snap from (optional)" ) parser.add_argument( "--minimum", help="Use already installed fwupd version if at least this version" ) parser.add_argument( "cab", help="CAB file or directory containing CAB files to automatically install", ) parser.add_argument("target", help="target file to create") args = parser.parse_args() return args def bytes_slicer(length, source): start = 0 stop = length while start < len(source): yield source[start:stop] start = stop stop += length def generate_installer(directory, target): asset_base = os.path.join(os.path.dirname(os.path.realpath(__file__)), "assets") # header shutil.copy(os.path.join(asset_base, "header.py"), target) # zip file buffer = io.BytesIO() archive = zipfile.ZipFile(buffer, "a") for root, dirs, files in os.walk(directory): for f in files: source = os.path.join(root, f) archive_fname = source.split(directory)[1] archive.write(source, archive_fname) if "DEBUG" in os.environ: print(archive.namelist()) archive.close() with open(target, "ab") as bytes_out: encoded = b64encode(buffer.getvalue()) for section in bytes_slicer(64, encoded): bytes_out.write(TAG) bytes_out.write(section) bytes_out.write(b"\n") def download_snap(directory, channel): cmd = ["snap", "download", "fwupd"] if channel is not None: cmd += ["--channel", channel] if "DEBUG" in os.environ: print(cmd) subprocess.run(cmd, cwd=directory, check=True) for f in os.listdir(directory): # the signatures associated with the snap if f.endswith(".assert"): shutil.move( os.path.join(directory, f), os.path.join(directory, "fwupd.assert") ) # the snap binary itself elif f.endswith(".snap"): shutil.move( os.path.join(directory, f), os.path.join(directory, "fwupd.snap") ) def download_cab_file(directory, uri): cmd = ["wget", uri] if "DEBUG" in os.environ: print(cmd) subprocess.run(cmd, cwd=directory, check=True) def download_flatpak(directory): dep = "org.freedesktop.fwupd" flatpak_dir = os.path.join(os.getenv("HOME"), ".local", "share", "flatpak") verbose = "DEBUG" in os.environ # check if we have installed locally already or not if not os.path.exists(os.path.join(flatpak_dir, "app", dep)): # install into local user's repo cmd = [ "flatpak", "install", "--user", "https://www.flathub.org/repo/appstream/org.freedesktop.fwupd.flatpakref", "--no-deps", "-y", ] if verbose: print(cmd) subprocess.run(cmd, cwd=directory, check=True) # generate a bundle repo = os.path.join(flatpak_dir, "repo") cmd = ["flatpak", "build-bundle", repo, "fwupd.flatpak", dep, "stable"] if verbose: print(cmd) subprocess.run(cmd, cwd=directory, check=True) if __name__ == "__main__": args = parse_args() if not args.cab.startswith("http"): local = args.cab with tempfile.TemporaryDirectory(prefix="fwupd") as directory: if local: if not os.path.exists(local): error(f"{local} doesn't exist") if not os.path.isdir(local): shutil.copy(local, directory) else: for root, dirs, files in os.walk(local): for f in files: shutil.copy(os.path.join(root, f), directory) else: download_cab_file(directory, args.cab) if not args.disable_snap_download: download_snap(directory, args.snap_channel) if not args.disable_flatpak_download: download_flatpak(directory) if args.minimum: with open(os.path.join(directory, "minimum"), "w") as wfd: wfd.write(args.minimum) generate_installer(directory, args.target) fwupd-2.0.10/contrib/tartan.sh000077500000000000000000000004571501337203100162100ustar00rootroot00000000000000#!/bin/sh /usr/bin/scan-build-17 \ -load-plugin /usr/lib64/tartan/17.0/libtartan.so \ -disable-checker core.CallAndMessage \ -disable-checker core.NullDereference \ -disable-checker deadcode.DeadStores \ -disable-checker unix.Malloc \ -enable-checker tartan.GErrorChecker \ --status-bugs -v "$@" fwupd-2.0.10/contrib/test-venv.sh000077500000000000000000000012501501337203100166420ustar00rootroot00000000000000#!/bin/sh -e VENV=$(dirname $0)/.. BUILD=${VENV}/build INSTALLED_TESTS=${VENV}/dist/share/installed-tests/fwupd export G_TEST_BUILDDIR=${INSTALLED_TESTS} export G_TEST_SRCDIR=${INSTALLED_TESTS} export GI_TYPELIB_PATH=${BUILD}/libfwupd export LD_LIBRARY_PATH=${BUILD}/libfwupd export DAEMON_BUILDDIR=${BUILD}/src export PATH=${VENV}/bin:$PATH echo "Build time test suite" ninja -C ${BUILD} test echo "Testing fwupdtool.sh" ${INSTALLED_TESTS}/fwupdtool.sh echo "Starting daemon" G_DEBUG=fatal-criticals ${VENV}/bin/fwupd --verbose --no-timestamp >fwupd.txt 2>&1 & echo "Testing fwupd.sh" ${INSTALLED_TESTS}/fwupd.sh # artifacts from the test run rm -f fwupd.txt fwupdtool.txt fwupd-2.0.10/contrib/upload-smc-license.py000066400000000000000000000032751501337203100204170ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2022 Kai Michaelis # # SPDX-License-Identifier: LGPL-2.1-or-later import sys import argparse from urllib import request import base64 import ssl if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( "-u", "--user", type=str, help="Redfish user name", default="ADMIN", ) parser.add_argument( "-p", "--password", type=str, help="Redfish user password", default="ADMIN", ) parser.add_argument( "-f", "--file", type=str, help="License file", ) parser.add_argument("hostname", type=str, help="BMC IP of hostname") args = parser.parse_args() license = "" if len(sys.argv) == 1: print("hostname required") sys.exit(1) if not args.file: print("License:") license = sys.stdin.read() else: with open(args.file) as fd: license = fd.read() sslctx = ssl.create_default_context() sslctx.check_hostname = False sslctx.verify_mode = ssl.CERT_NONE url = ( f"https://{args.hostname}/redfish/v1/Managers/1/LicenseManager/ActivateLicense" ) auth = str( base64.b64encode(bytes(f"{args.user}:{args.password}", "latin1")), encoding="latin1", ) headers = { "Content-Type": "application/json", "Authorization": f"Basic {auth}", } req = request.Request(url, headers=headers, data=bytes(license, "latin1")) resp = request.urlopen(req, context=sslctx) if resp.status < 300: print("Success") else: print("Failed") sys.exit(-1) fwupd-2.0.10/contrib/vscode/000077500000000000000000000000001501337203100156355ustar00rootroot00000000000000fwupd-2.0.10/contrib/vscode/README.md000066400000000000000000000003561501337203100171200ustar00rootroot00000000000000# Using Visual Studio Code to debug This directory contains a collection of assets to make debugging using Visual Studio Code easier. When following the [build instructions](../../docs/building.md) they will be setup in the environment. fwupd-2.0.10/contrib/vscode/launch.json000066400000000000000000000033331501337203100200040ustar00rootroot00000000000000{ "version": "0.2.0", "configurations": [ { "name": "gdbserver (fwupdtool)", "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/venv/dist/bin/fwupdtool", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], "miDebuggerServerAddress": "localhost:9091", "externalConsole": false, "MIMode": "gdb", "setupCommands": [ { "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true } ] }, { "name": "gdbserver (fwupd)", "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/venv/dist/libexec/fwupd/fwupd", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], "miDebuggerServerAddress": "localhost:9091", "externalConsole": false, "MIMode": "gdb", "setupCommands": [ { "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true } ] }, { "name": "gdbserver (fwupdmgr)", "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/venv/dist/bin/fwupdmgr", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], "miDebuggerServerAddress": "localhost:9091", "externalConsole": false, "MIMode": "gdb", "setupCommands": [ { "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true } ] } ] } fwupd-2.0.10/contrib/vscode/settings.json000066400000000000000000000001041501337203100203630ustar00rootroot00000000000000{ "editor.tabSize": 8, "mesonbuild.buildFolder": "venv/build" } fwupd-2.0.10/contrib/vscode/tasks.json000066400000000000000000000013551501337203100176610ustar00rootroot00000000000000{ "version": "2.0.0", "tasks": [ { "label": "build-fwupd", "type": "shell", "command": "/bin/bash -c 'source venv/bin/activate; build-fwupd'", "problemMatcher": [], "group": { "kind": "build", "isDefault": true } }, { "label": "test-fwupd", "type": "shell", "command": "/bin/bash -c 'source venv/bin/activate; test-fwupd'", "problemMatcher": [], "group": { "kind": "test", "isDefault": true } }, { "label": "gdbserver-fwupd", "type": "shell", "command": "/bin/bash -c 'source venv/bin/activate; DEBUG=1 fwupd -v'", "problemMatcher": [], "group": { "kind": "none" } } ] } fwupd-2.0.10/data/000077500000000000000000000000001501337203100136235ustar00rootroot00000000000000fwupd-2.0.10/data/bash-completion/000077500000000000000000000000001501337203100167075ustar00rootroot00000000000000fwupd-2.0.10/data/bash-completion/fwupdmgr000066400000000000000000000226631501337203100204760ustar00rootroot00000000000000_fwupdmgr_cmd_list=( 'activate' 'block-firmware' 'clear-results' 'disable-remote' 'device-test' 'device-emulate' 'device-wait' 'downgrade' 'download' 'enable-remote' 'emulation-tag' 'emulation-untag' 'emulation-load' 'emulation-save' 'get-approved-firmware' 'get-bios-setting' 'get-blocked-firmware' 'get-details' 'get-devices' 'get-history' 'get-plugins' 'get-releases' 'get-remotes' 'get-results' 'get-topology' 'get-updates' 'get-upgrades' 'get-plugins' 'inhibit' 'uninhibit' 'install' 'local-install' 'modify-config' 'modify-remote' 'quit' 'reinstall' 'refresh' 'report-devices' 'report-history' 'report-export' 'security' 'security-fix' 'security-undo' 'set-approved-firmware' 'set-bios-setting' 'switch-branch' 'sync' 'unlock' 'unblock-firmware' 'update' 'upgrade' 'verify' 'verify-update' '--version' ) _fwupdmgr_opts=( '--verbose' '--allow-reinstall' '--allow-older' '--allow-branch-switch' '--force' '--assume-yes' '--no-history' '--no-unreported-check' '--no-metadata-check' '--no-reboot-check' '--no-safety-check' '--no-remote-check' '--no-security-fix' '--only-emulated' '--show-all' '--sign' '--filter' '--filter-release' '--disable-ssl-strict' '--p2p' '--json' '--download-retries' ) bios_get_opts=( '--no-authenticate' '--json' '--verbose' ) bios_set_opts=( '--no-reboot-check' '--json' '--verbose' ) fwupd_modify_config_sections=( 'fwupd' 'msr' 'redfish' 'test' 'thunderbolt' 'uefi_capsule' 'dell_kestrel' ) fwupd_modify_config_opts=( 'ArchiveSizeMax' 'ApprovedFirmware' 'BlockedFirmware' 'DisabledDevices' 'DisabledPlugins' 'EspLocation' 'EnumerateAllDevices' 'HostBkc' 'IdleTimeout' 'IgnorePower' 'OnlyTrusted' 'P2pPolicy' 'ReleaseDedupe' 'ReleasePriority' 'ShowDevicePrivate' 'TestDevices' 'TrustedReports' 'TrustedUids' 'UpdateMotd' 'UriSchemes' 'VerboseDomains' ) test_modify_config_opts=( 'AnotherWriteRequired' 'CompositeChild' 'DecompressDelay' 'NeedsActivation' 'NeedsReboot' 'RegistrationSupported' 'RequestDelay' 'RequestSupported' 'VerifyDelay' 'WriteDelay' 'WriteSupported' ) redfish_modify_config_opts=( 'CACheck' 'IpmiDisableCreateUser' 'ManagerResetTimeout' 'Password' 'Uri' 'Username' 'UserUri' ) uefi_capsule_modify_config_opts=( 'DisableCapsuleUpdateOnDisk' 'DisableShimForSecureBoot' 'EnableEfiDebugging' 'EnableGrubChainLoad' 'OverrideESPMountPoint' 'RebootCleanup' 'RequireESPFreeSpace' 'ScreenWidth' 'ScreenHeight' ) dell_kestrel_modify_config_opts=( 'UpdateOnDisconnect' ) reset_config_opts=( 'fwupd' 'msr' 'redfish' 'test' 'thunderbolt' 'uefi_capsule' ) _show_file_in_dir() { local files files="$(ls 2>/dev/null)" COMPREPLY+=( $(compgen -W "${files}" -- "$cur") ) } _show_bios_get_modifiers() { COMPREPLY+=( $(compgen -W '${bios_get_opts[@]}' -- "$cur") ) } _show_bios_set_modifiers() { COMPREPLY+=( $(compgen -W '${bios_set_opts[@]}' -- "$cur") ) } _show_filters() { local flags flags="$(command fwupdtool get-device-flags 2>/dev/null)" COMPREPLY+=( $(compgen -W "${flags}" -- "$cur") ) } _show_modifiers() { COMPREPLY+=( $(compgen -W '${_fwupdmgr_opts[@]}' -- "$cur") ) } _show_bios_settings() { if ! command -v jq &> /dev/null; then return 0 fi local attr attr="$(command fwupdmgr get-bios-setting --json --no-authenticate 2>/dev/null | jq '.BiosSettings | .[] | .Name')" COMPREPLY+=( $(compgen -W "${attr}" -- "$cur") ) } _show_bios_settings_possible() { if ! command -v jq &> /dev/null; then return 0 fi local attr attr="$(command fwupdmgr get-bios-setting "$1" --json --no-authenticate 2>/dev/null | jq '.BiosSettings | .[] | .BiosSettingPossibleValues | .[]')" COMPREPLY+=( $(compgen -W "${attr}" -- "$cur") ) } _show_device_ids() { if ! command -v jq &> /dev/null; then return 0 fi local description description="$(command fwupdmgr get-devices --json 2>/dev/null | jq '.Devices | .[] | .DeviceId')" COMPREPLY+=( $(compgen -W "${description}" -- "$cur") ) } _show_plugins() { if ! command -v jq &> /dev/null; then return 0 fi local plugins plugins="$(command fwupdmgr get-plugins --json 2>/dev/null | jq '.Plugins | .[] | .Name')" COMPREPLY+=( $(compgen -W "${plugins}" -- "$cur") ) } _show_release_versions() { if ! command -v jq &> /dev/null; then return 0 fi local description description="$(command fwupdmgr get-releases "$1" --json 2>/dev/null | jq '.Releases[].Version')" COMPREPLY+=( $(compgen -W "${description}" -- "$cur") ) } _show_fwupd_modify_sections() { COMPREPLY+=( $(compgen -W '${fwupd_modify_config_sections[@]}' -- "$cur") ) } _show_fwupd_modify_config() { COMPREPLY+=( $(compgen -W '${fwupd_modify_config_opts[@]}' -- "$cur") ) } _show_test_modify_config() { COMPREPLY+=( $(compgen -W '${test_modify_config_opts[@]}' -- "$cur") ) } _show_redfish_modify_config() { COMPREPLY+=( $(compgen -W '${redfish_modify_config_opts[@]}' -- "$cur") ) } _show_uefi_capsule_modify_config() { COMPREPLY+=( $(compgen -W '${uefi_capsule_modify_config_opts[@]}' -- "$cur") ) } _show_dell_kestrel_modify_config() { COMPREPLY+=( $(compgen -W '${dell_kestrel_modify_config_opts[@]}' -- "$cur") ) } _show_reset_config() { COMPREPLY+=( $(compgen -W '${reset_config_opts[@]}' -- "$cur") ) } _show_remotes() { local remotes remotes="$(command fwupdmgr get-remotes --json 2>/dev/null | jq '.Remotes | .[] | .Id')" COMPREPLY+=( $(compgen -W "${remotes}" -- "$cur") ) } _fwupdmgr() { local cur prev command arg args COMPREPLY=() _get_comp_words_by_ref cur prev _get_first_arg _count_args case $prev in --filter) _show_filters return 0 ;; esac case $arg in activate|clear-results|downgrade|get-releases|get-results|unlock|verify|verify-update|get-updates|switch-branch|update|upgrade|report-export) #device ID if [[ "$args" = "2" ]]; then _show_device_ids fi ;; get-bios-settings|get-bios-setting) #bios settings (no limit) _show_bios_settings _show_bios_get_modifiers return 0 ;; set-bios-setting) if [[ "$prev" = "--json" ]]; then _show_file_in_dir "$prev" return 0 fi count=$(($((args)) % 2)) #allow setting a single bios setting at a time if [[ $count == 0 ]]; then _show_bios_settings fi #possible values (only works for enumeration though) if [[ $count == 1 ]]; then _show_bios_settings_possible "$prev" return 0 fi _show_bios_set_modifiers return 0 ;; get-plugins) return 0 ;; get-details) #find files if [[ "$args" = "2" ]]; then _filedir fi ;; device-test) #find files if [[ "$args" = "2" ]]; then _filedir fi ;; install) #device ID if [[ "$args" = "2" ]]; then _show_device_ids #version elif [[ "$args" = "3" ]]; then _show_release_versions "$prev" fi ;; local-install) #find files if [[ "$args" = "2" ]]; then _filedir #device ID or modifiers elif [[ "$args" = "3" ]]; then _show_device_ids _show_modifiers fi ;; modify-remote) #find remotes if [[ "$args" = "2" ]]; then _show_remotes #add key elif [[ "$args" = "3" ]]; then local keys keys="$(command fwupdmgr get-remotes | command awk -v pattern="Remote ID:.*${prev}$" '$0~pattern{show=1; next}/Remote/{show=0}{gsub(/:.*/,"")}show')" COMPREPLY+=( $(compgen -W "${keys}" -- "$cur") ) fi ;; enable-remote) #find remotes if [[ "$args" = "2" ]]; then _show_remotes fi ;; disable-remote) #find remotes if [[ "$args" = "2" ]]; then _show_remotes fi ;; modify-config) if [[ "$args" = "2" ]]; then _show_fwupd_modify_sections return 0 elif [[ "$args" = "3" ]]; then case $prev in test) _show_test_modify_config ;; msr) COMPREPLY+=( $(compgen -W 'DelayedActivation MinimumSmeKernelVersion' -- "$cur") ) ;; thunderbolt) COMPREPLY+=( $(compgen -W 'DelayedActivation MinimumKernelVersion' -- "$cur") ) ;; fwupd) _show_fwupd_modify_config ;; redfish) _show_redfish_modify_config ;; uefi_capsule) _show_uefi_capsule_modify_config ;; dell_kestrel) _show_dell_kestrel_modify_config ;; esac return 0 elif [[ "$args" = "4" ]]; then case $prev in EnumerateAllDevices|OnlyTrusted|IgnorePower|UpdateMotd|ShowDevicePrivate|ReleaseDedupe|TestDevices) COMPREPLY=( $(compgen -W "True False" -- "$cur") ) ;; AnotherWriteRequired|NeedsActivation|NeedsReboot|RegistrationSupported|RequestSupported|WriteSupported) COMPREPLY=( $(compgen -W "True False" -- "$cur") ) ;; ReleasePriority) COMPREPLY=( $(compgen -W "local remote" -- "$cur") ) ;; UriSchemes) COMPREPLY=( $(compgen -W "file https http ipfs file;https;http;ipfs file;https;http https;http" -- "$cur") ) ;; P2pPolicy) COMPREPLY=( $(compgen -W "none metadata firmware metadata,firmware" -- "$cur") ) ;; IdleTimeout|ArchiveSizeMax|HostBkc|TrustedUids) ;; ApprovedFirmware|BlockedFirmware) ;; DisabledDevices) _show_device_ids ;; DisabledPlugins) _show_plugins ;; EspLocation) ;; TrustedReports) ;; VerboseDomains) ;; esac return 0 fi ;; reset-config) #find files if [[ "$args" = "2" ]]; then _show_reset_config return 0 fi ;; refresh) #find first file if [[ "$args" = "2" ]]; then _filedir #find second file elif [[ "$args" = "3" ]]; then _filedir #find remote ID elif [[ "$args" = "4" ]]; then _show_remotes fi ;; *) #find first command if [[ "$args" = "1" ]]; then COMPREPLY=( $(compgen -W '${_fwupdmgr_cmd_list[@]}' -- "$cur") ) fi ;; esac #modifiers _show_modifiers return 0 } complete -F _fwupdmgr fwupdmgr fwupd-2.0.10/data/bash-completion/fwupdtool000066400000000000000000000200571501337203100206610ustar00rootroot00000000000000_fwupdtool_cmd_list=( 'activate' 'build-cabinet' 'clear-history' 'disable-remote' 'disable-test-devices' 'efiboot-create' 'efiboot-delete' 'efiboot-hive' 'efiboot-info' 'efiboot-next' 'efiboot-order' 'efivar-list' 'efivar-files' 'enable-remote' 'enable-test-devices' 'emulation-tag' 'emulation-untag' 'emulation-load' 'esp-list' 'esp-mount' 'esp-unmount' 'firmware-build' 'firmware-convert' 'firmware-export' 'firmware-extract' 'firmware-parse' 'firmware-sign' 'firmware-patch' 'get-bios-setting' 'get-updates' 'get-upgrades' 'get-details' 'get-firmware-types' 'get-firmware-gtypes' 'get-device-flags' 'get-devices' 'get-history' 'get-plugins' 'get-remotes' 'get-report-metadata' 'get-topology' 'get-version-formats' 'hwids' 'update' 'upgrade' 'install' 'install-blob' 'modify-config' 'modify-remote' 'monitor' 'reinstall' 'security' 'security-fix' 'security-undo' 'set-bios-setting' 'switch-branch' 'self-sign' 'smbios-dump' 'attach' 'detach' 'firmware-dump' 'firmware-read' 'refresh' 'verify-update' 'watch' 'unbind-driver' 'bind-driver' 'export-hwids' 'reboot-cleanup' 'vercmp' ) _fwupdtool_opts=( '--verbose' '--allow-reinstall' '--allow-older' '--filter' '--filter-release' '--force' '--json' '--show-all' '--plugins' '--prepare' '--cleanup' '--filter' '--method' '--disable-ssl-strict' '--no-safety-check' '--no-search' '--ignore-checksum' '--ignore-vid-pid' '--ignore-requirements' '--save-backends' ) fwupd_modify_config_sections=( 'fwupd' 'msr' 'redfish' 'test' 'thunderbolt' 'uefi_capsule' 'dell_kestrel' ) fwupd_modify_config_opts=( 'ArchiveSizeMax' 'ApprovedFirmware' 'BlockedFirmware' 'DisabledDevices' 'DisabledPlugins' 'EspLocation' 'EnumerateAllDevices' 'HostBkc' 'IdleTimeout' 'IgnorePower' 'OnlyTrusted' 'P2pPolicy' 'ReleaseDedupe' 'ReleasePriority' 'ShowDevicePrivate' 'TestDevices' 'TrustedReports' 'TrustedUids' 'UpdateMotd' 'UriSchemes' 'VerboseDomains' ) test_modify_config_opts=( 'AnotherWriteRequired' 'CompositeChild' 'DecompressDelay' 'NeedsActivation' 'NeedsReboot' 'RegistrationSupported' 'RequestDelay' 'RequestSupported' 'VerifyDelay' 'WriteDelay' 'WriteSupported' ) redfish_modify_config_opts=( 'CACheck' 'IpmiDisableCreateUser' 'ManagerResetTimeout' 'Password' 'Uri' 'Username' 'UserUri' ) uefi_capsule_modify_config_opts=( 'DisableCapsuleUpdateOnDisk' 'DisableShimForSecureBoot' 'EnableEfiDebugging' 'EnableGrubChainLoad' 'OverrideESPMountPoint' 'RebootCleanup' 'RequireESPFreeSpace' 'ScreenWidth' 'ScreenHeight' ) dell_kestrel_modify_config_opts=( 'UpdateOnDisconnect' ) reset_config_opts=( 'fwupd' 'msr' 'redfish' 'test' 'thunderbolt' 'uefi_capsule' ) _show_fwupd_modify_sections() { COMPREPLY+=( $(compgen -W '${fwupd_modify_config_sections[@]}' -- "$cur") ) } _show_fwupd_modify_config() { COMPREPLY+=( $(compgen -W '${fwupd_modify_config_opts[@]}' -- "$cur") ) } _show_test_modify_config() { COMPREPLY+=( $(compgen -W '${test_modify_config_opts[@]}' -- "$cur") ) } _show_redfish_modify_config() { COMPREPLY+=( $(compgen -W '${redfish_modify_config_opts[@]}' -- "$cur") ) } _show_uefi_capsule_modify_config() { COMPREPLY+=( $(compgen -W '${uefi_capsule_modify_config_opts[@]}' -- "$cur") ) } _show_dell_kestrel_modify_config() { COMPREPLY+=( $(compgen -W '${dell_kestrel_modify_config_opts[@]}' -- "$cur") ) } _show_reset_config() { COMPREPLY+=( $(compgen -W '${reset_config_opts[@]}' -- "$cur") ) } _show_remotes() { local remotes remotes="$(command fwupdtool get-remotes --json 2>/dev/null | jq '.Remotes | .[] | .Id')" COMPREPLY+=( $(compgen -W "${remotes}" -- "$cur") ) } _show_filters() { local flags flags="$(command fwupdtool get-device-flags 2>/dev/null)" COMPREPLY+=( $(compgen -W "${flags}" -- "$cur") ) } _show_firmware_types() { local firmware_types firmware_types="$(command fwupdtool get-firmware-types 2>/dev/null)" COMPREPLY+=( $(compgen -W "${firmware_types}" -- "$cur") ) } _show_device_ids() { if ! command -v jq &> /dev/null; then return 0 fi local description description="$(command jq '.Devices | .[] | .DeviceId' @localstatedir@/cache/fwupd/devices.json 2>/dev/null)" COMPREPLY+=( $(compgen -W "${description}" -- "$cur") ) } _show_plugins() { if ! command -v jq &> /dev/null; then return 0 fi local plugins plugins="$(command fwupdtool get-plugins --json 2>/dev/null | jq '.Plugins | .[] | .Name')" COMPREPLY+=( $(compgen -W "${plugins}" -- "$cur") ) } _show_modifiers() { COMPREPLY+=( $(compgen -W '${_fwupdtool_opts[@]}' -- "$cur") ) } _fwupdtool() { local cur prev command arg args COMPREPLY=() _get_comp_words_by_ref cur prev _get_first_arg _count_args case $prev in --plugins) _show_plugins return 0 ;; --filter) _show_filters return 0 ;; esac case $arg in get-details|install|install-blob|firmware-dump|firmware-read) #find files if [[ "$args" = "2" ]]; then _filedir #device ID elif [[ "$args" = "3" ]]; then _show_device_ids fi ;; emulation-load) #find files if [[ "$args" = "2" ]]; then _filedir fi ;; attach|detach|activate|verify-update|reinstall|get-updates) #device ID if [[ "$args" = "2" ]]; then _show_device_ids fi ;; firmware-parse|firmware-patch) #find files if [[ "$args" = "2" ]]; then _filedir #firmware_type elif [[ "$args" = "3" ]]; then _show_firmware_types fi ;; firmware-convert) #file in if [[ "$args" = "2" ]]; then _filedir #file out elif [[ "$args" = "3" ]]; then _filedir #firmware_type in elif [[ "$args" = "4" ]]; then _show_firmware_types #firmware_type out elif [[ "$args" = "5" ]]; then _show_firmware_types fi ;; modify-remote) #find remotes if [[ "$args" = "2" ]]; then _show_remotes #add key elif [[ "$args" = "3" ]]; then local keys keys="$(command fwupdtool get-remotes | command awk -v pattern="Remote ID:.*${prev}$" '$0~pattern{show=1; next}/Remote/{show=0}{gsub(/:.*/,"")}show')" COMPREPLY+=( $(compgen -W "${keys}" -- "$cur") ) fi ;; enable-remote) #find remotes if [[ "$args" = "2" ]]; then _show_remotes fi ;; disable-remote) #find remotes if [[ "$args" = "2" ]]; then _show_remotes fi ;; modify-config) if [[ "$args" = "2" ]]; then _show_fwupd_modify_sections return 0 elif [[ "$args" = "3" ]]; then case $prev in test) _show_test_modify_config ;; msr) COMPREPLY+=( $(compgen -W 'DelayedActivation MinimumSmeKernelVersion' -- "$cur") ) ;; thunderbolt) COMPREPLY+=( $(compgen -W 'DelayedActivation MinimumKernelVersion' -- "$cur") ) ;; fwupd) _show_fwupd_modify_config ;; redfish) _show_redfish_modify_config ;; uefi_capsule) _show_uefi_capsule_modify_config ;; dell_kestrel) _show_dell_kestrel_modify_config ;; esac return 0 elif [[ "$args" = "4" ]]; then case $prev in EnumerateAllDevices|OnlyTrusted|IgnorePower|UpdateMotd|ShowDevicePrivate|ReleaseDedupe|TestDevices) COMPREPLY=( $(compgen -W "True False" -- "$cur") ) ;; AnotherWriteRequired|NeedsActivation|NeedsReboot|RegistrationSupported|RequestSupported|WriteSupported) COMPREPLY=( $(compgen -W "True False" -- "$cur") ) ;; ReleasePriority) COMPREPLY=( $(compgen -W "local remote" -- "$cur") ) ;; UriSchemes) COMPREPLY=( $(compgen -W "file https http ipfs file;https;http;ipfs file;https;http https;http" -- "$cur") ) ;; P2pPolicy) COMPREPLY=( $(compgen -W "none metadata firmware metadata,firmware" -- "$cur") ) ;; IdleTimeout|ArchiveSizeMax|HostBkc|TrustedUids) ;; ApprovedFirmware|BlockedFirmware) ;; DisabledDevices) _show_device_ids ;; DisabledPlugins) _show_plugins ;; EspLocation) ;; TrustedReports) ;; VerboseDomains) ;; esac return 0; fi ;; reset-config) #find files if [[ "$args" = "2" ]]; then _show_reset_config return 0 fi ;; *) #find first command if [[ "$args" = "1" ]]; then COMPREPLY=( $(compgen -W '${_fwupdtool_cmd_list[@]}' -- "$cur") ) fi ;; esac #modifiers _show_modifiers return 0 } complete -F _fwupdtool fwupdtool fwupd-2.0.10/data/bash-completion/meson.build000066400000000000000000000012211501337203100210450ustar00rootroot00000000000000if bashcomp.found() completions_dir = bashcomp.get_variable(pkgconfig: 'completionsdir', pkgconfig_define: bashcomp.version().version_compare('>= 2.10') ? ['datadir', datadir] : ['prefix', prefix], ) con = configuration_data() con.set('localstatedir', localstatedir) configure_file( input: 'fwupdtool', output: 'fwupdtool', configuration: con, install: true, install_dir: completions_dir, ) if build_daemon configure_file( input: 'fwupdmgr', output: 'fwupdmgr', configuration: con, install: true, install_dir: completions_dir, ) endif # build_daemon endif # bashcomp.found() fwupd-2.0.10/data/bios-settings.d/000077500000000000000000000000001501337203100166375ustar00rootroot00000000000000fwupd-2.0.10/data/bios-settings.d/README.md000066400000000000000000000020151501337203100201140ustar00rootroot00000000000000# BIOS Settings On supported machines fwupd can enforce BIOS settings policy so that a user's desired settings are configured at bootup and prevent fwupd clients from changing them. ## JSON policies A policy file can be created using `fwupdmgr`. First determine what settings you want to enforce by running: ```shell # fwupdmgr get-bios-settings ``` After you have identified settings, create a JSON payload by listing them on the command line. Any number of attributes can be listed. For example for the BIOS setting `WindowsUEFIFirmwareUpdate` you would create a policy file like this: ```shell # fwupdmgr get-bios-settings --json WindowsUEFIFirmwareUpdate > ~/foo.json ``` Now examine `~/foo.json` and modify the `BiosSettingCurrentValue` key to your desired value. Lastly place this policy file into `/etc/fwupd/bios-settings.d`. Any number of policies is supported, and they will be examined in alphabetical order. The next time that fwupd is started it will load this policy and ensure that no fwupd clients change it. fwupd-2.0.10/data/bios-settings.d/meson.build000066400000000000000000000002351501337203100210010ustar00rootroot00000000000000if build_standalone and host_machine.system() == 'linux' install_data('README.md', install_dir: join_paths(sysconfdir, 'fwupd', 'bios-settings.d') ) endif fwupd-2.0.10/data/cfi.quirk000066400000000000000000000113061501337203100154420ustar00rootroot00000000000000# No Manufacturer [CFI\FLASHID_0020] Name = M25PxxA/xx CfiDeviceCmdChipErase = 0xC7 CfiDeviceCmdSectorErase = 0x00 [CFI\FLASHID_00BF] Name = PCT/SST25VFxxx/xxxA CfiDeviceCmdReadId = 0x90 CfiDeviceCmdChipErase = 0x60 CfiDeviceCmdSectorErase = 0x20 [CFI\FLASHID_009D] Name = PM25LDxxx CfiDeviceCmdReadId = 0x90 CfiDeviceCmdChipErase = 0xC7 CfiDeviceCmdSectorErase = 0xD7 [CFI\FLASHID_009D] Name = PM25LVxxx CfiDeviceCmdReadId = 0xAB CfiDeviceCmdChipErase = 0xC7 CfiDeviceCmdSectorErase = 0xD7 [CFI\FLASHID_00EF] Name = W25XxxBV/W25XxxCL CfiDeviceCmdChipErase = 0xC7 CfiDeviceCmdSectorErase = 0x20 # Fujitsu [CFI\FLASHID_04] Vendor = Fujitsu # Atmel [CFI\FLASHID_1F] Vendor = Atmel [CFI\FLASHID_1F65] Name = AT25F512A/B CfiDeviceCmdReadId = 0x15 CfiDeviceCmdChipErase = 0x62 CfiDeviceCmdSectorErase = 0x00 FirmwareSizeMax = 0x10000 # EON [CFI\FLASHID_1C] Vendor = EON [CFI\FLASHID_1C31] Name = EN25Fxx CfiDeviceCmdChipErase = 0x60 CfiDeviceCmdSectorErase = 0x20 # ST [CFI\FLASHID_20] Vendor = ST # Catalyst [CFI\FLASHID_31] Vendor = Catalyst # AMIC [CFI\FLASHID_37] Vendor = AMIC [CFI\FLASHID_3730] Name = A25Lxxx CfiDeviceCmdChipErase = 0xc7 CfiDeviceCmdSectorErase = 0x20 # SyncMOS [CFI\FLASHID_40] Vendor = SyncMOS # ESI [CFI\FLASHID_4A] Vendor = ESI # Alliance [CFI\FLASHID_52] Vendor = Alliance # Tenx [CFI\FLASHID_5E] Vendor = Tenx # Sanyo [CFI\FLASHID_62] Vendor = Sanyo # AMIC [CFI\FLASHID_7F] Vendor = AMIC # Puya Semiconductor [CFI\FLASHID_85] Vendor = Puya [CFI\FLASHID_854012] Name = P25Q21H FirmwareSizeMax = 0x40000 [CFI\FLASHID_854011] Name = P25Q11H FirmwareSizeMax = 0x20000 [CFI\FLASHID_854010] Name = P25Q06H FirmwareSizeMax = 0x10000 [CFI\FLASHID_852014] Name = PY25Q80HB FirmwareSizeMax = 0x100000 [CFI\FLASHID_89] Vendor = Intel # Elite [CFI\FLASHID_8C] Vendor = Elite # Texas Instruments [CFI\FLASHID_97] Vendor = Texas Instruments # PMC [CFI\FLASHID_9D] Vendor = PMC [CFI\FLASHID_7F9D20] Name = PM25Lx512x FirmwareSizeMax = 0x10000 [CFI\FLASHID_7F9D21] Name = PM25LD010 FirmwareSizeMax = 0x20000 # Fudan [CFI\FLASHID_A1] Vendor = Fudan [CFI\FLASHID_A131] Name = FM25xxx CfiDeviceBlockSize = 0x10000 CfiDeviceSectorSize = 0x1000 CfiDevicePageSize = 0x100 CfiDeviceCmdBlockErase = 0xd8 [CFI\FLASHID_A13110] Name = FM25W04 FirmwareSizeMax = 0x80000 [CFI\FLASHID_A13111] Name = FM25F01 FirmwareSizeMax = 0x20000 # Hyundai [CFI\FLASHID_AD] Vendor = Hyundai # Sharp [CFI\FLASHID_B0] Vendor = Sharp # SST [CFI\FLASHID_BF] Vendor = SST # Macronix [CFI\FLASHID_C2] Vendor = Macronix [CFI\FLASHID_C220] Name = MX25Lxxx/xxxC/xxxE CfiDeviceCmdChipErase = 0x60 CfiDeviceCmdSectorErase = 0x20 CfiDeviceCmdBlockErase = 0xd8 CfiDeviceBlockSize = 0x10000 CfiDeviceSectorSize = 0x1000 CfiDevicePageSize = 0x100 [CFI\FLASHID_C22011] Name = MX25L1006E FirmwareSizeMax = 0x20000 [CFI\FLASHID_C22012] Name = MX25V2033F FirmwareSizeMax = 0x40000 [CFI\FLASHID_C22016] Name = MX25L3236F FirmwareSizeMax = 0x400000 [CFI\FLASHID_C222] Name = MX25Lxxx1E CfiDeviceCmdChipErase = 0x60 CfiDeviceCmdSectorErase = 0x20 [CFI\FLASHID_C22312] Name = MX25V2035F FirmwareSizeMax = 0x40000 # GigaDevice [CFI\FLASHID_C8] Vendor = GigaDevice [CFI\FLASHID_C840] Name = GD25Qxxx CfiDeviceCmdChipErase = 0xC7 CfiDeviceCmdSectorErase = 0x20 CfiDeviceCmdBlockErase = 0xd8 CfiDeviceBlockSize = 0x10000 CfiDeviceSectorSize = 0x1000 CfiDevicePageSize = 0x100 [CFI\FLASHID_C84011] Name = GD25D10B FirmwareSizeMax = 0x20000 [CFI\FLASHID_C84012] Name = GD25Q20C FirmwareSizeMax = 0x40000 [CFI\FLASHID_C84016] Name = GD25Q32C FirmwareSizeMax = 0x400000 [CFI\FLASHID_C84018] Name = GD25Q127C FirmwareSizeMax = 0x1000000 # Nantronics [CFI\FLASHID_D5] Vendor = Nantronics # Winbond [CFI\FLASHID_DA] Vendor = Winbond # Winbond (ex Nexcom) [CFI\FLASHID_EF] Vendor = Winbond [CFI\FLASHID_EF3010] Name = W25X05CL FirmwareSizeMax = 0x10000 [CFI\FLASHID_EF3011] Name = W25X10CL FirmwareSizeMax = 0x20000 [CFI\FLASHID_EF3012] Name = W25X20X FirmwareSizeMax = 0x40000 [CFI\FLASHID_EF3013] Name = W25X40 FirmwareSizeMax = 0x80000 [CFI\FLASHID_EF3014] Name = W25X80 FirmwareSizeMax = 0x100000 [CFI\FLASHID_EF3015] Name = W25X16 FirmwareSizeMax = 0x200000 [CFI\FLASHID_EF3016] Name = W25X32 FirmwareSizeMax = 0x400000 [CFI\FLASHID_EF3017] Name = W25X64 FirmwareSizeMax = 0x800000 [CFI\FLASHID_EF4013] Name = W25Q40 FirmwareSizeMax = 0x80000 [CFI\FLASHID_EF4014] Name = W25Q80 FirmwareSizeMax = 0x100000 [CFI\FLASHID_EF4015] Name = W25Q16 FirmwareSizeMax = 0x200000 [CFI\FLASHID_EF4016] Name = W25Q32 FirmwareSizeMax = 0x400000 [CFI\FLASHID_EF4017] Name = W25Q64 FirmwareSizeMax = 0x800000 [CFI\FLASHID_EF4018] Name = W25Q128 FirmwareSizeMax = 0x1000000 [CFI\FLASHID_EF4019] Name = W25Q256 FirmwareSizeMax = 0x2000000 [CFI\FLASHID_EF4020] Name = W25Q512 FirmwareSizeMax = 0x4000000 # Fidelix [CFI\FLASHID_F8] Vendor = Fidelix fwupd-2.0.10/data/device-tests/000077500000000000000000000000001501337203100162225ustar00rootroot00000000000000fwupd-2.0.10/data/device-tests/caldigit-element.json000066400000000000000000000016471501337203100223340ustar00rootroot00000000000000{ "name": "CalDigit Element Hub", "interactive": false, "steps": [ { "url": "cc7e51c0f8acb853adf6ca25e99cd6ea41e47f4b2da75a1922bdcc814f05c5a7-CalDigit-Element_DMC15-TBT36.cab", "components": [ { "version": "36.0", "guids": [ "3e4c9daf-ad32-5470-b2f7-a14557c96c71" ] }, { "version": "0.0.0.15", "guids": [ "c15b1b1a-502a-535a-99a5-f929a3cf9593" ] } ] }, { "url": "b8fa86d745d9c8f4f207c7782a4a09c7381ad19e3733cc8574acde36d250c88a-CalDigit-Element_DMC16-TBT40.cab", "components": [ { "version": "40.84", "guids": [ "3e4c9daf-ad32-5470-b2f7-a14557c96c71" ] }, { "version": "0.0.0.16", "guids": [ "c15b1b1a-502a-535a-99a5-f929a3cf9593" ] } ] } ] } fwupd-2.0.10/data/device-tests/dell-wd19tb.json000066400000000000000000000022721501337203100211500ustar00rootroot00000000000000{ "name": "Dell WD19TB Dock", "interactive": false, "steps": [ { "url": "c05bacfd8f73f30812559f14245b92a069c680caf300e961c78e00c985efe3e0-WD19FirmwareUpdateLinux_01.00.14.cab", "components": [ { "name": "ec", "version": "01.00.00.04", "guids": [ "cd357cf1-40b2-5d87-b8df-bb2dd82774aa" ] }, { "name": "mst", "version": "05.04.03", "guids": [ "89fec0b6-6b76-5008-b82c-5e5c6c164007" ] }, { "name": "pkg", "version": "01.00.14.01", "guids": [ "8ceeeffd-51b6-580c-9b75-69143227aff8" ] }, { "name": "tbt", "version": "43.00", "guids": [ "c94770ca-1773-592c-b20a-e87243bc7cd0" ] }, { "name": "usb1", "version": "01.21", "guids": [ "ac5b774c-b49d-566b-9255-85f0f7f8a4ed" ] }, { "name": "usb2", "version": "01.47", "guids": [ "568ffa1e-a0db-5287-9ea3-872b60f7730b" ] } ] } ] } fwupd-2.0.10/data/device-tests/meson.build000066400000000000000000000011701501337203100203630ustar00rootroot00000000000000install_data([ 'caldigit-element.json', 'dell-wd19tb.json', ], install_dir: join_paths(installed_test_datadir, 'device-tests'), ) con = configuration_data() enumeration_datadir = join_paths(installed_test_datadir, 'enumeration-data') con.set('enumeration_datadir', enumeration_datadir) foreach test: device_tests configure_file( input: test, output: '@BASENAME@.json', configuration: con, install: true, install_dir: join_paths(installed_test_datadir, 'device-tests'), ) endforeach foreach data: enumeration_data install_data(data, install_dir: enumeration_datadir, ) endforeach fwupd-2.0.10/data/ds20.quirk000066400000000000000000000001001501337203100154370ustar00rootroot00000000000000# Nostromo n52 gamepad [USB\VID_050D&PID_0815] Flags = no-probe fwupd-2.0.10/data/fish-completion/000077500000000000000000000000001501337203100167235ustar00rootroot00000000000000fwupd-2.0.10/data/fish-completion/fwupdmgr.fish000066400000000000000000000162341501337203100214370ustar00rootroot00000000000000function __fish_fwupdmgr_devices --description 'Get device IDs used by fwupdmgr' set -l ids (fwupdmgr get-devices | string replace -f -r '.*Device ID:\s*(.*)' '$1') set -l names (fwupdmgr get-devices | string replace -f -r '.*─(.*):$' '$1') for i in (seq (count $ids)) echo -e "$ids[$i]\t$names[$i]" end end function __fish_fwupdmgr_remotes --description 'Get remote IDs used by fwupdmgr' fwupdmgr get-remotes | string replace -f -r '.*Remote ID:\s*(.*)' '$1' end # complete options complete -c fwupdmgr -s h -l help -d 'Show help options' complete -c fwupdmgr -s v -l verbose -d 'Show extra debugging information' complete -c fwupdmgr -l version -d 'Show client and daemon versions' complete -c fwupdmgr -l allow-reinstall -d 'Allow reinstalling existing firmware versions' complete -c fwupdmgr -l allow-older -d 'Allow downgrading firmware versions' complete -c fwupdmgr -l allow-branch-switch -d 'Allow switching firmware branch' complete -c fwupdmgr -l force -d 'Force the action by relaxing some runtime checks' complete -c fwupdmgr -s y -l assume-yes -d 'Answer yes to all questions' complete -c fwupdmgr -l sign -d 'Sign the uploaded data with the client certificate' complete -c fwupdmgr -l no-unreported-check -d 'Do not check for unreported history' complete -c fwupdmgr -l no-metadata-check -d 'Do not check for old metadata' complete -c fwupdmgr -l no-reboot-check -d 'Do not check or prompt for reboot after update' complete -c fwupdmgr -l no-safety-check -d 'Do not perform device safety checks' complete -c fwupdmgr -l no-history -d 'Do not write to the history database' complete -c fwupdmgr -l no-security-fix -d 'Do not prompt to fix security issues' complete -c fwupdmgr -l show-all -d 'Show all results' complete -c fwupdmgr -l disable-ssl-strict -d 'Ignore SSL strict checks when downloading' complete -c fwupdmgr -l p2p -d 'Only use peer-to-peer networking when downloading files' complete -c fwupdmgr -l filter -d 'Filter with a set of device flags' # complete subcommands complete -c fwupdmgr -n '__fish_use_subcommand' -x -a activate -d 'Activate devices' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a block-firmware -d 'Blocks a specific firmware from being installed' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a clear-results -d 'Clears the results from the last update' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a disable-remote -d 'Disables a given remote' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a downgrade -d 'Downgrades the firmware on a device' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a device-wait -d 'Wait for a device to appear' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a enable-remote -d 'Enables a given remote' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a get-approved-firmware -d 'Gets the list of approved firmware' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a get-blocked-firmware -d 'Gets the list of blocked firmware' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a get-bios-setting -d 'Retrieve BIOS setting' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a get-details -d 'Gets details about a firmware file' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a get-devices -d 'Get all devices that support firmware updates' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a get-history -d 'Show history of firmware updates' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a get-plugins -d 'Get all enabled plugins registered with the system' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a get-releases -d 'Gets the releases for a device' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a get-remotes -d 'Gets the configured remotes' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a get-results -d 'Gets the results from the last update' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a get-updates -d 'Gets the list of updates for connected hardware' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a install -d 'Install a firmware file in cabinet format on this hardware' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a modify-config -d 'Modifies a daemon configuration value' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a modify-remote -d 'Modifies a given remote' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a refresh -d 'Refresh metadata from remote server' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a reinstall -d 'Reinstall current firmware on the device' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a report-history -d 'Share firmware history with the developers' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a report-export -d 'Export firmware history for manual upload' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a set-bios-setting -d 'Set a BIOS setting' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a security -d 'Gets the host security attributes' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a set-approved-firmware -d 'Sets the list of approved firmware' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a switch-branch -d 'Switch the firmware branch on the device' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a unblock-firmware -d 'Unblocks a specific firmware from being installed' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a unlock -d 'Unlocks the device for firmware access' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a update -d 'Updates all firmware to latest versions available' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a verify -d 'Checks cryptographic hash matches firmware' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a verify-update -d 'Update the stored cryptographic hash with current ROM contents' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a inhibit -d 'Inhibit the system to prevent upgrades' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a uninhibit -d 'Uninhibit the system to allow upgrades' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a quit -d 'Asks the daemon to quit' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a emulation-load -d 'Load device emulation data' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a emulation-save -d 'Save device emulation data' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a emulation-tag -d 'Adds devices to watch for future emulation' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a emulation-untag -d 'Removes devices to watch for future emulation' # commands exclusively consuming device IDs set -l deviceid_consumers activate clear-results downgrade get-releases get-results get-updates reinstall switch-branch unlock update verify verify-update # complete device IDs complete -c fwupdmgr -n "__fish_seen_subcommand_from $deviceid_consumers" -x -a "(__fish_fwupdmgr_devices)" # complete files and device IDs complete -c fwupdmgr -n "__fish_seen_subcommand_from install" -r -a "(__fish_fwupdmgr_devices)" # commands exclusively consuming remote IDs set -l remoteid_consumers disable-remote enable-remote modify-remote # complete remote IDs complete -c fwupdmgr -n "__fish_seen_subcommand_from $remoteid_consumers" -x -a "(__fish_fwupdmgr_remotes)" # complete files and remote IDs complete -c fwupdmgr -n "__fish_seen_subcommand_from refresh" -r -a "(__fish_fwupdmgr_remotes)" fwupd-2.0.10/data/fish-completion/meson.build000066400000000000000000000001461501337203100210660ustar00rootroot00000000000000install_data(['fwupdmgr.fish'], install_dir: join_paths(datadir, 'fish', 'vendor_completions.d'), ) fwupd-2.0.10/data/fwupd.conf000066400000000000000000000000631501337203100156160ustar00rootroot00000000000000[fwupd] # use `man 5 fwupd.conf` for documentation fwupd-2.0.10/data/fwupd.ico000066400000000000000000002040761501337203100154550ustar00rootroot00000000000000€€ ((€ ˜œš´”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ–››Ñ–šš?”™˜ý”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ý“›—B–›šÏ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ—œ›µ“™™Z”™˜þ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜û“™™(˜Ð¾ÂÁÿáäãÿáäãÿáäãÿÇÊÊÿ–œ›ì“™™UšŸžùáäãÿáäãÿáäãÿáäãÿšŸžù”š—X—œêÆÉÉÿáäãÿáäãÿáäãÿ¾ÂÁÿ™œÑ”˜˜r¡§¦øáäãÿáäãÿáäãÿàããÿ”™˜ÿ“——;˜ÐÈÌËÿôööÿôööÿôööÿÓÖÖÿ–œ›ì“™™UšŸžøôööÿôööÿôööÿôööÿ¡ ÷”š—X—œêÓÕÕÿôööÿôööÿôööÿÉÌÌÿ™œÑ”˜˜r¦ª©öôööÿôööÿôööÿôööÿ”™˜ÿ“——;”š—X—œœÕ—œœÖ—œœÖ—œœÖ—œœÖ•™˜øÉÌÌÿôööÿôööÿôööÿÔ××ÿ•š™ü—œœÖ—œœÖ—œœÖ—œœÖ—œœÖ”›™pŽŽŽ —›Å—œœÖ—œœÖ—œœÖ—œœÖ—œ›ãœ Ÿùôööÿôööÿôööÿôööÿ¢¡ù—›šä—œœÖ—œœÖ—œœÖ—œœÖ•›™Æ‹¢¢ •ššo—œœÖ—œœÖ—œœÖ—œœÖ—œœÖ•™˜ûÓÖÖÿôööÿôööÿôööÿÊÍÌÿ•™˜ø—œœÖ—œœÖ—œœÖ—œœÖ˜Õ•˜˜Y™™™—œ›Ì—œœÖ—œœÖ—œœÖ—œœÖ—›šè§«ª÷ôööÿôööÿôööÿôööÿ”™˜ÿ—œß—œœÖ—œœÖ—œœÖ—œœÖ—››¸•˜˜‹¥©¨üÍÑÑÿÍÑÑÿÍÑÑÿÍÑÑÿÍÑÑÿêììÿôööÿôööÿôööÿîññÿÎÒÑÿÍÑÑÿÍÑÑÿÍÑÑÿÍÑÑÿ­±±ÿ–››­““”™˜ÿÆÉÉÿÍÑÑÿÍÑÑÿÍÑÑÿÍÑÑÿØÜÜÿôööÿôööÿôööÿôööÿÙÜÜÿÍÑÑÿÍÑÑÿÍÑÑÿÍÑÑÿÆÊÉÿ”™˜ÿ’››–››ª¬±°ÿÍÑÑÿÍÑÑÿÍÑÑÿÍÑÑÿÎÒÑÿîððÿôööÿôööÿôööÿêììÿÍÑÑÿÍÑÑÿÍÑÑÿÍÑÑÿÍÑÑÿ¦«ªü”š˜’——6”™˜ÿÍÐÏÿÍÑÑÿÍÑÑÿÍÑÑÿÍÑÑÿÝàßÿôööÿôööÿôööÿôööÿÕØ×ÿÍÑÑÿÍÑÑÿÍÑÑÿÍÑÑÿÀÄÃÿ”™˜þ•˜˜‹±µ´úôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿ½ÁÀÿ–››­““”™˜ÿçééÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿèêêÿ”™˜ÿ’››–››ª¼ÀÀÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿ±·¶ú”š˜’——6”™˜ÿñôôÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿÝàßÿ”™˜þ•˜˜‹±µ´úôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿ½ÁÀÿ–››­““”™˜ÿçééÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿèêêÿ”™˜ÿ’››–››ª¼ÀÀÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿ±·¶ú”š˜’——6”™˜ÿñôôÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿÝàßÿ”™˜þ•˜˜‹±µ´úôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿ½ÁÀÿ–››­““”™˜ÿçééÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿèêêÿ”™˜ÿ’››–››ª¼ÀÀÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿ±·¶ú”š˜’——6”™˜ÿñôôÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿÝàßÿ”™˜þ“››!”™™p“™˜¹”™˜í•š™ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜×”˜˜¸”™™p“››!‘™™”™˜™”™˜ü”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜þ•™˜ª’™™#•˜˜^”™˜÷”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ö“˜˜\”˜˜”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ•™™”š—b”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘””ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ“’ÿ‘””ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™™_”šš&”™˜ù”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ†‡‡ÿc\^ÿMADÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿMAEÿb\_ÿ†ˆ‡ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ø•œ•$“™˜­”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ“˜—ÿlhjÿG;?ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG;?ÿmijÿ“˜—ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”š˜«“™™(”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿkgiÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿlhjÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜þ”šš&”š˜y”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ„……ÿH;?ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿH;?ÿ„††ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™™u”š˜¿”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿaZ\ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿa[]ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜¾”™˜Þ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿL@CÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿL@Dÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜Ý”™˜÷”™˜ÿ”™˜ÿ”™˜ÿ”“ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ”“ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜÷”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿH;?ÿH;?ÿH;?ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿH;?ÿH;?ÿH;?ÿG;?ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG;?ÿH;?ÿH;?ÿH;?ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿH;?ÿH;?ÿH;?ÿH;?ÿH;?ÿH;?ÿH;?ÿH;?ÿH;?ÿH;?ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿH;?ÿH;?ÿH;?ÿH;?ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿH;?ÿH;?ÿH;?ÿH;?ÿH;?ÿH;?ÿH;?ÿH;?ÿH;?ÿH;?ÿH;?ÿH;?ÿH;?ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿptsÿŠÿŠÿoqqÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿOHJÿ€„ƒÿŠÿˆ‹ÿc`aÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿa]_ÿˆ‹ŠÿŠÿ†„ÿQJLÿG:>ÿG:>ÿG:>ÿG:>ÿXRTÿ„‡†ÿŠÿŠÿŠÿŠÿŠÿŠÿŠÿŠÿoqpÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿpsqÿŠÿŠÿqssÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿLBEÿx|{ÿŠÿŠÿŠÿŠÿŠÿŠÿŠÿŠÿŠÿŠÿŠÿ|ÿNEHÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ†Š‰ÿÚßÞÿÚßÞÿz|{ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿOHJÿ¸½»ÿÚßÞÿÒØ×ÿgefÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿeacÿÏÔÓÿÚßÞÿ¼ÁÀÿRJLÿG:>ÿG:>ÿG:>ÿG:>ÿZTVÿÃÈÇÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿ{}}ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ~€ÿÚßÞÿÚßÞÿ‚…„ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿLBEÿŸ¤¢ÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿ­±±ÿNEHÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ†Š‰ÿÚßÞÿÚßÞÿz|{ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿOHJÿ¸½»ÿÚßÞÿÒØ×ÿgefÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿeacÿÏÔÓÿÚßÞÿ¼ÁÀÿRJLÿG:>ÿG:>ÿG:>ÿG:>ÿZTVÿÃÈÇÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿ{}}ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ~€ÿÚßÞÿÚßÞÿ‚…„ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿLBEÿŸ¤¢ÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿ­±±ÿNEHÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ†Š‰ÿÚßÞÿÚßÞÿz|{ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿOHJÿ¸½»ÿÚßÞÿÕÚÙÿ…ƒÿ‚‚ÿ‚‚ÿihhÿG:>ÿG:>ÿhggÿ‚‚ÿ‚‚ÿ€„ƒÿÒØ×ÿÚßÞÿ¼ÁÀÿRJLÿG:>ÿcabÿ‚‚ÿ‚‚ÿwzxÿqssÿstuÿstuÿstuÿstuÿstuÿstuÿstuÿstuÿosrÿ‚‚ÿ‚‚ÿosqÿG:>ÿG:>ÿ~€ÿÚßÞÿÚßÞÿ‚…„ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿLBEÿŸ¤¢ÿÚßÞÿÚßÞÿy|{ÿstuÿstuÿstuÿstuÿstuÿstuÿstuÿstuÿpsrÿz}}ÿ‚‚ÿ~ÿYTVÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ†Š‰ÿÚßÞÿÚßÞÿz|{ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿOHJÿ¸½»ÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿsuuÿG:>ÿG:>ÿrssÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿ¼ÁÀÿRJLÿG:>ÿklkÿÚßÞÿÚßÞÿª®­ÿNDGÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿwzyÿÚßÞÿÚßÞÿ‡Œ‹ÿG:>ÿG:>ÿ~€ÿÚßÞÿÚßÞÿ‚…„ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿLBEÿŸ¤¢ÿÚßÞÿÚßÞÿoooÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿXRTÿÁÇÅÿÚßÞÿÊÏÎÿ`\\ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ†Š‰ÿÚßÞÿÚßÞÿz|{ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿOHJÿ¸½»ÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿsuuÿG:>ÿG:>ÿrssÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿ¼ÁÀÿRJLÿG:>ÿklkÿÚßÞÿÚßÞÿª®­ÿNDGÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿwzyÿÚßÞÿÚßÞÿ‡Œ‹ÿG:>ÿG:>ÿ~€ÿÚßÞÿÚßÞÿ‚…„ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿLBEÿŸ¤¢ÿÚßÞÿÚßÞÿoooÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿXRTÿÁÇÅÿÚßÞÿÊÏÎÿ`\\ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ†Š‰ÿÚßÞÿÚßÞÿƒ‡†ÿsttÿsttÿsttÿsttÿsttÿsttÿsttÿsttÿrttÿTMOÿG:>ÿG:>ÿG:>ÿG:>ÿOHJÿ¸½»ÿÚßÞÿÕÚÙÿ…„ÿƒ‚ÿƒ‚ÿosqÿsttÿsttÿosqÿƒ‚ÿƒ‚ÿ…ƒÿÒØ×ÿÚßÞÿ¼ÁÀÿRJLÿG:>ÿklkÿÚßÞÿÚßÞÿª®­ÿNDGÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿwzyÿÚßÞÿÚßÞÿ‡Œ‹ÿG:>ÿG:>ÿ~€ÿÚßÞÿÚßÞÿ’ÿsttÿsttÿsttÿsttÿsttÿsttÿsttÿsttÿsttÿ\VWÿG:>ÿG:>ÿG:>ÿG:>ÿLBEÿŸ¤¢ÿÚßÞÿÚßÞÿoooÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿXRTÿÁÇÅÿÚßÞÿÊÏÎÿ`\\ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ†Š‰ÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÆËËÿ]XZÿG:>ÿG:>ÿG:>ÿG:>ÿOHJÿ¸½»ÿÚßÞÿÒØ×ÿgefÿG:>ÿG:>ÿ‚‚ÿÚßÞÿÚßÞÿ€ƒƒÿG:>ÿG:>ÿeacÿÏÔÓÿÚßÞÿ¼ÁÀÿRJLÿG:>ÿklkÿÚßÞÿÚßÞÿª®­ÿNDGÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿwzyÿÚßÞÿÚßÞÿ‡Œ‹ÿG:>ÿG:>ÿ~€ÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÕÚÙÿhhhÿG:>ÿG:>ÿG:>ÿG:>ÿLBEÿŸ¤¢ÿÚßÞÿÚßÞÿoooÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿXRTÿÁÇÅÿÚßÞÿÊÏÎÿ`\\ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ†Š‰ÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÆËËÿ]XZÿG:>ÿG:>ÿG:>ÿG:>ÿOHJÿ¸½»ÿÚßÞÿÒØ×ÿgefÿG:>ÿG:>ÿ‚‚ÿÚßÞÿÚßÞÿ€ƒƒÿG:>ÿG:>ÿeacÿÏÔÓÿÚßÞÿ¼ÁÀÿRJLÿG:>ÿklkÿÚßÞÿÚßÞÿª®­ÿNDGÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿwzyÿÚßÞÿÚßÞÿ‡Œ‹ÿG:>ÿG:>ÿ~€ÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÕÚÙÿhhhÿG:>ÿG:>ÿG:>ÿG:>ÿLBEÿŸ¤¢ÿÚßÞÿÚßÞÿoooÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿXRTÿÁÇÅÿÚßÞÿÊÏÎÿ`\\ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ†Š‰ÿÚßÞÿÚßÞÿ¦©¨ÿ’•”ÿ’•”ÿ’•”ÿ’•”ÿ’•”ÿ’•”ÿ’•”ÿ’•”ÿŠŽÿ[UXÿG:>ÿG:>ÿG:>ÿG:>ÿOHJÿ¸½»ÿÚßÞÿÒØ×ÿgefÿG:>ÿG:>ÿrttÿ’•”ÿ’•”ÿrutÿG:>ÿG:>ÿeacÿÏÔÓÿÚßÞÿ¼ÁÀÿRJLÿG:>ÿklkÿÚßÞÿÚßÞÿª®­ÿNDGÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿwzyÿÚßÞÿÚßÞÿ‡Œ‹ÿG:>ÿG:>ÿ~€ÿÚßÞÿÚßÞÿ®³±ÿ’•”ÿ’•”ÿ’•”ÿ’•”ÿ’•”ÿ’•”ÿ’•”ÿ’•”ÿ“’ÿlnnÿgefÿgefÿ[VVÿG:>ÿLBEÿŸ¤¢ÿÚßÞÿÚßÞÿoooÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿXRTÿÁÇÅÿÚßÞÿÊÏÎÿ`\\ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ†Š‰ÿÚßÞÿÚßÞÿz{{ÿI=AÿI=AÿI=AÿI=AÿI=AÿI=AÿI=AÿI=AÿI=AÿG<@ÿG:>ÿG:>ÿG:>ÿG:>ÿOHJÿ¸½»ÿÚßÞÿÒØ×ÿgefÿG:>ÿG:>ÿH=@ÿI=AÿI=AÿI=AÿG:>ÿG:>ÿeacÿÏÔÓÿÚßÞÿ¼ÁÀÿRJLÿG:>ÿklkÿÚßÞÿÚßÞÿª®­ÿNDGÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿwzyÿÚßÞÿÚßÞÿ‡Œ‹ÿG:>ÿG:>ÿ~€ÿÚßÞÿÚßÞÿ‚„„ÿI=AÿI=AÿI=AÿI=AÿI=AÿI=AÿI=AÿI=AÿJ@BÿŒ‘ÿÒØ×ÿÒØ×ÿtvuÿG:>ÿLBEÿŸ¤¢ÿÚßÞÿÚßÞÿoooÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿXRTÿÁÇÅÿÚßÞÿÊÏÎÿ`\\ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ†Š‰ÿÚßÞÿÚßÞÿz|{ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿOHJÿ¸½»ÿÚßÞÿÒØ×ÿgefÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿeacÿÏÔÓÿÚßÞÿ¼ÁÀÿRJLÿG:>ÿklkÿÚßÞÿÚßÞÿª®­ÿNDGÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿwzyÿÚßÞÿÚßÞÿ‡Œ‹ÿG:>ÿG:>ÿ~€ÿÚßÞÿÚßÞÿ‚…„ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿI<@ÿ“’ÿÚßÞÿÚßÞÿtvvÿG:>ÿLBEÿŸ¤¢ÿÚßÞÿÚßÞÿoooÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿXRTÿÁÇÅÿÚßÞÿÊÏÎÿ`\\ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ†Š‰ÿÚßÞÿÚßÞÿxzyÿSLNÿSLNÿSLNÿSLNÿSLNÿSLNÿSLNÿSLNÿSLNÿSLNÿSLNÿSLNÿMDGÿG:>ÿOHJÿ¸½»ÿÚßÞÿÒØ×ÿgefÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿeacÿÏÔÓÿÚßÞÿ¼ÁÀÿRJLÿG:>ÿklkÿÚßÞÿÚßÞÿª®­ÿNDGÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿwzyÿÚßÞÿÚßÞÿ‡Œ‹ÿG:>ÿG:>ÿ~€ÿÚßÞÿÚßÞÿ„ƒÿSLNÿSLNÿSLNÿSLNÿSLNÿSLNÿSLNÿSLNÿSMPÿ‚†…ÿµ»¹ÿµ»¹ÿsttÿG:>ÿLBEÿŸ¤¢ÿÚßÞÿÚßÞÿmooÿSLNÿSLNÿSLNÿSLNÿSLNÿSLNÿSLNÿSLNÿ]\\ÿ¤ª¨ÿµ»¹ÿª°®ÿ`\]ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ†Š‰ÿÚßÞÿÚßÞÿÃÈÇÿ¼ÂÀÿ¼ÂÀÿ¼ÂÀÿ¼ÂÀÿ¼ÂÀÿ¼ÂÀÿ¼ÂÀÿ¼ÂÀÿ¼ÂÀÿ¼ÂÀÿ¼ÂÀÿ¼ÂÀÿmmmÿG:>ÿOHJÿ¸½»ÿÚßÞÿÒØ×ÿgefÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿeacÿÏÔÓÿÚßÞÿ¼ÁÀÿRJLÿG:>ÿklkÿÚßÞÿÚßÞÿª®­ÿNDGÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿwzyÿÚßÞÿÚßÞÿ‡Œ‹ÿG:>ÿG:>ÿ~€ÿÚßÞÿÚßÞÿÇÌËÿ¼ÂÀÿ¼ÂÀÿ¼ÂÀÿ¼ÂÀÿ¼ÂÀÿ¼ÂÀÿ¼ÂÀÿ¼ÂÀÿ¹¾½ÿhihÿOGIÿOGIÿLCFÿG:>ÿLBEÿŸ¤¢ÿÚßÞÿÚßÞÿ¿ÄÃÿ¼ÂÀÿ¼ÂÀÿ¼ÂÀÿ¼ÂÀÿ¼ÂÀÿ¼ÂÀÿ¼ÂÀÿ¼ÂÀÿ› ŸÿTOPÿOGIÿOGIÿJ?CÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ†Š‰ÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿmmnÿG:>ÿOHJÿ¸½»ÿÚßÞÿÒØ×ÿgefÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿeacÿÏÔÓÿÚßÞÿ¼ÁÀÿRJLÿG:>ÿklkÿÚßÞÿÚßÞÿª®­ÿNDGÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿwzyÿÚßÞÿÚßÞÿ‡Œ‹ÿG:>ÿG:>ÿ~€ÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÕÚÙÿhhhÿG:>ÿG:>ÿG:>ÿG:>ÿLBEÿŸ¤¢ÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿÚßÞÿ­±±ÿNEHÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿƒ‡†ÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿmmnÿG:>ÿOHJÿ°´³ÿÍÓÒÿÇÌËÿgefÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿeacÿÄÉÈÿÍÓÒÿ³¸·ÿRJLÿG:>ÿklkÿÍÓÒÿÍÓÒÿ£§¦ÿNDGÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿwyxÿÍÓÒÿÍÓÒÿ„ˆ‡ÿG:>ÿG:>ÿ}~ÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÉÎÍÿhhhÿG:>ÿG:>ÿG:>ÿG:>ÿLBEÿšžÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿÍÓÒÿ¥ª©ÿNEHÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ^Z[ÿcaaÿcaaÿcaaÿcaaÿcaaÿcaaÿcaaÿcaaÿcaaÿcaaÿcaaÿcaaÿcaaÿcaaÿcaaÿUMPÿG:>ÿK@Dÿc`aÿcaaÿcaaÿRJLÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿQGKÿcaaÿcaaÿcaaÿK@DÿG:>ÿTLNÿcaaÿcaaÿa_`ÿJ>BÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿYSUÿcaaÿcaaÿ_\]ÿG:>ÿG:>ÿ\WYÿcaaÿcaaÿcaaÿcaaÿcaaÿcaaÿcaaÿcaaÿcaaÿcaaÿcaaÿcaaÿRJMÿG:>ÿG:>ÿG:>ÿG:>ÿI=Aÿa^_ÿcaaÿcaaÿcaaÿcaaÿcaaÿcaaÿcaaÿcaaÿcaaÿcaaÿcaaÿb_`ÿJ?BÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ‘ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ó”™˜ÿ”™˜ÿ”™˜ÿ‘•”ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿ‘•”ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ò”™˜Ù”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿOCGÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿODGÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜×”š˜µ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿf`cÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿgbcÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”˜˜³”™™i”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿŠŒÿJ=AÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿI=Aÿ‹Žÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ•™—g““”™˜ú”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿxvxÿH;?ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿH;?ÿxwxÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ù•••“™™“”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿzzzÿLAEÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿG:>ÿLAEÿ{{{ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™—‘’žž”™˜ì”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ“’ÿqnoÿ[SVÿSHLÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿRFJÿSHLÿ[TVÿropÿ“’ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™—ê™™™•™™<”™˜ø”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ú•™™A”š—V”™˜÷”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ö“™™U’——6”™˜á”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™™à“˜˜4ŽŽŽ “š˜t”™—ê”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™—ê•™™sŸŸŸªªª–šš?“™—‡“™˜×› Ÿþ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿœ¡ ÿ“™™ï”™™Ì”™™Ì”™™Ì”™™Ì”™™Ì”™™Ì”™™Ì”™™Ì”™™Ì”™™Ì”™™Ñ”™˜ÿ¤©©ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ¤©©ÿ”™˜ÿ“˜˜Ò”™™Ì”™™Ì”™™Ì”™™Ì”™™Ì”™™Ì”™™Ì”™™Ì”™™Ì”™™Ì”™™îœ  ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿšŸŸþ“™™è”™™Ì”™™Ì”™™Ì”™™Ì”™™Ì”™™Ì”™™Ì”™™Ì”™™Ì”™™Ì“˜˜×”™˜ÿ¦««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ§««ÿ¢§§ÿ”™˜ÿ“š˜¦”š˜ˆ–šš?ªªª•˜˜‹±µ´úôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿ½ÁÀÿ–››­““”™˜ÿçééÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿèêêÿ”™˜ÿ’››–››ª¼ÀÀÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿ±·¶ú”š˜’——6”™˜ÿñôôÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿÝàßÿ”™˜þ•˜˜‹±µ´úôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿ½ÁÀÿ–››­““”™˜ÿçééÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿèêêÿ”™˜ÿ’››–››ª¼ÀÀÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿ±·¶ú”š˜’——6”™˜ÿñôôÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿÝàßÿ”™˜þ•˜˜‹±µ´úôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿ½ÁÀÿ–››­““”™˜ÿçééÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿèêêÿ”™˜ÿ’››–››ª¼ÀÀÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿ±·¶ú”š˜’——6”™˜ÿñôôÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿôööÿÝàßÿ”™˜þ•˜˜‹¥©¨üÍÑÑÿÍÑÑÿÍÑÑÿÍÑÑÿÍÑÑÿêììÿôööÿôööÿôööÿîññÿÎÒÑÿÍÑÑÿÍÑÑÿÍÑÑÿÍÑÑÿ­±±ÿ–››­““”™˜ÿÆÉÉÿÍÑÑÿÍÑÑÿÍÑÑÿÍÑÑÿØÜÜÿôööÿôööÿôööÿôööÿÙÜÜÿÍÑÑÿÍÑÑÿÍÑÑÿÍÑÑÿÆÊÉÿ”™˜ÿ’››–››ª¬±°ÿÍÑÑÿÍÑÑÿÍÑÑÿÍÑÑÿÎÒÑÿîððÿôööÿôööÿôööÿêììÿÍÑÑÿÍÑÑÿÍÑÑÿÍÑÑÿÍÑÑÿ¦«ªü”š˜’——6”™˜ÿÍÐÏÿÍÑÑÿÍÑÑÿÍÑÑÿÍÑÑÿÝàßÿôööÿôööÿôööÿôööÿÕØ×ÿÍÑÑÿÍÑÑÿÍÑÑÿÍÑÑÿÀÄÃÿ”™˜þ”š—X—œœÕ—œœÖ—œœÖ—œœÖ—œœÖ•™˜øÉÌÌÿôööÿôööÿôööÿÔ××ÿ•š™ü—œœÖ—œœÖ—œœÖ—œœÖ—œœÖ”›™pŽŽŽ —›Å—œœÖ—œœÖ—œœÖ—œœÖ—œ›ãœ Ÿùôööÿôööÿôööÿôööÿ¢¡ù—›šä—œœÖ—œœÖ—œœÖ—œœÖ•›™Æ‹¢¢ •ššo—œœÖ—œœÖ—œœÖ—œœÖ—œœÖ•™˜ûÓÖÖÿôööÿôööÿôööÿÊÍÌÿ•™˜ø—œœÖ—œœÖ—œœÖ—œœÖ˜Õ•˜˜Y™™™—œ›Ì—œœÖ—œœÖ—œœÖ—œœÖ—›šè§«ª÷ôööÿôööÿôööÿôööÿ”™˜ÿ—œß—œœÖ—œœÖ—œœÖ—œœÖ—››¸˜ÐÈÌËÿôööÿôööÿôööÿÓÖÖÿ–œ›ì“™™UšŸžøôööÿôööÿôööÿôööÿ¡ ÷”š—X—œêÓÕÕÿôööÿôööÿôööÿÉÌÌÿ™œÑ”˜˜r¦ª©öôööÿôööÿôööÿôööÿ”™˜ÿ“——;˜Ð¾ÂÁÿáäãÿáäãÿáäãÿÇÊÊÿ–œ›ì“™™UšŸžùáäãÿáäãÿáäãÿáäãÿšŸžù”š—X—œêÆÉÉÿáäãÿáäãÿáäãÿ¾ÂÁÿ™œÑ”˜˜r¡§¦øáäãÿáäãÿáäãÿàããÿ”™˜ÿ“——;˜œš´”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ–››Ñ–šš?”™˜ý”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ý“›—B–›šÏ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ—œ›µ“™™Z”™˜þ”™˜ÿ”™˜ÿ”™˜ÿ”™˜ÿ”™˜û“™™(ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿøÿÿà?ÿÿÿÿÿÿÿÿøÿÿà?ÿÿÿÿÿÿÿÿøÿÿà?ÿÿÿÿÿÀþøÿÀÿÿÀþøÿÀÿÿÀþøÿÀÿÿÀþøÿÀÿÿÀþøÿÀÿþøðàÀ€€€€ÀàðøþÿÀþøÿÀÿÿÀþøÿÀÿÿÀþøÿÀÿÿÀþøÿÀÿÿÀþøÿÀÿÿÿÿÿøÿÿà?ÿÿÿÿÿÿÿÿøÿÿà?ÿÿÿÿÿÿÿÿøÿÿà?ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿfwupd-2.0.10/data/fwupd.service.in000066400000000000000000000016161501337203100167430ustar00rootroot00000000000000[Unit] Description=Firmware update daemon Documentation=https://fwupd.org/ Wants=modprobe@sd_mod.service After=modprobe@sd_mod.service dbus.service Before=display-manager.service ConditionVirtualization=!container [Service] Type=dbus TimeoutSec=180 RuntimeDirectory=@motd_dir@ RuntimeDirectoryPreserve=yes BusName=org.freedesktop.fwupd ExecStart=@libexecdir@/fwupd/fwupd KeyringMode=private LockPersonality=yes MemoryDenyWriteExecute=yes NoNewPrivileges=no PrivateDevices=no PrivateTmp=true ProtectClock=yes ProtectControlGroups=yes ProtectHome=yes ProtectHostname=yes ProtectKernelLogs=yes ProtectKernelModules=yes ProtectKernelTunables=yes ProtectProc=invisible ProtectSystem=full RestrictNamespaces=yes RestrictRealtime=yes RestrictSUIDSGID=yes SystemCallArchitectures=native Environment="GLIBC_TUNABLES=glibc.cpu.hwcaps=SHSTK" RestrictAddressFamilies=AF_NETLINK AF_UNIX AF_INET AF_INET6 @dynamic_options@ fwupd-2.0.10/data/fwupd.shutdown.in000077500000000000000000000004071501337203100171560ustar00rootroot00000000000000#!/bin/sh # no history database exists [ -f @localstatedir@/lib/fwupd/pending.db ] || exit 0 # activate firmware when we have a read-only filesystem if ! @bindir@/fwupdtool activate; then ret=$? [ "$ret" -eq "2" ] && exit 0 exit $ret fi fwupd-2.0.10/data/icons/000077500000000000000000000000001501337203100147365ustar00rootroot00000000000000fwupd-2.0.10/data/icons/128x128/000077500000000000000000000000001501337203100156735ustar00rootroot00000000000000fwupd-2.0.10/data/icons/128x128/meson.build000066400000000000000000000002361501337203100200360ustar00rootroot00000000000000if get_option('metainfo') install_data(['org.freedesktop.fwupd.png'], install_dir: join_paths(datadir, 'icons', 'hicolor', '128x128', 'apps') ) endif fwupd-2.0.10/data/icons/128x128/org.freedesktop.fwupd.png000066400000000000000000000171111501337203100226270ustar00rootroot00000000000000‰PNG  IHDR€€Ã>aËÐeXIfII* €€†Œ”(1 œ2ªi‡¾Ú+Ú+GIMP 2.10.382024:10:04 18:13:59 Ø &g„iCCPICC profilexœ}‘=HÃ@Å_?D)-"vqÈPÅÁ.*âX«P„ ¡VhÕÁäÒ/hÒ¤¸8 ®?«.κ:¸ ‚àˆ³ƒ“¢‹”ø¿¤Ð"ƃã~¼»÷¸{ø›U¦šÁ j–‘I%…\~Uè}EA ‚q‰™úœ(¦á9¾îáãë]œgyŸûsD”‚ÉŸ@œ`ºaoÏlZ:ç}â(+K ñ9ñ„A$~äºìòç’Ã~ž5²™yâ(±Pêb¹‹YÙP‰§‰cŠªQ¾?ç²Ây‹³Z­³ö=ù Ãme™ë4GÂ"– B€Œ:*¨ÂBœVÚOzø‡¿H.™\0r, ’ãÿƒßݚũI7)œz^lûcèÝZ Ûþ>¶íÖ x®´Ž¿Öf?Iot´Øп \\w4y¸Ü†žtÉ)@Ó_,ïgôMy`ð­¹½µ÷qúd©«ô ppŒ•({ÝãÝ}ݽý{¦Ýßour¥8©d xiTXtXML:com.adobe.xmp zÖ^bKGDÿÿÿ ½§“ pHYs--žž ZtIMEè  ;êPâ— æIDATxÚí{PTWžÇ¿çܾÝÐtCƒÁG#‚ÐdbLÌc3³»Ù™IL%“1NÖd³›™%MFÝÔ>«æIíTí?[»ën”wŒ3Î#š53Éu’‰ëîø&1B€@ènúqï=ûG'Èínén ð÷©â>÷Üsîïw¾÷w·8 ‚ ‚ â΂M¶€êÚÊßjªòdhºÄyCIÉW%â¡««·?¦ íc<ä’&¸xüÕ’-'¤Þší§5M»'ÌVIú ä¯_}2uVU•m`»"´• ŠŽ-ç&S¾a²(1,,\¶‹/Mû¢»gNg%JµB¨sF“òøãß4ŽMÿðÊ/àŸ›¨z9gY«V~ ö¬¦]ºt­­­ g+›Ÿšfõ?úè×Mc±ïƒß4 s&[¾a*Òl6cNÆgºÑKŒ­3Ø@’–èJ-«ÎÖž«Wn¨$ÉZ¨­Sʨ.^æÒCCQ@Q³è㦠[mmÅÙÐrT¡V;J6ÿW,unݺ5Ùš–üH×µ<`eŒ‡u_ŒKLú÷ÚÚŠ×õujÎd“çñ—^úGwl]̶×8çŽÐtEÑÒ ¯TQ”ÂP[c"Pÿ¥´tÓ/c õU ¸¤½Ï³¾OCc,bW- »jk+Fô%%¯~kÊÀ4vŸ%-µpÙÒ¥R赬¬lÝo»ÝŽ\+AˆecÓÛÚÚÐÛÛó(€˜ §É骢>tï=_CR’Iw-%%%,ÿÃ=bt»]YFc´×çÃéÓŸÂç3¥ˆIŒ±‡ïÊÌ\¶(oQXcÛíúž-/7²A2ÐÙÚr¾Eíè_ &hš/[€ß÷Àk*íÔ4[XþÇûü>oÞØ´¡áa45}¾4a]€ÅbÑ ¥¨ 'QwXú€Ó‰ÞÞž¸ÃT^Þ"X­Ö¨ùB\.NŸþ4î:mi6ÆÔýEÊ×ÕÝ©õôÇ]oA~>8êbäæä†¥õôö ©éóøÆ54º³!Ð:ÀÍÙ½{·ätö=¢ õ[ xÆdJ*´¤X&¼vàö¸áóŽ8v1Æ[d¬L·Ùb ‹‘ûVN' D#cÌãm‹M¦¤ôHãŒXq¹†…ßïëXglÓ=‘Æ–ÎÉȘðòŒ¢0840ñK€upðý6Û]ذaƒ—¶nÝšl¶È›„`ÿÄ€LzOf.¸ÆÁþÕh4o{ùå—½QPQñÆÆù9ä¾YêÛ…ëKK·Ôßt PQ]¾q~˜VF‚\0ö¿UUeÏEŒUUÛ0‘»f5öM‡cÓ¡Q¼ùæ™þoÂ×–ýþ.¶và‹®^xÜ^hš Nçig0[’`Ïš‡ÅùÙ0囌 ä凣Ï{=Rãw´wãÔ±ø|òì bhÈ…«Ý}hj85­Bv΂Ðñ@¦†À¼ÆÊêÊì²ÂÚè¤Òv±ÇŽ|!èŸÑƒ?ưö‘{·8샥_UX.7*ìéÐÆw¹<8q¬6 þ„À‰£gàq„^2ržæ‚a]è•æÆ PU•¼7KPU MM£ƒë8òC/t^é!¯Í2:;®FZÈçìcÓEÅȈ—<6Ëñx¡(aQ=‹°è ·f)ÚÖJ_ïôur € $‚@A H € $‚@A H € $‚@A HÄÌÄ0Ueç̓Ñ$Oøþ¾!ô_‚Ñh@vÞüÑÍk®õ `È9þ¯V«sí£¿¯\¾ ¿_-Ê9™i¡iè¸ÜéÿãtÌ[Kjp»^%  ýRðŸ*³²3‘dŽo÷œ¾ž †<»$qä,š.Åþî M £í*”€:}°¢¸ëÖ=1áûk«ß É€g×?…ÜÜlø|~ÔT½Uóíw¡¤ô/a6'£««ÛÞØ¿_AvÞ<8ß|þùYì¬ÛUKîÎÁÆûöí@ÁÒEX¿þ™˜í ÔTïÀ ó²Þᲄ?úúZÜÿ}1—ÕÜ|;jÞžÞ$ ‹uJÊJJN‚Åb…,Çþ_Ê)))HIIArr².ý«g2™b{{»qÁpcsJÎY\ö7ßVG6Êq••dJšþ]ÀXzzzàóûâºg&íErýúu¸=ãG%UQ¤X“F{GÂEÑÕÕUÿÍîèG’Ùˆ¯vÈ÷{LQ4HˆÞ{ošZãºÇçõÏ8ð;œ:Þ0þèš÷¯]…ï<ûàÃãäÑð¼ßÙ³—/v[VªÍŒç_xééép»Ýx÷Ð~é‹é+Î9œý®Y;rfŒEµs«ÕŠ‚üÀï?ï´îâ"€â•EX`Ÿ?n¿ß‹“GÏ »«/ærW?°y‹òO9y¤é¶8­xUlé¶qóhšŠ ­ñŸÿ± àtÝ9X³fuôT_êO4ÆUnjªO<<çç»~qÛœV\T„⢢¨³€³Me8×Ô6­»3Z ¼ÃIHøÕ¯ÞFÿugÔŽÔí‰2’“gÇœ`w2àtbû¶ª`ÿ/üé· ö·œëæì‰æ½÷ßGgGwÔ|3` œ¸Ý#§<ñ"Ë26nüóÑßÛ«pâH°Üû×®À‹/¾p[œævO‰}Ô³3L@»÷¼ñåtoxØsã-ty°ëg?ÿ² `X¿þY˜B–LGÜÞÑ<¯E«–€³ñ5ïóݸg`Йp-V3xhü¥*Tøà dY†ª ø½¾;Cš¦âJ[wÄ‘tsãe47?´æ@QT„.÷_héÄ…–à™Móísð·ÿð*æÍ›7n;Þ܉¿e6þà/GÍÓØÐˆŠòð¸}S^?uÔL ½½½x÷׿†[·{Re?v Ð4 @ô­kEÁþý!8FFFiÝMÓ4:t¦$ã¸e »"܇†Fí蘔}-çZpíÚµ˜ó%ð0nVYU®ó–×ëÃÞ·AcÎ7žì9ú³’¢Ì½þ›œÁ>Ap轓±1Ö瘪ú¾âÙïýYØÌS¦òp‰xË f }ÆÛiM A H € $‚@A H € $‚@A H € $‚@A H €€nKnI’È+³”mëåt»˲²Q&oÍ2ŒF²¶@7C{hê{&yl–± +b›¶saÛ,[±dB»XÓÆ–­XžìçLˆwC/̹ˆåEKÈs³„ÅùȘ¶¹µ` ¿áÇ–s€Øzuå½KQ´²fò›­ºÅ÷F¸ˆÿ~å•Íç Àû‘&°€qlØXyïRdçÚÑÜtÝ]½ðûüäÕ€Éd„=+K‹ò‘žž)‹OåìG_vA*ªË¿ÏêÆ+X ¨Ð„FžÎózÆaÇŸÊ3௎Í;u€ÊÊòŸ€áurã,îÀþÙáØô“1bÐSY¹í%0Q ‰Ü5«¯”–nÙ¥‹¡¹JK7ý”A-P @%¿ÍxÀö0hÅ¡1Œ¥¦¦l‘¦±§Ä·¶š1–169$nÊ9´3†œ‹ß””l¹<Îx 6*+·ýÛÂì…¯ýñ7þdÂëÄ'O@ëù–½%%?ün,ùËêÊì²Âº¾óÌwaµNìTR—Ë…½ï¾‰kÙ%%ÓË=55ÛwÜýÜš5N¸þçðïW®”•¾²ùïcÉ_QñÆÆù‰_ø p>±ï1=½=8xp¿(ulŽù#} ¼Óg ä‚;›¸6‹v¹\¼µµ%,=++f³yôw àG[{B·ùtNì޶¶Ëa»\§¤¤Àn_¨Këîî‚Û­ßîÝë›Ø! ÎA'Âle ¹9y0ol9ïñxÐÕu%‚¯Üz¹Z/\é˜SÓl˜7WÐE{G;ü>ýáÚCÃÉ€àâ×ðPË©S'u§¨ÚâÕ÷)¦åËWŒiˆn?~L•8?ZŽíÿb­30HJK>ÚØx&=dXk5ä¹ßÛð¼nóÿ#Gÿà÷ùü×8ƒnƒ}Æ™ÓdòżɿâHßµkE}!{ú+ªV dC^Þ¢âloC}ý)ŸAâ—ôZa«9sÓ.iŸ|RÒ¬ó—@†ÍfK[÷äSºiùáÇ`0Hmн&q©ã–†ººÊÏNÕkôïìÙFQS»½/QuVU•=÷æŽjïØ:=—xkgÝHEuù󉪷¶®¢·©©QWg}ý Q[Wu&QuVV–ÿÝÛ»á µµ²ª\TT”=|K»€›áñxp½ÿºnä}+µÆÖ š¦&|Lãr ëlvŒüT ª®³u §â“€*ÐÙÜ|vUsóYèCïJXË3©×ï÷öí{?Ü.zU¯¦‰®ÓŸ}šyú³Oõ¶JRgâlW‡††lU8ÇuÆAAAqðÿ ‹9a¹a'8IEND®B`‚fwupd-2.0.10/data/icons/64x64/000077500000000000000000000000001501337203100155315ustar00rootroot00000000000000fwupd-2.0.10/data/icons/64x64/meson.build000066400000000000000000000002341501337203100176720ustar00rootroot00000000000000if get_option('metainfo') install_data(['org.freedesktop.fwupd.png'], install_dir: join_paths(datadir, 'icons', 'hicolor', '64x64', 'apps') ) endif fwupd-2.0.10/data/icons/64x64/org.freedesktop.fwupd.png000066400000000000000000000143151501337203100224700ustar00rootroot00000000000000‰PNG  IHDR@@ªiqÞÐeXIfII* @@†Œ”(1 œ2ªi‡¾ÅÅGIMP 2.10.382024:10:04 18:13:13 χ£F„iCCPICC profilexœ}‘=HÃ@Å_ÓJ¥´ˆØAÄ!Cu²‹Š8Ö*¡B¨Zu0¹ô š´$).Ž‚kÁÁŪƒ‹³®®‚ øâìà¤è"%þ/)´ˆñà¸ïî=îÞB«Ê434Ý22©¤˜Ë¯ŠÁW„À "ðÉ̬ÏIRžãë>¾ÞÅy–÷¹?GD-˜ ð‰Ä V7,â â™M«ÎyŸ8ÊʲJ|N Ö*bKGDÿÿÿ ½§“ pHYs  ¹;<©tIMEè  %êwjIDATxÚí[{PT×ÿ»÷.ûÞeUµèDƒ‚FkRѶƒÕ1iеã+±™:¶2“iÓfš>œ¾2mÆ™¬•"¤(dâ8­S ôaT| ˆŠÖ¨¨„7»ÀÝ]vÙû8ýcÙÇEâ0É Üofgv¿óýÎwÎïœó}{ÏPE• -äq‡Š‹ìv»AœÛëùãk·¾7šŽóósSLfÛY‹ÅÖÏó=1Ý.÷Ò;wÞ öðá¿ýÈh2¿ÉqZ¡»ÛéÛ¼éõÔÑNèÈ‘ƒUV[ìLèîvÕlÞ´uýg& ¨¸`5$¤€ÅjùÁª•«Szzzpªª¼ 2N€$yeeewÇQJIQñ7ˆÌ˜DYœ<75mû¼yóí×®_s]¿Q_À2ledÏÖÍßÍ'„ÐáØ¼¼¼xŽÝY½ò+WX­6”——5ò<¿ ÁÍ­›·—…÷àÁ‚E”b9pZî{k2×΀½Meä»”˶lyãf8–F€AgüÝâÅKž½N°ÙlÈøúŠ dtvt ®®¶ÀÃq………¦„„ø_/X¸Ðf“0{ÖÓöÄÄÄ·àʕ˹¹¹%øáX†¡ééóö$8 ¬V`Ù²å)>¿œ«9{@ZmÌo–.}aÇi‡ô+2VÍ’dißçÃÅÚ‹³ì aÄ8{\„Þ> ë÷û·”N§ „cµZƒ:^PšÍf„c # #€Óh%¬ÝBB7´Í$Ñëõ<þPJÉ_ò÷¾¬¡dÃ0™£Q¯E>Ÿ¯@xÏ Ë²Éz½žUÂú|>QÅOÈaM&½^ï`YE(úú¼ý’$7GL‚)&“Q¯ÒdY†ß×用\™ù`ÇŽïŸ!`ß¾}±ŒF*ÈÊ ü?ŠÑ ¶m{³‡”––j\ÝÕ@0ˆøýýhosBãjÆœ–äÉqÐéb—þßv›cëìéØJ&ÿî§8î*$I—Ëβ<»d>Rf$_s¹Ú·0„bð¼çÎÔÛÉc—„ógêÀóÞÁàñ: š´‚R:î¿,ËhzÐ2øs>ÀB¿€‰"GñÍÊLôg••••••••••€ *ìh ÃïÛ† ¥Áfõ‰2ˆ¥ 4ô{4»A|4Ãûô§4ž1%à• /búô©Ší%GþŽ/¥$㹯,@qÑ1t´»†Ú±Ø´å;¨­½Š»wîc㦵8{æΞ¾ÒÇ·Öf@§Aéá“øöº•˜9#%ÂO}ý TœüðÜ’ùX¶|q„M/ïÆûŽB Hcw“§³'êG%è zÌ™;Œ&tUC07m Fƒ´ô4èt‘÷­ñ HLJ8“0iÊä?²,cî¼§`±¡Óé–ž†Þ^wˆ Ïó˜:S§MÛ#p÷î=¼ŸT±=mþÓcz6Dø[²l!ÞúI6~û«=CºSåÿÁý{.ŠM&=ÞÝó œª¨FÓö±#`ÚôiXÿZfˆîÞF\8Wa;ó©©xvñB\:<ïy"¦&'FøëèhÇŸsÿŠ®N“&}¾A0>.é顯躮»] Ì#ÒÓSqûæmBà‰f³ÅFø«9ãFuyÍ“jk¯ ¨ 44ÚʡіeYüøílœý¸¿|ç=¼²þ%¼°ìyŒô²CI®_»¼}‡ÂÃû™Q9²R™¢¢¼ í ÷¢¯Ï‡Ì5/|^Ž}øH’€¯f„Fðû ò£JGáfÍNÁô”GJ–E”ý³m-ícKÀhD’%œ?wm-Î-[Çd;¾ùòj€³«'ŽŸÂ‹™X·nMö¿ÿêï|fŸ7¿ò»½½ï¼ýî¨Ó ÙŸ·W@ê.5 áÆÝ¨FIS$m-]ŠÙã,0™ hmî‚ ˆCzŽc1%)®®^@\¼FSh*ìêìAŸ×˜’˜B€–æNEÖX ¬VC„^¦@óÃöÿ ¥¦ÍÄ‚/Ïýhnêx¬ËÉÃåä#ô‚ âáýÐtäìê…s€ŒhÒÚÒùX½Ý ùr¤µµ«p÷îÝ¢¢ù±c¥ûú fwupd-2.0.10/data/motd/fwupd-refresh.timer000066400000000000000000000003011501337203100204030ustar00rootroot00000000000000[Unit] Description=Refresh fwupd metadata regularly ConditionVirtualization=!container [Timer] OnCalendar=*-*-* *:00:00 RandomizedDelaySec=1h Persistent=true [Install] WantedBy=timers.target fwupd-2.0.10/data/motd/fwupd.sysusers000066400000000000000000000001131501337203100175300ustar00rootroot00000000000000u @systemd_unit_user@ - "Firmware update daemon" @localstatedir@/lib/fwupd fwupd-2.0.10/data/motd/meson.build000066400000000000000000000047631501337203100167420ustar00rootroot00000000000000 if libsystemd.found() install_data(['fwupd-refresh.timer'], install_dir: systemdunitdir) motd_fullpath = join_paths ('/run', motd_dir, motd_file) else motd_fullpath = join_paths (localstatedir, motd_dir, motd_file) endif con2 = configuration_data() con2.set('bindir', bindir) con2.set('libdir', libdir) con2.set('motd_fullpath', motd_fullpath) con2.set('systemd_unit_user', get_option('systemd_unit_user')) if libsystemd.found() if get_option('systemd_unit_user') == '' con2.set('user', 'DynamicUser=yes') warning('Using systemd DynamicUser for fwupd-refresh.service. See https://github.com/systemd/systemd/issues/22737 for possible implications') else dynamic_options = [ 'ProtectSystem=strict', 'ProtectHome=read-only', 'User=' + get_option('systemd_unit_user') ] con2.set('user','\n'.join(dynamic_options)) endif configure_file( input: 'fwupd-refresh.service.in', output: 'fwupd-refresh.service', configuration: con2, install: true, install_dir: systemdunitdir, ) endif # This file is only used in Ubuntu, which chooses to use update-motd instead # of sourcing /run/motd.d/* # See https://bugs.launchpad.net/ubuntu/+source/pam/+bug/399071 configure_file( input: '85-fwupd.motd.in', output: motd_file, configuration: con2, install: false, ) if libsystemd.found() if get_option('man') custom_target('fwupd-refresh.service.8', input: 'fwupd-refresh.service.md', output: 'fwupd-refresh.service.8', command: [ generate_man, '@INPUT@', '-o', '@OUTPUT@', '--replace', 'PACKAGE_VERSION', fwupd_version, ], install: true, install_dir: join_paths(mandir, 'man8'), ) endif if build_docs md_targets += custom_target('fwupd-refresh.service.md', input: 'fwupd-refresh.service.md', output: 'fwupd-refresh.service.md', command: [ generate_man, '@INPUT@', '-o', '@OUTPUT@', '--replace', 'PACKAGE_VERSION', fwupd_version, '--md', ], ) man_md += ['"fwupd-refresh.service.md"'] endif endif if libsystemd.found() and get_option('systemd_unit_user') != '' con3 = configuration_data() con3.set('localstatedir', localstatedir) con3.set('systemd_unit_user', get_option('systemd_unit_user')) configure_file( input: 'fwupd.sysusers', output: 'fwupd.conf', configuration: con3, install: true, install_dir: systemd_sysusers_dir, ) endif fwupd-2.0.10/data/org.freedesktop.fwupd.conf000066400000000000000000000020231501337203100207140ustar00rootroot00000000000000 fwupd-2.0.10/data/org.freedesktop.fwupd.metainfo.xml000066400000000000000000006157501501337203100224110ustar00rootroot00000000000000 org.freedesktop.fwupd CC0-1.0 LGPL-2.0+ fwupd Update device firmware on Linux

    This project aims to make updating firmware on Linux automatic, safe and reliable. You can either use a GUI software manager like GNOME Software to view and apply updates, the command-line tool or the D-Bus interface directly.

    The fwupd process is a system daemon to allow session software to update device firmware on your local machine. It is designed for desktops, but this project is also usable on phones, tablets and on headless servers.

    https://github.com/fwupd/fwupd/issues https://fwupd.org/ https://www.transifex.com/freedesktop/fwupd/ https://github.com/fwupd/fwupd richard_at_hughsie.com The fwupd authors The fwupd authors fwupd moderate fwupdmgr fwupdtool

    This release adds the following features:

    • Include the AGESA version as the summary of the AMD secure processor device
    • Include the UEFI PK certificate key ID in the uploaded problem report
    • Provide a way for the client to restrict the GUID list to an emulated device

    This release fixes the following bugs:

    • Do not allow dbx updates on the HP Elitebook 845 Gen10
    • Do not warn about BIOS bugs we can easily work around
    • Fix a regression in fwupdmgr emulation-save when recording some devices
    • Fix a regression preventing installation of KEKs
    • Fix a small memory leak when getting security attributes
    • Never write a UX capsule when using Capsule-On-Disk
    • Use the 'OnBattery' property from upower to tell if plugged in

    This release adds support for the following hardware:

    • Lenovo Legion Touchpad
    • Logitech MX Mechanical
    • Poly Studio V72 and V12

    This release adds the following features:

    • Add some documentation about updating the KEK and db
    • Allow installing multiple db certificate updates at the same time
    • Show what certificate signed the EFI authenticated variable
    • Use readline to look up inputs from user, and make it optional

    This release fixes the following bugs:

    • Add several devices with broken firmware to the UEFI dbx blocklist
    • Constructing the authenticated URI properly when using FirmwareBaseURI
    • Do not enumerate non-updatable OptionROM devices
    • Do not export Redfish backup partitions as devices
    • Fix a crash when installing some Wacom firmware types
    • Fix a crash when parsing uevents that are not KEY=VALUE
    • Fix parsing the DFU descriptor when not using libusb
    • Fix PK and KEK enumeration failure on some systems
    • Fix SMBIOS parsing for ROM size >= 16MiB
    • Include a resolution for more of the HSI failures
    • Include more output when using fwupdtool get-devices --json
    • Never allow updating updatable-hidden devices with fwupdtool
    • Properly handle redfish location redirect when installing firmware
    • Recognize a very old dbx hash to allow upgrades
    • Require a reboot after updating Intel CVS devices
    • Rework the MEI code so that a device can use more than one interface
    • Rewrite the ModemManger plugin to be simpler and more supportable
    • Simplify parsing USB descriptors

    This release adds support for the following hardware:

    • Intel Arc Battlemage GPUs

    This release adds the following features:

    • Add the updated UEFI db as a new HSI attribute
    • Add two new plugins that can update the UEFI Signature Database and KEK

    This release fixes the following bugs:

    • Add /sys/firmware/efi/efivars to ReadWritePaths
    • Avoid any DPAUX IO if the BnR DPCD does not match
    • Be more careful falling back to older emulation versions
    • Detect the Firehose protocol features if not automatically sent
    • Do not match SMC Redfish method on non-Supermicro hardware
    • Do not show prompts or messages in --json mode
    • Fix a critical warning when enumerating DTH135K0C
    • Make the EFI LOADOPT either a path or ShimHive when setting metadata
    • Match lowercase directory names when checking for ESP
    • Only allow UEFI capsule updates on UEFI-capable architectures
    • Set the version format when using fwupdtool install offline
    • Support segment value 0 in the ccgx-dmc image parser

    This release adds the following features:

    • Allow calling 'fwupdtool security' with a fwupd version parameter
    • A new plugin to update B&R DisplayPort receivers
    • A new plugin to update Intel CVS cameras
    • A new plugin to verify UEFI memory protection attributes
    • A new quirk to signify that no additional ESP space is required
    • Build additional Redfish instance IDs for Dell server hardware
    • Implement the HPE proprietary Redfish firmware push method
    • Support cabinet archives greater in size than 2GB
    • Support for showing the SBOM release URL
    • Support for UEFI capsule installation in the bootloader

    This release fixes the following bugs:

    • Always close USB file descriptors after starting the daemon
    • Do not add a Redfish release date if set to 00:00:00Z
    • Fix a critical warning when rescanning a device with no GUIDs
    • Fix a small memory leak when emumerating Logitech Rallysystem devices
    • Fix a tiny Redfish memory leak when writing firmware
    • Fix building against pygobject 3.52
    • Fix Logitech BulkController setup for new device firmware versions
    • Fix scaler-only Wacom USB update deployment
    • Fix updating the RMM component in the dell-kestrel dock
    • Fix writing new EFI variables to workaround a kernel regression
    • Make PCI NAME and SSVID_SSPID based modem-manager IDs visible
    • Parse firmware before putting the device into bootloader mode
    • Prepend the capsule header when using Capsule-on-Disk
    • Put a memory limit on decoding LZMA streams when parsing firmware
    • Retry claiming the fastboot interface for up to 2500ms
    • Trigger dpaux rescan on drm changes correctly
    • Use the metadata version format to set the version_lowest when required

    This release adds support for the following hardware:

    • Another HP wireless dongle
    • Lenovo ThinkPad Thunderbolt 4 Smart Dock Gen2
    • Lenovo USB-C Dual Display Travel Dock
    • More EDL 5G modem devices

    This release adds the following features:

    • Add 'fwupdtool efiboot-hive' to allow setting the nmbl cmdline
    • Allow setting the inhibit reason from fwupdmgr
    • Allow USB-provided hidraw devices to use DS-20 descriptors

    This release fixes the following bugs:

    • Correctly deploy the dbx on MSI hardware
    • Correctly extract the milestone from Lenovo version numbers
    • Do not add invalid CoSWID entities to fix a fuzzing hang
    • Fix Logitech HID++ child device detection
    • Get the correct internal network VID and PID from Redfish
    • Include the payload length in the Wacom scaler update start command
    • Only use emulated devices when using device-emulate
    • Reload the thunderbolt retimer version after the payload is deployed
    • Speed up startup by ~1% by limiting the precision of percentage updates
    • Support new version formats for future Huddly devices
    • Updating the Logitech Rallybar in a more reliable way

    This release adds support for the following hardware:

    • HPE Gen10/Gen10+ devices using Redfish

    This release adds the following features:

    • Allow emulating devices reading EFI keys
    • Allow skipping device tests by CPU architecture

    This release fixes the following bugs:

    • Cleanup Dell kestrel devices when disconnected
    • Correctly build binary EFI_SIGNATURE_LIST objects
    • Do not allow dbx updates when no ESP was found
    • Ignore BootXXXX entries that do not exist when checking the dbx
    • Ignore EFI binaries that are zero-sized, or not well formed
    • Inhibit dbx updates if snapd is not available when using Ubuntu-style FDE
    • Only match the device checksum if the protocol matches
    • Raise authentication requirements for emulation-load
    • Request to upload failed reports for install/downgrade too
    • Use the kernel architecture when building the dbx instance ID
    • Write sbatlevel to PE/COFF files correctly

    This release adds support for the following hardware:

    • More ELAN Fingerprint readers
    • Star Labs StarLite Magnetic Keyboard

    This release adds the following features:

    • Record the entire USB descriptor in the emulation data
    • Return defined return code when network metadata refresh fails

    This release fixes the following bugs:

    • Add a new private flag of 'delayed-removal' to remove a footgun
    • Added a more specific instance ID for qc-s5gen2 USB devices
    • Add fadvise64 to the systemd syscall allowlist
    • Add the Unifying bootloader VID/PID as a full instance ID
    • Allow disabling zero-length packet for modem-manager devices
    • Allow recovering Logitech Bolt receiver in bootloader mode
    • Correctly parse CSV streams without trailing NULs
    • Detect if network is reachable before downloading metadata
    • Disabling reading the OptionROM device after dumping
    • Do not claim kernel interface to avoid Parade downstream port resets
    • Do not save BootOrder when measuring system integrity
    • Enumerate child nordic-hid devices correctly
    • Fix a possible critical warning for Mediatek scaler devices
    • Fix Firehose padding for some modem-manager devices
    • Fix UEFI capsule updates when using 4096 byte NVME blocksize
    • Get the Dell dock update package version correctly
    • Never read more of the composite stream from a partial stream
    • Notify snapd about DBX updates
    • Probe sd_mod before starting
    • Properly handle FU_DEVICE_PRIVATE_FLAG_NO_GENERIC_GUIDS
    • Remove the test for CSME 18 manufacturing lock
    • Restore the Logitech compatibility UFY instance IDs
    • Show the correct version when installing a same-device composite update
    • Show updates with problems when using 'fwupdmgr get-releases'
    • Split up the AMD GPU VBIOS P/N for the version
    • Use attr USB4_TYPE rather than guessing from thunderbolt_domain
    • Use the ISO date as a dbx version number for the Microsoft KEK
    • Use the KEK to set the dbx vendor ID

    This release adds support for the following hardware:

    • Fibocom NL668-EAU
    • HP 400/405 Peripheral
    • Lenovo USB-C 7-in-1 Dock

    This release adds the following features:

    • Add a power quirk for Framework systems
    • Speed up writing firmware to the new Dell dock

    This release fixes the following bugs:

    • Deinitialize DRM after getting GPU marketing name to fix Xorg startup
    • Do not show 'Device has been removed' as a dock device error
    • Fix a warning about legion-hid2 progress going backwards
    • Fix some small memory leaks in realtek-mst and dell-kestrel
    • Only mark supported Logitech devices as updatable
    • Parse FDTs with missing END tokens to work on more ChromeBooks
    • Reduce the device emulation RSS requirement by ~40%
    • Skip checking BootXXXX entries when the partition does not exist

    This release adds support for the following hardware:

    • Primax Ryder Mouse

    This release adds the following features:

    • Add fwupdtool 'get-version-formats' and 'vercmp' commands
    • Add support for checking AMD HW configuration MSR
    • Add support for enumerate-only device emulation to increase test coverage
    • Add support for passing a JSON file for emulation instead of ZIP
    • Remove support for now-obsolete CSR DFU and Nitrokey devices

    This release fixes the following bugs:

    • Add additional version checks for AMD Sinkclose
    • Check that getpid() returned a sensible value to detect minijail failure
    • Check the VLI USB3 firmware size before erasing
    • Correctly parse the ThunderBolt controller NVM
    • Disallow DBX updates on the Samsung Galaxy Book2 360
    • Do not create zero-sized archive entries when loading some ZIP files
    • Fix a critical warning when parsing a corrupt ELF file
    • Fix a small memory leak when checking algoltek-usb status
    • Fix a small memory leak when writing telink-dfu firmware
    • Fix eMMC probing regression in 2.0.x
    • Fix endianness parsing of msgpack float64
    • Fix failure to load the EFI DEVICE_PATH when there is no payload
    • Fix polkit message for emulation data collection
    • Fix sector erasing on Algoltek AG941x
    • Fix type confusion when saving emulation data
    • Fix UFS device feature discovery
    • Fix various device enumerations on s390x
    • Ignore LIBUSB_ERROR_NO_DEVICE when rebooting VIA VL103
    • Improve the algorithm for detecting multiple device-id matches
    • Mark all UEFI dbx updates as FWUPD_DEVICE_FLAG_AFFECTS_FDE
    • Only check for fwupdx64.efi when not using capsule-on-disk
    • Only set can-verify-image when CURRENT.UF2 exists
    • Parse install flags from apps compiled against old libfwupd versions
    • Repair the cros-ec device after flush failure
    • Reset the SteelSeries device only for certain models
    • Save the usb.ids and pci.ids name in the quirk database
    • Speed up decompressing large cab archives by 30%
    • Use a much more efficient method to calculate chunk sizes

    This release adds support for the following hardware:

    • ASUS ROG ally and ROG ally X (initial support, community provided)
    • Google GID8 headset
    • j5create USB-C JCD373
    • Logitech Sight (as standalone device)
    • MNT Pocket Reform
    • Netprisma LCUR57 and FCUN69
    • Parade PS188
    • Quectel DFOTA devices
    • Raspberry Pi Pico
    • SteelSeries Nova 5
    • Telink DFU-HID devices

    This release adds the following features:

    • Add API so that gnome-firmware can record devices for emulation
    • Save the emulation-tag devices to the database rather than the config file

    This release fixes the following bugs:

    • Allow controlling the libdrm dependency when building
    • Check the logitech-bulkcontroller response packet length correctly
    • Convert the wacom-raw device command buffers to Rust format
    • Convert the wacom-usb descriptor to Rust format
    • Fix a kinetic-dp enumeration regression
    • Fix compiling on Android
    • Fix compiling without git installed
    • Fix FU_PLUGIN_RULE_BETTER_THAN to ignore lower priority devices
    • Ignore needs-reboot and needs-shutdown for emulated devices
    • Read the Intel Thunderbot NVM firmware in a more efficient way
    • Simplify firmware loading from a specific offset
    • Skip the serio self tests if gnutls support was disabled
    • Speed up fwupdtool by only loading engine features when required
    • Speed up getting details about local firmware archives

    This new major release beaks the libfwupd ABI to make the following changes:

    • Drop legacy signing formats for verification of metadata and firmware
    • Reduce the runtime memory usage and CPU startup cost significantly
    • Remove all the long-deprecated legacy CLI tools
    • Remove libgusb and GUdev from plugins and use libusb and sysfs instead
    • Stream firmware binaries over a file descriptor rather than into memory

    This release adds the following features:

    • Add a config option to ignore firmware requirements for development
    • Add a device problem when the device needs a reboot
    • Add API to allow uploading reports in gnome-firmware
    • Add Darwin support to the build helper
    • Add HSI tests for Arrow and Meteor Lake CSME
    • Add support for fwupdtool esp-list --json
    • Add support for more modify-config options
    • Add the privacy policy URL in the remote config
    • Allow device flags to be loaded from the local cab archive
    • Allow overriding the detected EFI framebuffer size in the config file
    • Allow specifying devices to emulate in the config file
    • Apply revocations to SbatLevelRT when required
    • Choose the ESP correctly in more cases when using UEFI capsule updates
    • Detect BlueTooth devices by GATT service UUID
    • Drop the SuperIO plugin as it will not work without rawio
    • Listen to the netlink udev socket rather than using GUdev
    • Parse EFI LZ77 compressed sections when required
    • Support large sections in EFI FFS3 volumes
    • Support more CRC-32 and CRC-16 types

    This release fixes the following bugs:

    • Abstract out the D-Bus IPC layer to allow using Android Binder in the future
    • Apply systemd hardening to the service files
    • Coldplug udev devices with a predictable order
    • Completely deallocate devices when physically removed
    • Create the fuzzing corpus at build time to avoid shipping binary test blobs
    • Do not add counterpart IDs as visible firmware-matchable GUIDs
    • Do not allow system suspend during a composite update
    • Do not assume all block devices are backed by USB devices
    • Do not create a FuSynapticsMstDevice for every DPAUX device
    • Do not read OptionROM on Apple hardware
    • Do not run the quirk query multiple times on the same device
    • Do not set a zero-length device name when matching the vendor name
    • Do not skip closing the USB device if the HID interface is not found
    • Do not skip the first EFI_FILE in the EFI_FILESYSTEM
    • Do not verify the SSL hostname when using DISABLE_SSL_STRICT=1
    • Do SG_IO INQUIRY_CMD directly rather than relying on the udev prober
    • Emit a signal when a request is no longer valid
    • Fix several 'underflowed constants' found using Coverity
    • Get the DT-provided 'BIOS version' correctly
    • Get the new bluetooth revision after deploying the update
    • Keep polling Redfish even when encountering task errors
    • Only generate the client certificate when required
    • Only read back the thunderbolt firmware if we can parse the NVM
    • Pass emulation data using a file descriptor
    • Recalculate the device supported flag when reparenting devices
    • Reduce memory allocations by ~2Mb when loading the BGRT image
    • Remove the polling for Logitech Unifying receivers to improve battery life
    • Require a DPCD OUI to match DPAUX devices
    • Respect the device priority when matching in the device list
    • Respect the interface retry count when using an auto-detected FuHidDevice
    • Retry the post-update NVMe open action to fix BC901 reload
    • Save the metadata with a more predictable name so we can use jcat-tool
    • Upload reports from fwupd-refresh if AutomaticReports is true
    • Use a more standard ATA OUI vendor name match
    • Use the first preferred CRTC screen resolution if not using efifb
    • Use /usr/bin/env for wider Python compatibility
    • Warn users needing to fwupdmgr activate before uploading signed reports

    This release adds support for the following hardware:

    • Algoltek USB card readers
    • AMD Kria SoM
    • Cable Matters USB Hub
    • Cinterion fdl-based devices
    • CY6611 EZ-USB HX3PD
    • Dell K2 Dock
    • Elan 0C9F fingerprint devices
    • HP TBT4 100W G6 Dock
    • HP TBT4 Ultra 180W/280W G6 Dock
    • Huddly L1, S1 and Crew
    • Jabra PanaCast
    • Lenovo Legion HID2 devices
    • Lenovo ThinkSmart Bar
    • Logitech Lemmy
    • Logitech Sight
    • Logitech Tap Touch Screen
    • MediaTek scaler devices
    • Parade PS185
    • Qualcomm S5gen2 BLE devices
    • Telink Dual Keyboard

    This release fixes the following bugs:

    • Add support for capsule on disk for Dell systems
    • Do not re-use the connection cache to fix Redfish BMC restart
    • Exclude known recovery partitions when choosing an ESP volume
    • Fix the VLI usb3 private flag registration

    This release adds support for the following hardware:

    • More Mediatek scaler devices
    • Parade USB hubs

    This release fixes the following bugs:

    • Fix a regression in 1.9.22 that caused some devices not to probe correctly
    • Try harder to get a valid response when flashing usi-dock devices

    This release adds the following features:

    • Add a device test for the Acer D501 dock

    This release fixes the following bugs:

    • Add a PCB tag in the usi-dock GUID to distinguish different revisions
    • Add explicit hidraw permission to fwupd.service to fix several devices
    • Always load the flashrom plugin when using coreboot
    • Be explicit with the rts54hub detach retry delay to fix the Acer D501
    • Be more careful when setting thelio-io version strings
    • Fix a critical warning if a device returns unexpected data from DFU upload
    • Fix a critical warning if the DMI manufacturer is an empty string
    • Fix several reported integer overflows from Coverity
    • Fix the Blackbird and Talos II baseboard details
    • Fix transient version number issue after flashing wacom-usb devices
    • Increase the cros_ec acquiesce delay to manage additional reboots
    • Only accept valid ASCII cabinet filenames
    • Only require udevdir when gudev support is enabled
    • Only show one PixArt receiver device per physical device
    • Set the rts54hub version in more cases
    • Speed up the daemon self tests by ~60%
    • Use the bootloader build-timestamp as the fallback HWID BIOS version

    This release adds support for the following hardware:

    • Framework SD
    • Raspberry Pi 5 (unofficial)

    This release adds the following features:

    • Add a fwupd.conf option to ignore CHID requirements for development

    This release fixes the following bugs:

    • Allow loading Wacom device flags from metadata
    • Check for needs-shutdown like we do needs-reboot
    • Fix updating the Aerox 3 Wireless Mouse

    This release adds support for the following hardware:

    • Synaptics Carrera devices
    • Wacom Movink devices

    This release adds the following features:

    • Add some API to allow uploading reports for use in gnome-firmware
    • Allow the user to upload the entire devicelist to the LVFS

    This release fixes the following bugs:

    • Correctly detect Synaptics Cayenne and Spyder firmware
    • Do not offer the UEFI DBX update on Lenovo ideacentre 300-20ISH
    • Explicitly enable shadow stack support in fwupd.service
    • Fix a potential buffer overread when reading the algoltek-usb version number
    • Fix the CET HSI test by rewriting it in assembly
    • Fix using --verbose in fwupdmgr
    • Ignore --p2p when downloading the metadata signature

    This release adds support for the following hardware:

    • FPC FF2 fingerprint devices

    This release adds the following features:

    • Drop heap as part of a housekeeping action
    • Retry downloads to workaround flaky network connections

    This release fixes the following bugs:

    • Assume new ME versions are called CSME
    • Fix a buffer-overread when parsing invalid CoSWID entity data
    • Fix a logic thinko when parsing GUID strings
    • Fix downloading files on Windows with libcurl >= 7.77.0
    • Revert back to a simpler syscall blocklist-based filter

    This release adds support for the following hardware:

    • Acer U32 dock
    • Luxshare 7-in-1 dock
    • Pixart models 2404, 4206, 2440, 2418, 2752, 2840 and 2818

    This release fixes the following bugs:

    • Fix a small memory leak in fwupdmgr
    • Use simple allow-listing for the syscall filter

    This release fixes the following bugs:

    • Capture device status changes when in bootloader mode
    • Change the systemd SystemCallFilter to an allowlist
    • Detect when a CCGX dock forbids downgrade
    • Do not add an overly-generic instance ID to CrosEC devices
    • Do not read OptionROM verification data on Apple hardware
    • Emit a signal when a user-request is no longer valid
    • Fix a potential crash when parsing invalid CBOR data
    • Properly show SPDX project licenses with AND as a delimiter
    • Verify that syscalls are being filtered correctly at startup

    This release adds support for the following hardware:

    • Asus DC201
    • Realtek Gen1 RTS541x

    This release adds the following features:

    • Prefer zstd over xz for metadata
    • Relicense a few remaining plugin files to LGPL-2.1+

    This release fixes the following bugs:

    • Correctly record UEFI success if adding ESRT nodes
    • Defer the DP Aux MST scanning on hotplug to workaround a kernel bug
    • Do not do the post-update version check if the device needs-reboot
    • Fix a fastboot warning when loading device
    • Fix a possible warning in fwupdmgr when excluding releases
    • Fix a qsi-dock warning when writing chunks
    • Ignore attribute-changed to work around a regression in macOS
    • Ignore ZFS zvols when finding the default ESP
    • Mark Pluton TPMs as part of the main CPU
    • Put the fwupd-efi verbose debugging in the journal
    • Recognize zfsbootmenu in ESP detection heuristic

    This release adds support for the following hardware:

    • Acer T34 and U33 docks
    • Qualcomm Series 5 Gen 1 and Gen 2 and Series 3 Gen 1 and Gen 2 devices
    • Several Puya SPI chips
    • VIA VL822 C0

    This release adds the following features:

    • Allow loading in parameters for the test device from fwupd.conf
    • Ensure LVFS remotes are changed from .gz to .xz
    • Store the install duration in the history database

    This release fixes the following bugs:

    • Drop OverrideESPMountPoint references in uefi-capsule
    • Fix a potential double-free when writing AVer firmware
    • Only request the BOS descriptor when bcdUSB > 0x0200
    • Use the root device order when sorting device children

    This release adds the following features:

    • Allow plugins to opt-into a default device GType

    This release fixes the following bugs:

    • Correctly detect ARM32 and RISC-V UEFI binaries
    • Correctly migrate the database schema from very old fwupd versions
    • Fix critical warnings when using FWUPD_DBUS_SOCKET= on macOS
    • Fix DS-20 descriptors by opening the GUsbDevice earlier
    • Fix updating the fingerprint reader on the Framework 13 and 16 laptop
    • Fix warning when probing devices using the metadata allowlist
    • Only recover the version format for specific devices

    This release adds support for the following hardware:

    • Poly Studio V52

    This release adds the following features:

    • Add a timer inhibit if the daemon took a long time to startup
    • Add a concept of 'Test Mode' rather than enabling specific plugins
    • Do not idle-quit the daemon if there is a connected D-Bus client

    This release fixes the following bugs:

    • Allow plugins to opt-out of the child-device first depsolve
    • Allow setting multiple flags in LVFS::DeviceFlags
    • Do not migrate config comments for removed keys
    • Do not request the Advantech BMC to reboot
    • Do not warn the user about ESP when using MBR
    • Fix a critical warning when adding a PixArt wireless device
    • Fix migration of legacy config files
    • Only save config values to the mutable config file
    • Parse DS-20 descriptors earlier in device setup
    • Store the version format in the history database to fix offline reports
    • Use the correct GUID for matching realtek-mst and parade-lspcon

    This release adds support for the following hardware:

    • GoodWay Acer Dock

    This release adds the following features:

    • Add remote modification support to fwupdtool
    • Add support for more modify-config options
    • Generate HTML pages for all man pages

    This release fixes the following bugs:

    • Assume the legacy LVFS::UpdateRequestId tag is non-generic
    • Avoid crashing the daemon if not using udisks
    • Correctly mark the CPU as supported
    • Correctly match invalid EFI partitions
    • Do not change the device status until the action has completed
    • Do not require systemd for fwupdtool modify-config
    • Enable access to the home interface for snap
    • Fix an assertion when enabling lvfs-testing for the first time
    • Fix a possible crash in fwupdtool build-cabinet
    • Handle systems with more than one ccp device
    • Only check AMD CPUs for SHSTK, not IBT
    • Only write the mutable fwupd.conf with the current values
    • Re-evaluate supported every time pci-psp attributes are refreshed
    • Show "CET OS support" on AMD systems too

    This release adds support for the following hardware:

    • AVer CAM340plus
    • AVer VB342 Pro
    • More Algoltek devices

    This release adds the following features:

    • Allow exporting 'offline' reports for manual upload

    This release fixes the following bugs:

    • Add some recovery partition names to ignore for ESP selection
    • Check for CET and SMAP on non-Intel x86 processors too
    • Correctly mark the CPU as supported in the HSI tests
    • Do not fail on probing downstream Synaptics MST ports
    • Do not offer to change BIOS settings that are already set
    • Do not prefer msftdata when choosing the default ESP
    • Do not show spurious device request flags
    • Fix a missing build dependency to fwupdplugin-self-test
    • Fix a segfault when using zlib-ng instead of zlib
    • Fix updating Jabra 410, 510, 710 and 810 devices
    • Match more community-supported branches
    • Remove the Intel SPIBAR proxy support as the mtd module works
    • Show a better error when the ESP is missing
    • Show an error if the post-update version does not match exactly
    • Speed up Synaptics MST device enumeration

    This release adds support for the following hardware:

    • Algoltek USB devices
    • Luxshare Quad USB4 Dock

    This release adds the following features:

    • Add support for not_hardware requirements
    • Add support for loongarch64
    • Add support for per-release priority attributes
    • Make USB claim retry count configurable across devices

    This release fixes the following bugs:

    • Compare the HID report value when checking for duplicates
    • Consider the component priority when installing composite updates
    • Deploy the CCGX firmware correctly the first time
    • Do not export the 'main-system-firmware' and 'cpu' GUIDs
    • Enforce fwupd version requirements client side
    • Fix Genesys 'failed to get static tool info from device' error
    • Fix potential 'dereference before null check' in ccmx-dmc
    • Fix the 'already registered private FuMmDevice flag with value' warning
    • Fix the 'assertion backend_id != NULL failed' runtime warning
    • Fix Wacom USB device emulation by recording the composite phases
    • Generate generic request message text where possible
    • Hide HTTP passwords in fwupd debugging logs
    • Let the client know what interaction is expected
    • Make all critical warnings into backtraces for non-release builds
    • Never obsolete the wrong HSI attribute
    • Never show a HSI index that is impossible
    • Only apply fastboot plugin to modem devices supporting fastboot
    • Only send interactive requests when the sender is alive
    • Remove the now-obsolete Synaptics MST cascade device scanning
    • Replace the Redfish KCS user if required
    • Restrict mediatek-scaler devices on specific hardware only
    • Skip any recovery partitions when detecting ESP

    This release adds support for the following hardware:

    • AVer CAM520 Pro3

    This release adds the following features:

    • Add a new generic request for the device power cable

    This release fixes the following bugs:

    • Disable scanning for synaptics-mst cascade devices until it is more stable
    • Disable the mediatek-scaler plugin by default until we do VCP probing
    • Do not enforce additional requires on emulated devices
    • Fix a potential critical warning when listing the contents of the ESP
    • Fix 'fwupdmgr get-devices' with deeply nested devices
    • Make the Kinetic DP GUID specific to the customer
    • Only use the USB open-retry behavior when required
    • Remove obsoleted HSI attributes earlier
    • Remove the timestamp from gzip to achieve a reproducible build
    • Support cabinet archives produced using 'makecab.exe'
    • Use metadata authentication when using 'fwupdtool refresh'
    • Use the generic ti-tps6598x GUID and instead enforce requirements

    This release adds support for the following hardware:

    • Lenovo X1 Yoga Gen7 530E
    • Advantech BMC devices

    This release adds the following features:

    • Add a DP AUX device subclass and port the Synaptics MST plugin to it
    • Add a feature flag for non-generic requests where translations are required
    • Hide generic VID/PIDs to avoid accidental firmware matches
    • Optionally set the modem carrier configuration as the branch name
    • Rename 'fwupdmgr sync-bkc' to 'fwupdmgr sync' and also consider the branch
    • Require additional requirements for devices using non-OEM USB VIDs
    • Set the waiting-for-user status when sending a request
    • Support uSWID SBOM data with LZMA compressed payloads

    This release fixes the following bugs:

    • Accept any registry versions when parsing Redfish message IDs
    • Do not read the AMDGPU DM DPCD on broken firmware versions
    • Do not save obsoleted HSI attributes into the history database
    • Fix a crash when some DRM devices are hotplugged
    • Fix a possible crash in fu_io_channel_read_raw()
    • Fix parsing LZMA-compressed EFI sections
    • Only use the architecture-specific GUID for UEFI dbx quirk matching
    • Parse all the type 42 SMBIOS tables
    • Prompt the user to reboot after applying internal Wacom firmware
    • Respect no-serial-number flags when using GUID matches
    • Use the etag when patching Redfish resources
    • Use the username and password to get metadata when set

    This release adds support for the following hardware:

    • Kinetic SST/MST DisplayPort converters
    • Wacom Cintiq Pros (DTH172, DTH227)

    This release adds the following features:

    • Add support for child device requirements in metadata
    • Allow to have more than one host BKC
    • Delete BootNext as a post-reboot action to work around broken firmware
    • Parse cabinet archives internally without libgcab
    • Use close-ended mode for eMMC FFU to speed up firmware updates

    This release fixes the following bugs:

    • Do not abort 'fwupdmgr update' when one updatable device has a problem
    • Do not call unimplemented functions when running under Wine
    • Do not modify the BootOrder by default to work around broken firmware
    • Do not treat an immutable buffer as mutable in the vli plugin
    • Drop the concept of HSI:INVALID:missing-data and just show HSI-0
    • Fix a small memory leak in bulkcontroller when getting the device info
    • Inhibit the Nordic HID dongle when there are peripheral updates pending
    • Retry claiming the interface if the USB device returns BUSY
    • Send the Olson location to bulkcontroller rather than the timezone
    • Use a longer timeout for the post-erase STM32 GetStatus
    • Wait 60 seconds for the bulkcontroller device after writing firmware

    This release adds support for the following hardware:

    • Logitech Rally System devices
    • More PixartRF HPAC devices
    • More Synaptics Prometheus fingerprint readers
    • Some Western Digital eMMC devices
    • VIA VL830 and VL832

    This release adds the following features:

    • Add a launchd agent for macOS
    • Add a new security attribute for BIOS capsule updates to be enabled
    • Add functionality to fix specific host security attributes
    • Add global information from the context into the report data
    • Add support for coSWID payload sections
    • Add support for parsing the EDID
    • Allow adding only-quirk instance IDs from quirk files
    • Install a sysusers.d systemd file when using -Dsystemd_unit_user

    This release fixes the following bugs:

    • Allow devices to require a connected display
    • Allow Wacom modules to specify a status polling interval
    • Do not show Intel CET unsupported as success
    • Do not show multiple Genesys GL32xx devices for the same physical device
    • Fix a fuzzing timeout in the HID descriptor parser
    • Recalculate the SUPPORTED flag after adopting a child device
    • Reduce the amount of memory used when chunking large firmware
    • Speed up logitech-bulkcontroller firmware updates
    • Stop reading ownership and TPM flashes left in Dell plugin
    • Try to use the LVFS when using report-history --force
    • Write the coSWID TAG_ID as a bytestring when possible

    This release adds support for the following hardware:

    • AMD dGPUs, Navi3x and above
    • Foxconn SDX12, SDX55 and SDX6X devices
    • Google Rex Intel USB-4 retimers
    • MediaTek DP AUX Scalers
    • Quectel EM160 module
    • Star Labs StarBook Mk VIr2
    • VLI VL105-VL109
    • Wacom DTH134 and DTC121 Tablets

    This release adds the following features:

    • Add optional support for Passim, a local caching server
    • Allow using fwupdtool get-devices --json

    This release fixes the following bugs:

    • Allow adding UF2 devices without a filesystem UUID
    • Correctly read the size of Synaptics Panamera MST firmware
    • Do not return historical results with no AppStream ID
    • Fix parallel build when using a machine with a lot of cores
    • Fix uninhibiting ModemManager after the fastboot flash has completed
    • Make firmware USI dock flashing more reliable
    • Record the update state of success when the device is returned to runtime
    • Remove the default-installed fwupd-refresh systemd preset
    • Sort composite updates by the device order when required

    This release adds support for the following hardware:

    • EPOS ADAPT 1x5
    • Fibocom FM101
    • Foxconn T99W373
    • Genesys GL3525S USB Hub
    • HP Rata/Remi BLE Mice
    • Luxshare Quad USB4 Dock
    • System76 Launch 3, Launch Heavy 3 and Thelio IO 2

    This release adds the following features:

    • Add a new HSI attribute that detects any missing Intel GDS mitigation
    • Allow configuring the refresh interval per-remote rather than per-system
    • Remove the libsoup-2.4 compatibility code
    • Show the firmware release checksum in CLI tools

    This release fixes the following bugs:

    • Correctly query the Steelseries Fizz version on reconnect
    • Correctly wait for USB replug on macOS
    • Do not add gl32xx disk partitions as extra devices
    • Do not assume the logical block size is always 0x200
    • Ensure the AppStream ID is set on historical releases
    • Enumerate Synaptics MST devices correctly
    • Fix a possible CFU crash when adding modules
    • Fix 'fwupdmgr get-history --json' output to only show one release
    • Fix 'fwupdmgr modify-remote' on ChromeOS
    • Fix incorrect OEM trusted reports flags being set when not matching
    • Fix regression in returning device history with libxmlb 0.3.12
    • Fix some Wacom hardware by only attempting retries for the busy error state
    • Fix transaction timed out issue for T99W373 QDU device
    • Handle cros-ec boards with '_v' in their name
    • Ignore the authentication when username and password are both empty
    • Load the effective size of a PE section instead of raw size
    • Set some feature flags when non-interactive
    • Set the HWIDs correctly when running macOS
    • Use a much larger USB timeout for STM32 erase operations
    • Use the correct offsets when checking Synaptics MST Spyder devices
    • Use the correct URI when downloading from authenticated remotes
    • Use /var/run when /run/lock does not exist

    This release adds support for the following hardware:

    • Genesys GL352350 and GL3590
    • Logitech Huddle
    • Microsoft USB-C Travel Hub
    • PixArt BLE HPAC OTA
    • Quectel RM520
    • Synaptics Triton devices
    • VIA VL122, VL817S and VL822T
    • Wacom One 13 and One 12 Tablets

    This release adds the following features:

    • Add the expected result to each HSI test attribute
    • Allow autodetection when using fwupdtool firmware-parse
    • Allow devices to only accept explicitly specified release versions
    • Allow filtering by release flags from fwupdtool and fwupdmgr
    • Allow filtering by remote when looking for trusted reports
    • Drop the libefiboot dependency and generate UEFI DPs directly
    • Ensure that BootService-only variables cannot be read in runtime mode
    • Parse the various SBAT sections from PE firmware
    • Record the NVRAM space used as report metadata
    • Show the user a warning when the ESP may not be valid
    • Speed up the daemon startup by 35% and reduce RSS by 12%
    • Support reading and writing EFI variables on Windows

    This release fixes the following bugs:

    • Check only the EFI executables from the boot menu when using --force
    • Correctly obtain the Thunderbolt is_native controller attribute
    • Deduplicate the remotes as required
    • Do not accidentally depend on python 3.9
    • Do not misuse the offset as an address in the SREC parser
    • Do not truncate feature reports to fix Wacom ID6 update
    • Fix parsing of IGSC code firmware
    • Get the Jabra GNP device name from the device
    • Ignore small ESP block devices if there are multiple choices
    • Never install a shim too new for the system
    • Only add the little-used _REV instance IDs by request
    • Use a CapsuleOnDisk filename supported by InsydeH2O

    This release adds support for the following hardware:

    • Belkin Thunderbolt 4 Core Hub dock
    • CE-LINK TB4 Docks
    • Genesys GL32XX SD readers
    • Genesys GL352350 USB 3.1 hub
    • Nordic HID devices without DFU support
    • TUXEDO InfinityBook Pro 13 v3
    • Wacom tablets with ID9 Bluetooth chipsets

    This release adds the following features:

    • Beep the console when CLI programs are waiting for user input
    • Bump requirements of various dependencies to remove a lot of fallback code
    • Show devices with problems in fwupdmgr get-upgrades

    This release fixes the following bugs:

    • Auto-detect the BCM57xx OEM PCI cards with double the expected EEPROM
    • Disable ThunderBolt retimer offline mode for some hardware
    • Do not assume a file descriptor of 0 is invalid when updating NVMe hardware
    • Fix discovery of Nordic peripherals connected via the dongle
    • Fix high memory usage when writing some EFI filesystem images
    • Fix USI dock devices with an incorrect factory-set firmware version
    • Ignore a client refresh on a non-download remote to fix old versions of KDE
    • Ignore the immutable flatpak config file file permission being incorrect
    • Limit the number of possible file objects in the EFI filesystem
    • Make the installed size smaller by deduping and filtering assets
    • Only expose --force for security attributes for unsupported builds
    • Require the user to manually replug the USI dock after update has completed

    This release adds support for the following hardware:

    • AVer FONE540
    • Genesys GL3525 USB hubs
    • Goodix Touch controllers
    • Jabra Evolve 65e/t and SE, Evolve2, Speak2 and Link devices

    This release adds the following features:

    • Add a new device-wait command to fwupdmgr for use in boot-time scripts
    • Add a report flag if generated by the OEM which can be used for policy
    • Add CCID decoding support to pcap2emulation
    • Add fwuptool build-archive cmd to allow building firmware without gcab CLI
    • Add support for NVMe CA3 activation
    • Allow setting device flags and version numbers from the metadata
    • Allow specifying the MTD metadata offset and size
    • Allow using basic auth when uploading a report
    • Autogenerate enums and structures (with default values) from Rust format
    • Delete the obsolete .gz files if the remote is now using .xz
    • Read the AGESA Bootloader and TEE versions from the kernel
    • Tag releases with an extra flag if they have a report we trust

    This release fixes the following bugs:

    • Add the latest dbx fixups for BlackLotus
    • Allow setting the remote username and password on the CLI
    • Be more careful parsing the kernel cmdline
    • Convert the man pages to MarkDown format and use a built-in converter
    • Enable the vendor-directory remote by default
    • De-duplicate releases by the container checksum
    • Enable the retimer offline mode depending on HWIDs
    • Escape the username and password when using basic authentication
    • Fix a pci-mei crash by more carefully converting enums to strings
    • Fix Dell dock by triggering passive flow for USB4 subcomponents
    • Fix the version detection for SteelSeries Bluetooth mode
    • Have fwupd-refresh.timer trigger once per hour on average
    • Implement enough of the CFI specification to be able to update a device
    • Allow the firmware time to process commands to avoid a Wacom device crash
    • Invalidate the XMLb cache when installing new fwupd versions
    • Make the config file be called fwupd.conf and add a useful manpage
    • Move the expected default plugin config value to the code
    • Prefer local remotes when deduplicating releases with the same version

    This release adds support for the following hardware:

    • CH347 SPI programmer
    • Logitech Tap devices
    • More Logitech Unifying receivers
    • Nordic HID MCUboot direct-xip
    • nRF52 Desktop Keyboard
    • Wacom Cintiq Pro 27

    This release adds the following features:

    • Add pcap converter which allows emulating devices from a Wireshark dump
    • Add the ability to dump TPM firmware for future use
    • Optionally retain firmware in a backup remote
    • Record the ESP type in the firmware report sent to the LVFS

    This release fixes the following bugs:

    • Accept application/octet-stream for archives when the mime database is missing
    • Add the latest dbx version version fixups as Microsoft removed another entry
    • Assume DFU appIDLE if GetStatus is not implemented
    • Do not require signatures for local or directory remotes
    • Do not use pandoc to build the man pages
    • Enhance Qubes functionality to use JCat
    • Fix a CCGX 'usbfs: process did not claim interface 1 before use' warning
    • Fix a compile warning when using a new libqmi version
    • Fix a critical warning when parsing an empty kernel cmdline
    • Fix a synaptics-cape regression where the firmware pauses for INTR
    • Fix the defines for HFSTS6 enforcement policy
    • Fix the i2c name properly for ElanTP hardware
    • Fix the name of the MTD Intel SPI controller
    • Set the release remote when installing archives
    • Use the powerd power type information to better set AC levels

    This release adds support for the following hardware:

    • Framework Audio Card
    • Lenovo ThinkPad TBT3-TR Gen 2
    • Wacom Intuos BT S Gen 3

    This release fixes the following bugs:

    • Allow setting the package user agent before the client has connected
    • Fix a small memory leak when refreshing metadata

    This release adds the following features:

    • Add support for replaying USB devices so they can be emulated in CI
    • Allow desktop software to inhibit the system to prevent updates
    • Allow using requirements with depth=0 and no parent
    • Auto-set the CCGX remove-delay now we parse DMC subcomponents
    • Detect and warn users with the broken NVMe firmware 3B2QGXA7
    • Print errors as JSON objects when using fwupdmgr --json

    This release fixes the following bugs:

    • Allow installing battery firmware updates even when the power is too low
    • Correctly fall back to the compatible vendor when FDT vendor is missing
    • Detect CCGX factory mode and set a non-zero version
    • Detect fixed Insyde firmware that can actually use Capsule-on-Disk
    • Do not make any of the HWIDs setup failures fatal
    • Fix a critical warning when parsing an empty kernel cmdline
    • Fix a small memory leak when installing TPS6598x firmware
    • Fix compiling with -Dbuild=library for Flathub
    • Fix fwupdtool firmware-convert to work with image-less formats
    • Fix regression in downloading files in fwupdtool
    • Fix SMBIOS struct parsing when the tag section ends with NUL
    • Indicate HSI attributes that will only be returned for specific CPU vendors
    • Only accept application/x-xz compression for the metadata payload
    • Only offset the IPMI user ID when using Lenovo XCC
    • Prefer the Intel USB4 plugin over the Thunderbolt plugin when required
    • Require at least twice the capsule size in the ESP when updating
    • Save all the device flags in the pending database correctly
    • Set the device percentage and status for the duration of the update
    • Show the 4XX download failure in the CLI error output
    • Speed up regenerating the MOTD when installing composite devices
    • Use an updated shim if provided during for capsule update
    • Use strict snap confinement

    This release adds support for the following hardware:

    • CalDigit Element Hub
    • CalDigit TS4 Dock

    This release adds the following features:

    • Add a PE/COFF firmware parser to allow reading coSWID SBOM data
    • Allow dumping CFI SPI chips using devices like CH341a
    • Refactor the HWIDs functionality to include FDT data

    This release fixes the following bugs:

    • Add back a legacy eMMC GUID to fix a regression
    • Always search for uSWID SBOM data in the image
    • Do not allow LZX compressed cabinet archives
    • Fallback to the checksum if the metadata artifact is invalid
    • Improve FDT parsing compatibility with new OpenBMC images
    • Never call grub2-probe without arguments
    • Respect user requested paths for the ESP even if they are not volumes
    • Speed up ChromeOS startup by a huge amount when using directory remotes
    • Verify the Synaptics RMI signature in more cases

    This release adds support for the following hardware:

    • Quectel RM520
    • StarBook Mk VI
    • System76 launch_heavy_1

    This release adds the following features:

    • Add an interactive request for re-inserting the USB cable
    • Add SHA384 support for TPM hashes
    • Add X-FingerprintReader, X-GraphicsTablet, X-Dock and X-UsbDock categories
    • Allow specifying OR parent requirements in metadata

    This release fixes the following bugs:

    • Add the fwupd version to the HSI result if the chassis is invalid
    • Allow getting the ESP when there is a block device with no filesystem
    • Allow reinstalling on devices with only-version-upgrade set
    • Do not require the TPM event log to have all reconstructions
    • Fix a tiny memory leak when parsing signed reports
    • Ignore failure to mount the ESP if unsupported
    • Never allow using SHA-1 for checksum validation
    • Return a more useful error if USB recovery failed
    • Skip the fwupdx64.efi BootXXXX entry when measuring system integrity
    • Speed up daemon startup using prepared XPath queries
    • Suggest to turn on ThunderboltAccess for Lenovo systems
    • Use better defaults if the config file is missing

    This release adds support for the following hardware:

    • More Solidigm NVMe devices
    • More Synaptics Cape devices
    • More Synaptics Prometheus devices
    • Most Texas Instruments USB-4 docks
    • Scaler support for Wacom USB devices
    • Several new Wistron USB-C docks

    This release adds the following features:

    • Add BIOS rollback protection support for Dell and Lenovo systems
    • Generate OVAL rules for openSCAP evaluation
    • Show the signed reports from QA teams in client tools

    This release fixes the following bugs:

    • Add a X-Gpu category for new hardware support
    • Add more ChromeOS metadata to the report attributes
    • Ensure the device name is set for Intel USB4 devices
    • Fix a critical DFU CSR warning when deploying firmware
    • Fix a Synaptics RMI issue when updating non-secure devices
    • Match more device properties when using GetDetails
    • Move AMD platform rollback protection to level 4
    • Use the correct AppStream ID for the Key Manifest failure
    • Wait for the Intel GPU to come back after updating

    This release adds support for the following hardware:

    • Logitech Whiteboard cameras
    • More Goodix MoC devices
    • Several QSI Docks

    This release adds the following features:

    • Add a new HSI check for the leaked Lenovo 'Key Manifest' hashes
    • Measure system integrity when installing UEFI updates
    • Record more host DMI data when submitting a report for dbx failures
    • Use xz-compressed metadata to reduce bandwidth used by ~25%

    This release fixes the following bugs:

    • Add documentation for three existing HSI attributes
    • Add re-insert requirement for Analogix devices
    • Allow parsing metadata more than 1MB in size
    • Do not follow symlinks when searching for ESP devices
    • Ensure the config file permission is correct for built-in plugins
    • Fix a compile failure when compiling without efiboot
    • Fix a regression when using fwuptool install-blob with FMAP firmware
    • Only count the Microsoft hashes when getting the dbx version
    • Only use the IFD when the system is Intel-based
    • Support loading CoSWID when only one role has been set

    This release adds support for the following hardware:

    • Anker Thunderbolt 4 Mini Hub
    • ELAN haptic hardware
    • Fingerprint lenfy devices
    • Goodix GF3258WNC
    • Intel discrete GPUs (experimental)
    • More Star Labs laptops
    • QSI Godzilla Creek Reference Hub

    This release adds the following features:

    • Reduce the installed package size by more than 30%
    • Translate more interactive messages

    This release fixes the following bugs:

    • Allow disabling a DFU device when required
    • Fix a regression when getting the i2c bus number
    • Fix a small memory leak when reloading the parade-lspcon device
    • Fix installing the dbx update when using fwupdtool
    • Improve writing CoSWID and uSWID metadata
    • Only include the last 5 releases in the installed metainfo file
    • Only request the BOS descriptor for newer libgusb versions
    • Prevent high memory usage when loading corrupt SREC files
    • Try harder when trying to find the default ESP volume
    • Use a higher compression preset for the UEFI splash images

    This release adds support for the following hardware:

    • Focaltech touchpads
    • FPC fingerprint readers
    • Supermicro machines using Redfish

    This release adds the following features:

    • Add a new android-boot plugin to update specific block devices
    • Add new plugin to display SMU firmware version on AMD APU/CPU
    • Add support for platform capability descriptors so devices can set quirks
    • Move the generic Intel Goshen Ridge code out to a new plugin

    This release fixes the following bugs:

    • Allow specifying the ESP when applying the dbx update
    • Always check the BDP partitions when getting all the possible ESPs
    • Correctly update Wacom AES devices
    • Disable changing sleep mode on Ryzen 6000 systems
    • Do not show the 'may not be usable while updating' message for DBX updates
    • Expose Pine64 PinePhone Pro MTD as Tow-Boot
    • Fix a critical warning when issuing Secure Boot modem AT commands
    • Fix a fuzzing crash when parsing malicious FDT data
    • Fix aligning up addresses greater than 4GB
    • Fix a possible crash when dumping VBE firmware
    • Fix a possible critical warning when parsing cabinet archives
    • Fix a regression when parsing pixart-rf firmware
    • Fix a small memory leak when parsing UF2 files
    • Fix checking for invalid depth requirements
    • Fix parsing the coSWID firmware ID when encoded as a UUID
    • Fix parsing uSWID uncompressed metadata
    • Fix uploading to DFU-CSR devices
    • Limit the archive size to 25% of the RAM, or 4G
    • Load coSWID metadata from a uSWID MTD block device
    • Never save the Redfish auto-generated password to a user-readable file
    • Only create users using IPMI when we know it's going to work
    • Write all the CCGX metadata block as intended

    This release adds support for the following hardware:

    • Corsair SABRE RGB PRO Gaming mouse
    • More Sonix CAM devices
    • More Intel Goshen Ridge USB-4 docks

    This release adds the following features:

    • Add a translated title and long description for HSI security attributes
    • Add support for loading a machine-default BIOS settings policy
    • Add support for reading and writing BIOS settings
    • Allow loading BIOS settings for host emulation
    • Prompt users to fix some BIOS configuration issues

    This release fixes the following bugs:

    • Actually show provided AppStream security issues
    • Add Quectel secure boot status AT commands
    • Correctly detect CET IBT
    • Do not assert when running with no plugins
    • Do not require UEFI capsule updates for checking TPM PCR0
    • Do not show HSI events where we changed the spec result value
    • Fix applying the latest DBX update
    • Include vfat in the list of possible BDP partition types
    • Install all devices with the same composite id in fwupdtool
    • Only fail the kernel HSI test for specific taint reasons
    • Only show changed events in fwupdmgr security
    • Update vulnerable CMSE versions from CSMEVDT data

    This release adds support for the following hardware:

    • Elan non-HID touchpads
    • Google Prism
    • LabTop Mk III
    • ThinkPad Thunderbolt 4 Dock
    • ThinkPad Universal Smart Dock

    This release adds the following features:

    • Add resolution flags to each security attribute failures for the user
    • Allow loading in emulated host profiles for debugging
    • Check if Intel TME has been disabled by the firmware or platform
    • Wait for the system to acquiesce after doing each update

    This release fixes the following bugs:

    • Do not use CoD even when advertised on non-aarch64 platforms
    • Fix a crash when updating the Logitech Bolt radio device
    • Fix a critical warning when parsing an invalid PHAT record
    • Fix a critical warning when parsing invalid FDT firmware
    • Fix fwupdmgr security when plugins are added to the blocklist
    • Fix parsing SMBIOS data to correct the device hardware IDs
    • Fix uploading signed reports by sending the correct checksum
    • Use the correct protocol attribute name when exporting to JSON

    This release adds support for the following hardware:

    • Additional Startech devices
    • Additional Elan fingerprint readers

    This release adds the following features:

    • Add startup profiling which allowed us to speed up daemon startup considerably
    • Add support for OptionROM, CPD and FPT firmware formats for future hardware
    • Add the HostVendor to the D-Bus interface
    • Break some internal ABI and add a conversion helper for out-of-tree plugins
    • Optionally build the quirk files into the daemon binary to reduce installed size

    This release fixes the following bugs:

    • Allow front-end clients to read the percentage property
    • Allow more quirk entries to add multiple items
    • Allow to force install Genesys firmware even if the public-key does not match
    • Allow UFS disks to define the signed status in metadata
    • Autoconnect the Redfish network device when rebooting the BMC
    • Copy the instance ID strings when incorporating devices
    • Do not generate a capsule header for the FMP GUID
    • Ensure more firmware formats can round-trip to and from XML
    • Fix a regression for devices using the Atmel FLIP Bootloader
    • Fix running fwupdtool security with a user-specified plugin allowlist
    • Handle ENOTTY with the correct error code for ioctl calls
    • Increase the self tests coverage substantially
    • Modernize the AMT plugin and split out common MEI functionality
    • Only move the logitech-bulkcontroller progressbar forwards when writing
    • Set the device ID on the FwupdRequest to allow better UX
    • Show the get-details output when the device requirements fail
    • Simply quirk matching for i2c devices to speed up daemon startup
    • Support SHA256 fastboot hashes if specified
    • Use force-detach to bypass the DFU streaming check for camera devices
    • Use the SCSI target to correctly set the physical ID
    • Wait for the System76 launch device to re-enumerate if already unlocked

    This release adds support for the following hardware:

    • Corsair HARPOON RGB Wireless mouse
    • U-Boot devices writing simple FIT images
    • Genesys M27fd AIM101
    • More PixArt wireless devices
    • More Steelseries HID, Sonic and Fizz devices
    • System76 launch_2

    This release adds the following features:

    • Add archive writing support for devices with composite firmware
    • Add a way to read device composite firmware in fwupdtool
    • Allow clients to opt-in to showing updates with user-solvable problems
    • Allow the device to pause polling when writing firmware
    • Export the system and device battery levels on the D-Bus interface
    • Log errors and warnings to the win32 eventlog when required
    • Add X-UsbReceiver as an update category with icon usb-receiver

    This release fixes the following bugs:

    • Accurately return the last-set status to client tools
    • Allow dumping flashrom firmware using fwupdtool
    • Allow specifying a non-file D-Bus transport
    • Allow to request post actions from fwupdtool
    • Always be arch-explicit when installing OS deps
    • Be more resilient when restarting the Redfish BMC
    • Do not mark all Redfish updates as UPDATABLE
    • Do not use 'dongle' to describe USB receiver hardware
    • Download in-process when using fwupdtool
    • Fix a critical warning on failed modem update
    • Fix regression when probing PS175 devices
    • Hardcode the Redfish filedata name to firmware.bin
    • Set the Bluetooth version if REV has been set
    • Switch the Windows installer from NSIS to MSI
    • Use StartServiceCtrlDispatcherA for the daemon on Windows
    • Use the native certificate store on Windows

    This release adds support for the following hardware:

    • Corsair KATAR PRO XT, SABRE PRO and KATAR PRO Wireless
    • HP Thunderbolt Dock G4
    • Lenovo ThinkPad Universal USB-C Dock
    • More PixArt wireless devices
    • More SunplusIT USB cameras
    • Some UFS devices
    • Steelseries Aerox 3 Wireless and Rival 3 Wireless

    This release adds the following features:

    • Add a new attribute for CPUs supported by HSI
    • Add coSWID and uSWID parsers to libfwupdplugin for initial SBOM support
    • Add new HSI attributes for the AMD PSP and various other system protections
    • Add the runtime fwupd-efi version as a firmware requirement
    • Allow 'fwupdmgr install' to install a specified firmware version
    • Allow overriding the detected machine type for debugging and development
    • Restart the BMC after installing BCM updates
    • Show the device serial number and instance IDs by default
    • Support dumping the MTD image to a firmware blob
    • Take a device inhibit when updating a device
    • Use the CFI manufacturer ID to set the vendor
    • Use the correct icon automatically for more hardware

    This release fixes the following bugs:

    • Add signed-payload metadata for more devices
    • Allow Capsule-on-Disk to work in more cases
    • Allow quirking the detected flashrom flash size
    • Check for os-release on FWUPD_SYSCONFDIR
    • Check the alignment when parsing raw firmware
    • Check the update protocol exists when checking requirements
    • Convert the build system to use meson tristate features
    • Correctly probe USB-2 hubs with more than 7 ports
    • Do not add the Windows compatibility ID to capsule devices
    • Do not allow the DBX update for specific motherboards
    • Do not expect KernelCmdline on Windows
    • Do not export USB4 host controllers as updatable if they don't have unique GUIDs
    • Do not fallback to audio-card and use a more suitable icon for USB hubs
    • Do not hardcode the libexecdir to /usr/libexec
    • Do not leak child processes when canceling
    • Do not show unconnected or unreachable devices in the client tools
    • Do not throw away the TPM eventlog when uploading to the LVFS
    • Do not use /var/run for the socket
    • Export the version_lowest_raw value correctly
    • Fix build for MacOS and add to the CI matrix
    • Fix eventlog replay for Intel TXT machines
    • Fix several small memory leaks
    • Fix writing large mtd images than 10kb
    • Ignore MTD devices that report EPERM on open
    • Mark the ME region device locked if it is read only
    • Never send the DeviceChanged signal with old data
    • Only show the CLI time remaining for predictable status phases
    • Respect the NO_COLOR env variable
    • Return the correct error when there is no GPIO device to open
    • Support the new UPower PENDING device states

    This release adds support for the following hardware:

    • CH341A SPI programmer
    • Corsair Sabre RGB PRO and Slipstream USB receiver
    • Genesys GL3521 and GL3590 hubs
    • Google Servo Dock
    • Logitech M550, M650 and K650
    • More ELAN fingerprint readers
    • More integrated Wacom panels
    • More NovaCustom machines
    • More StaLabs StarLite machines
    • More Tuxedo laptops
    • Quectel EM05
    • FlatFrog devices
    • System76 launch_lite_1

    This release adds the following features:

    • Add a flag for UEFI devices that never want a capsule header auto-added
    • Add a flag to indicate the device has a signed or unsigned payload
    • Add a plugin to set a GPIO pin for the duration of an update
    • Add a simple plugin to enumerate (but not update) SCSI hardware
    • Add two more instance IDs to the MTD devices
    • Add X-BaseboardManagementController as an update category
    • Allow assigning issues to devices for known high priority problems
    • Parse the MTD firmware version using the defined GType

    This release fixes the following bugs:

    • Check the IFD sections have non-zero data length to fix a critical warning
    • Modify the AT retry behavior to fix getting the firmware branch
    • Do not run fwupd-refresh automatically in containers
    • Do not show a warning if the TPM eventlog does not exist
    • Do not show TSS2 warning messages by default
    • Fix a critical warning when loading an empty TPM eventlog item
    • Fix a logic error when adding the community warning in fwupdmgr
    • Fix loading flashrom devices in coreboot mode
    • Fix the error handling when updating USB4 retimers
    • Show the user when devices are not updatable due to inhibits
    • Skip probing the Dell DA300 device to avoid a warning
    • Try harder to convert to a version into a correct semver
    • Use multiple checksums when there are no provided artifacts

    This release adds support for the following hardware:

    • HP M2xfd monitors
    • Star Lite Mk III

    This release adds the following features:

    • Add a flag to indicate the firmware is not provided by the vendor
    • Add support for showing dependency versions in JSON format
    • Allow fwupd to operate in socket mode without a D-Bus daemon
    • Allow marking a device as End-of-Life by the OEM vendor
    • Allow specifying the machine Best Known Configuration locally
    • Fall back to the ARM Device Tree 'compatible' data when required

    This release fixes the following bugs:

    • Be more robust by retrying IPMI transactions on servers
    • Change the expired Redfish password when required
    • Fix a ModemManager segfault on startup for some MBIM-QDU devices
    • Fix a possible dell-dock segfault at startup
    • Fix compiling with new versions of efivar
    • Fix the Nordic bootloader type detection
    • Fix USB4 retimer enumeration
    • Get the SMBIOS table and host machine ID when running on Windows
    • Show results when calling get-details if failing requirements
    • Uninhibit the modem using ModemManager after upgrade

    This release adds support for the following hardware:

    • Future Analogix devices
    • NovaCustom NV4x

    This release adds the following features:

    • Add firmware branch support for ModemManager devices
    • Allow firmware engineers to patch files at known offsets
    • Show why more devices are not marked as updatable

    This release fixes the following bugs:

    • Allow fwupdtool to be run as the non-root user in more cases
    • Assign the Logitech bulkcontroller update interface correctly
    • Do not allow UEFI updates when the laptop lid is closed
    • Do not autoload ipmi-si to avoid warning on non-server hardware
    • Do not show a critical warning for a weird TPM event log
    • Fix waiting for USB devices when using Windows
    • Ignore non-PCI NVMe devices

    This release adds support for the following hardware:

    • HP USB-C G2 Dock
    • Many UF2 devices, experimentally
    • More PixArt devices
    • Nordic HID devices using MCUBoot
    • Quectel EG25-G LTE Modem
    • ThinkPad Thunderbolt 4 Dock

    This release adds the following features:

    • Add a sync-bkc subcommand to ensure a known set of firmware versions
    • Add FuArchiveFirmware for plugins that use archives as firmware files
    • Add quirkable page and sector size properties to FuCfiDevice
    • Make Upower and powerd support optional

    This release fixes the following bugs:

    • Add some sanity checks to the elanfp firmware parser
    • Add the CFI JEDEC instance ID if using the vendor-extended version
    • Check the value range when parsing the quirk keys
    • Do not wait for a USB runtime if will-disappear is set
    • Enable the MOTD integration when using pam_motd
    • Fix DFU regression when merging the FuProgress work
    • Fix running the tests when fwupd is not installed
    • Fix the GLib error message when inotify max_user_instances is too low
    • Fix VLI VL820Q7 detection to fix flashing of the Lenovo TBT3 dock
    • Ignore a USB error for STM32 attach when the device goes away
    • Make the HSI tests optional for embedded targets
    • Make the plugin startup order deterministic
    • Set Thunderbolt ports offline on host controller
    • Use endian-safe version functions when enumerating Logitech hardware
    • Use lowercase flag names in intel-spi to prevent a runtime warning
    • Wait for the System76 Launch device to come back from DFU mode

    This release adds support for the following hardware:

    • Most Nordic Semiconductor nRF Secure devices

    This release adds the following features:

    • Add a new HSI check that PCR registers 0-7 are not empty
    • Add several compile flags to reduce the install size by over 300Kb
    • Allow overriding HwId data from the daemon.conf config file
    • Allow overriding the firmware GType from a quirk file
    • Export the component release ID over DBus
    • Remove support for the SoloKey and ChaosKey devices
    • Show a daemon warning if quirk flags are malformed
    • Speed up the daemon startup by ~40% by doing less at startup

    This release fixes the following bugs:

    • Be case insensitive when fixing the device model
    • Fix a critical warning in ccgx found by the fuzzer
    • Fix a DFU crash if the attach failed due to a hardware fault
    • Fix a Redfish crash when specifying a URL without a port
    • Fix CLI downloads when using fwupdmgr --ipfs
    • Fix critical warning when /etc/machine-id does not exist
    • Inhibit thunderbolt devices to correctly use UPDATABLE_HIDDEN
    • Set SSL_VERIFYHOST=0 when using Redfish to fix OpenBMC auth
    • Skip UEFI devices that fail coldplug

    This release adds support for the following hardware:

    • All exported MTD block devices

    This release adds the following features:

    • Allow specifying 'fwupdmgr device-test foo --json' for unattended testing
    • Allow using a filename when using set-approved-firmware
    • Inhibit ModemManager device in mbim-qdu
    • Share the Common Flash Memory Interface quirks between plugins
    • Show changes in HSI attributes when using 'fwupdmgr security'
    • Show the user a warning if updating may affect full-disk-encryption
    • Show translated firmware release notes when provided
    • Support loading remotes from /var/lib/fwupd/remotes.d

    This release fixes the following bugs:

    • Fix a CCGX regression when loading firmware
    • Fix a potential crash when dumping Parade devices
    • Fix build error when sys/io.h is not available
    • Fix building the Synaptics RMI self tests on s390x
    • Fix the CSME CVE detection for new generations
    • Handle EPERM when running the self tests on systems with IPMI
    • Mark as SUPPORTED even if on battery power
    • Only save the HSI attributes to the database if different
    • Raise the client timeout value from 25 seconds to fix Redfish startup
    • Redirect the old HSI links to the correct place
    • Relax the ITE SuperIO signature checks for new hardware support
    • Set device time and timezone for logitech bulkcontroller devices
    • Set the verfmt of the returned device when the daemon device is unset

    This release adds support for the following hardware:

    • Dell Atomic Dock
    • HP Thunderbolt Dock G4
    • More PixArt devices
    • Steelseries Stratus
    • Wacom 3rd-gen Intuos BT

    This release adds the following features:

    • Add FuCfuPayload and FuCfuOffer for future usage
    • Add support for an 'unreachable' device flag
    • Add support for Logitech devices supporting the Unified Battery feature
    • Allow adding GUIDs to each HSI security attribute
    • Allow installing the LVFS remote, but with it disabled by default
    • Convert security attributes to JSON and write then to the database
    • Convert the device test script to a fwupdmgr subcommand
    • Create Redfish user accounts automatically using IPMI
    • Use an interactive request to restart some Logitech DFU devices

    This release fixes the following bugs:

    • Abort on invalid SREC files early to avoid a fuzzing timeout
    • Allow using interrupt transfers for HID devices
    • Allow waiting for multiple devices to replug
    • Fix a critical warning on a Unifying flash failure
    • Fix a regression in flashing the Dell dock
    • Fix Thunderbolt host controller probing
    • Forcefully set checksums found in cabinet files to lowercase
    • Force UX-capsule over full size BGRT
    • Make the SuperIO ports and timeouts specific to the DMI model
    • Only probe SynapticsMST devices that have opted-in
    • Remove support for --ignore-power as it did not work for UEFI firmware
    • Reset the CMOS as required when changing system firmware branch
    • Restart the daemon if any of the plugin config files are modified
    • Show HSiLevel=0 attributes in JSON security output
    • Update the child composite ID if the parent changes
    • Use a per-device global percentage completion
    • Write the BMP image upside down to avoid using a negative bitmap height

    This release adds support for the following hardware:

    • A huge number of Synaptics CAPE devices
    • Elan fingerprint readers
    • Logitech Bolt peripherals, receivers and radio hardware
    • Logitech devices supporting the bulk controller protocol
    • More supported PixArt devices
    • More supported StarBook coreboot devices
    • Union Point SPI hardware

    This release adds the following features:

    • Add a plugin to check Lenovo firmware settings
    • Add initial support for the powerd daemon
    • Add support for CapsuleOnDisk
    • Add support for installing UEFI updates from GRUB
    • Add support for soft-requirements that can be ignored with --force
    • Allow devices to only accept version upgrades
    • Allow discovery of Redfish BMCs specified by VID-PID or MAC
    • Allow the daemon to request interactive action from the end user
    • Automatically connect the BMC network interface at startup
    • Show the build timestamp if set on the device
    • Show the user how to switch out of Wacom tablet Android-mode

    This release fixes the following bugs:

    • Add the alternate vendor name into the 8BitDo allowlist
    • Allow multiple devices to set WAIT_FOR_REPLUG
    • Allow the client to watch for more property changes
    • Always ensure the SuperIO version string is NUL terminated
    • Automatically clear the update error as required
    • Disable all UX capsules for Lenovo hardware
    • Do not assume the metainfo file is NUL-terminated
    • Do not save invalid files on LVFS server error
    • Fix a VLI regression in enumerating the PD device
    • Fix a VLI regression when installing VL820Q7 firmware
    • Fix enumeration of the Synaptics Prometheus config child
    • Fix parsing Redfish USB/PCI network VID/PIDs
    • Fix the fwupdmgr progressbar spinner to actually work
    • Fix version number for legacy Wacom Bluetooth modules
    • Ignore virtual M.2 ATA devices
    • Preserve NEEDS_REBOOT on successful update
    • Prevent a corrupt PHAT table from allocating lots of memory
    • Read the Redfish SMBIOS table when required
    • Remove the vendor string from the device name where required
    • Save the update state to the database correctly all of the time
    • Switch from sysctl to ioctl for ESRT on FreeBSD
    • Try reading from /sys/class/dmi if SMBIOS direct access fails
    • Watch for children added or removed after setup has been completed
    • Work around a XCC-ism on Lenovo hardware

    This release adds support for the following hardware:

    • ModemManager devices supporting Firehose or MBIM QDU
    • More models of RTS54HUB
    • More Poly DFU devices
    • Parade LSPCON
    • PixArt receiver and wireless hardware
    • Realtek MST with RTD2142
    • SuperIO IT5570
    • USB4 Dell dock

    This release adds the following features:

    • Add FreeBSD UEFI Capsule support
    • Add generic ModemManager support for PCI based modems
    • Add initial support for USB4 module in the Dell dock
    • Add support for sibling requirements
    • Add support for the ACPI PHAT table
    • Allow building the documentation with gi-docgen and gtk-doc
    • Support binary artifact resources in cabinet archives
    • Use GProxyResolver to get the system proxy setting for a given URL

    This release fixes the following bugs:

    • Ask the user to confirm all CLI actions
    • Check the versions of libfwupd and libfwupdplugin at startup
    • Do not prevent firmware updates on desktop hardware
    • Do not show an invalid DFU warning on attach
    • Fail parsing if wacom firmware sections are not in sorted order
    • Fall back to binary files when flashing STM32 hardware
    • Fix a critical warning when downloading files
    • Fix a possible critical warning due to a bug in type casting
    • Fix a regression in updating the WD19TB dock
    • Fix GUID generation on pixart hardware
    • Fix the VLI i2c device enumeration, e.g. MSP430
    • Follow HTTP 3XX redirects when downloading files
    • Force the device locker to close() an aborted open()
    • Handle bsdisks' UDisks2 implementation on FreeBSD
    • Only lock fwupdtool when loading the engine
    • Read current Wacom firmware index before finding image to write
    • Support all hash types when loading cabinet archives
    • Support mirroring the detach and update images
    • Switch lock directory from /var/run to /run/lock

    This release adds support for the following hardware:

    • Minibons devices
    • More 8BitDo hardware
    • More Synaptics Prometheus hardware
    • RTD21xx devices in background mode
    • Some Kingston SSD and NVMe hardware

    This is the first release of the 1.6.x series, and since 1.5.x some internal plugin API has been changed and removed. Although we've tested this release on all the hardware we have regression tests for, bugs may have crept in; please report failures to the issue tracker as required.

    There are several new plugins adding support for new hardware and a lot of code has been migrated to the new plugin API. The public libfwupd API also has some trivial additions, although no action is required.

    This release adds the following features:

    • Add a composite ID that is used to identify dock device components
    • Add an Intel Flash Descriptor parser
    • Add API to allow the device to report its own battery level
    • Add API to recount why the device is non-updatable
    • Add lspcon-i2c-spi programmer support
    • Add more hardware support to the pixart-rf plugin
    • Add some more new category types for firmware to use
    • Add support for downloading the SPI image from the Intel eSPI device
    • Add support for some Analogix hardware
    • Add support for writing SREC firmware
    • Add the firmware-sign command to fwupdtool to allow resigning archives
    • Split UEFI EFI binary into a subproject
    • Use an OFD or Unix lock to prevent more than one fwupdtool process

    This release fixes the following bugs:

    • Actually write the bcm57xx stage1 version into the file
    • Add option to disable the UEFI capsule splash screen generation
    • Avoid use-after-free when specifying the VID/PID in dfu-tool
    • Cancel the GDBusObjectManager operation to fix a potential crash
    • Check PixArt firmware compatibility with hardware before flashing
    • Do not check for native dependencies as target dependencies
    • Do not use help2man to build manual pages
    • Fix a crash when shutting down the daemon
    • Fix build on musl
    • Fix build when using BSD
    • Fix /etc/os-release ID_LIKE field parsing
    • Force the synaptics-rmi hardware into IEP mode as required
    • Never allow D-Bus replacement when a firmware update is in operation
    • Offer the user to refresh the remote after enabling
    • Remove unused, unsafe and deprecated functions from libfwupdplugin
    • Simplify asking the user about reviews
    • Write BMP data directly without using PIL
    • Write synaptics-rmi files with valid checksum data

    This release adds the following features:

    • Add initial support for Bluez bluetooth devices
    • Add more supported pixart devices
    • Add support for the RTD21xx HDMI converter

    This release fixes the following bugs:

    • Convert MBR types to GPT GUIDs to help find the ESP
    • Do not allow updating a synaptics-mst device with no customer ID
    • Drop unused heap pages after startup has completed
    • Ensure SBAT metadata is added correctly
    • Move the plugin build logic to the plugins themselves
    • Only allow verify-update for plugins that support CAN_VERIFY

    This release adds the following features:

    • Add SBAT metadata to the fwupd EFI binary
    • Add support for GD32VF103 as found in the Longan Nano
    • Add support for RMI PS2 devices
    • Add support for the System76 Keyboard
    • Allow downloading firmware from IPFS
    • Install the UX data into a single .tar.xz file

    This release fixes the following bugs:

    • Add support for the Starlabs LabTop L4
    • Allow using an external ESP again
    • Ask the user to reboot when required if downgrading
    • Be more paranoid when parsing ASCII buffers and devices
    • Check if the fwupd BootXXXX entry exists on failure
    • Clear the pending flag if restarting the system
    • Do not allow flashing using flashrom if BLE is enabled
    • Do not allow Lenovo hardware to install multiple capsules
    • Do not parse the OptionROM image
    • Do not show Unknown [***] for every client connection
    • Fix dnload wBlockNum wraparound for ST devices
    • Fix OOM when using large ArchiveSizeMax values
    • Fix several crashes spotted by AddressSanitizer
    • Fix several places where the Goodix MOC plugin could crash
    • Include the PCR0 to the report metadata
    • Report the lockdown status from UEFI and SuperIO plugins
    • Show a console warning if the system clock is not set

    This release adds the following features:

    • Add a plugin to update PixArt RF devices
    • Add new hardware to use the elantp and rts54hid plugins
    • Allow specifying more than one VendorID for a device
    • Detect the AMD TSME encryption state for HSI-4
    • Detect the AMI PK test key is not installed for HSI-1

    This release fixes the following bugs:

    • Fix flashing a fingerprint reader that is in use
    • Fix several critical warnings when parsing invalid firmware
    • Fix updating DFU devices that use DNLOAD_BUSY
    • Ignore the legacy UEFI OVMF dummy GUID
    • Make libfwupd more thread safe to fix a crash in gnome-software
    • Never show unprintable chars from invalid firmware in the logs

    This release adds the following features:

    • Add Maple Ridge Thunderbolt firmware parsing support
    • Add --no-remote-check to ignore checking for download remotes
    • Allow creating FMAP and Synaptics firmware using builder.xml
    • Build a test harness that uses honggfuzz to fuzz firmware

    This release fixes the following bugs:

    • Allow using fwupdtool as non-root for firmware commands
    • Do not trust the Block.HintSystem boolean for ESP filtering
    • Fix a memory leak when parsing Synaptics firmware
    • Fix a possible crash when reading the Goodix MOC USB request
    • Fix crashes when parsing invalid FMAP, DMC, Solokey and Synaptics images

    This release adds the following features:

    • Allow setting the GMainContext when used for sync methods
    • Export the driver name from FuUdevDevice

    This release fixes the following bugs:

    • Add a UEFI quirk for Star Labs Lite Mk III
    • Add the device firmware ID for serio class hardware
    • Allow the client to send legacy PKCS7 and GPG signatures
    • Do not use accidentally depend on new meson versions
    • Fix a possible critical warning due to missing retval
    • Fix the endianness for the CRC check in bcm57xx
    • Lower the CURL version required to fix RHEL
    • Make sure the correct interface number is used for QMI
    • Mark more user-visible strings as translatable
    • Restrict loading component types of firmware
    • Validate ModemManager firmware update method combinations

    This release adds the following features:

    • Add a flag to indicate if packages are supported
    • Add a plugin for the Pinebook Pro laptop
    • Allow components to set the icon from the metadata
    • Switch from libsoup to libcurl for downloading data

    This release fixes the following bugs:

    • Fall back to FAT32 internal partitions for detecting ESP
    • Fix detection of ColorHug version on older firmware versions
    • Fix reading BCM57XX vendor and device ids from firmware
    • Fix replugging the MSP430 device
    • Fix sync method when called from threads without a context
    • Ignore an invalid vendor-id when adding releases for display
    • Improve synaptics-mst reliability when writing data
    • Install modules-load configs in the correct directory
    • Notify the service manager when idle-quitting
    • Only download the remote metadata as required
    • Remove HSI update and attestation suffixes
    • Restore recognizing GPG and PKCS7 signature types in libfwupd
    • Set the SMBIOS chassis type to portable if a DT battery exists

    This release adds the following features:

    • Include the amount of NVRAM size in use in the LVFS failure report

    This release fixes the following bugs:

    • Delete unused EFI variables when deploying firmware
    • Fix probe warning for the Logitech Unifying device
    • Make bcm57xx hotplug more reliable
    • Recognize authorized thunderbolt value of 2
    • Remove the duplicate parent-child data in FwupdDevice and FuDevice
    • Show a less scary fwupdate output for devices without info
    • Show a link to discover more information about a specific plugin failure
    • Use a different Device ID for the OptionROM devices
    • Use UDisks to find out if swap devices are encrypted

    This release adds the following features:

    • Add a compatible re-implementation of the rhboot dbxtool
    • Add async versions of the library for GUI tools
    • Add commands for interacting with the ESP to fwupdtool
    • Add firmware-extract subcommand to fwupdtool
    • Add FwupdPlugin so we can convey enumerated system errors to the end user
    • Add plugin for Goodix fingerprint sensors
    • Add plugin that can update the BCM5719 network adapter
    • Add plugin to update Elan Touchpads using HID
    • Add support for a delayed activation flow for Thunderbolt
    • Add support for ChromeOS Quiche and Gingerbread
    • Add support for Hyper hardware
    • Add support for the Host Security ID
    • Add support for ThunderBolt retimers
    • Add switch-branch command to fwupdtool and fwupdmgr
    • Allow blocking specific firmware releases by checksum
    • Allow constructing a firmware with multiple images
    • Allow firmware to require specific features from front-end clients
    • Allow updating the dbx using the LVFS, validating it is safe to apply
    • Include the HSI results and attributes in the uploaded report
    • Support loading DMI data from DT systems
    • Support LVFS::UpdateImage for GUI clients

    This release fixes the following bugs:

    • Allow compiling the daemon without polkit support
    • Always look at all TPM eventlog supported algorithms
    • Change all instances of master/slave to initiator/target
    • Correctly order devices when using logical parents
    • Do not dedupe NVMe or VLI PD devices
    • Do not expose the VLI shared-SPI devices on the USB2 recovery device
    • Do not fix up the version on post-update mismatch
    • Download the metadata first when using 'fwupdtool refresh'
    • Drop efivar dependency
    • Drop support for ThunderBolt force power due to hardware issues
    • Fix setting BootNext correctly when multiple updates are scheduled
    • Fix the topology of the audio device on the Lenovo TR dock
    • Make return code different for get-updates with no updates
    • Make specific authorizations also imply others
    • Make TPM support more optional
    • Parse the HEX version before comparing for equality
    • Prevent dell-dock updates to occur via synaptics-mst plugin
    • Record the UEFI failure in more cases
    • Retry the HID SetReport to fix flashing the TB3 dock
    • Show an error when a plugin is missing dependencies
    • Use libxmlb bound parameters to speed up the device verification
    • Use pkttyagent to request user passwords if running without GUI
    • Use the JCat file to select the metadata file

    This release adds the following features:

    • Allow adding a device 'proxy' device that can do actions on it
    • Allow specifying the device on the command line by GUID

    This release fixes the following bugs:

    • Add a device quirk that forces an explicit device-id match
    • Allow a device to set the logical or physical ID during ->setup()
    • Correctly format firmware version of Dynabook X30 and X40
    • Do not show safe mode errors for USB4 host controllers
    • Do not show the USB 2 VLI recovery devices for USB 3 hubs
    • Fix the correct DeviceID set by GetDetails
    • Make the EP963X plugin actually work on real hardware
    • Make the tss2-esys dep conditional for RHEL 8
    • Only update the FW2 partition of the ThinkPad USB-C Dock Gen2
    • Prefer to update the child device first if the order is unspecified
    • Refresh device name and format before setting supported flag
    • Reset the progressbar time estimate if the percentage is invalid
    • Set the CCGX device name and summary from quirk files
    • Wait for the cxaudio device to reboot after writing firmware

    This release adds the following features:

    • Add 'firmware-convert' subcommand to fwupdtool
    • Add fu_device_retry() API
    • Add FuHidDevice abstraction
    • Add plugin for CPU microcode
    • Add plugin for Cypress CCGX hardware
    • Add plugin for EP963x hardware
    • Add 'reinstall' command to fu-tool
    • Allow server metadata to set the device name and version format
    • Export the device state as part of the D-Bus interface
    • Export the release creation time and urgency
    • Introduce a new VersionFormat of 'hex'
    • Use Jcat files in firmware archives and for metadata

    This release fixes the following bugs:

    • Actually reload the DFU device after upgrade has completed
    • Add a lot of missing metadata about wacom-usb devices
    • Add a way to set the device timeout from a quirk
    • Add STM32F745 DfuSe version quirk
    • Allow waiting for the parent device when replugging
    • Always check for 'PLAIN' when doing vercmp() operations
    • Apply version format to releases and devices at same time
    • Check the firmware requirements before adding 'SUPPORTED'
    • Correctly attach VL103 after a firmware update
    • Do not allow devices that have no vendor ID to be 'UPDATABLE'
    • Do not conditionalize attach() and detach() on 'IS_BOOTLOADER'
    • Do not use shim for non-secure boot configurations
    • Fix a crash when removing device parents
    • Fix a difficult-to-trigger daemon hang when replugging devices
    • Fix a runtime error when detaching MSP430
    • Fix CounterpartGuid when there is more than one supported device
    • Fix reporting Synaptics cxaudio version number
    • Load the signature to get the aliased CDN-safe version of the metadata
    • Never add USB hub devices that are not upgradable
    • Only auto-add counterpart GUIDs when required
    • Parse the CSR firmware as a DFU file
    • Set the protocol when updating logitech HID++ devices
    • When TPM PCR0 measurements fail, query if secure boot is available and enabled

    This release adds the following features:

    • Added completion script for fish shell
    • Inihbit all power management actions using logind when updating

    This release fixes the following bugs:

    • Always check for PLAIN when doing vercmp() operations
    • Always return AppStream markup for remote agreements
    • Apply UEFI capsule update even with single valid capsule
    • Check the device protocol before de-duping devices
    • Copy the version and format from donor device in get-details
    • Correctly append the release to devices in `fwupdtool get-details`
    • Decrease minimum battery requirement to 10%
    • Discard the reason upgrades aren't available
    • Do not fail loading in /etc/machine-id is not available
    • Fix a critical warning when installing some firmware
    • For the `get-details` command make sure to always show devices
    • Set the MSP430 version format to pair
    • Switch off the ATA verbose logging by default
    • Use unknown for version format by default on get-details

    This release adds the following features:

    • Add an extra instance ID to disambiguate USB hubs
    • Add a plugin to update PD controllers by Fresco Logic
    • Replay the TPM event log to get the PCRx values

    This release fixes the following bugs:

    • Fix updating Synaptics MST devics with no PCI parent
    • Correctly reset VL100 PD devices
    • Do not rewrite BootOrder in the EFI helper
    • Do not use vercmp when the device version format is plain
    • Fix firmware regression in the EFI capsule helper
    • Ignore Unifying detach failures
    • Make the cxaudio version match that of the existing Windows tools
    • Set up more parent devices for various Lenovo USB hubs
    • Support the new gnuefi file locations
    • Use the correct command to get the VLI device firmware version

    This release adds the following features:

    • Add 'get-remotes' and 'refresh' to fwupdtool
    • Add support for standalone VIA PD devices
    • Allow applying all releases to get to a target version
    • Discourage command line metadata refreshes more than once per day
    • Generate a win32 setup binary
    • Get the list of updates in JSON format from fwupdagent
    • Move MOTD population into the daemon
    • Shut down automatically when there is system memory pressure

    This release fixes the following bugs:

    • Correctly delete UEFI variables
    • Correctly import PKCS-7 remote metadata
    • Disable the battery percentage checks if UPower is unavailable
    • Do not always get the vendor ID for udev devices using the parent
    • Fix display of UTF-8 characters on Windows
    • Show the device parent if there is an interesting child
    • Use a different protocol ID for VIA i2c devices
    • Use the correct timeout for Logitech IO channel writes

    This release adds the following features:

    • Add a new plugin that can parse the TPM event log
    • Add a new plugin that exposes the TPM device firmware version
    • Allow building on Windows with MinGW
    • Enforce that device protocol matches the metadata value
    • Export the device protocol and raw device version to the client --verbose output

    This release fixes the following bugs:

    • Add a dell-bios version format to match what is shown on the vendor website
    • Allow incremental version major and minor number for Synaptics Prometheus devices
    • Clarify error messages when no upgrades are available
    • Correct the default prompt for reboot/shutdown
    • Do not expose bootloader version errors to users
    • Fix the quirk for the legacy VIA 813 usbhub chip
    • Hardcode the vendor ID for Dell dock hardware
    • Only check the vendor ID if the device has one set
    • Return exit status success if there is no firmware to be updated
    • Set the correct vendor eMMC ID prefix
    • Use the baseboard vendor as the superio vendor ID
    • Use the BIOS vendor as the coreboot and flashrom vendor ID

    This release adds the following features:

    • Convert libfwupdprivate to a shared library libfwupdplugin
    • Create a REV_00 instance ID as this may be what the vendor needs to target

    This release fixes the following bugs:

    • Improve coreboot version detection
    • Invert default behavior to be safer for reboot and shutdown prompts
    • Reload the Synaptics prometheus device version after update
    • Use the correct unlocker when using GRWLock
    • Whitelist VIA USB hub PD and I²C devices

    This release adds the following features:

    • Add a new property Interactive to the daemon
    • Add a new script for installing a Dell BIOS from an EXE file
    • Add support for Foxconn T77W968 and DW5821e eSIM
    • Add support for matching firmware requirements on device parents
    • Add support for writing VIA PD and I2C devices
    • Add versions formats for the Microsoft Surface devices

    This release fixes the following bugs:

    • Allows confined snaps to activate fwupd via D-Bus
    • Correct Wacom panel HWID support
    • Don't assume all udev devices have device_file
    • Dynamically determine release version
    • Fall back to `ID_LIKE` when the path for `ID` doesn't exist
    • Fix a fastboot regression when updating modem firmware
    • Fix regression when coldplugging superio devices
    • Fix the linking of the UEFI update binary
    • Fix the vendor id of hidraw devices
    • Make loading USB device strings non-fatal
    • Reject invalid Synaptics MST chip IDs
    • Skip cleanup after device is done updating if required

    This release adds the following features:

    • Add a plugin for systems running coreboot
    • Add a plugin to update eMMC devices
    • Add a plugin to update Synaptics RMI4 devices
    • Add a plugin to update VIA USB hub hardware
    • Add some success messages when CLI tasks have completed
    • Add support for automatically uploading reports
    • Add support for `fwupdmgr reinstall`
    • Allow fwupdtool to dump details of common firmware formats
    • Use XMLb to query quirks to reduce the RSS when running

    This release fixes the following bugs:

    • Add several quirks for Realtek webcams
    • Add support for the 8bitdo SN30Pro+
    • Add support for the ThinkPad USB-C Dock Gen2 audio device
    • Always report the update-error correctly for multiple updates
    • Create a unique GUID for the Thunderbolt controller path
    • Fix a regression for Wacom EMR devices
    • Move the Jabra-specific detach out into its own plugin
    • Recognize new 'generation' Thunderbolt sysfs attribute for USB4
    • Reduce more boilerplate in plugins, modernizing where required
    • Remove unused DFU functionality
    • Rework ESP path detection and lifecycle to auto-unmount when required
    • Show a useful error for Logitech devices that cannot self-reset
    • Use correct method for stopping systemd units
    • Use device safety flags to show prompts before installing updates
    • Use `genpeimg` to mark ASLR and DP/NX on EFI binary
    • Use will-disappear flag for 8bitdo SF30/SN30 controllers

    This release adds the following features:

    • Add a plugin to detach the Thelio IO board
    • Add a plugin to update Conexant audio devices
    • Support issues in AppStream metadata

    This release fixes the following bugs:

    • Align the key values to the text width not the number of bytes
    • Display more helpful historical device information
    • Do not ask the user to upload a report if ReportURI is not set
    • Do not crash when starting tpm2-abrmd
    • Ensure HID++ v2.0 peripheral devices get added
    • Fall back to /var/lib/dbus/machine-id when required
    • Include all GUIDs when uploading a report
    • Move D-Bus conf file to datadir/dbus-1/system.d
    • Update device_modified in sql database during updates

    This release adds the following features:

    • Add support for the Minnowboard Turbot
    • Add support for the SoloKey Secure
    • Add support for thunderbolt kernel safety checks
    • Add support to integrate into the motd
    • Allow filtering devices when using the command line tools
    • Allow setting custom flags when using fwupdate
    • Allow specifying a firmware GUID to check any version exists
    • Include the kernel release as a runtime version
    • Print devices, remotes, releases using a tree
    • Publish docs to fwupd.github.io using CircleCI

    This release fixes the following bugs:

    • Add aliases for get-upgrades and upgrade
    • Allow disabling SSL strict mode for broken corporate proxies
    • Be more accepting when trying to recover a failed database migration
    • Do not segfault when trying to quit the downgrade selection
    • Fix a possible crash when stopping the fwupd service
    • Fix incomplete hex file parsing in unifying plugin
    • Fix thunderbolt logic to work properly with ICL thunderbolt controller
    • Never show AppStream markup on the console
    • Never use memcpy() in a possibly unsafe way
    • Only write the new UEFI device path if different than before
    • Partially rewrite the Synapticsmst plugin to support more hardware
    • Reload metadata store when configuration changes
    • Use environment variables for systemd managed directories
    • Use tpm2-tss library to read PCR values

    This release adds the following features:

    • Add a new experimental plugin that supports libflashrom
    • Add a specific error code for the low battery case
    • Add support for 8bitdo USB Retro Receiver
    • Export new API to build objects from GVariant blobs
    • Show a warning when running in UEFI legacy mode
    • Support a UEFI quirk to disable the use of the UX capsule

    This release fixes the following bugs:

    • Fix installing synaptics-prometheus config updates
    • Fix the supported list of Wacom tablets
    • Never set an empty device name
    • Prompt for reboot when unlocking on the command line if applicable
    • Show devices with an UpdateError in get-devices output
    • Support empty proxy server strings
    • Try harder to find duplicate UEFI boot entries

    This release adds the following features:

    • Add support for Synaptics Prometheus fingerprint readers
    • Check if VersionFormat is ambiguous when adding devices
    • Check the daemon version is at least the client version
    • Export the version-format used by devices to clients
    • Set the version format for more device types

    This release fixes the following bugs:

    • Allow using --force to trigger a duplicate offline update
    • Be smarter about existing installed fwupd when using standalone-installer
    • Correctly identify DFU firmware that starts at offset zero
    • Display the remote warning on the console in an easy-to-read way
    • Fix a libasan failure when reading a UEFI variable
    • Never guess the version format from the version string
    • Only use class-based instance IDs for quirk matching
    • Prompt the user to shutdown if required when installing by ID
    • Reset the forced version during DFU attach and detach

    This release adds the following features:

    • Allow the fwupdmgr tool to modify the daemon config

    This release fixes the following bugs:

    • Correctly parse DFU interfaces with extra vendor-specific data
    • Do not report transient or invalid system failures
    • Fix problems with the version format checking for some updates

    This release adds the following features:

    • Add a component categories to express the firmware type
    • Add support for 8BitDo M30
    • Add support for the not-child extension from Logitech
    • Shut down the daemon if the on-disk binary is replaced

    This release fixes the following bugs:

    • Blocklist the synapticsmst plugin when using amdgpu
    • Correct ATA activation functionality to work for all vendors
    • Implement QMI PDC active config selection for modems
    • Make an error message clearer when there are no updates available
    • Match the old or new version number when setting NEEDS_REBOOT
    • More carefully check the output from tpm2_pcrlist
    • Recreate the history database if migration failed
    • Require AC power when updating Thunderbolt devices
    • Require --force to install a release with a different version format
    • Save history from firmware installed with fwupdtool

    This release adds the following features:

    • Add a plugin to support modem hardware
    • Add support for delayed activation of docks and ATA devices
    • Add support for reading the SuperIO device checksum and writing to e-flash
    • Add the fwupdagent binary for use in shell scripts
    • Allow restricting firmware updates for enterprise use
    • Allow signing the fwupd report with a client certificate
    • Use Plymouth when updating offline firmware

    This release fixes the following bugs:

    • Allow forcing an offline-only update on a live system using --force
    • Allow running offline updates when in system-update.target
    • Ask to reboot after scheduling an offline firmware update
    • Correctly check the new version for devices that replug
    • Do not fail to start the daemon if tpm2_pcrlist hangs
    • Do not fail when scheduling more than one update to be run offline
    • Do not let failing to find DBus prevent fwuptool from starting
    • Do not schedule an update on battery power if it requires an external power source
    • Include all device checksums in the LVFS report
    • Rename the shimx64.efi binary for known broken firmware
    • Upload the UPDATE_INFO entry for the UEFI UX capsule

    This release adds the following features:

    • Allow a device to be updated using more than one plugin
    • Report the DeviceInstanceIDs from fwupdmgr when run as root

    This release fixes the following bugs:

    • Add an extra check for Dell NVMe drives to avoid false positives
    • Call composite prepare and cleanup using fwupdtool
    • Correct handling of CAB files with nested directories
    • Detect and special case Dell ATA hardware
    • Do not fail fwupdtool if dbus is unavailable
    • Do not unconditionally enable Werror for the EFI binary
    • Fill holes when reading SREC files
    • Filter the last supported payloads of certain Dell docks
    • Fix flashing failure with latest Intuos Pro tablet
    • Fix potential segfault when applying UEFI updates
    • Fix unifying regression when recovering from failed flash

    This release adds the following features:

    • Add a directory remote that generates metadata
    • Add a new remote type "directory"
    • Add a plugin to update Wacom embedded EMR and AES panels
    • Add a plugin to upgrade firmware on ATA-ATAPI hardware
    • Add a quirk to use the legacy bootmgr description
    • Add flag to support manually aligning the NVMe firmware to the FWUG value
    • Add SuperIO IT89xx device support
    • Add support for Dell dock passive flow
    • Add 'update' and 'get-updates' commands to fwupdtool
    • Allow Dell dock flashing Thunderbolt over I2C
    • Check the battery percentage before flashing
    • Show a per-release source and details URL
    • Show a `UpdateMessage` and display it in tools

    This release fixes the following bugs:

    • Add the needs-shutdown quirk to Phison NVMe drives
    • Correct Nitrokey Storage invalid firmware version read
    • Do not check the BGRT status before uploading a UX capsule
    • Do the UEFI UX checksum calculation in fwupd
    • Fix flashing various Jabra devices
    • Fix the parser to support extended segment addresses
    • Flash the fastboot partition after downloading the file
    • Show a console warning if loading an out-of-tree plugin
    • Support FGUID to get the SKU GUID for NVMe hardware

    This release fixes the following bug:

    • Correctly migrate the history database

    This release adds the following features:

    • Add support for devices that support fastboot
    • Add more standard USB identifier GUIDs
    • Add new API to get the release protocol from the metadata
    • Add the PCR0 value as the device checksum for system firmware
    • Include the device firmware checksum and update protocol in the report

    This release fixes the following bugs:

    • Add Dell TB18DC to the supported devices list
    • Allow replacing the last byte in the image when using 'dfu-tool replace-data'
    • Append the UEFI capsule header in userspace rather than in the loader
    • Check the device checksum as well as the content checksum during verify
    • Correctly parse format the version numbers correctly using old metadata
    • Fix a crash if AMT returns an empty response
    • Fix a regression when doing GetReleases on unsupported hardware
    • Fix the 8bitdo version number if the daemon locale is not C.UTF-8
    • Remove the Wacom DTH generation hardware from the whitelist
    • Sanitize the version if the version format has been specified

    This release adds the following features:

    • Add per-release install duration values
    • Shut down the daemon after 2h of inactivity when possible

    This release fixes the following bugs:

    • Fix a use-after-free when using --immediate-exit
    • Fix flashing the 8bitdo SF30
    • Fix showing the custom remote agreements
    • Include the os-release information in the release metadata
    • Speed up startup by loading less thunderbolt firmware
    • Speed up startup by using a silo index for GUID queries
    • Use less memory and fragment the heap less when starting

    This release adds the following features:

    • Add a plugin for an upcoming Dell USB-C dock
    • Add a standalone installer creation script
    • Add support for devices to show an estimated flash time
    • Add support for some new Realtek USB devices
    • Allow firmware files to depend on versions from other devices
    • Allow setting the version format from a quirk entry
    • Port from libappstream-glib to libxmlb for a large reduction in RSS
    • Stop any running daemon over dbus when using fu-tool
    • Support the Intel ME version format

    This release fixes the following bugs:

    • Add version format quirks for several Lenovo machines
    • Adjust panamera ESM update routine for some reported issues
    • Adjust synapticsmst EVB board handling
    • Check the amount of free space on the ESP
    • Don't show devices pending a reboot in GetUpgrades
    • Ensure that parent ID is created before creating quirked children
    • Optionally wait for replug before updating a device
    • Set the full AMT device version including the BuildNum
    • Sort the firmware sack by component priority
    • Stop showing errors when no Dell dock plugged in
    • Stop showing the current release during updates in fwupdmgr
    • Update all sub-devices for a composite update
    • Use HTTPS_PROXY if set

    This release adds the following features:

    • Add a new device flag 'ignore-validation' that will override checks
    • Add a new plugin to enumerate EC firmware
    • Add a new plugin to update NVMe hardware
    • Add a plugin for updating using the flashrom command line tool
    • Allow the device list to take care of waiting for the device replug
    • Allow updating just one specific device from the command line
    • Allow upgrades using a self-signed fwupd.efi binary
    • Download firmware if the user specifies a URI
    • Include serial number in daemon device output when trusted
    • Notify all plugins of device removals through a new vfunc
    • Use boltd force power API if available

    This release fixes the following bugs:

    • Add an install hook for classic snap
    • Allow forcing installation even if no AC power is applied
    • Allow using --force to ignore version_lowest
    • Always use the same HardwareIDs as Windows
    • Check the device state before assuming a fake DFU runtime
    • Copy over parent GUIDs from other plugin donors
    • Detect location of python3 interpreter
    • Do not add udev devices after a small delay
    • Don't fail to run if compiled without GPG/PKCS7
    • Fix a segfault in fwupdtool caused by cleanup of USB plugins
    • Implement the systemd recommendations for offline updates
    • Improve performance when reading keys from the quirk database
    • Remove children of devices when the parent is removed
    • Rewrite synapticsmst to use modern error handling
    • Rewrite the unifying plugin to use the new daemon-provided functionality
    • Show a time estimate on the progressbar after an update has started

    This release adds the following features:

    • Add support for the Synaptics Panamera hardware
    • Add validation for Alpine and Titan Ridge
    • Improve the Redfish plugin to actually work with real hardware

    This release fixes the following bugs:

    • Allow different plugins to add the same device
    • Allow flashing unifying devices in recovery mode
    • Allow running synapticsmst on non-Dell hardware
    • Check the ESP for sanity at startup
    • Do not hold hidraw devices open forever
    • Don't override _FORTIFY_SOURCE when building the EFI binary
    • Don't show passwords in fwupdmgr
    • Fix a potential segfault in smbios data parsing
    • Fix encoding the GUID into the capsule EFI variable
    • Fix various bugs when reading the thunderbolt version number
    • Reboot synapticsmst devices at the end of flash cycle
    • Show status messages when the daemon is initializing
    • Show the correct title when updating devices
    • Show the reasons that plugins are not run on the CLI
    • Use localedir in po/make-images

    This release adds the following features:

    • Add a initial Redfish support
    • Add a tool to mimic the original fwupdate CLI interface
    • Allow devices to assign a plugin from the quirk subsystem
    • Change the quirk file structure to be more efficient
    • Merge fwupdate functionality into fwupd
    • Run a plugin vfunc before and after all the composite devices are updated
    • Support more Wacom tablets

    This release fixes the following bugs:

    • Add release information for locked devices
    • Allow building with older meson
    • Detect the EFI system partition location at runtime
    • Do not use 8bitdo bootloader commands after a successful flash
    • Enable accessing downloaded files in flatpak and snap
    • Fix a potential buffer overflow when applying a DFU patch
    • Fix downgrading older releases to devices
    • Fix flashing devices that require a manual replug
    • Fix several small memory leaks in various places
    • Fix the retrieval of Redfish version
    • Fix unifying failure to detach when using a slow host controller
    • Set the Wacom device status when erasing and writing firmware
    • Show errors in the CLI if unable to access directory
    • Use the parent device name for Wacom sub-modules

    This release adds the following features:

    • Add a plugin to update some future Wacom tablets
    • Add 'fwupdmgr get-topology' to show logical device tree
    • Add support for creating a flatpak
    • Add support for creating a snap
    • Add support for Motorola S-record files
    • Add the Linux Foundation public GPG keys for firmware and metadata
    • Show a translated warning when the server is limiting downloads

    This release fixes the following bugs:

    • Add a firmware diagnostic tool called fwupdtool
    • Adjust all licensing to LGPL 2.1+
    • Allow installing more than one firmware using 'fwupdmgr install'
    • Allow specifying hwids with OR relationships
    • Do not call fu_plugin_init() on blacklisted plugins
    • Do not require libcolorhug to build
    • Fix a crash in libfwupd where no device ID is set
    • Fix a potential DoS in libdfu by limiting holes to 1MiB
    • Fix a segfault that sometimes occurs during cleanup of USB plugins
    • Fix Hardware-ID{0,1,2,12} compatibility with Microsoft
    • Hide devices that aren't updatable by default in fwupdmgr
    • Search all UEFI GUIDs when matching hardware
    • Stop matching Nintendo Switch Pro in the 8bitdo plugin

    This release adds the following features:

    • Add enable-remote and disable-remote commands to fwupdmgr
    • Add fu_plugin_add_compile_version() for libraries to use
    • Allow requiring specific versions of libraries for firmware updates
    • If no remotes are enabled try to enable the LVFS
    • Show a warning with interactive prompt when enabling a remote

    This release fixes the following bugs:

    • Check that EFI system partition is mounted before update
    • Disable synapticsmst remote control on failure
    • Don't recoldplug thunderbolt to fix a flashing failure
    • Fix SQL error when running 'fwupdmgr clear-offline'
    • Improve the update report message
    • Only enumerate Dell Docks if the type is known
    • Only run certtool if a new enough gnutls is present
    • Prevent a client crash if the daemon somehow sends invalid data
    • Reboot after scheduling using logind not systemd
    • Use the right encoding for the label in make-images

    This release adds the following features:

    • Add bash completion for fwupdmgr
    • Add support for newest Thunderbolt chips
    • Allow all functions that take device arguments to be prompted
    • Allow devices to use the runtime version when in bootloader mode
    • Allow overriding ESP mount point via conf file
    • Delete any old fwupdate capsules and efivars when launching fwupd
    • Generate Vala bindings

    This release fixes the following bugs:

    • Allow ctrl-d out of the prompt for devices
    • Allow to create package out of provided binary
    • Correct handling of unknown Thunderbolt devices
    • Correctly detect new remotes that are manually copied
    • Fix a crash related to when passing device to downgrade in CLI
    • Fix running the self tests when no fwupd is installed
    • Fix Unifying signature writing and parsing for Texas bootloader
    • Only send success and failure reports to the server
    • Use a CNAME to redirect to the correct CDN for metadata
    • Use a longer timeout when powering back the Thunderbolt device

    This release adds the following features:

    • Offer to reboot when processing an offline update
    • Report the efivar, libsmbios and fwupdate library versions
    • Report Thunderbolt safe mode and SecureBoot status
    • Show the user a URL when they report a known problem
    • Support split cabinet archives as produced by Windows Update

    This release fixes the following bugs:

    • Be more careful deleting and modifying device history
    • Clarify which devices don't have upgrades
    • Ensure the Thunderbolt version is xx.yy
    • Fix a daemon warning when using fwupdmgr get-results
    • Fix crash with MST flashing
    • Fix DFU detach with newer releases of libusb
    • Include the device VID and PID when generating the device-id
    • Set the RemoteId when using GetDetails
    • Stop matching 8bitdo DS4 controller VID/PID
    • Use help2man for dfu-tool and drop docbook dependencies
    • Use ngettext for any strings with plurals
    • Use the default value if ArchiveSizeMax is unspecified

    This release adds the following features:

    • Add D-Bus methods to get and modify the history information
    • Allow the user to share firmware update success or failure
    • Ask the user to refresh metadata when it is very old
    • Store firmware update success and failure to a local database

    This release fixes the following bugs:

    • Add a device name for locked UEFI devices
    • Allow each plugin to opt-in to the recoldplug action
    • Fix firmware downloading using gnome-software
    • Fix UX capsule reference to the one specified in efivar
    • Never add two devices to the daemon with the same ID
    • Rescan supported flags when refreshing metadata

    This release adds the following features:

    • Add a new plugin to add support for CSR 'Driverless DFU'
    • Add initial SF30/SN30 Pro support
    • Support AppStream metadata with relative <location> URLs

    This release fixes the following bugs:

    • Add more metadata to the user-agent string
    • Block owned Dell TPM updates
    • Choose the correct component from provides matches using requirements
    • Do not try to parse huge compressed archive files
    • Fix a double-free bug in the Udev code
    • Handle Thunderbolt 'native' mode
    • Use the new functionality in libgcab >= 1.0 to avoid writing temp files

    This release adds the following features:

    • Add a plugin for the Nitrokey Storage device
    • Add support for the original AVR DFU protocol
    • Allow different plugins to claim the same device
    • Allow quirks to set common USB properties
    • Move a common plugin functionality out to a new shared object
    • Optionally delay the device removal for better replugging
    • Set environment variables to allow easy per-plugin debugging
    • Use a SHA1 hash for the internal DeviceID

    This release fixes the following bugs:

    • Add quirk for AT32UC3B1256 as used in the RubberDucky
    • Disable the dell plugin if libsmbios fails
    • Don't register for USB UDev events to later ignore them
    • Fix a possible buffer overflow when debugging ebitdo devices
    • Fix critical warning when more than one remote fails to load
    • Fix DFU attaching AVR32 devices like the XMEGA
    • Ignore useless Thunderbolt device types
    • Refactor ColorHug into a much more modern plugin
    • Release the Steelseries interface if getting the version failed
    • Remove autoconf-isms from the meson configure options
    • Show a nicer error message if the requirement fails
    • Sort the output of GetUpgrades correctly

    This release adds the following features:

    • Add support for HWID requirements
    • Add support for programming various AVR32 and XMEGA parts using DFU
    • Add the various DFU quirks for the Jabra Speak devices
    • Allow specifying the output file type for 'dfu-tool read'
    • Move the database of supported devices out into runtime loaded files
    • Support the IHEX record type 0x05
    • Use help2man to generate the man page at build time
    • Use the new quirk infrastructure for version numbers

    This release fixes the following bugs:

    • Catch invalid Dell dock component requests
    • Correctly output Intel HEX files with > 16bit offset addresses
    • Do not try to verify the element write if upload is unsupported
    • Fix a double-unref when updating any 8BitDo device
    • Fix crash when enumerating with Dell dock connected but with no UEFI
    • Fix uploading large firmware files over DFU
    • Format the BCD USB revision numbers correctly
    • Guess the DFU transfer size if it is not specified
    • Include the reset timeout as wValue to fix some DFU bootloaders
    • Make the error message clearer when sans fonts are missing
    • Support devices with truncated DFU interface data
    • Use the correct remote-specified username and passord when using fwupdmgr
    • Use the correct wDetachTimeOut when writing DFU firmware
    • Verify devices with legacy VIDs are actually 8BitDo controllers

    This release breaks API and ABI to remove deprecated symbols!

    This release adds the following features:

    • Add a human-readable title for each remote
    • Add a method to return a list of upgrades for a specific device
    • Add an 'Summary' and 'Icons' properties to each device
    • Add FuDeviceLocker to simplify device open/close lifecycles
    • Add functionality to blocklist Dell HW with problems
    • Add fu_plugin_check_supported()
    • Add fwupd_remote_get_checksum() to use in client programs
    • Add ModifyRemote as an easy way to enable and disable remotes
    • Add the plugin documentation to the main gtk-doc
    • Allow plugins to depend on each other
    • Disable the fallback USB plugin
    • Parse the SMBIOS v2 and v3 DMI tables directly
    • Support uploading the UEFI firmware splash image
    • Use the intel-wmi-thunderbolt kernel module to force power

    This release fixes the following bugs:

    • Only run SMI to toggle host MST GPIO on Dell systems with host MST
    • Disable unifying support if no CONFIG_HIDRAW support
    • Do not auto-open all USB devices at startup
    • Do not fail to load the daemon if cached metadata is invalid
    • Do not use system-specific information for UEFI PCI devices
    • Fix a crash when using fu_plugin_device_add_delay()
    • Fix the libdfu self test failure on s390 and ppc64
    • Fix various printing issues with the progressbar
    • Generate the LD script from the GObject introspection data
    • Never fallback to an offline update from client code
    • Only set the Dell coldplug delay when we know we need it
    • Prefer to use HWIDs to get DMI keys and DE table

    This release adds the following features:

    • Add a configure switch for the LVFS remotes
    • Add a FirmwareBaseURI parameter to the remote config
    • Add a firmware builder that uses bubblewrap
    • Add a python script to create fwupd compatible cab files from Microsoft .exe files
    • Add a thunderbolt plugin for new kernel interface
    • Allow plugins to get DMI data from the hardware in a safe way
    • Allow plugins to set metadata on devices created by other plugins
    • Optionally install the LVFS PKCS7 root certificate
    • Optionally use GnuTLS to verify PKCS7 certificates

    This release fixes the following bugs:

    • Add back options for HAVE_SYNAPTICS and HAVE_THUNDERBOLT
    • Allow configuring systemd and udev directories
    • Enable C99 support in meson.build
    • Fix an incomplete cipher when using XTEA on data not in 4 byte chunks
    • Fix minor const-correctness issues
    • Implement thunderbolt image validation
    • Remove the confusing ALLOW_OFFLINE and ALLOW_ONLINE flags
    • Show a bouncing progress bar if the percentage remains at zero
    • Use a hwid to match supported systems for synapticsmst
    • Use the new bootloader PIDs for Unifying pico receivers
    • When thunderbolt is in safe mode on a Dell recover using SMBIOS

    This release adds the following features:

    • Add DfuPatch to support forward-only firmware patching
    • Add --version option to fwupdmgr
    • Display all errors recorded by efi_error tracing
    • Make building introspection optional
    • Support embedded devices with local firmware metadata

    This release fixes the following bugs:

    • Check all the device GUIDs against the blocklist when added
    • Correct a memory leak in Dell plugin
    • Default to 'en' for UEFI capsule graphics
    • Don't log a warning when an unknown unifying report is parsed
    • Enable test suite via /etc/fwupd.conf
    • Fix a hang on 32 bit computers
    • Fix compilation of the policy on a variety of configurations
    • Fix UEFI crash when the product name is NULL
    • Make flashing ebitdo devices work with fu-ebitdo-tool
    • Make messages from installing capsules useful
    • Make sure the unifying percentage completion goes from 0% to 100%
    • Run the plugin coldplug methods in a predictable order
    • Test UEFI for kernel support during coldplug
    • Use new GUsb functionality to fix flashing Unifying devices

    This release adds the following features:

    • Add a get-remotes command to fwupdmgr
    • Add a plugin to get the version of the AMT ME interface
    • Add Arch Linux to CI
    • Add some installed tests flashing actual hardware
    • Allow flashing Unifying devices in bootloader modes
    • Allow ordering the metadata remotes

    This release fixes the following bugs:

    • Do not check the runtime if the DFU device is in bootloader mode
    • Do not unlock devices when doing VerifyUpdate
    • Filter by Unifying SwId when making HID++2.0 requests
    • Fix downgrades when version_lowest is set
    • Fix the self tests when running on PPC64 big endian
    • Move the remotes parsing from the client to the server
    • Split up the Unifying HID++2.0 and HID++1.0 functionality
    • Store the metadata files rather than merging to one store
    • Use a longer timeout for some Unifying operations
    • Use the UFY DeviceID prefix for Unifying devices

    This release adds the following features:

    • Add installed tests that use the daemon
    • Add the ability to restrict firmware to specific vendors
    • Enable Travis CI for Fedora and Debian
    • Export some more API for dealing with checksums
    • Generate a images for status messages during system firmware update
    • Show progress download when refreshing metadata

    This release fixes the following bugs:

    • Compile with newer versions of meson
    • Ensure that firmware provides are legal GUIDs
    • Fix a common crash when refreshing metadata
    • Use the correct type signature in the D-Bus introspection file

    This release adds the following features:

    • Add a 'downgrade' command to fwupdmgr
    • Add a 'get-releases' command to fwupdmgr
    • Add support for ConsoleKit2
    • Add support for Microsoft HardwareIDs
    • Allow downloading metadata from more than just the LVFS
    • Allow multiple checksums on devices and releases

    This release fixes the following bugs:

    • Allow to specify bindir
    • Correctly open Unifying devices with original factory firmware
    • Deprecate some of the old FwupdResult API
    • Do not copy the origin from the new metadata file
    • Do not expect a Unifying reply when issuing a REBOOT command
    • Do not re-download firmware that exists in the cache
    • Fix a problem when testing for a Dell system
    • Fix flashing new firmware to 8bitdo controllers
    • Increase minimum required AppStream-Glib version to 0.6.13
    • Make documentation and man pages optional
    • Make systemd dependency at least version 231
    • Only decompress the firmware after the signature check
    • Remove 'lib' prefix when looking for libraries
    • Return the remote ID when getting updates about hardware
    • Send the daemon the remote ID when sending firmware metadata

    This release adds the following feature:

    • Add support for Unifying DFU features

    This release fixes the following bugs:

    • Do not spew a critical warning when parsing an invalid URI
    • Ensure device is closed if did not complete setup
    • Ensure steelseries device is closed if it returns an invalid packet
    • Fix man page installation location
    • Ignore spaces in the Unifying version prefix
    • Set HAVE_POLKIT_0_114 when polkit is newer than 0.114

    This release adds the following features:

    • Add a config option to allow runtime disabling plugins by name
    • Add the Meson build system and remove autotools
    • Support signed Intel HEX files

    This release fixes the following bugs:

    • Add DFU quirk for OpenPICC and SIMtrace
    • Create directories in /var/cache as required
    • Refactor the unifying plugin now we know more about the hardware
    • Set the source origin when saving metadata
    • Support proxy servers in fwupdmgr
    • Use a 60 second timeout on all client downloads

    This release fixes the following bugs:

    • Adjust systemd confinement restrictions
    • Do not hardcode docbook2man path
    • Don't initialize libsmbios on unsupported systems
    • Fix a crash when enumerating devices on a Dell WLD15
    • Fix compiler warnings
    • Fix fwupdmgr timeout with missing pending database

    This release adds the following features:

    • Add a set of vfuncs that are run before and after a device update
    • Add Dell-specific functionality to allow other plugins turn on TBT/GPIO
    • Add support for Intel Thunderbolt devices
    • Add support for Logitech Unifying devices
    • Add support for Synaptics MST cascades hubs
    • Add support for the Altus-Metrum ChaosKey device
    • Add VerifyUpdate to update the device checksums server-side
    • Allow the metadata to match a version of fwupd and the existing fw version

    This release fixes the following bugs:

    • Add a new method for forcing a controller to flash mode
    • Always make sure we're getting a C99 compiler
    • Close USB devices before error returns
    • Don't read data from some DfuSe targets
    • Include all debug messages when run with --verbose
    • Return the pending UEFI update when not on AC power
    • Use a heuristic for the start address if the firmware has no DfuSe footer
    • Use more restrictive settings when running under systemd

    This release adds the following features:

    • Add a 'replace-data' command to dfu-tool
    • Use an animated progress bar when performing DFU operations

    This release fixes the following bugs:

    • Add quirks for HydraBus as it does not have a DFU runtime
    • Don't create the UEFI dummy device if the unlock will happen on next boot
    • Enable hardening flags on more binaries
    • Fix an assert when unlocking the dummy ESRT device
    • Fix writing firmware to devices using the ST reference bootloader
    • Match the Dell TB16 device
    • Re-get the quirks when the DfuDevice gets a new GUsbDevice
    • Show the nicely formatted target name for DfuSe devices
    • Verify devices support updating in mode they are called

    This release adds the following features:

    • Add dfu_firmware_add_symbol()
    • Allow the argument to 'dfu-tool set-release' be major.minor
    • Load the Altos USB descriptor from ELF files
    • Support writing the IHEX symbol table

    This release fixes the following bugs:

    • Add a fallback for older appstream-glib releases
    • Fix a possible crash when uploading firmware files using libdfu
    • Fix libfwupd self tests when a host-provided fwupd is not available
    • Show the human-readable version in the 'dfu-tool dump' output
    • Write the ELF files with the correct section type

    This release adds the following features:

    • Add a set-address and set-target-size commands to dfu-util
    • Add a small library for talking with 0bitdo hardware
    • Add Dell TPM and TB15/WD15 support via new Dell provider
    • Add FU_DEVICE_FLAG_NEEDS_BOOTLOADER
    • Add fwupd_client_get_status()
    • Add fwupd_result_get_unique_id()
    • Add initial ELF reading and writing support to libdfu
    • Add support for installing multiple devices from a CAB file
    • Allow providers to export percentage completion
    • Show a progress notification when installing firmware
    • Show the vendor flashing instructions when installing

    This release fixes the following bugs:

    • Add XPS 9250 to Dell TPM modeswitch blocklist
    • Allow blacklisting devices by their GUID
    • Conditionally enable all providers based upon installed
    • Display flashes left in results output when it gets low
    • Do not attempt to add DFU devices not in runtime mode
    • Do not use the deprecated GNOME_COMPILE_WARNINGS
    • Don't fail while checking versions or locked state
    • Embed fwupd version in generated documentation
    • Ensure the ID is set when getting local firmware details
    • Fix gtk-doc build when srcdir != builddir
    • Fix libdfu hang when parsing corrupt IHEX files
    • Ignore devices that do not add at least one GUID
    • In get-details output, display the blob filename
    • Save the unique ID in the pending database
    • Support the 'DEVO' cipher kind in libdfu
    • Switch to the Amazon S3 CDN for firmware metadata
    • Update fwupdmgr manpage for new commands and arguments
    • Use a private gnupg key store
    • Use the correct firmware when installing a composite device
    • Use the SHA1 hash of the local file data as the origin

    This release adds the following features:

    • Add a GetDetailsLocal() method to eventually replace GetDetails()
    • Add fu_device_get_alternate()
    • Allow devices to have multiple assigned GUIDs
    • Allow metainfo files to match only specific revisions of devices
    • Show the DFU protocol version in 'dfu-tool list'

    This release fixes the following bugs:

    • Enforce allowing providers to take away flash abilities
    • Only claim the DFU interface when required
    • Only return updatable devices from GetDevices()

    This release adds the following features:

    • Add a --force flag to override provider warnings
    • Add device-added, device-removed and device-changed signals
    • Add dfu_image_get_element_default()
    • Add for a new device field 'Flashes Left'
    • Add fwupd_client_connect()
    • Add the 'monitor' debugging command for fwupdmgr
    • Add the 'supported' flag to the FuDevice

    This release fixes the following bugs:

    • Add summary and name field for Rival SteelSeries
    • Fix a critical warning when restarting the daemon
    • Fix BE issues when reading and writing DFU files
    • Make the device display name nicer
    • Match the AppStream metadata after a device has been added
    • Remove non-interactive pinentry setting from fu-keyring
    • Return all update descriptions newer than the installed version
    • Set the device description when parsing local firmware files

    This release adds the following features:

    • Add a version plugin for SteelSeries hardware
    • Add FwupdClient and FwupdResult to libfwupd
    • Generate gtk-doc documentation for libfwupd
    • Return the device flags when getting firmware details
    • Support other checksum kinds

    This release fixes the following bugs:

    • Add Alienware to the version quirk table
    • Allow the test suite to run in %check
    • Do not return updates that require AC when on battery
    • Do not use /tmp for downloaded files
    • Test that GPG key import actually was successful

    This release adds the following features:

    • Add an unlock method for devices
    • Add a simple plugin infrastructure
    • Add ESRT enable method into UEFI provider
    • Install the hardcoded firmware AppStream file

    This release fixes the following bugs:

    • Correct the BCD version number for DFU 1.1
    • Do not use deprecated API from libappstream-glib
    • Ignore the DFU runtime on the DW1820A
    • Only read PCI OptionROM firmware when devices are manually unlocked
    • Require AC power before scheduling some types of firmware update
    • Show ignored DFU devices in dfu-util, but not in fwupd

    This release adds the following feature:

    • Add 'Created' and 'Modified' properties on managed devices

    This release fixes the following bugs:

    • Fix get-results for UEFI provider
    • Support vendor-specific UEFI version encodings

    This release fixes the following bugs:

    • Always persist ColorHug devices after replug
    • Do not misdetect different ColorHug devices
    • Only dump the profiling data when run with --verbose

    This release adds a new GObject library called libdfu and a command line client called dfu-tool. This is a low-level tool used to upgrade USB device firmware and can either be shipped in the same package as fwupd or split off as separate subpackages.

    This release adds the following feature:

    • Add support for automatically updating USB DFU-capable devices

    This release fixes the following bugs:

    • Emit the changed signal after doing an update
    • Export the AppStream ID when returning device results
    • Fix compile with --disable-shared
    • Use new API available in fwup 0.5
    • Use the same device identification string format as Microsoft

    This release fixes the following bugs:

    • Avoid seeking when reading the file magic during refresh
    • Do not assume that the compressed XML data will be NUL terminated
    • Use the correct user agent string for fwupdmgr

    This release adds the following features:

    • Add profiling data to debug slow startup times
    • Support cabinet archives files with more than one firmware

    This release fixes the following bugs:

    • Add the update description to the GetDetails results
    • Clear the in-memory firmware store only after parsing a valid XML file
    • Ensure D-Bus remote errors are registered at fwupdmgr startup
    • Fix verify-update to produce components with the correct provide values
    • Require appstream-glib 0.5.1
    • Show the dotted-decimal representation of the UEFI version number
    • When the version is from the 'FW' extension do not cache the device

    This release fixes the following bugs:

    • Fix the error message when no devices can be updated
    • Fix reading symlink to prevent crash with some compilers

    This release adds the following feature:

    • Raise the dep on GLib to support and use g_autoptr()

    This release fixes the following bugs:

    • Do not merge existing firmware metadata
    • Do not reboot if racing with the PackageKit offline update mechanism

    This release adds the following feature:

    • Remove fwsignd, we have the LVFS now

    This release fixes the following bugs:

    • Add application metadata when getting the updates list
    • Depend on appstream-glib >= 0.5.0
    • Don't apply firmware if something else is processing the update
    • Install fwupd into /usr/lib/$(triplet)/fwupd instead
    • Simplify the version properties on devices to avoid complexity
    • Update the offline update service to invoke right command
    • Use the new secure metadata URI

    For the device verification code to work correctly you need at least libappstream-glib 0.5.0 installed.

    This release adds the following features:

    • Add a Raspberry Pi firmware provider
    • Add a simple config file to store the correct LVFS download URI
    • Make parsing the option ROM runtime optional

    This release fixes the following bugs:

    • Allow fwupd to be autostarted by systemd
    • Allow no arguments to 'fwupdmgr verify-update' and use sane defaults
    • Devices with option ROM are always internal
    • Do not pre-convert the update description from AppStream XML
    • Fix validation of written firmware
    • Move the verification and metadata matching phase to the daemon
    • Sign the test binary with the correct key
    • Use the AppStream 0.9 firmware specification by default

    In this release we've moved the LVFS website to the fwupd project and made them work really well together. To update all the firmware on your system is now just a case of 'fwupdmgr refresh && fwupdmgr update'. We've also added verification of BIOS and PCI ROM firmware, which may be useful for forensics or to verify that system updates have been applied.

    This release adds the following features:

    • Actually parse the complete PCI option ROM
    • Add a 'fwupdmgr update' command to update all devices to latest versions
    • Add a simple signing server that operates on .cab files
    • Add a 'verify' command that verifies the cryptographic hash of device firmware
    • Allow clients to add new firmware metadata to the system cache
    • Move GetUpdates to the daemon
    • Move the LVFS website to the fwupd project

    This release fixes the following bugs:

    • Accept multiple files at one time when using fwupdmgr dump-rom
    • Automatically download metadata using fwupdmgr if required
    • Do not return NULL as a gboolean
    • Don't call efibootmgr after fwupdate
    • Fallback to offline install when calling the update argument
    • Fix Intel VBIOS detection on Dell hardware
    • Reload appstream data after refreshing
    • Use the new LVFS GPG key
    • Fix build: libgusb is required even without colorhug support

    This release adds the following features:

    • Get the firmware version from the device descriptors
    • Run the offline actions using systemd when required
    • Support OpenHardware devices using the fwupd vendor extensions

    This release fixes the following bugs:

    • Add an UNKNOWN status so we can return meaningful enum values
    • Coldplug the devices before acquiring the well known name

    This release adds the following features:

    • Add a 'get-updates' command to fwupdmgr
    • Add and document the offline-update lifecycle
    • Create a libfwupd shared library

    This release fixes the following bugs:

    • Create runtime directories if they do not exist
    • Do not crash when there are no devices to return

    fwupd is a simple daemon to allow session software to update firmware.

    fwupd-2.0.10/data/org.freedesktop.fwupd.plist000066400000000000000000000012051501337203100211230ustar00rootroot00000000000000 Label org.freedesktop.fwupd ProgramArguments @libexecdir@/fwupd/fwupd Sockets Listener SockPathName @dbus_socket_address@ SockPathMode 438 RunAtLoad KeepAlive fwupd-2.0.10/data/org.freedesktop.fwupd.png000066400000000000000000000326671501337203100205740ustar00rootroot00000000000000‰PNG  IHDRôxÔú pHYs;;̶¡ƒtEXtSoftwarewww.inkscape.org›î<tEXtTitleAdwaita Icon Templateµ»ç?tEXtAuthorGNOME Design Team`Žv~RtEXtCopyrightCC Attribution-ShareAlike http://creativecommons.org/licenses/by-sa/4.0/ÃTb IDATxœíÝyt\õçýϯ6m¶dKÞÁ`ƒÁlÌ–@ÈʲAf’Cºs¦;²cC?>Ýsfž§§ifž3OÏ9O?!ñ†…!¤“NÎÄÝIH†„IKX añŠy‘-Ù’-ÉÚ—ªº¿çcaɺҽ¥[U¿÷ë¿RÝû½_ýªtïGuëþ®±Ö ¸%u`âppppppppppppppppppppppppppppppppp%¢nVW·öžôPLvjÐZÖ(k­ž­™:ãïîºë®lý»õë×ü•dï2Æ$ƒÖ²R¬ù—-[ñë0z+vëÖ­›‹yÿŸ1ZF=k´?Ó_¯üÖʃaÔ+v=¶öFOöïUuÐZ'÷=öùš©3ÿ–}O~"ä™NYMû¥‘jl­d¤¶··ì—´!Œ’ÅìÑÇVßnŒùGÉ(”ñ—$coذá{ÜwßÿÑVÉb‹eÿ‡Œ¹;´±·º6á©JÒgÃ*Y¬êêê’Vö)IÓÂÛ÷˜Sûžº0J"\œÈ3ÇŽMŸ)©&ìºVº"ìšEÉÆ.ÏAÕT&_ƒºÅ'f«\¼¦EÇó¼’¦…^—}OÞ"ä™D"c¢îÁeÆÚœŒ,ãuõÁZ…>N'?Ãh‰4ãäNä¹²²2Ý|ó­c^oÿþ}Ú±c{:rËEó/Ò¢ËÇþÌ+¯üAíím9èÈ-7~ìãšZ=¶ÓÑýýzö¹ËQGî(--Õ-·Ü6æõì߯í;¶å #„çb±¸jªÇ~F ¥™ÓÍa(-+×ø'“üi…¡ªªjÌãßÛ×›£nÜ2Þ}ϱ––tƒ\à"à "à ¾©¢õë×|Ì]¡@Á*Q­ð¦ 9Ý’ºº5µA Xk;’ÉòßÜ{ï½]a5–'Ÿ|²´?Ýóù˜Õô@…Œ¹!ãoeשׂ[èzt/f÷.¿ïMÊÑ$ˆ VŸÍÆo6Æ–©c¤™aõtZÍòÞûiÏ‹¿òío{WX}…i݆U7ĽØbØ÷X›š‹ &Õâ0ö=ƒƒö·>ø`gX}šõë×ü•Œþ1øž9gûö[¬tK  Æ(éßóÝï~wéw¾ó¾ú ìᇎ͚=íy#},Çÿo‚V6žÑúõk¿·lÙŠ¿ ¥£¬Ý°vQÜ‹½.c'ç]29iŠ•Öª`Œbq/ý裫?»|ù¿§­pÔÕ­ù˘bß <ö¹šÀèf+ݬ†QI‰Ù[WW·´¶¶–Ëž£âã2{vÍ¥’>–‹Úfœ{Åœ¿±9yŸÏÚ¯Jšœ‹ÚãÇXÎÞûJšxìÏrU|¼¬r³ïï8æê½o¥Ræ9)î(@H¬è£Ï‘Ìœ1c\ëMŸ1#gˆYÅrò»Ž—1¹ëgÆÌñÿ̳BîdH^½$)¦’\”-))QUÕØï‡UZZ¦ªªÊt$Éææw ('ï‰ãÝ÷LŸž»}ñòïý_À8#óæÍW2™ T£¢¢\—/ßiãšêÝrómj8tP6àgƒMMêééVd•””è‚ æªa$Íš=[sÏ¿`\ë_yåR•””èDGG >$«={v¬1±jj¦©zÈœ.ëÒK/÷„J·ÞòY½·ë= ¦õÑÕÕ¡£Gª1Ñæ]8OÉT°œRQ^®E‹Æ7…uˆûž#Mê. }O¡!äÈUK¯VeeŽþ ñiΜ9š3gNà:Ï?ÿ\A€òò }솜œð-‹iÑ8ÃÛé²ÙlÁ€¹çÏÕ•W.´‡ŠŠIºöšë×Ù·¯¾àÀÒ¥W«ªª*ÒÂÚ÷lÚô< ‡8€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ©€s$›Íjpp0ê6BáyÞÙ?š´nݺ±ß¥%Gb1[yúMû¬µE3öÙlöŒÇÖZ“Oc/I±„)=ý6ÊÅôÞÏd2gþÀ*•oãob&nÌãŸN§‹fü‡Ù÷ Dƽ[ƒ£}ôÑyŠg?j<]*é2}Uùx§6(m’´ÛZí–ìcôfmíÛtz …/ŸV­ZU™J™;%s‹Œ>#i^Ä-N:.ÙÌóžÿåòåË£n¨Îáá‡NÌž=ãvk²ÿAÖü;IeQ÷8§¬•Ùd¬~”Íêç+V¬àv‚# cÕªU%©TüOdô·’½$ê~ãÒe¥Ä”ü‡ÚÚÚ#Q7“o§Ù¸ñáTÛ‰š¿5-ifÔýBÑk¥ Ù´þëÊ•+[£n&_ÞWW·ö&+»ZÒåQ÷ȉvûpõ”™«ïºë®ìè‹7çÀš5kjI³N²wEÝ ÷¬ô†±ÞÝË–=¸#ê^¢ätX·aÕ'c^ì'’κÀ„êµF.¿åQ7W€y´nõÿidþ^L†βÒObJÞ_[[Ûu/͹°qãÆx[[ó:sÔ½ò€ÑæTÂûÒ=÷r­¨¿°~ýšÉèIɉܮµVétfô£ŠÇcŠÇã¾]+óàòÚ«'|ähÀã·:“M½-é‚\mÃZ«Žö.5mÕ±cmêêèVgg·²Y/W›'Åb1MšT¦ÉU“TS3E3gMSõ´©ŠÇM.7; k?±lÙoær#Q)ÚPW·æq+Ý›‹Ú}½ýÚ»»Aû÷Rw—ssG@^H•¤4oþ]|ÉšZ]•«Íl=zäø5=ôPÑ}¬[” ®ní'¬ì‹’B†==}ÚöÎ.ØXÙlñ"cŒfÏ™®%W-TÍ´)á×—ùëÚÚÿzáˆ]¨««KZ ¾%™ÅaÕ´ÖjǶzmß²[™Œó7€¼dŒtÑ%êêk)• õ«_ÝÙŒ¹|ÅŠ‡Â,µ¢» À3™»Ã<ø÷ööéÙg^Ñ»oíäàyÌZ©~÷Aýæ©t¬¥-ÌÒ“âI=fÁ|PTŸ<üðÉY³§½'éâ0êµ?¡ž{]ýýƒa”LX,¦ëo¼Ró/žVÉt&®KŠi–À¢ú`Ö¬š?UHÿæ#ÇõÜÿ~…ƒ? ÏóôÚÞÕ{;êÃ*™Ldì «X>(ª`Œù›0ê´?¡žƒü €Ykõö›;µoOC8¹·®®nZ8Å¢W4`݆u×ZéÊ uz{ûõÂs›•ÉÝàk­^u‹š¶†Q®ÔSæOÃ(”Š&ÏûFÐÖZ½úÒ[êï£%@°Öê•ÃÚ·ÛÀÇš|Q ®®.)éëAëìÞu ¬”È#}}ýúãæíëé#uu«/ ¡¥ÈEÈÆn0Òô 5úû´å­÷Âj gîoTó‘ã!T2w„P$rEŒgn ZcçöznàEîÝ·ƒÿ£g¥›Ch%rÅdn ²þÀ@Z{v©@¾:~¬=ŒS½ŸÜ¸ñáTýD©àÀ“O>Y*é† 5ö7rÉ8¢~oàË+Z[k>F/Q*øÐ—é»TR $¶o_QMï 8‡Ã(“öOŸ1ZR;‘)ø÷² ƒ¬?0VÛñaµÈs™LVÇZ‚0F—†ÔNd >Xk€–æã*¢Û!|8z4ØÕžb)`Áœû¿½­3¬>¢½­#ÐúF6”ûÎD©@U•;;ºÃêP :Oô-èØ“ >Xc&Y¿¯·?¬V¢¯¯O6Øùß@Çž|PðÀÈz2LþαVÊf¼ %Ê~øáDXýD¡€Ê‚¬ŸÍrý?¸(“ ö`uuuyH­D¢à€•LÀõ. x()) tü‰ZÁ0vDÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀADÀA”ˆºøSV^¢óçO‹ºÐµïÒñæÎaŸ›:m²¦Í¬ô]«¯»_‡¶†ÕšoóÌT¢ÄÿŸÒ‘†6uwõ ûÜì¹ÕšTYæ»V[K—Z ?~¹’HÄ4áì1­³÷QeÒÙaŸ»há,Åñ0Z˹¶æNµï T#‘Œkþ¥³Bê(÷WoÏ@Ôm "—JK“2Æ(+ün¬µò;¥—õÔÞÚx»õõõêìšØ™Oiin‘$UM)WIirÔåûÓêê~†Kä'@zõµ×ÔÞÞu@¨öíÛ§W_{5Ò&W•irÕèS5g³¡€wÞyG\'ˆêé#OHuºŽ=€C(RÖZ5F0/þXõ÷¥£næxK‡ú{'ö}SZžÒ´•ºdÁ]¼`Á9—Ý´i“2ƒ½¡n¿±¡UÖ³¡ÖMåÔrUV•ëºk¯Ó´é#߇¤¯·O/¼øÂv†°Š”µ å? ßtwö««cbÿӬ̔kÚŒJMŸ1CW\~Å9—}ñÅ—Bß~G{¯²™‰½×Eª4©Ê*éÂyjÞ…óF\®£³“P ø "à "à "à .,bƘȶmíÄ^³ì×XÇ$_—c&þ½ýþæ¬g•ÍNìåx@®ŠT,f´øš #Ûþîm‡50‰lû#™;šª¦Vø^þØÑmdVÅ|ráÅ3"Ûö«¯½ùl„@XEhæÌ™*++dÛímmêëÏÿé@gΜ©x|ä·ÿÀà€ZŸÀŽ0šÊÉ“5gÎy‘l{ ¿O­mmJ§3JŒþ @z‚'íƃP„¾ø…/F¶í§ó´Þ{ï½È¶ï×_ºCUUU#>ßÐpPÿå_&°#Œfñ’%Z¼dI$Û®¯¯×/Ÿú¥ÚŽu«å7²BqàK€8ˆ€ƒ8ˆ€ƒ8ˆ€ƒ8ˆyŠÐK/½¤žžîH¶}äÈ‘H¶+I¥eIÍ7íœË”W”H’^xñ¥’É—ëîé‘$UV•)™ŒŸ³f"AŽž{öîUýÞ=‘l»³»+’í¹D(B{öîQ{»{Ó×&’qM©™äkÙ={üHJÊR*)Ki !9ÖÒ¢í;vDÝP4EÊZ«½;š"Û~:=q÷())ÑÝßø¦¯eÿ :pð€öïiVfpäË+JtÞ¼iZ¼d‰®»úšQëšø¹?%@xZÕÛÕɶ3Lñ‹"B(RÖJýýé¨Û˜ÆÕL;÷Gÿ§¤R'ÿ›ìOkð ‘:y@//-ó]#=˜qæ½ ä'/pppq žçy:z䨯eûûÇvýxWw·šGŸO!iÖ¬Ycª}.G››ÕÕ9òìs­­­’¤d*®ŠI¥¡m×XÜH:9–£M_ßD´TôÊËSò<;¡ÛL½?fkk«R‰‘'ÃêyÖL Þàà ~ú?š“Ú;wîÐΣÏ>WZZ¢•+m»¿úÕS¾–›Z3IS}Î~¶#GŽälÜq¦y—ÌŒlÛ¿ÿýï#Û6r‹€¢‘NgÔÙîï?NÏóÎ]k £Öó¿O©©ðµœÝ'z•}¶¹d*®Ê)åš5k–æÌžÚöýÈdÒÚ²u«Òuvøïlvbÿ{- ,Puuu$ÛnljTss³:|¾'ûz& +„‰€¢1ЗQÓ¡Öpj ø¯5yJY(Û”¤¶Ön©uô9M®,Så”rÍ»pž>þñ‡¶}?z{{µeëVõ¤Co oéÒ¥‘mû¥—_Rss³ÚŽu©»“S9ň/à "à "à "à æ(0o½õ–Þyçs.3Ú$7…æéßüF¿ùío£nãœúûôÈ÷9ç2¹x]^ßüºÞxóÐëN(+uw÷D2~QÚ»w憎s”Šm¼ña€áe¥þþô–/ü?ÞLÆÓïœô¿l˜ûÒïÏÓîoûÙLð×&ëmlr!= ¥ÎÀ@Z™¬'¿ãçø¬‚ÖÚÈ_»±(†} †G(}½Ú³½1ê6&Tûñ.µ÷7o”öïmžðmövÏûaß.7r*™t¶h^;6¾€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ›ˆ²Š”/] x"øKæe³Úþn½zºûUR’Ðâ«(™J®k=OïmÛ¯Ž=J$ãºòêJ•–Œ«Öñ–ví}ïЇ~nŒÑÒ먴¬ìä6­§=;ÔÖÚé«î•×,Pù¤ò÷¶j8pTM‡Ž|îº*/ÿà¹õM:ÚÔê«îâ«/ҤɓNöh­šéà¾ßä&‘ŒkÉ5«¤¤ÔWÝî®nm{{ß°Ï]qÕEš\9ÉW³õv÷jË[{‡}nÑ’yªšZ9®ºgë<Ñ©[„R @¸"fbúÈu×é£×$p­w·lÑÎm'*&Óå—_¦[n¹%pÝ}ûö©~×'ëéÂùêËwÞ1æ:ÖZ=òȪaŸ3Fš9k¦¾þ§"IjhhÐÚ}Oø®=¥zŠî½ç›’¤¶¶6­]³aè¹êꩺû›ß$µ´´híšÇ}×­¬ªTíýß’$õööjõªõ#ôotÑEóuÇ—¾ä«nÝc#÷0¹r’–/»ßw§{â?ñ¹I“+Æ]÷lk×?¢Ç)Ä'Êó²Êd²ã[7{æzÙ¬§ÁÁÁ—Ovz NËZ;ìr™LzÄÖ³JŸãù³IñøùÔZOž7ü²±Ø™Ëzž§S-ÆbFÆßÛÍ…X,¦S-ÄcF^Öžs¼ÏdÏúÝìˆã₩$yg¯•çP7=( ÿ”ã^@Ž ÔK/ýAo¼ùG;öƒ›µVƒƒ'C@&“Õ;ooÕÖ-Û‡]6‘Šë•+Ç%IOüàêîì±v_ÿ°?ß³w·~õ«ßÊo·©Ò¤nÿÒÇ%IƒÚ¹­^M‡>|>~Ò¤IºúÚ+d¯9y ikk×;oìRÿɰqÅÒ‹4÷‚9CËŸ7ç|Ÿ„géGhö¬™’NŽ÷¾ýû´êûk}­›HņÆÁZ£iç–ýZn0=¨G]¯ô ¿PO~PW’š´ííúa—]¿¾Nƒý_uÏÖÙÙ3®õä @e³YÕï<¬¾¾±ü×÷a™Á¬v s@9eé5—œñØó¤­#(FshÿQuœðw@¸ås×ëk_ûš$©©©Q»wv¹êêjÝu×]C÷»Ó;oìzœ,I Õ‰JEŤ¡:;;µfMvm;èkÝë?±dh]ÏóôÈ#«G\6=èù~m®½aÑãòýï¯qÙlvü¯9€üÅwpŸ°òŠ%’q_Ë d48àÿ|.”–§ät"ÿ,ƒéA8p@’ÔÚ6ò¥xýýý:zôƒËízzºU>¹TÉÔÉ·¶õ¼¡:’TVV¦™3g޹÷d2¡Ò²ä?ëêìséä÷&W–ùZÖ³þÆë$ë»îÙãrÎe­ç»®4þq0±jɒŪ®žêkÙ¾¾>=÷ìKÃ^W?QfΚ­¯ÿ‡¯ø^~Ó¦´vÕ—ÀuŽð½ƒ––ýèŸ~ª®Ž“Ï×L¯ÔW¿öEU”WH’^{íõ3ê\vÅź÷Þ»ÇÜÿŒYStÛç>­)S¦H’6¿ñ†^}aëˆ_ÈIYY™n¿Ýÿ%—¥¥þ¼ÉxR·Þz“ì­þúyõ¬qé?Ç©¤[nùŒnúŒ¿ ²ùõ7ôÊ‹[|- Z€5}útMŸ>Ýײ'N´kÓs/縣s«ª¬ÔUW]åkYk­6mzQ-GOøZ¾§{`hÙÉ•ºìÒ…ª™6M’ôö;ïžQç²Åã¼"À]rÉ%:ï¼ó$IÛ¶ ÿ¥ÉÑ$“Ißã0&f´ôª¥¾—ëíw}ï•K®ô]wëÖm¾—-¾€ƒø @544hçÎ÷|-;00 lv|s„¥µµUo¾ùGßËcµà²¹C4SO×ð—ž.›õô‡W^šj·§gü—¡M®,ꡤ$®×7o:µÐÙÕ¥ çʾ{Ìç z饗}¿“'OÒ7Þ8êrž—Õ¦M/(“ñw¹^__ÏãÛu¢[ÍGÛ‡]vÓ¦Mô÷ý‘ÎÎ_ˈ @íÝ[¯§õœúýí˜Ç;iPXZ[ëwÏü^Ý£Ä%écŸ\ªo|ãä4Â-ÇŽéŸÿég¾À¡ƒ-:Òøû ­J’jjjtÏ=ßzüî»ï꩟ÿN}½’¤Ë®¸PË¿}Lìä‡h Ÿ÷hЛo¼£ý{}-¿ôÚK}€L6«wÞÙ¢Ý;|Õ½béEZ±ò¾¡Ç?þñOG ï¾»M;·Ž|©(€ÂD(`éŒï2ƒžï~ãñ¸ª««%Iýýþ¿UžÍd• !윾}éä—÷Òƒ§·QuuŒßýOce}ƒ1c8Kg͘êžþû²pA½ÏøÃwpŸ8.•Jèò+çx›áX\:}*ýT*©koX4ì²FÒ®ÕuŽ©‚ Eljúù/žšÉ~ ¿_Þi—üõ÷èÉ'(ûþàL«©Öã¸óa!H¥â#¾æ£éêìÖîÑ]~ `dÇÅ1-¹ò ß·þÖ½÷ŒøÜ¾}ût þ u…Õ\„uð@“ìmöù]Û´kûçÛoüLø—öå‹ßþö¸×=y;`8€ƒ8ÈŒu*Ó|³¾nÍIKÆ»þ¯ñ¼º à–¥¥¥I]zż¡{Û{Ù¬vm?¨ÁÁñݦõ”T*¡…W\¨XÜß=FS¿»AÝýJ$bZ¸xÞ—ÇíÙyP½=¾ê,ºržR©ÔÐãƒûŽèD[—b1é²%ó•L~0/ÿþ=êìð÷.\|¡JKK†7jѱæ“3â-Zr¡R%wÙùª˜T>ôܱæv5:æ«îü³U9eòÐ㎶NØwòž ó/™£ÊªI¾êŒ¦»³Wõ»‡R ÛW¾öY•–•Œ¾àŒ’Sjkk vò @@¸\œÀADÀADÀAÌX JË’ºlñ<Åb'/×Ëf³Ú¹e¿¯Ë“ɸ-™¯DòäËm­§÷¶T_% ]¾d¾âã¼ °åH«7|øÒ³x"®+®œ§Äi—ëEë±:øþeiçRV–ÒÂ%óóyÓœí]Ú·{ø;ñ]¾tþ—Ž›µj>Ú¦Æ÷ÇeÑ’y*;í2À Ž·œPÃþ“ã²ðŠ Î¸ p,úûúµcËPzP˜"ëÆ}L½þ#’¤çŸß¤ÝÛúZ7iÑå õùÏN’ôî–-Ú»ëÇï×éÊ1L|º¾¾~­Y½nØ‹I_r±¾<Žùñ­µzä‘U¾@<Ó'>q£®»öZ_µW­Z+iø0cæ4ÝýÍo ûÜXôööjõªõC fzµîûÖÈS(ûåyžydõP˜Z3EË—Ý?®ZOüà‡’î @áâ"à Nà ï¾û®^|áe)fF_ØZ•”%ô©[O~üžMg´ck½ÚÛº?´h}}½ž~ú·21™37Cu½LV{v7¨¹©M±˜ÑÒëhre¥$)“Në•—_Ñ«¯¾î«®g3CumÖÓÁýMj8Ð,I:ÖÒªU«×ŽôËê«_ýŠæÌž-IúÅSO©±¡Qv˜a2Vê8ñá1¤îî.ýð‡?’7Î 8;N ¯Åt:­ÇêSÖgÝlöƒq¤ã-­|'p gLë½ÔÓÕ?ê²ñD\_¸ó“úêW¿*éäí€wí80ì²ÙlFõ»«ã„¿i—oùÜõúó?ÿ3IRSS£Öxbè¹3gêëú'’¤††­]ý„Ž6¶úªû©[¯Ó½÷|S’ÔÖÖ¦µk6 =·ý}#®wÞÓe=oèqOW·þøú{ëTÚžgÕÙÙ«]Ûü}Ã/+«¾¾´¶¾½××ò×Þ°H÷Þs÷ÐãïM¨ýÈœÀA|€I¦*)ý­OÄ•NgÔÞÞ.Iêîþcï\‹Çc¾úÍ1Éw¿ž¤ô€ß;>Zßu­õ†^7n*œ½&&ÄÜóÏÓç¾p“¯e3™ŒöìÙ£u§}ŒÞÓ3ú©ƒ0UVUéSŸ¹A~?‰Ÿ1sFnEII‰®¹î*-½úJ_ËïÙ³G|í½Q—KÄãZzÕ]±ør_uë÷í=ãué; Šg˜={¶f¿ÿE·Ñôõõë½»´{gCŽ»Ù”ª*Ý~ûg#ÛþX•””è¶Ûnõ½|áC¾–‹ÅâºõVÿs9<ñƒ#zcçNßË(>|ñ ÎÐÞÞ®ƒýýGŸÉ¤eÇ{=[Hº{z´w¿o¾KRUe•æ_4/w "NkÇŽ¾¯Èdüÿ·žÕ–m[}¿ýš>kêÐãþ¾uuôúZ@q à {öÖkãO©¾þA_˧³9îèÜÚZ[õóý•ÚZ‡¿>þl×|d‘æ_|ZÞñêëëÓ3Ïü›löµ¼õya:›Ö³¿{^û÷5ùZþÒ…hŃŒÃ¯õ½õúèß5P<øÞÞ_óä‹Á¬º;ú|.ýY/ϳcèw,Œïº&ÓüyósЀBýÞL8>( [¶nу'g;ÑÑ®—Íõyù›UCCƒ~ö³‘tr:Ú(̽h¦æz>¦–t¢£c¨ßÁ~e³ÑžjSyEJ‹¯ºx\ë¶;¡#>g=€s!ˆžî~½òâ–¡Çó.ž­ûîû†j¦ÕŒºnGG‡6ÔýP¯ÿaûÐÏ&úË{—,¸T>0Ï÷òk×®×3ÿë¥~`½‘. “'OÖòeËÆµ®çY­[WG @ñ²§­”L%URR2êz©TòÃëO03¾z•4ô ù(ûÍcüÃÙ<¯øÆ@tøâ€ÕÓÓ§?ûWÉø9§nÕÝ=ü5Þ^Öjû¶Úµkïû³òüÞSö\[ô¬öï? µk×ký®ÎáûµV:ÚtôƒºÖjÀç%‹’ÔÞÚ>´®‘UÏãr¶þþA=õÔ¯%™÷÷ù¾–ÿD{û¸Çál]ÜM±óD×i¿‹ÔÕåïN‹§êœÞÓHã  x TsS›š›Ú×Hë­Í»BèèL™Œ§w6ï½®µVïþÑÿÄ?gÛúvý¸ÖkméPkKǸÖÝööþq­7š[Æ_w÷Žè¦o8€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ€ƒ8ˆ€ƒŠ!xAV62aõ($&Øþ?‘H:þD­@o•ã‰bÀX%“ñ «ÛC‡õ„ÕK ÿègÕdõT*V'€‹Å ==ôŸDÉuY¿¼¢<¬V¢bRYÀ ¶+”F"TðÀZÛdýÊªŠ°ZˆÊÊI+˜@Çž|PðÀ³'ÈúÕ5Uaµ(ÕÓ¦+`µ;œN¢Sð kì® ëOŸQ£X¬à‡0³fO ´¾ xìÉäKȼhýD<ðP8JKK4múÔ`ELŒµ¦¦ãû$ú2Æü‹Ï©@¾»pþ™€s(›}7œn¢SðࡇÊHöÅ 5ο`–JËJÂj §Œ‘,œ´Lkss Å6Y?kÑå‡Õ Oͽ`Žªª^`´©ÐçŠ%=´Æ%—ÍSyàëBù*7ºòªKƒòlàcN>(Špÿý+Þ‘‚]’‘HÄuÝG‡Ô ß\vùŪœ29h™¬1©§Âè'jE$ÉÈü$hóçÎÒE æ†Ñ L©®Ô⥠×±2¿«­­=BK‘+š‹yÿ$É­sÝõK4¥º2„Žù •JêSŸ¾NñxðCž±úQ-å…¢ ÷Ý÷À~IÏ­“HÄuÓ-׫b÷€BÇôé›?ªI•¡LûÞfL¢(>þ—Š(HRÌxÿF²òRÝrÛ šD€‚•HÄõ©›>¢é3«C©gd©­­ t ú|RTàþû|VÖ¾F­I•ºí ç^P€JËJtËí7jöy3Â*Ù‘J ® «X>(ª IÆÄÿï°j•••ê¶Ï\—Ÿ40AfΞ¦Ïßñ)Õ½áÏéŒVÝ}÷_ž¯`ôŒµ¿7—wÖ¯_óŒŒn³æ‘¦ýñõíêìì³, $©’”®ºú2]|é… :ÓïY'e—ß{ヲÏ7‰¨È…lÂ<ÏÚ­’Jê9{Î }þÎéÚWß [ëÕÝÝVi@©TR—^6O /¿X%%Éð7`ÍwŠíà/é'’TW·ö¿XÙÿš‹ÚÖZ556kýa5nQ&“ÍÅf#0ÆhÆÌÍ¿è<Í7GÉdÎþŸ}fYíÊÏçªx”ŠòIš:õØÿhkŸöEIׇ]Û£óΟ¥óΟ¥lÖª­µ]ÇZÚÔÙÑ­®Î f”Éd”L‡½ipJ"W"‘P*™TÅä2UVNVõ´*͘Y“˃¾$ÉJÇ1ïþœn$BEû €$­]»vnnLêojkk;¢n&J΀SV?±zN*cþÁJu/€œxKV¸ü_ÿégyì±uŸö<ïïdtsÔ½B±CÖþ?G¶þ䡇â àï#Œ ®nÍõÖè?Ëê¹wµ>£ÍVú‡æ¦ãOqàÿ0À(V?±zN2«»$s—¬>u?€sj´Ò¿Æ¤ÖÖ®|+êfò` êêV_&™[$Ýd¥O‹K j½2úƒ¤Mžñ6µ4¶mæ¿}ãgÖ>¾öâDÖ,´Ö[(£¿“TuêÉI“&Éa{áéëëS&“9ýGG%õFÔ·£”µ:ÿÔãx<¦òò⸠ÃZ«îî&&3FÖZí°¥3š*«©§–”¤”J•DÙQhÒé´úûûOÿQ·¤–ˆÚÉ\IÉS***‹ÇYËï{ì›Vf{LÚm­ÙíÅÍî¸ßY[[›Ž¬ÉFÉúº5»$]zêñ¿ûòWTYYaGáyþùçt¸ñÐÐcktÇòûWþ¯[:Ãc­½Ê³öíS§N­Ö_º3Ê–B“ÍfõÏ?ùÑé?ê_V»²,ª~†³þ±5ÿMV{êñUK¯Ö•W.²¥ÐìÛW¯—ÿðÒi?1?^V»"¯®Z_·f§¤ËN=þòÿ^UUUçX£plÚô¼nzìûåoßÿÀ¯"l©¨GLcBÀADÀADÀADÀADÀA‰¨(V{öìVII°ùÐ++«4wîÜqßS ³³S‡•ÍfõÑÕÝhý‰Ößß§mÛ¶ª‹Å4kÖ,UW׌»FÓ‘FµµµIfÛöláÝÓ¤åXsàñO¥RºðÂyãþÊdÒÚà€ΜÇÌÚÚÛ­…={w«´¤4PÀûž®.5>|ßÓÕh}œ G¶ïØJÅW,Ñ5×\;æõºººôôo~¥t:3úÂE¦¯¯Oo½ýÇÀub±˜nÿìç4}úŒ1¯»k×{z}ók{(DMMMjjj \gÇÎíºãK_V<óºÏ=ÿ¬š››÷PˆvìØJ+._¬k¯½nÌëuuuéé§ŸrrßSh8žœ¼ÛŒk½¦#M9ü´yvç­Üôãyž>4ú‚Ãhh8r7Còn¯j¬ÉIO:q¢}Ìë äîàomÞ¿”›žï=|äHcÎö= )Ïö=…#û¿sQ7ëïóc/àGoçЖŒ¥ßÈUññ˜2åøIûrQÛóÆ÷|ÖËÑø=“›ÂãçyÞ³’rò güs6ö’lL9ù;ÂÊäfß“ß{ßç>ˇöÁA³9WÅ]Ä)€9Òúgͪٮ˜¹$HkUi¤o‡Õ×iÞ”ÑsA kú2qýó²o}'¯NŒÞu×Cƒ«ŸXýÉTÆÜm‚݃ÙÓGdtsH­}ÀêŸÓá@5<Ûh”z<¤ŽB³|ùX·aÕMq/~»56Ø>ÅêÏ$NgCºe´&POžé•|º ö)ÍGŽÿ§™s¦í0§ÝŽ|\<;YƬ©­!Fú£5z6P kú¬ÍüdåÊ¿h «/BóÐCe$Þ9×Õ}ÿ«x.ÀËËî_ùŸrP7/ Adwaita Icon Template image/svg+xml GNOME Design Team Adwaita Icon Template fwupd firmwareupdater fwupd-2.0.10/data/pki/000077500000000000000000000000001501337203100144065ustar00rootroot00000000000000fwupd-2.0.10/data/pki/GPG-KEY-Linux-Foundation-Firmware000066400000000000000000000041711501337203100223520ustar00rootroot00000000000000-----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v1 mQENBFsDBO0BCACjkrMuRgaWxP88Ubc1Xar5mxLMNXAJUzeYQVh/LnkEwytO3Ekh GDH8Ch78269MiezkJmUGUUGyjKhqZECtZaKGp4LSl6gTPFDFHS/xKaq8L+8G/v4K LEtZE03PKSnY2XYnf+3Kc6tmIZBB67yRg/79p3OpFd95wqyu+2c1cVkjCA1Q8XpO bgCDfNacU3Yag6GXYlKpLmlVkYaAptjV0FrbLLBjaHvFeGAXgRUlv0PRyDjKD2XT PEBtbg2+qTxPJIOlFgGNsJjkFL7R3mWwn00yF4jt9JMYGkpNAuFg1c/TZ1v64wlP N6i2DsDwIMQ9S/ahJWdX/zP4JMTdpYNP91o9ABEBAAG0WkxWRlMgYSBTZXJpZXMg b2YgTEYgUHJvamVjdHMsIExMQyAoTFZGUyBhIFNlcmllcyBvZiBMRiBQcm9qZWN0 cywgTExDKSA8ZmlybXdhcmVAZnd1cGQub3JnPokBOAQTAQIAIgUCWwME7QIbAwYL CQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQxjYXh6CoSeE3pQf+MlPyQzkVhdRU fqjzu3Ba9dZ+hmsol2ooFmytd058AIO1eKie2LxnkQw4P5prFOnVWbbFi79vUEzQ KK5xpW46nEglU14xYgv+4cMlVNWBYIVsXIIKKs2z4gM8oCA20JpVunnVbaViWTna YwiUbTniIvLQH4RLo66Qzd0b3Z8ycK0bVbCl4RazSbWAGHMAnqm0xSQqsWRQwaHk vxXEbjb0hfO4P/PZui5vFGr82tZUdGVKift1JlOzDMjVcmFIuYITHQyGaZ5Xsh2h Pu9gI9Vp7OQoA7dJ+Qc5LBk3rVxN5Zx3jUSWOd7Dtvrm+ArBy/y0MCVyv9fcSvAH vEv9T4zhE7kBDQRbAwTtAQgAwjiRDT3qinYz7b1s9SM2Y7aZG9JhWi/Zp2qGzGVX QpVV2EK3PnAfZpyt99I63N7d/QDPqLFmTyjlv5cMb3QxVyXGBCGGz3OiWYY0NaDd s1sp3J+TT6bHNG/Lo+vgcTRvzHvq7HbbeNssIMLr6MDAj0fZSh5UlAfQdC3qz90A qIPGcx5AwgCwXLDqzCusz17Erc3IK/TG4r0AbRFtGx0hl2w5EOn7funw8BhnJ59w OMsq7sXDmFff4hQjgQoDezMkA1EgzFokRY7pToLG3X1KdDXKR0edQ3+1mlJTf9XN Zaz+ortKsugmTmzsF4DlRhq0Ok0VWuq0rzWndpFyFvIewwARAQABiQI+BBgBAgAJ BQJbAwTtAhsuASkJEMY2F4egqEnhwF0gBBkBAgAGBQJbAwTtAAoJENTAcNuxNA6+ 1T4H/jsWVrANLKElBwZJpPOVy4Haw7UG+zk7lfwck0H8J9ShDtwhNTg03BiOONP/ JuR8XvOxjqdUexEmAJdQCtxJgLTlI40xSlcmSEIneCamOhA/I/T+nkXFAfV65FKF +OR3Ee122sUMXvLCcNvcbM23GIWiN/YmYFlK1PGNe3oOn4MWJ/28dbCLuEOPP4Tu VJM/RpZ65qCnojc1meMcPJxI5iNWZtG9SmmGWDI3f7mDK+dtD06VLmPd3uc8P23t YN0o/Jkgz2oV5GKD9t2+Ne2C5H5xZ0aE7dDVM8ErEPd3wTS+bC8GhPxHjj4A6HyA MNZAEZSAJ4TVpzbfyOMcpSRK4yVLTAgAmTVV79EH/14s60Ya0LCtpifpvpZimbbo xBGFaymvX7doxZyITC66JNTzT4Eixp8FNRloKWkEo6gPwA6qshlhc0HpmiqNmC+k QNYIVeanrflV2bVzYSdsIzrZLUTd6P835YYxD1nsQGwnCqeeD0gJlV+alo0LYTRt lFwNYxHU7BM09wu7sUEvYW5wt4TXPUrZ9jV+BM9UQLatW+S5vO41wqfTmPKGEqct doW2ZYUgCc4aFGTOj3fA5hoK6EjAQpVdkcA7fiRYLn5AIvM4FGxIqI5khjsZUEPa 9Gpui69Y4a+x4QEIDj/WAHOOMSIg/n96e+uRdGXN4c8nr7JSASxlow== =RFA4 -----END PGP PUBLIC KEY BLOCK----- fwupd-2.0.10/data/pki/GPG-KEY-Linux-Foundation-Metadata000066400000000000000000000041711501337203100223160ustar00rootroot00000000000000-----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v1 mQENBFsDA8IBCACgSd0NAJFEUjqcyv38If9f/FCQi2C2MQbzNt05DblHAg6eBk/V eYM/GI+Cr9sPwxs8ZWtN0IRoQp/d7MRxe43zFT4IH2N4RVaBTgWCoRerPn09k4K/ 2fk6GWIY8lgxlKV/LinM5XkFDXv6Zf/o8Nv/i9bVO9Dv1bVh1ThgA3xy8WIzUQge cVviEjEYG10TX+NENGgdA+aD/fMk4Wzwz6L48D+ryTiXGFnwoizifr9DIn4yIp6i b4vTQY96VoXHSgU6JRvYjzPPME+NmmcLgW0hGJlVvi8RL+7wJPVeS0ioqPzMtonS evrwVv5E5k87i+LS/vdVu3SIUzR9JLIXtvNNABEBAAG0WkxWRlMgYSBTZXJpZXMg b2YgTEYgUHJvamVjdHMsIExMQyAoTFZGUyBhIFNlcmllcyBvZiBMRiBQcm9qZWN0 cywgTExDKSA8bWV0YWRhdGFAZnd1cGQub3JnPokBOAQTAQIAIgUCWwMDwgIbAwYL CQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQCm3O9rRvPb/wsQf7BGEkgT08Bx1v 657l//B/yniB7VGH+4plrX2OkWVzDxn+z6APcgqDMnzwatddxM7J6mm4yxA0nFKs D5BueTItYv5zQCzX8e4TM1oaUWBr8nACK7/WSxNIC+GRUKl68v+dIbp1bhdmAYzj wTn/uDW47Z7gtQYDJJit2MsKfu5Lwar7w+divH+6KWdaMctm2injYYIlpKCjffl8 RZ4PgX7lN5C0s9kWDkH9iI2i5aqJaI8gZHK6EKwitmsHMJBczekymlXh4MheYCKm IWJLh8tvK6LaVoddwfle4orhq4b7doA57H8BgJDDz+MxyjZn+GAireCMaJjtCSWv q5bnsdnMH7kBDQRbAwPCAQgAq6hTejZZcIXnfA4Z/1c03USKnK8SeG/yqlggvpyZ 9C3hAvJITtQ7iZmz0VKOjwQtds52qnZYbOn/Fr+4Ef+cbFZRNxamZ4kf0XSAVXSv EODMFj+BpdoDwAWhJcvyijoMV6N+gbCG+UedMNnpe25tlCRjouBSEF5KWJabejdh iZ8ikN+HNJa5uQ68F76aDP1BOE0XiLrOZC0MZ7mvbyOi9LPXFyV2EuVj+gt7r+2x OlSMmor7RTEAJcBK9tiew5LhNHeaqbe3xnOcpWrAaoVdIed7h5YbbetTFMWHCPGJ raGGRSv3OrZDfQXZvOi+k6I6wWEQrsUCIiVKPefU+5xSpwARAQABiQI+BBgBAgAJ BQJbAwPCAhsuASkJEAptzva0bz2/wF0gBBkBAgAGBQJbAwPCAAoJEL4e3StH4Ita hvAIAIlZlrAJrbj7aQ3VkFcTJJC+68BaGNqte2S3Zv7ONLjT1kc0xSflf4c4MHef q7WmLLsjUHocD0a8SUsR1V/Fp36qG1Yr7mfhf7dY3TwwUw9VXQoEdpEWZES/IJ4d oPXSowk8eSbb72g4dvt9p+wlDKlsT7YHjmfn9Zct5FqcQ2kV9+900DtWlvPK8hRR N0FibLR9GMorSFfHotFQ8AjdiXQUo+6GTb2HDJ1+aI4fpYo8NnqUs0wvCVtxrqn9 JBzSgKkFAjHOiz/C6a0JOBtmH6tL41U7ZtUI2idQWsHiufyCTVOHRbyHoOxxFEpX Xu3l2vckZaENNyNOGCqrIo8npFQczAf/REhK0Z8UumttRm2CQkJnkqRgLnIoJq3i l7ljKjFi06XLJxOjLLpIFBH/yAJ5YbsYZt+XuT3WgORfHPDU3xepWGfQj4QFcsny GWStnu6Ej/PogJyyvZ1hFpKu8g2cIp04vEebWeYHAMorvwty/p+MJnH6NeSQq5Pe n22AuKKENtgYwulgJH0VQ0lJ5k1CcFuEuZqnXk5CUIC4tStdUS6hgn+vEkaJ5dX8 nj/X9etEj2nnQGIL+7Dh2Z+UGaZqxSa1tjF5+7uC85K0NTq6Cpc+Bnd7Gqb+afnn /iFHG21+hpjQmdSvpbGTabrM9gxd0iuDFXSFSttMtSC+gR5VcNQpUA== =7Jrd -----END PGP PUBLIC KEY BLOCK----- fwupd-2.0.10/data/pki/GPG-KEY-Linux-Vendor-Firmware-Service000066400000000000000000000016771501337203100231070ustar00rootroot00000000000000-----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v2 mQENBFWt/98BCADZ4+lUHSp4OMlzVf4HlJNLJ7Ks5QxGwL/hy2wChoNLuA/j4GNM 9mBZutKynYmphD0Mi4XjXn7JNXyuJa8Qutz98/Iyhsjq4LeiL9ayaKMXT+3pKlTm Gd/Fzo3QEOqTJ5s2RamrfwFIVuvwoj+rNmzj5fUCgoDOZeqVl6gxb7ZPzL8sWTOU iLeGMSzZBGE0ioJ82PZzsHelrrObDP1mMre1jQ6zxLlnYUlLvtJpydAfeBxU+6yL fgPeoFeuCE6JIszyWuyAgpBpYSGgj1bpt9Sxc2+MoZ0BjDzoijZqt4O48gYuEaLf iqYzQybe1JF0McO4C0dmjdKQz2qm0XrQyNhVABEBAAG0LkxpbnV4IFZlbmRvciBG aXJtd2FyZSBTZXJ2aWNlIDxzaWduQGZ3dXBkLm9yZz6JATcEEwEIACEFAlWt/98C GwMFCwkIBwIGFQgJCgsCBBYCAwECHgECF4AACgkQSKbYDkU4usJjjQgAzmTcA8qH s+1kieEZvsUzH4wun2Hlz7R5FRc/7BijgIQAA9TTrJnwbJmEBzEvHv7FKQLiBN3a 0lQIZgahmcUt1qm6VW94VAio+SDCdqTx73wUsgM3t9sAwKxkEdJQQoO8PqYHV3uK rq0t2YjXglIBHRDiJlOTAR3if37OCDKCcHOOODqYrsN7wNleez+ulkDyP7C7ZTbm /A7Xec73t2OQUnejU0uvRvc7VSnQDRFBHA9TPiBhbruMw+ZX+z/wfPd7x2RCqoOE vHh+QofE41Ya2QOkT96fAKfcJ+gvIbmwp3w7h+Hus1h3xDrykCG9cCxuH0HxooVI XL3IlFx/6OUpBA== =6Dz2 -----END PGP PUBLIC KEY BLOCK----- fwupd-2.0.10/data/pki/LVFS-CA.pem000066400000000000000000000032171501337203100161470ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIEqjCCAxKgAwIBAgIBATANBgkqhkiG9w0BAQsFADA6MRAwDgYDVQQDEwdMVkZT IENBMSYwJAYDVQQKEx1MaW51eCBWZW5kb3IgRmlybXdhcmUgUHJvamVjdDAeFw0x NzA4MDEwMDAwMDBaFw00NzA4MDEwMDAwMDBaMDoxEDAOBgNVBAMTB0xWRlMgQ0Ex JjAkBgNVBAoTHUxpbnV4IFZlbmRvciBGaXJtd2FyZSBQcm9qZWN0MIIBojANBgkq hkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAtfUXH3NwDJzWyhkPyPcFI899+tPZ/SMp OkDtRr9dJjgQkSO9jKCue4DVq8Bd9RcL76F7XnEKG0LiuKnr+D7+x86TtDAPCbkP WAS7fAaetLtiNFU96cokhjeALB3hyamkMQnCw+5Ov+sHJfGI9Bor9UaIIbIB4r8v oU1WpE7N6Ix2qsS5b88+Z6EIV6CX8RbciOC/TfyYVnpF1cd4l7LH7TtL+ERpsPwv rk0JgVoRzG3BT5yYfuxHIe4H4Axh95tW9i6urzyQkXRz14twwwcEDvl5ALrBLNJJ 8EDz9oR8HBPbxbd4i2dBfziY7TW4o/VgZKTGWA39JfwWNc5RxaYzBhBmg5nRcVFs E7PlovhyFH/0RNm/3E6vZQCeM+FNps0ovVq8Yqg8whL/yZ0iNlavCGTWhaxisVHG 7mQopV4jZlafxvrcBFzK8RPe8Gi04FFn4ugZtJnOuMel+AiADhgtWZCENiyWV+V7 WF1SFF4HaHuS8qqna/p9lrpVq6TBr0WRAgMBAAGjgbowgbcwEgYDVR0TAQH/BAgw BgEB/wIBATAwBgNVHREEKTAnhhVodHRwOi8vd3d3LmZ3dXBkLm9yZy+BDnNpZ25A Znd1cGQub3JnMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA8GA1UdDwEB/wQFAwMHBgAw HQYDVR0OBBYEFLGN6uQjp34JjrXuMeBq3Z40N2WsMCoGA1UdHwQjMCEwH6AdoBuG GWh0dHA6Ly93d3cuZnd1cGQub3JnL3BraS8wDQYJKoZIhvcNAQELBQADggGBABNK mC4AcqsBCVRGpwJeUymh5G6uUpzkoEDw+y9TEoWzfldV0epU7ruqI2p8B8YshDK6 +D4CFmCnW8cc+Jb6jrJ2ZcjUqWE/c+uwZhwsUHNdk6ummPPKfMhRSbduk1ngdQe5 meIgWGkoCfJ48GUAVVD6MlrMTNFsot1GN9x3ALMqhSU49+X43yikcc9WY2F8JOY8 xYpGpgUQV1hBSPOGK4XhgztpFLqw0GxJiLrOfKjtJwSTkxGCpPi2dLS0huk/mreT NAQ5FnMLkoqfR1RGga3tiP5w13gqDBV7a6MYMdmMfAAZhfRtlDu6SiAmjEmlSkOK PNhdoCNVDQLQpGaKZUI5hjMfR90U8Cm/6e0ondwjV4J6f4CS4wkQ5zzITGWptagE 01tpgTXf7TLaFGtzR8cl8XgV+UO3T4DQjEQkXUaS7n72ZCGv/s4LraLunhBrVHSq glEXpU/V/JNptgArIiRFZOrto52cUnnlNEfgqIzAHv/LMFRIkMo8ZMGTgScFrA== -----END CERTIFICATE----- fwupd-2.0.10/data/pki/meson.build000066400000000000000000000015271501337203100165550ustar00rootroot00000000000000# only install files that are going to be used supported_gpg = libjcat.get_variable(pkgconfig: 'supported_gpg', default_value: '1') == '1' supported_pkcs7 = libjcat.get_variable(pkgconfig: 'supported_pkcs7', default_value: '1') == '1' or host_machine.system() == 'windows' if supported_gpg install_data([ 'GPG-KEY-Linux-Foundation-Firmware', 'GPG-KEY-Linux-Vendor-Firmware-Service', ], install_dir: join_paths(sysconfdir, 'pki', 'fwupd') ) install_data([ 'GPG-KEY-Linux-Foundation-Metadata', 'GPG-KEY-Linux-Vendor-Firmware-Service', ], install_dir: join_paths(sysconfdir, 'pki', 'fwupd-metadata') ) endif if supported_pkcs7 install_data([ 'LVFS-CA.pem', ], install_dir: join_paths(sysconfdir, 'pki', 'fwupd') ) install_data([ 'LVFS-CA.pem', ], install_dir: join_paths(sysconfdir, 'pki', 'fwupd-metadata') ) endif fwupd-2.0.10/data/power.quirk000066400000000000000000000002451501337203100160350ustar00rootroot00000000000000#This file provides manufacturer specified minimum battery thresholds [LENOVO] BatteryThreshold = 25 [Star Labs] BatteryThreshold = 30 [HP] BatteryThreshold = 50 fwupd-2.0.10/data/remotes.d/000077500000000000000000000000001501337203100155235ustar00rootroot00000000000000fwupd-2.0.10/data/remotes.d/README.md000066400000000000000000000054701501337203100170100ustar00rootroot00000000000000# Remotes ## Vendor Firmware These are the steps to add vendor firmware that is installed as part of an embedded image such as an OSTree or ChromeOS image: * Compile with `-Dvendor_metadata=true` to install `/etc/fwupd/remotes.d/vendor.conf` * Change `/etc/fwupd/remotes.d/vendor.conf` to have `Enabled=true` * Change `/etc/fwupd/remotes.d/vendor.conf` to have the correct `Title` * Deploy the firmware to `/usr/share/fwupd/remotes.d/vendor/firmware` * Deploy the metadata to `/usr/share/fwupd/remotes.d/vendor/vendor.xml.gz` The metadata should be of the form: FIXME.firmware FIXME FIXME FIXME FIXME

    FIXME

    http://FIXME 86406 firmware/FIXME.cab 96a92915c9ebaf3dd232cfc7dcc41c1c6f942877

    FIXME.

    FIXME
    The metadata and firmware should be signed using Jcat, ensuring the signing certificate is installed in the `/etc/pki/fwupd` location. ## Automatic metadata generation `fwupd` and `fwupdtool` support automatically generating metadata for a remote by configuring it to be a *directory* type. This is very convenient if you want to dynamically add firmware from multiple packages while generating the image but there are a few deficiencies: * There will be a performance impact of starting the daemon or tool measured by O(# CAB files) * It's not possible to verify metadata signature and any file validation should be part of the image validation. To enable this: * Change `/etc/fwupd/remotes.d/vendor-directory.conf` to have `Enabled=true` * Change `/etc/fwupd/remotes.d/vendor-directory.conf` to have the correct `Title` * Deploy the firmware to `/usr/share/fwupd/remotes.d/vendor/firmware` * Change `MetadataURI` to that of the directory (Eg `/usr/share/fwupd/remotes.d/vendor/`) ## Mirroring a Repository The upstream LVFS instance will output a relative URL for firmware files, e.g. `bar.cab` instead of an absolute URI location, e.g. `http://foo/bar.cab`. When setting up a mirror of the LVFS onto another CDN you just need to change the `MetadataURI` to your local mirror and firmware downloads will use the relative URI. fwupd-2.0.10/data/remotes.d/lvfs-testing.conf000066400000000000000000000007041501337203100210200ustar00rootroot00000000000000[fwupd Remote] # this remote provides metadata and firmware marked as 'testing' from the LVFS Enabled=false Title=Linux Vendor Firmware Service (testing) MetadataURI=https://cdn.fwupd.org/downloads/firmware-testing.xml.@compression@ PrivacyURI=https://lvfs.readthedocs.io/en/latest/privacy.html ReportURI=https://fwupd.org/lvfs/firmware/report FirmwareBaseURI=https://fwupd.org/downloads OrderBefore=lvfs AutomaticReports=false ApprovalRequired=false fwupd-2.0.10/data/remotes.d/lvfs-testing.metainfo.xml000066400000000000000000000027461501337203100225040ustar00rootroot00000000000000 org.freedesktop.fwupd.remotes.lvfs-testing Linux Vendor Firmware Service (testing firmware) CC0-1.0

    The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer.

    This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails.

    Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$.

    fwupd-2.0.10/data/remotes.d/lvfs.conf000066400000000000000000000007031501337203100173440ustar00rootroot00000000000000[fwupd Remote] # this remote provides metadata and firmware marked as 'stable' from the LVFS Enabled=@enabled@ Title=Linux Vendor Firmware Service MetadataURI=https://cdn.fwupd.org/downloads/firmware.xml.@compression@ ReportURI=https://fwupd.org/lvfs/firmware/report PrivacyURI=https://lvfs.readthedocs.io/en/latest/privacy.html FirmwareBaseURI=https://fwupd.org/downloads AutomaticReports=false AutomaticSecurityReports=false ApprovalRequired=false fwupd-2.0.10/data/remotes.d/lvfs.metainfo.xml000066400000000000000000000023221501337203100210170ustar00rootroot00000000000000 org.freedesktop.fwupd.remotes.lvfs Linux Vendor Firmware Service (stable firmware) CC0-1.0

    The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer.

    Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$.

    fwupd-2.0.10/data/remotes.d/meson.build000066400000000000000000000035611501337203100176720ustar00rootroot00000000000000if build_standalone and get_option('lvfs') != 'false' con3 = configuration_data() con3.set('compression', lvfs_metadata_format) if get_option('lvfs') == 'disabled' con3.set('enabled', 'false') else con3.set('enabled', 'true') endif configure_file( input: 'lvfs.conf', output: 'lvfs.conf', configuration: con3, install: true, install_dir: join_paths(sysconfdir, 'fwupd', 'remotes.d'), ) configure_file( input: 'lvfs-testing.conf', output: 'lvfs-testing.conf', configuration: con3, install: true, install_dir: join_paths(sysconfdir, 'fwupd', 'remotes.d'), ) i18n.merge_file( input: 'lvfs.metainfo.xml', output: 'org.freedesktop.fwupd.remotes.lvfs.metainfo.xml', type: 'xml', po_dir: join_paths(meson.project_source_root(), 'po'), data_dirs: join_paths(meson.project_source_root(), 'po'), install: true, install_dir: join_paths(get_option('datadir'), 'fwupd', 'metainfo') ) i18n.merge_file( input: 'lvfs-testing.metainfo.xml', output: 'org.freedesktop.fwupd.remotes.lvfs-testing.metainfo.xml', type: 'xml', po_dir: join_paths(meson.project_source_root(), 'po'), data_dirs: join_paths(meson.project_source_root(), 'po'), install: true, install_dir: join_paths(get_option('datadir'), 'fwupd', 'metainfo') ) endif install_data('README.md', install_dir: join_paths(datadir, 'fwupd', 'remotes.d', 'vendor', 'firmware') ) # replace @datadir@ con2 = configuration_data() con2.set('datadir', datadir) configure_file( input: 'vendor.conf', output: 'vendor.conf', configuration: con2, install: get_option('vendor_metadata'), install_dir: join_paths(sysconfdir, 'fwupd', 'remotes.d'), ) configure_file( input: 'vendor-directory.conf', output: 'vendor-directory.conf', configuration: con2, install: true, install_dir: join_paths(sysconfdir, 'fwupd', 'remotes.d'), ) fwupd-2.0.10/data/remotes.d/vendor-directory.conf000066400000000000000000000004301501337203100216660ustar00rootroot00000000000000[fwupd Remote] # this remote provides dynamically generated metadata shipped by the OS vendor and can # be found in @datadir@/fwupd/remotes.d/vendor/firmware Enabled=true Title=Vendor (Automatic) MetadataURI=file://@datadir@/fwupd/remotes.d/vendor/firmware ApprovalRequired=false fwupd-2.0.10/data/remotes.d/vendor.conf000066400000000000000000000007531501337203100176740ustar00rootroot00000000000000[fwupd Remote] # this remote provides metadata shipped by the OS vendor and can be found in # @datadir@/fwupd/remotes.d/vendor and firmware in @datadir@/fwupd/remotes.d/vendor/firmware Enabled=false Title=Vendor # NOTE: never point MetadataURI to a NFS or SMB *directory* otherwise all the archives are copied # locally where metadata is auto-generated by the daemon -- point to the `.xml.gz` instead. MetadataURI=file://@datadir@/fwupd/remotes.d/vendor/vendor.xml.gz ApprovalRequired=false fwupd-2.0.10/data/tests/000077500000000000000000000000001501337203100147655ustar00rootroot00000000000000fwupd-2.0.10/data/tests/README.md000066400000000000000000000056011501337203100162460ustar00rootroot00000000000000# Installed tests A test suite that can be used to interact with a fake device is installed when configured with `-Dbuild=all` and `-Dtests=true`. The test files have been signed by the production LVFS instance, and are available here: * * By default this test suite is disabled. ## Enabling To enable the test suite: ```shell fwupdtool enable-test-devices ``` ## Using test suite When the daemon is started with the test suite enabled a fake webcam device will be created with a pending update. ```text Integrated Webcamâ„¢ DeviceId: 08d460be0f1f9f128413f816022a6439e0078018 Guid: b585990a-003e-5270-89d5-3705a17f9a43 Summary: A fake webcam Plugin: test Flags: updatable|supported|registered Vendor: ACME Corp. VendorId: USB:0x046D Version: 1.2.2 VersionLowest: 1.2.0 VersionBootloader: 0.1.2 Icon: preferences-desktop-keyboard Created: 2018-11-29 ``` ## Upgrading This can be upgraded to a firmware version `1.2.4` by using `fwupdmgr update` or any fwupd frontend. ```shell $ fwupdmgr get-updates Integrated Webcamâ„¢ has firmware updates: GUID: b585990a-003e-5270-89d5-3705a17f9a43 ID: fakedevice.firmware Update Version: 1.2.4 Update Name: FakeDevice Firmware Update Summary: Firmware for the ACME Corp Integrated Webcam Update Remote ID: fwupd-tests Update Checksum: SHA1(fc0aabcf98bf3546c91270f2941f0acd0395dd79) Update Location: ./fakedevice124.cab Update Description: Fixes another bug with the flux capacitor to prevent time going backwards. $ fwupdmgr update Decompressing… [***************************************] Authenticating… [***************************************] Updating Integrated Webcam™… ] Verifying… [***************************************] Less than one minute remaining… ``` ## Downgrading It can also be downgraded to firmware version `1.2.3`. ```shell $ fwupdmgr downgrade Choose a device: 0. Cancel 1. 08d460be0f1f9f128413f816022a6439e0078018 (Integrated Webcamâ„¢) 2. 8a21cacfb0a8d2b30c5ee9290eb71db021619f8b (XPS 13 9370 System Firmware) 3. d10c5f0ed12c6dc773f596b8ac51f8ace4355380 (XPS 13 9370 Thunderbolt Controller) 1 Decompressing… [***************************************] Authenticating… [***************************************] Downgrading Integrated Webcam™… \ ] Verifying… [***************************************] Less than one minute remaining… fwupd-2.0.10/data/tests/build-certs.py000077500000000000000000000116331501337203100175630ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: LGPL-2.1+ import os import sys import subprocess import tempfile from datetime import datetime, timedelta, UTC def _build_certs(path: str): # expire in 7 days to avoid people using these in production dt_activation = (datetime.now(UTC) - timedelta(days=1)).isoformat() dt_expiration = (datetime.now(UTC) + timedelta(days=7)).isoformat() # certificate authority ca = "fwupd" ca_privkey = os.path.join(path, f"{ca}-CA.key") ca_certificate = os.path.join(path, f"{ca}-CA.pem") if not os.path.exists(ca_privkey): print("generating private key...") argv = ["certtool", "--generate-privkey", "--outfile", ca_privkey] rc = subprocess.run(argv, check=True) if rc.returncode != 0: return 1 if not os.path.exists(ca_certificate): print("generating self-signed certificate...") # build config lines = [] lines.append('organization = "fwupd"') lines.append('cn = "fwupd CA"') lines.append('uri = "http://fwupd.org/"') lines.append('email = "admin@fwupd.org"') lines.append('crl_dist_points = "http://fwupd.org/pki/"') lines.append("serial = 1") lines.append("crl_number = 1") lines.append("path_len = 1") lines.append('activation_date = "{}"'.format(dt_activation)) lines.append('expiration_date = "{}"'.format(dt_expiration)) lines.append("ca") lines.append("cert_signing_key") lines.append("crl_signing_key") lines.append("code_signing_key") cfg = tempfile.NamedTemporaryFile( mode="w", prefix="cert_", suffix=".cfg", dir=None, delete=True ) cfg.write("\n".join(lines)) cfg.flush() argv = [ "certtool", "--generate-self-signed", "--load-privkey", ca_privkey, "--template", cfg.name, "--outfile", ca_certificate, ] rc = subprocess.run(argv, check=True) if rc.returncode != 0: return 1 # per-user key user = "testuser" user_privkey = os.path.join(path, f"{user}.key") user_certificate = os.path.join(path, f"{user}.pem") user_certificate_signed = os.path.join(path, f"{user}_signed.pem") user_request = os.path.join(path, f"{user}.csr") # build config lines = [] lines.append('cn = "Test Key"') lines.append('uri = "https://fwupd.org/testuser"') lines.append('email = "testuser@fwupd.org"') lines.append('activation_date = "{}"'.format(dt_activation)) lines.append('expiration_date = "{}"'.format(dt_expiration)) lines.append("signing_key") lines.append("code_signing_key") cfg = tempfile.NamedTemporaryFile( mode="w", prefix="cert_", suffix=".cfg", dir=None, delete=True ) cfg.write("\n".join(lines)) cfg.flush() if not os.path.exists(user_privkey): print("generating user private key...") argv = [ "certtool", "--generate-privkey", "--rsa", "--bits", "2048", "--outfile", user_privkey, ] rc = subprocess.run(argv, check=True) if rc.returncode != 0: return 1 if not os.path.exists(user_certificate): print("generating self-signed certificate...") argv = [ "certtool", "--generate-self-signed", "--rsa", "--bits", "2048", "--load-privkey", user_privkey, "--template", cfg.name, "--outfile", user_certificate, ] rc = subprocess.run(argv, check=True) if rc.returncode != 0: return 1 if not os.path.exists(user_request): print("generating certificate...") argv = [ "certtool", "--generate-request", "--load-privkey", user_privkey, "--template", cfg.name, "--outfile", user_request, ] rc = subprocess.run(argv, check=True) if rc.returncode != 0: return 1 # sign the user if not os.path.exists(user_certificate_signed): print("generating CA-signed certificate...") argv = [ "certtool", "--generate-certificate", "--rsa", "--bits", "2048", "--load-request", user_request, "--load-ca-certificate", ca_certificate, "--load-ca-privkey", ca_privkey, "--template", cfg.name, "--outfile", user_certificate_signed, ] rc = subprocess.run(argv, check=True) if rc.returncode != 0: return 1 # success return 0 if __name__ == "__main__": sys.exit(_build_certs(sys.argv[1])) fwupd-2.0.10/data/tests/fakedevice123.bin000066400000000000000000000000121501337203100177640ustar00rootroot000000000000000x1020003fwupd-2.0.10/data/tests/fakedevice123.jcat000066400000000000000000000055701501337203100201530ustar00rootroot00000000000000‹aå¬eÿíWWÏãÊ‘ý+ƒy¥m1“2pš9ˆ)&‘+?0ç QŒÆýïæ÷Íxqç®w¯ ,A‚¤bUuw…ÓuþúU‰Ã·›¾Æ²ï´°ê__ÿüþ×Äe÷)F±üNÛñøýý*'Ç÷×,¬Ó$Ë8EPìOQÙ}=´˜¦¾k©e÷¡‡R¡ óñ»»lÓñ¶ÃÇ &hôL¡çã¾Ã¿Qch˜Â c•ÁIÑ)™R4Ñ8†')ÃxŠ#_ý×ÿ\ùgWIè3Œg)Å(’Ò$Ÿ©$ÃS R§±ó9 c:¥Ò”ˆ³„$ˆ&p2†)ŠLÒ ÈVGÿÙÕÿøñbxQÖ¿¢ñÅ’EØÎÿ”?ºGWšüÎÀ°$BÉóâ Ï_aÙÔ« º[jäsµƒO£,€Ö l^›—?ºiT äàyšÊ´¯ó.«Š‰ã·2’AÞbôíÌ„sö'©óšƒ*£„ýy3–„+»öÑQ4T±…kJäÙaB®"€^' õ´€9§bάHŒ+F2ªmÜízR ])ÖéÕA¤Úò£³‚‘-\hO ©È¨ ù‹S7Ú†ðJ9ÉÛ…¤/òÓ}šªÎêÆeƒ6RKöm?:BHM8“‡á‚AMyi,¨ÝßÌñ¥rë#~]˜eZèËÛD°å ¿çš&Ç/‰æÖ-ó:£Yþ¢€O¹Ê¯ˆz’²ŒxDarþènm³E÷¤ˆ['w$e[·J8 ¼^9°j¶¿h8>LxȶßËŽ80õ*ìÀerÝe€fs2øhqx4sKr»Pt÷#VŒü»Ýñ¸²§ Á‡›«Q¯Ñ©ˆAÁõ8bœ/ö8”®%ºñ@–™.‘ª®Ì÷qƒóû£ƒŒ“{kFéRªæK˜“#ãÊmoa†{h;ÁZ]*ë|Ê|lã4Zôa´Ò»`Z¢$“ÛþèªÐÕSè¦ë¸7GÕ%K;º£Kqp-NþŒ×j<}2#²ó/Ër´®J'ztIdó† a8ºXÄ”÷¾.Äð’WçÄêütDÉem œøMÄ¡iˆdD‡Û£ðÝh¶÷D×–ÍXcÊÃZzµ ÉMòž¼´ìÉ¡ž&ç Çm`é]»o s–Œú9zWI¯#A«;F…AOÔ;}1Nl—â$zÆ€!ÈóB ÝnÊ0ÓãR¬ÉR Eˆ“ð TÔi1Y €E_Žª¹Á7æÈó¥÷ï¡tƒcI#/Û¹M0w XbŽÛxޤ 7ˆá±oY?Z¶½ð#ï’>QE(¼ðß¼5 X6÷euñÆt¤£†îÛ3˜Ò‰wÔÃ’‚£ª“Š’LœrÓÑâWÇy5Æiˆ¨°OE¥ølqÎ@Î"}óP1L¾=pÝècO‚½vYÞ^ß)¾ââ+é* ÂeQóOíŒaµE¶AÖ3h.â~ðyB®Ÿ/X‚Ù܈΋ïá²ÿ¾‚ÇÏ >ªþÎ\ßn„<ËÄS\$V]“¤¢éítW.š:Œ ÝÒ0Ã(¦[© ÅLTÓ³Ý*-»:< ©Ãî#^aFƒŸI íc”Ç%“•xq.°»OÁ/áU85À¨K« ®0gù’ém¼êrýˆC5efx©¤×%²0»‡Y¦‘žûùÒÄ€×avœ‹~Îm[ „(sÇ"Of Æöèh¶‹œ;c›“& £ ï m¡wÌEÇ N(‡‡*SåÚIV¸——±¨œðvÂxÔ¡äѱ™ÌÇ) ¯ÝÈ—fNÛÅwµ!ºÐÞ¾Z߉„iêW*‘zFå@uûµ9'RXûãýèŽ;²½Œ£n²ùŠ£þÓl®ÜÞÈæÝ×>¸_p wc>rѾ2§yfAóB½=ÎÐn¬@yt½yÛEîÎÝJ_£ôZÝq˜ŽÈÛ ¸÷–¼»³u¶k\§˜ ­ÝIˆ>ã­uºWíU>."*TÖ>Ÿý 柘ííB±ùÉ?/Î1ÌÑëåX} S\¶ÞÃ}Ó|™ÁLIÿiË‘ÍêÓ(ýèþ NYóÚIT7e<í›y²ß¥F‘õXÑBŽ^!3hL.²Öó@KŽ0Îä°ó—ßV2st¨~{—Õ‚fÒwΖaã7Í„¶;Ç®n¿\–4yÍ’yY/MºÒrZtìƒñmn-®Í^2ñÉó^…}tØl¡€?wô}?G'9€9nÀûšÈVò*4²‰~ôŠwÖ»>H«RÀÑ6@e|Ìœ¼<{HB…¯W!ì7'ðº-V.“9_ÉÉTè•·(Ö¸‡èxDn‡jcYóýè¶àz .‘Kñ²+œ–Öɱ*Ê{Ç4D‡>O‚wd_*|ˆ¯„rYvû ãž-æ$åqð#ýÎïòÍ{^µÈŠŠ®¶8×õDï›øY°G‹_҄麋fÔA–Û>æ¢ÔafŸéa@k‹k”5ÎÜ´¸Ì®F°0&?<%(“b¤R¿† fÖ=ævCÀýú|ºÖ};ú‚?ËWA¢Ú …çʉ˴Ù¢"^{;sä,Þê–`Õn‘¥“ Ë/¿”~3Ì|ýõ/Ÿ3Ò?¢múË.ëÿ´¶Í¿‰g„8¦Ø™Nc‚ Q䜞aM±”FH8Fð¥C Iê_ãg„ B Ni"ÄÒ ;G)'çì`ŠÒqSé1šFhv0,Â"$%B„$q8JNÿ?àj›š9èŸ:qWT­.Î:1ÊÕ'm´ãE,f›„oÕ~´³´°uhÐ'ûì½üšFúVWNeuÑ哤Îðês‚AòQ‘æšÐsôG#ÜìF¢pïÔ159U!Û'«–é0H7ºáò «š&qVÞ5ŽïpKÈè¬È1Cj˜J+(µb×ùR±Ï)«“lKXú‡ÓT.k®å¥-¬Tž0ÆÂ›:Õ¦²Ã[sŠ­m–‚ ²‘òŒWDì”»íkà]î¼Z#- ˜„žÁ:(0;ͬ»E3v Õ­wæ»ÏCª:sŸß…Iµš5còOž7Ëù'ÏøÉ3~òŒŸ<ã'ÏøÉ3~òŒÿž¡î°BÑÄ\îÁùoªÖKxm祣®+ ­yyt°r–ÿ[ž¡TÇðŸ0°øl2‹Y‹ÞªõP üñÕèÓX+–ÈD\O/ÐcÎM‘qCÞ h’Âç\ÒR~×I/ÙkQ×ÔÜ zÀ³G°çìû".)Y‰z§j›§ ®+ªwG4íÍXZ•9 ì› ÊOð)Þ÷ßäÃ^;¸Î4ɹÍ #¯šâœ³©B›lý”¸:]ßœcb¢Š»gš›V<'REÏñ[qæíiÖ¦I㉕¥ƒ‘סµÙ”‹×°;q˜¨eúÆMQè™ÉçKA®$ÚK”ëÏà@û+[¿äˆÐ•ºlÙ.ð›&)ÌK™ê¶ì(ª¼ ñ‘ýÕØ¬‰¤ö?ñŒ¿üú7½Í¯˜IHATECDNfwupd-2.0.10/data/tests/fakedevice123.metainfo.xml000066400000000000000000000034351501337203100216310ustar00rootroot00000000000000 org.fwupd.fakedevice.firmware FakeDevice Firmware for the ACME Corp Integrated Webcam

    Updating the firmware on your webcam device improves performance and adds new features.

    b585990a-003e-5270-89d5-3705a17f9a43 http://www.acme.com/ CC0-1.0 GPL-2.0+ LVFS X-Device unsigned triplet com.acme.test bfc32ae0b003b7f0deb8e6e780b8434de7c04e41 d8904fe3bc21e867097df4e32be648399fac8e7ee5cfd65ab8546c0776def4b6

    Fixes a bug with the flux capacitor to avoid year 2038 overflow.

    https://github.com/fwupd/fwupd/tree/main/data/installed-tests fakedevice123.bin bfc32ae0b003b7f0deb8e6e780b8434de7c04e41 d8904fe3bc21e867097df4e32be648399fac8e7ee5cfd65ab8546c0776def4b6
    fwupd-2.0.10/data/tests/fakedevice124.bin000066400000000000000000000000121501337203100177650ustar00rootroot000000000000000x1020004fwupd-2.0.10/data/tests/fakedevice124.jcat000066400000000000000000000056061501337203100201540ustar00rootroot00000000000000‹iå¬eÿíWYӣȱý+ý*Ûb_1ž/„àÊìbG±9æ¿›¯»}£{l߈¹¶ßZ!!)©Ê¢2OÌó×ÏJM×ì5–}§GUÿúüçOÐ>ý`.»/fø0ËSÖŽÇïÿùëg9=¾?çQ¥Ù\&Œ`ŠËîó1ŠiúøÛ(µì>Æa‡Uh¢büæÇ-Ûlœ¢vøøOB8…Ð$J7¸hŠ>ü&H–“d„G1–%i†çpB— Í3šB(¦!8‰Óäó¯øô¿«À¿w’ÌÑ &ã(¥S†HŠ€Q”‚°<3‡0ŠÊ „Bp2É (!3¥Q*'cN°$JVG~ïêüx1¼(Ÿ,ÑúäÈ¢\ï±߻{WÚüÎÀ°¤BÉóa Ï›lÕvº9jpµ‡½GY­?â~Ø¢¶ÿn¿wïQ£g»Ð£Þ%¹^‰œ?í^-níÞ»hVñV™ÜÅ™hmâ6Zá Ù K’ËßOåF~·u—½dz1Äçg‡|š@°µ{B¼p‘Œ¼éRÖ)j¢nú)9ùcªH§¨ÞZF5;Üë¶v!Ƭӧ¢A`ë’epïøk"³duJÇð íuo ‡t €´j%oUýmÁa˜9öðÛ“# ¯(+0znîïi'í ÁB.+¯u Ж@Û'ÕCš‘ö/¿Ü»_x»ïÝ—ìñ÷Osú`Ð0*ëßœê²,zE ¨}(?æÄvÂö‡ÍÞY–©(Øüʹ@cЦxÔÚ: ~a—¯sîÝ·YÌ1‹7«c,E¡f=ÐÙ2Ë ÃëQŽelQTmAdPƒ)êç£.„CÌ‘"{ý®¸«msü’ê×:teÞ`t'XðÅ®ò+¬G>g,ã‡>^Ǩ\Ü»KÛlñ-}$­Wx’2GíµJ9 ´šXu7Xt&:lÛomG˜zvpe ãÊÝåeÇáÑ.éÚEâu?bÅÈ¿yú#>&û± |Œ` õ#jàuÁ»1ÑS8žz ¶h͇òêˆ×d Êܱª®Ì†·qƒŠÛ½;Yçë¥%­Tí—ðnlJŒ-“Û&a†úÓv†ôºTÖùœèƇH¼Ãèd7ÁvDI&¶ýÞUÑÕÈ öM5]ÇMY—,åž!%ÀNÈãÌX}R­g œì˜è|ÀË2›±ÞU™âÅ÷.-kÞ`$ŠÆ+Z„Ix*úÀè¦wf þ}Déʺ@9ó›ŠCÓÀ'‚=\neïÀ°Ûè®ÿDÖ–ÍYë]Dµôj}@›ä/<¡µìÙ#Ÿ6ç G5pŒ®Ý7†¡%«~ǘ«¤ƒ6NakxV…žžˆ'vÆbÙ.Ãä!ƒôBÝn˰ÓcR¢ËR¨ƒE°—ò  ê¼Ø¬Àb,j.Ð…9ò¬õÁ-’.P"é„¶ÑmŠ^—Åç¤MæX Ûd;1<ú5ë÷.i/ úÈ»d ¡ƒW1-üWo –…-Y]†±=éÀÐÂ}½g[:3Àã<,8P~ J²1^(lOO^ç×(§Ã¢Â>•äc°%sò髇ŠaŠEèwïÆ}â¬ÙåEkN¶bâ+í* £eQ‹/£s†ÕÙyÏ …ˆá—rý¬¡)zdsÃØg"Nƒ¶ÿÁダ1ætá§¥½yò8ÝÎ#uêš Ýh÷°3¹øÝ¡Lt-=µ¬ÇûR*ÈF2o²éÙn•–]ž¸Ôa^7°ÿ°£ÅÏ„„ô Âc’‹éˆH<{tÝßÅ/‘)Ü 3DI­U«0çÅ’müðÔÆ‡êç¨åg’1šKì n±L#=wZk@Šë°Æ;ÆÅ?®«†B…çg»k»w¹‚£Ñ-‚Ð÷–¾P»O¢g‘g„Ã"•© ý,+ÜKáËÄMUN˜¼(SzïØ\擎à×nK1çM ®úk”¿¯ŽC&7<ãäÃÔYZXDK'ûp»LÃVäÒΰoSw ìæ‘ìÏç%R ‹ˆNëëV³‹¿+OݲIí69Eû0½-˦ Šß…Šyì…þ8ÈOãØæ=μtFÙ0C­d†«_f$²H™/EY¼+X8õ³cFµA¿|ß)}×Ì|þõ/_z¤&/ÚlŠÊ.ïÿ´¶ÍHgÐiJFYGFÓ9L'TŽå ÑP~ôúiFai’&ýoêŒÃa ƒq*ð %Ñ(ÉbM)4¦â4N`:%󣉌Œ"‚Ji%h§±8Eé,ɿΠÿ»:£þ¡>+ŸýSô:sìÙÐö%©YW´tŠòžxI>“˜=®Þ-ãc~oFRöiÌ[÷®{+Ô`%¤ÙpázV=•ð^ïI÷"íɰãi~èÒcË610ª¸›Ì)u}hŠ“ä“q´§ŠGœ 첿°ë: |ͯîeI'oçúšqÂ2°c•°öx ø¥/Ãòü]éÅáýÑ0g5ou™±x…–Ç”ÎBxóPõ•[yÇÁçSGs˜“‘˜O&Fⲿ`õ3êbM„ˆ* Š{ã8xOD«õA5f©eÊTHODŸiGÂÙŸ[ëô¥æÍ\›§<«õïÝ,Ö3[ý1UÖë¡L®Ô¼}mí«g}ÕqzüÇuùSgüÔ?uÆOñSgüÔ?uÆw:þW:c^CÍ‘@¬ÀÄæx á3­Äݳ­B©áF»wNZÿZgt—¢‘û3~ $6𠯆O˜"®Hyq=ÅIppTæÖV¥Un$ ËÚEet-óeߣßý,:D)TŸOø¥ED[ »âÚœßfØ×Ãh|µ org.fwupd.fakedevice.firmware FakeDevice Firmware for the ACME Corp Integrated Webcam

    Updating the firmware on your webcam device improves performance and adds new features.

    b585990a-003e-5270-89d5-3705a17f9a43 http://www.acme.com/ CC0-1.0 GPL-2.0+ LVFS X-Device unsigned triplet com.acme.test c2ef77a5ab4ecde5f1c75f1e3fe983c0b901cbdc 77f3e17bad9d510786133804fc1e050488f628257ce00c7e23938f7b31c4cac2

    Fixes another bug with the flux capacitor to prevent time going backwards.

    https://github.com/fwupd/fwupd/tree/main/data/installed-tests fakedevice124.bin c2ef77a5ab4ecde5f1c75f1e3fe983c0b901cbdc 77f3e17bad9d510786133804fc1e050488f628257ce00c7e23938f7b31c4cac2
    fwupd-2.0.10/data/tests/fwupd-tests.xml000066400000000000000000000105421501337203100177760ustar00rootroot00000000000000 fakedevice.firmware FakeDevice Firmware Firmware for the ACME Corp Integrated Webcam ACME Corp GPL-2.0+

    Updating the firmware on your webcam device improves performance and adds new features.

    http://www.acme.com/ 17 1163 ./fakedevice124.cab fc0aabcf98bf3546c91270f2941f0acd0395dd79 2b8546ba805ad10bf8a2e5ad539d53f303812ba5

    Fixes another bug with the flux capacitor to prevent time going backwards.

    17 1153 ./fakedevice123.cab bc3c32f42cf33fe5aade64f999417251fd8208d3 7998cd212721e068b2411135e1f90d0ad436d730

    Fixes a bug with the flux capacitor to avoid year 2038 overflow.

    b585990a-003e-5270-89d5-3705a17f9a43
    com.hughski.ColorHug2.firmware ColorHug2 Firmware for the Hughski ColorHug2 Colorimeter Hughski Limited GPL-2.0+

    Updating the firmware on your ColorHug2 device improves performance and adds new features.

    http://www.hughski.com/ 16384 19592 hughski-colorhug2-2.0.7.cab 490be5c0b13ca4a3f169bf8bc682ba127b8f7b96 658851e6f27c4d87de19cd66b97b610d100efe09

    This release fixes prevents the firmware returning an error when the remote SHA1 hash was never sent.

    2082b5e0-7a64-478a-b1b2-e3404fab6dad
    com.hughski.ColorHug.firmware ColorHug Firmware for the Hughski ColorHug Colorimeter Hughski Limited GPL-2.0+

    Updating the firmware on your ColorHug device improves performance and adds new features.

    http://www.hughski.com/ 16384 18054 hughski-colorhug-1.2.6.cab 570a4259af0c7670f3883e84d2f4e6ff7de572c2 111784ffadfd5dd43f05655b266b5142230195b6

    This release fixes prevents the firmware returning an error when the remote SHA1 hash was never sent.

    40338ceb-b966-4eae-adae-9c32edfcc484
    fwupd-2.0.10/data/tests/fwupd.sh000077500000000000000000000027441501337203100164600ustar00rootroot00000000000000#!/bin/sh -e exec 0>/dev/null exec 2>&1 run_test() { if [ -f @installedtestsbindir@/$1 ]; then @installedtestsbindir@/$1 fi } run_device_tests() { if [ -n "$CI_NETWORK" ] && [ -d @devicetestdir@ ]; then for f in `grep --files-with-matches -r emulation- @devicetestdir@`; do echo "Emulating for $f" fwupdmgr device-emulate \ --download-retries=5 \ --no-unreported-check \ --no-remote-check \ --no-metadata-check \ --json \ "$f" done fi } run_umockdev_test() { INSPECTOR=@installedtestsdatadir@/unittest_inspector.py ARG=@installedtestsdatadir@/$1 if [ -f ${INSPECTOR} ] && [ -f ${ARG} ]; then TESTS=`${INSPECTOR} ${ARG}` for test in ${TESTS}; do ${ARG} ${test} --verbose done fi } export LSAN_OPTIONS="suppressions=@installedtestsdatadir@/lsan-suppressions.txt" run_test acpi-dmar-self-test run_test acpi-facp-self-test run_test acpi-ivrs-self-test run_test acpi-phat-self-test run_test ata-self-test run_test dfu-self-test run_test fwupdplugin-self-test run_test linux-swap-self-test run_test logitech-hidpp-self-test run_test mtd-self-test run_test nitrokey-self-test run_test nvme-self-test run_test redfish-self-test run_test synaptics-prometheus-self-test run_test tpm-self-test run_test uefi-dbx-self-test run_test uefi-mok-self-test run_test vli-self-test run_test wacom-usb-self-test run_umockdev_test fwupd_test.py run_umockdev_test pci_psp_test.py run_device_tests fwupdmgr quit # success! exit 0 fwupd-2.0.10/data/tests/fwupd.test.in000066400000000000000000000002141501337203100174150ustar00rootroot00000000000000[Test] Type=session Exec=sh -c "G_TEST_SRCDIR=@installedtestsdatadir@ G_TEST_BUILDDIR=@installedtestsdatadir@ @installedtestsdir@/fwupd.sh" fwupd-2.0.10/data/tests/fwupd_ioc.py000066400000000000000000000072671501337203100173320ustar00rootroot00000000000000# SPDX-License-Identifier: LGPL-2.1-or-later # Copyright 2013-2018 Vincent Pelletier """ Pythonified linux asm-generic/ioctl.h . Produce IOCTL command numbers from their individual components, simplifying C header conversion to python (keeping magic constants and differences to C code to a minimum). Common parameter meanings: type (8-bits unsigned integer) Driver-imposed ioctl number. nr (8-bits unsigned integer) Driver-imposed ioctl function number. """ import array import ctypes import struct _IOC_NRBITS = 8 _IOC_TYPEBITS = 8 _IOC_SIZEBITS = 14 _IOC_DIRBITS = 2 _IOC_NRMASK = (1 << _IOC_NRBITS) - 1 _IOC_TYPEMASK = (1 << _IOC_TYPEBITS) - 1 _IOC_SIZEMASK = (1 << _IOC_SIZEBITS) - 1 _IOC_DIRMASK = (1 << _IOC_DIRBITS) - 1 _IOC_NRSHIFT = 0 _IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS _IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS _IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS IOC_NONE = 0 IOC_WRITE = 1 IOC_READ = 2 def IOC(dir, type, nr, size): """ dir One of IOC_NONE, IOC_WRITE, IOC_READ, or IOC_READ|IOC_WRITE. Direction is from the application's point of view, not kernel's. size (14-bits unsigned integer) Size of the buffer passed to ioctl's "arg" argument. """ assert dir <= _IOC_DIRMASK, dir assert type <= _IOC_TYPEMASK, type assert nr <= _IOC_NRMASK, nr assert size <= _IOC_SIZEMASK, size return ( (dir << _IOC_DIRSHIFT) | (type << _IOC_TYPESHIFT) | (nr << _IOC_NRSHIFT) | (size << _IOC_SIZESHIFT) ) def IOC_TYPECHECK(t): """ Returns the size of given type, and check its suitability for use in an ioctl command number. """ if isinstance(t, (memoryview, bytearray)): size = len(t) elif isinstance(t, struct.Struct): size = t.size elif isinstance(t, array.array): size = t.itemsize * len(t) else: size = ctypes.sizeof(t) assert size <= _IOC_SIZEMASK, size return size def IO(type, nr): """ An ioctl with no parameters. """ return IOC(IOC_NONE, type, nr, 0) def IOR(type, nr, size): """ An ioctl with read parameters. size (ctype type or instance, memoryview, bytearray, struct.Struct, or array.array) Type/structure of the argument passed to ioctl's "arg" argument. """ return IOC(IOC_READ, type, nr, IOC_TYPECHECK(size)) def IOW(type, nr, size): """ An ioctl with write parameters. size (ctype type or instance, memoryview, bytearray, struct.Struct, or array.array) Type/structure of the argument passed to ioctl's "arg" argument. """ return IOC(IOC_WRITE, type, nr, IOC_TYPECHECK(size)) def IOWR(type, nr, size): """ An ioctl with both read an writes parameters. size (ctype type or instance, memoryview, bytearray, struct.Struct, or array.array) Type/structure of the argument passed to ioctl's "arg" argument. """ return IOC(IOC_READ | IOC_WRITE, type, nr, IOC_TYPECHECK(size)) def IOC_DIR(nr): """ Extract direction from an ioctl command number. """ return (nr >> _IOC_DIRSHIFT) & _IOC_DIRMASK def IOC_TYPE(nr): """ Extract type from an ioctl command number. """ return (nr >> _IOC_TYPESHIFT) & _IOC_TYPEMASK def IOC_NR(nr): """ Extract nr from an ioctl command number. """ return (nr >> _IOC_NRSHIFT) & _IOC_NRMASK def IOC_SIZE(nr): """ Extract size from an ioctl command number. """ return (nr >> _IOC_SIZESHIFT) & _IOC_SIZEMASK IOC_IN = IOC_WRITE << _IOC_DIRSHIFT IOC_OUT = IOC_READ << _IOC_DIRSHIFT IOC_INOUT = (IOC_WRITE | IOC_READ) << _IOC_DIRSHIFT IOCSIZE_MASK = _IOC_SIZEMASK << _IOC_SIZESHIFT IOCSIZE_SHIFT = _IOC_SIZESHIFT fwupd-2.0.10/data/tests/fwupd_test.py000077500000000000000000000247421501337203100175370ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2024 Mario Limonciello # # SPDX-License-Identifier: LGPL-2.1-or-later # to record a string for a device use # umockdev-record $PATH import os import subprocess import sys import tempfile import unittest import dbusmock import gi from gi.repository import GLib from gi.repository import Gio gi.require_version("UMockdev", "1.0") from gi.repository import UMockdev class FwupdTest(dbusmock.DBusTestCase): DBUS_NAME = "org.freedesktop.fwupd" DBUS_PATH = "/" DBUS_INTERFACE = "org.freedesktop.fwupd" @classmethod def setUpClass(cls): if "DAEMON_BUILDDIR" in os.environ: libexecdir = os.environ["DAEMON_BUILDDIR"] else: libexecdir = "@LIBEXECDIR@" cls.daemon_path = os.path.join(libexecdir, "fwupd") for dir in ["STATE_DIRECTORY", "CACHE_DIRECTORY"]: if dir in os.environ: os.makedirs(os.environ[dir], exist_ok=True) os.environ["G_DEBUG"] = "fatal_warnings" GLib.log_set_always_fatal( GLib.LogLevelFlags.LEVEL_WARNING | GLib.LogLevelFlags.LEVEL_ERROR | GLib.LogLevelFlags.LEVEL_CRITICAL ) # set up a fake system D-BUS cls.start_system_bus() cls.dbus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None) def setUp(self): self.testbed = UMockdev.Testbed.new() self.polkitd, self.obj_polkit = self.spawn_server_template( "polkitd", {}, stdout=subprocess.PIPE ) self.obj_polkit.SetAllowed( [ "org.freedesktop.fwupd.update-internal", "org.freedesktop.fwupd.update-internal-trusted", "org.freedesktop.fwupd.downgrade-internal-trusted", "org.freedesktop.fwupd.downgrade-internal", "org.freedesktop.fwupd.update-hotplug-trusted", "org.freedesktop.fwupd.update-hotplug", "org.freedesktop.fwupd.downgrade-hotplug-trusted", "org.freedesktop.fwupd.downgrade-hotplug", "org.freedesktop.fwupd.device-unlock", "org.freedesktop.fwupd.modify-config", "org.freedesktop.fwupd.device-activate", "org.freedesktop.fwupd.verify-update", "org.freedesktop.fwupd.modify-remote", "org.freedesktop.fwupd.set-approved-firmware", "org.freedesktop.fwupd.self-sign", "org.freedesktop.fwupd.get-bios-settings", "org.freedesktop.fwupd.set-bios-settings", "org.freedesktop.fwupd.fix-host-security-attr", "org.freedesktop.fwupd.undo-host-security-attr", ] ) self.proxy = None self.props_proxy = None self.daemon_log = None self.daemon = None self.changed_properties = {} def run(self, result=None): super().run(result) if not result or not self.daemon_log: return if len(result.errors) + len(result.failures) or os.getenv("VERBOSE"): with open(self.daemon_log.name, encoding="utf-8") as tmpf: sys.stderr.write("\nDAEMON log:\n") sys.stderr.write(tmpf.read()) sys.stderr.write("\n") def tearDown(self): del self.testbed self.stop_daemon() if self.polkitd: self.polkitd.stdout.close() try: self.polkitd.kill() except OSError: pass self.polkitd.wait() self.obj_polkit = None # # Daemon control and D-BUS I/O # def start_daemon(self): """Start daemon and create DBus proxy. When done, this sets self.proxy as the Gio.DBusProxy for power-profiles-daemon. """ env = os.environ.copy() env["G_DEBUG"] = "fatal-criticals" env["G_MESSAGES_DEBUG"] = "all" # note: Python doesn't propagate the setenv from Testbed.new(), so we # have to do that ourselves env["UMOCKDEV_DIR"] = self.testbed.get_root_dir() self.daemon_log = ( tempfile.NamedTemporaryFile() ) # pylint: disable=consider-using-with daemon_path = [self.daemon_path, "-vv"] # pylint: disable=consider-using-with self.daemon = subprocess.Popen( daemon_path, env=env, stdout=self.daemon_log, stderr=subprocess.STDOUT ) self.addCleanup(self.daemon.kill) def on_proxy_connected(_, res): try: self.proxy = Gio.DBusProxy.new_finish(res) except GLib.Error as exc: self.fail(exc) cancellable = Gio.Cancellable() self.addCleanup(cancellable.cancel) Gio.DBusProxy.new( self.dbus, Gio.DBusProxyFlags.DO_NOT_AUTO_START, None, self.DBUS_NAME, self.DBUS_PATH, self.DBUS_INTERFACE, cancellable, on_proxy_connected, ) # wait until the daemon gets online wait_time = 60 if "valgrind" in daemon_path[0] else 5 self.assert_eventually( lambda: self.proxy and self.proxy.get_name_owner(), timeout=wait_time * 1000, message=f"daemon did not start in {wait_time} seconds", ) def properties_changed_cb(_, changed_properties, invalidated): self.changed_properties.update(changed_properties.unpack()) self.addCleanup( self.proxy.disconnect, self.proxy.connect("g-properties-changed", properties_changed_cb), ) self.assertEqual(self.daemon.poll(), None, "daemon crashed") def stop_daemon(self): """Stop the daemon if it is running.""" if self.daemon: try: self.daemon.terminate() except OSError: pass self.assertEqual(self.daemon.wait(timeout=3000), 0) self.daemon = None self.proxy = None def assert_eventually(self, condition, message=None, timeout=5000): """Assert that condition function eventually returns True. Timeout is in milliseconds, defaulting to 5000 (5 seconds). message is printed on failure. """ if condition(): return done = False def on_timeout_reached(): nonlocal done done = True source = GLib.timeout_add(timeout, on_timeout_reached) while not done: if condition(): GLib.source_remove(source) return GLib.MainContext.default().iteration(False) self.fail(message or "timed out waiting for " + str(condition)) def assert_dbus_property_eventually_is(self, prop, value, timeout=1200): """Asserts that a dbus property eventually is what expected""" return self.assert_eventually( lambda: self.get_dbus_property(prop) == value, timeout=timeout, message=f"property '{prop}' is not '{value}', but " + f"'{self.get_dbus_property(prop)}'", ) def ensure_dbus_properties_proxies(self): self.props_proxy = Gio.DBusProxy.new_sync( self.dbus, Gio.DBusProxyFlags.DO_NOT_AUTO_START | Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION | Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES | Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, None, self.DBUS_NAME, self.DBUS_PATH, "org.freedesktop.DBus.Properties", None, ) def get_dbus_property(self, name): """Get property value from daemon D-Bus interface.""" self.ensure_dbus_properties_proxies() return self.props_proxy.Get("(ss)", self.DBUS_NAME, name) def start_upower(self, on_battery): """Start the upower service with the given on_battery state""" sys_bat = self.testbed.add_device( "power_supply", "fakeBAT0", None, [ "type", "Battery", "present", "1", "status", "Discharging", "energy_full", "60000000", "energy_full_design", "80000000", "energy_now", "48000000", "voltage_now", "12000000", ], ["POWER_SUPPLY_ONLINE", "1"], ) self.upowerd, obj_upower = self.spawn_server_template( "upower", {"DaemonVersion": "0.99", "OnBattery": on_battery}, stdout=subprocess.PIPE, ) def stop_upower(self): """Stop the upower service""" self.upowerd.terminate() self.upowerd.wait() class VeryBasicTest(FwupdTest): """Test the basic properties of the daemon.""" @classmethod def setUpClass(cls): super().setUpClass() gi.require_version("Fwupd", "2.0") from gi.repository import Fwupd # pylint: disable=wrong-import-position cls.client = Fwupd.Client() def test_properties(self): """Test the properties of the daemon without a client.""" self.start_daemon() self.assertEqual(self.get_dbus_property("Interactive"), False) self.assertEqual(self.get_dbus_property("OnlyTrusted"), True) self.assertEqual(self.get_dbus_property("Tainted"), False) self.assertEqual(self.get_dbus_property("HostBkc"), "") self.assertEqual(self.get_dbus_property("HostProduct"), "Unknown Product") self.assertEqual(self.get_dbus_property("HostVendor"), "Unknown Vendor") self.assertEqual(self.get_dbus_property("Percentage"), 0) self.assertEqual(self.get_dbus_property("Status"), 1) self.assertEqual(self.get_dbus_property("BatteryLevel"), 101) def test_get_devices(self): """Test the libfwupd client get-devices interface.""" self.start_daemon() devices = self.client.get_devices() # Should be at least the CPU test is running on self.assertGreater(len(devices), 0) if __name__ == "__main__": # run ourselves under umockdev if "umockdev" not in os.environ.get("LD_PRELOAD", ""): os.execvp("umockdev-wrapper", ["umockdev-wrapper", sys.executable] + sys.argv) prog = unittest.main(exit=False) if prog.result.errors or prog.result.failures: sys.exit(1) # Translate to skip error if prog.result.testsRun == len(prog.result.skipped): sys.exit(77) fwupd-2.0.10/data/tests/fwupdmgr-p2p.sh000077500000000000000000000007741501337203100176660ustar00rootroot00000000000000#!/bin/sh -e exec 2>&1 # only run as root, possibly only in CI if [ "$(id -u)" -ne 0 ]; then exit 0; fi # --- echo "Starting P2P daemon..." export FWUPD_DBUS_SOCKET="/run/fwupd.sock" rm -rf ${FWUPD_DBUS_SOCKET} @libexecdir@/fwupd/fwupd -v --timed-exit --no-timestamp & while [ ! -e ${FWUPD_DBUS_SOCKET} ]; do sleep 1; done # --- echo "Starting P2P client..." fwupdmgr get-devices --json rc=$?; if [ $rc != 0 ]; then exit $rc; fi # --- echo "Shutting down P2P daemon..." fwupdmgr quit # success! exit 0 fwupd-2.0.10/data/tests/fwupdmgr-p2p.test.in000066400000000000000000000001051501337203100206210ustar00rootroot00000000000000[Test] Type=session Exec=sh -c "@installedtestsdir@/fwupdmgr-p2p.sh" fwupd-2.0.10/data/tests/fwupdmgr.sh000077500000000000000000000161461501337203100171670ustar00rootroot00000000000000#!/bin/sh export NO_COLOR=1 exec 0>/dev/null exec 2>&1 device=08d460be0f1f9f128413f816022a6439e0078018 CAB="@installedtestsdir@/fakedevice123.cab" error() { rc=$1 if [ -f "fwupd.txt" ]; then cat fwupd.txt else journalctl -u fwupd -b || true fi exit $rc } # --- echo "Verify test device is present" fwupdtool get-devices --json | jq -e '.Devices | any(.Plugin == "test")' rc=$?; if [ $rc != 0 ]; then echo "Enable test device" fwupdtool enable-test-devices rc=$?; if [ $rc != 0 ]; then error $rc; fi fi # --- echo "Show help output" fwupdmgr --help rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Show version output" fwupdmgr --version rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Getting the list of plugins..." fwupdmgr get-plugins rc=$?; if [ $rc != 0 ]; then error $rc; fi if [ -n "$CI" ]; then # --- echo "Setting BIOS setting..." fwupdmgr set-bios-setting fwupd_self_test value rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Getting BIOS settings..." fwupdmgr get-bios-setting rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Getting BIOS settings (json)..." fwupdmgr get-bios-setting --json rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Getting BIOS settings (unfound)..." fwupdmgr get-bios-setting foo rc=$?; if [ $rc != 3 ]; then error $rc; fi # --- echo "Setting BIOS setting (unfound)..." fwupdmgr set-bios-setting unfound value rc=$?; if [ $rc != 3 ]; then error $rc; fi fi # --- echo "Getting the list of plugins (json)..." fwupdmgr get-plugins --json rc=$?; if [ $rc != 0 ]; then error $rc; fi if [ -f ${CAB} ]; then # --- echo "Examining ${CAB}..." fwupdmgr get-details ${CAB} rc=$?; if [ $rc != 0 ]; then exit $rc; fi # --- echo "Examining ${CAB} (json)..." fwupdmgr get-details ${CAB} --json rc=$?; if [ $rc != 0 ]; then exit $rc; fi # --- echo "Installing ${CAB} cabinet..." fwupdmgr install ${CAB} --no-reboot-check rc=$?; if [ $rc != 0 ]; then exit $rc; fi fi # --- echo "Update the device hash database..." fwupdmgr get-releases $device rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Getting the list of remotes..." fwupdmgr get-remotes rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Disabling vendor-directory remote..." fwupdmgr disable-remote vendor-directory rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Getting the list of remotes (json)..." fwupdmgr get-remotes --json rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Enable vendor-directory remote..." fwupdmgr enable-remote vendor-directory rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Update the device hash database..." fwupdmgr verify-update $device rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Getting devices (should be one)..." fwupdmgr get-devices --no-unreported-check rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Testing the verification of firmware..." fwupdmgr verify $device rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Getting updates (should be one)..." fwupdmgr --no-unreported-check --no-metadata-check get-updates rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Installing test firmware..." fwupdmgr update $device -y rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Check if anything was tagged for emulation" fwupdmgr get-devices --json --filter emulation-tag | jq -e '(.Devices | length) > 0' rc=$?; if [ $rc = 0 ]; then echo "Save device emulation" fwupdmgr emulation-save /dev/null rc=$?; if [ $rc != 0 ]; then error $rc; fi echo "Save device emulation (bad args)" fwupdmgr emulation-save rc=$?; if [ $rc != 1 ]; then error $rc; fi fi # --- echo "Verifying results (str)..." fwupdmgr get-results $device -y rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Verifying results (json)..." fwupdmgr get-results $device -y --json rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Getting updates (should be none)..." fwupdmgr --no-unreported-check --no-metadata-check get-updates rc=$?; if [ $rc != 2 ]; then error $rc; fi # --- echo "Getting updates [json] (should be none)..." fwupdmgr --no-unreported-check --no-metadata-check get-updates --json rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Testing the verification of firmware (again)..." fwupdmgr verify $device rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Getting history (should be none)..." fwupdmgr get-history rc=$?; if [ $rc != 2 ]; then exit $rc; fi if [ -n "$CI_NETWORK" ]; then # --- echo "Downgrading to older release (requires network access)" fwupdmgr --download-retries=5 downgrade $device -y rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Downgrading to older release (should be none)" fwupdmgr downgrade $device rc=$?; if [ $rc != 2 ]; then error $rc; fi # --- echo "Updating all devices to latest release (requires network access)" fwupdmgr --download-retries=5 --no-unreported-check --no-metadata-check --no-reboot-check update -y rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Getting updates (should be none)..." fwupdmgr --no-unreported-check --no-metadata-check get-updates rc=$?; if [ $rc != 2 ]; then error $rc; fi # --- echo "Refreshing from the LVFS (requires network access)..." fwupdmgr --download-retries=5 refresh rc=$?; if [ $rc != 0 ]; then error $rc; fi else echo "Skipping network tests due to CI_NETWORK not being set" fi # --- echo "Modifying config..." fwupdmgr modify-config fwupd UpdateMotd false --json rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Resetting changed config..." fwupdmgr reset-config fwupd --json rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Resetting empty config ..." fwupdmgr reset-config fwupd --json rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Inhibiting for 100ms..." fwupdmgr inhibit test 100 rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Add blocked firmware..." fwupdmgr block-firmware foo rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Add blocked firmware (again)..." fwupdmgr block-firmware foo rc=$?; if [ $rc != 2 ]; then error $rc; fi # --- echo "Getting blocked firmware..." fwupdmgr get-blocked-firmware rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Remove blocked firmware..." fwupdmgr unblock-firmware foo rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Remove blocked firmware (again)..." fwupdmgr unblock-firmware foo rc=$?; if [ $rc != 2 ]; then error $rc; fi # --- echo "Setting approved firmware..." fwupdmgr set-approved-firmware foo,bar,baz rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Getting approved firmware..." fwupdmgr get-approved-firmware rc=$?; if [ $rc != 0 ]; then error $rc; fi UNAME=$(uname -m) if [ "${UNAME}" = "x86_64" ] || [ "${UNAME}" = "x86" ]; then EXPECTED=0 else EXPECTED=1 fi # --- echo "Run security tests..." fwupdmgr security rc=$?; if [ $rc != $EXPECTED ]; then error $rc; fi # --- echo "Run security tests (json)..." fwupdmgr security --json rc=$?; if [ $rc != $EXPECTED ]; then error $rc; fi # success! exit 0 fwupd-2.0.10/data/tests/fwupdmgr.test.in000066400000000000000000000001011501337203100201160ustar00rootroot00000000000000[Test] Type=session Exec=sh -c "@installedtestsdir@/fwupdmgr.sh" fwupd-2.0.10/data/tests/fwupdtool-efiboot.sh000077500000000000000000000052771501337203100210070ustar00rootroot00000000000000#!/bin/sh exec 0>/dev/null exec 2>&1 TMPDIR="$(mktemp -d)" trap 'rm -rf -- "$TMPDIR"' EXIT export NO_COLOR=1 export FWUPD_VERBOSE=1 export FWUPD_SYSFSFWDIR=${TMPDIR} export FWUPD_UEFI_ESP_PATH=${TMPDIR}/mnt # use these to fake a UEFI system mkdir -p ${FWUPD_SYSFSFWDIR}/efi/efivars mkdir -p ${FWUPD_UEFI_ESP_PATH} error() { rc=$1 cat fwupdtool.txt exit $rc } run() { cmd="fwupdtool -v $*" echo "cmd: $cmd" >fwupdtool.txt $cmd 1>>fwupdtool.txt 2>&1 } # --- echo "Creating Boot0001" run efiboot-create 0001 Fedora shimx64.efi ${FWUPD_UEFI_ESP_PATH} rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Creating Boot0001 again (should fail)..." run efiboot-create 0001 Fedora shimx64.efi ${FWUPD_UEFI_ESP_PATH} rc=$?; if [ $rc != 2 ]; then error $rc; fi # --- echo "Creating Boot0002 with invalid path (should fail)..." run efiboot-create 0002 Fedora shimx64.efi /mnt/dave rc=$?; if [ $rc != 3 ]; then error $rc; fi # --- echo "Getting BootOrder (should fail)" run efiboot-order rc=$?; if [ $rc != 3 ]; then error $rc; fi # --- echo "Setting BootOrder" run efiboot-order 0001 rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Getting BootOrder" run efiboot-order rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Setting BootNext" run efiboot-next 0001 rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Getting BootNext" run efiboot-next rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Getting hive cmdline (should fail)" run efiboot-hive 0001 cmdline rc=$?; if [ $rc != 1 ]; then error $rc; fi # --- echo "Setting hive cmdline" run efiboot-hive --force 0001 cmdline acpi=off rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Getting hive cmdline" run efiboot-hive 0001 cmdline rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Creating Boot0002 as Win10" run efiboot-create 0002 Win10 bootmgfw.efi ${FWUPD_UEFI_ESP_PATH} rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Setting hive cmdline for Win10 (should fail)" run efiboot-hive --force 0002 cmdline acpi=off rc=$?; if [ $rc != 1 ]; then error $rc; fi # --- echo "Showing EFI boot info" run efiboot-info rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Showing EFI boot info (json)" run efiboot-info --json rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Showing EFI files" run efiboot-files rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Deleting Boot0001" run efiboot-delete 0001 rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Deleting Boot0001 (should fail)..." run efiboot-delete 0001 rc=$?; if [ $rc != 3 ]; then error $rc; fi # --- echo "Showing EFI variables" run efivar-list 8be4df61-93ca-11d2-aa0d-00e098032b8c rc=$?; if [ $rc != 0 ]; then error $rc; fi fwupd-2.0.10/data/tests/fwupdtool-efiboot.test.in000066400000000000000000000001121501337203100217350ustar00rootroot00000000000000[Test] Type=session Exec=sh -c "@installedtestsdir@/fwupdtool-efiboot.sh" fwupd-2.0.10/data/tests/fwupdtool.sh000077500000000000000000000147001501337203100173510ustar00rootroot00000000000000#!/bin/sh exec 0>/dev/null exec 2>&1 export NO_COLOR=1 export FWUPD_VERBOSE=1 CAB=fakedevice124.cab INPUT="@installedtestsdir@/fakedevice124.bin \ @installedtestsdir@/fakedevice124.jcat \ @installedtestsdir@/fakedevice124.metainfo.xml" DEVICE=08d460be0f1f9f128413f816022a6439e0078018 error() { rc=$1 cat fwupdtool.txt exit $rc } run() { cmd="fwupdtool -v $*" echo "cmd: $cmd" >fwupdtool.txt $cmd 1>>fwupdtool.txt 2>&1 } # --- echo "Show help output" run --help rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Show version output" run --version rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Show version output (json)" run --version --json rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Showing hwids" run hwids rc=$?; if [ $rc != 0 ]; then error $rc; fi UNAME=$(uname -m) if [ "${UNAME}" = "x86_64" ] || [ "${UNAME}" = "x86" ]; then EXPECTED=0 else EXPECTED=1 fi # --- echo "Showing security" run security rc=$?; if [ $rc != $EXPECTED ]; then error $rc; fi # --- echo "Showing plugins" run get-plugins rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Showing plugins (json)" run get-plugins --json rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Enabling test device..." run enable-test-devices rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Checking device-flags" run get-device-flags rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Checking firmware-gtypes" run get-firmware-gtypes rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Checking firmware-types" run get-firmware-types rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Checking for updates" run get-updates rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Checking for updates" run get-updates --json rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Building ${CAB}..." run build-cabinet ${CAB} ${INPUT} --force rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Examining ${CAB}..." run get-details ${CAB} rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Installing ${CAB} cabinet..." run install ${CAB} rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Cleaning ${CAB} generated cabinet ..." rm -f ${CAB} # --- echo "Verifying update..." run verify-update ${DEVICE} rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Getting history (should be one)..." run get-history rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Clearing history..." run clear-history ${DEVICE} rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Getting history (should be none)..." run get-history rc=$?; if [ $rc != 2 ]; then error $rc; fi # --- echo "Resetting config..." run reset-config test rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Testing good version compare" run vercmp 1.0.0 1.0.0 triplet rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Testing bad version compare" run vercmp 1.0.0 1.0.1 foo rc=$?; if [ $rc != 1 ]; then error $rc; fi # --- echo "Getting supported version formats..." run get-version-formats --json rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Getting report metadata..." run get-report-metadata --json rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Getting the list of remotes" run get-remotes --json rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Disabling LVFS remote..." run modify-remote lvfs Enabled false rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Enabling LVFS remote..." run modify-remote lvfs Enabled true rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Modify unknown remote (should fail)..." run modify-remote foo Enabled true rc=$?; if [ $rc != 1 ]; then error $rc; fi # --- echo "Modify known remote but unknown key (should fail)..." run modify-remote lvfs bar true rc=$?; if [ $rc != 3 ]; then error $rc; fi # --- echo "Getting devices (should be one)..." run get-devices --json rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Changing VALID config on test device..." run modify-config test AnotherWriteRequired true rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Changing INVALID config on test device...(should fail)" run modify-config test Foo true rc=$?; if [ $rc != 1 ]; then error $rc; fi # --- echo "Disabling test device..." run disable-test-devices rc=$?; if [ $rc != 0 ]; then error $rc; fi BASEDIR=@installedtestsdir@/tests/bios-attrs/dell-xps13-9310/ if [ -d $BASEDIR ]; then WORKDIR="$(mktemp -d)" cp $BASEDIR $WORKDIR -r export FWUPD_SYSFSFWATTRIBDIR=$WORKDIR/dell-xps13-9310/ # --- echo "Get BIOS settings..." run get-bios-settings --json rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Get BIOS setting as json..." run get-bios-settings WlanAutoSense --json rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Get BIOS setting as a string..." run get-bios-settings WlanAutoSense rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Modify BIOS setting to different value..." run set-bios-setting WlanAutoSense Enabled --no-reboot-check rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Modify BIOS setting back to default..." run set-bios-setting WlanAutoSense Disabled --no-reboot-check rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Modify BIOS setting to bad value (should fail)..." run set-bios-setting WlanAutoSense foo --no-reboot-check rc=$?; if [ $rc != 1 ]; then error $rc; fi # --- echo "Modify Unknown BIOS setting (should fail)..." run set-bios-setting foo bar --no-reboot-check rc=$?; if [ $rc != 3 ]; then error $rc; fi fi if [ -x /usr/bin/certtool ]; then # --- echo "Building unsigned ${CAB}..." INPUT="@installedtestsdir@/fakedevice124.bin \ @installedtestsdir@/fakedevice124.metainfo.xml" run build-cabinet ${CAB} ${INPUT} --force rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Sign ${CAB}" @installedtestsdir@/build-certs.py /tmp run firmware-sign ${CAB} /tmp/testuser.pem /tmp/testuser.key --json rc=$?; if [ $rc != 0 ]; then error $rc; fi # --- echo "Cleaning self-signed ${CAB}..." rm -f ${CAB} fi if [ -z "$CI_NETWORK" ]; then echo "Skipping remaining tests due to CI_NETWORK not being set" exit 0 fi # --- echo "Refresh remotes" run refresh --json rc=$?; if [ $rc != 0 ]; then error $rc; fi fwupd-2.0.10/data/tests/fwupdtool.test.in000066400000000000000000000001021501337203100203070ustar00rootroot00000000000000[Test] Type=session Exec=sh -c "@installedtestsdir@/fwupdtool.sh" fwupd-2.0.10/data/tests/lsan-suppressions.txt000066400000000000000000000000771501337203100212420ustar00rootroot00000000000000leak:Esys_Initialize leak:OPENSSL_init_ssl leak:SSL_CTX_new_ex fwupd-2.0.10/data/tests/meson.build000066400000000000000000000075341501337203100171400ustar00rootroot00000000000000con2 = configuration_data() con2.set('installedtestsdir', installed_test_datadir) con2.set('installedtestsbindir', installed_test_bindir) con2.set('installedtestsdatadir', installed_test_datadir) con2.set('devicetestdir', join_paths(installed_test_datadir, 'device-tests')) con2.set('bindir', bindir) con2.set('libexecdir', libexecdir) configure_file( input: 'fwupdmgr.test.in', output: 'fwupdmgr.test', configuration: con2, install: true, install_dir: installed_test_datadir, ) configure_file( input: 'fwupdmgr-p2p.test.in', output: 'fwupdmgr-p2p.test', configuration: con2, install: true, install_dir: installed_test_datadir, ) configure_file( input: 'fwupdtool.test.in', output: 'fwupdtool.test', configuration: con2, install: true, install_dir: installed_test_datadir, ) configure_file( input: 'fwupdtool-efiboot.test.in', output: 'fwupdtool-efiboot.test', configuration: con2, install: true, install_dir: installed_test_datadir, ) configure_file( input: 'fwupd.test.in', output: 'fwupd.test', configuration: con2, install: true, install_dir: installed_test_datadir, ) configure_file( input: 'fwupdmgr-p2p.sh', output: 'fwupdmgr-p2p.sh', configuration: con2, install: true, install_dir: installed_test_datadir, ) install_data([ 'build-certs.py', 'fakedevice124.bin', 'fakedevice124.jcat', 'fakedevice124.metainfo.xml', 'fwupd-tests.xml', 'lsan-suppressions.txt', ], install_dir: installed_test_datadir, ) configure_file( input: 'fwupdmgr.sh', output: 'fwupdmgr.sh', configuration: con2, install: true, install_dir: installed_test_datadir, ) configure_file( input: 'fwupdtool.sh', output: 'fwupdtool.sh', configuration: con2, install: true, install_dir: installed_test_datadir, ) configure_file( input: 'fwupdtool-efiboot.sh', output: 'fwupdtool-efiboot.sh', configuration: con2, install: true, install_dir: installed_test_datadir, ) configure_file( input: 'fwupd.sh', output: 'fwupd.sh', configuration: con2, install: true, install_dir: installed_test_datadir, ) custom_target('installed-cab123', input: [ 'fakedevice123.bin', 'fakedevice123.jcat', 'fakedevice123.metainfo.xml', ], output: 'fakedevice123.cab', command: [ gcab, '--create', '--nopath', '@OUTPUT@', '@INPUT@', ], install: true, install_dir: installed_test_datadir, ) custom_target('installed-cab124', input: [ 'fakedevice124.bin', 'fakedevice124.jcat', 'fakedevice124.metainfo.xml', ], output: 'fakedevice124.cab', command: [ gcab, '--create', '--nopath', '@OUTPUT@', '@INPUT@', ], install: true, install_dir: installed_test_datadir, ) # replace @installedtestsdir@ configure_file( input: 'remote.conf.in', output: 'fwupd-tests.conf', configuration: con2, install: true, install_dir: join_paths(datadir, 'fwupd', 'remotes.d'), ) if umockdev_integration_tests.allowed() fwupd_mockdev_tests = files('fwupd_test.py') r = run_command(unittest_inspector, fwupd_mockdev_tests, check: true) unit_tests = r.stdout().strip().split('\n') foreach ut: unit_tests test(ut, python3, args: [fwupd_mockdev_tests, ut], is_parallel: false, env: { 'CACHE_DIRECTORY': join_paths(meson.project_build_root(), 'cache'), 'DAEMON_BUILDDIR': join_paths(meson.project_build_root(), 'src'), 'GI_TYPELIB_PATH': join_paths(meson.project_build_root(), 'libfwupd'), 'LD_LIBRARY_PATH': join_paths(meson.project_build_root(), 'libfwupd'), 'STATE_DIRECTORY': join_paths(meson.project_build_root(), 'state') }, ) endforeach configure_file( input: fwupd_mockdev_tests, output: 'fwupd_test.py', configuration: { 'LIBEXECDIR': daemon_dir, }, install_dir: installed_test_datadir ) install_data('fwupd_ioc.py', install_dir: installed_test_datadir) endif fwupd-2.0.10/data/tests/remote.conf.in000066400000000000000000000005161501337203100175360ustar00rootroot00000000000000[fwupd Remote] # This is a local fwupd remote that is used only for installed tests # either from continuous integration or for fake devices from fwupd # frontends # It will only be loaded when the daemon configuration has `TestDevices=true` Enabled=true Title=fwupd test suite MetadataURI=file://@installedtestsdir@/fwupd-tests.xml fwupd-2.0.10/docs/000077500000000000000000000000001501337203100136425ustar00rootroot00000000000000fwupd-2.0.10/docs/architecture-plan.svg000066400000000000000000002241361501337203100200050ustar00rootroot00000000000000 image/svg+xml fwupd Bluetooth (bluez) Keyboard customplugins udev, sysfs, ESRT systemd pending.db internet system IPFS CDN sqlite gnome-softwarefwupdmgr Mouse SAS / RAID BMC UpdateMetadata() GetDevices() IPMI & Redfish only metadata manual user opt-in firmware AppStream XML LVFS session embargoedmetadata fwupd-2.0.10/docs/best-known-configuration.md000066400000000000000000000120701501337203100211200ustar00rootroot00000000000000--- title: Best Known Configuration --- ## Introduction Component ``s are used both by OEMs and by customers to identify a *known-working* (or commercially supported) set of firmware on the machine. This allows two things: * Factory recovery where a system in the field has been upgraded * Ensuring a consistent set of vendor-tested firmware for a specific workload The tags are either assigned in the firmware cabinet archive (the `.metainfo.xml` file) or added post-upload on the LVFS and are then included in the public AppStream metadata. A single firmware can be marked with multiple tags, and tags can be duplicated for different firmwares. This would allow an OEM to say *this set of firmware has been tested as a set for workload A, and this other set of firmware has been tested for workload B* which is fairly typical for enterprise deployments. ## LVFS The LVFS added support for “vendor-defined†component ``s in 2021, which are also supported in fwupd since version 1.7.3. These tags are typically used by OEMs to identify a manifest of firmware for a specific machine SKU. This is opt-in for each vendor, and so if you are a vendor reading this and want to use this feature, let us know by [opening an issue](https://gitlab.com/fwupd/lvfs-website/-/issues). ## Client When provisioning the client machine, we can set the BKC by setting `HostBkc=vendor-2021q1` in `/etc/fwupd/fwupd.conf`. Any invocation of `fwupdmgr sync` will install or downgrade firmware on all compatible devices (e.g. UEFI, RAID, network adapter, & SAS HBA) to make the system match a compatible set. The `fwupdmgr sync` command will also ensure that firmware is installed that matches the device branch, if the device has one assigned. Updating or downgrading firmware away from the *Best Known Configuration* or to different branches is allowed, but the UI shows a warning. Using `fwupdmgr sync` will undo any manual changes and bring the machine back to the BKC. ## Local metadata To define a locally defined BKC, extra metadata is read from the `/usr/share/fwupd/local.d` and `/var/lib/fwupd/local.d` directories. For instance: For example: 3ef35d3b-ceeb-5e27-8c0a-ac25f90367ab 2ef35d3b-ceeb-5e27-8c0a-ac25f90367ac 1ef35d3b-ceeb-5e27-8c0a-ac25f90367ad mycompanyname-2022q1 This then appears when getting the releases for that specific GUID: fwupdmgr get-releases --json 3ef35d3b-ceeb-5e27-8c0a-ac25f90367ab { "Releases" : [ { ... "Version" : "225.53.1649", "Tags" : [ "mycompanyname-2022q1" ], ... }, { ... "Version" : "224.48.1605", "Tags" : [ "mycompanyname-2022q1" ], ... }, { ... "Version" : "224.45.1389", ... } ] } ..and can be synced on the command line: $ fwupdmgr sync â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•— â•‘ Downgrade System Firmware from 225.52.1521 to 225.53.1649? â•‘ â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•£ â•‘ This release sets the number of HBAs supported by the system to 1024. â•‘ ╚â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• Perform operation? [Y|n]: ## Vendor Firmware Remotes Tags can also be included in the `metainfo.xml` files included in `.cab` archives installed into `/usr/share/fwupd/remotes.d/vendor/firmware/` -- although in most cases it makes sense to actually *decouple* the tag assignment from the firmware binary by specifying local metadata. If the tag is specific to the firmware build, then it can be included directly in the metadata: org.fwupd.myproduct.firmware ... product-mycompanyname-2022q1 **NOTE:** the `namespace="lvfs"` is required for fwupd as the `` section is also used by other software for different purposes. Forgetting the namespace will cause fwupd to ignore the tag! fwupd-2.0.10/docs/bios-settings.md000066400000000000000000000251311501337203100167600ustar00rootroot00000000000000--- title: BIOS Settings API --- fwupd 1.8.4 and later include the ability to modify BIOS settings on systems that support the Linux kernel's [firmware-attributes API](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-firmware-attributes). Drivers included with the Linux kernel on supported machines will advertise all BIOS settings that the OS can change. The fwupd daemon uses this API create an abstraction that fwupd clients can use to offer BIOS settings to change to end users. ## Interactive command line usage Both `fwupdmgr` and `fwupdtool` have support for the fwupd BIOS settings API both for interactive use. Using `fwupdgmr` will require PolicyKit authentication as only authorized users can view the current BIOS settings. Using `fwupdtool` will require running the tool as root, as `fwupdtool` only works as root. ### Getting available BIOS settings To fetch available BIOS settings for a machine: ```shell # fwupdmgr get-bios-setting ``` This will provide a listing of all available settings. If you would like to just see a subset of attributes, you can list them space delimited on the command line. For example: ```shell $ fwupdmgr get-bios-setting WindowsUEFIFirmwareUpdate MmioAbove4GLimit Authenticating… [ - ] WindowsUEFIFirmwareUpdate: Setting type: Enumeration Current Value: Enable Description: BIOS updates delivered via LVFS or Windows Update Read Only: False Possible Values: 0: Disable 1: Enable MmioAbove4GLimit: Setting type: Enumeration Current Value: Auto Description: MmioAbove4GLimit Read Only: False Possible Values: 0: Auto 1: 40 2: 42 3: 44 4: 46 5: 48 ``` When using BASH as your shell, bash-completion can be used to discover BIOS settings. In the above example, those settings could be found from this series of tab actions: ```shell $ sudo fwupdmgr get-bios-setting W WakeonLAN WakeUponAlarm WindowsUEFIFirmwareUpdate 130 $ sudo fwupdmgr get-bios-setting WindowsUEFIFirmwareUpdate Display all 131 possibilities? (y or n) AbsolutePersistenceModule CStateSupport M2Slot2Port PCIeSlot3Bifurcation PXEIPV6NetworkStack SetStrongPassword AccessSecuritySettings DASHSupport MaxPasswordAttempts PCIeSlot3DLFSupport QuadM2PCIeCardFanControl SmartUSBProtection AfterPowerLoss DataScrambling MCRUSBHeader PCIeSlot3LinkSpeed RealtimeDIAG SMT AlarmDate(MM\DD\YYYY) DeviceGuard MediaCardReader PCIeSlot3Port RearAudioController SRIOVSupport AlarmDayofWeek DevicePowerupDelay MmioAbove4GLimit PCIeSlot4Bifurcation RearUSBPorts StartupSequence AlarmTime(HH:MM:SS) DeviceResetTimeout --no-authenticate PCIeSlot4DLFSupport RemoteSetSMP USBChargingPortInS4S5 AllowJumperClearSVP DIAG7SegMode NUMA PCIeSlot4LinkSpeed RequireHDPonSystemBoot USBPortAccess AMDMemoryGuard EnhancedPowerSavingMode NVMeRAIDMode PCIeSlot4Port RequireSVPwhenFlashing USBTransferTimeout AMDSecureVirtualMachine ErrorBootSequence OnboardEthernetController PCIeSlot5Bifurcation SATAController UserDefinedAlarmFriday ASPMSupport FanControlStepping OptionKeysDisplay PCIeSlot5DLFSupport SATADrive1 UserDefinedAlarmMonday AutomaticBootSequence FrontUSBPorts PasswordChangeTime PCIeSlot5LinkSpeed SATADrive2 UserDefinedAlarmSaturday BIOSPasswordAtBootDeviceList HardDiskPre-delay PasswordCountExceededError PCIeSlot5Port SATADrive3 UserDefinedAlarmSunday BIOSPasswordAtReboot InternalUSB2Port PatroScrub PCIeSlot6Bifurcation SATADrive4 UserDefinedAlarmThursday BIOSPasswordAtSystemBoot InternalUSB3Port PatroScrubInterval PCIeSlot6DLFSupport SATADrive5 UserDefinedAlarmTime BootUpNumLockStatus IOMMU PCIeSlot1Bifurcation PCIeSlot6LinkSpeed SATADrive6 UserDefinedAlarmTuesday CardLocation --json PCIeSlot1DLFSupport PCIeSlot6Port SATADrive6HotPlugSupport UserDefinedAlarmWednesday ClearDIAGLog KeyboardLayout PCIeSlot1LinkSpeed pending_reboot SecureBoot --verbose ConfigurationChangeDetection M2Slot1DLFSupport PCIeSlot1Port PhysicalPresenceforClear SecureRollBackPrevention WakeonLAN ConfigureSATAas M2Slot1LinkSpeed PCIeSlot2Bifurcation POPChangeablebyUser SecurityChip WakeUponAlarm CoverTamperDetected M2Slot1Port PCIeSlot2DLFSupport PostPackageRepair SelectActiveVideo WindowsUEFIFirmwareUpdate CPBMode M2Slot2DLFSupport PCIeSlot2LinkSpeed PrimaryBootSequence SerialPort1Address XHCIHandoff CPUC6Report M2Slot2LinkSpeed PCIeSlot2Port PXEIPV4NetworkStack SetMinimumLength 130 $ fwupdmgr get-bios-setting WindowsUEFIFirmwareUpdate MmioAbove4GLimit ``` ### Setting BIOS settings To set one or more BIOS settings for a machine: ```shell # fwupdmgr set-bios-setting SETTING VALUE ``` For supported attributes, `fwupdmgr` will also use tab completion in BASH for filling out a value. For example to enable an enumeration attribute: ```shell $ sudo fwupdmgr set-bios-setting W WakeonLAN WakeUponAlarm WindowsUEFIFirmwareUpdate 130 $ sudo fwupdmgr set-bios-setting WindowsUEFIFirmwareUpdate Disable Enable 130 $ sudo fwupdmgr set-bios-setting WindowsUEFIFirmwareUpdate Enable ``` After setting an attribute you may be prompted to reboot as most settings will require a reboot to take effect. ```shell $ fwupdmgr set-bios-setting WakeonLAN Primary Authenticating… [ - ] Set BIOS setting 'WakeonLAN' using 'Primary'. An update requires a reboot to complete. Restart now? [y|N]: ``` If you would like to program multiple attributes, list them in pairs of the name of the attribute followed by the desired value. ## Programmatic command line usage `fwupdmgr` offers support for the fwupd BIOS settings API programmatically as well for use in scripts. To use the programmatic API you will add the `--json` argument to your calls. The output or input will then be expected to be JSON payloads. ### Fetching BIOS settings programmatically Below is an example of fetching the `WakeonLAN` setting. ```shell # fwupdmgr get-bios-setting WakeonLAN --json { "BiosSettings" : [ { "Name" : "WakeonLAN", "Description" : "WakeonLAN", "Filename" : "/sys/class/firmware-attributes/thinklmi/attributes/WakeonLAN", "BiosSettingId" : "com.thinklmi.WakeonLAN", "BiosSettingCurrentValue" : "Primary", "BiosSettingReadOnly" : "false", "BiosSettingType" : 1, "BiosSettingPossibleValues" : [ "Primary", "Automatic", "Disable" ] } ] } ``` Similar to the interactive usage, if no parameters are provided all settings are fetched, and if desired multiple settings can be listed on the command line. Error messages won't be emitted, you'll have to rely on the return code to tell if this was successful. *NOTE:* To debug errors, use the `--verbose` argument to see messages related to the error. ### Setting BIOS settings programmatically To set BIOS settings, a JSON payload will need to be crafted in advance. The path to this payload is used as an argument. ```shell # fwupdmgr set-bios-setting ~/foo.json --json ``` An important return code to know for programmatic usage is that *2* means nothing was done because all settings are already programmed. This message will also be emitted. *NOTE:* To debug errors, use the `--verbose` argument to see messages related to the error. ## Firmware setting policies `fwupd` has the ability to enforce the BIOS settings policy of a system administrator. To use this feature, create a json payload using `fwupdmgr get-bios-setting --json` that reflects the settings you would like to see enforced. Then copy this payload into `/etc/fwupd/bios-settings.d` with a filename ending in `.json`. The next time that the fwupd daemon is started (such as a system bootup) it will ensure that all BIOS settings are programed to your desired values. It will also mark those settings as read-only so no fwupd clients will be able to modify them. This *does not* stop the kernel firmware-attributes API from working. So a determined user with appropriate permissions would be able to modify settings from the kernel API directly, but they would be changed again on fwupd daemon startup. ## Settings types The Linux kernel will offer the following types of BIOS settings: * Enumeration: The setting will only accept a limited list of possible values * Integer: The setting will accept a limited range of integer values * String: The setting will accept a limited length UTF-8 string All of those setting types are accepted by fwupd. It is expected that drivers or firmware will validate the input, but where possible fwupd will do validation of the input to give better error messages and avoid failures. fwupd will also do a mapping where it can accept multiple cases or synonyms for words that are obviously positive or negative. So for example if the setting expects `Enabled` but the user passes `tRUE` fwupd will map this into `Enabled` before sending it to the driver and the driver sending it to the firmware. ## libfwupd `fwupdmgr` internally uses `FwupdClient` to manage BIOS settings. Any other clients that are interested in managing BIOS settings can use this library as well. A sample python application is included in the `contrib/` directory in the fwupd source tree. fwupd-2.0.10/docs/building.md000066400000000000000000000202531501337203100157630ustar00rootroot00000000000000--- title: Building & Debugging fwupd --- These instructons below can either be used by the silicon vendor, or the consulting company to debug existing and new plugins. Sometimes new hardware is only supported in the development version of fwupd which may not even be available as a Snap or Flatpak yet. ## Prerequisites * A PC with Linux (preferably the latest version of Fedora) installed bare metal (i.e. not in VirtualBox or VMWare) * Working access to the internet * A user account (we’ll use `u` as the example here) with administrator permissions ## Setup fwupd development environment A fwupd development environment is setup in a [virtualenv](https://virtualenv.pypa.io/en/latest/user_guide.html) to avoid development work for fwupd from conflicting with any system fwupd installation. All builds will occur in `venv/build` and all installs in `venv/dist`. To set it up follow the below steps: ```shell cd ~ git clone https://github.com/fwupd/fwupd.git cd fwupd ./contrib/setup ``` ## Building After the development environment has been setup you can enter it by running: ```shell source venv/bin/activate ``` You can tell you are in the development environment by looking at the start of your prompt for this prefix: ```text (fwupd) ``` To build the project a script is included that will configure and build the project with default settings. ```shell build-fwupd ``` To run the project test suite a script is included: ```shell test-fwupd ``` If you want to leave the development environment at any time you can run: ```shell deactivate ``` ## Running binaries The fwupd project is split into three main components: 1. **fwupd**: The binary that’s running in the background, as root 2. **fwupdmgr**: The client tool that end-users use to interact with the running `fwupd` binary, as a normal user 3. **fwupdtool**: The debugging tool developers use to find problems and to run new code, as root The `fwupdtool` binary does most of the things that `fwupdmgr` does, but without talking to the system fwupd instance. It is a lot easier to run `fwupdtool` with just one plugin (e.g. `--plugins vli`) than running the daemon and all the plugins. You might have to wait 5 seconds and then read thousands of lines of debugging to see the `printf()` you added in a new plugin with the daemon, but with `fwupdtool --plugins vli --verbose get-devices` it’ll be in a few lines, and instant. Within the development environment wrappers have been setup to allow launching `fwupd`, `fwupdtool` or `fwupdmgr` very similar to a host system. There are 3 main differences to note: 1. The `systemd` service will not run. That means that the daemon needs to be manually launched in the environment. 2. dbus *activation* doesn't work. This mean that if you are testing the daemon (`fwupd`) and client (`fwupdmgr`) interaction you need to have two terminal tabs opened each in the development environment activated. One tab would run the daemon, and one would run the client. 3. `fwupd` and `fwupdtool` will be automatically started as root (IE with `sudo`). With those differences in mind all 3 binaries can just be launched like normal: ```shell fwupdtool get-devices ``` ```shell fwupd ``` ```shell fwupdmgr get-devices ``` ## Using fwupdtool To get the list of devices from one specific plugin I would do: ```shell fwupdtool --plugins vli get-devices --verbose ``` This outputs lots of text onto the console like: ```text 10:51:49:0584 FuMain Lenovo ThinkPad WS Dock DeviceId: 73ef80b60058b4f18549921520bfd94eaf18710a Guid: dd1f77bd-88ef-5293-9e34-1fe5ce187658 <- USB\VID_17EF&PID_305A&REV_5011 Guid: 1c09a12d-e58a-5b4d-84af-ee3eb4c3c68b <- USB\VID_17EF&PID_305A Guid: 6201fecc-1641-51f6-a6d2-38a06d5476bf <- VLI_USBHUB\SPI_C220 Guid: c9caa540-6e27-5d40-a322-47eaeef84df0 <- USB\VID_17EF&PID_305A&SPI_C220&REV_5011 Guid: cfa1e12c-4eb9-5338-8b23-02acc5423ccb <- USB\VID_17EF&PID_305A&SPI_C220 Summary: USB 3.x Hub Plugin: vli Protocol: com.vli.usbhub Flags: updatable|registered|can-verify|can-verify-image Vendor: LENOVO VendorId: USB:0x17EF Version: 50.11 VersionFormat: bcd Icon: audio-card InstallDuration: 10 Created: 2019-12-20 ``` Using fwupdtool raw firmware blob (i.e. not the cabinet archive with metadata) can be installed on the device using: ```shell fwupdtool --verbose --plugins vli \ install-blob /home/u/the-firmware.bin 73ef80b60058b4f18549921520bfd94eaf18710a ``` ## Firmware Parsing You can also parse the raw .bin files using `fwupdtool` which has access to all the available firmware parsers built into all plugins. For example: ```shell fwupdtool firmware-parse /home/user/VL105_APP6_8C_09_08_06_20190815.bin Choose a firmware type: 0. Cancel 1. conexant 2. 8bitdo 3. synaprom 4. rmi 5. wacom 6. vli-pd 7. raw 8. altos 9. srec 10. ihex 11. vli-usbhub 12. vli-usbhub-pd 12 FuVliUsbhubPdFirmware: Version: 140.9.8.6 ChipId: VL105 VID: 0x2109 PID: 0x105 FuFirmwareImage: Data: 0xc000 ``` ## Using fwupdmgr You can perform the end-to-end tests with two terminals open to the fwupd development environment. In the first do: ```shell fwupd --verbose ``` and in the second you can do: ```shell fwupdmgr install ~/foo.cab ``` This will send the firmware archive from the locally built `fwupdmgr` to the locally built daemon using a file descriptor, which will call the new plugin code with the firmware blob in the archive. The daemon terminal will also show lots of useful debugging during this process. ## Using Visual Studio code to build and test During build time a set of tasks will have been created for use with Visual Studio Code. The default build task which is triggered by using *ctrl-shift-b* will build the project with default settings. The default test task can be triggered from the command palette to run the test suite. Open the command palette with *ctrl-shift-p* and type **Run test task** and hit enter. This will launch the daemon in a terminal window. test task screenshot ## Using Visual Studio Code to debug The [debugger](https://code.visualstudio.com/Docs/editor/debugging) that is part of [Visual Studio Code](https://code.visualstudio.com/) is really helpful for debugging issues. During build time a set of launch targets will have been created for use with Visual Studio Code. All 3 binaries have the ability be launched with a debugger attached as a **user** by using `DEBUG=1` in the environment. ### debugging `fwupdtool` and `fwupdmgr` For example to debug `fwupdtool` you would launch it like this: ```shell (fwupd) u@fedora:~/fwupd$ DEBUG=1 fwupdtool get-devices Process /home/u/fwupd/venv/bin/../dist/bin/fwupdtool created; pid = 595311 Listening on port 9091 ``` This will configure `gdbserver` to listen on a local port waiting for a debugger to connect. Launch vscode in the same directory as the Git checkout. After it's launched, set a source breakpoint. debug breakpoint screenshot Then use the run and debug button (or *ctrl-shift-d*) to open up the debugger. From the debugger choose the tool to use. debug tool selector screenshot Press the green start button (or use *F5*) to start debugging. The debugger will attach to the process you launched and stop where you left off. ![debugger attached](debug_attached.png) ### debugging fwupd (daemon) For debugging the daemon, a helper task is also included to launch the daemon with the `DEBUG` environment variable set within vscode. Open the command palette with *ctrl-shift-p* and type **Run task** and hit enter. Select the `gdbserver-fwupd` task. This will launch the daemon in a terminal window. debug task screenshot Then use the run and debug button (or *ctrl-shift-d*) to open up the debugger. From the debugger choose `gdbserver (fwupd)`. debug tool selector screenshot fwupd-2.0.10/docs/debug_attached.png000066400000000000000000012215441501337203100173040ustar00rootroot00000000000000‰PNG  IHDRÞî@iÄsBIT|dˆtEXtSoftwaregnome-screenshotï¿>-tEXtCreation TimeFri 05 Jan 2024 06:16:31 AM CSTg.¡E IDATxœìÝy\Tõþ?ð×™fØ×aßaXÑÜÓr©ÜPÒÜÒÌJÍnÝ[÷ÞÌúÞv·ŸKeÙ¦‰{™efi%š+* È"¢È¾8ì;s~Œsd˜``@ßÏÇÇÌÙæs`æÌ™Ïûó~±XÌ‚B!„B!„ô),Û~—MGë»{|ByX1 Ó£ûw÷ø„¾Í@ß „B!„B!ÊÚ Ši0£À!„tMG×ÏŽgŠý5mDz,ßy€QàB!„B!¤éjÐm„Ò;Ú^o»`£à!. ¼B!„B!„ô] ¬ip£ !„tO{A²ö2Ü(øFÈÇo„B!„B!}@gƒn”ýF!½G›,7M8 ¾òp¡À!„B!„Bˆžé"èFs¿Bˆni“å¦n;uÁ4 ¾òð À!„B!„BHÕ6HÖ™€Ø!¤{4]GÕÕÚ.×´Œl„<ø lllôÝB!„B!„‡Š64Åã¶ÿw´LÓñµYG!ä¾ö‚d­×©û¹½em«îy(@GHÿEo„B!„B!½H›À—ºÀš¦uíÜ(ÈF!]§îªˆµÎhkûsëÿÛOÓ:uÏMÁ7Bú'ƒ²²2}·B!„¢Šê)“÷è¹%„Ò?Žt§oEÛ2‘×D"ŒÁçó©³–Bz˲hiiA}}=jjjÐÐÐÐn&›6Yní-'„ô”ñF!„B!„Ò‡t&keYX[[ÃÔÔ´§›E!¤†a```SSS˜šš¢ººwïÞÕ8k›ÁFm„<¸(ðF!„BÈCÎ=~¢¾›@!ý“‚]m3ßììì`bb¨©©A]]š››©Ä$!„ô0EàÍÄÄ"‘¦¦¦àóù())Q*GIÁ5B>x#„B!„BуöæoS÷¸íÏÖÖÖ011AKK ¤R)šššz®±„B”°,‹¦¦&455¡®®VVV011µµµRæ[ëà›6Yo¬#¤ÿã黄B!„B!D{Š9Ýå%)èF!úÕÔÔ©T 055…@  ÌcBbx#„B!„Béƒ4e»€H$ //IA7BÑ¿¦¦&ÔÔÔ¸VhïzNyðPàB!„B!¤Ц3V±±±1 ®®®GÛD!D{Šk²âÝ™ë:!äÁA7B!„B!„^ÖÝŽV>ŸhnnÖEs!„è€âš¬¸Fwãéß(ðF!„B!„ÒÇtT–Œaë!„è‡âš¬¸F«[×ögBȃ‡o„B!„B!ýuÔBHÿC×nB>x#„B!„B!„B!D(ðF!„B!„B!„Bˆè»„‡¾›@!¤“nܸ¡ï&B!šK‘©[NeË!¤ïcYVe®7uËÚ[NéŸ(ãB!„B!„B!„ Œ7BˆÎÕÔÔè» „B: ‰ôÝB!„B!„e¼B:"‘ˆ:œ !„B!ý •—$„þ‹®á„<\(ðF!„B!„B!„BˆPàB!„B!¤u”ù@™„òà£ÏB\x#„B!„B!„B!D(ðF!„B!„B!„BˆPàB!„B!„Òc<¹qßÜBÜ7·0àÉúnN·…Ízƒ;Ÿ°Yoè»9„Bú}7€B!„<¶lÙ'''¥eEEEXºt©žZD!„þÀ@ Ð\_£çÖô=ö’H8H†Š®ŸAqúy=·HÃ7ßPÀýÜŸ šõoMy‘{<å%ÀåÝïê«I¡¡n_CµMÍ:=éŸBCC___899ÁÔÔP]]üü|dffâòåËHJJÒsK•ÅÅÅaúôé=rìƒbçÎ=rì]ÿþ¤#„B!ý‚@ €›› ¡¡[æââ+++H¥R}6B!}جí`x|ÀŽ8GµÛ ÍÑXW°lo6M§Œ-ìà2èQØù‚¹ØF"sÈš›Q_U†òÜt¥œFAò_hijTÚOôB§ýpeïû}2ðö 4û?šü‚Êòà)/ƒK»ÿ§‡Vu¸ p†Ïpo8ˆa.6‡ÀT€ÆÚFT—V#ïZ>²/ÜÆ­ ·Áö¡÷Õ#°pD§Çü6)oÿ™¨Óc’þcüøñ˜8q"œÕ®777‡¹¹9$ &OžŒ¼¼<üüóÏøõ×_{¹¥ªââ⇺º:­¶711Ô××kõ¾ž5kPð­ (ðFé3 M Ñ\ßܧnè!„tÞÈ‘#1mÚ4lذÙÙÙwww0 ƒ .`Íš5€·Þz H$8sæ ÀÈȳfÍBNNþüóO}!„BîáóùJ[ZZ:µ¾§ ̬4ñyø_ˆâ´søû³•¨+/îÕ6t—…³B¦¾ ·!“ÀÓ æ2?‡†j)Òýɇ7£¥±¾—[úpÓtSšò"À0¸ÿ½Øªö-;ø 審­S·¡©¾ à3Ü,+K•}ŒÍŒalf [O[„N AEa%Î}w)Ç®÷jÛÕY1t€ÎƒnðL¨Pðí!ãåå…E‹A"‘¿¦òóóqæÌ$''ãÎ;¨¨¨XXXÀÕÕÁÁÁˆŠŠ‚³³3–,Y‚‘#Gbûöí¸yó¦ÞÎá©§žBcc#f̘¡Õö?ýô`öìÙhjjêpû}ûöaêÔ©xë ¼BôÎHdÿñ¾ð烊¼Jœûâ"ª «ôÝ,B!]0jÔ(¼òÊ+àñxX¿~=vî܉ÀÓÓ––Æm›––†ˆˆàÌ™3 ÂòåËáìì ™L–eñ×_éëTHÀ7B`e¡)Íu5¨/ÉGKƒv#: !„tߌ3Àãñ2™ »wïîÔúž0a1WöÏ)t &½g>{¹—ëÕvtÃã#tÆ«šô‚Æ€[[S+„L{Þ£bqþë#÷Ò±n%€AqÿEФeÜãæ†:ä™#-õà˜ë+Á7Ïã 0áµqðŽòÒúbs<úJ $Ñþøùÿ~A}uCO4U+ÏÞ ºÏ+ÆÙ\Ý؇ºØcˆ³=ž õÃǧ“z¼ì$ǃ lll`jj ###@ss3ª««q÷î]”––öÉAéB¡b±––– …àñxÉd¨­­Eyy9ŠŠŠPSÓ?Ê>+W®ÇC~~>öîÝ‹„„µÛJ¥RH¥R\½z»víˆ#0sæLH$|ðÁذaNŸ>ÝËg@ú: ¼BôÎ{´‚¦l}l0á\;‚ô_3ûäFO[´h„B!8€üü|n¹››žxâ H¥Ri¢'...˜6möìÙƒÂÂB•õööö˜2e ÜÝÝ!‰ŸŸßÿW®\ÑCkõÇÊÊ sæÌ˲ؼy³¾›Ó)3fÌ€X,ÆÑ£G‘™™É-···Gll,:„œœ=¶°o=z4V­Z‡††ÌŸ?C† Ayy9ÕÀ„„„à…^À„ À0 ²²²àííW^y(øöáÂ&ll ƒPìÆÍéÓZma¤©Pzé¯^ Â9‰„²µ„Xh‚[•Õø» -²þŸbÄç!ÀÚ-,\ÚÿK¾>hçCéD6N°ó ÷¡“a.–LD¶.óÊ×8÷Õëz:ƒ‡GÛ [î¥c(¿“†à'–®ÿ² –®¸ zÀ½àÃàÒ®wôÒ^MLÌñÄ;“aëaÃ-«¯¬GʱTdÉFé­24Ö6B 4‚­—-¼†z"0F y€Ñ-̱ë§cßkߣVZ«¯ÓœÍ-Æÿ;{M7:Cœíus¬XZZÂËË FFFàñxàñx` ÃÀÐÐB¡hhh@zz:÷Jßø|>|}}áè¨Zê—ÏçÃÂÂpwwGaa!233ÑÜÜwçÍ>|8V­Zøí·ß°mÛ6Èd2nýâÅ‹1jÔ(òþ¹Òþ 8}ú4–,Y‚qãÆqÇ¢àio„>‡oÈÇÀY!p‰p~(³ß`aa_~ùEi¹™™BBBÔ|Hï˜:u*‚ƒƒ‘””¤òwðõõÅÊ•+add„šš0 ddd’““¹eöööpttDeee‡oooˆD"\½zµgØÚÚr*^»vMcç¡.Ú¢î÷WYY‰¼¼¼.]„mj­¥¥(++ëp///˜™™µ»Mcc#®_¿3¨î¤R)JJJ4NîØ×@[ ÃÀËË nnn°°°@uu5ÊÊÊÝgF‡í°2 OþZ²õ±Á„·cpíàÛýFú??? 0R©§NRZÇãñ0oÞ<a÷îÝ8~ü8†ÖÜ’¾íï¿ÿÆÄ‰„€€¥Ï@"çîî‡ììl¬[·ðÍ7ßàÔ©SX¾|9¼½½‘ššªôž¨««Ã•+W0pà@;v _~ù%Wšäûï¿ÇäÉ“add„éÓ§Sàí!`á¯éσѲÔ•ûO]ðùØ6nÚ[ã‡98•W{¡ ¬-°ëúƒñ¥zˆØp¾°DÏ-Ñ }ŸÏ[+ì4 <†Áë§q0óáÈ’æ1€X$„‰vBcÔ7ËP\[‡¬Š*f†.\¸o¾ù&,X€ØØXhµŸ££#vïÞ WWW àÛ,õ§O`&ö€kÄcäs§=þÎ\Þý.RÙô‘ï”a±¯+Ý®\‡«Ö‚e;¸Æ²¸}î'$'`ÔÊ/¸c˜;z÷ds;ÅÜÑ Þ£fáòž÷´ÿ}3 Âb_Ç?ãQU¨ß CkgüS5è¶~dͪó"Éš›ð×úE*Á·–¦$íû°×ÚÜ;/ùg$+cqä½_‘q2³ƒ=€ÆºFœÜv EéExì_ãÁð8ˆ57§¾ü»§›üÀpqqX–E^^žÒàÅýlSSjjjŸŸWWWxzzÂÍÍ ---z«º¢T+P]]””ÔÖ*g;677£¾¾¥¥¥¸}û6‚‚‚`jjŠ\ºtI¥OÄÅÅžžž¸víš^úl-Z‡ß~ûM%è€Ëtk»¬màMa×®]077Ǹqã°hÑ"üûßÿÖy›Iÿ¤“ÀÛ[o½ÅÕ£m+++ ;vìÀÅ‹•–Ïž=ÇÇÒ¥K‘››«ñØ«V­‚H$B\\·läÈ‘˜?>±|ùòv÷_°`1uêÔNÓ¢E‹0b„<Ýÿ?ÿùÆl]´EÓïeYáôéÓˆG}½våmR§®®7oÞÄŽ;4²æÍ›‡ðððvŸ£  ‹/îðàÒ¥Kصk—Ri) {¯Ö¢¢¢ðÜsÏÁÖÖVeL&ÃÕ«W±qãF””< ºô_3ÐÒ$CàD >¾ÑÃýFúŽI“&Ž9¢R2A,ÃÁÁR©Ç ¿†÷¯IÝ{ƒ““bbbPYY‰~øAßÍÑšL&ÃÏ?ÿŒyóæaÒ¤IxS#>>Æ ƒ§§'"##qîÜ9ò{ÁU«V!((Hí½Çÿû_˜™™©Œ†\ºt)ŒŒŒÐÜÜŒõë×÷Ê9ýXÛÃãÉgU‚n- u(»r U9h©«ßD¡ØV’pÛ9õx»^ “ ÌÞ_\ËÄG5ëÏ"å÷Ðç JõÜÝÐ÷ù w²aÐ"c‘\úàt5ág{ s²‡…@õû`aM-¶'ß@úÝ <߯¿þŠ ÀÍÍ ñññ˜5kV‡Õ Äb1t»s玣ù°:kß¾}ÝZß›ê«î"q×Ûð~sßÀ<#„Ïy N¡cpú“¨+/Òk-]øøsÜã”Ã[´ÿ£N£±¦|ü }óØx†èº‰]fîè…qÿ9¡•S+œÝþZÇÁ7†ÁÐEÂ7z¼FÌÀ±w¦ö™à[^Ò H{†Æ¦È»ü»Æ ›‚"ø6úå/ღújä_9Ñ‹-ÖÎÉm§´ ºµ–þW&Œ-Lý‚<(>- É¿¦¢<ïÁÿ¬ë.kkk888€eYdee¡¼¼¼ÝÁc,Ë"''µµµ ‚——7÷[o †@ ÀÝ»wÛMLQ¨­­Ebb"‚ƒƒaccƒàà`$&&*ígll „„„àêÕ«½|?~<$ òóó±mÛ6­÷ëh°ß¶mÛ‰D‚ñãÇã×_ínSÉ@g¥&ËÊÊðÉ'Ÿp­¬¬ˆÑ£GcõêÕxûí·qáÂ]=yZêªU«ðüC§å¬LMM1tèPäääÀÍÍ £Gî°LXwÛÒö÷ggg777 8Ó¦MèQ£°~ýz$%%i}Ìï¿ÿ))òr ÆÆÆpqq»»;"##ñþûïãøñãØ¼y3ššÔß4lذAcÆ¢º `ii)¶lÙ@^ß×ÚÚû3o¡¶¹I%w‘!­ÔKzÓâ~ð°0Õ¸^,bY¨?^=y-ÝÿΞ——‡™3gb÷îÝððð@||½©©¾ †BC ™=w-yÉùì%—ôãUxGyÂ}x<„=‚?¶RUŠöðx<®¢Z^^^§‚L¥¥¥¸uë¼¼¼àçç‡sçÎõj_˜““ÌÍÍQ__ÔÔT­û»e2RSSSSS8;;ãÎ;Üú7n€a¸¸¸ôzðmâĉ€½{÷j<Ÿ¿þúK©Ô$444ÀÂÂêÉd2ìÝ»+W®Äĉ)ðFè0ðVWW§2Ê/¿ü‚³gÏâõ×_ÇÒ¥KuxKMME`` ¦M›¦ÓV#GŽ„¡¡!>ÿüs.°³uëÖvË>v·-ê~`hhˆØØXÌœ9ÿú׿°téRoò¶222ÔÓ××+V¬ÀرcQ\\Œï¾ûNíþ/^ìÔ…¯¾¾^åo|äÈÌ;±±±xüñǹÀœ.,Y²QQQرc8 vÒÎ?ÿüSgÏGz—ôv9޽uAS$˜$Ï߈°Ù!pLÙo¤w 6 ÃàòåËjKã@¿œÓŒh¯©© —.]ÂðáÃ1|øp­‡&&&ðññÁµkÝ›ü{РA¸zõjŸž¤:99GÅc=†… bÓ¦Mj·SŒT÷ÅQ$aÙ2ùœ©©©8pà@Ï5˜ô K;Xú‡)-ki¬GÖÞÍ*A·Ö*2¯¢"³såä;c˜“,F¸RrÒzýÌAÑÓÂlÁg$•HQÓÔw¯-Úê çSZ×€oSž¹–j›šqCZ‰ E¥È­ªEY},FxÜÓ!vÖ+c\MEȪÐͽ{AAbccOOO.øVZªœåhkkËm“Ù³gÓ\əʂ›8úæ$„Nÿ‚¦¼†áA`fѯ|…Œã;¸s5šz·,¼ÀnC&q;1ÿ—oôØûép»úJý ¤¸ð훘ÙÀ#ê €ßØ¹ËâÜWÿRÝø^ÐÍoì\nÑ­3?àâŽÕ½Õ\­”d\@IFçú3[š‘°éùŽ7ÔCcC {f(÷øô×gµ¼ÀÙçá>È €??M«Ã’Âkkk¢¦¦¦KŸA999‹Å …°µµíÕÁŸŠòÌ7nÜИ´¡Iss3233¥ÀdfÊ3.{3ø gggäçç#!!AãvŠ’’Šà[EE,,,ðÆoàÝwßÕØ/Ÿ€™3gÂÙÙ¡¡¡Jž!&^O?ÁéÓ§‘›› XZZêôØß}÷rrrðôÓOÃÝÝ]glj‰T*ERRN:‘H„ˆˆ½´¥©© ;wîÄáÇaff¦TÞ±«233±zõjÔ××cÆŒpssÓAK5ûã?ÈGJèJxx8¢££qâÄ ìÙ³§OwB’®“µÈpíûTü¶æ¤9÷?€Ùoþãýz|nm™˜˜t¼Q;ûvõ<†P(ìô~<fffàñ:÷1Ðóì < ÇŸgРA R6ùAc``À;«+¯ÃÎêîûLL¯ m,[¶ /¾ø""##»ü¼111xþùçñÌ3Ïtù=ÉÊÊ QQQX¸p!üüü>ú¨Ê½Ÿ££#Þ|óM|ÿý÷øþûï±zõj•yi===am-ï¬-//Ǹqãàë뫱¤5éÿ̽ƒ€6Ÿƒe—ÐX©ß,âÁ÷æ »Pø`”`TGßó¡éÚƒv>ýÁÆË©xûl~ÉÎõR)ò«k‘ZVŽÿwùºÒàŠFN*,,Dll,nÞ¼ oooÄÇÇÃÆÆ†[occƒøøxx{{#++ ±±±të>Ÿ¯ô¯³ëõIÖÒ„Ë{ÞÃoÿ75e÷ƒ ~cçb⻿ÁÚs@¯¶ÇÎo0ø†ò{šò;i(»Ù~u¥ÎJ;ú2OìÔé1;ƒ•µàÔ–pëÌýj~1Ï rÁû`˜ûß=†‡Èï«ÝNmy¬¬ïePögçvé6 "?µòÀƒ@dÄÍGÔ³°Wq*..îR¶˲ÈÏ—_»ìììtÚ¶ö…B…B466ª lÑÖÝ»wQ__ccc˜šªfÇgff"77|>!!!:´&è§®ŠQ[­çs{ùå—qãÆ øøøà7Þàþ¦ê(Ž­x.òpÓYÆ[{ÊÊÊàââÝ>]SSÖ­[‡µk×bÕªUXµjU·K¸ººÂÏχ‚L&éS§ðä“ObÔ¨QøûoÍ“†öD[ZÛµk¢££¹ÉµÍzÓ¤¬¬ û÷ïÇœ9sðøããÓO?ÕQKU):ÏÒÓÓuvÌùó磩© _~ù¥ÎŽIú.iN9Ž­9ŽÀIM è3Ùo¡¡¡;v,ÜÝÝ! Q]]ÌÌLìß¿ÑÑÑÀêÕêGë9::â‰'ž€‡‡lllÐÜÜŒ;wîàðáÃõ•` IDATZá|}}ñä“OÂÝÝååå¸}û6~üñG“îÚÚÚ"&&aaa°´´Ç˲¸{÷.RSS¯vÓ€¸ó455EEE²²²°oß>µ7`K–,««+Ö¬Y+++<ùä“ð÷÷ŸÏGbb" 1jÔ(k̆äÁ GGGìܹSéúÁ0 bbbWWW¢¤¤III8tèÚŒ´ŽÚ´s§æ/§€+ ‘•¥<‚ÝÇÇóæÍã‚:îîîxçû£Voß¾ÌÌLÄÄÄ ¦¦ï¿ÿ¾Úç˜7o\\\PZZŠÏ>ûLí6‹/†››öïߤ¤$xxx`Ñ¢EJ¥X·nÚ}žyæøúúâÇTÊH600ÀêÕ«qùòeüøã?~< ±X †a——‡ÄÄD9r¤Ý/¡¡¡ˆŽŽ†»»;D"êêêžž®öF–ÇãaÍš5\ ÔÖÖVéwuðàA\¾|Ye¿ÈÈH >nnn‰D¨ªªÂ;wpôèÑçYëξšÜ¼y,ËB,ÃÄÄDe¢hu~úé',_¾ .nî3mEGG#66ÕÕÕ}r^šO?ý...JËX–ÅÍ›7•F-ÚÚÚâ£>RúB5xð`H$¬ZµŠ+ý•’’‚k×®!88Æ ðaøcæääà…^è…³"½Iä쥲¬'3Ù:2Í׿¦ï.¿ö{[šã•ð nýç×2PÙØÄmw$;×ÕÌ_5ÐÎcÝ‘!­Äá›wTÖkÃI$Äl‰'ʱ=Yý\,ËBýab`€¯Sn ¬^õ3ƸŠ1ÐÞÖò2Œiw+ðuÊçC3æóñ˜§3m,áoeŽ‚š:\.¹‹}é·ÐÒêóa¸“=†:Ú¡°¶ß]¿©öX €çïµu{r&ʺžEØWÏÇÝ\„é¾(®­ÇŽëê³ÞœDBŒv#Àƞ榸[߈4i~¸‘ƒüêZ•í„&çîˆ@K¸˜ ‘UQ…¿r‹ð箑‚m­0ÔÑÖ–p£ ¦‰EeøéæTw2S°®Yýw^KÈïi¥õ È«®ér{5)..Fll,víÚ___ìÚµ ³gÏnYff&fÏžÝåNC"7cÆ nÀžL&ÃîÝ»;µ¾/(º~?ýs "}Èec™;zã±5?áòîw‘zDý=¸®Y8ùr?§wîžðÆŸñ(HÖ\Ö¯¾¼Çwt¹mº"¾½ðˆz€<øÖ:ÏÜ|̬¹Ç·þþ§¶¾DA·pao"ªJª`é¬>¨‘w-¯ÓÇÌO-„…ã½iA'++ <NNN=žùæë+¿þª›w¼=x÷ÝwñÆoÀÇÇ[·n…±±1®_¿ŽmÛ¶)õÁ%''cÚ´iÜs‘‡[Þ àææ†ŠŠ ”•é>ÝýÆسgââ⸛îî;v,€û% ÓÓÓQZZŠ!C†@$qs•ôF[Z«©©Áõë×1dÈH$’NwÚ©sáÂÌ™3:h¡z†††˜:u*jjj¸Ì·î²²²âÊ…ôæœD¿Ø)‡®#/1‘‹ÃÊ]~Ó¨¹ß†ÁìÙ³1fÌòR±999ppp@XX –e5ŽöŒŠŠÂœ9s`ddÄÜ,--áéé‰åË—wøüöööxíµ×À²,ŠŠŠ```[[[XZZ"(({÷îUy¿¹ººâÕW_åÙÙÙhjj‚¹¹9 ‘HT‚n Ã`úôé7nX–Eaa! àììŒAƒ!(([¶lQ `ØØØ@,#** S§NUÙTWW‡äädÄÆÆB,ÃÑÑQí<ööö CuuµR°K$aÉ’% Dcc#òòò¸›´qãÆ!,, ï¿ÿ¾Êà„ŽÚÔ0 ƒ²²2•›eCCC˜››sK `nn®ÔÞŒŒ Ì™3‡û;´-±`jjŠaÆÇãÁÃÃT)! ¹Ìë7nÏ-*‹ÛÔbeeÅ•¥hK,#::ƒ ‚ƒƒêëë‘——¸ººÂÕÕ‰[¶lQ™×“aÌš5 ÑÑÑ”ßÄÀÕ¶ÇÜÜœ{_ðù|¥ßUÛL;cccÌŸ?áááä_6***àèèˆÀÀ@à÷ßÇÞ½{Už§;ûv¤®®ÅÅÅppp€‡‡‡V¼ôôtlܸ±KÁ·Ñ£GcöìÙ¨ªªÂÚµk‘—×ù/Æ=ÍÁÁ””„ëׯ#-- ééé*ï—gžy–––8}ú46oÞ x饗0lØ0,Y²kÖ¬ ÿ‚öúë¯ÃÄÄþþþH$H$×iö|G"##±råJÈd2lܸçÏŸW»ÝðáÃñâ‹òÎ¥M›6i¨5tèP¼ôÒKàñxزe N:Õcmïo Dæ*ËêËô7Ó+A°1¾ŸMí*F´«|¾i}#Ö%¦(m÷ý õ^¦ù¹c¦ŸÖÞÛ¾+F»Š±$ÄG²sÕ®·cÅ @Ô57cãeÕë‘¥ÞŠÈeï)Œq#Öß#´ÈÔχ6ÂÙ«£Báj&°›žôqÃxw'<÷ûnÎ.3!–„øáNUÆ@Õ ?¬ˆÃ7ït9èÖ×Ïgœ»–„øaÆm•í y<, öÁó¡˜(ß#Ž÷p‚¿•9Vüqÿ:Ãg<à…•ƒ!24àÚéh‡8‰¶&¥cã¥T-sr6Ƽ:8Où¨V<™âí >Ñx¾ajd€çBüq/·ÐSUÈJJJ0kÖ,ìÚµ þþþøî»ïÀ0 üýý‘žžŽ¸¸¸é‡ ýScm%6-EÞ•ã²à]›‚g`„ÀIËz-ðV~' —âÿ(ÉPo¡IÖ_}/ © |cYx { `l~?+•‚n½+íÝÎU[s÷~ߨ±¹±Ný 144˲íN_Ôž#GŽT¬­«þV…Q£Fq?«ØÜоÅwºö(2ßNžì™yß%ÛöÉhC|SÝ K–,ÁþsNJű{ó{ëƒæAúÞ£7@€%K–ÀÊÊ _~ùeuˆïÝ»‘‘‘˜9s&Î;§’‘ -‡èèhäååqµfY–EBBžzê)DEEá÷ßï•¶¨£èlSdu—"UYÓñ^~ùe ß}÷nݺ¥´ÌÖÖ–ë4Spqqƒƒ>ýôÓ.]ØÔQ”¤ÊÍUí€000P›¾Ì²l·³IßPž[coGÀãþ~"<ƒûÙoAö8¹®ç/ Ã‡ǘ1cPWW‡o¿ý‰‰‰`Y<AAAX¸p!LMMÕŽ:rppÀ¼yóÀãñpäÈ>|˜+•êää„gŸ}–«£­ ÇÃ… °sçNnÄ••,X€€€Ìš5‹KÙW˜;w.LLLðçŸbïÞ½JA6@+++•ç:t(}ôQäççã³Ï>㮘:u*Ƈgžy«W¯V{­xæ™gpçÎìÛ· 544 ¢¢™™™ðõõÅ!CpèÐ!•}%ùΜ9£TJvúôé ÄåË—ñõ×_sçonnŽgŸ}–;MYcšÚÔ±XÞÙª®<Ñõë×±bÅ DFFâÙgŸEVV>üðC•írss¹Úåm¯…aaaàñxhll„‘‘"""ðË/¿(m3`Àðx<¤¤¤´;¤³ø|>Ö­[‡´´4°, †aðÈ# ..‰&LP™ËlذaˆŽŽîð=КL&Ê+„•+W¢¨¨ÿýï5¶íÉ'ŸDxx8 ñé§ŸrŸÆÆÆˆÅ#<‚qãÆ!++ ‰‰‰:ÛW………ppp€X,Ö:s.##6lÀÊ•+µ¾5 qqq¨¬¬ÄÚµk¹÷`_õÎ;ï¨i[S”ÛØ¼y3”Û´i† †°°00 £t¯XWW‡+W®àÊy ¦ýû÷÷jùªgŸ}–éù /h¼áñŹí^zé%7üË–-ãÊ’¼ôÒK8}út¯NŽÞ—ñª¥`[5¿–zŸa°øØßˆr´Ã«ƒƒ‘ZVŽÿœ¾Ÿ[×Üò±¥u ¸©aÞ*®ì¡†ì+mp\Ê]FŠå%‡‹ÊÐÜæ¾#ØÖ »#>ñiÙ8|ó²+ªáci†XOLò’gª&•ªÎ‡6Kâ‰5Q‘)­ÄÒßÏ ±¨ 2–ÅWG¼3| †9Ùc¬›#~É–__oUÊ?›ì…ê;߬Œðˆ ”74âÝs]›ï²?œâïq®M©KC_ŽÁb[\*.ö«H—V¢¾¹îæ"LðpÆ®´l¥}>I^.8‘S€­IéÈVÂ\`ˆ§%^x>ÔÏ…ø!>í&Škµ{¯8‰„Ø7y4lL8˜yßßÈAVyŒø<ø[[`„³ö¤ßÒêXmÙ XYBdh3!†ˆmal`€™ »Ò²q:¯¸KÇÕVYYfÏž]»vqƒJÓÒÒ‡»wõ[²–ôUúûü-L=ÂÔÓz{þÞÄÊZpjëKÀßÚ¢ [ÿÔ:îÓW¦ÿ }Sg_=ùýHÑGÑÕþኊ .è¦Ð6™Eqlu}ÓD;Ò÷pÞÚ]D"\]]!‰ðË/¿àûï¿×ÕS©hnnƺuë°aüüòËX¹re—æü ƒµµ5¾ûî;¥å§NÂSO=…Ñ£GwxÓU[ÔQdwuvN&MêêêÐÐРñxžžžÓ•ÕeOðù|¥Î{†a¸”ê¥K—ÂÉÉ Ÿþy·_ÜŠçV×Y.‘HÔ–r“J¥˜;w®ÊrÒ?±-,R§¡àj!bþ=|#yG¬…“êˆy]xê)ù—†¯¿þ—.]âÖÉd2\»v ›7oÆ¿þ¥fòhÈ˰ðù|œ8qB庘ŸŸ>ø~øa»óe•””`Û¶mJˤR)Ö¯_×_žžž˜9s&W~ÐÐЛ{òèÑ£*™m *A%###<õÔS`YÛ·oWêðonnƾ}ûàããOOO„‡‡«--xõêUlÛ¶Mí{5!!¾¾¾ˆŒŒl7ðÖz$Š››†©TŠíÛ·+·²²_|ñþ÷¿ÿ!""ûöíSÛÑÒ^›4Qü-ºð:þ<\\\ŠŸþYiÝ AƒÀ²,öï߸¸8µ·ÐÐPèR¨#}ô‘ÒïJ1à„ÇãaΜ97nŽ?ÎJºûІ³³3ÆŒƒÆÆF¬]»V)»¹¾¾ß|ó x<† †™3g"))‰û¬íξÚR¼:;¯Ý7°~ýz­‚o#FŒÀÓO?ÍÝÔe†ò hiPÍ<æCÖØ½¶]j Ë"¥¬ïqNå#¥LµÂÂ{Ms‰9MàanŠºæf$«Ù_[ƒ;Þi ÌÙšcëØHðxX|ìo$äqëÎ6àbQìLˆt´S9‡!b[ü72)eåxúÈI¥R‚‡oÞA€ûb¤³¨Ê©¬ø|˜¢²Qù^ãÕˆ`XŒðÚÉDÜÕP³=ýá|ø<áòl޶ó*²ô¾¸–‰/&+uùß­oÀåbå{–¥!þ˜ä傸´l¼uæþüO%µ-Øp)#œílk…áNö3.[31à㓘¡°66Âó¿ŸÁmÊTÔÔu«tå(g1¦´É¢«lhÄ–+ij˰öØ@Ô1š+•šäsÀ¥þü‰[õ`SßX–…çð©Jë²OÄéO–SЭY‹¸Ÿë«ô3Xª¿hjj‚@ €‘‘Q—³ÞÓ5444h5?Ywýñǰ··GPP÷Ü]¥Ø?55EEE*놯¯/œÑÜÜŒ«WõWn^ׯ_W ¶uu òpÐM÷ƒ.ŠNNN‰Dؾ};¶lÙÒã7¾·o߯Î;ááḸ¸.#&&€¼« ÷¯¬¬ ååå U›ÒmQG‘u¡«òŠ–––#ýË—/ÇüùóÕþKMU-kRTT„åË—sÿ^zé%ÄÅÅáµ×^Cii)¦L™Â•òìE‡gÛùdmرc‡Ò¿î¦E“¾ÉÚà Cæ‚n€^ø‚ííí sssäçç+ZÓ 144DHHX–U ¾(444¨g­5MóG²,Ëóüýý¹„ÍÍÍ\9ÅqãÆi¼÷ññ••rssÕÎDz,wþŠùÏÚ:{ö¬Æ÷ßÅ‹Q[[ ;;;xzz*­óôô„ƒƒ²²²”~ááá`‰‰‰j[YYÉe±u¥Mš(‚+ÚÌå¥ÉÅ‹ÈËV*Fä(Ž€¬¬,œÑÜ!·uëVTUU¡ªªŠ+¡©Î'Ÿ|‚ÊÊJTWWcóæÍÔ)ÜJsM¥Ê2cG=´ä¾Hq™fd¢ ¹·^]&š¶|-͵Ȫ»l˜{}È8M°>1U)H¥ cY å÷ g[íËg|02<†Áª?/¨¿KÑ–ÚVë kêPïþÄA¨|m ³·ÆT_wœÊ+Æ¡¬ŽƒDêô‡ó ²‘g|åTÕ  æþ=ÃgL÷sÇÙ‚¬MLé0ÏÆÓÂ+Ñ_]‹wÎ%©ÝæfEõ½6k7päµÁÁX[`Ë•4• ›.ø[[¨\ÓÌFx}H¢ÝÄ:¾¶lll‰D‚´´4¤¥¥! ñññ°±±éø¤CûöíÞ={°gÏìÛ·¯Óëû‡€(Lzÿ„RЭ² ¿¼9©?÷Üœ÷D|;ýÉrdŸ>È-£ [ÿætÿ>­ô•ómOë*=]¥è‹îÍJ^Š·­ççî,†a¸¶WWW«]ß6èÖ“ç¨hƒ"û©+¶mÛÆÛs¼µ¦8¶ºó%Úy¾‡ë,ãMtQ Á»ï¾‹˜˜:tH%sJñ¸½¹i4u2·uðàADEEaúôé8{ö,22´¯a,‰¸Î¯÷Þ{Oãv#FŒÀ?þØ£mÑDQVQSWÇÓEÛÚ“ššŠÍ›7ã­·ÞÂøñ㹬Á®¾òóóÁ²,\]]UÊR•””`Ïž=Jû>ñÄ Ÿ‡ '8Q†?e½üNÎ}q¡ÇŸ_ïJéT{{{0 ©TŠÊJÕNF]ÈÌÌ„L&ǃ À²,Nœ8É“'cìØ±ˆˆˆÀµk×píÚ5¤¤¤¨ D)Jº²,«1`®¸†ØÚÚª]ßž¦¦&œ={ÑÑшŒŒDvöýòJC‡•ºËŠ6YZZjl“"Ò•6i¢¸Fuçù¤¤ÙÙÙðôôÄ€¸øÐÐPðù|\¸p---¸xñ"F…ˆˆ9ràçç\¿~½WoÞZZZ““KKK¥àTwÞÚRü­Õ}¤R)Š‹‹¹¹å$»³¯¶Ÿ_mç¥ÓÖÍ›7±~ýz¼üòË*™oÆ üyóP^^Ž?þÅÅ=[žKòóóáææ†ÐÐP.;“eYܺu +W®ä>ÿùæ 8#FŒÀˆ#¸ýËËËñùçŸ+ó£>R;!uoþ>Μ9£ÕˆÒ„„$$$èìx£š¼›°¥´ÌÂ7U·ÓôÒSCØXjœ+ŒAÇe$5îÆ{8ÃÍL¤nT44boÆ-î±"x×^V»¹uÍ͸V&å–[a¼‡*›ðmªú²÷¦†TsŽ£\Åp qýnì…ÆjK-´“—©/®½\bÜ©¬¯•9ì…ÆÈ,—ßçðy Ö ˆúæ¬>£ðø†™þ ‚§¼†¹ÍÉ<±w¼‰f5Y×D÷XY No} w¾h¨,Ëvm`J_‘úÓ'Èøý[@s½î¦èIƦÌŒQQÐõà†s°ÌäA¤úªzßPŸBä***`ee{{û.Ï9ªøŽÛ›ß…êêêP]] SSSØÛÛwé¹mll PWW§28··ƒn€ü{«¹¹9\]]!•J;ÞAœœ¥9ÝÚRLÓ×§‰èˤïá=6ÇÛÕ«WqæÌDEEáñÇÇO?ý¤´^QÖÌÑÑQe®0†a`ccƒôôt­ž“eY¬[·›6mªU«”9r$ŒŒŒ v^6‘H„3f`ôèÑZÞºÓuœœœ„ŒŒ ]hÇ@µs»'$%%A&“)eqtõ5 ˜ÊÏÏãÇÇÑ£G{´í¤ï°tµÀÐŃaévÄ ÛÂ"õç4¤ºYKÏ߸+^Ã] œ)öUŒê ÍÍ͸{÷.lmmaeeÅeÊüøã(//Ç„ `gg‡Gy<ò‘˜˜ˆC‡)ÝÚÙÉGл¹¹ÁÍÍMís)tµ\BBB¢££1xð`ìÝ»— < ¸pA9ªhSDD"""Ú=vGYƒ¡Ènj[Ë»³ÎŸ?¯x WÊ<{ö¬JàmàÀz¦ÌdG%([ÎëÎ{@[Š@_GÏ¡žµ…×}µ¥ðv' 2;;ëÖ­S ¾1 ƒùóç÷«  ¯».àîîΕŸ6l<==Œ¤$yÆFYY^~ùeÌ›7›ïíòåËøöÛo•:DMMMáãã@ž!xõêU\¿~ׯ_ïò$Ò·UÞL‘g­·šÂfà#(:ÿš*{ÿo!¶Ÿap¥ô.j›T3Š|,ÍaÝL4G‘úlÙ#ٹʷ²îZgÕµÈît¦ú¸ÃÇÃìÛhÐÐî þgù{¬-°ã±j÷Uø;_¹£íVe5¨Rx&ÀþVxïü5äVÕrË]ÍDx}ÈÇ^ݪ¼b8ê¨:¿›¥‹mQXS‡³wLñyxÊG^|ª¯;¦úºkܶ²± É¥WByÂÛ BC¾yÕj^Ϻ²@eC.ßEfy¶FÀχÐwªtß)mooøøxx{{#33SiN·¸¸8ìÞ½¾¾¾Ø³gfÏžÝo>W‰î˜;zá‘¶ÂÆ+”[ÖP-Å™ÏVáNbßîG°v §ß„½ß´47¢0å4®ì}yºˆ­,+C}Ѥin¨EsCmÇöææ2;Ñþ(¾Q‚=«öwí@ 5÷~ÅŽô?2ÀʨŠC{¤R)œ! !‹ÕÎßOOO…BÔÕÕõú ’ÜÜ\H$øøø@*•vªŸÅÀÀ€H™››«´NA7@>P]"‘ 88¸ÇJZsÏEHÞyjààÁƒ1gΜuµ-m1 ƒ%K–€ÇãaË–-]>Nk‰ÑÑÑøûh=‰Ïçƒa¥¬‚î¼¾øâ |ðÁ˜7oΞ=«³ò›¤obø 'I4%<þýQŠŠ,7éíÞûû+‚fÖÖÖ½ºogˆDò‘ô%%Ê_jNž<‰“'OÂÙÙ!!! ··7¢¢¢ˆwß}—ë¬P¼§nÞ¼‰¯¾úªÝçS”Nè¬ÜÜ\. , ))) ‚™™T2ñÊËËáââ‚ãÇãÏ?ÿl÷غ¼&t§¬`k/^ÄÌ™3>ŸCCC!33“koVVJKKáêêÊÝ”‡††‚eÙ)3ÙE¹ÀÖ¯¥Þx+2û:zu¯õîì«-E¶;7@þ9¸víZ¼òÊ+\ðM*•âã?îR»ô©¡¡ÈÌÌÄèѣȷŠò¯ eeeÜü“šTWWãØ±c?~<²³³±}ûöžj6é#¤%(ϸKÿ0n_`ï/"kÏÿCSµú/â>`æ€Üß÷ê´=e³u”‰&É3Ñj›”3Ñø ƒÿœÖ|-¿Su?«Y›¬º¡÷‚{gÛ¬¶•(È*×<ÐGݱòå^HÆ­JÍYÖ,€k¥ÊAÑÛ•òÀŠ"På 4Áò°$—J±£M¦ZMS3S?:Ú»?œŸÇpÙiç[JG8˃d–WvXbü¬,`!0Dc‹ +ÿT?‘¼‚´¾Q«2¦#\ämH¿ÛsfÚj‘É cYð2 i”þ‹Åˆ‡§§'²²²0{öl¥Adeeeˆ‹‹Ãž={àííÍß:ÛáIäø|¾Ò〰³i IDAT¶n­×Ÿ1Ocð3oÃ@pNÞ‚ä“8ýÉ ÔIûöëÀÂÙãW‚±ü~•od ·ÁÁiÀHüºæ ܽ¢çvÃôʽ¦ŸœÃAÊûSœ‚:y’_ëôq=9®åÓ¾47¶ ñà•ö 2™ ¹¹¹ðöö†³³3´Lhkk 777°,«—@Naa!aaa   \½zU¥¢:<AAA066FUU•J…}Ýù ÏÉ“'#** »víjwÛÅ‹+ýܶB‹&QQQÜsÒ£·‚‚:tÓ¦MÃܹs•‚FgÏžEaa!ÆŽ‹ÄÄD•Ô@ ,[¶ uuuZe˜µöóÏ?#** O=õ”V¯...ð÷÷Gbbb»Û'$$ 66£FÂîÝ»{¤-m9::bÅŠÆÔfãuÃ0˜4iæÏŸÚÚZ|úiÏ×2gÓ¦MÃ08þþÈî¼RSSqôèQ<öØcظq#6nܨv®!‘H>Ÿß'nüI×X¸˜cèâ!°r×_–[kŠ/ënnn*¥N;¢˜HÖÔÔ”›?R׬¬¬`bb‚ææfÇÏËËC^^~ùåîcccƒ‘#Gâ‡~p?-ÞÙÙ¥¥¥ç뮓'OÂÓÓC† AJJ WòW]ºx~~>‚ƒƒáèèØ«&ŠàJwj²ò` "[××׿ææ000PüÀ²,Î;‡‰'"<<III°±±AZZšJ—↷»Áö(JZ´þ}+^Ç]yh«°°ðððи ÇãÊ^¶.ãÐ}µ¥x-t5èÜZNNÖ®]‹åË—£±±ëÖ­ë×å°&Mš„äY,[·níò|ŠÁ[1118{ö,Ξ=«Ëf’>(ïĘy€ot?»H(vCàÒwPz%Õ9h©«ßX¡Ø –’A0±wAE†î;|:šßmð½`ÎÕõ&Š Pb±r&Z Ërå;âeikcjšš5gÕiÚß›“¬²Qó¨dusÔ™ Œ º7OZbQ®”ÜÕª­ ·ï¶ÏÿFäø|üûôe´´ù¬¸[ß õï¢?œO°%„†¸]YƒÂVó»)‚vÅZ”„§VÙÇst3¿§Ã½6”Ôi×mX›ÐÔ"C•š¿ â$^àñ䬧òŠPݨÛûHGGGìÞ½îîîÈÎÎÖXJ²¤¤³fÍž={àééÉ•ì/s§ö%3fÌàæ‰–Éd*ý!­ïMÆfÖ½êK¸F<Æ-“57âòž÷zä³~$ þtkÍÀX„ð¹kðÛÿM×C«ºÇÚsÂãÞ„½d(XY ®þ‰‹;× ª(»ãû þv>…•¸üC§É9Z:Õe5Èú[ûD‡À F>÷÷øÜwç»U²òa"•JQPP'''x{{#??¿ÝÏ"†aàââ///0 ƒ¬¬,ntobYÉÉɇ•• „äädÔ×k¾§066Fpp0ÌÌÌÐÐЀk×®©ôÔÔÔôzÐ WcËË˃³³3FŒ¡±LáâÅ‹1aÂî±â玂o#FŒ€““òòò¸ª/äáÖ£7سgÆŽ‹ &àèÑ£\ਡ¡7nÄš5kðÏþãÇGZZªªªàää„‘#GB(bÓ¦M\'Ÿ¶X–ÅÆ±yófXYYuX-&&ðÇ´»"ð6zôh­o$µm‹••–.]ªôØÝÝNNN¨««Ã¦M›pìØ1­žSáÑGEPPù<4ÎÎÎpww‡™™±eË–v/ÜóçÏ×x1­¬¬T`ii©4"€ÇãÁÌÌ ~~~prr¹sçpèÐ!n}w_Ÿþ9ðÄOàí·ßFFF²³³qçΘššÂÍÍ àóùܼr¤ÿ`x ÷Gð“àè7Ë­µììl´´´ÀÎÎC‡íTપ*ÁÁÁ“'OÆ×_­v;¦U©-u 4<%dSRR´‰TPP€sçÎáñÇWšÇëÖ­[hll„@ @LLL•t½pábcc1hÐ ìß¿aaaÈÏÏWšóM!##>ú(Wê m&MOQ”Eprrêv éüùóðóóCHHlll”ÊL*(oƒæ–©+3©(—$‰ºÈ æJ_¶&‘Hàì쌪ª*ܾ}›[ž ÙÿgïÎã¨Òý{M'ݵ³od! d'„’°È*‹ˆ²)£*£Â8Žãpõêu¼ÎèOFÆ…«   ""»ì Bö$dOgïå÷G¬2MW'ÝI'ÀûyžÇ9†œƒDü[äF¯«/‡!ØÁYµ (nV¢¶­­ k Æ»9Áùü¶wbO¾yÇ„õôôÄöíÛáííâââ>SHVWWcéҥرcFÅö|3×w8~ÆÌ}RçÿÆò\œúÏ“#ª—˜kH\/óâÁã  ÕŒœÆÅN~˜ñòÞßׄð7ÎÁðãÍ„ò–á±™‡£‘ZŸ_¿8 ïH/¸:C àÞ¿ÏÆù¯/âüö Puþ<‰mÄHüC<¢æÿž²µêF.îÒoüN «¨¨ŸÏ‡»»;¼½½¡P(PUU…ÆÆFöY±••ìííáåå¹¼û{»¨¨H/UãPêììDFFÂÃÃ!—Ë1aÂTTT ººJ¥’2D.—ÃÕÕnnnhkkCff¦^&# û>úÖ­[ý¶d öïß5kÖàÀéÓ§9Ÿ›%''sNë-ðÆçóñÀ°Û ¸G³6£ÖÖVlݺ<?þ¸Î¼«W¯âÉ'ŸÄ¹sçŠ%K–`õêÕ˜>}:òóóñÌ3Ïô;`R]]mT7P‡)S¦ ­­­Ï‡‡ÌÉÎËË f-‹T*ÅܹsÙŸ¨¨(tvvbÏž=xòÉ'qðàA“öFGG³ë›:u*ÜÜÜ™™‰7ß|/¿ürŸ9î§M›¦S¦ž?©©©zËËd2ÌŸ?Ÿý™5k‚‚‚PQQ×^{ ¯½öš^òèììÄæÍ›ñÜsÏ!==~~~˜1cV­Z…|8uêžxâ £»“áÁÖCŽi/¦"âþ06è¦Uk‘õýuzåˆÅ‚n@wê4&ÍáÒ¥K£3_.—ãá‡6øúo¿ý0qâD,X°@'ÈÆçó±lÙ²>{V) ¬Y³"‘Hgú”)S’’µZ]»ts¶3-”n'ؼÛ%%¿ß 455±=M,X€¤¤$΀ s1Ø_8þ<$ V®\ÉŽµÉ%##W¯^ŸÏÇêÕ«Ù1 zâñxlzDs)++ƒJ¥‚•••Î8•ýqéÒ%h4DGG#,, 7nÜÐkáuóæM”””ÀÓÓÉÉÉÓL655±é—/_®ÈЉ‰¯¯á±a+W®Ôû{xx`åÊ•ºÇìy±\[[; c€iÙçàà€ÐÐPÎe.\¸€¢¢"<ýôÓðððЙE‹¾þúkï–¼ø½‡£¡± œœ`ccFƒÒRó>LéÖ®] +++tvvâ£>b§K¥R<ýôÓøúë¯ñÎ;ï@(Ôm÷µqãF¼þúëøàƒÏN?{ö,òóóagg7à±rÉÈИ“Ümo¡«Ùøïys÷ºeÆw»Z[Ï9¾Xý–Rí_z|7 x<Ì ðÆ4ßîÞ†RQÃê·ÔÚž2„+t¿¿½ä6ìøh·÷ª€3¿U6Ïß›í}Çð‘Kñá´Î:¶©Ô8VÖÝØmux|mõ¿O|$·¥”c”ü6†—·Ü/ÅG¡¨I‰÷/7VwoFB}¸zÜ@úoÁ¾0…=’½Üô^go%¿Çgè\e êÚ»¿sÿ6!2‘~;Y®i À¯Kfãë9“!èÞâ_®î.üoxËõ{Ð0ÁAcE¹8"ÔÉ|>áΘëï•cðHØhÜàÍÝJš”xåL:jÌØÓèn\êííÒÒR,Y²Ä¨,•••X²d JKKáããƒ{îš@>±¼œŸ¿ÀþÿºgDÝŒ2zíõ4nùË:=ÚV2D=ðW ”h`Fj}T*ìùû>Ô–t/ðø<Ä-G>ñ+&À5ÈBq÷w¢DfÏpL^“„ǶþA7è–So7ì…F5´Ù‡F:­V‹ÒÒRäää ³³666@ll,&NœˆÄÄDL˜0ÁÁÁÉdhmmÅåË—Ùaz,©µµ/^Dyy9ø|>¼½½1nÜ8$''còäÉHNNFLL <==ÁçóQQQ .p6ºdX"èDvv6<<<°fͳ­wÍš5ððð@vv6Üë2Z­¶×„·ëÙ+Í\eékÿ™ÊØ2òòË/›üšÔ¡?Ÿžnܸ¿ÿýï‹Åppp€••*++-v'ãíà™Aˆ~bé^n·ûᇠ///<ù䓨¬¬DII ìíí1jÔ(½‡Ê=¥¥¥áܹsˆ‹‹Ãœ9s””„¢¢"ðx<øûûC*•¢£££×Þ@‹ððpB©TÂ××ÎÎÎP«Õؾ}»ÎC™LÆŽÉ´,jhh€X,FHHQSSƒS§Nélãðáà DTTV®\‰iÓ¦¡   °³³ƒŸŸœœœð§?ýɨÞu†œ>>P(F½/<O>ù$JJJP^^…B???…B¤§§ãĉz¯Ù·o‚ƒƒáééÉy ¨Õjƒçð††\ºt ãÆÃºuë™™ Fƒêêjö»K«ÕbëÖ­X·nÜÜÜðÒK/¡¤¤ÕÕÕððð`{œíÛ·ÙÙÙ:ëÈkîï”±cÇbÚ´iP(¨­­ÕùN Ð5epé»Áž={±XŒ… âóÏ?G\\þøÇ?²cî=^^^ì ¤\.gÓ‚:::âÅ_ÄÉ“'ñÑGaáÂ…lC§ž ÈMYš‡¬÷7@3ŽáaãêÝ=0ÉmZo£.ój3Nq¬¥ÿúW ~.®@¨£¦ø¸cÏü)¸r«1Âöp¶–€ÏãqöD3Å™›·ÐÚ¥‚Hˆ­³&áTyš:»ào'G„ÂÝ%\½êöä—`YˆlDB|6#WnÕ£¨I ;9Âöì¸`\uÜ’•‡D¸K­±w~*~.¹‰‚†fˆøÚJ‘èáŠ?;Óú øªZÚЦRcŒ“=´>pfH÷>ÜëÓs|· ·ÞUàÙ˜1°³ãý©q8^V…ìºFHEB;Øa‚› ÛDão=U 6_ÍÅóãÃèá‚NÃÉò*”6·ÀÁJŒ [Ĺ;#zë>týVo_[)^Ÿ¹ØÖB!:Õ¿ßíÎ-ÆÌQžpµ±ÆÎ¹)8Q^‰¢¦¸XKíâ>‡{÷1úý(lTbç"Lòt›ÌFg^—Zœú&)©DZu 4ƒ`Æ>tèI)#oÞ¼‰%K–`ÆŒ}Ž_LF¾ö¦ZœùøY”¥™–5h¸¨Ê>߸¹œóª³ÏA«9_—^zð¹MÂÒ ÜH¯OK] ¾ùÓ.ÜóÜ4$ød ŠCÂCÝõÒjµ³ð§•`ÿÿ@G‹åŸ·Å{¹ñáæ[×illÄåË—áääGGGÈårˆD"ðxØ>ýôS¼ù曘>}:gF·ãÇ뤚d¦²lÙ2LŸ>†Æ'':=Õ¤±´Z- t|—èg ³³s@ÄÉð`ïeÇþmɱÜz£T*ñü‹/F\\ÜÜÜàææ­V‹+W®àèÑ£X¿~½Á ÇæÍ›‘››‹9sæÀÁÁ‘‘‘lë§M›6!$$ .4¸ýÝ»wC*•bÚ´i Ð}TXXˆ¯¾úJ¯E”V«Å÷ߘ˜øøøÀÓÓ“§ÑhpöìYìÝ»ÍͺcÈh4¼÷Þ{ˆÅ¼yóàîî®óÚ¦¦&œ9sVVVº°***BII |||pùòå^[EÕÕÕáµ×^ìY³0iÒ$„„„°ûèNßÀL¨‹/²cÑÝ 4Õùóç1vìXh4α)™eî¿ÿ~ðù|Î4“Œ¬¬,üûßÿÆòåË¡P(ØÞgÕÕÕøüóÏÑÒÒ‚§žzª×ò¼û7oüüüØ^^uuu8tèŽá~§T*ñúë¯ëjµW®\ÁW_}…GyDç½éiË–-hllDbb""#»[O^¿~]g™’’¼òÊ+Xºt)"""àçç???¨Õj”””à›o¾ANNçúòÚÇÃËË ãÇGll,š››±wï^6È6aÂÝ=눮K—.á7ÞÀ† °hÑ"DEE! Z­@PPàïïÏž§‚ƒƒÙq 0}útLš4 qqq‹»Ó£mß¾Û¶m³`ÍÈPÓ¨ºP}þªÏ@b‰£+ÖRðø¨Ú”h¯¹ uûÀÇXäÒ×øn°ùj.¼äR,ôAƒ-‚lÑÒ¥Â÷ù¥ø43{æOA:GO4S´v©ð䑳x;9 k ¦ûz@ àzmþzò\m$øËø0Î^u¹õMxø§Sx+9£leçê„q®N(lTâù—`-àI1œu¼PYƒÅûŽáI1ëd{ý½Ùyuíø©¨…MÜßÓZ¥Í-r°Å77Šp¾—}hŠá^Cã»@m{îßw o'G¸³¦ú¸cªOwÈ‚Æfü'#í*Ý`Þ§™¹(mnÁ Âá)³ÁA£Øy…Jl¾š‹žŸ,1_•Vž–MéÙlqª¼kŸÁÿ$FÃMjù>lý.VÖ`gN1LÑØÑ‰}¥ØWP Ÿ'k+HÔµwô:Ÿ9õ7pV^^ŽÏ>ûÌÌ¥¹;ìܹs@ó[öÁÍà‹ÄžþTß8_?\‡¶†Þ3ü g»þÏÈT½qÞTí-¸¸õ% •jzJÇôD\æ7ÂëÓ®ìÀ÷¯ìG`b&®Œ‡“¯£Î|® ›²F‰3[Ï!óà5ÀÂ.?¼x JˆÀOLð4oÀì³´lƒÌI«Õ¢¶¶– ®õÜç} =biíííâÞ@`ãÆxöÙg±hÑ"ØÚÚâã?fŸá1YÓ˜”“Çç̤Æçó±fÍLŸ>@w&—‚ãÇM$w>ž››ÛÈê£N¶˜ÔÌ=Æ ‚ÈÅa:Ó³—›TÚ}cj9¹ðx<8;;C"‘àæÍ›èêêB@@^xáܼy/½Ôû‘­­-œœœPYYirðJ ÀÅÅ"‘P©ú¾@‹Å°··‡­­-”J%jkkîµ# áîî@€úúz455™=Å—©¬­­áææ†®®.Ô××›å=å"—Ëñ¯ý |>ùË_†t`c988ÀÁÁ͵Þ¡Pˆ>øðç?ÿMMM°±±««+jjjô‚°½éy û9ìùZ;;;´µµqæ~7×6úóZ‰D444°àr¹o½õàù矖ŸS0çBs—˜˜˜ˆçŸeeeøÏþƒÌÌL<öØcX¸p!öìك͛7V¬X%K–àÛo¿ÅgŸ}†qãÆáé§Ÿ†BÑüضm›Ñãê’þqrêî©Ó×yƒè“‰„°—£¦­ÊÖAyÅàc+ƒ\,BAc³Éƒ$bøÈ¥(lTšñùð·“C,ࣲµ µmœcˆ ¥‘\k¡ö¶èTkp³¥Õ¨ò3Ÿ±v••­mzA5†ƒD ŸêÖÞÓ::J¬àk+E}{'*[ÚÐn†‰lšsS{'p]G÷œÆõ÷íÓ˜´â¦ôþ©Ü||AwÛñ¯áúClc‹Î¶æ—Š‘‹ã¨0Ä®øo¸„Ä¡£¹UÙç±ëŸh,ϵtÑL6ãå½p žÀ9¯øì÷8ñîãœó†«;­>žáHð‡{¨ìÜl!¶CÕ©BSU3ªrªQx¡EЇUcè?N‹'bǘu›Ó²ñï³WͶ>w÷î†6ÅÅÅœ5CÁ6®ÀÛpÆT‰‰‰X¿~=›ó›o¾18ôÉí&Mš„xÐh4ظq#NŸ>=È%æ¶dɬX±ÂèÌoL×®®.£žé‰Åb|õÕWz=Iß(ðF1›þÞý·j<ìüðÃAÙz?þ8bcc±ÿ~ìÙ³ÇÒÅ®ÀéÛ½÷Þ‹yóæ!==ï¿ÿ¾¥‹3`ƒxº[Ž5 _ý5{0qâDlذW¯^Åÿ÷þþ÷¿#22¯¿þ:;Ö®T*ÅêÕ«QZZŠÝ»w›½lDÞ!d`(ðF/€V3²ƒäNþQ˜ñÒwzã¢u(ëqàï³Ñ\Ud™‚õÓV28(ð62øûûã±Çc³õTTTàÌ™3ÈÌÌDii)ÛØÖÎÎÞÞÞ CBB;–|vv6>ýôS‹÷t[²d ;–½¹}÷Ýwtë' ¼B̦¿7 {`ßQ‰¾h(iô±Ü;ðöüóÏcôèÑØ²e‹ÅZ¼óswwÇ+¯¼‚ÎÎN¼ð ƒöù x3 Þxã H$¼úê«(//·t‘l0o\ñå—_rÎ{衇P__?$å º(ðF!C7Búæà;Q‹Ÿ‡Kp4j*³N"}ÇÿBYmZºÛáâN«1? ¼,3fÌÀœ9st†WéMyy9öï߃rÉÈH6lÆx#„ÜÝ´- OYºF›4iJJJP\üû…µD"Áý÷ßÑ£G£  ¿þú«KHÌíæÍ›8vì¦L™‚©S§âûï¿·t‘ÈJMM… Ž;vGÝ,¡®®åååpqÑ‹¡ªªŠ‚n„B!w°úâ,}k¥¥‹a6wZ}¹Û ¼B!„B!„Œ·ßnݺGGGÈd2H¥Röá/!„¡¥T*{ ºBîx#„B!„BFzëõƵl]]ZZZ •J!‘H èa/!„ 2­V µZööv´´´ô™^Ò˜Þn„;Þ!„B!„BÀÉÉɤå ÑzNgþîmÚí¿5 ZZZŒÚ!„þá  Y[[ÃÚÚšgèw_ÓúÚ!dd À!„B!„BÈê­ÛíËô6\Ïßôþf–½ã!Ä8Æö`ë+°fÌ«ç›)¯'„b^½Ö¸þ7&Õ$Û¹3PàB!„B!d˜émœ7C½Ún_žAA8B1cSBšú?!äÎB7B!„B!„ 2”z²·à›¡ÿ†¦B1sáúšNy(ðF!„B!„ba ¾ú½Ún€K½Þ!d`ú Œ™šzÒ”uBF ¼B!„B!„ cÆÛúJ-Iu !ÄüL ¤Ñy˜»Þ!„B!„B†C½Þ Í£Þn„2´úÓë­¯×Q@Ž;Þ!„B!„B†‰¾‚o€á@[_¯#„28ú›B’Îτܙ(ðF!„B!„2Œô|ëm>×\êíF!æel°Œ‚n„ܽ(ðF!„B!„2Ì|c»!¦Šð™ˆ™ÑË!³²C»ª‡¯|ƒ y?[ºX„ [æ ÊBF6ž››5}"„B!ä.äääP*•. !„Þ˜Òkz¸spµ÷ÁüØÇàï2ÐÐz ö6΀²º<ì¹ð Êjó-YDB† S‚hp#äî@7B!„BîRx#„‘c 5 ÆcID6˜ñ ƒgƒÏ ¡µ?\Ú‚+Å¿"Âw"æŽûìmÐjµ8Ÿw.ÿZ;š-]lB†Ì@gt#äîA7B!„BîRx#„€ÈÖb¹#„6Rh5¨ZšÐv«ZµÊÒE#(ˆF<Äø§`nÌÃI ÖtáØµ½8rõt©;ÙåD1¦†?€”1ó!à‹ÐÚÙ„ýi[q!ïghAŸMB¸PÀ»Þ!„B¹K ·À›»»;D"ªªªÐÑÑaÔklll P(ÐÞÞŽêêê~oÛÆÆ>>>prrBcc#rss.Ã@&“ÁÑÑ­­­¨©©1Ë:<==%%%fYçH6uêTÔ××#--ÍÒEˆdvp‰›ûàX98ëÍ×jÔh.¾ºŒ_Qý´ÙËàààoooÈd2ܺu ùùùÐ Âv†šH$‚»»;Ôj5ÊËËm;C€‰DpssƒZ­FEEÅl“ Áþýûý½ötôÇ}Ç(çÀõòKØsáÔ4ß4ø…Ü ƯF¨ç8@IM.vŸûeuy. ¼¼¼Øïäüüü;ê;ùN=¦îÔz ÄpëÏõ¹¹¸¹¹¡©© ­­­Cº]BÌA “É^±t!!„B!CÏÆÆÐÙÙÙÇ’Cã‰'ž@jj*²³³ÑÐР3ÏÖÖ¡¡¡hnnFWW;}̘1XµjœûÐˆŽŽÆªU«0a„„„ 228}úô€ê3’ÄÆÆâᇆT*ÅÕ«WͲN©TŠ¿üå/ˆÅÏ?ÿl–u”¡ÏÑ`³··ÇòåËŽÚÚZTVVÙ¶¹(¢'#pézÈ}‚ ´–r.Ããñaåà û¨ÛZÐRQh¶íóx<Ìž=Ë—/Gdd$BBB0nÜ8Èår\¿~ÝlÛ±ggg¬_¿cǎʼn'm;<ýLÎÎÎxê©§0fÌœkjµZçõÂv̘1‰D€²²2ÔÖÖ\6<<|>jµ™™™zó]]]áææÆþŸŸŸo°å­¿¿?är¹Þt¥R‰ÊÊJ´´´p¾ÎÅÅîîî}–W*•"00ý¿ªªÊàQÏu2´Z-PWWg°666=z4 ¾¾¾Ï \ÛáÒÑÑììl½éŽŽŽðõõ…»»;„B!jkkQ^^Ž¢¢¢>×I!„Bˆ¥­Zµ nnn(,,Ä|`¶õzzzâÁDWW~úé'ÔÕÕÁÝÝÝà=ÙësÔ—††|úé§xì±ÇðàƒB£Ñ ##cȶߓGÊB¸%Î6íEf~x˜˜˜ˆ””ÔÔÔàøñãÐh4ðôô¼#‚n–rû^s?À²ôäG}®®®(**ÂG}dѲ ­V‹-[¶`ÕªUˆ‡V«Å÷ßoöí¬šúü\Æ KÝ_®îÆ‘Ì]&Ì2KÏáFE:¦†Ý)á÷#1x¼ñïÏ™ü9ôôôÄâÅ‹ÑÕՅÇßÑßÉ–>¦‹¥ëe‰s…¥ë|§ŠŠŠ‚‡‡rssuž¯GFFbéÒ¥F­£ººo½õÖ€Ëbkk‹ÿú¯ÿ✧ÑhÐÚÚŠÒÒRܸqC'HhI–ºæ5Å}÷݇˜˜|ñÅÈÊÊ”mH$$&&"""‚3¶ÒÞÞŽÌÌL\¾|¹¹¹:ß[¦|Ö4 ^xái ¼Ý{x4ià§Ÿ~ÂþóÎå¼½½ñÆo.\¸Àx[»v-ÆÇþ¿uëVìØ±ƒs}+W®ÔYövEEEøúë¯õZ¨Nž<øÃï½÷8ÀùúY³f±ËÀåË—ñâ‹/r.Ûs\rrrðÝwßéµôðööÆë¯¿øù矱qãFƒë0f;ŒââbüñdÿwqqÁ²eË0eÊðù|½å«««ñ¿ÿû¿ÈÉÉésÝ„B!„XŠP(Ôùm.)))àóùØ¿?Ξ=  ûúŸÜ™ësdŒÂÂBlÞ¼=ö–.] ­V‹+W® iBc9ƒnõ·P›q ­•%ÐtuB$³ƒÌ+cÆC(Õoô:PS¦LlÙ²…M{á³oçnfîÁ<|>ßb˜E"x<ûûnÐÜÜŒ?þkÖ¬Áĉ¡ÕjñÃ?˜mýáÞ ðw Ìø¿dîпώJÓ…ƒW¾F—¦÷Ž{£\B?úœÍ=dÒz’““!°oß>œ;wØóäö¾[ú˜,–®×Ýx®¸ñù|̘1pøðaÎeT*UŸéÑëêêÌ^¶êêj¶—›@ €££#d2BCCŠ{î¹ß}÷ÅX1,yÍÛGGGö}a~÷ìIiooo¶ž•cÇŽÅ<kkk@kk+ªªªÐÔÔ©T [[[( ÄÆÆÂÏÏo¾ù&çzŒù¬q54О?zô(x‹Ç{ï½Ç¹‘ÄÄD×ÜÎÁÁÑÑÑ:Ó’““ Þú2jÔ(üíoë5¸Öæ&€Ñï7=((ýë_!‘H ž(‹\.Ço¼WWWƒËØÛÛjÎyB!„BÌaË–-Ƶk×̺^&ëAvw¬Ï‘±ŠŠŠ°yóf¬Zµ Ë–-ƒF£ál˜:x!¼¦?¨7½.ó,ŠøZµJgzýµ (;²ÎãR¡îh7[9¤R)d2Ù€Çe$CËÒ°¿øâ ‹»–ÒÒÒÂß’’’ÀlÁ7…­;˜wuÞ¸Gᛈ½6£°Úô}ìç2óǯÂ(E0;ÍÕÎËäõ¸¹¹ÇãÝßÉ–>¦‹¥ëu·ž+î4AAApttDqq±Álm•••x÷Ýw‡¶`Þ}÷]m|>NNNðóóÃÌ™3!“ɰ|ùrÈårœ:ujÈËǰô5////üñÄ¡C‡pôèQ6ÖÒÐЉD‚Å‹ÃÓÓo¿ýö€ÓcÆÇÇã¾ûîÐÝ€ã‡~àŒíH$ꥉ쩿Ÿ5ÀÛĉqþüy¨T*CËëHKKCss3är9ìííÊùf2mmml‹•ž’““Áçó¡Õj‘››‹   øøøÀ××ÅÅŽ–áÕW_E]]ø|>\\\0wî\„…u·ØY³f Nœ8aR—ô€€øøø²³³>Ÿ¤¤¤>/nÞzë-œ8qB¡X±bâãã .4[àmûöí[öärÕªUlÐíäɓرc***  áàà€   Èd²;²Ë>!„B¹³TWWÊCz™LÃvìb^ƒõ92Eqq1>ùä¬Zµ Ë—/ǶmÛ-½NOŽaqÉíu¦µÞ,Fñ¾Ï¡50>‰V­Bõyó6 ¥cŽôÇ­[·pëÖ-KÃ"˜àÛªU«””FƒüÑlëϯ΂‹F)‚±nÖ¿p¥ô ö]ü·šún¤ílë‰{cA„w@ÙÑ„êÆ2ø»ŒéWYèü@ên>WÜIƸté’…KÒ7FÃ~î233±hÑ"„‡‡cîܹ(--í3¶1X†Ã5ïíìììÐÔÔ„Y³fÁÛÛiiiP©T°µµÅC=…Bêêj( ܼy³ßÛ5j,XFƒ­[·özͤš lîÁùóçcÆ xæ™gŒ~±J¥Ò(xâĉzËxyyaÔ¨Q€Ó§Oë†L³ÌÌL‹‡É“'÷Y†¢¢"äåå!''§N‹/¾ˆ²²2ÝÝ‹Œ®OϲtvvbÓ¦M&•E­VC£Ñ ³³EEExóÍ7Ù¸«««ÙZ}ܼy7nÜàüéÙ <<œ­ËÛo¿¢¢"tvv¢µµååå8zô(öíÛg–2B!„B1NII >þøctvvbÅŠ3¦©Ma¦7íæÉ}ƒn„0x<ûC,£¥¥Ÿ|ò nÞ¼‰äädÌš5Ë,ëåñx¸Vzÿ³û1Ìø]êDúLĆâþøµKì9_'—ØãþøµØ°àCDúLD—º‡¯ìÀÿì~ Y¥çû¬˜þy¹[>kwj=ïÔz‘¡%‘H0f̨Õê!OÉ=P­­­Ø¾};tÒe’nYYYxóÍ7±cǸ¸¸`Ñ¢E€§žz J¥[¶lÁÛo¿=  Ð=<ŸÏÇ™3g†¤q›!B¦0«W¯FSS¾ýö[“VpôèQÌ™3@wàmóæÍ:óûJ3éëë ݹ³gÏB¥RA(bòäÉØºu«IåQ©T¸ví¼¼º»´ÛÙÙýZ@€äädÝùå‹‹‹‘——‡ÀÀ@„††ÂÙÙÙ¤VZ­–í¦˜““cöÁ{Ããñàèè‹Åððð¸+ºêB!„;ÓØ±cáãボœäççs.#•J‘wwwX[[£¢¢………œ7\'N„Äb1€î†v­­­ìüôôt6þáÇ9³‚DFFB¡P ®®éééœeš6mD"Nž< ¥R Œ7 8sæ çkfΜ ‡_~ùE§áâØ±cáéé‰Ã‡ÃÊÊ ãLJ§§'lmmqóæMäååáúõëö`7GGG„……ÁËË r¹·nÝÂÕ«W .oee…)S¦°û= !!!pwwG{{;JKK‘žžŽ¦¦¦^·{;@€{î¹pðàAv¬Œž&L˜'''\¾|™óœÇã!<<¾¾¾pwwGKK ***pæÌ8;;#,,L/Š¡Ï‘»»;¢¢¢pæÌ477#&&>>>pvvFSSJJJØ{Us)++ÃnjիW㡇—_~Ùçû7ROÿµjš †îaóž3=Z¬¬¬t‚øõ×_ü~|^¼x‘ó< AAA())é÷•‹ +++DEEÁËË ÎÎÎhhh@~~>Û¸7 ƒ··7¤R)*++qýúuäååé,7iÒ$Èd2ܸqœërttD\\š››”Öªg} ûUTUU™µ>Æ|8::b̘1ðòò‚½½=šššP^^ŽË—/£±±QoyÃËË Z­HOOïwoÇ–ÓÃÃõõõÈÊÊB^^çyέ­­øä“O°zõj¤¤¤@«Õâ§Ÿ~ê÷ú˜àÇCGW\Þ†S7öcfÔRL šI!÷bBà4¹ºG³¾E§ªb¡RÇÞ‡©á‹a%´†œÍ=„Ó·¢±µö¶õ_–¾¾“:µZ=$熹ÏÃ혈á\¯‘p®¸]ÏkÆÆFDGGcÔ¨QpqqAmm-Š‹‹qéÒ%ÎTxÌk³²²PRROOODDD@.—£¦¦'NœÐ¹váóùˆˆˆ€——<<< T*QQQ¬¬¬>ëÃãñØq̘ë£òòrdddpî·ž˜}èíí FÓë>ôöö†@ @ii©Îy` ÌqíiŒ®®.ì߿˗/G`` Áá«û:À˜cÁ××ðôô„µµ5êëë‘——‡¬¬,Îsš)ï£!—.]B]]–/_¡PˆÜÜ\|ýõ×P*•F¯Ç___x{{£¥¥ðúúËÝÝÂ9sæàñÇGcc#6lØ`r÷ÇììlTVVÂÍÍ ...Ðy3™4“uuuœQj¦‡™V«Å™3g T*qåÊÄÄÄÀÝÝ£GFnn®Ieòðð`ÿ6æ¤ÏˆŽŽ†½}wkæ‚ÿôéÓ ÇÃäÉ“±{÷nƒ¯àó»;òx<¬\¹ÖÖÖP©Tؾ}»Iuè••¤R)ç¼¶¶6h4hµZ ((ð¯ý {÷îů¿þj07.!„B!ÃUpp0âããÑÙÙÉyóèææ†çŸž<1yòddeeaçÎ:7ï±±±lc=ˆ‹‹ÓYßéÓ§1yòdàÆ:óù|>.\tvvâêÕ«zAÜsÏ=hnnfŽ:99!55%%%䥤¤€ÏçãäÉ“:7½Ì>H$ˆŒŒ„\.שë¤I“––†Ý»ws¦éŠŽŽÆ}÷Ý+++vZ@@âããÑÜÜÌY‘H„ÔÔT„„„ ªª QQQ:ó#""œœŒ¯¾úJïAAoRSSt6¹~DFFbôèѨ¨¨Ð{ø!“ɰdÉö~§çk’’’ T*áî/êÞ }Ž\\\šš OOO( ¶##** 'NÄG}ÔçÃ%S”——³ã71Á·ììl³­¿'‘ÔVçÿކ½qÝSÏ÷èn Úóÿ²²2ö>œ9>óóó9æŒ5 ©©©8{öl¿®äXº³û,[¶ …BgzLLŒÁ㉉ûï¿_çX=z4&Mš„‹/b×®]ì1!—Ë‘’’///ƒÜâââššŠ_~ù¥Ïzâåå…¥K—êÕ'::Ú¨ú,Z´H¯>III¸té’^}’““áééÙk}RRRto#..ÎàwÀøñã1oÞ<6pÓ³l‘‘‘:c³ðù|ÜsÏ=HIIÑ陊äädìß¿Ÿý,ËÖÖ>ø õæÅÇÇcË–-f ¬·¶¶²ûÔÔTh4:thÀëe4·Õcç™÷q,k/æŒ[‰hßI˜ý0CæàBÞÏ8 vÖN€¬²óØwñ3ÜlxµqãÆé|'O˜0Agþ‘#G V«Ùåz;7¤¤¤àܹs¼999!%%¥×s3tŽ¡sÄp>¦úk¸×k¸Ÿ+¸0× Ìõ‡§§';ÏßßãÇG||<¶mÛ†ºº:ÎתÕjøùùaöìÙlYt¾ –,YÂ±ÄˆŠŠÂôéÓqàÀœ>}Ú`Ç‘E‹ÁÅÅEgZtt4¦NŠ]»vq6äböajjªÎ>3f RRRðÃ?èíCooo¦=Ó7Æ@¯=MqõêU¨Õjøûû#--Mo;ƒ}ÐÛ½“P(ÄìٳٸMO±±±8qâ„Î[ýy Æ”)Sàçç‡ëׯÃÊÊ ‰ûÛßpþüy?~œ3Pi¬ÝÙ%ÚÛÍ7F±©\\\ |â‰'ÐÐЀ 6ô»GÔÑ£G±téR@BBûfzxxÀÏÏpìØ1½—Çã!%%@w¯¶¶»uÌéÓ§ ûK¬·À››ÎÑÖÖÓ¦McÇxËÏÏGaa¡Ñõ`‚€]]]8þ<àÔ©SX¹r%ôx{î¹çðÜsÏéM/..FUU•ÑåèËÚµk±víZÎyþóŸÙ‡_|ñ^{í5ðù|H¥R,[¶ Ë–-CUUNœ8C‡ ¸ë&!„B!Ã\.Gii)~þùg”——C&“!44Ó¦MÃØ±c!ðÙgŸ±Ëoß¾b±kÖ¬D"ÁÖ­[Q__ÏÎonnFvv6ÆŽ‹ÐÐP½À[`` llltBBBôÆ`Ò^½zÕ¬Ù/’’’PPP€Ý»w£¬¬ R©aaa˜:u*bbbÐÖÖ†½{÷ê•eéÒ¥Ðjµ8zô(®\¹‚¦¦&xzz"!!¡¡¡½nÓÝÝNNN8pàrssÑÐÐ///Ìœ9xä‘GðÏþÓ¬A)Cx<}ôQxyy¡ªª `÷C@@f̘ww÷~­;((ÅÅÅøá‡P^^Î>Èš3g -Z¤ó92‡ŠŠ |ôÑGX³f ~øa¼ýöÛì½±¹ðBð:C¼CÓ©ß’y0uuuáÝwß…““–/_ŽÖÖVŒ9Ì}ýH`kk‹'žxb±YYY8yò$ªªªàâ₱cÇö:TEHH–/_ŽææfìÚµ‹ÍŽãïï  66ÕÕÕ8vìàüùóHIIA`` lmmõz—òx<¶w.ó£?õyüñÇ!‹qíÚ5½úLš4©×ú,[¶ ÍÍÍØ½{·N}æÏŸqãÆéÕ'99¹×ú0~cë“€  ££ûöíC^^akk‹€€½€ÈÌ™3‘œœŒ²²2ìÛ·eee°¶¶Fdd$fΜ‰y󿡬¬Ìègd"‘k×®…ƒƒrrrpüøqTVVB ÀÅÅaaa&5LèK[[>ùä<öØc˜:u*jjjôêÔ­¦rl9úü¢¼ña´k¦…?(©ÍÁ÷>En¥ùRÀíØ±b±«W¯†D"Á¶mÛt¾“GÚ˜o#ý˜ºSëeésE_ÂÃÃQ__;v ¤¤*• ˜3g¼¼¼°råJlܸ‘óšrÒ¤I°²²B^^òóóÑÐÐ Ó L"‘`íÚµÉd¸zõ*N:…êêj¸¹¹aâĉǼyóÐÖÖfp\5'''ø€}ÈÐÔÔ„ŠŠ à‰'ž@HHBBBØÞDÌÍs3^YY©×z>##ƒ ¼íÙ³Gg3žò™3g€ÈÈHƒ7sOqñâEìܹ“½GjjjÂÍ›7Q]]åË—#>>§OŸFMM €î¾Ìð»víÂ… ØueggãÆxà0nÜ8ƒÛÔjµøÿïÿé´´ÎÎÎF~~>Ö®] OOO̘1ß|óYëÊ…éñP]]M›6±f?äææbݺu …}¬I_QQÞÿ}i¨¯¯ÇSO=…ØØØ˜5õö½ý*wŸëW« U«t‚o|±U/¯0?­V‹²²2öºZ­6{kö¡2sæLˆÅb\ºt ;vì`§¢°°UUUX¼x±Þëø|>æÎ  »¡lÏT×®]C}}=Ö¯_éÓ§ãôéÓèêêBMM ›æ5""B/MœŸŸì®Ôõ IDATíí‘——§×”úXYYõYŸÛÇmbêÃãñðå—_]Ÿ‚‚‚^ëãààÀY®q£¬­­1cÆ hµZ¼ÿþû¨¬¬dçµµµé5‚vttDRR;fÓžIÏ¥Ñh°`ÁÌš5 }ô‘Qû/99ŽŽŽÈÈÈÀW_}¥óüª±±Ñä,NÆ`¶Áãñôz¯±¸JjrðŸÅ‚ñ«0%ì~\,8Š­Çÿ -zoLbêoÌw²F£ÇãüNfÖÛW™Í9¾X_Û34¤S¦)õ®çоTVVâ½÷ÞÓ ^¼xøóŸÿ wwwÄÄÄpÆD"¾ûî;ƒ½3g̘™L†‹/ê\«åçç#??÷Þ{/&Mš„Ù³g#33“3Íà7ß|£“Z½©© 999X¹r%BBB0þ|¼÷Þ{ìù‰Ù‡J¥ü±ÑûiØÖ3cggg<ñĽ.óꫯš%}aܺu ÎÎÎl}€áqàïÔÕÕáwÞÑiøÔÒÒ¢<ëïûÈåôéÓð÷÷ÇÎ;qëÖ-$%%ÇãÁÆÆ»wïFII ÂÃÃt}Èd dîƒn„àà`Îy 8yò¤Þt…B5kÖôºÝÍ›7#''GgÿoûÛ€‚n@wz fÅ>>>l÷p¦»bqq1gzC¦‡Ð}‚HIIAJJ bbbØãè舱cÇšTžÂÂB<ýôÓ&µ8HLLd/V”J%[–””mo-×¾üòK¬_¿ëׯdzÏ>‹·Þz‹íqgmmgžyÆ,GÅ|ÀùsûEff&žyæ¬[·»wïÖùb …X±bÛëB!„‘ª¹¹™sü­ÂÂBö!Áíiû’••…ÎÎN888ÀÕÕ•Îç󆚚üôÓOP«Õ …H$b—‘J¥ðõõ…R©4) ‡1jkk9&fdd ´´@§[@@œQUU…‹/ê½N«ÕöyO¨Õj9o滺ºØñ˜`ä`cL:tˆ³—TUUç#cêUQRR¶0îùY0777<þøãJ¥Ø¹s§Y³¥ôÔÕ¢ÛBÚÊ^¡× ŽôM$aܸqP«ÕÇ×2ô,"00...(**â\ææÍ›(..†H$ÒIçuîÜ9`[´÷Äd êoO–‘^Ÿ¸¸8ØØØ --MçyGoË …Bœ?ž3Õùóç¡ÑhtÒö¦g&§üѬ½› ‘H$Xµj|||páÂv¦¦¶îÞgʶ†>ƒnw»‘~L2Òëeés…1š››9¯_êêêØ4~†®µ Ýâãã{}ï:¥R ¹\Ž€€ÎezöBe¨ÕjìÛ·>>>:©ºû»™´ñ}¥ lmmÅÅ‹{ý1çØ¼¦bb=ßÓáp,ôL·iL¶s ÍÍÍxÿý÷Ù†Lo@fè¯ .àóÏ?7ª†`‡»¯¯/&MšÄùÉùš––œ?¾×®¬Bsµ0;zô(›_?!!§NbT®Þn‰ ìÿL:G.ÉÉÉz­Gÿþ÷¿ÑÐÐ;;;¬[·<ÎÎÎ&ßhõ N™2Eçÿž&OžŒ¯¾úŠs3 9#''—.]Â'Ÿ|™L///xzz¸UßåË—qäÈ“^ô`øüóÏ1zôh<õÔSìû“ššÊv_%„B!äN“™™‰˜˜½q)úÒÕÕ…k×®!**Šã ènY*•JqöìY´µµáúõë Chh(Û»-$$<Ïìi&û’›› ooo8;;³Ó˜z JYòòò Õjaee¹\Þç/ÅÔÍÐ8ƒ¥¡¡¶¶¶H$f[gÏ Û®]»8£æÒR^±íïÄx!lýÇ¢17cж9Ôüüüàëëkpþ‰'8Çt1…B¡ÇCmm­É©U™ mkk«ÁôXÌC0ggg”——è>‡µ¶¶ÂÛÛ …‚m¨, ÖÖVg&<ÉÉÉËQRRÂ?#¡>½qss£Ç²g·Æ`™[[[!“ÉŒ:Ÿ9::B,£±±‘ó¡´¹õ º1ã åw éÛH?¦ éõ²ô¹b rrrœœ¬s}×Soç…B@€êêj½4…ŒŽŽäææ"::...&el»u몪ªàîî6]v÷!Ó1¦¯‘uuuC’i¡¿˜÷ªçgƒŽ]MMMÈÊÊÒ¹6èwÓS’ æÝ.==]/6ÃŒ•mH}}=víÚerYÌÖ¼íäÉ“XµjؤÕjqüøq½åt>ïMRR>üðCÎ.##ƒí’†iÓ¦A&“á‘GÁÆZ¿³³3;.\_¼¼¼àïïoôM^ss3òóóÙˆ©9oÒú+777nĦM›À¬­3!„Bn˜V•=[áëòåËˆŠŠBhh({_ô6ÎÈèX¤§§#,, làÉÚaî4“}az¥õ¼Ùdnükü5•J…¦¦&ØÙÙÁÞÞ~P>Éd2X[[C¥RY,u¹ô ºíÞ½{À½úÒ”Ÿ ‡ÐXiîIsÑ”wZíÀ‚QÃEPP¦Njp>“i ˜ã©¡¡Áä×* Ýih™T´†ô,§J¥BZZ’’’…Ÿþ™]D"Á©S§tZõóx<Ìž=Ûàº=Ê>Ó õé óÓØ SßiÓ¦aÚ´iF—ÙædöŸ©zÝÒÒÒtR“ác¤S†ŒôzYú\1P\×wÆbêÞ×{Çs˜a¡LQSSwwwkíþîC&ð44º–ÆçóÙ}ѳg›¥+++ØÙÙ0þXÌcáøñãœq£¨®®ÆèÑ£  kjjôÒPöLjNf ¼544 ==±±± bßÄ+W®pæÔì٣쥗^â\fýúõ ‚\.GtttŸ­ÿ¾üòK$&&ÂÚÚS§NÅ¡C‡ŒŠÐ§¤¤°Ý?·mÛÆvßíiöìÙlÖääd£oB¡Ø»««Ëèh²9( ½Á4r¹œý{ ©F !„BΘ†ZùöæÆhkkèQ£`mmööv„‡‡£ººšMUtýúu´··#44b±AAAƒ’f²/VVÝãvõ¼™f`Èd²!Ýî``2›…BX[[³cs4=ƒnß~ûí¤Š«Ë<äÉhgã1 >sW¢äǭЪõïy!œÇ%CÝÑÚŒSzó‡›K—.õzÌõÕzÞÌñÔóžÚXÌç5//Ogœ{.ÅÅÅ:ÿŸ;wNï“rêö ­V«ÅæÍ› ®»çƒç‘PŸÞ0c"ûÐŒ)óéÓ§u2q1f,GæûÅØ†åýÕ3è–žžŽ;vPÐm˜éÇ”!#½^–>W Ô@®³Œ}ï˜óY¶Áìßž¼ú»™× ‡Î+ý >ŸÏŽ9Ͱô±Ðsakkk£± ·c¡/999HLL„¯¯¯I§ƒYº=z±±Ý-è˜húÑ£Gõ–sttdÇW())AZZçúNœ8Á¦¯œ½¹uë\]] >#áÚ®¯¯/Ée6´} û9—@ 0Kp÷v=ƒn—/_6[ÐMó[OW+‘yƒ†ÌúÔóï‹Áþ5ǶFú1eÈH¯—¥ÏÅu}g,æú·¯÷ŽÙFÆšež}÷¼Öîï>dÆ3w/¤þ^{šJ,³½Î;¦³]K ---hkkƒµµ5œ ¼ ·c¡/7nÜ@]]±páB¼óÎ;CÒ+• ÷(sýÄŒqÀèììäì=Ö³‡Ù©S†[Íõ2!!Á¨/»={ö°âQ£FáÞ{ïíuùÑ£GÃÛÛ@w¾TCQýüü|v½ÎÎÎ Ñ[fñâÅxõÕWñꫯâ•W^ÁúõëuÒÐ|øá‡œëöóóÃ’%K8¸ÒáLœ8=ôçS_777„„„ÀÚÚ‰‰‰X±bžyæ<ûì³X´hÛÚõÌ™3øñÇ{ÝG„B!„ %æ^Á†Ð€ÄÄD賕¦!éééºÇmcRÔßžB’iLÁ¦ŽáJ3É4ªsrrb[›ÊÛÛ›sI$6Å}Ï€“áÂßß¿×ñ¯zÃçóÙû¦Û1cw›²»ººØ›}f¬ c1õ14.÷pÖ3è¶wï^{Þ¡PŸuUg~Ò›nåà Ôû¸t=‚ú üî{.qÓ!’Ù iùÌqÂdÌm˜z,ÖÕÕ¡¥¥¢ÿÏÞ™‡ÇqU‰þWU]½wk߬ŒeY²lÇûï‰;+ „Àd€!É&Ã@B^†0@X^€Þ@Ù7“ÄÄŽ³xM¼ï’%Y’µïK·z¯÷GKmµÕ-µ¤–—øþ¾ÏŸÕ]w9÷TÕíª{î9GU‡Í‰ÚÚZiiiLŸ>}TuáÌŽö9sæpÙe—¡(ʸ=Y.öñ ¬Ó,^¼EQF,?àA0{öìP¤¦X‰4ï¶¶¶â÷ûÑét,\¸pTíÅÂ`£ÛÁƒùË_þ·Ä=•[q{û¸zögøÜŠÁnJW{6c"Ÿ]þu®žý<>7ÛNÄ­é\Ì g÷u©Ï\ìã:—sÅXIKK‹jl˳Ö¡s·|ùòˆeRRR())Áëõ†…FL´gíââbRRRèêê åwƒ±ëpÀxïû|<Ïž±b±X¸í¶ÛHHH ®®nÈæª é^¸¦Fâ|Ü ƒÑëõX­Ö˜ ±@€7Þx:†ÝrË-!ÑsM\ on·;ìÅa×®]] ¯¸âŠÐß‘ s455QQQÝcyˆñx<üá}¾ýöÛ‡M«,g.Qñ€555ìܹ“Gy„oûÛ£Ú 2^Z[[ùÿø¶lÙ2$¤Ž¦iÔÔÔðøãóƒü@„'@ \ „Ø5; ‡äP…믿ž¼¼<š››Ç¼øTQQAoo/%%%Ìš5‹ÆÆÆ!»+++éêꢤ¤„3fàp8"zcµ¶¶ât:QU•õëׇ-äʲ̒%K†5"B0ŸÃ'?ùɰrz½žÏ|æ3˜L&öíÛJÆÐÐÐ2~ö³Ÿ’ã9###êBÌ`îºë®! %%%¬Zµ*ì%w€ÐF²,‡vQfàE~ݺuC E]Ù´iS(œçõ×_¶€&I×\s ‹eÄñœkÝ^{íµC Mu[^ öÍ?ðâýô¿3,ú-_¾|ȵm#ìhë½Ø´ikÖ¬aÞ¼yaÇF#×^{mÔ>Îû­·ÞJAAAÄrшöïßÛífΜ9ÌŸ?ÇÊ59V.öñlÛ¶ ·ÛMbb"7ß|sX-Î ƒëöìÙC]]&“‰;¤¤¡Æ&EQ†´3sæLzè!î¿ÿþ°ùÅëõòþûïpÍ5ןŸ?¤½±ÎGƒn‡âé§ŸŽë®ýž¾žÝñßx¼.M½Š‡n~’+gÞŒ".8–,ɬ*½‘ïÜü{–]Çëâ¯ÛA{ïè½fFâ\Ì ˆ9"œ‹}\çj®‰‰‰Üu×]CB×®Y³†)S¦ÐÞÞ>¦ ;Þ ˜§k <áà~?ûÙÏ¢( ›7oŽšýæ›oâ1˜““ÃÍ7ß £¶ öú«6XMD¤†±>{‡N§#33“Å‹óàƒ2cÆ ÚÚÚøãÿ8Ä«îB¸¶lÙwK–,‰8žÁýŸ{a0«W¯æá‡Ž-0à½÷Þ`þüù<ðÀL›6-âš‚‚‚PdÆxw<þøã<þøãÖ¹ï¾ûbnïŸÿùŸ‡|÷Ýï~wØ:|ðA(,ä`žþyžþù°ïžxâ žx≘dùýïÏïÿûÛŒ…'ND”1£íÇçóñᇆÂHFÒÒÒðûý477O˜;­@ @0VªªªÈÍÍeýúõäääP__?ª†«¯¾šÅ‹SSSƒ¦iäå员˜ˆÃáà¹çžó‚e ààÁƒ,]º€·ÞzkHMÓØ·o«W¯FUUvíÚ±¿@ À믿έ·Þʲe˘:u*µµµ rssILLDÓ´½þ–,YBqq1•••¨ªJAA6›æææˆ-6nÜH^^III|õ«_åôéÓ477“––FNNNLº1›Í|ík_ãäÉ“ttt™™I^^\l9;”§Çã¡®®Žììl¾øÅ/RVVÆÁƒC›+ßxã Š‹‹™ILL Ý^ýõÐBýù¢eÏV:Ëö“¾ø*’Jæ£OºYUóûè9uœ¶Ûè<9=ÄD±cÇ–,YBZZ<ðeeeôöö’žžNnnîˆÆéXϽ¸k×.fÍšÅÔ©SùÌg>ÃW\ÁéÓ§±Z­äåå ëM±iÓ&&OžL^^_ùÊW(//§¾¾—ËEBBEEEÔÔÔðÌ3Ï ©ëõzÙ·oK–,!!!?ü0´aa<\ÌãéííeãÆ|ò“ŸdÁ‚PQQAgg'IIIQ__ÏSO=Ïû³Ï>Ë]wÝE^^>ø ÇŽ m¦HMM¥¸¸˜7Þx#,÷â²e˰Z­X­VJKKÃRvlÞ¼™ÒÒRÒÓÓùò—¿LYYõõõ(ŠBVVS§Nå¿ÿû¿Ã6DŒ„$I!>ÌŸÿüç •õaÅÊpÓÂ{˜?e57-¼‡¥ÓÖñ®_s¬nψõ§fÎâSK¾Ê¤¤|öU½Ç‹»ÿ/]ζá+Ž‘s17 戡\Ìã:WsÅxÉÍÍåßÿýß)++Ãív“——Gzz:.—‹çŸ~ÌálwïÞÍŒ3())á¶ÛnãŠ+® ®®«ÕJ~~>ƒ'N„Œ‘HKKãë_ÿ:uuu¡gÈÜÜ\EaïÞ½¡ŒU‡ƒ oƒ!®×ðXŸ=óàƒ†ÊI’„Ýn›{8À‹/¾5ñù¾Ž?ξ}û˜;w.6l`Á‚TWWãv»C6mÚzV=_÷ÂñXÃÙðúë¯ÓÝÝͺuëHNNæî»ïÆápÐØØH__6›”””PTÀöööúl²²²øÖ·¾5l@€þð‡aßMŒ)RpÁár¹B—@ @p!²yóf233™6m‹-¢¼¼ËŒ3Bá1#ñî»ïb0X´hóçÏ‚Ïû»wïæÕW_ y𠦣£ƒŸþô§lذÒÒR&OžÌäÉ“ñz½ìرƒwß}—o|ãQû <ùä“lذ!”‡‚QJ^}õUÊËË#Ö{öÙg¹ãŽ;HMMeáÂ…ôöö† oÍÍÍüæ7¿áSŸú™™™¡Ý¦¼ð ¸\.n¿ýöˆíþýï§¡¡ë®»ŽäääÐ9ojjâ7¿ù wÞy'f³ù‚‰îÑÕÕÅ¡C‡hoovQë\âíé¤îíç¨{û9ôödôöd“´ÞÞ.úZêÑüçgæÏçã‰'žà¦›nbÆŒ¡ð­n·›÷Þ{={öðÀŒ»Ÿ±Þ‹@€ßýîw¬]»–¥K—†êÊÊÊxñŹí¶Û"ær»ÝüêW¿bÙ²e¬\¹’¢¢"ŠŠŠBÇ›šš¢†ù‚àÂ÷Àõx…»ØÇ³sçNššš¸é¦›ÈÊÊ ‹zÔØØÈñãÇÃÊ744ðãÿ˜uëÖ1{öl.»ì²Ð1MÓ¨¬¬¤££#¬Î±cÇ(,,Äãñ ñfv»Ýüüç?gݺu,\¸’’’çÕÀ"éhF5McûöíôôôL˜Ñm€.g|÷1¶Ø2¢}åêG9T³ƒw?A[OÃ: æn\x7 ¦£H5uÕòÜŽ_QÖ°Âä„s77 æˆ×¸ÎÅ\1***8rä×\sMè¹ÆçóqòäIžþùPøÓ± iøÃX¶lW\q™™™!ﮎŽÞxãa#Á566òÖ[oqà 7„ž!!h¬xûí·ùè£"Ö‹»»»C¹ÌJKK‡ôÆÃxž=¼Á+àt:éèèàäÉ“ìÛ·oÄ|k½ðÌ3ÏPYYÉUW]E^^^h#@yy9 áóþ¹¾3VÃÀ{ï½ÇÁƒY¹r%¥¥¥$''SXX:îõz©¨¨à£>âÀQ•E‰èé7˜H¿ÓRffæ…ñ&"@ Î) ±$Ö>—˜L&4M %W ªª†Ez¸P /‘$‰äädÌf3ÍÍÍ#.Ìnذ%K–ðÖ[o±yóft:éééx<ÚÚÚb«,ˤ§§hii¶žÕjåá‡&ðoÿöoØívijjŠy1Ùn·ãp8¢îÔ¶Ùl$%%ÑÞÞ>êëÑb±’’ &Ë2>ú(’$ñï|GDû¸ÈˆÞâp8èèè˜{z´÷âÙ$%%a±Xhllõõ68:MgggÄtçš‹y1çv ª ÏÅ›ûŸæ#/âœÛ¹ö\Ì ˆ9"2ó¸&r®-³gÏæöÛo§¼¼œßþö·H’DJJ ƒÆÆÆ1{¹ ‡Õj%99™–––˜Æ>˜„„ÆT7V.\¸[n¹…ãÇóä“OŽªXϳg<9ß÷‚Åb!55•¾¾>:::bJ•5Q÷B$î¾ûn¦M›ÆÆy÷ÝwÇÕր纪ª8N:;;'äþ@x¼ @ ‚ ŠÑ¾ÄÆëõR__Gi&MÓhkk KD?|>ߘÆFÜ;ÝÝÝ£ö ©|OOϘv²8Gèsbb"Š¢ÐÒÒ"ŒnÎEô–ñÞ‹cÞé}!F§¹˜Çãñx8}úô¨êôööÆ´è‹·‰¦iãÒßù&ð³õèËì©z—|‘…S×rõìÛе–—\‡D0´âþSïóÒî'èp´œYc¹ÖæÍ›ÇìÙ³GÝöSO=fÈû¸Ì’$ñ…/|aÔõ8ÀÞ½CC_(ã ñž+Ö¯_òˆŒ•¦¦¦ˆáÁ5M£µµuTm–Xç½HtuuÑÕÕ5¡ý8p€ë¯¿žiÓ¦‘––FKKüç™ñ<{Æ“ó}/œý ã¹~FË@>Ïá¼cå\Ê Âð&@ ‚!A=zž%‚‹“ž¾þôþOØvâoܲä^rR‚¡¹$$š»ëxnǯ8Qns?Ž…´´4¦OŸ>êz’$]ÐócE’¤1éãbÙÐt>ÉÏÏ'??TuL&ÓÄó1Àãñ°yóf®½öZÖ®]1ß™àãÅb!))‰¾¾>ª««Ï·8£FÞ@ @pQQXXHFFÇŽ£³³MÓ0›Í,[¶ŒåË—ÓÝÝÍÛo¿}¾Å‚‹šªæcüèµ{™W° ›1—×ɇ[ÎyXɱ²uëV¶mÛ6êzSxÐÑøÞ÷¾7êz±„ž»Ôyê©§eyTu>®×Y¼øàƒX²d ³gÏfãÆãÎ×,¸øÈÍÍ‚yç.ÆûEÞ@ @pQ1þ|,XÀM7Ý„ßïÇãñ„vŽ·´´ðÌ3ÏŒ:@ †¢i{*·žo1Æ„Ûí¿gq¡åõý¸p!åÞû¸à÷ûyþùçñù|Âèv‰âv»yçw8~üøùeLÛ@ @ \ÔÖÖ¢×ëÇ•Ÿm´ø|>öîÝ{Á…Üzå•W¨ªªbúô餤¤ ( åååÔÔÔ°}ûv‘ÛM ÁEAGG{÷©é|‹rÁQQQq¾EœGªªª¨ªª:ßbŒ)33óÂzƒ@ ç„””@ì~@  ^Œ.ø¬@ @ @ @ ˆˆ0¼ @ @ @ q@Š~d¨ IDATÞ@ @ @ ‚8 o@ @ @ A†7@ @ @  èη@ àüÒÕÕu¾E8/H’„¦iç½K•¹³€Ÿü¿Äó,‰àB&)) €ŽŽŽó,É…OJérÌéSð¹4}ô:¯;ì¸$ÉhZàü^×ùEÐ5g:ƤL|Î.:+öžoq$ ÉŽÏí@óûη8c&7Ÿ@ájøÐíþ#ôuÆ­mÞÀòÛïàè»i®<·¶ÏªÑ„¢Óãê½øßI4MYÁ7ï3`°CëI”ã›Ð4-ôo œ¦i¤§§Ûžué×&T^³= ¿ßhèT:½Ÿ×ƒÏ|†p;z‚rX­ôööb°XÑ©|^7h€$`´Øúë¹0 ú¬(ªž€ßj/%³‹Ð4ÚÊ:ò sÐ4O0{ölÉÌ̤±±‘¦¦¦~ùLȲDw·‹Å¬Ÿ—A[[7GªªÃåò ×ëÐétø|>@Âï÷³÷Äj1ÅE§¬¹€0¼ @ —<²|éÂÐŒN›A{[Í uc2žI’DÑôÙ8=4Õ×âõzF¬“˜”JJzfÌ}Ô×VÑçtŒZ¶‹Ä„àÿ—âõ'ˆ.¸l!®“‘±d¢3Ùp6WßQg’tqëQ§7£Zì@ࢼ&žà‚àÅ(ûÇ{v1ÆÔâ¼\Àø]=H€t±ž#Õˆ¿t’΀R¾ÙÝ q‹¢(XìÁ+Uo¸è®e¿Çßã¾èäŽDðBC=µßÜ[!g6R㤮ú!Æ· œóø}ø}^ºšêQT=²ÇMjî$äîf4MC’$¦L™BÎJWS=ùÓ.ÇïõÒ×Ý’„³«ÙãfRñet6ÔëzÜdÍD ø‘>›ª}Û˜7o^¯UUñz½:t“ÙH  +'¿ßú<€ÕjÅf³…þ0¼Í˜‘,KìØq€Y³ hhhGUTU¡¸8‡¦¦N¦NDMM ³gO¡··¿ßÏAB‚Íþä@Þ@ ‚KžÃKîhÉÊÎÃj³#+ -õHý»5GCbr* ‰IØii¬‹I&‹…ä”´˜ûhmªÇíêµlI¶àÿ—âõ'ˆax‹cB²NEFûØêKg0¡7Û àûØŽQŠÑ‚=oΖjÜ Ú—j² 7Ûð,âšLþ©+‘2ÁïA©ý0îDYQ0Ûƒâ£áíãDȨÖYƒæu°eÀÔ•H{ÿz~‹‚¬èø}höºSäÎ\€NÕãq9ñ{=ô¶´ i¹¹¹œ:uŠä)3h¯;EjÞTÜÎ|^~¯£ÕŽNÕãèhÅš’ŽÏãF§ê@ €ßç"à÷‡úíêêÂçó ò< âêsáóúQ *J@ }@Ó4òóóq:äççsòäÉ#(Š*×ÑÑKJŠÖÖ.,#€F]]+éé‰X,F¼^~¿½^Å?H®x" o@ Á%Î¥ör®( Y9“Ñ):êj*Ç<þœ¼ #­Í øý±-wµ·âÁƒÍž˜LîäBš†ÇãúØžŸaxÄ€0¼ÅŽj¶"+z”Q,ºJ’„Îô’ð¹z#‡q“$ÔP'šdïÞñ¢3'`J›Œ¢ðõõàl>EÀëB§7¢šlh¾Ø<#£ szŠÁ‚ßãÂÝ~OO{Ôò’¬ 3''ŸËæ÷Û¾j †Êõ{\Î )I2:³=b½€ßß5|¸­ˆòI ÆÔô¶$YÁïvÐ×V‡Ï{x¶ÑêdbBãÐÌ ëUªÉ†!!Cb:Hå»iÞó·aš‘0¥ä¢·§^G'}-ÕÆ#|ŽT³ÕdCµØ1Ø’ÃÊjZŸ³{Ü£IŽñ^ká ^'²jDÑCŸ~~Woè³ÎdÔžÎ`Áïuáj=§§u1d )Ùl©H:•€×…§·w[݈!oeÅ`Žx,àuá÷Œ.T«¬ÓcLÎFµuèíí ¯¥M~±]Ñ›‘U=~O_(t¯¬Óõ`N@ó¹ék©Åë9²úUH¦äª(>ט½Ý$Y&-öÔLü>/ µt4Ô È &[pžÖéõ#ΊÞ@zþ4,I©ø½º[h;]Ãxb™S$ ¯«OßðϲF[ŠNÅïõàêz™’d%BMpt¶ +G43sHÌÊE5˜ðö9éh¬¥«¹>æ¶Æ¢“H öfS¢¥ÝŒ6y>ò±¿!¹zCoƒ½¸Î'=­Ô—Bë—§öðGË? _Ú½å;7#I2@ø}$ËJÿƒíI’Œ†6D‘û94¤ èñìÍ‘;wî$z½Öa[[:Biéä`hʧ#zîÛwY–©ª nê()É£¬¬6¢,ãEÞ@ ‚Kœ³_hÆâýu1‘™=³Ù‚Ï磽µyL úf‹Ôô,ZšêcnÃçóâë~q­`êtôÍ uø}_¯axÄ‚0¼ÅŽj´!éô(º‘]Й(ºå;Ôn~’ÞÓdž”‘õF¦Ýúuïþ™îSûã'ôÙH YKn"±hq(o €æ÷Ò²ŠÞ€Îd#0¦Y§'sɦ̅³Âk:Ê©ßñ<ÞÆ&EodÚÍÿŠBë¿Ó²SÔ>L)¹ä_w?5›~‹£¡,ì¸Þ–BᆋX×Õvšª×µíH$•,%möÕ(Æ¡¡°úš*iÚû}ͧ¢Ö«Nâ=6éó¯Eµ& =¨ih~/€oO;²,G=ÏÖœéd.¾ Õn, xúhÞ÷Ç·E¬gJÍ%ÿÚû†|Ÿ8e.‰Sæ†}çj=MÕÆÑ§X‰çµ6ÀXu2}éóׇ>¾FÓç_KJéÊ0cQoÍajßùcĶ’Š—’6û*“uȱ€ÇEgÅG´Ú‚¿/²ñ9±pYKo‰x¬íÈ»4ôzÔqœMêì«H)]¬ÏÝäw;iÙ÷&'vD­›¹ðz§-¢eß[´{Ÿ´9W“T|9’¢ž)¤i´}oX™4K RZ ´”ù÷,½ ˜Ÿ»krxä„–ê“ìzáÉ3†7uøß€™kn䲫6 Œaßw·6²ó¹ßÒPv8b½5wÿ+©¹…4UãÍ_>µ}YQ¸ùÛ¿@o2sðí—Ø·ñ/CÊ\ÿ/aNHŽPþüÍ;CùÅb!kÚLmø3r†ënmäàß_¤b÷»Ã¶1VD"ÌÐÓRNÀhŸÒ¦!×í‹Î6E£wûÏbî{,œØ>üñEþ†²Cý×^$IÆëvÑÝÙ933 EQ¨« æÓÒ‚×¥¢(¬_¿ˆ×^ÛA pÆ3¿¥¥³¿^rÄãI8.ÚÛ»±ZMx<^¶¶îðþʃ߯Z5›²²Ót® '' ·Û h˜ÍFœN>_»ÅˆÛí¥º\C LLèOax@ .q.µíÉE¨zMõ§Ñ´À˜ ySQõz»;éí銛±ÒjK -sõ5k#è@…XÇh0p»c_ˆ|<ôq¾ÆJÆ‚ëH™¹:ôYé_XÎZ|™ ¯ +Ûzh3Í{ÞÒ†¬È¨ý†YQ#êY–Ï”Qtº <yk¿4 îözzꎃ$aË.!{Ù­øÝNƒ™€«7ªŠÁBÁú{1¥å¦Ñ[wWW:“ {ÞL æ`Nϧò•Ÿàé 74¼.úš«°Ì&¹ørZü=ª´IÓ¢-x8›NF'€¯§-ì½-Åhŧ7ŬGI’É^ùY’Š/‚ž‰½u'x]èíiX2¦`Ÿ< kv 'žy_ßPï’ñè$ž¤Í¹†ÌE7Ð[{”žÓGñ¹z1$dRºd™†ÝÃŒ!‘ô”T|9Ù+?\îí ·î8ŸKæŒ)9䮺£=…Æ_RW øp·×‡>›R²Q fü®^úÚÃ’#Ÿ×øßkm|:¸zp·FRtX2 ‘S²‘$‰¬¥Ÿ"uæxzÚU:£ús>Mæ¢H›s ®öz ' øÜ(ªcj.æ´Éd̽sJ§ÞüŸˆ²h>÷{ǘ< IÑ¡Sõ±I"oÍI˜2€¾–jÍUȲkÎtŒ i䮾CBMQt¢ÓPÌ“Iž¶cJ6½õe8Ê‘•„)óÐÛSÉœ¿O{]U‘7%h9sŒÁÝFJGÕ˜®©ô)%\÷À£è F|ÕvÒÓÚ„=-“¼Ù‹¹þÁÇ0˜ƒótTI«î¸ŸâåWÐZSISÅ1tªJÎŒù¤çqÝ?à­_ŸÚC©^sp9ÓçwÙB¬I©AÏ´äÎ\€=-€Sû¶G”¥·½¿ïÌF4£Å†-5XG–å˜uT²ü–ßþUdEÁçõPüŽÎvÌö$2‹fž_ÄÚ{¾‰ßí¦úஸëd$dg;šÑ Y¥P·?46-Ê=t¡¡7YЛ,¬¼óŸél< š†Ïãâ­_}€»îº §ÓÉ3Ï<À­·®FQd~úÓX¿~1~ d€»÷Þ›èëssút+’ÿû¿opÛmWD<^[ۂѨrÍ5óùàƒ#ÜrËJúúÜ<öØ_ÂúKL´Ò×禶¶“I’ÿ–[Vâñxq¹<:tŠ… ‹ƒ¹ãL</ééIôõ¹yàÁ_Ç]wÂð&@ \â\ /}ñ"%={Bp—}CÝ©1]Uõdç È:NÇ×8–_XŒªSéìh«A/&“‰)S¦päÈ‘qµ3gÎ>–Ÿ!Ìý›Šc£ÅbÁf³âtöÑÝ=1a¿&.¥y*VU:(›¤€ @øýèwöDÔ¡$É¡ðr²¢ÄPfâ oIÅKH. —Zö½EýŽçC¡©ô9×µ,è£è QåÈYy;ÖìiøœÝTmüÎA^`ª5‰Â¾Ž1)‹Ü+¿@åk?R¿»r/IÅ‹1§OÆœ–G_käTIÅ—£è´9€az>G/ÿWØw¹k¾HrÉR5ºüg“>ç*Rg$Ûo¥~Û_ Z°VÍvÒæ~Ow+~Wäó<^ÄcJÙËnI¢yïßhØñbØñŽãÛ)ùì÷™|ÕÝÈŠŽöcDn')‹¼5ÿ€$+´ÞJÝ £˜>÷²–ÞBæ¢é=} Gcx83Og#§y±Þø/Xs¦Ó[{„So]xȹ'^×ÚxuÐU¹—®Ê½¨Ö$J?ÿ#½‘ÄÂùdÌ[G_K §ßy gK 9«î eæ*x‡èF5ÛÉXp=’,Ó~lµ[þ0¤cr6ó¯¥ñ£W£ê¶§ú=Շ¾›~çÑ›¬ÈºÈÎ&¥t%É%K8ýîŸh;¼5tLRtä^ñy’Š/'kÑ8NÇÑxrH²N¢7’R²Œ€ÛIí;OÑyòL¾öÃïPrÇU#)¥+è>u ²0É“‘ôfÐüÈ=-CÎßHH²ÂU_úæ„dœm¼üƒèh<:nOËdý×ÅÖï mƒDéªõÌZ{š`ËïÌñ÷ß Óé ¬ýÇ£pá*®úÒ·øó7îN²ê£XyÇýȲÂÔÅWppÓ å-¾| z£™æªt5žŽ(Ë[¿ü°Ï%+>Áš»¿Ñ?ÞØ oSJXsÏ7$™†òüõ«ïãèh93&UOÉÊu¤åQsh÷„è$ä¾É˜=ÄèvvÄ ‘Öêr2&ávôàs÷a´&àîµ¶¶—ëÌgMÓÐëÏx…Z­ÆÐ&*N¡«ËAKKN§ ‡#§cLcÏÍ/Do0âõxhn¬›þ F#ÙySd‰ºš±íˆ _ýêW)**âø»vEØk×®åÖ[oeçÎ<ùä“£ªk…áMUUôý9KdY¦«kb^Âã-:»^¦q×ˡϳ¿ú;d†ŸèÕÍ›MQûjrÛ,…ÊHQŒsñ`Ò’O¡¨FúZkhØñÂCËM˜3 ƒ†+]dÕ9£€Ô™«¨ÞúKúZªÃÊùÔnùÅŸù‰…ó#;ºkBÀb°\|9õm§9Kf!¦äl:OF^Є¢SQT#rŒ^;:“IË?¬3Ð]µº÷þ „ŸK__ ÛŸòýñÐIÂçölPî$ÊÊ;ïG5šØûÚÓœøà­°2~¯‡w~ÿ#&_¶ˆ„ôI”®¾–o>ÖF_O' e‡ÈŸs9ÓW\ῇÏú™vùZT£‰“»·Æ>7*:TcÐk[–¤˜ê]y÷¿¢7Yp´·ð·Ÿ~·3ÜÚïórdË«@äk-:‰ÉëÕˆdN µ?`t»žmŽð{_z´-¼éNö¼öçÐñM›ÂÃä–—×a4ê¹ùæ~ó›×ñû(ŠŒßŒ4¢(2>_07Ü@¹_ü⥈Ç£Ó)¼óÎ~EÕèï•W¶Gl÷ĉZ^{m~0¬ç¯ý:š¦qãKCa-eb¢¿Û@ @p‰s1¼ôÅ‹ÕFæ¤`þ‡Ó5¡qët*i™Ùø}>š‡.z F’$ò KÐ):jëÊ ΄ªLÏÊAQt47žÆ?Jï/€üÂTUÅÕ礹±nÂÏËo¼Aqq1_úÒ—e™;wŽªþÚµk¹ýöÛéééáí·ßµ¼Æþ(0±ÔëîîF’$ì¡DÂøvi0œÇ›ÉlB§Óáóùð¸=øýCiba¼í\(r(ª>h<…qL’d5x3JÑ oƒÊD3Îcræô<:Nlï_—ÚæëCQõ(Q W)Ó—£¨zÜ]-tWŒX¦¯ùÞîfŒ)9$ÎÃu¶±#à§»r/©—­!eÆ vŠªÇÙtG}YÄv|ÎnUÎ`‰*—dMARHαZŠ_j0âêé¤r϶ÈÍÞîP~²HÞËùs/Ç– åxèí—"ßn•{>`κ[)Z´Šƒo ½Ë·¿MÑâ+úÃIfÒÓÚv¼`ÞR,I)ü>ÊwÄþl¨èt!ùcñxKÊÊ%§4Fôà¦ðô~3]¼tÚŒŸ€jKr˜Áíbñx+Μ†ÓãdJZ©¶T^xùÿ——Grr2ú)sÃrÀ}x¸5,Üœ9sèììäÔ©S@P/>ŸŸ©¾…£ñ$;[T$I&sédêÞšIËo£îý§Y¾|9ÇŽ£­-Òtúô餤¤ðÁàóùyá…÷‡È:ØXé8Ê«÷òËg6wD2òÅax@ .q.Ã[þÔéÈŠ‚³·—Öæ†Ð¸-Vó¯ Ïáà·ê†m#cR.›MÓ¨=žgeÎÂe¨ªžwÞ|‰>ÿèBÑ(ŠÂäÂbdE¡öÔI`âwÁ?~œŸþô§<øàƒ|å+_A–evìØ1rE‚F·;3žž~ô£qútäPBáÓXL&SLå=ƒÁ€ÕjÆ·K„Á»ÃcµZ±ÙmètÁežî:;;GÝþxÛ¹Päe]ž¶ÑÞ¤`hJ¢/vJgÊH±çÞ ¶ìé¡>ú+£/`ËJpŒQ €ö‚9ÈŠJ_Sô6\mu˜Ó °dL‰X®ýø6Òç~Sr–Ì©8›‡æ“H.Y†¬¨´GYÔFHþCv&ÎEVT|®œÍcó„Ž—NÆæGVTT“-jûŠÑº ÍÁjÏòÆr6”GmÇÕV‹¬¨ÈŠŠ1)WGCÄrRÿ}3QFå‘ïµo žºkãh78Õny’Ú-O†Ê†÷qgË)¬™Ed.¸ˤ"Z¼M×Éð¹zcÖI$dE7ì½?[né™ù¤9úuß×\,§€9=GCyØñXîW-à êu8cº$#)*Œ"Äì`rgÎGÑ©´TW@”ÜÄÞˆÁî†ÎÓ³— èTí-t·4D•£­¶E§’Q8=b™Šß%àõ¢šÌL_±Ž_þcØñé+>¢S©Þ¿¾îÎØ‹dùŒüŒünR0oy¨|ÍÁؽŽÃÚˆ“NFD§G’uhý×îÙÆ· $K"“ÞDMÛoèüü|Ž=JÉŒåÃæ€óûýhš6$˜’³qw7‡>þ¿¦¦†ÂÂÂámÆŒ|ðAäÄ*Âð&@ B\ /€cAQtäå!Ë2µÕÁ<cÕë ¡†#JQ)²,ÓÜP7$T¥¾?çÐX^¤s&OÅh4á÷û‡ô&’²²2~ò“ŸðÍo~“¯~õ«È²ÌöíÛ‡­³víZ¾ð…/ÐÝÝÍ~ðêêÆæ'&‘‘>꺪ªbµZq»Ý¸ÝîQ×\Þv.9 ‘X–G•?H’‚Ƭ¢(3š¶Gƒb´žéÃÙpì^.¼Ë‘ó±èÌv$YÆœ9s攑ûUôQŽh´{Ÿ¬¥·Rº’Ó[ÿ7t$¥t’,Ó]µooûˆ}„É/KÃÊ6ª%8žÀ0:‰øéd|´ÜLÆ‚k1$M¢ð†¯Sñòðt·ö•ÈXx= shØýrÄ6”þ±¤Ï½&¦~eYöxèš’&&ÇÏÈŒïZ‹·NÏ ^Çè½w}ŽNŽÿùÛ$äÏ&eÖاÌG5Û±åÍ–7‹œ+¾@ÝûOÓ¸ë¥Qµ;pßÄržt¦à|rvèÈ!|¡±Êý^ƒûæ~iN\½ +`´ƒ¢ìáÈUÞhîw¸¹QB=‡•Å”„,+˜S˜Ã#ö«ê£8޾ó—]õI²§Ï!1#‡Î¦`hÚ) V`´Úq;{9¹këˆ}D—äßsB2²¬Ã2òŒ†xêdXŒ )Ðox»Øxmÿƈß8p€¼¼¼sÀUV7œ ®q÷˰;øì¢i8Ë™P\LYYYX;%%%TWWPçax@ .q.Ã[añ ŽºÚJ¼^O¸§šÁ€¢ÓELFÞÆ,Go-MõaeU½E7¼×H42'å’˜À©Šçå|”Ç×S IDAT——óØcñÐCñÀ ( ￞áÊ+¯äŸþéŸèèèàûßÿ>õõõã’ÕªÖÐÜûk©ÕjÅjµÐØØ„ßï¿$®ßK™h9Þzº{°'ØC¡»»ºÇt-Œ· EèÏñ¦bÊ4@0ŸSp11zŽ7)Tf¢Âñi~ï™>† Ç̹d沋PFóyÐY,t”í¤yï›#öëÆ»®ýè{ä¬ú¦Ô\l9%ôÖ$Rg®FQ ´}oÔºæx‹.ÿÙ|Õ0lxÆ‘ˆ§NÆCÀë¢ì¯ÿAá'¿ABþlæÜû$}-Õx]˜Rr1$s-5ì|‘¶ƒ›#Êðô…®““/þ¿gdç¾ÖšaÇÌñf>Tà3žk-Þ:‰eNˆ…îêƒtWIÂ:iÉ%ËH›·Õl#ÿš/¡èTê·?s{Áo1Îo?Šj@ÑéûP‘=‹½)4VÍíÒîHóMÌe:ª‘ {éÓ /ÿà¡ø¼ÈŠ‚¢S1˜­ÃnF.Ç›×åD5él¬åÍÿ~dä~‡y¾ª>° Wo¶” f\y=Ûÿòf®¹Õ`äð–WðŸõ¬=²2(Ç[ ÏÑ^·3T^o4ãu÷ÅÜW¨8ê$*’Œ–>-h íif2–ö¬K¿6º>Ï!M€5#ü»cÍ`^|_è³ÎhIÆ××ül²¡Îlœ3¥æáím†£Õ4¬&=&“‰î4;éË|H)7„Êž2/ï/¹öEZ[[q:ƒ†MƒÅŠ„„¬èðû}äΘGí‘=-v|^‰™¹ô¶5ÑÕ\>–W¿;~…œ…0¼Å‘üü|8zô(^ïØw$ @ ‚ø‘•ƒÕ|N¯,ºØ¡ªÃ`ÉœÉAOª“‘Ú»‡@añLZšêéîês;㥬¬Œï}ï{<üðÃÜÿý!ãÛêÕ«CF·‡~˜††èùrbE"¸H28ìÌpœmt‹µžàãIOO>¿UUñz½xÜcóo;Š;¾Až5z[ ž®æ1µãénEµ$¡ù¼t–ï—L}­µ8ʱd‘\º’ÞºØr¦£·§ðºh?>|XÞxàínƒI`HÊsñÔÉxñ9»ƒ^ šFoíQ É“0$dàsõÐvx+ÍûÞ¤ûÔ¨õÏxȳù}-5çBì g<×Ú¯M£·î½u'hØù3îú†„t²–ÝJÃŽ‚ž.qÆÓÕüC’ÐÛ’ñô´E,§OH;S§7r™x Wï ýÈžƒ2 À³«[JöÔÌ1ËÐÓ|nÓ-”ïÜ2æv4-À‘-¯²ä–{(]}-ÛÿòUÏ´Ë×pèï£óf =­gB&MÊ£¹êÄ0¥£´GDCK-½¼.亃ÒÇ…Ž5w:’$ÓQüý1g¢Ó›ñ¹{C¿’¢Ã–[ЦHôµÑÒÒ‚œjƒÀÈóCWWIII!Ã[Þ¬Et6Ô’4)€ßGo{p>Л­È7Šª¢ zo›7o^¯—ŠWã7æo¸ of³™¢¢":::¨©¹À~0†á¶ÛncÙ²e|þóŸ%ÿ@ çœÉ…@p¡`Æì…CŽ›ÍVŒFKW€Êò£4ÖyÉÊÉGéÏ’[@FVNXºAÆ»ùKVá÷ûikiäÄ‘ýÃÊ–˜LjøÊʲ££ZÜ)//ç‘Gá»ßýnÈø&I÷Þ{/ííí<üðÃ46FÏ•3QX­VRSƒ^Âè& ÏÙG£ßåïv.9ÆÄ ÐNçÓ{Ô1(·’%«ˆÞÓ£[˜ «j?–¬"l“gÃWÆ޶õÐ,YE¤”®¤æï¿#¹tíǶðNü<ÔUµŸ¤’¥èL6,YE8ÊÇÔF:Öª½Aá9!™©‹WG,3÷ÚÏ ÛÆ©}ÛqtDVÞ9þÐ…'†Œ]¥«¯£tÕµzû¥ ×%@Ë©²PÿK>u7&{RÔ²ÑòÆ[':Æ?;hÛPʶ ¹{ãßÇE€··OOÖœ¬ÙÅtœØIljtU쥳|wÈã·«b]UhjjB–ez•Ñspdƒª¦i´¶ž1à7–áÔ¾ô´6RñÑû´ÕVRüµ‡?¢jßvZkNÒPv(T¾¢¢bÂòÆ]p†7@ @ ˆ'‡öíbë[/Gý7àiæês†¾««© kãtuŰmìÝõ^¨ìÎ÷6±õ­—9²ø¿F“™ì¼`øÊÊò£hç`¡"V*++yä‘Gèî¹™‡zˆ¦¦¦ó&Ogg'§O× £›@g|®ÞP¶´Ùk‘•3Þ»²j$oÍÉ[óÅ —CÓ4ì †'³L*"gõ 62“³)½ã‡Ò‡mÇÙXIóþ·˜|õ?’¹ø&$9<Ø“-·”ÒÏÿ©³#øãutÐU¹€¼«ïAoOÅÓÓFWÕ¾Ñ oÌx{Û©{ïÏ$Χè–o£?Ë@j™4’Û%ÿ_‰ØF¼u2¼Š&]þ)¦ßñ…7>È”þ…)7ü ëïeÒòÏ`É,¶š·GÀçÁ˜IÉíÿ‰)57ì¸b°0iÙ§™q×Ï‚#Ð] ÿfÉ*"iÚ’³Ú2cÎ(ÅÇÎx®µxëd¬d]~3sî}’ì·¡3ÙÂJ©—­!õ²+è­;Žß31¾š îý WYâÔd¯¼=Ì0­3Z)úÔ¿£3Z x]¡{l"Qö=‹älYÁ·üË£ª»ÿÍgéë†"_÷Ïß'iÒäÐ1IVXúé/±æž¶ ¿ÏËßÿçÿ0}å:®ÿ×ÿÂdK +“”•Çu>Æõþ0&¹½ý2³ÖÞxNÃLðÆÏBÓ4¬)éÜùø3dÏ ;nOËdý×þ_üå‹a¡˜„µ_º-1!CÞ;:/ǦÞN”öR]}äig<6ív™™™X­&ìvsÿ{†iÉ}x¦ß‰uÙ×°\~ÿöz·ýŒÞm?£ÔûÚþßÐ×׋ɤÇn7c±1›õØlFìR'i© ÌŸ? ‹ÅHBB™™™,VŒf{‹©‹Va°X‡ôÂ~i- Ó§Oç£>W£K—.e÷îÝæ¦'@ ±âtô {Üå î¼tu¶G,ãv»p»£}äA!Tzº;q:FÞÕZP4Y–ñù¼ÔL@x›ñRUUÅ]wÝu¾Å!Þ/‚‰¡éÃ×Ƚò X³K¸ìŸ~Kïé£Èª[ÞLt&»_!qêBŒÉ“&TŽÆ]/‘4m1Öì²WÜFÊŒ•8*P- Øòfâw9húh# ®¶šMO`LÌÄ^0‡ÉW‰ì·áh†²4¥æ¡·§ Z’h;ôΈ¡Ûm!±pÖIÅÁχ·ÆäÑ‘P0—ÂOþkØwá´ÌS˜÷õ§ÃŽíÿïó@ ~ûsSrH½l É%ËH.^г¥š€§}BzÈgÍ™Ný¶g#†î‹·NÆJÍÛ¿Åš]Œ!1{þìˆer¯ø<'?¤â•‡¼ ãj¯§â¥ÿ¢ð“ßÀš]Ìe_þ¿8š*ñö¶£3Ù±d"õ‡†N.]AëÁÍÃÊÔ² oÀ”šË´O—¾æS¸{ZQÍ ˜Ò&ãw;8ð«»ñÑKo4ŒõZ‹‡NÒç­'ké§Â¼ƒ¦\w?þþk²öíßG ÙAPâԅȪœÕw’½ês8+ðô´£è˜RsCÞp^G'Uµ­ì•Ÿ%cÁuaß©æ A$cþu¤Îº2ô½³±’ãOgHM{6bŸ|É¥+ÈYõ9Òç~GãIdkÎt½ Íï£âåGÍW¼}èÞþ!Þ~ˆá(^D®‹žÏp0§ƒ?ý67?üKÒ'ñ•'7qjÿ<}N²¦Í"13‡]/<Éœu·b0G7 Úü2é…%,ýô—˜ó‰[˜yå ÔŸ8ˆ§ÏABú$R'!I>›mÏüšöºSÃÊuxóË\y÷¿’š7€úi­©q<ªÁÄ}~/ì;ÁúûÞ?½vÝ¿ôŸ_ yý ¦æÐ‡¼ñ‹‡Ywß#¤s÷ÿ¼JGC ŽöL É$gç#Iš¦1uÑjNlÛ4á:@3&à_ûMäï!·œ¼4ÃLF½ŧ Sttõu…¾OLL¤¥¥…Y³ò‘e‰;Fr¿½=ø~·hQ ^¯¯×ÇãE¯W‘e ‡Ã…ª*èt Ó¦åÐÞ¥…¼Ë–DÌ7„Þ¾ýío3sæLüq¶nÝ:¦o¼ñFî¹ç¶lÙÂã?@ `∇§Ù(›Pù…ÁÜ"5Uåø¼ÞñË c aûsèmɤ/¸Cb†Ä \muœzãW´yKæ” 7¼¼nN<ýyWÝCêek0&gcLΠ»úU¯ÿsFÁˆ†7¿§ãO?DÖÒO‘±à:ô¶¦Ì ÷ö¶Ó¼ïMv¾“©ýøvò=}(z­‡†7ä éô¨–ÈáÏ$Yr,Z84-à§â•Ó]}¬ÅŸÄ”ž9=?tÜïé£íðVê·ý5j¾¬xëd¬xº[9øë$aÊ< IYȺK)h`|–ÌB§.¤äöÿäÈïî˜Ïªýø6ÜO=HΪϑ8ua¿—Ü™|®]•{©ßþÝUÃçYø<ûßoR°þ>’¦-Á”ž©_¿ZÀGïéã(Ë91¼õZ ÖŸNt&+Ƥðˆª5™34RK-àçØŸ¾EÊŒUd.¾ kÖ4,YEX5éw;i;¼•ºž6·›b0G½wdÕ€¬ÎÈm¶GHãäK?$«¡œ¬Ë?…Þž20ôÔ¦vËSôÔv\ñDÙûWüÅk _…wÃÏÐÿî“H}1Õ=öÞ<÷Ý/³öKÿNrv>… Wàêíæï¿ùOv>ÿ; æ-#½ xØv6?ñ e‡XvÛ?‘9µ”¼Ygr{]NŽlÝȶ§ÿ'&SwK#Õv‘?çr ?Ìd H²„%)5êqKb¸W¯NoˆRö¼ú'ZªN°âs÷Q09IYy$eåðù¨Øó><ý?ÔŽîdOôïõ?@3'#µW£¾óÿÙ»ï0©Êëãß;½lï½Ñ–]ª4¥HUD,D£bרX¢‰ñ§°Ä$–XbŒ‰1ÆKLl ¨€QD@ŠH_`aÙ²½OÙÙ©¿?†Xw¶°ìÂù<3sßûÞwfî²Ãœ{ÎyþŒ º˜uf4F ö¬Ž#&¶örê£.`ì®–7©©±8Nìö<:¾†+f³–'/p¼ŽzÀõ%!!!ðÞ6Œ_ýêWhµÚß.¾øbn¿ývšššX¸p!EEEǼ Aƒñûßÿ€/¾ø‚^x¡[ûeddpÁ‘‘ATT¥¥¥äååñÑGµi°w´±cÇ2~üxÒÓÓ ¥®®ŽíÛ·³dÉ, f³™ñãÇ3zôhbcc §¶¶–¢¢"Þÿ}ªªªÚÌ·`Á&L˜À7ÞHmí‘+&L&3fÌ`̘1DGG£×ëijj¢¸¸˜O?ý”={öók%„B!ÄñˆŽöÿÇÖj=3{´êŸ=”Üác°Û¬|þÉ»=š#"*†Éç]À矼ÛeÆ[fÿÁ åÿ¢â‹eïc³´¿ªÿtWô©¿ŸBÆé]Œg²ÌL™·Â“¼’ÓŸÖŽépfLKCÍÕ%'m-SÆèÔz3͵i©ïYIE¥Æ•„.<…–ÆJškN~š=¥ˆG‡JgÂe­Ã^yà˜‚e§úk’xöå¤w+ùïýÖß«ZS8úÈD´æpÜÍVìU…=’i͘â³Piõ¸mþ¹ú¨b_êÍפ§Z†5ÆP¼7NK-ÍÕÅ'¤ÿ×÷)Š SBÚ(¼Îfõåþú’ωóæ÷ðÅöGU´Ý›×§û_)*51©Y„'¤Ðb³Pž¿·³}¦lw„Ç'‘‚ÎhÆZ_MeÁn¼?àꦈ("“Ò1‡GÑli¤² gó±÷½ñš¸f,À=özhnBûÖ(åþrò>Ÿ¯×‹×ëms;,¬ƒàña!ãû ÿÜ)@­VNT”FJåÏN<uY§û¥Õ€Ïçc÷nÿgEQP¯×‘FÿþÉh4jöìiû¦õx ø|>’ ¥,¾Ãû|ô+âc:îx,(¾—ñ¶}ûv~õ«_ñØcqß}÷t;ø6{öln¿ývY¸paŸ5¥ fîܹÌ;·MÃó¤¤$ÆŽËyçÇo~ó:ئ×ë¹÷Þ{™4iR›y"uÆOPƨd께ÛeoÄeoìbT÷¸l §ÌÏÀñèÍפ§zëg¸7ø|Þ@iÕ“M±×£{ózœ?yoÆ8œsÿ†î;Àݽà™Ï롺xÕÅûŽ{-•‡h¬<ÔõÀ{CöJÆw×ñ¾&îi÷ã>û&pÚѾÿST»;Ûͺ®{II?D@ûK|îks/!!µZÓéPÛ㣰Ù„;]8n&MJ~~)£F  ¸¸’ªo8ëìÁ|»îSbbüÙ•«ÓÉçÌaÉ’%ì]Û÷%f5ß`çÎ<úè£üú׿îvðmÖ¬YÜqÇ444°páBJJNÜUQcÇŽåºë®Àf³ñÕW_áv»?~<111$''óóŸÿœ|0ЬüÚk¯ Ýêêêøæ›op8deeáõzWü®_¿ž°°0¶mÛFUU†áÇ3tèPt:÷ÜsóæÍë² ú¥—^º­^½šï¾û·ÛMjj*)))lÛÖ½š¾B!„BˆÞ·ÏöïÙq\s4ÔÕ°äv{üŽ-ÈÛþ-š ÍÞ…BˆÓ‰Ïã&uêM (XŠwb)í8 £J ”Ýl9IYIBô%¥©ÝkWá¼þM¼¦â¼â%tÿ½õd/K'÷Ä;qŸ{7Øjѽ{Jñ¦“½¤Ó•WNA­VQWgÁãñRZZÁ eüø\¬ÖfZZ\:’“cÐhTôë—Ø&9kÊ”)(Š‚V«%::š;w¶™ÿ–[nÁn·óŸþÒëkoxÈËËãÑGå7¿ùM—Á·™3gr×]w‚nžØ«ˆn¿ývÀßhûþûïÿ¿ÿý/Ï?ÿ<‰‰‰äää0bĶlÙBBB—^z)•••üìg?Ãf³{ñâÅ,Z´¨Íco¿ý6=ö£G&11‘„„ÊËË;]czú‘Ò)ÿøÇ?Ú” B!„Bœ™<ϯ„•B´Šè7š¬Kïëz`[ÿts§™OâÔ0øº§0Æ{IàÊo?áÐêÿàõ¸°”æššKæE?£pÙŸ±–îÁç=RÂMm0Ño©Óoü}çò×÷Γèe½ñšˆ3›ÒPŠî—áºâ%4þu²—#zzǼƒÎCóñ”Ê=gt_·ÞäóùÐé´„„P«Õ¸\n²²ij²átz()©$>>’ÊÊ:EÅþý‡9²`ÿææfìv;C‡ $\;–åË—pðàAGŸ¬=hà `÷îÝ<òÈ#ßf̘ÁÝwßM}}= .¤´´´OÙ‘¨¨(âãýM7nÜØ&èg±XøðùóÎ;]ø-[¶0pàÀ@þ?þ¸Ã Ð&“-""‚èèhÂÂÂÚ쓜œÜeà­¤¤„sÎñ÷ox饗ظq#;wîdýúõ45IY!„B!„Büð(-ZsÏz¢´f5‰S›ÆÖ£÷X­7µ¹_ü¿W|ÃÓcÓɹñY|^7îf+> •ΈÆë´Ô’ÿίOÙþj½õšˆ3›b¯G÷ú5'{¢—(eèþqY—•ñıٷ?þÇÛḣ{½­Zu¤ºàÒ¥KB§}?ÔÏ>û¬÷}X‡7€={öððÃóÛßþ¶]ðmúôéüüç?§®®Ž PVVÖg‹ìHZZZàveeûúÖG—¼Œ‹‹ëÖ>ßwÉ%—0uêT tûÑ©‹Y´hdäÈ‘„„„0mÚ4¦M›ÆwÜÁŠ+xã7°X,]Î#„B!„Bqª°ï`×?Ñ£}=®¾¹Â\ô®‚ŸE¥5ó~NkÛ^K¶ò}lÿë$ÿ1áY£0D&¢5G¶»›-Ø+P·gÕ[?Ã{ Ÿ½õš!DWBÆß{²—pRíñF㲸<ûǘt&òÊvŽÑh $5©T*<ÙÙÙìÝ»»Ý@HHH ­@xæ¼n–ƒ»üÛ'ø_ßÊÃ{}í]Fòóóyøá‡yüñÇÁ7EQ¸÷Þ{©­­eÁ‚]f|õ•ææ#W¾DF¶¿ÒÄl6nWTTàr¹:ÝçhwÞy'³gÏà›o¾aãÆÔÔÔ0nÜ8.ºè¢n¯Ón·óÈ#žžÎôéÓ2dýúõC§Ó1kÖ,ÂÂÂxê©§º=ŸB!„B!ÄÉævX±Ús²—!ú½ª¨×ær6VQ´ÜßGGQT¨ fP¼N^·³×ŽÓ×zó5BÑ1ƒÎ€Ú­F£ÖÐØÜx<""‚êêj222P•J…Éd¢ªªªÍþñññôïߟ­[·þ ‡>_ûÌ·¾Ðuº°oß>.\ÈO<¾ÕÔÔ°`Á‚@@ëd8:£m̘1„††¶Ék-ïPXXØnŸ)S¦°lÙ² sk4f̘@AAO<ñD`[FFF—kS¥ÝcÅÅÅüóŸþ†ëqqq¼øâ‹„„„0räÈ.çB!„B!„âtàóyq7Kõ'!„3ëÌhŒì XG2×\.:·ÛËå"..ŸÏ‡Óé$99™}ûö ×ëq!ðÇlŽ.mÜ—ºxð顇â±ÇÃápððÃw«Tãñèׯ×]w]ÐmË–-£®®Žµk×2aÂÌf3O>ù$o½õ L˜0iÓ¦þ’™Û·o`ÇŽ466NNN>ø ‹-¢®®ŽÌÌL"##ùâ‹/Ðh4èt:Àh;v,Û¶m#99™ñãÇ]ÓÑx£FbåÊ•8N†ΠAƒØºu+ååå8NQ©üõÌzí5B!„B!„B!„ø!Û[‘ôñÊÊJÂÃÃÉÏ÷o/..Æëmß.//¯ÍýÆÂ­(ŠŠ”lèã~|ݼ?øvýõ×÷ÕZÚÉÌÌ$333è¶õë×SWWÇ+¯¼Â!C'33“‡~¸Í8‹ÅŸþô§@cC«ÕÊ_ÿúW~ùË_¢( “&MbÒ¤Iñn·›uëÖa·ÛÙºu+#GŽD­Vóè£v¹Þ;w²äî¹çn½õV®¹æ¦OŸÎ´iӸᆂî×QÖB!„B!„B!Î<Öu/œì%œ²»BBB$jµšC‡jˆˆA¥RPéiiq¡(ó1™Ž½ggw¨údÖ¨®®Ž»îº‹•+Wâpi¼j·Ûùꫯ¸óÎ;)..n³Ï×_Í<@~~~›H¨Çãá»ï¾Ãd2ðüóϳqãÆ6ûpÿý÷SSSÓn-+V¬`ùòå ŸÇã!##ƒpàÀvã«««yùå—ùðÃ{þ!„B!„B!„Bˆ€+¯œÂ•WNÜ¿ýö‹¸êª)Ìœ9š[n¹€K.9È™# IDAT‡øøÈ>9¶’з9u'¢(ÄÇÇãóù¨ªª À:£ÕjILLÄårQ[[‹ÓÙ¾™«Ùl&66–êêjl6[—sêt:BCC©­­m÷xTT&“‰ÚÚZ»—B!„¢oDGGþªBœhEŸú/̸ ý$¯DœÊZ«àÝŸC!„âTÔ8úïÖ?^¯¯×ÛævXXØÉ\îiïž{æ`2xúé·øå/¯@¥Rðz}TUÕóÚkŸQYSO|Lïà(ޱÔä©ÎçóQQQqLû¸\.JJJ:c³Ùºpkåt:ÛÝZ?Öõ !„B!„B!„Bˆî۷Ë/Ÿ„×ëå™gÞ@­Váñ´ï ×›N«À›B!„B!„B!„8³}ú馠÷uÐ NƒoB!„B!„B!„Bœ $ð&„B!„B!„Bô1­ÞHtjŠ"_Ë q:“R“B!„B!„Bˆ3‚.6 Cj"ŠZEã¦'{9â bŽŒá¶¿~LhL…ß­áß\²—$„è#xB!„B!„Bœâ/ŸIÔ´³i>pPoâ„J:†Ð˜2Ïšˆ)<{cýI^•¢/HN«B!„B!„BчJvlÂZ[@Ñ–utâ4&oB!„Bˆ3’¢(ø|¾“>‡J¥F­Ñàr¶×!„`ŠÏÄŸ…ZgÂÓbÅ^Yˆ½ªèÞºÐhLñYèÂbñºš±Wc¯8Эýµ§¥ŸÇx,$5­)—µž¦âí¸›-ÝZ‡Zg$$e0úð8•·Ã‚½ªˆæê’N÷ӇǢøç0˜ý**ôñíÆ:›jðy=ÝZÏqQÌÒÑ'Æ¡ AѨQ› h#Ã1¤%aLOE¡æÓ¯±tþüÔ&#æA™hc"ÁëÅY×€}o{÷~/·Rô„äöG×Ñ‚mo!ΪڮŸŠF1=mT8šˆ0|^/®ê:lùEx]—ÖÅFµ¹ïjhÂçrž\Áœ…!5EgU–ûð¹\ÇôÜzÂh42kÖ,Ö¯_OYYY§ãõæPŒ¡á4V–áóu|Q“F§'$*Km—³Ã±Zƒ‰ôac OHÁÝâ ¦d?‡ölƒ..x2GÆ Õ÷›-´ØŽü¬Åf $yðtFÖÚ*Š·mÀÖÐñû­3™I<’ð„Tj ަª ÷R]¼¯Óu˜Â#ÑÍA·Yëkp·8ºœãh±éˆNë‡9"šæ¦zJó¶ÐT]Þé>ŠJMx\">¯—ƪ#ïe\æ úç 5˜¨/+¦xÛ<î¾?¿„讽{÷’››Kii)7N¸‡ËÁ®2ÿÿáæœu1»å1¾ÿ98\^^ù7rssY³fÍI[7HàM!„Bq†IÍ€V«ÃårRZܳR?aá‘ÄÄ%Ǽ‹OJ!,Âÿ%۷߬¢¡®¦ÍöƒEû1Mä MJzvmÝDË1~!#„Â2G1óNŒ±í³¦ue”­y›êmŸw¸¿ÆFúÌۉΌ¢R·ÙÖ\s¢e¦©x{‡û¾ñY ‘‰ä½þ.K-içÏ#rÀ¸@ Àãl¦ðã¨Í[Ýá<*­´é7;òT]»í- •T~û1•?Âëiÿ…ù°Ÿþ•ºmµÆˆ{^k7öÛ§/Ãã<¶€Õ±Š?’Äë.EÙ~£Ï‡×éÂcoÆYU‡·¥ãÀ•J¯#ñºK‰žv6жí×›>‡†u[(ÿÏǸj:]J§%þò™Ä\8•þ¨××ç£ê£/)룠û)Z é÷ÞD؈lmû u¯£…ª¾¤rÑÿ: e¿ðP›õ>ý7š6ïžDÆ/nFŸ×f|þ/Ÿ¡¹èP§Ï©7èt:n¹åJKK» ¼¥ ÃÜ'þÀ›ÿw E[¿épìØÝÄôyóq;[xþòÑÞÆÏ½ƒI×ÞÎÔ6hUw¨ˆe/ŸÂïÖRu`sD4)¹£ˆNÉdÆí QëÞy%èZÊRžäRkuÄeêðõ &%w×üî5ô¦ZlömX‰­¾†¨ä ú9—~c&sÓßã_?û1•fUj “®ý)“oºæ¦z¾]ò&Öº*b3‘3ùB"R¸ì‘?ñÊ­t¹¦¤¤$~ûÛß¢ÑøÃ #FŒàþûï—Å¢WÅÇdzaÑßÓÿøú5ÔŠ·×ÿ8Jƒ×çҵrä"™Mqqñ _s+ ¼ !„B!Îq‰)„.…T¸¿ge&µ:=)éý8ÐÃR•îNÊøÝsÎ{"J~ý„„„ŽÕj£¡¡óìqæ’óD˜“’1ûE…å`û=‰Ór¤„œJ£#vøy˜ º¤Ÿ?clž{þýÖ²½mZs$Ù×>)>“¬K~Á¶—nÃÓbëp=‰ç\NsÍAö/þöÊ#%*ëó7}Íã¨4:¢O¢bÃíötŽ?è|ü<5Û¾h7&¢ÿhb†Gå†àÁ=o=¸<éjR¦Ü€§ÅÆî7~Ùášû‚!5‘Ĺþ€KÕŸSþßOÚl¯ýl-ƒ^XHêW>êVoRï¸S¿4|/¥—ºmƒ<†Ôæ^DÕâϺ\“'*‚â?ü‹†o¶¯xw9Ù/,DIä”qAo%/¾‰ÛbÅcµ·y\eÔ“qßOžMÌìÉÔ,ÿ gM}Ð9<õJà9EM;]\4©w^:ÄDÅÛK©þd%*“‘Ü¿ý Ý±úJËQÙ†®n”¶ôz<ä­ü„1?º‘Á“.àÓÅëiÿ&"!•ÄCØñyûs`ä¬+1ëJ|^?7Ÿmÿ{?°M£70gþï|î…\òÀ3¼tÃÔ Á»/ÿþ CgÌa΂?‘¤ëî&%ç,v­ü˜Ï^~kmw½¶‚èÔ,š­íoƒ&œGÚÐ1|üÌlûlQ»1ýÇNaØÌËÙ°ø_¾6«^{žU¯=ÔkÂ=ou¿”¸Z«c΂çÑ›B¨ØŸÇæßˆ­þH >){8×=ûoBc˜ý‹'ùÏü;oÊÍÿGþº/øø¹_bojw¬ :¦aÿ·ì_üTÐ2“§’¨igƒ¢à±5Sñþ§í¶»ê©_µ€„«fw8OèÈÁ„ö#ËÿóQ» €ã`EÏþ½Ëþp>·‡Âg^mtð¶8±ló¿ïß/õx´–òª 0os ‡^[ €¢R’; Óu-jÊ8ÌÙYzõ]*†×yä}õy½xºÑ7îX 2„#F UÕëõ.ú~à-33“I“&µ›gûþ@š)<ŠŒç=Vk¶›­¡–‚M_µÛ®Öh™~Ûƒ¬ïm‚nî=ó¶úÌ‘1 ›yy·žcLZ?ιâ6v¬øÅÿ ë÷ªËxkÍJóù|ìørIÐy÷o\ÅâßÞÓiŸºã5ò«ˆLLÃçó±äw¿ht(Û³Uÿòöú9—„þ¹Î·oý—¼û«;ÚÝv¯^¸•’ÑåºZ{nµ²Z­46¶…8™Î¾â6ιò6F]ráñÉ„ÆÄÇð™—3ü‚3mÚ4&OžÜ'Ç–À›B!„âŒ` #>1€Ge»i4Z’Ó²HHjßèûE!£k©Ê|è4­#‘ƒüŸÏKÍÎབྷì´¨Á;¯n÷׸lÁ.¢ðál¬@¥mß» Àm?|ˆ¢9p\7VâMFÂÂÃ0šŒ¨Õê.Çëcp”Vàs/óÜRî†h£ÂÑ„šƒŽ‰œ8ðg~Õ|Úý¬¡`%eØv¿€ÆmñgQ©tíû·u‡³¼ŸÇ¸Ò„‡v{?uˆ Ëö½ÔPôùüA7‹­Ó~qG;–÷窫®â‰'žà7Þà¶Ûn£ÿþÀ‘¬7—ËE\\W\qùË_øóŸÿÌ=÷ÜÓ&Û ü ÚƒþŸ‹œ©=Vkàm×—ýŒ“5zÆ0ÿ¿o?z3èÎf;»W/`À¸©>·VZƒ‰»•å|´Íã^Ÿ×CsÀ[k`JQž3£[Çé ƒ&øƒûûvRU˜tÌŽ/>ÄçóŸoƒÏí¼Läæß¤¢BcÕ‘•Z]û~v펹co¿ý6MMM:tˆçž{®[Ù‘BœH¿Ï›×ãaÿÆUDħ>l,-v+MUåäåå±{wÏ*˜tyì>™U!„B!N1­Ùn6«…ªò#WéšCB}Îì6+e_Ÿœ†É‚Ï磨`O›mg;­VÇ矼‹Ý¤g þ+È¿ùê3Ι<sH(“¦Ï¦¡®†ƒÅ¨…ì!gáv»ØðõT–—ãtáÃø{ru‡ÃáÀáÐc0 ñ!+åOz½ÿ 9ODw„g Ün<ð]æMóg‹´Ô•áqt¸³•ïÇŸ…9¡(J·"GóºZû*A·×í^Cêô›QëŒôÿñBêòÖP³ý š ·žÔ ·ÐÐPÂÂצÆ&êë;ÏÐn ¶©Mÿ,«ŒG¶µ­Ú{„ÿâkÞþx'ƒ¢R¡ EeÔ£´ºZÏ Uð÷·#ß/‘én´°}î/º½ÿ±¼?†øxÿE@QQQÌ™3‡9sæPZZŠVë:Þÿý¤§§·Éˆóx<¤¤¤PTTÔf¾+>dÊM÷‘=q&Ë^x¯ûÈ{™”Nâ¶âöÊLfŽ€µ¶Š†ŠŽ?Uð‹ËÊîpÌ÷múð Zl–6ýõ–™Žß½zÓoû%:£™?úgòV/gûg‹)ünmŸf¸}_Ú°¹,?xÉS€æ¦z*J‰LL ”ò–ƒ»út­Û”2´4uýü½Í-xü/¤ýì̃2üâÃ8Vàn²¢OŽGëÏ4¬þd%u+7Ãmµn«ôÝÏVï i?½CjÞ'…O½‚5o»1 WÌ‚c ¼õ†ž¼?G;ÿüóIMMÅétÒØØHll,×]wO<ñD·ößþù¤äŽ"{Âù,Õhñ¸]D%§“Ð?ŸÏÇÎ/:΃¢Ò8þ ,À›ÅbÁíq£Õjq¹\8»™}æ¶ÚüÙ\>¶=…ècÑÆFá±ÚiX»™Ú/¾Áºk_‡ûû\.<6;j³ }|L‡ãúš.6Š¡¨^º*hÐídêéû››ËM7ÝÀ믿ξ}ûxúé§?~<—_~9‹-êr޼UK™y÷¯Ð›Cé7æ\ò¿YAÎä‹(پƪŽËÀZªËÐÌì[ÿe·×}¢¸-ì߸ŠýW±ùãÿpãÞF¥Ñ0~î¼óð­½~¼æ¦F\;Zƒ‰°ÃU:—€¥¶ûªqª‹gÆæÿ…$ð&„B!„8­¥¤÷ü½+r‡i·Ýd À`02~ÊØ—GÅ¡’À˜Ä” ÔjÿŸ’S3‰OLi3ÇÑo£ÎžŒÇã¡¶º‚½»¶ŒŠ%,<€â{;\oÅ¡ÊJ‹HJÉ 5£ÿix;!!!ÄÄø{}I0EtDÎÑXxäßÜð~£zx³Ê'¢ßh ‘‰hLa¸íMAÇ…$gÐT´­Gkí –ƒ»h®*Ÿ…16­ë% »W¹3Íöfš9¶Þ‹I×^‚9;‹ÚÏ×Rúê»=:®uG>ág $wŠVƒÏuâ3ZuqÑÛÍ…§fù³ž¼?‘‘‘ÌŸ?µZÍÎ;Y²d >ŸÅ‹sùå—sã7²wï^vîÜÙù±- ìß°ŠAÎ#gêE䳂Á‡û»uVfàÀ浜}Åm˜"¢ˆËHUa~§ãO¦ƒ;¿¥ª0Ÿø~ƒ‰Mïß'Çðù¼”çï$mØX’ìp\ht}:>ú(*•„Dïš2e S§NeÚ´i\{íµ¶Ù~Ë-·põÕWî_y宼rràþÌ™£¹å– ¸öÚédg§qõÕS‰Œ %&&Œèè#÷Éxë£3GqãÄkÛ=¾&ïlxÿ˜æŠ2G2=w*iŠ@­Rãp9°¶ØØV¼O¶vþ K!„Bˆ3ÝŽ-سó»·§e$k`Žf;ëW@s³½Í˜ÒâjªÊ;œ#,"Š³Æ ÀúÕŸáh¶ãrµ-ítto9µºó¾fšÃÁ6¯×{Úöx; X­VéÕ%:%ç‰(Zþ97<ƒ6$Šœž¦à£ç±•)_¨ ‹!yâ\L‰ýÙýÚx=mÿm®.¡.ïk¢S\¤æ IDATr&‘2ù:š«‹iØ·1°Ý›F¿9`+ßOí®¯úæ‰( ý/›)>‹Ò•¯S›·Ÿçȹ­ÖI™r=†H–Kã-]NÙT¼#0wÊäk)YñÏ@œ¢RcN€õОÞ.‡9JÊ1¤$wÉtLýÓqÕÔÓšƒçsºpÖÔcÙº»Ó,²Æ Û°lßKè°A$΢QSýÑ—x—STT*ÂÆ %qîl*Þ]FÃ7[;œ«§š‹J^FŸ7‘º•Ç0õK#ý¾›Q!´Q]ªN6ŸÏÇÿþ÷?ú÷ïÏ«¯¾Jeå‘r….—‹çž{ŽçŸž/¾ø‹¥ëžqù߬ÀamÂÆÅ÷? ÀÞuŸÓbï<@ìq»øü/3gá|î,.~à¾øë“Þo‘‰iL¸ö.>~îÁ>ãÎ)ŠŠË~‘øþ9¬üdzä­ZŠç¨Ïc:£‰)7ÝGdR:6÷]~Û§ï3þªÛ O沇þÈÜLmé‘àCöÄ™L¼în6}øF§¥<{SNNN›@[bb"111’õ&zUss3v»¡C‡âõz±Z­Œ;–åË—pðàÁ6[ù|>tº#•Hââ"ðz}”3p` N§›¸¸bcÃÉÊJÄfs••ÈÒO{¿ÂˆÞú€Ak ö¨Zà•EQˆ0Û/Ú¬¸L~ý£G0upu“Ïç;eoZ–~ÙDD‡árº©•J¡¶ºž{‹»|½„B!~hì¶Î¿ rn\ïõzil¨ :¦¥ÅAKKÇ4ª£i–¦ì¶öŸÃjñz½¨T*SÒ©©®:—Z­&95€úÚªN?žI$˜"ºCÎa)ÙIÑò—HŸuƸ †Üú"-õ8muhᢒüH>áýGS¿·}Y¶¢å/aŒËÀ“Ê ¹¿Æ^UDK]šB’³QΦöðôQå{—>"S\&úð8úÍy€ÌÙ÷`«(ÀÝlAc ׉úp¶›­lekÞîrÎæêbª·}NìðóH<çÇDçN¦¹æ *­cL C(»þu_ŸßÊÞüÓ€t´ÜAÇ$^}M[ò8øçã¶ÿn¦äÏÿ&kþ<ŒY©$\1‹¸K¦ã(õÿNÕ'Æ¢6ù_—¸9çѰ~[¯¿G>·‡Šw—“|óåRÈ~á!š¾Û…§Ù1#…Сq54QÿÕF"'%zÚ8l{ hXÛþ" AÏ/@¥Õ ;\ö:5‘Ázgmý©W×Þ™•+W²}ûö %Ü ¹í¶Û¨©©éÖ\—“Ý«—1ò¹,­]”™lµcŇÄõËfüU·3â‚+2íÊönÇÙl#<.‰˜ô(Š‚ÛÙÂÚÿ¾LÝ¡¢vs\üÀ3¤‹Îh<6ïÕåsáõ_ÌÅRüs@DB qYÙ„Ç%1gÁ˜ý‹'¨Ø¿‹æ¦FLá‘Äef£3ùç.Û»5ÿy©Ã¹®ùÝë$ôÏ ÜWŽ VÍYðksß¾C :.¿|^¯—gŸ}Ç‹F£Æëõáõúo»Ýíçêm}xÓjµÜsÏ=,^¼¸ÝäéjMþZÖä¯ Üð¢û›5ú˜ç¹yÒ ˜ô&šš›øçê×É;´»³Z‹ÙÒ ¹ŸZ£æ¢¹ç3ó²©è mKà*®à?½Ãþ¼Âöî›õ<ðôÝdôO`Á­SUüCABJ×ýô oûaÏëõòÍ—ßòö+`·[ùƒ«n½”é—ø¯t~ýo³ú냎‹ˆçÊ[.aôĨ5m¯vn¬kbÉ[ŸòÕòuA÷½hîy̼|Z—kÙ¸z ¯üîõ ÛÆLÉUóæ‚„­¬M6½ö «?=qõ …B!NºÞøB®S´8”î#£ß 2äÐÔXOñ¶½Kôg=“Ùÿ\~ÞÉë$„?T•›—b¯.&yÒÕ„gŽD™€>2Ÿ×McÁÊÖ¼ƒåà® û»ìä½v?©Óo&fè4Lq˜â2ïï¡fçJ®| gcßev´ÔW°ã•;‰s q#/@™@hjn›1Φj*7/£býb¼ng3µU¸ôEÜö&âÇ\Œ.,ÝQŽ[Ëö¢¨ûî«BWm{î}’ÐáƒÐÇÇ hKQЄ…’Ócf a#sÈzø.ö-ø=>oû‹ƒÝ MìÿÕ‹ÄÍ™AôŒñhÂC1õKksœÚ/ÖR½ô«> ŒÖ,_ @•³ÐFG}Þ| ßl¥ìÍñXíh"žڼ´g›×P´tñ1þ;'¡l_G}“€nÝZmÿüF^8{C›ºŸºâo¿£<®¾‹„þ9¤ =Ò£×å°³kÕRÖþç/AƒnàïyÖšÖ*2ñÈ9¢Öt~ž×——ðÊ­3ó£›yáUD&¦‘:¤mŸà¦ªr6üëßÿ;ngÇÞ›Â#1GÆÝfi[ªTo^Ú²4ï;þuÏeœ×#dŒœÐf-k›?ú7«ß|±Óuô¶Ý»w³nÝ:ÆÀöíÛinîyY!‚”0»ÓNVl&1¡1,úÖÀOKK#**ŠÒR†´F£Áét’”4´M¸„„***¶Ôi7ávØ(_÷¯x¯ÛIå«ó{}íJBBBŸ^>9aÂ,X@SS , ¸¸¸/wJj ¼}¶ó ^ùòïÝÚÇ 5ðï;þ…¢(üùó—Y¹»J§[ï¿–s¦Áçó±uýNŠ÷—¢7èqöSãq»Ü<»à¥|ûñO.fÖ§îwxKJO`ás?Çh6bm²±qõjINOdô¤¨Õ* öñ̃ÆÝÍF½ÃÆäðó_Ï Üï(ðÂCø± Ñ4Ö5±jù:*Uc2È>Q†¡( ýç,ù÷òvûßôó¹Lšy6uÕ ”•t|uÎÞûYöîíŸxþ8n¾×_û¶po1»¶ìÅëñ2`H¿@òÝ,á‹Vvëy !„â‡+::«µë¾0§³þÙCÉ>»ÍÊ矼ۣ9"¢b˜|Þ%|þÉ»A3ÞÀŸwι3‰‰óÜl·ÒPW‹ÇãÆ`4‡J¥Æçó±kÛF öÿRøtPô©ÿÿ†¤w1RœÉ23ýÙŸßïé!DwiMáè#ÑšÃq7[°WâqvÿËa•Ö€9! 1—­G]îæŸÕ¡H@‡Ú`Æãl¦¥¾œ–†Ê®wì€JkÀœØ1³™æêb\Öú^\qÏÄ^<•¤ëçPôÜ?hܸ½ÓñŠZ>!mL$( ΪZZÊN\©;E«Á”™‚:,¯ÝAsÑ¡v=ì éI8«jñvQIŸLDB :£k}5•»ñžàìæˆ„TÂã“1„„âl¶S_VLCEÇ%Qû’92†Øôh FšªË©--ÄÝIU†¾ô»ßýŽüß%>ýôÓlܸ±‹=N­•*Žþ»õ×ëÅëõ¶¹ÖI/RÁÙýÆbo±“™D­µŽM…ßpî¹ç’——Ç”)SP­VKtt4;wîl“¹{Þyç‘””Äë¯û“SR¦Ü€»¹‰Š sÓïq6V±öÕùÄÇDöÊš(N@ÆÛÚµkyõÕW¹í¶Ûxâ‰'X¸p!%%%}}ؼsÊáæ«ùûº}r “Ã9ÓÆàõzyñ±WÙñíîÀ¶Þ\ÆíÞȨ øéçsyôΧñ¹J©7eÀ—O£ü`%‰©ñŽýɽWc4©ƒn £ÅÝBµž-Å[ù|ç êl_É£V©™‘;• Æ“•ŒAgÀÒlaOy>˶}ÊÞòü÷í©)ƒÏer¶¿L¡A«<~çôy¸<í¯äxÓbv•vüá/)"‘Û¦Þ@i])ÿøêµ^]okIÅu+6µ ºxÜþýÒ» —Kbj<¹g ŒQ…k<„”8ä—°øµO‚Ο˜Ï5w\Ž¢À‡ÿ^ÞiÖœ9ÔÄ­÷_‹Íbç?½ËƒÏÜÓáØ”ŒD2ù¯ª}ûo´ëƒ–¿óŸ¾·‚K®½€©MìVàíæ_\Chx/<ú 7Ý{u§·œþÚË›Vo ÚKî«åë¸òÖKQ«U ’Å7_~?ðæïùg³Ú»\×÷M<ÿlôF=Uå5|ðFûþ€ÿ[¼’³§Ž"­_ SfOàý~|ÌÇB!„ø¡Ù¿gû÷ì8®9êjXòÎ?»5ÖçóQ°w'{wb Ãh2£Ñhiiq`³4â«–¯£_N&³¯œÁè‰ÃÛí«Ñ¨™÷ËëÉ9K“­ËR•7þì*"c"ø× ÿ¥¡®ó+Ù2øk:{Üv~¼aðºþ«4¢ã"IÉHìt¾é—œË°19|þáWìÜÜuâÖ_D{ðhº³Å…§“æÃovë±×M6ÆßÌuóÚmþBÜtøý<ú=B!„}Ãfm¢¦ªœŠ²êk«$è&„â´ãs¹Á¶”ys1nÛï^m61~$™óoÀUßDãæ“‘'„8v99þï+Vt´ Dor¹\î º$$D’œ|¤ÇbDD‘FÔ¥kp~Ell8ééWÎë©’ñÖjéÒ¥x½^îºë.ž|òIæÏŸOYYï§[›t&æMý /~ö«÷¬Áw¸Û¹V­åÚñs¹xäl~:ã~öæ}íö¿kÆ L@US5¿ûäYŠkŽ”Æœž;•;§Íãšs®â@Õ¶÷^³óÅß.á“­þ4È”¨dž¼â7üüß÷S$;Ïá:9õ{SãÐhü’Š÷wœ½X´¯„!£²IÍJnóxiaÿ}y7þ|.7Üs{Š©¯il¿ìÆÙ¤õK¡òP5¯ÿñíN×rî̳5a8+?YÃÖõ;‰K Þ°´•Áäohki´âõ/Y]Q‹ÇíA­Q“œ‘DiQyÐqÉé‰\ñ“‹))(eÑ¿º—¶wG©YÉdÀ’·>m·=s`­¯×4àÈx³ÉxS¥[W–¤d&P¼¯ã:ÔEûüç{BJ­¦Û=î„B!„B!‚){ýúýên © ôÿõÏðyŸjK Zµ—ÇØž›Á¸ÿgï¾££®³‡¿§&™ôžB I€@èJ³±"Šbwýa_Ö}TlkY+vE]»"6š(‚Òkh!„ôÞëÔï< 2)`B(÷ug“ùÔ™rçÞ›8 €×W,p º¬Ø¹’İÎëS‡OéÔÀ›ÅfqÞ¥é°F¿MæFL-Kv§Ðˆ`¬+µÕŽRa=Bœ%K Êhj4RY榅F·ØcÕ’µ$õKäÌ Ã¸éþé<ÿÐëØívú HâÜ)ã0›-¼þä{Ûø‹VxT(Wß2…‚œb>gQ‡îÞx°D£_ /Z­«›ì2µZ {ì…ºÏ,Ôê´Ì~`v»Ï|ävw~úb9CF ¹_"×Ýy‹>þ…Úê:T*½úÆ3óW°ô›ß(+®h±¾9ãÍÃSÏôÛ§’6¬/Áa(Šòâ 6ÿµ%_¯lQBÓÇϯƒAÇŠ2G ×/À×ùüj*k©ª¨¡¢´Êù„„Qœ/½E„B!„Bqì÷ç²wÎ|B/™€ï€Þx„‡ õ÷uŽÛêiÊΧfÃ*]Ûf8!D÷{óÍ7Ñjµ”••u÷U„h•ÝnG¯×¹|ïëkÀß_…¢Ø±ÙÖ¯o¿‚ݱ8î7€%K– ( wß}·3øV\Üy bívGSO !¾!”×¹6œ´Ø,|¸úc·kG$  ªyîSÚWîúóúŸCïÈü¼|©mªë´»w¦eÙÌXàèñfS:ê(O/Gçð Ø5³§6Ü‘füâc Hß´›¦ƒå›çiá«_—Cï´$ο|<«–üŬû¯E¥RñÉë_µši Ñj˜=wjµŠOˆÅliuîád8‚©jµš#û»-•ÙX4õÁ»{´¸âÆ‹‰Ž‹äƒ—?;ªàTMU-OÍy‰+oº˜1çŽ`ì¤Q4Ô7âáéV«¡¬¸‚ÿ½ù Ë¿_Õb­J¥rfìͼçjT* õ”–ãèKxT(LÀ™ã‡ñâ?—u¨ùló:c“ã}9a(Wκ€Ÿ¿\ÁWï/ÆØxè=mí¹ !„B!„B syï~€J­FmðD¥R¡˜Ì(ü™ŽâÄÐUì„èlûöàé©çòËÇ ( Ï>û9[+•ð:K·Þ–-[†¢(ÜsÏ=Îà[III§ì½·huÆ:|=}ùÏ5O³bçJ6ål!£hŸKv›;ñañ”Õ•ávŽÑz¨ÄcÏž­躛bWº,SN§wüÖi¯ ¡Õju™$³ÉÌO}À£/ÍáÒ“HÞ—À`Ö,[Ïê¥ëÚÜû²ë&—Ã'¯EANdz&‹òJÈHßOr¿D®œu Y{r¨,;TÊ3&!Šëïº ›MA£Q£:˜ùv¸þCû0áâ1l\½?–¬íðÙÍ‚BÁH• EQhj0‚´¾<<õDÄ„cð18³óš|¼œ÷ÉË*à³·¾##}?v»•JÅ 3ûsý]WáäÇ]ÿ¼‰Gg?…ùà_^uºCïÍÒz ¶ù=Ðö‰!„B!„BˆÎ`WlõRžN!D×ùå— nïê tcà `ÅŠØívî¹çžxâ n¾ùæNÙ×h1òÔ÷Ïrßÿ Ä7„K†\Ä%C.Âl5³«pë÷¯gÕÞ5.å›ùy:ÒÜƦñÚŒ—Ú=ËÏË·Ý9§"³ÉÌÑÌ1›-˜ŒŽRÊÁ »æTÎæÇÝ)Ì-fá«_rÓýÓIî—Han1¿þU›ç§¤õâü©ãÙ¶._X}Ô÷ÿè•/xø…{ 䉱um:µÕuDÆ„Ñg` Y{²)),#¹_b‹R—¾þ>Ü8g•eÕíöŸsgÀðTnôFTÀ×ï/fÅâ?œÌð¡\}ËeŒ›<ŠÔA)üûîçYƒM FîŸñ/j«ë°VÞÒn·³yÍvj«êxð¹»  dĸ!¬:<<#°ù}³YlÎ÷Ærð‡§ß¶õ¾ !„B!„B!„ÂU·ÞÅ]ôôt_ŠðXeïãÎîehü`†'c`l~^~ ŒMc`lWž1•~~™»]Ln*¬*dwáÞvÏ©¬¯jwΩ¨±Þ´ôòvd`ÙívÞxêýó¼}½ÎoûSL~>ί Þ^xxê1·RÏÛàcàæû¯¥¦²–÷^üß1Ý¿(¯„ù÷½Ìuw^Ar¿DÎ;pš~ÿi _¾û=ÿ|å~ÀÑûìp7Þ; ?ož{ðUZoÛ¢Õi¹îÎ+Ðj5|ûÑ,ùf¥ËxIa¯?ù>¿6×Q6òʉ|óÁÎqEQ¨*¯nóŒÌ]ÈÙŸO\¯ûÆ;o ‡½Þ¾Ž>qË¿_Õ¢¤¥·ï¡†Æí½oB!„B!„B!„8¤[ocÇŽeΜ94440oÞ¼Nßßb³ðWæ:þÊ\‡ ±!1 ¤ç`à¡‹ærû‡ÿ ¶éP`¥Áè(͘Uv€×W,èô;*Š =Í45A¡T”Vºâ˜ßF´„Þq\~ý…XÌ2w ÏÀdnž{/>ú&v»½Åü‘ã‡@SC¼xo‹qFãüúþ§îÀf³ñÛOkXòµk«0·˜gæ¾B`Ha‘!˜Œ&ŠòJ0Íèõ:çÝ/cIÚð¾X­6n¸wšÛçãèÈ‚œ2óB.¸r"¹û œAɨž†°aUËÞràÈLÛ¹yáQ¡ôîß«Õ×­-Õå5Ð+¯Ãúº55©­®Ã/À—ˆ2ҳܮ ë 82+JOÏÀ²B!„B!„Bq4ŠŸ‹@Ý]hº566ò裒•å>ÐYìØÉ)Ïåë ßrï's©mªÅKïŸA.ór*òˆ ŽéÒûœìJ ÊœT½úĵ:/¹"Y{sÜŽ| Üúà 4Z _¼³ˆ×žxòâ R¥páÕçº]£( &£µFƒ _‹_¾þÞι¾þÞøú9ú©µ¢ª¼š½;2ÉÞ—ç,­˜:¤7­††ºFr³ \曌flV›Û³ýýœ=ؼ žøúáãw(ƒÌ×ÿPiRKýñš3A}ý}\×h5„„Ñ£§ûþƒÍšƒ{Õ5.8ø>´ùžõs¼gÙ¹nŸB!„B!„B!„p¯[2ÞšƒnMMM<öØcìß¿¿S÷O K 92‰ÍÙ[(©i™iUÛTK^E>©Ñ}Ñku.c›³·pÉà éK\HO²Ë[Œü¼|•|&[s¶QT]Üþ…º)xÅÝçÞ@nE¯.{£ÓöV…­kÓ5q8£Î=ƒu¿on1'>9–Ø„(6þá>»kÖœi‡±uí¡^m ž]ȃÏÝÍ%מOæ®,voÛç²få«YÙF_·°!ÌçQæÝñ,¥…å.ãjµš«n¾”±,úäg2wpW©TLºr"ëWmA9¬Ùb~v·O™ÛêÙÏ/|œÀ`>yý+g™Çfå%ίã“c©,sŸQ› @i‘ëÝ/¸b—]7 “ÑÌý×Ís[ê²GÏb¯ûÞ®¶6­Ù΀3ú1tô@>g¦#ú×yxê1nZyÏ„B!„B!„BáÞqÏx;2è¶oß¾ö¥‰ýÆsÓÙ70}ä5¨š›¶¦G`ÃØ[äz~zþN2KÁŠ;Ϲ O׌#­wL¼•›Î¾Ùãfuè>f›#‹** ‡3êxðÐz–@bXÑQ¾ÿÏ_®@QR¥pÁÔ .cÁþ̺o:[þÚA~vQ‹õç^6–#úQUQÃû/êÕ–µ'›ï?ù•JÅìfàèש÷V…Ș0úJfúíS]zš©5j¦ßv9 )=1ÍüðÙÒN=»8¿”ìLGVåÔ/" ؿŜ‘ã‡:³ÎÖ®Üè2öçò X­6<<õÜúðL—»„÷åö‡o@¥RQZTÎÖué.ãë~ÛDyqÞ¾fÍ™Ž^(ð¬Õi¹ñÞiøúûP]QÃê¥ë:å9 !„B!„B!„§‹ãšñÖt3Ì›7ŒŒŒ.9ç»Mß3^Þžô˜Œ^KÄ{F IDAT¯£¼¸‚^ù¢ÅÚø”X¦Þpv»wžû˜úÚ—ñ?_Nß)¤¤õbö×ñüC¯wjÙÃ/ÞYÄ#/&›Å3ï=ÆÎ-X-V’û%€Í¦°à™[”jì ï¿ð)sŸ¹‹°Èæ¿ó›Öl§¬¨½‡ž¸äz§%°á-¬]¹ÉemeYŸ¼ö%3ÔA)<÷á¿Ø·s?µÕõ…”šˆF£¦¾¶×Ÿ|›Õæ²ÞjµñÖs ¹þ •Fr¿yìÝž‰;)ý{áà‹ÕbeÁ3a6™;ý¹ !„B!„B!„§²ãxkº™L&æÍ›Çž={ºì¬’šRýj7œu=cÓˆ éÙb|ñ–øeû2·ë «‹xàóG˜~æ5ŒHÎÎsŽ5š›X–¾‚ÏÖ~Iucu‡îóݦïѨ4LHK€!€0¿0T*j•/ßö78-_ô;•åUL™1™È˜p"cÂ0›-¬Y¾ž/ßýžºšz—5o/n}p&­†>[Êží-³ív;o?·½6—ÞiI\zí|»ð§N»w~vÏÌ}…)3/$uP CGpž›‘¾Ÿ/ÞYÄŒÜN;ïȳçÝñ,—]7‰¡c2rüP—ñ’Â2–÷;+\ã6ظjÉZÊŠ+¸äÚ èÕ7ž~Cú8ÇššØ´f;ß|ø#5UµnÏß¿;›'ïy«o™Bï´^ 3p<÷=Û÷ñÙ[ß‘wD_;!„B!„B!„âx™4Îùx³ÜÜ\<<<ºáƇtyàm÷îÝìܹ“O?ý”ôôô®>N!„B!„B!„BœÄÂÃÃY·nóûwÿøJƒU± UkQì vìhT缬¬,z÷îMNNÎq¿s³ãRjR!„Bqâ9ÝKMªT*·=u»c•J…¢(kŸ“”š!¥&…Bq²R“'–àèx,&#`ÇÃà‹§¯?¦†:ŒõŽVOµeEÀIZjR!„B!N4Þ>¾Œ?™œ¬½dìÚvLA/•JŸó/£²¼”=é›165vx­ÁÛ‡„¤T"£{âeðF¥Ra2)-ÎgßîíÔÕVõ}„B!„B8Œ¸âf¬f#fc#y;6¢7x£óô"eÔ9 R¡+؉Íf㋯¿ëô³Õ¾£B!„Bœà’úâée "ªç1gšEôˆÅ×/€˜¸^G•õÖ#:Žqç_FbJ*o¬V ‹OObâz1ö¼KéwLwB!„BZ½£Ï›b³‘¹þ7£é™6Sc=µ¥EìÚµ‹Ý»wwÍÙ]²«B!„Bœ ´Z1ñIØwìÿÐJHî @aÞLƦ­  aÈȳQ«5”哾uõµ5øú0`Ø(‚CÂ2r,µ¿|K}]Í1ßO!„B!NW…{·±iñ'(6~¾à¸-oB!„BˆÓJL|:‹ÅL~ÎþcÚÃÏ?°Hàè‚w}Ó†¢Vk¨­©býêåΠ@]m5k_б©µZMïþƒénB!„Bq²›äfíÃf³Û³Ýjª*¨¬(íÐO/¡á=ÈÎÜã¶Ä¥Õj!'k/‘Q±ètúcºŸB!„BœÌöîÝKjjjw_ã˜HàM!„BqÚ‹ŒÆÇ×€™ÇVfR§÷ ºg"YG‘íæíãçüº¦ª¢Õyå¥Å¨Õ‚BŽéŽB!„Bq23 dggw÷5މÞ„B!„§„$G¦ZiQ> õµÇ´G\B2³ÙDAnÇKUê=<œ_[¬–VçÞ×­9H(„B!„§“ððpŠŠŠºûÇDoB!„BˆÓ‚·¯ᑎY‡e»iµ:¢bˆèÛî*•Џ^Í¥*3°Ö3 "*–¨Ø4Z­Ûµf“ÑåÌÖXÌfç×:½”šB!„Bœ|þn¶eË–Q[{l–ìnxB!„Bœš³Ýêë(-Êw>îíãËБcé?xD»{DDÅbðöÁn·“½ËØà3Îbèȱxxxº][_[ƒÝn °’AÁίU*U»wB!„BˆÍÉÜ£íï’À›B!„â”§ÕêˆK {ÿg @§ëxV™k©Êºg´Åd2RVRèÜÇÝüÀàP†žèüÞbi½$¥B!„Bœ¨Næm—Þ„B!„§¼Øø$´:6›•ܬ —1mo~þ„„Epà°R•àºu$;mçÖõ(Š o_?Κx!1q½ð &4"ŠþƒG0zü$—’”V‹¹Ý„B!„âÄt2÷hû»Ü7B!„BˆSH|’£/[Anf³ÉeL«k;S­YB²£LJC}%‡•ª<š=jkªX·zCGŽÅ×?Ágœå2^\˜KNVgÌz««­éоB!„BÑ™&OžÌ¶mÛÈÏÏo²Ë–-ëäø"þ/À›B!„â¯?Yûv·×u h¦×{Ý3h™íæØ£ãå*K‹òYþ×ÄÄ'‚N§§±±ÂülÊŠ œ>»ÝNmuE‡÷B!„BˆÎÒÜ£íXo§3 ¼ !„B!NiÑ=°ÛR k1n0øàééÅ™cÏ kß.Š rs"£ãÐhÿ|ŠŠ‰'<2ÚeÃ3Þ†Œ8›ÍFEY1{wnu{'³ÙÄþ½énÇz&$PQZŒÍfëÐsB!„BˆÎt:÷hû»$ð&„B!„8¥iµŽ ˜J¥&4¼G«óÔs¼è° ›cCÿt mó¼ p,ÇП-:6?ÿ@ìo™Y'„B!„ÇCxx8ëÖ­ëîkœ”$ð&„B!„8¥íزŽ=é›[O&!¹/ƦFÖ®Z @SS£Ëœüœý”—¶ÞÜ/ ÈÙ¯míª¥›:ðÂÀᣨ(+¡(?ç¨Ö !„B!Dg©ñÆ?¶ ƒ± sS=*•‹ÉHm™ûED¢Ñh((( 4ÔQî_£Ñ0iÒp/þ E±£V«(+«>¸.Èíxxx F*+kñññÂl¶ (v**j]Îk~ü쳑‘Ozú¢£C1™,€ƒÁ“ÆF#V«‚··§óqE±SR^Õé¯Þ„B!„§´Æ†º6ÇFGMQjª+ÝÎ1™Œ˜LÆV÷Pk4ίëj«il¨?ª;Æ%ö¦ß áh4ZŒMlZû;v»ý¨öB!„BˆÎ¢÷òFïåÍY3þAuq>ØíXÍF–¼öofÍšEcc#ÿûßÿ¸òʱh4j^|ñk&M:›Mqàî¼óRššLäç—£RÁ…˸æšqnÇóòÊðôÔqÞyCX½z'W\qMM&ž~ú3—ó|hj2‘—W†—סÞÛW\qf³£ÑÌŽÙ –‚ÍfÃË˳ÙBXX MM&î½ÿNí$ð&„B!„8½uF€ë¶P©TôˆŽ#©ïü‚¨­©bíªe45]àN!„B!:SyÎ>üÃ{`j¨ÃjjÂÓÇ«ùЇóòò0}o·ÛÑëõ¾öññDsðŠZ­†ššÊÊjhl4ÒÐàú¡Fwãáá$$Db6[ pŽy^óã99%„‡:Çõzf³›Maýú= –ŒÅbÃßßñø‘ûu& ¼ !„B!Nk‘Wf?Š]ü‚ˆëÕ›Èè8<<<°Z,dìÚÊþŒ(ŠÒ 7B!„Bˆc·gõ6ÿàÈhvé 6-þÄ9¾téR—ùûöàé©çòËÇ ( o¾ù6›‚F£ÆfSP©Th4j¬V€sÞÿû­ÛñÃiµV®ÜŠF£v®k>oÑ¢?Ýî»wo‹ÿ…Íæø÷Õoü€Ýnç’KÎt–µÔhÔÿÂ!7!„B!„8®ü‚ˆKì @MU9YäegbµZºùfB!„Bá‘L£¹‘„ÐxB|Cøú»ˆ%((} Š2v`9ØnCz¹K¸R]]Mvv6àÈP³Zmôšò Å™¬-Ó¡R©‰8³'|JÑ×PðǧŒ=šÝ»wSQQ@Ÿ>}fõêÕX­6¾þúw=~ÞT–W£ØÚî‘@Ïļ¼=©¯k kOõµ Çt¶F«!0Ø€ºÚLM¦stzþ¾íî¥( •eÕmÎ  ":.o_ õæSV\Ñ¡»ªT*â’b F­VQQVEÖÞœv_/!„B!„B!„§¶Å[tûø¶mÛˆm·\VVв\ñúï`=¨Ôìvì®ý²ýSRÈÈÈpÙ§wïÞäääüý'uœtyàM§Óq×]wñÍ7ß8kyžnÖìû“ ¢ rÊs»û*N£ÕpáÕçrÞ”qxxê]Æ rŠùè•ÏÉÜuà¸Þçÿž¹“¸^1|ù3V-YÛbNŸIüã_³ÛÝ«¶ºŽ{§=æv,>%–«g_F¯>ñ-Ær2óùì­oÉHßßêÞÃÆ âªÙ—:ƒ„ÍêkøúƒXõË_íÞO!„B!„B!ÄéÅb±°ËŸ=o8Ø®Y‹q¿õˆ+ ÿ0J·ü€¹Öñsüì’¢ÆÎ tÓ`·SkV±~û^|cR±™0×W¢õôA±šÁnÇ\çHDñG¥Ö¢XM`·Ôg Eû¨ËÛ €gPŠÕŒZ«C±˜Ðy`37AyU§¿F]x>|8ãÇgèС<ôÐC'UT²³|¹þ›î¾B—¹áž«9~v»-í '3O=Gô#ªgÿ7ÿž{èµã|»lÆ$gЭ-=zFððóÿÀËÛ‹úÚÖ¯ÚBuE Q=#:f £&'":Œgx«ÅÚ¡³Ó†õuÝÚâícÀd4³ogV«óêÝ>ž”šÀ}OÞ†N¯#'3Ÿµ+7R]YK@ÃÏL|r,÷?u;/ÿëmvnÞÓbýèsÏà†{®àÀÞvnÙ‹bSHê—HŸI\÷Uxy{²äë•yÚB!„B!„B!„‹özÄiâh,9@Ø ó±+6 þpdÎõu…cÞy·a37a¬,T+ò±ê<0‘ª=9b 6sû=@äÈËQ©µè þŽuùhô^Îs#θ«±ßt¾A˜ªKÉÉØÙéϽËokÖ¬áí·ßææ›oæÉ'Ÿäá‡&7÷ÔËü:¥ ëËÈñÃP…ÿþëmvlÜíûváOÜòÀõ •ÆÌ\Í?o{EéÚ†½Ó’8ÿòñå•ÞæÜï¹/o/J ʘÿËÔÕÔ;Ç~ûi5÷>q‰½ã8ç’³ùù«ížíàà ÷^CmuZƒ·W«s½ŽæóâcovðÙ2íÖ)èô:vlÜÍËóÞÂ~X*îÒoã®ÞÄÀý˜~Ûå<2û)—q¿¦Ý2€?_Æ7º¦ Ÿ7eWÞt S®¿M«·Q^RyÔ÷B!„B!„Bqzk¯G\]]9M•øöì‡JíªR©µXk1וa35b35¢ÇË#ÅjAïêw²ÛQkõÎÇËsÑû‡:‡­µ(6 ^ÁÑØí 6S#ŠÕHW8.=Þ-Z„ÝngöìÙ<õÔS<ôÐCäå嵿ðMy5I½X±†•»~ç’Á2®ïXÂüB±) Y¥Y,Ú¼˜6»]ß?:•)Ã.£¼®Œ×–/`hü.4‰¸Ð8´j-õÆ:¶ç¥óÚò–“K_ÄÀžZ<¾¯xŸþõ¹Û3£ƒ¢™uöL¾X÷» [f*5‹ Žá†³®àóµ_²§h¯Ûy="¹yÜ,ò+óy÷÷ZÝóX4gwý¹bƒKÐ Àfµññk_0àŒT"cÂIœâœ£R©˜~ÛåDD‡‘•‘Ë7üàvÿȘp¦Ýz9*|÷ñÏmfÍyû¸éþé4Ô5òÑ+_ðÀ³wµ:7:.’ø”ž|öÖ·.A7€Œô,~ùrO?ŸqŽîPàí†{§áëïÃKÿ\ÀÌ{®i3ðÖœñÖXï>£­-±‰Ñ¬üqµKP­Ùo?­aàˆ~„G…ìOUù¡>q£Ï‡—¥Eå|ûÑO-Ö.ùf%#Æ !61š±“GñÕ{‹úŽB!„B!„B!Noíõˆ«Ü½€Üeï`WlDœq)( Ù?¿Ž]±JåègkY‘N¥ÑR±ówTjs]Cñ~Ô:OJ6.vY×<žÿûBÇZµÆ±:.7€ï¿ÿ›ÍÆ­·Þê ¾åççwÉYQ‘¤Åô£¨ºˆþÑý“2ŠÂªB”eCߨ>ôêÃ+ÞbùÎ_[¬¯75Ó»ÝÎ{«>blï1Ü4öF—9ž:zFº=?68š´˜~-·Ø,­Þ¹ ª€ÿpÂüB©kªk3ðvnÿ‰¤Åô£ÞXÏþÒÖKz齜÷ðÒy¶:ïXè=ôô>Ømã[ÝΩ­®g϶}ôÒ›ÃS7»ÝÎo?ÿÉ#/ÞKŸÉäfæ±qõ6—µZ­†Ùs¯#61šõ«¶´[ªòú»¯"0$€WþýÕ•5mÎKŠÁÁt7¥ÁL¼xúù‡I~vQ«ûM¸ø,Ò†õeé·¿‘¾©õ÷­™ÁÇ”k¨?ºþq€K£Ic“É픦ÆÖ£ôiÃú°iÍ6·A;€ l%61šÃS%ð&„B!„B!„¢ÓÙ#®9V¼î;—yçŸ7”ðð@~úieeŽŸûGD1iÒp/þ E±cËú €ðð@jŒøûðññÂl¶ (v羡¡þ¨T*ìv;Šbç쳑‘OÉo'a·Ãýøã(ŠÂí·ßÎSO=Ń>Haaa§Ÿ£Ø% ‡ÆF­R3÷³‡*/½÷ž7Câ1sÌu¬Û¿:cËú¼Ê|lŠ Zðø!Ì3ƒŠú ¾Þð» wcµY 0´zþ[+ßã½U‡ ^7jçô›Ðæív;ËwþÊ´‘W1ÞÔ›ZÌÓitœ•2€_wýÖf0¯+EÆ„¡ÕjÈÉl={1{_.ý†ô&&!Êåñü…üﯹþW3㮫ؿ'Ç%3kÊõ“‰MŒ¦¤ Œ_þ¬Í»œuÞ†ŒÀÊV³um:a=BÚœïip!ëjêQlîË_–W`³ÚÐh5DÅõh5ðÕ3’+n¼ˆÜýù|ý~Ç‚TÍoGöpkþCߖƆ&ò߃¾’Ù»=³Åœ¤ÔÊ‹+\^S€èøäìk=è½ÏQ 6": ­NÛáwB!„B!„B!Dg0<ÉÊ*bÒ¤3°Ù>þx9×\3€;ï¼”¦&ùùå¨T—W†§§ŽóÎÂêÕ;¹âгhj2ñôÓŽøÂôéؼ9“ ÑÔd"/¯ //}—Ü]Ý%»¶áçŸæÕW_%00ùóçé>kìïhŽ]ûóöoï¹d…5™›xmÙXmV¼ô^Œè5¼Åz«ÍJE}3Ç\Gim)÷ÿï!–ìXF^E>EÕÅì.ÜÓjVšÉj¢ÁÔàüeU:¸øu×JlŠ Fǘ”QnçŒL·‡7v»¥éË;´oWÀj±R[í(ÕÖ#„¸¤â’bð:ܪ,s~B#ƒ[ì±jÉZþ\±ÁY&R¥RÐg@çN‡Ùláõ'ßk5³ <*”«o™BAN1Ÿ¿³¨Cwo.ñèèë I­VÃÁû…º²juZf?0»Ý΂g>ÂjíXzªÁבñf1[˜tåDm.o-~·xÿ,|œY÷M'ªgë.>{û;¬V\1ñŽF¯×9î£Õ0bÜ.ºæ<Eáo}ë²ÎÇÏÛù¾T”9¢ø~¾Î÷,0Øß1VZå| Bƒ:ôœ„B!„B!„BˆÎR^^M~~>>žùºŒiµjj((¨ ±ÑHii5áᤥ%`6[ pŽ7Û¿¿ˆäähçã99%„‡vÉÝkÆ[³%K– ( wß}7óçÏçÁ¤¸¸¸ý…G©ÑÜƬM-¯iªeoq©Q}é™Ì²ô–=¼j›êó ÃßàÏ3?þ‡Ú¦ÚN¿ß‘ªªÙx`3g$c\ß±ü¼}i‹9SÑÜíy;(ªnû5;P–ÍŒŽo¶N®Yêéåà»föÒ†;J¾øØÒ7ív–=lž¤…¯~I\R ½Ó’8ÿòñ¬Zò³î¿•JÅ'¯Õf‰GVÃì¹3P«U,xúC,æŽeÿÈpdt©ÕjŽìï¶Tfÿa}ÐhÔïîávŸ+n¼˜è¸H>xù3ŠóK;t6Êx;çÒ³Q©T˜ÍÊK*ð2xìÏ™†1üìÁ|ðÒÿøë×-ÖïÞšÁ~)×OfúíS™vÛå4Ô5âík@¥R‘¹û‹?]Ò¢ìes¦€±Éñ¾Œœ0”+g]ÀÏ_®à«÷cl<ôž¶öÜ…B!„B!„Bˆ®²jÕÞ|ól6…Ë/ƒ¢(ü÷¿ßb³)¨T*4µÛ„­VÃÊ•[ÑhÔÎu‹¯EQìØíöV×u–ãžñÖlÙ²e¼ôÒK3þ|ÂÃÃ;ýŒœògÙÉ#V9:áþîÏmÎRË.ÏaoQF§ß­5Ëf±%†%Ð3$Öe," ‚¾=ú°dDzv÷RìŠ3ëÎhi½ï×±Ðé1ÛöÊZ­V—ùG2›Ì¼ñÔ˜Œf.1‰;›E`°?k–­gõÒumî}Ùu“ˆKŠá‹wQÓz€îHEy%d¤;jÈ^9ë‚B]£Ú1 Q\×UØ–¡lÎÄ;\ÿ¡}˜pñ6®ÞÆKÖvølÃa¥&ßýÏ'Ü5õA¾éIîöÿ¾ûyr÷ç£Õj¸áÞi-Jt6 ‰Fw0ÓÍj±ÒÔht½} ÄÄGµ¸·Nwè=°YZÿJó{8ÏB!„B!„Bqú˜®÷:’Ùäòh æ˜ÍLF3ÊÁ »æ2ˆÍ»S˜[ÌÂW¿ä¦û§“Ü/‘ÂÜb>~ý«6ÏOIëÅùSdzm]:¿þ°ú¨ïÿÑ+_ðð ÷È bëÚtj«ëˆŒ £ÏÀ²ödSRXFr¿Ä¥.}ý}¸qÎ4*˪Ûí?çÎ ¼Z£¦±¡ Ó{çdæóüïóÄ‚‡ð ðåüËÇóös ]æ\>óB&]9‘êŠ^ý÷»l]—îì ×;-‰ë©7^D\R oÌÿÀ¹îðŒÀæ÷Íf±9ßËÁ ªþ°`[[ï›B!„B!„BˆSÓÞ½{IMM%??¿»¯rÒé¶Œ·fŠâˆNzzº/Eøw´Õ[Má`#¸–ÉL.êŒux£öÙív–ïü€1)£Ñ¨,î‚« IDAT=È4j ãúœÀÒô^:òh5Ö7àåíå̬zã©÷¹}Ê\nŸ2—][Y‚Þ¾Þç7¶¹Ÿ_ ókƒ·ž­754ø¸ùþk©©¬å½ÿwL÷/Ê+aþ}/“‘¾O=gŒÌ9—žMRj"¿ÿ´†y_Çj*]ËŒÞxï4|ü¼yçù…464õÙ5UµT•W·º5k¨kt–˜ìÕ'Îe,:.’ ®˜Àó?`ËÚΠÀžíûøïãoc³) 3ÔÁ½í{Ø{à}°ÏÜòïW9ß³Eÿ|pìP0º½÷M!„B!„'‡Á1 ¨TÝþcA!ÄI.""ïè"ƒìììî¾ÆI©[3ÞÆŽËœ9shhh`Þ¼y¾¿‡¶õ`ž—î`2sÛ%j/¿îZÉUgLÅÏË—a CY›¹Ž¡ñƒ 0`Sl¬8˜ëNÅŽžfš Ð@*J+ÝÎ ‹ qÌo£ZBï8.¿þB,f ™»Ðg`27ϽŽ}Óíë?rüPChjhâ‘ïm1®Ñhœ_ßÿÔØl6~ûi K¾^é2¯0·˜gæ¾B`Ha‘!˜Œ&ŠòJ0Íèõ:çÝ/cIÚð¾X­6n¸wšÛçãèhô8eæ…\påDr÷ðÆSï·úüÝ©®pdkÞ—  ï T*Õ5dî:àvmIAÅy%DÅE’’Ö‹›½ÞšŒÔV×áàKHDéYnׇõŒ¥UGuo!„B!„Bœ¸¼C¸ùÍÅø†Dp`ój>þ¿ëºûJâ4ÊÀ½(.®dË–Ì./õv28™_“k¯È 7œ‡Åbåá‡ßeóæã[9Nt½ððpÖ­k»”p¯Û>ÚÒtklläÑG%+Ë}àïˆh¥@˜%µ­„ºKUCµ³”äÙ½Ç0¶·#ÛmCÖF*º?RRPæÌ :2+ëpÉýÈÚ›ãvÜàcàÖg ÑjøâE¼öÄ{”W:(… ¯>×íEQ0ͨ5üýZüò=ìS¾þÞøúáéÕz¶ª¼š½;2ÉÞ—ç,­˜:¤7­††ºFr³ \曌flV›Û³ýýœ€^Oüýðñ;”A¦R©ðò#>ٵߑC¨>"ÛÎ/ÀÔk¯·^s&isÖ^³߇6ß³~Ž÷,;#·[ÏB!„B!„è±ý‡á@üàÑüÛY!Dç‰ çž{¦ðôÓ7áç×vûŸÓÅÉüšŒ7NËèÑý»ù6¿ۣmÙ²eÔÖÖ¶?Q´Ð-oÍA·¦¦&{ì1öïßß%çDEìDE½k6–N££wD2™%]sößµ4}9g$c@lü¼|çøÙ/;–uxè (î>÷r+òxuÙv?EQغ6Q‡3êÜ3X÷{ËžsñɱÄ&D°ñ­n÷™5gÁaAl]{¨WÛ‚gòàswsɵ瓹+‹ÝÛö¹¬YùÃjV¶Ñ×-¬Góßy€ywù¹Eæ˜J¥bÒ•X¿j ÊÁfùÙEÜ>en«g<¿ðqƒýùäõ¯Xµd­ËX¯¾ñ<øÜÝüç‘×%9§Óë2j;\?)RV\@px¾þ>ÔÕÔ·Xïá©'<Ê‘µVVäúÜ7­Ù΀3ú1tô@>gQ‹r—žzFŒÀ†VÞ3!„B!„Bœœrwl ¾¢Ÿà0²·üIcM÷¸[qrúí·mÌœy.V«Õ«Ó»û: éÑÖ}Ž{ÆÛ‘A·}ûöµ¿èo˜9fê#jV_:äb ¬6+k3OÌTÉm9Û)­-ÅCëÁ5#¯B§ÑQX]DzÞÎïá¡õ 1,İ¢£:ýŽ?¹EQH”ÂS'¸Œû3ë¾élùkùÙE-ÖŸ{ÙXŽèGUE ï¿t¨W[Öžl¾ÿägT*³˜ _§Þ[Q"cÂè;(™é·Ouéi¦Ö¨™~Ûå$¤ôÄd4óÃgK;õìÌ](Ì-à†{¦Ñ£g„˸—Á“Ùs¯#8,«Åê F6Ûò×LF3*•Šëî¼½^ç2®V«¹ê¦KÑ{è±YmlXµÅe|Ýo›(/®ÀÛ×À¬9Ó]ÖkuZn¼w¾þ>TWÔ°zé‰ùgC!„¢³4W*èn*• ®õÇB!Dgi¨*çÕëÆòÆ ùxîŒî¾Žâ$¶pá2¦OŸÏUWý?6oîÚŸñ‹c#=ÚºÏqÍxkºFæÍ›GFFËlŸÎ´áÀ&úôHá?ÓžaCÖFšÌMô‰êøA,Ú¼¸E6Üßõï)ÿÄSïZÖ0Ô×Ñ+¬Od Ï^ý”ËØÊÝ¿óó¶%-ö±cgùÎ_™6òj&¦Ž`éŽeØ9qJÿå•ðùÛ‹¸æ–˘zãEŒ:g89™ùxy{Òg`2z½Žòâ >zå‹kãSb™zÃEØívÞyîcêk\Æü|9}¦’Ö‹Ù\Çó½Þ©e¿xg¼˜@lBϼ÷;·d`µXIî—HPh6›Â‚g>töZë,v»wžÿ˜ûçßAPhÿ~ý2w ¼¤ƒ·'ÉýñòöÂfSøð•/(Ê+qY_[]ÇÂW¿dÖ}Ó2*øäGرq•åÕøøÒg`2‘1Ž«Ÿ½ý3C®™Õjã­çrÿü;2*ä~óØ»=;vRú÷Â/À«ÅÊ‚g>Âl2wêsB!„8‘xûø2züdr²ö’±k›³Tw{F›„ZÝ~À.sO:EîË­7‹ŽM !%•€ÀT*6›•Ò¢|öîÚFMUE›k…Bˆce15Qž{bV€Bœ\Š‹;÷gë¢sI¶îsÜoÍA7“ÉļyóسgO—ŸÙdnâѯçö ³¹|Øe.¿åG¾\÷u§Ÿ‡ÁÃ}=^ƒ‡Ä°—Ƕ絞†»bço\uÆhÔÌV3+wýÞ©wí ËýNeySfL&2&Üô1›-¬Y¾ž/ßý¾E9Dƒ··>8Vß-eÏö–Ÿˆ°Ûí¼ýÜBþõÚ\z§%qéµðíŸ:íÞùÙE<3÷¦Ì¼ÔA) =ÀynFú~¾xg2r;í¼Ãådæóÿîþ—]?™Á#û“”š@Rªã÷…ÍjcÇÆÝ,úøçVÏÿë× ”•sñµçÓg@g_p¦sÌn·³ogߺ„][öº]¿w6OÞóWß2…Þi½:f sížíûøì­ïÈ;¢¯B!Ä©&!©/ž^"¢z²'}Kû   ïP¦œÅ¼©Íñ´!gß«76›“ш§—Èè8Â{İiíïæewø^B!„8uùrÞyCÙ¼9“½{óºû:Bˆ£¤Ói0 ³Ù¦M]›Œt¸eË:Þ¶Jt.UDDD—§PtÛµkW—ž7çü0*y$«öüÁËK_ ÀàO¨o(VÅJ~e›¥Kïp: $ سÑDiQÅI“1eðö"8<FMEi•Û¾i]E«ÓЧÁ“Æú&*ʪZô]k‹ÞÃÑÏÍÓË“ÑLYQ9MƯ÷ñó&$<€ò’Ê™‡B!„8µP_üþþs"Ðjuœ{ñUètz¶nXCN–û,¹sñ•7 R©Ø¾é/ÊJ [×X_×j]Ï„d ÀþŒìÞ± ›ÕŠN§§ß 3ˆOÂf³òÛ’EÔ×un†Iö/ŽŒÀ¸ó{vóMĉ,>>€´3Sœî<¼}ñòõ ¦¤»½õLf­ÞŸ GoôºŠRl–Ö~¡ó4Ð3m8þÑXMFÊs3)س Ú©Êã‚ÎãPE¤¦ºL uÎïCã’‰ê3½—úŠRr¶­£¡ºõlg½Á›è>ƒðˆF­Ñb¬­¦ôÀ^ÊrÚ/ïfðDïåív¬¾ª«©ã?Gí™Dpl"ÞÁ4ÕV‘¿k µe-[ŒN¥Öà‰]Q¨9ìÿ?ÃâSˆèÕ§ªÂr¶­Ãf=ý~f§R©èÓ'–èèP¼Ñj5x{{ìG||$‰‰‘¨T*¾ûn ¯¼ò]§ŸïçgÀ`ðÀb±QQQëvޝ¯oo¬V…òò¶ÿ~b0x2hP"!!þÔ×7±cÇJK«5ªÿþ÷õ\qÅ¿©¬¬ksOO=ii DDb2YÈÍ-eÏž¼6«b…‡¢RACƒ‰ººÆ6÷ òE¯×b2Y©ªjû.Ww½&:–à`_ªªê1™Úþó@]] ®ÿmÐhÔ„†ú»]×Öï¶hµçï}NCuu=»wçQVVÝá=‚ƒýè×/Ž€êë›Ø³'‚‚ò£¾ËßJ||$ÞhµZ´Z5>>^„…„ÅbaÿþBþóŸ/QEQðóëÜKâØ””WØ©{vyÆÛСC™3gf³™Ç¼Ëƒn.û$juc Õ§î?ZO•eUT–|Myšhì¦ /«Åê¶ÿ]G™Mæ¿•V_Û Á6!„Bœvbâ“ÐéôX,fòsŽ­Ô–±©‘úÚ£ÿ÷…Z­¡w¿Áä’¾åPé‹ÅLÖ¾]ÄÆ'¡ÑhIé7ˆMývL÷BˆÓMlÿa\ýä»,¼oÙ[ÿjuîðËf2aöƒXÍ&^¸|h«·3¯¾•1ÓïDop ZUdóÓKr`óšVϸäÿ8ì,ç÷k¿|›eo>…§—=ò2½†u™¿ôõÿǺ¯ßk±ÎÓÀ„›ç2è‚«Ðzx¶¯.Îgã¢Xÿ퇭>sïø'ý'\êvì³Gf±oí¯­>ÃEôJå¼;þIlÚðcû7üÎO/=Juq¾Ûµ¾Á¡ÜõÉ(6+Ož—LÜÀ‘œ{ûc„'ônñ|>{øÆOcÇà–[.$,, Å˜ÝnÇl¶P_o¤¤¤£±k>ä>{öd.¸`8ùÜvÛËnç̘qS¦Œ&/¯Œ™3Ÿmu¯‹/ÉìÙ“ñòòpy|Ù²MlØÐñLŸ«¯Çôé0\÷)((祗¾fóæL·ëæÍ›AJJ4Û·gqï½o´º¿V«áý÷ÿ/>ýtï¾ûK‡ïv´ºó5ñôÔñᇠÕjX¸p|°´Õý{÷Žáµ×î`îÜ·[dfEDñÑG¸]ÛÖïÖ\zé(fÌ8ÿ– ؾ=‹wßý…ôôÖ?xc0xp×]—1qâ ÔjµËØæÍûxá…¯)*êúòí½zEñ\Fß¾=1›-Øl 6›‚ÕjÅdrüù­«k ´´š²²jôzm—ýY'Ž.¼íÞ½›;wòé§Ÿ’žÞzYE!„B!„8’ú›µ›Íz\ώ胧—£4ýÞ][[Œ§¤t~ÇöMX̯ˆp*óôôÄh<º¬ qú‘ß'§¯ý~§¡ºï€`úŽ»°ÍÀ[ß±“Øûç2Ln²¾U*.ºïi^p%Å™»Èß¹ ­ÞƒÄagÇ´ùðù?g“¹n¥Û3Š3w¢Öhð %4.ÿðh´ž\3ÿ}¢ûÆnW(ÏÝO`d,Z½F7Î*µ†iO@lÿaØívl^CiÖl6+ÞÁD§!8:ž‰·<ŒJ¥æÏϸ½KuaE;œßktzÂâSZ}}܉N´§?ÀÃàƒ©¡Ž}ëVÒPUNPT‰ÃÎ"qØÙÌ|ùKÞ¿{*5%­@X­Ñ2fúœ=sMµUl\´úÊRBãSè{ö$"¢™òØ+,¸éü£ºßÉjÚ´ñÌšu6ìeƽTW7ÊÅŸ‰J¥bÁ‚Åüüó†n¾iÇ\vÙ(î¼Óä­¬¬ãÏ?wÒÐ`$))Љ3rdßv÷P©TÜwß\pÁ023 Ù¹3½^ǰaÉDE…0þMüóŸ°n]ËVFË—o"%%šþýã ök5 kèÐd||¼X¶ló±>åvu÷kRW×ÄÚµ»=ºcÇl3ð6v¬£OEE-›7· ~›Í22\ƒëAøù¹o»ÔµZÍœ9Sϧººž­[÷ÓØh$**„ÔÔ8ÒÒxöÙ›™>}¾ÛlD__ÏþxCh›ÙläÞ{¯`Ú´QÜ}÷®¾ú‰°9>ûl 7Þx!z½ŽéÓGñÎ;k"Î3}ºdÚ»·œÒÒƒÇüz{âT9'+WnbêÔ È AiE.—>mÚ(>ýtsÄàPmmS‡¬¶{ٳÇwûŽtÉ%SCA·>øš¿ýmIX ̘W\1“ªªºNK€ÞzëEäåeÐÐÐÂü“½{Û‚‰‰Nžxâ23“¸ãŽK¹÷ÞŽi}=e0è¹ÿþ+1ôlÙRÄ}÷½zªªòÞ{kùÍo®æ¼ó&`4xá…ŽyÇäÛOx1©vZ¹QUPZf«ÓŒP°8L,zêhêØtžV?Ž8 •­¨*|Ú÷S£Å€3ÅFcU¨í J@!à“Ñt(Šv\p>9  ~¯ö¼Éj@QT‡'dE£TLvJ@Eö+ Ña}~O^‡*k¯'8ޑۜߙlÇ`ÑÓT­­;*ÑŠ§ÕÏåGoÐQóÆ'|žƒRî.@×õn‚ ‚ ‚ §œ!Ú]Å«Êik=ö>'Ê«õÕk¨ïØw"/,~¿ÝÛ6à;œåÜÿûNë“aÀá°Ó±— €xŸ°m¥H³9ãÈ3)â>Ál·¶Æ:ŠÖ¯î°]o02k‘VJí›·^ º¼>xânÚaM`ÔœK{´¶„ÌALºlÛ?}w¹Öºð ý‘2Þ‚Yiªª²ý³÷#Ž[¸nïüï­]ö©;Qcç^Nlj&ªªòþïî ºTîÙʪi½A¦‘28¿Ëñö}óo>|cXÐ `÷í§¸Œ¬ÞY|éééœuÖY=ú3lذ>[ÇyçM@’$Z[Ý,^¼¢Ãöººf>þx#?ûÙœ>[GoY¸p&ƒŸ/Àc½Ú!ø³ys!ûÛ’.Ç0ô,Z¤ý}ë­/ÂL^¯Ÿ'žx“††bc£˜3çŒc46¶†J$3¸"Í3eŠö>]¹rcÏ^àq8UÎÉ×_ŠÀŒ£"Î3lX&ÉÉZ«+úîœ8v®½V{?óÍnž~ú}ç[ùÛß–ðþû_E#77ƒY³´ÒíøÃ›aA7Є¿ûÝkL˜Gvvjo¿ &NJB‚vCß?þ±Ÿ/¼’†,+,Y¢e_ŸuÖ0OþÏ&f»¯+@|f4±éŽÐó“ƒIÎ ámk?ÿ’^B§“ôÎT;IƒbHÈnïC7ÀÞ¨ =•l#:Ù/n@z“¾ÃqGn·D™BÏG'ÛHÈ ïs ú*rÇ^­ÁõŤÚ1Zô8Sí8Sí¶ÇeD›Ñþzƒ¯3¸®Ð~‡×Ûú<ã­?|²c%ÛʶSÕTÝßKAAáaŠ&95€ýGd» F’Ó TW–öx<«ÍNl|« 9 ¡¾–æÆú.qDi¿XºÊŽˆŽ‰#5c {wnÁï÷ávµa2™‰ŠŠÜÀþû&˜½ãÄá°‡='Aâ}"TîÙJ]Ù~âä0|æ<öoì˜é ¼íü새q9ãÏÆ­]|ÞðÁ"Îãs»ØýÅGŒ¿èj†œ9“õïþ»Ûµ-6\Mõ,{ú×aÏ+rU‘qG¼S’$‘;éö|Ùw=¨º’7e6Õûvp°8r?ªí+ßcöÍ"I:†M;¯ËlµK^AU:žû¦ƒí%*¦Žýì"±Ú¬Fü~?>¯9Â×ôh6l ¹¹g7ßÔÖÖöÙ:228p ¿?réëòrmþ„'N§¦¦¶­§?K&®_¿·ÓòŽMM®.Ç?>7T¶ðƒ"]Ün/_|±‹.šÌ™gãÝw;öZ\±b'eøð$&ÆP[þ½`„<ìv ²¬ðÙg›»}mÇëT9'€ÌêÕ[™7ï,f̱Ÿ]0°¨¨’ýû«º~a'`Ê”|Ìf-ð²xqçe/»2s¦Vš½ºº>b¹QÐ2KJjÈÊJæÌ3‡R\Üû¯)##ÐEEE‘ǯ¬ÔzÌI’Dzz55]ÿ®ÐÛ¬Ñ&«Ú0Û Èr{£Ñl@g—´ì0wûçÏÀ`Ñ#ûTEË8 f£…HRèyEVQJhU…¶z“Eް5ì8¿WÆï„ÖÚïðzûÂixÛQ¾“å}“Ž.‚ ‚ ÂwS0Û­­µ…ƒUíwÆÚQŒŸ4W[koÃGÇÝ1(V[]ÁÆoWãÐcJ¯7 ×k¿‚ù¼áÛ‡æ!à÷ST ýìëf2÷ìÂãw‘ŠÖWÅbéÙkôx˜×_RRb‰×n.Ú³§ç71mìØÁ€–íW]Ýù9 †rr"g2­]»—Ë‹ÍffƌѼõVxvk0.ØS¯/œjçdåÊMÌ›wiiñäæf„õj“$‰éÓµL¸¾ìwZР¥ÅÕ!S­§ÆÓÎÉ®]ºÜoÿþJ²²’;=''*øX’$¬VSĺÕÚþÿ[‰pÃA_s5iß+«Âßçõå‘Kx¶r‡=–$Â>6¡ªjèù#ƒ[¡ct­µ®°ã‚óI:),ÐVSÐVÖôPI3’$…ž«-jâÈŽ^ŸÁ¤£±²­Ãö#Ç8òùƒ… Ö-éºÿ~s£CàÍh40y²v3V_–T<ÕÎÉŽ%TWד’ÇŒ£ÃoÆe’”ƒªª}ší¯'˜ v"cde¥tyNrrÒBƒ½mÇŽöþ™g‹ø~1b  eÅ8pì½[¿zêøä¥äâò¹ÈIÌ&!*ÿnÐn:ÉÌÌ$..Žêêèõz**´‰‰N$IbêÔ‘”SSSÍf¡±±‡ÃŠÕjÆíö"I^¯Ÿùó'#Ë +Wî"..޲CMø½@Ål‹ÂåÄÛÖ‚çp‰ÿæZ-@œœœLMM щ)è$]èóÂüŸ²þ½ÅH¨HR{—4UU$ySçPU°¦šrY8é IDATTEÅ{0½»•¦Šð ì¼qZ€wSq%¾€Œ/ c7›ˆ±[pyýŒÌÔ¾O5ÚÙ½»oú~Ÿ¾¿Å ‚ ‚ ‚ –™=ƒÑˆ,(Ý^&Ëp ·O–¼€×ãîÐt>3;—±§åŒeP^>»¶†|Ä]ŸG›—?–@ÀOÑÞ±ÃáCúæÌÓÁ` Ї›…ÓƒxŸ|ÿ4V—Q¾s#ùg0|æ¼ðÀÛá2“e»X—øµFÇ2þ¢«»ïXnh­ïYÙÂ#•ïÜÈÿýì†Ï¸€üóÈ3 £ÅÆÐ³ÏcèÙçÑ|°Šÿ|_Ä~u½ÁâhȾ®³ìd¿vóˆñÏÖNLL$%%¥Gû¶´´PRRÒ'ëX¶l]4™ôôî»ï þ÷_ •D”$‰K/=›qã´›†^ýó>YCo±ÛÛ¿æG÷¹:QQZ6|t´‹.šÜíþƒ®Óm+Wnböì3:t))qTWk%þ&LÈÃf³àryøê«¾«˜vª“`PíÊ+g1}ú(ž~ih[0ÛmãÆ}–Äì-N§–îñ֮ᓜœÔe³éõ¿ONDaa%«VmeÆŒÑÜpÃTUÕ±}{{0.??‹‹.Òú~óÍî~)kÁl0a5Y)­+ =Ÿ••Å®]»¸òÊèõ:ž|ò¿\uÕ,6m*Äj5aµšøá'±}{ çŸ?5kvrõÕçP]Ý€ªªø|~œN€ײַÜIÀçÁçqQ¶}&›£ÅJÞ”sA’0VìD–eôz=çœs ‰Ã(Þ´–¤ì<9€ÅádÒ‚Eè&Š7­%y$·9c)Þ´“ÕŽÉjgÎ-¿ÁçváˆK¤±ºœ¥¾/ìu¯Ù{$qùY#ðddUåÛÂrâìVÌ}h»#ÎÍ AƒØµ·°×Ͻ¼ ‚ ‚ ‚pÚË2 €ŠÒýøg•ŒÆãqwÞ‡£´¸€9CˆKH&cà 7ùˆ‹ÿ:v ÚKÚ€,öíÞ¶.Ýá’”r ¼ÙüéÄ h½|ª«{ÞWÁáp„ÊVW׈€Ê÷@]vG|uuMïaÛŠwÉÈ?ƒ¡Sf³Ô`Dø‰KHÊà|TUeÇÊÎ3ÏÜ-ZࣱºŒåÏþ¦Û¹"õ‰ëmŸ—mŸ¼Ã¶OÞÁ`2“5f#fýü™”ʼÀâ_.¤ldž^ŸÛïiÿ¾g0™»Ø ‡û²¹›ONyצƦ°-Í‘K§mÆŒ\sÍ5=Úwýúõüæ7¿é“u¸\^~õ«ç¹ÿþ+ÉÏÏbñâ{()©¦©©II‰à­·¾`ùòõ=³¿¸\í?ØLǹ¹¥EËn®®®çÙg»Ï•åγ$7mÒ‚HññÑ̘1:¼œ1C 2­^½í„bÝ9ÏÉŠ›¸òÊY¤¤Ä1lX&»w—"IÓ¦ËLö]`PðõX,Ç^q¢} ±±Q|õÕN–.ý¶ÛýZ»Ýçx=ñĸÝ>Î?O=u3û÷WQYYG\œƒ´´xü~™­[‹xå•Oûl ]IŒJÄg¡¤îÕMíYË.— »ÝŽªª˜Lí¿ U‘››Áž=¥$'Ç"Ë ëÖíaĈ,’’bhkóâõúp8lø|~jÂåò†Æ3˜Ì|Y¦pÝ*&_þsÌ9Úëð¶µR³kŠ¢0iÒ$V¯^MòXH͉ÑlA§7 7™ø<Ôí&5w$ •ð¹]XѤ掤rÏVœÉi¸šh©­Âh¶ðº6lÍÍÍTTh¿_4¶y£A/ ãÈ$DÙHˆ²a3Űå@5<ÀNKKÏ>³•¼ ‚ ‚ ‚pZKNÍÀq8ƒaÿ¾Ž¥DŒÇxëÎÁêJâ’‰ŠŽA¯×‡•” ü(Š‚N§ •·Ì˃,³ݣIÛîï&»à»LB»ðÒÓ€GSDéÀï¯W»p)Þ'±صj)sny³=ŠA¦Qðõ§ Ÿ>€ÒmßÒt°²Óc[—Á2Yììû泓²Þcðy)\·ŠÂu«Ø¸äU~úäëè &_q#o''Êëõãvk?«ìÞ]ŠÍf&77—ËË–-E¬^½Õ«·…ýNp2-Ù²4âó[·n%33“}û*°XL\zéÙ(ŠÂ’%ß (j¨ì}Ð?ÿ¹ƒA*E:þ–,ùåp¯4ƒÁ@ff&•{·²qÉ+¡›R¾zãïçÿý÷¨ª~EQ¤öYtz=’¤CQ¦^u3ëßý7~EQP÷É“¤7Ðéõ¡›sss9x0¼”§ V×óñ¶}ÈŠŠA¯#pD`ZE+ï_P^ ¥·ˆÀ› ‚ ‚ §µŒƒ­7@þè ¶Ûl,+“gœÀþ}»¨®(=æ¹|Þö‹üF“ù¨ 9W[ Ž('f‹•(g,i²)ܳ¯7<8`µiƒ¶Ö¾-÷ó]áp8HHÐ.Ј`ŠÐñ>‚Ü-~»Š¼)ç2|æ< ¾þ”a‡û»uUf`ÿƵœuÙ"l1q$eçr°¸o.Èõ†²8X\@ò a$Ü's¨ªBUÁ2GM$}ØØN÷‹ŠO&:Q Ì•lùºOÖ‰ÛåÆÍ±õ€\¾|9Ë—/ï÷u,Zt#Fd³dÉ×<õÔ;½º¦c,­Ó_‰ëÊÊ:Z[Ý8Vrs3Ž{7îã²Ë¦ã ;;…ââ®û vgåÊM\vÙt† I'==¬¬dl6 6²m[q÷œ€Sùœäæf0cÆhþþ÷Ce&¿ürû •ì©M›ö1uꢢläåe°woÏ‚àG‘››Á¨QÙèõº.³üúÚŒ£¹ä’©8PÃm·ý5° þŸ ÖO5~¿Ÿ¢¢"† ‰Ãé´óÑGZæ`  ¶µ`±^¯gî܉¡ [°jÇ{ï­ %%.´½±±–æµErr,mmêë›q8¬ø|~E •21"›¶6­­.$IâàA-Àœ‘‘ˆ×ëÇçóãpXiØü!©‰vŠ‹µã-šË{ï}…ÉdÀëõ#IZiÞ††¢£­ÔÕi8§ÓŽÃa%{ÊPâ*´›A$IÂb‰&..Ž9sò(.®Æl6qÖYyâOoôú9î›§‚ ‚ ‚ §ƒAËh“$‰ÉiþØ£´þ5:½>ôœõp0îXéõíe#e«575àˆv†²Ý Êv³Xm˜—ójn:9åºNuÁò]"˜"tE¼O„#m_©Øò&KRv)ƒ‡ðyÙ½ú£.+ÙümZ‰ÓiWßÞçëìŠÑlÅÜÍ÷#“Õ€§oÔØ¹êC’²óH::â>cç^Ž$I(r€‚µ+úl-§›Ñ£shkëßÏ,G»Xq{LŒ)Sò;=^UU¶nÕ2ó&NJLLÇ÷­$IÌ;±ËulÞ\Ê»úêÙ=Z{W +Cª3F3}ºöþ]¹rcŸCNÕsòÙg[P…ÄD'ùùYG”™ÜtÂc÷Äš5;BÁŸýì¼.ûw¶íóÏ· ª*±±Q\xá¤>YgO¥ýv¹¼§d€­;6›…ýû«˜;÷LæÌi¿A1øxá™8vn¹e>‹ÍeΜñœwÞøÐ~mOHˆ&>>Š… gÚÜ~ÕU³:43ôüe—McÁ‚i¡ã‚û9V,˜Æœ9ã¹îºóBÇ=~ðøÂB-Ã=¸_VVåååȲB]]ß”˜ 7AAANkÛ7˪ßëôÏþ‚]€Ö¿-ø\Eéþãš+:FëÇâõ¸‘åŽ}Cj«µ¾ΘxÒ2²()Ú‹×~‡|RJzèß«ý.àÓQcc#åå"˜"tI¼O„#|ý)žÖfL6;Þõ{ö~µ¯«ë^?rÀÏŠç`Ø´ó¹ðî'°Fń훚ɼ»~·/H’ŽK|†Ÿ¿ø1#Ï™Þ^Ùdµ1û¦‰MÀþkúl-[—¿MSöýë’ž&>#;lûЩs˜úã[XÿÞâ.Ky á‚A¡Ë/ŸÁŸþt#÷Üs÷Üs9÷Üs9·ß~ W^ùNïf”ÞX‡Vb566ªC€mذLž}öV’“c»ã7V`6¹÷Þ+°ZÛ{:V|ð*Î:kX—c2Ï=·€iÓFr÷Ý ˆŠ²…퓚Ï]w]Æ]w-èÑk[¹R (sθ#ÊLžœ Ó©xNZظq7Ýt!‰‰NêêšÙ´iß1½¶ãUW×ÌâÅZp~„<~ø'¾yyøýïqë­ó#ŽQXXɲeëí5\zéÙ á=ƒGŒÈâÉ'oböìñ‘†è5ÁÿÃÆeòüówpÏ=Wpç—rÇ—róÍrÅ35*縳IûÚ¡C”—×âpXˆ‹;2ÀÕþØ`ÐÓÔÔFEE.—'”i{rr,£FåàóHJŠ m nö” ö› ܯ}»v“URR ªÚÞ“îèñƒÇ·´¸™<9?´_°Ý¡CM|úé&bcX­Çßo°+RJJÊw/+‚ ‚  ‹×J²µ¶ö]Ãñï‚!ÃF1|Ôx\m­¬øðÍN÷Óë D;ci¨ÜïÅl±rÎ?Â`0²¿`Û7Óq³…s/\€^o@‘eV,} Ïå(%Ibú¹?ÄOý¡¾ü4r_†ÓAÉòd7°ŸW"œÊ²³µ üÅÅ}[L8=ÍûåãŒ{Eèñë\×ã¾m³n¸—É—ÿÐúªUî݆Ï݆3)„C$‰€ÏË߯?úŠ’Ç_x÷ 5“ÕŽ=6€†ªR8œñï;® åPç%ãbS3ùñ_!&E» é÷¸¨.܉»¹ ›3–¤ì¡˜—%®Ü»ÿÜu%>W[ı®üÝ¿I<<ôXÒé°9µE<­ÍÈþö í ïÿ‡/þóL‡12†ãª'c²ÚQä»7ÓÖXO|F6‰Y¹Øú ¯?p¾£Ê,D'¦ðÿ^×JPvöu0˜ÌÜ·lвKJªq»}$'džq.——k¯ýµµMưÙÌüö·?eܸ!€Ö/qß>통ÌÌäP©Ä²²Z®»î}VŽR¯×ñè£×2aB>ŸYVe…@ €ÏÀëõQTTÅóÏHII5ªªõPS…èèÈY¦'[°dg°×Û|,+¡ç%IB¯×…²ÞïèíG2ô¡±.ºh2Š¢ðî»kCÛEåâ‹§h=ÜTX²DÓ`Ðw˜ïÝw×¢×k¹dÁ}E+çyôøGÜO¯×“™™IQQQèu+ŠJum=É ]ß\ÐS)w—hsöÊh‚ ‚ ‚ ßU=( c09kÚlbãÙ»s û v´_H°;¢?i&ƒ¿ßGQAäæô^¯‡âÂ= ΢*X­öPàM’$FŽ=g¬ݳcs/¼8A„ï¯m+Þ Þ\õ­_Ýãc?}þwTlgÊ›I<œÌ‘í%¸ü;W-eí«ÏE ºÖó,˜›Ú^NKoèú’\CU)¿~.¾†±s/'65“#Âû”6¬bã’Wøæíø¼ŽesƆ‚G³8Â/úší‘K[–ïÚÄ¿n½„Ù7?DÖØ)akñ´6³ñƒ—ùâ?Ït¹¡£ÚÚ&~ö³?0~|iiñZÖŽ$I8vÆŒÄàÁéœyæPžxb7ÝôL¨‡TojkóðÀÿä®"--žñãµ`ªßà“O6ò,%33) êÌsÏ}@CC —_>§ÓÎÙg ªªŽGy…½{˺àùç—RPPÎÂ…?`ðà4FŽlϲôx|¬Zµ•W_ý¬ÛhçxëÖýŒ£õü]¹rc·Çô¦Sñœ¬Y³·û’PÞŠ=;'&“ØØ¨ˆÛôz]‡mÁ~`G“e…ßÿþu¶n-âG?šFvv ÙÙ©¡í.——Ï>ÛÌk¯}1èÜç¾û^dÁ‚éÌŸ?…øøhÎ8#7´½®®™>ZÇÛoѧ=àdYá¾û^$??‹!CÒ0ô‡k*V«‰ŒŒDòò2HMã–[æóØc¯„úœ ÇÌÆìLâàæåèÿþ÷KÌ1)¤ŽÃÁKÑ«*HÚ×2Ê™„ìmãýe[1XèíqèU_K€Œ9&Ig@ xAU‰v6mUûh)ÛI ³d…vs…):AûL’0šm,Y±$æèDÌñ™<­¨²Œ%Ú†ð±~ýâââˆIÀع ظäTEE:üs&§ãmkaÙÊXÑD'¦ **£‡ a÷îÝÔÕiÙpŠ¢„‚n@Ÿ¾7DÆ› ‚ ‚ |O‰Œ7Íà¡#É=¡ËŒ7NÇÈq“ȤÝͪ(2õ‡ðy½˜­Vbb$ 9`ýןSSYÖé|:½ž©?˜Kl\"ªªRW[×ã!6>›]»`±o÷6vmÛÐû/ö"2Þ„žo©À™œNLJ&«Ö†ZjŠv£:–îK1)p&§cqDás»h¨<@c?•#¶Ç&8pF‹•æÚ*êÊ‹ xE‰×¾rÙeÓ¹ñÆy<üð¿Y³&òÍ=½A’$N#11†ÖV7……•¸\Çþµ5› ˜L\\‡5STTyÜý¯’“cII‰Åj5ÓÐÐBQQUĬšS8'KI‰%9Y{=õõ-US@D¯×‘žž*‡ZSÓ@iéÁ¾Zn—‚_Ó#ÿ<8;ÏÏÇoàý÷מ2o)çãk®Å–œªÈT|ùÙÜ €Ñƒìs㩯${-‚ ±iª©õ8ë/Õe4Vw~SÇÉÔÖpˆ¶†î³j„ÎY­fæÏŸ À;﬉XŽ/è½÷Örà sÑétdd$öéºTUeß¾ öí;±÷»×ë§  wÃ55 ÔÔ4ôÊXýIœ“ÎUW7P]}ü¯G–JKžô`Û¼ygeeíÚ]νo_9%%Õ¤¥Å‡J`ž*|-‡p×W5p’.—n¸ÐzV}ýõ®Ðö ò¸çžËkþ«®z¼Ë,»ï3›ÍÂâÅ¿:®c{ì56mÚ×Ë+zÛ5×ÌaÞ¼3ù¸ÂÂJî½÷…ÐãM›ö1|ø@.œIMM_}µ+¬4«Á gÈtæÏŸBl¬ƒÖVë×ïí•×ÐÛTEûì©þö½°ç Ö( JÀ‡*ËèÍZÏ5Ðú¾ÅåM¦­j_è8Sb’AÁaCo·wÎd-û‚†kP|~Œñ1è­<åÕ¨r§Ó‰ßïÇ`fª‡{·I˜mQx]-H’Žè¤4ZÚZpÄ%"˜mZv›§I»iÑïoÿ< ãä€ÂÞ½ýw¾EàMAAAAá$zî¹øÓŸn$++™'Ÿ¼‰@@¦µÕÏÀj5e í{èP=ô.W{v½Éd$66ê¸æfÐ étÒqŸW“I\jÿ.°Û-Çõ5v:íaß~ûKÎ>{$&sÿýWj½›ëZp¹<èõ6›¿_ÆëõÑÜìâÕW?ë·ÞsÇ+mÊö|MÜð³Ãz®­Q4oAo² 7µV%þðHz=ŠÛKÓúmèÌfâÏ€1>†@}­{öã)¯`áÂ…´´´P*Å‚$‘28ŸÛEsm%eÛ7;ùŠ7­EÒé0Z¬Œ{ŠìÇ`²àim¢r퇴µµQ^Þ?=O»"> AAAAAN¢½{˹öÚ?rÅ3?>—´´xbb¡í--. +Y»vË–­Çãñ…¿m[·Þú—ãšû豄v.—÷¸Ïëw-¨ò}õöÛ_ðùç[Žù8·;¼¬|K‹‹›o~†‹/žÂôé£ÉÊJ&:ÚŠÝnF–ÚÚÜìß_ÍÆ{Y±b#uuͽõNš`/·£{®µ5ž:„Öʽa=ÜPAg4hnÅ‹ÜÒŠâ`IOFUTY¦es{ænYY­­­xãxÛZ‰NL£¥¶ I¯§pÝ*œÉ餿ŽÄÓÒ„9ǨÈ~?>ÙçÃ`0ôÊ#G 'Úaï~Ljn[€”’’"‚ ‚ Â÷P||<­­§V¯áû¡dù²ÎØÏ+NeÙÙÙ÷óJAú–N§Ãn· ÓIx<>Q R¾c$IËrÓëuø|~ÜnŠ¢ ª*ªª¢(JèqðßÑÑÑý½ìnIzÃáþkí=×ÚŸSÂö›y&:³‰CË¿D2èQ—Ï•ô:TY9zèŽsÕ»Mo0jçìp)LI§×æ<Ü;77—àõö¼×v$I)©½x›ý 2ÞAAAAAú•¢(´´¸ú{‚ 'UUimu‡þ}ºPå@ž¨ÿüÛö}ŽèYÙ“ €ª*Èö}ƒ¸ÐvE{\PPУqûƒ¼ ‚ ‚ ‚ ‚ ‚ ‚ ßKé鑃‰ÇÊîЪzˆNš‚ ‚ ‚ ‚ ‚ ‚ ‚Ð NûŒ·¼Ô\Lz#ÛËwö÷RNyÉi‰¤ LÁb5ÓÜØJÑîb<î«ÚSŽh;9y±GÙqµ¹(-ª áPãI™[AAAAA¡7ôyàÍh4rë­·òÎ;ïPRRÒ×Óupë¹7ÜþÊ]'}îïŠÌA\}Ëedç…75ø|¾t-ï¼ô!>_ß4tŽq°ðç—0þì1ètí ˜ªª²}Ãnþó—7©¯8AAAAAáûÃ1ùö~ßl:ü/T@ƒÑLÀïEBÂl"à÷ðyP½Ñ„"ËxÛZ"ŽçpXÑé$š›µ~–v»I’2$‚‚rTUE’$ÚÚ<¡ã"m·Û­øýÜn/&“YVPU—Ë6_ðùS¨«kæàÁ†°ùÕíÿîõs×ç·‰'òƒü€ñãÇsß}÷qàÀ¾ž2Än¶“âL¦¼¾â¤Íù]“™“Î=OÜŠÅj¦î`=×n£­¥´ÌTΘ:šsçO'1%ž¿üÞÒj³p×c¿ =+·ËÃÆ5[©­>DLœ“ ÓÆ2jÂpîùý­î饂 ‚ ‚ ‚ ßgÊ ³ñÏ{Ó«×"Õö÷rAøŽP“rñ-|ã÷¢+þª¿—¢¤Ž@>c!Ê€3Pé¾ü+†µëïe}ïUìÞ@MÑn$I‡r86Q½o'ªª¢ŽoH’Žª˜WTTö¸®®ƒAÏðáZLbÆEE§“P­„¤$I(Š6np¿o¿Ýqû‘t:%%ÕH’:.8ßÞ½eÇÝ»W+]iíÝÓœ¤ÀÀ|€,ËÜxã¡à[yyyŸÌ•—Ψ#B-FsØã e[?{lÔ5`~ÙÏøì3pÚœè$ÃÓ‡ñãÉ 9Ø\ËÌá3hó¶‘•À‚3Äžª¶–n á´E3jÀâ챜;b7þ`õ­õ8TJ²3‰Ìø\söÕä$åðôÇéöµŒÈÈÇb4 “tÈô^àmô„á””r¨º.â>ë¾ØÌ¹ó§“•›ItLÍZªäþ=%47¶pæŒ3Xt÷OxìÎ' Â×6~êh.Xp~ŸŸ7_x?¬T僰Ú,(ŠÂ¦¯¶ÉÖõ;ñz|˜-&òÇ e×[zãe ‚ ‚ ‚ ‚ð=a6˜ˆsÄSÝTÝë-4ú‹<êbüýtz”Ì èEàí„Ùœ±˜¬v«ûæZ¥ œ*”Qc3ñýøßßû%úíô÷’LºžÀì X¹NUdo×}O´~õT¿ÎŸ—’‹Ëç"'1›„¨þ»á]233‰‹‹cE0GEEÒIaÙqÑIixÛZð´6!”pðyi®­`̘1466RRR6ïàKºŠ‚ƒH’Ž(I¡â«WI?ûJ*¾|•©S§²{÷nê괘ưaÈgÍÚ5¬[ÓõëZwTÌÙš’zBç)’“xXºt)Š¢póÍ7óØcqï½÷RYYÙëóüë‹Å¼¼ö5Î5‡…“P^_ÁýoýºÃ~xU0â945—Òº2n~é6ÒãÒxòÊ?0>û |²_¾zµÍµüÏ¥¿fxú0ÎÈx Žkáê)WñÌ'eõž/­Ià‚‰—²àÌ1-o*«vvìÉ6 ' €’ÂΨ(-, 5,ÌÈNe׿öƈÿ~öM2e0pp_}oý³ýƒ:6!†«o»€×þþ.¥ûÃûì ÈÖæ®©¨ÅãŽüAªÈ eû+<<›Ùi"ð&‚ ‚ ‚ B9­N½ô·ÄÙcÙQ±“Ç>|â„Æ‹wÄ‘ŸžOœ=_ÀGƒ«‘ýµÅÔ4E®Ôäüyø/þ a\r/úM¢5ljÊ7……ý½ÑĪý‰/_îþFyAø®Ò¯Tÿâ¿øIøÐï^ÞoëQiν$ ý¶÷0|ñ,RýQ ókÁl0a5Y)­k¯`˜••Å®]»}Þ I¤ ÎÇçvÑ\[IÙö äN>‡âMk‘t:Œ+cç^"û1˜,xZ›øüŸ@–eTUåºë®ÃårñÚk¯iH`KÇÛ|0ôøÈ¿KKK4hP(ð–ŸŸÏš5ÝDÛº±{×'t|ýÌ‘èze´c°lÙ2þò—¿Ëã?NjjïG}mÞ6ü²V:QUUÚ¼mþ]¶QE»óÉn¶óñö(ªBY]9µ-µØÌ6¶—íä`óATT6–l Ù™|Ôìíc|¾{u(è\Çß¾Mqm ?>½×_û±HHI ¾¶“ÙDÖd @êíu2Í Z°-ñðþA^·—ÿ{ì%|^s.ÉÐQC-Àxý]WawØøæó¬^Ö1m91U«î`Cè¹ÌA¡ùƒ½ù‚Û“R:Œ!‚ ‚ ‚ ‚Й¡©¹¡6$#Òó‰²D×8‹ƒ[gÝijW=É3®gÁ„Kùñ¤…Ü:ë&ž¼â nuSo.»SÊ ³ñ_ò$H:ŒË+‚n½dèÔÙèZ›—ü™öójNóçÏgРAý½ ¡ú ¯bøäÐéñ_ú Jöä~[‹’>t$W=Æ÷îB:T$‚n§Ä¨D†¦æQßVOuSuèy—Ë…ÝnÇëj¥ù`®¦*JPd™Âu«¨)ÚMjîHb’38j" "ûý¸šð{Ú“¡ÚÚÚðx<”••QQÑž¸ÓZ¶É‚уÎdÁ`s’2ᇡ¿ív;--í B¥¥¥˜Íæ“rNzê¤f¼}üñÇ(ŠÂm·ÝÆã?ν÷ÞKuuu÷žD{*÷†þÝän&):‰’CBÏ5»›°š,Ž :2èv¤õû7˜E^j^·ëXôâM¡ ”_ö÷hí=e±joFK{³§e&óÐÓ¿`ÿÞhmkççYAAAáh{ª ht5c‹agÅ.Z<-Ýt£ÞÀýüЬ„ÈŠÌÆ’ÍT6V¢Ó鈳Ç1"=Ÿ•»û`õáTGþKŸ½ýúÿ ÿö¥>ŸóûbïÚŒ{z£‰]«–ö÷r¾óòóóY´hŠ¢pýõ×SSsò2Bœ9ãHŸvUèqÑ{ÀÛØñº³-)‹¬¹·°÷µ_#{ÛBÛâ†N!å¬KØóò}(_§ó ¾ä>LÑ 4o¦|õË]®§ð¿ák‰Üî§+F›“! ´jn•kߤqß·Ç<ÆÑ _¿ˆš0ùŒ…ø/}Ós³‘\õ'<î1Ó鵿}np;-Ùù3qëÖ­dff²î—ØöÉ;èôz䀿زìMEA=œø$éôZ?¸£J>ïß¿€O>ù$ìùêuïÁºÎsæåQPP6ÎСC9pà§Š~ ¼¬X±EQ¸ýöÛCÁ·“ýAÜ•¦Ã5 ”wähÃÏéƒGQU•u¥·U4h5Lñèuú™wGrù\Ƕðc`4i_þ£{³-¸Ýh2Fܾvå:òFfʹ¹å¡ë44 Ÿ×Çsý ¯'ò7¦àÜr ëÔÀáíÁýAA¡·H’tÚôûA:jr7qÇkwú8ËAž;5tûß%SP½/l»„ºaº/ùø{T[ÒÁŒËÿ§Ïçû>Ù¿q O]1 “ÅNcuY÷ÒétÜt“–º|ùò~¹Ök´Ç5`xèqÒ¸ó)ûì_öÓ›í¡ý$}øõ]cT\û¶N®ýÙÓ†`‰MÅ×|¨ÛõèŒÇ—‘#Œ¡1LŽØã#âÚ–=¬õ|KDàÂÇ1¾ñó^»§¤6-اF%ô¹…ãç÷û)** =VU9 „pç7ääX>úH ×Ö6’ÇܹY²äkEE§Ó 3&'ÇÒÖæ¡¾¾‡ÃŠÏçGQTêê´XMb¢I’¨¯¯&..ŠéÓGSPPÎŽŬY³†ÄĘàŠ+a³Yp¹í?€Á¨½ E Ê|Þö×o:póuDxù¯o‘5d¹#´4ò—ž~Êg1ú¼ásx=íÙoÁ F³éð¶ÎçAA„cewD1õp`ÿ^ vmíñÏúSfÎE§ëþkážTUôìŽËø„dróÇ`0(.ÜCù¢îAzÄðQÙXuÜÇç¥äPZ_Ö!èZÛ’¾¾‰CÉž„’; Ð.”ÓË‘p5Ö㢲}N3\pÙÙÙ477³xñâ~]‹"ûÑé$Ž™Mùªÿ ŠlªŽ>ŒË~ƒï'ÿA6ý€3Еm<©KZ_?Öµ› ú#ëNè36›…ýû«˜;÷LdYáå—W°páLn¹e>n·—òòCH”•Õb±™3ç Ö¬ÙÉe—MÃíöò»ß½ÀUWÍbÓ¦BfÍ‹Ûí¥¬¬«ÕšoΜñH œŽÛí¥¶¶‘íÛK˜"+2:Dàí´rèP#å嵌•þ¨ÌRƒAOSSµµM¸\ÚÚ<$'Ç’““ŠÏ ))&´=¨¨¨ŠÜÜŒÐóԜܞ'11†ÚÚ&ôz‰uëöœKnn--.rrR¿_Æãñáóu] ðxõkàmÆŒÜyç´µµñðÃ÷çRzÙØy Ñj²†þíñ{:ݯ¯U—$3'Ä”øN÷±9l¡À[uÅÁN÷»îÎ+‰OŠe׿rGäpÑÏgßÎbön/ìtn Ë¹’ÓÂöAA„e0=€â}Ç×—gÛÆ¯©=XÙévWk×›d fʉ˜Ì–P¦ÄÉ(U&‚pº‹²Da餜[“»9b…£®ô÷g³êLE<ý¦×éX³= k”€¦šÊðÁb0™qÄ%ÐRwÙßùyrÄ'‘9b<¶˜x<­ÍTîÙB}²¼ÉéaçóÈy$IÇ€‘ãIÌÊE’$«Ë)ÙüŸ·³áˆŠO&mèhq‰(ŠŒ«±žŠ=[h­ëþRtb :}äË¢5ú u'qàâ3a‰ÇÝÜ@ù®Í4×vi)éô8“RQ…¦#~¦HÊÎ#eðpŒ •8°õÛPÙ¶SÙÏ~ö3ìv;………,_¾¼¿—ƒ„DÝÎÕ$Ž™MÒsEà­ úM¯k·¼s0Øã‘ÚŽ½ÝqóµiLvÔ¨d¤ê¾ï™)œ<_|±€¿ýíCdYáÒKÏFQžyæ]dYA’$ôz]ÄVXƒžÏ?ß‚^¯ ·dÉ7(Š–i~äqÁíï¼³€O>Ù¶}Ù²u(ŠJÓét¨j{ÆzRJj¯¿ö~ ¼ƒn.—‹|0ÔHïta3Y‰¶FÑᎨ¤h­fm}k}—™q}­xï&NËàáÙî“w¸t¤ßç§¼8ò……9—Ìdô™#¨­®ã¹GÿÉäs&rå—ðó{®æ7·Ïî/–uº–kÿò.Q )·ý~^>>wÏ*/¥ ÎgÎ/~M樉¶­_ÍGO=HcuyÄc£â¹õ•/QäÎÉ%kÌ$fßüÉ9CÃök¬.çõû¯¥ö@ÇR§§Š¡C‡2kÖ,TUåÿþïÿN‰ºz‹ƒŸ/#qÌl¢³Fc‰ÏÀSùkñ}úl‘t(Y“Ðïüð¤Î/5× &ä FEþ?)|wÝãí¿ÿý8öoÁã‚=ÞTUëáìñÖ¾½ÿ{¼5r?ÐO·`ÐÍívóÐC…5âëMJ°O˜ÁØ'ãwgtæ(¾Ü»¶Ãó#3ò(¬éþu?ò£ß`2h?”ÜÿÖ¯{5P·éëí,¸þ"’Ó<,›ÂÝÅö™v¾öÞ¶õ»"öYËÉÈ¥×ÌC–žÿýbÜ.Ÿ~ðùãò=1Ÿ~u5z ã7܅媩'!9Ž©çžÉÒ7Vtœ{ÎYHÿŸ½ûªJ8þ½Ó2“Þ$¤@轂R¤‹½ º¶EWqY׺+kÁõqí못¶ÕŸeײöŠˆ U:„@Ò{¦ÏÜùý1dÈB€„PÞÏóð¹÷ÜsÎÜÜ´ûÞó¾ŠBmu{2¥Î…B!„hi=zp0w/îSü œ¢QˆŒŽÅá°³kûFòröœÒñÏdF£›­ó2†ˆ3ƒ\'¢´¶”ýey¾×:­–¤È¤6? ©?wO¼Ë÷:@ç]=×·KÞ¾õu¿¶9¥¹<ýÝó'7ácP»€RW‚RWrŒÖþr6¬À\]APx}ÆÏh5ðÖgÜÅìY³»¥éÔ¦p®ö}{ÀãQÉÝø ù¹EDÓcäxR‡Œææ—?åÝ?\EMIa³cìß²­NOt·î„DÅß•¨®©üæù Äi·R]”OLŠ7³µ®é.¡1ñüö__ùÚï[¿œê¢†Ð˜º I—Þƒ¸zþk¼3÷ w5h-ÉÙE}eÙ‘÷Ax|×–Of3ºöÊõϼG@`0vs{×/Ç\UNd—Ò‡_Húð±Üòòg¼{÷Õ-žVÇ7üž±·Ü‡µ¶Šßü—úÊRbR{ÒgìtÂã»rå£ÿäÍÛ¦×üNEQ˜3gŠ¢°téRvïÞÝÙS@k¤¾p7–’ýÆ¥7t:~üwgOë´¤T¢˜+ñE¢¦Œ<õ7{Às8µ¯8{œI5ÞÚÛ)¼tÛ»·ãžÖ¨±zWQņÆ“â÷‹€Qoì°T‡™ç]ÃÖÛ©³YõÖ·k&à—ì5Çì'5&Õ—"A¡}Ó”WðëŠÍœ7n(·Þ;‹¿?ü:•eU¾ý“¯Ç€á}ðx<,ü_ÓÀX`p wλ­NËï-$·Ñª´w^üˆ'^û3½epéõSøæÃ¦KÌ¿ûd 7ß=“³&“³+ÝÛ\ ýÒ¸ô†©‡Û-ms±{!„B!Z›Ð•àÃi·öï;õ©lêkkظv9e%8ì ¤W¯^lÞ¼ù¤ú9r$7nÄåê¸ fpp0ááaÔ×›©®®î°qÄ™M®ðÙÆ/ùlã—¾×1!Ѽ|ýßÛ|¼h•#«¹VT)Šâ· ¢¾ãky"S¼ãW4}XúXT·›¬å ~ÅÍô¾`*?¼òª»i:¯ðø$2ú¹ä«fûšò‡ù$ö€¹ªœþ-E{2}ûBcâ¹á¹ˆNNçâ{Ÿâ£‡nn¶çÝ ÀŒžað´™„Ç'qɃÏb cù;gÝçoc áÞÏ~ÀÖLàíü™wú‚nÿ7ç²&«À´:=ƒ¦Ï$<¾k‹A7€ÿò[¿×§\Í¥j{U«7pù¼  ¦x_=t3æªrßþÄ^ùÍóßê9i0îÖûÉ^³”/ü KÍ‘{sÕEùŒž5çpúɾïÛÙæ9‘#G2iÒ¤:6((ˆîÝ»À£>zÌcÏ>ûì ×VšÃ«+K7/"eÚ]D˜HþOï¡gºÙs…R‘‹'(Od·S;°!ðÈ÷9GÛVšŠ3‡Ôx;E‚n6›ùóç“Ý¡ãm;° ›ÓŽQÀ³3ÿFvñ^¬+AAD‡D¡Õh™ýö2vy}Å5żô›X½†ŠúJ#ÛëEaסݬݻ®CÆ>½ñ%)ÉÄwå©·þ®m{±Ô[INïBb²wyïçï,à`Nӥؿ½oQ±‘ìښ͢ϖùí«¯5óöß?à¾'çpÉõSØ›•KÖÿÏ÷/‹×Ñoho†ŽÀOßEή<ÊŠË‰Š¤Gß4EaÛúü´à—Ž;B!„✒֣¥E˜ë›¦;?Š N4êþóŸéÓ§¯¼ò ¿ürb¿sϘ1ƒ[o½•+VðÊ+¯´ó Ðétèt:‚ƒ½OBKPE4G®Ñ¶ågrë;¿ó½~pÚ} NÈŽ‚<óý §|>“÷aÅrbA¾íK¿bø7IÊ QänZÕ¤MÃj7su9š¦³LÈèOÿ‹.àÛçÿät¨-+æëgîå¶×¾%}ø…Ħö¤tÿ±Wtœz5¦p¾8-ß®_€Guc·š››Ö€â½;›M½èv9ÙôíÇûd ž>“ˆ„d<ß÷y¿Zn‡7žyiW_ÄÄË.¤{ŸT_½¹Úê:~Z°Šï?“ÕnB!„¢}…„—àM#•Ûhµ›N§'.1 ·ËEñ¡–kÓÍDDT,FS n—‹ªÊ2j«;~õC{ùì³ÏøË_þÂÝwß pÜÁ·éÓ§së­·R[[Ë7ß|ÓSôi „‡‡IPE´H®q:3šÐëõ8NvîfVž5 7ð†ÃzBãÚ½Šü\¢’Òè3~F«·?}Û슸¾ãgÞZcûÖ/ovœ¢=™”åe“’A÷óÆ·)ðf 'wÓª#A7¼÷Š<ªÛ»ê«™{M–ïÏÙØ´^„Çwm±~ZGë9z2Å{wPº¿ù…™K¿fò] (z_8µÕ Ù¦6 ºÔ”IQ©7Û<¿ã½Þ6oÞŒÙÜ4Ðy,—_~9ÉÉÉ/žð®Øïiô}Hu¡Ù»ݲçQÌÇ=oqz‹4™€°XJ·ü€–#5ÞÂãI<…ÒMßǃAñÖx ‹Åm7㨯Dg Fãr€çH7cd"ªÓŽ¢Õ3x e[3x _|ñ>!I}›ïq»Ñ¢ºć›ˆŒŒdÊ”žìß_L@€FaÑâÌfç2NIàíè Û®]§.¥ËŽ‚üá¿÷I˜)›ÓF½Íì—þ±ÁÁŠ|®zåº&Ûþl~“m+wÿÂÊÝ-ÿaª(`sÚxué¼³ò}âÃâÐitÕSokš+»%7¼ÞúRôöP_kæÝ|̇¯}AlBcÕ5~i'¶äë,ùúØE}?gŸ¿³ Åýª[å»O–°è³eÄ&FD}™ÒCåpB!„íªaµ›¹¾ŽÒ¢#7ê‚‚C6js}›o} #84¬Éö²âB6­_ý ¨1µcÇž|òI~øáã¾M:•Ù³gS[[Ëüùó9x°íËL€·&W[Øl6l¶ŒF£UÎ!ÞÒ rˆ3YHH¡a¡ètÞÛpµ5µTUµ|ÏÅOCìI£mµYk2—}͸[î£×˜)|ÿÒ#¨‚‰ÝHèÑ€í-¤™L2€‚¬ÖS—äî&&%ƒ¸´^­¶klÕ‡ÿò{m®*çÉIÝ[l¿õ‡Ïé}átƒ¹ã­ØòýÿØñÓ·ÊÎl6 qÂÝ IDATP×Q’û ðŽÛkmÕÅD$$ûRy/§µQÀUi[ š¹ÞòòòÈËË;®¹õêÕ‹¤$oýÄ×^{ÌÌö¿q 'ñõÓèk¦tó÷Ä šLH×>Ʀ`)ÍÃã‘ûŽ~ÎWÏË ^rˆÓEXx˜_m›Ð¶Þ¬Õ–€ÇrÂãg.ñÞL!ᤠþ_öíë3v:å÷QÔB)$Ú[Š$&%ƒiwÿµÅqn¦Ð¶¥ª«-+âÀö_ÛÔ¶Á¾õËùî3éο` ⼫gsÞÕ³©¯,#{ÍR2—~ÅÁÌ ÇÕçñ  Aà} ®¼¸Õ¶uå%D$$uü?ë€ ÔõÖFŠ¢0gÎEaÅŠtƒ?J£@e}á,%¹Æ¥;ôbò½Šç ø=íTòCP,m»VNøóRS„ñé~ Õ¡&ôÃuáݨpÌzÛ£‘àÛYÅQW޵²nýP4þ¡(E£Ãi©ÅQW†ÛnÁm·`Êêrb‹ñíoãû¾¨htxT—_¿G“â ¼A•ãMU%Î=rˆ3b­ö.z36]eÝVÕÅùìÜD×¾Cé3~†àípšÉ–V»˜‚½cÇ¥õjÓj¶†Çb®*?¡Uj›~ÄžU‹0å*z_0Ä^ŽŒaÈŒY ™1‹üùö¹©,Ì;î¾ÛÂêûØíp´ÚÖí´ h{šÈÎC|||›Û0€îÝ»ãñxøõ×_éßÿøVô©ªÊÎS¯®%¥›‘2í÷D÷ŸÀÁ¥ÿ‡G•Ÿ ~¾ÇXkNÍxnš‚­>¹û}ëñE¢¦‘ÀÛY¦r—7­èÁ%oãQÝÄŸw9¨*y‹^ó¦×U¶Ù@¸¢ÕQ±sŠFë;.wÁ?ð¨nï1þoØ_´æ3¿ã+w®<ü7›÷gM™^Orr2ï½ç­'øê«ß ªbâÚþý¯­:<ðf6›™7o^G#„B!„-Jíрƒ¹8v¿}:½¾Íýج–÷ÜŸM·´DFÇѵ[úxسgÏ1ƒo&LàÎ;浪ªŠùóçsèС“S§zëÖ·=}Zpp°/}`qq‰TÎÞz/ÅÅ%m>F®qº©©®ñKÉVWÛ´üIK”òH…'¦ÇIÍaû’¯èÚw(½FOæ;·ËId—nÄwï‹ÇãaÇÒ–ësYëª Šˆ&{ÍR6÷ñ1Ç2W•ŸÔ\ÛÂ\]ÁÚOþÍÚOþM`XéÃÇ2xúLº IR¿aÜôâǼyÛ4¬uíŸjÖi;ò»€ÎÐj[ÝáºlÖÚS—òöD®· .¸€Ù³g÷XŠ¢ðàƒ÷q6›«®ºªMmOæë§±òÌŸHž8m@ ÑýÆQ{°•Uz'RþÆÓƺ§#EƒãMñª”ïkÓ!íõyÁíD©ÎÇyR)uEǸøâ‹Ù¶mÛI?l×PòxýQ?k<žWŸ6l÷¸]MŽkè¯Å~ߘÓé$''Ç÷Úí³§¤Æ›B!„Bt–¸„®‡xŸâÍÝÛ´Þ´þ8oÇRZ|ˆÈè8BBÃÑjµm*2ºÈÎÎæ‰'žà±Çk|7nwÝuUUU<öØc|ŠzoêÀ¶¦<:˜"©Ï v»7P.׉8“ÕÕÕár»Ðëõ8NöÖWI5¦9°÷ðßà1…á‰JA©È;¡9dýüSæÎ' („ôá’½v}ÆÎààöõÔ”¶ü0EmYAѸœö®û鄯ïH–š*2—~MæÒ¯=knû!Ññ œr5ë>»ÝdzÖÖà´YÐ Mhµmhl"umxàdÈõVWW׿ëñññèt:, ••'VÖçx¾7ŸÌ×Ocn»…Š+ˆ<…ءөÞ×rJR§åȪ/Á„ÛÑrºcm@ ÷ó™[OÔ›ïÏNMÞº6Ó^ŸÉû{ºbîø ½8>{öì¡oß¾šåâL%·v¶£ ‹×—ý›Ó~ìÆB!„Bˆ×µ[:à­“Òwàð&ûƒ0Mœ?n*¹{³(.žw+û}û{™Â˜ßÌ`Ã×ÿi5•ç™bÊ”)dddàp8xë­·:{:'¬tó÷¤LŸKT¿qxTжùÛã…¿|DúeÞ}8i—ÜKþò÷pÖW 5˜ˆy £® dãBœæªcŽ—†ÎÚjKqªÛÙâþ€ˆx‚»ôjµ{U‘_ºÌÖ¸û]Š'"íš·é˜öä ‰;òBV¼µ»“­ÑÖ–•°¢yxB!„BœÕ,æÖ‹½ÛlÞ ›ªªÔT·\«D«ÕAUeY³ûŒ&ºv ðàþfÛœIöïßÏìÙ³;{>Lm!׉xhút‹Nö½Ö4Zuׄ;p5º¡½dçO|¹éëS:¿¡)܆vûW¸\kÚãhs~{ëÁ²æ¸v­üžÁÓ¯ó­ÒÊýôSJJÎÜIùŽå$Oº ÞØz»íËîÒ‹¸a3ˆ4™˜“°×”âQ݄Ǣh¼·Õkó¶‘¿ì6Ýãê‡ÙfãóWC+·ÄÑ3I=³Õ>²?}‚ª=ÇNoê1…ãšúÚ-Ÿ¢)Î:æ1íÍxsÙQ¬gn¼Ó•Ôhë<xB!„BœÛÝ\k‰N§gä…“‰ˆŠaÏέäfïÄå:rS$(8„a£Æ£Óéq:ädïèÈ !„hAˆ1„0SX³û‚§3k`2´~ãýt¢ÿþ1Ôn#ñ„%à¼ì9ôŸý¾M?¿Ž¶}ÉW žî pYª+ÉÙ°¢MÇ9,f>šw ç_{Ã.¿‘¨8Ò†Žñí¯¯(eË÷Ÿ°îó·[¬™Ø ­Þà{­ÕˆHô>°¢iaÕQc{×ýÄë·LdôõwÑgÜÅED“íÛïñx(ÊÎdÝgo³ã§o[ìGÑ(5:îhA‡ƒ t†€fÛdmæÝ?\Éä»%eðh’ú©#k«¯eÓ·°ò¿¯àrØùÞNw7Ýt¡¡¡”””ðų¢ñTqÛ-”ïø™ØÁSÙ6oÑ«˜e“pþ5˜¢“?²:ËQ[NÉÆ…­ù Gm§¹™qÛÌ'ݽ¦ QEƒóòðE¡T ÿá¯'=î‰ðFx§SßüƒmâäH¶Î£ÄÇÇÿOi!„B!Ä/*Ê{c©¾þøŸš?›tïÕŸ¾‡c1׳dá§Í¶Ñh4ô2Š”ôž¨ª›êÊrv;&áÑ(Š‚ÛåbÃÚå”Êoq¼ .šAx¤ÿM=Íá'ô=O£©÷ïeÛÆ5'ûOKy? ej·Nž‰8¥¦¦Þ˜BœëÔ.qÜü‚ÐýòºeÏuÊ<4Z-‘]R‹ë(Ô”P~0ç”ÎAQ4Ĥô 82]€[] eöa­=vº¿ŽML·è&jËŠ¨(ØË~vÔšLOOçå—_FQžxâ Ö¯_ßÙSêÆÈ.ÂbÐhtØk˰•ç·[À­3¸¦<ŠkÔl°›1¼šC™2wŸi8¯}ÅZCÀ߇ƒë̬‘Ü á÷øÆÿ7üSUUUý> m=õèÉš4iëׯ§¶¶ãj^ž bã j—¾B'?ÈŠ7!„B!„8&UUÙ¶q5Eyôì7˜È¨X"£ãüö:Ȯ훨«m=MŽF«ñÚŽ¦(ÅÿµBÞ”“†ÿýÇõïâºà.pÙÑ­xù”ÏCu»)?˜sʃmy<*¥û÷PºO§Í¡1sU9æªòΞF‡˜€V«¥°Ð[£2&& EQ3¦?ÙÙ””Th¤ººžà`&SV«EQ°Û\~ùù¸Ý*K—fI~y N» ð‚1$ »¹[}-µeEÄÅÅQRRBhL<Š¢Ás¸žßðËofÃ×ÿ<~µ®=EÑÐsÌв3©))À£zŠˆÆa­§²ð€ßûž1¤'r ™Ø/¶ï#РG¯Õ`u¸(ªö®¨Œ fb¿4–䔵û¹—À›B!„B!„B!ÄY$"(œ“ÁÄÁŠ|ßö””²²²¸þúqhµþñ/¸á†‹Ø¼y&““ÉÀ¥—Ž"33iÓ†³jÕNnºi"ÅÅUx<'aaÁ¸\n_#o¼—ÆÃf!?s#†À ôF=GOEA_¸·ÛV«eâĉTÅôfÿæÕĦöDu»0‡1êÚÛÑê ìß¼š¸ô>†E°ój ¦ ¦ ¦Ì}‡ÕBpd ÕÅ|÷â<¿÷½jÏ4ŠÂµ#û0u@wv*ãÒ!½(­5óÚ’_¸bxïv?çµ?þÀ›B!„B!„B!ÄY%&$S¤‘¼Š×û¶[,‚‚‚ðx< zßöœœ"22º²{÷Aââ"p»U~ýu7ýú¥ŽÙlÇnwˆÃ᤼¼‹ÅîëOgÀå°¡ºÝìûõgΟyiÁ˜«+°›ë)ÉÊBUUFÅŠ+ˆ ýÑÑhuh \%9»HÈèOÕ¡8¬ŒÁ¡$dôçÐîm„Å%b©©¢®¬}€—ÝJïÞ½©­­¥°°€j³ åð{Òi5¨›÷1±_:v§Ëïé´2wdÑ®ç^‰÷´kB!„Bˆ3BTTõõõ<q.ÊûÁ›&ej·Nž‰8¥¦¦°ÿþNž‰B!Dë<O“ÿþ©ªŠªª~‡††vÊ<õz=ÉÉÉôè‰Ñh@«Õ ª* ¬CU½ó;šN§Åårpùå£Y°`-ªê9¼OçM_Ù4›|ˆêv·iZUUQ|Çh´ZÅ;Ÿ17Üņ¯ÞÇi³zÏêm£(4Z-n—€ŒŒ öïßÓéôï_£àV›†¿.ÒÕãá‡m{q«JÊ«Ú=ð&+Þ„B!„B!„B!ÎN§“œœrrrZm7âÊ[)ÊÎÄi³â°Ö£(œv_½ºÙþÌæJâ#üjÆhµZ¦Oá Öi4ÞúmeeÕÄÅG6»¿jËBb‚¨¬tlÂáp¢ª**jq»Tâã#ÐjµTU•jbìØ‘dg°c‡÷­È¨PEñÍÓãñ ( ÎØPââ"ˆ,(AQJÊ«Úá¬ú“À›B!„B!„B!„ði¨©váM¤º¸<\‹_}€Ù³gc±Xøøã¸öZÿšqÓ§Ÿ‡Û­úpsç^ŽÕj§  Eÿþw)³fov~~F£ž)S†²jÕN®¹æB¬V;Ï<ó?¿ñÂñZíäç—a2|óo¨Y—žž@XX›7ï#0ÐHnn‘ofVûgVÀ›B!„B!„8cIJR!„¢ý•ØKX\"vs.»cp.‡Í·???›íÈë£kÆÑjµ€7UeM™²²,fó‘ãZÚAZZ‡‹ØØpßþ£ÇkØ~à@ qqGRF6Ô¬;t¨«ÕNp°‰ŒŒ®dgçSPPFxx0]Ûý¼ÔxB!„âœ%5ÞDg’o¢-$ "ÚB®!„§ƒ3¥ÆÛ‰~ùM­Öp›:u¸_͸o¿]‹Û­¢Õjp»UEA«ÕøjÅ]uÕ~íŽÞߘN§õõuÙe磪*V«£ÑÀ7߬i¶ß†šu7Ü0¯¾ZÍæð«a§ÓiQUE¥RãM!„B!„B!„BtœžñXÒbR‰‰æ‹¯ÿ@rr2‘‘‘ÒûÕ€Û°£§ÝFmYƒ ¢ººš¼¼<Àˆt¹Üt¿ræâ}¬+Ó£(âÏïFá/‘8f…¿|äÿÆodëÖ­ 4ˆ­[·’™™‰Ëåfeމ€°XJ·üà×o@x<1ƒ§°`Éwàñ  ŒD|ýsj`¨•ŒÁ¨.x<8ê*:ìÜIàM!„B!„B!„BøD… 3`2˜8X‘ïÛž’’BVV½úŽiµœÛíÆãñ4©‡¦È.ØkK}¯ýþ?,!!ÒÒRßÿ™™™h XJö;x*ÕMá/Þ~G_ãß”9¸Vl•…€‚­¢—>€è©Ú½–„‘WâvXÉùæ…9ošëù5:cÿ¼ñEž¸ê±ÎžŠB!„B!„Bœ±E!))†€ý± !ÚULH ½zRi®¤¸¦Ø·Ýb±Ô¤œÎàw¼ÙlÆf³‘ŸŸOaa¡o{}þ.4#ú p4#ºÀ0â‡_êû¿ªªŒ?Þ÷G]9ÖÊB´Æ ôÁ‘~c*NK-¶ªC¸íµeÂãéÖÕåÄãÛß‘¤Æ[;›Ò"¿•õ•ÜþÎ]=NDŸ.}ØW²JsUgOç´¦7èIï•BxT(N‡‹’ÂR òŠNÉØŠ¢Ò#‰è¸(4…в*r÷@u«§d|!„Bt>©ñ&:“Ôxm!µ»D[Èu"ÚƒV«aàÀtRSã1™¨ªª£¸¸ŠíÛsq:]=½“2yò0'{÷RXXÞÙÓ9.Ï?ÿ;† éAyy wÞù2UUu=%!Zt¶×xk ×ëINN&''Çoû±jÀMÑhñxTð4¢Òét¸\.ßÿͯº‰?ïrPUJ6}‡Guƒ¢x÷¹›9F«Ã£ºQ4Zâ†ÍUeÛ¢÷ϼoz½ž?üá|ùå—¾\žâì6$e0œ2—¾ÿk÷­ïì霖´:-3®›Ì”+Ç`4øí+Ä£v̺´¼1‚ &0lØ0æÍ›Ç:zHÑÉÒãÒ:{ §½[﹎Q†ãñxز6“û 04²]ºÅóàÓ¿çùy¯vHðmÌäó¸õžYìßs€[ö ºUzôK§÷ÀÜ|÷LLAF±¼ÝÇB!„B!„8Œ7G¹EQ(,,ç×_÷PSc&<<ˆôôDbbÂØ»·cS’‰æÕÔ˜Ù²eƒw§¢¢–ÌLYÕ*Ä™$0ÐHnnÓ§Ÿ‡Û­òÁK˜5Ë›6rîÜ˱Z픣(Ÿ_†Ñ¨gÊ”¡¬Zµ“k®¹«ÕÎ3Ïü€n¸ˆÍ›÷qÑEƒ±Zíäç—a2YÔrÕUP_o¼Aº¨¨PŠ‹« Âjµ³fÍ!²²²}ã½L™û8«…}ÜÚîï½Ão«W¯æ­·ÞâöÛoçoûùË_8xð`G+:Q÷¸ôΞÂimÀð>Œš0UUyåñ·Èܸ˷ï«ÿ~Ͼ™¡£p˯ã±9Ï¢ªí—ú14<˜ë︀ï>Y—ïç·Ê•ã¹ö¶Ë¸òælZµò’Êv[!„âtÓð´dgÏA§7àtØ;uB!ĹH£ÑpÇ3P…uëvñÈ#ï6ùÝ@§Ó¶ë½q|þô§·èÚ5š’’*ìvggOGvï½W‘É¢EX¾|kgOGœ¦ÊË«)((cÀ€T´Z­ß>NKM™²²,f³¸¸ÒÒp8\ÄÆ†ûö7ÈÉ)"#£«oû%ÄÅIY[kÆáp“œƒªz0›íØíjj8<Ž·6€F§ÇRSE]Yû–}Š0ÏûþÚµ×|óÍ7x<~÷»ßñÔSO1oÞ<òóó;|Ü´ØT¦ ˜LÏ„ž„™B±»ìTÖW±åÀV–ìXÖlý±è(~?qÿüñÕfÛ\Øs ãûŒ£¸¦˜7z»Ù±=xHHÎ¥C.¦[t7 Z=…Õ‡X²c?lÿ±Õ?òÏKθÞcI‰îFP@ u¶zÊêÊX½–•»Wawùÿa>¶×Œë=‹Ã ßÿ£Õ¾{%ôdæÈkðxTþ¾èeÌvóIŸ·à€ îŸî§ÇzW¼]sÞULî?ɯ]kç ØÌ%ƒÝ„1© IDAT¦34u±¡1h •æ*¶Üη›RV×4õ#—=„Ýiç¥ÅÿâÎ ·1,mµ–ÞXþ6; ²˜ÐgxõSž×—„¤8úéé×fì´ó~Á ,f+o?ÿGÓ_8 =·=øƒLlZ³å Wùö™<’S¥Eå|õŸï›»øËåŒ?”äô®Œ»x4Ÿ¿³ ½Þ¶B!Äi%(8„1.æ@î²³¶µù†ÚèñÓÑh”c¶Û·{E…Ígû0šIÏèKB×n… ( ªªR[SÉÁÜlòrötz@P!„8ÄÆ†ÀÂ…ëšýùërµ­V‘說rð`igOCeúôh4Ö¬Éêì©ˆÓØÊ•™¼ñÆBÜn•«®ºÀ»啯p»UEA«Õ4û}V§Ó²|ùV´Zï¸ Ö¡ªÞšykØÿÞ{?Þšn÷‘¿ïÆQ ÉÉÉ,zå1T·EÑ 9* Ø^NIà àÛo¿Åívsçwú‚o—y\ï±Ìx'Š¢Pm©¡¨¦FG—È.ôˆïÎŒAÓyòÛgØS”íw\€.€Iý0èší;64–Iý5…´8¾[u3màn{+•õ•TK·¨dn{+©Ñ)¼¶ìÍf½ë¢;¸¨¯w¹eIM)•…˜ &ú$ö¦×~\\ó¿ça2˜|ïŤ7¶ØîD ôèÍM½ñ—æ?GµÕõìÞ¶—~C{1pD_¿ÀÛºŸ71ùŠqô”AMU¾öy“㯽í2†ŽHÉ¡2Ö.Ûà·oÀð>lZ½­Å›9~ÙJrzWŽè+7!„BœµÒzôÁh $¾K7vïØÒæã¢bâP”cÞœŽMÍnOìšÂ cÐë½)Q<n· ­VGxD4áC£é’œÆÚ•?ân¦€¸B!ÚOhh ïãšs+-…âÌsñųmÛ¶ôECì‹/~ñÛîñxZ|¸¡a»Ëånr\ãýÍõÛ8èæ?ŽÛ¯6Ç£âvuÌŠæSxøî»ïPU•»îº‹§žzЇzˆC‡Ú?Gr !ßÿ-¯üø*+w¯ò­@ÓkõÜpþu\2øb~?ñNîþï}í>~Ã~sþ,þ¹äuVìZ‰Š¢pÃù³¸bè¥\Ôw<+öüÂÎÿ§&÷碾㩵Öñä7O“SšëÛj åé÷зK®y-ÿ^þ¾}¥µeü𻑑é#˜2`R‹·PS#Ò†ðöÅMæ|¢ç­ÆRÃMozW‰ýû·¯aÔðÏ%¯±!×ÿ†ƒêiþB6LüyÆ„†±õÀ6^þñ_¾À¢^«göØ[˜Ôï"îŸöGþøÁýT™«ýú4ꌌî1’{>x«ÃÆË7þ˜h~?ñN^Zü/Öì]ˬ‘×rõˆ+š2¤ÕÀ[GJHŠE§óFÑìkyÕgÞÞƒôÚ‹¤´.~ÛíV;¯?õ.¿tfŒ!sCÛ7¹†ŒèÃøcp:œ¼þÔ{جþ+ûº¦&zÇÞÛò7Û¼½ÞT°ñ]cÑéu¸œrÃG!„gNORª÷a¨ý{w£uó¶oZKYiËËXêëšl‹‰KdØùãQ…òÒ"vïØLey)S`=z µ{o¢bâ0d[~mú¦B!ÚOž¥i–Ñh <Ü›®¬¤¤ºÙ‡›ôDDPZZsJÒU¦§'’‘Ñ­VKaa9™™ûOhÅ^·nqôìÙ£Ñ@UU=۷綘 2bZ> ‘‘Þó+*êpußÉd ,,°¹C1›mÔÕY÷íd¤_¿bbÂñx<”•Õ°sgf³­Í}ï9éZ­†ž=“HJŠÁh4`6Û8t¨‚={ò›Zc40 øøìv'–²{w~«Ÿ·ÆŸãÆBB‰ðÛfµ::ìÜ´çµÖ؉œ€ˆˆŽ„Xjk-~×URR ©©ñhµÊËkس'Ÿº:K›Þk{سg}ûöíôÀÛ¹è”Þ-Z„ªªÌ;—§Ÿ~š‡zˆ¢¢öÍ£™‘Ѓ]EÕŬØíÿǪÓíäÃ5ÿó~“­+G¯Õãt·~à`c0‹¶/æç]+|Û<®þ˜a)CHŠêÊ„ÞãšÞúwõ®´ú5wƒ_Ð  ÖZËÛ?¿Ë=G“WÞ4mÍÂ-ß32}CS†My3iÇõ‹N«£¤¦´IpîdΛO“Õsv§½É¶–Lì;ž˜hÌv3/-þu¶#7*œn'ÿ^þôJèIRTW¦ ˜ÂGk?ñ[£hØr`ÕoÎ×ÌüLè3EѰfïZ6îßÌÕ#®$.,¶Msê1ñÞ%€.§‹Úêzb£ :ü£° «ÅFe™7°“Õ¤‚¼">|ísn½g·Þ;‹ùw=Kmu=¡á!ÜzÏ,>zý òs ýŽ  Âè]ÁWQæMBdŒ7¥BMe-U5T”z÷i4¢ã").åüB!„8»$¥ö@¯7àt:(8sìša³Z¨¯­9vÃFÊK‹ÉÝ›…^o`ë†U~È[-f¶oZ‹Á@—ä4’Rº³+s6ë©ûÃ\!„m3fL?æÍóÞƒ™1ã¬Ö¦%M† ëÉOÜ À5×þø§c^g×]7žn¸ˆÀ@ÿLo……å¼ôÒlÞ¼¯Ùã.½t·Ý6½Éö[n™Ì-·LöÛöé§+xóͶžŽG{^k NôœüùÏ3>¼§ïõÇÿÄ[o}Op°‰{ï½’~ýR±ÛX,v,ÿûßrV®ÜÞbí-00¼¼¼S6ž8â”Þ/^ŒªªÜ}÷ݾà[qqq»õï9¼ª*28¢Ù”ÓíäýU´Ûx-Y±«éª<ü𻑤¨®ôéÒ«É~õðßé±ih5ZܪÿS)+ò[\­µëÐnrJsIMcr¿‹ü‚S &öÀ™MkÌuæy;/}«²×øÝ¨•Ÿw¯äÆÑ×3"mx³ïmw£ô—µÖZ¿eÍámF½Å·šïhûËò|«÷Ž>ÿ'Ëhò¾¯D›õ»+0›ò¾ÉŽM»°Zl~í¶êÇõdôKgôÄÜzïõ¼òø[Üzï,BÃCXûÓV.^×tìÀ#}Ù¬ÞþG]4Œkg_À¢Ï–ñù» °YŽÌÍhj>ݪB!Ä™,­GoæîÅí>u«û=•[Ö·Ú&/g]’ÓP…¨è8 ó÷Ÿ¢ÙþŒF#6[ÛŸLç&¹ND[Èurn{ñÅ;éÞÝ›a¨qÝÖçž»Uõ¿W4þûlÙÒòM÷ÓAXX/¼pII1lÚ”ÍÞ½…„„2zt_ž|òVŽc†zôè³ÏÞNXX‹ öPYYGrr C†ôà’KF‘œǃ¾ÙduÕ† {¨®®'<<˜ñãµ 7n kÖìÄbiúuXSc&;ÛuNZZ‚/ƒT[…‡ñÔS·Ñ³gWòóËØµëÀáþéÞ=‘‹.BRR,sæ¼Ül'sNÚÓìÙÓ¸þzï=Ýýû‹Ø¾}?V«  #Ý»w¡W¯$®¼r ]»F3oÞÿ5Û‡¢(Üÿ5L›æÍ„¶oß!vîÌÃ`Ð3|x]ºDóôÓ·ñØcï±~ýî&Ç—–V³mÛ‘E"zƒ¼……å”—×úµÝ±£ã~mÏkídÏIÃ1Z­†ÈÈPRR∋‹$ @Ïüù7Ò½{"v»“ÂÂr‚ƒ½«8-–¦AúŽÇúõ­ÿý!:F§Þ–,Y‚ªªÜsÏ=¾à[III»ô½§h/u¶:BŒ!ü}Ö3,Û¹œM¶]´·CV·5Çãñ°¿<¯Ù}•Þ1!1M‚k›ò6så°ËHIᥞgYÖÏl=°å[ 5¶pë"þ8ù÷\ÔwŸ¬ÿܯïÞ‰½è‘ˆÃåà§Ë›Û™ç-5ÆûÔH­µŽøðøfÛ4ÓºD&¢Óêpu“¤Örä‰ã†÷]ߨž[=Ò^£Ñ´TS=j›Wê/½Áû%w¬ô®Ãõ<Ú7çƒW?'¥G2†÷áþ¿Í¡÷  óŸ~ÖüØžìq;[(ºÕÑô­ÎS!„âL›Ð•à0öï;±4“Éb>òûk€±i:ŸsUpp0ááaÔ×›©®nù©tqn“ëD´…\'B£Ñ Õjüê¶j4ÅÿÞ[IIÕ)Û‰¸í¶i$%Åàv«<ñÄYµj‡oß«¯~ßþ4Ó€h‰Á ãÑGCXXYYxôÑ÷¨>œ© `àÀtžzê· ˜Æµ×Žåãýï+ºÝ*Ë—oãŠ+FsÁýy啯š DÅÇG’‘á „-YÒ|=ÜÕ«w²zõN¿m}ôââ"šmß’ûᅥž=»âv«¼ôÒ|ÿý¯~ûSRâøío§òá‡Ëš=þdÏI{ fæÌqüðÞþÓ&mRRâ¸á†‹øÏ–´ØÏ´iÙ6m8ªªò Ÿ±xñFß¾€==t^8€¼–›nz®I jÙ²-,[v¤.ò’%Ï¢ÑhøòËU|ýõê“|—mמ×ÚÉž€·ßþ€‰‡0oÞ,ââ¹îºqôê•Ìòå[yçETVÖñÄ·jÂfs´Çih³%KZ¾&DÇê´ÀÀ²eËðx<ÜsÏ=<ùä“Ü~ûííÒ¯Íiã©oŸãþi$:$šË†^ÂeC/Ááruh7¿æüÊÊ=«±:Ž?'p[ÕÛ뛆4uEÁ¤7Rß(ȳ§(›7~z‹ß޽™ÄˆDn}=7޾žZkÛó·³:{öol1¿ìêì5Ü4úz"‚"Ù}«³×úöMêw¿ìYí7fƒÎ:oz­£Þ»²êšWr͈+[m¯Q4ùÒJ6°:›~ók©¦\gq^z®ks8œØÓU ‡^öV¾;ì^ê]}ù~zÊð½vØ›?ÆÙè馆ñÝN·oŒ†åÖ†FÁ¶ÖÆB!„8¥õðf(-*À\_{ŒÖ§ž^oð}ì°ËjŒ:NGp°·žŽÜ,Í‘ëD´…\'âž{^ó}ܳgW^{í<ðÀ›de5-ír:‹‰ gêTïjo¾Yãt°Û<ýôÇôê•D||d‹ý\vÙhºt‰ÆjµóØcïû˜¶mËáý÷äŽ;fpÕUðÙg+›ÔŽ[ºtW\1š°°  êΦMÙ­!X]]φ {Nè=·Åˆ½8ÿü¾¼õÖ÷M‚nyy%<öØû-öÑç¤=tëë 7|5–—WÂßþöQ‹}ètZn¿ýb>ûl¥_€ ¼×ÉsÏ}Jÿþ©DD„0eÊP¾úêÔÓŽW{\kuN’’bHNŽaÅŠ­¼øâçMîá7¼g§N ¼¾Â¢Fcó)õNTvñ^æþç^†¥aDúp% ÔÊ ä JÀµç]Í‹‹^fgaÇ<åÚRÐ Ž 5SÅuéΟø5w£ºdxÚPútéM¨)„1£“1šœÒ\ž]øõ•MŽu«nmÿ‘ëGÍdJÿI¾À[P@£ºŸÀ¢í‹[œ[gœ·ÆOí,È¢¸æØ+Õc¶<]Yê½AKS EQðx<¼þÔ»MÚ…nßzMSÉ·Ô^§×BQ~óçÏܨ¯ ÃEH—~»’¥ß®lÞ¼÷¸û6,ƒÐPïý¾o¿]Ól«ÕÎÊ•™\vÙùœw^ïÓ:ðÖ×ZG£Ñ@UU=¯¿î_ãNUUTUm¶¤8;ujàmܸqÜwß}˜ÍfæÏŸßîý;ÝNÖî[ÏÚ}ëQPHŽNbXêP¦œBx`8ó.ùw½ÿG_ ö hôjKŒú–‰&Ñ”1-­«µÖ±8s ‹3— Óêè™Á¨ôó˜Øoé±i<0ý^æ}úh³Çþ˜¹”«‡_Aß.}èÙ…‚ÊBÆöºƒÎÀî¢=ì/ËkuîuÞZâp9pºèµz~É^Í’Í/ñ>–xsÿÆDPQÚü™Ø„hoû‚Òû ä·nF«Ó²í× Ñ—;þ|Ï}ŽÚ£žÂ°šmÔV×Bt|4Ù;r›éb½7{'¥§J!„Bˆ¶jXíf®¯£´èHí à†‡Å\ßæÀ[ŸÃ k²½¬¸MëW`?ÁÚAÉ©TU”ú¥<yðþ]ÔÖ‡0m66[F£Qn–ŸC¼ÙQä:­‘ëDœ«úöí€Ãá"'§è„ú 2úê µ¶â¯¦ÆLEE-QQ¡¤¥%4dZ¶l3·Ü2…1cúñÒK_ú­KLŒ¢Gom½–Rÿµ—aÃz°m[®/ÃÓñhÏsr²òòJؽ;ßWÇ­gÏ®,\¸žµk³¨«kÛóƒw ¢¢–ââ–ïõåæz¯¡´´„“Ÿx;Ùk­#ÏÉ‚k›»çÏß|ç†N ¼5Ý, <ò¹¹ÍÚ‹Êr ü Kv,ãåß¼@¨)”¡)ƒY¾k…¯]ãÕh:MóE;ãBãŽ9žÉ`"ÔÚlp*.Ì{|E}E‹uÆs¹]ì,ÈbgAk÷­ã‰«æ“߃ØÐJk›>[g«cÅî_˜Ôï"&÷›È;+ßgb_oÎEÛZ^íÖœ¶ž·“u°"ŸôØ4’£’Ú­ÏÓQIaæz AÁtïÒbà-£:¹{Zþá>û¾ë‰Š`ãªm¼þÔ»Üõðo:z·?x/>òz³éH÷ï9ÀÀóúѽw k–6]fÑÏ;v^öÁSš !„Bœit:=É)=ÈËÙí÷{޾ ÖÍ`4Rp0K}Jdt1q‰ÄÄwaô¸i¬X²w+Y0š“˜”â[ñ¶gçÖãžÓ™Æ¥ùöî;¼­ê~üøûj[²¼myoÇIìØÙƒ,2 2Ʋ)(#”Uà×~ h ´%@ii¡ìFH BöNœÄqÏxo[ËÒÕïÅJyÈ+ƒœ×óäI¬{ϹGG'²t>÷œ{B$:ºçïWÇëØ&Î=y.¶ìù9 Àjí}Ê1NÎbœgªÐP#õõÍ}žÃ òü{òä¬n Z­;=‰ñ˜Ý’޵|¹;b4ê=:ƒ÷yŽMŸž@II5ùùe–jµŠà`w@½¼¼¶Ou dŸ „ÇÿO÷DHEžzd§ ‡Í‹Ð5H¸\€ b†…ÑTÙF}©ûÆ¿”qÑ4U¶aÊ¡hs•çç$Œà@e×ãJ…’ìø,’#Y²í«Nó“í=¼§ìD©P¢Vª}ŽwXºýkfgŸÃô¡SYW°¤ˆDͬ+ØÐe™þôÛ±\GÚ­Vù?±¥hiQ©LJŸÀÿÖ¾‹Ýá›[,ÔNLH4ÛKvØj»ÎćÅqÏœ_î€à?–¿2`u˲Ìöõ»™ç¤ I$1Õ= ±ù‡Î'\æ\|6#'fSWÝÀ›/½À_zŸ”!‰ 5„ ®žËï|ãSnËÚäNÈf씑|ð¯ÅØŽ[Þ¬Õi˜8c ›º¸¶ ‚ Âé(1%•ZÓé ¤Ð{;U/oß.ù›Õâ3Á•˜2„Qã§` %-3‹ü<ÿ¿tëô䌞@yé!ª*oBêçB¥Rápôþ.váÌ"Ɖà1N„Ó•Áà^åi·÷}üP?~¨_e:Òž¯²²ž={ŠÈÊJfÆŒ\¯`HÇÖƒ½Ú­cû@«Õw~ÑÙ'¡¾¾…{ï]ÄèÑÌž=†ñã3 $''•œœTn¹å<Þ|s9|°ªÓòÆ#)g‚‚ô\xáY=^O¥R dóEÇÚ`öI}½ïNd'‹1JOK…Œ)q´[žWúäXꊚ‰Í ÇÙΊR}ô9*5 ”jJµ’ÄQQFàlw²ý ÷⩸H™ 8ÛXšíÔ—¶`Ê¥®¨$Pª$æF"Ë.”*íVûW»¿ãhÕ(U TZ%ÍÕf¿¯›4&ŠúÒGFyÚ-I¸Û¯R4:ʯëwÔ  ÒP|¨çÔW½uÂoÇÝèý¾´=™•=“9Ù³X3”¾ù.¼¿džƒfJ`…÷õÍM4š›Ñ3sØÙì(ÙéùB-I¿œr-*eÏÝær¹¸|ü¥l+ÚN‹õè1¹‰#È9¸[½ïG¯2²,sóôˆ Åæ°óÍÎo}ê2ä,” %Í–æns¡•Õ—±£d'¹‰9üzÖ¯øv÷wÝ®°ëO¿«ÉÒL€&€q)cøaÿ^’$¡Vª}kËw­àü‘ó5„rÛŒ›Y´â5¯ÀcTP¿™{1!Ѽ»î>ÙôY—×ï/­JKZ”ûyÆò߯?úŽI3Ç’5*“ó.;‡¯?>ºµfhx07?p ÛÖí¢¬Èw›€ÔÌ$.»ñdYæŸÏ½…¹Í,6·šyýùÿñÛgïâWÏåÀîƒìÝáý:mXµ…_\=—ˆèpn¾ÿþõüÿ°ÛÛwޏ›î»cp uMüøm×AZAA„ÓMJÆ0ÊK ±Û½o>R©»¾©ìxVK×[ú”Ê')5ƒ°ñIi~Þ %ã'ÏD«  ­¥™›NÝœI%»¿øWVú?QèÙ®²²JL”Ÿêêê÷ëí/1NÎ ˆxëºY­Vž|òIòó}„Ï·|ÁøÔqL2‰¸°Xv–ì¢ÙÒŒF­%6$šq©cѪ´|»{Eµ¾oœßíùžKÇ]ì)¿ïÈ*³‘I¹j ¼¿înš~’$ù”UYßXZ_FC[/^ûgÖæÿD]k=q¡±L:€%;ÙXèôÔ…‹·×¾ËÂy÷qËô—2†ƒÕ…Xì Z)Q)ä&ŒÀårñÆš7{ܦr鶯ÈMÌ!&$§ìdù®ƒÚo6läÂ1pVÆDÒMi”5”£VªÒÍx‹eǵ¥¾­—W¼ÊýçÞÃŒaÓI7¥±©p3vG; añŒO‹Z©fïá},Ùöe·ÏãTWQZů/æªÛ/æ²›.`òìñ”`Ð1lä45µ•u¼õ÷}Êê Ü~$¯Ûâ·¿¦ ï×ñüÝYúþ·\pÕ\n{èzþ߯Ÿ§©áèê@‡ÃÉ?Ÿÿ Ÿù5c&ç0$ûIöï,À…‹Ìé…q´;xíÙ·°ÛúvW ‚ ©ÆO Ñ½Lá½>ÇÕ½¼õ¤ºò0a&ŒA!(•JœÎî?³K’Ę‰Ó Ân·±áÇ´·ŸŸÃ$Üþnívü$¹ØîÌ`³¹'•Å8º#Ɖp¦jll ,,I’úZ™Ç IDAT´Ýdm­{›JI’¨«kfýzßÏJ½±jÕîºëB ãÆe²n]žgë¿; ©®Ü|Šv»ƒ– Fc±±á}ªc ûd ¹\.öî-aïÞ>úh /¿|7&S(W^9ƒ?\í³ ¦ÆÝç:æ”{.ýÑŸ±ösí“ãÕ•¸oõ —î¨9pr!)$d§÷{Gå~ï¼w’’âh,¤h“ûF—ò]µ^å;ê=v!SgïK×wž“²§ëæ¯q߸·ûëC¶[¡”üº~G=’BÂ%Nª¥xëºÙl6ž|òIöíÛ×s¡>ªjªæw?ÉÓ~ÉÈÄ’#’|Ž/Ù¶”ov.ï´ü‡?A¯Õ3'{ÉIžò{ïãé%ÏêÞP£ô½kBsd{E‹ÝÂs_þ•›§ÿ’9#fy¶…tÊNVî]Í¿Wÿ·Óko,ÜÌÓKžãšIW22)—‘I¹^Çó+ðÞºÙYº«Ç~ØV¼ƒÃĆİáàFêÛºN ýï·ï­ÿ½VÏÙæIT;O…SvRZ_ÖåJ½õxü“ßsÍYW2O|òu˜‚¢PH êÚêihmðÙú²Ã ÿ¼Õç±w×}À»ë>ðz¬®µ¾Ë×íX« ý:¯¿¶®ÝÉÖµ; ‹ %$<»ÕFuE]·+Íþñÿî±^Y–yæ—º=§¬¨‚??ò2A"LaÔVÕÓÚìÿøAA8]¨Žä'–$‘¦Ø.ÏS(•žã}º(•G·Ml·w¿rmĨ‰$§eât8X·æ[êjútÍ3Jåþê*&É…îˆq"øCŒa ØŽ™»‰ˆ¢¤Ä{,I’ļyã½Û¶xþ=wî^}u©Ï9YYÉ$%™º­çûï·1lX"S¦d‘žKAÁá~µkÅŠ-LžœÅ¤IY¤¤Äž‹Ýî`õêž „ï¿ßδi9z,˜Æ»ï~ßéyÝ­è>éðð êêš»<pt‹À¶6ß÷µmÛ hll%$$믟ÃïÿV¿ÛÔÒb&$$ÈÈ~×Õ}kƒÑ'§¢PCZ•†M%u¥žÇ“““ÉËËC•šŒ¹êQ£ÎÅ%;)ÿá=b'/pŸ7÷œv Öúr@ÂZW†C­%"w ûÖ3ñœv ÿ€˜I—")T¨õÁîrue(5žëFæÎ$ Ñi8íìÍ5´”î!tÈDšmG’8ÔZ"G‹Kv PiqX[Iv"//É×ÝÀÜ»þv‹™ú²B$êJ QëÈ{)û~ü–‰ nÁn1³íÝIKKóÞn¾ùfÌf3z½³ÙÌ{ï½7h}?è™ÇŽËý÷ßÝnç÷¿ÿ=yyyƒ}I/v‡’ºRöUì'¿ò€ßÁ#[»ƒÕ…¨,ðÝúÂb·p¨¦ˆU½ œµÚÚ(¬>ÄÞÃû8X]Ø«²3‡ŸM°>˜¢ÚbòÊ{ììO¿uhw¶SV_Fq] µ-u=ÝŽÕjmu÷}Uõ­õ]Ý~êk(ÜWDYQÅ ßÞ±µ¹¢¥(A7AA~¶vmÛÀªeŸwù§0ßýÅj1{+/)ìÓµ‚BÜ75Ù¬œÝ|þÍ5Ô!Ãq8ÚY·fu5?Ï»lJcc#eeåb’\è–'‚?Ä8±«cæÏŸàu,22˜gž¹™‰‡ z;<Ì–-îT>_<… †z7.“gž¹•ªû|ª_|±Žââ* Ïu8N-ZÀ´i#xðÁË}ꊉ gáÂ,\x¹_íÚ±ÃýYyîܱ>[zšL¡', ××±6}r*Š4F24&“ú¶z*›Ž¾o™Íf ö–Z,õå(uÔa^e%…Švs3Ö†Ã8mfìÍ5hBL“F ;ÚÑGzŽ{¸\(TÏãæÚ4Á‘žÃõtwÉ2›i«:„!&ÃS?¸p98ÌÍÈí6O{*5æ¦Ê‹°™[i®® ÄORÎxvÁQ±žãƒ––ÏõKKK)//÷ü=˜¤èèèAh ~÷»ßñî»ï²k׉¹«A€„ðxž^ðè5<»ôÏl,Ü|²›$‚ ‚ œbÂÃÝ_’[[}·¸>“d ËaxÎXÌm­,_ê›g·ƒR©"(8”†úÎW¦iuÌš*•šÂüûì­Œ;poùXTTEx¸‘ÜÜ4$Iâµ×–rç¿`Á‚?P_ßÒ]u}ÉK/ÝIHH '÷Wuu#ññ¤§Ç‘—WŒÕjgôè –.]Ï /|Òi=‰‰QüéO·`2…PQQGii Z­†ôôX _½‘?ÿù£ÛõÀ—1oÞÑ äc½áW.­ë®›Í…Nòz,$$I’°Ùì^¥‚‚Ã<üð¿:­',ÌÈSOÝÄ!îT66›¢"wú›øøHÏó9p œ;îx©Ó•oÝ'½¥T*xöÙ[5*pïrUPp˜ººf´$$D@cc+>øO »Þnô¶ÛæsÅgî\xû÷—b±ØˆŠ %)) I’°ÛÜrË_(/¯í¶mII&-ºNƒÃᤠà0--f"#CHJŠbóæü._›Ö×±ýœœœT4„†±ÛÛ)/¯Åáq:<úè¿©«sç ”eY–½þ4 }Ð[jµšÄÄD<€¤Pâ’DO¸d™ª-_â’ IîcÜP()U¸d'’B‰iìù Ë8Û­(Ô:ª6/ñ*×Qoå¦/Žö®WRª@–q¹Ï!IŠ#ÿ']^í=TT„ìt"I J%NG»O»”*5²Ó‰B©$=-•üüüó_VÕ6`ŠíS_/úÁ"àl5ÙÖÖÆ#<2Ø—€¸ÐX†Ç #.4–ÙٳЩµüt`½º ‚ ‚ Bwzø"îí*'N›Chx$û÷l§0Žc¾èŒ4•JM{»ƒù»;­'wìd’Ó2Ý–ÓÒÔèÙ Ó·Y®nWÍ ‚ ‚pò<óÌ{<öØÕŒÁ!ñ ËåbçÎC¼úê sÇ z¸²².|»îºˆ‘#Ó><‰áÓp8œ|ýõF-ZÂÍ7ŸËèÑÝÖSRRÍ]wý_þr³g&&&œ˜˜£+™ªàÓOäë¯7ùÕ®åË·z‚!­lڴ߯rƒ–ÐPc§Ç´Z Z­Æósp°¡Ëzêë[¸÷ÞE\}õLæÏŸ@h¨‘ÌÌÏñšš&–.]Ï'ŸüÐå¤ü@÷Io92>øOfÌÉ¥—N%33ÞHì`6[ùþûí¼ýö ŸÜnÇûç?¿$?¿Œ«®šIzz,#F¤xŽY­vV­ÚÁ»ï~ßcÐ  ¸¸Š… _ã¾û.%--–¡Cömk«…²²ZT*%‡³—Ϻ÷ú:Ö ÿ}ä³â/:: §SÆáp Tú†ƒ}ÒÞÞÎÁƒ‰9Ç+Ç[å†ÏÞçx;Z΄¤P¡1†{åxë8Þ×oöæŸöºœN”z%J­…RÓn@n·yäºK ¹¹¹=憫ªíü†Éþôo‰3mèT~3çמŸ×æ¯ãå¯bsø.1AA±âÍ-}è²rÇu»âM¡P0bô$’ÓÜÛ É²“ÆúZì6Ú€BB#$ §ÃÁ¦u+©:\êSG 1˜sæ]êw»š›XùÍg}{R§±âMð‡XÉ$øCŒádŠ '11Šöv'ÅÅUÔÖvüL&S(qq€{ʦ¦¾¥Ñé4ÄÇGŒÝÞNqqu·9ÆNu*•’ØØp¢¢B$‰ÊÊzJK{·ãÀ©Ð'ÁÁ"1õ8Njk›(*ªêq5OgL¦P¢£C ÐÒÐÐÂÁƒ}’ÅÅEB¡ ¦¦‰¢¢ÊphÑ'¯Å±wü9•V¼uˆöæô¦¯o)óï@­ñÍñfm%8mt§9Þ’Ï»Ó'Ç[[EÝ ƒâ¦^E9Þ¬­„dŒ÷ÊñV¶òM¯ö“Fà’¸í´”î!zÂÅØ«i77¢5F`­/§­¢€œ8=yyyL8’Nî“ÎÚÚ̺o¾8ýV¼ 'ήÒ]¼´ì IV¢¬¾ìd7IAA~dYfÇæµT”‘™=аð(Â"L^Ç+—°wçZš»ÈÝËÞÛZNß .AA8S>\ÇáÃu'»TU5PUÕÐïz¬V;‡)(8Üóɧ‡ÃIII5%%Õ}®ãT蓦¦¶>S7Pc ¼¼Ö¯Ur§ºì“ÓEGŽ7cR6’Â;TÔ‘ãÍÞRƒÓfÆi3£ 1 MñÊñfo9&ˆ}LŽ7{KM§9Þœ63š Hì-5H’’Æ‚Íh‚Mb2pX[ ЦàÉñf·!;î2rl{]N‡'G\äȹÈ+Ú H\.Ùs]³¹Æ'7\KM6s+¶¶VBLñhSY÷ÍÞ·bÅ› ‚ ‚ œ¡ÄŠ·¾Óê4£Vk°Ù,´¶4Ón;Mô†Xñ&øC¬dü!Ɖ ‚p*8ÝV¼u8Õs¼_þøãÇ?þæ†;-s¼ ‚ ‚ ‚ üÜØ¬lVËÉn† ‚ §¡n˜ËùçOèu¹‚‚Ã<üð¿¡EÂ@Òëu¼õÖoûTöé§ßcëÖÜ¢“OôÉ©­#XÕ‘‹­C¦)³ÝLjd Æ>ÙìÞ?11‘°°0Jk›h·Y1®G«7¢3ckj!("€æš wýE먪ª"(2­ÞˆÍÜÒi®5kk’¤ (*Öó³ÓáÀ^¶Ç179æææÐØØHQQ‘W{/¼f²,³v…;ïâÔ9e™g A–e¾xgN‡Ìu×]ÇöíÛ9r$Û·o§jåšïSxAAAAA8A ¡¡Æ^— 6 Bk„¦PH}z}4šŸçt½è“ÓS¨!­JC€&€’º£9¬“““ÉËËcâu÷ã°[±[Í”îÚŒFo@­ sòl$Ôå{p:(•JfÍšÅa] ¥»63jþ•>¹ÖÔºÆ^x‡¶®ER(Pë5ïJdg;*kk+ßø N§—ËÅÍ7ߌÙlæ½÷ܹéB#Bp:Œž4‚v{»çg¥J…ÓqtÕ^LL ÕÕÕž¿ƒµ‚ ‚ ‚ ‚ ‚ 'ÈǯaåÊí½.g±ˆm­Of³»ïþGŸÊö'Ý©LôÉé)ÒI@˜Ž¢ºb*›*=›Íf *‡ÝŠìtR°qg]q;ÚÔ@Úë°µµR•—‡,ËLš4‰Õ«W“6kW‘9en§¹Öªî%fȬ-MhSÎövìV N»Ýsý¶¶6¬V+¥¥¥X­VÏãõMXÍ6 F=‡Ãó³N¯Åj>úþ)Ë23fÌ ¶¶–3fðÖ;ïx߉o‚ ‚ ‚p†9Þ„“Iäxü!rw þãDA8œ®9Þz«#wZ؈ÉlYò²ÓÙs¡ct•kM©R»ûèÈÖ—’BéÎûæò/„%Ip´ÿ;~îÐñ¸J¥Âápxþ9ÞAAAAAA~æϺ÷d7¡KU@U5è'ÜÝå9*] H –f'÷ïù´®}¥!I¡@¶·£P«HMÀ\PŒ¤PàrÊ ¤Ráhvß`ªÓép:(4º#5¹ÀH Rk©zë®~µ«3"ð&‚ ‚ ‚ ‚ ‚ ‚ ˜À„aH’‚†ü Z¯!3$.‡[y’JE@j"J½ÙbÅ^Ûà ¼eggc·ÛiB’D`X$Îövl暪hÛ:ˆÀ› ‚ ‚ ‚ ‚ ‚ ‚0p\ )•ƒS¯Jl“Qèmv\²ŒÊh— —ìÂVQã9½¹¹»ÝŽ#@³ÝŽVoÄfnA’Ô— |û7AAAAAAaµ·Ö#)UÆõ;O›_õ66#)•ží%­² €¤pɾ×ÉÏÏ÷ú¹êà^$I,÷.7]oˆÀ› ‚ ‚ ‚ ‚ ‚ Â)¤õ§Ovúeʹã0™Bùê+÷V“55wƼyãY²d²ìB¡P`2…ÒÖf¥¾¾™ÀÀìövdÙE];G\dd0’$á:àÂ)»˜3=—üü2vï>täxÈ‘+»ŽÔ+¡×ë0›­H’DT”»þÖV3‡ŒQ¯Ånw Ês7AAAAAAaÀèõ: +˜7oN§ÌÛo¯àª«fp×]a±Ø(+«E’ ´´NÍܹcøñÇ=,X0 ‹ÅÆŸþô>×\s[·pÎ9£°Xl”–Ö ñ\oîܱH¤§Ça±Ø¨©id×®"Î:k8[· PHètjæÍÓéD£QÓÚja÷ÞCþÜEàMAAAAAA0µµ”•Õ““‚ò¸\o*•’¦¦6jjš0›­´µY1™BIMÁnwâ9ÞáàÁ † ‰÷<^\\…Éê9ÞQOdd55M(•7îÃd eÈxZZ̤¦Æ.ÚÛX­vìöÁÙnRÞAAAAAA„³fÍ.^}u)N§Ì¥—NE–eþö·Ïp:e$IB©Tàpø¿T*%+WnG©TxÊ-Y²Yvár¹¼ÊuÿôÓøöÛ-^Ç¿þz#²ìB>’ N¡Pàr¹ë,"ð&‚ ‚ ‚ ‚ ‚ œr$I">>‚êêFl¶ö“ÝAzáÜãr¼}òÉ@ïs¼u”óäxs¹s¸M?’ãíèñÞåxÓÉñVUÛ0àÏ]ÞAAAAA8£)• rsÓHI‰& @KCC •• ìÜYH{»ãd7¯_æÌ‹ÝÞÎ唗מìæôÊsÏÝÊèÑÔÖ6ñ«_½DCCËÉn’ ~:s¼U>Ÿ ˆÀ› ‚ ‚ ‚ ‚ g°iÓr¸ë® ò9f6ÛX¸ðUöï/; -—]6•´´X–.]Ï /|r²›ã·à`£Gg̈)¬Y³ó$·J‰o‚ ‚ ‚ ‚ ‚ g˜³ÏÎåw¿»I’(/¯eãÆý45µb --–ÈÈ`8|²›yFjjjcÛ¶FJ§®®™]»nUŠ ƒOäxAAA8Ãtä8ÙmP©58Úí'½-‚ ‚p¦Q(Ü~ûùH’Äúõ{ùÝïþãóûX¥Rz&k…ï·¿}øøªªDŽ·SÈ}÷]JLL_½‰•+·ŸìæƒdþüùìØ±ƒ²²þ­øu:ÝØ:¸\®Nƒn€çq‡ÃéSîØãþÔ{ü5NÄ{º¼ ‚ ‚ ‚pÆ1™2s>Å…ûÉÏÛá÷—¯É3æ¡PH=žW°o7åÅÓh´¤É"&> cPÈ‘ LsS#eÅ)<‡ìœ-OAA8***„¨¨–.]ßéM0]M '†,Ë””TŸìfÇ™7o< …‚Ÿ~Ê;ÙMÑþýûÉÊÊêwàíL$o}pј_pÎð³ÙW‘ÏË+^=ÙÍÁ?Æà@>{ëK6ÿ¸ãd7GA„“,5c8º=ÑqIìÛ½Íïrá‘&$©çÀ[»}K§Ç&¤0jüT*56«‡£½Á@pHÁ!a$¥á§Uß`1·ùÝ.AAz/(HïùwS“ø½+‚p,½^OQQÑÉnÆiIÞú 8 ˆØÐXj[ëOvS„SLfÌ4J5»Êöœì¦ôH’$’3ˆ0…£PHÔÕ4P¸¿Ù98Kmõ†ô~Ÿ_WÝàu§Y„)Ìï²³•¶³×c¦¸H‚BŒè þ·¡3J•’ÔÌ$B#ÜwÄ5Ö5Q¸¿G»£Wõèõè :¬­Íþ¸7ÅF›.@Ksc+÷Âj±õêÚ‚ ‚p¦S©Ô$¤dpèÀÞ>Õ±sË:jª»Î÷bnmñyL¡P0dx.*•š²âƒìÏÛNks“çXRÚP†çŒ!ÐÌ„©³Yµìó>µMAÿøq/M§t: !!ªª;])§Õª  ººé„lm––Ë!q(•JÊËkÙµëPŸVì%%™ÈÌŒG§ÓÐÐÐÊÎ…Ý& F£{¾¥«þè Ñ¨ 3PW×Bûqó)Z‚ƒõ¥­ÍJK‹¥·OƒAGvv2‘‘!¸\.jj𨳧ˆ¶6«ßuô¶OƒR© 33„„Ht: mmV®cÿþRÏV~þÐé4ä䤊ÍÖNII5ûö•vûºûËhÔêõ˜Åb´¾ȱv¬¾ô @h¨­öhˆ¥¹Ùì5®"II‰F©TP[ÛÄþý¥´7gy*3™Llذád7ã´4è7µZÍÝwßͧŸ~*¢£ÂÏÞݳïÄátpï; OvSº5nê(®¸í"Býomnã“ÿ.eÍ7ëüš³/šÎ/®9×ïósåc^©gÿó„ße¿_ú#ï,ú¸Wíë‰J­bþ³™}Ñtô:¯c6«U_­eñÛ_c³Ú{¬+0ÈÀã/ÞODt8?,[Ï_z¿Ç2‰iñ\×R2“¼w´;XùåZ>ýïRìv±×¹ ‚ ø#!%µZC{»²âƒ}ªÃj1{‚fþ’e™u«—iŠõ¹®,Ë:‡ÍjaÜY3 ÃOU…ØÖEAN5S¦dóÈ#Wpþù¿ÃÒÉ ±cÇfò‡?ü€ þ@}½ïM9%""˜Ç»šœœT¯Çkjšøë_ýŸIHˆdáÂdg§x=.Ë2_~¹W^YÒižµ#Rxê©›xàWÙ¾½ëÏW_<…Ûn›ÝîàÒKï ™1#—XÐiÙ>ZÍ«¯.õûùhµjn¿ý|æÍZí= îp8Y½z¯¿þ555]ÖÑ×>h^x×_?›@Ÿcf³•o¾ÙÌ{ï}ßã8»òÊ\sÍ9èõZ¯ÇËËkyñÅOغµ Ór¿øÅ$n¹ežÏã7Ü0‡n˜ãõ؇®æµ×üzc ÇZ‡¾ö ÀC]Á¸q™žŸß{ï{^ý+¸ï¾KÈÎNÁf³c6Û0›­¼ÿþJÖ¬ÙÙ›§Ü/ýÍѶ|ùònÑ™cÐoãÇgæÌ™Œ;–Gy„ââÎóÂéΠ5l¢¬¾üd7¥[SæLàÆ{Ýí/f϶ýÈN™Œì4†åfðË{® À cÙ'+ôºÕµìÞ²¯Ûs ¤d&átÊ8»#«§²)C1õج»L’$î|ôr'dc··óýÒ)>à¾ë%1-žiçNdî%3HL‹ç¯½Òí]lY©ÜxßUDD‡û}ýÄÔ8zîntZêªëÙ²v'm-mÄ&Æ0fJ.³/šNdt8ÿø¿÷x'Ž ‚ š1 €’Â8½[µÞ_6«¥Û`_EYN§¥REph¸¼C§ÓÃ9fù IDATaµúgºpfãDð‡'g¶¿þõW¤§Çxåm}î¹[‘eïïÔO>ù&Û¶u=é~*6ðç?ßNBB$[¶äsà@9F£žÉ“³øãoôëFÝŒŒ8ž}öV‚ƒ ˜Í66mÚO}} ‰‰‘ŒÁL"1Ñă¾æ³ºjÓ¦ý46¶ÈŒ#» †œ}v.?ý´³Ù÷ÿaSSùùÞŸRScP©”=>‡c…„xúé[ÈÌŒ ´´†½{‹ÔKzz,çœ3š„„(î¸ã¥NëèOŸ ¤›o>«¯ž À¡CìÜy‹Å†Á #==Ž¡C¸ä’)ÄÇGðÈ#ÿî´I’xàœwÞ8 ³gOšqã†Á3ÏÜÂOü— |çફÙ±£Ðósn®;È[^^Kmm³×¹»wçÝ™ký퓎2J¥‚°° ’“M˜Lahµjž|ò:ÒÓc±ÙÚ)/¯%0нŠÓl>±»V‰m'Ï ÞÖ®]Ë믿έ·ÞÊSO=Å£>JIIÉ`_VN¸´¨T¿ò}œLA!\}û%|ùÁr>}óK¯ãs/™Áå·\È%¿<Ÿ-?î ¶jà¶S]÷ýfÖ}¿¹Ûsî|ì&R2á«—c9îâ wŸO1)=Çþz/­Ím|ûéÀ s'd‘;!Y–ùË£‹(È;úb튬_µ™Gþ|/Ãr37mVÍéHÚ°¢ã£7uIéñXÌVÊ‹*ˆKŽñëú×Ý}9º-yÛòùûþ…ÝvtU]ú’üÓ¯91›ñÓG{][A_Q1ñÝ«þôm›ÉÁär¹e¥’²%Õé"00`Z[Ûhlìú®táÌ&Ɖà1N…BR©ðšÇQ(H’wà­ªªá„¶­/n¹å<"q:eþð‡ÿñã»=Ç^~y1¿ýížDW4?~-ÁÁòòŠyüñÿÒØØê9ž››ÆÓOßDnn*—_>÷Þóžwq:eV®ÜÁÅOfêÔüíoŸuˆŠŽcÈw lùòÎç/Ö®ÝÃÚµÞ)\Þ}÷QL¦ÐNÏïÊ, 33§SæÅ?᫯6zON6qÓMçòÎ;ßuZ¾¿}2PBB¹âгøæ›M<ÿü‡>ç$'›¸æšsxë­®W'wÞ8Î;o²,óç?IJeGçè´Z5?|%Ó¦åðàƒ—sýõÏùª¾ûnß}w4/òòåÏ¢P(øôÓùüóµý|–þȱÖß>ø×¿¾`Ö¬Ñ<òÈU˜L!\yåÙ šÈÊ•Ûy㯩¯oḠ ¬~ì”5D޶“ç„äx[¼x1.—‹Ûn»§Ÿ~šGy„ÒÒÒA»Þ¥ã.&;>‹¥»ølóâNÏyü¢GQH Þ_ÿ!û+ò}ŽÇ…Ær鸋“‰V­¡®µž7³xëüYO¨ ä‚‘ó“2š¨ H’‚ú¶v”ìä‹­K©i©õ:?>,ž›§ßÀ‡>fïá®W÷$†'pã4÷RõÖľŠý~´¨gWOº’Œèt~Ì_ËʼÕ\8ú|f ?›¨ Hœ²Lau!‹·.aó¡­]Öͣ擟Ex`’$QßÚÀî²=|¾u ••]–½`ÔXÿõm¾¸æ<±)c3¸?DEòäÅ¿ó9¯«±v¼G.ø-•€?.~§Üû½¸»2eÎD´Zª+jùì­¯|Ž/ût%gŒ!1-ž³çOæã7–xŽM?ï,ÆM‰¹Í¿ž»Ó»¥45·x-J•’w^ù„æc>Ï…{«Ç_\=—‘GŒ¥ÍÂÞù|öÖWT•×ø”–;€ÒÂÃ^A·‡ö—PRPJJfÃr3¼‚_#Æç¦û¯v_Ûåb÷–½¼ÿÏϹôÆóý ¼¥M&53 —ËÅ›ÿÀ+èP°÷+¿\Ë싦3ûÂi"ð&‚ =HÍ@uEm­Í=œ}âŒA¨ÕîÏ‚µÕ'¹5§•J…J¥"0ÐOGL– ãDð‡'½÷.òü;33žE‹~ÀÂ…¯‘—wzíÔ¹çºWë,^ü“WÐ Àfkç™gÞcèТ£Ãº¬ç 'Åbã‰'Þô 0ìØq7ßü–Ûo?ŸK/ÊG­ñÉ·bÅ.¾x2ÁÁFŽLgËß9°Ž`cc+›6 Ì|fgÆÊYgeðúë_ùÝŠŠªxâ‰7»¬c úd $%EyÅǾŽUTTÅSO½Ûe*•’[oÀG­ñ 0{œ<÷܇Œ‘Bh¨‘¹sÇðÙg'.˜Ö[1Ö«O"ILŒdõêíüõ¯ûìLÕYðn0‰m'âD]è‹/¾à•W^!88˜§Ÿ~šøøøA»VRx"9 Ù$†'tyNNB69 ÙûˈNçù«žaúЩ„†RÓR‹J¡âЉ—ñ—=‰Bê¾ÛR"“yéÚ¿pÙøKˆ‹£´¾Œ‚êƒ냙—{./^ûrs¼Ê”7”l"'!›óræv[ÿœ³ÈIÈ&52™ƒÕ…ÝžÛq¡1ä$d“•Ê=s~͵“¯FÕ.†Ç ã‘ ~ˬ¬™–ŸÅ_¯~޹#fNEc%•DÙ}¹êO¤E¥vZ¶CNB6Ó‡Mà¾sïᆩב™ŒN­C¯Õl"<Ð÷ƒ‚„Äí3oáw>ÂÄ´ñ8œNöÞG“¹‰Ü„üfίyø‚Q+Õ>eU %9 ÙŒIErDÏ]ù4Ó‡NÅl7c±[ˆ0†3+k&O-ø=ß$¢ñaqä$dæÞ¦@§ÖzÆ×±:kÉŽÏò”éi¬õVÎ8÷DÓ–µ;ºÜ’pÓÛÈŸåõøúU[aÌä\Üra§e/¿åBÆLÎ%,*”ußmò»]A®¼íbÞ^ôQ¯?¨Ì¿b1 &ò¶å³qu×a­VÃc/ÜÇÙó&ÓÚÜFyq£žqSGñø‹÷›ÝI)w_Y-]ÿr´t±T¼¦²–‹WóöËñà/Ï ¿FEi•ßÏ-÷ÈkV”_Bme]§çl\ãþà•<$‘ £ßu ‚ ™Æ` ÂãþRxÌj7•JM\b*ѱ‰½ª/@o 6!…Ô!Y$¥fÒõ„–?T*5#ÇNàpYõµ=”8s466ÒØØä™, 9ÙMNAbœþãDø9™0a( …{îhÙ²Îça'mmÝOöϘ1€5kvÑÐÐyްŽÀDh¨‘ŒŒ8Ÿãûö•RZZs¤¾ÎWØuC¾ÿ~û nÍ8kÖ(ZZÌ}^5}2ššÚ<ÿž4ixŸê;vAAî­¿øâ§NϱXl¬Y³ € †õé:'Ê@ŒµÁêNƒÙlç•W¼sÜɲŒ,ËæƒLË—/§¹ùÔ»Ùðç,úÁ"¢,:1+Þ:|ùå—ȲÌwÞÉÓO?ÍÃ?ÌáÇOdz$Iwͺ­JKAÕAžþâYš,îÁiŒà¡ó2søô.Ëhxèü…„èƒÙ^¼ƒ—¾ýÍ÷›³Z©ææé70;û8ï7üæíhhsßYår¹X±ç{®žtãÓÆ¨5Ðjkó©_­T3-s ßç­¢Ý9pÉ;e—ûMhlÊh’‚ß¾ÿ¨'°  à¾sïaLò(n˜zn¢Åzô—ŽBRðëÙw QiØYº‹ç¿|³Ý @ˆ>„Ç~ñ©Q)Ü:ã&þÀw5@aMà^Ñ7mèTÎʘH~åo]Jqm1*¥Š€`̾IìçåÎeNö,ÚíüíÛ—ùéÀzϱŒèt½à·ŒMÍðöZï;@:ž·$IÜwî=l*ÜÂkÞÄb·pNÖ î<çv¢‚¢˜•5“%Û¼·güÏš·x{í{œ—3—«&]NY}9~ô„O­í'ÿøø”XŠt½¯oÑ÷V°ÑñQ¨Ô*GÚ,6^yú?<öâýÌ< »6å±sSž§\ÎøáÌ8 íöv^yú¿X{ñ‹dÁÍ¿À`Ô³~åòw÷.˜lŠä¼ËÎÁé”y÷ÕOz<ÿÜËfRWÝÀŸþ¦÷ÿí˜÷ÿñÂ"C¸öÎËxî¡x•Ù¿ë ³.œNbj£ž¶³×q­NCbšûVþïöçï.ìõs:VBªû5+*èú5+)p盓$‰ø”ò¶ ^ÂfAA8u¬vkkm¡ú˜Üi†@#c'¹­•ÊÃþm‹?ºrdPMæ&$$>Xÿ±'èÐhn䣟ðÐù É0¥äÕ¯ªšªwpñ†)×òÓõ¼ðÍß<1€R|ƒ*¥ŠËÆ»ó–}¾e‰WÐ à@eÿZýî?÷7ÌËË'›>óÕÜÏûèÊ/§ìdÑw¯y=öÝž•œ=tÃ㆑?Ü'ðfwر;ì´;ížúÚ: šžlAôî/!u5î_(A!FÂ"Ýw÷5Õ7ÓP×D]µû˜B¡ ÂFeYµ§Ž²¢ ÞYô17Þ{7ÞwOÞù,Í­…¹ñÞ«x÷•O(-,÷»] ©qLž5§ÃÉgo}Ùsã\vÓ¨Ô*Ö|³Î¯•dÆà@žºïOÐ  ¢´Šÿµ˜_=òK2G¤NÍ1«Ë¶­ÛEÞ¶|†Â]ßÌ;‹>¦¬Èý¾ÎU¿º„À ÷±¾‡ïulíØ޲’BÌ­-¸\2a&"M±DFÇ1ùìóX½| NgÏ=c&MG¥rïÊ Ë2‡ä±w÷6¿Êžî ÷÷®èhS¯Ëv¬TqOžŸüÜ„Á€ÕjéáL_bœœ9Ä8ÎT¡¡î]oêë›»ÜÙ¨'ááAžOžœÕm€A«uf1õ_¾Ü 1õŒÁÆGSéLŸîÞý«¤¤šüü®o.î/µZEp°; ^^޷ݲOÂãÿ‡'ž¸Ž#RÈÊJ&++Y–Ù½»ˆ~ØÅòå[hiéúý/"Â}³˜Z­âž{.îò¼¸8÷Ô`>—Òß±6˜}²jÕ¿Ï~¾Nxà `Ù²eȲÌ=÷Üã ¾UVvûëDÊŽwo¯WÕTMq­ï] å ‡)ª-&%2¹ÓòÒÆðcþO^A·²KfÕ¾5\7ùjƧŽó ¼5´5²ùÐV&¤cÆð³; ¼ÍÊšÀÎÒ]Tt“/­?Ìv › }ïh²4³¿2Ÿ¬¸á âx«jªæ¡ë²Î²†£+C ¡ÞZ,GûK’,úî5¯ [W†Å%(Àý ñ›Ë:=g}ÁFÌv zM#sXWÐùÞ¶K¶}Ù镚C Fˆ~ð· ¸õßwx’ûäŠFþè[&N:g,—ßìÞ6òë¾ããÿ,ÁzÌ–‰º­O=?~»!ÙiLž5žﻚ¿ý¿×¹ñ¾« 1²îûM¬Y¶Þ§Lw.ùå|$IbͲõÔVÕ÷ªlJf"£ÏÊÁáp²ä½Î_ûãíÝq€†:ßU“;6îÁép¢T)ÉÈJõ ¼¹\.þöû×¹èÚó˜zîD~¿è!ì6;.Ù…6@K[«™‹×ðé›KüΕŽ×ÀzdèØD¿ô…û‹yê¾°˜­‡¡Óû¾f‚ ‚ @bJ*µ§ÓAI¡÷M*ª^Þ¾]âþün³Z|>7&¦ aÔø)ƒCIËÌ"?¯û/ÝJ•Š¢ƒû‘$‰€aQ¤d '1e»¶m ¸pðrŸü¨T*ŽŸ€Rè1Nˆq"œ® ÷\ÝÞ÷ñ{l@aüø¡~•Q©”>^YYÏž=Ede%3cF®W0¤cë¿Á^íÖ±} €ÕjïSÙ'¡¾¾…{ï]ÄèÑÌž=†ñã3 $''•œœTn¹å<Þ|s9|°ªÓòF£{—  =^xV×S©NXvª>ëïXÌ>©¯oíù$ágï¤ÞÀ½¿¨,ËÜ{~à[U•ÿyKlˆû†Òún¶u«+í2ð–™@³¥…èÎrEAó‘ S\X,*¥ Ç1w³.ß½‚ iãH‹J%)"Ñ+øÍðX÷~²Ëv-÷ÿIõRqmq—¯Ã dÅ ÇÜõ]©šÂâ B£Ò T(<1¥¢ó_Dùh?ü°ÿG¯UiÝI=òZ˜ítš¢;ÉÃPÕTEJd2ÉI]ÞÊ:_©Õbq¿aª”ƒÿ_æØÕ‚I­>Úvg{×9ÔŽý²¡ÖøæÄxûåIÎH$gÜpxê†Byq%oýý£^µ)!5ŽœqÃq¹\,ûäû^•˜Ål6¬ÜB}Û‚”vú¸Ýf§®º¨Ø¢b#|Žõƒ={§[ÌV\² NƒZ­",2„S8åÅ»‚W­q¿n=å½ë8ÞÕk&‚ gº” ÷çèò’Bìvï-±UjÿZ-]V+9”ORja&â“Òz ¼9ölßèùY¡P:$‹¬ÜqŒ7™öv;‡KùݶÓJvö®¬ô¢*00г-\ee•˜(?ÔÕ¹oˆ«¬ô¾@Œ“3'™ª#_”FÓ÷ùªc·+|çïºÝZ±Cqqu—Ç–/ßJVV2“'g£R}ŒÃá$..‚ôô8wª[ûÜVû|tºÞïjp|Ñ'eëÖlÝzI’:4©SG0oÞŒÆn»Í}cûûï¯ô)×±®²²ž¿ÿýó¯3˜ù÷RÆÚϵO:“4:ŠŠ}õÈN›{þPgÔ IGvÀsA̰0š*Û¨/u/Œ 2éqÚeœ—ìB©VIñÖjpAê¤h ×Wbmq·5z•§þ¤ÑQîóÎp'-ððÝwßár¹¸÷Þ{ùãÿÈ­·Þz2›@ Öý¡«ÕÚudº«cj¥Ú½ÚdÁøKXpdûî($ZÇä,Û^²“êæ¢‚"9gø ÞXó¦çج¬™H’D}k=› Þ/©¦Nr¨u0ÛÜ¿xôßåµ1!Ñ\?åZÆ$ê2¸æ¯üÊ¿Ï5 êé5¼|ý‹~œoìòXK7¯ûé®Ý~tõœêHÎÙîÄvä=¯5Çnl]Üd·ÙyåéÿðøK0läÏÏv[ïî$šsñÙìØ°Çk…™?¢b"9!€_¬ñ»\S qÌmî_ºzïàm„)ŒGÿr/ÁaA¬]¾‘ÿ³„æFw=zCó¯˜Í¹—Í$kôPžäÚ?p{zw¼n¯™,»<¯Ë±ýÝñºÙûx7— ‚ üœ™bâ 4º·“)<°×縺·žTW&,„1(¥R‰ÓÙýÍ3Ç’e™‚}»4›È°ìÑ?ëÀ›„û³—¿[»?I.¶„;3ØlîIe1N„îˆq"œ©Ý©N‚$©OÛMÖÖ6{òÆ×Õ5³~½ïg¥ÞXµjwÝu!ƒŽqã2Y·.ϳõßÎ…TWn>E»ÝAK‹£1€ØØð>Õ1Ð}2Ð\.{÷–°wo }´†—_¾“)”+¯œÁ‡®öÙ©æÈÍê:æ”{.ýÑŸ±ösí“ã£ô´ÔXȘG»ÅAþîßÒ'ÇRWÔLlV8Îv'­uV”꣫ú†ÍLÀÜdGn—‘e*ûXÖœ$œíNj‹¼w³ë¨¿âÿ³wßáqT燳½kÕ{—-¹à^0¸Q ¦›˜`I¸„@ ”ä&„ro:!¤7 ¦]B±éÛ¸ÛÛ²-˲zïÚ^fïk­´ÖJ^I+Ëå¼ÏãÇÒœ™3gGGÒj¾ù¾s ƒž–á—}>kà ý ˆvܱ¦80ªÄaoÀ¡¶ï­-¥±ëØOZÉGýB ¼¿÷C®[ðe/ä_ëŸÅ/ûQ*”œ3i1ïîù¿ýðÃÕ?ólÀx92Þ£.A²9‰Ç®ù)f™WìýˆŠ–ÃØ\6|~F­‰û.ý^ÔcˆT¦s0½Cqxœl}mpÿ™c½?Žþ_yËÄ%XØ»óÿøÍsaodv'/ýýu,ñfÎ:o.«¾±‚G¿wìp´ì6'‰)`4¿.Õ‡j¹}Å÷ì×ûu³G¹°® ‚ œN²r d¦LŸ; Ý`0 Óé9kéET,¥±nøÓxÜ}7oÕ-þ!2äÓÒTOZF&K­.¬ÏÓ•Éd"))xóLÜ$#æ‰ 1O„Xè;o°û„ÇCYY-ç7FE^^‡¿ Ãábÿþ&MÊaÆŒ"^{í³Q©§ÇÁæÍû9ûì)œsÎô#ÁãSf²×ÎY¼x3f¢V«B›G+Ö×d,utô°aÃ^V¬XˆÙ¬'1ÑLKKxRÅöíY¹r V«‰üü4ýòE½ßã8ýG5×Æâšœˆ²LTíhÆœ¬Çëî»'ÚÓì .݈ÇéÃÕíÁÖêD×·|ÏíUL“ý2 @RJ¡ý½N?©ã©Ú¼lkuâuûCçÆ9ð¶téRî¾ûnìv;?þñÛyÕJ5ÒÑ‘£#ÜÞàSR:õà@㑬¸£y|¼~/j¥šueÂÖ@ŽK?âËó¿„EofnÁ6•ofNþ,¬+~ÙÏ{‡_’o8´ªÁ_»þÈuqyÂߘ^5ç Ì:3v·{ž{€ÖžðÅKÓ†(MÉpžÐ±¹ƒO÷¸½.žúàOÃ:ÏéÄiwÑÝÙƒÅj&)-‰²=‘)Éx<^Úš;"îc0¸íþ›Pª”ìÚ²—éó¦ðÍûnä¡;þ—îÎè²g=FÓáâ‹­¥Ã~=ó—Ì`ó'ÃËþÔQf ´žš3¼üÔ”YÅl[÷ù sóóM{8ë¼¹”ä¢Ñ¨ñxb³>_cm39™$§ þ”–ÁdÞëÄ/7AA8šJÌh“$É©ƒî§P*Cí #º(•}•¼ž‘e¢ûúÝR©Ô"ðFpý%7É…¡‰y"DCÌ!Üý*Ð$%Y¨®ŸK’$±|ù¼1ÇÎ}£–-›Íÿ¸vÀ>S¦ä‘›;ô}¹?ÜɤI9,\8…¢¢ ÊËëG5®÷ßßÎÙgOaÁ‚)äç§ST”Çãã“Ovªßh}øáç,^< ³ÙÀÊ•‹Y½:ò½Ô¡²c}MF#1ÑB[[÷ íz}_ÀÄnøsmçÎr:;mX­&n¼ñB~øéQ©§ÇÕj"9Ù:ê¾Fc¤sm,®É‰¨­:˜Ürt0¬fW Á¼£’BBö‡lÿOðgKï÷ˆ¤È$‰ˆû÷öoJмÔéhÜVJì º9~øÃRQ»,¤Þl5Õ åS,Ƀ>Ònoí3˜ÌøÁÿX¯n ®•“˜ÕX#é°w†JI.)YÀÒ’`¶ÛÖŠm´Û#Cbe¨ YJ\ MÝá߬%éÁÀÄ–Šm‚n0ºëq,ÕGÖÁ‹7ÆcÖ ^FR€Ã‚5©‹&å ºÏÄ©Á'Â+˪}óqËÝב˜϶õ»xâ¡¿°}ÃÄ%Xøú½7Fý¤×s‚묔í.»¹ôìT’Ž¢öîØŒ½Ã¥¤\¿ ‚kª$¦$„•½TkÔè Á€óPãìË–“²G+ô5›œ?è>ÅG¾f^—ÚÃã÷FPANT»wnæãw^ô_EYð! —ÓÚVW=²¿O,Öàû ·Ë‰ß?²õ‚Œæà{Ú@ €ËiQ§šÎÎNjkëÄMraHbžÑóDˆ…þÙ1—\2?¬-99ŽÇ»…3Ïœ4æã8t¨žíÛ˸ꪅ̟_Ö>wn1=vK¨bÑ`^}#UUM( {ìVæÎ-kW*œ{îLþþ÷{ÈÉI9æ¸6nÜ‡ÍæÄ`ÐrÏ=+øì³½8ÇçûnݺݡëróÍÏŒ IDATqã Õö•W(,^|ÿû=¡Ò„G‹õ5©k®YÂ3ÏÜÇW¾r>æ£î7I’ÄÌæ‚ ‚§—–Váp¸ôáóùyê©5,^|÷Þ{Í€¾Òӹ瞕ÜsÏ5Qk×®à{åeËæ (陚Ür#kcqMND™ªl²²XR¼ˆ«ç\Úž•™ÍôéÓIš~! ¯GcIBcé»gªµ¦‘uÎM¨Í‰h̉¨MIh,ɘ²¦ KÌEe° KÈgîûú{}t ™¡íió®Äœ=%ÔlKkNĘVÖONN3fÌÀš–Í97KræÄT,ÉéX’ÓÉž:‡”üb qñ$dæ†Ú.\Hbâ±KË­x€ô³V’¹ø:²–|…ÌE×ÅèJ4.o½A7§ÓÉÿ÷sèС˜öïð˹$™#ß`Ÿ_8ø'‡[*ÈOÎè5bw‡ÿ¡›`Œ§ eðàÛ+wR˜RÀ‚¢ù<³a5ßÀ§\'¤‘nMãóê/èvF~ZáÝ=ï3¿p.ÓsÎÀ¢733/X’ïíÝï zîXÉJÈ$Ñ”@›­=l»Z©¦$m"åMá_3õ‘§ˆ]Þ?Ô$$.±¼ïóç —ÖïÇåu¡SëX\²7>+â~O_FyÓ!Ê›¨æõ±ô– í½#õ³/=„FÌÊzð¥áá ›H¶oø‚éó§2gá ^øëk¸ÊìÒê4œyÎl¶®ûÑT8)/” W±¿oÑ\¯ÇKWG7qñò'æðÙ[#ö›S˜€Û馫3úR©Ç²cãn®¹õ R3“)š”Où¾ë¼,¾x_l-t]>AA89ìCÿnv¹‚?ȲLWgû û)•*,qñt´·Dl×êô¤gåPWym¶Ìì|:Ú[“J­&'oMõ5Öè8ù|±{_,œºÄ<¢!æ‰0Zuu­lÛVÆœ9ùÒ—3mZ••M$&š™>½I’xê©×¹ýöËÇ|,O<ñ*¿ýííX­&yäföí«¦¹¹“¬¬$ŠŠ2)-­Âåò0kÖ„Aûðùü<ôÐÓ<þø­¤¦Æóøã·ÒÐÐFMM Z­†¢¢ ŒÆàCÉ×\³„_üâ¥!ÇäõúøôÓ/X¾|>%%Á‡ñ£-3yà pÅ ¶Y­Á²à—_¾€óÏŸÚ^^^Ïý÷ÿ5b??þ<W°ZšÖœ„Úœ€»³µÁ‚ßã$³c ¥¥¥œ}Ã],»ã!!woRÑ–J=îoGÝ<ósTµo𦛔֖›˜Ãå³.ÁíýØvx{hMµ•óV„µ)JnYòÕ!ÿø}o÷û86•okkè >é3#g:*e_…)¦ Zu¤6¼ÖÆ6Œf·Ü}=M_P¥Vqó]×aŽ3ÑÙÖÅúw78¾ 8—/}í2dYæÏÿû4{p±L‡ÍÁ_~þ @€Ë¯[Ƥ郿™°XÍX¬Á'¹ëª†_<» ˜uZ_Ý8ì jJzç]¶(l›R¥äª‚ÁᚊºcÚôQðöâ‹ÏbâÔ‚}fä¤qáUKØòéÎ#µc£µ±-GÊi~í®U$$‡ÿ\xÕR¦ÍL `íóc˜A„SRï'T*5 –,cáy—0qòŒPùÊ^F“™3]€J¥Æëõp¨lÏ€> &LfÎYç°øüKÉÎ+B¡PÕ‡…K–¡Óe?öF~JA„ñ÷ØcϱcGð¾æÄ‰Y\xálfÍšÀÞ½UÜqÇïxå• còà÷Ñjk[¸çž?ñùç‡$‰É“sYºt:yyi¼õÖî»ï¯TW{YŠêêfî¸ãw¬]» ·ÛCzz"óæ•0}zF£ŽÃ‡øå/_â—¿ü¿¨ÆõÞ{}KƒtvÚØºõ@TÇZâãÍaÿzâ×j5aÛãâ"/ÐÞÞÃw¿ûÏ>û>=hµŠ‹³).ÎÆhÔÑÒÒÅ?þñwÝõ‡A¿N±¾&Ãå÷ËÜ{ïŸyä‘Õìß_ƒ$ILœ˜Å‚“™1£ÄD ‡‹µk7qÛm¿¡¢bè{lþóüô§ÏR^^F£âŒ3ò™7¯„¼¼TÜn/o¿½•[oýå1ƒnUUMÜsÏŸ8t¨•JIII6sç“——ŠÝ¶õ˜™–±2Ò¹£¿&‰‰22‰ï«Â–––@zziiñ(•ãVp€ds2%éÅ´ÛÛiìêËÔu8F<=­8ÛëPꌨM aÇJ ^G7®Žzünžî4ÖT̹g û¼hâ’Cí! •&´ÝÑZ&®¯² ÏÑìu£µ$ €ßí@ö¹úö?2.…J£«ƒŽºJÜÝÍ XS³È6ŸÇM\JF¨Ýh4ÒÓÓ÷`cMM uuu¡ÿ{Ùjö¡ÐèP­(4:T†8ÒæŽÍRZZÚØÿ8¢7èær¹øÑ~ÄþýÃ+-‹ÞÌ“7þ£Öˆ×ïeË¡`ùÃdK2ó æðò¶WY8ñ,2ã3øù¿bÓ¡-aÇ߸ð+\1ëRvUÁžÚR´* ó çgˆãÓë¸tÆröÔîåÇÿùé€óŸY4Ÿ»/º¥BIM{-[+¶áñyÉNÈb^áÔJ5ûê÷ó“W˜×ëê¹WrÝ‚k‘2 IÁ?×=ÍšoÆöbõs÷Eßáì‰ ØR± ©…ô¸ll­Ø†ÓãdRæ$fçÍàå­¯°zã aÇÎ-˜Íý—Þ Ëmn¯Ü‰N­eNþlŒ÷½øC¾¿ün²³¨n«áÃÒÙX¾‰Öž¾’~J…’ïø7¿ò_ÔD_÷Y£Òðã«~@Iz1>¿ÏÊ7QÛ^‹YgfFît²²ðú½üæ' 3ã3xâ†_ð­§¿KcçÀÅ4¿4w«\Cekß[}_Ä1Äé-<õÕß¡SkñË~Êâô81j$™ƒÛ¿ÞvÌ×òïÿú:u°6󵿿¯?6k…õ*œ”Ç=} FMO—_” @ñEX¬f|^¿üÁ(Ûž‰f0êùñ“÷’”šÀkϾÅë«ßÐ÷•7\Ìe«–ÑÝÙÃCßú9]‘Ÿè(œ”ǃ¿ü.~ýšê"?5>˜»vSf•°ùãüù£«ÁüÛçÁd1òþkŸpÞå‹Ù»ã÷B¡T2ûìédå¥ã÷ËüòÁ§8°»<ìX½AÇ=‹¼¢l»·í£öp=~¿Ÿ´¬f-˜†R¥¤¡¦‰Çï}[w_¦l~q·~ï+ÆŸ‡V¯ÅiwÒÕþÄûc÷ü6¬“Åȃ¿ú.©Éx=^öí:ˆÃæ$§0“Œœ4^úÛë¼ýòØ®ÿ(‚ œzzËqØlÑ­Ñzª**9ƒ)Óçâ°Ûxoí‹÷Q(œ1ky…Á2C²ì§³½ÛV¯ÇŸ„$Iø}>¶nüˆ¦úš}huz,YFÜ‘r”^¯‡®Ž6|^/zƒ‹5I’ý~vlùtЬ¹SEåÛÁ&ó.Êç‘'²üüàƒ•‡ŸÚßÂèˆy"Œ§ŒŒDrrRðzýTU5ÑÚÚ5ncIM'33XìСzººFV²Z§Ó••DRR—ªªæqÍÚ-•JIFF"))V$I¢±±ššáÝ‹:®I\œ‘ììdÌf>ŸŸÖÖ.*+›FàMM'--½^KGG‡5àóùG4®ÌÌ$²²’P(´´tQYÙ8â¾ÆS,®Iï×¢ÿÿ½ÿdYF–å°-KÌ_G4Ôj5999¡J„’BI@ö“6ÿJeš¶¿A@öƒ$Û"Td“”*²I¡$uÎ¥ Ëø½.jMÛÖ„×ÛoãÖ×ÃÎ××Yð<*…DNN‡++‘ý~$IB©Äïx\©R#ûý(”JŠ (++‹ú{AR( äИM­¤&Å&Y(íÞJà8–šì º¹Ýn~üãYÐ  ÛÙÃck~Îíç}ƒŒø ΞLO¶¹í<¿éE^Ùþ:sóƒåô"•|vÃj’‚åÓ—1=gÓs‚µ~kÛëxø•Ÿ193X£¹·àÑ6•oæ¿_~˜ëϺ–ɓȞ“jkéi僽òŸm¯á—‡þÆý`ïÇ|yþJ” %Ÿ‡J?þŧÇÉ_~˜ÛÏûWϽ*lûë;ßà¥ÍÓ–·VlçŸëžæÚ3¯!'1;´¦[iÝ>~öÑ_©ï¨çé ÿæ®eß&'1››~… ea·Ñðø<<üÊ#\=çJ.<ã/ µùe?Ûoç¥-ÿP"3–ºœÝ<¶æùæ9·ŸÁ¤Œ¾ºÚ]Ž.v×|êy<ÚWÉ#ßý×~s%ÓŠ˜³(XÆ4°ÿ‹ƒ<ÿçW©©X¾ñ滯#)5²=‡ͬzýßï0iÆDŠ&åóÍûnä>1CÔ`ìË:tFXôõXzw:œQ£>’Ý·kK){¶ïgÕ7W0uvßרµ±gŸú¿A·ày\üÏ=Opñ5ç³xÙ™L›;™is'‡ÚmÝv>û`+¯ÿûmœGÕÖh5¤e ^ë[oÔ£7†ga*•áOÙºí’$§£ÇÕsÌÀãHI’Dº5³Î„Ým§µ§-âÚsc)Á”@œÞ‚ËëÂæ²ÓãŠÝš_±d²IJ >qÝÚÔ–eu:HNKÄoÆasÒXÛõS‰) Ä%X$èé´ÑÒØv\ÊG@0—’žˆF§¥³­‹ö–Óã ‚06DÆÛÈiuzLæ8Ôj n·[O7^OäröƒÑLMfT*.—[O>ol«œÈDÆ› ‘É$DCÌAáDp²d¼EkáõwÐP¶›3WÞJgc-ø<.ÞùýO€¾µÔz×P»óΫP*¡õoºéBü~9€³ZM8njk[‘$xæ™àòQwÝuuÄöššl6'sæLdýú½¬\¹§ÓÍã?v¾ÞãjjZ(+« •½ë®«ñx¼¸\vï®dîÜbü~?z½ÇKJJ½¨ïß'rÚmí´ÛÚÇu ѰuÛO»`[-m´4ÿ©´¶ævÚšÇçëëq{¨­þºx‚ ‚ Ä–ÛåÄíŠ>û>§Ã†Ó!‚ž‚ ‚pºùêW—qé¥ó‡}\yy=÷ßÿ×1‘KƒŽ§ŸþþˆŽ}ôѾµO%âšœœZ«—šÛÞƒÏíDgŠÃçéKp©©©Áåêû< ÑôU4™t¡Ê^*•’®.;--]8.ìGUA‹ÔžšOAA:”k¨ýèóõn¯ªj"5µ/€¦Ñ¨ñx¼øý2[¶ìgî܉x½~ââ‚Ûî/–Æ<ð¶oß>öîÝËêÕ«Ù³çÄ(µwª)J-dQñÙ#>~[ÅöðÀ› ‚ ‚ ‚ ‚ cÂhÔoöqqqÆ1k …4¢¯/€FsÜV†:®Ä599í_ÿ;Ö3Úæ^y#Û×ü;Ôþî»ï†íð`:†«¯^„,Ëüñkñûe”J~¿Œ$I(•ŠÐÚy½û=ñÄ+ÛûS©”|ôÑç(•ŠÐq½ç{íµÏ"ö{à@ kÖlÄï.…ô‡?¬%pÅg±fÍFd9€R©ˆý…ã8•šN¡R“ÖóÛwžïá‚ ‚ cL”šÆ“(5)DC”¢!æ‰p²IM'1qø%æœN7‡7ŽÁˆ„XR(””dèØêêfl¶ÑUU8.×äT+5Yœ6‡ÇAAr>Iæ$^Þö 999$$$ )˜9èpC)ZñöÆr*5’¤ ËÔ­[Mæ¢ë¨[·š… ²oß>ÚÚŽÏú™M­'_©IAAAAA!¨©©ƒ¦&±fû©J–eJK«Æ{'qMNNñF+Z•½FOu[Mh{^^¥¥¥”LYˆFodñßr 8ƒÁ¶è2qw7‡>ïÿuu5………Ç-ð6DàMywÏû|Q³›†.ñäŒ ‚ ‚ ‚ ‚ ‚ œ®’ÍÉètT¶UÑØ/fàp80Q¯§ÓéÂÖ‚³ÕìÔU‚ÚhEöy”jÒæ^ŽÊGÚÜË1ÚÒÓÓs\_k¬‰R“‚ ‚ ‚pš¥&…ñ$JM Ñ%…hˆy"‚ œNµR“ƒQ«ÕäääpèС°í½kÀÉþk´E")”2ÂCTÅÅÅ”••…®ãX¥&AAAAAA„qáõzݶ¾útØçóV|mÈ5àrx€.--¥RÉHNŽ@©T²|ù<Ö¬Ùˆ,P(´´t9.!b{jjIæ$^Þö 999$$$ )˜9änGë]®hÅØËQ¨ÔH’‚€,S·n5»í¨{{5 .dß¾ýøý2@èÿ@ T{ùåuaýÝÞ_ïvŸÏ?ั o‚ ‚ ‚ ‚ ‚ ‚ BH¼ÑŠV¥A¯ÑSÝVÚž——Gii)%SFµ†›Á`[Ë ô ™¸»›CŸ÷ÿ¿ººšÂÂBÚÚÚŽÓ+=xAAAAAN<’„.!Ow ²×=Þ£NP µ%W{ã=œSF²9}‚ŽÊ¶*»CÛF£1ê5Üt:]ØZn¶š}˜²JP­È>’RMÚÜËQâH›{9FÛAzzzŽëk5xAAAAá´&)”Xr§¡OÉE©Ñãµuàîj¦§j7²ß;ÞÕ¤éçðy°7”ãj¯ïá KÉõ—?OO{þòm¼öŽñÒ)I¡Ò2{9îÎ&zª÷âsv¨Ÿñ˜kjcì Çad±‘>ÿ* ©4o“Ãoþn¼‡5•ÁB\þL4æDÌ9Shß·~œGuj’Tjr/ü&åÿyœ¶½ŸŒ¨Ÿñ˜kæœ)¡ïݸü™¨ |Ž‘…èx½^:4`{ïn½æ­øÚkÀäðõØÒÒâQ*•8p€ää8”J%Ë—ÏcÍšÈr…B@KKç‘ã"¶§¦Æc·»hoïÆdÒãñx‘åmmÝaçkj}@_ÞAAAAA8-%N^LÑŠûA’pµ×Óyh>GjC†Ô4–do0 cÏç覻r–¼éxmíôTïï! ' žê½xmí¨M tWîA·ˆFoŒj ¸Þµß®¹f)J¥‚_ÿúe–/Ÿß/‡pwÜq%N§›ÚÚV$ žyæ}V­:'b{MM :šeËf³~ý^V®\ŒÓéæñÇŸ;ߎ]±°BÞAAA8-I’DàXâD‡ ‚ œn$IAη‚$Ñyp ^xh@É3I¡ –BÆÅþgD—˜‰»«Y¬ñ&Däµwðù“7£KÁÕV7ÞÃú‰v ¸^@FúÜdÒ¡T*P©”tuÙiiéÂápa·÷7X{jj<éx<>RR¬¡öÁÎK"ð&‚ ‚ ÂiÇh2³ðÜK¨ª8@Yé.d9ºjgŸ³…B:æ~åû÷ÐPWUŸ3ç-D«Õ³ïN:ÚZ¢:FA„ÑÓÄ¥ ±$мãÍA7€€ì;ÞÃú dœ­5ã= á'{Ýbžœ€†»ÜÁƒuèt®¾z²,óÇ?®Åï—Q*øý2’$¡T*ðù‚%*{÷{â‰W"¶÷§R)ùè£ÏQ*¡ãzÏ7DàMAA„ÓNÁ„ÉèôÒ2sÙ¿ggÔÇ%&§"IǼy=Û£êO«Ó‘™S€B¡¤òШÇ!‚ Âè©ôæÐÇ^{×{ ‚ £uôpG{ûí­·ûýÁ‡$@XPíå—×…íwt{½Û}>ÿ€ãÆ‚¼ ‚ ‚ ‚pZQ©ÔdçOàðÁ}#êã‹íii®´Ýa뉪Ÿ¼Â %N‡ÆúêEA„ãK¡Ö¡6×rw63åj-j£OWËq)WiH-À˜^„¤Pâj¯§§zïˆ2öôÉ9Ó' TëðÚ;é®Þ=äºYJ­•Þ€»«9bæ`/…JƒÚ€·§Ùï ïK£Ge°D<Öï²ãsÙ†ûrPj˜³'³2žžVzjöáwÛ£îc¸×$Ö$¥ CJ>Kjc<dÜ]ÍØj÷á÷8£ïG¡Äœ3]bŸGSö¬a«¹6R*ƒ¥F±Íkë@öy†Ý§Æ’Œ)«µ!ÙëÆÕ^­þt¯K©ÑcH+B—L ãno¤§nÔÇ §xAAá´’?µZƒ×ë¡¶jø7\N¶îÑ=¯P(È+,àpù~±Î› ‚ œ$J΢ðÊ{Øö?+">â f1ñš°ã××áµuŒÙx4æDŠV܇9猰ížîV¿ñÛ¨ûÑ%fQpÙw1gO ÛÈ´ìx‹ª÷þq5sÎН}€}ÏÜOwå®AÏ‘:ï rλÙçaǯVÁQ·Ä)KÈ¿ô;mØô2Õïý5ê×£PkÉ9ÿV’g.C¡ _Ç) ûhÛ»Žšÿ§{ðRß#½&±¢Pª)ºúâ g£P ,‰ç÷8iØøÔ­{nÈ€'€9{ …WÞ‹Öš¶ÝVw€Ê·ŸŠj<±šk£•{ám$qNĶÏ?DçÁÍQ÷¥O#ïâoa-˜ GU¶ð¹l4m[KýºçæI™‹®#± œƒ IDAT}þ•H* YFöûø}xlÔ­ž–/>ˆz<©AÞAAA8­L˜@uÅAüãøjFv>:½YöS]Q6nã8Yètº°Å×!1O„hˆyrz›tãÿ`L+ ~")BÛK¾ò(••VöâO‡ "T “nx]b];°7B¥3_¼€‰_~(ªìcz%×=‚Ê`ÁïqÒU¾ ½}bqù3I™} º¤ö?û9¼”[סíxí¨V'/òš%N^@ÇMøÝŽí^Gö†ƒaÛ )ùHÊáÝÆVâ(^õSŒÁ*®¶Zzj÷_kj†´’Î8}R{þzgÄ>FsMbEö{ј“P(ÕØÊq¶Tá÷¸Ð˜°äMC©5’µä”ZÃAISf %×ÿ …Z‡ìóÐq`#îÎ&´ñiÄO<“I×?ṟÄj®Å‚»³!lžHJ5†”¼a÷c̘HñªŸ 6ÄA @OM)ÎÖj”¦¬´q)d.¼…JCõ{Ø$1aÅ$™×=Õ{é©Û’sÎTT†8²–|…ÖHÃæWGúr…“¼ ‚ ‚ ‚pÚHIÏÂd–†:\>²2“±R0a2uÕ‡q»Å ࡘL&¬Ö8l6;ã=á%æ‰ 1OIR )”G>é ¼I %³]<]ÍÇsh#’}îׂee?_~”ŽýŸ…ÚªÞý—ÝMâ”ÅCö¡Pi(Zñ*ƒ[í>Ê^ü ^{ß÷‡%wūƒ{é ®¦~ËaÇd?m¥Ÿ’6÷r&-¤òí§"¢´Ö4ŒéÁ@XëîÈ@6Òq`cضwþ m\ÊÐâ(ù—}cƲŸÊ7Ÿ¤yçÛaíúä\²Ï¹)˜)Áh¯I,•¿ú?øÝøœá¥Ì•=¾ôâ g“6ÿ*·¼qÎJ’‚‚ËïF¡ÖáµµSúô÷qµÕ…ÚµÖT&~ù! :ÓãˆÅ\‹•ÚŸ¡öãgBŸk­©Ìøö?‡Õ‡Rk`âÊÿFmˆÃkë ìÅŸ`«Ûßo kÑlRç^Nݧ«#ö‘2cY(èvøÍ'iÚ¶®5&)”dŸ+Ö¢¹¤Ì\F×á]ØFPÖs<§MÄáqPœO’9‰—·½@NN h fÒP¶¯Ë‰ÇiC’xÝ.º[˜1cTVV†õ[´âìå(Tj$IA@–©[·šÌE×Q·n5 .dß¾}´µµï—3"ð&‚ ‚ Âi£7ØÕÜP‹ÝvüÖå8š5!‰øÄd`ü€'•J…J¥Âd2ˆ›åBDbžÑóD(ý×½¡é˜zëì{ú¾£nºŸø4–d’g\@Ó¶µaÙëæÐ«?Ç”Y< ¼`©s.C—ßã¤ìÅŸ†˜º«¾ ö“gÉ9ÿVÒæ_IÃÆÿ XÏ«í‹I›{9*ƒKÞtº*v 8Oo¶›×ÞIסí#zÍѰÍ!~â™Ô|ðA7gKe/þdÐ>bqMb¥¬?¿ÇIÕ»bÚýIR`É›Fë®÷ì—0e1ú¤l¿õûý¹;›8øÒO™þ­¿ :†X͵IæÂkÑX’dÊ^x[ý£öÐY¾Îòm‘;$²–Þ@û¾õ4o#ühÙOÍûØ1I©!iÚ¹']à-ÞhE«Ò ×è©n« mÏËË£´´”’) Ñè,¾ñ;t6ÖB €Ïãâß¿·ü~?@€[n¹‡ÃÁsÏ tK OÈÄÝÝú¼ÿÿÕÕÕž”·´{+P ½› ŒŸ.»—ßÝð+¾qÎ-ã=AAá`4[HM–Æ©èìR©Ôdæ–‘3¬þô#ÙùLœBnA1kBÔÇN ®ÒÙÑJG[ߺ" ‰)dæ`0š‡5–S]gg']¡›åV«u¼‡$œ€Ä<¢!æ‰p*±ÍA:’µ×²ë݈ûd~·}È~§,‚Á¯=òZt-»Þ@mŒÇ˜^8 ÝVW[m°¿É‘³žŽloÛûñ˜•fHœz.>gM[_Y1¸&ǃ«­.t-Õ†È?Ïâ'ÌÀçì¦óÀ¦ˆûxC¯]«¹vâHœ\#®«|[„ Û±™3'¡6ÅÐ!à ÁR¡·`Êš4`­Á]²9™’ôbÚíí4v5†¶;ŒF#­U‰KÍÀmïÁçv¢ÒhÃŽ·Ûí¸\.jjj¨«ë øÚjö¡ÐèP­(4:T†¸#ûàÿF£‘žžð Ï“Èx"*NŸˆF©fwíÞqCª%•Œø ºš†u\’9‰‚ä< Z#.¯‹Úö:jÛkÇh”ãËd1RPœ‹ÑlÄawP}¨ŽŽÖãó´Þxž[Aa$z³Ýì¶šúÞMfæ,XŠÃn£±¾:ª¾&O›ƒÉ7`{KcÛ7‚{ˆµƒ´:=Ùù>ží6aÒ4Ò2sعeÕ‡Oî?6%€®µ —Ë…Ë¥E§Ó‰L•ÓˆV¼#æ‰01O„Ó•)+øÞFöyp4QJ­1´š­vð,|Ÿ£¯­µ)CJ>¶ºŠÖÝ‘µôâKÎBzóɰ 0]|:Æô¢à~_|8¢±FËZ8€žªÝÈ~ï°å5‰5IR 6Å£ÐèûÖ½ ‚mŠÈ96¦ì#ïÊ µŽa´b1×N$úä4–$€ˆÙ™Ñ0çL }lo|½fGKñ% ‘$Úø4-Ñý½q"Xóù·ïÚµ‹œœö¯€kƒ™ls¯¼‘íkþÚ¯¢¢€wß Ö6ny¶Kürh÷Š+.¦¬ìä^{ÌojµšoûÛüç?ÿPËS8q}û‚Ûñù}|÷ß÷ŒÛ6üŒ„†ªZ£ûa”—Ê×Ϲ…9Ó´Õw6ð¯uϲíðØ¥±O«‰Uß\ÁœE3Pôû¥ؽmÏ<ù"í-cóGÃxž[Aa¤T*59yÁ(•‡öè÷ÇZ­vŽÚê ¶™„¤T’S3HNËäì¥óÉ{kðû#—Ê+,A¡Pàñ¸©­®§úäz v4|ŠLÒÒ†_ލ7S%xó\¬w*KLLÀårûX1ONbž§+Í‘l¯­}Àë¨û0÷eìÇŸ…!5Ð}ªà{&•>rf~ëîÉZz*½™¸‚a%úz×Ár¶Ö`o88¢±FC¡T£2XpuÔ¨X^“XP(Õ$ϼˆÄ©K1¦…Î-µ1˜ çénñb1×N$½A7WûÈæ‰Útdžxm‘³"°¶Þ¹y²óz½:4°læÖWŸûü¢‹æ’šÏ›on ¥%˜Y™––ÀòåóX³f#²ÝcNMÇnwÑØXKFF"YÐÖ\" 99I’Èr€%K¦SVVËž=Á`pffF…ÛíÅj5átºCýV¨llcæòkؾæßäM­ƒýFjÌoóæÍãÜsÏeΜ9<ðÀTUUõ)…Q2j¤Å¥RÛ¹~ðñòÒ–ÿD½oZ\*]óS,z Ÿ‡Í‡¶ÒÜÝB¼ÑÊÜ‚ÙdXÓ¹ÿÒ{øÕۿ峃‘SªOzƒŽ{ý™yé8.¶¯ßEKc+Ö„8æ.žÉ´¹“¹ï¾ÍÏîú5=]¶SæÜ‚ ‚ £‘“?•Zß"üéIÕ0oï®y·Ë¼ žc"3ç-ÄOañÊJw 8^¡PWX @UE²?¼ÌÒpÆrºS©Tø|c³–ŠpêóDˆ†˜'ÂÉJ© flʾáguõRéúFÖ¢9QÊ´:Š»³‘žÚRÌY“I˜¼ä¨À[°ÌdëŒx¬ÑèØð{FLå5-9‘’ëAŸœ =µû°7”ást‡JLf-½I¡Œx¼B©êF3Ob1×N$*}ß<‘½#›'¡krŒ¬Ê@¿vIqú,ɹjU°Ìçw\‰Ó馶¶I‚ššt:5Ë–Ífýú½¬\¹§ÓÍã?ÀõןǎåœwÞLœN755-èõ}?]}õ"l¶àC8ÉÉq$&Zhlì .ΈÓéæ³Ïê)--åìî`Ùáq:(¿çk1ícxÛ°aùË_øú׿Î#<ƒ>HuõÉ“Ny:*L)@’¤cïx¹yÉW±è-t::yàÅÑÜ»0#`ÐxhÅ)L)àk‹ndãÁÍ8yŸÌ¸ôÚ ÉÌK§³­‹G¿÷[ÚšÛCm¯¯~‡~ñR2’XqÓ%üë‰N™s ‚ ‚ ŒFþ„IÔUWàñ¸ÃÚ†“eær:m«>\FnÁ’RÉÊ-ŒxËÈÎG§7¨,XºH}e¼©äàƒ~‘oEb2™Beá›ÄòÓ@[[üzGK̓Ә'ÂéÊï¾/Q¨FþþÁçê+m]·þylµûyŒ³uðÄŠÖ/> ÞJpø ¿]BÆ´BhÝóшÇ Ÿ³ïõ(5Ñ•ŸÐGŒ¯Éh^qúä\d¯‹ÏýˆîªÝöÉ\ò$"¿Ÿ’ý^~’R5ªy‹¹v"ñ÷û+Ô#›'½»àºm r¿Y¡ì ÉžÁÿ–8µ¶vR[Û´iù(•ásT¥RÒÕe§¥¥ ‡Ã…Ýî"55ž‚‚t<))ÖP{¯C‡˜81+´½ªª‰ÔÔøP{w·ÇONN2²Ànwãv{èêâÈy‚kÓAp.;º:èii“×~\Öx{íµ×|ãßàÑGå ¦¦fÌÎwÝ‚k™VÄú² |Tú W̺”s&/%Å’Œ_–©h®àµkØvxðú­éÖ4.›y S³¦hJ@’$Úmì©ÝË«;ÖÐØÙ8è±—Í\ά¼Yl«ØÆ»ÞF!)¸dÆÅ,)YDº5 Y–évõðú޵¼³û½ˆ}ÌÌÎùSÏcBj!f‡ÇIekŸì_Ǻýë#Ž2â3øúÒ›ñø<<¶æI±$³bΕLÏ™†ÕG·³‡Ï«wñ¦—h°(è%3.fNþlŒÁÉšlIæÇWýpÀ~Ïoz‘CÔ­©+f]ÆŒÜé¶l<ÈêƒqÌ:3³rg°zã aA7‡ÇÁs_à‡W<@‚)ôøtê#¤šK’Äââ…,,>›ü¤\L:·ƒÃ-•|´ï6”mŒ*`wÝ‚/3!-XÆèÙ «9Ô\qŒ#¢§R)Y²ü,^þçÚ°À@wg/þõ5îøÑ-œ}þ<^üÛk8íÁ_F³[¾w=šOßÞÈ–OwF<ǼÅ3Y|ѼûÕ¿±uÛG}nAA„ñ”šž…É\­âàØ»šëIHJÅl±¢T*ñ•ÑV01¸>FSC ûÀ ªa–:™IŸF¶´ÛÑ7ÉEI¸ÓƒÛ ”‹y" EÌátåsoHKÞ ~ã(žž¶`é@IÂÛÓFçÁÍ£S{é§ä-» ¥Öˆµp6e›CÙnÝÕ»ñt5£‡Ñ‘}|.* m|úˆúˆõ5)m\ –üà=φͯF ºEÃëèBcN +¯8\±˜k'OO[èãÏ“®–à’„ÆœÖgjsbècŸ³{Dç:Y}úipÎþñkñûe®¾z²,óįà÷ËH’„R©Àçó8V¥RòÑGŸ£T*BÇ­Y³ YÂŽëmÿç?ƒkÉ)• üþ¾õ {Ï#I rrrxë‰!ûýH’…2ú‡‡ã¸Þ^ýuü~?·Ýv[(øV[[{ìG 3>iÙSièlàŒ¬©,*>›úŽz·T’“˜ÍäÌILΜÄ>ø3ï︘çYSxðòûШ4¸¼n:$‰Ìø .˜z‹ŠÏæG/ÿdȀʴì©µÞØõ6w]t'gM83¬Ý 5hJpœ„Ä7ν… §ž@SW3ûê÷c5X™ž}3r¦qö„üâÍ_ã=*U¥P2-{*@€¼¤\ZñC´*-õ hU’̉œ?å\¦eOåîÕ÷áô„×>ÏJÈdZöÔÐç:µ6ìó^oízgÐ×=9‰YÏwôë<šùÝ{Ogˆc[Eä5Üš»[Bk”o¬4î¿ô¦Y(´ÃÞACg#‰¦fäNgFît/ŒxÝ–—œz–×wž0µ½A‡,Ëìøì‹ˆûìÚº·ËƒV§aʬ¶­û{ƒÏ7íá¦;¿L~q.eÕ´6†ÿBHNKä¦ï\‹N¯å™'_ ÝF{nAA„ñ”•[@ 3eúÜíƒ NÏYK/ â`)uïÔáq÷ݼUk´øûeÈÍâ’0™ãBçêO£ÕPTrY¹…ø¼¶lø7ËéÆd2‘”¼i!n’ ƒóDˆ†˜'Blô :ŒcÕ({C9‰SÏA¡Ò`HÉÅÑ\9ì>ün¶ú2L™ÅXò¦Ó´mí¨ÆäsöÐY¾•øâ$L^BGÙf'×wë2“½ºN¤…Xr§¡PªY ðh±¾&#Õ? äh(q?ö†r4æDŒiEÁù:‚5Úb1×N$ŽæJ¼öÔÆx¬…³iÚúú°û°Õ÷%¦˜2Khß¿!â~†´|.®Îè3³cá’K.a×®]c‡‰Voìå—×…mƒn@h»Ïçp\ÿöHýöº…ŸÇ¶6] ã÷…ï+Ç-ððÆo Ë2·ß~;>ú(÷ß?õõ#[¼p(r x±æäÏB!)øþó†‚dzž».º“Ùy3ùê¢Ø|h+=ýSK%ߺà¿Ð¨4|Q³›Ÿ¿ñkGR@­+?¸ü> Ròùú97sÿ ³Á*Z*ÈIÌfqÉ"Κp&eymÇZªZ«P)UXõqt8º»|ú2.œz>^¿—'Þý}ØzdÒŠxð²ï3'_>s%ÏnXñuK’Ä]ÝÉÖŠíüýÓ…lçM9‡ÛÏû&)–Οr.kv¾vü?>}šg7<ÇÅÓ–±jÁ5Ô¶×ñàK?0F×ëÞËŸ?ú;ÿ´oÆÎ¾Ž ¦žwÌãìn;ŸìøÍ×_ajðf‹_öÓ![ñ¿ÎûS²&ÓfkãWo=Áþ†@ðZ.)YÌíç}ƒ9ù³¸þ¬kùçºg†ó²b*;?€¦º\NwÄ}d¿LMEE“óÉÎÏ ~}úöF&N-dÁ¹søÆ½7ðø½O ËÁy£P(øú½7 ÓkÙüñ>~3ü—ÅhÏ-‚ ‚0^TGJâH’‚äÔŒA÷S(•¡ö†Ý€°*^'|ýÖþ0™ãBYx‘˜-VÌ뀲˜§+•*xíÄMra(bžÑóDˆ¿·ï÷³ÆœˆÓ}Tù8I"eæ²1G×á¾û.IÓϧú½¿ØÇœ5}rÎý´íùSf1ñ%gaH+ÀÑ8ºêM­»?$¾xñÅgbHÉÃV€ìóоoý¨úVÛžI˜´•ÞLÚ‚«©_ÿ|ä‡BÅúšŒ„¿ß¼R›&pè³ú­ï9Ü}øsâ'ÎGe°`-š1ƒ/eæÅCŽ%Vsí„ÐVºŽ´¹—c-š‹9{ =5{#ï;È<é©Ú×ÖŽÚ”@òÌ‹"Þ”Ö¢¹¡ýGô0eÊ”q¼ŽÇû„o½õO>ù$ñññ<öØc¤§,•s(½ó7Ñ”È_>þ{XfšÓãä÷ïýŸß‡^£çÌ¢yaÇ&[’èrtÑÚÓÆ ›þ/tèttòÒ–—˜ZD\¿Eûk:’2­Vªùê¯ðÙÁMüॳ©|3 Ô´Õ²»v/µíá^¥Tñ¥y+xuûš° ÀÁÆrþúÉ?€`€N¯Ñõºû¾qý²Ÿ§>øSXVÛ{?¢´.XZgê‘Ì®þ<>v·¯ßêÏî¶øç—#G¡GËís‡Ç'Ǧ¾ºZ©æšyWðѾOpûÂo^ä'ç…2ûÝ x >Þ÷I(HyÑ´ Ѫ´1×H$§SÂÛšûJ…æf‘7!›¼ Ù¡µùzÛSÒ¦?óä‹4Ô4Q8)ËV]Ú~ÙuË(œ”GCMÿúÝÀÒž±8· ‚ Âxؽs3¿óê ÿ*ÊJàúm½ÛêªGvsÅb Þq»œøýáïgm=]CŽããw^ =µÏ>~çU6|øæ(^ù©£³³“ÚÚ:q“\’˜'B4Ä<bÁÙ/Û'eVxÀBcI¢dÕO±N˜?æãp4UÐU\J'mÞ¡ü½¬…s(¾î'HŠ¡s/š¶¯ÅÙR$)(Yõ3¬…sÂÚ%…’Ä©K™vÛŸÐ'es\e›ñ»l(5zò/û.6…’ÆRûþ ¡ë’½ôF2_BÝw?O’$LZÈ´ÛþDÂäEûˆõ5 Góa|®`iòÔÙ— X‹Ì˜>’ëA’‚·ø+%Ùòù;¡‡yߎ®_&$)H?k%9çÝ<ôXb4×N$uŸ<‹×Þ’Dñµ‘8yqX«B­#}Á—˜qÇ?®Y¯@@¦nÝsX‹æ¹øú°ã•Z#ù—~•Ö€ìóÐòù»cÿ¢Žb0¨¬¬<îçŽsÆ[¯wÞyY–¹óÎ;yì±Ç¸ÿþûil|Í´‘rxœlPz°ËÙÍÆ2¦dN¦$}"ïíéKsnêjæ¾~0hŸµýÖ‹7ÆÓ¡.kO¿E<%IÁSü)”6”I%XŽóÞþ"r9ÇMå[pxœ4zfäLccyäÃkv¾ˆëUÑr˜É™“°¬ÇÏ©@!)¸óÂo‘nM£ÓÑÉ ›^°OoЭ¾£>,èÖßk;Ö²µb;6·í˜Á_¾õTG~ÉÄ:;P§¾IèŸqvÿÏïD« ®rÛ•÷âõxq9\Gö¸8¨Ûåá©GÿÁÿæn.]u!{vìGBâÒk/Àãöð‡Gÿ‰;BF[,Î-‚ ‚0öž!Û]®àM Y–éêlt?¥R…%.žŽö–ˆíZžô¬\êªh÷ûýCö„ž"t:ìÇÞ÷4ãóÅæÁ<áÔ&æ‰ 1O„Ñrµ×ÓU±ƒ¸‚Y¤Í¿ sÎTœ-Õ¨MñXò¦Uïþ‰Ü ¿9æc©|û)&õ—¨ q_û0¶ºý¸»ZÐ%fbL+ÄV·¿ÇI\þÌAûø}ü¿ŸQ|ÝÏÐÆ¥P|ÝOqw4âl«E¡ÖbL+@© ®‹˜~æÕT¬ýÍc’ý^Úö­'eæE˜2ŠhÝ]™ÉÌÅב:çÒ°mÿÏÞ‡GU_ ÿÞÙ—$“}BBØDEQÁ­îZ´j­Õ¶¶úZ»¨­­íÛVÛÚÅÚÖ¾VÛjµZk]q´ˆBd‘ YɾNfŸ{ïûÇ$“ Y„(œÏóøÜßzg&HæÌïsÇû˜îS/&}Ú9±ë¾šö<×{F²â×~ÅÄÏÿgöxrÏþ£Î¸}¶´œØýŒ:ãšv­áКeCý˜ ]Pùþ3ä/ùöŒ1̸ý Zöm@ ùp¸Çâ*˜I¨½‰†mï‘>}'Ÿ§|;WÇÍ£†üxó÷Œ¿ò{X]™LûÚã´•nE pfÇšì¦fý«dœ|>F«£Ïý Åkm¨Lºî§±ŽŠÒ•ubÜeߎK/Z»ñ ª>ˆÏÑ´¨ûþõS&\û&G㮼1¾Û 6Ä`¶bOÏCéÈXáž} e+þÜcŽÚMo’4f:©SÎ"÷ì/>míÕûP FY… Pƒ>*þóá^²ß 7·ÛÍúõ#S£ðD7b!è•+W¢iwÝuW,øV[;´9NËÊú xl®fjÎÜ.wŸãí;£SsI²'a1Y0 ±À€ÑÐ{á½î™÷®éQK­/c3òhÀÐf±“uȉ¶Nµ­µd䓟>¦ÏÀ[esU¯×=þè§$º§¹9^™fîZrs çùÅ¿¦ÉÛܣߨÌè_Ò%õ=ßéÔæo£mÅ/ƒá A†'%Ù}ÞÔ~Ièü%¢³ÿ¡–ÕðÌ^ä–o]Ï­ß¹…hªÉgÿøoªÊª‡um!„BˆOA¤|1™ÌÌ>)iìÝù %E;‰Dº~¡w&$2ëô…˜LfÂáÅE;Žl+ŸñBõB!ĉ¢øÕ‡wù=$œŒ3{<Îìñ ë´•ï |åøjŠsÞmÃ^.ÐXÅîgî%ñWIÊŸABîdr'£kê·,§l埽ð¦ƒ!þ† vþõ.rçôé‹°¦daMÉŠµûêJ©ÝðuŸô~XàP ÛÞ#sf´žmØ×JkqÏý1Z˜)½¶ÌÖ¸“k&GïÙÈÂíÍìzú;Œ:óZ2OY‚Ù™‚sÔ„X{¨­ºÍoQ³þU ºuêÇäHtÖË]p–¤t2O½]‹Ð¸ëCÊß}‚ˆ¿³3Wá©}Κv¯¡èÅŸwî—±¥ŽŠàS픯|’ê^ÆUp2öÌü>÷2T¯µ¡`r$õù:1Úèþ®}ÁDOå.vüåNræ_OÚÔ³1;\˜]éà½5ÅÔ¬…†m}Ô\Öuö¿ò ²«÷‘}úUX’ÒHIHAS#èj„¶²íT~ø|\=¸ciåÊ•#²®ÁÀÀ{g®ëÜu×]üô§?åÖ[oÒù[û‰"û:Ž6;,=𲓳¸qÞ85fŸÁµÁ*ª|áËÄŽ žÃbç7ü)‰D{bŸmžŽcÈ'Áýzó IDATªdG2÷^òmÆ»ÇÑæ÷ðàë¿`_mïÏEgÊÐ6ÿŸ„þ4£oî˜Ì]?ºÁ@W¯ó”£Ùjéh‹¯+ÒÝßû˜‰ÓÆ1ïühêƒ5+׳öÝ Çdm!„BˆO“Á„º4MÅÓÖBZ†›ÉÓNaâÔ´45 ±Úí$§¤£( j$¦VãóžØÿB!> ¼ÕûXÿ“þk[õ%ìma÷³÷aKÉÆ–ž‡®†ñ×—ò4Æú¬ÿé…CµÕ~ùëJÙý̽X]™ØRsðÖñE?D^úΟ(}çOÎnoæÀ[¿§låØÒFaIÌ@‹ 4TÄÝ×`xÊwÑc[¾òÉ^ë‡ -¤òý¿SõÁsØR³±$e (-5Wój(“#UûñëÔo~Gö8ÌjЋ·¦5èõÙóÜý8Ü›ûÎ*×¼w-Eë±¥çbu¹Qƒ>¼ÕûÐ"Ñ÷ð¶=þµ÷2T¯µ£µãÉ;‡l®`K-%¯ÿ†²wþ„55Kb:Z8ˆ¿¡œp{ÏC‡Ò5•ƒÿ}‘êu/aÏÌÇäLA ùð7Wjkì5+8~Õ<œŒpà ˆÕ0°Ù†>-])µÎ_¯ùÐIFb:]óm‰xÞÛ¹Š’ú´Ú‰¨œÖî¹ø[ƒÞƒ'0ø`NçV|!?ë©ïÖ›îµëD—üô1ÜwÉwIOL£¢©’_¼ñ+ª[úþŸŽÁ̓üYøKÐ×==éHè óºôèçìh÷¶÷Ÿ;;ÑåŒ}’æBQ”>‡¡^[!„â³DÓ4¶n\Kue)OšIjZ&©éî¸öšƒåìÞ¶ O[Ë‘/ôéÿ'©B!º 4Whî={бl­#ØZwÔóhᾚ|5ÇÇ{ºÁßP¿¡âˆçéÇDSôWîî·¯¶ïl^t]Ã__Ž¿¾ü¨ö3T¯µO5ä?ªçX×5¼5Å_Ë?êOt#x[°`wß}7^¯—x`Èç·šúæÙ;ŠQBñ5¸.Ÿu)‰¶D¼A/ß~þ>< qíYý¤¦ìÍáüµw|J!ðØ{Ö:"jZîTî½ä;ØÌ66ØÄ#Ëÿ0`ªOo ú¸;­Î~û}ÔTFÿ‡–‘•Öo?÷¨ô¸þ½Y|ÅBfœvµUõ ÀÔS&qÑçÏãç{/ô9”k !„B|šìß³ý{¶ªo]Mu5UXmv]˜Í‚A?íž6¡£O7þÆK?ê9„B!„BŒœ ¼uÝ|>÷ß?%%Cÿiþ‚d™®LjÛ⃓²£?7”lìtÈK=„;ŒWÞý¤AŠ3%vâN ÞŒ¼iÜ{ñw°˜,,ÛòO¯yfPÏŠ¦J¦äLftZnŸ}ì;Y.7¾O#ª¦åÖí@Q´lº;Wj­M=ëÎ9äägGûï-ëuž±Çpå/&QyüO ßÿÍ]\zýöí,aï¶ži9‡jm!„BˆãA0à'\-g!„BˆîrÜ@æ)‡Ÿ†ÑWSžç¡d´:˜ñõ#K—YüÊôØ2Ä;yò˜Œœ1§dR½§ MÕ‰£ïiÛ-(JG©k²'§ÒZ㥩"°'EK u¶-´ˆ†bP=#ƒ²Íu蚎bP0Y´7D/²8L±uÆœ’IÙæ÷`ƈÞ:ƒn~¿ŸüàË:¹©9¤%¤ÒØÞwÝl43)+ZLsmüÚf“€@8þ$€‚ÂÅ'wåfV†¸8ꮃ{„ØÌ6æOšÇ›Ÿ¼Ýk¿ f,fm1ûk‹‡åتÖY««ã±ø,ÈMÍå;ÞÅdá¥_å¹uÿôØÍ¥Ÿ°xÚyŒÍ(Àíʤ¶—cÒçN]ÈϺM׸ùÏ·ÆN'öææù72yÔ$þ²ú)öV]ñ̲ý•4Ô6‘îNeÞy§ñæ = dÎ_<EQhkñ°w{ÏŸ-G‚ƒ¯ÞwF“‘žx•²ýÑœÖ/=ý×Ür)_¹çF~ôõ‡ik‰üÅÚB!„B!„':£ÕÙ™rØãLޤaØrŠrDÏ/€b² ñf>%ä1‰™<õ~ÆÏË!ìPôaãÎEci£¦¦¡†UÚ͆ظœié( $¹¨a•HP#ˆ`qDÃI…s³Ñ4MÕhªðÄoëTïmÆSbHñ˜Þ ºíÛ·oX×ûâY7òÛwEӵصËNý«ƒˆá£ýëãúW·Ô0*9›“óf`2šˆ¨Ñ:qÅÀϺ›Å†¦k©Gø—E_ü!?Ë·¿Ë¥§\ÌçO»šÝ÷PRŸ›÷ªÙW°ôôkhó{øúÓÿƒ/4ôu´ZýÑ“L™Idäs ¾4®Ýf¶õ˜)ÅÀÝ܉ÝbgKÙ'‡tØ\º…ŠÆJF§åòõs¿ÊCËŽKO™“2Š+f]ÀÚ¢uýݲ“³)Ì €Ãb?̻؛/¬ä¦;¯åâ¥çS¼»”=Ûº~†&œ4–Ï]¿¤£ß»±ŠÝ}ée¦²cÓnV¾º:v}ÅËï3uæD¦ž2‰Û_ï±Ý£][!„B!„âDWóÑ+4îX=pÇC¨Ÿ¢÷ãDß´ Ÿýæõ7yºO3yLFFjne›ëH̰vepóÔùpe; ù#ÚB´7ø±»¬±öHH%T±%Y ´…P  E¢ï+F=¢¡©º¦S_Ü×Þà'Tc랈²¾S ãÀ[gÐ-ðÀPT4t'zóñML5‘__÷ >.Ùˆ?ägrÎdNÍŸ Àk›—õ8 ·rÇ»œš?“ìä,þüCl*Ý‚ÍleVÁ©8-îù×ý|÷»–ËÒÓ¯Åír³nÿG4x‡dÏÿüè_LÌϤì‰wý^{6þÔåѬ-„B!„B! ØZG°—ŒKâø ëíU{FzŸ*ò˜ŒŒÆòhF³Cƒ`[뉞SЦ‹ÔÔøÃ¥×Pµ½!®]1(Ñ“ŠÒk&¾ÎuÒ‡þ0ÊgÍ1 ¼uÝ‚Á <ð{ö ÿš?äçþ—~Ìí‹nãÊÙ—Ç]}Ë›¼¸þ¥c>.ÙÄSþÏϽ†¼´Ñ±šn»ªvóÓUOr°ù _û¾¹øòÒFsÓ¼/°·ºhÈo¡Hˆ¿ò3®œuçO;ùçÅÚTMeãM¼¸áå)2‡R«¿‡–ý’¯,¼…Q)£biZ}­l¯Ü1äkdäã°:zmsX±d¶Utí!ÑžûÚåpáÂÕïZ‰¶Ä×Ê˹ç…ï³ôôk9­p6çLYkó<¬Úµšmx)î$ÜHÑuÿûùS\pÕ"νt>ã¦0nJm-þ³l o½ØóÄYÁÄ<®ºùt]çÉ_ýƒ¶–ös·6·ñ×ß<Ç?º•K–žÏ¾%ìÚ²÷¨×B!„B!„Bqìä˜FãKõ16£€ôÄt^Úø ¹9£IMM¥ŠL¬®Lê¶¼@¨­kr3S·éÍh¡7%š†ÒêÊD z µ7a²% EB ë„:b#Öd7ŠÁD(Ä’è uòYx«÷á©Ø €%)£cg:è:³ -B1cë骊b4b´:ȰCjj*¥5̼ð6-ûGGm¹è~\î‚^íMuØ’ˆ„‚èšÎŒÉãÙ½{7ýÇlÆ]qÞšýÔ¾þç!}Ü”¬¬¬¡/vˆCƒn»víÖõî^ò?œ9át>Øó!¿[ñG’.23ˆh*›ª«á~ç0Í䥯 ¨÷ÔÓâkkW—Ý…'àAÕÔ>f9:Š¢œM¢-oÐKƒ§ñ˜§xLMHÅeO"Ððâ xôg6šÉJvã°8h´SÓZ;lÏñÑ2 dŽJ'!ÑI»ÇKÝÁ†côɵ…B14ÒÒÒhoïù!†[é;eä/3Â;ŸfÑú8p`€žâD&¯!„Ÿ§ÀºÿÙùŸ¦ihš÷uRÒðÖmœ[8_ÐǨ”Q4¶7ññÌŸ?Ÿ]»va;P[=wº¦Rõáó\tfG2jÈO © P4V ´ã*<…æ=ëÈž{jÈOñk¿ ÿ‚ÛQ &ÌWt\c%Þêý´G×Í9k) àÌ*D ùQC~"vÌõ+µH]‹ GÂx*v2}”ƒ]»vqÚ Ñ4¥Žä4B~M•% (4V”ho£pÖYìY³‚¹W™ßÇ–ç!++‹ 6pË-·àóùp8ø|>ž>zŸã®¼-ègÝSàNš²bÇ,Õä¬Y³¸ûî» …BüøÇ?ö [E‰}Ùâkí<ëOX S\WÒg»®ë´øZŽj{ÑuƒÍ‡u4µ7ÑtH:Îî&šD~Æ‘ÿ¢üÁž5x¨™v¬…Õ0•#½AÑ4šÊ‘IM0’k !„B!„B!„è[FböT¥eÔ´ÖÄ®û|>œN'Oþ¦*Çœ„bˆ)a_!O=jЇôaIvc· EÂX\±ö]Ç`²Ä®ûʱ¸2bͱy’2yêQ#ZÇ©èú!t5‚®i´ì߈/yN§ƒÉŒ¯µO}5A_;Ao;Éî\¬cˆ„‚¸2GÅÚ£÷Öu€¨¢¢‚@ €Íf#è:ØÔ^±›„Ü®lCiØo»wïfçÎ<÷ÜsìØ1ô) ÅÈç.䬉gñø%›>u7!„B!„B!„â³lÙ'oöz}ëÖ­äååÑ´{ å+ŸD×T²N» 4Ò·C×TPƒ]ô˜C1šhܹÅ`ŒóÖc0ۨݸ,n\g{͆רßön\»b0¢k*Šbè8-¨ÇíóíGˆ¦FÛ F#j¤g6C£ÉÌÎUo`0W8–¢¢¢XÛŠ+z}j6¼ ù`¦a¼y½^î»ï¾á^FŒe[ÞdÙ–Þ€…B!„B!†›¤˜B!/S\\û^ï(³T³þÕ¸~s.ÿ"ÕEÛ ü„üí(Šp0@[}u,h¦«‘ظ%Kfãv§ðVi4ud}}4¡Z² €¬¬T.¼pË–­CÓt µÚê룙ÝîäímmL™œ‡× ©©„;¡PMÓillë˜7£Ñ»>uj&fsˆ;¢ÿ>ÈÈHî¼S4MÇf³ E0 \xáúåóCúøÂ1¼ !„B!„B!„BˆÏ‹Ý‰ÅîdþÿCKM%è:‘P€åü_ «vZgÍ4‡ÃFII5^xªªñì³ïľÏÈpðo\†ß¤²²Egž‰ö[ºta¯íõØlf/>•5kvrõÕóñûƒüüçÿàšk`4HNNÀïRQQÝn‰ÝÇâųP7.¿?ˆÏ¤½ÝËå¶Çî¸ ¼­Øñ.Û*¶SÝ-o©B!„B!„B!„XCÙ>\îQ½"A?¶‘PW´ÎÚi±þ -TVÖ3}zF£1v=!ÁûÞd2ÒÚ꥾¾Ÿ/€×Û5¾¯v·;…±c³ …"df&ÇÚ;麎ÅbŽ]/+«ÅíN‰µwΓ‘‘L}}+F£B(¤ârE×JVV–>,3 !„B!>ÕÒÒÒhooáˆQé;eä/3Â;Ÿf€¤‡'ð½](6”º½÷¬À¸õeûGz[B!ŽsÑúdñvþ§iš¦Å}””4’Û=l³/»‘MËþ¦ªýö3 ¨ªÆ•Wž…¦i¼þú:TU‹]W£Ñ@$çÐ~‡¶wg2cs]zéhš†ßÂf³ðÚkÿíuÞW^Y ÐcÞÎýÔ64ãNOé±Ö‘ÈúNitŸC2›B!„B!„Ÿº†ž”…ž”…6îl"snÄòÌ(žÚ‘Þ™Bñ™11k¾±¤'¦óÒ« //ÔÔT,cgöY ¸ØCKK ªªÄþ,¼ü^¼5û1˜Ì(Šê4ª>|Žœ³®£êÃç8óÌ3Ù½{7=öÔ4‹DT^zéÃ>Ûíº®Çµwîg8†mf!„B!„B!Ž1ÛC'aûùtÌ/݉âoEÏœHäÜ{Fz[B!ÄgJŠ3™G2v‹òÆŠØõüü|*++c5àÝv/§_ûUæ^}+§_sk¬ŸªªèºÎ-·ÜÂÒ¥K»&VÀžš÷}÷?ËËË),,Æ;~xB!„B!„Ç—@Æí¯c\ó'´Ü™#¼!!„â³%#1ƒIÙiò6QÓZ»îóùp:=jÀ™,Ö¸ñ^¯—@ @EEUUU±ëí»1Xl˜É,6LY³?ûÓétâñxŽÙ}I5)„B!„B!ŽKJ 5ú…Ù!„Ÿ1Ë>y³×ë[·n%//=k–°ùç®pJJJX±bEÜøš ¯ÂP Ft]ƒŽšx\'RTT4d÷1$ð&„B!„8!)Š+|.„âø¤'ºPúa˜¬¬T.¼pË–­CÓt †hbF·;¯7@MM%£F¥ …Ñ4ÆÆ622\±ßá4Mçì³gPTTÉŽ:Ú“;V޶ÛlB¡F£!¶^$¢a2p8l躕ÔÔT–¿»jÈ# ¼ !„B!N8΄Dæse%{)ÚµM\aí3^ˆÁ  ØoÿžTW•õÚvÒÌÓHIMpŽúÚjöìØ<¨} !„螘 €â© §B!†’Ãa£¤¤š /< UÕxöÙwXºt!ßøÆeøýA*+P¨¨¨Çf3³xñ©¬Y³“«¯žßäç?ÿ'×_¿ˆÍ›÷³hÑLüþ õØí–Øz‹ÏBQ`ܸüþ >_öv?.—€k®Y€ªª„öo/ÅfËd×®]CzÏ5çxB!„Bœ€ÆŽŸ‚Íî +g {vlô¸´ 7Š2pà-ÚÔg[’+…Ôt÷€sÔÕô¾„BôNOÌ@i«á!„'–††*+ë™>½£Ñ×f2imõR_ߊÏÀë àv§0vl6¡P„ÌÌäX{§ââj&LÈ]/+«ÅíN‰µwΓ‘‘L}}+F£B(¤ârE× …„Ã*ªª±aÃfÍJÀét˽KàM!„BqB1™ÌŒ.À}»hŽm›ÖQ_×w`Ì×>p1ðŠÒýíÞÚg{Ðï?¢½ !„è&–jRN¼ !„ÇÒlàÿþï TUãÊ+ÏBÓ4}ôTUCQŒF‘ˆÚc¬ÉddÕªO0 ±qË–}„¦éèº7®³ýå—×°bŦ¸v£Ñ€ªj t]•è¬U7$ð&„B!„8¡Œ.Ùl!QYÖ³6Á`ü>ÚÛZîØp(xÔs!„蟞=ñF›Þ„BœX.ºè"¶nÝJeeåˆîCU£iý_zéø뺮÷tb×#µÇ¸î탙·sýCË ôU«n(†eV!„B!„ø”;~2å%ûPÕÈïF –Ífé-ˆÏyˆCéJÈ7Â;B!Ž­½{÷2uêÔ‘ÞÆ IoB!„BˆFfv. ‰.ì?²4“âØKHH ==äää‘ÞŠø“׉èl@Ë?m„w"„B[‡ƒÒÒÒ‘ÞÆ IoB!„BˆÆØñS¨«®ÄÛÞ6»ƒe2™0™L$$8%¨"ú$¯Ñã–ˆÌû‘EßE5 lI`0Žð΄Bˆáåv»©®®émœ¤Æ›B!„â„àLL @I·Ón&“÷¨Ñ¨‘5Ë=ŸÝá$%-›Ý‰ÐÜTO[KÓaíÉl¶šáÆáHÀÓÖBc}-º® 0òÄÒÒÒ@r²‹„gÜ5!:ÉëDôÆôŸ‡Á׈:ó"gÝgÝ€qó ˜_¿g„w'„B Ÿ•+WŽôNXxB!„Bœ:O»yÛ=ÔUww&$2ëôø¼íƒ¼M™>‹„$Wëõ5UlZ¿š` 0àY9cS8£1þ×2¯§MëWÓÜX?¨½|VéØÁ×ä Vl6›UN V«׉8 ÖôäÑàHíºö£„¼#·'!„B×$ð&„B!„8î™LfòòÇPZ¼]×cmf³å°ç³ØlT–—àk÷ ë©én2Ü£ÈÈÊá̰zå2T5Òï6›úºj<­Í„Ãa“\ŒÊÍÇ™˜Ô1Çëxڎ߀AÄ@V–û°Çv¦ŒYrŠÏ®´´4ÿa•׉_ökÔIçC$ˆiùO0îxÅS;ÒÛB!ÄqLoB!„Bˆã^^ÁxLf3ª¡¼¤(®Ít·Ë^ ðÇï¢kL`æœy$ºR(œ8•¢][{cãº÷1„‚ÁÁ¹}ÉÛ9kÑE˜Lf¦Í<ÿ®^>è½hL&‘HÿÁM!äu"Ôqg`Zý(¦uáÝ!„âD 7!„B!Äq¯`üdªÊK…‚qm&³yÐóü¾>ÛÊ1fìxRÓÝäŽ)ì3ð ö}ò¦­¥‰’¢]L˜2ƒŒ¬¬6Û ÒV~™´*jjŒƒ“KXSS+•@cc#}¾K^'"Ž!úwŒ¡ê“ÞˆBqì9%“ê=MhªN$¨P0;‹Ö/¾– &«5½îo 0ùœÑ”n¬Å`4 F¢µ§MV#ZDcôŒ Ê6×(Ñëí þóêšNÐ`ôô ì. ¥m„ƒ*ZDC‹h(…Ñ32Ø»:ZÀ™bCh<Ñ}Ø“¢Ôu@£Å7®lsº¦£”¸}8Smh-¶‹Ã„Ò¢÷¢C =·ßÚ†æ!Ü%ð&„B!„8®¹³sIHŒÖc+Ù·»G»ù0o©«9Hjº›Ä¤dŒF#ªªöõ5UL˜2WJz\=ºã‰BôãÁ¦<4˜"©O Á`4P.¯q¤ozb&º5q¤·"„BS‰™<õ~ÆÏË!ìPôaôƒoF‹£ÙÀ˜S3iªðšýdgÌh6’73[’…€'DS…‡¬ )L= jXÅß½Þðꜷpn6š¦³û½hýl“ÕH[k‚£É€{B á@‹#><•;=[’…­ËJÈ™–Ž¢@’Û‰V‰µ¸qëhª·‚ÙY„‘Øþm f|­!,vjXå“×Kâö;NÈÀ›ÕdåWKà·ËOIÝÞQßfäMçËg€_¾õ*Ï_º…B!„.¹c Ðu©3f÷hw8€h͵3, dß.jªÊ{­î§ÙÌ+j?'äúì6‡Åb=ìñÇ£„„ÒÓ£µ¾$˜"ú"¯ÑÅS‹ž˜ IY#½!„â˜JÍM ls‰vÂÁ®¶7ø±»¬èšN}q+¶D jH‹µ‡üa´ˆNBº ]#Ö/kB ŠQ!äh ¡ ¾¸µç¼DƒaüžÞ¦™…ɘ2±ù£ëgwYл† ©D‚*¶$kl½îãôˆ†¦j±ýõµÿHPE «„€@Ç©¾îû'dàMQF¥Œ¢A¸O3‡ÅÛ«ÅxøEßOŠ¢?~4éî4 …ÆúfJö–Åýp+F“‘”´è§©=m^‚þ`¿ýíNc'æ“”œH0¤ª´šÚƒõG¼~jF2ƒ€?H{›wÀ½Ž8†”ôdZ[)Ù[F$Üw‡ÓŽ#Á>à>‚žÖö~û¸Ge0jL6»•¶–vŠw 0Àã%„Bq$L¦è‰6E1áÕg?ƒÑk¯>‚ €ÑØ•61 õÓ³¿9º~M ‡äßG­ÓLý“׉蕧˜†žèé!„ÇTc¹ š²›š½ñ©m fJ7v¥õ.ú z2N1(èZW]ëËË¢©•h›¦Æ×¼îœWQ”¸zØ5{šðÔûãú:ÿÖ7Ä}_úqtOUÛâÖëwè:í¿sß}=CiØof³™;—_~™ÒÒÒá^î¸SÓZËŠïÐêoáÝ|úÌ>k&×ÞvY,ØÕ©½ÍËKO½Áï¬;¦û¹öË—²èsóxúwÿäƒåõÚÏî°qÍ­—qæ¢ÙMñ5-öï:ÀS¿û'Õƒ¯apÊ™Óùú÷¿ÀG«6ñÄÃÏôÚÏd6qѵçqÞegcwØâÚ‚￵–מ}›` çEþ<_y΀{ÙðÁÿùÓ½¶åærã7®¦`☸ë‘p„2ǺÁ IDATUo®åå§Þ  ¸†B!Ä`mß²ž=;6÷ÙžW0±¦ðûøèƒøà¤@Rr*Á€U=²ºRsxÛ=G4Çñ¦¥¥…ööv©Õ%ú%¯ÑÅ}³Q—oB!N09¦ÑøR}ŒÍ( =1—6¾@^^©©©T4´¨Ý«‘ì‹-ÑEÐë!ÐC´ÕWàv»©­­eÆùWárç`KHbÝ¿þŒÉb% b4™0MDBÅ@kÝAn¸á>ùä¶oß]÷¶_¨Ü‹ÁbE¡æ•GȺì@Q°f%XS‚Õ=_éNŒ6g¬Ÿ®êŒùÚ£½¶í DÚ[PŒ&´Pûè‰øJwRÿΓñëšmÔ¼ö(éçÝ„êmÅ–3Õ×FíÓ ùc?ì·9sæpÎ9ç0kÖ,î»ï>ÊÊʆ{ÉãÊúRÿÏ“#½O¥yçŸÆÍw-àÀÞ2vnÙ‹¦jŒ?©É3ÆsÓ×bwÚXþÒªc²Ÿé³§Ä‚ný±Ú,|÷—w765¢²þýM,¯Á•êâ´³OaÜ”î}øN~v÷o©;Ø0¨µSÒ\|ñÎÏØOQnÿÞ™qÚI„BaþóÆÊöU ë:y…¹Ì_2—ÅW,$¯0—ß|ÿOhZü©AG‚€¦ú–×ô¹NEIU¯×óÆæpÏ/ïÀf·ÒX×ĦµÛðz¼ŒÊËæÔy38ï²³ÉÈJã?ùK¯ŸVB!„8>oÿÁ«@ dÓ4Ö–¦>û&’\)47õž¡Àj³“ýpQUyßéìÓ3³i¨«îµMQòÇM ­¥‰vOk¯ýNDLƒ!¯q(¥-ú»«ž(7!„'–g2V“»ÅNycEìz~~>»víbî w |Tl߈ÅáÄl³3ñÌó@Q0WíDUUŒF#çž{.ûüjKö`íHÕ?cñU$fdö{1ÍèºÆÍkc·ììlêêêb7]n©Ãš9¦çfÀÁ}LtU%Ü\ƒ!3Àížk ·Ö“I(ØõÛ¸e|çç_çä¹'1çìSz¬-„B1lñ“ÉÌÜùç“’–ÁÞŸPR´“H¤ë”¾3!‘Y§/Äd2‡(.ÚÑë<'Í< S©,+f×¶Mø}]é¹Mf3ÓO™KJjôßs{vn9ÊB¡´wœxs¦ÐS!„8¾d$f`OµQÚXFMkW Êçóát:1Y¬DB4Ueÿ†÷9ãÚ¯`›€·¥‘ ·Ú]»Ð4ÓO?Õ«W“8vM•%$gç1áŒóIÊÌA×TtL+•»6“=a»?xˆ~°qáÂ…,_¾€ps Íÿ}•Üÿ-Ü•Rß””†bJL%X[ÚqŠ--@õue´ºó{mwN˜ÚÞŒ®FH_ô…X{§Îusnøç}ƒÍªŠ `IÏÒÇ<ë;¥Ñ{ÒYûðÚk¯¡ë:·Ýv>ø ÷Ýw< ÅÀâiç2ÒYd&e8P_ʲ-oRÚ0ð©;£ÁȹSræø3ÈMÍÁf±áñ{ØS]Ä[[ßaouQ1_8ó: 3ÇR×VÇŸÞûs¿óßxæõdPÛZËÿýç‰Øõd‡‹ÿY|G¯cžXõ¶ôþÉØîEaþÄyÌ›x&écH°%à ú8P_ʪݫY[´¾ß\ÈMÍá’™15g )Î"j˜êÖ6oä­­ïœ+Tr6·.¼€Ê¦Jþ²ú©ÇŽyçÏÅj·RWÝÀ+«Gûò—W1wá©äæ²à¢3ù÷_—ÅÚξà fŸu2>¯Ÿ'~¶×´†‹™/ç 8œv6ýw«ÞXÓï~nþæu$ºxä‡óÅ»–öxS…yçŸÀ»¯}thmjãéG_à»?ÿÓfM&#+úšÆ~×^rå9Lž1ž7ÿõ.i)ýÞ&Ϙ@EÉÁ¸ [§{Ë)ß_AÁÄ1Lž1¾—À[ôÄ›·ýðS/”ÏØ‰cÐu§ÿB\Ð `ÿî¬zs-ç]v6ç]:_oB!„8fsÎ^ÓToïõn=­ÍhšFî˜BrÇÒÚÒ„ß׎Éd&%5cGª½;?¡ºR²…!ÄÑÒ2PT)i „âIJì“7{½¾uëVòòò8¸w+›–ýMUøï ½yíµ×¢_TVPw ÙüFtìÉK®Æl³óÉÛÿŠË öÈ#Äe#¨yù·TýãÑ5ŒÅ_M‹}Ÿ}­Sg¿ò?kÀ{VLÞ{6n\lÝg~Ô³¿qxBdÇ$ððú믣ª*_ýêWcÁ·ÊŽ'j¨)(|ë‚ÿaî¸h€£ÞÓ@«¯•)£&qZál~¿â±~Ç'X|ÿÒ{™5]×)m(£­¡ìälæM8ƒyÎà¹u/ðÒǯÄ;Ø\Íå§~.z¿›ß ªù`¯ó';’¹xæ… Fþ^¶5®Íb²0}ôI½Ž³[ú>EÕÉaqpïÅßfjîš½ÍT·Ô–ÊÉcfpò˜œ5q¿zë·„{ùGç‚Éó¹}ÑW0ŒxŠjöa5Y›YÀx÷8Ι²€¿òSê=ý§@´[ì±û°›mýö=ÓgGïoÓÚ­}¦$üøÃOÈ+ÌeÆœ©q·ÞßÄù—/`òÉhmöðÇþÝcì5_¾”SÏœAíÁzÖ½÷q¿{Yô¹ùLŸ=…¯¼ÏŽM{úí›æN%!É À¶wõÚgï¶ý4Õ·š‘Ìô9Syïõúœo̸Ñ\~ã…”ì-ãÕgÞâ–»¯ïwýη•þ¾ƒ§~_°Ï6glj7_»¿Ï>}™Ññœ••ÓÐG0qÃ[8ﲳɟGRr"m-RÓD!„Ÿš¦±uãZª+K™xÒLRÓ2IMwǵ×,g÷¶MxÚZúœ§¬¤ˆÆúZ&O?•¬Q£q%§âêVÓ­¹±Ž½»¶R{px?¨(„Ç5«=)µp>‘y_ÀPüáoJ!„øt‡ÃÓ–XȨyK©Ûò¡¶è{þÖä,2f.¦nÓ›Ñì é­®LÔ —P{&[Z$ºÎ'ï¼Ø1Îb0¡E‚ ë¤N> oõ><;°$uÑA×ñ|¼-Â’˜[OWU££ÕA†RSS)ݺ’™^æŒ,tMG1D÷ãrçôzhoªÃ–D$D×tfLÏîÝ»©_þ×~‡qW܇·f?5¯÷ˆêH³ÀÀ›o¾‰¦iÜ~ûí<øàƒÜ{ï½<Ø{pêhœ9áŒXÐíï>ÎvEk|)(\0c1·,¸¹ßñ·ŸûU&d§®­žŸ¿ñ0e ]§’M]È×ιëN¿–’º¶t œ­-ú/7Ÿu«ƒs¦,à™µÏõ:ÿÂ)gc4 «aVí~?®­ÞÓÀßû>Áæä±›ô½mÑmLÍBc{#¿yûQöTïÞ»¢pö¤ùܾè6fœÂõg|ž§>|&nlaæØXÐ핯ñÏõ/é(ŸâLæ»}‹ Yãùæ’;ùþ‹ô{jn¸åŒ l_ßÁÛÒ}Ñç-+7“ÙD$½— ?ÈŸüßänιxÛ?Þ›>g /žG8æO>EÀßw *gL6WéÊ‹+yéoËúì×Éîè B¶4ö]¯£º²–ÔŒdró³ûìcµY¸í»7Gøó/þަöþ‰€îön/æÜKÏ&olÎD^OüÉ5«ÍB^aE;KzŒxë6NQ”AÕc=6úœ•îïû9+ß­7§( ¹ÙìÚ"7!„B ¿ý{¶³ÏöAõ­«©¢®¦ «ÍNB¢ ³ÙB0è§ÝÓF8Ô÷¿»k÷´òñÚÿ`2™It¥`µÚ‡Cø¼ü>ïÑÜŠB pÏ60cßJ×cZÓÿ‡°…BˆÑâÀW{€Ì™KÐ5•ªŸ`Ô™W¿øk¨!?¦*@!ÐXIÄl%}ƹ4ïYGöÜ+PC~Š_ûÙ§_‰b0av¸¢ã+1v;L”1ã\@Á™Uˆò£†üDí˜Iã¯B×"è‘0žŠär°k×.μá›,þÆù}4U–€¢ÐXQ‚ÙfgÆâ+Ù³fs¯þ2!¿-Ï=Baa!Ñ÷Ür >Ÿ‡ÃÏçãùç£÷‰öÔœayl wZo¿ý6øÃHIIᡇ";»ïÀ‘:Ú"Ö ºèè¼µõ>9ä”YwùœV8€ÇÞ{<.èðÞÎU¬Øñ.W͹"®- òÁÞhJ“çcìö¼îMYÀº}Ñæ,躎7èýç þdQAF>gŒŸ tëœ÷ýÝ«Y¶%z¼tÉô󱚬q㯞s%Fƒ‘Ý÷ðìŸÝš½-üúíGP5•‰Ù˜ÖÇ©¼c!!É `5Ö7”œHþøÑäMJš+ÚVm3 ¤»ãs¹W–VÇNºÝüÍ¥$%'Äæ¹ù®¥<÷§—¨(©ês&³‰Ûî¹]×yü'QÜ{÷€UjfJßs£¯ÔŒä>û|þ+W•›É³|qÀt”¶¬Ûή-EØv¾ñƒ[â{Yi|åÞ›HHrR¼§”þ³±ÇøÎoV›…ëo¿Š_üí‡<ñÆoøó²ßðàßçª/]B¢+¡×µÓ³ÒhêxÎ,VKì9Ëý´x$¢ÒÖìéØOú îI!„b$~ëk¨9XNscý ƒnÝE"ašë¨9XNc}Ý„bˆ(Í媶bÜü–翌åé¥v !„Ý…< ø›ª0Úœ˜âß?W &¾6ÍQƒ>BmõX’Ý$Ž™† cqeÄÚctƒÉ»îk(Çâê*‹Ô9Og{Ä׫õ¦L€Ž®FÐ5–ýcµè &3¾Öfš«J úÚi««&Ù˘ésˆ„‚¸2GÅÚN'O×ÿ÷+**¨ªªŠýÙ©½b7ËÐgëƒc|â­ÓòåËÑ4;3‡zˆ{ï½—ššš‚Ñ`dRöDÖoèµÏš¢µÌ›pF¯ms çPÕ|í½D_µk5‹§Ç¤ì‰$Ùã‚g+v¼Ë’éç“ìHfæ˜l<°9nìÔÜ)d'gðÎö•‡wsè ºl>tëîµÍoðqÉ&ÚƒíD´®ÀšÙhæ”ü“x{Ûò^Ç6xÙZ±SƜ̜ÂYl«èûSÁêKc'÷Tmà€Ôá°u;5Ö™2ñôE³¸æ–KxûÅ÷ø÷ß–è–2Ñf2¬Y±ž 'ræ¹s¸ù›×ñèžèÂ%²î?óÁòúÝÇÕ_ú¹ùÙ<õ»RSY7¨½7Õ7ÓÚ܆+%‰SϘÎîOzÖ LJN `Ò˜Ž}÷þƒÊÓ™¿x.ëþ³‘u½Èú¢ë:þø .ûœµd.?~ìBÁº¦cµ[ñ¶ûx÷µxùé7âòñBôd[çcÿÅ»>¢(xÛ}Ôl )%wN\µˆ3ΙÍoøx eçsðEŸ³Qyn~ð»h^Þ’½eüì›Ñ\»~_Wj6GÏçL!„B!„ˆõ÷ Gz B!ħ^Óîè!¢ò•O¢k*Y§]šFéÛ¡k*( ŠÁˆÞí€N'Åh¢qçjƒ16Î[SŒÁl£v㲸qí5¢µâê·½×®ŒèšŠ¢:2«E³«uÖ¢{ûÑ¢©ÑvƒÑˆéYBËh2³sÕŒFÆŽ¥¨¨ë}÷+Vôzÿ5^…ÞCHGmDo+W®DÓ4îºë®Xð­¶¶ö¨çu»Ü±“f•M½ŸV*oì»^BAfMù˜Õ ;T ÒUkLú˜¸]YC9ûjö3>kçLYØ#ðvîÔsbýöV÷ º±™c(©?ÐgŸ6mþ¶×óÒFÇ·@(Ðç½7tÔvËOÓï^4]ÞOìšÍ]/[5ÜwP¯{áF³ÅÜkŸgÿøoòÇç1}ö¾õ³¯1ùä T•Õð÷ß¿Øï¦ÍšÌ¢ÏÅÆ5[ùp€Ý¡>xg—,]Ìü Î`óºmìÚÒõ:HHròµïÝŒ±ãÄJÏñÉi.nºóZêkyöýï³7 ‰] :ráú}tMÇb³`6›HÍH&ÝFUYuÜ8G‚E‰n¨¢¤ŠþùUŠvÇRCΚ››q8„Ãat]gÊ”)ìÞ½›ÆÆFÔˆFm«7ßͦeÿTM¸Ú†æ!LG,ððÞ{ï¡ë:wÝu?ýéO¹õÖ[zΫ3öµ'ÐÞko ï€P’-€“ó¦óÇp½${bk+v¼Ëø¬qÌ*8…${R,Ðå´:9½£öÜòí½GY†ËÍ…zhúÊÁè~ßûÜ=ƒèŸtØk •p·`Ž©#§†U‚P´½£–›¥[঳íP¡`ˆ?=ø7~ð»o1ùä ±ïCÁÞû$ºøÒÝ×ÑTßÂÓ¿ûçaïÿ­ÞeÆœ©äær÷O¿ÆÎÍ{8X^‹+%‰és¦ FTÖ®\Ïü%§Çڃ詳/ëzìN;¿{àÏýÖŸëMº;•ïýú.\©I¬]¹ÿmm-Ñ׋Ãiç¢kÏcÉUç0õ”I<|ß8°·+ÕªßàÛ7þ€¶j·š®ël^»¶f÷>|'i™)Ì]xjÜ©ÁÎç­ó9Ó4=ö¼t¼;Ÿ·PÏ™B!„B!„BˆáÕY³íÀ›>ã<¬‰é[k£Á:]Çh±Çj³-´Hˆ·£ÅŽûÔ‹âjµMž<™H$‚Ù}ÿ÷òË/¢5ØDQ***0›Í=®WUUa±XX°`6là’K.!ðꫯÆÕt;séWÁׄÛÿ훇ü±ÑÀKgg³ M.ÍîuÕ4]뽓ÒË1¢X[ôƒÍÙ}°÷tÝ5µ÷Œ†®-ZÇÍg݈ÃêàìIóX¶å-æOš‡ÙhÆò³zÏšç>\áèqÌãt;Zµ¦h-ÁpÿAö>‚šÇ‚·½«Nš31ZsìÝ×?àÝ×?ˆëçLtľöus(»ÓŽÉ}ݘÌ&’’©®èûôå—¾y IN¾÷ø¼ƒ¯Á×) óË{ÿÀÒ¯\ÁÜ…³8éÔÉœtêdt]gûÇ»yö±sÁÕÑ:…­Íñ§_±É'O१ޠdoÙa¯}õ-—âJMbç–½üí‘çã^+>¯Ÿÿú:I)‰œ±h6Ko»‚¿Õ|Ö4憖~çß¿ëeÅ•äMᔂ¸À›·ÝOZ&8¢ÏKyq%·_ñÝst>oÝëá !„B!„B!„8†:j¶u²&e ëF›Å`¢õÀ–ŽnÑÚl“-ŒÕv‹ÕjKž…ÓéDQ2331 „Ç4L&ÚÚÚhllÄï÷ãóù8餓¨®®îq=333vÒ----Ö~hM7èª ç©¯&èk'èm'Ù‹ulB\M8O}|Ö·¡2¢· p÷Ýwãõzyà†dÎ@¸+ ¤ÍÜ{0ÏiuôzºNÕÔà±÷?¢=#AVïý ¦/fá”±À[gšÉÕ{>ŒÛçPéÜ»³Û©¿Ájï–ò…þÍÁ–áyÁ ¿7@[‹‡¤äDÒ³Ò)ÚQÒk¿ÌQÑ¡P˜ÆºÞ‹:|õÞ›0šŒlݰ“s¦ò•{näGßø%m-=ƒ‹¹ùÙLŸ3…HDåæo^×뜮”èéÁ+¾x1\s.åÅUüéÁ¿õ¸‡¿þæ9žüerò²AÚªz<­í±u€é/¼æ\Î^r:g-žÛsíäèÚ3çžÄC¹€|å¡XúÆ©§Dënüð“>´Ÿ|´ƒ3Ífì¤1X,æ¸t‘ƒÑÒÐ ãFcwÄÿüÕTÖ‘76‡Œ¬´>Ç:±À[MÕàêæ !„B!„B!„Z5Û:k´•,ûm´[GM¶NµÙܳ/¡nÓ›qmÐU«í¹çž‹•XÒ4 ƒÁ€ªª(Š‚ÑhŒ•ŽZµjUǼñ×»3™L¬]»£ÑHaaa\M·Ã­ 7 Ã2ë tÝ|>÷ß?%%½OWS{Sìë̤Œ^ûä¤äô9¾¬£þÛè´ÑGµ•Ûß`LZù䧉ÕE[±ýÝ£š»/M•ŒNËí³Ýb§ #·+3ît`ESe,“w”÷~,è8í5nr~Ÿ}&œT@iQyŸA¦[´Ì6®ÙÊ£?z‚Mk·áJMâÖïÜ«gv¨` „Qq¥$õú_ç8»Ã†+%‰„¤¾½~o€ý»°×XÐ-!ÉÉØIÑûÚ³u_\ÿP(L0"19±×µMiMsìZç O³Å †EÂ=ÿ²êÔy UQ”¸SƒF“‘tw*£Æô^ÿ¯SJz2-­q×cÏÙ”‚>ÇNìxΡ0•ö»ŽB!„B!„Bˆá1Åïgžn$´ãü;V“bw‘“’ ¬åååqòÉ'cNOÆšNSɘRqN‹-oÖì ÌiɸÎ;ƒsôýyUU5y&ùPÕè<º®Ç×.»á>wýbÌfsÜ{ô£OšEfAô`I$A×uÒÒÒØ»w/º®ÇÆ¥¤»Hs§2÷œSXrÕN_4‹‹>‹%–æRü?{wey.~ü;û–u²LBBÈB¾/‚‚((Šk]У¶U»¸ÕÚöÔ¥Öz~­õœnÖÓÓzj=®­»¸áR”EÙ„ì I&ëìËûþþ20LBHˆÂý¹®^0ïó<ïóÌ8cMî¹ï;Hî„é¤,’×nX2Þºƒn^¯—x€òòòA»w‡·“VW+ö;sƳ¥ò‹¸93 ¦÷º~Kå\2ý"F¥å‘Ÿ>ŠÊ–ør~I–DÎ*9“­UÛhhoìñ>UÎjÊ÷Q’UÌÌ‚ét‘”ÌÒú=T9«{\s¢¶Tneé¤ó(Ì(À‘œÉÁŽøŒ¡s',âëóo@Q¾ñç[¢™nÞ€—Òú=ŒÏÇÙcçóYùÆ÷˜;ú üA?;ëvõ^Ž2מÃKn ÚYÃÞÿÓ <ÃÃ6¯ßΔ3&2sÞTþñ—×ñÕëÌd62gÑ >_»µÇ{,¹l!SçLÄÙÔíÕöäï_  $ñÓJX~íRÞxöݘ5µ• =–G<Ò¯Ÿ~ˆÔ´džýãK1¥»³|>¦¡tkY\yL€ ¾¶NK}u#UûkcÆ~x}ß™¡·üèzæ,šÁçk¾à‰_=3 éhë$95‰‚’<>ùðóï‘W Üú½~:Ú§è^påb.»~~_€^ÿ`e6GŒÊ"¯(ØÞ»#ös½åÓ\uó%8r2=®€ý¥âÖ/¸`.Û?ßÝk_>!„B!„B!„CËb´Pí¬á¢©Ë+!Bá{ʨk« ??ŸÝ»w“qÉ9ht:Bm¨Š‚¿¾ ­ÉHÒÌI„ÝÔ@­Ù½obºƒÙBÓH«¯›nº ÇÃóÏ?D;¡¸Ýnjkk{\wûí·³eË<³núÜIA4Z-u• ¤¦§PWÙÐëý†ÂIÏx;:è¶oß¾c/:NÊ#…ÅÏ!#1=f¬$«˜ùcæõºvgí.öŒ n?ï»$˜bÆMz·ûn>û|kÑM}žãý‘¬·i£¦2«0Zµãýã{2ÇaKåÔ8kÑh4Üvîw°cß49©#¸|f¤aáú²OcÊK¼¶ù fÍbñ„Eq÷Ÿ’7™ï-½û/¹‡¢Ì¾#Á&½‰¢ÌBŠ2 Éí#Ãp 6|´™–F'¶D+7Ý}ÆC™^éÓöÍï_KbríÎÖ½·!n}á˜Q|íËQ…?ÿ×ÿEƒH—‡'~õ4ªªrñµK7¥xÐÏn¶˜˜:g"—^A4ÈÕmî93YzEäµùÉ7}ïÏVo`ÁgR21þŸáˆ¼,–\¶€k¾@ î“øÉŸ …1™|羯ÇdÃ8Fdpë}ß@£ÑÐÔÐÂÖ ;cÆ[lüx ßøþ ì©1ãK.[ÈäYãQU•7_ºÏ‰B!„â«ãÀ8ÿ¥=!„B1´ZÝ­4v4‚ª ‡ +a¶ÖlŽ{<l6¨ 5ÐÙ,èS’0fÚ±+"ÜåBñðÕ6bL?ü»à;æ‹­ IDATG.£&ÏŽ>®©©¡®®.ú¸½µƒæÆVôz=Z­¶×uo¿ý6©©©qël‰V’m´;;hªo‰þy¬û †Æ_åÓø«|4YYY=×àÝA7ŸÏÇOúSöìÙ3$û¤%ØùÝu¿Âf²Ñéíâã=ki÷´3Ҟ˼’3yyÓk\9ûr´-¼ü»ëJcÖHÉæÿ}íAR¬)tz;Y_ö)mžvÒÓ8£pÉÖdZ]­<øêÏ©o뽞Ioâ/7ý ‹Ñ‚F£¡ÓÛÉ-»•P¸ç2óJÎââéÆ\ÓiuÑ•µ­uøC±™]÷½øÓ˜ûJËãg—ÿ„$Kž6Wn¡Ëç&+ÙÁÌ‚éè´:jœµüô•‡èôÆ6øúüëY>-r†í5;Ù]WŠA§§$»˜‰9Ðh4¼±åMžZ÷Lÿ (³ÿºæaö5îçžþ¤ÏùQ4.Ÿþò6ŒF].önߊʘI£IJI$ ñ›ûÿDÙÎØÌ+«Íƒøé;¯?óo<·*îÞ—^ËW,¥³½‹ŸÝö+:Ú:û}®îŒ·§~ÿBo&³‘Ÿ>öC²r3Q…Ý_”ÑÑÖInþFŽâÞyñC^úûÊã|Eg¼}¶zs\ÆDÊ_þð‘ÛÈ=UUÙ±©”Úõ„Ãa²r3™>w2:½Ž†šƒ<ò£ÇpuÆg,à w^F£Áï °oW9í.ì)O(B§ÓâêtóëûþHME]Üþ I6îûí]8Fd )ݶËK^Q#ò"%,_üë¼ûò¿Žû¹ !„âø¥¥Ez¯º\ñ½m…j•ïFª‹äŸ?j˜O"„B!ĉënwtäŸÝÿSEQbþž””4œÇí7í¡n*±¡$ƒÁ@^^myéhMFœï¯G="‘c ºËKSUU…ßï?ÆŠØu½µ*))éñ~[Úp¤§ö¸f NZ©Éî ›ßïçÁ² €ÓÕʼö0w.¹œÔ,Ÿ¶ €`8È[[ßáÅ/sá”óI0'`ÔãÖ×·7ðãÜÏug®`NÑl.˜²4:æ xy燼ðÙ‹´{Úû<‡?äçã=k£ë?ÜýQ¯A7€krŸ™d¹öøÌ1 ±}ȪœÕüø÷³bîÕœQ4‹sÆÎ\ëòu±z÷ÇüsãËxñež\û4•-U\1ë2&œÈä‘£cåM¼±å-Ö•­ïõŒ'Syi%¿¸ë·\óíË;y43çO"¬=Û÷ñŸ_ë1øóÍ»¯%Ýa§lgy¯™Uo<»ŠqSK=®€oÿø~}ߣ½ÏN”ßà?ÿý¿¹âë1gÑ &Î;XßÌÊçVñé¿6 Ê^Góz|üçã‚«ÎeÁÒ9Lž5žÉ³ÆGÇ]n>ùðsÞxö]¼_Üú5«>£¹ÑÉ%ÿv£Ç0qƸÃ÷v{Ù¼~;¯<õV¯JW§›‡ï~”+oº˜ÙgOÙ»¡æ ¯=ý6›ÖmÄg,„B!„B!„b µçß‹ƒÁH±^Z‰™Rh´z|­‘ßÑ›í#P‚~8 ³‡»aþŽƒ¨á0:³%À>ö,Ü û¨l<ˆ.ÉAžìv;&LÀívc0Ðjµ¬^½šsÏ=—U«V¡ªj4›íú믧®®Ž?ü0æºN§#++‹¶¶6¬V+Á`UU9ØÒ6Ø/ÙÉÉx;:è¶{÷î¡Þ2jTz‰éx^ªZªãÊ+‹Ag`Dj6fƒ™vO;-]N‡š~Ùt²RXV\>ëìé‰i¤ÚR …C´t9éòÅgÈ}Y$$ÙHwØh9Ø—©õee4HsرØ,t¶uÒr°õ¤îŸ–i'Ùž„F]í.š½~#àh›™Ìì ô]í.œÍm„CýMF2³Ó0šM´;;hmüÁ !„¢o’ñ&†“d¼‰X¹üU\A7:ðIÃg¼_ýþpÿ¾-„B1”NÕŒ·Ê¿àV4Z=Þz €ÜE7bJLÚ”0¨*î†ý$ž‰ª„Э(!?!wî†ý$æM «f“GXÙ½{7çw]]]X,´Z-)))$''ãóù¨¯¯G£Ñ ª*ƒ‘#GÆ\¯««Ãív3uêT6nÜÈòåËñù|ÜÿÓ‡¾zo3gÎäî»ï&ðÐCÔ @UK5U-Õ^ Ohýp †ƒÔ8k¼¾¥ËIK—sO4t\î¯L°íH@†šƒÃ¶¿³©gÓÀ‚}^·ªý5Þ;àP[Ù0àõB!„BˆÓ‚Bº%tK³3¹¤ð"îYÿœ¾¯ÆÏ®B!„§ UE{DÅASRªª 3ÛÐhõtøcr ¢†ChõF”OK5Æä TE¡}ÿ&<)3±Ùlh4233ÑjµƒAôz=8N¼^/‡‰'ÒÐÐw=333šé––– Cžñf³ÙøÉO~ÂsÏ=ÇŽ;†r+!„B!ÄqŒ71œ$ãM T‚ÁÆ,Ç,îœz †>¨þÿÜüëá>–B!Ns’ñ+cê´3­…ƒ›ßBUÂh´ºHÆÛ!šC=ä³–ÓthΑº{É•——£ÓéP­VK8F£Ñ Óé…bÛ|õv"»p8ŒN§£®±ù«—ñæv»¹÷Þ{‡z!„B!„Bœ\A7«k?"Ó’ÁÍ¿ÉXûØc/B!„'UAa e;ú¼¼.ì#òú}t6ÇVASõ›–Ö€ãúżýöš›;HI±ÒÙÙBV–eËf³rå§(ÊáÞmÍÍí„B¡^ÇŽTÜn­­$$X‚(ŠŠÓÙÙcPn0 yàM!„B!„b°u#}ÈÃêW£»B!ÄéÄh±a´ØXpÃ÷ho¬U%ð±êþ€›nº ÇÃóÏ?€Õj¦¢¢eËÎ Vx晢32’¸ýöKñzýÔÖ¶ ÑÀÓOGæ­X±¨ÇñššfÌfK—Î`ݺ]\yå¼^?<òÂ=w ¼ !„B!NKÝ·¿ ´Z-z½`0ýƧ¢oiæH¹ÜVßÀúV !„Bˆ¡ÓRµdÇüî.B~/æ„dB_t¼¦¦Ÿïðã––vjk›™<¹ ZR !Á}¬×ëèèpÓÜÜÇãÃí>¼¾·q‡#•ÂÂl™™)Ññ¡$7!„B!Äiǖȼs.¤ªb/e»·¡(ý vµhZ­æ˜óöïÙIC]UŸs4-ù£Ç’_XBRŠˆô*hom¡²|µUå_šÀ _Fi–Hà­Åëæ“!„Bˆ£íY· €-oF2Úf]z›W>ï½÷bæ¯Y³€Ç“pXáŠ+æ£(Jô±N§%VõnÓ EªtÏ{ì±W{?’^¯cõê­ètÚÃ÷âÍAîxB!„Bœv ‹Çc¶XÉÊÅž_ô{]Z†æØ·``sŸãF£‰3œ‡=-€p8D0Àd¶`OÏ$)%•¦Æ:ü>o¿Ï&Äé&ýPÆ[‹¯e˜O"„B!Ž6&«OÀCaFé‰é¼üÚÿ——‡ÝnÇX8-¦œF£éW^ÞE{{;ápäK’Ý]vîÆýhõ4-Ÿ5)Ô­}Žœù×R·ö9Î:ë,JKKq:#_Î7niii¬[·.Œ …¼üòÚ!{îxB!„BœVôz# Š8°¯t@÷ؾùSš›ê{÷¸ºzÓh´Ìž.ö´L?Û7B}M%ªiž•3 ­V+A7!Ž¡»Ô¤d¼ !„B|ù¤ÚR0éXŒª5ÑëùùùìÞ½›±æõÙ.£ªj\/84`±çàïlŠ>>òÏêêjŠŠŠ¢· &°nݺ“ñ”£$ð&„B!„8­Œ,(Æ`0 ¨­*Ð=|^®Îõ((KZºEQøôãU´·ÎÖQ…úšº¯§›ôC¥&^ÉxB!„ø²ÉHÌÀb7S鬢±£1zÝãñ`³ÙŽÙÎívãóùâzÁ¹jJIÈ‹Á–‚  ÑÈšu1zk2Y³.ÆæÚGW×á/BVWWc2™NΓ>DoB!„BˆÓJañ8ª+ö‡NêÞ–ⱓ¨Ü_t}3›Í1?p aÑ[ð†å}!„Bñe³rë[=^ß¶myyyÇìWQQÄ÷‚kÜølV‡ª*pT_ìä1c(++‹¹Ïرc©ªê»÷`’À›B!„â´‘™KBb2ö¬Ì䉰§g`¶X¨†ý¿ªHIIÆårÓÞÞ>ÜÇ_î “ÎÄäôIliê¯F!„B1|‚Á ååñ•G>?Ô®ÛùçÏÂáHåí·7ÐÜ©8’•egٲ٬\ù)Š)×àp¤âvûhl¬eĈ4 Š¢ÒÒÒºuëÈÈHF£Ñ ª*Š¢röÙS(+«åàGmƒþ%ð&„B!„8m ©¡·«ó¤ïŸ–‘ €»«w×Éßÿ«J¯×£×ëIH°HðM°ªú=V”\Í5%W¡ÕhY[·žw=žEU†ûxB!„âX­f**X¶ì Âa…gžù€+pûí—âõú©­mA£ššfÌfK—Î`ݺ]\yå¼^?<ò×]·˜-[ö³xñ4¼^?55ÍX,Æ!9»Þ„B!„§[bŽì\ 6ÛL¯7à1’p(Dc}u¿ïg±ÚHMËÄl±…hkm¦³½µÏ5ié™´·G}'&§R0z,©ö tz=·‹úšJj«ö£(8èÖhKII–à›ˆúû®§èðwpþ¨%¬(¹š%WðNÕ*~»åÑa>B!„8--íÔÖ63yr:.fL¯×ÑÑᦹ¹LJÛíÃáH¥°0›@ DffJt¼[yy%%¹ÑëUUq8R‡äìxB!„Bœº³ÝÜ®.šj£×m ‰Ìœ»ÛÕïÀÛøÉ3IHJŽ»ÞÜXÇæ ã陵٠yÜ.ò‹Æ2iúœhi€Ä¤Ù¹cÃÚ÷ñy=ý~~_5*‘þ\f³¹_ó}>>Ÿ ³Ù,Á7€Õ`%ËšE’ñðgÑöã z‡ñTB!„b0¬Y³€Ç“pXáŠ+æ£( =ö*á°‚F£A§Ó …ãÖêõ:V¯ÞŠN§®[¹ò3EEUÕ^×  ¼ !„B!Nyz½¼üb*Ë÷ Ñ€Û`8þò"F³™Úê <®.TUÁžî Ã1‚Œ¬ÎZx¿¿’p8¿ÎhÀžžIñØI´·¶°oÏ:Û[Ñ dIñ¸É¤¤¦1ëÌE¬û×Û1g=•„´9de9Ž{mwÙÉH0®ç §8õýhÆ8+{.p€Çwü™j×àô9‡ûXB!„b¬8ã*ÞÛõª £…ÕïîBE!=!ƒ`(Àâñ‹X³wm¾vTU% F×vÕB¡0/¿¼6îÞCt ¼ !„B!NyÅè ÂáÕe1cúã¼½·òø}Þ¸€X^A ÓfÏ#19•¢1(Û½-n½Þ` -ÝAUÅ^¶mú$æ>í­-t´µ2{ÞâH0/+'&;O¦×ë …âƒ›âô1+sÏì}ž—÷¿:̧B!„ƒÉf²qáä ð‡üìm(cѸ…ø‚>œ.'ö;­žñ#ÆQ¯6âv»©­ýòüÜ$7!„B!Ä)¯ xuÕþ˜±î`XôUú±ú@£ ‹±§;ÈUÔcàM ‡AoÀÙÜtëÖPW…×ãÆbµ1"wÔ)xÓ+u46êŽ1ó°„„„h™ÉÆÆƒx;Íé´‘÷ΞÖ=Ã|!„B1Øô:=P€°fkÍvfÌ Õ݆#)UUpº[ÉLÊ ÉÝS¾ÿË@oB!„BˆSš#;—„ÄH¨Š}¥qã†ã¼KSc=öt‰I)èt:ÂáØ&Á`£ÉLKSCŸ%$;;Ú°XmX¬ ƒv¶/ ‘>\ý-ytÐMJLŠv³›Á6ÜGB!„ƒìñÕO Óê+‘Ÿ©þºöIt*jô@II ®cöHoB!„BˆSZî¨"TUa”YqãÖCÁ-³Ù™ Ï bßnëª{¯€ÿp0È`4>*C.à÷bKHŒözër(`7þs§¢„„ÒÓÓ º‰Ãœ>'v³tKÚpE!„Bå /dÛ¶m'TòÈ›ªª„ÔøŠeeeq׆›Þ„B!„§4½>’ѦÑhÉpŒèužV§‹Ž7 è Ó.› âÆ;Ú[IMËÄv(¯7f‹ˆô’‘~n A7«Åë¤8¥˜tsúpE!„BeïÞ½L˜0áKÕ{íd‘À›B!„â”¶ã‹ ìÙ¹¥×ñ¼‚ KÆãózølÍ{xûèåÖ—¤; ˜…Ãñ߯ls6“_4{z&Z­EQâæh4Zl‰I¸º:tŽSM{{;.—Kzº‰NŸ@2Þ„B!¾„¬V+•••Ã}Œa!7!„B!Ä)ÍãîêsÜç‹ÙE¡£½µ×y:ž¤äTÚZ›{7™-d玠®ú@sj«˜4}z½œ¼Bj*÷ÇÍÉÉ+ˆ–¢¬­®èóì§ º‰£µx#·4³Þ„B!¾l6lîc ¼ !„B!NoªzÌ)z½9 –š–ÁÞ][©(ÛE(ŒŽÛ™9wz½`0@yÙÎï ¨ª(£¨d“¦««“6gSt<1)…ñ“gÐPWEG›óŸœ§®V_$Pžbê»t«B!„8ù¶”î“ {N*A¿LÖDüž.TEÅ`¶ ø;ÿ|ÊvP³sS÷ÉÊJE§ÓQW×@FFä¿ýt:Ë–ÍfåÊOQ­V @ssû¡uöÇŽTÜn­­$$X8ØÒ6èÏ]oB!„BˆÓÚ±Ãn (aº:ÛIËp0nÒtÆL˜B{k ¿“ÅBJj:†p(ÄæÏ>Æãvõz¯ÝÛ7‘jÏÀžžÉ‚s/¢ÕÙ„ÇíÂl¶`Ow ÕjéìhcëÆuƒ÷$…8Ù͑ҮAE²!…B!¾læ\y ¡€€×MbF6]-ÔìØÄ´ ¯!àõð¸ð¹:úý-¶èº›nº ÇÃóÏ?ÀUW-D§Óò»ß½ À²eg+ÑÜí·_Š×ë§¶¶ž~úV¬XÔãxMM3f³¥Kg°nÝ®!yîxbäåræâYäæ`K²¡* ~_€®N«ß\Ï®-{z]k48óÜÙŒ›ZBºÃŽÁh ààuûp6µòäï_èsï”´dæ/™CѸ|’íIè´Z|>?——][öðþk÷¹~ʘqæ²r31[Í„‚!|^]í.þñ—×imî=â¢{çëv´1“GsÃíW+üô»×Z!„BœúEaÛ¦õ4ÔV2fâ4ìi™ØÓ1ãõÕ”nßLWg{ß÷ ‡ùlÍ{L˜:›‘ù£#÷JË  R¹»wl",¥…ˆcÕ[H·¤3Ó1ƒkJ®`KÓÃ|*!„Bq4½ÑD(à#)3U £„Ãìßøcæ-¥«¹NG8 ¥z?ÉŽÑu555ø|¾ècUU1 ÑÇ ft:]d½ŽŽ7ÍÍx<>ÜîÃëzw8R),Ì&‘™™24Ï}Hî*ÄqJH²Q8f¶D·‡êò:ÚZúþ…Å@Œ’Sû=¿³ÝEÀèu|Áùs¹áŽ«Ðh4=ŽïÛUÑkÉb3sﯾGN~vã~_ ÏҨѹüè—·a±YzWµÏà×Í?¼Ž¹çÌêuü—>ì5ðv¢{çëÖ³ÙDVn&Š¢ô9O¯×‘_’GZF*ªªÒÜè¤r_ j?JT¤eÚÉÍÏÆ–hÅíòP_ÕHscÿKH9Fd0bTf‹‰Îvå¥ðyýý^/„Bˆxû÷ì`ÿžýšÛÔXGSc&³…„Äd #~¿WW'Á@ÿÿ?9 °õóuìübÉ©iŒFü>/]í1%,…±^½è%´môñö–<_öa<‘B!„èIýÞml^ù,ùòa··w?ZŽp/?÷¼÷Þ{1÷í«Ãl6rÅóQ…Ç“pXA§Ó+h4t:-¡Pdîy=öjãGÒëuƒõtcï;$w=‚Á`àŽ;îà•W^¡²²r¨·_1I) ¬øöåÌœ?5Zc"Qì›Jyúÿ¤µypp…cGñïÜÞïùzøïlZ·­Ç1‹ÕÌ5·\ŠF£¡r /þå ê« ‚˜,&¬ ¼.o¯÷^rÙ"rò³QU•Wžz‹Íë¶ÑÕáB«Ób±š±%Zû<Û5ߺ ‹ÍBW‡‹çÿ÷UÊv–ãóø0õX¬½µÆN.ŽÝ>[½™U¯¬ÆÙÔŠª¨˜­f’l4Õ7ÉÞÃýº „F£aÉå ¹ðêó°%ÄÞßÙÔÆ³z‰mzOM.“Ç5ߺŒÑã âÆªö×òŸ#¯aoòŠr¹áö+)3*æz(bõ[ëyåÉ7 ä—tB!ÄÉâ÷yñûzÿï•þ …‚8›áDBœêÝ ¸‚.tVòIý§l8¸±ß_‚B!„'OçgŸ‘“œMaFé‰é¼¼éUFŽÌÅn·SÓÒAÐïCo4öÙ®Õ:±Çpáp$BUUB¡0£/¿wã~>k6 ÑhÉ:sukŸcļÔ­}ŽyóæQZZŠÓy8 ¢§`Ü`òÀÛìÙ³9çœs˜9s&÷Þ{/UUUC½¥øŠ°XÍüðáÛÈÉÏÆëñ±yÝ6š±'3kÁ4&ÏÏÿó~þýßÑÕÑ{Œãåîò°ssï% 4?m ¿¯÷l·Ñã 0YLüéá'i9"sÉëñÑîìèsŸI3ÆðɇŸóö??ˆsuºûÌ„2YLO(àÅ¿¾Á†6±w$S¯/gFönntò×ß<“éåq{û,1y¢{çë6P7Üy5 –ΠtÛ>ÊvìÇ`40ã¬)8r2¸ã§7óLJÿΖõÛãÖO(ä¿ø.£ªýµ|¶zí­¤Ø“˜}öt Jòøá÷òûŸ=Ñc–_^a?þ¯;0[L8›ZÙ¼~;î.7#ò²™1o ç]z6YiüáÿýU~é „B!Nißxÿæá>‚B!„è‡T[ &½‹ÑBµ³&z=??ŸÝ»w3çú»O¨œÕjé‡,öüMÑÇGþY]]MQQQLàm¨ yàmýúõ<ñÄÜrË-üâ¿à¾ûºz¨·_]³„œülÚ<üƒßãlj޽ñÜ*îýõ÷È‘Îå7^ÈS ^éÚõüîÇûœsîÅ ˜0},{wìï3H—šiàØÙÞ<ꯔôH ÙŠ=ÇNNMŒf•Uì=þõ©i‘½”U³¼âàï=|¯Û@Lž5>t{þñWøà5ѱןy‡oßs#ÓÏœÌõ·]Éî-{ãJ?^ûË1 ìØTÊïüsLpì½W?⎟ÞÌÔ9¹î»Wpÿ·Ž ž]ÇU˜-&vQÆÿÇ_bJŸŽ^YÀ¹©s&2ûìé1AP!„B!„B!„‰Xìf*U4v®òáñx°Ùl'ÜÎl6Çô‚sÕ”’;ƒ-%@£35ëbôÖd²f]ŒÍµ®®®“òÜOJ·×_UUùÖ·¾ÅÃ?̽÷ÞKMMͱž \{˧]È„œñ¤ÚR …ƒ4t4²±|oo{_Ð×ãºI¹¸|Öe´t5ó?ü/3 f°|Ú2ò3òÑkõ¸|]l¯ÙÉÿ|ÀY>mÓ󧳩bom{­FË…S/àì±óÉNÉBQ:}]¼±åMVíx¿Ç3Ì,˜Áâ ‹í("ÁdÃðRÝZÃ'eŸñáîÕ„•žS OôìGºvîÕgðÌúç(oªèsþñÐëuœ½ìL^~ò͘ D2ÿüËëÜþÓ›8ëÜÙüó¯¯ã=ÔÑ–hå¦\‡Ñh`Í»Ÿ²qMÏ´g/˜Æ‚óç „øëoŸÅÕéî×Ù²r3¹â›Ë ø<ùè qA«o¹”‘‘zê¡Ùjæ‡ßÚãýþøð“x\FæpõÍ—DÇ’’8gù|fΛ·vë†]|ðúá>ig.žÅ™‹#å!fcôúw^M(Š[¿ò…÷Ø»}ôñ‘gìîV2¡°Ç³;›Ûøûïž´½‡óuëfµYXvõ¹Lž5Ädî.{¶íãí?@¥÷L±yK"A·{«øpåÚ˜±P(Ìß÷<㦓”’ÈÌySY÷þ†Ã{&XÉ+Ê`õ[ëzÌHûèíõL3GN)iÉ1ý ÇæS8fªªòÔÿ#®ßàþÒ¬~k=ç]z6ç]²@oB!„B!„Bˆa·rë[=^ß¶myyyƒÖ®[ãÆ×`#h´:TU£~›‹yÜÓÚ”´dRe ©±¶iP÷Î× !ÉÆ}¿¹ GNªªR_ÕHÀàÌsg3kÁ4^æÏ_<€í›J{ œyÜ^¶nØÉÜsf1yö„˜ÀÛ‘ÿ‚?:®›×Óó—¦Ì@eïón×|Áy—žM~II)‰t¶Ÿœon!„B!„B!Äñƒ”——S^^Þ㸪*„C +θŠ÷v}€ª‚ÅhÁð¢¢`Ô›†,¿ˆ5{×ÑækGUU‚ÁH Ní%iiïÞ½CöœŽvÒoo½õŠ¢pë­·òðÃsÏ=÷P__?èûeFƒn¯nz6¼H(ÉÈIµ¥ðïþ€’¬b¾þÜÿâƒq™.5­µ„•0:­ŽY3øúüpºœ¼üùkì®/%‘bMéuÿŠæJòÒF²`ì|Î,žCYã>^ßò&U-UèuzR,É´yâûX]>óRŒO0ä·ï<ÆÆŠÏ£cÅŽÑÜÉ“]ÂMgßÈ¿ÿ§¸õ'zö“¥;óé`]s¯Á%¬PSQÇèñŒ, ¼¬y÷SJ&1÷œ™|ëG×óÈ‹–KÔjµÜò£ë1[Lløh ½½¾ßç:sñ,J&ÑÜèäÝ—þÕãœßÿì tZ- Ο˕7]LK£“‡îøuó ªìÛUÁWÞ}üÈßÀ–håÿþûŸ|ÞCæÞÑ™do¿ø!ï¿ÉäÊÎspßoîàï<Òc_4ÿQÙQGî}Ó®c꜉|ú¯ÏyîO¯Ä­=ºüä‰î=œ¯Àå7^ˆ#'·ËÃoîûUû#Y·&³‘»íJ.»ñÂÏ`±™úì=×P TæÄµ=n/µêÉ-Áø)%1Y€Ýºƒ—-Θl7€‘…‘ÏJåþÞ¿¨P½¿UUÑh4äd³û ¼ !„B!„B!¾ºl&N¾ÈÏÞ†2[ˆ/èÃérbO°cÐê?bõj#n·{H½Ž×I ¼¼óÎ;(ŠÂí·ßÎ/ùKî¹çu+g_N«£´~Ï|ò|ÌX›»ß¼ó(¼ñ1Æd—0iäD¶×숙 ‡pºœd&eòõù×ÓÔÙÄý/ýŒNogtNC{#½9Øù¼AgàëóþOö}ÆïÞ} E=Ȩ!þM`Ò›¸dúE¼´ñÕ˜ À¾ƒûyríÓÜqÞ­,;Ÿç>ýNWl‰Æ=ûÉ’‘€³©-z-¯(­6Ò7¬j-ªªâljcôø2Í?ÒÓø'ùÅ#)—ÏòKxýÙwX~íRŠÆåÓPs§þ»ÿ½áŒF—=òú¿úo÷¼ð( "QtEUñ¸½ÇÜC +1󺳧þ@¿ÖA‚‡öôyŸÃëñõký‘s‡ÒwCÁðIÙ{8_7£ÉÈÜsfðÆ3ïFƒn~_€§~ÿc&ý«ÍÒãzO—«ÍBZFj¯{èô:ìéñížx»þãÛ\påb:Ú:Y÷Þ z½Ž™ó§²|ÅREáù?¿·6=+òÞomn‹>—yŽèÙj …élë"ÙžDFV:prR¦…B!„B!„b(èuz¡a%ÌÖšíÌ,˜A«» GR&ªªàt·’™”A“»í¡„/‹“xXµjŠ¢pçwFƒoƒ 2è LÏŸ À;ÛWõ8§¥ËɶšL5•ÙE3ãoÞ.2“2I¶&óŸoý&&pu,]ÞÃÙ&–?~ø¿1A·ÞLÌÕdà_¥õ8çÓ}øÎ9·`И:j îZ=¨g?ÒoÞy½6òé­Þ@™-¦È}ÆÜó«;1êöKD0Äw(ëÉl1ÇÝÃï ðLJÿÎÞÍE+–°sË4h¸èšóøüéá'c‚=ÇrÎòù¤¦%SWÕÈÆ·œÈÓ_2…cFa4EÞ[[>/m …Ù¼~K.[ØãúвjÒ³Ò˜:w¯=óN\¹IFÔ3&`0Ðê´(áßùÒ­eüæ¾?rùrÝ­_ãÚï^»Ëƒ-ÑŠF£aéV>·Š›÷Äíý¬ú,ŒÈsðÀï9×Þ*~ñýß‘h²= ³ÕtŠ¢p×]wEƒoƒñâä¥D§džø>²Râû?´êí–Ÿ>ªÇñÉvªl©boÃñet¯X»wÞÀ±3r ÒS  ÝÓAëQ™lÝü!? íä¥dTZ^Ÿûäì1{ýøéàêxŒ‘·^8ÔsVY·Ð¡ñîùG«¯jäé?¼ÈM?¸Ž[~t="¥&ŸùŸ—¨«ê&¥Þ gé‹x÷åõØÇK|u9r2€HÖ]ks{súz¿¬yçf/˜Fn~6˯]ʇ²+´:-Wß|)# r_Óh8:Ôžž•†Áh"¥0½F“£Éˆ-ÁÊÈ‚vmÙ÷Þë~ï‡B=×&îÖ=Þ½‡B!„B!„B SŠV¯µcR:h´ * ªØÇÍÇݰ®š]Ñq5&kÎeü|%[ ž@g3¨*J8ˆÞœ@ÊèÙ¸ö‘ªéÂn·3iÒ$L&5ÍíL[v›W>‹ª¨heÁ%;rð»»pµ6aNH"ð£**]Ρ Ö [à àÃ?DUUîºë.~þóŸsË-·œð=“,‰Ñ¿ßwñû1?©Ïñ}ñý˜ŽGÙq¬ï>{‡·÷>Rp8£.ÁœÐç¼=ûP ø#¥õ†ÃoA¿ïp¯;ø`8”¥ä÷Åö ;Ò'~ΘI£™·ä Ö½¿õl<®óÌY8ƒ¤”Dº:\’ív ²%F2I]]ž^çx\½ÈK·í㣷׳pÙY\rÝùL›;‰²åM&LKjz2«^YÍ_[L(Š ’]ñõ‹XvÕ¹´;;øÃü•­vFßãc'sýíWòµo.'¿x$úå“1k»Ë{vVE~GôÑ3 ¸úø¬!„B!„B!ÄÉ=÷ 4Z=Þz €œy×Ðq`+éxñ9kÑ-GÌÿªBg´’=÷r:öoFg¶1bî×¼=í´íùÑŒÎh!?'Ý»w³dÉšššÈ;÷–Þþ3^­µ Ñ଩À`¶0eéìY÷s®¼™€×ÃëÜ=dÏ}XoŠÉ 1›ãK „MôïëÊÖãöýKh—ÏÕçx—¯«Ïñc9žõZM${¬l«ð¡²•Ýócï“­;ÈaM°F¯}ÿÚâæÙ»]½L“mÑ¿§¦%£ÑhŽ+kmþùsØðÑ–^{»‰¯.­îÐgKé£ä«¦÷!€gþç%œMm,»r1y…9äF2ܪ+êøÛoŸ#19ïh‹-íš›ŸÍW.àO¿|’ý»ÄŒïÙ¾Çz‚ÿ÷ø½Ìœ?• «Æ²kËá’“n——´Ìß…êòZn½üßãÎ×\t÷\B!„B!„Bˆ“BUÑêчeôtèjÆÓR19ãȨáZ%äÇßÙ„Õ\¯Ñ0&gD×y<ÍØl6EaÑ¢E¬=F«7àéh£«¹¿Ç…ßí"Å‘‹©0PÀOræˆèøPÖÀÛÂ… ¹ûî»q»Ý<øàƒƒrO—ßýû?>{‰úö{O´äàñ¬ï>»Ídës^¡q—¿ï á—¹\bcmYi}ÎsŒH™ß“¥—/bÊ9X× ˜0},^so>ÿ^¿Î’î°3z\×H¶Û©¨; Ìdé½ÿ™Õféu "Ÿ§·ÿùï½²šœü˜­&œMm´4:¸ìúeÔUÆö«?m †vgG\ЭÛÁºfk’“ŸÍ˜É£coµMäæôùY±&X£·ÆºÞ?+B!„B!„Bq2¸ËÑÌdq)( ·¼ Š‚ŠŠF«C G`ºÇ«ß{âPL#2ž5çr:lŹ{Mt¾F§®k3ÈËËãÑG% ¡ÕéPÂa4-ZŽp(w&ÞÀ®Õo¢Õé8ãŠo¢(aÞ|âÑAîÃxëºy<~ò“ŸPQQ1(÷­i­EUU4 yi#O8ðv2Õ¶Öž†Åhé±7œV£%ûPߺgíI=ß`:PVD‚^Éö$:Z;ãæX¬äägGæï­êñ>…cFqÅ×/" ó¿ÿù áþßÞÅ%×Ͼ]ìÝ~ìr›gŒÀãöR±§ç}ÄW[[K¤|kbr&³±ÇÒ¥Ù¹Ž~Ý+ Sµ¿&îú”3&Pº-¶¯bRJ¤„ì±2)»³»3çºØ[ÅìÓ=¾ ×µc&‘²”µêñ „B!„B!„¢o^x!Û¶m£¶v`qˆæ­½'ÆtÏ7¼?®„iøäŸùG® ƒ”——G+áHûUU‡z®|ÖŒ ‡6¼ü·þ<é»Váéºy½^xà˜çDy^Jë#Ù"gßë¼¹£Ï`ú¨©Hun;kvVÂh4Î(šÝãœ)y“°-¨ªÊ¶šCzžo,¸ÿºæaþ뚇“]2¨÷®Ú_KËÁVæwFs,ƒF£¡³½‹½;âß#Ö+ß¹÷Ftz/ÿ}%Uûk©Ú_ÃËO½‰V«åÛ?¾!ôè˘ɣØ·«âK%(®ºüðÿ9Œ™4:n\£ÑDg=™0},ßþñ |ãû+zŸû! !„B!„B!DìÝ»— zÿ©èÝI¼tÛ·oß ïñÚæ7˜]4‹ÅÅOÉ›Ì÷–ÞÎý—ÜCQfá ï?PÞN>Þ³€kç^EZ‚=f<ÁœÀó¯`cÅ&Ûãî1˜²S²)Ê,¤(³«±ï2|ñÖ?Þà¢K;¹8f¬db!_wþ¡yD³ŽôÍ»W–igçæRÞíãèõ÷^ùˆ][öœšÄ·~|MßÍ»FŒ ®ò«“)ŽÏÁúæè?ß‹®Y‚^¯‹?÷’dd§÷ºÞïõ3ûìéÌ;ï æ-‰ çæDr®\K[K{ÌøŸîÀï  Ñh¸þö«0 1ãZ­–«o¾£É Ü­ù"f¼¥ÑÉÆ#%P¿ñýØ3RcÆ—\¶É³Æ£ª*o¾ðþ±^ !„B!„B!„8&«ÕJeeåpã+餖šìºù|>|ðAÊÊÊŽ½h6W~ÁÊ/Þbù´ ¹uñ·™Wr»ëJ1èô”d31g†7¶¼ÍŽû²xjí3;Š™–Ëo¯ûk÷¬£¹«…ôÄ4Î*žK²5™†öFþ¼ú¯Ã}Ô¶vÕgLœ1ŽgM懿¼•òÒJšË´S<¡Fö ;ù×ʵqk—\¶is&ÑÙÞÅ_ólL¦šªªüõ7ÏñÐÿqSйøºóyý™wz=GwÀ¥;Oœš^~êMîøéÍËçgÿóï|¾æ B¡0c&1~Ú^{æ.»~YÚý¥øô_›˜{ÎL¾q× .;‹úêFRÒ’7¥­VKÙÎr^~ò͸µí]<ý‡¹é×2ã¬É”ÜÏŽM»imi'9%‘qSKÈ)sù¯E3äŽôÜã¯_’GVn&?q¥ÛöáqyÉ+ÊaD^¤ôìK[“Ù'„B!„B!„åp8ذaÃpã+é¤Þºƒn~¿Ÿ|={†6àõäÚ§©l©âŠY—1yäD&œ+oªà-o±®lýža \~7?yéA®™{ Ç.à‚)K£cÞ€—÷v~ÀóŸþƒNo×0žrp¨ªÊã<É_[̹—,`ôø‚h«Îö.þµro¿ŸíV0&¯}c9ªªò—_?Kg»+îÞmüí·ÏqçÏnaùŠ%ìÛUÁî/öÆÍ3 Ñì'¯;¾§ž8ulÛ°‹¿ýö9®ùöedtD3*]nžø¯§©«jˆÞô}\O¶¿?ú<ëšY|ñ| Jò((É‹®ÿè­õ¬|á½^û¸}ú¯Ïinháâ;ŸqSŠ9û‚3£cªª²oWo<·ªÇ÷h÷ßý(WÞt1³ÏžÎäYã£c 5yíé·Ù´nÛ ½>B!ÄéH£ÑH©q!„B!Ä)éD{´½ÿ¾T×(MVVÖÿ¤ytÐm÷îÝC½eŒôÄ4Rm©„Â!Zºœtù¾A+VLjÔllFú¶ÂJx¸5$´Z-™#ÒIH´áêrÓTßÒcyI!N”^¯#¿8[’•®7Õûk…úÿ¹Òê´ØÓSHJMÂëör°¾%Üÿ÷ªÑéçf¶˜ðû47´àõøŽk}fvF³‰vg­Ímý^+„B--- —+þ‹L§:[B"óιªŠ½”íÞÖïÿö_u[Úp¤§{âqòŒ·™3gr÷Ýwx衇NzÐ  ¥ËIK×Wï‡Õ°¦Æyz”ŽS…ÆÚ¦á>†8 „Baö—ðz%¬Ðr°uÀ¥Iþ5uÞ?àP+ý…BˆVX<³ÅJVÎ(öìüâØ IËp³‡0@0°¹Çë« {º£ßû Æ~ÏB!„BˆnÒ£mø yà­´´”]»vñÜsϱsçΡÞN!„B!ú¤×YP À}¥ºÇöÍŸÒÜTßë¸ÇÕs•ÚªrÚZúþÂYºcc'NCQº:Ût>!„B!ÄéMz´ Ÿ!¼¹Ýnî½÷Þ¡ÞF!„B!úedA1ƒ‘`0@mUù€îáózpuv hÏëésÎØ‰Ó¨«®Àï“>ÄB!„Bˆã'=Ú†v¸ „B!„'SwµêŠ}„áa>M¬¤;éŽl*ÊN~™~!„B!„'FoB!„BˆÓFfv. ‰ÉØ?°2“Ciô˜‰´¶¤½­e˜O#„B!„âxIàM!„BqÚ(,@SC-nWç0Ÿ&–Él!'¯€rÉvB!„Bˆ¯¤!ïñ&„B!„_¶Ä$Ù¹T‘í¦×pŒI8¢±¾ºß÷³Xm¤¦eb¶X ‡B´µ6ÓÙÞ:àóG«Õâõ¸i¨­ð}„B!„`ÔôLö´¢„UBþ0æD# ¨* Bö8;nZkº¢ãz“.2_Í¡ô-K²‰ ?LÐB£‰}¬**z“%¤àí `´ê£ûŽšžIÕ–¦áx †…Þ„B!„§…îl7·«‹¦†Úèu[B"3ç.Äãvõ;ð6~òL’’ã®77Ö±yÃÇø}¾ã:›N§#ôX RSU•ãZ/„B!„GJÌ´ÒÕì¥x^Aoˆ²µuŒ>kÎÊNFLH# ãrúÐG5#“Öš.ò¦fFÇ5"óôZFMÏÄYÙ ÐéµäMÉ@QTtz-A_ˆ½G~ÖêÞ·ao]ÍÞay †‹”šB!„BœòôzyùÅT–ïAUÕè˜Á`<îûÍfj«+(Û½½»¾ ù`=Y9œµðtºãûŽãÈübŒFápˆªò½Ç}!„B!„8’=7Öš.\-^\­‡¿ØÕä!9ÛFÀÂÝêÇÕâÅ’lŠŽ«ŠJsyGt<ãí `M6aÏKŒ®ï~¬¢¢„Þ áàá/vïÛ}ŽÓ‰d¼ !„B!NyyÅè ÂáÕe1cúã¼½·òø}Þ˜à]d¦ÍžGbr*Ec&P¶{[¿ï[4fµUåþ~¯B!„Bˆž8«#Á®£K<Ölk&R`CE£Õ „c®)[ÉŒÛùÎǵ: ªBôç!F÷³Ñ‘û&¤[ãé|¥HàM!„BqÊ+(@]uE\`Ko0ôû>>¯§×±êeŒ*,Æžî wTQ¿oY#F’)[YQ¶»ßgB!„BˆÞäèGâ±{(Ì( =1—7½ @nÎHìv;udbJΤé‹wt¶`JÉ"cÚRš6¿iw¨É›)9“°ßMÀÕŠÞœ€ €ªèrZç@£Õ£„ü ªØÇÍÇݰ®š]“2L=t_ :“•°ß-¦¤ Â~7!Ÿ 5Fg²¢„d¥X°ÛíT6:™¶ì*6¯|UQÑh#çJväàwwájmœD(àGUT¦Œ+¦´´§Óy²^ò( ¼ !„B!NiŽìÜí}¥qã†ã¼KSc=öt‰ÿŸ½ûªL?þ=Ó[zO !$„Ð M@ª¨ bEÅ•µ-¶u]×òs¶uÝ÷}wWײ®»®‹®X°cCiŠˆÒC Ò{2Éô™s~ 3ÉIƒ„XžÏuqeæ<§Ü3Lf&ç>Ï}GF£V«ñù|n“5x8Õ•eXë{,AAA~¾bÌÑè5:Œ:#Çj‹ƒË @AAš°W’˜7EöQºþ RϼܿÞì_ãs;pÖ•ÎÚ¼Z=ñ£Î¦~ßFR&^ŠÏíàð‡ÿ@ʤùH* ZS”»ÚÔº–Ùn £Î$ÌÉYøÜÜÖjšŠ÷“3‘ÆÂH’ ¯VOBÞÙ‹J£Çëlf€¯‚‚μúNfßön‡º’# IÔAk02jö|ö}ó/¿·ÃÎöeO“••L¼-Z´»ÝŽÉdÂn·óÆoôÚs/o‚ ‚ ‚ ü¤õËÈ@Qd†ßfÜd²`0™1ÅL1ÛMAA„’‘€1ÖÀÑÚ"*+‚Ëív;f³™¦¦u¥Dd GR…¦Š$•ÝŠ»©ŸËŽÏeG„QŸ‰ìõ ‹JŽ) *.¸Ü^s ]TBp8¸ŸÈÜMÕH’š†C[ÐE%aN„×ÙŒQŸ ((>/^· ÙçÁîôÇ  Òh±7ÖÓT]ŽËÞŒËÖLtR?ô-xÝ.¢SƒãþÇØÒ[®¸¸§Ó‰Á`Àélù»­7ˆÄ› ‚ ‚ ?iF›$©HHJmw=•Z/?‰¤€Z­Þö¸Ý®˜ífkn¢²¼¸“µAAAºfÅŽOÂ.ÏÏÏ'==º½ßpìË¡È>’ϸd™£Ÿ½€"û@’TjŸ·Í>$µ†Ú=_!©ÔÁíl‡Qi TnY²]`¼bÓ‡Tï\2^½c%È2Š¿ñ’¤:Þ3Îß7®Z«%==Ïþö0²Ï‡$©P©Õø¼ž6q©5Zö¬ý•ZMvÖ@héïýÅ_œä3Ù}"ñ&‚ ‚ ÂOÚ®íß³o÷¶vÇÓ3s˜3§ÃÎw_ûÿst2S­=‘ѱ¸œ|aþ@mÍ`4‘–>ðϰ ×\AA¡'y<>LÂèsCz¼U|ÿÐýo-Ûù{¼é"âBz¼ÆO¶Ç›ÛZÝ&^ÅçCmR£Ö›P©µøÜd‹„ãÇu8š5jT§½á*k¾ëñçX$ÞAAAøI³Ûš:w:ýI6Y–il¨kw=µZCdT õuÕaÇõ#)ý2(=VØi\™ƒ† R©ðz=+<Ðéú‚ ‚ ‚ =E­3ý(z¼•¬]oʤËPd/Š×CSñ’ϸWC{úˆxœu¥¨uF¤Åu©7\oPõÊ^AAA~,º0ÓL£Ñ2iúl¦Ì:Ÿœ¡£ƒå+Ì–&N=F‹ÇãæðÝîO­Ö0 +€c…ñzÚ–IAAAè-îã=ÞÔ3ZKlÈX Ç›³¾ ŸËŽÛZ.:‰ˆŒ!=Þœõe-µêñæ¬/ ÛãÍm­Ž+²Lá-Ø* 1§ î?ØãÍnEö¸ÚÄlïsÙ‘½Nô‘ ((ÁãzÙAKo¸úÒ£¸ìÍX«Ê‰NêGÆÈ ½òÜJÉÉÉ¢ž‰ ‚ ‚ ü ÅÅÅÐÜÜÜÇ‘ô­ìÜ 5»­™/?~;ì:*•Šc&1 k0²ì£¡®·Ë…Þh$:&I’ðy½lÞ¸–ʲŽûµefaäØI¬úôlMÖž}P?G?/`ÀœŒ>ŽDAAN] t|럲,#ËrÈíÈÈȾ 7HR©Cz¼Uný¤K1N6c IDAT=ÞÙ‡¤R“4nÈ2>³ão›?:¾qè~%µ¦Ão'nâø‰#@{¼7\áÑ£ö†«¬©')>攟ÇÖD©IAAA„NȲLþ– ”—eð🗚ÊòŸeÒMAA„†@²*Ћ-`Â%×Q~`§·£IRáq9±V—“fŠÏÜ.99µZ ŠFZBB”GE_3wîVB–TÇ{¬UWûÿnJJˆô¯Ø2ž”ƒÍæ¤nß—X,FüÛ×ÖZCŽçv{e…éÓGqà@ »wß>«µ–¸X &“»Ý‰×+c6GãryYV¨¬©ïñçT$ÞAAAøY;´o‡öíêÒºU¥TU”¢7±DD¡Õêp¹47Yñ¸]ïà¸]Û¿§`ç4ZÝɆ-‚ ‚ ‚ÐktF3:£™i×üކŠP¼n'+Ÿ €E‹a·Ûyã o¸+®˜Z­â¯}€¹sÏÀ瓃 ¸Ûn»‡ÃEII ’¯½¶ €_übfØñââj -³gå›oöpùåÓp8\<õÔ›!Ç‹Ž¶àp¸(.®Æhlùûêò˧áv{p:ÝìÚu”ñããóù0õ¸Ýcp8\ÜùÿþÞãÏH¼ ‚ ‚ ‚ t“ËéÀåtœÒ>|>>ß©íCAA¡7Ô$*)—­ ¯ËÁ…×í Žãt¶ÜW®¥¶ÅbðÏ€456ª«±ÛØl-Ûµ7ž”ÃÀ)¸Ý^£ƒã'/°¼¨¨’¤¤–’‘:·ÛƒÏ'³iÓ>ÆÏÁãñå_~âþz’èñ&‚ ‚ ?S¢Ç›Ð—D7AAá§äÇÚã­«Æ_| [W¼Žìó…Ÿ3g<ƒµZ…,Ë|ôÑF|>µZ…Ï'#Ijµ ¯×¿ýüùSCÖ;q¼5FÜ×EMF–e7ƒŽ?ü6ì~V¬ðïü}»Eᢋ&ËZªÕ*J+jz¼Ç›H¼ ‚ ‚ ÂÏ”H¼ }I$ÞAA„Ÿ’ŸZâmprv· ™ÄGÄóî–÷HOO'66ÝÀ¼v{ÀŒ=š††Ž=²ßìKc«8„J£E’T(²Léúe¤M½ŠÒõ˘2e {÷¶ö´<ÎÊšúO¼‰R“‚ ‚ ‚ ‚ ‚ ‚ BPŒ9½F‡QgäXmqpù€((( wØ”{Àù|>EiÓ  Œ±i¸¬UÁû­;vŒ¬¬¬Ó–xë ª¾@AAAAAAøáHˆH 7e0u¶:*+‚Ëív;f³¹M8N²½ÍfÃétR\\Liiipysñ^T:Zs4*)ŠäñšÍfšššNÛãì ¢Ô¤ ‚ ‚ üL‰R“B_¥&AA„Ÿ’ŸZ©ÉöhµZÒÓÓ9|øpÈòÎzÀHR©Q”ÐÕàÁƒ9pà@ðyìm}RjR«ÕòÛßþ–÷Þ{¯M-NAAAAAAáçÁãñ´IºlþàÕûsæŒ'))†O?ý€êêF’“c™;w+VlD–T*aƤ¤l6'%¤¦Æáv{e…ÚZ+ QH’t ‡ÃÅSO½ ÀÂ…³Ø¶í³fåáp¸(.®ÆhÔ7{ö8$ ²³Óp8\TW7°k×Q&Oʶm‡P©$ -sçNÀçó¡Óiinv°{oa?öNo6làŸÿü'7Þx#øÃ¸ÿþû9vìX`2™4hP›å>Ÿêêjª««‘e¹×Žß‘K.¹„«¯¾šªª*î»ï>ú$AAA„»÷¿À¸q÷öq$‚ ‚ ‚pêäãU[ÿôùÀëUðz%¼^ GÁã¬Ö¾Ž¸wÕÔ4PRRÍÈ‘™¨Õê1FMc£êêFìv'6›“¤¤LÁíö’˜8|¸œœœ~ÁåEE•$%µ”ˆ ì'!!šêêFÔj‰M›ö‘”CNN?ššì ˜(x<>œN7nw×ÊbvW—{¼]xá…ÜtÓM444°xñbŠ‹‹{% Áƒóç?ÿ¹ÝqÇÃÊ•+yûí·©««ë•ÚóòË/“””À3Ï<×_~yZ/‚ ‚ =IôxúÒ3Ïü¯½&o‚ ‚ Ÿ,+Çn-?}>¯W>ž|“ñxZþY­?ÎoÝ¥V«ðùdæÏŸŠ,Ë|ôÑF|>I’P«Ux½m“_ŸOF­VqÑE“‘e™+¾;þÜ*!ÛöûþûÚìW£Q#ËJpB—J¥ öÞƒÞéñ¦¶X,teÅýû÷ÓØØÈ”)S8óÌ3Ù¼y3Ö^HÉÆÇÇ3{öìvÇÕj5999L˜0Õ«Wãõz{<†ödff’••…×ëeéÒ¥455u{³fÍâöÛogΜ9S]]Ý ‘ ‚ ‚ BçL&n·»#~ŽÎ;ïvî\ÕǑ 5 ]ºNZúž¤êëA~ŽçqB~p²øÙòÏåÒ÷j<çŸ>‡£Wò8ÝHpíÝ{Œ}ûŠƒ÷Áÿœ„X.ËJp»@ÒíÄíãá¶Ün}ÌÖ·lv'“ñdZ»:-5ÙÚ'Ÿ|‚,ËÜzë­<ùä“Üwß}”••õh@­mÚ´‰eË–`6›2dóçÏÇh4’––Æ‚ øÏþÓkÇ?Ñ3Ï<ÃgŸ}FEEÅI¿Xcbb‚¥4ÍfsO†'‚ ‚ ‚ ÂÜ CÇü–¥5K¨òôNµ!Aè)²ÚÀþ¼gÐ9ËÉÚýH_‡#‚ ´²ÿ~† FIII_‡ò³Ó­ÄÀgŸ}†,ËÜvÛmüñä¾û¼¼7bÃjµrèСàýüü|ªªª¸ë®»ÈÍÍí•ãväÀ§ý˜‚ ‚ ‚ ?u.—«¯C~d´*=‘ú8êå?™Ùac#ÎæÊø{PIjÒTƒ)vê|#Aè#^]'¼FsÜ8ŒÖØe=jϳa‘΋FoÆ^'’Ý‚ œ¼@‰É–YnÊñ>o­ËM¶Üîm&“‰£Göúq„¶ºxX¹r%²,sûí·“o=[X;wî ÞŽŽŽn3n6›¹øâ‹ÉÉÉ!55•ŠŠ vïÞÍûï¿¶„ÎСC9÷ÜsÉÎÎÆçóqàÀ^ýu.\Htt4¯¿þzðÅ9þü`²ïÅ_¤¶¶6¸ŸÁƒsÞyçѯ_?bccq:ÔÖÖ²gÏÞzë-Eá¾ûî#===¸Í‚ 8÷ÜsQ…'Ÿ|2$®¼¼<¦OŸÎÀ‘$‰¢¢"V¬XÁþýûCÖ Ä´k×.V¬XÁ¬Y³ÈËËÃb±°víZÖ­[×åøAAAáÇÀ¬âúQO¡‹åhã.ÞØódçu RÇ€¨áXt±xe7ÍîzÊ›Pï<=ç:F[fpUÒ}¼]õg¾·~vÚŽ-Ý%kL8óul±c05ìfðú?Ú¤[BÎ&Ýô*޽Ÿþû¿x¦¯CúÙR©5Äš‚9.êëi®.ì넟8ƒFGœ)‡×M½ ‹ÞH”Þ_’¿ÉåÀ겟Ò1†$¦ãt{(k¬§î4÷ÖNJJâûï¿?­ÇüN*ñðå—_"Ë2wÜqG0ùVYYÙ“±…•šš¼]\zJNN÷ß?ñññÁe)))äåå1sæLî¾ûnl6[plþüù\{íµ¨T-µ¨³²²˜6mZ° äDzÿI“&°téÒàòóÏ?Ÿ_ÿú×mbMOO'::š7ß|€ & Óé‚ã$žÏ×Òùä“61%&&2tèP¦L™Ûºuk·âAA„ÎhÔ º.üá“Áå‘z? A~–úGæ¡‹`@Ԍڞî÷a7j,œ;ðz†ÆO;^Pó-xö”bíŠÓX&-FBâýêçDÒMèU*• NÓé8©íIÃÁIÿÆ;CÓaó 4îúŽòôIy*ÿ|]Ú˜‹Dâ­é#˜|Ëë|÷ÏkEâMèu)‘±\?v6•E¼¹s#“3™“3€Õ‡·óÕ‘í¢ScS³±hÍl+9ÌÚC§¶¯îúòË/Oëñ„'xX½z5Š¢pÇwðÄOpã7öT\èõúà¬6FÀ¸é¦›‚ã­_8:Ž{øøxš››Y¶lLž<™³Ï>›~ýúqíµ×ò /þÛu×]‡$I(ŠÂúõ멪ªbäÈ‘ääät9FN|ܵµµ|üñÇ”••O¿~ý(,lù€øðÃ:t(Æ `ãÆ”——‡Ì6;çœs‚I·ï¾ûŽU«VÅUW]E\\‹-bóæÍTUU…Ä‘••EVVõõõX,víÚÕ­øAA¡#éI^Þz¬’”xo§ëúdøl£™¿-â@±ö4D'ÂÏI±uÍî,ºhŠ÷œTÒM­Òò‹ad€¬ø8X·•ZG)’¤"BGfôpŠ z!úPê®Nzµ¤aC㇬o|¿×)ü<3–éÓ§Ó¿T*n·›}ûöòÅ_t«ÿOÙл°&NCí±2èÛkиj;ßè¬b×JL¼ •FGÙö}Ž §‘ÓëiuÛòÀÕj\ºã”o²,`0N9˜M:•©S§†[¹r%›6m Þ?÷ÜsIIIüe %7mÚDjjjp6X ñvÕUW!Iþ+pŸyæV­Zøgœýþ÷¿o÷¸'JMME£ñ?[¶laùòåí®»téR.»ì²`âí‹/¾`óæÍÁqI’¸îºë())áücp6\ee%O<ñ:ŽñãLJÌz ؽ{7O?ý4¨T*E!##£Ëñ ‚ ‚ B{2’½¼ùh×’njÌ;ÓÆô<—?˜Ì¾¢^òM¥RÿžéKZ­½^Íf%à¡‹lžF^Üv‡¿ÇÛI–ƒ‘05˜t{}÷ã”4í?a )xÞ 7-Hü˜ÕQT¸òAÍ ½~¼ƒéÓ§3hP•••¬XñQ_‡ó“pùå—sæ™þ*I‡††¢¢¢9rC‡ãµ×^#?G§û±ÅŽ¡|ðoÈØ¾Cóÿ‚îªý_³ò‘q¨õfìµÇú:Ah—).!sîÆןmËîÂVs´¯CúÑszZ’l$œÓÓ’lsxÚ¶®„Ž$ßs8ÅÄÛŒ3¸ë®»°Ùl,Y²¤'âê’þóŸ|ôQè¯Ö³Ô$IbüøñÁûDFF‰Õj ÎklldõêÕÁuEáèÑ£]N¼UTTàõzÑh4Ìž=›ŒŒ vìØÁöíÛÙ³gO·Wbb"‘‘‘ÁýŽ3&8¦×냷û÷ïvûçž{.Øk/p¡'ãAA~ž4jxó± Râ|lÞ«gÃNc§ÛDše®?ßJ„Iæ?VréâdÊjNùº¿“œœÌã?ΪU«xçwBÊ¿wä±ÇC­VwºÞ‡~r¡à‰t:sçÎeÚ´i¤§§#I^¯—ýû÷³f;úê+‘„„NxdµŽ²“Þ¾_ä`ªìÇÂ$Ý”^ÿ=Ì6Žf¨y"ïU?‹OéÚÅ ?uÆ '''§ËïÍBÇ&NœLº}õÕ:>ùäÜn7F£‘K.¹„ Î`áÂ…”••Q]]ÕᾊG<ˆ"©‰¨ÞH\ñOgv¦«¹šÜ3÷ÒÒrˆ‰Ib÷îõ}ŠÐKŒQÉôj­¾“µ…®p…Ìns·»Lºë¤ÿò $Ýìv;>ø GŽéɸÈÏÏçƒ>à /$//€ˆˆˆ6_~[÷~»ûî»Ûݧ$Ièt:ââ⨮®>¥/ÒN§“§Ÿ~š›o¾™ˆˆrssÉÍÍåÊ+¯¤°°eË–±qãÆ.í+---x{ܸqŒ7®ÝÇÐñ ‚ ‚ ?O*•BJœÿäç–}ž~;ªKÛ•Ö¨yèºz’c}¼úP—=LC³ªó Oƒ¹sçË„ xë­·º¼Ý!CBzD·gÙ²eíŽ%&&òðÃ+v466ÒÐÐ@bb"Æ cذaÌš5‹'Ÿ|‡ãäúï‚Ð9£Æ€Ýcí³Ί¹€£Î9:Ÿmt²RRR9ãŒ3øôSÂEøùÐh4Á–&»wïæý÷[’e‡ƒ¯¿þš Î@§ÓqÞysxõÕWÛÝ—-v MñþDqÊþ¿õnàB·dgeÁ‚ûQ©T86ÚÖ×! ÂBë²’ÁoaÊOþ˜eŒI¤|_²OÁëòÿMgˆÐ!I (€)Cbi¬°QWì/ÝmŒô÷½ Œ«u*d¯Œ¤’è?*¢mU(²‚¤’ÐèÕ4×øÿf2+ÓKD¼‘æ:'×— 3i‚ÇÏ“HѶŽ/òø)8©Ä[ éæp8x衇8|øpOÇø“bRŒ¥¥¥¼ð h4.»ì2Ö®]KiiipÝúú–&®Ë—/§¦¦&ì>›ššP·Û^¯'&&æ”ã\·nß~û-Ó¦Mcüøñ 6Œèèh233Y¼x1¿ûÝïºÔK­õc(,,äóÏ?»^wŸïžŠOAAºã呤ÆùXt•ì~^¾¿Š…$át÷~é¶Ž fΜ ÀgŸ}vRûø×¿þÅÎí7GT¢8‘Z­æ¾ûî#%%…ŠŠ žyæ8ø/°›>}:7ß|3C‡å†nàÙgŸ=©øá§È¨@§ ßæÂæiÄ+w÷äXß¾EkÈ5ù«õ|oýô”ö¥ÕjÉÊÊ"&&“É„Z­Æd2C¿~ý‚!??ÿ”cïHTTéééDFF"Ë2ÍÍÍaµ¶ŸàÔh4Á @þÇã?]¥Ói‰ YW–eº´€ºººàmNGNN±±±x<***),ìüBòèèh €ÅbÁíöPSSMQQQØyZ­–ˆˆ\.6› ‹ÅBnn.*•š}ûöŸ‹ÅÂàÁ¹h4öíÛ¬ÖÔšÙlF¯×c·Ûq:$$$žîo+R[[Caaa‡3‡ FT”ÿ‚™•+W¶Ÿ={vðöèÑy¼óÎ;Øíö°ûªÎ\€Î^JTå×í3I¥ÆŠ¢È8êýçóT ƒÎÄ“†«¹ki¶Ú¢÷£H@­myðØð8[z"§µ¹—NâWVb³ÕÄW,æ•WüäÎ×jôbŒÁÛI¥ÆmoÀZ¾¦Šn§Ö™Ð[üï5í=‡j½%GCŠÜù¬Vÿëd ƘT\M54–t«$¨!*‰˜ô< ‘ (²Œ«¹–ú¢m8­íŸø×[âQëŒÈ>ÎÆŽËkMQh ‘!¯íp$•†˜ôQX³ÐèLxœMØjŽR,En¶së×¼>"¡ÕãJÆë ý]uÛêðºlÆ I*bŒÅ’4I¥Â^WLíáïðy\a×Wk è#ðºšqÛêÑ[âH̤RSµo]ð¹Ô[âH<•FGå¾µ8+;¥¯ÉŠ‚ÛçE§Ö„íñæìF·äˆX’ÌÑ€ŠZ›•¢º*dúvævD¢‰¦jƒ¦¤áqx9p<–}f*µG­¤‹ÃçñÑ\ëD­m¹¸0mD<’‘If|^—ŒÇéEgò¿ofMLA–dŸL]qS0ñV{ÔŠËæÎÔš–ýŽ_¾¿ž¦êŸÇ……ÝN¼˜t;xð`oÄÕFYY+V¬à’K.A£ÑpË-·ðÐCÇ‹ŠŠ˜4in·;l´÷—™™I\\Çg÷îÝÁ±®\Áz"·ÛͪU«Xµj’$qýõ×s饗¢R©5jT0±ÕzvÙlÙGqq1>ŸµZM||<ëÖ­ÃfëüͲ'ãAA„žôÄÒ’â¼Ì›lgì`ÏÞUÍ-ÿ“ˆ¯[«Íœ9“É„Ífcýú“+ÇT[[r!`W3†ŒŒ þò—¿„\T§( ëÖ­#66–… 2}út–.]ÚáÉjAø99gÀ5 K˜vlùÞÿåPýk†G¶qtðö^{û¥i;¢V«9÷Üs™>}CÛ¤¤,Ëx½^š››©ªªB£é½~›qqqÌŸ?Ÿ!C††­ÔSTTÄš5«Ã&þúõëÇwÜÙfù!CyøáÐÖ&ÅÅÇøóŸÿ6†pû¹÷Þßãr¹ÈËËãŠ+`4¶”Kv8,^|_‡é²Ë.'77·Íc²ÛílØð _|ñžVý€rrr¸ñƛغu [¶låºë® ¶q¹\¼ðÂóDDDpÍ5×¢Óé‚ËŸ{î9Š‹C“ \p!'NäóÏ?cÆ ,Xp%ÇY§®®ŽåË—³woAØÇ0tè0¬Vk›ý§¥¥1|ø6lø†3Ïœ‚J¥"77—mÛÂÿ.Y&U¹è^õ(Cd"ç>üŠì㣻Ðoì%Œ¸ätæ–Ī¢È”lyoß‹Ïã »Ÿ± Ÿ&1wFðþ¡µÿ`÷‡¡5F2îšçIrVÈú»Þ„Ã_ý³Í~¦ßù1†¨ä°ÇøøÞœ“q'0ñ&ÿÌÀož¿œšƒß¶»îÀi‹vÁø<.>{hT¯%Þššêxíµ%,Zô?˜L‘,\ø/¿|õõ]ï©Ö™vÁýdLüEHr3À^WÌ‘õÿáÈú‡}©#Ïcì/ý3!Û{OçŒE/ðùÃy&¿ú¹ˆ‘óÿ€ÎÜ2qBQdJ¶¾ÏŽ·ïÃçŸ$0Çe0bþã$9 ¼'Õmãàš¿S–ßö‡ÓÅàs~‡ìóðÙC£ðØÛ&Æ&\÷ 9S¨9ô-ߟLFF:'N¢ººš7ÞXÆÑ£G{8úPÑÑÑÜyç]Çg„¹)(( ®®•JETT4ƒe“‘‘Áõ×ÿŠ¿þõ/…Îjr::t(x?-- £ÑˆÍf£¼¼‰ÅÍ/ù(/¿|v{çß#$•šÉ·ü—¸g€¢P}à¬eÈ>/úˆxbŒÃ’8á=„¤Rqpõ ½úXÒÏX@êȹÇ_'ãuÙIÌNdJ.ýÇÍGð1è IDAT‘ÀƆqhŒNaÚ+Ð[âð¹T¬ÆVWìŸy•Bü IÄdŒaÂõÿ䫿Σ¾h{Èö¥[?dð9¿C¥Ö’2ü\ŽmZ6F)šølùÕ’m†]gè¼ûÈ9û·XË÷R{x^W3CÑý†>š¬i‹°$d²ñW·Ù¾±l/*ÿóJkŒ$*ÍŸLo(Îo3ãÍZ®_i‹ô3·à‘Tj åTïÿŸÇIlæx¢Ò†2òÒÇ1D$PðÉŸÂn›1†Ô‘s©+ÜBÍ¡oé?n>–Ä,ò®ü?Ìñ™4–`5©£/ *u¹çÝÍw/]ÓaL?oïü ­JMy“†´ÝãdéÖ/hrµŸÜ0iõ\7ö\âÍþ™ÅGêÊ(µÖaPkÈŒIãÒaS°»]ø¼}Ó»¹ö˜ÿ}çÄdWq~5þ_¹HÙßÑÍþJ鮚qI%ùKLJRØö]ûו„#p|K|ç½Â*ºœx $Ý\.K–,aß¾}½WX6›×^{ßüæ7Üpà lÙ²‡ÃAee%/½ô·ß~;jµšo¼‘o¼›Í†ÉdB’$Þ}÷]^yå>úè#Î9çúõëGTTwÜqÇIÅ”””Äe—]Öîxee%[·n ÞÏÏϧ¦¦†øøxrssƒ¥cn¹åJJJøÏþÈ#ÈÈÈ 77—gžy·Û¢(èõz¼^/ .ìòL¸îÆ'‚ ‚ '+P7âÄÓnÄ O%ðÎ*Èéïáç4SY¯æé·¢OwˆŒ=š´´4Ei·´ûé(Në™­o÷4“ÉÔá̆®š8q"[¶lÁëm¿LÒ©2-X,Ñ8Í47‹ ®¾>¶œ¯µœüŒÒ'pëØîô™’P¡nuO þl½ ÉÝýDXwÅký½ê«=áORufòä3:t(/½ôó4ßÿ{öìáÆoâÖ[Ã_ÿú—6 ¬žtÖY³‚I·¿üåÏmÊíªÕj&NœDlllØäPEEÏ=×RZ÷Ö[CNN‡â•WÚξhOee%/¼ð<>øñññ¤§§sñÅ—àp8x뭷صk'yyy\s͵í–T4 üêW‹°X,X­Vþýï—C’—’$‘›;„©S§òùçmË7ÄÇdzzõjV¬øð_ô|ÕU IIIáÛo7°|ùrE¡¼¼œE‹n 3s`»kôèÑ455ñôÓ yþ²³³ùõ¯oE«Õ2oÞ<žþù6Û&$øKÓÕ××…,OMMeĈ|ñÅ80›Í$&&†Áei‰ÏÐÔyyÎŽäœs;G¿}]ï/ –¶+Xñ$~õ/’‡MÊÈóˆxµGÚÎ)øøô7Ÿ±¿ü¦Øþ >÷wÄKɶÙýÁ#8­Uœ}ÿ×X³p·3Siã É€ô W0檿v)~EöRºí#Nû©£Î'ÿÖ 4Å¥Ý$Å›ßéÒ¾OUiéAÞzëI®ºêaââRY¸p ÿùÏýxÚ)!2|¶?él{ãΰ‰¦¤!g‘>árõ¯^‰ýD©#ç¶yH+Ôä]ùgÒ'\Nâài¤ŒœvÆÚ ³n &ÝÖýen›2™*µ–Œ‰¿ÀÛ¿MÒ ü ,kù^"S†:j^»‰·ä³‘TdŸ‡²·×[âtÖ¯8öý[l{ã®6ëD&&çÜÛÙ÷ù_ÂcÏGOoÇ œÀÔÛý}·-»kylj¶Ö"’1úŠ?!©Ônx•]ï=Œìkùž™=ó†_ô9gÿ–Š=«¨;Úö|±9~W¿ÀžüåBÇ\õ4‘)¹ýö5v,_ Š‚µ|g,ú7q™ã»__:Öš”’…Ãu]ûÌŸŒN§Á`ÐãñxÑj5Äx¼Ô×û‚–3»öøÔ»_Áh4ÒÜÜr<³Ù?;W’$ JãÀE ÎR·ÙœÁ8ÛÍF</•KÛþnžª.ÕT<1é¶wïÞ¤«V®\ü²ÇUW]ûòË/¹ãŽ;ÈÏÏ6 6›ÍH’DUUUH²Êãñp÷ÝwóÍ7ßÿ@T…Í›7w«ÏƒV«eýúõmaŠ¢°aÃ|ðAššZ®h±Ûí,Y²„={ö³Â‡ƒ¬¬,ÀÿøwÜÁÒ¥KƒuÈu:]0é¶k×.¢£»~‚¢»ñ ‚ ‚ Bw ’Ý<ç¬äk[_ÛŠxÞYIî ½–¬6×>‘HEÿäöW4rÕ9ͧ=Ö¹sç°cÇŽvû°õ¦’ÿ öÄÄDRSSî(#V__ϱc]ïaÒ]÷Þ{/‹/fêÔ©'½yóæqÏ=÷pë­·ö`dm©ÕÔjM0''ãHC>ÿ÷ýuÁ‡üå¯ w…,ÿ¿ï¯ã“CÿèõxLj/2›¯ýf™8Ñ?ËbïÞ‚°GïÙ³‡òò2t:gŸ}öÉÚ÷³ÒÒ’°ï­>Ÿ ¾ &¡N§‹.ºµZÍ /<ÏÎù!Wè;áoçœs.ÑÑÑȲ̿þõ¯63EaïÞ^zé8ágÍ9>þxEð~s³ÿ3O–eÞ}÷Ý`åF£±ÝÖ'n·›¿ÿý…6IËC‡±i“¿Lé A9múáétº`9ËææÐóBsæÌÁårñÕWë‚ñ!’ZóêZÞ{5ëtUñ–wýe[%ƒdŸ‡ü勃=¿2&^Ù¥}Y’²Éžy Å[ßcË«·¶)Õ×Q‰ÀSQ¼å]tæXM»N`¶›«¹†Ê}_õJá>¼>ø+Š¢––Ãå—ß‹J¥îp›ÈÔ\ÿ E¡dëûaשܻ†ÍKÝkå2Oîu¢È>v¾÷Ppö`æ™×†Ý6ðxJw‡íM'û<nx5˜@ §d«&kâàih a×Iéÿ^Yµï+Üö¶‰‰ˆäAÁ^‚Åí<¯ÖŠýlyõ74W\O¾®vშÔZê‹¶±óB’n‡Ö¾è/S)IdŸuKØ}xÖ`òÀÕì¿HE‘}ì|÷Á`_¿Àr­1©“×ÞY”ÁL^j6›Š÷±·*ô{»ÇçåÓ}ßcuvƒAɤÇhÔvWtt4V«µÍñ J#++•#21tL˜Ë˜1ƒÈÊJ%;»åo­öÆO6ž®ètÆÛ¸qã¸ë®»p»Ý<ú裄¯ÝSöïßϼyóÚ—e™Ûn»­ÝñÇóÀ I‰‰‰FjkkÃ&—l6O=õ†ÔÔTª««q8Ü|óÍÁu¥)þøÇ?¶ÙGII úÓŸ$‰¨¨(¢££q¹\ÔÔÔ´{ujQQ÷Þ{/:ŽÈÈHjkkC¾øy<–/_ÎòåË1™L$%%a·Û©««k³Ïp1j|‚ ‚ ‚ÐUd/:+0¶ú>›çsò¢£‚[ŒÉìSµü!S^£áº'yç‰J,&™Çoª¥ºQÅ—›L§%Ö””òòòB.¶3Œ;§ÓÙ­²îñññäääƒËåâàÁƒagr´öý÷ßSUUEbb"<ðÏ>ûlÈ óÌÌL,X€Ïç㥗^êÕïìË—/çþûïçöÛoèv¿»¹sçrýõ×cµZùðÃðå•zJ`–›ÅÑh Y&}Mo0¡Qkñúù8¤/§¢(ȲÜ&þ«æÇŽ À¾}{9v¬ã÷õöȲr®Çëmy]ø|¾°ËÕj5²Ü¶l^eeeHë’Öòów0iÒ$222Bžû@ÒÍœ–Ï“””TFŒɪU«‚³þ¥·Þ¦5¯6*x[å=µ“È¥ÛÃ'` eÔË'vÀb2Æti_ ·­ŽËïY.Ë^Ù‡'L2¤'ÔÛAsÕa,‰Y¤å]HÕþ¯Û¬“–çO¼•lý ìŒ8h¹°¤»Ôj >_û3Í÷ïßÄG=ËùçßBNÎxæÍ»•>z¶ÝõÝÍÇ_7’Dò°s)ÛÙvÙéÖÞëÄël¢rï:Òò. fÀ$•¦Íóxdèù÷¡ÒèHvÅ[ß ×è-$žv|ݶåf[Ç<üªœ\oáS¥5F’˜;€Â ¯†-Ï þYyÉCg‘˜;I¥&ÂÙ²­Üê}Enõzl½\¥ÖàëâçâÉ:ÙÏáS5(> Õñ™SÛÊ…]ǧȸ¼n´ÚÞë±Ú[} –¸DÔ ’¤BR«Áç .w65âózÐèôXâiª©DoŽÀãtâ²7¡ÒhB’´'T¹PP«[’«f³Eúúfââ"ñùdÌff³­Ör|Ö›…²²îUðx<Ç?ŸB§Óù€J%átº±Û]x<^<žÐ÷…pã‹­6ü'§ªÓwö½{÷²gÏ–-[ÆîÝ»{%ˆÞ (JHÒ¬#^¯7x%©N§cÜ8×f³uyŠ¢ÐÐÐЭ~in·›ššš×±Ûív¸NWœL|‚ ‚ ‚Й= !I· ¿u×óCRÈò}E:núŸ–>X…V£ðì5 ½*ù4´=˜;w.’$QYYR^199™;3êêê.'Þ~ùË_’šš,SŸŸÏ3ÏðÍi›áÖe¤å]€Fg—NsuhùÓ¢Mo‘:zCgÝ»Š¢Ë(Ùö!õÅùÁYY±×Swt+±™ãH5·Mâ-yØ,T>·ƒò]áKÎZ+öS_´˜Œ<²¦-"¦ÿ(Žnü/»¿ ;C®·ÄeMD¥öNÕ¶ßrÈZæ¯r×ò¼žú9éÞvjŸÃ§&=ÊŒWöQÙ|zŽÙ]WÝ“@9þ9 ©THø{´¥OéÞÈ^/Š¢«Òn$IÕ&‰ybêÚÚ&45C‡f ( ß~»YVP©TÇ÷ç¿}âE%õ»š¯¬¬$**ªÍñ¶l9püxòñÞs’$Xïûï÷†ïM&Þl6‹/îõ@úœ9s8p ûöí£¡¡¸¸8Î?ÿ|RRRxÿý÷ûü ¢ ‚ ‚ üòµß3d”Ï‰Š¶=ß¾Ýeàÿ=ÇÓ¿«Á Sü=zùk·ÑhdÆŒ€¿|}ëïù&S÷gÜEFF²aÃ*++ñù|äææ2räHFÅ£>ʽ÷Þ‹Ëþ¹)++ãŽ;îàÑG%;;;øü'FŸþù^OºðÄOðàƒv9ù6{öln¸áxä‘G(.îþUä~þ“»11þäl ]@WÊN’q‚ÐW,–hT­®¼6™#»|ÂÏîk"Z“ˆQe>©c9r„#F0xðà°'µôz=ýû÷ ¨èèI£« xûí·¹è¢‹Ðëõ̘1ƒ3f`µZÙ½{7[¶láÈ‘Þ-£ÖžÝ»wµû~Nëö],ýCк“Ñh ký¾˜U•’’¨Q£X³fuH[’Àl —+ü{±ÚÝ’$ðj#ÑŸzèa9›ªý7$ ÁÒ¥R‘Å[ÞëtÞP¼ÅŸxÓ™¢IÈ™FåÞ5Á±@™É¦Êƒ4ïì“øNÔÑÌ:WS5ß¾xã®~SlÒò.$-ïB¼n;UûÖQº}å;? I.ö%W«’¢:s4T‡ŽW¬aÇÛ÷2ü¢‡Ñè-d͸‰¬7á´VQ±û Š·¼¶‡à‰J¶}@læ8‡ÌD­3ás·Ìö ”™,ßýEÈò}ÿò¯Ý‹Ä <ƒØÌqÄfŽC‘}Ôn¦4ÿSŠ·¼Ók%QŒQ-IùAgýŸ/üï¹FÛò¢5ý8J{ŸÊçð)[レ\Ž^Ë!4ûô)m?89»ÛÎÀ„Lâ#âyw‹¿äizz:±±±yg™Ƙ"#.¾–ͼ (HRË¥AŠ"#I*O™Mù]4V– È Uñ¸ÍÔ•úg‡=š†††6%š³/]Œ­â*IRQº¿ŠÒõËH›º…ÒõË‚ë]}õÕìØ±ƒÑ£G³cÇvíÚ@\¿L\NPЛ"0DDá²5A³`­ö÷äKJJ¢²²’È~gßAEVÐŒxÝ.Tj ys¯`ëŠ7ñy½¨5ô¦ð¥dOU÷ç2ÿ„L›6‘#Gû<´¶eËÞ~ûí>ˆJAA„ž6sæLL&n·›Õ«W‡Œx¢²#²ô mNrŸuÖYüæ7¿¡ÿþÌ›7wß …{rr2·ß~;ÙÙÙìܹ“o¿ý–I“&1räHT*wß}7Ó§Oçé§ŸöÜéM{÷îå±Ç㡇ê4ùvöÙgsã7ÒØØÈ’%K‚=ëúBge¶á‡Î.û&FõÉ•8Z¹òsrssILLä²Ë.çƒÞ&Zt: \‰ÉdÂçóñÕW½ß_êÛo7°sçN&LϨQ£IOO'22’É“'3yòd °lÙ2ª««;ßY²Z»××ÞlnI„vç‚€¾(›©R©ÐjCËDºÝn|>jµ£Ñ?ûqöì9x<Ö®]²nà³°½ÏM«Ä›¯UÙÉž¦´š]¥ÖñÐyBÂÕtz_SöÚcÔn!6siy†M¼on¶À¶m_°{wÛ2•Ñjux<¿>ssÏ`îÜ[Ðhtìܹ–Õ«_ípýºÂ-¬þãtÒF_@ZÞ…Ä:ÎDêȹ¤Žœ‹£¡ŒoýžÊ½k;ÜÏé Ÿð: çè·ÿ¥|çç¤O¸œÔQs‰N!2‘“ɀɿ¤¶p3Û—ÝÙᬮÒí+qÉ£¨µ’‡Í¢tûŠãÇ48Ô?«²½2“Nkëÿv) 9SI?ŸÄ!g¡·Ä—5‘¸¬‰ ›·˜}Ÿÿ™ƒkþÞݧ¡ËZ'Ñ2&]Õ¥m3ä„öéϲóž¦Ò–'#Æ^£Ã¨3r¬¶åb¹PPPÀ”_ÞAá¶ $fFöy1X¢˜tŨµ: ·m )k(¦¨ ·m@g4£3š™}Û#¸v,± 4T”ðÉ_ü·|>Š¢°hÑ"ìv;o¼ñ†ÿ€cÓZ’æÉÕ'L²NII¡ªª*ø3x›xùxÝNÜN;Å»¶ 3™ÑŒ >ó$´¥{‚ŸwgŸ}6ǤéÜìa¸vÜöfœÍ˜¢ü½P']q3²Ïƒ×ã¦xW×[ tÇÏ:ñöÅ_ Ë2ÙÙÙX,¬V+ÇŽãÓO?mS†@AA„¶v¨õœÕN¯—|µ¡Íl7€É#œüßmµH46«ºZñç¤I’ÄyçøJ^DÝ™ñÖQߢ5kÖ0kÖ,rss™:ujØÄ[zz:K–,!::š÷Þ{×_÷—|úòË/ILLä’K.áì³Ïfܸq<þøã<ôÐC§%ù¶ÿþN“ogu·Ür õõõ,Y²¤Ý~C]çÿ¯¯÷—÷ïÊŒ£Ñ,3YWW!oBŸknn)qe·Y»¼m¥ûÙÆÑ$ë2NêØ%%%¼ôÒK,\¸É“'3zôh**Êñù|¤¦¦a6›ñx<¼þú»ÜFãT577±fÍÖ¬YƒÙlfÈ!Lœ8‰ììl23rÛm¿åOz*ØWì‡ÈnoyÏm¯ßÙ‰J¥B¥ò÷·9ñóBQjkkILL$""’äädFÅÚµkÛ|fúµ7ËÏÐÔÒ¿È9˜ÈªÞéU¥Òµ”Nõ8ºþûÔWŠ·¼Clæ8RFÌF¥Ö"û<˜ãÕo8(J›ò„'òzÝx{¡”ã°aS¸à‚Û$‡oçÃÿÖ¥9>‹c›ßáØæwPkõÄgŸI¿±ÓoÌE£S9ã†WØðüÔÙÔã1w‡Z×’lëèuâj®áàš¿spÍßÑ™cI2“Œ‰WŸ=™¸ÌñL¹íÖüiV»e]Í5Tø†ÄÜ餎šL¼%æÎ@£3á±7RÕÅDdõõTX$©ˆNEêȹ ˜´­)Ša>’Šƒ«ŸïƳÐu{Ë °ÍKÝá ½€@ÙɺSù>Uîã½ì4½X~÷T%D$`Œ5p´¶ˆŠÆŠàr»ÝŽÙl¦òð^RrF ÕP©5¨u:¼ngpy}Yn‡ƒ%’”œ”íË'*){c=MÕåhõ¼®–ϛ͆Ó餸¸§³¥*Esñ^,ýrÑš£‘½n$µ–äñ¢1E‘<þB*6û{:ʲÌÌ™3©©©aæÌ™¬\é/ãªÑéñºÈ>‡6­cò‚›Ñ´`k¨Åek¦² Y–™4i_}õ©ÎÆek&2!•¦êr$µŸÛ Q ÒhÁíÂçñ÷×~Ö‰·uëÖ±nÝ:À?¥¾7— ‚ ‚ ÂOÑKÚhÆûœDœÐ¤ÝÄsº˜6ëçf¸yé÷Õh5 .·Ä O%öz·¼¼~s³6—Íf£ªª’øøT*ÿŸ½;¢¾?þš½Ïìæ¾p_rƒ VåµJQk«V­ÕöKûÕZí·­WÛo­öø¶µ—mmµÞZ«ˆ'¢à…rߎ„$äÞì½;3¿?†lX²! $áó|ï·ÇÓÆæÍ›ùðÃNû ³>Ÿ7²qãF.¾øb/¾—ËÅ´iÓãj‹iÚÚ:/À§¥¥ âHzçØÔ˜χcÕÖÖ’‘‘AFFóç/ ²zõûqm\.Wl¦_wïïÆP#–öÃñ¥LêÇ#ˆguiÿ_Ëá@¯‚ƒ­fË Æ]ýSŒÖ$2FÍ¥nç»äNº€Æ²O ´ôýu}ªŠŠ&põÕw#I:jkËxá…‡OêF9¢~ÏûÔïyŸÊµO3gÙ¿Ñ錸è[ƒx³¦äÆ~zzwcAØ×LÕÆ—©Úø2Å/côâ`qeQ0í¬ùk·ÛUo~•Œ‘’9ú"ôF r$HÎ-{Úámo È}»ž¬ª -·Ðrp Öü• ïz[JÅ_ø/¬~¼Wu û*ÐÚùºöÔ–Ò^·¯ß÷1XNåÿáSå‹hŸEf+§!sþIY±õ„Ë·mÛFAA[ßzEQ$PŽÖƒÕéõGë»)ÌùÊ·ØðÊ“D‚E‰=?%étz=r4þù_^®Õ[\¹reÜòºõ¯Âz´ÚªÒm­Åßþö·D£Q ÑhçûÖá½ÛØ´â™Ø×¾ð—„Û/_¾€êê'ؾò?qãÔéõ(²Üã8úÃ9x;–º ‚ ‚ Bßҹ͒Å7íLQ‚U•z34%Sª‹Ÿ)å‰ûŽà°)È |û7ilØ3PUb:]pÁ€–úä†nè²>==Ð.<>øàƒ¼õÖ[¬_ß÷‹Jv—­$I8ޏrǧ  Ðf¸ugýúõ¬[·Ž3f0wî\žxâ‰ÓV{zÿþý<ôÐC<øàƒ±à›$I,[¶Œææfxàêêêzè¥ÿY­\.í¸º gšPÐÏÉ\æ+ lCEEBb”}: ­}«S%I7Ýô5Ün7O>ù[¶l9‰Qœk×®eñb-õ^VVV­¡ãò¥$õÐlÔÖÖÒÞÞŽÓédäÈ‘g|F¤ÂÂΓ••»¬ß»·” &——GAA~øA\]8€’’‘±ßKK»Ÿå’Ôð1Açp<ç£êŒHJÿ_KK.œ@óÁÓSëôT…ý­Ôï~ŸìqóÉt…x›¸è9Íä@ÈÎƵ×þ½Þ@KKÏ<óP¿üŸÙT¾Oí^\¹£qfHÜèØÏ*’î”÷y")C¦àk:Hð˜zo½U±ö)F/ÖRã9³ŠOØöðö·˜°ô &£æQ¿ë]²Æ\ôœf²'¡öjw¼Í° ¿ŽÑæÂ’”$ë"îó`ïß ËÖ¡¥Ð¥ IDATÈtz#é#fŸQ7—+óÏ_ŠÉdeãÆ·8thwŸû8Ùÿ‡OÕaO㳊0èôd8’©÷öm9Ǭ;û½Ïõ€5óÄmvÔ8°L¿ÃÑ™¥ŽÙ§6ï'ZÍ:½ÝФӡ„#茬EùøDÒéPe$ ¢mvv…Ç‚kÎw™­‚ :ƒA ¤I:²GŒáð¾í¨Š‚¤Óa0šñµ6àÊÈ! #G"dCÅ–µñçâµOé¸7AAANÉ!‘XÒOØ&É®ðä}GÈJÑîR¼ÿo)¬\ßû§ÂlÖ‚{z½žñãÇwÛÎd2ÅÖŸLЭ£>Ÿ/n]ffç7Ûž‚Wååå̘1‡ÃÍfëÒ×@*++ë|kjjâ8méꎧ×k_]EÐM8›´Ë-ìð~ÄxÇÌJºœûx³Ûí± Ö±éœƒÓéÄëõv{“À±ï½IŸÛ‘îÑíî:sz ©ªÊ–-[¸à‚ 5j4EEE±;ø'IÒi¹1Âétb6›¦ä>} ¥ ;x°²ËúíÛ·sÕUWÇ2=_çT§ÓÅnP©¨(?aJåŒò§8Rô5"æ4Zr‘Rµü¤ŽÇž64áò”¡SpfjAÞ¦ï;Tmü7Ùãæ“=öR’²Gâʃ Q³-ñL“’’’ÍW¿úcÌf+>_O?ý ^oâŠÇÓ›¬H:Ñ`÷5 fís[w©£‘Î×¶Õ•Iûq}I’ŽÂ_îÕxl©‰Óð:3G6L{Þ×íL|#“Ù™NØÛ¤ÍhIÀ`êü ÚSJÓh°ú=ï‘3~¹.C‰1Z“zêi<ðiÇaIÊ8apÐ`é¬ó œ¸&e8ÐYóКœƒ§¶´ÇýÃÑcØýÙã0|î78´áß'ü[Ÿ.:ž›nú))ÙŒ=›?ýé¿hnî¿Ì ©¼¹óóüÄœa¼³¯k­°\W)ö$Âá3·܉8òG!I:Zö­ë×~í%CAÒ¡F£„jê‘ ¬EÚ Šz›%$ÜØ ¼;–p8L$)éÈ‘r$L4ÂhÑ2˜ä™‚ªÈ(ŠL[ýáXàÍds  ‡H9Û½½¯›åóùðx<$%%qóÍ·°oß¾cÞ/T‚Á ìØ±ƒÖÖÞ]|?‡“;ï¼ §7Þxƒ²²²¸`”Ûíæ+_ùjìñÞ½=_(>p`?'N$??Ÿ±cDzsçÎØ:‹ÅBJJJ?Ô™ìÞÛo¿Å¤I“p:ÜvÛ7xñÅغukì¸L&sæÌaΜóùÓŸþØm]´þâv»¹ýö;xúé§b1I’˜?>#Gj³ÕÖ¬Y,w½°ëõzùøã™7oŠ¢àv'Çfgët:®ºêjòòòxë­®é˜emÛƒ«î}Ú².¢¶xÉÕ¯#©}¿˜<æŠ!Güì¹XÚ2Gzç}åw€6‹¬bíÓ}îw°ÔíZE$àÁhMbÒu¿Ò–í|ç´6Ždn¸á'Øí.Âá Ï>ûššz÷‘$Soü®Ü1ì~ãQj¶¼—>Ñ`¶3já÷°§ àÈÞij@­ 6dæWØñêc­îl&]÷+2FÎíõ1¹üGDü­Tmìœ9hMÎeê×GÒé‘#Aö¿ÿ§.Û™i\xçk<õìyãQË>‹›)fugsÞW‹=îîxŽU½éUrÆ/"kÌŨŠöY¤fËkÝö: Ÿw£~}«~OÅÇOÆÕ’“$yç]Eþ”%4Wn&òv×Þú=G°$e0â¢oÒT¾!îy–\0‘¶š] Ó_î\þS2FÎÖZÀìo>Ëæg¾³v£Ñâd蜯‘=n>ÿáKÈ‘¿¡ÃíΈÝ #C†ŒûÜÞêÚ›)k:̰ÔfŒ¢¢¹ŽÒ#U±õÃR²YX2 IÕŸÏÀ*Hú¨a§‚dС„t6+J(Œª(œvPUTE%TÛ™*Ûãñ‡‰ZuÈ‘0f›“¿]K‰)ËI§9z4%¦JsMel{‹# ƒQûþ%é:gä¶µµÅ¥´ìO"ð&‚ ‚ €‘$ø¿ï42}ŒöåýùU~󂻇­úWO£‰µ´VÑh”ŠŠŠnÛ™Íf Ø¿Âõn·›éÓ§ðÉ'ŸtY_QQ«Y0}útvíÚ•°“ÉĬY³­îÚéJ3y¼ŠŠ n½õÖAÙw""è&\;úÈ´‰=ÖÑyáäòßBV;Ÿ#›ëVòqUßf †C¡½lj_Åy΋¹*íÛìóo"¨ô.Ø®ª*Ï>û ·Ür+&“‰±cÇ&lwÕUW³fÍV¬xm@ÞO p:¤¦¦²lÙ·ioo§¶¶–H$BRR¹¹¹èŽ^äZ»öJK{¼­[·ŽóÏ¿€ÌÌL¾þõÛ¨­­¥µµ‡ÃAVVÁ`Ÿýì»ÌôËÍÍåæ›o 9Y›-7cÆ Æh5mV¬èù¶v¿ßÏ?þñw¾þõÛ°ÛíÜtÓ×X²ÄKcc#F£‘¬¬,ôG/DΙs>¯¾úJïOØIpß}÷SUU…ßï'''—Ëh3ÕÖ¬YÓíöo¾ùEEErçwR^^N{{;¤¦¦ðÞ{«Ø·oocÉßñS<sð»ÇP?ü6²ö?Þçã ûZ˜xÍ£Œ\ð]<‡K1X$LÔjî(2[Ÿ¿§Û Õä/ÿ©Ã¦c0ÛcËæÝûn,°òÑï—l;ñ¬r½ÉÊ¥÷ÇÏTÒ-±ß/¹m\ fãSߦa_÷Aq%æðÖ×)œy=É…Zý»ª ÿ>áú[vvNg Š"óÒKRSÓûT‚¶”|’rFaMÎ弯>Æ„k¡­z'+&{ I9#1˜µYY­UÛØ÷îïöãk¬äHédŒ¼as¿Aê°´×íÅœ”IÚð™H’ÄŽWbÜUõj\¾Æ Îûêc”Ì¿‹¶ê­NR‡ÍDo4ƒª²íÅïlë:?¹p"fg:¶Ôæ,û7¡ö<µ{‘#A,I¸rÇ é´×oåÚ§8Rº¦Ç±Ôï^E4ØŽÁâ$ïh ¬zÓ‰ÓLJ:ƒVÎdeÔ¢{¹ànÚjvl«Ç`¶ãȆ%Iˆò6²õÅ{{‡ª*ì~ýa&_ÿ[Ò†ÏbþƒëñÞ¢ÈØS ±¥ä±kÅÃìï]¶õ5V²é©eœwÃH.œÌEÿ³OÍn‚žzLöd\yãÐéäL¸,.à9PÚÚ¼±¾~Û ¶8ó¼^ºŽ¯O]ˆÝdá+“¾@ES­~.«4«‹Ê–#øB!2¬§wˆx›‘ôy#ûµZ¤Õƒ¤×ÇÒK-ˆ-é$ÔÀ÷í‹O«/Ûs´ÝÑšs:–bRÒ¡¢ÆÆš7zªªRµsc\»eeeývLÇ7AAAÌ}7µ°x–vyÕF+?úKê ¨«Þ\ˆ¶X,Üwß}óâ‹/òúë¯Ç]ðÍÌÌäî»ïÆb±à÷ûyýõ×»ôÑÖÖÆêÕ«¹ä’KX¸p!‡â½÷Þ‹Û¿ËåâÛßþ6¨ªÊË/Ÿþú0‚p¦²’°] ×Y ö¸Ç&ýéIeÛ^nxŒaÖñ¸ \›qÿªû‰vѨJKKùÉO~ÌÈ‘#q:“Ðëµ—^¯Çår1zôÜn7]tƒžÿü§ÿƒ‘»víâç?˜‹/¾$6KÌéìœÕ¬ª*UUU¬^½šÍ›7õªÏH$Âÿø–.½†±cÇ’Mv¶6+B–e*++°X,]oF£‘´´´¸eV««UKAuì¸zRQQÁ¯ý+,XÀ¤I“q88‡«««Y³f ›6uM-Ößyå•W¸æškâjºii17óâ‹/‡»Ý¾ã|^yå™6mÇw¦Úòx<¼óÎ;|òÉǽ‹Õ³—¼?§jüƒÔŒ¹gãgØ[¶öéx¶¿ü#R‹¦3tÎMdŒ¼0¶<ÐZËö—ï£vG÷3ï,®ÌØÌ«öcRvND’t˜Ý§È6;âŸCzcÏõhm|™Â™×ò6Q_úAÛô§ýû7ñä“?"99‹ýûûöœô5äýG¿@Ñù·P8óËØS I-š×&Ðz˜ŠOž¢lÍ_#ÝWÓÚôôw˜rãI/žƒ;<îüñ ª4–}ÆÎå?¡­fc¿øR/jÀ}ò§ë9ÿ. g^#½3=i°­Ž¯ûÛMÔíZ…ÁâÄ7ƒÅI°­Ž¶š]±Ô“Âà±¥`KÎÅhM"òák<ˆ¿¹ªç aO+Ä™9%ÆS·¯ÇYˆ'b´8qåÅhM"ÐZG[ÍÎ^?O$I‡3«KR:£…ˆ¿öú}„}-'5–/üàœ™ÃÙóÖ/ÙûNß"&{ ÎŒaín”h„`[-íuû{LWÙIgÀ•3 ‹; U–ñ5Vàmè>sÃñÌŽTìi…˜©Düm´Þ3(ußæÎ½ž¹sµÚë׿Á›oö}í™Âmqà6;PU•ÃíÍøB!EEQ@–U¢Q…hT%U‰D"¿ÿÌž wþW—$‘5| ဟ°ßKÐۆ͕@4F‘#D#aªvläÀú5Œž{A¯‡Qç/àßü€[o½¿ßÏsÏ=Àw¾sz½ŽßüF»ÙðÎ;¯fóæäç§³o_5ãÇeÇŽJfÏÖ¾;57·#Ë éé.Âáü£6‹ý®»–GÃìØQ‰ÍfÆë 0kÖ¼Þ¡P„}ûªÉÏÏ ¡¡IJf÷îÝD^t’ÄžÊÃd¦õÏß"ëžJ@ÌxAA¡Š"QÛh ;-Ê”‘Aç0Iv…›/Ó.J–Õ¹åáŒ32èÖ[²,óøãóé§Ÿríµ×R\\««Ó±~ýúõ<óÌ3TWwñ1‰ðÐCqÙe—±`Á²²²âfgȲ̶mÛxþù绤Táìu(XÊ?jà¶œ‡ùBòõDÔ0+›Ÿê¶}VVcÇŽÅëõñÙgŸvÛN–e>þø#ÆN§#--m@oªªR[{˜Ú~,Ïãõ¶³woÏ)R(¢¦¦†šššA@]]uu'Ä”——Ÿâ(TŠ6|Ùè¤-s¥¼Dɇ×`ö÷-8 ¶Óx ûç¯08üM‡ð7:¥>|ñ5ì—ñDNáy¢ª žÚR<µ=§¸íIÖØKqfG‰†9øé³}Þ>ìk¦©¢ù”ÇÑAU¢´Vï€ê'µ}ÈÛDÈ{r5ûSa¡PQU•õë?ß7œµ½4ûÛQÕ~ÍÊ8¨B~/!Ÿ—¤ôÚj‘ôzäp\ 3!BŽDPd9tpgæa.Òf‰ë 3‘«ªªâf««ªŠÉÔ¹¾¬¬–ââ¥s_eì»óÐG~&  œnVw6—þ€²ÿNÐsâúÁBïèõòòJ8p`3ƒsƒoýž`ûÊÿ Óë‘£Úç ^"ËZmNU‰E§/¹E‘YûÂ_âÚuX¹re\ÿû÷×`±˜X²äü£ß‘>;:K0~&èc½‚,+èõ:dYá‹_œÍŠŸÆ¶Û»·Š+>E–µí^xaÍ «ºº‰‚‚€^&÷î;xAA¡G5 ®¹?‹'ï;B~f÷x;D¢ÿYãà/Ë“¨oÖŸ†ž^­­­ýrûTg.‚pvÙëßÈkîâêôï°ÍÛ}¨ÊÊJÂá0f³™n¸7Þxƒ#GŽÄÕŒt»ÝLž<™… Z-¶¶¶ÎË .bÖ¬Y}cMM ?þç>o'œ}tráŸ}Cÿc NÝ„³ŽÉžBÞ¤+(Yð]ÌŽTÚªwRúÖ¯{XgœœÖP\·îµAHúÄK1»28²E«½){´ZlFg:铿sdÓZÐíhÝÆÝë×!‡|lI,”hT•p»6»Ò’’ƒ !é¤OšÏêÏÞ!}Ò|ªW? €))­'mÚ ÎhA‰†‘tz²îO'˼ùþ^L)y¼üòG8óÇ`N-@IŸ4]ÙjRRR¨¬kŠÕ¢SI§õîÊÌ¥Ý׎͕ŒÅ‘D}ãÉ¥Ÿ=xAA¡Wê›õ,ønvÏ A„“VÚÏcÕß>a›`0Èò寲té5Œ?ñã'‡ X­VL&S¬}EE9Ï>ûL\‹§ÓÙçñÙíö>o#œ½$U¦pË{‚ÐïòλŠ)_ý=HZ"º¦Š ¬ÿÇmÈ‘`[ ½Õ‘f²±±š²²-ƒ<!½É†¿¾‚ŒI P™š´Úl9³—0dþ7‘ÂÍ5€D°©š¨ÑLÚ„‹i)ý”ìW#‡”-×Öi.ÁìLC‘ÃqýtHŸp1 aφ ‡Dƒ^Œ¶$²g~ U‰¢F#´Wí"Р¥Å59S‰ÍdŽš @þ!ìÞ½›Ù7ÜÀüeøi®.I¢©ª£ÅÊ„ùK(ý8~^7AAAA„Ï™O>ù„ššæÎGQQIII±`›ªª´¶¶RYYÉ–-›Ù¾}{Ül8€>XÖ-›û¼ßP¨÷醅S …hjü:LýAU‚mõDbfœpæó5T ûi­ÚÎÁÏž£zórT%:ØÃ:«lÝú¥¥Ÿ z»ü%œÂíškpŽEÒŇ’$ˆßC¸½9äGù1¹3±š‡¢D#˜\é±õÌIéZjÊ£Û«J4®ßX?Ié„Û$=Љµ‡0ªEUZlŒmױߎv~¿?v£Î`ÄßÖB{Cm¬f]G ºh8„+#g@Δ••%žÕ‚ ‚ ‚pJMM5Å„Añ»ßý€§žú> .æ ©2 ˜ÍfTU% !SSEø|2'¬ß÷y"I:$]çTA8ãIR,H ½¡(*ª–%ñh­2EYV‰F¢Q•hT%QˆDTüþäÁr¯H:=ª"“5ý‹ (ÔozU‘µ×‰N*w JKzª¢ÕËœ²8n»ŽþŽï·nÃÑ”£Çõk/éŽiµÖñÛu´3PQY©Õ¢“tq5ꎥ79\w„Ì´þù[dÝS ˆo‚ ‚ ‚ ÂÀl6öás/Õ.P  qÉçlpÖ½7βãA8FGàíØŸA7IR‘$PPUå´u/»ì2¶mÛFuuõ)õ£*ÚÍ߉‚qýA7 ½ ‚ ‚ ‚ ‚ ‚ ‚ ƒbïÞ½Œ3f°‡qN7AAAAAA„³ˆÍf£²²r°‡qN7AAAAAA„³Hff&µµµƒ=Œs’Hø-‚ ‚ ‚ œvW–\ Ào,ä‘g²¡C‡PQQ1È#AA81UU»üÔþiõÞ:k¿iÿ’’v<ï¾ûîÀî@è–˜ñ&‚ ‚ ‚ ‚ ‚ ‚ ý@ÞAAAAAA¡ˆÀ› ‚ ‚ ‚ ‚ ‚ ‚ ôQãMAAAAANÀå²cµš¨«kì¡B¯NΠ¶´EV‰†dìÉ䨂N/‘?!ªm äOHgïÕ”ÌÍ#”ÑtHèMz"Á(Î4+Þæ U[$x¤ä;‰„d”¨‚UbË-Nª¢"é$ f=JT!B:7À`Öãm ĵÏæÆê2±ï£m¼)”¨‚ªjíMV‘L$ë¿cyZamu>m! sœ&›!v 'ghã "ð&‚ ‚ ‚ ‚ ‚ÐòóÓ™8q8uuÍlÙr€hTì! ºÏó9™ K’ %ª0lfvü†*(ŠK—ØTé!ä‹ 7è>;‡¦JO3f‡½AGfq2‘`4À6#EQQd…æªv²Š“si!rD&à Ó\Õ ¼u´zÂxŽøcÃ)šž…U#Z?“½AGÁ„ô¸þ;–ëM:ôF“2bãé8µ{[hoôóÙŽ'RM ‚ ‚ ‚ ‚ ‚ ô£‚‚Lî¼ójyäë$%Ù{8g„Ïó9™3gF£6‡eÞ¼‰ƒ<Aè”ç6mÚÇu×ý/‹™ººæÁŽpÔÔ©%,\8 »ÝÂ÷¿ÿ·ÁÎ'×?ÅOQúPÒœi¼¼ñòóòIII¡ª±H(¨˜mN,N!_;A¯OƒÈÊÌ̤¾¾ž¤ôŽ””*ª¢b´X‰†Cèô&-º†M+žAŽFÑ ˜mNî`âĉ´¶¶RYY©m­‚*«|ñ†…(ŠÂ'«6pþ¥ÓQN×åçø…æƒ>öìÙCSSÓÑ~Nü]nÿLJ.ïHm9ÄŒ7AAAÎ9v‡“9]ÆÁò½ìÛ½­×A¯Ùó¡ÓI=¶;Pº“ÚšƒÝ®·ÙCv^!V›I’ƒ©«fÿží´{Z{},‚ BW9%ã}á"ž–ÁŠ g‰ÖVàìaÇ()Éç Çãñø{n|J¶»1LXMV5UÅ–2„Ý»w3ã†ï  ýTíØˆÉfÇh±R2û$Œ5»e½^ÏÅ_Ì!)$‰¬ácü„ý^‚Þ6l®f^s;Š! Sµcc,ð&Ë2ªªrë­·â÷ûyî¹3,£ IDATç´ñ¥¹‘£Q&ÏG$‰=Ö nýhÆ ‹ÞÎd†Ïà AAAèçrö‹¢£±XmdåRºsK¯·KMÏD’z¼E›º]—“7„IÓÏÇ`0jm#aÌ ùC†“[PĦO×p¸º²×ãA„³EJŠ“ùó§°yóöî­êyAÎ(f³‘ /¢¨¬Zµy°‡sNKw¦cM±PÙtº¶ºØr¿ßÝnÇ`2 Qd™ë×0ëÚÛ19ðµ6òy©ß½EQ˜9s&|ð9Ó.&ä󒔞C{C-’^ƒ t#„CÈ‘H¬¿>Ÿ`0HUUÁ`gí¶Öæ6‚þv§h4{l±™þ´Ûí´·lm¶þ"f¼ ‚ ‚ ‚pN1Œä@Åþ='ÕÇöMŸÒp$qê¿7ñBwJçͼNO}m5;·®ÃëÑ Š;“ÜL˜:›Ô´LΛ9ÏÛ¯àmoKØ ‚Е+37vs„Å‘€$épgåuiëi¨E‘å¸e§ ‹Ý‰¯µ™HÐOZÁpòÇœGÐçáÀº5DBZjª´‚aä™BÈßÎþukˆ»Î´p¦e¡7ho¬GŽFÈ=‰ŒÂb"á MUåÔîÛѯÇÿy!I£F——ŽÛmÇ`Ðc·[HMMbèÐl† ËF’$^}õ“ ¼%%Ù°ÙÌD"2MMž„mœNv»™hT¡±ñÄÿÛl&MFZš ¯7ÀŽ9Ò÷™ë‹‰ñã‹ÈÊJ&ŠpèÐJK×;ê™™Œ$Ï¢½ýÄ3~RRœ˜LB¡(--{áúl9'éé.ôz]Âmëë[O*eyQQ6Æå`³™ñzƒ”—×RYY×ë¾Næœ „üüt†Ï%%ʼnÙlÄl6’œì¤  ƒ’’|L&u=Þ 3))ÉÃb1ÑÒâeûörÚÚN<«°ão àpXí}%++¹KÛ††6dùô¤sOMM¢¸8·ÛA4¥¥Å˾}U47^ hÅÖ7.ß¶mÞ»M+ž‰ý¸ö…¿$l¿|ùrª«Ÿ`ûÊÿ Óëcÿ¿éôzYFÒéQU…ã‹Â•——°råʸå¯þë- 3edO78³oß¾¶9SˆÀ› ‚ ‚ ç”ü¡#0MD"aª–TÁ€?0ë‹Ñã§ Óéñ´µ°þãUq).Û=­|öÁJ¾°h «‘ã&³qíꓟ ¹连|½Ñ·Ìâtñíg>êÒöÑÅZš¬cÿ•ÿbÆÒÛxý×ÿƒ3-‹ nü’¤]toª®àË®bæÒ¯3ûúÿŠ]lª®àïß¼‚ß××M¿yžäœBþõÝëý\yï¯HRצz×&–?z7Í'HM|¶™;w·ß¾˜Œ w—uªªGðzƒÔ×7 †d ßøÆe,\8}ûªùæ7—°Í7^ÂÕWÏ¡ªª¯}íÝöuÅ3ùÆ7.Ãj5Ç-÷ÝMlØÐû‹Ã×]7¯|å ØlñýÔÔ4òÛß¾ÌæÍn÷àƒ7RR’ÇöíåÜuן»íß`ÐóÏÞƒÃaåÙgßãï»×c뫳éœüáß&-Í•pûÅ‹ï#õúx&Oβe_¤°0³ËºššFžyæ=Þygã û8ÙsÒŸ†Ïå¿ÿû*F.ì²NUU"™H$Ê‘#-TUÁj5'_æf‡•p8‚¢¨±›rsÓ…" z-šÆ›o®gÑ¢iüãÚëo̘!ø|AÂáápP‘$‰9sƱo_5--M¤¦&a6 ‡£„Ñ„ûIO×^·‹Ï`çÎJF.äàÁzÊË[HII¡²®)V“NUTêû?-µ¼ ‚ ‚ ‚pN)1 €Cåû‘åèiÛ¯Åj#=3€Ê¥ ëÊE£–ï¥dÌ$²s bB,K\jAHDå¯}ƒhTî¶Ÿ#ryôÑÛp¹ìøý!6lØKss;éLž<‚Ë/ŸIAA&÷Üó—„³Õ¶o/Ç`Й™n²²Re…;+ãÚy½þº¥§»øÃ–áv;…Â|òÉ.ÖJ{¹ÝJJò><‡;††VÊËkt<ýÍf³P^^Ë¢EÓ‘e…§Ÿ^À—¿<€e˾H ¢ººI‚ªª,#óçŸÇÇïbéÒ B<òÈóÌŸ?…ôt×Ñ Zg?ÒÒ’°XŒÌš5¯7@R’Í›`µš°ZML›¦m„ñz˜L†„ûéïGí$9ÙAnnƒEq±{÷nfßp—6žeø9ð½›ûíœÕýr o‚ ‚ ‚ œC2²óp8µ; +œ\šÉ“e?šö  ­¥ûZÛGê(:wJ õݧ´ ñ&Ìÿ9%ãyõá»ØñÞ«€–Îrú’[È;¥ÛÀÛô%·P³{ />tÞ¦#±å³¯ÿÝzÉ9…Lýâ|ôÔïûz¸½’››Ë-·ÜÒ«¶áp˜G}t@Æ1th·Ü²€gŸ}Ÿ¿ÿý­¸õ¯½ö)OÈý÷?AkkçŒá †ñð÷0aB×\s!Ï=×5ûý÷v¾ß~õ«sóÍóñù‚|÷»ÝÏp(×^;/tûæ7£²R«£Ö‘:Q§Óqá…ãp»íTVÖŸöñªÆÆVª«?~(z½>nÁ §­ÍGCC~Ÿ/Hff2EEÙ„ÃQ22ܱõ22Ü(ŠÛ>•1:ûíØÞãñË44ÔR\œGié!23“cÛw¬ïèóøý8ôz=Ó¦•`·[¨¯oF’t±Úv Õ¤ó·µÐÞ00ÁPxAAáœQ4B»ÓúHm5>oâ; ŠÉÜ™(Mœ:ˆ«ëæpºDà 0  틲ª‰ˆç‰Ð_ö¯[ ºø[›8´cC,èvìrkR׺Bàéïß@Ø_³è“çþÌ„K—š_ÄÄ× XàÍét2cÆŒ^µÈÙ¢ LE’$¼Þÿú×»]Ö75yxçM\uÕln¾yþxûò—ça0è ‡£<üð³][¶àñÇWp÷ÝK»íÃ`ÐsÛm—ðÒKvI3 EøÅ/^dܸ¡$';™?ÿ<^y哸6­­^6mÚÇ´i#™;wB “Á göì1€6l ˆs’Øw¾s:ŽÆÆ6~øÃ¿ãóÅŸ—p8Êk¯}ÚíöýqNúôi#ci7ÿö·7â‚nѨ̋/~ÀCÝÈ%—Læé§WQSÓØ¥Ÿ+¯œMnn@ˆx2.è°m[O>¹’Ûo_Ì’%çóÒKžpöÜ`+*Ê`ÿþÃ<Ø5°&Ë2ï½·å´×àë/~¨Õ"}üñבe…%KÎGQ{ìdYA’$ôz]¿‘Á gõê­èõºØv¿üå‹È²‚^¯‹ûÙ±þ…ÖtéCQÔX¦·ÞZßed¢ýtŒ·ƒ$IGǪ§  €·{@«I'éÐPì/"ð&‚ ‚ Â9ÁîL"3;€òcf» F2sò‘£QêêuV›äÔ ,Vr4JKsž£a ‡:/´ަBK$îL-i4™ºmw.é ¸Ý.Tº%ž'B‘Kñ+GÃ'\®?Á{úu«»ÝPUJ?~‡Ù_þ&î¬<ìÉiøZº^¤>žÕfÅh4‰D‡ÂÈò‰/H×ÖÖòØcõØ/@4ÚûôË}G^^:Ö‰$ÞOuuii.\.;mm ÎÛ¢#e↠{»MeØÖÖ}:SÐR&%Ùxíµµ Û!>üpW^9‹éÓG% ¨¼ûîf¦MÉèÑ…¤§»ihˆß›:U›ñ!Ë ï¿¿¥Çc;Yâœt•››ÆÈ‘ù€6#ìø [oô×99Uyyi€6“«¬,ñ ¡Ž×°$I –“0ð6oÞD@ è´´´wYðÎ;¹ýöÅ$';1"—={zÿùütëxŸ**Ê"++™ÚÚî¿ †’¬büa?EéCIs¦ñòÆW((( %%…ªÆ6"¡  b¶9±8]„|íÞ è9:,--úúz>ÙÖL8àÅž’b4[ãj¥Íºîv>}ñ¯xêˆFe&NœHkk+/¿_kõòëç£( Ÿ¬Òn²mEá‹7,DQt:]ÜÏæƒ>öìÙCSS׬!A¿hTªª¢ªZïØºvªª G»¦3í"ð&‚ ‚ Â9¡c¶›ÏÛΑÚêØr»ÃÉ”™sñû¼½¼?G’«Ëò†º6­û€P‚Y^OªªONI£¥)qZ#wJjìwI’z5žÏ#­®ŠÅbéUû`0H0hÆb±ˆ Ê9Ä|t¦¨xžg£#åµ˜Ò Gôxs:$¹’bµø€Ãg iŸÉäH¬ñµÒí­Ϻ”Ëÿ€Ïç# RUU÷Y¬µ¹ ?„Ýi#Æ[læ„?ív;íí‰gHž©DàMAA„³^ÁÐŒFd9Ê¡òø;Ž }¼­\ñ¡` Ë]¬C‹™4mNW2ÃJưo÷¶.ÛïÚºž´K¯ÀîLâ‚‹³¿tž¶Lf Y9ù V‚ßçÅdÒîú—ÖLèd0ú”M87‰ç‰p&“¹ùÂh¶È>l6Æ ëU[EQصk×€Œã­·Ös啳ÈÍMã?¸ŽŸþô™Xú?I’X²ä|&OÖnyþùÕ2†þb·wÎê9¾ÎU_8Úß<)ÉÆ•WÎê±½Á ëvݪU›¹ôÒó92Ÿ¬¬êê´twS§–`³Yðûƒ¬];0[ç¤;)"UU%:¹›©ú󜜊³fÍ6æÎÀ7¾qµµMìÜY[?aÂ0®»n. ¥úL”nôØ Ú´i#{µ_ƒa`êoõ§×_ÿŒ?ÞÉüùS˜1cÇçÄêíÍ›7;*øóŸ_‹=O§[ßH¸|Û¶mpxï66­xåè ŵ/ü%aûåË—ðï I§GU¦‚<¦Vš|ÜM…ååå¬\¹2nù«ÿz ö}ª§,ÅÅÅìÛwr³F‹¼ ‚ ‚ ‚pÖ:b5‡Ê ‡Cqë Æîkó/è¾6É¡Š} %-“¼Âa ož¶Ö}üSfÎÅéJfòô âÖ×>ÄÁò}L?:ë­ÝsòwŸé J uu½¿ âp8béëêêE@åБR¨®®¾×Ûˆç‰ðya0u¦V ú×Ä:V[k[\ŠÇvOÏwÿðÈ#ôj<Á`%K– È8üþ÷ÞûW~øÃë3fÿú×÷©¬¬£­ÍG~~:YY)¼ôÒ‡¼ýö†^w°øýŸ#L¦“¿´Úޮͩ«kæ÷¿µÇö²Ü}¢Í›÷ÇRóÍ;!¼œ;w<|°ý”b=ç$1¯W;I’0›'|ëÏsrª~ñ‹Â,\8•ßþö[TU5PW×Lzº‹!C²$‰íÛËyì±WnßÞÞù9ú™gÞ;aºÉ&NÍ~¦imõò kxþùÕ$%Ù˜µ4ýcÆd`4†iii'Š`6 ‡£„Ñ„ûIO×fŠ.^<ƒ;+=ºƒë)/o!%%…ʺ¦¸Útõý?{\ÞAAA8«efçápj_¾Ê÷ïé²Þ؇À[OŽÔ&%-g’½^Ÿ°¾Å‘ÚjV½þùCGœ’†ÑhÂï÷q¸º’†ºŠŠÇÚ žÖÏOƒ¾’Ð.$õ6àñÁ‘:ðÜ isÅóD8%¥w¦Y멾@{{;Q9ŠÑh$‰õ<+: Q]]Ýc;èýëìdÆàñøQUUUÙ¹³‚¼¼t23“ñz¼ÿþÞxc[·žø"ð™ µÕû½# àÉè˜ñg±˜øì³®ŸOúBUUÞ K—^ 2fÎ<=)Å9IìØ´›99iTTÔö¹þ<'§*Šhÿ/ïÙs—ËNII>¡P„÷±zõVV­ÚÜmð¯±Ñ«wÜÔäôã(ŸÕ«·²zõV®¼r&K–œÛí`úô‘¼÷ÞæÁ^ŸØlÊËkY´h:²¬ðôÓ«øò—ç°lÙ BTW7"IZª`‹ÅÈüùçññÇ»Xºô<ò<óçO!=Ý zwôÓ!-- ‹ÅȬYcðz$%ÙØ¼ùV« «ÕÄ´iÚö@¯7€ÉdH¸ŸŽñ~ôÑN’“俦a0èP»wïfö wiã9Z›îÀ÷nî÷s'o‚ ‚ ‚ œÕò ;êM(Œ™0µËz›Í€ÅbeÖÜ”ïßM]Mßë1„C-&3r73äÂáe{w&\WXT @Ó‘º„»s‘Ãá -M«õ%‚)BwÄóD8V,pé«SÎÈ DCAêËzw:à ÷5+**¸ýöÛOj|ý9€Ûn»Œ±c‡²bŧüö·ÿé÷1õVGj3î䞇7áõp8¬çô86mÚÏÒ¥âv;:4‹ŠŠºž7:U«6³té…Œ‘KnnC†db³Y8r¤•íÛ+zîàˆs’ØæÍûc¿OZ|R·þ>'§bîÜ \}õ¬ç;ßùc—´ë=ñûƒ”–V1jT'gùòµ§<¦ÎT…§ÜÕ€Xµj3K–œ@vvÊ ¦ï[©®n`üø¡qµ=AKÚÖæ£¡¡ ¿?ˆÏ$33™¢¢lÂá(îØúnEmÊqéD;¶÷x|„Ã2 µçQZzˆÌÌäØöë;ú<~?‡½^Ï´i%Øíê뛑$]¬¶Äצ§x5j&“VáСC ©ºÝn’““­h_}}âô&h6Âá0{ötý°‘™™IVVVìqYY^¯7®Ûí¦°°°OÇPQQÇÓu:¿^¯§¤¤„ÜÜ\’’’ðx<466RZZJ ÐùÁ¢¨¨§Ó @iiiìn¼ã•””`±hùŽwìØ¢ Ü´_AAA: ÚŒ6IÒ‘ž™Óm;^[_{A7 î i$Ü÷úlyE$¹´ïO½¼{.èHg&‚)‰ˆç‰p¬ W»gq¸0ZlD‚ƒ“Þ+9gHÂå&›1s/ rë§]êâœ&L(ÀçÜ×g0¨ëÔÔ¤„ëÝn;³gév{UUÙ¶­ŒÙ³Ç2mÚHÜnGÜŒ/ÐR .Z4í„ãØ²å­­^Ün7Þx)?þñ¿úx$ñ8LEEC‡f1wî 3XµjSŸ$}%ÎIb••õTTÔ2th6_úÒ¼ýö<žÄïE’$%SŸ“S1~¼ööûC'}þÞ £F0gΆÏáÀç4¦Žtž‡‹ÅD0xzë#'';imõv{>ÌæÎ̳?O>üp?þ:²¬°dÉù(ŠÂc½‚,+H’„^¯#íz³ Á gõê­èõºØv¿üå‹È²‚^¯‹ûùÿÙ»óø¨ªûñÿ¯;wö™$“u²‘…%ÂaßD•ºÕZEÅj[ý|ºhµÚO[­Vmmµ~mkëJµuÅ¢­¢bAö5HHBÈBö}öåÎï!Cöõ<ÉÌ=çÜ3—›Iæ¾ïy¿Û·¿õÖ—ÆP”@(–òñÇÛ:¬¨ìl?íóm'IÒñ¹Ê¤¥¥ññs„Õ¦  ¼]{íµÌš5 €¿ÿýï¼þúëÚÜsÏ=Ì;€ªª*V­ZÕ¡Mjj*¿üå/ؼysèû“Ý{ï½L:5ôøµ×^ã­·Þ k“““ÃC=Ô§×ðØc±}û‰ÜÑ’$q饗ò­o}+P;™Çãaýúõ<ÿüóÜvÛm¡yÝsÏ=].ßÿÎw¾ÃðáÁ7§ë®»N|AA„Ó$o÷Víï:­KZfóÆâr:ØòU°ð·³›Zn݉´ïdu»œøý}«b‰‰cÒôàg§†ºª*z®{q¡hnnÆf³‰Z]B·Äy"œìèÞ`=I’XðÍï±þO¿&^„SÉ2I£'P™¿{Èç1zÎ%\þ½ÿcýKã9þ»Eg4sÍO~‹!2x£ÅÆ7_òyœ Ž©&=ÝÊÊ• ÉÎN£¶¶^¬v»½ÔÖ6³m[EE•C<àê†èèæÌÉaÓ¦¡mcƤñðÃ7aµFw;Æ[om`Μqèt~üãoðØc¯….ª›Íx`3gŽév ŸÏÏ /¬åá‡odþüñüà7ðâ‹…ÕÁJJŠå替ùÍÛ=¾¶Ï>ÛŪUKY¼xJ(åã§ŸžžÔvâ˜tîw¿{gŸ]Mll$Ï<³š§žz‹‚‚×ã㣸ùæ‹5*•ïÿ¼ÞðßaCqLú«}µÝ˜1i¼ôÒ”–Ö„²3ø|~ššläåa×®Ã]¦›üðÃÍ\qÅLÒÓ­<þø<ùä[lß^Ú.Ë*,˜È-·\Ì£¾JYY÷5Þöî ¦§•$‰o~óþô§u¡ ˜,«=zX¯jÉõ‡Åbæ¸úúV^~ùcöí;€‹äÞ{¯ =>t¨|H†N5Þ IDATæÑщY8<†ÇgÇ;;‚õ÷ÒÒÒˆ‰‰¡¼¾¯ÛÐ#ÐGDá¶·á²*µ_ OMM Ÿ~uIRaŠI wùmlÿU‚µØT¡ý ’¤bôÜ%TæñéW‡ (â3ÇàqÚh¬ þŸ˜b‚Áðöí1)øŸ¿ÑŒ¢(Xâ,Ì»tŠ¢ R©Â¾ŽŸ…¢(|øÆ¿ñû†fÔ€oÛ·oÞÆßa»J¥bòäÉ¡ÇIII¤¦¦vNM˜0!lÌSEGG‡°`Á‚·²Z­<øàƒäät}W‹V«Åá8;Š ‚ ‚ ‚Ð3‡½­Ûí®ã« E¡¥¹±Ëv²¬&2*š¦ÆºN·ëô’Rƒ8*Ëú–¾(cD6ã&OG–Õ¸œvnÙpZîÄ>—ˆ`ŠÐâ<ÚÕ•²÷ßÿdâ’똵ò.r.º’úòb4:=qi#ÐGXøëwW yðÍÖXÇ´«naÂ%×PU˜‡â÷“’= ­1˜êjë?ÿBùþC:‡³Å‹/®%;{‰‰1Lš4¢Ó6wÜq9[·â׿þ;--ö!™Ç† û¸ãŽË±XÌ<öØmìÜy˜ÆÆ6ÒÓ=zMMm¼ñÆzn¾ùâ.Ç8p ”wßÝȵ×Î%7w4o¼ñûö• Ñ¨7.“Ißãë×ïbĈ$V®\Èe—å²hÑd Êq:Ý$$D“žž€$Ix<>Ö¬ùœÊÊîk®_¿‹;<´´àEñ‚‚Šƒå|;&:†7Þx¨Ãsí^ýÇa«ýêWkÂRK¶ËË;ÂsϽÇý÷/'33‘^øUU 46¶e"%%.´Úmúôl6mꘊ|°I­[·•9srÈÍ͈ÉŒÑy&‰²²ZžxbMX€±ÏççÑG_å‰'îÄjæ‰'ªòò:t:-#G&c2³ÆÝpÃ~ó›t;§ÒÒþýï,Y2•+rÑE“(/¯E§Ó’––@D„ï~÷ù! ¾eg#:ÚLbb Ï<³š¦¦6 +p»½DFIKKÀçóãv{øê«}<Ø¿Œm² Sk1h ”5œüeddŸŸÏÌ[Äçqáq9(ÏÛÖhB£70zÎ% Ih*à÷û‘e™Å‹Ó?†#»6‘9ÅïCoŽbÖ «5ZŽìÚ„uD°†¢1*š#»6¡5˜ÐL¡Zjæ˜xš«+ø×3ÁŸ­‰KV€$‘82ÓÇaÃekÁ¼™qÖ w£ø½ø¼Êóv„o~¿Ÿ@ ÀwÜÃá`Íš5Á×gÁïó1eÖx¼o豬Vwúu(…ÞfϞͶmÛzý‡êŽ;B³²²ÐjµxNJ§’Ê™Ùnúôéoíi&ÛÇ<Õ‚ P©T>LVViii¤§§sô艚}ûöñ?ÿó?a}çÎË5×\WåØkŸ‹Édâ±Ç#55˜‡¸¦¦†5kÖP\\LUUF£«ÕÊĉÙ¸qc¯Ž ‚ ‚ ç€^¸Ôj 3ç_Jtl<öPRxßIiÁLæ¦ÍºµZƒ×롸°óúm'“$‰äÔ FHÔñ•r­-MlùêSœ[½A„žü뙇q´4’{õ7‰LH"2!)´íØ¡½ÈjM7½ÇþõïÓPq„‹¾ý¿¤OœzÞã´³ù­—øêõßùÎuu-Ü~ûSL›6šääX4š`z/I’ˆŠ21iÒFŽLaÆŒlž|r«W?7$¥Zìv?ùÉËüä'7“œË´iÁÚª^¯ÿüg'úÓ¿HKKè1@ô ÒÔÔÆÊ• ‰Š21o^pQBUU¿øÅ”÷8ÀK/ý‹Â n¼q#G&3~|fh›ËåáË/÷ò曽 ¦ÔÕµ°woI(°ùÙg;{ì3˜Î§c¢R©ˆŽî˜ ­Åb{¬Õv½¾åÃ7säH5·Ü²˜©SG‘”KRR°&©ÏçgçÎüùæçìßßõ[ƒyLúËïWxè¡¿““Á¨QÉètÚPm5³ÙÀˆÉL:Š´´žzênîºëª«;–¦*+«å¾û~Ïm·]Ê%—L ;\•úî»ùøãŽ t:óÌ3ÿ¤¥ÅÎÕWÏ&!ÁBB‚%´íСò°bƒiË–ƒ|ë[OqÓM‹X¸p"ÑÑLž<¿_ÁïWðz½WñÁ›øúë=8â#â1Äè)m8Juˉú€íµÎÔZ> Åï§hÛ—Ì^y7ºáfìÍ ¸í6jòóQ…Y³f±aì“!)k<•¬FÖjñy‚uJ“²ÆÓtì(§½9’¤¬ñ;´—(kr¨–šF§Çç>QÆËí°á¶ÛˆŒO¦­® I–ñ{<¬Á†Çßë ͯÝnÇårQ^^–Y°¹±—Ã)ˆÏç =Öu~ ‰?(@JLL \}õÕ¬ZµŠÏ?ÿœgžy¦×ýö·¿eäÈ‘<ôÐCäåå…¶Ýzë­¬\¹›Í†Á`@–eòòòÂÒAJ’Ä›o¾IDDÅÅÅ|ï{ßë°çž{ŽáÇ“——Çúõëùþ÷¿À[o½Åk¯½Öíü–-[ÆêÕ«xöÙgY¿~}§í¾ýíosíµ×Áàßã?Þe½¶“=öØc½J5Ùþ@¤šAAα±Á¸§ÖN¾ÐŒÌOÎÄ\vŸ~Ôyz•JÅø)³È1EñÓÜXÇíFg0`‰Þ­ì÷ùؾù jŽuJ&ÊCÆÈl’R3Ðé‚wôú¼^ ó÷P\xà‚©]úIð&ÊŒËúV§[¸°df/,9Ò·U¤‚p2ÞHÒ¨ ‘&99<ýôÝh4jÞ}w#Ï?ÿA·íõz-©©qÄÅEáñx9z´–††Ö~í[¯×2jT ‘‘FœN7¥¥546vŸùb°H’DFF"‹ ­VMk«²²ZZ[`2EQ¾Œì¼ÖäPÓh4Át“ãç°sí(þÞ?²Zƒ¢(H¡>*YF’‚)çÞ|/Ûß{¯Ë|­J°M{-µöz¦3V|Eñ³ý½W:Ý®’å` 6•LÝË÷éx$¸ýý¦ýqWÚÛÕÔ7aë>ÅpoµÞÔW^y%«V­¢µµ•wß}·OmÛ¶-x?~|Xà­= µmÛ6âââ˜0acÇŽÅl6‡>Ügff†j©u¶Ú-===°Ú´i[¶lÁçó¡V«™?~·ÞÐjµ,[,hët:ùÃþЫ › ‚ ‚ EQØ»cU¥Œ7™˜Øbâ¬aÛ«•qpßNÚZ›»+ÒCƈlZš8ZRHyiQØ :Aaðx]Êòz·rb¨(~ÿi©)w62t,_>€wß݈ÛÝõï»÷ßßÄ]w-E¥R‘š?¤ó fÖªäðáÕ”s»½v~#~_ÕÔ4QSÓq…йF“®57ÛinXÕ3qL®¸b&6m:ÐmªÎJ)((gܸL† ëùgØåòPTtŒ¢¢cž£Ëå!/ïÌܨ8r¤*Ȭ”ñæÙß”q:SÔÔ‚qÆý]¶QëÍ ©ð9ƒÁPóœîçs°¬þŸ.·Û6ý€C­$•ŠÈ‹~ˆJ£Æ0|Ž¢£H*¿Hj5¾Ö` I¯×ã÷ûQiuHHŽ×MÉžD塽@‰A¶ÀñÇ®J´«ùðg½j×êeË–q÷ÝwÓÒÒÂÃ?–º±7¶oßÎM7ÝÀ¸qãBÏ[,FŒ.áݱc‹… & Ë2S§NeÆ @Ïi&-ZOÖÍ›7c³ÙØ·oS¦L!))‰Q£FqøpÇü¹}1~üxt:{÷¾Kr###±X,nS«TNOAA„!Rt(¢Cy=7j«+©­®D§7`ŽˆB£Ñâv;±µµâõôîæ½šcåìß³ªŠ£=ÖŸA„sÏççŽ;.G’$öí;Â¥]¶MI‰E¥RPW×ý,‚ œ>K–LcìØôãÙë>ï²F£Æj ¦P¯­?Ãç:ó°1H’Š¦Â­ƒ:®it&H*>îÊ$µÃð4d£ÅéÂSß ¼7ǃÓGsU9¦èXŠ‚Z«cXÎ$•|>&ìÕèô4W•‡oS¦LÁëõ†-jê{ææf~øaÊÊú^௨¨ˆææf, ÙÙÙ¨Õj|>S¦L †Ü½{7ƒ»îº €ÜÜÜ·¶¶6 ÂÆ–$‰… pèÐ!€àÊ·)S¦Áúo ¼%'Ÿ(ÙUªÈÞxòÉ'4AAAÎ n—·ËÙsÃNxx¿ûÝ»äç Kg6˜6-‹»î f¤jhheóæüÐöÜÜÑüèG+ûµÿ›o~¼ÛUv2£QÏ«¯þ°_}õ«5ìÚ5°k²ÂÐûÖ·–pÅ3úܯ¨è?þñŸCwí:ÌØ±éÜxãEÔÔ4ñõ×ùa)255cǦsÛm—…¢(¬[7¸ÁšÁ2XÇä‚I‚yÔ*·‚Êh@q{‚´”îª)¡[[[ñxäé§ï!#ÃʳϮÆçóc³9ñx| :"" ¡¶õõ-üô§Ãá8qQ_«Õѯ}·¯ :R©¤~W­Vd÷:˜Lú~ýGE™ÂÿóŸÿeÞ¼ñ¤§[yøá›´µ9q¹Ç5×ÌaÁ‚‰ddX‰Œ4iÀíöPZZËŽ¬]»™ºº–A™ÿP¬cr:ؾþíißgØþ{Øž˜,ËTVKwÅÇG!Is玧°°‚ššFŒF=n·—¥K§³öðfteZÜn/¶ã)…SSã¯L`4ꉈ0b·»ðx¼x<>æÍ Ž—š‡ÕÍ_"&&†TU0I`Á7ùä÷’8öA†û8¼å FͼI¥%P0FFóÉïeáí¢øOÞ.(8øÇN=РÀîÝ»ñù|¨ÕjÆÇÞ½{‰ˆFO¨mݺ5T.77§óø عsgؘz½žY³f…ßvÛm]îÁ‚ ¼| FJ—ÙWëׯï2ˆ¹xñbxAAAA¸ü¿;/Ã×ËÚŸŠÚÚfž{î= ¸ ÍdÒ£RI¸\žSA¶µ9ÉÏ?z:¦yAQE×ó\MM55/é+—ËÚ5_°fÍH’„ɤG­–ñx¼8že¤;ó˜\èn¸a!²¬âÙgßàæ›/f×®" -ƒ–«®šE^^)sæ³.]:¿_!>> ÇËóÏ#^×_?Ç‹Ëå!/¯£Q‡^¯aöìl6'n·ƒA‹Ñ¨§¤¤ŠŒŒ òóó)uÚPIÓçø˜rÅDÆ'á÷y5k²ZKce).[ iã§w»}( ÊZk§Ó |3†3‚9R›šš()) µ+..¦®.˜›sÆŒ¡ún´µ…Ÿ5kVh5\OæÎJ]Ùû÷ïÇápÀòåËû=– ‚ ‚ ‚ ‚paA·î)ŠB[›ƒ–»¨¿&ç @ €Í椹نÃá>g‚nÂà hµšÐãââ*²²R9z´«5¿_aÛ¶C¨Õ2f³ž˜˜ˆ°>@èq{{«5š †ÓÚjÇåò†Æ«¯o¦¢¢‡ÃÉd¢Ùî¢Éæä«WKúÄ™´Õ×Ðtì(µÅ‡ÐMD'§‘8blۇ %ãݾ};“&MB¯×³téR ¸ÚíÔº-[¶på•W’ššJrrr¨ï©NN3ùÈ#P__ß¡Í÷¿ÿ}²²²ˆˆˆ`òäɦ«ì ÇÃ+¯¼ÂêÕ«¸é¦›hkkãßÿþw¿ÆAAAAAA8_>\‰^¯eÅŠy(ŠÂÚµ[P”Êñúkíž{î=ü~YVá÷+,_>‡µk7‡ú”³vífüþ`¿·Þú²ÛýVT4––@p´4ñÏÇîE’T@ À/? ëÓÓöÁ6¨·U«V`2 vk¼Á‰â¦§¶‹‰‰aÒ¤I”••±k×®N÷ùÕW_‘••ÀüùóûxX·nóçÏ'''­VËý÷ßÏÅ_Ìž={(++CQâãã7n|ðÁ€R[ ‚ ‚ ‚ ‚ ‚ ‚ œ­.»,«5šuë¶„j÷%&Æk¶­ÝŒ¢Bqžìì4ìv­˜Í</Š ¡¡5ÔÏíö²eËAn»íRÖ­ÛÆÒ¥ÓyùåO7.3¬¶ÂjÆ55µáv{Ñé4´¶6eêt?]Õš tÄÄÄPZÝÀä¥7°sí”5õƒŸztÐoÇŽ£²²’””ü~?{öt,R¸ÿ~l6fs°€jcccX:J€… "I7nìrŸ›7oæÎ;ï‚©)µZ-Oÿ ¦üqî¹çæÎ Àرc;¶ãRúº:xAAAAAá¼Ô^S­½6Ûë¯À7^À}÷-ÇétSQQ$Ayyz½†%K¦²q㮿~>N§›'žø;K–L;^ßÍ6N»¸¸È°Ún‘‘ưšqÓ§û;l6'Z­ºÓýtUkN¯O ??Ÿ9·>œÏ}âq:(úßÛýØ J·vÛ¶m }ŸŸŸÝnïÐÆï÷‡¥–ì,åE8à_ýu—û«©©¡¸¸ƒÁ@nnn¿çÐÜÜÌO<Á/~ñ ;ÌËf³±~ýúnç$‚ ‚ ‚ ‚ ‚ ‚p.k¯©Ö^›ídjµLK‹ÊʵµÍ¡Úl„Kh{»„ í!µZû t¨ívj͸öþíÛ»ÚOWµæÚkèÔ-M4U–ê1«~*ƒê§2EåÃ.Fbcc‘e™úúzl6Û™ž’ ‚ ‚ šØØXñw®pF”~r€ŒËÒÏðL„³Yff&GŽ9Ã3AAè^ûBž“¿¶ÿSEQ¾ŒŒ<“ÓíµöÚlí5Ù>ü0XM’$dY…ÏçïÐG­–CuÝ®¾zvX¿öñN÷½÷6uãäšqíí{ÚOWµæ4 iii)-Eñû‘$*YæXu-Ö¸èA=f"ð&‚ ‚ (xÎ$xzCÞ¡*5(¾3= Aḳ-ð¶lÙ2öîÝKEEÅîç\WSß4è·AM5)‚ ‚ ‚ ‚ ÂÐRFÌÃ}ÿçâGžé©‚ g©‚‚rrrÎô4.H"ð&‚ ‚ ‚ ‚ œ—tj-I–$$I:ÓS4þ ×à¹é¯,ÃPÒrÏôtA„³”Ñh¤´´ôLOイ>ÓAAAA„Áeˆâ—+#ÆÍþÊüê£'4^¬9†œ”bLÑx|šÍ”Ô¡¦¥ffÜ3Îx¯yЬý1ò®·NÛ¾A8ÿÈ:#Qç ‹Þ à±5âª+Çv¬8w+TécS‰Ì˜ˆ»¹šÖ#{ \ iy­V+[·n=ÓÓ¸ ‰À› ‚ ‚ ‚ ‚pÞÉNÊ"ƬÙ2.%‡}m®¶>cÖ›¹}έÌ9³Ó훋¶ðûõÐ\{C1ïµÏ‚¤Bóñ£"èv–X¾|9yyyŸé©§Z¼œîóõ=£Õjñx<ƒ=¥þ‘$RçßLÒìëQ©µ6»›«Ù÷Ç»Q|gÉ|ûÈ7ŒÌ¥÷°ëÙ›ðÚšÎðŒÎŒ²¼£D©#™\À‡wÅï@Ö o yë߆|ŸBÏrrrXµjŠ¢pçwRSsúV?D ŸBÊü›C‹ß wsu‡vÆ„ 2–Þ@ÁšGð»í¡m1ÙsHœy-‡^¨Û@ËÈkBGë‘ÝTlx½Ûù½ó+| %%U,]:¿_áõ×?àÆ/à¾û–ãtº©¨¨G’ ¼¼½^Ã’%SÙ¸ñ×_?§ÓÍOü½^Ë•WoR‰Âíö²kWQ(ð‰^¯aöìl6'n·ƒAÚO]]3yy¥Ìœ9›ÍgÏžðŒF=%%U:–,™F||N§YV±sgùùù̹õàöûÅãtÐXQ’DCy ½‰KVphã†äØ‹À› ‚ ‚ $I’žBGV«QI*¼Þþ]œ$ µZÓïþ‚ BçZœ-<°æÄšc©îg:ÈyYsCA·ÿ[û8…ՇöKH§¥~œ÷ª_0Æ Õ¢ùäçýcDî|&]¾½)‚7~ôÍAžá…G¥R±zõj>ùä“ÓtИ,D zœ0årÊ?ÿk‡v²Îj'Érø1'¶©ä}OfJ…>: Ok}óQit½!'‘ÔšÐZst¿ÆLéééüêW¿Âb±àõz)--EQÒÓӹꪫX¸p!>ú(aýdYæ§?ý)¹¹Á:Œµµµ455‘””Dnn.¹¹¹¬Y³†×_ïÀjúØT¬S—P¾þeŽ}ým$•œâ#â1Äè)m8Juˉ ¼ÃáÀd2Q_ßLEE&d"ŸòÞ VË´´Ø©«kÁápa·»°Z£>< ÇGB‚%´½]k«ÇOZZ<Š ¸¸Š¬¬T¾új@¨{»²²¬ÖèÐ8²,±mÛ!ÆŽM §­ÍÉìÙ9|ðÁסy'%Å2{v %@ @«Õ„^€J­ÁÑÒD[]n‡ ·Ý†ÅšŠn¸ŸÇMTBò{ñ$‚ ‚ ÂÇdŽ`î¢e-) 0/Š¢ô©¿V«cÔ˜ ¤¤eb0šðù¼Ô×TQT°Ÿ†ºŽw›Ÿ*5m8ÃGç`‰ŽC’$ü~µUä便©ïwˆ ‚ ¹}Ž5Wõ»ÿèÄ,ÊË;Ýå&Žî(™³P².@óñÏÀïí×8É£'0vÁRœ­fʵÁ¶lÙ2233immåÕW_=£sQü^T²†øI—Rñåkl=«Á&I?ü0‹…ðøã‡V·EFFr÷Ýw3gÎbbb:ô½á†ÈÍÍÅétò‹_ü‚={öÁ€íâÅ‹Y½z5 ¨Tª>ÿ:PÃÆÂñjvþ«Ó6â:?¬ÝÓùÿïÞ½{IKK㫯‚«._|ñ#ü~…+æ¡( Ï=÷~¿‚$IȲ ŸÏßa µZæ‹/ö ˪P¿¿ý-¸zL–Uøý jµŒ¢Ïœ­' IDATBÛßzëËNçsê~N§]û8ï½· €#G‚Ÿ¹>úh ~¿Âe—å¢×kC¯ïãçAñû‘$*YÆïëøûSVkzs(ûLÞAAA¸à 5½ÁHbJ:‡öïîSߨèXfͿހ×ëAQt:=‰)ièô¾úlm·cL˜:›Ì‘Ùøý>Ü.zƒ‘¤Ô ¬ÉÃØ¹eÇÊKûõÚA„ÁcÒï˜osö=Må`ñ͹Uù.TG6Ÿ±y'X,n½õV^}õUÚÚÎÜùÁT©cј,ÄdϦ!ÿ«3:ŸóÅĉIMMàw¿û]XJÉÖÖVžzê)Ö¬YCEEE‡¾W\LãøþûnŠ¢ðŸÿü‡üü|*++‡"8¿¿ÛqÚ÷/œy^¯—ââbbS3ñº]@1‚mNÜö6L1VZëªðùüX­VjjjˆŒODgŒÀíh «™ÖÞ/2>IR™ŒÛÞ†ËÖ‚ßçã‹-åø<îÐþ'MšDss3¥¥¥|>?Ëo½EQØôÙv–­\̱²j’Ó9VVMMÂÁƒ‡BãŒ3†ØØX6nÜÀ'Ÿlm;¹îf  à÷uàî,7DàMAA„ ŠZ­aXæ(Žî[]ƒÑĬKÐéô´67²gÇ&šêÐhu¤eޤ¡®ûTSéóBA·âÂÌÛ‰ßçC£Ñ2nò Ò2G1eÆ|Z››°µµt;– ‚.B¾‹w-ÎV<}¬Yt:ÒHv'•„2rò®¿÷¹”5%ôôæH$I…%1µCÛÖº*øªslj––Ú*ŠŸ´ Ó‰KIÓ±£”îÞL ¼™6>—¸ôQ4W•Q²kœLT2Q I…–ÚcÁףђ1iQÖdÍÔ¤©ª¬Ï¯ñL¸ýöÛ1™LñÉ'Ÿœéé !Ñp`ñ“.%aêRx$)))8N*++;mÓYÐÍh4b±X8|¸ãJÙ®ú6ý|[S"‘uŸ¯­±‹6È:#¿¯_5þúJ։̘ˆ6"ŸËN[ù<-µ}G¥Ñ™>]”ÅçÁY_†­²è:0ª‹JIÂïvàëáæ µ!$>¯³µÏó*3¯_…ÏãÂãrPž·­Ñ„Fo`ôœK@’ÐTÀï÷#Ë2‹/æ˜>‰ò¼L^öNk¦M»úVŽìÚ„¤R¡Ñ˜¼ô(~/j­—­…/^~¿ßO àŽ;îÀáp°fÍ¢ã,ø}>¦Ì×ãEñ+´6¶‘˜’@kcÕe Œ1‚††à¹•““ ºmDàMAA„ ʰÌQh4Z¼^G‹{îp’ñ“g¢Óé±ÛÚØøùº°ºl^›â‚ÝöW©d²ÇM º²Œý»·žèïõPr8Ÿ´ÌQȲšÑã&³só—}šŸ …ofΨYn{ê“gÙ}tO§ÛÎVJƉע*ÚÐçþßyåsd6ì9}D÷¿ñßm}Egøê—oüâÏ$eç¥U—sÑÿ˨™‡¶åoXLJOþ€ë~ö<#§/ =àËx÷ÿî'"6žûßø/ÅÏ//ÍbÜÅWqé½?Åu"E_  ÷ÙüëÙ‡ñ¹]}~­§Kvv6_|1@€?þñgdÅÒ©d½‰Ú/>&~Ò¥DfLD›Š«á vÎ--Á ôz=f³›ÍÖ«~ŸÏ‡Z­&>>~(§xZ¥]ümâ'/Á^u˜ýþn§mRæßDâôå¸*Øûª!uê2†-¾Yk{¾~ßzZJvözœäÙד<ïÆã¸qdÝïi=ÒùïQ×ý˜’GÑV–Gþ+?ìr|I%“µò`jÏšÝÿéõ܆šZ«Ãçq¡øýmû’Ù+ïF7ÜŒ½¹·ÝFM~>Š¢0kÖ,6lØÀˆÅ×S´íKFÏ]ÒiÍ´šâƒ$eÇÕÖ‚n¸à÷zñ¸œø='>7Ùív\.ååå¸\'Þï›9ܘ"Œø|>š[ØöÕnRâØöÕnÆŒ¶Â¸¬¬ ®õ$‡š¼ ‚ ‚ ‚pA>j e%‡ñû{_¿""ÒBRj:öl ºõVbò0ô#ù?ÄΙú>eXûvêðž”–åB¦×ëÃ>˜ BgÄy"Ô¶Ör¤®4ôX-Ë ‹Öëþ†ç»‹ï =Ö©ƒôrRÆòçÛÿÖ¶¸¶„ÇÿõÔÀ&Ü%}R[ R[÷+ª;stß¶Pýš(k –ÄT¿òýá¥]¶–A·“-ø·DÆ%²åB­30íª[»`)–ÄTâÒF°ý½WPü>f\w9 ¯`óÛ/QU×aI%³ðÛ2÷¦ïÐR{Œ½‰×é`ÔÌEÄ΄K®Ak0òŸÝÓç×z:H’ÄêÕ«‘$‰Ï>ûŒC‡õÜé4µFl•‡pÔÁhÍÄ:u)GÿóÒ™žÖ9oÏž=8N ·Ür /¾øb¯úù|>vîÜÉŒ3¸æškøâ‹/°ÛíC<Û®©4z¦<ðzè±$ßT²†i?ügXÛ@ ÀîgoFéãêàÓÍš{—­Àkk¢©p ~·SâHâÆ/":kF/F‘~å÷‰Ÿt)ŽêÚ*òQ©µD˜Š>&™ì›þ·~NsÑö½ëóÖcJEİqh#b»\á‘6YkÄçvÐrdo¿_óP8V°—kß­vþú­ÿ×i»>ø€Š¿= ÀºgÒiÍ4Y­AQJp‚©Z ÃÆÉÎÎæèÑ£½|ŧ¼ ‚ ‚ ‚pÁHHJÅÀ‘¢¾¥™L–€Óa§ª²î¬ÉÁ‹¿n—“æÆú°mQ–’RÒ9RtˆÌ‘ÙH’Š„Ä*ËJúµ¯ó‰ÙlÆb‰Âf³ÓÜÜ|¦§#œ¥Äy"ücÇ»ücÇ»¡Çñqü{Ý_dI>ñøxšFI’žhè"ÕÚ` Äd÷ßp¤_ýßøá­¡ïçÝr oÿÜö6^}ð}ÇßW_J9rúB,‰©Ä$§óò}×RW¼š>i‰#Ç2,gZ§7€¹7}‡½Éžÿy¨æÏç~’ëû#£f^LöÜ%¤Ï¥,¯ã…îÁ0sæL.¹ä’~õ5™LŒ9€¤¤$~úÓŸöØÇãñðë_ÿº_ûë-ÕñUµ»>&ãò{‰›°˜òÏÿvÖOÎv6›W^y…»ï¾›+¯¼“ÉÄûï¿OUUU+_{í5FMbb"O?ý4ýë_9tèO×ÿ'~¿¿Ûí!©N~_Suú<€·­á¬?ot–DÒ/ ®¦k-ÝKáÛ?«U™1‘¬zþÙL˜¼„øI—(Yû[êö~Ú¦Rk±üÄŒ™Ëð«`ïówv¨‡×p`i—¬BRÉÄŒ™Gõ¶÷;Ýeä4œõå¸û‘s(mÿÕn·_vY.Vk4ëÖ³tÔÕWZ­–.ÎÚµ›Q”ªãµÞ¬Öhìv­˜Í</Š ¡!˜^3%%·Û‹Z-³tétÖ­ÛÆÒ¥Óyùå`ÊÞœœ ìvÇ$‰¹sÇSXXASS±±‘èt<“#G¶Ÿøøà罫¯žƒÇãåË/ ˆ‰‰¡´ºÉKo`çÚ7ÂjÔÕÔŸ¨ß8XDàMAA„ ÆðQc¨­ªÀnë[}…ØøÄ`ßêþ§nŠŠŽ é” ÀèœÉx½îÛAJZ&Z­Ž¨èXxÔj5jµ³Ù ‚*B§Äy" †½åyÜþò]¡Ç?¸üA&§MdÅžX÷›Ó>Ÿ€!xñPr }¯;›ßz)¬þš£¥Kb*Ûß5tkÀÝåXyŸ½ÇºgöœßçeÝoÊwß\ˆ¤’™tù CxKNNfæÌ™'''§WíNÇ*ÜöLõyŸ“¶øÔ†bÆÎ£~ßú!ß÷ùníÚµ”””°|ùr-ZÄ¢E‹ú<ưaÃxä‘GzlWPPÀƒ>ØŸivKñºØþÄ5¡ÇI³V¶øN¿7ìùsEòœd5ŠÏCñûOuˆµ–î¥ìÓ?“yÅ÷ºC’Õ »øvª·¼tP|J>|†ˆ´4¦hâ&,¦fû‡am¼öfZJvc9˜œÎo’,5|2´ïèïK>cŒF=%%U,]:¿_áõ×?àÆ/à¾û–ãtº©¨¨G’ ¼¼½^Ã’%SÙ¸ñ×_?§ÓÍOk”.Y2øø¨ãAµã´‹‹‹D¯×0{v6›“ÈH#»va0h1´LŸìïtz°ÙœhµêN÷Ó>_FÆëõ‘‘‘A~~>sn} 8û «QwøàÏý؉À› ‚ ‚ SD$Ö¤TJNZí¦Vk°&ÃïóQ}¬¬Ó¾’$—@KS0LBb i™YDDFÚZ›)?r˜ÚêÊ.ç`ŽˆÀé¯i‰!)5‚{ðz=8v´ZÇWç]èÚ(K”ª]ç‰p63 h4¼^/·ÿñ´^=w<þ{ÀãºÉõ‚ÿ”0þãé–»z^ÖhºëÀuú|k]Çí#eìdRÆLîÕ¼ús\wíÚÕ¯´Ë—/'--²²2Þ¿ó•-ñùzŸÖº¿ç‰J^âõ»í4Ø@ü¤KI˜ºTÞŽë÷ÏâE‹˜8q"­­­´¶ž¸yËh4ƒÏ磺º:ô¼J¥"999ôØãñP__¢(¡çRSƒ—ÖÖÖâñx¨©é]:Ù¼žóeÔtZŠwv™ÞÑëhévŒ¨áSP‚—×ìèü=ÉïqÒxp#ÖiWb•Û!ðÁt“–‘ÓˆHƒ62¾ÃжÈô ÈZ^—–’Ý=¾¶³M}}3uL˜‰,‡¯ŽT«eZZìÔÕµàp¸°Û]X­Ñ ž„Çã#!ÁÚÞ.!Á‚¢Bý}>?jõ‰qÛû·¶ÚñxüÔÕU‘••Ê¡CeX­Ñ¡þíÛÛÇ~QËé°3yú<Ò2G…µ‰²Äš6œŠ£ÅìÚúß`=ƒ“Ȳ:4†Ç~÷{vÎ$|^/Å…BuÝ´:}_îY/@°€½^ß»×èr¹p¹tèõzT¹€ètÁúZâ<ÎeDFE¢V´¶´ÒÔÔË´VíÙìNIw¾ª)9DÊØÉĬ ¤tPèïq---¥´´´OóÊÎÎfذ`ºè^x¼¼ÎÓhĀΓ“ÎÚ]눟t)©c1&dà¨-íð7É…d Ç5--Ç‹ÅÂÎ;y饗¨¨Ï|0þ|~ô£QWWÇÝwß ƒn=ôÉÉÉ455ñüóϳeË–é)×®]‹J¥â׿þu¯ëè<9è,V´Á ¶c…=´îZTf°®²×Öˆ»¹ë€§£&˜æ×˜Ùéö¦‚Íø=Nd­Ø±ó9¶9¼fž%+¸ºÖV~Ÿkh4Çë¢tG£Õ…¾™=Øx+•åG8Z\€ÃÞ†Á`"}ÄhRÓGš>§ÃNþ¾ðt2íH€°;“#£¢IJÍ 0o(àÖþáRVŸ¿Ù|ª­}îÛžN0dú^™¼¸ærõ}µ8O„³E”%*l¥@DdDï/”;›!*‰€>bˆfwv±5ÕÁ•æ:“W[׫Vt\û@’$V¯^$IlذaH‚n0°×Ó^‹ÀVY€£¦£u8 S—Qúñóü½_uw¾éïq•$‰þð‡X,8Àc=Öë•eW_}5³gÏÆçóñðÃSVÖù]ýqºÎû³•Æt"­§µ®ÿãÞI²šŒËïí²>&ø÷ªÚÐù{°âuÓtèkâ&\LLμ°À›J¥Æ2| M…Ûú=×X¶l{÷îí0î«ö`×;ïü7ìù@ ÐiÐ =ïóù;ôk¯«qOãÔ~}Ý×른¸ø¤y+ø}C{CÂùû)NAAḴÌQ¨5ü~e%áwǪ{x;©ML\»¶~EyiQè9»­úºjýôÓA º ëM¡ïX­¤Ö›ƒ_ ‘X§]Ùc{Iî:ŒRŸ÷9q.Æœ<ÅZA‘9 •Ö€×ÞBëѽýžë@““3àÀ›Ðw"ð&‚ ‚ Ây/sÔ*ËJð_UÖNÝMšvþ“R]>¸/,èv²’Ãù ÏÊA%ËX“‡QqôÄ•þ“.þ«T* ¸Ú-yX‡î ›W¨V˦?9ÓÔJ°^uuïÓ§™ÍæPúÀêêP¹44k·TW÷®î ˆóD8û´4·„¥†kkmëu_©¾2gˆÕsãó€Fwb…¹ËÖÚMËþ×yóæqÇwôyn’$ñƒü Ïý\.+V¬è±Ý@ΓSÕç}NÚâ;uFâÆ-¤µ¬›UzJ?V}Κbý=®£FæœN'ùùù½Þ_DDD(°»sçÎ>ζgƒyžœ‹÷‰ð’ºç¿á»âsÓ>º›«)ýäÅ^ì¸ës¾åÈnös§vf«Á¬Ek·Û;ÔfëŽÑh }o³ ~M¯Á&¹Ëv®¦7™‡Ó\Ôy€Q‹Æ¼‘ÉÕØõMHg£þ×ö׋ƒÁ€ÓÙ»þMMM¡¿“’’†¤&à`ž'}àø›ˆ¤:íûnçj:†ßeCÖ›1%÷%pkÉ’f®@cŒÂ³¶t@óªÏûœ¤™+0%@Œ>6YkÀÙt [eaÏ ‘²¼£D©#™„½º•ZÜŸ¢Pùß7I™w•ÿ}“¹sçrðàÁÐÏݘ1cˆeãÆC†È™û)AAA8 ÔÇÓÐH’Šxkr‡¦ˆHT²zÎp<×Îï÷co ÁÚÛwF%ËhŽæÜîŽDZ[š0GF…V»²ÚMo0†‚{­-çÎåC©=’¦Ýç‰p>’lõÈÿ €oÚ-Ëe ®Ñ›£Ðè=´:1)>Ÿš3•¸´‘mßpgÔ‘$I¬^½I’ؼyói º 6Û±Õ%Ä¿¸ËTwŽê’P Íš{U—ã%N¿€€â§ñà¦AžíÙgÏž=Ôj5‹/î²]{úðv'”šrÉ’%]Öë=ëø*Þà*tmDL§Û5Æ(¢GÏÚI´ 3-#sј,ÛH “—t;LKéžÐÊÍÔù7xZŽê’Pð.6{61ÙÁãÐtpÐû“ƒ-Úd!ÚhÁ 5PÖPz>##ƒŠŠ æÞ|ÉÙ™¼ôL\rzs³nXuâùe72yÙ¡ÇZƒ ­ÁÄ’ûeѪqñ]?fÖÊ{Bãúý~wÜq7Þx㉉H`ˆI {|òײ²2FŒÚœ““CQQç©ýÏ"ð&‚ ‚ Ây-o÷V¾ü÷û]þ+) ^q9¡ç*ËJ:ŒÓÔLIŸØå¾""O|ø·µ¶tØ^W¬ke‰%95ƒÒâÜ®ð]B≥µÕ¢:@ss3•"˜"tKœ'ÂùJÞ¬?ˆìåýçèÞ`º1I’XðÍï…­fPÉ2)c'l¢½tñ]?fÊ7!©NÔøŒMÍdùŸÀÙÖÌ®µož–¹teÉ’%deeáñxøÓŸþtFç2µ»Ö;n!¥ëS•ÿ oËÈ\†_ùsth›¬52ÿ&’f]@ÍŽðÚ›zÜ·Ñ:sJv·ÿTr÷é¾uщ=ŽÑ¾ o°ÕÖÖ†VÛ|ë[ßbÆŒÚdggsÛm·àpœHúÎ;ï„¶¯^½­6< BDD?üáQ©T^¯¦;ÓÚKSt¨~Y;sÊhÆ~ûYtQ C>ª¯ÿ €J­eÄÕÿ‹¬5„¶Éz#¯ù1–Qÿ¿Nðû(ûô%bÆÌeø• 6D„µÑE'2üŠï3üÊz5¯ú¼Ïƒã[HÔÈ`–ÆCg6HOvÒhíT·T‡žw8˜L&jŠ’”5½9sLòñsµýù–š êJ Cë&ÊšŒ£¥‰¦ÊRÜö6|'Ýlh·Ûq¹\”——SYYzÞV~•VÆdA¥Õ£6F‘˜{Uè«Éd¢­íD½Â²²2tºî³ŒœíDªIAAAÎk{÷Eç]ÇkÈ(ŠBKs×õ[ÊK‹–1’øÄdLæì¶ŽãfŽÌ‚©);«WUq”q“g Ëj¿Ÿ¢Cáé‡$Ibø¨±4Ö×à°~ms•ÏwfŠÒ çqž?^ú¿¤Ç¥…«N .Ý»èn|~oèñ§>çÝïŸÖùõ‡ªr/ò¾÷ðO¸ßå"ÿÜ}ÿýPWZÈÞÿ“‰K®cÖʻȹèJêË‹ÑèôÄ¥@aá¯ß]Ñë4ýålmfÙ¿dÁ7¿Gí‘´F)Ù‘T2ÅÏGO?„Ûqæ~ÿ™ÍæP0åí·ß¦¦¦æŒÍe ê÷AÚ%w¢Òè»o·o=æ”l¬Ó® ~Ò¥ÄO¼wK-ÅÎ’€¤ ^Bn-ÝKùú—{µïQ×ý¤Ç6;žºNú™i9$dƒÉ„ ÐòÚCøºŽïÝ—(Xÿq² 1Â!ÜGÅ}ZÖÇwýeÔí»w隸¸˜]O=„a˜LÑéò!ú"†ÉdÆ0 Ö~ü lô‚>/†a1¢û˜Lb¶X‡†~ëê¢//>ûì³Ã¶·½õ¼Eôÿóㄹ£Ò.äСCÃâ,Z´èœùlŒF…7™Û"›þ¥£½…¾ž.Ò2²X±fÛ¶>Ï{üíæüŠJ£kMÜ¿Ã0FÄðû}Ô×V³`ábŒˆABBR,†ÉdbÉ…“–]£ªúÝ™}ð)"2¥8SHK}L’}øôŠ öñ‹gÛ“ßÀ(¹„HZ>Á¾‡íá¿ð¿_'úË¿ýƒ}ݬ¼áS¤ææ“š›kk©ÞÅ:þ¤éðôO¿Iñ’•WÑÞ¾ IDAT\ôOP¾b]l{G+Oÿô_8øÚ³ãôžyŸúÔ§HMM¥½½=6ré\öÒùîËä^¸ñ”û6<õ3<-‡È_³™„ì"é®X[ ¿“ö·Ÿ õõ‡ckA~nÂ>ÏiÇñ÷Í\at``€/ùË|ò“ŸäÊ+¯¤¨¨ˆ¢¢¢X»Ûíæ™gžáÁÄã~.¿úÕ¯¨««cóæÍ”””°råñu† Ã`ûöíüö·¿åðáÃ3–ÿt û=úý7™ÿáÄ™‘OZùrŒpÎ=/Ðô¯HÈ.žñÂ@ã3?'èîaÞš`ML%sÑ¥ø{Ú¨ýßïân94¡<š^¸Ok-ó.½™Ä¼rRŠÇÚŒ ®}[&Ttƒè礿i/)EÑݶLñìf^0vïø¯I´-°%u¼ÉÍ7^“OFGLwtDgôp¹ÒÙ´i?þ†‰M¹êreàñøèîî'99@ ˆaDèêŠNÙûg¯á±Ç^ÑåÊÄãñqå• éînÃb±°iÓ*î¿ÿi^}õU/.Ããñ BDמ3±víj¦§g¿?ˆÃa#G=îP¯×‹°qãJ\® ¾÷œöklÊËË‹ß$£"""""7YYÑÛ=·GU-X´„ªe+ô¸yÆÝ7)9…õW¾»Ã‰ÓÙÑF à'%%-V0kn<Ì;o޽6Ùbaí›ÈÈÌ!‰ÐÕцßç##+›Ä¤è75ö°ÏÛÓw’g¡†§£o°–n,‰s&r6+++ ¾¾>ΙˆÄŸQ°ŒÀ­ÿö$¬[ïÅúÂ÷NÝi 6g"ùU$¤¦ðÒÑp÷{S*τԜ<þ¿?DG%ýákŸ¥æÍq$&“W±GR2í´Õ—ùóçóãÿ“ÉÄ·¾õ-¶mÛ×|âÅ™Y€=-³ÙŠ¿¿_ç‘i+¸«ìv;±ŸŸ»ººhllõE«“åçç“Mbb"n·›úúúaSSžsL&’òæcOÉ&äs3Ø^GØŸó1Û$dcKÎ$8Љ§­Ž©®©æHËÅ‘îÂlO$èîf°½~Ü)ZGyï…ˆÿ;ôÇ0Œè¨±¾NM{íè³Á‡?¼ŽŽŽ^ÊËó ‡ ~÷»çøÒ—n ==¯×Oss'&9ÒÛíeÅŠJ^}u›7¯ÇëõsÏ=ˆõ ‚¸Ý>ÂaƒææèþYY©ìØQÃ'>]Oñ‡?Œ¾ôpùåËp»½¬YS…Ûí%55‘;j)*ÊáСf–,)#'' ¯7€ÛíÅn·ŽzÜ¡8ëÖ-èüþøè\ÙǧØ=yw5ñ&"""""2a÷¯¾ø$K—¯&Û•?l=6ï ‡Úê½ÔÕì7†óÚKO±ø‚U—V}Âh¿ÏËÁ};©¯­ž±s‘s“ùènìø<ýšÐº/@Èõ•O)VÐ7HÓÞíÓœáäøÝ4î~3®9œìꫯÆd2±}ûö9[tðuП¹$P__?¥AZ[[imm¬â$ÁÓZ‹§µ6Þ™`ýxZk¦%–¿ïþ¾cÓk¶èì쥹¹ƒ¥K˰X,ÃÚ¬V }}::úôáñøp¹2(/Ï'‘››kb·[ß•æÄb± †(/ϧºúkÖTÅâŠ×ßï!ÓÑÑJee!ÕÕM¸\俦c‘X;0êq‡âœèüf‚F¼‰ˆˆˆˆÌQñvzœ ‰¤¤e`1›ô0Ð×{»u¢l6;©iØìv|^/}½]“Žq®Òˆ7™xɘ¿ŽÀÍ÷EG¾=ó¯XßøÏx§tJ£x;[-_¾œ––ÚÚÚ⊈œcfÛˆ·!‹™pØàÆ×aþó„Ã&“ ‹ÅL(4r´²Õj!6°XÌÜpà ÃàÑG_Ãb‰NK-zòq}ôµ1 #i:´ÿDŽ;^üöÎx9ø¼ƒÃÖx›Š`0@WçÌ­M"""³ùðV쿾™Ð¦oaÙ÷—x§3ëìØ±#Þ)ˆÌ:G"Ëþvj/ ~ôûôÕkíãx*n=òÈÖaÛ#‘ȨE7 ¶= ë7ZÁíTÇ99æx±Æ:îDâO'ÞDDDDDDDDDÎ!æÖw±ÿêÃñNCDdbL&lISQd²Ú§9‘™§Â›ˆˆˆˆˆˆˆˆˆÌ¨HÄ` ³Ÿ»?Þ©ˆÈfø½ì»ÿKSêëí:2ÍÙˆÌ<ÞDDDDDDDDDdÆ tã‚ñNEDâ 1p­Žw"gŒ o"""""""""2c"ƒphìu}DDDfs¼™ Tx™*¼‰ˆˆˆˆˆˆˆˆˆˆˆˆL­ñ&""""""""笲²2êëë㜉ˆˆˆÌemß/4âMDDDDDDDDDDDDdZ¨ð&""""""""""""2 Tx™*¼‰ˆˆˆˆˆˆˆˆˆÈYÇd2QT”ƒÃa‹w*""fw""""""""""ñd±˜Y¶l>eey$$8èé ­­‡={êCñNï´\}õ  55G9z´3ÞéLÊ÷¾÷9–/¯ ³³¿þëÓÓ3ï”DDNI…7™³Ö¯_ÊwÜ@VVꈶÁA?wÞy6Ç!³éñ‘¬cþüy<ñÄ›üð‡Ä; KKKbùò ²³ÓX²¤Œ-[öÄ9+‘SSáMDDDDDDDDDæ¤Ë/_Æ×¿þqL&GvòÖ[éëóžžÄüùóÈÉI£¦¦%ÞiÎI}}vî¬å ÐÕÕÏÞ½õñNIDdBTx‘9Éd2‰DN;ŽÅjÅl2 ¦!+9SÌf3õW×c2™xóÍ|ýë¿ñ³ÕjÁ0Œ8e(ÿ𿤰0›ööüþ`¼Ó‘÷|éK7’ŸŸÉSOm祗vÅ;‘³Ž o"""""2ç$%§°öŠëh¬;È¡ý»'ý@ÍnwPqÞR ŠËHHL  ÒÙÞJíÁwéêh›p¬¬l•U`µZ©¯­¦¹ñð¤r‘©ÉÍM'77€'žxsÔrB¡ð™NKN`MMÇ↜dÓ¦U˜Íf^}¼S9+©ð&"""""sNyÅù8É+(¡úݓꛖ‘ÅêõWãp& 0 ‡ÃI^A1g[žü”qìvç/[IIyel[[ˑɈˆˆˆLYjjbìë¾>O3‘ÙD…7™S¬VEeÔטTß„Ä$V_v ‡“þÞnv½ý=]ØìŠËÐÕÑ~Ê8E¥ X|Á*ìgìíz“É4É3‘Ó1ÕzN;ééI´·÷Ž:RÎá°‘‘ìXß™®rþüyTV`±X8z´“½{ë§4b¯¤ÄÅÂ……8vzzÜìÙS7na2)ÉIJJô…¤±®Ç»ÝJff ]]ƒ¡aí ÒÒGëŠÇãc`À;ÙÓ!)ÉÉâÅ¥ä䤉Dèèècß¾<ß„cLöšÌ‹ÅÌÂ…EåàtÚñx|´´tqðàÂá‰ß_N§¥KËÉËËÀïÒÔtŒêê#ã~ßNüŸ(%%‘¼¼ŒaÛ¼ÞÀŒ^›¡ÏŸÏ¤·× D§]¼¸”ÂÂl"¨«kåàÁS¿Ðf2™(*Ê!??“Ì̬V+ÝÝ8ÐHw÷À˜ý†>߃ƒ~úûIOObÅŠ…¼óÎ!ººúhq¿ªª“)ÂîÝuôôŒSfÞDDDDDdN)*«Àf³ &=­ã’ /Áápâqðê‹O[×-ðsøà¾qû'%§rÁʵdçæÐÓÝÁî·_çÒ ×b³Ù'2"""rÆ­]»˜¯~õ®¿þëx½þû¬X±o}ëV6oþÖ¸ñOWvv_ûÚÇXº´|ØöŽŽ>þíßþ8á8EE9Üyçf/.¶Ý0 þò—müÇ<>ê:kK–”ñíoßÀ—¿|»výóÕ‡>´–Ïþ:7Þøÿ(¼mذŒ/yó¨}~øî»ï‰ ŸÃaã¯þêz6mZ…Í6ü1x(æ•WvóË_>EGGï˜1¦zM¦Û 7¬áSŸºŠôôämƒƒ>ž~úm~ÿûOyŸ}ô£øøÇßGb¢cØö£G;ùÑaÇŽÚQû}ૹýöM#¶úÓWóéO_=lÛC½ÂÏ>ñïÓd }þvî¬å®»~Áõ×_­·^EFFʰývì¨á›ß|€ÁÁ‘ŸO€ÛnÛÈõ×_BZZÒˆ¶H$‹/îâ§?}tÔbï….àÛß¾^ØÁóÏïäßø  ¯~õW¤¦&rÇ7`2Áà Ÿžž~øÃ?jêÔ9B…7™SÊ+Π©®†p8tнKIM'¿°€}»ÞVt›(“ÙDfv.€Ÿ{Þ¦áðÁIǘ«œN'>ßÄßL—¹I÷‰L„íßþí¯Y° ³ùø·ï}ïsÆð?ßüæìÜ9z!âl‘––Ä~ðWåÑÑ655GIIIäÒK«ø×ý À© C|÷»Ÿ#--‰ÁA?Û·¤»{€ââ–/¯àýï_Mq±‹»îúùˆÑUÛ·¤·×Mzz26\0náíòË—ðúëûù9ìëópèPó°mååùX­–SžÃ‰ÒÓ“øÎwngáÂBŽéàÀÆ÷âÍcÁ‚y¼ï}Ë)*ÊåoþæÇ£Æ8k2>ûÙkùØÇ® ¾¾•={êñzý$%9Y° €E‹Šøð‡×RX˜ÍW¿ú«Qc˜L&¾üåÍ\{íJjk[Ø·¯»ÝÆÊ••ds÷Ý·óoüÛ¶UèìX/»w×Åþ¾lY´È{ôh'ýÃö}÷Ýúi9ïSÉÊJå+_ù(W^¹œ¦¦c<óÌÛøýA–/¯`É’2–/¯à¶Û®åßÿý±QûÛlVÒÒ’hi颦æ(½½nÒÒ’X´¨ˆ¼¼LÞ÷¾ ™7/‹¿ÿûŸ9bõ¼óJX»v ûö5°{w—]¶”üüLþîï>HNN:‡aûöƒ,]ZFff W^y÷ßÿÔL^9K¨ð&"""""sFn~!É)iÔ×NnšÉyE¥x=´mœÒñÝý}¼ýÆKtu´ðë¡ïD%''“žž†Ûí¡·wì·ÒenÓ}"¡ûDÌf3‹>ͳÙlÆd^xkoï9£¹MÅí·_KQQá°Á·¾õ[^}õÝXÛÏ~ö'þánŽ»Æb·[ùçþiiIìßßÈ?ÿóŦðX¶l>ßùÎm,[VÎM7]ÆïÿÒ°þá°ÁK/íæCº”uë–ð“Ÿ<:j!*//“ÊÊh!ì¹çÞ5—×^ÛÇk¯ ŸAàþçŸp¹2FÝ,_þòf.,$6øÑáÉ'ßÖ^Zêâ¶Û6òßÿý¨ýO÷šL—ôôdn¾ùrž~z;ßÿþC#ö)-uññ¿ßüæ¹1ã\{íJ®½v%†aðƒ<Ì3ϼks8l|å+eýú¥Üu×M|êSßQ}á…¼ðÂñu‘Ÿ{Ífþ÷_å±Ç^;ͳœšââ\ŠŠrøíoŸç·¿}.vÏýö·Ïó㪪R®ºj9?ûÙŸFFó¡‡^áOz¶¶áŸs“ÉÄ­·^Í'?y%çW̆ ð ;FÍaÞ¼,|ðe~ñ‹¿ÐÚÚÍ—¾ôaJJ\üå/Û¸ï¾Ç1 ƒúúV>ó™k(-uMóU³•9Þ ˆˆˆˆˆˆœ)åçp¬µ»ÿ{—•òX[ó)ö_ks£Šn“dµZ±Z­$''‘žžïtä,¥ûD&B÷‰|ñ‹÷rÝu_ãºë¾Æ—¾tolûwþ<¶}èOKKW3=µœœt6nŒŽ`úÓŸ^VtðûƒÜ}÷ïikë7Î 7\JAA6^¯Ÿo|ãa&€Ý»óÀÏpãëF}öüóÑBZZZ\°`Ôã {{Ýlß>s£þW­ZÄš5Uüò—OŽ(º44´óo<ÀÁƒ£ÿ\7×d:””äÆ Å'¾NÔÐÐη¿ý?9Ò1j»ÕjásŸ»€‡Þ2¬èÑûä{ß{ˆžž22R¸æš‹¦ñ fÖþçSü×=3¬Ð‰Dغ5úYHNNu*I€žžE·¡þ<ðllÚ΋.ªóø_þòÉØßûû£÷‰aüò—‰ü†¦«LH°k]ç9B…7™’RRqåGß²®;a´›Õj£ ¸œ¼yÅcö5™¢SDôõDÂåæ°bõ6\óA.¿æƒ\´úrró fð æ®ÞÞ^z{ûô°\Æ¥ûD&B÷‰Ì&_¼³9úx÷™g¶ºO(Æãÿ…Ÿ .`Ë–½ôôŒ¾FØP±&##…ŠŠ‘?ïTW‰~6l}„ÝPáíÅwÍèÔŒW^y!ƒS5×d:ôõyb_¯^}þ”b¬XQIjj"þóë£îãõúÙ²e/_|Þ”ŽúÓèçsìØñ‚šÃa›tÜH$‘#ѵØF[WoH8l M †‡µÿúøv‹åÜ)¼mܸ’[o½šœœ4rrÒbÛóò2¹í¶ä䤑••JNN:99é,^\FYY>iiIdÇÚ‡|îs›ÞÛ7ohÿ¿ýÛ““‹;d¨=+Ú/77x‹—QPMvvZ¬}¬ãÅ99þÐùÍM5)"""""sÂÐh7{€c­ÇßnNJNaÅêËô¸ikiµ¯ÍfÇb‰þúäôpáªu— û5-=“Âârš³cÛV"‘™{ 4DH¢k-M„ÏçÃçsàt:IN޾¹¬iâf?‡Ãè>‘ñé>‘¹ªª*ºöl âðáÖ)ÅHJrÆÖAÛ¿ì©´ûúŒü™é…vðéO_ÃÚµ‹ùÑþ—Pèx±aÞ¼¬Xqj¬i&§ËŠ ؽ»Ž`pâëù™ÎkrºÚ©®>[ÇmáÂBžxbo¼±ŸÁ ŸðÂèÄ®®þQGx ©«‹ÞCååù§Ÿxœù|Ç×bžè³ôô$’’°Ù,˜L&,–è(ÆׂœkÔÕµ²iÓńÿûÝóÜrËî¸ãƒx½~š›;1™¢k):6®¹æ"^}u›7¯ÇëõsÏ=¢#oºi=n·pØ ¹9ºmm˰¸C²³Sq:m¬YS…Ûí%55‘;jIH°“`gÕªää¤áõp»½ØíÖQ;gݺ5£žßLPáMDDDDDf=«ÕFqi´PÖp¸zØ›©6›ý”ýmvGìë‹–’•ãâè‘zdÐ3@BB%óRX2ŸÂ’ùx=ìßóö8%dŽ>|ËË›üZC#U¢Ï5mçl–••€Ïçt_Ý's‡î™«22Rèîîu «‰8qTÈ¥—V[t9”’’8jûsÏE o))‰,_^Á[oUÇÚ.»l)MMÇ8tèô¦íÍfM-xôhç”bLç5™ÿüÏ¿æßø$K–”QUUJUU)†aðî» lݺ—çž{'6•áh²³£#•l6+ÿ÷s¿‚‚l`fÏåL1Œ‰}V¯>ŸM›V±lÙ|’’&öòÆ\ÒÙÙKssK—–Å ‘C¬V }}::úôáñøp¹2(/Ï'‘››kb·[ ‚$';±X,ƒ!ÊËó©®>›öÄi[‡âõ÷{Âtt´RYYHuu.W¹¹éF$ÖŒzÜ¡8'Ç:¿™ Â›ˆˆˆˆˆÌzÅeXm6ÂáMu‡†µY'Rx;aŸÌì\vlÛ‘†ÚØ6{€ÎŽ6~å•U̯¬¢æÀ‚ÁÀhádX­VB¡É¿Å.s‹î™Ý'r®*S¿O,²¬ZµhB}ÆZϬ­­›}û¨ª*eÆeà oCÓLÎôh·¡)aø¨§É˜Îk2º»øâïeùò ®ºê"V­ZHzz2K—–³ti9·ß~-<ð>øò¨ýSR¢³ ¤¦&rà kFÝçDVëì_ÊjµðOÿô±XAøÈ‘^ziÝÝA : aaaN<ÓŒ»¡éGï»ï Âaƒo\‡aüä'ï 4Ý:ÄjµðÒK»°X̱~ßý5 G›nÖb1;ÎÉ÷´ÕjÁ0"F´ïSO½5"ÎhÇŠsrüGÚT´¡Â›ˆˆˆˆˆÌzeѵ*Ž6Õø‡µYm§^÷!lÿe²æÀžaE·ÕÕì§¼² ³Å‚k^͇O#ëÙÍj ­m⪒““cÓµµµëAùÐÕ]S±­­}Â}tŸÌ=ºOd®òz£?ÓØíSÄ{ât…ÿýß/Œ;µâÆÆcc¶=÷ܪªJ¹ôÒÅX­$ SPÍ‚D"ž~Ç”sˆÏÇé<õËU§Š1×dºìØQÃŽ5˜L&-*bݺ%lÚt1)) |þó×a2™øÃ^Ñoh4\[[7?ýéc§<ÎL®¿w¶øèG7ÄŠn÷Ý÷?üʈ}–-›?ç oC†î‰GÙ:l{$µè͇BáaýÆ»¿Æ:ÎÉ1Ç‹5Öq':©ð&"""""³š+¿ä”è;u5F´Û&Px 0r­£½eÌý<îÂá‹•„Ĥ)d;w˜ˆ>šèÔn'?$×”psƒß}¨¬ûDÆ£ûDæªÞ^™™©˜L¦)M7ÙÙ¦Òd2ÑÕÕÏ›oŽüYi2^~y7wÜqIINV®\Èoì8öì©ãر™]O110à%%%yó²¦cº¯Ét‹D"8ÐÄM<üð~ö³¿ÃåÊà£ÝÀC½ 4¤£#zÍNûYw.ñríµ+x÷݆Q‹n"S•wW³ܨˆˆˆˆˆÌi…%óˆD ª–­dÍå‡ý™_¹§3!¶-¯ xXŒ€ß{˜e?a½·Ñáè[–Y;N&&99™ììèÃ3=$—±è>‘‰Ð}"ÓáÄú–ÉdŠ[Ck¥ÙíVJKó¦cpÐGuõ.¸`Áiç400ȶmÑ)&7lˆN/yÙegfšÉ!;wÖpÁó±Ù&?îdº¯ÉLêéàµ×öÑ)%³²RFìóÎ;Ñ둞žLYÙÔî““ }âxûO™ÉdÂåÊ ¦æhœ³‘ÙJ…7™Õ¬Öèˆ6“ÉLŽkÞˆ?I)©˜-–ض„Ääa1Âá0ž~€Øþ£1[,ØÞ+Ìùýc/r/“cµFšé!¹ŒG÷‰L„î™~ÿñ‘ðÙÙ#.0™LlÚ´jÆóعóøÔ××\sѨûTU•RRâ7΋/î`íÚ*,˜wÚy=ÿ|´À¶zueeù,X0@ Ä+¯ì=íØñâ‹»€èZm›7¯s¿ñЦÓ}MNGVÖØ?{$$)Ìãùÿµ;kéíuð©O]=-9 MÇ™““>-ñΤH$‚ß]Çm¬këpØb#&ãY\—s— o"""""2«íݹ—ŸylÌ?u‡öàóƶmª§§»€ìœ±ßNI=þðÁÝß7Íg2wõööÒÜ|TÉe\ºOd"tŸÈt¨¯o‹}}ÝukËÉIãî»?Ë%—œ7ãy>ÜÂ;ïàCZËÅ/Ö¾råBî¾û³X­ã¯§úç?¿Acc;f³™»ï¾•+k·XÌ\qÅ…Üÿçž2¯7Þ8€Ûí%1ÑÁwnàõ×÷18xf>w[·î]—ÛnÛȧ>uÇñ©ÅÍf3ë×/áþûïŒMƒy²é¾&SuÓM—ñÛßþ#ŸøÄ•¤¤$k3™L\uÕE\uÕröïodpÐ?"F(æÞ{`ýú%Üu×M#båçgq盹óΛ&”×îÝÑŸ•¯¹fň)=]®Œ³¾ ·kWtæ‹/^4âû—‘‘·¿}Ù@ô3-2YZãMDDDDDfµAÏÀ¸í>_ô]Ã0èëís¿# µ•. 'oIÉ)xÜ#ã–-ˆ>ð ø}ã®'“ …ₜtŸÈDè>‘Óuôh'o¿}ˆ+*ùÈGÖ³ti9 íde¥°lÙ|L&÷Þûg¾ð…Ìx.?ùÉcüøÇ_ ==™oû6hâØ±^ ³Y° €ýûñù,_^1fŒP(Ì¿üËo¸çžÛq¹2¸çžÛimíâÈ‘; Ì#)É D A?øÁÃãæ †Ø²e›6]Ì¢EEÀħ™üä'¯â†VÛ–ž‰àXÍ•W.m¯­má+_ùÏQãÜsÏøö·o£²²[o½š~ôrÚ(,̉Ï-·\Á–-{G¬7Ý×d*,3«V-Âá°ó™Ï\í·^Emm ]]ý$$8(*ʉØêíuóÃ>2f¬^ØÁüùùÜ|óålܸ’+®¸ƒàõúÉÍÍ ¤$“ÉD â÷¿‘£G;ÇÍížåâ‹‘žžÌ¯}µµ- ’““NII.o¿}hÌïÍÙà7¿y–+*q8lÜwßÙºu/}äåe²zõyX,~ø6o¾Œ’_øÂ¸÷Þ?Ç;m9‡¨ð&"""""sÛIZÆÒÑÞB_OiY¬X³m[ŸÇ猵ç–PT}¨upÿî Û‹ˆˆÈìq÷)ìg IDATÝ¿çk_ûË—WPYYHee!‘H„={ê¹ï¾Ç©­máoþæý3>M]sswÞùsî¸ãƒ\pÁ|Î?¿„óÏ/! óÔSoqï½óÙÏn·ðÐÔtŒ;îø)·Þz5W]µœüü,òódª¯oåÿ÷Užzjû„òzî¹lÚ ØÛëfûöƒê—”ä #cä:e‡‡ãøºiiIcÆéîà‹_¼—}ì ®»îb22RX¸°(ÖÞÑÑÇO¼É#lQt2Ý×d²Âaƒ»îú6\À7®cáÂè}v¢ÁA/¾¸‹ßýîy::ÆŸmá¿ø ‡5sË-W°`Á<–,)‹µù|^~y7ÿó?§.º46¶sç?çK_º‘ùóçÅ ¬n·—ææN¬V ¡Px’g}f<ØÌ?þã/ù?ÿç#d+èîßßÈüÇãìßßH(dpóÍ—‘‘‘99i˜ÍfÚÛ{Noè{qâ‡þ†aþNMý>93Ú;{pegLK¬¼»x™0{€W_|’¥ËW“íÊ'7¯ ÖæôP[½—ºšýãÆ0[̱BÛÉL&3'¾o2iYn‘³YKK--]ñN€ööž NÅç P[ÛBmíì˜6; ÓÔtŒ¦¦cSŽq6\“¾>Ï”‹©'›®{¢S¯Nd”ÜÙ(‰P_ßJ}}ë˜ûœj$¡ÈhTx‘9­¶z/µÕ{'¼ÿ@/¯½ü΄DRÒ2°˜Í zèë™Ðǯ<«õ!DDDDDDf+ÞDDDDDD¦Àç¶Æ›ˆˆˆÈD|úÓ×pýõOº_mm _ùÊÎ@F2üæ7ÿ0¥¾ßùÎïÙ±£fš3Š?]™kTx9C’’œdd¤Lº_ZZÒ d#ÓÍl6Méû `·ÏÎÇõº&2×è®9CþøÇ-¼ôÒ®I÷ózý3L·ÁA?÷wÿ>¥¾§³ÝÙL×DæÞDDDDDDDDDDÎööÚÛ{â†ÌÃ0Ø¿¿1ÞiœUtMd®1Ç;‘Ù@#ÞDDDDDDDDDDDDDNCÛ÷Kx™*¼‰ˆˆˆˆˆˆˆˆˆˆˆˆLM5)"""""""g¥úúúx§ çÝ'"""r6ш7‘i Â›ˆˆˆˆˆˆˆˆˆˆˆˆÈ4ÐT“"""""""rNxüýâz¨ï¯çõÖ7y®éyüa¼Ó‰QáMDDDDDDDÎ Ù Yd'd±Òµ‚ʯç+¯}._W¼S4Õ¤ˆˆˆˆˆˆˆœ#nxüF>ôÄGøÎöïâº)M-åöªÏÄ;-‘ÞDDDDDDDäœázx©ùeþpð!e.ŠsF"""""Ç©ð&"""""""çœàáH8Ιˆˆˆˆˆ§Â›ˆˆˆˆˆÌI&“iZâX¬Vl6û”s°ÙÓ’‡È\“åÌ Û×çLDDDDD޳Æ;‘3-)9…µW\GcÝAíßa“êo·;¨8o)Åe$$& élo¥öà»tu´Ù×™ÈüÊ*ò KHLJÁd2aý}Ý4Õ¢áðA"‘ÈiŸÈ\•-¼uz»âœ‰ˆˆˆˆÈq*¼‰ˆˆˆˆÈœS^q>΄Dò J¨~wç¤ú¦ed±zýÕ8œ ƒ ÃÀáp’WPŒÃ™À–çµï¼ÂR.Xµ66B.‰‡°X¬¤gd“~Q6Åå¼±åY¡Ðé¤È,—ýÞˆ·N_gœ39N…7™S¬VEeÔטTß„Ä$V_v ‡“þÞnv½ý=]ØìŠËÐÕÑ>jß×€Éd"+Û5©ø³ÓéŒw r–I°F§{õ†}qÎDDDDDä8x‘9#7¿ä”4êk'7Íä¼¢R ::­õhãt§À g öõÐrÉÉɤ§§áv{èííw:r–ðqX,Í^ÂŽc“[«QDDDDÆ·0¯’ÁÀ å9ed§dóÈÛP\\Lff&yy6\® ž|r}äåe²iÓ*ü #‚Ùÿåreàñøèîî'99@ ˆaDèêêà󟿎G} »ÝŠßÄdоxìXôçÿÅ‹Ëðx|AëÖ-áСfÚÛ»ß;މÄD'¡P˜@ ‹sóÍ—ñÐC¯ÄòÛ¸q%.WÉÉNzèÚ;{¦ýÚ©ð&"""""sFyÅùkmÆãîŸT߬œ¼hß¶æiÏkˆÍf}ðkϫՊÕj%99 @Å7à™¦g¹¥òf>Zyf“™­G_£ÕÓÂ`È‹1➈ˆˆÈ9-#)‡ÕN‚=¦®#±í¥¥¥ìß¿Ÿòòó¨«keÓ¦‹ ‡ ~÷»ç¸å– ÜqÇñzý47wb2Á‘#86®¹æ"^}u›7¯ÇëõsÏ=Àé´óþ÷_@NN~;jc…·ììTœNkÖTáv{ñûƒ$$ØcÇéèèeïÞ.¹ä<Üno,ή]×HLtRW×Jb¢cÆ® o"""""2'$¥¤âÊ/ î„ÑnV« ×¼"¡m-M£ö5™LdfçÐ×Ó@n^Åe•¤¤¦ú{9R_ñ¶£SÎ1#+'öuwWÇ”ãÌ6C…¶ôô4ß$æ×û ÏßÇÆ’«¹¥òfn©¼€§Ÿáßvü(ÎÙ‰ˆˆˆœÛrRrHÈtÒÐÕH[_[lûàà IIItvöÒÜÜÁÒ¥eX,–a}­V }}::úôáñøp¹2(/Ï'‘››kÒßï!S\œƒaD8|¸•ÊÊB¶lÙë?´_SS;.WF,ŽÅbâ­·ª9ÿüâaq¼¬YSÅŸþô:@,ïüü,Ö¬©âÝ Ó~íTx‘9ah´›Ç=À±Öã£Ö’’SX±úr=î1 o6›‹%úë“wÐÃ…«ÖQ\V1lŸ´ôL ‹Ëin<ÌŽm[‰LaÄMqY%=]džM;9EˆN¥9ѵÛ|>>Ÿ§Ó©â›hK$/1T{Zl›?ìÇôÆ1+‘Ùáñ]uûîÝ»)..fË–½Üwß„Ã7Þ¸Ã0øÉO%60™LX,fB¡ðˆV«…—^Ú…ÅbŽõû¯ÿz‹ÅL8l`µZ0ŒH¬ýÁ_5Ÿ“srœ!Cq}ô5êëÛF›&*¼‰ˆˆˆˆÈ¬gµÚ(.ÊW‰Dbm'Nï8›ýø4$ -%+ÇÅÑ#õ4>È g€„„$Jæ/¤°d>…%óñzØ¿çíIå8¯¨46âíà¾]“ê{. ™ ÈËsMºïд“Ñbœ¦äœ«îºèË\š¿š@8À}{ÁËÍ[èòuÅ;-‘Y- røðñ释[<²uØ~‘HdÔ¢Û …GôŠ7´ÏÉí'ë8'Ý&g:©ð&"""""³^qYV›p8DSÝ¡amÖ‰ÞNØ'3;—Û¶p¤¡6¶Íã ³£€ßGyeó+«¨9°‡`00¡üœ ‰,]¾€£Gêio¹uäf «ÕJ(ŠwG+s/àwÏ#µÆ9‘(ÞDDDDDdÖ+«8€£MuþamV›í”ýÃÆñ7(kìVt;Q]Í~Ê+«0[,¸æÑÜxxÔýNd6[Xué8œ xúÙ½ýµSö™ ¬Ft-¼¶6Ë)ö<.9996Íd[[» osœÅ½wª»«ãœ‰ˆˆˆˆÈq*¼‰ˆˆˆˆÈ¬æÊ/$9%ºT]Íí¶ ÞB'Œ\ëhos?{€p8„Åb%!1é”qM&]rY¹~¶½úü„GÉëLD×ášèT‘'Ý4ŤôùûÈtf’d;õgMDDDDäLQáMDDDDDfµÂ’ùD"UËVŽhOLLÀéL`Íåèȵ¶£M±}~‘H“É„ý„õÞFc„ÃX,Ö ­·ô¢5Ì+*% ñæ–gèïðyÍ%ÉÉÉdgg*ºÉq]¾.2™d'dÅ;9ÉÆ+q¹2xòÉmttô——ɦM«xüñ70Œf³—+ÇGww?ÉÉ A #BWW?ŸûÜ&{ìu òÞþ™x<>6mZÉC½‚ÅbaÓ¦UÜÿÓ,^\†Çã#„€èïsk×.áСfzzðûƒ´wöLû¹«ð&"""""³šÕÑf2™ÉqÍs?³Åko=¡è‡ñ ô“œšFRJê¸1lïæü~ï¸y-¹ðJç/$ ñÆ–gééê˜ÐùÌEVkôWWÝäDÞ.*Ò+ÈvfÇ;9Ib¢“ººV6mº˜pØàw¿{€[nÙÀw|¯×Oss'&9ÒÓiãšk.âÕW÷±yóz¼^?÷Ü󒓸é¦õ¸Ý>Âaƒææèþµµ-ÃâÉÎNÅé´±fMn·—ÔÔDvì¨%!ÁNB‚U«V““Æÿ¹ë¾i?wÞDDDDDdVÛ»sÕïî³½¸¬’òÊóñyys˳x½ƒ#öëéî 95ìœÑÓ™“z{{q»ÝZÓM†éòuhÄ›ˆˆˆÈY¨³³—ææ–.-Ãb¾®³Õj¡¯ÏCGGƒƒ><.Wååù!rsÓcíCìv+@äd'‹…`0Dyy>ÕÕGX³¦*wÈP¼þ~@˜ŽŽV*+ ©®nÂåÊ 77ÈÌȹ«ð&"""""³Ú g`ÜvŸ/Zd3 ƒ¾Þî1÷;ÒPKQéròæ‘”œ‚Ç=2nÙ‚E@tjʱւ[ºüÊ*Î'ðó¦FºM˜Šnr²No´ð–åTáMDDDäl³eË^î»ï Âaƒo\‡aüä'˜L&,3¡PxD_«ÕÂK/íÂb1Çú}÷»b±D§¥ ‡},ó°ã<øàË#bFÈö}ê©·F3Tx‘¹-2±·;Ú[èëé"-#‹k6°mëóøN—_XBQi÷ïŽýBw¢e+.¥tþÂ÷ŠnÏ1Ð×› sdZÂa›DÆÒí‹ÊÓiqÎDDDDDÆ2TÜz䑭öG"‘Q‹n@l{(Öo¼BÙXÇ99æDb.ÞDDDDDdN›Ìä"Û_‘õW¾ŸôŒl®ºn3m~RRÒHËˆŽºinZy;ŽíŒsV"""""Ç©ð&"""""sZmõ^j«÷Nxÿþ^^{ù)œ ‰¤¤e`1›ô0Ð×Cdœi+Ýý}üéÁû§#e‘9ëÑëÿˆÙdŽý}Oç^~èÁ8f$""""2œ o""""""Sàó[ãMDf^‹§wÐM}¯·¼Á¶ö·Æ-x‹ˆˆˆˆœi*¼‰ˆˆˆˆˆˆÈ9á3ÏÝïDDDDDÆe>õ."""""""""""""r**¼‰ˆˆˆˆˆˆˆˆˆˆˆˆLÞDDDDDDDDDDDDD¦ o"""""""""""""Ó@…7‘i Â›ˆˆˆˆˆˆˆˆˆˆˆˆÈ4PáMDDDDDDDDDDDDd¨ð&""""""""""""2 Tx™Öx' """"""""""""r.kû~) o"""""""""""""ÓB…7‘i Â›ˆˆˆˆˆˆˆˆˆˆˆˆÈ4PáMDDDDDDDDDDDDd¨ð&"""""s’Édš–8«›Í>ålvÇ´å"""""""ñew""""""gZRr k¯¸ŽÆºƒÚ¿Ã0&ÕßnwPqÞR ŠËHHL  ÒÙÞJíÁwéêh·oyeù…%¤¤¦c2™ˆD úûzinj-)9…«/gÐã³ðf³Ù±X¢¿>y=\¸jÅeÃöIKϤ°¸œæÆÃìØ¶•HäÔSEæÍ+Æîpœ’FQéœ ‰´©gǶ­S=M‰#ÞDDDDDdÖ³Zm—F e ‡«‰D"±¶¡©Çc³;b_/X´”¬GÔÓxø ƒž’(™¿Â’ù–ÌÇ;èaÿž·O÷¢Õ—aµÚ€èšnõ5û9ðîNÂáÐdOQDDDDDDÎ*¼‰ˆˆˆˆÈ¬W\VÕf#ÑTwhX›u"…·öÉÌÎeǶ-i¨mó¸èìh#à÷Q^YÅüÊ*jì! ŒÓbµÒpø &“‰„„$2³s)«8Ÿâ²JöîÜFcÝÁ)œ©ˆˆˆˆˆˆÄ“ o"""""2ë•UœÀѦ:ÿ°6«ÍvÊþa#ûºæÀžaE·ÕÕì§¼² ³Å‚k^͇ǎ ±o×[±¿›ÍfÊ+«¨Z¶’ V^J0 åHý)s‘³‡9Þ ˆˆˆˆˆˆÌ$W~!É)iÔÕÑn›@á-tÂȵŽö–1÷ó¸bÓD&$&M*OÃ0¨­Þî¼ÅË'Õ_DDDDDDâO#ÞDDDDDdV+,™@$bPµlåˆöÄÄdœÎÖ\¾ˆŽ\k;ÚÛ'à÷‰D0™LØOXïm4F8ŒÅbÐÚq£ého!o^1É©iØN~ߔ∈ˆˆˆˆÈ™§Â›ˆˆˆˆˆÌjVktD›Éd&Ç5oÌýÌK¬½õ„¢@8Æ3ÐOrjI)©ãư½W˜óû½SÊ7 Ë]…7‘s‡ o"""""2«íݹêwwŒÙ^\VIyåùø¼ƒ¼¹åY¼ÞÁûõtwœšFvN5öŒ+%5=öµ»¿oJù&¥¤‰Dðy=SŠ!"""""r:®»î:vïÞMsss¼S9ç¨ð&"""""³Ú g`ÜvŸ/Zd3 ƒ¾Þî1÷;ÒPKQéròæ‘”œ‚Ç=2nÙ‚E@tjÊÑÖ‚+(*£§»sÌœ¬6Å¥´·Á0Œqs™ ¤ªªJ…·)0Ç;‘¸ŠD&´[G{ }=]˜LfV¬Ù€3!qX{~a EïÍîß=¢hV^q>+Öl`ý•×STº³yø¯cIÉ©¬¾ìœ ‰F˜ƒûvÆI‰ˆˆˆˆˆL]bb" ñN㜤o"""""2§M¬ìµýõYåûIÏÈæªë6ÓÙÑF à'%%´Œ,šSwh߈¾GÔS\^IZz&Ë/^Ï’å—Ð×ÓE($!1‰ÔôLL&F8ÌŽ·¶ÐÛÓ9Mg("""""29.—‹mÛ¶Å;s’ o""""""äqðê‹O²tùj²]ùäæÄÚ¼ƒj«÷RW³Ô¾~Ÿ—-Ïý™ÒçQ¶`É)idçæÇÚ Ã ­¥‰{Þa ¿wÆÏEDDDDDd,K/úÐ%<ùd´øÖÑ]Ã://“M›Vñøão`‘ØL.Wîî~’“‚F„®®~>÷¹M<öØë ½úèreâñøØ´i%=ô ‹…M›VqÿýO°xq@ H "˜L&Ö®]¡CÍôô à÷q8l!à¨ÇŠãõú‡Å߸q%.Wßû¿NûµSáMDDDDDæ´Úê½ÔVïðþý½¼öòS8IIûìÝy|\å}èÿÏ™3û*FÒh_,Éònc0Ø€Á@!hYš&ém{³5mïMºÜÞöwïmÚ´M_é–¦Kš4 Ù „±ƒ1x—mYû¾köõÌï±Æk´<¶õ}¿^~yt–gžóÌ9g¤ç{¾ÏSŒªÓ ñOO’Z`ØJMÓè8s‚Ž3'°XíØìôz=‘H˜€šD<¾ÒÃB!„Bˆ³ZÍtt rï½7’Lj<úè3¼ûÝûøØÇ ŽÒ×7†¢@oï(f³»ïÞÉþý'xç;÷GùÌg¾€Ýnáá‡÷DH&5úúÒÛ··d•;Ããqb6سg@§ÓÊáÃíX,F,#»v]Oi©‹p8F ÆhÔç|ß™rn½uOÎã˧¡¿­$ð&„B!„Ë ‡ˆ„CËÞ? òX#!„B!„ȱ±)úúFÙºµUU³Öéõ*ÓÓAFG§ …"ƒÊË‹il¬ KPVV”Y?ÃhÔŸÏJ3£ª*ñx‚ÆÆ ÚÚzÙ³gS¦Ü3åù|Ab±$££ƒ´´TÓÖÖCyy1eeEhZ*³Èù¾3å\ZþÌñ­Åëõ.eJ!„B!Ä5¢¤$='Y Áqùu=Õ @ý=u®‰B!„+73úÅÅÿÏüÓ4 MÓ²^;ÎBVwÑTUG2©ñÐC·¢i?þɤ†¢(¨ªŽD"9k½^%™ÔPUoû4Mã±Ç ªéa)“ImÁ÷yì±³ÊÔ´tû]¼ýbÞw¾ò‡Ç&)÷¯¼¡."7!„B!Ö( ¼‰B’À›B!„¸–\«·kÝjÞty-M!„B!„B!„Bˆ5JoB!„B!„B!„BäÞ„B!„B!„B!„È ¼ !„B!„B!„B‘xB!„B!„B!„"$ð&„B!„B!„B!DHàM!„B!„B!„Bˆ<À›B!„B!„B!„y 7!„B!„B\sTƒ^º=„B!Äå%¿ !„B!„âš³nw%w|dU›K ]!Ä*²¸L­úBWãŠ"m"®&F«‹ËTèj (`+1£$d"VNîÀB!„B!„¸¦èM*õ×—c0«’õ¶†Ìz\VÌ#ýÇÇIi©BWI¬²¦›+Y[5ZRãÕoŸa¬ËWè*\>ÛDÑ)”Ô9p”ZQ :bÁ8¡é=>´ä•{}9Ë­X‹LDq¦ú…®Ž@ïÒg IDAT˜‡§ÞÉ ïjA§ê8ý|í V—]´âipñÇØÿ¥Dƒñ‚ÕE\ý$ð&„B!„BˆkJýÎtÐ-ì‹Ñwt¬ÐÕ—IY“‹í÷¯`¨m’D,Yà‰ÕV¹1ѪSux×KàüµIE«›o®Ål7ÎZ—ˆ%yùëmLWT×Õ²óÁf¬Å&:Iàí ç]_ŒNM? S¹±¤`7£U§Á €ÙaÄ]ã`°m¢ u×yìK!„B!„× Õ £a—€Ž—%ëIˆkØà©tǸ–L1tf²Àµ¹2ä£M*6¸¹îM˜íF‚“º sæ…~º 3Ñë'Nà å³Úb:3…–Ô€ çn!ÄB ÆÏ©£8½þ‚ÕE\$ãM!„B±&)ŠB*µòyU¯G§èˆÇc­‡"­îº2ŒV=Ñ`œž7F ]«†µÈDË­UXŠL}²“àd¤`ui¸¡œ’zñ0mÏö¬âÊwv?ýÇÇHÄ’ÄB‰BW犰Ò6Q… wÖ‚#íSúî.ý5EÑ­þï.WÒ=I¬ž±Îi~ñOo 7ª„¦¢­ËÁožÆæ6öÅHƵ‚ÖE\ý$ð&„B!„Xslv·ÜñVº;Nsæä4mi\&š7l¥ª¶‹Õ@"glxöÓÇZtY;vÝ‚Éd¡íÄëLŽK@ˆ•Щ 7Vél7-‘ûÚ6Ú UØïöI页ª¶xÐé•‚Ö¥¬¹O½“TR>±°BwÖ_‰VÒ&—‹3=¼dÏë#³‚nÀeÉ$¾’îIbuÅB‰+"pžJ¥ŒK€Wä‡ 5)„B!„Xs›7b¶XñVÕ-9èæ*.ᎷªA‡§Þ‰ÅeBKhøÇÂLöÍßFvg¹ƒYO2ž$ê3ÙXðSt f‡1ç:-© ÄçÝVy ¸*íX‹Lè*Œ‰^?‰èüõP :ŒVÉ„F,?_Vö¹æ 1µ„sm¹òÝ&0¹l5ïIf»‘â{z(â@œñn?ñÈâ‹Ë¹vòa5®¿åÜ×fîʼnX’xxþãžùµ„F48ûü3;Òßw¹,t»”¢S(ª°a+1£TÑ¡É(SƒÁ33õFÃß$¢É%3å9½V,N#)-Ep*Êd¿ŸTÏ…!7!„B!ÄšRÓÐŒÁ`$Ñ×}nIûnÙq&“™`ÀÏþg’5¯[<åÜé‹.K§ÓQ¿®€Îö6™çMˆªÞâÁâ4$ézmþ¸vQPn®á(W›jÐѺ¯†ší¥¨úÙ…§£t¡ëÐÚ;Eû¦‡r¬¬.·xkÎuÓCAöiñßÃܬ¿½[±yV=ºÐöËÞ9ÛÄ»¾˜í÷¯c¼ËÇÁo¶Q»£Œæ[«²‚E¾_ûÞÙU ZC~Û¤ÐVãždvÙô¦ZÊ×»¹ø™ˆ%9òd'Cmóî¿’k'òyýÁòïk;lÂUac¢ÇÏKÎýàšNU¸íw¶b0«´¿8Àéçúfmsó6Î,ž9¶Å¨ÛYNó-•³®;H·CßÑ1Ú_Èü¨Øèfë½ 9×uâÔ/÷Pœ¢@Ó-U4ÜàE§WHi)4MCK¦ˆúcœ}±Ÿ¾cc‹*K\;$ð&„B!„XS›7ÐÓq–drñO²:œETT×pâW²‚nËQYÓ€ÙbEÓ’ôtœYQYkÙl&‘y7DnŠ¢°nO:Û­ëÐPV¶JQ…àd4ëÉõ‹³Ü. ¼™íF ÿèÒ2ΖZß]¬Owä¦Òóþ‘š–Âd5P\ΚÙpg Šç^ÌYŽÁ¢çÆGÖ㪰‘JÁhÇ4Á‰&›²¦"<õNv¿/~åTÎ,ÿH(“]b0©8ËÓÁéÁ ‰K:’W³=Ñ$ã=þÌÏÎ2+³J,”ÀIæßx÷â‚]ëo«¦þ†rãúŽ¡%S”Ô9p×8ð4¸h¹­š“OwϹÿÖ·6P³­ßpˆÉ¾:½Bi£ [±™]¬çÐwÏ2rnu‚‘ýÇÇi¼©Õ £¼¹˜“ã9·3˜õxœç÷ÉݱëòÚØõÈzŒV=‰X’ÑsÓDƒqì%f<õ.ê®+Ãî±pðm93DÑd¦3¾¸ÚŽª×aq±•XØþöu(ŠB,”Î>²™æÌ)[WÄõ7£( Ñ@œ‘Ž)bÁªA‡ÍmÆ]ãÀ»¾wƒg?ÿFÎ`F2¡Í ÊY\¦%[}9›ÞTJ:È=ÚéCKhUÚ(ª´S}9ŽR ¿yzÞ¬“ÃȶûÖQµ¹$}®#™ÐðÔ;ÓçZ½“õ·WsâçsŸk+•6¹é½­¸¼éy//Îx»ñÝëá’Ã?ôý³Œ¯RÐ9ß÷$«ËÈÍ؈Ñj`ðä8þ±pzî¸M%èM*Ûïkä¹¾ÀœY¥+½vò!_׬ì¾Öw| W…âf»qÎ6ó4¸2Àþ9‚M¾‘PV0ÌhÑÏÊn^ÈúÛ«iÚS ¤Ï›‰ÞtƬޤâòZqUØ©¿¡›ÛÌ+ß>³ŒX(1ëÚq”ZÑ©‹OûTØñ@ÜLôú™ì (வc°èiÙ[Þ¨Òñjîïrqm’À›B!„bÍ(«¨ÆîH?IÝÙ¾´a&+kꇂ ö¯¼­±9ýôrO'Ѩ”æc·Û)*r™šZNnqu«ÜäÆVl&KÒùjv¶ÛŽš0;Œœ›bàø8ÃíS‚m)Ð’)ô&•Šõn*7—PRçd¬sšW¾•»£.Ê[Š2ÙGžìÈù$|é:Õ[Jé|uhÎr6½©W…h0Ϋß9“5„¢ÙaäÆ÷´b/1³å-õ9çÔ³½™×î»}C¦N«h»T`,ÌËePÜøžV<õN&z|¼öƒöe•Y}9g÷pvV‡øÎ‡šñ®/¦z‹gÎÀ[ÍöRj¶•’J¥8úãNúŽ^ø|T½Žm÷7RÑêfëÛxî_.8,árøFBøGB8ʬTltÏxó¶£è´dŠS³3wtz;Þ±£UÏd€Cß;› ¤ÎÉ ·PRë ñF/ç^šÝ1 sðmìûð6¬Å&Š*ìl¸«–x$ɱŸt2tz’Ê%ìx`ñHîöhÝW¢(øGB¼øÕS³²ZŒV=7Uà Í™AñÇfepm»¯‘ê-žœÛçb÷XØxW-(ÐwlŒc?éÌÊ*­»®ŒÍ÷ÔSR眳M2e•˜±»Í³ÎµöýýìþõWÛ©ÚìYÕÀ[>ÚDÑ)†þS²—_x[êP€K‘ï{’·ÕM`,Ì«ß>ƒo$”YÞóú·|h3ªAGÅF7¯Ì¾ÏæãÚɇ|]+½¯ œœ`ãµ(:…Рų¾kgÌ ¦ƒÆsÿ~ûê·³8«ÞêaÛÛ× €Ñf`ÝMé‡mzŽrôÉÎYÛ8J-4í©äÌþþ9Ë>3Éð™É¬ew|tÛ’‚€5ÛJ3Ç|ü©®LÆ}*•B§Sh½£šÒ¦"j¶—2Ú=5g6¯¸öHàM!„B±fÌ»Fû–ö´vI©7½ïÐì!s–ªÈí¡¸$ýÄñR€k‘^¯G¯×c·§ŸÆ—à›¸ÔÌSï݇G²æž±¹ÍX‹L ¤Þ–âtöÀù¬©)®{°‰ò¦¢L–¤;|UƒnÕ†Ë ³—‚þ¹*£ç¦=7=g.¯ªÍ%}²sÖ¼eŒ7?Ç-ÜDi£ G™ÿEÏ׺¶çzsv„žÀ»¾ƒYÅh3du¤Cz˜´Ö}5tÊ꜆tvÑÑ';q×80Ù ToñÐuhñs{.EÿñqZï°RÖèBoTs¿V±1Ýá;Ò>•sÞ¥úe™ ôk— Axæ…~6ÜYCÃ./‡•¹³áÎtªÂ‹_9•ÐrÖCQÀ^š>ï‡ÏNå<–X(AÛE—ÕÒrkŠN!âÍ ºAú>âitám)¦ñÆŠÛ$×¹–J¥Ïµâjûœçڕ䥯]ø]ÄUaã–nà寷1Õ¿ús®–ðt”—¿Þ6k¨AßpˆðTk±)='_«uíäÃR¯¿|Ü×bÁ8c>J×¹¨ØP’3ð¦S¼-Å@:Cnµ8JÌ™@ñÀß¡þÑ0¯ÿhiCÊ/•¢@ËmÕ ¶MÐ}x$k½¦¥h{¶W… E¯PµÙ#·5dö âB!„Bq ²9œ”W¤ÿ8î¸(إרªmÄ[Y;羊¢àö”0=™þ¿Ì[Åõ»÷±ïî¸ýîعûvʼU‹ªËº–t‡ÖÔä“㣙åî’2ªj±Úsíº&MMM155 ¾ºJâ RÑêÆî±LhtÌÎZNDxú_çÈ“ ¶Mˆ¦‡¡šéT…ŠV7:½ŽX(Aß±1?ÖÎ3ÿøú¢ƒn«§Ë‰ÅjAUÕ…w `HWÊ›—w>Wž¸„§£sw8=Ìd‰”­›=oÒjZN»äS÷k#9—gÚrέçipe†è뚣ŒD,™™ª¬iqŸßrÚ£ÿÄ8©T:ó¦¼eöû,z<õ燙œcH·ÊéàìPÛäœóõK™l† à .À`ÑÓ~` «Ó?•J‘J¥ˆåèøO¥.< Μm9èT…Òó×ÂÀɉ9çOì?˜0Zõ”ÔÍÿ<×¹ö]Šo±Ç[èë&ß }<ƒóœ÷±pzù\ŸÍj\;ùj¥^ùº¯ÍÓŠ«ìXœ³çh+mt¡7©¤´Ôœ±|¸øË›ŠWí}RTeÏÌ/wi0sF2©1|þ!w•U•pÌZ!oB!„Bˆ5a&Û-ð32x!kÍfwpýîÛ  äžDÝ`0¢ªé?ŸÂ¡ ;vÝJmCsÖ6®"7ÕµôuŸãðÁ_‘Jåî´7™-TÖ¤'rï<›íÖ¼a+ÞªZ^åWôtúsí~ÍHaÒs·-F$!1a6›%óMdiº9íÖóúhÎŒ’X0NßÑ1úŽŽ¡èJêœì|° ½)Ý隊òúÎ1= µÄd‡ÃÓåD¯Oß|Ó>&''Ø †Ú&hÝ—žó庛<5Iÿ±1ƺ¦ç \ÊÓLöÍŸâ á(µ\Ȳ» –Û.W‚™@V4ŸwH=ßH: é(³,XærÛ#â1Ñí£¤ÞIåÆúgwdÏ 3'r_õ&WEú~99OÖR,” ˆc²p–Y˜X8Ã)KÎʈ<5Á`Žá.gô¥ñ¦ Š*íÜö;[è~m„Á¶ BS«7tᥜå6ôÆôµi–èÅ.ÎJqymŒu.}N³dli³Wóu“ËUs<9¦óZk'Ÿí±Ôë/_÷µá3“éyÔŒ*ܳv©ØVŽvLg=äoþÑ0SAŠ*mÔßPŽ«ÂFÏë# Ï‘ù»Zf†Œ†ùï'þ‘ÞõE S°™ð­ìóµLoB!„Bˆkž^o ¶>(ë:×Fê¢Þuƒaö»—2/ÌõÐÔº•’Òrú{;é>wšPÐÅb£nÝzªëÖQ]·Žp(ÈÉ£‡r–U¿®NG,¥¯§#»žÃr杖Х³½Þò%ï;“ù–ÆÉüxkYysÎr+ZR£ãå…ç×Ii)JêèMéaûTƒk‘ {‰yY鹊\YY §cQ©Ñ`œW¾ušo_‡Åe¢r£›Ên’qÑŽiO3Ø69ï°e&{ú~a/µ°ùîº9·› ¸Íd;\Ëm—+Ù‘þNPTeÞvµ§0šnו´GÿñqJêx\ÌjÖüM3ó œÊ¹5s,Ò9ç Î µjXäy2|&÷p‘ói{®Õ¨R·£ ‹ËDë5´ÞQC`,ÌЙIúŽŒœ\Ý{úÌuéÀæ\"þ A|³}áßrI-1’5_7¹\Ídz×N>Ûc©×_¾îkɸÆÐéIª·x¨ØP’xÓ©J&ƒ;×¼¥ùvè»g¸îÁ&Ü5Š«íWÛI¥RLö<=Aÿ±qâ‘Õ ÂÍd»¥RÌ™  ]Xg´J8f­OZ!„BqÍ«mhFo0L&èéÈžÐ]¿˜ÀÛEÛ¸=e>ø½]í™eÁ€Ÿ±Ñ!bÑ-›Xײ‰³§Žgwêét:ê×­ »ã Z2»Ód1uiz½žDâò=Õ,®L3Ùn½GÆæíDŸQÖTDÓîô>ÇŸêÆî1Ó´§’Í÷Ôã Íš+g5MöxîߎQ¹ÁMÅF7%uNTƒïúb¼ë‹iõ¥çŸíÈ=ÏÛL ÍYfŹˆl¶™ùpÄü ætǸѢ§nçÂ(êê¶ë`Û›î©CÕë(o)Î g¶˜a&gŽ8?¼âÂÃ.ö<™¯“y.)-Åñ§ºè~m˜ê­¥”7as›±{,4y,4í®¤ïØ'~Þ½ä ÞbLÚDKΑ–J¥Hi)‚Î Cí5«yíäÃR¯¿|Þ×úS½ÅCQ¥ k‘)“±ZÚX”~¨%šdøìêH Æyék§ð48©Úì¡l]F«w­w­ƒÖÛk8ó«þE=”³\†óÊùî%©Ä…õ«ý!®xB!„B\óš7ÐßÓA,–=ÄÎb²Ì’Ú…À³§ŽfÝ.Öqö$-›Ð©*å•5ôugOê^YÓ€Ùb%•JÑÕ~jÖþ†5”ñ¦×úZü'v»=3ÌäÐаÞÖ8Oƒ‹¢J;)-Ź—îX³™Ø~#(0tz’þãé¡'ËÖ¥³æv>Ô̯¾tœDtñþÓSÓYC‡ù}K"VKhô£ïØ:½Ž’:U›ïkã]ÓD1Ìv#Ü™ïß™ ÜÁ¶ ´ÄÒ†Y]‰±Nc>\v¼­ÅÔn/Å`ֳᎅEýްÉxº]u ÌÛvñ½&¹„ß1ÄÕMoB!„BˆkZyE5vGúIåŽ³Ë v%.Ê\˜s»`ÀO2™@UõX¬¶Yë[ÒóÌ ö ÎîÄÑë×NÆ›Bºƒj±CE^t“!&Eóùl·þããóÎYéN¯ëlÂ`Ö Æ9öÓN ݹøÆÜòÁX‹Ml¿¯‘Cß;»è:øý~ɃxÛ•´ÇhÇ4±P£Uw}1ç')©;?Ìä<·D4ÉÔ`€¢J;ž:'ݯ,ëý/-™¢ïÈh¦óÞî1¯JàÍ7BKjèTE•vúçÜ®¨Êžy=Þ}ù2“Vã~RH+9ž|ß“–bµ®B}¾ù¾¯õ£ñF/.¯-3\¬Þ¤öÅï)|fc4gèÌ 7”c0ë1Ù ‹Žz©¦.Ü£Š*m ÎýÎòôpЉH’дü»VÈ ÅB!„Bˆkš^ŸÎhS¥å•³þÙéÎKªf–Y¬ö¬2’É$A¿ ³}.:UÅp>0fwjèÕ Ï=Ú®œuQÎw,9œE”–WRRV±Â£¿6Ì Ë$A71£ù–t¶ÛàÉ ‚ Ÿ‰X’×tŽ—¾vŠ¡3“³ÖwäůäÔ/zVÀZ Õ Coš˜U½1½>Í=ÌÝÀÉqHÉf îº²×)¹ð¾ÅQØìÛx$}Ìæy2’VËx·/3RË­U—ýýsIi©ôç Tn(É 39Ù 49¶çÀ‰ô~åë‹3¿…b²ÏŸa®/\Ÿù¤%4†Ï¤3~*6¸3×Ù¥j·§” NDð_ƹÅ…¾']I×ÎJåû¾æe‚wÜ™a&ç{ ߺŸèÂKBz)&züDév­Ýžû{X5ª”5¥GÞïñ± ‰wâ %oB!„BˆkÚ±×ÒvüðœëkZhlÙH$âå~@8<»“mrb»Ó…§ÔËÙSGs–åpe^|ÓYëþižûÙç­ëÞ7ÝN§£íøa†ú{VeXœ«ÑÔÔ@@ætUØð4¸ Å¢²Ý.–kÈFHQ5Ù·ð>ù (°ãu8Ëmœ~®—ÁShÉ ×ºÞ¨Ò²· kq:ˆ?ÖéËYŽo8Dï‘Qj¶—²ñ®ZBסá¬ÀaqµÖÛkè=2JßÃÇÃDƒqL67y™èóguVUÚ𠇲êºZÆ{|TlpSäµQÞ\ÌðÙ ÁR½IÅê2á[¥`ˆ–Lqê=l¿ÞV7[ßÖÀ©_ôfÍùd-2Ñt~¨Ó£?î\•z\ªÿø8õ×—SRïÀ`QÏ/[¸“»ûðµ;ʰ{,ìzd=Gžè`´ãÂ÷“¢S¨Øà¦ùæJ^ûAûªÍñV\mç¦÷´2pr‚öfÌ‹*m´î«Òò©Õ»Û à]_ŒÉf`ÇëxýGç2红(¬¿½:“QxêÙÞU«‡˜_¡ïIWʵ“«q_ë?>F뾪6•d’¸\ÃL6ÞXAËÞ*Ú_ ûðHÖq( TnòP½ÅÀd€Dluo©TгúÙ|w=¥ë\4ßZÅÙ_õe‚kz“ʦ»ëЛôDƒ1zfNQxB!„B\ÓBÁù‡¼‰DÒ·š¦1=51çv½]íÔÔ7Qê­Äfw Ì.·¡©HMyé\pÉdrÞòfþR‡‚ o»ÆHÐMÌhº%ý´þЙɼ x9Y\&œeV,N#Ûï_Ç–·40=$I`´p”Y2Y8ÓƒAÚÌ\<ùL–"žz'瘟éæJ|ç‡ç³—X2¡&›þãófó¥RÐöl/Ûîk¤¤ÎÉÝŽ$DJKa-6aq™hûe/ç^ÌckäÖwdŒúåØ=®g3þÑ0 £UÝc!MòÜ¿]µ,†þãã8ʬ¬»©‚š­¥Tm,aj0H2¦avÓÃ*éì©s/ .*ër¥¦'"ØÜf\^Z2ÅÀ©…¿'´dŠ×¾–]¬Çâ2±ë‘õ„¦¢Ç#è :\åÖLöeãÞœîÎr+;lÀâJŸS5ÛJñ¶0xz‚¶T¥Eèô:ª·z¨Þê!8!4•ÎÖ³ºLØJÌ@:»ïøSÝYÃÁ^ÌÓàdûýë²–Î×ßYfå®OìÈZ÷Ë9B2®e-ó„8ùL›Þ\GYSw~|;“}´¤FQ…=“IÓñò Ã9LA§b IDAT2d¯4ùh“+Q¡ïIù¸vò!×äÿ¾Ö|œõ·×`??òô`ÀøÂ÷BÕ cßG¶e/Ó_ÈN»ýÃ[³Ö½ñø¹¬PBÙ:ªAÇúÛªiÙ[…o8DÄGoÔa/±d®áh0ÎñŸvÍY—æ[ª¨Û™©f´¦÷­»®ŒªÍ%™å¾á¯|ëô¬2zPRë¤bƒ›–[«¨ÚTÂôP§×Š¢@<š¤í¹>b¡ø­#®%xB!„B¬m‹Ì*`zrWq ×ïÙÇÁ_=Cä¢Ì¸Šê:jêÓ#§OAÓ–Þ©•B2Ü„˜³ÌJyS:³ôìþþ×fyBSQ^øãÔ__NÍöR¬E&Ü5ެm¾=‡Gèxe-1÷½$KòÊ·N³î¦ ê®/Ãl7¦³Ï‹âô¼1Jç+ƒ‹B³ïØ(к¯“Í€»öB½ÂÓÑUþïRÉ„ÆË_ocË[ê)o.ÆQjÁQšîÜMi)&ûLêªÞ Ýá?=¤iO%ÎrkÖg”Œk œçÜ‹—'è6£ÿø8-{Óç‘ö©¬,ùÆ#ø¯“´ì­¢j³k‘ kÑ…ùJý#!: Ówd4çþ:½.“9Ã`V1˜ÓA“mþ!ßμÐÇè¹)Öí©¤´Á‰ÍmÆæ6gÖ§´£ÓœÝ?0o¶›NÕÍù~ŠN™µN™cn°®CÄ}1Z÷Õ`/1SÚxẠMFi?0@ïÑÜíq¥ÉW›\‰ }OZ鵓ù¸þfäó¾ñǘèñe²Cû9̤¢Ì>'/vé:š=SVJKqð›mTl,¡á/E6\^.ï…mÑ$'Ç9»`Þ¹Ýô&uꨪáÂ{­¹Ã(©¼þ£sLi¼±³ÓˆÉ^Œ¦ihÉãÝ>Îè_Õ,^qeR¼^¯üe'„B!ÄTR’~Š3XÛ6µnaÓ¶<ýäwæÝÖfw°÷®û0šÌhÉ$c£CÄbQ®ât{öuŸãµ—Ÿ_V]ÞökïGUõ¼þʯèé<»¬2®]OuPO]k"®&×½£‰Š nFÚ§xõ;g ]¼°¥37ô&•d,Ip2Jxzþy»rQt ¶óY î”^LöÁ\e9ˬ˜4-Eh2zYL3Ú 8ˬ¨±PÿHxÕ† ›‹ÅeÂê2¢U¢Á8¾áÐe™ p5¨6·³Ãˆ–ÐŒEˆæî˜^•:èu8J-éÌ…Ìçšœ'ȼšln3Öó×Mh:Jh2"ó0]a®„{Ò•píäÓµr_3ZõØÜfŒ=Z2EÄ#0.È5¬(JúÞfÓ“ˆ% NFˆøc¤R)R©T:§iY¯Î¹çŽ—ÏðØ$åžâ¼–)oB!„B±HÁ€ŸýÏþ„­×íÆS^A™÷ÂõáPö¶ctœ=¹ü7¸úú;„¸lì%f¼­nÎÎ3üâÕ&4Í ¹·)-E`<²ì`Û¥eM™ZqQ+ Æëœ^xÃUž^^0ôJ”Œkø†Cø†Wg޼EÕ!¡15,Øû_*8)X`Y,ΕpOº®|ºVîk±P‚XèÊxˆ0•J1=̼k›Þ„B!„kZ{Û1ÚÛŽ-z{¿oŠÏý³ÅŠÃUŒªÓ ñOO®øì'¿ÿÕí/ĵ,NÐ~`›ÛÄTÿ•ÑÉ&„B!Ä¥$ð&„B!„Ë ‡²æxB¬®X(Á™ú ] !„Bˆ5a½·…P,Dci‡‡ïz €ÚÚZÜn7^¯òòb~ò“ƒŒŽ¦3Ô½^7÷Þ»‹'žx MK¡Ó¥çË+//&Œ01áÃn·‹ÅÑ´ãã>~û·ßÊcÀhÔÆQ”ôž##SlÞÜ@0!‹‹%¸õÖ-œ9ÓÇððÄù÷Q°ZÍ$Ib±D¦œw½ë6¾óç3õ»çž(//Æn7óï<ÏðØdÞÛNoB!„B!„B!„"£ØV„IoÄb´Ð3Þ›Y^__ÏÉ“'ilÜ@GÇ ÷Þ{#ɤƣ>À»ß½€}ìÂá(}}c( ôöŽb6¸ûîìß‚w¾s/áp”Ï|æ[˜ÍFî»ï&JK]D£qnÏÞ<'f³={6„‰FãX,ÆÌûŒŽNqìX7Ý´@ œ)ç7Îe—Õj¦£c«Õ´jm§[µ’…B!„B!„B!ÄU§ÔQJkÅz&‚ ]4Éb(Âf³166E_ß(v»·Û‘µ¯^¯2=¤¿œP(ÂÈÈååÅlÝÚH,– ¬¬(³~†Ï$‰SVVD*çÎ ÒÒRY?³ÿÌvÝÝÔ—gÊI&5^y¥mV9~˜={6eÊ™©÷¥ËóIñz½2ÓŸB!„kPII €Ì•$.¿®§º¨¿§®À5B!„båfæ{¾øÿ™š¦¡iZÖk§ÓYÈê.›Á` ¶¶–sçÒ™dªª#™Ôxè¡[Ñ4ljdRCQTUG"‘œU†^¯’Lj¨ªŽ·¿}š¦ñØc²ÊÓëU4-Å;ÞqsÖúKÍõ>3å̘©ß¥å MRî)^Q›Ìª“Þ„B!„X›$ð& IoB!„âZ²VoךÕ¼ÉP“B!„B!„B!„BäÞ„B!„B!„B!„È ¼ !„B!„B!„B‘úBW@!„B!„B!„B\;î¹çÊË‹ùÉO0:: €×ëæÞ{wñÄ/¡i)tºt~Xyy1Á`„‰ v»…X,ަ¥÷ð[¿u/?üá‹@êüön‚Á÷Þ{ßùÎó¨ªÊ½÷îâK_z €Í›#Äbqb±BQn¹e gÎô19é'3<6™÷c—À›B!„B!„B!„È«ÕLGÇ ÷Þ{#ɤƣ>À»ß½€}ìÂá(}}c( ôöŽb6¸ûîìß‚w¾s/áp”Ï|æ[Øí~x/@„dR£¯/½}{û@V¹3<'f³={6„q:­>ÜŽÅbÄb1²k×õ”–ºøƒOþkÞ]oB!„B!„WUQI¦’…®†BˆÕ¤Óƒ–(t-D¾èôŒºW”±±)úúFÙºµUU³Öéõ*ÓÓAFG§ …"ƒÊË‹il¬ KPVV”Y?ÃhԋűÛͨªJ<ž ±±‚¶¶^öìÙ”)wÆLy>_X,Éèè --Õ´µõP^^LYYš–Z•cW¼^ïê”,„B!„¸¢•”” \±u=Õ @ý=u®‰W–e×ñ‰íçO_ú3zü½…®ŽBˆU*ª&öÞ/£îPOA=ýL¡«% HoB!„B!„¸ª´V´à¶¥‡Ú\µ ‡Ù?â_r9v³Þüëìnº)çú—Ú_æŸ~ñ…Õu1v–]Ç]ÿIEáóG¿°ì ›µ¼‘Æ·ý¶ÊæYëRZ’îŸýÇžXiu ¦¨eÞ]ïãÈ¿üV¡«³$%›n@§p·î‘À›‹ ·:q5ìÀè(ÁQ»‰‰Sû \«åK9+ˆýúù Û‰§ƒn©•gÜè&Þü‘ÿ Àþâw9ñË«ç>ßzË›Q F6í»ïª ¼¨‡¾^5¸óI¼ýoP¾õ;(ݯºZ¢@$ð&„B!„Bˆ«JÛà¦BSY‹8ÑrYA7ƒªçOÞú)ê=u$µ$¯u½ÎÀÔ:·ÍÍæªMœ8µ µÏVl*æoøCô:=w<ÉÏ=¾¬rÌ%Ulxÿ_£7ÛIDLž:@Ô7Šj²b.®ÀÙ°éÎ×ó\{±Xã'_ ú¶_'•L0ÙöR¡«#ÄU!òáë:‚³~ñÀþž…®Òò):âþ=)WºÁãûƒ¼Ý®v§<ÍŽ{A59ùÜ ]Ó¿ü_¤Ü $·>Hü¾Ï`øÏ!tõd Šü‘À›B!„bMR%/óB©z=:EG<ËC­„‹1žæ÷¿ùIJì% -s8È[[nÉÝþßÅ™¡³YëEÉGuçõ×}—ÑI—¯›/û·e—S½÷½èÍvb¾QŽýÇÇfÍ#¦èô¤´ÄJ«+–©ÿ…o0vôY´X˜xhºÐÕçyw=€³a;‘±z~ñ¥BWGäÐöèŸ`.©":=rUÏñ–Øý›hõ7B"Šá»…ÄÕ{,ùÔñÚ~þþ‘ÝÍ6¦†®yM ?ÿ4ZÕ´¢joúcôyNT\}$ð&„B!„Xslv·ÜñVº;Nsæä4miO&š7l¥ª¶‹Õ@"glxöÓÇšsßÍ;n¤ØíYð=F‡i;~xIõb-‰&b L .{ÿõÞz&zgÝR¤òœŸÏöÒmÜä½€Ïý‰Æì5›?ù«YA7@‚nW€èÔÜß ¢0Š[nÄÙ°‰d¼ÐUsH¥4ÂcWy@Æì qÛ'ÐïÿÊDw+te MMb¢ÐÕÈŸD ýÓEìŸGk¾­b3ÊÀ±B×J\&ÞOvxB!„B¬AÍ1[¬x«êh;¾´¡×\Å%ìÞûfLf ñx MÓ0™Ìx«j1™-¼ðÌÜsk8]Ÿ=å ¾ÏÈÐÀ’ê%„X›É€?¼ôa*óåáæ_àäÄ)Þ=²¢²ô ɦW9ƒ½˜Òmobºó ‚g ]!V,qýûÀd‡xõà— ]qè:_D7tÍÓ„¶óݨx[s$ð&„B!„XSôz5 Ítž]ÚüM«Ý·ÝÉdÆ75Á‡09> €Áh¢¶¡‰ñÑÅ {×ÛÕΙSsw´GÃá%ÕMˆkÃìÀl0å\7öK,m¸×Ë1Œä|J-¥ÜP~=?ízjÅå-çxE‡ÑU @<0‰–« “«,½Mp-YQ=Ãè,ÅQ»½ÉJ,0¯ë(ÉhpÉåìnµ›0X]$"A‚§‰LÌýPƒN5`p¸ˆ§ÖÎT”~ˆ"̪Ÿ¢S1:sg7k‰8ñÀÒ³;{u+æ’*t:=ñÐ4þÓÄ|£‹.c©m²:”Ìq¬.Õ€j²b´»±–5`-oEÁøêãóÞGõ,žZP¢SÃø{Žç>Ï»ø3PôÆôrƒ9óyÎHiIb¾±•ê"ê±Òs-Ë2Ú@5ÙÐ[왟µxŒxð¼TFG Žº-éó&$Ð×Fd¼oîjèTl•-XJªÑÌ$£A"“ƒÎÒ’ó×ÅhAouæ\—ŒIDó<[eK¦ £“Cø{O.˜¬·8QMa?Éh(S–£n Fg)Z,Œ¿÷ä¢2Y“×=’ÞÿäOQÂË8B§ªÔnÙEIM#‰X”ás§j_ú¼w³•º­»py«ID#Œõ´ÓßvæÉòv•W¡( Ñ`€°jÞòíîRôFñh„àäìkÈYêE§æKL ÷Ï[¹”7¶R¾nF«ƒhÀÇpÇ)FºÎ,º¬å´ÉBÔ£¡ÝñI’7£³¸ (s½­%xB!„B¬)5 Í Fâñ}Ýç–´ï–7a2™ üìö'YóºÅcQÎ^|çG<%à“Ì!ëý{ÞËÍÍ»s®ûÛ§>ÇëÝo\æ­Ì¶Ò­™×¯¿V:n¶ü¿8ý­ÿ©³gm£š¬™mÚðÆO<¿jõQtzêßòÊvܵDŒþç]t9ªÑBý[>JÉ–}(Š.k¯ó :~üD'gw–ë &¶}ä‹(ªžþ¾Aßó_›ó=ì•ëÙô›@Û×ÿÓÙC›ŠÊÙöÑÿ̹opð,Ç¿ø»‹>€òî£zïûr$ü=Çè}ö+ø{çþZn›ä[ɦ½ÔÜù›™`n–T -# &9O0ª¨yõ÷|dV°, Òû˯2üêã9÷³z×±éCŸ›]^Óõ™ó|Fpà,ÇÿsiŸÓbåó\›±Ü6(¿þ­ÔÜñÁÌÏÎQ…Ú»>„÷Æw èÔÌúÉÓ/qæ;ÿw޲î£jï{0ØŠf­KFCŒyšß&È„(Ùt oûDÎuƒ/Ÿž§¿8çqdQªn}7>€j¶g­J„ýô=÷U†=9çîµw~ˆÒwÓ÷Ü×zå‡Tßö>Êv¾Ýù`-©ƒ0oR® Rî:tíË¿Öl¾žþøsy«³–÷Ÿzƒ§þéÏ]ΞGþ;·¾÷c­¶¬åý]üäïÿ”ÎÃrî÷kþ/T®ßJÏÑWøÊï¿kÎòU½ùÌv'û¿ñy~ùŸŸµÍ‡þù1oÎýÿúm›ˆ…C‹>ž†ënæîý9¥uͳÖMôw±ÿëŸçÈϾ7oËm“…è:_L¿Pth•ÛPÎ>·¬rÄÕIoB!„Bˆ5¥±y=gI&?ç‘ÃYDEuºãäįdÝÄê3›ÍD"«Ÿi#®\#¾:G»2?ëU•wÍ¢÷ßZ³…ß½ë#™ŸMútöܦª|ñƒ_ÈÚöÜHõã¿]Y…ªg ã‘qÆ#ãKÞ¿úö÷ãÝuægÁ @ÕÞ÷RyóÃYÛ¾üú_øÆ j{9(4=ø‡¸7Ü@x¤‹éÎ×AQp5ì æÎ‘\D¦‹Þâ õ=ŸÆVÙ ©Ó‡ ÷a°Qܼ gÃv6þÆg9ùå? :=’µo"`êì+·î¡dÓÞyƒ!îM·ó§ëy -#8˜=w ©¨½%w&Ï\EGÃÛ>Aéö7Mãë<Úé¯~*óó†÷ýÎ†íøºrö{ŸÎÇ¡.Z¾Îµ•¶ Àø‰ç?ñÿþ}DCÙŸñ‰gŸàMÿý¡Sõl¸í^^ùAî¹ê6ÞöVNe¬'÷Èßü“eý¼íî_ãþO-ía—Êõ[yëïE§Ò{üßÿÇ?v!kWo4±ížwRѲ9wЕ·É¢LvCÙzpU.}_qUÓ-¼‰B!„B\›702ØG0à[Ò¾%¥é!qF†–Þi"VF¯×£×ë±ÛmͺJˆÅ8Ò{Œ~é·3ÿŽô¦³^Ž÷ÈZþÁ/ý6ÿþ|î!óÉat0]Ú½hFï/¿Â«ŸyGæßÌÜk}Ï-kù«Ÿy£GžÎ[½WKÕ­ï 4ÜAÏ3_š`|ùûŒ›?ãÀVÑŒgË>:ÿܬ¬™˜oŒs?LwîºÖíÄZV?«ŒÉ³3Ù<%ç;î/e¯ZŸ ŒËÝ¡›z«“êÛßÀÔÙƒtýôŸgÍÕNÑóô0|艜eä£Mò¡lû›AQHFô??;èó3zô€Ì1çR{×o¢èôúOÓõÔç³Lƒ/}Ÿ©³¯€¢P±û¡üDžåë\[­6i|ÛïëåÔW?Ep(;€’Ï@X<5™‡Æ?—³Ìðh7íýõ¼ó혢dΡ‰Sû³‚n©d‚ÎÿczžEE¡âæwÎ[\*™àô·ÿOVÐ Ò™yþÝUsïoIÿÞ¢D|°À¼r¹lºý­xj×ðÓü³¬ ÀÔPßýóß™· UoàÎßúC^þîÎz1ðøß|’àä¶b[ïž}ž§Æéxm¦Ns½Ïú›ß¤3äVÓ[~÷ÿ¢èTücC|óO>”tHÄ¢¼öø£<ùÙ?š³®+m“E™™ÓÏìXÞþâª%7!„B!Äš`s8)¯HcÓqQ¶›^o ª¶oeíœû*Š‚Û“îøšžL Wæ­âúÝûØw÷Ü~÷ìÜ};eÞ¹;^r1Œ”WÖÐ䆦 xÊ*fͽ#`jjŠ©©i ¾‰+’ÅjÁérb±ZPÕÙhs±ÒsE’2„ª¹¤êÿgï¾Ãã*Ï„ÿÏôª2*£.K²lYÆÝÆ\À„¡%„@ ¤±À–„äM¡l6Ɇ}óÛÍfCÞd“eÆÀ ¡ 1`Šq·ä¢.«×išþûc4#5£:²\îÏuùòhÎ9ÏyΣ3GÒ¹Ï}?²"×ÏHplV@À3þ¼˜Y‹7‘’xýGßO¸Ž«ížá’‚ó׌Yb7ØmÃíÈV½ ˆ Ýõ ×IÛ ¨´‘’¨- ‚U“‘Š1I…h`ÂÝÝD(èO¸N4£³f%œËNm0“^± €Žÿ”0û ˆš3*VÒ?SSq®Íæ˜hŒVêŸÿ1AïÈ|[ápˆp8DÀ3ö¿{佌k'µÙ`-\„Ö’ @מW® øè9ø&9çãæm;»³GÓþ„˼ÃÙÄãm1RiŸg¢®'T¹~+žÁ>jßN|<îÄÙ®Qå«7bL‹ŒÉL\–ÒçqsèÍ?Gö¹ö‚„ëDKGV¯$-'ÌòŠ5›Ð›­„‚öoK>§àLÙ K)¨ZÀÎß?‚×å˜r©“‰(þÈ÷=l˜Z™aqú“R“B!„Bˆ³B4ÛÍåtÐÙ6ò¤µÙbeõú-¸]NÚ7%ÜV«Õ¡ž[Äãv±âÜ””ÅOâžža£¨¤œ–ÆcìÚùáphÜþä–RZ±0Ön”Ë1ȇ;·Ó×sê—†›‰0F 2wÛ‘í/ IDATd 14¤Ç`0`±˜¤ì¤˜sV«•´ô44šÈçxp`¾¾ño€ŽˆÜ ?±ÌåÙÈZT{íj=<ívÒÊ–$,¥7š»£cN)&{YÂåÝû¶‘»ò2 ™ù˜ó+OÈSb%1»÷n›v_'#P xc2Õ&+Uc2SÑL=ΔtµÞ{Ç,O+Y‚¢Š|ÖÆ;wg$@¥ÒÐgæ3Ô;½r®'ÃLϵÙ“ÁƽcJ06¼ø0 />œp}OW#ÎãµX ’wîÇ1TÒ¹ëEúï$à™z`dº¬%‹c¯]mɯ'®¶£@¤”£É^†³µvÊûŠf3\º8¡h04AiàÉ(^¹´9@84ös1e+6àìé¤œÊ u5ä–W%\^»ã|n:“™ê-WòîÓ¿Œ[^=œ wìýí¸ûg¯dsùê‘@uÝo³fr©“ E¿ïü] Î<xB!„Bœñ4-%ó"²†c5„G=®ÕŽó”rt>öz~ÕR²rì´6×Óx¬·ËÑh¦´b!E¥•Vàq»8¸÷ƒqÛ4Œtu¶áèÃï÷cMK§ hfkçm¹Œí¯üÇà™X ¨"Ùyyö)oÍ|‹ã$[HÌôŒô¸,7kšuÒ7‡ÏAŽ1³6y âlÍNð:º§ÝŽÎšD摚wÙI׋—ÔÆÄ¥¿Mðöw Ï°“U½1.+ýÓsài÷u2tiÙxûÚ¦ßFŠÆd¦Í°-:CvúÌ<¼}ícÖI›·ˆd¾½®1ˣǿþÂI2çTš‘ŸÙ£e¦]ŸU3=×fsLz&(íšÈá'ÿ‘Êë¿…µä¬EÕX‹ª ‡C8›Ð{hÝ{_‹•ל-Z‹-ò"ÆïL~=ö Ž\k´£Æq*ÂI2 ãx"¿Ë…§YjМ¹ vMÿ:`ÍŽ”KWkµ\vÏ?&]ÏV¹­‰+ ø½jþú–^|‹·\xSku,Ø)3¹÷•Ù-3™6\þ wšó.§jL&ÖG>oÊÐôÊJ‹Ó—Þ„B!„g¼’²J4Z-Á`€¦ºø§Ÿ5“ ¼ZÇ–Ë®oÒÜp4öžËé »«Ÿwˆò‹©X°˜#‡öâ÷ûÆ´õÁ;o V«ñy½O˜‡åHÆ>6n½FË’ky{û_¦z¨g FC 0õ¹R„8U ú" V­Ìû—é{Ýœ¬h@Ád/›Tæ–J•ì¶X˜žý¯Spþ§°Uo¢éµGbK²GJÿ Ô„ÏÑ3í¾N†f8ôOÿƒÔÉÌt}ô쫯Ä`+¤âš{9úûâŒfv+ä­½†ô²ûé„mŒ 殸tRûUféxRgfçÚlމoœ U2~g½—ô²d/ÝJúüÕhMéXK–`-YBñ…Ÿ£eûc´½ó»‰›&µ!r=IVÒ4jtrt`2Õ”h‰\ÔNþwµV‡føá¯€Ï;í>¬‘r—Æ´LV<ùбýj’Ÿ#û^}–¥_GAÕ22òŠéoo†ËLš,xÝN')‰™*Ñ‘áp¿wz%C×î—¶1º\áÑgþ™à$îΆ”ô6Íä\;UÇd þ#ê?EÁR°[Õy䬼 ÁBÉE·£(JÒëLEË?ªÔZ@!Ùœ‘£ƒmÁY,…©tü.ΩDi¿ìëhA¿`ÀZ£à¦ÃãˆdÝõ·7óÒ<8áú¡¥^£êw½£§k–ê-Wðöÿþ)3ypû 3 NÆ3ÌT­Þ8­à[*Ç$)EEØ6/òº?q9{qæ’À›B!„âŒfÏ/Â2üTkÝ‘±7;´“¼Fe®uuOºžËé   Vk0šÌI×OW{+ ª#ƧgfÇÍGw&QˆÜ$™l©ÈƒnRbRœ `­V‹ßïÇç|¶ÖÞî½\Uv­…BK!­ÎSwªÙp”ÕÕY³ð tN«ß`7Zs&ဟþ#;gÔ'Ow3®¶#˜ó+±UoÂÙZ‹µhº´Bþ!zkÞžQû“áìÐgæO»TŽÉL܃‘ù®ÂaœÍÑÛ Ð§Û 9èÙÿ½Ä`Þ¤Û. èîlÀÓufÜȞɹvÊI8Œ³µgk-mï>ËâÛŒ>=—üó>IÛ;¿Ÿp>Üéð gR* :«-i¶ .=gdçìe¯ªß½.G=…À€{ k–´ì¼‰WNÂ1\¦Rg0säÝ™ÍM‡8°í¬ûÄb7µVÇ‚õ°o–ËL8ºGJÕf”ÐY?õùùR9&É„³+@gÿªÖ½³²qêRÍu„B!„b6•V‘‹—­aÖKãþU,8ˆÌ¹}/¯0>ÅçŠÍã¡›à‰ã豓™;.兀¸‰öu¶°X,dgGæ_‘ ›8ÕxÜñ¸=§ðDüž®}±ëʹö5³Õ½ñšŸHQ”¹éàjÉ"6çWN»úÝXKÏAQM>›6™î}‘›±YÕ›[õFzíˆeÕ̦èñhŒÖiKªÇd&Š·ÞŽµx1»þ̡ǾÉîŸÜƇÿßìyøŽ>ûиA7GÓ>ÂÃeúÒæ-KIŸÂÑl¨9<ÿaúçÚlŒÉlñ»úè«5 Z«mVöã<>RRÜRX•t½è2¿{`V³g7JWdî¾PåSÞ¾íð~ò*£(Ó»•_÷áL6rËL«Ñö½úù•ç`+œÇüs7£7Yèom‹Üø»ªìò9éCÈ?rÑZ³®“»röûæn¯‹dC9K/J¸ŽÆ”FæÂ ã¶Ósà ‡Ñš3É]5ó~÷ìßN8B—–µ¸z¤ôß¾ÙÉŽ8QoÍŽXP¥ø‚Ûˆ”ÍK"Ià(Õc2i¥KxÝÓÚ>èuÓô}ò×]‡ZošqŸ‚žH9O}ZÎkήéžk³1&3¡µŒLSëFúrÍJûð;{ÈI2ïZoŽ7ûjÞŽ{a6hÞ €à‚­„Ó¦–ÁZ¿k8@”ncþÚ- ×YqŧÆm£á£·qõG²ú6ÝúwSÚ"íGƲ̪·\IõæH™É}¯>;ëc ÐÕp8¶ÿu7Ü›ó-‘dÁÊTI‚\vêÃÛP¼2ÇÛÙFoB!„Bˆ3Ú¾vòÆ_žKú¯îðA†<îØ{­MucÚéë”.ÊÎI^êÇš–{í˜VÓ2FnZ¹œ³7çÈ餿¿Ÿ––V º‰3Γ‡#s[‹ÙXpþIß`È+Ö³ì¢á9‘"TZ%[?OÉÖÏÏz?Âám;#åÉÌ•m¹•ÑA&ƒ­êÏ<„>=wÜvÜíutîþ ¥‘¼µ× ¨âgY±WS}Û¿½,q€o4¿«º]”\ütiÙø=‘¹«N¿³—Ö7 @zÅ**?ñmt'HÍ ¨ºùûÌ»ô+ ÛHõ˜ÌD4«¨`ý ,úÌ©øø×(¿ú«”_ýUÊ.¿‹‚ó?…9¯bÜ6š^ý¡€}FU7ÿcvqÜrµÞLÁy7²øö£šÄ|ƒ‘òoæüJ2¬;¡-&{ÙŽpúfr®¥zL¦+ýõ,¿ë 7Þ„Æh_¨(d/ÝJöÒ p¶ÖÄÍí˜JápˆÖ·ž cþj 7ݘÖ,TÞð-4 !ÿPì36›Ô=…âÀù_žÒ¶»_z Ï`dîÅËþöŸÈ,(-STj6Üø%¶~áÿŒÛF0à畟}€E›.ãª{ÿ£5#nÌü®üÚ¹êkMª_Ѭ·%}ü¤–™Œzñßï#cÉÊåÖ}‚ü…Kâ–§åäqùß}ÏÿôÔ ªPÌÆ˜Äµ_}9áŒ"T»ž˜òöâô's¼ !„B!Îhn×øÁ«¡¡È“÷¡PˆþÞ¤ë57¥xÞ|rò 0[¬ ƒbeó#e‹|Þ¡¤sÁeçæÓÝÙ–p™¢(Ì›¿€Áþ^œŽéïÎD@`®» æØÿ¹ük”f”UzŠýÎ ¿D è}ýÊm<óás'µÓQÛw˜×š·±µøBþfÙ—ù°sîÀô²¦«ãýç)¾ð³X «Xzç/q¶D¥5`-9ÑJû{ cþ ¶‚YíGûÎgÉ\°Ka…o"kñ&\mÇКӱ–œCpÈEÇ/`_}Ÿí4½üŸ2òH+[NéÅ_¢pãM¸Ú"¥,Ù%èÒ²К3éÙ÷:áÐøåA{öm#£b5–‚ȵ¹gÿ“ÊèH/[Aŵ÷ƽ§ÖGæ©4ÙËYùÇ-ÛýŸ‹Ë@Œ:þöÓ²ŠÈ^º[ÕyØnÀÝÕHÈçA—ž ÄYŠq|ÇSqs~ÍÖ˜LWÓ«¿ÄR¸}†=iYÄâ n£ÿèûûÃbY£ õçØ³ÿBŵ_ÇR¸¥_þ®Ž:üÎ^4Æ4Ìy(Ùã¶êtï}mÜ>uí~ûš«1f³àÆðt6àut£5¥cÌ)%èu±çá;N3Ko*¦{®¥bLrW^Nþ†ⲃʯ¼‡àð9ÙüêÑ[³#i•šŒùkPiõm¹•ÂÍ·àn?†ÏÑ‹ZgÀ˜]ˆó»ú©á'IÛ*Üôi쫯Œ{OkŠDì«®${É…±÷ÝíuÔ<þ1mt|øi¥K±Uo¤hó-䮸WûQT–¢E¨uFÂÁÇžûQÒ9àRÊïAóêCø¯~ˆàšÏ Þó ªÖñK«FùÜ.^ø·osýý?%=·€¯<ò2 »ßÁçq“¿` yEìüý#,¿ì“èO¨Ø0ھמ#·¢Š 7~‰å—~‚s.¼šãµ{ñy\¤ç]Z‰¢(|^v<ñÿèmm·_û_{Ž ï¸—ì’ù¯ÝKwÓ± G«7r÷oߌ{O£7Ä^ßõØ›qçý³?ø»XÖßhMûÞçÅŸÜÏew?HnÙBîøÙékkÂÕÛ…1݆­pŠ¢‡™îjw¼<ëc6¤¼è¨ö?ªë(³Ÿ(N5xB!„BœÝ&Y§«ã8}=¤gf±zÃì|ëU†<#7âò‹J)ž™ƒ§öàB¡Ð˜6ÎY±–Š‹ii<ÆÁ½âq”Ñhµ,]¹ŽL[¤ÜUÍ““Q!ÄéÂj°’nLO¸Ì¬‹/¯fÔ®w*ú=?ciörŒ9|uåßó½÷~02ïÔIÐööÓè¬6rW_‰>ÃŽ>ÃÀPO+ />LÏí˜óÊg=ðò{©}ü>J>ö²—nÅ`+Ä`+`°qõúwLö² oAŸ‡šÇï#à ØW_‰ÎšEzùÊØr¿³—Î^¢íÝg&`ê­y›y>j€î}ãr¢­9qù3E¥³,Y9´p(ȱ?üˆÁƽ䯽cî=ûßàøŽ'Ý¢ë¤rL¦Ë7ØÍÞÿ÷EÒËW¢ÏÌG¥‰fX*‘kéRÌydÌ_CÕÍ?àÀ¯î!û³´·fÞ_¢Í·1Íp–ÜÈ|®u»8þöÓ Ïo7žPÀÇ¡ß|ƒ²Ëï&sÁ:Œ¹ó0o8ÀÙRƒZo>)·éžk‘mg6&£Cf| D­ÅFô;4Q Ëp(ȡǾIÖâÍä­½KþÌù•˜G5ôºéÙÿ­}"é¹ÝW²ÏŽJ«G¥™ÿVcJRþ;æè³‘ßv„üõ7 KËŽ˜MûiÞökÍÆ=®TRïz’à‹-üþë~ŒîW×¢xú'µí¡7_äé¾ÌE_ú¶ÂyT¬Ù ÀsW~þÞýݯ([y¹e Çmçµÿü!m‡÷qÞMw’7¿š’%#ó‹ú‡Üxãv<þ³I˜»Úiܳ“yË×Ãe&'AQ)˜3³“.7gÄgõjÆ™ïøÃ?>FW}-o¹›²Uç“™_Bf~äP À±ßâ¯ÿŒæý$m#•c2|€ø¯úgÂ&Jo#Ú×ÿU‚ng)%//O¾÷B!„Bœ…²²"Ø:g÷œó«–°xÙÜ.'¯üé©q×5[¬lºè*tz¡`î®v|>/Vk:陑ñli<ƇïnO¸}iù–®Ú€J¹Á:Ðß‹ÇíD£Ñ’iËA=<—YíÝÔìߕ£<õ4¼ÔÀ¼KK'XSˆ3_UæBþåüƨ1òÄá'yäÀ¯Oz´¦tLÙ1Þþv<]M'½QSƬ"Ôz3žžf¼}íÓjGQ©1Ø Ð¥ç¢ àèÀÓÝœâÞž\ú ;úô\T:~g/)ËNõ1É_w=%»€ÃOÿSdþ­qhMéè3óÑšÓ xœ¸;ë§$Óš30ÙËQiõ\‘¶f©âlJå˜LWô3¬1Z ø=xºOÊü_'R¦¼r´!Ÿ‡¡¾¶q³)lÊÄ÷¹§ çÌGÕ°ÝonQÙÚQTj²‹ËIÏ+ÂërÐvxiΜn/$#¯ÑŒ³¯‹Žc‡ÆÕL62 J1§Ûð8è8vŸgjç}*ÆÄÑ7 œûð ¢ýím(m ‡Ã„ÃaB¡¡P(îuZZò¹£ÅÉÓÑ݇=;ù\S‘wo 7!„B!ÎZx‹˜Jà "ó¸-]¹žl{ü“á·‹£5û¨;rpÜí-Öt-]E^A1*•:nY_O'µ÷ÐqüÔ¹ :[$ð&D¼9Ëùþ†ï¢UiyôÐox¬æñ‰7â4 Ö±¯¹ €ö÷þ°¤f”J­eõ7ŸCQT4¿öÇß~úduSˆ“"œ–ïó¿#œQˆêÈëèžü2¦<§–À…_ÿá às£}âTïÇ‚nx;µÍFàMJM !„B!ÎjGköq´fߤ×w ö³ã1MXÓ3Q«T¸Ý.}„'ñ$·Ó1Àû;¶¡Ñh±¦g¢×ðû}¸]ie˨ÿh®»yÖê9ø&E›?C8 ¯æ¹îާ…€{Á†=¤Í[†ßÙ‹£éÀ\wiÎɵþÔ*?À¦» BóÂ}¨Z÷žÖA7€ÚÚZ/^,·iÀ›B!„⬤( áðÌÿVk4¨~¿oÚm¨T*4-~¿Ÿp84ã> !Î>E›nFc°àìbß/ï"àŽ/5¦¨4„C9êh}óqº÷n#äóàwÌuwݼs¯!­l9CÝM4½öÈ\wG$PóØ·0dâè$ä÷Îuwæœ\ëSOŸ‘GÑæ[ÐgØ©{þßê=>õF iø¯ù( ê]O¡>ôRê;:L& sÝÓ’Þ„B!„g³ÅÊù^Ac]-‡î!šZ°K§ÓS¹h)…%eM?Ým­ÝOOWû„m(ŠŠyó«˜W¾€´ ¡PˆþÞnŽÕÐÒx,%A!ÄÙÁR¼€žƒo¹ ÈØS€·⟠âäÊ\°–´²åôýsÝ‘D8ÂÓÝ<×Ý8eȵ>õtÖ,²—n@Ñè¦ÕF`ýí„ÓòQh¶ýk*»7§ìv;;wîœënœ–TsÝ!„B!„8ÙÊ+«1Mä–N9è–ž™Å…—]Çüª%Mü~^ï–¼Â/[3a:žó·^ÎÒ•ëH˰ ò¸Q[v.KW­G§7L÷ð„g!Ñ @@²©ÄiNkɤà¼Ob.X0×]â”#×úSÖHpÍ­¨?ü_”¡Sç{sÅWPTT4íí_yåÇxÅÄ$ãM!„BqVÑh´—E&£¯?rhJÛMfÖo¾½ÞÀ`/»?ØA_OZž’²ùôtuŒÛ†¢¨8wãEزrñù¼ìýðmŽ77‡Q©Tä–¢R©ðy¦w€Bˆ³’¢(ÓØF….=¿³P AÉ\EAŸžYÇ5@È?4£~N†.-kÉ9hô&|Î^öôº¦ÜŽÖbÃZ²­)À ×ñÚqKˆ©ÔZ´ÖH²ßÕ?aY;}†€€Ç9¦ŠJ.-;áv¡€¿³w*‡2ܦKQ†¬BT* ~÷ÎÖZ|ƒ]“ncªc2;”ØqhMé(j-j½ ņ)· “½ ÝûÄuüð8Í(X‹aÌ.EÁÛߣiâóxØèï1Œd·¨´†Ø÷3* âìžÙ¡N¢3=×âLcLÔz3£%öuÈïÃïê‹}­³fa-]9o¼.œ-5 õ$ŸóIQ©1,À˜U„Jk èu1Ô׆ëøa¡àø}ÑјÒ. ¹ 9ÇÝ>Q{悱1ôöµãh>8af˜Æ˜†Zo$àqôºcmYK— KË!äóàh>xÒ3Y§s­?‘Κ…É^9¿wW#îöº ·Óš3QiG2ÂN<9¥X ¢Öñ9{q4îÃïêö}F.á`Ÿ£E­!cþ´æ œ­5¸;ê#kÅÞÏ~?y¿µSnº´l´æL‡ðtâl9DÐ7þïÑ£EkɋРÛ܃ã¶\t)aS¤ õž§ÇÝïÉ&s´Í ¼ !„B!Î*Åe•hµ:ü~-Ǧ´í’ëÐë ¸œþºíÏqóºù}^ŽÕ˜°²Ê*²²í„B!ÞÙþú{Gnê…B!Ž7×O©OB1]Z«åwÿ€Úÿ}þ#cËI©õ¦Ø:GŸù!=¶ÏZ•†y—ÝIîŠKaÔ]m)9 IDATÍåPÀGëöÇ&ÝŽZgdÞeCÖ’ P”øbOƒõ»©{áßñö½Y®ÒêYvç¯PÔZß|œ–í¿IºKÁBßþcj~ûmêvÅ-×gØYö7ÿ•p[WÛöÿêžI€}ÍUmº%a@ÂÑ´æmâhNþ3hºc’jY‹7Q¼õöX07N8L(à#àuáíï 8N0*£ò\æ]zç˜`YÐë¢ùõÿ¡ãý?&ÜΔWÁâÏÿÛØö毎çQ®ãGØÿ_Sû>MV*ϵ¨éŽ €}õ_ø¹Ø×#ç¨BÉEŸ'oíµ(*uly_í;~ê»IÚºŠÂMŸFkγ,èuÓµçŽïx¿³/ÁÖµx3eWþmÂemïþž¦W~•ô8â( …?MþÚkP,q‹-oüü)éæ%[?OΊKhyã7´¿÷E›o!wÕ¨F—" ‡iÛùÌäû4Ç4Æ4J/ùY‹7Ç}?<ÝÍ4üù§ 6îMº}ÅÇ¿JzŪØ×Ñï‡Ú`fþµÿ‡Œù«ãÖo|ù´ï|µÎÀò»ÏÑÃÁG¿FÕ§¿‡ÁV[·áÅŸ1P·‹…Ÿþ' ™ù‘7Ãa^|˜Ž_ˆkW¥Ö2ÿúo’^±*þû1,èóÐöÎïh}ë HR¶ýÄc‰ªºùûcÞ«ýßè?ò^’QPÙz”žz”Þ¦¤ëÍ™£mîHàM!„BqV)¯\@SÝ‚ÁÉσaMË ¿¨€»ß‹ ºM–¢¨¨¬Z @ÃÑCqA71>ƒÁÀÐÐìgÚqº(Úr+yç^ûZ¥”§-Üt3ç}2nݶwŸ¡õÍÇOjÿ¦NaþußÀ¶è|< ÔŠBzÙ Š·~žà$2]4F+UŸþ>æ‚J‡¨Û…§§­9ƒÌÊsI+[Nõm?âàÿÞθmCNú¼GfÕ²o7b[¼Ÿ£'ÒÏ„>\mGâÞÓgØÑgò$£(*Ê®ü[r–_ €ß=À`ý‚>7[Ö¢j¬%K¨ºåìþÉgã²”R1&©TpÞ_øYŽ}Hÿ±¸0dc_}(*š^ù%]»_·œåSvåߢ(*|ƒÝ Ôí"ða-®Æd/gÞ¥_AgɤùõGÇlô¹lÜûÚl/Cm°pâîjŒ[w°aÏÌ:‰Tžk0³1ðöw0X¿E£ÅZ¼8¼+½ä‹ä{Ml•Î0œ-™ø³X|ág)8ïFÜ 8šöôyÐèM˜òæc)X@Þ¹Ç`+¤ö‰û¶áwŒùì˜rËPÔS¸­(T^÷MlÕ‘±s?Œ£µ•ZKzùJôvæ]ö7h­Y´$“(cn)Õ·ý“½ŒÁ†= 6îC¥Ñ‘U½ }fùë®ÇÙ|ˆÞš“ïߤêZ¯5g°è3aÌ)! Òô}†zZÑšÓɨ<cv1U·|ŸÃO~—þ£ï'lÃÕ~ E¥FkÉĘSŠ>ÝŽJ££ê¦ÂR´Âa<ÝÍè3óPit Ï­)ª›¾ahÙþÖâjÒËWR|Á­ä­‹x[ßz“½œÌë(Þú9:?z).S2ô£³f£RkqµÅÓÕHÐ7„Îj#mÞRÔz3E›?ƒZoJuwÖÇ2^5z3¦¼òÈ1?Bð„ÌnOgCÂ6bý)] €êxò å\‘9ÚæŽÞ„B!„gÜü",ÖtêN­ÌdAñ<ø€@`òAÌ©²X,dd¤ãtºèïïŸx!Ί¢ŒÉXˆ¼¯‚Þ÷ L¾á\É^²%tk{çw4½öH\¦Bþºë)ùضSzÉW0TâwõQûÄq7ðuiÙTÝüŒÙÅ”]q5gÌöÝû¶‘YµCV&{yÒ2g¶E‘ú=ûßH˜Qáì“ÕV~õWÉYvÑ„Ç0ZÞÚkbA·Ž^ é•ÿŒ+¨5g¿áxûÚÝ`æc’ ÆÜy_pÇwŽ–ƒIÛʪÞDpÈÉ‘ßÿ€ÞƒoÅÞo÷–ÝýߨuF²—]4k·T]ëK/þ"Æœ‚^5}çñÚØ2­9“ª›¿É^FùÕÏž‡¿°¤ió¶ÿ {É…T\s/ú ;…oÂR´ˆžÛi|ù?ñ;{Yvç/1dôŒ=OµŸ³‡Ú'î'ä÷¢OÏeù=¢6Xê=ΡǾEÐëB­7³úë¿C­7cÌ)óù8úÜC܃<ޏ÷Õ:#•7|›ôŠUä­½–ö÷þˆ/ÁM¯Žd$[‹SýÙpìù0Ð@*™ő—=§^ÕŠW^ye®»pÖRM¼ŠB!„BœÊ+«èlkÁåœÚDáY9y‘mÛ§?GBVN¤tŽË1ˆË1·•ãßà›ßü&7nœvW^y%÷Þ{/wÞyg {6–F£A£Ñ`±˜ÉÈ[ºJˆ³Qóëòþ¯ý‹Î½Ö²ý7qï¿ÿÃkéÚsêßx+Üx3îŽ:š^}dL€¡íÝßÓ½ïõqÛ0çW’½äêþøoc²f|ƒÝ{îÿ^± Sî¼1môÙËÒȪN|}´.Œº÷½6Á‘MŸÆ”FÑ–[è?²“†:f®.¿«Ÿ¦W~IÇÏ'l#c’ ¹Ë/E!8ä¤uûØ ‹ÏÑC×ÞWbÇœHÉE·£¨48[kixéá¸@Û;¿”„Sò×_ŸÚƒH±Tk³5&åWþžîfýÏ×qµÇ—æ$¨³‹c¢žýo$lÓÓÕÈÑgw~¸S”Ø9Ô{è¯qA7€p0@ý ?‰Ì³¨(äŸ÷‰q› Ô>ùqA7ˆdæ9†Ë2Ž.™˜j©¸ÖsJÉ:'rhÞöh\Ð ÀïêãØ#'­93쟈1»˜¼õ×Ó½ïuŽ>óÃ1sW&ËŒlzå—±y ýî‘÷_{$ð z]±yø4Fë˜6†zZÇÝ Rf²ñå_‘àdÚ¼¥“:–é Ò`¸|¯âžúÜâÌ%7!„B!ÄYÁlMÞ_Äg›i4Z KÊÉ+(Iº­¢(ز#7¾úzÈÍ+dõú ¸à’kØrÉ5¬Z¿…ܼño¼d ·ÑßiÚžÉÒUëÙü±«¹ð²ëX·ébJÊ RÍþŸjO?ý4~¿Ÿ{î¹gZÁ·Ë/¿œÏ}îs ò‡?üaz8¢¿¿Ÿþþ ¾‰8F“‘´ô4Œ&#jõØl€“ÕΩÒÓ™!«CVäú¹qœxNž€g áûQY‹7‘’xIK¥µÁ3\R0cþš1ËÃÁ@ì»m¸½Ùª7‘ ¡»cö2l 7 ÒêhI¬šŒTŒI*Dîî&BAÂu¢Á5+á\vjƒ96'SLJJ:wS4øQ±jÌ|v§’Tœk³9&£•úçLÐëés8D8"àûðß=ò^Æ‚µ“ÚÇl°.BkÉ kÏ« × |ô|ˆœó‰æ ‹rwÖãhÚŸp™w8Ãl¼íO™ #s…Ã!º÷oK¸Ž»½.–UÍ>žˆJk äõÐðâÃqï‡CÁáódl` 4*8Nòzô×*µvRý‰êi•¦ÔšfùwFcúÈkŸ;ùzâ¬#¥&…B!„g…h¶›Ëé ³mäIk³ÅÊêõ[p»œ´OŒˆÌÝ6CCC é1 X,‘q”²“g/«ÕJZzMäs980H__â2{³ÙΩÒÓµ¨:öÚÕzxÚí¤•-HXJo4wG=ÆœRLö²„Ë»÷m#wåe2ó1çWž%¦ÄnJwïM|;U¢•€Ç1&Sm²R5&3ÍÔÓèLI×Qëͱ×á`pÌò´’%(ªÈgd¼ãqwFT*­}f>C½­ÓêóÉ0Ósm6Çd°q /><&ÈåéjÄy¼KÁBòÎý8æ‚J:w½HÿáI0³ÁZ²8öÚÕ–üzâj; €¢Rc²—ál­Mºn2Ñì3eÊÛžLÑ1ñö'84¶„d”«í(&{9漊È1% äŽÖþþÇ”¥Üûó/ϬÃS¤(*´–LT:ãÈ\€Ã}Wfûa¶Ñc¤’P‹!gƒB!„⌧Ñh)™ ”5«!<êd­vâ§”µ:}ìõüª¥dåØim®§ñX-n—£ÑLiÅBŠJ+(*­ÀãvqpïcÚÑ ·cËÎ¥²j ý½Ý©ÙÇ`/­–¼‚b*-%#3‹5.à¯Ûþ××T;xð ßûÞ÷øÎw¾3éàÛ%—\ÂwÜA?>ø ÍÍÍÓÚw@É~È˳OyÛhæ[$74­ý‹Ó[zFz\V˜5Í:­@ÕLÛ9Uúqº‹f§xÝÓnGgÍ"eÕæ]–¼n4¸¤NP¾ ÀÑtoú ;YÕã‚!±Òá0=Þ˜v_'C—– €·¯múm¤hLfÊÑ|Û¢ó1d¡ÏÌÃÛ×>fhI¸¡ž–„óKE ý „“dΩ4#?³5FËL»>«fz®Íæ˜ôLPÚ5‘ÃOþ#•× kÉ9X‹ª±U‡p6 ÷к÷¾–´ü`ªh-¶È‹p¿3ùuÔ78r­ÑŽÇ©˜ÍßÑRIgŽŒ‰ÏÑ3îz¾áë¯J«G£7Oê{Õ³êçI*¨ÔZrV\JÖ9[0çÏŸ³¬CÅ3òXØ0;×Ï™*]™K[M/¡`˜€7òPƒÁª‰­†!‘v½ÍŽ„ËQ@£W£3jð{ƒ„ü!‚åëò¨{·!Gäá IÛOéÊ\w_ïl!7!„B!᤬VK0 ©.þégÍdo£Ö±eç²kç›47½çr:èîjÇç¢|Áb*,æÈ¡½øýñsñh´‘R9YÙvëjÙóÁÛq7mú{»èëåÜó·b˶““W—7:Äw¿û]î»ï¾ ƒo]t_øÂà ¥evû6FC ˜xE!Ä)/.Óé„9̦"P0ÙË&•¹¥Jš¦gÿëœÿ)lÕ›hzí‘Ø’¬Å‘ÒõMx{¦¢óýÓÀ uc23]ýûê+1Ø ©¸æ^Žþþ‡ø»†—*ä­½†ô²ûé„mŒ 殸tRûUNù ”™k³9&¾q‚VÉø}|ô^ÒËV½t+éóW£5¥c-Y‚µd Å~Ž–íÑöÎï¦Üöd© ‘ëI²’¦Q£ƒ”£“g¢Ø˜&“QËUZ=L"ð6^ps¶è¬YTÝü}Œ9¥ãh9„«í0÷`¬ÄdÑ–[csÎ*¯BAP©Á>ñú'™5ׄ£ËCåù…ø=¿Év^= ƒ,Î"èâìB­ÉóÏOøyžˆ»³!%ýŸM39×NÕ1¨ÿˆú@Q°,ÀVu9+/Cc°PrÑí(Š’4À:SÑò‘yÁ’Í9:Ø<‰¥0çBlL4ãÿΫhGÆäd–ªŠ cN)!ÿµOÜÏ`ã¾1ën¾…“x ‡Qzê çÌ'œ»`ö÷7E¶" »:±æñ{GþžqtºIÏ7ãóôáìö`L×']®¨ ëØëòÑäª1¦ë‡Àï b_Iã‡8»=ø½ÁØ~ÏfxB!„BœÑìùEX¬‘'P뎌ÿD;‰À[`TæZWGò€“Ëé   Vk0šÌc–ûý~tzÝmã–'èÃh2c4¼Y‡æ»ßý.÷ßÿ˜àÛ–-[¸óÎ;éëëãþûï§­múeÏ¢"7Ú'[*òÄ ›”˜<»9ÁZ­¿ßÏ;½,©™¶sªôãtp”êÒY³ð Lïfo°­9“pÀOÿ‘3ê“§»WÛÌù•ت7ál­ÅZ´]Z!ÿ½5oϨýÉðö@è3ó§ÝF*Çd¦îÁHݲpgóAô¶ôévCzö¿AçG/1ذ'éö£Kº;ðtM}~ÑSÑLεS~LÂaœ­µ8[ki{÷Yßþcôé¹äŸ÷IÚÞù}ÂùpgÊ70œI©(謶¤Ù‚ºôœ‘mœ³›½:×¼ƒ]sJѥ医ž~xy`È›—ñT£OÏÍ]Ù¶ó¹„A·“MÕ¸“`Î|B…Ëæº+cô4E¨'Áš÷tùø…QT ¡`xR˽ù@Q)„Ccÿ–‰îÇ’mLéqœŽ$ð&„B!„8£•V‡X¼l͘å¦áà–Á`dÖH™¦º#io¹yåó‡Q%6O[2¡`µZ“pî8Ÿ×ƒÙbT0¹ùçRéÈ‘#<øàƒ<ðÀ±à›¢(Üu×]ôöörÿý÷ÓÞ>v^žÙf±XÈÎŽÌ¿"A7åq{ð0ó2F3mçTéÇ´Œz@Q”“»ïQ\í#YÄæüJœ-c’˜Œúݘó+±–žƒ¢RÇJŽMW÷¾m˜ó+ɪÞDÓ+¿ÂV½€ÞC;b$³i ~7™UЭ˜ó+ãæÿšJ©“™(Þz;ÖâÅt~øõþé”·w4í#  ¨5¤Í[–’ S8š 5‡ç?Lÿ\›1™-~W}µo“wî5h ´V[\à0UœÇGJŠ[ «è­Ù‘p=KaU¤_îÓ"3r&\­‡É¨X!3)-O :&ãÀçÚèÜm‰+PLݨŸ…ÓØZUÿ6ÁÕ7¶Ú Û«P:jRÔ¯™+Ôã¶¹)Ï)#ÛšÍï?x€¢Âbl¶ÈÜýýýTUå`·gòç?GÐèê /ÏÆå—ŸËóÏ¿C(F¥Š”£´Û3q¹†èíÄb1âóù …ÂôôDέO_w!Ï>»Nƒ×ëGQ"¿gtvF´9çœ2\®!|>?>_€—pøp ½ÃûQ0™ A|¾@¬oÜÌSOmõïÒK×`·gb±xê©íètZ¼^? µZZmÂf³ÑÐÞÊË?ɇÏÿ–p(Œ2|éöB¼.Ýï¦|ì%ð&„B!„8£i†Ëê(ŠŠ{AÒõTjuly[kü«`0ˆË1ˆ%-³5mÜ6´ÃA5¯wì ôþ^2³r1[ÇŸÂ`4EÚ:ùs#;vlLð­§§‡ûï¿ŸŽŽŽ“Þ V~O‚nB¤VÈ?R–NkÍJ¸NîÊËg½îö:îA4¦4r–^DÇû³ŽÆ”FæÂ ã¶Ósà Ö߀֜IîªËéxÿùõ«gÿvJ>ötiÙX‹«GJÿíÛ6£v'«·f¥E­¡ø‚Û¨yü>’•ÍCQâ©Q©“™H+]@ÀëžÖöA¯›þ£ï“¹p=ù뮣{ïk§ÙV¬MO¤œ§~‚L Ù6Ýsm6Æd&´~goÒåj)ö:8äš•>8÷áwö¢µØÈYqiÂÀ›ZoŽ7ûjÞNøÙ9“ôÜNá¦Oƒ¢³üÚ”ù´–œƒ!«€ÞC=Ù]œ´Ñç·ÖjK¸Ž!«hÔün‡Ò£æ²Ó¥åL9«®y™€£ƒ°ÕN`ÕMhÿüÀ”¶ŸM™æ ôF‘¦žæØûóæÍãàÁƒäçç‡1™ ÔÕµqùåk C<öØ«ÜtÓÜu×5x<^ZZºQhnîÂ`ÐrÉ%«øë_ð‰OlÂãñòÃþ/ƒŽ«®Z@NN:^¯Ÿ]»ŽÆoÙÙi Z6lXŒÓéÁëõc4êbûéêêgß¾Ö­[„Ó鉵³{w|ÿh¿M¦Èß_—\²šœœt<jµŠ?ìâàÁƒœ÷™¿,¿ëA|7½-u (ô4ס5ÌNvžjâU„B!„âôµï£¼ñ—ç’þ«;|€!;ö^kSݘvúz#¥‹²sò’îËš–{íÛFO¤ [vnì‰Ñ)Š*Üs:ƶq2ÔÕÕñàƒ288Hgg'÷ÝwßœÝ ò$nKK«Ý„H±À3V†-gÙEÃs"E¨´J¶~ž’­ŸŸõ~„Ã!ÚvFžÂ7TR´åVFß,5Ø ©þÌCèÓsÇmÇÝ^Gçî¿PzñÉ[{ Š*þ™skq5Õ·ý ÙË.š°_~Wu»(¹8ñ9z"sW~g/­oþ€ôŠUT~âÛèNš Puó÷™wéW¶‘ê1™‰èÍì‚õ7°è3?¤âã_£üê¯R~õW)»ü. Îÿ漊qÛhzõW„>ôyTÝüŒÙÅqËÕz3çÝÈâÛŒjó6î"™–™ ÖЖ “½l G8}39×R=&Ó•¿þz–ßõ…oBc´Æ/T²—n%{é…8[kâævL¥p8Dë[O15…›nŽËhÔ,TÞð-4 !ÿPì3v&ót5Ñ{0R>¼hó-dTž·Ü˜SBÅ5÷àj;JÏí'½“åî¬Êì«®@u˜æüJªnþ>Šù=[—–=a›žîfüÎÈü–ùn@­7Å-·,DQ“¿ô£~÷¿#/—^C8múåS-ÇšCUþBz]½´ŒT­p»Ý˜Íf\.CCCtw÷ÓÒÒ…ÅbÀf‹ÿüj4j\´¶öàvÑÙÙÝžÉÒ¥åø|rs3bË£] ùÉÍÍ †cÇÚX° (¶<º}t½ÆÆìöÌX;Á`ˆ÷Þ«ÓŽÃáaÆűv¢ý޾]/£ÓicÇ ‘9Ý}ôµ6àu;ìl#Ã^DéÒøÏCªHÆ›B!„âŒæv?9üÐPäÉÙP(Ä@ò§´›ŽR9y˜-V\αí–Í”èñy‡Î×ÖÒÈ’•ëÐh´–”ÓÜ0¶DNaIY¬eK‚àÉR__Ïí·ß>gû?Q ˜ë.qFêxÿyŠ/ü,–Â*–ÞùKœ-Qi XKÎAc´ÒþÞȘ¿ƒ-yÆp*´ï|–Ìk±VQ¸ñ&²oÂÕv ­9kÉ9‡\t|ðöÕWŒÛNÓËÿ‰!#´²å”^ü% 7Þ„k¸™1»$vVkΤgßë–^ìÙ·ŒŠÕX F¾ÞÿƤ²cÒËVPqí½qï©õ‘›&{9+ÿáñ¸e»ÿãsqˆQÇß~CVÙK·b«:Û ¸» ù<èÒsc8KÑ"Žïx*aé¾TÉt5½úK,… ÑgØI›—x.¤â n£ÿèûûÖÃê=αgÿ…Šk¿Ž¥p!K¿ü \uø½hŒi˜ó*b7ÈmÕéÞûÚ¸}êÚý2ö5WcÌ.fÁàélÀëèFkJǘSJÐëbÏÃwœ”,²éžk©“Ü•—“¿á†X° üÊ{Ÿ“ͯþWÒ’ŠJMÆü5¨´zжÜJáæ[p·ÃçèE­3`Ì.Fk‰d'ù]ýÔ¿ð“¤mnú4öÕWƽ§5El²¯º’ì%ÆÞw·×QóøwÆ´Ññá ¤•.ÅV½‘¢Í·»âR\íGQitXŠ¡Ö {îGIç€;Ó4¼ø0ÆÜy³‹Yø©ÄÝÙ€·÷8K–Â*E…o°›£Ï>”ô¼+¿êï±–.A­ v-ùâÏb%[=zï¬g8 åß0ïÒ¯`Ì)eÙ¿¤ÿÈ{}nLörÒËVàsöÒ½÷5²—n%gùÅ8šöL ‡iÚöW•´Ò¥¬¸çQÜõ„C!ô™ùèÓsi~íŽ'ȌҼ÷(Á•7N/ÀéýhŸüò,ýÔ=¿û…„ïïÙ³‡’’Ž‹dE°ûùÏÿD0âúë7 …øÉOž% ¡( jµŠ@`ìÏFÍë¯ïF­VŶûõ¯_@­V †ÐhÔ„BáØò'Ÿ|#a¿NÜωíDEÛyöÙÈu©¾>TüÓŸÞ% qé¥k0t±ã|ñ'÷ Q*µš`À?Õ¡œ´¼{ ¼ !„BÞ5 í IDAT!Îv“,/ÔÕqœ¾Ò3³X½áv¾õ*Cž‘qùE¥Ï« öàB¡Ð˜6ü~u‡©X°˜%+Öât Ò×32Ù¹5-ƒê¥«hkmd ïì¸$„˜;mo?Îj#wõ•è3ìè3ì õ´ÒðâÃôØŽ9¯|Öo!¿—ÚÇï£äc_ {éV ¶B ¶HÙ³ÁÆ}Ôÿéß1ÙË& ¼}j¿ü 7`_}%:kéå+cËýÎ^:?z‰¶wŸ™T€©·æmæù<¨u‘RTÝûÆäD)Zsfâe*õ˜e££…CAŽýáG 6î%íµsçaÊ[ôyèÙÿÇw<™t¾¬TÉtù»Ùûÿ¾HzùJô™ù¨4Ñ K%`-]Š9¯‚Œùk¨ºùøÕ=„Ãc–öÖìÀûë¯E2wæ¯Î’™Ïu nÇß~šÁúÝö)ðqè7ß ìò»É\°.˜ßp(€³¥µÞ|RoÓ=×"ÛÎlL4F †Ìø,­ÅFô;tbЉ¡ ‡û&Y‹7“·ö,ù 0çWbÕdÐë¦gÿ´þõ‰qçvSëMI?;*­žÿŸ½û“ª<?þ=s¦o™í­,}) ‚‚ bE{‹šÄkŒ1æ}c‹¢ñ—×hF£IŒF£11ŠŠ¬ ¢"–íl¯³ÓgÎüþw–a;ì²”ûs]\»sžó<ç9‡³mîóÜ·ÎÐU'Woí%ýw0Èîe¿%½fés.Á›±òÉ^±•ÊžÃ^¹­Ïó:šøœm=÷s²N[BÒ”S±îó½$¨hÜú1•?‡·­¾×1Œ1‰ÝîS|W&ˆ>W… ¡ÎtÄ™ ¾‡16‰”ãB)‘ƒšŸ¦¢UT¬ü+~W†¨8lùÇõ{ÿ4nZ‰‚BÖ©K0DÇ“=%Üæi­Ãß_jTŸ Ãk·â¹ú%´± w%ê7/õÝgù|>JJJH.<“-…ú ï„¿6ßúpÉÓ¢FíD áÛŸQ1¶ÞŽæÐªQ¿‚Aüö&üþo}XŒ¢Ó‡¾Þ‚A&ž„£fWøkmùŠí èÂí( ªÉŠÞCÀã@ó{Ñ|rN¼„š/^%-®çm+> =<’5y&‡ŽæzÌѱø½‚ZŽ_½=|ž‚A€¿ëçʘ‹îÀQ»›º7ÿ2ä×XIKK;º“Ø !„B!z”˜zJ¾£££Ÿ=nc&L¡`Ú,œŽV¼õŸ>÷ŠŽaþéçb4™Ñjñz=ÄÄØ°Å‡®gUy ߬íý©Zª2wÁÙ$$…R¦57Õãtt`6[HHJE§ÓÑÞÖÂ罃×Û}õÃÑ¢ì½rrÏÊᙈÃY^^(Í[iiéÏäèg°Ú°~»2ÆÓZ‹«¡¢ÿNÃDoÅ’˜‰jŠÂÕT‰§¥¶ÿN=Pt*æ„ Œ¶"µÔE‡5m4†è4¯ wKMŸ¿cÎ`&*m4zK >G+îæ½ø]}gˆ8éTÖô1¬6ŽÚ=<‘2kjž–Ú-+:kjƘD‚Zws îæêÏÉÜ•øÞ >†W~ŒR²Š`0H0DÓ44M‹ø<6¶÷ÚчBÚñàmoÀššGP „Ó´æó ´Ú4àu}{ ÜMUøÝØògвã Òg_DÀë¢äß{öM(:}èÿÄëÂÝT…£f7­%ëBã.º™¶Ò$M>…€×…·½{å6T£¿»ƒ˜œ)˜b’h/ßL[éfN¢¨¨ˆ¾­ÑfKìV£ÍÝÑNþ̓رúf_z-^—“ /=JZZ_}õ×\s N§«ÕŠÓéä_ÿ 瘋ï@ó¸øâ¹{IMê9à?èk*+Þ„B!„bpvVôSgÌ!)5”´Qá6—ÓÁî[س«¨Ï1´@€µŸ}@AáñdåŽ!!1…„ÄPÎï÷Q¶{E[ÖÔŠBˆCÈçl טi~g;vgßßK"¨p5VV¥ƒåi­ÃÓzà57G⚨F ©³Î ö«7zL©Ù©îë7É:ý‡(ŠKÂ(ZúÛçlÃçšz¨>Gëaó5p0†òš¨¡ú Á N­*B4Ÿ{åáñÿs0´€Žªí}îã¬܃;A-€£f÷ß3êº šcñÏ» ï¿ÃðêOQJ¿8 ±¯½Ws519“»ÕÿTtz|Îv¼ö'c\*Sšß‡Ñ–n ÑéáíÎÆ Œ¶äp³£®”¨ô±ávEQiݽŽô/ÅbÊÛL0¨áwÙ‰7§³¦[6{C gGq©™˜FGã÷z°¥d„Û£¢¢°Û»‚É•••¸ÝnÌfsDÍèŽÊíDgN–k++Þ„B!„8FÉŠ·ƒc¶X‰±Å£êt8ìm-ù$·^oÀŸˆÁhÄãvaokÅ?Œ5'²âM „¬xâȧS ̺ã PŠžý_ìU½¿áoIÎaêO°{ÙC4mýøPMS!Zçßþ9×â›wxè_¾eïÖÃrÅ['E§Ô¤phuß¼ZM­(¡¶@÷UOP  èÔP]FM#às£3˜©[·<¢_xÜõ$Øã¸óèd0ÈÎΦ´¬¬ßmªÞ€ SUÆä¦¸¸xÀ›Õ5¶ÈŠ7!„B!„8¸]ΈoÂï÷ÑÔp`)Ô„b¤ÅåÏdôùÿs@}7>¾¤Ï•Oâð0ñ»ÿ‡%eðHÔ­{‹êÏBõ´€{U1Yä-¾…ÒwþDGÕ‚Z×®ª9ЏüãÈ:í Tw®µxíМÄŠk"Ž~rŸÛÔ5…¶ZùóÐÕíäp_ùÔìªýòõˆíÇ_øjŠ·às»ðº:P>›ö†špÐ,ð‡û¥¥Å£ª*ƒ~’“m¡Ê?cÑ¢ãY^…¦ÑéB5ãZ¿í—j_þED{jj<‡“˜h3ÑѼ^š¤©Éq¼Îí'Ÿ<ââ*¶nÝ @ff2bµšq:ÝøýQQæðvM RרßúêÁ“À›B!„B!„4EoÀu`Oˆ+Šnˆg#†ƒÞ{@ÿǪÉñºüý§™øýßbIÎaÒÕÔüø]>tF zstx_¯½‰âßwØÖWªk"ŽnrŸuÛrt[ßtFŒÃ‰Ñ…ÑÅüïÿ”ÖÚ*ñ{ݼÿÄý@Wí´Îši—]¶UÕñÈ#¯°hÑ Z8wóÍàry¨ªjDQà…VpŧôØ^YÙ€Ùl`áÂãX½z—^:—Ëþq¼¸¸h\.•• X,Æðü/½t>^¯·ÛË–-eÌš5ž@ €ÅbÂëõ‘’Ëåág?ÿó_; ¼ !„B!„BˆA³—oaÛßv@}>wÿ;‰WòúÃè æA÷óv4G¼vÔìbóS7’qâ%ØF‡9>CT\¸Ýï²ã¬ÛCóŽ54lüí0¾?†êšˆ£›Ü'âhÐX¾ [j‡¿Ç…9Ú†ßÛõý¹³vZ§`0ˆÑh¿ŽŽ6‡VÀz½J[›ƒ††6œN7Gä÷ùžÚSSã=:¯×OJJ\¸}ÿãun//¯#5µ+àm4ðz}_}µƒY³Æáó°ÙBÛ÷o(I7!„B!ŽQRãMŒ$©ñ&Bj¼ qtRª9 ÍëFó{GzJBqÐ:W·íû±óßá\ãm f]ð}¾Yþ"Z ÐcûYgÍÂl6¢ª:4MãÍ7¿ ÐPU€†¢(¨ª¿?Ôÿâ‹OŠØoÿö}éõjx¬óÏ?MÓp¹¼˜ÍFÞxcMヰ|yhlNG0äüóO §µTUÕµC^ãMoB!„B£$ð&F’ÞÄ@HàM!„GŠ£-ð6>mN¯“ÑÉy$Å$ñêºedgg“€qôô^kÀõeÌEwà¨ÝNo@Qt5êU/1ê¤+©^õóæÍcûöí455ŠÓ¤®±eÈo’jR!„B!„B!„B„ÅGÅaÒ±-T4U†·çææRTTÄ„‚yªgµZ#jÁ¡€%ažöúðë}?VTTŸŸÈoÃAoB!„B!„B!„",9&K‚™²¦rjÛjÃÛN'QQQ®g6›#jÁuTn':s†¨84¿E56ë<ôVi³Î#ªcv»ýžëP“À›B!„B!„B!„[¾ñí·oÚ´‰ììlv¬~€õo…V²uÖ€ëôÁôØ¿ö«×á+Pt*Á ÁÈjh¶ñã)..ŠS1xB!„B!„B!„ýòù|”””tÛþõëÏG¼>þ¢%}Ö€ jˆýÓÒâQU•;wœl@UU-:žåË¿@Ó‚èt:Z¿í—Ðc{jj<‡›ææv¢£-x½>4-HSS{Äñê[†êÒ„IàM!„B!„B!„B £%j@5à:k¿]vÙTUÇ#¼ À¢E'háÜÍ7_€Ë塪ªE^X ÀWœÒc{eef³… cõêm\zé|\.>ørÄñÖoÚ5äç.7!„B!„B!Ž4:=hþ‘ž…BÑ£Ö€ë 1 á×ÑÑfTU@¯WiksÐÐІÓéÆáèê×[{jj<£G§ãõúII‰ ·÷v¼¡$7!„B!„B!Ž ZþIøÿ?Œ/ý¥a÷HOG!„èf°5àvíªÆl6rñÅ'¡iO=õ€†ªê4EAUuøý¡•û=öزÛ÷¥×«|üñFTUî×y¼á 7!„B!„BqT2é$D'RÛVK0éé ‰ÀÔ ñÿ0èT´ìY¨GhàMQt$dæÒ^_ƒÏãééˆa`0YˆMI§¹ªŒ`Pѹĥeâu9p¶ }-'!ŽVãÓÆáô:œGRL¯®[@vv6 GOﳆ[aa!­­­”••]5àÆ\tŽÚÝèôEGPÓ¨^õ[c©~ï%æÍ›Çöí;Bß7:?ƒÁˆ Ú«¯®Š˜ïþíûêÜî÷ºõxB!„B!„G›ÅÆÿ»ø>¢âÙZ½ß¼õÐA—@Á¨¢âñú½´8[ÙÓPJ][Ý͸‚Åø.ü= `X~;êú²cµ«úy3æao¬å¯7ž‹£¥q¤§$†PT|×=µœ˜¤4Jׯ柿øÞˆÍå¤ïÞÌ‚%ÿKÀçå_wþÒõŸØ\„8’ÄGÅaÒ±-T4U†·çææRTTÄ„‚y}Öp ƒÁnµÜPÀ’0 O{}øõ¾+**ÈÏϧ©©ééГÀ›B!„B!„8êLHGBT<“GcŽÁî¶zœhs4Kæ~9cf÷ØþÅîµ<þáŸj®¡åŸ„ï¢G@Ñaxwét³ÚâÉ›1€˜¤4²§ÌbûgïŽð¬ÄPÊž2‹˜¤4òfÌÃj‹±Õf§œ €j02aÞ™x;H™™™wÜq¼ûî»x½Þ‘ž:“ÙˆËéîçaf2›ðû|½®¼:’$Ç$cI0SÖTNm[mx»Óé$**ªßn‡·ÛÝ­–[Gåv¢3'`ˆŠCó{QTi³ÎCoµ‘6ë<¢:va·þçõáDoB!„B!„⨳£¦˜Vg+qÖ8¶UPÐÍ ê¹óœÛÈMÊ! ø¦l{[÷¢ÓéHˆJ`ò¨¶íÝ> ³ŒNÂwñA5 ~ýê—Ï û1‡“³­…² kÈ~"MõTlùz¤§$†XÅ–¯éhª':1…² kF4ÅcÑ'osòÆðûرúƒþ;ˆ>ýèG?¢°°ÜÜ\þøÇ?Žè\fžTÈ©çÎ#;:¯ÇGц¼ûÊGTî©>dó°Åǰèò3˜2k"« ¿?@ÝÞÖ~¸ŽÏÞ[K pdá–o|»Çí›6m";;»ßn{öìº×r«ýêuø JC»_*hÛøñÙyŒ%--íèHp-„B!„”ÄÄD:::Fx&#CQ”!©÷£êõè>ßáñÄï‘¢ì½rrÏÊᙈÃY^^¥¥¥#<q¤2é$F'RÛV‡v5¦N¸€kç/! øõòÿ£¸vWD»‚‚¢(4ö`x¯|mÜi(õŘž>¾a=Þ¡ èT3si«Û+5ÞŽR“[jMUeµ‘ <Ä¥eáu;p¶6è<ŽtóæÍãŽ;îÀëõrã7RWwèRíîï;7\Èü³B+‘}^í­Ä%Æ¢ª*~ŸŸg}™ k¶ û<2²Óøéý×c‹ÆçõÑÒÔ†˜­f|^»‹Jyö‘—ñz½hšF0DÓ44M#66vØç7’’ ÏÄdK¡~Ã{xÛC)…Mqi$O_Hý7o‡‚nŠ.´Ý–BÀãÀÛÑŒÞæ÷B0ˆ×J9iŒM íûm°.aâI8jva¯ÜÖc;Š‚j²¢7Çð8Ðü^4Ÿ‡ô/¡æ‹WI‹³’Àû+?&5)~HÎ9íe€¬xB!„Bƒ¢¢c˜wê9”ïÙIqÑ&4mpo˜&ÆNœÊ¨ì<,Öhü~u5ìÞ¹•¦†ÚûÊÍè±|œÍë×ÒÖräÖ6Bˆ‘æñ{ÙÛZsÀýǧ ¢¹²[Ð HpHâè‹–7mÜiÞ½÷¨ºµ%#=ƒ¢ª*^x!o¿ý6.—÷çó¸›ÿãÖÚÊþw}2™L\{íµ¼òÊ+#t›{Æñá ÛGËWóæKïãu{±DY¸ä‡‹™sêL®¾årªËj¨ß;|õ# =7Üþ}blÑ´6µñÔƒÏS¾«’`0ȘIy\~ÝùdÉäÌ‹ðÖ¿Ž½Õ–ªÑг®””égÔT¯ ­ŒË˜{)¹ DÀëÂÝ\ (¸›ªðL$M;–_>û"^%oü€Qó¾C[éF’&Ÿê×T…j´„·»·½{å6T£¿ÁDLÎL1I´—oÿÛZuÃA7,£ !„B!ÄalôØI˜-VÒFå :èf‹OäÔ³/bÌ„)X¬Ñø|^<7z½´QÙL›Õk_‹5Š„¤Ôÿ3Œ{ªB!B”) »käjÍøçÞ€®r=ºÒ/Fl"’N§ã®»îâŠ+®à¶ÛnCUÕ‘ž’Ãê;ßùÉÉÉÔÕÕñßÿþwÄæ¡7èYü3ØüUÿýûr¼îPæ —ÃÅ'o¯Àh2„÷.óÎ<äôD‚Á Otë´»¨”Š’PºËÂÙ“ILŽÖ¹޼öF\ÍÕ¨æ( Ñ mŠNÏÙŽ»e/o{ƸTbr¦ ù}mÉáöNŽºR¢Òdž·;+0Ú’{mj­»×…Ç5Å&$ˆße'~Üœp­ºá +Þ„B!„ǽÞ@VÞXJw ®.ÅÅœ“b2™iomfãºÏiijÀ`4‘7†¦†ÞŸþ­*/¡¥±¾Ïc$¥f0aòt4MÃÞÞ:¨ù !ı.ƃÙ`ê±­ÍÕŽ×?¸´ÀŠ¢ Å´XЖŽ6ædÔõ/º¿Ál%*.ŸÛ…£5´‚ÚOnál,±ñ´×ï¥zÇ&\í½×ßR FbSºæ ÒV×U;É`¶’7}¶´Lü^廩ܺ®Û8F‹«-¡Ûv·ÃŽÛÞÖç¹ØRG¡( G.{ß?£’ÑMøìM}¯`2GÇbŽŽívt²Úâ1Zz~»£¥¿ÇÝçøûKÎKjþDÌ1qøÜNìMuT­ÇëtôÙO§ªÄ&§÷Øæ÷yéhêûw±ý)ŠŽŒ‰ÓˆOÏÆh±bo¬£bË×x}ä{úúSt*Y“#13‚P·g;{wnÔ|FBFF]tùË_ðzG.Åú”™±%„R4¾ûʇÝÚ]vZøó'Nåå¿¼Ž³£ç©:®Ï‡ðúkŸ{æñlûf'廫"Ú2²Ó˜0m >ŸE)ÇOâ£å«{?±£PóöÐùV¬øA-@Ú €¦Qöî“¡ô³Šªóðw뫨zš¶}Š¢SÃýêÖ¿šF`D¿þÚkÖ¼S§F¤½m6ÈÎΖs—À›B!„☒•7ƒÁˆÏ祪|pé¦LŸÉdÆÑagõGïDÔuóy=”ìÜÖg·Ë‰ÛåìsŸ “gP]±[ÒV !Ä`|ÿÄ«˜;vNm¿÷Ê7â-·ë\t»?tÿ óÎä‚;¡|ÓZþyÛ÷8å‡?ç„‹– î³¢Úër°ê…ÇYóï§{#ml?|üµˆm¿]\€×å¤`ÁbýìÿaŽîªSäqØyè¼©ÝÆ)8å\ÿïƒ=cí+eÅS¿éó\.¹÷I2ÆO¥bóWüãg—÷ºŸª7ð£gWbŽŽeõKOðñ3¿ës܃ñꫯb³Ù8çœs˜?>---<ÿüóîÿã|ññò]×°kíG¤ŽžÀÅ÷%%ÓWoB!„BˆcJgµŠ=»ôò‡^ObbãHÏÌ`ÛÆ¯"‚nC%6.¤ÔÐÚ{Ї§ÞÀ‘Êl6ãvîIyqì‘ûDÔ·×SÚP~­WU²²ÜjÖn9ý¦ðk“>´z®`Ô$þ¶äÏû–ÔïáÿÞ~øà&Ü-ç{ŠýÀë)E'¦rÑ]dâüEìݹ™Ê­ëЛÌLš6–ØxN»þvü>_½ö\·¾ž;¥ë? «à8ô&3qi™$fåsá]¢èTœmÍx]âÒ²põ²rÍÙÖLMñ–ˆm)£' ê :‡-+—‘1~*YSf“˜Úëê¬Ñ3O ·¬X6 ±ƳÏ>‹ÍfcÞ¼yœþù455ñöÛo¨oé†5¨zI9cˆIL ]×Ì<¾ûð‹Xãðy\´ÖT’œª5ØÛµ-<ëRÿïÿ¡èTÚjØóÍjü7Y“g’š?‘³n^JtBrAÈ-+–1ç²ë1˜,Œ›sÛ>^Þã1Ì16FϘÀæ^®këÞʈÿcÕ`ì1xÖ—1'œÂwø+ŠN¥£©žÝ_гµ ƒÉBBf.ÙSgÂIg‘=õx»ò¤…~¯§Û½—–‰%6~Ps™uáÕ,üñ½(ŠB[]5%ëVðzȘ0Q ™uáÕ$çãÅ_~Íßûï”щ©œûï˜rú…4V”°éýÿâó¸É›1—ì)³È›1—Sø ÞûÓÒAÍïP™3g3gÎÄï÷óôÓ=襬є—t&-ºütÜ.o¼ð.ÇÍJTŒ•¬Ñ£ºÞÌWÝt1Q1V~öÀ <ú«§iké ¾uÝ&NËøÉùlY·ºê†ˆ12ó2Ÿï•“FÁŒñ¼÷êÇØ[íŒ-È#>)ƒÑ€×sèV îܹ“‚‚‚¼‹$ð&„B!„8f¤¤gc t÷àÒLfdåàr:¨©.ê©0füdšëh¦ÔX¬V+&L`ýúõ5ÎìÙ³Y·nþ>Þp:XÑÑÑÄÅÙèèpÐÚ*é7EÏä>¯¬{WÖu­ÎJŽIâWþ~Àý@Uºjuu¦T%b;@SGóÁMv‚ ¹¡ã7•Ô8‰™yħgñÖïogÃ;ÿoÿìù?òÃÇ_Ö:Š?ø6¾û¼û­Ìn¬ØÍ?ñ]n~áâ3rÈ?3~t7îŽvÞúÃìXý> sÑÝáîè98´óóìü|EĶ[^Z-uÔ€ÎaÛGË9ãÆ»Ð©z&ž¼ˆ¯^{¶Çý&|{wn¦±bxV2ì+ òøãËÔ©SY²d ---¬Y³¦ß¾ÿºc ‹þ ÓϾœ¸´,ÎýÅo±ÄÚøøï¿gíÿ†9*†Ÿ½ò@é8“²ÇpÎÏ~ƒ¢SY÷æ?ùà‰µ3“œ IDATû ø}áö9—]Çé7ÜÉÜ+nb×QUùs¿nÏêKw’’7ž‚S÷x›0÷Ltz=¿¢Oz,~òÜøä¹?„_Ç¥eò“Wõ{öuÚu¿DѩԗîäÙŸ\Ôí~´Úâ™sù Ô—ìèuu^{Cm·UmçýòwL;óâÏ#){ gþènEaÓ¯òöï®Ç÷]ýô×äÎaÎ¥×ñù¿þÜÇXù$ffÕ óÙ D „Vݬzáq~ðÇÿYpSθ÷ž¸‘ªt0~ùË_b4Xíà B+Ì?üáÔgÅŠö2.%#TÓ«¹!ògý¨Üt¦PÀ»¯|„Ë馥±•¨+)£’»ávyxâgùɽ×:*™Ÿ=p#Þó4­MíA7MÓxöÑ—»ÝR3ºÆÝ.g^´ËÃç+¾"gLè{œ¢@\B,õ5Ãû;þ¾¬V+eee‡ìxjÎ$ð&„B!„8†Œ; €úš*íƒê›˜J%S_;CaSå–üýúðë_œý?LÏžÆÖªm<øÎð¥,ìMÐzXDq|ïGîfûÿŽØÖÑTÏGÏ<Ì…w>Š)*†‰'ͦ^íw¬Óo¼Õ`àÙŸ|‡º’ÈYú«Õv ­Mìùf5cŽ_@Á‚sz ¼©zãçž„VÈ*~¿Ÿßþö·Üwß}Œ3†[n¹…¶¶6¶më;õþ¦u –˜8ÞúÃlxûÛš~Q1@(EšÇÕ=áé7ÜŽN¯§zûFÞ{ì^‚ÁÈzT_üç¯äL›ÍØÙ§2ç²ëxe麱eÅ2N»þvògŒÑÕcÊÃI BÍ]k?ê·ÆÞRɹ¡:¼ÅkVt º8ÛZøð/=§,J'ÿàgèT=öÆZÞþÃA7€oÞü'ù3ObüÜ3™}éµ|ñÊ_û\õöÑßbÍËOEl 5¶¯zŸÌ‚ã0GÇeKׂjÇZú½Öé­€‰í¹î`YqÝûW~rﵤd$ñ³_ßÈü˯¿ +èöÈË|ÓCšJ€h[׸{׽𑓯ä™ùàµq;ݸ]5-Q÷0X©©©|ùå—‡ô˜"DoB!„BˆcBTL,©é¡º'{öYí¦×HÍÈ"à÷S»·¢Ç¾Š¢”@[Kè ‘”´Qdç#&ÖF°··RYº‹úÚêšß豓Ðét¡uUó¢n_¯¼ò wÞy'·Ür À ƒo‹-bÉ’%´··óÆo ÇÃ:(qq6 ªˆ^É}"g«ƒÁ€ÏçÃëñúïðmà ïÁ×üÜöIÏ+™Š×¬@ øÑ©zFMš> À›%&ŽŸù]DÐ-Ôjœm-=×ÞlY±Œ1Ç/`Ô¤Ä&§ÓÞPÑž?k>¦¨´€Ÿ­½Ùë8&“)¼šq ôz}¿«»~øan½õV&NœÈí·ßÎ]wÝEEEÏ¿[ôÄÇžoVwÝ­¨ _×ýVC™£cÉŸy2 ítë´éýÿ2vö©äÏš¢S»Õ9ÚòáœzÝmè&ÆŸx[VFÖK²ÄÄ‘7}.›?ˆ¬÷7”‚A W{+V[y3æ±êÅ'ð{}ú`Uo`Ìñ¡ëºíãåzI/¾éýW?÷L¬¶r¦žNÉÚ“uoô\û¯½¾ë÷F½i`A™ù~òä“O¢×.žžÎ¥—^ „~o¬©©é§G—;wxßÁœÑÔµjÏïí †fä¤1í„Þõcœ¡ï—~_èëÕhî}¥_ùî*þxï_¸eéu$§'rÏãÿ‹N§ë7è`úv.~ŸŸà>_› /Z€×íeÕ{¡€—ßßu>ÃÀRë•+Vô¿“xB!„B:W»9:ìÔ×t­Z‹ŠŽaæœ8½Þ #ªúóÉåt0ýø“Èα-.ÌìÑT•—°þËU½¾ùÕUUÉJãSº{û ú¨­[·òÀp×]w :øvÖYgqÍ5×ÐÞÞν÷Þ;¨7;± øék·ÛÛmÂl6KPåb2…êkÉ}"Žd111ÄÚbÃoz··µÓÒ2ÀàTç{¹:µÏ݆×夥¦’ÄÌ<’sÆößð:|õúsÛŠ>y»×„Ceçç+ð:­QLZ°˜µ¯ü5¢½sUVÉןâlí}•àÓO?MLL̰ÎÕjµò«_ýŠ;î¸ƒÆÆ§–[ýâŸ"^;ZyàŒ1=î›=õxtßÞWû§ÜWýžÌVâÓ³h®.‹h·7ÖR¾q-¹ÓO¤à”ÅÝoæ-D§×ã²·²ûË|.bÓ{ÿeÎå×3jb!?úû ¾yóŸlÿì]Zjÿ»ÆJ[€Ñú²wç–^÷Û·Ž\ú¸)}ÞzãuwÕ >Ðï'~øá çvß}÷°yófž{î¹A÷ˆÁžN×u4­+ØuÎe§ãõxùðÍ}— µëú¹®•{öòįÿÎ/~ûct:¯þý­>ƒnÊ·sÙwéY©L=~+–}ŠÓÑË÷‹#˜Þ„B!„G=½Þ@ö·©‹ÊJvD<•j0ô_ïÂ`4…?3a*‰É©TW–R^²§ÃŽÅENþx2sòÉÌÉÇåtP´y݀痕;£ÑD à§¼dàO¬¢¢"xàî¾ûîß.\ȵ×^Kkk+K—.¥²²ò€Ží×…ê]¤¥¥ºog:ÁPåÐ? /ÄÄDÜîÁ¯ö‘ûD.lq6Tµ+p3ðÀ›«léÍÃ$r47˜™‡):v@ûïübE釛ÏãbÇê÷™zæE,8'"𦌌;1”fróŠC—f²/:.üFþ@´7ÔP¾ù«ï“”þ|Îå×÷º2Ko²„?·ÄÆA‹ó·¬|Üé'2zæ|Ìѱ¸÷IÉ=ñäElûø­n)‡ÚGÏ<„ÑÅŒÅW—–Éi×ßÎi×ßNCù.v~þ›Þ{…æaªµÛ):¡«v—½±¶×ýìMõáÏcÿû @PÜÃVõýdæÌ™ÃÌ™3 üùϽׯ;Xƒ=¯§ëþSõ¡~ÙiΙ̊eŸF¤|4C«Ë<žž¿.Âûô,¾â̈Àç‚ÅsÙ°v ­M½§¦ïœ‹^ß5ÿ³/= ¯Çϧï~±Ïø]í«ðÄÑ+íe€Þ„B!„ǀ켱è ?{Š#Úô ¼í³OBR ë¿üŒÊ²ÝámŽ; µx=nF+ \»¶oÆ×Ë`ûË_@Uy ^¯§Ÿ½‡ÖöíÛ¹ÿþûùÕ¯~ÕoðíôÓOçºë®£­­{ï½—ªªá©w7Iù%„Ü'âH§¸ZCk6̶a=NÀúye`ª;GsÃpN§O[V.cꙑ1aqiY´Ö†ÉŸ5“5³ƒâ5+ûãæ›oT@ Àh4âõöýsÝb±ð“Ÿü„‰'âr¹xਯ¯ï³Ï¾-ÝÒIöy¼è®ûbú¢ï ¨Níùíà¢ÏÞáì[îGo23~î™lzÿ¿¡cÄÄ‘7ãD ”ês¸iï‹Ø×b µ»zZyö-ƒAÏw^ÍÄÂqh7_zŸ3.8™ä´Dnýõ |sUDý9“¥ë>{`ˆ#ŸÞ„B!„G½¼±¨®ØÓ-°¥@­…À>µPvmßtÛמ]EŒW€NUIÍÈ¢ª¼¤ß±Ó2²ˆŽ ½q¶§¸¨ßý‡ÃÎ;û ¾zê©Üxã´´´pï½÷²wïÞƒ:¦^ =n_[;ðôiÑÑÑáôµµuP945…j*ÖÖÖ ¸Ü'âpÓÖÚ‘JÍÞnp_¥±òæLX È¥7†Þ Þw•ÓáªtýìMuÄ$¦2iÁ9¬yù) +Ídѧoãïç!–ŽŽŽ!Ÿ—ÉdâöÛogüøñzè!JKK‡ü8ûrÙ»Ré¾öëŸàu;ûØ;¤¾´ç•õ^§ƒ_¬¤`Áb NY¼M8i!:UOËÞò>ÓYµ†²bV<õV<õFå0nÎwÞU$ŒÊeÚÂKˆNHæ¥Û0,ÇÞ÷:ê÷Éz°?ª†™nû¡Ik| ßOî¿ÿþ§mÞ׌3˜1cÆ û=óÌ3¼öZÿõ{>Á`¦ºR2’ˆ‹!=+•és¦ðá‘Á.€ø¤8êkšzkÿ Ûßÿð/Ö¯ÙLцbn½ÿzRÒ“¸õþëyäî§ioí>¯†Ú®qcãbXtùéø|>Z¾:b?[|W ·µ— ÞpÊ™‘BÍŽf´@¿'ô7MT¼™€_C§*dMK¦rSYÓ’Ùùiè¡:kœ‰¬iÉ”¯¯‡ (߯l-6>OO‡ƒIOÀ¯AÜ¡€¢ÑªÇ`Ò“33…=kkQõºÐ>€¢„ž+Ppµwíß9¯œ)¡ã%$ð&„B!„8ª¥¦gv¶vmïÖ>"çû>íÜP×{ÀÉÑa'ð£ªz,Ö¨Í/üäð¸ímCŸ*h Š‹‹¹ÿþû¹çž{ºß,XÀM7ÝDKK ÷Üs555}<…ÐSÂM¸0ER<žÐ›çrŸˆ#™ÝnÇðc0ðù|xûI{¶/]ùZ³¾KÐb#˜˜‹ÒT6,sŒMNÀÙÚóÔ‡“`PcÛGo2ûÒëÂ7Õ`dÜœÓC³*kªªrÛm·1~üx‚Á úӟؼyó°·½¡ëçq}éNÊwÔx[V,£`Ábò¦ÏŇËÞʤ‹‘MßÙ\]ÎÚÿþ¯_ÿ—,ý3ãæœFþ¬“I;™š][‡üxû^W[rF¯ûÅîÓfoø"ã@¿ŸTUU (ðfµZIHHB«Öôá»}`Á9ŸêòR2’H•Ì¢ËOÇïó³òO#ö‰KŒ%*Æ @ME÷t¡ƒžïú§E h<ûH(èPUº—Ç–þ•ŸÞw©£’¹õ×¡à›½-2`¿·¼kÜÂ9“™qâV,û´[0-3‡Ý‰Ûyh/‰I±bop1vÞ(|.?Å«B¾eNMÂkDû6(–?'=¢_çë‚3søt4¹Qèhr£êudNI¢®¸…¼ãÓøl|sÏ·ý2¨+n¡½Îqœ€O£©¬ÄÜXšÊÚqµ‡jpvΫfg ö†Á§?œIàM!„BqTËÌÉBoÔL›Õ­ÝjÀl¶p₳€ÐʵÚêŠð>^›`0ˆ¢(ûxòB)’TU? Úq¶¸’RBØŽÔj·}íÚµ‹¥K—rï½÷†ƒoŠ¢póÍ7ÓÜÜÌ=÷ÜCmmïµN†Ktt4II¡Z_L½‘ûDÎ\N.ÿ¦¢®l-5PthcOAmzvÈçLlJ(€Pµ}Ã?¶¬|Ù—^GúØÉ¡4„¹c1Y£i«ß;¨iC¡óçdaa!/¼ðŸ}öY?½†FÅæ¯ø}¨z¹Óçtà­äëOq¶6cK`ÂI Ù±úr g¡k>Ò~ßýãæœ@RΘa ¼Õ•l'à󢌌šXÈ–{>÷ÌIÓß—mø¢Ç}†Ã|?ùéO: ý–.]JBB;vìàç?ÿyD]äá2ØóÙ¾qÓçL!kt9c2ùø­Ïio ŠM˜6.üù¶õÅû¦ñº½hZ(èöÍç‘òŠ’j¿ïnYz-·¿?ÐmŒ½µ´5·cKˆå¼+â÷øðÍÈŒŠ¢0zB{Š+º1Ü2£)__OL²Ÿ§ë,6#ÁoË *ªB0DQ•ˆ¾Šªàuùq·{ñ{ø=¬6úͯaŽ5†Û;un÷¹¤Ž‹Ç^ïÄ–þX³#xëhtáóÂó<šHàM!„BqTÓëC+ÚEGrjïO-ëT5Ü^Sù‡q Àao':ÖFTLlOÝÃc¾ Ìy<ý¿Ð¹ÚÍÑa§®¦²ßý…’’’nÁ·¦¦&î¹çêêÍÓÜûëL?$ÁѹOÄÑHéhDÝþ>IgãŸù]ÔµxK•KíîîyL;ëR%ô†kÉWŸvk?Õî.¢¾t')yã™´`1É9c€Pý·ÁÔH W_}5óçÏà­·Þâ7Þ8dÇö8;ØýåÇŒŸ{&³/½–ͼ†Çyài4µ@€mŸ,gÖWSpÊbPtªžªmßв·|gÞ»èÄ:šzÞ´OFáJê÷zعf%“N^ĤSóÑ3áuuOã9ýœP]½¦ªÒ^SxIN8áfÍšE0äÉ'Ÿ<$A·±qíV.½æ\ F>¯ŸË>‰h×étœrÎ\J¶—ÑÜÐ=£D ào¿HÞ„lvoë9%lÙ®J½ç/4Ô6÷Z'îëÏ6rúóÑô|üÖên))'MG|Rn—›í.0~ š*BóÙ?¨µé­R‚ZE§D|ì´õýòÐv‚è~/èT…š¢&]WÀnç§UèÔ®ý+6„ÆÕ©Jç3$á€ß¾óŠN² Ù9.WET!„B!Ž0[6|É'ï¿Þë¿Î•fn—3¼­ºbO·qZšCâ“’Óz=VLl\øóŽö¶>çe¶X•=­°;œÞÜØ³gK—.¥½½úúz~õ«_XÐ  µµ•ªªj ¦ˆ>É}"ŽVêç¡fÁ¤|“Î>àq¾óÿž!gÚ ÛÆœp ó¿zÈ¢týçìÝ9üé‡Jç ¬)§Ÿ?bi&/¸àÎ=÷\Ö¬YÃsÏ=wH°òéßà÷zˆKË⪇^ );?¢ÝÃÜ+oâš'^Goê?Õ`çuÍ-œÃq‹¯]šÉ¬É3ùÉ??ã¼Û&aTn·öŒ Ó8õÚÛ€PMºê‡m.«ÿù8ZÀOT\"Ýý¦¨˜p›¢S9õÚÛÈ-œÀ‡ù¿a›Ç¡b4¹îºëx÷Ýw))é¿NñHéhwð黡†@ \Ë BA·K®9—¬Ñ¡‡éÞþ÷Ê^Ç ½Ý:U”T÷tXùÆgxÜ¡´ØÖh+z}Wíâ´ÌÎÿ^è{våž½”ï:ôÙÒg‘•ÉÉãOâ♆·gefQXXHLb*¶” bS‰MNã´ë~Ilrzx{lr1‰éÄ&§aKÉàø‹–5y&±ÉiDÅ¥š_@\ZvxÜÂÂB²³r¯;ƒy£Ï¿´9—>÷ FÍ¿ŠQ'…¾·t~,œp‰‰‰á~'NdÞ¼yÃzm†›¬xB!„BÕœŽ¾kL¸Ý¡'˜5M£­µ¹×ý*Ëv“•;†ä´ ¢¢cptt7oÌ ”š²¯Zpyc'¢Óéðû}T”vO3ÒJKK¹æškFzaZcD[ä>·/ú99I]oê”®gÎo:õü_øõŠmñÚ7#Ÿ>¯?ºêM¨›—˜z!þ³—¢–¬ÏàW5):•ïÿáe+Jh­­Ä–’Arn(›£µ‰w½»Ç~ic&qɽO`K@áÙ—3~î™l_õþåÁ~?ÿ{·0óüïEl³Æ…jIwÞw™rzכµ»‹xéö«ûo뇯s굿 );´ÚmïÎÍ4Vº`N§ §—ܶm=öØ ¢¹ñï 7±Æ…ÞpNÉÏÍ/|@{C-ÏÿÏwú£¹ºœe¿¹• ï|”Q ¹ñïPW²Ž¦z,±ñ¤-@ývõÿ¤ùg÷D«Þ¾‘æê2Få’>n ¿¢OÞÐù\ùà?H3)üZÑu}ý]pÇ#ö©™»îøì…Ç"úçÏœÞhbÚÂK˜¶ðšªJi­©âÒ2IÌ =°¤ü¼ûدpöò{[ÞŒ¹\xç£Û:g©ùùŸÿ~ÑöøUóñí—© nÏ>øóœuóRÆÎ>[_þ‚ÊmßðyÉ?•èÄPÝ®/þýv~¾b —ç°vñÅ“žžN{{;Ï?ÿüHO§_Ë_|Ÿ1“òÈ›Åϼ‰ÝEeØ[íäŽÍ"15ô}åƒ×>aǦá]eÖÞjçùÇ_áÚŸ_Å f0zB%ÛË0™ŒŸC  ¥©×_xoXçÑ›ø¨8Lz#£…Ц®À_nn.EEEÌûî­”®ÿœ”¼ñh?æhs.»Õ`¤týç¤æ‡¾ž­¶xJ׎хÑÅ›—âu9‰NH¦µ¶Š·ÿp fƒA®¹æœN'ÿú׿BTÀ’0 O{}øõ¾+**ÈÏϧ©)Tg´  €Õ«WûõNú}#‰B!„BqÌà›d u{ikiŸÈÌOáËU+qï“v(=3‡¬Ü±ì,Ú„¦i½ …ªêÉÍé*Jwá÷ùzÝW!ÄÀŘc°Yl=¶E­¯-ÆþWÿ. ï܃–3› -ßùaxåǃN©øÂÿ^É7ÞɘN‰XU½}#˾æê²û©Fñ9ÛÌѱ˜£C©—£â“t|STt¯ûL ¦®TcV[|¿ãµ7ÔR¾éËðª£-+íj7MÓøõ¯Í%—\ÂòåËñÀÏò„ŒÔ}jªcøZëÔ¯—رê=ž»õRNþþ­Œ9aic `LA-Àžu«Xóï§)]ÿù€ÆÛ²b'ÿàgìZû.{ë€úYmñ½þwÞ/LQÑÝöùä¹?°û«O˜{åMŒ>n‰™y$fæ…Û5¿Ÿ’uŸ²êŸ¢z{ï«ÝôFS¯óЩúnmû¦ÊÛ××ËþA{ý^N½ö—$eç“?k~¸­eo9«_|‚ï½Òë<Ž)))\vÙeüãÿÀnïû¡µÃÏççÑ{þÂÅ?8‡Ù§ÌdÜäÑá¶ö;ïüg%Ÿ½·öÌeÚ-<¶ôo\víy$¥&0}Îd ¿ÏÏŽ »Yöwhož´¨ýIŽIÆ’`¦¬©œÚ¶®:ÍN§“¨¨(êJ¶“>n “ªG5ñ{Ýáí-{Ëñºœ˜£cI7…½;6aKÍÀÙÖ‚½¡ƒÉŒŸ µÃáÀívSYY‘ £r;Ñ™0DÅ¡ù½(ª´Yç¡·ÚH›uQ»"î»ŠŠ L¦¾ëj‚‚Ã'Ÿ‰B!„âëè8ðZ Gƒ1¦P0mNG+ÞúOŸûFEÇ0ÿôs1šÌh µx½bblØâC5V•—ðÍÚ¾kää™ÈÔãBo®|ç¿8ì#óÇøH*{/T+&÷¬œ~öDz¼¼Ð®¥¥}§‚âX š†÷ê—À…~Õ“è?|¨ß>SN¿€ îx€ß..Àër“”FJÞ8tªž¦Ê=½Üđɗ@|FQ¶\ö6ê÷ì8¨ºo#Eo2“’;k\"Š¢àhm¢¾t'~ÏȤN•K\zmµU4W— öþÕ‘äî»ïfΜ9ìÞ½›[o½õ°J>«™Œœ4¬QÚZÚ©*­éó¸á”’‘LB² Ÿ×G]Um­v‚Á Á`MÓÐ4-âóØØÞkG'ƒÁ@vv6eåhš†¢„j¬j6W—•AêÔÔT ƒ<ùä“G\Ð ÀåtS²½l¤§@]u=uÕõ‡ýuôù|uüöm( °y$çŽÃçváuu (:|7í 5ü݃›iiñ¨ªJuu#ÉÉ¡說²hÑñ,_þšD÷mÚ††ÐjÚ¶¶–,YnW?;wneòä<7ÍÍíDG[ðz}hZ¦¦öˆãun?ùäiW±ukiøøŠÒµ¢5 ¢( 3fŒ%55žwÞùEQ¨klš »©ñ&„B!„8¦íÞ±…Ý;¶ x{{+Ÿò.f‹•[<ªN‡ÓéÀÞÖ2à?²·lø’¢ÍëÐï“^J!„ènÏçÿu ÞËŸÂÊÏÀë@ÿÅßFzZBˆ#T]]K–,¡°°;wŽôtÄa¦³¦Ûüïÿ”ÖÚ*ñ{ݼÿÄýÝj¹]vÙTUÇ#¼ À¢E'háÜÍ7_€Ë塪ªE^X ÀWœÒc{eef³… cõêm\zé|\.>ørÄñââ¢q¹³‚Þ„B!„â¸]Έoƒ\ýï(„BìCW² ã³—ã_t?ê¶·Gz:Bˆ#œÝngÕªU#= qj,ß…-5ÃŽßãÂmûéóQ IDATÃïíJ÷º-·`0ˆÑh¿ŽŽ6£ª*z½J[›ƒ††6œN7GdÚØžÚSSã=:¯×OJJ\¸}ÿãun//¯#5µ+edII ãÆe²wo.—‡èh ãÆeR\\IUUqqÑŒ—9¤×¬öáÜÐù é¨B!„B!„Bˆa¥«ÙŠñ™‹FzB!Žb;V¿Àú·B+Úf]ð}¾Yþb¸ýƒ>ˆØ×®jÌf#_|š¦ñÔSoh¨ªŽ@@CQTU‡ßJeÙ¹ßc-ë±}_z½ÊÇoDUuá~Ç{ã5=Ž»|ùZ4-ÈUWʲe›q»½hZ0\°²²Mž´ xB!„B!„â(ðûh««¿Ñ(„B Äø´q8½NF'ç‘“Ä«¯?@vv6 GO§¦xK¸Ü×[Ã5à imm¥¬¬ ­PóûŒ¹èµ»YÛ`@Qt¤˜Cõª—ȘwÕ«^ ÿ{ßû7n¤°°7²eËüþŸ¾µ[-9EQ˜7o ÅÅU|öÙf¬Vs8g±˜°XLØlQx<>E¡¾¾uخބB!„B!„8 }ò6[?|“`P‚nB!'>*“ÞˆÅh¡¢©2¼=77—¢¢"&Ìë³\   v«‡–„QxÚëï#>~+==úúúðÇ-[Bu¹÷¯%×Y«Íb1b±9ï¼9lÙRFyyf³‘sÏ „‚tõëwkàM7l# !„B!„B!FLÀï“ ›Bˆ’“Ì„ôñ4;š©m« ow:DEEu«§7š"ú;Ün7•••TWW‡·wTnGg4cˆŠCg4£·ÚH›u^øc'MÓ8å”SÂ;í_K®³–[g·@@㫯v„ÛÛÛ¸Ý>RRâ»öNJZZÚð$±B!„BÖèèèᙈcQÙ{åäž•3Â3‡³¼¼<JKKGx&B!„} ƒÝ>vþÓ4 MÓ">Éé0ƒÁ@vv6%%%Û;kÀiî5Úz¢èÔÐÃ!ÁžCTz½¿ßþØé¬³fýöî;<®òLüþ÷L¯šQ—¬fUw6¶1¶)¡·P²ÙP—,d_†FÂ.a³ËB6É’Ý5?'š „jŒ»{—­bIVo£išvÞ?Æ{¬‘4’%Ëåþ\—/kÎSÎsÎÙ3ç>Ïý`2Ðj5qk¹ŸR¹o­·×__ [kN§Ó‰¨|ñ‹F×¢ûÍr²3R‡s †>6 ¼ !„Bqv’À›OxÉÀ›B!NgKà-Ysoú»¸5àE·\jj*@Ç×nÉ5bwXùÛk+PU•`08¦ãlnëõÀ›¬ñ&„B!„B!„B!FÁlt ¸[n¹—Ë…Íf‹[nïÖý”O/¥¸¸ÇC}}ýxƈHàM!„B!„Bœ¶df¤Bqê9~ 8“ÍA(à•òÉ'x½^¦OŸŽßtûõw^Éö{Ðéth4šñú “À›B!„B!„B!„5{Wý €ÍË£3ÙúÖ€ësàÀ>×îÅg–PQQAssóÉꨓÀ›B!„B!„B!Ä)Ķà‘qÝYY6›‰ÊÊ<žè¬4›ÍLyyû÷×£ª*Š¢`µš Cø|½ :Âáèšv^o/ ~ F¡¼<ÊÊÊËóز%€ËÊJ% ‡ ‡#tÎ"(,̦½Ý…ßßK(A§ÓG‡# ÷c»0¹ó¦Ýù{Ìf3Í/Þ7š§ £À[ZZS¦L!-- FC[[ ÔÔÔ Ø&;;›œœœØëƒ¸È{II v»€={öÏÌ™3Q…`0ÈîÝ»‡@Ç((( //ììlíííTUUÑÖÖ@~~~l‘údíØ±ƒH$·ÍjµRVV{ÝÜÜLSSÓ°ÆY^^ŽÃá ··—¶¶¶¸qdee‘›› @CCC\YŸ²²2¬V+.—KÒ6!„B!„B!„g8½^Gg§›òò<"•íÛ«˜1#šâyîÜɃ!\./ŠÝÝ^ ¥¥¹:ÔÊ´iEƒ!V­Ú @ié¬Váp$®Ÿ>‹‘@@CAA@£QOccz½½^K^^´}0&¢Õjî'YN§“ÖÖÖ=M jà-;;›x€óÏ??aùáÇùÙÏ~Feee¿²|óÎ;/özéÒ¥¼òÊ+ û¹ûî»cu¿þõ¯ÓÒÒ2è¸~ô£a0hnnæïÿþï“=œ8S§Nå®»îbÆŒ Ë+++yüñǹöÚk¹öÚk‡Õ÷-·Ü—Ãફ®âk_ûZìõÖ­[ùÞ÷¾7d_Ó§OçÁ¤°°°_™ªªìÙ³‡üàøý~-ZÛÇ‹/¾È[o½WΜ9üà?@Qü~?ÿüÏÿ<¬ãB!„B!„B!ÄéÇëõãryÈÎv¢(ñk­i4 ~¯·—`0D0Âf3¡×Û‡#X­¦Xy«Õ„ªm‰¨h4J¬¼¯}oo€pXÅëu“žžB[[7V«)Ö¾¯¼¯Ïã÷“¬`0ˆÁ`Á™Ú¨Þ®¿þz¾úÕ¯b2™¬“••Eccc¿í©©©Ìž=;nÛâÅ‹ ¼l“'Oæ_þå_ÐëõÖ ‡Ãý‚g'â’K.‰{=sæLœN']]] ë î»ï>.¿üòØÔÎã)Š‚N§KjœEEE<öØc(Š‚ªªüû¿ÿ;þ!„B!„B!Ä 0¤dâ(>ƒ=H(@ §Oc%þŽÃC7§ Cf¦‚\­†î;Æ{8bµµÑõÕ6mÚO$¢2ujªª²~ý"‘hŠIEQúeóÐh4ÔÔ4¡(J¬Ýš5»bÁ¶cÿî+ß¹³¦_ªª¢‰Ö8Ð@$¢¹Ÿº$¯¹¹‡Ã1ì󒌸ÀÛ‚ ذa¡PhX\uÕUÜ{ï½@tVÕûï¿ÏÊ•+©««£··—ÔÔTJKKq8 ÓG.^¼8v+++©¨¨ °°¢¢"jkkOàðFÇ·¿ýmôz=ªª²lÙ2>úè#:::°X,ddd0mÚ´XÈ×_O>ù$®ýÝwßÍÌ™3xê©§èììŒ+ïíÆ–––Æf¬íÝ»—É“'£ÑhX¸p!Ë—/O8Æo}ë[\tÑEø|>þô§?±}ûvêëëÑh4¤§§3uêÔ¤¦N:~ðƒ`6›øío˺uë†l'„B!„B!„£EgNaâU’>mqÂòö]Ÿrà/?=É£#•}ó¤]2_UÞ’à^óܸîÿÊ+çÊ_ÿº€ kºÈÉIãê«çòöÛkТ³á²³Sñxütt¸°ÙÌA"• «\ÜsÏÕ¼ñÆ@=R? ÇÏœÙsxõÕO±iµ\}õ\~÷»÷˜>½ÇO $Ñ`ßÂ…3Ø¿¿žÎÎz{£))ûv±ÙÌ„O‰¨´·»âúñùzãú¿òÊ9d§§²q Î],ðvà 7pÏ=÷ðñÇóì³Ï&ÝÓ錥oŒD"|ÿûßgÛ¶mqu<õõõöÑ7»kçÎ|ôÑGTTT°hÑ"–.]šüÑŒììl²³³ضmøÃbe.— —ËEUUUl[ss3ÍÍÍq}ôôôÄ~®ªª25fßùü×ý¿üå/èùHx›3gN,èÖÖÖÆ£>Úo½¶d×gÓét<ñıc~çwxóÍ7‡l'„B!„B!„£E£Õ3ùÎÅšSŠ Ó¹o-¾özEƒ!%Gñl\5Û†îH1"‹‰ªªF®¾úÂá/¿ü!·ß~1=t#>_/õõm( ÔÕµb2é¹âŠóXµj·ÞºŸ¯—ŸþôØlf¾ô¥E¸Ý~ÂáõõÑúŽë·OFF &“ž ¦ávûHI±°yóÌff³¹sÏ'3ÓÏÀíöa0èî·¯Ÿ‹.ZðøÆ‚àºë®ãž{îÁårñ—¿üeX\}õÕ±ô’Ë—/ïtJQQ%%%¬^½šuëÖ …Ðét§Dà-===öó„ 0 1ÛŸV«eñâè7n¤¶¶–PVVÆ”)SÈÌÌì7kí¦›nŠýü /ô º Ç7¿ùM¦N À¦M›øõ¯=⾄B!„8•õ¥U?QZ¢!Ù÷F‹V§#þºB!„£)gƒ¿í‡>úݸŽ%c楱 Ûžÿ{ŒžºÝÇÕP\rGqâÚÚº¨¯oeæÌb´Zm\™N§¥»ÛCkk7^¯ÇOvv*%%¹!²²œ±ò>ƒŽ@ ˆÍfB«Õ †()ÉeïÞ:,˜ë·O_.—‡@ Lkk#ùìÝ{ˆììT²²œD"j¬H¸ß¾~Žï¿ïøÆ‚îšk®á¾û»›ï~÷»ÃNíxÞyçÅ~þðÇ=€¾Ù]ªª²víZÜn7Û·oçÜsÏ%77—òòr*++‡Ýïh©ªªBU£S³²²øùÏÎ[o½ÅúõëiooõýÍž=§Ó Àš5k€h@²¬¬ EQX´hþóŸcõÍfs,PæõzO(%äÍ7ßÌ¥—^ @uu5Ï<óLÂü¬B!„Bœî¬6; /¹†Úª}ìß½mØŸ{ #åSf’WXŒÙb  ÒÖÜÈ};iom´½V«¥¤|*ùE¥Ø©±µ:ÚZ8T½ŸúÚƒ£B!„ŽÔŠ H)>‡Žpp¼‡‚½p:ÞæêA7U>/ 1†V®Œ¦}á…å„Ãn¾ù""‘?ÿùë„ÃEA«Õ …ûµÕé´|òÉV´ZM¬Ý3ϼ‚VMK÷ÿþ¥ÕjâöóÊ++úõ‰¨±ïnï¾»¡_?‰öÛ×Ïñý¿þúê=EÒÝÿýtuuñÝï~—C‡ »ƒ &ÑÀYCCðÚ*ŠÂ’%K€èZf}¬Õ«Wsî¹çÑõ߯3ðæ÷ûY¶lwÜqùùù<øàƒ<ðÀTVVòé§ŸòÑG%\»n$ú‘Á` 6°jÕ*î¾ûn€~·ìììX´¹®.Ùeû›;w.³fÍ¢i)Ÿ|òI|>߈ûB!„âTVR>“ÙBN^{wnV[Gj:ó]ŽÑ]9 ‰D0Määb4™YùáÛ¶·Xí,X|V{ ½~?½½>,VY9ddåPTRÁº• ÿM/!„⬤(8ŠgcË›LÃgËÆ{4g%ùÈÃMÞî!jŠÑ¢s¦¶d.îûñ~¬@œ™ú‚[þógqÛUUMtbÛC¡p\»D·¡ös|Ÿƒõ5Ð~“é4éú‚n# ÚØlÑ#‘ápâ“B>W\™F«GoO;:!º]oÂèÌŽ««FÂ\ÉÝ/4gbN/@guò¹p×ï%ànJ·‘¥‘Ô[SÑè ±×!Ÿ›p¯ç˜±aË›„Ö`&àî §vÇï¯9³kn9Z½‰ § ס„¼®AÛŒE'LQ°–aÌÍB›bCÑiÑZLèS˜ '`.šŠBÛ{Ÿ xÓZÌX'£ÏH…H„@GÞ}5„½Ã›Ì¡1±M+CŸæ$âïų¯š@ËЙæsQú4:g j$B°µÏþ"þ¡S¸2Óâ^»\¨ÁБά“K0ä¢(hé gg%jPŠÉÑ=þøã± ÎHx<l6Z­–ÜÜÜaðúfwèõúØì7€¶¶6²³³IKKcÚ´iìܹsÄcÒ¦^„¢Ñö+WÃ!"á WÞÖ )cxË¿øn²Ï½%¥¡ªÒ¾k5ïþjÀ€À”»†)5—ÆuæÐ¿MXgÒí?Æ’]BËç¥ú¯ÿWfÉ)eÚ×ÿ³_gÙùœóÍÿ‰Ûæ9\ÉΗþqÐc²æ”RtÅ}Ø gô+ë:¸‰š¿þ‚Þ®R ¥7ü8J.Ôwn´&+e_üβóãê×¾ÿkšÖ¿‘°/Sz>%×=‚½`ZÜvUкù]j?ø ‘ààAžÑèc48Ì&÷Î0d¤ö/TU" a¯@K‘ÞÇ£1ȽóÒ/™‡¢×ÅwÓµf ËÞ&Ø>øïŒÆ 'ûæ+ȸz1ãÑ@)ªJË[ÓøÿÞJØNÑë(zäk¤œ3E¯ïWñ÷ÒòÖÇ4ÿùo0ÈdžÉÏ=7þêg^Äõù.LE˜ø­¿Ã8!+®þþÇþ _ÍÈïý‹³CηkÐHÐ  ¾¾žÉ“'0uêÔ¤o&“‰ùóçÇ^÷¥RLdñâÅãxë‡Ù´i›6mB«Õ2oÞ<|ðAGìx^{íµ÷l ò’K.‰{}¬coMMM„B!t:N§“¼¼¼ÿþû¿ÿ›²²2Š‹‹q:<þøã|ç;ß! ¨?!„B!NE%åS8TUI8œüg]{Š“Üü"vmÝtK–35ˆ¦—<>èÖ§¥±ž©3ÏGQ¬v‡ÞŽ0™Løýr.Äàä:Éëä즷¥2íëÏatdö»iþü|muhœ¥çã,ŸKwõVª—?G glH×hõè,)ø;ñ6 èíFgNÁ–WÑ™Cúô‹1¦N`÷ïÿ UÞz´É ¼¸jwÄ^[³‹Ñšl„¼.¼­µqu]5ÛíËž?•I_ù Z£…p¯‡®Ê=]˜Ò&à(=géùLýÚ°û÷ÿDowK¿öŽÒó(¿ùñØkÞ@Jñ9œÿXü=WwÃ>öþ¿'ŽÃÓtE£EoKÅœY„Ñ‘Fg`òí?Á–?T_[ÆÔ4:ÀMkn“¿ò/è,)„>ºl"àéÄœž£x6Yç]ƒ)£½/?ŽIœ n4ú Y_ü¹·_ @϶½ôlÝCÈåÆ8!‹ôË¢hÿß›t|²nÐ~t)6Š¿Ki!½‡[ðTÖ`.ÊÃ<1Ô‹ÎÇ”—Íþïüû€ý(zÅß¹Ûô Ü»*qï>€ÖdÄ1w&†ì ²n¸ïþjº7îè×V †Ð§9Pt:|Õõøë‰ø{Ñ¥:°M+Ck1“ó¥«ÐšM^š8  àÞ¹E§Ã˜Ÿ>Õ!3 cn¥ßÿt)6"½-˜ r{dY&‘<ÝÐU·qãÆXàí®»îbÕªUx<ž!ZÁüùó1›ÍIícáÂ…¼ð ÃNe9–Âá0«W¯&=={h®ÿ6R™™™LŸ>=©ºùùù”””PUU…ßïgçΜsÎ9Ü{ï½üð‡?ÑO?ý4Ï?ÿŒFãˆÆqøðažþùØëk¯½vÀ™wB!„BœnJʧÑYe÷ðÖÔHÏŒ>íÚÒ4ò¬!=®è‹Õ /#+¿Ï‹«{èµ]Î:N‡ÍfÅétŽ÷pÄ)J®‘ ¹NÎn:“´ÉÑ W¢m‡W½€9£€Œc{O,èéLœvQU©_ñrl·”âñ»/š¬ÌÙW`LÍUåÀ›?‹ º¸ï£~ÅR :³ÍšSÚ¯®ƒ›ØøÓ/ÆþtØ€«jKÜö?ý"Uo÷O9sF9óo¦mÇ'øËO º;âÊÍxË>ÿ:Li|ìõ'ýRºj·SÿéËä\p#Цÿü–Ñèc4¤]2…°ÇGÓkïõ+vvÓ¹bCt_¾fÀ~ì³§r~tâHã²·úÝüuMÔüì·C®§†ÂTÿÛoâ‚n‘Þ=ÛöôKõx¬ÞÆ–„°ˆ¯—†ÿù ŠFƒmZù ã8VÚ’ °N.¡á7¯Òü—÷‰Ž>(§F"„“X7Nˆ>'üÛì÷ûùÅ/~ÁO~üΜ9“_ýêW¬X±‚ššZZZ°ÛíLœ8‘””ž{î9ÒÒÒb3´:ÄæÍ›ö½råJ***€hzÅM›6õ«sÓM7Å­sÖ§¡¡O>‰Äf³qçw&Ü×æÍ›Ù½{w¿íV«•_þò—ôôô°iÓ&éèè@¯×3eÊ”XP*xÉèëGUUÞ{dO]}ðÁ\{mtJð¢E‹øŸÿùTU¥ººš×^{/ùË\wÝuÌ™3‡5kÖP[[KOOiiiTTTP[[Ë›o¾9èXV­ZÅÛo¿Íu×]ÀC=Duu5ÕÕÕ#>>!„B!ƛ՞Bvn4KEÕ1³Ýt:=Ù ‡B4N|“@QÒ2¢_þ»;£7é²rò(,®Àžâ@z\]ÔUWÒÒ4pê÷ÆúZ¼ž,V;ó]Îæõ+éh;zÃÍáLcòôÙ¨j„mŸ¯!r eýO}ßœN6›5n›}ä:ÉëäìfLÍÍ¢ò6%^ç5äë!äs¡3§`É)9™Ã;ŽŠ¯½½-½õÔ§NŠ.)äi:€¯¥&a¶Stù½ (¤NYˆg€÷`´iô&B^5ïþ2n» £ªB¾ž~mÒ§E'=tìYÕ/ˆØ§uÛ^ö ôÖT¬¹¥¸öz‰˜-fôz=Á`@o`È,qÆÜè$}j0qšõÞÆhêO}šÝJ¨§F»Ô…ѵñÂn/mï}6ä8ã?tÏž~{¢÷ú5†þë·%#ÐØŠŽ h5èö¡¡µYèÙ¾öcŠª ºõx]/îX&³ ½NO  ÐeœÎR£Fß²e Ï<ó ÷ÝwN§³ÙÌUW]Õ¯žªªüîw¿‹›ÝµjÕªû]»v-ßøÆ7€hjJƒÁ@ ?½»/u¼ 6ô ¼Y­Vn»í¶„õ=OÂÀ[ß~ÓÓÓ¹âŠ+ëK/½4âÀTyy9ìܹsÀ|¤¹¹™ììl233™ª««‡ ¼õϤI“¨¨¨À`0ðÄOðÈ#$ r !„Bq:è›íæq÷ÐÒxtÖšÕfçüùKðzÜÞôzZmôë“ÏëaöÜ‹(,Ž‚ÖáL#¿°„úÚƒl^ÿYµX"‘k?}Ÿù‹¯Àj³sÑ¥×ÐÕÑF]íA4ŠÂäéç YÿÙ‡47žØzܧ:•èÒ&“)©ú~¿¿ßˆÉd’›åg‘¾l.rˆÁÈu"’ íº¸¢ IDATÁ¢5ZWR”ØÚbêI~Foq 5YQ´ú#éuG†tÂIËÆœ½0:Ês¸rÀ:!Ÿ‹Þ®fŒ©9XsËNÖÐhÚøáÞø`ÒöîOXWk´býœç®85yÈë"èî@oKÃ’U4>±Ûí¤8RÐé¢×†«ÛEgçàú‚mZËÀÿFjÌGËÔpâõíçD×Ivï>0`o<(šh€Mc6¢h¤$í ’ig±Èñ)2CÝ=l¿í[I··ÛíØSìhµZ"‘®n×€ÿ¿h4§þïµ¹Q›¿úÙgŸ±uëV¾úÕ¯rá…’’’+SU•ƒ²råJÂá0_|q¬lÍš5öÙÜÜÌÁƒ)--Ål63gÎV¯^=ZCNÊÚµÑ÷¢E‹˜:uj܇¸P(Ä®]»X¶l»víñ>’=}å_üâhºÉ¾À[(bÙ²e¬^½š¯}íkÌš5 ƒÁk Ù¶m+W®LjL¡PˆŸþô§<ÿüóØívrrrxôÑGùÑ~4Ì£B!„büétz 'Fo~ÔÜ‹zÌ«z½a fGëަr/›<“ôÌlꪩ=¸¯§³ÙJQé$ò‹JÉ/*Åçõ°{{ÿŒîžn>yïu.¼ø*œi±?ýî´}Ãgg|Ð ¤É ''{ØmûÒÄEožûG{hâ’žž€ßïv[¹NÎrˆdùÛë zºÐ[8ËæÐ±§ÿ„{þ4ºègwÃÞ1SjÅdξ’”¢hÖ1ßßXЭ±s–(}ç±=mSs0ØÒOÆÐbÚw&¿FžÁžû9uÒ,ÙÅÖí;n9~fÕhô‘ˆÃé@«=ºÞ=Å>dàͳ· dz0NÈÂN ¹ÿ{d›ýœÜ{¸…°·ÿ¿¥Š^‡Î½>{›Z‡çXSô:Ò/™sá¹XJ Pô#›w¬`{îfá%+Å‘‚V«}×°ÙmÞŽ]×YœyF5qlOO¿üå/ùÕ¯~EFF)))¸ÝnÚÛÛã¦T~ó›ßLºÏ‡~¸ß¶þð‡ÃW¢™_Éòx<|øá‡|øá‡@4j‘‘AOOíííq_ØòôÓOZþâ‹/òâ‹/&5ž—^z‰—^ziÀòÚÚZž|òIôz=X,:;;éììì7Ö×^{×^{mÀ¾ZZZ¸ýöÛ“—B!„§²Âârtz=ápˆCUûãÊtÉÞŽ©“–‘Åæõ+©«9Ûæq÷ÐÖÚD ×OIÅ4J+¦Q¹g;Á` __V›s/XŒ3-ƒÖæÃ®«fBþD2²' ( s\LÓáR>_û)¡P°_{¥Óé$u’\'"rœ=ÔpˆúO_¦øê‡ÈœõÜõ{hÙrtÍ+£3›‰W=€¿ã0{Çn€¢ÑQöÅÇH›zQtíõ´ïZIÐÝA$ýü9ërLéyc6†Ñ¢3 ;«0‘¾rMŸ¿FSßzyÉЙŽÀœeç'ÕFÑÆßf>FKûÇkI¿|!ÆÜL º‹Úçþ‡`û‘`¢yõbì3&Ðòæ‡ ûÐÙŽy{û¾=™ôiJžxSAtMAÏþj|ëõ¸c³õr¾| Švx3ÊBÝ=I§“b(còÛ¬ª*­­­´¶Žô{´õôôÐÓÓ?ïï©& ÒØØ8ÞÃB!„â”P\MÓp¨Š@ ~at]OȆ#GSMUîÙt;VUånJ*¦¡ÑjÉžP@}müS³)ŽT,¹£ÉÌþ=ÛØ³ýsjîÃbµS>eE%“È™PÈÂK®fÕÇ=cƒoºHt-¼¦&í5²Ùl±´pMMÍr£ü,ÐÞ}*¿©©yˆšGÉurö‘ëD GËçï Ñê)¸ôï(¾öaò}_[Z£Kv €«•ý¯>‰»ëbÂ…·Æ‚n‡>ø-ëþܯNJÑÌÓ"ð-ªÑ þ¹J£‹fùNÝålBþ£÷~VýwýÐ3}mµ£ÞG"Ý]Ýq©&{\Cß§Žøz©zêWþãW±N*fÊÏ¿‡¿®‰Ë1/Cftv^ëòOèød}Â>BnoìgñäMWøwb*È!Ò úé_ãÞÝÿsyέWÁ0o£ÁÕ튥šp÷œº×¹[cFB!„BˆSDvn>6»€ªÊþkl蓼…Ž™¹ÖÚ|xÀzwáp­V‡ÙÒ?]Ôyó—`4™ikiŠÝúx==lÛ´†æÆz.XxŽÔt&Ï8—[ß9Ý)DÓ%›Úíø›ä’îìÐÛ ”Ëu"#׉® §_[j8ˆ%»Ôž¦tWn¤yÓrBþ±½ažyÎôÔíNt;„|=D‚~4z†”ÌAëR¢éµƒC¤¤OžöèÌ'E!ØÓNWåð?‹F‰ôôô ‡ÐëõƒAIÎ> ¹=Ññ¨*ž½Õr3Ñg¦v{éZý9í®Å½kàõùÔ`°Ç‹ÖjÁ˜1*Ç2†Ì4l3*h}gE Ûxêéé! ¢×é I¿?âÌ#7!„B!Ä-¿¨U0mÖœ~å‹ “ÉÌ‚%WÑ™kM ‡bu½~TUEQ Ǭ÷–H$F«Õõ[;.5-“G*µUûlßÔpˆÃõ5LÈŸHÁIJ36ð66›ŒŒèZ0r“\ D®‘ ¹N„!%“ÒEÑhÙ³ô;Ý'Š‚Ñ‘€§éGRã)ÊèÌîQ9’jOQ†5Oc%öÂØò&XÍ`OÇx$0×]³íD†9¦Â½^܇÷cË›DÊÄY4oZ>.} ÄçõácxkZN¸ãz¬“Khÿ`5õ¿yuDûuïØcÞ9ئ•£èu¨Á“?SØutm@_õ©¹&²ßçǧFߟd–©g¦“?ßR!„B!N"Ý‘”GŠ¢!3{B¿?V{ ­6¶Í|$×'ãéqÄê'¢ÑjÑ ÌõöÆß±ÚŽ®õÑ××@º;£O Æ~¼³Q_:%¹I.#׉H†\'–?E«#  õÝ`,¨jlß[ZÂ*½cjNôÅ A°H0ÚÞ–ž°Ü^0 sFaÒC IibæÚñÚw­À’5Û„I ëdξ5¦sÿºaõ²µï\@êäXrJÆ­ÑbZ@Ø;¼€Ý±:Wo@k³yíÅWNÐv˜"þ£¿³úÔÄŸÉ²Ž¦™ñ1™ñ&„B!„8£íزž½;7X^X\AIÅTü>/ëV¾€ÏçíW¯³£[ŠƒŒÌ*÷lOØ—=ÅûÙíêŽ+;vm¹¾u¢;l‹D"gìoÃÑÕÕ…Ûí–5˜Ä ä:ÉëDøZkAUÑhõ̼ÿ×ôÔí"Ò—RZòõàm>Hçþõ„{û-®ší8Ëçâ,Ÿ‹9£_[]¬LoM¥ì¦Æ”]ßÍ`8µŸ·¥Kv1Îò9R2¸Ú¢ŠBÆŒK(¾ú!mò·€]µÛI›zÖÜrR+æÅÈ´F Fg6Þæê~íZ·}@î‚[1:²(½éŸÙ÷‡ïãooˆ•§N^@ÞÂÛhÞø6î–¤Ç4š?_NÖ¹WaÎ,dòíOQõÖ³tÜ+W4ZÒ¦^DÞÂÛ©|í©¸÷o4û-þC˜òsȺþR,eEÛ:ûæ6¢‚Ú:éÙºgÐYdÝë·Ñ³}ö™“Ƚí–Ö·>&r$¢¢Ñ2g¹·]CÓ«¥kíÖQ?_mC,åeúÒñÉúØþ,¥…ýÓß¡h¢7}šs ®„M?›HàM!„Bq†óz_tÞïÞT‹D"tw œnª®æËÈÌ™€ÕfÇãîßoqY4½R ×ßo-¸î®v"‘†Üü"ÚZ›îG«Õ’WP @g{‹¤¨9Bn’‹dÈu"’!×ÉÙÍ×zˆúO_&É]ìé¤O]”°^È×Cíß^ mÇÇc2ŽúO_ÆQr.é÷ü‚Ž=«ô´ctd“ZqŠVKãº?“;ïfÌ™…]~µïÿº_?-Ÿ/'cú´33ïîꭨᶠ“0¦æÐS¿›@w+éÓ'5®Ö­ï“=çzÌT|ù‡øZjèíiCoq`Î,"ÜëaÛ/¿Ñ/( örà/?eòÿ‚)5—™÷ÿwÃ^BžnLéy˜3‹€h`¯~Åÿø DÉußÂ^4­ÁÛ6ãÞ_ÅÒhîùßoG×`„QùÚSLúÊSYLúÊOèílÂ×^FoÄšS‚Ö]+2wÞÍT-nLú-‡—¾¥¼(ºFÚ´ò„uro¿×–ÝÔýâeB=ž„uýâeJ¾s/æ’rn½Š¬ë/Å_ýLkÌÍDk1uãèZ·-– u´¨¡0M¯¾KÞßÝŒ© ‡ÉÏ=kó.Â>?æ‰ùØgTìrÑùéRÏ%ý’ ðì=H×êþáMzöq4zÚ”#iç r™ò_ß ÐÞÅÁýרŽ]œ]$ð&„B!„8»%yC µù0Ýí8RÓ9ÁŬÿìCüÇÌŒËÍ/¢`bôFƾÝÛˆD"qí{ý~UW2±tÅåSquwR[µ?®ŽÑdâܹ‹°X£7öï>u×?B!NW Ÿ-£cÏgØ ¦¡5ÛQޤ£Ó茘ÒrqVÌCg¶SzãD‚½tì]=êcð4V²wÙ÷(¾æ›˜ÒòȘqI¬Ì]¿‡Ú÷_Äݰ5&wÁ-è­‰gîôÔí¦jùs]~/Z£•´Éò¹¨ÿt)«ÿÄ„…·%=®H(Àž¥ÿLñÕß$µb欉˜³& FB¸ë÷¢5ZÎt×ïa×ïÿ‰¢ËïÅ1ñìÓbea¿›æÏß¡aå2"¡@¿¶£É`OÇ”š·-–¶’žèk«c×ï!Ñd̼cjN\?Þ–š7¼IËÖ¿i£!ØÞÅÞGþû¬I³3PôG΢ K±a›Z†¹8Ÿ”ÙS)ùÞƒT>þ¨Ç}–u¹8ðß“uãe¤_¶ÃŽ¥´0n?í®¦õOG=èÖ§íÝhZÓœ/]…>ÝIú¢×¼Óµv+‡—¾AØíEçLÁ>k2Z³)a?qçPô: ÙGf—jd….qb”œœy|R!„Bˆ³Pzzt-·Û=Î#_e“g0mÖ¼7,|±y«Í΢ˮÃ`4 ‡ikm"èÅnwàHžÏúÚƒ|¾îÓ„í5Z-ó]AFVô†‹Ï릫£p8„Él!-# F‹ªªìÚ¶ƒûvîÁžBjÞ«`â•Eã<q*+.ŽÎþ¬®îŸÖLˆ>rˆÑ¦·¥2õîŸaJËÃÛ\ÍŽý)X²Š0:³‰„‚øÚê¸ZãjR2P ½ƒ¤gÔèXsËÑ™lÜx›¢FÂ'42½Õ‰%»ÞHÈÓ·¥šp ¹5ÂôÖTÌ™…hôF®6üíõcpK½ Sú öL"¡^ümuCΚ‹>ÆRæu3ᮨù÷—èÞ8µzE«Å˜“>#…@K;½‡O^ QE¯ÃRœ6ÅFÄëÇWÓÐo ;SÑ-íD|'g=ǾLÇþÝ÷'‰‰Dâ~NIxíhqò4·u’‘:ª}ÊŒ7!„B!„H’ÇÝêÿÊÌsç“‘KVN^¬Ìçõp`ïª*wØ>³fÅ»”TL£¸l V›³Å+WÕ­M ìÙ¹…ÎöS{í!„âtã,=KN žÆtW ¼þkÐÝIû®•ä]t;¦ô¼ëoK Þ–škÄÖmD$ØKÏ¡£8.zº=Oƒ·í$èéÕñŒ§Hз© oSÕ¸ö1\“‘Œ+/¢3ÅŽ]íxmï}Fî×£h4s³†ì[ ‡ñ74ãohµñ‡ áÙ_3híáAË…+xB!„BœÕìÝÁ½;’®ßãêbõŠw1™-Ø©h5¼^=ÝI­Ç¦ª*÷íäà¾Xm)˜-Vt:=½½~<=Ý'ç‰\!„âlc/šÎ„ ¿LwÕæ!J挀~³Ï„8¨á0¹·_ Š‚gÏA<ûžlÌÉ@9’b1Ø~æM…xB!„Bˆðû¼qk¼„ÇíÂãvÒˆ„B1˜îê­L¸ðË8JÎ%ñ]4oZ?+KQ°d‘=çzÒ¦, åówãú˜rçÓ˜³†Ÿ&¹yÓrV.;¡ñ 1\j0„g_5ÖÉ%äß{õ¿}ïþÔðÑ4¤Z«û¬ÉäÞq=ÁNÝŸîìI!Î6xB!„B!„Bœñ\ÕÛhß¹‚ôéKÈ[ôò}…p¯‡p¯E£AgNAѽ]Ú¼é×ÿ%®%½uøki–¿#qø_§ô‡a*È¡ìÉD ‡ »½¨¡“­õèµìè¦úß~sÒÖDâL%7!„B!„B!ÄY@åÀëÿFgåz²f_‰5·­ÑŠÖh–FÂøÛëqÕî eË{xïï×ÃÁ7~†FoöžîŽ!똧ވãšçb¯»ßyßî7¤þYZ´xbß?=Mæ —bŸ5cv:‡=Vv{ñÕÔÓ½q¯t8!Dr$ð&„B!„B!„8K¨´ï\AûÎh f4:‘HˆH¯U ÚÚÛRsB{7O½1a°åØ L n†‚y±×Rÿì«?Úm4¼ôŠFƒÆbBQ"½"à˜ï_ˆ³Öf³ýh¼!„B!„8ù,–hZ™@@žj'ß#wvðÜËÎq‰8•¥¦FÓ¹uuuóHÄ©L®q"ÔpˆH°5Ô1ß_Æ×Þ#ÜUC¨uolÛ±A™žÕÏáz÷QTÀX8SÅ•Rÿ,¬ìëQ§ª¨ ‘@5Çg¤þÙS_og ¼º$ð&„B!„5xãIo"PÉëDœNŽ ¶èsf‰ƒ2Rÿ쮬[—°¾8}IàíÔ57%''gì“ !„bÔ &StQd!Ä™GUUü~?ÝÝÝcKOOÀívzßB ¥æ½Z&^Y4Î#§²ââbª««Çy$âT&׉BˆSÁ±µ¾¿ûþD""‘HÜÏ)))ã9\qDs['Ù©£Ú§nT{B!ÄIa0ÈÎΖ€›g8EQ0›Í˜L&š››efšB!„BqŠÓŒ÷„B1|‡C‚nBœEEÁápŒ÷0„B!„B1 ¼ !„§!“É4ÞCBœdò{/„B!„Bœú$Õ¤Bq’ÙnBœ}ä÷^œi6ì?€²²ôq‰8•õ­E©ÕjÇy$âT6Þ×ÉÆe¿Bœ E!??ƒ––.z{ƒã=!Î:EçfѸ·ƒHX%ÔÀšj"Š Ñ*Ìʤn[+³2Ù÷i=§‘‚Y™Ônn”#Ó¿r&¥ÑÝäÁ×Ý €F§!Š ª€ ~wtiƒEÛ_ѹYÑ~N3xB!„B!„BœÕ´Z ³f•R\œƒÙl¤³³‡¦¦N¶o¯" ÷ðNÈå—ŸO ¤²²††¶ñΰüÛ¿Ýùç–ÓÖÖÍý÷?OggÏxIˆ³†=ËBO«ò…y}!öÖ@þÌ L)"¡¥ósãÚõ½žvyá`w»EU­^Cáì, ` ‡"˜SŒ„ƒa¶¾UÛ_ã¾NzZ}'ëpGÕ)x+--¥¸¸­VË8xðàxé¤*++C«ÕR]]M ïá$%;;§ÓI[[íííIµ1 ÷ÛÞÐЀÛíÖþóóó±Z­qÛ¼^/uuuÃêG!„B!„Bœù-šÉCÝ@zzJ¿2¯·—G}}ûêÇad£ã–[.¢´tË—¯ã?ÿóÏã=œ¤9VÎ=·€Œ 3f³råöq•g´|µ›[°gš ™í`vP£17­‚VQ´ñZ­BÀÂï  „ õFÿ˜F¼Ý½„2K„C‘X½>î6ÁÞplÿ§£S6ðæp8xøá‡™;wnÜöŸýìg|úé§ã4ª“ïÇ?þ1)))ÜÿýÔןÿÁßrË-\uÕU,]º”W^y%©6YYYüÇüG¿íO=õëÖ­KØFQŒF#½½½¨ªÛþõ¯½ßu³cÇüña…B!„B!„8Ó-Y2‹ï}ïE¡¡¡ öÑÝíÁé´RZ:ÌL••‡Ç{˜g¥în[¶`öì2ÚÛ]ìØQ=ÞCâ¬Ò~(:Ãôøà×¶åÕ¨E£ÄýÝgçßj£ÛP4 ‘°×¾|áj65c°èޤ£Œöѧo¶ óXÚ˜;eo=ôsçÎeË–-|ôÑGØívÊÊÊØ¹sçxMŒ¡ÎÎN>üðÃØëÇû°Q…… rÇwŸŸÏÝwß7³nõêÕÔÖÖ––Æ¥—^:öB!„§EQâà)­N‡FÑ Ž,C…¢((ŠB$9á±!„"y†ûî»EQX·nßûÞïû}6Ðé´òô8zì±ßŸŸAss§¬ñv ùÖ·n&77wßÝÈ'Ÿlïᜱl ×ý—•åa³™¨¬Œ¦˜ôxüØlfÊËóØ¿¿UUcë‘[­f‚Á>_/ƒŽp8‚ªªx½Ñ5ÝRR,„Bz4 óþ>ÊÊ>Ç–-ѵR³²R C„ÃaÂáè¿»V °0›öv~/¡PNC8!Ž$ÜíÂäΛvçï1›Í4¿xߨ³>§dà-??ŸùóçÓÖÖÆ“O>I(tzçQ Z­6–šñLZÄ·­­ÿýßÿ°|îܹÜu×] ÓRöùè£b?—••IàMqÖ1 \xá…qÛB¡---Ô××ãñxmŸ––ƬY³X»v-~¿¿_§ÓÉìÙ³©ªªŠ=ìÈÅ_L{{;۷ǧÑëõÌœ9“¬¬,œN'‡öövvíÚEWWW¬^VVÓ¦Mt¼lݺuÀú}ÇÞÔÔDww÷ } Æét2cÆ ²²²0 tuu±ÿþÓ`÷½ªª²jÕªA?Ï\tÑEtuu±cÇŽAÇpÁ`±Xb¯#‘.—‹ÖÖÖgÆßÀãñÐÚÚJmmm›‰ÚoóæÍ Ïg2×Oß>´Z-kÖ¬IX~þùçc·ÛÙ¾};ííí'|]ôýknnf÷îݱí}×XOO›6mp6›9sæPYYI}}=eee :Æ>n·›7&UWŒœÕfgá%×P[µý»· û†šÁ`¤|ÊLò ‹1[l„BAÚš9°o'í­Mƒ¶·Xm””O#7¿³ÅŠ¢(ôúý´4ÕS¹g;=®®AÛ !„âÄee9ÉÊr°|ùº„ä„Bá~ÛÄɉD8tèôL5w&»úê¹h4Ö¬Ù=teqÚÒëutvº)/Ï#QÙ¾=ºÛŒÑ{ósçN& áryQèîöh(-ÍåСV¦M+" ±jUt2Uié¬VS,¨Ö×O‹ÅH  ¡  ‹@ ˆÑ¨§±±½^‹^¯%//Ú> Ñj5 ÷“,§ÓIkk뉞¦„NÉÀÛĉh@I‚n‰Ùívž{î9Âá07ÜpÃxgÌsÎ9|õ«_¥¢¢€Ï?ÿœY³f¡Ó’—°BŒ+“ÉÄ­·Þš°, òÁðÞ{ï øìÅ_{hÁçó%Lù›™™É­·ÞÊo¼1hàíæ›of×®]q·sÎ9‡/ùË8‚Á >Ÿ«ÕŠV«EUU^|ñE¶mÛ@AAÁ€ÇÒgïÞ½±ÀÛPõ›ššøãÿÈþýûíóXŠ¢p饗rýõ×£Óébç­ïÿ Ý»w³téÒ~A¨c߇””Þzë­÷qã7²ÿþ!oW\q999 Ë:ÄË/¿Ü/7X›––^yåöìÙ“t›c÷—(ð–ÌõÓ·£Ñ˜0ðvå•Wrýõ׳zõjV¬XŒüº>Ñ÷oÆ q·c¯1ÇÓïÜõq:Üzë­üéO¢¾¾ž™3grÙe—%¬{¼úúz ¼%åS1™-ää±wç–aµu¤¦3ÑåMÑÔ'Á`€H$‚Ñh"'¯£ÉÌÊß°ý„ü‰Ì¾à"t:}¬=€Ñd¢`by…%|¾v‡ëkFvpB!„HJJÊчͺ»K!Î6^¯—ËCv¶EÑÄ•i4 ~¯·—`0D0Âf3¡×Û‡#X­¦Xy«ÕDßó B$¢¢9&Åd_ûÞÞá°Š×ë&==…¶¶n¬VS¬}_y_ŸÇï'YÁ`ƒÁ0‚33´¸¨…ÕjeÊ”)ƒ>½›Œ °aÆÍÀÈRµˆ3Ë’%KxôÑGQU•Õ«WóꫯrðàAþô§?IàM!ñù石lÙ2Ìf3………\wÝu\}õÕ˜L&^{íµ~m4 sçÎe÷îÝ”——3gΜ'#QT\[y€ IDATTÄ׿þuxá…bA;EQHOOgÚ´iìÛ·¯_»W_}•õë×'ì3îÿê±õÍf3999sÙe—ñÈ#ðÁðúë¯9^EQxà˜>}:[·nå½÷Þ£¡¡EQÈÍÍeÉ’%ÌŸ?Ÿïÿû<ûì³ S$«ªÊå—_ζmÛ R&«»»›þð‡@tæ Óédîܹ|á _àþáxòÉ'ûÍ2ëîîæÇ?þqì˜3gÎ䪫®âÞ{ïå™gž¡©©iÀ6‰ôööÿP;×ÏÅ_Ìõ×_ÏúõëY¶lY¿§Ž‡s]Æû7UU¹óÎ;yê©§ðù|CÖûí·y÷Ýwã¶Ýÿý”––òío;n»¤2{:ž‚ârª+Ob¶X™¿ø ŒF®®¶nZMg{ô)I½ÁHaqí­Í¶w¦epÞüÅh4ZšëÙ¹u=nW4ðkOq2kÎ…¤gdsÞü%¸Þ{wÏÈgê !„bpŠ2tDL&N§€ææ®„3åŒF=©©ÑYñ--Ý'å3^ié**òÐjµ44´±cGõˆfìe3iR>&“ÎN7Û·W ˜´ZMØíÑ’:} iivÚÛ{ãï›ÍFŽÄÙ7<?==CöN4¾éÓ'’™éDUUZ[»Ùµ«&–6/Ã='cA«Õ0iR™˜L<?‡·³o_]lQ2L&3g–““JooC‡ZØ»·nÐ÷íØ÷øXv»…œœÔ¸m>_`ÌÎÍh^kÇÉ9HMµc4½?íry㮫‚‚LŠ‹sÐj5´µu³o_==Þ¤ŽõTP[ý^³iÓ~"•©S‹PU•õë÷‰¨ƒ¦Ì×h4ÔÔ4¡(J¬Ýš5»bÁ¶cÿî+ß¹³¦_ªªÆÞ‡ˆDÔ!÷S—äñ577ÇbQ£-.jñÄO0}útž}öÙØÓÅÃuà 7pÏ=÷ðñÇóì³ÏŽÆÅYnÍš5,_¾œwÞy‡ººdm„B„ÃáX@ÀçóÑÑÑÁž={øþ÷¿ÏÅ_ÌÊ•+ii‰OÙ1mÚ4ìv;+W®$ 2cÆ RRRp¹\£2¦  Óéx饗hkk‹mWU•¶¶6>ýôÓ„í@RÁDõûŽ}÷îݬ[·Žx€Ë.»Œ;wRYY9h?óæÍcúôé¼ÿþû¼ñÆqeuuu,]º”ƒrçwrÛm·%üì³qãFf̘ÁÝwßÍÓO?M0xbëD"‘ØÃI@ÇÃ믿ŽÍfcþüùLœ8‘½{÷öksìùóz½466âv»¹ãŽ;˜?~¿@äñm’q¢×Ï‚ ¸å–[ؼy3K—.Mø%g8×õh¼Y¹r%‹/æ–[naéÒ¥CÖƒýÞû¾ ñpϳ8qÅåèõ‚Áõµ‰ÓdÆìy&<îV}ü׸uÝ‚ÿ{÷e•>|ü;%½÷@JzH$¤@èMTT–&K±àZØu]]ßE]u-¿U×ÝeuEvm *ºP¦Ò!t!zzOf&3ïã 3“:!”ûs]^’™ç9Ï™3gڹϹ¦¼#›<¿WrJ¥ŠÊŠ2¶ÿ´ÖâGjUe9[7~Ïȱpus'¾w*;7¯o݃B!D‡4(‰¹s§pë­ÏPWg=)---Ž^˜ Àĉ/PZZÕaõ ôáé§ï&99Òâö¢¢ þñë —öDDñÄIJ²Lÿ¦×ëùæ›m¼ýöW6÷YëÝ»'/¿|/?¾€={쿺óÎA<ðÀ84&üÅ*2|xÜv¦‹+6²`Á×-~<..Nüæ7·2vlNN–“÷uºF6nÜË;ﬢ¨È~Šï¶¶‰£ŸÅŒ£ñõõ´º¯¶¶žÕ«w²téºfûÙ”)Ù6m$îî.·Ÿ>]̼yŸ±{·í-n¿}÷ß?ÖêöY³nbÖ¬›,n[¾|#ÿùOËŸ§Öpd_3ik›ü¿ÿ7™ôô8óßK—®ãw¾ÅÓÓÇ»‹¤¤ž44WcÕÖÖóÉ'ëÙ´iŸÝò.W½y^‹½¶ÛÞ)¢ùó~ê˜rí_ç->§£¦9Z¼ã|òÉ'<÷ÜsüáÆŠµ6øvÛm·1{öl*++ùüóÏ[]™ððpFŽItt4={ödæÌ™æû|˜íÛ·[•áïïÏèÑ£ã^_—ê™ÄÇÇ“™™ÉÉ“'ÍñÖ[o% €µk×rúôi«s’““IIIáÈ‘#3·]]]™99™Þ½{…B¡ //½{÷6›ËQ4 ,¸"×Bˆë]CC7näÎ;ï¤{÷îV·PSSáC‡pvv¦OŸ>¤¦¦¶yBÎåºté‚Á` ººÚ!åµVII K–,áñÇgÊ”)¼øâ‹vuqqaüøñ”––ÚýLã‘ÔÔTzõêEjj*»wﶸ¿¬¬ŒåË—3sæLn½õÖ­´k SŠI¥RÙÌ‘íÝ»—iÓ¦æ:´§ÿôë×iÓ¦±ÿ~>øàƒVͶկõüÙ³ÿ~œœœÈÊÊ";;›Z—K^t®È˜ ŽçÒØØò,^Þ¾t ïÀÁ=Û-‚n-áêæNPHWòåØìç:–“Ç—˜B—°næ¡À"]¬öH?-!ýäÆö>‚»îDxx sç¾g³ …BÁãOä–[Ò8vì æãììDzz,aaüõ¯÷óì³ Ù¶-Çêü ÊÙ»÷¸ùï>}ŒAÞÓ§‹).¶œhyàÀ ‡>0¾A\Úf.\°¼) ÆÏÌ™3qrr2ßÞ§ñgìØ±üãÿ°J/àîîÎ<`µIzz:S¦Lá»ï¾ãwÞ±Jg%„âêfš”rùgŒ‡‡IIIlÙ²NÇþýûÑjµ¤§§;,ðvöìY¢££9r$ß|óCÊl­ãÇ“““CBBÔÔØNѽ{w¼½½ùâ‹/š˜ùöÛoéÕ«±±±67Û¶m£oß¾Œ5н{÷rüøq¥´OLL :޼¼–¯ÞQ©Œ?^«ªÚ?û¶=ý'))‰Y³fqøðaÞ}÷]›éC›sy¿väógϧŸ~J||<Ó¦MãÅ_¤¶öÚIr# îާ—1ȉc­K3Ù5¢uµ5œ=Ýúß;žÞæW”•Ø=®øÂ9âA©TáëHÑù–§A½^¹¸¸àææFCCƒ¬vI?-!ýD(•JT*ãd5Å%¹&•J% …eàíüù²+Z·¶¸ÿþ[ˆˆ¢±QÏ /|ÄO?]œöÖ[_òä““Í{œÕüùÏ¿ÆÇǃC‡Nòç?/¤¼üâdÉ>}¢ø¿ÿ»—>}"™4i(K—Z®ÈolÔ³~ý^î¼s ƒ÷æ7VÚ D…†úk „­Y³Ëf]~þù ?ÿl™A`É’§ ñ³y¼=?>‘¸¸põÌ›÷ß~k9ŽÜ£G÷Þ{3‹ÿ`óüö¶‰£øúz2yò0V¯ÞÁßþ¶Üê˜=B˜6m$~¸Æn9·Ü’Î-·¤£×ëyýõ|÷ÝÅ­¦\\œøÓŸ¦0dH2üã$fÌxÍ*PõÃÙüðÃÅ}‘׬y¥RÉçŸÿÄ_üÜÎGÙrŽìkím€wßý€Q£R™;w*!!¾L™2Œøøn¬_¿‡÷ß_Eii/¼0 oo7êëeBÝÂjƒ¬ðì³Ïò—¿ü¥ÅÁ·[n¹…|òòržzê) ÚT™C‡ñûßÿžÁƒ3a²³³Y´h‘ùþòòr¼½½™0a±±±øúúR^n¹xРAÿ¾<ð¦V«IMMÅ`0°eË–6ÕÓ–ž={Ò¥K-ZDvv6ÅÅÅDGG3cÆ "##yî¹ç˜={6%%`+ ž{î9bbb(((`Ñ¢EäææâííMrr2Ó§O·¹‡Ù¼yóæ…^@¯×›Ÿ'€¢¢"‹cõ«_1sæLªªª˜?>ûöíC©T’œœÌ¬Y³èÕ«Ï?ÿµÊ4g+x'®OÖQŒ°gŸ}–^x¡ÙàÛ˜1cxøá‡ÍA·öìÁUSSñcLj‹3æE­ªªâØ1ËåÜÅÅÅœ:uŠððpúõëÇ?\œ•@\\Û·o'22’ž={nN½ÆÕìÛ·Ï*h׃9sæX¬Û¹s'ûöíãoûQQQLŸ>Ý"H5|øpbbb(,,äøƒyåXii)ùùùìÙ³‡yóæáììlq­S§N™( ƒU™2yòd´Z-O<ñ„ÅJ¾³gϲgÏÞxã ÂÃÃ?~<Ÿ}ö™ùþ¬¬,RRR(((àñÇ7â•••‘——Gnn.Ï<ó #GŽäÛo¿åèQë78!„WŸ€€FŒAaa¡Õgö€())±Xµk×.úöíKZZ«W¯n÷õÏ;Ç«¯¾ÊèÑ£ÉÈÈ **ŠÉ“'“““ömÛØ¹s§ÍÈcÆŒa̘16Ë|ñÅ[\¹”)hd7ð`1iÆFCyy¹9øcKUUK—.eöìÙÜqǬX±¢Uu6ñôôäñÇ·º½±±‘˜˜8`ÞÎÄÙÙ™¤¤$À8ñÇÝÝøøx222ÈÎÎfß>ëóžžž<ùä“6ëÍ;ï¼cq[[ú““÷Þ{/jµšeË–µ9i«_;úù³çÈ‘#lܸ‘aÆ‘m3øzxxyÒÅ8óõø%«ÝÔj'BºFШÓqîŒíI„ …ÿ@c1­V  £[ÏX¼¼}0`Ü£­ðD.ÎYgÐÐ4\ü¡­V;Ù<@{ÉkØé²ß7*ÓÀ¸»»› – »¤Ÿˆ–~"®'™™ñæTóß}·Ãæ1:]#55MöÞ€M›öSVf;Æwßíä7¿¹??/bbÂ8|Øò;SNN!……EDD1|xŸ&ƒ!ëÖíéÐÔŒ£F¥PUUÛæÕXŽhG¨¨¸˜¡eÀ€^ìÞÝô>嶤¥ÅâímLuø¿ÿÙÞL«®®M›ö3~|™™ WuàÍ}­£ÚÄÕÕ™²²jÞ~Ûr»½^ÿË>ê’jòFa3ð˜7©o*ø6jÔ(æÌ™CYYO=õ”E€«#mÚ´‰»ï¾›ôôt‹À[VV …‚7RXXÈ„ È<À믿n7õ§IKûuG<ö|ñÅ$&&r÷Ýw“——×i{Šæ™V»ÕTWqáìÅß.ž^¤ FmMµÝÀ›““3*•ñçS]m )ƒéÖ3Æâ_»Erêd»·ýˆÁ`ù#¿º²ƒÁ€B¡ÀÏ?²Ë•£&¾þæ_šëzcÀ|´•åíV‹V«ÆÉÉIËo ¦-¤Ÿˆ¦H?7ªÄDãÞ³޼¼ÖMF4ñðp5ïƒÖÔŠ¿ŠŠJJ* ð&2²‹Í Ó?ìfÖ¬1 ”ļyŸ[¬ëÚ5€˜ãÞzöRÿ9JZšqQÇÞ½ÇÑj[ŸQÑmÒ^ùùçÉÉ)4ïãÎ×_ocË–CTUµì÷KJJ4%%•œ;g?}êñãÆ>ÙºL ¡½}­#Û䫯¶X»Ÿ{n‘9ø&n M~#ÉÉÉá™gžáÅ_´ ¾9’G}”ÒÒRæÎÛ¢ G1ÞRRRP©Tæ–¢Ñhضm'Ož´xËÌÌÄ`0°y³íHvGØ»w/ƒwwwüüü(+3¾˜Mû×8p ©ÓÛ¬[·nÍ–¿eËî¹çÂÃÃQ(æÁ²–ž›––f±ŸBˆ«Ghh(€q áܹs¬^½šµk×Z­ˆ2¥LMM5¯<7Ñëõ„††Z­"‡ö WWW³víZÖ®]Khh(Ó¦M#**Š©S§òÞ{–BŸ9sÆnà­-L+›.O—x©óçÏÆUq‡7½”§§'-Zù¿lÙ2bbb˜1c/½ô’ÕsÑNg•&4??ŸÝ»wóÇ?þ‘„„¢££-‚z•••üóŸÿ4ÿ=zôh²²²¨ªª²»ŸšV«mq›·µÿ€ñ»Æ’%Køõ¯Í#<¼yóšl“–öëŽzþlÑh4|øá‡üá`òäÉVýW\Ôj'ºõ0Êòór,‚ÄNNÍOtsr¾˜&2:>™€ NžàdÞjkªpsó {TáÝ£ïE]m ‡ö]ž²¦ž¢óg #2¦'rÑ]–rÒ/ ˆŒA÷XnëJÐk^aÜ«ÅÇÇ»™#­™ÒÄiµÚ6§¨×´ÚÖï‰"ýäÆ!ýDܨüü¼(-­´9™®%.~˜Ød€ÁÅÅäöòr·yÿš5Æ`ˆ——;©©1lßžc¾oèÐd .pôhÇ-ÞprRããc|O8}ºmÛ%8²MáÏþ€gŸNïÞ=ILìAbbôz=äóãûY³fUUö'÷7vrRó»ßÝi÷¸°0ã﬎|,ŽÒÞ¾Ö‘m²aƒdAÍÞŽ=Ê3Ï<ÃK/½d¾) ~ÿûßSRRÂܹs[Þ©½N:E^^QQQôêÕ‹ýû÷ãëëKbb"›7o¦¾¾žüü|Nž}šÈÈHÒÓÓÍÓŠ'??û\»ºº¢T*[ô™{îÜ9Þ|óM^}õUâãã›=¾=Ôj5±±±”••59qÈôý¦OŸ>lÚ´©É2{õ2®¢i.ÀÆÔÚ‹/æá‡æ®»îâ“O>iEíí3 lݺ•=zaxkll4¢À¸B+%%…;3½{÷6¹ò¯9mé?—Û¼y3>>>ÜvÛmÜÿý,X°ÀîLÀ–öëŽzþìÉËËcíÚµŒ=šììì&ƒº¢stëƒÚɉÆFÇ-³5¨[x»äÿÀ`voÛDaþÅ×YMuÅEçÐ4Ô›HTl"¹‡÷Y ܳÀ›nÇÃË›!£n%7g?•e8»¸Ú5‚QqÔÖTãüK O׆Aä…J¥”YâYÒODKH?×*WÀ¸â­­. (dd´ì·˜Z­²yû¹s¥<˜Obb†ïc 1¥þëèÕn¦ôõõmûåÈ6q„ÒÒ*~ÿûù¤¦Æ0zt?22âðõõ$99’ääHî¿ÿ-ZòelžïåeÌøâííÎøñYÍ^O­V:²ú¢½}­#Û¤´T2 ˆÞrssyê©§xùå—ÍÁ·ââbæÎk3½â•°iÓ&¢¢¢HOOgÿþý 0…BÁO?ýd>fýúõÌš5‹Aƒ±dÉ233,޹RL)­LR¦ôGNNNxzzvHJ"Ó š¯¯¯ÝcT**• Ng²i4 C³u3¥c°5È&„âÚ‘˜˜ˆ··7+W®dÍš5V÷;99ñÚk¯‘––Æ_|Á` ¨¨NGdd¤ÅŠéK™VOÛJwl‹F£¡¤¤???»e:Âðáà ´Ú£ìräåå‘@||<9996S«ÕÜvÛmTTTðã?¶¨`óæÍ <˜ìlÛd·…éûÅå© /W]]ͪU«¸ë®»;v,Ÿþy›¯Ù–þc˪U«ðõõeðàÁüú׿æ£>jWèÈçÏž¯¾úФ¤$¦L™Â|Ю²„ãõŒIàtÁq4ËTêj'ûû­™4ê/®Í=¼Ï"èv©ã¹‡ˆŒMD©RÒ5‚S'-÷‘¬¬(cÛO?6`^>~¤f±¸ÿÜ™N?Jæ/«Þª*+šp×(¥Á81£¢¢å«§]]]qqqþå¼J(¿˜RWT4ýÙv)é'7é'âFeÚ/ÊÙ¹eiVm¹4]áâÅ?4™ZÑääIû“ÌÖ¬ÙMbbLB­þ®‘°°@¢£Ã0 ¬]»»Íum‰K«kÛ¶ïqt›8ÊîݹìÞ‹B¡ >>‚Áƒ{3vl&^^n<ðÀ8 Ÿ|²Þê<Ój¸sçJyóÍ/š½NGî¿çHíék×k›8ÚÍ7§âÇ·ßn ¨ÈøÛ$4ÔŸ±c3øê«-èõó^“!!~ÔÔÔSZZ‰§§½Þ@I‰ñóyöì±|ñÅfÀðËñþÔÔÔ3vl:Ë—oD¥R1vlï¿oܪ")©'55õh4Ú_&ÓöÔ›£GOQVVECƒ'4ÖæuMåÔÕ5X”oz|¯ý}™ÃÛ®ÅïÊyyy<ýôÓ<ÿüóÔ××óÌ3ÏXÌœ¾Ò~üñGfÍšEzz:ï¿ÿ>ƒ ¢¡¡;.n$ºaÃfΜÉ!CÌ7{i&M;®®®¯«¯¯/ÔÖÖšgý×××SRRB@@=zôèt“§N"66–ÈÈH¶nÝjóSŠÉS§N™¿dšêØdÝÂùŽ/Ow%„âÚÒ¿ ;wî´y¿V«eÿþý¤¥¥űcÇÐëõìܹ“þýû3räHÖ®]kq޳³3wÜqÕÕÕìÙ³Ç|»¯¯/••¶6zöìIhh(ì ›J¥b̘1ÜrË-ìÛ·¯E¯åË—ó§?ý‰{ï½—>øÀjE”——³fÍ" €>ø U+Ç>ýôSâãã™>}:*UûgG* ÒÓÓÑét?~¼Ùã7lØÀàÁƒ>|8›7onódª¶ô{–-[†··7ýû÷§ªªŠ•+W¶©N&ùüÙ¢ÓéX´hõ8\8 IDATO>ù$'NlWY±Bº„ãéeL's<×ze£S o—®<+:oµlMu:T*5nî6¹pök¿^ADÏüüqrr¦¶¶†3§ò):wšÈØDÀø¥²¼¤Ùº]«Óh¶4µ›‹‹‹Å ¹¤„»1˜Ò­J?M‘~"nTå寠³¿¿w›'/Wš÷ -)©dëÖ¶gcš½9sÆãááJzz[¶2§þÛ·ï8.tLæ/FGUU^^ntíÐü 68ºMÍ`0pøp‡°bÅ&Þzë·„„ø1eÊp–/ßhõ{»¨ÈØæ®®ÎWÝciöôµëµMÍÝÝ•ãÇÏ2vl&z>þØ8ö3uêpæÌ¹ƒººN*F¡€ÂÂ"\]3¦?ýt‰‡PW×À+¯3ýxzº1iÒª«ëilÔsê”ñøcÇÎX”kè««YY‰TW×áííÎîÝÇpssÆÍÍ™ŒŒ4‚‚|¨«ÓP]]‡³³ÚæuMå l¹ºÑôø:B«¦Cäåå1}úô©Hk]¸pœœˆ%))ÉœfÒ¤¸¸˜лwoRRRˆ‰‰áðáÔ––Z•wþüybccéÑ£G›öS*•ÄÄÄ››kuߨ±cã^o—:zô( `òäÉ­¼™Þ@•J%jµÚæ—ÄÜÜ\FŒÁ­·ÞÊ_|Am­õ†›·ß~;€Å ¨©nLš4ÉfÝŒÑç±6Ï5¹ž7‚Bˆë…‡‡½{÷æèÑ£M¦„ܵkiii¤§§›'Ÿ~ú)±±±Üu×]$$$pèÐ!*++ "++ ___Þ}÷]ªªªÌå<òÈ#øùùqðàAΞ=KYYÎÎÎtëÖŒŒ êêêX¾|¹Õõ“““í¦µ,..fÛ¶mvwvv&44”ˆˆ|||X¿~=ÿûßÿZÔ>………,\¸©S§2gÎ:D~~>555tëÖÄÄD\]]Y±b…Å䟖¨¯¯ç£>âw¿û]«>3ÝÜܸùæ›Í«T*¼¼¼HHH 00%K–´(½§N§ã³Ï>ãÁdâĉ¼ùæ›V×7nœÝó7oÞŒF£isÿ±E¯×óþûïóè£2zôh***X·n]³ÅžŽ|þì)((`ÕªUM¶¸ò»G`0èIì“nu¿»»'®®nd 3¾¾ŽçâÜé‹Ì4 õæçKö{³EߨˆJ¥nrï8¦¼#¶tŒ äÂ9»û0Þh\\\ðô42e\Ø#ýD´„ôá—Æ·:süëèÑSŒ™‚³³š=B9q¢õȵµõää’о}£ùòËÖ‹^ªªª–mÛr80‘áÃûü ¹2i&M²³s2$™¾}£prR£Õ¶îuîè6éHeeUüüóAîºk^^nx™W%™ìÚ•ËĉCñõõ¤gÏPNœh;Ók 3‡ÛÓ×:¢M®GÅÅåœ:UDrrO« ÃjµŠŠŠŠŠ*¨­­§¦¦ž?"#» Ñèö5ßoâì¬þeUš+*• ­VGddrr ÉÊJ4—kb*¯²²¦‘¢¢³ÄƆ““S@HˆÁÁ¾èõóý€ÍëšÊ¹¼|Óãëm_‡|Ø´i <òÈ#¨T*›)$7lØ@ïÞ½™3gŽU*ÊKåää0xð`n¿ýv6nÜh‘+,,Œ´´´&ë¢P(xá…˜;w.ùùùæÛÓÒÒ¸óÎ;illäÃ?´8gñâÅdff’’’ÂìÙ³ùàƒÌ_ø Ó¦MÃÛÛöFã•••TWWãééIVV–ͽKV¯^Íøñã åé§ŸæÕW_µH=5nÜ8FŒAEEK—.µ8÷ã?&33“ÔÔTî»ï>.\hþÑïììÌÃ?LXXÙÙÙVƒ¦Ÿi9!„W¯´´4Ôj5Û·ooò¸C‡Q__Ojj*Ë—/§±±‘ÚÚZ^~ùeÆOzz: Æ4nZ­–cÇŽ1þ|«=Ô>ÿüsÒÒÒèÝ»7éé¿5 ;vì`ÕªU””X¯îHNN&99ÙfÝrrrlÞLÇkµZ*++9zô(6l°øœn‰;vpüøqîºë.ILL4×9''‡/¿ü²ÍûÝ9r„72lذŸãêêjž8cªGII ùùù¼óÎ;v÷Q³eß¾}9r„„„úôéc1IÈÕÕµÉàQNNááámî?öhµZÞ~ûmüq&L˜@ee¥ÝÕt-ёϟ=«W¯&99Y¾ ]EÔjãŠ6…BIPHW»Ç)U*óýgO[fuhll¤¦ªOo<¼lG7•áôK`®¡Áþ&÷ö„w‹ÄÛÇ8qàDžÌ¾51¥¯‘ArÑé'¢%¤ŸGhh¸¸>0Л‚ËÌ …‚±c3:¼ÙÙ'µÓ ¾¶:&1±Ý»‡4YκuÙ$$tcРD¢£»šWŸ´ÕÚµ»80‘éÙ³ ÑÑ]Ñhtlܸ¿]å¶Ôºu{2$//w&NÂ’%¶'ó5µJÐÑmÒÞæ”y¶¸¹]œVScÅ#;ûååÕøúz2cÆMüå/ZÓZUUµøúzd‹£+¡­}­#Úäz´i“±,øšÆF=& F¯×óÆ+ilÔ£P(P©”ètÖ¿ñÕjë×ïA¥RšÏ{õÕe¨TÆÏa[é;U*¥Åu.ß·P­V¡×Ì‹’V­ÚnUŽ­ëšÊ¹¼ü•+noÙ¥ í˜ TÚaܸq<ôÐClÚ´‰×^{Íîq¾¾¾|øá‡(•Jêêê˜6m妙|üñÇ899a0¸çž{lîIæääÄ›o¾Ixx8†ììlÊËˉˆˆ 66•J…B¡`ÕªU¼õÖ[uøøãÍëõzöîÝË… èÞ½;qqq,\¸Ï>ûÌêºÓ¦McêÔ©”””““ƒ^¯'>>žÀÀ@4 ...<øàƒVi3fÌ`Ò¤Ièõz¶mÛF]]eee{ŠôíÛ—§Ÿ~777jjj8|ø0ÄÄÄЭ[7jkkyýõ×m˜Mœ8‘3f P((..æðáÃ( zõê…¿¿?gΜá¹çž³¬ÊÈÈàÙgŸ¥±±‘Í›7SRR»ï¾k÷ycÚÊ ››Ëc=Öä±&+V¬ÀÍÍ™3gÚ¤ˆŽŽfÞ¼yìß¿Ÿ¹s綨\!„¸˜öO»šøøøàììLqqq‹Rœ¸ººâççGmm-•••¶§›#)•J¼¼¼0 TWWË^ טëáùstŠï€cúŽØoøjâîáÕd:Én=c‰ŒíE}]-[7}@]]-šË‹ÔÌ!DôˆæÂÙSlùå¸Ëùø0ì¦ñlÝô=ç϶<îëÈ cQ©Ô”ççõß^ïmµü c€ñ©7Z–J©T^“¯[Ñ>¦÷){¿ù.'ýäÆÔÙý¤©ýâêÎüùðÛßþ»E{y„…òá‡ÿ€O?ÝÄÛoe¾/(ȇÇŸHzzœù¶‰_ ´´ÊªGxíµÙôë‹N×ȳÏ.dÛ¶‹{§§Çñç?ÿãÖ:_½•þÓz|R­Vñßÿ>F÷î!”–VñÚkËØ±ãˆù~•JÉС}øõ¯GòüóRPÐô~fNNj>ýôY<=ÝÈÉ)$>>‚ öòâ‹7yž=K–«óÝ&m5iÒPfͺ‰%KÖóå—›-öŸS(Œ•ÊOLD­VqèÐI~ûÛÛ,gäÈTžzÊ8½zõ,øÚ¢¬.]˜6m àõ×­³Ð\îÙg§3th2ååÕüö·ÿæÌ™‹ï¿!!~èõs:ÇŽÔž¾æÈ65*•¹s§ÒРåÎ;Ÿ£®®Áü=Þ`0Š.ÿ¿^¯·»èF\Yç‹Ë ´e©­®éoåååìß¿Ÿ>}ú°}ûv« 7ØÝ±cYYYäääØ ºqvõÓO?ÍC=DÿþýÉÌÌŒ«·V®\ɺuë¬R0]ª±±‘矞Gy„””Àø¢*,,ä¿ÿý¯ÝtŒ‹/æÄ‰Üwß}„„„0pà@òóóùÛßþFjj*S¦L±yî’%KÐjµÜ~ûí 0€úúzöï·ŒæïÙ³‡ßþö·Ì™3‡¤¤$óʽ††vîÜÉüùó¹pÁöÊ+ÈÍÍå ""‚ÁƒPUUÅ×_Í|@CCƒÕyÛ·ogåÊ•Œ?žÁƒS\\ÌòåË-VÛ !„¸þTTT4Ð%êëë¾Ò¨£éõúV?NqõçïÆU[Óô@W}½ñ¶^¯§¢Ü:-½Iaþ1"zDÚO/jª­ËíSS6µÜåzDÅ“”’J¥¦¾®–][7^×A·¶`Šh é'¢%¤Ÿˆö:}º˜;’–˯~5„ääHòóÏàEŸ>Q( æÏÿ?|{ó…µÓo|Á¿þõ0¾¾ž¼üò½>\À… 儇Æ¡C'©¯×šc· ®‘çŸÿW^¹Ÿ?^yå~Ξ-¡°°g¢£»šƒw“& åõ×W4Y'­VǦMû;6“øxcˆ–¦™œ>}4ãǰ¸Í×טüöÛ0jTªùöcÇÎð§?ÙžìÿÊ+ŸðòË÷ÎÌ™71eÊ0òóÏd~øà;v†ªªZ‚‚|éÞ=˜;Ú}n©=}ÍmòÇ?N"997·‹)æÿýï9ètètzž~ú}JKeLüFtU®xëlîî‡SQQÁ… šüÁkZñÖØØÈøñÆ™­þþþQPP@]]ËÓËx{{Ó¥KNŸ>ÝêYÇ^^^TWW7YWµZMxx8œ:uªU?ä]]]éÖ­¥¥¥vƒ——svvÆÝÝòòæg7´eÅ[PPy5ž½/ͲâMq½ºW¼ !:ž¬xë1ñ½éÕ'ÚšjÖ|ÝôlÖa7ÇÇ/€ò²b¶ý¸–úºKfņw'mÀp”J%û³·qüèÁ&ËR(t ïAL¯>øøúPYQÆÖMk¨«½þŸ“Ö®x7¦Ö®d7¦Îî'²âíÚÕÖo` =ýôÝ-ƒÁÀ¾}'X°à+Ž;Ã÷ß¿‚B¡èÐo={†2gÎôíe¾M§kdÍš]ÌŸÿ÷Ýw3wÜ1ÐîŠ7/fμ‰Ñ£Sqq±Ü«öĉ³|þùO¬Zµ£EcŠÉÉ‘üóŸƠФI/ÚL-w¹¼•‰‡6{÷¸{è¡Ù½ßÅʼn»ïÁ¸q™øùyYÜWTTÁ×_oå³Ï~¤®Îza‰#Û¤- Ç÷e„ÁÄÅ…[í)X[[Ϻu{øøãµV{»Ù2lX¦NAt´eúõúz 6ìeÉ’æƒn& Ýxì± DEY–U]]Çš5»Y°à+›)­­}ͤ=mòÊ+÷[¬n­«k@¯×£ÓéÑju<ôÐ<.\(—oW¹ŽXñ&·v²x­×–À[KHàMq½’À›7& ¼uŒèøÞ$¶0ðæáéÅQ·áì⊾±‘â¢sh4 xyùàãglÏS'óصu£Ý2||ýéO—ð¸¸gJë´ZŽÚCÞу7ÌJ ¼‰–è쀊¸6tv?‘ÀÛ­k׺u F«määÉów^†…?ÂÂÈË;CEEM›Êquu&<<À@4-'O^hr±«Z­¢kׂƒ}Q(œ;WJaaQ«Ê¸ÚÄÇǃˆˆ ¼¼ÜÑé).® ?ÿ|›‚~!!~„†úáææBYYyygÛ$ $<<¥RIQQùùç®HÀÍÑÑ&ƒÁ"Åäåÿ™n¦Kàíê ©&ÅuO¥Ráááaþ»¾¾žÆÆÖ½Á¹ºº¢R©pssshý„B!®Gžžž]…NuîÔ Î:´¤- ü¸öÍ–ÙT9: y9ûÈ˱ÞOÄÝݽٲ¯÷>¥ýå_ç:µâêvîœôѼÎî'7úç讲² Íwf¨©Ñrô¨)¿¢]u9w®’sç.–®õ~^ZZKiéÅLmy<Ý&Ÿo¹òêÒqÔÖ¨©Ñ’—wqû!W×¶¡VTÔSQqq_ãö”Õ™Ù&âÚÒ¯e ¼‰«Jdd$Ë–-3ÿýÒK/±uëÖV•ñä“O’‘‘áèª !ÄUÅ`0X¥˜B\ßd¿/!„B!„âê'7qU¨««cË–-V·—–ÚßÜÞž#GŽX­’;y²å9º…âZP__/«z…¸ÁÔ××wXÙ’ÂM!„B!„p ¼µ“V«eýúõ2¹JJJxùå—RÖ¥+æ„âzUQQ«««¬zâa0¨¨è¼ýB„B!„BÑ2ŠÐÐP‰ !„× ggg|||$'ÄuÌ`0P__OEEÆáå²âM!„B!„pYñ&„B\£4 EEE] !„B!„B!Ä/”]!„B!„B!„B!®xB!„B!„B!„Â$ð&„B!„B!„B!„HàM!„B!„B!„BPwïc÷Ξ=Ã8qâô•ªB!„â ©®.íì*!„B!„×Yñ&„B!„B!„B!„HàM!„B!„B!„BÀ›B!„B!„B!„ 7!„B!„B!„B!@oB!„B!„B!„B8€Þ„B!„B!„B!„p ¼ !„B!„B!„BáxB!„B!„B!„ÂÔ]!„B!„¢£xxxˆŸŸžžž¸ºº¢V«Q(r=ƒÁ€N§£¾¾žêêjÊÊÊ(..¦¦¦¦C®'„B!„¸º\··Ûn»…””¾V·öÙ>>deeÒ³g||¼Q*•44h¨­­åðá#¬_¿±Ý׸VÜÿ,ºvíÂúõùñÇÍ-ÛËˋǛcu{qq óçÿס×êh>x?ÁÁA=zŒO>YÑÙÕ¹!Èëô¢Ž|^Íbb¢™:u"z½ž—^zµ³«Ó¤Ö|¦‰Îñ‡?üOOO¾úê[²³÷vvuHOOå–[ÆPSSÃüùïPWW×ÙUº®ÉëTÇ #""??¿+z]…B““NNNxyyÑ¥KÊÊÊ(,,äôéÓW´>B!„Bˆ+ëº ¼yxxàoþ[­6>TWW×V—åííEHH0¦ÙcÝÝÝ ÆÅÅ¥Õ×¹\DD8>únn¶ël0n¨ý€BB‚ñððtxÙ*•Ò¢¿(•J”Êk3k``!!ÁwvU®*• ///jjªÑju->W^§–:òuz5sqq!$$½^ßÙUiVk>ÓDç ÂÛÛ 77·Î® !!ÁÜ}÷dœyûíwíÝ þþ-Ü®¬¬B«Õ:¤Žjµ ó߃††jkë0 ¹Æ•$¯S!Ú'$$„˜˜ó÷»«…ŸŸ~~~ôìÙ“ÜÜ\Ο?ßÙUB!„Bt€ë6ðöÉ'+,Vû¼þú_íŒ7'/ï8*•²UƒñŽð«_Ý››+ÕÕÕ|úéJrsS__““ww÷+Z—ë]yy>úGóßÇåW¿º£kÔv»veãëëÃéÓg:»*׌`ž~úIÞzë?:”Óâsåu*®5õ™&®MJ¥’3¦áììÌ–-Û8pà ÝcÕj/¼ðç—ÝÚ÷Û¦„††2wîV·ëõzNŸ>CNÎQ6nü‘²²r‡\¯£ÉëTˆ¶KJJ"""¢³«Ñ$///RSS),,äÀ]!„B!„ƒ]·7GÚµ+›]»²¯è5]\\ˆŠŠ`åʯرc·ù¾úz¨ªª¾¢õ׎U«¾ïì*\st:Í7G^§âZÔŸiâÚ5hÐzôèFmm-+Wþ¯ÅçUWWS__ßä1µš«¼¼N‹‹‹+îDD„ÎðáCùôÓ•üøãÏr]G’ש­çââBJJÊO+Ùxzz’MCCCgWG!„Bá x»Jy{{™7û>q"¿s+#ÄuÎ2ðÖØâóäu*„¸ž9;;3vìÍ|ÿýÔÔÔ¶øÜ/¿ü†Í›·vTÕšôþû’—w0ÄÇÇÇ2vìÂÃØ2åW466vZÝ„ÃÅÅ…ôôô«.µdKøùù‘žžÎŽ;$ø&„B!ÄuBo6$$Ä1zô«ÛµZ-o¿ýn K1î'Ò§OoFŒFxxWÔj'.\¸ÀO?maÓ¦Ÿ¬öÉÌL'33 gç‹{ÄM›6Ùæ*œU«Ö›{Œ€¦M› À;ï,´»÷Ê¥RSû2hÐΞ=ÏŠŸpóÍ£‰&'ç(ßÿƒÍóæÌy¥RÁ×_¯æøñæÛCB‚™þøÀø~3bÄPÂÃÃP«ÕTW×päH.¼Ôf]»vaĈ¡ÄÄDáííN§£¤¤”ƒ±nÝ&«÷s[<<<;v ÉÉIøúúPWWÇ‘#¹|õÕ·\¸PÔÄy­ëo¶df¦áååICC?ý´¹ÙãÛËŸ§—khh`ïÞý:t˜‡z€¸¸&N¼“½{÷ÛlÿÖ¶›J¥böì{pvvbïÞlÜø£Ýº¨T*xàœœœØ³g›6]\yw5¼N…¸–¥¤¤\“A7///RRRغU&!„Bq=À› >>>ÄÅÅZÝÞš”H :˜I“¼‚³gÏàO×®]˜4é.Âû²xñ2‹sl^×”Êîr?ÿlüaVYYEll …‚`òóO6[¿¸¸Xââb-aºvíB\\,••UMœƒR©dãÆŸ,nW©TæºÇÅÅ0{ö=46ê9þ<îîî1lØââbùûßÿesà|ذ!æAæšš.\(" À™3§Ñ­[8z½Þn½zö쎇‡ááaÜsÏtü9sæ,UUU„…u%%¥½zÅóϾIaáéfÛ§5ÚÓn`ü¡ýÄ¿#00€sçÎÓÐÐÀ AYôé“ÌÇ/µÙ/Lºt µyKö…1µ[PP ÷Ý7“°°®œ;wžêêjüýýIJJ$>>Ž¿ÿý ›-¯µ23Ó™>}* …‚ÊÊ*ŠŠŠQ©”„††Ð£Gw†Ê[oýÇæj2¥RÉôéSÉÈ0ÁŠŠŠ9{ö8^^^ÄÅÅCffóçÿ·É@ XÛšK5ÙÖש‰³³³ù|gg'›ç¯áááÙd]Ú":: oo/vî´ÂÌÍÍÍ\?•Jeu{^§ …‚)S~Å AY——g|ÎâãcIHˆ£_¿¾¼óÎB‹ç!?¿€Y³~B¡ --•´¸é¦‘tíÚ…ƒ94èæááÁO-tEXtCreation TimeFri 05 Jan 2024 06:08:46 AM CSTÀY¢ü IDATxœì½y|”Õ½øÿ~¶Ù3“ɾï!!’°P­ˆ{­Ú«Ö®×ÞÞªíím{÷öw[ûm«­µn]Õ¢¢W[E@Mö5Ⱦï“eöùý1ÌÉÌ$3!AÑy¿^¾$óœç<Ÿçïƒ> ¢¢‚¸¸¸ {÷îÝ˺uëp8ȲÌüãÏêN¸zãcÞ¼y\ýõ¨Tª kMMMüêW¿òÿ}É%—pÝu×!IAü×ùË_ÒÜÜV®±ðµÕ£G2kÖ¬ ëv»çž{AX³f F£1(Í©S§xúé§q¹\¿G#÷7¿ùMrrrBÊøÊ+¯°cÇŽ ½ßhη-DJÑíÿ„±`FÀo‡~õ ޾)y^(xಲ²Â^ÿÁ~€ÝngÕªU,Y²„-[¶°~ýú tyyy|ýë_§½½GydÂòDZJJJ¸÷Þ{±Ùlüçþ§¿ýdÚ´iÜwß}ÔÖÖòøãçÚ\uu5III$$$Ý×ÙÙÉï~÷;úúÎ}F÷¾õ-ÿ8:»ÝÎÚµk9tèPÀï¾ç:tˆ„„233ƒîmllä/ù ÝÝÝþß|ý_(öïßÏóÏ?Œ=Fn»í6ŠŠŠBæõûßÿžcÇŽ““Ã=÷Üãï'G—ýßþö· ñ8R|åPSSCrr2&“)(ÍñãÇyå•WX±bsæÌ ºn·Ûyá…8räHÀïÑŒ=‘–ëD¨ªªbÍš5!Ç%‹ÅB\\\Ègˆ¢ÈòåËYºti@y8Nþþ÷¿³}ûvÿo¾~pÆ lÞ¼9¤,¾4O=õ”_·xðÁIIIáñǧ¶¶6 }4õ@§Óqë­·2}úô ´ýýý<ÿüóœ>}:d^áˆf¼;_æÌ™Ã-·ÜB__¿ýío‘ǿ哇,Ë,]º”;w²ÿ~Z[[INNæ’K.¡²²’ë®»Žžž><岂À=÷ÜCVVmmm¬_¿žÆÆFôz=………¬X±"lÔÔTz{{Y·nµµµX,ÒÒÒ¸úê«ÉËËãÖ[oå‘GÁjµúï¹í¶ÛHOOçøñãlذÖÖVÀ[Ñssséï÷ö1<<Ì“O>É}÷ÝÇ•W^Igg'{÷îøþhäݵk—_~9EEEÆ wªª*ÚÑTUU±wï^vïÞM[[jµšòòr–/_NYY‹/æÝwßõ§ooo§««‹­[·RWWGoo/²,“‘‘ÁUW]EJJ ·Ür ?þ8Oð}šuëÖùëDyy9Ë–-cöìÙ óÚk¯މߣ>Ê´iÓ¸úê«ihhàÕW_õçÕÓÓQy.\¸n¸›ÍÆë¯¿NMM }}}F ±Ùlþ´999\ýõ8^}õU>ÌÐÐZ­–äädòòò&¬<ûe™ÒÒR6nÜÈñãÇéîî&!!eË–1cÆ ¾ô¥/!'Oždݺu477#Š"eee¬\¹’ÂÂB–-[ ŒF+÷‹/¾H||<÷Ýwn·›_ÿú×Q—k$œo[ˆI­ úÍe·†H9uüõ¯E¥Rqÿý÷£ÑhøóŸÿP–¡Ó©"šúP]]MOOf³™éÓ§sðàÁ üfÏž „îC¦M›F]]ÿûßijjÂår‘ŸŸÏªU«HJJâÆoäÙgŸõ§·Z­tttpäÈÿ³N'III\vÙe”””pÓM7ùû÷ÑÌœ9“žžÖ®]K}}=N§“ÂÂBV­ZEVVwß}7¿üå/ýýÏöíÛ9tèwÞy'f³™—_~ÙÿîCCC•§¢(|ãßÀl6S]]ÍÖ­[immE’$RRR(//§¦¦ÆŸÖ§<¿ûî»lß¾ÞÞ^EÁl6SPP @M”¢¢"jkkyõÕWýïS\\̪U«(--å_þå_°Ûí¼ùæ›ÔÔÔÐÝÝMrr2+V¬ ¨¨ˆ›o¾™ÚÚZýyF3öLF¹†¢¬¬ŒÛo¿ÇÖ-[8xð ýýýdff²pኦ«¯¾šË/¿œÆÆF^ýuÑjµTTT°råJ®¿þz©¯¯¼õ9''‡ªªª tRR999ôôôø¿ïXDSOàœ®”““þ}ûxûí·éîîÆl6sÙe—±`Áî¸ã~ö³Ÿè4cÍx7|ôÑGx<n¹åî¿ÿþ‹Sï ¸gÏÿ߃ƒƒÔÖÖ2<<ÌÂ… ¹æšk8zôh€Eq*˜3gYYY´··óØca·Ûïlª¥¥…“'Oò­o}+ÈR n·›Ÿþô§ñÓ§Oóì³ÏòÐCa4)**òOdYöÏŸ{î¹€ÊÓÓÓ3©ÊÂH<¿5 G´òvvvrêÔ) ™5k|ðAÀõüü|âããýèhFZ¥Á»ªòî»ï"×\s ³gÏP yì±Ç‚òéèè ®®Ž‡zˆÜÜ\ÌfsÈçíÙ³‡—^zÉ_6¾:ÑÞÞÎwÜÁ‚ ضmx<ý–*«ÕJccã¸e8­VËŠ+p»Ýüæ7¿ñàUîÚÚÚÒ—––"~øa€²0<>ž’’ÿ„6Ú±g2Êu4’$±jÕ*^~ùå€ïüøqNœ8Á-·ÜÒªžÀ¢E‹àÉ'Ÿô+œ‹…>ø·ÛÍêÕ«Y¹r%¿ûÝﯥüºë®#%%…ôôtZZZòi| eM4õ ²²’œœNœ8Á /¼àÿ½³³“W^y½^ÏÌ™3Y´ho¿ýö¸Ïv¼›,Föïâ”<áÐÙÙò÷7âp8HJJš°å7,Xà®OyI[[ÛyÍ‚B¹“X­VÿÌ.--ÍÿûÈeœPnSF£á¾ûî#''‡Ý»w³sçΈ¾ü}}$cYŽ ¼UÌ7IIIA#k½½½þ›šš2MWWWÈŽèÀ444 IÒ˜†h™?>:޽{÷t&áð•NN΄&x‘ªÜÝn·ß25zùÑÇÑ£Gï`1R½PrGËù´…hp¯"i§¾¯û¤m}ؽ{7n·›ÒÒR´Ú@kþŒ3P©TìÛ·/d®©¯¯÷[,Ãõ£q»ÝTWWyÅb 9~twwû—çgΜÑó"A.¿ürÞ|óÍq)_Ù'%%…tÁš,•»¯ë>_Ücåh INN¦­­-ÀèÃãñÐÐÐòÞùóç#Ë2»ví i­Ýµkn·;ÀÍÊn·³ÿ~ üøév»CÊ2šhë xݬÞ{ï½×?üðC€1]ÃFíx7TUUqë­·bµZyæ™g.^ t8inn&77—äädššš¦ôyÉÉÉQûíœ/>?»‘~S‡ƒ£G2sæL¾ô¥/qôèQŽ;ÆñãÇC.ž/#†={öðòË/GÔÎG^ßòlvv6IIIþ‰”,ËÌš5‹¡¡¡¨]w|V I’e9äÀé{†ÉdÂh4¢(ŠßßR£ÑDõ<€“'O’í¯?“o€í§Ž#GŽpùå—“ŸŸÏƒ>È8~ü8uuu!WnÌf3aó;xð`HK|8|u8œÒ3Ò—T¥Rù¿K´r_η-DÃ`Óif. øÍT< KÝñ)yÞÇA4u-ÚúÐ××Guu5¥¥¥Ìœ93`Â=Þ$|,z{{1cö‚ `41™L¨Õjÿþˆ‰ô!ÕÕÕ,Y²dRûßdµ¯¯/¢Ëúúzz{{‰ç;ßù䨱cÔÔÔ„ìGA`É’%cæÍXêp8B§Ó¡(JÈ4¾~ddOÕXM½MII¼ºC´}…ï^·ÛMAAAÈ4CCC âââüï´sçNæÏŸOee%ëׯ÷?7''‡ÄÄDŽ?Ðï†#Úz2RfµZRfƒÁq}Žv¼;_|ʳÍfã©§ž¢¹¹ùÓ§@ƒ×ò—››rÃÁdb0Ðjµ8Î ¾É2\ƒ[»v-ÝÝÝÌ;—òòrÊËËïrÖ‡~Ⱦ}û&e`©0ìÝ»7ÀU!¢•×ét²wï^-ZDee¥©§¬¬ FÃ|Òj?QÔj5 .dÖ¬YdddDl¡_'?)ùÁ9+V¤Zcc#Ï>û,W\q………,]º”¥K—244Ä¡C‡xçwòJLLäšk® ›_sssT ôD‰Vî©f²ÚB¤ôŸ>Œ°¢%V.¢m×&ýî½§’hêÚDêÃÎ;)--¥²²Ò¯, Š‹‹iii™T7A¨¬¬dîܹ俿NškÏTô!>%g¤+ÄXØívž|òI®ºê*fΜÉüùó™?>N§Óï;RÁñ¹Ë…cË–-Q£&ÚÖ¦b¬Œ¦ÞúÅHÖÑøî½òÊ+¹òÊ+ÇL;rÙØØHKK éééäååqæÌàÜÄ1ÒU³hë‰OW¸ë®»"–w,¢ïΟòl·Ûyê©§ü†ÙO¥í³ÊN¤bFƒoiM–e´Z-ÃÃÁ¾‰»ÝÎo¼Á[o½ÅŒ3(//§¤¤„ÜÜ\rss™>}z¿R´ŒTöíÛÇÚµk'œßDäݹsg=ÖæÁ‰b4ùú׿NBBÍÍÍlÞ¼™ÆÆF±Ùl¬Y³fÂ˾:i >Kn¸¨!¡8yò$'Ož$))‰ªª*ÊÊÊÈÌÌdþüùÌž=›ßþö·~e¢¹¹™§Ÿ~:l^S½Ú3’häžJ&³-DŠ­§ƒÞêýÄ—œ[†•ÔZ oþ&§Ö>6‡©h&qyÓi|ûÅ)•o2ˆ¶®E[Ž;†Åb¡°°Ð¿!¹ªª Q'ÕõFEîºë.ÊÊʰX,ìØ±ƒ††úúú°ÙlÌž={L‹ìXLEâ³âŽvm‹ÎÎN^xá^{í5*++)++£°°²²2ÊÊÊxõÕWýËógÌïz!'¾S1VFSo}zŸå5|ºÆ¶mÛÆÝð7z“ãÎ;Y½z5UUUœ9sI’¨¨¨``` â ŸÑÖ«Õêβnݺ1 Ž‘êQï&Âhåyd?ò©T }³³H7µŒl *•*b ¦Ãá ¯¯“ÉDzzúwã —ËÅÁƒ9xð ¢(2wî\Ö¬YCEEÛ¶m›ð²ÇH…aÿþý“¦0D#o[[uuuäææ’™™IOO¥¥¥Ô××Oª/Ô¼yóHHH`ÿþý¼ð AïÎÕ#¢­£‘ÐÑÑAjj*IIIQßÛÙÙɦM›Ø´if³™Ûo¿¼¼õÕ¸†‘4zti9Ä—ÎF›’E_õþ "Ÿ‰n¨œh]‹´>ø|=—.]JEEï¿ÿ>³gÏÆét²oß¾¨ŸŽÜÜ\ÊÊÊèììäÑG òW"Z¦ªï½$IAá#ÇbhhˆíÛ·³}ûvÔj5«V­bÁ‚\{íµìÚµ —Ë…ÇãùÄõ!“9VFSo}e‘‘µÌmmmäææ"IRPx¾ñØ»w/«V­bÖ¬Y¼öÚkL›6 ½^ÏÖ­[#¶þF[OœN'ÝÝÝ$&&b³Ù¢–9œ ï"ŧ<;ž~úé Ÿô‹vannnÈß HIIÁb±D¼ ÓápøgD#7D‚¯@¯¸âŠ¨î»¸ÝnvíÚåï"Ýä2š‘ ÃÁƒùë_ÿ:%>§‘Èë³4WVV2kÖ,$IšTë3xcÔ‚wGôD£ìììä4ßOnô¤ë|"8øêûüùóýþÙ¡§§‡ 6¯+cÉí+WQÏ«l|\¨¶[wµ¯=Ç8pIj-©ó—Sxó7™v×w)¼å›¤_öy´)‘mÌ™l|ËÕbC÷hÆ«Ç>Ksee%)))dffrèСI]IôSgΜ‰84×H’““ÃZØ.ôúÁ‡²@úê{8¿àptvvâr¹e™yóæE)í9|aÅœN§?¤ÝÅÀxcÏDË5>Ý¡   ¬>Žºº:À';ZWU«ÕÊÁƒÑétûÝ7BE ÇDê‰OæE‹Eí j¬ñ.>åÙétòôÓO‡ŒJuÑ*о°c#ILL䦛n`Æ –dŸµPÅNê¾»råÊ  ÅÅÅaë7âv»™6mZ@ ð~ô+V ×ë'ð†£¤¤$䆭Vë‘6+íH…áСC<ÿüó“¢0LTÞýû÷c³Ù¨¬¬dΜ9Øíö nÎßÒèܹsƒhYYÙ¸»…ËÊʸá†: •JÅm·Ý†V«eß¾}AKÑ#­¡k‹mÛ¶a³ÙˆçÆo Úœ'BÀ`œœœV±ñ½Û…ÚÝ ‘{ppЯoÔ‚©j ÑÒW}€“y‡%ò%ü e!÷1RI}˜ŽV«eîܹçýŒ‰ÖãîînjjjÈÎÎfùòåÀ亀Á97ÂÒÒÒ E'55•ÊÊÊ1ïçÞ{ï Z*_¶ltwwûÝ#FâëG¢­ë‡ÃèÉŠ+üF„‘ŒÏ4 EEE!˜´´4dYÆáp\½Ñ2‘±g¢åŠ––¨Ä/|á AãIjj*‹- yïG}DSSZ­Ö›z4¾Mñ¡ðÕó… RVVÆéÓ§£ZɈ¶ž¬_¿›ÍFNN7ÞxcȃcB}«¯¾šÿú¯ÿâöÛoø=Úñ.F*ÏÏ<óŒ_?ÍEëÂ!·Ýv‹/¦©©‰øøxòòòP©T9r$(‹Ýn§©©‰ÌÌLî¹çª««9xð §N¼×çÿôðÃSSSƒÛí&==ôôô°Okk+›7o檫®bñâÅTTTøwçææNêñ0 Ü{ク\.èîîÆb± Óé˜>}:ƒ'N„ A¸÷Þ{ÉÉÉáðáÃ<÷Üs“¢0œ¼‡ƒ}ûö±`ÁL&»wïžô é»víbîܹñ½ï}S§N!¤¦¦Fäê³`ÁJJJ8}ú4Š¢ŸŸO\\ííí¼ùæ›Aé[[[©­­%//o}ë[=zµZÍ¡C‡Æ]Zà7Þà†n`îܹäççsêÔ)z{{1›ÍÓÜÜÌþðÀk X¸p!½½½477Ó×ׇÓé$==¢¢"\.[¶l™PÙM%•ûÃ?äŠ+®à¶Ûn£¢¢›Í†Åb ˆ_;SÕ&Ê@C Gÿ>I³/#aæ%èR³6új©£ûðNº|"—©£©©‰>úˆ9sæðÅ/~‘3gÎÐÞÞŽÙl&'''ä -çSwîÜIQQ³fÍ¢««kÒÝðŽ?î?¸å¡‡âĉX­VRSSÉÌÌŒhB“Í÷¿ÿ}ª««ý HJJ V«•—_~9äòùŽ;(//gÁ‚¤¥¥ÑÑÑÑhäÙgŸ÷™›7o¦¬¬Œ””¾úÕ¯R]]Mss3’$ùËô±Ç£©©‰iÓ¦ñÅ/~«ÕJCC½½½ ùªñå÷q¶‘PLtì9Ÿr Åo¼ANNf³™o|ã466ÒÞÞNrr2YYYaËÍívóâ‹/úû¢|cÇŽù­²III”””°~ýú>ý¾vèûF™8FSOÀ;™|õÕW¹é¦›˜7o%%%TWWÓÙÙ‰F£!55•ââb~ñ‹_ø•yI’¸ì²Ëe™ªª*6lØàŸŒE;ÞEJii©_y~öÙgý-CqÑ*Ð/½ôsæÌ¡  ÀÌi?7n gðÅ_äÎ;ï$))‰yóæ100àW ÛÛÛyâ‰'¸é¦›HKKó[zzzX·nV«•;î¸#d¾›6m¢¥¥…k¯½–„„ÿì´­­'žx‚»îº N7åÖÇÃÛo¿ÍÌ™3ÉËË ˜Úív¶nÝ:¡ÎÌãñ°}ûv,ˤ* ç+ïÎ;ýq¸'Ûr^ëÙŸÿüg>ÿùÏ“à_ñèííeíÚµX­Vî¾ûî°÷¿ûÕj>÷¹Ïùƒá[­VvíÚÅ믿Ò‡Úãñð§?ý‰ë®»ŽòòræÌ™ÃÀÀ@Ä'îØ±ƒ¶¶6V¯^MzzzÀ­­­?~.ÔÙñãÇ1 ”””PVVO}}=o½õ–¿}|’˜¨Ü›6mÂét²hÑ"f̘ÝnŸÐŽÿ©h çƒÛé }×fÚwmFÒèÐ$¤"iõ¢„sxkg .ëÄOK;_Ö­[‡ÅbañâÅäç瓟ŸËåâÈ‘#lذ;ï¼ó¼¢ÛœO=>|ø0ƒƒƒèõzvïÞ=é}ôðð0Ï>û¬ñol°Ùl¼ûî»ìܹ“‡z(ìý§NâÈ‘#¬X±Â¯Ó餦¦†—_~9¬e·ººšçž{ÎjmNN­­­˜L¦q7Úl6~õ«_±råJæÍ›Gii)¥¥¥À9Ã…ÏXÑÞÞÎŽ;˜1cÅÅÅùôöö²eË–ò›‰Ž=çS®¡èééá¿økÖ¬¡¬¬Ì¿Ñápðá‡òî»ïòðǼ·¥¥…Gy„•+WRQQ°Âãñx8}úô˜›2wíÚŵ×^‹Õj :J>¢©'>öîÝK}}=×_=+P‡ƒÃ‡´A—ËEuu5eee´µµ½O4ã]¤ÔÖÖR[[˦M›Æ„´´´1{ Ÿ@:L[8FŸË®ÑhHII¡§§'âøF£‘ÁÁÁ°ŽïqqqþÓå¢}o½^Obb" !Š"ÿó?ÿƒ üà?˜Ôkc¡V«1™LèõzúúúèííýD öᘈ¼YYY<ðÀtttð³ŸýlÊdøøxŒF#ãnüY³f ,`Æ lÞ¼Y–IIIÁn·‡=\%Üsu:Ý„7©T*RRRp¹\ôöö†õíE‘¸¸8L&.—‹îîîODD™ñ8¹u:ÃÃÃܥ᳌ïˆ_ÇCGGGTÔ"a"õA–e~ðƒ Õjùñ<¥‘›  X,z{{Ǭ{ÜqÇœrícêq¹\AÇO&©3gÎD§ÓQ]]=åõ```†(ÇöôÝñ˜ÈæÅ‘Ï´ï=ö],Lt¬<Ÿr …Ûí>¯='çS¿Î—‰Œ‘‡îîîˆüã# éx7™\´›/|ÇWŽwähŒèP«ÕþØÏ{÷îý˜¥‰#ÆÅˆÏlª'á1bÄøôqÑ[ ? ’ššÊ±cÇüKs:ŽK/½”E‹Ñßßï?ð#ÆäPUU…J¥¢¦¦fRˆ#Ægƒ””òóó±Ûí>|øã'FŒ1z˜3gsçÎeõêÕ¸\.ìv»?ìPGG/¼ð¤Gˆø¬³ňã|ð¹€vì˜ÿ8åO ¯½ögΜaúôé$&&"I'Ož¤¾¾žíÛ·_PßçσÖÖVZ[[?‘–£††T*Õ'2†rŒ1¼ˆ¢ÈÞ½{Ù¾}ûÇ-J===ìÝ»7âÃÀbĈqá¹è¢pĈ#FŒ1bĈñqÛD#FŒ1bĈ#FÄè1bĈ#FŒ1¢ ¦@Lj#FŒ1bĈ1:FŒ1bĈ#FŒ(ˆ)Ð1bC_˜ IDATĈ#FŒ1bDAÔaì.†ã~A !)…¤”tDÑ;G°Pæä¸÷êtr §›ÎãñpâÈþ°çÕ§¤e’˜œêÿÛåt!H¢à•ÇépRw¦»mìã@Ó2r0'&àö¸q9\ÈŠŒ xެÜ4Z·A°ÛmÉ.Éj÷”“Çáv»B¦óx<‚€ A×’RÒÈÈΠ£µ™ö¶f\N‚ `ˆ3‘•[ˆ>NKá´2ª«„'$¥–™ @g[ ­Íõ¸Ýn$I"=+„¤òŠJ9yì 6ëð¸ïv1"ËÞ*úI¯w1>y¢„&!Yc@T© Q¥FÒP“‘uFœC} µžš29$•>I­CU¸6ìý8ÆŸŠŠQVð¸]¸lCc§ñ¸qYƒããËÚ8ÿ¿EoDrØPéMAi]ÖA<wï¤G—„¬1àq9°tcïï÷¾¹Tj½ÛUûe¢¢öÿíq¹pÙÏ•¤Ñ£IÈ@R´¸6l½m8Ç>­TRëP›Ó‘TZÜN;öÞ6CÑ‹IA“ެ5âq9q ö`ëmÆŒ;éˆ5êÔ$$A‘A’4jä8=Jr¢FÖÚ&ÜÃcp”JB<¢"ã¶;pt÷áèí‡0c–¤((FƒWIÄã%5Ùpv÷bïŒÀ "(&’A¨Ó"ˆîa+¶öîqåµé\½òظíçÌQÌF”äDDYÆ5<Œ­µ3¢|'ƒéÓ§£V«éèè ©©iÜô‚ ¢‰3‚Çðå\½ŒKLŘœ†¤¨ê릻±6¬Î2ÁˆÞœ„Zg@VkpZ‡éïha gŒ6,hãLxÜn¬ýˆ¢DrÞ4T:=½­ X:½1˽¿£ÒèmmÄÒ9þyj}æŒTZ=N›•Þ¶F†z»Ç½ïã&bÚwpÉ'U‘‘$‰ÌÜB’SÒp8ì4ÖÖ”ŠÉœˆM¥ŽL–dÔj N‡×–©Pù‰’Dnþ4DI¢£½…¶–†€´ƒý4ÔÕ0½|6jµ†”´L:Úšƒò‘œübEE_O­Íõþ|<½Ý¤gæ•[À™“ÇÆ}·‹‘˜#ZQÂ\²øÂ9ˆŠ*èºÇãÁãvâv:q ô ¨µSR¿ ™¥$”-FeˆyÝÚÝJû¾·°÷w„Í#eÖ2ŒyåØzÛiØòÇi’f,&¾p6öê7=t=ïê¯ ŠÒ¨_ã(¸æëAiOÿíW¸ö°òH*-I³–aÈ* 2Ø-=tìßÈpg}ØûóÒ ÒÅáŽR6Í%qÆbÿßçÊF ±| ñEsdl9EËŽWBæ%Ê*’+®Â==è}†;êiß·a\åÀæ’…ˆ²ð»} Žýøý&ФÓ`¾lºÂ<ƒ ;¸Ýx\®³Šp*£»-ô·Öd§c^2Å\wýýôï:ÌÀ±š`Ô*ÃYZ”ÐÍÈ#þ’9H:m@:kC olÅæÈø…UfNCT«ƒ/z< ž¬¥gë.Ücœð›²ò24Ùþ¿-ûŽÒóÁDµŠ¤‹Ñäf¤ïý`7ýû.Ì8z饗’Àž={hii7½ÖÏÕßø7Ý‚@Æew`H/ÂãñÐwfÖ®&ð¸Ñ$da*¬Âné¦u×kX;ü÷MEÿ& OÎÆeb¸« Ç`’¢BeLF“Ž’‡.9“º·~ÖÚ)«5(Ú8\Ãae”UÞ4gèU6§¥á¬­èM(úx<7à éÜ+¸aŸ#iôd/ýjS2›ÁÖÓ8ú»4: ÅèSrÐ]õ4½÷W[‚ÝIEÑê>÷ƒ(#JÞö]ró÷Òz­·}˜úO1ÜuNïÔÄ‘{Õ—Q'¤“½äj^ý©W! Š(ºsn#‚ äUì=J ¥Ñ1Ô‡àq#„g°ñ(ƒG‘u&Šoþ²6SÞ,’+®ÄÚÕDËö±v7“¾àFâK Z1J_°Cæ4\Ãêßù=Ö®Fÿµ.‰Ü«îG›Aæ¢[©;ز_ü9¯5Üã¦eÛKôžÚsîý$™ÌE·—7ël™ü/nGx‹éùzÝR4)¸‡iþÃ:\–s®<ûŽ‘¸|ñ—ÎF“žLÃ/àq†^æWg¤¶f9ˆ"ÖúfZ_\’„qvšŒ¬§‚ÊURdä³ tÂeóèyo7Ý[w‚Û«äömÛKÚ­× /+"~~=ïì)‡e÷!öÃÙk º–pÅÌK>‡\œ‡±¢ËÁ!óè}ßû-âf•rã ´iÉ$.]€aZ‡ªéÜð.Ë9ÿx'r|Wt+!烢( ¤³“¿ñG(П[}7 ‡?bÛ_‹mð\ù؇ngæ²ÕhãL$eÐÝT”OÛ©£l~ò't5ž º–9½Š+î{mœ‰ª•7óáÚ'e8«@X:[yûw?Áå°£7'qÓ~ƒ6ÎDgÃ)6<þŸ8¬Ã(-_øÉÐÆ™HÈÈ¥§9p²µàæ{É,­`ØÒÇ;Oý/] §ý×tñ \õµ’‘Ë¢Û¿ÆÛ¿ûñ¸e4Q–-[FVVVDiwïÞÍÑ£GýŠ,Ð0pÖ7h¤r¯( ŠJ,+)ý’(¡¨Î)Ðnwt­V¢Rã°ÛÆ|žÝfó?G¯cp ?àz|BŠJM_OP>Å3D‘ú3Õ¤gç¢(*LæÄ–ì‹éìÉ·S•Jåï”,–àÎ7Ƨ›¤ò%(=–ú# µœ ª7ƒMÇq õ¡IÈ möJ¶üa ¥ñ`ënYw›O0ØRM|áLù´îX2Y¥FÑèqª´aÛ€¤xÓ¸­ã¶IQ¡hôˆx¢2‚¨Íé$–yÝ&š÷¼µ»1à~·m€–/Q¼æ_P4z§_Jç¡w3q;8ñüü&ͺ’ô7àv99ò§‡ƒž9ž|¢$¢h¼«}9KïÆ9l¡aÓïpÙ­‚à/Ü® ¼´I9$uiÚòGlÝMi\Ãý´l‘¢5ß%¾p6û2±vºÚ ’Dæ%7!iôtx›¾Ó>Çí¢õ×0æÎ@cN#¡d!]GÞó&‚¤Õ`š;Þ­»p ½oß¶HºêRäÜLLsËéÛy0d^i7]lÐãì íù¿ã¶ŽÇÜn,{c!ô÷ù¬»Fç›ïÒ³u'x—cÎ2\Sç•W§EŽ3àöÛwy'_¡žÑ³u ‹æ Å0L/dàPuPšP2I9èò²°:Aûº þüe­Æ+³#¸žœ/ pèÐ!l#ÜMdYF¯×#ŠbÀ3M&³fÍâý÷ßÈG%4ï^†úƒ»Ùú‡ÿ‡gT½n8üó®¿sFN €ÇCwSmÈ÷l>¾Ÿ–êƒνŒüÊ…ìxñ©€ë¢(úeØû·çqŸÝ×eðÿ¾ý‹8mÞöç´YQitˆ²„Îd¦·åœkWRn13–\À–ßÿœîÆ32 ÷õ°ý¯¿eÍ÷IáÜEì{#?ä„`2(--eúôé¥mjjâØ±sn>¶@_,H’Œ"+ȲÑ;ˆ¢ˆrÖMŒ&³ßªì°ÛéîlsÞÛíD‘¤Q #\:•JÍШ´FS<’(ãtØò1ãIÏÌátÍ1\.'.§VÑÑ~£±ðMÜ"y·¯¥ÞhŒCEDQ¤¿¿Üûb|zÐ'ç (jì}­aëŒs°)­CzáÇÚf–.$•19¬¢¬BRiuxZV¼iTáÓøó“¼iq;£zwsá$•ÇMßé=!ïµu5áèï@“”yÚ|ºo3OI–‘TDWt²øÑû.xý©ë7=…ÛqNáe QQáq3J ©4Øû»h<òùÖ®œݨ2ˆÏ¯¤­'p…ϘSŽÊèõõì>þAÈ<ÜN; GH,_J|álº¾7î{i´dYÆétâ°;p¹ÆÞ¦NIDÒzËÁÞÖZŽa¸\HzÚÜLúw J£J6£/Πkømö¨¿‹(ÉH¯a¨oÇþвXíþ4’Z…{0ôæØ±pö JNDIÜóˤQã¢ãµÍ÷ˆ²Œ¤RðØÆ6zùˆæû,\¸Õ«Wc·ÛÙ·o»víâèQo}Óh¼ßL¯×SUUÅüùó)-- ±±‘ÚÚZ>‚(¢Òx-û'¶m;HÖ¡ÞNµV?¡6eéhC¥ÑaLIö:!ÃÈç{Ü®¿NÌ%EAQkPÔš€ßK.Y†J££¿³•ÆÃ¡û“®ººÚIÈÌ#¿ê’ЂDÛ~Þ{ï=>QÞgÎ*úŸ~Z–‘I’"zA‘o±Ì_|ú8c`‡ú3'9qä@ÈÝêCƒÞç!c4™±ô‡Þ€ŸäŽ(*Û’$£:ëßìr.Å¥å4Öò†²s»‘­nb 擎V« øÿx8jµÚ¯D_ ¡cLÞö«AÑöEgDR4bd“êÉBRk‘5q² •Þ„¤hpÑ7I²‚¤hUx%[ò¦‘ähYFR4àrDõîq93 ¶žÜöá°÷ÚzZЧcÈ(öº_„ñOõÊ-Ÿ}ÿèdñßö[ 4c¨íT@>Mï=OÓ{ÏÁã—1¿IÑ`í¬óÙ¶Þ6t©èRóƒÒ™òf!)œC½8,]c–‰¤hЧ?a3 â ~×µ˽½ãlbt¹‘ÔÞͲ².ÌJ…   ²Œ&zTÜŒ>Ã'C[)ÇC”%B#’(K#Š¡e§GÒªdtj$µ YI?÷¼î··YÕÏüï9W…Éþ>YYY¨Õj4 K–,aÉ’%X,Ün7†+Vð…/|E9·ùÔáp››K]Ý9…QE”³ÑÁÄ0ý…Ënó§‘"\qWkõhâLÈjwφɌ¢Ñ"…08Š‚0B†sßVù»(›J£AVkƒ~ϯºE£¥³¾fL9{ÛI-œNjað¦åPL¤ýŒtɈ„Ï–-JÈ’Œ$En–ÏnlQ-uX‡‘$™”´L q& ŠËP©5Úì¿ÕÙÑŠÇãA‘ KË9°{[Pšì¼"JÊ*ý»]K1Š¢øeðp®ìãŒñddåqªúˆß2-^yõøÉÅHR’×ÂãpD¿1PuÖïÝn·cµ^˜ðD1>^†ÚÎ_4—øÂÙ4½ÿ<žQ!$•Cf ’¢ÂÚ11%!t)¹¤T­$.¯•Á2à ¿ZåU2Ucˆ^¥X…4†’íO+zÓz\Ñõc2’¢ÂeÛMÄ9Üï—EÑèq†«çC8ûnc½ÿX‚ˆt6ÊJ_õ®èÞ'>IQ¡OÍ'÷Ê{æ3¤zßÅ`Ê_eJñ–¥Z7fj³7̘—0®Œ¦x“ßm Î7®ÀÞÑ…ÇfGŽ3`¬šŽep$ }Q²Îk%´6´„”C•lFRyËÓÑì: ‚(úóæ5 Í+µqÓI¸l.ºiyHa (b+Ë#ŸgÙ}ø¼Ú|´ßçé§Ÿæå—_¦ªªŠÊÊJfΜéÓrssèîîfß¾}ì۷Çce DÅÜ`Œ ·/M(ØGJ^1U«n'¯j!†„”i\bð3Q<'ƒ8JöËø=µY­ 2`Ƨe£¨5¤”råýß )@zQŠZƒÁœÑw›Hû9>õ ´(ŠÞ 7ŽK…¾Þ.¶n| Çm”ÒuòØAfÎ^@Vn!9ùÅ4ÕŸ¦§+0•Çí¦úÈfΞOVN*EMÙj†­Ãhµz²s IÉÈbÐÒï·n;Ž Æ"ú}Ï•ý´é¸=nêN¨¼¢$Ela¿ØùžAQ\®É÷m‹ñɤuç+ÄÎA›”CîU_¦aËü·DECÞÕ_E¥7ãq9ißÿÖ”Ö‹Ô¹×’}ùÝ 8†ûè9±[_›_ž¸ì2LùÞ áäD QRÆ0øÓ„ô‚ÓŠˆ’2æ  YgD”pã;ív{ÓáÝ.v5x`_Ú‰)Ђÿ~ÇpTy¨ô&QFŸVˆ>­pÜô¡¬û*­·LTz3©sV/o$ß'„Ò9î{¹Ütüí2Ë>‡õL#Ýïî>'g’™Œ»o@%ìíXö†vY‘Mq²ûÄVQôææ]¼i„1Ó’DÖWnÃ4o&öÖú÷ÅÕÛÛáÊb^<Uj2‚4þ¸>R&§eð¼ÚüD¾O__[·neë֭ȲÌW¾ò–,Yâ¿þÄOðÎ;û‚Ÿ!"ù]KC¿³ þ4áô¹×ßÉåÿðÏ‚Èp_'¶m¤¯­ ÇY·Ôìò¹äϾ4È@€sù t¨ßDYñNþGÉ£7% Ê2i…e¤–Qz^d•&¢ï6‘ï“››‹^¯7o€ööv:;ÏÅÊŽú •Hú$!œ]ÆWéFãñxü>ΡÒ=°‡Ìœ$I"+§€ÞîàÀãu5h´Z¦•U–™í? Å—ÿ©‡ñx<OŸ…ËåÂf \u9ÏíöYÎãŒñdääqêÄa#ü¢eEñF ±QæÓ€où¥½=|¬ÜÑ ôz¯µ¥­­=¦@†n¯¥æ•ÿ‚k¿EÊì•$ÌXŒµ£Û…6%YcÄí´sfýca7øM†¬2r–Ý ‚@ÏñmœyãÑ øÊ¢(_4aŒ }‚(!HrÐh`Ñ›&‚U6ñl~c)ä!q;½cõØ™¨Ö"œ]=s³©Q<+÷Xï?&‚àV8e-nÇ0²ÎLoÍ.:öo7½c¨7(—}A’±÷µQ"þvðCÇï‡,ýŒF#ÒY…oÀ2þÆPðn¬U*Òo»†¬ûn!uÍr¬MíH: šœ DEÆÑÝKý£W°ÿ,€gØŠxö¹’ZpèH¤¢àÏC ô*ˆ¢? !¾[ÊuK1/ô®Ð¶¼ðw:×ûf£ÉLCˆ`"‰L‘2Ñï㣰°%K– Ë2ÍÍÍdddpË-·°sçN††Æšl HJh%u$¾4¡ô¬³Yvÿ÷ãïoàÿ÷}œö@#¡(IÍ¿BµÉÎ}·ÙF¹äx]h• ™Ö!tæDjvlaÿ[/…}oC½á]¤F2‘ïs÷ÝwSYY9füãyýõ×ýê-Ђà=>{²äw¹œôõt‘”œF|BrØt5ÇÑÚTOV^†¸xAdÐÒKCí),ý½Ì½d)¢ ÒÛ×ÇÒår‚ÇÛTg—ž¦•Uàq»ƒLQ©5ˆ‚ˆÓñé<2ÕÓÙ6FÀü‘  ïl²­­=âûb|zp÷ƒ(â²bënF“ ¢„s°‡žcÛhÛýw¬=Á‡M&É•W"Hn§Ú·}8‰ x•ß1Be ¢èÿ/laü4AÏF¬26ön´)y¨M¡—|}hLÉ¢ˆÓ:ˆÛ5ŽÁûs»ÿ£íß=(†Dp¹èz.ª<»½Ê Z?áp7‚$ræ'¿ÃÑúûŒ•×бijcU¶ÆèÃ¢Š’äÏ#¼ ‡x.Íèo/ÞCN[CøH:¢"#ªTˆJ«.È ù>‚ ðÝï~—ŒŒ úûûùíoËÀÀO<ñÿþïÿÎ¥—^Ê­·ÞÊÚµkCß?Òÿ8Œ.##}‘ƒË%)§E­¡§¹{}A’eµ&¤; ÿp>Уڡ/Çh™ï!»|.…s!É2îq"eDC´ßçñÇ*ÿ•èD‹øOϲO0(~ÉŒª³‡´¸©;:†e_£‰’•¸\NjN†ZÑhu¨ÎÆ’îïÿ¸ÙO;¾·­­m±MƒŸQm²÷p!—5úðX“…ËæuS¦Ñ&ç „´ÃyñùK«âBç£èL˜K.‰X.§Õ{À¬1 ž` ]GÏZ‰äÊ!ÓÄ唣IôÜ},ôѽŸºŽlEo&eÎ5Ê£¯v¿ÿ˜ï¬Ëî˜Dé¢G?-A–ð8]r½°6´b­÷®Ì$_»9n ¿Ð©š€z<~ù•SÈ$¢Z…*%qjž?E|ñ‹_ô» üú׿ö»&îÞ½›·ÞzËŸfÖ¬YS&ƒmÈÛö c¬œ'çOóþcŠ G¶ü ǃޜĜë>Þ¶s>Äè `Š÷þ Žü¢Rò K8yìÖáЃ|GkÓÙg%’‘•Gí©Añ§SÒ2ýÿnomä³Noo/M1åù3Œc¨Ç€wòY|ó¿2íÖ£àóß9ûß?“»â«¤Î»•1ü 2ô×yªuF’*® ¸&H2¹Ëï'iæRÿß#Oè‰Ïª©è͘K\3d–PvÏ/Æu«”ëlü_A kÉ¥ J2KCËÑQO÷QïáYK¾H|ñç®k“s(\ýƒ-5Sr`Èd2ÔzšöýÞÃ4r—ßOÚüÕbà‚l\vewÿ4èûùð¸œÔoò†@K˜¾ˆ‚뾬 H£6§Qpí?QpÝ·§à-ÎamhõnüRdJ~þ/ä~ûKdãï_ûw^yñ\¼èp4>ó²wba6Røïÿˆ®0'ຒOÖ—o¡ø¾ Lh Õ¸ ñPTà IDATyWU†:35àšlŠ#ÿ»_Fžì—çbàÃ?¤¹¹™-[¶°m[`d®§Ÿ~šÖÖV>ú裀ØÏ“MÝoÔ0)Šå7\“d…å_û3—­öÿ­‹?ù?_Zk޲½×Ú¾ük?dþ÷ Êõ)»|.wÿbm¬Ÿ$¦¦\Äè FÜnÃC¡Ã/¥ed£Õhj>3’$Q^5Ÿ¼BïÕÙÞÂñÃû¦oi¬£¼j>’ä]Þ¨9ø^ н»W»;ÛŒ] àt~:}ÁcDЇS¯ÿœi7ÿQÑ`ž¶ dªÜå_¡uçÿQ¿ù™1cO”ö½ëI{-êø4 ?ÿË.c¸£I­ÇŠ!¶=#iÖ•H*-E7ý+Õkÿ—-°ßé:ú>YW| EgbÚÍ?¤ïÌ>=h“²ÑgLÃ1ØCÓ%sÑmÉ5ÜQGÇM$W\Eú›Hœ±„áÎDEƒ6) YÇ‘ßÿ3Mǃî­]ÿ´)yh“²)¹í?j¯ÅÖÝŒlˆÇYŠ ˆØû;©yõ§¤L}¤Ì¾†ôKnBÎÙ ®}×Yk}ÃÛÏÐ}<8|èhê7>‰&> c~%¹Ë¿BæâÛl©¼'úIQôfºm éæÑyh º”|Ò/¹™äÊå$–_Î`s5.»µ)mRn§æm/bí>Ö}2°6¶ÒúâzÒn½%ÁDüªé\C4ý~=ï‡öÙÿÝG(»|µ'QŒL[p†Äö¼ö'f]u#*ž›~ô8køå€ãÂ'“¿ýoâӲɟ})Ë¿þCñ›´œ<xÝMŒÉéèÍIÚü“êæ1YÄèŒ&.½|%‡÷áL@…NNÍ êsÞã_{»;imj7OYVÈ+*¥pÚ 4Zodˆ¦ú3ìÝù^ȃX|ØlVÎÔ§¨¤·ÇV«÷[«A`fÕ|Lfï2ÖXŠxŒŸ5úN}ÄþGïÆT8ǿ׷ÁE”QÅ%_ü9TÆ$ÒÞˆ ÉÔmxbÒepÙ†8ö§ïRpÝ·1æW_4ø¢y€÷pŽS¯=BçÁÍôפàºo£IÈÀe^rÙ©~áß(\ó]4ætL³p»tÜLýægÐ&åD¬@œyãQœCý~KüHkü@ó d‹Ñ8†ú8ú‡É^ö$ͼ]Jº”<À{Yçá-4lùö¾öˆe™²Ö€Æœð›bHÀw…¤ÖE”Ë>ÌñçHú%7‘:÷ZTq‰þòp tÓ¾ï-Zv¼2¦týægl©!ãÒ[Ñ¥—Sî¿ævXÿöÎ;<ŽêÜÿŸÙíU½wÙ’-÷Š˜jŠ!´Ð! $@B éÉ é7$Ü_ É $ÀM¡cŠ)6÷n¹H²¬bõ¶½Îï•VZﮊ-YÆ>Ÿçñcíœ3gÎÌ–ùÎ{ÞBçÞw&T<ÐúÏWéyæÊRÔfctuA¥Õ ÍÎÀ¶`j³‘ÂÏÝFØ ÷ƒ Çé\»oÃ1²®½ˬ ´Yih³"÷%±ëmϽ†«ºvBÎÃ]s”Ú‡ÿùw߈.'ƒ” ¢m®ƒGh~â9܇ޠ„Bd\u»%á8šTÚ¬ô˜mç‘ø‚S‰Ç“¼‚qggç„ßçvòäoæÊ/ÿ„’yË(_|>å‹#«`=-üû'_b×ÚqdÇû\ù•Ÿ’šW„?‰!q<ð»]<ýÐÇYzçYpõíXÒ²(¿<Úîìlcûžáý<~ZŠg);;{ØGž´´ÈÎé<ý-œÓg- ¬¢*fÛ@úEQb«ËéàÍWþÓW§7°hù…¤¦E–Dý~}Ý]CÌffkd™Õír°aÝÜÃ<™eçRX\NfN>êþ’Ëé`ïŽMkªÕù¨Ôj–_p9)©(ŠBg{ >¯—”´tŒ¦ÈÆ¡ý»Ø·k|"ÀOGJJJ€H M`Î(]5gå3FîHD̾þr|þAI’(.«¤¬¢ “9¶Œw à§ñÈaöïÞ6be¼¹‹VPX2E ÓÞÒL]M5­Í c^¢QË23æ,¢°xJ´¸ €ÏëáÀÞíÔŽ_j=“Z0Z …¤L=‡ »¶ík†ík-™Ã´Û~ÀÞ?}gÓS1E`ܱ̙†¡8O]#ŽÃß²o¼œ¬ëV¡ìºõ˧h†Á™Éå±wÇ&öîØtRc(ŠBÝáýÔÞÉbÅh4£R©ðzÜ8½£^J8RSMGÛ1ZšøOü‰= ²sË{ìÛ¹«-V‹×ã¡·'>´@p6£µ¤SpÁ'ù=tì~3qîå~ iƒÅü}ñÅ‚ æied^s1Ž]FÐúþ <‡ÈÚ$œ,g”€o\Ž>\Ž[Úíîl+ó}2~:;ZÇm<àLÃÙTM8àC­5Pv̓4®{OG#ACZk:iU+É_y;=‡>Àïô?Ì_y;™ó.ó±Ý-µT?ýÍ“>`¬8ö$󚋱̪ û†Ëèxu=ÁÞ!î4ý®é—®ÀvN$•Zçë#Y ‚áZ œ„|nê_û%—ŽÔÊe¤V.#ð É}lŠÉ}ìhØGÍ ¿ˆC­3¢1¥ŒùزÑ:r'`pî9Dφ­Ø—Í'ëúKɺþRBnaT*d‹ Itÿë\»žö—Þš¼ gg”´àÌCø@ ÆŠ9¯’œ%×a)¨Bc"†¿£gS5{ߦkÿ†Z§!¦¦øI2Bï¸U¢ÆŒ$a_:´‹–`(-ˆÉ÷¬„Âø[;pî;L×q×ĉ gB@ Nk„€œ *µ•ΊBÈïA ‚‰D¥×¡ÒjPB!ÂJ8yÊT@pb@pÆ»O¬´±@ða%ìõºtsÁYŠ(å-@ Œ! @ ‚1 ´@ @0„€@ Æ€Ð@ ÁZ @ B@ @ c@h@ Á‡IR‘VPŠFg8%Ç…T@0©¨uFl¥óÐ¥ä I~gÞöœÍ9¾ÜºàôEm2b,-@“f§ûÝ-(¡ÐdOIpqëOŸ dÞr-üá3Wâêî˜Ðã -‚ÉA’È?÷Vr–~•¬köõ´°ëÿ}špÐ? “Œë¼éÞw;=ïïZpÊ0ÚR(™·Kz6…3²ÿW&ô˜B@ `R(¾ô²¬ÀqtΦ„>4æ,…3p·âY Œˆ»·›#ÛߣxîRœmݽyÂ)ô)B¥R£(aE,G ‚ÉÁV6ŸÌ9«PëMT?õI‹>-Ÿ¬ùWÐðÆŸh~ïÿâúH*q‹£ã¯ÞAZ~1½­Í|ž ?Þýëd4™™6kF£‰®ÎvöîØ4â>V[ ³,Õø[ß·Ë™´=5=‹’)ÓÈÈÊE§Ó£( n—“cG8¸¿oÔç–žÅÔª9ȲLÝájëkÆ´¿@ 8»1çN%uú ‚ž¾Éž –‚é I´n}9a%<•Sb”pˆŽ£§N‘Z’T”UTQY5µ9Å`pt?Ä–Ôô¬û…Ãa¼wÒöé³0eÚ¬èk¿Ï‹Z–1™-”WÎ$¿¨Œ ë^ÁéèñXZ­Žé³RT:5º­¥¹aÄýArŒ™Åd̹„†uOŒíaVpòÈz áP€/ùo©`‘$,3§bœRLë?_ìÙjÎ8š–Éì…˰ÚR€ˆÐU©N,[ßÛ¯½@0Hئ„ÄÃá„m¥S§GÅóáêݪÞßçE’$2²r™»hzƒ‘Å+.bÝšç’ŽPP\ÎŒ9‹Ðö[°¤~«@ HŽJÖb)š‰ÎšÚ`A¥–‘ ´Ö L9åèìÙ8öѵý„ÍCkMÇV€Æœ‚Zg$èuân©ÁÓ1òC°l°¢Öœ]IúXPëŒ(¡ ~Gg\»Î–µôªõ¦ÈFI…Îo(ð÷u „GüÒZÒ0f•¢µfxp·×ãn©q¿NðgL­3!ÌÑ×ဟ€«;fn–¢™hŒ6‚>ÎÆj¼ÃŽ©1§b)¬Šìãuáj>€·«yLóRiôX‹f ³eúñtÅÙt곈ÈV3¦ÊR4)6T’FF¶˜Ñe¥a(/B¶˜PÂa:_`¯cرôE¹ŠòPô„Ü<õÍxŽÁ\%• cE ºÜLP¼õ͸kŽŽbG }n&ÚÌ4d»IVìéÃu¨ž`ÏÈ+(²Í‚J«‰¾¹<„܃Ëúú‚lŒåŨô:‚=½8÷ÕŒx=Æ‹K.¹³ÙLMM ;wZ£Å’– €«»sD×{v>G/>Wòó‘$ùUóH/,CR©èiiäè®MGX×™,,¶èë€Ï“íÂ’–EÑìÅí©øœ÷o§³!ùoƒJ­&·bv$ýœÞˆÏÕGwóQšì$ëÞÇQ©cÏQÖ›™sߟãúnùɵ„üÉoвÁJѪO“Vu’JÓæéhàÈ~M_ý®¤ûY ® à‚OD_^‰Â‹î${ñ51së>°‘ƒÏ~?áXj­âËî%mæùHR¬¡¥¯nµ/ÿ _wˈsÊ]úQrWÜŒZ›ÖÛÕLÝþ‡¾ºc8ÃC¶YÈûø5ØÎ™‹¤Ž7)ÁJ H ³oÃ14©¶¤‚Ñ‚³1ñ÷s¼¼èSóºûp6UãëmG­ÕcH/Ä”;s^Óïø {þp¾Þ¶ ›‡£~7R¿€ÖÙ2ÑÙ³PÂ! ûbçëu+ž5&;Ónÿ †ŒB”pˆžÃ›ñv6¡1Ù°OY„!½€ÊÛæà3ß§çp|¼J£gÞ¾˜“J­aÁƒÿˆé«( Û¹5a_O+}u;d –‚ª¨%½hÕÝd/º:ÚG¥Õ÷[”Ç©È •·<Œ)w ( ½µÛðt6¢1ÙI™²kɦìçìûß/óþH”^ù2æ\€»¥Gã>T²[Ù|ô©¹TÞò_I¯Éx!Û­Lyø´©„\:×®ÇÛÔŠÚ¨Ç2gÖyU8w á·'Ð=¼U.õÂ%äê$µ %À±ç®^4v ¦Ê2tÙÜs A‡‹¾­{’Ž“só¤_v¾¦VÚÞÞ„ až^ŽiZ–YäÜtMþWÂ}UÙbÂßÚ»¶‘`ŸÙbÂX^ˆ63”åóÑe¥sø[¿DI²‚ë©kDR©íVôÙh3SQi5”<ôLS‹#Öð¦VtYiH !ç©s#òû#Ÿë@ ñ ÷P¼Ž^½ÿ&•ËWQµrõ°ºje$(×ÑÙJݶ÷ö™séGYý¥!©Ôôµ£vëz‚>/3U6K?÷]Ì©¬ûãÏîßÓÒHݶ ÈZ-3FEøª{¾Å¢k?í£Õ1ÚSñ:Þ.øäWXvË=´Õàè®Mø=.t& ÙåUäVÎbѵ'5¿˜¿=ô‰„c¸{»8vpw̶ÌÒJÔc0ŠH’Šk¿õ?L?ïršì¢ißvÔ-¥ V`ÏÎç²û¿%-“uúï˜}Ï bÛûï èëíy‡ Âd‰<½„C!ÃÌ£³ýé™ÙØSÓP©ÕqKξ^¶l\Gg{+~Ÿ„£Ál6c·Ûp:]ôôôLöt“@æü+°—/àÀß¾MoÍÖh[û޵tÚDÅMßeÚm?dÏŸÖú{²ôÖleÏã÷á:VÃñËøöò…L½ñ;È+¹+n¦î¥_MØ<†fÛÈ[q3ù+ï äs±ÿɇÙ+ž¢KîÆQHÈç¢ú¯ßÀÙ<øP¯1¥PyëóJ(½êv>v!Ÿ+nŒ¡–á¡ßã­ÙGgÒôu{ߦsïÛh­éÌýü_ VR*—’½èj\-5Ô½ø®–J.¿Ìù—J" ‹V}Sî®nüí;¸Žжi­éTÞúC é”\q?ÕO3á™sW‘1ç%LÝ‹¿¤}çkÑ6•¬¥ìê¯:myÿ5ùÔ„ùzçßyÚŒT‚}N>øS]ƒ¢¥cÍ»äÞ~5WžOÙ·ïåàWFØŸX¸Ë É¿ë$• ×ZêùsÌX’FCêù‹1– +žÒ/=—Ö¾Jë?Ö „Enñ—îĶx6)ç-¢é‰çºƒ´½ð&kÞÅß~œÛ’$‘}Ãed]· ã”"ìËæÑýî–„Ç?öô‹¤¬X@á}·£ÍH%óºU˜¦Ó³aÍO>G »Ê_~]n&Áa¬ÙãÍX4Àîן§rù*Ò JÉ*›FkMâ‡ÿiçFDàž7þ¢Ä?X¤–sÅ?DR©ÙòÂ_YûØ÷ qS]rÃ]\ô鯳ìæ{8´ñM÷m‹cïºÙ»îE¬Ù|þï1XS¨\¾ŠE×~‚–Ã{yñg_¥åð^.àa毾%¡…ɞƒ#Vékþÿ[”Q<•å·ÞË;O&ÿm<°á5lx-fÛýO¯Ç–•xÕ/s.¿!*žÿóËo²õŧ¢mjYÃê/ÿ„Y_ò[îåÐoѸwðžrF•òîêl›Tñ  Õê’úNàtD|¸$I…é¸%ƒŽ5Ö ñ<dYF–eÌfv»}²§#˜2æ® çð–ñ<@Ï¡p·A¥Ñ“»ô† ‹á:v˜D>°=‡7Óµ¶’¹:ñÀQDÚŒóhxó‰ñ puSóBÄb¥1¥D-²C ¼lþñ5Ñ ëþÙ ÄlßüãkØùاÆ4¿ÒÕ_ÀÓÑÀþ'ÄÕ…ôÄ hSÎÒgFΧö…GbÄ3D|ÁkžÿIýgÌ,ŽCRË\±Œµ¼ÿ¯ñ ú©}á\ÝhL)¤ÏºhLç4ZÔ&#¶Å³hñÍÁ;@ëskÐåea_± éXyŸ¼I¥"ÐÕKÝ~7–йv= ¿ýÛˆó:öôK´<óŸñ лiWÿ¼ ÈVs¢] ö:âÅ3€¢Ðòì+QhóÌ©ñ}’ ËË"sõùt¿»…ú_=A ;Ö:äÿ´g™™™¬\¹½^³}@@ü?@jj*W\qEÜ8‡6¾¢ÓWÆ·äM›Ž»_{.aŸ‹>ý5T²LÓþ¬yô;1â`ã³àÐûo"IKnˆwKÆê/ÿ˜Ž£‡yò›h9¼7¦Í“À^TuóØóÆ¿ŽÙ~ä Ï=üùaý§OIR±òã_`ÿ;ÿ‰Ï¡`€—ù:ÎÎ6$IbÙMŸ‰i?£ôD`³§’_TFYEÅåè ñ>•Cñû#‚Wa ah ;áÓ<>ôôôÐÓÓ+DôYŒ!-rq·NÚg  Ì”]zJæ” OG$Jc:ý?§)KP”0{ÞLØÇÝR‹»5r³K¶ü”Í "îu/þ2ÆÂÉ»N˜²/­ê< âê‘̵Âuìžöz€èªÆPl¥ó ãGë–—Žò{¢ªö)ñc$Â`4`µY1 ¨Õêûë²Ó£A¢žºÄÁ’!§› #²"`(NlÓe§c,/ ýå·bîN„ŽWßM¸}`@LߨQ¼M—Ùfõn*–×GÓs …QÂaBÎø“DŒåýY¹r%_ùÊWxê©§øò—¿Ì‚ P«Õ1h“ÉÄÅ_ÌÃ?Ì“O>É=÷ÜÔ)SbÆ ì{û?T­\ðXÓÏ‹ëÖšý´ÖÆ»…êÍVÊD>÷[_økB 5ÀÎW#×§lá¹q+CÉ0Xì¼øó¯ás>¬*¡J8„§/~5Ø=ÄÐ9uÉÄb¨Q‚‰?÷–ÙÓ¢;vÅ °ÉD¶šQõHäÈwFãw§cÍ;q¾ô£Qï?Ö÷'//EQÐëõœþùœþùôööF3o­^½š;ï¼fðAÂï÷S\\Ì¡C±«"»_Žy«o&%·ˆœ©3c}%‰iýn»’XŸ g-BÕ?ïD®´õ‹oÞHJN]MG’ö ~çû1® ¯<úm^yôÛ û·9HsõNr+g³èÚ“[1“m/ýƒßÀã8u÷뙃µÇûRe M¥–É*­¤i$(XèaP©TÔ×Àãv¡VËdç`±¥P:µ Y£eû¦ø'ìÖc~4-S¦ÏfëÆ·âú•VP9c^ôup”~Pg#ii‘è\¯wìÖKtDT W˜³Çѽ¤T,ÁV:I¥ŽKɦÖ0åD¬;Φ‰ Ƭ²]­l>ZK|¤ù‡­)’**Q𼡸‘tV*YgJÀ7Þtî^7¦þï…!£ˆâËîIÚo@8« ñ–NMÿ’ZvŒ,,r‚1ŽÇf·ÅX5-VˈÚw¬`¯ÙfÁ:o:½Ägu0M-AêiîÃõ ÇѤ ‹ü- ûœJ¬ógváLÓËP #ï0 z’øK–±¾?<òþóŸY¸p! .dîܹØlƒ)àòò"ŸÎÎN6oÞ̦M›Ø±c>_|v’£{¶ÐÓÒˆ=;Ÿé+¯ˆ|yÓæ`ËÌEQÂì}ó…„s±¤fTYrãÝ„’d“uƒ×Ú`µCSÒÓ‹²ûõçGîtÏ|ë.®ûöcÎ\H~Õ|ò«æ£„C4ìÙÊþw_a×kÏ9 ÝX°>+J8i¦€¾ŽÁdKÚ`úO! £»«µ/>C8ÆwœhÛ¿{+s.§°d …%S¨¯=@WGltv8bïŽÍÌY¸ŒüÂRdYC}M5£ÑLaɲó q:z1÷çRkª<Áè‘eyÔEt~šÞy*’ý -ŸâËî¥~íï¢ERT=%«?l° „‚´lûþXÈ^| Eß’£^f IDATDÀÝKÇîuøzZ"s–¢™ØË’û£žN äÛ¡ iWitpŠ´ß9¶Ø—<ÒÆ¬’„ÖåãQ%()>°‚ ¬d-¸rÄ1$õÄÜn•`ˆ–g_!ÿ®H]¹÷Á#t¾±1Ú®ÍH%ïS")è lõ@Ê8E!웼{’¤VSxÿØ—D,ü¾æ6zÞÛN°»p¿±)õüÅèr2LJÛç‰/Ü@ɼe̺øZÊ­ÄhO¥pÖ" g-â‚O=ÈÛOü’Ïü~Ìc–”u!ÿðŸ÷¡Â…cÂá0wâ%JEQؽí}ò KP«eò‹Êâ4@}ítz=•3æ‘[@vnAÌ÷í@Q*ªæ “Oy2hiI¾DzyÓæD\ $)š}c×k‰Ó1®ÿú¯ûð{GÎ:ÒV7ñésë¶m nÛ$IEnå,*W\ʼËoBo±qÑÝ!I*Þûû(¾_'@ ÿ¨µÚa]ÎdíàïÚPŸn! ÇH0 »³ôÌì)éIûÜ·“æ†#””c±Ú‘$ξŽÖÂÑ×âå‘'ÐÞî®h…AA<KY£uÁ8^< ׳€;²ìt÷áélDŸš *5~G{Þ¢eÓ x»F±.yd̹$)’‰á¥_~èK…ûúÚ1d¡µf ÛO×ßô:“¦¡;ð÷u 1¥ ô¹ HÂ1«›Jk8á1ŽÇáp Ñh4üc±‘¢(¾¦VÂÁ †â<”°‚§®Çö}t¼º~ØÂ#C3nh³Òñ[%Æñ"õ‚HÀªë@mBñ<™œÌûc³Ùx衇e™>ø€Gy„ßüæ7deeñå/™ïÿû#jŽ£5;¸›œ©3™~Þ4íßAþôyX3sxÝT¯OnÉîk?ý»­îíõ‡’ö %LÓþ4íßÁûÿ÷8Ÿ|ìylYy,»é3l|ö£ª:VzÛ"×D’TXÒ²pt$.šdËÌþíbáúH-§òT’§£—ý»âSié F²r"Vé¶–áKÌ FÙl&==â—(ÄóÙŠÄ”kBkMçð¿~DçÞw&eú”ˆ¿¡·«ù¤rÿ*)ð¤q 4ŒÞ Ç¹ìj:ˆ½lú”d£• ;q)es^¤ò[ߑ᫫M6½u;0åLÁR4#¡¯ühè«ÝAÎ9ס1Ú0d[NqÛƒ‡±Å|hÒìÞ{’ZEÍ÷—¢m48÷ Z-³+'G@KÚô<µ§ç½ñDÞI’xðÁIOO§¯¯G}‡ÃÁ£>Êw¿û]-ZÄ 7ÜÀ3Ï<3âX»_>" W^Ák¿ûa4‡ñþw_ZTqt×&BÁjYCñÜ%§€Š«»ƒÖ²èÚO ·Ø°¤eÐ×>rEбÒ\=ø;•7mÕï&^Ê›I5êî颭î`tûéþ}2àä?A«REÕ\T*Ц¾öàÈ;FÅÀ²šÏg/£CF!¡Q,QN!_ä«5§&ícÈ(@FÌX®µ–ÄãhŒ6R*–Žz^A}²ÞŒJ3ºÌ6ûÞŽü!IdÌY•°¥púþ4‚©ÛNW:÷¾Š‚Æ”BæüËOhŒÞ#;¸"˹ùçÞ:޳;¦©ÅH²%JZ$e$¼ -QÑœ±úüÄe´˜¨ÔQŠ¿&Õ–°‹J§E›ùá Ƚí¶Û˜3'â–òë_ÿ:šjóæÍ¬Y³&ÚgÖ¬Y#޵çÍPÂ!¬9TÍgÚ¹—°{÷ ˆ”Æ>üAÄ¢ÎG?…Θ8÷©Âœ6¼ ŽÖ0øùóº&Ƭ~×&œ7ܹ—ߘ°Îd‰>¤T¯5&ûšÐ'€ÍyBv;Çþ¦–”WR\VÀ¡ý»ñz&ï&¦ÑÓÓCcc“Ïg1wþ€²)ýSoü¥W}©ÿß)Zõ²^9¢+ÂÉÒW)!­¤ÏŽÍs*©eŠ.¹;ZÈCRËhŒ‰Å€UScJ‰æbÀœWÁô;Ag½/h_}ä¾$‘Þ­1BHR©£ä¸y´¥k_$ëPþy·aŸ²(¦ÝQHÙÕ_"©ì:÷¾=ê9Mî–ZÚv¼ D*,f/¾é¸@AKÁt¦ì§qïßJ(ÈÑ×"N©Ó–Szåq™6t)Ù”®þ¥W>0g1ˆ·¡I#SñßQôÀÇ)¸÷ÖÈ¿ÏÞBîí!eÅÔÉÒÜõÓøÇD,R¬”}÷>Œe…1íš4;ùwÝÀ”‡èO)7þ8÷F,£–¹ÓÑåeÅ´É6 %_½ ]NFt>6nÜHss3ëÖ­cÆ 1m?þ8---lݺ•#GŽŒ8–«»ƒÚ­‘ÔK>ûM¬9ÖîÊë¿û!A¿{v·þô/¤–Å´ëL–ÝrŸ|ìù˜`¹ñfÉ wñ¹¿¼ÅŠÛ>‡ÁûJ’ŠY_싯 ißvü'¦„C¼ûÔ¯(_´’sïø|L…T½ÅÆõß~ ½ÅFÀëæ¿<;×ìììanÒˆ9§&šz¼Y¼ü"²ó ikibãÛ¯ŽØßd¶‡’öeç°xÅÅlÚðǧ:µZÍŒ¹‹).‹Ü :ÚŽñÞ[¯&MfžŒË¯½ F˾][8´טöý0RR‰¯««›ä™>,ØÊæ3õ£ßŠdH‚ÑòÁó}ã’«X­32óîÇÐÙ#®=‡7ãi¯G­3‘2u1s*­[^$}ÖE¨µúêwsð™ïÅ•¿VëLÌþÜ#[Qè­ÛNÀÙ!½SîT®nÚ¶¿JÞò›ðv6²ó7#W+½ê‹dÌŽü†ùûÚñt4 Òè1¤ç#ë-ìýß/&Lñ§1Ú˜ö±ŸaH¸Ÿ¹ÛŽàëjF6Û1çU"I*ü}ìÿëCÑb5ѳä: /úT¤á¯±ÿ™ó.'géõH’ ="°Î.BýÖú†×ÿHWõ†á†") §ÞðíhNè §¯¿r$Ò ÑZ#1.ÞÎFvýö3IÝ< /¼“œ¥‘,á WóAB~/:[†ô¨/üîßÝ3¡¾÷Y×­"ûÆá­é!§›¦ÿýgÒò×i—,'ïÎë¢9–ý­zú­&tÙÑ`«#?ÿ#½›csç”ÍØ}ǃ„½ñ«´–9Ó(ýz¤¢Ûþ{¿WqÐXVHù}IVöèý`'®´iXçW!Éj:þóWF@Û_~‹æ'âs|öÌÓËPéuÑ‚+þÖN*ƒþΣ «6Nƒ•J•0X0--®®ÑÇCͼðj®þú#Ñןý=¯ÿntù¬+W\Ê5_ÿ%²V‡¢„i­Ù³³ ƒ5…ì)U¨û…ÿýã/&Ì)=oõ-,½ñn$•{v>ÎÎ6ý«n¯ÿþÇI]! R#ãÖŸr{Ì6£=IRðyb„wËá}<ýµÅ!I*®ýÖÿD­Ì}íÇh9´Y«#¿jZƒ‰P0À¿~pÜy è!˜­6–­Œ,‡ìÙ±‰æ†º˜tFV.s­ §«ƒ–¦‘£øeYCqy%eS«¢U ›ŽÖ±íƒwÆ,žÁÈôÖleÇ£ÃV69%ZMKRÉh-iا,BkM'gÉuHj™úWÇ?Â;äs³ÿɯRzåXKæ`/_­fçëi¥æß?§c×ôÙEé• OÍ%ä_ ù\üÛw(»ö«èSr°•FòLJC:v½ÁÑ7þˆ!½¼å£KKP÷ò£Ý}QKüPk¼³ù@Òtkw/ûþüe .üé3/À˜Y-q­„CtìYGú?ãïÏL4žÈ3ú”œ˜ms*ùA†+¢3”ßCõÓß"géõd-XÖ’½¾åmÛ×pìý ë#}ô?á:v˜Üe7bÌ.ÅR8#ÚxéÜûÍžðÀÕÖ¾JÏû;0W–¢6£« *­mv¶3P›~0MDçÚõxŽ‘uí%XfU ÍJC›1¤)¡Ž]h{î5\ÕSbÙ]s”Ú‡ÿùw߈.'ƒ”!¥Ç]ÐüÄs¸A …ȸê4öÄùµ5©6´Y±þç‘ty§'¹ßô@¶©ÑR½þUüWÔÍ!Yéî„û¾»†?ᣜwÇ(_¼’ìò*(JRÂ!j·¼Ë{ÏüŽºm‰B )¹E1Û†ºcŒä…øëWn§êüÕ,¾îNr*f‘3u&C¿Ñ>·“=o¾Àú¿þϰ¾Ï:“S’dÍœÖF[JÂ~Š湇ïçØÁÝ,¹á.¬9X3gst÷fÞ|ü§4ì‰è<£,ÐÓg- ¬¢*f›$©$ EQb«ËéàÍWb}†tz‹–_Hjÿ‡Áï÷Ñ×ÝE0Àl¶a¶F–YÝ.Ö­Á=Œ_Nv^!…Åådæä£î¿!¹œöîØÄ±¦ÑY­W\¸{j¬¯—ª_ DÊÔ¾uGë±sËÈK86„Z0ÞH*™)×=DJåR%̶_Üœ4(n<ÐÚ21f¾Þ¶hyèÔ:#Z[æðAh’„)» ­% ×‰»µö¤‚!’Û”SŽl°ò{ð´×GÝ_Fµov)²ÁBÀÕƒ·«ù´N[7’J>5­- _oë ¥9ÔÙ2ÑÙ³Piœ]¸[ëP§GMÙn¥ü{÷£ËÉÀSßÌÁ¯üdä}¬f´YéÈV3!§O}SB«ò„ Iè rÐf¤¢x›Z tÆV©Ó¤Ù‘Tª8+¶`tí©¤äa²¥âqôÒV[SŽû”ÌÁ–BZA‹P0€££…ö#‡'ÅÀ(©Ôd—MÜ–‰ß㢻¹~XFY %•**0ãÚ$ IlKÔÏçõ°þ—).«¤¬¢ “ÙJzÖà“H à§ñÈaöïÞ6bñ“œ¼"rò‹Q”0mÇ©«©¦µ¹aL)ëTêáÎGÇ!W”¾@ð!ÅQHÊÔsºûhÛž| Q iÙò")•K‘$ú”œ( ý½mÃZeC>÷È×±ÃQƒñ ðâ8ºçÄ÷mØ7rÇ J8„§£á¤sƒûzÛðM°þx,s¦a(΋¤«Û™¼ºf°§ž÷¶‘uÝ*ô¹£‹ö9 öM’ñLQðm6Èñ‚Z06Ü=]¸{&÷áÃÝÛ»÷ä*DŽJ8ıC{`” JÎ(½wÇ&öîØtRc(ŠBÝáýÔÞÉbÅh4£R©ðzÜ8½„C£Kwt¤¦šŽ¶c´47ðŸØûÛk—äñh-é\ð B~»ß6±!m°¸‘¿oòË 'ŠyZ™×\Œc×a4€¾?(Ïß!„§@p²œQz¼q9úp9NÌ2ÕÝÙNwçØË[ ‚ÃÙTM8àC­5Pv̓4®{OG#ACZk:iU+É_ <é9ô~Ç ÿaþÊÛÉœwÙ˜ín©¥úéožô9cÅ±ç ™×\ŒeVÙ7\FÇ«ëû‹ªôÓï ‘~é lçD&;_9ÈR  ÐàŒ äsSÿÚ(¹üs¤V.#µrá€oHîcSLîcGÃ>j^øEÌj)q°ÉpÈFëÉM^ 8Aœ{ѳa+öeóɺþR²®¿”ÛCØã• ÙbB’];×®§ý¥·&oÂÁÂD(8óA„‚±bΫ$gÉuX ªÐ˜‡ˆaEÁïèÄÙTMçÞ·éÚ¿¡Öiˆi†)~’ŒPÀ;n•è‚1#IØ—Î#í¢%J bò=+¡0þÖœûÓõÆFÜ5G'q¢Á™ƒЂÓ! 'ƒJ­A¥3€¢ò{PB§GF`"Qéu¨´”Pˆ°Ç‡)S‚ñF¸p‚3–p(@Ø}b¥‚+a¯ïÔ¥›ÎRDî3@ ` -@ Œ! @ ‚1 ´@ @0„€@ Æ€Ð@ Œ3’$!!Mö4Á!´@ ãˆN­å;‹¿Å½³?3ÙS„È-Á8aÖ˜ùÁÒïQ•:zÇQL®€k²§%´©„½>‚ñ~N! @ ôj=?Zö*S*8ÜSÃW7|]ˆçÓ„¬ë.!ûÆ+PAjü;œ»Nö”ÎZT:-æéåh³Òèý`îÞÉžÒ !´@ '‰ZRóÝs¾EeJ ÎF¾öÞ7èó÷Mö´ýØ—Î@ÒÈØÍzÑçgSòЧpªZ ‚³•Û§ÝÊüÌy¸.¾µñ»ôúF/ ò?õQôE¹£êëké á±§NtšI)úÂÇѤ٢¯•P˜°ÇG «O}3Žm{ñwtûqO=·“]pJ0Dߦݓ=1#[Íå“´=ÿ:}[÷NòŒB@Ÿ"T*5ŠFQ”Ú_’$d–€ß7Î3g ¶²ùdÎY…Zo¢ú©oLötNšìEWc-™ƒ·ã(GßøÓ¤Í£2¥‚›§ÞÀ£;£ÉÙ4¦ýõE¹˜*JGÕ7Ø71.!Ʋ´YéÉ;(×Ó·mMúþö® ™ÃDÒú5t¿³9âÝçœìéŒI#G?#²Ý:ɳÀ. &3Óf-Àh4ÑÕÙÎÞ›FÜÇjKaö‚¥£ëûoãv%ÿ"¦¦gQ2eY¹ètzEÁírr¬ñ÷ïQ ë FʦV‘“_„ÑdA’$Âá0}½]­=È‘š',ÈÁÙ‡9w*©ÓWôœ®)Sc-™CW(0©ó¸{Æ'QI*vuìæÍ†u'<ŽûP=-yØ>þ®ž48vVÓþ›HZ ÚÌ4,s*±Î™†u~¦éeÔ|ï×xj&t¿­s²§ 8ƒ8#´$©(«¨¢²j.j9rŠÁ`pTûj4ZRÓ³Fì‡ñzÜIÛ§ÏZÀ”i³¢¯ý>/jYÆd¶P^9“ü¢26¬{§#ñ2_n~1s-G£Ñ ( ¡PµZÆž’Ž}~:y…¥l|g-¡Qž›@ ĘYLÆœKhX÷á€XÙœ•)ÌLŸ ÀÓþ~RcN»ŒÇ´N˜@O_Ü:^ySe)%Þ…Úl¤ôŸ¥úó? äL~ÎtÎ8š–Éì…˰ÚR€ˆÐU©N,ÝõÛ¯½@0˜Ø²¡„ÄÃá„m¥S§GÅóáêݪÞßçE’$2²r™»hzƒ‘Å+.bÝšçâÆÉÈÊeÁÒó‘$‰Ž¶cTïÙFWGŠ¢`0š˜2m%åÓHËÈfÖ¼%lßôî Ÿ@p&£’µXŠf¢³f 6XP©edƒ­5SN9:{6ކ}tí_?aóÐZÓ1¤ 1§ Ö z¸[jðtŒlÁ“ VÔ:á`€€3ñ²¹l° ÖQBAüŽx ›Î– R¤ ‡ZoŠl”Tèìñ†_J84ò9YÒ0f•¢µfxp·×ãn©Õ~’ZÆïèD EüÍy•2‹üx;q;”t•ZƒÆ’}-ɃJ£;%Âß×1ªqP|½mƒí=¶’ÙhmY(A?žŽ£8ö%ëò’Ëhs·±­mû0g?qÈ*½%$ÐxuAm6¢6èQB!]cÚrU×Rÿ«'(ýÆg‘-&²®¹„濚+¦ÊRd«™Ëƒûp=¾–Äï DÜ4ýî Á>'aŸØñµ‘÷0äòr{bÇR«Ð¤Úî7ÜuI­Æ8µ]N’,ìuà>\O sô«c½&…Ê ÃXVÔ üm¸ªëPB#ÿ‡2×$š4;R¿~ ôô¡’ U:-¦éeh3ÒPü¼M-¸…Vée›•V}}ügI_±¼•^G°§ç¾‚½ŽÈ¾'sr§²¬¡jÎ"ŠË*ðzÜìÙþùEedçžÐ˜.GÀð_àãQË2ÓfD¢}ÔT³wçæh›¢(´µ4ñÁú×9ïâ«0[l•N¥îpuÌm-ÔÚ‡F£eÇæõ1n·‹][7¢ÕêÈ+,¥ ¸œý»·k Î&$µLÞò›É^|5j1®] ‡PBî^¼¨dM‚QNž´éç’þèSó¶;›PûÒ/ñ´I:Fá…w’1w®c‡Øóøý ûä{ Ù‹®ÆÛÙÈÎßÜ×>ëÞÇQ©cÏQÖ›™sߟãúnùɵ„üž¸íÑý VŠV}š´ªóTê˜6OGGþókúêw%ÝÚÇ~†>%‡ýO>HÈï¥ì#_ÂQÓÇѸÚÿ7Þ®æ¸ýÙeTÝùHÜv{ù‚¸óq5bÏ_³Dã œ{ZÕ¹”\~j½9Úò¹ØòÓëŽ5'}vdÿ¶­(LŽK]έW‘zÁ9xj8øµŸ'ì“}ý¥¤_~¾æ6ª¿ðð DZ³×:L%¤]¼”c%odRtäßy=ö ¢hçîƒ4üþïø[ãöTZ-•¿ú&’¬¦õkhyö•¤s1–1å‡_ ö¿Á±+Öj®ÍH¥òÑo%Üw¸ë”ŒôKWõÑË-¦¸6×þŽýí%\ÕÉ"OôšŒ;’DÖõ«È¸|%j“!¦)ätÓòÌËt¼::ƒÂÉ^“áȸb%¹»€ž÷¶sôÑ'“öÍüÈEd]{ *ƒ.f»¯¥Æ?<;lÆ•ÂÏ݆eveôuû‹ëhþËó¨MŠî¿ËÜé1ý›ŸxŽö—ßÎ ‡ÉÈÊAQÂÔÜGõžíƒò‹ÊNée×|IRt÷ò»ÑÙ³ zǺ¤ÒÈ1EV2tŸiÑzÞÛ†©¢•^‡©¢çžØópñ0–‚¢àØY¯¹ ÙnÁ:¯ óÌ©”ÿóþæ/ã‚C.7}Ûöb[4 ûÒyà hû’¹º{q$Há@0ÎO[“‘šPì ‡¤R‘ÿéI=ÿ bwî9DØãE›©¢Ó´2J¿yûïý^Ô:9^×d\‘$оðqìKæà®9Šûà$Œev%ÚŒTò>ùQäÛ°¾øãqM†#í’åƒâycD<+‰Vü%‰‚Ï܇çH®u¨´2–ÙÓÐegPúõÏpä§Ó·=ñ ’§®I¥B¶[Ñd£ÍLE¥ÕPòÐg0M-EÁÛÔŠ.+ I£‰q[:ƒtˆmï¿C0 ¯wòRí˜,‘å§p(„c˜yt¶#=3{j*µšð—MÜ®Á¤No¦çÙƒÙlÆn·átºè陨@ÁéIæü+°—/àÀß¾MoÍÖh[û޵tÚDÅMßeÚm?dÏŸÖú{²ôÖleÏã÷á:VÇY&íå ™zãw VrWÜLÝK¿š°y Ͷ‘·âfòWÞAÈçbÿ“Žiœ¢KîÆQHÈç¢ú¯ßÀÙ¬xjŽFÿ6åÆ è¼O\‡±¬`¯ƒºÿ÷þš4;eß¼]^ùwßHíÃÿ/nüîw·`[4 ]n&†¢<<õ‰¯«íœˆå¿gýÖ„ËôΞ8+sÁ½·’zޢџ,~ùyQÖ¹v=ÍO>OØ?hu—m2?r!þÖΤBñd¯Éx‘vÁ9QñÜø‡gé|mC´M’Õ|æfRÎ]HÖ5ãØ¾×ùñ¸&ÉH½àò?YíV<÷÷M=ÿ”p˜Æßþ®· *­†ÂÏÝU`,u IDAT†íœ9Üs Õ÷ÿ€'Þ¸vìéHY±€ÂûnG›‘Jæu«0M-¦gÃ6šŸ|Ž@w•¿üºÜL‚®A}bÎÁ§)]m“*ž´Z=@Rß霎ˆÿ•$©0™Çž’f ¸"ŠeY–1›MØí‰}ßg6sWÐsxKŒx çиێ ÒèÉ]zÄÎE ‡p;Ìñâ92¿Ítíܼl%s'tã!£ˆ´çÐðæ1â àê¦æ…ˆXјRȘsɰãy:¨þë×ãüº›7<‹·³`Ä1ƛ‹>…¤Ö°ÿ/ÑU½¡ï[(‰€¶h-ѿϖ¢)þ!¾­šT[L›¡´€” høÍÓ1B"¢öè¯ÿ €ev%úÂøÜ×}[÷ê)¶¥‰¿Æ)EQÿç®w†_í8d‹‰ì/ïŸ×^ÿ¿¡ìuÐüäót¼š8i<®É¸ IÑsé}GŒxP‚!ÿLÄ7\’ÈüÈE ‡k’Œ”sRðé›@’Ås(±x–d59·\ @ûKëbÄ3@Øàèož&Øë@¶YHY9º']^™«Ï§ûÝ-Ôÿê‰8_ùkp•@O6{*[ :½¿ÏG{kó°þÆ~DÌÊ#øUMa7T –”´Œèß]ícÞÿLdÀêl·Û0›M1Ûg†´ˆEÐÝr8iog#ÆÌbL٣˻;Qx:"7RéôØK©XDÜË:ö¼™°»¥wk-ƬRR§-§åƒç’Ž×sxSw…®ê÷È]v:{S ש1ŠÈ ëžÀÝ:ÄgSQP”0wâ ;‹fÐOÚúİâÀ<3r]Ý}Ãú<`åºv¬y'.«Ë/ý(®ŸÐàR©¨¯=€ÇíB­–ÉÎ-ÀbK¡tj²F›0}\ë±F?–)Óg³uã[q}ŠJ+¨œ1/ú:˜ Šy8r Š£è{wŒí¤>d¤¥¥àõ&nJÆ€;GDT 7—³Çѽ¤T,ÁV:I¥ŽKɦÖ0åLÀÙThˆqŘUBö¢«±•ÍGkI›ðãMZSd¹ü4½ó¶²ùèÓò)¾ì^ê×þ.Z$E¥ÑS²úóÈ J(H˦ásØž,Ù‹¯¡èâ»@’¸{騽_O á@äaÎR4{Ù‚ Ãx1 dÃ#Äv(CÚUœ€€V†TTktÃô_®±g?è h‹ÖlÚ¹èf³9ê¾ÑÒòÿÙ»ï踮jñãß{§wz¯Vq/qI\R ’!H¨¡&xG¨ÉK€ÇPC „:’'ÁN\ânY²-[’U­®éõÞß#5ÖŒ4#Ë–-ŸÏZ^Ëš[tîHšÙsî>{Ÿô%ÄÛ{œ#¿û,5¯ÿù«®'{Ñ&ü}m¨JsAZ“%âøß¿žV#“™²•/‰ÏC‡ÿÍñG¿1©Ûa1\4ôxÐ?]ÍliBÀñÏ,µLÖN8Çé"‚÷é Œ [Åœ5R9Ÿ,µ•ñÿ:z¶E=>´®—1¸ùÅiÏ™"P~aoxú¼l̵±TI"k¬úƹNßâ‹ecæë”âç˜ÅçälŒ7¥‘uÚ)Ó«&Í‘$&gã9IW Dë—Ä‚/|CQUŸ|?Ç?ÿ½i?HŽ—“ õ Òõó¿Lû}Ò™ÕΔ 3‰„ì'7¿ˆ,gnÊýŽÞOwGeU °Ù³$k„“­Çp»FX»ñ•Œ%4JIE’$.»ü*œ9ù„BAvlýWÆM^.FÁ`,èH7ãÌàY¤n\zÆ|E|.üƒ³‹AÖr1xè9zw>J`èÜ–Ë[ñj$”Hˆû¢otõcÊ«@oÏ›r?ÃØöHÀƒ™Ùë“Þ~úu5ì½°×/ŒGèpwPf+£ÁY7×Ã9/ìkc]v•PxR™³ðàZ‡ %Áµ»ñ¬¾O°ëþ˜ªËȺb%¾cíXê*Ñå8Q‚!Fw¦nØ3[ÆÓ ©ßë§=Ç,>'g#<0ö·$Ièœö”Ý(u¹§×}%›áŸçäL—‡_üµ÷m–ª{þ“–Ï|‡`Oêúøãe£aÎž× õÊh¼lœ^?õíE{”¦»Ù¹u3;þý ûwáv`4™)(ŠÍJ÷õ¦—‚±ì²õ—UDØþÂ?q».ì7–¹`µZÉÍ噊àùR%Q{ó=èí¹´=õ‡ñqö|ëíìùÆ[8ø£ÒöÔÎyð `tÆšk†º‰gÞ%4¾HMš¥—êø‡õÌV.{»b·TÎ"´æÔe7­%±FS51™Ž¥8ÖMV‰„+b$qúù™ÁJìY²·?v­«òW¢•çhNJ=?σui]¬¹0üüÎI­¶Ýcå ­‹j4gÿ;;<–‹šuÅʄŃ£;öOÛæ{6Œ§h¬æX”˜íçd¦|-§š'ÜE8Óø¶ˆËCàdϤí³ñœ$êäÄ—~ˆâ¢µ[©¾ïƒIs°Ç?¯Z»cYѬ#"€žY{‘ ÍpV©~ñJdYFUÚOLŸë³tååTÖÔDxé…2,ÊÖ%¥ÕÆ~."x¾téÌvLy±õh`îÚÛGƒ±õzkvÊ}Ly•HS³ã3×z[òóèÌœõëÓ×ø¢>­ÑЬK¯² Ààáçcÿ‘$òV\—t[ùŒce‡š¦nlt&ÏÕèMä,Žå3ºÚö£F§N¿ŠŽu4L33~.=ÞëØ–eÈbSñ†9ƒŠ“gÖe§µ[±¯YzVßÃPR@ùïŒ}¿`(i ³‘m{@UÑ:lä¼ê쟋᭻Q]N–ú*²ÎSõq£;÷£Fb ‘ ßòS@I±m¶Ÿ“™ò6×4ÎyÅåI÷јMñF+£;$Mó˜ç$[­_5EŸ—Mõ}Œç:ŸÉsèh¼IKá-×gô}f‹ gÀ‘åÀçÉP÷îx„öþxÚk‘µz–Üþ½ø˜ü}mÝèÌLyDƒ^ö?ôþIi4æÂjjßtG’¬%ð[ø8Ü´•“›6í÷Wi¯àû×|¬ãG‡æÏǦ_Ô4Ñ‚/ü7–új\{iýÊô×}&ÙDÃ÷Å‚&UÅ}à‘†ÒÂx éÁÍ/Qpó« v÷ÑüÑÉmÏ~÷3è§Ék ööÓùÃßã9œºQ‘l2Põ‰÷ÇëôFÜ^ü­±ßcIÁé2hÝ}ùø—§]Ü5Þby\ÿcÏÒýë¿OqDŒmi=åygÂc³I§C&6Ähºóÿ%M ‘4r¬Åõxp«ª:zˆBèóœñàQñiþØ—â¹¹ÍÆs¢ËÉbÑ>?íuk{àaFw<ãb$*>úîø,sxpk'’N‹¥¾ Ùh@DiÿÎ/Ý‘: ëlŸsM9µ_þ8ÇîùFÒƒ¹×_EÉ{nÀµ§‘¶Nú¼½ýFò_[S¦†ÃøZN¢‚èr³1–€$¡†ÃùÄW öLŽ¡Ê>ø6¬‹j´ŽX©¿Ð©AÆ;‘¶|îÁ”ùâ«Õú¿)Ÿ%ÀlŽEô¡ÐŹ`­´¼«Ý×㦳ýø”ûZí6^sÕõü>ÖdÔWŽÞ–ƒ)·,–>¡* zž–¿}àðä¼Éq…k_Öd£oÏ?Øÿ/ìË0å•cp"ktDC~º·þžŽg™Þó¬DjÞŠ1»SN):«cvñXÍmWÛ\mû'ÐGÅëß„ÖdC›1–µz´&Z“ÀPÃG^Jk #ÁQ‚Ñ « .ciÎv÷ía00uÍ쉲_q9ú\'Áž~F¶NnA?5Á{ø8¶%µh¬ …¹˜*KÐÚ,Œl}™¶oüÅ$ûêµDÝ^žšÜà &Íà)þ ¡¾AÜŽpêOOÑý‹¿ŽSŒ%edÛ”pci:‡ CA.†‚\4f#áaÿxŽŽþ%8}„Щòn¸ i,E¯óá?ÆoÝOÅXVHÎ+×# ñã3¿’,%<. ôýí_ñÔ„Ä RÝuðÀ0†Â\´Z‡ }N“Ådèùœ|ðW„SÔéžçDc6’÷Úk¦½îq}ÿWÒŠ®]P‚aLU¥è²ìŠó1æ"iµx›Žsò¡ßàÞ?Mü³|NtÙr®¥ž m~)ébE_K;²N‡¥¡CQ>ú¼lF_>4i?ÏÁ#;Oa(ÊC—“…>/CQZ‡%føß/sòÁ_% žr¯Ûˆ¹¦ÙxzÂEc5Çÿ <9¹©Ê¸y5½hÙjjê'<&I2’$¡Žµdçõ¸yöÉ¿&ìk0šX»ñ•dçÄn‰†BA\ÃCD¢a¬VV{ìS•ÏëfÛ–§¦ìXXRNyåò‹JÑŒåL{=n÷í¤§kêYk«ÍÁ+oHÿÖ‡kt˜-O¥.¾13ÐÂl“d-µo¼gÃzTUaÏ7ßJÄ—º±ÀÙÒ;ò1ç•ÁÑ>üý‰ÿƒ½#?ž®‘|ЖÂô¶\"¾S'Îjq"ÄêbÇ‚hÑ{B“i-¬Fk²öŽêN«lÝø tÏö¿pò™‡‘d –¢ZtV'¿oOK¼d^¦t–,ÌÕÈ:ï(¾¾ÖŒ£œ ‰ÿYs7W—^ÉP`˜<ÿQNùRW87ƒ0U– ËÉ"êõhë"ꟻ”6I#£/Ì‹5>‘$BýC»Ò/Iz!Òçe£ÏËF6»´weT"íByN$9ÖiPçt ‚O $=OÇÙ>'³eâ8"#nüm]ç´ãì¼*c'É2²œ¼X¶$IHZ­&Û/ð³uóTÖ4PS¿‹ÕNnÁéÕápˆÎ¶šî™¶„\QIE¥•¨ªB_O'­Ç›9ÕÝ‘Vɺ ÈãuŸ»7A¸X˜òÊqÖ]NÄç¢ooòü=U‰Ðûòc8Ö#I2Fgžs@‡Fû’Î莋}SÏªŠ·§oOê[ç™RÂÜ''Ïè¤}lÇ᳃ªDg­#dØ;Âè‰sÛ™n***_Ýý™5«y`ãWùäÖ»9å;Á‘ªâo팧Ì55ªì:uÑÍ…ú‡¦l= å9QeÖ~WÎö9™-ç{ó*€nÜ·“Æ};ÏꪪÒÚÒDkK›³ÙŠ,Ëü>ÜîQ”4?Í´of ¯‡Þî¡̪ux\£üýéçß ‚z[.e¯xÑŸƒÏNY‡Ø”sº¹QÈ5;-i!¢DøüŽ/ð?kîf}Ñ|ëʯsÇæà _Ø aAÈܼ  g›×íšñìîð`ÿ%¹èO抧«%D£7Qó†OÑ¹å—ø:™¸NAoÏ%gñÕ”^[\4rl!÷éœÎÒ«ßIþªÌK"ùzOÐüÛûÎú„‹_0âó;îçÎåbÀ? ‚gA˜§D-¼ úhæ'TÝp'Ù ÈnØ€N¨}lI¨}ìî8ÌñG¿™pÁŒÎâÌø{OÕ\D¸ô(ªÂƒû¾7×Ãá´ óFßî'ðõ§èŠ7b+[ŒÎêD?^ÎNU ¹ðt53Øøa÷ÜçK ‚ œyU…C˜DálȲÁªJ4䟶³pnÉZý”¹é‚  1-¼¥DÃ(¾ékÍ ç‡žA˜/D+oAAAÈ€ AA!"€AA„ ˆZAA2 hAAAÈ€ Aa–I’„„4×Ãf‰FÒÌõ„ Œ Aa4z>·î3ü×òÌõP„Y`ÖšøöUßà–Ú7ÍõP„ ˆ¨-‚ ³Äª³rÿúϳ8{íî“Xt¼aï\K8 å¶r*lå4,y¹¦\~pàG¨LÙƒN¸ˆhAA˜F‘/o¸ŸÅÙ‹h9ÎÇ^ø¤žçæá#|jë=xÃ^ÞPóz>¸ì޹’p´ ‚ œ%¤á/ÿ Îz:<Üý⽸B®™ŸÏdD6ègq„ÂÙh>Â}/}Ž`4Èjnâmõo™ë! sL¤p‚ ÂYzç·sYþ*¼a/Ÿyé Žf|ûeKÈ~Å娖Ö! Dý<‡Ž1ø¯m¸÷6¥<¶òïCë°¦õ}ºñ¾ã'3ߥîÐ`#ßÜûîYý)Þ½ð6 6r`àà\K˜#"€>OdYƒª*¨êÌò¦$IB«Ó ‡f|A.mŽšËÈ_q£…æÿ»w®‡sÖ ×Þ„½j“œÜü³9Gƒ³ž·ÖÝ Àƒû¢ËÓ•Ññ‹™Š»ÞmYýäm&#Ž5Kq¬YÊ讃t|ï7DýIû™T Ëv¤õý¢¾ÉÇ éy¶c —å­äÕ¯âS—}‚;6_Ä?×ÃæÀ¼ Í+ —­Æl¶04ØOã¾Ócw8Y¾z}Zçß½ýy|^OÊíÙ¹TÕ.$¯ ƒÁˆªªø¼z:Û8Út€p(8åùõzÕu‹)*­ÀfÏB’$TUÁ5:BgûqN;Œ¦5VAkqÙ‹6ñÏ<µàBâ¬[‡½jCÑðœŽãŽ%ïC–d äÙŽ-«±˜XpÿG1– F†¶lgèÙí:{‘dSU Ù×\ŽsÓj,õUh³D»RÀéü¹)¿g¨o £1 ‰~ÜøS6¯§ÀœÏëk^ÏïŽü~®‡$Ìy@K’LMýb¯D£]b$IëXNOvnÁ´û)ŠBÀïK¹}ѲÕÔ.\ÿ:  Ñj±Xm,hXJiE Û¶<‰Çü6_qY+×nD«Õ ˆD˜-YÙ8²²©¨®ãÅçžÂï‹T!SæüJòV¼šŽ-¿D OýaVRipÖ³4w)¿A UöŸo‰Ïá0'¾üc<‡Ž&l÷4¶àilaøß/qì:5åùÂÃ.Üd<!}£ÁQþÑöo®}#7UßÈŸý…°2·â„óoÞÐÙ9ù,_³»Ã Ä]YžÙZÉçŸy”H$ù…ª((Š’t[uÝ¢xðÜÒ|cÍ H’D^A1+×nÂh2³nÓµlyê‘Iç‘e™ºEËÑjut¶çÈá}x\£ñm5 ,ZvV›ƒu›^ÅsOÿmF×'ó™¬Õc«XŠÁž‡ÆdCÖhÑšlèíyXŠ`È*ÀÝq˜¡¦­çlz{.¦œ2tV'ƒ™HÀƒ¯÷8þŽiÕšìh &”H˜°g(Å>643j4BÈ=8i»Á‘R¬¡‡Æh‰=(ɲ&O„\¨Êôwµô¶ÌÕèíy(a?¾þv|½'Ò:NÒh ¹Q£±I kI¦ü ”pˆÀ`'Þžc)—5:t¶ìø×’6¶ÈNÖ']ªD ¹’Ï´žyT•àhßéí:#Žªå訑þ“¸;'=× U×ÐçëcOßÞ)®~2Ë—¯ ÷ON ž'rïoÎèÜ™’tZtYö„ÇBý§çdƒë’:ôùÙ¨¡0ÎSxœ˜õsœIçt`ª,A—ëD  tôào›>EFë°!ëuñ¯£^?QßéT cY!æ•ÈF‘‘Q<‡uO{ÞqOŽÐÙF'W­ã…®s÷r&ÑLŲµ8 K‰ œl¡«y?L“Þiqæ¢3ã_ûÝ£½§¯9¯²Ž’…+ЛÌxûhß¿ïHâkŠÁbÃd;*ðŸþ;³åP±|æ¬l‚7M{ì˜úgœWQKNy –¬ü®a:ïÅÕß3íóà((A’N7+rö ‡€ØDjÙÒÕäUÖ!I#½´í}‘È4wþ31oh­VÇâk©¬‰åü>íÝAiE …%å3:§×í"<öÃH—F«eá’n³IÞ IDATËh;ÞLãþ]ñmªªÒ×ÛÅŽ­ÿâªWÝˆÕæ ¢ºŽÖ–ÄFEQxéù§É+(¦³ýø¤m­Ç øY³þYÙ•rª§sF×(ó¤ÑR²ñ­®» Á%W¾Âµ7ìdÿ÷oŸ´}Ù=Œ¬I¼F­ÑÊŠÿbÒ¾/õf¢¡ÔùœZ“Šëþ“œÅW!ɉÙü´ýã{¸Ú¤<~á»Àè,¢éWŸ" PóúcÊ«HØÇÝy˜ÿ¡îIÇ› kXüÞoMzÆ¡Ÿ&Î’güÚs_IÕ Fc<½ /ôòò×’7ÑX‘»úŸ|>£cg›©²”Ú/Þ•ðØÁÛ>…’µ~%¥·ßŠÆbŠo‹úüz÷ݳ~ŽqZ›…âwßLÖúUHšÄI°`×):þ#žÆ–”×S~ç;°-oˆÝÿغý74¹ ÛÊE ûwÿòúŸx.åùÎÔáé¤××K¡¹y+Î[½þ-`ÓÛïDo¶$<>ÔÕÆ?¾}­{¶¥<öõŸþ5k®Œ½ýO?á™~ £ÕÎîý Ö^°ÿ?¿ÿvü%qmÁêßÁ+Þÿ©ø×=GòðoIâÚÛïfݛދ¬9ZÙö ülò²… sÝ}–òek'm;¾ëyþñíûéMÛü×/ŸE£;]©æ÷÷¾cÛŸ¥ º7~ö!rʪöÿÉþ½-É?Ïļ  E!¯ UU8qô0͇ö‰„)­¨9¯ã(*.G«‹½Y8–|ÅôÈÐ}½äæRVY;)€ü“‚ç‰z:ÛˆF#h4ZÎ@1bÌ¥J’dênù,Y Ö ª }{ŸÂÛ}U‰b-®'oåu†º9ñØ·ðt¦®h0¢áÆì">ž®f‚£ýhôFL¹åXŠk±–Գ趯rè'N˜ýœmîöƒHc´Á‘!«U‰NšQ$ÉD|.¢!†¬B"þäk]rM9Ybw2Ž §ž=NFÒÈñEƒ£»¡†ÓK3›æQeÏöˆD¸F‡çl[ìö•âžbƒý=äæ’•ƒ¬Ñd¼PUUEA£!e*É¥Æjµ’•åÀãñ2222×Ãæ@þeÿAÖ‚5ùÝg=¾;¾­ß?>¶“ú·ü/ ßñ%ýì®)gÏÖèñÝzøÃx{ŽÃ3“Y ÖPwëçКìoz+­çœcbµ’Mo¥ôêÛˆ½4ýêSS5YÅ«ïÀ”WN4è¥ù7÷âé>g«³8ixû1TQ}ã]ìèv¢ÁÔk3 ×Ý„§«™£üBBjJñ†[){Å»1:‹(Xs#]ÿþmÂqþþ“ ã^øŽ/c¯Z«ýÇþüÅ´¯Å?ÐAÓoî`ù?Ãè,ÂZ\GÅ«n'ðÐúøƒ 5¿HÎâ+YpóÝ“ùq%–Ów:3¬¼aª*‹×yö_%å]§8þ…‡XøÝÏ /Èż œâÛÞ@Ôë§óÇ`tç²Ö¯¤â¿ß•4àœs¿ë K ‰úüœ¸ÿøZN8Ò:lTß÷!LÅ”}èm4ÿ÷R3Æõüö1œ›VSþáw¢ÏË&ÿ×a©«ddÛºõáa ß¾Cq>oæôøÏ¼ØR”ñ±™Zyý-¬¸þT%Êc_¿›ýOÿ9¾Mk0rÓÝß`á•7pã'¿ÆC·]CÐ7ùwöÙ‡¿ÀÒkoâ¦{¾EVa)›Þq'¥‹VѸå1þùƒûñ öñ¡_l&§¬’à·qËc4ny {^!ÿýû—0Ù4l¼Žµ7¿‡Þ–F{àÓô¶4rÃ]_ä²×¾-i®Ñé¹éžob0[ém9Ìoï~WBHqÃrÞñÀo°åòw}‰ßÞý®¤ÏÉïî‰ì¯ýÄWXyý­d–ñºO~“ÝÁ–Ÿ}ƒí~£ÅÆ]ŠH6–³1¯© öÍið  ×Çò‹RåNó¸c«à%IÆbµO¹o2›ÝØ­‹¾és….Z­­V‹Õj!++k®‡#̼•×0ÒòrBð¶þß÷ˆOßV•(Aÿì\˜73ÐçŠ#+›Ã‰Áh$ Òª{Êê¡Pì¦&¯rb ;.³nSZ­Ž«7ÐÝÙÆÈ(IÄg³²X­–„Ç„Kƒ)'6#èëMìÄœ_‰¥°:å>çƒ 6먳\øöœõWÄnñz6é>¾ÞøNÀ\PMöÂôîx$åùFZv¦HQj~‘â ·`È*@gqöžŸI­ÉFÇ–_â;5aÁ“ª¢ª a_ò@¦;'íd–:¦Ì*IÒΪïý Ú,ۤǽͭtýôO)ÏëÜ´ç¦ÕI·EÜ^ß÷?iQc5Óó»Çñ·O˜]WTTE!âN/ÉôöÕ±Š&ª¢0¼õå¤çô·uáoïÂTQ‚ãòåiå.Ë=·—®Ÿ&ŸjTAU¢žÌƒ«À„Ÿ¹]oßF=h“Ù„N§# †ˆ¦q÷¹zõ&LöØ®—ýuÒ}B~M/üƒÕ¯¿Úu×°ë‘_N{^ÑŒotˆ'¿óÙ„Ç•hU‰¦ò`²eñû{ߟ0ë­F£±s¸&¿×oˆ}8î=vˆ¾Öä©Oÿõ7^ý¡û$™…W¾fR`žj'voeï§«á¨ªŠªDñO»È2S"€žÂ¦k_‹Ížøæ¦ª*­Çshß.TurêÄø ¸¬Ñ`w8SΈgeçÆÿ?aiJ…Ååè ¬6e• 0šÌtw´²gÇ¿3¸¢‹ÁëÆe4§Ù3&0"ˆ¾)‘²Î˜tñà¸ñmêy®¡®1˜ÑšìÈ:HÚñEj3¬t>ÙÊc3@Á¡n¢Ô†·§sA5–ÂšØ Û Þ°|}­ñÿ›òÊÏ[ ù9µóï ~ÁÃ/¤ }ÿv:ïø=ùàgyòÁÏ&Ý¿|Iì^÷ÑÔ]ý®aFz;q•ST·4í±lý¿ï%|íàþW-HûøLˆz ²,Ó~â~ŸFKaq6‡“êºÅhuzöžêé$¡Óé©]´œÝ/=7iŸŠêz–¬Š O_?ò²+®ŠÏjWâh:´—htnžœk999™ß^OçˆÕbaá¥À}²gý8ªW!ÉšI%Ù4z–¢Z<]ç¶,€¹ ŠÂµ7ᨹ ½-眿sEo‰•|KV&o¢;v7LÖÐ,)s‡§öœ(âe÷΃á#Û§\D™Œ;t:ð°è2kÔú.¤¬Ÿ|²ó'LhÍ]üÔgÚ²ƒŽþ.£±¤2úòAÿÙ•ýÊôÚ±2xá¡©'>ÂCc¥] z4fSZ)%#ÿN>£=SÖ w ÒIáqd9ÐhNÝ6»-­Ú–[¨ªÑé¸þ#ŸO¹_vI›‰MסÍg_÷à¿Ò?‡ÁbC;VJÏ=Ð;å¾îS8‹Ê±æLß›ÀÕßCûéæÍ@Ÿax¨Ÿ>öE!xFÐÖtp7+Öl¤¼ª–òªZÚOah 1gH‰FiÜ·‹k6PZ^V«£ýx3~¿³ÙJyU-…%åxÜ£XÇj)NW*O£ÕÒvü’$a2YÈÎͧªvåUuÜ»ƒö¢h~*Z­6í&:Âůë…ÿÃQsÆœR*¯ÿ/Úÿù£x“Yg¤êµÿÖdCFèÝynë§®{¯º$‰°o”ƒ[Žô¢„cælKɪI~«ýB3È*Ó¬íP'l—u˜A­Nè*¨Ñ2>~¦ÂÞäu¶§â Ÿ mzë{N6qÖ>ùØ@g/L˜lœIŽîÙšv¶ûœc¼ÌÝtUI&n—õ:¢id`„gáz&²èca%L0zîš1Çb“ÝÉê×ß6íþã äÒáêŸñ¸frã„5_ÑÐÔ±Otìµ{bíê©x‡f=Mc*"€>ƒ¢();û©ªÊÁ=Û))¯B£ÑRZQ3)€h?qƒÑHÃ’U—QX\–pŽ£‡÷¡ª*õ‹WF¦í$DÚ˲LuÝb/_Ê5‡Ctw´Nq†‹×à`lÆ«·wêî[Y­ÖxúFoï)@_B¼½Ç9ò»ÏRóúO¿êz²mÂß׆ªD1T¡5ÙQ"!Žÿýëi52™)[ù’xð;_¤&ÍÒBÃø‡õ4V.Oàí:JVÍjŒÎ"´f;_ò|OkIlñÒTML¦c)Ž5Q"¡ÄŠIœ~~2»žÙ´·?e¶2Vå¯D+k‰(éßñyq/¹×mÄ\W‰¡0—`¯¨¨äkiǶb!†‚\´6KÊjæºJ<‡R·~?—œõØõ±t„=}ûÒ>Îïóã'³tœ»·qù›oÇœ•M~U]ÊÊUUè9zˆòek)Y˜º„§-§{^,ÀnÛ÷Òù^F.üåß ñ6•¡Î*Õ/^‰,˨ªBû‰™ÿ!D&ä€MW6ïR0~[LÏ—.ÙŽ)¯€h`nnëDƒ±7H½5;å>¦¼J¤)‚Ùñ™k½-ùytfÎúõik|QŸÖhEÖ¥—W0xx¬Í´$‘·"ù }[ùŒce‡š¦nklt'}\£7‘³x ÂÕiIGÇ:¦™?—o}€,C›Š7dtìà?·¢F$Y¦ä}oFº*²œk#/Ž¥ÁHÙ¯¸<é>–…5Šbi/#Ûgþaíl¼®úµœòõ±ëÔì.NñÕs:–Ù°ÿ©?ÇÛsß|ïwÈ)Mü;mØxßq'+³7Ú×}Þǘ©°°pÊÜñ2bOæ«©/ë6^KaI9}½]¼ôüÓÓîo±ÚQ”hÊ…}…Åe¬Ûô*vnÛLOg{ÒýΤÑhX²r•5±7¨¾^|î餵¤JʪÀçM¤ku:^yý1šÌôvdÇÖ¥5Ž‹MUU쫵u~æx ³ÏQsuoþL¬ D ª¥wÇß8¹ù§çdÕ¶Æ`féaÈŠ=#-»ð÷·£1XpÖ­CgÍæÔË‘»ìZ4z®öƒýÃç'µ¿Ö,,¿ó§±[UmÝKØ3Œ)· Kqaï0}{Ÿ¦dã[ v²ÿû·O;¶ê?FÞòØkXÈÕ YgÄ”[ŠÖh£ñçKZâOgv°ð]`Ê¥ŸùúÚu£µfa-i@’dB®š~sO¼›à™ÆÛf‡=Ãè¬N¢!?Þžc (XJêãuïŽGhÿç§½Y«gÉíß‹Éß×FÐ=€ÎìÀ”WA4èeÿCFc.¬¦öM÷`pä!ÉZ¢á±…ÃM[9¹ùgÓ~ÿq•ö ¾ÍwÑÉ:~tèaþ|ì/i‹$Qö·&tÔ õ q{ÑeÙÑe;NÐQUúŽî_O® ³è‡ÿ/¶oŸÙFçOþ˜ð˜©²„Ê¿ˆuI”4¢^¼ÁÈÈöýôüߣSžw6αª$ >ÿ %±òe“Ý{Ð:l˜k+d™ðàÇ¿ðÁîÉ ùÊ>ø6¬‹j´ŽØdSèÔ ã&[>÷`¼^¦¾ºáK¬Ê_I»û$ÚòaBÑ™åûgê•wÜÍú[ÿ€H(H÷‘„ü^ùÅäVÔ"I‘P½ÿ5 uµM:þuŸüËÖ¢7Y°Œ­áî9 üå]o™¶´Üª×¾õ·Þ$kÈ*,bõ©ÃcwÝþõã¯Ðü罹½–ÒE«xû×~…ÞdA‰FèjÚ‹wdˆœÒ*ò*c¹åíû·óû{ßG(ÅDã~öO´:=æ¬ f+Ñp×X¾¸«¿—_}ì-ÓŽãlˆè ¬v®¾€CûvÒÝÑš°À/¯ ˜•kc·G†èíš~å­V«£rA5u‹1šb ºN¶²gÇ )ƒçêÚE,]u9Á€ŸÆý»è:yeB|‹ÕΪ˯Äh2£(QŽ4¦Ÿ%óÝèñÝì{ð]8j.Cgu"ɱº«’¬EoË!«v-z{.EW¼I£¥ýéÎú¢AM¿ú4Õ¯» {Õ ²¬!kÁ‚#§8þ÷¯3p`3®¶T¿î.ŒÙÅDC“ß$¢A/G÷9jnþ4FgŽêXýx%fàÀfNnþ)¦ÜrJ6¦ÿFÑúăD|®øLüÄÙxO÷$Mò·…°o”ÿøe¯|¹K_9¿s~%û@2ph [~Ah4y@3ÑÀ¡g vQvÍ»±W,;}½!?=/ý™®Ò«c¬DB4ýúÓTÝðaœu—cʯÄSOg3ƒeR-kô‰ ˜4F+š±æ6Z‹“L´¹ÚùYã/øÏ¥·óî…·qpà G†ÓLÏSU:~ð[Üûš(xÓuËŠÐçç Ï?]7<âö2²õež|áœåIK:ú‚Ä…ñ‹)^V.YWÄsq€ˆËCËg¿CÑÛ^‡óÊÕË‹1–ÇÒ~Ô¨Âð¿_¦÷wH]CY—í˜4}ÁéçTÒdÖøfÜ-µobUþJ"J„¯¾üÀy ž6ÿø+ô=Ȇ·~ˆÂ‹(_º&¾-ðÑøÜlûí÷“ÏË+vW$<æ,*ÿ?òw&›cÒ9¬9§ïšÌé•sì<¼‡Ÿøf^ý¡ÏP¹reKN_KÀãb÷£¿á…_?H$”:U6»¸Í„NÎ>>69ÅëØlšW3Ћ–­¦¦>ñv$ÉH’kç8!`õzÜ<ûä_ö5M¬ÝøJ²Ç~B¡ ®á!"Ñ0V««=öéÞçu³mËS)g‡ KÊ)¯\@~Q)š±¤×ã¦qßNzº¦žµ6M\qÕu8²byápˆÑáA"á0&³{VöXnP”=;_ ëäü3ÐÂl“d-µo¼gÃzTUaÏ7ßšrQÜlÐ;ò1ç•ÁÑ>üý‰ÿƒ½#?ž®‘|ЖÂô¶\"¾S'Îjq"Äêb[Š 5Ùˆ†üøûÛš˜L{la5Z“°w„ÀPwZeëÆg {¶ÿ…“Ï<Œ$k°Õ¢³:‰øÝx{Zâ%ó2¥³da.¨FÖˆxGñõµfÜålHHüÏš»¹ºôJ†Ã|äùrÊ7ý‡‰3éó²Ñæ¢1ˆú„† ºtÊ=¦ÊR4V3—›`OB íóiCÑ|nÝg$‰oï}'Úžœ“q8 JÈ*,Eo²àîçÔñ&”‹´d«Å™K^E-:£ Wƒ­D.‚¢ójZ’ed9ù§JI’&´ÝL¶_0àgëæ'¨¬i ¦~1«Ü‚Ó³ápˆÎ¶šî™¶ùIQIE¥•¨ªB_O'­Ç›9ÕÝ‘Vɺ`ÀÏ Ï§Á3Àè©®xñÅÎ;<k‚r‘™Wtã¾ GfBUUZ[šhmiÂb³c6[‘e™€ß‡Û=švò¶ãÍ ôõÐÛÝAxŠ[©(ŠÂ‰£œ8ÚˆÉlÅbµ¡Õj üxÜ£iµÿ„K‰Þ–KÙ+ÞC4ägàà³SÖ!6åœnnr]|/ÜÂ…)¢DøüŽ/ð?kîf}Ñ|ëʯsÇæà ‹EÞ³¥9KøÜºûÐÈnü8ú§¹’p˜Wôlóº]xÝ3›™ìgxðì[dø}ü¾ ?…Fæ’§«%D£7Qó†OÑ¹å—ø:_4±&9‹¯¦ôêw0rl!÷`|{éÕï$Õõo_ï š{ßY_ƒpñ FC|~ÇýܹüC øDð<4áå¾Ý<×ù<›;¶Ìõp„YP³æ*^ÿé¯ÏèØï¾ýJÂA¿ A˜¢AíÏü„ªî$»aÙ PÂÁ µ- µÝ‡9þè7Ρ1˜Ñe¸€ båêaœ¢*<¸ï{s= a–„•0ŸyéçzÂ,Òê ñJ$™’ »³ô IDATäXU@ ‚0oôí~_ïqŠ®x#¶²Åè¬NôãåìT•kOW3ƒÏ3Ô´‰³Ó½ÛaðÐÔ5Œ“‰ÎpáÛ%GU‰ø\„Ý"ÇW„¹Ó~`?ûðÍ3:6<Ökb^UáæQ…C8²F‡l0ª ù§íl'œ[²V?enº ÂÅBÌ@ ‚0o)Ñ0ŠO,¸½PˆàY„ùB´òAA„ ˆZAA2 hAAAÈ€ AA!"€AA„ ˆZAA2 hAAAÈ€ AA!"€AA„ ˆZAA2 hAAAÈ€ ÏYÖ IÒYãlAAΞv®p.™-V.[Ùlah°ŸÆ};§=Æîp²|õú´Î¿{ûóø¼ž”Û³s ¨ª]H^A1ƒUUñy=ôt¶q´éáP0íkX¹v#ƒ‰æÆ½ ögt¬ ‚ ‚0;æe-I25õ‹iX¼6v‰‘H$­cu:=Ù¹Óî§( ¿/åöEËVS»pYüëP0€F«Åbµ± a)¥5lÛò$÷hZã2””W#ËÚŽIëAAaöÍ»:;'Ÿåk6`w8X +Ë3ËTyþ™G‰DÂI·©Š‚¢(I·U×-ŠÏ-Í9Ö|P0€$Iä³rí&Œ&3ë6]Ë–§Iyž‰*ke ~Ÿ‡Þî“3ºAAáìÍ›Z«Õ±xÅZ*kêø}Ú»ƒÒŠ KÊgtN¯ÛE8ÊèVËÂ%—Ðv¼™Æý»âÛTU¥¯·‹[ÿÅU¯º«ÍAEu­-ÍSžS–e*khmiFUÕ ¯DAA˜-óf¡¢(䡪 Çbó?þBWGëyGQq9Z€Çš’î324À@_/e•µÓž³¸¬ £ÉŒ¢D9yâèì VAAÈØ¼™V”({¶¿@$Æ5:&‹•áÁ~|^wZ×"‚ ‚ œ=@OaÓµ¯ÅfÏJxLUUZæÐ¾]¨êäêãé#²FƒÝáL™N’•ÿªþ(£‰â²*ZÏȧ®]¸ŒÂ’röîü7'[E-‚ ‚p¾Ì›E„ç‚,Ë´Ÿ8Bó¡=k:€{tI’¨®[ÌŠ5’sª§3^¹£vÑò¤ûTT×Ó°dUüëH8yºG¬tL(¤ó䉄mã AA„óKÌ@Ÿax¨Ÿ>öE!ð'lk:¸›k6R^UKyU-í'Ž04˜w­D£4îÛÅŠ5(-¯F«ÕÑ~¼¿ß‡Ùl¥¼ª–Â’r<îQ¬6@ÒRy±Òu±’|í'ŽNZd¨"íCAA8wD}EQðû¼I·©ªÊÁ=Û))¯B£ÑRZQ3)€h?qƒÑHÃ’U—QX\–pŽ£‡÷¡ª*õ‹WF’~¿ñÒuªªÒ–dñ NÌ@ ‚ ‚ Ì @g( 3<ØOn~YÎÜ”û=¼ŸîŽ6ʪ`³g!I2×'[áv°vã+JÚ¥º.VºîTO>¯gÒv­VÌ@ ‚ ‚ Ì@Ï@h¬³^o˜r?{”¦»'=n4™)(ŠÍJ÷õvNÚn±ÙqfÇJ×YmÖ_ýšIûè ±ï½ a)¥5DÂ!vn{6³ AA2&è5±§-N³grõ‹W"Ë2ªªÐž¤³ VsúÇbµ9â¹ÒÉØìYØìY„B3‹ ‚ ‚@Ï€#Ë €Ï“yù¸ª ñÅÇš&­)íqòÜÓ›ò¯F£aÉÊuTÖ40Ð×Có¡½I÷F£ŒŽ M}±€ÙïóN¿¯ ‚ ‚0kD=Õî`ÃÕ×phßNº;Zfvó ŠY¹v#CôvuL{N­VGå‚jêc4™è:ÙÊž/$mÄ’.1ã,‚ ‚0æU½hÙjjê'<&I±^1yżîÍïŠ?îõ¸yöÉ¿&ì…ðùñ®!Â%­æŠb^ñ¡å”,ə롂 \tôf-&‡a®‡qÞiçz‚ sEkÐP¹ºQ#f¡A˜–#¹v|#AÚ\¨Š:×CJ[n¥5·Ö!kdŽ<ßI˶î¹Òy#Þ1A¸dU^ žý®æz8‚ \‚l9&–¼¦’µo©Goº¸æ5 ëÈšX(Y¼èÒº‹'hA.ILÕÚBNl﹨f}A.½GGP¢ =MCs<šóëâú¨s“e ªª ªâMZ.«òÑ›µ½aNîëŸëá\4ÌYê6•`Ê2pàñV¼Ã¹’ Liéõ•˜³ tì ûðà\g^heów÷¡Õkðçz8çÕ¼ Í+ —­Æl¶04ØOã¾Ócw8Y¾z}Zçß½ýy|^OÊíÙ¹TÕ.$¯ ƒÁˆªªø¼z:Û8Út€phê_¶%+×áÌÎvý§zh>´'­1 ‚²F¢z]›}V"JÒýôYEÛ]DÃÉ÷»”mzJ–Æ^—d­4Ç£„é•­ÈC’$N™ë¡ÌK!_„/2×Ã8ïæe-I25õ‹iX¼6v‰‘Hz?\NOvnÁ´û)ŠBÀïK¹}ѲÕÔ.\ÿ:  Ñj±Xm,hXJiE Û¶<‰Ç=šòv‡3­±ôõ^:Iû‚0ÊVäc°êù"´ïéK¹Ÿ#ßÌš[êxö{ûð‡Cçkˆ‚ ÂlÞÐÙ9ù,_³»Ã Ä]YžYª÷óÏMÅñƒþm^¯—Öæö¼›×߆Áh&+· U§ÇmÓ餯7¼¥Z LžôñhM*œv75‡[ÆÝ×3J\‡só˜IŠ7g±(³Ösá` …ëÓÈ\šˆ|TÊ=¯×78õÎÅqÛ2%é(½>‹ØLcж¶ó=œÜYöå·èÖ\rÍAŸ—ÝSôÙÁÎÒZ5sKåÆ- nÎ!&Õ€â>Ðë òíZšÏtÍXÆ¢‹Q3ÿ¦lrÌ0¦ON»‹ÚíTíiÄæ’$È_—FΊd”yàñ.Î|XOíáð+%YK)Þ”áÿ»§ÙÆÇ¿® ds9eÉ‚§ål‡^:õ6†Q¨ä”Þ˜EÚü8¤1©½¦—“o\˜Ô¤/Òq{.ÃnH£pCZÀgç?iâÔ»uöeºd-M¤xs Uàõm8ÙNÛ…É¿ßóV§¿65¨[—ò5´×ô†k˜‡Æ¨ yü®žÒ:>ÛDé YâµAÛl]vªö4N˜!)Ò1™.sF@{<’Rðz=œ?[Ééò£¸\NÒ³òfµ)©™(”> Íùs¡oÒîÎvÚ[›‰OL&#»`B-˜<»]5 B#Iyk|ÖçšCÍ~«.@LŠ[× Nûˆuf´Õy¬€ÖT(µr¬m3¿dLбúÞL‰::j{é¸hE®‘R‹.FMîÊdºê­aE£%Ý@ÙÝECÖ7­UÝ Úœèc5$äšIÈ5³æ¾öþæTHmmí÷[ •j9¦$=M6\c,ø3=v«s²ÛCw£þN;^@gV—eBkR±ìνtŽ–s3/¢cRô¬¸»•N^誷Ò×nG®’cIÓ£5«É_›ŠL! 9É‘$Xr{>)%±t7ÙènèC&—HÈ5£5«™c6ƒŠ3‹€í5½Èä±F¿µqÞõYä¬HÚg¹Rî·ÏDJ­‚•Ÿ-œ¢ÇëõMÎlvÔz%‰ù1Äg›X}_ {w„¾×¢1®½:.ZýÇ Mm]vìÖ@‹jgýÌZ)²—'QzCƒ6'-g»p º1%ëI›ObeRí,¼%‡ŒE ô¶ôÓU߇Lá»Oô eŸ-âЋçh­žÀÖ—·cNÑcÉ0¢1¨°÷…6Æç˜ý“¸†“¡…kok€eZ¥¬JÆâÜ”$“ð¸<´×öb·:Që}+z‹†E[sq¸Âú¯OgL¦ËÐnŽ|ò!.—“ÞžÙ³<ŒEo4ùúãvc§mMÄ'&‡L.Çãžù%ϹŽÁ` &ÆL_Ÿîn,"&µ4½EƒËáæÂÁ@ëó’ÛóјT´VwÓXÞAKU÷ˆhö‚ÇíE¡–“RKêü8â²L´_èáÀÎÌx¿SJbqÚÝùsU@ª¨óû›Øô¥E(TrÒƇÐ2¹ÄâÛòP¨åô¶ôsàg^~1©zVÞSŒÆ¨bÁ–ìç3ÚB›adõ½%íü¬L Fã´»ØóL}íAKøº5«ï-AcTQrmÆŒ h…ZβOø³¹zéÝ ‚,!ÏLöò$Î}ܲŒE ~ñ\þfM€O¾L.±ðæÒÄ“¿&•ÖênºB¾ÆÊ+;ÐU\û׋Qi$YÈY‘DoK?Ç_;OoK? ¶d“¹$1`’Í6J¯Ïœ¢gÐæäà géi²ù·iŒ*V~®Cœ&ì½qm¬è ±b$ÛÆÍO®@’$j¶PshüU§h£‹QSr­o¼£¶—C/ ˜¸Çe›Xþ©‚ ÛÉXœ@Æ¢¼^/'^¿`••+d,º-—”âXnÍáýŸø€ÆÊNæ]›‰$“H)±ýþ 3|/ö4Ùè ã¢sðù³§/ŒgÑÖÜ Ïa41)zlÉF’$ºê­ùs5v눨—)dd,ŒÇœ¢+ž§;&ÓeNåîìh½¤â@¥Ò„õ¦Ïê[R$zƒiÆûu5 P(P( zbbb.uw—!ùkR¨=Òà¨Õ ‹Q#“K$ZXzg>×ÿÍŠ7û–½xýŸ-ÜšC|¶ Iò‰I¹ræF=n/‡^:”gÕÑï¢sÈÒ¦·hB›±8]ŒÏ§öØ«ÕA>Ý6Î~è³l&äšýÖå˙ޖþþ¯ý݃þJhúXÍŒ—Î_›ŠÆ¤ÂëõrèųA" ­º‡ƒÏŸ ùò–$(ܘ@ÓéΠ€VÛËÉ5 ö9AÂï»?ÞœC_ûû~wŠÞ–@_Òpâwºm˜“õ¤Í÷Ó8ñÚ…ñ `·:8öj5à»×Œ‰¡ïµéŽëåDÞêdrŸ…õØ+çƒúÛQÓKåîW2¹äwI¹°¿9È¥Áíòpâµ Ú|ÖÛôÁÙ»6'í|º#¥$tÁ“áß?ðY¬g’Ò}âÙnupàù³â|+~µGZ9ñú…°}î˜L—9% gsL,éYyä•’‘F;þËÅáðÍØ å¸ûNa§T†ö%R©")5ƒœüròKˆOLA’ÄåMww7ÝÝ=BD B’R‹!^‹Ûåáüþæ€m¶N;oÿø(Ç_;OÓéN\ƒnj¹ÿe"I)űÈ2ý.êO¶säÏUìþñÑI§¶Óê´˜Ì&´:-r¹|âFamë÷ å± ôø^<á„üð9ô´ØÂZ‹Ê;NQŸR;¥¾M—éŒK(úÚGÎQ­›ü"k$ýHª¼ÖVÝCw£m‚½ƒ‰I3øƒ2Ãùyº]‡&N‰y1þïã¡Ô*8ñÆ…Áæõøbq‘EÚFê<ß½3Ð3vɼ§iä>LÌ ö«‡éëL2Õû$±À÷j;ßÖmÂ9Ac|ŽÙ_¡°&Œ/¼Ëá¦ùôÐ}’úÝ7,Š-i´¦`Ý‘kF¡–ãõx,øÑFoÑ“êKzá@KD“ hÉX¦r}çŒ ÇL°þº­Mƒîõz¹p®’òcñzƒ_œÃp™\ŽÉl k•ß9T Ìh’Ó²ÈÊ+B.¼\6k/‡÷@WÇÜ-¡Vû,HMhëÚXìv;v»FƒÁà{@…;‡|V-€‹GÛp„ˆ,w؜ԟh§þD;’L".ËIJ;óQ¨}?¢ý݃}¥šžÆ>¦ZÉh4b2›P ¥Õìí饫+:«ençø/K†Ïÿs¬5p4Ž=ƒèbÔ˜‡ò^ÏÑ™B†ZçËV"I Ü&S@4úaLТ!S MlÆH@çx×gx›$“0&ê&•] £¶7ÈÝ£|W å»j&Ý¿©¶ŸãÄ¡ÜLFcmíǘ  iŽÆ¸ÎS½O´f5ƒï\¦3ˆÏö­Rö9ÇõïmõMLŒ‰Áyà üt9Ü(TrRJbƒ Öé¶ó=3š™$~T@r{„×8Zc2š©^_! ÇA&“Q{þ ý6ärÉ©Ír KQ(U=ðQÐ1-Mõ8”JóqxßûAûdåQ<©ÿo—s|wFK[kÖž.œN'F“™ÔôlôFk¯ÙÂo¿ŠµwnŠÄ¸8ßm·OÝÏrØíÕ"°ðj&© S’ÛÃùOš&Üßëñ—eôÝ9ÜÈ•2t1j qšËÉaŽ1X3Œ&cÔôxb^¡–û-–c—HÇb·:ÐŨQÆ_=‹&‘Ž‹J¯$gEÉ…ôqÚ 3ÑÑYú;#KÝ5l}öz ™.l˜Ñ–Kqr×§! Ä©¶1|ï´Ì¿1+ì~ÃÂyØ‚8šhŒëL1Õûdô³4Ñó7Ãc"É¥qÇU7䯥 ‘ÂÀíôÐ|¦‹ôñ¤”Äh™\"iÈZ^&x0ZŒ¶~GZÉ4Zc2š©^_! ÇÐÕÙÆ[yÇÃàÑvêäa¯XGfN™9Ôž?CgûXŸ57DzxÅZÒ3sQ(”ÔVŸf` Î@fNÉi™ôY{0}³°p©òí{¹\Žcp·;p6x.æ$믽…BÉ‚%+ÙûÁ®(ŽÂÜA¡PLºˆŽ`î2l}®;Þ>©Yb~ ù«}Ç”¿Y‹!^CþšTæß”Mos?½SÈOz) Ìë;¾Ù|xûd].q™F–}º¥FŽÇå¡­ºkÛ®A7^¯­IMÖ²Äï‡r”ø›h lC×Çãß htÙ$¯Ï`ßø†™™hcX›u˜Âø7&T.áhŒëå‚R="ÆÂ¥0œT;CY1TZYË&.®&ÉÃÏ(Ê;H_OLª]ŒÚŸN0!7ÆŸ¥g¦+6ú¯±—ˆ«»FsL"Eè1x<úC/µx½^Nù„´ÌäréYyA öüÔ Åó—’œšArjF@g+áõz)*]‚Ûí û}ŽÁð3³ÞîNΟ­¤pÞ"’ÓPk4 ÎA+kG‡ÏÒÜ<ùÈiƒÁàwßhnnú*'>ÇLLª¯ÇKõ¾‰­Ïº5‹oË šÏtÑPîséHÌóY±—}ª€~]>%¿½žîž€¥Akohæh3Z€L$¼äJß f²fÑ`ªã¢ÔÈYzgJœ¾öüáLP!Š˜4Ôt$×gô8É•‘ùn_™|¢k3²}¢"—Ç€ µ^I˹..ص0”+U4Æu¦˜ê}2:òt&¦Ž_;ý݃T¼U;áþãÞé¨ñùbk *RJbý¿‰ÃÙ7šNwÎxÎ{ÿ5–|÷v$":šc2ÌT¯¯ÐSÄårÒÕÑF|b 1–ðQg+ÓXWCFN>FS ’$£¯·›‹Îaíí¦lݵôtuâªCåmÍ Î[€ÙOkSè¡W2ƒƒ¾Ùñd]0ÆŠgáº!(²>7”wLX%K¦±ôÎ|”_ú¬“;}à^—c9Ϻ硳¨Y|knØ"¡°Z­¸Ü.”J%N§Çàì”w¸p;=È•²AC£Ñ˜|ñcóäÎ$S—¤‹//0PñvmDUܢрÁQ㤳D–íc¸ÿ’ä[’·:¢5Ž´ ËòLa·:Pë•x\Þˆ‹éDc\gŠ©Þ'£ýˆÃ™ Ã÷…B%Ÿv‘"¯+:É]™ìУÝ7Âå~Ž&£ïsE3¥ŠƒcۈƘ 3Õë{y¯Õ]¦ [†Uªñî>k§NæÀÇï°ÿ£·©8~ko7­Ž¤ŸUºµ9rÑ;8ÊB=Q_® ññ>Ÿi!žà ÒŠÍ4âõz©ÚÛ8áþ2™ä/O\¾³&àhmíçì à[×`ØR¶áè ·§—þܳ”÷Ýë @Žz…Æ B;ô‚憎jWÀ„ºÎÇLm\F ªžæèºÑLõúXÛFŠI„ªÒ8F–w}bÒ|Ûý.¬m—¯ûÐpš´ØL㔟a¢1®AD±²ûTî“þ.»¿àÌt‚s‡íT:Æ„‰ƒá&¢a(‡9Yï+¦”çsß[€f¦èU0!'²kí1f*×Wè eÃp8# p(*]‚L&ÃëõP{þìÄ„atVŽÑiñ®V†—]„x S°Îg}nªìÄÖ9ñ=ár¸9úJ5û~{Šæ³ÁÁ#ç÷7±÷·•œzç⤖/OùÜ Œ‰º°"-cqH>K{KˆóÍè tÚiXÕ"Á58²Ô« ì8úe}¯Ç¼^üy¹òb2jŒ%Ü<£ó¢ÕoQÎ\ÚíD¡–û—×›ÏtN9ÌlÒXÙ^_pdÖÒÈüУ1®c¾g'Z…‰6^/t\ô‰Å„<3*}ð=+ICÏß8tÔöú'…ëÓÆÝw2ô¶ôûS ¦”Äú﯆Îý<ŒµmÀouÎ]™2˜t˜p×8Úc B@G€9Æ—Wµ¿oê3µœüb²óŠ8wê$öÈ­ ¦˜‘|­¶ú2×èî¾Aˆgà«tŸc/“²>¦³.ôóäõNœ¢ër£þD»ßueɶ|ô±)!“‹,þ ËšC-ºEØ:ü/®ÜUÉþ4ÃĤê‘Í@ÀŒˆ€œ•ÉAÛÓæÇ1ÿ†‘ˆ|Í ¦³50hs"I°ü3…¤”ļðåJ¹«RØô¥E!ݼ^/çöø*é%ä™)XŸp¼R£`é>—"·Óù§vÏ6½-ýÔ÷ù>Ï».“œ²ä K´%ÝÀêí%㶘exU%mA|ÐþZ³zF…õpÖ¹BÆâ[sQ¨Fž¥FÎâmùæ(ö¸½þråÉC•õ”cD§.FÍÂ[rXxKΤú5,–ÓJãfÕ}c˜ò]µ¾É–Aɪí%AzQÅü›²YsiÈß“™“©"| Ç 7˜ðxÜaû’S3Ðê 4Ô…® ¹\Îü%+ÉÎ+ ½µ‰ÓåGÇ=&>1…öÖX”`³ IDATÐAO’$‘ïâ½ÝôY/¯|™— 0(&Ï*Ñ|¶kÖËM_N¸޾\MÙ=Eè,j6>¶€®†>ý.ô±¿Å¶£¶—³†.7=¯N¿[Ç¢[s‰Ë2±ùË‹±¶öãõxÑYÔhÍjN¿W7©€Í©ÒÓd£ùLÉE2&`JÔÑYׇÄf1%êè¸hE&—°¤(Ùœ­Ã>cYSœ.ÿñË?]ˆJ绎~¶.;r… C¼ÖÿòÏ^–DåîàŠs´—i"¥$–Âõid.J §Å†L.Ã’n@¡’ãq{9öjuXéÌ%‰ä­Jatm­…7çøƒ³N½{1d™÷h·P¹û"Ú5ñÙ&æ]—IþÚTz›}ïSCÜHŽgµ^ICEGÈ•œhŒëhÎ~Ô@b~ j½’-¤·¥§Ý…ƨ¯¥íBOزâÓ¥«¾šƒ-d¯H"!×̦/-¢³nèM÷¥Ê¬ÚÓèŸÄ†£¡¼c¢Ž¼U)d,L m^ÝM6Ü“ï<|[ª÷5M¸âÖPÞAÑ5â}Ïÿx¥»G#WÊØô¥EŸ ¼æñ…ÛŽ½ZíwíMg•ò]5”Þ˜…1A˺Kéïd°Ï‰J§ðUV•¯¯€P¨ÁhÉTz“™µ×l üØë.øû%$¥²¤l=Ýí47ÔMئB¡$;¿˜¼ÂRÆ‹8²ÿÃ…X†™¿d%y…¥Ô×VSyâ0ý#V/…RÉÂ¥«°Äú–}NWŒ/Ä‚« S¢Ž¤!«Î¹'…s®†>öî¨dÞuYÄg›–Åv7µGZ8÷q㤣ïëO¶ƒÅ›2Pë•ÄfŽ´7Ð3àæm޽RMÉu™d.NÀœ¬Çœì³\¹ÝTíi¤jO#³Š²»|†é¤› ]õ}|üt…ëÓH™‹J§@5ddŸUöüþæ°Ëã^/¾=Í6rW¦ 1©,çuVμ_vU|–̱–ÕÑ9ˆÇ®ÌTàsƒ:ð‡3ä­J!ky"ƒÊ_`|Aµqá@Ó¸nPÓ×Ñôµðɳ§Y°%SR +“ÓîÆÖiG’I3æ–Uñv-ƒ6'¹«RPé$ ­bwrôå*zšl hðM\{šlä¯IÅ”¤ xŽÝN§:¨Þ;9¡h·:è¼ØK\–¯ ÉdKwK’äÏ_Š±ÛÆË0S{¤kÛùkSIÈ1£‹Q£‹ñ݃^—¶ó=Tïm÷Þæ˜L)99yÜ;f¸E_ßå¿l9oáròŠJ>“$’$áõz«­ÏÊ»;ÿ°¯Z£¥lÝµÄÆù|·ŽAz»:q¹ f &ß@¿ÍÊž÷Þ¤ßþ¢&§e’™ObJºßWÙÖg¥âØš&N¹’•[ÈÂekÉ|7_Ow'ý}(J,± ȇü}ÏTãtù‘ Û»RÉÉñ-½\¸0yk¿@°ôŽ|RJbi­êæà ‘ÇÌEÔz%†x-r¥ {¯[§=b‘)É$L‰:4F%—þ®ÁyQ…B©Q`JÖ¡PÊ´9èiî@’Lœ¬›ÕRÐ •O„jŒ*Ü.}mãH‹$I˜’t¨ JÜ7¶®Áià¸ÔH2 ýЪø&W“±rŽeºã:}¬½Eƒ$óeA±¶ ÌZ<ƒ\)çEmPb·:èm‰|eDkV£3««ä Úœô¶ô_1q¡Péè,Ô:ŽÖÖ€4€“a¶ÇdN èÒÅeäÍŸÔ¾¶>+»_1èsI’ÈÎ+&¯¨½Á°ÍétP_SÅ©“GÂ?fIÙz2s ðz=´57r¡ú4-uSJYg0š)Y¸ŒäÔ d²ÀÙWG+g*ÓÒ8±üJFhÁT1ÄiØðØB$ ö쨌¨r @ ã1§t´ÑMètd2ö~¬Ö<“L?e‰KÀ`4ÓÜX7í  …£Ù‚Z­ÁétÐo³†õÑžk-˜**‚ìåÉècÕ}¹úRwG s! —5B@ @ ¸Üiì@ ‚) ´@ @0„€@ ¦€Ð@ ÁZ @ ˜B@ @ S@h@ ` (.u‚ñèï)uªÑêQ«ÕÈJfçûµZýì|‘@ ‚+! Wæ˜XäJå¥î†@ Ђ+ƒ©ˆçe…Vn\ÞÁ¢Ü>’bÈ$/í½J*kô¼u8ŽNÆà·þ¦@pe —d¸½žKÝ @ ¸êZ0gH²8ø§ϳ¼°7h›Aë&;ÉÎÍ+;8S§ãÉ_åSÓ¬¹½¢C©©˜2ïá‡U?¥ÑÞ|©»#W"ˆP0'X^ØËï¿YR<¥(£ŸgŸ,gÓâ®Yè™àR£”+IÐÇ!I³ä8? ¬‰-ãïò¿D¼:ŽCÞ¥îŽ@pÕ`4hˆ7^ên.äƒáÇÛA§Óàp8f£?AƒÏ8. v~ñÕÓ˜tîI·«Tx¹fQ7·Ði ï¢Tª&ݦàòèÖóÕusmÞz²-n81­öb4&æ'•P’X@FLm .‹~ç@”z<1e–¥|!÷$IâéÚçø°}oÄmefƳ|i f’‘‡Å¢Çjµãp¸¢ØãÙ§t^:ÅE©(rººl—º;‚Q¤$ÇP¶"ƒACGGÞ+ÈŸ®t^:ßýÇO³å¦Åx<^ΜmºÔ]\B„ ‡àŠçkw×¢×L^<£Vzøþ£U|æŸæãñÌë¤`„Kf €‚¸\ô*6GÿG£Sj¹£ôf§Ì¹ýXS9Ïûã´ú:JMÅ<–s?¿«{1bñœ™ÇÃ]CNvbÐ6·ÇóÏía÷;åÓëì%dñâ,n¼~!MÍÝ<ñäï/uw£HMµðÀ}øë¯ì §gêÏã¥bÙÒ 9«VæóÊ__â .%B@ ®h–ä[Y;¿'âãsR¸~i'»ÅE±W‚Ë… ]µôZ1©Tu\ˆH<+dr+»4S2¯‡Š–3´ÚÚ‘If‰‚¸\ª;j¢ßù1˜”F¾˜ó IÁ»m²»õýˆÚINŽáÉ'¶¡×«±Ù9tø<}hµ*MÌ+I§¢¢>ºæGŽ^àš%(rö¬¾ÔÝ\b„€ž%”J%.—kÚËU …­VK?n÷Ô­®smkÚ§ÝÆ Ë…€ž«XmüÛÿEc¦½¿#¢6–¥-ò‹çŸî†š®º€íÌŠõCYÛ1* 4 4ñ\]äÖî;¶-G¯WÓÑÙÇ·ŸzkŸ=`»\.Ãí™=‚±”WÔó7÷[4%mmÇÛf‡„wÞ¾‚øx#¿üÕ{´´FnT› sZ@'$$°}ûvâãã9{ö,;vì˜ð˜¬¬,{ì±Iµÿ£ýˆ¶¶¶°Û‹‹‹Ù²e .Äd2áõziiiaÿþýüéO¢¯¯oRß#—˹ᆸþúëÉÌÌD’$Ün7UUU¼õÖ[|ðÁW”Y4YR`v r&wW&N·“V[ä­K&MÖ– ñ à…þJŒ…,6ûÜG~W÷.oä>Ê…)IQQà æìëëÃb±PTTDff&ÇŽ£»»{Rç7×H²L?¸5ÎäD&¨(¸zÑ)µôEàþ-nN¾€*ÛNYÏN«-½^ @¯`†$‰Òyiäå%ñÊ«sÓW|Vt±ÇÁ­Î>–yì${\x™‚½r-/+4JÑëFQQ_øÂÈÊÊÀív#—G6#yâ‰'ý’q»Ýa](n¹åî¸ã^yå^~ùez{{‘$‰E‹ñå/™ØØXž|òIþöoÿ—+´5I.—óÄOPTTD__?ÿùÏÙ·o…BAYY …âªÏN—„jš·Û#!´óÜA¯Ò¡’‡ÎžÒçèÃ鞢õö§¿‹UYX`šÀí{¦Ý^$§#“IÄÆú2âôö„ÌÒ!Iqq¾}¬V;ƒƒÎiõs2ÄÆ(*LA§UÑÕÝÏéÓ ôL<©V«•½½¾ßxƒACIqª/;DgçÏ·ÒÂB?I‚” ñFÌf …œžž~ªª[& g±è‘Ëe 8°ÙCï£G®a·;ƒú$—˰Xô ºÂZG‡Ï ¯ÏŽÝ>³×G«UQRœ†Å¢§¿³çšèè˜újŸZ­¤¨(…„8#§›ÆÆ.Î_h·V|œ$ÆÓaÌfJ¥§ÃEOoðØÅZ È䡚ŽkDE¹22âÈ̈C«UÑß?H]]'õ “n+’1™ ŒF-…ÉX,z4%J…ƒQKR‚‰ÜÜD  —÷Þ¯ô?g¡ˆ‰ÑQX‚Ѩ¥¿óç['tÃ0›´(‡^þf³n¤-‹û˜ßž>«=è³h0£Z‰—¿qtq§3x™=×ã$×ãäsÎ^žUšù_U ÓY8Ôh4<ðÀ\wÝuH’Dgg'O?ý4ëׯ§¬¬,¢6›šš°Ù¦–I­VóÙÏ~€·Þz‹ßüæ7þm^¯—cÇŽñoÿöo|ÿûß'55•k¯½–]»v…l릛n¢¤¤—ËÅw¿û]ª«G‚\.{÷Fž¾j®ÐÒ¥"7ez–´Ž^¥¨L8‡¸­ä&–¦.¹íׇÏ©ÖéYpg›bcÿÿ'{*/IbÌzþë?¶ð_?zƒcÇkƒöÑjUþ}~ò³·ùdÕŒõG.—qßöõ\³±$ÀÿÜápñò+‡&<~ÙÒ¾øØµœ:ÝÈ¿ÿà/|úÎ2n¼a¡?ÀÝîä•¿æõ7ކmçÓw–±iS)FCpQ&¯×Ë'û«øÍï>WÄ}óëÛHL4³óÍãüþùпé_ý»[È̈ã½÷+yzÇAßó¥/^O~^ýýƒ<ñäïC Á/?~=‹eÑ×gçÉo=?£úÚM¥Ü}×j4šÀ¡{öžådùÅI·³õæ%Üvë² vZZ{xfLJTT†vý뿺œìDΜiä_þí•°í+2¾ÿ¯ŸE§SóêkGxéûƒöyêîôOPÆòØ9%aV:/íŸ_GZª%h[Kk¯þå}|zÜ6"“hb2iùü=k)+ËC. .'âryp¹ÜtvöQßÐI¬ÅR@k4JîÛ¾ž5« ‘É')•õ<ýÌ´†ñ5ìÑkY0?#èó¯}ukÐgá~³¦ËŒ h%^~loa‘{üÙŸ ¸×ÙCžÇÁ×4‰‹h·ÛÍ‚ ðx<¼ñÆ<ÿüó °~ýú[ŒŒ+Vøsg¿ñÆ!÷©®®¦²²’ÒÒR6mÚR@Ëårn¿ývvíÚ ž#ì?eš¶€>rN$ÅŸKtöwQß3’ŸU.“‘bLšôñEñyl_òÿß*¹ïEU—Ã?]ÿõ€}/v7ð‹ƒ¿fǧØàÐÝκSŽùÔe\ÝÈ„B­öÏÛV°õ–¥û¾¹ëø¤è¥D’àK_¼ŽË}dê:)¯¨C’$æÏKç®Ï¬¢¿ü÷Î0–¿­ 5­œ=ÛŒJ¥`Åò\  wfN§‹·Þ>òx…RŽÑ ¡µµ‡šÚv¬Ö  ¹9‰$$˜X½ª€ÄDÿô/Æ3C>b—Ÿÿâþù»w¡Ó©ùÜ=kùéÿîØgeY‹ùVeŸÞñÁŒ¦Ž»þºÜûùuôôôsäh ²²âY³º€%‹³&lC’àá7±a½Ï²öb;UUÍ(• ÌÏ )ÑÌÿ÷w·ð£ïäø‰`A¾gïYr²),LÁ£§«;´!l~i:zè˜3!÷¹X×0^† ©lÜPÂ÷o@.“átº©<Õ@WWf³Ž¢Â’Í<úð&úúì=VtütÇ$Z˜Í:žú‡;‰3Òß?È;ïVÐØÔ…N«bÁ‚L/Ê¢²²ž_=ý~ØqŸ+Ù×þ¿­äd'âõz9Y^Gss7&“ŽÅ‹2)—Î7¿q;ÿôϦ½#Ø[W×á÷wÖjUdeÆp¡¦•ÁÁÀ²úúðî¶ÓaÆôß8º&Ï£Yãà Žn~ªŠ‰èûœN'?þñ±ÛíÔÖF¦1YRR|:‡ƒúúð3ÁŠŠ JKKÉÍÍE©TâtÎb ‰`çÎ3×á+œ×>‰çžÍ-ÓjCdà˜[ì:÷»Î½çÿÛ¢á×üÍä$daüÆ~ÞcŸùhïDuÍöÖˆŽ—$ ¹,ø|B}Éûl³zU_<¿±óÏ¿øI@ç–›qÏÝk&ÕVrr ‰&~õôû|ðá)ÿç~å ßþ–O$ÜyG~x:¤¥qçÎcì~§œööÀ¼$IÜyû ¶Ý¶Œ¼Ü$V­Ìgï¾s‘œî¤hnéáùöqïöõ¬^UÀGŸ¦|(¡N«âó÷øíÇ{ÎpðÐùëGB‚É?ö§N5ð£ÿ~“Q.5óJÒø?}Ó„ílX_†õÅx<^~õôûVY•JÁÝÌŠåy<òðf¾öõç¾à“ýUÜóÙ5Èe2ÊVä±ëíДÊVøî£ 5­45…v…üϾð÷úuÅ<úð¦ Ïa4¹9‰ÎˆÕ:À·žz1à<Þ~§œ{î^Ö›ñõ¯ÝÆ?üã‹a‹2Ýûùõäd'ÒÓ;Àýðu.ÔŒ$dˆµxâïo%%%†ØÈüçkAÇÿá…}þÿ¤ð­oø Ž¿øå{Ô7ÌŒ`ËŒèb#¤ÛÆD|ÞÙÃ+JCÄ>Ñg΄žAÎ&F£ofj·ÛÇÌojòYÈär9ÉÉÉÔÕF÷—––ú÷ÞWÌ©‹zÞ<ÇM+"KQv¼ÚÀ‡'"›´ æ&gÚªøæ[ßóÿýÐòÏQ’PÀ¹Ž üòàïf½?…oùØêŠLܾôÇýKÓ¿øÙ#¨ÕJþôò^{=¼{ÂåÊí·-|–Áç_Üä~µóÍãde&°fuAˆ£ƒyfLJâ »»Ÿ_ÚÏã_¸VÅòå¹|¼'øýÊU|nzù×l,ÁlÖQ:/}F4ÀîwËYº4‡ÒyéÜß¾ñ­çq:ÝÜ}×jbbtttôñÛg?žÑ>l½y …ÏÂú³Ÿ¿$â*O5ðû?ìåᯠۆB!ãîϬ|+"c]¿øå{¦b6iY·¶ˆ·w®ôöPQ^ÏÂ…™”•…Ð …Œ¥KsŸÅz&¹oûzd2‰®.ÿùÃ׃üôN7ï¼WöøhŒI4ÐëÕ,_– ÀoYåóÕ׳å¦E¤¤Ä°zUAг=ò|þòWïˆg€Î®>~öóÝ|ç©O³`~éqÔÕGöŽŸI‚W¢ÀmÎÈ~èeÀíï™$''‡ 6pë­·²qãF¿U8V«¯ÿfÜܰ£SØéõÁþUÙ@.\¸@FF>ú(ßÿþ÷ùÑ~Ä7¿ùM6oÞŒB1'©L‰ïÿ!‹Ú–`ĉè´*ù‡gòf GA0ƒÁ€F£AÂo0:¹Ï%lÐ3ù½¹Jrr Éɾ ïGŸ»`훼[×þ0¾ÚGÖàöøœ óó&ï4Œ× CVM“I7ÁÞÓÇë…_üê=ú$%š¹më2 ò“¹fc ^¯—Ÿÿ2XÐF›ÅCî'Ë/†]¾Ÿ(0s~i†!ŸòÝ†itrp¨É¢…™!÷ÅùyIþØÑ,˜Ÿ‰N«Âíñ°ï“™ó×OJ4“›ë«ø¹ë­“ rK´Ædº$&˜üº¦¶6t _›mЇÝ*Ʋje>ííÖ°î&jÚh²$ÏÔùL—Q_K=“KŠ5î~B°ƒý¥à_ÿõ_IOOøÌãñ°sçNvìØ2 ÇÅ‹¾›A¥R‘™™Ö$/oD¸…ÚÃB½µµ•o¼‘‡z(@,gdd°téR¶lÙÂ÷¾÷½qSê]É(•CãÔŒéTò·?-åé¿?ŽY?¹ ƒN_ùŸBêÛÔQè¥@0>z½ƒA}}ôöL¶ƒO%Ê¥ÙËoz¹RŸìÿÿùó‘¹´Lû “¶Ö^’“cB}…ÂhÔ¢ÓªP(åH€Bî›( š):;ûøÝ³óØ#›¹åæ%¬^•$Iì|ó8§NŸ2uºÄÇ±ÄøŒAÕÓ¸6󿥾U€±®1£¶Hfd„vÁ;|ôv»FÉʲÓeFt½LA®'²“ ²K«é].¡Õ=¿þõ¯Y·n*•Š 6„ \ܽ{7f³™{åË—³|ùrÿ6¯×ËK/½„Çãá®»îbppööà2ÇFCee%ÿû¿ÿÒº|àÀÚÛÛ‰gåÊ•sR@û”Oö]ùÖ¡˜ ô΃q¢ê `V±öö¸pôõMÞB×do¡ÄXHª6yâç8öÿÑÙ(Ù«Rú®W¨´x[o^âÏ¿~o‹@IqÚ¬ èuk‹([‘ÇÀ€‡Ã…Å¢ç¾íëƒRÛE›Ñy¥•ÊÈßã6›Ï´­­wRA㥬¨l «Û†%FÏÊy¼6”Ó»l…Ï÷ÀÁꟈ ç—$ •J‘ˆŽæ˜L—wß«@©s×gVñÐÙvÛ2»ÐjUdfÄ¡R)èèìãGÿw'.WhŽ>Û f“–#Gkx8Æ+Âr)™µºW®X@ï½Ì—)8w¥~ÌcùãÿȾ}ûØ´iéééÈd2x÷Ýw©¯¯ç‰'ž ¦¦&ÀZ0ú{L&åååãºf\¼x‘øøx¦r—!Ãéýœ“L^Ó¢ú×Àçþe!N×È}ø›¯WR’iã\ýå} æ6› ·ÛB¡ÀårápL>èŒõ›Ö£—ëHR'Ò28³¾¿—3VëH|Å¢Ÿñ´{–XßïIoop\ÏÆ %œ;×R<_ âãŒþÌÏ¿ø V«¿þò ¬^UÀ‘£5ì?0sÁr£ýˆ‡}¡#¡³Ó7¹Ôh”Ó.~áõzùä“*¶Ü´ˆ²2Ÿ€ö¹oø‚g:ûL™”hŽ(›D4Ç$ôôö#IÐÔÔÓå&+3ÇKíÅvŽŸ¸È;ï–[<¨«³³I‹Ëå¾,Î'RfD@¿¬4ò9gï”S|x†Ž½Üééñù1 Á‘½£illäÙgŸ úÜb±°lÙ2€°Vãžž’’’ü>Àᘡ2y\m¨Õj\^ß-íñx°;\@°ïtO¿ð\Ì>v{dÁÕ§úÎâÅ‹„Ä"s)oµÎ¾€öŽ*v?[q¡¨•î*';‘ªªé倳YGÜPö†ªêÀ`§ÑeËkÂd#˜,Ã#+Ms•Z’${t3Z­Š3gyïý ¼^8r´†¥K²yà¾õœ=Û4nq‹éÐÒÚKÿ :šœœÈ :•õl¹iF£–ô´Øiçôݳï,[nZDvVÉIfRScÑjUttôqæìÌV‚Ï >Ì‚ù èhÉtˆ5ðØ#×"—K|ïß_¡»{êEyÊóSæ¤ IDAT+ëÉÎN ¨(¹LæÏv)£ŸÆË~mfı¤QRð¥iÊÇ=§4Eœz6Q©|¾^£SÑM…»ï¾¹\ŽÛíf÷îÐËjÃéë† ³„c8[ÇXí«‘Ñ%Eçb@¥àê¤×iåp×16%ÌneÕaF/;Ç„±.nºfÞŒ÷ãb];Ö¡YëÖ†ÜÇhаl(¿ïdH ¼ˆuÅþ—ñÉ“ÙFû³†•JABÂÄïAÇPÕ´pVÛ‚RS&Îró–Å¥âtºùÕÓïûSüýæ·a·;Ñë5<üÐ5¶)^¯×Ÿécá‚LL¦à•>I’ü–ûpTžª÷/ÙßqûŠi÷ëâÅv¿à,+Ë÷gߨ³ïlØ4ˆÑ¤¡¡Ó/šoºq‘?](‰¿hÉtÈÏKB¡áv{p8"sùd^¯³IËæMÓÿÝèeíŽ=câŒyfÿTeá€|ò‘ ŸÈµüLuy¤¯›ˆììlZZ¦ný¸ñƹþúëxùå—éê í¯[Uå[j+**B.íë7\„|Öî«þ¬}—Wq ¼Þò6)š$–[Ïú÷ÛlƒþÌ ëÖ¡PŒü&©ÕJîþÌ*¡‡™Äãñòæ.Ÿ»DNv"Ÿº³ŒÑš#9ÉÌ“_ßF|ÜäW2¿ú•›).J ølÑÂLnßæ‹]©¨¬çü…`«ÿ°X\´0“””@?g³IËß}åf’‡Äy¨<ÄÃÔÕçºÍ ÈØ Ik×ò÷_½…büWufFŸº£ €?½|æQÙ:»úxá¥OŸ°½vSé¸mM‡7vú&z¾Êx×¢Jg¾`Æ/}ñ:Iñp¸\žûÃV,Ï呇6eaHL0ñðƒ×ðÈC“«¸wÈUcͪ‚Y˾1šßüö#¼^/11:¾ñõmädZèc-¸oO}ë΀gk˜™“HihèÄëõ¢PÈùÞ?ßÍ—¿ÇÙÌcl摇6ñÙ»W³fuAØ4wà›Ô|ð‘¯ÌçîYË7, ,(Hæ›_߯ºµEö©±©Û_nýæ-K‚¾;77qÂg(fÌÜ뾪IâqGŸÇÃÏòü3•…éñ£Crr2.—+d`Àòåˉ÷%ß»wï¤ÛU©T<ðÀÜx〯”÷óÏ?vÿýû÷óðãÕjY¿~=ï¿ÿ~Ð>kÖ¬ñ»x|ôÑG“îË\f`h"jÔ¹Q+= :‡ó°z1ê|–‡óòŒèD‡GVl'Õ8RøB6jmüž…wà•*sßÅC¼]õÁ¬ö/.ØjÙÛy€5±elϸ‹ŠÞÓ ¸#Ï· »ß)ç3Ÿ^I~^ÿþ½{8WÕŒJ¥ ¸(½^Ã[oŸdÑ¢L’C[t£Å®·N°dq6ùyIl»u+Ëò©­mÃdÔRX”B¿ƒwÞ«˜´P”É$¾ñõm45uÓÖÖK\œ´4ßÊ^oï;~óaÈãþüòAæ—f R)ø§ü ‡Ÿ§³ËFB¼‘%‹³‘Ëeì|ó8[nZDZª…Ïß³–g¿'¨wÞ+gõª|4%ÿú/wSYYËå!77‘Ä窚éèèóŸ‹R)çñ/\‡B!£¦¦o þŽw+X³ªüü$îùì**ëDv´8WÕÌ[oŸä†ë°`~ÿùƒíœ9ÓˆB!§° ­VÅ«9Ìm·.·½ûΑ‘Ç-7/aÃúbV¯*àü…VíNââ ¤¦Z$ §ÓÍk¯™ð\öî;Çg>½’Ô¡|Þã•îJ¥à?ÿc{àg£‚WðïŸg´ûgÿ»›ŠÊú vΜmâ7¿ýˆ{·¯'=-–ï<õiZÛzéééÇhДdF’$¼^/‹frøÈ…“HihìâO/äSw”a±èýý±Ølv~ûìöî ígþÜï÷o¤t^:Ÿ¿g-Ûn]FM­Ow¥¦Äø'&³Ž}ûÎëæáõzyþÅOxì‘Í”§òÃl§®®·ÇKb¢‰ø8#/¼ø‰?ˆ4Z̨¿„ø* R¹Ýie{€ô¡—W½LÁ^¹–—•ÆËÆm#--ï|ç;<óÌ3ìÝ»7 ÀoÁ‚|ùË_|iäš8ç°V«å†nàÖ[oÅbñ=¼{öìá¿ÿû¿CbÆf³±{÷n¶nÝʃ>Hcc#gώ܈éééÜ{ï½€/ǰËÇÕNm‹†–.Iÿßø‡òGË$/ ¹—Ž^%U"ˆp.£Wê0ªC[ü´ÊÀU1µâòÌ/Šß^|bC±* emç'çàû7Ó¼¾ó(1=×n*%>ÞH|¼oòÞÜÜÍŽß~Ä'û«ÈÊÜ6ãÚápñŸ?|{î^ÃÚµ…$'™ý–ÞÓgùõÓï“‘7iý½•{î^dI®>ßÂ/õ^X!r¡¦ÿø¯×xðþ$'™Y³zÄ¥¤ªª…çþ°‡ªêÜn7oYÒ¥|Aˆ¿zú}>wÏZ_Ùð¡RÉ}}vþôòA^{ý(·m]¶ÿw}zii±¸=~ùë÷Bf`ðz½üê™÷ùçï|•JÁc^Ë?ÿëŸg$[ó¿ÿ˜žž~n¹y1FƒÆ>­m½üäg»9¡uB ¾ ÈšÚ6¶Þ²”¬ÌxŠ GÜì?P=i¡ØÙÕÇéÓ””ø ’L6xP&I˜Ã\7 蚎—æ÷*¨oèä¶[—1¿4ĉC.>n·‡òŠ:þòÚΞk ÛF4Çd:¼òêa¬¦¨0½^í_R)$%™Y²8½^ÃÝìÏÇ=»ÝÉþëunÞ²˜ë®%FÏüÒ‘ÂuÝÝý|ðá)vî:>)é÷œA’$îúôJÌfE£V•ÚÛ­ØBdÒ™.Rrrò¸OP\œ/G_¤þ¾³ÉöíÛÙºukÀg …Â?³s¹F,OÍÍÍ|å+_ Ø7&&†¯}íkù– ¬V+555 ’’’BZšïákmmå©§ž¢uœ`ž+VpÍ5×°téR¿ÏtKK Ï<ó ˜Ôù(•J¾óïPTT„×ëåìÙ³´µµCII r¹œÚÚZžzê)º·¹FR’Ï’8Ù,%™6¾ñ¹²’ìÈe#·wk·ŠüM.Ç«ÇþV+‚3—¹úl¾VøÐÈÔ¼Ö¼‹—^õ>Z²²âQÈe´µõÒÐ8~ÚÈí‹ACrJ :­Š¦¦nZÛ&WœfÍêB¾øØµ<öÅ_btb±èIO‹E.—ÑÜÜ=i"IžG|¼§ÓEcS7ïËX‹™L¢½#üï´J¥ ';½^Mww?µµíÓ®º”¨T RS-ĘutuÛ¸x±=bŸãø8ß„M£QÒÓÓÏÅºŽ°UF-I‰&ŒF-}6;u;°N-sÙå<&f³Žo>y;ÉIfêê:øæ·_w¹LFb’ÏR,Imí½“Z!×VFF‹·ÇCkKÏŒM(攀¾ÿþû¹í¶Û&µoss³ßš<™LÆ7ÞÈÖ­[ýþÅÃØl6>üðCž{î¹ÅOFóWõWlÚ´ ·Û͉'xë­·8tèPÈ”uã¡×ë¹ÿþûÙ¸qc@)o»ÝÎ;ï¼Ã³Ï>Ëà`ôgV— ‘èh"´àrcž±ˆ¿+ø IÁŸ_畦7.u—®8B h@0> d’™GMm;ååã—%ÿÔel»uN§›‡ûù,õpv¹<|'¢ÄŽ;رcÇ´Úðx<ìܹ“;w’’’BBB …‚®®.êëëýiã&b×®]”——sèСiM>l6?ùÉOxúé§ÉÉÉA¯×ÓÝÝM]]]Äi±Á•K¥õ ?ªú•÷(w¤Þ g7[Þ¹ÔÝsœ¢Ânݺ”òŠú tê;TGçåo|”9% £MSSMMáý‘Æãܹsœ;w.j} ²râŠ=`îSÞ{Šïù!÷fÞͮ׺;²pA&>²9¢c¿ú÷¿‹¸²`r<ñ÷·’ž>qIå±¼ón9/¿2q,àÒ­ç¯òT·n]ÊüÒtî¼}ï¼[NϨ*’$‘žËu×ÎgÅr_pá{ïWLÿf€hŒ‰Ð@pRÛ_Ç?ŸþÁ¥îƤP*åãc‡l #\­Úˆ®V>U™àò!ZÏ_å©zö}rŽÕ« ¸}Ûrnß¶œþö2™ ƒA.î÷*xs׉i÷&ˆÆ˜Ì)hÁÜ#'ÇWÁá/QàJE¯W“’3ñŽ!¨>ßõÂHkVòÈC›hï°ò­o¿pÕ[¸3ÒãP«§nOëê¶Íx9uÁô‰æó'I°ª¬€k6–sÙíñÐÚÚËéÓ|ðá©ùÓ/¢1&B@ .k„€ÑÆWIÍ+*– ÓD£V¢T)p»=ØíŽIx¹"\8@pUár]龂+û óªÍb#J² @ S@X WNÇ JÕ•S5N0»<³ì.u@p!,Ђ+‚Öæzº;q:…ߢ@ ‚Kа@ ®úz»é물§`޳Ì÷ÏõÞriû!‚«a@ ¦€Ð@ W;2á”0„€@ ¸Šq­{ǃ/€J©»rÅ ¦@ W#’„óæïâ^q/ öá‰ÏEÖxòR÷êŠ@X @ ®Bœ[¾ƒ{ŽH=¨vÜ#ÄóZ à*ÃuÍWp—ÝŽ~”Ï=(ÄóZ à*“»ׯ¿@ùÊ×Õ¹Ä=ºò>г„L&Çëõˆ" @ ¸¬Y±¢ˆ-[ÊÐë5<ñÄ/.uwÑFcÂyû@’þ=òŠ×.u®Hæ´€Öé ”,\ŽN§§³£Šc&<Æd¶°hùšIµø“è·õ…ÝŸDNA I©¨Õ¼^/ý¶>šêk8{êNÇ`ØcÓ2sÉ-(™T?Nù„ž®ŽIï/A(ŠŠ2ظq!½½ý—º+‚Àµúa¼¦¤»¿©»sÅ2'´$ÉÈ+*¥¸t r…ï].פŽU*UÄÆ'M¸ŸÇãÁ>þÇeÞÂå”,ôÿí´#W(ÐŒä/ =+=ïí¤ÏÚòx­N?©~Œî·@ ¢KNN 7Ý´œ_ÿúM—º;ÁôPjq¯¸ù¡g‘DußH™s:6.‘E+Öb2[ŸÐ•É"sõþàíWq¹Bÿ`z=<OÈm¹…óüâ¹êôIÎ>‰cÐŽ$I$$¥²¤l=­Ž•ë¯ã½7ÿ²úÚjºÚ[Çí_|R*Åó—àñx°Š×@0eT*‹员ƒÁ E©T`4jIL´PX˜Frr,5|øáÌYÅÅ™(.Î$6ÖˆÇã¡»ÛÆéÓéèèR;YYI¥£Ñ¨èêêãĉóôôØÆ=&)É‚$ùþo0h$‰ädKоmm=¸Ý¡ßÑF£Q±pa.ÉÉ\¼ØÊéÓuºCZ,FÔê‰cµ`³Ùýgg'QR’‰V«¦££—ãÇÏÓݸ¢¬×k0µþ¿]tuYýÇÅ™X´(˜=}}vNª¥®®mÜ~ee%‘™™HLŒÞ^••ik›øý=úúttXq:}†AI’X° ‡ìì$$I¢¹¹“£G«p8‚ ‡î’›ðê|×T~ôù ¿w"² H)Z€R£ÃÖÕÎÅûéïé÷¹R…1.Ñÿ·×륧¥Áÿ·R£#gÉjÌÉ鸃´×VQW~(¨sRÒ¨A±v´âv:Ÿ!5cÁr² ‘$‰îæzjŽîÅ5ÎÊ¿J§'µh1ÉéàõÒÕTG]Å!
    Ð?®… xþR.žgÐ>0¥~ ÁÕŒB!gûökÿöî;<®êLüøwîTMWï]²dË\ÛtB NXzHXÒH6Ù°lÈ6› Ù„¶›Í’° I%„Л1à‚{‘,Ë–eõ^Feúýý1ÖHãI#ɲlùýOì¸$£´‚ÏþìÙˆc?XWwhŠ‹×qõ]ßÃdµ‡Ïy\üðc‹O¬†/?þÚQOßÿ|ßç8´ù-ҋʹþ_œù·þŸú-5¢êÑhÖþã¿°êúÏF¼.À«‡ ¿ý1üí÷Q×Í™: ’šž‰ª9R}€ª};ñû}ääŸÒvdfå¡;þA{äPeÌ2=]t´µ’–AnAiÌz"vg)陡שŽîg+“É„Ûힸ 8«I?9»)ŠÂ·¿ýiV­*' òÒK[8x°@ @yyW]µ’ÆÆ~ø/80õ #©©~þó;p:­x<^¶l©¢¹¹ EQHMu°ti1óççñïÿ~wÜñ3*+Ŭ§´4›üàó8=lÛv®.yy©œsN)ýèùäå¥sÏ=ÿsôxÏž#èŽOyLOw’‘‘D dß¾£åúûg‡Ùlb`ÀÍ–-•tw÷“Še¬XQÆOúeî¼ó´¶Æ¢wî¬A§Ó’ŸŸNr²ŒŒ$rrRyøá/„ûNss7¡©Ÿ±Þ@0JÓž±þüÊ,]È'~ð8fGÞÁj¶½Í@W;ÉyÅž³šs?úIRòJøÃ=Ÿ$D]ïéwQ»ã]r+ÎEg4áÌÈ!9·˜kï{¢e°· ïÐÎŒ\†Æ˜êZ»ó=´:=)ù%Ø’ÓCuärëÃO`v&áó ÑÓ\OjÁ<€˜õh4 ×Ýÿ3\ºQj:¸‡Æ;Ñê -_‹3#‡«îü6¶ä4Ö?öŸ×Ρ:ÀŽÍñû}ôMðø`&Yl¡»—` €kœvt¶7“’–3)E«ÙÉÆSR¶€®ŽVzº;¦Þà9Äjµât:èï §G¦´ˆØ¤Ÿˆ~ô§ÕéYw÷X|ŵ¬þÄ—9´e û·‡ÏÏ©<Ð]m³< ¡GcÍÖï ÍiÓh,'<2˜ˆÑ”@v^èÑÄa}Óétèt:¬V N§s¶›#NSÒOÄÕW¯`ëÖªˆàyØæÍ•ÔÖ6c2¸å–Kf´-EE:Ô<øýžþ}~ýëǬãškV“ÂЇx°¤$;®v|øÃËY¸°Gy–'žx3bjN0Œù”@5ÙA û4ƒ]q½Î‰–_sIÙx‡øËÿ<ÔíÞÌÛ?Àªë?‹¢‹oœöò/~­^Ïïïþ$Uï¼£æ¸Ç |cYòáÈ]¸‚ù76=ñsüž‘+5À3¹@£Q¸øÓ_ rãKÁ3@ÀïãÅŸ|“þÎ64 «?þňós*€ž g9ùÅ—U[P‚)Á÷¹Mª}»wfÿþ£Ç}ô¯<úè_c–^ÐY]Ý0f}}ƒ´´t‘™™Ì¼y9q·å‰'ÞŒø½»ÛÅW|}ì F÷'eòáŸÑb#«,´˜¯áÀØ;övÓßÙ†59´¢r+wMX·wp€­Ïý.âØ /r`ÃØO^bÙôÄÏ#~èîà»W”Ä,›·hddsõØ t‡Ï)Zé£ÞÐãP…º#@«Õ‘‘•‹Í‘HѼ tz;·¾uMks>Ÿ½Þ@é‚%lCT™ü¢²p ¿/þÜ¢¹¥ F?u‡£=Î5ÉÉɸ§edx„1,É‚±¹Lc=LËȈ?wú0é'gŸ½{kY½º‚sχV«D-ª3›á@¦ªjfÍ-[ªøÉOžá‹_\‡Ùlä†.ä†.¤«ËÅ{ïíç7v°wïØóU““G¦®^]1n@i4†î4m¶ñŸ¤Î¦””Ð(©^¯ãÎ;¯³\vvhj2ïåÍ7wN¯q„F‹ãe±˜ÂóŽŽñ§!ttô‘™™ñï9žöö^öì™Ü<æÑ9ŸU“mRר’G>_ËVˆô¢ò1ËÔ&Øâ{Âwðý×ñŽŸjq"}íÍÔí™xüaãϪ¤¿kìôƒ}#S«Fÿ $€>AwW;¯ýýI‚Á`Tj¸Ê½ÛYºb y…¥ä–Rwä ]'äjìßµ¥+V““W„N§§îpCCƒ˜ÍVò KÉÈΣßÕ‹Õú ˜Lª¼â²Ðã †ºÃxÇÉg(Bt:]ܛ舳—ô“³Ëÿýßë,_>ÜÜTî¼óZ~ùËçÃsHM&_ýê Ølfüþ@Tj³™ð ›Ù´iW^¹œµkQ^žKR’uëÎcݺóØ·ï(?üá“46F?2@ž˜šm,§óèá¼Ëv»™k®™xW`.þ™¨]]±§·LÆdê°ZGrHÇÊÉ<ÚpNçá€{"ÝÝ®¸žPDð¸ E &ÇÄåO`²\S²òâ¸®ÑÆ9z`œ6^ÝQÓ4Æ3œ².à? ŒŠÑd Ç8‚Á CcÜ©ªÊÞ›ÉÎ+D«Õ‘“_@Ô9ˆÑd¢|á9ddå’‘•QGõ]¨ªJYÅ2ÿ˜¯w¢Œ¬ÜpÐ}i>Þ™ IDAT¶¤®ëì -Phi‰^\3«Õ~,ßÒÒ*ÑY@í J?ñ¨©iä¾ûãë_ÿ8ëÖÇE-áèÑüþÅÅYØíf¼^?=ô'ŽC«“¥§§Ÿ'ŸÜÀ“OnÀá°°bEW_½’%KŠY¸°€ÿø‹Ü~û£æêŽþý‰'ÞŒ+í^]Ý©yOS1œ=¢¥¥‹Ÿýì¹ ËŸª ]¦Âí ¼ †ñãáó3º}ºª¢é¬EM-AM›7éËÝ®‘ìMOüœ†èwÔ«{¶ùÜ¡¿µÖ`Í¥#øÖFòÚ´NèIòû}tw¶“’–‰31eÌrÕvÓT”ÜÂlv'B_ÇjáêëaåšËèíîŠû.²øxêºöÖ¦YÏ6rªx<¡Qöx­ŸÉ#ù³ÄñYPÒOD¼†3UôöP_ßNVV2Z­Bgg/o½µ“gŸÝsÄ÷Tèíà7vðÆ;¸å–K¸ýö«IIqpå•Ëyúée;:úPUFCgg›7ÇÞàL1¼+ŸÉd8ãßK_ß n·“É@ZÚø#¾ii¡©“Ýur²”º-RKf/™ôµ®ŽÖp_su¶qhó[3ÐÂS§·-4^£Q°%§ãꈽÈÓ‘–þ¹¿sdFè)ðOb0Ç-×ïê¥rÏö¨ã¦3陡Qé¶–±Œæp&‘’&§ŒÇjµ’’š3-A‘‹ô¡Ñh¸ÿþ[IMuðïü vÏv“Æô ›Ã»á o’1Úà ›ªªzæÏÏcéÒþö·Ø™+&cxPg2 ÜO–íÛqãátZ),̘ÑÜ3MUUª«X¼¸ˆùóÇÞ99ÙNjj(ÀÞµkfGl•Ú÷,ÿ$ª-5c>š–øoR<ƒý4Uí&{þR –žsw¾3ISÕÈ÷Ùó—†ÒçÅ=ƒ=]´ÕV‡K»)P´¡û¯ojsË*–¡( ª¤îHõÄ02ú<Ð¹~J¯;× ï¢%A‘ôáp˜ÉÏ£'fe8Õm1·ïf2¤9k§»·Þ =J_³¦‚’’¬˜e&cøu¬Ö„ˆ×?vî¬ ?¸í¶Òמ Ã7g……™”—çÆ,sõÕ«Ðh4AÞ}wÿŒ¶G[õWhÕ¿üÖI_¿ï­ç(_ó!2JœÔ¶ju{¶Òߚδìê›c–1ZláVª6½‘ùLè)p8C¹:û'¿ ¡°¤œ‚â2UîÅ=4ñ|'S‚9¼qÊ‘C&¿pà,ÑÓÓCCC£Eb\ÒODoï`x1Ø·¾ußùΧùú×oæë_¿™¯}íf¾üåkø‡XMjêÌæw:­üüçwðÈ#ÿÌ’%EQtjªƒ{ï½%üûöí‡bÖóüóïSW׊¢(<øàí¬XQq^«U¸ôÒe<öØÝäå¥Mخݻ¡‘úÛn»"¢]Z­Â‚ùq¿ÇÉòûüò—B9‘ï¹ç¦¨L™™ÉÜ}÷Ü}÷M3ÖŽ“å•W¶…·ç¾ï¾O†s[³f!·ÞšÒùÜsïÒÖ6Ãé4>´›úqÉu¨ŽÉÝpmþ´×B£h¹åÁßQ¼â¢ˆóŠVËÂK?Æ{”¼ØéãNj0À;ÇÓÞ•¬¼˜ oûW4š‘°ØdspÿÀdsàs²ñ÷F\¯ÉÈÈ7N#Ößß?^±ÓÖª5—“‘G[K#ï¿ýê„å-V;Á``Ì…}Y¹¬Z{[ß}3îL´Z- —­¢ 8´Jº£­™÷6¼Wçù‹ÏeÞü%øý>^}þÏ“J{w¦+, åЬ­Ú¶£âìðúµ/pÅ_¯šå–ˆ3ÅŠeüÇ܆Ñ8ök ä™gÞá׿~qF.Î;o>?íí¡ö··÷NnËu}ž/¾„š\ˆRý&†?~.þk”¼b>ñÐã8ÒCòt7£³þzc% 0ZBï/?É ?º7êúŒ’Üðï¿À‘–¢Óáîïc¨/t£QùÎ+¼ùë‡&lÇ{ Þ€Ù™ŒÑl%àóÒך×Ü×ÞÂÿ}õãÖ¡Ñ(\wÿÏ£Ì}íÍ´ÚÎ`$§â ~Ï~÷Ψ)2z«ÝÁê‹C_Àûvm¥©¾6âC35=‹e+×ÐÓÕAKãÄS)t:=%åÏ«ïbØx¬–[6Æ©©nºé"t:-¿øÅßNz6o®äÓŸ~˜O|âR.¾x ‰‰¶p°#shŸzjcxšÆXŽkãŽ;~Ƨ>õ!®¸â23“ÉÌLŸ¯­mæÙg7ñòËÛâjÛü4½½\sͤ¥9Ã‹Ü ´ýôL§Âûõ¯_¤ºº[n¹”’’,-* Ÿs»½lذ›?þñ­1z&'ÛÉÊJŽ86úïOú;›-!ªŽÑyšÍæñ×A ;p Žù—ŸóÏÿü1–-+ o®¡é2Ï?ÿ>¿ÿýë㦺ËÊJF¯ Ùôz]¸mÃ}7n¾!ôÏ~ïçž!8ï2+?…vëãq_Þqì0Ýq-~ê+,¾âZ3óHÌ™ãÝV{­Ïþ–]/ǾQÓŒ$fE>Å0Yíá´r–q4Œ–”•vÔNÎZ½!\ïðTÛ‰¨j¿~ïNš«÷rþMŸÇžš‰=u$—ú±½Ûxë7?¤~ßQ×Ωè‹—‡ó$Óh4Mh;ÇQë@¿‹·^~6¢¬Ñ”ÀÊ5—‘”zÄåõzèëîÂðaµ:°ÚC“ü\¼»þƞ‘‘G^A i™9hÿCô»Ø¿k+Íñßµ–Ìgñ¹çðÆKO3àšÙº§ñhq²étZî¿ÿVÖ¬YH0䆾Þv{&h4 2HJ²a4êq¹©«kRZ3“É@NN ))¼^uumSÎî`2(-ÍÆn734äáèÑÖ“’Oy2ÒÓÉÈH$!ÁHw·‹Ã‡›g|‡È™’˜h#?? “É@{{/ íáíS-°âñ}ä; `øÓí(‡ÖOº½ÉLrN¶” ü^u5¸:ãO'z:Ñ(Z2ŠçcMNÃ;4@wS}íc/bS#ÐEAQbßk44š‘s±ÊyÜClzóE ŠË).«Àbµ“’>r'âóyi8ZCåÞn~’™OfNª¤­¹ÚÃU´6ÕOú1 Õf'ðÓÑÚ|ÖÏBq²åç§sÁ èíॗÆÞµÌï𷿽˚5 Q…¬¬ä  UU¥¶¶™ÚÚæi×åv{©©i¢¦¦é¤Ô5ÞNˆ§Bkkwxñ™®»ÛEw÷©½‹vÛïQ͉ø/ù*Þ›~…áÉ/ ÔlœøÂQ|îAZjÐRsægSƒšíƒØK ¢Ì©zÿ®­ìßÿ6ޱ¨ªJmM%µ5•XlvÌf+Š¢àÄåê%ˆï®÷èá*:ÚšiiªÇ7÷îÜÂ= ÓŸÚ•ÐB1¥¦:¸ýö«ôðÆ;Æ}lž›;²ànx¾©s‰îíG!ÀÙ=xoù_ ¿»¥>þíÊÏfs*€>Ù\}Sõíîl§»sú[SØé‹„BÄïÀcx<^Ìf#ßüæ'xì±W¨¯o\ï’êàÒK—ñ©O…Ò¨mÞ\±àìÓŸ¾’uëVMúµkjš¸÷ÞßLÿMœ†~ø fLúº¿ý-4ÿWÌÝ;¿@Ó×Bpþ•(M/Š!@ !„8k ºùÕ¯^à+_¹Žµk±ví"</ýýnTUÊ}¼oßQ~øÃ'#ê°XL‹ýâåpX¦ÝþÓ•Ãa™ÒßÄb‰o!ž˜YÚÝÏ Ý=v–Mh!„g•¿ÿý}nâÆ/dáÂÂã ÷BA³ªª´·÷RYYÇúõ»xç}QkWž~z#ë×ïšô뎕¢l.xðÁ?“0ù©†¥’ât%´Bˆ³ÎuüÇ„¶"Öëu˜ÍF‚A•¡!Ï„æÒ¢¶“åd,~âL"´Bˆ³šÏç§·wìÅ„Bq"ÙÊ[!„BˆIZ!„BˆIZ!„BˆIZ!ÄY¡°°ÂÂÂÙn†8ÍI?ñZ!„BˆIZ!„BˆIZ!„BˆIZ!„BˆIZ!„BˆIZ!„BL™¢3 5Zf»'­É‚Ö0aIÙÊ[!„BLŠ9½Œ×à,]‰Þš€ðÓßt޽ëißù*jÐ?#¯­è ”ßú`Üåë^ý/šyÞš]FÖ7a/Zž}ý]tÜLã»Oâím‹ºFèSDQ´¨jUU§U‡V§ÃçõœÄ– !„BÄI£!ïòÏ‘¹ê:Ðh"OiuØr+°åV±âczú» uÔŸü&(Zl¹ â.¯3YÇ<—vîG(¼êËQïEoM"íÜ«I^t ‡žú.½GvDÖ9¹&ŸYÌ+ó/Çl¶ÐÕÙÎþ]['¼ÆîHdÉò âªûæ·èó|RJ:…¥óIMÏÂh4¡ª*ƒý47¥ºrO\°V«¥¨t9ùÅØ‰h4‚Á ]m«­¦¡îð´‚r!„Bˆx•üÃ=$/¼€Þ#;hÙòMÕ|nLIÙ$-XKÆÊk0¥äV0#tÀçfÿcwE˹èVÅçâëï¦ú/ߎ87Ôq,f=‰å„ƒçžšh|ç ¶Ö¢è Ø “{Ùg1%f2ï¦ûÙ÷¿ÿÊPûH=s2€ÖhŠË*(¯X†Vz‹~|ôzI)é– ƒ¸‡Ç<¿`ñrJç/ÿîõ¸ÑêtX¬6JÊ‘“_Ì»ë_¦ßÕ;ff‹ .º‹Í€ÇíÆãÂl±’’–AJZùEóؼñuü~_\ïO!„b*Ò—4<ׯœ¦MŽ8?Øz„ÁÖ#tìz sF1]•›f¦!ªJcUÄ!ß`Á€/ê\,ŠÎž»«7Sýä·Ð€dÐ禫r}u{XüO¿BoM¢àª;¨ü¿¯…¯ŸstRrKV¬ÆîÍÇ ƒ(ÊÔÖJ¾ýúóc¦j0H0Œy®hÞ‚pð\Sµ—CU{ñzÜh4RÓ³X¶r-¦3«Ö^ÎúWþ³Fa՚˰Øì ô÷±}óFº;Gæàä”°tùj’S3X|îyìØòΔޣB!ÄD´†r/¹ €žšmQÁóhîîfÜÝͧªiS’ºìÃè­I ªÔ½öß Ï£ùûhxû~äNìù‹°d–†çRÏ™,:ž%ËW³öòu؉¸‡ùà½õ´57L¹ÎWý}½1ÿ7ÐïŠyV§cþÂs8z¸Šý»·áõ¸PU•¶–F¶lz«ÍA~Ѽ˜õ¤gå`w&ðÁû"‚g€ú£5Tíß @N~ F£iÊïS!„b<©Ë®D{|.qÆßÏrk¦/¹â"\õûñt·ŒY®sßÔ`€Äy«ÂÇçL IMÏDUƒ>¸7_z†ÆúÚSގ̬Mճܘé3§¡Ñ†Bà¡Î‰g*¸;±f•aÉôœ3#Ð]m³< ¡‹‰õõ»B“Ý5‹ÕuÞÕzf‹ë£Ë)i™¸‡gý}Ÿt::«Õ‚ÓéœíæˆÓ”ôé'BŒÐ%ØÂù‘=1r"Ÿiô–‘ÿ¦}®® Ëû\@høpà=gF gŠÃ™„Í‘ˆÑdÂëñÐÞÚ4nö ¯74ßY§Ó[ïèvz½!ê|sCƒ.Ìç]ø!vlÙHWGkD»Ê.CUƒìÞþÞ˜#Øg“áQ"§ÓÕj‰8&Ä0é'"ÒOD, æôz=>Ÿ¯ÇK`’ß½Ó½~¶êÑ%ØÂ?½CQçs/ùÎÒ•Qǃûÿ÷+SjÛLý~>÷„徑˜M—`Ã×ß-ôxÖ^¾›=räAUUj`ß®m¨jtöŒá‘`E«ÅîHsdØ™”þù„ÜÝ@hzÆûo¿Æù]‰Åjcíe¡§«ƒúºÃ( å ÏÁï÷±å7hÆBÉÓÑh⟋èv»q»˜L&ùÒ;›¿_•~"Æ#Ÿ'b:l6v‡Ýñô¸}½}twÇÿôwº×Ïf=E;n]j0ˆ9½(êx0pz¦Øx?cdT‹0*Þ¾Vèq(ŠBÝ‘ƒ  ÕêÈÈÊÅæH¤h^:½[£Sǵ67àóyÑë ”.XÂö÷7D•É/*£|á9áßý¾Ø¬ßÕËúWþÊêK®Â™”þ„ù=[ß™ÓÁ3@rr2nwôïD†¿†¾'¾Ãg.=4-#câî'’~röÏ1§­v$ð²Ùm“ \§{ýlÖðŒü7£èQçÛv¼DßÑÝáßS–\Aê’Ë'ݦSÅïÙO‰1 àDŠn¤Œ(”…MètwµóÚߟ$ â9áC¶rïv–®XC^a)y…¥Ô9HWGä\ ` Àþ]ÛXºb59yEètzêW144ˆÙl%¯°”Œì<ú]½á¹Í>Ÿ7f[,V笺gR í­M4Õ×’•S@Jz†\BKS1Ûß[6RƒN§‹{qö’~"â!ýDœ­£N9z]–×Õ‰÷ø2=ñËNµž%Žß;J?ã‘Ï1.— À±èîT^?Ûõ 4Uct¤aÉ,EÑ›Ʊøîtåuuâîj””…5wÁ¸e5K8gôÞðq  §`xgAƒ!zÐhý®Þ¨àÀ”`&=34*ÝÖ=‡91)5¼yÝ‘ƒcÖßÒxŒ¦†£då[P2'èɰZ­¤¤„æ8Ê—‹ôé'"–¡Á!†˜üú“uýlÖÓy`#Ió× hõ$W\Hû®×¦ýú³©÷ÈvLIY8 —¡3Y#æEæ(>7œÂ¯§æƒðñ9•úTQŽçôŽJk2eËPU Rw$:!¹Å:’^eàx¾è±ôv‡I ƘéðÎ&áäËNŒGú‰ˆ‡ô!"õÜÎsÑ?¢·$Îr‹¦§ùýgPƒ~½‘¬µYF£QÈ¾è“ 4¢÷ÈŽð9  §Àá ušÁþÉÏ;*,)§ ¸ €C•{cæ”öŽÊ=z…l,ºãAs0œ“s '£§§‡††Fù²ã’~"â!ýDˆHÁ€£/ý Tƒ=…Ÿz{Á`$¯FÑ‘T¾šÄyçÏ^Cãäéi¥uÛßÈ\u9Ý‘aDoM¢ô†û°f•¡ªA޽ùXÄõ2…ã«`00æBÂŒ¬\ÌVëkã®W«Õ²pÙ* ŠËèhk¦jßΘe{{: ƒ(ŠBfN>í-cÖ™[@wg[̹ÔgYà#â!ýDÄCú‰‘zj> öÅG)üȘ’³™ÿáèÁÛÛŽÖdÁèH ïÔ0Ôœ4ZCË¿þlÌsFG«î9âXÕ¾IomtÌuìõß`t¤“X~Ù~’Œó®ÃÝÕ„¢ÕcJÎç|®}á§ôÕVèQ¬v«/¾ €}»¶ÒT_”¦¦g±låZzº:hi¬Ÿ°NNOAI9Åó*0%˜h¾n†Ú¡§¶µølÒ(ZÌé…lɨÁîÎFÜÝÍc–ŸS#ÐEAc»IFƒF3r.V9{ˆMo¾HAq9ÅeX¬vRÒGòú|^ŽÖP¹wǘ›Ÿ ËÌÎ'3§U ÒÖÜ@íá*Z›êãžf xoÃËÍ« °d>«-,V; f :ÇÍ€«7b±¡B!„85æT}² ¸ú&L#7–îÎvº;ÛO^[úûèŸZ[„B!fÛü[$!-Ò×µ~ðÿ8-š:  …B!ČәíSÊ­5šg 5Ó#´B!„˜q‡Ÿ{EošôuÞþ®hÍôH-„B!fÜ`ÛÑÙnÂI#; !„B1 @ !„B1 @ !„B1 @ !„B1 @ !„B1 @ !„B1 @ !„B1 @ !„B1 @ !„B1 @ !„B1 @ !„Bˆ)SôF´fÛl7cú4´f;ŠÉ2aQÝ)hŽB!„˜CrËIùÐg°/½½#ÕïcðÈ.ºß{ŽÎ·ŸD øg䵃‰â¯ý!îòO|›ÁÚ=cž7/%ý#_¶pM8xöõ´Ñ·óuZŸÿÞΦ¨k$€>EE‹ªQUuÊuh44 Á`ð$¶L!„"N…¬ƒ´4‘4:=–y+°Ì[AÊŸæè£_ÄÝ|øä·AÑb™·<îâZ³}Ìs)—~’œO}'ê½èi$_òIÏÿjý"®}ïDœŸÓ´Ùbeþâå˜Íº:ÛÙ¿kë„ר‰,Y~A\õoßü6ƒýcžOJI§°t>©éY&TUep Ÿæ†£TWîÁçõÄõŠJ+ÈÌÉ'ÁlA£Ñàq»ikiàPå\}=qµU!„bºò¿øÏ¿×Þ´¿ö[ì&è˜Q€såGHýÐg0faÊ-Ÿ‘:èâз¯8–qí]Ø]ˆ¯·£?ýBÄ9w㡘õ8ν2<÷íÙ@ës2T_…¢7b™7ß‹1-ŸÂýoª¿uMD=s2€ÖhŠË*(¯X†Vz‹~|ôzI)é– ƒ¸‡Ç<¿`ñrJç/ÿîõ¸ÑêtX¬6JÊ‘“_Ì»ë_¦ßÕ;fY9,[µN€ÏçÀh2‘[PBv^Ûßß@SÃѸޛB!ÄT¥\~[8xn~úaZŸÿEÄù¡c• «¤kãS$ä/ gÛË3Ó5È@ÍΈC~WWè”Ïu.Eo Ͻ;^§ö§_€ã³‚žAz¶½LÕʾ÷ zg9Ÿú.5ß¿9|ýœ  “’ÓX²b5vG" tejk%ß~ýyü~_Ìsj08æTŠ¢y ÂÁsMÕ^UíÅëq£ÑhHMÏbÙʵ˜̬Z{9ë_ùkÌzœI)œ{þE(Š–ÖæöíÚB_(ضÙ,Y±šä”tÎ=ÿbú^ù븸B!Ät(& ™7Ü @ßîõQÁón‚g· IDAThž¶:Bîg¾µ|æÂÅá¹Ôs& ‡N§gÉòÕ¬½|vG"î¡A>xo=mÍ S®sÀÕG_oÌÿ ô»b^£Õ阿ð\Ž®bÿîmx=nTU¥­¥‘-›ÞÀjs_4/f= /GQ´ôõv³uÓáàÀÕ×Ãæ·_Ã=4ˆ¢(”/:gÊïQ!„b"ÉÝžKÜòìOf¹5Óç\µ€þêmxÛŽY®ûý¿…CÚ—^>>gè`0Hjz&ªäðÁ}¼ùÒ34Öמòvdfå¡Ó‡¦\9T³LOWm-ä”F7%˜IMÏàhMUÌj¿ßGÝ‘ƒ¡×ÌÎC¯7œ”öŸéL&Ól7Aœ¤ŸˆxH?b„}É%x;ÇÍhq&P &ÌEK8¸mܲA÷Cuû°–Ÿ7RÇÌ5ïÔ ìØ¼‘ ¯þ}»¶Ž9õb¦Yl¡»³` €«·{ÌríÍ8“’Q´ÚÈ:¬#«E{»;Ǭc8W-Τ”)·y®°Z­¤¤$ãt:g»)â4&ýDÄCú‰£h,%¡§Ýgzð 7Íñõež–#–.c.\>6gh€®Î6úÆ ZOƒ!4b1QßïêB GÌ£1ü³oœzFÏ{¶Ú“në\£ÓéÐétX­ùÒc’~"â!ýDˆ:«s$?rgã,·fútöäðϾîÖ Ë—QL–pà=çžlg6G"F“ ¯ÇC{kÓ¸Ù7¼ÞÐ|çáÌcÂîÄéÃs¦'ªÇçõŽÔa)==¡”~N§«ÕqLˆaÒOD<¤ŸˆXÌ èõz|>^—@ pJ¯Ÿ­z´–‘AºÀPtúÞÌOÄtØl6ì;ºãéqûzûèîŽÿ©÷t¯ŸÍz4ÚñÃE5è'!o~ôq¿7FéÙ§ÑŽ NªÁ‰o>"Êÿ[H=EQ¨;r¡Á´ZY¹Ø‰Í«@§7°së;Q×´67àóyÑë ”.XÂö÷7D•É/*£|áHæ ¿/rš†Ç㦽µ‰´ŒlŠJp¬öPÔ”ÄäTV®¹<ü»Ï7;s¾gZrrè&ÁíšôµÃ_C_‚î‰/g,=4-#câî'’~röÏ1§í¨5K6»mRët¯ŸÍzF:+Æ„¨óëÿHåûáß“ÖÞHÒšë'ݦS%002V1L|C­èGÊúC7Ñ@Ÿ »«×þþ$Á`Ï ²•{·³tÅò KÉ+,¥îÈAº:"G‡ƒûwmcéŠÕää¡Óé©;\ÅÐÐ f³•¼ÂR2²óèwõ†ç-o2Úþ][IùÐǰØì\xù:Uí¥¯·ƒÑDFV.Åe ôc0„FTü1ê¡/½x7Ñg/é'"ÒOÄÙ*0ØþYgKŽ:ïën˜K<:[ÅéÈ?0ò$)Öû9ÑðœiÕï%è ņ@Ÿ  248󜪪ìݱ™ì¼B´Z9ùÅQ4@Ý‘ƒM&ÊžCFV.Y¹uTØ…ªª”U,#ðÇ|½¾Þn¶lz“åç_ŒÍ‘È9«.Œ8ßÒtŒº#Õ¬:> íê››©tv†²´´L<ɘÕj ?nmii•/¼³€ÚšN%ýDŒG>OÄtôöôFLypõÅÞb¦®ŸÍz‚î¼R²1fNéõN'žQÛ‹Òó',oLËÀÝP>&ô$ùý>º;ÛIIËÄ™8vê¸ê»iª?Jna 6»F¡¿¯‡cµ‡põõ°rÍeôvw¡ÆØý ­¹7^xŠÜÂR“RÐë ÐÔp”ö–FŠæU¡ ¼¯gìtwg2'´Ø2ÞG¦'~ÙÉ£Ö³ÄñLÒOÄxäóDL‡ËåÂðG,º;•×Ïv=ƒµ»1¤dc.\„b4ǵøîtåënÅÓzczA8=ßX4Š6œ3º¿jKø¸ÐS0œ%cxúÄXú]½TîÙuÜ”`&=34*ÝÖ2þN‰^¯‡Ã÷Å<7¼‹ag[Ë”WàÎ%Ãy[A¾ìÄØ¤ŸˆxH?± 1ÄäçП¬ëg³žž-/â\q5çªÐµñ©i¿þlrí݈1½ÛÂ5h-ŽˆyÑ£Ù]Ná×·gCøøœÊ}ª(ÇW`z}ž JÆVV± EQPÕ uGª'¾ †œ¼"ìŽDjÇÞñðl3üJ¾ìÄx¤ŸˆxH?"RïŽ×ñv„r@g^÷UtŽ3{·¶—~ð£HÿØ1Ëh-×~€Á£{qíI!ô8œ¡Àu°òóŽ KÊ)(.àPåÞqsJÅ™”ÂÒ•kèlo¥¹¡nÒuÌE===444Ê——ôé'BDRý^~w¨*ú¤LJï{ ë‚ "rñj´:Ë?ŒãœÍbKããíh ãõÇHûðíd\ûÃH†½3‚;~‰¹h j0@ó“E\/S8N`±Ú c.$ÌÈÊ%Ál ±¾6îzµZ- —­¢ ¸€Ž¶fªöíœtû ŠËY¸l%Z­÷Ð Û7¿=æ곑,ðñ~"â!ýDˆH}{6PÿØ7Èýì÷1fRrïñ÷uâílDk¶cHÎïÔ0tlfž+& ‹½?æ9CJ6KÿïhıÃ?¸×þMQe›þü} )98–_IƵ_!íªÏãi=ŠFgÀ˜QÎ]ÿؽ¸ö¿q­УXíV_|ûvm¥©¾6"8MMÏbÙʵôtuÐÒX?a:ž‚’rŠçU`J0Ðx¬–[6ÆÜˆ%FCVN¥ –àp&¡,›7¾ÎÐ`ôŽ@B!„3¡óí?3X·¬›¾ŽuÁèìÉ[c½núv¯§ãõßE,º;©ÁGñeÒÖ}‰Ô}-‰„üŠðywÃAšž|ˆ¾Ý룮ÕdddŒ;|9œx¾¿ÿôÔ,^NqYEÄ1FA£Ñ ªjDÀ:Ðïâ­—Ÿ(k4%°rÍe$%§¡|}Ý]ø>¬VV{(oóà€‹w׿ÂàÀØS82²óÈ+(!-3íñ;˜~ûwm¥¹1¾)g%ådæ`4†’xû}>ªìâpõ~‚Áøð3Yaa(]Nmmü£ýâìóúµ/pÅ_¯šå–ˆÓ™|žˆxH?‰ŸÎšˆ1»ÅAÐëÆßÛŽ»©5pæ=½Ñhu˜rËÑ'f@À§¥OÛØñÚœÖ( Š¢}N£A£9«œÇ=Ħ7_¤ ¸œâ² ,V;)é™áó>Ÿ—†£5TîÝsó“Ñ2³óÉÌ)@Uƒ´57P{¸ŠÖ¦úIM·°;“ÂS>z»;©;RMýÑš¨] …B!N57þƒ[g»'…ð3ttCGcg>;Ñœ  ÷ïÚÊþ]Óû‡TU•ÚšJjk*±Øì˜ÍVEÁ=4ˆËÕK0ÎtqGWÑÑÖLKS=>ïÔ²u´6Õ³o×VšêÆíB!„§Îœ  O¶W®¾‰ ÆÐÝÙNwgû´^¼ÐB!„g’â¯?ABNÙ¤¯ëxó÷´<÷ÓhÑÔI-„B!fœÎ–4¥üÑJ‚uZ3=@ !„BˆW÷ßw¡5&L\ð¾îÖhÍôH-„B!fœ»¾j¶›pÒÈN„B!„BL‚ÐB!„BL‚ÐB!„BL‚ÐB!„BL‚ÐB!„BL‚ÐB!„BL‚ÐB!„BL‚ÐB!„BL‚ÐB!„BL‚ìD(„B!â–“dǤ…õ½xüŠÒQ4·v¡ÎfO  …B!Dܾxù *rÒø×ÿ{‰c½|ç¦Ë0ôU•ëòçYnáÌ“)B!„"nƒ^ßÈÏ_ÌÿŸëd:Nz½UUñû§Ö1E‹¢(S¾@£Ñ Óéñù¼S®C!„b:ÜcÐ6ðœ™1Ê—._AºÃÊûްé`Ý„åçtm¶X™¿x9f³…®ÎvöïÚ:©ëmŽDæÍ_LzV.z½{ˆæÆ:UîepÀ5îõŠ¢PR¾ˆ¼ÂyX¬¶Ðõ7MõµÜ¿ÛW;ròŠ(*«À™˜‚F£!ðÓÖÜÀÁ»éíîœÔ{B!„˜ŽAo(HVU:>=püØÀ:}ù¢b†­‡ã*?'hF¡¸¬‚òŠehu¡·è÷û'UG^a)K–¯FQB³\<î!´:FSÅåø|>ìÞ6æõ:žÕ—\…3)Ÿ×ƒßï'Ál¡°d>™Ùù¼»þeú]½ã¶cñ¹PXR@ àÇãvcJ0“™S@zV.Û7¿MSýÑI½7!„Bˆ©’‡¼¾ðbÁá‘è3uz²æ\”œÆ’«±;ƒá 8^é™9,[¹€ÆúZìÞÆà@?«¼¢yªÜ3nK–_€3)…`0À®mïR´æøõv–Ÿ1ΤV®¹Œ ¯>G0ŒYG~Ѽpð|¸z?•{·ðûÑë ,\¶Š¼ÂRÎYu!}=ÝâB!„'Ãàñ yxÔ9t,¾9Ð&½{‚?@ï`èI¼¢Ñ0?;•¬Dª G;º©i銻=IÖ SI±™òù©ï襶½{ÜkÌF=V£!ê¸Õd Ín‰8æöùéòD›3´N§§béJ ŠËp ²oçrò‹ÉÈ΋»­VÇ’å«hj8Êï­8?Ðï¢rÏöqëp8“ÈÉ/àÀžÂÁsèú>ŽÁ™”‚Íî$¯°”£‡FÕ¡(ZÊž@Kã1öíÜ>çóy9rèy…¥hµ:Ê.cûûâ~B!„S5#X^X8Ñôª’¾rÕùì=ÖÊ¿?³ž-*æã,Âi6E”Ûs¬•‡ž'þûÍmì«o‹y퇗”òk–D¿å‚EÜrÁ¢ˆcÏ}PÉãwE›3t0$5=U r¤úUûvâ÷ûÂl¼ò KH0[ìÙþþ”Ú’_4¯×CmMUÄ9E«¥¤láHÙⲘtFV.¦3ìŠ:_V±4üsvn{¶ñy=QåÎ6&“ wœsËÅÙKú‰ˆ‡ô!bÛv¤‘o=½>búoÛ«x¿ºž×`\u$Zø×ŸÇEó hèêã­ýGðú,ÎË`Av*‹óÒùäêÅüf}ìAK‡ÙÄwn¼”Üd ÊöÚ&šº]8Ì&–f‘“dç[×_ƒ{‡íµMQ×wô °¿a$¸NË×Ü㢫(¢lecGÔõs(€°cóFü~}½ãÛ'+§>{ÜC”Ž-=+€ö–F‚@Ĺ‚ârL fŽ®¢ ¸gb ¦3î¡Á˜uxÜCôtEþÃ9œIdfçS[SEaI9BZF6ÇŽL©½s…ÕjÅétÐß?@OOÏl7Gœ¦¤ŸˆxH?bl=nzZ"Ž5w»hî?¹Âh9Iv²íüeó>þ²y`h6õ_6ïçû7_NyV /(à×o¹)Ëg/>‡Üdƒßzf=‡ZF’*8Í&þýúK(HurÇ•«øòo_ˆšZ²±ªŽU#Ù6ž¹ëã( /ì¨æ¥]Õ¶Nåîêl›Vð¬( ‰)©´67L©½Þ€Ùʸqbà«hµ”–/¢£­…ÚC•áãgRT=ŽÄdº»¢ïzÊ*–áóy©ÜóÞã£ÎÃåÏf:N‡ÕjÁétÎvsÄiJú‰ˆ‡ô!fÞ6íæOïí Ϫª²ùP=£û S;r“\XžªãÝÝÁ3@Ï ›G_Ù „‚éËô¶Ï™è“Á‘˜ŒVú“ôvw¢( ¹%då`±â÷ùèîjçèჸÆÔ-6{øçÁÁþˆsEe˜Ìlßò6CƒáãV›#*`·¯gè„:ìÎ$2sò9¸>Ÿ—¡Á #6›cêo|Ž%r:X­–ˆcB “~"â!ýDÄ’`N@¯×ãóùðz¼NxÊ<Óן®õLÕX#½í£¦tÚ¨ó«Jrª*+cçl®mïæh{©NÎ+ÍåùíÑÓe§CèQ†çÖ^¶.œ†nXbr*Ååìß½•#Õ¢ê0Gž‘9ÉŠ¢¥tþb:;ZéhmBwYƒ1òîJ«Õ…y¯'rþ]yÅRü>‡«÷„ç=ŸXÇ\a4Ð\Äx¸ÝnÜn#&“I¾ôÎ&úÐÿI?ã‘Ï16› »ÃŽîxzܾÞ>º»ãê=ÝëO×zf‚Ç7’zXãü‚ìÐlæ׸‹·vQê¤(5FƒªÆš 25@b0„>\UUåÜó/ÂæpRSµ—¦†£x=nlöDÊ*–âLJaѲóèwõÑvÂȱV§ÿ<úN.¿84ú¼cë;ácªD£Ñ†;ï°Ñ¿®ÃîH$3§€ê»Ãóp <­nnþS&'‡¦¦¸§0}øñkèKPÍe{h6ZFFú¤¯•~röÏ1§­vd4Ôf·M*àœîõ§k=3!8A ë´„n‚»\ãÿ·ÜÙÉ6êuXŒzúÝ'/GõÜŒº¦hx·AFƒÅjgÓ›/ÑÓ=2y ßEks^™.¯X@kFÝ*©j(¸U-óæ/¦«³ö–Q;ܨ1.:á÷ÑwKeËðû}>¸oô‹„Û,¢étºIo¢#Î>ÒOD<¤Ÿqz°Ïßì›`ʉ/0²ÏF¬© Ó!ô(£G{·o~;"x¦ªAjkªX¶r ‰É©Q4þ‘:%ô•_4S‚™Û6EÔ¥¿ó œð<ú÷áM`ìŽD²r 8T¹'¼p0T‡îø5gæÖ™éì - hiiû«Õ~ÜÚÒÒ*_xgµ/ô!)ýDŒG>OÄtôöôFLypõÅŸqâd\ºÖ3Üǧxè'Š£Î»Nâè3HÁ?*íhÎ8lôB³Ù@ÎŬ×ëQ…Òù‹ééꈭí†ÐÆ('¶cxÅáreK ü‘£Ï€Þpü.Ì;7·ÎôŸGï#Ó¿ìäQëYâøºÒOÄxäóDL‡ËåÂðG,º;•ן®õ̆N× yÉRlæqË%?ßïöâóŸÜ’@2:ï³^oˆé-ùGÐ"·¹ƒ3šÌäÍ#Ála÷ö÷"Ê%˜G¶‰põE½Æà€ «ÍÑ”€Í‘HVn!5U{ñœ°¨p¸žþè:Î6V«•ÿßÞÝÅÄyÝyÿ>Ï 0¯Ì <°qŒ½‹6v²N£VÉnV»+µÚ—^T•Z¥•Ú‹¨R¶–"Y•*U•{±j´½Z­öbsÑínV©¢lÚ¦©ÛupmccŒÞÍë˜a˜÷Ù‹ ãÈ v &¿ÏGÏyÎá >šçÏ™sþÇçK¯qÔÃNÖ¢q"…Ð8‘|–BK,±±3"Eý­ÚÎãvsb†CÍuÔz\”ÛËrŽÙ^Ö^—NquøÓ¿uJ¥#wUíZ¶U臘_É#èZ'-Üêl¶^ ­äföV°goós3LŽ gÝW¾*÷s¾ÜÕË×\åžÌìó­fŸmvGfããý€v†/ ¥‡¬GãD ¡q"²u}Øÿ1ÞÿõÂ9ž;j¨¯HŸËqf`8ï=«-góø´Yíe  W‰„Ù¼ËU5µkÞç.O×Éd’P0wÍнO6 6·v`w8é¿v1çžšÚzB‹A‚ œòå͆oõ ÍÜìÏ Ökjw®ú™;øe;™ŸŸgddT;Y—ƉBãDdëž pæf:ˆþ‡/>ÅŸï®Ï*o¬òð½¿Àí{sœþ$à^Ïò,õóûZ¨õº²ÊªË9µ–p<`øÎ mûºØÕÒÆ­W3™4Vköwpo|$gý2ÀÈÝAšvcšó³LŒfÿÇÙìê[2÷æ3>r—ý‡žÆb±’L$¸uãJV¹aìÞ³€ÙéIB‹Á|Í|îhƒBãD ¡q"²uýâ·=4ù¼4T–sâoÿ‚»ÓóŒÏñ:l´ÕUa3 !NýúLAùŸß<{•?k©Çã°ñ/ßø+†¦æ†£T¹4Vz¸twœ“¿ú}æ~Í@?`èÖu‰8Nw9ÍdÒ€tк¯ë0.·‡d2Iߥ¼mLŽ gŽñ6€2›=SVRZÆágžÇb±‹Es6.‹D ݺ@2•Än_Y3mOz:s|÷«¹3Ü""""ÛÕý¥ÿüæoxïÊ Ñx‚]>/_hm £ÞG*¸~‡×Þ|ѹÂöˆ Ïxý—¿ãÎÔ‹ß_ÏÛoŸãg?û¯ÌuÐ""""R´ãÇ»8qâë†Áèè4çÏ÷,âõ:ñûë©®ö000¶ÙÝüL(€.PII)©TŠx<¶¡ú¦iÁ4Í ×Ù*LÓä•W^Æ0 λΉÿF*•ʺÇjµL&7©‡Ÿ­m@;œ.öèÆáp2;3ŵK狪ïöTж÷;ê))) ^b|ô.ׯZ\X·¾iš´vÿRf9‰ˆˆˆÈ“䨱ý¼öÚ?ðòË'XZŠäÜÓÝÝÎÉ“ßàk_;ÉììúKf†Ïçá‡?ü'Øu}j*À©S¿\³Þ¶  “É$Õ;êH¥’ܾÙÇ«‰Çc™@¶PM-­ØN’‰½În¨/ËK-¢ÑC·nd•™ ­íûWîõ·ç   Ó ÒWC4ázoOÞ{$—Íf#\äÚrùüÑ8‘Bhœˆd;uê;´¶îÀ4W¦ ò“o“LfÏ$¿þú¿sñâ-¶2ÇÉOú Õ\¸p“QÜnGvò£}“h4ò‡m@'øÓ¹?Ǹ˜Ûp;õ -@zö9^ÚP;ꘚ%™Èžþoöw`³;¸3xƒfÞ 6»ƒðR(ë¾àý=gßgfj’hDà…p¹\x½‚ÁEæçç7»;²EiœH!4NDr™¦‰Å’^»zïuC¸ÜIDAT9©išFv=9¹ñXìqùÖ·^¢±±šD"ÉÉ“ÿÁéÓW3e?ÿù[üàÏñã]yën›`væÞCÕ7M“ _ú¯Éñ%ý.))ÅáLgܘŸÎ*3-öt<Åô½ †®ÓìïÒK>  ÆGîn¨ŸWV««ÕŠË•^[¥‡žä£q"…Ð8Éõýï¿‘yÝÞÞÀo|€W_ý}}OVÌR]íåÅðÖ[g²‚g€H$ÆüŸtt4R[[™S[ÐËSQ…Å’þ•æf0M“ÆæVêš±;]Äc1æf§¸3ØÏ³ÜN÷ÊI<¡P0«¬yw;6»ƒ ü€¥ÐÊŽU—Û³á€]V,?à¼^z²&)„ƉäcwØ)))!‹DI$ŠKÓö°õ·j;O¢§ŸîÈ$™x÷ÝüYÕâñ‹‹ùW(€^Åf_IÉ’H$xö…—3iè–UTUÓìïàÚåóܾٗÓFY™-ó:YYošöì=ÀÌô$Ó“ã¤R) àtUÉVVV¤×""—a³ÙôÐû<)Iÿ£q"ëÑç‰< ·ÛM¹§ë'éqïî37Wø2…‡­¿UÛyRuvî 388^týÿ&‘¾C^dCIEND®B`‚fwupd-2.0.10/docs/debug_task.png000066400000000000000000000111701501337203100164600ustar00rootroot00000000000000‰PNG  IHDR¨„%`ùsRGB®ÎégAMA± üa pHYsÃÃÇo¨d IDATx^íkLUWÚÇÿvm8åRQ¤ZÊX°±F¼ÀòN-Õ¤‰u’—ëÓÄh?T‰™4y“IC˜öCíëÓ2Æð?”J?ØDªb;íÈ{,·0Ç*P¬{A;ÓɼëY{í}Ö9œ+^X–çgvÜk¯}Y{íÿyžgmö³÷¼¼¼¼ÿ€a å!õ?à ”1(c4,PÆhæ­«¾Àƒ$ÆXæy<(c,ìâ£a2FÃeŒ&H |ðšc3` Ê ”1(c4wG i+Q±e-rTñÞ“ƒµ[*°2Mg‘œ_oAÅÊTUbî6Ó*Ŷ[ìé×÷Cv©XY±ksU1”{öˆqÜ»IîZl©X)ŽÈ$Â4VlÌÄÐgMhj²&ïÍTîTfÖúS'Ýf:qâñåÉŒ©eÁkõ`±œŸÂåÏN¢Ã/fÉÂmLÆ?šÎa€ªd9I4kðÚË%ú>Dí/°*PÆÔe´êÇ'˳ʩÕ­8Ù‘&öñ&. !s•:NÈv©++P–gµ#^4}hEp;œíƒÚNík¹~kžÜ¹'Ú·Ú1&—=uÓš·ÛªoCµEàÔ‡ËÞçt¨­â|/O"/o±\ïKû‘%Ë…ïçD×à—ÀwÔÏòzd!û!k}ê¿ež¸†.aø¶µúƒÂ4ßqR¸ux¦ÇŸ¹…ÈÃetÙ–Á?€¡)RC*©+ŸÂâ‘X–•è¿‚kIÉ¢‹¢Ó kæX:4K“‚e[Ì\\)ŒˆR¾°b¾€5¸z IÆ7¢ë8Üv5;6> <)Üb–pò‚ò(q`õ“Ö¢µ¾kXüD¤H[œoO¨ˆ†ð\¶××* É *%ì(~às’PõÀ>)eΪ yIN:C \NäF-!§¦¸0u3Î+˜ þ›SjÎbñ*ûøb"7Q\¡•×Û®è?‡Ö¡LëüC;Iyy!b‹i}áŸÀTÜíL„1ŒÝT³Qo3 |Þ*lf&rlR,§Oöî¢P¼¼^À²ÄkÉî Šõã‹)b\­Cⱞ³­×± ÄXÇI¹\ Uó.S—½¸ü¨'áÛMÓú"-I7…˜T‘ #Р[.i9È´#xrwÂ2ƺ%3ö­Àòž{’ê¦ô}ˆ`~å]¿Å3†jÂs3¸¥“–*lý$ÆlÃF!‰šÕ™îîý"4òb2¯,n‘Në ±7š\ÏS’2sTĶÏ[?'¦ ’Îÿ%E((“ø Ú#uqáE°ý«JPDu4eÏ·?ú éö0.ÝÎDÉ ÅÖ:4-Tƒ.ªÓ÷!¶ù¡—÷1üoˆmì}Ê)Äv?-,†§¸Ùói°óˆ4,Â?ûA¿>h AÉ­%%(ëÛmH·PA„·»ßÍÿJJЬíšÂ50®I¿±—gNÁÛrÃbÎ ixXöÏ2ÏjÙç«ãÇüe«±Zô­lËPH_Xƒµða ’‚ÏWß_AÁüÐ=ˆéú É´ZP;S&ÂõÙL»Í´k×.Ub˜Ù'j Ê0³ ”1NšcŒ†-(c4,PÆhX ŒÑð›E£á÷ƒ2FÃ.ž1(c4,PÆhX ŒÑ°@£a2FÃeŒfÞ/_øo¾Ê [PÆhX ŒÑ°@£a2FÃeŒÆ¾s¼W¾Óñß©%÷—݇ çð‹ªÄ˜BT’hî좽ˆ?Ÿ¬GëïU1ÛbSÎU4”T!kÛ‡j!Øäâ'üøFÍ2ŒMÄõdùÖ&«ȺUc¿˜#WøV±zÉÊÀé€ÅV°gß ¤È­k¾Ø€'eY0у?TÔàˆ*:ü¾W^zB€‹==X²8d¯+ëá_–‹."«i1z^ÓÖ“Öz3ð§Ýxõ¸eý7]?Îü ê<¦pîU'ÑÛ+ÚÖàËÅ&ü+ö|"«3ˆhA_­¨BÃ0ÞÞ€,Moå÷ãäŠÅÔ€ *øZ÷å¢óµÜZÿC”•4àÜ„ÝÇbY8qoW#ëPÆIÀbÛ²_£¹(ßfUï~\´a" ÙªüNѸØ_Rü¬¬jÓÇ~¬ÝW‹wdMH{ÿl²tŒQ$àâ……Ú Äq: ´ýW‘²ˆ¬ßUŒL$!c™µ<Î`(â€è|{# E¥$üQžtúà”³^Åù·åŠ1oÿKÀb¾}ZüXžÀï>ü,žø{ îx µ¿¾‘1ƒ„cÐ'_ÒF®yábì¢zµâ4 ê¢ ŠöoSV‹¦"GøÛžAÑ‹xµ­°Ë˜i¬JÂW³‚ñëWÕc2 ”â8M`49®›\:-#¡ÁŸ•Kžo_ÄÅœ'ñβ4 “;?~ ê ß×áC…˜õÂÈ%«dYþ˱‹7‘ú N W»öµƒÂbF#~wÚG6mHS‚úç¬rg›Äh“µXõðfmPg‘R¼AÅœªý8%Üúa‘Çsž üˆäm.5ÏETîoêŠ+ܶ5¸8²g7n¬À[¶‹wܹt8Ë~‹"_Êdœ(\ÿé«VXp2–°u¬Cб¿Ó”d?¾µãF!Úãíâ³Ï:î>ü]ÈtÆÛýX£ÚõV± ¶µ1ç 9hRm~ háÔH~¶ÏƒZ·™ø¶ÑNƒ$†¹Ÿ°@£á”ÆhØ‚2FÃeŒ†ßnÇ [PÆhX ŒÑ°@£a2FÃeŒæ® ´´ú(ŽV—Êùª?6:ó"t.h¬;ˆY9‹íµhüc•*Ì]Ø‚†¥ •Å€÷ðVlÝYƒ6µ”¹ÿ°@#2‚+gÔ,3kĸQ_…ÚÆJ¸å|š[€Ê§»°õ@½\‚õqt.šŸð¢Ù—rÔaGm›tñå£ÍðåWÂ#$žijÔE'º·Xn ô7;ûŒ´ÜÚŸ#Ÿ'úÑ\üçÌVT“ÕV[¶uÒâéíÖŽ+Û›_{<â}-Úö6ú9 &ûûôÔ~äz7Í[«![FeÙ'Ç,D—½\@m.ì¶Ž«?böõ%Š-ÅÁºJ@\Ä­[iêBá&«û,D‡îÉ‡Ü Õ‹‹Pn Ká*.Ž©ú–xöÔŠ­â¢îÍ÷á}¹_1Ù!dy3*ƒâXWqš€¨ÛyõMÂýt F+-É|g…ˆBÚ}؇|û¸7òňꦉ“8Sƒ[›…D„HÄ:;übX·Þª.ÍÒšÈ@–*W=íF_w|"ŠØ1ûzîY ÛÅ/Â*:±Õ-}jž,]9Üý§@\غöIU°˜l¯ Ôk†wÂBaåÐçÇdrš²6â"­[ìÇŽùê»ûàJ¬5ÙÞìX§¶/|˜LÏR˜R¬#}~!¶ m÷™³ði‚"ëtJXx²lr0DSØAI®Œº_BGRÇñÁ)g¥÷¡+œÐñ?bôõ\&z :zÅK8&GéDºÐj–¬¹0) ݺ û¶I YG„!ðlËFxTû±${°×ÞGã^áR]H‹`êØVKLÜ©óCY¿ù£]¨?Øeø…äg‚ÖDŒ¾ž«Dhˆ8J33Ôœ…nÝwz°‹†¬Í$üöÕÄÖÇ˱.ôå¢Ê†:Ι+Qe+¬˜ !ý£¯ç*‘JEX¢JrAºõ t±¹åÂ-«bpQž«æ®âJÇ:–Vïṅ³Î @á¸{Kpžíñßw¤6@ : u7+ÅT‰Z§Ýwƒ>øE˜P¾>C ª]ýVY†ZG¹lBÄÓ•ñöGŒ¾žËD± ":ìE†ãrÅU‹¤›¦@_Õ‹Î=5-õ£P¹Ú½Å#hÖGÂjy£hY#ß¶ÚhÕÝsct¡I7ï†[¸Ý€ý m·˜îøf»õãqi?°úõ[Qm"Äè „(bÞܯªû#V_Ïaz”níT·‘˜Ä°n3½Ï}— ÑcPáÂw«‘2ÃÜ'¢”nÛ.GL{<iÑo,3̽‡S>£‰ßÅ3Ì,ÀeŒ†Ê ”1(c4,PÆhX ŒÑ°@£‰"PúKÒÑÀÓJ aýÊ~Ѓþ9Ã“Ö ~&4΃ų’éHO¼Ï´/˜;áYÐzTßé3˜:òQ>+c®çèÌ5?1Ó'×™™ØuÄQûý¹JÊ~ zÎRwƒÑÃ=(æƒÅôì(eYª4ŽÚÚàÈ,hí 2…öÿAíUíÛáÜr¿ªîhu„|æžC .x„ÈêTÚ„|˜øc@ºð•¤dt=m§ÚF€RC{19á•ÙžÕÕ*ýBU»Ó)QÄNÀÓ’çbåÜB²KëP®R…™ûM NÂ{L˲üXEGâ¨7v|¬=ÿ~€R|m¬ÁUt몧VT¡0Ýߨ]v ©†I+ K¤s›ž]ÚV[oÈ7˜˜ûCb1(%‹©Ù™íÖàʶ®áY”~1‰ŒLá·"Ãw5Ý}NÙ=ÓìÈ sÓ’Ù˜Y%1®ÏBÆVôuíSÍÆ‹•,·U™Ê÷ùU9#î—(L#èÜBÓ”…ef?+ÄŽA_²ºp}Û=4[²8ÉÚ7(K1æE¤lH}ŸÂI¿xÍLܨcÓ§º¥5V9òåù#NvgÛÐ\B´ÖÀÇj{È{O"œ›e¡ÝbPešäK*Ôs4åƒcå𽼋1‘ÄbP†¹Ï°@£á¬NÆhØ‚2FÃeŒf^ii)»xÆX8eŒ†]ÊJÙ“ýͬË35¨q20ûÐl¿žFnc' ©”c-Õ×JËä5̈́իWcppP¾ÐáÝwßÅùóç¥8ssså2šFFF°aù~jj*¾ùæ¼÷Þ{èëãÔMÓ‰îâ#|”•¾#߇dC>˜:-åØú’p¤½ÆÃ7°|ùr)J›… "##ñ TŸ’’"ëÆÆÆÐÙÙ)çó‰.Ð(e ýl|„¤ çôAÕ0ŒŽŽâá‡Frrø”ÑÖÖVi%Ébê.ž¬¤mAi:zô¨\Î’ë0æÃσ2F=e˜Y†Ê ”1(c4œÕÉ â£aÏ ”1àÿÈ#‘Œi.ÎIEND®B`‚fwupd-2.0.10/docs/debug_tool_selector.png000066400000000000000000001244061501337203100204020ustar00rootroot00000000000000‰PNG  IHDRÐÂÆ¡þ‘sBIT|dˆtEXtSoftwaregnome-screenshotï¿>-tEXtCreation TimeFri 05 Jan 2024 06:10:33 AM CST;l{ IDATxœìÝw|TUÚÀñß”ôÞ{#…„ ¤A€@–^E@D\@q—]wUT^] ²ˆ ÁŠ  ‚ˆôÐ¥‡@HiBBHi3ïa.fÒr¾ŸOþÈÜöÌ̽3ÏœûœsdÎÎÎjAAAhy[ ‚ ‚ ‘@ ‚ ‚ B#ˆZAAA$Ђ ‚ ‚Ð"AA„F ´ ‚ ‚ 4‚ÒÎή­cAA„†hAA„FPæçç·u ‚ ‚ ‚ðÀ-Ђ ‚ ‚Ð"AA„F ´ ‚ ‚ 4‚H AA¡D-‚ ‚  hAAAh‘@ ‚ ‚ B#ˆZAAA$Ђ ‚ ‚Ð"AA„F ´ ‚ ‚ 4‚²­áÏÃÏϯ­CAéüùómÂG´@ ‚ ‚ B#ˆhAZ\III[‡ ‚ ÔÃÌ̬­Cx`‰Z„‡ŽæK£¤¤„›–Þäúá†]*…aG&‚ÐzäUåXäŸÄ9u¦EémÎM$Ђ <´nZzs®ë+"qá¡ Rrݱ37ìBð?0³ª+mÒK$Ђ <´r}‡ Rb—µ×”ïP–ßhëAZM¥¡Ù£Éw‹'×w)ŸµuH,щP„‡Ö »‘< ‚ðPP–ßÀ5å;àÎçŸÐ4"á¡¥)Ýɳ  Íç(]k‘@ ‚ ‚ B#ˆhAAážX´h®®®Z]¾|™©S§¶QDM#Z AA„Vgdd„§§'J¥’ªª*ªªªP*•¸»»cccÓÖá5ŠH A¸o˜ “ÉÚ: A¡™âããY°`>>>Òc^^^Èd2:ÄðáÃ>|8‡ 00PZÏÐÐñãÇÓ³gÏ{vƒ‰AÚœ¡™!íûùÐÇëYEìÿü7rEÇ>A„Q=xñÅ‘ËåÌŸ?ŸåË—³fÍ)™NNN–ÖMNN&22’   öíÛGpp03fÌÀÍÍ •J…Z­fÇŽmõTj¥077­­ƒáÏÁÖÖ€ŠŠŠFmÐןСÁ( ˜ÚšâÛÃU…ŠüÔk­&††Õ½Ï3<à’º®UŽÓTO=õݺu#33“7îüðôôdüøñpüøñ6ŒðáåîîΓO>ÉÅ‹)..ÖYîèèȘ1c2d$44”›7o’››ÛѶ&OžL×®]9pà@[‡Ó(#FŒ wïÞpíÚÏ GGG&MšDnn.ׯ_oÛ/×o(ž?h=ÏæêÙ³§”<—••ahhH§NèÔ©¶¶¶xxx°jÕ*._¾ €R©$!!|}}™:u*–––¤¦¦bggGLL ÙÙÙ\¼x±Åbl ¢„C„ûŽÂ@A§ÑaôþwO,œ-Ú:œ{.((ˆ°°0LMMµ·°° ,,ŒöíÛ·QdÂСC ѺݬáïïÏœ9sˆŠŠÂÂÂ…BAûöíñôôlƒHÛ–‘‘aaa„††¶u(æççGXXÖÖÖZÆÐ¡CÛ(²û_Ïž=™5kr¹œß~ûqãÆñý÷ßSUUE‡ˆE­V“’’"msöìYÔj5~~~<úè£0wî\fΜɚ5kËå¼øâ‹ÄÇÇ·á3ÓÕ"%aaa:u‹7oÞ$''Gï/tooo¬¬¬HNN¦¬¬¬Ö}¡P(8yò¤ô˜££#...‘––Vgl¾¾¾˜™™5ºµÆÞÞ777Nœ8J¥Ò»^KÄ¢ïõ+**"++‹òòòFÅ]3¦šªªªÈÉÉ!??¿ÞíÛµk‡…EÝIKyy9gΜ‘þ×÷ ¸rå ¥¥¥z÷Ñœsàn2™ŒvíÚáé鉕•ÅÅÅäçç“––FaaaÏE¸¿¨UjdòêsÉÞÏŽGÞHäÄÚSœýåjµº£f„††RPPÀîÝ»µ–Éår&L˜€¡¡!ß}÷Û¶mC&“áààPëg ð`Ù»w/=öÁÁÁi} Õ¼¼¼Ë夥¥ñþûïðÕW_±{÷nf̘¯¯/§OŸÖº&JKKùã?èÔ©[¶laéÒ¥”””°nÝ:ˆ¡¡!ÇgçÎmò¼ôi‘úµ×^“n‰Þ-55•eË–qèÐ!­ÇÇŒC\\S§N%33³Ö}Ïš5 333ÆŽ+=Ïĉ)//gÆŒunÿä“OÒ¡C‡Fÿb|ê©§èÞ½;/¿ü2üñ‡ÞõZ"–Ú^?µZÍåË—Ù³g+V¬àÖ­[ Š]“>¥¥¥\¸peË–ÕšN˜0ˆˆˆ:‘““ÃäÉ“ë}GŽáÛo¿Õªy‚æ5ÅÄÄðÌ3Ï`oo¯³L¥Rqüøq,XÀ•+Wê|NÂýáì/)TU¨èðX 2… …auk´{¤›¨ÚÔ€Õ%??ýô•••ZËœqrr¢  €mÛ¶ÕŸáyyy÷<Îû««+‰‰‰±~ýú¶§ÁT*?þø#&L`À€"ÖcÅŠÄÆÆâããCTTû÷ïªsÁY³f¬7÷xå•W°°°Ð*Y˜:u*†††TVV2þü{òªÅ:æççóñÇKÿÛØØÐ¡CzöìÉœ9sxã78xð`K¨®cœ5kûÛßjm!n sss¢££ÉÈÈÀÓÓ“ž={Öš@·T,w¿~xzzÒ©S'† F=˜?>ÇŽkð>×­[Ç©S§066ÆÝÝ///¢¢¢x뭷ضm~øa­õª|ðA­wô%óW¯^eÑ¢E( lmm‰ŒŒ¤K—.1{öìz[éÃÜÜœ™3gCZZ›6m"##ƒ¼¼<,,,°··'$$ Zì¸BëRUª8±ö™‡³ˆzº ÖVÀÖèãkN‘²E´F ÷–”——³oß>åÎÎÎ@ËÖ’þYÙØØÐ½{wrss¨`ÿþýŒ9DÃÌ]ÊËËY¸p!óæÍcÚ´iœ}ú4:t`ذa|ÿý÷-¶ßøøx X¼x±” }ôÑGu–S47}¯€£FbäÈ‘üóŸÿdêÔ© î¼’’¢wŸþþþÌœ9“Þ½{“——Ç7ß|£wûC‡5ªüáÖ­[:ïñO?ýÄO<Á¨Q£èß¿¿”`·„)S¦òeËX³fN‹Àï¿ÿÞbÇî­‚‹…lymÁ $h@ r……¡‚ð1axt­Ñ½‹L&ãèÑ£zKÎ Z´1G¸ÿTTTpäÈââ∋‹kðüüü8qâD³Žß¹sgŽ?®÷ûî~qòäI6oÞÌ£>ʤI“X¸p¡Þõ4eŸúCÌÌ̘6mP_­Y³¦õn¢VïD¸gÏ233qrrÒ)Èo®o¾ù†ŒŒ üq¼¼¼Zl¿‰‰‰pìØ1vïÞ™™‘‘‘mKEEË—/gÓ¦MXXXh•M4Õ¹sç˜3g·nÝbĈ­ÞÁeûöí:35GDD üöÛo¬\¹ò¾þ0šNU¥âĺÓl}ý7 2îü˜Ó´F·ïpߌmbbÒ¬m›úËøñ㛼ÖdccCLL “&M" €¾}ûêä~...¼úꫬ[·ŽuëÖ1gÎ~[>>>Ò¨N………ôéÓÿZKEÛÂ=:??www”Ê–=\EEï¿ÿ>ï½÷³fÍbÖ¬YTUU5kŸ°aÃT*»wïfðàÁôèу½{÷ÞÓXjúöÛoIHH G,^¼¸ÙCèäçç³zõjÆGÿþýùä“OZ(R]š‹àìÙ³-¶Ï‰'RQQÁÒ¥K[lŸÂý« £-¯o£Ã€@‚ÿtß´FwìØ‘Þ½{ãåå…©©)ÅÅÅœ;wŽÕ«W“@PPsæÌÑ»­‹‹ ƒ ÂÛÛ;;;*++¹té›6mjP2íïïÏàÁƒñòòÂÈȈÂÂB.^¼ÈÆÉÈÈл½½=‰‰‰„‡‡cmm\.G­VsíÚ5NŸ>ÍŠ+ô–t…††JÏÓÜÜœëׯ“ššÊ÷ßÏÕ«WuÖŸ2e ¼þúëØØØ0xð`Ú·oB¡àðáÃäææÒ£Gòòòjm‚ê¤ÃÅÅ…åË—k}~Èd2‰ˆˆÀÃî\¹Â±cÇØ°aƒÞâúbZ¾|y­qIÊSSSµ–ùùù1aÂ)9óòòbîܹÒò‹/rîÜ9)))á­·ÞÒ{Œ &àîîÎÕ«WùôÓOõ®3yòd<==Y½z5ÇŽÃÛÛ›§žzŠ‚‚©ÃÖÝÆ¿¿?7nÔºC¨T*™3gGeãÆôë×.]ºàììŒL&#++‹Ã‡óÓO?ÕY.Õ±cGðòòÂÌÌŒÒÒRΞ=«·ÌE.—óúë¯K?xìííµ^«µk×rôèQí¢¢¢ˆ‹‹ÃÓÓ333nܸÁ¥K—ؼys½uÈÍÙ¶6.\@­Vãì쌉‰Iƒ:‰þðÃ̘1ƒI“&HµÁ •À¨Q£(..fË–-MŠ»5}òÉ'¸»»k=¦V«¹pá‚Öl{{{Þyç­¤ºK—.2kÖ,rrr8uê'Nœ $$„ØØXbcc¥}fddðÜsÏ݃gU·VO •J%žžž\¿~½A#@4ÖùóçY¹r%cÇŽeÔ¨Q|ûí·ÍÚ_ïÞ½;·þÏž=ËÕ«WéÚµ+fffR-Ͻˆ¥¦’’Μ9C×®] lôŧÏÁƒ7nAAA-¡~ :”’’©%º¹lllðññ#l&&†qãÆIc.]º„µµ5>>>̘1£Þã;::2{öl©£±R©ÄÞÞkkk‚ƒƒYµj•ÎõæááÁßÿþwé ?--ŠŠ ,--qrr"00P'y–Éd >œ>}ú V«ÉÍÍ%''777:wîLpp0‹-ÒIDìììpvv&&&†¡C‡bnn.-+--åäÉ“Œ5 ggg\\\¤/Í»Ÿcxx8ÅÅÅZI«™™S¦L¡C‡”——“••…\.ÇÕÕ•>}úÎ[o½¥ÓÈP_LuñööF&“‘ŸŸ¯S§i``€¥¥¥Ô@¤T*±´´ÔŠ7%%…qãÆIïÃ¥K—´öannNll,r¹oooÖ®]«S[kjj*Ý =þØØX¦L™Â믿T'Ê/½ô&&&´oßžÀÀ@‰ˆˆhÑ»Ùõ‰ŠŠâùçŸG¥R±`Á­1Í[5622bÊ”)ØØØ°téÒVûb[µjQQQŒ9’ýû÷ë´4”\.'!!¬¬,Î;T¿‰»víbÈ!ÄÄÄð믿ޓXôÑ\4šÝæÊÎήs/¼ðB­ußß|ó éééZÙÛÛK'¿†»»;NNN|òÉ':_M¥¹Õ£oä¥R©õŨ¡V«øï…j…™×ÙòÆ6‚ú·'dPäÊ;­ÑNÁŽì|wý;i¦¸¸8zõêEii)_ý5‡F­V#—Ë fÒ¤I˜››ë­‡urrb„ Èår~úé'6mÚ$• ¹ººòôÓOãááQçñår9dùòåܼy¨N”ž|òI‚‚‚=z4çÎÓºFžxâ LLLøý÷ßYµj•V²ldd„Îq¢££éÛ·/ÙÙÙ|úé§Òg†R©dèСôéÓ‡ñãÇ3gνŸãÇçÒ¥K|ÿý÷RÒWVVÆõë×9wîþþþtíÚ• 6èl«¹Õ½oß>­­áÇӡCŽ=Ê—_~)=KKKž~úiéù×ÖŠ[[LuÑtÔ7Ê™3g˜9s&QQQ<ýôÓ¤¦¦òßÿþWg½ÌÌLÜÝÝ Óù, G.—S^^Ž¡¡!‘‘‘üüóÏZ넆†"—Ë9uêT 9edd„B¡àý÷ß'99µZL&£[·nŒ;–ÀÀ@yäZߨØXê½jR©TÌœ9“àà`žþy._¾Ì+¯¼Rklƒ&""‚ÜÜ\>ùäé;ÐØØ˜Q£FÑ­[7úôéCjjª4tKlÛ¹¹¹899áììÜà–ì””>øàžþù'Ñ=zô`ìØ±ñÞ{ïI×àýjîܹuŽÀ‡~(%× .$66–ððpd2™V®¨âN3ÃêÕ«km˜h O?ý´4¬ïsÏ=§•@·X ´&yÒü½ûî»|ýõ×ôëןþ™uëZo¦¯ÊÊJÞÿ}Ôj5/¼ðB“KEÂÃñµµÕéx¦ï³!s²·T,úhZ[[³X›ÒÒRÊÊÊjÝŸþþþzÿôµf( lll¤?[[[é¶æÔ©S™2eJ‹Ô«jŽ­ïK/00åË—ëüi~é ê*5§7%³uîoT•ß)•²rµ¬c«–addÄ!CøòË/9tèô¯R©8qâDçÛˆ#P(lß¾uëÖi%‡ÙÙÙ¼ýöÛRRX›+W®ðÙgŸi­WPPÀüùóIKKC.—3räHi™Ô7cóæÍ:-Íeee:É¡¡¡!C† A­V³dÉ­/îÊÊJ¾ÿþ{ÒÒÒ°··¯uØËãÇóöÛo³wï^òòòÈËË“~ÈîÚµ  ÖšPÍã5Ç[öôô$..Ž‚‚–,Y¢õü‹ŠŠøüóϹuë‘‘‘µ6 ÔSm4Ÿ9ÍI\5_¼;vÔYÖ¹sgÔj5«W¯ÐÛçF³]S’½ú¼óÎ;œ9sF:5 Gß}÷}úôÑš ¹×@C¸¹¹Ñ«W/ÊËËuZ]oݺÅW_}%•UŽ9Rë»¶9Û6”æ\hlÝ÷ùóç™?>åååLš4©ÎšèîÝ»óøãSTTÄ»ï¾{ß'Ï›K ïNž\]]133cÉ’%,Z´¨Õo«^¼x‘åË—ãíí]ëxÁõILLª{ÚÙÙIùùùÒ±cG½­4­‹>šV–*[°¶¶ÆÈȨÖ/3f0qâD½§OŸÖYÿòåË̘1Cú›>}:cÇŽeöìÙ\½z•¿üå/R‰Lsh>¸î®·ÒİlÙ2­¿úZ—„“­· Ñ“» 0¬ÑqÊ7|}}±´´$;;›#GŽè]§¶DËÀÀ€°°0Ôj5?þø£ÞuÊÊÊê ½¶þjµZj¬hß¾½”TVVJe }úôiÐp???lllÈÌÌÔ[S­V«¥ç¯©¾[RRR­×ß¡C‡¸yó&øøøh-óññÁÉɉÔÔT­¤!""™LÆáÇõî·¨¨HjUnJLµÑ$IÍ™EÓùÐÛÛ[+555%((ˆÔÔTvîÜIII žžž8::Jë( BBBP©Tzk„›«¶NØ»w聆°CCC­÷È××++«&] \.gïÞ½µ~ç­_¿•J…­­­TBÐÜmJs.4¥cbjj*óçϧ¬¬¬Ö$:..Ž'žx‚ëׯóî»ïê-sº½òÊ+<þøãDFF꽬9§M›†……üõ¯ªçŒ¨™+*•J¼½½éÙ³'O>ù$sçν'†kZºt)ÅÅÅi 5 -X¡Iž4ÂÂÂøÏþCbb¢Ô!¯&Íÿ ùå×ÐÎxk×®%&&†áÇ“””¤5Ud}ÌÌ̤“xÞ¼yµ®×½{w6nÜØª±ÔFS÷£)/i©ýµDlu9}ú4~ø!¯½öýúõ“Ê`šzdgg£V«ñððйÝsåÊV®\©µí AƒZâi÷ ¹BNð  i¢ÂK×ÙÿyË•©æ‡lSJ’‘ÉdPTTÔÒ¡ÕŸ*• ¹\Žƒƒ999¨Õj~ûí7HïÞ½‰ŒŒäĉœ8q‚S§NéM(5¥RjµºÖ¾šÏ}“Õ§¢¢‚¤¤$ˆŠŠÒã5::@g¶?MLÖÖֵƤIhšSm4ŸQÍiºrå iiiøøø*µ€vìØ…BÁÁƒ©ªªâСCôèуÈÈH~úé' zDΜ9SëØü­¡ªªŠŒŒ ¬­­µ’Ìæ\ ¥y¯kë Õw]òòò¤ZzMKss¶m(Í÷WSG,¹páóçÏç…^Ð)çˆe„ òî»ï>“ñdggãééIÇŽ¥»%jµšôôtžþyé;ü«¯¾¢S§NtïÞ]š¬ª/^¬µÏwÞycÝË×cß¾}z;ÄB+Ö@?~œ}ûöCÿþýùᇴ–knº¸¸èÔÒjÈd2ìììokÒÄäââRg<¹¹¹uŽÐXš$©f+lSrîÜ9LLLð÷÷',, ¥R©Õˆ¡V«¥[ù¸»»Kwáî~5e­9¹¦¢f'SÍyÜ”k ¡4Çóöö®u¹\.•“Ô¬•oζ ¥9šú㱦ŒŒ Þ{ï=nܸA~~>ï¼óΗ<×4`ÀBCCøè£š\¿téR®]»Fbb¢TÖu?iõq W®\IïÞ½yä‘Gؼy³”–••±`Á^ýuþñЯ_?’““¹qã®®®ÄÇÇcjjÊÂ… ¥‹µ¡Ôj5 )Ÿ¢› IDAT,àÃ?ÄÆÆ¦ÞÛ‹šÎƒõS¼k×.FEÏž=¥ÞÉ-‹ S§NÕúßËË WWWJKKY¸pa£OïÛ·/ÁÁÁ@u–››^^^XXXpøða-Z$µ6é3qâÄZ¿„ŠŠŠtƹ¶¶¶Öš)Q3ÓY@@®®®ìß¿_k¸ªæž‹/¦¬¬ŒAƒñÆo’’BZZ—.]ÂÜÜOOO"##Q(õ?(ÜdrYõPuƒ«‡ªÓh‹VçšÒÒÒ¨ªªÂÁÁèèèZïžèsãÆ ._¾Œ““äË/¿Ô»^}I‰R©Ô©ÿ×Дf:uªAÓJçää°ÿ~ú÷ï¯UçšžžNyy9FFF$&&¶Z©ÔÁƒ5j;wfõêÕ„‡‡“­U­‘’’Bß¾} ÄÏÏO§e«µh†tuu­õuo¨@XXvvvZåû÷ïç±Ç£K—.ÒcúÊ74·÷ÍÌ̤ïM¢w¢°ÀÀ@ÜÜܸqã/^”OKKC¥R5é€;‰]ÂΟ?O¯^½ˆŠŠâÇÔ[ÊЭ[7ÌÌÌÈÎÎÖºýßœmJÓ‰½¥ÆdÎÈÈàoû[‹ì«-¹¸¸0qâD :gJJJÒZnnn®·Ž_ÓQöüùóRÂ]RRÂG}ÄË/¿ÌôéÓINN¾¯î6·z}óæM–-[ÆôéÓyæ™g˜={¶´ìĉ<ûì³L™2EkàõòòrNŸ>ÍâÅ‹µ.ÚÆÈËËcñâÅÌœ9³Îõd2™4–e}éééÒXž¾¾¾ n nH,fff 0@ú¿¤¤„Ë—/³~ýzÖ¯__g¢[›ððp©Æ¨²²’ÂÂBNž<ÉÎ;õ¶îÜMóÃBŸœœÚÜÜ\«Ã^ee%yyydgg³dɽã]6ç(//çóÏ?g×®]Œ7ŽiúPµZM^^»wïfÅŠLf¡š¥«QOwÁ®Ý[°÷ºÖ¹6×®]ã÷ß§wïÞŒ3†²²2­È¢Ωv×®]˳Ï>Kll,………lذAJÈär9£G®·¥ÓÞÞž)S¦°téR­Zß„„zöìIUU•4$šFDD.\ЩT(RGš®ŠŠŠØ¸q#ÇgðàÁ³gÏäÑ¢Y5Ýeee8p€øøx&L˜ õEÑçØ±cœ8q‚ÐÐP&OžÌâÅ‹u’h™L†™™Y‹v¶ËÌ̤²²###ݨSÓáÇ;v,áááXZZröìY‘rrrÈÈÈÀÓÓSSÓZË7ŠŠŠ¸rå <þøã|úé§Ze/;wnÐÏ &pëÖ-­óØÕÕ• &Õõó5÷›ŸŸÏï¿ÿNBBB“®Íç± AAAzÇQ>xð }úôÁÛÛ›éÓ§óñÇkµ‡……1lØ0V¬X¡u^6g[¸sÇÁÓÓS§ª'å155E¥RµjGÊÑ´iÓ022¢¼¼\k,v333&MšDß¾}9þ<ÿûßµFùàƒðññáÚµk|ôÑGRâ””Djj*¾¾¾Ì˜1ƒ7Þxãž?§Ú´H=tèÐ:—ÿòË/üòË/z—]¾|™¹sç"“ÉprrB&“‘››[ç/üÕ«Wë|9è³uëV¶nÝZç:jµºQóÊ×l%n©Xê{ý«¡1Õ¦¶©‡ëÒœçДs ¦³gÏòÊ+¯`hhˆ FFFäææ6¹c“ж\Ã]iÿH ƒ;¥7mÝê|·~øöíÛãîîγÏ>Knn®4Z··w#Ë9r„ýû÷Åc=F·nÝHOOG&“Ñ®];ÌÌÌ(++«w¸¦ÈÈHBCCIKK£¸¸///¨ªªâ»ï¾Óºånnn.õ£ÈÊÊâÊ•+ÒðdØÚÚrõêUÅ[·nÅÏÏN:1aÂ¥©y­¬¬ðññÁÎÎŽ^x¡A­ÝµÙµkñññtìØ‘ÊÊJV«š–/_ÎÌ™3quueöìÙœ={–¬¬,nݺ…­­-\ºt©Î)ÂK3[¤flüæ$ÐÅÅÅœ9sFº;X[”¤¤$<==±±±!%%¥ÖáF¿ùæžþyBCCyóÍ7¥†OOOìííô¾Èd2ž}öY222ÈÊÊÂÞÞ”J%G•ÊljÚ´iíÛ·ÇÍÍMï5PUUUëgxaa!‡&""‚™3gròäIT*yyyÒw—Z­fÙ²eÌœ9ggg^}õU222ÈËËÃÕÕUjÞ´iÉÉÉZûoζPýLbb"öööäççk}§jj233[´³êŸÁúõë ‘Æ‘ÿâ‹/ˆŠŠâ¹çž“jÒýýýqww—úAYXXHå6¶¶¶¼üòËìÚµ‹O?ý”!C†àëë Ô=ªJ[P˜››¿ÖÖAhßÓ!z„ûOsΪª*JJJ¸~ýzƒ‡>Z–æ²±_*ö8W÷ì7¶4Öªu>ýC2IŸ ´ yuj244 óú®Kjã&z*//gïÞ½˜ššâää„ nnnØÚÚrêÔ)V®\Itt4UUUzÇ{>räEEExxx`mm³³³Ô²ùñÇSPP@PP{öìѺûäèèHtt4kÖ¬!##???quuÅØØ˜‹/òñÇK³viPVV†©©©Ô9ÍÛÛŒŒŒØ¿?_|ñ…ÎíQµZÍÁƒ¥é»5Ûµoß///d2û÷ïçüùóZ­IÝ»wÇÆÆ†Ã‡7¨¾´°°N:aeeÅ‘#Gê¼XZZÊîÝ»Q©TÒ”ÓíÚµ#  8räˆNÇãÆÆt7‚ƒƒ122ÒŸ»»;;wæÚµkìÙ³§Î}ÉårÂÃÃQ©T|õÕWzìçç瓘˜ˆL&cË–-zKZàÎðx~~~ØÚÚâââ‚‹‹ ÅÅŬZµŠ½{÷ÒµkWŽ?®u7O.—Kw=ÿ÷¿ÿIÓR{xx`gg'ÝYµj•ÞD¸¼¼œ={öè\ÖÖÖœ999¤¥¥GHHQQQ <ccc6oÞŒ\.ÇÖÖ–äädéœ £W¯^Ò¬¾¾¾xyyiÕR÷Ýw|ýõ×-cK‘9;;·þÌ‚ <4Ã5¶ÓHÐct¢õXk¶:kzßïîV݉6ü—'êZ½N2™ ŒÉÉÉ¡¢¢___þùÏ’““믾Zçö–––ØÙÙ‘››Ûèý …GGG ÈÎήuBŒš ±¶¶ÆÒÒ’ââbòóóüƒG©Tâââ‚B¡Æ²níI²êcbb‚³³3´è4×5YXXðÎ;ï —Ëùûßÿ^ïì…mA3‘Y^^^½ J¥RšâÅ_¤¨¨HJ†¯^½Ú¨²œš×@CÏÚÛZYYI3ã¶Ö1š²­±±1¦¦¦Jɳ……ï¾û.³gϾ/σ†8ÚoÝvWßUoéqqqÌž=…BAff&~ø!'Ožä©§žbÈ!¬_¿žÏ?ÿ€qãÆ1zôhÖ®]ËÒ¥K‰ˆˆ`úôéÒXîË—/op¿³{©Õk Aêsùôe®gybåfy§ÖyãT•mWëÜPšzûš4ãä6¤¥³¨¨¨É£XTUU5º¾¿¼¼\šÂº±4¥ ÷“ÒÒÒZ[g[Ò78zô(‘‘‘ôêÕ‹õë×·ú1«   YcäÞ¼y³I¯¥¾k 1Û6¤cXsÑ”moݺ¥Ó‘¾gÏžÈårŽ=úÀ&Ï÷ž={xÿý÷ñööfÅŠÒ]–3gÎHeš¡ƒ‚‚¤ePÝOà¹çžcòäÉ\ºt‰5kÖ´Í“¨‡H Ahs×Ò ØüÊV¼ã¼(Ì(¼oj›J3,ã‰'Ú8¡%mܸ‘Î;Ó»wo¶nÝÚj­ÝÂýÉÔÔTš3¢æˆR‚~;vì`ÇŽZijÎCCCuújÕ¬G/))áƒ>hý ›¡UÇAh(µJMÚ®ô&yîÞ½»ÎÆÆÆŒ7.\¸Pk¥ð`ÊÉÉá÷ßÇØØ¸Ö©Ä…?¯^½zajjÊÎ;[løº‡Íµk×ÈÊÊ¢¢¢Bë/33ó¾™a°¡D ´ B#1vìX”J%eee\»v …B½½=r¹œK—.±dÉ’6¯ZÞÆ)//¯w„'áÏgÛ¶m5zNAÛ3Ï<ÓÖ!´‘@ ‚ 4Ryy9‹-"..œœœ(--åܹsœ:uŠ-[¶ˆ‘`þ¤JJJîÛšÌÆÒŒT"ÎÕ†¹uëk×®më0„û„H AxhÉ«ÊQ) ©4´@YÞðQÔj5'OžääÉ“Õû‘Ë›5² Ük•••Ì›7¯­ÃÚ@¥¡Pýù'4¨á¡e‘_gŒ–¾TšB$Ï‚ <* -È ÜùüšF´@ ‚ðÐrN]Ç »òÝâÉw‹oëpAî yU9Μ@JÐ&Z Axh™¥ã`.VyGÄíLAþôäUåXåÁÿÀ\L‹ÒÛ:œšhá¡£¿W3— ÂÃ@¥0äºcg®;v–ÓÌF(4ŽH Ahqš©²AáÏH$Ђ <ô¼V<ÖÖ!‚ ÜSÇüØÖ!<ÐD-B‹9þ|[‡Ð8ÝÚ:A„¶õÀ}nß'D-‚p[–k¶A¡U¹eïhëþÄ(‚ ‚ ‚Ð"AA„F ´ ‚ ‚ 4‚H AA¡D-‚ ‚  hAAAh‘@ ‚ ‚ B#ˆZAAA$Ђ ‚ ‚Ð"AA„F ´ ‚ ‚ 4‚²­Aªùy8akiNzöò Š¤Ç ” ÂÛ{pàTjE÷p²·¶ÀÁÆ’3iYz—ÛZšìG;wG²¯ðë“ܸyëGÙvLŒ õ󠢲УgÓÛ:œ©ëz’ËeÄ„ú³çXJD&%: ‚ Â]l,ͭ̕ò >^ó«ÎòP_wöŸ<¯C{`ô‰ aÿÒ×Yø·ñmJƒ•WT²pÕ¦ Ihãh„û™H Aá.£ûÆ`d¨dãÎ#\/.ÕYî`c @iYŽMheßmÙG•JňÞ]131jëp„û”H Aá.÷‹`Å–}m‰p¯åäòÛ¡Ó˜›3(¾s[‡#ܧD ´ Bø¸:0´WÂÛ{á`]=ÌÙ¦ÝGÙvðÿ?€‹9Wk­‰ñ£OTÛ{£T(HNÏæ³£nÔÅ@©àÉ=ˆ õÃÓÉŽ³s8œœÎ7›÷p«\k¨\.c@\8ý¢Cñp²ÃÒÌ„¼‚"N§e±i×Q'ëÖyÊd2Åw&6ÌŸŽþ^äñGÊE>ß°]o‹ì øº·ãƒ›¹z½˜þ±I삱¡{Oœãú›D…ø’’‘ËW?îÒ§B.gÎÓCP(äüßÒ ”–•·Z<Ë~Ú]ëklbdHD7*•šGÎh-›6<7©eò…1p­¨XZþÖW›øç„,ß¼WïðwQÁ¾$DS¥RñÎòQ«Õ:ëŒíCp;w6î<ÂþS©´÷ra|ÿn\º|OÖnÓ÷Üg†#—˘÷å&ŠKïÔlŠ ¼½o,Y‡…©1ÄÓ)À 7ŽŸ»Äöçùqϵ¾æ&ÆŒîMD žÎd\ÎgÇ‘dŽ$§ë¬«T(x}ÊP‚¼Ý€êáß|v„´üƒ›u:[*ärFôîJD aþž\),âXJëw®·~ÚÖÒŒ½£èà…¿‡3ç3/sü|ßnÞKañÍ:·­ÍöC§éÓ5„¸°¾ýEüˆt‰Z¡‘$Žÿ>sc鱄È<7¢¿$§_t+MÒI  ” Þ˜2ŒïL&“$&ŒçFôáz=_ö ¹œ?–¿I€§‹ôX|x “£úòø«q25Sk{k ¶üï„úyHÉšL&cHÏHBý<öZÛØY™³äåÉ<×IÚF&“1ºO4ÓGöå‰9³ë³ZÛô aÊöKá‰þÝžÐUZfdhÀÊ­û˜ýÄnܼŪ_÷k%ǽ»óÒÄ¿°÷ø9­å­O] t§/” gÒ²t:Žï߈@éÿ§õÔZþŸ/7Ò+¢]:´C&“ñ¯VéìÿŸ2 [8»eïñsZËe2oÿu ÎvV|};N_7Gf?1€ý'SkM ÿ6®?J…‚ÿ­Ü¢•@k^ K3FôŽÂÙÎJZÖ+¢3G÷cùæ=<÷ß/¹yK÷}éèÍò×§áïá¤õø¸GâÈÍ¿®³¾B.cö¤ÿ½œíµþÿêÇÝZ ´Ÿ‡_½ú ]ƒ}µö3*1šWžÌ¿>ZÅ¢Õ¿êý¡Ñ7*”Åÿ~W{ké±ꎞOýßb~;tZ÷Ū‡æGe—í½­ðp ´ B# èΗ¯NA¥Róö×?°fûAr®àÍ3C¤$OŸÿ<;’çÇÞübiÙyx:ÛóâØG™<¸ßÌFè˜êmù]þÆ4 •J¾ßv€ÝÇÎRQYÅñsÃÔØ-ÿûV榌ú×BÒs¯JËKËÊùnk]:´ã±¸N: ´…©1}º†Jÿ襓@GùàlgÅ© ™-:ñô‘}Ùq$™iÿý‚ÃgÒ°³¶`pþýä Æ=Çõ7y~þr­m\ì¬Ù¶è%ÌMŒÙ°ó0ÿ[¹…3iYx¹08>‚™£ûé§¼²Š¨IsèÊÜg†sðôþúîWÒòŒ¯—•¹ ;>yGKÖl?ÈÂU[H¹˜C‡vn<;,‘a½º0ÿ…qßdùÏ{´ŽêÏ￈J­æÍ/6°fûA®Ü K‡v¼>e!¾îlzïE:Žûç/]nÔkuüö{êç\.C¥ÒMÞ…‡›H AÈ@©à¿ÓÇðÌ[Kùò‡Ò²Ÿ÷ã—ýÇYü¯§ß¿›Î¶¾îNLžHyE%Ý&¿¡•]̹Ê/IÇÙþñ¿ ¹=ºƒ>*•š¨'ç™wMz,-û [÷Ÿdÿ¯ãæ`ÿ&þ…—n'mƆôŠè€Z­æñW?ÒJ¬/æ^åbDFcLßh¢B|ù%é8^ÿDz<5ó2Óþû%¶Væ ëÕ…é#ûñK×ël«¼‚³ÞÕI ¾üasžÂè¾Ñ: ´™‰ƒã#¸qókj,kÍxjcge@¾ž1“Ó³¨¨¬àä…,R2´“ÜÕÛöóÎô1tðqÃËÙ^ëu~,®F†J>]ûÏ M`XB^\ðV‚6ðvëôêß68æ†øêÇ]LþÏ©%7ûj!'Î_"9=›oç>Ç”! ,ü~+©™w’Í7¦ÇÜĘe?ïaÒÜϤǯÞ`ϱN¥e±ø_OiG­Vs$9?÷ê뢒R½¥¯O†£%_þ¸‹Éo~.=¾ãH2;Ž$óÞ̱ÌÕyÓF±aÇaéVÈå,üÛxd2“ßü\ëŽÂÆ]GØzà$¿üo61¡þ¼?óqþò·÷õZ]+*F¥R£T(°45ir)ˆðç%: ‚ 4P¯ˆø{8q:-Ko¯J¥®uÜØIã14P²tÓ½­Š…Å79›Qwk£J­ÒJž5._»Îk‹×Õ£GhÈd2é¯[Çöuî[ãÙa‰Ì_±Yïr͘ÇÞz—¸jK­Éê—?ì¤J¥âјŽX››j-ß3#¾Û²’Ò²{Om4±55iʾZ(•”<ÛQkÙÐ^]P«ÕÌûj#‡Î¤ábgM\X€Ö:šòŽ5Û[6NÍÌÓ[ñý¶<}¥‚ÝîÜA112dü£Ý¨¨¬â•O¾×»ÏæL-oh dÊ**«˜óéj½ëÌùl-yE8ÛYÑ3"Hz¼{§ötô÷ätZ–NË4Tß xqÁ·@õ{`enÒ¨ØT*5E7«ïhXY˜Ö³¶ð0 ´ Bµ÷ª.±Øuô¬ÞD¤!Ûî<šÜâqlØqwG[©6»´¬œM»°îçYõŸéLx¬»V ìݽ]êRƒøð@?ÇÛ÷x:ëݾ®—%3ï¿$ÀÐ@É^]´–½=êÅ’;´oÍxj£é X^QÙøo[ùkuý{ÿ ´©±!Ä„±÷ø9²®ðÍæêÄoDï;õÙ^.ö„úyœž­·bkÙvðík”ù{8#—ËHͼLÖ•‚?¦Ÿ»J²òtJ‰4ŠKoI±z¹JùTwPÜýGí×âÁÓ¤ý¶¯±mCiÞ3c1” «Y%:tÀÀÀ€ÌÌLòóók]744¹\NUU'OžÔYîä䄳óÀÔÔTŠ‹‹uÖh×®:“››KII‰Þíqqq©7^333üüü¤ÿ/_¾Ln®þ^À5÷©¡V«),,äÚµkµ>SSSüýý((( #£îš<}Çѧ¬¬ŒädÝ/h[[[¼¼¼pqqA©T’ŸŸOVVéééõîS„jšäâR^íŸuµ ð¨þ|»tY·¹%Ü(ájá ì­-ðvµ—:NšûéÙWß¿CzF2¤g$jµšý§Rùdío¬Ø²W*p´±”Z_¿Ÿ7£ÎãUU©šçÒ;èÛ‘1}£ùâv­³“­‰]C8qþ’Öˆ ÷"}4ðÌMëY³vk·ä/>Aψ LŒ )-+çÑ˜Ž˜²jÛ~ ºå÷Ý™cÚ« ÏÏ_ŽJ¥–ZŸõÕˆ·¦ ÙW€ê`š%—ôÜõh A·e\®ûzÒŒÂÑÎͱƶ. Û6#W{kÚ¹:4ºµÜâöû_óŽˆ h4+8p Ý»w`óæÍ|øá‡z×óðð`Þ¼y¨u 9ŽÆÅ‹yî¹ç¤ÿ;v, Èåº7òòòxë­·HII©wß‚ð°»qû–®“mí-¸µ)¹=ºAS¶m(#Ãêì­…%¥eüãÃïxåÓÕü¥{gõˆ _t(Ñ!~D‡øÑ?¶#ãæ|ŒZ­¦¨¤•J\.cê[_p¥ ¨ÖcÜÐßPQŸ÷üAN~!=ƒpµ·&ûj!£úD£Ëù|ãïZëÞ‹xôÑtF´4mÜmÿšò¯óëS<F¯ˆ ~Ú{Œ¡½º R©Y·ýP]zóë“ô‹£{§öì8’ÜjõÏõÑ$‹5K„4‰£ÓíVþ–¦ñ£¾ý›p©F²|³×“‰Qõ5ÑØJ…B:n}£ã§f•plß~gÜÒèèh­a™jŠ‹‹Ó»† áááZõèÑ£Éqy{{óÒK/ñè£6iû„íé;ð¶¶®eíºðüƒ>}ú4iûæ°°°`Þ¼y$&&êMž¬­­Éʺw· áA–r»'˜Ÿgã·Í¨nE ó÷hј4Ül°05&¯ ˆkEºÉdyE%«;Às>Ƶÿtžyk)U*#£ˆ ­¾ãv«¼‚ Ùy߼ů]Gjý»{ظ†ª¬ªâëw#—Ë‘TOZr«¼‚w·{/âÑGSûlgmÞ¬ýÜ)ã脱¡ýã:²ë³ääß)WÐ<ç QX™›HJF§.hG¨º]¦ÐZ3㵿ÝÚ\³?åv˯¿§³”L¶¤ä‹Ù Ú¿¦üéÌíœÕÛVÇY×µ(—Ëð¿ý¼’klÛö·ß{µúN-´ Ô¤Õ˨¬lXÝב#G¸qãX[[ÄéÓºã-vëVÝ#½´´”ýû÷ë,ïÑ£r¹µZ͹sçÀÓÓ///.^¼Xg o¼ñ×®]C.—ãèèÈ€ `Ê”)ìܹ³Ö’}|}}ñô¬URŒ IDAT¾ “““ D.—Ó­[7~øá‡:·}÷ÝwÙ¹s'J¥WWWÆGtt4C† aëÖ­ Ž£.ß}÷êo(+»s«éé§ŸÆÉ©ºô®]»X¹r%ÙÙÙ(•JlllÀÜܼQ¯ <̹T3êϾ ïœvðt*cûÅ0mX" ¾û…¢’Æ)Ëd²Z‡ÔznDõôߟÑYv·Êª*–nÜÁãýb‰$ÈÛMêh—tò<~îNLÙ—5ÛRYUÕè8ë³dãïÌ~â1F÷‰aó¾ãtôæÛ_öémE¾ñÜM“l…ú6ïÇÎÆG¸U^Á£qÙzà$æ&Æ|¿Mû;pýÎÔ”–1¤W${O¤` Tèm}N»]báëî(•„4V×àvzÏ+sFö‰F­V³ãÈÀôì+\)¼ƒµ3Fõåí¯ëþ¼›¦¬ÆÔXr|1÷ª´ÿé#ûòßeºûoçæÈ£1¹y«œ¤ç¥Çk^‹Ñ!~$<¯³íØ~±ØX˜ñGÊEò¯ë–SÖÖèrû½OÉÈCØ zIÍ’ƒ â_ÿú3fÔ]gVSee%;wÞÆ)66Vgwww¼½½سgV‚§¡iñ=yò$?ýô“ôx|||½1¤§§sþüyRRRؽ{7/¿ü2™™Õ¿Ü ðõõ­gúc)//gáÂ…Š¥ªª •JEyy9ééé¼ýöÛ”—ß¾ÍääTçÅÚ999œ={Vï_ÍšæÐÐPé¹¼÷Þ{¤§§S^^ÎÍ›7ÉÊÊbûöílÚ´©Eb„‡Á‰ó—ø~[umê²×§Òù®‘‚|ܘ>²¯Þm?ßð;—ó±·¶`õ[3¤ÎoCzFÒ/:Tï¶ ¹œÕófjMàÕ»ÌÙ²òJ^¾k´„~Ñ¡Xšé–"ØX˜I5¥5[;_ùd57nÞ"*Ä—E³'è л¿ÆH˾ÂöÃgˆ òáµÉCXºéw½ëÞ‹xîvøLjµçfµø•”òóÞcx:Ùñï'ÿB•JÅúÛ=5JJËØ¸ëŽ6–¼6y ¿þùü¥Ë\+*ÁÄÈyÓF"—ßù>QÈåL’€¢–;º…³ðÅñZë™™ñÅ+Ï`mnÊŠ-Iü‘r§ÑªJ¥âÏ×ðï'ñø#qZû³27á­çFÕz>žeË–élS—ÊÊJNŸ>»{õXªVV ¯7T(RéÈÁƒ¹xñ"çÏŸÇÏÏ   ¸råJƒ÷§V«©ºÝZ’’’Òè^ûÍ!“ɰµ­î bhhˆ««k½A¨ßK‹VÒ5¸^ÎöìþìU'§q&=›g"ƒÚ¡TêObn•W0ý¯øîÍ¿Ò+¢§W¾ÍþS©ä\-¤s{oB|ÝõÎw·ÝÃIÛ0Ÿ§RÉÍ¿Nx€¡~TVUñüûË¥–J¨î„·é½©¬RqðôÒr®p9ÿ:6–fô툓­¿$çàé;IBfÞ5¦¿óŸ¼4‰I{ðHLG¶ì?Ajæe,ÍLèàãFb—Ÿøw½S,×eÉÆßIˆìÀð„®¤få±ó¨þŒ{OM…Å79wé2žÎD‡øI£@4ÅÊ­I éIx{o~;tšË×tgîûö—½Œéƒ«ç.]愞 \ªT*þ¾pKþý4ÏèCBdöŸ¾€¥™ ]‚Úáád‹J¥¦¾vš)CèÆÎ£g112 [Çö8ÛY‘œžÍ¿?Ö5ñó ¿3´W$½":ðå«S˜ýÄcNNÇÁÚ‚¨_ •µw¥:u!“=ÇRˆëÀþ/^ç‡Ý`ajÌÚí¥ä÷‹v0(¾3Æþ?{wÖÔ™öü›A6e€l;nµ¶.¨µUÛ±ŽcuhëØÕé6™v^§SûÓŽK7k•ñuéåØÖºàŽû‚beY‚ìA ÉïÞœ!Ú~?×Õë*çœçœÛ4±7Oîç~†á›wWŸ„ ×nÀ¥Ÿ-"ƒü`ce‰='SñIbë5KÍŸEøx8#yýÛ8—žƒkyEì冑C| •Hð—¯þÓªa­æ.fäBå?»>^ŠgÒ°ýàn±m{d`skÁ¶ÚR™=þøãX¸p!ªªªðÖ[oݳdânW¯^Eqq1\]]áìì dgÿw¥«¡|£¼¼©©©­Æf|õz=Nž<‰šš¤¦¦bĈpssƒ¯¯/23Åõðtwÿo»ÃltG¨T*¡Öùĉš“úAƒA"‘ &&;vìhs¼L&j% æÍ›…B¦¦&lÙ²EÔŸ¡=èÓ§És:z½ׯ_‡Ÿ_ó_+V¬Àþóœ8q‚7ˆº ·¸¡óÞÁêWçá‰h•°ïNCÖ~wêò*üíÅ&¿öÝ}â"æ¿uoü!JŒ kžq.©¨Æ¢¿o@Na öþ¿×Û|öÿ9Œë7KðÖó“…±MZ-Î_ÍAÂÇß¶ê2 Óëñ· »056‘A¾ˆ òÎÕhêñM?âovA«3î`±yï œ¾’O^~1ªÁxîñhá\]}v==º6!°óÈy¡kÈ×ßiw‚¡'â¹Ûw‡ÎâyO ~LX—èÝ'.¡FSo²|à éLJ*ª›wãk§ûÆÆA¯×ã–ÌÂ#J<¢ô€^¯Ç™´ë˜ÿ—ÏñdÌ¡”Ç”7ï†mž"s&4OnUÕÔáË]GðÇO7›ì6ѤÕbÂK+ð§ß>‰EÓC€ÒJhu:ì=uXñ ¾yo!õo=“«Óé1ã­UX‘0Sbƒ1gBJ*ªq©ÅN:O¾ú –Äÿ¯ÎyC}ú › å—bùšmX³#ÉäŸçæ­ „Ì}+žÆŒÇ„÷¸V§ÃÏÙøã§›JRZzá¯_bË_—À×ÓžÎPWT ´½µÆ…B«Ó í!‰î&9{ö¬ÞþøcëÂÑ‘çÀÿøG\»Ö<“3lØ0|ðÁ­ªÕj=zûöíCQQ÷mKô°)~®ùÌ^ÿnþí¦»¸ÅË2©(Ý¡Õé‘™WŒ&­úíSx÷…)øë×ÿ671ÅÊRŽ¥ŠËªLnŽr¯ç*=œ`×Ç i× PßÐxÏ16V–ðpî;kÞª@¾º¼CõĉÝáö3n–” »ðõ†žŠÇÛÃW·þ*kêÐRB—zBw7‰D¥»úÙöÁÕEBÛ½¶|öê<ünJÞY·~³ æf<еõwÚÜ\¥-^nŽp°³Æ•ë7;ô¾3J%ègkR»;¶äÜ×Jw'dä‹ê¬bx_ôµí#*67{”U×ý÷}nR >k>vŸ¸„'Eî`ø0ð(ln™ûô× õf8-iee%Þ|óÍ.}½øðaáß[ÖA·,ß8xð`«qÆ ƒƒƒµœ;yò¤PúÓníð'Ÿ|‚M›6aÓ¦Mø×¿þ%$ϵµµB‚ÚVVV‚¿””h4Í |ŠŠŠpýzó×›>>>Biˆ>>>XµjDíŠK—.á­·Þâ7pqqA||<Ö¬YƒY³fõhLD¿$†™®ôœ›B2j¨+NÏiÕ]}Î¥çˆNž ÏÍÊWãüÕœ' ·ëêqõF!Ž_Ê@Ná­/ÆÓëõÈ)¼…©™¸Qx«W“瞌çúÍ:ŸŽ¾6}„M^z½×o–à\zÎ=“gSî46áRf²òÕ¢K s‹Jqáê QÉ3Ð<Ó|¯ähþ6ætZ¶è¶„†÷…ØØŠÊ*’g‰D‚%ÿ7‹ÿ勊~]ÌÞ|óMQe¦Ü¼yB÷Œþýû£  @(ßÈÍÍ5Y6в]œ¹¹9bcc…ŸKKKáââ‚~ýúaÈ!&{G·%''|ðJJJ:<&** ryóJášš£XÊËË…:혘$&&š¼ÇÆqá€T*…»»;¦M›¥R …B„„,Z´¨ËµÐ‡2¹Y Ð<»ÜÒÏ?ÿŒ„„øøø &&QQQ†5fffxöÙgQ\\lôKuŽ«ƒ¦Æ†àv]=ޤܻ=Øþ´vŽþ–?ÿ$6ï9Þë¿<Ðý÷Ô¨` ó€S?g »x™bÖÕäÙàСCB½mDD’““…¦fŸ---ñß·óæÍkóÞ£Fj3þôÓOQYY ;;;¼ôÒKH$prr2Ùí£=-“ù¸¸¸V½  ÚK ‹ŠŠ•õßV:8þ<>ÿüsX[[£ÿþðððèò/,/^ÄDÉÎÎFvv6¾þúkøúúbÉ’%ŸѣG3&áíùO!ùbN§e¡®¾f2‚Ä¿^{}øã§‰(.k½XŒ.g¯\Çæ½'ðìø(<Üã»RÏK˜9z½KWnîÑ…ÿôðéÒN„-;v /¼ðd2"""„7ž^¯Ç‘#GZ]…¢c­‡}ôQ¬]»V(ëhéÒ¥KÂLóСCñØcÁÚÚÏ?ÿü=ëŠ œœœ„ÞÑ÷Ò¿x{{·*‹hËíÛ·‘aÆhþÅ¡·effbåÊ•B›¾Î”¥ýZ pqÀ;ó§?WÜ®…µÂæf2ètz|¼y7>ÛÖ==ß©÷-ÿ×V.55¥¥¥­®o9ÃûÎ;æå—_†ŸŸlll R©pîܹvcظq#¢¢¢ P(0fÌìÛ·ÏäÆ.w‹ê¬7mÚ$tàhiâĉ˜4i€æñŽ&ÐfffpskÞE©±±±G;`8::¢²²ÒäÆ8666¿çç·n™DD¦å©ËúÜ;˜3! *ÿèïÜi× š™Ä½'pú®Nôp+,­Ä·»“{;Œ.9så:úXY¶ÚÝZ+©¨ÆW»ZOúÝ­Ûh ¹Œ#$$„ðLõ~îׯ†ÈËËê†ïvôèQ¡,$&&æž tyy9¶mÛ†¹sçB"‘àÅ_ÄK/½dr溥–­ôöìÙƒÊÊÊV×ìß¿_H cbb°aÆV_ïx{{ »úI¥RØÛÛc̘1pvn^TtðàA“ɬž}öY“±íÞ½å寋ŒÂÂÂàèèhòúªª*ìÙ³ðÆoÀËË .\@NNÊËËÑÔÔ///Œ7NÓÖ®†DdÚÅŒ\£ 'ˆdßüx ßüx¬·Ã úEéÖúÔ©SÐh4BiFCCƒÉÙÜ–3¾ÉÉmÿfòäI¼ð šK>är¹°³_[vî܉qãÆÁÅÅÄO<;w¶y½¯¯/<=›·ìüùçŸM&Ï@s ±Z­†‹‹ œœœ0xð`¤§/ŠG||¼Éñ—.]ÂÚµkMžS*•P*•&Ï:uªUir×G yÁæž={àêêŠÁƒh^ Ù²#JK'Ož4Úý‘ˆˆˆˆÚ×þ¾Ÿ"ݹs'Ož~>}ú4êêêZ]7zôháßM%ØjµZØ”E¡P 44ôž1444à믿~~æ™g„Vy¦t4–»Ïv,l/޼¼<œ:u ï½÷–/_ŽÆFqmº¢´´ï¿ÿ><ˆêêj£sz½yyyøç?ÿ‰?ü %ˆˆˆˆD¸ºº2{ú°´´„““´Z-JJJL–’ýÚtu#"¢‡ 7RéÝZÂA®úúz.$"""êÝZÂADDDDôKÇh"¢ÿcøj“ˆˆ¨=œ&"""" 4‘L ‰ˆˆˆˆD`MDDDD$h"""""˜@‰ÀšˆˆˆˆH&ÐDDDDD"0&"""" 4‘L ‰ˆˆˆˆD0ë툈¨™››¬­­QRR‚ªª*á¸L&ƒ··7 33³·ÂûU²µµ…òóóMž·¶¶†ŸŸ\]]Q^^Ž‹/¢¾¾¾‡£ì=r¹^^^Ðjµ¸~ýºp\*•Âßßééé½ÑýÚˆèñÌ3Ï ""k×®Åþýû…ãÖÖÖøè£ Õj1cÆŒ^Œð×gúôé?~<>üðC¤¤¤‹ŽŽÆ /¼kkkáXQQ–,YÒÓaö|ôÑG¨¬¬Äüùó…㯽ö‚ƒƒñòË/ãæÍ›½!ÑýÁšˆˆÈgggŒ7¦ÕLªR©DBBîܹƒÄÄD¨Õj 8Ð蛃_³ . 44³gÏÆŠ+z;¢nÇšˆˆÈ„™3gÂÌÌ »víjU–1eÊH¥Rlܸûöí$''÷F˜¤ƒbÚ´iÇ Aƒ••ÕÛ!u+."$""º‹µµ5¢¢¢ÐÐЀŸ~ú©Õù222z:´‡BSS~üñGÀ¸qãz9¢îÇšˆˆè.ÑÑÑ077ÇÙ³gQWW×ê¼ ¡¡¡§C{h;v :°´´ìípˆºh""¢»ÄÄÄhN©s***šš …B‘#Göv8DÝŠ5ÐDDàâ₈ˆ(•JØÚÚ¢  gÏžEjj*¦NŠ’’’6kbýýý1|øp(•J˜™™¡  {÷îíÐse2ÆŒ8;;£  ×¯_ÇáÃ‡ÑØØhrŒT*Epp0FŒGGG( TUU!??çÎ3YŸ*‘H0räH <Duu5rrr°oß>“3²#GŽ„ŸŸvíÚ…Û·o#88AAA077ǵk×P[[ âàÁƒmÆ9kÖ,H¥RlݺÕhv·»ã9|øp›¯±\.‡t:._¾ltn„ pppfT'OžŒššáüöíÛ1}útÀ‘#GL¶¿óóóC`` t:vîÜ ½^ßꚘ˜ 0gΜAFF<<<0zôh”––bÏž=&ãž={6¤R)vìØFcôZx{{cëÖ­°´´D\\”J%››‹Ë—/ãܹsm¾ P( ¥R‰þýû£¤¤iiiF­ëL¹|ù2†ŽGyGm÷Z¢‡ h""‘F… @¡PÇ‚‚‚0qâD¤¤¤@¥R!99¹U-“É0{öl<ùä“H$Âq•J…‰'¢¶¶¶ÝçJ¥R¬\¹îîî±€€Àã?ŽO>ù¹¹¹FclmmñÞ{ïÁËË „dM"‘ <<ÄG}d4ÆÆÆK–,AHHˆ0F"‘àÑGÅĉ±råJ\¹rÅhŒJ¥Âرc‘žžŽØØXDFF çär9’““1eÊh4$''›,} ´iÓpõêU£ó÷#žöh¥R ™L†üüüV‹ccc1hÐ áçßüæ7Fç·oߎÀÀ@ášM›6µºÿÔ©S HOOÇÕ«WÎK$Ì›7ööö8tèÀÕÕS¦LAFFF› ôSO=™L†ï¿ÿÞ(6¼ …>ú(ìíí…s˜4i>Œõë×ãÎ;­îëãプ_~¹Õû.66•••&c10$Ø-_3¢_&ÐDD"„††"!!:ß}÷Nž<‰òòrx{{cܸqB’gÊœ9sðÄO ¶¶›6mÂåË—¡Õjáëë‹Y³f%(¦H$ôéÓ«V­ÂÕ«W¡×ëñÈ#`Μ9ðôôĻヒ_|Ñ(éKHH€——.\¸€ÄÄDäççC¯×£_¿~ð÷÷Gyyy«g¼õÖ[ðóóÃÑ£G±mÛ6¨Õj8;;còäÉ;v,–.]Š„„“3¿¯¼ò ÌÌÌpüøq¤§§£©© ¹¹¹ÈÊÊ­[·àä䄜8q¢ÕXCÙDRRÒ}§=J¥L^·jÕ*XXXà½÷Þƒ••V¬X[·n çpìØ1 4!!!­hKKK >\ø922²Uíãã{{{äååukåI“&!-- k×®Evv6lllŽéÓ§#66uuuøòË/ÆôíÛï¿ÿ> Μ9ƒï¿ÿððð@XX&MšÔî3oܸðòò‚T*…N§ë¶?QobMDÔA2™ sçάY³Æ¨áÂ… ¸xñ"/^ŒØØØVcÝÜÜ0aÂ455áÍ7ß4JŒJJJpñâE|ðÁÂL±):¯¾ú*ÊÊÊ„cjµ)))X±bo¿ý`nnŽÀÀ@èõz|üñÇF‰õ­[·Œ?ƒèèhøùù!%%Ÿ~ú©p¼¨¨ëÖ­ƒ """0iÒ$lݺµÕø††üå/i•Í­ÍfΜ‰èèèV ´¥¥% Ñhpòäɉ§-666€êêêVç 4w™€¼¼<]sâÄ Ì›7žžžprr2zCBB`nn޽{÷bܸqˆŒŒÄ† ŒKC½pËס;:tŸ}ö™ð-Dyy9rssQPP€¥K—bܸqؽ{7ŠŠŠ„1³gφB¡ÀáDZjÕ*áxuu5ÒÓÓ‘——‡Å‹·ùÌššèt:Èd2(Š{~ËBô°à"B"¢ „»»;òóó…¯Ö[Òétmö»3f ÌÌÌ””drV±¶¶¶U"v7½^o”<TUUaË–-šN‰D"üc(õ¸—ñãÇvíÚeò¼¡ç±akñ»íÞ½»ÍdõÀÐét1búôéctnäÈ‘°´´Ä±cÇŒýûO[ ±u6Ù+//6^ 6:½^;v 33}ûöÅàÁƒ®1|‹ÑÝ tQQ‘ÉzëãÇ#++ 2™Ì(^¹\ŽØØXhµZ$&&š¼ç½¶–×étB9ÉÝÿ͉fL ‰ˆ:ÈÃÃpåÊ“‰HGÇÞgΜ888µÙ 8{ö,àÍ7ßÄ«¯¾Š¸¸8£Ø»õïß@ó¢±€€€VÿØÚÚýyîÖÞëRVV†””˜™™!<<Ü蜡|ãÀ=O[,,,üw–¹3 Ý;Z&¤1b®^½Š²²2áš–õÙÎÎÎðòòBAAɈ÷Ë¥K—¿ŽîîîJ¥(**2ù‹[G^GÃëJôKÀ"¢2$¥¥¥¢Çº¹¹uzlGÔÔÔ ºº¶¶¶pvvêwW­ZµZÑ£G#<<áááÐëõÈÈÈÀÞ½{…^½@socÃ,ák¯½Öîó´Zm§âLJJBpp0¢££…dÙÎÎÆ j¥ z"S 3à]é]|êÔ),X°C‡…\.GCCFŒ¹\ŽãÇhÞ¹ð¹çžCdd$¾úê+èt:aöÙTøý¤V«ŽŽŽÂ1CM~Wß³†_èî^Iô0cMDÔA†¯¢ ›hˆaH:3¶£ÌÍÍÀh¶°¾¾7nDbb"Fމ°°0 >þþþð÷÷Gpp0>ùäèõzÔÕÕA§ÓA*•bíÚµívXèlyÃùóçQQQ!C† _¿~(//Gtt4¤R)öïßotmOÄcŠa1¢••U§ïqûöm¤¦¦B¥R!00çÏŸGDDt:N: ¹ôæÒ¥KP©Txä‘G––&tçèîò{1$¹-“eÃ{¶½o,îE&“A.—€ÉEžD+&ÐDDd¨Q6ti£¨¨¾¾¾P*•B¹Ew2”nTUUõ%6hjj‰'pâÄ Èd2Œ= .DTT”P'ÜØØµZ 777ÔÕÕ åÝI«ÕâСC˜:u*"##ñÃ? &&Fè^ÑROÄcŠ!7,&ì¬ääd¨T*#55#FŒ@zz:***„kŽ;•J…¨¨(äää`È!(,,D^^žÑ½ ßܯ2÷+-ëó ‹ ÝÝÝ…Yt± ¯¡^¯7j­Gô°c 4QÊ † ÿN?~¼Qÿh1$ ¤RÓmO˜0ðóÏ?ßó>Z­IIIÂâ:OOOáܵk×4·<“ÉdŠó^’’’ ×ëñè£ÂÃÃ>>>8uê”ÉÄ¿'â¹›¡ÓF{Q:âôéÓhhh@pp0T* …P¾Ñòšúúz„‡‡#$$2™ÌdùFII €æR ÃŒ®X¾¾¾&ß?VVVˆŠŠ‚^¯GZZšp\­V£ªª r¹üžíêÚbx ÙÂŽ~Q˜@uPnn®½òÊ+ðññ1:ïé鉉'š»oß>”––ÂÖÖ¯¿þz«RŽððp£þÀ¦H¥R¼öÚk­ðQ£FaÒ¤IhllÄæÍ›Î·»Y[[ÃÅÅŒf;OrÏ0 IDAT¡Ñhàç燅 šÛÙ_ Ôj5._¾,ô¿Œ{?·ÔñÜ-;;z½nnn]ªƒÖh4¸páoT¾aP__³gÏÂÎÎO?ý4ÓõÏEEE¨©©\.Çœ9sŒa©TбcǶùË•Ahh(,X`t¥¥%ЧO;v 999Â9N'´ŒÇ¨Q£Œîgee%´ul‹áÛš{uë zذ„ƒˆH„o¿ý~~~prr‡~ˆ¬¬,ܼynnn4hP›³¤X¿~=–-[†ÀÀ@¬^½¨¨¨€··7 `r¸»…††bݺuÈÌÌDee%”J%¼¼¼ Õjñå—_ ‹Á€æzëåË—C§Ó!33jµ•••°±±App0ììì’’b”Ü”••áóÏ?Çïÿ{Œ3*• /^Dqq1 <==1lØ0¼òÊ+Fý‚ÅJJJBPP"##Q\\Üfw’žŠ§¥ÚÚZÃÍÍ ~~~HMMíô½’““ooo¤¦¦¢ªªªÕ5GŽAtt4œQXXhrN‡ 6`É’%˜8q"‘™™ +++ 4ŽŽŽÐétF;\š2vìX¨T*¤¥¥A.—# ööö(((0¹kâþýûŽÀÀ@$$$`Ê”)ÈÎΆ|}}afÖ~ahÑ—ÝîuD&ÐDD"ܺu Ë–-Ãï~÷;„†† ‹ñ±wï^TTT`Μ9&Û§?¯¿þ:-Z„Aƒ 3ÎUUUX·nÔj5Þ}÷Ý6Ÿ½oß>¨ÕjLŸ>]«Õj‘••…/¾ø¢Õ,Ÿ^¯ÇöíÛŽÁƒõÖh4عs'¶mÛÖê«õ#GŽ ##óçÏG@@âââ„swîÜÁéÓ§Å¿pw9}ú´Ð5äÀí¶›ë‰xîvâÄ L›6 QQQ]J ÏŸ?F…BÑæÂ@Cbmgg×îâÁC‡A¯× ›´xzzB¯×#33«W¯ÆÈ‘#ÛüvîÜ +++Œ3FØì§®®IIIøúë¯MvÉÐjµøóŸÿŒøøxL˜0Ax®N§CJJ Ö¯_—^zÉä.š}úôJ¥‚N§»/ÿˆz“ÄÕÕU|“L"¢_€âçšË¼þýx§ÆK¥R!™(,,„V«ÅŒ30sæLlÛ¶MØÜÄ xzz¢¢¢Bt]©T ôéÓ¹¹¹hll¼çKKK8::ÂÆÆååå(--íPë7‰DgggôíÛååå(++ëÖ–qbõT<®®®X½z5jkk1þü.õ„în‰...°¶¶ÆÍ›7ï¹8oáÂ…;v,±cǘ››ÃÃÃwîÜAqq±¨^ÙÎÎΰ±±A^^Þ=ßwqqqX¼x1Ο?¿ýío~õŒÜ§¸nx¬—#y8qšˆ¨“t:]«¯Û uÅ÷ÚãÎ;mîZØ‘çŠ-W¨¯¯lj¡×ë¡V«JCzSOÅS\\ŒË—/#((111FÛ¶÷6½^âââNollÄ7:5¶¤¤DXÐØ‰D"̆ßÝžè—€‹‰ˆº‰½½=ÂÃáÑhŒºÐÃióæÍÐëõˆï± ¿aaaP*•¸víÎ;×Ûáu;&ÐDD"Íœ9BO^™L???¼ýöÛ°´´Ä–-[ÚÝôƒYYY8räœÖÛáúè#hµZ̘1£#üõ™>}:Æ?ü)))F碣£ñ /ÀÚÚZ8VTT„%K–ôt˜½ÆÅÅ}ô*++1þüÞ§[XZZbÕªUhjjÂþð455õvHôbMDDd‚³³3ÆFƒôôt£sJ¥ ¸sç¡V«1pà@£oèáT__ôôtDEEaìØ±Ø½{wo‡D &ÐDDD&Ìœ9fffصkW«²Œ)S¦@*•bãÆØ·o 99¹7¤û`Û¶mˆˆˆÀôéÓqàÀܹs§·C¢ ÝÅÚÚQQQhhhÀO?ýÔêü€=õ€üü|\¸pvvvïípèÄšˆˆè.ÑÑÑ077ÇÙ³gQWW×ê¼ ¡¡¡§C£rôèQ@\\\/GB"&ÐDDDw‰‰‰;v¬—#¡ÞræÌh4 2½=`XMDÔ ...ˆˆˆ€R©„­­- pöìY¤¦¦bêÔ©())i³&ÖßßLJR©„™™ °wïÞ=W&“a̘1ð÷÷‡³³3 pýúu>|&ÇH¥RcĈptt„B¡@UUòóóqîÜ9deeµ#‘H0räH <Duu5rrr°oß>“3²#GŽ„ŸŸvíÚ…Û·o#88AAA077ǵk×P[[ âàÁƒmÆ9kÖ,H¥RlݺÕhv·»ã9|øp›¯±\.‡t:._¾ltn„ ppp€¥¥%`òäɨ©©Îoß¾Ó§O9rÄdû;???B§ÓaçÎÐëõ­®‰‰‰Á€pæÌdddÀÃãGFii)öìÙc2îÙ³gC*•bÇŽÐh4F¯…··7¶nÝ KKKÄÅÅA©TÂÁÁ¹¹¹¸|ù2Î;׿ë …ÑÑÑP*•èß¿?JJJ––Önëº'žxµµµ8xð ˆÈÈH 0R©8}ú´ðú( <úè£ðöö†‡‡Ôj5~þùg=zÔäëc ös¨P(0mÚ4ܸqÉÉÉppp@tt4<<úÈhŒ –,Y‚aŒD"Á£>Љ'båÊ•¸råŠÑ•J…±cÇ"==±±±ˆŒŒÎÉår$''cÊ”)Ðh4HNN6Yú„iÓ¦áêÕ«FçïG<í%ÐJ¥2™ ùùù­ÆÆÆbРAÂÏ¿ùÍoŒÎoß¾Â5›6mjuÿ©S§"44žžŽ«W¯—H$˜7oìííqèÐ!€««+¦L™‚ŒŒŒ6è§žz 2™ ßÿ½Qmx- Iª½½½p.00“&MÂáDZ~ýz“‹å|||ðòË/·zßÅÆÆ¢²²Òd,†×ÆÉÉ *• Fïùàà`L™2ÿüç?!•J±`Á£YÞ!C† ..qqqøàƒL¶’ëÌçP.—cÊ”)HKKCuu5–-[†>}úç“’’Œž‘ •J___&Ðd„ 4‘¡¡¡HHH€N§Ãwß}‡“'O¢¼¼ÞÞÞ7nœä™2gÎaVnÓ¦M¸|ù2´Z-|}}1kÖ,£Å‰D‚>}ú`ÕªU¸zõ*ôz=yäÌ™3žžžx÷Ýwñâ‹/%} ðòòÂ… ˜˜ˆüü|èõzôë×þþþ(//oõŒ·Þz ~~~8zô(¶mÛµZ gggLž<cÇŽÅÒ¥K‘`ræ÷•W^™™Ž?Žôôt455!77YYY¸u뜜œ‚'N´k(›h™ÄܯxÚ£T*Àäu«V­‚……Þ{ï=XYYaÅŠ¸uë–p¾¡¡ÇŽàAƒÒ*¶´´ÄðáÃ…Ÿ###[%Ð>>>°··G^^nÞ¼Ùn¬bLš4 iiiX»v-²³³accƒððpLŸ>±±±¨««Ã—_~i4¦oß¾xÿý÷¡P(pæÌ|ÿý÷(((€‡‡ÂÂÂ0iÒ¤vŸ)—Ë¡R©°uëV\¸pÅÅÅpuuÅôéÓŠ7ÞxR©©©©X¿~=rrr “ɂٳgcèС˜6mþ÷ÿ×è¾]ù€¯¯/þô§?¡°°û÷ïG^^lll„Ùgƒ7nر™~5˜@uL&ÃܹskÖ¬1*E¸pá.^¼ˆÅ‹#66¶ÕX777L˜0MMMxóÍ7£’’\¼x|ð0SlŠN§Ã«¯¾Š²²2á˜Z­FJJ V¬XÄÇÇãÛo¿˜››#00z½ü±Qb}ëÖ-£ÄÏ ::~~~HIIÁ§Ÿ~*/**ºuë`ccƒˆˆLš4 [·nm5¾¡¡ùË_Z%…pðàAÌœ9ÑÑÑ­hKKK„……A£ÑàäÉ“=O[lllÕÕխ΀0#š——‡ÂÂB£kNœ8yóæÁÓÓNNNF¯sHHÌÍͱwï^Œ7‘‘‘ذat:pÍÈ‘#Àèuè‡ÂgŸ}&| Q^^ŽÜÜ\`éÒ¥7nvïÞ¢¢"aÌìÙ³¡P(pøða¬ZµJ8^]]ôôtäååañâÅí>wÅŠ¸té’ðsVVþçþü1 €à_ÿú—јݻwC§ÓaÁ‚5j”Qݕϡá[ˆ5kÖ´»QŠá=`xOp!QÂÝÝùùùÂWë-ét:“õÄ0f̘™™!))Éä¬bmmm«Dìnz½Þ(y6¨ªªÂ–-[4'œ‰DøÇPêq/ãÇìÚµËäyCÏcÃÖâwÛ½{w›Éê Óé0bÄ£¯Íæ¤ÑÒÒÇŽ3Jôïg‡gx´|>À"¢3ü»´´TôX77·N툚šTWWÃÖÖÎÎÎBýîªU« V«1zôh„‡‡#<<z½Ø»w/Ž;&”ØÙÙ 3m¯½öZ»ÏÓjµŠ3)) ÁÁÁˆŽŽ’e;;; 6L¨•6è‰xL1Ì€·œõëÔ©SX°`† ¹\ކ†Œ1r¹\XŒ–œœŒçž{‘‘‘øê«¯ Óé„ÙgS5â÷“Z­8:: Ç 5ù÷ë=ÛY]ùŠexܽ˜”ˆ 4Q:6ÑÃð?àÎŒí(sss0š-¬¯¯ÇÆ‘˜˜ˆ‘#G",, LJ¿¿?üýýŒO>ùz½uuuÐétJ¥X»vm»:[ÞpþüyTTT`È!èׯÊËË ©TŠýû÷]Ûñ˜bXŒheeÕé{ܾ}©©©P©T ÄùóçN‡S§Nh.½¹téT*y䤥¥ Ý9º»|ã^ ßZ´LJ ïÙö¾±è ]ùŠex˜Z J¿nL ‰ˆ:ÈP£lèÒ FQQ|}}¡T*…r‹îd(ݨªª2êKlÐÔÔ„'NàĉÉd=z4.\ˆ¨¨(¡N¸±±jµnnn¨««Ê?º“V«Å¡C‡0uêTDFFâ‡~@LLŒÐ½¢¥žˆÇC2ÞÕ…cÉÉÉP©TFjj*FŒôôtTTT×;v *• QQQÈÉÉÁ!CPXXˆ¼¼<£{¾%¸_¥†YÝ–õù†Å„îîîÂ,úƒ +ŸC± ïîü~XMDÔA†ò‚!C†Àßß¿ScÇoÔ·V ‰D©Ôô_Û&LüüóÏ÷¼V«ERR’°¸ÎÓÓS8wíÚ5Í-Ïd2Y§â¼—¤¤$èõz<úè£ððð€N:e2ñï‰xîfè´Ñ^G”Ž8}ú4 •J…BѪ—ðéÓ§Q__ððp„„„@&“™,ß0´Wsssk³fø^|}}M¾¬¬¬½^´´4á¸Z­FUUärù=ÛÕõ¤®|Å2¼º³ ý20&"ê ÜÜ\!zå•WàããctÞÓÓ'N49vß¾}(--…­­-^ýõV_?‡‡‡õ6E*•âµ×^k•€5 “&MBcc#6oÞltθÝÍÚÚ...`4Û™˜˜F???,\¸ÐäØÎþ` V«qùòe¡ÿ5Ðz‹žŒçnÙÙÙÐëõpssëR´F£Á… àèèˆøøx£ò ƒúúzœ={vvvxú駘®.**BMM är9æÌ™c”K¥RŒ;¶Í_® BCC±`Á£ë,--‘€>}úàØ±cÈÉÉÎét:¡5`||ÿüsüþ÷¿Ç˜1c R©pñâEC¡PÀÓÓÆ Ã+¯¼bÔ/X¬¤¤$!22ÅÅÅmv'é©xZª­­Eqq1ÜÜÜàçç‡ÔÔÔNß+99áááðööFjj*ªªªZ]säÈDGGÃÙÙ………&7pÑétذa–,Y‚‰'"00™™™°²²Â AƒàèèN×fK8ƒ±cÇB¥R!-- r¹°··GAAÉ]÷ïßððp"!!S¦LAvv6ìììàëë 3³ÞI#:û9C&“ ;Jfggwù~ôËšˆH„[·naÙ²eøÝï~‡ÐÐPa1^cc#öîÝ‹ŠŠ Ì™3Çd›¬óçÏãõ×_Ç¢E‹0hÐ aƹªª ëÖ­ƒZ­Æ»ï¾Ûæ³÷íÛµZéÓ§ cµZ-²²²ðÅ_´š%Óëõؾ};ÂÃÃ1xð`£~Ã;wîĶmÛŒ6ñšºŒŒ ÌŸ?ˆ‹‹Îݹs§OŸÿÂÝåôéÓB×´ÛV¬'â¹Û‰'0mÚ4DEEu)>þ<4  E›  ‰µ]»‹:½^/lÒâéé ½^ÌÌL¬^½#GŽlwæuçΰ²²Â˜1c„MFêêê””„¯¿þÚd§ ­V‹?ÿùψDŽ „çêt:¤¤¤`ýúõx饗fwëÊç°£‚‚‚`ccÓæ/5ôë&quuíü»‹ˆè!Vü\sـ׿ïÔx©T*$………Ðjµ˜1cfΜ‰mÛ¶ ››˜baaOOOTTTˆî±+•Jáââ‚>}ú 77÷cii GGGØØØ ¼¼¥¥¥jý&‘Hàì쌾}û¢¼¼eeeÝÚ2N¬žŠÇÕÕ«W¯Fmm-æÏŸß¥žÐÝM"‘ÀÅÅÖÖÖ¸yó¦Ð•¢- .ÄØ±c‘˜˜ˆ;vÀÜܸs犋‹E%™ÎÎΰ±±A^^^‡Þw=¡+ŸÃöüá@ll,6n܈ÿüç?ÝuïË}úG€ë†Çz9’‡g ‰ˆ:I§Óµš™2ÔßkŒ;wîÜs·´öž+¶\¡¾¾^X'†^¯‡Z­6* éM=Oqq1._¾Œ   ÄÄÄmÝÛôz=Š‹‹;=¾±±7nÜèÔØ’’aAモ+ŸÃ¶ôíÛ‘‘‘hllÄáÇ»"ýq!Q7±··Gxx84Q7z8mÞ¼z½ñññ=Ö„º®;>‡S§N…\.Ç?ü`²nˆ 4‘H3gÎD`` Ð“W&“ÁÏÏo¿ý6,--±eË–v7ý ‡CVVŽ9ggg„……õv8t—ûõ9´´´D\\ªªª°}ûöî›~!XÂAD$‚££#f̘!ü\SS…B™LN‡;wb÷îݽ!u§Í›7ãòåË=¾3 µï~~ëëë±|ùrôë×[xS›˜@‰PZZŠeË–!66J¥NNNÈËËCnn.Ž=ŠŒŒŒÞ‘ºQyyùC_›™™ ‹V»>Ìî÷çðÆ®§_&ÐDD"åääm8Aô ;xð൲»ðsH½‰5ÐDDDDD"0&"""" 4‘L ‰ˆˆˆˆD`MDDDD$h"""""˜@‰ÀšˆˆˆˆH&ÐDDDDD"0&"""" 4‘L ‰ˆˆˆˆD`MDDDD$h"""""˜@‰ÀšˆˆˆˆH&ÐDDDDD"0&"""" 4‘L ‰ˆˆˆˆD`MDDDD$h"""""˜@‰ÀšˆˆˆˆH&ÐDDDDD"0&"""" 4‘L ‰ˆˆˆˆD0ë툈týúõÃòåË»å^•••°··ïò}¬¬¬ Ñh ×ë»|¯ÆÆF˜››wù>uuu°²²êò}d2ÌÌÌpçÎ.ß«»^o™LN×-¯·™™šššº|Ÿüü|¬\¹²Ë÷!"ñ˜@݃™™Ø-÷ÊÏχ§§g·Ü«»TWWÃÖÖ¶Ë÷)))³³s7DÔ}Ä×[£Ñ@¡Ptù>Z­¶¢!¢Î`MDt·nݬY³ºå^‰¤[f1¥R)t:]7DÔ}÷ê®?›D"D"yàbêŽûͳÙÝ‘üvWFfÌÅÅ}ô–/_~ߟeii‰ˆˆ¸¹¹Ý÷g=8MD$ /¼€Ç{ :ï¼óÒÓÓï9fÍš5°µµÅï~÷;”••õ@”=kîܹ7n4 æÏŸ;wîôvHDD÷g ‰ˆ:A*•bñâÅ¢f¢©ÌÍÍ2™ R)ÿ·BD¿|œ&"êN777<ûì³øòË/{;œ^µiÓ&äää ;;¦·Ã!"ºï8U@DÔ û÷ïGCC&L˜€¡C‡öv8½ªªª »wïÆµk×z;"¢Ášˆ¨òóóñïÿ‰‹/†B¡è툈¨‡°„ƒˆ¨“~øá„……aðàÁ˜;w.Ö­[×é{ :C† R©„T*ENNRSS‘––Öî8…Bèèh(•Jôïß%%%HKKëPë:'''„……Á××¶¶¶ÈËËÃùó瑚š**v///DGG£¨¨huÞÇÇÑÑÑpvv†½½=ª««qëÖ-¤¤¤àâÅ‹Ðét¢žGDÔÛ˜@u’N§ÃªU«ðñÇcìØ±8uê.]º$ê …óçÏÇèÑ£Žc¢Æ IDATúôéØ¿?¾ù擵Å>>>xùå—áîî. @ll,*++Û}nTT-Zd4s„I“&áàÁƒX»v-´Zm‡þ ýû÷Ç”)Sššj”@K¥R,Y²£FŽét:a¡aLL žþù=ƒˆèAšˆ¨ Š‹‹ñí·ßbÁ‚X¼x1^~ùeÔÕÕuxü²eË0|øpäææbÓ¦M¸~ý:úõë‡ððpL™2¿ùÍo “ÉðÙgŸëÛ·/Þÿ}( œ9sßÿ= àáá°°0Lš4©ÍgªT*,]º•••X³f0 ŒU«V Ç«««‘žžŽ¼¼<,^¼¸Õ3e2ž{î9èõzüýïGFF†pîìÙ³())Á?þñ̘1»wïFCCCg_Œ1°aÃ\¼xQ8^UU…”””Nß—ˆ¨·q!Qéõz¬^½qqqîи‰'¶nÝjró‘³gÏ"%%‰ÑÑÑÂq¹\ŽØØXhµZ$&&š¼wff¦ÉãC‡EÿþýqíÚ5£äÙ 77×®]ƒ……ú÷ïß¡?G[ ¥]ºу† 4Q7(++úA/Z´ÖÖÖ÷ãéé í.<}ú4ÀÃÃC8æîî©TŠ¢¢"Ñ;žyûöm˜üÇÌwukîãÇžzê)üíoÓO>Ù夜ˆèAÀ"¢nrèÐ!„……!44óçÏǧŸ~Úæµ¶¶¶°µµEcc#ªªªÚ¼îæÍ›WWWá˜aÑ`ii©è cCCCÚîµz½^ôý[úé§Ÿ ×ë1qâDøûûÃßßsçÎEaa!Ž=Š~ø¯ÑC‰ 4Q7Z»v-Œ˜˜œ:uJ˜A¾[CCôz=ÌÍÍammšš“×¶ ¿uë–p¬¾¾`oo/:>Ãs._¾Œü±ÝkM•xˆµgÏìÙ³ˆŒŒDpp0ÜÝÝ1kÖ,DGGã­·ÞjóÏNDô bMDÔ*++ñùçŸcéÒ¥X¸p!ÒÓÓM^W__²²28::bÀ€¸råŠÉë ¥±¢¢"ͳÉr¹\ÔB?Ã}pöìÙëª+W®àÊ•+øâ‹/€¥K—ÂÃÃãÆÃŽ;z,"¢îÀh"¢nvüøq?~vvvX°`$‰Éë ]5¦M›fò¼L&ÃøñãÀhsµZªª*ÈåòvÛÕ™’™™ Nwww„„„ˆ üwa`W\¹rûöíðßšìŽ<£½gwG\DDÅ¿qˆˆîƒÏ?ÿ•••ˆŒŒlsAá–-[ Õj1|øpÌ›72™L8gnnŽE‹ÁÍÍ —.]2š-Öétغu+ >>Þh£°²²ÂܹsM>³¨¨»wï¼øâ‹mvȰ´´4úÙÍÍ +W®Äš5k:ÜUÃßß­ŽK$x{{òòò„ã2™ o¿ý6¾ùæ¡C‰AXX¾øâ üý­­Ñ¹Ù³gcãÆx饗:QW±„ƒˆè>¸}û6Ö®]‹7Þx£Íèüü|lÙ²³gÏÆäÉ“‰k×®A"‘`ðàÁèׯ ±~ýúVc÷ïßððp"!!S¦LAvv6ìììàëë 3³¶ÿzß²e üüüàçç‡>ø—.]BNNêêêàè舠  dffbåʕ˜èèha¶x̘1m–œ´´hÑ"xzz"777oÞDyy9d2† ‚ ´´®÷÷÷ÇðáÃO>ù¤èÍ-ÿììì`gg‡aœL&ÃäÉ“annŽ˜˜lÙ²jµúž±uh"¢ûäìÙ³8xð âââÚ¼æ»ï¾CVV~ûÛߢÿþˆŠŠМ€ÿôÓOøöÛoMöˆÖjµøóŸÿŒøøxL˜0žžžðôô„N§CJJ Ö¯_—^zÉh›oFƒåË—c„ ˜§OŸVqqq[—?xô@€¾¾¾j×®]ƒ?&“©Íjš4i’^~ùe¥¦¦¶Y p-¡ ˜>}ºFÕ`û¹sçd·Ûuøðaýë_ÿÒ‰'Ú :@k @@3TTT¨¤¤D’d2™Ô©S'EEE)**J6›MK—.ÕªU«.ùõíÛW÷Þ{¯òóóµk×®K:ÏäÉ“5f̽÷Þ{Z±bÅ¥– hŒ€àp8”——§9sæè«¯¾’$=üðÃm\àr @@ r8úûßÿ.I VHHHWhi á€vôèQ;wNþþþêÞ½»®»î:Ùl6ÕÔÔhÙ²er8Û3FV«U›6mRMMFŽ©˜˜IRÿþýe6›%IUUUçШQ£Ô½{wËn·+33SÛ¶m»`ͺùæ›Õ½{w…‡‡ëøñã:räˆ6oÞ¬3gÎ48Þl6kòäÉÊÈÈPNNŽú÷﯄„uíÚUÕÕÕÊËËÓ'Ÿ|¢ÒÒR#—® hhauuuª®®–¿¿¿knæñãÇËÏÏO™™™Ú»woƒ6íÛ·×ý÷ß/‡Ã¡+V(66V“&Mrí‹‹S\\œ$©´´Ôc€8p &Nœ¨öíÛ»µ5j”ÒÓÓµ`ÁÕÕÕ5h׿͘1CAAA®mñññ’ê§È{õÕWµgÏ·6~~~š4i’’’’TQQáªÍiÀ€;v¬þð‡?(++«)— ®hhaÁÁÁ PMMŽ=ªššmÛ¶MÆ “Ífó ,???¥¥¥©ªªJûöíÓ“O>©ýèGJNNÖêÕ«µ~ýzIRmm­ÇóFGGkÇŽúôÓO•——'³Ù¬äädM™2E Є ôᇺµ‰Õ³Ï>+‡Ã¡>ø@Û¶mSyy¹¢££u×]w)22RÏ<óŒfÍš¥ãÇ78gDD„N:¥¿þõ¯ÊÍÍUii©"""t÷Ýw«wïÞzôÑG5sæL={¶®,\ -Èd2é¾ûî“$mÞ¼Y555’¤uëÖI’RRRäííݠݰaÃÜŽ«ªªÒÁƒU^^.I*..ÖÁƒuðàA9rÄã¹ÿûßÿjÁ‚Ú³gÊÊÊTXX¨U«ViÙ²ençpòòòÒC=$“ɤ×^{MK—.U^^žJJJ”žž®§žzJûöí“xà笫«Ó£>ª5kÖ(??_ÊÉÉÑ‹/¾¨ÒÒR©OŸ>® \ùÐÐ :tP—.]\zõê¥ÔÔTÍ›7O)))²ÛízóÍ7]ÇçääÈn·+00P}ûöu{­àà`ÅÅÅÉn·+77·Ù59Ãö÷íØ±C’îÞãââ¥üü|mÚ´©A»óçÏ»ÞCbb¢ÛÐ'‡Ã¡êêêÛ«ªª\Ã>ºuëfüÍÀŒ!Ð #FŒÐˆ#lw8JKKӻᆱªª*·}ëÖ­Ó´iÓd³Ù´{÷n×v›Í&“Éäê}ni§N’$ùøøÈ×××5$""B’”››ÛèƒÐéÓ§¤ððpíß¿¿Éç-..–$×ÃðCA€€f8qâ„òóó%Õ‡æ'N¨  @ûöíS^^žÇ67nÔÝwß­Êßß_çΓT?´¢¶¶Viii­U¾¤úiI:yòä;~ü¸‚‚‚Ô¹sgC~¨ÐÐ Û·ow[‰°)ÊË˵sçN ë'N\¶s8yÜ^SSã1]Ÿ}ö™.\xIuX,éºë®Sii©Š‹‹UUUuI¯ÙÒ5cÆ %%%¹m_¸p¡>ûì3õîÝ[íÚµÓž={TWW×FU^9"##ªŒŒ UWW·u9ÀU — oß¾úío+I*//×ôéÓU[[ÛÆU5O@@€Æ¯Áƒ+""¢ÁþcÇŽióæÍZ·nÊÊÊÚ Bw¿øÅ/”””¤ÌÌLmÚ´IêÙ³§rss«¹sçJú6P_Ë,‹^yåy{{kùòåZ¶lY[—\ÕÐp †.©¾÷900PýúõSFFFWe\rr²f̘¡öíÛK’*++•ŸŸ¯Ó§O+00PÁÁÁêÚµ«¦Nª;w¶y€× AƒT\\¬y󿩦¦Æm—.]\ÿíçç×Ú嵺ˆˆùùùÉn·{ü¦ÀÇÇG^^õ=] ׸ÜÐÐLf³Yƒ Ò™3g´yóf;V6›íª УGÖÃ?,“ɤôôtýãÿPaaaƒã‚‚‚­üüü6¨Ò]dd¤$éСC ³$eggë÷¿ÿ½üýýµuëÖÖ.¯Õ=ñÄ ×3Ï<£½{÷6Ø_\\¬9sæ¨K—.Ú²eKTü°  ™RRRäïï¯-[¶(--McÇŽÕÀåïï¯sçεuyMÒ«W/=ôÐC’¤E‹]0\>}Z;wîl­Ò.Èb±HR£×¹®®NÛ¶mkÍ’®x{öìÑž={Úº àiì ™œÃ76oÞ¬¨¨¨HíÚµkðPÛ•ì¾ûî“···Ö®]KÏ$4=ÐÐ ¡¡¡ºá†T\\¬ÜÜ\IÒ矮‰'Êf³]tØ€Ùl–ÍfSTT”ºvíª¢¢"egg7iê:«ÕªäädõìÙS;v”ÝnoVokLLŒbccUYY©þóŸ†Ûß7Þ¨øøxEEEÉËËK‡Öž={”íñø***JË—/W‡4bÄEFF*$$Dv»]™™™ ÞWxx¸†®=zH’ºwï®{î¹Çµ?''Ç5„ƹýƒ>ðØSÝ®]; :T={öT—.]TXX¨ÜÜ\¥¥¥iðàÁ ÕŠ+\Çûøøè§?ý©$éý÷ß÷ø°èÈ‘#¦-[¶(//ϵ=22R6›M;vìÐþýû¥””uìØQ………úè£T]]-“ɤn¸A‰‰‰²Z­ ÒéÓ§UPP 5kÖ¨¤¤Äí| ŠwõÈßrË-®pGŽq{xr„ ²X,Z½zµNž<Ù v///¥¤¤(::Z‘‘‘*//×áǵcÇŽF§Eœ0a‚Μ9£ 6(""BC† QDD„Ìf³Ž=ª-[¶èÀÛW344Cjjª$iË–-®)Òœ:11Qª¬¬ôضgÏžš5k–ÛƒnqqqJMMUiiéÏ;tèPýüç?—Ùlvm»ñÆ5f̘‹¶ý¾ÄÄDIÒ×_Ýh­Ma6›õàƒºzänºé&ýøÇ?ÖÚµkõÖ[o5x¸-!!AcÆŒQXX˜’’’ÜÞS\\œF¥ôôt-X°ÀuCCC5iÒ$×q]»vU×®]]÷òòrhçq~øaƒ­Ù³g+44Ôíœ#FŒÐ­·Þª9ŽÚùšË–-ó ‡ ¢¾}ûêðáÃnºk×®š4i’ª««¯»ï¾ÛõPß©S§´bÅ µoß^ ,PXX˜ÇëçWæ7Üpƒ‚ƒƒÝöùùù)55Uµµµnñ» }úôQ—.]”ŸŸ¯76Ø_WWgx¬©O}Š···¡vß5nÜ8IÒòåË=Ž3NOO×îÝ»e2™d³Ù<¾Fc3iìØ±CRý¸çK©ñ»üüü4tèÐf݃–Ýhx¾ÚÚZW@õ´ÈMsùøøè–[nQmm­Þÿ}Ç,]ºTeeeêØ±£úôéãñ˜Æî¡s¸I·nÝZ¦`à A€œÃ ¤ú±«½{÷vûc6›U^^.“ɤ!C†¸µíÒ¥‹¼¼¼tüøq:oxx¸¤ú‡ä¾?µ¹ÊËË%I!!!Í~ g˜kìAAÉ=qêÔ)Iõ!Ï××·™º»”{ÐÚ¼¼¼¬^½z©oß¾®o œ‹Ý´„°°0y{{«°°P§OŸöxLUU•kú;£÷Ðy¿;¾ø!`0xð`µk×N’4{öì k³Ù\C¤oWÇsC#.¥mcìv»úöí«øøøfµ T`` ª««/¸2aAA$¹=°×Vœ°%¯cKr~ð>|¸zõêuÙƒ§óLO³r|—s˜OçÎ ½~K}Ø®4h0À9|#''§ÑÐáëë«””EEE)<<Ü cG=‰F8Û:§+k YYYºõÖ[£~ýú¹amŠóçÏËápÈ××÷‚³ŽøûûKºxHk gÏž•Ô¼{p¹yyyéÉ'ŸÔ€TRR¢uëÖiÿþý*..VUU•† ¦;EÏézq±ëáüàJïµZ š¨sçΊ‹‹Smm­^yå×Oºu릮]»Êf³iéÒ¥’¾íÅëÒ¥‹üüü\~5…³mTTÔ%¼w:uê”BBB4}útÍž=Ûã²Øùæ›oT\\¬uëÖM999söú^êl-áRîs*=©þCsöŒ–«Èn·ë7¿ù+ì;]èç­¹œî.v=œ=ÕWÂ=®Œ€&6l˜L&“vïÞ}Ñ0ã\Hå»Î8qBeeeòóó»àtuž8l‹Wll¬ÁÊ=«©©q=8Ö¥KýêW¿2—.]ª©S§êöÛoWJJŠöíÛ'“ɤ޽{+((Hv»]o¼ñF³j»œ÷ oß¾ïÁ…f¹xçwÔ¿ÅÆÆêÕW_Õ—_~©ššEEE)22Òm˜‡Q:yò¤:uê¤?ýéOÊÌÌÔ™3g¡ž={^ðµW¯^­j̘1êÖ­› ¤¹sç^ôA¾õë×kàÀJLLÔÌ™35qâD>|X‹Å5³ÌîÝ»õÑG5û½}ŸÍfs}Ø9r¤ÛðŸqãÆÉb±Èb±())É5çµ···n¿ývùúújèСZºt©ëçhmô@@Œ1BR}ÏrSÇÍ:{Œ‡ âúÚ¹¶¶VsæÌÑòåËUQQ¡ˆˆ¥¦¦ª_¿~Ú·oŸfΜÙèX⢢"ýú׿ÖÖ­[U[[«ØØX1B=zôÐêÕ«õè£6X.»)‡–.]ªÙ³gkÆ *++“ÅbQ¿~ýtóÍ7«oß¾²Z­ÊÌÌÔk¯½Ö`Æ•+WjΜ9ÊÏÏWpp°n¾ùf¥¤¤È××WŸ|ò‰üq®ër©««Ó /¼ eË–5¸_ýµæÏŸï:îû ôÿ÷:zô¨,‹† ¢ÔÔT™Ífýå/Ñ¢E‹š]×™3g4wî\egg«]»vJNNÖÈ‘#¡U«Vé±ÇktÜuVV–.\¨ÂÂBõîÝ[©©©êر£‚‚‚št=^zé%½ùæ›*))Qdd¤RSS• ÊÊJ-Y²D/¾øb‹ŽùÎÊÊRuuµêêê”™™é¶o×®]r8ªªªrû·P[[ë:6??ÿŠx(×.Shh(s̸&Þ·N’ù¾ç±Ÿ­ÁjµªC‡:zô¨¡€âå奈ˆÕÔÔèøñã—Ôóù}&“IV«U‹E‡C§OŸVIII“Îa6›®’’’«fƫժÀÀ@åççëܹsŠ‹‹Ó /¼ œœ=÷Üs¶ëر£¬V«kl{K²X,êܹ³JKKuòäICÓÁ™ÍfÕÔÔ4;ð:Ïm·Û›¼xsÈßßßãωÕjUEE…Ç„*((hÑŸùkQÞ]ÿ“$…þcTWrub´¡¢¢"nWWW×`ñ–âp8tâĉf}=^UUexEĶöý{àœ¯úb3N”––ª´´ô²ÔTVVÖìPÞœo!ZêÜFTVV6Ð/ôo"??ÿr•4C8øÿL&“n¹åIÒ_|ÑÆÕ¸RÑ ¸æŒ1BçÎÓž={TQQ!©~ØÀ´iÓ­ôôtíÚµ««p¥"@®9wÞy§¬V«¤úa‡k쌌 -^¼¸-Ëp…#@®9Ï>û¬† ¦øøxuîÜYß|óŽ9¢Ý»w»Á€Æ \³|ÏW¨Ú¯ƒjÚw’ÏY¦Äº–kåÊ•Z¹re[—´ºšö$ÕÿDóð!€k–¹¢~‹ê–Y®ÎßyÎß0Ž àšÕîx†$©ä¦‡Uãcnãjàò«ñ1«ä¦‡%}û;Æyü®­‹€¶`²gÉ+"AUÝõMÏQò9{R¦ê*yUŸmëÒ EÕ´ï¤sa‰*N}^Õí‚Xü¥üÒ´uYW-V"pM3uìª3¶_«<¸O[—­"°øK]·år”^x± 4Ž ’꒦雰DUuˆTµ_‡¶.Z”ïù ™+òÔîx†¼¾x»­Ë¹ê xˆ0€ @€ @ ЀhÀ4`0€ @€ @ ЀhÀ4`0€ @€ @ ЀhÀ4`0€ @€ @ ЀhÀ4`0€ @€ @ ЀhÀ4`0€ @€ @ ЀhÀ4`0€ @€ @ ЀhÀ4`0€ @€ @ ЀhÀ4`0€ @€ @ ЀhÀ4`0àÿÉ!Œ¿>U½IEND®B`‚fwupd-2.0.10/docs/device-emulation-assets.png000066400000000000000000001452261501337203100211140ustar00rootroot00000000000000‰PNG  IHDRÔ¨§«­ IDATx^ì €NUÆÿ³Ø÷}ßE›6©H¥ˆˆTŠ …ÙÉ"K–]Y³—T"T$%mZµ~”%û¾/ƒY¾ÿsÞ9¯;ï¼ÛŒƼÏéóï{ï¹çüιwÎsþË ;tülœ„P‰‹‹“˜˜Ñ¿$6.Öü,ñð]z/aaaÚwe#áa¦»±q1!ö»ôÌÀŒ½v0Lÿ‹ÓÿÂô爇ôRB¡WÊXq,.ŒY\ü¬ %†¡þ»*¹³…Ü’KîÒœJ÷ð¥!zé¯Â{L×Ъ%LÑ¿°† ×Ï\?»¿»ôCsÙ®˜/g¦°°PÔ Qéúãâq*bÒÅ Öu?è2 Ü›'±î~ÇãH?à }¼RF‹cqa¤Èââgmˆ1 éßU1[Èí"à¥ö©!v§6ÎËU?ï1—aÊ¥!\Û i êË5+/Óu­5Úi• µ?ä陽ÉÑG»Ãèüì2MŽl(ô1E¥be‹ pÉââ'Z¨1 ´(KÏ¿«.f¶ÛÅÐKÝsCíN]š—¯vÞc µÔVXÇ[­/ßÐ\¶+‡¤…Ú“v(ÿRNob2)wR(¸·‡B“2æ—óXŽEBqÊÏÝ”˜‡¡6ŸBùwÕÅÌr»z©{n¨ÝéKóòÕÎ{,¡¸¾|#qy¯LA}yùóê$@$@$@$@$@$@W( ê+tàØl      ËK É‚:Ôâ/ïððê$@$@$@$@$@$@©IÀ(NÞ³5tjë&     HK’"°ý joY°õŸT&t3½¥¥ÉÀ¶ ø~E²/«t0YÌ} jûŽfOð±±®÷»Š}—q輿™‘H€H€H€H€H€HàÊ'pAH_гááá‰:æ|×¶ç—^µ71m…´¾¾WX®ã Ñö3Ú¥¯ü‰Å @z$`¥³[LëNKtü«µÅSXûÕ‰µ§˜¶ÿ¶B:k挒-K¤D„S:§Ç Æ>‘ @¨ˆ‰“Sg¢åtÔ9#°!¬=E´7QHP;]º­˜ŽÕÊñsžœY$sƈPcËþ’ „3g£åè‰(#¦ÃÕˆì)¢=-× µÓ:íúY?gÏ–IrdÉÙE      P%püô99¥\¢:Üm­OíUPÛxhc™Öÿbcb¤`ÞA7ïPTì7 „è˜XÙø¤„GDhn—¥Ú)¦YÁÝ‚ÚÓ:mݼãâb•ºhÁ\ñ9½C!ûH$@$@$@$@$@$Џl÷þcñÖiX¨/¸;…5~N$¨]Öé0#¢c!¦Õ:ŠÊŠ,Ùg      #°sß1ãê +uxXx|Öï8·Ë·µRA}ðX”Éž £7œ½UTǨ¹;FEuÉ"yB !»K$@$@$@$@$@$жï9"*¦#"ÔB8jüç‘ù¢: veø‹Ö.1Ï¢c¢¥tÑ|¡È‘}&     1ÛvÒb‘Æ2mDµZ©]Vé8÷;ª½ êxo·u:::ZbTT—)FAbsˆÝ%     $°u×!‰P1é¶R«–v¹ëç(>µÉí­¾áê£qÔ±Rš‚:$';M$@$@$@$@$@¡F`› êpµLG„Ãí[³}ãÔñnß^µ}ï´+)™¾*+^PG#)™þ)]<¨1dI€H€H€H€H€H€BÀ¶%L…t¤‰£ÖÄdæÕYˆ£¾K ,ùsev%%s jرUTCP[wïXuû.S¢@bd—I€H€H€H€H€H€BÀÖ$îÞñnßFPCL;’“%ÔÑxÙ4²{ µº{ÇD»\¾Ë” …:Ô&ûK$@$@$@$@$@¡H`뎃.—ïHµP«Û75²}Ãþl~6 Ê<,ÔÑøÿCvo$#‹Që4u(N!ö™H€H€H€H€H€B“ÀAí²R›¸i©õ¿ 5â§cã“•-N—ïМJì5 „-;¸,ÓñqÔA j˜§‘—Ìõþi êК6ì- €7Aíòò`¡¶‚ÚõÊ,¥Æ{¨ãb…jN*      P `u˜ºzÇ'&3¯Îò/¨]Vi êP˜ì# €/þux¼¸N”,± 6‰Éh¡æ,sÀœ8rô˜dÍšE²dΜ.Ø ?g¢ÎJ®Ù%[¶¬ÉîS zðºýK¸nm.tir\Žk& O$    4BÀ êµP#~:¡…ú2 ê³gÏÉ'«×ʆßÿ–£ÇŽKî\9¥Bù2ò`{%Göli_àfÀœžÆ Þ1!ŒIp1åÇ_~—Þ¯Œ”cÇOHüyeÅ¢S]¢s×ÿü›|õÍRªD1y¬á ¾_þéùß?[ä–¯“{ïº#E¯ÛkÀkòùÚo¥kûVòÔc ’]w z6oÝ.O´êlDûËæû½ÎFíëg_|#Ûwí6Y÷ Ì/÷Þ]UnªtM’Ú—”kSñå£`ÚÆcH€H€H€H€H ¥¤  Ž‘èØh)W¢àE·m˶Ò¹÷`Ù»ï@¢ºrªupìð~RéÚŠ}Ô®V¿:¶”ukJ—Z¤öå.ºþyï.‘qoÌ’/W¼-Y³$ßªÜ©× ùvý/R÷þ{¤Þý5äŽ*7]tÛœÌZð¾Lš6OªÜrƒL=0AÝ}’Uk¾–'Õ—;<›¢× $„ƒ½X z°ñéê¯$C†Hy¤~mŸÕŽ{s–Ì[¸Äë÷õ”}ÿ^ƒÞIiA}¹Æ(Ø1àq$@$@$@$@$6ïØ/‘áúÊ,}õEZ¨SFP#ÉYãdûÎÝR®LIéÑé9©xUY9tøˆÌ˜»H>^õ¥,OÏC2¨Y®±(Õj_š}øÈ19¯b6w®’)cFcá>{î¼äÉSΜ‰’»öÈõ×Tðù¹»ïÀ!Ù½gŸ±Ž—)UÜÍûüùh9¬î¿UðäÉKp=ÔY²xóo”“§NËZµ¢:VmP[Z5o,òåñi©FÿvíÙ¯VÉ,RZ-¯žVâsçÏËŽ{´ÞSj…, …ÔéYpM´ K/*Ø|ð,ÞúË48¾ØçUÙøïVyoÎ$É›'—Û ˜zíuö<${ –·üg]µÛn–|yó˜¯q­ÿí4ÜóåÍm,ÌÎâkœ<û±†¶Ÿ:}F²©ëyöx7í³çÎiN˜ùƒ~Ú‚ãþݲ͌7Úa¥ž¹sê<Ê”Q¬î¦Bý‰G”MÊ ãT¦t óÎ9gñÇÌ› ¶}G;³dÎ$5.ß°ð{++V~!†3ó©U³Ç¤¾znäÔv®ÿiƒ 3Å0îÙùyyüẠæí?Ú?x/Z8AÝþõ¾ýunî3 qOF*7'3ô5»~©÷ÿ›ÿ“²eJÈ–½é‘’c”EÖA$@$@$@$@ÁHs‚zí×ëåÅ~ÃŒPh.âC:rü4)^¬ˆ4xà>#&î©÷¤éëçÍs ÀæmºËÿ6m–‘ƒ{Kê·÷ÍúŸ«îLåJ?¬YìóóÓ*ºû«þrÝ÷nŽ×V,/£_í#ùUÿù¿¤Å =¥|ÙRÒD… D,„F¿îíŒ[úè‰3ä÷—%‡OÞŸé—ö ˆóW†“•Ÿ¯s[¼Xa9¨·©ßôMÝŒq ÄÞÚRU…êÐþÝÝ"qòôy 3êCA[žT+mÇ6O›}‚X¼¯A³mÅ&ÀKÝ^@õzN´Ûk6ŠOZçúƺ|ÿöçF€ce箽îS°I1¸_7©¨®ü(¾ÆÉóIÔcÕâ>_¹8-Ö_}û£tÓ̓«Ê•–Óǘê¿ùþgu¢å>u›>e,íÃt—Z5ît êž}J¾Ð¹ñ÷ÆÍæØk*–“7Ç q[õ1óÔþøŸ´{±¿™¿3& 3">Ëw³çº™ÍX°û¼øBD?¨K<ÚWóžjÆõeñ²•2~Ê9yò”ûØ;ï¨,_ê,¹Tˆ{Ô1÷ê³ÇvïØZ¨u·ùhÎÛ‹eÂÔ9Òø‘zòçßÿ˜{ãÍ1ƒå÷¿6-¨SrŒ<ç ÿM$@$@$@$@©I Í ê‰ÓæÊì!0eì¿}‡H FP÷xy¸Ã7\wµ-RP:·m!¾>3ù-Y°h©Ô©y—t|þiùò›õ2rÜ4¹»Z#ªÿÙ¼MžjÝÕĸšÜHãx¿Ôpˆ¢UÎ6–Ø©³Þ‘5_}'wßy›Ù¨vû-jÕΠOsÞQA¢Bçºk®’Á}»aýæÌr£ÆÀN?Tà6^£~SciǬ½,ýD¼÷‘Z½—Z=%þø[Zwì£}«(½º¶51Û¯Oš)ˆež4ê¹­òâ¯O#ö4|^8Ê´íÕ—»IéR%Ôš&`½ž„ ‘ו߮Ý{åô¦®5±ï=ÝÁˆ¹O52V­Y'‹>üØôgñüÉ&q™¯ñð¼†ÔeÕ2üŒÖç,‹>\!üµÉ- ƒkØ y¸é &Ä ^íÆ£àÃ埩7ĺQëÞ˜±BVm¸ñ*_Eã\#ÂáÒß´qàÆÂ)¨1§Z´ë)Ѻ2eÜ«fs!û5,ÌÕhbº ñZù¦ëýÞ'ßþð‹tê9H²¨?Üà‹)$Óf/”Ÿ7ü)wU½U^Ú×ë5[µïm„1ŽAßÐ.Ì#ÄÙÏœ4\®»ú*³i„Í#Ü[óç“*•oй^Sçý·FP_ê1J͇%ë&    OiNP5I–¨˜ Ó_ VP[sû­7ÉÄ‘ÜUúúünµzÃ5|Ñì Rº¤ËÕûá§ÚÊî½ûå³%s䀺GˆòÎ[ã¤\é’rBÅ¢µò.y{Š-\ÐXî`ÁƒñC éCêÞ ÷lˆºO[2×Xš!Zë<Úˆeëé`Ûwß=UeÄ+=Íå±qñ¶nnÔ× “êâ 3[O맛̫ñ#ú›±E $¨1g|¼µ9öƒy“¥„zlø+ÈG |ó&K§¶Ï˜Cðh³væg$;~âd«¸õ“OÞË>ðò«cä“ÏÖº-ãï.^!ðÁFÒòEÓÝsòr‘_ü’H€H€H€H€R˜@šÔ£&L7ñ—Õn»EÆxÙow“*¨Ûª5÷YµêÚb…ós¸¹Ö}¬•9ÄŠQül]©ßš<ˆj+zm}ÔÖïÌT‘­±¦Áj¸q#6ü×ßþ2qÙÊÇ4¾né(}v»„C$_­1å ø÷)a}ºmwŽ’Ycp‘åùñ‡ë«z0}‚û³§ T¯¯ÁñÔGLeŸ|.?x¿ôU—x[lò²ç[ÔÜ\Öä׌wƒ¿‚‹=*Ú±Ae]µq<Æñäð+·ÓÍ|™fK8|¼ëí¶Ì~û™8u®±ŠÃ:nµg‚¸Ë5F~AðK    HaiNP¸|•¼:j²IõÑÂi‰\¤W~þ•Që/²|;5,Ç([t4.מBÈóuG¾’CÝÿ°ËŠ×[­½ž¯èºõæL,³·×Z Ÿ6ñÙIÔí»¿bIáO׿_N«EÔº^[Ad^ˆû…•.¸H†…‚ømij¢ ÞîâˆwE¼2 DÜçõ ÉÐ<u z!"½OA=øµ‰²ôãÕòºI¿Ü³ƒû”öÝhß+ e¿¶'&'†1¾Hp‡òÙ_Æ6†ÚÆí#ÑÛ2s¶3¬ ´‚:f¶ŸðH€;<•{`Ÿ.æÒ5Ž©÷ø³ÆS Ò+ï,HpþÚ÷Ýeæ/¼+T ñÒpiGÁœªV»±ôÓ4´Ç9ç´Mz†6~¸àMwõ3ç-’7f,pgX·ýGXÃè!/¹»\cäuBòC    H%iNP#;qý&Ï—[¸JwÒ¤Zöοê;©‘¼ ÖâùšLª¼Z! ;câpC k,\¦!’#¨ÁÖ?Xa¹³I6iÜtNÈ0±žAwCÌ{¼£úÎÚ›8]ô§‚Z‰‘€ ‚ eýç˜låHä…øW› Íºã­îå°ŠnW^Xm&këfØmdÛÔ'\Ç j¼ûñáÁÔëm^z j+ºO»ð­ñæXâá¶Œñ3¬¯T¿ãV·…:Ð{ž“"Ö`ýGLºÓÝ.Êh“Ô›·é{Ÿ[v6ÉÛ ÁÙÐÒy×tóÈSPÃ̹q€M¸^c“¯ƒ¥7A¸{Œ1bЧŒìΘŽyß[7 ÏMš×õrsEÆodþFåx$ÿCÌÿ~í¯sNÛ6DD„Ëòw§»“齨w¨É^߬IC“‡ %uJŽQ*='Y- x%æ5Z¹dÅg2dä$ÓàR%‹É5Êák&Ž3& ]b­NÍ»åsuß=¦b±È°ÎÂrèËòéës$lBB1dÙFœë_ÿ5±Ð`ó¦ŽZP¿5ÿ}“%»T‰¢ZO“ŒËóUV4jib¨!¬*é†Àt½6^ñ…þÂE·`Á|Ò¦s?ámË'%ñguG¶rËkÝp¯üèCuôuKá&«3¬”=:?'Õõ;PŸ &‘èêœ^‰ÃEÖp¸÷ú«7A 7xX{±ÙXã;4–ý“ÕkeZÝa¡3e”yíTjX¨‘ÝÙÃQ`¥FAænÄÃ-Þ(va¬î®v›ñ€È…ÇARu0cáÙÏ¥:ßë|Ç«ÎÞž9Ö¼-P–oÄv·jßKðÎvĿ߬Éßðʶ_tn`îÃÃcºf Ç& <žëÔÇl<ߢ‰n ålJü·}—a˽7o³®ßxýÕòXúšÕü_“ 1æo+7l<¤„ NÉ1âsžH€H€H€H€.%4)¨àëï~2"ÖX[ãÚ\-czÀý¾Gvh¸¿BXtm×R¾ÿqƒq£Ô·‹Ô­uO’5¬§È\¼tÅjcéFåp@ïNšÙ9ŸO ¢§Ë÷NÍtýl‡Þæ=Õ°²¿;k|¢w/ÃýxºD# Ä2^UwmÄ_[‹éwš¥yê,MP¦®¼(æuaêb‹×%áÉh#²/í:*ê¬9¢"¨ýsÍÌñú„sl/ü\W]û«{v z½MVO 5ŽÙ¶}§ ý†|(°|V¯ZEûÛVòåÉm>K AzB€P„ ´Õ×^µq€¾~­°,žçŠ™†ÀÆ&æ^õÕZß¾hÉÇf³®ÌpiöÖ>O u0cáYܯ‘ ×F,9^wHP£Íب€õïfÇÏ.®Æ2Ýáùæ&É-°*™<ÓýÚ2x<<¦÷Q»ÖMÍ+Ö¼ jXÛ_Sk>B ì}pµnnõîÒÆ·‚:%ÇÈÛ|äg$@$@$@$@$ZÒ¬ ¶†%î¨Z sj|´g<³=‚.«ˆ¡FB®”*p;?xè°­pNNÚ_ÛàÂ~PÅr~ußµ‰Ð®EÛôñÈÑã&‰”ó8g›Ìµ Þõ¼|ys»]åÇê,²¨®Ä¶So°l üp ´Ïf)öÜ‹9óèÔéÓæÕNÞ <0N°ìB”¢ÔoÜÚ¸~Ï›öºû]ÙÁ¶!%™º¦m;B°`ÛïíR[rçÌž¢íÙòß.ùñ·Ò¸Á}I®7йßýô‡¬ýîWÉœ9£¼ðô£’!Cd’¯áï„e«¾Öñ*,•®)wÑõú› ]y:® &&V6mÙ.×\U:Ù½tŽcJÝwsßÿDî«VYŠ)ìv¥§·ïÚ'¹ôÙ‘+G¶ôÔ­$õÅùz\}ô¹´iþp’êãÁ$@$@$@—–@šÔ±±qÒí•qr×í7I£k¤•c'N™L™’ESíI­xÆÛIö¬Y¤ÖÝU$2"BÞœ»X…eMÓÆÏ¿þI6mÞ.mŸ~$©Õ¦Êñû½.ý:·Bò¦hý¿þù|úÅwÒ«}ó$×ëï܃‡ÊÀ×gJïZoœ¤ˆ°Áõnºî*w;'Ïz_®«XVî©zs’Ûîy‚¿¹pÑ•§Á ¶íØ#yrå0"ëbÊCGeüŒwepÏç“]sSê¾0jº4}´¶T([2ÙíJK'&u¼<ï•Y —ËÕºéqÇ-×¥¥n™¶x¶5¹ ôWçóèèñ“úŒÿQ:¶z\öì;(#&Í“±ƒº$÷Ò<H€H€HàH³‚ú—?6É»ö”‘/wðð°TÁñÙÚ$êì9©ÿ©Rr*}ùµ©Òò‰úRÖ‹ÈO©…}rÚåíœ+MPÿüûFYóÍÏòb›'SA\œnü /c^éœ*‚Úß\H‘¤±J&Ì\$uï«*åK¿ì-£ <I/o÷Jà+\ž#vªÐÛ®’/4½¨ªÇßóˆ‚ú¢Ðód  ¸dÒ¬ ;m¡ÜYåYûý¯R³ú­n  êù`ÅòïÉùèÉ©®‚Ï7k(… äóúy‘‚ùn³sÞûXþÛ¹W2fÈ êÕ›+U¸Úás|‹X{n—›®¿JÞR« \£££¥x‘‚ÒéÙÆž`Pö8, >øÔ¸eÇj£ªÝZI~àns bp÷…XߨÖäíš`´û§ ÿ3ÇTÕãª}—„9ö П™j^ÿË_R0Ó·—:>-㦿+ ëÜ%¥KId¡†…hþ+åèñ’O]Ãá2X´P~9}&*¨~Ì|g™ÜvÓµrýÕeM»¢cb¤ïð)2¢o;óï¡ãçH·ËG«ÖÉqµæ—/]Lžn\ÏXÐQœ‚n¶~²ÖŒ ê©X®”±ÆeÍ’Ù ·Ù¥Ÿ~%'OŸ1–÷Gu*ßPÑ|K ¸ïS®ysçÔϯV Ñ&·…ÚW?ƒ9ׯï-[#D€Ñ) IDATGŽW¾y¥î½wÈ•¯—ï~þS–öµœ:%… æÕ6בb…].¹ÞÆÒntœ?-£§,¿ÿùÏŒ æQßÎÏ„XAµØoÞ¶SçÇQÉž-‹<Ñð~u?.eꌉM‘¹à­mt>T‹Ý{È_›¶™0æÕ•­ÛwË—ßý¢s:Fî¼íyìÁ{eõW?ʉS§Ýó÷Úšu?Iÿn­t^†)§fî èö¬ì;è{¾¯üPB4p÷W—”í7?þ.+V+gÏ“«Ê”fê¸ç‚·'ú³êËõfîgÒ0Ü÷¿ÿo³´oÑÈ}øÿÛbž ížyÔïÜ„…zÚü¥Ò§ÓÓæ\ÌÝù‹WÊ?[vH¬òoP»º©ßß}ìMPƒåð‰stœ[¸CQ0ŸGLœ›à3ÛàO¿øÞܳx~ÜR©¢ü­cÒ¤aM·…Úßܳ÷Óú_þ”3QgÍ3幦 %ð=;[7«Ë;KVÉI}ö]u9yPCDð¬Ãó*·z´~²Û;Ã×óÑÎÿòeŠ›y±gÿ!ɦ!8O<|¿\¯žã×äùòÈìw—Ë¿ÛvÎxîµ~ê! ×ùäí^AØ<©rÓ5Y {ÑW[<çS gžkMÕ– >’£˜çƒ»¹ŸÅ;vï—‰º±kqaýýo<ÇüÝ·¸–é3$*êœDFF¨WÕ½ÆkÇ[=þžG9ô™ÿ­Þ7`æ)¨ý“·ëßzãÕÞn3~F$@$@$ÂÒ¤ ÆB–9¸ºý¤q´ßþô»tiÝÄt"a‘Æ÷íÒˆ2,ò ê"ÂÕÛçXÈŽ˜4Wn¾¾¢Ô¾ç6Ùèˆ ó–ÛM‹Ì¼¹rº-Ô½gtQ„,*»ÕíΊ+'{ˆ¯Z„Ô}‡½)/¶}JJ+dÕÖí{ä™&õL '“KW~¥±Ïäy]tb±÷Úäy]Yª«¸ñ,½^,íZ<*¥Š6_õ1U…òfaç´PŸ>sVú {C:>û¸”Ó˜;°zwég2´Ï ²D…m0ýùÆ|©Qõ÷‚"±u÷a2{ÜËæÚÌXð¾ .æÊ{–.–Ïž;oýÞº|/þøKðm§‹J°Ü©‹Ó®Ï?aŽÅXAàA0oÖéQzí7†÷4ßÁövuû¬{oUÝ9e\sæÈjµ¿~F„‡û=ד-ìßýü‡{>A Ïx{©¹N|¹›'ÜP‡ë†b÷½%Ħ-’]^+3^ïãþ çlQÛ³}337ù}“Ì^´Âíº¹äÓµ)2¼µ Ö÷ùï*½;67"ý¾`©Ô¾û6y\ãÑs5êìY™³èyµwÓöaæÈ1Ï>ÕÀœûõ¿Éþ6¬üÍwÌIŒ}=µ,×ÒëÀ›› 3ßY./©k}ÎÙeŠÙ3QQF(ø+}ô>jõd}3ç°)¶/wmé)˜¤lÑ6ÜËþ榧¾ªLIiøÀ]«÷¼R°aè>¶®ûÎûnÌÔwŒ8¶.ý«×ý(nÜb6Þœ^6 t³ Ï*äÀqs}l6]p/š{ØœúgëyAﵬ™3á‡| Áܳµþ6zÏÆ©¨2n–ég/ÌGݬÀ³hË»¥Ës®gª¿ç£ËöþÀ\Æ3`Üà®æ\çxÙ¾£_˹\ÚÇM_¨áet¼n7›.ÞîË8@mq²ô,ÂÜÁs½…nP&xŽ8Ë¿þ-_|ûs µ¯ûöö›¯•}GËkýÚ›Y_l²".Ü[=Îëx>ð ÿì«õæyä9‡}ž§¾®ï÷fã—$@$@$@)B M j,øÛܼÑÆ ÝuÀXyµW³X(6~¶Y˜Wº¦¼Ûªàëóƒ‡É€‘Ódx?—Åå-µÊ^{U£ì)¨aý‚åîyµ“8¢û”Zƒ'¿õ¾Ô¼ëVcYÅÂ/.ržTKŽ-/ª[pÛæHáBùÌGë¾ß Vœ&Vγ+¨×­ß ?ªÅÈ–—†¾aâƒ!è‚éG0‹sX¯.ï²®b\ºö+ÓG÷1ÂÉi¡îª1ï_°J¡`ã ÝK#8E\¬-øtÄÈÔÕ,´Žž!“†vw»öúÿÓïÿ3 KýŒŒŒô{®'[ÏìÔyKÌ&,ª¶€IõÛn”ªj½ö6–Î:}‰„"ê%ðHÝ{Ì¡ðªhÕmˆLÖC²dÎ$)5¼µ ÂÍÞ\–YÇ6=GÈÄ¡/JŽlYÍgcUäT½åz¹õÆkÌøŒêßQD÷Œœn„*Du“†µÔ»Ä$G‚%×oó×üB…ü Ϲƒu¸háür÷7™Ïày0fÊ;2iX÷DóÝù§@{gÉgƺûxýûŒ•¶ë€q2²ÓÌ=_ssßCîøSx?`ƒn¢Î/Oáä¯_¾\¾±éòÑÊuFè£ û–Ô¯u§nÚUHз7æ,–2ºáfçæA‡¾£¤sëÆFPš{Ý^¯Ï‡Çåwæžíв‘ñAø¼zÛ<­Öu”ÿví•7f}`îË@ÏG0ð7—½ j aŸ¬ùÎÜëðN$¨ñÔ'ü@Ï"ÌVVã9f¶oBØ×}Û¡åãÒ}Ðx³ipß•$9L)AíoœjÞUÅçõýÞlü’H€H€H E¤9AEg÷AäÜùó’9£+»÷ug~¸ÎÝn+2²÷~øñZ9¤®­Èvm-EÞ>߸ù?õæÛnk¯¥v×í7šó<5¾‡¨þäóïÔU7«Iˆf­-Nâ¿ÿ½Y–¨û2,±péýwëNã* ×E,ü®©PÆdèFAŸZvbb¢Ã–¹§w-r%XA âÌáfé,X¼ÂrL?‚Yœ÷éôŒÀuÞ–Ö/•Ñ3 ŒÔ°|¡_ínxØÒ[­íÖâ‰Åõ·êfwq³^]ÃÇé¦Vì2O³[K)Îýé·ÿ©¥æ#¨ýõVxçz²õÔpý¼ÿž*REÅ¥-³Þ]!ùtCnÁžcéYŸ/‘àœól·¡2vpí{Ö› ÞÚq»ñßÿŒUÅÓãŸ!îµr¥«¥Z•Jk+6 zà]P[ÝûÇêg#ÔÚñŠŒ­¿ùîyM\á£ÇO¹CLcT´÷Õ¹ä´ð{òôhpÉ®–ó×u¾Áb«o'õÈ@ÁÜó57Oà ;>¡Óÿ”îó¡½Ûz^Îo¿| j ÜSãk±¡3xÌL£Þ4žbs N×Û~#¦˜{‚Úß܃ËvË®ƒÝOè@0÷,8Ã]åÝ¥«È³<ˆë7í]³1èùèmŽÙ¹ŒM ÏñÂÆÉ| …˜†‡ „ Ü´!æ êä܋ζØÁÅøzaî8yN O!èŽ~.þø AHBuýÝÒP_eTæ)%¨“¯ë'šðü€H€H€H Å ¤9AýçÆ­æ•QÝâÝ„ÑcÄ>ãu3¯õë æ.Ô“f¿/ 5î¶8?/©ÖÒWÇÎ2:oÅ› ¶ÇÁ5uÞ‡2¤gãjiË9u‹n¯–=Xä`½A1‹Aµx[Aí™å¹“.à ƒ±z+¨!¦Á«ƒZ±ü_ýÀ9£§¼mÜQ ëŽÓ廓Zѯ*[Â|áÕî¥QÆÅÂi¡n¯ŸÃ½1Ü(X„¶ï3R^éÞZ邱ŸCÔÓn߈G„0‡ >®Â .ðÖçÂ* fþú‰±öw®'OA qy­n~8­°È}ÓuŒu5PÆî@"Á^ß¹ðO©¹à­mžIë jx ¬›S°°Á=óÞp‡è 4ß½%Ê›¬¯S« s¦–ZÏ’R¼Y<‡k|2b€?þü[#ømFuÌ=_ss¿ ;+¨Ñ?¸³Oйæ,úå/)ÜÏž‹Ö°€ ÆÚéb¯1qæ{&7Aø5|+'b!¨Í=¸ñBôÙgŒ­7˜{Ö™yߟ †g¿ç£·9æOPUï!䇰›‰p½Æ=Œ Ä#P[œcëïY„0Œ@ɽ á`î[ôu¶nÈa3îä)%¨“í»çõ“rïñX   äHs‚1’ˆ;†ëœ³ôVWægÔ²wâ šü)ƒZ†QȪ˜º–Bzûü~éì¯.ßGøn°°’Aؽ·|‰Ö.Í9³g3¯îê3üM/ k²-HpÔEÝÐá‹8Û»÷·ã6êÒíKPcC±umš=l\XQ÷©3gÜn¸Î¾+¨!~û½6ÅÄùÙ×~Á]®ñÁô×Äâ/£r°‚ `X„‚n‘pDyù²uÇné®ñâ(Î…)âC÷Ú²I}ÃϺϿòbkcþT-ÔHx…aŒ6:´¬çàIÆrwãµå«8ÁÒAí¯ŸæþÎõ¼-<5Àá}Ã}4®c¹K“y 7[†h\1ÜÔ jô·õ‹ÃŒXË‘ÝåRháŸRs!%õ^TH<¥‰ÃàúM°¿þÙf<*0&æ»7APÜ[}ÔÂXe;7‘ïïØ…K½g¢›_H‚g“TáûïuŒ@‰ÕF¿ÒÉm ÆÜó57ñ§¸×^Ò\Hì‡ä(ÓQêBì}ìÙGäy€`G‡¶O?l’z¼ë÷B0pb„] Í\1Ôæ^™–4÷Ú «g0÷l°‚÷˜¿çc ¹ì9^xN7Õ÷Ò#¬Ô°Þ_S¡´y¾ºWñÔ'Ï"° $¨±Q9ñ§ ¼|Ý·x~áwˆ ©À\Åœ…k¿·zœí 6†Úß8ÁKÊßõ³dÉäõ‰&,?   H4%¨±»ÞsðD= ³Š¬L :wá­ê’ZKã9aý‚Ø[8ëˆaÛ¥nŒÞ>G= ˆéÿÆl `á‰äipÁFÖ^\§Ã³™d[ö{»xG ˆSõ6ÈÀ®®§Hdöš ÄÁbCáêò¥.úÝ_xÊ,D‘|iêÜ%‚ŬªwT¾NàªkßCí«ŸhC sý-`ñ’§Aü`#â´Z›ªË¼µ‚Ô8.®°DåÏ›[ú©…>ÐÂ?¥æBJj´VSÄÈ#ƒ4 bû‘¸‚É·PüÍw_¯rÃFÅJ½—”ì¤ÞÛ·hFw+ÄfãþB–wÏ‚lÅð)Z¨€I ˆÐl°tî?Ælšáž±ÅßÜôLè„9‰¹‰{$FÿàýöØÁÞÇÞúˆl T o›=¸çá.Í<#pm„Ø÷Pû›{'O1¬¶ë†]–L™Œ— 6²‚¹gƒÔh·¿çc ¹ì9^ð@:¼uÏg<±ù`7,Ý+I½½¹|£OþžEžÏ-oc‡Ü°¶ŸÓçB~°±äë¾EÞŒ~úfäG€k=~¿ _6a½Õãïyä/)™¯qÊ«÷’¯ë~sÉêþ¬fug!  HiJPÛEXœ ¯Öp³Å×çö{,ˆ°@ÂâÖljó pŒÏ±‚Àq°,ù*¨É~¬e"˜öC`ãçx¾Š+˜ó}ƒÅ7’„ÙWTá¸`ûca=‡Å"ØY Z^îÒÒdÀ†Å › ¸À¢‚¦gÁu°ètŽ›óXC³iŒµ/6ÞúiÏt®¿vƒ²^'e,õÁB„ aÏ ×L­¹h|’û}ræ;ÜþëFæ‚[pëFfzg\¾³MÖzl-Û¸?‘ÀžΉäÌMo㔜~¡½›TlrkÇ6”œ¹œý 4÷p>Úh= ì¹¾îÙ‹_oÏÇ@õyŽúsJcØséFŠóµ€¶ž@÷J ÚãüÞß³(˜zàé“5^(Ûã½Ý·˜çè3Š·qöVO0×÷vŒ·ßc¾® LüÃ3—…H€H€H u\‘‚:uP°Vo¹F’ $…,Ä+@Öî` ^†ÌåÖ[Ážw9ç&b÷‡ëëøF¾ÜÁçæP°ýãq$@$@$@$@W. ê+wì.IË/§h¹$äEÒ,Ä¡.ÕLúðvÀ»Ìá%á,—cn8tT´ÖáMt‡¤Yˆl @ª  NU¼W~å'Õ1›&Ýñ÷ª£+¿—ìAZ%Wi„"x+—knÂ- ½¹3§UŽl @ê  N®¬•H€H€H€H€H€H   NçÌî‘ ¤ êÔáÊZI€H€H€H€H€H€Ò9 êt>Àì @ê  N®¬•H€H€H€H€H€H   NçÌî‘ ¤ êÔáÊZI€H€H€H€H€H€Ò9ÔÑ+e‹HçÈØ=      Ù²ó€D„…KDd¤„‡ëß2aæß®ŸEòçÊvðXT\\œHll¬~'ø9&&Æü;&š‚šŠH€H€H€H€H€H t¤ˆ ŽQAKA:³†=%     p[¨ÃÕBœ…:N-ÒjšvX¨)¨9“H€H€H€H€H€H€B€µP{Ôaêòíòùv¸|'Ôp÷Ž†Û·þ‰‰a u¨Í ö—H€H€H€H€H€B”€Ôá®±Ó‘úçBÜ4b¨ýj †‘ÚÄO;u‰Âù$Cdxˆâd·I€H€H€H€H€H€BÀùèXÙ±÷P"A­ùÈôIÔÆåêèÉ“;»äΑ9ø±$@$@$@$@$@$@!JàÈñÓrôØiÍðí²P#†ê 5ŠƒÖxêX}]ÄtlŒþ#%‹ÀJ¢XÙm      ôLàÜù—u: b¯ÍÒ¿õõYÆÍ¢Z½¹}º|ãuY.—oW<5^+uþ†¸Ö¿ åÏ#Ù³fÔJã_¼•ži²o$@$@$@$@$@$@éž@¬jà'ÏÊþÃGÍ;§ñê°xë4þ —O›ÿÌ^’’AÿSQm,Ôx5µqýÖ˜êØhó7¾Ãqp—àñçÙóÁ;ÌýYº§Ï’ ¤I.ñ {Aã3—$Ö˜hÌø7D3¬Ò‘á‘æoX¤­»·ÔFLûԨʈdˆeGb2¸}o¥†ÈŽ3îàñÇê¿aÑ6çhå.AÎB$@$@$@$@$@$@i‹€תYœ†86ñÑ*¤ÍÏÕ*¢áâ «´Šj—»·÷øiÔ•/g¦°°ƒÇ¢Œ ¶‚ÚHu‡Û7Ä3¬Ô.!­âZÿ˜ŸñY¼%ç†9Ä´KXÓ-˜˜89-§£ÎšÎ@D;­Ò¾Ü¾½ jO1m“åΞI2gмòi±$@$@$@$@$@$@$àAàÌÙh9vò¬ycD´§¨˜”Ì龟!¦ñª¬ìY3IެœH€H€H€H€H€H€Ò-§ÏÉIýƒWhyŠjÏXêD| ¾]ñÑøS0ov‰¤›wº4ì €HtL¬ì?|Êíöm­Õ`㙬, ¶Öiûº,X¦Í*¨‹Èÿjk"&      ôI)Êö8nÞI­6j·¥Úº{;­ÔnAí|ç´uóvýc,ÔÅ åNŸ´Ø+      pعïh¼…:"ÞíÛåþâ´R'Ô.—ï0—«wœþÑ×dÁR]¢05g @ú'°c/µZ§õ5Záa6ãwœ[L[qmõÁcQæÅ[ÖJ M Wo+¦£££¥TѼéŸ{H$@$@$@$@$@$òþÛ}X"##Ý¢®ß6–Úi©N ¨]1ÔañÂ:Vb4;F-Ô1êö]ºh¾‡J$@$@$@$@$@$@éŸÀ¶Ý‡$"%      +’Àæû%2\_™¥ï¡¾H 5õ9Øh      d  N6žD$@$@$@$@$@$ê(¨C}°ÿ$@$@$@$@$@$@É"@A,l<‰H€H€H€H€H€H Ô PP‡ú `ÿI€H€H€H€H€H€’E€‚:YØx @¨  õÀþ“ $‹u²°ñ$      P'fõ¹sçåȱ Æ#R_Ž/O.ùã[ä¬~_ù†Š²÷Àa™ûÞÇÒø¡šRªXáP?öŸH€H€H€®(Xï:}FŠ* ááaSð¦À IDAT>Û~þ|´Œ¾PjÝUEn¾¾B¢ã}ÊÜ÷?‘\9²ÉCµïJtè+¾Ôµç9yòáûUÃïI€Bœ@šÔ¿þùŒ™úN‚á(˜/ŒìßAÞ˜ýœÔošÊÖí»å•Ñ3¤g»fr]Å2!>|ì> \Yú {Sví= ]ŸBnºî*Ÿ:{NÚô!Í= µî®’è¸@ߢ2`ÔtÉŸ7—tlõx¢C_Ÿò¶œ>sVúui¨~O$âÒœ îÖæI)V8¿–ˆˆÉ“+‡ù9..NÂÂÂü j=Dñ=¢¶oGøûÎy}ç¹þ®¨-!>ïØ}   $` #åK—<¹sH‡–%¢`×PðN|¾ÇðD‚:Ð÷®u›ï5¡ý.  ŽRAݹ…ߺBpÙe iNPêù\"Wî)s?”3úPëò\“D‚Û‹WÊ¿þ%11±r£ît>óx=É–5³lßµOF½¹@Z4®'pë9rô„”,VHÚ4XE{ƒç~ÿóŸrìÄIï9åñú÷Iµ*•Ìw¸nNuÚwð°üúÇ?’5K&y¼AM‰‰Ž‘÷–¯‘èèh¹¹REiûô#nÎYùå÷òñšïäØñ“R¦dQiÙäA)^¤ ' „<¹ï}"7ÿ'õï¯.Óæ-‘ñCº™5JLl¬¼½x•¬[¿A2fˆ”º÷U•…KWK³Gë u ï­ÃÖ~ÿ«¼¿lÀ²}ÛM×Êî}%w®ì>-Ôg¢ÎÑ5f®Ùå1]#¢ÞoÄ©}ÏmR¯f5ÓîÃGŽ«÷äty´^ ©Qí–c P#æõÀî­¥„Š^”p57Ã*ít»ñtùƃù§ßþ'Z=¦ß 2aæ"©X®¤´~ê!·ø.˜?<£¢u½1ë)Wº˜q3B˜Î‘=›È—[>üd­|ûÓïòÆðž’)csÝ ý«È{ä†kÊËÛ~f~ \sU)#¬ÿÚ´UÞÓ3®]åÆkä‡ ˤ·ÞSý¨”+ULæð©ìчõÐ>/¸w¨M0ö—H€H€H€@ :&F:÷#µkÜ.Ü{‡tè3Jž|¤¶Üwgeèó¯’Ùï®& kÉÕº–[öÙ׺ÆÛè¶PúÞß:lïþCW󪕯7×ÿM×w‹?þRn½ñjŸ‚Úµ¬¡kÀrj0Y¯kÄ?äµ~íu=¸Êˆñ}Û™v¯øü[YôÑj;°‹äÊ™ƒM$bÒœ vò¯~Û ò\Ó†~5\îЇcÍ»n5§®ùúg#°'莧ß-ÔJ|oüŽá´ùKdÓ–2òåîK!FæðÑc&ù’ÃT-”ß\Vº˜c±c:mþRãþsUÙÆ"ÞªÛ«Ò¸Á}ò`­;ÍñG”g›60ÇoÛ¾Gf¾³L†k}E´>   U?nÐõ™> J È+Õënÿn­ ’×&Í“§NËàžÏ›:rLº½2Þ-¨}ïo±ýþò/dò°n‹ø‹'Hé…} ê»÷˘M[Nž:#íu é£µ¥pÁü2Z= ¼ø¬”UoÄþ#§™Ek¬ Õñe¿I T ¤9AÝê‰úR0^3¹sf3BÔ—…º”>Û¿4Ê<3gÊäCÄQÐÉ-¨»·}J*éî"ʬ…Ëå·¿7Ëë¯t’£ê–=Y-Ö[·ïRWðÂr^]¸ÿÛ¹W^íÝÆ¸iãºx€ÚýwjÍF‚´Qý;‹6J‹.C䑺÷HÃ:wIßáSd¿º‡Ãâí,í[62k   UH>»á¯Lvoˆçã'NÉpµô)˜ÏX Ì+žml¾G~¬³lR²@ßû[‡}ýÃoòí¨b7þ¡ãçèš-‹OA}B×€âÅ>NjÝ}˜ºzß®á÷J÷A4óxE¹_]Ñ{™dꀵ›…H ô¤9Aí-†Ú— ¾¶BiyN-ÔÔ¸CãZîM4zÞ2‚;5ÜÅauÆN)\t¾ÿå/Øï'ÔÎ VPÐQ³B&Ô#'Ï—(}ÅÂË]Z†ÞLbI€H€H€HÀçÎýÇHåJW«U¸ˆû¨?ùRÝ¿«šuܰ sŒ[¸]GY«°Ô¾÷·CXþLÙÛÄg£ôþ¦ò¾²|#9 4(§NGI»—Fš×hÁ]ý£•댋:ÜÕ?ÑÜ9ã†t•HM¦ËB$z®hA×fÁû×?7™,‘¥ŠQ«ó uß>.×W,ë5#¸SPÏxû#M4ñ·qñÆ.è›skŒôöd jës$»ˆ:{VþݶKn¿ùÚЛYì1 Ä€èDX\¨óæÎéæ·ïͺV‚çàòϾ‘EË>—v-ÉUeŠËòÕßÈgkp[¨—­úÚï÷þÖaÿnÛ)ƒÇ¼%õ4Ñb¨‘gª&E Cýôcu5ámyÓ6è¡/µ5‰m‘|¶ë€qÆ Sù†«¥Y£:k %pÅ êÓg¢d΢e}|–odÛnP»ºqÃd¡¶YÀñPÌœ)£<¥‰1f/Z!ƒz<çvùNŠ…¯aøhÕWæ¡‹ ’(×U(#=Û7 ÑéÅn“ €hfì©æm)}:=“Ç/¿o’±ÓJÏvͤ¼Šh7~þ}£I$[W-Áˆ}†'"²|ãÍ.þ¾´ƒ…zéʯLe²eÍ"±šYÜ—…:K–ÌF8ÿýÏ6Éa²|Ã:m âÁŽuc©â…9Ì$@!J Íê‹åW)œÕ5ǃH€H€H€H€H€H€Ò# êô8ªì @ª  Nuļ @z$@AG•}"     HuÔ©Ž˜      H(¨Ó㨲O$@$@$@$@$@$@©N€‚:Õó$@$@$@$@$@$@é‘uzUö‰H€H€H€H€H€H Õ PP§:b^€H€H€H€H€H€H =  N£Ê>‘ ¤: êTGÌ \.Û”ó¿ýO$6îr5×M«""$ËÝU$÷°.Én!u²ÑñD     ´LàH‘õÕOi¹‰lÛe&'q’­î=’k@»dµ„‚:YØx @Z'°çΦ"11i½™lßå&!RŠ|5/Y­  N6žD$@$@$@$@$Ö ì¹ã‰´ÞD¶/(òÝ;Éj u²°ñ$     ´N€‚:­PÚiuÚ ¶„H€H€H€HÀ CGŽ þÈ—GòäÊ‘àˆ8͵}÷^9{öœ”)YL2DF¸¿Çw»öî—(ý®h¡’5K¦ç9vB>*ùrç’¼yrúe1×ñ¬ø|tŒlÛ¾[2fÌ %‹–°° Gœ?-{ö’ØØX)V¸€dP—RÇŸ:%¹sfOp®±c×^‰ÐäIÅŠHýÛ[›cÇO&ø*_Þ\æømÃÁCG|—#{¶Dí§µh{æL oÛ/0މ‰Mty{çËV}-¥Š–J×”»¬÷õeÅE]üŠÔ'¢.’3³ãI”F‡àÌù89q&Nre —LþŸi´l–%¥cyê\œäË.ç¢EŽGÅJþìáW4 ”žŸú{\ŸŠ•l™ÂÌŸ´RŒ•ì™/î<®÷q¸wöTî®s.&Nòè3#âÊž^—eø±øÅ»ÜÊðJ()1?ýLn}{ŽÆHá\ ùÎzãw¯Ε%õ¸büvk;Prè=›3ËÅ=G®¤õBZ˜«—bŒƒé癨³2yÖûrèèqhùeË»¥Ú­•ä±ú÷šÓ!_Ÿ²@Nœ<#¹rf“ýŽH¯Í$Þܲc÷>ycöbÉ–5‹äÌ‘UþÙ²Cžiü T¾¡¢Öú&i½Ñz~Áüyd«ŠÛë*–•–MôÚ¬ä^Ç[eû‘çJ¡üyΖ%³tkû¤°+¿\/Ÿ¬ùNŠ«Æ=¶c÷~éò\)]¢ˆ×vA¼N˜±HòäÎ!=^Ð˜ßøòÝÏÊ¢VKe†zö8l¾/T o¢zV¯ûQ>Xñ…äÊqAw|öq)R0ŸìÞwPúœ&u#Öúµî”jU*%¨ט6©ü»m§”(ZP9n¾ïÙ¾™dÉœI†ŽŸ#'ONpή½dDßvRX¯ã,oŒÅ=UofФÚ1Ô©†6ÝU|Å ê)_œ0ùÚÕL¸[‰‘‚~åŸgdï±¹ºH©VÞµ+ ´â·„7õíe3I‘Üvîpοû£¥úU w2¿Û|VþÚ}^ æŒÚ×e‘ŒñÂØ×µìŒýÉqùüï3R"o¤ùÓ·A.÷dòv­Oÿ8#gT°9Ë•²Hæ a¦_«´_ûOÄH•2™äÚ¢—Ô¶Û“!¨Vêuï×~eÍèZ¸à-ø ‹\«Rq×µ~ÚvNvVŽr«~_<÷ÝOÏ;ç‹ÿEÉõÅ3z¡;ÄÈ_»ÎKíë3»O[¿åœü½çœË)wWÌìæ~^Y€+ú|]ÑŒrk™Œ—ä&]÷ÏYyûÛ“2éé|òóçdªãòVëü‰® në·œMðùƒ7f• Š)Ð8â$o,6ë¼Ä<Ì®›HÕ+d6¢Þ–oÿ=«œÎK:Ž™Í| ¦ø›Ÿ¾Î÷w­•DÉÈI¹‚‘rVçÕôVùý B_ó›Ù˜ë΂…uÍk]sbi•ÎϨè8Á=ŒûšŸ¸÷>ÖçèC7»Ú…âï¹àï™hÌ|}¶¯û'J2G†I-eä¼¶Œ–f’by"(ì|N}æd´Ï´}ÇcäªBÌ3ÞZPüÕçëô¹Žïñ3¬.å fp?'“Ó/_ó39uáœäÖwÛÀ=òUß">7§êï^ÔðoÑKn»qï‘­¢åN§®u.îZþÖ ÓNçúú}ìëœ@k _çz.$§—bŒƒi6Vþþg›\[¡´9üäé3òâ+ãeèKm%_ž\1ø“¾Öb1Loæ•_~/›6ï­3Ç:|ÌX;QþܸUf¾³LFè¨Öß8µ\0âåì¹óòâÀñÒK`‰¢…5-¹×ñÖGùâE JÃ:w™¯ÇÏxWûWFjÝUEö¨€Åf€µJ¯øü[ùwëéôlãDUË[ —Ë*n7éfSPCˆçÊ‘M7²™óÞ[¾FŽª5¾õS%ªçÃOÖª;\Ü_=Ñw7ÿ'K?]'=Ú]ë¾Æí?µ†—Rk»-¯Oy[®¿ºœÔ¾ç6/mÿO,þT÷|>ÑwÔÁÜ<&-HW‚‹o§§ÝœCFXÜ^.“¼»þ”.Z3I›9Œ8l>õ€ùÙ–;¯Ê¬ ¯#Ì~ØzNf­;¡Vïð"é­¯NÊ‚Oܞ͈¤‡cT(¸vÕ|] ßEk»n´[Öõ)’`ñëïZý AíZ¤Va=síIù´G!³pl3ë”Ì)7–È(Ó¾-Ø ±å‘ÊÙ$R5°¯q´ÇycëNü츴»/‡ÀòôÆšòn»Æú6yõ ùE…Ý#•³ÊŸCÑ2óÙ„"ß _óÓ¶ÃÛ9®ÕRÇ¥õ=9Ì"سx«Ï×|Ï.l~mÚ-;´_SZä3ýêÍòäÙŒè‹WåI0§¼]Ë ÌclYÙ9pRµ¯kyöÁ)¨—ê/úá´4Ñç—ú|ÈŸ#\zÕsm˜áþQ³Ü¡Ï ß”Öwç0›°ü4ž´_ß–ÍXŸÑö¡¹î-<ƒÆ®<&óÛHpé-*,Úës lÿ§'ÓZ&ÜÙ÷¼ý]Ë×|O_óõý¦›ï|Êô÷÷!EÝmô÷\ð5ÆY›>¾îUo÷161z½{DÚêæ6 æë×û º-¦˜£°îw©P„an^U8ƒyVá÷~?¼ uø«Ïß3¨Î¨}òTÕlbûQ!¾nçÀùë—çÜr ज—èF‹ÿ ¹õÔ¾®è󤌱­kÖº“rD7ϼ ê@ŒœÏr§ t^ ~úÞßïwçú[Kø:/˜çB ö&õ{ðs>?“zþÅßMu'µ Âj;vÚB¹ý–ë¤jåëMµp_îØw´Ly­·z%ü%·ã¾Ã§èw½¼þnê6pœtVáêÍœR×Á…ÛözM†õyÁíºþëŸÿ˜€žíš%j×—ßþ"?ÿ¾Qº>Ÿ8AÖ±§Ô¨#Ûwí“Uk×'Ôž}õýùqÃߦX…üÕÞm›øÜ÷>Q·óB^-Â?è9?ÿ¶QÚ48QÛ<ëñ<`ÊÜM½u﫚èÜÑo.Ð1«”ÈÒ!¨ ª%#póϯîçO?VWr««ÿð‰s¤oçÆ¥›&°ö;?»Øùeϧ…:¥H¦ÿzÒ… Æîõa]\<#ר•òÕF¹ÍÎö]öT± Q‡êxµ"Î}>¿Yˆ¾úÑ1ó³g™ýõI)¯–5h§Õq¦þ»¶ZA`…Å1•_Ù-?(j¬¸¾®õþXlŸ2×´–ä{¯É"ÏÝ“]ü]ËÙ®áËKJ#µP«‹.¨?îæ›_«ÀÂb‹êä´Ý†ÁÊë,_Ó[æ7‚.XT/íâÚ½Ýw<Ölxº×¯þ+J>S+â°Ç]î@°ªüð¨À-úôÙXi©"¢þYd—~>cí ©wCVyS„7+ák+ŽIÉ|‘ÖVP7žt@^n˜ÛX}0®·Ü-¿ ,jê¿gØùñׂ,°Á0K-Å´áßýçÍXÝ\*£[äÒ…Yw›sŽéB¼•¶ïAmÊ/jm¡m€‹òIƒu¡Ba—åðÝÀ¢½Lþ ÆõpŸn¾X õëê÷È=Ç\› ›ç3Biî7§t!#Ô:æ,þÆÑçÅÈK>u-ou—Ë2Útʵ¼å’ÛÊf”éºéòð-YÕÿ¬6›8?)l2ÍÄÕÇÕE9\ÙÄÉ Gò˜1ö7?ÁwÈÒc²õày‰ÒMjºéÔM-D¸Ÿ|]kãÞózÎQùG…oɼ’IŸ¼þd^åtŽ ,ƒ°¼ÂÂZx}ÍwççÇg¦”ä6–o\ëoõAŸQÆ®<®Ö´0yA7üµ£¼ZƒÿÙwÞl®µT+3„­¿ká»AKŽÊ¦½Ñ¦?Ød‚ Cû·_Æ5Í«ó"ÒxÔzm¯|ÔµiÓoì—U=\±qêFЫ•m ˆµ 7U!†ñ‡:1O+óïÕáÈé)¥÷Jûš9ÍæÄ€ÅGu¬3¹ç+¼hð¬²ÅÛ}ìïZ¾æSÁœá>Ÿ3k7•·žU‹y6i0vŸl|AP;z>üñ·Ú_<Ÿ#TŒÎÏ—Êmž—þîcô5£Ž·õ"‚P~^…qUÈØhÁØb>à~±÷$þgKÇZ®ÍTÜx&aãÉ_}¾žA¸·ªVK®n–:B6Ý]Å3y¹nŽ"l1¯>†=ž7 Û2æ'<-ð;c—z6`Ža³¥Æ°½òÅK.ëžU¨ìS.÷Íþ:76ê9…t~–+i<ZêsÂ_}ËéúÌ„'Ëy½'û)wÜ[(VPCtYpXÑû¬–z½€kw}~bêÞ«3m5ö5Ƹ6d×èï´#o¶}îæN°ùìMP£ío|~\²e ×g|œ x8·”Ö{¥ùÔƒÒY7R†éý/l@á^ƒ Ææ,>ƒàÅÃF úøÌÙÍï OÖïéïñyºa‹{›cðJ¯÷¼¿ìïwgþÖ-þ®è¹àí\lNÖgš-{ŽF«·A.©{C¿cÜiþa3¿à‘ƒgy³ªÙͦæ¥,ÿíÜ+c¦¾£VæNƪÚoÄi¡nÜåËw7£CŸÑ2°GkcÁv¸Rÿ»u§±^Û²sÏ~uE>#k¾ùÙ¸F·{æQ¯Ý¹ØëØJOŽ’ŽýFËÌ×ûº¯KùëSÐ§Žæ³ã*”Û ‹ïŠÕßJë'$èŸg!È jl\uYc?¡®×K?ýJ?TËÄ›Ožýn.‡Áš]Ýão½ñj·ö𝒝ø]*”+i\Ò¯¹ª”þ)mšàY>; ñÖÇOž’?þ·E~øõoã~Ÿ#ÛO&ë9µCÏþ@PoV×þ—:>mÄ4<,^)¯õë`¬ù·Tªèÿðøsã¯ü‹—Ô‚™2HÆë+ȹŸþ¼X¬éòüt!¨W©{é;/¨%Gñ7š°_ú«øªoé±£C]d±`…ëÄʵÅ2+~A>¥¿œÖmn¼¨.­°ˆzåÎk™£ ³z£÷ɺ¾Ü`œ³Éßµ°`î³èˆ,T+$娉î4ï°,ëê¹X¤c!°J­×¶$µí°÷}ÛçÔX¢–dÄ|oVññÂ}9ÝîÖ¸Ü'¿OElõp-\Ûª›%¬¤utó ,¾Þ|&‚ó¼¹!~£Ög¸Ç>{wvyZ…“Ô°þ}µ)Ji°€A,¾üë—äKÊî÷X-RkS]u§†x‡ø‡—,…X”Ê„Es y¨c›rÄ"nr­Ô"þYOרÀêV¶@#P!6!°Q/Ú#÷©P*h„2DÏ^]€XAÝzæAY¡›Xp½¢¢Ãr6I…Ò&~ˆ·Ei¨î±5th}±@¸,-«ç0BV¿Ú/“‡…8>‡;™²Ý/óÔÚYH…\o?þíŒ{!îk~ÎP¾Gý4< a l¥csM¼[µ·kÙ9Øhâ~®âá*‡3ê; a ½Ìe¼6šLÞo³Õ¢g‹ç|wÞ'°&‚ÎwxŠ€Ë”5ÇeD“¼F`úk;æ^]"ì›Fhëê^…ÝFÔíy-<3°i‹±æg1mo cÙ¼Zv³qñ}ÿ¢ò¡z" ¡Ë‚CÒ§~n3g\^¹pkpSV¹kè=6aÑð˜%Þ(¸÷–©e~ŒnD8 žoÍôš?„ <§^6Ö+Ä×}ì<ßóZ¾æ„\ ç ê½ñåÝ^µ·ç‚¯1Fh „9æ'îx¡¬Öçæ¿ûØÙ/lÖáj¡¶±ÒØˆÄæžížÏ%Ü—pi…ÇHƒ›.xàXÏú|=ƒp_TÑ >̈ÙòºñÙZ7Kaáÿm‡ë^Å Æ ›PpIdzÑ_±¸{ÝœVãê×½]Ï''olOÑIxëàçQbñ¶þĦÎãêýPOŸƒðÖñW6pBˆâÞ;¤›Ò@ ^Û§°ê•Ô¨yµ„q`ã̾ä«_þÆØ„[Ø® Ù”ÀóaQø=`‹§ Æ½…ß©ïèïEÌ™åÎè3ï´þžqyktÐß‘õY3ꉼ  ¨×nŒ’Ùúû ¿Oë¾¾Ïx»`3Ìów’e¶×¹W7– ™ñƒ× 6¬—”ù(Œ IDATßÁÔ/ý>öw¾çZÂ×±þž Ú‡ï¿ÒÍ Ü Íqæwñ6ÆÎççA]?=¤÷îjýýézÌu“s ¬Ï¯Ž›%Ö­!7Wª`ªè5d’´kÑÈíÖϺ§ÖÞ¦&~ØgˆÖ>ž1" z” »3gtm¡.ß÷ß]EjT»ÅkÓ.æ:Î jò/Ôå´’#¦zð˜·dÂnæÐuëS»ÁÄ!—)YT=x¯&cËíY Aú <û©e›žnÝû4ö<«ÆrïÞw@V~±^Ú>óˆ\¯q̰êoøó_ÛÙm_¥1Þ·Ý|V%›Ù°øˆÆWgNð Üß/AX2` ›¬Ô3ìíZhÃÅjˆ0ì£`‡E6°È»)\¿Œ·Zà˜¤´m† G,*°À°jìÊýx†þ’Å´ŸÆ²Aˆ@(Ú‚cþÚuÎlbØREbhÃöv/y.^Ž` ‘ Íò÷M§ ï…*t¯Ó ­j]€õƺuÃU Ù²Ú.l.`§¼P¬%í„Æ7B Lhž×¸Éc±¿L_°pžV+ „ËÏjñFÁB.Ú»tÁ´ScÄÁ .Ä%¿üþQy¯ƒËwÕŸQòáO§Ü‚zزc²¨½ë»eº¸ƒµe´.æ°!k4íûu7¿ÛÛ‡kü Ú_ãèúkA„Ó~ÁªÔ_E³u7…¸¶ì¨q™‡u ó\ÚÏ=$u¾£@`¤óÆZº|ÍOÄŸâ>º'ž§ç8z»–=Æ› n?÷°™˜g®ñ‰’GU\8­ÃžóÝÖŽ…6E°€vkÝÂ"¼—Ší¼ºèõ×vÌ=X«l¬s=ó4üÀZæ¼]kŽZ±¹`]¹‡êÕœ hû}#öÊÚ—ŠÈƒwËTõî€Õ¬í½9wî:ðÇ»¹¿z èßÖ qÛ˜˜íy | j\ °¾AvÐza Ϧ\}ÝÇ–•·kùšO°äzΠ^_‚ÚÛsÁ¶ÃsŒ×g³KP%to·Çûºí÷èCµš½¨ÞHwkè‹-þ5r8ÌSïX6{èïg oõùzaS ÷#î3xÁ« ^1v°„Ãs ÂØ[ÁÆÍCüx¶¢`~BLÚÍ•›tÎü:Èõ|ò%¨ßþî”lÖÍ&lB Àó^4VPûªž«ÿ:#u4$¥†zN9ãø!¨ÜœÅlñÔžõá™ù ÞÇßôsmVõÖV¸Þ[O[5Úf=†àÕ€Ÿ±!ëKPc£áE½G—tvmbÛ¹c5¾ÿy›+OF)}¶Ù {Íäj_k ¯I?ôõ\&i"6Ez.?§™¬G½±À$«Sãvw•FM—¦Ö– eKº?k÷ÒHÒ³;k7rA8?ß´¡\U¶„׿œVQ=tül# i’²Ñÿ‹‚$^¢É½Îgk%+¿2uU¿íF7Ý®÷H™¡j‹í¸é eT—…ÚYÖ­ß Ë>ûF†«‹¸¯âOPÃ]ü½ek¤w‡æîxê@ãk=¬ÂÝÛ&O{5 ÚËš¤lªºÍ#fÝWÁ¦þ¬w—«±*\ÁÕsî~#¦ë´gÆu{uÅò¥¤fõ[ÝçÁ¢ ~·Ý|­tÓX÷ž3©3eÌ .æ:)]Ò“ Î Þ ùg •MtÓFÇ-B7,2Ýy³dmp¯0AÎ~õ“_|IÔæ”C‡H¬æ/•’.5D‘­‚›oE\Øé£"¿T`1òæ’‡Æâu¡î¶OUád‹¯_‚ó¿=e,|paufuöw­ä j¸Ô= –Õ5j€…Ã,Œ±hÁb¢– çb4)mÇÎ>,í6a„ëƒjIƒÛ,,°ÒNR!Š‚…í;Š[°èÆf†µ’a!—lÏxqÏÊsñ«"ĬµpÓÃB°ú«{Íb¿ìQ?к`=§ãÛ]:Ö%Vƒ¦Úð2Z7`})¤l$x‚µ‚ mx&t¬år_n¨Vy+¨±Ó__û2¬>ÞÔØµÉrÆ¢oÉÏ5Æ–(”j=Ä"‚Ú³àÚù²k{ÔŠåký±€hB<¶Éî§"qãk|9,÷Öº€ºÏüæ:=tÁ4PݼmA¿m¼®¯ù w,œ`uõ,¾®e].½ j U@[oÖä\¶`CÊ&ô5ßq,b}!j÷(æį½7°0‡È®þÚŽ¹×B7¬«pý1.—m+¨½] B sÆŠ#XŽà2wÜÛí1óÝ&(|d¼ZAÔ Ï%ˆ­ym\l=¬ßYk#6=:Î;¤c—Í„s8‹/AýØÄÆå›K(O¼q@ETn/ïë>†G…¯kù›Ož3¸¾/Aíù\°}ó6Æp'ãeäQüÝÇ8Ï:<óšqC‰„_‚Ú9w!̺þ¿½ûŒ¢ÜÚ8~’PBoJGÅÞÁ‚{CQQ? *ŠŠ]QÁÞ{ïD;(ŠŠ±×kÇNï½§|çy76ecÜdÿãå’²3;ó›Í’gÎyßñŽ‚¨C%Ñö½E¯÷h·õ³¤a(’¢Š´.ö&š´KǦ𮥛O. -Iì {«…úqo•V°æZî.90¨¯ó×§:†¢@?¼¦ð9S üÈ'vé﹇z4 AZcÃ5ßÅ}cÝ-ñK¢@­#uÑN‹.8iˆPI縰“.VªÚßR]8PÞž~¶ö÷ŠõÇ—¯ Ô»úûVt!:Úï“’)PŸåz›BZï+¯Œu_Dó¥³cZÿþ?÷Ù’ü µº<ÔQ¤¥“wǾYÖ@]Úß[âÏE¢÷MÂWÒ¢÷4uW©³'þbJ´N¢@]øýó.ÿŒzRâ“–ó› Ów>2Ü6÷ ®ºªj>ôäK¡9šzá¢%ar1U€øfÌšë•éáavoµ+—´¨ºZ«fÍP}UÅZ‹Z¡uk«ò>nµ2+ö³®ÛW©ÅúÜ+î •rÍ.®E­Ñ|òµõ/&Àª*êE7‡ñâªËZ?Q Ö×GøLßš¬¬ð­ÆJrÐzj5¿äìÞE&—~>ü‘Û&¼ W´Ò×ßÿj¯.HDËÐÇø‡iá"H¢EZìƒö‰MÚ¦E4Ž:x¯p®_óŸŸ,G]ÓÔBßë½ËùÊ*yµê¨ SÝC÷¶§a3:Ãrýµ–áó4<Ãgœ÷‰ü²ýggácÏÛÊï'Xá@èq/;ÍêøÏÏÊŸ~·>c+¾øÞjwÝÊêÛÃÒ½ËaåomÁ}C-Ç·]–j¨5Þ0š|I¿`^àã€4Û³ÚUÍR5'~Ñ/2ªêF¿èÜá3ö*ÄEW÷õØâþÔZ¬½}¹ðm¯=—¶UÞ@ý¶ª œïóªm´(DhûzØTâ6¯D4öŠÜÉqíqeÙw#S[a´ á·UðJÏn>S±®äùÀL{îÌæá—"Uƒ5›mT¥Ó/1 ·ã|&Øøy?4Îõ$ß?U‹ô ´ª… ¥ñë¿4ê—mOËoe¾Ñ!Ô?Ò ê»Ý4Í{eRµ¬îãíwjmÔìȧ Ž'×ÅUœÏñJÕý[„Vë…~++µ+lêAùQ¯ê'Md¦MM¥Öè+^œµªÿ[û/Q G²U«ão%Õ/¬Q$jmÓ/ëš+jùÖD2úžÆ^ëœèo]”иPU®ô‹]4ø¸ê…âÇ’,.ô*¾f§Žªº:^‡fZV•B]ª¶)4ìû´Puhå?:Oj©ÔØXµÑ~?iEþLÙ‰^Ÿ÷ùdpëýœè5®çU5'Ñs©S@KqZ-…êˆZ¶õ‹©f†ff.îõ½.¸Z{€ÕÇhQîûÞ¾ý|èy]ºí¨&ÞY‘xßõÚë솚ˆJqÔ:úv\ËwqÏ¥÷ Ÿ×ûŒ^'ú¹Ð¸{j…RÍ­ÐÝϱZÒ’5±ŸZÃÕññd_?^Íá¿„ÿÏÝ5>TaõXÿ~¾¿WiBÁÂK¢@­×Œf9×·ƒü¢É‹þ‹þ ?O‰~޵‰ž+ÑëIíéÿö>£}..P'z_Ðã‹;Çz8Ð^ò‹f ÿšËá…/‡‹a%ýk<û9†é5w‡†È²¸@­{Òã³|ÈL‹ð~¤mèb›:jJÚ^¢÷ ÅFC½û˜X÷“ZguqM5´$6oÇÚáýQ„2üƒ(°&úE¢¤@½ÓõS}Û-ÂÅ\ÍCñ…_ô‹Z¾Õ™òÜÍÃ…Vµ|ìcžK Ôz«²­‹ÏêUŽæ‚ˆ&¹‹ÆPëâ †Â赯É£¥,-ß%c9©ãk˜·üëB_üEÇè¹4y–_€ŠÞ‹4»øþÚ9ï5Sø¢¸*Ôe Ôú9Ö0%½Wë5x’MuD-ß/ø¿ƒú}B?3š mu[¾œ/ó{Nñ¶üè}SÇ[Òïú¾º T}×xöhIô¾ n-zmúpa¸Ø-ú7BaZÃFâ;;â_—‰µ~¿R§šnë§gtAª2[¾¦ïzôÛr“N*ÓѾ~ý¿_íeXš”ª–ß¯yÄȱ¡ÝX“h©•úNoó>ñ¨ T°µ®Ú©33k…6g-G}·÷ê¹?×úE~DËû<Åý¬k<°î­Ê­·&Ûy»-ÂEU«£™ÇµîØ¿°½J}Uÿ“½m¨Š_ð™½UenTèþÔ éyøî;u·i ·îw­EÞ÷~Þ:uhgî½S¸§tK¯Òëâ„~õÖ‡öËïÿ„¿+W†Ûbi†r-:®G†¾bM}\ö‘„ƒ±O vÁÕ÷ÚuNÉ\q¥@=eºW½­[íâx ¹.Ѝ‚¯ÉÈt~o¾ÿéèOë}H˜1½2–TÔúªÕ{OÚìþ7Û Ÿ|®ÅówÛü{ž¶eo·Z>–¾émÚŒ#γ´zuVµ|û…ŒDÓ/I-ƫ樌s·¦·Yåõƒï, üh"µ×i†]Uõˆ~©W{pörôK~™íïãzõË´¾§öFµÒ?>úE\ãu°5á†*¦jÇSej§ë¦†ñÄú…6Zn>¢Iy‰žK+)°$z.­§Š›Ú„ Õýç}¢3Ulµ/g©Ìʳï…+,ñ-ßÚµ/k¨lÔúw·‡û(©ºs•·ËkfÝøEöWø×U‰ÕÄ]jÃVNá]P‹ª)š]çD†ñû¡ÇnùVë¹Æ6«Er/£ ZôË…ZðÔ’®É³ÎÝ'Pôˡƀª[¡±gÔó(̨%WMD¤ Ÿvð1«¯z¨VuK!]êß?g®ôÇûí#|\¥Âñy~[MÈ£[Ké|i_5¼Æ«R®_j5¾V-ÇzÝhüé#'ÄÚçTýÑh²Ðãõ‹È¥^QÔë.ÑyŒ·,l!7ý²¨ª».h¼èÍG4 UZuÜä¿T·ö[‹MöñÝúå0šáXUµ"ËI†øEMÞTÒëS¯Û^ÙVµFÁZí»~ù.é¹´ÍâuT)×…ýÒ¯±ï7zhŠ:G½Þµ=]”ÑèvYúš~î>7'ÜRG0éb~ŽUi.ißûx êà¯%=^mUˆZÜs鵯_°å\+#Í2ýçMÝ!j)Uëû¦ zEs8¨Ú­ùô3#G½7领¿BDü/ÒÑûŒŽ-Q Ö/°š“@˜éýI O¨¦õãŽKz®’^O‰^ŸjÖØ]-ªÎéNôÞª¯%z_(é=McÂ5¡ nq¥÷ŒK}Žu“”ôs¬!(ßøMÂ-Çû]ØÐ’¨B­!/úp Ý‚O?«çíÛ(Ü¢O*Ñöt̉ރô¾ Ùεz¿Ž¯–ëýã oÖ…šÂmµÞ<ý“’õÝþšÑk££¿Æõ~óƒ·Ñ¿iú%÷z¿èô­_(nëóDèýZï k%mOû§÷EE½/œé];Q·Rü,ß—û¸aí·ÞO4i£žG¯uMV¨ ]ºí[4Ñ[áã‰>OtŽõ}mS]@ê&Ó{ã­þsV‰5T@U÷hROuW)$ÊA? ×y—BÔeRž@­GÔy¢îýÛð¼_ÔùÈ/kÑÅô§<Ôë=Ku—³ë>î’þ=Öch»øÄ¦:Ñ-üJú½%Ú¾º?vö "ãoúoï zÒûZüŒüz_P—.øF‹Þ_u‘°¤s¬×“†»¨ A ¸É%½Êóuÿ}Ôƒ™ÂSü²“Ð>G¾ôÌ+oû˜ãïBتï`é¾ÍºeÔcÃFúxäoýß­‚ëöéu 5¨_Ï euëú­8½*­‰À4†º¸©£ç-Ïóì´íE[AT“kiÜòJ¢›o¼¾ßΪG­ Žšµ»iã†aÒ/M¬¦{cwÿèhÃÅU¨û{p3o¾ÿLì.¹ù²3üXÙÕwx«ôUç„¶ø›ïʦy[|C7Ñ­µÔV}”W}ÕFýâè÷í½ñ_ú}®}¼ú¯°‚ï*Þü=9;iþëٶ³ý*®&!SèÕìãþoÿp¡C‹*Ë“¦Î´3|Ì{I‹u{;þ‰W7µ]MrÖçÈÝi½üÕî~lD¸—øÕœ\ž—U©ÖI‰@íÍ=D/|øÙP¡npê‘6ó˜ ó}šÞ1À–}ð…-÷s¡ÎìÖ%áã–ŒþÀZ}ð”Mï~jhùnx®w9øÏî‚ÛǶé¯ñ–cÛÌã/.ÕØíR¨$xP•Ôñ†ú…]¿Ø•vQøÑ/÷š¬¬ª- $ ª¤Vö¢J†¬J3+~_TEÐ:% ±)Ó®«ò¥ …çÓÐ9ŒÂeüUÐ~GFòŠÚr£{|ëB„ŽOU¼(Øé—ý2§Ïµ =_t jºS¸C!z^UbŠsÒÅ­Wø¾Ðå=²Õ ÇÅ퇜ê»S|5Iû§_ºµ Ÿ‰nVÜ Ñ/N5|…øaÑã=WI'6Þ¾L/€Ö/¦ ü…gŸ×ÃKÚw[ÝÒ*ѹ,îé4C¼ÂQq~‰Î½¶SÖ÷¦sÑöôZ*fn™[µØï'z=•÷õY®ð•ôóUø<–ôs\Þç‰Þ34yXYÞŸ½é‚Ë"3¯×FáEAZ%Í)Q–ãй×ìùñ?ßúÙÖ… µøêµ©6øÍü‚Oá¡Å=~~–ø-uá±,eÙçøÇwŽõ}fùdY*zï–GEý®#tq>þVnÚ7]´kêôzÑMl_í-¯EE¯WÑï ÅퟵªÓêÓ…½Âÿ¦Uô1•e{ ©+¼9ºïriÖÕÏ΢%K¼9̓xÁÉ ­_žçI´-UÅÕÆ]'³`§’ªÄ‹½šÛ ~Ým«.Íq÷ÝrK¢Evšð«aƒúEÞ TmVõ¹ž¾°Qx;ÚFtJc]Úý–QqçF¡[3kÖòÊZR"P+à¾õ¸Íx‡ÕhßÚžu¬åø…žøeñsoز¾ÊÔuß'áã¿ôv@Ýä†s­v—Í,gÁâÛœ{Õ}¡•¼º,Õ*PW—“Âq € P&^™VuUÃ)tŠhÆó²l‡ÇÐØku!¨:®a_êBQ·Q*.ÅÝ¡#8æÿV`²W¹oòªú­—Ÿ™?êÊØ£TÔ™»nk¯8ݦu?Å2wébõéa³N¼¤güê̽º&|œ·#Ô.:Ér—.³÷­ŒS”4Û$P'Í©`G@(Ÿ€ªµjÅV¥W•ꊪˆ—ooªßZ¾£–~_ŽŸ´úiÉG¤ñåšÏ$ŸjÇÏñþ·ºÇµZåÕz®¶ó­6íT©;T­µ·afú‡ÆO¶ ·%~û¬t¿\óçï²Y§^eYþ­›¼e{ÑÐWC»fÔò]Òã²g̱V µ=ϲl¿ðQ{—Îa3{ô[u-µtßàäÃmþ-yKPlî¤ê°¨«ÃYä@@¨æj'¯écÞ×Ä•ê¨sýöpaÉͱ,Ÿq{ÑÓ#ÃéhÉÜ}{kxö±–í“Âe4ojË?ùÖæß:È2򿀦q÷¡Nô8/löà•V£]K—=–Œz×ô;Êêì»s˜¤,ÃÇà/ü¢-yåjõJ%PW«ÓÉÁ € € €Àê T§@]V‹tŸ¬.ÇÇñû-J\5ÑãÒêÖ±\­->ÁGºOl—ã“ÕU§Êttxê²¾Âx< € €TkTÔÕúÄVÂÁ¨+•M"€ € €@Õ PWÝs·¦÷œ@½¦Åy>@@HjuRŸž¤Ú9uRv@@þkõ}ªÎó¨«Î¹bO@@@` ¨×r5y u59‘ € ’ºûb¿P㺥;ü>ûpZf53J÷ø²>jþ³z™~ßôUkjÿôœ>ÛqX²|Ÿ—¬0kX§¬[OüøÙ‹ÌšÕ/ßö´/~Ë#«W»|ëWõÔÕð¤VÒ!¨+ –Í"€ €¬_§š½ü¹ÙE•îÉîmÖ}k³ Z•îñe}ÔM¯˜õÚÉlݵ̦Í3{ñ3³¦tg.0Ûu³-Ö1ûmšÙ+~ÿßþ–uë‰ßïq³O*ßöFc¶ÔCõaÛ•oýj¸ÖÔŽ±´ìlóË ,$¨YÃZ}øt¹„~Ÿ8ï»Õ°Œ–žžny÷X÷ÛŒùçÑýÖ×j”™–6kþ²\]ôÊÉÉñ'Ë À²ýªÏ³ý ]VN–­×®y¹v„•@@RD@¿DF¿e–敆òŠÂùÔÅ}_ü·ç)éûQ nÛÔì¾1f}÷0«ë•ßlÿ=øÁ·Ìúìn6eΪ@hJs|ñ‰uEm/Úvy¶WÒ: Í‹9We5¨ÀÇÏí‹-ûèËäÚ© <>6µziþ:ÖK¹Îþ;[ã«Î,ׯÔåbc%@@2 ¼àÞvN?ûÝìŸYf¼²¬ªEU-Öµj˜]ØcÕfÕ>lœWˆç›­È2Û¤Ù¡ÛǵuÛff“f›ÍZh¶÷^9Þ8¶îû?™ûÙ,³¦Y†·lŸ¶wìc-·Œ4ëéÜgÆ›-ô¶ñý¶2ÛcÓX»ù£ccA¿I½Ø>×-V‘^¾2öXµ™«ì•(Û¨µY#oOÉ«êjÑžáS@?{ÿXËõÛÿóðíŸïëû¥å7Íö÷çêàŧ?g˜=íÇUß[Ê·\×ì­ïÌnì{Ü™ƒýq[šý<Ålž›œ¸›YǼ‚Õë^þaR¬ ]­ç'ìjVÛKmâÚ÷e¾Ÿ[zå\Ç-Ú=—Ìôýƒ»˜mìŽ%-%¹ë8Ÿþ0Öö.‹¶‰Uë?tï ^±—Û6Ìþš3»¤gÑ‹!ezá¬þƒç¸Ã–óN] aA ^À«ÉuvÛÎ_N¹]Ôå¦cE@@R (`¾éÁQ!p“¶EWS[õÃo›]yøªïùÖlއEµ^+¬>þ®T›í¢k¬‚ÜaíØØêhÜ÷(¬*âÝ=Ì—¨û?ev•‡ð^¡VÕø¼'W›VË÷}'ƶ£qåÚιÝcû¤çV ßGígá ÜŠ ÔçVŸæUïÚ^¥Öß>îÒŽô*µÆo' Ôïþà“–ùcuAc/}Æìô}Vµ|ïæ3…«mZ!x¸çŽBtü,ßñZ ¦£ oû~éøT –ⵜTåߺ½Ï,ì‰Xmä;{Ëv4É›*ù¿’Üoó*¹:t‘CN×z[÷õGƶK .ëO¯êjp9@@ éj!~ÏÛuOçé>ùXŸ¸L“u¹oìÏ¿›LJÁºu“Ø8fMˆ•(P/÷y³‡hÝâJ]…gMú¥1Óí½E;Q V¨¼ûõØÄiªÄjœõQ>Y•sm³¾‡mM‚¦qÃ}Lp+o™.|Û¬ø µ& S‹vßçLâj‡VXïäjMØ5ì£X•Xy¸µ«' Ô Ïš]\’ù-~ÂþilµÆ'ð LU»õ8M’¦ Üò1ÛS}ÿUUoêÓ>©ý=¾Ýý%oYÿÃÛâãoýU’û$;>äýØøs]×1)¬ë:é ÙÁŠ PW¼)[D@¨H…D¿Ÿk“¥Yˆ{¸¬çÁ\•X…õh qIë+€+ª•;Zæò ÛÍ«¼ ü£Ï²­@¯öð[´=…ñÂíÕ Ãº@ Pªoô‰Î¢ÉÆþm›s­[Øê^eY’Õ¦­±Ù¥]Jr×ö4®¼ðíÌJ»m‡@5 PW“Éa € €T’€Â¯*éjqV»¹n™µ:‹ÚÒ5³·&AÓäf›zûtáIÒVgû¬‹kL€@½Æ¨y"@@ò4&Z·¸R{vI3n¶Ú yÉþúgJÀ‚@¼@ºw›l¶aG;îÿÜ} \êR ñ@@¨zƒŸm?þâ·k£5½ê¼5´ÇºÐÒeË í¨Cö*×3¨ËÅÆJ € € ì®{Їœ«4My:ÙÏÕ·i>qšÝti¿ríº\l¬„ € €É.pá5÷'û.²I"pëg”kOÔåbc%@@Hvu²Ÿ¡äÙ?uòœ ö@@’@€@'¡ŠìºŠœ(v@@ &0qâDûí·ßlúôéáó-ZX§N¬mÛ¶!P!ê aL‰¨Sâ4s € €@Õ˜0a‚3ÆfΜ¦~ýú–™™isæÌñ ¤rB°Þÿý­C‡Uÿ`9‚ÿT€@ýŸòW©''PW©ÓÅÎ"€ €©)0vìXûàƒò¾iÓ¦Ö·o_›?¾Õ­[×f̘ao¼ñ†Íš5ËöÙgÛi§RŠ£®u…0¦ÄFÔ)qš9H@@ ê |üñÇ!,kIOO·fÍšY5l‡v°7ß|3ë&MšXVV–½øâ‹öÃ?Ø¡‡j[n¹eÕ=höü?¨ºF kß¶¥ýö×ä µlß®¥MŸ9×–.[^¡Û-ÍÆÖ¯ë)umÊ´Y¥yøy z0ó$ € €åPåùÁuOàœ°z=B«÷Ë/¿l]ºt±Î;‡0-¹¹¹6dÈ›Ü~þùçümiâ± X×®]mÇw,ðÏ?ÿ¼-Z´(Œ£~ä‘Glë­·¶<°Böƒ¤–Àêêë/>Å^|í}ûgrlÒ¼ÚµjÚv[oboÐÞn½˜­X¹r`®é@]·N¦qâ¡vëÃÊ||2[±2Ëîyô¹ºvÙÌöÛ}{ûþç?ì¹Qï–y»kbõšPæ9@@Ê,°ÒƒÇM7ÝZ¹ã—u×]×úôé“ÿ%›þúë¯-;;;T¯5~zèС6mÚ4»à‚ Êü¼¬€ÀêêAÃ^+RÁ½þâSí±¡#íϦ†Ý}Ϯ־]«°?ûú'ûü›ŸŠ…ßl£Žáq¯¾õQø~Ãõì˜C÷±‡¼*¶ë´iaYþÚßr“õmÑâ¥öú;ŸØß“¦Yá@½Å&ëÙNÛnauêÔ¶™³æÙè±ãmöÜa›ë·oc{îÒÅ7ªoó,¶1ï~bMœ¾·‰_Ð÷ÔBþÉ—?Øn;nc#^)X¡®åù3újk7kl“½B=høk–––fìÕÕÚ¶nn+=,óÃ÷éwÅ£õ÷?ýöçÍ÷?ËÌÙ'ÿŸÍñ¯-_±"ê;µ·NÚÚÈ7ÇÙ^ݶµ ÙÖ›oh¿þþ·¡Ï  ö ¼-|Ȉ׭U‹fÈw°¦MúŶ%öÎG_Ù„?&†íÄþöÍ÷lÏn]ì•×?,¶â^šŸui”x  € €ÀPP¾÷Þ{‹<ï:ë¬õ‡~&#Óã¦NªÖ›o¾¹µiÓÆÞ{ï={÷Ýwí / !›²Tt V¸àí×7ß7ÔCãüææ{pU0lP¯Žvü!6rÌ8ûiÂßEvs§m7ÕíÇ†Ž ß‹ʪ|¼ßÎö«ïÙ·?üfÛm³‰í½ë¶výC<7ÈoùÖ¸ç: „ðé3æØNÛoa;vÙ~ÇCÃmμ…vñÙÇÙlo3í fΚ롽|ê²üdñX@@5&0eÊ{øá‡Ëô|c­±Õ_}õ•½òÊ+vÚi§Y«V­Ê´ ŒÀêêßþœìÕÓŲ–W£7\oÿùÿBõUクÐ×®¼õq[²tYxÌ®;nmmZ®mÃ^|³Ìz÷¶ñ þtX/Íÿ\;ð”8/Y–¨Ù¿›eø„~/¼ö^ìq^=Ö><øÄK6yṴ́žB¬‚ofíÚ6ð¬cíº;Ÿ°Žë¶¶=vîb·{Õ¢I~{zT‘@­cØ.^ÖÕ~ªÒ>ø™×Š£¶y‹_l8ÁCÿëc?¶_½Š|à^;ÚŸøLó"¨ÿ¯Çî~1 Þyùí°½m6ßÀö÷ªÿõw Ÿ+àï¿Ça¢åä£{„mðÉ7áÇ~øeÂ΀";™à êÒJñ8@@5* ûKß}÷ÝEž³Aƒ¶çž{†1ÒZ¾üòK5jTøÅûøã·Ž;ÚG}f?ï¼ó¬qãÆkt¿y²ª/°ºúƒ¿ 3akéîmÏšì#ÔZZ¬ÝÔ.è×+Tªã—‰“gØPÔqŒß_½VøÖÃO¾Ú±KªPwÞbÃPyŽ–gª¶zþhR2UÄ'ûÌØoðyþãT¡}Ù'÷úù·ì°vµõ;´óŠòÜ0ÄBaT•âÍ6îh›x›õÃУåòóO´a/¼Yb V«õy§iÅMˆ¦ýÜy»-ìîÇž+ò‰õ&Þ¾޺m‚Ã¥ço÷øcµ^¢@µ¸kƒ Ô;z5ÿ¾A/„íïìUøM7ìè†/ç?ŸªÚKüBëoZcݰWg!P¯Žë"€ €Tš€ÆDßpà ÆPëþÓýúõóª_,pDËO?ýdŸ~ú©Íœ93„mÝ:kâĉ6pàÀPUcA ,«¨ãÇPoÜi];òོ½zh¨×÷oµX_zã#ÅNPV¿nPAÖ¢¶å|b®M7è`úøk-­[¬eçœrD˜½[-Ð;n»™ÝõȈüûò‚>6äÙ×m¡Žµª¹jÃõfl¶–k.:9´‘7jXßÚg§Ðþ­‰ÁêdÖßS îÔ±­íÐy³lµh¯®¿äT{|è«%êF>Îû²óN°Ënz$¿•Zaz£õ× íã…—(P«íZmÚjaï²ÕFöø°WCe;Q Ö1¾ñî§as ÔšÄìþÁ/æÞÍ[ÔãmtaA“Žëc©¨õ<þœT–—F‘ǨW‹•@@*SàÙgŸµü1ÿ)¶Új+ëÙ³g§ԘjÝ‹ZcªujÝš² Td Ös÷þ¿ý,Ûoý6Ô+»ZÎõ@ü¹ ŽªÖ ~Ûü‹O®UxQµøoVë²n§6æÝàÌcCµ]§¨ËúSÁã@@ªœ@áûPoºé¦vÄG$<µ}<Ø&MšîCê*wðìð&PѺ¡Ï:}¡·rk¼¯BeËæMCpÌÎαš5jØ"¯DõI¹ôwáEß× Úš|Ùò•öÕÿ~±ûìl]s¨Poë•Ü%K—‡±ÙšÈë Ÿ¡ûÓ¯~,2Ëw¯B«Z3cׯW×^ý¾ýæÕYw>³Ïa¦ûeçøÏÚÕ·Ýzã0›¸ÆXï±sçðs}"¯)Óg[;Ÿµ[-å…+»ª¬_púѶÔÇ=?é3l§yïuÈÞ¾ÏËC¸WeøyŸXL3’—¨×ów]€¸æŽÁÁ§¼j=‡.hµy~ÿoMÒ6î³oíÃO¾ OO þÏ~¼xb@@5)0~üx3fLxÊ.¨7ÜpÃ"» ê݈#LíßÑädkr?y®ê#°:º, ÁZÔŽýo‹*¬j‰ÎÊZHìכ®oø¬ÛúþÒe+B;Ñ¢Jq¦·tG“¡Å?N­èK=˜«’®E³+ÐÆ~î2BðWX.iÑð M~¶2îVwj!WX¶ûoÇYß—Úí+c¡å»2TÙ& € €@… ¼ýöÛá6YÑÒºuk™vÐA…ñÔ?ÿüs˜Ù[•i¡îÖ­[…>?K-5¨WW5þ6R«»-Ö/Ÿº|n¬… € °†šU©ÖìßÑÒ¨Q#›??6[r‹-l¿ýö ³|³ °:U%P«=Zãuû'–ÿF€@ý߸ó¬ € €åÐé¿ÿþÛ~ÿýwÓøjµ˜6oÞÜ:uêdmÛ¶-ÇY¢U%Psîþ{õØ@@H"uŒ$ßu’Ÿ v@@Ö¬zÍzWåg#PWå³Ç¾#€ € €@… ¨+œ´Ún@]mO-† € €åp݃%Þ~ª<Ûdê' [‹Ýti¿rØïgXô–á·$ ·ËÈ0¿«™/iáóØÇfk5ÊLK›5™æÈ{Qæš>ÎözëiÙ~µ¬œ,[¯]órí+!€ € €)0høköã„?=Ú䥚ŠÜ8۪ʴ·ØÀzõÜ»\ÇC .+!€ € €@U2b´ýøë_^ôäÄ‚@œ@zzšm¾ñzvìaû–Û…@]n:VD@@Heu*Ÿ}Ž@@@ ÜêrÓ±" € € €@* ¨Sùìsì € € €å P—›Ž@@@RY€@ÊgŸcG@@(·ºÜt¬ˆ € € ÊêT>û; € € €@¹Ôå¦cE@@@T P§òÙçØ@@@Ê-@ .7+"€ € €¤²:•Ï>ÇŽ € € Pnu¹éX@@@ •Ô©|ö9v@@@r ¨ËMÇŠ € € €©,@ Nå³Ï±#€ € €”[€@]n:VD@@Heu*Ÿ}Ž@@@ ÜêrÓ±" € € €@* ¨Sùìsì € € €å P—›Ž@@@RY€@ÊgŸcG@@(·ºÜt¬ˆ € € ÊêT>û; € € €@¹Ôå¦cE@@@T P§òÙçØ@@@Ê-@ .7+"€ € €¤²:•Ï>ÇŽ € € Pnu¹éX@@@ •*0PgYvnŽul»v*{rì € € €)"ðǤ™–‘–n5jXzºÿ‘aii:ø´ðyìc³µe¦¥Íš¿,77×,''Ç¿”kú8;;;|žE N‘× ‡‰ € € ਲ਼=Pç¨yA!€ € €¤@¨Ó½BQº u®W¤½4W¡&P§Ð+†CE@@%ê4oùŽõ|ǵ| Ôj÷ÎRÛ·ÿÉÎÉf 5/,@@@”:=ÃÒ}ìt ÿ³jÜ´ÆP—¨}µŠÔaüt| nײ™Õ¬‘žx$ € € €@j ¬Ìʱ‰Óf Ô>™ÿ¯ :´|«B•mM×·Æ 2SS”£F@@H ¹ –ؼùK|†ïX…Zc¨U¡.U Öƒr¤}+´y+T{7w–oÝ.+ÖòO­[g©J«¿®ýïk5±úukùFón¼•Z¾- € € €@5Èñ ¼pÑr›1g^¸ç´îA–WÖç Ѻùtø/|X̤d!PëÿÏ»¯tl<õª0­oE­Þ!Lç‡oÍðííßyK~ ž½`¹çáØØç’ó&'‹füÖ3ÅÚ»cã¦Ã˜é¼ÇEƒ»ó5C¨yõ"€ € €$£@Þ¨äU-àq“ŒåML¦p αyÂb3{œŒ,Z?Œ¡ŽÔQ¨Á9± ´s‡VïX—w^¨^5V:ðâz½“ñ¥Ä>!€ € €ÕW Aïw4¿·²n4–:¤Ú¼Êt¶ð¼yÁâï=-°": Òñ‡P7AYüéU㦣Ì]0HGÍädÕ÷UÊ‘!€ € €É(P0§ÆOLö6/pGwÕŠÂu´£VïØ-³ Þr+:Úü u|^õq¬Õ;|æóÖëU­áùˤ]8H£“ñEÅ>!€ € €©#P¸:ÿ¾Ôy“’Ňe…çXv,ÍF­ßña<þ㺸P½j¬tØj˜”,ö¸hfïUG§„{Q§Î‹“#E@@ª‚@á uôy4>:kÎKàaÞ²¼ÛkEÇWxEu|¨Ž…é(ϯªVG·ÉŠ*ÐÜsº*¼|ØG@@@˜Àª{SÇÆOÇn£¥%B²¼¯ÇïÂzÅj=(jõŽöªYÀ U¦óRuôýUmÞ4|óRE@@H¼¡Ëy»’_i.ÔöÍcV¸2»¬ø£)±B]8TkÅìììüõssc¡9þqÉ@Å> € € € P’@’ÓÒV²ÎÈȫķv¯z\Ñ¢qÂ1Ôñã ˜£Ð\7³¶ÕˬaT yy"€ € €T}ìì\[¼,Ë–,[F!:¾*¨í»Ø@]8LG“5®_Û2kרúZ € € €…–.ϲù‹–‡;f)DÕÿ:)Y|û¶>V˜Ö­²ê×­m êÖ@@@j+°pÉ [ät ­Â¡ºðXê"÷¡^5ù˜føŽÖŸæMë[ Ú¼«í‹†C@@0ËÊαsç·}GÕjÙž¬¬@ ŽªÓÑí²T™ÿy nµvü[[CŒ € € €@õÐeSg.÷¤öu~¥:j÷ޝRçêø{NGmÞ±¿³C…ºm‹ÆÕS‹£B@@@ N`Òôyyꌼ¶ïXû·–ø*u‘@kùN‹µzçú¿M–*ÕíZ¨y…!€ € €T‰Ó¨½:í·ÑJO‹füÎÍÓQ¸zÖüeáÆ[Q•Z™Z­ÞQ˜ÎÊʲu[7­þj! € € €@Ê ü=eŽÕ¨Q#?T«õ;K_©.¨cc¨Óò‚uŽeû`ìl¯Pg{ÛwûÖÍR@@@ê/ð×”Ù–‘ža^¡ÎÈð±Ô^¥ŽéÜü[iéó":¯ã;LD¦@­êt¶Ü¡ ºú¿l8B@@@?'+P§‡*uÔþ±géP¥Ž&%K¨c3{çæW§sYv–ß6Ë[¾;´£B¯@@Hy?'Ίµ|×ð µON¦@£W¡VˆÖúŸfüÖddÙ^&P§üë @@@ eVêX•:Œ›V‘Úÿ+u ÖøiÝ‹Z•êŽmiùN™WŠ € €¤°À“fÆ*Óyã¨K¨UžÖ¼dªP¨SøÄ¡#€ € €)*P\ ŽuyÿK…: Ôšá[-ßš,;7‡ uо8l@@@ ÕB NóV)É4Ó÷¿êXU:™õ¬ÙsmÅʕִq#Ë̬]ì9]°p‘-Z¼ÄêÕ­c6HµóÎñ"€ € €¬¦@É:=/\¸mVÑ@&&«„ µBï£Cž ‡¸^‡uìö.Õáwêöó¯¿ÛË>ì IDAT­×´ÝvÞ¾Øuîzð :â;ê°­ÿ™'•j»<¨|êbÐX‚h†»òm…µ@@@äˆu†W¨•y V¨ÿã@ýÂÈ7ì¦;bª$yñ «]»Ö¿ ¨ÿ•h=à'¿°ÑÛ/pÜqÃ¥¶K×.kìyy"@@@ ²*0Pg[VN–­×®y…íóIg´ï~øÅ¶Ýf ûü«ïìú+úÛ>»ï\dûK–.³ ¿ÿi5²uÛµ¶âµÆyÿ2á°îF:Ú=?Yª u–WÞÿž8%´‡¯Ó¶•5ñ6òDËÊ•Y6gÞ|«U³FxÜœ¹ómâä© ×›>s¶M™:Ý÷»¡uX·mجZÕçÎ[`™~á jE_ºl™-X¸ØjøýÍš5m=WíZ5ÃúÅ-¥Ù÷™³æØ”i3Bk|ûuÚXíZ/Xh&NšêÇ¿ØZ6_ÛZ4/zŸqÙüþç?VÓ{ƒõÚ[ ¿:£EÇÿègìùWÞ°+žm]·ÛÚš5i\a¯6„ € €ü—¿Oœa5Òý–Y~êÕ¬PWl ž4ešõ<¦Ÿ‡ÑÖ6ð¼SíôþWÚN;t¶»n¼¬€—‚öEWÞb‹-_ßk·LëÆ·|Ϙ5;¬ÿ÷?“Ãc^7Ù¨“½6æÝ[¾?ùü»ê¦{löœ¹a=õ:¼‡Ûï„bÏÙ?O°ú]dëw\׎<ô€P]W»³æeœnì»{XO®¸á.{ܧùÛÙdÃõíöë/1…çC=Ý:y0öØáû·Þó¨xi´ÕòðüΨ§Cè}ýí÷íŠëï²#zv· Ïî[dþmßõ<—\s»ûø‹üu6¨oÎ=ÅöÙc—ðµw>ø8Ã\¿H- Å7\qÕ¯W7|é¡AÃlÈð—Lá]‹ÿW^`[o±‰}òy~¡ã¯üuu±ãù'ï/ÖŽ/"€ € €UM iõ#O<ÆOŸtÜÿYß޲ýëãUÚ…6ú¹AÖ´I¬J¬±Û‡÷>3TÜoc½—|ý{õwÂ÷¢1Ô×Ü|¯ò¯m¼áz†O´Ÿ½R}ÏCCBØM4†Zà}=ÁzP¿Ê««ùºç]|½Mõjîý·_mÛyÕ¼ð¢ð¨YÏÃfûvmì°ƒ÷³÷?ú,çõëÙ[/ =õw>0؆=7ÒöÝs;ë”ÞöþøÏìÖ»µn;nBõGö5UŽß{m¨ÕÉÌ´Ã?ÓæÍ_`óç/Ìî›ïz8T~o»îbÛu§í ìJiö}ÐÓÏÙƒ³ÃÚÏ/ªÉ—\s[¨Ä¿þü P!ßíÀcBx—£‚ò‹Þ‚?ìùQÖÇÏI¿>GÛã?·þ—ÞÆ·ßtÕ…~áažÉõ¡Ú=rø#ö¿±ïxÈþ™4Åú{¸uÞzóbݪÚ û‹ € €H iõ!^žì•ægßmëµ_Ç¢y¾O ÖË'Óòã/¿Ùñ§]Zß~å)«['Ó–¯Xaûö<Á/Yš¨wÙÿ([¶l¹ÝÛU¶]ç-úg^xµ}úÅ7%ê¿'NÛÙ|“ Bu: ægžrœßëÐ"¯ µ=Õçœðõh¿È÷èqløÚ+öÖ-›[·î½l©W©Ÿr¯·YÇZ½9ú´Ðzýö+OÚ]¸uàá»®³6­[ÚGœl'}˜W‚_ Ï{Fßc혾çÛoümcG>•_-ŽvHúßöýÆ;ò€<Æöß{×pÑb]¿ c­ãaXÇ:{î<ÛïÐÃØõk.=ϺxV{¹ZÀòµœ{ñuöÑ'_†s-×ÞrŸ_Ô›ôO8ý"ûá§ Œ¡æý@@ª@Rêo¿ÿÉN>ë’PÕ½ ¯YãŸUÕÝhƒõ쩇o 'B-ɼݻM«öò°‡òOα§ôã¥UYUëñ^÷ß{õÙGóÇGUâ’fùVuY­Ö“}œó2o‘V8^±b¥õ=þH;ū慗(P«ª;nLlvr- ÔZ÷™Aw‡qÑûÞ'|]¢E!XËànöŠîT»Ò[ÂÏ:µw­úؽ7zû‘Ð>þàרnãmëëÛàûo.öEùoû®‹§ž{Y¸Ð Ec¾·ï²eîª8k¹ôÚÛíÍwÆ…²5ö\Uu™éóÃŽ;#TŸUuOOw/÷ª¿ß“Ü»¢‹êbO_D@@j ”ú†Û´—^}3!ïˆ'î ã ßýð»èŠ›­•W}Gzõ7ZT%V¸U ÞfËMmÏƒŽ ßùÌ#ÖªÅÚáãh\r¢@ýÍÿ~²¾g_*´çÑ'„ö§ž}ÙÆúÕ¿jµ|¿÷êÐüýQ Ÿ¿`aÔÍš6¶½9>|O•]]4ˆ_ºl½E¤ Ý{tëÆMè­ÕªDß÷èS~QaTh¯Öq«ºßIÇq*í¾kR´1c?°¯¿ûѾûþgo©_dõ}ž~äöp¼¹¹¹ö¡±Ö1«}û×ßþ Ï¥ñáœu²yâÙöÇ_Ã8î-7Û¨À~¬×aÝÌ ÔÕà]‚C@@@b’.P«¥x_o5Ö$c œÍ×^5«ô˲5nWUTµ=« ­jt†Ï~­[j©ú«àª0ªŠo4†z·ŽíÌ·ûxãnyã{ŸvýôËï [¾5~[ã¸wõûXßæÁ\áRmÖ'}rï#ìÔ{*Ô%j…L|…Wµt+ðkùÕ·ÛÐÃlóµ›…ê¯ÆM«-¼¦W¤;¶oZ¦Çö•3àÚ°ÎWßþ`Ýy­uÞj³"ûQš}×Dk“§Î°-6Ý0¬¯IÅ^}[ï­1ã{ï±³Mš<Íêx}t"štMmëj_W¨×E UëUµ×¢ñìºß´Ž£VÍšù:ÞžŸE@@@ :$] ûþxxÕ­aìðËC,`¬1Ïû¬[7òj³–ÿ;á¬0{÷ö]¶òY´w³‘£ÇÚ÷?ýZ™o¹f€í¾ËU¼µÍ¾†ÿ÷ã¯öÚ›ï†ÇDÕÖÂ'3º¶Z¡Ï9íxûìËoí‹o¾·^ÕÕxâ‹Ï?-Ì@¿”6PGW³+œ«ýúIŸ)[3{«:¬@}‹·w?÷òëaóý}ܸ*éš™{Ç…ð«¶òw^}:„Öòìûm÷>föµõîÕ3L¦ nþ=Ɇ>zG‹~âél]¿•Öi~ñ@•ô¯¼’=è©çl]»ÚÍW]B½ÚÆUÕ–‘*ëj¥ÏÍɵžº?œ§h¬º.dt÷ñÚ{îºcuø¹á@@@ä›”L³F« ]\;³î%­Ù¾u§ï¸&[·Ö8jÍ0­ñŽ:$fÝNëºËÎc~5ÁÖy>–*ÒZtû­NÛÛÃ^°žîc—ôïW䥰|ù ;gàµö¥‡h-;n·]tNß Õ*ÝÃg¿bÀYå ÔªÂß~ßã!üG·›Ò½¶u¯æ^ÙÕµ³ëãgߪÔZÔ†®–n]@¸ïÖ+‹} —fßO?ùX»æ–{M·×Rõ]‹°¾®à«åÅQc¼JÿlÛ†iFqyE÷¾ñòh{xÐðPq×¢[c]|~¿üʹƹ_vÝ¡c@Uë×F<Æ € € P-’®B]UÝþjÚŒYab-Íôh™î©í·ƒŠÂ`ižK·“Ò«.M0¦{6«µyußY³ç„ýQ›xE/¥ÙwUéçø ¹碰=wÞ‚P±^Ëo?‘Z´¿ºÐ!ÛÌÚµóâE•õ ÙZÍš„ ÌX@@@ê P-uu8 € € €@Õ PW­óÅÞ"€ € €$‰:IN» € € PµÔUë|±· € € €I"@ N’Án € € €T-uÕ:_ì- € € €@’¨“äD° € € €UK€@]µÎ{‹ € € $ê$9ì € € €@Õ PW­óÅÞ"€ € €$‰:IN» € € PµÔUë|±· € € €I"@ N’Án € € €T-uÕ:_ì- € € €@’¨“äD° € € €UK€@]µÎ{‹ € € $ê$9ì € € €@ÕHÊ@=qâT7îs[¾|…Í›· j‰®æÞ6nÜÈ23kÚÎ;ogmÛ¶\Í­±: € € €@e $] V˜þì³o¬sç-¬nÝ:•uÜI½Ý¥K—ÚçŸg;ì°5¡:©Ï;‡ € €©,tzøð‘Öµkç” ÓÑ‹qÉ’¥öñÇ_Z¯^¥òë“cG@@’V éõOÿŸyÿSë×»§µlÞ,ÿ{ßü0Á~øåO;æÐ}Êä1eú,»âÖGm³ ;ÚíiiWžßǦϜc®Àž¸ë²2m¯<®ˆçš3weÔȰF ê•yοêë{ÌÁ¶q§u˼.+ € € €@ê T™@½ÝvÛÛÅ_ÎÔ¢E mûí»Ø%—\nÇsl³·xñ"›>}ºuì¸^øznn®Mž<ÙÖZk-Ÿì«üÈ… ÚÌ™3¬qã&Ö´iÓÏ©çøçŸ¿­I“¦Ö°aà {5¨G¿ó±½úÖ8kÛª…]|VoKK‹=݇Ÿ~kŸó£j¯2=ÿxècÇ}a—Ÿwbõ*"ä–vG*⹞zþ ëÔ±í°Í¦¥}ÚüǨËLÆ  € € àU2P+Àn·]g;묳­wïlÆŒé¶ù曄Àýàƒ÷{ÈL³Ÿ~š`¯¿>Ú.¸à<¯b׳Y³f‡ð}õÕ×ZFF†Ï¢½CäÝ»`sæÌ±M6ÙÀÎ8ã,»üò+à £k×íìÚk¯·½öÚÛ·{‘½ðÂó¡Õ\¡º{÷íÖ[o{ÿý÷¼Š|VØæâÅ‹}"±®vß}THå7_«Iý/¿ÿceó,²v­[XŸ^ÚÏŽ¶KÎî]¤BýÝ¿ÙèwÆÛ¢ÅK=Ì7·#ÞËš4jPä‡Hûöü«ïÚ”³¬®_Àè¶ÃVá–DÛ(¨Kz®ISg؈‘cÃþµkÓÂŽî¹}ñíÏöÒëï[C¯NË£û]>—öãëﵑoŽón‡,Û}ÇmÂq|4jÞ@@@ lU&Pë°ºuÛÕ²²²|ìÏ,''Ç *ÂQ >äžvï½ø8ëš6eÊÛqÇí<`?B³>?à€}­ÿ íØc{{˜ª×7Þx‹ùŠÝpÃua[o¾96T´·ß¾³ýòËo6qâ$ëÑcûâ‹o¬Q£FAWßoÓ¦M×­`~ë­wØö°+VØÉ'÷±N:å󲎂.)PïæAðö‡†Ù÷ A2¾B­1Áw=ú¬ ô vÛVkÛ[|fc?üÂn¼ätKOÏ+iûSegçØ{㿲¿üÞ+ÛGÙ‚…Klà ±6ïøû÷äivËýO‡Šxküú»Ÿ˜ûUýO*rx7Ü3Ä:o±‘íÝm;›9{®=ôÔËvúñ‡Ú’eËn£´ÏµbÅJëͽvÂÝmóÖ³Qo}d?þú§ 8ó8»þî'lÏ»Ø7³©æíïÂEKì¼+ï²þ§m®·®ËÇöÂkïÚ…ýŽ¥å{u^¬¬‹ € €@ T™@½|ùr¯Joï!0Û~ýõ[°`¾=ôÐca,u¨ß{oœm¼ñÆá4>ñÄ {òÉ!öÎ;ïçŸÖÛo¿ÕgÎoÏ?ÿ’W¯_³ë¯¿Öïwý‰]xa¯Pob·Ür“ÿ™ó† ú”5Ú[¹ÿ ¡ùæ›o³}÷ݯ@»÷СOÛ£>ìÏ34ÿ9>ýôc¯Pßã•ëVûå”(POõqÏ'õêaO¿0Æ.Zlý<°Æê'ŸÝr<,Ÿpäarrrí´7{åùxkß®UýÒzúS¸*r‡¾øf¸€qÜáû…uu!âÔ‹n¶k.ì[`·¾wóýOyE|-ë¹ÿ®Ö ~Ýüç*iê(ˆÆk—ô¸ÉÓf…ð{Ãŧ…íªÂ®Õ¶¡¾û±¶nÛ–vÈ~Ýò÷¡ÿÕ÷†@¼Õ¦ ìWiõ}ƒž·'üiõê¼/ø©Çbëwh[`{šì™WÞ²o½E¼ÅÚMm¯nÛZ·í·²’¶¡àê’÷פ©¡½{ W¤ /ñº¤müö×$ÓnñÛ8ûò;}’·C Ô«ýŠe € €¤–@• Ôª&¿öÚ«^ —¨ýõü¶ìâ*ÔwÞy»}ôѸP¡Ö²ÿþ{ÛÞ{ïkÆ=Zº zÌ~þùg{ã×=@?êã¨w,ðjP¿ÿþ{½"ýD£ýôÓOz dcǾW)¯š ÔzÒ¯þ÷‹WdÇØ{ïl_ûÇš”Lã¢ëdÖã‹£¥ßÀ[½ {´­·n›2ê'FŒ¶ÌÚµì(7]ÚE•_µc?2ôSðVN´øjxIÏõÉW?Øè·ÇÛ5õ »¡Jùro×vãuIÛxÿã¯í]osÚÕµ¾^¡îêÑêÒž\‡ € €A Êê.]¶ ãŸÕzüË/?Û)§œl{ì±§W‰o-6Pkœ³ÆP?òÈã¡U[máÝ»ïkçœsžwÜñáà5nZ!ºGƒìÎ;ï± ~õ½Oh+Wµ»V­Za¼´BWÛ¶±JìÛo¿åÕñS¼íü0AÙÎ;wµ+®¸*ŒËÖ²téÒ°[mµuï}ñÅü^Ò'Ú¦›n¾ÿÀ÷ù¶ÚÙA>3æu¯¤ÿ`çŸA‘—diµVºçñ6eúlkÞ¬qÔßüܨw캧ZíZ5íû_þ°‡},óíWœåÇT³Àó”¦Býõÿ~µ¡/ñïS¬nÚ¦ûV¿üÆv¼eÎHOÏß^–»=8äÅÐjÞ ^¬Ýûú»‡Øî;mcuj×N¸Y³çåW¨Kz® Û…×Þ°&Fûò»_ìÉçFÛ]לg×Ý=Øvëºí²ý–~a!ñþNñ¶ñ+o{Ôn»ü,kÚ¤¡}êV<ñ‚W¬{‡@­ º'uK¯®³ € € €% T™@=~|lLrº¸-ZØ~ûuiïh u|…ZÕdc]Ô?ÜêjÆŒvä‘GÚu×ÝfäÖòá‡Øá‡÷ —zèaák[n¹©·‚olÏ>û|ø\Ï{ DžmDÏuíµ7ä?^ßïßÿi  € € €@ò $] ‘Bõ¸qŸù,ÙËmÞ¼…ɧV‰{Ô¸q#ŸÔ¬–uë¶aºÙ4 € € °ºI¨W÷ X@@@Ê PW¶0ÛG@@¨–êjyZ9(@@@Ê PW¶0ÛG@@¨–êjyZ9(@@@Ê PW¶0ÛG@@¨–êjyZ9(@@@Ê PW¶0ÛG@@¨–êjyZ9(@@@Ê PW¶0ÛG@@¨–êjyZ9(@@@Ê PW¶0ÛG@@¨–êjyZ9(@@@ÊHÊ@}Þ°¹öÞÏK-+§²?9·Ÿ‘n¶çÆuìö^M’sÙ+@@@Kº@}á³síï–Znš™ÿ/e—ÜÜ\;h›zvÃaSÖ€G@@’Y éõÖWLñÊ´¢tn2»Uú¾åúå„Z¹öÕÕ­+ý¹x@@@² $] Þü²)e?Šj¼Æÿ®#PWãÓË¡!€ € €@ P'ùÉ#P'ù b÷@@@ eÔI~ê ÔI~‚Ø=@@HYu’Ÿzu’Ÿ v@@RV€@ä§ž@ä'ˆÝC@@” P'ù©'P'ù b÷@@@ eÔy§¾v ³-שeŸý±¢B_ [ù6ÿ˜±Ò,+ßmÀÔz:Ø € € PaÕ&P±]=k×4ÃncAœý·¨cÛv¨m×¼2¯D4­;êܶ•ß»"—1´°Ë^˜kŸÿY¾ N ®È³Á¶@@@Ѝ6ú¬½Øú-jÚ9CçÐ9vÇz¶çÆuìÄÇg¨+îuÖ@@@”H©@½ûÆ™¶yÛZ¶2+×öݼŽÍYœcw¿¹À¾¸"T·ã+Ôûl–i½v¨o ë¤Û_3³ìÎ7çÛ¤9Ùá³]ÇZvÊn ¬e£ ›>?Çî}{}óO¬½ëF™vÊ® ¬vM³ç>_b'î\ß.‘ uÊÿ¤€ € €ÕN ¥õ¡ëÚÀ…öï7¾_j‡v®g§ïÑÀöºušµòpj{¾÷ئÖǫڿÍȲ£w¨gGm_Ͼ{†ÕÌ0{ïâVvñssí½Ÿ—™ZÊ/>°‘ízã4kìáû­‹ZZ¿!³ìË¿VØñ¦ÏÚ«¡úÄ,Z¾«Ý„ € €©.rú¤n ì€;§‡óž–föñe­¬ïàY6oIN~ ¾ÄrFzš];26îÚ?´/oe'<:Ë~šº2¬—ësŒ5©›nõ3Ólôù-l¯[¦Yçöµíd¯Nz^t³O¯hm§yÀf uªÿ¨qü € € PݪM >ÓÇPoàc¨Ï.4†º÷Nõm× 3í¤A³¼"]×zlU·Àxê×= ß4z~˜‰;ªPßutSûÙƒóCï.Ì?߯÷oa7Žšgã&,·Ënl;t¬mÌʲå+smïM3mßÛ¦Ûž›d†–ï“ÍÎ_ï-mÀˆ9êêö“Ãñ € €ØŠi‡ IDAT €@Ê T›@}L×zÖÓóá÷Í,pR/ëÑÈêe¦‡m꣼}ûˆûW=æý‹[Ú¹ÃæØ¬…ÙùúêžmѲ»õõU3†ti«PiÖ¸é ÷oÚ¿—z˜nèê¼Ê­@½ýzµíˆmëY¯‡bÛW%û³+ZÙéOÎ&P§ü € € PݪM VÐuns»aÔ|{é«%ái-ÛãæiÖ¤^z~ Ö„dçîÓÈóÖm…æ®”oü¿&¶ßíÓ­»™Ö-ºŽz0šÏß·¡© ®6ï oñ~öôæÖýŽé6m~¶í»Y»õÈ&¡:NËwuûÑáx@@@ ÕªM Ö‰ÜÞÛ°/ñŠt³úé–år¯ÌÎõ¸Úȯc[úmêÚü¥9V¿vºuh^Ãî}k½ðÅ’"³|_¸CÛÃo·5}AvØÞu#çÛ§,·F>ñØS§®eKWäZNŽÙ…í®Ý0#Œ±>y×úá9¦Ì˶_§­ôÐ^Ënñ–rúò,܇º6[«QfZÚ¬ùËrssÍrrrüK¹¦³³³ÃçÙYê¤8£ì € € €À¨@í:‡@½FNO‚ € € Q N÷ uFé*Ô¹^‘öÒt\…š@'“½@@@Xs%ê4oùŽõ|ǵ| Ôj÷ÎRÛ·ÿÉÎÉf õš;w< € € €À(uz†¥ûØéþgÕ¸i¡.!Pûj©Ãøéø@Ý®e3«Y#ý?<$ž@@@ÊX™•c§Í.¨}>2ÿ_uhùV…:+Ûš4®odVîž³u@@@þC¹ –ؼùK|†ïX…Zc¨U¡.U Öƒr¤}+´y+T{7w–oÝ.+ÖòO­[g©J«¿®ýïk5±úukùFón¼U9ÇÁV@@@X#9ž.Zn3æÌ ÷œÖ=¨ÓòªÓú\!Z7Ÿÿ…‹™”,jýŸ‡êP¡Ö}¨¨Cë·©ÎÉ ë{zœÚÁó"xÞzÑú:æ´ü¯­ž@@@B±ð«œ»*ëk±Hìc¢=0ës…fU¥k¤×«"µ{‡@Ât‚@­M…¬°71™Ú¾CˆÎ«R+dç†vð¼Çúçªh‡u|ã±@΂ € € €@r „pí™5Äi…ã0>ÚƒtøX¡ÚC´Z¼U•öPk÷.~ü´¶Õ¬aí´´Yó—…êÕãÚ¾žU¥Ži×þ'|¬¯åU¢µnZ\˜ŽkÚ“ëåÃÞ € € €©&àY5nÈrn¨2çýñDn‰Bu,@ëã´ý1ñíÞ…ÇO¨ãÛ¾õ±BsѪ\ç…jU££P‚sù±]R…šâuª½p9^@@@`Í ”4ÝWÔþ«TÇZÀ£0ÆGGa: Ø Û¡N\´Ý»H ÖÃBÕ9*.«2ªCØŽ Õ ÏQ{x °×,φ € € €@a(@G_ŸçÝW:6žzU˜Ö·¢Vï¦ó÷føööï¼%?PÏ^°ÜóplìsÉy““E3~ë™bíݱqÓaÌtÞã¢ÁÝùš!Ô¼z@@@’Q oTòªð¸IÆò&&S¸ŽçØH¯ú8Öê>óy‡nïØçq-â±Ð^tB2bt2¾¨Ø'@@@ u ÷Qçß—:oR²‚a96¨9ªbG­ßña<þãºøP½*D‡Ù¼£€·W…+ÒÜ‹:u^œ) € € P W¨ †äØèvYÑHæ¨c*tEXtCreation TimeMon 11 Nov 2024 12:19:25 GMT‡Ã è IDATxœìýwxTÕÂþÿ¿3™L)@¤—iBŒ±P¨ (Ò@A¾* ‚GM ]¤ˆOPÁC=GŠ€E¥¨ôš‘BBÊdÊïOf~ )!I¸_וK²÷Ú{¯½@¸ge³ÙlÇØíö<¿¶Ûí¹~}ý1qoxxx8]бëínŒ%]ëyxx8CqAh³ÙܺQEDDD$ƒ!×÷×g:wÏxnžÉÑhù…h»Ýž§ÑEDDD¤ôÈ/ »{hvpËðìp}ˆÆ…†ÕP‘’u#A¸´„f·Ïù…è¿++""""î«´f¶Rž\idõ<‹ˆˆˆ”¬ÒŒ]QªÂ³+Êòo–ˆˆˆˆ”,ͼq‘³ˆˆˆˆˆ‹žEDDDD\¤ð,""""â"…g)<‹ˆˆˆˆ¸HáYDDDDÄE Ï""""".Rxq‘³ˆˆˆˆˆ‹žEDDDD\¤ð,""""â"…g¹ex>wî‘‘‘ί¸¸¸[ö,›ÍÆàÁƒéÒ¥ ?ÿü³ó¸ãÙ6l¸¡û-Z´ˆÈÈHºuëV,å\Ñ·o_g}[µjEÇŽyõÕWùþûï‹t¿¢¾»ˆˆˆHYç–áyóæÍxxx°uëÖ[ö,³ÙÌ‘#Gøë¯¿niH¿]l6ÉÉÉìÙ³‡1cÆ(‹ˆˆˆ#· ÏŽ°üØc°eË–[ö,/^ÌÔ©S‹¥ØÁüo§§žzŠÝ»w³lÙ2jÕªÀ§Ÿ~zÛë!"""RVKº׋çĉ † ÆŽ;8}ú4§OŸ¦N:ôéÓ‡“'OòØc1iÒ$çµ³fÍâÿþïÿ¨P¡ëÖ­###ƒU«V±sçNÎ;‡Åb¡V­ZôêÕ‹Ž;:¯ëׯãǧK—.Ömݺu|ûí·œ9s†””*UªDÇŽéß¿?^^^¹ÊšL&vîÜÉìÙ³¹téaaa¼ñÆÎw(ȉ'˜?>¿þú+‹…:uêзo_ÚµkçRûyzzR¿~}ºwïÎÌ™39wî™™™x{{œœÌܹsÙµkÉÉÉT­Z•'Ÿ|’^xƒ¡àÏR6›+V°~ýzΟ?OPPmÚ´aèСpíÚ5—Ú{íÚµ|õÕWÄÇÇãééIõêÕéС/¼ð‚ËϺpásçÎeïÞ½$''L³fÍ0`uëÖu©­DDDDn”Ûõ<;z™Ã¨V­Íš5ƒë†n8nLL ™™™ÎãÛ·o cÇŽxzzâååEtt4G¥\¹røùùqôèQ&L˜ÀîÝ»o¸nÛ·oççŸÆjµR¥JâããY´hóçÏÏS6!!±cÇréÒ%²²²øõ×_1b×®]+ðþ§N⥗^b×®]R£F :Ä›o¾é|7Weee`0œ¡8##ƒÁƒóÍ7ß`±XhРçÏŸç“O>aæÌ™…Þï½÷Þ#**ŠóçÏÓ°aC233Y³f ÇÇjµ¸ÔÞ111Lž<™ãÇS¹reÊ•+DZcÇ8|øð =ë­·Þâ¿ÿý/Ô®]›ÔÔT6oÞ\è‘›åvIÞ[·n @›6mr'; ÒÓÓùñÇ8tè.\ sçÎÝû;qâDÖ¬YÃêÕ«Y³f 5kÖ`ÇŽ7\·¡C‡²xñb6mÚDtt´3Ä÷ÝwyÊ^»v·Þz‹;vðî»ïBv .lüöÂ… ÉÌ̤E‹|ùå—,_¾œðÙgŸ¹TG‹Å‘#GX½z5<ð€³W|ݺuÄÅÅÄÿýßÿ±hÑ"þõ¯°fÍ®^½šï=O:ŦM› »wÁ‚DGGÀ±cÇøá‡ÀÅö>~ü8þþþ|úé§|óÍ7¬^½šaƹü,»ÝΉ'èÕ«+V¬`óæÍÌ;—»ï¾Û¥v) ·¶Ç©S§ GxnÛ¶-3fÌ ..Ž“'OR¯^=*T¨@dd$111lÛ¶‡~˜mÛ¶pÏ=÷pÏ=÷8ï @jj*™™™Ô®]›øøx.]ºtÃõ«W¯d÷থ¥9Ç_¾|9OY///þñ`0èØ±#}ô =z”§žz*ßûïÝ»€ôôt¦OŸžëÞŽv)ÌÚµkY»v­óûŠ+òúë¯ç¹¿Édrö–ÛívÈÝqqq4nܸÀz ¶lÙâü c2™œu{衇À…övÜÿÚµkôèуgŸ}–îÝ»pCÏjÔ¨û÷ïgÑ¢E9r„Þ½{Ó¼yó¿m#‘›áVá9gï²còõç¶K—.ÄÄİ{÷n²²²œáÙÑëLvÿè£øé§Ÿ0›Í7U·ôôtfϞͶmÛòôÐ:hNåË—Ï5„ B… $$$–––ïým6)))>|8×0²‡aX­V<==]®óÌ™3©^½ºóûäädÈäkÖ¬ÉS>###ßû8®³Ùlù^—žž.¶w‹-xûí·ùøãIHH`Þ¼y¬X±‚÷Þ{–-[ºü¬I“&ñÏþ“ü‘]»v±k×.Ú¶mË{ï½çß-"""RÜÜ*<ÿÝ’t›7ofÈ!Ý#@JJ _}õçÏŸÇÓÓ3×Ä´×_¸¸8ªW¯N¿~ý¸ë®»X¾|9û÷ï¿áº}ôÑG|óÍ7x{{óòË/s÷Ýw³gϾüòË|Ë_¹rÅvív; ÎÖë çû 2Ä9\ãF<õÔSŒ;–—_~™}ûö1kÖ,>þøcgˆ  ""‚9sæ¸|_Çu&“‰íÛ·8®ØÕö~òÉ'yüñÇÙ¸q#K–,!!!qãÆñí·ßºü¬ *0kÖ,Ž;ƲeËØºu+;wîdÕªUôíÛ×åw¹n3æ966–Ó§OðÏþ“Í›7;¿>øàÈÞ<娱c=,±”Ýúõë!{È@…  »Õ±nsÏž=éÚµ+\¹r¥Hõ;zô(-[¶¤oß¾<ôÐC…öf›Íf¾üòK¬V+ëÖ­#11²'Bä€ìáްMöÊGŽq©žŒ????öíÛ—k¬txx8d8pà€óxZZZ® b®ç¸Îl6³bÅ çq›ÍÆ®]»°Ûí.·÷‘#G8wî¾¾¾tïÞáÇCö0ŽŒŒ —že·ÛùþûïÉÊÊ¢AƒLš4É9¶ÚÑÎ""""·‚Ûô<;†lFzè!üüüœçZµj…··7™™™lÙ²… @öÐo¾ùÆ98ç ªT©Â… øôÓOùõ×_9qâD‘Æ:Ü}÷Ý>|˜Ý»wóÆoœœì ò™5ksæÌÁf³P©R%yä‘Ë2„Ÿ~ú‰?ÿü“gžy†5j’’BBBýû÷§aÆ.ÕµjÕªŒ1‚>ø€E‹N“&MxòÉ'Y·nÇŽcذaÔ¨Q«ÕÊ¥K—hذ¡3¼_¯N:<óÌ3¬^½š¨¨(¾üòKüüü¸tééééìÞ½ÛåöÞ±cŸ}ö!!!TªT‰ØØX5j„¿¿¿KÏ3f &“Éùñññ<øàƒ.µ‘ˆˆˆHQ¸MϳcÈFãÆsg²„ߢE ¸n\tãÆ=Ž´mÛ6×u“&M¢Aƒ¤¤¤ðË/¿pÿý÷绬œ+^yåzè! ?ÿü3>>>,]º”Ê•+ç[¾M›6tèÐL&Í›7gΜ9øøøøŒZµj±téR:uêD`` ±±±¤¥¥çªnݺѲeKl6ï¼ó)))x{{3oÞ<úöíKµjÕ8þ< Ü}÷Ý<ñÄ…Þo̘1¼þúëÜ{、¤¤pîÜ9Ê—/OÏž=C+\iï&MšÐ°aCÒÓÓ9vì~~~téÒ…©S§ºü,Ç$ÌÀÀ@çdĆ òî»ïÞp;‰ˆˆˆÜ³Ùœw¶›ˆˆˆˆˆäá6=Ï"""""îNáYDDDDÄE Ï""""".Rxq‘³ˆˆˆˆˆ‹žEDDDD\¤ð,""""â"…g)<‹ˆˆˆˆ¸HáYDDDDÄE Ï""""".2–tŠ›Ýn/é*ˆˆˆˆÜÑ<<ŽNP‹ÅBFFf³“É”+D»sÖó0›ÍnÑ}›34Ûl6Ìf3þþþn—ñEDDD¤X,®]»€ÉdÊÕQêŽ!Ú-³#8Ûl6l6øøøàççWÒU‘Û --Í™ ƒs\´»èß$åúàœ––†ŸŸŸ‚³ˆˆˆÈÄ‘ÿÒÒÒœ¹7\ ¢DÃs~=ξ¾¾øøø”dµDDDD¤øøøàëëKFF†Ûè Ï9ƒ³Ýn'++ ²Ç8‹ˆˆˆÈÉ‘³²²œsáp£]"áùú—·Z­˜ÍfJ¢:""""âF0›ÍX­Ö\ÇÝ!@—è° G¯³ÕjÅ`0àååU’Õ7àåå…Á`pîñ‘s“¼’Vâm6YYYš (""""N~~~dee¹Up¦$ÂsÎõœ_V«“Ét»«"""""nÊd2åÚ]:g†,I%:a0çVÛùíq.""""w&ƒÁàâ›3<—´O¬ŽFq·°EDDD¤ä8¶ê¾ã‡mä”s¹:‘œÜm™:cIW7kùÒÓÓÉÌÌÄb±”tU¤1x{{ãëë{Ó÷rÇŒèáùftïÞ¸¸¸Ï·lÙ’>úˆñãÇó矲xñâBï׿:uêD¯^½n¨;w¦[·n <8ßó‡¦_¿~Îï}||¨U«O<ñ={ö,–1ß=ö½{÷fàÀyÎw;Ý«V­âÃ?ä§Ÿ~*–ûç»ÅÇÇóôÓOEDDD‘ïsöìYzõêEëÖ­™6mÚM×KD¤8X­VRRRš¥H, ‹…ÌÌLðôô,é*«Ržßzë-ÒÒÒ8qâQQQ¼ñÆT©R€ *ÜÐýjÖ¬IÅŠoI]FEݺuILLd÷îÝ̘1ƒ„„FŒqËžÉ-h'ÉkÓ¦MìÙ³‡‰'ºTÞf³1qâD¼½½oyÝDDn„‚³‹ÅBJJ ÁÁÁ%]•bUêÃs‹-œ¿v„æÍ›S·nÝ"Ýïý÷ß/¶ºå',,ŒfÍšðü²{X‡ †Ñxë~;Š»J«[91õ»ï¾»¡õÊW¬XAjj*÷/"n#==]ÁYŠÅb!==½X†p¸‹Û:a°¤×çóôôäÛo¿¥G´jÕŠnݺ±jÕª\e:wîLTT”óûñãÇ3}út¶nÝJÏž=iݺ5Ï<ó «W¯.ôY¿ýö­[·fÅŠ…–kÖ¬\»v €¤¤$f̘ÁsÏ=GëÖ­yâ‰'ˆŠŠÊÓf_ý5]»v¥U«VôéÓ‡={öÛ®´SDD+W®ÌuìÏ?ÿ$<<œ˜˜ç±Î;³uëV.\H—.]hÓ¦  `ÿþý¹®½p᯽ömÚ´¡C‡Lž<9ß¿¼ÓÓÓ™6m:t mÛ¶ ><×p”øøxÂÃùrå 3fÌàñÇçÅ_tž7™L¬^½š®]»Ò²eKžþyvîÜ™ç9ÑÑÑ<÷Üs´jÕŠ'Ÿ|’¨¨(²²² l³~ýú±mÛ6Ö¯_Oxx8ááá\ºt©Àò±±±,\¸ñãÇë)q+™™™%])cŠóÏTIgIÊBÏó8sæ +V¬`èС±aæM›F½zõhÞ¼y×mÞ¼™Ÿþ™ÿùŸÿ¡bÅŠlܸ‘É“'S¯^=š4i’§|ll,#GŽäÙgŸ¥OŸ>…ÖéìÙ³ÙÁõäÉ“ 0€Úµk ¨Q£O=õk×®eÊ”)tïÞÇœ .0iÒ$RSSoº¸‰v*ÈôéÓ c̘1F.\ÈèÑ£Y»v-þþþ˜Íf† FFFãÇ'00o¿ý–… æºÝngÔ¨QÄÅÅ1zôhBBBX¼x1ƒ櫯¾" ÀYvâĉ¤¦¦2bÄçЀ#GŽ••Ř1cðòòbÍš5Œ=š… Ò´iS¢¢¢X¶l¤iÓ¦üñÇ|òÉ'œ9s†éÓ§çûŽ|ð£G¦FŒ9€|ËÚl6&L˜ÀsÏ=GãÆÝr2„ˆÜ¹ô^Š[Yû3uG…çääd>ÿüs*UªÙ½¾;vìàÇ,4&%%±dɪW¯ÙC ¾ÿþ{öìÙ“'<_ºt‰W^y…Ö­[;CTN™™™\»v¤¤$vîÜÉ—_~ÉóÏ?ï<˜«ç»Q£FlÞ¼™8ÃóÒ¥KiÞ¼9cÇŽu–«T©/¿üòMµCQÛ© &“‰?üÐ9)²R¥JôîÝ›cǎѼys¶oßÎÙ³g™7oáááDFFËÁƒ÷ùé§ŸøùçŸùøã‰ŒŒ nݺtèÐ 6гgOgÙÄÄD>ûì³<1ÓÓÓ™>}ºsüUDDO?ý4Ÿþ9M›6%%%…åË—óâ‹/2hÐ g™FÅÁƒiÔ¨Qžw¬V­&“ ???ªU«Vh{,[¶Œôôt† rÃm)"""%ëŽ Ïµk×vB²w®©R¥ ÉÉÉ…^W£F gp&»w¸J•*üõ×_¹ÊY­VFŽI5˜0aB¾ãks\???zõêŰaà }~ÕªUu¼víqqqtëÖ-W™x Øvi,j;$"""WÝméh¿C‡áççç Î>ø`®ðüË/¿àãã“ku‹   ªW¯ÎáÇs]ûôÓOçÛµjÕÊ5qÁÃȈçP“ßÿ¬¬,Ú´i“ëºV­Za2™Ø»wo¾áÙU§OŸfñâÅÌŸ?_[Ò‹ˆˆ”BwTxÎùc}Çî5…)_¾|žcŽ-#sZµjf³™J•*a6›óøæ›oR¿~}Ê•+G­Zµò”¹xñ"K—.åÀ\¹r‹ÅBjj*-[¶„ìðL>«c †|߯(ŠÚN¹¾ý*÷KMMÍw&îõ×]¹r…ŒŒ g¯³ƒÍf£víÚ¹ŽÔû›ß„¾   g»:> \ß¾F£‘råÊùÙ®&NœÈÓO?Mhh¨sõ›Í†‡‡iiiøøøh«z7vG…碮´àêu 6dܸqôëשS§òî»ïæ)SÐ8i³ÙÌ Aƒ(_¾<ǧfÍšøúúòÖ[o9ËøûûCŽç`·Ûaìf¹ò¾ù•qнŸ¿¿¾¡4%%%×÷æ MŽvù»gæ÷œ+W®8?08B|rr2•+Wv–±Z­¤¦¦Xè»æ?þàðáÃ>|8ÏdK€¶mÛ2uêT{ì±"?CD‰¼úê«Ìž=[KŽŠÜ&wTx¾Õš5kFµjÕ7n£G¦U«VtìØÑåëOœ8ÁŸþÉ„ r--wñâEÊ•+Ù!±V­ZüðÃôîÝÛYÆ1Üàv È3lå÷ß/Ò½ÂÂÂX¹r%¿ÿþ;7v¿~EŽ-Z°téR233 +Ò³Î;Ç… œ“m6{öìqÅhÒ¤ åÊ•ãûï¿§~ýúÎëbbb0›Í…nˆb2™ÈÈÈ(ð|•*UX´hQžãŸ|ò ƒ¡C‡r÷Ýwé½DäÎ’””İaÃ8yò$ƒfÞ¼yNT‘â£ð| ´k׎nݺ1yòd7nü·Èîºë. »ví",,Œ””–.]Jbbb®õ˜_xá¦L™Â‡~H»ví¸|ù2K–,¹¡5†oVdd$6l ]»v„††røða–,YR¤{µk׎jÕªñöÛo3|øp‚‚‚ؾ};ÇÏó̈ˆFŽIÿþý©S§iii;vŒV­Z9×Ï.L@@£FbàÀ”+WŽÕ«Wsá¦L™ÙNþ÷ÿ—9sæ`4iÚ´)gÏžeΜ9tèСÐÐ^·n]6lØÀwß}GPPsxŽƒ··w¾u ÄÓÓÓ¥ú‹ÈëÅ_ä·ß~Ë÷\ll,:ur~߬Y³|?¬—&S¦LaëÖ­4lØ>ú(×¹wÞy‡˜˜î¿ÿþWArWË–-cÙ²eT®\™Ï?ÿ¼¤«#E ð|‹¼öÚkìÛ··ß~Ûå¿À*UªÄĉùì³ÏˆŽŽ&$$„=z0hÐ 8à,×½{w²²²X¹r%«W¯¦^½z¼ùæ›·õ/Ê1cÆ0uêT†ŽÅb!,,Œ>ø H+Høøø0wî\¦OŸÎ»ï¾‹ŸŸíÛ·çý÷ßç•W^q–3 Ìš5‹ ÍåË— à¾ûî£sçÎ.=«wïÞ3{öl å_ÿú÷ÝwŸ³L¯^½ðòòâ‹/¾`îܹT¨P'Ÿ|òoßmРAœ;wŽ &àëëË’%Kr…g‘›QPpÎOÎ3D¤xy˜ÍæÛ¶ÈlÎ…­ív;V«‹ÅÂÕ«W©Y³æíª†ˆˆH©sýŠDç—_~)ÒsŠt]qSÏsÙRÔ!Eñññb4ñôôÄÃÃÃ9¯éVî\õ<‹ˆˆ¸¹ÄÄÄ<Ç*T¨ÀìÙ³xõÕWó”ILL¼£&Ž5Šß~ûwß}—'N°qãFÒÓÓiÙ²%¯¾új®‰å6l`åÊ•$&&rï½÷2bÄ^yå233Y±bUªTq†Ü#FP§NæÎËÉ“'Yºt)¬Y³†Ý»wsþüy¬V+5jÔàÙgŸåÑGu>Çb±°páB6oÞŒÝn'"""×Ò·9íÚµ‹èèhNŸ>Édâ¾ûî㥗^"44ô¶´Ÿ¸NáYDDÄ9&æT¾|y¢¢¢¨W¯ ,`È!¹zïÔI„3gÎäÊ•+F, Û¶m#--÷ß²{ägΜ Ù=—‡âÿûÿþ¿·Þºu+ ,pNÀh4²zõj®^½JåÊ•ÉÊÊâøñãLž<|ðAæÍ›Ç7ß|ã|ÖÖ­[óŸôŸÿü‡?ü€úõ드œÌ?þÈo¿ýÆ‚ rí”+%O ÊŠˆˆ¸±#F8WÕp˜={¶38„††2cÆŒ\ebcc5jÔm«§»ÈÌÌdÁ‚lÚ´‰îÝ»ðã?rîÜ9¾üòKÈ^é‹/¾`ýúõ…®¤tèÐ!*UªÄ믿ÎôéÓñ÷÷Çd21vìXV¬XÁŠ+øüóÏ=Ê»ví ##ƒ7BöjQk×®å‹/¾ÈóaÆf³9ç,õíÛ—¨¨(–/_NãÆIKKãßÿþ÷-i'):…g‘R&¿•Šº„gYF:uðððȵ¯ãˆã¿íÛ·'$$“ÉijÏ>[è=?øà:tèÀý÷ßï<NåÊ•¹víZ®¹[ŽÞÿØØXÌf3=zôÀ××—»îº+ת(ŽrW®\àÔ©SÌš5‹9sæ8{ºÏœ9S,í"ÅGÃ6DDDÜØœ9s:th®ÞgÇäAǤÀü&†††:‡'”FŽÝV4'«Õš«LN9w¬Í¹±UZZv»«W¯Böî²+V,°+V¤R¥J¹ŽÅÇÇ3þ|öíÛ—§~ŽÅrn–³·ùúg9êCöž×+lÿ) Ï"""nÌ1¾¹C‡7tÝ‚ Jõ„AGð½xñ"‹£ñÿYþøãÈž4y½œ'sn怇‡$''çÚq655µÀzä|®Ã;ï¼C|||ø ÕkܸqÜsÏ=¤¦¦²ÿ~š4iâ\s;§aÆѵkWÊ•+‡ŸŸ;wfæÌ™yêýÈ#0mÚ4ÂÃÃñððàìÙ³xxxðè£:'"6kÖ 5jDPP<øàƒøøø8úFpp0µjÕ¢F7ôNâ:í0(""RÊ$%%åšDÊüùó øæ*wÙaðF96I騱#cÆŒ¹¡kcbbxçw {ˆÅõ½ÃróÊÒƒêy)e“àcÁ‚ÅœËGG]A:ä#ì(¿}ûvÈ^ CÁYþŽÆ<‹ˆˆ”B*T`Ù²e%]RgïÞ½,[¶Œ   BBBHMMåâÅ‹ôéÓ§¤«'¥€Â³ˆˆˆÜ1ÂÂÂhÒ¤ qqqÄÅÅáííM“&MèÖ­mÛ¶-éêI)PfÆ<'''³iÓ&¼¼¼œÛq:œ;wŽíÛ·“””Dpp0?üp¡Ï‹gÓ¦M4mÚÔ¹?=Ù[hîÚµ‹Ó§Oc6› $""¹숈HiWZÇ<‹{+KcžËDÏó‰'رcÕªU#===×¹ÌÌLþýïóè£Ò AâââX·n}ûö% Ͻbbb8yòd®Š<È™3gèÑ£~~~?~œ72xð`L&Ó-}G)yeb £·¹zõêyÎ:uŠàà`î½÷^<<< ¥zõê=z4ß{ѳgOüýýóœËÊÊ"00йkP•*U°Z­;9ADD¤´ÈoG=‘›QÖþL•‰ðJùòåó ±‰‰‰yf ‡„„øc©ûɔï½Â¸zõ*Û¶m#66–7Ò¶mÛ^`]DDÄ]éß4)neíÏT™Ï…ÉÊÊrnéàååEVVÖ ßË××—ˆˆŽ=ÊæÍ›ñðð nݺÅX[‘’åëë[æz ¥äF|}}KºŪ̇g“É”'(geeáããsÃ÷úõ×_9pà `àÀ4hЀèèh®^½ZŒ5)Y ÐrÓŒFc¾óËJ»2ž+V¬È_ý•ëXBBB¾ÿÎÁƒ¹ÿþûñóóÃ`0ЬY3‚‚‚8sæL1ÖXDD¤dyzzŒ¿¿¿B´Ü0£Ñˆ¿¿?ÁÁÁxzz–tuŠ]™ÿ?¢N:ìØ±ƒãÇS¿~}Ξ=K||<>ú(Àb±8÷…/Lùòå9uêõêÕÃh4rùòeþúë/îºë®Ûð&"""·—¯¯o™û‘»ÈÍ*óáÙd2ѵkW¶mÛÆæÍ›ñóó£cÇŽÎ#\¾|³ÙìÒ½yä¶oßÎ’%K°Ûíx{{Ó¦MªU«v‹ßBDDDDÜA™Ù$EDDDDÊwÜ$¥Ìy). Ï""""".Rxq‘³ˆˆˆˆˆ‹žEDDDD\¤ð,""""â"…g)<‹ˆˆˆˆ¸HáYDDDDÄE Ï""""".Rxq‘±¤+à˜˜XÒU)”Û„ç *”tDDDDÄ\»v­¤«‡†mˆˆˆˆˆ¸HáYDDDDÄE Ï""""".Rxq‘³ˆˆˆˆˆ‹žEDDDD\¤ð,""""â"…g)<‹ˆˆˆˆ¸HáYDDDDÄE Ï""""".Rxq‘±¤+ """î)==ÌÌL,KIWEJ£Ñˆ··7¾¾¾%]•[¢Ô‡çîÝ»Wàù–-[òÑG1~üxþüóO/^\èýú÷ïO§NèÕ«× Õ£sçÎtëÖÁƒç{þðáÃôë×Ïù½µjÕâ‰'ž gÏž 7ÿC€Ç{ŒÞ½{3pàÀ<犻n‡U«Vñá‡òÓO?ËýŠóÝâããyúé§‰ŠŠ"""¢È÷9{ö,½zõ¢uëÖL›6í¦ë%"R¬V+))) ÍR$‹‹ÅBff&xzz–t•ŠU©Ïo½õiiiœ8q‚¨¨(Þxã ªT©@… nè~5kÖ¤bÅŠ·¤®£F¢nݺ$&&²{÷nf̘ABB#FŒ¸eÏä´“äµiÓ&öìÙÃĉ]*o³Ù˜8q"ÞÞÞ·¼n""7BÁYŠƒÅb!%%…ààà’®J±*õá¹E‹Î_;BHóæÍ©[·n‘î÷þûï[ÝòF³fÍøÇ?þÙ=¬Ã† Ãh¼u¿ÅÝN¥•‡‡Ç-»÷wß}‡ŸŸŸËåW¬XAjj*Øl¶[V/‘‘žž®à,ÅÆb±žž^¦†pÜQ===ùöÛoéÑ£­Zµ¢[·n¬Zµ*W™Î;åü~üøñLŸ>­[·Ò³gOZ·nÍ3Ï<ÃêÕ« }Öo¿ýFëÖ­Y±bE¡åš5kFFF×®] ))‰3fðÜsÏѺukžxâ ¢¢¢°Ûí¹®ûúë¯éÚµ+­Zµ¢OŸ>ìÙ³//¯"´J^®´SDD+W®ÌuìÏ?ÿ$<<œ˜˜ç±Î;³uëV.\H—.]hÓ¦  `ÿþý¹®½p᯽ömÚ´¡C‡Lž<9ß¿¼ÓÓÓ™6m:t mÛ¶ ><×p”øøxÂÃùrå 3fÌàñÇçÅ_tž7™L¬^½š®]»Ò²eKžþyvîÜ™ç9ÑÑÑ<÷Üs´jÕŠ'Ÿ|’¨¨(²²² l³~ýú±mÛ6Ö¯_Oxx8ááá\ºt©Àò±±±,\¸ñãÇë)q+™™™%])cÊÚŸ©Rßó|#Μ9Ê+:t(AAAlذiÓ¦Q¯^=š7o^àu›7oæçŸæþç¨X±"7ndòäÉÔ«W&Mšä)ËÈ‘#yöÙgéÓ§O¡u:{ö,Avp=yò$  víÚÄÄİ`ÁjÔ¨ÁSO=ÀÚµk™2e Ý»wçñÇçÂ… Lš4‰ÔÔÔ›n#n¢ 2}útÂÂÂ3f F£‘… 2zôhÖ®]‹¿¿?f³™aÆ‘‘‘Áøñã äÛo¿eáÂ…¹îc·Û5jqqqŒ=š/^ÌàÁƒùꫯp–8q"©©©Œ1Â94àÈ‘#dee1f̼¼¼X³f £GfáÂ…4mÚ€¨¨(–-[ÆÀiÚ´)üñŸ|ò gΜaúôéù¾ã|ÀèÑ£©Q£#GŽ $$$ß²6› &ðÜsÏѸqã<ŒDDJ’>ÐKq+k¦î¨ðœœœÌçŸN¥J• »×wÇŽüøã…†Â¤¤$–,YBõêÕ!{Ä÷ßÏž={ò„çK—.ñÊ+¯ÐºukgˆÊ)33“k×®‘””ÄÎ;ùòË/yþùççsõ|7jԈ͛7sàÀgx^ºt)Í›7gìØ±Îr•*Uâå—_¾©öq(j;Äd2ñá‡:'EVªT‰Þ½{sìØ1š7oÎöíÛ9{ö,óæÍ#<<€ÈÈHbcc9xð ó>?ýô?ÿü3ü1‘‘‘Ô­[—:°aÃzöìé,›˜˜ÈgŸ}–g"fzz:Ó§OwŽ¿Šˆˆàé§ŸæóÏ?§iÓ¦¤¤¤°|ùr^|ñE ä,¨Q£8xð 5ÊóŽÕªUÃd2áççGµjÕ meË–‘žžÎ!Cn¸-EDD¤dÝQá¹víÚÎ@`0¨R¥ ÉÉÉ…^W£F gp&»w¸J•*üõ×_¹ÊY­VFŽI5˜0aB¾ãks\???zõêŰaà }~ÕªUu¼víqqqtëÖ-W™x XVìà&Ú© ¹êæhKGû:t???gpvxðÁs…ç_~ùŸ\«[Q½zu>œëÚ§Ÿ~:ßö¨U«V®‰ DDD8‡šüþûïdeeѦM›\×µjÕ “ÉÄÞ½{ó Ï®:}ú4‹/fþüù˜L¦"ßGDDDJÆžsþXßÁÃÃãol^¾|ù<Ç CžI^«V­Âl6S©R%Ìfs¾ß|óMêׯO¹rå¨U«Vž2/^déÒ¥8p€+W®`±XHMM¥eË–žÉgu ƒÁïûEQÛ© ×·ŸãC…ã~©©©ùÎĽþº+W®‘‘áìuv°ÙlÔ®];×±‚zó›ÐälWÇ„ëÛ×h4R®\¹"€ ûÃÕĉyúé§ u®~b³Ùððð -- Ÿbû$"""ÅïŽ ÏE]iÁÕë6lȸqãèׯS§NåÝwßÍS¦ qÒf³™AƒQ¾|y†NÍš5ñõõå­·Þr–ñ÷÷‡!ÏÁn·;ÃØÍrå}ó+ã 7z?ÿ|CiJJJ®ï Ì3šíòwÏÌï9W®\q~`p„øääd*W®ì,cµZIMM%00°Ðw)ÌüÁáÇ9|øpžÉ–mÛ¶eêÔ©<öØcE~†ˆÜ9yõÕW˜={¶–¹Mî¨ð|«5kÖŒjÕª1nÜ8FM«V­èر£Ëן8q‚?ÿü“ &äZZîâÅ‹”+W²Cb­Zµøá‡èÝ»·³Œc¸ÁígØÊï¿ÿ^¤{………±råJ~ÿýw7nì<~ýŠ-Z´`éÒ¥dffV¤g;wŽ .8'Úl6öìÙãŠÑ¤IÊ•+Ç÷ßOýúõ×ÅÄÄ`6› ÝÅd2‘‘‘Qàù*Uª°hÑ¢<Ç?ùä C‡åî»ï.Ò{‰È%))‰aÆqòäI̼yó œ¨,"ÅGáùh×®ݺucòäÉ4nÜøo'9Üu×] víÚEXX))),]º”ÄÄÄ\ë1¿ð L™2…?üvíÚqùòe–,YrCk ߬ÈÈH6lØ@»ví åðáÃ,Y²¤H÷j×®ÕªUãí·ßføðá±}ûvŽ?žç™Œ9’þýûS§NÒÒÒ8vì­Zµr®Ÿ]˜€€FÅÀ)W®«W¯æÂ… L™2²?œüïÿþ/sæÌÁh4Ò´iSΞ=Ëœ9sèСC¡¡½nݺlذï¾ûŽ   çðooï|눧§§Kõ‘;׋/¾Èo¿ý–ï¹ØØX:uêäü¾Y³fù~X/M¢¢¢r- ëççG5xì±ÇèÖ­Û ïZ7oÞ<þûßÿòüóÏçš`î.FÅo¿ýF‡xýõ×xüñÇ3fÌ uÆÉ­¥ð|‹¼öÚkìÛ··ß~Ûå¿À*UªÄĉùì³ÏˆŽŽ&$$„=z0hÐ 8à,×½{w²²²X¹r%«W¯¦^½z¼ùæ›·õ/Ê1cÆ0uêT†ŽÅb!,,Œ>ø H+Høøø0wî\¦OŸÎ»ï¾‹ŸŸíÛ·çý÷ßç•W^q–3 Ìš5‹ ÍåË— à¾ûî£sçÎ.=«wïÞ3{öl å_ÿú÷ÝwŸ³L¯^½ðòòâ‹/¾`îܹT¨P'Ÿ|òoßmРAœ;wŽ &àëëË’%Kr…g‘›QPpÎOÎ3ÊŠ´´4Ž?ÎñãÇ9}ú4cÆŒ¹¡ë=JJJJžŽ™â2wî\¾þúk–.]šk‘){<Ìfóm[dÖ1AÌn·c·Û±Z­X,®^½JÍš5oW5DDDJëW$ú;¿üòK‘ž“P¤ëŠ›£ç¹Zµj,]º”„„>úè#bbbððð`åÊ•Üu×].ß/!!ƒÒ¼y󛚿’»ÝNÏž=ù믿ŠžËzÏsQ‡ÅÇLjÑhÄÓÓ缦[¹kpaÔó,""âæó«P¡³gÏàÕW_ÍS&11±ÌL"ôððà®»î¢ÿþÄÄÄ`·Û‰u†ç«W¯²xñb~øáRRR¨\¹2:u¢GÎŒ&Ožœ'œ:ëG}ÄŽ;ضm4hЀ¡C‡R§NÈÞäcÉ’%ìܹ“„„üüü¨S§Ï<ó uëÖeÊ”)Îy@ýû÷à‰'ž`äÈ‘¤¥¥±fÍvïÞÍùóç±Z­Ô¨QƒgŸ}–G}´„ZTn†ÖÄqcŽÉ9•/_ž¨¨(6lHÆ Y°`Ažž½Áƒ»M/rqɹS#gffòꫯ²aìV+õêÕãâÅ‹,Z´(צc…yçwøê«¯HII!--ýû÷3~üxÌf3Ÿ~ú)ÑÑÑ\ºt‰ZµjAö¤ö„„Nž<™k}­Zµ¨[·®3ØFV¯^͉'ð÷÷Ç××—ãÇ3yòdöìÙS¬í#·‡zžEDDÜØˆ#œ«j8Ìž=›zõê9¿ eÆŒôë×Ïy,66–Q£F±lÙ²ÛZß[Án·sñâEç»Ò°aC6mÚäüÑþ§Ÿ~JPP{÷îå7Þ`ýúõôïßÿo÷A¸rå |ðááá|õÕWÌ›7‹/Kýúõ9uêd¯È4mÚ4<<<8räwß}7>>>øûûóÚk¯ðÞ{ïå¶a2™;v,5kÖ¤råÊX,ȹsçØµk>øà-l9¹žEDDJ™üVþ)êžîîÏ?ÿ¤}ûöÎïF#£Gv®0å˜i2™œ«>9æXY,âããÿ¶m6lèSþðÃ3oÞ<È']¿~}ÂÂÂØ»w/û÷ïgÈ!Î!®®øá¸÷µk×ÈÌ̤fÍšœ;w®ÌýdàN¡ð,""âÆæÌ™ÃСCsõ>;˜cR`~“ CCC™9sæm¬éí1hÐ ZµjåüþêÕ«tׯ_Ÿ§|aëï;äÜËd29íá}ûöåÚµk¬]»–S§N1uêTV®\É”)Sœû$>>žùóç³oß>ç0ëï/¥‹Æ<‹ˆˆ¸1Çøæµ`Á*V¬xKêt;U«V-[¶Ð§O¾øâ‹\“#+g´hÑ‚-[¶äùjÞ¼ùß>ÃËË«ÐóƒaƱbÅ zö쉗—ñññÌ™3çoïýÎ;ïðã?R±bEFŤI“ ÜiXJ…g7WЪááá.aWVVÚpèÓ§õêÕsŽOvôÚÞÿý=|#çN·iiiìÛ·¯Xž½wï^RRR ᥗ^r#IJJ‚ìM°Ξ= Ù=â™™™ÄÇÇCö ;w¦E‹$''©f³™¹sçòñÇ;{Ô<Èäɓٱcd÷f/]º”ýë_ù®Ò"7OÃ6DDDÄíFÞ|óM† ¾}ûˆŽŽ¦gÏžtêÔ‰ÿüç?œ8q‚Q£FQ½zu¬V+—/_æÞ{ïu©çùï,_¾œÃ‡S¹reÊ•+çœ@Ø¢E j×®Œ?oooZ´hÁ?ÿùO*W®ÌÅ‹Y±bäÔ©S\¾|¹HõØ·o_ý5÷Ýw<òK—.eÿþýìß¿Ÿ‡~˜ØØX–/_@õêÕÝr7ÅÒN=Ï"""¥@³fÍ\.ëè-kBCC0`K–,áÈ‘#x{{3cÆ zöìIµjÕ¸p቉‰Ô®]›:Ës[¶lIÕªUILLäÌ™3T­Z•>}ú8ëâëëËØ±c¹ûî»1™LøûûÓ AÆÇ=÷ÜCjj*û÷ï§I“&Îõ¹oTݺu©R¥ wÝuõë×àÀÛÛ›–-[P¥JêÔ©C`` 5*–÷—ܴàˆˆH)“”””kahh(óçÏ/–1ÎZBn…²´Ã zžEDDJÇ$°°0ÂÂÂÊÌä@‘Ò@cžEDDJ¡ *”‰ PDJõ<‹ˆˆˆˆ¸¨Ìô<'''³iÓ&¼¼¼èÞ½{®sçÎcûöí$%%ÌÃ?\èëøøx6mÚDÓ¦Mól›i±XسgG%++‹ªU«Òµk×[ö^""""â>ÊDx>qâ;vì Zµj¤§§ç:—™™É¿ÿýo}ôQ4h@\\ëÖ­£oß¾ùîuÃÉ“' Î÷YkÖ¬!88˜Þ½{ãëë{ËÞIDDDDÜO™¶áèm®^½zžs§N"88˜{ï½BCC©^½:GÍ÷^AAAôìÙÿ<çNž<‰Ùl¦}ûö Î""R&e¢_MÜHYû3U&Þ&44€¸¸¸<çóÌ@ )p)žûî» Øo>..ŽªU«²qãF.^¼ˆ——εEDDJ;ooo,KIWCÊœ;0–e"<&+++Ïžõ^^^deeÝð½®^½Jbb"O<ñ•+WæÜ¹s¬Y³† *yýBwâëëKff¦´ £ÑXæ~Z_&†mÆd2å ÊYYYøøøÜð½<<|8V«•.]ºCff¦ó:GÀíØ±#žžžù>síÚµ¼þúë8p€ŒŒ ÒÓÓ9~ü8f³€S§NñÒK/±k×.©Q£‡âÍ7ßt)@Ûív¬V«óËn·ç:Ÿ’’‹/¾HLL f³™«W¯²nÝ:z÷îÍ¢E‹¸rå éééüöÛoŒ;–¬¬¬"µíõŽ=ÊÖ­[IJJ¢fÍš$$$°aÃ^ýu’““9|ø°³¾•+Wæž{î!44´À{ÆÆÆ:?X\¹r›ÍÆÑ£G™0aŸ}öYžòË–-còäÉœ?žÌÌLN:Ÿqã8uêT±¼£ˆˆˆHQ•Šð PµjU._¾ ÙáuÓ¦MÝ“¼`Á¢££ àØ±cüðÃtìØƒÁ@zz:?þø#do¾pá;wÎ÷YV«•¹sçBv/ð–-[عs'ü1íÛ·`áÂ…dffÒ¢E ¾üòK–/_΀ò „×[·n­[·v~åwŸŸ6là?ÿùÕªUàüùóŒ;–;w2tèPÈgÃmšŸ®]»2yòd¶lÙÂçŸîZrøða.]ºDHHË—/w–8p Ë—/gêÔ©Þsþüù¤§§S¥J¾ùævìØA¯^½ »·9g9@RR/½ôÛ·ogÉ’% ¬V«ó÷PDDD¤¤”šðìè96ÿß0í½{÷`0زe S§NeáÂ…˜L&È×*T 22€mÛ¶åúï=÷ÜÃ=÷Ü“ï³Îœ9CRR/¼ð~~~FÂÃÃñõõÍõüôôt¦OŸÎÔ©S9yò¤óÙÅáÿøåË—' €ððpþíÝyTTgžøÿwQPEQ«²#1*Š*Š[4ƨÝÑ;Ó鞤—L2™¤§ÏÉéÌ™Éô·Ó=“éžžîž>I&é±³LÒIÚé¸Å˜hÄ=PdEQYY¥¨‚*~ upAb>¯s8§¸u—çyîóÜû¹Ï}î-“ÉĦM›Ðh4¬\¹R™·©©i\¶éííÍêÕ«±Ûí477+A;ý.\FëìÙ³<ðÀ„„„àææÆc=@OOR–Z­–Ç{ ­VˬY³” 'ÇÐ!„Bˆ‰2)tÅѳ}Ãè÷¼gÏžAó›Íf6lØ@FF_~ù%ÝÝÝJð0èÈ+}¯íÀÍíÿ®y jØl6Uó}ðÁìÚµ‹ÚÚÚACIþ¯Fÿ²ê_–þþþÊç=Ï~~~J~ôz½².!„Bˆ‰ô•ž ))) 55___è ?®”-_¾“ÉD{{;;w¶­V˺uë†Üž···ò¹©©iÐ[#ÜÜÜ”u>ýôÓÊpñæ*ø. Ðh4Êçžž峚žéÇóꫯ°eËRRRhmmå—¿üå(Sþÿë_VŽ ú†f88ö¥ƒ‡‡Ç˜·'„Bq;MÚaV«•ÚÚZ>ýôS~ò“Ÿ@_Ï¥£ÇØ1ŒÁjµòþûï+ËÙívN:¥ô’zxxpß}÷°ÿ~èÇ0ä¶cbb”žÏ÷ߟ›7oÒÝÝM~~¾2ÁñZ»}ûö9 '¨««Sõ¾h›Í†ÕjUþÆë?åsAA555Êçá+ëxá…X¹r%žžž.çuô———ÓÛÛë´lÙ28@cc#v»>øú†áÌŸ?TyB!„˜(“²ç¹ººšåË—;M àW¿ú•ÒKÖ-[ؽ{7¯¿þ:}ô^^^444`6›ùòË/•^Ú 6°wï^e,òpC6èëÍ~ì±ÇxóÍ79}ú´ò Ýnç¹çžcáÂ…<ýôÓœ9s†šš¶lÙBDDííí466òøã3kÖ¬a·ñé§Ÿòé§Ÿ*ÿ×/ ÆÇÇÀ7x饗xï½÷¨¬¬Tµì=÷Ü@[[Ï<ó ÞÞÞdggãáá1(¸¿÷Þ{ÉËËãÃ?d×®]èt:ÒÓÓ]®÷™gžá‹/¾ ººšM›6áîïÛßþö¸¿·[!„âv™´=Ï___âããyòÉ'ùðÕ×Õ9üä'?á…^ >>žöövª««ñ÷÷çÑGuÆ1wî\"##¡ï»¹+?øÁxþù牉‰A«ÕâááÁüùóIJJ`Ú´i¼ûî»<ðÀøøøP^^Ngg' .TRœüîw¿#!!FC]]«W¯æ?ÿó?G\výúõ|ë[ßÂÇLJóçÏSWWÇ/ùKV¯^=hÞø‡ 11ƒÁ€N§¶÷xêÔ©lß¾´´4ŒF#ô]ü<ÿüó<óÌ3·˜c!„Bˆ;GcµZGÿ˜B!„_C“¶çY!„BˆÉF‚g!„B!T’àY!„B•$xB!„B% ž…B!„PI‚g!„B!T’àY!„B•$xB!„B% ž…B!„PI‚g!„B!T’àY!„B•$xB!„B% ž…B!„PI‚g!„B!T’àY!„B•$xB!„B% ž…B!„PI‚g!„B!T’àY!„B•$xB!„B% ž…B!„PI‚g!„B!TrŸèô—››Ë©S§M×ét<ýôÓ’¦¯² .päÈž}öYÜÜä:é³Ï>Ãb±°yóæ[^×öíÛILL$99y\Ò&nÍ©S§hmmeÆ ”¯L»kiiá½÷ÞcóæÍDFFNX:ÆÚ.dz=;Ld=zûí·™5kK–,¹ãÛ¾Ó¾*md¬Ôœ.]ºDVVíííx{{óíoNwGÓ)ÆnRφ7:M›¨ÆÕÑÑÁÎ;yâ‰'&dûâ«ën«;—.]¢²²’µk×NtR\2ôööªšw²çe²;räaaaÌš5k¢“2îFSÆêN¤ŽOnf³™ôôtfΜÉܹsééé‘Àù+fÒÏ÷ÜsÏD'€²²²Û~ ½4ÍD'ákçn©;eeexxxLt2†”˜˜¨zÞÉž—ÉÌf³Q^^NXXØD'å¶M=«;qløªÔñ¯ë¹©¹¹»ÝNbb"S¦L™èäˆ1˜”ÁóP>øàüýýyðÁ¦ñÅ\¼x‘þð‡¸¹¹QYYIff&MMM æÍ›Ç‚ ”ù?ÿüs<== çôéÓ´¶¶âííÍ‚ ˜3g޲Îüü|z{{yå•W¸ÿþû‰§¸¸˜ÜÜ\ZZZððð 00ÔÔTBBB\¦Ûjµ’••ÅåË—1›Í˜L&RRRˆ‡¾á*¥¥¥477£Õj‰ŒŒdÅŠ è»Å•ŸŸÏºuë8yò$õõõèõz¦OŸÎÒ¥KÑjµC–™F£¡¶¶–“'O*å‘ÀÂ… æ©ÌºpáçÎãßøÇŽ£ªªŠ„„–.]Jww7_~ù%—/_¦»»›°°0–/_Ž¿¿¿ê2iiiáË/¿¤ºº›ÍFpp0K–,!44Ôi?zyy1cÆ Ž?NSS6l ::šêêjN:Ecc#F£‘ùó综ƒ¡&ß………äææÒÑÑ¡ìëáÊ|¸ºPPP@aa!­­­xyyOJJʰ븜Á``ÆŒ¤¥¥)·ßŸ|òIΞ=Kqq1~~~<òÈ#äççsîÜ9ÚÛÛ `É’%DGG+ëmlläìÙ³ÔÕÕÑÙÙ‰¯¯/ÉÉÉÄÅŰcǸxñ"ßÿþ÷ñööVµ¯i{ý™ÍfÞzë-–/_NBB‚Ów;vìÀÛÛ›7òùçŸÓÖÖ¦äy¨º‘••å2/mmmìܹ“¿ú«¿bêÔ©Ê6.^¼Hzz:÷w§ìŸ‘Ú«#•›cŸ~ç;ß!++‹ÊÊJ´Z-3gÎdéÒ¥äääpþüy, S§NeÕªUPSS£:/•••qîÜ9šššèééaêÔ©,_¾œ©S§ÒÜÜÌž={¸yó&éé餧§ãëëËã?®¤ù‹/¾ ºº777¦OŸNZZšS§¶]¤v¹áÒ æü‘žžîTèw,ºvív»Ö¬Y£=#µ¯ÛîØ ÑhÈÌÌäâÅ‹˜ÍfIKK#<<\YÇ­´×îÆsSvv6999ƒ†z¦§§ÓÜÜ̶mÛèììä‹/¾ ªª ‹Å‚··7±±±,[¶LYF;íù!++‹‚‚>üðC6lØ@ll,ÝÝÝdff*ûÃßߟÄÄD§»<Ã÷Ä3)ƒçîîn§ÿÝÜÜÐjµÄÇÇsúôi¬V«Ó-ŽÒÒRâââpssãÚµk|üñÇÌž=›´´4nܸÁ‰'°ÛíN²´´”k×®±páB¼¼¼(..æèÑ£JRR===\½z•­[·`0¨­­åСC,[¶Œ°°0ººº¨ªªòÄÙÛÛ˾}ûhii!%%“ÉDss3~~~Ê<õõõÄÅÅB[[ÇŽC«Õ:Ýrkiiaÿþý$&&’’’B}}= †aÇUi4Ž9BJJ ÞÞÞTUU‘™™  ”‡Ú2¨££ƒýû÷ÀêÕ«•“÷'Ÿ|BKK Ë—/Çh4’Í®]»øîw¿‹^¯±L:;;ùè£ðõõeÕªUèõzΟ?ÏîÝ»yä‘G r*»K—.1{öl,X@pp07nÜ`ïÞ½„„„°aÃl6¹¹¹Ü¸qÃiY5ù.**âØ±cÌ;—3fÐÞÞÎÑ£G±X,C–ËPu 33“œœRRR ¥µµ•ŒŒ nܸ1ì8ËŒŒ rssIJJ"$$„›7oâîîÜ|ÓÓÓ±Z­,]º“ÉÀ™3gÈÎÎV.<._¾Ì'Ÿ|ÂÖ­[• ‘›7o¢ÑhHKKÃËË‹ÜÜ\:DPP~~~<øàƒ|úé§øúú*'£Ñ¨j_»2Òöú3 DFFréÒ%§à¹¥¥…†††aë§«º1T^ÚÚÚ†\«õŽÔ^G¢¶Ü>þøcHHH ¼¼œÜÜ\jjjÐét¬X±FéS§8xð =ö˜êí¥©©‰   ’’’Ðh4œ8q‚O?ý”Ç6oÞÌŸþô'–-[ÆôéÓ•Öl6óÑG)ÁiOO'Ožä³Ï>ã›ßü&€êv9ÚåFJƒšóÇ@7oÞä/ù >>>¬\¹wwwFÕ¾úîØpþüy"""X»v-v»3gÎðÉ'Ÿðýï_Ió­´×þîæs“ééétuuqÿý÷ãááASSV«Uù^;Ëù!!!€€<ÈæÍ›ñõõÅËË‹ÞÞ^>þøcZZZX²d ¾¾¾TUUqøða¬V+óæÍsÚ/mâΚtÁsoo/o¼ñ†Ó´™3g²nÝ:fΜIFFW®\Q®ŒkjjèèèPþÏÌÌ$$$„Õ«WFss3yyyÊ ¾í‘GÁ××€ððp®^½JUU¡¡¡xyy¡ÓéÐh4øøø(iijjB«Õ2wî\¥Ge¸+¾ŠŠ jjjxøá‡•ÞƒÃRÖ¯_¯| ¥¦¦†ŠŠ §yl6‹-R®@#""¨­­¥ªªjØ”ÝngéÒ¥ÄÄÄ(ùìèè //O9ø¨-³Ìf33gÎdùòåÊ´ÊÊJ®]»ÆC=Ä´iÓ äÍ7ߤ¸¸˜yóæX&¹¹¹Øl6zè!å„1mÚ4vìØAff¦r2¦oÿoܸQÉŸ#?nnnlÚ´IY>22’7ß|Ó)ýjò““Cxx8«V­R–3ìÝ»wÈ2ªîX,rssINN&%%EI———û÷ï§®®ÎåÝ ‹ÅB^^ÉÉÉ,Z´hÈívvvò­o}KÙ_V«•ììl•žšÐÐP®]»Fnn®¬GEE¥¬gõêÕüñ¤¶¶???|||Ðjµxxx8åG;ve¤í 4kÖ,>>tuu š>ð‰x___:;;G\ßÀiÓ¦)å1š2seà­ôk×®áîîî”VOOO|||¨¯¯eRSSCxxø (bbb¸víšÓ4£Ñ8èÒÐÐ0hyNçtu®&ßV«•æææAóDFFŽé ]WW‡Íf´¾èèh´Z-ÕÕÕÃ.7ÒrΜ9Néj¹àà`e_¸â0‡ë=Aå¾Vc¤íÅÄÄ Óé(--U¦•––2cÆŒaoºª·ÃPíu(£)·c‹}|| tªÛŽ“¯šcÁh9.VFÊßµk× s ZíÍ‘'5íҵ˔†‘ή8ŽE®.JÆÚ¾†3ð˜èطÕ¿Úö:ÐÝ~nɬY³ÈÉÉáèÑ£4559}§fߎ÷ù¡ººN§Î±±±X­V®_¿®L»SÇ61´I×óŒ‹F³fÍâÈ‘#X,å„êè©q4êÌÌLåöOý.®†Yh4šäp¼R&77—'NpêÔ),XÀ‚ \6˜®®.§ƒù@Ž[åååttt`³Ù† ȦYMzé;ÑôçHÕjU‚5eæÊÀž®®.zzzxíµ×œ¦÷öö*ã9G*“®®.M7 Jù8†,¸êY±X,.Â0 J~ÔÔGÙ¸*÷±ôð9¶9p}nnnèt:Ìfó¨–h`Y8Ö÷ÑG9Mïííu‡Z[[KAAX,z{{±Ùlªò3Ò¾ve´Ûsww'66–’’,X@SSMMMN½=®Œµgy8£i¯CM¹ ¬gc­{jséÒ%ZZZèîîÆn·«ZÎl6S[[Ë«¯¾:è;G›SÓ.]Q»œš4 wþp¥««KŽæ*Ϩh_£1ð¸èêœ2Öö:ÐÝ~nIjj*ÁÁÁäææòÁŲeË TµoC<Æóüàêï(“þ-·ãØ&FgRÏÃ‰åØ±c\¾|Ìf³rËÍQÉ.\èòá#W·„ÇÂh4’––ÆâÅ‹9wî™™™h4—1ètºa{ ÒÓÓ©ªªbùòå¡×ë)((P(èo¬·¨Ìf³ÓÃ"Žƒ^¯÷2Óëõèõze<_ŽåHeâéééòÀh6›ÑjµƒÆúºÚŽ«õ÷_§š|;NH®Ö5p\¾ŽmZ,¥G¾Û—V«uÈ™ã@ÜÕÕåò¡‡õñ¾ 68Ýeé?occ#»wïfÆŒ¬Y³£ÑˆV«åwÞ1?jöõ@cÝ^||<{ö졵µ•’’|||ÆõCµ­þc e{ÊhÊm,m^m^:þ<ÇŽcÉ’%,[¶ OOO®_¿Î¾}ûFܦ§§'Ó¦M#--mÐwŽñ¶jÚ¥+j—S“†áήèõú!UjÚ×x»•ö:Ð×íÜäªþÇÆÆK}}=Çg÷îÝ|ï{ßSµomu¼ÎƒÁåºÓ†»ÐwÞ¤¶1NGLL eee\¾|™°°0å*L«ÕÂõë× ô7ÒÛ ÒjµÃö*yxx””DXX˜òtó@¡¡¡X,jjj\~åʘ9s&þþþxyy8td´u¨¨¨Àh4b2™Æ½Ì"""°X,Øl¶AërŒ#•ITT×®]t°»råŠ2>m8ÁÁÁTWW;í»îîn§}¤&ß:???*++Ö_[[;bO«ºŠN§ãêÕ«NÓ+**°ÙlCæ-88FÕ+WFÌûÀíiµZZZZåÏѳ_QQAoo/kÖ¬!$$“É„Íf”?WùQ³¯R»½"""ðöö¦¬¬Œ²²2fΜ9ª²)/ŽáÀ[͵µµNÿG{K¹†Ú¼ TVVFXX .$00£Ñ8èdîx¨n`pNSS&“iPž'}5íҵ˩IÃpçWBBB\‹PÙ¾\é¼2œ[i¯®Ò7ž›ôz=V«Õ)ÿ½½½Ã¥ &-- ³ÙL{{»ª}{+çW¢¢¢\î+W®`0œÞœ#&Þ¤ìy\¸¹¹9= Ïðôôô@Bjj*{öìá³Ï>#..ÚÛÛikk#55uTépܾq<`0¸~ý:„††*ÿ×ÕÕ±téR—ë¸çž{ âÀ,Z´Hy²ß`0‡Éd¢ºº³ÙŒ››—/_¦¬¬lTéŽV«%##ƒžžüüü¸ví—.]rJïx–Ù´iÓˆŒŒdß¾}$%%@ww7ׯ_'**а°°ËdÁ‚\¸p?þ˜äädÜÝÝ9þ<ªÞh@QQ‘ò¸ÝnW:êOM¾9vì'Nœ 66–›7o’=â í]Õ___/^Ì—_~‰››¡¡¡Êk°âââ†|ã€ãNgΜ ((ˆÎÎNnÞ¼9ì9:Žääd233éìì$<<œÞÞ^ZZZÐëõÌž=“É„ÝnçêÕ«DGG+¯úxb ¤¸¸˜²²2<==™:uªª}=Úí ¤Ñhˆ‹‹£¨¨hÄ7“ŒÄU^0™Lœ={???ôz=¥¥¥ƒŽEãÑ^ÇRn£¡6/™L&®\¹BSS¾¾¾ÔÖÖröìY§y4 Hoo/$&&R\\ÌîÝ»™7oF£³ÙLMM K—.E§Ó©n—©]NMáü1Pbb"/^dï޽̟?­VKcc#111L:uÄöåÊPÇ5n¥½}ºÒë9ÖöêjüüÝxn2 øûûsá²²²())!44”ÈÈHZZZ”}[]]Maa!çΣ´´oooî¿ÿ~¥ž«Ù·c=?´µµqñâE¥Í:ÄÄÄÐÙÙI~~>………tvv²dÉæÎ;dCcµZAB!„â6úÊyB!„b¢Hð,„B!„J< !„B¡’ÏB!„B¨$Á³B!„*Ið,„B!„J< !„B¡’ÏB!„B¨$Á³B!„*Ið,„B!„J<ß%¶oßNvvö°ó\ºt‰wß}—W_}•W_}•¦¦&Þzë-jkkïX:ÚÂÝ·IDATÅí§¦.L6¼õÖ[ÔÕÕMtR„BˆaݵÁó¥K—HOOŸèd é½÷Þ£­­íŽmÏl6“žžNXX<ò?ü0F£___t:ÝK‡øê¸]uôÈ‘#\¼xÑiš»»;~~~R…BLzî€Û¥¬¬ ‰N†KMMM´´´ÜÑm677c·ÛILLdÊ”)Êô­[·ÞÑtˆ¯†ÛUGm6ååå„……9M÷ôôäá‡÷í !„ãMcµZ{':f³™ììl*++ikkÃ`0Ï’%K”yöìÙƒ‡‡7ntZöwÞ!..Ž¥K—²cÇœ¾ÿþ÷¿··7ÒÚÚŠ——ñññ¤¤¤ Õj¸páùùù¬Y³†“'OrýúuŒF#ÉÉÉÄÆÆròäI®\¹‚F£aÚ´i¬Zµ ½^¯l+77—ÒÒRš››ÑjµDFF²bÅ ¥¥¥>|˜îîneþÙ³gsß}÷PYYIff&MMM æÍ›Ç‚ œòRXXHnn.’ššJzz: $''*׬¬, °X,Ê´ 6È{ï½ÇæÍ›‰ŒŒàí·ßfåÊ•tuuqæÌn޼ɓO>Iiié˜Ê$77—ÌÌLž~úi¥|/^¼Hzz:©©©,\¸PIÓÿøGæÌ™Ã’%KhlläìÙ³ÔÕÕÑÙÙ‰¯¯/ÉÉÉÄÅÅ)ó•VN§ªûËÎÎ&''‡§Ÿ~Úizzz:ÍÍÍlÛ¶M©yyyÜÿýœB!ÆbRõ¯¯/÷Þ{¯êõ<øàƒ|úé§øúú²lÙ2ŒF#™™™äää’’Bhh(­­­dddpãÆ 6lØ ¬£¥¥…Ç“””„Á`   €#GŽPPP@HHëׯ§­­“'Ob0X±b…²l}}=qqq„„„ÐÖÖÆ±cÇÐjµ¬]»–iÓ¦±råJÒÓÓÙºu+ÞÞÞÊ­êk×®ññÇ3{ölÒÒÒ¸qã'NœÀn·+AfQQÇŽcîܹ̘1ƒöövŽ=ê”@@@dóæÍøúúâåååÄôwþüyšššX°`ƒAIßXÊdÚ´iœ:uŠºº:ÂÃøzõ*žžž”——+ùjjjÂl6+AüÍ›7Ñh4¤¥¥áååEnn.‡"((??¿aÓª¦oEkk+‡"))‰E‹QQQAff&FYYYGeÞ¼yÄÆÆÒÞÞαcÇ0›Íƒz]‡*ïá꣨ £-[©£6›½{÷Ã7¿ùMzzzhhh@¯×ãããÃæÍ›ùÓŸþIJe˘>}:nnÃÛ½{7qqq<ðÀtvvrêÔ)Ž9ÂC=¤ÌóÉ'Ÿ`±X¸ï¾û0 ”””pàÀ1ï_!„b8“*xöôôdóæÍÊÿ!!!”––RSS3ªàÙÇÇ­V‹‡‡>>>Êt‹ÅBnn.ÉÉɤ¤¤‰——û÷ï§®®Ž軽¼hÑ"¦OŸ@pp0Û·oG§Ó±jÕ*eõõõTWW;mýúõÊçÐÐPjjj¨¨¨@¯×ãå倷··Sú233 aõêÕÐ×ËÖÜÜL^^IIIh4rrrwJƒÑhdïÞ½C–‡——×Ût¥ººšï~÷»JO½ÃXÊdÊ”) ª«« Çn·SYYɼyó8{ö,‹½^Ouu5îî†ETT”²ÎÕ«WóÇ?þ‘ÚÚZ§àÙUZՔ㭰Ùl,Y²„ØØXè«Cäææ*Áhvv6QQQNU¾¾¾ìܹSuyWÕua´åq+u´½½«ÕJ||¼²/DôµMƒÁ0b=t,ë¸èêêâÔ©Sôöö¢Ñh(//çúõëlÛ¶Mi»‘‘‘´¶¶j—B!Äx˜ô šL&ºººÆe]uuuØl6î¹ç§éÑÑÑhµÚA'[GO)}½N§Ôkh2™èììv»>>>#æÁf³Q[[;(mÁÁÁ˜ÍfÚÚÚ°Z­477š'22ò–Âþbbbrc)“ˆˆ®]»}bOOóçÏÇÝÝ] ««« S†v d0ÐjµƒzU¦UM9އþåpÏ=÷ÐÕÕE[[½½½466í4Ohh(îW‡+ïþú×#µua<ËCͺüüü åàÁƒdggØ6F2mÚ4§ÿ}||°ÛíJ9Ô××£×ë•Ày¨å„Bˆñ2©zžÛÛÛÉÉÉ¡¶¶³ÙŒÝnÇb±8õ@Þ Ç ·ÿ˜Q777t:f³ÙizÿqÌCM´Úl6rss)//§££›ÍFOOê´eff’™™9è{‹Å¢–Ó¯Ñh\¦u¬†ëK™DFFròäIl6W®\!,, ƒÁ@DDåååÄÅÅQ]]í4n¶¶¶–‚‚±X,ôööb³ÙFL«šrFõôô„¾ ÖQw]•‹«òsUÞ#Õ#«Õ *êÂx–‡ÚumÙ²…ÂÂBòóó9sæ ³fÍbéÒ¥cª£®òÐÛûjܼyS)ûþ\MB!ÆÃ¤ žm6»víÂ`0ššŠŸŸéííUÞ1cJÿÿF#&“ 777Æ'Ó7Ä@Í]TÔ#µua¬å1Ö:ÚŸF£aúôé$$$(o¿q< 8^{S§NÅjµú¡Ÿšš§ÿ{{{imm—m !„øz›4Á³ÑhTêî£ƒ'N ”§M›F]]—.]Âb±ÐØØÈáǸÁKYYÕÕÕX­Vt:‹/æÌ™3äääPSSCQQ‡&..Ž   [·Éd¢ºº³ÙŒÅbáÂ… ”••9Í€F£áÌ™3ÔÕÕ)Ejj*|öÙg\¾|™ŠŠ Ο?OFF†²lbb"œ8q‚k×®qéÒ%Ž92é\"22’¢¢"ñõõ…¾‡Ñ¦L™ÂÅ‹‰ˆˆPæ5™LØív®^½ŠÍf£©©‰#GŽ è ¤¦]¥O£Ñ‘‘Ùlvz;Æ@nnn|ùå—QSSCff&%%%$%%)ó,\¸ÊÊJNž|Øå˜gWÔÔ#µua,å1Ö:ÚÒÒÂÑ£G¹|ù2 \½z•ââbåáAFC@@………TWWº­˜˜øüóϹ|ù2ÕÕÕ|ñŃ‚éÓ§Oóî»ïºpB!FkÒ ÛðööfíÚµäääPPP€Ñh$!!E‹9õ"Íž=›öövN:ÅáÇñóó#))‰K—.9­oÑ¢E´¶¶’žžŽ»»;Û¶mC§Ó1þ|´Z-ùùùdffâååŬY³X¼xñ¸äcݺuœ8q‚ÿùŸÿÁÝÝØØX6lØÀþýû•y¼¼¼X½z5gÏže×®]Ì;—   """xøá‡9}ú4‡¦··“ÉĬY³”eç΋Ýn'//óçÏ3eÊV­ZÅ™3gÆ%ý·‹#xž3gŽÓôèèhΞ=ëôF†3fÐÐÐÀÉ“'ùüóÏñ÷÷'55uÄך9¨)ÇüýýY·n§OŸ¦°°ƒÁÀ¬Y³HIIqp­_¿ž/¾ø‚ëׯc0X¶lóçÏW¾‹‹Ãl6“——ǹsçð÷÷gÙ²e>|XUÔÔ#µua,å1Ö:ª×ë1›Í;v ‹Å‚§§'ÑÑѤ¥¥)ë^³f ÇgïÞ½ò裪*W4 ßøÆ78~ü8‡B«Õ2}útRRR8uê”2ŸÉdB§Ó)¯¬B!ÆjRýHŠ“Ý… 8räÏ=÷ܘÞpòÆo°xñboKúÄÿÉÍÍåÌ™3ƒ~ôF!„¸U“f؆_%Ž·=ŒF}}=ÝÝÝ£~hQŒ^UU•”³BˆÛbÒ Ûânrá Ãh4ÒÖÖFff&~~~òâqæøEC___´Z-¥¥¥TTT°fÍš‰NšBˆ»ÏBÜ”––RZZJWWz½žÈÈH–.]ªúÁG¡N`` ttt`·ÛñóócõêÕ£úUR!„B-ó,„B!„J2æY!„B•$xB!„B% ž…B!„PI‚g!„B!T’àY!„B•$xB!„B% ž…B!„PI‚g!„B!T’àY!„B•$xB!„B% ž…B!„PI‚g!„B!TrŸè¸b6›±X,ôôôLtR„ÀÝݽ^Á`˜è¤!„bi¬VkïD'ÂÁf³ÑÞÞ.A³˜´ÜÝÝ1™LhµÚ‰NŠB!&À¤¶!³˜ìzzzhooŸèd!„b‚LšàÙl6Kà,¾zzz0›Í !„BL€I<[,–‰N‚ªI}B!¾ž&Mð,½Îâ«Dê«Bñõ4i‚g!„B!&; ž…B!„PI‚g!„B!T’àY!„B•$xB!„B¥IùóÜ·Âf³±k×.ÒÓÓ©««Ãd21mÚ4ÒÒÒxà&Ý/Ã=óÌ3\¹rFƒÉdbÆŒlÚ´‰Å‹OtòT{ÿý÷yÿý÷ â½÷Þ`ûöí:tˆmÛ¶±mÛ¶‰N¢B!Ä-»ëzžßxã Þ~ûmªªªèîîæÆäçç³wï^%pnkkcãÆüú׿¾-iëú{{{ikk#''‡—^z‰ôôôÛ’¾;åÒ¥KtttPZZ:ÑIB!„wUϳÅbáàÁƒlذÇ{Œöövòòòð÷÷Wæ;uêÔm}OïXÖ¿nÝ:~ô£Q^^οýÛ¿Q]]͇~ÈÚµko[:o·ú§âüùó$&&NtR„B!ÆÅ]<·µµ)AkBB¥Ì³oß>Þ~ûmŽ=ÊÑ£GøóŸÿŒ¿¿?ìß¿ŸË—/ÓÔÔ„¯¯/óçÏç‰'ž 00€sçÎñ /––ÆSO=Å믿NNNÏ>û,f³yØõG«Õ˦M›øÃþ@mm-V«NÀ•+Wx÷Ýw¹pá===DGG³mÛ6RSS•uØívvïÞMzz:µµµ ¢¢¢øáH\\YYY|ôÑG\¹r777fÍšÅw¾óâãã•õüä'?¡°°íÛ·STTÄÿþïÿb³Ùxï½÷èééá­·ÞâÈ‘#ØívRRR ”Ÿ_ýêW²víZžþy§õþþ÷¿çäÉ“;vŒ®®.fΜÉSO=Å=÷Ü£,áÂÞxã ÊËË å‰'žàÀœ={–çŸþ+}a!„Bˆ¯¦»*x ÄÏÏ––þë¿þ ›ÍƪU«Ðh4Ê<ÙÙÙÊgooo‚‚‚ /p(--å‹/¾À`0FUUéééTTTðÊ+¯8m¯¨¨ˆ_|‘ŠŠ e}'Ožvýj8.4’öòòr~üãcµZ Å`0P\\Ì/~ñ ~úÓŸ²téR~ó›ß(»»»;­­­œ;woooÒÓÓùío €›ÛÿÚ9{ö,ùùùüò—¿dîܹNiùïÿþorrrˆ…¾±ÌûöíSÒxôèQ¼¼¼Tçàç?ÿ9ÍÍÍxxxÐÝÝM~~>?ûÙÏxóÍ7Ñét´µµñÏÿüϘÍfjjjxùå—Ñëõ£ÚŽB!Äxº«Æ<»¹¹ñãÿwwwÚÚÚøõ¯ÍSO=Enn®2Ï¿üË¿0cÆ RRRxýõ×yýõ×ñññ`ýúõ¼øâ‹ìÚµ‹?üáüøÇ? ¤¤„ÆÆF§í555ÑÔÔÄ“O>ɯ~õ+fÏž=âú‡ÓÓÓCII û÷ï`Á‚xxxð§?ý «Õʼyóxë­·xýõ×yôÑGرcW¯^UçÇ{Œ}ûö±{÷n^~ùe°ÛíJ¯xrr2»wïfçÎÌž=›žžþð‡? JSNNÉÉÉü¿ÿ÷ÿøû¿ÿ{ººº”¡1 ,`÷îݼÿþûL™2eTûª¥¥…—_~™O>ù„¿ù›¿ ¡¡òòr>ûì3Ìf3nnnüÇüû÷ïçé§ŸV‚i!„Bˆ‰pWÏ‹/æw¿û2¡²²’_|‘¨ZÞh4’––†Ýn§¥¥…å»Á3ÀßþíßòðÃ3þ|UòP>ÿüs6nÜÈ~ô#êêêð÷÷çÙgŸU¾/((Àl6óÚk¯ñÊ+¯põêUè땦o8 }½ÜßúÖ·pssÃËË‹¤¤$***hnn`óæÍxzzâååÅÆ(++£µµÕ)]ááá¼ôÒK¤¦¦GEEV«€­[·b0˜2e ÷ßÿ¨ò¯¤+--M™~ãÆ %-³gÏfîܹh46mÚ4ên!„BˆñtW Ûpˆ‹‹ã÷¿ÿ=YYY¼úê«455±}ûvî»ï>eüðPvîÜÉþýû©¯¯§··wÄmõ'<žþõ_ÿ•ÐÐPè{ GGGôõ€—””8ÍÛÝÝÍfS_£Ñè2Ÿý 峟ŸŸÓ<¾¾¾ÊÿqqqNCNnÞ¼©|îßÛÜ}j+ŸûŰÛíÐ×3 8¥E£ÑàïïOgg稶%„B1^çþ/^¬<¨f6›]ö÷wâÄ Þ|óMêêêØ°aƒ2Ta8îîãsý±nÝ:8@BBô5vïF³üÄOpðàÁAZ­£Ñ@gg§Ò;Ü_ÿ@´­­MùìTL&Ó°ùëßóëèŸ>5ÃQ†âèö„;."„B!&Â]<ò‹_ü‚ŒŒ ÚÚÚhkkãôéÓÐ:Þváéé @]]V«‹Å‚ÕjUÞGl2™xöÙgIMMUæ¡Ö?FÃóÏ?Á`àܹsüùÏV¾s¼îíàÁƒ455)Ó”žhG/xOOþóŸ±Ûíܼy“'NPWWGtt´Òã»wï^¬V+Ê먨¨ß¥¾{öìÁjµÒÞÞ>îï¤v¼u£¨¨HÙ/YYYƒ‚éÚÚZ~ûÛß²sçNeÚÁƒùõ¯­üøLkk+¯¼ò ï¼óŽÒ³-„B1wÕ°žž222ÈÈÈôÝC=„Á`€¾¡ÙÙÙñðÃÓÓÓÃk¯½¦¼Ò®½½^x£ÑH^^îîî£zoóPëw¼­b8ÁÁÁ<ù䓼òÊ+¼ÿþûÌ›7Ù³góÄO››K]]ßûÞ÷ ¥££ƒ7nðè£ǽ÷ÞKBB‚xÿå/Áf³ðÊ+¯Â3Ï<ÃÏ~ö3222ؼy3½½½Øív4 ?üáGLŸÁ``ݺuìß¿Ÿ¬¬,¶lÙ‚ÍfsêÕëׯgÇŽX­Vž{î9¼½½éîîÆÍÍÍ)Þ¿¿¸ßwß}øùùñꫯb³Ù°Ùlüã?þ#Çç³Ï>`É’%·m¨B!î~wUÏstt4>ø áááxxx ×ë™>}:Ï>û,?øÁ”ù¶nÝÊŠ+ðõõE«Õ2}út|||¸ï¾ûx衇0™LÓÐÐÀOúS–/_>ªt µ~µ|ðA’““±Ûíüû¿ÿ;„‡‡óÚk¯±zõj¼½½©ªªÂl63oÞ<åÁ;FÃÏþszè!¦N }㟗/_®<ø¸hÑ"^zé%âããÑjµèt:æÌ™ÃË/¿Lrr²ªô=õÔSlÚ´ ooo¼¼¼X¿~=¿ùÍoœ^ x«üýýùÅ/~Att4îîîð³ŸýlÐ8ôùóçc4™={6>>>h4RRRðôôTÊåÞ{ïÅÏÏiÓ¦1niB!ÄׯjµŽüTÜ0Òxd!:;;Ù²e /¾ø¢Ó[:&Âh_Ï'„Bˆ¯¾»ªçYÜ=êëëÉÉÉq¢qüøqåsLLÌ¥L!„_gwÕ˜gq÷¨¯¯çÅ_Ä`00uêTÜÜÜ”_r\±báááD!„B| Ið,&¥   RSS¹téµµµ¸¹¹ÅêÕ«•¡B!„wšŒybŒd̳Bñõ#cž…B!„PiÒÏãõK}BÜ R_…Bˆ¯§I<ëõú‰N‚ªI}B!¾ž&Mðl0¤7O|%¸»»+¿V)„Bˆ¯—I<˜L&  Å¤æîîŽÉdšèd!„b‚Lš·môg6›±X,ôôôLtR„€¾ Y¯×K³Bñ57)ƒg!„B!&£I5lC!„BˆÉL‚g!„B!T’àY!„B•$xB!„B% ž…B!„PI‚g!„B!T’àY!„B•$xB!„B% ž…B!„PI‚g!„B!T’àY!„B•$xB!„B% ž…B!„PI‚g!„B!T’àY!„B•$xB!„B% ž…B!„PI‚g!„B!T’àY!„B•$xB!„B% ž…B!„PI‚g!„B!T’àY!„B•$xB!„B% ž…B!„PI‚g!„B!T’àY!„B•$xB!„B% ž…B!„PI‚g!„B!TúÿEãM¥½KIEND®B`‚fwupd-2.0.10/docs/device-emulation.md000066400000000000000000000214461501337203100174250ustar00rootroot00000000000000--- title: Device Emulation --- ## Introduction Using device-tests, fwupd can prevent regressions by updating and downgrading firmware on real hardware. However, much past a few dozen devices this does not scale, either by time, or because real devices need plugging in and out. We can unit test the plugin internals, but this does not actually test devices being attached, removed, and being updated. By recording the backend devices we can build a "history" what USB control, interrupt and bulk transfers were sent to, and received from the device. By dumping these we can "replay" the update without the physical hardware connected. There are some problems that make this emulation slightly harder than the naive implementation: * Devices are sometimes detached into a different "bootloader" device with a new VID:PID * Devices might be "composite" and actually be multiple logical devices in one physical device * Devices might not be 100% deterimistic, e.g. queries might be processed out-of-order For people to generate and consume emulated devices, we do need to make the process easy to understand, and also easy to use. Some key points that we think are important, is the ability to: * Dump the device of an unmodified running daemon. * Filter to multiple or single devices, to avoid storing data for unrelated parts of the system. * Load an emulated device into an unmodified running daemon. Because we do not want to modify the daemon, we think it makes sense to *load* and *save* emulation state over D-Bus. Each phase can be controlled, which makes it easy to view, and edit, the recorded emulation data. ## Tell the daemon to record a device The **emulation-tag** feature for the daemon is used to notify the daemon to record emulation data for a device. If the device you will be recording from is *hotpluggable* you will re-plug the device. If the device is *persistent* you will need to restart the daemon to get the setup events. Here is how a hotplugged device would be recorded from: # connect ColorHug2 and use the device ID to tag it fwupdmgr emulation-tag b0a78eb71f4eeea7df8fb114522556ba8ce22074 # or, using the GUID # fwupdmgr emulation-tag 2082b5e0-7a64-478a-b1b2-e3404fab6dad # remove and re-insert ColorHug2 fwupdmgr get-devices --filter emulation-tag For a persistent device: # by device ID fwupdmgr emulation-tag 02b7ba3cca857b7e1e10a2f7a6767ae5ec76a331 # or by using a GUID fwupdmgr emulation-tag 310f81b5-6fce-501e-acfb-487d10501e78 # restart the daemon fwupdmgr quit fwupdmgr get-devices **NOTE:** *If you are running in the fwupd development environment, you will need to manually restart the daemon because dbus activation doesn't work in the fwupd development environment. ## Record some data In both cases the flow to record the data is identical. You will use the device as normally and when you're done with the update you will record all events into a zip file. fwupdmgr download https://fwupd.org/downloads/170f2c19f17b7819644d3fcc7617621cc3350a04-hughski-colorhug2-2.0.6.cab fwupdmgr install 17*.cab --allow-reinstall fwupdmgr emulation-save colorhug.zip ## Test your data Now that you have data recorded you can remove your device from the system and then try to load the emulation data. You should be able to see the emulated device as well as interact with it. fwupdmgr emulation-load colorhug.zip fwupdmgr get-devices --filter emulated fwupdmgr install 17*.cab --allow-reinstall ## Using GNOME Firmware For supported devices, tagging, installing, emulation file loading and saving can be automated using GNOME Firmware 48 and newer, for example: ![GNOME Firmware emulation recording](device-emulation-gnome-firmware-record.png) ## Upload test data to LVFS Test data can be added to LVFS by visiting the `Assets` tab of the firmware release on LVFS. There is an upload button, and once uploaded a URL will be available that can be used for device tests. ![lvfs device page](device-emulation-assets.png) ## Add emulation-only data to the plugin Rather than simulating a complete firmware update, we can also simulate just enumerating the device. Normally this provides a decent amount of test coverage for the plugin, but is inferior to providing emulation of the entire firmware update process from one version to another. The advantage that the emulation-only test is that it can run as part of a quick *installed-test* on user systems without any internet access required. Creating the enumeration-only data also does not need redistributable firmware files uploaded to the LVFS. To create emulation-only data, mark the device for emulation using `fwupdmgr emulation-tag` as above, then replug the device or restart the daemon as required. Then the enumeration can be extracted into the plugin directory using: fwupdmgr emulation-save emulation.zip unzip emulation.zip mv setup.json tests/plugin-setup.json The enumeration-only emulation can be added to the device-test `.json` in an `emulation-file` section. ## Device Tests Device tests are utilized as part of continuous integration to ensure that all device updates continue to work as the software stack changes. Device tests will download a payload from the web (typically from LVFS) and follow the steps to install the firmware. This payload is specified as an `emulation-url` string parameter in the `steps` section of a specific device test. This causes the front end to load the emulation data before running the specific step. Device tests without emulation data will be skipped. For example: fwupdmgr device-emulate ../data/device-tests/hughski-colorhug2.json Decompressing… [***************************************] Waiting… [***************************************] Hughski ColorHug2: OK! Decompressing… [***************************************] Waiting… [***************************************] Hughski ColorHug2: OK! ## Pcap file conversion Emulation can also be used during the development phase of the plugin if the hardware is not available or to reduce the number of write cycles on the device and the testing time. But this requires a way to create the emulation file while fwupd does not yet support the hardware. This can be done by converting a pcap file generated using WireShark, on Windows or Linux, while performing the device firmware update process using the official update program. The saved pcap file should contain all the events of the device plugin before the update starts until it ends. Since the pcap file contains the firmware uploaded to the device, it is closely related to the firmware file and both the pcap and firmware files must be provided to the plugin developer. ### Record USB events to pcap file #### Linux setup Check if you belong to the wireshark group with: groups $USER To add yourself to the wireshark group, run the below command, then logout and login: sudo usermod -a -G wireshark $USER Depending on the distribution used, you may have to load the USBmon kernel module using: sudo modprobe usbmon You may also need to adjust the permissions with which usbmon instances are created: echo 'SUBSYSTEM=="usbmon", GROUP="wireshark", MODE="640"' | sudo tee /etc/udev/rules.d/50-accessible-usbmon.rules sudo udevadm control --reload-rules sudo udevadm trigger If USBmon is builtin, you may need to reboot. ### WireShark record of USB events Start WireShark and open *Capture→Options…* menu (or Ctrl+K). This brings up the *Capture Interfaces* window then select the USB interface to record packets: * USBPcap[x] on Windows, * usbmon[x] on Linux, usbmon0 interface can be used to capture packets on all buses. When recording firmware update that uses big packets, it may be relevant to increase the packet snaplen by double clicking on the value in the *Snaplen* column of the selected interface and changing the value. Click on the *Start* button then plugin the device to record and start the firmware update process. Once done, save the USB packets from WireShark to a pcap file. ### Convert pcap file to emulation file To convert this file, the `contrib/pcap2emulation.py` tool is used to generate a json file for each series of USB events between the "GET DESCRIPTOR DEVICE" events, limited to a set of VendorIDs (and if necessary ProductIDs). Depending on the device there should be 2 (setup.json and reload.json) or 3 (setup.json, install.json and reload.json) phase files, which are zipped in the emulation file. For example: # convert the pcap file for the CalDigit dock with VendorID and ProductID 0451:ace1 contrib/pcap2emulation.py CalDigit.pcapng /tmp/caldigit 0451:ace1 # this will generate /tmp/caldigit.zip # the new emulation file can be used for emulation fwupdmgr emulation-load /tmp/caldigit.zip fwupdmgr get-devices --filter emulated fwupd-2.0.10/docs/ds20.md000066400000000000000000000202461501337203100147400ustar00rootroot00000000000000--- title: BOS DS20 Specification --- ## Introduction When `fwupd` starts it enumerates all PCI and USB hardware and generates *Instance IDs* based on the reported vendor and product so that it can match the device to a plugin. The plugin knows how to communicate with the device (for instance using vendor-specific USB control transfers) and also knows how to parse the firmware and deliver it to the device. Before a vendor can ship a firmware on the LVFS to a machine using Linux (or ChromeOS) the fwupd package often must be updated so that it knows what plugin to use for that specific device. At this point other overridden [quirk data](https://fwupd.github.io/libfwupdplugin/class.Quirks.html) can also be set. For instance, the icon, long summary or even per-plugin flags that change device behavior. For example `colorhug.quirk`: [USB\VID_273F&PID_1001] Plugin = colorhug Flags = self-recovery Icon = colorimeter-colorhug Updating the fwupd binary package might take anywhere from a few weeks to several years due to various Linux distribution policies. Clearly this isn't good when the majority of firmware updates are distributed to address security issues. One suggestion would be to put this quirk information into the existing update metadata provided by the configured remote, but this has several problems: * The daemon needs to *enumerate* hardware that does not have updates on the main LVFS remote. * A *lot* of machines using fwupd have never connected to the internet and we still want to enumerate hardware for audit and verification purposes. * Re-enumerating all physical hardware (to get the latest quirks) when updating the metadata is not straightforward. * The VID/PID is not necessarily the information that the plugin matches to assign the firmware stream, and so this would have to be a separate facet of metadata -- which may have to include quirks too. Matching devices to plugins should be thought of as *orthogonal* to matching firmware to devices. To simplify deployment it should be possible to allow the device itself to specify the plugin to use and optionally additional quirk data. ## Specification Devices that implement a fwupd BOS DS20 descriptor can be updated without always needing to update the runtime version of fwupd for updated quirk entries, assuming the device is updatable by the existing vendor plugin. USB devices can create a new platform device capability BOS *“Binary Object Storeâ€* descriptor with `bDevCapabilityType=0x05` and a fwupd-specific UUID, specifically `010aec63-f574-52cd-9dda-2852550d94f0`. This UUID is generated type-5 SHA-1 hash of the word `fwupd` using a DNS namespace. Using this custom UUID will ensure that Microsoft Windows does not try to parse the capability descriptor as a *Microsoft OS 2.0 descriptor set*. ## Implementation Create a BOS DS20 descriptor such as: 1C 10 05 00 63 ec 0a 01 74 f5 cd 52 9d da 28 52 55 0d 94 f0 0e 09 01 00 20 00 2a 00 ├┘ ├┘ ├┘ ├┘ └─────────────┬───────────────────────────────┘ └─┬───────┘ └─┬─┘ ├┘ ├┘ │ │ │ │ └─PlatformCapabilityUUID │ wLength─┘ │ │ │ │ │ └─bReserved dwVersion─┘ bVendorCode─┘ │ │ │ └────bDevCapability bAltEnumCmd────┘ │ └───────bDescriptorType └──────────bLength The `dwVersion` encoded here is fwupd `1.9.14`, which is also the first version that with working BOS DS20 support. The BOS descriptors are sorted by the requester and can appear in any order. The descriptor with the highest `dwVersion` that is not newer than the current running daemon version is used. This means that `dwVersion` is effectively the minimum version of fwupd that should read the descriptor. This allows devices to set different quirks depending on the fwupd version, although in practice this should not be required. A suitable bVendorCode should be chosen that is not used for existing device operation. * The `dwVersion` parameter **must** be larger than `0x0001090e`, i.e. 1.9.14. * The `bAltEnumCmd` parameter **must** be zero. * The `PlatformCapabilityUUID` **must** be `010aec63-f574-52cd-9dda-2852550d94f0`. Then allow the device to reply to a USB control request with the following parameters: transfer-direction: device-to-host request-type: vendor recipient: device bRequest: {value of bVendorCode in the BOS descriptor} wValue: 0x00 wIndex: 0x07 wLength: {value of wLength in the BOS descriptor} The device should return something like: 50 6c 75 67 69 6e 3d 64 66 75 0a 49 63 6f 6e 3d 63 6f 6d 70 75 74 65 72 0a 00 00 00 00 00 00 00 ...which is the UTF-8 quirk data, e.g. Plugin=dfu Icon=computer The UTF-8 quirk data must **not** contain Windows *CRLF-style* line endings. ## Workflow To generate the fwupd DS20 descriptor save a file such as `fw-ds20.builder.xml`: 42 32 Then run `fwupdtool firmware-build fw-ds20.builder.xml fw-ds20.bin`. To generate the control transfer response, save a file such as `fw-ds20.quirk`: [USB\VID_273F&PID_1004] Plugin = dfu Icon = computer Then run `contrib/generate-ds20.py fw-ds20.quirk --bufsz 32`. The maximum buffer size is typically hardcoded for the device and may be specified in a microcontroller datasheet. ## Prior Art ### Hardcoded Class & Subclass The fwupd project already support two plugins that use the USB class code, rather than the exact instance ID with the VID/PID. For example, this DFU entry means *“match any USB device with class 0xFE (application specific) and subclass 0x01â€*: [USB\CLASS_FE&SUBCLASS_01] Plugin = dfu These kind of devices do not need any device to plugin mapping (although, they still might need a quirk if they are non-complaint in some way, for example needing `Flags = detach-for-attach`) – but in the most cases they just work. The same can be done for Fastboot devices, matching class `0xFF` (vendor specific), subclass `0x42` and protocol `0x03`, although there is the same caveat for non-compliant devices that need quirk entries like `FastbootOperationDelay = 250`: [USB\CLASS_FF&SUBCLASS_42&PROT_03] Plugin = fastboot #### Microsoft OS Descriptors 1.0 The [Microsoft OS Descriptors 1.0 Specification](https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/microsoft-os-1-0-descriptors-specification) defines a device-specific variable-length metadata block of `CompatibleID`:`SubCompatibleID` on a string index of `0xEE` where the `CompatibleID` and `SubCompatibleID` are also both hardcoded at 8 bytes. Using `FWUPDPLU` or `FWUPDFLA` as the `CompatibleID` would be acceptable, but we could not fit the plugin name (e.g. `logitech-bulkcontroller`) or the GUID (16 bytes) in an 8 byte `SubCompatibleID`. Some non-compliant devices also hang and stop working when probing this specific string index. #### Microsoft OS Descriptors 2.0 The [Microsoft OS Descriptors 2.0 Specification](https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/microsoft-os-2-0-descriptors-specification) is more useful as it defines a new device capability that can return a variable length vendor-defined section of data, using a UUID as a key. Using the `BOS` *“Binary Object Storeâ€* descriptor is only available for devices using USB specification version 2.1 and newer. The BOS descriptor is used for Wireless USB details, USB 2.0 extensions, SuperSpeed USB connection details and a Container ID. The BOS descriptor does give the OS the ability to parse the platform capability, which is `bDevCapabilityType=0x05`. For UUID `D8DD60DF-4589-4CC7-9CD2-659D9E648A9F` this is identified as a structured blob of data Microsoft Windows uses for the suspend mode of the device. Creating a new `bDevCapabilityType` would allow vendors to store a binary blob (e.g. `Plugin=foobarbaz\nFlags=QuirkValueHere\n`) but that would be out-of-specification and difficult to implement. fwupd-2.0.10/docs/env.md000066400000000000000000000062271501337203100147630ustar00rootroot00000000000000--- title: Environment Variables --- When running fwupd reads some variables from your environment and changes some behavior. This might be useful for debugging, or to make fwupd run somewhere with a non-standard filesystem layout. ## fwupdmgr and fwupdtool * `DISABLE_SSL_STRICT` disables strict SSL certificate checking, which may make downloading files work when using some antisocial corporate firewalls. * `FWUPD_CURL_VERBOSE` shows more information when downloading files * `FWUPD_SUPPORTED` overrides the `-Dsupported_build` meson option at runtime * `FWUPD_VERBOSE` is set when running `--verbose` * `FWUPD_XMLB_VERBOSE` can be set to show Xmlb silo regeneration and quirk matches * `FWUPD_DBUS_SOCKET` is used to set the socket filename if running without a dbus-daemon * `FWUPD_PROFILE` can be used to set the profile traceback threshold value in ms * `FWUPD_EFIVARS` can be set to `dummy` to emulate an EFI variable store * `FWUPD_FUZZER_RUNNING` if the firmware format is being fuzzed * `FWUPD_POLKIT_NOCHECK` if we should not check for polkit policies to be installed * standard glibc variables like `LANG` are also honored for CLI tools that are translated * libcurl respects the session proxy, e.g. `http_proxy`, `all_proxy`, `sftp_proxy` and `no_proxy` ## daemon * `FWUPD_MACHINE_KIND` can be used to override the detected machine type, e.g. `physical`, `virtual`, or `container` * `FWUPD_HOST_EMULATE` can be used to load test data from `/usr/share/fwupd/host-emulate.d`, e.g. `thinkpad-p1-no-iommu.json.gz` * `FWUPD_SYSCALL_FILTER` can be set to the name of the service manager if syscalls are being filtered, e.g. `systemd`. ## Self Tests * `CI_NETWORK` if CI is running with network access * `TPM_SERVER_RUNNING` if an emulated TPM is running * `UMOCKDEV_DIR` if set, running under umockdev Other variables, include: * `FWUPD_DELL_FAKE_SMBIOS` if set, use fake SMBIOS information for tests * `FWUPD_FORCE_TPM2` ignores a TPM 1.2 device detected in the TPM self tests * `FWUPD_REDFISH_SELF_TEST` if set, do destructive tests on the actual device BMC * `FWUPD_REDFISH_SMBIOS_DATA` use this filename to emulate a specific SMBIOS blob * `FWUPD_SOLOKEY_EMULATE` emulates a fake device for testing * `FWUPD_UEFI_TEST` used by the UEFI plugins to disable specific sanity checks during self tests * `FWUPD_MACHINE_ID` used by the tests to set a predictable hash normally loaded from `/etc/machine-id` ## File system overrides These are not fully documented here, see for details. * `CACHE_DIRECTORY` * `CONFIGURATION_DIRECTORY` * `FWUPD_ACPITABLESDIR` * `FWUPD_DATADIR` * `FWUPD_DATADIR_QUIRKS` * `FWUPD_DATADIR_VENDOR_IDS` * `FWUPD_EFIAPPDIR` * `FWUPD_FIRMWARESEARCH` * `FWUPD_HOSTDIR` looks for host OS `os-release` in this sysroot, default is / * `FWUPD_LIBDIR_PKG` * `FWUPD_LOCALSTATEDIR` * `FWUPD_LOCALSTATEDIR_QUIRKS` * `FWUPD_PROCFS` * `FWUPD_SYSCONFDIR` * `FWUPD_SYSFSDMIDIR` * `FWUPD_SYSFSDRIVERDIR` * `FWUPD_SYSFSFWATTRIBDIR` * `FWUPD_SYSFSFWDIR` * `FWUPD_SYSFSSECURITYDIR` * `FWUPD_SYSFSTPMDIR` * `FWUPD_LOCKDIR` * `FWUPD_UEFI_ESP_PATH` * `HOME` * `RUNTIME_DIRECTORY` * `SNAP` * `SNAP_USER_DATA` * `STATE_DIRECTORY` fwupd-2.0.10/docs/fwupd-remotes.d.md000066400000000000000000000076161501337203100172210ustar00rootroot00000000000000--- title: fwupd remote file format --- % fwupd-remotes.d(5) {{PACKAGE_VERSION}} | Remote File Format ## NAME **fwupd-remotes.d** — remotes used for the fwupd daemon. ## SYNOPSIS The `{{SYSCONFDIR}}/fwupd/remotes.d` and `{{LOCALSTATEDIR}}/fwupd/remotes.d` directories are used to read information about remote metadata sources. The complete description of the file format and possible parameters are documented here for reference purposes. ## FILE FORMAT The file consists of a multiple sections with optional parameters. Parameters are of the form: ```text [section] key = value ``` The file is line-based, each newline-terminated line represents either a comment, a section name or a parameter. Section and parameter names are case sensitive. Only the first equals sign in a parameter is significant. Whitespace before or after the first equals sign is discarded as is leading and trailing whitespace in a parameter value. Internal whitespace within a parameter value is retained. Any line beginning with a hash (`#`) character is ignored, as are lines containing only whitespace. The values following the equals sign in parameters are all either a string (no quotes needed), unsigned integers, or a boolean, which may be given as **true** or **false**. Case is not significant in boolean values, but is preserved in string values. ## REMOTE PARAMETERS The `[fwupd Remote]` section can contain the following parameters: **Enabled=false** If the remote should be considered when finding releases for devices. Only enabled remotes are refreshed when using `fwupdmgr refresh` and when considering what updates are available for each device. This value can be modified using `fwupdmgr enable-remote`. **Title=** The single line description to show in any UI tools. **MetadataURI=** The URL of AppStream metadata to download and use. This should have a suffix of `.xml.gz` for legacy metadata and `.xml.xz` for the more modern format. Only prefixes of `http://`, `https://` and `file://` are supported here. **FirmwareBaseURI=** The optional base URL of the cabinet archives to download. If not specified the `MetadataURI` base URL is used. Only prefixes of `http://`, `https://` and `file://` are supported here. **ApprovalRequired=false** If set to `true` then only releases allow-listed with `fwupdmgr set-approved-firmware` will show in CLI and GUI tools. **ReportURI=** The endpoint to use for sending success reports for firmware obtained from this remote, or blank to disable this feature. **AutomaticReports=false** If `true`, automatically sent success reports for firmware obtained from this remote after the firmware update has completed. **AutomaticSecurityReports=false** If `true`, automatically sent HSI platform security reports when running `fwupdmgr security`. **OrderBefore=** This remote will be ordered before any remotes listed here, using commas as the delimiter. **NOTE:** When the same firmware release is available from multiple remotes, the one with the highest priority will be used. **OrderAfter=** This remote will be ordered after any remotes listed here, using commas as the delimiter. **Username=** The username to use for BASIC authentication when downloading metadata and firmware from this remote, and for uploading success reports. **Password=** The password (although, in practice this will be a user *token*) to use for BASIC authentication when downloading both metadata and firmware from this remote, and for uploading success reports. **RefreshInterval={{FWUPD_REMOTE_CONFIG_DEFAULT_REFRESH_INTERVAL}}** The time in seconds after which the front end tools should re-download the metadata signature, or `0` to re-download every time. ## NOTES The basename of the path without the extension is used for the remote ID. For instance, the `{{SYSCONFDIR}}/fwupd/remotes.d/lvfs.conf` remote file will have ID of `lvfs`. ## SEE ALSO fwupd-2.0.10/docs/fwupd.conf.md000066400000000000000000000307201501337203100162370ustar00rootroot00000000000000--- title: fwupd.conf file format --- % fwupd.conf(5) {{PACKAGE_VERSION}} | Configuration File Format ## NAME **fwupd.conf** — configuration file for the fwupd daemon. ## SYNOPSIS The `{{SYSCONFDIR}}/fwupd/fwupd.conf` file is the main configuration file for the fwupd daemon. The complete description of the file format and possible parameters are documented here for reference purposes. ## FILE FORMAT The file consists of a multiple sections with optional parameters. Parameters are of the form: ```text [section] key = value ``` The file is line-based, each newline-terminated line represents either a comment, a section name or a parameter. Section and parameter names are case sensitive. Only the first equals sign in a parameter is significant. Whitespace before or after the first equals sign is discarded as is leading and trailing whitespace in a parameter value. Internal whitespace within a parameter value is retained. Any line beginning with a hash (`#`) character is ignored, as are lines containing only whitespace. The values following the equals sign in parameters are all either a string (no quotes needed), unsigned integers, or a boolean, which may be given as **true** or **false**. Case is not significant in boolean values, but is preserved in string values. ## DAEMON PARAMETERS The `[fwupd]` section can contain the following parameters: **DisabledDevices={{DisabledDevices}}** Allow blocking specific devices by their GUID, using semicolons as delimiter. **DisabledPlugins={{DisabledPlugins}}** Allow blocking specific plugins by name. Use **fwupdmgr get-plugins** to get the list of plugins. **ArchiveSizeMax=** Maximum archive size that can be loaded in Mb, with 25% of the total system memory as the default. **IdleTimeout={{IdleTimeout}}** Idle time in seconds to shut down the daemon, where a value of **0** specifies "never". **NOTE:** some plugins might inhibit the auto-shutdown, for instance thunderbolt. **IdleInhibitStartupThreshold={{IdleInhibitStartupThreshold}}** If the daemon takes more than this time to startup (in milliseconds) then inhibit the idle shutdown timer. A value of **0** specifies "never". **VerboseDomains={{VerboseDomains}}** Comma separated list of domains to log in verbose mode. If unset, no domains are set to verbose. If set to "*", all domains are verbose, which is the same as running the daemon with **--verbose --verbose**. **UpdateMotd={{UpdateMotd}}** Update the message of the day (MOTD) on device and metadata changes. **EnumerateAllDevices={{EnumerateAllDevices}}** For some plugins, enumerate only devices supported by metadata. **ApprovedFirmware={{ApprovedFirmware}}** A list of firmware checksums that has been approved by the site admin If unset, all firmware is approved. **BlockedFirmware={{BlockedFirmware}}** Allow blocking specific devices by their cabinet checksum, either SHA-1 or SHA-256. **UriSchemes={{UriSchemes}}** Allowed URI schemes in the preference order; failed downloads from the first scheme will be retried with the next in order until no choices remain. **IgnorePower={{IgnorePower}}** Ignore power levels of devices when running updates. **IgnoreRequirements={{IgnoreRequirements}}** Ignore some device requirements, for instance removing the generic GUID requirement of a CHID, child, parent or sibling. This is not recommended for production systems, although it may be useful for firmware development. **OnlyTrusted={{OnlyTrusted}}** Only support installing firmware signed with a trusted key. Do not set this to `false` on a production or trusted system. **ShowDevicePrivate={{ShowDevicePrivate}}** Show data such as device serial numbers which some users may consider private. **TrustedUids={{TrustedUids}}** UIDs matching these values that call the D-Bus interface should marked as trusted. **HostBkc={{HostBkc}}** Comma separated list of best known configuration IDs to be used when using `fwupdmgr sync`. This can downgrade firmware to factory versions or upgrade firmware to a supported config level. e.g. **vendor-factory-2021q1,mycompany-2023** **ReleaseDedupe={{ReleaseDedupe}}** Deduplicate duplicate releases by the archive checksum are available from more than one source. **ReleasePriority={{ReleasePriority}}** When the same version release is available from more than one source this option can be used to either prefer the local version (avoiding a potentially expensive download) or to prefer the remote version (which may have updated metadata such as release notes). The possible options are `local` or `remote` or empty to not make any adjustment to the policy, relying on the `OrderAfter` and `OrderBefore` sections in the remote. **EspLocation=** Set the preferred location used for the EFI system partition (ESP) path. This is typically used if UDisks was not able to automatically identify the location for any reason. **Manufacturer=** **ProductName=** **ProductSku=** **Family=** **EnclosureKind=** **BaseboardProduct=** **BaseboardManufacturer=** Override values for SMBIOS or Device Tree data on the local system. These are only required when the SMBIOS or Device Tree data is invalid, missing, or to simulate running on another system. Empty values should be used to populate blank entries or add values to populate specific entries. **TrustedReports={{TrustedReports}}** Vendor reports matching these expressions will have releases marked as `trusted-report`. Each *OR* section is delimited by a `;` and each *AND* section delimited by `&`, e.g. * `DistroId=chromeos` Any report uploaded from ChromeOS is trusted. * `DistroId=chromeos&RemoteId=lvfs` Any report found in the `lvfs` remote uploaded from a ChromeOS machine is trusted. * `DistroId=fedora&VendorId=19` Any report uploaded from Fedora 19 is trusted. * `DistroId=fedora&VendorId=$OEM` Any report uploaded from Fedora by the hardware OEM is trusted. * `DistroId=fedora;DistroId=rhel&DistroVersion=9` Any report uploaded from Fedora (any version) or from RHEL 9 is trusted. NOTE: a `VendorId` of `$OEM` represents the OEM vendor ID of the vendor that owns the firmware, for example, where Lenovo QA has generated a signed report for a Lenovo laptop. There are also three os-release values available, `$ID`, `$VERSION_ID` and `$VARIANT_ID`, which allow expressions like: * `DistroId=$ID` * `DistroId=$ID,DistroVersion=$VERSION_ID` * `Flags=is-upgrade,from-oem` Any flags listed here must all be matched by the report. **P2pPolicy={{P2pPolicy}}** This tells the daemon what peer-to-peer policy to use. For instance, using Passim, an optional local caching service. Using peer-to-peer data might reduce the amount of bandwidth used on your network considerably. There are three possible values: * `nothing`: Do not publish any files * `metadata`: Only publish shared metadata that is common to each machine. * `firmware`: Only publish firmware archives **after the next reboot** of the machine. At some point in the future fwupd will change the default to `metadata,firmware`. **TestDevices={{TestDevices}}** Create virtual test devices and remote for validating daemon flows. This is only intended for CI testing and development purposes. {% if plugin_uefi_capsule %} ## UEFI_CAPSULE PARAMETERS The `[uefi_capsule]` section can contain the following parameters: **EnableGrubChainLoad={{uefi_capsule_EnableGrubChainLoad}}** Configure GRUB to launch `fwupdx64.efi` instead of using other methods such as NVRAM or Capsule-On-Disk. **DisableShimForSecureBoot={{uefi_capsule_DisableShimForSecureBoot}}** The shim loader is required to chainload the fwupd EFI binary unless the `fwupd.efi` file has been self-signed manually. **RequireESPFreeSpace={{uefi_capsule_RequireESPFreeSpace}}** Amount of free space required on the ESP, for example using `32` for 32Mb. By default this is dynamically set to at least twice the size of the payload. **DisableCapsuleUpdateOnDisk={{uefi_capsule_DisableCapsuleUpdateOnDisk}}** Allow ignoring the CapsuleOnDisk support advertised by the firmware. **EnableEfiDebugging={{uefi_capsule_EnableEfiDebugging}}** Enable the low-level debugging of `fwupdx64.efi` to the `FWUPDATE_DEBUG_LOG` EFI variable. **NOTE:** enabling this option is going to fill up the NVRAM store much more quickly and should only be enabled when debugging an issue with the EFI binary. This value also has no affect when using Capsule-on-Disk as the EFI helper binary is not being used. **RebootCleanup={{uefi_capsule_RebootCleanup}}** Delete any capsule files copy to the ESP, and remove any EFI variables set for the update. **NOTE:** disabling this option is only required when debugging the flash process and normal users should not need to change this setting. **ScreenWidth={{uefi_capsule_ScreenWidth}}** Override the screen width in pixels of the EFI framebuffer as used by the UX capsule. **ScreenHeight={{uefi_capsule_ScreenHeight}}** Override the screen height in pixels of the EFI framebuffer as used by the UX capsule. {% endif %} {% if plugin_msr %} ## MSR PARAMETERS The `[msr]` section can contain the following parameter: **MinimumSmeKernelVersion={{msr_MinimumSmeKernelVersion}}** Minimum kernel version to allow probing for sme flag. This only needs to be modified by enterprise kernels that have cherry picked the feature into a kernel with an old version number. {% endif %} {% if plugin_redfish %} ## REDFISH PARAMETERS The `[redfish]` section can contain the following parameters: **Uri={{redfish_Uri}}** The URI to the Redfish service in the format `scheme://ip:port` for instance `https://192.168.0.133:443` **Username={{redfish_Username}}** The username to use when connecting to the Redfish service. **Password={{redfish_Password}}** The password to use when connecting to the Redfish service. **CACheck={{redfish_CACheck}}** Whether to verify the server certificate or not. This is turned off by default. BMCs using self-signed certificates will not work unless the plugin does not verify it against the system CAs. **IpmiDisableCreateUser={{redfish_IpmiDisableCreateUser}}** Do not use IPMI KCS to create an initial user account if no SMBIOS data. Setting this to **true** prevents creating user accounts on the BMC automatically. **ManagerResetTimeout={{redfish_ManagerResetTimeout}}** Amount of time in seconds to wait for a BMC restart. {% endif %} ## THUNDERBOLT PARAMETERS The `[thunderbolt]` section can contain the following parameters: **MinimumKernelVersion={{thunderbolt_MinimumKernelVersion}}** Minimum kernel version to allow use of this plugin. This only needs to be modified by enterprise kernels that have cherry picked the feature into a kernel with an old version number. **DelayedActivation={{thunderbolt_DelayedActivation}}** Forces delaying activation until shutdown/logout/reboot. ## DELL_KESTREL PARAMETERS The `[dell_kestrel]` section can contain the following parameters: **UpdateOnDisconnect={{dell_kestrel_UpdateOnDisconnect}}** Delaying firmware activation until the dock cable is unplugged. ## TEST PARAMETERS The `[test]` section can contain the following parameters: **AnotherWriteRequired={{test_AnotherWriteRequired}}** Do two passes of the write function. **CompositeChild={{test_CompositeChild}}** If the device should have a child device. **DecompressDelay={{test_DecompressDelay}}** Delay in milliseconds to use when decompressing the test device. **NeedsActivation={{test_NeedsActivation}}** If the device needs activating before deploying the update. **NeedsReboot={{test_NeedsReboot}}** If the device needs a reboot before deploying the update. **RegistrationSupported={{RegistrationSupported}}** If the device should register with other plugins. **RequestDelay={{test_RequestDelay}}** Delay in milliseconds to use when requesting user input from the user. **RequestSupported={{test_RequestSupported}}** If the device interactive request is supported. **VerifyDelay={{test_DecompressDelay}}** Delay in milliseconds to use when verifying the test device. **WriteDelay={{test_DecompressDelay}}** Delay in milliseconds to use when writing the test device. **WriteSupported={{test_Supported}}** If the device write is supported. If unsupported the device write will not start. ## NOTES `{{SYSCONFDIR}}/fwupd/fwupd.conf` may contain either hardcoded or autogenerated credentials and must only be readable by the user that is running the fwupd process, which is typically `root`. ## SEE ALSO fwupd-2.0.10/docs/fwupd.toml.in000066400000000000000000000027701501337203100162770ustar00rootroot00000000000000[library] version = "@version@" browse_url = "https://github.com/fwupd/fwupd" repository_url = "https://github.com/fwupd/fwupd.git" website_url = "https://www.fwupd.org" authors = "fwupd Development Team" logo_url = "org.freedesktop.fwupd.svg" license = "LGPL-2.1-or-later" description = "Functionality exported by libfwupd for client applications" dependencies = [ "GObject-2.0", "Gio-2.0", "Json-1.0" ] devhelp = true search_index = true [dependencies."GObject-2.0"] name = "GObject" description = "The base type system library" docs_url = "https://docs.gtk.org/gobject/" [dependencies."Gio-2.0"] name = "Gio" description = "A modern, easy-to-use VFS API" docs_url = "https://docs.gtk.org/gio/" [dependencies."Json-1.0"] name = "Json" description = "API for efficient parsing and writing of JSON (JavaScript Object Notation) streams" docs_url = "https://gnome.pages.gitlab.gnome.org/json-glib/" [theme] name = "basic" show_index_summary = true show_class_hierarchy = true [source-location] base_url = "https://github.com/fwupd/fwupd/blob/@version@/" [extra] content_images = [ "../data/org.freedesktop.fwupd.svg", ] urlmap_file = "urlmap_fwupd.js" [[object]] name = "build_user_agent_system" hidden = true [[object]] name = "hash_kv_to_variant" hidden = true [[object]] name = "variant_to_hash_kv" hidden = true [[object]] name = "input_stream_read_bytes_async" hidden = true [[object]] name = "input_stream_read_bytes_finish" hidden = true fwupd-2.0.10/docs/fwupdplugin.toml.in000066400000000000000000000041511501337203100175110ustar00rootroot00000000000000[library] version = "@version@" browse_url = "https://github.com/fwupd/fwupd" repository_url = "https://github.com/fwupd/fwupd.git" website_url = "https://www.fwupd.org" authors = "fwupd Development Team" logo_url = "org.freedesktop.fwupd.svg" license = "LGPL-2.1-or-later" description = "Functionality available to fwupd plugins" dependencies = [ "GObject-2.0", "Gio-2.0", "Fwupd-2.0", "Xmlb-2.0", ] devhelp = true search_index = true [dependencies."GObject-2.0"] name = "GObject" description = "The base type system library" docs_url = "https://developer.gnome.org/gobject/stable/" [dependencies."Gio-2.0"] name = "Gio" description = "A modern, easy-to-use VFS API" docs_url = "https://developer.gnome.org/gio/stable/" [dependencies."Fwupd-2.0"] name = "Fwupd" description = "Firmware update daemon client library" docs_url = "../libfwupd/index.html" [theme] name = "basic" show_index_summary = true show_class_hierarchy = true [source-location] base_url = "https://github.com/fwupd/fwupd/blob/@version@/" [extra] content_files = [ "env.md", "building.md", "tutorial.md", "hsi.md", "uefi-db.md", "device-emulation.md", "ds20.md", "hwids.md", "bios-settings.md", "best-known-configuration.md", "only-trusted.md", "supermicro-license.md", @man_md@, @plugin_readme_outputs@, ] content_images = [ "architecture-plan.svg", "../data/org.freedesktop.fwupd.svg", "debug_attached.png", "debug_breakpoint.png", "debug_tool_selector.png", "debug_task.png", "test_task.png", ] urlmap_file = "urlmap_fwupdplugin.js" [[object]] name = "Device" [[object.method]] name = "incorporate_from_component" hidden = true [[object]] name = "Cabinet" [[object.method]] name = "set_jcat_context" hidden = true [[object.method]] name = "get_silo" hidden = true [[object.function]] name = "common_cab_build_silo" hidden = true [[object]] name = "Fmap" hidden = true [[object]] name = "FmapArea" hidden = true [[object]] name = "IhexFirmwareRecord" hidden = true [[object]] name = "SrecFirmwareRecord" hidden = true fwupd-2.0.10/docs/generate-hsi-spec.py000077500000000000000000000072261501337203100175310ustar00rootroot00000000000000#!/usr/bin/env python3 # pylint: disable=invalid-name,missing-docstring # # Copyright 2022 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later import argparse import sys import json from typing import List if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( "filename_src", action="store", type=str, help="markdown source" ) parser.add_argument( "filename_dst", action="store", type=str, help="markdown destination" ) parser.add_argument("json_attrs", nargs="+", help="JSON attributes") args = parser.parse_args() with open(args.filename_src, "rb") as f: template = f.read() txt: List[str] = [] for fn in sorted(args.json_attrs): try: with open(fn, "rb") as f: item = json.loads(f.read()) except json.decoder.JSONDecodeError as e: print(f"failed to parse {fn}: {str(e)}") sys.exit(1) if "id" not in item: print(f"skipping {fn} as no id") continue txt += [f""] if "deprecated-ids" in item: for deprecated_id in item["deprecated-ids"]: txt += [f''] if "name" in item: txt += [f"### [{item['name']}](#{item['id']})"] if "description" in item: for para in item["description"]: txt += [para] if "failure-impact" in item: txt += ["**Impact:**"] for para in item["failure-impact"]: txt += [para] if "failure-results" in item and "success-results" in item: txt += ["**Possible results:**"] tmp: List[str] = [] for value, desc in item["failure-results"].items(): tmp += [f"- `{value}`: {desc} (failure)"] for value, desc in item["success-results"].items(): tmp += [f"- `{value}`: {desc} (success)"] txt += ["\n".join(tmp)] if "hsi-level" in item and "fwupd-version" in item: txt += [ "A test success result is needed to meet HSI-{} on " "systems that run this test. *[v{}]*".format( item["hsi-level"], item["fwupd-version"] ) ] if "resolution" in item: txt += [f"**Resolution:** {item['resolution']}"] if "issues" in item: txt += ["**Issues:**"] tmp: List[str] = [] for issue in item["issues"]: if issue.startswith("CVE-"): tmp += [f"- [{issue}](https://nvd.nist.gov/vuln/detail/{issue})"] else: tmp += [f"- {issue}"] txt += ["\n".join(tmp)] if "references" in item: txt += ["**References:**"] tmp: List[str] = [] for url, title in item["references"].items(): tmp += [f"- [{title}]({url})"] txt += ["\n".join(tmp)] if "requires" in item: txt += ["**Hardware requirements:**"] if "CPUID\\VID_GenuineIntel" in item["requires"]: txt += ["This attribute will only be available when using Intel CPUs."] elif "CPUID\\VID_AuthenticAMD" in item["requires"]: txt += ["This attribute will only be available when using AMD CPUs."] if "more-information" in item: txt += ["**More information:**"] for para in item["more-information"]: txt += [para] with open(args.filename_dst, "wb") as f: f.write(template.decode().replace("{{tests}}", "\n\n".join(txt)).encode()) fwupd-2.0.10/docs/generate-oval.py000066400000000000000000000133241501337203100167500ustar00rootroot00000000000000#!/usr/bin/env python3 # pylint: disable=invalid-name,missing-docstring # # Copyright 2022 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later import argparse import datetime import sys import json from typing import List import xml.etree.ElementTree as ET if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("-v", "--version", type=str, help="fwupd version") parser.add_argument("-s", "--schema-version", type=str, help="schema version") parser.add_argument( "filename_dst", action="store", type=str, help="XML destination" ) parser.add_argument("json_attrs", nargs="+", help="JSON attributes") args = parser.parse_args() # parse JSON items: List[str] = [] for fn in sorted(args.json_attrs): try: with open(fn, "rb") as f: item = json.loads(f.read()) except json.decoder.JSONDecodeError as e: print(f"failed to parse {fn}: {str(e)}") sys.exit(1) for tag in ["id", "name", "failure-results"]: if tag not in item: print("skipping {} as no {}".format(fn), tag) continue items.append(item) oval_definitions = ET.Element("oval_definitions") oval_definitions.set("xmlns", "http://oval.mitre.org/XMLSchema/oval-definitions-5") oval_definitions.set("xmlns:oval", "http://oval.mitre.org/XMLSchema/oval-common-5") oval_definitions.set( "xmlns:unix-def", "http://oval.mitre.org/XMLSchema/oval-definitions-5#unix" ) oval_definitions.set( "xmlns:red-def", "http://oval.mitre.org/XMLSchema/oval-definitions-5#linux" ) oval_definitions.set( "xmlns:ind-def", "http://oval.mitre.org/XMLSchema/oval-definitions-5#independent", ) oval_definitions.set("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") oval_definitions.set( "xsi:schemaLocation", "http://oval.mitre.org/XMLSchema/oval-common-5 oval-common-schema.xsd " "http://oval.mitre.org/XMLSchema/oval-definitions-5 oval-definitions-schema.xsd " "http://oval.mitre.org/XMLSchema/oval-definitions-5#unix unix-definitions-schema.xsd " "http://oval.mitre.org/XMLSchema/oval-definitions-5#linux linux-definitions-schema.xsd", ) generator = ET.SubElement(oval_definitions, "generator") for key, value in { "oval:product_name": "fwupd", "oval:product_version": args.version, "oval:schema_version": args.schema_version, "oval:timestamp": datetime.datetime.now().isoformat(), }.items(): oval = ET.SubElement(generator, key) oval.text = value definitions = ET.SubElement(oval_definitions, "definitions") for item in items: definition = ET.SubElement(definitions, "definition") definition.set("class", "patch") definition.set("id", f"oval:{item['id']}:def:1") definition.set("version", "1") metadata = ET.SubElement(definition, "metadata") ET.SubElement(metadata, "title").text = item["name"] affected = ET.SubElement(metadata, "affected") affected.set("family", "unix") ET.SubElement(affected, "platform").text = "All" # is this valid? if "issues" in item: for issue in item["issues"]: reference = ET.SubElement(metadata, "reference") reference.set("ref_id", issue) if issue.startswith("CVE-"): reference.set( "ref_url", f"https://nvd.nist.gov/vuln/detail/{issue}" ) reference.set("source", "CVE") if "description" in item: ET.SubElement(metadata, "description").text = "\n".join(item["description"]) criteria = ET.SubElement(definition, "criteria") criteria.set("operator", "OR") criterion = ET.SubElement(criteria, "criterion") criterion.set("comment", item["name"]) criterion.set("test_ref", f"oval:{item['id']}:tst:1") tests = ET.SubElement(oval_definitions, "tests") for item in items: red_def = ET.SubElement(tests, "red-def:fwupdsecattr_test") red_def.set("check", "at least one") red_def.set("comment", item["name"]) red_def.set("id", f"oval:{item['id']}:tst:1") red_def.set("version", "1") red_def_object = ET.SubElement(red_def, "red-def:object") red_def_object.set("object_ref", f"oval:{item['id']}:obj:1") red_def_state = ET.SubElement(red_def, "red-def:state") red_def_state.set("state_ref", f"oval:{item['id']}:ste:1") objects = ET.SubElement(oval_definitions, "objects") for item in items: red_def = ET.SubElement(objects, "red-def:fwupdsecattr_object") red_def.set("id", f"oval:{item['id']}:obj:1") red_def.set("version", "1") red_def_stream_id = ET.SubElement(red_def, "red-def:stream-id") red_def_stream_id.set("datatype", "string") red_def_stream_id.text = format(item["id"]) states = ET.SubElement(oval_definitions, "states") for item in items: red_def = ET.SubElement(states, "red-def:fwupdsecattr_state") red_def.set("id", f"oval:{item['id']}:ste:1") red_def.set("version", "1") red_def_security_attr = ET.SubElement(red_def, "red-def:security-attr") red_def_security_attr.set("datatype", "string") red_def_security_attr.set("operation", "pattern match") red_def_security_attr.text = "|".join( [value for value, _ in item["failure-results"].items()] ) ET.indent(oval_definitions, space=" ", level=0) with open(args.filename_dst, "wb") as f: f.write(ET.tostring(oval_definitions, encoding="utf-8", xml_declaration=True)) fwupd-2.0.10/docs/hsi-tests.d/000077500000000000000000000000001501337203100160075ustar00rootroot00000000000000fwupd-2.0.10/docs/hsi-tests.d/meson.build000066400000000000000000000031731501337203100201550ustar00rootroot00000000000000hsi_test_jsons = files([ 'org.fwupd.hsi.Amd.RollbackProtection.json', 'org.fwupd.hsi.Amd.SpiReplayProtection.json', 'org.fwupd.hsi.Amd.SpiWriteProtection.json', 'org.fwupd.hsi.EncryptedRam.json', 'org.fwupd.hsi.IntelBootguard.Acm.json', 'org.fwupd.hsi.IntelBootguard.Enabled.json', 'org.fwupd.hsi.IntelBootguard.Otp.json', 'org.fwupd.hsi.IntelBootguard.Policy.json', 'org.fwupd.hsi.IntelBootguard.Verified.json', 'org.fwupd.hsi.IntelGds.json', 'org.fwupd.hsi.Cet.Enabled.json', 'org.fwupd.hsi.Cet.Active.json', 'org.fwupd.hsi.Smap.json', 'org.fwupd.hsi.Iommu.json', 'org.fwupd.hsi.Kernel.Lockdown.json', 'org.fwupd.hsi.Kernel.Tainted.json', 'org.fwupd.hsi.Mei.KeyManifest.json', 'org.fwupd.hsi.Mei.ManufacturingMode.json', 'org.fwupd.hsi.Mei.OverrideStrap.json', 'org.fwupd.hsi.Mei.Version.json', 'org.fwupd.hsi.PlatformDebugEnabled.json', 'org.fwupd.hsi.PlatformDebugLocked.json', 'org.fwupd.hsi.PlatformFused.json', 'org.fwupd.hsi.PrebootDma.json', 'org.fwupd.hsi.Spi.Bioswe.json', 'org.fwupd.hsi.Spi.Ble.json', 'org.fwupd.hsi.Spi.Descriptor.json', 'org.fwupd.hsi.Spi.SmmBwp.json', 'org.fwupd.hsi.SupportedCpu.json', 'org.fwupd.hsi.SuspendToIdle.json', 'org.fwupd.hsi.SuspendToRam.json', 'org.fwupd.hsi.Tpm.EmptyPcr.json', 'org.fwupd.hsi.Tpm.ReconstructionPcr0.json', 'org.fwupd.hsi.Tpm.Version20.json', 'org.fwupd.hsi.Uefi.BootserviceVars.json', 'org.fwupd.hsi.Uefi.MemoryProtection.json', 'org.fwupd.hsi.Uefi.Pk.json', 'org.fwupd.hsi.Uefi.Db.json', 'org.fwupd.hsi.Uefi.SecureBoot.json', 'org.fwupd.hsi.Bios.RollbackProtection.json', 'org.fwupd.hsi.Amd.SmmLocked.json', ]) fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Amd.RollbackProtection.json000066400000000000000000000031711501337203100257000ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Amd.RollbackProtection", "name": "AMD Secure Processor Rollback protection", "description": [ "AMD SOCs include the ability to prevent a rollback attack by a rollback protection feature on the secure processor.", "This feature prevents an attacker from loading an older firmware onto the part after a security vulnerability has been fixed." ], "more-information": [ "This particular check is not for the Microsoft Pluton Security processor which is present on some chips.", "End users are not able to directly modify rollback protection, this is controlled by the manufacturer.", "On Lenovo systems it has been reported that if this is disabled it may potentially be enabled by loading 'OS Optimized Defaults' in BIOS setup." ], "failure-impact": [ "SOCs without this feature may be attacked by an attacker installing an older firmware that takes advantage of a well-known vulnerability." ], "failure-results": { "not-enabled": "rollback protection disabled" }, "success-results": { "enabled": "rollback protection enabled" }, "hsi-level": 4, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "references": { "https://www.psacertified.org/blog/anti-rollback-explained/": "Rollback protection", "https://www.amd.com/en/technologies/pro-security": "AMD Secure Processor", "https://forums.lenovo.com/t5/Fedora/AMD-Rollback-protection-not-detected-by-fwupd-on-T14-G3-AMD/m-p/5182708?page=1#5810366": "Loading OS Optimized Defaults on Lenovo systems" }, "requires": [ "CPUID\\VID_AuthenticAMD" ], "fwupd-version": "1.8.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Amd.SmmLocked.json000066400000000000000000000016771501337203100237670ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Amd.SmmLocked", "name": "SMM locked down", "description": [ "Disable SMM_BASE from being saved to and restored from the SMM save state area.", "Code in the ASeg and TSeg range and the SMM registers are Read-only and SMI interrupts are not intercepted in SVM.", "This check also ensures that the system is not affected by the Sinkclose vulnerability." ], "failure-impact": [ "An attacker with an existing unrelated vulnerability can the SMM save state area to exfiltrate data." ], "failure-results": { "not-locked": "SMM is not locked down", "not-valid": "Vulnerable to Sinkclose" }, "success-results": { "locked": "SMM is locked down" }, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "references": { "https://www.amd.com/en/resources/product-security/bulletin/amd-sb-7014.html": "AMD Sinkclose" }, "hsi-level": 1, "fwupd-version": "2.0.2" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Amd.SpiReplayProtection.json000066400000000000000000000012041501337203100260520ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Amd.SpiReplayProtection", "name": "AMD SPI Replay protections", "description": [ "SOCs may include support for replay-protected monotonic counters to prevent replay attacks." ], "failure-impact": [ "SOCs without this feature may be attacked by an attacker modifying the SPI." ], "failure-results": { "not-enabled": "SPI protections disabled" }, "success-results": { "enabled": "SPI protections enabled" }, "hsi-level": 3, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "requires": [ "CPUID\\VID_AuthenticAMD" ], "fwupd-version": "1.8.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Amd.SpiWriteProtection.json000066400000000000000000000012011501337203100257050ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Amd.SpiWriteProtection", "name": "AMD SPI Write protections", "description": [ "SOCs may enforce control of the SPI bus to prevent writes other than by verified entities." ], "failure-impact": [ "SOCs without this feature may be attacked by an attacker modifying the SPI." ], "failure-results": { "not-enabled": "SPI protections disabled" }, "success-results": { "enabled": "SPI protections enabled" }, "hsi-level": 2, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "requires": [ "CPUID\\VID_AuthenticAMD" ], "fwupd-version": "1.8.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Bios.CapsuleUpdates.json000066400000000000000000000011071501337203100252120ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Bios.CapsuleUpdates", "name": "BIOS Capsule updates", "description": [ "Some OEMs offer a switch in the BIOS to allow capsule updates to be enabled." ], "failure-impact": [ "When this switch is present but disabled, BIOS firmware updates for Linux may not work." ], "failure-results": { "not-enabled": "BIOS capsule updates disabled" }, "success-results": { "enabled": "BIOS capsule updates enabled" }, "resolution": "Turn on 'firmware updates' in the UEFI firmware setup.", "hsi-level": 1, "fwupd-version": "1.9.6" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Bios.RollbackProtection.json000066400000000000000000000015371501337203100260770ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Bios.RollbackProtection", "name": "BIOS Firmware Rollback protection", "description": [ "Some OEMs include an optional firmware protection feature in their BIOS that would prevent installation of older firmware that may have security vulnerabilities." ], "failure-impact": [ "Firmware without this feature enabled may be attacked by an attacker installing an older firmware that takes advantage of a well-known vulnerability." ], "failure-results": { "not-enabled": "rollback protection disabled" }, "success-results": { "enabled": "rollback protection enabled" }, "hsi-level": 2, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "references": { "https://www.psacertified.org/blog/anti-rollback-explained/": "Rollback protection" }, "fwupd-version": "1.8.8" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Cet.Active.json000066400000000000000000000016341501337203100233270ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Cet.Active", "deprecated-ids": [ "org.fwupd.hsi.IntelCet.Active" ], "name": "CET Utilized by OS", "description": [ "Control enforcement technology prevents exploits from hijacking the control-flow transfer instructions for both forward-edge (indirect call/jmp) and back-edge transfer (ret)." ], "failure-impact": [ "A local or physical attacker with an existing unrelated vulnerability can use a ROP gadget to run arbitrary code." ], "failure-results": { "not-supported": "CET not being used by the host" }, "success-results": { "supported": "CET being used" }, "hsi-level": 3, "resolution": "Update your Linux distribution, which may now support CET.", "references": { "https://software.intel.com/sites/default/files/managed/4d/2a/control-flow-enforcement-technology-preview.pdf": "Intel CET Technology Preview" }, "fwupd-version": "2.0.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Cet.Enabled.json000066400000000000000000000020121501337203100234350ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Cet.Enabled", "deprecated-ids": [ "org.fwupd.hsi.IntelCet.Enabled" ], "name": "CET: Available", "description": [ "Control enforcement technology prevents exploits from hijacking the control-flow transfer instructions for both forward-edge (indirect call/jmp) and back-edge transfer (ret)." ], "failure-impact": [ "A local or physical attacker with an existing unrelated vulnerability can use a reliable and well-known method to run arbitrary code." ], "failure-results": { "not-supported": "CET not supported" }, "success-results": { "enabled": "CET feature enabled by the platform", "supported": "CET feature support by the platform" }, "hsi-level": 3, "resolution": "Check your CPU supports CET, and ensure the feature is enabled in the UEFI setup screen.", "references": { "https://software.intel.com/sites/default/files/managed/4d/2a/control-flow-enforcement-technology-preview.pdf": "Intel CET Technology Preview" }, "fwupd-version": "2.0.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.EncryptedRam.json000066400000000000000000000024631501337203100240000ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.EncryptedRam", "name": "DRAM memory encryption", "description": [ "TME (Intel) or SME (AMD) is used by the hardware on supported SOCs to encrypt all data on external memory buses.", "It mitigates against an attacker being able to capture memory data while the system is running or to capture memory by removing a DRAM chip.", "This encryption may be activated by either transparently via firmware configuration or by code running in the Linux kernel." ], "failure-impact": [ "A local attacker can either extract unencrypted content by attaching debug probes on the DIMM modules, or by removing them and inserting them into a computer with a modified DRAM controller." ], "failure-results": { "not-encrypted": "detected but disabled", "not-supported": "not available" }, "success-results": { "encrypted": "detected and enabled" }, "hsi-level": 4, "resolution": "Ensure that the motherboard and CPU support this feature, and ensure it is enabled in the UEFI setup screen.", "references": { "https://software.intel.com/content/www/us/en/develop/blogs/intel-releases-new-technology-specification-for-memory-encryption.html": "Intel TME Press Release", "https://en.wikichip.org/wiki/x86/sme": "WikiChip SME Overview" }, "fwupd-version": "1.5.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.IntelBootguard.Acm.json000066400000000000000000000015401501337203100250170ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.IntelBootguard.Acm", "name": "Intel BootGuard: ACM", "description": [ "BootGuard is a processor feature that prevents the machine from running firmware images not released by the system manufacturer.", "It forms a root-of-trust by fusing in cryptographic keys into the processor itself that are used to verify the Authenticated Code Modules found in the SPI flash." ], "failure-impact": [ "When BootGuard is not set up correctly then the chain-of-trust between the CPU and the bootloader can not be verified." ], "failure-results": { "not-valid": "boot is not verified" }, "success-results": { "valid": "ACM protected" }, "hsi-level": 2, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "requires": [ "CPUID\\VID_GenuineIntel" ], "fwupd-version": "1.5.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.IntelBootguard.Enabled.json000066400000000000000000000023241501337203100256520ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.IntelBootguard.Enabled", "deprecated-ids": [ "org.fwupd.hsi.Kernel.IntelBootguard" ], "name": "Intel BootGuard: Enabled", "description": [ "BootGuard is a processor feature that prevents the machine from running firmware images not released by the system manufacturer.", "It forms a root-of-trust by fusing in cryptographic keys into the processor itself that are used to verify the Authenticated Code Modules found in the SPI flash." ], "failure-impact": [ "When BootGuard is not set up correctly then the chain-of-trust between the CPU and the bootloader can not be verified.", "This would allow subverting the Secure Boot protection which gives the attacker full access to your hardware." ], "failure-results": { "not-enabled": "not detected, or detected but not enabled" }, "success-results": { "enabled": "detected and enabled" }, "hsi-level": 2, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "references": { "https://github.com/coreboot/coreboot/blob/master/src/soc/intel/jasperlake/include/soc/me.h": "Coreboot documentation" }, "requires": [ "CPUID\\VID_GenuineIntel" ], "fwupd-version": "1.5.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.IntelBootguard.Otp.json000066400000000000000000000017221501337203100250630ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.IntelBootguard.Otp", "name": "Intel BootGuard: OTP", "description": [ "BootGuard is a processor feature that prevents the machine from running firmware images not released by the system manufacturer.", "It forms a root-of-trust by fusing in cryptographic keys into the processor itself that are used to verify the Authenticated Code Modules found in the SPI flash." ], "failure-impact": [ "When BootGuard is not set up correctly then the chain-of-trust between the CPU and the bootloader can not be verified.", "This would allow subverting the Secure Boot protection which gives the attacker full access to your hardware." ], "failure-results": { "not-valid": "SOC is not locked" }, "success-results": { "valid": "SOC is locked" }, "hsi-level": 2, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "requires": [ "CPUID\\VID_GenuineIntel" ], "fwupd-version": "1.5.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.IntelBootguard.Policy.json000066400000000000000000000016301501337203100255560ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.IntelBootguard.Policy", "name": "Intel BootGuard: Policy", "description": [ "BootGuard is a processor feature that prevents the machine from running firmware images not released by the system manufacturer.", "It forms a root-of-trust by fusing in cryptographic keys into the processor itself that are used to verify the Authenticated Code Modules found in the SPI flash." ], "failure-impact": [ "The attacker can invalidate the chain of trust (subverting Secure Boot), and the user would get just a console warning and then continue to boot." ], "failure-results": { "not-valid": "policy is invalid" }, "success-results": { "valid": "error enforce policy is set to shutdown" }, "hsi-level": 3, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "requires": [ "CPUID\\VID_GenuineIntel" ], "fwupd-version": "1.5.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.IntelBootguard.Verified.json000066400000000000000000000017471501337203100260650ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.IntelBootguard.Verified", "name": "Intel BootGuard: Verified", "description": [ "BootGuard is a processor feature that prevents the machine from running firmware images not released by the system manufacturer.", "It forms a root-of-trust by fusing in cryptographic keys into the processor itself that are used to verify the Authenticated Code Modules found in the SPI flash." ], "failure-impact": [ "When BootGuard is not set up correctly then the chain-of-trust between the CPU and the bootloader can not be verified.", "This would allow subverting the Secure Boot protection which gives the attacker full access to your hardware." ], "failure-results": { "not-valid": "boot is not verified" }, "success-results": { "success": "verified boot chain" }, "hsi-level": 2, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "requires": [ "CPUID\\VID_GenuineIntel" ], "fwupd-version": "1.5.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.IntelGds.json000066400000000000000000000017501501337203100231120ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.IntelGds", "name": "Intel GDS Mitigation", "description": [ "CPU Microcode must be updated to mitigate against an information-disclosure security issue called Gather Data Sampling." ], "failure-impact": [ "Firmware without this feature enabled allow an attacker to exfiltrate secrets by running a malicious program or script." ], "failure-results": { "not-valid": "microcode is not new enough to support required mitigations", "not-enabled": "mitigation is not enabled", "not-locked": "mitigation is not locked" }, "success-results": { "enabled": "microcode mitigation is supported, enabled and locked" }, "hsi-level": 2, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "references": { "https://www.intel.com/content/www/us/en/developer/articles/technical/software-security-guidance/technical-documentation/gather-data-sampling.html": "Gather Data Sampling" }, "fwupd-version": "1.9.4" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Iommu.json000066400000000000000000000017321501337203100224670ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Iommu", "name": "DMA protection", "description": [ "The IOMMU on modern systems is used to mitigate against DMA attacks.", "All I/O for devices capable of DMA is mapped into a private virtual memory region.", "Common implementations are Intel VT-d and AMD-Vi." ], "failure-impact": [ "An attacker with inexpensive PCIe development hardware can write to system RAM from the ThunderBolt or Firewire ports which can lead to privilege escalation." ], "failure-results": { "not-found": "IOMMU hardware was not detected" }, "success-results": { "enabled": "IOMMU hardware detected and enabled" }, "hsi-level": 2, "resolution": "If available, turn on IOMMU in the system BIOS. You may also have to use additional kernel boot parameters, for example `iommu=force`.", "references": { "https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit": "IOMMU Wikipedia Page" }, "fwupd-version": "1.5.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Kernel.Lockdown.json000066400000000000000000000015671501337203100244060ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Kernel.Lockdown", "name": "Kernel Lockdown", "description": [ "Kernel lockdown is an important mechanism to limit what hardware actions userspace programs can perform.", "Turning on this feature means that often-used mechanisms like /dev/mem used to raise privileges or exfiltrate data are no longer available." ], "failure-impact": [ "An unlocked kernel can be easily abused by a malicious userspace program running as root, which can include replacing system firmware." ], "failure-results": { "not-valid": "could not read lockdown status, perhaps from an old kernel", "not-enabled": "lockdown is set to `none`" }, "success-results": { "enabled": "lockdown is set to either `integrity` or `confidentiality`." }, "resolution": "Ensure Secure Boot is enabled in the UEFI firmware setup.", "fwupd-version": "1.5.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Kernel.Tainted.json000066400000000000000000000014101501337203100242010ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Kernel.Tainted", "name": "Kernel Tainted", "description": [ "When calculating the HSI value fwupd has to ask the Linux Kernel for information.", "If the kernel has been tainted by overriding a firmware table or by loading a proprietary module then we cannot trust the data it reports." ], "failure-impact": [ "Using a tainted kernel means that values obtained from the kernel cannot be trusted." ], "failure-results": { "not-valid": "could not detect kernel taint status", "tainted": "the kernel is untrusted, perhaps because a proprietary module was loaded" }, "success-results": { "not-tainted": "the kernel is trusted" }, "resolution": "Remove any non-free kernel modules.", "fwupd-version": "1.5.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Mei.KeyManifest.json000066400000000000000000000025351501337203100243330ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Mei.KeyManifest", "name": "ME BootGuard Platform Key", "description": [ "The BootGuard Platform Key is fused into the CPU PCH during manufacturing by the OEM.", "At bootup, an authenticated code module computes a hash of the Platform Key and compares it with the one stored in field-programmable fuses.", "If the key matches the ACM will pass control to the firmware, otherwise the boot process will stop.", "In 2022 a number of Platform **secret** Keys were leaked by Lenovo and confirmed by Intel." ], "failure-impact": [ "A custom system firmware can be signed using the leaked private key to completely disable UEFI Secure Boot and allow complete persistent compromise of the affected machine." ], "failure-results": { "not-valid": "device uses a key that is compromised" }, "success-results": { "valid": "device uses a BootGuard Platform Key that is not known to be compromised" }, "hsi-level": 1, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "references": { "https://github.com/phretor/intel-leak-checker/": "Intel leak checker", "https://www.tomshardware.com/news/intel-confirms-6gb-alder-lake-bios-source-code-leak-new-details-emerge": "Tom's Hardware Article" }, "requires": [ "CPUID\\VID_GenuineIntel" ], "fwupd-version": "1.8.7" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Mei.ManufacturingMode.json000066400000000000000000000023341501337203100255210ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Mei.ManufacturingMode", "name": "ME not in manufacturing mode", "description": [ "There have been some unfortunate cases of the ME being distributed in manufacturing mode.", "In manufacturing mode many features from the ME can be interacted with that decrease the platform's security." ], "failure-impact": [ "If the ME is in manufacturing mode then any user with root access can provision the ME engine with new keys.", "This gives them full access to the system even when the system is powered off." ], "failure-results": { "not-locked": "device is in manufacturing mode" }, "success-results": { "locked": "device has had manufacturing mode disabled" }, "hsi-level": 1, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "references": { "https://malware.news/t/intel-me-manufacturing-mode-obscured-dangers-and-their-relationship-to-apple-macbook-vulnerability-cve-2018-4251/23214": "ME Manufacturing Mode: obscured dangers", "https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00086.html": "Intel security advisory SA-00086" }, "requires": [ "CPUID\\VID_GenuineIntel" ], "fwupd-version": "1.5.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Mei.OverrideStrap.json000066400000000000000000000020371501337203100247020ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Mei.OverrideStrap", "name": "ME Flash Descriptor Override", "description": [ "The Flash Descriptor Security Override Strap is not accessible to end users on consumer boards and Intel stresses that this is for debugging only." ], "failure-impact": [ "The system firmware can be written from userspace by changing the protected region.", "This gives any attacker with root access a method to write persistent executable code to the firmware, which survives even a full disk wipe and OS reinstall." ], "failure-results": { "not-locked": "device is in debugging mode" }, "success-results": { "locked": "device in in normal runtime mode" }, "hsi-level": 1, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "references": { "https://chromium.googlesource.com/chromiumos/third_party/flashrom/+/master/Documentation/mysteries_intel.txt": "Chromium documentation for Intel ME" }, "requires": [ "CPUID\\VID_GenuineIntel" ], "fwupd-version": "1.5.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Mei.Version.json000066400000000000000000000024351501337203100235400ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Mei.Version", "name": "CSME Version", "description": [ "Converged Security and Manageability Engine is a standalone management module that can manage and control some local devices without the host CPU involvement.", "The CSME lives in the PCH and can only be updated by the OEM vendor.", "The version of the CSME module can be checked to detect the most common and serious vulnerabilities." ], "failure-impact": [ "Using any one of the critical vulnerabilities, a remote attacker can take full control of the system and all connected devices, even when the system is powered off." ], "failure-results": { "not-valid": "affected by one of the critical CVEs" }, "success-results": { "valid": "is not affected by the most critical CVEs" }, "hsi-level": 1, "resolution": "Update your Management Engine firmware", "references": { "https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00086.html": "Intel CSME Security Review Cumulative Update" }, "issues": [ "CVE-2017-5705", "CVE-2017-5706", "CVE-2017-5707", "CVE-2017-5708", "CVE-2017-5709", "CVE-2017-5710", "CVE-2017-5711", "CVE-2017-5712" ], "requires": [ "CPUID\\VID_GenuineIntel" ], "fwupd-version": "1.5.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.PlatformDebugEnabled.json000066400000000000000000000030601501337203100254030ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.PlatformDebugEnabled", "deprecated-ids": [ "org.fwupd.hsi.IntelDci.Enabled" ], "name": "Intel DCI", "description": [ "Newer Intel CPUs support debugging over USB3 via a proprietary Direct Connection Interface (DCI) with the use of off-the-shelf hardware." ], "failure-impact": [ "Using DCI an attacker with physical access to the computer has full access to all registers and memory in the system, and is able to make changes.", "This makes privilege escalation from user to root possible, and also modifying SMM makes it possible to write to system firmware for a persistent backdoor." ], "failure-results": { "enabled": "debugging is currently enabled" }, "success-results": { "not-enabled": "debugging is not currently enabled" }, "hsi-level": 1, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "references": { "https://www.intel.co.uk/content/www/uk/en/support/articles/000029393/processors.html": "Intel Direct Connect Interface", "https://github.com/chipsec/chipsec/blob/master/chipsec/cfg/8086/pch_4xxlp.xml#L270": "Chipsec 4xxlp register definitions", "https://github.com/riscv/riscv-edk2-platforms/blob/85a50de1b459d1d6644a402081120770aa6dd8c7/Silicon/Intel/CoffeelakeSiliconPkg/Pch/Include/Register/PchRegsDci.h": "RISC-V EDK PCH register definitions" }, "more-information": [ "This attribute was previously known as `org.fwupd.hsi.IntelDci.Enabled` in 1.5.0, but was renamed in 1.8.0 to support other vendors." ], "fwupd-version": "1.5.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.PlatformDebugLocked.json000066400000000000000000000021561501337203100252570ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.PlatformDebugLocked", "deprecated-ids": [ "org.fwupd.hsi.IntelDci.Locked" ], "name": "Part is debug locked", "description": [ "Some devices support a concept of whether a part has been unlocked for debugging using proprietary hardware. Such parts allow access to registers that are typically restricted when parts are fused.", "On Intel systems access to this interface is done via a proprietary Direct Connection Interface (DCI)." ], "failure-impact": [ "If using a debug unlocked part, the platform's overall security will be decreased as an attacker may have elevated access to registers and memory within the system and can potentially enable persistent backdoors." ], "failure-results": { "not-locked": "device is not locked" }, "success-results": { "locked": "device is locked" }, "hsi-level": 2, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "references": { "https://www.intel.co.uk/content/www/uk/en/support/articles/000029393/processors.html": "Intel Direct Connect Interface" }, "fwupd-version": "1.8.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.PlatformFused.json000066400000000000000000000011411501337203100241460ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.PlatformFused", "name": "Part is fused", "description": [ "When fuses are blown in parts from some manufacturers the hardware will enforce protections against tampering or accessing of certain registers." ], "failure-impact": [ "If using an unfused part, the platform's overall security will be decreased." ], "failure-results": { "not-locked": "device is not fused" }, "success-results": { "locked": "device is fused" }, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "hsi-level": 1, "fwupd-version": "1.8.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.PrebootDma.json000066400000000000000000000030431501337203100234320ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.PrebootDma", "deprecated-ids": [ "org.fwupd.hsi.AcpiDmar" ], "name": "Pre-boot DMA protection", "description": [ "The IOMMU on modern systems is used to mitigate against DMA attacks.", "All I/O for devices capable of DMA is mapped into a private virtual memory region.", "On Intel systems the ACPI DMAR table indicated the system is configured with pre-boot DMA protection which eliminates some firmware attacks.", "On AMD systems the ACPI IVRS table indicates the same." ], "failure-impact": [ "An attacker could connect a malicious peripheral using ThunderBolt and reboot the machine, which would allow the attacker to modify the system memory.", "This would allow subverting the Secure Boot protection, and also invalidate any system attestation." ], "failure-results": { "not-valid": "could not determine state", "not-enabled": "was not enabled" }, "success-results": { "enabled": "detected correctly" }, "hsi-level": 3, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "references": { "https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit": "IOMMU Wikipedia Page", "https://www.amd.com/en/support/tech-docs/amd-io-virtualization-technology-iommu-specification": "AMD I/O Virtualization Technology (IOMMU) Specification" }, "more-information": [ "This attribute was previously known as `org.fwupd.hsi.AcpiDmar` in 1.5.0, but was renamed in 1.8.0 to support other vendors." ], "fwupd-version": "1.8.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Smap.json000066400000000000000000000017611501337203100223030ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Smap", "deprecated-ids": [ "org.fwupd.hsi.IntelSmap" ], "name": "SMAP", "description": [ "Without Supervisor Mode Access Prevention, the supervisor code usually has full read and write access to user-space memory mappings.", "This can make exploits easier to write, as it allows the kernel to access user-space memory when it did not intend to." ], "failure-impact": [ "A local or remote attacker can use a simple exploit to modify the contents of kernel memory which can lead to privilege escalation." ], "failure-results": { "not-supported": "SMAP not enabled" }, "success-results": { "enabled": "SMAP features are detected and enabled" }, "hsi-level": 4, "resolution": "Ensure that the CPU supports this feature, and then contact your OEM, who may be able to issue a firmware update.", "references": { "https://en.wikipedia.org/wiki/Supervisor_Mode_Access_Prevention": "SMAP Wikipedia Page" }, "fwupd-version": "2.0.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Spi.Bioswe.json000066400000000000000000000021321501337203100233560ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Spi.Bioswe", "name": "BIOS Write Enable (BWE)", "description": [ "Intel hardware provides this mechanism to protect the SPI ROM chip located on the motherboard from being overwritten by the operating system.", "The `BIOSWE` bit must be unset otherwise userspace can write to the SPI chip." ], "failure-impact": [ "The system firmware can be written from userspace.", "This gives any attacker with root access a method to write persistent executable code to the firmware, which survives even a full disk wipe and OS reinstall." ], "failure-results": { "not-found": "the SPI device was not found", "enabled": "write enable is enabled" }, "success-results": { "not-enabled": "write enable is disabled" }, "hsi-level": 1, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "references": { "https://www.intel.com/content/dam/www/public/us/en/documents/datasheets/6-chipset-c200-chipset-datasheet.pdf": "Intel C200 Datasheet" }, "requires": [ "CPUID\\VID_GenuineIntel" ], "fwupd-version": "1.5.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Spi.Ble.json000066400000000000000000000020061501337203100226300ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Spi.Ble", "name": "BIOS Lock Enable (BLE)", "description": [ "If the lock bit is set then System Management Interrupts (SMIs) are raised when setting BIOS Write Enable.", "The `BLE` bit must be enabled in the PCH otherwise `BIOSWE` can easily be unset." ], "failure-impact": [ "The system firmware can be written from userspace.", "This gives any attacker with root access a method to write persistent executable code to the firmware, which survives even a full disk wipe and OS reinstall." ], "failure-results": { "not-enabled": "the register is not locked" }, "success-results": { "enabled": "the register is locked" }, "hsi-level": 1, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "references": { "https://www.intel.com/content/dam/www/public/us/en/documents/datasheets/6-chipset-c200-chipset-datasheet.pdf": "Intel C200 Datasheet" }, "requires": [ "CPUID\\VID_GenuineIntel" ], "fwupd-version": "1.5.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Spi.Descriptor.json000066400000000000000000000017751501337203100242600ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Spi.Descriptor", "name": "Read-only SPI Descriptor", "description": [ "The SPI descriptor must always be read only from all other regions.", "Additionally on Intel architectures the FLOCKDN register must be set to prevent configuration registers in the SPI BAR from being changed." ], "failure-impact": [ "The system firmware can be written from userspace by changing the protected region.", "This gives any attacker with root access a method to write persistent executable code to the firmware, which survives even a full disk wipe and OS reinstall." ], "failure-results": { "not-valid": "any region can write to the flash descriptor", "not-locked": "the SPI BAR is not locked" }, "success-results": { "locked": "the SPI BAR is locked and read only from all regions" }, "hsi-level": 1, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "requires": [ "CPUID\\VID_GenuineIntel" ], "fwupd-version": "1.6.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Spi.SmmBwp.json000066400000000000000000000021001501337203100233260ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Spi.SmmBwp", "name": "SMM Bios Write Protect (SMM_BWP)", "description": [ "This bit set defines when the BIOS region can be written by the host.", "The `SMM_BWP` bit must be set to make the BIOS region non-writable unless all processors are in system management mode." ], "failure-impact": [ "The system firmware can be written from userspace by exploiting a race condition in checking `BLE`.", "This gives any attacker with root access a method to write persistent executable code to the firmware, which survives even a full disk wipe and OS reinstall." ], "failure-results": { "not-locked": "the region is not locked" }, "success-results": { "locked": "the region is locked" }, "hsi-level": 1, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "references": { "https://www.intel.com/content/dam/www/public/us/en/documents/datasheets/6-chipset-c200-chipset-datasheet.pdf": "Intel C200 Datasheet" }, "requires": [ "CPUID\\VID_GenuineIntel" ], "fwupd-version": "1.5.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.SupportedCpu.json000066400000000000000000000024351501337203100240370ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.SupportedCpu", "name": "Supported CPU", "description": [ "Most platform checks are specific to the CPU vendor.", "To avoid giving a very high HSI result for a platform we do not know how to verify, we include this attribute to ensure that the result is meaningful." ], "failure-impact": [ "If using an unsupported CPU then fwupd is unable to verify the platform security.", "You should contact your platform vendor and ask them to contribute HSI tests for this CPU type." ], "failure-results": { "unknown": "platform security is unknown" }, "success-results": { "valid": "the CPU platform is supported and has HSI tests" }, "more-information": [ "On AMD APUs or CPUs this information is reported on kernel 5.19 or later via the `ccp` kernel module. ", "If the kernel module is enabled but is not being auto-loaded, this is a kernel bug and should be reported to kernel bugzilla. ", "If the kernel module has loaded but you still don't have data this is NOT a fwupd bug. You will have to contact ", "your motherboard or system manufacturer to enable reporting this information." ], "resolution": "Contact the silicon vendor and ask them to submit new HSI tests for fwupd.", "hsi-level": 1, "fwupd-version": "1.8.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.SuspendToIdle.json000066400000000000000000000012101501337203100241120ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.SuspendToIdle", "name": "Suspend-to-Idle", "description": [ "The platform should be set up with Suspend-to-Idle as the default S3 sleep state." ], "failure-impact": [ "A local attacker could overwrite the S3 resume script to modify system RAM which can lead to privilege escalation." ], "failure-results": { "not-enabled": "deep sleep enabled", "not-valid": "could not determine the default" }, "success-results": { "enabled": "suspend-to-idle being used" }, "hsi-level": 3, "resolution": "Change this setting in the UEFI setup screen, if supported.", "fwupd-version": "1.5.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.SuspendToRam.json000066400000000000000000000022341501337203100237630ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.SuspendToRam", "name": "Suspend to RAM disabled", "description": [ "Suspend to Ram (S3) keeps the raw contents of the DRAM refreshed when the system is asleep.", "This means that the memory modules can be physically removed and the contents recovered, or a cold boot attack can be performed with a USB device.", "The firmware should be configured to prefer using suspend to idle instead of suspend to ram or to not offer suspend to RAM." ], "failure-impact": [ "An attacker with physical access to a system can obtain the un-encrypted contents of the RAM by suspending the machine, removing the DIMM and inserting it into another machine with modified DRAM controller before the memory contents decay." ], "failure-results": { "enabled": "sleep enabled", "not-valid": "could not determine the default" }, "success-results": { "not-enabled": "suspend-to-ram being used" }, "hsi-level": 3, "resolution": "Enable this setting in the UEFI setup screen, if supported.", "references": { "https://en.wikipedia.org/wiki/Cold_boot_attack": "Cold Boot Attack Wikipedia Page" }, "fwupd-version": "1.5.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Tpm.EmptyPcr.json000066400000000000000000000023111501337203100236750ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Tpm.EmptyPcr", "name": "Empty PCR in TPM", "description": [ "The system firmware is responsible for measuring values about its boot stage in PCRs 0 through 7.", "Some firmwares have bugs that prevent them from measuring some of those values, breaking the fundamental assumption of the Measured Boot chain-of-trust." ], "failure-impact": [ "A local attacker could measure fake values into the empty PCR, corresponding to a firmware and OS that do not match the ones actually loaded.", "This allows hiding a compromised boot chain or fooling a remote-attestation server into believing that a different kernel is running." ], "failure-results": { "not-found": "no TPM hardware could be found", "not-valid": "at least one empty checksum has been found" }, "success-results": { "valid": "all PCRs from 0 to 7 must have non-empty measurements" }, "hsi-level": 1, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "references": { "https://github.com/google/security-research/blob/master/pocs/bios/tpm-carte-blanche/writeup.md": "TPM Carte Blanche" }, "issues": [ "CVE-2021-42299" ], "fwupd-version": "1.7.2" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Tpm.ReconstructionPcr0.json000066400000000000000000000022771501337203100257130ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Tpm.ReconstructionPcr0", "name": "PCR0 TPM Event Log Reconstruction", "description": [ "The TPM event log records which events are registered for the PCR0 hash.", "When reconstructed the event log values should always match the TPM PCR0.", "If extra events are included in the event log, or some are missing, the reconstitution will fail." ], "failure-impact": [ "This is not a vulnerability per-se, but it shows that the system firmware checksum cannot be verified as the PCR result has been calculated incorrectly." ], "more-information": [ "Additional information about specific bugs and debugging steps are available here https://github.com/fwupd/fwupd/wiki/TPM-PCR0-differs-from-reconstruction" ], "failure-results": { "not-valid": "could not reconstitute the hash value", "not-found": "no TPM hardware could be found" }, "success-results": { "valid": "all correct" }, "hsi-level": 2, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "references": { "https://www.kernel.org/doc/html/latest/security/tpm/tpm_event_log.html": "Linux Kernel TPM Documentation" }, "fwupd-version": "1.5.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Tpm.Version20.json000066400000000000000000000015721501337203100237310ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Tpm.Version20", "name": "TPM 2.0 Present", "description": [ "A TPM securely stores platform specific secrets that can only be divulged to trusted consumers in a secure environment." ], "failure-impact": [ "The PCR registers will not be available for use by the bootloader and kernel.", "This means userspace cannot either encrypt disks to the specific machine, and also can't know if the system firmware was externally modified." ], "failure-results": { "not-found": "no TPM device found", "not-enabled": "TPM not in v2 mode" }, "success-results": { "found": "TPM device found in v2 mode" }, "hsi-level": 1, "resolution": "Enable this setting in the UEFI setup screen, if supported.", "references": { "https://en.wikipedia.org/wiki/Trusted_Platform_Module": "TPM Wikipedia Page" }, "fwupd-version": "1.5.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Uefi.BootserviceVars.json000066400000000000000000000013431501337203100254060ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Uefi.BootserviceVars", "name": "UEFI BootService Variables", "description": [ "UEFI boot service variables should not be readable from runtime mode." ], "failure-impact": [ "It is possible to read security-sensitive data that should not be readable by the runtime mode." ], "failure-results": { "not-locked": "bootservice-only data is readable in runtime mode" }, "success-results": { "locked": "bootservice-only data is not visible" }, "hsi-level": 1, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "references": { "https://uefi.org/specs/UEFI/2.10/07_Services_Boot_Services.html": "UEFI Specification" }, "fwupd-version": "1.9.3" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Uefi.Db.json000066400000000000000000000020061501337203100226100ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Uefi.Db", "name": "UEFI db", "description": [ "The UEFI db contains the list of valid certificates that is used to authorize what EFI binaries are allowed to run." ], "failure-impact": [ "Distribution-provided signed bootloader binaries may not boot on any system that does not have the new Microsoft UEFI certificate installed in the db." ], "failure-results": { "not-valid": "the certificate store is not up to date" }, "success-results": { "not-found": "the certificate store does not contain any Microsoft UEFI certificate", "valid": "the certificate store has the Microsoft UEFI 2023 certificate installed" }, "resolution": "Apply the LVFS KEK update for your hardware, and then apply the LVFS db update.", "references": { "https://fwupd.github.io/libfwupdplugin/uefi-db.html": "fwupd Documentation for UEFI Secure Boot Certificates", "https://wiki.ubuntu.com/UEFI/SecureBoot/Testing": "Ubuntu SecureBoot Wiki Page" }, "fwupd-version": "2.0.8" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Uefi.MemoryProtection.json000066400000000000000000000026701501337203100256110ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Uefi.MemoryProtection", "name": "Early-boot UEFI Memory Protections", "description": [ "The UEFI system can set up memory attributes at boot which prevent common exploits from running. For instance, the stack can be marked as non-executable to prevent running any malicious code that can be written to the stack.", "In the same way, writing to sections that are read-only can also be prevented, making the system more secure from attackers.", "Distribution vendors should tag the bootloader as NX compatible, and BIOS vendors should provide a 'memory attribute protocol' table that tags memory sections with specific properties.", "Microsoft Virtualization-based security (VBS) also requires NX protection for UEFI runtime services for DeviceGuard compliance." ], "failure-impact": [ "The attacker is able to run arbitrary executable code from the stack, or write to memory that is supposed to be read-only." ], "failure-results": { "not-locked": "memory is still executable or writable", "not-enabled": "the bootloader is not marked as NX compatible and the firmware may be operating in a compatibility mode" }, "success-results": { "locked": "memory is not executable or writable" }, "hsi-level": 3, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "references": { "https://aka.ms/WHCP": "Microsoft Windows WHCP" }, "fwupd-version": "2.0.7" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Uefi.Pk.json000066400000000000000000000014311501337203100226360ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Uefi.Pk", "name": "UEFI PK", "description": [ "UEFI defines a platform key for the system.", "This should not be a test key, e.g. `DO NOT TRUST - AMI Test PK`" ], "failure-impact": [ "It is possible to sign an EFI binary with the test platform key, which invalidates the Secure Boot trust chain.", "It effectively gives the local attacker full access to your hardware." ], "failure-results": { "not-valid": "an invalid key has been enrolled" }, "success-results": { "valid": "valid key" }, "hsi-level": 1, "resolution": "Contact your OEM, who may be able to issue a firmware update.", "references": { "https://wiki.ubuntu.com/UEFI/SecureBoot/Testing": "Ubuntu SecureBoot Wiki Page" }, "fwupd-version": "1.5.0" } fwupd-2.0.10/docs/hsi-tests.d/org.fwupd.hsi.Uefi.SecureBoot.json000066400000000000000000000015721501337203100243440ustar00rootroot00000000000000{ "id": "org.fwupd.hsi.Uefi.SecureBoot", "name": "UEFI SecureBoot", "description": [ "UEFI Secure boot is a verification mechanism for ensuring that code launched by firmware is trusted.", "Secure Boot requires that each binary loaded at boot is validated against trusted certificates." ], "failure-impact": [ "When Secure Boot is not enabled any EFI binary can be run at startup, which gives the attacker full access to your hardware." ], "failure-results": { "not-found": "support has not been detected", "not-enabled": "detected, but has been turned off" }, "success-results": { "enabled": "supported and enabled" }, "hsi-level": 1, "resolution": "Turn off CSM boot and enable Secure Boot in the BIOS setup.", "references": { "https://wiki.ubuntu.com/UEFI/SecureBoot": "Ubuntu SecureBoot Wiki Page" }, "fwupd-version": "1.5.0" } fwupd-2.0.10/docs/hsi.html000066400000000000000000000010471501337203100153150ustar00rootroot00000000000000 Redirecting to https://fwupd.github.io/libfwupdplugin/hsi.html fwupd-2.0.10/docs/hsi.md.in000066400000000000000000000232641501337203100153630ustar00rootroot00000000000000--- title: Host Security ID Specification --- **WARNING: This specification is still in active development: it is incomplete, subject to change, and may have errors; use this at your own risk. It is based on publicly available information.** Authors: - Richard Hughes - Mario Limonciello - Alex Bazhaniuk - Alex Matrosov --- ## Introduction Not all system vendors prioritize building a secure platform. The truth is that **security costs money**. Vendors have to choose between saving a few cents on a bill-of-materials by sharing a SPI chip, or correctly implementing BootGuard. Discovering security vulnerabilities often takes an external researcher filing a disclosure. These disclosures are often technical in nature and difficult for an average consumer to decipher. The Linux Vendor Firmware Service (LVFS) could provide some **easy-to-understand** information to people buying hardware. The service already knows a huge amount of information about machines from signed reports uploaded to the LVFS and from analyzing firmware binaries. However this information alone does not explain firmware security to the user in a way they can actually interpret. ### Other Tools Traditionally, figuring out the true security of your hardware and firmware requires sifting through the marketing documentation provided by the OEM and in many cases just "trusting" they did it right. Tools such as Chipsec can check the hardware configuration, but they do not work out of the box and use technical jargon that an average user cannot interpret. Unfortunately, running a tool like Chipsec requires that you actively turn off some security layers such as UEFI Secure Boot, and allow 3rd party unsigned kernel modules to be loaded. ## [Verifying Host Firmware Security](#verifying) To start out some core protections must be assigned a relative importance. Then an evaluation must be done to determine how each vendor is conforming to the model. For instance, a user might say that for home use any hardware the bare minimum security level (`HSI:1`) is *good enough*. For a work laptop the company IT department might restrict the choice of models to anything meeting the criteria of level `HSI:2` or above. A journalist or a security researcher would only buy level `HSI:3` and above. The reality is that `HSI:4` is going to be more expensive than some unbranded hardware that is rated `HSI:0`. To be trusted, this rating information should be distributed in a centralized agnostic database such as the LVFS. Of course, tools need to detect implementation errors, and to verify that the model that is measured does indeed match the HSI level advertised by the LVFS. Some existing compliance solutions place the burden on the OEM to define what firmware security has been implemented, which is easy to get wrong and in some cases impossible to verify. For this reason HSI will only measure security protections that can be verified by the end user without requiring any extra hardware to be connected, additional software to be installed, or disabling any existing security layers to measure. The HSI specification is primarily designed for laptop and desktop hardware, although some tests *may* still make sense on server or embedded hardware. It is not expected that non-consumer hardware will publish an HSI number. ## [Runtime Behavior](#runtime-behaviour) Orthogonal to the security features provided by the firmware there are other security considerations related to the firmware which may require internet access to discover or that runtime OS changes directly affect the security of the firmware. It would not make sense to have *have updates on the LVFS* as a requirement for a specific security level as this would mean offline the platform might be a higher level initially but as soon as it is brought online it is downgraded which would be really confusing to users. The *core* security level will not change at Operating System runtime, but the suffix may. **More information** Additional information about specific bugs and debugging steps are available on the [fwupd wiki](https://github.com/fwupd/fwupd/wiki/Host-security-ID-runtime-issues). ### [HSI:0 (Insecure State)](#hsi-level0) Limited firmware protection. The lowest security level with little or no detected firmware protections. This is the default security level if no tests can be run or some tests in the next security level have failed. ### [HSI:1 (Critical State)](#hsi-level1) Basic protection but any failure would lead to a critical security impact. This security level corresponds to the most basic of security protections considered essential by security professionals. Any failures at this level would have critical security impact and could likely be used to compromise the system firmware without physical access. ### [HSI:2 (Risky State)](#hsi-level2) The failure is only happened by the theoretical exploit in the lab. This security level corresponds to firmware security issues that pose a theoretical concern or where any exploit would be difficult or impractical to use. At this level various technologies may be employed to protect the boot process from modification by an attacker with local access to the machine. ### [HSI:3 (Protected State)](#hsi-level3) The system firmware only has few minor issues which do not affect the security status. This security level corresponds to out-of-band protection of the system firmware perhaps including recovery. ### [HSI:4 (Secure State)](#hsi-level4) The system is in a robust secure state. The system is corresponding several kind of encryption and execution protection for the system firmware. ### [HSI:5 (Secure Proven State)](#hsi-level5) This security level corresponds to out-of-band attestation of the system firmware. There are currently no tests implemented for HSI:5 and so this security level cannot yet be obtained. ### [HSI Runtime Suffix `!`](#runtime-bang) A runtime security issue detected. - UEFI [Secure Boot](https://wiki.ubuntu.com/UEFI/SecureBoot) has been turned off. *[v1.5.0]* - The kernel is [tainted](https://www.kernel.org/doc/html/latest/admin-guide/tainted-kernels.html) due to a non-free module or critical firmware issue. *[v1.5.0]* - The kernel is not [locked down](https://mjg59.dreamwidth.org/50577.html). *[v1.5.0]* - Unencrypted [swap partition](https://wiki.archlinux.org/index.php/Dm-crypt/Swap_encryption). *[v1.5.0]* - The installed fwupd is running with [custom or modified plugins](https://github.com/fwupd/fwupd/tree/main/plugins). *[v1.5.0]* ## [Low Security Level](#low-security-level) A safe baseline for security should be HSI-1. If your system isn't at least meeting this criteria, you should adjust firmware setup options, contact your manufacturer or replace the hardware. The command line tool `fwupdmgr security` included with fwupd 1.8.4 or later will make individual recommendations on what you can do for individual test failures. GUI tools built against `libfwupd` 1.8.4 or later may also make these recommendation as well. ## [Tests included in fwupd](#tests) The set of tests is currently x86 UEFI-centric, but will be expanded in the future for various ARM or RISC-V firmware protections as required. Where the requirement is architecture or processor specific it has been noted. {{tests}} ## [Conclusion](#conclusions) Any system with a Host Security ID of `0` can easily be modified from userspace. PCs with confidential documents should have a `HSI:3` or higher level of protection. In a graphical tool that would show details about the computer (such as GNOME Control Center's details tab) the OS could display a field indicating Host Security ID. The ID should be shown with an alert color if the security is not at least `HSI:1` or the suffix is `!`. On Linux `fwupd` is used to enumerate and update firmware. It exports a property `HostSecurityId` and a `GetHostSecurityAttrs()` method. The attributes are supposed to represent the *system as a whole* but individual (internal) devices are able to make a claim that they worsened the state of the security of the system. Certain attributes can "obsolete" other attributes. An example is BIOSGuard will set obsoletes to `org.intel.prx`. A plugin method gets called on each plugin which adds attributes directly from the hardware or kernel. Several attributes may be dependent upon the kernel performing measurements and it will take time for these to be upstreamed. In some cases security level measurements will only be possible on systems with a newer kernel. The long term goal is to increase the `HSI:x` level of systems being sold to consumers. By making some of the `HSI:x` attributes part of the LVFS uploaded report we can allow users to compare vendors and models before purchasing hardware. ## [Intentional Omissions](#omissions) ### Intel SGX This is not widely used as it has several high severity security issues. ### Intel MPX MPX support was removed from GCC and the Linux kernel in 2019 and it is now considered obsolete. ## Further Work More internal and external devices should be factored into the security equation. For now the focus for further tests should be around internal device firmware as it is what can be most directly controlled by fwupd and the hardware manufacturer. Security conscious manufacturers are actively participating in the development of future initiatives in the Trusted Computing Group (TCG). As those become ratified standards that are available in hardware, there are opportunities for synergy with this specification. fwupd-2.0.10/docs/hwids.md000066400000000000000000000123371501337203100153100ustar00rootroot00000000000000--- title: Hardware IDs --- ## Introduction Hardware IDs are used by fwupd to identify specific hardware. This is useful as the device-specific identifiers may be the same for different firmware streams. Each hardware ID has varying levels of specificity for the hardware, for instance matching only the system OEM, or matching up to 8 fields including the system BIOS version. For instance, Dell and Lenovo might ship a wireless broadband modem with the same chip vendor and product IDs of `USB\VID_0BDA&PID_5850` and although the two OEMs share the same internal device, the firmware may be different. To cover this case fwupd allows adding `hardware` requirements that mean we can deploy firmware that targets `USB\VID_0BDA&PID_5850`, but *only for Dell* or *only for Lenovo* systems. Microsoft calls these values "CHIDs" and they are generated on Windows from the SBMIOS tables using `ComputerHardwareIds.exe` binary which can be found [here](https://learn.microsoft.com/en-us/windows-hardware/drivers/devtest/computerhardwareids). The list of CHIDs used in Microsoft Windows is: HardwareID-00 ↠Manufacturer + Family + Product Name + SKU Number + BIOS Vendor + BIOS Version + BIOS Major Release + BIOS Minor Release HardwareID-01 ↠Manufacturer + Family + Product Name + BIOS Vendor + BIOS Version + BIOS Major Release + BIOS Minor Release HardwareID-02 ↠Manufacturer + Product Name + BIOS Vendor + BIOS Version + BIOS Major Release + BIOS Minor Release HardwareID-03 ↠Manufacturer + Family + ProductName + SKU Number + Baseboard_Manufacturer + Baseboard_Product HardwareID-04 ↠Manufacturer + Family + ProductName + SKU Number HardwareID-05 ↠Manufacturer + Family + ProductName HardwareID-06 ↠Manufacturer + SKU Number + Baseboard_Manufacturer + Baseboard_Product HardwareID-07 ↠Manufacturer + SKU Number HardwareID-08 ↠Manufacturer + ProductName + Baseboard_Manufacturer + Baseboard_Product HardwareID-09 ↠Manufacturer + ProductName HardwareID-10 ↠Manufacturer + Family + Baseboard_Manufacturer + Baseboard_Product HardwareID-11 ↠Manufacturer + Family HardwareID-12 ↠Manufacturer + Enclosure Type HardwareID-13 ↠Manufacturer + Baseboard_Manufacturer + Baseboard_Product HardwareID-14 ↠Manufacturer Additionally, we also support some fwupd-specific IDs: fwupd-05 ↠Manufacturer&Family&ProductName&BiosVendor fwupd-14 ↠Manufacturer&BiosVendor On Windows, CHIDs are generated from the ASCII representation of SMBIOS strings, and on Linux the same mechanism is used. Additionally, on Linux, the Device Tree, DMI and `kenv` data sources are used to construct emulations of the Microsoft CHIDs. When installing firmware and drivers in Windows vendors *already use* the generated HardwareID GUIDs that match SMBIOS keys like the BIOS vendor and the product SKU. Both `fwupdtool hwids` and `ComputerHardwareIds.exe` only compute results that have the necessary data values available. If a data field is missing, then any related CHIDs are not generated. For example, if the SKU field is missing, then `HardwareID` 0, 3, 4 6 and 7 will not be available for that particular system. ## Implementation Users with versions of fwupd newer than 1.1.1 can run `sudo fwupdtool hwids`. For example: Computer Information -------------------- BiosVendor: LENOVO BiosVersion: GJET75WW (2.25 ) Manufacturer: LENOVO Family: ThinkPad T440s ProductName: 20ARS19C0C ProductSku: LENOVO_MT_20AR_BU_Think_FM_ThinkPad T440s EnclosureKind: 10 BaseboardManufacturer: LENOVO BaseboardProduct: 20ARS19C0C Hardware IDs ------------ {c4159f74-3d2c-526f-b6d1-fe24a2fbc881} <- Manufacturer + Family + ProductName + ProductSku + BiosVendor + BiosVersion + BiosMajorRelease + BiosMinorRelease {ff66cb74-5f5d-5669-875a-8a8f97be22c1} <- Manufacturer + Family + ProductName + BiosVendor + BiosVersion + BiosMajorRelease + BiosMinorRelease {2e4dad4e-27a0-5de0-8e92-f395fc3fa5ba} <- Manufacturer + ProductName + BiosVendor + BiosVersion + BiosMajorRelease + BiosMinorRelease {3faec92a-3ae3-5744-be88-495e90a7d541} <- Manufacturer + Family + ProductName + ProductSku + BaseboardManufacturer + BaseboardProduct {660ccba8-1b78-5a33-80e6-9fb8354ee873} <- Manufacturer + Family + ProductName + ProductSku {8dc9b7c5-f5d5-5850-9ab3-bd6f0549d814} <- Manufacturer + Family + ProductName {178cd22d-ad9f-562d-ae0a-34009822cdbe} <- Manufacturer + ProductSku + BaseboardManufacturer + BaseboardProduct {da1da9b6-62f5-5f22-8aaa-14db7eeda2a4} <- Manufacturer + ProductSku {059eb22d-6dc7-59af-abd3-94bbe017f67c} <- Manufacturer + ProductName + BaseboardManufacturer + BaseboardProduct {0cf8618d-9eff-537c-9f35-46861406eb9c} <- Manufacturer + ProductName {f4275c1f-6130-5191-845c-3426247eb6a1} <- Manufacturer + Family + BaseboardManufacturer + BaseboardProduct {db73af4c-4612-50f7-b8a7-787cf4871847} <- Manufacturer + Family {5e820764-888e-529d-a6f9-dfd12bacb160} <- Manufacturer + EnclosureKind {f8e1de5f-b68c-5f52-9d1a-f1ba52f1f773} <- Manufacturer + BaseboardManufacturer + BaseboardProduct {6de5d951-d755-576b-bd09-c5cf66b27234} <- Manufacturer Which matches the output of `ComputerHardwareIds.exe` on the same hardware. fwupd-2.0.10/docs/index.html000066400000000000000000000072761501337203100156530ustar00rootroot00000000000000 fwupd

    End user documentation

    {% if man %}

    Manual pages

    Manual pages for interacting with fwupd.

    {% for obj in man %} {% endfor %}{% endif %}

    Host Security ID

    The fwupd HSI specification.

    BIOS Settings

    The fwupd BIOS settings interface

    Supermicro BMC license issue

    Fixing the missing license issue when updating Supermicro boards

    Developer documentation

    Building fwupd

    Building using the fwupd development environment

    libfwupd

    Functionality exported by libfwupd for client applications.

    libfwupdplugin

    Functionality available to fwupd plugins.

    BOS DS20 Specification

    The fwupd Binary Object Store descriptor specification

    fwupd-2.0.10/docs/meson.build000066400000000000000000000165641501337203100160200ustar00rootroot00000000000000plugin_readme_targets = [] plugin_readme_outputs = [] foreach plugin, enabled: plugins plugin_readme_output = '@0@-README.md'.format(plugin) plugin_readme_targets += custom_target('doc-plugin-@0@'.format(plugin), input: join_paths(meson.project_source_root(), 'plugins', plugin, 'README.md'), output: plugin_readme_output, command: ['ln', '-sf', '@INPUT0@', '@OUTPUT@'], build_by_default: true, ) plugin_readme_outputs += '"@0@"'.format(plugin_readme_output) endforeach if build_standalone if get_option('man') custom_target('fwupd.conf.5', input: 'fwupd.conf.md', output: 'fwupd.conf.5', command: [ generate_man, '@INPUT@', '-o', '@OUTPUT@', '--replace', 'PACKAGE_VERSION', fwupd_version, '--replace', 'SYSCONFDIR', sysconfdir, '--replace', 'plugin_uefi_capsule', '@0@'.format(allow_uefi_capsule), '--replace', 'plugin_msr', '@0@'.format(has_cpuid), '--replace', 'plugin_redfish', '@0@'.format(host_machine.system() == 'linux'), '--replace', 'P2pPolicy', '@0@'.format(get_option('p2p_policy')), '--defines', join_paths(meson.project_source_root(), 'plugins', 'msr', 'fu-msr-plugin.c'), '--defines', join_paths(meson.project_source_root(), 'plugins', 'redfish', 'fu-redfish-plugin.c'), '--defines', join_paths(meson.project_source_root(), 'plugins', 'thunderbolt', 'fu-thunderbolt-plugin.c'), '--defines', join_paths(meson.project_source_root(), 'plugins', 'uefi-capsule', 'fu-uefi-capsule-plugin.c'), '--defines', join_paths(meson.project_source_root(), 'src', 'fu-engine-config.c'), ], install: true, install_dir: join_paths(mandir, 'man5'), ) custom_target('fwupd-remotes.d.5', input: 'fwupd-remotes.d.md', output: 'fwupd-remotes.d.5', command: [ generate_man, '@INPUT@', '-o', '@OUTPUT@', '--replace', 'PACKAGE_VERSION', fwupd_version, '--replace', 'SYSCONFDIR', sysconfdir, '--replace', 'LOCALSTATEDIR', localstatedir, '--defines', join_paths(meson.project_source_root(), 'src', 'fu-remote.c'), ], install: true, install_dir: join_paths(mandir, 'man5'), ) endif if build_docs md_targets += custom_target('fwupd.conf.md', input: 'fwupd.conf.md', output: 'fwupd.conf.md', command: [ generate_man, '@INPUT@', '-o', '@OUTPUT@', '--replace', 'PACKAGE_VERSION', fwupd_version, '--replace', 'SYSCONFDIR', sysconfdir, '--replace', 'plugin_uefi_capsule', '@0@'.format(allow_uefi_capsule), '--replace', 'plugin_msr', '@0@'.format(has_cpuid), '--replace', 'plugin_redfish', '@0@'.format(host_machine.system() == 'linux'), '--defines', join_paths(meson.project_source_root(), 'plugins', 'msr', 'fu-msr-plugin.c'), '--defines', join_paths(meson.project_source_root(), 'plugins', 'redfish', 'fu-redfish-plugin.c'), '--defines', join_paths(meson.project_source_root(), 'plugins', 'thunderbolt', 'fu-thunderbolt-plugin.c'), '--defines', join_paths(meson.project_source_root(), 'plugins', 'uefi-capsule', 'fu-uefi-capsule-plugin.c'), '--defines', join_paths(meson.project_source_root(), 'src', 'fu-engine-config.c'), '--md', ], ) md_targets += custom_target('fwupd-remotes.d.md', input: 'fwupd-remotes.d.md', output: 'fwupd-remotes.d.md', command: [ generate_man, '@INPUT@', '-o', '@OUTPUT@', '--replace', 'PACKAGE_VERSION', fwupd_version, '--replace', 'SYSCONFDIR', sysconfdir, '--replace', 'LOCALSTATEDIR', localstatedir, '--defines', join_paths(meson.project_source_root(), 'libfwupd', 'fwupd-remote.c'), '--md', ], ) man_md += ['"fwupd.conf.md"', '"fwupd-remotes.d.md"'] endif endif if build_docs toml_conf = configuration_data() docgen_version = meson.project_version() toml_conf.set('version', docgen_version) toml_conf.set('plugin_readme_outputs', ','.join(plugin_readme_outputs)) toml_conf.set('man_md', ','.join(man_md)) fwupd_toml = configure_file( input: 'fwupd.toml.in', output: 'fwupd.toml', configuration: toml_conf ) fwupdplugin_toml = configure_file( input: 'fwupdplugin.toml.in', output: 'fwupdplugin.toml', configuration: toml_conf ) custom_target('doc-fwupd', input: [ fwupd_toml, fwupd_gir[0], ], output: 'libfwupd', command: [ gidocgen_app, 'generate', '--quiet', '--add-include-path=@0@'.format(meson.current_build_dir() / '../libfwupd'), '--config=@INPUT0@', '--output-dir=@OUTPUT@', '--no-namespace-dir', '--content-dir=@0@'.format(meson.current_source_dir()), '@INPUT1@', ], depends: [ fwupd_gir[0], ], build_by_default: true, install: true, install_dir: join_paths(datadir, 'doc'), ) subdir('hsi-tests.d') hsi_md = custom_target('generate-hsi-spec', input: hsi_test_jsons, output : 'hsi.md', command : [ python3, files(['generate-hsi-spec.py', 'hsi.md.in']), '@OUTPUT@', '@INPUT@', ], ) custom_target('generate-oval', input: hsi_test_jsons, output : 'oval.xml', command : [ python3, files(['generate-oval.py']), '--version', fwupd_version, '--schema-version', '5.11.3', '@OUTPUT@', '@INPUT@', ], ) custom_target('doc-fwupdplugin', input: [ fwupdplugin_toml, fwupdplugin_gir[0], ], output: 'libfwupdplugin', command: [ gidocgen_app, 'generate', '--quiet', '--add-include-path=@0@'.format(meson.current_build_dir() / '../libfwupd'), '--add-include-path=@0@'.format(meson.current_build_dir() / '../libfwupdplugin'), '--config=@INPUT0@', '--output-dir=@OUTPUT@', '--no-namespace-dir', '--content-dir=@0@'.format(meson.current_build_dir()), '--content-dir=@0@'.format(meson.current_source_dir()), '--content-dir=@0@'.format(meson.current_build_dir() / '../src'), '--content-dir=@0@'.format(meson.current_build_dir() / '../plugins/uefi-dbx'), '--content-dir=@0@'.format(meson.current_build_dir() / '../data/motd'), '--content-dir=@0@'.format(meson.current_build_dir() / '../plugins/uefi-capsule'), '@INPUT1@', ], depends: [ fwupdplugin_gir[0], hsi_md, plugin_readme_targets, md_targets, ], build_by_default: true, install: true, install_dir: join_paths(datadir, 'doc'), ) man_cmd = [] foreach man: man_md man_cmd += '-m @0@'.format(man) endforeach custom_target('index.html', input: 'index.html', output: 'index.html', command: [ generate_index, '@INPUT@', '-o', '@OUTPUT@', man_cmd, ], install: true, install_dir: join_paths(datadir, 'doc', 'fwupd') ) if hsi install_data(['hsi.html'], install_dir : join_paths(datadir, 'doc', 'fwupd') ) endif install_data(['urlmap_fwupd.js'], install_dir: join_paths(datadir, 'doc', 'libfwupd') ) install_data(['urlmap_fwupdplugin.js'], install_dir: join_paths(datadir, 'doc', 'libfwupdplugin') ) #make devhelp work install_symlink('libfwupd', install_dir: join_paths(datadir, 'doc', 'fwupd'), pointing_to: join_paths('..', 'libfwupd'), ) install_symlink('libfwupdplugin', install_dir: join_paths(datadir, 'doc', 'fwupd'), pointing_to: join_paths('..', 'libfwupdplugin'), ) endif fwupd-2.0.10/docs/nda.md000066400000000000000000000135361501337203100147360ustar00rootroot00000000000000NDA Policy ========== We, the individuals who are the maintainers of the fwupd and LVFS projects, often get asked by companies to sign non-disclosure agreements "NDAs" or other types of confidentiality agreements and we have always said “noâ€. Rather than rewriting a version of this document for each individual request, we have summarized the key points here. We Are Individuals ------------------ The fwupd and LVFS projects do not sit under a legal entity and it would be us, as individuals, personally signing these contracts. That means not only putting our names and reputations on the line, but also our personal property, homes etc. NDAs Require Time ----------------- We have limited time to spend on these projects and time spent dealing with NDAs and lawyers is time that we either cannot spend on the project itself, or with our families and friends. Cost of Lawyers --------------- Before we sign an NDA in a personal capacity, we need to spend time to read it, and have our lawyer check it. The protection language of a “generic NDA†is almost always broad, vague and ambiguous and we would probably need to negotiate to make it more specific, with both legal teams involved. That means money and time – which is a really bitter pill to swallow if the company isn’t actually contributing financially to fwupd/LVFS. Limitless Risk -------------- The remediation language is almost always open-ended and requires that we agree to eventualities that are nearly impossible to define fully. We often see language like “irreparable harm†and “cannot adequately be compensated with damagesâ€. This means the **personal** potential negative outcomes are near limitless. Worldwide Risk -------------- Companies typically propose that NDAs are subject to governing laws and jurisdictions which are most favourable to them, or relate to where in the world they have a presence. The complexity and cost of reviewing an NDA under a governing law unfamiliar to us is significant. We do not want to be in a position where we could be sued, and forced to pay to defend ourselves, in countries around the world, by working (usually for free) on better supporting firmware updates for a company's hardware. Really "Confidential Information"? ---------------------------------- Agreements of this nature are really about protecting confidential, proprietary information. We know it feels as though the protocol used by your device is unique and contains valuable intellectual property, but unfortunately they rarely do. In reality, most firmware update protocols are very similar, and specific device implementation is broadly the same as direct competitors. Firmware updating is a very low level operation, with only a few different ways to achieve the same result – so many companies we’ve already worked with (over 100 other vendors!) will have ideas similar to whatever you’re trying to keep secret about. Corporate Policy Dictates ---------------------------- We do understand that some companies have a corporate policy of requiring NDAs. Our policy is that we do not sign NDAs, for the reasons in this document. If that means that there is an irreconciable clash of policies, then that is unfortunate but acceptable to us. Trust Is More Important ----------------------- Working together in an open source project is a very human business that involves networks, relationships and, above all else, trust. If someone actually lets you down in a confidential situation, they lose all of those – that would hurt the fwupd/LVFS brand and standing much more than any legal recourse you might have with an NDA. The LVFS works with all levels of the ecosystem from ISV, to IHV, to ODM to OEM and we **rely** on these trust networks as without those the whole ecosystem collapses. Hardly Actually Used -------------------- NDAs very rarely ever seem to actually get used; in 21 years of being a software engineer I’ve never actually seen an NDA that I’ve been involved with, actually be used in a court of law. Almost always, NDAs will be used against former employees trying to compete with their former business, rather than between partners working together. You would be asking us to spend time and money on something that is likely never going to used. Organization structures also change; if your organization ever changes hands, so does the agreement. The new parent company may not love fwupd/LVFS or Open Source as much as the old one did, and may be subject to different governing laws or jurisdictions. Signing an NDA also opens us up personally to the possibility of abusive or frivolous litigation. Even if we have done nothing wrong we must pay legal fees to prove it – and remember that most companies are not paying us anything for our services anyway, so we have no legal defense fund. Conclusion ---------- Signing a “generic NDA†means the risk-reward ratio is strongly stacked against us. Our best-case scenario is (usually) adding one new fwupd plugin and adding support for a handful of new devices to the LVFS. The beneficiary here is the company providing the firmware, in terms of getting better support for the hardware, not us personally -- as we do not receive anything for the provision of updates. The worst-case scenario of potential frivolous litigation is complete and total devastation of both projects, and often, the destruction of both professional and personal reputation. Important Note -------------- Both maintainers of fwupd are employed by huge companies that may or may not allow them to contribute to all or some of the project during work time. There maybe an existing NDA between your company and theirs, or that a new NDA can be signed between them and you. If this is arranged outside of the fwupd/LVFS ecosystem, all correspondence must be made using the corporate-approved communication methods rather than leaking out onto GitHub or personal email addresses. fwupd-2.0.10/docs/only-trusted.md000066400000000000000000000141521501337203100166400ustar00rootroot00000000000000--- title: Signing Test Firmware Payloads --- ## Introduction In the normal vendor update flow, firmware is optionally signed and encrypted by the vendor, and then uploaded to the LVFS wrapped in a cabinet archive with a small XML metadata file to describe how the firmware should be matched to hardware. Upon uploading, the LVFS signs the firmware and metadata XML contained in the archive and adds it to a `.jcat` file also included in the image. The original firmware is never modified, which is why the [Jcat](https://github.com/hughsie/libjcat) file exists as both a detached checksum (SHA-1, SHA-256 and SHA-512) and a detached signature. The LVFS can add either GPG or PKCS#7 signatures in the Jcat file, and currently does *both* for maxmum compatibility with how client systems have been configured. The keys in `/etc/pki/fwupd` and `/etc/pki/fwupd-metadata` are used for per-system trust and are currently used for "did the firmware update *come from* somewhere I trust" rather than "verify the vendor signed the update" -- on the logic the "signed the update" is probably already covered by a signature on the payload that the device verifies. Notably, The LVFS both verifies the vendor OEM → ODM → IHV relationships and assigns restrictions on what devices each legal entity can upload for. There's no way to separate the keys so that you could say "only use this certificate for per-system-trust when the DMI vendor of the device is Dell" and there's no way to do key rotation or revocation. The trusted certificate mechanism was not really designed for any keys except the static LVFS. If the intent is to use a test key to sign the firmware files and get installed purely offline with an unmodified fwupd package (without uploading to the LVFS) then the following instructions can be modified to suit. First, lets verify that an existing firmware binary and metainfo file without a Jcat signature refuses to install when packaged into a cabinet archive: $ fwupdtool build-cabinet firmware.cab firmware.bin firmware.metainfo.xml $ fwupdmgr install firmware.cab --allow-reinstall Decompressing… [ - ] firmware signature missing or not trusted; set OnlyTrusted=false in /etc/fwupd/fwupd.conf ONLY if you are a firmware developer Let's download a script that can generate some test certificates -- feel free to copy the commands used and of course you need to modify the details of both the CA and user certificate. Please do not use the unmodified `ACME-CA.pem` or `rhughes_signed.pem` files for signing any cabinet archives you're going to redistribute anywhere (even internally), otherwise it is going to be very confusing to debug *which* `rhughes_signed.pem` is being used. $ wget https://raw.githubusercontent.com/hughsie/libjcat/main/contrib/build-certs.py $ python ./build-certs.py Signing certificate... $ ls ACME* rhughes* ACME-CA.key ACME-CA.pem rhughes.csr rhughes.key rhughes.pem rhughes_signed.pem We now have a CA key from ACME, and a user key signed by the CA key, along with a CSR and the two private keys. Lets now use the signed user key to create a Jcat file and also add a SHA256 checksum: $ jcat-tool --appstream-id com.redhat.rhughes sign firmware.jcat firmware.bin rhughes_signed.pem rhughes.key $ jcat-tool self-sign firmware.jcat firmware.bin --kind sha256 $ jcat-tool info firmware.jcat JcatFile: Version: 0.1 JcatItem: ID: firmware.bin JcatBlob: Kind: pkcs7 Flags: is-utf8 AppstreamId: com.redhat.rhughes Timestamp: 2023-02-22T10:24:25Z Size: 0xdcc Data: -----BEGIN PKCS7----- MIIKCwYJKoZIhvcNAQcCoIIJ/DCCCfgCAQExDTALBglghkgBZQMEAgEwCwYJKoZI ... ysAcwqcDY7+k9TWB8V2MeZCHg6/aF4Oj3R16Nvag3w== -----END PKCS7----- JcatBlob: Kind: sha256 Flags: is-utf8 Timestamp: 2023-02-22T10:30:19Z Size: 0x40 Data: fce1847b0599bb19cd913d02268f15107691a79221ce16822b4c931cd1bda2c5 We can then create the new firmware archive, this time with the self-signed Jcat file as well. fwupdtool build-cabinet firmware.cab firmware.bin firmware.metainfo.xml firmware.jcat Now we need to install the **CA** certificate to the system-wide system store. If fwupd is running in a prefix then you need to use that instead, e.g. `/home/emily/root/etc/pki/fwupd/`. $ sudo cp ACME-CA.pem /etc/pki/fwupd/ [sudo] password for emily: foobarbaz Then, the firmware should install **without** needing to change `OnlyTrusted` in `fwupd.conf`. $ fwupdmgr install firmware.cab --allow-reinstall Writing… [***************************************] Successfully installed firmware Vendors are allowed to sign the Jcat with their own user certificate if desired, although please note that maintaining a certificate authority is a serious business including HSMs, time-limited and *revokable* user-certificates -- and typically lots of legal paperwork. Shipping the custom vendor CA certificate in the fwupd project is **not possible**, or a good idea, secure or practical -- or how fwupd and LVFS were designed to be used. So please do not ask. That said, if a vendor included the `.jcat` in the firmware cabinet archive, the LVFS will **append** its own signature rather than replace it -- which may make testing the archive easier. ## Debugging Using `sudo fwupdtool get-details firmware.cab --verbose --verbose` should indicate why the certificate isn't being trusted, e.g. FuCabinet processing file: firmware.metainfo.xml FuCabinet processing release: 1.2.3 FuCabinet failed to verify payload firmware.bin: checksums were required, but none supplied This indicates that the `jcat-tool self-sign firmware.jcat firmware.bin --kind sha256` step was missed as the JCat file does not have any supported checksums. fwupd-2.0.10/docs/supermicro-license.md000066400000000000000000000015571501337203100200040ustar00rootroot00000000000000--- title: Supermicro BMC License --- ## Introduction While all newer (some X10, all X11 and H12 series) mainboard for Supermicro support Redfish some features are only available after buying them as additional feature. One of those are applying BIOS and BMC firmware updates. ## Details If you want to update your Supermicro board via redfish using fwupd you will need either the [SFT-OOB-LIC](https://store.supermicro.com/out-of-band-sft-oob-lic.html) or the [SFT-DCMS-Single](https://store.supermicro.com/supermicro-server-manager-dcms-license-key-sft-dcms-single.html) license. The license can be installed via redfish by POSTing it to `/redfish/v1/Managers/1/LicenseManager/ActivateLicense`, using the web interface or using the `contrib/upload-smc-license.py` If the license is not installed fwupd will add the FWUPD_DEVICE_PROBLEM_MISSING_LICENSE flag to the device. fwupd-2.0.10/docs/test_task.png000066400000000000000000000151621501337203100163560ustar00rootroot00000000000000‰PNG  IHDRœ©ÆÄ±sRGB®ÎégAMA± üa pHYsÃÃÇo¨dIDATx^ílUºÇŸª×hKo{-Tð"ÐEªPP+×\ fëŠæBÝd ׄ,\,»H—n6㕆ýcS*é% ÔÄ ÃfCˆ—T–°å‡Ao–E~xÏsΙyÏÌ;3ïÌûc:´ßO3éü>çÌ;ç;Ïsæ}Ÿ§hìØ±?“‹ŸV«¼þó4nÜ8ºví]¾|Y®€(ܤÿgÄŸÒÒR9ÑdKšðXão5j]¹rE¯€èd´xÜBtË-·ÐÕ«WõDÇWxLÁ1狊ŠôdGä1È•ÐÂù"íuºeÙ˜ÿ­‰™0a?^λ)¯~†J¦=OôO£ôH'’ðð4qâD_áù×gÿ‡~ì}‹®žû»^éä×Õ–D ŒñbÂˆÏ @GG‡ž`xáÄ„;@ì@xÅohUÇZj™¥¦Œ áy”ZÞê Uz1fµÐÚŽUB|h\Eoµˆ’AÀâÄN~2ñ›èûíKô’lÜw˜š_}_¯pîFÿâ4Íx`]úb=½GÏ‹¿÷hùº½j¶:„ÕòÞïÚi/Ï¿PF»wÍ}j‚ÜÌÇØûÚðyç’ÚCðý~ZÏÇ;Ö_¢ýï,§ö=r]¶––>P¬Nn§æÃ÷Q‡.ƒq—ãØ_pbG3­î%ÿwͽ[¯´Ëe¼ÊÖí×õPÇžÛ'Pss³Ü€á@¼O÷jÕ;:hí2?‡¤˜fÜ!ÄIt´tñbÍe1û7¿³Ÿèç=ÆHÞ§ÕÍëiÿ÷Jšeçg×k./ËcÒä´%ré䣴ž×óÄBÉuç¿D'h»GÝö®[NÍ;„H°¸ˆí,:,.÷‘-yQ>Í çe»]e7§Ï‚…lî¿ð¹Vë5 âwµ¸‹ÎÆ– Pú˜‹xúï𳈼B`YP{öÒÑ̲0‚hœ+d`?m—!ÇŽ¦q,Z'ÏÓ¥Qe) )k„àÙÖÝ^Ú{ô’=ÿÍ?ˆFWúˆïL!|œ¦í¶uÀðbÈÆxö®{OZ ž ¬ŒêС5ƒ– ñcìèXJ3FiÑÚÓNËÙ}“ës«[.êüWìýW×ÓÑÉKåz§õ',>áfžØ±ZÈÓ!ÇàŽø¼°~ØÍÂÆc7ÒÕIMÊEhˬ™(Ë7UrÜçŽÝö¹×aY<Ì^jÿ¯W”²üØâÛO£ŸÂku0|‰Wxxp¹ã>:,;bú¸†{NSñäGuÇ”Zg› t¦wÏÍüz=·kÂÅté,3¢ “½jîávd‹ë4Íxâ†'ñ.Gµpº·ËAYå +iðfƒ°0öœ®8´`Þ§Õï°e¡Ü 9Y–H½î…Étô]gá‚í>9Aº`žƒãF]YÐÞu;~@¹S\÷²X5Wß)²Ê˜KÛÓÒŵZÿ ñÁDÁ𣨦¦F¾7·^Ÿ3îWèׯ_·ÿ———gÿ:d‹»j Šf·ö¦„çg!0ׯéé*]¿ú]¿&¦«—Åô#]¿ò#Ýt°Â30Ü(š8q¢mêHñÑXÖŽ{â¼éžxð€áƽNáè€á„;@ìäWx®|O·”ÿB/€7y\F&Q@ò*<Œñbˆ v <€Øðbˆ vòú=ž«¥¿ k·g½Y¯€tòfñHѹ},D‘›×¯_ÿ‡“'OÒéÓ§é±Ç£—^z‰.\¸@Ï=÷-\¸êëëiêÔ©´wï^š={6={–._¾¬Oq¥¬Zˆ<7@fnºrå Ý{ï½rÿ_º¤â;–Þ}÷]jmm¥öövºÿþû顇’Û<¥ÉMléLžwzcÛƒO¯û*$*¦i¿vퟫީɴð¬6ªÉÛ¢²÷i^A·êud"QÂób]9í_»ˆÆÏÓÖS4e~½Á^i£UÕÇh5¯ç)ÍÂYJ]>þoë"š¶ì/!ö÷¡d*Í‘LwfahT>mŒßŒ›ÑUóÿçxðég”Uæ;V“-ã¨DŠVЉ+¹õé?Ië(!„jŸSô™´G¢„çÏËZiÁf½ðÚ:¢géè9ºPZNêž›1´eûtç¾MôË×ô*Ÿý_^¨…) ìJDîLÅD'¶ÐÇo/¡Ï¾QkJ[Ÿþ7%:\¶8ÇÇ¢Óbqô„û k‚]*±Mlÿü£Õtäí-tP~ËA¯äþlÇ«s¥ê\L%wÊ v?åv·wMÑBxüÓˆÔ`Ä“°¯ ËÅrör 4áHŽð÷hí¾rí‰i¥,žWÚ´û%¦å“hÿZ÷ë÷¿Ð‚¹ÂBš(ć­›Œûgæü–]ÊÕÐÄ–$‰5†“W„à}®Çu2Ân¡ÏùN}~J­‹ˆíN1ÚÄ_+H{/ê{PZ_,z.W €†ö{<€I—# vò'Êigc«è* ùÄ›©í¾¡­'ŸÃRa±:@Óͧº´\>¡Ù~Og²ÒpYC¦¦ž¶=ÔWÝ ­5®ŸKY¶Çõe79Ê2-qÜ6¢†š²mÜÞ&êL‰.×Aa§´Âø¸z:·Otæ™Uv}|¯‘l‘Ì9ç°ê¸- “Ô¼ÙÆôó’Órò²ùüº- ¾MD]lõ„¨¿a!™õJ}Æâxc»iñ„¿W>s0$$kpYÜÄ-Õ}öÓ¹‡hCkØ n`‡¥Â7?ÕÛ©÷¢¸µ„ûИÁ]ª›UM%ÇÜ€\F- ð¹díBdZ¨Í°†JfÖ‹¥¶÷«¢†5ü\ß--¸žc~õàŽ¨Æ)ÔyEgr¸&™(¡Ú ѱű²Ãù^£ppgmG¥µÑu^Õ‰£]co<ê?sÀ¶r×õQµ¶6®h¤ö}ƒR Ôg@¤{$ è sª¨WêæÞøe?•Tp'í§sK¨¯èÌìþ„FX[­®t°ˆf’q¾ÝôIß UŽ­è?Gƒ¥åžãBQ¨kmíí£Oì1úo3Ä@´e§ñé‘ÜÝ|Ü+`(IÜëôªyÝÔÝ­'¶ dçfAá7j½i…D!SM&îŒ~âÂÛôlFÎ|mw|à}Â",e©ãÙU‘×Etâ%ìÊõÇh&5¤ÎÇVH e4HçÜ—ùŒ¿ˆò^…%aÂÃþ¾e"ëɾ‰­S¾©6ˆ'ž\šÝ{úhpÒôÀ•&LUåT’Ñp CÝØJ=— A×( <æá:ÞéjUË,@+˯XçÊX—tk¤ª¢$‹ùÂÝ+ ð$HxØì'ª]œé†2¥y<Åç)§Mú÷Ó|q›Ü_ Sƒq¬6ç¿´‚ìè:@ý¥µÆ›3vw¬UQî·TR=[·Y”¹ØxÕŸFØkäÇF:`M'·+åÚUÍ3>ƒ9+©~R?ðq½Éǽ†’DY<»Û–PÏ™Zj±ÌgÛTæ·#Öºªîk×c1z†ÍêOh5xY©Ý =ÕPçbWc]/UÚæ{Äïù"ž¾ŽóN§æOWõ’Õæ&QþŠ?þ×(W´SoEÊ5êîÖ`{œµÆa¢]ãLpýŸA¦7>D¿W@’À7—‡~³äx…À#qƒËÃáZ4Í$êÛÑ#X<‡MçÏ¢ÿl€á„;pµ±áÄ„;@ì@x±áÄ„;Ôo¬¬ßq´Á(ÎÀ‚½ ¢—ËdQ¶ ÛIéÑ¡O³!¨^éߢ¶(|øhe¸Ãœ‚áÅXÇ‚bŠwFï¡þIõ:–ŽŠëÜÙöÇH×ÄDÖ=Ž ð‹§íé%òm{ª¨š:å9Ùªòm H ÑÉâ önZPRtùY"×Ô»ŒN(ƒ|UÓìÀŽ%:1ç¢òr¹Üâw}B}+i¼<Ÿ>nþJQïJg¹‘)Px#³m­uµ¦>Ù=Ÿý´ÓvÓÚCâ\-iº[7k!ƒ½sÄAýÔMEüË5ù}ípãvÓ×gB¦èŒ¾.W©eÝã|,lµT-\6§û˜ ïã±\\å¦Ú#÷Ì3>m‰!aÂ3´ÁÞsDnY#u4¾"]̼°].wÿp Ðòd[ l +ÎßU‹B‚À§Á¢£Þ„©2”»–ÚA‚„'¼ÕÓÔÓ"ÒnSZ°w›\‘s uñ¤5ë/]%3ÇTÚušg¼êç@ñŽô&‹¨mžp±¶ kiŸ¸nó#Ù".â _E奆Ï™MÕ!,žhñC¶ )‰²xâö.ŸÚÆùåëu=>-yzÙ|~Gý]©ƒ3Â.×1=/bäo•ÅÛ@•:Áàî¶NÀ] Ägœ=ž ðZ\-7hq9 „±x"Ä÷m H ˆ@ˆÄ .†?@ì@x±áÄ„;@ì@x±áÄ„;@ì@x±áÆ8ãŽPdÀ7ý#Q&¿[zeB0tY&ÌZp ¿_Â'›ô¶h8˜|”_ç3ükø€_õ§—Å1œÜq´}Ÿ}PÄCbñ Ë„‚Ϫ‡ Ç!ŒE’àp"ªFf^Ž*:!1¯[*²!¸‘Hœ«ÅO4;ŽŠé&H3ÙŠ¯Âæ2Çèqg:ÆÇ'-Ë„Áî¶Ô¯ƒlÉ:¬Y)Û’:‡“(U/.Ï™E÷SǸ·ù×-uŒïk_Ç´kïz3f}¸ž²ÝîV‡{õ€æós3ê(ëîUçà6«k·RÕÝú|sºÀ$YÂãÊh0²Lø2©š¨‹ÏÃmU¿<³Mp@|;:Ÿ`ñtªòhgöuóºöañ¯·¬ BY»Òr2ò}…waëhv…qßä)aÉÌr:À生¯ë:ävd“ á7éˆÌ2¡X´¦Á)Çv¦,³ lÕGº¨F´áK·4äR·°×Þƒ€zs8SÈ?:%3[´ÕaYâs_á¼oòÁà¾ûó¨k­Ÿñ™¸®;ˆFâ\-éºX7Õ0Ï2av ùô Hî›mBÅ–â(Ä´^X5vGweŒ\¯½_½»Z©½¯Zm‹h98ÇxôƒÂ ÑêJœ7éz8´;¸?K„gde™pt LY²Mð¾ªf‘t'©ï}½ÜdŸ#§kPokPZ P.®‘õ&Ì*ƒÝµà<ž0°- ž‘že"€Àl¹}:5TÝÙã%;™êÆ×Ôp)EGn˜¤çDt»2Õ[“‹ÛÅÔ­$:óµ-¸ìnf&l›üàÊOÖSÀ$ÊâñY&|‡g¶ ånUUø‹\pÝŒ¶ð¶šF¶ ¿kÿz;ÞBñÛF«.FF‰°n•eÃ:ßt!*™ j³ìš›2xB ¬A– @ì$np0üðbˆ v <€Øðbˆ v <€ØðbˆOÁàWFß‘(d°´„´!AuQaQ³ý-=`FH– Ü”‡. ÿkë(p™õt.l†ãÊx±Ê"wÔ _×sY5Òâñ¯Æ›¨3üç`ÅÂÉ˯ð Aúgý>sÂÂS&·sŒt†Äâ‰?˄ȊÃbôÛ1}y¹0¢“%]­ªŽbâ ©ÀSE¸HÝc2«ÈÌ–Ð!7@aHœ«åˆÓ’Ï,˜ÙÒ²˜åŠ2‰ú™f¶Yß ­‹Dùþî€YN6¦ºßñi׌­™;ŒŸåàȶ§®‡»|Y®WVÇgå,ŸÏafoø½\6ëçã¢:ê¢÷Yl”:6ÎFêÙ— gë{¿ ‚¶ÙðµÛ!dÑH–ðˆ±PY&üà›Ë?û‚«\aÕ›Éä\õí¤zO’‘åØYÚ©¯:ÚS×÷xW¤ëÄ–“aMæÃ’t–”‚¯Y- X™%<Újfoø£g– #¨º/%T+Ä©S–Ñ*Òa¾÷[†m,†ó*e¸ÞDYÎ7 žÂg™H'8ûBZfQ·Nñ´T¤×WFÂB˜Ž.g«å0©0«áƒÈߎ³Ívà^®&ÏŒ ‚°áIÕ5ë1>é¯&e1˜ÙÂeÉðbÐøÜ8#…»žB,šÄµìÛC÷[нh1[ ,â.gCâ\-;%OyÎ2áMpöGJœ4ÂKgÄÚ›ÉÜy#gÂçxÑ9–Øá\]nb&<±·ÛÂê"BVˆ´kÆâè+ZF– Qûé¡c\;ál þ(—S^;iÁ¦ÄÂû~˼­df­K`A&<…Ë2áOpö·8pÖ‰n+¬ŠÊ}\-¶ÚRœzÊ”YÂAÀñÖ t“Ì…Í ‘&¨UåTbcw³qk/U²E䓌0wÌkgZ(A÷[Ð6±UXm½îE‚„§ÀY&< ξ RâÔ§DN˜éõv&•B¬³Ž•n†žw¢žêÙg· y|.nW‚Ü.uÍÌñí¶¹Oœè¯b:µÕTîd¡ ºßÂÜ‹ýÒ ç7dŸè$Êâ)t– /³/H7ÆÈ, ê²ÓpEÜõm¢>c<\N»#‚^vo|_¬ÞªÈIºúÕ»¨»Jç“Ã[-Çž ¬|Í™%Â|ÏŠ;ze`–ŒBà¿oK¡²h'fÌú¡22A–‰ˆðëàé_úu$Èétß»‰ [¤/.‚šÄ .'a]4 ~¦å?!á·v´¢3b€ÅOC~Ï‚Sƒ“ü”v¼~æï³ 3lArÏÂýt$ vàjbˆ¼ºZeeeT\\LEEEz ¤ƒ1@ìÀÕÄ„3Dÿý¹þyŸàpIEND®B`‚fwupd-2.0.10/docs/tutorial.md000066400000000000000000001227211501337203100160340ustar00rootroot00000000000000--- title: Plugin Tutorial --- ## Introduction At the heart of fwupd are plugins that gets run at startup, when devices get hotplugged and when updates are done. The idea is we have lots of small plugins that each do one thing, and are ordered by dependencies against each other at runtime. Using plugins we can add support for new hardware or new policies without making big changes all over the source tree. There are broadly 3 types of plugin methods: - **Mechanism**: Upload binary data into a specific hardware device. - **Policy**: Control the system when updates are happening, e.g. preventing the user from powering-off. - **Helpers**: Providing more metadata about devices, for instance handling device quirks. A plugin only needs to define the vfuncs that are required, and the plugin name is taken automatically from the GType. /* fu-foo-plugin.h * * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuFooPlugin, fu_foo_plugin, FU, FOO_PLUGIN, FuPlugin) /* fu-foo-plugin.c * * Copyright Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-foo-plugin.h" struct _FuFooPlugin { FuPlugin parent_instance; gpointer proxy; }; G_DEFINE_TYPE(FuFooPlugin, fu_foo_plugin, FU_TYPE_PLUGIN) static gboolean fu_foo_plugin_startup(FuPlugin *plugin, FuProgress *progress, GError **error) { FuPluginData *data = fu_plugin_get_data(plugin); self->proxy = create_proxy(); if(self->proxy == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to create proxy"); return FALSE; } return TRUE; } static void fu_foo_plugin_init(FuFooPlugin *self) { } static void fu_foo_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_plugin_add_rule(plugin, FU_PLUGIN_RULE_RUN_BEFORE, "dfu"); } static void fu_foo_finalize(GObject *obj) { FuFooPlugin *self = FU_FOO_PLUGIN(obj); destroy_proxy(self->proxy); G_OBJECT_CLASS(fu_foo_plugin_parent_class)->finalize(obj); } static void fu_foo_plugin_class_init(FuFooPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->constructed = fu_foo_constructed; object_class->finalize = fu_foo_finalize; plugin_class->startup = fu_foo_plugin_startup; } We have to define when our plugin is run in reference to other plugins, in this case, making sure we run before the `dfu` plugin. For most plugins it does not matter in what order they are run and this information is not required. ## Creating an abstract device This section shows how you would create a device which is exported to the daemon and thus can be queried and updated by the client software. The example here is all hardcoded, and a true plugin would have to derive the details about the `FuDevice` from the hardware, for example reading data from `sysfs` or `/dev`. static gboolean fu_foo_plugin_coldplug(FuPlugin *plugin, FuProgress *progress, GError **error) { g_autoptr(FuDevice) dev = NULL; fu_device_set_id(dev, "dummy-1:2:3"); fu_device_add_instance_id(dev, "2d47f29b-83a2-4f31-a2e8-63474f4d4c2e"); fu_device_set_version(dev, "1.2.3"); fu_device_get_version_lowest(dev, "1.2.2"); fu_device_get_version_bootloader(dev, "0.1.2"); fu_device_add_icon(dev, "computer"); fu_device_add_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE); fu_plugin_device_add(plugin, dev); return TRUE; } static void fu_foo_plugin_class_init(FuFooPluginClass *klass) { … plugin_class->coldplug = fu_foo_plugin_coldplug; … } This shows a lot of the plugin architecture in action. Some notable points: - The device ID (`dummy-1:2:3`) has to be unique on the system between all plugins, so including the plugin name as a prefix is probably a good idea. - The GUID value can be generated automatically using `fu_device_add_instance_id(dev,"some-identifier")` but is quoted here explicitly. The GUID value has to match the `provides` value in the `.metainfo.xml` file for the firmware update to succeed. - Setting a display name and an icon is a good idea in case the GUI software needs to display the device to the user. Icons can be specified using a full path, although icon theme names should be preferred for most devices. - The `FWUPD_DEVICE_FLAG_UPDATABLE` flag tells the client code that the device is in a state where it can be updated. If the device needs to be in a special mode (e.g. a bootloader) then the `FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER` flag can also be used. If the update should only be allowed when there is AC power available to the computer (i.e. not on battery) then `FWUPD_DEVICE_FLAG_REQUIRE_AC` should be used as well. There are other flags and the API documentation should be used when choosing what flags to use for each kind of device. - Setting the lowest allows client software to refuse downgrading the device to specific versions. This is required in case the upgrade migrates some kind of data-store so as to be incompatible with previous versions. Similarly, setting the version of the bootloader (if known) allows the firmware to depend on a specific bootloader version, for instance allowing signed firmware to only be installable on hardware with a bootloader new enough to deploy it. ### Setting the device version Although the version can be set easily as a string using `fu_device_set_version()` directly, it is more flexible to tell fwupd what the *version format* should be, and to allow the daemon to convert it to a string internally. This also means that if we get the version format from a quirk file, or from metadata, or even if it changes at runtime -- the correct string version is used at all times. static gchar * fu_foo_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint24(version_raw, FWUPD_VERSION_FORMAT_TRIPLET); } static void fu_foo_device_class_init(FuFooDeviceClass *klass) { … device_class->convert_version = fu_foo_device_convert_version; … } ## Mechanism Plugins Although it would be a wonderful world if we could update all hardware using a standard shared protocol this is not the universe we live in. Using a mechanism like DFU or UpdateCapsule means that fwupd will just work without requiring any special code, but for the real world we need to support vendor-specific update protocols with layers of backwards compatibility. When a plugin has created a device that is `FWUPD_DEVICE_FLAG_UPDATABLE` we can ask the daemon to update the device with a suitable `.cab` file. When this is done the daemon checks the update for compatibility with the device, and then calls the vfuncs to update the device. static gboolean fu_foo_plugin_write_firmware(FuPlugin *plugin, FuDevice *dev, GBytes *blob_fw, FuProgress *progress, FwupdInstallFlags flags, GError **error) { gsize sz = 0; guint8 *buf = g_bytes_get_data(blob_fw, &sz); /* write 'buf' of size 'sz' to the hardware */ return TRUE; } static void fu_foo_plugin_class_init(FuFooPluginClass *klass) { … plugin_class->write_firmware = fu_foo_plugin_write_firmware; … } It's important to note that the `blob_fw` is the binary firmware file (e.g. `.dfu`) and **not** the `.cab` binary data. If `FWUPD_INSTALL_FLAG_FORCE` is used then the usual checks done by the flashing process can be relaxed (e.g. checking for quirks), but please don't brick the users hardware even if they ask you to. ## Policy Helpers For some hardware, we might want to do an action before or after the actual firmware is squirted into the device. This could be something as simple as checking the system battery level is over a certain threshold, or it could be as complicated as ensuring a vendor-specific GPIO is asserted when specific types of hardware are updated. static gboolean fu_foo_plugin_prepare(FuPlugin *plugin, FuDevice *device, GError **error) { if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_REQUIRE_AC && !on_ac_power()) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_AC_POWER_REQUIRED, "Cannot install update " "when not on AC power"); return FALSE; } return TRUE; } static gboolean fu_foo_plugin_cleanup(FuPlugin *plugin, FuDevice *device, GError **error) { return g_file_set_contents("/var/lib/fwupd/something", fu_device_get_id(device), -1, error); } static void fu_foo_plugin_class_init(FuFooPluginClass *klass) { … plugin_class->prepare = fu_foo_plugin_prepare; plugin_class->cleanup = fu_foo_plugin_cleanup; … } ## Detaching to bootloader mode Some hardware can only be updated in a special bootloader mode, which for most devices can be switched to automatically. In some cases the user to do something manually, for instance re-inserting the hardware with a secret button pressed. Before the device update is performed the fwupd daemon runs an optional `update_detach()` vfunc which switches the device to bootloader mode. After the update (or if the update fails) an the daemon runs an optional `update_attach()` vfunc which should switch the hardware back to runtime mode. Finally an optional `update_reload()` vfunc is run to get the new firmware version from the hardware. The optional vfuncs are **only** run on the plugin currently registered to handle the device ID, although the registered plugin can change during the attach and detach phases. static gboolean fu_foo_plugin_detach(FuPlugin *plugin, FuDevice *device, FuProgress *progress, GError **error) { if (hardware_in_bootloader) return TRUE; return _device_detach(device, progress, error); } static gboolean fu_foo_plugin_attach(FuPlugin *plugin, FuDevice *device, FuProgress *progress, GError **error) { if (!hardware_in_bootloader) return TRUE; return _device_attach(device, progress, error); } static gboolean fu_foo_plugin_reload(FuPlugin *plugin, FuDevice *device, GError **error) { g_autofree gchar *version = _get_version(plugin, device, error); if (version == NULL) return FALSE; fu_device_set_version(device, version); return TRUE; } static void fu_foo_plugin_class_init(FuFooPluginClass *klass) { … plugin_class->detach = fu_foo_plugin_detach; plugin_class->attach = fu_foo_plugin_attach; plugin_class->reload = fu_foo_plugin_reload; … } ## The Plugin Object Cache The fwupd daemon provides a per-plugin cache which allows objects to be added, removed and queried using a specified key. Objects added to the cache must be `GObject`s to enable the cache objects to be properly refcounted. ## Debugging a Plugin If the fwupd daemon is started with `--plugin-verbose=$plugin` then the environment variable `FWUPD_$PLUGIN_VERBOSE` is set process-wide. This allows plugins to detect when they should output detailed debugging information that would normally be too verbose to keep in the journal. For example, using `--plugin-verbose=logitech_hidpp` would set `FWUPD_LOGITECH_HID_VERBOSE=1`. ## Using existing code to develop a plugin It is not usually possible to share a plugin codebase with firmware update programs designed for other operating systems. Matching the same rationale as the Linux kernel, trying to use one code base between projects with a compatibility shim layer in-between is real headache to maintain. The general consensus is that trying to use a abstraction layer for hardware is a very bad idea as you're not able to take advantage of the platform specific helpers -- for instance quirk files and the custom GType device creation. The time the vendor saves by creating a shim layer and importing existing source code into fwupd will be overtaken 100x by upstream maintenance costs longer term, which isn't fair. In a similar way, using C++ rather than GObject C means expanding the test matrix to include clang in C++ mode and GNU g++ too. It's also doubled the runtime requirements to now include both the C standard library as well as the C++ standard library and increases the dependency surface. Most rewritten fwupd plugins at up to x10 smaller than the standalone code as they can take advantage of helpers provided by fwupd rather than re-implementing error handling, device quirking and data chunking. ## General guidelines for plugin developers ### General considerations When adding support for a new device in fwupd some things need to be evaluated beforehand: - how the hardware is discovered, identified and polled. - how to communicate with the device (USB? file open/read/write?) - does the device need to be switched to bootloader mode to make it upgradable? - about the format of the firmware files, do they follow any standard? are they already supported in fwupd? - about the update protocol, is it already supported in fwupd? - Is the device composed of multiple different devices? Are those devices enumerated and programmed independently or are they accessed and flashed through a "root" device? In most cases, even if the features you need aren't implemented yet, there's already a plugin that does something similar and can be used as an example, so it's always a good idea to read the code of the existing plugins to understand how they work and how to write a new one, as no documentation will be as complete and updated as the code itself. Besides, the mechanisms implemented in the plugin collection are very diverse and the best way of knowing what can be done is to check what is already been done. ### Leveraging existing fwupd code Depending on how much of the key items for the device update (firmware format, update protocol, transport layer) are already supported in fwupd, the work needed to add support for a new device can range from editing a quirk file to having to fully implement new device and firmware types, although in most cases fwupd already implements helper code that can be extended. #### If the firmware format, update protocol and device communication are already supported This is the simplest case, where an existing plugin fully implements the update process for the new device and we only have to let fwupd know that that plugin should be used for our device. In this case the only thing to do is to edit the plugin quirk file and add the device identifier in the format expected by the plugin together with any required options for it (at least a "Plugin" key to declare that this is the plugin to use for this device). Example: #### If the device type is not supported Then we have to take a look at the existing device types and check if there's any of them that have similarities and which can be partially reused or extended for our device. If the device type is derivable and it can support our new device by implementing the proper vfuncs, then we can simply subclass it and add the required functionalities. If not, we'll need to study what is the best way to reuse it for our needs. If a plugin already implements most of the things we need besides the device type, we can add our new device type to that plugin. Otherwise we should create a plugin that will hold the new device type. The core fwupd code contains some basic device types (such as [FuUdevDevice](https://github.com/fwupd/fwupd/blob/main/libfwupdplugin/fu-udev-device.c), [FuUsbDevice](https://github.com/fwupd/fwupd/blob/main/libfwupdplugin/fu-usb-device.c), [FuBluezDevice](https://github.com/fwupd/fwupd/blob/main/libfwupdplugin/fu-bluez-device.c)) that can be used as a base type for most devices in case we have to implement our own device access, identification and communication from scratch. If the device is natively visible by the OS, most of the time fwupd can detect the device connection and disconnection by listening to udev events, but a supported device may also be not directly accessible from the OS -- for example, a composite device that contains an updatable chip that's connected through I2C to a USB hub that acts as an interface. In that case, the device discovery and enumeration must be programmed by the developer, but the same device identification and management mechanisms apply in all cases. See the "Creating a new device type" and "Device identification" below for more details. #### If the firmware type is not supported Same as with the new device type, there could be an existing firmware type that can be used as a base type for our new type, so first of all we should look for firmware types that are similar to the one we're using. Then, choosing where to define the new type depends on whether there's already a plugin that implements most of the functionalities we need or not. ### Example: extending a firmware type Our firmware files are Intel HEX files that have optional vendor-specific sections at fixed addresses, this is not supported by any firmware type in fwupd out of the box but the [FuIhexFirmare](https://github.com/fwupd/fwupd/blob/main/libfwupdplugin/fu-ihex-firmware.c) class parses and models a standard Intel HEX file, so we can create a subclass of it for our firmware type and override the parse method so that it calls the method from the parent class, which would parse the file, and then we can get the data with `fu_firmware_get_bytes()` and do the rest of the custom parsing. Example: ### Example: extending a device type Communication with our new device is carried out by doing read/write/ioctl operations on a device file, but using a custom protocol that is not supported in fwupd. For this type of device we can create a new type derived from `FuUdevDevice`, which takes care of discovering this type of devices, possibly using a vendor-specific protocol, as well as of opening, reading and writing device files, so we would only have to implement the protocol on top of those primitives. (Example: `fu_logitech_hidpp_runtime_bolt_poll_peripherals()` in ) The process would be similar if our device was handled by a different backend (USB or BlueZ). ### Creating a new plugin The bare minimum a plugin should have is a `constructed` function that defines the plugin characteristics such as the device type and firmware type handled by it, the build hash and any plugin-specific quirk keys that can be used for the plugin. static void fu_foo_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_plugin_add_device_gtype(plugin, FU_TYPE_STEELSERIES_MOUSE); fu_plugin_add_device_gtype(plugin, FU_TYPE_STEELSERIES_GAMEPAD); } static void fu_foo_plugin_class_init(FuFooPluginClass *klass) { plugin_class->init = fu_foo_plugin_constructed; } ### Creating a new device type Besides defining its attributes as a data type, a device type should implement at least the usual `init`, `finalize` and `class_init` functions, and then, depending on its parent type, which methods it overrides and what it does, it must implement a set of device methods. These are some of them, the complete list is in [libfwupdplugin/fu-device.h](https://github.com/fwupd/fwupd/blob/main/libfwupdplugin/fu-device.h). #### to_string Called whenever fwupd needs a human-readable representation of the device. #### probe The `probe` method is called the first time a device is opened, before actually opening it. The generic probe methods implemented in the base device types (such as USB/udev) take care of basic device identification and setting the non-specific parameters that don't need the device to be opened or the interface claimed (vendor id, product id, guids, etc.). The device-specific probe method should start by calling the generic method upwards in the class tree and then do any other specific setup such as setting the appropriate device flags. #### open Depending on the type of device, opening it means different things. For instance, opening a udev device means opening its device file. If there's no interface-specific `open` method, then opening a device simply calls the `probe()` and `setup()` methods (the `open()` method would be called in between if it exists). #### setup Sets parameters on the device object that require the device to be open and have the interface claimed. USB/udev generic devices don't implement this method, this is normally implemented for each different plugin device type if needed. #### prepare If implemented, can be used to put the device into a mode that makes updating possible or anything else that has to be done to a device before updating it is possible. #### prepare_firmware If implemented, this takes care of decompressing or parsing the firmware data. For example, to check if the firmware is valid, if it's suitable for the device, etc. It takes a stream of bytes (`GBytes`) as a parameter, representing the raw binary firmware data. It should create the firmware object and call the appropriate method to load the firmware. Otherwise, if it's not implemented for the specific device type, the generic implementation in [libfwupdplugin/fu-device.c](https://github.com/fwupd/fwupd/blob/main/libfwupdplugin/fu-device.c):`fu_device_prepare_firmware()` creates a firmware object loaded with a provided image. #### detach Implemented if the device needs to be put in bootloader mode before updating, this does all the necessary operations to put the device in that mode. fwupd can handle the case where a device needs to be disconnected to do the mode switch if the device has the `FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG` flag. #### attach The inverse of `detach()`, to configure the device back to application mode. #### reload If implemented, this is called after the device update if it needs to perform any kind of post-update operation. #### write_firmware Writes a firmware passed as a raw byte stream. The firmware parsing and processing is done by the firmware object, so that when this method gets the blob it simply has to write it to the device in the appropriate way following the device update protocol. #### read_firmware Reads the firmware data from the device without any device-specific configuration or serial numbers. This is meant to retrieve the current firmware contents for verification purposes. The data read can then be output to a binary blob using `fu_firmware_write()`. #### set_progress Informs the daemon of the expected duration percentages for the different phases of update. The daemon runs the `->detach()`, `->write_firmware()`, `->attach()` and `->reload()` phases as part of the engine during the firmware update (rather than being done by plugin-specific code) and so this vfunc informs the daemon how to scale the progress output accordingly. For instance, if your update takes 2 seconds to detach into bootloader mode, 10 seconds to write the firmware, 7 seconds to attach back into runtime mode (which includes the time required for USB enumeration) and then 1 second to read the new firmware version you would use: fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 10, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 45, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 40, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 5, "reload"); If however your device does not require `->detach()` or `->attach()`, and `->reload()` is instantaneous, you still however need to include 4 steps: fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); If the device has multiple phases that occur when actually in the write phase then it is perfectly okay to split up the `FuProgress` steps in the `->write_firmware()` vfunc further. For instance: fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 5, "wait-for-idle"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 90, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 5, "reset"); It should be noted that actions that are required to be done *before* the update should be added as a `->prepare()` vfunc, and those to be done after in the `->cleanup()` as the daemon will then recover the hardware if the update fails. For instance, putting the device back into a *normal runtime power saving* state should always be done during cleanup. ### Creating a new firmware type The same way a device type implements some methods to complete its functionality and override certain behaviors, there's a set of firmware methods that a firmware class can (or must) implement: #### parse If implemented, it parses the firmware file passed as a byte sequence. If the firmware to be used contains a custom header, a specific structured format or multiple images embedded, this method should take care of processing the format and appropriately populating the `FuFirmware` object passed as a parameter. If not implemented, the whole data blob is taken as is. #### write Returns a `FuFirmware` object as a byte sequence. This can be used to output a firmware read with `fu_device_read_firmware()` as a binary blob. #### export Converts a `FuFirmware` object to an xml representation. If not implemented, the default implementation generates an xml representation containing only generic attributes and, optionally, the firmware data as well as the representation of children firmware nodes. When testing the implementation of a new firmware type, this is useful to show if the parsing and processing of the firmware are correct and can be checked with: fwupdtool firmware-parse --plugins #### tokenize If implemented it tokenizes a firmware, breaking it into records. #### build This is the reverse of `export()`, it builds a `FuFirmware` object from an xml representation. #### get_checksum The default implementation returns a checksum of the payload data of a `FuFirmware` object. Subclass it only if the checksum of your firmware needs to be computed differently. ### Generating a skeleton Rather than copy-and-pasting from other plugins, or using the `FuDeviceClass` as a guide we have also provided a script that can generate a plugin skeleton. This skeleton contains all the parts typically needed by a plugin, and plugin developers might find it easier to delete unneeded code rather then trying to copy and paste the correct code from other plugins. To use this, navivate to the root directory and run: ./contrib/create-plugin.py \ --vendor VendorName \ --example ProductName \ --parent Usb \ --author "Your Name" \ --email "your@email.com" ### Device identification A device is identified in fwupd by its physical and logical ids. A physical id represents the electrical connection of the device to the system and many devices can have the same physical id. For example, `PCI_SLOT_NAME=0000:3e:00:0` (see [libfwupdplugin/fu-udev-device.c](https://github.com/fwupd/fwupd/blob/main/libfwupdplugin/fu-udev-device.c):`fu_udev_device_set_physical_id()` for examples) . The logical id is used to disambiguate devices with the same physical id. Together they identify a device uniquely. There are many examples of this in the existing plugins, such as `fu_pxi_receiver_device_add_peripherals()` in Besides that, each device type will have a unique instance id, which is a string representing the device subsystem, vendor, model and revision (specific details depend on the device type). This should identify a device type in the system, that is, a particular device type, model and revision by a specific vendor will have a defined instance id and two of the same device will have the same instance id (see [libfwupdplugin/fu-udev-device.c](https://github.com/fwupd/fwupd/blob/main/libfwupdplugin/fu-udev-device.c):`fu_udev_device_probe()` for examples). One or more GUIDs are generated for a device from its identifying attributes, these GUIDs are then used to match a firmware metadata against a specific device type. See the implementation of the many `probe()` methods for examples. ### Support for BLE devices BLE support in fwupd on Linux is provided by BlueZ. If the device implements the standard HID-over-GATT BLE profile, then communication with the device can be done through the [hidraw interface](https://www.kernel.org/doc/html/latest/hid/hidraw.html). If the device implements a custom BLE profile instead, then it will have to be managed by the `FuBluezBackend`, which uses the BlueZ DBus interface to communicate with the devices. The `FuBluezDevice` type implements device enumeration as well as the basic primitives to read and write BLE characteristics, and can be used as the base type for a more specific BLE device. ### Battery checks If the device can be updated wirelessly or if the update process doesn't rely on an external power supply, the vendor might define a minimum operative battery level to guarantee a correct update. fwupd provides a simple API to define these requirements per-device. [fu_device_set_battery_threshold()](https://github.com/fwupd/fwupd/blob/main/libfwupdplugin/fu-device.c) can be used to define the minimum battery level required to allow a firmware update on a device (10% by default). If the battery level is below that threshold, fwupd will inhibit the device to prevent the user from starting a firmware update. Then, the battery level of a device can be queried and then set with [fu_device_set_battery_level()](https://github.com/fwupd/fwupd/blob/main/libfwupdplugin/fu-device.c). ## Howtos ### How to create a child device fwupd devices can be hierarchically ordered to model dependent and composite devices such as docking stations composed of multiple updatable chips. When writing support for a new composite device the parent device should, at some point, poll the devices that "hang" from it and register them in fwupd. The process of polling and identifying a child device is totally vendor and device-specific, although the main requirement for it is that the child device is properly identified (having physical/logical and instance ids). Then, [fu_device_add_child()](https://github.com/fwupd/fwupd/blob/main/libfwupdplugin/fu-device.c) can be used to add a new child device to an existing one. See `fu_logitech_hidpp_runtime_bolt_poll_peripherals()` in for an example. Note that when deploying and installing a firmware set for a composite device, there might be firmware dependencies between parent and child devices that require a specific update ordering (for instance, child devices first, then the parent). This can be modeled by setting an appropriate firmware priority in the firmware metainfo or by setting the `FU_DEVICE_PRIVATE_FLAG_INSTALL_PARENT_FIRST` device flag. ### How to add a delay In certain scenarios you may need to introduce small controlled delays in the plugin code, for instance, to comply with a communications protocol or to wait for the device to be ready after a particular operation. In this case you can insert a delay in microseconds with `g_usleep` or a delay in milliseconds that shows a progress bar with `fu_device_sleep` or `fu_device_sleep_full`. Note that, in both cases, this will stop the application main loop during the wait, so use it only when necessary. ### How to define private flags Besides the regular flags and internal flags that any device can have, a device can define private flags for specific uses. These can be enabled in the code as well as in quirk files, just as the rest of flags. To define a private flag: 1. Define the flag value. This is normally defined as a macro that expands to a binary flag, for example: `#define MY_PRIVATE_FLAG (1 << 2)`. Note that this will be part of the ABI, so it must be versioned 1. Call `fu_device_register_private_flag` in the device init function and assign a string identifier to the flag: `fu_device_register_private_flag(FU_DEVICE (self), MY_PRIVATE_FLAG);` You can then add it to the device programmatically with `fu_device_add_private_flag`, remove it with `fu_device_remove_private_flag` and query it with `fu_device_has_private_flag`. In a quirk file, you can add the flag identifier to the Flags attribute of a device (eg. `Flags = myflag,is-bootloader`) ### How to make fwupd wait for a device replug Certain devices require a disconnection and reconnection to start the update process. A common example are devices that have two booting modes: application or runtime mode, and bootloader mode, where the runtime mode is the normal operation mode and the bootloader mode is exclusively used to update the device firmware. It's common for these devices to require some operation from fwupd to switch the booting mode and then to need a reset to enter bootloader mode. Often, the device is enumerated differently in both modes, so fwupd needs to know that the same device will be identified differently depending on the boot mode. The common way to do this is to add the `FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG` flag in the device before its detach method returns. This will make fwupd wait for a predetermined amount of time for the device to be detected again. Then, to inform fwupd about the two identities of the same device, the `CounterpartGuid` key can be used in a device entry to match it with another defined device (example: ). ### Inhibiting a device If a device becomes unsuitable for an update for whatever reason (see "Battery checks" above for an example), a plugin can temporarily disable firmware updates on it by calling [fu_device_inhibit()](https://github.com/fwupd/fwupd/blob/main/libfwupdplugin/fu-device.c). The device will still be listed as present by `fwupdmgr get-devices`, but fwupd won't allow firmware updates on it. Device inhibition can be disabled with [fu_device_uninhibit()](https://github.com/fwupd/fwupd/blob/main/libfwupdplugin/fu-device.c). Note that there might be multiple inhibits on a specific device, the device will only be updatable when all of them are removed. ## Debugging tips The most important rule when debugging is using the `--verbose` and duplicate `--verbose` flag when running fwupd or fwupdtool. ### Adding debug messages The usual way to print a debug message is using the `g_debug` macro. Each relevant module will define its own `G_LOG_DOMAIN` to tag the debug traces accordingly. See and for more information. ### Inspecting raw binary data The `fu_dump_full` and `fu_dump_raw` functions implement the printing of a binary buffer to the console as a stream of bytes in hexadecimal. See `libfwupdplugin/fu-common.c` for their definitions, you can find many examples of how to use them in the plugins code. ## The rustgen Helper The rustgen script generates C source files that allow parsing, modifying and querying a packed structure or enumeration. This functionality is provided as parsing untrusted structured data from devices or firmware files is something fwupd does *a lot*, and so it makes sense to abstract out common code for maintainability reasons. It also allows us to force best-practices into the plugins without having to do careful review of buffer reading and writing. Structures support integers of specific widths, arrays, GUIDs, strings, default and constant data of variable size. The generated code is endian safe and if used correctly, is also safe against malicious data. In most cases the structure or enumeration will be defined in a `.rs` file -- which is the usual file extension of Rust programs. This was done as the format is heavily inspired by Rust, and it makes editor highlighting support work correctly. Although these files *look like* Rust files they're *not actually compiled by rustc*, so small differences may be noticeable. #[derive(New, Validate, Parse, Default)] #[repr(C, packed)] struct FuExampleHdr { magic: Guid, hdrver: u8, hdrsz: u16le = $struct_size, payloadsz: u32le, flags: u8, } #[derive(ToString, FromString)] #[repr(u8)] // optional, and only required if using the enum as a struct item type enum FuExampleFamily { Unknown, Sps, Txe = 0x5, Me, Csme, } struct ExamplePacket { family: FuExampleFamily = Csme, data: [u8; 254], } The struct types currently supported are: - `u8`: a `guint8` - `u16le`: little endian `guint16` - `u24`: a 24 bit number represented as a `guint32` - `u32le`: little endian `guint32` - `u64be`: big endian `guint64` - `char`: a `NUL`-terminated string - `Guid`: a GUID - Any `enum` created in the `.rs` file with `#[repr(type)]` - Any `struct` previously created in the `.rs` file Arrays of types are also allowed, with the format `[type; multiple]`, for example: - `buf: [u8; 3] = 0x123456` for a C array of `guint8 buf[3] = {0x12, 0x34, 0x56};` - `val: [u64be; 7]` for a C array of `guint64 val[7] = {0};` - `str: [char; 4] = "ABCD"` for a C array of `gchar buf[4] = {'A','B','C','D'};` -- NOTE: `fu_struct_example_get_str()` would return a `NUL`-terminated string of `ABCD\0`. Additionally, default or constant values can be auto-populated with the `Default` trait: - `$struct_size`: the total struct size - `$struct_offset`: the internal offset in the struct - string values, specified **without** double or single quotes - integer values, specified with a `0x` prefix for base-16 and with no prefix for base-10 - previously specified `enum` values Per-field metadata can also be defined, such as: - ` = `: set as the default value, or for `u8` arrays initialize with a padding byte - ` == `: set as the default, and is **also** verified during unpacking. Default values and padding will be used when creating a new structure, for instance using `fu_struct_example_new()`. ### Building When building a plugin with meson a generator can be used: diff --git a/plugins/example/meson.build b/plugins/example/meson.build @@ -3,7 +3,6 @@ plugin_quirks += files('example.quirk') plugin_builtins += static_library('fu_plugin_example', + rustgen.process('fu-example.rs'), sources: ...which creates the files `plugins/libfu_plugin_example.a.p/fu-example-struct.c` and `plugins/libfu_plugin_example.a.p/fu-example-struct.h` in the build tree. The latter can be included using `#include fu-example-struct.h` in the existing plugin code. ### Structs There are traits that control the generation of struct code. These include: - `New`: for `fu_struct_example_new()`, needed to create new instances - `Validate`: for `fu_struct_example_validate()`, needed to check memory buffers are valid - `Parse`: for `fu_struct_example_parse()`, to create a struct from a memory buffer - `Getters`: for `fu_struct_example_get_XXXX()`, to get access to field values - `Setters`: for `fu_struct_example_set_XXXX()`, to set specific field values `Getters` is implied by `Parse`, and `[Getters,Setters]` is implied by `New`. Regardless of traits used, the header offset addresses are defined, for instance: #define FU_STRUCT_EXAMPLE_OFFSET_MAGIC 0x0 #define FU_STRUCT_EXAMPLE_OFFSET_HDRVER 0x10 #define FU_STRUCT_EXAMPLE_OFFSET_HDRSZ 0x11 #define FU_STRUCT_EXAMPLE_OFFSET_PAYLOADSZ 0x13 #define FU_STRUCT_EXAMPLE_OFFSET_FLAGS 0x17 Any elements defined as a typed array (e.g. `[u8; 16]`) will also have the element size defined in bytes: #define FU_STRUCT_EXAMPLE_SIZE_MAGIC 0x10 If the default has been set (but not a constant value) the default is also defined: #define FU_STRUCT_EXAMPLE_DEFAULT_HDRSZ 24 Finally, the size in bytes of the whole structure is also included: #define FU_STRUCT_EXAMPLE_SIZE 0x18 **NOTE:** constants never have getters or setters defined -- they're constant after all. They are verified during `_validate()` and `_parse()` however. ### Enums There are traits that control the generation of enum code. These include: - `ToString`: for `fu_example_family_to_string()`, needed to create output - `ToBitString`: for `fu_example_family_to_string()`, needed to create output for bitfields - `FromString`: for `fu_example_family_from_string()`, needed to parse input **NOTE:** Enums are defined as a native unsigned type, and should not be copied by reference without first casting to an integer of known width. fwupd-2.0.10/docs/uefi-db.md000066400000000000000000000261641501337203100155100ustar00rootroot00000000000000--- title: UEFI Secure Boot Certificates --- ## Executive Summary On the 11th September 2025 a certificate used for signing boot media will expire. Microsoft will not sign updated boot media with the old key, and that **at least one major OEM** is not going to be shipping the expired key on new hardware. This means that existing install media may not boot on some new laptop, desktop and server devices, and that future updates to boot packages may not boot on old hardware. Microsoft is shipping fixes for select OEMs using Windows Updates automatically. The workaround for Linux is to manually disable secure boot which would be unpopular with anyone that cares about security. Using fwupd is a way that can distribute the updated certificates in Linux. ## Important Terms * `PK`: “*Platform Key*†– one X509 certificate created by the OEM, e.g. Lenovo * `KEK`: “*Key Exchange Key*†– multiple certificates (created by Microsoft and the OEM), used to update db, signed by the vendor PK * `db`: “*Signature Database*†– multiple certificates (created by Microsoft and the OEM), hashes, and signatures, used to allow binaries, signed by KEK * `dbx`: “*Forbidden Signatures Database*†– multiple certificates, hashes, and signatures, used to block binaries, signed by KEK * 3rd party certificate – the certificate Microsoft uses to sign non-MS bootloaders that Linux uses, e.g. Shim * Windows Production CA 2011 – the old “PCA†certificate * Microsoft UEFI CA 2023 (third-party) – the new certificate ## Introduction The Microsoft certificate which is used for signing “3rd party†boot media (e.g. `shimx64.efi`) will expire on the 11th September 2025. Microsoft has created a new certificate which can be used for signing now. Whilst Microsoft **may** allow us to “dual sign†the shim binary with the old and new certificate until the cut-over date, when the certificate has expired they will only sign shim with the new 3rd party certificate. New laptop, desktop and server products from some OEMs will ship from the factory with **only** the new Microsoft Windows UEFI CA 2023 certificate. This means any shim binaries signed with the old PCA 2011 certificate will not be allowed to run. This means **it may be impossible to install existing Linux releases on newer machines**. Once the certificate has expired, newly signed `shimx64.efi` binaries will only be signed with the new certificate and thus **will not boot on any existing system that does not have the new certificate installed in the db**. This would potentially mean Linux distributons couldn’t deliver security updates to shim once the old certificate has expired. Some OEMs have issued BIOS updates to update the KEK and db, but some have instead opted for Microsoft to update the various dbx, db, and KEKs – using new functionality [built into Windows 11](https://techcommunity.microsoft.com/blog/windows-itpro-blog/updating-microsoft-secure-boot-keys/4055324). This means something has to be done for Linux too. ## Solution At the moment the LVFS distributes `dbx` updates (signed by the Microsoft `KEK`) and users deploy them using fwupd – this has been done over 10M times and with a \>99% success rate. The fwupd project now needs to distribute two additional artifacts, the **vendor-specific** `KEK` and the **generic** Microsoft `db`. This will likely need backporting into any distribution release that needs to run with Secure Boot turned on when installing onto new hardware. Should backporting the fwupd package be impossible, it is also be possible to build the changes into just Fedora, and then use a `Fedora.iso` LiveUSB to preload the new certificates into the non-volatile machine storage, and *then* install any distribution release that includes the shim signed with the new key. It is not possible to load the expired `db` certificate into a machine with only the new `KEK`. Updating the KEK and db is a generally safe procedure, with the only limitations being: * The amount of NVRAM space may be insufficient (or fragmented) – although real-world testing suggests this is a “*failure to install the update*†rather than a “*failure to boot*†scenario. This may be worked around by doing a “factory reset†of the secure boot keys in the BIOS setup * Some firmware has a toggle to “turn off†the MS 3rd party certificate and the *new* 3rd party cert won’t be matched. Some OEMs are planning firmware updates to add the hash for the new 3rd party certificate. Given that the 3rd party certificate has to be enabled if secure boot is turned on *just to boot Linux* I’m not really worried about accidentally enabling the new certificate as the old one can’t have been turned off. * Updating the `db` means that the Microsoft Windows BitLocker recovery code may be needed, if the device is dual-booted. It may also cause full disk encryption to not work in the same way on Linux. ## Example of Affected System (Lenovo P50) fwupdmgr security Host Security ID: HSI:1! (v2.0.9) ... ✘ UEFI db: Invalid ### Affected fwupdmgr security --json ... { "AppstreamId" : "org.fwupd.hsi.Uefi.Db", "HsiResult" : "not-valid", "Name" : "UEFI db", "Description" : "The UEFI db contains the list of valid certificates that can be used to authorize what EFI binaries are allowed to run.", "Uri" : "https://fwupd.github.io/libfwupdplugin/hsi.html#org.fwupd.hsi.Uefi.Db", "Flags" : [ "runtime-issue", "action-config-fw" ] }, ... ### Affected KEK d5cea02c70cff53bb24bd8cce5035897e565463b C=JP,ST=Kanagawa,L=Yokohama,O=Lenovo Ltd.,CN=Lenovo Ltd. KEK CA 2012 C=JP,ST=Kanagawa,L=Yokohama,O=Lenovo Ltd.,CN=Lenovo Ltd. KEK CA 2012 b1d0e26aac012618513d33bdb176bbf53962350e C=US,ST=Washington,L=Redmond,O=Microsoft Corporation,CN=Microsoft Corporation Third Party Marketplace Root C=US,ST=Washington,L=Redmond,O=Microsoft Corporation,CN=Microsoft Corporation KEK CA 2011 ### Affected db 7bef7077a4d5017e88764fdbf8d274e74a4411af C=JP,ST=Kanagawa,L=Yokohama,O=Lenovo Ltd.,CN=Lenovo Ltd. Root CA 2012 C=JP,ST=Kanagawa,L=Yokohama,O=Lenovo Ltd.,CN=ThinkPad Product CA 2012 5e53870688239a03e705b05e4f57c33746db42f9 C=US,ST=North Carolina,O=Lenovo,CN=Lenovo UEFI CA 2014 C=US,ST=North Carolina,O=Lenovo,CN=Lenovo UEFI CA 2014 03de12be14ca397df20cee646c7d9b727fcce5f8 C=US,ST=Washington,L=Redmond,O=Microsoft Corporation,CN=Microsoft Corporation Third Party Marketplace Root C=US,ST=Washington,L=Redmond,O=Microsoft Corporation,CN=Microsoft Corporation UEFI CA 2011 cbbbf4b136db90d11fd37a4a9b2106973aecc095 C=US,ST=Washington,L=Redmond,O=Microsoft Corporation,CN=Microsoft Root Certificate Authority 2010 C=US,ST=Washington,L=Redmond,O=Microsoft Corporation,CN=Microsoft Windows Production PCA 2011 ## Example of Unaffected Machine (Lenovo ThinkPad P16v) ### Unaffected fwupdmgr security Host Security ID: HSI:4 (v2.0.9) ... ✔ UEFI db: Valid ### Unaffected fwupdmgr security --json ... { "AppstreamId" : "org.fwupd.hsi.Uefi.Db", "HsiResult" : "valid", "Name" : "UEFI db", "Description" : "The UEFI db contains the list of valid certificates that can be used to authorize what EFI binaries are allowed to run.", "Uri" : "https://fwupd.github.io/libfwupdplugin/hsi.html#org.fwupd.hsi.Uefi.Db", "Flags" : [ "success", "runtime-issue", "action-config-fw" ] }, ... ### Unaffected PK 9aef2123f4de7c19afabd909bb2c8cac4411e07e C=JP,ST=Kanagawa,L=Yokohama,O=Lenovo Ltd.,CN=Lenovo Ltd. PK CA 2012 C=JP,ST=Kanagawa,L=Yokohama,O=Lenovo Ltd.,CN=Lenovo Ltd. PK CA 2012 ### Unaffected KEK always-search d5cea02c70cff53bb24bd8cce5035897e565463b C=JP,ST=Kanagawa,L=Yokohama,O=Lenovo Ltd.,CN=Lenovo Ltd. KEK CA 2012 C=JP,ST=Kanagawa,L=Yokohama,O=Lenovo Ltd.,CN=Lenovo Ltd. KEK CA 2012 b1d0e26aac012618513d33bdb176bbf53962350e C=US,ST=Washington,L=Redmond,O=Microsoft Corporation,CN=Microsoft Corporation Third Party Marketplace Root C=US,ST=Washington,L=Redmond,O=Microsoft Corporation,CN=Microsoft Corporation KEK CA 2011 4e17021ce9f830eaed13d2817db58a7d9f995838 C=US,O=Microsoft Corporation,CN=Microsoft RSA Devices Root CA 2021 C=US,O=Microsoft Corporation,CN=Microsoft Corporation KEK 2K CA 2023 ### Unaffected db 7bef7077a4d5017e88764fdbf8d274e74a4411af C=JP,ST=Kanagawa,L=Yokohama,O=Lenovo Ltd.,CN=Lenovo Ltd. Root CA 2012 C=JP,ST=Kanagawa,L=Yokohama,O=Lenovo Ltd.,CN=ThinkPad Product CA 2012 5e53870688239a03e705b05e4f57c33746db42f9 C=US,ST=North Carolina,O=Lenovo,CN=Lenovo UEFI CA 2014 C=US,ST=North Carolina,O=Lenovo,CN=Lenovo UEFI CA 2014 cbbbf4b136db90d11fd37a4a9b2106973aecc095 C=US,ST=Washington,L=Redmond,O=Microsoft Corporation,CN=Microsoft Root Certificate Authority 2010 C=US,ST=Washington,L=Redmond,O=Microsoft Corporation,CN=Microsoft Windows Production PCA 2011 db926014f95ac9ec837442d5d96178538c62434f C=US,ST=Washington,L=Redmond,O=Microsoft Corporation,CN=Microsoft Root Certificate Authority 2010 C=US,O=Microsoft Corporation,CN=Windows UEFI CA 2023 03de12be14ca397df20cee646c7d9b727fcce5f8 C=US,ST=Washington,L=Redmond,O=Microsoft Corporation,CN=Microsoft Corporation Third Party Marketplace Root C=US,ST=Washington,L=Redmond,O=Microsoft Corporation,CN=Microsoft Corporation UEFI CA 2011 a5b7c551cedc06b94d0c5b920f473e03c2f142f2 C=US,O=Microsoft Corporation,CN=Microsoft RSA Devices Root CA 2021 C=US,O=Microsoft Corporation,CN=Microsoft UEFI CA 2023 fwupd-2.0.10/docs/urlmap_fwupd.js000066400000000000000000000002311501337203100167010ustar00rootroot00000000000000baseURLs = [ [ 'Gio', 'https://people.gnome.org/~ebassi/docs/_build/Gio/' ], [ 'GObject', 'https://people.gnome.org/~ebassi/docs/_build/GObject/' ], ] fwupd-2.0.10/docs/urlmap_fwupdplugin.js000066400000000000000000000002671501337203100201310ustar00rootroot00000000000000baseURLs = [ [ 'Gio', 'https://people.gnome.org/~ebassi/docs/_build/Gio/' ], [ 'GObject', 'https://people.gnome.org/~ebassi/docs/_build/GObject/' ], [ 'Fwupd', '../libfwupd/' ], ] fwupd-2.0.10/docs/win32-run-anyway.png000066400000000000000000000556101501337203100174310ustar00rootroot00000000000000‰PNG  IHDRò%pÐeXIfII* †Œ”(1 œ2ªi‡¾ÎÎGIMP 2.10.362023:11:28 15:33:53 +[tÇ„iCCPICC profilexœ}‘=HÃ@Å_[EÑJ‡v©¡:YqÔ*¡B¨Zu0¹ô š4$).Ž‚kÁÁŪƒ‹³®®‚ øâêâ¤è"%þ/)´ˆñà¸ïî=îÞþF…©f×8 j–‘N&„lnUèyE?¢aa‰™úœ(¦à9¾îáãë]œgyŸûs (y“>x–é†E¼A<½iéœ÷‰#¬$)ÄçÄc]ø‘ë²Ëoœ‹ûyfÄȤç‰#ÄB±ƒåf%C%ž"Ž)ªFùþ¬Ë ç-Îj¥ÆZ÷ä/ 浕e®ÓŒ"‰E,A„5”Q…8­)&Ò´Ÿðð9~‘\2¹Ê`äX@*$Çþ¿»5 “nR0t¿ØöÇг 4ë¶ý}lÛÍ ð \imµÌ|’^ok±# ´ \\·5y¸ÜŸtÉ)@Ó_(ïgôM9 | ô­¹½µöqúd¨«Ô ppŒ){Ýãݽ½ý{¦Õßpør¦Ù‡– xiTXtXML:com.adobe.xmp ç<&¶bKGDÿÿÿ ½§“ pHYsÃÃÇo¨dtIMEç !5{úÎQ IDATxÚíÝ|TWÿñ×¥ iË”2ÒÚJm ™RA(…¦[uQwt㊉֯,v-dQûÅn « í®åËj5‘]£FW¶3þDMail:¦Ûbœ@HÛ†¶–óýãÎ{çW&É$åÇûùxðx™;÷Ç9çžó¹çœ{¯mê²{ƒY=íØÎœ‚Œ,o¯`’ý¦¹ÃLŸ -Cû?šéäœìq¾Çc;¿Óúm?~ÛÛP–2¼ù`ŠuÓý±-ƒyŒn:8Àªm©>¶aKð}0Ýï`0h:Œó¤¬'Jò`’t ŽîÙ|»Ò"ÙþÙàì˜ñ¼qùtÆ´;>Œˆˆˆœ#1MCÅy¹ßÓîø[Æ(ûDDD$#Q"""’6›M…ˆˆˆdF0dÌ…?aLDDDF‹z,DDD$#4""""¥ÀBDDD2"HP…ˆˆˆdŽ Q`!"""çMÞ¹p›y› ¿ßŸô{¿ßÍ–Ù÷ð(°¹@=õÔSÜ|óÍ ƒ ¿ßÏÍ7ßÌSO=•±íé‘Þ"""0§Ó™0¸0N§3cÛ³Ù0Þn*"""~p"Z(°¹˜‚ `„‚ @ϱ‘ Æ ,DDD.tæ9©&t—ͦÀBDDä¢ *œNgÒ ™¢ÀBDDä" *ÂF*¸*°¹p¥ºû#vBg&„n7µ)åEDD.@Á`0å÷N§sÀeµ==Ò[DDD2Y(°‘ ź+DDDD2ņ É """’ºÝTDDD2ÆÔ»BDDD$ƒX —ÇE°¡‚`ƒ‹Z¥†¨ÌŠÈEnd^›î,£kƒ©œeŸïû”6žI±ðXjk>ÁÚé ¼ì’÷ñÊg¦bçMßiàŽàøÈyöC¹\~æ ÏïÞÎõßzQ¹*"U[SÁÚ鉾9Ëë'Þä¥c½ìøÝ~þ±é%zÓYáåÓø§OÍbŵ“)œ8žËÇE××ò4oôŸä™¿æ›ßkæ¿)ýEø;9pÒÁÔ c¸þúYÀ3É—‹óŽ“–u—NÄpªí;B^:–ñcmŒU~ŠÈH:ü·|ëyãÿWäðÁ’©ÜVZÀ_UÆ7MÄ>ö,ý¯ôñÛ'ÚùQÓQÚ¸ŒÒÒ|æ¾#‡›§M¤´¤ˆ¿™®ÀBÎ/¶ ,xí‡çQvíX²¦pÏðH²Eï,ä¦ñÆS/›ËG¦MàtW7‡> 4<Ê¥ ÊLI\f¬¹ƒÏLËâà/ë)m<véÖ;|2Ÿ©§Žð‘UMx•Iç—ÓoÐÜÞú£‡æ§Ÿ‡ÿ‚ëÜïçñOaòÕ3Øò÷Ýüük/Ðw5•-ÿ\Æç .Ó'øÍ÷›Xö«c–åšÛó­È_cɯ$¿hèòúór¿óÁ9ßð‡N˜ ù€3ùr÷”ØÉ ÿ‘jÙœ™Ü<ÅøoÛ¡çâOZ‘8(²g™º™Ï…³n…Æ2N™sAyÞûkÖ=÷¦QU]=_ßSQ{(¨x£ÿø—Ÿ²4&¨ˆw†þSJ[9Ï¢1#ù®ßtóÜ£r×-¹IºÏ5ã- A²e³î˜L1ÇÙé=¡Ü‘sÈÙÓKÀØlnœcýöºòÅ|aú%ÀI~ñm+œQ’ÉéìˆÞnzê9þšGYT4G¢eœ…Ìšp’£}©—ýüõvã*量«1G9×¼úV¨â2ò§›¿(æßM" xý…|ô *äf³dì ¾ÛþŸË¿®žÌÊñPÓ­ç^œËT€c/ò…ý“øqÙÄ$ËNç¶?Ëδ‰—EO¿Å+Ç_ãñ'üT ˜—r÷_-äþù“™5)Ú-úÔ)Žõç-ÏóZ¶Ÿåqñª+‡qç?þn+ã©kùÝ# (›p†Ý?üOþý§#KOší¤~ù5¼» ›«/ ÅÂáü;ÔÉüô)ê;ÎÄïŸÉWAWø¯Ø²’8mN½qŠ_ìæ¿~üëö¿™49²¦Ïä›spL '2̺[ »‹ ÿÖÁ¾j”Å»ƒGL¥§£Û:Ó1–9wÝj-³¡tm{þë¾åç—¯T™qÝbþò•kp˜îÆJ,›o~ý#|¾` ým~rîo5•§ÁŸÖóûÊ}T'Ùr䮨t —…“G¹·¦“÷þm ïÍ»Œ¬±Æ¶ïmà¶G3~UEïK¦??p·^n”»Ç~þŒ†qå‚6fܸ‘ê}jûK´/¼‚¢±vn½°4€ÑɘGÿÒÎOvž¥½lbâe‹¼óJã„=Ðä^¼Å™kncïÂlÆœ|…ÿÞú¿lúóß\ÊW—pËü[xòú<þzMSüLî©%<ñ•9,¸r §Ž¿ÌlÞË÷»!÷šÜwW1w»Ë8t2ÅÕÇøéü׆…|âêKàWùÍ/žäKO½ \Fé‚ëøÜüi¸Þ·»ÝÀwüY¤{´ÿw/ÓæÊáz®àÝwŒ…Ø[p—áœ`4>7”\ ?ÁÚÐÝ5•ëǧzùåoBŸ½ûvžûÛwàw–—ÛŸã¯ëÑÆ•Ü^v ËgMá½·ÏâÎmG7™ðµ³,üúGùzÁXúŽfýƧyì5(¾á]|á®wðîgñÓßÁ¶ÿ÷S>œð*í,×Üu›¦Œåȳ-Üý££pM!ŸÌ=i\?ºru ¯ärÞ¢í™?²úÇ¡ #>Wv ï+EÝMŬÞùï® F¿ØÉ-Íã«ùÚ?ÜÂWÂwþŒÊ¦ðvÏpô°©/¿›Ç–æbklcåÿBÛ9”ßu+f9øâ?|ŒO4ÿ/×?ÔÓ(ŒeIåÙ¶ø*ìœâùÏšö¯Ï.`ÉÌ)¼oúnVýËÏøçq@ÙBž\|¼ÚÅêoøÙ‰u^³¿ü>þ®h<ãÞx•ßü|_zêÕH¹»½´„_lž‘8]‡[fyþOüþØ58rÆóžÅ%díhMÜ8^7WÁàMÿi™!žuúrVùŠ.9IÓO~Kõ³PZZÌô ö~fg3Ù8ƒ9Øýü£×_iÌ#{õß÷«á‘ <°ká9díü±oEö `Ñ,Þ3ňâ›w ¤Ë:çRÖÛLÓv 3 .áô+T¬ýß]å5·ÿ†>>Ÿ?ýËõ\Õ46|új~²åEKPà]gTЯw<Ë¢õOF¯¦Ú{øUÓS|´ê#ü tB’í^ÍýËm|bÊN¿r”Õ5¿æá£Ño›Ûó­Æ6Þk¦_ÅçªÞGûÊ_ðSÀ±Vvv]Ïõù‰o×ýä»'¾øµâæK@ðùÙF·ë鮣GmÍY;=›åŸ\Œû¦;K†]f“y‘ò÷ñ‘;íd]3¯Œoë}øä‡òaÌc]üóã82éÊ+(â-öù~Í?;ÙvæLdÓÍW18}¤‹ÚcÑüœ—s™ñßÞWø‰Ú¹ÀÏŽø“7üüÈ)SH|°ðú+üÔŸjÙ±¬œq…qÑaºÍtpNòËúhPqtu‡Îc(ºö:n6}uëgoÁuÕxã%¾j® ‰^õþdãΤ÷˜;V¼‡Mg^ãGß´V¦ÑÓcT­’¦×˦°úÞðìc ·àZÖ̇ /5u©_ÅGŠÌß_ËÒic³<Óº?tÕx-óòŒ¬>øG(¨ˆÝ—3Cê¢=ýÒŸù¸)¨°Û¦?évY.+>~uâœ9Î÷N°O9óøùW0޳´ïÙim´My°¯á¬yöMàܵØRÆæàG™JpìÙ'YÐ¿ç›øÞ‘³ÀÞë*‰_9óØöþ.ç,Ïíüu’ýçbÖßp)ð&MßÿE4¨ˆ8Nõ¦6œì¬uÍX™My&ÿðOÆ^…ëãÙ zçJXyÝ¥ÀYø›y<#çA†;-ºññÆã#°æ‰|þ‹ïã3ù¡ãüî^¢ýª_iü¯ïÄkjuä‚wæì(¼+ä‘Ö>£Á²çRéŒúþÒÁÆ-kn,gqkþ˜˜†rºÜ›¤ òá¶PE“3‘È<Å|éF£ò ì{*ÅÕÓ‹lüS¢Ê"›êw“MûÿüŸ}>žzo¾p€©E3# ãSO¾ÂQ€ 9x–˜ž™Ìµ§^£½à n^lªèoÎì ¯ñ‡ß…»”_"ðj¨¢ÇŒL!žÚ¹;ÒˆÄ9¶Ÿÿœ m·(ñÞ#‡Y“ ±q¸ó™5Öht¾ûÝSîÃ#Û_4ÒÊRÆÒ°d‹ìF ÷ÛŸ¾¤làÏ…‚¼ü|>Næ;Œý{ý(µu™k¬îYššwÔÅúI†Ž=îóèúfd¨ÌàT+Û:Œ |–s.·Ææ×Çgpóx#¿~x"cçA&Øÿ$Ïw%ã.cnQ.s‹r¹«lß©ú‡¾½ŒoÞ0q§OàûöÏø›$éó µ:rÁ³Ùl£ð®Ÿh9–[IÇÏfiáà ϶¾`YSc¹¤€ÆÇ6”ƒóÒ‹GI63£ÿt0ëØ¸$üaQ>Å—¼Á3Í©þTo¢üZn] |áÙƒ!ï󯇪‰¼/<çÐæ>€ñÌ*6Éá ¯ýØÐiL(,žq}äJÚ}ËUät5uÇá‹ãuÀ~ý\Zï™Æ¤ŒdîëìâLÊFÿ7ÝFcÁåÙ¼;Q\ñâ‘„éó™kŒÀ“î—BÃ9)ø_¢=TÆ®½);í½¿ó¦Ð“\_íe[Š'ðç“Æm„.åúñ¹|vFv\`<|¼/߸ýú¥@Gò€´¼êÝ»"ô˜a—Ù}ã±nŽäLáo¯3“KÍl#€8öüs|íTσŒ9I[kænL»–'ø O>ðA~Y9›¿™5‘ì}üfçïY°êǸŸx3îW1“í9ˆ\è‚gƒ£ñœž<ÞUÊ‚écB·’ö¸3/4¹ðÞŸ'^Öh,ýÜ~Œ÷0n3=<;¸\uI¨¡~“ζ¡lq cÇœâå®4*³£oÐdq‰q›Ú1 ¹ìã™:m:7à)®æcå¹ÏòÈKù|ýÆ©Øó§²z<|íTtBl{ûAK0õ|ã/XtúN~åÊãÆÅïåÅ[^ç÷Oîç¾­“ß3 Óô '‘FìÊ ”BÜóKG$üÝ%cŒ˜·ïµ¾4z©Žsì$0&^•¤÷œ“¬ð(•…ü¸!{%.£øFÀ?+C_é}¥7ƒçÊXÆÙŒÿM™³„`:O”½ò î¼Ã.³ixüi~ûÑ>–“Í{?T Bºî],šêùùŸ¶ Ÿ™ÒÏ Ïd`5qwï ä/®‚q—_ÁÍ`½«MäBë±€Ñ,ΰåÐk¬½•´×™CÐ茙3]v\~Ÿ'—’ð#±·™ŽŠ3–Éúƒw–7Ný×ì9Æ×o˜Š}J.ŸÎ§&^Ç-9À™WØþ?gàÔžýäTŒŸÄÝwÂ×ö„ŸNú¿ßÞw,û~ú+ò¶Oáÿ|ÜIõ-SX´x!- ü¡é îþÏÃôŽ@ :²CEìÕ“´ á÷§N£{;©|p?L£<™ï÷ú«¯ŒÈ.Þ÷Ë·¥±îÓý1û=Ü2›Ê‹üÓþã|¬l"93й‡6!:ióô‘ÿô|æÏƒóÝO¿ÆÚéW@Žǘ«"r‡£òdáÀÎÓ­¤×òFhrásâ»G#ËŽ·³Ô5“‚)ÆÏào3Í„ðêàŠ3g€±—QX<z ÎÔËBwy¼EW‡éóáÀa"‹ÝÙÜ|銀ÓGøê)sq'cÊT®8Ö÷ۓlëõ—øÖ#¿â[\ÊÝŸ¼“ïݑ˻—–Ñ:ù™¹±csXlŒ¤¨(—æ…iÞ8ÅŸ±æ·ÎžÆ0%g Y ´_É Ýèpü•!\æŽ Ò›ðåM-:2^òÆß4½“b4Êlšç²·‹‹'2kÂ*ÝcyäW7„&mžá©ÝÍ1CŽ:,kc?†sÖSõXxޱW±ì©®;ŽÈ…ltNÇöüþ%£¸¡¬Ä˜\xæUv'š3Ñþ¦à‰—Œ|™Rà°ÜM4Ìf;²Þ©y8FµÌ¦éX3 >Œåæ[fs]xÒæëÝlñžÉüyp*ˆ19ëæ$ûå nÊ=‡kÙççáÞÆpý‚Û¨ª†GXd@ÿ}Øè µO¹Â˜\øb7ßHxqù"?úKhvøUŒå¾1š©rê9vwÕÙõ³ãgÀGŒ¿‘˜5!áñ®ÛßÇi ëškùöu)¶YÇYÚÿøÇ˜TáÁÆzÆåOÇ=%> ìì¡ÀžÏ{óÇy.HºuÞÓÇy`l³nlBç=wÞB²ÃsxfóÞð]ÿ3¸ÿ§~|ظ¥r|ŸþôÕ©®íù‡Le*pú¥£ük’+õ‰“´<¿éäéSF‡ûSƒ»Åña¿1–)ÓØ°dìàËØ„˸6Ñz÷÷ÁN¾ƒo:G³Ì¦ë _û1‰s\a1M·“6Ï<`k΃g^¦ã”ÑóÎÒ$ïúÀLÞ=á\®fÏðoÿ‰}o—åð÷ëÊø¨ÞZ* ,†ÏÛrœ>ÓßÏý©5éÖeÏòÜsF9YNpoc'GÎ9ïà‡UÓ㯶Æç°ñþ›XpY’kÏ­~|¯œ…±Wð±{ßÇç]¥ŒŸÊ–6Öqú•ìMp[eÿï^â…3D`uºû¨5 3õeÇô\³)üuÙ”„w‚Ü:?Ǹ½ñÔë<1„îó,ǵü|e|údM¿…Gï4æÒÝ÷$•ƒ½ÏïØ^¾Ør’ÓŒ¡hþb~wçÄ„AÅ’J_¹ö8s’_þ(öÖד¼ê:ŸtU‚£?Õʧ?Îi`êMó“l¸| ÿçžà[fÊ—Ÿ?ÁÃo—Rö‰÷'¿ )“ÌȉÐóBÆ_JQ‚^’~ïãlí2ž›qwE’rLºö:¾³þ¾”Á2›¶ÇÛñ¿Œ@8Ó‡ï‡/ŽÐyð ß~Þ¸Ó¢höÍÜÛ Ou²ûCS¸ü\¯iúYðí¿8 ã®rðƒ{?ÿwúéåÙLS"ç™ ŒâÛ›#ó^ã©'Ò^v¨·™‹ÿ1Ê~ñAö¾?—‚ÒÛøËWó…úC´q¥ fñ…[§òÎlè;ùö —$ªRùðÚ'ð~m!®œ©<øÕå|ð·ûC2¾’Ûホ•7MáÆpêx7ëk½À‚gðvÌaÁµcSO.Ì·¾o3}¾ñgä?3›}âZn›v=?x`–q¡ûÆ)^yõ?úŸ=T^¹€“®$ô¯Âýw‡¹;ô’®[?pO†žüsêS¼xì%|¿}†Š§º#Ãô27^c×ÏO$îá)€Sø÷¼`ûùô÷¯0^huS ?(-ÎÒò4=‡ü¿^²•Ü«<ü…&¬žÇ_YÓçÅðÝ_ø¹÷ ã¥Koòý?æ—³ÔÿÕ n»î]üàã«ÓoñÊñü¾å9þ%Å ¹ßò[îÏ]JuñD>ùä“gÎp¼ïÿn þÚ³û¶y<´¬Òw–D¶aäóq~ûÛ–Ä/È:ÕÁ߬9ÊCù[rÇm<¹ëo¾Ÿ sÃpêÞûí«ðWLgæ;oæÉàÔýÞoºÝä蟸cÍKT¬¸…/ÍÉa±;\nÂ/6;ÂæŸì§:Aڻ̦Ûiñ›—øËükxGì{AFâ<8êgîêù·Õóøë¢)|qÕù"gé?ù&G;^ ò¿örí=Ÿdíåç~…Ûß±Ûþ¶•9·ÍcÃû ˜;¹€sðéÐ÷§Oâµþ7 9Âæß?ÃWTÈyÆFÛÔe÷N˜£ÔôEÞšú“ráºõóÅ®ùÙpìÏ,ú»iL‘‹Aþ±VÆ(DdPÆßHíœlbß ""(°‘Áq|¼˜w_FÌ{ADD‚Œ±)D$]㯥î=Æ-¦GÿØjz/ˆˆŒÚ“7Eä<–3¯¼g qð…»ÞÁ»/‡Ó¯øÂ7J‰+XˆÈò§óé^ƒc¬qGç_ŽðõoîL|{´ˆ(¶È_vo°Kw…ˆˆˆÈp¯CtWˆˆˆˆdŒMw…ˆˆˆH)°"""¢ÀBDDDXˆˆˆˆ(°"""r^A½,DDDD2"¨ É"""¢ÀBDDDXˆˆˆˆ """¢ÀBDDDXˆˆˆÈÅËfS`!"""™£ÀBDDDXˆˆˆˆ Q`!"""¢ÀBDDDXˆˆˆˆ Q`1’jklåmV.Šî>8J”§€¼–ýTŸcÍR{€õ:áE. }} *.ì ؾ­‡¥‹àÄWø´±¾·€JÓU} >JÍWÀ%Ì ýu´¥‰üjk\°í+Ö8˜Ê16”û¨Æws.{4¨1>O´®“–žw•‡GK'D–Üç«§´Ñü¹ƒ`=Ô3r™ ¸* /À²Uƒ‰Œý¬owQé¯ßÔ“áʉÛçÚš ÖN¨ èŠîSôs £[øÊÜYFË⃬g~ôXÌß'LÏÖHo‘ùê>Õú·O*cíôh^X´€’É@â`ÈšoDÒsúÇì³e,yjôL¬ï-6~ÛÑŠ­µ ’–Á†’к^¦“æAò|r–ÑÞ§Hþ‡z¦Z³yÔ•cÊkk†óRçg¤'Å´©ËO¸g¬ûœÑ^‚#®)aŽ©L'ÎÃøž#}=ÌÚ:,é`>GBû² ¶D¾OpÞ•V4ÿ.éúäm_€ í©Ïžäe!Õþ%IÛͳ9°Êú}m Ö…Ï?¬;ÂÒ$uF:uPÜ9’°œ%:¶ø´Šÿm+,²³>\&S–-SÞǬ‡å*il?RÆõÖå-iªï×O3׿ Ιmö¤éf½°qÅŸÿ­´°ŸÎEF¾Ä×í ÖéqY~ÉÌÇ’ª|)°…n¸ƒtVΦ–€%ÜUtnóÁ₤Wš- Åtnª§48Ãó ²XQ [Ëë£Ëæ2 wÕck4èDjóº¸Mó:ÔfêØ\A õ”nlÄ¡÷ IDATjPK#D:C!ÉyÃýÅ€?t"”ÁVî‹óæ2ö®j¢z]=Õ1V\—¾ÇEWÕËѵh>[Ú÷`+V-è÷ñéY[SÁZû1þúW´7a[HÚåºr±‡` 1MXk+O°¬¼o(í·lö0µ/šþÆœ¨RuBÌþkœT‡×m/f æ¼óckŒo@Ýu3-e!¾âË6•%s9ËeË¢–•û¢tÌPOmo[¨H‘ŸÞÐþv•´E¾wWyØRåÀ›t¨+—- ƾ•šÊy‹'Z έ,¦9&Oç¡Ÿí¸¨Åo ¶—ÒfüÖYFW%¬ åMdþ?œf¹l©ì‰~oÊ ÷ªúø¡ÖW[SÁŠ^SYò¸º&°ïpòž§”e!Åþ M+6DÓÞYFWenø¼OT͇ºzl¦ –OÀÈ«¤å,Ÿ8iiðàµhÙqõËÀ§[¶üMä—ÇÔ³›í¬7Ië€tö#†»¸•‘a #¼Î@¤œä-òXê,<.‚±çŒ9_ÃiÓ–`H¹Ñþ{ È[4LmCÊ:?´Ì×|:7…òÒYFW¥‡.{ôØ=.ºªTo (’ý¡€î]°Â2ùÐÉýE'¨Kqλ« 茉ڽ‘^ tï25¡¹–(ÖßD¾–zŒ‚3ËÞÏtŸ¼þð~ô°ÌRùp¯j…’‘žtèÀ[r"fÛ~Ö·gSéLù/í9ÆÆý4OšZÉ£‡•¦‚^½-@^è8ÜUÅ೦gõº&|}C_ÂÀic£ÑsÐP‘`"ÞI|uр̻±n{?LiPÝÚÏÜÅŽHzTÇì{t(ÉÞÏÖtùåGXš`2iíòl¶Æ‘rf‡fÓþeÒºlõ¶ ;ÒËϘýõnl£»hfò¡1;4[¶À]ÍSÈ‚ö=–Æ P~j—ç¼±õA£?¦ ôSXý~j_[\9Ø`Ng{L9òSê3×›Ú×féO§Î?Úb:¯üMlí›@·ÏtìGÒH_õXŒ¬Æý4×ÌÄM t5Q»Œ«@w’_9)Uàq’Î6S2 šw&hðÚNÀbG¨BžIWCKÍ]^N;´LÐóð2…fäÆê&S8ÝÁ£ qßì;êÑ0+ÎfNiÁ†Ø¥/Ð{,þ>º—ÿ7©ŸíSìNëO{ì²Ñ‡-4lÓ5ÍÜlîBéÜagoLžuO‹­üLC\}ê"c©'¬¿`ª´Üº"²G†5–r$ÅDSëþΛ”ƒkMÁØÅ:ŒèlÀüì8Ócò2}öä»Üטòbæà ”‡GèÞ=kK`ûºpàƒ«¡‚µq]äŽ$ûà@ßì¤ VêõeÓÝÚ4´` Ë»± •ávΤ°} ; 7ñ@¢u&q×Xq6ĪêuõØve³¥Êa\$ìΚL!#0³ØYÆ ÚB•ÏËt¦«¶í„© |ðööfE»µÍÇiÏÌú“of qµµ%°¡Ü\Y›öw8ÚNÀ4Ç ó qš®`†Õõž€=›yq Byɼ4òлó…‹¡9Nþ!oé\H¤\_’ýœ7iÂ(•…úõ² £~KV T6’å™Ó½/'OK§¼aÔC]%GN¾N^~†°Ι¶7Y‚m÷´,Ë"yÓqùPYD4h³Ûãêg÷âܸz=ý ÌQ®ó/B£üäMc¼yKe6ÛÓ˜ðR½­‡¹k¬2ªõ$)ôûi.*³Žé;ËèrêÎtPëq$ˆbý¬oÏåQ˸»ïæ’ÈØsÚþÜU‚•°r]´2w±qq:©u&îqÙJI܃²ÜgZã|Þ=ä¹bÒ³&z·Áp×o4zΘÊsà¹4©å˜rÞÍÅäõ¥Ó ›+#'µ1Á@íò\:w’äA|Y±Ǯ؇¾…—d~¦uÕŸÅ Ëúœ´¸`ëÆä“gÌCÿA:'Íæþ˜îõDç[¢´K.Ë2vžr}þƒ4ÛK¬ç«Çeº+"ƒe!Ehžo⮚oº£lÕA‘:+E9K˜gNZք˨1ïÈUiý­·2þÎŽ´{*³ÏM ü e?ò̸ÇÅ–¢˜Šæ[ÒÌ]5Ÿ¹– Ï\¶˜ëgg[Šz†qûøë|IÛ¸ÑÞ wãî¯!½nqù›Êè2ÓîóÕ'ùm÷ª&ã)•‘1Ns·u€½Ó<ÊŒ¯:Z#³ž½YVå1nO°;0Ì&ØPÁڤݽYqcñG[š°­ŠiBÝòAó¸|_€eI†‰Œ¡… ËèÑ–&ò‡œžMøì³3³~€FXjW7ò`¨WÕëZ —.0nùÚCóò™iTc®Cxh¢Ä:ö½Ï½‹">Œ[Ëwÿû)-Çx²hä³ðòƒÏÏõ°²¶4Tð¨i[Õ)Ól <4†%·°?f>N|ù0òÏŸæ¹ÝÆý ]á[%S­/ÁùÚÑʲ–¬¤CC+ ©ö·‘y¦t:ÚÒĆŽáÌÉHT™ê¬¤å,QžÅÜnšð·­ì[>ø9`F5!nþK¸ÎKY~’íG¥=i³r±':﨣Û®ËPH÷®F¶—XË害¾6ò[ LõóðꔡÕù2¶©º7x4kŽRBäœ3¼Ûšåç,£kyùëÎã«ì¸çÅdÂg¯È¹+ÿ•§õ2‘s¿.‹~3†Jš·©Á•sÏ8%ˆÈ9®ñ l® ¸&üÁÉÁ½sHdi(DDDD2"ÿ•§cS:ˆˆˆHØl6ͱ‘ÌQ`!""" ,DDDD…ˆˆˆ(°8Ox\q¯Ä9Nã鋿±rQ¼›c-=²jk\)Þƒ1úû3:Ù\F× òÕ]åºðÒ`¸«<Ös§!YÙIpžÕ8Ï­:ÅYFK•C™*ò¶=Ç¢¶Æë|C{ã¥ù„^|Òð{B¯ê51o²tWyx´Æ9Jo· à^5’oììSGzÞ&þ&ò3™±ev„‚¡߯°Å¼ ÔYF×µæ7ÇšÞfZš‰<ð) Œv"¢ 1ón܃û(öZˆ\8ÜÊó›c´,:Á²r=JD= ®‚®ãÿ}–­êãþ†Ð5*XyQ˜ïæ²èÛÍ/÷ ]mŸTÆÚéÆ qV2ŸGK'‚ ¡—äìœiºR =c¾µ fûæ+ð˜mv´²bfmËDeæ¤e³õ±Û‹ô„þ¿ ¶¬ ¿ÐüÒ´¾·öúÿ?ÂÒpÚ&x2Ÿ»ÊJ³Ðº|°bÚþøW';Ëè mÓÕPI·Èq%Ù†µ*ùrqéÔ`zê¹(|å}xv4-ë §§©L* É®Ö=.ZØOç¢p¾‡ÖGô˜­éÿ®‚ÚšŠè[8ûlhÏeéáFÓ•°µ\…_nMS™ÛÏ©¶ÆWFÝU.*wú¨[<ð6öZz9bòÄrÞ8ðnžÍUÖßÇ•Åm'X±ÆÁÔ˜2›20?Üy™»ª€Îº!¼hÊ\ߘÒ>šgÆË®¿pÊÈëõ½ÅFv´bÛf·ôþXΡ˜KËy6©m”z.EÎ-gƒÁ‘,ÂW¡ ÂéÀM€Òr|åæœÌºúHeé®ò°¥Ê7\)ÍgE{“é­wl±]¾Î˜7Ú‹é*iÃVîK°N£ò/ÜUéê4* ð ñhkkÊ(l­De˜Ë–ÊV–‡~Ï­ŽTH}o–ÅŠÍl-¯§4T–áö B¤²+÷[‚ËkMWùåñC!ó؆5-ŠéŒta;p'¦öÀöðúBv‹Ç­ô‹æœÍÃø.ó\¶4dG÷)”¯-žäo*œãšOç¦zlþð1xè²÷³µ<ôJ‹®*Õ ‚“Úš VôšÊ¡ÇEÐ5}‡£ËÌ­œOs]hý8iipQÛè£zcc|™MV.yLÑ^³·QÝJÜñÕ.ÏfûºpPaλÐ1n.Kò_—ÅJ¢é™&÷4èÜioeØ8„s#ѲbNûuõT§3b/f ÖóÄ|m±œCÜļ]Ôãb {¬oç¹ÈŒìPˆÓN^__ôÄó’WL~?Õ~sÙF÷¤É‘¿óèaå`ÇŒíýl55ÂÞmtÍ4†*œ3)lo²T2ÞlèÄú§—X&•-mäkwíÐ\gª¬÷ã³D'Hô½Åºw™5[ûr©t†*êI=,3$þ&ò}Ç™¡©¶a½êžeïç@ds¼É.Þý–Ƨz[¦9,ù»ß–.s;4o2_p×È+I>áîhËžèÕ¾¿‰­}èö™ÖÑx$ZNb奴Z{},k9iiTi7­?ë-]üi–‹AÓ ÄŸ“¥¡pWƒ/¦w©ÑDzöìùœf9I³§áÑ¢Ô…·Ûw‚½C8´êFóŽûYß³œC¯3,г¡÷eÓêqAE°äHòÞ2‘‹€mÄ +{‹ 6¤º› ¦‡#ÒP›ºbÌJº:ŽÄTn/ÓÙº:ZœMçÎaV­ØÊ뱕׳¡ã$mƒü}_O´" 5 úñ½Å1¶Ç5{{û#=9…½3ðêíÛÀºŸîº¬h¨ %ÕYFW8ß×8È3}ÕÝ¿ßÞÃý)Òð÷Ñbs݇1Çp,&ï¢åÄÚø@ë@ÝÛý4Ç”+Ëþ©\ |Lƒ<1Yo ÜUÅ‘ãš7©?.c€Ââtןι…kéN’#ØVeæõðæ;N-ÍdÚ§h}lT–ðÒ2ã4ü!}d1 ôönlÄV~„¥ t¥¸%˨ Øj¨må­ì37æ+…sPõ¶æ.×}†–‘òz¶—¤¾ ·¶¦‚`%¬ çû¦@PIz^î˜[äÈD‰õÂd {"ë]Ö2Ø .õLõºzlu°%&hîn °Ï®‰Û"0jw…ø)-¯gë¤ùIîUuÕ›'y9í–+׌W¬‡aîbGÜ~̲½!ÝJ±éø]õN¦Ðþ6䲿tí»§eø¦«×ÕcÛ•Í–„A¥1´`¹R-ζä{ži8,Œ”dE¯ˆíÙÌKг7hÛ‰„Ãó&MÈìv8¦D½Dó& 2/ýé,šMmLoÖÞÞÄÃ6îi„Ò |^pk²ÙnJ+ïÆ6pÅ<¬*fþÃÞÞ,V˜òd¸çM²Æ;Ù0ªÛ™¼çsŽ)ðsWyXa·ayÓ†ñ +Óº/±Ã&þ&òë`Kz.å"Ž+éÛMý/3ksÁ5ÆŸû|ÑÙÿÕÛzèZSA0t;š»nfèo0n÷kciM½ËùÆí¦ƒíEÁ˜ÓúähK:fCäöÉ ÖÒšþ¸iã~š7Ϧ–Õ@õº&ãø]áãoÂW2ómÉìêuõPSA°!\)Xæ;ÆýR4<³ 6T°Ör»i:ìæ!ØšQßÑŠm]’<ðÛïS]÷/6-Ò¾‡•Ì®«/À²Uæüèae¨kúQ |ûhõˆ¤b÷ª&¼¦<¥£•e-P™îüšØ2›0à˜},›æ!ØPbÚ‡*µ #¸ÞWBLZÅŸ±·i{762ÏT–"çM†…ïpé —Óyšp2p£ %‘t9ÚÒÄÊöÙÑtiÜOóæ2‚ Én7èü†¥‘}1òÄ›¨7£ØE°¡€ å>l"r°M]þwÁ£—ÍVJ„®´Ý£¡rWy¸ßòü…sÌ€·föé ##Ñs%†UGé˜ÂϾШD$Sò_yúb~ò¦o̸mMsÛ÷_AEmUÌØ¯ÇÅ£E™ž´wžUθàÌ24sÞÿ2¶˜oïÉqï¡û©ÃÔ]ÏOl<ÏUï„®H×: žB* à><;:IŸ¾xNG›ËpÙ“t勈 “mê‡î Íš£”‘a¹È‡BDDD$ÓÆ`³)DDD$C…ˆˆˆˆ 9÷‹`P© """Ãf—‰ˆˆÈÅ!8âï ‘‹Š Q`!""" ,DDDD…ˆˆˆˆ %Æí¦z¢·ˆˆˆd€n7‘ŒR`!""" ,DDDD…ˆˆˆ(°9ç ÞÍ´x”ðx\*6¸¨}ÛwÆwsºûá¤esî¤_—ÑÕP:6^gªU bÙ DmMøx+èªrd.MÒü]mÍ(œƒÎ2ºR•¹`Õ­9gRØwŒ¼'4ú/ò¤wà]Êë©>ß‚!WG;z’k²Ù>.g]k\Ô–ûç`–ÍDƒî‚u¾·5½ÝUVô6a[05òõ”6šdsš ¦!OçwÎ2–ÚOŽüú›È¿ØOqõXŒB¥º8›Îmûi¶œWè瀾ì=߂В#ØÊ÷М´á,Ÿ©ñö7±²%‹¥žá-{¡“•Eýl݈|R½-`Úá`³2—æMƒ ~ÒýïrX¿«_瞈\…ƒÊI'¨ó¨kOÐxx\´xŒ+º`¤;×Ü5拉ÆÓÝkîÒ=†Yœet5”ášîàц ‚›Ëøïš˜®kg-–nòÐñ'²¨­‰~füßHHZÖ8ã{ Ì]ò“ÞïF¶u]†öÓÙfýÄ»³¦9†¹lL¹9QÀ4¼d.?Ʊ®žÃZÓçµ5ñÃî*Wä³ðÿÍCÁ»÷äÇòp /þݽ/‡²d&sûÚpöJ?Íß¹«fö&¼”Í–*‡å˜ô5——˜t3§y$}ÂåUDXŒXc:“ÂÞƒxCGô*-*o‘‡-ìÁV^oüóÁZsEn/¦«V†¿/oW¸’ à^eêR>—ù›È/oÂ×`Yy=¶UM|¤µŸÂbsïN.sŠfFÝi‡p4 ,Vlžuõ‘´Ü@‰i<ßIKC1›¢ßoT‚Ëž‰ƒË²ÅÙÌ™4y˜ËšÈùîŠîûú6GLJø»#,Ý\†?¥åõlè8Ɔòzlƒj™[YÁÒÖè¶–µçòhÊ3€»î+"Ë8iYt‚•¡^ŠêVX[cí¡è܈{w«à9ħـ¿ó¸ØÂþ´‚–¼Ò2Ó1çX×fSyÙÔÃÜåÖã[N󺃪UEXŒŽÚå¦ Ô0ápÈÔ¾6òMÝÄ4úØÐ—Ke¸³÷³u•ùŠËO©¯Ÿ¹‹çN4S—xå¤6´Ã¼H ‘M¿M {W£¥©^×Jw(P1† b¿oÂ×7üÃðîì!ÏÓÓäÊö²fó&aééðúF#]r‚e–?ëÛ³£ågAíM–`Õ»q¾†òüM¬ì-ÀëwUu¦2ÛècYoq((£0&Ÿòy,ÌÖIeiõÂ¥ü³Œ®’#Ös+•ŽVÓ1ûYß´ï‰î§ùüuÚÉë닟?ºGDDXd¨»‚¥öêüÑ«ºDÃ!ûZã/§ööšÆƒ;ŽÄ_i¶ û‚ÈŠ—éŒTÖFïNõáìP…‡‘Ò]×1¶7&X_¸aîOð½5¿ÌÃ$ƒºkÅßD¾ÐCÁ†¶o °/QoËËZ‡Å¢WáÕëÚ(\{U>™ÂðÐ’éߣ¥9ñ½"ië§9.˜3 gX†]¬ÃMÞG(\S×KP[Saé•Û^sí{,LõºVSÀ™BÒßó*V®K|¥;&¯¼‡ûé>Hœþ&VöŸ#w6‰È¹`tî ñ0Ǟã ÖÏ‹Êp76é 'lÍ6®®‹1z'ü“¡Æ @hitø)-Æ”þF6sàâq±ôp`ÐËz76&yGž±Fà1› ᡎÖ4æ€d²—)fßry–x­Œ¥½M–žƒêuM̪™‰›dWú/Ó‰ÑÓ4¸üýÎS€kz®Øs ‚®cÑ´N/ÕÆFl¡`tEKSú=#"¢‹¡ª-ÉÂgÏ7þ5á#×ÒM7iÏAe·v{ÜÄ9÷â\8|aTdÞ'(\ì v¡Þ‰—éÄŽÛS@aäMt0Ë2?"‹Yqs5g2£Ëzoo¢;/&Sh‰£2n«ÝÞ˜éeC Ú¦ð|SOš,½a„{s²,éßÛád©}à»yÂó@¶Nšþ>µ€¸ù%“)¤/uP‘êw¾˜ó®›ïû|ƒ›k’^0ZÏÖIó/Šg‘ˆÈÛXăD¯ÐÝ»bæGÍ·Œ'»«æ3·}¿©òËe‹y✳Œ-E=¬o¼@rÃÎIóY1)ܨëÍfKLc»·7‹¦îswÕü¸‰—s+Í]Ó¡Û·ùCLìܨ­)aÎH•5e1y8üek=Ö–ËèºàÞ3ÙÀé¤Ö™8àò×r§¥·dítóûÉ[d½¢¶¦v |gÅ–Im”6Bõ6ÓDNÿA:‹¬ ¯»j>®pààob+%1w:•@ë½0Cý]FNosúiv@ϯ¹¨üPˆ§€¼öý‰+âÆ#toŽvwïjd{IAWèûŽÖȃ„èk#¿µ€`CIè£+שL˘Û~>wÅ8@…¦Á»ó[&õY[ïÆFæÕTl0þ>ÚÒĆŽÙ–Ê}ëª#,m¨`mè“}¾zJýц(“qÛnôû&|öÙC *XKhÂYF×SMÛÍoÚ²ÉTc*}–­ åw£Ƽ‡h™ °lUèwÛzèZSA0Tnª},›æ‰®«£•e-Tš¶Õ\·*+®‰¦uê}tà­Ìf몦HZ¯\ì¡«êeò7p¯šlÌ_Á\Æ£ù]½®‰Y›£çÀ>_zw: õwqy2è`øec»kLÛU½*rQ³M]~oðèesÞþ=ñ¸hÁ—¢2tÒR¥ët94pCñö?aòBà®rQ¹Ó7øçJˆˆ\¤ò_yz”é-"C r<8i©q½CáßnŽ>°S™Ö7T?üÉ£Ð0BêôLGæ}³—“–¸}M4üaÍ£¤ÛŒæHµŸ)!’ç±ûÚ·ðñl`˜FDäö¶½6}Þ$8àOwé,VlžOs]=6´èªz™ü” Ø–ImØÊC?r:p^ o‘‡¥»ê±­ -óñ:¸ýá«€íåõ”FÞ¶ÆÐ÷Y¬¨„­åõÑáü”–ûG ¥²X±¹€­á}q–ÑUY†Û;ŒäÀ»¹€íåÑ}²ÓUÒ†­Ü ²¶T9ð†Ò¬¶¦‚½MØÖ¬É¦FÜGè®™ „¾sÎdîô\p‚×olo'¨ ý2uz&P4Ÿà¤è¾á,£k‹ZóYJ¹liÈŽ¦ ¼›ËhñÔSšj(Ìã²³Û Rƒ³Œ®JXY^J÷P@ìïo.[õ°¬Ü§a¹ˆÙÞž w•‡µI³˜@÷®FK#U½®•©¯ ‹³¡÷åèßþ@¤ÒŸÚ×fj€¸wõ3w±#´tn²6pÕÛz(\ì0íoû?1Ûò7±5nÉh©‹Ù'{?[×EÍ»±-šfÎ2VИù)Ý>N?Û)ˆô¹gÓìëb"FaïÁ´Ò3‘=f¯?úÿTÇPña˜p IDAT»<›­–9AÜ»ˆî¯šëšTˆÈEo”‹Öšº¬·°ÛºÁ\Ýc{ÜUèËtö ð³F['•%ìšÞ׳ý¶tGzSrp­‰¹“eƒ9“&‡–8IgÛheQü±G‡‘Âi;êã{:bƒ7SšgÓÝš ü}:ν½á†ÞAå¤Ô5öQj¸Ý‹³éÜH+=én?×{÷§Ÿ,}=Ôùã÷½{€Ÿy7î¡sQâ¡´äÇà`–ÝZ†ƒ ]9äM Oýƒè¹p½=w…Œ¢êuõT‡ÆÆï÷ ÐMžöþf i;A÷´D_8˜e‡ƒH[_K…Å@†6ïÎî_ì&CïA¼˜‡‹ZüÀ‰ø†ý¼À½ª>4¿£ߦÆäÃ5–@&À²¤w19 ï{UŸˆˆœ È p /qã•Å¬Ø LçLæÒ—^׳¿‰üò&:¥÷̇è•úò÷A¡œÉ2¸FêÀÆF¶—T î9 m'8íÑá#ÿA:'ÍÄí±C¨wbo/Ìr:Y:©oXÝþy‘ÞŸ¨Ú’¬POP¢Þ¨ÉšË„=›y±‹x È3}¥ÒèÃVÞFae:-@w1‰ˆ ,xn9'- %öp‹ PSA°¡$òÙ¾Aü>~}{ðn® kqù³}ë1š‡¼‡áÑI¦¹þƒtVfs`¸!kßÃJæl( 5ð–­ò›ö­‰Y›+ºÂéÒ„¯d¦i=¬¬ƒ- Ï3P…ˆœWvßSlTÀÅ÷°swuú¾·"ÜPÝž¤"¿J}èöØ9Í|y…±ƾ­à;‡`‰ë>kCwhßÛ Üçb‰ã£§¥ù‡€¾Ðžì~–pèP3°€O-™‘rRnã"+3–|ÊHÓP²c5¶ÛŠM¸ø ò{+ˆ.ö·g Eoþò—yhпú+LûûÐíçwp¨ÀBDÎ[÷=ä‰HwuÚ¡ |ÊN ÷¢7'KYƒáîöȲ»y6`]×Ü‚ƒmÍ¡ï—|Þø{÷÷ØqíøF›¿f,áS Âà!5 éžÝí…Iµ©¶q± <ËnsOÓ—Š”˰ÙC>Kƒ½›OC*Ñ‚@\I˜ñž>†±Ä}<L]ævÏ}ÀØæƒKY$ƒ#Û9¿‡÷XˆÈyŘcaTÀñ=i­!ÔŽ$_,æ„Û^†Þ‡ÑŽÏ`Æ\KkÄ#zà{;v°ÃèJˆY¶™C;Œ^†¹Ÿú BËj6õ:¤Ü‡TÛ¸¸½<À‚p˜òùG¤KÃæbìŸZ £ ° SÁîP2`Á§0Фƒœç™T`!"ç¥%|Þ¸\çž¡ñê3˜k¹ÚÞ„»CÆUo¸G"}3>ó÷»¿÷e¾·Û:dc cìæYß³ìæ>\Ÿ1ö}÷³>ãn„P‹8Ð>¤ÚÆEcÇjŠï12ú¾>à S7@¤w)óÀ}¦tÞ|«ä“7“¢ÐÜÐþÞþÐE‘= ,Dä¼íµxð±p²š¦`Á# º—!aàj´»o{š`íê¯<Ü5íŽ Ë˜–Mg’nã"²à‘6ëœÓ"æe?'ZÖ>?”Â4ã3D‹äyì\ùa›úÑ/^r“ê(1Á†Š‹ò¸w¬6æE ¯Q{{¶a+¯W¸ù çã@ò{ŸR…ˆH¦…Ÿ_`ô|›´t~mCd(XˆˆŒ˜<ÒöàO¨mˆ¤oœ’@D$³f|æ ‚Ÿ9ÿ·!2c°)DDD$S…ˆˆˆˆ Q`!""" ,DDDDXˆˆˆˆ Q`!"""1= KDFÜh¿¯@TäíT…ˆˆˆdŽ Q`!""" ,DDDD…ˆˆˆˆ Q`!"""çg`T"ˆˆˆH¦ """¢ÀBDDDXˆˆˆˆ(°"""¢ÀBDDD.êÀÂfS*ˆˆˆH† """¢ÀBDDDXˆˆˆˆ(°"""¢ÀBDDDXˆˆˆˆ ß82õôÓÆ_±Ë ÆD!x–é†E¼A<½iéœ÷‰#¬$)ÄçÄc]ø‘ë²Ëoœ‹ûyfÄȤç‰#ÄB±ƒåf%C%ž"Ž)ªFùþ¬Ë ç-Îj¥ÆZ÷ä/ 浕e®ÓŒ"‰E,A„5”Q…8­)&Ò´Ÿðð9~‘\2¹Ê`äX@*$Çþ¿»5 “nR0t¿ØöÇг 4ë¶ý}lÛÍ ð \imµÌ|’^ok±# ´ \\·5y¸ÜŸtÉ)@Ó_(ïgôM9 | ô­¹½µöqúd¨«Ô ppŒ){Ýãݽ½ý{¦Õßpør¦Ù‡– xiTXtXML:com.adobe.xmp ÙºÄcbKGDÿÿÿ ½§“ pHYsÃÃÇo¨dtIMEç !-I r IDATxÚì½yœ]Eµ=¾Î9·3ô1$ˆ avÊWß“)èOA& ƒŠÌ2(G@Ÿˆ>gT‘I|Ìð”YE@@pPf8@LÂ$„$}ï©ß÷TÝUëìºÝÐ$íé|òéîÛ÷žS§ªVUí½×^;Ã2üµñW.p/ù"YõÝ%^_š¾Üb>ÓËyïlž8n„ûj¤û̽ˆù¿$?ÿb¯õàûâoå^áɸ,i$1+ã”e/í}¯T»Gzü }PÛêœC6Dÿ8çš¹·$ÆZûÙ¿—__ûÚu'V† ®7¹ükKû3½Äöe/ò6~Ø_j÷dKùþñòö’·õÖ2½èdÀ‚ŸŸ,ËP–%ò<:Ý9W-ôY–Õþ úLžç(˲š¤ÝkE^ó¯§ÞÏ×Ö¿ùÏø¿ùëóWžçèt:áZþ»KlVþšþù:Z­VíºþË÷‘õÙ²,£û:ç¢÷@§Ó mç>ÒçÑ~×¾óïãkk{üç¹] üû|ûïEQ Óé„þN}Öÿÿî¯ÇÏ¡}¡ãçïçïïŸÁ9þÆ×Õkè|äùçÇŽÇʺŽö·ß_ƒû?ÃmQ¬ gò}ùµN§žÇªÝn‡ÏøyŸç¹‰ Šéÿ þüRü; Ë3¸²ÁƒÿeÿeéÐjh·ü/ûÿÄéû _ö/®>ž˜EQDSƒ™ î|?0þo<‰xQñÿ˲Œ‘'‡OBž0©A+ËÒ\0øV{øÞºpð}øwý®mážÓéÔ®¥ãÃíÐçÓלsæ³è˜ñÍ× s@Ç¢(Šh<|_ðµùzüçS»ÝŽ€Ês’ÊÏÔn·‡\hx.ùŸy^jûô¿µHi_Z›þ¬øâgIÍi>Dð½ý3ð˜{œr_¦5ÿlQ R‰ÁºZ‰«>yR?§ó¨Áƒÿÿ£ÿEŽö`®lðßàéÁ¿ètó||?Q~HÐ)»Ñ ÿKçþ?*"AüªÕ‡×ÉÅ!hNø²]Ô+Ã!mþ?'¿é„Ò‰`yHøþÖb¨ µ–Õ»£‹O–V«Ú«É:A-o‡Û5AM=4ºˆªçÌò ñ&¤É˜êPo+4ñBÀ ²6@[×¢Nx•åp[ ‹7ÍóQÿ¦ÞÞ,û©¥ð‚ÈtõFñ¸ò5œs!áÔ™‹ äûD=è:¬±æp¶nV–'³Áƒÿÿ£ ÿ­ÿ þ—NüEÿÞ0êæOµÐé”á}^(¡ÁÿÒµÿ #HÃu tî<õ^°¬ ‰Ê¿eŽª•x§¼]ËÛ¤!Y52?SC b íòâÕ/\­“ÆOÐV«$6uÁRNêÎgµ‹3¯À¶<\–¢‰õ|Vx™ÃŸæÄ7¤B­ï—Aãk0çW¥Py÷ïç~ÖdÉÿ þü2ü;C¨t%Úƒ þGÿÆÃ«—_ë¼îµ˜¼ÒŠ˜;>æ<ñf?ùþ1ç “º,àÆ‹‡{|wÞ}fΚ¹óæWÆÒ¬ºÊ¼}Ú›ðžMß…<ËðÀŒG—Zü `Bý©áÞÿÞÿyQÀ…CvŽVQ  ÄæÕÜ+ƒ=¥}]–enð¿TíÿkM‘ O³ˆ4íu°TÊT-LKyE=‚š4i-X–µ¬–·.šþ½>·Â¥l¡[Àe€YòZW=––·Ê{Ê|²š&¿©7ÀóéS ºz=x SÅÙ+äÿkec Øš §ž\ PÜ^õPXŽÕƒbmHzo«îï§äcÍ—¡ª§6Må[‹¬Ò6,O¨)PÏ•U1ÅT#?Ç×:Ÿbžvƒÿÿ þGþZè´+ü· tÚ þ_ü/7qüènØôí› Ë2Ì~âI\yÝ¸æÆ›ñüÜy&¶søàÖïÅvïÝ+¯ðoØ{·ðŽÿ÷f~ì7‘>—üÀ¸±c äêçþ³<‡óm1ÉóÆÃ‚…‹°hp"6þó,CI”¸Nƒÿ¥fÿ‘ ò‘;Í ÇÒÕ÷ ±6+ÿÖÄÑP­Õ±©D>å«•¬‹¦õ¹T,‹€=JšO¥übž4©Pµzot±·B®Únë¹Ôš×¤T‹£?œÿ)ž¾z5”c­‰ƒì´’U²2¥F£–%/™Z˜x±Þg%òª×LûSÛmIVª·NCÏVT:6•#¡N¥#é|±’6ü7øoð?ŠðßiðÿràýuÖÂ1‡†Ö›Šyó_À7O=g_|)î¼û¾*×%ÿûšË¯¹{ü˜öú ±Ú*“±ýû¶Â]÷Ü'Ÿ~f©ÀÿØ1c0~ÜØš2Ùpðï ŸÌÄ—þÖCÈC ?S'ÌY*ä  lð¿Tíÿ­µÞ¼ìAƒýÁôš°m%0¦Âp¼ÉYºr¾û-zšP«^"hÖ5y¢Y‹‡eá§Bà–—ËªA*~Æ¿nV¸=¥ÖÒ«ÕŸ5¡Îz&ð© Ýֆǀg[…ít.©BÎP…øôo @å.™ß›ªc`m© áV¸ßÚ€ô€ÜÏ{ªžp+XßkQ¬¬ö[Ô K¡©Áƒÿÿ£ÿ®Áÿ’Æÿk&¯Œ¯á³xõò“pßC3pÔ·OÄ=<„Á*Y}¸øÿëcãç7ü›¼ñõXy¥ñî·½·ÜöGe\)/Ùò¾Z˜Ò¼Šÿ þü7øoð?|ü7ûï± &LÛîº_þöI˜ýÄ“/ÿsçÍÇ'þûpÛÂÄ ãñÅCŒ ½‘ÆQä30 «EhB¿•%à\7⣅6ýߪ$Ÿ(Å?Sõü­"Ǹ1cç=c§FaË2 þ—Zü/ãF«ynx`-ZO@KÓ]yŒ–wÂZ-ë›"Ýœýddu«J´%h^³M,´ôôSœxµºS%-&–ª¬€âv1`ä7U½¸ .öf%ªr(ÀØ{¡çç´ø¨:NŸ·ŸÚ‹ª¥J”J®ÖbtÊ1ö×ÓDGVÇR¯~J%†ŸÓònêøè<ìa°xíL°æ7?Sƒÿÿ þü7øþ÷ÜyGlò¦×ã‰'ŸÂ·N=óæÏ_"øÿæ©gá/k¬¾*þûÐ_1ü@Qä]yõ²Œ ?U$ˆ>åþ„ˆ7z΢Ê ý“eƒÜÀ¿/ªÚàéÄ?0J$²UœCÑ–º ‡-x¿°­ÿ,ë–³D_ªÐ•zC”/Îi%ã*çÓׯ°ŠVñF¯ gÜ:­ÄPí;¾·ò‘‹¢À`•(˜ºNŠoêûÇbRŠ œpÊ^/¡Åƒ¶’C•£I£*E©µ 8ôª ®} aaõ˜[|Vž#œ h-„)ï]?*Bʃ”Jçâ‘|O‹««²É)Å"«¸œÎ[‹ïË/õ°Ûà¿ÁƒÿQŠÿ<Êÿ/ÿ믳¶ž¾æ¿ðN8ýǘ;o>²,ÃÛ¦½k®¾.ºâgȲ »n¿uu¯ΕÈÁÁ!C7±ß¿o÷>€ÙO>…ëoù,\ˆ£¿}"ÎüÎqxë´7bÕ)“ñØ?f(þ³,À—¡÷xàûù6PÎOÀHõº# 8Zìç¬!?çЮTãJçP¶‘çY5*üK´<[)¬†ð­0©¥3¯×²Bù–Š{ê|øþS±¥µ¯ ‡VÍV,ágã{óÌ/4©btºûù ›¯å-³\_tŸ™ç¨¥¶¢›³•´È8T¯¤¸Yql8´¦b5øoðßàã¿Eø/zyB þÿïÚd2¿ºå÷¸ëÞûÃ}ŽüÄ€ .»ªkm· («ûcÖ{é‚Ë®BQøPe,ÝðÛ[áœÃì'ŸÂU×Þ€m¶Ú»ïø|ã”3Fÿcº¹=ÉâŸ+£$3‚ÿ,ÏQ¶ÛA;cšZÿ^>{`` çÍïªÄe~íÚ„ÿü_ÿÛ½w \~ͯ†…ÿm¦o†+~ù«Ûÿ—ùœ ]¤X–Q“¥ØsgYÔNVOƒJûé}x£óÕzÕCi†R=_ÇR\bÃOÕVü¤Öð¡r4uBð®¡R•h´(!¾ª¯.Xü,)1ƒNû„+ó&dyø5Àñ¿óøZ ?+'H«Á]w—µ1° ¼YÔVLá¹lá³ú,¶Ö˜•TÈý¨\?Ž|(ñ‡^͉Ѕ’Oîs¥SðÂÊ÷I]Ó¢ŸXj? þü7øÿÀÞ 5ø>þ'-7[o¹)€_ÜpsLUsèEIœë°Õæ#²ÿ #H;Rk`¨ /nÜ1¾Ã9Q؇f-/'[´°çÎ;âÁ›~Ž=wÞpÍygàÚ ÎªMr­IË“ÆüÖ„©š§j"J!aïªÎ´I9ƪv­£Ú~NväçS¡nV½+œnÑ xáeoo"z-þ¯‰—úüüœ©‚ŒVQ1ë``ÕyPo!oÂVmˆ”üej¼RzûŒ•;µ’­E‡_Ûfúæ8ç{ß0Œõ° I”ü¼íiiÀ¿ÖIèü7øÿWÄ?…ÿNƒÿÅÅÿ;ßòfL˜0·Þñ'üå±Çkøb‘`üx[ÈÊ?rõƒùÜyóqãïnÄñãðŽÿ÷¦ÃQäµè´â?Ô1êt€,ÃçÞüÅOqûÕ—àöŸý_ø?mã ƒÍiëþ[E‹æTW–e Ãý«áÿÊkoÀU×݈m¦o†ý÷Ø%‰ÿ>ü!l½å¦¸òºpåµ7ŒÈþ?*Œ ‹«­É®\Eײȭp™%ãçœÃ%§~3nù%¾sÔâ)ˆÃì=ÑC…%ÙçU-Øki).YV½æE½~ýuñÓÓOÄÇ?²[dEkXŸë°çÀÚìûUðµ¨/Þ*·ø§)JBà×–Ùýâ¡âÒ3NÂegœ„Ÿž~".?ódœrì—ûROÔ[¤m+QÎò®X*QªJ¢Þ¥OøC¤öÏ·“= Û¿wz´9øÅ@Cñ–WHCÚº(Z‡¾Ôæ~êñ_Á>ùñ¨­Grþ÷»_úé[mÿýÞ×k‘‚h||J&“•‡¬Bj<ÿÛ¾g \vÆI8ò7ñ¿ßî;ãò3OƆë®câŸ]‹6£ô€WÿüÜ£ ÿz@Ly>—eü3Ýç•Æ?Gêæºp?ü»%ÿŠ"—yȉýÿ•ÆÿÉÇ…ËÏ:?=ãDœ{â7 ÿoÜp}Àï︫†dˆðèo¨~v¼–S¿U9Åçïþx²,Ã7\oÄðß*ZQþ üƒ¯Y–pøçsÏã-ïû`øãïnÃißø*¦½~£!ñŸÑ|çï`È—èTml·ƒ‚Ü¿þð¿ô ¡ïRÃÿþÞÛlµ®ºþFüðœ‹^¶ýßëXæ ?Ðþ;oÔÊLàbÉX‹ãî;oµU¦àÍoˆûg<‚·O{cį'ñ‘ÞDq1÷Ó²¼u‘äIÍaJUÝÐ ÃÜößÿÌœ5›¾ý-ÑýTžP½rÞ  ±k!²8¬Vø^ÃÌü7­„­`{`Æ£ØaßC°Ã>c»½ÂÜùópÙ™'׸Ô*skiìûkj¢œÒtqâÏrŸ*H) FkAñƳ\}¿|^ð­b|ðÓgôÞC?ÿñî{±Ú*S¢Å÷µ«¾“–›ˆ×_7<ÿ›6ÚÿcvÔïV;ôY¬EŸÃ÷þwÆ2<‡|ñðßé†ë®mâÿÝoÛÏÏ›<«ãŸŸÇF%¦Õ+÷JàߢȌü+µO•î–uüû¹¼4à_ãzHå±Su+m‡RŒ–þÛtÚU[Û/ÿþ¿4àÿ±ÌÂö{„í÷:Ïχ#ÙØøŸ0nàù¹ójøôo¨.‡¤XÚï~àÁ®žŸ;Àr'Žþ£¾$™ëþ³Ü>þvÔ±øçsÏã½›ý{|mÿ%á¿S: TÑÃ"ÏÑn×ñŸÿ ãÿÔs.ÄU×w ¡ýöØ9´ãÀ·ÜW]{#N9ûü—yÿ¯çê u8õ´0úy•úa-d¼8î·ûÎøç³Ïáä³ÏÇ÷¾òl3}s\uÝ Áøñ+DW–f¸ß êœ*eU¶<·N]ÇôCqÈþ¡þY|Â×pÁåWc¿ÝwmØqßCàœÃSׯ±Ÿ? gœ öÙm'Àã³fã #ÏrÎ÷¾IËM\}ýøá¹% jY›Oj Ùkâ±p J}÷™¯~ßøâçðµ#?ƒÏóÀ+Ýfúæá}W_#N=çÂ0/ŽüÄÇñ¶7¿!l9ôsøú>‹gŸ{Ç|ÿ¾µëö[ãßø,Š¢ÀONû~ðdÀU×ÝÀ…û\uÝ 8õœ C_|ý ŸÅzk¯ ¸ÿáGðùc¿Y½þ<ûü\¬6e V]e2àŽøÚwpÏ…¾Ý¯âÅî°ï!ØpÝupÜ០ÏrÄñ߯½>l&9*ÇÚ'<êÆa©UñÆëßsËíÄ6Ó7 cÐjµðš)+ãÁGÅ¿o2 wßÿ ²,ÆS×—wÕ‚¢ö:×mïC3B¥ìý÷膼áxä/aÌ,¾¸E!±h#‹ƒÿî˜ÏÅssçaÿ=>„3.¸$|v¿ÝwÁÌÙs°ê”É(]ÿ_ÿÂg±þ:kEcpßC3àœÃÙß97ýþ6l3}s<0ãQ~Ü·°Á:káø#?Ó{ÿqßÂ}?bâÊJ+âø#?ÿíßú®~ÿVÍ‹6£žA«À•\­‘¿Å·j¡X^t« ¥eÄ)u޽«–!Ê-ˆÉ‡l¾o¿Â´JŸ³Ä&RIÎKÿº?±ü¸+Gß’C¶ú÷Eã?\9rû¿cdå.¥ßø¾ZÒûÿ±ß?5¼çñY³±ü¤åj þ×ZcuÀŒ¿þ½†ÿþ|Þ>íMÜ0b¸d1=Žúø‹_;w?ðP ÿþ¯[mÕÃ!40 ÿYžwŸÆ‹$$ðÿêå'áoÿ#?®ªdá•„v‘ÇÆgQx5MÊ-«$¶;ÿ¢ø?íÜ‹l3}³àôÝzËMqÕõ7â´ó.®)7.éý_ñ?jèp–·À{i¸S5$˜JP´t׳,Ã{Þý.Üþ§»qÕu7`Öœ'°ëv[Ó{2ÓSay+ÐÖä¶xÁ–¼†a?°U÷P~ßC3pÿÃ`Ó·o&ƒ¿Î~»ïŒí÷>;ìs0n»ëÏ8é˜/yF0àý[nŠíö:;î{²,Ã>ÙÕüÿú>‹ûž>{ê9Ö<ÑšàÊ}`É0*ø,úAo¼kõüÀ[ßô\ý«±Ã>ãþâc;ïˆ<ϱû!ŸÆÌÙspúùc‡Ê0=òpÚ¹a»½쾿:Ls¿xšªÈ¨Š‘άjͼ°ÝûàØ9{öÛ½ËãÝg×ðÀŒGñУÃÔ5_xýúëaÒĉ¸â—×£,Kwø§pÚ¹aû½ÂU×ß„#?ùñpïIËMÄ:¯{mwlö>«N™’%Ù ¬ZÍcI‚.þ W_¦m¼a„ÿi¯ß¿¾õö`äwçþÇ1i¹‰ÝÈã^âªënÀq‡*šã›¾}ì¸ï!ÁÐ=þÈÏàôó/ÆŽû‚«®»1Pï,ü?ñô3øü1ßÄSÏ<“\㮺.v6ôÊfÂÞîÍË’äµ§VÎÏÈà?«½W½íªÔ–¢sèÆª‰èª„§­»¡óŒoþÙÙ«ŸòØóܶ’Á-#îå¿âÐÏ¡”±lE'­~±(PÝÐ IDAT@/ÿÅÈíÿêu×âe¿œûÿS×ÆÍ·þaØøŸ0~<`î¼ù&þo½óOáý¡xª§¼üï~àÁð¹?Ý÷€‰mš0aüˆáßKR÷ÿ£ú?¨ÔÛ¢|È¢ÀN8Ÿùœé•] m¦ËøweÙU¤«ú+ŽX¸Ê ʃñÓYBø?óÛÇàÌoSÃÿéßø*Îúö±5üŸù­cpÖwŽ}Åñê9vÊ[n†­§w  üø‚ÛÿkΖeÝÒzKEÔ2dJ„Z¸þç·¾éõXeòÊ8óŸ Ë2üþÎ?Îh÷z€HéÚ§ôέp­•Hª ÖªÒžÎsßÃÀ9‡K~Ö_g­ÚAæêëo ÷ÿŸïž‚U'¯\9zº÷üþYç„k_uí Xý5S½_5iR4 5O@ÕCRu9øY4Ĭ®”šï³ürË6{ÇÛpõõ7…ûÝ÷Ð ÜzçŸzéëà‚Ë® ×:õœ £Ãq8ðÀÕÂÇ'}ʲ Ñ¿³/¹Y–…ß_¿Áz(Ë®»Nüѹ¡ ¿¾õv¬½Æê´ÙüWü²+yËmĪS&G¡\çâ9°Úk¦$½ˆÞµd[ý⥞K]œÔ{æ#S×\eYbêšk`Æ_ÿ†ßÜ~G˜WÿþÖi˜9ç äyŽdWÜÿð#¸²êž{!à€Ö›îå …¢(ðë[ÿ€u^·FMnØÊ7á±öÏÃûaã¿ÚÔ¯ºîLZnb0^·ïtÀe¿¸.Âÿ†S×ÁI?:/¼vÆÿ‡™³ç`»÷lÚóëê R–%öÝ­k(^yí pÎáôó/†s¯_Ý$þŸxúqÜ·ñÔ3ÿ4  pê9 ÿÖ‚¯ø×ú&|]¤¨ÑÀŸ_šð¯ñ”2[jƒçƒέùiEÒô £‘"‹ÏI¿üsJÙo¤ñ¯5C4ÿA<õl[‘Ç@ 2re ÿíÊN‰,ù÷ÿwR幩º•J®óý–äþ¿ÿ»tóbÏ:_;é4\yí ÃÆÿO? xÍ”•‡ÄÿŸ? ÇŸøÃž1äýûc8öÄSñßßøÞø_¥:S<ñÔÓ#Ž,þW/? ·ÿìÿð‡Ÿÿ·]u1î}ðaì¸Ï!”7Ëa[øçúC=ªlW.ϻѠ%f3ñÿp¦ÿJà?Ïòžaí0rû¿àTÐᬪÁ¼!©¢….2VH2J¬¾>±×GçUɈþë¿öÝß9íGnÍ¢œ /ñ888µ“µp¥¼ªÌ¯U ÏN§ƒõ×Y GÿmÀ=>„™³æ`ßÝvÆéç_Ü]à³îÁ6“*ʯ·n¸Î½>ý}RÅëýÌW¿†S¿öüôôñÀŒGñ¹c¾QÛ,8AÓÒœWo™$,*€ªƒèA žŸ7Î9Lš8ÏšÎaùIËuÿ¾ÜD\qí¯jœë°¤ø{—’$MJ1©ÅÙ?ˤ‰qìçë­²Y†fô0fΚƒm¶Ü ë¼n ÜyÏýȲ +ýÛ¿aƒ©kã²3OÏÍ_ÏÏW“Ì]~Òr¦—^ÐZgD#‹ƒŸÈ{ÓïoÇ6Ó7Õ×þ ïßrSÜyÏ}Q{‹¢À¤å&âÞ‡®µÛoò]CqNð<­¼âŠXí5qé'EsÞK ÿsžz‡û͈wõõ7á´ó.®yòûáŸÇ]ëï–gÜšßJóшÃHà?•”k]ÔÜœ”ç•”Rs©–Ç— ¦pUM†×~aµ/Í»²dtG ÿ)YY¥ZéA<µ©C÷ØÅÆî½û/ÿþ¯jf:&\7‹çŸÇè’ÚÿO?ÿœzÎ…]jûw¿†ÌžƒÏóÍaáÞü˜8~ü°ð?aü8Lœ0!œÖ|íj˜²ÒJÃÂÿ*+¯8‡'Ÿ~fÄñÏ´µ,Ï£¼h.–%²¬+Œ0}—=?:áxü›¿_;é´ÅÇ¿`¨Õ*B^PQä(K‡¢èJgyŽöKÄÿ^‡iâ¯OiÖpÛë°#— üïÿá]B2„ƒüï#²ÿkäu™7‚4\mqJS€U`žçˆ¤=`§½~#üæö;píÍ¿ …¶ößclõîwâ„ÓÏ qä¡Í _ùC ÿîÛÀüa¥_X lªã'é¾»í8åp"cQ¡€ÿò^ú{|®»Nm1^í5S‚‘ç9þÂWP–%N9îËøâ¡â¾{J- ©QNå ë–åoU#VJ_Ó׺óžûƒ1´jåéò÷A–á¹¹s«„ÍyØxýuqïƒG“gŸŸ/2º9ù Ò†ñ£¿??wŽ;釸ÿáGÂs÷ 9ñŸˆc‹ºØû ç.;ódÌœ=—_s}mPÙLõl÷[8¼¡ÎóÞÏÓ{*¾÷~»ïŒçæÎÅ=vÿû?þ7m¼!V]e2ξäÒѸÿáGpøqߪ-\ë¼îµµÃÆÊ+® #ås͙ԡÀ*d8$þinÿðÜ qÎ÷¾mß³&MœˆÓλ…äù¹s±áºëàžŠ<]3gÍŽhšþúO=ó xä/8ü¸oÕøÓ\ÏÂÿœ§žÆÇÇ~~÷Ç»pÚy/þUúYU:GôЧ4 U=S¬%ýür⟽|Œ=Ìr2®•ƒ 1Í I)ߥª°[ÞO•Wu8¬±,&R d#…¥>*КgŒSöfsQÇT^Ö‹Æž¡l¿|û¿GÜ7:縟_Îýÿ¸ˆ#?±„±~øÿÛã3±ÑzS±ö«[ ÿ[þû;ðÉ}>Ê!dȰÏnÿ‰ÙO>‰[ïøS_üOYyE Ë0çɧF ÿ¥shÑXäE œš9Eþ³ÔG{úy\ñqÂÑGâSG¿XøwÕŸÉ?Ýö·áÊŠZ–À«(ÐÅø?pÏݰ͖›Š·W"Üfúfpp8íÜ‹_Þý_ð?*èpº™{OêÏó„ãjË:`<ð~ÛwOŒ7^q5~|É¥8ëÂÿÃÙ—\Š»|¬³6VÍ”(_¿zØ¥…Êò2ï2•´ÆaO~¿_<޼ц¸úW7a»½ 9/;ìs0V29’üÝf«ÍC;öÜi<0ãÑîʺ÷ü⡆ öî·o ßGEQàñY³ƒ-¥žKÕ’NÔI˲ŒV¸Ö:èœtÌ—WQ¯€Ê»¿y¸ïÆë¯‹·½ù ¸ìÝœ•ûg<‚C>¶G¸†Ï×xâ©§°áÔuÂ}½w Ík’¡~æì9øènoR+üªkåXøy¹Ú*“k‹˜¿®£JéVÁFå[¼ÜZEvQÇÒÍhæì9x÷Û6ÁÌÙsB{þ|ÿƒØhݵçpßC3P–%n¹ýØ`êÚÁ°Ž©:Àò“–Ã~»ïæôÛÞü†“•ª®Îý­J1V"õø¯xàþ>÷Ïxûî¶3núýí&þï{¸7_€n-ƒI'Êà! ôž›~wÖ_g-l0uíÚaw8øŸ5ç ~ì·‚´8ø·DT¬š>špÌœz - aU)ük”IŸÛg#Ö5,Ãe¨¼vʤ6hKé¬F¡1ð¯r°ª<ÆÏ7Røïy¶[‘S‡¦Vò²EÕ³×Gÿ/bÿWz©lüxC—i”Krÿ?à#»bÛ÷lÞ»çN;à¹çç ÿ?ú7À;7™6$þ·{ïÉ3סû|tHüûú@³žxrÄðï™>?DZ£ÑT)Kàÿ7ÜŒMßñV¼yã kSˆøÐ­ÛΪOÿ9³ƒª½?Oà¿=ŠñàG{ßß²,éç\ˆ«¯¿ ˜¾yÈ)üŠHUk@==:ø–ßò ù‰6ý?Þ‰gž}W^{C´8]~ÍõØê?Þ‰ÃöÛ º¯k(øDMV‡³Ô+ØKÌÞ#kâëdS©MÿÞ×LYùåZxò*ùþ³ÿÓ­åòØ?fáò³NœÃÌÙOà /å5Y÷ïUþÊ©ç\ЭQpÌQ]E³*â²ûÁŸ®qAû©¨'Å*8¨뢅¼ì*Ø]zú‰!RÓÔE¡Ÿ¯¼öWXuÊä.%«ú:íÜ‹p÷"Ïsó½àë_øL—®äfÎy¢Ê_¹Ó^¿QøÜ­wþN]Ë ·§ª0û÷|þØoâäcŠha§w1®¼öWU³5Á³÷œwÜs¶™¾¶Þr3l·×aàÁGÅ¿¼>òÀxžzJ­Åâ‹[ž*ðxÜ|ë°Ïn;á®{ïï¹òÚ°ï®;áÁGÿÞw÷ýâ´s/êQ+êÀ#¾"dS×\#ôñÏ~uSÈRSª–zÉÿ¥ÆU¯ûýSñÓÓO sˆ)NÇøCħñÓÓO ÷Ýn¯ãþÌzõü§Ÿq¤7sÎøøáG ÿ³Ÿ|*ê“áâŸ<îz`ã{[y ©÷óaƒsF ÿÞc™º–%<À‡ÿwÞÌ­ÚK©Â¨* ^H‹g%Þ+NuÜøYX–ÛòdŽþ59›©7)£2•ÁmäˆÊKÆçåÛÿY0U Ò¿GiML{©ûÿ¯;Ž=ü°®²kEkÿøáGEÞù~ø¿ù¶?à½?ŒÖ›ŠUV^ ³žx2‰ÿÉ+®Ø†ãï®K§ãu@ñ?eå•°ñzëbþüðË›~3bø5xÈøs(™’Z–(Z­(×Gçæ×O> ïÚäÍøÒacÇŠ…‘qdÎÀ¿¿Þàà`÷ÂtsÖ¸¶R7m)ƒk‹€I–¡¥ø?àÃÂ6Ó7 *pŠÿž{\p>s®ö’ÜÿMúé²lmüÕ Ü ?;}Ø^z®|ÎI›z(¬-U?ê¤k…Sù$ÊK·(V’²Ö,PõÝs¾÷ \pùÕ¸úúk óFë®ƒãŽø4vØçྵ@4ÑѲÐSIÛºéka/æžZœvëÐÃÞ/-@—:ØiŸ[bV…oË;¡ÞcËS¡Ÿ·<3:§¬ù¢ —.”µP¯Á¥M-‚Z,²Á’¾ÑðzŠSÌ‹¢§„èu•Na}6Å™nð?|ü[òÄVÿ5øoð¿Là¿lð¿8øÿìû`³w¾ W_w#NùñùIüŸpôXóµ«×d±‡Gÿþ8ýÒ1Iüï¹óøÐv[ãÖ;ÿŒ¯žpòˆáß9‡WMZ.P¨ ”þ?ð~xïfÿ­>ô±Þ8ä9vùÀûðÙ÷Á¿» Ÿúòq5üfT'(`ÀÜyóÐé”(+ž(‚X–‘Œw·=£ÿÛ¾g ì·ûθúú›pÊÏï‹ÿýöؘ¾9N;ïâà }9÷ÿ±›ï9:ÔáüàXUœ­ä?¶t-J„56Ô$=k³KUQï†L-ŠÊ3V>¬U·Ââ¨kjM~3‘¤iñç-"}/'¦qØÝR”ñr†Ü¯ºQ©×†©#ZE˜)) M Ã3à™žPãý’7X72ï©Òb{–´±†ÁuŽ0‡Ÿ™uÿ­bh:,Þlj~ëø)¿ÜJÆWŒ±gWF?Ϊ ¦íæ¹cm –œjƒÿáá_UÏ,Ê@ƒÿÿËþ‹ÿÃÁÿ9?¹°ù;߆µ×X=‰ÿó/½:b´ ó‰A®»ù·Iü¯²òJøÀ{¶€pÙ/®QüçyŽEƒíÈà);¨(jî)ŽÕk_;é4l±ÓG‚¢œ7lοôJ¼å}ÄaGkâ¿ìt¢Üjç,XXEÞÍŠæ†ÿ<ÏÐjnü_ñË_á´ó.Æ)?>Hüÿðœ‹pÚyãòk®‘ýTäY‰––Ì o@Vœ©Vgóç-9KKaH-ò”õ¬^T ϲ$ Zÿ©IÁÊ*¼ y׎o×ÀÀ@<iRsQuñÓPªÊaú¶0Ø»¦ô®lm¦V]¹ÉV‘1‹¶¢Š:ÜoïT=꼑pr!Ë9ZmRõ¡~9Lû±ª¦§ªö½*©hâ»ÖOI-¢©:45Æ’~çblêÑQn½uhð?4þÕ+­ø÷óªÁƒÿeÿ­ÿ‹ƒÿ™³æà†ßüÆÃ=0‰ÿßýñÎU9PâºÚ«¯¿ÉÄÿ„ñãñ™ï ãÇãg×ß„?Ý÷àˆãpp°n ÍÙŸsÿŽ©^yòŠjE:øwÎa°Ýó§S–hµzcÔnwºÔ8ÿÎèÇÿU×Ý8lükÚÉ˺ÿ)ªe˜·àçgÔ&AŠbò`²õkqU-º„~ÖЛªþ°d«ZÕZ—#U!YCò)n3ߟ“y½×Å{,Um‹z[¬Inåˤ*`§ ÍY@Ô°¦ +Lj̓Ե´=ÊU×qô_Uܲ²:¿RI}L}HÕ6°6Hå§ÂÖ©­…«ò²ÕO|pÐMÞ‡œuÓ·rùRõ!R•¢SüÞÿ þüÿ‹â¿lð?\ü·Z-|÷è#ñºÕWÅmwݯ|ç¤$þ×Zcu¼aƒõ‚Î_›™Äÿ§Ø ›½ã­øëc3ñùc¿‰ççÎ{Eð?nìXŒh™QŽáà߫ʱáãÏ‹Â^P  â… ºýæŠ>øïªÅebØ•‘2\ƒÿ‘ÙÿÇo±ŠeÙš¼åN_n?|‡)±«¡S^ˆR@Ò I« 6jÑZ*•Ô…×âÛ[aQKQH?¯‹‡?ðèÀ+Ï]rSùê±ÒBg í¯TÕ_•ZúÜ7|xÓçV{¯“†–Sª"Ê/¶Tt£ô×WÎvŠÓž:¨éXò<µ>+4Ì?‡%§ìûKR!jÝØS^"½{2-E2KeË¢£XmÓ|ƒÿÿ þüçEÑü§ñËmÀû·Øk®¾*þ}“ÿ‡›~.\TÃÿ?Ÿ}=úW<0ãQ<ûü\ÿËMœ€#ÙïzË4ÌaŽùþðÙO¼bøo·Û7nl ÿYüg\è4ÿŒþæ  ,Ë08ØÆ¢E‹‚ìõPøïªÅ¹ª´B%qç!ýªÕàÄöÿµ¦-ûFТÿЗ˜¢4XR<8VÁ®”Œjª"»*lpûR \Ö54ì›â •°É÷µþ®i [óá+5RtëïJµPN²¥‚¥ÄT_ë³ò2+p÷¡½ªŠSƒÿÿ þÿÅñŸ5ø_ü¶;¸ãîû°É7ƪS&ãý[lŠçæÎÅ£{l±ð?mã ñåO‚u×^ó_X€ïžñcÜuÏý¯(þ»†É •]ïƒø6ôÁ?ŒùäÊ .B[ò\â(¯ ”7ÿ²âßøo5øÙ÷ÿQaufÜin,¼€©b+’¨‡O«’qª–„M$V)Ô¡d#ÕÊ· NäF+ªì¤ ·*ô²wÆ¢¹°¥¯@Po³µ˜§,zä:±Uµ‡ßˇ:åÑêQPêâ›[ à)…Ë›§y©š,–¤®æÐë"£}oy…¬,‹‚·*ѧÀ–7ÆJ ·”’RÜ{¡÷Ë×hðßà¿Áÿ¿þ‹<Ôaéyëü§ðÿ̳ÏâÚ›‹ÖŠÕ^3oŸöF¼k“ilâ/ÍLââ„ñxû´7áóí‹·~/&Ž{š£¾u"î}ðá¥ÿ®’¤nUý ÿŒsûø‡1. -ÂÂEƒ~Kç‚Áå©ožþV§²9´Zò<ƒUÆSƒÿ—}ÿ³öÿ[ös‚æ_}ÚbIjª„¥ÅgT Ç¿\¯›µ.¦JAIM=pRaJf3%k%`j_(-Àªì¬UŸUÂRûL7 KÆ’µO‹ªý®I‘–äP!à'ÛªíÀcå««§Æ’›Ôù˜âå¦êh?iZi*ù›º¶¾WNž–l«ÖÊPþ±u Yk_ù¾áZ/ þü7øoð_çÁÿpñÿÁ÷¿Ú~kL?>|澇f`îÜyøëã31~Ü8¬ùÚÕ0qÂxL^qLœ0Î9Ìa.ºâgøù ¿Æ¼ù/,uøhµ0vÌ€™Ëøbñßnwç â… kø÷9AívEÑÿN‰¢è)ÉåÕçüÌþ?~‹½FG KКà:©u€t@¬…‹­XæT¦jU**tmñÌý==Ð,ýwK­HŸ­_‚ ÕGúYÿ,ÖC½ º(ùçi·ÛPR}ÎÀQšõ,V‘B`Q@X¼o=Ȥ¤OuÁµÚ`^©œk-¶‚Û¬÷d‰^kl”lq~-oµYëüU¾6Ïcïµ²pfqÆÙ“ÏãÇÒ–ZD®Áƒÿÿ þ#Œ• þÿY–áÝoߘ¾96ZªZ¡ø(² ÷=ø0~ûÇ»ð‹nÆü^Xªñ?nìØ`EFWQDÏ•å9: üûzC¾o.Ä‚… ûâ¿Ó)«o$ùˆQƒÿ‘ßÿ'nµ÷è2‚RàPŽo§ÓÁÀÀ@$E¨a6•ÇTωu€°BŠ~ÑboA¿h©Ã‰ªthˆ¯ÇÜQ­&¬#«ßRÜÒ”âOPžˆ–ž¼z•5”™òþX‰s| ëW™fêKD¦4é-¯…U€K½¿ê ÓDÆTî€.@*Ê^=«Úz¿±òó›åáäÞOñÒÏ£¦óSÇÔ:˜ZÞ>ö¥f?ù$žŸ;o™Âže;v,Š<«w%xŒ<ÿþ÷Áv ,DGäÒ‡‹ÿNÇ™àß!D€üÜþ?~Ë¡…eüË’oµ¤/5̬JFþsZ ÊêLõÐ0 Ù²Ö¤-¥KhÈ2UÈÊâÚ§8ü\›ƒC›ü\ê1Iñq­DQþŒ¿Žç¦*àõš\äM«fžåiæ°«†÷ÕÕó¼´#ÏŸ¿oZ-^•TtågÕ ÕjsíÚÅ‚eyS‡)^È,º…ò°­ªév¬¢šV"+Ï;õ˜Zɺ¨³çÎÚœSÏÖà¿Áƒÿÿ þ—<þï¼ç>8çpËm4£ÃËþË,Ãà¼y3f Æ  ¨rp:ÁvDí-Ëò¼àP䲬+$±pÑ"ëÖk²DÎE[«Ü ùa\x…¸ÿ#çܲ_,U“´xq°8½üŸÃnšÜjY–¬ð㯭ɴFLiœëÀ¤ø»–d-oÂV(™ÛÃ*Pz_KªSûÒ{ ,^`¹Nˆ—§µ8ÒV¨–Áè?«ÜPõFpy øzÚÿVpyU,QéG>À¤¤„ýñ‰©<‡úÕw°hucNyRtÓÅRH+Ì­sSÛÁɱUĺrµ¦†n^©½US¥Áƒÿÿ þü7ø_ü·ÛmÌá,\X‰ãÆŽÁرcÐ*rŒB>ïç… ¢þ ÿ^¡t¥óâiüEÕv¯ÞÙàDð`Ù7‚Ôµ*q릊'ìÒ×,Ñy²ú…@«—ûkZ‹ƒj¯[Ï3TÁ,K}È·…7^ùÚ?œ¼Èž‰ZüTß_¼0¦&,/Κ,©ÞfÞp´0£z–,¾¾åà÷iŸó¡Gû •HhÍGßçZ {ppP¼AõEÄò˜©wÇòîYÉSu,¯ ß˪FΛ¥÷¯˜Ò…ÉÅ´å¥ôÏë7f×ÿ þü7øOâ?oðßà?ÆÏ¢ÁAÌ›?ó_X€gŸŸ‹gŸ{ÏÏ›çæÎÃüTÊo‹0Øn¿hüyŽá¿tíN‹ V}åûµš«yW¡ÓàäöÿÑ@‡³¼ìUðah]<Øþ5_c¢ŸŒ w¤†ó4ÙRT½<~Ñ`/d*aÖÒî·Ô3”³É^ ‹ß¬…õ†EBÑ |«v†_ üX¨7Íß«#:û:¾ÖÆÑ 5ÇàäP¯Žµ8^pÕ»¥Þ*#ÕèçEÁ' r}_xzŽ.ív;ÉáîW_Â?»zÔ»£ô_öàX¼b•N%‰3ÿÖJÎÕƒ‡R4¸>Gê^)©Õÿ þü7øø$üç þü<þ;¾¯*Ùî,ïYõ¹AE‘‡Â«moÜ5ø‘ý™7‚RšüDVž¨U¼Ì [µ¸ð—åµQ˼_‚†Ú9üÛ¯ð %Ñ©ÏmÝK=‹ÊédOJÊÞAÒXÀ¬¶êâ§ Ð…DûŽ‹<ªWµó9yκ'ƒš),½¨T Éû¹eI§òBb…qýõj^2­¿Â‡&åÙêó(eGùÀêm´ÂÄV4M¶µ>kmö:κÀêÜK)é<¶^oðßà¿ÁƒÿE…ÿ¼’ÍÎü7øYüg‚ÿ"Ï1fL…ÿ AIŽû­]µ­ÕàÿeÛÿG…dqaÔü:{#4ô« Ž·d-)Õ~ WJþR'Ÿz Z­V-éÑÒÝ×Ä?«:z¿IUG4\¬¼WkRk9«#O\Ëpå…(Õ~}?{¸{¸ôú¼Y¥ŠxiÒžU!Ûš3–Zå-á…%•¬gy«ü}}©7Ϫ ^J+œÍ}gÉhZ¥~’®ÖB¨ÉŒ*­ÌmáÔÒô·dµ€gƒÿÿ þü÷Å«@Ù‘±l5øoðÿÊà¿,ò¬+˜à©qÞh Uƒÿ—mÿ5F>Õ¤VõÚX‘$?Ù-ÕMcΫZ£–%mqw--v!¦Rs¿g÷“€“ét²x’¼ÀYr±–ò%g©4E UZÕÜ­b̵副^ ­¾ž’ÀMyì,yRõDYãn-Ƽ¸h!J¥è<ÕdL-˜Çê:|hê·Ps1×6u¢q¶/ IDATKöTyÙšÐèå8Õ Ó¯0¤²VŽ»µ¹6øoðßà¿ÁÿøÏ³pÊlðßàÿ•Ä??ÝöV©Ë5øùö˾D¶†þ´Su’À#›~©~žu~ýÉZò ¥ï®žõXµÔŠj¥¨*%j“$¹”šŽþݪâs8–¥"™c¯Ö¿ÞvÞTè;U3„= ì­àšÌeÕ>°ª‰[:öº ò¨Ú^K1¡µ ¸­1Q¤å!ÒSå6­š©ß-åõnñ"¯J[V_ …½Žœ9B¤‹±n¢)Ê ?sª¾ƒò¦yüü7øoðÿ2á¿hðßàÿ•Ç¿W†Ëó¬’ÒFÒŽÔá² yQTjs þ—Äþ?jr‚¬ïoÐR¢XšŸI'..VØW½+°•ó<”ta?/ {ZxaS¯Œµ!ð5,eî ÖñO;‡\-N°&Òy>2VS-ª{e,o {*TaÊúoõ/êVÿXɼ1¤*AóÆÁ!oU@âûZu ø>z HUVWvJéÈêË#«ÞØáâߺ·Å—Oˆ,î¼5®êõbU)Æu€jðßà¿Áÿâ¿hðßàdñ_:jõú2CŒÕþó, rÛ þ—þG…¤–òjXž†¥ùKCàº8XIg–Iy¢Z)¸_4î+NÕ$@A¶ÛíÈ+¢ VOqt­0¹.HÜW)Ï–<¥³Ÿ*Æ›Š%S©tŸ4©œ`¿Àª÷Œ×v%Åi)Á¨D§Ö:QYI®S¢g‹ç­2¾Ü·ƒƒƒI òÝS˜Z/@Ãè5Ã’pÕÅýÅàßò&qÙ;¨õcøY™Wíû[iºØ3UA½‹Ú†ÿ þü7øoð?úðßîtÐüVø/]ˆee¨ ¦ÿKÿ£‚g%ZÉ[)«yi¥÷i2Ÿ%»i)©h(Y•ktAèW|+Õ. Ukåjå̪—K«®ë¦¯õ¬š „V«U ñ+X= 2+ìn©ðX<`KÍ„½QÜNõDéâÝo‘ѱI©®X{ªz´56ZÔŒ½F©º¼éû÷Ɵeh­Z)Ú{h-úp嬾ð R,ú…nžV¥k•‘å1ñ4î#®<ßà¿ÁƒÿÀ§Œ"A þü¿Üø/ªûxiìN§ ¸¢°jçdpe/ä£Cƒ þ_þÇŒ#¨\T¢1h™·ÂÚ×Ðëô,œ ‰hQWžþÚEW–áý|ÝN¥%,VÕjµP–RA€A+¬×!¯ˆ¨•äEµ©ÝiwÏ28^»Óîþžçp´ /õIII™a1©úY†²êKߦ¶Lhp<º_XŒªö:çPR´y’Ò"©}>ȧ³ ?ù=ÿµz]ä6«“̓º4I³,CgQµIp cÜ÷Ï¢E k ½z_üØÀx~k£cYµ½¤: @—+ì/ÿçBr%Ï0﫹ÍÏéûYóg—ÅcÃíˆðO}ã¯Ñ¡ç(òøý{üðø¿û1ȳ ‹Õuœ«ð_ÿÒígWeÞýÞUëÍ©6å!«Æ¡,¬7²¬5WµyR¼‹ïÁîû àŸæÞøÏ*üçÃÀ?²`@•Œæµ¿Ç-‚+ò"¯æ áŸæ_À?#£¨ûZVÃ묃 ê—÷“Úõ³J‡áMØâøZ (¾Ò·u­àe4¼z€©%Ï•­ÊÉ:Léìó¡D7…~¼i+4Ì¿«%·MUfRamî'KBQ ;ÿŸ©©ÚZŸAÇÏâok¸W+}³þ=oü–T¦ß#>õ­DTN/?'f²4¦VWNÑ,µN–e.ºöYªŠºRy<÷\i)iKÊ¡öÉ™¾ ÷pòþü/jîbáßP8óui žxn7W«¯~ç÷Fø¯ ­€ß?¨WYcÈsY“ÌéÀ¯Å #<ѽÿ¡ˆbeŒ,pþˆOèåœ?—ÿõ&ϲð?`Å6öï)•.âþ²¾æšbHá.þ­:*ŽÇ·þ«:ÏmOÉÑ¢žKÿYÿyVº‡Ëê_wzöÚêàÐîtÐ)ËÊsNÆDÖ;ðFlV÷h³ÜÿcO|w®dÑgø é/Ï@¯ÈȲ*Ï$±ÿ{£¨:dwŽ @þ¬ï—<Ë£g÷í¬á¿jK0\@ø/ ÿÈ¢vûç÷†V÷~„'øG/úÁ}០ÿ~|Ø ÏF÷ðï® =¨9Egÿpá5‡®TäEtÿ"/¢9yþ‡H“ÇA5“ª÷xc(Ì“jîcÉc'ÏLe¾ÞšÙ‘þ©>&þiîÂ!d&þÑ›¾O_þ1|ü #È:Ø«G–+ªŒÂê ,ÃgÈ­û*ÏTá~Þ0õ«¡ •ÃSeëp”’!´èx–Ü"s/­ëé©Ñ~¯åcÃE+siKæ£|}UðÑ¿{5‘~»T¢¤~6EÇàqä¹¥‹ ª=w–¯eyR-P[í°*e[ÉÀ|pÒCºeH³º‘Î>òµ¬ƒ’Ö<`©MU®áƒ¸eD7øÿ/å«c$1/þýëb<ÕðO}Ra“”áœú! ô÷÷Onðá}ô¤G—èE|¼›ï[‘=Žx¥ð/ÊKþñRñÏïñø!ãÇĿҭÈQ ÿ²6 ‰ÿì’‡¡í‚ÖwÅ?ˆ’dâ_Šp.þI-,࿊†dHà?ëšs5ÂèÐØ„d D8G ­ê_§$ªYeEQŸê½jãñ3xCľۿ±!åÿ៮gâŸ(Q=Ãů q$'PŸà¢¶g¹à߈¢åÔ®¢le0ð_ºÞß½AâÿPÃÎ…ˆÿÏýÀ÷®õµÇ?|Ä+G‘5ƒ´‡<Œ{‘è”èZ²S[7¬»RíØØàh?ÿ•áê#Glœ¨á WщJGø¯Æ>Pñ|ÔˆçJÞã`‘¡Úétzó‘æ¹7Ä}[ýEΆÊh.{ó´SQ:{Æ.]ƒôÑ ²4õõ°¤=.¾Ä²ˆ¬ÀÁ›U¿ ߊh¨×ØòØúöXImÞóÍ›ŽUy™ðªIŸòÚ©”#kækul¥ápÿZj8¨êˆuÈÓ{¥oZ!Ù_K+_#UêýÒ1ãùÀ*&ì•PéR®xÌc§J::v~Zí±Ô`4š¤‡]ö(óçU=Æòè[Æ4¦†q*ê•*„¦Ñ~^­˜®ÂYiG Ëÿiü/‰õµ”ëÖð¯‡ïÊ(¨a|€‡:xG¥þ­u&—>énžˆ ®¼z-g*¢*e¹_«µ·ÈˆðœH¨Vü£þ‡Ù.Š¢gpÒ<Ë6Ôð/E>K2dù€ïÆPü ] ›Â¿î¨)‰å•‘”¥öžS€Y…>‰ÿhÍôó‘ö‡<º^ìyŽ 2Z,%ä Ð2PÑ¥Nu‚ñ¡?Æ?(jQaÐõ<ó܆”ö"#]*z7Ÿ"¯Qãº[]× KW†v:ÇÆwÿY^QÊ\¸N&Ñ£¾øw±ñÇ4Bß>t‡¿# ‘=¥ú—¼Á•W‘²$þsÁeÀú}„ ÿeGÚÓ‹òð< †]IØGœûRäE ‘y£¦te˜»lè°Qª=oP„èQnP’‹ó…J®þ+c*Š~©BÊNl„gyµ—Ò0ú9ãÿúÐõŒ,ß>ÿ¼£J"B[`ùI¿8úª%Sê¿óa^© |¥eùÏëBkÉœê‰ÿŽ$"{¹Ôè "‡åYVJ”Ò“ôÙý{½Þ20t‚j’ïÇ_*ýiE8´ ³¾®Ô$n§%ï©×Ó+G&”¢dÕ=Pu~v.Ž¦ÆŽ1|°a:œFx<|¹–5xµr¹æ]°§” õú[ÊFV õ[•Íõ½lìøÿ<¯¼ò…ÃÿCãÿ¥~et@î‡Ht„sf"ükn"K¿Š£¦$='ÿQ´pÉ÷Y,ü{ξPÃó¥Ù¾ø7Œ™Å¿Œ ÿ´¾æ2ßzjÔñŸçiükÄRÛ…ŒÇÿý °ñ_–ÝÿŒÂÆpñ­<ªÌsúLÔ½„>“Ñ}}žHm ½üçCbDÓ¡þàÚ¡¤÷<ËÑît"Ã%Ïò.MˆŽõÈR5?@û¿Oþ–D§,#c¢]yá™ǹ6]‹#á9RøgOE“ܤÞaßEQ¥``0þ³^”« Ê_µC¼…þýA¼ÿøÏx=C4ÿNðOóÁÄ5~³ŒØ‚ñŸþGµø|ņ‹ï,ÏB…#%¹âŸ)ˆLE¢KÞÈ„ZæYÒ\5¦ã±íÐÃm§ìô¢PYÿâ ðNçÊN¢>¾Ýe§ÂSìÂ:5Œ Ë›¤aoNÀW.·Tx"Y^ =$éç­ÃŽRIø–aÀ5¼‘›°Z³¾M,è\Xª4T¡¬HQ*ï*åí·Ät“ê§ë®Æ‡Ò¸,£‹¯*Iª†Ÿ2|_©Ý÷•å½·„ ”²b½Æc™’¦ÔBwšO¢FDDõIDaô`⣩q¶je¨·½&TQõ‰_¬üuS¹WêÝf…qçh?Ž–pCƒÿ¡ñŸúzúé§ñôÓO›vÜÿêTÿBÛŒ<ú~®Ò¡Ç¿§†9¼2¥I1χüþ #¥K˨ç©Sf±ñžàAfä¥h´*åANâ¿jsÿÞa#}a⟠¦¾øç|'¡ÀE>e¼kø¯r®,ü³ƒF)Å\.ùQjDvȸÎ)þ¡ðï‹pHÁ¦õæ«‹¢7Ü7]êS|¨ËåÐïÍ• Çó;Pçªûµýz\Ñ«BÄFè\YóàR9[| 4*‰Èê¹IzHe£!YÿUDÆ¿‡ o¸ù,ßo0 ÿèÞc#ŇFï|D¥$CN)u5ü£—»D¢‹ˆ™f„dAÑ­·WÒØ@ðïzª{ Üµ` †>-ƒQGFnçåxƒ)\‰ñïßWº²µñ¨Ñ#K¿O ¢§E \ð_©Öü#¦çE8C¯è±7²ŸWVBèåUŠb©5`ï®ðYÕKéJ£ÐÄxK½ˆ+Öª'Š7Z¥Yºúªz¥†'º¥‰nëRO±.ÒìQÕKJ<€“¬%ßþ¦]ë •â•Ár=|鯖Ü|ž)½FÛl³ŒA64OǪpÍžuP¶(h|f#ÄÊ ÓBe5K#ˆœË¡§E#LUš¶êhtƒ«XëAO£™ þû㿟tÅWàŠ+®èk9`xøgƒ™ówúá?ahe W4fj¤Ñ¼†é7Ž–DøúZJv±ñÏÆ+04þiNûû…ž¬ é?£¹;Lg‹¤šÿÞ‘ä&„5852¥õE8*äŒq­á¿js§Óéå”Yø'Å<}OôŒ2õJâŸþVVÉÚžrŒgÊIðFFQB;½¼š(> ÆN/'%Žš„hL ru£AÍPƒ#Ë@9Dˆ QPÊ{iOlÀ Á •8A0F2Äô7d‘Ds8h;]kê†Q=¯ŠEbë§¶XFí øgÚ"㟢xyÆÏSÍ´Í‘*\tWÚc„œð‚lvÞ228:dPóðüþlàbü‡}&Ï„šI÷ÌÄy\ºµ ˜)%ú–ÓwÄøga†þ)?*Dð\Etˆ =u®¡Gs˲,bóÇ¡e"˺¤‰*’Y Ôþ€å)'V¤BQ­bYL%ÒÊÉš‹a-´êÍSïº*|qÎŽ•ˉ*ˆY‰ò;õgöF³Â›z²RÆõ]ûr¨¼U0Óƒ•öRˆÔJŒ­ByV´Ë¢±¡ÅUÜPÛé½õ–Œµæ4qÔŠÇÚÊ¥°<Ô<'­Be–ÑWJ’w¨ Ñn£ÕjÕ¢J–¨€Öø²GŸ uͰÔúü÷ÇÊÚvÛm±í¶Ûö5„rÉÿþû¬|€Vу—„ñð_–5ê/þ#úMò•ÄeèŒ7&þýZ‘ ßyƒ/#Š ÆCâ_ò*r:à× eQ¼š‘›e¡â}h@nxø7r²jø§œŸÜPí ê€ÙªIð“˜E†X("ÐïÄÉRÃu"Ú(nq6²|]+/‡sZذéIGçA‘¬G¡bQŠžú\„ÿÊ au­ÒõîE’DršÇ0ŠFùüÆ¿HnÇûl0µ*¿!8aâ=9ëȰ"ék•åæÃµWm‹ûÞ™mä<Ÿœ$˜-ñ 5ÔjøwLoË"e¿þƒ—×r]²¼;q;U£W¾"G^ô(})ÁŠ(€REnü|`ÕÅ˳:õÝç{kTɹبª9´rîÑ㜟`XqäÍG¼*#.r´‘øB8ÏuÕÿÌç´Q£g…ìSÜ~õ[2ÄìùÕ‹7ߢ(¢< ‹² žr:pØŸ7{N’ÖdxõXó j¢.Ë-«×‘ŸÕÊ_Ѓ'+xiÄ!ÅSçë*gžï¥yGÚ׺ÁêßÕp`Éb5üœqÐ/†L³²”Á”^¥íñ×VƒŒç˜%Ó¬4{W8¢ÃÔ'5,És¦‡õ«t­ã«t"ÿ¨ü¥Èh˜ kh¾†FzÔÈlðo㿟´Â +`…VÒâùãsNX £‘"#Újå½$üSÒ¾•ÏŒ` W_üS¨”(K_üÓm3'*Ik>¬s^TÀ¿Ô£bƒ¿†vä V‡Ë%¤¢þi|¹}Jº#}ÜÿEâ|°aá_¨Á&þý¾'mKNÒê}ñŸeæáÞ{»9Ÿ¦7‡zÞ}Ÿ_”sEò¼Wä1D‹(g¤UÕá7V}ó‡ï²¢…qô+ìq>ú“gq»¢“.R«Ñ¼è€ðŸ þ9R$¹JQ†« GÁŠ(ÝN=ì[í³6%¾¦Ö¡±’ÖUí,•0ÞON\ûÑJÒeãF½Ÿl0è¡É2RB0¸ô¬Jç禤*3kÕwI9,YdKõŠ ozp´ž1•f)~e‰Âª þmüó×5×\ ÿå ¡k®¹ÆÜ¼ø0^TkEÛWf÷Ô´áâß¿·þ­C©à¡êPduü#.Úœ”7¥¸±¤7G†Ä?Ñ×øòêQÁþ¥š£Ïöÿÿ=Ôѵ–E \­VMVËMâ>p‰h^)kdn´Û2LÕè{Qø—È–‰)w`âŸòEZdÐ3ý“ Ô:­VQǧŒD4¯†#Q/Ë£ˆÞÃÿÊ*G%ÏB>‰&Ñ{c —\@kŽù÷úˆS‡ŠA3%f‘‚þ©ž•êmYP]üC÷ÿ˜rÆêtÞˆ¬áßñþoà}ðïbIoVŒ‹òdª|¡ÅÆ&QAÆ?ÿa¾ C|½Žâ¿Óí[.+É®“HÖèñ"µhžCMÝ-:–RrÅçURÜ5üÓëA±OÅ8ú…8×ÉÅ!ÔÚI³»ÕÔúþ#Á«Ñ R»Îu1Õz"^ž—©$êI4¹üJ-³xÛÞÓÉž÷~4)‹ò ´õ +mĪ”nånX+ëþ–*›–¥y-uHÉHë¡T#5|­Rв*÷onVÍâð=5‘×?%ɬ‘>tk>‡ïKVA±¨F)£ˆ£7mP»JA§ª½sDÂ:Ì"q@´" |pLÄÕ\‹>¦} ÆGƒÿ4þùk×]w 6„vÝu×:ÎÀ¿&“›ø7°[‹:Xø×zu†’ø—B›ÿÒO¬až4„zeâŸ$¹D¿À†µ·—ÜKý…žÚcQ”„<ïPø¯Ez꘽œžäyÿÉ":j·~à:>Qæ,ÅÊÄ{nâ_¢‚%Î5×L¡Šj~W ÿ¾à´áfC±=Ø&üu~#Iùœ‡Ã¹H5J­Ò¤ô0NIÝ>"àÔqTʩɳè˜Q-#îk^È©àhˆˆPR/©þÙ€rˆd¢þE¹.äí ÿ¢ym¢R¹èóþ Ý)ËH>™å¼Yk÷Dø§è‹'ø>ŠðŸø')ðžC–1]EÍÿ.¦ï±óBq’9·Í"ú˜FÜò,Ên¾žŽUô48·}t‡%·û,]ŒÞàǺ$%Âõ)cük±ÖšcQä¶…Ô‘3ÂNs·ü“qµÌA~r DÑ–뵪I«gYÒ-/¬Ò¤úÑqÔ«ËíÕ¤mKµ¿k­„T1M¾æ©_û ¾ùša…µýš cåï¤Ä <µ‡9P&2[Ô•R¦ó>°jýðø)Š$ÍÙQŒi䯡ÐRÓ<g6BüÛø_’N&.v:,ü“À@Qâh„W4‹kn Nͱð/Ñ ïùõ‡ ÿzÇ:TOÿ¤á¿2"2Ãøò“dÌjø'z!Ó×^4þå^Έ"q´d±ñ_µ¹È®ï댌&_±ð×)Ä8±ðE§J2Ðüu8š«rÝ,¶1$þY.ÛSý|ŽRÈ3è*vÅF¿ ê_%)œi—<ÏC4ÿΪklDy)aŽÌZ•­¨åI=U±©a:Ÿ?”+E.ʉÉ8?JðOy1L1 }ECaRóBò|ˆð½$³þ«×køwè,@PÍÓ¾éåfÅ…e}4¬$šc”g•g}’©‡Z£ÆÄ¿DÔBŸg½hQfŸJ) jqRÌ6Œ5MåDÕ¢¾Þè˜þ}½ÿ^øÏ­ß‹žw1~tÑOúò•c%©sõl=lüàø£±É6Æ&ÛìmlÌ´Z­.„ÊZ“±UK¿íÀÊ—°Ä,i_Ñg°ò:8Âct–b•R”’Åc¬ãäŸ!7êXÑŸ~ô,(=ÈÊ{`£FŸùy5Ä*<ÉÆ\ª>ÉQ)vV(\Å4tXõZ,j£R^4wJó¤4ÊÁ‡ºÅÅ¿%BÀaòÅÅ?çÇYtÊ%‰+÷É¿%þb öàk$C æýó©è`¢ã ßš?Á°1f¶ó¢gÍ ·g’#è{4©x^ uˆ¼±=gy^æ¿[Iݾn†o[¿¬JÎå!8ç°¨’¢õŸ‰ðÆr¹Ä‘‹¨.YL-1û’?O‡‘à5¥~ð^ÖZÜGª´×æ%õRIï4¶z8©åðÁñóé½¢gw0å¼Ã5a–J r¨Q–Xî7ôC dE\XLÔV)IR—ˆ_KíÇþ=ÚÑZZvs=,á"‹ m­[ÖýÌycÔâÒv[ïé÷zÊyYÿqOîcë:–­Š^è{Sòø:wþóô˜úû-’|šn”ÆïiçWB s(„{?gÍ3Á?Gqs.¢,ueLëõÞŽsaŠÈ ú× IDATzo¦×¨Þa|»<ŸÞ«úÛØI¯ kt$8RýÎÊ€¾0õ2 ²±œḣpžH³žx_9ádœtöyxÝj«áÇQ™ôЙ#€ ¦®U&¯\óx§JÛN¾ƒßÔ5×À«_µ¼é9SúQð®¼ö¬:e2NýÚÑØ`µ¿¹ý¼°`¦ÿÇ;ÿ}èøÈ·ÃV»íð?¶ËŽøïCÂ¥¿¸›¿ã­xõ«–Çon¿^q5ŽþÔ'Âï9ôs€oéplV½ï… qËmÀG_žw&Ö\}5ÀC¿þ®ýõopÀáGáK‡Œí¶Ú"´÷Ë'œúæ¨ÃÆNÛ¼ãÇÅu7ÿûþKqNJ«ˆkÝ(Ë3äÈÍü =d«â{Ïõ°nKž*ÄÞuþœyaâC¥.ú*®ÀžöÔ\bYpë9-•5+'£ÖŸ‰ÈŒÕ¼³…F6üÏLSë'.¡†'«i¡\jpßsôE/úå ©±’ò¨iÒ¼%OýbðŸ’i·Š€j_i4'eĦò†|¿Z}¤&_ËcC×1‡ÍZ´¸¬ò”Õð_ÿJ.fÊÄsd&s™AŠhTR\S£kµçt¸˜êdFЋh„…ÇK£.‘aGýU‹qT92VRµ`¼YìnÀ²Áåudõvø÷¹¼š[.‹*Õó8Gêãž¹Œ# œSÁßkE*3#’’Õ¯¥Q Ó9˜ÕY­PÒÆcÇø÷a¬%NøG|xðo`ÐÄV”E}¯F`&†ç´PÍš$þ Q—Z[yî&œ}‘a ;U‹Ä¹¸­ÜçN7­¶†UQœn+Šö¸.‚؈ˆ 520Š"¯ŒÿÊ`òÊmeâÙ« Ô³ºÊu•ÍâŸÇ*\åÛãz¿;¢G[£^´‡£hYÔX<ë[^Ê>—Bª¹í 9AªL¦5$,.6óó·™¾¦¬´î¼ç~À[Þ°1þØøÃŸïÁæ;³žxGꀎ>¯[m5|òKÇàC8í¼‹qÇ=÷.ýÅu8ጳ£C§ç½À;¦½ ßÿÑ9¸Æ#x×&ÓpÄ!à{gƒënþ-ÞµÉ4ì¹óŽ€•WZçüärlúŸÆ3ÁVï~޿Ŧ8ጳ1kΓ€¯œp2θà¶ïžøènÛÿt7¾ú½SpÃïnÅŠ¯~Uxþ7l°¾ñƒ3pÿÃ`ú¼o›öÆ(<VQãmöªÁ#Ò_€¢UD90V®Ò«´ò7¾ùp©O+„þ>¯†iKí° 8­Ec)šÄnEŸ¬ú>|€ÖC6‹è‚èÛäéiš+”¢¦(TšlEQ9ªÄýfsz`ï—“b $èAŒ ;ËxJQêR5¢†‹á¼~/ÓÜúÿVŸjN’dVÄHó–ý\·¢cLLEY_ Î2ä|.FnÌÇèýTüNidÁ€”Ãet` D042–ÎCRltÈáC‘5ô×T+5R‚Û°§DDËß?ŠŠH=ð<èô‰ïÁ cê-2C4ÀEÒ°JG´¢«5Ê`•wÁçpÀ'ïz¹Éêyލ±aŒWŠ °aW£Þeˆ æ(Ò#ש­CY\ÿÅ2à¼Ç¸G?çQfþÙУL?è7¥AγzÅ ƒ–ªi50(šÂ† F ¬õ&.bì’‘#ù„vgP¤úQÌ€nR¿„š ÏÏY‹Ì”.ÚÕÈÉò8z­áyV3x”šZãµð§Ñáõ'2”J»>‹t¢³K×Èñ†N4~ä È5"$•šá R€¬>—‘ Sßx‰æÈMfØ:Þ É²^(Lo2’2>CP´(‚¿É銮#ø÷âNj^ñÚ¥QýQ âõhg µœ5W_ ßÜ•„½Æ£øÎi?ºïGñÏgŸÃ¾Ÿý"ò<ÇEWþ_ú¯ƒð±]v Ÿ]oí5qõõ7âcŸ:eYâ©gþ ¸ëÞûqÅ/eÝß/¹êç8ëŸÀ9àKÿu~yÓ-øñ%—âú[~‹éÿñN¼k“iøñ%—â#Ÿü\hï×Þ€7o¼!V^qœ}ñOñ_ûì 8ëÂÿCžçøÎQG`ÖOâãG|9.Ë2¼ó-Óÿuô±øû㳰«–ÇSׯ†S×Á­wü)®ì”p™‹Ì‘7 Sƒ'+2tÚäEø¨zèO周Üò–kÞL*RÓáZ%eY«d݃ó^|Â;B|ˆd%7ÎûHE9¬ŸUŽ ?ø·è‚¾]Vôk(J ÿypp0ªìÎÑ+ކiŸ·Z­H>Ýÿžòf©1hQ6,.¶UcG ÊN§„Ttƒ#$‹ƒ6,˜þ¨9AZL– -UÙÓO¿hŸe$ë8hÁbrªjœÖz*ËþóŸ_ÒÚZŠÔ¬Jêrd('¯xô]<à!Šãê{+_D# Á3ÌNPdÈÑᯬ{æ9ˆr™NEJÕу8ó}]œ;†_ÀY†šá½à\í>|^ê¯XFY„uþµÅM¥+ëÔ¬7w½ôl8l²Ú”EA‡Ú{kÊQY5Ÿ³:õ-ô¥« Ö"¡L‰¢çQ*_RD‘;–‡h‰E‡[‡(_L#Cå‹dY:²Ãó\³5Ã\’ÌÕ"Dýòv,ÃXsj¢¨”sIbíŸÕÛ^+n©9B}"55ü[!mW ÿÛŽ-æGÿ†óŠûŠ ¿²c×Õñç±"rxr¨-Ž«Efh·;½2Õç¢<Ÿ`¤—ˆm—žaÓ¥”•nçWHƒE ¢È˜u$`ƒ,CF¢;J}sè]ÇÂ?"œ”QQ£”)µ¼Q Ò⋜Lχ\õ6Ïšó¾úÝSpÒÎÅ«—Ÿ„KÏ8 Y–aÕÉ“ñêW-G{fÜòK|é¿ ƒqè—ŽÁì'žÄ!Û·_u >üÁí"‹Ûò\3Zžyî¹èÕ¿<6Î9ü}æ, :|l—âʳ€ß\z>þûГ‹U–eXeòʸçÁ‡Â$è´;Ažþþø¬èÞ¹¯´[:´ÛÝIÞ©{q9âSSMꔵŋETX#3ºëØhŽ…7 ,ºU#†­}b+«¶7¬{õ;Ìj¾‡µ8sôÊl˜¥åuóÒˆ«‘Y†—n0l8Xb\ŒV‹”*½Œ7 ¦·ñ!]Åt[Ñ?Úi®Güÿþù8²¢×Q5;ËÕèKª8›µ9[jw)µ.ݤ9OK)QÜOK"T¨7IRüWªp¬å#Q„A©Iš7ÂÑ*~çe]ƒÁXX!:\±·^è>Á˜÷"'Ñ5*Ïa”ûR«+äb£+ëC2Þ vʨ‚|4×Ph°E|(b_º22 •’">$1Q]=ŸÍ_—û>Šœ±4³‹Ÿ7¬Ë™A•E³E;óy;\k&ôE^dðµ` «å¼p4 Ús$šX‹)U„û…rU¼föœé²#ƒÈa©jÐáñ÷¹ÊË¿ˆË¯ËI+ÆÐøF-¹±¹{lYåjqŽVâÆæðú¯™};K7kóì›y¢Ù 7» ÷M-ÕQ>(“;êAКµ" ^)"rÂWo·W}ß÷ÊÊ~ë?˜Tíßþ럖ÿçcWK)E^pþs䟽é'äWü ùƒO\-×Ýx³Ü³oŸœúÄ'Jï]þ¿ÿü§ò’}«¼àüçÈ Ÿ½Y®ù/ÿ•XžÚ³›ªÅ.ü×?ókÇí×È/ù{îø¹Ïÿ…œõô§Éë/ýùÍü®üçÿz¼äEß*WþôOÊ'>õ§ò_®üÙuŸñÉÝ•‰¶@i¶ü¬‹+ +ë´ù÷·6[KZvî\ÅCó™3΄df8#Ã)¿X°²t,+@³.àì俯`¯gÀd†èØÇ`åy\ø¢s]Æp0¨Á Dex<ëÂ9KI:BÖÙkNkTô8ƒçîñŽyB+¶×kë¿À »vÜ®ùöpÖÉíµñœé%³ha}7·tý'nrΚ¹F'U®™}:7*6;4Ø@<æydÌpN‹ô¬’uLZm+ØP…s?rœI‰ C45‘©é³OÈA°Öæc¶ç‹Õ¬ó&9ƒ#`•:ï˜ÍÓ¼£Öš3Ò©¥ÊTF}wL0AÜieª7sÏ:ó)gÈÛ^7KÝ>só-òî_¸RzïòߺJÎúª§É…/ùûò‚ó¿IöÞ{ŸüÑJZk²ÿÁíwn¿ã.ùç—ÿŒÔZå>~µüÀ÷¼L^òâÈÓŸz¦|÷?ý!ê&sÇeÜLlÝûߺJþá Ÿ//yñ äü¿ólùÃ?þ”|Óyϲïÿöïÿ¿rþ7œ'¯ýÁï—§ïzª¼æÍ?!¿xùOÈ ¿ù¹ò_ø­rûwʯèwä÷¹Ž"O¥.[bülU7ˆ«ºkìîdÞù,çbðÃsÖ9G Áól(À¹+`Él¢Ù^šÕño–qe,Kk-ÍcZ›qÉrqÖl‘)É:ý‡ xePÅA¯™¹Ÿç ä ðåÎb÷¸µZžóC€Šë›Y—G²þÙH#;6 %匡LºÆAv³ëe%qS%Ù=Ž€×É‘ÃaÇ® Ù®å±ð¾ÊŽÝgh15Ï ÁBšHé±;èŠÿ$£ÝçÍ r=)«ì§sÈ*‘`klW”÷ìµ3I^0@`¶ :Ò67#ÝËíHž©Ü¥f– gHª@Á5´Sƒò:š3Áf!7³Ä:Ö deîþ@ÀÕzp¼+<ÏF¦AI×¶K7°ê€ô[¼ÿzà#ÿe%{G¢ã[„ž5³¹"»—3ç·5ÉÉLmMÏñ]梀@I,Åq-×’Ó…õOÙGÁ‘Ád„ç²<¦heÖà’ç1Ù>Ðv0^àžþlë ‹oFMJd+ãe¿×ñÜ«äÕ €úü`Ö 4Õ×-ùúïÜÄ"u‰[ãB†Þç°â2¤~åh@ç½ã×ûÁß{_¸¹2Ý>JbXb“ Ag—þ.âg¹:a+Ñõs1Ä\6bŰ7Ø1Å…ˆ³>:Û(ç Šøã¶ÌøoÞÖwüqîuyöbMʘ¹ø­eäd™,|®2àÀ׋H¾&\è¢l,”‰KÖø:³Ì‰Ù¨¸ð{(¬Ùü XüÙ×ro²¼¡ìþc`™ÝŸ™<,»ž\ÐãùC E+×?Ü·¸˜‰ú¥ŸøñÇ´Ç^òÆ·Å!ÔE•Ò{÷«ö¬˜e™WÈJŠ}–sa7go°›€䄸yž.N ÆÁ›ÙZî’;.µ)JÖPNWjÒ=§ {gáX¡ûlµFÐiz~WÂ\sFs>é÷%†yÒ'ëkd{8ˆ5ëð ‰r˜s¸²i³ÉŒ'Vï)á¾³™­MœñqÇR£² ÌÓûì¬ÃÞž¼ß Càƒ’5áž"ÖŸK̘„õ¿(}¸çÿšÅtº>²óG’ŵó¾ÞbA®2bV2¬ºÃú¯™¼ßœŽ9/btpÙѬ›Ëk $u£ó=¿¯_  Ìÿˆ¿?Ñí­Çöpk¬Š›:áÔÓ½¤š2³j©3Zþœ~áëŽ~c}Øg[ÎáÁq” ­IcpŽc§Tøµ.~VlòLgÐpßÉÖôw¦ÑÁg­+oú›­Íl˜Ð|Z:(ìËEFÝT^[ÕC—<@™ 2žk”‡é{®m˜(â™f Øâ™ø‘ Á,œ Ól#ÎÒ²¼"ÎJ°Rd¦x%uhb€r9ž± CºjÁLÏÍdà@gª8t—¯'ûº^Ýõ&™!¿?f‰þV¬8w߮ع©ÀÔŠ÷}²~1D•¥¶ÎN¶ç³ø@DÛj¶F0‹ÖÛJ0ØÓ@/ô8P¯Ýý]Dï‹¶Íî½ ÒpýÄ)ÏY]C°,þmlµ)Ã@z§9=)¶‡Š. PDV]× ßeg8À™ dQøz¢ƒ —”:óÆöt ³9ÈÜð¾Ë³"h#¬_3E¼lÝÔ ¹iç Ð졌a·t=¹ð òÞÖCïö“ ø~ÅŽüàÈ,â‘i±½kís%*ŒÁXa­§Ï«t?Ò眿ðL&ã 6–¿2Äp“û/ ëì9CÃûašØÆà>ÃÊú¯&•ä9ÝDØy·X@ =Yÿ ÂKAúìdgÉód¢õ¯2¹F²ºùÛ£öäïø<Êötò Æc°ßÀ!NףɟýŸÚ4Ï¡xÔƒ ¾ñÙÍŒ%>Yh'Ò©¥”1CY"\ ¯Ù2;ýÿ&¦Õ×Z‰ZP³lõ¿Ø…âÅßǽ½¼_›š=¸Ðô€7­`´À«ÔXm¶Ùͦº»4“CqáÇlAx¨-ïçÝÜ2·vµ[›ÑŸ ®pÛS”‰m<óà®eõ÷JêxCçÛ…«4që¸-©˜sÚÄ€TÎ'R£‚™5W±5Kf? \‘UQУ „Ù¨ è1:t(X9ãñâ×Ù5®'7_fXöú'Ù¾gºþë#XÿueýˆcÖpÍ=›˘Ë5ÉÅcn6-€GÁö†‚Z[C, ²8í!;l”Œ¡‘¬/./È×+ÅÏÐà{Æ<±+ÊÜP&æ Iä;=ߣ;î!#Hײ9Àe©=ž#-ð3FÜÝ›”»ƒï­×MëØ¡µñìpkä\±àZ´5Ô‡Ã:yY×59»&“Q:GÂZRÀí%`¼ Œ‚ã¾'Î¥€Î™E")˜aðÊÖØÁ¡F02ÈÅ笲õÏöÐ ð2IÖKMÃ5i×Z8kÊöÐüÍZð:—fúÞ*º†ÐÔ`vТ+Ï“­Ñð¾‰åùNÏnîéï"À æÀÐ!ëƒÏÚ–‰ äÜ•¢‰³Ï®¾oµ ¹ÙØ|í³Xq£]¶»Má~³YUqvžÅæŽêrŒ…ê9Yi¾» V”Ã%òÚÁë,½LPE¾ÚCë&ŠE|ïÝ’‚ýÝíCۮضBØ60°ÎÒЭÅ|æë®¡îU»uPª,Ž~Œøx°{è|´e÷8z<ºÁ«U6R½*˺}¸iê9EàˆŸµn|x•}fGl•17£€MŒú^[Çm[‹èi8P! Qpa? çÛ€R!Éðƒ¢ÌÂŽ§KtKƒ"ÖXœÞ]qÏÚ ÇŒŠÀ¹ŸNóÝ ÏãŸÍæP«³ â»ÐNôv…h‘êûãÜA-KsÀcQ ûÈ*‘îÏñ¨Ý¶û>.H0ÐÔ5.€Q³çJ—Ô}mMÊäd€’g×8ËçÁ¸Ýƒ\K4ge…_Ï‹b½8 ®_ó䊇6åt0V|AËXÚ¼­%_qdÅÛ$Ø{¯¬ÿ à°•7K›­ùF…t&ÿ作¯M&ív@&;?¼þ©ùär|x¾‹Ž ¬±h,—dVÇÀJñÒU7/”­ÿd˜ÃÙõ^æçJ:ÿØ©‘±°>ÈÈŒ&ç¨5eÔFc/7{ktfÓ:Iï#бU›Ý_£kÈF˜éS-Hµ ÒºñÚóZîûØ3‘¯U7ëp¼G­™£ÏÿºI›vÇ„NofÇh€;­úÀàÀ,fÜÆ¾Ü›­8mÌÕðk›¥t)H £Ò{·d6[›y!ƒ6šåkfRVjSŒþíÀLó†¾nà&¯Ú¢Îür<Óö4>GK’œ!PAšnòx.ñûú™Ë?·}hÛ*=àŠÀ|øèg×M_;±ú¾*ïCÀæî*D¸#?mOv®ÃŽj­5NôË{âC)ºÝl6có_Ó{CG6s÷¬"<ƒÁ¤Â2 ,]ÌÜÙv²Ž^3 Èð–ƒI3©ˆ‚'8ìÔ† -³JgùK1¼uÍö<ÈBˆiE`ýXÿl6›ñ@„ó8M“å…Ž4ͯ t 3e ¡<ÌÞ§ESÓy“3Yê˜E@È``%æÏpÊ»›Hؘ0,ލ}dR¸œ*È!B¹Ruãgh\ç[z°ÃF€–厑ܠY,—)$Ý“’ìÍ dÕEÜuàÏc’ )¹ëYç>éî=˜QsNgd1î$_h¢A~Ìrlgë!8”'k3 ØÀt­}=7“1»çAëé±d{žÞË "\iXú²ƒt÷ZÂuE)þNó«mZYÿÖ?7ùj"q¯%5“û Íç¤ v#ÐÒÇy✺`ÊÁ€³ù÷ä9"V:l¬)ÛÓç k'y\VÓ(-ƒ™B‚>â(OjO IDAT~t6DZHÞ ?ƒ„ faŠJɬãG¸+¾ Ï+¢´•K †>ÇrSËM°}hÛÝPnñ![ÃV†HCáhañ®$Dµž€xá¡w|Ïi{À­ 6A—îÊàèëNÛ“l·5˜&,x§¸9âçu{kŽ A ‚¯áÀÎ0Bk-2·¢_?+ËŽZw@KÏ3ÊöÐÐA_A(Þ jô\ë9Òs€jæú›(:ë)Às  vl4Ì€™+´—sªlšWð=Î׊»h€Å¥f 1SÁÿò}‹¬’ýZÆ ñ,N¦ãgCLΧ<|£}6d(3–¹½­…½2xe)_¦cÇßaµãú¯Éú‡÷dËwfóxV ¯ßÖÖV¸öåçXuË™¨–WáÖ³J¿’B‹ed4g¤Ð¨ÀY2—’&ÌÁl¡­–ìÚÉJ.‰$¼..Ï;í!h±u÷¹+Ò}Gí¤™aÊf².¼+Ì´ƒ¼©>O €îþóµ1d†p‡í‰%‚(¼‡°ŸZ`ŽŒ¡#¶Ñl¹§–º½9PG€ g¡x?a–¯=΂8ÐL¶Ûㇶ”î9ÖsÓÞלµv!‹•b™‹q7/FR¹5׳Ð@X‘¬ñžÎMÞA  ¬ÌP*‡ÿŒ®aÞòg€“?f뿬¬ÿený—‡±þ¡IœI [BÙ,¡9¦kþþ ˜Ä…šÚr-$™£×.þ{ašJÈ3CÀ"‹CpÑ»àX~Ú[<8‡ÓÔÜ,*¾&>·ìœ‰Ÿ·*0+uÔ[dc¡nV¸[›áÙù6ÜE×¢W¥GŽe©³‹„vþMž´¼^-5äèèÿ«?yvFEy Úvsà t= ðû"2è?`¯ëñºftx˜3–NÛ“ôÉçr¸lŒ)ïJá‰sJlÛhÎ;ØÑ©˜"»Úv3P¨?ÏŒSº¥ÌǤŒv£x~Dÿ¶Ù¢êÓ´qvˆÁ×|€±Øf°kÀ­x3¸ÁPîs¬gdz¬ðçÿlF'4%C6@ǵ5©•Y @°‡ÆhƱ&)[src–…s|8'ˆM0Ø=pÇõ¿Â ±ÉÏM!èÁ\ ,*ËÕz¤ºH°:/Üa–‘Cጚ„ab.$±8w¢Ù| É·,Ä’˜"voÃ ÉЄ«dÈglöCøŠDÙØ3ó ç1S…6Ü(H”à ’i]º?ÿÈ^4š_iÞ¦|ÍÀ& ¡M@~vNR‡+é.À–g^4?ÉIpUÞWQ®Ùœ“f)°)‰ø, –q®†ÈbvSñyJ¥ÌaâR%ªO)¼0@8Ë;Zôý”I†ûyæˆ Cþß#=1_H\åÒ¦¸òg `±ä‘ qN„Öƒõ_«Ûv ¯ÅºåkÁZ¿JÑel2²Ì™N6÷¶ÓúWG·ºbXÅ^¦Ú¡[²Yi¹l‰µ»cJáçãjrŸ æ–—Q<ˆŽXh wÊ[­ZgÑLj˜«DyâÑ>„ô)~0í¨ÛÀuó‘±É.¤Î‚8Š,¤­˜n$…#™ƒ~] ×`áz`(ÕbyŽ‚!e\p¨Vÿe 2AȱDÐæ†à½+Ø¢c²ú¯^”"ƒÃ‚(”ß­Ñ÷ܳƒ@Hçu”µrs6-æ d¹*Y~Éâ€)Àâ?'oü(™Â™Zeõœ1SÆAƇFdjøóp&  µË<ƒÂLAVägns™óËÞáùbKë°þ±.c`ø>S¦‰%&,ÅËL@2©!~爲n*»ÂeçO_sÖº™še§sœ¹Ó\×8›’`ž!žY°b¸øyî¨g®K,½sb¡C×1œ7)’Zjc‘æÀÚÉýáµ@Žå€̵X·ºûÜ%œÅA°†ò±"ÅfƒT³Nn¾¡û9ƒ5ÆmmÆÌ*âÝè2·9÷3d»½»çdƒÒ=h–1ßÅsx±³V.BÕ„¥Mœ¯„ìÎ$e€¿ž:‹›\áVŒp¶PváÛÉÕ•×Tfµ&c.rkë_$7ÏY[ÿ’Ï`! Æ~>_Aýs‹È,í¸þ‰\Ë& ³ä;Ìd†õO÷<ª=RªD¦ ¯Çxž—ðœÑíCAO·ë‚«‘q[sù3SÅý=îGxMø™Nó<™<²aÃ{yé,¾¶™;ˆwÆ{ át^H™¨£?,u…-` I쨻|'´ ȾIQñ¼ 2$è³ïf z“>yS-p§íQä)pps%­»¢¿Ô"}‚Í­zæ†}ì•q0©Þòÿ›­lOÛv\Ì$dlÎ:q'KƒUqIœ@¬âgó×PA¤ž6QàN¯=ôªï&·Ö¤ô†B³BÆX(˜ÑÑ÷ÔÏR7u6Қγ DÏa˜íÒÈfe ’¦õžÉ:{[Çm9×&ý™Q€@W_Òšc‚‘5i[r¯9þe¯Á`) ØÅ×a°æH·ö}vãáy0໪«öá«8_ÄSv½Ó¿õº ÈSÐÆ¦Y0?|³üfË5„sO2ÒÁÝúÇ Ùƒ ­Xí¾ðÄî<®]œWAY\&Á3tú³äw¡n8æÇp– »5µ©gÅŠ2ËÉÜà=¥À3¸qs2]Bp%2aö0'0?º¾qÍZ°)†~öbÌš^—P('Ř­Ýê™»ÀØ#ˆcªzÛð3$qž(³(wFÀ0¦ åÖkv$1’°Aïê œ™Elhï#7(³¹gvÇ>ã]l.ö3fÇÖ-*8@"‡a§øŠ,3‡Ÿ);®ÿ$Û(¬ \g+ó9«ÅxÏ­µÑHÃf§;zRC°[#ÿŒ5qד¸þk Íb ëßÅ…¬¯ÿ™ááçÿüõjï!`:oEJbq®?ç÷¨'`ægÞK»Ô5—˜ùœg¡€Á±ù£Œây¢î¿®ßäŒ+{ÆÓ~Ž*¥cB—IÞXZ”Íu°ä Uu“ÙI›™yá3 ÁY=>×af‹}–¼™~žì«ùý¹°6Ö«DËn7kºYpk7“sÐûNÍK·¦è cÌT"y³î8€*”¼áq¡ä ™«$oµ yZo‹#Öà R’ ²B–¼1ãhòK2ÍÀ™ =,yC·fL’Gó k’´Lò–ɶ²×Âÿfš™™–ÜéÃDCQ0dR‡`—“Ȳ”ŒçyØù,³{Í,SÐd2Äišl6‹ÁÏšä-mÙçÍÎÉcB(yÆ$¥âuÖAe™ Ûd¯IÞ‚E¢TÉI^€=eg¸TòVa6H†³”+¤ uлb<œï$oìr!§ÜñæÀLWðRîJo˜ ’–¾sVƒ77»jN/áqsA2l·íÿ;tù1Ü™n‰áÔ[(M!V¢–Uy#þŒ»¯Šøó^J¾þÕÁm‡ P–Ú¥Õ+ *¬ˆ±û~'É[ñfZ6o†…t¥5BR?w%’·µÆ}èÙú§óæ5:,˜AÇÏÊ€Ò<$ÈèôôଫÅöò» dHÞì´™ÙPua¨®¡Mïp˜­™ù{‚lSjØÔ=î˜0FÀî9ÎÎpw^ÿFç4¦Ã¤ö˜,®$¥›8 ÇãŠÚZG± …:ÊÉÐ1 c¤’v4‰@Sîóæ˜1l*{k­`C¹˾ô|:Ɔ¬¡m–FY‹Ã˜\† C "œT 7#³æÇòC6J@öɤ~T¼²¼YF“.÷‡‹µ„ë¬?¯¬ Îqv“T ¡D£ ýoùkr .”±ë•i”³`Xg1/²²1G0…³<™ãœþ ;1QÙÜ ´ÓL“þ­Ñr ™ƒ÷AI>è˜ÊX3fQ2ö %w8Ÿ„ÙDSµ“¡Ñ0FÐó¿¥sS TUˆa¾­… "µý¤WnÐÁQ™vNb:Ð32lxáî$ œê:òÄ6àë¹Ám´èN ,ö­0C#y~ån¨~p~d"vx^`h=ÎüˆxY"X„£õ,wõ×À”Z£×·î™@žàsI>°߉ÕpÎi}HÞX;Q¢Ï|e¬™»?õxª8U ‡/sÔ?çÜ5Y îYßvn„dá§Ù~ꚤ`¿ÌZlÈ¡Ô çoxÔ`5a€­ëÕõßz*½Ï¤´ÈBÛÏfë_A™U ›»´™'P T²%¶áÒ8º¢Öè°š‘$« ªR¤!Ùk¨¶‚vSsƒ¬˜K\A& >K%0Ujq  %Øo—² ðt?Æ‘©MÇ´ÿ¿9ovŠ€ÙÚ°ÒCÄ<œ,<ÃQiˆVõ.ï¾ßý{8—H½—– ¨ïŽ«9€l‘=½}{kÞ[ÝöÔM•nÅÒC£‰¯Ç÷ª~={?ý¬Œ—âkö¥3Áç2|½ŒcD7ž‹?}6<Ç›:ºйŠóá¬RMó:ŸK=Ó„…gÚ !‰l*’ÇyµãƒÍÝ]PÎç6˜?Ëî‘p.ñ÷»˜±½€«9[¤„ç  ödãœ;æ#;¦”Q€•þ¼\a|/è‘¡–kÈb-›öÐw÷¹øÖõ¿|Ÿ«aÇŠØ“x1ƒ Ý+¶’íô]BƒÔ>“¾w8®Rä±EüمϼW­°þUr‡ëÿ0A3+íȳ?H'ÝCïCrº§ ÓârÄ»ˆY!!1‡ÇšV8x2.®­ó.q>Á«ÀHö´"ÓCPvÇi¢|—Túç’Ï´I €Á€+öJ"¿°„¬Cz ¨ÈùuùIò3PE¢´J<(²B•é%öîZ·À’“YâûÔ^]3” l Îç®Is€1œ¯JŸ©Í¬I°u^3+€gg&‰ã.>Ç{¸ ¨C–h?œKH3{æÀ@Õu¦‘ó”{"cý—És-CÛëÔ ˆ"G”ía3šôÖ.‰Ì®†iLä¡d÷d¼£øÜ®ìžÍÀ0:Ä)S„& ´üG#‹¥–ì¢WKé-+Ö'ÔCãk„R£Ù‚Êôhù‡š½#”6zàžv9† 0:ê™ FŒˆ”Õ–“mɺµzæÁèøZ‚Ý tÖRƒ3êQÿcº¾àcsŠxã6‘Êhξ(Ý(_€3oT½3@c©½5ycƒñ6kvâ6ÛŸQu®ëéÁá#PËŸÃòšZA^÷pàÂyçOÍ#7cÔszX XìœhòòÔ옫ËDP§Ãå:÷3V£#ƒ2´®tƒºø,©Z‹ë„(ð±óS•âA34¥ØN:w”f$’tá%¸ŠñFeµÓ’Z]ˆ@ £ÇÒz²þmo>!…dbl´¸õo]«’êñ`‘òh¬“Œ‡­=.xHÌZOÖOÖ¿$ëÅèãÑ0ìA×?ß´b'Ž)çnV¦G­|èäChgVlÛLF(r03C`eØ|#ư”Èx„ÐÒ ;€YQ‹©èËgGFƒ‘±Æ8ÛÌŒËc9œ›Û`€‰s5"!c‰Ñ>ƒ€,Ê,M[ò&ÓdÚAÄcq1 pM]¡,=Í©Aù!š+ë"Ñm׳T®²A~=LEF¯¾”…/yþÏN’ælF3X¼g²èì^ê+®bÄzî䎙‚¼"!»+}ýBQ(ü,Ïf ‹7+Z- WÌsÜú¥{zÍÁ›ïŽQêâf¸8Ã='W¢¸FÓg¾Ö9öüo`ȦÚfr¨0Ô:¨'ìË(}Vj8xm®•KØÆó "m¾Žk$Ôª8¨¥:“ýZëm°ôž =Zÿ¨+–§î¬Àç±Öº  ³IØ£Pôteõ>÷t«ïRi÷¥Sf2X3”öà§ó­‚¹æŽ=ZR3pW­¿k€Zê ™¸Á¿tÿzcÓ:¶Ž`« ¾÷¡y`2£îÖ^Jé®tJw/ЮÁI6kGûR×Ù%=½wêvØGö´³bÚZÒÒû ØF¨ Å—,¬ÁíƒÕB`ê$† @˜ì$4˜-ÀM&MDpµÜ;dèá˜d/§½WËHøÿ e¸.Ö– oê$³k‹<,¬ÿirLT-‰ëOæŽ$Ñl¡ÐµV€Æ™I.,ïѬx}´ÅVÀ7†”ÛAÞÏŸë»À±Œ‹f/¬¨E)”Äœ5fF~n¸>Nl;íŽgÅ&Ùu¤°q 6Ø÷Š/ Ã9X~ÎæªJâ°†¯•¼I ³¢ %|‰™ƒc3„ €ÓŽ4‚+4¶äC΀@’9éÁM.°QhqŽ \áܽBÌ6Q0d×d‹(mïÑVŸƒrÝLSKä–=dÆ£–Þyç$¹×»gô‚ K(S¯ç®ªYQîÌVZáÙ¬æÈX—3µâ ‡2¿ H—àØz8IwîÕ’;Ðe iMZVÖ¿Ä cnŒ­]Ë0XËê±¹õßzf.Ø´_ L-ØŠ¬>þmþs„•ãÒš=c+ÔÌÚì7ï“Ö½ê²$f´¸fš‹QhTúß5u×ò¯9Þ«AòÓ4ù&‘ÖMrŒýAæe,î1‡2¤.‹¥³³Ò+‘­€×’Ài肼†Ç°ˆ.X_}óX„«Å323¾[# Y"p÷ïå?“ëÀöÐNêК~­Ö† ³Ö»Æ+Ͱ à0»™OÕv!vEc4@gu×]Ù–Ù&»‡ó’«­vb`®áFÚë Œ wmóapZ!Ôl€UÿsÊ*ÙýçQvص7¦ ð¬XJ1;-çpPéu€}vâkT˜Oj 5ÓnЄó-ô¼°‚ÞòÑEI]ÊÖ$YÈL„õ`Dåc›Jë¬1Ýúwò/q Á­ÿB럸pãÐ(ϵ Õyá?2gÏ}DöÓæ®‹Ïy‘ wDC™µ,eBôg²K´œFp‚‹NgU]$,;Í®øÁYøtXˆwj‘f`Äu 3yÉÚœ¹¹Å± ‡ ç HÊHÂPW稺=.ϨÒÏâ¹Фv´nÆf_³Ä´zY!ÉS˜prÀÖÓ{+Ücâg´LöÏ?uLÅãËœýœ)CñsNÎ$­Ì™ì¹[.» GÅñ$,YþO°'VO_3Üœ¡KÀçUL–Wëaç²8 šAÒZÈ*ƒ$g|À{Lëaž{ \ñüTÎMh'ëQ˜9 å\±'g+o¬=´þôõ ˜!8æŽ:Ü£~i 6R5Eq¯”2Õ„ª+\þÔ ¬±JÍØÞòõß[s™F޽J”A®yÑÇþÆ=s}»9@ÿ `Ìm&CÛìÝ‚ið°ˆßu3KX¬0… +nHm,„Öz˵©¬É3 X軇8þ‹Ô )LäÝX¶8„‹CA²?…4“cÀ®x ,Û¬†Û™«í8†;Ú`? d¶â:, D ›™j‰1€ôw/’JØ$”Y@ƒ1n =… Ø· ÖÌÝÓÛÜxô8Ù"H¾ÄX²1×Ö[wݧ}%m0RâÅÉ:º»VºÁé1*èqC7“ÔÓŽSø€„:£!rò;1õΣ.>§ ­dê¸õ_JÚuêÚ%Yÿ‹„­[eÒé!HiVÄyÓAv¬Çð:—óq¸õ/°þœ˜{Ìr¸DÓ‰ás{²¡ e©€#2)ÖÅ_à8¦`EbÄáŽÎ ®ø‚MßÍ|×ÑK‹PNe…8½./ü5-2à³ÇâÙ1WÈ÷(Ír®’wü9SDZ!̓"”ë¹aòŒå‘hÁ`ÆÍ1 …³•9˜ºœ(’1Úq Iûzœ»â{ sö[³ç÷Aé) @pÜz³"Ì¿²À%YçÉÚßég|ñ×V ÛwŠ—1è `"Óè€l ×ÄãEC'l^ e·[ÿ=ÊçBNPY™9KŒAÖâxÞA‘>óÚ «fÅDÒãæ×åó¼ÑlÁ¹G’úÃIÒVŸÿKý uGcPTШÈ–g9a i~éi A¢9hkôÒ ½m¿uHöÕ#zo™y íÕ–ûËQoŒP¡p+0gƒÅš“ñÔœdãŸ^>ÀŠi{Œ´@u6ÆmHlÜZ|Ñé’â£qœ Ò¬Þ£q¤¶3q8ÀDõ›ãòÿÞ” —fá‰,•c¼„%=‹“IÙ†ì,2UVßµˆ¥L„û¹ÊŸbéü}e5f€¬šWNçÍ®8É‚#ën€qBÔ(´ÅÉ€šk]“Uã4{ñ& ŽS@нq1Ð~s,%¬œ‹BÇ¿65)nn¬xИ¸ú¡†Rá%éèÙç ¦GPV†,C™ó :àÕÜùÞd`f¤®¨NØ8 ß›ÈR;¬7cr¾Bë?1N0y‚u{4NP{o;GºÞÈK ²e fÙ*yMÞƒ™+ŽÁ´™^•©¨E/fÇu½c%4FbÎ S€ly‹óF«Éõ4×”ežÈ@ã.ໄNy`ÆJ’‹ÔýkÆeE"ˆÅ`p'“Ää"‘¡ÑÚVé#}6 Û´zÝœ“™—xlfrA :eâJ‘>ÿ›å¢ .·°žF@ä®;Ù‚ÏõéB¡¼]¤I÷J‡ÇÞ¶ÿØ,Òà­­-ûÚ¡‡Éý÷ï—ºÙÈN>A6›-yèÐ!™¶›œpÂq6çqõ'ÿX>ôÿ~.™«qf)ÛÖæ}ÒæghÉ3à‚g‘Uãd7ÝúYÝÀœ'¾50€–ï%–úqcÇ!°²jœ@Ê™5ôùóö m ÇÀà¨Gg7}®»ñPôykÏ!Ûj)%^~»hnÞ€.',Fjq£)Z/Ì{¥Ë|©~Vý9AÀÂŒî¹~y–wm6þ“›‹T¡½7]™NU7›9s‘Cy÷¯QȼYÀþO™„¡ËR­Pµ@-{w5¼Y™‰p®qˆ¬›Ï"R ‚î³›D ÝI«ºëˆ‰ŒTq; W@žâìáwÍ Ý$ƒ3 ”xã£ÉAø½Zd³UA÷¬nUóƒÖݬ%솜v?°¼ [¡N-ÅÀ›~Þ Nv›…u.kqÌ¡ẖyç”È1ða€ŽçV$²Öº®°y±P@i3³ÿî<èØAš”¤jÀ¼0d›/Èï6ÌE ×?ý¿~ʺÙÈöö¶Í =˜-q[üØ*Í=²4xbùD‚ó—–Ǽ6ñ† Î5îHUY$ d9^¯| 3aB8(þ<Ø%£TŽ]ЂtŒƒO%ÊçT¢åœÕ Ãjë´Èªd -Vuͱ嶛·IŒvø)@p€¬Á\ɯ\Ž ,æ:2ê'à㬵Kâ6G 3xØ0ÍCéôºe8¦YP˜T¼5w`ÛØ|‚‚T{‰Î n}”(éúÚsΑÿñ{ÿ»1[° žÉ½6Ïâõ.½‰Í÷è,Þ´(Zï0ç ϶ùÿ…Q{n9,N>«f¾a´©UŽ?þ8)ÒeÿÊö¡CrêéO”ûöÝ/25ÙlmI/"§ŸvªÜ³÷^¹ïÞ}ræSŸ"¥V9ñÄãeÿûä·?ú»R ƒî×zd\"“DQqî6«ô9¾bØ0D!ø4‘ÏY$ïAÀ°#ž”( Nåx ûck×óó|Õ$'KøÏ@fv~¹¬5ÿÏ$ƒ+iër¸ kã~— kȚɗ2ÍÅÝ×ÞßÕ%wŽÃ:­8ÛíT9SSŸÿf/Ç‚;š =¶©–i² ζ„a¢ìgøÃWW0ö>»um–{5` KYæR¬ðo"0¤v+s ÐíÆ¢´gR9ŒÇ·¾ýèB‡r¾ùüùÙÌØ3)#ˆ;éÂ2i †ÄÛqµåhd.Üçì²cW å|zNê¦:Ër݇©-˜y*.‡@‚Ü1[¨#Ã'2˜ìm0;Åä’ZXйÉ!•óHú…¤UÓÔè~Š×Mß{ZæªÜÏ×BŸcdJÙæ«…e-#¯&“i@ö ÊÈ*怭ÐýáLÀ­LÝåT÷«r¹Ö»loo˜Av]Ûô9=M“uXuÙðºÈB \5±‡g¬} ×A¹B"•‰š žo2q¤ÂRÑb£³ITdÊ ;é±hvR–¥KA'=•È­)Ä ÎRcÐb÷ÅvoÝÉ)ÜüK-8yžDùfßÈ k¦Ò.s+³¹²*ÙšÏ,¯öFœ}„²:±g?×”Hu˜x¸Ÿg¬È˜oOî%ëy1†L_¬§ $qu³ŠT).ߦƒ ÊþX ÖzƒY‹QS´Þ`ÖXœdnH…º±E|°÷Ñjñ‹ûs_äsø;(ÍìÒ¥uÒ.´CÛÛRÊ †N8éD™¦.'x¢œxòIò'|…œt òÐC‡äø¯8^žôäÓç9‰RdS«7Ä©qÆFlö³†ßµ}†¯c$J~?„õO×v'©ÙêÞÅl}ó¹@lÓnd„h–(cBÖ ²<ÆÌðÀ±SN†-ù¾²4ù°žBs„ #80MÍÕ®Ròõ_aý‹{ÖÑì’0 4: IDAT¥ ªÉäñ’‚áyåÙ80-ÂB¸§5&ÔMe®›,x]F£x{{Ûâc‚ r®C=Ú5éa@‚vª.«a1íºáÀ`tèLK詾/³Xh6FG‹éÞY[Jã($‹ëÝ«?bòÃÙmHõpS6fÆÑŒÃ|íŸ jBGlJ½ŠŸ‘b§9ë ¶QhÍçr|Þæ\lJDf­R¹©À×67ì Õö¼X›ë2ðùÇ. V© (žý‰™Bâ3¥J/>cªÍ GÏÞ‡z~7êü9/hÞ1ü·Øü{ù×Í„úÍ €z ûu øÇÌǘ)0¦->ã"³ƒ&ËAªƒþ\©îÓdýëᯬø;„š2;_Ã9—Xë =Ñê¨á¼¢îœ¥w!ÿ’dq|¬GJsƒR·i™;*+3B;åÊ  tHHïqOw­M ™-œVÏCìVÐ”Ñ Úhà+7±Ä#dl*<”%‚¡íÀZÑ`3˺3$qÔ=ûan›ÿŠr›™’Òâ2ú9x¶ÒGFâ–HïiÇÚšüKF™)ÐÏé´FAÉ#¬ BuÑpþ[ ñÑ/‡ƒY†R«TiŽn‰ Lq²¶ZsÆÞš“iÑ\ëh4àì–ß¡3›Þ˜ÎÁfwðPPråÝÁF1­7“Ô-ûnÝ[:Y;“ÌÀNuUJoÌYQÈ3AÝIüì¹Ô_ rÞŽzùÒ=ˆRY©Õ˜:”Bª ÁÆÎ‹_xâætj6‹ÔsÇ<‰Á°dL=¼G¡\§Á °ÚZâ,¬ ÎY9)Ü<Ê2!ã$˜h™º)îþp¾î 5ëD7?ÇbÔeÇ)Wƒ˜0uÖàgf‡*µÑúgÌ T‚¶ºþA^‡yDÓ4 é’,Ò–Á›U81_šÀ†IeØ£dÀ­ÐÍ›¤ÜáŽÄŸ 0\ìij>'ê%¸F9]?ŽJìú"‹â ëE椅°ÎC²ÄÄ™åuPÅf6#Ãh¡xG9‹ÐüÙBgòç,Wb¨­âÂìŒt/;ßyçâ+3„p¶»=†AºâeCŠ9œ‘­ÕŠrž?.Q¦f Ù‚c>LÉžãe‡0Yt5sõA—›oú¬Ütߤ ?s6[ûþ_Ç¿™Tëp† øï‰'=Q^ýªKìä0Àpà¥ôt_  ×2“æz2àØÅ[‹SnºÙy3§8£¯“í‹Ù>Æs:©ì·û@ÖL‚—}žž’&AÆè@éj£dvŽ7i½µÆ¹–„Š—Üi1Õ `v“´H«˜ ŒÝüVÒ‡`} žøèˆø~*ûsúY ÁåìÎ0ÚlmV±»,&¼5è­ÖbÙ\¸±hß°ú fhx€³7N!ifN ëÊu£VÚ¾$ë;ªðó(¨ïSy(JÜí|fÎq,sk­9y^ÆÄ ¼nƒ9C“†B’Ç*‡Cf ¿ŽRDÎÉìwÙñkU^Ì vrÙLÁØ|é)a‹f6@ëæÔÎW¼C†›f@å[‘â&¼¯cÂP²¦ŽbÀ¸.°ÄðDçbVK÷DyÎÓØ  £„— OÂ,&'„Æ îkxî"«o'§+y¨-›V ‡†êyB†g‡°DæeYWyò“Ÿ<ŒDä OxB^ø? ÅÊÃù½‡ûºGªÉˆÞÏWC›f~1˜2kYtý‹wgË‚ªÙŽ;¸®µ¾šM´ÆLy™–¬š¬®^[ÿh†lúnomÝ­µ5÷SœÙF…'|´ŠÕ" Ógû˜MöÍ_Ì`@k '_öÿ Õ5E$ !«o”ÓÙŒ(DÙ‹†I†fh–„öý¸' ¨a€—s‚Èô©c@µ’8Ÿ :b­0yu#Û ÿ¢û–k¨‚°¥4#LÉF]$9´1{¤óPúo°{6µ:ŽÂ0™]ËÞ«€³1u }'EÀ‘®†N‡19Ê€ì Z4#ƒÒȤ °ƒK—ûbë¸-w|NSK-««eé;ÿj˜Þx/˜ïqLcÏ¥…ÈŽêï°ê=Ü»›­MØ”4g :e\캂š3‹È¤ú°Ÿ;ó0iæÝ15 ¤8(ì—óXÔwÏF!Ó2,tõ$£ 1rA8’¡¦ RT®µ°¤.ÉúÇkåŒF<u¤@®•FyúßÊÈØìÎÊP.ÖaÞƒ²Ô\ødë.eW(ïbÛä ì„äwñvÒhðØð:…9í.&–ÕV ÁZ;!=¾7:^ucÀù$8+ãæzž±£çmŽ€ Û[œëª±Ðs ‰ò3¾©_w Íçº@ÕâƒK\"Os…u§¼$`îtoxÖ³ž%/zÑ‹äIOz’<÷¹Ï•§=íirþùçËi§&ÇÒ;4 Ï2/^;!×îo¬QÖ2xNhÕm±F69Hìz’…C3È= Gv  YsÆÖs,«ë_]‹Ÿ÷‰kŠÙ'[ÇÍ×Fü§RMèê ‰y†&X½4±~æ¦+ì”'PO²ƒzݨà %Ýh €,s3†YZw~‹·‚w8aÙËZo!¬ø¨A•Ðe…"Ò$`Ð#Þls€†”•ø>ÂvÑâó]DxЋ4Ïeˆc¥ ùxõæ¬&¦/>‘< ‡ÞTWE{ctR+a@m7:Çy ãJ]l‹u»2XøY¤ø0SO¯Ç™c›žåIÒPÆFª.lœc†á™BúJR(’èÂÆa® :2c–Ñõ¢ÀU{oØØ‹·âž¶'7|Ès@î¡@€ÊÐ6†çë\Ý=ÐZ¹A˜R]«ÚŒfBu6—Å™N98Beeè¨TñÁL ™!*ä‘Q©Àäô`4t|ÃõO GßOM"•BËíB×ÅähIî•Ȱÿ¶õÁrG`&H%€ þc£ùO˜×%% ˆD‰ ê %h!$€‚Lî¬à1¸`Sþ}d(¸XnÑ^zÉ-v‹;YYcLJܡ8Ó³@9³….79b1c„R Ì¥aKíÐéO3sÊR1ëå\ÎðØ‹/,ƒ±HE(ƒWvÔBÉ s·#ƒ'ÿëÀ‹¤ÖëøÃÇwœLÓ$>ø ìÞ½[þê¯þJn½õV9çœsŽA´œƒŠ,˜›$½õD®¬ 0F5@Z‡$9«ëŸÂf³¹¤53Znÿ‰òCz–Šãï;0©ë¿5÷N2œœçl~ŠY2¬‹Ût߃kqœcÐPëh6b³ Hűѧχ,ÛjÏò›[&@é   –õdšÐ¡ISKõ.©`l{N?6f‚¼ëX³‹é(¹+nù1Z<÷È`p¾O˜ya Pü€ü†²„²"³nœ¤m3Ï=éqT©ÎUÌåï4q™3a@è“Át5gWͶß;1m™‹G›f``r½g_´¸Vf„Y£váýÑÔ¡8àÙÝÜ—Í õîì»ñ|*}À[ÎÏÞQk<›ë(‹]º¾w©$/NbÛ‡¶ÄMÄÏôÌ]՘ƙÍl8[RÛu¿Jš/ÅkÂÉ(¥;‹Ëº •ª»–O}ДîÊì h¬¥»cö,]ì IÍ\§ 6¿39rlVgåõQrëL jIÖ¿Ä|ŸJ™A~?g‡­k˜%TÙÀ™³&®.ìPCË *þáu$˜ â †Žu}òNLn0çX0Ì´'Ú0ÿn`ò>´x¶™!Œ—BÝÈÄv-ª{dšpÉŠP´)ä¤g]M&&×=癞9ÿ+:¸õyêß…«²pµë.”oR$27ú~ÝÖ* °í% ²8Î=¯uZ@‰½v[É]ÊdSà,†,3~a c ñVëó~wÜqÇ‹ˆÈ 7Ü wÝu—<øàƒrâ‰'ŠˆÈ—¾ô%i­ÉóŸÿ|é½ËÕW_}ôPMÒü»ÌgX2ùX¶°ôËóÐDÀ™ZwÝ8“Ž€¾“ÚÑ=™Y¤ãñd9=L£¦©wçÚÆNoRÄɱ2`3†—ü\H4ŸaöÕä¦%˜áù/Ñð¡ £Ì"ë76Ëz©¶673$˜ ¸Ìmw2CÖ‡­eq I}©ÇMß„åë>‰íãù¬c"'È4ÂÅî<¤VäÎ:SN=ù­fÜ€u_†¸ÐéÂJѲ<Ðys† *Y ëZ«zèÐzfG2©"×}þv¹ïÀC)‹ååsÝÜÑÐM '»aÕõœè T4ïÚVÁh0QÕ9}1`CZuCH°Wî=—¶%£ ‡ô6[™¶'?#"t:{ådãâ÷É6[»Òíý:–Å#že1g¹ j, ¬ÃRêp¶6Ã]ejŽY›Ý¨†ìQÁ :Ï.…ÔEÀ„’H3€«xM§í î  ƒ*÷šîú¹,›ÙaÏ~¾wÿî²Ø9gd1ºVŨuYŒJ終´‚c]–Ï^ù!ŠH^z3fí4’Ù5Çh°nè ;„†‚] Iæz&ŸcÙV¡õ:i”g8Ä©œ =7m1|(ìúFÀ®`WÍ>Ìñ˜ ͈`Nf)Ë…çÄp±«…¼…¥ØÓB. SCQŒò6ÎG©¥Jß }“ÁõæÁä}téyH\#9¡aÁ†€gŠ,`¶5Ç^±AÎÓpPl–w¤{EÊlt M¢ V=jˆ€F!l`õÁ¦öâ í–‰˜/Å:Êeâ NLãë”/~ñ‹Ò{—|ÐŽÿÖ[o•[o½õˆgq=~5”ÏË\Þ88tuý¯Ìì /ùŒ³4«ë¿GS nš {¢kºôžš) ³ÜZ8¬¾^ƒg8J„l ¤?Í9B¦¼F³nÐ`ÎP6e³“Ÿ'dƒ–Êùšd†ºeª||©‘UÁT‹8×77>¬†š+TüÌ Îý̵Àü ãЬηN,´²sh—Ô4ÏÝQ‚ìÄZé­”QÞÓ¦&oúþ$ßú_7g†L]¦6Éd7ÙžšLÓüßS›%K„yË[’™RdkS¥v‘ííI¦6Éf³‘­­-9øàƒrêiO”þ©_‘OÝr‡pZ÷N¢‚õüK¿ëEòŽW¿Ü}þ·^ùAùÕ|,°㦃!ö:ŠÞÖº¹½uÒ‘£ÉÙÎiÍȉ¯“ó˜)tŸksÔb0*.ÚþÌëEDä‚7þ Ø6ù—¯“óÏ=Ë›÷ôãò¦_ú¿œõ³ZQKú*°Ù øC†3°Í°õÀhØ¢ÞøpVLÈX!¸ì.sÈo†l•Bkš_sXi©ƒÜÈ£òm.½¾úùÀ*èý(& to3Ç<Tì:ð)bNqEß3sjË:rÀÜØú'@Ôœ< :ÎórR/iTàU ¤!y*ŧ7j°»Ú,¬øÇ‚›]6÷à °i8l)G¤HCæªÑ½¥³UèRÇò-ç~V°@¡–ìâÅŽ^̰ ŒJ 22HàaÇæƒ )Âù/œÄ^¼­©€q8kÙxg8¶j ¿x‘]×—Bb˜dÌX6݇ uq $H ÊtCâìl—8æáÏ!Øuŧxcpr"ù$3fÊÐ!ˆr€ˆÌy´CÌöͼ–º#àæ(iDr0Ã6Ò¡ ÌVØ$ß2æÈ-¥’ƒR'Õ.IGŸ@.ºÀññqÐãèÖ¿xÙÏ ±,Ç/ÄùX!G´8ñá¾™ÓÊ} m¼n*ÃF|VwPê~fGDFzëÑ´€òóÄr’âúwÁïÅ×.´(yù›…¶Öãê¸0ÏÌcÓ?G?B;æÍè®sg]‹ïk®ýsùWïüI†`ÑìÿMõüwƒôç%Z¿agÍMš}í¥]$½äm2Ià| =þOüoo’gì:Cžzáeöµ¿{îYòöK^&ïûû"³D¹†¦6`>è>s…=ÊÛª›m™L]|à]BY:­yËlüºË[’!ùsŒµ èrퟗ ÞøsÒ{—W]øíòŽW],""o¹òßZU¥“N&Iªº‰Lš¹¶¡¬Îdblƒ-NføÊ ¾MÞñê—ËS/¼Ì½ÊÊt§ÖD?½K£ 2ìêSãŸ$S 6F“YVñ6ÙæuÚ òýe,b Ï­“{Š¸ÎšÒdr( ³&j&-ìYs ¯åÂ]¯Ãìݧ¸C5¡ ÊÜÚ€MaYÚiûÒžg‰—h+v2ðÿTXª±V¤ý®ð>\ # á²98[¦‹R“\:é°%.œMqÒ»e;.(˜(7c"Ã|XaB²šÞ»ôÚ=XÀ÷@ RVôÿRÜ9÷åXðe–Þ Ý±Š·ÓfëigУ¼,ƒ‡ ”ãHœµlÉ÷¸Àc›sÇT(ãCÌš9l Í ±”RÊÉ×:šþTroËIîárø|0f“œ•z23È‚%LYÖ—s‘ì~Æ$“íá1!8b*Ì& ûAXÿp,œµÄë ¬Ü›0ÀvmýâùÚfê—3AQúÈ×ÏLÀàyS¨ÎèRR Â@sl2MrlKòónþVIdg 5³ÃV‚aãÏåQ‚t6g$Ù<ŒÎë”"<(Ü'gžy¦ÜsÏ=RJ‘Ûn»ÝÇò/‚'üÚwÞ!åħû Q‘ÔlÁÉߺÈÞr©œvÊɲëe—9¶äSŸÙ-/ýñ÷„¶Î˜ ðôçqõ\:çüˆ9 K^IÙ©"~Þ(P¸=JÎÐh¡W¿Zúîl veúȨA¤³PïýðÉ‹¿ñ™ò÷¾é\Dzu—k!iŽ‘cuˆ•ò¢8ÖDºguÐ;;l0aŸ½u2vð3:iÞlÅ·¸ÒHüìèä6f“ÄÍÊŽ‹yU8ƒÆ]Æ1›ÖlöËQùmÌÕ4®ÃìC–Œƒ0qhÒdl<«N„8“„r2Ìã èŽÃIá^@ àr†ôØÈò“¥w®ÑÉsFËqmÀR4]ÿG¨ÉÄu6³P©œ9#%3.NrúZj˜õ1ÉU÷V¦8KÂólœ€™4è>çÌ d%Ljދ‹n "õ º]aÖ¨û¹%”»¹2@°Ég\l6¨Ó|:ð÷`"#– b»sÞ=£å$nN!ù2W’³ ÜÍ–pá^R¹#ÎH!Ò÷äì(wo@¯Ôý·Õ*¥T?8_"óLF$ÎׄõOñ n¶FÀ×?ÈŠyÖÈÙ¡w/Ád@”™3¸9´¾’c”ÈêÂúïÉú—•õOlZpIlž-Ç<%ÇÊ%,¾lïîél{'à¹"™ ª+_ ¸ø‹F9@ú<ßÖLj‚7ú‚ÌDÌöƒüÇ# ²EÆßÈ2*¯†5&îÞØâ¨AfÜ%C€¹¨žÓÜUGÊ)§ˆÈ<ðøee¬D¤÷6½ ÊÊàžCÄ]¾ãyçÉÛ®ü`µäbö•¼ØÉåvï¹S^øšË­ ûŸ¬ƒJÇÞzååºÏï‘«.­}ïÌ—¾ÞÝ'~éÍróï¯yúSäì]gˆˆÈÅoþy9ï¬]òÎå½öî{@žýo)óÍúÊ—~›üä+/¶×û½O~Z~ðò_•Öº¼ê¥3ò¶+?èŽu×EopR«ÿÔØ1Þ²çN¹gß~£2õœy/üùîG‡6tèz×}\ümÏ•k®¿E¾ó[žm¬Qk]nø?ß%§Ÿr²ËÅoúyùäõŸ“ÞEÞõê—ËÅß~¾\õ‡×Ê%¼xþ<×\'¯xç{åú÷.9ý”“DDämW~PÞû‘I‘.¿öÖWÊ3Ÿþùz£\òÝ/²×Uï#ÿò29ÿܯ‘=¿}…ìÝ÷€œ÷ýo•ÞÅîNÙÀ¢{ùšÛ(TWÄÉ]]F¶V&g,ð~λ–]„ìÕñnÆïu¿µ)›*Eç« ¸ƒäO¦YD¬º²5ëlZ ØÿC I2/c=Ô;Zï]*IÜ X~WÚÌ•5™¹Z',‚TtüZ—Œ0Øé`Cx$ÌúÂ.i3GuöSkœ±ÃÏ™|Ê:¢Í0˜õà ’Ã1]³7\pdØvÇÞ%€FµNëÒ°±ãè%ê8±è¢9&d7ô¦ N²ç qò5ÞªÝp¢e®mC’%‰Îh¡H`ÉBvI³PÚ´3OFÜ%çÛ9ÊÉ`º8»û-K/.¼ÕÎ{ó¢¿]<м¾G?…ÖLÂÀ¤LïQ5i’©²ný—ì ®áØ{j"ÀCñîÖ¤ ž5“lV ™–þ0öÜlŽ Y"œ5âçAö˜±Æ3Z!·(¹V^¾N $á¨Ê¸`]Z¨Yì ¨‘§tËÝñõ ¹½eÑ4…^jF%½öMlT†ížõÇJNP`ÀÉöѵ¹ÿþû妛n’»îºKn¾ùf¹í¶Û¾üÇ©RZ²ÙÚ˜VÖæ˜`èõU~»ˆÈ\`oÝ­sJ­uyç«¿GÞñê—ËEÿüßÈ®‹Þ g¾ôõÒEä¿øf7¸þ¹gÉŸ}ö ræK_/¿wÍuòÎW¿\Þ÷¦Kä+/xìzÙe²{Ïò;?s™c$¾ãyçÉû?ò1ÙuÑäÚ?/W]þZ¹ô‚Ë®‹Þ°€—"ïÓ%v¾ð»^$O½ð2ùÊ ^'¿éçå;¿åÙréßæl¾/¹àŲëe—É®‹Þ ·ì¹S>üS¯³"÷#?ýz9{×Säiÿ¨|寓ÿô'Ÿ‘çžûÕqàXs—àÁºÙ ëæ'Ÿz²Ü³o¿ÓO9IÎ8íyê…—Éoü9¹ýÃï‘k®ß-»^v™œùÒ×Ëû>úq¹êݯ•ç=ël[h§Ÿr’|ã×þ·²ëe—É[¯ü |çóΓÛ>t…üà»~Uv½ì2ùýk®“7|ßwšY@‘gì:Cžó5_%O½p~Ýkoü¼\ÿïÞ%""ßýcWÈÛ~å*yÚÅ?j÷º`xê,Ï«ÃbÜr0Ì™½ú_-Þª ŽÖî• 2Ü9H•Ce¨ôGæKd¶‹·áòl ¿{Š\-E h©Þ"ÛžsÙúWkS|Ê„h$uÃÜ"üÌD(ø\…Ù!Ís‚°YºÙl`˜}yx˜×å~bfAß3 €¦v„äp®XÚl†lPíÄÁR‹}-èYR¦aul›¬'È:öÉP3»K©ì¥4 Ìâ{¥wÙ:‰¬/8ªuŸ[ƒïÇŸË…q&iÍ l¢óÕü¬Ê –`'Ža¦Ž)Ââ QŠ—Ÿ­ÉD¹,#b~˜¡Òk€Ò7k€T/Ïë>ÄÈ7š|òE’ô±>Ǭ‘Ñ*¾H.Råˆæ LЦZqjs³4d ¬îï?#¼þ!`‹S½ð}ÒûM#N$ί…LJ륵›êÁNë? eÍØ\üº»·W2†œ!€x3w|]üL¶8ã–ðf¬Ñ8‡ç?613– ŸÿŠªñ3µ„ Õ¬(ºþ«õÀzç|DØü ö ‹´ê-²‡z£Æõ?5Žå˜AÅrw<ºlcVæ0ÔÁíq9ÒÅ Ëf˜P“ÊTí²8ô³íÝ·µC¯ àâo{®¼ÿ£—k>sËsQ{Ë•”[öÜ ›†—rI‰DïzõËåüsÏ’w|àÃ#¹KD.øñ÷˜»ÙÞúJÙ½çN¹ä_¼ßŽå­Wþ{Ù½çNùá—ÿÇÆ\ðÆYvø«ù˜ìÝ÷€\{ã­òÉëo‘ÞE>ög7“4;³ÌìØwýض)}÷½GN?å$¹ô‚;¡YAº±¨KžÙˆC¶“U ÄÐ\éY”f0up¬“åµ*¹ãáµa3äu”qVÈÀÓ[s¯‹†œê¸X‰Exuà¥wY_ÿX€“ev°¶†b»&ó7“Ê\—ï+˜â Tüf— ø~Ì0%]»NFš)„{y!GK!²?=þ©›Ö IDATÍfsDäpJdÇó†ùlal³‰m¬Óï˰{ÆPÏ Wƒ…¶³vF @öÛÈòX–ÌÒév`‹Ø’Y&YƒŒÎIÄ€ùÀP>Ë/A§5"¥ŸwÅJE Hçyþ ÌSp)~8÷Ô¦å9\ýžKÒÉcB‡®[®² ÔAÊæñióÔZ¥Ê˜IàNAM‚°t[A‰ÎÈ8éåó~ÊIò¡Oü°¹nòÉë?7KÕž±KþøºÏ‰ˆÈ_íÛo7á'¯Ÿ¿¶ûKwØPþž;÷ÊóžuÖ˜ù‘»ï½ßɧo™eƒŸ¾eç]÷Ü/Ïüª¯tó"ÿÅ7ðùìïps4×Üp‹Ý˜»¿t‡œ~ÊÉRk‘gŸý4Ù»ï¹æ†ÝsðëRôìÝ÷@,ôe yî¹gÉmº"ÈÏô½Æò{O:õ óq5?ôÙ/Þ!O>õ žöî{`8ÏL“ܳo¿Üuï>»¯ôœ<ïYgÏŸKDîÙ·Ì- ~ï¾äË91·5àÙ#°è6âCÛ÷åï1¬9ÏfÀQ–"oKê‚ÅÀE‘Okk^›<~_ÌeÐÏ!µæL0†3 Ç¬Þ‘9ü÷°ÛƒßÓù¢tý[wͳ$h„P˜Òn“ÚA{ƒŽs˜–ø Ïnë×Éä³õOÆ BR<ûܺì{`^bîíÈ 6L0+Õ€jήšÜƒúŠ53Ë7 Ø.£LlQš¥ã´ñ’ò* ô ßñSéÔš´0P÷Ê šrž[q€A$µív¡ªÈ%.rþ>*Þ  {°ÃùI:—×K_0=Cб$6“ÖRǹ#‡:žCRi#Îá8ç?éñu$ÎŽá÷ÜœQñÝòº©iáΟ6ßEÎu}Êl×ÿšÿLÓ$‡’‡zH:dÀÙ®´eqÇO“å®d¶Žyj6'wXéÏÇH÷‘ä§Y:’¸œ±fŽóÂÎ\‰„=þKâ:«È…ƒ÷øY – kõ£?,UÄåÐØ9^ÆÖq[£Ðœ)”‡ýº­5¹ÿþûåÀràÀëÌ<š?mв`kK1õÇ×}Nvï¹SþÕücyÁ½+ø¸w`.|Ñsä“×ßb¯ÿügŸ#§Ÿr’|ú–/­8ÏÁÍÈî(–£ù#9M®çZóÂ>Gvï¹S^±!Ô:ÏÔðÜÆfSSöãÓ·ì‘ÓO9Yž÷¬³ ôõ.rö®3d÷"‰°Õu °ª Uo÷[Þóî{ï—g>ý)À¥ákŸþùìïpÏ·O‘W€uÚ)'IëMê¢8}þyçÈé§œ,~ó–Í £óç×ÝÔjÀn³ÙŒl",K®M©>:MÍØÎÌ !£~0v³µ‘¦³:̓;+Dkcn’r-¢Vs˜¥ƒ`)”T½“ñð€*!õ¿å׿¹®tO `è`À1Ö…²—a-âÜßzk«LZi»®`Â"ỡù4!2ùÇcÚ[MÂ&î¬`–ÐI6çØâ»í˜l‡kToiòfÀÕ âÅL€å$Ì$±I2,k3+}k S_sŸr9˜AçË‚£`ñÆl 3f஺äfÌ&Ù÷:XÄ'Vƾ§VÀS¼m¶Ðüš;PæäEeåæë9;•šNàçîâ\#¹@Z“¼•ŠãÇ |<(÷Þ{¯8p@¶··e{{ÛI^×f2ÕÌv ë*³¤fyœaýgìG‰†éœ ½gÂúgp°Ö«ó±:Kvh>»5CÒB׬ÙAÎÖ?0Yf‚?c­¥<¢gÖ”Ê^kÞ˜ÔføÔÀàËËâž>n¤x{qVhÈŽÿ¨jRÛ$̾š"lmm¹ k¼OôëG½1‚,¤’KÈœN< ­;ïºgG&h{{[îºë.Ù½{·|æ3Ÿ‘/|á r×]wÉ<ð¨ˆÈ=ûȹ®i{r6ҘɃ¥/üáwËٻΫù-°è‹<ïëζÑ«þðOä’ ^,Ï?ï»Q~öµÿÃ<»rÃ-ᜡ+H¤%G[Œu"iënàŸ­ÏØu†}–÷¿å•rú)''Ia.b ïŠùÇ ½:»­Í@jtê²à[SM:² Ë÷¡€Örr‚.Û+Þõ^yÆ®3äýo¾Ô¾öÎW¿\ÎÞu†¼âï…âÜkfñ±¦_=ý”“åÛ«ÍDàgÿçùZ\sÃné2d…ßrÞ9Æ8u²M¯k6×n¤º R7Rëuv‰£[;Ô…d¶RA¯K˜2@S"R“”vâ}­k-9±°ÄŽ’[ÿâól,Hl·§i¡ÇúsË5–bßi®yÝ ãÅ…Äb‘\t½¦irkGh.©ý_ÉÁŽƒ[5Ëhµsú(L þFðÜü}³Ç^>dëÍ;¬õÈœº ž±¹d| Ì›t4œKšHÌìØ±ì®ðÖûÜuo¥‡ÂJ‚}}q¶¬ähÛÙm“+ëBK2l ¸Í ÈA.+ùœq!®O6ÇvÎ:õÙ°»Ý›“%vø'¤ÞCþ ³aî£ûßA Œá¼fû<5gÄrÃÖšì½o¿Üµ÷¾#R—:tHöíÛ'·Ýv›|ö³Ÿ•Ï}îsVKyý ö®ºüµ²wß~ùúò àcx-Îúù`rxN@ϸ:èu{\/%““@¶4r¹×ö–™:¿ƒÌ'6*²fv¢0´Ùݰþk‘Ú £É] cÙ#æJ  hñO÷:ƒÿ¾€^d5°ÇÎÌPW!糖«Îçàg)Ð ]ë¬=ÚbN wÉÍ(e²> ädÐcAv­§!V¬¸…|¯yׇ&gkH‰YEdÑëX&ò­65)o3ífVʘŸÁ϶ݡK Eø¤cFp—‹"“ãfmê’½„a§$7Ìfš¬ºcNÞ?—“Àµ>‡B`®ýLóÖéxN]VLIØTŸ§”`çíÎ7Ìf´:ä¼™UñÃ]/÷ÜsÜ}÷ÝrðàA9tèÐßX“&ݰ°F†Á¹#â=¼ÂšeÎ|pº6£âÀ7þT·þÉFŸe¬k,ÊҰѰ#;¨ëB"é¼OÆVik{–3¨Ñ–Ÿƒ.&@H®HóLl‘Æ#)d]_ß=ÿ9½{ Üœ%ªhò©|.¸¼‰—««ì×áÏ-Þ]]â´RK•V¼lRäq|lÎ{çoô»?y5\ܸ´F!ÿý7?C>õ»¿a'bÿþýrë­·ÊÞ½{¿¬Çú=ß÷ Ù÷ijäÓyŸË6ª( S™$à¢#—nN: ¯6özgkåž]v ‡÷ÐÃ÷ô‹‡$›ef¹´Xuß/>·ógpN_K)^Æ%éŒjš5¬Ùu!<ŽíQ¢!Yêryÿ[/•g>ý+å…¯¹<2_P膮¾øëæòw`C>U6Ç G‹ýºJМ­$]Ó\ RSøùi.xâÁAÍ!³3b—˜þ^eîÐióL¥˜lPÁ4¬ŠÀ„»Vm&§A¡Ä›ff¤$¯Ã¯Í?˳C‹ÇÝ€Ù²`Xx [ÿôþ¿öÚô˜ö­Küí [ln&ª&C¦Þw=NWDb$ÀæçTœÔ‰¤q±€+¡ ÆBœS¹›ë†¯[Cû,—sÿÍÅ›¬t¾%yXÉ?Ï…Nq ÇÉ¿à÷[Âù>Éžçf<àç] %__¡<£a¦ »„×1+¸3yœo,–(o£\%°k÷Ã÷^t±<øÀ¶|æÏ?![[[®e‡5tj{ðÁåž{î‘»îºKµ6wƒ÷(ϧdläÚÌHÆt¬½Ÿ À"€ý~¿'a¨!k äI4.XÍþ¡Ù»°þ™1ê¹|1{ý–ÌÝv09` ¤ÏÿBíB²½£ ÝÏ =Ò yüCþ|'<ñ4¯ àù,ê9íÂ9˜ ,þ}Ái’/L•aIºgÏù‹¿ø‹¿¾AD ã9 µý Çm3@ÏN:ü› rx×νR‘ ¾…dÖZb¡ (-%yȽ‚ …eÝÔîFQßÍ=ÌdµÀ^liçS6!GëR±Û׽޻¢YGɺ¿u]RSKuÝjg‹^ËÀšW÷¥{Á÷ÙÈz¡áï^ ð°AKll‹› R !RÖ­5©=JÜÔ|ƒP'ù£XÀ=°«n¼J ¬í–Í]:KЪ¿ß`¾¨w} Öð>fÞëŸLtêbM/Ë¥›%‘²XÑã=]6Udj–„ˆÞºZ7eñNr•$k;®”¾`Ó¢gD&¶oqÀéHöŸ±ðCù[*TWµÞ†¬È§»—áÆ&2³X€êù5ø“%m ,Ø])+ž¥Fâ@êþ)ÌQŠç§Ô†€Iø<”GÂo–œá¬l¡Pkƒ JLºsh®¬Y…‡ÙRso ³DÓgå …ÌÀö.|¥/Vã‚êXÇBÀ‡3ʸß2YÖáþìß¿_öìÙ#÷ßÿc’ËÿM¢„ܳ/Ù2™ãØ4aöÂU”h’€Ì231éú—¨ŠÜúo=þ MÔç\s¢yÆRÎ;¹ºì+ñ÷)M8Kl^ÿt®yZz%y›¡“>ÿéØ{æÊæijNqS0Ôš‘Û61ðœßGÝéf‰~ ¡âÆJ"¯…{â˜0F3 ,7@G+E³™¦Inºé&¹ûî»ÿú÷˜ÅÆÝ׆ ¢{4 Îrâr5JøœR U7‘S…›»wY›EÇBU K£fÉÇžS@¦²႟Ø-y ›wqFà—…@f(½ø lJwÄ»!=êžùaÚýÆÙúÒíj= /³øÔÂnlró¸Öjôãü.­6 æ­Íf>w0ÂAûp4ë~vDï7³âæY0Òf›‘‚/=öÔÞ9.ÆÍ¦†ôhwoËX— Â4 ÁÚ˜N' „ª×ïQvªëe¢Ž66-ÈTÆgdíËuÊ@;Ȱî+,Ëè+ë_hý³å –s% ‘Í-špÁkm¨‘ÈX”`±}¬ý\Il_¹»Ù<ã⨃s™xiŠ+nzÜ»ÔE+sFCyV8O2{Õy‘s˜» ¿lC®¯§Y8™u9T¥wòX`‡!ê¨IþžYv£ü1a67×j@4÷ÕÉA¢„Ò~®gî¦=®‡ë…°ÿ~ùË¿üK¹óÎ; ¹ÛÃo*{uÄêúoÝë \²º‡ 7²½Á5,2¦yf9³Üv ÐÖ?ËÐøØpý“QÈZsÏßt®Ýƒ[€•ÇnëòŠw½7tPõ5ZaOÍLVn­ T†¬”Áá±he­›\lãn# ´VÐ6IÓã0 9ºqÃúºÙF‡ÒÈÞ;‰s=Ò˜±-ná¾%ç8Þ|٠´Îsýgvòƒ½*aí(‹e3O­SÂu#¦w&œ² ºpÖ ë¿$ë_¼+[ÿÀ:Ùú_W=ê=ËéÑ{7‹ìR|~SÆv0ð vÏÉüK‘/¾øµâaÅÙÍÙ.‰ážÝg󘃼ÒÛßZh—ñPuw$_³­‹³bfX`-™InÞ/êyÅc ’™ƒÝg4Е7àÓ¼ vºõœÖI†U94ÛÏz´'G n–­Í”1 rÄå}í|%³-®p•íêWÔ!Ó4ɾð¹ûî» æ'cz’Ž:ÿ·ÝÒ=8JŒ5Ø5-XoóúOôC6W" y:ÌÐ*‹Yj ¥õ†vظvÑÄ$H“¹™Œ-sVã$Ô¯i¬x¶‡euý»ê>Kòpî§¹ÀZq’¹¹!›ÄE,ÒP¬ßú¼ÑY¸¹<‰ùOIœY× €R|>’tàÄYK•I&rWëû˜p‡3)ê@l £ŒÀáñ@ƒ•Âm)°Pâ³±®Ÿ‰B—-uÍ(,D³‰2$ ÀÝ« @b¿ Eáfk¯ésk<ºïάÖÞðu†,A|Ñ]$Èæ =…!ødÙÉšúb¥]r &;»b·zÒöVø$úñyf¯ @Kï;ÄÈ>»¦./›73\…liëÂÖÌ`¶: [7u¼§ŒY2çÆ÷ÚüóB[‡‘GáÏgÃÎ.C†\"]ÿeeýÓð8î?Æä@Þ ²öÙ0p²û˜,$IEB( µ„ÏÊÒ¥ÔŒ¢–ÔèB÷Î>‹-@ËüÁKç€g=°ªxàß|Püµn½&ÎÝ’[ï2#zß}÷É 7Ü ·ß~û1€lûìñÞÇ R^ÿlÈÁ~kì…[[+¡£«Ì’ËÞLl¯ë:#•åïdÆ,Åã×eö‹ ËÌÂý£øsÈ÷˜c´À¼#c2ñ53™n`¸±žéÑ ¡'£h”¤¿ºÙThÜx÷SU5|¿,‡ÑÜ3=hÃõ¿V”:¾®Žq™²IŸéS›Ògò1#‡sÒ0²l´†Ç“¹.Ñ®xhßç¢ݳ´+ŒƒìEz*_7“P   ¡€.ܺÌ)tòþ¸58W ,×él8/3M­uάÏLZ•>“º²m-Ev0t±ÔRGÇ6i´až]¿”9‰n4,qóD”i‚]ž,ã;©b IêèÖÇ,³^33^g@Ñêâ¦26lÌañ¦ óÀaO¤Tmš™®lKÜ}¡×™ü3Óã]Ûœ+›Ž€L²7Ï8n”9!÷6g¦¡ò¶´û×åØiéý‘¯ñ¦È !óƒö¡´¼Ò?êúV(¯È fvlë>äM™"Ì ²õ×5È'%Yÿ{„,²ñáŸÃYe‹ÏŠÁÇÒp#ƒ× fÐ@QåÜ–!îBãûiNŒÓÕÓì ‚.œ'²ã¡zd‘‡éHÌQ)RÆœƒŒù›º©D†çМµ¶sºs1ìZ×z3W6¶,O]Ù’f~ß¹²uÉå9Àêë¹a÷6'5îÅ,dn˜Ers’ƒA Øt˜«ù&ì¨;¬{MéaâÖ[o•;î¸ãoŒ­õ—¡4Y•Œí('ãy¹â›ÉI'ka¡ŽqiÑÊ›Ù;+Ý)ˆ™5)“i‘Éc²%7càÚ€ÏÎÁ0™äv­wœÁb6:0C¨Þ©ƒùéËó~ ÍZ‘) ;q@0ÈáF]u3`‘ÂöžÛN1«Ÿ54“å¤íǤ–‚žçÔÑÆ™–dzӲÆéq+}‡ìR /¥÷ãl…É&9ûçPä“îÔ’8l¯s&éST_h Èݨ›ÀvÌ›ÙÚlæÌ¶Â„.‹v-ÜP%T Æþû„qnJ’B‚7ÔÅ€À%i’uªðomfe0>ñõà¼53­ÅÜÈô¸a0@îIþÐRÄ!³2gí, djÊÎÖ±Þì@}û]Ã&Y‹v/­Xlòýž¡ gÈŽ>Òõ¯/s»ë,¹àÆS!ÉÓõ $i vô³¼¾§ûJ×w7Œ38=ÛWvXÿ•ië¿8£º<ô22½?þ#h¼Ò=ëëf‚jqÎYGØX L@ÇàVN‚ÇÀl.d¢8»ÃLÕšœž#ìøf2. X𹊕gXVDL³s˜cTd–dá§Eæ.dT0§Å1Óâ­“¹ô401\/J¤à ¤’¶w'ï™±8p =ïÆIDöY@¨¥–,í:axtýË?²Ç~49'Ùƒ4+0Ím)ÓÉò§[Ǽ™l.ˆ™ƒâáíá¶VµT?+”¸Œ…ÑžäÁ^èáP~UýuÎ!«~"PÂ&È yœk>ù\ +æhÀ±Y"ÁþŸƒHAg.&«sæØ3œ[®ïò&¹OòÜœ$ÉÝü&Í‘ñ¼4 SÊ ò`Ü‘u)»8öý÷ß/×_½Ü{ï½Ç<ö=4?o·¶þÓ¯iã¢õÐ<གWñÞÌìãQrŒÒöµãsaª¼þ‹—krƒ5ä¡áúöÇ5~z%kf(-tïQr;kþœ|߯Îa&ñÒ#á:ŸÝ­áóß«#j)K Ʋ:¯ÒèÔˆí¦ð(ÑíÍ îv§‚ªda§&P8¦P—yW÷,ƒŽ>ðó³ýKWdûжK‘·" M•ç”Ç1I/üŸ½ç IDAT~ àB… ¶ZЖZ¼µJ¸@©R4\xÜu4sX7vÓ¯—…ºlèîŇ$Å9WU+ÚF‘6C´˜R‚äK-°1Ÿ‚³€T6ã˜{äö]*y‰iÎ.Ì3±hi­yïòY§iXwΊm²˜Èü”RÆRÚ¥¸‰ ¤ çz¼†u𗂪Ñp–1?µØÏI' dn!Ñ™6 ¶ ñ¿/â³±º89¨„r)^¦æäœî¼-oòˆÖ?ꉅÅÌ#Iq:euýâÍÄŒv§áÏë OXÿÄð8Ý5¯+ìàaºHz,@hõsáÿ‹¤:,žuý Mr|#‚²2KÓKÛDl­›‚¢E›)KÕ(F +ܽÌÅÀ‘Ÿ#¤ûx6ÊÉm Vû^fa˜ÇáùB ”¤‹ûT)ÔaG¹ç&š¾nsPÅ;pºb$…HDÆÊŒ(šwètçmùU݇Ýk YxRÈ'êÈß{ï½rà 7ÈäoËŸR$us笭¯¡ÚƒgsŒ5èñu}M‚Q ºn~¤õtž(]ÿfx& Ç5æ¨Ål(d…9¤UA Ö*óµÖˆÃÏœBéél‹»§äUXÿ­öŸa¶Q%Pצ6ÜÚÌ8¡‘"etÔ1FEHôª†®€KY¤êeq×óì¾îKz©Ê›6Úô©ÇÂÂÕ›ÃfB óÜ0c¥õ4lï¯ÿx¡#ìòS<ëÒ;ӱņÝ{C7‘8;ÑT6ÚÍ›Õâ†Ó»ÌeÎ÷ÌP³n{Úêî °¨ÖYt¿S𥠨&@´–ÛÆ,ÄSz 5K4B&™£.Š+”é)ÕûÚ']H|x[Ggéìâç´™˜ÂÍe)­wwŸ",¸©‘AÂÔZ0œ0¦‡ÓlþF@&Ö‡Tsï^RcÝ í~•âÀîdÜ1ÊÀŽÏ¥Ó¤Ÿ7aÝÄ'|Ø,ŸÃ|ÉúG×;³9ÝT0•¥2† ›` íŠÇÆ‚5“B,§B£2ü7‚¾²Ìy7¡hÝvà=’?Í䃳b4*$ÜÃVU4ê¨ña]ä°Ò,Ô×739if 4>XæÀÌ pÇ3s¸c†ú9ç&CŒÍò8ûðA‹‘ý°ó@Ï4f“Üù`ƒ•¿‘)³›vfñ¬ƒN.J3°ã†ÈA¦è@]'AÂ=ùþöÞ=øÓ³º;çyd#Šì +bƒÇ6¤‘zp¹¨‰p¡Ô—i}Á1àØFKÛdÆM±“§ +Ùž21ÍLÛ¤ÆZü‡…S ¦%­-Qs‘!ƒT®0Ä»üÞçéß÷œçs>缿]IËeW^†ÙÕîï÷ûÞÞó¼çœÏ͇´âùsÆRh–†Z ÖËG>ò‘ *ôô|üê CøWÕ4YÂä§Sý£¡B;¦þ«Ák‡æ…y%ê´/­ÍÒÃôL>¶þmyØ þÑà¤åü=Ö«IÅØCßFFß*Í]•åÄç/}º[Dk*ö2)FjhM õš‡¼ÿÐC„¥žL­Px¯ð¼‚ÈÌ‘äaÐcMð,4'Õx“Õ"SG<åK7åHÞ¨£©ˆ6ÚÔ‹èŒÑq¬qÅF7fªlƒÎ‰<ÎË"*êBüNg -wD)¦w»vÝLÕ sØ/ŸŠÜ<­>ïV úÐ";lh†$¸=4M”S·X¡ñžpõ3}XqÓ}bYÀjRFÈÃrǦ0Wwji- Oˆuø€xX]7'4|Ñ ´Hã oÁ¢~=°yý"¤>­ÓÕ¹ÄfÜàîm'Q\fîÖñ€Íýý©ÏÈrç:uG8s@tmOYÛ!€U}Ø‰ŽæŸã2ÖVÕ? Ü©@[9#HizÐg«9ØmÃÐZˆ–Ãâô7s¡…Ð ¤Qˆ6LÜ´†&dZXB_hil®{æß³5pøù™Âh<µx…û¿¤æV–áø^„áI¤ÌøáFцµ40¡ ±Ì¡ÇÎDóª&K) Ø£«Xç}AòH¯—Z8`ÇäbIÚ*vîåh å U9::’‡Ú/<¯9Iæ;´¸*÷*¹~U(#S¹ZtÓ,ë_µtsc40 Ú4ä”ß{.õ߈ÖGë<¬ñyîîŠ;´—«–î‡ L‹KÍ? ¨"Fú&dœLÙB¼®  ÖjèÖBÈha9A‰ìÂÇO#—¸ùÞµÈTàû€HéÞëˆÑÝ/†íEØ4oüº"4ÝWÿ•'@]ðN“·‚¶5z Á¢óbˆ7M )ßZ¢O¨'± ΑöÅ5C–d`p' ;…¡€pƒb—!çûŽÉëdw¤°ÅíqËT~¤Õßmýt¿+nK!mšCv 9ÚPÚºÓ\Àåîh]Ýð`Š’uÛºƒå´L”„-ÅLj[6û¾e9臌âfCÆ€°Õæ‚èÍ*}j™WØá³­ž“ƒá¥LwÙB åÁa?£ê¤Ò ©I™Â0³±îoý#uÅŸ}í²­þµ†1'gn©N;ñ¶é… ª?ZEÚRÄm‡·çj?)u`‡øã\ÿeMŽ ,|@G-4iœ‚uýN8q@(dÖ7ZI÷@“Õ˜èRtÓ­Ðýêï¬æm¹R} òÆ9kƒiw–7„ÎQèzǃ™gm¯æÙ:=4Q2’¥8‡žº¸tJÉ*W‰g\f¦Ð9Ÿ~;ÛXÓ¨R„¢ãÐ=çã2œ³€~WÀº‹'n„ùhè Ôö‡Üü³i³¦e(o:ûUš‘êiò›ñ "sèdVÆYWÿ;Ô²r õ_|»»m,ÒÙQZP„­2Œ†2[þ…EpË:%ì’;^ñ8¨r= ƒõ¯Pÿaa{HÏþp~• ËPÖò !æ),íÒý¿¢U¶¸íõ÷_Û‚j=&"møsμ( ƒçÖ£Õ®Ã<ˆ ³ø28i<Ã,£0.BÓU`ºn˜CVS/2À)jí¹ÙVUG*ñ; “/ª$Öé@¥ãܤn‹R‰Y˲øP€ö×UƒÃPE±A;T>DÝ š¤E„ìÑ´ JÁˆ’Õ5S_÷Äl¯†Ñ…Á(NN9 SÝédPàÖÜK ì„f‡£i€ØgO¡cóÆ;¢“âC»«u[ÅA߉NÞNÉl1ÀÚ¢ë[¶Y¿õ.‚˜}e&Íé#ظ£YŤÜáf­Ã5i‚Òî5ì4?û¹ËÌJõ¨ßì0kˆ)uèzçÓ)nÖ+ê߸Û=Ô×wÓD?`+mz1 xÔ@XB0‚nšeùÏMnSjèL©ø S#!4.”f- œe¾4aª ž¡’mœíq†¿·?œ¯0„±M.º¤±ÃºÈ1šå‹²*¼T4¸¶ù¹¿öøÞ‰F„†*è°‰‘lº¡²ÑÊ%6œ~ïê#Rž½±JäáázSÐØc«Ch¢«|.U–Ï©þÉp ²VõÏçHñ¼wëŸ,¼ùÚÚ­ÿÂâ¿v×R\3=Ÿo@Ô{Öb¾‡CêaŠ^K0‘zÅ!!îÑ@ ð‡ðR™´íÞÁµÍY ]ªïÿaY qSO:2“Â(óp6õÑeiKbMGãæð‹ 'hBdü–¹3?<‘G?æ1òâïù~ßâŸ;䤎\Çz¿¾ýÉW}«ü·?Y%«› ˆÈØ>üqh"1kÇ2‚øŒ |'sÛ¿hä{6hV[k2Öî‘åû,K“£u¥)ÌNózo¨i`KMoDGLÇüŸpãš7‰fAM%zᇡÆicà¥ß%Ü4¬-ÙÎoê6@Ù¿­ëê›bû=40D ËÒ¦×èo¦jdñŒ×ƒ7÷ÖHlß3¡lßÃù)–iÙTlƒ=’‹º@…³¯÷ p’[Ö'„?Ç,¸3"RçZÿ ¸{9§ôQü‰nÜn XˆÛûŠÚ-kê¿÷¤Ù; mÓý‘O«ÿm°Y{QÿEfÏì¾t>РA[4§t`†…ÌŒ£€£0][Br‡B3>&õÆEñä,WÙ*Êèý áE0èh^1¹ší‚®ânjo°‚íÍX›?Ý)Sø!Rß T©%ÕpUÒ[¼/°}tÒ\i˜ì³al¶»öÆT³ág’µhÔÐ×=ùýű$yVY):‘v ò†ô&p*d‹ì‡¤‘Õ0‘ýÚ ßë¦-EZ„FziÉ ¯sÖˆq®V©Û9np+(k¥={‘EURѪúïÇÔïISˆ×\zÈbû¬õ_ WœCL`ZmŸ¿g!“t ¥U¹âs²‹Ú¼o,t’³žÀç¡à«¾X ½R9¤×šÒµ¯A‡æŽÂ[ßgýÞ?ÍRÃv™ŸìÍzËm¯üêÇ$k¦Anbxßaaûýwîü„ÜùñûDdk‡Š™ç¼l³>º4i³i"7ü½ï’“—=Jþ³Ÿx­ÿŒÃëm›€ Zì`t ì™qºÇl˜±mðMÝûåq lú× Ái#¿¯v(žïd1™ ë1o”(4¶-Òº®Ù‘{ÏÉÐ3L¶7ã?Ê2¤Â4¸ùÄ,¡R»€Ÿÿ60¬½‹™ª¶m+þ樞}º†¦·8¨í³³÷ÏL+,¯(P›6½N‡›Õ²4Y]ó e2s@M¨6ekj=¬t£×-'ÿ»ƒ Bæ×èÔ+pr)<×úÇðÑ•¢Ÿ›zØ*¾¦p³XÚ¶ ´€Î(‡×<¿B÷Ìr–Öˆ˜NoHäÈ+Pâ»Pÿç#,uÄ…¾†çÂE2ÒÆ?Üü5Z c-âgc”1C˜tDÄ I σW:QGHÃR5߈ÀðßáýÀ‚˜ƒÐP‰@ªô>JÔ1X MšqàjŒ¼QÔµ†)¢R ¤7S…5/¢)]¶Å”ŽˆvÑ’(e+ÁýÀQBg§ÿÝØ1€Ði<WÍ÷hü|ðÌÝ~‹YǵúЂÐJ¯·:Üó¿wý´µ*8 Ð°š}X%÷BXD2-.èìŠ#Õ¿è¾FˆQ3á©EÑîPš:¨þaÉÄ®“eý«î^ÏÞë4@C¨þ»Ì÷ùÚ þ-¤$YÇ%²×ØÇà ¾m!ºlõïÌ!—[ 8Yå†û¸/jþ=.Pv³Éô"‚Rs :6'öÚïøøgåL³Kos úbê=¢Q“3?‡k0×m¨˜[û¼Ì:Džìˆ |ó×?Vîþä}òŠ¿{µÜø«·Ä´{xý'6>är1[ ×´F{]{‰(ˆˆ,!»Dåðuæ*fÓh*€V˜Í‚e¤Ëñ¥2Cm›hT¶pÐ ‘µ¯óý”H_ð1wiKr!áv^Ûð%«”è&Éã6ˆ­»mˆ5:›Ñ ƒOvÌC}#s´xÉŽxdGî-¶hsl×ÀѺnC”¦ç-,¼¤ç›’‰¤,'ægê5DÖšÊXG²K½GKi*Îs©üZ¸þí{m[U!E ö†<©Îϵ!µ†õ5„¦HA'ŒlÂД„©†.KÞÆ=ÈMÞȤ¨ÿµ÷Íð#»!ƒÈè2”$,žFè{pªJ@ ëˆ!²á®$¤‹”nJ¢iKlÙ×.½õ²ÁÆæž›ó08è¶×®GjšÕübˆ ¸*Ü•‘1>ëJŒtÞ*CÈ›béy'³„àÜó×3âgŒp µÙ.±K$ð³Èž]êZþü ZÐ)LJü¯Ñ ÍÝ2@Ôû3~>Þ¸Ó”σê±í³ÁaÀ®Å04´˜©ÃH ›_¤ú?‹›Z°õ4hܤªI+Äç ;$âЃ½kxªs‹‘¹Šª˜–òš?›Eò÷72_MëpÖvÈÄ£ ´=>TO': Gä§ê½ýôAR»sûçæã0ÀI/D4.$è@ŠYÃé4 Á¤…tþ·½èèŽQ!ÔQg“Ж¶‰IÕð‡Üá8Õ0¹mø‰þøŠ]-·ßu¯|è®ËS¿é çg~ì»åÓúYyÜ×ýrò²G‰ ‘×Þt‹Üôö[¥w‘·\J^wæ­ò²<[DD>óÙÏÉ‹âÆ`{˜Píu/KY% VÞ컳V´†D¸8}ã&ÕwY–•k††±ën…†Ö¡)k$À¢-Â÷î^t7¾ÁÛßá0¶KÑ1ûçAÈW›l® ®ˆÒÅkw¸Fƒfe£ÙtêµçÁºQï¶!樯ò°'ľ¡Wn_àšÄ>]¹cÀZ°ë}W "$lt¦û^T¢†bS ;Û¨s^§vm|¹ÔÿBõ¿úþïÕ¿ÔVÚ£@e ê•s<¡ó¥Z–Å‘M4F°ú7$v¡úÇ ¬i-€Ø‰É-¦qð‡úÅ6Òi_ ¡ô£0*ð âÖ']Xæúë’é Z¡.ù1óÃèB!`¹_té[:]7Poíq yZÇA4,y3¨¿dÏÞ$ÇÊ]Qç?¤ô§p ‘^f2òãþ¿ÖVÝL…c×͇ô/Òײå3êCý~[è^8ä65ã…[[ZZ¤bh0›"$$ JTÓcâ×𠌚–÷ô°@عXRlv{h‰&eîô{ÁaQŠú§PUÔîzº¥Dæâóvت1ð¾F·æ½§©F°€{`2„úoõð£á9~Æt覕`¼€ún¼ÆòCñEa‘óP"¥¥²ÐeÍÿ t{ã2F$ºOhËDᎨ@ÎNp`’<Öe«X£øÔ¿öõò¡»>.7¾é9ù˜GÉxÅã`í/ò´'^)o~ç»å¹§®—×Ýt‹¼üÚ«aùÞ¿õ4yÎu§å9×–{þøSò³?ö=A`¹?XPfºÂùìGy á&.1…94=g¸sÓ Û•V†ÂC˜·:­¹òF9Á›ð²m²«ì¡ðXRäà†FæZÙÔz&L;l ’æúfƒ££*›±‚5£è*×ÚÌcc ž•y-âg…ƒ]xÏ¡ŽV‹E Ã €Ͳ5²è<ƒ”^»¦mìcHÎtR¯Oïý—mýoÿ¶,Eý[Ýâæl„0¸RzmâH+„ÃРóðË‘‰Dò‚8ÄQÚ '’‘4F(Î7 «ÓìdžÇYâô ÚøïP[páƒÆH¯ ¯Eê|²€‚ )­©ÑØ%8á:†M ¾?˜YÂÍe•8›PjtIÃ3ßs¤ò‚( §’i9¶½g“§œŠ‚ES]9Vï}ø¼ð6j m¢‡Œ¿@‚$SC‘Í]=GúÚ½ejÈk¡žÅùÍÖ)ë}”¨U:Tv7þeýEÞŒx*×¹ðwz ÚRÕb÷Ô¯1¡þ g8-,ÍSý÷¢þñümsQ„¯®k®3dÂ,&ÅÁÙ>ã>¤jÜD!è”r€Q.鳨ò ˆõgçE1Åi5O² ¬zƒÅžL!V s”i™‰–ºüYúO2 HTCVoÍ;Ié14”!ò¤+.—“—=Zþù›~KTEn¿ë^yñ³ž:7Ö"rûGï•3o¿Uzræ·ÉÝŸ¸O®}æSýçüâÿú.ìyæírò²GÍÐÌM{` 6^[};èšä,ñi!‘܇ ÜÜ'Ø_Žx#CÛI£M%()B2ÒŽó2ªÍ ð–<Ž›GkJPôm=ü3ë‚ÎlHi²°¸óH´²ÈÑÝ êMìP²ëÞƒda[Û0tŽo<œ‚!el`iNqF“[*‡>GF§)‡ q¸1Âׄ& îF7fHÚ—eý‡ç·SÿÝ9öeØ8dH´DAˆ¡›²s<øµv÷fwþ9ØÙJ‘‚¤…ñ‰‘ ªmÎ *)e*ü/Ñëø³!ÄÃ)nආgƒ`ùŒgY–ÂV[¢E3—…§˜Ÿe býP%ÞÆÁ¢ÂÀG šPÿ#šÛ„ïeÝ”NƒOh7#ŒÖJ(3|v-[_§LB“Îÿãsõë Î=Ô~âµáC¹œGêè þdC¼×^„ojQÿŒ­þÕ/êŸp s"¥2 ÈDƒ º„²öµC4Ègžyì´†H;Ö%wÃÂdÁm­«JjcŠÊŽ)|hŒò„ˆ´ö¨MÖ b”ÞÃU™S-î—-Ô;ÜB“%·Œ‘¤'nB$ÓÎïçÈŽ ˜B«&¸EÊ ònK÷~˜XýÃùßûôdÿ÷):pG°mÔ‚ƒˆ.iˆQ ¿`# øt¬m~·ïyñ³ž*·ßu¯oÏßvÛä[2ÀŸþ?—¨N>æküϾû.æ¶¿ô’Gâ“8Û–ZmÏÐÕ'¸A)å`H ÜsñðÆïw_~7Oäk£fŵ,@Ly2†miaCÍ(¾&ôÙº•6 ¸iwD 7àd²!r°? †koÌø€55 BLíÚè}*•5ª³aÍ›!¦ r›$:r™3™ ;ÁÍ®¸±ÌíëHõã;Ñ&¸6Ò!ËIÅPñeWÿzõß þ!|U ÑÁÁµZJOªm7ŠF÷`~aPãm?oS!‡·¡aHQI(°ÿÞ4:Â5Mt4vŠ‹–á´íÝPTôsæ^ Šg¸ÎÇF„—8vf1‚ÅhRxMCRšC ]¾hŸ†Ûc)ìqé}áðQtî Ö=Úb' b1X†!ƒ‚\‘žW¡0Z¹e’ž ©€üàÂŒ0ØÐ³xßGyèBHJá°TŸZ¾6SýëNýãÂõMº×J°)R •ç»ÙcŒÂF[vêŸL R ¯Æ;F9À4Ô?¢ºY1•¶gô|¦úÇlǰ" ¼-¢Fh¸4F6H0z~+(ˆ‘nßÿÖ4P´Ç(>+4< Ár–|ýØ«ÿ=j¦-5;!Tr±#¨ÊA.¡%œÍîQøfVæñpÇ7Tƒó”ScdräÃÆ>0å@¹Ô¡`?¬aÃ/"òÄÇŸ”K/y¸¼åúSáï_øŒ§È™wÜfkâ ˆ¿ô+!÷|òSîD¥›—zS•+N^&Ÿùìçü±Møæ†ïf…ã 4º¦ 0Ã7( /CݘÄô†tÃö‰šf´£479lÈp ¨E°«MpÆÁ “M»„6¾HÛãÍ%w£fîж!öDk®­ðÏ_45úÜüªkÚÌ.bã$ÉŠ·mHKD;\Ì̘0±æf yÑ`iƒøZ€³½Ç>@âÁ؇ôÍÜkÕ¨F÷ÓÖÜîÛÏÏ/uýƒ­÷ý­ÏÇñ%A„Þ w$«{slSx]ÎÑ>oh»&›l«3Ï0ýÚãæžëô1Eöp³ ~@ ¹ÁÅͦÿ}—8dØcp qìy–>~8O¦ëE³å®kT‚jÑ@7·½Ì4´±É»÷Œ¶ˆŽ„ÇÉvÙÏ2nxÃ࣒^Kø¬Æ¤ásòkkôš€º Ñ‚ØÖ 0öBr•6²¨@ÁkÃcöȸþ ¡·ÆÚt˜á¾õOÓP›’5*¬ «Ò‚s]Ëþ­Èü+²ŒÐÈ!ýXôB“ÉÎy~F.- ih ê_÷ï·Õ™ŒaËi8AÆÇ1hÞ»x¹©;Îv%Z%h6Ñ\þõÍq6-FûuÓœ+2|шÒeÀmUå˜\v4Äóù"˜â¶àIþÝš¨ÉËgþct{²ÆÞ0$ß4nBbj™€¾qk.¹53ó":üÿ…ϼJDDžûªä9×–kN–ï¼î5ò®÷Ý!ÏzÊ7ùóÚ¯‘Ãöþ/ºZTTμã6Ìü;O÷Ç}Þ·?YÞÿ‘{·Õ¦êB: €ÝÚÔÜÃŽŽ"¦°…tÚ“!-…#I•P<¢¸ð¦ 9¯„®5nEBª=>!¥ä@)ä;´•dºÐ<ä°h  u‡€PöÛfþŽÜ wJ8ȉ¦BÑa°É+iÑ!°lYfÃk ¾‘Ы…9„iÝÚ@Ô:óv<„usáâË¢þ—¶ zwêÔõ”‡ÞRýÛÿ‘v‡×„4-o|ªç§­ ´”m çúo4 % "kMËœœŠ’„/6*ñ™3~ilš‚ ÚÒ£–]±*”‚—< )Ù®9é‰W¡2b­†Ç€l§Ó ¤|¥ÎWH·1FF¯ìlÖèd͂Ð}p]ׄʨžCý¶ÕeýÛ½n‰÷õ’vw\ý÷}'µPÿdÛ] #LåOnn#;zòcTÔ¹Ê`‰-ë+qþLp1´>[,5’C/õpŒPÇŒzä1¤DbÐY¸ZV6CqÃAC øè]ö€!¹þÃRWDŽÌü¢È à"Þ;%ÈܤÌF6°³sñM€Úód$£¹Au£]…,ºƒ™å1nðñy5Ún¯}Úg?ÿ?þòþÜã:û`ÿÍ[WNÿÈwÉ·|ÃcETä]ï¿Cn¾þ”­ÆåšS§áZòé?ýÜIÒƒ~èþü¯ùÐÔt"BÁ ܰХ‹…vÕfÓÃQ”BOm2Ùç IDATsI®pøoHW¨²%†¶9Ž•D[ÜZŽÚÒ2½€ú]àc†ç ”{f“^[êó–FüsáZÌZ\è8©K†Îáv* _H)*q ­ùüÑ‘,Ûµk¹Q«½–µÇÍ5 4™Ͳ…¢Ù°ybYœ¶æ ÌÔÉ87ܬv,4¿Põ¿¹{ F-ëÛîÙ‡zé3‡qW;mMš@ý¯E8ì…Sº eâŽq^ î½;êËMQºAc6mhS!¤|'ŠW•ëÂf„ð"š‹ÛR·ÔEw1©àª×àõÒ(œ™ ÔáàB¯…*™2ÎÌñ.'z®V›¨7éZ’¥/…hû0ÂYh(7„¿E´ÈÏU\&i4%ÀǰçéÛ¶ˆ w¤Ú{_Ò‰H"ýJ¯%Ú{‡{ˆŸË½)hÐÖÙ‘)à³!g­ Qõ?vê¿@O*ª^¨²”NˆÏq¯Q¢V‡³ãódš`¨ÿsˆ/z+²÷…îÚƒÑAx%?·Ê@¢ªCkŽŽVY–ˆùÒ~[ 3ûésvì~ÿÓEÔX‡ƒ?d·ÿ¨=m`˜lFÎQJáücÒ•í~sáA›çDûA›Y¤«á1Ã@ñï¬,þÌvØØ ¸‹ÜpYè%RßìÏÖ¼®ëÖ6=˜XÓñƒ?ý†°ý·‹ð=wÞ-ϹîtØ€}ç+_3-‘UÁï]å¿õÊ?üù_KúõFò?¬X·f»õý6¦ óö dnTÛX•d‡ÈŒ¼9Ï6ÇnT,à:L Ô b ÅÁÃkWl©±!b§;í1´¦"⟛¢Žä0TØ5ÓÇ!hTi{Ä”¿™ÕR ž½/}Tµ¾vY´yŒq„ÝùÉÐÒ2¸´;lÃÃ6¡­ëöþnvÑCEMè?J7ߣž»/HýSÝYF‚×UŸ‡~YÿŽ˜@}Ãj ‘±¢mÛÜäš [ŒàÏOù “ö66 ë>Ž·p½?¿Ú6øV Ê®.‡ê"89å³’2Û˜ÊVŠàS)4.MË /$¤G=žي¡S^ÿätÇ™BÜ̇ÁL轕\ò< Ÿ‹Ô¹=¡i„bõÙ„b¢:nšƒa ¢ƒÎ°²¡L¥kC‡F:d‘ûƒ¶âÁ`gg±>ïj·< Õe§À‡ò/ÌÿA#ŽãhU¥mr«ƒrùƒ¨ÍߣÑóõÕGêÓ.x:œÑo:4!6Ã:ш–áM´Ö•¬pí€Ä!/>k°¡µ”us^ÃÍî´›ÕÐ|[³µ®õVÄiê:):vSiŠv²ðõ›Hm)({<Ørµ5Ú:¥¢¤H²Âµ¡ƒ7xlt—‘¾ ‰Æåpy±‘bj žCºø†‚&¯ÇÜâ™J¼d°¶µ!YØ]Æ !k-ÂÊ}LZ~ýѺNèYUNlgÍ‹5{n~¯ <úîN7¨o\̓áFª;Ñhò¡·Þ¶>«9»Ñ©s:_·H¶×,ªÁ`ä‹RÿªaÖÕÿÚÝ{"¼‡7Å(}\ÿ¢™ãn:DE¥p’´Ç‡ò÷Ï$ð¦4Ò>ƒ*Y¨Â×õ1‚.鬎böISvó-lDjÓ¸ðÚÎ@>'€ºÅ¦YAkrhd)=Èõ’‚ZƒËÖˆ±Éåél7ÎÜXM‡Å©8*+YXUƒ¨Ÿ<„ìÑiðÜsS ÑR'‰îq‰‚K´„îh‚ë\(Y‹K–Ù‘.4ÎúеÈVz’-:Ä_0kÙ,@}$Cþ|“¨B”+zº6ÝEs˜ú¨¸š$ ~s‹Ë=˜† ¯A4LÀ$dõôú§¡EÉdÇž¾WöxHáÇšfš"›AMI‡¦áeJ$F±àÆhìÔiy殳€&…-fòñ½.}Þ2ƒv¯sg\s&Ö–Ç>òyy1 AÞ„Áv·A31o\„Øpº¤ÿm”hZ¬¡ ƒ˜ôMÀ·ƒsÈà<l,;"ZCøófƒ¬îÞ%ð÷èèÁ¶†vÊ/ó[Lñ#m|²x•·³±›H„ýœu]'›ù·¸Q=n.Y ‡FJ¶Æ à¥WBi§ûUÛ`ÚøF¿ëCÆ¢“NfCÃÒš¼îNF9:hFkŸ E_Á‰(‚A¸ ï1RáÒ f{ƒ& {ðÙ¼_r”éÀÆ®p*¡mPo0œ™Î‰QcÖ&®_’úoTÿÛ¶ŒõEþy㉠=5 ç\ÿÀÆC^Îõ<œ¯óÌjE†„ц Õy¦fjå@S!þYz¢€‘QRZ™FËVÔ†°ky‘Òb¹¬²sæ!ƒ0Ú4>攣“γ>àÌïÑ~[%¡%á=–HLô?ÒDñ¢ UXö(xÁÂ\)ï=w „ÁÒóãzOè[@Š˜æ‹ 3>ó4»v‰Î|¯‡,tœ.§4öè®)Ôo¢ž™­2/Ó@€h „Œ¦úo;è„o&—8Êö .³*)‹k9KÙu}¤< @ÁW¼äl zAkTÕ?Ùú#³ß›°x)¨pø¾·”Ã5­¼°¤­ú!_h‚Õ6†ƒ+0–¦ùÏŒß0:¾š)ô!ê1"1+ ©ÐH½¾àép¬/Aî½SIHKÅ_d Í Ø´Q!K?‘ðw¨‰0”È è&:›äHkáïA×¥I•ÓtcWUyåÏý’7èªe}íúª5«é4ø¼L>hèŸhÂŒ™PÔtçmLE5h܉?Ó¸ž‡Í«iÈ8Ð+6zÎæÎV%”3&„ôÁk6޼ÓÉ -Þ¿ntìpÃ-BÞÐEÍÖè¬2ˆÓ:Ÿç ÜZDÝx³Î´½`×Mî6Èƃúh]7Ët…F|Ââóz®p= P‚©imL ú5-9§å¾¤õËL¶ÆŠNÁRIkãïÑÖd=ZÓÆm^«c¢I›ÍvëÀî8Ï+m®üïT ¬»`º\ÀEÒ®CÜÚ…¦—–5CIcèLzn£ ÌP¦Q°ómÓÖϼð|‚~4SܪíÁ»ÌsEǤâ{oºGHzÄJÚžŒ¼4"Êp(݃*½³É gäsž/EqŒ´ÜJƒ ¶œÁĨSšýßeÔº#¤s0yk‚*Ûët(hlÁ° \Uoi0 è#:̵úç¡ë5fðüÐí8Ûèà~‹ƒMŸ9B¡þ+CÁ¦¹¤ç=ÏÒê=+>pÅãAì8Ú^E£K”ÃíûÖuRä´™ÅÅ9ºÂ¥…ú¨2G^pQ4jf¨P4ùþé¦Mºô¼¨‚¾ýÂG‚0´®ÓŽa”ËnzÙ–œi2¸’ Ù’³´VÀŠ˜ 1)D®ù ;ëîAOŠ2 hfG £¨f$d]×Ýl…¨á5ã@g¯ÃQ’ÁT'É!¡ÕMvïÔ¥›¶ @ -6T¶I®tù¹þ¼á¹"]l×ö–¨78í5ŠáF¶œ²??˸AŠ"~¶†úaF`Ѐ™ ƒLddg*‰‡vø,G¦7Øt´-  ;˜kÔ8g¢ÕàüËjµm6â² —N·ƒ$jlŽ«ÿöŨÿÉqj„SæÀ50ôͽ6·`‡æSõi­®tû´Ê–v~›¸Lð3[A[v:Z—ÎÏàa>5éÜôÂŽÔ×`‰LŽbh"‚8ed­QoQ»¸Wÿq^†˜h -Å-­k|¤piBÔ ¶ÚžÖ¯·J®ßÓ,ñP„.rSÐ| -§V4A®WšþeNw³$Q(1º „ߎÂÝrÌŸ\á!šÃ›K‚t8¦$±††)rè¸VÖÿ1çPÊÏiSG‹÷Œ³!W<@URу‹%™ì+k\¸ïÖÛ©ÿ")Õ¸:Bpx!îĶIn¹V=æ}…ú9lQÓ´Ú0Tõa|Þ# ?CmUÖOagÝšßï´ÊÒlØSàý¿®ÿƒ.‰V뤿ð‘ 17 D=ͼ€¶&\¬\à Þ±‰µlÙ?5öß*ýÁ-‡Ÿ›ñ1Šm)x¬³ö§÷‘¸˜( çÌ¡›k?Ü™mwÓ Í†wöy†™˜-ÜXÀ–1d`Eur="Wµ#F5õo[^ã Ñl|«#™ ›¤s‹ÐP]†Â6  gÛ8¢^Œqö9¨‡NúeÌ™›YØhB¸låä…5êìÀ64È‘°|ŸŽS§ä5¬‚ãû|ÈMj‰þå¯Ç›{!ÔO@æ|Û¯ÿq¾ê5Cc´?‚Åy‡FõðÞHÒÝå÷h„¤ý©„¿£¨ û(ç†úk-ò&–Ödáfcœãf~߯ô=ªÝ-‚6½yÒ9Ԩр`¢YKY:ÎÍB[ý# 2¸$£1MÛîß»ûžFj™Sq5¢f¡Ö•¬©Ù|b2Œ›çAï¿hpµr”Jb°rÕ€"Õ ßg[1Úì÷,FÈœÆ÷]Uc>’C+,è‘""_ñ°¯¯|ä×DÃ8×j¢éðØÿ. I™uá<<‡G{ ¿.ùªGnáÝ™ê¨lÅ€“ò|zaZpL6MZŠ$ÀcͰþÑ!ăђ”GHÌþD¨ÂuÍæe‡žÖZÎGàÈ(ÙÝÍë_âsJÈ\á¦YÊèy¶V=ØB~ˆ8dÆÇÀþ¼pd«j•.ަÃ3ðfv£L³·›÷ߥ-²ö•X7ÝQ!;W'%N/ްT§@‰ÀD-žeâáÖL¡ ŠsKÜš;ãƒÔ&‡Kü¾9(i°?f&‰§*E*ÃŒ¸ÑF›Dþ«9{•à×.…· B \ÅìªN¡¨D©ò¡œª ÂŠ~’Pì¦CÖ1¶ñfz‡}'ÚW!n í¦§›%4 I| £5wI­Ð輂´°u=XH‡¸qhʃí¤Džµ[fÂc\äOûA ïÚx»¥{k‡¦Î¶Á`€€Ð5^£Ý@7—å@édKjkªÌ)Ñ—‚*ÓÉ¿Î^ë‰æø@ê_Rxn¬˜RýÊìåUS ûУѕÎQ"•ÂõMJoÞDv øìñûÎÇ>Û¨n蔈”Õp¤Œ< My™hŽJÈ{Ø3NqúÜè™®#QôB#"§i{moo—”+ÃCR…f»&§˜DƒK戎e}ôIݯÅV<ÛŒ>—èq4$ àÞ‡&‡l¼]T¼5i!ÌTFýZÓÚc«Ñ˜]/â‚S“htÉãÜ'üº6í¶NM4:6I2Üðó{À ®}‘<ã™Ï$£=#ǹ1_Ì!hiM{ùåòÁ¾Ì”4êQ4[dWˆ-[`;² Cªÿ‘MNMµÅ†…wêE*–¹¦…³Uéž>F©ªœØØ8!Õå׳…6–Õƒ£C<$aÐl5äDDÊП=‘†ßÓ{nq!NÂ’pX–u¤¹í4Í)éóë¦9Qs=ª-þÐ5siËÔþàgI ¨ ~²­x †m 5Yfa»®Ýs{ƒðìÀ3ŸóYÐ¥Ë]pàŽù,èÇ™N„ä­m£ÞvÁ˜¾‹Z1bó|ØoÁ™Úv7 €-â²! vqMǯ©û¬MÍIŒ„­•miEaI\ÜŽà"4Fàcš6'<ø^ÏÑ€élÜÂ¼ÑÆÏm˜¡‘a-¾Ÿ^œ}$íÔ¡y9 v£V¸q¯æhèžqbH¢yðKDÒBÆ´!)"dmƒ½o‡ç:Pgc•i ¨‡³ëm÷)g–3„º§¡Á!ÈÈÚÒ÷ÞƒãÛØ©-ê_ÏRÿ£ªÿžùòFuðü~ý#‰¦Íì×?@áýC†¹­Ö`qz¾Ïر =œÜÝ >;7²å¦§)0´3üÛŽó›¥ÁcöC °uI–Îaˆ$1wÊåàóŒé_¢ÉT„é)iÀŒ3FC†$J6aèÀæ|wÈë ú™Ûðp–i¤M×Å‘4ÁT¡:B#(”Ÿ6²YBlPSHt;Þüó½3Úð9Ù{ƒtgoG*mÃ~çÞ!·ýÞïÈN<Í.•Œ¯Â^ ÷ßôsFuÙYo¸3)ý]îTðóË_;Däο_ÞûÞßúC^˜Ú Î¬Ê añ)éO…íu0U8†VêŸn|÷謸þvÌZ{÷=Á<zœcëŸî=¾˜VÉ ƒQÛ†WÈö)Ë¢ ka&•‹›HZ"/˜(A0` ¹?]]ãý;XÿØv¯ë:{²ö5ºã"‚4êè8å±mlúš‡81ÓÏ>¶9u¹}6 œR^¡A3ô±KÙ³ÉÖ(kÖpñ~ AJô»¹…ÆÐG©S³G À[G—F™Ñ`xÈ b¢©pS†Ä-Î!´Oª ÑÛøµ›&£An3®‡a…‹Ð•®¢¡ ¾:øuGA³ká0pG¤o€1C:Ø&¶Ü˲ªR°ä%“ÃêkŸÝ²,)É6«K[º¥ÑÒ1H7.zà¬H-‹ ©m+è¤"Ê9ņ¨ˆèYë¿õ_ þZ4¯J¸Áì׿j‹ÙbF%æ8Eõ¿öˆBª¤:C–ü{ŽV°/Ï|lqdâ0 ´ó`PÕ¿s×UM›~¶º®šŽ”tÞ©Áƒ3®Ô»VG4¡%µ|ªm1:7%]RízXUÿ]QÐ|˜o-¸Ú1ªU…-â{ŽgP@åBç=|oüýõ¥\$§¢Kњðh¨÷èáìö×cŸPÛИ&|Ž£¦î-ÞxY…÷¹ (Ñ=NUE–å°¨RÙ: ‘•”…(Éžsؤj¢Iââ‰õ§æN¨;×õ ¸§áбÑ&‰†•4,@+Ú³ËÆklwéÀÙWºSÿ¤SIC‡½„QgVqÏPrüà£Å½Õë¿éñº¦Q&ì…˜VÎ{HƒãhPÿ4H‡…°Q֌Ҳ{ëš*†-9 L–ÿÆÃâ©"y¹÷¨ÂÞã ïÃ<,t¢FEù×Zð¬}öЦ ºàÝáÔso:,EèEqê;hgFB]"Ç>füzœÆ¦*vˆóL—‚>´l™èDµlŽXH'2ºM?ÆmIA‹a?·‰†|˜(¨3hÏž¹Y³Á¶”øgçàDn80mdÐRµðþçMKÚˆî 8ñ!8ê‹€*¢E*øñ‡¤Lפá¶ÊâÆè#^o”Ö·ÁÀÝàd„È‘…Âɯm_·n¸=¶¼IŒøàEê\ø\5® .,}«!ðו¶‰ÆC·ãÑÈc^ÛâöØ}ÔI÷ü~XFjj¸þÑä õ?‘Ââ}ÆÁ¨@ËÃúÇ›ÿ{hз¸à.·Ýh”ê¿--hí¦ ®Û ½”ÿ\=o.Ù{õKn °æ}Іš`YÏ„´òuá¹5R4[ô{0K¨¸E"AãèC ŒHóòáe«¯u&µî°¸êa‚Æ·`Ö 3ØÕµ/¤á Ï[%Pçx0Ã-¸° @·ÆŸ¯»êIF½üyKFäü Þ ÈÕ<È´®'DÇCNÔˆÙ3xm2ÚT¡Pü¹¡ªÊ²6<*î!(s™ Ò «Ç*©¡€fa­Ó/ÑUËšâ…(ocÊûÙ΃]Ó‚!¥™÷,X¯\7<0TT.^ °IÂîÖwꃎ‹`a|mÁ¡´˜Ý­ÿ>MõŸ²uö©ƒ¶(²Çö%îØÏKòÚÑé«\ì€YûàîCÒ\ŽÕFWþFŽ$uöëc¯üCÐ7öŒª^õv ©Ã0i;™t¡AÝnì%¯‚ ÂqH‡ß(Îm†€Õ¡)aº\ºZsö r,1‘Eeè4å›dÚÐZ+·‡‰[w@ã IÚ žÂVIú¶N‹füßn¢!\T'âb”„²`!تra ”ÎÇÁ¦ÕïQ‚ª$qob4ÂÝn2@¼\+2w†ÓH7;hVzhp8;ÇCQe^ ¨ë@ëé£u ˆäѺN—$Û”ÛµWvé†SÜ`zï3Ä®ô@M5뢜CÜfXiìóƒƒ"þ½o£a‹$ˆDþæ´ÇÞêÜMH€&tc;õ¯ þ¥¨{Ü…èr з²þ9ºAý: q¢-%<nv¢Åà†F!z¢›r/ÄÀ©¡Ö¼ñD4Ð5s#Œ`˜1oñѲ޷öJ …/½IpäÃ)uˆòYc ;.ÙÂðAt3·§%¿VüCF™‹„Hœ gaƒ«4>Õ²ƒ©À)CDÅÍQ•‡iÕ !L›~‰úC]õ&”Ô’²¤q`ö% ¢Ë¬¯ÙÙ¨Ú8L—ìa•|/•‚ÖÆÁ¿¡im†ÜV÷º`0@Ú¯ÑÙ¹Êc2³¿^–œÇÇC#k4ø5‡ ©W=ZR›‹e¥÷á¡*Ä_pp{dŠõßϱþQ æ"…­5 `Îa2è#9Ä&te´¥§3ïö9ûsý÷:(°Cè¹`,RYŽ!³?ØJÂÄdý„|b嘙~\þ*S+$Sƒ*>­ÜWAV“}vŽ)ÓRÇÅ€™iAX‡íkeéס!YÀ,4kÍ0m¦†@¢éAhtu>>nÜŒƒ?ƒ³e½+`|œƒ!y¢÷yðÙ²Xi9jÔ¤&¥í ¶ S¸"ht 4T ƒ *Gt2È‘¤Ð¸ˆ$¤ªÒ-úQûü±`Ãm¢»û¶¸ÛãÛÆ_Dzmü[U æö¹FKýIeì`enH#ЛÆ< QñœŸ.¹AbÁ+ÒaºxZÔ×ps‚phÉ´QV­ƒñ¢MÂÈ9=PöwŽ<ÁaêYBEýs.×;¦þ¥¨9KýëNý·¥%D MÄÑ„ø3ø&ã¿Wÿ;Ö¶†—¶åP‹¡>5ºÃÍ­iw{ê;í€ íän`°¦§ƒ“Nˆ›öÒ>–)vZ;@³êÈ Éš›[{|qBêoÞ‘Æ&,ÁÁ´‘šŸ—_+•¾€èC)r•‚[ÞÈ[ë@ƒá1]T"µö‹µ–…¡ChÒ`ùÃÏÑGDj=±ÕC\Øàã5²cÚèƒ"i˜®Ð7nˆùgp¶T0~¨‡‡™z/§oË(¯M¿4)©X\ÿ•( 0HR õíØi3­Ð>ÑfŒÖa£cÔöÑ)K¯²ú÷D\HT?+Õ?ɼ'@ªZËç$3g…‚Z•¡D5à•õO¹>­Åy3 ·'£?½çmÞ·t¿üM‚·5jƒB©þÁLÁ²„ìì ï‹hªM¼7^øa©¶=ß(:6e6Zò)#ÑäzÑ·ÛšL«—jÚD›½e _kñ×Úc*Ó‡°i@Æ™Á‡¿e™C†)^qÓRWƒ‡|ƒ;Q±È²†…µGGG³ˆ O!†$„µÂE¾a¡ðÿþèè(ñ°Ù„Ày½¨ëNxµ­ô” n¦Ä!ͽ§¸@G z0Æ`Ä‘-™‘êˆ×½ÛRÓêþþ½G7?Л1=( [Ð\7ÙêÍ=°&‡=š ¬Å€´ö,åØénØ zf\óVÿ£¨ÿq–úGÍÑÜÝ¿ú_ þÅoT’øÑ€šèƒ”…‚:¥Þsý‡¡Šëÿ<¦&1²ÄÑÑ–k…èNBŠ)l4ÕE¡S 6QÅ| öˆúDŸÂ`ÓGí–tZãÄZ±¯iK Ô´=§¸ÐHñFTÂæ“ɯ ÖÞ7¿£>rzk¦(’epW­s IDAT96×¸Ø ùAUóËf½Ðo™±ülFýxXe*S0Ï‘iú‚ƒ·»-;è¬È­/lÿa ªPJÕ?ñö6ñNBÖTÃVµÊþé…¥ÞÜòæØ¶÷a‹$qëÜ Oƒ›K¼·F7ËÃsˆâcYXä¦Ûp}Òöü_ÿ~@^öÂgç Žihsœ@Øë³pàѦrí3Ÿ*¯ÿû?pÐÃH €Av¸…Jbd‘:[ŽèFOÃ9lTFFÍžtÅåróõ§äÚg>µ¼¡MÁ¹º@àhË›OŸ’'=áòÌ+ƦªÍ†' A7‚Ä@§‚³ Á”J¢VØ¢v'Ѱ¨q !ÐE Ù,²aÈ© ³iÝ´.:¯Ù´8ëf€àävHú¶ ̇þÐTϼMÿVÁÄÈ·oªÁU­ª×™›Ý9Ö§ú—ªþ¡Î׿/!F8ømHk˜¸mgÎѤG¢àÕ†,Õóq®ÂÆòÕšjL>Ô¿a„FJÆÔòäþE4U¼ù$d´Ô¸V´—Á´×,,pkŽg`8KEK jr*W©9TïЩd$„ª¢Tñ…g¨›àGCJE‘ ÙoCbªh=PÀPu0ÓÑp@ê°Ýw®È¨uxéŒ (רéÍÆ”¬w(¢v-’‡”Ȥ{wOCù)˜;PìÓ™*ȉ.d}¤ðr^Æáß;±j ùñ…hVà4†âzaZÕC/P- yy˜œ'éÜñÏ–Š}d[KZÛ¬;ÚÓŽ©ÿ‚rƔÄÆ)Qt|NÁåŽô6L ¬hnŽ&¹>I{Åõß ?õÓ;/¼$ T{Ú¦'òIŵ‚×*éZA.¢|ϯ4Tp0/¼îíù_ðîpÜÐ`È'Nª†Â°í37 \|¸1nœG `ËM%Q¯,Ï(¤¯È=Gkd< ßøS/—K¿ò>ÜýÉûäúÂP|܇†Æ³e<˜kErÃ~—|à®?’ßt‹Œ1äD[DÉ'X› ;ÔM¸ÿóÿàûå×ÿ·ÿKž|åãäŸúŒÜø¦[äûžûíòßú$ù_Þö»rÓÛo ¾þ­µèZV˜…£DëL³à6ôà½wÞ-×¼òtA †æW%§¦Âk8 ßø^.?õ†7ËKÿÎwÈo¿ûrÓoßQ(¢^4ÉN*ó:ÉF‹µwÏ”™0òWÂipa(d§¸W׿Uƒšm±ÛiwšóÆ„Ø^¾‘Ó Âåž$¾×(}Gëz–Útf ¡Ãf·«zp>ì= fg«χ$ò¸úv³³þgC÷Àê߯iøæžO¨D74T ¤û©œ7W‘l'Ñ&X£Ý°ù×Ù|£(5Ü•ÜÒ$neѵ,Q!#oA‡gH¹HðˆÞjÖS„tvD™4¢P•^_CØÜz(DW7i½ò<’¶ñÛx\N †‘&ôzH¢"‡³6ô<𦜲=Û}sl3zäœ…Ï ß-0znŒ#˜â• y`Šç1|z¦ Íb$ÄkD#êÌQíyÛ– †h)2h¤ˆbú °„àÀBè~#:Õ1‚áT^p`«hv!×Pµ>}–ér„ÌU:lá Öø®õ´JéÒÈCkð 9Û5™ÐÌ aël΂ÄA„‘È4˜ÎìuGIƒÄàTÎʫ̰\SäHntx³ðxc3¸ó›Éâ–õ:F4 â`dgNdiL½.::£!¬‰Žt8øšÃäsÓΓ9¦°£(Ì\²xŽîVøõ.Ї­ŒmÇmCŽº »˜ZË”€×Ýt‹<çºëåšS§EEägü{ÊaP‹ °ß8£¢OJ‘‰ÈÑê 8í9+?{¸yàëX”­5yÂÉËä1_ýU%D™(rd±®EÌõÄíBº!¶ÌãåM ¢ˆÚÔ—°Ûq—â4r§bŒÎ¡Ü¨af|€´IÓ ž¾Œ@órä8ûƒ‹æH“}­]kMÕÍðÆB×v¶{µ’56 1Ô¿—é›Èõí ”Äfl ÍÄèÇÔ¿oÃz´ææúªÌ)P¬çÞ}HÙ«=¦þ ÑiKsÄ­r†bÎ*™;è6ïÐ/=ç«!¶š¾!CF‹k´ÝFŠ—][•h™®ŠÇ(iV^ãèÞ5$»tÁRfºUÕ?Ó^­ö„Pþ]ËD«úˆyfLù‚%"ÜHóª Ü’;*×f2“@ê1Õ}¥Ca¤õ ¡á‡ÿ;rr„ãϕ߷D]$(Q9Hµ°[gç´2ˆ`QV!`©ÅÅÚ º#Ôx «“™CbDÐïiˆƒÏ.}n*Ù:ž­²qCïW@Já{|©*’hkA¼öÔ#] ÓjØ©ê#Pï8ôX" žçÂïmǘ´EhŒ„´ÆŠQ2FÝ‹0ÉHHPú(Ñ® Û©þ¥¨ÿÖâàß4PþÙAi±ÈÌ£raP-Xð^FÙ€æBÜ—9 ¦D‚Ì"^¡×u®Pßbhœ~ú?¼~.x$é9ˆŠ :rh:5á›?9˜QÚ¬q; °Æ´MÌ à)ùp#ƒ0ÕÙ8ï›)BlBÔÓžíçÞúÁ»äÊË¿ÖæŸûñï•oþúÇŠ¨È|ô^ù±Ÿù%?ÿÆÖ„üõ+'¯yÅîÕúëï|·Üø«·ÈÏÿ·ß/'/{´|ó×?Vž÷íO‘S7þмïλå†ýnÿ™·ä^ù±Ÿý×NùåŸüá25†¼ë}öªyÁ3ž"/{áÕrÍu§ç!Øs©ªü÷?ø|yÚ·\)""/»öjyþÓŸ"?ôOÞ Ïøß(ÿÅs¾MN^ö(ùóÿïHÞò¼G^û¦·Ê Ÿy•üð ž-·ßu¯<á/_&·}è£ò‰O}Fžú×¾A>óÙ?óçzÍ+OËëÿþÈÉË-""ל: ?ýŠÇÉ?{ùKÿ­"7Ÿ>%ÿâÌ[å‡_p  ÞóÉûä¥?ý§Îá{uæ·Ékíñ„†*<•¶Kæ×o[/·Ð‘Eçõfˆ|ÕGT"Í6l@›n¤p=E4IËF—CËl@^G—64 6X-èþ'HÁk˜³DärÅ , V]7„¨¡SÐäÖ^SNC`*n`)E{ôáa«\ÿãÔ?NrLý g㌉ô¤ù6Eími[¨asúæ ÍF@ÊÏå.™œ‹ A'–ÅÍ6Jѱ\tCÃäµT›ÚÑ£RƒƒHê§âÁ£©´úÑ‘†.㔇æ[´v< ƒq Æ ŒBŒ1È­0º_z‹d*S‘º& tŠÜöDw­²ÅÛæßcFƒå°Záa„³ÊØÂ¹¯=ˆÀpˆté%åŒmÂy»Ÿ¨V`47¦* Üø8+ÔtT Š!¸&jbÍ…Ôž+. ÓçhÃq—ê¹—ï$ÙàÃõgmjxðþVÒEêa<ª Ö»AÚ¤€ÔJ¾¦9?ç¸eRê*znÒ%š DÔd”(KkÍ?ï”}ËY%¦áÓê¡¢°¥8ƒFÈfý˜úo­d,°] Åê”s•‰8ðÔÃOœ »MNÒ°8ïm¸ØŸ™A"2z9¼2Rmõ‚×"G…;\ú€ÇoˆQR˜F²7ݶz†š:rÁÂ×P"t€bºÐ¾¹]Qs€YD“Çý˜VÞ¡ajºC•€¨ZlσÓPÛÖ{’\K>þ—¯íïˆrÆ(L•Iä›&ªÿNõ[ؽƒDZ¼KÐu°²F o{~ãê߆¹±Wÿ°Isýžé†,g‚t‚x36—¸¿dêÓÄËlõíÆn¨Ûö|pû›¬z5P ŽG>„3ç½@–ÓÍiI¤¬nò9[ØÐT5k ´/ꀆxeK!Â8%̓Rn ެ·ÀáÉ: ynøþUÈYBTC2_$j[Ò’ó¡:@ËÈ ŒOBßX“7ÿŸÏž½ÙØ,Õ¿ `r Jé®ÈÈÞíZb½O÷¸ãD©ÝÁ˜=êý÷  ¢¤Ž¶Ð@óZ–¶5ž3ÀÔ„ÙAO#3u5¸oÉ>±¶Æ6¯Q|vø;$2-c]ìË^xµÜ|ú”üæõ¯’7¿óÝòº›Þ*kïòÄÇŸ”Þøo}°zÛ­ õq_— ÍŸ|ý›å½¾[†ŒƒþFä ð§÷²©Ê·|Ãcå†7þ[ÿûß~÷È•ûZyÒ—ËÉË-¯þ…3^¿ø›ÿ»¿/gÞq›\óÊÓ‰Ç75k8ÞsÇÇäOþô³""òÁ}\Þ~Û忼æÛåÒK!¯»émòñO}Z^ýó7‰¨È‹ŸõT¿ûÝO|ê3þyÝóÉû䵿v‹ˆˆ|è®?’{>yŸÜôö[eŒ!w|ìãré%ŸnG>VùÉ7œ9l×UäöÞ+Où«ETU^ýú›ä½wÞ-½w9óŽÛDdÈ2qÝf´Õ)åL•@×¥>F¢Àa6T§€Ýpà¸hm‘[´sïó`óA ¨C+:@ )5LÊT¸>fH£!9•U®ë“@àY˜½e‡Ò²ÑÜÐR>dÁÅõ?Ž©£Ïy€)¾ÏTÿ1ÅÔ©¢þõÖ¿VõÜù‚z¸8¤a° Ãày’µ ‘Œut. 7{l¦˜Ÿ›öŠzYiÂÏ(èrH£bÊ*jYsa”1¦ÐùF™Æ„††¦5¥vPP±-æ9½GúˆïYXõN%ÓÆRx*:iF®J Sá Šƒ "S‡ë´@š&½¥ˆÜLƒž6såÆÈTBrtKZ$Ò^¥Ñ©0”Ë´ÀÒ0Å4+>ïÙlÇ7ÉÃþ^"&hƒ]P= Sã`Å_—ŒŽ1“ªD´&Ø9ïÔ?S ÙIn>xÎõOCLe p\­ÇBFB>!=Fø¹”ÿslýÓsÙ¼þ«Ç(ô<ŒxW¡²áÞ4å¦UyoÇѨ«÷)ÒAëN ]mSSjA§óž;Ù-Ðørý‹Ë3Æ a ÷ÿ=€R·´ÅOvÖ:tá#8E¥íÒp¸±›樅̛¹@W¶(f“0Ùãm4‘£!‹Sëæ´›sÜåÉÓ)Q¡×y«¼ém¿+?÷Êï•g=å›ä×Þ~«¨¨\zÉ#€ºuøuûGï-·cÿæ¿ByÉÃ#?Ùx»c ÖÓÏÜ(qW^þµrÏ'ï›êÀÉ>ZDº|{-æ –ÑñáÏO8y™ˆˆü³W¼ÄpO¿àíýüÌgÿ,ñž9<. §58¿ñö+AÇÛ÷½ñ½\.ýªG@NÌW´Ð üp¤ílhR îVÓþZ*·š¹ƒ¾u\«oM‡œX–`"áš¡1?_>àníÁÈB‡nÖËè¸%Žô=«£‡-'¶ÜªˆjY}´íµ-`9óŒÄÑZÆE‚AÈbpðý©}€õ ªÊnÖ­ëôù†/²x#´WÿH¡³÷KU7÷·vpú[æï3 s¶£àƒ?_=¼œ—Ò J¦¦+Øúê¿·¥írÉEzÚqBv´”vª[‹"eܪý¸m¹«—Æ% ¡Ã@@Á‡Él@%ç߈DÊB»%nx°™; 22LÌ&iÉDÂè}•Ãld1t׺ tgM‰ýcšÑ°¶‹_Chª‰ª‡áŸÉh Kب£h}†—(bH,Œ Œr™‚:Ù4G"¥®¯Û5Ž×ȳcðsa]QДh¤Qe­À+Íf¸¿gdßÜnðy·0Và ŸŸ@žñfØØàiϽ¢®UƒSÚŠ`O¦Ü¥óƒÐ•€˜Àõ‚´SûóƒªÿQÔ¿SÿZÔ?±høt\ýo×FEï«ö¬ÿˆýìü=[qû͘»ç{|žK>àXìDp`–®£m8¢åÏãR5Ýð’Ì.›¸ô¹8ÜáF=Å"Íÿl›Í>ÆŽ]æü¹qèÁ! 7÷fYì?Ok`Æ0Tqd#®ê‚7ÆíCýñŸù%yËõ§äÚg\%gÞq›üûÏþ™¼ún’÷ýá=áçËi+ü§¯“×Ýtˆl41±yšõg>û9ùÉ_8#ï½óžÐX=éŠËå‘_ùwú9ù˜Gm!y¹;D·æwn®gîTóÚëþø}Ÿ‘Wýó_‘÷äÞ0X>ÿéONƒ ¾þOáðI¯à7jpäBšÎͧ¯“×ÝôV9óöÛü¿ý×™!´nÚÞ¤6m)Œ<ÀÅ÷ìpx,‡qÐæÌÃqÒÔ´<|Çáb+9ÕÉ(¢Ç¦!¼®í³³ë|øÛ†—u퀄F«GS—ƒnÉào ‹µmÒ‰e ?'4õCc¾€Œ/xýKÈì8¼3a0ƒú—¢þ?ï®u0LÍaqÖ¿ýûbæ ­Aã!7m«ˆ&»eúgÿ B*1¤²¢›p3Àa€¡¹“‡lÚ.f›tËëJM®ÌÁ=PÜ dVâš"¤Ø&Ó›;ÝÉi³iEíMjZmõ[R¸¶ó(4hB ÚÊÐxÓ0ÈÖÚÖ„¶ òŸñuap&Óñð3 ÈÜ J" ¶‘ú©=îÜh6ýCsƒ¯q@Ò¡y¨(¨a‰&Fa¢œ•®ÈŸÃûe` ~Î6|c6Ñèù3õf´IÐ’„!Z±h0b[qÒØÆ[È„l¹ù½aDcÏŽï§¥ÃÈ®x¡þefôaœ?^åÇ‹™ðX…N †×?^Óä‡?‡Ýký°ÐŸV”=¤mqèjüø>NÈ^ZÔËdj”'qˆKC{!ÓñvëŸú h›÷ÃÃ{I¬»ï Ñ´áSXF.Yø»Ä›yLøg_ŠÙó¹Ð‡ ƒÎ èÐ6„Üœ†¤ÇÖ¦. ¨ì×BÎqvX˜‹×LªŽÛk§Öy  ¤žTîYÖdþú;o“ïýÛß&""wÿñ}òCÏ{5G‡H©¼ãîOHC^ví³#º¡"—}Í#ýqîùä§ä‡ž÷Œ;4dÈûÿð0½ú¥/؆‘g]õMþB^øÌ«äæNÁÔ?ß3{Wл?t—ˆˆ<ùÊÇÉ“žp¹¼ém·ÊŸþóòã/ùÛòü§?Y^ðO‘úò—Tð_ÜÞ‰v(è9F‘ÕÀ¹Cx­¶…øÃ{ÿXDE^ñ¢«ý̵¤ Än‘Jv BJnºO,‹ç7!%Ñ!Ñ’.ÀÖáx½–iÜ}¸à¸²“ !bð5ÁÌÃ_oÌ•BWÁé6ë±5õ…@7;ûž¥µªe©ÜøýEMö>¾hõŸœã þµÍL¡ªþÕ¿Pý Ö‹®tA/Ô -/{‚vJ<åxudàB}Ó,áÙ´ ´¸¨DÌ2÷]‹×l‘!h3ðÞå,(-ÑLpršœR€ãÆ1g÷ND—pø OÑd"²fù-¨«¨²Ò‚ƒV‘†^nêÙá !Öý„m¯d>6HÚT3Dê]Ô¯1˜Úèo! ÝõÆÎpŽˆh¹ÐF<Ð(%ŸÝ ‘ª"‡vÎ\wñ##Š@‰E(ªd^' ›P¢,;q×Ɉ6ÙÝk¯‘ATÀ¼ÎìIõ_äþ0Õð¸!]ÿ„,&¡m}J~û¶8"o¶¶êöÞ`ñ;ÿ¹ç“÷ÉÓžx¥¼üÚ«åÿ¾ïOäô/¿Eþ“~Á³åûŸûtyø_zXº¬nç\$‹‡Cqvšœñà þz×ûîúò—ÈͧOÉ•—]æÅ6JcáñCwvØ›6'Ne6Á×¹ÆEˆ-?ÞØ­%“#ÒwÐîZ¦²#:cÞlˆ™9A³qäûvñg Î­/äTk~/0ÔºÙ@Ó/hý·5€e8ÚcT5Dk¯þu§þu£ ë k”üïûS &,HÏ›Ûó€m›9%N»=5kZùÆ4, —‘éóF>P^Í‹\¦&Ù×bøg¥Y“úX˜×heU^Ø-ç} mÆGzA49@D9XJøþ2…‰é]éýÖlîP9Þq㌺»*g„7ï•}phB° ïÙò›mŽùšÂ­7b¥ùÑ`p u{ê¦Ù2A<ƒ5ÝRT!RÜì'$¢9•L>”>çÿŽQ·Šú,Çé{ƒ©‰DÄAÆÒ—ê„P F‰“®T³ÁGB”I¯SÙdc~ÞÕPÀ9x{ }ÕW¦úgGÏFŒ’3¬ͯi:Cð3 r[äöugýОA~ÍqµQ9Þ)Üw‚÷9gŒ-Êɘg’Ô=¯ î}PAï5¦ ƒÉ·"¬fV^˜¿žøyü?ÿî]I€t³x#•@I9Kô;6JÖ$YÃb”žæA‚ÑEËžËâT!¤¼]Ñéç$6™R¿¼ûÍ; íy½äÌM%zÙ|n!iF–@Ñ‘2CE7'1C¼ÆrÓ0á×Iåq+ó65R­EýQÚ„ LU´.oA„ˆL€›ÑbSµD‘Ê-,åYT48C®·Ó¢¶Ão}CFzD"$êŒìùvÐÕD´ V{TÃ멲Òܨ‹sˆ9@{y9Ñ)ŠvÏ•™.3|=Û°ÛÒá>¯ü¾/÷úÇÀÖs©ÖÉnöDQÿp½ü«yîƒ:cà¿þïB bP«i pÛù¹O*kLß@M%0·Aé ÞüT¶±E®¢¬¸¥ÇÖÐk¤´ra…b%–Þ„HA_*è6xö8°ÒC +J ëaШ&dŸ ¢WÑÙ¶÷ï»y9õ®Ø”WAœ¡ñ¯š;%…(¼f|ß™ 9òc!:Xi¨ª¡ËU“>'PÿZ élcôo#~¼™è]RØ›÷L¥²ç±W3A³CˆåžF'ÐG+z ¹voÚE $ ý'{&?•±×>g¼>P·‚hóÙYÓÓÇn€»$“œú¹òï©þG¼¿ßïúßy|ëÛPóãN§ü:‹¥FEMœ=×÷P¼;³tôØOðó®l¸ñµà÷Ùãÿ¥G~49,oK*$½G_ý¼¿wá#Aƒ<ÌÇ€„sEÁ¹xSâö½Ô¼Ç¿‹t4Á²4ùüÑQÈ$iîý>݆ÙÊ–Û&r6,ÓB˜†!h‰j!xRH ¤.Ç:ÆÈÞ£–ì[ئã÷Û°3í@çóF„'¡,c¾vËA.uóŸ1·ÁÎI•ô$6ØUbIÝÉïAظoN{ LªPH¾®Öé#nƒ/$¼ãdè »ZuG@úÜ ot¤´ ^QÔ¡±`#üîeYæ¶´ X!ûen¤£¥e¸Ö6ΰ}Ö~-Qš96ÿöYµ 9š€Õ×tpœ© @_¬ú øUõ¯Eý7¨ Àó÷êßj`ú’± ‰DC,(sF<Ÿ¡©FSp—ç­Aï¶r=SŸây‘Ê“gÒ.£„´m$‹c•’Êh?ß¶ö™_Øoîšq2 .]*Ñvë—he¥U‡Í|U6 IDATå‚BSå…|Õ$ÇfÙ1΅Ê[ÓÙg²òöáHÎNA{ôDëIƒ$¢XŠÌ1$êl‚Nõ9Û{ÊïÓ$*ÃײnzPÕê¨yM¢È–Û®Ùjs!–”ï*cˆS£bã ‡?¿Â‚ºÒ4‰€ÁƒNTÕôŠ~F´HsQå{t0T"{é½Ì¯Ýú—ú~Ж!eöN9Xí@é£0 à> BÆRߢq *ë¿@vvi²Ègsû9 4ù½B=ê„‘mû!¶]¤N^nÏﯵü:€p\¢68"´v]6 ÛîcKº.üœ ÚêúVÞ,³©Î[1e~ŒHÍÑÀÕVȲ˜8g«7k °ó…œ“Þ‡#D¾Q¦ °á¶l{žÜÔaó8dÌÇ,KäzºvrZ̼!dªHuñNz‹É½)¦F™›[{޶6hÙó[ @§E‚(Ï^CÚ¨ŸÖ v„ ù$Û¡´®kIéTØ6&þ­B£™ –á"!ÇÉÐ,4:<ÉTpàŽ’Ä”ä3•®¹t¾tØM½ÏÈ ‰E~Ñ~´Ô6ê\KÔš‰ Ùuµöž—/dýO]ÄðAÆê_à:–¢þ;ÕÈ9Úê_Û!¶ma©L¡2MnêoŠúßò6ü9‡){úàmŸ1‹†©bŒö8ŧE êEX—Ãg:<1–1}”h Ø<Û²„©S8¥@I¢%yŠ—,¬ÿ0<¨$MSE£ÃaéKÁ b€Ísá f–ÛÜ@Pöh83‚ó\aPÎÌÊI²`©­”O¢‰ðk¦Òä†=‰å‰n†ÈÒÏÂÀ8]" ‡ü¢ÆË…@ÄL  Í2„³"ðsöϪÕ9U~oêÙY¯Òp *9éº=Ùº3Újºgê[6sCŽ´0olÛ~S¿GÝâáfÏ- ZÄ™rŠHõ²¿«¬¯Ù鯬[öòÇõÃfÃ5ݥхú§EïqüÜUƒEk-8¦ŸÎs¥Þ(fªf*jÖ‘9Ñ «qD&B˜3ø|“1G GŸÏ]bêtp¾ X”–z”ÅÅÓ#pÙýbnMðk"E ÃÐ.Ûš¤ŒYSÏ”±°]õdy ¹"þ3 ºO$¢B†¶Øs±­1:baìÛô±?\† w+^l€C(ç ¬WÊÀˆÈ4í}¢f½×J…œºèÇ™2EBkÏŠhm¿ÓƒƉ'’£ò¦1Å|Y9::ÊÜÚ‘JÄf¥™¸~§pî ‡>¢@’ƒÐ–J©ó°sËâÐ@oH Bžµa(`Ås_é&‚‘çÜÀ†Æ†d„ÉÍŒc¡Ï¬©–z®/Tý QÔÔ¿œ¥þ[›HÏ^ýËVÿrõï¹1#¬ÿ¶™´tЗE“Ý€ÕfA/n31LÒo˜•†mD½ºO…æ•r{B#7¤Îé@þ·æ >ëÐNxY–䍿T¯œhßš·ß0ôÚŠ*ظúùÍ4£ešûÈa¨pþT6ƨ}qdÝu®=5j< p2;#9¸Éæ [DTP«ˆO@#Ƨ_$¡÷{Y<öùû4$ëdd?p4ÑiFJ<ƒy‚z¦p?߬Èý^¹B‰rÉHæàS.Bö•d6„È–Nùþˆ± •ÀÎ)‹l6ÈO•…êìdƒ©fm׋gçnýÓÐfL Öù‚–uæ[ ÈvaZ>¢»dÕÃ7ò¾4Â{TUÿ:Yøf ×°{ý›óAªÆ×Ve•ÁªÅÀ–ê¿`FT r@ 1 ZαþѤ¥Ÿ¥þû(s²RQÐØ…Ž®Ó4(¶Âv]$¡_½w§\Oín^Öñó ›{KÍÞ gÚâs¬òœÐå•`c+•÷)‰×“׉İXF²/ø!èIJLQÿ¦Ã‰Ô‹É‰eªº9Ùð‚©µª‹”Ý ö·öáu²\ZóÿW0^ æQ“€:"œ~­Q:PŠfC·ûA¬c 6HKÑ™]ˆG뺹ll -lÀu»è1ìÒ·¥%j‘¿žMèßløqëçæ7{ø¿ëo¶÷\H§ƒ‡s±Á”*ÅÃ$ñ”1ñf³ ‘!ßöˆ>W9âM3l|TrSÍ¢`¸e±fkñ>º††I4YÝ"…7wê(Èx˜‚fŸgm6z4Àɤ‘MÈzY¤Î}©êŸd¬§Qýëý¨Ù†áVÔ¿ñ¸Wªÿ€ØõóƒÙ ­ƒQ‰ D 6£öÿ ·CZ•‚.Št=’†›å0HÉÌ YCR7w ùA2ÍË„°E.l©… ~NÐø jŽyع).z,xZ†@ÌZZ\°¤Add<èdúNРj:;Òë"bZVƒ £*h16‡áLÕhEŒz!×®Ð5ÉïSyÒùÈMuƒ›"Œèbô>ìzØsÐ)æÔ¤Åàv¿B*g°°®š}[úŒˆðUtF ~­¨Öö>›Æ—ζ(ì³µ±´”©^¯j‰<¢m2_—áþÜG‰îð0þMkÉë_ö‹@aÓœ ´‹ÞìÕ?GH¤ÿóë õO P¶ófç7rdžø…åå(Q`ì!Q«š‡(I?³\¸ñ²–¨8 !unÙhÙ‡˜ cWä÷vôÃ=Ñnu\øCкö0(ô-H²ÓfF$™Øæøhƒ8û60%›˜%ØæÆ/£û Î-ä—ÂkŸŽrøØ|Üá±l«í =@¶Y>pbY|èh\´›ŽÉÞlÌžQ£g¬Üa˜ðMHhÿnÍN¥€³„¤7[¤%±ªd_yäY'/fïàaÃ[]¥íDÉõŒüTk¼Á„FbÌí!>öºmy¹ø€w»k•’R€¢ä6½ ðáëm@Çh²³í«ìK6ÈJ¤›éE[ÿ^'Eý§ ´„†p<<ƒÐ ‡C ŽŽÖeõ(£¦<ü‰ô¬@ÙIZ–ÊÂ9 ÔlWVºÜô'ÔM vl‡ý¿Ušj¤ïX¯›|vº+C?AhÏC /QX#Ö T£@Û\+!ºC3Y”¯QÀ÷'^Û„hË(ìi³¨:}M«T®ïju¹w5ÃÚWIØÞ+±8$äHI°h#»jãmÏÙ0cHÎ_jZÓ0G¬ ÔD âVfwID}8 uµÆ&ÀÚÀσ¯¡Pÿ}¤Á…ù¤ïÛ¡µ²Ž¨júõªç +mæa |MAǪl»µÕ%2´ƒp…úçMCTMý®)¡~Ϥ3'–FIáK÷Ö‘ú̳™P8Û îù!°Ç<<ë›QC>dì2È. $h&ÒOw&ÖË ç’¡,´‘9v³] ­}n±Ý¡ÉAnBÌÎÑ‚(75G¢Š¿ÇMˆä%¸ÝÀkdÆêVÉhy¨)†uný›ª,eCBÊàÍ›Hv3#w(Uh¢ûø¶¹ò…„ h;m¯`·•zK•tÚfUö«áæ©’‰ÛíòðÖxÐîqµM¬Žƒ‡ðÙMÈnp^h®9°Ù >ss«‹ô(¦\hl¶XðéSD¿TÒÍ^ª›3øÀ?.êú—³Ô¿$‡#¨ÿóH‹ ÔÏmðœÎ€S+äM¹§ÙuÇŽl{¡{œ™ÃhE5$¸ ñÈÂðDC‘Zã’B)%k¿inäeAÌ…Ùj¢v!ç£Ñr'$%×'Ѥ0 ’ëašӘ餚΂´$!ô&|®²?œpî7è{._A¸?hÐÑl"mûGútACý‡F¬Õ‚3< ‡Ôt™ßÔ³¶ \Q›ËÜ£‘¯IÖ²ákdêÜÂ"‚ôX ¡ƒ³|/LÕÿ^ê ¬™Qdôì-E ŠCG… „PÕVÔáöÆÏ—ÿ !a`ÔÄ®;Ëa¦Ò1¢µ·Ä@ƒ Ö<;3¥åçî}Àæü4¼[o&¦$‰ÄÙЛ¸ ܯ Se)ôa`ƒ{>Û²[ˆ·±“ú@#°u‡5ò…&hÈæ´„pµŒhqM™.ÁÇ|kžÌY ©;øA¡T¡)éI¼?¸¶…>"lh Öº9Ù‹læzh"âh?‡ÔûšÞàç[šÂ*;5Ý’on©È€®åt¦ÞÇø,ޫ¬Dk>n¢„‘P¤$~0‡ÿq“"¿HØö¤âíõŸ8q"ßÌZÞö¦ ¡$¾å@Ô†w±I×Y‹Nê`7J»1W›.þ<*´ ‘!vß)—0ƒ(J‡o0-·E&8^¤õ/çXÿ–ª}pÅi¡æô’©eä@yc 73 iJ\Z¤’WõY®‹ÔöÁ,VgÊ ´C$9Ùñϯ¬x18Ðoè2Ö¬É(Ï7•:Е UF:!Uz Då”¶¦%ÒÍ”±jøa É^߯Y«Ä¬ás,tKœË„¦þ9“¹#~ø3Ããdµ@^v̪ûcþ`ØÄ°U4Þa 6ÛáAØ]a¨aËvt§ âvº¯1âP"„ÕRPHSÄtF``tpïb½.a%z¹7¶®L<12Šû]u.ì±ö7X(ηcëÿF£V!®…wÔèöpm6¯ß[ööo£B§€ž†¹?­í#l®%7 H73íQÔ.æú÷£þÇVÿ½¨ÿóÙ°cN†ˆUÃLµ%uìyƒ“¥Ø—×`UÿªÑakÄm?ÒÉxÐÁk­lü´nÚ«Ìà ûHFÉ^»íÄ…¢Z~^ozŠæ9¢³RÅR]Íz©?Úq[Kh.  Wiã_d0ù}EóÀR-Ú}”!d…î™^‹´<2ûóp )-Á+ëpÖ !› <4§W ýõqLýo·hÿôêjhäx‹ôïšWRCöµeZ#å‰C& ‰q2r-àc j³æÊÿqËèæl’¨Ïd n4VÖCŽE­â<…Ïu4fÖU Wªñq§Ò¥%‰¤EÝ®£µ¯~†á5è¯ïB‚: ¦Z‹VÐtðmaÞŽ*3à½ë½¦Öa§°@lûÛ:¤øµÂýžßÖcÓ# àns‘ A ¼XüµnÉé˜Vonom«k¹!€QFB»ÛöÍŸu ¡uD:¥óZÂnõEñmÌÍáš©røg¤“!R†"ðp‘AÒtÿ†f¶i=$±ã lñ4A@—X@—‚š¡À]…]\Òæ…ìo‘6 >~ë6´q0bÒlpóºÝ¤9å:=7N7'š•ý½YZÛðbÈMr­Ó™÷Ã4¤=ìĉ¹5™óœå¤¸1mŸM8LǾsÒýÖÑu ¸s)Q$†ù2Â…¢žõúGÚf _mi—23úáªD½nÂAFå—õŒHKíDÄOÿÙ*)P¯ynü<÷CãÏ*›Ý‘VB <#ŽdÍŒ4«PÓ,ô&ª ]/N}Yßd¯aÙΘ M’êŠ*›‚Q *-kƒÐ¤€—f†Êf=|¶3¬×5Šé•–jè˜øÊêX5£Y;h–¹Ägg@2p®LBà¹1Óáþ_'Ÿ¼óʤ@i£~ºvµ¾‡átm<Ìî I•+`Ååú/‚GòƒJW:ÞêëÁ†#/B9ö„êáðµKµÛAS°)P,¾GÁ.š† DPË,®ÿBnQ……Ëf€±(¨rJEÌRŠi‰Cæ FÏ(Ù|^üïeGKÇpÿï«;´úrqõï1rü:X/Ǧ$  ¶–%l‹ñƒÁÆ¢ÁĺlY+'–ù½½çFofÓ;Fœ¸Cf‰¶ …Mn¨-['B†ñõ­½g ËMɺ€`7¨…¾d Y–EÖu  LY0áËRŠ“«í¶Ìf:ÑcdˆŽ˜œ >’7ÜÜ鿀ï7íìhS…Ãz52ø÷ÁO#Ô¯#š?¬½‡a©3mi"}Ûòæ×_H¥§MâѺÊÖ3dÐ&uíÁª››XkúªCÚ·VÛÖ¹¢00- ¯ž*7°ä¶D´’p3AËWГØÏü‹ú߯ÿ´÷¾¥¢w¿[›³ãz´T©1ñkBùìi‘öBVöC£M=¢"x´d«3¶­FÛ|­6ÑøsÂf¾ÆÂB£ié䦢24R_¼ù²Å5ÖârÀú(†åæÝ7ƒf† E¢!6ÙŒƒ Þß÷&%=°iAz¯µ6¢"ÿî·Þ"Oø†¯ÏçSò'òW=-YKDFØÚš)TÁÏ”åøœÞðû² Þc¤R3%“M)‚cZ¥ÃRŠ³Š˜*]rV½>»®¾í?ú›òë¿ò‹©†ïüÈGå[¯þÎ<0-œ¨«¡ ï#ÑAí}á!1 ;0t›¿Ç¾B{Rý¨C8·˜–.Rþ;ƒÍ9ÕÿN˜kɸ5AÈtA“(Õú߉Ù $ &4;ƒÐ )Ÿ7ÐÃóšÚßšr•©¨Áp®£5p¿m-R¿q1Èdï#QäÚ—ò¼¶kÛÑ¡óÞ{QÑáVO½Åé6oloñ¿m«k|{Ûâ6Ê&À‹ïQI´Šmæ ‚6k\üð–ÍE ÚÖ„…í2Ð|¬Çs!4ÿ½ÒSÁ’N¥o[.,Üžø°¤-'ÀóA ‡ç y+>íƒ9îþýùÓVÜeÚòŒÅ¯yÞˆ„!úã4ÊKÀfŽ^¤¾_ÞOÚÚÿOÞÛÇo^ŽyãÇy^×`÷õSÃÆ¢ÜR(,J!íZeiQ„ŸfWeÓ"n¿Õƒ2•ݵä!TîíA÷!j…ÊÞ„Ò–MÅŒ±[¥FÝ÷‹šë:Ïß×uœçûxÇçš©{›Ùzõšéû½>OÇçsçû ÐñýÈÞÎ3™¢ÕÒYs^›së£Pé¬çAý–…¶³ÕûÊP(qö’R{<ÝpZ½¾›LÕ±©ž(r&´õ_©þë¼þëzÔ{ïoAýWªÿœ¬þghä^SâæÚ¦(Á‘)Î-á•VÖ:$†5n­bœÒeÓÜ1;‡ô'†ÒY)7ˆ$Î^qèƒ.  %Žìs1vè8˜áê0Û/+Òfh‚dNàV¦kfð>Ç"y‹šúÆÙd­IŠ­ri?ôŸ“—¿UnYy½Üvûíò ív”m¿£lµÝò íwt4ì(?È,V•겘ÂP´¯öó ò„´º*žöXíµhý(7ùy”û@@µY— ÜÙˆ‚GÏ<ç£ò ífÇt«íví¶}„Üø­«|IЊnn8(„ÃÃU.ÒŽšú¯¢h\k­rÕå_K>õqgÄèžYˆY‡¹ê³wŒ|÷<¢Êñ6E49vÕezm”Q¤Ç©sú>—½6°ßœ-Ø]Ý,Š£¦¶W`L(%®[Ø`U|6¦/mÛ“œ5Ï>¹œ¸oŠ0´ T|ÃiìçÇ †9*ÙúÇ{Ÿ+»ÍÏß(CP0“ãåÏÆ")eŸŸÁŸEŽNŒ¢´D¼¡ ×€2„–¬“>/…4ö½B9(5YËv Åºï¬ÃzÓàUñ®›¨L}s:|¤ê­º«§£ªÆ·Ñ­®ˆ Œ„4H‡Ð bõƒΊ‡.;P>p9úøwÈçœëšÃG?ùi½é®©¡ƒ<ˆš}›§Í9?OÿL4Ô ®¥@Of l•d¯_¢²8kuƒˆ1¥¼ŠÛVG?Ë ˆrp^¸ß«ä¢óÏ““—¿UŽ:þí³ãQ$ ®m”ÓJõVI‹\lµÎ™E¦þÉèÀÔ?Ý«°tK¹íöÛ-Ò–“qDÂGñ|Fjnу\3!6èDD53èp¨x­©Ç(hÛ½¨ëx|^òñŠц¨èí©{Q\i¤±!½1}¢e,ôš±šžìô]¨ÿ©&ã£&”6׎"2)Sƒ(Íèâúß, <çñ«ÖC›k´œÕ¦†!£? F\›+NèmèÓüï8(5oøy•¾¶ó”gMQ·÷íß­M†8©•îx42.d¼í#8Q3:¢fƒ/õxL¦Ó60y¥e¾MXôÓù†ƒ¨¡;iøê¼!× IDAToƒ4o<¼âgD|a„»y8Ôÿð|àû'Óiû9~—Õ”LͰÉ(ZÏdêÛÈhïcË  {^¢c‰û„0yË‚ h<§JsÄ•eæ<ã1f¤ }ý­-C—„ÿçᆑH3€@-áßõ\ã@˜uXÕã0?£ùQý§õ¨ÿç#Ô?î+ŠF×Uÿ¼P`cp·Ëü>4ÚÐc®eºÒ@ýoºñh´ØÚ´hý¼g‚Š…É¢ÖP¼È€„)PQ":;Èáñ1º@±‰ê-§$YüаflúHej™fŠà×áʾ±n¦àR¤ÑÔ Ý¿4³ ÜæŒ°»R>™ÔÐö˜‘XÖ(šÌ96˜Ë‚F/o~ýá²âšïÈgŸëè½è~†tJ³0 60ƒ@™ŽPn4\ÔX¿a(mlûŠ;âó¤Lˆ6šÌ¤ìd¤—²“¡òS¬ëÒï»jÅ·DDd›‡=Ì ¨M‡WJÅ"KèŠ,ŠhаAíV«”F—P½;!/ öûŒE4†zSÿâ¬Z¨þDù8¬ - ñó$”Œ+j ¿‹Ð+^Ì0z5v´#Ô p÷üŽ»Ó<á5ËÚfEJÿ½(T0LPd¨›%“·gŸÿLÍó.®˜ÔkSˆj×- %×-€òhqaÓw‡›7 84(ú¡@Áì™-èÎöæÅÎZx‘æUùBßQ¥)µ¶¦ ‹w2™¸Ö¡®”bšYmqUºéN‚æ÷3B2¥kó³ˆV†Åd21k$ä¦] ^£AH÷7Úw=Ì:=ºýº=c@CÚ¹V¯ù¶àù‹¾‹ŒhUw.j€ 8pÀíµ0qqÇ%Bbð»q˜ˆà|&[N€ âñBKÚµ Ç· ÏsÞ >¢! ~4q@Çᑇ—¡úO )g6Ô“ëïØË­«þuÿ2ÕÅÚ þu¨›Îwüœ Õÿˆêci‚Zý#uÞen>·Òn®Œ°š^k©¯5×›XT£¹‰Iòa˜Ôx`³"yc… cºgß…Ûó@ÆÃEoÕËîZŒéô˜;uØq, îtõO"xƒ‘?Òù3XðŸr’C:P¸t©Êß:íŒ9†Ôôßú£ä–•×Ë-«®—[WÝ =ýƒ†ºvÈÈ­«nC<@nøÖ•r˪ëå–•×Ë%|\RJròÛÞÚ~v˪ëåÐeš}ùæ—¿(W]þ9eù±rën[WÝ ·¬º^vÛuI’äÆo_Õ¾û›—ÑïsO?möùðßÓŸ²k»®v{Ê.r˪ëå”åÇʹ§Ÿ&¿X}c{Ýn»îÚïïóëàÐeÊ-«®—_¬¾Qn]uƒ\uÙ Ý"Ô5 ùãW‘Cš¯C—(7|ûJ¹õG7Èyg~¨ïC–жïÖU7ÈÏW~_]v XÔ¨a·§ìÒŽÙ­«n“ÞvŒˆˆ|þ‚µóvëª\œÂ­«osO?MÞyüq³ïY}ƒÜú£ä Ÿ>¿}ÏÉËß*·®¾A°t©ìºó“ÚëP7°dÕoR£úÇÒõVÙü]["|¶ä4h^€ÇƒŸ-RèlXÿÅg±‹®«ÿE÷d–K Úüj èn‘³G–€×ùDlDˆ8H5¥xÐÅá ésH5ÏÿZÜ=z³A‚FÐ<#ò¡ƒL„Šðʪ¾6Ãï›[TpOÉ™#ú€+%‡›Ÿ†Ñª3 Û¢"ä•+m°¸ÄÏÒaƒ)Yãñ¸5ª8XFß…ƒW„üàwNç¨ o7|œdÌ (7ѺÍÈŽïë‘¶„ŸÏCwÑyÆ}ÖíÅÁˆE‰|£´ÆTª%œƒ‚*Ý€y•ˆ‡.¶+g´*ºÞë@PÚЀÈ_´z]=иè \‹z¾qŸxHã¡(õ_ýÅch°¤®«þÅAˆë?¥q}ê_¿_5be#"A¼’ڸܜ vج0Ô!á|”o‘bê%Û1ãŠgž›“˜Kr²d‹ÔèóÍŠúüs±AáÀS§…;q ªYÁŸ³å’µ³EdDpÛP[ÅûÞ¶‰l«"È[8GÚHg‚Ú@‘?|æÓEDäW¯0ͼÌÃ>rିäÒË›Æeï—¿Jž·çsäªË¿àÏNzÛ1rÐ_.Ú~G9úøwÈ®;ï$7|ëòœ?Ü}¦9ÚnGYq͵rÒqÇ8õvÛ>Bžô„ÇÏt4ÜAV­þ±|îãçÉß¾RþöÔ÷˃¶ÛQö~ù«d»m!çž~šAƒžºë“›þf«ív×\+Ÿûøyp|g¯;øÀýEDf:¨ív”U«,žÑ¦ž¼ü­rÒÛŽ‘£OxGû¼¸Rž·çsBJžŸ…]'‘C– ""ÿëk_7æ›_¸üÝ©m·£øšÃ%§,çž~šœtÜ1rÔñooÇüÌs>*'wÌl¿)Ã뜠éº.¹ôr9øÀýåß¾JnýÅmíý·¯Y#ß¼ü‹ Ò†þi»>Yžô„?­¶›½÷{•ìºóNòù >&µV9jùÛe«Gî ·Ý¾FV\s­lõÈÙ¹aÄÃQâsPÿ´ˆ¸^õ¯‘ 9GNÉk~¨Š~õnÁbDë;ÆVi[ù~ÜøùÏû€î£¹k÷¿¶¹@”åã.Éù•YPKUÃ1DÖÕo`Pj·6ïÈåÌ5¶TT‡æ›þ4_½EG$DØW›¤±d¢2)¥¢­ójwç÷‚F²dp¹äÒËåô³ÎiŸ·ÿ¡‡ÉªÕ?î÷ ÊŠ ôz{Ú.»ÈIÇÓ(ˆØsÅ·åô³Ïi5ôÔ]v–çíùùÈ9çÉgÛjãÈå'Ê%—^Þ1¼Îþö½hƒõþ‡Ö~®O)ÉŸ½hæ\÷ÏÛg¿v |ãêræ9•]wÞIv{Ê®tM,¨ÿÓp9´4HÐbméMöÝKXÿ3´`4„Ò­wý£Eý:èyQOIx[Ôtdž½ãÂH^h¯›‹|ÒÜÒ­Ö®š=Íå BÁÒ‚ú_´_ªbF .Hå4ôü‡ÅÅÍaRc¦ ;χE`Zc~’kÓØ•¸ÔòCÊü½¼2>ôsD|ÖN&ó€N1Tmtq{‘1š QŸí™Z%FQ¸Á¸Ï’%fUeJ%6#?h&À+ÛºMÜìâû˜J5úLÃÁ1äUd¢í¤¯1bÀ+)%“‹›~lª#¡=GúÿJ‘ ǽêoX«}jz˜Ê¥Ç¿k$¢—-å°Ö:Ë)"Ô;D(y[õüábƒ Ý R³Y¿Æ”B¤ÛEÇ„iŠÎήÖü-ª½ßŒržý9ZÅž yH@ÛÃû‡ç×øx4 M]îÒžîAýçN-ÉÎoÀæµ¹’5ñÐJ©¡vd‹™&#‰ËÙAÚã›c Z!—R‡žAi0+ÅXâBw3˜€>Iƒ&ù»£¦ÁYÃ`ÂHß/PëѶ±ZÔ.ºÿah žóïuˆËÀÍo+Ùd  ¨nënOÙE¸t©|ê³Y‹íZ匳fCÉ=óæ||õëWšc~Ûí·ËmkÖÈ•WËmûöÜÖœ‡U«l~ÿ/ÿzˆˆ\ûÝu÷ö¥K·4t@d”B¦6ÕÛ=òs½Íì5ßùî÷Ìy»jÅ·Í`ª”³œñGA¼ñ‡+ýsV¸+@A¸£¡]tþyò‘sΓç¿ôýøÏßÿ•¯Ã\ƒ/Ù{/™ =¨{Ã×ê6ê?gœ}nW–å$·­Y#+´ÚEWˆÌ2lä¯ZñmWÿ^ôyyâãwÏêŸÓ¬3K)ì•XË3Xÿuõ_jȸZôzþõÑ÷E¨ˆ˜oϨéš9Ö+rƒ}³¨K)ö´sZÍâ_·ÃNdRå™C  lö‡R›a‰1-™£ô-d|sA‚ ™`ãÀH6*Hk+ðzlH"O&ý®¾"Õ-ƒ0Ú Rs~ÿš‚QÔ q9±áÎ<\°ÁÁ44•¨:C®køêYisAãw 'N³Dƒë}”VÅM;ër"ój‡,¸Yœ #DZ!J}Gd÷­n{¨¯b‡»ÈU é‡Cž'þ|Ü÷ñx쎉6ëSÐ!õ+úNDµ^# '^ŸŒ|EnЬyâA:zè0­ _?ZGý³~h †&ëªÿÔ™/¼$ªÔ«ºï …Ðq-Nƒ¡|C™j• þ½}þt2 ͘êÖ¶{ZLCƒ«²½ÓÜ/iPÔ&29a޽±Â Ú&·?S¬ett¶È;gQ㨒q 2|4ƒÅïΪÚïÀPYnBY|îŒÄ›E j]ÖJ³¦ïˆP4t!¢wÓO~"|ÀRÙí)»ôè´Æ‡¡ó÷XÛÐVKýSD‰pCÙ3Û„4Á@¶õÖ©â2“¢¡9jÄùº9óœzÞ[–ŸàUC™›_ƒ[?ô¡fÛYE¾{Ý÷%<ª³5Ú=­ËQ§fEõ_`ö¾.#jÓ2ê"š[õ„Ë¢“õ¨ÿè‘ÈŽ>rtô®K b:S†]Þ™øhŽÁúheh< Ñjr€ñ0Ú7‡Ô{c"æ™Ñ³ò:E® ³`ŠžÿÖ®_s¸èÄïÝ,èpfužz¢2 CJK+pÑHAí¨¹‘A«Û1 *¸ †b ­Ù§æ*jø¦ 3q«v´’Á9ÓèØb™W¼ÑI¬Qã``áÏG”€WÐuÕ?ÒpðÊ´"OQ–¢jº "­9V•yhbñ9rÄÔåÇ–AǸˆ …|ìxà`Ú ¢1¨BÓƒàóf‘n |ü6Aã£ñ halNÇš¯16IAä]ÿ¢!¶ J[ ÔAj i‰ôø‹êÔÿhõ¿áHPRšsd¶ñhð0IìŠG!]Á¥½‹Í‘n°‚\t0§ì(C¦‘IÄ­÷–[Λ[ÎêbW7“O6½jPdÌ ¸iEm¸¡¡Ú,žI Ý«L3Çùb¸zš<­Fsΰ‘Žh‡x|.ü§KDDäðC^m·‚dµIýž6ÝU|ŽŒˆpYµúÇ-ãÈQÔ )cÔÌäc%o‚í·qò«4˜óy¨¶B“ÔÑ›~ò“Þt‹]|}‚¥"¡ íÑ/ãæÍRV³ý¾—ð‚dÈeøz|àà†›ŒA¶C 8Vî^Õ? 8œ¥…ÏêVÿÅçäá=³‹¢Úfw¼°þ¥›Û Qø äç!ò nhI>$5…Ÿç˜U”‚œ¹Ìk5uz³j«šö\TàÞºÉAèÒTÀ|@©oiN+ÒkS#mâôõÚÐh.GYŽÉAÚ#:¨‡PÍQ VâS01MŒ©GØðcð*ÿ´ lÞÀ´§EÓ¹áÖBC>&£Ôô°r4$rf[g3Šiux¨`T(²šŽö)\QÊ8î7ñL/ÔaŽQ¸ ΀ˆìà0‚”/v±ãš¯¼n iÆx?˜v†Ÿ¥Ç ³r†ÜûŒ« ƒ#BŠ"ëq¬7¶”f;u|`âðŒõ™KDÙU¬Íjûrê¿iÖ€§÷Šô³°f¦”íÕr½¨þÇtMn´½ØÕÁuÕä~؆ºÜ}/ Á[[jÜfÑŠ€‡_j1 %£Bl5öoÕ6Cˆî0*Qjqa¬é1‹6€.TÓT%2-©âhZl댰Y톬"G_K´Ÿ¨KQ@µ]00°ùÕß\Ñ´%»íº‹u7ƒŒ‘™þæ¶5kdŸ?ÛË `IÒL $"ï?ý#!BèšAØ£P\Þ×4žÉÓ›ÎéöÛ }íE{=Ÿó¸oáá3ÿ“ˆˆ¼x¯ØÌ™™/˜[È53úÔï „š"zyáÅ3*Ú;O8Î];øŒ§ËmkÖÈg;ŒNµu‡þQÖ‘þó˜í·7ƒT‘}_ôB¹íö5ÝB]Dn_óK 67ûë„ÂúçÐÙÛê³ývXÿDù2 rõOŸ›G¹í?ß­¡3 3IÌ{ù>¼ˆŽ‡AçEbãSÄàŪ)/+"4`?_*¦R-ˆ!cf;æõó±×Æ~~vOÖçoÞLèp &ZRxUœVŠ Œçnc#hzPCŸ«ŽR“À²Z¤ë:ÐyjÑ0§Ã¼²òÂiÁâJÃÎUÎs^bñ?jo·§î*'¿í­²âšïÈ•+V„+ðæÜÖaºÓKj–ø!Ò~ÉLsûí²ëÎ;Íô.UäéO}JsSt• û7®^!+®¹V>hÿ™£Û|°¸äSoÈW4”ƽz5×€RÛÌ5˜Ää]¹â[³ï?pÿ6hŠˆœrü±ò¼=Ÿ#ŸúìE¡zdØBF­œKuÚÓëïäåo•]wÞI>õÙ‹Ìko»ýö®áJÃvà<00š6äD‰ -C÷0Dšœ1L‰µ…ì -ð˜p]‰3“¢>¤ÕÿˆÓkŒBév kN•‰²å,Y¹Â3©/êt8Ç?ÿ!ì»ÔF·K)ˆ¬PªL9Jî9ÔZ‘¡yA}‘JDd¼iÃ@=ááírccØN Úö‚¶&2>‘‹[76üÜhU›oy«àQ>k¢U/6Ð €éî!lG”gƒîVÑЂïe½N”¥Ò Ö9-Šii£ÑX&“µmè(e4{ip¸ÙE?2›#”Û÷ÏŽÅd¾Ÿc)eºÚéþL&CçÔ"¾7^Ç‘» N¦Óf›ÌÔÖ¿°‘ßL™^ ›H'ŒŽ+ç?E×Cè6oðQ³§â¢ú/ëQÿ|éµÇÇŒiˆ÷¦þGAýëÅî”Cõ?½õ¿1™T(›’¬wý󵋨Fô¦´p¼~g™“/´h¥Ù 4ÌïÊïa{g, ¨s¯1;˜‹p™€x¿¡Ž’7T A-¢Ý 㓤¶¬Ù,»áúf­VÊIRµ:¦f%^Ejª!ƒýu¨Q•p¨uÇ5%yÚsþTYv€œtÜ1f`™9¨é¾µüíráÅŸ—Ï}ü<¹eÕõFórÔò·;Š$ë–D¢ðÚX§á(Qƒ(`5V¿ÏÛçåòÍ/Q.:ÿ¼9*´Fö~ù«š9‚†I:ºPo¤‘DžÿÒ—Ëç?ù19é¸cä¤ãŽiû{ë/n“çíù3`"jBl9R4S€:k¾Ÿ·ï~rÊòc夷Ó2DD^¸ß«äÊ+Ü n( Õj@" në%—^.[ýÞå–•ýÜ}Â;f.vPÿÏß÷³Œ Ý ""[=r‡ð~³hqYk—]-£žÿˆ”:³¨<Îf"üÌÆ ]]Må`ý3C‡t~¨•r†Ùk-Ë´X/ÐNµ¥ç´i¶";j›m·ÝDg˜JGZ\¡h<–VÖÒsЇáï}Ǹ‰ýóøw|¬þêâ3ŒÃÒÐ $7ÙüÇ`.^õšÊK)²×Ï–?ßo9âø“äæÿø™síâ̾Ї¨Hé¢f2BŽ>5™›`£ÊÍ£‘ÀŸçðçw¯]ë¬y£&rŽd|ü¢U˜”fCFΣuÞì꜂2ç:¢µm`ÒÏš}¾žG誯íÝß3rŠÔZÚÏõwüÿ|lÑÀÿŒì´ñç‘.%BÎ&}nÔ˜âv ]kCf|FÍî“,ÑÀÌ Õ‰÷é4m{ÀÑˬª)ëeŽu&æ¸Ì…ó:@ª¬,ó÷š{ÐÀ10µÁôÄyI܈!¹ˆþñgq¦RH=Ãë¾z×>³OÙÛR«w¿ÂÆ÷—ß=[Ãó ŸQÙôúGÇ-üy»/$ÒbM¤œfa¶—^.úZJÝÛúî1eZl-@y„P,®«!5SÜÔ\×Ї§EÏÿÁú§ó½ðxñöà ³ÞÏqNmvŸU5Rê’½þ»ÊçX§éÙtCÇ3•î{ÿ-MÍFÄ d¾Üï¿Ú<4Aꢉ´G :×à€}_$_ùäG寝|A~xÅ—äê‹>é¨5eëƒ'-—_`.²Ïõ!ùÜYjÿÿœgì&;l¿<ìÁ )‰´4&5ÄØì¡‘)¯½&ÒDÍ`4Àܽvmió>p0åt:uº Ö¸ô dÅ" ¸ê=…ÁföžÉd-™,Œ¤Voû]Ê´½¿V ]Ñï«L§“6é÷ͶÏSýs<^C’5M.4È‘u4[³ñB4|òö™UbD5T%â3}n]èRà˜GŒ;SÐÊ@H¬þwOëufÑ3Tÿ8ñÂCôй'õ¯Æ Ð"¬½1Õ?ºb®«þ7ˆ Ç®F4…ÉdÒj]=$K=q DÙˆ¨+Cƒ8Ó|ÚªfêM†x jr’m쑲ՌI D Ä2`ß“Yueí@ÓžD¢w–­ÏÐ@ƈG@?4ZR$ô}nå+ZAoÚZuŸÄ;Ñé¶5=ñÇIW­Q Þ¨<¥ÆCL¥†<€º 6¡ÞQÅÄæEµë+I¨ãÂj4Qèîiò†4Ì ½’#S›@ï34TÈy$ÍU’Ôó¦Rrû`Ì6’xÍ¢³Rÿu þa;Ø”€~\ÁÁEÝ £E2v¤ããoôÉ/@ðêí:eg7®ÿ ?oÑóØ â°: }Žþ¯Û„'ªÿæ7zRžKUн? âåž1‰åözR;lÔà*õYûMßik )@Ê>ÄŸò¤'ȱox­¬¹óN9ñÔÓäÄSO“Õ7ÝìV­™nRJ‘í·ýo²tË-L´ãöÛÉýî{ßv±üïøyž/”«¯ý®[ 2@ˆ9Rœ$Oëph2ncA3í—ä[¦G±5†Br½¨‰²šÖ¡¡Smf Òßj-RÊ´ 5:ŒÌt9¹ý}<^®ÔØÌ—2Gƒò|°©êÛ;j–2m(’~7n‡nn7‹ö›°}ŽÀàqcmÛ|³†%Z©ºpÿ£ë0 î²›x{øšŒhmü¹£à:ŠèƒJwÃki™S÷¤þYÇ7÷EõÏÛáéçÖUÿ ®Dv”ª‹6þ#8|sÇ¡i<mðýu2µ”Í)[ÐÒ9„ð0íhÈ u½À«&ºåRŠ¡Àé5—çÛ•“fèYt“G‚xµ1BVpuýò¯])¿úõ]òºeûËqGî4ËßøWòë_ß%Ï~Ùòµß’÷}±ìöä'É{Îåyú.;É…_¼LÞuúÿ¥[nÑöç);=AØ÷ErÝVʽtù_W]-ú½†(κr€˜6Aº¡ hM½1Ðúšî(Âø}è|æši°c6‚ö$&|•‡˜Áh×( %mx ¡É‰ôçÿŽF£Žæ”š Qþ Š—ìï\(¬@øk-F›„HMžÛñâ0’rjèÝ«ö³ñ1¹ØÈ娑Ñ17\UÒÂ¥ ñ•ähs†6&ASja"gBÖõ%qÇÏÔQ(·zärÀk^k‡Õ õŸïaýËÔ \OË‚úGÄ­¬©V#zZŽþ6TÿíÎ…³F6à†šÚºÀ«úÀ,ÂÔQòõoóÿÄQ§‘²æMTR{Þ åwöçZ¨ÁB„ïuNeÎ\ÿ²å±‹PÔåœåæŸþL^ýæ£eÍwÊû¾HV\|ìµÇI­U^ÿÈÒ-·ãßý~¹é?~*¯=úx9ø•/“‹.ýgùõ]w‰ˆÈ¹\(W_û]9ûŸ‘_ßu—œóÉÏÈMÿñÓðAæÇ>)ç|ò3òš#g"ãõHyÜ£·—ëW®’7ŸøN9ëüOÉ{Î<ÛíßïþÎýäa¿ÿàö¾p8¼“›ÍhU5Ìãê¯xG02£ ‘ ;ŠEh“þ~4CvÎú|´Šƒžgß—ƒá¯£C3³„l %Ò©£<}8ꟑd4Ëx¼¤ëZ¦ó=:¨uR×a3Î;¯1å Ï *™’h†^ñÀí¼@XªyMÐäT¢ôUÚçê3–p`Rĵ¡*‹ê? ÔÚ€ú]ND7sõTðà>àЮÿb+3ˆaë¢ú¯ÞêÚÐù¸þs ï‘!E;§X#ÞæÛ9Ңᧉê™ +kæ^ ,¯ƒïºÔHgi }0ÒŸÏhsyóA‚X ¬ cdɬZ‰«®¹VžûÊ?—·¿÷ƒòë»ï–“|³d²y¿æý(ê¤Ã”"?щCš4ˆøÐÏh˜º¹à5†?Ó‡+ÝÂã‹(Sa˜+?Pp˜6TKÈíªAØ1Sðîmýëö.JØ^„d`óŽ5ÑðÌ>Ó9ˆ†X5 À`çÉtêž C<m¬°TÌ|®ÿêh³‹4‡Ñ=2ÐCÊXD{¬¶A ù÷hðóÁ‰ô¼úØVØAk`Pü,¢:-ƒ G•ÉÉÚ`Óê34np"­AK’u¥3na¤‹àÁ m#h%zdO2'l¦™j…û`»äi^&k.Y÷¬HxoŽ}±z¡TÔñÐhÐÂbõ[8ü nB_ëÐF¾ÎªEx 5¤/UïüfÂhe©M&‰äa›Ùm)}Œ$šú—äËÐv›‘¢—ýp¶Þõ_îaý³ONá³chp B]°.ÌóL ÝÃúg§»2Rí‚dK CU ÚVý;Ø×–ê¶q4Ê@—æPìj]F•  ¹t³ iƒŒæETw¤ec߀õŸ¨þ‰Ýä‡ Z}Åæn¯=ž-çžú·rÐË^"¾ôÅrúßœ(""_ÿÖ5ò‘R~õë»ää£þ{ûý9ïyg[™^ùã‘#>Hþ°‡JÎYVßt³,½ÿýeÙ~û„6Äøýœ1²úßo–µ¼ë¸#å —½D}ÕËÛ{–í·|àÇÉCü ¹é'?57µÈ.:Èx¥›g‘%síEþè4@ã0¬µS²ìïÙñF#g?0Fæ¸[g´‘ü˜æÅ ß@u;ðxGç€éDÒ bÇ'jdñ8à6¢iÆP¶€Jßc»ð¡&|}ëŸo Ü´Gµ«õÏîx80Fº7ü\¬ÿ‡íÑkt\;™8ÊYYúŸ£ã†"íª²õß©r3wvˆa4ôeND &Cè’61ÆAެf‘R° 6–ȈL@Ót5^=o>5I& WrÅ»YµcÅ.cIB €k²“EwÐpÀü¾ÆÂfƒÑPg¨NÉH¹‰ì´QC…VцfG(ACj§± B~wõfƒû!¤1‘ïÛ<ß(j¶ ·_ìçFú³}žÈ¸¬¢Ãog¢!­ø`T‡Hè"Z ÊK¸×?éŠÂ ÄìuIC Xa®T¨ÿÀ=¯ySÿ;Í9 ­”Ó¡ú¯žf9Öæ<»8g(gµÞö‘.ö|ƒ“ØûY’î‡ÃP¥Y{“ýǸr,à™ë??ÿÅ/d»ÿ¶û†×ÊqG.KïyÿY•s>ùùñM?‘£Nù;¹ß}ï+Ǿáµòæ×¼Z~ç~÷k•Ï}érYóË;䯖í/oxÛmý¹~¦GxýÇKŒuvϪ00e ñ¥¦WBT«;ÙÍ·þ‰ÃÑìšÊ0PÚç§”ƒ›[2ƒ$ž FÓúµÕ;¼ô»õ{ERhyÎË¡0S¦—E®k¸?ìh·¾õoœáh{uØa´^£mG÷9lôy3¶è@Óc |Ôe©vìñhd‰¹þ'j¹>À‘¿7ÿ¨‚ýÞùƒJ¦–Ζƒ _¦³,B¬¢›¦¦ÍÁA6H@çÍšcVN¡hõfå»Z½6>y”-E(‰Ñ­h³ÈVÛ¬?i°`{Ì‚˜$O™Jqþ ®ê›•orºã@YnFÙÀÙBWK)kCYw³­¸iÆ%¶A®ôohÉ.5¦ÒpbPéãEðj(`Õk4ñÁðÉÈ?#*N“1ÿWµ ƒ: £1U…ôœ)5… ³¡Ñ2UK/d*œA€èûéAZRØ¢¿[Tf¨)Ë¡æÇ®h”ðõú¹}É …ÑcÉÛh–Òî˜ê‡ß‡Ÿ¥Ç ?ofM½6Ì1B¤,ú9軣cÄïU=½vøFÇÔœíSkmVÔ²QÍÐ5ms¨ÿ9ÄEõ?}SÞsTÿúy§ûæ ºÇþÅ_kVݬصÓT„z÷ÿþ¥C}òüèQ‰M"zÈ"£–¡ì¦£àjh¤1X”N?t-r²;kášUÅ9Ϲ`N Ü® h’5Jí;SÜÈFaŸŽò•$Ôxj•ÔÁ÷ÎRݳÓµ„÷“²}ªT;˜Šwãï7¡šÉêŠBûo©qcž­’H¼‚N>['»óÈÛ!k³«FLßjß“,¾S0$‹ Âv6ÆâÑ¢›Ï!¸YLtž×¶Èš¼5äÃV×HqtçgS«<ÿvmÐí2ùkÇ|ç´„9FÑgº!‡LE"Aôü5õ„¡¢}9o3Ó«+hy:ŠŒSµÔ#ý»[>P¦eJˆÐ)ôY-³·Øûõ›~N¯þ27? 8Dg.y³€³¤¥ GÎá×aã妼ëØ#åÎÿóäGÿ~“‰Ð ,F’ð8B€WÊóÕæ)N&ñÿÔ4÷ŠàÀ¥ƒAzjˆévLjT4e@Ä uµZ$kÈ€×—Úi«UxGZ„ht³ W¦´õs3%ô®o6%³ßÉ*îü³nJa¸ð¼zÔ( Z#9š07¨-­§¡®õ­ÿh0ù¿UÿˆzaÆÒ¢úWôdœ³ã2æ4¸)Ôÿ†þ3™NgæómÖá Ñ5Nhn=„ð`ß;D‹ôZCt¹èáΩib‚ÆÕ9L«¿‘í26J†÷_¬HõC+¼ØLºfT¬Ž§Q„ꥅ(f Ñ÷do7Ÿ$IM5¤Aáw¢3®®¡6‘~Ç5I¯˜3Ú{s˜lµ!™˜×cÎ7åÖÚoSMv ‚j±Éo!ŸåÍ ®Ò?·!@h÷=-Œõ>ºOY²ÙO¼Ö‘‚×—`€0BýÀ´@V0–ä4„™¼δ‚×·ïȽA6æ’lF„×â¹b·@WÿˆÐü¦ë_6°þiXBÓÅ8Z`Š>·í#l_)ź,BÈÚ¢×ÿR64øä`ðë½múê÷©ê²êº6ÞD„ê?µ,0 ØI7\Ø, l~k°¤¦Ðœ´úņKW—Mªýdb„ÛÆU -\iN)ÉX©/9Ë©9GÞ}ÆYa"†Ý¨t›P4®ÛÏBï!g/¤ÙX N1ÃË 1(Ææ©eØøwÅ408°ÎlŠÜ -¬#K•J;:Õ_WLƒô,«Åñz$‹^Ð&ÍheˆÜ`Ó«¿C„ÅÒ»«ÒÐú±Ì²vmG¼T‹Ô°ÛÍ- Fµj“Ý‘ÍUò7  M*î¯EÐt`©FX95”¥öÕF½›N'²dÉ}LÀ¬>âÁí«ÃU'¸nÇãq«Ï~®&nøªÖÊý6Ô?/jàöÕ¿RÕ>| õ¿v2iŸ›6’&h”³Tm0jC½ÑQ6„Ì w•sð;ó™i±e¯ÑaÌQ#$ZÓª sŠ­„y0búfNÙ: UqA¦¯)b»ë-0‰PBz[M^Ð"êᬚõÇ–Í‘Eq¤ài›W‘E¤e¤ŒÀÙŠ­†‘®ƒ:—š4TŽ£+Μóe’DÖËøçLПè¡0…qmk„Ñ,À ø™š8 µ.¨Ñ&2YÛ^¿WuGˆ< ¹ÁÌœ¡8ÔI3,0KKf¨±+ÌSC3ãkvqc“u±ëhSvC…"GŠ0á1e]PL&kÛ±‹¶ Q©h8óš){N‘âÆz ¬™Éd­3˜@ô(¢6…ÖMD;XŸúÏ›Aýë¤ÃÇh®jväsºÚÆ@€"DHix#2Ds M•AsÉD³†8¾vÚjz5´(è›03tñbp¤)ÇÙ „A›ý ãD‡BzDÀuÌ4áÅ ù 'YꓳRÆ@×ÀÂõ$Ƭµh 0ÿ~3ÔIuÝì¶Çè‘q~«~€SÊk’ M,§8ˆÜùÐ.²(7ƒxÝ ÓÄ̵Ù1-ü“L4š^i.®oÎwà¶Æš ñ45Qì¹à’³¬ðšf}£»ö±g˜ãt‡HŒÉ$K¯#§u‹šéi韕|«3Òªÿ õŸbÎoEý3ªI‹æ÷•hu” ¤ÛT¦%tp3®9¹@×!ÍRC‡³uðº7—¹)Ž¡á‘·Aç‹iö^-ͱ쥳ËÀó_¥ 0E*ÿ¼þ›©¹tnÙœ˜ÎŽS¬3`KV¤—q#_ܼÒÄbïu ÛôsG£‘iÚ†¿¢äÞh%&gó y·³îVSC%³(F_çAƒ©_úú¦Ÿé@Ãîo*ªg75DØ9Í¥Vƒ&!µO ÜVD¯Ôé©\8°þ µ1¶ªßÁƒ†uS³úELppAKoDfpøÂý×aË:óM+Ÿ¾ÖÖШ=ŽÝà!™ýG“†hŸx;±¾”Z¡ÿ1½ŒioŠqM*êù_©þY ¤„±pÿ,Çz£Ücaè±6ÝLað\¸”sFóEd¡Q‚yH/p¿Œ(,nKí5… :ÚTDç·Ú:¦¯ð¹dýŒÉt+d¶²Žè9­Q)>÷3bZsV}p*º…µãXªªp€C*Mäðåô@âÓè™^fhM€aó¢¶Õ.“‰ÜwDjؽÌlÒ©)6ê¤/@tbÈØâÄcŽ’Ë>û©~‰¨žVØ ‚¨qìö!£¨b„¬9Ìåì\¼\ö9õ+g ‚Z!߉ݓØ`DœÜ>Ò©?×A’MÌu[%4:¸Çõ/¿úÏq.7‡Ê‚ú— ÷‡ë?¯£þ‹uUuõOn8, ¹Ñaf—©zjJgΩɡ@.fþ:}ö˜ç‘ô…¶V^´¡Ä›ýŒ©¥˜:ê ¯ëÊ-†A2ýŽÖÐáIÿŽ”En0Ä4ZõÔ¿¯LÜk¸Aì™YMv¡ªü]>µ¾jóÌÍ{×ydãÇF¨éÙ?ÅÐÆ°©Æ!‡©S^_ ÂÀH"¨Ç±úl‹ZÉMƒC›½–Š£Î!ZÒ3‚2XjO *c©iv0Bä ‡ Çk¦AE)†ˆ¡=:Ð q6¤ŒŒí8ËnëÌ9`-›FÙX?ü‰ÞÃVñÿ•ê4PÿÓÒùûJÛX÷Ø6pRnO)q¡ÃØÈ×@ F¬+Ävø¾6à’Ti´Æ( ×£•`¦À„”ΜÜb•ÉÁF“£& G¤š†š¨¡Sšœ±1o¤ƒúhÏï쬮 z(F4ð2-†2mŽf”chš#j[Ôdcƒ†A‰H=3MoµVÅœ;‰ây•qþàq;ÊM7ÿDN8æÈ~žä]ÔÊ´8›sÐÌ9sŽ5Û3¢c†ÎQvj"ñ IDAT?G½ÉFŠ´6 ¸Rc[ô:p\‰ÉÆ<!Z’s¶×rÞ€úÏ¿ú/5DãLýCfRÔó™}ÿ|S·FSÿhûièŠê?ýtQýÓ0QqˆBñÙ6¸¹€ƒý³“ïF ˆ8U©2~m³r‡c ®6£p™õ2‘˜ŒCõ³U8¹O™ÕX5a ïú(5bíœ=‡Ó™uÿúê|·,ÆæÜfZËä>Œ2R+æÿ( ¦!Žl°9_ÈÒã’AÆã%¢}ÒÌTÓÒÑâ`]ˆZLÞélÞ|‡Kõ*fØã XÜ>sñçåÀWìg¶õ„cŽ”,ÝRjyÜŽ‘Ï\üyy߇Δ“œýÁ÷ËŠï\+ÏÙýY²Å÷©"GuŒ|ïºëýu6ß¾¨9Ç!d(ˆ‘,“‰T%D ÝŠl°1[ŠÏ—±äV‹uʘaDS ¬÷еÿw(ß,rÃCTožO™¡ˆœŠ®´,óú ø/BúºeX6ÊÀh‡iô ÙZf{䈾ã%j,9¸5º\0'êY@Ün¨_UÂ<Ó¬JàB—lãØîY)÷ÐSD¡¤Zj}¯¡f¥ì>ß ¢˜©ÄBvüœŸ×?Þ}wù÷›o–O_t±HyÉÞ/0Ô¼ÇîðùÁÊ•²ç‹ö•#Ž|«¼øÏ—'<þqí³_ü‚çËÛN~§ìù¢}åÂúü Mb\¥¦+Y„!Ò ¹•u¦à}‡“û QÁjýv~/6ö&PWUËÿIõ_6Bý‹8GÁ鋆=Dv†hræP—uÔ`mͯÈÑŽ¶.ÛEÏFº]ý#“$d2ú!ê¿õ°› ŽaÕÉÜ!‰*êÐL šr¦K`M€˜xƒ!÷»Hi'W))GÄä·”êB̓¿ú¦Ã5`á™!<¯J3:a¨A¤ÁMéCR6–ÜX³3Tûü<Üãà‚6"h•")¥ÚŸˆ7í8x4MQÂBóP{ƒÈW ìÐ`§M1 ÓuK)Éãv|Œœÿé %¥$ß»îzyÎînŽñM7ÿDÞ÷á3EªÈw¯»N®»áFyöîÏl¯ùÆÕWË¿üëuRk•÷~ø ‘*òÄù似Ϊז53!mQ•p G Ÿq³có¢—UúwˆîÖLj±:®ù÷«j:p›#ÚgDGu§ÿÞÖº—õu¡BC_ˆ ÈùF5К&«ËC§:CW«Á°¿GM¢2!Ò÷ßRcª[µFCÃÓºlÆ'Óé\+d7š•ŒÒÌÍ´À°Y A"ÊTN“ÇÆÖ‡VB§¥8Ä WyY³€MæŽph#žØ¡Ü”!1ðÐê³m0kÓ¢t‘{6ƒPÿÌB”¸ÔÐlÂû¾óZÖ ©Ñ"¸²?³MêÈl'£èÖ†ÇÈÚHWC‰ŠŠMfCÉÔèpx ìHM ›p—¶ê¥LQ‚ÆGo+nÝ÷ú¶eó>¤Ïu·»daáÂmµCCq×)¢^8áÐÄt¿N2iš2]}˜ÄýÃAË¦Ñæ#6´þË&Uÿuo}o‘ ü¾B'¶¹î+ò^ü›¡pƇÛ:ven?΃©šADç¡AÉ<Øa(a!¿áô—Z>·í{]“uSüëóÈ^¦©;€óЃÂgmò}†šïœ²CmPRjt¸2è 4÷fÕ›?!Ú5Á˜sdI¦!T]‹qrªDµb'µ£X.Ü•šR“7^(Ðd’a‚¢<¬Yb³Š×½æ`¹é柴!æüO_(Ýá1°­"wÜù¿Jòà­¶j¯ùÙÏo±:±$ò¨íi‡—8îåd»v^®È,@cŒžØA_ëôB€˜à±cz¦¹öª¥ÁëmjŽÑýÃ0\׈5zØXõ_îAýçõO p\Ÿ¨ORŸQ8”ñ‚ #\æóiAÇ z)vê4õ_† fÚvW‰÷èyŽ5Qê Ù ~gj÷NÕs–!bbìšM^Ä:¤‰ 5fÀ ¨±E0òïÝC…¦:ËÐjp¤0¹#‚lF?4_LJ¡ŽŒ¾#rGëÛ, ÏèÖÈ3‹ì¹2ÙXÆÅhFŠÑ½èg --bù©£‡):dõ:ɈõS™ýç!¥ëHFtœÄäY»éÚ\Jlhl=ÚÈ¡JxÎú1ôƒšô\¥BÚ­ ×YÏTš 96 (Z­a lÔ–é÷u]Pß§.öŸ´MÏ^[3s‰äP%ùZD=•â£Qž£ 7ôÚú·ƒ¤”îyýçM¢þ«ÉlˆxÞrLgIÜ#°-¨I©­°iþO >·,T4;ˆìKô/³ût ΃¥õ‘"ÈõhÜö GÅ$¥ºl¼8Å …Ç7¶ÏEó\…6v͉bƧNSACŠiü)Åe¯T;$”Rf+õ¢È”Ns¬Å;u¹•x8Om»0;)yÄÇ bBH8Ául²Šò𪼡4Úš”’ì²Ó“d›‡=L.»ðSæ=¯{Í!ò¾Ÿa·g®¡ÙâþÿÜøÃz'¸ùqÙâþ÷—®ú‘FJ?.¡Ö"ÉâÅ’jénM?$݆¹e\Á°ÏŽ0•¹ÞªZza£|H„è9{m8¿zí¹Œ'®ÿLõ_ºûºöqøé:ë_6Rýç þÅfýDAáÅŒã4ÌÔò×5Ö™©»`qÉÔ¦úŸç9T¹øá²}V^Ïú§Å]@1 4Ò5§|½¦Í-,•›ˆé\Ì('¸ëª°%Ö@ÈËýh:Ç×rƒÃ¡‰(ðšúy Á¾ØíAÀg‚†^ -M¢›:6ËØDö̘BÂö®ÁÀÆyíÚ»gØx‰f"Ël¤c銾º•©é‚îf×X[ÞRÙ´IG”HáRÌÝÁ•=^µöYÃI±AWE0ç u*Òý*X‡ÏΙ:ÏuÄED«6h<€º.lþ™:†¨SˬI‚nÿÈ!p8ò5ÊM&¿W‡(õó†»î«´cXŠ8$ˆÜ4ÙÔë¿ÌCQãúÏÍ.¦ômè½uDlo"²L&³ó¨Æ¨ƒRíPàsx*ÞÇ»#]RµîHÃô‘vMè•`‹Ow9ë”?‚TDf†L—él``ûkÓÜSÈ% ®ÃÐvˆ:Æ«÷<ìpj´ ©û]µºþ½àW ©;œcƒH‹(oÈ"Ì›Qô‰û–ÅPœ0x5 š5:ÚG3T"ål~ÞžøøÇÉ6[?Lö|Ѿfh|ý_"»îô¤¶ÏÛñ1ò}¬|ïºïËKö~l³õÃdÙa¯kûøœ?Ú]Þwú™3«íc”›nþ‰|÷ºïÏ4PzßÈd™NÇÏ Yxþ’8ˆ ÚýŠŒ²dkóŸÄÔe‹–ÙHÇÔcïBE«EÓVN’HÛÌ+5ÒÝ?ª¸Áß ’Rÿº˜Õ¿ø°R¬™ˆ±Âú\Ûh!n°þ± ý<{Ëñp*Ò Ñçª1H)¸(6ïud¾èFîu›ÅÄùꪤ Ä}Æc·*Ê®S¼JŽÚž´™Š‚îlܤ ïuIøYœ¢ßË\^‰ž½¯ºkmT(Žô/¤!Õj‘!Ì’aR¿èzƒÛ)bÕhb–,¹qRc   CPñ³­.¦;ÆYQ¾¥[éÏ:šb ¤£!ÝèT‹ÄÙDœ×Óî/MSœ~u+ÝMŽ©[#)á†Ñv¾Ó’‘ hÅ󀟩ߧ׌î3! £# ºÃ樅bs Ô]Yç¾lP3Dκ¦Èæ) T-ßgõß;¶€ßöú-¬ÿ<¨]Ú8Tã ÇJ¨þ³”éTr)Ud<Êr÷|ðF"óûoDmã•ÌJ ³>ÀIraÀ_4{ø&?p­£bŒÒ ­‡V·ŠN´l,Ü€•|¦\úÙ dŸ½_(µŠ\wýrÂÑo™¹¿‰ÈGÓö]Dä_¿ý,c¨ŠÜñ¿ï”}^µ,¤‹1’¥aš¢qŠK6KÊ­´Wq׸ÒZ-ÌÏãh4jÇ&Òqá a‚1êÁ¸µIrèECD)¨4ɼ¾éó M‚ú— þå^Ô?i¢Å®°þÆhê?`ð¢>#8`w]Lªö}ÓõŸ“yñ€Äè/ˆ„Ñ06z";ðU X8X·èB÷¼¶Ç£áþ³‚xE‘uAH!Ñ‹%J‰ÇUTÎ!Aw¨HtÍ–»Ñ W16þ¿››«ˆrci;IRª*¤MaDqÓ†pÖÌG”Ð[ ÎRÈ’3@*[·"™ÂBJþÜSÖ²A˜fŸ'©™…eu«nÔ‘ ú¡CÕþäu6ô:˜(2Ý$lîM;äµáˆÝœ³Õ¡‘Ź”º=MgƒVݨ'bKõšœöçž4ôCÔ¾{]ÿ5¨YÏúŸfp_Õj á¼.Z±>Ì¢JŠâŒKg`W­aýO‹ àmõ¯è_ªƒtÎhtý¿Ò)KÊN®¨P>Rd31FГ‚A‹ØxD0¡&¶ã´=Œ±1rE ^ÔŒA‹Ø!&rJÒaN gz†¾†mŽ1H‡k[m‡.º/¡žÇêD2íç´¹‹1 çÊàwò°¦Ûᜆl¶ý1èÈ@§£ †o#|œqSÍ ºËáë…³ÖÕÙ¡2h8ÁnuöºœºD=ýÚ Èíà›ÉÞº84O¯+¦Øá0‹Û‚yFHËDĦk‘2!–vÀBQµÖ–ÆÎ+°ÜPhða”ñÑílt§ëVÕYD†„éµ­àÛ!`äÂy_‘ÁA…µ0hmÍF 8” Ò`ói’£C¡åôd²Ö4»ˆ1­.n6 å Yó„ŽŒdXý·Îv<(ùÌ#‹á Ã4ÀE7ª—š]Ó6ptÉ’¥ ¯·Ù1L²víÝsVD¬N¨¥±PðæÄ\sÝ.»£>ˆ$!Í­_×ÖíNj€P_4û|E‡Ä!Sz^ÑPÃÒ½UºÒëfç6tÊÖÿÔÃ[ÿVcÕ?Zó+’„ œH>Û½0ª EÚíÃGd4Ét2Ñq•Ê@ÚԌôw³éÉ,Í0?ˆü¢¡¤Õs£@Ì>Ï®VöQÐñ³YDërÚ’J+˵sÛ×)(ß\¢Õ¶yÐÏ?¢é%²˜!CµŽ2“¥£‘”Õƒ¹>¬uἤà9»lZ‘æÆFßkÜîªw»2I&±ž‘)¥¡ žé‚.Ð4>Ýö½8݆ˀáˆ:é)0pUÛXÏ·õØwœlµc`7\r‘e‡½®oBôØ…¯!-#¡”Ò)\0/jøùAújÇ$‹£šiCÞL@8 VÈœ©4·¨þA„jÇ”âIÃ/¥÷¸þIb,Æ7vý3˜G94 ™i4‹]8™Ÿ‡Vÿ©z4…[‚¡5¬ÿ4PÿQàj@ó6õÎ|暀úFç¸È>›côš-µÌá:6 –›]›¦Àÿgn|DGa¿6#øw{„rbËÈJ4` Kçp`ãó;eŒ‘¤·!-Œ©eýûí‚ÛÊÔ£hÐÁL¾ðaàvûÂs†È6ÕªCêÍ­Ïž±ÈF Wqûõ½ŠN0Çz“xµ:›Á pèÃíÁL¦b¨-»è¡€E+,J§çI‘A¼&"Zn/î»`Hëç§Âw#¿¿»Ž ª‡ô;=ÖhÏÑ7õà~0j†ú¶ZKPÿØkkbcÕ¨Xxà?G­:„­+tCvkó¤L§ÍºÑ–ä&l­*W•6'=#h]!«xLœÝ{@ß«óKµD)˜_Ø>–ï?|"7¦H‡Â«¥Ídˆv­ž£‹•®ø¢ˆm¬y•^›]  4a‘­Ì¬º'k…lâŒ0d  ‘ë½»WP)Þ1ÎØäJ`©œ¬… ¡ƒÕÒÆÌv*M­t“…öwñA¢f¨á*¡ñS jÆ¢@ˆ”öÇ÷˜F%Äq%?[ ¢k8Åî+R£}P혣 Îbˆ02âç‚}¹þÅgRqýGÙSzÝ›¥ó‚úÇÙeêÔß@ý§ú‡Eý!ê™ µª^Ó¼ÒuÑÊÜýë”Ã>ÚÔÿ ±¡ b]æØâÛ\·y û ‚lU‹£L·E ZÚä‡ vF£QàÒä9ÄÅdþxÓÐø$°tfýÐÍ"ѳ0÷'8°¹^ÄúÑké[èhø«ÿºŽúŸç7á±â°QSÿóņ¡pVÖ¹çÚ´„Ú&=†ydsÂgYc4(ˬhl1TÿBõŸ£}¤šÚ, ÈkȈó3ðBF2™LÌgàŸÚ ÙÀO«ëJ}/®ìV‘{c8À iGüj?¯X#â`÷•†ï&‡Ã¥µ¥ÎN'„&6 uJ:#‹h BâÍ].ûnl€µ F$A›àˆÊÇgÍŒ[± PSmÚû@ÅTBëÇú¥ŽrMÍ€×AqNˆ¢áöφõ%FÏ‚ˆš[ö"³ =¸Œ¼0¢C"Õ¾p¶”5MHí¼ê‡ènSç´žxøà!ßÖ¡ì&oNß×QÀb†8u¼ÛøõŸhà+þ¦«wˆLÛú¯¡×ð†Ý[cä¥Ï4©i„5 ‡\´šÓØôC*jhq*q~òÉ«}ØåÇ.âëœí6¯Š_àè¯u ä5HÕQC”™ä>m½¿oÞ`ê*¨1SÄîÍÉ-@VgÇ´¯ãç©xWU%õ Â¶Ÿ=it:Àµï¬Ö5=.%ÐßàIlÒšà °iˆjw¨Âÿt‹ ¤Çq”­d‡2cÇ,ÞP ŒH‡$±„ö9ÄJ¬¦Â„¿ÂP2¤eq 9ükù®bЂÅ Ê î £@:Xéµaì»ÙpP»F¥ n¦~MÛºs40ôÍ쯻uÖ ŒÕZGýg_l¨ÃÃÞ"ÙÖ-¢GxN3™áèöÙØnr]è½Ö m¤k2÷»2i³Æ°þ Õÿf1¥”ZX jƒÆ” Ä¡Z«ìµÇ³åKÿøòƒ¯~Q¾ôÿ Ïÿã?”œ³üà«_”^ñ%YùµK0YP7C“Õ)Bó¤õyS9‡qC† Ÿ}HfcFÀº ^æ _ïC¶rHñ‰B´ü`ä°b+n6Y¬Î 0¢Ø4ã¾è°Ã+&Ø¢A¯êóöóqC!æ ñþ!³yü ZÕ4Ÿ34-ÀA§ç YW=£!“ó”Øh€u,Š&EYNj¢ÀçCt¥ÃóÝ)ÙhvŽÆuÁC©"=hh¡Ç¯kÝþþ=©å:áõ×?¿„ú7Ì&²µ“À€`ºäÝûú¯ƒ(;½-rP Ý÷W^Aµ¨m5hê‡] ; IDAT}Œ{é”8è´Š„öá‰ö/“K/…4B P'|O&Ê\ÔU2vD£´ñ˜–ÁŽè\ 4`ƒ„\ùöù© €£&Óå©Tñ¡”¤÷i Ô”ZÚÊn(.–jV =;Zn‡ ¶Å[›J!íO!Á \Z¥›¦[Hâ,zQLï\ãøy—(p6Ù‡çÌ%М§ !%d rfkˆJàôÖŽ¹ £Q<`™×“Åvsb«žòfV³~/غl)±š0S Õæ5Û÷Dn| †¦2’E£Îå •a„Œ×ÈGõ_ŠŠÄšzpP«Ë¼)±ôB‡óÁú‚œy›‡" šÄ?/–¡-Ú™úGéI±Á×¼x·h@mÇ¡T‡„MW¶Ñµô`ÔÑhdîGýÚòõ¿Y¸ÃE)îl­HÒÜRJòÆC–ÉÊÕÿ&""+Wÿ›¼ñàeíõùÃ?uÈÒŸXHMm O…4>HCÊ¥’%‡4 õ éB6|Ò£Ñj?ÚYÛãZœÞ§ë@¦æ;¬KX6ÃLä5¦l;=¤©°ƒNvÛŽƒB×"ÙàWFc”v†ç©pø;¤”éö #K¬or_Ãã ×€!|CëHS 4 S7 c3®œE·*ÑA, gQšBƒau´M;(ùXÏ1‡Í­¦3Mÿ®çLD°ðzVt¦VÅ!¨8LYýRÙÈõŸÌ€Å¹?ÞŒ ‚E1¨—ãvo$M~OÏ…*„^Aˆ²>…Pûƒ}FŠ‘Û[&Ì £è³s÷ÒZƒÏàyŒ. ³˜ŒµQ<î\£2Äû©ŸÇCÒ¤\ø)­êcCju[˜û”­‹!-(Âw(¹¿ùÁ´6í 7Ä­ÁtÉ" ‘ þݘ`N4.ÝWÔÔ°›YkÂ’§µ1’eA‘þÿh@ÑП䤇!e0tT \¶ i~än7HᔊWкÝèŽDÜ ËNlxÝ`*ôšüµdj4k=ÑdÑyåaÃ8‰;q–ú/uP>д2QýWÝ{ývvˆ¥¡³Õ3B¸uá¨-¨ ˆ¦þe8V`hÁÍÜ_K=$6ŽàúX¤rTB¨ÿi±Y@3·¸Ñ<;hví(¾Y A<¥¢ÛÃ’ø@Úv›­å5G'k~y‡üåQo“m¾µ”RäW¿¾KÞú†ÃäW¿¾Krίϴ5nŽ:Bе<€ôfÙÒÔ4û'ÒçXÛèj>§7ãÅ4ݨ½±t¢Q` [Àkd4hÈÀM¯”w]Jl‹¯ÌçpH³y1ÉY[¡†è'3ëqA pý½ÒépðcdL÷ÕšMX»pÖüĶ9.n0f$‡‘1O‘ÌŽ–…–ÖÑÂ볘¡FÑÖñ9ÂAÊ›E”À°"™·kà<ú4Ô,¢1‚n7/ý KKL‰óg1Ã3æ!ò¸ñë¿ê:»áP_³ûÓv•÷¿ý8yðï=ÐäqÐ+ÛjoØ"£–>Öí²;”†l±ƒH"Ž> 18éc,²i¸â÷ð³‘"ÜÔEÛ»£ÙAxîàû°aåÆÚ4hÉZ_«ÆY›_„(CÓAgö˜û>¼ç¸êllžýF’E툆#>&ߥ¡ÙhZsO«û|<íÅn]÷L“Y-5]ËŒÐåÀjÜ 2àŠ†t5nhñ\á¹wc2£6pH2ç:—s ‹µVÓá*r`´R`øÑènô¹¨aGwµ°þ3!X/ZÜãú¯u”:“ã“ãoóÝ9Öü1b=Ÿ£íuõçeèåì¿1Ô4{S†èx•RfÔhX°@—H¾Ü6y‹l^¹Ãôöèdaêûê¿Y>tÊñòÔ½_&:ùxYýï7‹ˆÈQ§¼KÞ³ühyýqo7ʆÚ]úgɶßzpû½ûŸö‡p[yÏ¡ 6l(WKeÖ»èçå<’/ž÷YºÅýå)/|™PºŽ#Ál·;‚²dÉ}ä ç)+Wÿ›vôñnhÓ¼´î(Jn0k)V?ô¿®/Ȇj¦ûõÍÏ/kî¼SžûÊ¿0ŸµSF£¦ êÓ#`‰f“Ñ8uÊÒÈPªuAGÀè÷ú~ýžHË‚ùNjn ´O/"’CÐÐ?Ýš¼4}ŒEƒ,P‘*<6}/f‹Ì^³b! ÅAp:ÅÀUÎj‡DG‘¦™ÍñUCÏ·µ¾ö²8l¬oýwKñlè8h‰Tªÿ˜#þÿ¾xoyØï?XN>êÍrÔÉ'·Þ¾,¸³Ch6†1Âh”e:-s‹ò2?çã™®‡]ÔæÂÔŸ¿÷Fm°àÅ(n* HíýsZ]³ß6vÇU* J]x›¬Ýüuf¸ †.ê[!«í28@×rmhœhÐTøý‹*¾þ\¦ŠþM¡h!õ±ŠÔT›ˆEèØè·{Fµ‡Æ™öÅeº°¸œœÛL&.š&f¨IN5õm•*R¬V‚m™‘ÊÄÈ‘£Ÿ‘¡¡‹ÁÆÖ¿¨ŸB[ßÐOìyÖíÈ¥­Q¹i¤!Bu\QÄ{ˆÚ±Ã1E¤+B—Ú0—,b¬‹)ËÆØb hŒ*Xyó5?§æ3Ʀ¶ï«Éæ"Õá>2r¼SZ*‡§âоÞõO®|Æâ¹Ø\©õ®ú,¦ª­5«Úmã>³I„"Նޖ“svÃcçÐÉuÕÿ`.gŠûIRæé‰YԙΤYFÙfâ=˜ØÉÚwzÁ»ÏŸP-D€½E]‘ÑY‘þƘ+ˆ7O0”:@!AÊBÓéÔ‡¤=Í 2ÉU»ÇT›qƒT%—7T‹sìº^q°Ãú/µ˜A× =‰Ê ”e,½ñÚ¨Þƒ #D`̰Bf aý×{Qÿi=ê569Æ3@‰†ôG­þÁn‡>‡ô¤xxtîv‹ê?{zgh¨ ÞkÃþŽå þ³¡ûnFš mB±Ê{s«ªäßtØÑ‹ês_ú²iòûÁíy2Š"a3Åà R‰ÞþÞÉaG/¯=æYsç""rØÑÇË_µÜ¡(ìöÅLÔÔ ýqTl¨ïˆ´-V|m­¤Q¬ï¶£F•¥…qv_¸h†Ð‡7]}ËÐ'3à1Å õ%܈ãPÀZ&ünPî„Ç‚ó’PC¢ŸÓëŠA܆ )sÚO§z‹CjpíÈ^èô†ÃÒ½LP©$'_”ábtÔ!4MZD勳Ifš«bVö u-ÈWr +æy0 +И4{i¦ëEldÛm(p°š®™Jx\[CGÚ©(׃mÕ¶Q¤ÈÖÚ´AŸ2„–älÀÆ NDhBæý2.nb.Ö ¡c¢-‘£jÉÐÏù;ž#æ5àAÜ, –ÉhSRà<Ç’‹!'½»¿…õ_´"C®þ«9‚ë¨ÈÎqÐþpámâQ“h¨w¥šð¶cÆQdÁ½£— ÓöOALÅ@ý«;\Á|“§Ãaƒˆ4¦Y“WÛ*¦]¥gzO2(‡ovK›g'W–³Ôš`°H†þ„T!¦š‰ˆlóЇȩÇ#;=þ±""rýÊUrüß¿_Vü˿ʲýö•cßp˜|}Å5²õC,Øfk9ñÔÓä‘ßF^¸Ç³ås—~YØ÷E ]zÍ‘ÇÉŠ‹/¥[n!""'žzšœóÉ EDäƒ'/—íñpÙãåIJI®¾èò¹KÿYöÞóÙíõç\ð9áÝhÇð²Ÿ-ÛnÓ©|zÖsV„5Úœèäå²Ç³žÞ^óÊÃß$W_û]Éy$<émáï.ýØYíû~ðÕ/Èê›nnÚ-‘¿r‰¬¾éfÙãåɧ¿ODDö;ìRÊT~xÅ—äÄSO“cßðÚö¹ºÿ: |ø'ÊÏÜ­ýþÜ .”½÷ücÙu¯}EDä —í#Ǿá0øýgåøw¿¯QéjCSš\ :»þ&f(@$)^q gl"€Çë^·‡? ¯awe´§Sµ’Ôj]ç˜æÆƒiúð2sj+©Œœù¬Ã"XÞM±o㽯ÿRª€~Sõßiw- d6LµÓ~ë­rô;ß#'½åò{x€œ|ä娓ß%?ÿÅm4¡G¦–1‰¾X{[­Î)Ö½äÅ®j­a(–"!Qcj(Vªå@ZƒTpÙh]‹ÝÔÐÛè&ôXXæ­V¿ÑІÓ‰Ì9§]µf,yd©ƒL!¶zAʆ®&K3F ·ia·b©c£,&ÚJf æ˜`°-Ùª3õoHSåò|dqýÝ¢ü¢PŸýðÌ:'’8wˆ›{Ð-Bš#%í¸«£c-®‡´²uÖÿœÈuv¯ê?-F½øšÇïpGÙÓÜ ”¬v§Õÿ(ê,Ò™S|­$|‹ßߎƒtš§{þ§ç¾¤ûûÖ ´…$´€Š1 R˜Ø€¨‰8"(š(ƒÌ$*E )aŠE0…J4¡2H)Q0€P‚TaPÁV)ŠîÖ¤Þ{Î÷ǽûœßþíýÜ*¨2ýQ‹r¹Šzß;<Óyž½÷oÚqáÞ²Ãî{ÉËï•›®ºÌ¹È!¥IEõ§”¼|—Û{¯Xrm;—œyš¼üÅ“ßí´Ç>rÅ’kåʋΑ½÷ϧhÈ›¤Ö*W,¹N;yQ»à®¿éeÁ¦›ú>åÝä<’g?뙲jõãí\œ~þÅòÕ;¾.""{.ÜMŽüÀémON?ÿbYõØj9íø£ÝS¡M§7S;Q´ÓzEv´Àþè•WË‚Í7“]wÙYDDްׯËK®“[ïøºÔZä«·ß)K—ÝÒöá…;í ""_½ãN)e,·}ýn9ãüKÜõŽlL™Bô‚mª;«˜ soø{ðýÚVÂæˆƒAù¼`!aªŒ,éµ­a¬ˆü1º„Våºo¼æÐo²ø¹øYìæ†š/ÝEVÖwý[j¨?nÚDmˆõ!±]ß4KùÜßìZ-›4zͤ2Ûê H‹UC×Å'ó'¥àœ!?»/‰ùéÐxT7qí.qØu}…ZZ#(7mª\aÃA¨@¹+D‹ÓŸ•:œ‚!­“â>¢9Z5VêF7TÔ!R©qá–|OµMžŽØ\>nÆiÌtY’qk‹ÄòÎ*™P l`˜ê„º6Gp!é0ÑO9 K5M•±[Nu2¨RqªEÌ ¯XQwÌjN“1’H95#‡„õ |¿Ó„(OÛ'ÔÉòäL ª=wÍf\lQ´þAGUcs s}#uPBm$ÙhÃÑv)÷›q=?aðtе)¸£0j^§Œ\ ¥5=™õ_×cý—õ\ÿ9…MQ£Ý Äày Ñ4U!=ÛhÏ\ÿ0d˜ q~ý÷çGñ7 Mfr ž¹÷¬3Ài; ;.Ü[DºkÛ»ïe QÕHØPÊÚ;qŽ3E&Ûóìgn=E0Ž•Ën”•Ënlt­­ž±Eûœ»ï[ÓñÉ)[µzµ™Ä¯züqyäÑUmÛ–ó>Ùu—M¾_ü…ô½#Ÿ¸ú3òñO-‘‡¾ÿ°y8Z Þá)¦NÈ›%-LŸ¥Š)¹€œ\O“ã¸ýÏo+g\Ðé`ÿ}šuM—qá¢ûÞ'®þŒì´Ç>²ê±ÕòtFCgÞqÀ›Û¹B‹îÉ.C§_ú4* YqO§P¤_AT£³¬zlµ¼îÕ¯2ÇpççíÔ nÜç¾êµrûò{äýöuÁ©ˆ,DçU¯ tnS݈~ï„jf‘¤!- ]tôÍWqE;¢Ž¹©ñyE½ø‹.h(`‘ìÜ aC§ÀÈi÷Õ9¬ßú×mÔs‚È :ñmˆõ_ki×{×>UÙd“Ÿ3 ¢qÚÕZdë-Ÿ!g¿ÿ÷§Ъ‰! ×?ëÊÖ÷ÛÍb èŠSHø[PK…´µéïš5J ^Ût7’â&gè¨Yj¦HÃÆœDÅFtß3”šéw”Rš!C’&@§ÂÂI$Ì}Š€¹XÁü´ÕåsÌŸÑ1TKÃæ¢QªÐP $y·2´¶&§°Ði, á„‘˜~ž|h%þ õ<ÊÖ‰)QÕÒóLPmZ+=Ä­ÉóÊÞó&:ÝQ¨À, g>ûdÂp±¸Õ€éâÝÊœ½v²º%´­ŽSc ÓyÔT!¥±7IÆ¢Ûä%k–¨ÒÒxýš^›Ô,›s èÅæˆ‘ §é™µþ§ÇA51ˆæ22Y?›a@%'ÀÿìõOô·ðJvÀÀÍgô<1ën~¿y \{ÖeRófZÿ#ºwŽrÞø4A8…e]ê!° á"–'øH·ã ÖDÎY(¨¾ëÜ\~òÓ—ý^¿|øÎwð&ùÓ¾¯9Ÿ‰¹ñe3q¯åHTÇ@Ô©µ!ÿVD’ä¶€_ùò—¶cuð[ö +æùèÿ/9ó4Ùu—Mñ?𰤔äŽå÷È¢ãÞÛšƒÑhN®¾ôü¶ÿöÝïNµL½èüÖw¾+ 6ÛÌgÔ°JËN˜7.Y""_»k¹²ß¾­(=ô­o‘—MùrÉ;x“,:îÈNq‘U­6È„¥cÌô¾7-Å4z¼mP„ÃC#- ÒÄÐÊ›`v1cç5l¦øsÒÿÖýÁ€á®CóÇNuH«Ã‡†¥Úâ¿s‚þ]™r¯25cÅ Pë»þñœzbý™®~!­Î>&ßwÚñï•-·X <ú¨œô‡"?òccûµLó]Ÿ?¥Å›CY@Ã3Ír¼v Èé $¢ƒT:m†Ê@ÃÍ :ÁM²\,= m¶…60-Å"¦p^Zó;›öKÏeÚ\ÎYÆÓfÍ éŒB¾|&?Dû1ÓQ²ö^kÇQ&4›¢i\®IKà¿úãhŒÀœ eÓHrˆ‚)œÄ;Yaã‚E:;ÉqFŒ£Ø ëÉÄh|ðuN8çá>º¸‰¥Òáwqö5fŽ IDAT‡}x`µ-…îUf⟭‰íÆÛq¯®ñð¸rža°…3Ñ1ôÖ4ŸBMç@Ãap‚›„5{«q$œ¬yR¿Z#hD„Úˆ·ã=}mËH«Õéh†B˜Ùñ×ó]ÿyÆú{."§:3¤ÁõOnví|VÈ:ÊvÑ™¦Ç×6ÁxŽñš˜‚;»¯ñä™C#Ê&¿3U óbD¤ixºŒyF[°ØÄ?ßÿá#rÒ™"ß~è{²çÂÝäƒÇ!¿òK/‘o?ø½`ºPlV ìÛ¦z5ïáÝvs+Ih†ðpù³_!{.ÜMVÜ|ƒÜÿ¥›äôó/²Et(¼×råEç6c‰Ûîº[N?ÿb)e,~œÜñ{›Å}_ü¼üèÑG[­îp+n¾A–~ê""æg7^ù±0éQŒj¼)vò"¹cù=rß?/+—Ý(¿eßÉç>þx+6~˾²âæä¾/~^l¾™ìØ1íxMlžÇ¦€ÖÏV!;" QvNÏÜI.ÑREÚ"Òù!ƒT,kï‹pvwC$_?Ù·bÐ2l:ô=ºNÐhÀÚ…ÝC÷ &t»Ù¼…þê8·¾ëߺÇ0KØðëŸÒn™Ýƒtùœ vêÊk?'ßûÁÃrÊYçÈ#«sëŸMÖ%%|mÆ¥L'ÕxíˆdpEÔc9OŒRÎÍÁ@Ø”ó„"6}ÞO2äü¸†„jkÈŒ®(#5>Lg L ìlNÙÍ ]ë´‘Ñ×a¬Q#hš*"N µ¯¦‘âÂm懃 ïA‘µN™…rSRP°æä2^ ç¾?yê jQp:ïÐ=ãb(uΪºŠŒÄ`W¡PK@®œ.P,SøæÀ¬@ªC["Š–)p+d/y'Z°!=Í ˆX¼¢‰ƒáŠÕÞ0½r†0Ö ÛëT|Î¡Ò ŒÍ1Åõ4×I_‡¨7“¦!‹þ›rØ7ç`™7Ã*Õ2Èé¤Ô°Â¬ \#è‡Áº‹„ü¡‘…H˜Á£Íe¸þëÿ…õ/ÖˆÑ9 6Û4kýs½KP ¸1‚Èù4Y'“Ûz6A_Cllòþ³ó^UúwÇ¥ìRæAñÅX–7ß y¼\uñyÍaå²å ÷ž W^t®Ñ²ÄžâôXQ©°xÀB/š°s!‰Eªf²  /.8tØBj檰£ïËOµxi•&›"UÏ¢HóÂc¡T$Ùrº=Ë¡ÉrêÖj‡òFùB•¨1Ö¡[[dv€ú¡!.=S„‘*ÏßÍØÌ%¢ü¡!ýÛ”G¿kÛú€4C×qò¹ÉÀtx¤ÿ„Ú°kf]Ã,퇛zWÒõL³[Øe̸¨1mHè¾ 5QÖŽ­?k¨u©®ékÙ­Jˆ°¹œ)±y(Îå ÎÉû!] Ú »¬ŸuÈòa´)|þ‹mR[ÆNWÀºÐÒ†Ö¿ÉÞ‘od8ý7€Òp“7DC›µþù^QZyý·Æ ­ãú‡,žÿ”õÒàú§Ïp¯ ж–8ƒù…Û µÖÿ³Ùný«þ'§žµ¥¿ÛìÇnîpZÀ²^€‹jFdj-²tÙ-rÕÅçÉM˾Ü~·tÙ-råEçÊÒ/ý‹›¢[ÍG!ºÒÈPZXb“êc×1ÿþØ‚ [K—Ë0%ÊFH’üØõÌKÉAKhtÒBqyß§ž­‚E¬Nû-…,µB혱øÃ…ô¸¸p´Bô‘¡ž)Róöýß({îþ«rýMÿhŽ%Åø]ºXl£Kœ™ ƒ¶mê¼e¶~&çÞØs^Í5Ç(‘¿áŽÍ1`ÚŸcF¹ðZn«3 àÏP—:b)Eкäôýbg¶þþ±3B@ôéÉ®´ ÷¶×é)¿þ×ÿþZd<ž¢™Ó«ç•) Ö£˜M&ÉÐRȦN!‡8ÖÝ$¤‚ƒ:"[ÌŠ¥ºÁµ6#£QÔ¦‹fWÂòâ3óÛiR‰µ˜Y㈠qºÄ&•,½‡(1<0àb3r®â)÷ ¨5ÅÚÛ\%[” Ùb»i€ªå†D¹@è|åôBÕ#IνnV^MòT¶Jÿóvðâ4Mìbj¢Þï4?¤B—4lP‘ê…jŒÞi…Æ LÉCKhgߌF@Ù˜ŠeP"¸´F-»¡ùÃÖi$ñEwõÙI¼þUûe\é Ià¦ÑCDiBšÖŒõ/´þq­µõ_%D׺þÓòú—õ/)o† ÅF(JdèœJ× ¡[ÿÅ7ufý;CÜ&ÊÚ‚í}Ê»Ãñ„×fމ¨ýõðS·âX_sÄû?dŠHžØÚ©|uÅ/wí“âÆQXÙ}ˆtIâPœ†÷BØŠ”­kW2±:q1ÄÊáŽ:½PÚZ cè%N£µ¨Çã—RܼbÁ¯ïå)ÿä»’sáªulŽ?:uéâŸ4"þã?höãúç ÷ž ·}}y;–ìä‡t7mbbd›8nFðºDÄÃÞâtuÃfŽ,ø±`ÆýJ©çë`(ÓÖt Û™½î°øjBÙZ·ÈD.RŒ#›G­²qaëÇÃkÅ wOvýO>o<¥;²ñÅSý¯ïŸÒ’©Øt*°Æ©fÊYjhrØ©ÍÖI š†Ô&Ôù â Ÿ§ 6]ÚŒiSª¿ëˆØ\C—¼uu lÙ˜¬³14”‘,KáBD…›‘ZÇ®ÁXF°l掵). ôéõ?¼þ×÷O†&›õ¾þ‹$B<“ÈÚpc©X„h:5¨$@BíN6-}º™3w\á Ô¶¿k 7‚MQ1œŽ>Œ2A³ÃÅD† )ÜqÁâk›BO¼­.ðµT«Ê ë'°ŒšÔf "1dòÀ™;8¯©: …ù¼4YúÚ0aØ6ÚADøÓ€`œåUœËY3-À÷à1I1AÊ ;B#!-Q!­J 9rvËÒÝÞPßd´+ÙRíù@w@£ÉÁÜAÒý åËØs‹u|c"Ò,ÝPÿ»&Ó4tµTGÝsY>U<¯P “EÏ b—ÅQ±œÎ‡‘–d)b¸ÍÜ$¹ øpW¦‹ýÌ×~ëÃcÉV;¢»ÕR'k7hÑ®Û}4w¥Ôf‡­Z 6û*“ÿ˜Û}þ)O‡ÓâÞºx%›g°#ÎŽŽÅö¾è 7`W—$aÖ^ µÊOu‹³ÜÕâ‹?´1Ö÷èÏÔf—QœÞ#'ÁÔ­ÓžŠƒo£"·)9HóplovЊXéNxL-Beµ±ìΆSpºë{±ÄPL.j=ADévH ã\#´Qæ;wbc:?¿Æ¡Aˆ¦á¹àë‚©dÚ$ñ1±Ç%»Ègødw>ú«8g4¦ì1ý‘޾Ùì—6ŸL=䵃ÍÕÓëÝÖÿú˜Ö¶þ cš5A°A—VœuÄÆ¸^²[›6Ô¸ëkt{‹èd) €`“¦×S£ë$C·C“‡Ê^S‘f"˜¬ªFhˆï_w½F'  #ZÝ íî€n_ç²vRE4-¤B™7Yº¤' ¼H õ'QxdoÒ³Ó2´"½Š§ˆAÛÄødïLgnÜéêÀ9.±¦ ãP¯ÂÍŸÓ7Õþ³2.ŽÞ†Û¦Æ¦ñ¢‰¹{^—Ú2{"Mâ:ZZõZ©F„FéÌ6&kÁœq÷cjc¥ÐÔdõHŒÚ°¬ ãùÉVËÅõŽ<„M}±Z\WË`ýg$Úš™ë¿n õ_6Ðú‡`^n€Ú1¸÷‡¼'ºþ™š~þX)ÚS„2½NFÛyId5 vºªÖÄøGÝyXtÚôøl¦Ûö÷ÕL¡QW¡”#œ ³Ø¾»C§u`”Å@~9SQh}èÕ9 €Îám•1` 5ÜW,ʹÈÖýBdFMlXÃÉ?~߬»aƒmбÀïÐÏUzkhØî y,Э B5ט^[VC㫞ÿ“Ìg¡¿QÛœd@’k^¢R¤aáµ…Í êPvÅ×;6¤‘nˆs›ð¸õcVIêXø<ê5hQ¾ž‹ôôúŸ½þ×›W Pò,Ñ®ÿìhFB×/Ó-¸¡Ð¦Ä`SŸk§à\höÏ-àäfX'dwðv .P+ zm:¦Æ¢1C(Tuhˆ”£l£ýk°‚Ïd+ÞЙ ½(”TsOPóa2I(¤´5#-¥=wãäu,LÓâU Fc9]Å]iÅ\@ñiE1h=˜n£°AÚ!ÐÏÜ3 ý S¤&ëªç´NŒ² ­ŽÚmH'Õ9´×;±ç,ç.7ÖÞx߇6B•Ð&ÛPû"促n˜SÄ÷vµÅëT›'nâÉ2t<ÔþÀv¸ Ué®|¨g‰HÓsHðŒõÏÔ<³þ‹7£Xçõ_¼mù^ÿeÆúšÿŠ]Ù|FÔX¹ë׿X=`¤IÔõŸÓ¤ùÁsŸSnBz½o9A}‚š¦Ñ½£è¸‡ ÚŒj#¥†©NLB º‚aÛEñ>s„­}]ø^ैµ'ÎøÚ ÄŽÆ×À š¢…'ü pÅbŸ'¦®±Æ›6ÀcY-#"‚ů[w3Dª˜w¯ÈÓ‰p®9GýÈTÐCMž'ê’Ê=nø°iÃ&uZx"‹)<.ØÌF46ºÿf³|˜ˆZ«ñx¾ÑÙ›é]ܼ²õ9ã>½þ‡×ÿú!AÝüîÿL9FÓõ/ÕJA`¨`$D‹P×$=ÄÔm#+ü=Ø ›¡ã¯ÑÍÂ>0ñÌ oÒs%$KÀ’{’WB’ F¨~HR¤ª°ñ=°NMgMˆ%ÐeEÓXÌ™QÁóдY I¶X.µ—ª¡Æ8B 8‡Èh’Íëié0Ð2¹5R]F‘1j@JˆËâ“ÜöW5%ãb¨^­ 2‡ØÚi£BÏP¦ÀžÜØiÂc~OVÔz|›“ æ<¹Htê§ZCª›»«mH ÒÈ }šU´k%‰£ã¹cG7€¡PÐ4²îm % š.Ü£l+vì ¡ÀHÁ ª˜õèd±çf½×¿ÌXÿÔ ºõ? ìuk’Ð3ÌQbÄÝ­lLSjšÃ*uœšêÞh°SŠÜF¤ ²|´àÕ)iŸZ#}ªOµ&z`£}t/ &:ŒI‘#M³ ½¥¥„%II'ÃÙeͨ»ÖzR¥|Á˜Œû—žúGiWØ(wiez,°¸‰ì»ž¥¸Å<ùžl GÝ=¾l ÀFŒ^à”<1¯tz­v6ˆA§iS€Edvº nÖX+b‘É6ÎÏÛ<\´Üx¡aA)‘­e¢B©e™ŽU•ZÇžŠY3ÙÐÍ,âå«î`V͹DÄFŽ×–5è߯ÛÄäY½P°þî“¦Þ ÖfÐÀ¹§×ÿìõ¿~&“8î)IÖ¿$©†q”Úƒ¨€¨ …ý‚‚ùrÌîAš¢D`pЖ ÙÑÆSu;×ÃD.N‰PÆh~5XQã%` ^‚†.©#žˆs<*3l˜ù>ñô‡¨znA€y6HmaAªÉâÍ«ÄBzS8cšª£œ™‰5“<Á¯ât0¨×i(GªÝ1*ÙðH5öÑúY$K6(UMÝÜ Q¦æ–É. . AAz[ÿw¡å6^SÕÑÔRô©Ù~ô¹ÒÄãLó©NCÔšuK©ëôqûeB[oü™@Ãé~R€nU æ_3í#¼¿§Áçcä|9 9bªæ¶°Ø9„瀾åÄò$:çüžü±z rEÄëŠvÀ™ ´@ãf‚ç™:çmÙ«oà(j€ho{.Ž‹™¡pACÄ–ÓZ—±G!Ìö!ò€NsÓïÉ)= 6ÖhdÀB™ì¿Íñ…†¿×˜F$lËnøßÆå–(Íæ;‰3–`Í6ŠÎð"‰C'ÌÚH6@Ô5ýB˜%ð Àdí q…ËšAÁú®ÿdj=Ùõ/3ÖÛÅö×Îü [i¤gRݘêÆe<_°Aμ¶ŸòM‹Šµàµ®Y£pR¬EÛúvŽ}/Ö‘×ßµ Õd¢paƒ™,Z¬ Î…äq•/¬Ôå^`†ôÖ¹ UÈ¢,ɸe±0ÛvíÅ%Ôãy`š“E!Šil°¹`?fÚt7«¯Ðb-jа€G¥7w–vÕ‘žlŠo½þ×¾þ7Ý|hýKË*e›ï¨É±÷­FÜ%ÆcÃ~#ræév %Á\â³¾¿°peSöZ £1Ú„cegëÍ}ÔµÈÜW¡‚šÝÜ­ Ið±D ‹úÅÂ'‘:×4Iܘ$1TA÷ܬºf'îµQ±Œ¾gÀn¹_!kï$Þ )a” Ô\Ü xWT£Y‡€€_’£]25­ïÕ6Ñà×Љ’ÆÔJÔ!ªÑrt$ÖçD”Ô™rë?­eý× °þázà‰¯µ¶,²/v;ñXM“T7di!©µRôÏ ÊÎAÊNÀ+Mú²Ë“å+è)&T•>¥Å©sÔ…wŠ™Í–AQ9£‘V€é/8Çb·¿ë/F††ÄnS8Qb×±È zëžÆô‹ìŠPÔX`Sa‡d²S0xU߃Å9£š?Ãô4<ノû¦tÔ« F‰ÃYíB îs¤}Ñ«fõPh$à>+]çÙD`Ȩ^ÿèœÆZD/U:YJÙ¸æ!Ú„ «n/^H{³Me¡¦ ´¿qÍbƒ(ÛÓëÝÖÿ†h€f¯ÿ® ª<N)ÐL÷KÕ+½iò>ú>EzRnÍQ£ÒÑÐ!£Ö,®£@ÑE\j›¼N¹ý€þ¸b?(q• @<8Õɕҟªß5=>™u6Áç¢ã\Ó9‘–,š4Ôñ¿1¤¹f¥Nž[¡—ûTÜL­µ@Ì6” E,vX›ƒzÖ‹Dûc Ò°pÝPÒX£DNn-'IzvÓ‚¢é4[1c£Ã¨Z^ûõ8yMkò¡d4–u"h½nsL;bA‘ Q1ïun&äÔVí³½}£=Ð9š=è¾Z³Ì!·œ1“|(m´þußAC“>uHDµh¡ËEfAgKIÏiðÚ5û‰š& Фÿôõ/â,ÄÙQ±d¨áq¢õòþd²nÏFúáÏS^4¤áÀi8gàL4#Ö ŠÜ™Þ4T ¾ :¡¯þ¼´ãñ¼ fŒŠ&.q?P“‚.[QA§š,ºúw¢p?ê’yx¶é¶8‡µîfeQ”:%ebCÕѵHNSqüˆÙ®F\á÷átCSË”&£ÙHãñ(>$')Ûý÷”%tòêç+Á{S@/™|ÎÜÜ&ÎYŒorÖò9C!èaš§û42Ç=ÒŽpЩG¼æÐ­MB45P„s¦°éá¦Ûër }Ê`P  ü RÓõ¤¦ V[õôúÖÿ†0­}ýOB^ûÔÜá¦%H¢&ª?¬‹ÓØRÿAUûçIo&Ǫé‘ji¨Ÿ®ßßu:yG’‚7ìghrh{P·„ Seš›ÈÔ±¨ÿ,*Q¼AÄîs¸.›ÓÖôlÕßÀ¼Ž³Ñ°èl”ª©þE‹K-ŠUE$Ì8 uIœµ“í¤¾Ôb²\Z¡š çGR( ÇF€›§fPÀÁ‹’BýÉÃ"#[5yãýy+ø§(Ì4M}ІT+ˆt–ègúºÔ§õ©0‚Ѷ4ä•i„ Aª¾©i×^-–þ%Õ–òÏ ákútÏd§=éxF×ú.)>Iµ8ˆxµk5'—û]‹Å8j:˜B†Íu¨Ñ†Âi¹AÂ÷pÐé^ÿà ·Á×?]ߌôcC‹ÙLQh-®Õ¢­‘úTo‚’AP€Ž2†" aùó¶Ðà=h¬_™üÞ»iÁ³fÍ414ê*Øa «XÜŽ7Ž^H) 2…! zò$=rk+ºµÇ ¯M6ù9W„a³…Ö…]x>6 †Òh¼½O"æç+ìÛ&2??/£Ñ¨ñ¦m`Yvp¯¥Ë³è1,Ö6¡²saÀíd'°Yt“hª‡á­ØÌé6Úóg·;:f(ëm¹‹s"Ë9É&›ü4[ÙVøýú:« ËÆÐë ²Ce˜ÞˆÇ¯'Ö^éñêÍYyzý¯eýo$È­ÿ©á…ÔÚÖ´^3çüá*ž Èâd5ã¾ÞßmD2ÀÏŒÚ !¤ÇûÖ¨@T(üf„𬽳´¡€JŸÂÂ;Þcb¦Èºö¾uJíãÐv³Å:+·­„hE{WéÁð5d¯‰ðؘïtI›Û3ë»úÖ™`Kz=º(Fpí£À¬c¹Nׂ_+ñ{ìUÇh§¿öâ5øD®ÍðøSoZ¯}µÇ×~žßÏ™÷)y¢Qv[<Õ¸ŸòŸùú_ë>¤àÛy»‡¶jö±òÛ­‹'¶þ?pöyÝNŸÿÓF¨‡P×Ë"i[l÷Ë‚h,fp Î ð½!(ÆN7¢ušQvT¤äh-u±0ÃàKžÜk2?¿èBcóØxðT§àèþC¤)Ý,%‹æôm²ø}¨I`^'ýa£ƒaƒx¬´Àêt› Z•‘áÙŽF#ÇŸEªÿŽÓ×±ðljǚ5ÿá—N•«n¢Âødj4Ø$EéÇ8-²¨\§.òßxÃF ð¯ÆÝÍæá~³aÀ8†à¡ÆàV¤vM58Pª^gŒbx,Sàl$úôúZÿë¿þS’ñü¼©mû5«úQ‰¯MÕ@P´ŸéÄ{  Àì©Á³Øsüe–kšx-Ð:5=ŸÿDK'!‡¼4TØã÷‚ x¶iÖ>m/ÃRðlø=xƒÕ8oG\’US²%y¢g.œÂf}mÛã 7q´¢Z5ЊYXþ"´Ÿ_û«k{DÒ`Ù_gìG% kr^m œÕÓDåm|d#%}ZËñOm&A´ ÿ•fœŸÙµ?°Ÿ¥Î¸djxƕҬÕ5³eˆŽßZ×ÛçdþmÏ? …fna ·¦ïV5GÐ}O{M5[î¯Xoõɯ¤µ"e |ÝSž‡f¢àï=ÇóBzh%(X`(÷ßæ¾äA-…Ò`º Ni<#G«!ìg©9–ª„EY1ŽkX¬é”3Z”~„“pt³5ó†ÒÔ§Òz¼1PrÎ4ƒár­À+Žê4d~~¾õ‚…f6ôµ†ö»F®ÁÁFÑ$D´ØçÆ®Ÿ¼dÎ IDAT´rV‹îy™››3ߣˆWÔ¤)åMª£A°x~<OžBèÙÔë‹-ðLžLõe?¾m£ MM¼5¸ùüdéˆCY<¦8…ãe R½>“„ú/DÕøç|<¢ï½·QS@“,“œ ëHYMîŽjŠÔ~–Æ…±fÝá¾ ¬=³þlªy­z[@±3Ù;¥ÎÞ†uøýZÿt=Üö(ˆ·ÿ¿n¾€F‰\'·Ô2­6$ vK»©fÌv äÉ.'˜5‚ßM\ÑÑI'ÍZÐ!½½znOq!–8™GtGkrÓTpQ†ÓRìÈP1ŽdV@— úÛÄ#LöJ7Ç&Ã@×äØLL£¯ëtºDÓh1hŽ69˜b<½Ohq‘µ%7fŒÖŒÇãö^'¤¤Š›'D´ìµ™ ª”b¶O&EËÂxø7a «»É àßfY„ÂÐV£Ë¢ž(Aëh`6fjîá]êòÓëÖÿ†¹ÇN(pvýë1´[^³ˆ7`Ë^s 1R„¶¥8TÃ4À9÷×*ÒâÐÀh_Šy­T¯-qMD­RKivÚh^о“,bµyʺð™‰ÑhÒPÓc„Ë®L›W¦˜PƒçÙ…Û ¥­ˆs²r߇:±ÎeµÔ¦Ÿc! ¾AàÏÂ{'²¯‘0.g¥zs…ÈùŒšµXV óÎ…ŽsŠÄ†¢ ‚‚bÀ+OÈ›[d£˜ ÔÚïØ.Û4`uúÌ¢†aìÖ îÜê}YtŠCw¾$ î'g&¹õEy)e’#4=ç–UIÑ`V€µ‰¹6ƒ°VvÅkëŸtLѠ¼.IFj¬æÉtÁ­ÿ<°þË“XÿÁýƬÿ©æiÈ ³]¹[»«;œºÁá529æWÚ(Üáx¢‰ ¥—tÊM"‘óÄÊZçÁ))Þ|;U' i²÷02Í kt´ØÑBŠ­r±°R'0ý7¡è®Ö Y!¢BQž“ÕèGë¯ËdË]M(¯½¹[¡¯´5,úŸ^ÿë¶þ×wÈ”R’ •výws“^Ä{¤D`Z*Ô 5‘³’.ÎêÚ4IÓ¢>@‹™L ýœÃózh« fû˜üúY°ØFNÜ“"NÑJÄ Mi€V—’%UkõaO…Âm5iïÁä:*VfÙsGE Fæ;P^Éa  ±°Ä¢ß¬Ãj x‡ØP“Óž#É#y”»£›ø©º;>ÀKÊ)×7cM™7<ýw¨ž$3$b«l4 (ˆ°Ó6£»š ®Lv`Æöá˜Q¤ #oznŒÅ4åø˜sF‚x<6èŠ×¬«Éê¯qwL Çh‚˜‹ôˆý¹³W®¡âœ(l@ñ87›ë ÆÙºý•qßF4J)Æf|(ß-¬ È’Þ …ƒì)k·½ÖõÁ©k[ÿùI¬0ù0û…Á«0ˆpnp0d(œáH#§$óPkmHˆV°Kþ6Ň{¢p™óm°°Âìœ\w½L2îl6¬²+hÖ$`1ŠN\,l÷A›‘¦£¸) gæX„¨´ã§¨ê,í'©ß°±±àÅ¢HÿB‡‘ÑA2´Xm.Jn‹‚39&ç,sss­Ia4 Q%oè̾v Nv…³ÒÖ…RÍCÁh•ÉÓ1l”æææ\Så]Õ°q+&è¯m4øúaËéY”0D‰0èÔêV²ÉõÁ¬l¾†¬Õùü<½þg¯ÿõ¥Çc3HÓÜ…~-ŠËÀjá¨PìO ¦(‡ÍÎé™@v†yRcƒP…‰¤È ;bÅ §Ä¨¢ðf n%ªbà+¸Æ¹¼ ®T@šÚàˆ¨v5Ð$j”rÎâ³­wC”‚iñÝmh(³¶F©?9eEç²l§þ³¬•ÝùÉ6R•Ìt ÿä‹'ÓÕ L’r{´àBûkýY+xÉÍäÝT±VÐ2@G¬b¸ÄÀVæáAµPŠ™9€iÖRûY©¦!AËpntŒ5;æ:M·O‘D‹ŒËÛ„âd¨‹0ôàõŸ›¥¾xFúõÖÖÿ¸Ì\ÿ˜„TAFÙ²¦Ûóº–sÅMÞ4xýCc‚߃[n3º³Në¿<Éõvúpü‹Î®ÿq)Æ*-ÙS²C̦ Bš‰§ ç):ªGÀ"­O “)Š¢É2Úk37·‰ËKQMNÅùâ±Ûç9 ú]X´ac_ŸýÅ?6£øÛRs,Å/htžê>Œ(G;ÓæM°)‰Š{tœÒ×!ªÃ4 ýLlÀðÿøÝŒžpÃÆÚ nJº!2ˆv1’9ÓáôO·%¢øá SDš~*2YÀ&Ën††¢†ˆ!";¶+í:d-6 XPb“€h"PX°³‹[¼ÊþÓëxýoˆ{«‚3ºÎ°qШ]ãH›æùÔ¦u©D ³ù;ŠÄ°=*"B˜AÔ´2.ãŒlõỚOÅ41U ¸UÏ5¢O`ÇßÖÓÇnçšÅTÀ Òè>Up`Äç‘§†bÄÚ§Â÷ž @Q«œ}/²fÍL‹ý9S0#õ‡©K\àaÖ‰:_a‘ªE+æšX*Ü$tÒÜ\HÄi(°¹PjÜ,'´!F4}À϶ÅçØ5gØ,E“wEŽ8ÛÈL ݉\ì˜N‡¥H#5ÄËEý^׌$égã÷uÚÝ mó’Ò&.ð¶ß82Ø2‡öéÿX6Ùäçàýcʪ)N“c¯kÄÁ£"‘B³oÃÎöØhd‚õ_a[“CWìz¬æ¿mÓ‚ë?›ã&R階ÖÿØL®4ðÖ¯mFëtýf¬ÿºAì±ED榔ÁB:š”²ŒhÒ˜’˜â5œÌ±Â«q袠IÐdÜt 6"@ãIÓ<ˆ ±Ú,˜¡¸’yÒ,Uc±Ý?[ó%†²B´¡ªµJ+cg…ˆK­R…AfTkÈÜ·ßÕZ¥Ð½Ê„¤Vû€¯T›åÔ§Ÿ¥¦­…ÿLèþ„÷šHÓ0$&ç}ÖýŸ«Ø•,Ô/Zž<²ô7 &ÅâÉ Bä,‡¡¨Œt5ª–Ú©ZG2¤Âe‘,YŠKÛª°­ÒÑ››&C1CJâ´<|<g¶UªiÐLÑš’¤Qr&†¿öõâÑ·U†á¹÷ñ±Jux@jÜê0¸µX4 ¿O·O…öYC•«GÒø¸Dnsa̶ùá¦@5CHC4è&^¯iÊY½éÓýø5†2«-T–ifDuäý@íž[ÿäL·AÖ¿¬ûúO3Ö¿ÙNhâ7 :\„z ö€ lŠðßHaÁÏà†iaV«’iúÝDÃqªŠ9<%æŒnãûÐþé¶pŽóYÙÑ‹L~kP¶››s&¨ â™#U‘ü9Ú‘kÑš5kœ; Òç¸ÁA*™Rí°B[ðgŠXaÆšG Ñù”æíg&1:†º$lñXaƒØ3lP;e-¯±1›|frŒÍb‘ªßkm´c]£kxþ-…3›óÃî‚ÑßnÒE¨jmüú/†úÉHØ$'!;›p¦°EŽ}~ýWw®ðwÜl+r½)õIæÑDÓ}çp4kýƒ˜ÚhPü> \ï(MéȸG5ÔÉW4@°HR£\C 6ÈSËfÐШöº !JT,+b•À0¡0  Jt‹5O ià—sû܆ØÐýê ‡GCã5hÂB9¦²D®™¸ÖÖfò2”Û†¨êÐUÍM°éotA‹&Ô¨µ1ôgrHã†Â«¤UÁÂ͉ë\ЪE‚õ/@‰°ØVº7˜÷í5Ò²R@{4ô9¢VaÑ ï‹9—à&×hjÔœi3çèzbinHeÃ"8¢î9JÂãñ86Òk„À½†!ú6dýÍLŠXE†î¶A[ÿd”Ð4ZÙî‡Aw’Õß°±þ7:˹õ™Ä=«Íõ“g¬ÿôÖ]ÇõŸŸØú/do^©*ÄDÚ(š LGÖÆXhqÂúë^½‡,ýÔ'då²妫.“×½z©µÈ}_ü¼Üÿ¥›dÅÍ78Íòû¹±šåH ƹÑÒF |ÈsQgm…«i€°Pä׬q8Úd÷ øpƒéU´±ñt$k‚€:Ÿ¡l/Ï툦ŒŒ¨`×ÏŸË(QäU ; ;åÂÆJTüGÅ|Ïó™sŸÁÓ~>vD‹7 ÔqDÖå\DãñÔãå6q!Œ¿G4)BÆ”V¹®¡v‹7£zœØTbxý[ÄNÍ8ðœ#M’›)}¯RÒ¸±Z7x<;•ˆ3‰š·Ùëßç] 9Ú­ß°iÝÖ?ºFé¤Öè|¢°Ú$Ùû)æö•¬ý¾ZªÕèP,; ízÖrtÚÊöýF*Žw¹ÓðDl (QóP;”È$!Ñk9\3Òį/d¨6ëŸî²†Z&Q@u‹®Ñ¡Énô¾ío÷‹äÆZ9'†Î–‚¢ cSôVkŒ07ÀHbšÕc¸Æj†#ңܺô©5Õº¿™ý"¼q²z6M÷ݪ ÓqÎnû1”ÏÄ ‚”Z:â„HÒÙÈ6Û4 dlá¶)r²ÃB]§È&ÜêÐÂéZj7Ð0æ,5È©ÐÜ  ÙsI ]-¨KìøÆMhcŸ}sK h‡†yný |-Ǹ».nˆõϾ9)u´;§Þ)Bø”o‚P_€©íظ` (6,Ç¿ûP¹ÿÈý|GNxÏ;Ûçî¸po‡4aX"67HƒC EçÒ[{d¥Ÿ¡í4ÚÛ"²„NTLÅÐÆéo:©Æ O›6vþâìÄX¡6"(Ñ4?šòEú6˜°Ênh˜“ÆQ³ÂûÄÅ\tCÀb}ˆžQä”zÇSþèsôXb|v:ÑÄ<ÜŽ¨™ÁFS·ƒ'»óóó¦ñࢇ#k¡¢ ”ùùyÓð¨é_CºýìDÈÇ–¯ `•çe„%o¾Îë¼~ë?«ôD¦Ùh- P"hNŸòMñnV•s„)mûí¶•ÃN^$«[-‡Ÿ²X¶ßn[Éy$?ùé¿Ë⎖ŸüôßM†u|*&ì›6#@ªœ ¦ùu¨µA¨[gúžôÈïáfI…ìHíA»^«ƒ—6w¬íÐâ4Ò¿`Á¡ž:i¦<™šâ‚@ŠÓØ´°Œ>Ñl¸ˆæb›Þvnò°Â÷¡6>à±Xg=ÕÜÜœƒmU¢}aã…Váª!^ŒFð±‹tX¢KˆÜðqäæ„¯ ¼†ðXñþ!25®zL¢‡>œp‘¶ˆ?çFÏ_D­ÔfCu鋚rDŠ˜ž5ò?›aS‘ùñxíë_Ô‚yR¬d¦ ŠÏvH|– £?zÿÂ1RKi¦ŠÜÔ6ÍÖ¨hÈ-4UŸí„Bõmî£y£B辦 þ™†ÞTB™¨©Õ†IQ%×0 iÛ§L&4µi“'§ Ô7Ü~ÜÌ÷º€90´þ#jÍÐŒ?+ÒA0ZÜh2¥"2­X& –)¬©‘jùDˆ‰ ;e—3¤V! † ­–ä>Ë #99ý ÿjë­ÑR“§¬b‘ÞÖmJNW¢Y7hmš¯Já¬HÏJ½1ˆ ›ª<Êá¾¶mgä‡QIÞy°ŠkÊÌu@6ꆢ8Æ‹ïS ìvŒ€V=ÿÙ, Ý ’8HÔ!jâÂXÁt€`õóqÉo+™H´†ª®ÃúÏöšaD3´žÈú¯ub­ÿœ&q#ÒËn$îpãÖ$D–±ØdhNJYxð!ù‹³O—WüæréY‹å’Z«œtæŸÈÁoÙWN9ûʾ¨F ­úl¸XCƒZšCöÛWV.»QîÿÒMrÿ—n’•Ën”wðfI)É¥g-–¥Ÿúi.âu@ˆxu­ÇØÙ[ä¬a»-xGD±ëÓ}´˜æQ˜"êÓt"Äõ/5HlÇB9²³fTb<ËW¯ÿ´ÜxåÇiH³ô)‘ñÃ×þn‰,:îÈp‚¹ëK^$+n¾A~ùe»˜…5_X°#½ SÍ0o HkÓcƒ “ž‹ûOÔ·Èâ;²G”K¯ƒ›®ü¸¬¸ù¹õ³W‡×gEñùcô,Ò XûmdË×`ä¶7 ÝÃýÌÆ%žfL MÃ#DŒµbø^üÜ!¤’ŒZi"¦ Ê9Ë(2î õÏv½¥–e>Šd "ûHÁÁ¦Œ]Úý7y>„¢3¥h¤¢íÀ›hÿÝl³«§ÓAC  ÑÁ§""9B•´:hÚ¨BRFt}3ÅÎ FŒsa MP ®wsž€f'ðX(ì˛E½Zÿ‘}2X\xc“º”hºo¬ª«ÄúÒD°Æ5)Ô¤¹u’¼Ö›3.ú(Y$(ës3“X?S­­5SŒ†¨wÆbšÏäî°£×ÚÖ?6H3h å4µf&°³fÔ™m–#"š,`S†´A¦µE¡³Æ|­È1,š/ ìeÎPfæ 9$¶X÷?\kµÄëß}g†\¨ê­ë͹„ךk²†Ø¸Á¬ÿ4ûdמþß>ƒ¬Nn\¬Nì)ßaP¢‡ç‹3&@ÚØùý„ì¹p7I)Éž w“ó>r™ˆˆüÝ?ü³ˆˆ|ö¦l.sè>gsMJ :Ì *e,7^ù1YtÜ{幯z­ì°û^²ã½堣N”7ìõjg¨8jQ`¾=Bh1ƒÍuu¯Á)§Rçô¸tê^™ºgòÁT)-q®ïCÚ×õ¿D®¿ì’À1ÌÒœz¶Ê¼ƒL#‡Ø‘FB_¿ëoì/{øN7ù¢R1²ã`â$®árúdrñÍ´"Fx¢Ü RŠüÒ‹^(+n¾A^ñÒ›› #?\h㹈 ˜BÆM&6>øZ<Ÿ§{„,ØlSÙi}d×ߨ¿}ï,Ž1¢$l÷ÍÛƒÇj È»FÐ>OØ#*[týðqä¦tHtŒ£® mR¸¡gXD•ˆh§|<Ñ #¬?q¤}öúÃúϰþóAÉàææ,i?:Hé>cp>+­)é‹¥ßkQ:µªn:]®˜Ác&â áÑfŠ(Í⌠è»hý³=okŒ¸iѰXhˆ"Î}2ïÚ>§2…lÇY›ÔP;lô´Ö­ÕÝ_s€Ðd²ðÒ Í¢5çµ…¯}ÎAz™û<Ò‰0m s¯HÖ™5‘ý¯Ñ]Ù2mÒž“§™Ñë£×$þz2Ú”j>cl@¯1Y>™&ï…Ú ¨˜û_ƒ°¡&?‡»a˜m–®jj n²±v´·äM tÇ9Ð…ú3¼Î+lÔH™k{º_‘)‚Ó[qÖ"Ÿ9 ê‘ըƜ‹êQy§“+Õ5èfýç4¼­FúéX¢VˆQ¥'²þY{fPÑäYI„Iñ³®Mj€ÙŸ[úÏæ³>·ôŸŒÆ6É/b† ÚÓrs–óHþâìÓeÁ¦“Âp·Ýy·pøq¦©Â‘n¯Ím–.è~3B¦Ù%Z6Í5KáÈŠéC¬¡AÄCÿ<ÇçÈý/ÿÅüž'Ùl0À”-D ô&ŒºDV"óƒhÚˆE=n×'~ŠÌà÷Y²z{Ö„°Ñ»½µï"‡lníáb›¡HßH/DÿP#ñìgn#«Ü\ª]áýÁ÷³Ö›8̧1Ó°`»±¡fíOd—>„¦ES9ÜNü6*à}¦ëCŽ„ú]»³Œ¢íYŸû뺮ÿ2wà ·XH¥Ä¸IêIô7æ#­7%”ºN¨FÇiŠ–ØU«*µ ÞcÛ¬M§sÓ'µ¡VC†èúPªÀ¤7¢Æ7Ê3ÎmÓ}±4+qt»LÇ"qSD¹¶þƒ€ÕÁT¥Ê‰×`®Ç‚p‚}ó28`IÖe+ÔR¤) ol2Hëbš!@|\€fõh(" ÑHêO׬xjæ 9 ‹XZ’ñh6´ª¡a]‘ŠuŠdZ¡aÊ&íäžf²c ÑAó׬hÒAà úºÇ¢ÏyFfP–¡& ¬½™²†Úš<ÊÖš~_Jqy?.à5'gó>ôlBG¾µ1ë¿Ä×hËš¢ýqk–"ôüã>­mý7;yDS§Z œr§ÊÁ><ås‚ÐÞYm-2Ò3o°9BäˆÃ!íg'g™=AkìëYÓ“Ò¨½nÏ…»É\ÜPoÃÝOÈÅg.’½¾RDDxð!Ùû w™‰çW~L¶ßnÛöú3.¸X.¿æÚéçU²|庫eÁ曵×tÔ‰rÛw·ctÛç–´ß?ðàCòšßþ¶-ÿú÷Û÷݇dïß~—C}´ðù‹?:]öÜýWa[.‘˯ùŒ›è½}ÿ7É©Ç!"ÖŠ›o›–Ý"Gœ²X¶}Ö6rþâ÷ËKw~ˆˆÜ{ÿ¿Ééç]$·Þy—úÖ7Ë9B¾üµÛåÙÏÜF¶ßn[9ã‚‹å•/™ì°ý/Èý|Gö\¸›ˆˆ|òo®“Åç^(÷鯾ßï=A¾zÇ]’s–«/=_DDö?ìXYqó rúùË¢ãŽl¯?ýü‹åWÿm;_½þÓíX¬zlµ¼â7ð“ i“d­´' í;=GþúÂ?5Û󵻾ѮÍEÇ)¿eßöûÛ—ß#o=ü¸vìÙïfû®Xr<ûYÛ´c~å…爈Ès_õÚP×tõ¥ç·ã*"rà‘'È­wNŽÇ’ÿ™Tùñ£«Ú1\õØê†ÞÔZ妫>.÷?ðÙr‹ísôšŒxûK?uY»6WÜ|ƒ,]v‹¼û}§Êh4’[?k¯É²ývÛÊ¥g-nŸëg¯–G[Ý~Æ—È©Ç)oßÿ-øñ›ÿü÷òµ»–ËŽ ÷–î-W,¹V®¼ðÙõ%/’œGrã•“¯Ýµ\vÚcyî«^++øv h¼õ³WËmwÝ-;í±ì´Ç>""rõ¥ç&ƒïð‹?/Ï}ÕkeÇ…¯‘+–\+§{DøÚ¿ø%9ã‚KDDäû?ü‘œqÁ%ò—W^-µV¹êâóäYÛl-}â¯å¢ËþJ~qÛg˹§<-z'ŸõÊ—¿LnþÊm²Ãî{ËK®kÇJD¦?»V~˾²rÙÚöܾü9ë”]Qˆ½EÇ)Ïûµ×ÉN{ì#K—Ý"ǼóàvÞ?xìrýMÿ(;.|ì¸ð5²jõãrõ¥ç‡¤î˜ø}èÁòœWî%;.|ܱü¹äÌÅ­ø¾ô¬Årð[ömŸ¿ãÂ×Èßn[¹úÒó›®hÑqGÊAï=Av\ø9è½'H•*‡´H:êDyÛÑ¿ïÐEýÿËwÙYD¤}öÒ/ý‹\rÖbsƒ{Ù´±Ñó½êñÇ妫>nöoÏ…»É]÷®öØGvØ}oY°é¦íšÀF¢Ö*{øNYºìyàÁ‡d§=ö‘÷œ´HF£‘¬¸ù†vÍM®•ë䪋ϕ_~é.-t‹Í7“-·ØBvÚcÙïÝGÏL£Ç‡á=6éjf`8Ú„nD΃výçЬÅrNxJe«2ÓÜÆ XRBÒ§l² õZõU³þ–jГ0Pu-yRÑúÌNœ†§;pPÄa€‰Ã—™Ïÿ™ë.‚QÉ(çiƒ ¹é-6o<š Üâ”Æ¥—ùù5ζº;vÍ™`RmPVÜ|ƒ¬\6AV.»QVÜ|ƒŒÇóàÔ–ŒÎ›"+ÜîÍͪÇV·æˆÃP9XõØj9üäÓÚçܱüyÆ›KJYŸp´ˆˆ¼õðãÚû/¿æ3rûò{ä {½Zj­rñ™‹ä’w¿ïƒí»?tÞ…òÀƒÉ︿ÔZdÁ¦›ÊC?øA;~ïyß©RÊXN;þ(YõøãrøÉ‹Û±ºâo®k@JI6Ùds!ªÆ¦”":ï"yÅK^d¨LµVùŸÿP.¿æ3""òÓÿw¹ìÓ#·}}¹,>á(yÖÖ[É_^yœ÷‘Ëäü¿¼\¾|ۿʳ¶ÞJ^ÿ?~­m‡6N¸ W­ž8úåœÛw/]vK»ùÝ}ï}²`ÓMéAe‹Ð+–\Û^ÿ—WMб_~Ù.RJ‘?¸àY|î… ùúâWo“›oÖ,qJŠ¢púÇŸ_öÉæfwýMÿ( 6߬¹ŠâÍæÏ>v…¼tçHJI^¸Ó""rëwOŒîú†œ~ÞENc4D}ºõŽ»dÿÃŽmûÿ‘¿ú´l±ùfæ†ñÀƒÉ{NZÔþ}ò™çÈöÛm+/ñ âŽå÷ȇλ°}Ç…Ÿø«¶ló­E¡Þ,G£Q39ü”Åí%zMþÎû™pÿÃŽ1×Î]8Óüа›Þùùyg[Ž}LÅãCf£BŽ›¢È¸?+ Ce§5€¢î‰0¦<êk7”)?ÔбÐNBû1uÓ;¶ïK}pÑô ˜l²¼¥Ê3íqÚ85©5ÂIl4&°Óçž3„zÐ<¥w¡–î´¶´  φ Hã¤SÓ%Æhì·1$P±RŠ1BˆÐ¥ÚšºjipèlGP£¾»9pÐdj]ú\T\±–(77÷C ì^iš. Mo[]Å #P[Às+>… <"JЙœL›# CuNkhF }²=d;nL¦fh#jZ˜ŠWÉ™-”Ûd×·AUŠGvqÐÁ¥AÄ#jxl2tºä]Üœ†]'±iƒ@J$¯‹1îgpOkÍ 6ÓDŸÓc‘GÙØ„c“kþ[¬.*”1Ò‚ÍÂPCÔö¹X*j€#$ʬ( Ñ­]P’u¢ÀF߃&¦Xÿ£pÌá²rÙdÅÍ74jÖÖ[>£­Êåß\áLV­~ÜLV=¶ZúÁÃMŸóÀƒßkÇ…§­úß~è{m[n½ó®Éöï´cÓŽ,>á(¹ï‹Ÿ—û¾øyyû~o”›nÚ‹ÿÚ }êøä¿¿qßÊ6¡¿gÅ·DDäå»ì,»NQšË¯¹Ö<Œ?ù·×O^óâÊK®“|HV.û‚\rÖâб-%¯éÁ›Â+^ò"¹ÿK“Fþª‹Ï‘_~é‹Ûû}lµ¹þo_~O;‡ú=<ºÊ¼fù}+EDd×—¼(tRÈã(¥È–[,hY\¨ßºÿÛßm×d•Ú¶…›‹Èìo¤výMCÄhËÍ8S8£\"tÎã†7Òñ„‹›¤ñáCƒ~ÙåŽucR´!)qha>Ö)¥¹•¹ï±´¢Í™ &ä-M«¥Î4·¥êuH-òüüÔì²¹é¨S”å°YÒf*‰5×Q…4\%ñâ[m”z±[¦6áÙPìºæ§ocÎ#cø¨PEt'1õ ê=4DÈËRí,=)69¨âG,ÒÐ'¬%˜æfhè"“¨ø‹œ0#d©­Ç$¦c8†‡F{'úN–¦elœ‡ (§È݈öeÍr2Á¬a‰%Fg9Øéz¨E1‹$‹º%«rä S|ñh@3@ÍQ¡†ƒ\“¸ýkÛè\øøâ¶è~2Ínè½x¯iÍTòI’úÏB´Ð(v‘,²½vÉ0‘vá3M86ndÄ1뀶Ühô8"&ÏÈ4­9…8èâfè‘eX„®„ë²þÇeÜ2‚FàÜØÑ¾É­q uûF¡ J`K¨bþ®ÊéAý7B켆MR㺉@!KiÛŒå}Éyrß?/·/¿§é/îX~¼õˆãˆ¼Ö*sss¦;íø£äýömš¤Zë´JNÐ:ä¤.¼ì¯äÇ«ƒÂ>ÉÒe_–=§Ú(‘9»€a™M(f÷­äœijô“›ýÒO]ÖÌ,”:÷†½^MÖ—bŠ{.ÈÅ<äýϹç‚w¢›ו˾ g\p±|âêÏ´‚Fé‚쒦̞ w“Áì!ùhŽ_ÖœŸ1 ®´÷·†)väΆ6×P%µ°æpVÓ¿d$}ÿM3„ÍÙ.Ïž<úØjæäÇ®’¶ÿãŒ'"²Ã/þ‚<ò誶ãñ¼pøqrÆËËv~¼â%/’ïýàᦳÑ÷ÏÍ͹l ýûÅÏßInZvKk€^xl±¬V¥ù_ÿçÿˆˆÈ‚Í6•OþÍurù5Ÿ‘˯¹V®Xr­<ôý‡Ma’¢ÁÄÁ^h+o2CîfZ#äœeûí¶•#>ð¡F3zö3·±HÜ ‡tbš¤ŽhñöýßdšŒCö›˜$Üöõå¦ÀWMÑ!û½ÑX°çQóvÆã±ì¸ý/ÈåK®•½û’s–§6~Š”©næÐ·¾EDD¾zÇ×ÛjË-ªØoìùë²jŠÚ BƒèŠîs)E™^“|ýìð‹?/¬zÌËa…éLûC”ˆédøŽºÃ®>’QN8XÏ7%Hçc³ ¼æ°ã†K¨ÁfÇB6'Àu:Är÷בÍGü~'Ô­ÝŽ4 Á´Žoè”–œ˜Ú¬]ä3:DEQ+¦ã†J”èh•ŽÖÄY>˜ „· ß³¦1ײèJû7RÑRmÒäZS¤¦!€¼´l"F+%…×°o( “Î:~ˆÞ Nx '^§Ÿ‘Hùô¥ç·Ï~Ço–—îüùãK>*)%yÏI§ÊöÛm+—œyZ{ï¢ãŽ”í~[9ìä‰öcɇ/´Â]ùÚ]ßÅç^( 6ßL>uÉy­øú¥¿P.™3D(ÎŽÓâ6ç,g|B{f³³þ{Õc«å™[m%<ö9öwÞ.ç}ä2ùÉOÿ]ö{ý>rñ™§É;x³üéß'Ÿ¾ä¼éûcKkœ¶·ŸU!»˜§`Ò…ÛÖÏ{ÿ¾×¿z¢Kú•—íÒhz5¢•°53Üt}¨Zcé²[ÝPßô¡ËÒe·HJIÞù[ûÉ¢ãÞkŠõU­–”’|åö;§Ô½ b&7"²ËóŸÛ¶÷¨Cßf¦¡"s‰xL»Ù}èÛÚ÷ëü¥;¿@Ùïm;~˾rýMÿd²z\£ ×É{NZ4¹&ÏZlPÄí·ÛVÞó¾S[Ê37‘œ;5«AaÊLìŠèPÄ­g›qÞl¸"ŠRÞ°@SýÓà¢à߈î‡ßÿª_ÙU.úƒE²ÍV[:Ôr~–"t AçPEÓô‰é€¥ i††šiµØ`Uc7Š ºMÍ~ø­oÊÃ÷ß#?ú·û¦ß‘ -CP7(¶Ú¢£ñ”½B~O_‹9F0ŒöÄ릹1 ¤™¡“]µ.x5pÆH÷Ñ °ñBjlgtL›Ã!£ƒê:ojMVkÌAy¤9˜•DâÈ™‘ ;3\A©T‡`6ÍŠã“m´ôzÂæÜˆßE* ÊÄâ¼ý7¢Xë¢q§«Ãi–ÄROsÎm{g«˜É¾XsIâ þVŒ*â[¬¹ê™9aÍ« ­­Û±H½¹h¶RM³ƒÔCÖC™ÐÛœEŒ-9ë ŸÅÑúw(Z¶(Z¶Zjœ$‘:®!*bÙCÁP [*$3Ü`+FvP H+6d­ñ&+y¦ÁÍ\ÿuÝ×QësÐf¶f8@¢žòt¸>5êH̤Y©¹™ .ÙdÿXÊ[6t6‹(ÕÖÜÔ:6N·Í.—Xú\‘‰×Õ—ž/+n¾Ál»jŽ„NîÜÜ&®sÞõõûÉmŸ[b>ã £N”[ur?ý¼û¿t“y:ˆå<’í·{vÓïˆLl›•F´ãÂ×4mŽþ9ðÈe ¸?nj"1ù,µ›®UŒ@¢ëoú'Ùÿ7ö‘·ï÷F¹ö ÿ }ÿa9åìsäˆC’½î&{-ÜM¾ÿÃÉWn¿sº¼KJtcÆÉN”ÝR`’»ö¦wj ±äZ9d¿7Ê!ûí+«[-W,¹Nްׯ‡wÎYÖ¬YÓ³t✓+æõ»?e±\zö‡Ì¹Xºì9ü”ÅÓí­rÈ~û6tH­©µè¸cù=rê±GÊ©Ç);í±9ÞãñXN9û\¹òÂsÚ¹œ8 aŠ€;¾q¯ìñ+/oÛpÇò{å°“OëûX'Ûtô¡ok ÛÒe·Èâsÿ¼ÛJ“ì|üdŽ»"Y+—}ÁØ—ï¸ð5º– @ ™î«ê¼ØššlDØP)jlÁYV£¹€±—‚IbDÈM¥éÚÃf ß;77gr·DD~kß×˳Ÿ¹œuò rÊYçÈ~ôˆq'4Mÿ*ù„©ppMqî†Ì" óLš qç¡Ø¹O|Mîxê[›ÐŠ-PuˆòîcŽ—?>ý4ÙbÁ›iI¹DhL`/Sûk¤¬u+Vù· äì(2ZBÜL3@¨‰š¢í fĽMÉJb­º­BÓEÈÚkÂûa«n·¦ÅˆŠ¨ ÓÍ®AuyÂ;ýY iªƒVØ”dÓztâÂ.œF§`Ò_é:+ž ç¨T)µ13ÓÇ1™\ïª3ƒû§ P—üÀ8©aÆ ‰ÞMfNòç¢ÔÒì‰Ûö b¥ŸßU¤·;Gƒ²ŠNñýÁÐȪËj£¡’d(uس‹€>Èô>D•ðùŽˆt{VÀ:Ç=diù50i@c—êïƒæÚ*Œm .Ö?øqô?±”@Gíèníghô Æ#ÉëÏ ò”b´Ì\s#ÏàÏkT¾´îë_ïIèÖíÙ˜éü Ô…O™?;ÿá§êO>÷a ½e/wÍMd¯§ÓÊe7ÊAG(W^xŽì¸poÉy$÷}ñórà‘ÇËUŸ×4!M¬:5U@m‘jƒ|&Hi”¹Þ@ucƒžmÔµKº½˜ŠHVKHŸ¾—‹Dºð½9d~~ ó4¼¤ùiˆ(3ÃÛ`±àMÿ›§ýC{|(rPj”Û¢E1ºqiáÆSwÔ‹ .…ÝÈP·2¤ ‰4³šxÜ/5 ÀíE¤" 3úh›ÌîuK>üg""ÆAÑ‹Ñhb§~ÿ·¿+‡Ÿ|šù<ÖÔðybzk|p;ùØêwógb£ÉæL{C-ÚP0,7N| Í …ÕýÆk%šüEA°Üœã1ÐcÄbsý¼m¶|†œuʉ²å ä‘G•SÎ>O~ôãG½0 »‹N=q½î±¿{ÒiOhýÿdõ£!9óC¹¦ÐŒø÷"òÃoÝ»Öm¾íö;äuû(oú×ÉŸ~šœtÚòá Α­Ÿó<XY×È$NÛ¶jî»·ñ¾DE®pã1|0ˆO á6”52VP$¶5j82h†$ òjï´™ã}Mb׸F»ã¦Q¬Öˆ‡áµïãï (ò},¢d*^ipQáä¦Ñô^§WÀA õœ1#ÉÑâì6÷æi“®0棯ú¡ÒÄtûZ|¦ « Žq_•‹õ@nùW“4´þ1[ŒØ–›sƒc­´\Dl±°o÷d›4Ç0û‰ Rëðu6ëÀú²FÝ«Dw±†•~¢®a3Dåãýê2ƒ6åæÍZ_k[ÿ^:#ÀnýSÖWJIþßÍŸ1+˜0LFA$E¿ÙüMÇ=õépØM È5P ÎQøii †þ|é²[äÊ Ï‘¥_ú—v²–.»E®ºøA¶\°¹k®~vhûº­3•-ó˜Í”6c'‹áŠ@UÙú9ÏŸü‡þ÷6;¼@¶Þáù²Õsž'¯Û¯7@ï[ô!ùÌgÿN¶Þáù“AÓ´¡ièÞW¤ä¥Ùa q‰Bªe YwºlØœïƒ÷v¸áôÜÝäN Ðî”"µ($u¦çƒœÉ žÆ6éuüzÊr÷MhÂÚ:g‡9]£€Dñ:Ò¤¡}1[^GÙ"C÷;Ôã¤Â‹Ò²Ú5<£îß?m€¤›_`Á¯š¤Ñ!RÚ¦òN`8¥¡ü9q†Õ´8=Û6s*êoÐq÷%°Mæc‡³fp"¶yc´˜íųœº!'ÉQ±‘,ã6@JYcê!g ±;ú "¨ÏçQŽ]Ô¨fË£lµ7©kiõ`š»&"ÒD R #fƒC“Ⱦ¼=ÇÅ™g¡žÈ QÇÁúO¶™Øbë­x¤hªˆ;'øóÀ®‘l€ðõH™;âý2´Z«vò¢vbææ6§»ÃáçãD§ßº&hnn“†NuS+†UZ6UÚÐŒFÂm„fá¦4iþlQÖ]óÒ ÅoÔ ­íçXìé§ï\,1] ‹VF¡°°çi:¾‹Ð¨ GZºÜ ­C{½GˆP„Æh1« 6‹Nc0 ÍÅ7/ž¬ jÃæ|~¹Qc rVcÄ ð¿çз˙‹>`öý ,·|å«úRÓX4«ì)%°qÔs–$ÝgJ†s“ÿÙ!¥.¢fý‡Ú˜UDr¦Ex&~=êx*— k¨BÓSá³Fˆ‘[øÜ¬#œ ­ÿ¶Vsôñ@,,*)˜Rm‹£L3}‚ª¥‘7™nU…htÉRã°Hz‘ù® 4Ù¹ö¼× Ô:YgÜ %V°–¨}V (“CTW³¾|tnOªÓÆ'‹ÕBUB²Xô®RCRzîO{f1zWmqmìÊñ#¤*´"kŽÑ¶©xj×À D×Úgçä›ÊµÑGÉmÍ!q)fõ„P9 6E¨ÏnÆeÀu.§Øè~ë¼þ§ÛƒÈO©%D%'”¹-´ŽæÆ€‹+Š IDATGF·M„"A‘‹7œÑµŽÆŠÚ¹)h`Ü€”^×ÝYP5”)ˆˆDq£Ìç ›]n²ðúç&‹Ýçôþà‡?’üñùSDhœuʉ²ÍV[†ëà š¦ë?häxý·‰¦ÍRe~•MlgÍê8¥½…”’•{­U¾óÝå¼á-ríç>/¿äSF¨m‚÷‚¢” L‘?úÐ"9sÑä ÜЧ÷Ÿþ‡rýUŸ”ÃÞy(L³;ÔÅÖ©? D@¯SBZœq‘ÿa é0®¸ªÑpˆSW<ú¾ˆQbtš¦T5#Ñ‚ï@B´¼9bù",w ¨·JËHš K¡éóÉ"IÎul‰¹PÆé>Û:·+£âPŠÚ b‹Øœ²[ŒzT£zú[|sàZ'7—´ê ~C{¯BTȀˣݔÃ;ò!‡¦Rœ×î-0”1Í(Ya›0\j:B“ƒÔZA ‚ìXÉygÜä˜{•Ôab»ôµÑ;™"‡.x‘ó¯AŒÒ°Ýµ¡ä%BQ“Ïüqë_Ÿ£ºA ô\¼.6bú¿f‘šÿ2nçb£KUDE3wvÄÆu5×DZäæÀ¡M‰¨„ aS=ËË[°öfjžs ‡ hÍ¡«ˆÊ"’æÕ¯ÿqÓß‹À”€ÊÙLH6M-83Æ…(bã†Ünö´‰RT†/¨^Hf÷@š 3â²yš¸µB’lÜu;ÙÐêP·Ä@.n£Ž~¨ ÇLFPð4-TÙt€ùÞQfK4Ùdž@›n†Å…d¤uáÂq転Æ$*”YtêÿÇÞ›†ÛU”Û£æNŽžs?’G!—VMBs‚$A‘N@@Mà‚´ HH'zé‰G‚‘&DI‚ÒJ«$ ‘† (°¿ï9pÈšu¬U5GùÖ\;’û=—\ÂÓdg­ÙÔ¬ªù¾ïïüoážy¬”.§ô¸Bf‰9¨Ä²&2ŠvXNÊJµjjX´ä¡ùsšP‡ûÖÀÛêß I'ZÚa%´‚§Á·&sœ$«:Ï'‹~¨¾õõïLäN+‹Vi%›öú·“>v[ ᤎ@BNûÁÅxâ™çúýRZY\mýûO\—ìµAÕФ2 ªþv$uå-ª–›…Þ£]\r"ò²ÚDÎe+P/ìovž_üæ<<· x:ãpÚ¹çcÍÞ^Œüô8ç°fo/¶><ö&môß‡à‚³ÏL8÷,ŒÜúßð‘ 7ÃG6Ü pÝ”IñœGrw,>²áfx~ñ ¸þÊ+j…¯4°ª—š#2àlÜêRyY1«§þ&•׿„©ÖOÈIQßøÿ8•¢.—ô…õ.gÔj‰–,·¼/kUtC9N}UjÑ"­Œ'Þ<fUч=OŠŽ¼9­_št¨äºHm,0¼¶É¡†%2Ë¡ ‚uõü¡óGyîÂèYBšòõ2RSC¢|¦¸FbQéKä­ù˜ñ?Q”‹×»÷«ôe-y ÉKR0í¨_¶i®¢'BÄÉ]"æ ‡"±^5äUêƒÚoSë"ÅúÐTtÄÓx}¼ç“ @+cd(PÇéá~ž U¨‹ö€´j¦«JaP–èæÄ‹‘%6gmSV$(Ju³?7x)Å)W·’%ýÓ•¬LŸƒRF{´ ¼ÆÑΘÞib¦}ŠÄX½>9ºƒ…(]MixŒ\ñÏ5ãqÔ$Eoå j¨&š}³6mªoÒÙo rãÕçx¸–°7í[t âñt8¦?ZçÈöLCÔ&5Ue@ÜÐ0«oÊú™ö.¤ë¿'™3Ú¿ž 'ÕúŒu½­õáÅN;ÿºæšøÛ›}8uÂExåµ×»¢qï ê÷ú— ±Ò"ÔÃB†“D®f6¨ŒÒ×,ÚŠ/( /úù†l€7Þì“€±¢{¼Ñ×—ŒÕîcÿG úoºåvl9|8à=v= ?¼ôòˆ¸üôÚë±éÆÅû¼êÚ©˜óLJçð»Y³±éÆV&ßBñˆ1°$7OŸ4¸)ë±”¨ãŸyú}øçl¨š$>£ ¢HâTÊ^^z_SËÒ‡ŒäK÷÷&fC®°¡¢µª¹‘±Yj-Y‡P>#¨¬OHRÍ"AW"½Ë¥”·¤q¿Þ‘$‰j]hò±‹¯܉8³5T‡}|P‰äÄ'¬ê>'`ŒÚr¯O­áumx³ ’J’§4¼€>hp]ÛWeÜ“ØFTº_©}I¢ÉcÆj½=iÀž¨²jšÚJ¿P!åã&(T²i¡§V2e­»Zq±ôu¬\ï©ëÇúG{O ‰÷aœðž*W—ž @E rÑuSÓ¢ÓóS¦’”„â„dÃûï¼ó_I«¸XÊm)Í.íGjµVÄk ¨P;+©FŸJ!ˆû—4 ±P1eÊRÔá>Œ\µÕämõ¤0*£IBps‹Ðª@áz´wFÕÀ¬D€ÿ®‰!÷)NUÇ4aQƒVË7#G=ã¤J)kÜbõò(MOÇÁvàó0ÊÀÏ”Ñ ¬xK$Ãz6ü»5¦Ú£×gUˬíµbtÇ25eªŸÿ0nï¼óNr"¦½=¹keAŽœèuï¹ö#ÿú!\pÊIøÐš½x£ƒ½úúß×ÿªBƒú½þ éaƒW£¾qBâŠxÌ$(…ÞëêFŸ‘Â×1åõÞw—¢díq/(1ÒSú²F·St*)`¢Ÿ*‰%Þ¢À–ˆLø4Á«% Ü+呟«Ô‹˜nJ_ODÍÏÉP´¸ÿ”>ñ>ʱ9aäž©q*\b¾š¤†¡î©öçDNžºlâ Ô$ª+„ܖζúÌ“$°phYr^3ÞŠ¢ ®h‹#t’‡}›çFgÌzBÛÆ{ŸW%ŒÈ°B\HŽB/O;i瀖‚\LVÉKûebÑÜ<€’Ôã”Vµ‚¼‚Òóõô ˆÞ=ì.ö²zS¬Š¾Ò¥´Ç@E rh‘¾¬+!ÓëÊI›òñÔ£Å:Ošô©ImpjìQ5ÂTäK¯U{=%½lQµ©ßªd„ë´ jË7‰ï=ܯÉ[G½/I)­ŽògrÓMˆƒ*ÉYA‰õ\s¨VÒ¬¾Vü<•†¦ŸÓ$-×;£ ¦öòä\¶-Z¥Õö„Òê–®‡sN:6RàNp^ûÛ]×ÿªH€VjýS@«¸pfõ7qv7˜r/áþx)ˆƒ ËIœ),|}?ùùµ8ÿ¬Ó0rë܇ç&UÓ Î9Ï/~=ü†꓉š+ ¬»îGñÆ›mߤ7û–ᇗ^Ž+¯¹Î¤¡yNF8ˆA[• ÅÀGÌQ“¦s_ jFT¶ ΂>U¤SóT¦ÆÅëʨ¯9JÊ çÈ­Ãw¨l(Ì•]Ô¢,úpnÍæúj:.5›L_×¾KVÊûOçù%ÏmcزÕj?Cp.@ÔÅÐßÅæ$•’$©¶êûÎ×E œgÊdHä$±£Þ—DÍ+˜©:¡²¡î‘d¡¿¬D§(Cí<µù…º!g% ¦²– h Ö )¨8£—†ö¯ä<®Žn)¥ý»Ô€5Ù—àSñ QÕ4}“:÷ö±¤¢E'FÉŠtïå䨆Ôè¸1¢.Éh²F ÓU‹}b;›þ <_øœ¡`Оw¼O´“£VY¶§séÛcÑ‘õ\ „*µ¶´`@‚²„Ï(TZŠT)ÈYÈ0ˆ/4‡QŸ*9’—÷xfÖ]˜1åÒx"åc‚–Nˆ"¡ù? ­2[•tM*4) Á¢E²huá÷‘[mggß+&œ› !|npÏ%S Mû•¬ :Mz[IÕ[|륧#5Ä´ü‚Ôˆ•{`”z¥•Œ+V˜´*íQ:l¿/ãéîLP m,mµZ˜>e"fLiœºï—ðôwâì­)5UXõY$ô#V@S¤ÎòÓṨôH~&‡Œýž™u—Ù§ÅÏϺ/MÒÃ\³ÐL¦lñµóœåk¶ä¬M\e¿5Îõ)ò¦ôG}vê±ÕjµpïoÃ_^y§ýàb¼üêkýZÿ« ªÑ6[B£k»§§B$‰} …#¥u$UhkmôI|xÃÍbßÍG6Jý‚ôÏÞp³¤/A}],~9ÓRà€;ïý-nýåTl·ÍÖ1p8ò°ƒqÄ!á§×\ª5{{1áì3Ût¯²ÄvúD$‘ß“‚~UèLGT‚‹5Rsâ½*Ü"Ir*,éBˆ¿·×nêÉã„¶„"ÊBtÑ|2©y âaúåt(KI€íP—Ù/SÚV"þ ÉÑ+Š¡KÑÔý‚”öç©/ªL5ÿ<îß¾ŽdðÞ’ô4:—(­%ˆ‹ô„ï$2ÐôÎ’ÚNæ'ê pªˆQ'ñJÖájŠŠ9ãïä]îQ£Ñ%”T/ãÇëßðÚ=@EbàÑÓ™óÚ´ A=‰ahY–8p@–¦¢^>œ4±:ž©•\‘ˆ2°ïSìœ6½˜=Dµ+“ó†dè±ûàÌãÆÆ£vIŽÓN´zš¿¡ºÌ4+ÑPï¦Æ9ç0cÊDxc¿qŠ¢ÀçÍǦ;|!Alø|œÔªökŒRø,˜Ôò™±šå­—¡¥\©ãÆO%§-¯¦7å<ƒ¬dÍ¢+…3˜ &rÇ´üì3ñóÕˆlålMPƒ9«Þó¤ïŸ7X;íXMH€Ç@ûÖ¼œ’ ©$4Å/G[c*‹9´×ÿÀ†õoSí-´ªW/G_°úxžsR¥kÑ2vÎaÖÁƒÿ)Y{ÝÖÿ»ýÕê U‰W–ôéqc7¿ØÕ°PùäÜ;ÄNîµ¾—¾ * Õ¤m‘ôS%V]éãõ”iÅAÀÁß8Gv0nùÅõÉq?¼áfT•vx³¯[ Š×Ÿ @»ÏçÊk®ƒ÷9¿™y#^}î‰øýŸ^{=N=ç¼ä]ÃQ½©ºìÀ­Äð4 B)Bà£Ak’…ÄÆØ— êÅ)ÅKÈ¢¥%}>ÞÃ…ugá䄊ﵤóè¬-¨w‰}…"š«ªsZTà=¶jÛ­zÍs´ ÏÏw hÙ0·J\}'ñBeô _&î÷"aŽ‚Ð£$ê KE\Ô÷Õ¶êçt|-ò|kïØBšìHÛ=UéÑ>˜@ƒÍùãXÞ=*m_K°U¡MŒQàP"µ%Pz`Îþ6ŒJ88xç£q©&†Œ*rbLU#^k«LÎÁ1G"?#ɀϖm/¬pLK6^ *w…àkbSÖúgÙëšc½ [%Þ3£/=zôäßÿ¨«Ä­&f©¿v€10°CCsQ1.EZ*º–Àv.T”C0¢A_¦—öùHãû) ¦Çô$h‡سé£ [rØJÁ ÛŠŽoJÓ$AÏk™¥²Ç‹Uµ[±bXKV¬DË D9¹ÐkáÀ™EvÉáDR“Nv4ùÑkmŸ§þÂåçÃÏHÇÌêÑ…ÏדƒV3Ø+Vt(/i`®TCK„MT9Éˉ hR¥I³•|Zô´p/AZ»yýˆ44VžÓÄ9ê|jzö,ìa%}¦¹(=+Mâ­ç¼2ëUÑậ_–mºêhJ0T´8ìáEÇŠÝ ¾§¨ÓêBü3 ÎÕ¾CΧÝ$¼À|zX®¼æ:\yÍuieu ÇîcöOz"8YÙ}Ì~‰AkøùÈÏï€SÎ9–ª‡5¡¶…¿ç”ÑC’dÑIżn8Ÿ+èd«RóT¾†ÄDÕ ÒÁ@§sT¶Ú1Q B8Q–ƒ"Xy6eõ‚mÐ×l«¬šå¹ðÕ™\ E¤ÄA’›Š^åñ‰D(!$G\´óè$3^Þià‹D%°è驨•1NÏ yœpy2Üõ©Î`JEúhöMIÖœˆ–8Ý[ü€ÊVç]iN>E3ÄCHå÷ã>@ý8a/J(„E¥¨Æý¹žU1cÊ™ªí1M®¦`Ù¥ká(AÇÅC­†^Âès)"‘¹^ÿ–÷¤&/š(qÉ¢³©o‘&U¼/ë1­ãh–¾w óx`@OV´Z(ŠÕ$ /‰ ŒÀJk!Ñ}7ü™€ºp‚Žþ=$@¡ïˆÅ R\‘½:î™ö3<·øEŒ;õŒ>¿˜tΛ8 gtüÌÇœ„?Î[€S&bË¡Ÿ<3ë.ô-[ŽOq_E3ŽCÆì¿óÝK&áú›nswO½ ϽøF ûzÂ÷.Œ«yæþf&z ô-[Ž­÷›T oœ| ¶ú‰xÌï]:g7>þýÙÙwã¾ÙaÜ©çà·ÞˆÛî½/,]Š3Žý> Ì_'àoo¼&Þ§sÜ>½ƒÛç^¼d)vÞÿkjqsï˜?Ú5~ž_=ödüáÑÇàœÃY'½vÞ·Þ{?³7Äk~ø¶éñ{çMœ„ëfÞ‚V«…+.8 YÏ-~;ÚpÝÌ[pîÅ—áÙÙw'Ïå‘ù‹âuòñx<½÷˜{ÇLÜzïï°×Î;ÆÏ\7ó|÷’ËcâtÈØ}’ñ½næÍµà[UËÚÁnZíú;qÞÄI¸vÆÍ8ë„c°×ΟKîŸçM¸ö{¦ý CÖ[7ùwž·áyüé77ÅëfÖ]˜·èIŒ=ê8xïñ“|7ŽWç£N9;¹>O{Ü'ãšé¿JÞLa#Ûzó¡˜vùEÉñ¾ñ?ÏJ. ¥Q¸è1M”“:OÐuÊ ²*óñÏô¥ Ï*·þ9ÑÂE…S**†D'Œ¢!y[U^AášV´ZmJÐõ8­™r¥X£2Wse—ßcƒq§z[3=t. üã±½KÍ+CPä:Ž÷Bñ.mŽN~îªà0ž_T¯¼ó5G¦º…F§¾›vS$t¨‚ ¨ÂcDÊÊ$øõ5ÊU iq²Љ8·9nP”LŽ‚vƒ~_ï7JtSá èÐZÊJMôbDºó¢Cߌ $Œd?aôµunõ¹NŨ,«Þ ¯àÓˆTàÁbßQŸ«öôD0˜ðÆ}¥Õj'B„&”Ä€i‚F²åÜŸæqÙê$^ì³ÕY?ѼṪÅ3IdjªK{…´O—ˆÞŠéqDr .ޤ»µð.®õmòi’Å?OÖ¼«˜†D«¦¬V¸z‘,î£ çIú~€D)OئE¨ä|œ¬µÒk‹ÅÞV™¢ôbØkœcgè÷}@¡ç)0qŒ£ vO0@u•$v«Õ‚+•Ñ£U¶’bÉjàT$ÂVo ÷1cõœ¨\Šœ”‰ªsaU³èœZøõÍÃÂ&£wÃFÛï‚y‹žÀ§~ ===óãðÝK&6Ýá Øzϱ1Ð?dÌ>ØdônØxÔ®øÞ¥“qÖ Gcë͇ÅE³ÓöÛâÇ?ŸŠ¶ß?¿ñW8÷[ßÄ-÷ü.~§ïïÇŒ)ãõ͘r)†¬÷1lºÃ°ÉèÝpÞÄI(KGíŠy‹žÀ¼EO`£íwÁQ§œ]½˜üüÆ_¡oÙrì¹Ógã$±ùP Yo]üô3à½Ç#·ÏÀŸ>ÏíLŸ21KIš{ÇLüyÉRlöÙݱñ¨]q߃sbðøÌ¬»0wÁ"l ò×áwV‰ èM²NMQ«æÁVkÑÕJ+(3&¨ÕwÚÁG«VNagÚÔ;ïÒ«+~ùm÷Ý!ë­kök„@é‹;}ß»tr<æÏoü/YŠÃ÷÷€G=ëfÞ¿ÞÄÉ8ï’Iñz˜óp¬ðz‹aØrè'0þ´sâ1¯¿éV\Ó- ߨe^.s,Âèm¶Ž?;bÿ±X¼d)yl!Î9éXôýýï!(Š×μ[ ý¤©,væñmddߣŽÿ¸SΆsW\p/YŠñ§[! —LÂâ%KqÄW÷M¨Cc:EQàšéí$mÞ¢'ðÇy àœÃïç΋÷î§oùruÊÙèééÁ¹_‘ˆ0ö ž|½k¬?ÿ½‰“qÎE?Ž÷4ësÑ;häå~ßì‡ðð¼(Šgÿ¨-dpØ~_²ï—p߃spÍô_Çs$H½w4p·‰i°=æÇſϿÙEQàà1{£w50ö¨ãc¢ðƒË¯ÄõÖ‰äŠ+Dƒ©'EQ`ë-†aË¡ŸÄ1gœ—\Óµ3oƈáCÑjµpÖ Gñ<íyzæ-z{ïòùämU–%¶Þ|àÉgŸIß‘ß93YGŠ®p’zÈXjœUâ´WÈò ÷«þDÊ We½ðe°Ö?ÓÜ,W{5VA¦Þibf©®lÀ·2t8‹vªÁ¢Y1/}âU’|þ-rÑákÍ̹^¥¼°Á_Ò¤mðù•š‘Ò—ì^¤”ªUFºÜ&[L¥p;ôÀø–†ÜZBÑ hK_&)Ëc'AYP)¥×LbÎhs¾L“Cv¼,[qˆ¢ A©=J¤VT5¤&Êíúš|v¡IQ’ôÕ ‚«1&á„’Öu¤<Óu©}‚Ré´—¯Ö·`"´e hÛ±DXëô›øúï¨RY ÿÛr IDATöœ1u-{}fbF²æDct°Ca M1 crCuªUB;T×DÌ;Šm*t¯«óóD%ÍÙE óÏDy‰ºÖz‹D€¨FÝÊô"%®²^‰Šot_ñ<ÎN´¹PäKŸ|ÖÅϤ¯ ©±©…”ò1X*=œ#^+*éì(<@cÌèhrÏeê¿¥5ìÒNú’õ_ú”^G’èáºþc¡®£æjIÖÉ‹¯&=A•M NÛÁÑ éË ¸(OÍ¢,e&GEôñ þ?*Oh+Ýxù?ýlüÞ¢§žl½ùP<2zªcö„3ŽŸP©àÙÅ/v2~à7ûj|ɳN8:¢ {|þ³í¤eþ¢š .hê†rí{úé´é˜vùEØz‹axä±…1|(n½÷~xïñѵ×ÂõÖÅ3³î2’ÖV­ù}óOn†¹ O«?ÿëš½xnñ‹5ªÂs‹_ć×ìãî+ŒyßßÿŽ¿uLsxüég:Éßp<üØÀ9ô-ÿ{rm}Ë–ã/¯¼ÏóÂÒ¥è<(AfÎ9雑r¾Ñ }õµ$ˆî[¶¬û1xïÑ»Æxhî£IoÔŸ_Zš\ƒÒþåÍÜ8éåî?œû/¯¼Š›·““ Ö[½ƒ%4?Quf€^–%†nº1ú–/ÇÃ-H¥ßüö?pȘ}àœÃÇÖ^ ‹—,­½<ùFÜ:¢V‘þ¸`æ-z7\v!æ-zû?±^ 68Êú¢µ(hÜ—¤}Y‰ã·Ðâ4‘±Ì[Y%ï]ÿŠVi£ÞB9¹õœR}®üãûkîxÚÓdqþ•ã®·Ñ(O{h¸ZèP7a¥JŸJp«YaÒƒ(p¨K¼&MÃ>UDJÐ.‘ùŽ*M®j†nW4‹ØÈî‚&+%&r;6M„[N`½<ÞUó}‚4ÂÁ»NŸIGÞÚ9©ôŠxAÒ3ÔIâ  üïLos”äxoÈt£.$Kô³2gjÚ u]8‡Rè§Ž ;V‘/'§m­ÿ¢§èÄ^˜}¾ìÐÑ„—ÙZ\"³ÎýV±‰_cBnÔàÖó8;gˆ¥ðù¼Õ³åSeE °„Tzžƒtí Œ»Ò§ À{»¯F…T’b' ”øDªš>6x¦BFfŸ"‚Iú<™Ê†Be‘"·ìo”3åqbóW¦ëñ¿%=V>]ÿ(©¾ðY¾äù;é…ô†Ÿ^·Êu×Ô팤RÕæ”ê—Œ|zfû¹-WV¢.-¬ ÒXWu8N†ªêB«“¨xñô©;Û·_J-³çÇJŒØ'($V¼™îºôs‰*H­@cV{8ö[xä±…µàĪø:çp÷ W¡w5°ñ¨]á½Ç9'‹½vÞ±ñeÂUºi[¿™¿‹—,Åî;îéhç^|YT¥™·è Œ=êøZ §×)…ÂÏV̤¢åI굦”¦œg®¦~|Ût<ýÀ±‡Ê JTò:'ÎÀ=C¦ymýÛɃ® K~Ýâ5ëúWQ«ÝBT¬ªž^·šðæh ï†nl%àêg”Ì+äåcåÐySÚ–“‹šê['Á°¨w±bXú:gž.í/ŠMÉ!x)\â˜ÎªMŠÄ„…¸ŠÍ×^‰Ý¸šùiLT¿z(óIbâÈ3¨ÝÕIH¤_.gZš¼Óÿ»"éQrZüò¾­øÆ Ç´çzé_‰I“&(jÊI½CÞðN 等ʠ®W[×AΛš†$¿?ë¿N‘sÄêsp®EàIW!©XArïrßÉõÒx¦k¤Ú«Uf;5IEûÙYê{Šú„qšœEÃLŽD–;¿þcPê]"£_‘@=ñé¬QËJÅ}¸O‡.í/r>Ur 븖ø†ãDÍu©°£ØÉµu‘¥Æ˜ž"-À@â3ö3*«Þ]ÿ–hKò¾BµWªìµz)™ï)‡šÜ8›¦FÓÜL?öÿ(bd¢Y…-T¡¹€õD#`}ÿ§U€÷<ŽÍP-Nû~ªA-£/PÜüIñ-Ð묤'ü>`ÀÀŽj[Oâ5¤’ËI`–T5‘ñëÆUWmë[¶<&–tiåï Yo]Œ?ýܤ|líµb`³xÉR Yoݤ÷$ Òª–Ä#œwöçb‡‘[ãˆýÇbÞ¢'bò—W^ý6*ÍUíðï{óMl4dýZeþöf6Ú`ýZ`¶ÑõñzéI¸âί^ ï[ La°‚uòñu1¾#ú[û#få°nÄÙ—¾åÇgFl•|~ûOÿ[Òב@ʆOýý•BÝÕg«`ïÏ/¾é–Œ\hÒ–ô H…hÑSÏ wð |zËá‰×Óî;eËQþúÊk²Þºµ`}ø'6HUxRìå½Çˆ=Æà¾Ùa¯?gö›ð†¨hLccä'§öÄIŸV¾XnÛJzªõ? æ5ÂI€µþýÌI¶[¦§|ÿÚ›ÄóB¥åW ¤ë_é#¡‰ÿ=J»f¤qº RξV–­à«F3œëU`!‡L%årRfâŠ+WkߨWŽMå"¼~ÕéT,ÂãŠ"éò]ZF Ô ή¢µù2RítŒ+„®¨!6í®ºö¶4´KpÇòóÐUÎîñ=­…€Æ‰Ê\Õ÷UQã’yEô¹zÃI•¬/^ÿ…+Lz©§Ú;ŠŠ±Ñ©4·é;%½†«^ªX*(·ÙòBs!)øé^–µ3¦ÄAötF¡µ8IF½AäþêJx¡Œ¾uiü¢jö×€ÛgEh¬d××)mAf9¢R¢=‚L›­õR­Ô²ˆ¼0r¬ Ó|½q=…K{®¨gHi»Ö &NúQ!è‘ Ÿ 8 z^JQÝðfâužôýt)–LÏ­¦ ë»@†Úµx ñÿLUl•¥Ù÷Æ$®¢G®Â=F…²ŒêpÎÆ(Q@‡˜âf¡Kl¸(sí¤¡$"ÕH1˜f†l=6ÇI€‘T¾¼ÇãÏ<ØfËÍcÐ6wÁ"2fl³åæñ>9颤™ÌRìØcÇP–%¶Ùró¶ªWçZ®q3/YŠ{qu<Æ¡û~ ‡ŒÝ'&½ƒ×H«Ûòr¸½ÓË´Ñëã–{~ƒÉs/¾½ƒazG„¡Õjaë͇aò÷Ï6‘–q§žƒ!ë­‹+.¨Œ)Î…÷ãO;C>Þþ·°˜Ï:áèv‚wê9YÓÕXŽ ó³•îäÅ•‘YÞãóŸkS·–¨¤Y/Ì ò¢G°Ó¨í°õæCQF ж߶6&¯Ú¤'¥²¬@»·'QŠq×μ}Ë–ãÞiW'/ÿS&&Io¶yåÕØå½ÇŸ>Žy‹žÀäïŸÌùƒÇìÛî»ιØ/5cÊÄx ‡îû%l9ô“øÁ¤+“7ZY–1üS¸òßÏK¨¾å‰¹"Vjæ«æ¬Šñ1,D*§p>ˆ«œˆpá ?ë¿( ¼óÎ;‰Èƒµþ5Ù ÷§&» Ô“_Jš®ªD("•F_ó±9˜d$'$IÒâê2ÙIpN•à‚ÐÆ XèXBS“Jª&Õ—Œ,9IK¬]&8«/ÿª‡È³‹‹bÁÅ«x"EI€,²ÆD)‹sŠ*Z[‰”r0[í¯ø¤ô"Ÿ¡Õ6™.'’Ó5E¶š2íQœ¤1â[3?' R‘š2ó^žSÜKÄÉUr.*ADh_ bEÇ(7ÚJTÕ·Xq.Ù²PîûáäD‹_Ö±àQc0Ä}EªxÇ…¥˜xia¸Ó÷Sú²#é/ë_ ]ïy:\ºH}’´„D¨BŒŠÄ•¡ §=pà?E4(ð}+žžØÔþL™(Ó±±c˜ ˆ( SµÒ€­' 6ž·‹_ZŠ.»}Ë–cÄcpÔ)gcÊ„s1õÇ?ŒŸ è Ó80j«“탃Ç샾eËqýM·à‹;}..þ]ø:î™ö³¤Wd“Ñ»%=?Ï=xO”AÖ@|î‚DZxÉRôZ#6øDaãQ»âÙÙw'}AsR Yî×9‡Mwøž~àN<ýÀÚ’ÚáßÿñunºÃùHå(¹šWõ„Ø´‰­*ùL µ·ƒÇì¾eËqÝÌ›±×Î;Ö6S«‡¥( œsÑeøØÚkEIè¾eË£Tz@ Ø‹'W2%ÆAÍ"uª­÷‹GnŸ‘Œáy'Åĉ½rʲÄ÷.½Ù§#‘ýöwÆ|ã8ÌüÉ¥ñÀõ7ÝE%œs±Ç̽c&ž{ðžø™ý> s,"¿£jÇ *~a<‚¬wjÈÑß,Jhâ!êkªÚ¦ Çá»Á“ŠÇ™‘öKâ@¥iý‡çz…xß"õéRuº(SM¦¶*£ÎHT ûGépÜtÎHyŽ^ÄÉ@xA'”RšŠH…wfŸ»Ó3¥ƒ&O/¾ØgäQóÊà&æH$}A.MŒÒ†£—@é<p”Ni›UV_) ’vuB”ê¾¢Q”|&²Ù1™+ërÕà6…¢ÙQ–›+¶Ž£Bªì¨Q³’Þ› é,rÍúwph‘!l¨ìbüÊÄ UôQZ¸Rp"µ]iTrÚíu®½LÞC1Ä®ö:`Q~M›ç‚”u§¤­ ýE‰bœô›D!ŠV™*Á 5P †Ea*&½YDÏs@M:]‘(Kâ¸F_ }×ÜÃQ¦íì­T¾BVƒ¿ª qO"1ÍçPl)cÏî JúpœAa£HOxaiî]6¬]A4t¯âóäDxÿJ’9é£dÊ^œS¢ÖVÏeÍÐÔ Ÿçz}Xü ¡*"#ºSmi£±UÄâû+\MpCï(#á¼o½g =ÿ—þ¿îüY²ðB’£æ¢ ¯Úð±8ÀꜾ4Š„ÆÄ´ý¼lsah¹>þ,WŒÕüR›¾•~c%#LmÑûÒfr­³{¼µ!ê3PSæBPnÝgø7ßàY¡ \lº9`À¼óÎ;©Û½x¼äޡՋð nÕГǰÖ/f J‹Ê%¬&7pàÀšgÔÙ7úN¬¤¡©ÀÀãϨDÊ•ò¥Aº\kÐÅIƒ0ÎI%8Yÿem“^õ뿬ÉSÿŸºþ/;ã¤wµÇyÊ9µõƒ¡°þ;ª<Þ{¼õ÷¾zràQ“iå ©Š$ĹW3Ë÷­Ò®*~` ª±Y88®Se4I–2ÇH{ * I8n¬„ú”ë_{iâ*ÁÁ’«>P¡HÜçPv‚kÙ/RSײFKÓ¾Ÿ† ÿ€J„õ‘$›¾æud÷ˆPB‘1ê¿¥çÁ}í¹^qûµ?í£+ªÄ’+õŽ œIb$àªäHÊãzk¢Ö0ëçL‰¯¨JvQžD%SóÛÄ#*fUßV'Ñ­¤=S‹mõÞaƒ[»6PëY¢à¼&’àè蔆—öjR’ ®9ˆOÞ-´F1A¢+¼g„ãF$öG3ÌÐMñ‚@Ý(Iôipµÿµf>ì Zõõ÷ý¯ÞA‘êI½˜ñE,!wL+~‹ô»¢Þ—i%güëŸþŸÁÉzKŠpüþïä ƒ¿|â{Ÿ§ÔŒÐóÓîÝ©÷=„j©>x•}eyÞöŸ‰f»Ù,ÃV€¦çcù\¥»ðµrPÉ?³T­øóáØê&Ï“Ž• æDEƒ>–%æ ³)àUôÂJ`Âý…­ˆJH€¬cY†”œ¬p㤥Üey¯ä(AšYQ­’ójï‡J—jOˆúáXAzø\H !™¹þ¦v±?zÔx‘œµP‰Ü½*}%i`µ‚U9†æÆ{~BïΪ[ÿ-³õúú_û«®ÿ²Õ:üë½0ät­€¢æùA¢ÑTԧƉּ°-FbBÀ´(îÓ4ýŠœPcPçè—¾L¨ª€•xw‡«Ã1r¤4—ŠÆáÄËÇ‘KOå ÄA,ªëµhi5©sªÜ§ýA©:Sï Vºˆa-÷yNš$٠ǃPÛ#²·h ËŽxKXG=EO DóQV»nXS]JõLÚf‹Ž¤¸ºqT¤Éh‘0ÌX…*ö¬< bLènD“ çŒÄ,[î}ªØù7UŽsœU‘ûˆ’¾2J’U•·±æÂ%@¹õ_f+öU2O"$bÀ2ÎÖ^RG=ùgUcWÍu‰oAJ’Æ,¡ÒùÒ§ª”¢€™œ›©WŒ;˜*“Éþ(ÍþVᮓĞCêc"Ék‹æ¥©Ëºdx6¢kˆ  "xÙå5 —f €3ü [)–¯¯è˜"÷ôôÔP´²³¦[(ÁêA‡ËÑc¬à#<Ü+Þéôû¦¬®VÓÛ4—µžN&©Ðc*R¡Õ] l¹ ÍÕåt**ã½h@Îÿ qØ6‚(¾vE‘´j®I¯OÇDÑSê‘ÐZi×à\•°4Èä •õ^Êy¿p/Òõ,tAïMÇT{84yS4DÇ@‘ö¥aºŸšbZÈG®úb™ˆ†yÀc©ÏÞ’ŽµÝ‰BÎ…]ÕÔ,ÄJZVný¯H„ú·þ{Þ“ëUí¯æú§Ä4\ßZùpVÉΚ{ª<Ôô¹\rÕäõb©zuSγp‹~ií'Ö÷¬km¤uhõ½vœ2A‚̓-¥=¯jî©ÿæ‰fæêÁk@i’æÿG°Þ,•CVócAæ¤2g¨ÍYc®®¶c|‘ÃY½+öê±~‡m' KÖB¢êh CV ‹¢2Œd¿">Ky×%¼UTÂ’øæ9ìÈoÑQ©Ô¥¹´àg½Û›Pƒ¦céØädþ-JŸ*¨Zë×:^ÓÏ€<¥+W,j*~6í)MŸoD_¼˜ÅºÌxIÈ¢š½R®Bãú³þkßõ¾Ö3šìXþöŠê®fÌU캚#0…Ærq·‚Á ­ Ζt1h”^–€UƒI­ˆç^üÝÝ àJ½EeR'w½¯ò `”Ç¢àèÂÑ$qÖ &59PÚòà…û´‚]+¹âqâ6Lx=&_¿&]ì½’Û4ÂgøÓø,50w¾žWŠ©bÞŠ+’€^5VßLn~ëóÓþÞ@rkŒÑÝôÃsΡ*üâÉó¹dqåÖÏÿUëÿÝüúGÖ¿¥êÇ·®›‘h¥±v“Ê×ä2A­ä™Z ”:ÏrþNMë8—åöU-Z䯾¾FªžÖ4Qq]ËÕf[51í rÕÃç""Q_#^‚ûÒ D)¸”€›ŸC'üñyt?(“§¶þ)¤OzÔP©ÌÅëË%£Rp€¾ö\Eš¡2Ül’”¢è¶F]BK«DÝŠÄȉp Ò¹Ú¸DÇ ã[QKõƒ= «µD5éÏŠ –¯=gµU° ª­ùŸSûl*Pè¬h„Åø°ö•\1ÈŠE»í¿V,c½#rÖ–bi® !WüÌÕ)’cv¹Dý.Q‘SÄ<1»5£@qcÔª¶®Y„Æ£æçÆ”WSíõŸú'Åw*¥Ì÷|dÑ…,™a~qYY3SZ¬@Gé&ºˆtáÔ$83£öîX“Ÿ%¹ŸG]À,a«{ø}àÀµ—»¨sòÂý4Üñ¿ie=$:|LSÓMM õ>”¦ÅÏÎ’YÖñ‰Ô B¤tÜ,ž7ÚÁMõüŒXÎÙº&K}L¥œ•N–£ª?ŽÔ:öŠžiã;ìYwmçÌ$"G´¤ŸMQÕÜôØ<ÇÞ_ÿÍëU%B¼þ‹†õo%ÿ|ŸMtV+ÑȾ¬j¬•˜Úþ.ÈÎEجùl!ÖñµøÓTý´þ·*—ˆúX G}jR¾O5'*6‘2¢C\Í·m+‰ª<¬ñrÑ(Y­€”=Pyîh‚Tšèj!†¯œ€xá U®ñuø%ÐËâú1Æ@«¸n¨ÉW'ÒÔüwêóI,®¢ú™Oõ XÆš×y­ÇÖ CòÁ†¸m`Jl­¹¨CD=V£R©C}þð»¶ ¹ëM$(·&r¼…ç&kÝkòe.r¶ö./ô}g½ÿs÷Klº± š ÇZ­fбïÉ¥"¹õÏ‚΢‹ZVD1LŠX^ÔQ?6÷sòúW‹/Hf E«šä{> Òê«Núœ#¼õ{~•ò¤†%¡›kÖ×6¡AΘåui`´#|J-³‚ª\s@A¸¢m-~õpáäC+ž\Ôí½Ð` ‡4ébæãò³Ö$¥ jV$+PÎ%}Vpm%Rz-JùÉ#Öüããp¢“3ÙTôC“›\pe™¤Z/ƒ&ï"î{Òc¨\³µÞ¬>#UkB$õZÞ_ÿh¤Ý­ ºqLeý;A%u­7U]­{È%º4!5Pt»GÓÈ/³þsSî9hœ»«Ïš»UÅ¿Õ4¹©èIÕÿ]Š|zà)¹J£”ô=à…ÞV ‘Ѓ«¡SÞôíq â“þÙeétVþEQˆñ³—¨+æ©U‹F”ü,#¢$tb:–'T¤òÇhLÙjµ“„’äµC.Þ1„Æ‘§ol™À”:ƒá8$¸î‘|—çµ%‰({²áyxîÕœ-…ŽçëD³´Ö½&r9¤¤[¡éýŸ£ÈõgO´öÙ\c Uåö¹\¢“+.Zû’•LvC EaymF}’¾'òYâ>ÌĨš¨sj4›˜Î:—§ÓÁPò£$ õåEM8!J®cuI‚¬j+cµ*äJU¼êdÙ] ¤5aJ—öWpÛ4¬¤{±*›zzÍžeÑÏT%Ë¢#ñ¹•æÆh —ƒk[àà;Œ‹Ué°6îEÐÄF“ð³Äಉ¢®ÇR6Stˆ«pá”?¬4ÝÔ­ 1_›¢9OY–1ÎQ ´?ˆ¥¥Y•÷ð¦qÀ/NèTrš¯—?—£=Yɳåõþú¯¯ÿUQ`ê¶þCƒµEŸÌ¯)ÙÍñæ5€iJ~tì²UL ~›œÍuÿ°h}¹çÒ­ÛDŸËÝcJÛÏ‚RHŸÿîzRønD—&ê†Äж¨õ µUXµé¹0«ãiðÍn½Ë†MGû3Æ‘¶ÆH®9K¸‡²#"À Öi?P‹Ù%Š˜'ƯíªuimŒBy/ÁsðE)ÒD1 H•ɪ“ç©Á{uëÕžU$ÉKr„ÞÙtÄ ½rÉñS4¯¬Ôí<7­{BÿR¤¨B…´G-·N}­¨d"toTµÜëuŽ¢–ë)ÊÍU‹E ß×¢o.QÒwеwäú-º ‰ •¾nh-ªoEG*ŸÑžˆÐˆÿOLlÄNio<½ œa Ë6¶¶àž NÏóžF°ú!rH¥o©W¿ìRÏ™|C¸Um°út¢›&pRý·èFÜ,m©§0ÝŒÑnhV…*]`VUS«öV"À.™áœAÛ¢iYUWâËzŸŒ)’®‡ÑƒnØ~ö®¾ZU|k15Éôå¨R\°`˰rf‰z©tÃcäBïCƒü\£²ÒxrÍöL{³ø¯P‡ëÕ Øêá Kî›ïÇ¢i/‚ËP˜~ÆhV°ú”ÖT•Ö@€ÿgFÇ™ÇqàÀÙ€LѫɢÝX½C¼É†¾>¯ö'iRCm”ògUÇ8‰RÉvEÞ_ÿÞìk{·HP×õ/¦²MJEŠZ‚<–V“²@XãeUls(W“¿W·€'W¶*Õ¹€)רk%¤j²í3AF\RµW5¸ªÊï)IâÂI=™áŸW ðJ'.´ò_õ6y#øvI•öÀÔÇ«ŸHýi˜W1j0’-¨ÿF}“L8çò:xªFWÔxp¹wN(hDe,=IS§+´Âíå]è´¯J,$šžäÄ«>¦Srâì(yâ„ÍŹWWvô ½®’5oŸ/¥“W”Ê´­Ê }Î'‚ V‘Û+rûV®P“³7°„Sú+䣼å¨mZà·öQkoÚó¬c4¢ë¾žÌ„¾ˆò  2ïëgÔ&ž)J”˜Vó/Òé5”þ‹È!@!bX—m IDATƒÚÕBN}m,*Õ« T¥óXòÆú²UãPmœÏUosº«I?Ajb° )âó3]Šêëý.õ>@oÌ@osa~„yØI êe²!B&LÓBÏžËôY}¤^ ËÈ“GŒë$žŽ„껌À°\µ'4i‹ž’޲ò\rHz¬½ Éù«ù’ñ áâ±/òIÏ‘*Υײrë?'oLšAËï%Ö¹,j^ŠÕDïÍí-¼×YФMtáÜ~˜ ®®¾T³jéÉQå6Fjœz†IrU2ŠÄg-ˆÄ¿sæ}â±”¨Áéûßµ){­²ŒŸ óû=O‡ËÑ rêÌr¯Ô+Kž‘î©È9ö2í§ÌÊ F•ÜrR»úÀ9‘ÑÊ,# ¹ŠªEUáà’U¢,n«V¾90gŸ–0î¹1çëSYfM~-Ùa>&«¿)íK“ +V4VL,‰o °úIÔ('{Þ<4`çEé@*çmù&qÝ$F“0`@;žgù }CZ¨ ö¬…ç§Âï¯ÿþ­ÿUQhŠãã=z )êþ4«ÿ‘…æè!áX çÌú‡îaèÈQ&Šfíy9žÖ‘0-rNÎK¨)kº&õ´Ê!e,Pà\JS«hWeçÏ^$™u½¦a}•(+eΣ,[(ŠžŒ´¶«q`ªï;y¨”í Õ${ðlô™h¦_ðÞ£§èI×?ýî„ÞB€e.7î‘;ª§ §Ç¨©Có[>ŽîØ ‚öé¾J†œ$©Š,¡æ3T%¸íçQ­]XÐù‘ú…)ÌkýWççD¯i|Mk*—ÄXÒÛ¹Þ œâªUœÊ¡A¹D-'˜Ð­ £ï“\¡Óòݳzòº¡ÍÙ{bT'wˆB‹ÔG(HmE‘_³˜BL˜ÂeE¿‹¨§~9o×P\˜ÏŽ=\²_„µþžO‚š^ –¹fH•ˆl®[&‘<ÉÔÅzyq`”`Ôhå¹þ ¥ñXÁW€™f4`À€]ÍZÌV@ª•Y¸QâÄ€«ÿ| Ò­Fr]ÈŠdpâÃt:¦Íñ=0šÅ=5š44U¿t š$žO*jÀó¦¯ý;š\ñ½äL%­fÎè…q±P0>¶šâò/Uw]‡9^4ÏqêÃ<Óž÷×~ý¯JÚ±ï$@µõ_–11Êy3Yk;G%ëVɶíèl@b% zÀLs’î¹jmLV»õ¤åx+j¢èåÄšÄ ÕkÁ¹ž$ùHiv8•YãÔʫƉoRyíØ‰ÓêϥA}êUfÈj¶ê›gl_ÆäÇzß8êÒþŸpLî=òÈû?YktP£²,Û¹ðsÔ P}™ö×T¨Zszë)d™Öƒß} †ú’Þ³jÎ ÒÛÚçaJ•Ët.ù$¡Mç®Ò ‘ãv Î94¾“µcRsIеþ› W›ö¯¦~Í&Sç\ïb“ç_Ó^¦îáÙ5Q¢Í…>ˆ1®q@•1ÒÃÇ.\aú"µÊV,à¬6=Aì(-ÈÒ†ç@ÈzAéä´z>T:¸IÒØjB³”‹ø~x¡ª[æåëQYgkQ[><áßIÉÑ–˜ºªõ–¹¦Òì”ÎÅ~:Ú˜¯h„ÒÊ,´JÇßîAáJz®©ÝJô´wÄê{jJPµÐñVéq«âgU¿TáOÕ¥¬V“M~~–1)¯=œªÚ ñŠ‚å6\«š÷þúGÖœõÝüêiZÿ”¸ª(AŽ6i͹Ò›C5sǵWØkÿƒj×Î뮩yÚšý©[ý…ýA™¬Ïähný¡ÌåP¢ExPö N&|¦‘»ÀÓ§aê•WP`ÊÕÿJEîÀýÆ`áœY±åÆÞ›z 1‚ø´‡ÈuQör z¤Á¡ ÖP¹ø¿KèlÐýš¬àªÐÂÖIèê-Ä(MTž£¢(¿YÿÁŸˆÅBQ@¯*¯ *Ù‚>ïy:œ¥üÄ/íK‘ •ÌåʪKRÁŸ³ä…µZÎ NÍsôn§Òv”F–£(±²”*Ui¥^ûSø¸ÖËšM!­ÄEÑ…±5°’5,µ8³¹Ær+gÁK>™å¤ùà{Õ &ŒO *Z¦š*mnU¨µßÉÚ-u4­‚[Èœ%AÍk‹{u8á°úˆš“…NùÉ<¿œáIóþúw+EÝê7Ô´þeÝéZ²’•\òÝMÄzÙ+²ûè:øÚ`÷]vFïàAè[¶¿¹ç^\=uþò×—)Ô9zÞ¬;oCïàAñß^|i öØ÷€d½çTœ4×⓵ÿäd½­€ÞB‰4È[9nXg„&|¶}ßj³É_†M-ÓÀrÒê7cQL× ÷‹Y7'‡,¿¬I£„z.MºÞ_ÿeV¾}Uì³¼þ{ú¹þù3r•£[t£zYß]8gîþÕtŒØr \qÕվݘ|ÕÕ±å¸eÚu¦¨BîE¯c7ᢉ¶íh 9 pÀÔŸ^aR­þ½¿¦5Ó­—«?^#V@ʼnÓÜÒã²âZꔢLk¯ˆ|3#7EüqõŽªÐÞ'êçò”(ÀD3Âç‹¢ ê ÓùR/¥ªÏÁGJœ'!pÉ¾Í PÁ»¤b Yâ‚—÷íbCYŒV¨ÓXˆ”t¸_£¼©ái…òé¾’äxt¬R’ªJ­¯nÂ]ý(qETøc*]¥lèQ–Þ<†ö%5­~×6­“Üú· 9E-W€ÊÙx4½k­¶€\âÓMáR[¬dG)½ÖXäCëýg%@¬ÛR9 Èmç3®p‰EUDŒ:Ôµà#”<ñ²ÖOуŠy"ªÔùL@‰ÞóHPΈJýk8p*Œ&^‹"þ‚¨°)‡ [‘EºñÇÕ‡G«À|=ZÎm 9.¨ö4h@¢Á›Uuæà” ‹cÛMóžÉâ¹j§"/ú<Ãý„>¡œ£´nŠ(tF©lìYIŠ&VXM_™ÞgmÜŠ(ð8«Âž&,zl®u#´ÆJïÉBs½Šêæ®TÇ0÷,´Ãêi°zÅÞ_ÿè—qç»)4%X?Ö¿UÕ³Ö‹õ<š*ƒ¹*( ßn‡äsSoœ©7ΈI’e&m™þæ|…`öœ?bèf›Â{ÓO>ÛÜ&"Cá<‡Œ;š÷fßu;&_u5N=éx@ß²åµÛž&BtÙ'às£·Ç EQ$hÔ´7á{?¼¨†ªÝ1}Öÿøzm´jÉ’ˆ°xïqÃUS°Å°¡€ßÍz0¡(-œ3ÓfÜ„Æ~%@ò÷ïÿè”e‰;¦OÃø#οðbL½r2Þì[† ‡lÏ{ÈQÇàÑù ðÕ}Ç`üá_èÝöl?‡+'c‹áÃ-\„ÇŸ|*ÁCàþY⡇ÁøÃ¿†GçÏÇŽ£GaÂEñÄÓÏàš+.‹W®åöoÀú_[ †Æ~‡Ž;sç=†~zE<2îüiÞcð¾ÄÂ9³qÃŒ™øêØ1¸Öƒ8öÛÿÚŸÔŽõ‹½ñ$ÈQ œìADsuˆ¦2c% œ—(ßED,Q‚²¬—l–`ý»(·Yâ1*ŠºˆŸ‚“¨²ôIb•îEŒqòA¥Rt(E È"bT–Œ•ÔŸæIÙX$'*bõ}vó³è±MȬ•íÐM¸2s4X‹Þ–SŽl:F7,+èDPïbãÓHí$ÁK7ÅÃÃy—ø Å©&-†!Éj•­x¾€¥EŠÕÄ'ÈâÂsuW_ÐŒF¨¬²V5YùÌrˆW{ëmeêL‹âId„›èCúâæÊuŽ®IÓTÖ™ÇÁêçQÄ2#UÂjÂãÍ¢éúõóŒ,„ójŠež¨BVÕ9ñxéœÉDjB¢Ô¼¦ Ñš3,Þ ÷ÊsÈ2¸U4‚Ÿ¯%ÀÏ­iÃËqï-*##`zŸ*7mQõ,‰p•8ý£_ˆÊ?ZlZÙõß-(°ÆÅZ›zŒ…sfÅÿýa6Ιe>‡n”·Ü{Äv ï޹ êéæwýùÔ“ŽÇ°mGǤæô“O4ƒ5{ÇÏ=¶p¦^y¼÷¸þÊÉxtþ| 9 CGŽÂù^\—[nç/ưmGcøv;`ЃpÆ·O‚s—]8k®Žý¡Þ^¢µ¯tû‘Û`èÈvâqÀدàS›mŠ¡#Gá‚‹.Ác¿búÝÀçFo©ÓgbèÈQxlá"|ëØñ‚ 8œvò X³·÷Ûö…8 ç_x1.¸è¼øÒ Ûv4Ž9¹ô‡aÛŽÂõ7NÇ×<‡Ž;ö\Ëû€Ç.´7aض£0wÞ<\öà X³·7"vÓfÜ„k¯¸<¡ØÚv$†Ž…c¿ý?#òP üˆÊV†õn$ömFÔQ ÖçN„Â>Ù Û2•Þu rB L|é#eµŽ)Ê£½+Uâ¤ýE*|¡2×L£K×|T‚ õÄ&E¬ŒëG]N½,zIŽŠ˜ ®QÔ iÉ%GMIS£nÆÍMý~–°Qév¹¥É|º›b^µfêç‰tµ nP.)4HO‰ Þ>®¨Ð¤¢Bž*5¹NˆÆõßSuX!’«Ed=øœ_e:©í@õÒ¾BÕÇCƒfÜ:é¬&jíQaÙ[¾§×”ïKûer\ñ\€c- ´r>=V“²R•ôíéÑÆu¾F<-a¦Vq€–S>k ¶´ZÌT•\²¤ ‰µ‡`^Ÿ/_³U¡ÖйŠC0uŒã\¢ÆãUE Y²$V51V¡ 4` 3¹þ‹²¦=nVrýþú¯¯ÿU™ý#ë¿:Õ¤&g­ÅáÛ퀡#GÅ@7RÔ›¦¡?ë?$2!ñš:}fLDš·ðë‚‹&ÆŸ=:>µÙfæ˜xäøxœEO>…5{Çc|¨wÍFð‘Gçá¸ïœ÷‹/bµ×lµùæ˜:}füìGŽ«ÍÕ3ÏŸ€¢(âç~tÙd7L¿)&Yå6ª4»ÓûãpÇÝ÷bƒõ×OزlÁ¡À Aƒ’ ™M55uüæwNŸ;öÛ§`î¼ÇàœÃõ¿œAׂD"pØróá8óü ñ¾Î¿ðb¼øÒ¸ß¾qŒ§Þ83sî ˆ}?äÊ[Hd’Ä#z Ä¢¯›±¢ê ‚RŠ1ñh÷±"–+ŠÆG6% èŠú ±Y©S $3žh­­æf KD ý¿Rø­~’'ö› ÿ΂ j×~a>ç÷Ù¦Þ¸þ€,ÖDnOi¢¥Y½ÅÖ~•CùsJ–Ý ˜­÷?ÿnyZTx+±r…« sÕ¢(Yë±@SÕªß\͸5:h` D/;TÞ¾#å~gµH‚,¹A+ðЧ6ÿr@Ý-ÐiB/4øV.¨öMpЫ=&V†6à+ĪALGÖª²u ç<Ø/ƪæ+‰Ïkqc­à0œ›e®ÃÿL§[±bE£×PøË[" ¹ ²VÿùÞ9pÏDÇÓª”k’¦ŒpÒbQÅtn[Õað¼÷Â4HÖ·l•š)tú‡Š*±’d…“ˆU@¦Fî¬5¡ NV©© 4Á²dýyÞ9ÃãÇ%&½Œú„3îsj+àUbUrê ºç–nrc.i±ÞÿÝ’4+>ê:“AŒô˜Š6ådüsý¨‰x)´©\2mBuU/Pò32PýAQý­ÚWSÚ«Æ%‰ÍÁP¬xÏ'A9ˆ\@– \T­IËÇÃRk Â8!à §IÖT‘…F•ö¥èŠEgÒž‡nÒÅMUR¾€F„_Dæ('VÀ­ç¹çÎI‚%i©—)½JÓ­ÍQ ṡþ9ÖÿÖ80êaOnãÖy“3] ÷Í •„¨œœºÕ£Ç`j÷¶ä¤ˆ­±±EcÞ_ÿe–Þún LMëßÉú·Öu7ªF“Œ¶•Œå•;î¹_ÙkOó|_þ➸ãî{ͪ¿u½¹JñGŒÃÇá ýÆ^xiImoh ¢‚ðš>»ÓO>ãÿZ¤¬M›qS8ï±ï¶íh¬9x0.ûá„ÚqoøéXsÍÞH‡{lÁÂøï}Ë–cÄ–[$Ô¹zPSšÏÅi“¿X¼x V]æÙ–e‰o~û Ûv4?w̘V%a¸¶…sfaòUW'tÂHM”gÜ·l9þ­so| ‹_|©‘&Ô4M·(’Ž’„1ídŒÅ—sñOHQ-Ϭ˨(päí2“d¤-õòô;')5Ž¿S÷‘jBr}4ÚeJ]Ò%Îßtž¥²ÝJ«œä‹ü9Ü€IvôK‚ºIÆÞbÑtCc¬â\“HƒÕCܹí& púÞ$¢c½sŠ{ºŒ•8öþ)½l¡&¨(xÙk‚—PÇÒ§Iýž$dÞ§&­ê%´:ÐßrF‚¹È’Ö,Y«°V@Ë}!€VZµ`Xv9Xú’W Œµ0¬ê/OZíë*ߟ¦1³•C¶*³A‰‹iCªœf™±æ^D–ôuŽ®fÉ4kQ“Lf“ž¾EÌ2šÂ#ásXtÊòêÑê•ö–ð±yÞXÞ9VåÛ2oå±}çw²Bþ\D¯~AJ£³;U©ÓäØJšÞ_ÿuyòUñ«§§'öG$ë1ãÕ¤fÔÔ¿“ šè+á<—ýä*¼üÊ«˜úÓ+ð±®ï=>ºÎÚ¸þ'“ðò«¯â²Ÿ\Õ•â’ "=¿i3n¸ÿxòég°þzëÅÄâÒÿ¾Ú[Õ[ ¬—/_ÿmû‘ÛÔ’ï=ž_üBÍ= o¾Ù¯{‹áÃâ÷_xñ%œwú)ñúO:v¼Q1.ºSÝžïÇwþððÜèÚƒ j|¯Àþ+î¥ #ûѵ׎ÏðÑù ð½ÓO‰×púÉ'bÐ ¤¹wJ£ ÝkÁ´k®òê—<÷ÐgT¶Õç˜ææÉ(¥IùÚ“ä5'ɘ.éÇᤣ*j ‘Bç„¢¢©!Ih*±'b ÕJÑ|[¨D¬Ä**Ur• )4%&9è’Ãæ=)—÷ÇÜ8GCS&ŠÆMÊÈèFoËYOX1\}Oiî¹qQ!¨¦ñ«æ:b':ÞéË4Y‰I ©ÇEÅ9Ÿ ,Äd'$3„‰“Œ'@¨|³ÂZX-Ôá”fÓ$ý§Ôp,Õ :µ{«jª±:A„{_Øû…†%»ÛäblÉ sUÞj†oB”ÊcQ½4PÌ™÷Y~)–1$ûY²•áÏ Hhrªg!dëy-ÿ•ÏÌmžjþÊשH”&oMI†> €Õ4Ô¢å(b`=55eÔ(çÓ¢¾J"§J<Ƭð§×’“ýU„¦¿~2ÿ7¯ÿUJ;Θí¦ë¦'ûÒÕÀÁZËRcªe懩WNÆuS&á·ÌÂçw—_}1.;ÏúÕ@síü /Æö#·ÁÔ+¯ÀGŽÃý³Œ f¿›5»Žnqò*¥RþÞ/ƒwße¼[¸kn÷±ê[ß²åØ~×=jãÿ£OµS.”±Ç,ŠIÉAßYwÞ}ÃŒ›°Å°¡} >÷®MuŸÓ T¨ma>_vá°#©Þ:îXÀõ7ÎÀ¸Ã¿†…sfáw³Äœ‡©ów³ĵS.cÂ4–;î¹7öj2îX|óÛ§`ê•W$Rè=Ê%¿¹À¸)!.x¯á‚˜üœ©n¿7:cU†ó¢p}aΓO'éð{Cr•¨BÉ¥E┢>1EìüÌbè¦Êsº¶êJwí¿ò,ªc0ý-©Æ>)3isòÎFÆ7§$d½…Vöýߟ޵wåö¼•‰wsòn{[“z²ú#Úœ·C[ãßæLÖ_ Ô/ ç“"#<,’ÔÞ”fØ3+ -TÉvéK„\|Õë©þÿøkèù7ú·óÓÚ‹Ø Ú,•ú™(šÀBƒq>nè¯PªÐŠ+0pàÀ$`›2á\ì4j;l0+QSëöR7ªzM…š&¤£é:º!ZÇkº«àÕ´çåzþѱS¥ºhÑ›XšÜäîÇDøú1V¹¹¬”ÕšÅAYF5*• ð»0Õ!`—Ôº¼ubbÚQ¯K •·Ù`55JõIâQ%ású|P3V­&íñA†yò{JƒR½åtN¥rÞj¤Ê‰SY¶’dÇÞs‹š‚¡Î¡äýߟâNS¦›Ái“ÕþÐÍU™+Œ®Lr–]÷pI"ãJŠ¢Ú›_¡`všYÿl–úæþW4ñædªæt úò‰ï}$H7hE,ÇûC÷ýŽ<`?¬³Ö‡ÛÕµåË1b÷1ÉçCÐþþÎ;ïa¾†:ª“63[6Ä[UñÛ~~<<¾xè8“ožSÉjÚ¨s¾-Ï–UK ¥.±ÏŒ"LÓ1Ìõ…Xt' YÈ98'J@*Ó”ôÅ͉G ¾Cá3l0›g ìCbÀfœŠÎ©Ù¦å>¯È„zY ÒŒ’©§A®šÆ‰É£$+‡ÄhÀÏêwŠT­ìú·®E“õÿëßòíÊ­ µâ9Пõ¿*÷צkϽäu}Zþ M¡{Ì IDATÞü³ó/¼¸¦àÖ aÍíϹóX=]úg9á\Å֔ъ³å5ÒͫIJ3°è:ýI€¬ W±ÔC›ž©^_N8×\ÞM=êy[e™¢9¨ú€,O¼ä½Ë" ò=O!>S.!:¨D¥îÕJ¥nv’TYE;ò$']GYmI%¯Yè€ÿ½Þãc#ú>1ê­ú›¼Q€äùW*å#ß sB‹Ó„ÑgûYº1;²ÙL­ï+þèV ÉYäö#m{PD*çí×­³É³/·w™ëß!éó‰b…x %ðy:Æ…+ªž @qs€ó.ÒïÂmÿ¹ˆˆP)Ë¥(“{ï'AÖͽ˜sØzó¡8óø£ñäsÏãÊi7öÚyÇZ€£¨£ ZIU_¦ZY¨†õÒ°^´ü÷Í6úïX¼div·’‘nr‰–*›]¹±ƒÔœÛ±.8þ®ÒÕrÕ•ÞÕä}ñXÉÓ´xÃPaK2\“5z9@fu:¥çXèŒpÂeÑÒ4(â8Ü #)š”)åO7m ¶ôzXrÙÚ5á Ÿ¢MüdK`#|'¯é:ú³þ-ŠBn~ÿïXÿJyì¶þ­DÝ´´¦õÿ®(p+¹þs†ªA6Ic7UXõy6%2ò`Ñp›*¦MÕb‹šÒ$žs¡Ï÷ý¡ÌXõýÜßsÇíFCìÌåL•›æ§U°Ò`²âÜŸ„­)ÊÝo‘I}D\ÊŠêFjpÈÑ“Âú·ûÄc0ÿž[ðôw⊠ç&´ðŸ}7`ÈzëâÙÙwãÐ}¿”|Á½·âÙÙwÇïE=wú,îýÅÕxfÖ]˜Ï-8ûÄcj‹õб_ŠÇsÎá°}¿Œggß³N8Þ{œpø!xvöÝXï£kc|3¦LÄÓ܉gfÝ…k/ùAmq^sÉ„8^gŸxŒ‰TiðÆA®"===5Ÿ—\°ÌÇW/ý÷ &– rª[Öwst,~ެ<§I+7˜³Ì´å‘dQÿ8àÖëÐkW*•&ªê”Ûpõ²¤³¹Ñ€‰ïÝ¢éÔDéo*6`U¶ú»þU‘Ÿ'”á;¹€ÝR’kRÉkªZ+Õú¹R­cZsáÝþÒy™«æ(‘¹*~“džš çP‡\O–µróÆz¦–Š“nš$¿›’¼œBa·€§?L®š÷&§ûœ¿I·Þ½&± ëÞs…K¨ÆºV+Ë!M÷ßT<̱“¤¦–Üj¬GÈOmý£êRo!_–QŠÛ{$j[œh*o“"E—Õ÷궯Avš©oL[SB_ªÄFá-ùCUkµž0ªZø_)oœØTb>"K*îѾöêüí5ì©ßÈeçNâÓ¤æi ï4 -(=?g0Ÿ[3Mâ ¹ugpúc&›Û?sÌ“Ú8Æ{‘„Ú¨iþÆ´9BˆFeÌÊ‚#­²ipÁ'¨pEL–¿­6>A9/­žÞóÀïñŸo½ovÎ:áè$ð(ŠgŸp Þzûmì0æ üþ‘?á1ûàÓ[üó?ˆS'üû}Þzûm¾ÿØd#è<»|ÆŸzN<âP÷<úÊÞøôÃ’&yE2,Yʜښ•ÔX“ÀZÐà*÷"TÓXV1 ¿%Ñ‹%¥5²´ù9ÐÔë±ÔàXyÏ ÖX–œ¿¯E«¢oŒ*²¤¾6–AlNÒÛB_ø~ƒ©ª&ƒšhòœZÙõo™¯*5O‘+ðUª¨]Ö Ñ¢$åD-xqÒŸ+ñ¼Ðë^Uh&ïŒP)ÚÇónePˆn}'ý¥®ä­n‰‰¢åMÒ°…¤Iu0wíݨkÝÂ&’n(NNL£IYÊB›ªÊMè‹å ×t¯–’UêÕ4VÖšm¢ åú÷Ò6ýêgLwS¡   ØJ½î°§1òò¤ŠÙÑÝH‘-œ)è$ãH*qeÉ…ÁBhn–Zj½7©:·O¨J&›éoNü„\r.[–éý=¯:†ªÍÉyœ+º" Xý^ŠÚh졾në<瓘Sƒk’Ú¶úw›<Ùš TûC‹Ë¡ñMŇèã¼¾\uíQÝ­ÓçÃæ¨1*RÓæ(‡íI±U¢Q ÒÇã­VÙ¨ül˜¬!@íééÁ__} _?ù4ôýýï8è+{ã‘Ûgà Ÿ²,qüᇠwð œwÉdüå•W1îÔsGt’== ·ß÷øã¼ùXôô³øç~ ý€:“øö{ïÇ’¿¾‚²,1fÝðòk¯cüiçâÚ¿ÆÉçý;®qs¼æÎù>®q3îÕNl>±ñ†5úØÏoüà­·ßÆu3oÁKùkÄžÛßÿuüþ§6Ý'yz¹_Ž¥}GŸþÝxýA;üzaÉRl4d}8ç°áúëáÑEO`à ڦrnðq,zê|líµ°ÕÐOâ¶{ïÇŸ>ŽkgÜŒÅK–bôÈqr¿üÚë8ù{ÿŽŸßxN<§-»ÿÞ{Ö’~>úBÕÀS«ð$jO“Ò&rt?‹‚ÇÈ„EQRß#U×ã@‰{w4ÙÑ$†¦Ã)Â`¹9³—‘ÕCÂ=(jèÉ5#šYœ–d¸…*ñýçz‚¬æþÐ×ÄÉ$+ÿå^+»þU¦<üÎÁ<'’Öyµf!5¹>4;aìUˆ„ •––{öꓵ*âøsUò¤éù¬‚†•Èu ös~­³[ÒÑùÛn}¹Ä©)ÐΡMMÁXŽ>Ýt¹%'RÑ”€äŽ£ó/WîÖ¤¶ –Ò•ŠØtKäº îXÉœ%MÜ(í®FmBrcÐè’‰þT‹Ž‘jTaÔ€µ¬"g Kp®cœ êU‚ý|ãû°}Gí=” 4­c± éñäÝjQJù½QÉc§Çi^ÿ©èB•@•9ö1ªîÇ Š•Š)X|n-[¾xÖ>“óSÓ„%ço˜‹crÎI¼[Ź\‘.÷ÙþH7y5ªnŠv”·.}•¤ø ÙéïúT·¶ü|Yõu~æÑ±´ ´°Õ9îjÑd£:A9 úã£ó±ëWÇÁcöÁ‘_Ý?8íd¼öÆøäÆ¦þø‡æyÎ>ñŒ¹5z×X½ƒÕ`¾7–-ç]g­ã¾çÔÕp:ßyqé_1`À¼±l¹ðjëŠb!¸-Hfé˯¶Õ0–-‹ðr¸þi—ýHç]RQwÎá©çÿŒ}vÝ ë®³ÖYë#8þìïã—“/ÆÇÖY ë|äøà²)Øyôg}eoô•½ãñ/Y¯ï­·ßŽÇ\úò«€ÿößþ¥øäD¬ ‹ªÂYAIè±(ma°÷Ø(ÅÍjBÏAÍ9Áˆ\ a!;9ªYnCÒ™œü6#!–Ñ­ÄZóLþ-ÿšœÚ ÿ™e²›P1^·ºù†c„ç˜S[Ùõ¯É¤|<¯¬—»Ò.­DGç „ûâ$ÊZ/œ$6ͽ0~aÌ˲¬%ï–vlQÏâsl rå¸õ¹@½É°UÉâ¥+ZØ h àû+‘Û%³Ü±›zr×k%#M O·^¨nÔ뺛”¦ºyó4)Ôåú‹,¡’\à§TÒݲ?Ï·hB2â}2È£w ‰Òû¨bUJOÉãþ/Ëè±ÂÉT í;±AûsEG@¡%¹*±©¤©+Åòþá(‚%¹›¥˜}&WIj»(v`™¦‚Q‚ˆ9A}P3B­-žç=¢ç“ëÏõD7‰¬ŒGzØŸ‚Cn¿Èõã4Íû­´›)l“ðK.>ÓÞíè D™å‹ÕQ+Ra6ˆë\Y›’„ ¾¾®CQÑyþ!ñé)Šˆ­êp\‰UŸ¥õpsÝÌ›qßìßãþé×áˆýÇâ/¯´ƒøŽýæÎ_”(”uâ18xÌ>¸ìçS1㎻pÆqã±ó¨ídbW›lß²åXoµ“@›7°º*˜3¥ù%a7QWŸùË+¯µ‘˜£OÂÜ‹²¦]Þ{üâ–;°Ï®;áÌŽÁ“Ïýs,Bß²å8ó¸ñøÏ·ÞÆo~7 ÿ6ìSqœÎ½øò,5 L¼ÃÛŸÿkgsºóM†`M­`XÍ{*J ýz]–‚&&MªU¹ª‘• X<`MÎT½ÉRÏco ÍQ8ÝœX½,§:~ 0 QžcÔBMÔ˜VPíëÉ©Éñß¡áç ='êE´²ë_7l-ªðúg±Špï9: Ã’ïUÁKD“¸€ª\­à9±*’‹ÂsÔJl»½H›^Âý‘Zo2þ³(M:wš”ú“¬ô'ùÉ%lM‰Š5¶J‹ì†ætóËíŸ9ä©iÿΆ9:aÓ˜w£ [¨¢VŠ-_¸\_f“ì¸e¼­kÕJtTç¹$3ϹšS’aª'È%˜áÆk %UtåÑÀ¸J8H,ÁU…Ô ’àDöÚ%rÂu)ët¬+lKÑî¿©’/H$ë]B-±‘UfÊqWI’Êq‡ï”¢ü»½›XS’ß­hb©™6%Q¹ä#‡Lu+XX×jíc*Ê£JªM¨zR¤(ùq)-Ò×\j\¢L„ ¢yjD3É‹¨“PÅ÷sGž¾í)ä+óÖNÄ׺Z˜¥²/Sœ”¦_Üùsø{ï{g=xýöÞðû¹ópßìßcìž_À§œ„©7ÝŠÒ—ØiÔv8ø¸ï`ݵ׊Ës§í·Ã°M7¦ÊP‘,ÞžžÌ]°;ÚS~ð]<øðŸ°Å§6Ãc?e@«>YìŠf,^²½k¬C÷ý®™þk£SÅU¿œ±{î† §} SoºŸÿ̶8øøï$rÍÎ9<òØBô-_ŽÏŒØ w?0Þ{üyÉRì4j;<ùÜŸQ–%æ.X„ÅK–b¯wÄòÿ÷ÿÃßÞx[ý$~ûû9¸íÞû´{‚~xÆw0ÿ‰§päûâ?ßzWÞ0Ý\tŠÂ4yvX´ú•45ÅóËÎBŒ,}¹Z’ÆMA÷öhš –Ù/Ge¹µ±RQ*+©ËU¨¬ÀA}qø™X¨ŸŸƒq ¸,¤§¿ºÒ(­€Eœ•YÿJY³šW-=K♓ON8U:=ü›…¢…± c©ë_“@ËcK±þV´ûó‹ÇE½ò¢rh«Ý¨PýQùêÒÚ”€Xèe·ªl·€¦éš-ºdÝÍ­›&t#§Õ”Ôå»nIußÖÙ„þ5Ñ+›ž›¥®˜ó¾³¡¦b™Uíïv­¹dµ4PBz-´Œ²¾ôo”°„¿{E»´Gß‹ô:Vc5ºHsÑ”7Hj{ˆUuWöòIrÀŠm\¤íé)ªZŠþ¤H÷¥É#8ZÈ%=š¸hrfõ¥Šsib•ÿ÷§8››'¹ŸåÖ†Îå&ñ«÷Ïzu+Èä¡Ü>™1ÑÞ(ë÷¦ûጟUÛbÂÄßaù©°BA*pE¥ºØêPêpüË?ˆ¢(ðÝK&añKKqÌaâà1{ãù—TÁYwê9¸oöCøÌˆ­pÖ GcäV[à‰gŸK$3Û«sÏúÃ#øÀ>€3?{|þ³Æâ«¾ÿÒÒ¿âÔ ÅëÿÖ7¾ŽùçFêVf¿´ÿüÁà¾ÙsP>ù àáy â˜vâ)xåõ¿á˜C¿Š³N<›n8¯¾þF;Hóí$m‹On†3~àŸpê„á/¯¼šå›kÏ Otí;È-&K}Ì’fdC“ªœ/‹¢7L§Sùcí¹áнEɱz¢8±³dˆU2\½”Â1˜úÄU&¥*¦Á»õ²W4ƒÙð7äAƒ>íÓ~íײ|’r}#MëßB„r’Ô:VÖßÓÓ“ôaåFÈøù°<ºµþ¹xa)ÿñÑÏ›fõš3û$¯Y&tïïÐ}p×.ï©jßHè¨ÛiÓá5Q¬×!«jCkË9½PœàÊž[ÍaŒ¹—æÏß´ÍS>—°ñ«ËÞkšöÞ¬û';Æë&(¼l°) VG¸åqž Usa<P‘….n³ ¹=ÚY¤åqIÇãLkqÖåâÃE£†Ùü¨EOÕÕiK§¢ÜÈh¡×EëÐ :_?F`ßTZg4;ÕëTŽÁ4‚œæ^QIóÚA[l‚ô*¹ÑjzHWM›6@@Õ™f‰ & ÕÐS>ŽöÜÃPÓÛóZS¾2‚ÁÍKÛ(ã—ÿÞ¾GþùZçR48‘»¯P™7©V§¤]ÓáÏåB úðþÞðý'„Ò<•óAM ;ærÃã^"]Çô³)¿F~få×¾ W] AŠd£ÍͶ)ìóZooÆí̬³†èW´7CªQ oÿ»Í‹ _t Sî1ܰ%˜}bžrÑa‚9Êý2ؽ µšn¥yiìˆï)&#ÊÑýožÃgF~Îp£¥q,\Û Ã(3è[×› v’Rº,^6ƒJJUâ"„sFtb¾*>2# ªsðŠcÓï¹¼y´ djŒgÙëA«}… P ÎM…_W@iþœŠ8èôÜÓä÷ì²pîr ã÷ãˆ_“é{°šuFZôw¡ Þ1éµíBÐòôÝ ™ï:é1{ü`>ª©Õžsž]ãŠ:róſ빷ñ1{¿¯6éëÝþïÑþ/bÿo0²V› 5ÙÿfÂè=(‡¢LI(ß—";?ès·«¡Tq3iÍùgù÷Mj|¾‡ä0E𢗝±Î  B½F© M'· Ÿ%ãØC×,¤§1¡öÀC@›o""SÈK?3["M¯!d'­MëäclPÓÐøóˆö­Ö‰ÝnÛ¦BÐ{Og6ü0M¯W;d¤§4MA~ª{_'Þ{ÙMOkŽü”ãÜÚfºC‡cš¢„JwÂè§¹¥úû‰a#½Ô¢e/ ½ fB÷!u]Óï8j1Ù8Ù½mY¦èý˜ŒËý„›Âܼðžíº·{yH”š³…´žÍýÁ½zØ÷æ|)¥Ð…õ  òŠ óT؈ešR^Ül};ÞÄ·+ÉÚ Íòè2Úpxˆ‡W(yŽa:Wô…3‚<Ú“WÀ*_ÚÓ.ð¹÷ÜÏ< #^é¡1=JßWC*½\!Ï"Ú›¤3…Nß_Cgõç4äTé{ú~]zŸü9ºÜËÔþÛVwqÝ5tSÏSµæóÑE¿óœõ¼cÖ†ÆC·´yÒæc½ÚÿM0 ˆý6aPàð$ÌÝÿcýµÖ­îþÏSXžÖç°)¨)3©¥S…šÒUg0dhK°ÓzÓx*ú)f)„“Ód%'X~Žú\$UT·POdµp M{¾L“•‹ÐIë$ô,‘=¬RòøwøÜåã7éìÎõ0ÍŸ¢H2‘Ö 87v^ä!`ž¡¦W¥‚婼Ò*K1˜ìó#7Àfâ®tÍ|u8­žÌ µ !”õpÀìi”š¬y‘f†ØœvRž59®sZ$„$µl–=¡AÜp¸&!ôór"òõê°÷gÛï@´7Ðk….:6#Dd"ÁFT¤ˆq Gjjaˆ.È ¿é5Õz«(ž¼'`i²jÊÈŽ NÊó1 ‚‘ì°'E‹¦*ÊÍ“Ù;„xqpi>þBB)ºè¥Ë™AÝÿ+Ô¿ ã[{, ^·Þó2SVc©/j:)Ó× $È»12Ñ5‘΄Nõsát[%vÏiñ¯VÆž•²¢^Á1^žŒ”ZÄ7u×pI¯‰oª­ÓvÑàBÔCQøX9ð‘ <Ï}J¯#7ãMßµ1ôÖ’6C^³¨l|žu­ñ×<;cmâôw÷7Ùðý]E¸—”=V¾^]´]  6öj¤{tjdìoCGc»ê\©vFïÄè_¨-¥¿åÂÏPebÏ_¯&çmõ_Ðά(Xæëíµ¥yR}•¥2±SìÀ¹õ>†UÚF¬µÍft&¥Ü$À6. \W/û5wFršf(oy0“ IDATj 1¤­«ùaÓR5aâ²ç‘©!ª.ƒ<¸04ZF+“mb*tSÐÓt„à¢6}T–rNÙ4ºF]«ŒàXí z‹ôfÔ›÷¡=:@iìK`jÇ`£ëùß5|4 ¬ «ý ÚšÏyj,žu¾ Ò‡´Š½=Ê”¦Ë³è—–y†Z€z®ZÚ”pS£™‡À™°ðä˜EÛZD2ÚÃ(‹.*µbÖ‰¿GKò4ù³¨Ã›‡ åóË cýËŸE‹t¦(©»[3m‰ßK‰çãå÷T㉮k¬(×|éù×O'šù3©»ŸŠã½€@Õ)U”禯º>ožU³G;ô¨Z«J‘Ï?§I^󴪺uzÿ{ÂP¦‰³7Ò3 ¦×”b=»Å¥”Í”¬É—B‡{1ûŸ‹Ð<]/yjÁž+£×© XŠkJHƒÄSÊ‚0…D›&4¦ Á÷ªYò§ÏÉ69R€”÷§"©Z\¸#´Ÿ©8){N˜OïQG "$ˆJg`#¡Jl—ÌSêjš‹Ú0Âhn´I£);7Så’œsÌ“f-îäuô>d®yª5&¥m“mÖ;Ú”y];jÌQ­¤ÔfÓgUv :š£_D¨Í#„Éñô‘š^“ÁèƒÚµÄ(U0öÔ,ŸÐÍ‚ ¿ã9'‚—bF@¯ÁÖßù èccÉ…­#J[Ö Ý*Õ¡è’êí?° ‡Ó<‘0£·•雂¤U˜ÀP³S x Tµ!rc,Ã#´0½Ø(0"dhlãìF•ø¾iPjAsÍPB¼×½?çb?vFx4pT{ßPVíÀ‚éÍÊHÖcÖ4(’¢ú}Pç"š‘ :ôÜŸxª¯4«UQõEðÇŒÚhÖ‹ŠÛ•fÆÅ³çbÕ0ꉜµùRKh¯è÷üß½ ¸†^*}«KÜÍô/-Ú½ÍÁ 7SÜhðgcdDÅñ]7zïÿÕ ?6ŽÐsŸ?—ZF{ÿÞ:kš/¼ðBù¼jåíSèºËŸ“í Ù–Ý££èzó¨úçeìhCÙï÷‹£¢HäsõŸ²ÿ{AnÖxÿ£i)G±m€ªýßsöl'xù³¤”Ðé¹ÈÛ‹ý³¦û¿¢v‚SF)ÔrJßJ©Ó)-¦ˆ§F^­ÏÿøO`—?}{1H(k5ÿ]±òd”¨}¥HpÜô ("èa!P‰d?6ŽÐC]ø÷•Öâ5eÙ=°|Þ è£G1UÈNAåJ°æÐ\¢i* nP´T̓1?È…_Ê;¡jèŠáÀ°plzBOuœ®ÚlI˜µ»U¹×d›fB. %Ö1Œ-S$­ toÙÆ™:ÅASÄ4¦ã6ÐMYJµC{RÛHkq>NÀ(oØPÄþtï¦æ%yÃB[œíoæ002P9¦¿i°¬ÖÆÅ©.?›Æ`>Èüy³AB¾_ ÿJ“+d´œóh]ܼB½´ð}#¦Ê%ΰ6BMÍ{­¯ðï ­ÕkÊ̳ž>K¹Oä¡ ÓYù>•bÙ_ùï¶;…‹xU”Ö„Š®[tYýXé)=VyFÇ–F—ÿŸ’‘2«ò>b&‚Ö‹&Hi^Š(ªÁ…?#+^veJSR¤@5¯¾K³Á“cžt5\$k`V>nÕüðÄœõ¹è«¦i4-÷>³6ZÜóñ²c\—¾Gkb‡·‘‘‘RÜ«ÀßËâà"Ùã”æs˜‹{ÏÑÌ…ØN# O¤M/&¶KW!¾:Åi£¤<ý^WþL>¯cccUáÙIkHiW(%ŸS Ñ&ÊÓ–¸jÃõÃfLWüOÝÿc/qÿ©n©ŸJC…f5÷?:öÿ:×ôšZ[ñ"þ¬îþ?6ækQ™úy8&ŸÖÔæ© ¯M?š úŠ•+ñ¹“NÅõ7-¬Î†–.q‘Š&4¶¸”4OZ帼qah-©þ¹L³‰1šâ^÷•gÀš&é("g=,‡vÚ\ ä›hHanö˜6Ä7[U„A¬‹ÈBÕ8™ÿf‰Õóq¤&XíVƒÆœgFí 5Ьt<‹XÃ[lawi B§Åq$íc]1YJSJaš:8Èü{¬ihBƒˆHô6~­X†+ö¹Ñæödt*ň¦ŒIBKCK—(+QÓ—–-îÛfFãVsû^bfF¦×éu#j\n Ya3•.#AùuñQJšçVé¹¼gFÑDV?7PÄ~¬¤ žõvõž±61÷†&TÇÀ뿇¢Ìáïk“ÁŸÙÓ×xZ>¬+ÒpQE·<Ëe}}þOˆ®…¿Òü´ùò|è=MW—[‰žCïµ¹@U­Œ®ÎåñP ='Úuqg½ÈCV¸ÙóÏ¡š ÓÏj~‘^m’=³…‘‘ó^zÌLaSǸurÿ©]Y¿±V÷ÿ R;×—rwÿ;öúšÃH O6= g¦‚UúÑëÆ›0ûäÓðå³æÚ‡uB%6œ~6Ь`suª|™$Å>$Ã"ÔÔ)ÓTÄÚA®²°{m/8UÍ3ØÔA§ÈšßdôFž¾1¡¢Æ˜s‹|éTÚ¸Ð%™ü²9Ú©¸iDbòr›P;ÆqÓ.©qÏ"$ÌkìÕLEÕœíTcĵM°=–¬G…ÎÉ"JóÖ4-¶‘äZjܰaA"D¨¡ïÕyCå\õ¬P£h-2À’ œà–¢5>€ q`ýZ·88´9D3˜PÔ‚Aœèt ÈGÄ.tüß¼/Øq/Ø"ßäê¤Zã¦yf¥A3“ñ­ ÑÓˆ…½Úk»Á©j©Î:9A‘™Bk 0¢¢÷=ŒŸEfBPQëuò^ÑgUnØ´A5za-ηÈõ!#a,?ÓsͲ>4AUwIL\è¨+˜êrôëêTÆE³'Ä× ­¸"IZh{c-$Uç¡Å¢¢1Zä{Ù.Ü”å×cº›¾ÇxN^yj­=O°¹SZž¿Ê¿ë™h‘­Lïái»ôjo­ˆ SõÜg¯¬ÍW ì‡.›f¥AòÍDÃY=÷¸|t­±Å¹[,9{C©\###†RǯË!»Œfs ½îÞÔþ?lÿ÷Ö`ÿ‡UìÿliÝüžö?9ƽT¤}•ûŸÀ<í“‚žCOËÞ –VÂxØ›<¨_ãMxå+_Q¡#ú€Wg¥Dc)œÔsCƒd°cX$Ú‹RµR2¨í·i€œó‹»Ý‚å£K0oÎI¸ë¶[°ðW#47]w.¿è—6ª `~ý½öØ Ë—-1ôÓœ éqÞuª¨m)ÕÍlPõ¼9'aù²%ÆýªÐ7 ±3÷›ä µ’=¥Å¤7ÀÊM”æ¹:°wÈTÎ¥ Z„:ex[åó¶ÿ6¡ ™!¥K¡¬5U! së†6—s‹ Õª¸ÚÀT >hØ” í±9[Hšä9âºÓá@iˆ(ô”ƒVƒ&¶¶Ü=£! Œàdc„Ìïô‡š4~»ÿc‹æÄdh¨¬òóp‚>ò°"SìôžfPédï®ëZjiàR¢àdòiã<*÷g…ÖÆŽu†þ«¯;PBÚ@žu= ¨éža–ÒÇõ³{ÃäL ÍCƒòüozUHðz‘ÄÔ kÑÏ.Òºò,Èöl»¸Œ]!‹^Œ¢Zäxmm<Ë^.8•¤èƒÚžÎÅ+ú´xìj2º).‚ù¸½ Y&§©‡Ä(r¤‚z=^±©ÍÞxY?ã ý)ã&t<ý 7j sSÉæžæË»†¹YVÝê}<Ž.£*|¾õzò Žu8jŽi`úþz,Jcü½ïÿl\@SÿBðÃK›ÚvÕìÿþïÿ¯?k²ÿ»š 3ùGݰð¶šÌë—¬0˜Ðuãb´Ô+bk§ 3›Š§FÆòeKÊ÷zóînˆiå”P@eâ¬1/ÐIoLóN= 0iÊ8vÎçñæ}þïùðA­¸]uC¡Ö5¹—5;‰‚kS]tU[¥‚2oÎI¸éº«J7{Îi˜4yÿzà »5‘Þ4/:4‹µ@šµVåkQ&å…v·ÀbC=^CXô¨s›Ô¾Ø>rÑm ´‰ÊA©‰¦Ø5ek`îæˆG$G¹TœêÊ2i ³e¨`-ý†îú°é1ƒº’A•àARekäû¶A#L§eïG„@©QC ŸOæÞ“ŠU·ÑÁ°/Õy8r:-òé…˜6ÝFÆêšÐnþ¯§_Säƒi¤œweLh`‡,†Ö){ؘ±Phµ xÖíýX!¯¬ó1ˆ8é¢ŒÑ l*ç–yZP¥àf•Rš©OhYûÃû»þÖù&H‹ uÛÐI5Oì=Ê“N–)ð» ® Ko¬áŒ]¶ÈÞ´Ø Ïôt1L…â0L¥÷¨vG7 7V¬Cð¦ª5ò2[7ª}a$ÆÓìxE}~߬aÔ„›.æ½'EC²ã›6÷LÿbÆ+¬Ù>[é†LÏòŒPy{CCX=ô”_[ó«”¾©ëÿ%íÿÆ*«bË‘OÅÎZ) ±˃+#B%Ä4ïúþôþ_[ÍסfÿË5UX¥ aDò«Üð0mŽ nÎñѪLU‰GÿG¯{]Up‹egÒ˜‹ÿ#¦O;ï?èܹøžR,¸ô \rÙ•øÌ'f!¥„Ã?6 Kï¹óN9 ËG—`ù²%XxÝU-—_|–/[‚å£Kp÷íß)Æ]·Ý‚¹sNÄÒ{îē˓>4(DD?³ðº«ñî}÷Á ËG—à-oÞ£ün5ý Àå]0@­F—à®E·X'´B¯«uRO>úž}O>Œ»}Ç X‡ÿÏiƒÏ¸ì¡¶ûn€¥?¾³ÛÂë®\vá|̘vèàó.[‚y§ž„¹§œX^3 à-{ì>x­Ñ–Q£”.¿ðÜtÝUXø«çtt fN›ZÛê’±†ºZ™Ÿi‚ ßÍ_oú¢ Ý£j!lŠ_¡ª&„p69ÈëöÞjH~Äì1;ñFEùá>£ ;¹ÁëGzÿ¡°¼×´¹>1 ®mÄš¡B 7 p¨JŠâ¤TÝOø€ 3‚©¾Lo§*‘Bç4.ˆ­ÑЇ4D³-ºÌ2!1ž67ûŒ67aýÆ÷ƒ¢$Ò¾%Ôa¥¡FCcŸVIÏÄôOn€™^§k1PÝba?ÐÀ ¢{ì'ˆwÓ¡onêú«d£ªù}rÚÔáBþü½¦7üocnùëE¤Î]­Kµ&M‚I=þ}.ž´ÁÑ‚^sÔ:Wé/J]âðFÍÀñ íúUÐ͈C—}¯7Í÷Œ øü0êàùÜwѹ9ÐëÅ×ÁC™Øæš›*¥+*ýÊ£üi’¢9jIÜ…Æxˆ_¯~¿o:¦Ÿqã¡ 'ôÞºÔ¯¥u©k™— Ĩçö¦tIÕu!žQ€îÍ*[4sŒÆñ×ù¾èýß´‚ŒÐtîÿœ¿ÃÇÐüÿ{ÿ¿d*ïnþ»ö?Bžê‰{©Ôï{fDÝ”¬ÓÄàŽÿ÷‡8÷¬3ˆb“Lødn6tš›ß÷ðÿ9 £ý?üÑâJrìœÏc“ °×ƒF`â„ Øeç7`³ÉÛc³ÉÛcòÖ[áÌ“O@LgžröÜíO°ù”1iòMJ.@¦O=ÓŽ˜…Í&oÑÇ~Ë.œo ŒÞóáƒpó­‹°ø'÷aÒäpçð3µÈW{ªæröܽ~?žò'Gìþð=?Ä%—]Y/ÍQ˜yØÔ‚ˆm6y{Úà¼ÎüØT|ó†1iÊàwvßuÌ^-Øù÷5“H7g¹ÜEdíƒk—›Ó¤ØI«‹j¥ ­L±ãI|×õ¯ç ?ÓÔ”§¨œ6Ú¹©ÉZ½Vz Õ ?·"?MÓJ?»gž¡7ŒÊå5àé´éñÄõÚD1 ÂÇ–)x¼Ø>ûEíÿÞýÿ’Q ~¿XË®ÎþgÎ|…´0MVœúešÉM‹>Peþ±ixfåÊò f†cŽ={ÿ÷·¸Î_Lsb?Ü'oµ%žY±²3xvÅÊ•¦xχ*Åò·þéF¼içûìýVüí—ç´bÁeW`»m¦”cYpÙøá£ n¿ãØn›)éypî=µÖPÿ¼cï?Ãß}ù‚‚üðû1…Ô¼þ±iX±ò·ƒfexŠ?9ûxLÙzKÀ‡þâ}¸ù¶E¸óG÷  \zîZüc\|éå8nÎé¥}옲õV¡‹5åqîÉ'bñOîÅÅ—^^Îýßž;Ûm3¥ÓâŸÜ‹Ù'Ÿ˜ñ%˜¼Õ–­¡F©Ð3שnŠ ÍR²N€ÎªlšFÚ¥Š¾‰º 5ë­‘A*Ú¸DZ†0Ô5¶9AriŽ!´îXƒ{h‹ÔCGdžj¤€š¨AÓÜZkPJ3ÃÚgû·æ¬K¹Ç¡µÜVG8}>e›ì¢ jÚÓµ>‡÷w13aZ¦‰xCGƒ%«)Ì¿"€|Ÿ«Ön°ôËЄB™ÖÆ&uíÿ丼Eѱåg_ÛoF»Bí¬É(>75&WH‘!É|Ó†±j.c­í3Ϻ`kÕòuë÷ûÕ5·M ׇ&È ôÓ"¥+@‘ÅûÝ„ ƒÏÅ®š"x Ô]M@e1+n\ì2R¢âzÖ;(U‘=æ|š¡ÂÍŒW°òä[Õ½x×@ój2râix<³~UŠ"âå…£r³Ìb}ïZ¨½r-+ÿ,7!”ªÅ¶»ŠTŒgÞ•uĮܘyH £,@¯aÐæÏ³"„Jcóè›zî• ø¢ölQœõyÿ¯ ªqo8•]ýÏ7¡¬ì­“£òÚU4ohFª¿Àg>q$þîÜù6üoX€üê׊ZÓêÄÞhxðóÇ~‰M&N(4-<&N˜0œ~'¬X¹²5~ÎM&N@'¼gž|B¡™yò ¶¡vt* ®ÂFÙ¼!YáÌÄ ¯Æ'_èpùýÊtZ£RiøŒIÃðSJØdâÜñ/?´Åÿð‡nºîë…Ö6y«-+zgôº×¾O=ýŒ™z# >»qÎjl£ îSLƒQýN¦µbgóÆÉ5 ƹø5ædŠÀSv5Ú0]•·…’VŠdk÷[îCç+׳¼il¶Í€×”ÿI˜ÑGZÝ)âó/÷ÿbËN÷ZÒÿ0B¤—À:=ÃYÝP€o– ~ƒ¾>"d5=åÙ öÕJgÓàâ,Ö¯Í,`ß²gcri™†• Öÿ•V¨Aô½‚EMƒDJCÃdª1u¤TiŸ&ËHhz•©OWN[#·Â¤¨žò³89ç\j¶2©ÎMɯKë1‚ê ¸°ñ hOÄÿŸ'ÍꔥTÏÀ£ñ×µH÷Ðu…òÂ*Yc²:î^Šú(ºã¹“y6Õê^Æ(Fþ—ѦšyT«ü³¦Ù%îó5_Ð"-ãél’'ðÜü¦Åyz)=Nqó¬¯sC‹Yïg<[mEsØÖšQÝ/j×®{‚Ã;¦[zVߺ½<µ´öÐ=m‚ÕÌáEíÿÞý¿6Pö5Ùÿ™Î“©jLå¨}§†Ù„ÉÚHõÝ+D&N˜€‹þá2S UèqhŒë˜A–$‘ÿ\tée˜¼Õ–ØkÝ+jÍÜ9'bô±_XZ ú_÷Ú× P$+Vþv@ûRÆ6›¼=öÚ÷Ýe…u-jíiVÌÏ‘)D~¿Í·Ù±¼çžï|— ½Éèc¿À&'T4¼C”í™+ ŽL¶ïºí<õô3…Ò6úØ/D+S[†'$üêñDZ٦›Tf+Vþ–Š=Ö+ØsÌvæ•^c3Ÿªã uþ ÿož4sqYÌ2Pç‹p.¬ktãÄg¥0E“S»ÂÙs‘†6_÷– IDATÛ¬mËkÀ$(Žï»-:Ô"L­9CL³´Båû¹“"?ù_å-7H•yua!ß3)G()TEš­i,õ. Üðb2ôÚ\L+âWÑ)átV¹Y‚\8æ+šÌd Ž(ëƒÝÓÔMRg éeêW^óŒ¬+"d‚*]œGI«˜1ºM¾ç6§ƒ¦†²&¨4Kâr©CE×¥W6Ãû;•ç zëKN‡(¥IðZЬŠ¤ÅªÒƒ áb……îìÅxÕu¡%JïQ!5/4¥…q3¯«®7æ)³ý|ÉaÄÁ¼ë”œ]ÑÔAÑiÐÛ›X{ZµéÖ©6µ]«zcÐ4S¶¼€\/ȕ׎f1ý‰ÑuÑãßÓ‚:¯9ïu=ʤ'TTûuïx9FÚXv¡<Šz{l÷ó_gÿ¯­!Óxû¿aº!9qPËÕ*ˆ #„ž¦ºŸ|mâ ,üÆUåëOýüá"žÏýüµÇŸxÂå¯k¢±m¥IçÍ·- W]·¼y÷ržg63¦ŠK.»’З ˜;çÄrŒïÞw|ó†o XºlfL;ÔLS»lç M0脲Òåíqä÷3S^ÊÉÅÓ…\z&Nx5æÍ9©\ã/Í;w-þ1¾y÷ÇuGsNÂ[öÜð«Ç°×»—F)Ÿ×M&N¬rLŽ›s:vßõM8ücÓÊzúì'fáö;~PWŽdÙl&äha…t‰„64†œ,•$Ô'¡qÃjlº‡Ÿ5]¬[ˆtmÃëËœm¬[Û_€ÐŸÖŽkÁAÈjkŸÝ61ÃÆÇ™ëûgº”J†½.¬²HNA³ !G`c„r_ê€cˆ¼Á©ä 6K`7µ˜jŠ[°tFµéç°P£/¡,4× RPƒ:èÚ%ÄZ ¹!)Ï•˜ ‚‘×)Sfó?|å,£*{­ ¶ñ ÖåQ5V^pr©‡4X5M½PQI¹5ÍM\ê¹÷\®žÿ8ÌŸWY Ù).!ad} Ãç¢4ž®ƒ§Í1F¼ùMoÄç>>ÛO™ŒW¾âðoÿþ;<¼lg}eß÷³jjì ɸh÷ÐÖHdZÿ¬j*x*Þe_­ïïMô»,»»Ü¶¼cͯ×Lz4(¯Ðõ„çÞ¹òDôja…?WŒ±è ¸à÷(oºò{1 à‰ó= ÈŒêx._lD¡šÏ’]Ï›GëÔ"Yõ ]”4mÞ¼×òö7u*Ð÷Ölñ5dÍÛd{Ú/£¦«‰ìÜÿ¡{ÿ{ͯ뺸ÿ×Öýuµö¿c[ýZýÀ¥:í,­dmüÞ?Нý^7h„6›¼}yÈšk͓֎ ÏMkœi3ô¾ÓŽ˜…ÓÅ W_iÎˤÉ;˜×Y±r%Þ´óÎxjôá¢Ã¹øÒËS? ¯»ËG—”Ÿ¿äò+qÜk`Ä÷ŠF'S‘T9$üaùqÜü y¿Ë®Äì9§‰i…]ÓÛí¶–/[RšÅ?¹S8pñ?\ŽÉ[mYÂUàØS>—]3O>3¦Š+Wbô±_”yìœÏcùè<¹ì!,¸ì Sˆ¦”ð9ÿtÕ…®wó­‹p쩟÷§è]ˆLQK45N¡sXêQôŒåöðõ4!ú`HÁÍOj-¯Ér]·¢×ÍýlpL­½/PS…¹9jß¿žè'öóý½rìJ•yBÑøÅ )"ñ3à ½Lžö¯¥é¡¼Ê„¡î§m\ã0ÿÈ~®˜š^oøýP9Ãé½)‹BŠ6¤7éh ¸‘à¦ÞRm¡mh¼]7f=¢ïYí÷d‡E&ƒ*¦jÿŒ—hÖ+ºU-òlˆM€qCOôœàðë(L—a£ÇîŸ^ci>o#¶áDsÓ=é™Iôû}¬Ý ‰ÿà?;qMúÝÂÓå.dÀ³é=ù˜Yxÿ¾oÇö .þÚ5XüÓ°ÛÎ;aæGÄn;ï„ný.NûÒü ¡à©;óô9˜±« ÷Š@ž¯ª(ÍÅ´wcgd€ßC‹EƒêìÀmƒ 6¨òy<Ž^ãÁŒ·=-„G ïz3*Õ¥µéZ^3èYxkÇË»nž~‡›Ö]yY7êdçQêÔ|¼&ˆN…ø]Ô¶¼®¸xWÝÑxïûú5/¬Ö£Çu]Ãq÷ÿÐæ:ÛV{–îiï\¬Kûÿ+'ö%Ýcg7ccÆm5öÿïžûmMãp&p]¡¦ùákÌAÃßû£ÿö:|ûšAc²Ë{E±3ïåÐRÌgpÝè\š\ªЇ6 Ÿùä,l¿Û^~–,­ÅЦÐRšŠ¡jÍNAȡʜž0#¸…‹¡©äŸÉRdœ{LóôÜ Qä"ÒÝÿŽÃ•VÉÍsL±nH‚?]6ç_®±¡>©v!;u?ÆŠò³Â[ÖzUáœ_#’żê“Ú¦)XÈh%B½‡r#Oô¶Æ˜#À CЉa €Á=³7Ò]¿#:àÐÒ¢µ³žA#Vµý‡MS~4 IU:«ªÈŽzÑɯ\—5™U—ÙKðõõ¾gSªèaül2Y9U³dìõRוgÙ­MY‡<¬CÇš'”³¡æ]uGº¿Ã¶Þ€œ›¤vúÏÿWl< õ5ŽÒô¿zÿO®H—Þ„T³aø÷bŒxó®oÄû÷};N9ç<ÜxÛ÷J‘ø£ûîÇ=÷?€ýöÙ§~êh,¼ýÜ}ïOÍ´Ý+Úókk‘¢™*9‹‡¡>ïX”Êç½&TZŒ{loÊ­ŸAé0]Å+e½IiM^à·æþ(*ÃM#B:¹Î4]Ú ëyP¯Ðìryó6WPÚÿ?¯AU”Ç+Ê»èd|]ÕÑKýkÀ_cŠ¡6]Mާ¥Ò†ÀCj»Ü¯ÊqP€ãjí„Aø©ì5YÐ×`J[×uøØÿöæÝðѼsÎ9O>ýÌjíÿµro…®bÿkX§É¨©Ê:1йQÎ}J ÿûW¿Æ›þôUÈ_þÒXhð£S «]we¢àšZ0-)š&F'À­[XS9,g5™þºn'ˆ–³i´˜ÓÆÂ›{Ïïý¼&GµT…š¬& ¢Û$ …äU³àé:ª´ä¶À·æµb\ãœs‡è7íüUÏé`CSšÈÁùj¡””þ 0¢½Æ УÖÑÍ6Q­õ¶6?¥1 ­B=º»aK››-^Û\GH‡e¥òsቴ7b1þ¬•Á÷{©Í*´¹Ü 4í½Û„pBš2Q0(ÓݵP9”¹è¨€Op©Ö&Gid&¨·8 Ên¨tÚ qü@ã胚P­¥ÕÝÿ^“©çÁh©È¼¸×©­P7Þ£šä¸h2ª•÷gå!Å:¯ ÒìY³kMéG³?>÷ÜÿÞþýÊ÷ñé«Dtq{öÅJuñ(jªUÐ×ó¨Y\˜jS ‹LÀ<‘½:©y4$>>Ï’—õ8Ú\i¡§ÇÁEýªô7¹Yà¿s–Ç-åŸQDCÝÁ¼ó0žÝ:^¯ÁRCÎ@âsÆöО 5¯%Ï}u?y±™AW«§×ê ÖÆÒ»†ŒŽt!J+dÄÅË{ŠýXïÿaXiÓ“ýßëÞÿ:ÐýÏçø?sÿôïÃk·Øsý46ßt“ÕÞÿ/åY÷T‰yBêe‡Œaû)“qÉUߨœ¸È^ðõk±ý”Éb¢“ö½^§~úh,¾é:ÃCì*œ¼S©øõsáÁ6Âêžæ¡Iª¡EUÖÆèDZ'Ü^c¡Í‹æŠxŸAÍ ¼Æ§ uÐ`×Õµ¾¥ˆOÝg~ô/qÁ™§¸úO»£M¦w,œ £VßúZüµ÷îó6\~îÙUcª_.*"#dбÆIMø{ZŒkãæM—T÷¤27e>po¤WOóÍÒülF6ÿ«ƒ¡·ÿµ!^“ýϦ/eÿÏ9ç<<½b%68sý46›8aµ÷ÿKi‚šÞ`zžÓÓ{#=Ã97{M»Ìéqx/V@nŠïPÓ©:§Ž”ÏŽG q‘¬P‡zˆ”6lùkÿÃåØn·½,B ‰:9ð)+M0È9§ôZÞû¨0ßÐFÈQÍ£¢¹ò`¸Ò(rØ-…BšëȈOÇðÛjêëL ߟ~—¯9uü³Æ,özVT /Ê5-³jÖC° ¡A¬óRW±š£Éë\›ŸÁ×¾/¤X¨q¹)‰®ªÄ43ÿ_¼ b"¤gPçÔ2ÑG6*Hv+a$fAfÛëòÿŒ° Mb^`._Ξ«¬C1û¾ƒ ç>û ÃÔÏ&Ÿ'¡Þƒ¢2î–u3ì|¶œ ¤Vÿæ}‚ÿ><¨®{€&up!W”˜ÙTÁØq«1ˆ 4‚:! »þUõÉíüìH¯‡˜Æú}“‰•þz‘¤È€™@Rq¯¡ƒ###èõzxå+þ÷ÜÿÀ¸›wß{?^ùŠ?¨lµèñ ¾$™\« ž){Ló1ÓqE¿¸âòì>‹ë/™ë/™o-8;m·m iÕë°Æ]Ó¼…ªSlOÓÂ×Fâ¸õ¸¡Z,k# Í ç ©æ¦ UòôNŠ.jªÄ^ÆÏxMœ§Åb‚—¬2ÍßßžøY~È_VÔ>u­ãl(nºòûæs¨¨…¢üuþ}¦šñúbã5OàsŽEç5ÖWôÿbö?¾®ý¯zk¶ÿë°ß³ÿŸZ±ÇužY9h„æ÷l±Ù¦ãîÿµq?í÷‡…V¯16oÿ—æÓqªè í¨ö¤PJ … 6”U‹•Œ¶p2»±vš+.lª&ÌièÌd× Wt3~€Õ×Ïä_îµQc×8]Ÿ]"mŠi¦¤y-ë!+‡š‚Œ'Ït>¸ijs}Äâ–ׯŘqçãõB(]>ÞÔ‘^«E+Ó‡Ø.¸ zÄJ[ÑJ}-3áwèMFƒÀ ¤˜÷D2öÓ­ÅüÑ zÍÀ=.t¬]œah´Àë¼a'8´o-Ë-x' Ý÷bŸžÃ| 8Üÿ¡‘ê›  0˜pÓjÿƒQóÏDG»«”R£ÿ¡A€jkZT(™f“-—½æÊÜW¤ c4£ÅKòùq<úƒï`é7c·7îÔiàYJÇqö Ÿ|pÆQøÀ_‰Îúþt?©´4^ÞŠnªñ,ƒµ çŒEIXXϨ‚NÜ»hhJÁñì^Q• "¼ þ³îÅ ¡ì²çö¾Æ¯QšÇO㼌Fh¬i\]ŠÒ¯¼i‹æÑ0]SåéjF4;H›vß㦱 =Ò½Öôš‚ú¬ýÏ6ê]ûŸC†×dÿ+"ÃTÈ.«xµuçŸzfŽwŽA„6ßlSwÿk¿¼HP…iðyØ&¥œ¨YEü…µ”’®éἋb¤D°:²VÁ™¨…ÿœŸæ¹¯ (/_$Õ)æe­e*œ4|L‰1èòœ¢¦ž.E ¥ ;]nŠ˜®Ç”å©è1ÚÌH0hn0ŒûÐéQo =’yYód´²*“ r¨m:iµraªÔ*C Š˜¬U¯Ñ™æ¨-ÐbAt’µ8†¥%¶ùÎ Rcéu!èÔ-rYƒ¤“zT´¸6gÈ( ß,RflÿasÕRß_/Œ‚rLM%Ò¯Ѹá#nÐy5½Æ4´žö„<Bë¡ÓžqŠAc²Ÿ9¦êw̽I‚~ÍÞ¡LóÌPãgÿ£ v5Ÿ™èz…JGŸ§²wnv]î_Ã{hìG×Y×è„Ðì9`FBÊŽ‹©$ÑGæ¼ ¢sKëE¶ŠÇUÔÍßë Ä|xÙ(¦ä,¾ïg.×?„€éù0^6j„Ó:aÏÅO…ÿÇÁUœçvïåõ¾:wv{ãNØö­ïP]>u4¾~þ°ÍŸýyù S?ü|äÈOañOVå¹ðMí‡yñl¼ÑFøÉÏ*dzdÙ(|äÑòp›{ì§°ý”­KGŽÙg~¡üì_>m¸!à¦Ewà¢+¯®4 Çý×xõÆaã 7Äkÿps<ûüó˜öÉÙ¸ìKgaã6,ÏÞ·ïÛ1ý#”¿ß}ïý˜7ÿ"ÀÙ'|ÿúÜsØaÊdÀ´cfã Ûo‹Ó?wLùùÏþ~öð#òó¾}ߎX~îC3FŒÑýz>†o-8¿ý?ù)Î<ÿB„pù¹gãûw߃ýöÙ/û9Žû¼ßw`ÆÁ”í~ÂY_*·øãþk¼yؼ޴è\üµkª&eÆÁb×7ìˆçžÛMÞÇÍ;=² —ùll<<Ç7Þö=\rÕu½ÛnòVõùs|öô³ñÕy§âÞ‚¯~æðC®;툣Nú<=üœúIxí›c»É[c¿}Þ†ãÏú"ΘݞÇÎú|äÑjB—Q¶éî*Ò½"_›ê./E¿Ô`Àsº M"*ÚæÚØÿJ=ôö¿‡ä¬ÎþW­‘—+¤Í<Ÿ nHóû,ú7ï ˜{ì§±éÄ 8ósÇà¸yçTf ]·/æë†'ÇÏqëR*?Œ+!;"]ÂYü†$Y꺫IAÜ™RýùÌVcâ!7оt!ÙÍP ¯¯£¼ …©jFŒî@í•–#bí.·¼lÞ nÌùé ýj°c±‘v¬‰ù8ØaN¿Œ±CHÖB7žfJ^Lã»Õ õ¨j(Y;Ak–¶âÞ—›]Dk›,ç5HQPØ8ÔîdT(Á6-ös©¶Ã®¤! ¡*ÛõÌÎe¤ŠHT c;ûõÖ"HæÙ@ ^6‹ ›é®ýŸÍ3˜©®k•Ú§WÔÇà \’¥…–A|æ†fXÏbô-Ñ6´Œ´Ûw6ô”pC×L©jòÕ¬¥Ë-/£´JÔ|¥UíF­ŠNèx&Ð=ÞÛU@­ÖÀb)>¸‡3‰CGÂí½ó|[×› ž¼êÄ›é7<ÕΓùwÏþÊ%ØmçðÞw¾­ºiŒŒŒà=ïx+vÛy'œõ•†ÓÏEŽ:–"s!Cx矽?nN)žæœsVþö_qòßÌ*ïÛ~XÑô<ú‰oùxï}` öÛgo¼÷o«hEÇΚ‰7Úšy4>|ø'ðš-6Ç̈”.8ã<ôèhAöÛgoì¸íG³l?ek,üîøàŒ£ðìsÏã[ ÎÇÜù•¿ôhš¯ßnLÿÈ8ñì/dêÍoÚ¹¼g{ì²3æÍ¿S?ù9¤”púçŽÁ%_¿œqnZô=Ìž5Ó¥bÍ8ø@œpÖ—°ÿôYøàŒ£RÂNÛm‹ˆãÏú">4óh,¾ïg˜úÉEÏsñ×®Åg…ãæ~oÞõØqÛ)åüí½çnØú,Ì>óïð¾w¾Ó>ÇõEì?}.¾òšr}í›#”Ï·ß>{›ë¨×n±9ùù/±ÿôYxè‘eƒsüÈ(>8ã(ÖñÞw¾ ;l33>møÿàC3ÆþêHÌ>óï:,‰SE9‰1âãÇÍÁÒÑŸã¦Ewàu$zdŽ;j&|ýZì?}>4óh<øÈ£®ý·g°à51]ɪAa}“Þ(=ÔK› ¨ã•Bÿ/eÿwMmóþW#‹5ÝÿЍ)µ³‹ÂæÙ™—;اƤM7­öÿÚüãiö²…Ñú9ÖÐÊû6¡.Œã‚VM~*íŠòæ JaaË×.=‚™è&Ô´:§‘2(FÄÕ]JDË<W5ãêp§”C¥ñÄûÒ„º ¨izuƒ¥ã˜âb¨×1bnFZÛÒü0õ'ÀuŽS¦Ò¤ý®2ª uh®)ø2%qTÉ v¦Yr#oL9µ‰ö5µËP5MÀc*ÏÙXO-­-%EjH¯æË2 Ìµ¡¦!¨öÐÒäLÖL^›„Zµf4MÕè°Õµ·ÿ / \Þtÿ¨ š"¿†¦¦È*¬ |•õ–Z$$#Df½ âSBG%¡{DÄšêZÓpQÓfLKb´Íz¨Mý Üpëwq÷O~êZØjŠ<„úŒ}àìþÆ7¾>ÿœBw[ö/ÿŒ ¯~•y`ýú7Oºä?ä/ñ­ç­ÏŽÛN©è4íÖ1ãàñÞ°ý¶¥Ùq›)¸àÒ¯•ówÇ‹1eË?ÆÎ;l‡Wm¼!N?÷+€–>‚¥£á­oÞ­ |Ó\:únøçÛBÀ¯Ÿø –Ž>†—P†_ýŸ'ðª6ìÿ®wâG÷ÝŸ=üH9ž›}“ÿøÊÍéèc¡˜ùÑ¿ÄÃË~Žn½páW æ8yÓMþãÿfÖÅþï~'î¾÷~<ôÈ2„pÆy_Åk¶˜„^¯‡ ¯¸ 7-úbŒxèÑQ<ûÜó˜²å•×ýÞ‹Ëu|ëž»ãî{Z^çÆEß_ÀãO<‰3Ï»1F\tå5€×S3ÕÈÏ>÷<.ºòêÁÏl· 6ÞxCÌ=ÿB¤”ðàÒGñð²ŸãOwß!l¼á†fý˜‡T9îÁ×ÆÆÆ†k‹CJm.PðšÍ7¯L ¼›‰®].ø=úŸj¸dú›G×㦇©{ª?ÊNdž^`mìþ ê766V®RæVwÿóïy:0µW©÷›mŠyÇ~ ›Nœ€§W¬Äñg,}¤ÚÿkCÄŸmÐ\‘®ªoQ/ïw]ê l‘âeJ—3ØI¼§Ùð?öK)2Wm´Q9'¿}öÙr>&mº ¶Ÿ²5®_p~uƒUÍÈñg}gÎþL?ø,øúµ¸áŸoÇ«7ÞÛMÞº¥½ o¹ÀžúÉxínînþ'ž\^Þcã 7Äwý¨žèÑ$³®ÃMÆÚ‘ÁMxöùçËgžòÇ„7ÜÐPòò9¾øk×b³M6Áõ—ÌdzÏ=C?ñYš´×ë¨7t>IPÑa»f¦~âs¸þ’ùxï>{ã¦Û¿‹¿v ikØ'£Y\è²gLÁºESô\wíO¾½0V5m¼Ö_ÊþW‹õ.´K¿¶&ûßsôcÚ\n ù3+Z–ÿóÍ6ÅÜÙŸÂ& Ðqó¾€§V¬ti²k« RZa9 {¼\‰’™"SI/É[]ÂøwKa— ѪÁh?@=[™Ü—”êÉ©N<ù©&´¢S[p-ž ½…­x‰c´á©•ˆY*ø3³›[Öâ1¶k IDAT˜Äz*6¹q)Ÿµ©óH ÕD)‘©u͈Zc#ù +ÓÇ2•†‹¶â ‡¦Ê92Eq®½ƒµµæ©;Ô-[ÚQª0C©ZŠ4 Ñëß9´iB17` Ddíß+Z\*¨OF±Õ@Ä!â28âœÔjøR ÅðÙ©o![P—ç~ä±Ev)¨“äÅZää¹ä¯WÏ7¦hõ‚qc3hJHõµgJYhõ(¬»ÕÌFQ]!R…ü˜gD.4VÓè÷Vè+1`×UiÂ*gBóTŠž4Ù9(¼üŽPæŒ6“Ö¿y¶Ñü©£"Rê0böS¥ÃìBv”B˜]I•¢¨Hï~ì;ÔÆVS,™AëEX*S^<®¿Gç !àÝo4@s¾x>nZtàÀ¿>¦šÖræJžl{AŸüsé˜è/9põcù!\zí·* ê¨GJÃÉÇÏÿÕ×yèÑe¸ñ¶ïa×7ì`P”Ï=ÿ"<ôÈ2³ wÜvJ)¾5ÇëÌ`h´ÎVþë¿bҦ嵀gŸNnü©èX§¤Ó€üÙÆÆÆðÐ#Ëð¡™GcÇm¦àŒÙÇàç¿ü5~ûìsøÑ}÷ãôs¿Råô\vîY¸ãÎŘuâiH)áò/ŸírdcŒxöùçðÚ-¶p X4ɦڕk5úË_áÙçžÇ´cfWù7!œyÞWÑëõp쬙øÊÜ9˜uÂixö¹çM“ÉçºiŒ=vÙÙÕbd´Þ¿ï;Š9÷¼ã®ÅØoŸ½Ëß8ô ì¸ÍäÚu¤N³£¹É“þœãhšE>ßý~÷=¸¤\ûß>ûv}ÃŽe]¾uÏÝÛó•êϱùf›T.zMÓàñßëaº\™"¥t³¦iJ6—‡Þx7Ä~¿ÞHÏ8ˆH¼©©¬—y1ûß³]ÿ}ímh”æÆ¦ãíÿ9Ÿ:›Nœ8D€ÎÁ“O==îþ_[H¡µ°F¨+$¨ˆ´½ÐIuüê Wq!„„ʹ‹ ò¼þ½‰{…Œ8F_Þ"JA µ-7l]a°Œ ðû2VQÖxŠ©¡ª’Ƈ.ƒ˜8á“U>éaªHBIJí4·×͹ÓסscŠ@ @Uz”+Z¤¡8z%l«ôKµ÷\sAe7„N7€È¡°ÎqìWŠ8„ŠMQS Aèå0hxÍò¾‹D•n ”¦I3‚ìpˆ Å&Ф/9÷%ÊZáµ[LS|g¯Ê6¾Á€¡qPך^SgCTÍj`Îþ×`b7õ¶‰®†$@¥±R3½çA¥Œko<ølB5pÑð_PS•a”)e¢BYL†[ÊqÓk¬Ë[¨Zõܘau„6™*œÖ幩òÑš“›ü`Ý×y$H…Ç\øx:‡”ÞûηaÎß…SÎ97Þö½*σž‹ žØvQm´˜O)áÒk¿…£?vùþ-xìW¿ÆŸt:8⓸îÂsñÈ÷o)¿sÛ¿Ü)ÅK0'6{Žü9–ýâÊÕãO<‰?pìÜs0ÿô“Í÷/þÚµXxû…>µß>{—ï}pÆQ•®£ë&깉=¸ôQ\rÕu8söߔ߹ñ¶ïŽ)\ðУ£XðõkÍÏ?þÄ“½Éç £?ùÏÝ÷Þû—,ÅKÅk¶˜TŽ/X2t{ûþ]÷`úÁ`úÁàñß<‰gŸ{žn*í ¥„oßú]¼v‹-pý%óË{,øúuƒ&„èpÊcßÔ 6?è°cŽÅ·.>¯˜V÷ºcg^Üæ2Õ¯iÌ›.ýâ<|óâó?ºï~¼î·0k.7Tß¿{qÑô·çâóŸý¤¡<~ûÖïVk×Ò7m–“æÙhÓ¡MI5eS‘‹Ò¼ Ó²óÍ­?Ö76]ªkºÿ=zÜïsÿë5âó«¹Eº—Ùòkÿøm|ôïÃ)_ø2ž^ù[sl]v£k“Wíÿ¦N/7IíäÌæã­y@„ J…Æ # âJ«ì‹R<8SI.œ* eýlC«]ãØ&SP¶s® B*ç(8ÓPˆuµÚçfT€-©K“R…zŠ|tNt.JS©Ü¯È©ªB.mÞ #5T¬±fŠ \¤*ùëØ4ŒêP˜j-;Àu…LWn|²~ùëå<'‹BÆ4°÷Éê€ØÌ ýüq³HFx=J¬iPóa£ÓÁIÖÎ>ëµ2®ƒ Ñ"#N8±EŒìÚh?Ÿã¢“1`Ú•A_"ªì+Fþª"<8h© Õ¼ÞôXíÿ&4èǾmúRMÙ,Bhj ›˜Çp™›Ãxà5G÷ãÒ$´Ü‹R¨uS½ºÙ1öíh9:p㢴UÏM³hŸ¢ XJgPÕp 3Õý¡á‡6Ûye™2­Lbý³Óפß-\à¢\\ð¢ß¾o/ ÐM‹îp‹¦¿t§.Û"ŒmvuÒËŸÑ µãejLp\UØA„ j¥"qX©ç€åi/˜Æ¦…ŸR½i3Óòº¾Ï×C [Evø¸U잇?'¿Gb¨þ®XQ•® Ÿ/ï}¸A¯pî:O¢ïÁ: ¥•qP.°½óÐÖÊ:3mj¸ ÷r«ô<2báiWJaÙ:3œô1ºâ4¬Éþ÷h•ëóþÿÊÉŸ}I÷Ø™CËÎýíþÿ¿Ïý«8Y¡SÁYãQœÚ*7!nLè*š>µ£U~¾«fBí¹JûóÞGï».åFÂ^«ÀXÇÀ€ßÃ4 B+c‹ë²?BMÅs-¿¹hLuS£“fS,:Sòrœž]µ4Ÿ\„ñt©-z}*ô¡j~ÔR¼²g²J¯¦bÜ„!T:OóÅkœ‘‰œ Ô®…Pë ¢w}Ž tEMi^Ú“ì5GêF—ÊkØ{t*Îq¡ ¡tH2¤¿55½ÒÛÿj·^jrj³Öáuc£Yeã}œu¯k¨¢¯i(ªg|Àš*Ôÿê4Î]Ç &ª-bûw½—hÞNÑC:÷AϘ†‡Fêd™‡&f°}ŽÖ-Zƒx¨ ÌÀG ] ýpøû¯Øx‚}þ§è_µÿ'Ö}:œÒyòfÒÂ3ÇÎ:'aбí«Ò¾¼Äuc±)…˜7V;Ü< ÎïÉ9,\L1€orŒzéT^'äLâÍ+ÄYÇâ½Ó¹°V;_¯ÀÔPµôæcËÅ¡º[1EKÃ=µ÷&é2áeÝð¹gš—GŸâk”é_ùzðuazH>n.®½sáQ­øÿPj›®{/(U3y´ÀçëÄHgveóBU5t—VCQËúë›â©iöËŠøòõÑ×Zý¯ka}ßÿk ªötö?j÷7¦ia[&¬dUÍA…J])5X= ¿O/M eÓ¸Yä×°e¯N29ÇÉÜgU7É4šBßRM­š=ªý¿§¯Ò¢_©Hlº` B(*_jQlC'¥ävÏZi€.²”$D¶c¿zô· AL–>è¢bnÖKªÝ媢7ÀG)‰z§Ûz]8˜÷̾hØ!%¶Ì%;hðÿmNPþÙf˜Ë‘^¡É‹¡ Mm6>Ê9v3’×ël*ÒtzgB%l/Mg3¥ âŠºS­] öux•ç7ðƒÃívã D|®ŒžÍÝÿ1*(S²ªl5>œæ4CŒB›¼°ŽýÏè©6SJwS*_þùªžîér?oBÅàæ…ïeÅdׇ~î¦Öµ¶ì~ìF(Ô÷Þ‹¶Z/4A^W©iìùÿßú¡CŠ=²¸* ׂF }.TÔiÊËC©rè÷‘ð¦ìJ£ÉßË…©æ“hA«ˆ+M\@zM‚ç”å5Vy*Í…cv SÁºžK}ª½r×ôGÏ=OÅsÑÉM„Ò¬4TõP½^¯¯¶[οϖÑccc•#_kn"ÔíL?S>zêJWé•äúróЕká;}/IQ§5^kü¹òµî Œ úc}W#ÅkŸµ9kºÿu/¯ïûmÐ×dÿ[ŠO¬ŽY,«º¥¹© ¹âîkø¤»B™ÉEgvïr ì×öÖºY…’c´*š /Bg]û*º7EM¨©):Egý•)º›Ú­Ì4Lp2;$[D °|ΊV 4 ¦œ -Ô´)RíMif†¯UŠ+H–‹PôŠÞ†é—Áj·X_¥HŸÚž«+_WVŠÉ0b=Y‚›Vò\›ôä ’H…êf¨lB}Ë?Ûkzƈ'àö÷s³dŠB²Ì¬Ug fŠÔPœâ8ÐÕC8UçÁû¿²]jš›’ˆv¯Êåƒîe”tvÅqÍÙÿ& ˜QNq³lz­Ó¢±j—ßg‡ÀLõ6÷j¼yVn‡1¹ûVïcE£—• 6½h¶˜`rl@¹‡æÏ9 0÷Òq ã'´Í¿Ñô ƒYsSÃè•ËΉÉÚ¹wäÏåAkì£Õ9w ëINNÛ½bõì*å ÙC\_ß+´ÈÒ u…Æ+ì=Jÿ«Å®w8F©\*VÍE´«lìtÅß÷Pæ¦<7Ѝèûx×H§.ZXx6ZÄ)£×˜‹ÕÜÀ1 åi2rq¯šiñ® 3Ó§¼ù\z×È35à}áìJ³ãu¦…q—†"yºž¹ë÷ûýñ^m^Þÿãïÿ—úgM÷ÿªRÅ‹œ¬n=×2 Æå½ÚÉòtß ,~¡vÇ‚Æ>|² ZÿËM7EY¢|BR<¤E]ÛrsSé˸¸&ÍQež’ë gÎU+¹ Q§¼L=S[g-ô½ªô_©^¯Æ9M;-꼆—µ<&0¡³Ø3z)±-ö´ƒÆô‚,/wEM)ªL‡Bg)‡ÉAV¸Ñü¿u½’¨\—6œ½ Ö¡¸¸Ò>Œ1U?lª Ú!¦ÁqÃ48÷Ks« m1b½LåZÆvÔIеjjcj›EuÀÀè ßt0`E©0®f5úŒdöU¶‰ö¥r–Àd^»½Æì]½ð½Ô4—Ž©IeC÷)m`Ù*Û쾦Eî´‰ÑýïéÛMPz:ŸÿJiäϤŒ;z9?± i4Œ\×› Õe(µ„m¤ÕÑÊC¸ ÍE 6¹hdÚ¹ˆÒB«Ë…Ã0½î×›V«fC‹L¯¨ešO5Σµi!›‹H-’Ux®Z ÕxHHþ#(]Ô=F[øs°+—gÕͶÊL½ëâ¦z6-hyMä×S·2mƼIºÒʸÈ×ë•×;#.<}ñœÛª‚Ôã!SC¡È†Gô;]WŒüy®yÙAׄwóÔ÷|yÿ¿ÿ×ÎÝÿM½ÿMÑGø2)5òÃâeS(+E#ÔÈH.68èT×Á¶ÊÆyJÞÜ-®ˆº’ lF;ÌÃÝüª3›KzOÕøfZ`¬“ØKÔŸ.&âµ 6ÇB“a—&j-‘60¥© ¨B["ý˜êž1ï‹Z—¥6é…–MÚ).´ •R-‡ÇËuYo|¢‚ZE覩G]œ6-JÙq•i<<á6ï%¶ÀÙÑ«¥Ø] °‚t0Z”]àbŒó"É?_Šôa³Õ®ÓXÑ*¹P­7jª¡&¦€G¶jÐ1g÷5ºŒÜª‹¡G'ÕשLY‹©r+ìtª >Té½Õs5Z©ÑN!çF3ØÑ¡ˆ|ã‚'F%MP©¯^ðr¦½yu)SãôþéÖbÙ­º¬T²ÊÚáÁÀ°"Ó+Sã­ëMPž°s2¼Z\,( J'ò*ŽæÉ¿+Þ$I'µòŠ8o<^QäÑ­t#yï­ÂòjJ'kôµñ+µ¹òœ½XkÀt'ý,:÷&ñž²G×âu’Þ£–)*ä¡*¼¾rQ›_GוGubÝ’š#tѪ¼µâ5#]kÔ5& ý¡úÝmKéXú9Ù­œ—Þ€Zañþ˜¯«áϨèÉËûÕûÿ¥6Aãîÿ&TÔ¯ÐfÊÃù¡ÆóB5#õÀ¥I­>¼9(´©K“‹¥–qÎPõ>R0±kçÕð¤—ѵ®Uç"ƒ\QîåÝxú-Òji‹¥±Ãš&ÎW¶ßÉÒËÔ´¢Zsb¥ÍÔ#ÖI°û6›¦ »\¾×ò$^­Ô;÷–"rô~ŒÊiž®oƒ8‰°AÍB­÷bÂ6‡§¦ñ0 =Çë@Ѭì¾YSÞüç? …«2½ -}ÎsØó msÝÙà¡Ò˜DÒð±^ÏÐ {¥8*]“‡ ÅÔ"Ö!ÆåñŽ+‚™àƆ¨ò=É ©Ò[Õ)L×Lõç7”áà_ÕLò±C,é5äÕ±ýV3”BM£F°³‰:5ÉÆ½ÓqY4ëS 2Z–÷G¢Ðf`à—›!óüOë‰&Hµ \©Ë“þà]!…J£aJÐxúÕ°°;”gÅÛ¥Pª‘÷^ ç{™/]ǪÅÿªœ­˜dh`dSØô—v¦h£7¹yà K5GÈ_ST‹ ë±±±ê\xÔ0¯æk¯Å¸'6çóééH<äHH¥þ1²ãMˆº´"žˆ¾K ÏDE’ôÚðzWšX9_M¨¨Zè›óL§?Ö7Np5‘3¥1¾¼ÿW½ÿ_jÔ~öXïÿØVpÕÃQ'ë¡XZSØe.â¸)*k!E÷þÄ –Ò1+7:89L,¬4S(À™`R±¤“p¯ñð,t«4v§à1Í·ãfg¬£Aòtd;îM‹ ÍÆAf<Ç>ƒ8 ZÓ5Ñ®Ö5Ñf°ÔÆJ«Ût´PŠ6~:P±ý æw“=Æl"Õ™2Õý _»è µlŠ5=¬q°ù|5®œÏƶ¾ÙØ€¯cŒæç*zQ:¿¥V×2p‚ ?¼?å1Ù`‚T˜€ÝbÍ-4×.!½hBÌso¨½qkÇ@aÓÌ‹œ·—Ê=>Ô–ÒÒ„«…>„ží <בÑü=ÔûÍÓMò¹÷Ðb¦,rÃj¨Ô6?Ò£±® ¬4¡:ÇúºZǨÜ"ïŸV79°žÏÇÐýj °Î7AÀÈn-&=g3E´ÐÒ ®²Z¤ªå±—iÂ_Ë¢f-®T8®M‰þŽÚÚhq=žu5S~uÓl–‚Ð)ß,ó{rƒáщ<‡¬ìDÆ×4¤Œ1òÂt F x0Š‹\mšTàî0‹jEP”Çç:’ê̓ѰŒHhÞŒ6šê–§Ÿ™ßŸÑ‘.äL¯óˆ3‚£4œ2 £ì‘ FÚIÍñAÆ^Co¤×ætØ”óΣս¼ÿÇßÿkuïÕûظBĶÌ7ç‰&†fò(Îqåš$K{R[ÛÜ<-_¶K|'ŽûÔ'…nÔ˜ébšzrÚ<Ãò‚š˜¦±ÔƒÄIp²g¬i÷œ‘Ãï“i>ü¹5ôPݲ̤2”`t„'îjà`'ü“‘1)0ƒB¾bŒí5%m‘žU1=ºÍ uPÐ=7<]7EjpGÔ Ÿ %ëú×ÚkšaÓÓ†·Y@9ÜÑŠŠ3¤K6·LÁâû‚×&Ç6,Vçní«Å©‹—@z!œßç87p· Ö™>‡Ò)4GÑœ˜lT£4•£Z²Î—ü~z\ܸ¸9Aè‹©Î*×2&;Tij}cå–jŠSÔÌçC‘ñö6E¨Ð?øŽ³sÁHNÑIN^ŸÊÜ(Àe¼ð}£}þ£Ü&1Y·› 3ùçƒåÜ›L3U†3O´HQT³‰ö(W^‘Ä:5ЋÙëõ̱y–Ò¹aáB5þ\\W§¬#'EÝ»tî7=~=^=>`p½T‹ÃŽfL‡ãÆA5ž3ˆëÀD×FiO\ˆvY*sîŠwô|åµÒï÷+}’j´À_Wí§Mâuî!O|¸ gmj¬´Q.Ð~ÓBÆ~¬è\ w3:˜‘~5½¦¼FΆðP&ýã]‡—÷ÿšíÿ5ýÓ½ÿc÷þ‡åÈs1c¬¸øª˜yý]Á¡Y“Q¹8ûÜóqð±Ç|ÂÖxï{p×¢[°|Ù;Ñ íÓ t-41[p§½ÁN6SL­‹D=)ŽU©¦£1ŸŸi>l Ä V‘'cÁ,ÁNÿQg81‡‘,Ó($ÔyFÉj}ôuÕT¢šþvhÙ¼`Í® 6ƒ ¹J¡Ê©Ý®Ø‰«8Û®³I¯sÛózã\m`¸Éõ¦Ö`„¡¢_–õÓa‘ÏM£V¨ ¿³iZÄ%Õ¦…ªÕ„êÚÇXkñØ¿ O‚}þpch`ÂYeŒÁC¶‚¡šºq‘¦F]ifæs5¶Ð(cªè¿&Ü—ôr:bí 7ݼ4¦ BžØ–\‚›Õ~Z‡3Jù3ùJ1Ûà™½ëgcÑ¥!ëëUíŠ)=_Ñ¥¼w˜ž 2 aÝÈú  ò²Q¼¢~<+]¥G±ÀXé5Juñ””†ã…TòïxÔ/DÓ³äUÍgT úí®¹øãi»"yú«´“U½¦'ˆ÷¦í|3Ï"ó.9k#ôuõüä‚× iõÎ?_û±±±²ÆÆÆ:-»&!žs¯3F8ty*µ=çfMôò¹PKï. þ^Foz#½^gx³jù·5Í.öcùùŒÅ~,¿SÖ`ÇOÏ«¢½º^ÞÿãïÿµA7Ö}BƒÆÙÿ¢ÉðtÀ0¹<``³ªfHýTSÁám~Í—]Î<ù|úè#ÍÏÝ|ë"LÞjK÷÷<š‡æBˆµCùÁz×¢[ðÌŠ•xχò¹ùðÛL`ݬCÛ`èDdôЄ!¤ ¥«4–\$G7¤)÷Ex,z–€Áû˜Æ ±}?¡Êqqê…ªšÏ¥Ó^'ŒÕ IÖឺöUÙK©>v¦ù˜\ºNêW,:Wó€U91‚õ / ^n ¼‚P­À»²Ž¼ü$.pÝi¥´òzSãÕøx…§wÃfƒ€Üà0BáYd{Ô9~]¦^•ãíÇ‚ä0]ÍÓÇ”ã(ÎaŒ¹vÉBÿêJ×îšöæèåýß½ÿ× à^Q»÷eûšj‹kåzëdÖØg#t:yÔ£‹þ×e˜4el¶õö˜4eLš¼6›¼=¦~d]`Úáu!<òã»ð䲇ðä£á©Ñ‡±ðº«Já»ç;ß…÷pP…Àh±bè2˜yØTܵè–J'Áç¦únFòͪ®díˆ+S„q&ÃŒàiΉçðçR{dB^¡¢ÐÂÍЕ`s›Ø Ñœë`‹ò ÁJ­°›óx"å…€Ú-.W_T™ADÚê< ¡¬ƒ1´A ²}¯lÓƒ?äSS,Kf˜A*Ù–(x±«†P‹ü®õÂè­6aŠPy ¦A Q;›Ó‚TlT9FމŽr/=ae| ÔYX·>vÚ+뱟ª¦¸Ò«JàiAò¢è1ç¿¢¬ÎöR­arîçUÏLÉaZç› }sqÅÓX-»DÃZ¤* E ÕÒxÂe^+Õ„i2ãM)tbí-,-*3ýLÓîµ“ÖÆ@§Þª1ð&Óå¸Fzn³‘¿–¯‹ç4–Ï£N¶»Þ“Ѧy©I¯ öE~òï(ªÄµGŒ½›Ü IDATðùÏ¡©Þd!¿êˆ¼uÇh™7ñ¨ZÞ„$ìÜ–¯eìÇʤ‚›W>?ž>Kÿå¯k3æ=Œùw=¼¼ÿýý¿¶î­ûøË{ž6 ôIHJ¥0Øfâ—ÿ/<¹ì!,_¶O>ŒåË–`ùè’ªP«t¡æíkAl¦ãÑš[ÚØ|ÊŽ˜4yLÙz+\váüjªËô.“zŸ„Ôe]ž`ÎM—#ä^{ìŽºê ¼ÿ#‡š½É;à©§Ÿ®òV<ª–ëEöÂ!ؽ—¯…šTèD×›àê";|Q¦c%Iš¡·æ™u¦Y>èvN4¬°/^‡ížl%LÑ²Š¾Kï5¡¨™i¥dÓíCÍwJba7K-]®ÒåHNÓxvúLKË6ÇÕ -â¢Ó"^>K„×iYCEZªfBCUQ»“ò}À°‚Q¡Z˜ÜÐfÚveïTÂ܈­³d¡Ëõ£»WÊgr—AR<ô£qºÓ@Ys\È/Ç GK¡Úk/Ty¦óõj,Ų+óŽ× ´˜6<^ã™;Ó{ ?ÿ[-%£·ë<Ž‹gÏSr¦3yÉïìD¥“`¯#UªŒÒ¿´SÁyþ,^&Nƒ•J¦…9Søpñrƒ´˜õŠc.ꔢÄT#¦äe¬n¨¼]Ò<ô¤KÏ×T_‹Õ{䯷ž¸¡a*™—'¤™.ú–‡ ðçëÊfò,¹½¦Ç ^õ3mˆ5´VÑ„Lƒó>›Œêéµ´PD¯‘:Õñ1«†æåý?þþ_[M»ÿ³£_h5^ ©ÐÛ*jLªÑ.•Bª…êÃ×9`ê_•¢‚)^O>ÜRëкÉiaicÝ7š—"Ú^¿oüÓ·±ÏÞoÜuÛ-XtÇ÷qܜӱðº«ñÔÓOc»m¦êÝû?r(îúÑ=¦¨›;çD̘v(`ù²%¸ùÖE˜zø‘˜yØTœyÊ †Â7íˆYôÆ×¿+V®Ä]‹ï)çiÚ³00}Ú!Õë.üÆÕØ}×]+V®Äv»í¸üâ °Ù&›b“‰0y«-1úØ/0y«-±Û›vÁŒi‡âý‚þh±uʢ룗A}d¢nt1UÍ€ç6§'µ;Ï^ÛWh2¹VåµÂ´S\'kã\>‚¥ÐHɯe-àÉ5±]@íÑ„L÷jÚT{uMKšäE ðýe8˜B¦ù4[n"ºBMy P¨:ÊÅrc÷i¡· 5¶r[#z³f’PÊCGÀh´Ú­L­3ŠÜ‹™šåoÿd´oÅô)ÀÍ«tt|ÏoPc£5ãë Bƒ¥6CŸ„:¬W!E˜âéš ôãžk†Áglu”M^ã?k+»sÉ j)¨„ˆ÷ç•|;„²ïÖy$ÈsBc×+R«@™‹]ÏRzuþÎ_^(^ð¥gu¬µ"$ì©‹Ê£êx¶ËÙ‰L§éZ`…Ž]㈾ù<ë×ãö(]:ç‚’_S_— _.ªóë©=¶¢ŠÒx¯F¦ôø¼Ì‰üY²>]Åûš#¤!±ž6ˆßƒ©„º¾ÔeÏ[/Št¡z­½5͈g3®”¹®ãQD°â¦Ó^×°Ó—÷ÿøûÿ¥™’'¤&me- |J—qdG¯ªa‡Ÿ™ÁüþÜÔÄ1ócS±|t‰¡Š-]‚Ë/º Z·Œâ0%ÇœWņßfÅJ77æÝû]I“wÀâŸÜ‹Sf¦zèwêé8þ´30úØ/0iʘzÄ‘ØkÝpæ)'àýRh|ïÞwŸAS$hÚ…ÿp)àÎ[o6 Áì9§áøS¯»ù”1õˆ–¸Ùäí±ù”±båoqù…”éûî»î2ø¼SvÀžû¼ ‹r\v6Ÿ²#îüÑ=vz'Ì•±7N¤‹RW9CLu±il»…ŠÄa”Šôqè+›Kä‚Ê4O‚˜¢_3ª†œ4KYˆ2|S¬§‡POdM!9L{ƒ¡o%2¿H†<ùâç"yÃ>VÕJª+[…ªØå¨gΣCé2Á£ÉR6+}@E©Ô¦:þžNÙËKª"Ä,Áäф֌A)­ž¦Mµ(jÛ]Ñ Ù|¤ .Z¡iÚÜI°ª‡˜dí¼ioWá©I¨¡þìåý$I¯c9±6»áÜŸ*¼6XT¶Ò•‡ÚŒ¡þé!³© k/×+ëŽõ :‘Ö€POÄœ§²]ˆ@¿ß/´²®÷WZŽ—¢úuûR§¶*N¬“™~¥HˆNѳc–GQâIzŒ/üßLž ÄüÚ\äwñ7ûý>z#½JÓ£ÓòñògÔBÛ›Îóͯ­ê>ô&4žï<öÕÆÊ‘^Wv„ÓóËŸ[)F^ާUbºžj•tðb{s^ê WÖl΄é5U.7.] ”®é 6Ø B}º¸úÚdpC£æ/ïÿUïÿ—:dêõšÕßÿü nêpHCZUþ ¹]U¢`§9âc‡á¨Ã§ã ç]0 ®MÙ›OÙ3?ñ)l·Í”Šþå…ˆ²FÅ›ªÀô©‡àÞûï·èÇpÝ|ë"\ô—øæ 7bÊÖ[ÙɳC€Y3§ãæ[™Æã’˯ěv~Cuœ°íŸì‰gV¬ÄòÑ%XxÝU¶È—õûž*\K]†Í6Ý´ú£ý_z¹C¯JÅÆ Qtä±d¾jȨjÄM­jª‚8Ž¡.ÒT ÎT4vÝ3…¡ÐiÛ†lдÄc¿åÑɿѧ¥J÷Ñ„š–ÕTY;Úœcõsl;Ýeˆ &GÞý+S¸Xóë É<9m²™ˆ†·2ZÖe ûß3Ñ$Óè<ºžGŸ¬rv¸ ÞŠYªÔuŽP x6:R„΂_›¾ QI5UQ×1ÛêW2º¯1Be†Ñæ° U.“aÀ4ÁE~L=hÀK¯gö»óüggZ¥Ïw iQîGk†Ñ3a©qýA‚4ÑÓðÀtÇ»О­qçEFm‰Û…2pÞ‹?já§Å›#\ð*¢ãÙV³}0 º•§”%ÍëÑbW'øMÓ ò`ä3ë\GÕÚY-¥õÏ?§/£6c:9× ÏíN¯©ŠØ¹¸W÷9‚ç5jІ(ZäÚX‹ö‹Ï›Úus£Öe P´½¶o è9Ê …ÚJ+ÅKmDaÑ÷á‰ýËûßßÿk“vìîÿèìˆP×3Å ì C7kWµ”Õf€‹‘OâH|ýÚobÞ¿læ×û&ìùÎwaÒ”ª‰®ÞCL.¾3O>ËG—`ù²%XpÙ8þÔ3ì›8#jï=®zXÜl¶é&øÕãWf›Lœh¨Øï€`³ÉÛ–,þagÆÎÜSNÄ“tTïÞwŸ|b<³b¥ëº¥k»BíšP#}©F2™ºdôŠh¨.›'Ää¾g÷¸VÁ:Ou $7æ¸óå:Ô.X-åfðʧágPÑN þM1V´](oEÜ‚ÉYÊÅ[-{ûß0)b­#äF é5&S©Blš:#ÊdWµ;÷?gW寮è\bì´”×f€3ÆX§c4ˆ¨³sªÜFžØ^[î1fM#Uz@^ÃÆ˜È¡à]•Ї+£ Ÿ~7ü¬±«¼-4G ¶MØëƒ Âè¼r 4p^yfO%VCÖ˜g¼Å”E¯®ôIbÇ×E+$›u^äuÕÞ@ n©à¯eªüµÿ½wº«*ÏÅŸ¹ö—üÆPÂUÐSL´`S$xA´ x«×_崢ܪA ¨h(­ ^¹yIP¼€- –»§ˆ ”¨‰z*T!tüÆšoÏùûc¯9×ó>ó;äô R?#É÷íoïu›k½ïûÜÔéIƒ,éPŠŠÇå÷¨MÞÎsÓG×sB[1ºæäèÔ-íqÍ­üs…€yº=MKáѱ´`o=ðùÓ}÷Õ8p~‹Âç¼ÝÜ4±C™66lñíY–·Dþyáçë*†"Z˜z×¹îÛ´F¹"$«9Ñænš§¿Ò̦­Gï=9ËçJ ¼PÕ?¬ý¯ë×x1I9®£ãq,Iݞ˗¹Ö’EÔ©Ì›€šB#ÕÓíæïX ´½âžitÌ­/–ƼЦebŒpÞç—Ú¼ GcŠ«(²Ê'*ßʇÂvÛnc§Ï)á¡U«ê–hf^ùÆ¿ÆO¿3>hÿê>¾ô¼³ð'óçcë§?)%,=ç,ƒ™é4XÍòɯc½GUÂNšù÷²²j‹ªÜ¡@ôÆþ\™¬Î\ ~ª(a^WijˆFdžÕT×úm™4;“íÈûسø¸gÚ[J…—R(rùú B4¡ÊÙ&ÜÓ†´èÊúœô´¤Oà½Eµ†ÈE2ʱ CCP‚/ƒ“¥¥÷5¦Ÿ’ŽÅ EŠ.'9gN­ Z5×R?Z´…‡¬‘2Þd]ïØ­u¦±®X-„̸÷ ÇÉ›º|5Ûå xPQiÇ0ÜÛ<ëò² ±?A\甹†L/ÍvlÕtJ;4¨.+}n»á³)»Y¦ÁÜìPj³x˜Ðm S§øÀ0eGOŠÚ6ç‚U4‹é]>©Ðuô†Ã7 -DÃD=#ƒ‚¨Û:͹¥¯Ð)‘wÓäPÏr\gÇ.Í­4ãèê<*œ·ô¢×›¨6M^†ŽN¿tñ1¤9.\˜«1ƒºyiÏ͉j‰<:•cTC-•«ôtÖJ±¯×!ÿ.£3ݨ+Z0F,øø0òàåñ6*ͯ5ÕQ+ôiÖíÞõëúú_wªq~>‡Îú—sW=$“X'¸öÐÕ½IhÉ耊g½†LPãìË›þtŠ1$—ÃïŠwœÍænZÄgž{^¹×žxÞ®Ï)ß[tÀ~øúeWØ.¸?N9áƒe[^°ÛBl6w.îüÑ4y_Þž‡V­*ûú¼…Ï1HPž£ŽI Ûm³Í´JÇÇX_3¥ bz çÖû;#3lƒÎE Ûl9®V†ÞM #‘Hl›KÁÊÍKYOrE>Ï]ß 1ê”z„H ˜uÀþžÇÆš\ŸÙãƒ6µ{Ú ±5i µØ]QÞL144×.4T™d³ ǺÚv-h)§Æœkn铆L³º *B÷ƒJ‹1¿¶Ô!𛃖:ܨJ…<ÏkªÌ©i›çhš¬ÞÈ&„0q‚Ë×Éx®5X¥Ú%û™*Јãâ–’1ݨ$SŸÿÚØO®ATµF…dÃÚÓoM¢ù‹'Ê®û‰“ Ò²³e÷´Ó;a¬aªÓ„x ®Å>_äœ^ïi ”2¥“eï}¸Õnþ{¦D®¾c¬ßkÑØZph•žîˆü=Û`ïõ^3Ũ–—¤Í˜¢*ü}Î…aA>_“žÞ+˜ó9ñ(Ep<š£º¶q¯)¥1f[ãòÕôÈçUHB¢šâeš¨ŽBOœç¼¤7-ÄØ²Ö+®´°UÛg.–¼IŽ kS¢Íî…«%Zo¡^š£²&H©I­sÅSyÖ¡x!‘Œ–pA©…«.Ê’È9-J_GžN'ӞͶ‡zx(]«éá‚Øû<=®¬ÁÊt.EY4)\‰º™Í™3Ǽ¯š ¸“|‡§×._{Üx¶èºnÿ°þÛëÿìãß·N÷ØEÇ,ž¾þÇvý?ò»‡«›¡^ CuY`¦wòZHÚhJÌñ hgQ$k§Íˆ’ns¡Ì!¹:\LW6»š ¯ûjjž¡­ˆÅ°ûy°ÇG× ïŸ ƒô ~¦aÈÃé4s+tMdEigUµ‘êO§®Ê Qûà*HvÚ5•ß7Ó 5­Fðx¿= ZA…öfþäõÝg%¥ÍÏtjŸ?[™¥BÕKN}¦ñ16÷(²ˆvM†D´^Ué©¥À ²FÅ<ƒµa:‰ã8èPê×J.Lõçå})ÔÌ5]ë±FªªmÎÚ>±š6õ‹üÜüŽ@‹¦Ð‘8׆š©ðçé`ô&k¼ŒÉLŽhÝ»P™/W_±_õmmžÿžSjõÜs(Ï|-nòäÍìó¿G¼²1Ì€hMÖÕ“ßðî'>¤v°j—Ûšþ{êVH¢w°UÏ ¬Guimþ“žÄk’¼ 7‹ Þ»½ O"jÃÛ™ÛhfT¹^™pÝâPˆ·o¹h÷ø¿š¤ˆwü¼ IÞgÏÁÛÎÜ 1µ&¤Š·-#(Œø¬ =ààOo"U‰ÉU+‡f43Bèd…QmØò9ÑÌ¥–•¹·½Z‘/#H›Zoýýaý·×ÿº"íÞúï:Zÿ£ݨ^lq\9€ñû';EÎE»]LÅ(ŽXhtEÀœÏ‚ý~¡Í4&m› ·àÙàÍ—k.úoK±jO:ÙÜÁÐN´Ð“i¹¢s¹@/ÙÎ6QŒÈuÝÈPcrÑ©VÆfªÏÛÍt#øÆ¡[kF£1­è59­ÏÐ?™þ÷–R¥¼ÔÀ¨6"e>è²’-:[ ½(Ãn,KfZ «E›nêÐÖÔ VÔ)¡ÕÆ;·*ÛɳdN°ë 5Z7 ¦äÊ–4׆5A¸ÛZÿê ÝF©Ì¾…éÃüšLõ7´M$êïëúgj¡i0erïžCÕ9ÍhóÞ'A¨ŒÖ=b@ESãl¸|oe$ÑšÇÔ4ò†VÞ3¹ºwíA`^OlÈQ¡}ņ Oø&ˆ'ÇÙ*WiL\`´ò347EiU¬ ÐJ…Gµ˜VkÊibsEV<íN+íÞ˜«¥ 7¥÷ô.^#¤MމÓ(CšãiyôØh6)â©Éñ ëVআ—jƒåÑs!*¾<ÆsËÛÃ͆þ¯ –žG"¨çG s½Æsáªv×Üœ©mº¾?¯7O—¤ ¶"3Šnµ‚s=Ú~ÆÖ{ý¯*œ?É‹娸:)õAô6•~'¯?Õ¦k0ÍC¹ü\(ñ0ìv[Yi°â„ñ‚¦8X·:Œrå¹+ÖÛÁRÿ ¿ŸtN%×%Ip¦æyÀLVŸ/4¥Òàöç²ÐÉJºjdàj†ºpÕéh’qÔ*&Lg4Á¹ôCoJÅ‚ºrü ºÆ3e³–T¸ÕI”Ï!W·Ð¤8A”x»‡Ì˜ICV.ü}£æ™2ä—E =­\Þ´?™’ÿå Ì5¤X)b®Æ4ÙŒ¤Êþ¨MvÞ« CTo#úv4S—µ¢‚·œóÄkIŽqBFs“oö3¦ ,Ô5qJK‘²±<µG>>¹Ù0ô8¦g‰.®Ô°ëZóƒx}ªn‘ïIÜÄ(&W¥nKö¾®Ú:n„/›-oÇ4—¹ÁP½™^Ûÿ·Öÿ >Ÿ9åDl½Åæiý¯Ë×צNyÕm DM0VÒ±N‡7|v¡:•@+f¯ŠÖ&)&7Б‹Šêþ@H óêÙ]n‚0B¤!b !B^ójš(µÝF9ÿPý\g.>cÒ䕾˜_ãx “Ô|tæÔ–æ‘©kE˪F!Š’ÄÙ‹ ª‚¿ÐÊŠkhÊõiòJ’ù(js#D÷ B]˜~Ǩ¿g>&L©ã5SúÞw¢Íµ4»ýa€Ó£:ÿnårÑY„íäVQ’á åÚc4¦ 'ŽÑ ²Æ¥¬ûL9Ñj¼’sw4¢[-çù߯›\ü{¯a³6Ê IAy™æ%•¡¹úWîy•ƒŠÜÎ IDAT_eŠ‚TYŠëz-u锦é¼Ñ¡ä±FG›™j€êç¾6CÓ ±Ì`‹(–µIJh®ó,ŽyVŠ˜úA‹d#‘¾,ÿì ßå“ÏÅÓ²´Xá©užü}èÁ¸íλqåÿúvåèBÀ7¯¿·Ýy7Ž>dQMP}YJ‘ñ¬xµÈUê7\ ïöìpÇÕ—âœSO¬ø™¬wÑb› 7Oüž¿Þ³è@üðšËðžEÚ ø¨«L º®Ã5}?ýöU•>!oÛÎ87_ö ¢Q M«C:º5]à&Œã&¯$¡vã"Ôd@³]N$t„d»âòÞyMŽíLvU+×RgŽyLdB`t6§'#2±§ÆT&n™úÖÈÁC) d'Í47Ô%gXˆK`ê`²èß±¡‰°¦µívË*ßoRQZÂ˃Ä3¢RTHìº Á ˜JÉ×®¡7 JÉ÷‡bãŒÐtÔ!]Œ)Bç88l•‘+Ö *š¦h°:ç)•ÕÐsƒ ‰Õ{YglÛmø¨¹‡BŽ!kI?S2šôþš,u.¥T챇\¯TÉ4̱øúÒAQi¨crõ­Þóßc-d¦ÉµòœCÍš)ÏÿnDäÁü"?#fò0wCi‚¼©;B©£XŒ±Läw˜?ç_ôµR¸è"ƈó/úv˜?¯ò©Žh¹ FX|Ä;qË7¾f..U'¡ŽSže(Š'ßdãË{}ãóŸÁåŸÿŒ¡1©ÞQ¾(³qÂhfT:›l¼ Ânõé/Ÿ‘à©333“c€6Ú¨ž¤ÊRX\½ä_¯±ñ¬¿[BwmÞ•ò›¤ª9þ·ŸjsäQù<ƒ©Òë…¯Sƒ^uMV×õÆ]h:½é±ñt_úEŒ4“Ê[‹|W¦Òlås“ª±>Ö?óÖúgõ±®ÿŠnð8×ÿ‰gœ‰‡~›Ï‹SŽ™4BjTPé7Ö ä­GYÿyí:4³üÀ/nNI vÉhá‡9s½]­ã ¥åjêK”œbóÌŸçP[¼!†Räøß‰lp½0XÊã œÝI5àÚWÕ^e¨ŠRìzÛQ’‡›˜@M2› ì‡Á)rb̺¡ÐÿNêðgËÿPsŠó¡\ }cÄè3!E-±¶A›B°ù­Ž&5#I]æó܂У¿IñɯÕ{Ie„ÔΆ#¡;wÀì€`ÐMY-Š¢·æ:#t·¢™…}ÀCv ,Ù4ro`tʽŸyΊ(;Vèe0Ú|¶R[§¬zÈNk^¬Gåõ N<¤•Ò[ÿYÅÁ¤I„0•¥:¡:Sí­“8Êg(’S³]ïᢗ]°Ïç³Ôˆ§zþÇqOy E”©ˆ1EÌf·á'z4-¿Ã›ž{.b›l¼n¹ã®jâËÅø­?¼›l¼QU ªû߸†›™…²µxRG..ÌYóÁ.eù=nýáÝxöË_ƒŽ<¶lÓŽOŸ‡7úo®õµR‹´0ÏöSåñd’~ÆK0ÿ…/Ã,qk«mÝô÷{÷û𬗽·Ýy·)øôáÔ. s®Ÿü¹¹xdš™§ñLôœjÌô7¦Øu]g²„fEÊÕáø•ª(„!XaLÖØuFŽº]Qœ 3¦Ñä{œü@`‡Öæå•­I~À”5¬à¡§rÍHÒT[xGóĺÎH(ym<1ùÕ'¼;\«“Ì„—þ®…Éïy»í¼n½ã.×-§ë:,|Öüþ‘GÝ)·Š†yŠ{ÍEŸÅöÛm øÉ W⿺/Û÷­ˆ1⼿ÿöÚãå½ÞüÎ÷â_oÿ!àûÿüu\~í·°ÿ_?¡Âv$N9ö(l¿Ý¶XzÉeØçÕ¯ÀoV®ÄÒK.Ãq‡‚ënü¾{Ûí8îðCÛo·-–ç\÷›ðŽ£GŒçžvvÝiænúdüþ‘GñÝÛ~€w}|¹—çüâW÷â¾_ßÝî‚ë¾söÚãøÐ'ÎÂÒK.ë÷z =p_ì8Þ¤0{`%N=ó\\~Í·03gÆtì¹Àc Îu_ù<¶ßn[<}——ãvøÛÀ_õrýcYÿ^¨ëßsç[›õ¿&}ž3EêôZþÍÊqìiÇ)G‰-6›‹SŽ>Çžú1ÜÿàCƒÑI¯‡ZHÐ }pÇqÄhf©žŒzÃj¸’ + âLHèú9œKyÂ[Ü–Ø(Ìr±Yx.îQò¢ TðiÄÞYÇ@ì*zÜPÜ[Jœ)Å-P¡žEÎ}qUMzåaÞ w ¨ìu-šQuÐK­dâREÖÔì˜ÆM‘R·D÷“‘ž®3‚Ä÷«ríw6sCÀ}‡Ð3}vÇ´»jò¬‘¸ÜúÝ@”ºJ—Cß‹ÒÕך÷Ê@Ú$ï~ä¡-ä·X=3BHô½r_ô¶!Ô´Nã’Ù°˜g§:“¥“ÿÝùÃ’Œ˜°[]Œq ðÅd¨fiœ¬FHl¸«ã(k‚ c&âXçF'vÑ –ízýœ¡Ä¡T.ÎmB‹=³ÉXsH1¨¨Ðh /Ïì‚)“Ù1Žï%Æ¡rJp©q¥Tš=]y;´îòP?„µ­ÿ1a' Vf=C£´h‚Âæ<FT+Á_¶|½iWÓ‘ÿ¾èÍ…e+~Þ.6‰ªÄ‚ù½ßô7Xrñ?ááßþòâWâeû¾!œ÷÷®;-À3^ô <}—cé%—áKŸþ¨¡ íÿÆ×cßÃŽÄü¾ ·ÜqWùÜ—½xw¼â-oÃËö}«9×}ç{øð'Ï.Sã“Î8 çé«€KÎý$öÚãøÅ¯îÅÒK.Å/u/özáó±äŒÓÌ>m¿Ý¶ø6ÙOßãåøÛcWîU¯ùžxä‘GKS´é“ž„Žxç¤àû”)žf+…‚Ã|3yôQ,½ä2|ï¶Û±Ë‚gâ«gŸQMœU,ßrþòœê´YQšRF,Th¯7|fåÁÒÜœy×”fÇd}ŠÒ®ÉP(ÞCh 3ŽUŸMØò[Q.ÕÂ0JÔ²%W«j¦„iAÏ7;EŸ¸™àF‘·Êóx×Ëü@Ñ:½Ž×fýsƒ®N{-ëmÏ-?oåC«pÌ)-ˆÐ)ÇeÌ2%ΘÐßCOm ÚtÑï:›Ð°#µæ?5;Ž·®1ù~,C ¦xåµÓDŒ!¯.tF×S’Shr¥IVƒc(ZÉ%:ZyΨNœ>uý3íKCHÕê¹Ü—:ëdihðÎB]+Ù@EXÓœÅTQá†&›TG]õ9E«C¦*¦áRª¢Ð÷,ˆõ8Ús:nÐ?*úßk[Ï&C¥o\sUز2:(KÈ„¨vµ1‘qÞì÷=ß+ƑРԣ®%š mXHÐ4}O˜®.N§}>Î>y1^½×ŸãŠëþ¥š¿öe/Å®;ý)9ö„f÷ËSe*ûÏ`-p÷ÚãØ÷°#˶žtÆ™xíÞ/Áq‡‚}â쾩¹ ·þðîêBºäŠ«ðï÷? Ã/Üûï¿Á’‹/Åß}ýÿÀ¾öèºÏÝùYØyÁ3qûÝ?Æ>ï8¼|æµ}»/ÜOÙr Ü÷›ûËû¼ûø“jÍä˜|è“gã¾_ß_Š¢ùüGØ}á.¾Ã‡ëÜÄäcóº½_Їû;ì½ïß”Ÿsê‰ØkàˆƒÂ硉æ°(œ'÷Ó&þü=E²9ƒ7¹×\O(ïQ¦ZÂx¾©sá®nfm‘·A3sè[žš©ø~ZFްÀßÓÉð>ð¶ç¬$÷%M5#).Šà 9­cÿx׿§Ùñ²¶Tô¶ë_-Ý[¦|ÎõgU³&çcËÍ7Ç­*¯Ë¹Oë~onè/7—l ¶ªü°Îh‹fað”ÓˆŒƒ-b4§äì°»jj‹÷{…^‚öu¢"á\ÄÒ¯˜€O. S´T¨B•Jm ÚXF@59ƒutl©ûÏêú&ɤÇÓû0 « e³;Y‡«DM˰Ÿ¡4Sð ¥r†lˆ(?°BÑOº"A¬ œ¥ícy SãjǶªùÑFÖäa(Ä¢y §ùyÈ:£<‰d1sÈP’¦oêí%”ÜÇ•k¸–ú7Uî„~V ±q6÷zÉ·a¿¯Æ¼–¦ÛÅt/ãæjê™û{‰tOõ_QÅ’„Â&m*KkLöާ4Aˆ•¾†²”³L`rJ „o]g3±Â( ¡¤]˜ê€Z5Ù€kƒÝrÖ­öÙAƒZµŽ{í§VýÏÌ=yþÓùÝ`ÂRµèÖ IÏâ8Ÿ¬­·ØÀ‰G¾ ¯{Ùž8ÿ¢¯á–;î®;-ÀÛßòWØu§¸üšoÝt¦Üx¨AI„¦j¶Ú¾èÌOÝ·û~}¿ÛõžqÁÓœ(Ÿ¸œüþì8ÿi€où¾A2–ÿòcû?Ú/ÿó=°ô’K¿øÕ½¸÷׿¡<‡á9‘…¤ÿð÷áÙ vÄÜ'= s7}2à€}Þ€%ÿ“)p5X‘FþÙÜMŸŒïÞúsL¿së°×/ÀŽóŸf¦è:M˜™™©-§§žÎ… <.sñ«º"«3šÓr€ó43zSá㤋Z‹ý¼úyåè!lE<˜ Ç%kv¼ ¯ UšëF4¨V›O #ÕÀU¥Œi°hëÆúx׿wãÖæÏËiZÛõ¿¦ ¨i|g¯AÚj‹Í îÁUãý:~}ÿf-(ññ~)z¦h«Ò-¹Àâ¦Çè `õ-Jí0¿KDEÉè$•Ý¡»idЬdßêÆ’42¥i yð(WÄ'l²:YkQoô6=¥,Æq_;$Ó¬(;³E#æ/èO¡lYÍ£4ú§Ú[ƒ29(Ÿ›¯“ª ²¸‰+Í`p(cýû·5Ù³­9À4Sø„ÖfÌ·¦„“fw·Nûa¡Øuܨ9T½àü\ÑŸúÚ¥¹€3œñþm9é)MÙÁü,Xô¥²ÄFª)M¾™‚X8»>¹QR*ž¡¾)Í«h|lÞÀ¯ç¼0—:jÛ~£1 –.îšÅ4Ùǘ*7=ƒöt>åJµJÞ  Ü @Wã†IT¼ÒÉ}ÍÜóB› WÇX°{¬¡5 ÕÒ£qÊ›O’u¥k!ŒiÃkнŠÒÞ£²A®W}þ‡iCÒ©Ø+¨ôk4á5{¿'yìS¸ÿÁ‡ðw‡,Âg?v 6Ùx#üþ‘G±lù òþL¤{"c/ +øÏµg¼è••/œÛa¹HƒÇ öyÂvÑç 3ØÏk“wCKºë:|ù¬Ó±é“ž„k¾ýÜñ£{ðÚ½_Š<“"\4ÄË*ÈP€8cÁ/Òx¨–E X]˜ùzÐÆAѦ3±@ÝC*Ô”.½ÑŸ1å«F Cå¦Ów5[P;å ­{ÓBµæÎÎ}L7Óæ®…L j¼“«R­6›Š®i“åÙŽ¯¯õ¯Á·Ü¼«aÁã_ÿµÖˆ?·•‡¥Í/lÙ뀸ºåƒSõOëF‡ë\MY*\E¯ ÃCÖ Dåê;Eí@ ˆ­;¢J§³|?ñìMº¸Lùá °Ñ2R¨q) B%‚"\ûôÅL±ü9.•(Ô0¦fõÖÑС ýYÐ#9C1`‹ú. ôFN¢qÙ“FÏœ÷hQ ô0FkR\ß Ü74)üzÒâ˜f27:-·LYKŽØzÊ•­­‰ñ#mbàh„"£R|OkìÄY@ ´M<Þp…éC­[>ïFE]u8Љ(ß)¤YëÇMS‡ã8®P\ó:i*LSÐÁ:RšésêtföY´Gù3* ï*¸B·M°èY²”>5e0Ï{øa²LuC׿w«Õ 7Æ ¥ëj W@æZÑ3ª6²÷A¦ )bº`ÂO½ç?7·æé|}0_7-Í´Ø}¾iŽvßuÀ=Ën¬§ÕÝMu-LëRô@sh<}kQTã1èBÔé~ÞNÍ@Ê–" ¢ÑÊÞõ¯:!‰ñŠ‘ÇºþÕåßS‘¤iëÿ)[nSŽ9ª4@Çžú1<ðÐ*7j}6Cþ¾ À¸0T”Tð %g‚²Øâ* ½ËdXˆ8[u-H@DB 1|Fe¥šPуŒ…3LÐ7#$î yT͈ҳ:BE¸01Ûè0 ÔÌ<"dFQ‘òú¨S}Ò;´‰“Õ-š!½T]8´° (QØ"Oé¹Uru¤ùIòl ô=cÕ-Ÿ§ÇJª@H‹g±]ö‰ÙLÑãæÎ4ìÞCgSâM‘? îôXMÈåù_šúž"Í:¡R€¦P!¶!I ª"¶Îs(†jWÌ BPÓ EKï2È BÝÈÐï¤É À4qå34ØSò° %Œ¨dLE3ï“P5€¦‘c¤h¬Gd¤ƒuSL!le¦•&6ú”—?¢¹˜¹±©(d]·æ™xõ“AÞäZ¬Ìئø3µÏè™ò=³ Íh>N­Üœ ´ ),U å#r‘ž/ÈW½ôE8áÈwbñÇ?Ut@ÊÕgÁ½¢Ü•F£ª˜aØÏÿí^ÌÝôÉæFvûÝ?Æq‡jŠã¯~æ ¡ÖX!µN½½{÷x<ÆÃ¿ýž²å–X|Ä;qøÛÀ×ý îYþsì¼à™øÚgÎÀû¼ŸóIl¿Ý¶¸îÆïáßîý÷&‡s~³òAÀ‚žÃߺ?Î9õDüm·³'—‘’ÑhT~ž‹è«ÿåFÌÝôɸæ¢ÏâˆEâœÓNÂî Ÿƒ_üê^œ~ÞçË9Q½±\Œ«mvžö³æF¯ ¦rqCÐeràåÑza´^‘G§S»Ò¼òþxÆ™çí5žM4o£·ù¸2âÂÍ'7Œ^Ö7.­‡º—íÄz¶õö4FeýO£P®ïõß²¬æë‘9¼,„Ñh„Ž|Wß­2ŽpJuÕ°^î¯6«kÔÓŸ†¼#Õ‘é= #,¡ &+H¢nîEv‰Òõ,—¾ WÄ67çä(W^'Ú\@pœÉ§±{&äÄX‹?©Ö$ MIF}ฉ¥+:Üð`•­Á(ï+ˆT*™Y¯Îv^×ý¿#5B¦¸•æÀ 5J/óšmj»ÜLíY´è8Â%ZxÈé‘ÔnÛ8ÖA3PeÉ÷áþÿJê…;VtËä†U7óPäzÒ,Z—×Z±Ÿf] kQ‚]g¼žò ,l ¶°V«öŽ¢ºàRØZ¹7ì©8Hu£§ Å´‹jÅ],šÃ@ÅS›yŽÒjB……Þ«a´Æì©F¦å<™i¤Ê  ÅTî©õ@&ø×‹§Éšòœbz›©WÈÊ»4¾]psÎxµFi ‡P“M»½ëy)þ/Ñ×Îu¡+(»‰nîp\8h #ŸÔá—_û-Àþo|þh›§"¥„¿=v1¾{Û°Ãüy8þ=‡bûí¶Áµ7~ÿÝqUqW í.˜"à–ÛïÄÒK.ŦOz;è-ØdãñÝ[¿ï^pš)â™ÀQ: —^uæ>éI8ôÀ7ã… ŸƒeËWàÀ÷S!^ÙÒ¨äïåÌmX<=ŒkÏ*6áJ‰Rºœ^‹ŒJ­É2›)w¥Né~^¢Súõ’Ü$hÀ®ÒðTŸã%±ó>´ÞWׯwóÔëI¨ÇºþÕHãÿôú÷(uj²Á¢|éy¿èÒ+pßoîDZ§~¼ $˜.ÙráY'šñxbEzq<.!–ÀĆÔ} G\ çuJ?ñ(”’^Še¦`ãB!çäW(PekZ³9\Ù¦b°æîýLÿïa’‹Bï Œ†ÙñιXºo7´¨ŽÇ»>tä9¼–û½Þ¿ •âSµ çãi8Îûr#;uh× ?jƒÙ‘ãœGoÇqq,)¶¯'Îׂ|5ýþ纰¢øª—¾'õ.œpú§qÅuÿâºpypŸ6 ^ÆH+Ü“µ#ü=u°ã5£†¢p^öŽGQÓã¤ÍŸgu½6ë_î6ÈõßóþCøôŽ\§{ìÁÇžP”×fýÿþw«l!䈅՚טHÞGÃЙ¦sÒ* Zdþ4ÝñÌ3X*YPŠšjX@Nvô³ÒHyy6®¥×PÅhë&ù}_K”Œ–‰ã"5]Fx¨6ë(Ô” s‚¼wy½4ÕýR#—¢fŽ“}-SØšt¶ÆqLŽf+¹¹d5†¦/®Å}¼ºžFÇ{¾˜g#:7º›gf@¡BAèaB?b݉ûY€›Q£…k-IJ WEÝ‹É8ˆ¹4,¦ãÁ?ÆEôưÁA/¼õïåîC¹)åŽÑ nœÔlÂì—ü=£l‘†/Ü òëØü€Ï¡Ë8‰dX£†½}·æ2ú£¯74K]ÿ]ÎÓ÷xf zíëß`ã'Í­†Jü÷È<O~ûŸøHÒ[¸ˆÒ›UŒï×;pÂéŸÆ?ëÛMz“"•¸ô)LCÑIÞ¦+yhœÉÞ ³Nùó<e¦íx”¯èÏŽ+*Ö4ƒß‹óbøóxψAËX§ø+ Es€ÚAC=V£Ñ¨ü9SGTµ°Uëë¬wQW5¦qiΚ(‚ïgׯ®ëÊç1j4 ±UZnDø¸fÊ`ËJš©rz,Õ ­¥“Êû¯Äiš=wº]jâ°6럑© výsþƺ"íñ±­F] ƒ!‘>í«‚©dävYJ›¢9f:Ì÷X¾:Ò㚊Š¥yáäö¡Hïˆ*ú¼ØÓÚì„å!e”ˆ‘­´yá{J‰è:ƒÄ™)ABØUS›=EÔtøVj¢ð¦”F¡Ò̘F„×§èmô¸ºëŸ›ÎòÜà* Q5ã(¦©ÃC$tй˜Š _K1%Œ¶ÌA‡S15OQ¹xèº/úË·à›×ßà&ÝÏÎÎVºˆéM¦U—Á73OÿÁ€fGÜÐqpa©šWœ³]²Nä•VèMĵèò4ZÀ+š¢™)ùx{Ÿ­†À›óCHQ.Î3…®¶UL¥YÑ"Uo8-+g>&¹è×&CéKœ©£Po?µ¡`êTùÝ.˜é½Wз²qT¥j㡨›‡ i3®Ú>.|ñëòÚÔæê±®Ïuƒ[ÿýôÎ- Çýõ±®ÿV°o¦!”Àº~[K˜ŸÐ9¢å€Ðai×ýËŒŸ~ÿfóžw#X[4S|X×¹d„Ò)E±zf;ÖàN¨«s—Ô'çÿdÅàŒ„Ò4tb/@vÎÎ~†žÖÁ´4”q@EŒp»èàRáÔþ¹S“z]ʘÑÒðý‚m¯i -Ž¨Ò©þ¦“L¿Q'Âá:)gœïmâàÆŸËòqŒíóî˜Ï$Aé½f¦eÔâý|m×)ø¤`d͆|]¾bª3µšVóf äg”æM÷MkäLcmŒRïŒñ£N ±A|Z^i&’u«4Ù>yX \å@V¡®Ð vÎE³Ø3œâLèjM¢¥V4c —ÍAÓL  êÚiPáLýÑùTNƒ<9"¾=c¯Q/ÍYjnÈÊptÛ9B=Ÿ]nâ)+(ß{þo0H§™Ðf ¥¿(°~ª®479Í‘«u3ðt­ÂŠÎZ7i6Ê4«Ìl¥¬KËÖ:¿¾¥ýhYéj!©á©jýÌqþÓ³ žöÐQJ£3­FÆÓ(å*£0ªaá}ÓÆ…›mœL˜žN—úýæ÷áíWT›«Ó˜*ˆ#­¬¦¬is¯6*Š~µÎRÿ´¹eTÑ+¶•"ø‡õ/ë="Av*¸ëÁM /SXú¯¸$ÖÙÔ0xÔÁA~þá“gbßÿù—8ú=ï2Á¤ùÚ×àæë®ÂÊËšŒ15 "›‹¨ŒÂ¨õc”ÑdŠhcnÀïOVعÉc)‡—Â4¡ôò@èG ¼ 8¨H]’RúJÓ@š 9<Ú¨psWÿ„ªç7HžNnØœ&ËÁjüº`PbS“aƒgAíé ¹™j¢3FÆ à¤f„ÝíÔκ4‡ U`tÑÁ³l·h’ZŠ›f@Ð3¥52ÂÆÍnëùßÒ’yƒXn`´FÑë’k5=ÿ ݹ³µ!?¿Ì«‘×Çׇ‰\]e™­Ïÿ'|¤âmo‚ÂÙ!^‘ÂΖg¹'Äò4üÅ4uÍò¦ò*ŽVñ¶ÇÖý÷¶}võ¬;•R'íÞUË0ž»…´÷Þª4mÊ•÷Oé^Ó¦d,^÷Œ 8ÈÓ›ZëBòÐ1³Xú÷ò¨w^C® Œ‡*äbŸ§úJÁcô‡ó…U3T°>'h"d•®ÉkUŒÏ¦¹Ôc«ëã Úü'™:Ezþ¼Ü>»xæ ÿ¥×ÿ8®Y`ú8¾Š3àxí×ÿ4­ !L‡ó´VJa䆉󾰟>ç|õ®CñÀò{&ÿ¯X†s?ñ1üägËM‘Ìt©IÁ”ªpOû4®h¨w7_5þùâ/Ó› ÛÀx¨Â$¬”¨=b Þ¤ œìkÖåíhwLcj ‡'· Ï=»lwiŠúÿX± Ï_¸k1^tqè(7HMHh_bÎàÙYg§»Ž ˜h† ,²6”%{™÷<9ÇÉCzÓŠà afý7¨m^3ÔLzëZéL-¶J_Ëæ&U!®E7BEAåÏæéº"4SÑQwù;ù9 ÍõïéŸ E–Œâj춚ƃè{Ò%Í&ëU\ª¢£c üŸ¸ç•¡_ÉÚr\Ú5‹ÉnG°ûk¶¯§‰q§ˆŸKE•³6M}ù¬Î§[$Õw¡cDHŸÉü>¦ÞbJœ„ÈŽºQ9×ÑA©žð9ALáÑôzº·ø÷Zi!¿&q¼ž¥_i@¢wƒh¥Èk’½wQ¶ÿ\LyÚþ“ ^¥=éñÌÇ›iS4é5ž«š×,è÷=‘9o‡W2]LÃ?ùûZìyûΆZŒê5ÁüÖCK§üüýÕ«W»CµHgëh>v1F{Ó#>°gi­7¥måH‘']Ú¸z×27kÚØš‡C/ò/!ƳcŒfF˜]=[ÐÉ?¬`43BG×Öu]¾Ëú¯ÌPŒ½*W%-ŠÏißD¤Ê·!G6d~gê£ó–\ˆó¾°ÔÁ}üÀŠeƒQ@Fu‚àŒtôÅÉO¿36›;·¼îÖÜŽW¾ñ¯z|àþXtÀ~“ß§ïÇñxR˜³©Gá€>·&%\yÉWqÇ]wá˜>dšc»MèŽí·>mnúf`’ýB† lZÀÇØ)ö˱С‚S×2ê’©{ÑAN³¸yÀ¥ãà Ð­0U£|ì#¬~Éc¨„G#òBŸ[uŒ‡Ì»ÂÏlAí¡A¹=Ræ×–gcê‰Ò(”æ@£!I°„LæK#/zu­cjj¹&“µßÖ{K¡B5B9CÈ^C±¼¿bògQÓaŒMÒpa—ýJNÐlBeÁÙ_Þ÷Ë`)$ƒH•gGH6kÌ Ç“ŸWNsL+ì,jÈHŒW³å!$&T&5ÉA³ša5­‘ª×mÝ|æ¿Çq4A®0êFGçù/™kð„G‚>zí2„[nª‚Hšï¥IaÏ7Â\ìgǘ™33 ^ÿUמòÆd²¡ZÅÖã2u]‡ÑŒ¬Z›fý£æœëDµ)|.“ù®Ðáì‹øú…_((ÏÊËpÿòãþå?&N=Ñ¥ ŠÓYD‰ £y™4Z5yßÞÒÉØjÞØjÞ˜÷´íqá¹g[4Cî?±eQÎŽfJ¥Êˆ6¿–é`¦¶±÷ž@7v`SÄ¢e‹]Ö@´Mf"ZXp¨sC³8hŽ m¬ëªÚ’ nëkƒ²È÷4t5Ž›JêÅ‚©kÝɵGozl×duí9èé}«! ó ÚµnÔYkú ŸGºC%J6kfÈ D)Œ]ë{¶²×fÁE+^Fib2Œê9H€Ñ'•ûç ÷ŠÙ/ºÉʆ.ÜL1VÙfdžLë¸Jý O·QtNyÛ zÔ7i,h ;ÃÁšx׬aJ„ºa®§P›-»êF@½ÒáZÖצ9 ‚Lö”ƒ òóˆo1­!ÄA‡cíŸä,¼gÝ„ Éù óÄS]´¼ &E,¼“®Å¯Ìžó–Ú{…þÚè0”òÃb}Þ>uÌâÈRÏÖ†­¼™î¹Þp­ú$ÇLjÏg‹Â¤(jaTw“¥M­ÍC‹·Û£ªiÃ^=´ÔÏÍŠ(áÿ8–æ‡6Õ eÇ9Ö¢¨]zuƒqÌ,< »±™ãS1nÈ73¾Q›cl‘¿¼_Yó”𺜗‚i ù†; ¥Óãçk¼¬ÿ±¬ÿ±¬ÿ¸Žë¼Žë?®yý¯¯{lTÃ)ySþ˜nˆž†ã!Õ…|=¼qÿ¿ÁVówÄÖóŸ‰-û¦dËy;T”™!È´3³l`9㡱40­`ϯ_ö üÉÓç!àæë¯Æ)'øæÅ_ÆÒsÏ.ú£•+–áùÏ]h”pÊââäã?€yÛÿ1X~Ϥ¡pðûãþžÂ÷ÀŠeXzÎYUÞLþÌ…»ìŒE쇖߃ì¶Ïßm!V®XVšÃ,þ`ù Ï=÷/¿÷/¿7]w!S“cþ¼]Ÿƒ–߃¾ä+¥qxÖ‚?-ÍæOn»ÉÐÍ–ÝvSùÙ7‰RwóuWáÔŽÃËïÁÍ×_mÜØ*³„œ3¤ºq˜SÔ±»¤¡‰1Zç¹b’XšR¶)’õx”ú Eûô®ÓÖúoE,´š¤–ÀÜP¯Xoý`ÔJÿ#ÍIußB2"v×0 ?,úãÙf3ÕË¢‚ÖðÀË5rº‰Éà'çv9&ƒè¨µx@ý¼qœÜÜ@€i-| ÓHéñ1gù>ÉùG±íÂÆMbA6邯Ÿé‚ÛTó3Õ˜Eki]ò|b­½õè˜æ:oX]s>•7¼vÑ¡d3‹ŠÛ`§Ž¥“ÿgÇc£ÏÇñ@+L©Ür6cÖTðÔŸi2)%Ì™3ÇÀ\T¨Ã“þ[‹ZÏjzeÏk4xêìѨZ–Î|!+5¦¥ûà ½×$èöçf€´Øõ´4ž€›÷igž6HƒêdÔY,ÛZ3ÅÍ£2¹t ÇŽ˜ÝÜZÚ¦Ö$…›Mµ ç&†é‹zN•¨7ˆ–fH›3¾®%`”fff µÉ£Ä©ý:ÿ[uNÕ¹'­ ‘Õõu‚É×]iöF]Óç&7UÕúï­9ÿÖ¶ï·)ÿ;oGù··þ)-[µW*à$7kåZO´þ‰ïÍljŸÃ5­ÿõÑ äç©©9`ƒOMS×0åÑ›¤7¬Ó&Œ—Þà¾BN§ÐûLÖQEÖ7êFCÐsšÔ:ù\»ll _kòÝ×bQ[4ªVã76.ì½fE·KQ'oR¯ZέыIs ñÐÝçLÃò~_m sñÊ”!O8δfªhH«ÚE«µî‹ê~”"6 Ó¦r£vÝkº6<ËPêœfòpóæÝxõZ˜f>33c7¦4†s zàäý-BÁ¾©Ô묅Øq¨Çj<›Œ‚²?} kѦõ×”G1ã SAdbjçoÿ=7 yÿµ¹`ú{Ž£Eòï™õ?3ªš—©ëŸQ§~ÔÜ€)ƒHÀìêÙ‚ŒñçL[ÿë㾌Ž&  –jØ"Í_•œNhŒ¹¾$Å;¥híœI¬^ÜÖˆF–‘”ÃÞþV|ìSga«¾HßjþŽ8øÝGNH÷ÿ³ÑAÖ€¸“Éþï‹Ø·ßy—1éÊ¥Ö•×]ó>¿)%üãåW`þÓ¶Žm*ňÃ~®¼özÜtëm…ºuþ’ ñìþ̆‹Bœìú÷Þÿ‡â{ÿz º®Ã¹Ÿ_xþn ‘l6wî ï·³›¯» W^{ýD[Dëï>yfi*N:í£˜ÿ´í‘œ¿äB³ø¤B}[ñ‹_b^ÿ3¸òÚëm­Ù#>AëÆÞy® Š9Ÿ 5=2éŸR+=¹ÙŠžÙA§´Ñ½.8õ„K jL´½ü´j_*tW©¯®Se¬Cl6M­Ðæò *F²Âƒy¢iéjÛx¦ˆ1}Î^ï©i ÍÉãÔð¾ðç”û 54¦é@péBÿë„A"VãJ±Ó&ϸîÁÒÈʳOŽ ?§Ì–RXWvæ£á›±>§a_e*ÐÕ®m­zŠŸã•Ë¡¬qóèd¹A»DÛOÑqy>òYØ:ƒ·ªþ/Ç}‚q\°#äÉ»(r1àÑÁ<Ž­¢z¡0z“ãž52o›wQ1ÃÈ“æ°[T«¨Q[cÞ^.îµ0ËŸÇ„×LVÄc›wâÁýŠFMÓEh±îѼéŽR#´oñ¬«É5/¤ÔËîÉÇnff¦œcv¶SŸS/—gMÂYF™X»”]â²{›g]’œ”†‡jFMé` ¾öòç©vÌ$02£ÎeÜH˜bÚäSèµZÿý>ç¦+7ÍõOÛ•9Ö\€4×?ÿœVãYYÿñ1¬ÿñú[ÿëòUiøÒÖ¿$©ó¿Ù‘ BàG“Åc ZÖ‰u* OfÀû'>»ôK8õôOLLz꟮ø&þéWPå£;,þ7!¥>rüð‘ã?ôÀûOüpeñ šlwšÅ…7ShûÏÝr‹ÍqÇwÙý°ùf›™ÏH1"*ao¨#~òý›±98$$œ÷ù%˜ÿ´íñÀò{[eê €…»ìŒU?ŒýßqhEE»ëG?6 a6†H1âʯ wÙ¹¼Ïõ7 Á㿺ï>ë~Ç׆id“q«ƒ“Ûc¹wjZ"ÑâL€*kÎöñ4H‰ÞKs€@H“§mlÝû¼g?ëë¯bÈ ¦µF–ƒ‹îd›$““¤1 NÓ&h óŒ´$$ó“Á²ÏgÝxŽFw#Ÿcç`m¦+ƒ‚T úÍ9!z›ÚZ„1@èºnplì…ú1ÓT¥AK©û«[)ÜÇCøma¼Œ£i¾<K¥’¹µŸP5‘uÝ5@Í‘²fkݪÚMBPs&“ÒSÆ)"ôú¤q#% ßA %ú‰ÞyųЉ¹ñ\W¦q}UœÌÓpn"tBï! ¿X y娫XßÓCØn™©n¥àu•fŠ)`zá)Ôï…Lzô°®ë*úœ¢j ç®å¦p ØdZ‹¢è¡¬ÒfQ)f¢Ä×k‹T·£ûªš^.RÖîðqñr‹¼ \¥–ksÔUëˆuVjÏìQÃØÊÚ»vtºç5ÜÚ4·(hz®ÕBzêúŸÝÐÖ?£}ã’ ããñ,€õC‰ËÆESSÖ‰²æ&<ý¥ Ä~íÚîÄ(!CÉ|1Îel—ÜN[Ï&vØõ8åôO y=¤j±Y˜†—r㘋ã0ØO¿ÿ¤“ ²t쉮Šs/(?ˆ“ƒ0éBJXùàCØv›m*AÿC«VÕöîÎPhåŠeøè'Ï´©þë˜Å'aëù;â‚%â'ß¿¹\[·þàv,ÿù/póuWYsBË€ü)zøaL,ÁW>øP¯ËÚ+~ñK#kàcÁ¡¨üýÈ_ÿýäÐÞ (``ŠšÐ;i¼ªàÕ|äkȱÔn¡7eM×—G5j#L»÷T?w.ÇZGƒ…,%²“dåö¦t16Ðé¿îÜ`)}N‡•^^LÉ bÔlÓS¦ A®BCk‚’ß`ª,}ÑF¿Š^©Ê=*šÓä7—ÅÌÞ£ä§ÁRìLË`ªqÑÉL‰úP£C'‹2Ô$ý—§Øpª ~ÓBŸ© œ !öV± ªe2›…ëÃQ»þ1Žã’¤Mû¾ Rj› ºõæäñè½´w¦zoÒÖ›£Ó- ¡ú­"Sµ,J£ãb8ïK¤K~ŠWtN£“±>ÃÕLÐ"e:_n6<·+=î­Ü¦d)K5,ރţ´éDÛ{Pñu5;;ë+mˆ˜ÇÖiÏS/ˈMÆ”Ý2Í ‘"5Ð<ÍßÑãªnŒ|y­y×—·=d¯…òuïÚþ×Zÿ³ô½‰åóD#“Ñéñz¸¿¢ &ÍŸ•Ñ“áµCnMA;*»÷n(Hs §Ï%p´«-›!Áš=:Á!¥¡l‹ìˆ±¶ß2yjĉ·/nìCD[õÕP¨;ùA½ÜÜM7-Û{æùà•{ï9¡°õï½è€ýðõ˯0ÍD¢&i»m¶ò}Üù£NëØ /ƈúæ?W(Ë+ßø×Û%_1ÇxñÑï+û²è€ýð¯·~¿ìï¯î»ob¨°Û®˜·ý7ÝÖ:Tm¼&•-¶AŒøÚð\G‰æÇnuIhp†>(t»nJð)¦ÐàZˆ½#à ò\¶CL¾ šs­MLláWšƒdŒ@DÒÔeKl“m“écÄõô2ºŒ##7„öªV¨D¤è*q£•¦äîHÎLn^4¶6JI.}Ê6q*ä¹|Çq8NZXÓv”{w²ˆ“A÷¤ˆgº0ë…ªak@EÝÓæ†™.Ê¢¹>]MÃæ [Ï Î£Ø™ë¶upRÏäªõìö¤'™ªƒ¾Ùöþ ßñô\§×<×¢Fy¼ okÁ7mŠŸ¿TG¡…U.Þxz½¦"Í3Gà‹‹*Ow¢”7eÖ[oö­ü‚üÚÕÿ±º*>+ê‚„rqÇM‘ …^ó÷s–N«QѢصgv¦©Úpî’êœØŒAés^Ã¥ME><$@¯ Eˆ˜Ž¡ˆ‰êß=lYªQ„çb¤/Õ3•s=;6înl{ί÷ìÙÝ8lÆÿÞ†¿þ•Øa4š)Τñ›®­§!Sg›¢E¤1;»š¯hs|JaFç2Ešt%³G)p)òAßãñ@YC²tãÇb·]¬³[öÌLËj„q²ž§ˆè¹ÈÖ yæ²»N1âÜ/,EÀýËï™8ÊÝrÞâ‡qùW¾8q‡[~.È™G¹À§ëïë—_Wî½çÄn·…¸òÚëqù—'¿ûìv*Ç?»µ­üùOpù—¿ˆf­}=o¯W`á.;cÙm7•}ZùàCx w¸{hÕ*ìÿöC€”pÁ’ ±è€ý°rÅ2,ùÌ™ ÒzFµUùÜ&^ƒ°9N^ó”C÷ þ½|>8·ž{§œ3¦½[Ã’zÍ£ ¯‰çëÎP „ uIry*ú7ΊÁ68Üljjªµ6†j&ÆùµãñØ8–UÔYE°bj°•ÇVÒüýñx<4gbUl¨ -*ßbÏL.ojQhUâX—5Tª½2Z¹”Æäh×ÙpWv¹Ó}æ¬ s}t>•M)”¬âæÆ«§ {& ²gç@Ó@9N=ßM[ƒ€›6ðL÷ÉÍ´Ì dW²ã›(ú ¡Ñ|-øÈWÓ#Wœ×„±¹€š–ÅÁ»ÞÀ”ŠâAxj£í]¬ ·²Féð.lÕ,1šÂE;ë ø3Ù-O~O7£Áü:Ö^µš&n”¸hl馺³L¡xÓv¶Èöœó;ÖŸäÃ7ßц‚D%A­’n?í£î›Ñâ8û¥gê˜64üûå8 ²`uU¬³ÉÛ4ÑPXûqn`<=ŽÚ•wº_Þu#×B0!¶5¯Õì¬Éº×»§®íïWf\ÆäS¨ˆ®¥Bñ¢­H°l¢Ïàß+Âââæêf’ì#½žž­¦‰ª’)k”`×rhÚmO»xŽp®SüõÏ¢in 6âšù“P‡Ò$¤éëŸ÷Å QI¦èÛN—F¹ó3y¦>ÿekP#XMZŠ]¨‡†j‡dBWóg°DÃ8jð*]í†iã'Ï5(e D òüïwnÓ7¾áÐáZ8· Î–(³²£tœæø©FÇ;^J£âbÎC“´xÌúEK”®¤6ØÍ*²ž)ß\ù8¶¨[JqâEÍÍ£j[ZSúµqd§BvzãÂ=;°ñ¶ò¾hQ­º¦óq‘Á&…Ò–(¼T¡õÎwhS7M©Ö\=^JãýnC^kjŒñŸ·þgõŸÏߨYÿãBéZóúׯ9š&Äþ*ºÿL5<ëò•inÃúŸ :þú¦ ͵Xvô±hIWÑ•Ô=,p¡+”(PÁ®ýÄ÷ ‘9á»Tëò@îŒÀ?9aPM 5A¼i>$öº6 BÙãfƒÍ«Ìïui†J3xÛÆÂ³›N‚´$§5èK>—šµ¤Žpz^å¸)uÑdiöL¶c•S)’4¹*´6á¦-WRS›-ÖM˜ŸyB« ô*AM\ ‹Ó¹±i….wà0SE@´h6Z!jÆJ0¦ %ü>F¤,‡†‡Žšs—¬ µ¥€F¤Šcï=¹WYÍRË.ÛÐØi“†47œ0š÷/ë†ÊyÈ[]“©B~7P6AäÝ`G®Æ­<½xƒoP=Rê¹ö²9ƒ:&€¸Ãx¾ÏÅ”0ŽÑÄ.„Buµðá§…K­àP.L[StF2¼‰´=úy\Ä)BÒ¢î¨ÐœŸ‹[Þf/\›Öx¨6Â+H[FhfæÌ4E+¿ÄãOç¢PC<µÈò®Öž¯‰–îfM)>¾«W¯¶7Žã—šTðÍ(#¬‡ñ®#>~Ù¹O›ï!­¨Iþ]mØDÁº£!¬3߈ŒU´äädg¡nÔ•\ŸËùñ޳6ý¹Qæu“÷ƒ¯~>žÿ9ë¿3MÍÐ$XfxïQùùàVÖZÿÝ”õM#2¡ºeDw¶üP£èwÙ N”¦£bS0BÆ 71^~X‹ÆHÍL’遲Û8F†Ú/É5™AIžcÜ\ºN‚š,бäí÷B‡[:E—ÒÔH¶œVmS>åçRè3•lr$ƒX¨ˆßuãJBqCªòpŒû[qÜê ­©jÒì À¢ ZD3RÕYíNÅŽ€ul3.pŒ …öúg ‘iId(pDÕǦÑF‹X;ž è6 êÆ&êj¦º°lzÁt¼Êv[õ9}a®¥PçUë¿ó2žÁ7MÓS¯>«´Ea°ú.ÍM'1!IZçǹ”©ÇC`j¢8óü§AáM›Ê°Å¬§Éh½O.¼¼¢V)cJYòœÆZŸZ(+½Li2ZœµBØô3¹øn ÙµIðšL]bW1µñõ¬º½É¿–íº¹n=P<ǬUÎ{¸ðöµnùø3…Ë›js¤a³J ÌçT‘ Þw¾5¤Ô£²ÞKÐV¨fŒÑÜ4 ʘü5Rhl«g‡\rÁÑâAéiª¹aô†#øÜ* ;-”ðÿüú?Žõ?zŒë¿£ëo†Žåx ë\šªõaŽ0)4sý1Î.ƒqÈ_ˆq܇Ðu}1yí0ë*Ô Ì|y`#:n$‚4'Æî˜)aš3¤t3º¦¢Ó$ú¬‚:t¡pñŸU(ÓÒ4¤æ=Jcu>šoî}¬’Æ¥#”¥Ú§@ÛFÓœÆÄ]‚Li3œsáÑÓ8(Êôt-ð¹âø9f®L„!ƒòá{X+š}˱Ò{6yô–ÛjK÷©ÖÏlA¬´µ¦V‰3w‚ÕexK¡¤%Tæ M‚cŸÌöÍÔ TVÝB_cjX¹gjÐr¨]ê©1Ž„Œç7¢›)ýim¼ÿlªPl á¦JsduŽÁš&Ð ;óiÖÒšÖ¿É\Ó¦¢tQNux«†MÎgwþ½ 4.ÎÏÔݯ5$àæQk’ŠZ“Û¤ùÏÿáügD(¾×töÏ  RÚ/LAቸòóÕ!ËÇ{–ÇžøYÑOÏ¡HŒw3Ó‚‹h-´§¹ºyÓp/(VEÙ\ ªów¦^égó>L+þòrh§61Jmã"Ù³çB;ÿÌËÒF†§ózN5_©¢Zе£.gJãmׂ¿åئz,o½ÊÓ6M³¥öt7šYäM8•’×Ò?qã—GÝ=Ô+ý Y»õ?KëÜXÿ³²þóŒî0%n@j&OrFc×´ÁŠ™ÿTúÿœ·c8þ£ÑLÑeklïuë‡׋Ùûœ…l„Àô¸QG´>X ÁÄ ‰48ÙØA kÒ›Œ ]Öýˈé‘`t„)cB¡QÄ’ 8Ü4tžNL”µ±\N1šmO¢[Šê˜Æ40‡6ÆÁ ÆR\¶‰µ>A˜/É£ÆQcÃÚ+m\ŸK ž…lн-Ñx¥5Î}Üd)r%¨•É#MQ"dÎc@xÏR¯@m à8³gMÏWs”P=3¸‰`9knôu¥°´¨C9 ã¡¢;³v¹Ÿ³sY°™D¥OˆVƒctM¨³a<á¿y‡.Çô3nÒøwM#D Ê¢Y¥ì1ÂÆô4ï`r€P»œi=c\û’=fÍ£FÓÓ²éõÆ(Oë^¤Me lðÜ ½ÆEQµÜÖmóX1lÕ]e%KÃÏÇ:öá¨ù{ÙíqÔ0ÊÆOyX™M6$H'µJyñn>ž®@ K¶zÓvJÊ98Ló™éi#-Ç/EgxŠœQ¦SynVÓ¯ Õc–‹l6#ð&Y^£ÂÇÒÓ1Ú¡7(ž}·æ¦x& ü½¼MÊ‹å¦J‘¦@1µ­˜[Aþ¹6âô«¶Éªëbdˆi‡Š4iCË×Fë:óΡVÚ }ÕÜÞ†ü>sæÌ™êºÆû¥hªZ§·Ü Úãi›‡qâÚ­ÿ¹­ECwcÄÇžS÷ïÑavvuù{¦ÂÍÌÌ¡óÒ•ßgt„M$'`vvui„BèÊŸŒFi#fP—°>ÂRc%®€Qç­ÿqEÑ2¡–¡ó HFDw™æDÿ›÷ïÿì ÷ž©]MñÊï‡| Òˆ ?©‚øÉ£¸–²'èêS ÈPÞ—$Z›ª!ª]1DPzŸ¿z†nc"«$M¨Ò »ÑÈh¾*×8 ;åó[P%m=¡:zœ;Þ7År‚Qu€èÝsZú‡äÐ=†HÉÑ“Éxhlk)Š`<³.J6 ó%$„ÿÌÚ–Ò …ÁŽ9;‹©³‡—òçUt¸à P FÓtÙ$ƒÕݰ€ßüGï_Áα§ç¦Ù#XËþ¾€f*¡ÑG97€ŠB¦Ô<¹”\&FŽèx3…¼/Œ^G«›P XÕ…¯ùü‡˜åÀVo šPQé!Òàð 5íì¶WöÝÑÑiÎgÉqö®­.¬áù¿!#´¸‰Š ¨y[-a—Þô<äÅ{_E mЛ¬Núµ0†3S72¯)ÑÏðÜÖô½µ³Wô U zE,߈¼ßçýãB~ffÆ ^-^µ6¤^öQEÛFDQDµ¿Vt"ÿÉÛËïÍ ¢6-4ìQ©4µ•g£:™i 7ÉÔ†€Ñ)P¢Ü8fÊç}bw8v$Ìׇî÷TQqAA|3€ŒÂä¿ÛLdPEv¸`¯×WôACã2˜äÏÊï=ÐÙòyT(7n“àÓ4eý'ƒqg¢T7%륂AÝ< yEå5*æ6À.€ì&Æ4(¡E%-N©HNVM »ž1}Ì éÔœd÷º¨…5¿Æ+ÆMŽº¿†@þ,®§œ¢Æèm¼ÌVèÐØŒ¦’\Ô:Þ'A©ŠIEnJb†L÷C6‡Èf¿¼WPôÐûL§Y‰Ü@9ÃYSÍÑ¢æè{2JÂÈ"ËqnR˜æ"7DÁ⼄p:hfçX¤Ð69UÕP^j)7Æú98 `ªkpX¿Äf yc¢zKº ‚v±]8Xôç¢h£T ‹`š±ª! &Có“cV®Ñ@±M¶ÒÞÔº1R ʃml =>7®d¢` 14GžY”¢¨Ê˜*æ‚V÷8g[×ô=u»lK“y¢7A^®ˆºgy7FdDZÛÎ;áïY„çÏÃ&o„ß?ò(–-_ÓÎ>·ýðnƒüä÷á¼Õv¬éû*Àô O+ÂŽnÓœhrQÊ(¯ÚPiþ SUó ˜êví™V¨kNM5qjÐÁ´ÙòÞÇè+ºÆù3üwEéZÚ#>>|ìXÓãe<åðUÍÉñÂbµÙÖ\ Í*j]‹J5ÓsÊy<-R)™¼¯|ì ä<#=J©äãïeI h]¦•ØüšIá1I¤Ñ‘¼›B1³×Üh4cŸü>33sèØÎ6ÖÿX¾¯– òežÎÿa†üùy;s32¬® GÞÚy|”cÖœÅòïu Ý© fÝÐÌñ1*'/-Þ[ {Ó|n6¼pMB8ï¦ÔqÚ(pÞ lÆ ÿ]QP!_¡< rô{liÝ"R™‡0•Mè‚A(i×꾘AvÍŸãè’¸ù̯1…ê¼ãèFç0(5»?/LƒËï™ß$–æAΓiVºÎ+ú|>•f¦÷àåãy¯Ó{±G1Ÿ¦…ð4ÜŒ´´"šÓÂz^t1ßLö×ôpÞ)ê=Í k<¼FÌðZ-ÚcBKa-©M°iôO룙>è†)?3«ƒ7>…—ÏereØQÑ’‚ð1Ñ…*äµb¹°K^G†²þ‹Ó\{r>S2ƒª<e u~¤ŠÉê‚Ën1ûK×£qŒâ0¨ˆi绺6K´¦Þ¾èï0—5AÖ´"%«Ü 4AžÛ?{Ýe¾(ñNœý‘ÅxhÕÃxëQÇâ/zÞöÞàÁUãì“ã¸Ã­Þr¨ÈÆ´fMo޹Àk¹g©C E9”ž¥”Auaã†I›2^mYæÅ«âs.°&) ¾ua«U¹ç4¢ *£2žW'üÅvÍÚqQŸÿ­ÇÁ[ØŠºð¹RT)£/là™<´¦0ú0ÖØ{°{“MÏŒCƒ`[ ¯ÒÑÔýŒ?G3°ØîÛCí5 ÕŒÑþ“)b5,#{ŽFæ5õúŸ©ÜÙûúÝ]ÑYÿãªñ©×ÿ˜›uÏ J ºbŒPIDºb/>µhO¥7Q¾º±©e± eĦ7PG7Õ©€¨p•F­­u¿s5-7-o_d:š @š‡dÖq´Ç­´)´L±1Ò Ñü0zaÚQ.Ò¹iðøËjq­ˆ‰j3^½×KpÕÏÇÏn¼×~ùsýÌ£YéEǦ J—òÜéIÈS}/üR hFS¸@VZš×DyS;ÏdÁCE”*èñÅõªˆfòxz%þÌŒÂq“— h.´MÒ37jZpzz+ ¶ÕÏðìá½k­eœàÂÖ¼%Ï5Ñ Åè`©4.Œ dz[¦±å×dú7?CþÕܰօјÑh«WÿÇúúÓë_mž²N}ÿû°õ›¯õú__£ò°FÉ_ubLíõOEvàëD(jL±Êb3ÙäÅ ¡„ í¬Ökÿ¾Úôx¬A‹yÓŒn2ø³´!KÒDÒð~%)$iØ´™0:& T…B››H¶êf»íHF^3• ´Ï-ㄤˆKqA°ø<%É7‹‚héõ¢M­w¿÷håž¶‡ïAJG÷(Cl#ÌZ~þÚQ(VÑ‘fˆó‚”fŠ_Øf¦ØÕgú˜cv –ÆFgÔ…ÊÚºl?!Žž®‰› }€qÃÁûÍH‰k{*çDÏ»¥p§&^67‹N¯ß ’éSjê…ì²´‡.ðþ¨[Üš®½!Ïήvµ\ÓPÐŒþø”¤NÖÿà¶6PæBÑqÓÃÆj7=„ РZ»õ¯¶ðwýŸð±Oâ¡U¿Å›ÍÅ)Ǿ[m±ùZ­ÿu»¿bÊúOÕσ¸™ N¥…1RC”'ã$Æ:Ù(nntŸ“ì»Ì• XG8p! ÈÄEtMUÈh¯o¡ÚqóÀwæ¾£nmÜìðpÙР¨ùTn#Èr›uU¹!Êvr“£ä4ˆLƒ«‚`¥!“¥vBMó†øºb­QjL‰•ã!5Šã!”Þû*B®©÷å¾@Óq1*¾q,PUÛ0õ ©Ê«1èK"[gǩ̯°ú›Ð…AH,­É :°: ¥®±‹›RÅ”Æ.lÜh•¦‚Œ*Ôˆ›gý[ðź̕& 6l67[qm0+SºDˤú-F׌áe= ƒ†`øŒ´G£C´z.ƒŠL±à®(rNc1m€fèp1¹Fœ_ä5k|=˜g]ð­Æ™ŽÉkÊNWà×ÿeýÇÊ"½ Öqƒ¢ÃµèX¬iñt2›l¼n¹ãNsµÐºõŽ»°ÉÆUNp ¥s1SŠúÆMU‹~Õnxy=J=âÂ1Ðúóí·Û¿øÕ½xï‡NÃIgœiÂL[è‰çÊÅSm -õt=ùw”ÖÔ¢NñûçíóŽ1®WìyÛÆVÙ-šCþûœ9sª…uSÜ(ÌÌÌ`õêÕåR)oŠ 1rÁÚïál¾R•çÄÍG¯œ|ÎŒ;ÙáܜᜌMs˜í™µAÉNhô‚Ïk,OÖ¸xÇ7o›BëLiÌîjµÓÞì¢ÆŸïeðd4h°§NýÉ”8‹Žé5©2&x¬ë_éwý¯\õ0Ž9õcxpÕªI#tÌ‘xÊV[N]ÿëkДéo™×ZÿF×CŽ_I´!AšŠ$´²¢÷7SX³3ª¡¢¡ŠǵÍ4U„u£‘qeãF«4*ùµ]‡È4F  õ츱`SB|8ÿ¦ *%P y Cec…àP±2ݬ#½TÑcõvÛ¶Pƒ;éØÀA’ÈûÔŽnüÜćM’ÇéoœÃiÏâí¡8žNÈûwINZÜX¨Õ²>ÏžÉäᨠ¶AjB|Z2YH;dìí“è3bj¢'Ö‰ºá¡Á×õä 7 üÙ%ë&£-ÉÒÊØéMs{L#—àçô0U,ÀЬê]µôÀÑhd]Ù¨Ñ*îoahv˜ÆœHnúºQgв2àì÷­ëuªìšgœä‚³þ*]-*f>†ê- ”íÙ bŠZÕ´'Û0M«'ò ¥iU§½.¸ÔBO ÇVª—šØøÄo‚Z[uòòrsÑñûGÅswy–Ñ·ègá³ÿ ¿äÑJ@©Sy-z®½èsØï/_‡¹›>?»ñj\sÑgËkÏ:ùxüôÛWág7^ŸÜp%v{öŸ•ûÖ+.ƉG½Ëüìê/^€Ÿ~û*,>ò¸ãêKqÝ—?8çÔqË7¾†ŸÜp%~xÍeXú‰¿°Ïð“®l¿Ý¶øé·¯Â9§žˆ”¶}êÖ¸äÜOâÎk/ÇÏn¼·^q1Ž8ø òÐ8è¯þ_üôÛWaɧáê/]€ŸÜp%Þú¦7âÚ‹>‡åß¹±ç‹qÛ7/ÁOn¸Wéló”­qΩ'âÎk/Çòï\ƒÏü”ª8W håc·L,Za´žÖ‹‹K ¬õܘåŸÍÎΚ÷479 24à–‘5v…ã}´Ú¬®Ìk9è£òúÊj²jìDuAy›ó¶ràfˆxùYLk469‡m›U÷æÁóªGP³Î„’æ}ÑðÐöú·!ªœÄèO6ÈÛœÿ>\g±Êæy,ë?7÷žÓŸgÁ ¶fEÇcÜ¿òAó‘âÁU«°åf›á”cŽÂÖ[na>g}ÒàÑc • À æõôAР*ÜÒ™ÞC‹g¡‘iÑ­MB'&Ú”€ô*P]L¾71 êLT¡îqJ”»Æ1Ò”I‚š$iè”ÆÍœœ X¶|…[ëEÏÙËßò6\øõËððo‡?yñ+ñ²}ߊ”Î9õDìºÓ<ãE¯À¼Ý÷ÆÒK.ÃEg~ÜÀÐûýåëðæw¾ÏxÑ+ŠY¼üÅ/Ä«ö_„=ÿú ¤”°ëN pٵ߇?y6~yï}Ø}á.8î=‡âº¿‹“Î8 ðëVâCŸ8 ç}髨ö©OÁ—Ï:;ÌŸ‡Üý#,½ä2<òèà°ߌ÷,:°@°ûÂ]pÃÍ·âé{¼K.¾´,‚#>—_û¿°lùϱývÛâ+gŸŽ;<—|ó*üúþ°ûÂ]pÄ¢+Ê£"JgôB½”ŽåÕBªçƒ§ÕÕ4D,µ=ûp}Ï, £*”› R K­›lþw2…¶æ¯ä†E·7¿Žéo9Ÿfvvµ1Ȉ »Œå¬v÷bêYFòït²@ W4̓=‡õ{Ç7 NFºxÿ»ëá3ógeƒ‚œÅ3ÍÈúïÌöeºÛ°9^ƒ«\WšAþûc]ÿùÚ`«Ryæõ¢HX>ü[{êDZ’¡­6ßÌܿ֗9B×ÄÈ®›ývFèºPiáXÇæ'Ò£¨›™±Ï ‹)–%œ4Ó¸ î@JÉÓ¼#ò§&R|6ˆ³rb£ ំ2F–ÓÚÌUEºèªIå"ÒØu‚Jk#ÂYMPMQϪã-¶æ^,“ª~Ox \¨éë yýÛ¢BÚýn58-»n‚“û+z–~:ÏE¢ê8Ê{ TÓÿjNY5LåbÚ;Qóú IDATìë›!u”~!äç¿ÇJ˜¬‰®ÒÂ7¶T#,¹`.dz}6ÿžAÅ’uµ+šŽ0„ò±¬P#E~˜†AJ}0× ž"ØÌž ŽDW¬Ð8šÑXqs™‘°Š:–P…ðækG׿:¿UMcÔQ51l¯Ý…ªùª&Ž ÜšX]f°ÉŒ„9ïŸC)Æò uRÿcüäëÄh¹6„&(7@ªj¡zƒŒ1â´3Ïî;-À_ìùçådxŒ¯øó=°ëN pÚÙçWEµÒ¬¼ÐÁYi(ä÷Úã8ä'–âøÄÓ?‡û;,>â°²x®ûÎM¸ùw ra]|ÅUøÕ¿ÿ¦|îëßvN:ýL|þ«ÿˆw}<`Û§l»ï×XzÉ¥€GyK.¾·þðn,Ú÷â©[m‰‹¯¸ ¾çœxú§ñ¦ÃþöÞ4ÚÒ«º]{Ÿ[H¼1‚z‰î9 DbbàD+d;46 "°„i$@‚%7¨À±„ó@ÂF@°<Œ0;$KÂNhŒhd! /«)Qâ‡Dݳ÷ûqÎÚß\sÏýÝ‚ªø¹î¨«¡QU÷ž{Î×íï[s͹æ<Ó¾wË­öøGýT@ö×ݰË~s ¤ðë=—|Ävžÿ;õ?ýº™™Ýþ˜£í‰Ï=Óvž¡ýþÅï23³»ßí®á¡ Áϲ¨Ù| 1•ÊvÕì~¢æ,ؤ`d;Îò=uSÀÐOÞ^µŸXäÇ<˜,C?p¬ üÒ…V:@À±*Ô—¤•]v–Ñ“»Ïô߉¦‹Dð5 ´¡ó‚®¸sìɯñýî%i‹€x§ý¬a›°µ‰Ÿ‹ÇÏg–”úßý÷~ðõ_;ƒ|=3>>‹`:Ì~Q@â1GÝš„}ï+7ι—2,7—VJµM’ÔVd¢(Ð4qlÄdð\J¦Y ¡å½Êj&[Î¥@¾ÏÖ˜ÅÐÏh¨ý„"Ÿ3zTè§3j™so ó‡­±+Ygºí‰a ~^°…Y`õ´UÌ ‚ýèäyÌì3gÓ ä¡þÄÖl¥ Qϼ—Ëaq Ñk¢`Qw  <0ž- “ŠÎööÙæ9à¿®@§£î¬Jî°³Þ=˘¡‚!ÊhÚ™Ç1µ—øñ  Iûh¿‡3Z&T&L ¶ãíßÉ:6© iUÖäb?›ÙƒÕ.* ^³1j9Újg¾¤ÇCðý欴GßoRF6å÷ü~$%œWܶdÃ&ÅpâLqÎm¦rÊ‚yàº;9íEýïf ÛA ‡Ð^ ¨Â¸Šðºœ³wÌQff¶ó…§Ùcù0»èâwÚ'>óy»ÿ½O°g<é»ï=ÌÞû¡Úÿ»Ït.iüÈCòÜõ÷mû·÷ü7ffvñ…¯üôõÍënèþÍÌ^}Ñ[ÂÉ=ù!´Çœô0;ü°ÛÙ]î|'33;þ_þ=`§ ÷Ž·?ÖÌÌ~ïþ¸½ÿÿüæuvý®]v—;ßi}¬V¿vÕ—¯–Ö»¿ó¦?´œ³ýà 7š™Ù׿q­}ã®{±bR\b¦l˜98“g•F~öx‘cшïç?sÆPɱЩK¹ï)–¨£oIîÆA¬±‹X,%ïXøuóm „ON95¥s5s†È3-ÓÌ ƒ¨Ú†ûÈx°¦ËÏVÛš¡©\HªXš\ÖVì.`l ½ªÕrx­*Ø%óÏsIžïÛ´þ—°þÍA­ÿé¾`ݱ_,r;V{ö|¿¬¿.ðºd9¯ ü÷1Gaçý";òˆÃí¦Ý»íWû»þÆ]CÏþBÞ—ËÒ€Q­Öç±{š7=°Ð_¿¦@AÝÖ™Ô*Ǫ \ÖÂû­” O–c9³ A²ÄÀÜ °ïF† ½ƒýH”û“Èô!ËŠpXgˆ2Äb>Ê+ äqèÜ—ÌZèh'ä;¼vx^•¬Œƒ°J3E‘k÷´MŸq±b4’Á)[m^ë8»À3?s¶þ!¦cíšl8\^éÒž®ÃŽÅv¢pÒF þþ˜æJŒQ‡Î* ú£Ã™˜Å@¶'¯Y|Î ò %ì[­V¬t¬ØÜ ”–x¬\ޏb £!A¸r’ ¬ dÅ¡z7œ°d5­ÀI³÷N¤Af n~®Ø­.ÌUÙ¼å´_ƒ*ž#°;™\e×ûŒ3Üœ¤­Ÿ%нi W?‚ÁbÃÕ=Çï§)éçø:?ÊÍ/ZVÐúÜ·æ¹ð ‹`EW+ĉLÄ£Oz˜í<ó4ÛyÁ…vÝ »ì¬ÓžiozÕyvÛC±ïÝr«}隯ڳÎ:Ç>}Õ:Ç®pA ‚'W?#Íðúäüë‡üLgž3ݼãöòMÏÌìÌSŸf§ýâÏÛ—®ùš½ç’Øw¾û¿ìמÿlIws±ÂÅ‚–½ªæ"Oiª‘ýqöÎÝöøýðÁ÷ãï3€ÁÅÍÒ4æù}4U-Lãõ¶¡â¬[4{²Dda’õ̸Û!€D@É€oÔ÷F˜„€,€XqÇÆR77åࢳÆ&©¾l‡Š-Jé¨>‘k‹ÀRsó™ an²×Æu¸f{*I qƲ ãg­ÚTŸÿàxÀƒ f؆¥MX<ê±gžfçœÿ{߇þÚRJö„gŸ!/V\³ n”MÔ«oßÿôgÌÌì)ÿY{Ë;ÿ¬+èG(›OjJÉNzðÌÌìÑO{ÖêÏ“:{£/¥Ø?þãÿkffÏ|òlçùš™Ù;ÖŽ;úh»î†]¦ÔJf¦Ž¯Z@8ävÀØWÌ¢ðÅHúˆ ƒÁ‹œ¤XB¾É³Í3Jœxþˆ;û Ì'&c²wv@°±±£Í¯8ÁŒwMcæÄÁZ;søg¿nÝçãŒÌêß Á@-‚,m:,ÌÚLç~Œ¦ë¤=÷±CµI±pÎÇ_·¹é9]¥IôAŠë¿ÊõÏ w”mäÙb?ìúGc 5w6ZÿlîrÜ1GÛ¹¿rf@¿úÛçÛ »nŠ25°¹ßîp¼NØØCÍ3äN|Ú”õ¢ E!7T=çÁ,eå k”¼Jäð&…0]@Ö+Ã~uÒ¹ZW€‚‚Hq;;Éh2™¹™ÅY&>†Jq 2’xK±aƹP$©cW¸T–Ô0;T×Ç÷ËÁÏRÙ¢‹{¸rÕSÏ+õ¼Wb7xž£‹v¸eö ÓØ¡=?ÿÁ`emÁ N6ׂ`Ï&ƒK¬ãÑæe˜¡ù¢ö~Ƥä(õëƒA;jLŽì£-?…”ºíg¦(ÀÇy:Óûàt¥Ûjýš(ÿ;Ö?gHñg#àÁ¦eçœqšuÄkèUvÝ 7vîÌöÇLKFáÈj ƒj¹ƒÜ %m‚åðŸ¡\ÍD&Pâï ¢•3ü¼à³ ¢D,T"æ&‹°W´f‰ àöIK@HC–¨ œö¼«ŠLK¦ˆ×NÐä#Té&Ú†ŠÛ…Š›I8×C¦Æs•ìF' 6•¡6rÛj.¡ŠX a‘Éd7^Ó~Ö€h浟éð|¡Ž9Ê)”V°'åÀسLݬPŠlŽ GU=œMÊ)Ç‚”:õ!sGÍ!å|uó<$ý/5Ê ƒ¼nÀ0 bù!ÝZñ7€ö½Cª)ä)uìHÑ–œë¿9·Â¡|-Ñüt"i¨Û¥—ÞÀAÕd#:· $÷ ÌY©Vê²k b³¥­¸FWpxX²eY¶ë rï€þB)Ï…@2x?ê ˆÃ#G³ª0`wJ777íßýçvó·¿cW_þ»äâ7™™Ùž}†]yÕíêË?Ðl²wí¾9v€­vQ¼À‹…±ó\ûú7®µ“N|€ýüãc¯¾è­áˆ Íǵ×Ý`Ïùµ—ÙÿXÿÞé¿ô v衇؅oþ#{Ë;ÿtv&@H½<. ÆZUÈŒp`(ÛkcAÆתû®¨ßÍÍ=òÁ‡7ºŠƒê,…böU?°¾l’-úŸ¤`Ì"1؉†½ÛÝ¢½?Êij¨…™®/âœúõ³Jx,|CV'‹deþàf|¶ã$uCÙVwÍÁöwª5:Ûµ¼ŠåÏà€b7¹)­žMVMp³t­i8¦î.çåhæ'HÚðž/ÂNQŽ8R«ð3'QÍgpŸŸ¤Èð^»!&ë!£¦ÙûusßQ¿ÿ—o캵¬‰Ç‡øcO~¸½ì…ϳ\hïûÐ_wÊ\°›¯tÿLõa–“ÛUqÂ]`î s‡šßC±1<¤?÷ P³l4¡fkƒ£:oªó=Ò™2[Â6ÕÊIŽ TõoI<ë4z²¹Æ(d×AÆ4{“H–7ÉÚ`L &5vÃA΀¸¥xœ£„mš³©˜  Âô95€0ß>è¼6ú9‚ÌÒñŸá±ÁýFà¢f‰ðç[1À8»³7럷ûúío¼xŸî±Ï<뜽XÿS}~Ëw¿ÝK½H&õ¶dïÌ6Õ©“Eb#°ÎÁYáö€ ¬ˆ‡%[y­WçÙž0 %œd4_DE{30XÏö0ó2Ú&Ü?4AàýAÖÊÄ|S[oõóØdë J)¶@Ëtâ8—Áà÷ÞJ6·’öÌJÜĿà ÎýÔ(w›­I¾Ô^F3ÌR|SokÜɲX~fƒ™”»YïÞ…ÌPË(BV*õ@¨“5 ©WDZäÌúÜž<ÏÒŽ+ÏG naŸÛ€dêíÅÃ1L)Ìrñ ™ –Ì1ÃòƒJí˜÷IkRÊ8¯BÕÃöå$MAp¶gôsùo˜enbàjü~‰îoyï8”ýþ‡ 0ô€“œÿìv?wúöKÅ U ;›°±±agŸö,ÛyÁkíý¾¬Ë‘á¢Çßm¸1kHF#ÒxgÂ#qðß;Ô<€Ï£}4PÒ6ü;w\|vÇ‹8,Ê9ÔÓ?‡YžŸÁó¤l®™a@V©Ïç{TîÃ,¹);õý¯Ðû qé¶ÿœˆ"õåh\àÅ:g¿--«ÆY–Õ¿3…˜F׳hŬ²…r7åŸ#˜AiËËøóq®™&dxB°æàªcMíç½í÷Rþ}rÂÛzýóu¿Ý×ÿþ`ÚWÇ£Úr‰’ÌdË6“—SfSˆ…0`XY²³B]:"ÍÈ\ˆÕŠ¡š4¨ß¹ŽñûpÆ¿·ãÁßÓ,:·¨àÚÖ˜¿¾üaíób~³+!Ø” dܺì!~NsÓ/”!²€ž"€fsãcûm`³2Íy1+©²kF럟9sr¸nþ!§Ö)ÇN=ΗH“5cÕÒ@QœÎlr R3£5bÐ=WÑÌ:°uöÊi«vÆ Í.Ù¢T sx:ƒ&*Ó¢¤Ua~¦ôöÊ!ˆ4§0»Ä‹´I‘ã;8Å E¨;èñ{6&ròÉj«óœs^åJ²'³ï À³ºe4ÛŽ×"³8¤ã0ÊÙêä& R ÙYMM¿vÊ 2·BÛì°þI®é–Ù^' Óh¶ f‚p¸%M#ù˃ÿó’Å@À ´Tf‰J?ã‘Ç¡Tf]A柃‡*vÜõ‰‡úY‚ƒ7n~Ÿ®»eÚ ‡;ÚôÉA¥¸Ï¡c!˜—õÝïB€dA._L‚”e`9ü1:‹È¼8`ÀßGÙ–öÇs™Â0?ʲpÄkq˜ŸØ ÌÈq»ê ¤LÇÊYdH67÷霃™”R3R˜äe“ÌÌçd"Û4'¬‚0—Íá ó~–ËÀ^¿+`“ƒ•4š 6æ!M×Ê2­: Ê™M ŠìÞ"`öã··ëäú´Ý×ÿ¾Jâ —bb=ºàÅ<¡Î‰L€Ffc:É=ÍÁ$rˆc‡°I²cÑ,Á¬Ìg§5.mŽ‹n³àhÇ–Í(ùqvÃhÎ ç5ÙµíÈu©±)çuȦg’*ƒÓ”¦(ü<–š6\¨$mS³Dè$$]ðf궯Î\¿[­3^‹sŒCÞ/U2>’EÎiv»°O ͪª`Twóíi3˜Ù’S7\ß v9C{g`&Úà}&ÉZ&9|/Ü[­JæÆA@ÇΩmL½Ïm¨™Ùá9©, ;'02ðûÓµ]#rö'UÉÒá|Rd$*ɼR 8 sN47Æ@’²\'6MpIäÈÕP24®µDŒ#_å>Çϰ>kÄyUܘÁFÏˆÉ ÀbEkþé~Zl‘Ï2óhëe;0AØÅ­µÚææfR© ž¥pPÅZ{,¨ÃÍòe”aaâïÅÝf¶yöï!ÛÂ7U¼¹ûûb÷xŽnçEÂ3Xt©ÀP–Šqç ‹A~?.R;/{‹Cð(¯BæÁÿÎ2/,þy`n&EL #Kæ)£§éè"Û¶èþޭƹJ~ ¦ÏIFZ rFÄ…ƒ¶ÓT»µêæ/Û„¯]¹éÜ¢½µÿ¾‡ŽN®všç“&»x¼üw¢ù²Â8ÆûÎ ÎÁõ¿õúß Ýúäú71¤Ûœ›R_èsWÚë2Èòaæ3†2;½Ë€5UX©ì °Ûî줩øOÌZ ìeÕµRE3¹K”¼À<Ëþ*±YÍÈÀQU@f}Ü2”ŠÙN¢¨À‘3IØ*Ø£0ß„l³Žqë2!ý=°!7rIã˜ÅÁùˆ”uøªwìCç}Yºn:²%m>„X¤¶ÆJíC9±‡¡r–{ %gà×AKíf€Z6 Œ KÝ„)D{¿”º\)w#|ož;j€‚@a4€QB¸N†vÛY#“ˆàÎ[£|ooÖÿèàÁS«½($6ÌÑñÄp]9¦' 2b)ñZ5 ¢…?¶Z?×1gäöVKéŸ5(g3N•‚ŠslÌøó¹ J-]Þž¿a‘ƒôXœ°tŠo,'ñB5ø|“E&jêðGg&e+Cû(»Q#ÞÙÆ‚OIgF)õÜ¥æN6‚"&ÊÛæÛÄ…ŸÒžba†n/È'–(ӬĢc<°èGðÂR57pЂŹÿ†‹Æ€Pëdb#’é¸çƦ Pš€È2°?˜±¯7žkëgJðýýïþ'ì“eò*8"Hé¬ÜÍÂ6³³^³ñz_ÂŒR%#‡>~`Ä,£M˜3*ësš¥CàÁõ?^ÿûåÖë5¼^0äTX(ç‘,ƒçè=’`–º5KE²ƒ–¬@ØzØ>°>,…Zh¢cVt#“(èýw2;¨ (4•ˆ°x,«ƒùI ÃSAžVÑÒ ƒož9ƒ¹%ÿ·gCUbíPê–È "H_`U ™ºOÂHGr¸9 ‘:êóøõ,7Ê‹,탱ðÂ"=fk•a©O`DPÖ†²:2E˜˜R2ëÙœ¥À+Ù@QÆœá@0Aît\(ìµ°©5²CƒsÏVØí˜ä„µ ÐDÙFÄêtn:ЂcLâ=΢:}‚â.ÃÀ4Îñ¾†k¥ŠçwîFëjNΆÇFÅ;´ßC·¼R5&†œùû ŸÇ0C„Í•&}Ë‹vM,ò"rÃÛ vâb0çy®B9ªWÅnK3+Ã]\³ÉE;Ý8ƒïãcv¥ÂN´Òó wוOp·»Ý›››A7uàã*ž…AfÄËT¸g9ÿ‚³CS±?9¤!0âY/º}[¬Ls+…f"À˜×R0@—´˜“cmáù-+ÄksÙöu®ë‰ÀÆÝÓFób,ÛRÙ9üã‚Y Ü~ÀL,¥ Û´>jDZÆÿàúß»õ¿O÷ذþ§ðÞiýçp>:†…¤c‰eRÎ$ D™Ÿb††æ[pv‹ý&¿pàÃÇÆß·…gƒè³`…{Zç„U·”»ë!ðèŠééØf¸à8!#sÐA¦áø#XÁ9-d D¾G"“`-JBçŽy¢«¾7ÅZ£Nö0“oÐIW)á)µŸ£±(ñr‹â0òv,ëfIü>âÏžŸcµcceñlf´´Îx². AvÕ r lFg\db6)YÌßâ=.øpNð&6*Ú1;hšÎJ7xÜf'¹B–Ç b±ºAò0œSÉÁéÌ·Q DMÒZu$]BæŒY ¼6G’GÕeö Wg2Ôõ>”1 Š5ã¢æ{ðúàN­_',›äkñàú¯ÿý%7ŽÇ<Í®ιñ´ïÎŒ†ì»™„Ýè>7¨±84ÓïCqZ:YW¢÷ïæˆÜ‚šŒE©¤MG`Ô¹äÍ$FÛÙ³Ò 0@0üy­½¤ º§áßàæ2HÒ¦¶›ë2-¨ZTµÁ<ÕÌý&š©ôvü¿j^()\ Û$)S'5¬ýúã"´Uf§uç­w! q&fPDÆgÔt36P£5³‹ o@oáØYêrzÈ•ZVïKÛ‡f ÌŠ¡6‚ް Àxä”»Ùv‚c‹äàÓâï$~ Ûå–àŒØ%¯íO±NrØ1QfN0'Ô$t)õç6ѵ›âyQrD¶­nŒ¤¸î˜éW Ô\Þ—Õ,ùÈ%².Îxvæ²Zmf¢ê&Ûqƒ„ÒòûEÀ2:¨ìŸ¹=×!ö¢dcc£ëHãüP,[SR4•ÑÃŤKoXBäY7QŠ´´={ötÅêˆ)KRtÖbFÍ+¸ t–¦wb›ÜÐd |ÎÇjî'-˜ñ}&æ&PƒA¥¾M(Éb‹kžùÁ÷ŽlWøT'’%Y(B©áˆÅ DŽæYüÏ={öt30ÊX€¥vL…ãµ…×çô(Ê]͈áö*Ëuîê\ÿzýïëW­ö¬ÿ~¦YŸšuaš $ VB2!¨Yð=„ /ÈÓhVÉÈ*ºiÑ18¬Wr‰FÍ‹Pä(AÀ2’ÄÅy“Ü6[0ñûÆyC$›“Û·fù2Iê27NHj‡=vžs‡²>®e ¬Š}Vs7â^9’°2¬ôu=vhã‘!qÞG}Ÿ Á&‘³^VŠk²Ò.W`ZÐdnÖ;sͦ» Âî=þ~©¥“Ãã,¦ð‘ IDATPºàØ cÂÀÑCKùàïá?ë¼k`¸½ –é=˜¤L"eÕyBmë”)ã3A³˜K4Zÿ8'åŸá§íÜ-¿Ãæ>Îê°­ôÞ®“­ÜÜ–ËeDZÉQ¨F+Op®b`­‰ÆÜ–ø³ë_áµ’ÓÌóÇt4²™å,ÞtPŽ"oJ¢Ë42 }·ess³Ý`°Øó¢»Øøº‘®YÍýàö¸“Ï* ÃÀ¬GJÉvìØºØ>©:3?è ÇíÕ6mvŮϪ8»‚R2FÑl²”F6ˆÙ„É"º„<ÿß%k“|-‡Y!W£‡'4|²Z+`V†(#½ºK¤FשJMÇù,æGï3,,år0„ŸÃVÏ,/ÙØØ×ÂÆÆFÇzàÌ2”¼­hî®9î \ÿzýïÓž~€õŸ#Ës")gÙÙìlr¡‘Yòl…x–àΆÛejÐ`$:ûkõ â×AÑ fò‘p»Ûõ¨ì¿Õqd©*Ä…÷À™ü=‹®mêþWQ¢èk|ý~y âîhg“«J ‘Ñ-‚íT¿#£ræ2NºóH’£(}¬r¾夥›­hëgeØî:Å|š®n,æÛ<ä#&Á&W»/V s/ ”°áÿ©‚¨Õ¹ÏRªçÅjçY4à@áåÈñBB†(X[íÌ\Â,÷¦¯‹ð¬ ‰U¾%Èå±4 Þ¹1ª~¦Ž£LÊ›ÖkdÒ¬ÎòN\ïØ4È9“âbï5J+êŽZÿbäÙÊRKg°ºGÅçÿ¶AXðð é4£Y ËjF2.Õ\ kcQß?ê¦bA¤>w¤ådÛjePÀ³lpàìP.:;öÏóææžî˜GCTµÍùøà;ºŸE»çÈÚ Õ225X,Ç9ž  ªƒ±ì §Ÿ8#4uEÉ6ÍðùçDffƒ<׆ĨõŒ‹v”ŽÂañ<ñû#{³±±Ñ±:^¬—–³sÔg"#ˆÛÎ.¼>™ùêtÏ×ÿ–ëÜ_k5±þk'IØtÆ—‹|³ Ã, špÎ…,P±{؆éQv7`*ȯXž†ìDf(™šeìç‘0?‰d_AF¡®†CÀâ|ñµŸT™Eg5dœ2½jº~Kd,î{+Nðzƒ¯{” И(3·=æ G€edV°7lPÇ añù,ÌÙ J…¢XÿQ&ÙÏÚv.nu’yáÁC £Ü¶¸àCõT€ãö³¤¯Ýaöe\£» -ËÒ›X<¸ýMªD ‰Áç 5°X-°6yfý£ÙËÓPNÈljýw3BÉâZ¾”N’¶X,¢DŠóBlØàöãþYX9ã•¢Ì €œrwÜ=F7,|½|\âÅÆ2//è©Ø#ŸáÁ t2ÈH80Â/õäŠÏ(áÜÏV’fPþ¦ä[åáLò°lK^³>¥ ~e©ÎlðyT7;mÄPµˆçej~-3+Á²Å,1 RÎk#Ɖç_¸q «º~•Ê \ÿzýï¿&“×ÎSZw— /Ä9œª QBÞçGHšÕ¬¶ùóæokÂÀ¦³²fvY.ÈÂy&g}ØéŽ¥fèôV‰5Ai2J’ucà¶~¯ÌÙDköK…Î&=”*~66àßlkÎͦ.hÕz+r%%ÉÜx¤-f‰ä<`ª9Ú7TǾD‹æ §" ]Ë"I[˜}a°GgèÇ@åYl¥3>ÍùÌ(ä3§˜½C¶Î,óB™œ’€©€V‡†*A+; ”Éj_ðΗˆÑñO›nŸ‰Ýñcî–ËXH·šËï³äq9ÉœV{þãÜR—Ë”S§üAPŠ×¸'|%¹–ª ò‘Kc¶ñýß›®>OºnäÄõß>ÌÜUΛÒSÇ]…k—§•ô@Ûðç<JÐMW¶±X,ò´ºA*Ë[5'ÁÚh,”ƒvÈqÛPÏÀdäV¥ºZ^ª½’Èá6÷öE0%@6ÈŒühír3dp¦YŸÅwn³9*ç!0SY¤³‹‡›®ŽíbÈŒq1Íny*OÇÌlÇŽÛàÄ×n»šY.—¶±±ÑÚü>ÈŽ¨ Qu£S×§›+°# š?Áíå"+£Õ©â,žÃéÏ]‘@Tåáñ=¸þëìyØW¹1²A-µ›gR²¥ËÍÁÁ9,Æ!é›ä³.Ž)§Õ ĺඅPR6)Àá~œ¹ cöÙO‡Y"–®ß³€gô0+Øö¥_ä€;Öý\N»>i‡NjZ9è•‚V»9ý[›ŸÃ‚²»:úû\ðé¨S®d¦Ãõ³,Hš°%N±`ÛLvÚ­Pν{[ +Ö¤“ä‘s°Þ캯I€U£ ¶¿'²I gµ³ðN¶>528Ìnø1ò´Á·0| ‚È`¬ÎJaÎRÛ?:ÓÜ^d¼‚û…~w¹O°ŸèFǶàu¹fKJí2zX’ɱ ‘¶ÎìA]ßíÙ•SüñwyFŽÁ‚,w2?îñJ]$ïyÅ õ¡J]ßjs¸ ¶l&ø½ðü÷øà†{ávš òBOÝìpè‹ì¢38`F„»µ£.þè&Ìô>…£iÆf70fFÁ’,AЯ)aÞÅfø8Ë ‘Ïžï7ànn¼?–0¼”ÏÁˆµI"°àÅ÷ÙÜÜ€e²³˽(°ªëá@D] $Ëf x-*½¼škQVÜ£"€%Zxý)ã¾öX>·{…8–²"+´ÕÏYæwpýϯÿ}½¿V²­Fw8”lb¡”| ‹aÅR ÉVǦàŸÂ®YŸùD‰¶ŸÙ œÉI<¿ìHË5ÁLÖÐÍ.k?æœp»0h´’ËZX'všCg=¨ª¨áƒ ˜•"†gomÄÛýTÜGƒ¤œß”t{.;m”íÃöÖaH¾F{håŽfƒ¬ÏÝé@I’7tmC¶!tdË …Ù”a‰ ËàJfÚ¸ç‘0‹¨s…£,ÿ>æÍð> ¹³e¥- ¬}°l(¸QÂÆ³,4 åCýͽ-¥ •êÂn­vR·x˜rHuçž¶5ÌCáö—o ×);»‘Õ¹ÊÏ›{·ífC g«Êø}:ùjÞÚ„Õ@˜!ÄÛ’Àê¼ýi“)Ïêx<„0{Ú6[Yö§”l±6rÉ1ª¶,Û@W©‹ì3­7Jo±l;;bFZg¾‰Žh{e™È\\Ȥ0îÄÅqE_:Aœ³7˜³ã©Oç^Íâ¬~/76f*(—ÃÂYÙ¡:S²,U3^3s1ʃuñºâbXS+ ÐïNsÁUX0•˜%žÃRÁ·!Žae1‘Ô.Àò÷¶béÕäh0‹½`5ŒlPŠ (0×`Œò6fL¨0’I¡¥Í»„¢æxÖ¥Xm{ÑÞ:寶DóŒ*J¥‚ܫԾà' …\<›ÒfG(`•óz4´gÈëh¤ÌŸ 5ë ⚉³S<;€\¢ëÍRçôž#h^Q`?l°=uxvÖh’€ P]ßÎ2qæOö…;ÞÈ\¢oˆY/ëfKòwìý–æ9Y­æ­Ý¥´_ÿ+p”‰u.âY[èþv`3ATŒf+pQ± XP`ñ±¹¹Ù[,ò?G!…úè…¾*Ä”½5Ë¡8|’g8˜½@6BÉÁâ!-¹OÑz_ëæn.à† ˜Ñ£\}¸Ðf‡1fÔC²Ëå2)tSÅ+¾Ï+Ê˜Ô Û&沋Ep)cƒeE­œüÔ¼oKÂÔ1PøêZÆm›cBðzÅWu#ôßU& JÞÂÇSó\ÿzýï;ÄYA]ÝÚñ­+¹Rš›Y7ÿ#L°Ð.8S£B…±àf+gbAmªF9Èpc¤qžaÁ€ÀCeéÕ¸mH˜gÌ ðµ±#9 ÍÍeÞ fŠØ™ÎXIÞ0$Õÿ,(Ë$ð‡Û†¶ÜS¾—Í%xßd&-öUçZ1FÜhQÀG)-ÓÊ9i‚Ï¿ sì¢æ ¤Též‚Gýó,J°‚¬Ëbn¶ ‚óž¼ÈCvG‚"„˜„…s6m¶î¥Iÿxþ'°0Ì\¸ä×…Âßz[ž#ÔØ¦jmn'ÈüRo,Ò¸j5H Õ¾c¬ºø6圃Å7º½×©Ÿ[!,V¼&e Í\r–¿ßäö[;ég¥&TµjžJÂT1¾ñC”àV[á8ùïbãr}K…ûVkªAFÙ÷ßnÛäk” ÀÝ#Uø±Å/w1Ù Œäîþ(Ãÿ…ѪT3#¹ÇJR¦ºhËÏtÓ^„÷[,6øáâÍ™Ÿ0*Á$Á_«ŠQ?^D2Tþ‘êˆpA¡’jVÅ H´¨V`Y Õ•uR8s¿¦ðûÈn(½/_ÇêópŸÔÍɯm.öÙ‡ñùc0ˆçš"Ký5;vì褑ÊUj4#ppý—ý~m3AëC¾XdbE×õ´Ñà>2~Š0#Èx¯b¹°9ÕbPgO>¢²ƒZb ub+l—B™×»55’ð©¡ý`H¹¥%%»c³³8Œ Ÿ×Ö-±;~¸¢‰B_eéa®¶«Àû1pãf JXGAÝ£õ?’€F6kkéÐP"™\^ä:° Ä€ÆÐùW®fzºWÖŽd*G¨ÓÖèvÆR5–la÷_Í)àÃÒZ˜“Ь(¤JÏl ‡ `ÔIžõxÛšSÞ ÿHJ!E *îW˜7 Ö5 „¢L CgÉ:×<”é¡\'+zý+ ÍfhÆ23t†S’ç`Ç2Â0ÙÞ.S $o¸¾kgQ¯ý–²ZÿٔЧRê_u v¢™Ë¹Qµmr‚bHe–…jíy8ZQì£î¾²ºU"Ë´¦Y‡•ˬSѽ:‚øÏ¸¸SfUd1ðŠ?« ¬¸nº8µæ:ºÃe*¼Á›‡û¸àÄÓel̺Œ iu>àurG’í™ù=ÜÚA oë\0ªb¬ØF›-¢}~…™ ,ôpàNe¹V¬s ænü÷x6\?Ø @eñºÅy*Î*êýŸÿú×N\ÿefý—-ÖÝ2¹û‡ Û‡ÖÃêšçÁú`Gboqé€ú@²e0_“ؤ€;¡^è+T$7c†%Ó \7ßB®wœ „ B°• §&–¥¬_›É^›÷¯]ó4«-ge)ì´ É8l€õ™%T‹bš†& ãe®¢%ÿ[Y‡â¶Ö®X•uõ™Â­­ 9åy¾Ú˲k©Úñ¬R^™ü<,ÂÛÏ­·”¯§Y“ÀfUëÙ*2B”ô‘ѨâÜMcJ€qqЉ`#H¶Œf£<ÌSÌåsƒ¬MBÀ*2{0ïÕ€lN-ÛGIÌ:×»ÚK$Ñ ‚ë Z¹ƒÔ,9Úkw&6IêæÍC¦PéÑü]Ê1‘¥à¥,£›'°Èèë:–ëM·ÑUMŠ·ªÔØ ÿ¼äÛ=*ÜGàþî…Û aÑç#f} Ü ÿäY•,Í%KPxè_exMÛ¼ènÊÀ/Ô‰9X9ªU@È©Q•PºÚ¦Õ… Çg|ܽMZ¾mè~æ`g=%â{9ó·­ÈœdEüœ¨!Xpðø3X¢‘]ºøuÈ6Œ´èl¡ŒÖâl, €Ãü½Qø˳°˜æ"6WÀB­ŸGT:Ë«˜±CÇA¥˜…³±±Ñýs¹*¨Ôeƒlïr6>?ÿüו疯i<_Ì¢YÆ äki_åp¥É"ÐåP³É‚F+Í®$ÎÀAжΊ@Æ ðD… ¡Š}e¥-_‡Á«øþ”¥w°u³^蜃ES‡$€‹1°Ãb„@€QHj"@„û22/@–•@_Pcx »ô×àl˜éœ%¶Òæ{ÞCÈV¹ #FxÄ×"²‡rš5·Á®z+ØÈQÇPMÎİ‹[»§™nZbFQ3Z@É”’±Ü«”A3K‰œîR/ývÙÏŶ"õ³+œƒò³RËÄÒÔèR×Ú‚‹^x¶sÄVÚêu!û9eéf L¸Š@ËJ}î”r $‹ÐÐÒ‡¤röŒTT®ö©dZÃ’{op³‚ˆG(:vv`¥ž9­eÕØXžØ s¦{-Ë^í9® ëÙHDî^º¤fâ¶KE§',Lqæ@ÝÐü÷ævG¿ËŸšgq° ŽÛK#ˆñ×EyÛA£›ÕfcWð¸cÓ‘þZd—66v@†Ìf˜ò×ù¶ ÔmEÜA/a!¬ µ* KÅLño$}PRˆ¹lŸ¹,–?¨÷BÆÏ  Õ]dËcÜOU3«2J:WR9Îxbö  †€bÍŽkxÌØ\`4ïÄŽk<‹Æ%5w„Çr4G6’àýó]ÿU?> IJtÙ@< ç:Öû„R29G‡ ­¯ƒ< i›qJÄü„‚²ˆdÀ¨K6œ)EÐCÁ¡¡iŒÒ]5bs 3€t ‚ï§eM½Í¸,²T.Ãñ Nwð^AzŠà‘ï£0'”`ÛxÖª{dàpWÁŠÖÁìªXŒ PÝKFÌÍ쌌Ôï˜UD[Í%I·-d pžEY-ch©=Û`µs$ ™B0\OiYíßÐÙ¬±,žIFCò°Í¼Ÿ,éC@Áre1 »Œ,ГõNjdŽfZ”µÆÏ?%ëås˜ҼƼáya Aà èÂ~–Ú1C~NUÃT^“,•Ë©wº+:7 ÃÃ÷D¨jpéìŒðr¥Ð:ø4Ü£|” %é¶]BÁ0¡ÉL-¨Œ&ã“ë?ÜÂsåÔ†{ùÆ‚˜6Í<(-1wrïïì%Ïy†ýèñwµÛzˆ}ï–[íK×|Íλð öwŸÿûÎ .`}ˆxuK°•Θ¡Ø˜Òææ@è%HÞ…qpRë²ÿdu¦âyÃJYÚŽ·iŸ‡æ+C Ÿáf“]6¨‹0GäL³5舆0:¹1ÐÀ‚ÐÿîÅ+ϲp±ÌN^æ‰ŒŠš)bœoT.C¦·› Ô` ;–±#¿^Iq»”{š1Œ$e\Dï ðÄ<#eEº¢óÕqõsá¹JÊú{¯•TïöÏB»wë_>ôë?u Ê?WBU$â6ãúÚ_rãÅ" ´µsÏ”#Äzï 7«ÕR „꺨lÞúµCvÉ]‘ÁcCI“Eç6fTëÅá=•CZÛ¿œÍ¼Ù€ù9°Ÿ™-¬ƒ¦¿q ¡ªôÙ!kg½S©©„û•­#EöØ øX’¤rÀº‘5ÔY$l!…~ØÏ®vŠkb´,‹åE–Æý¨½<.Y²šâÜJ`tLz®Rh ¢Ã{²es©¡Mðuí¸eë@A`´teë‚ZQnVk]pGÃ9—Àµb? f•ˆqûiþ œ±Âã„à2oÂà‡A’Tp§9ó1šcBÏ1“¥ddX´³´K¦ûÆ’dW憌*›ouž¸Hçy—9V‰åŒ,3dæd¬3 î}¸ |]íýú_„ëHõãÊL:¿Ì2»¯_<ƒæÏ— Èå²tÀǸó/€D33‰:‡,JV£ 5úœ Åw°Éösà'˜&Ûmƒ×p—•ÑþùgWdoÈm-㬎²¡E ÎÁ}&sn£¤wfG„ÍwRf m´3­u¿U,êèus¦8s€iný£{W¸,²l Ȇ}BÑŒJíÃG­gžÃÿëÿr¸Ê 9KÕx>äYoÿÜíoŠóAÁf¼ÔÎY$ ‘„­xŠÎqÈÈ´c™S@-x5÷³Xxìqv©Ý¯M›A(Ûíöýç…ºæ¥à¨‡ò ´ IDATNƒZÎ5ÍŸ±‰t<¤ëÈç~ºkŸn£Fšmñü¯$_ž¤zæ,îU“ÙVì¡ÔŠ,—…çK3jŒmü¿1¡¤ê>ÿñOîÝr8.ðÁÍÚb,<¼`ý‰ûÜËûȇÙË.¸ÐžuÖ9öÉÏ|ÞRJöñ+?gÏ:ë;çüרcNz˜ÝïÞ÷œµáD\dEV³<ª[U€µñnúFC(¥CvÈAÓ4[”ÿWÝÄ"±kÎùøïúçùLÒÄ÷ Ï'‚a–"ÖZíÁ?q?{ÍËÝŽ9òˆn¦‰3˜Ðí Ó>9” F[é·K$ ‹Á¨æ9‘à Š—d QþF²¬J™=uÁS„k‘pèæxFCÉóF€·$kÆ•€Mâ'>ä_pè+gq`jb‹R+0=•Á _e°×Hž±ñ³„WÀb4ã£~¦d=sÒV7^äaJG­U ¾•ÑÊ jn¢}CÙ øcáÏ!ža¦f‹ðßèݲ<ʺA+g”õ…YšÍX2‡³nÀ€CüJrÆ ³LMbf}¶†£rgÇJ`¨+æù €f0 °ñúW¹G|`.äGYt„«¥Z]Ö¡¼-0wƒç×lÍIÏU'ëæ|FóE«]Ãc«L"¼oã=–kÖ^:ŠÛÌy‡¤>j³RE¯ÿ¢ÖÿŠIZ¢‘ |!—,/½qs ƒŸ9™ŠšˆN«ïýèñwµ‹ÞöŽ0HÍß‹Þþ.»ûñw•lÓò9ç&3+ei/=ã4ûÄ{ß 7á‰Bç5%arÐ/È"Ã>ÝAƒKq~(˜¤d‡Þæ6Ílab›rÇmnÃy%±êÄÍìC‘«,×ÛÆ``TÐ3À™¡QÎ Ïí¨KÅÄð j.¶UÆÊpáQf³˜5Q¹êžŽ:©èœ6’_àû"ÛàîoÈîÍÍ'ÉbÓ&Ìüù,M5`¹÷ë¿ÈYt½eJØ@½þõL³‘ÈV×ÿêû;_õ»vÓîÝvÔ‡Ûyg½ÐŽ=ú(±þYľ|m.—–r^¹í ×ÿêAÖ²„ˆÁ(<Up_Êlu=0cà‡3®‚ °ÎéÍ_Ïë߬w«SÅ50_ÈF©ŒiH"¦Ò¼Î3u·O™e‚ãÛIÉÝ¢u†¶ÝÍM‘ò„öfýwlÚÈmO6gï;’ñt?+‘aÀ×…9ºj³ë¿±$ë ~®‰ P؇d‹Äžyàò^¦ÐÚØf¹ÙaWa8R ŠåcË™6ˆ@[o·«n`Ò¤ähuÍÀE_BE`Ò>‹;½5i™‰&t«³”Âï/«¼¾P¬f;ÙgÙ;Ötôüç Ô‰IÊq”I®ÕZ-»ì¯möùÏ@7ݶø¸¡Bj²Ã†æKµæ7ÝÞkרÁûUí¼]Üá°Çzì„®ŠÔÍ`M‹ê¶‡bŸøÌ盄ßÇ •O\ù9»í¡‡´÷ó°C”Ñ8‹²¹¹§IÊœ‘ñßÛØØ!¤Rј%tìö†Æ.OkËæž®;ŒÎp¾}ff'þû'ÙCNy €egå½ÚþÞX;àªgkâ§¾à%v¯“kŸüìç;™wÝk­ö Ç=ÑrÊ/H{kŸu hQ£Ê{d®ÀŒÏÈîÕÉbÖÉ‹Vdox»âõÕϺŒì½@âçâ ’gLð}ØfapfFÉ]°`ëMeʼnǛÍXº¶õú_JǸ~ÆëŸ¯u½þWÛŒa¿È@zî‘’JòìS£êáu7~k·uî+í¦Ý»íÈ÷såL;æ¨#Ãö«Y»}ùZ¬vqý+‹ð¨ÇVó<è̆À$ÈÏ@®¦ŒRΓ]Hµx®ˆÙ/Dºµ£šmݰ£ZyHø0f6'C[ñ¸Qþ§¤·´•˜2þ\œ3r÷¶¤dç>d{ʺ˜R€Ftµ%7£æ6XöÙ1w¢idØ9 ׿KȂĆðqÞÂÁAëtü †}bq M`ÖK‘¥aÌ»”µ¨cÔ<á ½õ6ÑͲ٢3.¦~’8ŸV»UiMó1ç§Bø)¸çÅY3xæ¤ÜXd­Ô e‚jýs®PÞS¿þyΪálÁ¡køÂÊÙœQ ¶â\ßæ±»n€š"KÕ@Èèâsga6Z³)µOÎÍ¡¸kfÕéBä93||?€™ ug3MùAزë3‘ \ÿkÖ§=ÿCSn›0A“ló+Ëîd«YŽR–ö½[nµûßûžÀàôÎW÷¿Ï ö½[níd.X$nlìhŸ³rS«öÁ·½ÑžòøŸµÃ»}ù²¿²üÑE ̼îÜ—ÚW®¸Ä¾rÅ%võåXËÅV§äãïy‡sæéöåËþjõ³{ßÓ.yÛ›ìš]jçœyº}îÒ÷Ú‡þø-¶\nÚëÏ=Ç>þÞwØ5»Ô>{É_Ø[_ý_Ú¾¿á/·k>v©½ò×_bŸ|ß»í+W\b¥ûÊ—ØßöÆ€^ôËO·ýéÅm[Þóæ×ÛŽ=&X~ì>ôÇo±¯\qI;Îw¾Ãíí+W\b—\ü혽þÜvõå´ûßç»ôío¶«/ÿ tª13û½ÿò›öåË>`o>ÿ–R²k>v©}ðmoê:ƒê!Šr"ddq›™ ÕMa¦I± òÌ Î ñ9w:•å1ð8ûã®e^+ûhÒÇ¢›-KêF`“ßó2:‡ê<³cEÅB²;]¿þó`ýÉà¨<#íýúßÊDÏ­ç)ö”å{þ½¿µÛÎ>ïUö­Ý7ÛQGaçþÊ™vô‘Gt`lmÂñgC„ݸÐ@vÆ„Kù¥” ˜ba(ëÍ ìïK˜ƒÑ¯ùªúäÿؘN—7«vêO±/]óÕöï)ó'Z"»¤ÌOþÉOzº½õÝf7û;v·O¶Ÿzò3Ö…ÿ¶ûÝëžö¯üÓv·O¶?|÷_ØÛ_wA`bžòøŸ³'Ÿþb»Û‰'Û§>{U»¸O~ðíß=õ™vò“žn)%»ï ÷°÷\òQûÍßy½ýk¯µÞïÇíœ3Ÿ·ÚŽõ©þ©‡žh;/¸ÐþÕƒzb§\—œöˆ`W}ù+öŸ÷õvéåcw?þ®ö»/ÿõx“\—Ÿ¹ê‹ffö´'ü{K)Ù3žxŠ™™ÝåÎwjƒãïò#vÝ 7Ú'®üœ\hþº§žò8;éA°/^óU{ê/ ƒ¥ü@cç4Jå¡r–¼aËs8Í#Ú½…óš`€:XJ ˜Äu?7ë0ZO¸føõ|ŸL3ÙSª°ÇY´zžcФ\ºDiU(ö­Ÿ% Ÿƒyr%‹Tãô8ÐÕŒ$tÁ–ærŽÆ $L ƒ!±]Q­ ²v,0$üžõ „á>²qÀúû$£M3E D¦‰›±Í}ÎzöÖɽÜp¢ôL™ºŽºÐ[ëm0à?øâYŸ°¶ä©‘La IƒY“í5à¨9KÊ&< òdâÂá¯(€Ýô±n`ba¦¨”åd–ФuÑ…´(WIloÄó28ÿ‚–БÊ-MJ–R¶ÿûõo´ûÞóÇìÑ'=48²ù\Ícù»ï ÷°W¼ö÷&ô+‚DQÆVÊr’´µ ÜJOzÐOÚsÎÞÙ@ÙË_}¡ÝüíïØ9gžÞÞëCWü­ý·¿ût'S{÷û?hß¼î†öºÇ=ót{ù«/´7¿ãÝöœ³_fffw¼ý±«íZŸêOþïí½—~¤Â@åZµgŸõR{öÙçØ[Þù§öÜ_{¹]wã.;ü°Û…âÝÀGþö¿™™Ùï÷ãffvÂÝÿµ]wÃffö‹ÿñ?X­ÕŽ;úhûêÿó )[ð¯;{Œ½øYϰën¸Ñž}Ö9rf&t=R’áXüo/´YÄ€bÄá÷iRò¢½±ivPÌ[>#ˆSÖÚ,RR3*.SC †`A”±å66›Àã§,£YF©f_|?•©ÿÎHÆÇ2À‘ôƒfn˜Õb—C,܈£‘g¼T·‹@gÙ:f,ˆ-Ù1GÙs5öòíÓ¹4 KÑŸ¨'g6§Š™—®pÅu ,DŒp¶YQ!«l…ÍïŒN^Ö**Þî ,  r‡Ô­a`Ò2¸ïF€nJÏQÖ§î-ëß-$å ö·ÎJ™éŒ-ƒª‘4’½) 2·fù6’ßÍ6jÏ,! Npe’fñl† Åg}ÏeÈ *ÑZY178ÇS–¥s°` Z8'>÷›¢ACp™ƒg°Â^ƒŒ¼ÈÝ÷1«(HØgýúns¦×bj”;”¢E5n3þ;‡Ùç^ÇÁŽ-ªW˜¸ž*ÖŠëÎø(6®1‘á¾iÃë]òÂ1- öÀ7“g©9#¹4ËY…BË& Ï}˜¿l,Ö4T6rl’² cj{ZâÞ3È#·;bŠø°Td„0Yv ý\´¿£-ô^²{ôJW¿óÌÓí±|¸]tñ;퓟½Ê~â>÷²SŸô„ÓréGíÓW}18§aHéÄ2y~2aÀm43»ÿ½O03³‹_{þp_ÌÌ®½þúÐåö¯óÿÂÂ8éA?i}ä#ìðÃþEccŽÿ‘ÿ3Ì#}èc;;8~‡ãޱsÏz¡Ýñ¸cí¸£nóO|³Ê9Ûû?|™í<óyvü¿ü‘ÆýÑŸ½Çžþb¼ïÛ®oí¶Ûzˆ}úóßb¸ˆßñúW›™Ù+^÷ûö?¿ùmîƒoðŠ Éx>„$±¨C†H=pÕ¼Òˆ­P,³E| X‡E¹ËÈ0(äýS¬ŠÒ˫‡èQZˆ7<„lÔ(sHIù}T("3rxÜûÜš^B§Bqùšš“í° ç·”L“­ÎùóFŸÇWc–çsäJwä‡ÛM»Wò¸]»oì‚°ýÁùƒÈ¥pLWŸ•WÚªWÙ×ô ÍôÀ‹p$3KÄî$ é4’³…Ž-< ƒù‚ÅàT/‰$iè¦VÉÈÁÈ\ ²Âê ±Z% àÜ •zÍÌ þŒ,Étn9ÿ ,nâøçãvrX}ÏŸQLÝ?mæ½F3‡Je0’±‡ƒ@©€v°ÔYZ§ÔÏÓ¤Õ Bv¡Ëì±h"À<¡€Æ T`}Ô~8K‚³'ø™Í` ÄLVȼ©F ƒ‘Ü*¹"ðhLºÈ¥ ôyÑÎyAçæ)ZuwǃՂt¬½6ÇàÔºªµ€Û`—9E°}l– Ï ®/”«¥¾‚oÏ*6íZð³TÉ’|½½ÓµZƒ© Ë_U·¬¥ÅÃ@a`š*ÈÞÖi¨í‚žÂTá{í}4‘p Hò€‘´¤S \6œ×÷yÿ{¡l:ÿ÷¶A«›Ý¢”îÙóý® v{Ó1Û£ñÛyæé¶ó‚ íúwÙKžûL{ӫγÛzˆ}ï–[íK×|Õžûk/·_ùQÀä°4ÝžÛIupîvâÉͬa’Ñ•ØÕ`måzöÈÙª3žñ4;ý—~Á¾tÍ×ì½—|Ävç;ö/xîºÝj®±`¾ï ÷°7½ò<ûöw¿k—^þ7öõo\k§>ñ»ý±ÇHëçZ«}ý×ÚW»ÿ}N°Ã»½ã=iºß¿µ;Ýá8{ă~ÒÌÌ^ýÆ·ÌJr®»q—ÝþØcì¾'ÜÃÞ÷¡FYšè²{q¾±±@ZÄ>”4eLè¦:ü p˜&ÈêÜq7ldø|)p0rªõ|Ó¢$b\ijl…ƒPYz7ÒðÏž­Ž•’8C´gϱþu@œ:WÎþñµÀò@vE”rÏ“!8ä9*µ]9g;öè£ì¼³^hG¾@gûJ»á¦o…÷Àµ²?î­«sÐ7úëÁú§R÷4³<°+Áïñ࿇œ–Z-9`ÁkŠBGë@šÆ¹U01Љ¼d\¬Ï£H”gÔH$‰+lÕíå‚&bvŒA—²²ºî{8«¤²™Öï]Ľv«ùŸÑÂ<ÜÞæ¤jsNŸ|0B3L0/hŽÅ’óÂÕ­É”Ö|˜“©+梫!Ï'm‚#]Ç"‘¬/ٵ϶ir±Mú<[Ùœ„s/V»0U”¾qŽƒ‡.¼5õvÝø¬jCò©†ß5[m+‚Ëfr‘ ìT’12c4¿\ô mà-ÛÊ!ýô M´Q`šÍ ’nt™K9^gÃJ÷Ïî{ä¤iÄ>KwNŒ?hvñämÈô¬^>ð÷âm^ß×|ž(÷Å/Û)þ»å–[í‹_ùê°@ô¯œó[öǯ»ÀNyôÏØ'>ó9û«^, •U°³Eè¦Ød|QE0=Õ©G ÂÈ`qÈò$gY°S?2K`ÉÏ9!‹Âf{3ŒûÄÛÎE>þœÿ 6’*pÇÁ¡Jv6'=ô?7ùHûÉn|øpÙ˜œìØýM$^»¸h¯Î†¯ÙœóÄ­ÐÙ¯8ßví¾9ÈÒ°1°?f‚¦sÛ ·Å…‚j· ë!9¥©¸#c’(wɘº žƒ1ë\ëÔ{T2&àkÓ–ƒ‰k„fnnržX-f…ÐÉç¸NsȼÕ5PÄ㜨BÆ‚ï,PcjØ×:´b¨ëÈÚxàÌ™li3<’B>P2)oR],“â OD:–Âb±ë…u©Å™¶b»öÿnaœÂÁ¬g–eš·©d¯]©p^ƒ2&(ÑC°1dRP*–b~3ayªpL̬H¤)¯Çj»?Šy(d±,CF9ßb$q´Ú!½!ÕÏó"M@€(º»5Y˜9ó]%+¤ÌyÂZ(5dUáŒY˜1²ø6zî1ã÷ef–'€´%•L Ìp;¢ëÛ¶è±@”-¯#3¤Öÿª6)|Üà~µ,Å97§¸&›Û CD§Âc~æFÏçyÌ#ÑÐû>ô×k‰3?þ¾þyËåæÐ†zUØmÚ׿q­~Øí€J)Ù•W}Á^zÆi¡Xçý) P…˜<·Ür«™™½þÜvÆ©O³3NýEÙ5Só þïïþ¯43³'<ægì©§<ÎÞóæÿÚ-FÄ¿èâUîÑÝíx»êËW[­Õþò#—Ùm=Äîrç;Ù—¾úµa¶Šß‹¾yý öŠ×ý¾™™½üEÏ·;wì¬ý3JŒ°Ë–É,IR6Ál¢°\.;¢ Jâä‡$º´Åy‰~6g|æÀš ôDf m¶è 0Âs€à%f£"\¸"óƒ`Y Âäàõ‹#Lóµ=Ê8âàR4Àm±& Ęók‡YvX ñ@7¿ïÜúßù¢çÛQGÑÐ7}«+þFyûÂU1,¯Ð!O†,¦CÆ ÏD &uëºÙ’—e˜7ªøûÊþ•ôâ'ÞدB 7ÿÛ5* 4ºãŠ×Q˜ °†’6”çÁïej&É["Ç$#ÆÍevÊ‘-Øi#ä€i²-N=«$®ÅQð´j̨ Hõ{J7YX0&,åêf=X:…²2f|À‚š }ŸGjû€31`¸æJ¬ÆV3%º³5·7žÊ© m€©öûBeCë¿Ô(/kùHVCFÓh.,ØN—:<¾Ûc¸”eé Ô¼>ŸØƒ¯Ye‚סƒ¥ðXeíÄ&> tF$ø ‚&S¢ é©A ÷²Š9I5˜ kkÿsu½ø³;µÆ[¸îÀŒäVŸ_Ÿ±qM¶åÚQïYÛÂ.Þ@ +›Â?Ý).¥d>é¡¶ó…§ÙÎ ^kï¹äÃÍaÎ×ߗǪr˜?šŠÜþ÷ßÏyao}×Êî+W\b—^üfK)ÙãùyvåU_°«/ÿ`ûÿ¦›o¦áܪõ p±¿àœß²¯ãZ;éÄØÏ?î±öê‹ÞÒI F2'ÿÚyþkìo>ùi»ûñwµ?ëöñ+?k×ïÚ^> íÅû7¯¿¡™'üí§®43³O~öóvó·¿Óäq zZ±÷ÞK?bïzß_Ùá‡ÝÎÞþº : ÀnB¼íX$¢]6°HÈ!d泂áJ”K„ìM¯ ÀÎbplãRMäþ ù@@Œa¨œ©ÔŽ9†XH2£0X’Ïñ±(aðÝšÙjíݲ›e°s öÊ6_…T«u­®žÓëâRp°“.ÁSîsY‚´5Õœov!ªÄ@ á ´FiVsS®–Ig©Tuƒ†éZšw lÛ T)‰ F ˜œ¬û/%Í(Õ:™¤~>ˆç»ºœ£‘<»ö W'{ón6ÇDæ:Ï1ˆ.:×0ÐÌ\ýlõü™y*T¹kì’ÉA%… ¥bàwÎA‡ëeõ½u£©PÆOÅõˆ½u.ÐÚ!nàÐ 42Û™æ70ÓuÐ(IvÝã·Þ^oyÿErâÒ7tnK)Û¿{øƒíå/~¾í<%ó‡çpZÃ<( ¿ç¶R`{ðçÓÜO Ý÷ÑÜ»–¡Ãf#¡‚-yÀ þÑç+—EÍzN ßp½0öpȹ9ÎÃA·2Üwÿ;[9³|Km ‚õqÎ IDAT2þ˜»£²LœA¤ÝçäHá{¶Ξ(†9³€Ñçó6"Ë1êøŽ$fc <—*àU±Ìd*ùá(³‡Ï%_KÌ ±ä ÷מe¹Ë@iŽ•Ùüâyü§^ÿ¯{éÚ§{ì©¿òÒÙõŸ [nN,Ú­ß½¹¹ …û #Ç8”c¡[ÎêTÈ»©Ì@PñŽr8–°elZJä× øbÝ=‡©Š¹'#–º  Ñlh3扈¡Vß;Y]wáª(•̰yC¨™ç ÙÁúÝ'G÷ÓÑáÏ`Àˆ êØÞuÀ)ÏsL Óº¡ì8_­Øv¶†%h6þ;ΰ”Í™—‰Œ.iȱ|.ŸdÝßx ¤G4NÈ)wCÿ¾M«ûCï£,³0+5ü~»gæÉ½ g·ÂyRF4]` J#;Yc³V9åN²¨Œ.f¯kú\”çýÁïm庨֖464®ƒfÐhýv2´Reà*ÝjˆJÔ ŸÍþ:Ì ùs!u?1‡ü‹ÃZ8êÂgGI¦»€gñí~öôí!‡s ÚjÖÇÍ &“Åb£š_=ýY¶óü×Úû?|Yû]Ÿr¹ÛhJU“Ýv×äõkJc~ÜèЪÝì:ËjÆa«ôk,ÐEÍ.8HÁÇ>3\ˆ3KÁÃï<'âÛ¼cÇŽf` Ø–qÇÝg~p[”»Såœÿ‚vÊôxÈ¥’VåœÛ =Ÿ Å4ðyT45Ï­àþ0Â._ªãƒlPž×Q ìÈ©Â-®1'h䣺Z*+dd†€çA² =Eg4½sC×Ëš% QòH| Ï¡ñq`#ªõ¿ïRcd¤âðkΫ ‰Á£¸f9ÀÔ¦ü?–LT‘̲,d[0û†œ÷2z [Q389ô80bM¬êàá9ßTÔ|‰b­œ©bö‡323ê&Á¹7£s¥ ~+8ПíÙ³§“±u3Þ}?à 0àë‚]ê8ˆ 9vÎóó¬ œuáy-?O;vìèŠeY­¤ˆÌâ0;¢r¢ð°áaÜ]ÆíšcˆIü§\ÿû~ò‘LÒÏfEš#8¨ë÷ óšUIÂ2Û(„‡öƒQR²¯]fÄñB‹n:A·EÑÛÈûlxM‚mmßG&k¹ éíÁÁmu±L ÐV’Eq­kí;³Lüýä…Íj1UKr8Té^3º—ޤ©#›ýQ8g˜ îlïXêÚw ¬ô¶ÓÓqIÑe üÔ©6iW…pÑÁ~†÷ÊîlÚΪ™ž–S”Vƒûi‘zßÂqOÚ0óŽ ƒGøýUc¤D ld˜Jmä4´AMÛU:F•@JžÞ#cðú_–&§ãmfyœ’†ó‚ ­Z`yØ’gx€]ÿ‘r€F~NÛkÑlÜ0ݘ¥û>%°”7Îg{­[Áar„9,:늼.»s§åšƒ³”„»ÿjFyÀý?4óm›|y8)Ú_{0iD³-àì–SSç—¢3•ØAÍ¥ÏM³Ci-Í[H×/,Æp@Ÿ Ïð ÷$p!‡ d4èÏá¨ÜáÆ‚‹E–~q® KÞpŒf*økî¨Âÿ=Ò˜sW^um¸ûƨüz4aÍ€ðÏU l¼.K@èqëD_ðWÒ–_q¡£AÙ˜@0(À©yvuà¡:Ñ‹ÅB:»©ã¬:Ósë‚8d×þ©×ÿ¾ƒ ØÏp¼××5J X›`÷܋Ж^µdñCBGîr óê$i<“BÇ0°B4üÏR06gèŒĹä×g*>Âûsó„ì·Ã±! Z¥ý ó8Ú2LÜV:õíBŒ,ä·sÙ;|³tv$³UÆ!#|d·=Çaç]:»‘K³4jýw9shíœÀ! ¤iÁéÍ¢Éι`QìÒ8 ne Ô„daàßÙ™& $Y+”ç)FÊ#<‚D.•¡ç9FÛÜz;ÚûU ¢ÌK4§`;é0”ÈiÏz9]7G&L$8•_Lf¤fÝl\!²52‚áøæhÔáל¤Ô0GHyn”iÇÏN~ï M©ª—EóNÓm­€eù´N×`ÇÒú¹‚L³A“E¶Ï¥ X»¸$®ªgÙºQäó­ÛÊÁÚ\ʆöÕf13he§½gÍŬÿ¾¨R–à@çh·´'üY†¯kp%iê"Ã|£l.ÂYŠƒR4,F™ÍQ¡“s5»ÂÉö,ac0±·ó4\h«™ˆ:îS& ª˜gù”buxŸXæ§ÎéHšÄflxÛ˜R²§RJ›ka`ÁùÆÆFg¸À5£B]ɯxŒƒ^y¾…¯îhq!Å6à|íáìQ¸ªòkV¯ÿX¸©u¢ ;6ÃÀ9¶ÿ?ÖÿþcÜyýGÒ¦ÎÁÀ~çô¦€eä ;Ôé(PæE¡}CY³GŠåᢙlµ‘¡ Öôú:ØWÜMUÇÅÖç•¶©d§f$1TjÿÑ9ŽïG™X´Jf E8D*®L?F낯y–.ò½— ·‘|λÿÝvæ¤ÍJ•sB¡°¶¤33:ajÐLÈåÕ:y‘[4lR4tNÃù$¶ÀöJ¹Ô!CB¹ÖÝç} öÐ37ž5 ß«1t¶½Žœí:na¢€ì fÁä³³RK²jÖjæ^ÚIGÇ…gØ¿ªe8›VÑù¶”µT”Îüð3þ” à‚@*$Ô NpµBªÞbʲD&Ì]çµuX×nwDqEá9X-šøŸëgÖ¶A˜ŸÂr%HÀD‰35è…rœ™P?¼m%`Ü‘ÆBm4¼ÎÌ v|}[Q2Å,ç\ÜqW~«5;³6Èúp·;Ü,sPÛ®,ví¬šQ@Ž­–¹PD¶ çGʱm5Ïz(&kä–äïÁì Ÿ'wJs+iß_—’¡K˜¿çšø:`Éž2P`+h?¦¾-È$á1D»k¼Ž}%â?ƒ5K¥×¿úýú_þëßëßþY¯ÿ}gÚKgš‹u¨x^Kârt#I[%¶FêÒ1Ø×5³)ÈVPþ ¹ÿ)VæAð©?4;$ëbI‚m5Êßê ')æ¨[”Ì1è3í€èì×Åb1ÍõÀ1Ìp &t°wÆ6æÀº•™fNI¯á‘ýµ&[²,";nd3’I¹¬õ÷¾Of10¸²ÝÃ’(ìK Á©-¨ä`+ii‰ŒJ©SÐúZÖ€̓ԳDy[X‡5v3•;[H[]LLC ò/Ì7jM>rSë­´Ñ.›ÂQ;¶‹7‡üY¡¹f¢ð¶{èÜ  X`7š±Œ] çÀш]mkh=ÃÄŒÊb±˜Ž Ì­¥œÛÜ»ñì!6BhmrÈjhr­??/”ç“Z“Ìen«mL¡Æ¶ÜQ‚ù¢•uv´ßž¬¹'w:W%ä<ÍYfšg kÙ×va‚ðÀ£$‰ÿQõ|³Ä"å4ª3Î4>;8: l3¬ì”çôÏ\Hb¡ÊûÇóüù#04Çr ¸ÁšÑ¾3㎿꼳¼ˆ3ix©¹bÔC·ßîìwãUb°pFÙcD½7³+Ê€Ù <ªùYV†ßç¢mÈùø*=f¯Ø\Í `àñÀõupýïÝúßw¦}´þW†‹Eœ¥J¦íù±ðf¦Âˆ…éXTxøfÎÊA£|p³QÕv™E™ËÃ̇ •˜Ï²Af±@ÊLБA¾O›i(1ȯgdÇÖÿ·Aáµ<„ÝïlPà³2\»â:ËBoÏï;'Q“CÜÓçÂPG,R-Æ>†‡6Sv¡œÊéÌ×0 ˇý7R>@ ©œ1mÅì³Ê.Ú¥fÈÐðüSøü¬‹ld8ËAûPj‰3R>ð˜C†ÎÊp¶²ÍÆß›Ø‚~ýwf–‚±Ãèæsmš ï™RTq Õ –§Ô¡Ìl$‹&-Ç+sŽ ì>¯ŠÏ¼—f›oY^ª¼wå 래o ¹…5[† Ø‚(æ“ ƒRáïyLUÑMÒüæX2Ír`ÿÉ‚°ØáBŠ JuÒYƒï–Î\`s·[u‹XÆÄE4wŠq»G2õÞì 5ê¶2Cd(/Î7$žaA°‚ïÏ€‡í­•£ÖäsŸ¥,‹%S#_ Þbw–\,—Ë ßR†sç¿Çr*õÐWîd6°±qõ#›µU8+V^7njÀןÿZ¢«ù<Çh•tkn¾ Cn®ÿñúßwd3ë‹Íµ“H§ ¤XÁIÍ¢½µ‘)ÏŰT uß8Ä39•f“‚M5<†3Éðº‰ÁÌP!ùVpwÃm“ø`"B€ÀVØÐ¡5f™ µ¸c€$Ÿ«B&(°M6gíîcƒëº¨ù@‘¡6w/T÷âWM n¦Œ¢æ¤<ƒã–Î\`Y›Šâ:€5“L~fÊÉrÊ Të%i6eÑ(+èp>p&h¦!Ã2µŽùªÄ8ÁÖÕ²=àjÇ·ÆÙ¤,Jà T%ÐD ‚=– †k¤€ Ø¡9Èðëá«…2ñ%‘ßC7€¹ÙV W^gñ8`ó}BDP€©Šß’P64 Aç°þ—KAÆ&…õcm~ÍBÞÏt_[5ÒjcÍŠ~ø@Я¿;–.iý·¦"6m䬩ɶ¤Ü¹”3váêÆÌ.â\³¯>‡®ÜÑepX¦â|É2FÔðüÈQk4„ÍßhNHÉ׸¸TŒî‹Ú~´_fËh,‚‘IPVÈ ô1¸Q36JkŽò3äXR6Çr)ãžAP‚Û? †E6Ck–o©ý`æ…Ùe†×,žwžÉa›8%-ã™v!T¬ÜÁõoCFõ‡¿·¦™õå!á›ædv±ˆfÈ&w9´ŽN Ú±rs—a¥$ƒïÔ|^73C #5µ5û‘ppN‚ ž_êX.e| Âû”lVÃ"Àl:íd›í4 V1ÌŒ#¥Ìtæ$ol~2Ç„(™[䞌€vÔ€ “³8GÓœT.Ñ<‹P€ƒ¤Y¤.\v†³IþÞ'H±²8<Â>”ŒfYº‚þc]g`J‹aQ2ˆûÂ,/¤•Ýq €§9ÒU0‡÷5·ÂÞ›õL”œÉ@c°òt>ÙÝŒ%ˆ£ç<-[çÿ°›_¿-6G¡ªåÀ™@ÝìtÈL\Æ{×:&ÜÚÊê`å6ûß'(‚!duà4£Ü—RêBŠ“båÔóŸ˜úR{Ñç‚0,çCØ¢—íUçÉ -e³‹ÏÜ|M¾m,3b©×¨‹¬BUîê²uÚçÁÃf˜m6 eÖF`<˜=*Œ¹F6C™40ÓÁÒ7RÍëŒ >‡ÊÅŒ swÌ(©ÀÚÑC^g8ëŲ ÔùQ¡þ…v×,¯BfDíèJ‡ç›s‹¶…ÊP~‡çäàúßzýïëýuï׿6£¨ÐÍ–8Oz®O$%KvËô)c'‘tÙÀBçq(•÷ßâþ…uç6ܱ> L‡­&’¤¾âÚW=—¸(ÉyJ!@äs2·ˆ®Å²ÄŽj·þ×ç£cÉè>©ÖÊ ÒqHƒ™.SJrÚIO¨·:–~rR¹(Ì‚…t+˜ËÄät)1Ê™­1*Âé­9¯Ù˜b€^Ÿb;g|Xò5›dÝ,~7°5FVÔF +fŸj –õpØàn‡r3q€ :V·ŸŽÊC½T{ d07Z À8™Q„ëJ€NQ’)·ÔÀ*À)š[YÉ—KpYËètC,©Û³™Mœ¹Yç ‘õ5‚ÂI Ç¡”æç iõüK­A—’Ù²,»ÙßÅ"²Õpï/ä· Üáj` °ç¬wWQ.¢ x.8å>Wˆø€:Jtx„ xYJÆE'Ê‘Fœ[u„¨n:ÏûpŽË•¸Ð>ˆDa:_ü€T@GÉ“¼ ÅãÍvÙxÜYºÅa¶£æ9Ìêáb€Ù€‘¤%zfÖ@Èœé½üàåb÷—Ù-H«RÛy_FÝX%wS¹7Ê%j´Ÿ ¼®ÿùõ¿?äÆóëlRѽ@Fg/9ÙN')ˆ@GbÈ(…˜²]¶Á{¡å´ )þ %eS¡’§ b–Ø*œesUŸÄ3b4ƒÔößâŒ6³M â=¶; Kòà†Ác— 4ÃÆH{ݵöÞ<ëÕúŸ{¾·r‹,M`PJœ­!EP¢€ÈÊ b*`™qÁBgW×¢Õ€ik»Z7Ó‚ìÅèØr`kŸuU»y”]1{…€ÁŒr Ò>«Òvšo¸/áX1; Ò4œÿÂã¶¿Övä‘ … çd†ÎèøgT$c!ÉÅ Q£œHµ#W­NN@E¾²ËÙbÏuæù‹óbx®„úÔ¼_ÈPùÿª£‰¿¯æ=F]˹œQö…bé˜S)µ¿,kÄó«ö]àØ}lÄX(³ˆÑµ;ʃÂó>bñ°KÜÁõ¯×ÿ¾7™xN/›NvÙ4´ÅÈÛY8<Y!ësnŒÀŽúüà:ÇlŒ? y{˜õ32 y žùQ38]–:¹.™`r2F8à…˜)dšÅŽv RCfmÔÚµvÒ=ÌÑYÛÈ´°ÙA÷讆¦È( † ç‹)‚*¹ %¡1O ¨3“X¾H,LÇâ0Ð&;ðÀ Ñï3 %šÆ]a3šÑ1;ymÒ€9ACðeó®ms’Ne2·þGàª,K„ovÕ‰‚F±@KëvÖÍÈ hr-ãA÷&óÊidZcˆj·þ8¸¡‚bmŒt ~Ûê† 2¸smµŒ2-oÀŒæWšÜ0x?oÎle‰i±Þì@Í/!Ë…F|^CóE|à™tm³2Õ°êž9u2;%‰ë:£98d¾JÿY{Û C78w“LÔ¬ òÜP_$p–³èìæsB,FæèBSÞepÖ+“MÚýµð˜†MÙ@Î`/½n<ÐAºá±;’êVóÜK\¸ŒÃøD:7ü¬ìx¹¨å›9˶óá?ß³gO’Èò4îŒábRƒà Œ8oˆ]¹¸­ò‰ð󸸜›Q?ËT>wâÕ9¹²àõ€E¾2hàOœÌB°ìN #™›¹¡æÁêse„ïÍrªZ«íÙ³§“ª®*wrì2ë„?ìJb¥ œ(3ˆƒë_¯ÿýñ•ƒ3ÑJZѯÿ²8ÁFТè ;À%Å>ÉÈ=ŽF®tFs<­>‹hÅFöL ’²Á†™§B(á‚7»Åáõ±þ~ÆûùÀlY7Ü~¾FÑPeÔ,ê˜8þjîqdi?ºWÍ'~vÖÿh&"tæyn„$nÉr›¥†ø:Ìg  ¸ dtdÐR²´Haþ(NÓó×£Úg†FÏÿPßåA½Pµ c[X•õµäµ`™d³î>Ù1Sæ"W-5¦÷ׂÁñ~çHÁ¨%ud’ß“s Íãe²÷ÇѶLYAÛqç~+ݱ²íŢË|ž)ÀÔwž˜Ëµñ®©šWæÌf s`»”$gn°š»ìøv9cæcôpSlÁè¦82 P‹q˜YR³ ˜)SŽoªKƒ€‹c,ç:”ÈŠáõÇÇÃF©ëèô†`Á­þ¾Bp& o¾#`ˆL:>*‡ŸíÏ}ÿyM²´îàúOÃÂò‡ýr¨_ÿʽ+ʺì‹A¡Á ÁL„*dq¡˜W Þ8ÓÃÒ56Ár™,tž#6©eø 3D’<Ê’¹)ð²†â£„ø{áüKvy¿É,b.‡G]ƒŠ±÷lØ?¾ç)iÈ 0S×ð(ˆçóæÖÿ¸($pÐM_eªX59ûÂÃõ*ׯ׃-±Ap"͸fÁÞw•z"ó³-0‘•4štÛ!:Ö!¸PŽruÁ¹ òÂßñ¡œ ó†€!êæ„ªu³LøÙ Øp¾Ê 9/ÉÿXÖ(Û›a<Õ €óšüúãœ(•#Ø5*²ÈÌ©[a¯^çMKWLÏÿŒ LlÄ•!©ë™·ín!±“ÁA“®åÄ<€™h…g¼Î‰JëŒÞY5·üþ\PÆkÛàKåÉ(h–¡ 3»pñE6 {ãδ¿?º†± ‡sNX“­\ Ü¹Îd1¨w§µá‘Ì® SÃR!.º™9Q…šb9ÑÜvbÑζ­Xô+ ^(Ràk”­ƒ&Îb°%4¾Ïíàµ6*”ýóV&|Lð¼)Ð9ìÔSA¢d]ŠácÖ ‘ÿÞÆÆF73£¤,,µCÚBþ†ëƒëïÖÿÃõë¿×?Ϥ`h§¼M{!sæuðÿ±÷¶Ñ–Vå•è³Ö>ÕÀŠ5ÑN2ÄŒäÐkÀ{‡ íNŒÆø‘‹æ‹6j?(û"…^)4"¨šDµ4`¢M¤ ~€Éè(mI¢€mF$‰JQ…¹ãŠ©³×º?ö~Ö;Ÿ¹æÚ§°*£NS FU:gïw¿ï»ÖûÌgÎgÎQÑæXÐÉx& æÊÁ¦@ˆ%ê¤s Ãë¼!4C0áøWIÀ‚\†„ƒKõÎ|½¤):Ôuìíl;ËRIÞçW…©*ÉÏ(hr´×îû0—‘ÀÉåDTì"¨a©Z6©ÁäX'Ëg0€ ÀQcmjts·4.ðƒM60 ÈN ×Us.á|–ÈruÎiB¢×¹Ö% n5᎚öoý‡¼!šÓê€NNÁ]µöØœ‚‚V}¦Ë¿§âP‰KçJ”áMngµ75Pk‘Ü q(Hò Þ‹,àñ>·ó¡,—E·Ë΀˜UTkC‹ ˜N‹F—Ú!ÂI>—»Ÿ”&5~Œ©Ž˜€6®ºçJÍíÍ`6 R²+t sFŠêHñ,Þ.ïRÝ[¶iöïã|”9d2\œ± GuÒ¹ËísøúhÀ€]D¸ Ás(“SAœªæŽ‡*þ\°ä ?’©×凵²«e !ÃÁªøºÎøñ±¼ŠgeµQÇ6:_,-Íä¨5£Š–‘1>åÊÇŽ|>#†¬•º—Xv6råã{ó¾õ¿zýˆ½uÑm£kfjý’dP&f^x˜eaŠ)JJr†f#³…Á Žz߯è)]“‚ Ò Ö·cñïÀ"óq‚3^³Âñ.J¥bdzÀñ±”.Ø[ håì”Á Ï—$føò¾¡d¡#‹m›K IDATÿ4b¦”:¡…HŠâÏçK°Ø•-d–É‹áwv{žÙ#aONY2e^ZQË2¹RKœ?"÷5dmX¶… §¶.,¾Q2ˆÌPǪax©ÕÁÄsVÌš¡Û›d»’`ŠJí³ˆ*I«6[ÀkÂ2µî}SÌ“ ©nR`“t°gY`ýg8Ÿø½¥v×&0¦yœ—Åζ¡qîÃ2 Õ×’p–Ïnø«“&,¥Îó}°*í¶ÉN'oº-$×Sn6#ê”QäÏÒåìOʤ™×D› qñ†ƒÍý½vÀ€g¸k­(•Ä ¥P*C"q¾;¿kkk!e´XФf1Ø?³Ò¦ò±¬òSCú(IÃk¡ ÖÑÃK¹±©¢™@ð<¨®¹*¶9kh”ƒú£Ž•2Àïw€„ @¹Åq‘«¬¤G Q¹¦¡Ä‘Mð^FæAåÃp±íŸM1x#s6„¹¹) 7˸oý¯^ÿâKPwB†i}:ªÀÙRw¬v÷ ¾Àrv¦(w#¶¿žL- #[¥¼ ü<Ø–†÷¶Ñû ¾g9æaþ˜Ÿvß‹¡kÖÊ+æ•Ã\ƒ’l•ÔmU·Õž5ÚqÏ’lRŽ95ݾ“Èn†÷C6NÝüæÈF…{°Y&yWø>!én¬„›#@¬¤pÁU rŠ UÉüƒù†§‚Ó[`’¶‚žTë?ÌÝÔ|༔rc †Õ lz&RîÊÝâ¯#Øêm U“Ï–Åà2ÉQS@=ÿG³sx¯Ê9¼ö¼žwó@ n‚ ŽØà,y†Oš$oÒð$óàN¥­-…×ò@U³Þ¡5!ÃljÞ”ölrÃV"†ç¿_‹ÍÀa&‡ª@5îðªA{õ À JÉ;¡)`¢\ÀTÞ‹²#æ‚$ k$P¸Àl¬#ûj•N¬ä £Ì¾nXo”>²äf`•î|±¸WæX„+[h4Vàë:šA扭­ùBI#²V̱ôñPò7dÄFÅ:ž\oÊ\€çŽFÙQÊ ã¾õ¯×ÿ~Ëáäú§½—Öb…Â=ÅÍ:JeÁ½ ܬ™X!© ån¼F ÿ-‘AÈÒû†¿w&÷£Š¦¸ ×̰©Ä3/Àä8 ©ððWóf˜— †%‡™%w˯o´þGÁÂ#‰îª Õ‘›dXô«ÀH>£A{4@©Yžåð}(­ãÁ/Ì#jò»Jó/<_BÁ—­x¯Ñ\A5b‚DìÀ;f‰ÝçD&R‰Ï‹ ÀY*¶¯V!®½B·¶ÝÙµóENp¥–(³Õë¿Í’ÜWŸ]›z§¿¦ ¦sò:«m`áV±¢«œ_­BóY7²ÞnlJ‰yhmÖǃ¨)ÀºÖCßöü7wì¬}ô[T$ó` nð8p²à“O5É-²&¥B)uÁðÀþ‡Ù.EÆgÏ&É êÙe « ,"q¶Ci—‘õ@g.”Öðp{)%8¯±E4H˜kÂò)Ì+áÙ,tF Æ‹6ÿ:‘£3Û*‡*êÈ6ªÛÎ]<”“aAÇ]nÞh¸‹ÎÌ[Q³U8ŸóU³:£Bs”A£²‰”ý5ºªAG6À—ߥbœÅ€eyÊ”{œ:¿Ì©¢Eͦ0s¡>‚\~ÝUìâ}ë߆Eß×Û­ÿºrýÇ·If¥Òx8Ô,Îå„÷!pbÝCWK® ZNs~ЋF8CÌ ¸8åÁZ#hÈ0eö±üÞ™‚N÷Ej†óJX8ð÷¨5nFF â|$i£ãâ=m´¯¨{F1 P^°–Y *z(”y,øÑ(¡É¥|­ÌKÁ"RÛ{Èz;§ÜÜèºØ…ZÈ¥X£$TF™G § l•©P×À‚™0}Aæ)éõŸÓxýc˜,o]æ1?¤ÚÝo%‚]ÌÚ—õߤ’¯»b£ºû¸Òú/uøüÏ+`ªØŽm×[>T© ôL³h¥Ë[s—¸ˆšk`̪ìfI³‹±á5™èX'‹FCÊ-MÊÒxA¼,d áó?u¬v÷ü'É蓮 RRÖÕ³»´^|¹üÇL;q©¦œ‘ÂÖÉþº^¡kË–-¡èÄn¼*ôGÖºÜåÇty/¨üµ¹¨Dù3%Jr3JèÆ¢Z¹¶q±„…"_K,R±häÁrÜ(ØÐ@Ií°Ð];ä:Ê¡Ü~xÖÃï5dfð1ƒÁVÓ£Áh•7ÄëAaxøwÅx­*^ø˜”Ó›rƒ¹ ª9fÞî[ÿãõ öØés¥ÁúÇâL °@ÙšëÅýsƒ‹3}2ØPVXÌŽ L.8Ð1“ò´BÁ«x U´#ÑÉNlX,^j3)5†s€¬ Ô5«É„Á ó9jØd"òT´9@*P!;Yd¬­• ‰ÚWFl}…šg„xa™g9‚Öi˜%c耆ò7tQ«†F!Q²Ås¦V£‘L¤÷¢P Kš¹Y_’'ÂìÝÊ:{íç–жº p%¹\“ÚÕ­¦ÁƺIç0S™;Êôñ¹þLȆ CÐŽÀ§µÍæ¼DÙ_8 NE€92ž’Ã%ÀoF.ŒY[¿O’³‰âk>2‹ë¡”ÖlJˬ¶a䯸½µk¶Ð©&ckÛvJ0ä_¬†½£DŽp©ý|T /Ò„ˆ].ç3D¾÷ejh±tn–óæ› b w¼Ør˜å%ªƒ?*¤eÉY!ØÝ_•eÀ¯¹ªhGv†±|Qa$wüÕ?˜Ô<Ë…Ôœ >dy>„åqþÖ××;YšÊCa°Ãçg{ðZó|ƒ9•ƒ×]¾XŽ…Ì Ë®8Ãg6›50„l‚Fµ™ò|«ÊÑà`VÅnŒ¤Wþyp€Ÿçtð^ãB¯å(°—ׂ\¼™qRÒÁûÖÿêõ ~•R÷aýà ϒµètí ‘k×KØ@·n¡ “‚ØÄòŒ0Û"t'iv0Ñ¥n”qº§MëžåçBÉêž¶ànª™fÙèó#°T{z·¯6¦àšÆûŽdSQMëA4Räy¸ŽrìVeÉaïç7Âü‚¥Ž ™͂t,õ @`X(+­²ƒ¤ ʱÉê̺ì!”ÒM͘mYfø„ý#õr¬nN@ gÌt3*|^9Ï'EÖ"Êc#KUJ‰à´ZÇ<±IÚx¶g‘£Õ[£´.8ú¡QÄ`ý‡×¬òjxKôÜÍÓïò™PªdgÞ™ûõÏlÏá1²¼6~/ͺç³=•Ÿ›e’²åÜiá¦yŸ †n|àÏœ—{ir=YT;§.š!羮ɢAc¶˜!J›• âlƒÑ 3 ª€>éaÇÛ¼ë­ö¥WÛ­7\k_ÚyµýÁ»Þj'=ìø0ЮºË\„3•© R—á`‡›$ìþÅS>LŸ…»ÒÜeSò"¦s‘m *³Î ïqVDIì°¸öî;vݹ˜dÖ¸¨Ùf"¸hV³4#©ÆÚÚZ7à •ƒ™;*D”³wø3ãuU³J®Ôµ1€¼žðûý³q^¯-ÎßQò0.^üó£a¯¡‘5»;ÎÈõÏÆ|­Æõ¿¿¿æ¥Øl–{I‚\ÿ¥1ÌæÄ{žRÉmr<Ëú ª}¢!C sE RÀÇÈ `TÚƒP¾† ­Ý‡œ=´,82€¦Šì 2-f}ªÿŒßÿ$¬ƒ®ðFë_2{F†•›/$ÕÛˆ½QMYtf„x݆÷ˆÝÊ8ã&˜Ð`|øwÈýÁŽ;³Y iY— ä.:qGÞR EyšÊrêL8,¥ZçØætÈ"šk2¶xÜ÷,EÆÝñ’¥ö}Ⱥ1˜ €°aeªÂÁ’€3`èTÇ@{ÍY\ÿ ¦eNв¸Jɽ]YXo{íŸy‚]#ÐEÛ÷•ëдH´oá¬PT…kK0KÈSXugR³H!ÿ'-g| )ƒ©©Yæ1r@T`Å}+»gð A˜ï¡:GX ©BÈoÌs_q†½ó‚ív×î=ö¼³¶ÙCO~’=ïÕÛì®Ý{ì];η׾ò%]w•Yå4ÅE<ì*K‡5¡ºÿì¦ÆÇËs9ØùW]kd;PNÅ] foªÈñ×pIZB#øa–bÄ(áÏd¨\#žéà{>y&…Ï>tئ[ɲügý½ùüp…¶åU¸I©ÎžCœ…bF†ådʈ‚™6×@»j”‘ÍÅ€<ºÍ)É ³m Ô¼ÏͨõÏÝ66t` °¬ÿýý5k÷/žÿ9¸.-€#Á‡k+¹Sf!—«8ÈÚu”ã +fÿð|Mµ°Z,Q›`Ã& 8h^ßôª±„ 2Qx¼YX‰‡×¯½§ÉVü¾f;õÁÜâ*÷©Un“þúAR9D#i¦j8˜,ÞB|‡ú& vÁ˜y†äy¦&‘ *k[z·1´£F6%d½°4XOãß ²ÈPà¼Køs¸G¢T‹ ieq^iÆ&¸†Z”¤1ÐD©`° ‡ÏPJióLmw%@’âë#KÔòœj4³P†x.}¦«c‰ Ý¥7Q@ j|mwæS¬¥R¢HÐAÙSì.7ÊÊRÍK¶íž˜§~_¹þÛµÉí÷Ž­ÍÙòl6I¡ÛÞšÂ\¡§ F–¨L`ËJK´áÓþ1÷g+›Ü¹íÁk›E‡½r­bVo€G>ü{ê©·í¿Ý®¹îSí$Þô¥[ìó_ü²ýÌgç¿êeöñO~Ú>÷W7w7Ÿê~«NµésA6úy¥ÉßT覚ùñså–½\üs1Œ’"6/`PÊr?Nd‰pø‹A?6vùR‹–%|#»e?vdtx‰ƒ3ÕŒ3:XxªÎªbN˜!C—4.ÐUÑ«,¥G– ²lM&p!¯Œ1¸pg å|žýYp¯’:"(ÃÙ™ýYÿxߎ:«fpþ­ÖÿÉÿljöܧ?Õ¶_ô6ûÖ®»öiý˜ýu1 4IHÎii’`’aIÔ¨,mxwbJ‚seï ³8žß‡Z8ŸÀI¼Úµ@;iVp<Ë ̈3XÆlR(–cç´1T` Péý*Í%IA0‚9^ÿÁ¢Î?œsœõéØ:œ›¤B›yIc[ÿ«ŒDøöHÞ¦Ö›Úó|¦'U^ § f)hX2KulážC͘iÇR£å2ʾÔϧÚ3C¨\þ–*§ÜLôé³B8'c‰˜br-º;PK€ƒ } •ÂEÑu-É;Ûî™”î¸ ÷çqÌâµÅ \¾²åÀÒ4¦ËïÇÜ?ÿkÃÍ1~?¬ÌY7ª}¹“rºz§9Ÿî™æûÒÈ$1ûûœ¤ºQN`Ck%0WÍòôܘ0ƒïÔ1«ĵ™Ô~^{RÞî1XÇšt{ß\ÆÜñVƒæl_›R²³_òBû_þkûØõŸ–s1ÿ³OÛM7ßbg¿ø+Â7‰2`ùÖªB^åä¨"J¼èØù âg®úovÝ/ïë(kˆ,ªÙ™ ™žÑ¶¿6qj–id!Í£«Œ¸HVLZX« J”gvJI¶F² ”\æ`”…8Ÿ¾¦* –%¿?™!á]ÿyt™ókÅVê¸ÞsÃçî*w@^ ÌÑ,cÖ?ÊÜÔúÍèü[¯ÿç>ý©öƒ¸¿íØöj{À1GïÓúßo¦½ ¬Öeç?µ9¡ÆL²BNU9,u 3;P¼ã\MVs%b†…'m; kõ5L­+FÚ~äY°i¶à0—Ô øá¢…å®¶ÚÞŸÁQÆœ gu„ 0§| 6a`Ó… áFJ^å8`„”ÚYfdZÁ b1ï4IªDá|O˜O3:Ý\ 3E8C„žÌè¤A¸¨MeÓf5‚¹]¥ìd¢X¦¨fb\Úç€, –Ÿ¬Ä;5ÎÒyA—¶®`'©:Õµ¹ÊIB†…Y¯„âç³Þt™+—ìÉy'³4’v¶Y ~Å¢LÖ!¥®”•¢­µ¼-¬1hN){þXoÍ£Cœ°1f)åfƒí PœGÂðíÔ¥Zÿ¥às0…f›¿N|/eÚÐÌÊÄb#jÆi“#(KM.Ù½ ]š~ü؇Øï^ñ¡a)λ¯¸Ò~ìØ‡HŸw• ƒí¹¯<Ã>wÍUC„«˜•w¢$0\hc†ÇÀ®i–’zÈ!¡;¯2Z¸°U²:œéÀât$±à™üvñRVÊXœãŒ2 ƒ64‰àY¼¹˜ä²p|Â☠*P PAægMÔà¿ñ5VÒ/eh€çï%‘ÄÙ0¶ÛæõÅC–eŽrlü(0ºQ¦Ð÷»þùªõ?ʶڗõ¯ò·¾ŸõÞ›ßj»vï¶£¶aœ}¦Ýÿ¨#÷iýïϯLçÁ|† ¬ÿÖqD (‚y˜DßÓâÁ¸†ŒÆ/x*/„¼+\W°¢NȘ·¹„¶ØŠÍ @U¨ { IßJ°ž@_!¹ þ[¢Y¨¼Â’»1Z C¬pŒjîÉÄ\”’PQô)Æg4“42‰áâ° ä5’)Ys 5έ¸|®±.¥7Q@klEÙW&„ãZ`˜ Ž p¦ ASqÝ3µ‚L¬»Áí­N3=˜g€€»6Ü/BQÛõZÎ,á³­³;#š‡Áïa¿Î‘ÎÙŸ¥4Îh5··U†6–6t3 ×–‚^Ô5æ£T9çÃ.oÈT¶ù§Òª Q*X6Ö(gd¦©›ÃùC`ƒ*2óÌ^ã,î7™Ÿÿùƒî Õg{êÔo*8e±þ ²K)Ìùk·¼¡ZK“C©Å ´ðüÇçäA.† ÒžKà¡{U¤Îf3;ìÐCì¦/Ý"‹nÀþü—¾l‡zHÇšð¦í…%w„-™¤÷ØuKÙ£L m¥¹äî½²Zöï?åϵSžñÜP|r‘«Š85û³ÊÝ„­ˆÕì Ïn¨YvgSŒÏ‚pG]G>nž]ánþ,ÎãŒò+ø3!8áÌfÓª(œð|²7Ÿ£‘í&J¿{Ä×CUt„S³7J>ÇŒ3C#–è@¬~ ªõ¯«îÍúW@þÞ®ÿ]{î¶m¾Åî¼k·µu«íØöj;zë+×ÿh0­^ÿ=Î ²5D”oSq^¦˜#j6Ö<‹ƒŒ º½YŸÕaBžX…Œ¬Ýk.€- æqø¸ù}'ŸO R?}…ÀQB À³ƒ¤ƒZÿ)o…f6Zÿ­AF—®Ë?hnŒæØF!©«$F]®M&ÉMšŠi”1p*ó³&U;ò1#À…|ÁÁ,Kw\ø3˯µù'뙕ƒîÆúäXGt3g`$®Õò¾Èygù¶býw®—jýs¾ß†Ï£l :E'Ë…în–j-í{m…“j¿þctüenÒÀp¶µ9è™ ,6¸`Ã.RðáþÝ{¾g?yüOH—0/ºNzøñöÝ{¾×ý;¾J¤\æsÝ—Û¯<óévÄýîg·Þp­]÷ÁËÛ1þΛ^o_ýÌ'ìÖ®µÛnÜi'=ìøö:yÍ•vî+ÏhÿöȇŸ`×þþ{íÖ®µóÏz¹Ý|Ý5¶óŠË,çlïÚq¾ýåÕWÚ­7\k7_w]vÑŽ¶0Þuáùvë ×Ú›Ï=ÛþûÿÝþÙëÌÌìÖ®µW\ÖÎÃY/|ž}úÊØm7î´[o¸Ö®¹ü·íÁ?ð@i«²ax惻zy 1e&DmÖ#74%cÙ•³'#ywí;êzöPÇÆ«²[øžý ϵ08 wÉéDË+“‰Uçïw¤ G¹JÌàáŒϱ¨Ž<•Õ³¿ëYÄÑúg¹æ¾®”þ!dzYúÊ.„ø™¾½ë.{Í…5FèÂ×ü†½uk¸^Ü„9BiºÖuãúŸ˜ ®E I¹˜AQßé¶ „°ã\ê Íéu ÈåB?ŠÀ‚ëcd¼Às9óM†!£XuÙG0 ;B‘0K8ó”¸ c„Ñú¯db övÅ1SÃæ Íð¨ùÍQcE=Ì$+ãŸ08oS€d(àÁ"ºÍœ‹ÎÕ•³8ÄÕíÐÁ÷×EjÆ¢­1»ñPÏN`¡Ýö «C[p½³˜«”s¶RKÔB~$² Gæ"ØiÓ¹ëÀ}þ¬àPÞR¸æ$åCÔËáÀv×Ì€áqàì‚CMŠ—kÜõTÃVíÕ‹Ÿ©²9Íõü_¹þIò\<ÿy?/eì·k T¢ ƒ#ïYùLP\ÿ )³iþóµÖn¦¯MóªÔw î›3˜ßDéo˜Ý r8Õiçay”#ñ÷Wnÿš½à¹¿ _Ãÿþi϶¯Üþ5©»gm2°§>çt{ÿU±=wÇzò“ìÔÓN·Z«½ã‚óìÄã³=åÉö£§<Ù>ðáÚßqqÐÒþÊ3Ÿn§q¦û˜Sís_œ žtÊ£í§ùùvêi§›™ÙI;Þ®¾î“öú·¾ÃþîŽ;ìÑ'=ÂÎ;ó¥áñäÇžlÛ/~»ýè)O–3O<åQö×·Þn¯ë;ìúÿÂ~ü¡±‹·o ³lËÉ3Y ܱLN=´x R~0*«efE8ÃG=Œ‘©b°Ár9&œsÄìn"œW¤äOÊ¢œÙŒŠöFùŸš×ႃ‡ç•Ý8ß#(‹Â¢ž³|ðú²Åº;ÀášTÀX1lIò¿ýYÿŠ•äõ_EA¿/ë?¼GÐöZÍ;0ðÄuô­]wÙ¶ ßÒ€Ðÿ˫혣ŽšZìä×5ÑúGÉ,œ7£1xÌò ¬°‹¢–”¬Šß‡4îaD‚ È:58 :ž™BLÅfd²¨UȦHÌXM:Ž’ÚÍôµD Xå³³XäÁ,Yòdáʆ’:ü˜3>d%…œ²ç懃žnö¥D ÀCøÈš4©Í–L¸!É"¼"0f@G2”9páÌ¡Îq­F¦…8 ì´á ÿNVÔ.™kNjÀTVAä"qÐiœj”m"+l¤È>u÷ ½J 1ÇeˆÝ¹£ Àåh}€hêrެX0Vè"{¨d¡U1Bøü¯Ól"ßkª`‚pM`F³¤Ý>† ËŸÿ–Âû…,Ÿg€Ê2¨µI}Ër_LYÏžy(*þçMuÐò §i?Ïð,ÉàW`¬› ©¹ –bñ1º†½ñ¿k'>ì8ûéÇ?VJ>žòÄÇÙ‰'gozç»ålʈÖgëJtç:õäGÛ‹_³½=^wÉ¥¶çîïØk_ù’&ɺþÆ¿°›n¾¥Ûü?üñkíŽúf{¨üܯaÛßòv{ÿU±m;ÏÌÌ~ð÷?ó…¥ñC,B¦‡Ò‹Î9Ï^xö¹öþ«>b¿~Îk퟾õmÛzøáÝŒ’›É@?OXˆr')9”©^¸˜%u\F¹I®¬qŽE;‡Î" Z¥ÑUà#àˆçÏ%³l#–ˆåfjÆEÝ»Êö™ (üœ¡bËp—2¨e™Ü(,‘ÿ]ÍzˆõÏ÷Žº?˜=¼7ë xv‹ÍC<“Š×PÐÛ'ö^3;jëÝ5;0 &¸k6Ö?±á>D8° FiZð‡a¢ðoi…¤.€úžJ¤RC‹. æap)¢ŽÑ@à¥,ÀÀ1Ù8,4äôÀ÷"èá9– )¸ñÚ¡ÝvFûY0Á¨ò:jªŒl¯ùßÔÞ=š¥c™SkΘ©g<³3c°X³dÎ/:…qš0`¶ çØ³›fj€ T¯am 0c¶Œ˜%<_¥ ¾–¬“pñ¼‚ÉÀN§¹ÉŒýÍ5Ùp2’­¹Ajç ±²·ýûb½ÕÀÈ\À³{ÏÇø½‚ΣÂ}#Ûr.ŠG²6ža[ëÑžoú©ro6Jcçë„l²Ð–ݳNh]76…ÀÏŠrJv÷û~׿2Tà\o?®{»þX±7JIXªY«”ÒeGo=Âîܽ۶]øû滤+ÞAqÈÄú7›ÏË27¨Dy[]“U4ü£F=ìWðs˜ø]GÒ`m8°µò<ϱðÊì¦D¯“àCÉ ]³€c t„;³0Šã6šYʈX¢dh™‚h¹ñ²¯jŒ‘d}$ =ëV™+t`(SZŠƒíXô†AõY4ThEi¹=8¬î`Ƴy$3bœãò,OîbIœÒ\~-¸Æ5`;Íð„€)ruᢎ>+›ð½FlX5m‚Q‰üž I£pÚ.s(§œÚ]c`ÂÌSé[ÑyO‚r^™žUÕz¦mËs În8³…2±RK:øùÙ1n£þ: X³”£$R5lÐFdRM´}ÝMn„Ä8-g}¬Ö0#„ ´fžÅú¡²ì$gV«?ÓKc‰&3ÌP–Ùe‚£}¦@y³1Aœ›ÂŸêôÿÌgÛ_õRÛþ–·ÛÇÿl‘ô¬_y§9Ä÷ápOzæŽ7J¨Z·"gûË/|ÉÌÌNÿ…gØû®ü#IÅnôyýuO=åÑfföÔÓ_df$}J†ÀÔr“>íiO±Ã=Ä.ú÷Úeú°åœí¬>¯×à¬MGà€g,ðÚ(Ù–*"••ò(HÓ_}}½š*ð……3›"༊rµc{pÎüÉâø>D–€ €žQ쯧f v'ó‚ØÏ˨#Ë×(þY]?e¹­y^†>eJ0ÊíÙßõ¯˜)5PÊ6ä÷vý«0_9?…¯÷Ä-ÐŽ‹ìÎÝ{d³#ì=û €V¯l¤JÈÐ8ë&ÌÊ ¤n * ¢›œ ˜¸ AÕ Ïw ]>&W»DóI œeï˜vÖÍU2N®›¯áÎe­áuqïÁDôàì…À ™•ë_0ëØ#µ¦`9z*õĪ÷Üh6(°-jÞ"{‘LZ:³“[Î9°mÎCZ»ù†>Ì3XC×Èî¸S[¯0hˆÂJR"ëå¢0“„<ÌDP»¥ÎÖ»¦I†Çi”R†ÆY@À¦©³j6±y8U ;/-5:غ{ůý|:¿ík¥†y 0ŸäŸ;xÎÔÔÎiås§”¹å<뤋C]iˈa¤)e+óù²aSc>Ð’áÁ÷Iû°þ;eHçÂX€ÍA67ИþârEæó?¼þ¹Q-±{ò gà`¾š;£õЃ 5p¯:ÛX,Íf³€Î»èíö±ë?Õ'Po0\¼ÊEƒ7ü¯ã;âðûµjmmÍþê–¿±s_q†½ÿª´×¼ò]oµg¾ðe]VŽ’¥ù óÿÝs™™½kÇùö··}ÍžòÄŸ ?£f-pƒ¯µÚwþùÿ53³g=å?†ß»Šx@ Uò9,R‘e`•šUQA>6ŸµÀa~çBžÏ;N”)«f|NfÄŒðû"C¡œÜ@p‰ cfÕPs˜¬ƒ&ŸÞ(ç¥+Ür&{6k™E‘*dG?óâýðÙ€ò·Ø¢yf5€o¸¹y‚"*Iýì¹]vpŒ;ØAkë•lˆe3?ó„ÇÙù¯z™wÑÛíO?uƒt)³ÑC{´Å²,Ð.ûЇmÏÝß±¯~ævý‡ó<Ï|áËì¯nù»õ†kÛÿ»vï ´ñHS€3·_`_ÿÆöÄ“e¿øóOµKÞsy,ÚA“Û¹®-_òu—\j~ÓìÇ}ˆõÂçÙçþêfûæwvŸÏÏ)JÍðÏÊîÙìT4á,EsÙÌÉ[,»â®v÷qÓà1KŒ° æ÷_àà)ËåÀPAuínG¨-[¶tE ^K¿[¶l ÷ÎÜpƒ–ìf—üÚ`#€×›çd)g½}YÿxÏðœÎ¿ÆúGÓ fµ¨e<ýþG®¶üæ·ìœ ~Ëî¼kw7;†YbÜUÜ&¨[ÿ`Ñ:Ÿ#;X¥.Ý‹øÂsBmî¢EPôÍ1iׇsrH Ì„ì‚×2Î!è"榛M¡n£‘ xÈñ‘…´Åã¹`”KŬ¤J³Š×Ü—õïŸ×@7'²‘¶›I㘚f£YU诒rŽÌo˜õ`éS°¾6ØªÉæE¾·>Û§ç0ƒÂ¯œrp¦Ccþþ=„“Z”N¡ 8,4S‹¦V-„š"CÀ®9³¥~nÉÏ©›I4¦Ëzw:¾É³m]„Â,‚R?w*qz¾N3hîŒд…”%3TK¶‹ŒóA !÷g²K—ò¸xNë>­Q3³2ŸG Ÿ¡Y8Ì7š@LmÆøÙfíØÒÒtg²ÞF£4ö,¥o# _>Gpfò AµV[_.ª›ËÅÝc~þ9ö'Ÿ¼¡Ë! W²ÖUnd£7Ö9û €J…çbŽYUÔ`Nœ>?d:nª;ïéö£:bá8еÍñùe&A±,§SׂóY°Xç|—Û©€SÅ|à{¡˜³ÆMC\Ïø<£ ŽMZNPX5 TŒÒÄ„MÖØ“ì®¶ÏÊæµZ BÍ9…øÌ.KT·á³eS„¥®AG›‹¤.õ{Ù)73[§•6#±Ì›QÎRªÆÂ±ÈTó\Xò€7ÚûzŽê*x!ÈÒ6Xÿk›lýï/Ji¡Ñ¯ÿ" û!à”ÀP°|†ö^Çðˆ\ ãQ,ÎU’Íb†QÇÆp¡NvÔø54kl2žüxq Lë=à'bÃZî!<7áç ¡l®2µ±‹âÊPÈA“E“¢ˆSvñÌ*ÉæD2ÍÖ,åQí9BöÔ–ò%/à«Õnv¦+¸ÓôN90 qÂL™åÏ„Ï]{)k²ØÁáÿ¬6榔,fY¼%™Y8N’Ìù±cV²Qô,¨L 8•LXµ ÊÌ\†øÜ±(éCÖˆ-Äý}ýë±qg¶º<)0—Páµ Xƒ¤0ùyÙaî\5gZ- ³M$ñcÆ)Øyt€>÷óDpü ´øúŸ¯^ÿhÞ@µæþW‰YƒŸŸBRã>šsnà!!û¨v€äÌÉ$sã¦J—ÆëAW©‘¡ªµûz´úŽ3JzÌ‘gƒ2ÌEg» •K)² ï—sZΠ*Yaj9u~8ó¥$÷nnüøÌѦA(3Q2"–)áßçÐå÷;º«2JÐ6Ø7ÖµµµPø9Àƒåüðàãã v\ndSª¤oJšÃ:lîr3k ˜üvrcY˜*îÙ© ˜JÇ™ õ?¾7_Slj°Ÿ¥…lͬrmc35ÈŽ¬‰’o!+ÁöÕ @1Ó‡×sn˜1T’I¾™±AÐÀ–Ô¼SÃó^좦¢¹þË&^ÿboõ‡Ýl–áJM“Žîp! ;}Nˆ»ö<7G™WÔ>ƒÉ•r’MïàXbÁ–ô|LèÖ÷sz h‘}vçÚ £–¬ _dž‹?kÑ ‚ÁbR*œÀ€ØÀžAý*'L™å#d¼jÿÍU´½;§zŠò.–)µ ~ ^$3»ÀÌ’[.‡çÞ,/Šð4½F™—¡iD4UDŠ290øýÈ8!¸‹ ™Î3Küjd¹0Ü]l»Ü`±Ðç›P×]'aûÝŠòåwè7Zÿá½!0·“K³¡œ×v¯ðœ®-,´Ñô"0SY17=Y€Ú€Ðâ{Êô9»¦ÀÔx2[Ìäp€ÑšìþÞD©š [[~=µÆTäÝ %yµÚxýCÎÑ‚*]¥rT›G]Ûx™åé=2³Zp’›äq@íCî~ÙíÄΛm‚°Téíïºxê¶n¡åy)¶†ÌD­–Yb 6óQÁÏE,3Ê) _Ûݱ”ÅòÞ½{»bQÙ_{  =B¶BéŒW-z'˜%1<·Ág—¤paµÊ‘J Œ—Î*1I÷•‘ÀÅ熰s•×üýêa3ú£álÎèQŒÏ3¨ù50Îl‡êªðLÎÉá¥rrCà2 ‘e£Ž…MÔŸÀó®?¿wûùÜ)Ëj̉bY¿Öh ƒÜm„$œÏÚkå` óóŒNxÒ£[ü|·>¼ƒJBöîõØAV˜^37~œ3܃Cd‘N þèûý|g›rý³LŒ*üuÊ€Uì¨bvÔÏBST~¦ÛÏ’@¥P‡¢WH–xî£c?–àÅç{:`µÂ‘åYݺö©„ÝßbUg=ø¨”ÿCÎu¨¢a¹µ±íµlˆˆ°Ù¨F\\‹RËÄ8!Ó„r6²Í„Œ-ºcRNvX¨;pR6éàV‡†Ý\K 4Ð:›_/JæóÐèc ú,aæf2I˜-~>§h’ú›º…•Ö¿ë(|À|gô•©”ÉœBþ[ßÌëÔÊÝ\5àÞ›M4y_Vë?fÓ©š†gÕsŽï3|þ/÷´Š{äfAܹgYÜáUnf\ôpñÆ.²8¤Ã2ý5½{Ž31É7ÊvÙÞ<^´dS£¼f‚AQÝ|[¡gÉŽ åΠʦXv¤®³+ü™¸øTNsx½Gò<%‘ÃŒ9êuPJÈÒ:ž Q¡˜ŠQSvä Ê`†ßTž£ò|“ +fFaqmvæ\üýÔ=ÄÆ*kç¾õ?žµÛ?&(5c„R°X.A ¨ˆGCŠß‡?[Ñ ©›V¶Šn³6V’`—b1GÖÙ.WC+mgX”,G0ÂLä´{^g a¬´Îí"YJV‡]ó’Ź(8À ]|&¥š_ àölõõÑŸ72Ø©"«©s"dМ¬)éf†²¬®x«$UšÜÌBxf"f£D†¥± 9õ®m¯9ÑñÜ£¡ízí;õ ¯k ­ö̉‡©àS­c\‚ü ¬¬{úµ ÁÏÀPmÃküÙ ‘ƒ€Óã“¢.§,M!] ŸÂcQ溬šÚ”Dn’ãÕÀü”2ï¾wzŽâ³Î;ÎÍ,Ó‚…t)NÏÓ’jv96«ºµÚ+ʼLÒcéÃ'¥4µŒÁa¬IÇÈål¶Q*Wj‘æ•6Ëqf¦³&9~Ýg_ÐÕ Id5:Æñ¬H›¡J"뇙«ÃkY²ÜÔx&(Ef+ÎRÌC"÷¶fS]üfºðú¡ü/Áµ&ög¡ºc/8ë2ÍÄÔe•}. iŽgb…8Ÿ¹Z†:&ƒ™BSع°n±…O«c00ÎÊÙ œßZVPxÎLsJ¡©uy- =’Œ*êæÔ|ïlìe?5=’ê±Ñ:Z*ó•¾ï望ìxSÈáØåKYÆ*·­UE’ 9T6Ï®ÏuÃTGœgEÔ¬KÙ”z$9ò"—‹YÅœñ¹áù–Þ0ÂcU&%}b™ŽbÚÌ“º¾VÃ5÷™$ƒVÌ ŠáàæšºÙ# EÛL+¨ã÷N.qÎeÈ€3bÍÝXf}Ù\jkæ`Aì6z((Ùˆ’²(ª .JGC‡Ê–Ú‹$7 Èu•œ äU1Ç/(»A6õ˜,•R™3þs(ûY³Ì ³UT¸+Ïx0èI%Ô9I©”ìI=ìÕ9QVØJÂÆ²B,,MãÙ T0d7?ÅȰµ7w£ÐÑLIìXÊ‚LØHcÌ, çáñà¹òã¸oý×ÿþ3í¹¹Ã©{}¾>±È3§(‡&HÄFÆ"`Ýdd Tßpü¦„£5ËÞpN§Ò¬ÛjfŽÔø "ÇLVØÜ|áëZ[^DnjÙWo#&ŽP#û0“9xfð^<’»¤Ã#p®Ø¾÷G{{7ób½u3ËÆZa›è|¤hÜ$cÄô¶Þ[eËxÑØ†û© ÇïW®É¢Ýs7›R#à Æô=!ËÁ Ü03Õ>ƒ¸ÙÛþWLÍäàëáùèö1 ³­.Ãê,ÏxMwpíú³ì-õ&(±ëγ˜9 @Qìfît‚ží… %5Ö%²oø†ž·ƒæS(©¯|Mh%¸lq=Éñ¬Í¡|z)ñ{ûõ€nrÚ°éF ÞÓûOLP‚@n7N(-§n‘Y×ï/….M1kަ¸Wm &ˆeMªó‚™í’GÝô‘Å-Ì:08[<©º^ŠýPÅǪÁg5›á ŠXxãqÎf³æî¦œá°ÐÆ"Ÿ¾F]?Uxó±+“d PJäׯ¯2?PÝF,êñ¸Fó TÜ­ ¯9ÊQrˆçžgBXbÈnoÈ1ðÁóÂr,¾Ÿ‘­ÉYJ©¤b H¨{³¦”3!Ëá˜ÝãÌ”î[ÿzýˆýuJïFb#5†hҎσÒ5à¡W‘9Áb†Àdè‰"ͳ$`lº¹£Ð…®“HÖ°|)#{l¼-&’‡&ߧؘe`%=’=†7>4<Œ ŠÁ"¿ïè9й¶þE7UH*K•G2ÎUs?#§Ã‘[›$ È 3Ý—fJdámdÆP¸K‘ñA&‰õ‹Á‘LÈØ}Y$ƒŒ.M󅺋²üÐ@‚G:Ÿ¡á¹">ÇíßKÏd…¹¡Ì&%РɆԛðв–²54Ì.seƒõ?Ë“ì‘L|éšeË<CðXœe/&ãlM±<›àÑ^Ç&p]S'·C¶¦íÇ5 Dn"ÆQÆgÆŠåZëß³¦èóD…qmÑMÎÓßK‰‹'çi&;çÜäp à,÷ËYJMêàÍ‚°KÍ…É(ø‘‹EÅŒ†žÙAJnÂÖ[òâÏqŽ‹_.¸<’pך¥6ÜéÙ.£ ³>|¾WI”F’'ìŒ+7,žP©,B6 ¬‘½5wOÙjß‹ÝÇøã”¼álÎÈMž èb5‡kÁ™>(ÉÂsɼ„£á}™F¿üy4=àëºÞpoºÄ ϵúe÷­ÿ~ýÈ_ë©KÝúOÍ^8ËfŲàÏ»"å*†¡§Äî4† ,¬[ÀçòA9?ARG’¸BÈ,æþ°\(–^žw¬4UHìËúG0æsBF€¤[ÿ ñK4ÿƒÇÄ÷x #ºÌñ ’ZÿêkÌv¯bÏG$ à03ÙÆ^:%•3{ü„ M”cò5'“kWŠL…³L\HÙO[œabç¸è˜X«vÕ)æ"µý£öf e^` € dŽËjüÍ0·Ÿ*XXg—’æˆÊ`’ <_(0{•æÄL¯ÿ–á„ Ž øÜŒ¥ŽˆýߣÏJ+Yá4#ƒ¶×!dµL2¼”Sc„ÔÌÍ”N³:5"xvšëÝçj“ÆÑÏ2üúë<±¡8±=Q×ÏSgbÓÛ³Ðà¤Á›f&HQü.?áHÍ¿øC^ÉM¸8Âμ–f„™õÐðˆ!ù!¡¤4(]C9k°Uá«2wF9 £.(Λ¨ï›*£A±=*ïˆ7ÎáÙ»woþÝP98øÚøzJ6Æ÷ˆD¶WƯñgbÛr¥£Ç{‰ qe4 ¬ÏW;t:é ²oÔõÝÓh®Á !ËÛ|oT$©û{×ÿ|®ÿµÇn´þË|¾xH‰'º*6=ý“ðn-äÅõy†„ÒÀ‹ß›||Áû†{Ý ~LîˆÕréžÂU,úGÌ~®BvÖ í¿±? @Žó1’­¶¨ œvªk?2fáµ£æëxÏc;üUë_ßÑ>lB˜ÛI}¾ ÎÑ(`ÁìGhÆá¥?ø³a¦þ[Mös-X´+)]W‚Hñ¿‘ 5gÁ*™&4x(Ë¢_}ïlòlQ KÀ§ŠL"¶¦Æz`bUbàmN¹—¿ásÏ|À_ÏbuNx¥6œG¼6¹= ÖfÞKÂ*;ï×àN8šÚfˆZX*˜/L¿O.l ™<Á¢ºÂa$£þUÑ!ë4­G‚îK·È2ª!¨âŒäòg§9ÇE䂆qýOŸÁî¨k9Û,g›‘³¦LÞL3A8°Eïúúzg[‹ÎeÜqŸqp&„Lªœ‹¢QÞŽ”*ˆŽ<Ïž`ÖŽ²2fÉ“ïÈÊw”é æmüáyÞ²e‹|!³£@ ³ X33€ò,RUö¿Ê”óšròByÌÂ÷ÀÌ% ;eùœŸ —r0*áÊœ¥UÌR XPÌ#2bJÇ…¶2H@F†gŒðX}½¨PQfºXÉŒ¡2àûõ@­(u“¬ÿµÏ"CÖ;§¹5sšÑC‹èÖ]€'.øŸ`/¡à‹AöìFçYFx\hJPUSÎ=†²|ΰ3·íö{c_ƒŚÊÂv:Hh`Å#4.Fǯ,v•‰ÎHZ—FÌÙ@§ÞgdÐÀÇÁ «1l[]á™hµ—¸Y庰ÄÁêYIÂÀ"˜çJ8¢0ÔLÜ ÌÜÄad;L– m¤üH)0\x#GWÂø|¬ Í ¸à V>ßÒRtä³h`€G~n†Ell¶Ð”LÖÒ@ fûø1xƒÅœíšå€Ni¾_Ï&ð2Ùf”€ŒŠÅðPÄGS³@È|N¬ˆ >ëSJ3f@¶GxV  òtŽ=(µ»¡,ÁlO ‘”À#’ÁÕÊkÞâüRš¾¿Öðñ†Øú|nó¥[G/¬åÜÓ¦b‚°›«Š0îºuð¸-­ôØLY3«™îp1¨@ÙnX€3+ÀR UÀ²ô)ffB”†›åT¥”‰*[13낯©,˜qfDÍÅ`g‡-»¹ƒˆó98û2réR]=þÇ®>ž?ž¡1ZÈÉŒÞ~ìxO°ŒM¥¨saÎ÷Ü*g%ü…EîˆI`J™o ™F(®às1‹¡lÏq¾JOßïú·ÁúgÃŽƒqýïï/7D(¥4„éX–ïG2W|Ú‡3/ÆIm„iÆá©‚AA­=‡„fØe²ßNd§~Öb (ÎÓàŒŒ2`ZYºEìL° §{ ™ªÙr¶ˆes•Ø =Ñ 7"d±G~fØlø3¼nòX^:’u*–T=£³ÔÖ£”9º¬a˜R7gJAhw°—ÇË ¥\ás_–å‚;ÞÌÖÔv–ƒMÑDs„p1\«°(©¸Uœ‰G¸íuJíä†mßÖÛÈÒuª–ÊÁ±4™›‘ÜàgqH)‰kÈU²d9Ï&Ú¦=EØÇ+eB'pàœ5é[Ø›JS~Ø–3@hˆÐfz¬›fº%ZÏêù×ÑõÎ’!C²]fë¦c›äz¥Ô‰Á¬ås?9«=£Of ç7o¾Íšs#¸o.ÿ_ßlÙ­¢" xf¸È.ƒÎÕŒf]ØŒÿ®Š±‘‹×DFAɰVY˜%RL SÊhª Ši~À:yJ÷ÍyE#à†Ì*1 àÎ83 IÇÔ`=ßG\xªÐK•vÌ]`~ãõeKn.8Ô™0d€¨ª{Æ?—3'þ^hÿ<}êš3ˆãs¨~ߋϗšKR²™²ëԹΛdýïϯi_µ ׿Qj%9YWÌB_*=¨a–§K?Ö¿g 0N Àñ±‘ô-H'ðó`ˆ)ëòPjX…f¿ÒÜ!Øü®î]ä ÿo4+†ûIg»ÝÛiÐYeY=ú÷¬*†|XdY !›Óu# V{—0]°gŠr5þ{8‹y7,ok®e:>”•%#¡dXu…Õ¹\ŸÂ‹þžv¾÷$ëBMYòçr‘±¬¬cϼ†/=pÃ|#tŸCÖ(d ÁìÏ侯 ȆÕ(œ®K Ë©í5eñ¿á~¶4VáÐÛ k«a6'Ž8V…GÂ÷£t,ÊÞJcžšSÈíœZØjç>»AV© æ´¹ì¥%(‹óIlÝXž”àßk› ;Èþáçÿ Þk1KÁ°r.É™"Tn›+?§= g)gÐ ;èAvºYÎÆ.[ªcÂݲ¤þ–ˆ¹pŽbY ËyXB€›:º¶©K“Ô\Ê·”ÚœÃ¶Éøskkk]6Ò÷Žò f"ðK ¹c·¤Cg¹3 x=Ô0îúúzÈa‘6µbîh”åƒ×[ÍqpqÎŽl Ìá±+kl>uÎüžBƒÌ&ø÷ð÷ñçæy*ü³’ª™Q§W¹Ÿ¡ÜeÈ©ÐÒ{³þÓ>®ÿr®ÿýÛ_c—w_Ö?†¥*–€ßf;ˆ; +E³,PPÒV9óÇ¢;JëÐ"»šõsG ãG[jd´2|µÞLÝ›¼þí)ƒ8å§²¦„­" •DáœAúÇÀ Aiya°àd³¥{Á>©ýž×ßoÜXik7÷M,¹øGKåÀð(@À¢œã:YšY(ºE !¦à¸†ÖÎ]˜$I“Ô\æ»4&% f?:«eÈ"€ƒ3UlÛÝœ’ôÏÈ>ypk“QD³®liZ°»& d–ʼÄ&”ÈùyE†Ø¢UÇ&J³IÖ¦>m6¦ŽÖ€5#…IêUB?¥cqýE¹™¡œ}Æ,7»îæ@G3bɯsFãšiß3g›+›ÙÚ”JMÑÞ» ö)Áçáç²y™[œwâ ôÔÎk]Îz¡yKC~¦v;Ø™ ¦ÌU°£r¡Â¯¯X§Dn¨¬¹ã˜Èpaèÿ³Ãš²ÁUšÅШbˆ%XêáçfxZkµµµµv¼<Ü=êÚ±Ï|Ll›¢õÿ}KáæÅf³lóyiðzýãiÙ%m!.€©@O]÷~úݸ ìç}€u±QîŒ(äjlÇVÉ;¾ B ›+$;m,M°øµ$bájݱ"ëƒÌþ¹s»£Fˆ’ØÖÁìHp›„÷j¦}‘›"üwµ¯€?ÔërÉ”ú“ì fL”R¦àSaµcžßav¥1!Àê-žÃlIjmf–Ã!c‚óGÈ”ZÂë•ól! 33Éú`ÓJÀÔ_‹R‹åYŽF e*fXS¯ïU¬·;\Á‚f{ý¥„(_ëf¨@~‡ÈVˆ×ªßÇ¢¼oñ^Q*iÍ g·4}Æù|ÝRBéti d±„A ˜‹©Æ*àÀ2±@YÊñj ÀZÍVÒ¥Àƒ_[dê@ði©mÖ±]§R,e ®…&]Øæ£Ì€yÂ5kÒ¢òxÄ?ûÔ?[HâªU›åYNµb­Àè —½,÷rÎN;è™ îª+‡¥†ø`@çÎ}ÅöÎ7l·]{öØóÎÚfyÌ©ö¼Wo³]»÷Ø;ß°Ý^ûÊ—„ç¦ ø0àŽ‹£œ™‘ ]ÎT׋ß]Ì”}©¼#Yw¬G*f^xˆ]=Ðp#ÁBŽçg°°ãs6òØç0Lõ½ªeb±PW€Ò_ uîÄsˆ,^£*f”ôHYy3äÅð*g@¼ž*(3“ð=¹Èa01 aec‹ôß÷îÝE?ÿxÿkæ{±þg+Öÿ¾8Zq1X‚õ¿ÿ¬OÚÇõª!GÄz‹høz¤ÁC«"äàg %á‡ûÌ2ËÒ$nÀ"ç !Ë-üZÙ¦º5×LÏ÷H»}ΉÂÏÎײºÏcz>+µzs€%øYšaÞÐ*öe•ür_Sy¯1¤ŠM–³1aÜ:Æ¥S!ØÓ„ý18¤‹š.€TäÌ4 »ÂÕ"Ïé$Ý‚0T²xfè¥éçŽr@CH38i3ávhqî*<ÇK<çÈzá9æsÆ×&\£ÔÛ•2}4–2àZAj÷”<›õ!Ë{…ƒƒêt.½I&“ë™…bÞ,“dØei5ª50AVs=©•5°3“D˜Á* ê”7”S|?6p´k`/°èËu R¹h½³‡Öçóöš9çå¬P]Êã&æhqœPjÏxþ‹kŸs’èÅþ›~ÄÎ{½#JR,4¸?éáÇÛÏžúSöÚ‹Þf/:ç<ûÜo¶YÎöù/~ÙÎø/çÛk/z›=õÔÇÛ#~BЖϠÆâ7g~(ñˆ Pîòó€?ì,Ýá¢É‹£õõõÎýkT\±|Ki´Gr.®¹èg‹èѹÃÏË e)¬ÎÚèú¦¬ª•u³ƒ fPò¦d.laË…*D(/Y[[묹UaëlÏ ¹Lg FYKÈTuNKðgÌüAé—’¡aá²1tä|)–Ð!ÓÁ’7f&Í,Ø—ïëúW×o•}»b-ÿ-ÖÿcÿÏGÚ¥o8ÏŽ9rë>¯ÿǶOÍÖÅs‹F OH¼ç :n˜ñéS8‘ÌÍ„-jv#Ú·¦Æ9*Þ•…9£ ’µÑý£œÓ2…’ò>à3A³åÃ:˜,bn®©™¯,š+ŠíÎÁ5Ë:Y‰’«©]}fej€kƒçGL'¯£ƒßägµÏˆ R+•ƒ@DÚ·Ç0Md5P–̪.Þy–Ù*u)BÎÆ8C@‹jÎKpÿꜿ’ÅÙÒÿ{¸ö 2pþ7ñ³àÜV¢û®M˜™°Å©MVÌÎ.ef«ÚпEPš)Y™ÏÚǽ çY8O-†²·”ÄüÊÐB3v¡§Ö¥*ÜÜ*ÈÑlù3íÙéÏy©a>:ÈM@)Î2áŸS„aׯBZ›n§r3;pß|>ÍÙ7‡¼å÷•Rƒö|^‚1†csbìËQê‹Ï ÞK7ÍLÏé¬ÁA„ÑaW8çlg¿øvÓÍ·Ø'>}ãÔ¹.¥%¤üÏ>m7Ý|‹ý拟ÝÓ–©³Š¥PÃ䪓®žœKã?íï½×n»qg'ÃÏù¾KÞh7ýÉÚ#~Bx˜ £¡Ìø¸ñó<è°«~ûmvûg¯³ÛnÜi|ø ö¥WÛo_øº®PâZIݸøÄ¢•C0ÕŒÏ>àŸÙ A™ÿœÁQð*ïêýØñŸsâ¿#˜U~¼GÕ½«ïÑ ‹¶&W2)>*×ÉA |Èùy@y˜Bx}Ñ-ÍÕ¼ KG€”³‹îÍúG v®‘ÁSç†íÁÿµ×ÿiOûûÁÜßvl{µsäV¹þ™]:’ãœu¿Xÿ ODöÀKìžÓzc €¡ÌÙ>V9j ‹ÀO;ë Ò¶DÒ·pÎXN7x?”]0°È2 „ôå¦ ððkw÷“˜ Ÿ}F²ÈZÅ€v×qÀ¢J9"ÉD9ÿL­#媩ֿʪ‘ÿ&²¼0'Å=¸+ˆÌ›&«½[Z`ûœPpPK±¨Å{ŠÎt~\螆LW×ÐÌ,X]Ú@@÷º†S‡ÓRÒW¢”];@Yi& \C™f¨jÌšdy w4Ì1*íç&i[Qàþìðö÷k@4Ì»D7Kÿ½Âó­;—1•T޻ᵸ°åé½jµNà¦"š²Øúù$Y8_4¹ÐMó>›`¾sAÌüw—ºµÏ“¸ë]õèg¿W{«›*,ÕƒN÷’¨ÍJCwµVû±cbï¾âÊ0#“—¯é÷WÚûIš³ì´Í€Íà"ß7èÿûå/¶Ï]sU×õgÉv•¹ÀñüìÊ+i‚U³CþÝ!¥°JÜÁ̈œûŠ3ì?î³/Üò×ö‘O\׎ã°Ãí´è ¸`TE8Ëx(_IƼ8d·"t¿Ã"R™+ðëÖ3“3 lràlЈ `À¤òiø^ÆcU©è£¢¯[K3pâù$¾GÔ½ÙÉ8„,‹3Œð3baŽ÷´¯CU±Ëßë *Öë¿l°þÐaÖI†€ab6s´þýšïïú?ÿ’Km×îÝvÔÖÃíÂ×¼ÚîÔ‘òœtÒŸÐhrC˜ZM¸OÖI‹N  Œýô+K®‡”Bù[ÈÃá¼,œÃý8gf,aÕ‘…»Ÿ×N3fÁàóºªé IDATWëçxÚõ€ž‰‘ëß$¹ì­"G¼ŽÉ![mµÇŒ$¢£¯íëLÐ*æH©Ôk¡¬¬Ì ISœQ™[n„PÉ E& œòäüÆ` ¿¿úõ/44—6Ñ1«Ä'8¹-U'‘#Ö#˜$ü-´šu¶Öºü{K);^'g·~FY¡`t.ouyJ‘é1:Î4 îO ËópM™þ8‚¼‘d¹ˆ’ˆSðý!ÏfÓÌTÅpÑÚd]Í`Páv³wj8úšÞj-M67]ÆÑ!Žm§§÷˜,· ©€ÕÚàO³hDPà *ÈñÏÜÉô |{¥`½“ºœ)Êó2ï¤Â¸çaÃÇ ³M`Œ€›ÄÚrnµŽúæ¥4–ˆ7­Ã=Ä>ÿÅ/7`£:Z7Ý|‹vè!­€ÊÀ1c€Ýrå"æ´ÝSn´À´ÿHûÌaþû¯žyvǨ¡r弆.nøÞÇþð™™Ùÿõâ3[¡x©?Ûåò `à¡y–C$$Îb¨\,L›äƒ[póCYÅŽ±|‹ xö‡çƒ:Ê–SíãÀ®õ&,ÿDÓe* fc;§©*lX¶Å¬Ùªa|–5²3–£ÔÐïñ¬kex÷šÙI1V×E1‚ÈN•}XÿŠåAsͼÁúÿæ·ï´m;.²Ûβ£¶nµÛβs.x³Ý¹{µ¡TÁ÷+ß'Ç[ãð.åñ‰\ý1·©ãœQSfÕ÷+ö»“ú­°Øƒó¾/Ͳ”Ÿvà X™.ø4‰Y pÿ¦fP /”Ê•àªi7ÏåLŽƒ®dý 36 X¡}4ƒ>Êì ~.<×,Îù Kå¦ Í`ÂÍ$àuÜ n´þÛ±¤kª J‘¥²âfe8 Tf³¼ ;Ç¿IÎêç¨La§fAîºXKy*ŒrÀ0…Ïf…Þ‚Ââ¬.gg8¡L&Ü‘<Û‡.Ü­­úýÒ閟̦?×ø:0«Sáó;P2ë3…ÚÜñ¨<î­®kyñu3S8lng”¥ò³Ynòëé¹HóéuiÀ±¼wò²Îg#š~ýG¹)f‚°³Ö߉Xƒn>Iß½ç{vÒÃŽo]½Ðå\ê´O|ØqöÝ{¾×m®¬bwtqQw^q™ýò3ÎŽ8ü~vûg¯³W\Ö˜«w]x¾ÝvãN»õ†kíþùõvÒÃŽoÇÿ¹k®²s_q†ÝvãÎ&Aó_OyâOÙuW\n·Ý¸ÓnúøÚ+Ÿÿ«íçv^qY“Ì™™Ýzõvíï¿×^ûÊ—Øç?öaûêg>a×ðr{ð<°»ß¾ðuvÓÇÿоú™OØÍ×]cï»ø­ø¼íÆö#ÿáAfföÕÏ|®ûàåvú/<Ãn»q§½ó‚í àõ쟷ÛnÜi¿ó¦×ÛåoÙa_ÚyµÝþÙëìò·ì7ßSŸôxÛyÅevûg¯³›¯»Æ>ðÖ7ÙƒàÁV˜ç6Ôšua°ÁVÙ#G9%ÕC‰9³9h›ìŸÙ žÕ`©“#à þŒ#“ þW%É]XÂ…ùNl©®æ·P‚¦Ž8Ï¡CtU1ÂlälÅúWŸ$y›m°þUWŸåpÈz²Ó¿ß·vÝeç\ðæ%#tÄ‚:ú( è¤ n6ƒ¢Cñ õ|6Ëݼp¶25°ZE¡uñ\c *´+1¸5ü»¼©Î5%f‘d¦œC~N0Y8¿1øáýŸcJÎ1š[QÖØU¸¼%`•²pYSªÑ¼ÉHV=j@­2GQ™]AòFù>Áå«FËåàJ–S˜#@ƒ\Ç Pmµä-ÇïÙ<©ÏÕiòºœ`òÙdi:ŒŽÙ@$rU‡™)YÇ5ÐÎjMºÇ™?5vâÛ Ï,¹ìÁñNLM°,¯)äuš•Éd'6"µ›Ø€Öi3…=ÃÝÐd¦”ƒE"%H^þ»gö4©Z©d`™$ØosC)ÚK» ›³ZÝ{ZBÖ®NÔF ¥±;gs8 ·„ãhì–ÕÀº·†“‡h^ÿ‘ÅR5‚B?çSî>[\iPº’»¾ç,Í]BSeÙòæÒ¦™ R™+Îú(+\œåð9¢¯Üþ5ûµçðáÚž»¿c?zÊ“íÔÓN·”’½kÇùö“Çg=ùIöГŸdøðGíƒï¸8,˜_~æÏÙs_z–ýØãþ“Ýtó-íu_ó²Ùÿúoí£×^o‡rˆ½äW±(õëGmOzìcì÷þèjûÛÛ¿f?üàÙk_ù’vN|ØqvÍõŸ²×]r©ýÝwØ£Oz„wæK­Öj¯ë;쟾õm33{ý[ßa—¼û}r¸ßÌìÑ'þ¤m=âp{ﯲúÖ·íQ'>Â~õÙ?o¥;éáÇÛŽs^m÷|ï{öú·¾Ã®ýÌö¨aœóªP@ûïX\bqÌEƒ’¯)[m–«)©—Ï q(*;ßañŽ …__å\L0äMœSáÏÈì̪œ ”dq¡¬XA%Mã¯ó,˜b¶FLΩœQî‰êóúWá¡jý«9)õš ´îÍúÇ{†éüš°œšøo»öÜmÛv\d»vßmGmÝjœ}¦säÖ•®[ûË!(Ír›Šò†%%)Ê"¸ˆ¯6AC"fÃPjÇ »ÃÕIúQ…;Ÿµs,ö’D†&œ +çY BU H& á@ªÍ¸`Ob K°Hë¥0•#„@RìÑH~­š jÞG9r20Qà YˆÀôà÷r7 MòÔɹ8khÄ”1ðpÉε&„I²\ÈZÚøo-X$oÁ‘:I8dÁ€jò:±@ƒ9ƒHt¢S9>‚™¾¾Xÿ.›°ŠP×d|-°5âÑHÅŸv½|Ä פ ðá¤Î6±/-®QÖÎÖ@DÜ2'f07{hÚj4póƒ|rœ­á›"8´¡s¸¥†ñë.=NÝX›è °1«BžšÌta1£hÄðvÞ&Ä¢s€?§Ú³æ`Aóe‘:ƒ¢–CM×ÁZ7Hn–7ýŽKÇN:áxû™'<Îþeï^ËËŸñEù”'>ÎN<á8ÛqéïHG¯‘œº,Ål†SOy´½hÛyíÁûºK.µ=w§Yq[2»îÆ?·ÏéË]»ó3ŸµßxÛì7þŸ7Ù{>x¥™™=ÿ9Ïî6ÿ¹Ã=ÄN;ãL»øÝ—ÛSO‘™™ûÃ?Ô:Ù?÷¼3ìu—\jï»òìEçœgff?ð€cÌÌìýW}Äîù—iþØõŸê:ìþëîï|ÇžöŸ_lo}ïûíw¯¸r Œaff¿ù☙ًÎ9ÏÞwåÙ«_ÿFûú7î°G÷á†öŽ=Ïΰ˜œÄ3#j6g}ü}°àçåì•k¡ Î?ây5ËÃ3*|”‘¥yý¤ö†™ŽQÆ”’·ðµÁßÕ9åù>¾U³ªPBI$‚Zf“æƒõÏ´Ë´ªh»7ë_1GUÌ™°“%v³Ž9ê¨Iª ’»Å©Îþâ^Ê y°Àô4ê>¤—Ëã ¡+Ìm²| …ÈáØÄ 0A`ª ÁèÀY,tL„÷4>ž"óÜ’ß?"¨¥nEd¥ ø²Ø„!C7²“_eª1’·­Êðù*«í•!ibSРÁ‡š6Àa¢@ÏlT[†Á&e^¥s[8V²×.µLÁ¡‰ò€r’À‡gu‚ô  9²¥š ûslPP“Stq`gŽ¢¶ÔºÏLO©¥©%vª3<ÈPíî±dA^8צ˜lÓì_¢¦F˺™‚¸Wq1à¶ÆF$`#kB[Â4쿼îèWaÎ2¥¨‘]IA~™@6h}@i59CË®n휵ý‘öiš]j9dš—àÉgDûõ߃1ŒV ]^T!§j±?–Àî:ÓS‰¹ ÷†Å@êƒ~&h–s›¯ ™;ƒ~}}9Ë3/¥ÎK±½( ÎÕËì©§>ÞÞóÁ«ìs_¼ÙNzØñökÏy–tÂñvõuŸ´ÿqó-RC[ÓŒŸ¬ª I–Ò¤mZ2?ºýjößü¶œSxÝ%—¶?_ü»—ÛKOÿ%Y<úEþú7î°øæ·º‡ÿ©§<ÚžzêãíˆÃïg?òà…ôí¡?òÃòÁÈh ­[¾z[ßÁ[n![ïw?;ìÐCìSW~@J90rô@ZjšÏðÂ]ØpöBSvÙS@D½Ïª™#t@ã¡~ÕÉðãTzy5çŸÃQ•ŹšB 5ÊÉáðTõ9G̬ Â]å ¥fjTÁÄÅ_c%Cãïc6ÊÓa€¹j–Ì6Xÿ ±àÖßÊ"˜×ƒÿù˜£Ž´ Î~•¹õÛµ{·mÛq‘}û®Ýr~î@ Å}¬“¹ý—xS9߇@ çüà×ÕCå^ @?E³‡üÙ4«“ÙöZ$‰'<&.¦|æ‡;X/ŠûÀ– f´VB‡rpâÊó’õá¨ÈŠv¹ E54<°S¾‚Vr\YØÌel8Gº})"7°d‹0R sÄ쟮xv;éRͲYªã@aRaN&õ27ŸgA÷¸ð¼ò¹ ñô÷(óÜÏ0/'¨&b›ºa¶ …£4}^41H5Å×á[BHå¼T™›°¨f‹p¶真UsYájæ %§<[Îl%üÌav® @ö¯ãìí_1À”Ù­^ÒM=À_k« kw<Óo‰ÀÏÑÔŽ™“˜¿ƒ3HMj–'‹µ.–ÂSåç4eÎTƒ¹Î Q|ÅA*ÕÄ|Ÿd‹MÅB« ŽœwÐæÅ6‹1BY"°…Ù 7Nðßý×ÓžôxÛ~æË쵽;½ë.;ûŒØ{/Úa‡zˆ}÷žïÙWnÿš½ø5Ûíó_úrøÙ½ëë H¹ÝšpöÂ‡àœœ~ô”'·Ÿs€–`À•%~x쯯Ûl6³ÿðƒ? ;ÑÜçî·?^ñk¿b/=ý—ìooÿš]½óÏlÏwþÙÎ}Å«;ñ¥vr.²øJf{îþŽý×÷ýÞðÁŠá¤e`'‰R1õ^£aÚQq‰Å.›FpØ+£xùûq¾K¨’Á¨a{g‹ü\saÎ,êÖ®fidá±øžlVÀד_—%F̦àüÏP’DR¼™Xÿ B˜"{‰`H.Êüg1·ˆ%›ÜÁT€‹ï;²•ôñ˜£Ž´ Ï9ËŽ<âpÛµ{m»ð-vçî=Rªy@2ƒ–OêÙl:^7qAÛì,èÇMRµ2´ g9VcK©¯ÉÀ„ ¸Ÿ1ㄳ<ð9PYDÆ*tÃlKÀeaŒ @MN&‹uruóï¯âï(óMŠ[³rý `§€ÞS« WC¤ØÄØ‘à«Z`(´g„0hò)Emî&kyq›Õ4¢«M`ËÁK.Þå·ÞMN®Î­Ô0W„àÁÁ†AV™ƒ(ØF[²*J “àí}A^Öæ–`2œG[õ×h。þ9sZîc<ããJ#CÁ­‘g©"ß»>$_Êdò5£ýň-ÆfKZ2uäÒÞ0ËÉT¡eû˜Yše«Rê29²;›ö÷:¹«%”¦Eùp ’3ƒ0Ò)¤42+z&G¹q‡Ù¡Àh-©œ¢Ü.-›Gy¶œ·éáâºO§½ÔbÙr;Öþ˜¢a‚(eF… Ùªµü¬ÖDL}›Yí™KÏÅMÃe0Døw[¶ e*¸ýì©?eÛÏ|™m¿øíö§ŸºÁJ)ö¬_ù°{Õ´üK&)çå%†n#‚ŸYÎ6£áØœ³}î‹7›™Ù¯<ëéö´+z±#‰¯ã÷ø/=ãiöÔj­ök§-f™þá›ßêÝi„¤‰;ûO:å1ffM&÷Ól'c H)^7<$ºDóÅŸï¹ç{văd·|åVûü—¾Ü_ ùÜÈr‡Ü•Ó—üðǹ ¶Qv€É–Óœ…Ã,ËëøçF8ìrç@C™*8Æ÷PÌ9>߸ÖÖֺό̔r[ãâ\ÙPou8 ås¤%[C)&š9”RÂûn4«€ I¹ª©õÏsb#3 eÆÁçßwäîç¯sôÖ#lÇ9gÙ‘[·]»ï¶m;ÞlßÚuWÜ—ì²ý’¯ÏÛC©ÝëËÎ%£– ÷Ê:}궆R´·eh @'8ñZ²ŽE†×kQMÄ"‹lqX§*@™²–î:ÔÀÃû§ð\ËO)ïIÞ—p¾¨ ¦†çÎ62òíÇ#3– +ÉÞF`-a‚ÀÐZªåµÃ>Ϧ©¬Ý׃[šMR­6“B]Ú;ÁÀ&¢ƒ‹¡ã" UjÈâ Òµ%»”Ý5 €Tc‚Ä{¢4orÞ‹ñç¡Å7²YøZÙ2’&&7  ¼vÓÚÌ-4/Ôþ‘ Ÿä  Ö¸XíÖSÖçÅ,³´,µûaŸ&—8gKÜÙl*ü'i]“‘婹’›3Zœš$eóðqýG ‘Rtlër§ËòýæÈ˜%¼FÆÈèx¦crw»´tª33[˳Áš7P`ó3M T™Yt[äºÈ–òÂ…س±o ›Â"õÖ[è䎞®¶emÍþããN¶í¯z™wÑÛíO>ù™)iÖÌfË|ïúºm¡Qg™æ¥˜QÁ‹…Þ(n¾þ;ìˆÃï^ã ·üûŠ3ìþèâgÍìCï¼Äžýë/¡mk`à_{ùþeû‘?ȿ߿·'?ödûî=ß³÷|ð*iÑÌȘÿý»÷ÜcffïÚq¾ýÍ­·ÛÏ>éñ+&¼³•Eyé/ÎÍ•ûS;÷gØ;/Øn×\ÿ)ûŸÿ {âɲoïºË~ó ¿Õ X¶¾¾n[¶léÀʪ¢C%“s*/°ã~vìúcAŒ÷@˧"ðÃÇÊ…*ëæy†„‹i5? ån¼éûﲸØa©áÈú™,¼Šc01zM%Éc ÝšØù;ècç9Îëëë]È0¦²bý³”PÉôXf×ùüW¿ÂŽÚz„íÚ½ÇÎ~ÃoÙ®={$°÷c> (%½øÃ·]¯esˆ‡“óC,P07@Fƒò€J)a2²„VBhœ€Ò¼6 ]]ã쇖Ò6…šrCM¯Ôuz{+leÓŠL“Ú·q˜7k“‰¥­ÿB`QÍÝ(Æg”=µ‘ÌM5F …X«éÞªa~GÉÓ¼XN³Ô j–{ÑgÙº°Õéâ,  k’rv !Ð@¦ Y`”js»¹dZj¢ _:~ÌDðØBv²MT8p•A(ÉóÐ:›Ù»&„ŸEsƒð\§< 2ŸÛl¶d…l/5íTf¡+ØIûµÈ“³%¤t`ÜAË,7s‹fZ&°ã6ÖVû k3iu’•oú+î5ÞkMb–å¾íû2ÊÞxžÇئÔ@Vì=-ë§R ÑÔ›08˜AÜv‡ÁçÇ™!4Ù sQðšx?¢+aØ3RÿÜ5!]luŒÏåì ˆmf9Çg°3öŸ~ê{ÝY/·íoy»]}Ý'Þ2[ÈN]‚%¶æ.Ïš¨‡Ìû®ü#Ûs÷wì¶wÚÎß¿ÌrJöì_¹}á–¿±¯~ævë ×Úm7î´]»wGËO’Fù×þÛþ±=íÉO°Ÿ{òí›wÞiÛ.¼ÈþþþQJFE¨Ï+Ï¿À¾þ÷wØO~”ýÒ3žf—¼û}CMöúúz|àÙÿÏÞ»Gýº•ÕaÏZ¿o£CîxͨÅX3 hm‡)Þ QšP­$µ Q!Þ0Z„ä`Ð$\,(‰´ŠšH*x\´êH½ÔŠˆW†•DDÈh‹º¿w­þñ{ŸgÍg®¹ÞoŸ³·mÏ.ßg콿ï÷ý~ïm½ï3Ÿ9Ÿ9µÃOzÈíoñ=ßÿÃö²Wþsû“?û3û¢'<Þžó÷¾Â>âCbÿë¯ÿÆTó<‡ÊëQ]K%3b‚ªÊ€ÃNÙxAu>Ù’™‹#†Ç‡†’Œ0Ðã…€MYd3Ø[…¸®2•0« Á²+¹ÕÊ.÷È`€åŒl¢€û«dW­4³@pªÜq¿/nÑúçã¦d•«.;®ÿïýÁµó®?´¯þ?¶;ÿø'q\§«™»{Eƒ™qŽóµ^ÑrÚD–ΩkÛ›²ž›ÿœj[¾Ö\¡4¥0>749̑ԭ“ÓKwØfhªf°ÔP˜ö®©±gë|®*X—nv¥»&ÞCUPµ²­^5&WûtÕúÇ¡ø¶µ(”²3mÒ?ȯάZà:Çè4—Rw°„¦ è®–dk.QÃü  ”¹•1ã$Ù)2^˜œ?O9°5Ý·–ŒÂD‚œò8ë(L €‘Š¢ÛÎ S’²1Ъd1ÞmÊuë£OõCÚNç» -¿×2P2)Èㄹ@ø^è"7äTd €¡Ë`t€ !™'4^/=e°†Ð(.W«•gÉjø;ãg$sëh–!¨SÀÜ©u'Œõ?\莚/ €ÈËF„¿’Sh볺¤ç=@ÕN(yÞï…Ϊû³`£†d¹' ‡ýÃWõë?ùÊÔÉF#Õ©{Ü£?Ýžû´§Úó^üR{íf’ž¸Ü Þ@h"æJ^ÀÅ’Ï0¹®·×Ì‚‘ª”‘Â’Ì(°À«àÍÕp¼ªs¨4ÞÌ‘¿G©b]V÷q5›¦X­UAį9jÒ©µÆLÿ9Àõø>ƒÄQaŽ<~톦NfFp&dã=Åi{©¸Ë!»Ir!ffØ ‚g©«Ã€FÍ&¡,g•„ j!É%9é¥ûëîø5f~ñp|IàçwRÃ%qL¢QÂ~o¨n°€¿Ó:19Ì4@]«!Sl#‡ÁA˜µim‰¢¥¡,m+i6(3ºÎè”Ùe.™‰  Y£²µiÿºQø+éB‰ÙiU`ÉÝÇ,Z ‡Ë ÃN e2ո׿sßaC÷~¥ºïã¿ú6°ÈÞ»“þ'ºÄ]BÑàìÎ×ÅSìy/~©ýøO½) ú":¬40ÏÅ£ÿNeÐÑç‚ÇMØ÷R×vš€l.î8¸“;â«ÐPÊŠÁ¢Ó€yP¯ä¬ÕD ŒPÄÖÄJu$¥RÔ<쯶åY¾¤dPø»,ùB‹jÜ/ÿýÕð4gï°á¿žÙd—pû¹ WL²h)öÝ•„Ë_‹Öãø,1Äí@y‚ÿ7®,ô}† ·]âxVˆY4—`ˆ«Öÿë¿]±þQæV¯Xÿ¸{³ëÿÖÈáVlŸfdÑìŽm¢ó†? ¶_·I¥ÏÂ×Í5Î1Šã$ÍžµâÂO•¬l’Ó0&±†é~WÄg¡û[x£ ŽMV,ŒrT3rÊÒ›h2÷å  åÅú9w¥åïÁ¼ʽcì,ÿ‰ó>8ŒJ }„b,£#Ì´”ôiÙ}.XòR{=ZRGÆÎâyšÂG5šT&¬£m–âC ÂçÙ¦˜±*uÌ:õ…œ¤hùíÖë?X2q+ïvàQÖáòïÄ ÛÌÁ¨]¨ˆæuÒÞ>˜ÁÒÔ1׃¿ã¡72KŸSf6#IáÎïy‰`ëè<ÑâŒL¶èîé:e×µÄXµq—mm ³·ín­ç{¯K-YnW†Õ·oƒ²¶Ƹo P”_9±³[!…ëÙý0žé{€²ÿ‰.qm®$ðÛe&Èâá7ëÖ{0.Îì<ê +¹íTø]UȨðFtšc{b—ß]»A]~.\*äqe·¼Ê@a` d3,‰`fåªn?: þûŠ™ò‚Ÿ™ŸUx&»Ñ)Ÿ7eà³²eéÊÑT!ŠÀA ó~109 U2¬#“|ÿmÛbö…oô«¹–wq7ØåaV™9Q× ¾§:¾xrÁµbf«¤XN5—•¿Iw£ëÿt°þýØßÈú/Îëÿf¿X§¯Óyf®À¢-nÇë<óˆ™ìöN!¬&,²yþ2–ÀñÏ0«ÈÙE4{c)½LÍ dfVׄ²gÏÒ¡’»ÞÄž2½z_õX13ÌÞÚ 0` Ié_-éO6`§0Ÿ£A¹Zb1öß"ªD $\QH) ÓgÖ£Û0&l04ÍåÞæÏHnoX-b—¢¨5’º©Ì¡bçy)`ðyl¬$ç†Ä(‚2:’A4Z€×ù tš)6b{)ã%Ö©tnš°Ke)g'IÕÝÁ€gß?7¡è fgDpp¨UJ™š™S$KØ·±Ù.ñ±üûô3%sŒ IDAT|ÿ†û›ždž½u^ÿ5Í¢­·›%ÔÓpC–ÜCÞ#‘Á[ÌY‡œŸšŽCb6;eY6è`Ip #¯šÍwPæ|;Ìù( ñâtJvØÉ¥MÈtŽ.“k®èøSùDlfW„ŽQ<4Ž:r8¯Ý㕼»ÝGôÔE£Aõ{܉Vf ¬3Ç‹o,š Ze±Ón›2àÂÚÿîLKÚ”´ ¯]gXØú¾³dK1v¢É¡ ø™x\|V‹ÝRØÅL½/‚ ð:ôkh–;¡Ž ¿/27Š©ÄãÈÁ· šñrfScån¬fÝVëŸY$f+•œìÏsýßÌ—g­×¿(€抌òRx!ï?UئEãdƒÉu‘S„ÅR¡õUÈ»$ýþ°°®‹pPœ—ŠÄrÁØñܺ>Rè*wÒäŠ|]¬î««€d6˜¹JÆvôóåg´N³ 6œî ‰‚f µh#Ÿ’Añ\ÀO’-2I@Pï®i (¬èa°Ÿ‘悌Ž5Î2•lmÍŽxÈv1Ë>öÅ1Žm),–-°ëŸó’ó_qî-šÅµY‚™È†#ɤ޿ ÑTá,£¢õ_³%¶Ù`g’k[’Ó»Kœ…qÁœ——ý\üs …°†óÚ”Ó6î‹çY­>¹È…̬–if'ž’1!Í ùL‘³7]Ø~Îò5UdzBá.gƒõ‘L%Í·¥õŸÎAvòäÚ§g¤8žq÷x9œËmöÂÃ¥gm!5PÃòf`é:»_ïîs\t¸|m ~Þ%±ü «B®…VÛ’6*ê9U~¥«f Ãù&˜IaЀ¿_kB*q]1:  °ÄŒ ®•“ëOKöàF |\œ«yusg ÃL‘dfW:%)Yå½08`éÊë|?xΠϳg <¸¢ôA®’Ùàç¢ìr5o³šUóýA`ÌÏQãjý£Ý6_'ì>Ç믹M¬f?W–í+Ƀ¡»²þoši‡›^ÿ¼§Ô=Îå˜ ” P †F]Cè ‡à¥0ÈY0Sð$NÎL8ƒ‹F “Å7I+0œ•™ÚÆR ’ØŒ;êš^Îù˜MRÐÕu±rn[]§ê±š ’ìäãLÒë^g9:±½O3@‘ucºÇ‡ ðTrÍ$ÓÿÓóqt | A¤sèj’•K@#9²u›Lpæ&I«Ê¼hâu%›H •ÜâȉޱéRH­3Si~Áwí»` WÂú2Ñhò®¸˜ œÇAÇÙ䤙5¿'dùX©è‰õtvX €ÆÍ0/'IìúXE#–Ti•Ù-ÀMàRviZF6ñ<&“††ë?QAÄ|Û4¹»­ó¹=<îpu¬Ù>n!µÀ‹×ŸËàüOŸÙA6³|û[y3 å‘·¤œqN`”0#Ò¹#«Šì‹½PC­¶¿'ÒÎ<¹ÙßCUÿìúõ˜%âðTÕAŒ"cÿ?º‰”ƒ .±x“ízXà ˜‹9<ßÁN–ì°´‚þ}Z¼O é«qU(CeS­ä‚G’¸6ÉaY ,ä™ASºêUуû嬫ò̉²Ãe& Ïô0C¡†¨ÞÏÁ·K!Y‚†Œ/Ü^ *í®¬ÿ­Ünµþýõxí¸9Åõ}ý#Xi7°þ´ÝÕõ«¾jÝ cäú‡»·H ¥”É&ÛˆiqÖ¨ò³È¤I¬Ûqãç"ø{ÁFu³ Ùë¿ I¶ØnhÑ}°þ“dO0;Š!ìÌ– ¢ØÑ#€´buŽzM¿SØ€´>A–Î ²‹$/3bÐŒ¯Š‘Y@:>µ$y–u“Ï‘˜U¨ä4j=BU·mCÞ¶:·@$Ài¹ ŽntaÒyIi?{"x\3V4#ŲÝdµòB,ØAšçrC”ê%;qÈÎøVb(k*`…΀ªèªy> Z]êæ®o²uÐqn2‚Z¾€†<Ž›W£+è¹¶¾´þ\¯àÚkb^=_¨©!ÕéÂ×Ñm‚Ò"3,{c·0.=hÕY¥K­tÀuq:M3DÌŒœ h¹vqÛå³B9ù༚QBã…ëä°ÆuÕéÇÙõðâ™ÅB0Ë€¿‡`„3g|ÛH`Á›Ž#&vWc&Auѹ»3!ü¸ý tð5Âû¿º™òÌ ~ø;ÈcÕ•Uìîk¹BJ³b8ùµÃ¹IÊ\€ÃjœW®kJ ¦Ì˜ñR€]ÍO°Kà¬ÿk°þÑxÏ!¯#¢êÛÁsT˜cÄ3qê:áó}y7ÖÿÍ‚³œÇ6¯ÿ’^Ç×A‹êÎ ™–ùæd÷Ü%ïÉÒ:|âýµ€ô.5öQûìÈc æØ©®¯Ö?TD“!¿Æ²¶ÚF „Ä›g³Ê0Q×—²Ù_͘†›.­s»|.©˜ÎíIr8bq¸èFùÚ@[ÃLÌÄÚv YƒßÙœZä Q*Ìwp‚6ÛçÌ«š¦~6$H!çÖ“%0/d‡ÅÁUw#ëàÑué £“ÔmïÂäÀrvP:âY™˜Å:Œ(ˆñ¾™3&Á4ÖÎî);ÈO6×f‰(à4™B”yÄýÉÎ,P¬]Xî‰õš û Žú$™³„=[ï{“rßï$w3Ýs養ŒgP;àÂá!ܧ1,[238»°ˤ~‡ÏÝCm+òl0ãYln,í Y&žsBùudÀõë™é=Xƒø#ŒlðÙÑÆ²f»r‚¤l;Sƒl KÁЦ˜‡=±ˆ¸8&©šƒ™ lN¼¶V»¾¢ãƒÅ_{±3ˆJ/ v#ßܯè|“DˆÝ¹Ð(çÌç¢v%ÓñcÄrìèÆv|o–âñL »q§]±Y,b‰ÎÜð{ p¹Ñ¹}«Y(åt6ÍGÐuŒa¤ÌbqÑŒÀGÍzáµÁs1|žØðË x¼pK]+,ÕS‰°*ˆUu‘ýi·`ý#Kµ2ã@÷;·%çõ@…Ï`ü|eZÁ óFÖÿ­¸¿Nçôt‚tqKÌ÷db€31>Ùª˜æº¸C»ww§ì WðЭ$ +IãOï… Î È ¨èýì^’IŽ`\xn¨¦9‹21Fä׊md–t%ce€ÎëuÅ©âX5yб-Ë ûl•ŒI1 ªPfÇR°º§Ò'б44:@C…4üÒ3”y±‚‹þt¯IlÁ^ðy}Üô<"ÊÚP¶ælPë-Û}Ãì J Yž–äF%HH7·>—A€縒,”œñm¨õ4€¬éÞ[8ñ%¹+4M‚)Åf%ÜwÂÍÏç‡h&-è%ÀOnøµ ÌeóX̸t(ö3[1-ÿñ–Ô=)¶Ô. ý+eÌM ÎîÜ6Ï+Y°[¥¹=°2iýŸ³å5éY^W¦ãÇ ¤zçóÇl4 PSkI³~Q;£“"Îí¡‹$ÌïcdB-Å.@ß. ˆ»Y.óÙT’ oäöKÈò¸¤YÕår¦'I„´öÙän§½+è@³‹|OuŒ7êró K¼°˜cÛ`,ÀYæ„¶Ä V¦ÊZÉÍ…à»àü^ÊóU¡RÎ]ÊÂuðç Á*† à qÕMC€RD¢³ x®›¸Vó1 X¦Ü‘•`P¹bžð½ØýŽÏÓUÈ-Ê€j­1»ÃE)®Å$±QʯZÿÎôðq[uÒØá±ÁmesŒv°þoä¼ßû+¯ÿ¶mf0£XÉ`u|’l¥ä”t”HT46€_üûé8CPj,á¡4DRºv åk¿‹@X|(3HDðf$ã(6Vö&™ k6Á:©û&dPÕýÐçceÂëå"—db$ëòÚådif@G¬£S²k†1Q›å^dûŒ¡Á‚[—ÉŠÁž4`ò}É­µ[1‹ÁA¥aéÛÓìNÌÜPØ«¿n#ÎçLÖÝ%Ï…ä®[Ê@ây ^g[ò X--”M€®3‡ºß¿ö÷có’sîÍ|/)$ÅùC7K×[³z:·²Íú³qÉr¢'ZK»Ìgsr6N„жžJ}Æ pÞçyŒ ðwðPëiH’8Eó IYZdkµNó:…@  â3wk¹©ÔœåB WÒ!.…•*JÈ?s°6r‹J `¼A*wq:M#rA4øñðFèí‚ÐD€oòIµHÈÞ°ÇAÈ 2}üµ¨ßF©Gd”„,Žÿ:a7ošØÁo“Û|›u”Òü…k×–y Ü9gg-Ÿ_àî³zÀ¡íóQà*y²e2Ï©‡«²=V’1ž‰àù,°Up(wçyއ¡Ú7ZY£,ͺ¶ìVÃÜ8@º…L‚¢Â!sˆÜÁf¢èbc–Dz7£dTPl’  Î=ññÁ÷ÌFçü›^ k¥¤½÷`ì²¶.h}€erãá¼րáÀŸäÛÙÍuO,æÏ ¬€ÅsŒ€+%±A<\åLUÌõ>Ý8dåj˜û`«f†*Ø7g•„c]f–z½)ÛÄ Õ0q9–ós§Lò:ÿ¼þi¦i?ÏÃH‡C c;<[ò¦¢T/ä«å,Ÿ«¨€ë›rn³Ý.LÐ… ¥táœ3…*A^Ï%töÙ½m¤D—”ޝm‹ì…ÓBb…¬ÐjûŸ—Û–ì·Ý‰.¸;haÍ…»ÿ‰ÝcdJøAàM ¸¸S®da±ÜÎâQ²;UL+×6žMR!ƒ,[ób”Ȱð\%¦³yn²6œÓ¤¶I¹­f´XÆÆ‡2ÀàóƒÇ²r'&„Y&UÜI`”s! 4`6‹gk8J®_­–æà5Ôë_Làu¥Ö½bý÷ƒõ–ู‘¡Öÿ­BçÏÜ;gë¿BA„ÅgåLNnàÌæO»FöÕF·ÔõÅXœ")Û”Aà CRë"s©Š¢~ ]Á¯ wÆ•‚e±Ä*5z¶±Å‘ÜW±®jVðh†gØW,Ó¡’c/jÂD€ƒ­d‹fÈáI×8±C(o3Ì(C…øÝÖ%ØBàêZ²¯NŽgÌBK“¬®»`ºT5¸Ç”l\aý c§ÊNiYãIìQºþ¬ÏÇß;ç¥fþ\Ù°Röuæf]É2YŽº¿ŽÙÝDYßbÈ8í!ŒD[³Zù­Àlb)âZ{êsdh.0š:è7$÷ºÖ!hÜÅ Ù*8ˆ1°Ýθ±ÇÏÒúYšœ#A†ÊmùVŠé*”ÿ•¤_LNZÿ}È aÛ蹕¸YI³óÐÚ„„9dÌ»x›¸Ã©îR#G7D|C¯÷ã6×f9ˆ ”¦àk»Èˆð€U.àÌÎNoh¯cnÂPw&ê’çqûù¡Ìó3΂`G[=U† J¯83Y Ô䙡U±ˆÇ”%U3HQ]G,d•Ý5á ô©âZ)Þžwq𠄺ñ}è¬B6™ýPá«ÿQúÅÅ8^«ì"5¨Ï2AÅD1@`öGÉ#©ÜÙT1°z½¼ì×JÑ RÚ+é$ƒ{\ÿ—°þWf#ÈÆ)ã µ®Ê—º;r¸ó¶4hÀEVR4  "œÕØx€m´Y-–—‘ƒ› øšŠà*оüdìÔE\“"Ž3‡ÆFãŒî©«çÃÓD×ARÒ3Î*R 5ƒX VÌêŠ5Z5DæN=̼4q§PR 8Ц9‘R&@4IÆp.¦Î,ÜÄü”ìÈ6ÉÞ€=Šíòؘ¶µÙ^Ûzb¸bnÇ‹õ6ds¼ý<—Ã,º»Ð€&p[ Ͻ–Öm)u€|^¡¬XœŠÐÍôÞBÞ‰ EIU±8ï8+gI•8n­O²Û €3jé½%…Í\ÜÀÜÓeÓ ‚—¸ÇU¬9ò¬Í°¾.Á<á!ÍÑùvçï5Öj*oó0aè“ô ó‚:¾Çî7f¦rÀlüR»ïS&[ã6›±ñÄìY>€¸Å çÝ H PWÑ}ºrgQ.Ñ¡‰rfXêÀöÎ.Ãî\Êò.3 ¯^kþ€;‘-p]$Ú«BÌåYJ~†s4+9’ Ye ƒ Ï ±Ùß8ËÀbUT`ð$fLü{Q\IÔÐ1l´Š¯A«d•»Ä2%.þ2Ãì3Px¾¼V@ϳW̨±”…Y7”!Hs‡:Ï\`­æÂ&Yg•Å@Œ¿C¦$M¬ÿv°þÙ>œKü¯Ö?Ïž­‚¯Zÿ·¢Á4®Á¹@cý·&ܶ6˰J™Ì ÄpƇ(z :z\d|Ÿ»¾qìüZˆy>%†Ç}áñ¦×$в°~–ë_€ÔÜ }ر9Çš÷ £Œ:Vs>Š™Z1Jj½­Ö'—zªD°ÌLK)PâY”éZ®ª°`³€R¸©¶l0Ðóï IÛC‡ÙlkzÎujäA×?A aDlœåPIhêŽraŽ@²9Ü–îZ*…¶Ú`8|NÖYX6/VˆÙ„hrÔÌìŸÏ÷ÙŽ`‰îiF b–¨žgY*0ægúxõÈÝ w¶6²„✛ÊÊR9žrG8d[P¢Æ8ïlRÓÂ;€Œ˜£`¶¯5yëMv-d•à LÎûQ!W(Às[or¦“ž: Y¸ùÙ—{\ÅNz¨èìngL†»¸§ ,§¹ƒ¯J(ÿq`„2YH;qƒ.ìOtÃOÖÓdiÚv°t¢ÌJ î?ï+S<\Ç V”59ha‡*,ÊÔ\ç1€â‚Œm€ý3O§Óî‰ç‹sUœªc†ô•LCéÁÙÑíF?ëÈv%£RÌo»7<îü3ήáù&žÉb «eOƒq`àÈ®x|M¬€Œº>0,µvâ2ƒ¢²‡îîúgi§²GvkìWë_ÍdñïÜèú_Y¡­ÿ›ý:»ñäΛ»ôTìvzçØŸ›ÆÁLñ|Ñ¥"yrU×—³BŸú£™B7b{8a¸êêÞ¬U9p^KÅÊ"#G±KØpH $Hþ*ÍùñS† |-­ åÚ¶úûdþ¨µ§Ü-§†;e Î „ù>š¥Ó&¼êóA(¥CC„pœƒ940@ ßĨ [E&ü;<÷„Îw‰Ýi3ðéÖÃ::™+¬$›µXé#‹%î¡`žØ/ŸkéÝz×Iœß–2ºpÔRw)ÞÈüinyݶ,£C¦eºvÚ+óÅ›³Âü~ƒ÷!v¨ Ul€4·îó`Ý­÷¹àžgaºuÛCr'7 m‹;T¡Õ‡1ÏÍðÜŽA¢wCù¾'ÏÑ€\§ÁL1ƒ†ŽR” ÔÅÜ›,Lëÿ@bv$c\=0´Ay˜‰IŸsÔ\8rYTf¤WLÑÑïJÉ ;Þ+ççt(—`€:0Ëvâ‚jBN5Šö{ܹá@MKÙ=[pÝF®ƒžÛ=V;c|Ï©€ës¿ß ³<€-Èêjɹ@­ÓœŒQ¸h–ŠM {wFK¦ Á*W1_d‰wwµÜ€B€ÒÀ,”e´zðõc¦ÇDø´%W¸lãÝ(Åç€ÏíËÓíŠõO7tšÃ\Ÿ¤’ìaï „Þ!tÕ˜5+Ó,¨«©œ¨­;: ì·‡;çÚ()M6§€ëÚT\,”•²*€CZD ÏUÝ[~)†%u©#LÝá CÉ—”m‚Ìž³ûø¾yæ ÓXLöÞãuØ5@·mS‘gŽX>‡Nv+€€p¸pŸxä¨[Éò½U¸ŸÒ»#àà9œaâ…¥Pêgü^ø;xl]&¨:´*dsÕfÉ¿—K,W39|>8³IuŸ™ñaЈ’FþÌv×?3fPXÎwWÖ¿Tweý—ƒõ¯Ø*5S…l®ÿ›ýš×¿¯'YoÇôuœ R_”®±¬­’|N5&°á쀜(¦ ÒäE»å{Ýìpý«5¦2wÐ œ1؈«QŽº?M 7¸¶”#áj­^ÇòYÍ ¶åö­LGV÷µ•¤3IÚ Èí°ALÖÒÎ MR1’v€êcÆß#ÜÛp6ˆ3vºé&F·ü;Öå°8Î9M.rÈ(!k€ŒPÏï¹b)Û(ÍÙ0ž@ˆk¡&K•{–¦îëªz1.ò®"˜eo~/Ç÷â"vwðº+ë?5\,»R&#ÙèéÄ@>KÚæpÒȾ™‚MGó§ÖB3@ùÐX(æÉÓK4áõOÄä¥álQz]Rù”Äús:Ù9¨„±nßpÚ£í¶<»¤šløùf%™BxnQ¾‚4»Àý@\ÛîhWlÏ6Úï·õv±ÈVäÑTøú­5»vq1ɺ¸#ë¿z0ㆠl΀RU\«‡f)ƨ.B(0ù6â×mùá5u³UYQŠ˜;ÞxV?G'œ{Q3|p3Àbyç,QSF,ëÂ+.HVF  ¬ŒVîL«,™Óéd×®][º=±å7#eýÍ\˜sQˆ›m²ý|3RÝ#t———ÉêœÁÔJ’ÃçqêVп‘õ¿’3¢Ì” H”ìÝ•õÏ@teµ­Öÿ‰Öÿ­‘ĵèø5°ÇÆ.Î ¥‡oꆖ+P؉ 2dŽÈ¼ÝäP÷ΫMòÑI.$£9ÊÅ)œªpRœË)ä ÚIÆ#Á¯éŒ+fO˜õUF1êú»Š±â׮ث`¿ÊÒ~õÙl–à ¢m-¹eM`*ýµd·6tŠëóMïýªº½M Šò‚]B™Ï5u  V¡¢¾Áàôü>‰í¡ýNöÙ¦mÃcî Ž:é%>š}2`[‹0%é$Ùè.ƒ+µžÃ–Ö?fu3kÛ69}¡›ÎÓ1À 6¥>˜™‰ lNΥɸ8ã2æÁª›YL²… {q„{V³<Ö8XºÓé4rŒ¬$ Ó0ÄÔðwA’=6aÀ5ÛK€,œÝ`ȯygþÜÒ›èR3‹n[ÛR}U‰ îxßo (h9*5ôí6úšæÀ¹ o Î–œö R–©‡œ‡¨¢T‰oôüù×..bŽÇ»Ëþ,£`+ï*ô×­5²Ì’ v§Wr!dYÔð-ï‹ôæ¼N¶g‰÷­~^ IDAT˜ƒfV27–’qû @„çì(‡¬Æj.H쨦˜$ÅFàqGcUÀ³óϰ}5ÊÑT7šìyÎH¹üaŽA ÜíŹ+),3¼¸¸ÙK«¹ÎuâàX•¤Ö]¬ÜV¶k_­87²þ/..òl!Y±¯Ž¿×L]¬ÿ¶Xÿ·¬ÙT‡ƒæ´þ£°¤0ƒæŠ™ ¬xâ¶lê“Á¨ä‚`úzÙ·›C(Žr–j)Ëܲ°ùAœéÃÑ K¶`•ŒU¯BLWssjßk©f7¹©Ä²ÜÕöåó=6éwÈÛ¹o.Ÿ3 inÄìX–«%VÞ¾õ\è±­tÚö½š÷Ùy@+í8Fe6|X±wöĪa¢ä‚1ë$¬»HëZ ©q¿ÂuMÏÀ3m˲,Ì2›e®À  ˜ÍhÖT=fGɽnÛ6eÐDØgÕ¬%Î×$Æ£2»1S ¿”é=’y/ÛA$ ¬N§™›P–Ó;@Ëë߈—䈜 ¡ª±þwVª–˜êÛ\ë)ÞG·é^Çþ´+Æçc)í@-ÉÇ}»·–"kîñ MÐ6TÞD]§¿?趆r2™ú¼?P™ˆ‹˜}hddpýòr"zíÅB¿ß­£ Š'w¸Ãl%s⎹š}ᢇ }—Ãa1Ízqœ3QîGJÖ´’áa_±=‰ÁÛYaX ûö00a€æE¯rÒc&L ¸ ÊÙ?"«äsÌŠ©Y<†HW,…²ˆfФä‰ÊDƒ×#?œ9ë‰6?f $3wÕúÇu¡X³Õú¯ë¿/Ö?nËåååòµ«ù#£GWÙ߽ƒ]½þ©¹P²>ºs(qãF~çBˆ3”Î!05›†;Ø^‡Í4^#^ í¿À¤˜¶•V¹<ü÷¶˜â6pSBÉÓxNT5 T•šeZýΊS bå·’'²pÓ «ì&‚Q‹¥<“$cÙ°5±]=WÕ¥’CHÒ¤¹ €”®¡”L$p¡æ9?ÒL¡.²”:1\4G„À'¹»µ>ÌlÌ?±åøp¸CF¨IF¦0 ³¯òú‡Ù@”£¡Ä«ðô‡›¤FÊí}TaÇd+ïï÷ct½[ߘXIÒ. GM Ç.u ÀÒ{²xf×½pbÛAB%F$0Q²¼îÁô Ì ç“Ø€³L¦à~†uggÆYàiÀMt¹K³DÖè¼þµþm’ºÁ‡­ú÷ko§|¯³ GÂFŠL(§j—~-ôÛh&èDÅ:}qAî¥K(„¸¨Á‡v<ñÁ´í&Ø!=²4®Ð äç”"¾3O'!E Y–HXçpV¶uV•Ãã?ù îz!‰Ýtw}Sáš,ÏÀ9þ3u\qöbJxþ† bžObódDðº@fa%C×±U*”ؤß“­ÇYƶ²À>bÚV 6™•QÅ Ëýx^œšma#ž-Sã Fë]Xÿ|ý®ÖÿÔt3_Ìx­9~°þù\òúŸÖ¯°-¾;_§‹“XÿçÔÀ5ˆ Daƒîo˜w‰ŠPEî¢;Α#]-eiDTF0‹‰!¤¼Þ9Ëhj´ “*ìª+ g«{w¥Ì •üíªG¾æùú8š;R€L='V×´šuS.shP€¨ÁR C±p€CöfbnÐ ³‚ºM31jàs9©³ˆb)1%çYdæÊ 'ÉÛJVÉÀÁ²7ËÛ¢€-#—ˆ%v)7Iäðا×Àœ]s§0 ?mg×·"îF`%%`—jÙÕ¥NVÏÊdz†sDêÇ4# žS ³XW;@A›g«ñÒ5Ž–×5^4(Œ€ƒV帱û5GžáVœ( w·Á*!H§¼'mç“u·oK¹|NGc‡}þÎ]ÝX®çñ xV3T `:;\‹yJjïãÝZ§~ÜY&gíl/~[0APf†NkM$¼W˜ŸaÉ~ÏÝâ<¸´‚¦}#iv±/·-~‡g+.8Ha£ÀBTQ,®‚'• %KœuÃ3&‰-ƒ¹d°SÛX¼:k¢ôæÊ­ ;áÌêpQ7=ˆTXŸ*’•uójæ†÷è↳"ÊÁ‹ïëׯ'–D•ñÃìçÊpÄrB–û)Y; *ðá`—%‰,TÀde³«X%µ¿Ìñ¼*9 ²&gÇCÞVþo¾ëbý#kÈëÿtÅúg¼›]ÿ7ÍMë¿M÷ÞÜ<EMÈÂ:ÌðD} … ¾&Ùds±OÎlEL0óµU×][ÎÁÈ(Ƨ isüÈp8ßgÛC°'—9—l\aÌ ÀÊêÜ+¶è(dZÝk&v‹î•«<£ÕÜÏUêŽ<#BLî©&°˜ ,ô!¨4˜drj–Ë!#ã jHœ,`óq+C"×vɲU(§Ràe~lxŒÍì ÅŽu‘´oS¼Z&pC«f¿c=¶rÌ. àØè€ õ5flXRü Â6Ù…š¶¸1³ÌòYcV«aQÞ ÐìMt.î[²¾V¨Œ "4!èhÕòÜÕ˜ééš,ÙHv*0Õ²”wFr? v-Ä{–üzýïÌ^7 †µÛŸ-ÈñýÖf)­ÑþFmZ6ÝÁOoýŒfú|Ó(ë÷îý}îñ9A*«gpjïvýòÒ®‘3Z% N)Å>å?|„=óËŸl÷1µûÜû^ö?ùSûÍß}‡}ó·½Âþõ¯üjîºîŸŠ÷Ëm ×9e½ VÆA϶maxp"VÙ Ô’@`Žê°cÎ qJ…Iö̆ÄlÕ./[Íh0‹Á…´ê"²„g%mB{eúбNÉ?ð=9<Ö †¡¢“|"ËE­d‡°°öŸãþ ‰€*|VÀ÷]1.(ƒs¡*PTFÒ*Óƒs‡”‰º±ñûàqQ–]í¸ù¹XåîøÏpŸ™ÁbGA8ªóÎפ "æë!˜[±þQ*™Ø‘)´’)'º£kþîÞgÏÛ7æ|¶Ë-d #!<7¾½˜¯âAZ@æf †‚CMM° Xü4`æ €V[Þ•ºË!-ÜïµéyANž^XáÏp¢ÑügN bÀíP÷GÅØJé4IE•¬’ïû+ð£þ~dó½jh±KÓ @³9e02©p³ÑµgyYÌàø½ikጆÖÙšæ|¾ÁJz?”ÁõÂ’¡óFÑäy=±­ 3=gèX¶èV™BÝÆç£BkÚFøÌpŽƒýTVÝCg‘å…͈ñg±Ö6m?k7ŒÔ5‡Ö4€çëSÔxŸfë€w©ÅúÖ‚Áˆóך†¡ú9mX./îk²yöbßÔ0w(@`kûñ«ißœ@ûj‘Û3è˜L‚çCØYjmÛÙ=Îô){\ÎHÂí3FA¹Cl"a½¥Y¥8^‰Ùé)0A¦;Å™e+ûÖ‡tÖs¾’œ4žÿ;Ar¹Ùéâ”A.°D÷x&h5dŠú~ ?EKm\”ßð5_i/ÿ‡wØ{Þû>ûâg<Ë>öÓ>ǾøkŸew¾ïíÛž‡=ïéOƒå”3„ï}‚Ì!² ÈJ5Ñ}KR©«H”iñYÌ,0ØXÍbpqŠ®]þPÅ".i/i¾eåN†ò9bÊNhªvâi°_}&»¡%kô}˜¥l,ÉC³U¦ÍJÆåïï9?¸?<Àù0ìbÇûÃ’=øWVÞÊ6›Ù%6}P¿Ëû¨X¨U 'Û®Š@fHÔõ¢Ö?‚ô¶Xÿ ÄU¨-ÚÈ+ÖF9'ªQÍ÷p¬ZÿåŠõ4Ô³h\×=,.eض–¤‹&Òé£ÙAÈáhŽ€%4è„ÄŒþüä²Q³ÔÁF†®+ž»Á¹Ï.$k¨6h0{4M8ÃÏjĶ3»„€lÅò)©ÙŠ âkg%‹Vï¡$W®€Õj^ˆå-, ÃiøÈ"ÓåyìÀ šAµdãÌ2’Ëí…f’µ›äæc¸ŸL Ð1û`†¬±§0Ø©øï:Ÿ(;Ï?·<”Ïñ=b̈ØäWè¾u–ÂmÉ!˜";Dl Jî Ì U—‡šâ³fˆ»´ƒŸŠƒý,óÃ\2³€ÎÖÿ'pm›]ÒÎÙ;-r î,èÂ7†|tC æõ ·± lg’J «mÙpãP.ÜÆö•’[·>góÔ"æ†éƒÊô™¥jd†Lš[fgðÖ@’7ß:_Ç–×ʼ^,dÚÎXù±Äc÷u» ¾xÆ%iJbƒ,?xþãOüû¼Ç~–=÷E/µ/öö¯åW­”b¿ð+oµ§<ó9ö¼¿Ì>÷±ŸeŸò H¬ÓÅ¢p‰®›ƒáÃ÷Š¿¶ÛuóàZÊ6r±ó÷¼Ü¶dí:t++hÎÔáFx¾…Ù e½ÍÀ•]ÆÚbûùõ(3B'2vS²(îΫŒ U¤3ÀfÕ¥W.|GÒ<6àkÍx_™9à€[Õ)Æc§ògxžheí¬¬|•”Áï'º´1³ÁÌJ0™‘Ãó±-Ö?²4]£<›„²?ž¹:]±þûÁúÇkÙ÷íB¬<^ú”GÚ·þÃçڃm;Z?·ªÙ4ò.JäIxÇÎÓÍ1¨NYç’@;îk,uã´¼E-~0U,ÛQOš$zEÌì¨FT0=ÈȪ¢L­ ³iF© °¿bx”±ÉU ƒÕþ­îQ+ã%û:2P01£qôïÔo=úQðÔlÙ|.pm "åâe;´_·­È[–¸¶­MŸá8´A¿m@ºÙ]Œ«Ñ¬ ž°¯û²/±_zëÛì'~úMFpžèÇÞðÓöËo}›ý7_þ¥ƒ Ak]b£*°A…†m èÄ«ÈaÁ"Ü­¼7È"B wô0òß}Ú“Ÿd¿úúµ¯ù’¿-;ƒ+€s”÷ð[oúI{ý÷~çôUvSj†AI¯pp=ëAO‹ƒóGÈš @[9Ÿ)°¥€Ï%­ÀYŒíÅ<[²äå…ÊuÙ4˜ÀÂxÔðxá¬Íê3Ø: y¼õ×»mÕÙ^9O±¤‹åŽ \ó~­Öÿêœûqw6esBñšãëF1iêº/W¬¾>þË¿ñyöúû¦g=ø8\ÿþ»·¡F».eÚÇ9boÎu)éÿ4ˆMjí£à‚bÍçn̰z 3õ©ë¬df„ [4Úl_-” «†ËÊámeرš¯SëjeŸ­Ö¶óŠe\Ùw㽓Uê8E1c$;k¹¨°oú^=ÍûP*;¶%ˆ\ SÎOÉÙAh­=YóBáÎqè$‡±ö–Í‚U²ž‚OÓ>-…3hŠÈ.b`ÿ†°RbKŒÁ X2”µ‰Ù¼‰¹¦ÿ“Ý=5PÂzJûšŽ²ÓŽl-~¶d³]Š@B"HXšsñÜ2scjMaxhB¯QÜGÓ¨â5^ Ê·ÍšÝÚ,Ï ™ÞΞ`ƒ´*#f½¤Y»ØV›ç’Øè"æ²¼)VK’ÅmmÛ³¸ æG-1Xs íX?î !ùëÇ ™˜,Ã|!žaÜüº-˜ a7ÚÒÆ±F÷1µoÿÞï›Â/  þöïý>û¸yh¼ÎîpWv½w{ÞÓŸj¿ðc¯I…ž4,ìpŸ\šáë»LÔê€ Y¡bÅîsï{í¡Uçï_‡|¶zæ‹ÐÄ› ®Õœ‚Ò¡³%¶zس3Û\£DÉ£‡k®ž튕1ÂJæÁÝž³B°¦@ Ÿ?ž¿áù¼>p_Uö†š­RÕf³GÈø¸ÌOÍã0ðÀýTr6Å,©‚L1YÊE g´øøÞÕõ¯$¡l“Í€ú* sÄ^2ø¾ÑõÏYXÏùÇßbw¾÷}öÀûßÏžÿ̧ÛCø€t,TÖÔÍ~µ6ºzµ–É*õ¼þa† ­nM ‘sƒ* õ1±PìxvICöRt°QÖ³AµrY»*Ê­Ùq> ›\Ò1+£˜šaZ^‹€šÃÈ¿gt„L­Øì ŽnÈòøE± LøÜÑ>#Å#t’sKà:düØ‚ÁƒÏã÷ÇÙ¡RÎ3&) F ç§9B çyÛ²Ùv:€ÁMÛJæ‰a ëù8ÊÖ¯90=H,ŠY–¾ÌÌÀgù}¦zŽ\‹ f4hÛ6Üé0£'­ÍÜŠõ?o”‘ælJIÏ$6%ȳ>.½C‡6 %kµ€4®%àEŠ€¦XÿÁ”ô`”ZÛÃe JB³›^Çc³7¥²œ-Kåªá`g°íxÉy[¸î¢†tY*š1ÔH³“°I—±n>F÷lÔ×úäUAfBžpŸ{ßË~ù­o“,KÖþ—_ûu»Ï½ï5=0›'eRU«ËØüdb‘Ó’Ÿ3b°Ò@ÎDz/ø:€zÉÿð]ö±Ÿö9ö¢W¼2@Ò‰f®².>bIÒ~€i€*"•ž‹XÄǬ ²ä¢|eÛŠÇ]é¸(ÆÿެŒbc°@b‡9õ¿:Èz¨ãsT\°5´’¥ø~#R Ôs Vvêj†ß¥i8ƒµr:SÇF12ÌÆÜèúWŸ­æå©!»¸°¶Xÿý`ý#óøî;ï´g}ó‹ì=ï}¯=ðþ÷³<ëöàÜ9ûu+@PYF²G ††¹+›Š',¼,ª²ímr‚£ "–¼$y³!–çnPªÖAZ§ZR¨´÷0ãéÒ*À¹ TU²8fˆ¥tç@¾x¤–P9bË®ªpß\Ý˯aJòƒÌIžõ)–™]Är4”ƒ%éæë›]Öļ 2Q*Ç'17=ÛoãQb£x> (ÜgqO ‹ereìÏæÁŸa! Ï|`tbjƒšúšÍ.oIŽ…†&aÆx=”îç´Ì&0eø:\ñýÁÈžÞAË H½.R¬–A˸¾«`c¸笑 ¸OóO…ÌÆú¯À:‘¥µ\çešÍIÅ¿¢£ ª ˜crƒ‡zªÀÖpÀk»í’%r!ue‡ÙL…æì¡u}7adPk·6Np˧*±d½çl"E·MXjA‚«¨QnËþäOí“?áá dTbaùˆ‡ÙþäOÌx1ì€ÂÙdoJ)öúW}§ýWÿùf÷»ï‡Ø;~ö öúW}g²¾üùwØï¼åõöŽŸ}ƒýö›_—¶áþÑWÛ7|ÍWÆÏùñ·Ÿú—ße¿õ¦Ÿ´Ç=æ3í_ûýöŽŸ}ƒ½á_¾Ò>üCb/ÁöÖ7ü˜ýÎ[^oßó-ÿ(ŽÉËŸ‡ýö›_gç¿x‚™™ýôÿø]ö;oy½=îÑŸaoþþa¿õ¦Ÿ´_úñ°¿ö˜ÏŒcö´/}’½ù¾×~ç-¯·ß~óëìG_ùOí/~ä‡ËF¶‹v&†¿ŽÂX•œGÉäVw”\¨Sf*TÎÆQž†’*·6/òÙlÁ­‘ñØ9³E‹˜2Ä, Ï–à{cÀªÊÎY™N°7Ûëׯ/%„øº#‰ç±ŒN1wÊÂÁÝQèjýsn“²WgFZœ«òãͲ@%ÿAS <&ªSëÇÌì]ïþ#{Ö ^hïyïûì÷¿¿½àY_k~Àýe í­A£¡QCÆ•ÖcZÿyv­¯Sp* *‘>Zicwب8Bð÷v–³©¢½ˆ¹H 0+@¯Óg5²¯.Ë%v‡/¯ë•¼ÿÎL¨T«U¼×®ÜÝðþ¹ZÿGVÞèp39.Ðú9¤cdáŒážˆ¥á Q€†%´%‚€M”<ÑÁW¬ƒ’e˜)稤Ú0X­”9„ «-œó:gÈ4bi îטýq@{ÊœAE2¯9VÐÈÂzqo`ZÒÚ– Âúÿ‡¹¾>+ódÀ¯$«ñ,PÌJ¶àYËA;([C™Û0% 'æžX±¾ú8Äë«ó^°J»ÁŒEÑžvÚ-ÔZ¦ÖõÊÌ8¥[iñðÖ:¬®×¿8”ª‚ì©aîM¶| ŽÜŸP¦¸tOÁ°ãfSÐ, ‘o&R÷tÄ.kˆ(y¸©› ¸¼ì7~÷ö¥Oü9Lï'ìKÿæØoþî;ð¬ÈùaôØ'>ɾçûØÞ÷þ÷Û_zÔgÛcŸø$«¥Ø?û¦o´OzÄÃì/=ê³í¡ÿÉcí{¾ÿ‡íU/{Q8Ï™™}Ño_øO³ï¯<Æ~ù­o‹ áéO~’ýè~Ú~ãwßaýQiß÷m/¶‡ÿûk¯~íOØüá»íSù‰ö´'?élÜÀúîýßÏ{úSíõoþ9{ã[~Þîwß±§=ùI±íù´Oµ·ýæoÛ?ø–o³7¾åçí/ÌCí%Ï}¶”ò(w,V]Í@pF³-ÌÔ(ç8³³#™#“Á¦ lDÀR¾U×S·£9¾I+—¶U†š›b¶ØÚØx. _Ïk‰‹n48r‚cêû†s]øZît+PÃ73ß6DXY¼«ë·yÿxýã{îÂúçÐX,P1Wˆ>€Øfþßs§=ë›^¸3B÷µ<ëö>`’C*öê¦X! ÁUë?>,²@ŽR8%žÜ«*%ö…º‚8¢¹‡¦Ã*PtáHØÇÉÄPy.m+w¿Õÿ+‡ÍÕ:(Ž¡êYªB”W³eG@„šÑ&½¶ÏvÏ8§Ã¨9ĶL.k‰!É™d“Î3'eT…m›†¹üª¤ŸÇ6”ÌæàgbµïvÚQ@›Áê^ø*§Ñ$q³Ii߆UòÈ Êצ1ÐX°¾æºe§¶´° ;<²ÚVŸ,j»°‰@×/­íx5³|@ÊÅþùû-õs„†·LJš‰q—6g[ BZQ67þ/É;LjÀÖNǬ"^ÿ­O&¾_•æ‡rhl'À7Vdíîo­ÍÙvìc¾Ç„Šp“Ãs8#ëjÜT÷ŸamïSo-ŒZ{ìír»}˜ tÓÝOùaàA¥(ó¿ÿ£—¿Â>鳿ö˜Ï”,ÀãýöȘ}óË_1݈؎»Ì—Øí'½ïcõ©öϾã\TÕjßø’oµ÷ýñûíyOj ú½á-?g¿ô«¿6}æ¿ñgì_ò­ö”g~ƒ™™}ØClOüʧÛ?xÉ·Ù+^õj33ûË0¿¤ºwÿüÄîxÑKíËžõ\û½ß§}ôG}d\°_ö¬çÚSžùûî×ü=å™Ï±?x÷Ùýîû!Ën!t|#Ws6+kgž¡àœ¶xæÏ@[iÕ):’O1ÛÂçÓ™Õáç"M ˜¹`ÉpÑ­Àa¡bÔ­½kâïÇîkÊ0Ï£*Š©ÃsÂÅð*;JÉpÔìZ¨û¶ùÜ×*_ÏÇσd5ׄ×(gÝÈúG¶O2eD¢LVAÆX˜=äAšÌ3nÅ—Ï!`WY^¢Šr·èÌ’«,r€bš'²<Ĭd`Iò&d]èÖy´þƒÈÒà£93d£ºõ(€¦ä­+Ëirø¾päò¦ T£fµ}JRwUãbÊBZ¬ÆÓ|Ï^ùú|‚1mk“œ-Ø¢ž-®;dŒ¸Œ)Ù_šk²ìîá*d^…odpÎÒœåéu~þC0+3:8]qÊóAÉÙ(âéšì³l͘éDy˜Q˜ñBn^Lgù ä ånÖ368ÐPa Pv–UñupŸHÍ?‚µõmì]z¦”ÓÌ ~LÛ3i $™Zhƒ=õ(þm’•á º²)ùhÄàzÛÙža>0.Ò!Ás£¿¨L G8ßÏ´}-;½!°@І¶à\S¨€7 Ðl'eƒ£Yk5«£¡Ø¶6KµÄ [ïq,ð<Öz–õ¡qƒ7 n‹°T.,x°_ ƒâÿ°?ØŠ™=ïOµÇö£í¿Õ«í—í×í“ñØ—<ñóí“ñpû‘×ý”ý¯¼Uv—¹KïÛÓNÉÛúÉÿp33{Õ·¾h©×.föoßõîèr ûÜ _ñfföÎ?x—™™ýÞï¿Óþ÷û¬˜º+øï½â•çl"Áì|øƒlÏæÓí#òûÐ?(f¡V>\xsÁ°àvÅvÃhEŒÇp5¢äNÜ¡æ´ ÔT3QÈŠ0 Á×`€ªb·0w]Ǹ8W`Á®Â8¹ ޹E &Õœ²J:…¸2X¸ÊÊCYU™³x[Iõ0¤– (nÅúÇm@;ovׯ_—AµGë߃lÊC1šÚZ³Þï¾ö‚¯º=ðþ÷·?ºó½öìoz¡ýá{î¼á™»v5Y¤ÏÛ® uè²Êê`è8\<{·EP;0/h‹p_5wÅ!¨êýŽ~†?_É0Yš¨ÖÃUÖØ ä«û/7†į²~”|må"·š»SɃ"çƒÂÝÎÍ)Ô“*רþ6,¤yqæ¨ô’í«“{VŸ$fȤ#_¢~ IDAT”.˜¶b‘äÿÞ¶-eùhé6¹°E(iõ¢œävð{¾|:Ièì@ Á€§—Ø·xÞ­t`gûùÄìÈÄÐà`cttcÐ3ɪƒy3|?II.Öͬ-щ®AI_’ÅYöè¬ìàÆ[cÃñI€ç›bm·™þg$êi„¬žËP§ îpÓólÃ8® 6K׳Af† *ÀMYÏ,†D®;gçŸ?çõŸ–È7¦XÛ&°Ž7€ZêĈ©pa?>­ŸcN¯Áý¡ôÔq \8ð•ÜÐ#"ñž‚Ô }ÒðÒ¿÷èϰç~ÍWÚ/~©½ëÞcÏüò'Ûw¼ðvŸ{ßË>ð'j¿ù»ï°/{Ösí‘Á¢Í‹$;<”¬ìBýëc?ís–’ž˜ÙX7¸øùßRž0ÿ0Ûx‹¼–RŠ}òÇ?ܾã…/°?~ÿûíõoþ9ûßÞùoìKŸøùöayð Q.`±(^u«£œ‡¸+Ë3)äA´oúúgØî?{Ï{3:îÀÝM œ©ÊÑ1± ÀîL Š]ÁPD¸bËÏB  ‹@ª¨GvÆ€]òYžv éBÐVÂ9TlZ50ghW¸$raqÕüÞќЊ]• ‡8êsxãë΋¿”IÓÍzé¤Á²$w‹ðQš…‰br?C‘X¡bIf‡–ÐÊ.:$zp:dõžÙ¥ÞÏh o+ïdK]ôqNöØþÒ6vKÁ¬!ì°³ŒØ oo&L¦Üñ@+inÊ ¹ª5(,Y­ÿn876ðõÐ 1XS·’szèÄáûÛCQ]NÖš•SMlËû¬L縘?[ë!7«»¬Mò»˜IÁY¸4g»Ã]5¤o˜|¾Ãm79 ¯‹Œ£=»’ìepé~Ä÷žÌjåõï5*1ø5SÒhFáëÜ áŒÑét±Ýú–ÙÕä{NLk·ÅlÚfÿº¸¢È.`ñɹÏ}ìgÙOÿ*{î _j?ñÓo²mÛì ¿âi©`VnÕéSy9+GŸ_üÕ_33³¿ýþëÕ?¯»Ü6û ×®M¾÷—Û–²&Ôµ½‘0²’Mx…›}áãg÷¹÷½ìŸü³WÛw¿æ‡¬µfÏxÊ/»Ëìf´’µ1¨qke>¾,éYÉÎø3”#Ö3ûÁôQnÿ®²¨fVKÉóð÷™ÍaKè˜>Òú«5,^<“¥ )– 9 ²%–îà¾x¦JÌÔ1W…ãÝYÿ}»}¾lÅ8Ýõ¯:çêuÛ¶Ùµ}ýcÃÄ·ï!z =ÿ™O ô¬¼ÐÞ}ç{¥êV1AÛÖÂhÅ$w:îÎBñ’ôú®ãG­¾eçÎxHŠk¿“¯Ê6ñJ¹ç)yÚê˜ñüdU`»ÃNHBÓÇ÷•gµZÿ«&ØÊ DÝŽîí«gm—áˆE†!gVOvÎXØ·mïö"›cÙ*:ÙCp‰"Õ3EzÓýÀ¹EµÏ/çËZv|c‹mËÛÀ!¯°C€‚¯s6+š?TtzæO²Gi3Å–á­Ézzÿ‘ÓU­·Zû scçFœÒ5çkÙ#hj$•Öx i¥{C’•"(ò÷&Ùeÿ|ŸsÏÿ0Ó3$¸™9 7Àùþ¹./K³Eaàk³‡=4²6>S“Írψ%jÈbu7f(ƒ-rðƒ’<72À¬":I*È÷üÄžù£Ì¸àó²„hž¯ì™B§z¬¯ ˆV´Ý<ÎÖñìòë T}Fáz;0Aª°QECŸûØÏLˆ‡åyh^±:G8܆ßûýwÚýîû!é¡ñ+o{»ýý¯þò°Í.¥Ø¾âei ­Ô$V,Lб.lÍFð*º"µÖìÿø?ÿ/33û‚ÏýOí¿þü¿n¯ý®oŸŽ5w‰‘QIöÞ”v] üYûâ0ÿêüª¢›ç`”üAôø{È’ LÍ?Ë-¥y¦Cesx‘«¶G¹Á¡©Òú£-9þ>KUG_Œ2³Ä¹6* veé­ØÜ_ŸkÂî3~Îö(`yWÖ?–æÝêõ¿²\3°N(£d–ê¹Oû*{àýïè]ôžtìñ:YÍïÝ}6h#ÍëïöAñÑ™Å'9Î qé’éf–o+vb•}³šuáõ¡®7f]æã²ž•õ¹º¯©Ï_¹Á©uÏ÷ç•3Ü À2£w£²¸• YÄÒXK×ìž G5˜»°2ƒ+Jè,P›ƒY¡#Ýì(ÄgXæ–¾WÈ!(¡sÅÞþßùêÉ©n:ßI&ÖÎÀFlw¡çï-eð´Þ¬zçžD³ ÓR{¶/(qå¼/ G5vc\¬“Nïo¢Øz÷Å–h&)W3.x„¦bœÚÎ`gÌܸìm0#ešïApÐRŽpB„AlÍA¬h àë´ÎÛP ­ñZa|`I’׬'Ç8`nR™ÈvƳa\§§zB v~®l-ý;ê3œOª™Avà|ÛÝDÎÝm!‡c—1Îpñïyñô¸GºÝñ´§Ú7ü“ÿÎ~ü§~fê2c7xõ9X`ó4âßýš²§>é‹ìwÞòzû½ß§}ößü;ö_ö÷ìÕÿô[ì?û†xíöç¡èÉ åTëx‘,:mQlu‹±ø}øºÜ¶Ñu…ÎÀ7¾ä[í¡ÿî_´¿òÈO´¯ý»_b¯yí¿²{ßû^öÑõ‘KÃ?6<°í`v*SïÁ22´|FÉs,"ày!”œ±M³Ë÷PbƳ.\¬ ¼îȉ‰ÝÓX:xÄrà6s¡Æa² "3‚ìþ+9–b—œ†ë ‰¢2€àbÕm×ùš¹™õÏÙSxþ<ÖÿÑ …ûËËËt=â,¡¿Ï¿ø±/zÂãí¹/z©½ûÎ÷JF‰á#ñˆ›º·†¼áT—Ý/&ùƒ<Þæl 9–*Ö‘­©<l” „ ;¿Ì~ؘ †‘æ“Vs„uÏmSî„)ómܸ°9rE\±@«`^–ÉB'‰Ò HIy?V9é>Q»FҢ鸮•ýþOóER&h%…ª"8òïùL‘uœñ€?w Ukµfmb0§Ä‹»¼%ùN³9“ÌŽ¤jnºÐöN=Ïnz;üÏó[âyr w³$;ó¦Tßfëxd…˜e!Ik3oŸ?þžâù¸Zÿ8›äÙFi­³ý>¢Ïâ çxL ÉÙ`\Òüãjæ\¼ý^ï…þn*à5[ïjîdž+:_“Ø *˜Ú·Ñó†1í®•’ì§äëÙ&@Îl¡´.…îÛ[ Á^݉8×ÎJùaÁö–áĸmy¾ˆ³ÆÐò¼ÿ5í¿ƒñ˜)2Êa»'ƒŸ‡ý·¯êú¯¾c’3­´ø½wû«ŸõéöÏøj»ãÅ/³×¾ñZJHVúêUFÂjÆae#}¹mvíâ"ÀHÈ¢|{AZÁÝü•¬ÍmÀ/N§È2òù¡ÄÆéž£4k5<¿2?Àme»êUs¶ VgÕ¡õÏC[m–â]¥©W×ÐêÚXݼ•¡+ey»rÂ}Zm3!,MÄ×ù÷]þÅ×/P5/Å ƒç¥\îÈÒ6[ÈÔlŒ¿Ï¥ ˆ]…á®Ö?Ê1WÃâÿO¬–Þ!³*çSD(íê|à<Ù[^ó=7uýþê_¿Áõ~è}àýï›r;Œ$,,£aÄu•”ð*in…Ns#¦èˆ™Y1#wyý/ج£Ï;bíŽ2~Vïs#ÇPå¾M¯oäPÆÇÐg$zOóõTCÞ†²2“~?Ât¡hTÃÿÄÚL28œ¥!ʃmµ™Ý™fm`['Àr;UûÀ©&{ìtIJ×!Wf’‡Ázê8Ð/ Œì­XøµPJ’À¡scÏj6?H×_—(©²Ù^¾¨õD¯/“ †Ìú 6 ‚(ôXš%hÜbyÚø;ïÝ|¬Œ‚QèÐÑ€־ܞa¤P&`4쀩µ=7ª“¬n6m Ë'¶õìšw­ŸÝ½!æ*¦i¦¨Ì'6ÍÑy³¤–˜ÄûŒß¦cëvïûÞ˜^’Á¢¡[·û~ÞWÝÙ()Áì Eß?û«þ®Ýñâ—ÙÿÔÏH}÷J§Í{N¬ç,”±|ÈÁIó‹Ä_ ¨AÞ‘‡íØíJH"Ê>C„Á'OhÎ94X|â¿y6‚»ŽÞeW,SCù+PÂÌ?È•IKš”u6³>lÊÓGºû#>°,yCF…¶ºÛ²Zœ³Ä@tOóßQ†Tª¤Y(QÃb—æJFˆ²@ÜfÅØ±õ°ï“o?üÂÙ]ÿ,ûûkýs«2á¹(6Ÿè‹õa½7ûÕ»Ýàú‡Â‹äol…» >\ìO…ÎidlÀ(%Njܞ£ã{t/($çSr¶ °‰Y®«¬²c¥ Ó#7OòÔý÷,í±\<`ÇZR>M=‰™F`EЪ;À^èx"<ÕŽíæìË–×¼j™*ë b˜@–†9C<ï“ÎÇ>o²$ƒÖÊÁ½ż„»|ùuÐ2ÏÇç?@f–,¦A‚¡¨Èªð\NÙYa°ç¾–†äol…_ýé÷éó ¥s|Sˆ€Z£LØÁ–`*Z(c(©BsdQ‘À©C`«Á¶{ãh€¡6Ù-Œ‚¹3fm‡ ö`Ʋ\=0‘©T°Ê뜃yýÏvÚ¾ßîøVw£ˆ³9‚¥5‡#ɤ̲վõ\ÊÌ¥9Êý5.ŸÏÒ¼ÿ©~²ÛÄ";ÂÈFà°9.ŒG=áoMó J¢Âźœ÷ÏòŽ5K–80RɘBvq±',—`sü³ÉñïwôâAÿˆí½Ûéâ"Âb¹{®òj˜ ¾¼¼LƒÛWÑÚêOU¨ŠMR Çj&ˆÄJ×Î’5>ÿ|M( 3+àâR/–èqÁ LW~íªØA È§ªxæy"ÿ 71à¡sÎÿQl—*y®I¹©±;àÝYÿ*ƒêÿ ë9vÇCˆûªÖ¿:ÿ7óUAZtéÌáUëß((•²B”¬!ê¡ænž%§6.þÙâ»éš[Év\ÁÖñ9GîlJ{HQ@ˆÏý«µº7«ë›FÊ÷âY$Y~Hîm*6¤ólBÑnjˆ *fŒ¤p­gŸdú¼˜•èóÌRµzv²ƒ :˜&*ì¼ûù?“q8Î!èéeïôŸÆv$w<01ˆû²2»óW2?ˆy Ê/ó °«îÄØpXpÛ6;.öÂ;Ë>'D@Ú…Xà$Ç»BËl’b•ôLÐ~}o,œ Šÿù’Ê’8ñ{J2ÜAÂèæ ÀÊ×üóC¾.rÜ8 <¤óDHè\†7ا2특mÌYG(#š|ΦçÆ—ßÇz» ì`îÖ”1U’„4êž–ÛôüïM+oº%)\dF%·ÆÁÅsüvA܉gëâÕÃDr—V¥hãEêzþ²r0¢¢U±._»8‚¹ — ÞíÚnìÿÆ¢À­®·ÖìºßQäíïŸôï”÷Y'+º¿Ö÷ Uvy›ìRižCYs#“±”fk¡Š žÕ`ɇrýZ,Ÿâ9,б[ÏœxݹÅ)b¶ ç,%ΣÂ÷TÒFˆÌJá±ÅóË@Öÿ_¥Ê+VŒ,>¶<ÓÅÎs·Ãú÷µäç–e°þWàÙFÕ¸Y¶ÝïÛåî’x:E'pÛZzXž'8˜‘ÇtM†.€‹dÀ ø8xb \!Æy%³ãc]!(’g’pP\1×W1ÝGnoGÆØhPyoü;G÷»£ë¼·žÂH}§[k`zëˆf ,¹6:?q¹XfŸðõÉŒqâmÁùY–±!óÃ,‚º˜ç!KêTÜ(K–úÜÃy}Õ, ÙÏ)œ;á¹óõÜÛîoÎÔÔ:fIqsi¼ž°¢.µZÛ]äj!ÆÁf£ƒr°VkWE{¨ô$‹+¥˜ùÜÍÖÒŒX€àŸ ñ†=¶½´žX–Bîjx&Jÿ'Ö­¦‘€ßà÷r&’³Yñ?Èâ0Ø´µm’Ç]‹9"ü¼YóO%»Þ p×xt¸Ñì»Busx¸ŽÓŸÍbЛØdIµ#.†ÈŸQkBm#´³\·…;wÓT6 Ë\æã’,ª9±ÝÿíÝZ5ØÏ ›geXVsàXá ÀNÛÁ ³ÎqŸƒ§?»~]v#]n×HÒƒ.hʉˆ3q.//ƒÁÒ4eÿÊÅà Ü¥øž~þÔlõJòÂûª˜V]]+EéÖª›ÊÖÍÊJ Ý+ö„ò øµÉ€t5£Þ[±cªXaÉœ² WÀtõZ yí­¶ùž¾þÙù¥p¸Fùº^X\§õ³M¦óú÷<«ó<ãv¹Á`qnt¤€EÌð0›€7êó+½ï7ÌÂà3±RÒ<$IÛVrÝtà.Æ9À}îQ†Öê^µš¹<šº!y_ÏVÒ ÄÀ| ÊÙÔ\OI… ¥¿«,ü9‚žžßtÎê4·SòìÀ´=V&—SÜ÷dÈàô1G0Ý3IN—Î'å¥Xgj ¢îܱnç绬í`i’“C²m—DQе y;šR\Wvã;h ÕVÒ4d𠬑pƒLÖÆ³|t2` ×›óÖâØµ¿©•ÁÒL2\<Ž&˜£óÖ45”o=äjç—©É“×ÿ¸ˆÑõÍ ì³¸c9~Ôv€Rwé™Kàp ÁV–”1ëCÇ]é:Ê£{l¿ÓýEͺÕÁ–à!§7”·y×ðÔÔ€¹:x-ÖðÌØ•bÙðêva‚¸ÃÎÅ9SøoRö"ß;èÜIöÔx5¤ÌÝ8eUʲ³³k[-%ævðaг@Þ)I³D$‹sàTwKm"Yö÷Àδ*¬xÀ83¤4å  x®Y‡•4¥Ü¹äðSdÉ®_XÜ"ÓÀWV±x½àu£,™™9Á¢›¥;~,ýÚPÓÊý*ž¹R34l½ =EFâHn¨öG%Ô#CLϤ…1ªâO9½ÝÓÖ?ž“«Ö‡·*Ó %½99œXÿµN¿ŽÁƒT@aª=£ë˜B/¤IER;Œ“1ß A:×ç€W³)7-­q<Û°@­‘SÊ÷#0¨Ö?7ŸŽ²ŽŒIÒ{T’dAhèzšÝhÒÞÆœL›ƒCHÀÙ2ËdàÄ'3’¦¥/Ôy&CN¤—¨ÞÂ9ntÏÏ,‚.6 J…ž-ìÆI>¬fµXÉÖàTJbGG¡Íf~Û°†f–AþÙ+ÖÓ:+”Ûe+Y8±T>Ü‚²¹˜Uò÷0˜+Z4bN&ó²Z ?£gW00ˆÐÎFQ•ÿ^& Ù (ÅþÔ`b×!có&щUßC3UØÎ!=éTâêÎPENO-J×däu[…ž—ØÕlÞZ?|þ›Ñó¿Î0”~Ž"ϧf–”e²‰½%l>LM¦ýó+0I¸é f©îñ3A.%¹víZ*¶=àÀQÇVÊè¶Ä²#›Rî0¡ä‹gð†…®øï© “^!—c£ÙƒòiÔ¬Ês¸ˆZ±#+ g´‘æ~ü,|¨('²U÷_…X"ãÀL”qá¯äUXô«íây.~Œ”¼Ð™vÔ[9Ž­¦Õм²–æ÷Á}äYeå‹ „;ªóÄLˆr›ãÀPb8sO_ÿxdá~¬Î5K V¬ÃÝm2dô«×Áõb–,£yÞ ¶óŠYtn+gD[E!%³KÙ]fËàÛ\ØÎ,üUìªÊ*_I|Õ5¦¾wUàé‘£ÜÑ<†{²i@žÉ@H…Æûv³@¢\mbÈÆ8`;NÖz›Aw¥mžIÀ"+¤thˆv؉aÚCNq¿¦snÙí ÃXÐ뼎k**'æ§µÙ äž|:ÍëôùTMññ:`q”D_ŸiŽuMF)äYx= Iœ L¶¶ÏƘíÈfŇø÷9›¶Ðà¬Ö8AI‚‚²*áf¥Â~1ø­€s`&¨—¶šgëpF‡v6=£ƒdgÝú²´µ|O¶×ãšS÷žž\åØi¯$'½2¡!“¶Ä2³}=®y?歷݆;ßw¬“ʤísÁh†RiT8þÝ6LºAñ€:,JÃ…æÔp‚ ÛA¯:Њ)á9Ô˜£­5ôVèXžÐÙ‹º¦×..âµ—Ûvž Úu¼«< fQ”¶'›#0;‚Ÿ± (õî;vs÷\Í CÃû ôóò¨¤m,yü޸P¾J²ÆûĬ„êæ#£´rÙ['Þ畳î“À\|+Ùb[ÍÁ øb†O}Ï4áïã>ÜNëŸG|ž5ÃÏÁÐÙ[‚Æúï´þÅ, ³¶ ÔìÿŸön° cäYœ&îuÊ­‘lïÈøðµkd­æpÚ‚a¿æV±EW…C_Õ„Z'ª`bC볤 «pü~.°3 ,•,³ÙYšÂÏÁ™’º%F© 6§P(eHi`ž‰åq ”ØÅŠ¥Üîë_9Ò©pL–Þ*„àÃ] nX¬`ç¤oŠ·nÚ$À,‡¤$ý &es,©C íz*Ø<†¯Ý#ðÊ„ce‘}tž”4WÓUîØUìQiŒrgÃtöÔ‰m>ÎÙd ÅŽe§(œ!*&@Ï×X_‚Y–5¶?$n%;³…·‘Dˆ2fsØô`šê–d}Ó~÷p%Ëmîð¹‚JJÌЭõ̾8CvUÎѺ5?iÖo¸ñõß×,g©õª˜B,VÁïúˆâózn]vW½‰Ø9ùÃ<à4±3ŒbávÒf‡º!9ëFtà$úú§|£P:æ‚Ðä™™qßF»íž­Áðì’6pœ«ð: ¸Ã1§äèÆ2Y£L°¶¿®Î LÑö“QÂrý÷`‡ê©îç³äü§0P¹M@:0j]É”x6 ?õQëjæ„Ldž+9üÓ&ÖQº^jɹ=ƧÂçx €Ì,IÄJbF 22`Å^›©L!t­c¦ÅóoÆ%Pâïµ:8a“drÊ‚5¿Î3> ²Š”#^k=ŪX?“6øÉ`¤ˆÿáÃLÏÔÊÁtçgzò·âÎ}%7Yü¸-ÂR¹Àe­=K¦Ø Cñµ8 À‘•+‡!*iÔd£|TŽŒÐ8Zpê%¸»¹e6£Ø'²b掿zhò±sÀ‚ÀC áûëýø"à2³dý‹ÛÂE:J”°CéÅ¡[£l ‡°ÇýåásÎwQç˜Y ¾*x\ ù6ù-”*@Á9?ߕ¡ Ü3ë.F`S¶×im C„Ttœã»:«£@ÍŠýÅûÏÊÚzÅD*Pv$¹KNlúZŸܨ›;¤N³V 燂 )–m£÷×·˜ÝèûƒÑÒ~ÚféYd™ì•¬:ŠûxËm(›ä>–:™7Xf¹W®±§ IDATâÞiÄž9 ØHétøzÂg ³/¾þz9>Ü®G Up)5$sYîhyîÿ³ Rº ²¤øºW"°óÁü´Ÿø¼õýØ»ûg°åy¿ŽkI–Ï wÕš˜m,î} ¸ Úäè_Ñu¯S®“Ù˜-*I¢æf ,) ©Ú¾Mn9ÖÖuìo)J ë@¨eÉ[·Xÿ> äýt}šSš¬þ-çyEYÐ=ãË_ß¶–ìà§æ †(ã~$'ñ2ÉžÙæ»õ<—çëö¶²Èæ9UÐ(·,ÎÁúòòRJMXúÂ[’W¬Àjh™|Ÿv‡8gt¢ÐÁ¼’ýçž Ä†Áò@÷[ͨù,ÞØ6XO®ºæ8ã‚’ õ oËNÍÈÿá¢]ÉïV8 øofFxXÅ@®Šv¢0Siö}¿p.iåä¤d3†X‚¸rØÃÏF€ŠÅ¼’^Åô¨N8ÿóV¯uPÈk«Ûeý+ #fHñïó9>Áú_­¡»òåűþûbýïò8á©Z q³º7à,ÐQÐkrÁ\2w.Ø@—ÌûÔmvö»J¶¶’3®ÜW@—gÂÔš¿Êü@…ò*0ûL³ þïzª©˜÷‚Y‘`n¨Ó; ª”ܱu†Æ;ÜI ‡À§»ä;³U(/ãðDž]b)Tž'„RÀ¶µä@ÅE;ÚaÒýôL@i`°gÍÂKAäXaÖV!™Ï­tfkv qX<ïD24”¦úödu£lm­æƒÂÜd&Í’tª´þ.¶ž“+f#ÿÇ J¥ÐÓd#F8ØÃÙ±Å~­ûûõÞf[í ¬–<ÿc9ƒ'Öÿ©‚¼­¤£åf ~Ÿ×£‘¼N­ÿl„• ³¤ÎWKF …T9C×Z¦×ti)gxaÖ2¼ÝΆ'„&éÍý!³ƒŸ‹,‘ÿ¼¡C)J÷lfÿýç÷xÄÃÕ8ÜÌìË`°³zWܱ°ˆT’§Uz·*šÃ 2<ÊàÂ%fb¨úÒY—8µÚv»í `®ƒ¢æ*¡^™*8«ÃòœÑPÌŸ<æ^üyÌE-3ÄP¬˜,•íãÃöê³#ÀE 3Vêu¾M———©¸WntªcÌŒʺÐ|¥£÷}çcÅLK™ÍPÒGÎ;Bcu)öY;žÕAÀr;¬vNd ‡Çç´xýãqºLÐéÄVÜ– b=]œâ¡Š,PXö.®ÇÕú÷û_Ì;.2¨VöØþ@¬j(Û†›fØí.ìðodýõ:žÍ[½çŠ1ZÉðX6ºZÿãƒó*=8ˆë ¢ñÙ™ZC"SÝ©ÛRµ¢eg>·Óz Ìz,iÉÀò¸’®Óù#ç¬B¹*óŸntΦ-ÂYJsÙ‘GÑ;Ë1Ý6»­‡tL̈™%Ƨ›YÛ6 8Ð&~—¨¥Ï¦ ñ,!ȨYïÍyDFÀ©€Œ®°!»Ò:¹g“€Äž‘;˜[R;ƒ„ï‡VХ̎‚¨âSù8i&R¼®Äsq ©±hd|P†ÆÙ@™ñA…º› áøì2¿ä̹‰Æ÷M`¯’dµ§6g| ì[9Ê(²B#(v~þ§÷íÙ¸?ŽG$ <lÞ6r8.6Vºi.R8i»uªXóN+gð€÷ê¡ÈvÍøþ'èà*™™¥œ ŸÿÁêåýð¼Ê%IJ"¤ n.ˆ¹DÉ›šÁA¹[¯’Û%àâ”d@Ô{ðbv Jé”$ìÈ~˜;ºlŸÍLxŒ yq¿ÊÌQRd=؉Lͽð±ë½øbÙš p÷š÷‡åt|ó¢‡-ÀY:†r4µŸ+™–÷äõ_o`ý£…6»Ùñ<Ó­aÙ[<Çuji¶°”bmÛâAÃ*Șl|àMBŠö×È «÷èTܱ‘ o[¥"ïH†©Ö¿b„Wî„W¹*–Ye¦­\êV Ìeh1äÀ ù䢆Œæõ¸œŽÝ(8µžèâ`D4#@`t•Í»wše¶PÏÛ‚¶Ü©#í÷Šv|üøÕRÃL‚ÁQ–’iV,@KÍ £J¹ª)¹±t¬³”Œ 2ìÌh´Ýu®p…L9”²¥g(¬#$ù"ƒ dmiý#‹„ $ \5.Lra0#H×þy‹ò®¡ÔÎB‚vþÛÎN¹Ô¬ÖK(`ªÀ럇%çõ!ØÌ::Ú™aVÑlB°íR95e:¸ægéÜyýW§]bß[ë¦ì 8¾ÉZÛ:o€H÷d^géhº»±˜‡®%¹½gÀxæ‚ümC>}OA«„l*È…ªš9`½=‹Jo¿JmWrìúªl,~°0»$£vXó‚åAG7ø0f;f”âàqdÉK·pVù2Žxp Fîü+)˜²ëVy9,OÃsÁÙ4,ÝÃãÁl RXÐó\ ‚!Þ™x ¦¢F™W%Ç_P×Ï*L%_øwzÊE®²\æ÷ÇßQCÊÜéçyUÁÂÝd#«nîJ®§Ž»êóµ€Å¿*äù˜(Ëã£yž#bf…‹i~À(C„Õ܃Ê/âóœ2[@†óT|½±cßÿÖÿé&Öÿ­bÛ9ÄÜŠ!/è$ùÀY¼>Ü-Ž6ÉšºÐÓZ´œ”Öÿbö&ÖJ77Ùu?2˜ „RI×T(ô‘î*›ë#ÆŠåžÓýËt¾P0(b`™]šBS,YDO¬t%¦ØŸZkú¬i^ ¤1ì—¶gH®äà‹d/>,W28 }Ä¡ðôšB3QÜáH€ ®aR˜¸^ y#£Ü %vºÁQsè)²©¬Á¢@O³˜Ç’COÓ¬Ÿ ·8Ãû!¼g'ß, þçwi°1IS³2#Ó§dÉ\Ö@åú¤Ü “†i.€¥ä gä ׆”Îgup*{h0¸µ|œ²SRTÍ9@©ÉÁ ‡Ã“Û¼k©gðÿù{:[”îW6»gNs獊#Ö­Aàí~Ü¢Á¼³r·•;œ\ÆB‡]Á8”¿ÏºzfLV®(+Æ…åb\h+ö‚ IÖys±ˆ Æ¥VØñ¬"ÛÅ °àF&‚?ãÈÉkÛ¶ÖÈàÇχšZ]ð\°ãï®ÂY¶ÅÀ‡ F&ÜùWŽc,äcwd¯Ìà ·‹>+›æ• ŠÊÔL3x­¡Å¹ºYªµÁR®‹‹‹$©Ã÷eæÐ‹g³ÂÛøÁõ¿^ÿ·‚i¿ÜÙ¹rHcI™Q\Añƒ¡§ —BÙ#©¹ÇÍ 13¤À oϨ™¡&d¹+Ëk¾wòõÉn„+9߬ɕTyõ \)%¬ ¹½ËÚBB"lžñ6аÐzªYR!ŠÁÁÌOk †Ó³b™Ö@P‚Ÿ?gïZs^PËâìâ¦÷± $R. èÑX(8CôÅä”H.mœ”Î-ZKàÀTd_<ÇÆß¯Á⌛®Föÿãy‰×½Ëô¨‰Y”,èd)vÏABCž}1`U‚9€\,Œ¤¾¤0S'³gÑ.ì;àhI–—ðWº·Œ3:‡½Âq¨%Ùbµ€æGeÉ '¾t¨çZ¿ó5<„øjÅ.[s:ÆÜOMd¿õÑ¥Yî:‡7£ôŽ-íñó«ƒ\D'x6Å|¨Ý_J‚]Ö¤3‡p,ÒV,téR¶Æü`QÌÆQG‡‹,L¹€WL‹r§¹8bˆÙ U]Y­r°¦ê<^2VŒË¿Ø>WÜh%̓劉aæˆêù8ªb“ÁÞQÖ® qÎŵê*#›Ã’(›œ“Ãlž? öØ5på…¬ o>ŸÈèáïº >ï }\ÿëõ+¾N˜Û>¯ßZfvÅ,Û[ Ôt¬›yi_„LÆ­·; _¯òw|;ðµí@’v8Ì®MîQ”;Nr䨕ºÎ6G 0›41bl.Øö/fœ gÃŽ³7Ü#ÖDZm{ø'fíp()JÙ "çÆéýͦüžBú4£¤žÿ´Ÿ%±‘f¢rs+;Øèaë ‡8e÷àG÷a¡ ¦CæFg Ú†’f„Ò2íCNg&2!ˆØâó1#¨Û`iøzRœ/27qhas="Òæ“ICzþ‡¬½€#^ÎâëyjœBÙÖC… ¯³ùDˇ¹f³˜pQl}ZcÓ=¾ 9¼¯4:Iņó2„Ûb&èÈÍŒ»¸<Ï¡4ÚXÄ"S W…Ç*ÈRu˜9œq^¸ñP}/öËÌ=CÈlÌñ ½¿Ž»‹XTžN§äú¦º<ø«2[”¬‰g'°°V{%ÕB72%_Y¹Â©Ù%îòrÁ•G²¾y0ã⬠Zxóœ çᜒš3áâ‘·‘ÁWdô¨™™(ðm¸~ýzš?b9~Ÿ×‰*ÜTðì׿K¶ÎwåkkgÀSJ±z:ÙÅ©†s[bqp‡$m+)™¼ dm§õOŒ˜ ¦hX¸à]9YªûŽZçGëŸÁ½2E¹j–SÍî…ÃÙÄNÁö¸D%Î'YܶÖô¤µÀ¶ÖPÜà¹Ú.·áHWKÊq†%ÒæÉ5Žœ!@¦*:Ç]³ine3A¸ïø^x,Ü!±SœY„ûm€_Ëä“ç!:3Îæ,Ø¢ßG•aªY¯àEó5WgÌê8 ks˜jmÌ*1óËR»I²i¢Zl›Öáxç ±QÆM€‚6»ÂÖžL{TòÉöäeÆL¦dk¼þÇ\P­§BN’µŠ÷x72 «"t9œ×ÿ¼çy;Ká«‹¼³ü[£æå©N¶Ö©)LÌv¹…$¶ÖjV¡1ÓÆ½'=›ë‚µNÒËù~41´µPt£ŽÛc&ˆôØ8”ž´´PÐ`arEhã*»†µ‹<ÀöÁª8ÆfVû²>r Rs î,çÀÈ­²q_¯]\$C&ð˜* Å Y5cûR>¸Éúg¡)ý»­XT3ûöͪƒ| džg_‰YÙèrQïŸë30Èš ¸Ábž‹G¶©vF†g’8UX³aϵ0(å>—¸68„U±'œ[¥®eÆðÁõc.awëÞºƒì‘ssE™c ü,IØ„Ëà*esÜl`Pä;høµ¤oW…T¯˜HvW24u\*V+wÅ+µ:Ç«€ULq¦[ 4M2°IŠÂŠú´@•Ís2’ýç¹))Þhž žIŠâ®ÛdŽ2§®Ao·Ý:˜Œ’ACÏ`+ò… • ²áÔféÆþah)àýìЕæeÜ*öd!Eæ&þ;œ`v¼Á·[e#Uw€£MbµÖì·˜¯ë؈³<ïWÄÀKRF`cà o·¶´Vz4µŽÀÒÞ†q@8Á5%³˜jm ögHâü÷aÊP´v­àú¶ƒ³ÙÌ)d{cýïÛ¿3hal×A)îÂu¼ívÛøsWcýãg•ÉJûÜ$lӽ夾†RvÖjý·á×wënœB dE0Å‚ – ¾µõéÙãûë¦\oÞ ˆsI”ì€:“„€˜%Íp‡*î*“îsŽ ;d!#àÅ»[)€ÁÝAèN.SÎ@ÁÐö€Õ&ºŠ•@dV.jJ>„à€ÏžÅ2¡Í5;Ʊ›*`…Û‚  V¶$^±1ŠÑÂóµm[t(?ây#À‰½º.¹@Âk†;ìwî?¸þçõÏ_wÞyçò>ª~v^ÿÙÆ?®ë–Ô²Dnž_KöØ\ªX¯X¢¨Î®a#÷g5ŸÅ–Ù1¬MÒ8%7SLßj>G¼Öø9zÏ_­ÿ¸NZÎûaÖd’lZž¹QIðP’3Ìa±ÄA‰£ð©òefS’á@¼b ”L¹DÖÓg ^Ûžem©‘ÀŠ(öªäÔyd±ÎïݲÃÊXco½§Úή”½Øçü 0P¢6r‚L*æY¾Ù<´dYÜÚbÍSo’f‡:Ïcª0>9ºuvKNg±&Zv/à  ²KÈc4À†ÏÝ[â…z©‰Í,ëÌF9ÈB ÞÙ›g{”îF—§ÖÒgbhª’´Õ}?ZËv؃(¿”%Ûãu¾]•Ü x3<ÈÅÚówˆC{l\;!+e—Å buí°™î' ÷g»¨af·âÁh¡äUØ£rãx¾™àï(ùÖÊò”%Nü™,…ân>»Xâ@¹˜G§º¡ÛN-c'ªTnƒb|£ðNÎ$Qò œ½QL~àzÑûºrC9:i­¤V\\°5³D\¤(iàØáG™ËàØÔ€ m/>}V…d«Alî<#¸Â‡‹³P¼í«Œ“\Vί5oÄ G™k0ˆB÷ ­µñ{| ?¸þçõÏ_¯{Ýëìíoûôý·¿ýíöº×½Nj¾± :›¨œ  Hg³m!ICÉLÔHVÀJöy?x=“)¯ìB7fª>ïhý+ÆýÈ~}Õ9åï¯lØS¥Aþ–5ýg)cM€–¡…TÍ ˜dmÊ­·7#eè|FÀ~ksälU¾¦údËÒ4E Èpê峞À‘u<ï>-Å,ɧp'IFýÙ aÃé:V ±ËÔb |p¹2UÏì°¸º¶nOš{L"’àEæ ÈÍTIÁf{›~Þ˜ RÃËgBúx%FVÀT˜êä‡z^ÿ™ísgØaÓµ³ƒ›B×®…¤®Æ¾Ÿ¯ îlî–L¶ZwƒŠ3 å`0¿ˆ‘³abñ`N¼–µES@2ÌíÿnïÝ£5«ª;ÑßZ{Ÿ*ÞRˆP<…* *AD|„¤E;u LºÌÃîa3¨Nr}uˆ7&©ÛmT¸Ä¤c.tˆ¹×dÜ´uètì˜F&­[AI(TuLšX%"TÕùöZ÷o¯½çúí¹öyç|ÅœcÔ¨óøÎ÷íÇú­=¿ù›M®öÖRˆƒ*~CÌ¥µÅϸŸPŒjŸ[⚥ê•C6ìêÃ!’Ý™²DRÂŽWʼ¾èùçãš«çnÝ‚#؈'öÀ—vïÁµ7ü.>{ÿêNÜ/AxÉËRPÀÍÕÚ4{™½æ¦pî×_ÝnL5Uº×ˆLm:®Z™ÃŽyjÈçkËJtZ•‚Õ娉֍Icy8XHA¡ ´á¨òknÖ×z†äùÈž¦ñðÌ¢±¡`š³+7M›ÿVS7b.¸ ù>iÕ‹Rsu‰Éxáë¯ 9Èë%ç±ZÝj⟫,WLOœü³mÛ¶ ;wî<ç9Ïé ]»vaÛ¶mÊli-=ÍÓ°Ôôšºò˜4Au®’óFæàô q´¿&Ã?qXd’ À2ùpﳟ—þ4ºõB2Ùcï[š€FQÙI¹*7žužù˜ý@ý+ATŠŠô¾.h‚S9úƒëóÌî@áR¨Áõ ^²D~b|ÂGr®Dú6ÄÐQo’°B¦Þèòª‘ ¼ÅOS»ëkº¼Êå©§ "©"à]_à ŠM™ú[Hΰ…w¼®äñ¥^¡ª&ƒˆ /I!í÷”ŒèŽ(oQÌâá¥]À“ÄÒßKU9®öËó–ƒbål5ž¨0 AV…ã@J\_'÷H` ©ËAÌ&’ÃP˜óÓQåH²:“Üî* >«fôCEÈ!î]ä䈃›@âõ²‰­ ¢¤`z¥À"œœ˜ùJP©Z )çÈ×I‡ö¢óÏüâeØñþàÏ?þ‰îoïÝõyÜó×÷ã/ÿü»·ÿ<þüãÿ÷ìº?Ëó P­×€›²“ƒ+<9ä±È”†…F…Ò!{M$U'‰.”•±Œ£ÖÎN¿&u-+|¼Ú=ä¬6 "hô1¦i™þ1шL½HT¡$•ˆÕÚR#}IVW“ÉÖz;¸ç£´ŽÓšÑúReK®ït Y(@o)@kPçóÐî›”ä.)þiïÁÁ’¼OÚûh÷L«,ÿò²(ü—LV„J¤›5ípÔÊ÷†JLŠ—ÕžÀ޼ÒûÅë9tu÷ÀbºWš”›ûÆîoæÐS5”ûË4…ËRâD«øi¯+Ñl5:®6J­…\b6{_L ÀÅ6Xˆ®«šdLJÙ_8¡º(è/™$©qqXé²ÃI¤§³Ÿ‹ß?Èû—:+"§õÅ\ð «l9WíHJ" ±ÏTwA^RʓҼ’æÕþMˆ¡ïƒìÅ‘}@ò’ÉŸ¹(€"@‘J  9á9UmxW©ZÅU&@x ¥®ÓÌ!ˆþ;'Î "Í“jsÙ@ØÊgý1à@ÈRg™~¸¶GËåŠ×Yu ¢WFú q@+ë„CB %x'Jkš/S+¤ «I©Â(×LpöB H?p¨ªaOfÈÙ®òå;™íéGGQYVÝKƒ“}åš¶’CÕÙT­‘”YÙÇ×½®­e”\YaJ<ŒË…]ïUãÍI ¢wبÃ-4gAËΧï¯ÙþÓøìß|¡ €8sýßþâøìý_À5ÛZýLmj½tì4%19T”ïɪ‰–ÅÖhNL¿*9àÞ{|é¿ßŠ;ÿ¿ßW'Ðk4,錇pã{wàÁOÞ†7¼îµhš7üÚ¯âËŸøÞðº×ªÙM^zÀí§k¥QI¸]Ò¥¤¬^p³»vÍdÕ UJÕ-Œ•Ÿø<´Þ-@”kG FäýH½1Zæ†ûA4ª ÷>i²½EjŒR…â*’\Ïé8%­RSúcép^“)¨ç*çJðÏ÷•q=Vµy2ñÿú×ü0NÝ|Þû‹oÃI'>}Ñø „Æ ùPM”·&Äî_h©¦¾Á…*ƒ¦{êíQ)¯Â©Ëz§„ø‚g•4VHS ­oG ˜ùµ¥ÀÅ)Ÿ9öÚ±AªšPHiö÷Õ±¤o–ІÒû½ÚQËgÇ¡jœ™îœ-ôBêÑ‘EÆnðÜÑzˆd3ö`6‘T­rã×\ œeÖ:æŽ× nß—9š> ü¨¶$¹ìÔ¯ã(ÈȆ©¦^ÊbGH Ùœ¨e‚ 2aD<¬uMôøÈ¾ºD“Í”ã˜úÇÕ':nù;YmÊ£×KcgI)1ãˆ(àÓê‚oébüük"» IDATG¦ &ƒ‹®º!ټ˂õ>h‰Ävì«9u/Òúè¯Ï0b‘™Pè©n‰zòhÕAÝÃ:‘tàÞ§ÁL.½*ˆ@% €:ªmR‚Cì[:Ì eÉŽ²í5Nª`gŒ%äC»= R +*?òvÇx¸Aüð)õ4¤ÿ¥ršsçnÝ‚îüãÌÙä¦òîücœ»uË€3–ýO@ýå·lÇ=vó ó,)7²?AÎ&Ñ2Õc íò¤‚[‰c>FµÃOe3½Cyh¥ÖðÏ´¦^1¥„iz’n§Q©dÅ©¤ÇŽ, %°ÓÊÇ ]ž…#væù>K/È54»Iò:Ê*Êc9Ðd™Ù¡Ó/´ë¨­í85Å9ŸQ‰Ø‰ûÌ}ƒ#ؘ=И¢ÄÙ{nL挺®3úg„5Ž)#̓Dµž-;¬É³C_¢€”dZùõZ_ˆ(i/3èÜø=Ó×K^ÿh°Ã#Y¾Üë¢QJX:[s`YjùZ>GúÇU*–?ÖÖ¼FÕãÙ4\)d©lÜ1--ÚÐÑ¥M»öòwjsw¡·EÒ;WÿÒJøçÀg-ðÿõo>‚w¾ïýxdï^<}Ó&¼çš·âÄ6uã߯¬ éƒ¢ïQH‚A¹/u峞éLIE6™ëÖ,©W2­.Dòïdö¸Ëö¡—þ÷¤<§ ‘Ö†æjëBÿ6¿«Di+%J4Q-ÉTªÒŽÏ»a%ÇõÎ+?¥ŸeÒPÔÂRP”äq•¡£%J[WrȲ¸]5jÈ*ÊTÝJaçtEElCéAr~Já)=Cj&wù{ÈóO×® ¢ÄõÍŽ‘é¨1fí¾ª2úÄpQù:©æ†Ñ]Û'ý«ï’ ]p¤­E¢¾EzmxΆ´RLt´Qid(‰þ#9è4«"—Bî䬻sJ¯þú‡–ʛŀí{öñc.CB.ÀU¤ ÉA¦Î瑹 Ø"%¥@@xpÞ‹ê’ëÕÞÄ9ç2ØÃ5ï|?'(þ§d"=ÿφ²Êž«¼Šç#2:‘úÇ ‚¬ÿ0柑IÊsµ5F}P²P 1ä•¡œÕ)æ;õ>•|6Î|ijC˜ºÄòµÜÌ^UžØ/|Þ÷ªÍÓéý^üüçá‰ýTz‚VzLÿßþŸþ¼áª×âø§‡Ýwß;v~¨{ßß³»ï¾Ýu;¾ü‰áEÏ?¿{¿{?z ~ù-Ûñà'oÃî»ïÀ‹.8w|øCxð“·áWßú³¸ÿŽ?ÃÇþð÷ðß÷àÁOÞ†oû¹,ðøò'>†þþM£Üó_yË¿Á=v3üäm¸cç‡pÚæ“;êwÞ÷ïpïŸß‚‡îºŸ¿ó£øý߸výc8ÿ„³]C¥j&(a΢j™v°SEHsªe`QrzXÂY,¥ rLn¹DOJÇÉ‚r’¬IyhM†Z*…i½b²º’Ö¿t¼KƒX5šß˜T±<í²úŸ¨Èc’ˆ¬ Êca‘åâŸ{z4üs`¾Xükòå¬î¦ÍÒúÙbŒø§o|ï|ï¯ã‘½{qÂñÇã}¿øvœx¦Aâ¡Q‚°å˜t5éw%‘„&ôÁ“&¨¸ ÉÑ’ï¯Uv8XÒ©z1û,V‡* r•÷j1´êtŒÐíÆ>³Ô|3E‚Š­ÅDGUK—„÷ di‘ËTg}52B \}‘AÀ`°(Wi°¯H X2ÆÏ3Æ#%ã$',K¼!ªsª:Š  íiâ 2{=¨´ÖDd¥0ä l©‚3˜EÔ0I[Sijm£~8Õ¯LáUqæe ÐPU!Š (%÷"òÜÅ Nu­í.Š÷ :¦rf´ÀR+º¤ôýôµ¢8Ƭ*ÓiÀÈg:ÀRÛý±)}JRj:ÃlìJé˜O ÿ±UÝlD/__µHçÃFÌi0hÿLuJ¹|¿> ÌÊrnhH•ÆÛœwI§ì³Þ*rÙþô¼1Û‹d`ìÏS0¾M¨µAj¢NûñúçQÍz¤õWpÆ/5k³C&“ ¾´{Þôã?:Pp’Nö›^ÿ£øÒî=E™è|±õŽý•?ñFü¿ÿù#Ø÷è£xÖeWâÊ#š¦ÁïÙ‹Î?ϺìJ|Ï÷¿pËÁ‡ûýÙbxÃU¯ÅÿìÛ±õ¥/ǧïÛÕ-ŠW|ÿKñªŸü׸â_üKüîþàüçœÛ-ðŸãOîøä_µ“O<¯¸ìüáŸüW<°{Î:ý4¼çš·uçpÑóÎÃGnû8Þý[7â+?ŒK^x!~ù-ۋΈFŸª¤bÍ*I7kyɹàÏåµPšÓ¡ey5?WkXÝMËæ–rMÉ9’ç I-óƒ“+:|~<”u¬×§TIäÊWü;n0çÀM£°Ô·ü^«$vklðÏ †þ9 ^,þ½iv¥À©äà6bÖ×7÷îÃ/¾÷×ñͽ{q¦§á}ï|;N9émPÒÏú«f,…fœ(o)àIŸŒo- Q(²1NÅÿHp0ÀÿuW~žÈ0—±`|,R…Fú¯JB0%)n­â±˜Ï9YF^ÌîPª.Y5C!ƒûY?2¸êjó×ÓQx’ÝòçÃW¿þ©‚Ýß|÷¿ƹ[ÎîŽé¥/zžØ¿ñÁ z?Ò±yÄFüØÕoÁoþÞïãg®ùÀ©›Oê2õ¯yãvüûÿp>ôGÿ?ýoß8íä“Õ…*+C) 4¦ônšçÆuv4x¨"÷Fq–>ѵ$ÍŒ«¥Š’V1â^ùY¥áªÚìœ1z;`r~M‰R(à˜&&¯…¬ŒH‡‹«IR5NRÞä{s•‚›øÓßk½lIPs2¹*ˆ½+&S7űpåz0Wn葪X^Òß´{Ó*ӥׄD«¦æ¦9'ߣ§±A(!JŠ\ꇊ¡ÜÒÜ-¸ê07Â:º Úìz|]K/û™ÿzº>¡ÌuýK.gÚ@gò:¦uŸ‹þäÁ””›NOëzŠÿ~xi÷üñBµÍ *`ºN•Ëçfb>Í‹ù=Ðð/«@é|KÃk³uÄ2Z\z1ˆ7†¾·ÙÍrÀsÞ»ÿSüÂcÂÌÌÌÌÌÌÌÌÌÌÌl1öÜê3‡_%ÈÌÌÌÌÌÌÌÌÌÌÌlÌêÃåDâ{ŸmwÓÌÌÌÌÌÌÌÌÌÌL5÷κ¯­dffffffffffö”2 ‚ÌÌÌÌÌÌÌÌÌÌÌ,233333333333³ ÈÌÌÌÌÌÌÌÌÌÌÌÌ‚ 333333333333 ‚ÌÌÌÌÌÌÌÌÌÌÌÌ,233333333333³ ÈlÑæœ³‹`ffffffffffA™™™™™™™™™™™™A˶={ö`ûöí8á„àœÃ 'œ€íÛ·cÏž=vqÌÌÌÌÌÌÌÌÌÌ,:¼lçθòÊ+qÖYgáÞ{ïEŒ÷Þ{/Î:ë,\y啨¹s§]$333333333³ÃÀj»Ó лÞõ.ÜrË-¸à‚ ºŸoÙ²¿ð ¿€W¾ò•¸êª«ðâ¿[¶l± ffffffffff6Ãf• ×_=Þüæ7g´ .¸o~ó›qýõ×ÛÅ2333333333³ hömçÎxÝë^7úš×½îu˦ÄíܹçœsŽ]h3333333333 ‚Ö‡íÝ»wAšÛ–-[°wï^»Xffffffffff;mÚ´iA¸={ö`Ó¦MK~ïW½êUxýë_Ý»wÃ9‡W½êUÝï®»î:8çºòÎ9çìܹ³ûõ"™™™™™™™™™™Y´j¶mÛ6Ü|óÍ£¯¹ùæ›±mÛ¶%¿÷­·ÞŠøÃغu+bŒ¸õÖ[»è¦›nBŒ1Æî5ÒRðc4™n3333333333 ‚VÏÞñŽwছn®]»ÔßïÚµ 7ÝtÞñŽw¬Úg^sÍ5¸í¶Û²@lëÖ­Yßѵ×^k 3333333333 ‚Vß¾ó﮺ê*\wÝu]ÕeÏž=¸îºëpÕUWáÝï~÷ª$éý·nÝšÑávïÞ½îÌ3Ï´›cfffffffff¶Êö”Ÿ´k×.\uÕU¸å–[pì±Çâúë¯Ç _øBìÝ»›6m¶mÛpÛm·’ŠLŒÑV ™™™™™™™™™™Ak¥A7Üpn¸á†Cú¹) ºë®»p饗Ú*4333333333{mÆépnU Ce§Ÿ~ú€êvõÕWã²Ë.Ë~¶}ûv[‘ffffffffff­¾=™\zé¥]ÿO’Ⱦá†põÕWg=Agu–­H333333333³Cln–þ¼w8~á±çâ{Ÿ½. U»QÎY‘™™™™™™™™™Ùrýéw>xnõ™§^%èMozÓÌ@fffffffffff«gO9a„{î¹Ç™™™™™™™™ÙSØlN™™™™™™™™™™™AfëϬÈÌÌÌÌÌÌÌÌÌÌ‚ 333333333333 ‚ÌÌÌÌÌÌÌÌÌÌÌÌŠƒAfffffffffffO-³ ÈÌÌÌÌÌÌÌÌÌÌÌ‚ 333333333333 ‚ÌÌÌÌÌÌÌÌÌÌÌÌ,233333333333›-«—qï|À™™™™™™™™™Ù‚f• 3333333333³§”¹-ß÷²Þ{Äc¾È¹îëc÷½s1F„àœë~B@UU!t¯Oï›^ÇŸ“¾OÇÁ¯•ŸËë½ïŽ!ý]ap|ìòXÓ×ü·Î9xïÑ4M÷;ï}öºtÌéwüyò³´k™~–ÎCžc:¾fÞ{L&“ìóäkùýù³åñÈûXUUvnü{y­ùØäñóßÉóLïŸþ†/½gúYz]:y^ü:^¯|Þé}œsÝ=MïŸîsz]Ó4Ý:æŸñz’k†×™víyÊëUUÕàºÉs”×ZŸ¿ü[þyú[y¾†ÿáÿpÅ¿<&TU¦™´çV!ÆPÀ¿_ÿ œóâZLß{zâðÚï«Eà?Ì*þÿá…ø‡çó_â?}í劾€ÿÊ{4+Áÿ¦ÓÏÞ¡ÝÈt£å›&Àñ&Á€(]` tþ}ú<í5é³ä…M¿g°°C%Á#D]×ÙgË›ŸŽ•7¦´Ê÷”`¨ëZ½&Ú{òïy£Oÿóµâs×®¯¼.é÷$ìÉÏÓ¾O¯õÞgן]nÆ Lí|äÿ¼n´ C¾>½?¿N— ¿¯nz?þ™ÜµõÄ×Xž›|pVU5xP•0àœë6&ùó´Ykøàë"ïc’7@Þ4 ÿ†ÃÿáŠÿëM¤Ä(£ˆ&Äö•UUþx_uAàFð/ï«.âcKŸW/€ÿ&9m³Žÿhø7ü/ÿ“Î÷ëçp|þ3þ]·WM÷¤ ð_y?Ý7V ÿÕ¦ÓÏÞ!#reºé¢ÉŸÉÐDF¬éæ§¯ÓÆ.H 1FÌÍÍ 6%^cNFú,- M‹];Îî”2=é|ø§kÙª,’m¯íb26òµü·r±Êã– Af¾ä÷Z¶Z»Ÿòoåu•O¿Üüä}ã Š?+]O~°p¥@.t^òØd&DËÉ͉75m#Ö6/κhkE?c3d¼iòëJ™mc’¯å Ýðoø7üÎøï³©ÓÌj@å«.ËZù Ρs>úû]·ë¬^ÿ²^¼¯ºßM¿ž¾~Òâ¥jž1ü‡±Aà¿™5ü7aZ‚áßðoÏÿÅ<ÿÓ^åžüWÇŸvÖyPÁ¥™AÉQ¢–‰H'(£N^ ÚbœL&ƒãàc‹V+µjm±heS.ñÊ¿ãÅ/#þô;™!b r„®-Z™íáL’¶ŒÝxyLZ†ˆË ©ó5沸¶4G•7ýÒ†À@ä÷ÓîMÓ4j™»´Á•¨UüÀã͋χ¯o:ÚýÐÎQ+‹kÙK¦ŽikޝŸoJ†ÿáÿpÄ?ÝgçàÜÔ¡ð"(qÎ!´š)5-´•yòJP¢ÃõÇÿ5¦3 K ÿó“I¬ø†ÿ=ÿûüŸÏÿ(énòïWÿ>tÊÈ2 9i±Šø¯N8ãìZ¹KkâˆOnÌ郵M[n¼àÙ9à&7·Xj.䋯e^\zäŸJÐÜ\Æ¡´ñE– lZÆCž»­òC›õä}“Ǭ5V¦sÑ2=%Šó”9òÖ²NZ†BË´iåJ-ëÀ\ì‡ZÛùo8ãÆçÆÙ -Ë6.yN¼îäq3…×i)[“ŽO+ÓË÷Ès—M¿ZéXËŠòûrÖªÔlÊ´ÿá½ã¡5Ɣ޴ëÆÍÚ¼f8Ó«ñÙ™N£9º¥¬|~üýßö4’>³êœk{rꎗ¾® €!å-U„b ÊóZ=Êñ?à?UÒ¹6„ÿ@÷¦™Eü7U]M‹gÑðoø_%ü‡Åàß©÷˜×ózñÿ›zþÇþ£‚)¬°XüW'œ±e;GòbÈ&*~s#ƒ@ŽË¡Z ŒH%WU; ŽVeY¯”íÐÓ˜Ã)*7!Žq¥§9¸Ú¦­9߀4‘jä5Ñšú˜·[âºjÀ.mÚyqÖ#ýœ¹ËZVF{¸”²MÚñiaíá¢©Ë¬Ç aÖÿáÖð¯©ñûk™\íKI–’³+•¦´ë ‡±Ì½¶>B'€‡”LyüBÁS?P" R,!}_~þ÷¯—T¹1üËŠTB÷õaÿ˜@†Ãÿ¡ÇÈ|­ÏI{ïµòÿÇð˜N˜Ö7ru¸åà¿zú™Ó ¨t!Xq,#ÌÑ»ü½Ì §ï5'G;P—Y’CLÙo-“¡ea5óbáL.Ë–Á4^o)kQ’–Na œœIáRýX¹˜Kм!1¨µ RË€•®µ–éÐ0ÒÙÖŽYËH¥÷œL&ƒ5À›ÐXs¦%–ïþ ÿ³„‰–¹-]¿§œ®{©§ ä”H:‡Ö;ÂΚ²Óyx’ûO¦ç«®Ú3 VdÿPhÕ˜*zþ‡Nf[þ7™ 0•Ѯ빮/h¹ø÷³ŠÿÊðoø_!þ›ÕÁI¼a=øÿQóKÿ>Em©;®ÿõ˜ÆúX¦WÞP ’ÔÔÌ`©ëºsʤ&{éf²d!—oÓß³4£–!ç Ϫ.ÒåÍFsx¹QqŒ‡Ë ‡Ï“˽òš”šùJ Ž›å˜ ¥UÆ8Àü7Zé–ù¥¥ÍJ*„0wZ:ˆé}ŽŠ Îò\\7¸ÀO°ÕGÛ*}5Vx8:üu¨ñé¦ÂWàñDì?7­5mó犆¶!h€¦i¤ò¾2 AVPä}Mklnn®»gÚCV~¾F3Ó¸ÇòoÒçjÁ‹ÆU.Í6àsbyMù ¨ªÊðoø_üËó‘×HSÎb[ þù}J\òMFÃké8“èsÖK Öü·©·l2™t_=£;g®¥¦uëªu"¼Pu›þ]Ȫ6Ã÷\à¤JPþüŸ’ï$³ëºBÓL–„ÿ$Ú ¡4G¨ò“Yˆ̱áßð¿,ü·½e“ù¥á_£Âk³˜Öƒÿï ÿ¼g üwÃRÇh8cÎYPFÛRéB‹Êyai„<–’ó&#Î^hr‚%Ê“ÜÓ1Ôu­eãÁiÚBNίyËãÖ2K fÍñæÏ`Ó”B4wùëdÿ÷UW%JB iXjú~ª4·tüwÒÞ­3ÔÌ*þç'˜Û0‡Ðþ ÿ+À\.þà y¨UæÖƒÿß%ièùŸð_ ÝŠñÿô3·î(Eý¬.OއÉÒ-›Æ—Õ8¡¥ ï€Ã'6­’¤!ƒO›:,ËÆ¥~ >O.ýÊÅPÚ¸X_½T6Öø¥¬çίçl)£½NfÁäÏåõ–öcÍ‹ZÃ!SiA¦ÿ7:àç7Ìã_mØ3°Ó §£kˆMÀ9  G8Wóx5þ!Ä¢†<ß#»[ÚÆfh¨ÌPÈÊ‘V‘M«¼®5š]éÚsP‚¶Öä¹È²¹fÇ˳&J´8ÿá)øçk¨9Ú}ggVs@Â)Ó­ÑI´~BéÔðú(eâ5gKÒFJTر{]Ú—’HBŒ¡säSÙëÓ9!Þw¿ŸþŸ2èÍ`øiGH•é±õÁÕâð?išLªÃžè³†ÿ§âè{ƒ ÿ†ÿ%ã?¬ÿC¡±ªßZùÿÿT¿‚ù»•à¿Ö4Õ5%í¢Ê’b]×™ß(-ÁNÚB^‡³ªÚ‚NN•lPOå9–ªdŽR”«}/ߟ„i4$Má„5ÍåùË{•~·ý%àGÎ{µj¦p>ò·§á>{;¸qÐ+ÂeãRùZn@ZoŠÌnðq¦Í!š« !ltÀOÔñCÕØ]ÇYo¢È@pˆpÑ!¢¥8ÞoõÁ7GáÔNoT•%b®È°t'ÏSÐD¤Ã.χþ[Vu‘ãlCh«3ü÷òs[Z ›×7vúùüü|ÖT«e•XTfL ÿO.þ%ÍRËr1mf½â_Þsù •SêǨ »,á_Sû’4TMœ¤4oBâ„yñú‘p¸eŸ¡tJåzÕiÊc—¯ÕÔ%ÿ–J’I9ËÊlM„ŽÚ–f Ÿÿs˜LæÛŠPhwë^!U’RÅh1øOÇÆøïh1³„ß $þ ÿËÆ¿wYoÐRñ¯=·×«ÿŸèp’×\BMn¥ø¯KÎßD.5iÎ#g)B8ó´Sqú)›qêæ“qÆ©›qîÖ-8îØcð}/x>àœK®ÈйÉò£üZ^Xξ—´öÙQýò]·ãœK®Ü8^¼»?õqœsÉÝ{§ïå„ä´4çL„³cå×tž¥A^›Ž<#B£Æ@ðˆ¸êE~mç‡ IDAT^tÙøÎþv±µŠ@QäìÒÏÎ;|ð£ã‹·OÝT嚎Grpµl™¼Oèé÷sˆØ^ÀW0¦T·®úÓdˆ@ÕàØ6ÆD>F#Þ꿃“ÜQø0ŽÆA ¸2à \Ϭß]f-åì>·µÀ¿öÔð¯5$§ó^KükÜq–Ô•×*Q45I×óP㟳šübçAúÇyIï\ þµL);@œa,Ésö\k¦—û¶ÉÕC^7ÉÑ”k‚“ìˆIššäÖI; ¥ö[ÊI¢ÆMs’QæÊÏŸÑäzüW˜LæøOÁŽ|ù¿Ä|R‚¬á¿1üþWÿM@=W#4KÅø4Keµýÿå<ÿ“̵ü?íGòëf•ð_3Àå ’ÜJ¹`Xê¹ßsžvܱxÉ…ÏÇi›OÂé§œ‚ŸøÙ·aÏÿø ,ÆØA+qù4Þ*SKäBgg“gnäfÄ\%*GøÌ;Õ¤#µlgÒ¹% k޳sž(_ÛÇŸg<ûGpæQ}Q¶<î¸ÇãK¯O#–ç!gV\Ó”]¸ 4ýîªÿÜÀ†Ð4m 3íê9°)Pë(-EÎH·æ?ˆ'pw܈ÿI onòš¯× a–‚ÒgÉ^nŽÕ´'ÿò˜Æð_z®5þY~]>Y‚UîåcçǦ‘ ü—œ:y,"ûí¤³'×µükkGÛë%ƵÓ},ÉÛ2MV §È{£õ¦Ï“ÍÇu]p¬;QÓ]55' ì>h!ZM˜ Á„1×Je§hz¯ÑF©Â#«<égÓãÇÜ܆. bü§JT¢ÁŒá¿™eüW>“É6üþ—Œ?í-[:þ‡÷/×ÿO?+ ú]ìó?á_>^&*ü×Z©Ía©àô¦‹ tŠN·âpÉE-“Ìb”ÏdÆB6ž©žìþÔǬ¥ãúô}»²FIŽð¹ßC› eŠ5šW Ò}hš§_øZ<ûÅ'Œ\àq²Ès\œd*4°ÉŒJiZ³¶Ø³kPG4Ïۈɿ ˜ò<ƒìQÙ|*Žzåcî^ƒ§œ ‡y5yþ‘?Eüö§ç¿ØDxÂ4^:Õÿü¿ÄMw‰IƒEezÖÛ†0‹¦àÆ_¯RöêÉÂ?‹BÈ×§‡­|øk۵Ŀ–Aæì²t´‡­t¾xÒ÷¡Æ¿THÔöS™hàscõ!é0jІíØäµ-­—Rã¹öžcCY9r¬'DëßcÊîØœ•F:`Þc~2Ÿ©¯%¾öÙó¿É¼¯… B¯ ç}½,ü˦çäÉû=?Ëø¯|×Ïaø7ü¯þYÜ =OWâÿkýjË}þ—🂡„©n™z…â ð_k nܧ•6Kj5Ú_È4Î&—)y"³ì—Кÿ¤£ÜDø¸Ï¹äŠU%8Ã?¾ý­ƒ²qI¥¤ÎRRÿâ¹4ÔéüºÜ<Ì;¬¦Eä¥féDóµ‘ Ò‡é ”cŸQa×EGãÎ8Wß·¯oróGÿàkpì¿ø)Ü?½oÿø‰'àÃ)ÇŸ‹×¼à—ðüg}á+ÿ'Üc÷·7­ ]õ¢Çqó§æðµGóõÁë’•ÜÖˆ0‹Ë€k.Mñf-ñ¯=,4¥mfËzÀ?ÿ¦ÈNv¸8 ;˜Â}ñÏT¯Òq°’R©Ê)P­éZég«DUby\-û­5èò3OR|ä÷éÚi½w}¶D ÒÖz]Õ˜´RÕánêlDº/Uáùß÷úä²Ú©çQ*ŠåÕþ=qþÓë“nrx¦AÜ á?¢ „ ÿ†ÿeã?¬ ÿ)ÀÓ‚«åøÿ¥‚å>ÿèù“Á‘Ä?ÿa…ø¯¹¼ZÚØñ“²b§»pÓØá”t€º®ƒ§´²­FS’_?ôWw.ú8SÐ}·Çs }ì1\øŠW« ‘ÕA¸LYj䬼¼¬>ÒF³ºAP Å*o´\fÖšö5'¾Ó¾?½F˜þàÅ›ðâÝáäoÏ£°áäSqìý$þägà†;Çã#Ú1püåóø¥9/?õ ½kZíjir§ü³óâÃwo¨‰ÉM19öëqC˜µ€€g/ñC¼DyXKüÍï)Ý+Mg­ð¯ñ¾åL+ù¹<—{˜Zódà_£Wj³Vd²€“UÚº’$ á¿4oK«\_ò:-¤É{LLHGQR‹ÒÏJ4lÞÿ¾9ûŸKò6¨[Z[¢ª¥Ê>P hRPêJ• ég&:/÷¯4´¶›á?M’ÿ~–ðß„NÎðoø_üE”´°¥úÿZºZÏÿ#äéþç'ÌÕu¯·Bü×Úh`”ÖIÆvµ‚!#ÊeAÙÈÍéô-¤_Ÿ>ëY/}ùÀåÍIKÇsLö?Ó‘8ËÏ6µf<ÎøsfAkœ¾ObXÕûPùj¡aéçt}$H8óÍŽŸ:‰û”é5>‡íñuøqä¥/ç柉Ü> €R–`Ê©¾³?â?þÅœ÷S—ã¤M/‡ûæŸÐ9—?÷þøÓGwkA^_Àφ0 ¯ý”±d¹o­!w­ð_rj˜?_šÅ ÑŸ\ü£ØàÌëIú`ªI¢–pöôPá?Q`’“§õ\2]„EB8ą̀b­hø—2É%ézrÄûgØ5LJ+ÏìŒjb#¥=‰×µÄ6ger«£‘Ä@ïÏμɰõ MŸÿ“®JÄIÃT§øŸo®°dü§cõ¤X7WטÌþÛ P„áßð¿Bü;t}eKÇTçï­Äÿ×ñ¿üç¿/\ã°þ…n9ø÷Ü@%³ÜœeHÓªÓÄÛň,ÖX™*mBšC§)-± si¥/V.Ár¥‹ÀG{ ðèc©‹Eû:eeÆ[Ú48Û›c*Q;çàb˜öý´ÿâRþ…éÿ i¨+»”8¦c1¹VT©é§µ×÷œq$¾¸ù„̽òÕøÓûâ»âT.MŒÝ¿#þá[ îûÊ~ó¡ Bhšþ:<÷ôÇ»ägË€@–ÑÊî±ÂOº?Rß_›g Q´÷Ó6MÝELIà¥ùcYMÎ6•æ•øÌÜó¤eýÖÿšÒ†®è•¤¯Ÿtü+Øô¯´ÇŽ XÔ¸ú‡ÿ’ÂÍÔ²Z: éûä„¥õ’8®ºŽá_®#mŽ7‡ËóbZŒìÔð,0é°”2rö^«§ÏK{cµ»BÚZ!MÿËa¨©r4½_ýÔéó¾½§•†¼2ãÆwtº¼ò´0þ'íý¬ü‡v’üLá?LEªÚðoø_ü—æ­Ôÿçµ¹ÒçhñŸª¿• À1þ½”_þk¹ÀåBå²¥\ÒY|ÖK_> (§É;j–yê/sYÇú5´—c2‡¥ác¥ì8;’½òµj†EËÐiñš&½Ì4pÃcmÓ¹mÔfÎBˆˆÝ0¹‘ÆŸ©‚sðÞÁ9ôQx ]£¡lžÔ²^Z? —ö5u´îwØL{yömt¸íÜc°å«crÆ9xäî'†çá\'w°‰xèë šó¾• ÓŸûiªÄEàéGa4›£•ൠA›LîS9@Ó2/ò¼YiNÛøÌY5V±ÓæhBʺq¶©äpó&sùB¤Éjkò®³†¾?šÐÁZàŸi)¥áp íiœ|²ð¯ Î+Í·•TNH¹b-ã]¿œò^RÍ*5žsö7eµµÌ=㢴5ª-ÏŽáëTš;–;cÓ@Æ‹gyÝVl*15©À¥žôÿü¯I"¿î‚¦ªª øo–„ÿºªº¬.ã?õÍþ«ÿÿá­ðbÔJüX¤àh¥Ïƪ§¯ÑÖ•›UÂÍg9ØYcþâƒwßQìŸ)ýNÚÖ‹/4‰ñ *^|œñ^Hš‹•+¹Èˆ|ñþð·ßŸü¹·”AägkÇÆ%Rí<9š/ÅOõÔš&`~ðµƒ'á«Ý*¥BÓH8ý¨oãÄ {QW•Ÿ–\e“ûM¸'EnÒZC ËDËŸ5!ÀÃÕtÐ]g…—=pN‹Î)¢ÞòZ¤˜'ìbƒ¦ªzú7m«<.©F³^7„Y 8;X B™ž±Þñϧ|ŸÖ ÿ¥X-¸/Éýk\ý' ÿZ#¶æô°ÈÍÚìLÉþ´1ük ‰’sÊ8c‘‚DqÑšåqÈ> éÀ”¸ÿÚduV³Òh¶œ(™Þÿ ó“ù–ZÒˆójÄ}j:J\>`*\? (tÐ8þ‡âcø¯«JäÚÌ*þƒáßð¿ ø+Å¿Wg.×ÿO×Xö¯äùï ÿ®€ÿÊ{¸$£½Bü×­ó­Ì)M:£/yÁ™ãZjJÓJŽØµ¹Æí•›mãÄf‚¦ xbƒßþêOâsß:iITÄWôyüÌæÁo¨¦O;5òɯ©‘É “7B.…ÇÇ€pLÂôw+à?|ÿ‰xþ?~çn~6>÷•y4…–§¹Êᜓ*øïþU{M¦U€"v}CÉaÖ4ÿ×Û†0kJržQiöĬà_S»dªÇZà¿4ƒÿgJʘ3Vr¸þ5çOÎÁ)‰uÈù!\‰”ŽÓBøg–{Òg°ãÀÉî{'&Xq‹«Ç‡žéNr¾H‰ž•Ä ¦ËtNÐT ¡RTáœZZ-ðü¯³5]×shšIûóžB—ªJIXa!ü‡Q‰ã¯ü'U¨™Ä¿wSjœáßð¿ü·´Jï<šÉbñß ªo쿯Äÿ—}WLã\êó ÿifä¡À]:Y>Ó‚Æ2+E(Ñ„$GRò¥S!8ƒPreYŠq± qZ¶@sŒK›S9Êg0ŽÍè%§êp1ÌO&øÛo‰ý“¸¤kÿз7¢9q‚0 ¶t2Ez™› yfŽ&Y:—îÚìk€£}[á™*À}ý?Ù÷9¼æÂçá¿|îÝÛŠ—X§N;Áá‚3kà>‡¡‰Hû‡wÀî¯Ï &nóF¶^7„Y ´ëTê‘âàs­ð_¢'”Ö¯6Cj-ñÏr ¥ãäsàkw¨ñ/3“¬ÖÄ#&íÏuyî%üË¿a5)¦Ë°SÊPv)+»Z#ö@ ‰²û²ÏçvuÓÕ ûŒx]\Ó]Ð3ØÛÅ'.øü—Ÿ3ýû\.»OŒâ?‰ ¤c*áÒ4˜kñfÿé:ÿáø¯BPÏ-ÿÕèàW¾~Kõÿåç¬äù/ñïÄžDòƒñß}v[Z.þëB×× ©”TJV+’Y-­ï¢Ô„§ÝXÎ’qY49¨ò}Òœ ÉÔÑ8ƒF*‹”J¨¥¹0œ=‘‹‡Á8FQÊ©L­:\ Ó)ßqy(Âôï§C&%_¯M;–Õ y^rÑq•dòðõæºåµµõÞ|ä¡¿Äg]Š_yÍYøÍÛ÷ãá½M÷P>rƒÃ'x¼ý•Gâ˜ÇÿáÑ]ˆ¡éߢ%ÞyÿQƒ)ÓÍq½m³hkE®É1—YŵĿ¦¸5F‡Mø£>™øgÕ¦’C¦Ñ:µ×ˇþ“ÿùùù„-^ú½ü 3Ü\-ñSÂ?'$ø=Òñ&¬iÇËXcz '%xÐ%c€“#Lëág5ß_vpó*dÕ©½U~:¥}Ã܆©0U‡R¯P/š é9ÎUƒggÿ5&“yÿi0¢th¼–˜‘×d–ð/¤ÇCcø7ü/ÿM€¯Zü7+Á..ûš—ãÿ/ÿãÏÆ²= 9„Ïÿš¥u9#¡ Ô*Ék™áÅVX´#oTúŸåqµAcZ6F+²ôõbú—xq²“Ê»x¨—AeöÁ¥}Ð ‰¤—RÕdj?÷ŒßEåå1O6>ð7#k®I³†„6»6˜¹®ÜxÏ ™IÎyV úG <'GEˆ0ÿôØ7ñkw݈7½àGñ;ÿòb|fÏÿð­jà{O«qþé5îÿ§bßžßÂQá»-g=vMD}mw?xt±ñyÈëiC˜õ€@{`ÈÍ^£ã­þYV\ R9ÃY¢G® þ‰ò¡Ítâõ/÷píµ¼®%þeµ±”˜`ÇŠgÔiCµ™'%ükÍÞ<ÄXs"dã:;‰|¯¤âX¢QiI7MYI‹ÌR—Ö97>§€eúºD¡u˜4¡£œÈ¥ÞWÓ!‡¡æÏÿá~ØgÖ'¨ë9ÌÏì(r!LE¦ô<ÊêÓ}¯ øOYàɬá¿pé¼CUWáßð¿Dü{‡fÒ ª«n=-ÿe±£•úÿ:þWöü¯¼ïfÕ-î+¿o•äb«·ü×ܨ]R¬àò•FëŸY¨ !?GS®’‹‚e†KYv-úÕʪ)¸YhQíþÔÇ‹“ƒK<[¹qHÇšgÈh“äy%g)úk$ƒ €ØL›ž"õýOû,Ž:¢ÂÜœ"p`>`ÿ€ßú§‰¸Y“ö=¦APT›[ºî¬AÏeúôuš5 yi×߈{Ü‘í,Ô6Œ‹Øõµðoo¿/=ó"¼ñy?ŠW_øl„ØàSïþÔÇQï½ïÙ¼p±cË…0¥ÄÝ|ïÉøö^Í–pi=o³¤ûÎÇ´xVãr-ð_¢~h×RkH^{üÇLu.Q †ø3SßÑ´¾A]Ï üOUæêzN(È ñÄ|IÓdY& Ýö$ºL˜ü·4¦Øþ ÿ+ÀX.þý@¢[ž¾ÿ_^ó4³p%Ïÿ„H%=ÿÎ9´S2WŒÿ:ñ åÁI^¤tÀ4…«­_ŽóÎ}Ž=æh¼ä pÚæÍ8픓—DÅ›9$#ò…Ž©EÜ ÍÙy33n¡¸¬üUj*“`-)pi —+ \†æ™Zã¸xpZÄ ÄÉÄfClØ ¾Â†ÊO‹<“€y4ˆ“ƒ¢DOM8cÇŒ€–×]ã3sPQjì”߇ù÷ň¸ pÑBˆ.âñƒûqûCwáö‡î”9àßàƒg~¦Á[ ~/}õÜñ…ãÐW|xÉ{´^7„Y 4å¸1úŽ6ÀìÉÆ¿t–‚Vð[3üS¶’߯” å¦mîÐè‡ÿšD®¬XrÆ\¾‡æèq¿êbð¯ PÖ¨B²o@Ëš§¬¶Ü´Dƒìgàu+û8›Î˜åµ*×sÞ<ßË]OçoÔ4v Tªög!F 4¨Û N¢ÎÉÁ¨Ið ÇD= põׯêªQ%üWrvÌþÓ;§¬°áßðÿ”Á\ þƒXKJürý~íJŸÿ®€)™]‰êÐjà¿–l&]Ló^%àþöË>ý¹]YP³õâËqÆ©§àÔÍ'ãôS6ã´Í'ã¹ßsŽ;ö¼ä +] ª€šmIÇ›F5Ž®¯¼¸I¢[“s”ïsÎ%Wd;ç’+Ÿ“(MÚfÆgåK%Õ’Ž~ZXì{“ƒ€&ÀM„ùhšýbköÁ5üdÚœêš7 hæû (Î?Ìïjýó8ÞoP©`, ZšQÀç¡Qª:P?à> ¸—x`ƒl(¢¢£Éµó„àšSþçoü#B˜J}‡&âÛß­ðë; ÿð­¹, ¥eb¸'g½m³hœh¾ßLYkük¡:Éš2½Ú¤ûµÀ?£öZ©—šÊÒ“…^2«[R{JNgTY)q!üs\-ÑÎ[áaü§ˆ9¢ÜÈŽã›Áµž„a²¦ê‚ï&må¦j«BÙýkß{ÒLÚ@¨ŒW©pœ­ŸTÿaQøïh.ü»éUfæðßþ ÿ+Äÿd9øÏÏG>ûWÃÿâyÏÿDs•ÒøI®o‘LýÍm4W׈ËÀ¿ÛzñåQÓ×4è5Ði}=òBÉ¡Ô?ÁŸË cL+_›Ì«5‘—ú4yıßkÇÀ™Þ¬Xž°´AñfÆ÷DÛ°Žœ ØX‹…uÙÿ¿ñ˜þ ïü¥\£?]Û—¿§ÿ̽{àïû=ßÝï0߸A$¯…ªª0??_¬FhYÞ(ºëã€øL/Š@= €œÏ.vw.:ê»øg>ˆãê¦ ‚¦?ß÷D…·î<ŸÞs€ò@Rù3VmÑTzx0šÖ£­‘±õ%¬Ü÷Ãò¡¯»Ds“ÖOįÒüšåà^Û( ÿ‡ÿì|Há Ù¬]ç’°‡†óCŽÿÂ:K hÃ¥œíbð/ ­ßU{¨ó:רÝì w ÀâZòçq€£­}MÄD«°ƒ–º-ó]€3i&]65ˆž îýÑ×ièªAãÏÿyTUÝCŒÿFT‚²*xûólmÎ:þ›€ª®º¬¾áßð¿dü‡•àƒã)­µµöÿþU–…P:%ÉUÀ¿;ç’+"Ó~¸üÈ™¾‘Ü/ÁB^ ‡*ËscM}¼È4|'ÉŸ­•mµÍOk0ÓdÇøªœa×2UðZFBkè+éÂk>9¢/eÙµÙÌYÕ¦E˦xub65økŽnÇ…x6¶8:v Bi¾[5¸öÌÿ‰—·©Éa÷׎Àµ·>Ÿøòq©BË\ðõ\ïÂ,†Ãÿ’ñ¯\Ï…hԌҺÐì•ÒÎsC·¼§Úp\®²2 V£çpz©ïV›n_Â4€n@j LuUgb )óZwbòù_uÔº^à@–n(` Ýïû`hˆÿéqôøO4—þìâ¿1üþW ÿqéø—sŽ$ãE£ý¯¥ÿ¿ü§ HÃÚÛ–‚ÿjÓégïå:íbjt-úç¿O z)òÔ”µ¸üÊv Í$)9¦²DÉ¥iÙ\ž>79§¬¾Sj/ ãÁÒ<mHZ©L¬M.. ·,ýœ©WZC¢¶Y²Sª „ãæP 8Ú6h& Õ·*à#pˆÇF jïk½ø˜Gñ3Ïx\ƒI¾ð¿ŽÆÿ}ש¸á/OÃç>VÎyp—µ‡ŠÜ4q Þ(xÓâæÍÒùÊk/ZuˆËöòŸg ´Ò¬Ôv×8«2º-•9 Áª])‚×6 yãRS»¦¾¡mÄ7v,jÖ¦—¦E—fÍh \æì†¦r¦)‹h™-¢)“0yLš³»."lŒÀ)@Ü7àH‡ÿë™â¼øþöácqÇ—NÀ'¾¼ í¯1ß 'U/4÷I–†­Tß 2’–§Ú?ÿáUð¯¬×1Ú¯»Åâ_“ñÕ°ŒR¼Œ¹O¥,v©Š±ÐÜ––¸È×h¯ê&+>™Lr[Jô8Y% ¡É†¡.üüo:¡„|‡Qü§ ã~2içõ½ýäø™ÁˆpÞ©Y|ÿáIøKÁPç>i×n-ýÿ„Yé© øOç¹Zøw[¾ïeQã³j‘6?ÌKS“¹¹\[P%ùÞÒ¤ûÇQãÞj}Ú±3OŸ 7´I ”6ŠR)v¡’!ß°R¯†o š±ÍÔKw ÿ†ÿYÄ¿ÌLK‡ˆ3˜ògZ`[Zg%êJé:K:ŠvŽ\ÁÖÖ ;oÓ~ ×Um$­-ÑÝd”§_àùß ¨q½8BXþy2üþìâ¿1üþWˆÿ°\ücpL²r´Öþ¿ÄúÚË5$dôÿ’·,üo:ýìÌ”ÃÇæ‡È)À<阥ùÂñð-­´)³Ñ\:•Ò‘ 5³s¨•÷4e.Ç177§Ò˜^$i;¼ ê1)½^s¹Ä½P6ˆ¯©á^û¹Yh¼\ àrݰ W8Æ@Çï£}—“™÷[â{ËŠ _'M™§Ä{•³äq2Í äÔ/¤ö£õ ixÓŽ«ÔŒÏ÷°4¼Žø%ÙlÿáÖð_’ª×rb¤eƒ9{¼˜²æ—›Rÿ‹&UŸ‘DwëÕÞDE<4¨Ú!©>õU5œóTv¢Å Ÿÿ5=ÿC8õø÷ â?¢W’¯›üqžuU œÿáßð¯³X Ië)^Kÿ?á?ýŸªC©O¨S„Ïÿ&`…ø÷ŧ /oãïµ2„éçiŠÆ}ä&Ã’Ó'ß[Ó®çH›5]}D6Ʊ岬VŠ]HÊ·”Ñ2Ú<÷)K­ò\Ù1ÕÀ0™Lepyoåâ.5¾q™9]×Å¿ç÷r•ÚÜæ±Ê×É5ÁŒ2³!ï±vòzñza ˜Œ ¾¥FQ- Çëg¬š)¼!hÃ÷ø3Òëæææ2>²áßð?kø—ïÇIŒ%]Iu•ø’Ã!µ5=&÷.ïiUU#½§|_¦ ×¶Ïe²Û¾Ù@\ùiuh®žËÛz:]?huáçÿÜþom1 IDAT+ÿ’Çïí9h'N ÿ†ÿ§"þ+_-ÿ®8Bc½øÿ’'ß{2òüOƒUC+®²\üW›N?k‡\CþÜ’$nNÓÏJÕžºª»a§“v©wS…Å=ÿCFƒÓñŸæ Å"þj~vLƒQðf ÿÁðoø_þCÌèpR*{ñøÏ{¸da=øÿéH==ÿSp¤²tVÿÕÓÏܺƒoÜBåRmp•¼‘ÚÍç“@*•5.%órTl¼˜åÒÔ@¥²¦F3Ò"}>^ž­kÉá-)Šh%?^d\Få@Þ{–¶ÔTË8#Ãj#¥‡DI冿—ÙN™5JN~ZgÚÆ£)àhô5yoÖÛ†0‹gŠ ÿ†ÿõŽÆ)sûǪšƒ¡a”×Méò~"÷ MRÃt)ñB3­ðT5šÐô¸VÛIŽ¿úü÷ ½N>ÿ§ÊqÓJü‡"þ†“â¹:ÑeÒÏ+ï;̺Çà+?õ`ø7ü/ÿ^à?.ÿa ÏÍÔµôÿYRMŽü§ß­ÿÕÓN}æm²ƒÅÓ…5‡DÒfd†›K›Ú¼?²ÄŸ,ió³3ªÝHM6OJr |!@•TN´‰¹¥&³g•3/¥ŒÏ=à¬FIEËœ”š K]mNŽ–ÑÞ·Ä»UUŒDµC›\Ê4±Áó!ÖÛ†0«áßð?køçäDI†^sèxŸXˆ%ÿ—fÆ‘¤Ø”záäõ(õ*šÄc„k‰spÈ%¨óç¿Ï¦6ÍD3^\»&ëêñ?É$¸§¯Šÿ¦€ÿŽ’§œŸEüÿá™ø„,ÿš\¶6¿i-ýÿÔÔíS à߯þ«§Ÿ¹e‡¶Xy¦ˆæ˜h }l)•KKjTÌ?”™æ±f@Îd³Ê†Æ#æ›3–(mŽ=¼Ï7 *íßËw=D©ÿ'8Ã}( oÓŸÿqÑø÷üOÿÞ¹îg©4Sø¯|'olø7ü/ÿ¡¦—Ž7xþH:æzñÿÃð/i±+ÁuÂSu8Ùô'³´ÅÁt^4š4¯æ¬i’Mfï”›®XÅ‚K¨%µˆ1+9¡Z¶^k,v¬Ï¢4œr¬§C›£•Ƶû㤴þ +Îדï½v²¬ÎÚÿüš¢Æ¿SÖ)5 j¥çõ¶!ÌZ@`ø7üÏ"þµõ«aqÌñ”½Üã¡eù:É×Èû Ý yJYô~]¥¤H¨+åÿ<&ͤ Hb×+”;~‰Ïÿi/PR¥ÃW¥"ü§~€48q®Åœ5ü§†Ãÿ*á¿®Ô€¨¤Ôª‰mÈkéÿ'ü×ücÿuUu ‘åàÿÿ;Ù‰£áÐâIEND®B`‚fwupd-2.0.10/docs/win32-term1.png000066400000000000000000000467251501337203100163560ustar00rootroot00000000000000‰PNG  IHDRÈýøÐeXIfII* ȆŒ”(1 œ2ªi‡¾ÎÎGIMP 2.10.362023:11:28 15:32:03 fwÖ,„iCCPICC profilexœ}‘=HÃ@Å_[EÑJ‡v©¡:YqÔ*¡B¨Zu0¹ô š4$).Ž‚kÁÁŪƒ‹³®®‚ øâêâ¤è"%þ/)´ˆñà¸ïî=îÞþF…©f×8 j–‘N&„lnUèyE?¢aa‰™úœ(¦à9¾îáãë]œgyŸûs (y“>x–é†E¼A<½iéœ÷‰#¬$)ÄçÄc]ø‘ë²Ëoœ‹ûyfÄȤç‰#ÄB±ƒåf%C%ž"Ž)ªFùþ¬Ë ç-Îj¥ÆZ÷ä/ 浕e®ÓŒ"‰E,A„5”Q…8­)&Ò´Ÿðð9~‘\2¹Ê`äX@*$Çþ¿»5 “nR0t¿ØöÇг 4ë¶ý}lÛÍ ð \imµÌ|’^ok±# ´ \\·5y¸ÜŸtÉ)@Ó_(ïgôM9 | ô­¹½µöqúd¨«Ô ppŒ){Ýãݽ½ý{¦Õßpør¦Ù‡– xiTXtXML:com.adobe.xmp 9úÖ¿bKGDÿÿÿ ½§“ pHYsÃÃÇo¨dtIMEç  ­[j‰ IDATxÚíÝMˆãh¾ïù¿’dvN&½Œ]'ôªº§„½°£¦&¼èKO—¸8bÎIæÒ³Ð"‚ÛsÂÌÕÛÜËØ"WŽ>¨ŠæöÂÞöÂÁÕµ¹ {'Û ´ë[ÙI.îjF³,Ëï/‘¯ßMu¤-?zô<ŠÌŸÿ-+?ûíïånÁüÇeÝ-·¢ˆ""K¹»cœØñf0¦¦ov~ïsºWï÷Žv§l< Êýë'hô[èШ‡Áýæmý»s¿xÁ[ۻɩPî·Û÷>`›õ_ u7Øhû»êO°I+Ûô%úû*ü½TÂÿWD +Ç'uAý7üm ÿïU™ó ¡ˆüÿïÊžÇc(÷þ×óÆ£¶Îߢ«‰æo÷7Qp7}Sîᯟ™Ìö\Q<~Ï1ðÁ¢ãukÝ32¸íx†¿YŠ¢Lí(˜l'ØêàŠÈú×ÿR܆ Xk€` €` ¬‚5ðáy¸ä¹ßý{£¶~ó¿ (Ösüwÿýÿ°è©ÿóÿúãÿò?ý2þoö¿þ¯M,ñüùó%Ï>}úôMöäW¿úÕO~ò“©Çÿò—¿üáØ®'+‚õyýŸ=õ?ÿùÿòú?ÇÿíýßÿÏ¢-_|ûüùâéçŸ,xòûÇ_þúÓG«zúò‡¯¿ºÞ]´åògßœ—?|ýÕw?ŠˆÈãÏÞ~wV õ‹oŸsýüÎu|eÖå™ûÎýêW¿úüã¿øå/“Ùú/ùËüãõ«_m׿Š`ýßþ7¿X³¡÷ïë‹¢æ÷/wwåû^~r«ÜöèÓ_?ýtóg×îl¹,T?úâéÓO¢?þðB}òŽŸÛQž~ùÃ×_}ûÃOï1ZßzxîÎO~ò“ñË_&³u˜ª§¢öFîýË/ÿ|-»?ÿù®\ÿùå=;/øö»GÉuùGŸ~úÉ{ÓûGŸþb÷Ç}Šæeë¿üå/·OÕ²rÅ:ˆ(Š’|$ùǹúóGdW¾ýóËO=¥Ð°bâñgŸV0_|ûüûÇ_ì^óÝ"»_<ýù_£¢ŠQB¼äùâÛçß?þìÑwß]Ï{v\Œ±ûÅòÍ7×"×_=ÿn÷‹§ŸË·Ï¿—]¹¾~þ!*ƒØÿi´å'ÉšŽÝ(.¿¿ü ùfjùõ埯ÜýÅ'ó#÷ܦVì² eQ£s‡eîP/÷"9JŸÜºó?ûòןþuzxÞ¥l-"·LÕkë8C‡?¬©“¹ZD~šHÖ/¾ýê»G_<ýu_EG[ÿøÝ÷»_>}úèÅ·Ï¿yþò³/Ÿ>}$/¾}þÍŸ^|:•Å~üîÇ_<}úùœg_ü)j:ôô W ¼ùñåã/Ÿ~þHDäó§O?bâ?ÿõç“[Æ=”—?|ýÕ×?D^þâÛ9ûøñßÎÍ© šZu°+6ød¢ÿŸDmNË¢¡ž ÿß_?Þýy½ãQºuçG%&“à ޴wçSƒ°‡ëoæé8U¯“°Ç¹ZýtW¾ s×Ë¿¾|üY˜¿}ú‹ÝモégŸúHD>ù»]yùø§$úùû¿¾™d?ûù'óŸýÛǯ¿ùza‚{¼ûÓÄy´äûx:n¾üëK-??úô»ßýó_Ã]Œ^þÉçOgW]üñ¯SÝ\ÚÔªƒ]µÁœþÏ ‹,êÄ%ÊWÏ¿Ùýâi’ë~!£.îþÝT\~ñíó¯x9µÏ_ì^óüÛãìûË5šÚÎòþ'ä6C½}çG„|ùçëïèî-UËäg·ks­ë¹%ssöt®Ž«vE’‹ºŸ±ûü›ç×"?ûlWî4ZO|¦ï‘È£¿Ûýæ›Ñ‡Ç>ùùgßõÕóïäñînTHñIbËO>ÿò³¯¿ k‘öå¯×J†Ÿ|þôËÇ£W…¯‘­šZ½¯Ùþ/èÓ­†zëÎ?~ôã·ÏŸÿNÃ'3ÃË/3­7|¿êEþð‡?Ì~AL˜­·þ‚åg¿ýýú×ÿrîs¿ûöÿö¯ŽÖlèßýû:_iŽÑÅ ŸS‡+ã2cÜ*X³ ÜA°¶2÷f„¸ @°ÖÁÁ XkàÃóPDþæoþ†b¯_¿~õêÕF/aŸkàkEQ+N§QQåétØ£P£¢ÞqãÛ¯Rª¾ÉQÁ›šwð|À–“x?¿A|ÔÁZQJÕÄ¿¯qnzW3AéÌÖ=«X( …ÂA­>ÆÚuZ˜ÝR­4:ÕÒû7{® A„Ç5u³¼?™¯Zš î¥jg£ ‰M·Ÿ»ßd3¶ Ú^”x<ÑÊZYvÜøäÆsÛQ¦ÜõÜ~A¿vP( –ËßÜ™‡""ÙŒïKnO­’Ýˉï·ÿ ®½;]ÎîdĽlmßÀõŸJïŠôç>ûÎïÚZ]×4ó%¥Õ ‚ð $¯‰kµîv/÷:>J©Ú63ŽãŠ>4ÍŒcëÉ]ØöÙ°pÜ\L7Ú~î~E=º°s=£Pë‡?7*×á…Ü’öE=:Õ=×Me&vá;F|¸Ró¸ÐŒ/#ÛvuXÛ_2_Kú3w|–Ìï¢q[Ðÿ~íà8ìð„ý¼“(jåÂÖS£ós¾ ·_¸ßø´ÞéXÒ¾¢VNuqžÕwô««¨•²…ûudÊš{YëÛŒX/U/ÊÖÆ¢Õõr‡‡é›«5þßÏk¾ó,ú÷{ŠïOêý ØÄÊŸˆ Â¥¿x NËO/H/Ø>LuEÃñŵŠÅQäÛ&ƒJfGUÔ½´Hn/Œ€QY̵ž„Ççãäêà:ý—ë?èEÝIV ÌŽÃæÂ5ë’¢(Šº—K…ëÕSýl;~r_³óµi–Îïì¸ms`þÍuö¨Ñé´í\Ϙ{ás»í—ŒæþèâáTO­jÿP—¿ã·S6©oÛº8çÉ“|I;ŠR:Ôeℼ“q‰T=ûód°oØš]Oˬ‘«ÃD¸0‰zQ}g4 µ~ »éTXi‰—þ¤_;0œL´üW,Þþ.e÷rryé%"þÆ-,†Eý ½TzW²{iÏK‡»¶›?·É‚Ù8W‡ýLév;*Y±õT²¸`v¾6íϘ¯Ñºz¡p°Öêí¦ÛÏÍÃɘíN§ÓnŸÊe"—Îk_)UÍÌüÕê裂ñ;‰ýEšÇÑö—i;ž‚åídÊšß›ø¾‹qÉ$ý»ßýnQ¶~0õoùA­ýÉ¥ËùÖ¯ûœ ZA,Z‡.NÆŒÄÒíï@ØþnÚë¶Z]/··¿“¹ƒö³ñúóòþg÷Ò7ç]ÉíewÓ©q.Ÿ3[eÁ0Y'ë@d0ôÄëvFÅ;+Vú7èϘ¯ÄÊ·d÷rËÛßtû%ã9N²uIGCº ýý¼&)Ýn·£‚û”n·Û³åò­®»QEF«ëÎ]éŸjGQJ‡zʽ¬÷ãåí»LÕq¶ž¬³;™rsô¯z~J?\·T`pÕóµòQÔàþ™9½®6jsýíç§»Q]Ç2å²t["­®§—sÛ m¢ŸJ©jÇ• û}ãgò‡9Z]/w˜ŸÍóã°•V×­|:^¯Óvjy}óò¹¾ýüÞFЯ]º¢•£ÕÙýC=—n™ü„ë¦Û¯C)UÛ¦X'­%íÇkÌQÁ½ïÅâT¹¼¢¨GeMâkžUý‰¶½«°¤ìQyª.iù8€íRõìϱ‡Ë„¢]ÄqÑìtLq­ðsýÚ! »Ý6EdÕ­Ä‚ _b¥ÛvGmý¤Þ‚ÉöÃÇ[K¶_‘îêÏœœm·u‰û¹`Ó¡'º&½ëQ ×Rþòã]x\Ïœœmwtß1,±Ë²´ÿý¡—Ò5×:‘®gššÛ]2Û'kÓÔRw$l;v§cF^z\‹ú³h|ÏïÆ}/U;ñ½UÌvÛ×*ž4ƒ ìÿZ“›8Þõ·_´ßñã¾cÇ'áFíO§ï…¥÷Ý[´ý’vµrª§\kú×dÓqë¤êE(?ûíï‡ÿø Ö{G­4ìô%Q ·QªvÊ7Ü`€Çëׯ_½zµÑK0jÀí¬ßgš¹þíÛ€Xü…>‰/5·þ–R`Ê¥ 5`Öõõ÷©ÝÝÝ¥¸ÓÁZ}Ýöò—)£ín߃¨Üó]* FÌç™ñNÍËxB&¿ªfÑã÷z¼á–³ßüò‘SJUÊýëdb(Ùºw¹ú¦ÑAЯ_zº}¶Î·l„_'Ô8¿¼«²tfëžUŒ¿¢0|<Œ ›åŒ÷!oz\oèªf2¢…_€h8Ó_U³èqƼå`½ffcê»âfëæ±ád̳5¾Ï/»“ñ}Éí©Š¢(ê^N|"£×ÂïØ~7¾.»“‘ø[ ·¤Vm3‹ø39|g/$–\;½•yÉîåıÉíe?ŠãŒñ‡µRÖÜ˓鯢VJÕöè¦\Sß°8¨_ºírEm­ñ=ɽž—ÛËÖ²—î]örå8€F_%7óµps÷«V§ò왜†¯ŠŸüRºèkóf׊¾bzÑö‹Rr´©6úrÁß\X:ÔS®UŒ‡¥_;îGOÍßïÜã 7N_Zb†á;Æ“°ÍÚ 7J|¹`´ý’ãZ4/s÷»¤Ÿ›Ru/—ò.[WR>ÝËÖúwüÅ%‹Æg½ãstÑ•½(:Þ%ã¹è<Ü´Ÿó˜øFÒ%íÏÝ~þù¦V.ì\/>Ç&ÿ8·ÉóJÄ¿á¯TÀÇl¼bÝË¥ÜîT¾TJÕäÊëÔ7´A³ë¦Ö\Z¼êz¹½lv/}su-’Jsþ[ùKö›Òm;×3ŠÅ¢áˆ~ZQ•0UäzF¸±ádÌ‹£° {tª{Va$™ªg·kÚ¶žÍl·£Ò]E ;i¹â;FÜвC]°æ½¤Ÿs+|\3M±Š…BÁpD?= Wý7mG­4òÝh0-W ·_r\sçeù~5³|cŒúy¸¿åùž…- ½Tnï> vçŽÏ¢ópÿÌÏŸbÑp|ñïçh¦¾œ8Þ%ã9÷<Ü¢Ÿ¥jÇ”¨ËÓíÑûE‹Ú_´ýüöW=üëœÝË¥üÞÕ`Y;ÉÇ‹–Ëß§‚u”™ör)ÿfú–"ûyÍwž-Y}¼¾ñW ÝtJDZ]/wx˜¾¹¬îÖ²ýÆ fƒ¡—Hc—õ¨ÝAýr"íkùéBðÛ‡Á¨h8¾¸V±8Š(›¯¼†Ç» 5.ìçìq‰Hbõq¼ýæíôkqØju· @K÷ëZÑÒæ`èIfg»L<¾ºk­ɶ¡ãsA艞ߗf3ºòì]o·Ïùçá†ýTÔŒ¤4»­'Ò÷Nvñyž]¸ý¢qÔ/]=¿+2ÈÆS±¸Yù×q°ÎŒÿÕ]ñ`,»“¯»æç½âÀ‡ž¤Wm¼|¿¦ƒñDì× µ¨Œ¡S•Âqsùöw ÕuM3¿/Íæý\-ç˜ÍÚQõèTÇ(„õè¥j»¼ÕqÝ÷¸e÷r)I™íŽŸ{j}ð–?M¨™°C®U¨mŸò眇› =ñ{ ê×ç´¿tû×Í®kæ÷¥%éqaØ‚vuæôj¬µ£P§ßé?¨.y {l•ÝÉ,ÈÍ‹þu_½ßÉÀqÕóµòQÔàþ™©jC§Ú\ûù©r½ªß hv]ÑÌ‹¸NZ­TG5¬ï7¶¨GYgãvvÓ©èíE)?k¶Ùqm×%ºï`uõlî¦S¾cDE8…¢áø©7}o©žçµ¸(¨°f^>žçá†Â4,¯_O¶¿Îös¯ µüþ~>×x,l'q>(¥êÄg<7šw>'þ•´ó%¥•¬*žx‹yæ® a‘eï|Ë7ƒ'oe`v:fü.öòýΆ‰ú+ÝÝŠaT<:}Kß1ž´–l¿"UÖŸ99;z?|Õ»ùÍãÂu¥1ÚZ|Ç8è"ÛìW‹WpG;Ý´ÿAÐ<±òí¨ß±œLyÙq-š—Åã|7çâ~^ó{çã\õ|=¼•ÌÜþ,9îä< ‚æ¹SŽOÂuæ}ñŸ‡[h;v'^Ò_ÕþÜíWíãÜ)Û¦8Æ`Å~ƒ kGñûÌߨ€šò³ßþ~øÿþ¡Tí”o–EØ)j¥a§/·ySk¾ôeqÍ;‹ãŽÇêösJé¬mŠÅtðQxýúõŸþô§õ·ßÝÝø‚˜Ö‰åéöšßÕ¬”ª¶îY'ܨ©»»dw2r§5åàÃ2ñáÅ hž;§GjkE‰‚¢¨GåŒcdj’ÒíÙE÷‰¥fE‰Òpµ¶S-…Ï7¦.6‚ Ùu%³“÷q3ˆ®¢Ñ šÇ–§Ûí³Ò̼,zG"~¸mj÷t’)JéP—ÞÕàiྂµdw2âv[¾´ÕuS‘x0ôÂÔJ#Œn»éÔ:íxúaiI@eǰ›ñjeô‡ždv²‹ö†QÍlÇ ½Ëû/"Ù£ÓxE¼P˜^³{¹”{^xÌݾ_;˜XžO¬Ð—ªS¢í-O·ÏF—šž¾,Z®hfùÆ0œ™%øÅ%ÓÍã‚ádÌöE2š+¥jò‰xå>ÙŸ¢åÞÓI–=*k£{Ú¸·`½4ÿA¿vP(Ôú“ï¿Aó|vÅ8³£*ê^Zâ æ¹ ÆS ÷ÜÉ”²+ûº¬› öÛ<.-W3ÛSËÆ û/"Z~*å‡ËÛ©Ü^X޲—KM\…Ìl¿ˆ¢”òšïœ·âÃöã×úÎyK®o|…çxq:\QnÛú܈ý33Ñd¤_;(Zžn¯(öóšï<›Š¤Sý¹'sFì­¶pŸÁz[ƒ«ž?+³{9Y^ß1ÓN}­Í¯oüeO/ØoÐ<k”]Íœ*h™í¿v`8³=Äãåí°È·µjûù=ÜÉHJ·ÛQé…­§Æùy¡¸Ìú2mOíB­4LÍwžÕû³5Ç×7¾HXë¢(êNfÞNvþÃwlÿP—»HïwÕÀ=ëV×-¿ù'ù‚~íÒÕÆ‹Íá§wÓ^·Õêz¹½ý̪ë° Ùõ”0»—Kù½«Á`èIf'NÆŠRÊkâ ëì7š'†ãOÖ~L÷”•ÇšÙ h†É:»—“ÞU2ÈÎÝ~ÁÄÐ?®Çe¬[ØÐêºÉÃW+ [Çx2Û‚Zi´í\Ï(&ßgXá³;™{ÉÕyÍŸ±·ÛÀ=ëQ-ò¸*W­TÇŸÕ›÷áÅdÚKMÜ·.S.K·VN—sk÷£uîdòùÙÇ¥tfëQMs«ë¦ôÓQž¬‚X½ßQ>_Þÿ8ˆ÷¯zþTE?=Íysïõ6»ýõ?Õr˜Î·»‰¢¨GeM¼a˜,•RuQª.U;¶îYÅñSaßR³…샫ž]W(¥ª­§ÖëIXœR-MîsÎãKîŽwWí¼#†ÿ×<.\W¶ÝÖEDÄwŒƒ5WR[çNÙ>ܯ7ÃY]“Þõ(Xj)?N{Ñ='4»£‹¸VâŽ{£—±uño¢?kf»mŠˆˆk Í ¦'E9kÛ¸—Oêý PìWQ+É´8Ú~Qÿ£Ûù¥’Û·&c¨®{—q¡óòíõgNn4¢£ãm;v§cFÍŽÃT˜N´ï;F¡Ö.6LMDôÑ|‰¸Vñ¤J©jf£8}˜ýÚ! {4¤¾cÔúáWr¶ÝÑE|ǰÄ.ßñé~ÐóNî²w'íÜåg¿ýýðÿáVM”ªíòÍœÌú¾ Ázýcnú²Ý*+V)µË7ó–ÕßJ;ë{ýúõ«W¯Þt°þH¨•†ë½¿×¸×`ýQ[+Rë)×*’ª@°ÞV¿vP¨1 XæC¬€w$X+ pû` €` ¬€'XSd ÜA°@°ÖÁÁ¸Ó`0 Àíƒ5‚5@°ÖÖÀ[Ö¥j§QQï¶+Jب¢,ÛF­4:jIQJÕN§QQ•~†”ð˜'GF­4:¡jé~÷®V«&eÛƒš3}áü€ÆÃd®m›Zø³ïµþ’—µÎòéžZôƒ··>EQ.l=?àZÅ“fð>ÝFP­4’°rüûµƒBMÔJÃNßëÀ–ÎlݳŠ+gYQJgñÙ#¾c<©õƒÙÙ‰+úõK¯mŸ ß·™X¬ÕJÃÖÅ1ŠaR+ÕŠzg£9W=±÷ëÇÍÉ€Uª¶Ë7Æ“ú}nÿæZDn|É$u­âq3‘Rµc¶Ïä½Kl®U˜M‘0ƒÖ µ·Ñ£ý33ãÍ•©Z=º03ŽQ¨õ£séôèêI½ŠZ¹°uÏ*f šÇÆNÃ>ÛoNõüùx‚µ¢”õ”kã$ݯ÷'b“­‹“ŒËAЯ_ºíò‘ÚšÈÐAóØÊwìvzvÍxÑŠx©Ú‰—:Å¿YÞÝDÈ\˜6[çNYËídEú¢V§ò왜†K¦ñ~'¹Ç+Ü3‹ßQ^_ÔÎdÿ£v”Rµ¿q2ºžײÄ4µÉEÜõW²ÄîYãþøã)›:®ø"dY\V+eͽ<$ O„ðØKÕŽ©¹Vñ¤%»é”xÃA|Íåëé]‘¾Èþ¡.ޱh_ƒú¥Û.WÔV<2ëÌ/À»éˆHv'#n·µáK[]7•ÛËN?Ü<.NÆl_$ d•Rµmf£X( …ÂD*+|°h¹÷q„)ݶs=£X,Žè§U Sf®g„û5œŒyq–ïŸÙºg …bÑp|ñ¡p¶0øæ»ÑAY®·#šž¾,Z®hfùÆ0?•ÞݦóýÚA¡P0Íí“ãiyº}¶>ž=: +´2U‹Hv/—r»É«£ _{4{ßßfIDAT¡Z)k¾cœ4ƒ šçޝ™U «GÄ9o¢”òšß“è|¦f:šóÏ!€÷5Xï¦S‹·K µ©êŽ hž;¢îσEËÓívµ©ý¼æ;Ϧ–lÃà圷îööõ”ß»ŠWZã…ÛÁÐKdÆËúh…µ~¦;EQã+Œ è_õ&ãðl;"ýÚAR[]7¹ñyK®o|í'³³*>jæ(€vâqÛÈÔx¶Î_Ë—âÏ&^Ý”º—K…5“Þ¯=sD?=;;Õ%1¡ñŒ·Û¦XÅDxJOwGNžnŸMõáúÆOåöîüÑoÞÃÛ¼xpÕóí|IiÍ©f¾¾ñEÓÊGj«>ìNF¼î`z›oG3ÛSDD\«p¨šð†á%A4 MQJéÉÌx}ã§Ò»ôûCOôü¾4›Q²ì]/iGFÕ‰OÞlk{,”ÝÉHJ³Ûz"à‡%1ýÚ! »Ý67ÙQ\Ý11éõgNÎÖÅ1êãgGe!Ŗ쟵Û|¼‹Ä…S«ëšå°?ãÖ†žäø5L°nu]ÓÌïKsÃ\ôk—n§|”mNÞÂbê£árdf*RÍ…²}0]§nx2HǽÙÚšÙ ºk–ÞšCQÔ£S]FÚSJÕvù­Næ`è‰ß[PÌV/‡e0ª¬“­çNYXUb‰iŸ ÃF’e!"ÍcçÂ.WÔV} "=Ò½nŸƒ€÷߉J]%¬‘%ãjüóÜû(ÇÂBëäS¥jÇÖ=«8ŽwQY…~8]Š0¸êùZù(¦Ò‰Ï ÞoïWDöÏLÍï] ÂR ×*ãJäU ?´§(¥³ñg0ߎ°dynqNb›þUÏ_£©™J˜ðd(UmݳNZ­ËǪ̃de0ô$Q*=•è¼*GuçÙ£²–,Ñ™sMð‹JAšÇ…ëJÃUøŽq°æ-,ZçNy|ß=¥T53ŽQœ¾ÝÞD)Âè®AЯ?sr¶ÝÑE|ǰÄ~#+¾AЯ?±Òm»mt÷Œæ¹SŽ;)²¢j"š'V¾• øŽådî´ÿ“·ò0;3ìÏ¢ÇE¤ubì\ØÑŠû¨ÿÓ·:ñãÉêºöÙ:Ÿð¾.®UŒV¦­|ÛlW¥xÜ W©Gã)¾c< _ö':­|gê>ŒÑ§Ïü€€ò³ÿý÷CënÕÄ›¸wõ µraçâRŠðKOdý “N©Ú)߬øªšÛP+ ;}yÛÊr€{ðúõëW¯^mô’·ßkÐ<ž½gÈ{iêî(ÙŒ|Ôu ­ËÓíínQ²ÎõXXUÂï-ø0wP |`ÞN)‚5@°ÖÁÁ Xk€` €` ¬‚5@°@°ÖÁ X Xï~°.U;ŠúQ”¢¨•F§Ó¨¨Š?¨VPµt¿{W+É]ßÝA5*ê]6«„'Ç]UÆÃw§Óúý¸‡ÉüÑ6µðgß1jý%/k;åÓ=µ>èA2¥]Øz*~ÀµŠ'ÍÄï>µÒHÀÊqè× 5Q+ ;}¯™¾tfëžUì¯LE)ų(¾c<©õƒÙÙ‰+úõK¯mŸ ßøLÅuÐ ø=D°V”Q Ç(†9L­T+êIœÉæ\õÄ>ܯ7§Ÿq­âq3‘Rµc¶Ïä}ËÖâZ…Ù£ ‚0E¿ûgfÆ1VPEQ.ÌŒcjýhNO®žÔûA ¨• [÷¬ba¦‘ ylì4ì³ýf⨕Rµ]¾1žÔû÷7wÙŒ¸—-~Àâˆ(JéPO¹Öxu³_;N®tΖ@A¿~éjå£%ï°·Î_2;Y C^£¢ÆïüÇïû+Éj€Nµ4jmòñè¹%í„9~ª¥TíT+á¶ÕRøü–e[”|Œû“º©ã jE\V+eͽ¬’4Ç^ªÆ‡¼›N‰7Ä×>~*½FóC]ãxA4Ô/]­œ™ ylyºÝ>+Í̯’è¹ã?^1ßb'‡wTÄ>£Œ;Ш¨Š2z<ÞóÄäÞ¾?ë ×»›®¶ºn*·—]wó”nÛ¹žQ, GôÓŠª„Å ¹žQ( …‚ádÌ‹(©ïŸÙºg …bÑp|ñ¡p¶0“å»Å°ËÕâvDÓÓ—EËÍ,߆'Í õk…BÁpüõSµ)VÔO·ÏöÃdzG§áq…Ž×¨‚ÈîåRn7¹êôkOâ1T+eÍwŒ“fAóÜñ5ó¢¢*a¡…8çÍ P”R^ó{rØ™@E$š³sÙ<.NÆl_$7VJÕ¶™qŒh¨ã:™äñ-w«”¶­§D3ÛíøZh0ô$³3u©_',¢™¦XÅB¡`8¢ŸFó¾Qî(XK¸Ð¹PXQ8¨MUAóÜýpÑ ÷õ”ß»Šs‘ïD¥ƒ¡—ÈŒñZì ~¦;EQã¤ýÄÂë‚vDúµƒ8¤¶ºnrãó–\ßø2ÚO´„¾4©m´¢<+ ²Îyt¥Ò:w|-?^úMþ¼º)u/—òo®§'¥_{æˆ~zvvª‹ó,ñVÃAÑòt»Ýn›bâ)=ÝMO·§—¢¯oüTno*ÔÆ­Åã°Ÿ×üÄîæïšW)EÃñŵŠÅÑ%F ×7þ(y‡ Õ;™Õ­Å¥G‰óg³þ܉‡·yñàªçÛù’ÒJ®§jf»cF‘§p(Vö†a4‚æq¡)"J)=™¯oüTzW‚~艞ߗf3J–½ë%íˆHXCœøÔáÍöGµ ÆzÙŒ¤4»­'þNV¤/ýÚ! »Ý67ÙÑÜUÛAý™“³uqŒD•H©Ú15×*[²Önwòñ.A³ÕuÍrØŸqkCOróö}}㋦•ÔV} ÙŒxÝÁ¼ãóðæçÓÐKåw%»“ö<ÙËÊ•Èì5ÅšãýØ8X·º®iæ÷¥¹až úµK·S>Ê6·ÎˆWW‹‚tðv'‚¶fv„îZ…¥·æPõèT—чö”Rµ]~«ƒ:zâ÷wäH ?–Átª²N¶ÎL¥à0=êže‰iŸ ÃF’e!"ÍcçÂ.WÔV} "½(‹N½)1õ‘Öp9{ng¦›’[\Ød÷Ò7çÝôé^v˜Nm’ã<½Óþ¬ç(J4»®„µ¹£DUžûáÅXXh½åM‚W=_+E9hÿÌÔüÞÕ |+?.(¬=ÇÚS”ÒÙÛþ°ZX²¼¤HFF%.k45S NJ©jëžuÒjX®fF¥ƒ¡'‰RéѨDó;ú¤iö¨¬%Ktæ\Óˆˆ”ª[÷¬âøò êŒ~8]Ê’˜G¥TM¾s°¡ë?“?ÌÉpÐêz¹Ã|Æ¿¹ ‚þÐ-¿^<\,˜ßýC=*F¿³þl *i®+ {T½à;ÆA½UçÖ¹Sžß½uRcý‰•nÛx¯ÑýÝšçN9ª—-­š‚払oG%(¾c9™;]±ž¼?·Ùé˜a=."­cçÂŽVÜGýŸ¾Ï·ïOV×ÏÖÛ„ww­b´2måÛf»*Åãf¸J=OñãIøª°?ÑôŽÇ9>ÀR^ó{çƒdp73ŽQœ¾ÝÞD)Ëè~ØAЯ?srá~}ǰÄÞjüƒ ?ôRºæZ'"ÒõLSs£EôÖ¹S¶Ãw0\ËpʧãWM”7£óê.ú°YhüÙ¿ù§á¿ýû[5q×÷8¥j§|³â«jnC­4ìôåm+Ëßü¹«¨Gvúòã=1Àýyýúõ«W¯6zɃÛï5hÏÞ3äV¦îR’ÝÉl÷!¶EëÄòt{»[”¬s]V•ðûpßÁ>Eß8¾«ÆÄWs„‚ ybì\œ©­;þ*DEQÊ™¸bÛ'«Û—‚˜·S €` ¬‚5@°@°ÖÁ X Xk€` ¬¬‚5@°ÖÖÀ»¬KÕN£¢~T#¥(j¥Ñé4*ª¢Äª•F'T-ÝïÞÕJcr×wwPŠªl4¿Êø°ïñ4PÂNÜúW¶Îlµ¤¬?ï³&óGÛÔŸ}Ç8¨õ—¼¬uî”O÷Ôú Értaë©ø×*ž4¼ûÔJ#y+Ç¡_;(ÔD­4ìô½fúÒ™­{V±¿j0¥tÏ¢øŽñ¤Öfg'>® è×/½¶}6œœ©¹ó;Õ™ƒfÀïÀt°V+ [Ç(†9L­T+êIœÉæ\õÄ>ܯ7§Ÿq­âq3‘Rµc¶Ïä}ËÖâZ…Ù£ ‚0E¿ûgfÆ1VYEQ.ÌŒcjýhNO®žÔûA ¨• [÷¬ba¦‘ ylì4ì³ýfò¨ϯdw2â^¶Þè(¥j»|c<©÷ßø¹ôç¼_ˆˆ¢”õ”kW7ûµãäJçì[áAЯ_ºZùhÉûã­sÇ—ÌN6 îŠWÄõJ²ª S-Z›|?§ìa[”|Œû“º©ãZT~0‘)ÕJYs/ëƒä#ı—ªñ!ï¦Sâ q6öSéÝ0šêâÇ ¢ù ~éjåäȬ3¿ƒ39,£â‹‰‹¸$côx1ns/o£‹çe?¯ù‰ÃŸ;þkŠû#­®»Æi¿Ã3÷º|„ÞæÅƒ«žoçKJ+¹žª™íŽEÂA¢D׆Ñ<šÇ…¦ˆ(¥ôdf¼¾ñSé] úý¡'z~_šÍ(Yö®—´#"a qâS‡7ÛÕ‚ë dw2’Òì¶žø;Y‘¾ôk†4ìvÛÜdGsWõgNÎÖÅ1U"¥jÇÔ\«XlÉþY»ÝÉÇ»HÍV×5ËaÆ­ =É­5¿ó΃¡—ÊïJv'íy²—•+‘Ùk5ÇÍë._é¾¾ñEÓÊGj«>Xô‚uÚ¸k¢¤%Z~ów³ƒ~íÒÕÊGÙÉ\ZŒ V¼> ұ݉ ­™N§Ý¶uÏZ~kEQNuqŒÞú¿Gƒ¡'~\Ÿ0*R/*G%.sÍŠí¹Ë§aUI²Îdª,äÄpüqåôŠ˜ìN&¾bY>¿‹›ØKßœw%·—ÝM§VV‚Ìì{n<ž¤Vm;×3ŠÉ÷OV®-g×Y?¸“`Í®+amî(ÁT㟗ßÇ7,ÄÝò}ðÁUÏç¶ý3Só{Wƒð­ü¸Ø`€žøÐž¢”ÎÖý°Ú} K–—×݆%.k45S NJ©jëžuÒjX®fF¥ƒ¡'‰²èѨDó;ú$bö¨¬%Ktæ\Ól:¿×7~&˜“á Õõr‡ùŒs-ý¡]±)jåbÁ¼ìêQyâ|PJÕä;"RªvlݳŠãØFƒ£N—Ö,mg¥ý3Ss/—Ý`ž¨¤y\¸®4ìQõ‚ïk‹Ö¹S^p_¶5Rcý‰•nÛx¯ÑýÔšçN9ª—-­š‚払oG%(¾c9™ò]ŽÑäý¹ÍNÇ û³èqi;v§c&û?}Ÿoß1ž¬®ž­Çï8îZÅfˆDÇ^•âq³ybì\Ø£ñß1ž„¯ ûMïxœã,å5¿w>Øn~ƒ ?ôRºæZ'"ÒõLSs»aû­s§l›Ž)âZ†S>¿j¢dè¸Ïœ\Øß1,±Ëã 3ãÅéÛíM”ÖŒîϽ¤%fû³d~æ„ÆŸý›þÛ¿¿Uw}aE­\عÞèËMÂ/=‘ø³eŸRµS¾YñU5·¡VvúrQd¼ûùUÔ£ ;}ùñN(x÷½~ýúÕ«W½äáí÷šø ᙺKIv'#_üØ´N¬|Û®ï%‰FU%ÅÖ››_€ÑÃw°OÑwŽïª1ñÕÜ¡ hž;§Gj뎿zPQÔ£r&®ÀöÉêö¥ Àf‹RŒp{k€` ¬‚5‚5pÇÁZa€Ûkk€` |0Á:`€Ûkw¬¹+pÁÁ XkkàíëRµÓ¨¨ÕH)ŠZit:ŠªŒo¤¢VPµt¿{W+É]ßÝA5*ª²Ñü*ãþÇÓ@ ;¡(‹úÎHµ¤¬?_÷ça2Ç´M-üÙwŒƒZÉËZçNùtO­úAŒ2G¶žŠp­âI3xŸ¾F­4’°rúµƒBMÔJÃNßk¦/Ùºgû«SQJgñ,ŠïOjý`vvâã ‚~ýÒkÛgÃÉ™š;¿S9h¾¹™]Ò€wă8P¶ÍŒc …B¡Px&‡³«˜W=Ñ÷ç<ãZQ#–«™í³Ò{·^èZ…‘dª‚~í P8¨½ùl·ff㸹2U«GfÆ1ÂÎŽè§Gáz­¢V.Úvú²8縚dž“1Ïöל_ÉîdÄí¶Þì,éÏÍWru`û`­(¥C=åZãÕÍ~í8¹Ò9û–zôë—®V>Z’EZçŽ/™lÜ5® ˆßÖW’UjœÂ'ž[ÒŽˆ”ªÓí(¥j§Z ·­–Âç+.Ø¢äcÜŸÄÐM×¢2†‰Ì§VÊš{Y$i$޽Ty7o8ˆ³¨ŸJï†ÑüP—ÅÑ|P¿tµrrd֙߉Á™–QL(ÕˆÃëèñx„&&%~t¼ò¾¢?ÙÄé ,™¯%û šÇ–§Ûïãu x§‚õ–k­®›Êíe×Ý<¥Ûv®g‹EÃý´¢*aqB®¯°fÌ‹(9íŸÙºg …bÑp|ñ¡p¶0H廉•òQ;¢ééË¢åŠf–o É“æ†úµƒB¡`8þú©Ú”håÛòt{´œ=: +t¼F5Ev/—r»É: _{¡Z)k¾cœ4ƒ šçޝ™U 6Ä9o¢”òšß“Ãμ +"AМ3—3ó¦Õ¶­§D3Ûíøf0ô$³3yã|¿ˆfšb§VÖ“ãV´ÜuÎ7ÍŒVâ‹–§ÛQ2^2_šY¾1FûM¬7 †“1ÛÛ]z< :Zô–z4ÏeoÏïê)¿wç+ß1žÔûA ƒ¡—ÈŒñZì ~&'EQã¤ýÄÂë‚vDúµƒ8¤¶ºnrãó–\ßø2ÚO´„¾4ñm´¢<+ ²Îyt¥Ò:w|-?^ Mþ¼º)u/—òo®§'¥_{æˆ~zvvª‹ó,ñVÃAÑòt»Ýn›b%)=ÝÖq]ßø©ÜÞä›Óó¦Õ¢áøâZÅâèÒ ë”¼Ã…êÌêCs­b8e‰yŸ·™ópþù·#­®»ÆôÆïÌÌ^Ä£·Ý¼€ÜÃÛ¼xpÕóí|Ii%×S5³Ý1£S8hŽ·ö†a4‚æq¡)"J)=™¯oüTzW‚~艞ߗf3J–½ë%íHXC<ñ©Ã›íʵ ÇÍ[ jv'#)Ín뉀¿“éK¿v`HÃn·ÍMv4wõwPæäl]#Q%RªvL͵ŠÅ–쟵Û|¼‹D`mu]³ögÜÚГÜZó;ï<z©ü®dwÒž'{Y¹™½Xsܼî`³óíŽ]ßø¢iå#µU烒`#¢¤%Z~ãýÚ¥«•²“¹´:¬1kÑ»A[3;N»mëžµüÖŠ¢ê2úÐÞt Á›7zâÇŸ}\p¼¨•¸dÌ5+¶ç.ÆU%É:“©²ÃñÇ•Ó+j`²;™øŠeùü.nb/}sÞ•Ü^v7ZY 2³ïÁ¼‡×8ßîZi´í\Ï(¾•¨€!XA³ëJX›;JÕøçå÷ _·¼ÂàªçsÒþ™©ù½«AX¬ÐÚS”ÒYâCooEX²¼üa‰ËMÍT„“RªÚºg´Z'–«™QéÂ`èI¢ y4*ÑüŽ>ù—=*kÉ9×4›ÎïõŸÉæd8hu½Üa>ãß\‹AèEWlŠZ¹X0/û‡zTDž8”R5ùÄ:ýÙ?35÷2¾€ÙT©Ú±uÏ*>Ùºð‘‹JAšÇ…ëJÃU/øŽq°f¼h;eûp¿¾EõDôëO¬tÛîÄ{ ‹§¥yz‰ÐÒª‰ hžXùvT‚â;–“)ßåMÞŸÛìt̰?‹‘Ö‰±saw:f²ÿÓ÷ùöãÉêOŒÎÖ?„ww­b3D¢c¯Jñ¸Ù<1v.ìÑxŠïOÂW…ý‰¦w<Îñ–òšß;l7¿AÐz)]s­éz¦©¹Ý°ýÖ¹S¶ÍNÇq-Ã)ŸŽ_5Q2t܌·gN.ì¿ï–ØåÕý™mgɼ,œâRÕÌ8F‘òp‹Ðø³gÿ4ü?þþVM”ªíòÍTV»UƒjåÂÎõF_n~é‰ÄŸQûø”ªòÍŠ¯ª¹ µÒ°Ó—‹¢çÝϯ¢]ØéË-'ôÎû0ëõëׯ^½Úè%o¿×ÄgïÈÔ]J²;™øøâǦubåÛvux/—QUI±õææ÷];ßîÂÃw°OAóØØ×¥L}5÷G(š'ÆÎÅéÝߪBQÔ£r&®ÀöÉêö¥ Àf‹RŒp{ä_2Ü>X Xk€` €` ¬‚5@°@°ÖÁ X Xk€` ¬¬‚5@°ÖÖÁ Xkk€` ¬¬€U~ó›ß¬|„` l–­gS5ÁØ,[ÏMÕk`µßýîwÉl|„` l“­ç¦j‚5°Y¶ž›ª ÖÀÆÙš` Ü‚5@°Þ ÿ? ¦yŠ¿a<IEND®B`‚fwupd-2.0.10/docs/win32-term2.png000066400000000000000000002741621501337203100163550ustar00rootroot00000000000000‰PNG  IHDR²½”J;ÐeXIfII* ²†Œ”(1 œ2ªi‡¾ÎÎGIMP 2.10.362023:11:28 15:30:59 *í5‡„iCCPICC profilexœ}‘=HÃ@Å_[EÑJ‡v©¡:YqÔ*¡B¨Zu0¹ô š4$).Ž‚kÁÁŪƒ‹³®®‚ øâêâ¤è"%þ/)´ˆñà¸ïî=îÞþF…©f×8 j–‘N&„lnUèyE?¢aa‰™úœ(¦à9¾îáãë]œgyŸûs (y“>x–é†E¼A<½iéœ÷‰#¬$)ÄçÄc]ø‘ë²Ëoœ‹ûyfÄȤç‰#ÄB±ƒåf%C%ž"Ž)ªFùþ¬Ë ç-Îj¥ÆZ÷ä/ 浕e®ÓŒ"‰E,A„5”Q…8­)&Ò´Ÿðð9~‘\2¹Ê`äX@*$Çþ¿»5 “nR0t¿ØöÇг 4ë¶ý}lÛÍ ð \imµÌ|’^ok±# ´ \\·5y¸ÜŸtÉ)@Ó_(ïgôM9 | ô­¹½µöqúd¨«Ô ppŒ){Ýãݽ½ý{¦Õßpør¦Ù‡– xiTXtXML:com.adobe.xmp &çbKGDÿÿÿ ½§“ pHYsÃÃÇo¨dtIMEç ;ÄœÉj IDATxÚìÝoˆãhž'øß“$½Õ3Ý™toÓxlW¶oÆÚ.èžêº:6ŒìºâëÅ,½UâΑÇp•(|³ÇÜ ÁE@ÌõlBø8õí»w¶ÈáÂQ‡ªhvàì.*,{WTÕl³ öô¹2mz§7³r³kêÝé^<²,Û’ü'þg~?3tEÊò£GÏ#EzLDD·_¿üê,iê‡>ø`àý|Ū˫v.•Zù;—¯_@·ÀU5X6ÎÜøÃýçþï÷‚AÅ/ùËýçþÃþp³2—Ü£øÏÿ³×V,è_üËzÔûã'™ }üé“;§ôÜzõ­{¯®ÿéêã­SÌž|úþ{ÝzóÞ½;Þ??}H·î\ñcÛ³?ùôý÷>üôås—®Õ¼?ü`‰ B1ÇÀ¦¾ñoüãßû½`PÁɹc-ç>3ûÉ£e~ðƒ =y®{çɧ~t+8¾õê«w®Mío½úZæñ•ê¢Û·¿ŽSàƒŠ_þò—§'ˆè&¹îÒ•\×%"Èr]—ŦKÍoܺEúðÑ“WoÝš ¿yrÐí×_Ÿ\?}øáƒo¿™|ðÑc¢Ì›÷~ð¹—?4É|ñ/¸>üðÁÇ·_¿õÑGƒ°O§yG™7ߤ> Þ{ðQæÍ{oЇ>¦ ·ø?¼ŒŸÌô_“5ïÓ—&ËN¿þ&}0wñ÷É£ÁãÌkwÂcТ–ïlÜ Œ%¯ÐÐf mêxƒ­tçÔ•¿ýúÛo½úù|ó.©€·òàõ·_{üÞgß l—oáɧïòõ·Þ À5øÙÃcfë¡íðp…§ð…q]쵇—¼I0³NhIO±;´pHÄÒçû®IPAD§ 'hiÖS0xà?¬K "z9R<üð½n½yï->È!ºí­ýø£3oß»wëá‡>xðäõ·ïÝ»E?|ðÁ'_=þèñk÷î½òéÃO¼¢¹{oR`”FôøÉí·ï½q‹ˆè{÷Þð†nŸþà­7f×ôkHO>}ÿ½÷?õ–O¾þðà ¿ªYÔ²]²Â™úßñÊœo–¨¦^<¸ù78õ[éÔ•ŸdSÍ6o¼;3+?Ì|ðÙÃ7îÜ¡‡Ÿ=¹MO=yõÖ­'tû ¢Ï#J˜ßú«·ÛáÎ =µû‹]<ȼ}ï­[D?|ÿSzë­7?øäá«ßùìzóÞ«·BÖY+p•Ýñ.öÞ¸רՙý[23;ˆ~8±Jh1 (èÖËz8Ÿ|þäöë|àyëÕ×2}ìÉ_ãÕ[Dtç;zrûå[äýüñçOˆfÆ?·_ÿÁðO¿~ûöàƒ÷#‡®·3/nŠxWtoϳŸ|þ„&7n½úZæ£Ï>盘|ýÎ÷¯ñ>~üù\5c‹Z¶³ËV©ÿB³PTSF«ï=øˆˆ(3ûvóô•ŸùVH³üªù×oß~òù¢Ï?{’y-3øøÑ“WirXEE [_Ú¡{JQ»¿øÝǯ‰2ß!ºóÆ›Ÿ=xðAæÍÉa²Îêú•wgÉ!½q®1Ä ¡üd'":‹¬§•ïQð–}ãá'=~Lþ(†ˆøÕæótëÕ·î½úäÓ÷™Ûæk™Á>|8á}úp…¢6_ÿàŽœ¦©7¯üd–÷“GƒÇ§Üå[/ghðáÇO2/ߢ[/gž|öÉcòî¡|ýöíÇ?Ÿl1[_Ú¡{ºd÷'Á­¯ß¢ÁÇ3‡ÃÃ? 7ï½IL‡unwV8¤—U^¸p‚f'joVæJ÷(B³›BŒù€ÂÏÌ÷††Þeü7ÞÌ<øàÁ€èöë¯gèLcŠ™Ù©·ˆn}'óÁ“™ÙSw~ðúÇï½÷à#ºÉx "wkÞyãí×ß§ÝÝ~ýí·VßyãÞÛ·'ßâß#¢ŠZ¾­ÅúGÔéTM½qåoßzüáƒy7ÜYhÞµCŠü®Ä­—3O>L«[¯¾æíe2™Ø­‡µÃÒ_q÷ùj“{q™×_òÑ“×ß¾C·¾þúÇ“Ésë„6Âévg¥C:¾pŽ.ø½Q~ò“Ÿ,¾áŽ¿á޽òGïþ»?þ¯B?û“eüÿýΊý‹YÿêâXË~ Âsö¼S®º›Kã´lQàžÄ‹ËzˆwMˆ( ¢Dˆ(\A7‰è×~í×ÐðìÙ³§OŸ®õÜ£€Í!¢D€ˆQ" ""úÃ?üCD°y8T ¢€%áÄâψ(`ÕpâOþäO¢‚ D‡‡~PˆÖ 'ö±WþèÝá?-Ïž={úôéZ_Á= Ø" @Dˆ(àz¹‰&ß`0X}åL&ƒ{°¹ # ¹j5* ÕRŒ •†e5*cþB¡Ò°¸ª|¾[*ÙMŸÝN5*[«Ùt·Ïñ0`¼ŒEÕ‡÷HUf«÷׿±û{V.ìø<÷ƒÿÚ?þø€ÕÝ hÚšÈvLu«Ö‹ùZëÐ,ï…z¿çºÁ¿Ð;G†’ðØza¯Xáê*à,m‡^mKª‘PiÉs fäCé…Þ²ÆdL>ð{‘S½[빋½ãï—ëöêÇ£¶q0œí©Ðþ«ÌVóâz6¦>çI^Âþò ª<^rø-u1ÇçsÙþ°ºþHº­¥Lµ I’$I÷i{ñºõŒþI—”íRÈ'¶î¢Û¢Ö>Ùu»hëÒDp<纽ږ$mÕ.~P[:ÐR¦ºÛ\N;GZÊTyåU“”ý~Í› •£¶‘<.„ìWsW5SÚAiÅþ¥l:Ev§u±MSŸkõWÜýKÙ_@û\¯ˆ‚1y[IØúôzv¯¶¼¶½˜=⺽ú±-–wb’4Z‡¦C©tÖ‹XÁO^ðSX0¡ÁªúáÇìrﳘrˆH®Î—ÃäªU­ðu«2ÿ¼±$RаAöÈ´>¦›Û¯U2.˜P)‹öq½\Òì»\õw9“LÐhØ÷áN"™á1ɶBÑ1I¿~l‹å`ˬҿ33Û,“ñùLV‰?jŸ,÷[h¦Sü¥Ó{-Kê“ ,¦¿b¶ë6wõ‘b¬Ÿ÷þòÊk"%cn/V;_ª§ ãÛèã-ò8iºåçï9”{Þ…?°vD±áUÀVÇNäŠÙUWO(†‘명BA5IÙ¯Œçáäºþ5õ”vä K†2Ò%I*TÓ!'0^,‡0òÀ½‘I9$*Éã‚n“¨•ǪjúCì5õj[’$©¦³ú°L#ï^‡>RŒÉ€ìÎ>ß/nw…DŽl1—°;Á”$·W»ë·¡P)‹Ž©î5]×u›‡¦#jGñ\2›®Ë˜œ.m[VøÈÉu›!}¹Ð¿|PÖ6”‰Z»í^ûÃ¥Òs£@?°‰"jé…¹{)Áv+èö*Ç›¨y÷^ úH1¼ ¦¿D­ŠþI×1ò2k¯ ‹ZÛÒ¼¿ÕÒVsºöhÈc×mîJM"brrv°<;‰d†Ü^o8"%_¢fÓRw1åŸ'03¥z¼ù^Ù:¿$¼¹l:E Ñh+aT:KÔ£^mK¥†Ñnkël(ôúw¿~ßÌ ™j !J®Zšhë…B‹Jí¶•÷7¹¶:¶Væõ™–6Qn¥þ ;†£D>CÙtr4¢b–Nˆƒ ÛmÔé¯w¼±ÁØ!Q,ï­z䌋Úßy™ðó…(byïÌŽÛðã-vý˜£yþ÷€pîålvÞÀ:E«ckZ¾DÍ5ÿκ½Ú±m•w²ÍÀLßé5ãUFo3#ŸÙ“¨Y<4±u)ö‰7Œ ;û ™*_ÉÕvùRµ?‘Ó ë* C!S-„vÜÅïïÊçKìytFÇíâñ¿þYm÷Ï;XÑ òÒè‰çßO†VUÿçøçúóäö Sú']G,ïx«Ò&:Ý“>Ï^ðó¤þöOg$3&fô^ >-!>'›gs­PÔBÒï¹j(#}¯ÕÚÓmQó²³úÃò]&­âõïdZsv§,³Ñb¡«õï`ì¤òÛ9ö[Qn;ŸrÆ"×í G$æK<Ú9Šè—Ò¶âM L®ï9­RŸÒ&ÚÇJåªe(#½°Ê¸ö"öw1-ê|‰\‹1>‡y:3!æ¸ =ÞV9ÎÏê|¹àóÖâe=5w¥A¥aLSÝZqdÖ:4ËÆv©¾Á%?×íÕïêɶaù[å‰ÑÔ<4Ë^Š›¨àºÍ==ßö²­S7Sgzbö=šei¼>Qˉ¨µ§¦ ïˤþóïëpLõîòúÅTþæ[/x÷"ô|[kW©°Ûä÷%&íIŽ©ÞåßâõñºwÚÎÓÁe^tº‡ýÍú×u{ÃQBm}ˆ:#MmïvGëÐ,ü^“­«fyú­™ì¸Ý¦w<Ü7s¼þŽ©êd”—×g±œ˜~‰ìb¹ª¥LµP_åQ³³¿<ËÈë2[—v›ÑçKøò Ú!ü¸<ÞÜÐõ78ÅεœÍÎ;Xc´üʽ;üç¹Ú.ç©§*P¨9?{¿µVO¦zîœÉËÎb•†‘<ŽÃ}ÿ2açÈHoØ¡g^Ÿs?ÇN·¿éÙ³gŸ|òÉêëg2™›§ßj`‚ô™{öT6¢™¹Ù/šÖžžoÕá¹ I½„–Bëâú÷ªop 7¯`Üæ®šž¦`ñ4…ÓÎÚ¼Î\·¹§¦öc@´Y8Á„rÊOŽX{Hyú¬'x>lõt­»‰&_&“Ykýs¼GÁäjð-ñïµ€ç•PiXÜB× •†÷&…¯`9–cù/ùÎfN_ü6€èˆ‚…ŒþÎ’­K’$mÕüiårժʬ?ññ?XÁr,Çò‹_Åu{µ-I’$ÝÆo1ˆ½õÊÿôîð}ç<žñíÞç!êerÕâ/•vÌ™Wm`9–cùÅ/wޯĀ+åÙ³gOŸ>]ë+K²ž‚w½-«*nbÈÕ%ËÛü×dXé¯;WxUö¿Ñ¨,l»Þ6ÐÁÏ“ÁØÁr,Çò+²à4nć;GF®«J’$I’j¦´£ž%TùN/×mÑ_.W-t¾¼¸SÎï«fÈ3Q+Õ‚$IªIÊv‰/,ÊH—$©PPM‡SÅû†Ÿ'rÕÚ§ûݶõÂ}Úfe`9–cù/8¥¸¬'&TŽŒäqa¿þŒ1ù ]/¼lÎÿnŸJÁËœK€áKòØ{t œlpyTÚ \ 1Ý'TÛû‹±"–c9–_üòøKÈzxqlõQÌ. L¨JÂ_Õ1Õ»õ¾L"M#Šžëz÷:v›þ}ü1{þ" ¸Q ¢ˆû>ŠÁØI$3D“?#™dˆ1ag_!S•j½I$òíl:E4Þ|oDͲ4""[—ð— àJŠ™Ý?é:by'ËÿU:ÐD§{Ò硆}â©Pþ ìÀúL®ïa¬…19/ÚzÁ›§1½§!Wù,p¹Êgq{ÿE/\–À=Š„b´ïgÇTïÖ{n¯~WO¶ K .t‰š{z¾­µ-ˆS7Se""×íÕï›9ð"ÇTu2ÊŠË‘IÎäKeÓ)rÆô/À•só ÖÉmîªé†1›ƒ5÷ÈZ¸Nø$ûiÖ\Á„UgŒö€è?W0ë .Åó’õ×" @D±Æ„JêÊ,æS«Q»ôªÊÕKxáF|û\÷þ½R„JÃâªòºÇ!ãǹ¥§,ÿJG×Ý¥ü¸˜ãž›ˆ‚½ýÝÒw¾\úý;¿óóïþ†72øªðA¾ôóׄïOFŒ}m÷µÒÏó“ÿŸ|ä­é‘ýÆŸæg÷«ŒˆÞþn`ýÉ:1ëû‡¯ùëÏUoÕ1ÍôQ0¾Á êªèÕ¶$IRMçÅi·ëÞÏåqøçp¹nÑ_ùÅË_ù5¢ðßûæ·ÈùÙŸÝΔ^êÿÛ_M—?rºo>|ÊØ×þÇäÞÿG¿úÍ¿ú""úâÑW2»_uª¿šy¤Ï{Õzèíï–î}Ù}óapCáëÿÎ? ?ûË¿àËßþîko³OÞ[ó1An¯¶%Õháaêñ×L]ÿk—纇8^¬ˆ"Ô£/Ÿc_+Ý~éÑØiQæÇßüõꯞ.Œž¶ñû·¿ú}Æ~FDô«cº¶f„õÙW…'èÏþòÿöÃŒ÷þêãé§ü†túG‘f+ þ`ÄéÓi…É¢¹·tϾö›ìØ×íñ•“Ç:išH4÷ô[¹jñ¥D¶^ØkNvÉÕöäÇœ™Ô¤Ó¢BË™«g°ªÓõW}ŠkHû¬¸ÝÛg¶¯üi§ˆ“׷Ǿs}Õú϶Ãâòø~ Ù¯¹73Îþ3ªßCE‡³åÌ<Ñs±üøv[Þ´ùCCë¿n?2¹ÚÎÍ”¢$ÈÖy/ÄÿñíñÇáâ»ýK¡åó¯ìÓýû´ï¸K¬µ_Qå‡ÖgÕßcãgiýW?_àz¹AD?û»õðý;¿Ãó‹~ë+/yŸ¿ôíâW~ñÓ¿£Ÿ}ñ«—oûû —÷û?H¼ôèñßþ[„ôw¿x”øOß^=yzaýï}ó[/ù‹Öç»ç¢f$ ’$©&)Û%¾0êî|vg_éÒÄ*oï5ôIùû;<›\¨4ò/D·EíÈ[Îäj[K™ª÷Qø0b¤KÒ–?2-§tÀW“ ÕtÈ1Õi8A^ýõ‘b”6kŸ¨ínÔ>å±:S>ßÍ\Wå…¨fŠ—Ï;E·É1U«×¿ ã@f,¦bÚg±ž‘ú']'‘+f½c¦˜K8Ý“~\»E‰:ƒõ,èvpðºX~L»­ÒÁò×ZÿMúQT’ÇÝæ½ šN"™‰nÏÅö·—OÏï8ìG”JÏuôhØ*Ÿ¯P #×U …‚j’²ÌÒ\ü=°îïŸÐòãëZNÌùSÿåçË ýW7¢ "úÊW¿Ï¾Vú Ñ$lxôÅô÷ñžëÒßýÂúÊ·J/M¿ùr"÷ó|é¯sߣ¿þ‹ÙD&çs¾zïå__¹‘ë3öêM¥˜Î¯àÙÒVí”ïJ󯣇þáû—×™c:-¿~lOþnöj[þh»Õ™ŽØJyÑ1ïGü5ÍxÈÀð%´Æ„tŠìN‹·ÒI×1&çEÇQõߨ}¼‹‘Óòù˜¢Þ÷Æv;MÿR«cS*l‡øö ©g¯ÍsE1ƾ3ü(k·•ÍÕsn°¾zù+¶Ã•à˜‡-Œš©t6j]·Ç™iûwZ—yÆÎ$ÞkT~zNÇÎQåû—üûÃѲßëþþ /?¶>‹å,ù}Yÿóeƒþ€«)õôÒ·‹4xðe¦ôRÿÿõF_+Ý~É;DäºóÓÇß ¦'ñyo·tðýû¿ ú³GƒG?øö÷ýjÅJD­ïºóNçoû¹Ìå6S¯¶¥RÃh·5Z1å&b0'Tf’’œ± Œ:ýЯ$M!ǼßZZŽëö†#Rò%j6ù_h§; "ʦS”§ï '"'%êMýϬ}2É„3ÌŒÈÉÌ&õ Õ±¼¿öúõc[Égˆú|”ÖZÒnë×?ü0Y¯ü¸vˆ: ¯–Èýmul­\ÌÖûTÌÑÜIsáÇa8Jä3”M'G#*fé„È–•?ò(Ôu›»R3þ÷À&ÊgrD}úk?±õw&ýW%¢øû/}å¥ßúôè—ý÷é[?ýf‚¾òÅgÏSžèåßúÝŸÿÖdõ/¿ýýGÿñgï¿?úìÞoïOÿ¡óοwƒ‘ÀO¿,ýÁ7¶b%æÖÿÙßýâQâ[sÁ/ŸgÊ3¬*­1hž vö2U‰ç.ËÕvyºV*bŒï˜êÝávÛ8¢`’wt9$j–¥Ùóì©þpDÎé”ã·»yûDŽÜæG^§ÑŒE-_{ ®ÛìØZ¾D-J&ìÎt’IL&>¢ñ&åGµƒ^þ• '¢÷—·¹˜=¡uïoxólÃl19>ì$÷‹Ùa2Á¶MÊ_ü=pú@t½ý–s6¿Oά¿à ðßGñÕ{IúéßñY ™"_ö^zùËÏÞêþÅovZ¿ÙiýÖ_~öh6ñ‰ˆÜ_õ<&)™›bñþè³—¿ñ­Õë\ßýUïÁã—~ÿ·:ãrŸsÏ3[ÖúJi[™ .3Ɇ}"bL>˜L,õ²e”í¨¬!·¹«š¤G“¤äðr“ó¢­ sIÞ®ÛìØ‰%sV¾ÝS¶O`¤{ÒuÄòŽ—oQ:ÐÄÉ<oØ“+nÐé¥M´k=7ªή}ˆˆZ[Ì—JùT }hy»­Û>L®®ÑÇ•¿Øn‘ûY¾wtUù NwÞmÜ+‡­C“”ýýÜÈOã¹¼ãp0vRùí û­Î(·Oñzlùq¿|æ¬ûûg“ý],çŒÏ—ˆþ:«ã .ÆMb.}ñ«Gô‰~ñ×DDÏ>ûò%é+_Ñ[ßøÖ£ÇÝé”ë/þöäËï¿ùëÿóóÁÀ½ßþÎ|³ïïK¿øÛ“¯ä~Ÿ¾øŒˆˆÞþnéà6ÿ ÷óÑãŸMž6¾þ{Õ¢ï–r¿{à}ü‹Ÿ~qîm1ûÀͲ4žÀ3ÿ%ÇTï.¿=/jm~«ÀÏrÝæžžo{ËS7S“k«3YCaÏHñV0ÚI½°Û /Çu›‡fÙ/$¸éÖžš>2¼{´abRTý7kŸÐh¤~WO¶'Ò™}Q¿~ßÌ^ªÅ õ_lÿ˜v8“ö™‘ʆF¦Ú_«Ý¢Ž·ér×íñ6°"ÇTu2Ê\Eµ[èþF•ˆC4QL-Í—‹ªÿfý¸úq+Ê踹éï³:]·7%ÑÖ÷ˆ¨3Ò4Ñîô\—‚QhY IDAT(®üx³¿Üuÿµö—ä×:68Nß_p°W~ôîðÇï !Î<2IVyäÑYnwîiŒL>hk¤_t5ày%T†2ZúèÛóì{þZàbZýp={öìéÓ§k}å&Zíù‘IÎä©dÓ)òæfœ:XU+¼IãÒeù Ìñ½&Ð_ÏDÏ·¹«¦Æô,KÞȰê¡uÞ=y­ê˜ã{õ¡¿ž'ÈzÏYO7Ðj°1Dpå# ¡Ò°¸ª|Ve2¹|+Åå¾§–wýBוFUf¡_Ár,Çò ^ò;–Íœ¾ømqEà¯Æú}½Ú–$Iªé Å_D¶.I’´Uóç_ÊU«*³þpÄÇ7þ`˱Ë/~y×忹%ÝÆo1ˆ½õÊÞý_ŸÉãÂ^Óu½·Ñ/]ZPiÉãÍß6Wu¹Ú.Ï£žp.W-þ²ã¹×ùa9–cùÅ/'W­òxõàZ;ƒ™Ùü½¹N"WÌÍÞõ¶¬ªÌ#ÎÝǘäñOäj ÍeITV¾_füò6ÿÃ8¶.Þ`‰©›¿/c­˜ge0v°˱üŠ,8°yý“®“ȳü~E®«J’$I’j¦´£±þpD©ô\NühØç?ˆZy¬$IRMR¶KñáDhùì$÷‹Ùa21êô×¯ï ¼|Æ„}…LUªõ&‘@X5Ó)¢ñæ­%j–¥Ùº„¿”™ŸGÁ˜|`( û¸Ös©ÒuÄòN–T:ÐDhÐ`ì¤òÛ9ö[Qn;Ÿ kˆ,?“Lx3“üØõ™\ ÞÃX cr^´õ‚7O#xτϗ«|·÷_%Qü{¢ÖnkDDdë©éîÓ]=Ù6,…ˆ¼Ô£žëõ†£„"ÚúuFš&ÚžëF½YÎ{"­7þ×,K#[—v›Ñå7÷ô|[k[9¦n¦ÊD“çPå ÃRˆSÕÉ(Ç•µÏ®Û<4ËÆdù>ϯïÏ4€h약;üñ;/Ö> >O„ÏØfL>hk¤ð¸§ó€y׿Q¼PÎà}/„Lr&_*›NÑfi[/º›/à>»Í]5Ý0Œ¶â-pÌÉý 8|¼câç×H0¡Ô£= úOÆ ˜õ¡õ  ¢€Ë‰(0S6(Q" @Dˆ(Q" ¸ŠchØ4¢@Dˆ( ¢@DW,¢«V£"¼P-ŘPiXV£"ž·+TW•ÏwëB¥1»é³Û©FE`kõ/›îö9ŒW‚±¨úð©ÊlõþÚ¼ñ/vÏÊ…Ÿ«ŸAQýµÖywÞík¹I®ëÿank"ÿÙ1Õ­Z/æk­C³¼_êýÞäëüoÿΑ¡$ü¶^ØkV¸ú„J#¸KÛ¡WÛ’j$TFò\‡bò¡ŒôBoYc2&ø½HŽ©Þ­õÜÅÞñ÷Ëu{õãQÛ8ÎöThÿÎUf«yq=SŸó_Âþò ª<^rø-u1Ç'¼ànø#é¶–2Õ‚$I’$ݧíÅëÖ3ú']R¶K!ŸØºWˆn‹Zû@¾v×m]šŽç\·WÛ’¤­ÚÅjKZÊTw›Kà açHK™*¯¼j’²¿Ã¯Ý2¡rÔ6’Ç…ýjîªfJ;(­Ø¿”M§Èî´.¶ bêa­þŠ»Î})û Wༀ5" Æäm%aëÓëÙ½ÚnðÚöbÖëöêǶXÞ‰I6hš¥ÒY/biT?ÃOaÁ„«ê‡³Ë½ÏbÊ!"¹:_“«VµÂ×­ÊüóÆ’H)ÂÙ#Óúšnn¿VÉaB¥,ÚÇõ~pI#°ïrÕßåL2A£aß„;‰d†Ç$Û EÇ$ýú±-–ƒ-³JÿÎ4Îl³LÆç3Y.þ¨}²Üo¡™Nñ—Nïµ,©O6p8°˜þŠÙ®ÛÜÕGбZ|ÞûË+¯‰”PŒ¹½Xí|©ž&Œ_ddLðC×í.µG”CÔ«mù£‡VÇ®|Ø¢ÁØ¡Év¼›&±CžS^ãä#xóÐ ÑZ‡¦#æ§—ƒ?//J(æÎx0ß)½Ú}“”ýƒƒ}…Ìû›K[}¤ív[#½ÈnJ(ÉÎd6yûc'‘+ÎÞ†šï_>¼+¨¦C¶^(LÆl. ÆÎdÎ/Õ§SËwÍÖ ¼Ëý>Ón Çaøñæ—C­Ž½B÷ú÷â²~ëñ~¿Üý .Ï—Èågw܆oKŽó¥íà·ÿší¹¤ÝÖ9¿àLÜ<Í—û']ÇÈˬ¼‚.jmKóÆÒVsºöhÈc×mîJM"brrv°<;‰d†Ü^o8"%_¢fÓRw1åŸ'03¥z¼ù^Ù:¿$¼¹l:E Ñh+È&%êQ¯¶¥RÃh·µu6zý»_¿oæ …L5%W-M´õB¡E¥ƒvÛÊû›ŒÀZ[+óúLKŽ(·Rÿ†ÃQ"Ÿ¡l:9Q1K'D‹AЊí6êô×;ÞÎØ`ì(–w„V=2qÿ¢öw^&ü|!ŠXÞ;³ã6üx‹]ß#k¶gt»mv~ÀYD­Ž­iù5×üûëöjǶUÞÉ63}§×ŒW½ÍŒ|fGL¢fñÐÄÖ¥Ø'Þ0&ìì+dª|5&WÛåKmÔþpDN7𜥹ATó2X¬*­2èI… Óx‚‡Nšq0ô²bf2Rš{júÈ(W„V½ODK†˜|6*¬£‹(&LJä~1;L&Ö(ûÄìÂbo^Ÿõ •†¡©B;îâ÷wåó%ö<:£ãvñx‹_ýøjÃöœk· Î/8¥ä¥ÑÏ¿Ÿ ­ªþÏñσçÉí> ¾ÒuÄòŽ7°*h¢Ó=éó¬?ÏDZaL0‘̘|˜©y)ø´„ø'ñl®ŠZHúâ"W e¤ïµZ{º-j^vV8¢@¾Ë¤U¼þLkÎî”Å`6Zì tµþŒT~;GÃ~«3ÊmçSÎx@亽áˆÄ|‰G;GýRÚV¼‰"ãÉÕà=§UêS:ÐDûxãÑ­\µ e¤V_Äþ.æ¡E/‘Ëc1Æç6OçXÇ·¡ÇÛ*ÇùjÂÛs•ßQÇɊ眉›Ä5w¥A¥aLSÝZqdÖ:4ËÆv©¾Á¥@×íÕïêɶaù[å$¨yh–½Ô.6Áu›{z¾íe[9¦n¦ÎôÅì{64ËÒx}¢–QkOMÞ=–Iýçß×á˜êÝå9ô‹©>üÍ!¶^ðîEèù¶Ö®Ra·ÉïKLÚ“S½Ë¿Åëãuï´§ƒË¼ètû›õ¯ëö†£„"ÚúuFš&ÚÞíŽÖ¡Y6ø½&[WÍòþô[3Ùq»Mïx¸oæxýSÕÉ(/¯Ïb91ýÙÅrUK™j¡¾Ê#J/fy–‘×e¶.í6£Ï—ðå´Cøqy¼¹¡ëoð{ ´=£êÕn›_p£åW~ôîðÇ¹Ú.ç©§*P¨9?›‚¿µVO¦zîœÉËÎb•†‘<Ž ž}ÿ2açÈHoØ¡g^Ÿs?ÇN·¿éÙ³gOŸ>]ë+7O¿ÕÀé32÷ì©l:E3s³_4­==ß6ªÃs’z -…ÖÅõïU;Þàn^Á:¹Í]5=MÁâé g3ûózrÝæžš>Ú}ÑfávÊ)?9 `í!å鳞àù°AÖÓ ´l \Ɉ‚ÉÕà[,âßkÏ+¡Ò°¸…®* ïMpõx¿´'§/–Ds£€³dë’$I[5Z¹\µª2ëG<®À`à rÝ^mK’$I·ÑçâžõÄÿ8ÕÐä@ÔÜ•¨jµE"j§Ìs|Õ\rD1ûZ[/ì5—™51~9‘3æÿ* ¯”À[u'oþÒIãߘ>%vþõ·D¶Ž„=Wc‡ÄÚົ±4œÈuUI’$IRÍ”v´Ã3£„J#ß)ðåº-úË媥‘ΗwÊù½sÕt·"jå±Z$I5IÙ.ñ…¥Cé’$ ªécª'ž'rÕÚ§ûݶõÂ}ÚGÖÀóQP¶˜KØÇõ>ÿW¿~l'rŬ!øCüVÇžD r^tÌÃÖZ5°uï¾D8¢TZ`Œ1!"»Ó""×ítD2ƒ®zž4w¥­Z/›Nñc YO×WlÖS&™pƃé¿c>¶ï*3II<»)›NѨÓ?m\·7‘’/Q³É˜PÌ%œî]õüéÕ¶vÑ ÏsD1 f" Æ„}…LUªõˆˆÉÕv9äÛÙtŠh¼yÕDͲ4""[—p àJŠÍzêŸt±¼“åÿ*h¢Ó=éóЂFÃ>1&ø±ë3¹:3±zŒÉyÑÖ Þ<éLn¹jYU™1¹Ê_nàý½pY÷(ŠÑV¼ŸS½[ï¹½ú]=Ù6,%¸Ð%jîéù¶Ö¶4"rLÝL•‰ˆ\·W¿oæ ÃRˆSÕÉ(ó!øà&Ͳ´àŸ¹nóÐ,í¶æ/Z\Ÿ4p©Ø+?zwøãw®V„Ê‘‘ëNŸ$+´5ÂÓc¯'¡Ò0’Ç1$\qrÕ*ñÞ€ųgÏž>}ºÖWn\ÅýÈ$gò¥²éÍÌ€+âæ¬“ÛÜUÓ c6‹ß¯€k‰O²ŸfÍÀ5LXuÆhˆþ“q³žàRÿW¿~l'rÅ,9æa‹c‡&Ÿ§Òü²uï^ÄÌúë`L΋Žyè…8­CÓó²™ø·,úÃÑ)öÜÖ½ë÷ýáˆRid_PM‡l½Pð†ö®×Qõ‰hŸ^mkw2=£Õ±7¬zl}÷ G9œŸØwfg’ g<˜þ{0Ü‹8GÙtŠâôÙD䤳D<¿i4äP®ËgIœ¿¨vˆ¨‹n"&T¦ÉJ´é{h£êÓÇñ —Q´:¶¦åKÔœŸÏŽœ§#ÙƒŠl:E£ÎÃÜþpDN÷ê̈j‡51&ìì+dªR­GÞ|’ˬÀéÝ "×mvlµ#N‚P©VFý“®#–w¼|šÒ&:Ý“•#„Ò¶’°;M×uÝÞpDb¾Dü"ýÌó\i0v¹b09Çu›;q…朮2Ɇ}âϨ]Öç_€Óò²žš»Ò Ò0&‰FŽ©nõ\¢^ý®žlO@4™0?æµ¶¥ÑÌã‰Z‡fÙÐ,K#²uÕ,ï†Çõûfn²åÉWZ{júȰ¼‚VzD¨éƒ•øc”6*ÇuÃÛaýrš{z¾íµcêfª×³tÒ,KãË£êƒpñØ+?zwøãwΦ¬Ù§ÄÀõòìÙ³§OŸ®õ•h5Ø" ØÜÍ3,Ëu{µ- m ðâÀ= @Dˆ( ¢@Dˆ(<_¼7Ü1¡rd( o¡­öš®‹åÏÍr€sÂ^ùÑñðÇ÷ÐðìÙ³§OŸ®õd=ÀænchØ4¢Ø<¢ÀÄ]Ø<¢@Dˆ( ¢à* ËjT„+öœY&W¯`­^¸ˆÂ‹|g=L—«–U•ã—\qQÑËlÃYŠ@D^sNvM¾¼ÐÔU™±øö ]àjEœ­$I*T“ã ä/wÝ^mK’¶j=wÃͶ:6‰y4̘œÉî´ž“&´uib«Ö›,uœT™GÓ°„ ;G†2òÖWÍ”v´#0Õ>Që㨀«QøñÃI×ñÿ9½B>{=êÚùÜ펪̦!…¤dÓ©@@!WçïðBª²ÿI#84÷—¶5qi}„J£QüÏø=„™¯œWÞÔèøx”+fg–•¶•„­ïy{Þ¯ß7IÙ.E·OÔúW6¢`L(æþx¿WÛ’$I5¹uvŽŒ\W]¼vžÝÙ÷¯©K’´Ût‰Èu›Á!s¶˜Kئëòð@#o}}4soDÔÊcµ I’G×/èöÒúQB1Œ\W-ð»/ûs÷ ÎQ«3R¶ƒyJQë㨀«QˆZÛ²¬vÛPF:"e‹¹„}\ïóõëÇv"p9>À]{‰=Áˆ…19/:桽´M'ð][¿[ë¹DÔŽ(•›[Õú8¦z·Þs]êG/>›Ëo¸ù{2äïPª¼3 2ÉDtôÒ>1ë\7ýŸl½À ¡Ò°io*“L8ãÁô߃±“HfˆzÔ«m©Ô0Úmˆl]ÚmN‡Ìš–/QkPÌ%ìcdÓ)JˆF[™夳Dý¨H&¢Q§¿F}ˆˆFC¾#®ÛÜ•šgß„ÁݜկŽŠÙúЯ%£CŠ…ö‰[àj¸A I@ý“.GšŒØCGô®Û«ßÕ“mÃRbÖçË[3!…¦‰~Ê_¶§¦ ËÛp\Rê÷ÍœaX ‘cª:åÉò°úœqS%”i~Vp¢æ×ß1ÕÀd'ÑNÊPȹns¯@Qõ\hŸ%ë\ì•?~w¨¿ƒ†€gÏž=}út­¯Ü \ò€MÝ@" @Dˆ(…çbÞÛpz×¥ž× *+ФlRôÀUŒ(¼±¸ï¬GrÕò_žµäºŽ¶­éŽ0&W­é¨—ùs¶¡­FEˆY>×-~ßÌl}¡³äê|?òj×*UÚŸ1ùÀPFÇ3/Áݯ¸ö±B­i%­ùº…rÝ^ýx¤2ÂE€+fú†;[/ì6½·ÔCÿMs®Û«mIµSl£Õ±5-/³VÓuù85/’­ŸñËŸO_Ï8Nª\ZµÞÌSx™\mk)Ó´IYøFÔ[ü–û;$W­òxþÝyÙbŽLÝÌ•‹Ùz¯7 '´”©ê}Êúý8;‰d†¨·î¾•´”©n5Ý•ök±Gf;D®Z ypÒÜ•š~ÐÒ6ªÃÂnsÉcŒÝ殚n¥fô;àâÝX—ŸtÿŸÓ«Ì³—´gokL/WÏÝî¨ÊÌ )HÌ—ü±p:Ev§5h.Þá9.~Yþeìðò׬'_X•ý-ϤӬ™=5:>åŠÙ¹î”I/Ü­Ï«ÛйÄhØ:é’¿uÆä¼è˜÷ë=×åïwÄrT¦3,Ù„P)‹öq½&ûåÕí°µp¼5;6¥ÒÙU é×íè=€+Qð¡ª?ÞïÕ¶$IRMg~Ä|d亪$I’$©fJ;Úáãïìξ2Ò¥ ~á™ý"[Ì%ì¿_!W-¼õõ‘bøa%ÃÈuÕB¡ š¤ìóqdhùÔ“ˆD­A–I&S-øü‚j:‰\1Kn¯vl“Xö抔¶•ÄtêBª\¦N‹Ïú(çVkÞD2³VäPµBž*›Ý)‡MÉž¶öNYôïG­´ëçÀ¦¦YO¢Í{2ßP¼I¼šei<3Çu{õ»z²mXJÌú|yk&¤Ð4ÑOyâËöÔô‘aùÛc3¢Ê_¿žçÐý“nÊPÈŸq.zŸhí¶F¶^ØóîÌˆš¿¿Žx&lÔò¥¼èt§CðþI×QrÅl­×ãíéå‘ùÙŽH©; â7nÄ„³Ê9Æô™¿´t¿B»L¨ì/Ü ˜ëGÇT¥ÚJwBøÄ›ùô)¸Tì•?~w¨¿ƒ†€E¡ïÁ¸DB¥a$%¼àÜ<{öìéÓ§k}å›áZ{úH1–>xê‚b_¹j(#}¯…~¸RcÐÕ IDATn  Šë6÷T3UÞ¹ô§µ2&ì”S¦º×Äœl€+†½ò£w‡?FÖl˜õ°!DpžÅfïg8+r5äçG¨4,®*ãàX!¢`“˜ÁwÖÁ÷Y£"FíŠpîAÂûÕ«mI’¤šÎÕï¹ÉÞ]\¸QLØzA’¤BA5I1¦ï±vÝ^mK’¶j½³“M§‡rE1Æ„bŽœ•GìÍ]I’vOó„ÇTC÷ëZcrµÝÞ§®#.×͹»nï¤ë(IïŸB¥á½ÜxömÖ³¯=ž¾8yîuȶ^Ømò` ÛåŠÙzŸŠÉîq7Wö‹ ¼†Ù+gþÝØ‘åO— •Æ>Ý¿Oûü³ÅwN{û•K ŒñÐhºÝÉ»´ã[*jýÅúǶCd9ÞWhyevʤî¶J a¸RcB1—°½÷ˆõj[R¿«˜æÂ‰\W•j=/ê8ÚÜ­÷\7»³¯Œti+ä­Æ'Ñ~1{BÉñÉ åÉ Q„J#ß)H»“!¾Wß,*G“mÇl—ˆŠa8¦Z¨÷³;GÆ~åän½¬³¼­$ódN.IMþ³q0ŒsÔúõl‡u·Êu{µ­]» à ˜f=‰ZÛ²¬vÛPFºA=\¶˜KØÇ“{¿~l'rŬ_P~>³?“LQ«3Êmo'Ç'~¯¶åo«ÕY–ÿ]ÿ’8 |)¡–eµÛé~ã‚19/:æ¡5µMg±Î3QVäúqõ_(3~»g]pá¦÷(ü̡Ұé¸Ü›L2áŒÓÆŽwÏ¡WÛR©a´ÛÍ&J†=·ÙiåñaŸ¨è²…ÊL~“3Ž«lôv'›p‰Èu›»R“ˆøXg@ÉUKË—¨Ù$"ʦS”v eÈIg'å„D2‘ëGÕ?¼ÖÝ.À5Š(|ý“®£Lê!fGòs#}ž±Ä3”¬*³z¼‘>ÑpDIžÅ´¯ée11¹Ú.ÇV6v»ñZ‡fÙЪrk·Éob8]õn­·ÚÝ€ˆõãëÒënàÊ yE¶˜[2PïŸt±¼ãå•4Ñés™h2Ú+0Z,#•Îòt¨Ñ°ODŒÉ“ ΧÙn·W;¶I,n³c'”íUŸû½þòúÛ!~»—ûÞ€ÍLïQˆZÛÒˆh:!aöEšei<Çu{õ»z²mXJÌú|y+~¤¾§çÛÞvS7Såø‘}øvWÜÕÖ¡Y6”íR}·I­=5}dXÞ{‰IQûK¾~TýcÚ!´œ º-ð€)ÒÚm-ð˜)€‹Ä^ùãw‡ú;W·~rµ]¯9Àfž={öôéÓµ¾rãJ‡;LØ)‹þ”k¸jn^Å@"ø¥Mó‚à(/¸€+íšQÀ•Œ(Îû= Wê= B¥aqUÀR7)üý gù´VÆäƒ¶–2Õ­ZÚ …ÌóuôûÅgp•†‘¼º}63s}vׂï©p& p¾gë…ݦ÷–:ã`è?aÉ=ýDél:å8”+ õ~Ÿ²Å9ÎôC÷œ'bóõâ~]_s &W-¼ì6w¥æ4Š3ªÃÂnÞ€s4Ÿõ亽“ît¼•ä¥*ùŸM–f—[UÙOdêvG¹b–([Lv»±åóBª²\õ>kT¿]¡ÒhTÿ³FE߯TÚÏ­òK_1á*jýéò•Ú!²œÍ²¿“ó¢c¶ö·Ù±)•Ά(й„ÝñF¨½Ú–$IªéÌ­³sd亪$I’$©fJ;Úáãàìξ2Ò¥‰àò“Î(WÌf‹ÉñÉ€(‘ÌD–ωZy¬$IRMR¶KñÛ%¢„b¹®Z(T“”}?ñGÞÛJÂéžðkùrÕÒÈ«§>RŒƒÒÒp"t}¡ÒÈw Þr[\Úënw©ìNY´óǘP)‹öq½C.(¢µ¶eYí¶¡Œô%©2Ùb.1­öëÇv"Wô¯†‹yyî*{&™ ¢Vg”ÛÞNŽOVåÚº7Ñ¢?y÷â·ëO“èGrŠaYV»­‘^àó æ®ë·Mg±Î³ÑHÔú½Ú–ßV­Ž=Í—¿]žý%mÕVŸÁ˜¼­ÐÜ ~¤m(d6ñ®q8góó(ˆg"5Òq“˜3É„3Lÿ=;‰d†¨G½Ú–J £ÝÖhöu×£aÏmvFZy|Ø'*nVÙèíN6á‘ëz³ øXÏ£«––/Q³ID”M§(!meZ”“ÎNÊ ‰ "ןŸ$íŒùÃÛaÝí.“Ý)‹N÷p6Bó§RÈU˪âãpQ…¯Òu”é@=ÄìH~n¤Ï§ ó %«JÁ­?ØŽh“g)Ån7^ëÐ,ZUní6ùM §»úæ"ÖgLØÙWÈT%~ëC®¶ËÓOCÚaÝíÆây\¶øµ:¶VN Œõp§ÎMÈû(²ÅÜ’zÿ¤ëˆå/ߨt ‰Nw.—)8Ã;›N-–±É¤á¶ÅíÕŽmË;c®ÛìØ >7c¥ïF®ŸI&h4ìóñýÿÜÖˆvˆßîº3³³;å°)Ùh§,ú÷mÎÉMšŒ_E­miD40û>Ͳ4žÀ㺽ú]=Ù6,%f}¾¼³í¨ò£GöáÛ]qW[‡fÙP¶KõÝ&µöÔô‘ay;ì%&ÅÔ't}×mîéù¶×nŽ©›©òâ~Í´Ch9t*û 7(æ¶ëLnžœöÊ?û?‡ÿË‹†€gÏž=}út­¯Ü dÅÀ¦n   ¢€kQ¬ö¬R€°ˆb™uß“p½ÈU˲ªòEíšPiX\UÆÁÏOD1éZ–eYŠpö1¹¨³ÿvg›m… «WÛ’$I5kk…ì×d¯/. €«ìæôLj·­¹n¯¶%Õ®òNdÓ)Ç¡\Q¨÷û”-æÈYyÄÞÜ•š§Û¸cª[µ»œq0ÜìuW0œÐR¦Z¨÷)Ü/&WÛZÊ4mRpîÑÒ÷QDeé•F£"øŸòûL®ZÕ _V•ù5îFE`B¥1÷u¹:½¥0½¸ìZþüú3×λÝQ®˜%Ê“Ýãîì¶æÊŸ¿·ØîìG+ÕÇ»Nº¥Ò~•¢®ñÇ âC×mŸ¹]¨Êli9«g¯1&çEǼ_ï¹®ëöê÷MG,WƘ°S&½p·>ĉ~D+&K'¡F®« Õ$eßË2•äqA·IÔÊcU5D2Cýá(8ÔæFÃ>©ç;I’$IÒmQ;ÚñW -_®ZéÞú#Å8(ùžtF¹b6[LŽOD‰d†¢Êçw]¸B`ßø}†\Wå©fji}‚£ðm%átOz®_Ϩp"tý¨öÉîì+#o}I’v›n|9ë)åE¯ˆ •#CIP"™áw«v›x%"„G¢zÍ;’cªwë=×¥þpXxØ¢ÁØ!û¸Þ'"J¥³4;“ñq£"0&¤SӈŠ·:v|ùÞµóÖ·þ¡éˆy™1Ê$DÔêŒrÛÛÉñI&"Š,Q¶˜KLêMÔ¯Û‰\1»¿D Ű,«ÝÖH/lÕzqõŒ³~\ýʌ߮GmÕz«„ÎxÝiXVÛÈuÕk1ï.Çòy‘FC>6u]o2ËD¬ÙŽù eÓÉшŠY:!rÆ¢é%ðé@6®|!¢„h´9üN:ë¯Ü쌴òø°OTôÙ1å/Ê$^½¸ÁØI$3D½ˆú0/ÐØªõ䪥åKÔle#êÙ‹Šd"ת¯¶¥RÃh·µ`Ç­»Ý Å0S-Ôz.cÂNˆŒ(.ì‰=Ùbr|ØIî³ÃdbÔécÂξB¦*ñKûrµ]Ž-¡?‘ÓUïÖz3WÙýHÆŸf=Q’Ö/6‚XŒ0b´ÍÿŸ½7ÖMéö¸×<ÚwaE [z®À‚ÂPâÞ‰”⼟äлH¤ßÉ+xÏqn#¾ç¸‡2vò-à"F¾€­}þбÛ`B²³÷þÿš½cÆ3kÖŒÇkͬñô,c¨ÎïgQ‘œ•ëU*?ß/Ï#µÜ!)÷³Êå–èäÀLò;M!\¾áa9|ΙÙo›°ÞºmÒÚŸ/‚æm«êW5!ØgLry.Q4[x‚v»¿7@Lƒ¨2ÔbÕüÉ]†rï.Žsê 9\fc¨Êd[¦É=¾O#_Ϊõ:EþxGø‘|bÿäÄÙÙºQ÷V¼é{½ð›ò­ä7>ÿÄÜ®kTŽŒJ¬Þu h²g>Ñ"0 Ù[¬¢ˆhö`¶Ãq " mÓ®÷Žd5Ð/–ëñߥòDQµü£h5¾1kŽåò°¡tãÄi̟힥ÝvÇ÷³|9Kô™›¾HþÝ|¸œó3ôsTÏqüT&uè¦~á8yfû{µþdØßÿýÏúÿõçÖ_:½M%πߕ?~|ÿþ½Ò-GΣøÍÝ &ÝõätË5 *ßþDG"û¥sエGñÏ$ÞÍ_PàŧGø =Šcœ~ŽÁ×äKÉ/õ'.g¨¢ó€ßÇ£ØZº®ëºî¤/ýR>ƒ:ÌÈœ:Ÿä«dµvÌiY®EÑíð—ñÄÜ¡º[©¢ëàÏ$³3»à«GÑ×ßÈ,6êaHÍŽ4ö};M ÃÏ“?´õëÑŠŸ:g Ö¿Ç—£˜:tŒºm{¤tü±9¢(JGêO&})ý•¯0uèûüÚPU‡É¼½ÔŸìÝ®·K <]\F2í›ÿ~ú5å2hvD"±S[N—¥ò³X¢4§í‚Æî²ÃIò¤~Ëë2¤z#©@ÎBŠÒçêgoyd¨²£ùœýŘt×#³}3^ŸtüÙE)%Q:‚fYÍ¥Þn·u›´ÇØ(—µÚ´mz$½®Û¡P»"dMmN°ö¹¥ÞZ´EQÅôdãå.M–›¿:t 2ãôf ºi†¯‹ ÙÅNmóúF$Ô®ŽÈ/½ÞVE·I»íÆó‹Õ\ê<Ý®•'cm«·š._ùay%r¹¹é‹ô#Þ=jAœ^Q”ûYTžO%¢h5º¾Ÿ¸šE×<ŠØÄÎó.$´õ›ñ*ŠÈ_™‹ÏszÛ„äMÇ>Q½!ÒÛ&LìãI_bLjÔ·Kj Ï^yþŒ©-9´Ÿçqúg;”[*ctUˆh¾š··µÍ«RÍ=óf´Šˆ2Øi ‰ÜDþxê ÍŽXZ_"A³\×uƒÌöõhU&g%éËôsgy¹<úK¹á€ppAŽï£($XsÛ4Šf÷ÊŒˆØUAJ­+µ  ŽH¯Dáæhïøj" 7eùK: ²ådbøÃ†˜&ž-£·yö‰:ç)ãª&ÄrqÞ6¡P»"ZÈÃbGãz´R‡®ÑêÒlFD$ȹ*(·8}‘~V£k&–ãÙ†«Z.ð(>í‹=b§¶y^Ô;âº& Ÿ“î5²u…Oí«C§Wšƒ¿(\êñÚBJêÉÄ–>Ñ: Úîz‡F óg»gCu~?‹Šä¬\¯Rýðýæ–.f¨2Æ5&Nu¯ä¦à#= "õ0¤fGbŒ1©Ó¤0ü€¢ÄF¼ÅüW¹g* ·* ë½=/.>É.ˆÓëvÝx¹“›/<’[©·À˜Ú’É[̋ң—€ö(ˆˆ–Ë Ù‰ÄNm9]fR¨Ãý9o>>TÓ_v&Ô‡jbéÌÓïq˜ÿööݹ|>ýnÈ$hVzGy¹R2éKé¼}vqc›}FÈ’ôñôÿ 5:‹`:å ¡{« žù;aþøÉ&í¶KÄ]Šî«V”€÷(èu4;¢Ø©m^߈„ÚUla·qœ’éÉÙ9oÙèmô¶¢(ú1»•莥 $Ž“uŠògêÐ1ê¶¾"µ]+ŠbzÚz<É?;ZCA³¬æRo·ÛºMÚ#w~Ô¡kP<—oš5è–§ÿ æ‹@»ÍúTÜS˜Eÿ3ŠVë€ê 1ŠfY—Bì4o1‹¢¢ôèåàƒ=Š«šÀ-Úæímmóêo^®ïgQbñzÙ;=óf´ŠˆÈ_To”ÌÜsO ­Û!yf»»QT’·%‡öÏÿ½„¶~3^Eù뀈â0¡Ð~ŽçòçÏv˜ ":LŸè£kE¹­¢÷I%ÙE™¬Kñl×{w[€7K÷>1&uš&+IÀ‘|ë)X¯¢Ù"0z›gŸ¨“üœý:Q¸¹lñ¹ù3&5ê,üË”¬¹E³{eFDLjÔI-G˸ ‘h•ŸþÂxfÑÒŠ?ž/q¼æ¾mBª»†ÑêÒü­Ó¼éüXz>Ö£ "¢Ô‚^Tã;ƒ5²ue´"‰Ô»¨;Qš}kã_P¸Ôo.³r9¢h¶ÜÛ®¹³ÙãßÍ­(šq—Bl4oÊ|JÒðAüE<^ÿ€zCäq4ÁÚçæéÀÄ«uÇ÷3©ÿr,}QþQ´z]†Â”·M(4;Ù «ªåFÑlá U÷,ðÎì˜ù³]oµ’?ž =&qPÝ‘‰ÕâO½Ç$äéhz>‚oå–÷ƒÙr Ç5ˆ(´M»~lbþl÷,Ãu "ÏÔíÞ#—ÿjt­ÓÄrƒˆvÃóÇOvÓŠC–’¢JåÑüAo¼X®kÄG" mãªÒ½Ä÷QtP¯ÿuY·4 7\?m8–«dBqà“œ„<ÑÑô|ìïÿþgý¿ÿ‚"?~üøþý{¥[þ‚Ögâ\rÏmú—3T?­n\’¡ÊN—³uèNúÒ…Åã™~äé|4ßR‹ßÒ„ôjÎ9 ïc5ºVF$õ'Ví<©ÿ’•>®ÁÍøƒO‹fLºÛ)Ø3Û³ËÿP’I{©C7>âçQ€Oó(¸½z‘SÞ¢ˆ»ŸGZ :t{›­m]>û)9=³}?‹bSÞÐEŠ’üs½>uèu[o}ï^,k°¾üÉ}d8õ¤]7XbŒx¸Î°Ï㘆*ÿ}Ò—UŒnbR’ܘóg•¨¤RÄD¨mæ¹r&QRi·é÷Òäþ4¶Cª7ÄB½í]w⥄SÙË?GŸLmÉ¡ý4^EQ­ÆOv(÷åàó< ©?i-ÚŠ¢(Šbz²ñrÛ÷²V›¶Md£·Ñu;jWD<ºIQt;<©pÿu ÍNl#‹¦àMG« GéȆU›¶EÑmÒn»üb‰œ²ÑÛèûéSwâîÅÒSQ®Ëå,Ò›:t 2ùõ¶é]¸1»-™‚µ»g/–&PÜ.|¼G!ÙIõÔòæ!7D4_d,àÐ~žÓÛ&$oÊ·+”MžE«ñÔš‰1ƤNSðó쯣knº¿ËÇH£†üu@õÆÑϼáÞÂAú«Ø(ˆ#êÞjB¸|õ õ¯!<ÏÏô2ù‘ Y™¥”DÈpó&ÞM\×±šKýTÏ€³9²bÓs¸¹pùó…gô:âØ§N“ì§ù×Õ” …‡"ʆ㉯gezu ÷‹çæŸÝ¢]ë…âè‚fY¡­·G«ˆ1é|žGqcÒÝ£F¶®ŒVDÄÔ¡Ó»pñQ4ã.Å+5iùô•?LÚúÍúÖ±^H¿É†<¥k Uõ&6êDÇ=´Ãüsœ2£vEä¿mB’ƒd…ÄNS—oèäà)ßGqUâÀ|ÆÔAÅÄ'2¶I{|lÓݯ½^ngöåüŸÙ½n“f½Ûî\ 7ÿuʽ;‘»û¼= Ƥ»žLÞbEÑj4õHîÅ{6º·ÚGìK KÙEÍÌ–GÝ„¶i×{ǬÛm¬áº¤*ºž±³5-˜Î>kâˆ<¥¬F×:M,Ë©™í‡y5½ñï/5-ËÕˆB[7Éê½_~ “Å"š?èËr4¢‹}€2ëôïÿþgý¿ÿú™0éîŪMÄö>š?~|ÿþ½Ò-ýt¡Å»GÎþàgòí'–-õ'–&yf{ü•÷d¾¢G‘ý*àWä/¨ð{yL~©Ó$âQÄgÉmª_À àþÈŽ‹ÂŪñŸ‰Ëò…dL&ZÛ“vç‡DŸ{ŠNoÙm‚­þ÷š&[Dö§|E%å–ë-WÎØ!,¨)ÛÙžŸ¡]ÃPûaö¿¿†õ^_š‘£ »ƒÌ¶2‹˜Ô±û¯7\0uèu[o}ï^,k°Þ6wpð\|b]`*׳¬þç$Þ½XÍåö»¬ÏðbiÙVñ`Lº{ÔÏêÇõV$§ÔŸXÙz›§—úþô€ã·À!9QOóg;¤zC$nYNúR:~tN2sÞŽ!gÕdÞ}7êp.œgmÈ$hVZB’}0ÍŽx(v&›X¦ÝaŸË9Tùï'ÌÍóª¯5»Wâƒùü×e(pÁS[rh?WQÄÏÉå^ÙZJ÷V<39‚{þ`z$·ºDtU(Xû¹w­çž Ÿï±Ø»œ£·"9So5Á3oRb5º‡;Nõ(ö4Ëj.õv»­Û¤=ö%ÆçÔ›K]QEQt»n¼Üqû[º™üzÛôŽfÎÔ¡cÔm½Ío¹­ˆUVQLB;.bgR¾´Û½¸,©?i-âLLONå!Y«MÛ¦G²ÑÛèº µ+Ú“Ó 4kÐ}·&3Ö·%'ÿå‹ Ååæ"6ênÞ’?£h¶ðHnuãÿNêŒe=%Ýf]¢ø'©ÿ¨‘ý4ÎñBõV$§Ø¨“·À™ƒà<¢{« áò5µIC[¿¯¢ˆüu›À¦àM«ÕO=¡Ù“9ï*Ç_w[rh?Uœÿž?ÛõÞÝÎtûjtNØÏO&´Ÿçô¶ )‘·Þ÷äœ?Û¡ÜJMí(Z®åzTéнîÀØ©z¸yï&®ëXÍ¥®Ûa&©ldSâ‹Á:·¸Ù½Ò6=ÙpvjˆHÐj‹Äs 4kÀåïÞjT¨Ï½åËyUðd€Óø–1t× ""/èß3v£hv¯Ìˆˆ©5!3§Nô¶‰çþu þ©e3&U»!ÁOƒ—Ž8^o³Š§Ø·†rÙýb£N‚l9ZƲnˆD«3•(õ'†ÚzæäoA³¬ïC`LºË¦>ØGÁ‰±¬S‘*˜k1uàßpÏ(ã¾ÌžÑkˆÄ®†FÝÖüÓõV&'•<Štgöq"±À¯ö<Œ­ÝN›£™Õ«ÛòQ4[îm×L=“»Gìx3S‡N¯Ü#Y.õ›‹ì H61gr{Û„$é>±ÓÂå[©8ÚVŸŒ©-yßÑŠ¢ÙƒÞx±šqìûD´ÓœnKÞu”dËÑ<³ýð–¯·B9ý…g­.Ífx@ÀÎ:Â]†r?Ó2’Ê\gê0]3ˆ¢Õ:ˆ÷3©ÿ’ìØŽ¢Õë26ElÝ–f§h{ôüÙ®·Z´uiâ Œ©ƒ$ÿ2‡Ä´Ûü½•vf3u¸ïNE«ÑÔ#¹ïåèÞj‚7-ó^æ d#Ù˱@•zh&F‹÷W$ù‹w=®ÿÙ½’ÒæÛPÚû^bVoEr&û7^ÒíRøs¿Ò ¾,ßθ'ŠVã³æX.Ÿ O7ZÐjüd7-ËÕˆB[7Éêm Ùže¸®A䙺Ý{Œ¯¯F×:M,Ç1’œ®“/¥úã~)ä¿.ë–Fá†Ïß›-'ŽÚ mÓ®÷ŽTaþ 7^`B˜ IDAT,7ó*ˆD:êN$Þ‹¶]ðÌöÃ,ŠxþùÂËFZ.¯oÍÚ4p’ë‰>B¹R=ÓNþ™ë'yƒ‰ÞöóÉÈ9»WÞú“ä mýßz¹VñßÿýÏúÿE|¤þĪMÏðm¸?~üøþý{¥[þ‚Ö¾þøÉ®¹§dð5ù|ø‡kGPøuÀE S‡Ÿ¶½˜…æe’Iý‰;TYɯ'öØ/Õß¾x½>sÀOá·9îv>ˆUø¹’©C··ÙO‘šGYQ³çW|¬Ñ¦ÞæðØ©?Ù;1âz´ŠÏ‘HŽw`L8½~3ZE»òÇzV‡®A;gV¤WrÓ[–êÀ1ê‰NÒüǴۼ±¤eG†ó¬ä<%«CwûC¢\=´×QùwûáιµiroVŸùùìã‘æåÿ´ÎœßO Úë«Õë£Ç‡’ü‹úUQ?ÀOó(Ò×8?_Yº†3 /àTäZÍ¥®|Šq*ù§ã…a½×—æYë0¶œS¹žÅÖÒËÝÛÍx¾ð £¥²yj1·dòÌyQúBãIlÔÚiìû$vš†DDé¤r´\ºƒÌ¶2ãÖücÿ56sÕ¡kÔm½=öI¼{±¬Áz[÷=ìÉb¿âýß»“ÿÉÕ·|–üÃ?´uØO ÚëkÖë£Ç‡¢üsg Êú!>œ¨§ù³R½!·b'}IêOÒ3Rc1¹æºîPÍ5ê0¾šÌqïGeìEA°ô†$žµ!“ Yi DDtU(Xûå^G’œæP^n•hêÓiÐ숻vú­&xæÃœÿåŸlÒn»Dó…Gr«›14É[̋ӗ±\òbÅNm9]ž+ýì^áæù¯ËPàaLmÉ¡ý4^EQ­ÆOv(÷úR±övåŸ?˜;Õ¤#vðë2ü˜>/fºs,ü¶7Ç=-ÛoÓ:9¬l¶¿%ÌeÚë3땲ÏûÑqàDöòÏ{Æ+öC|¾G±‡ YVs©·ÛmÝ&í‘[êñZ¢(Š¢ÛuãåŽÛåq¸Ž¢(ŠÒ6½£™3uèu[oó[ødäjt­(ŠéQhÇEð È(š-<’ §$d<žW®KBz>‰ù"Ðn³6÷Ò™Ý(Z­ª7ĸ^‰­-vš‚·˜EQQúòb_A³#ŠÚæõH¨]½³/®Û’“ÿ&8eù‹:…›·­“0[œìR0&uš‚·˜_¼YdêMÛŠ¢èÿŒw9ÝóÒ÷6ú~ú ö·Kµ×O©—ÔŸ´ñÃkzòÙã@5*öCü¢{« áò5] HÔýu±y§Iø¶?žzB³#&s‡Ï¬ÀnKí§Ó­±Ù½Ò6=Ùpö¦]¹í›A§Å?ðhåzôÞlÙÈNÒf]Šg»Þ»wlóBïÃ#¹¥2–µ¤KÒÿDóEм½­m^/^ß;MnÞÄ»‰ë:Vs©ïZª¹zÖUÕË×q,-0ãu’‹’F×øë€ê£+TžG|¤¯ÖßrôsÑöú´zeLJÕè:m£ùÂK’Êã@Éø³]¨Ì.'–ôC|:ßv,9#¶5xàûžQE³{eFDL­ ™¹g¢·MÈçu 'FŒIÕnàžÀì^™Å[‡ÝI#Ý”)h†F¡ý4ÿlz…¶—?ž/q¼NÕDµb—Â0Z]š¿uš‚7K_D°^E³E`ô6Ï>Qç}5“úCm=³sCÐ,+´õv¼Ëü¨ê ‰±¬S±ÓqJ-c©?áíëÉ'§rËÕÏEÛëCë•;>ìo7D|qªêcŸv ˆ:t­ŠûCI?À§óW֒ˆ•‘z1W{GlÔO‘àhO¾_Ít;‰ð'" m½mšõòu"ª£h¶¶%{“Á|6ÞH#‚Ò§òôåÌîùÒË*YT:ÓØÿôÐÛ&ÌÌm“Øi e‚¿²Ñ(§Ëßþº ¿p0ËûÛEÚë£ëu8>0&Ý=j”„&E78æ /îNû!>Ï£¨€ÿº å4ž§;0d¥¹ÎÔa:wÉM%@ϤþK²S“ï¾v7ì¸-ÍNIð†ØiîDgE³{ݦÔ**7µ‡>rgvb=ÛõVkk ÚãŽÞ¶±!ó…Grï1»y 4ý‰¦ÛyS‡‡_2V£©Gr/Ž•ïÞj‚7-‹X›/<’A÷DùóÚwó»&Ûþ&ÞõäÝvÿiãn«ÊÛë'Õk»Ç†1u>_ãÀ{`LºëÉ|[Qå~€æU·…¢h5¾1kŽåjD”ýüjüd7-ËÕˆB[7Éêm ëže¸®A䙺Ý{Œ¯¯F×:M,Ç1’œÒ/Eú"`þ 7^ve{Ûù“ë§É¿~Ù¦ÿX=잇`¸®QɶÇ^ûRÏö¥êõ0/ffˉÛ+´M»ÞKƇ‚qàòSho?]ÖðùVÇßOÿ¬ÿç_Pø8¤þĪMaö|}~üøñýû÷J·ü­Æ?Ùu#ç‹=à×çTðkÁ?r•sfXÅØKås Ùc›SŽókÉÿ»¶ Àe !D=8ˆz|*ð(_Æ£ú“xîPýâ5?û< ¦±½€bë $ßã9/»ÕèZQÝ¿ŽÏ0T}Öyvüid¾õô[LF¼{Ôhçd´ìˆïö(àgÖÖ¦&üs–¡­ßŒV±EÎÔaú•Ëœ³¢ó²JŽ¿`>¸NžÙ¾ŸEI¹ñ!ÄL:½ w¤þ䑞žè‘ß“-Wº±8g*3©ÿ¨f{{QêOâR3~Ty}·ùQ¸Ù¯zs&Ÿ­øE9¾B6z½­(Šn“vÛ͸u[o+Š¢(Ê)îDs©óĺ]7^îxô‘x÷¨¦’À]ˆrͲšK½Ýnë6i}‰Åf:Åù˜f º™;x!;¶{ItVn}³ù·MïluKýIk+ÍôäTüú…l¤û(øÞŽgÆóôþ: zƒ[ÀÝ–ÚOéüýÄNSð¦cŸÿå§žÐìˆi¹-µ’U.Aøë öXÔ–ÚÏsþûüÙ3yʆÕ\ê§ø*EõÝË=¥\NY X®SIæ ýüꜳ‚1©Q§`áŸZÈUM7oÛ¿ß6¡P»"ZÑjt­ÓÄrãt‚5·Ý£hv¯ÌˆˆI: ²åh·£!ù¼Z&ÖpÝ>Ý©8ðˆªU·LuR?äµ=À/îQT¤Þ‰V'%Ýz9ßÍ#£Ü!³;Ü_.3{χÿ;0[ŽñÒ»9uQå¨AçxŒI|¸2ZQ¼?=üÚœsE­^—¡ Ýž­ä¿.C¹wÇ9u†._ýœ<Óÿ¯’[]â“úÛ ÑEòÌžîyÈI0»×mÒ¬zÞ¦…ŒüLfרÚwi¯jkŸˆSÇêÀ×'³F!®kðÿývÓN´R’~÷ÃM†ë<)ŠVã³æX®–¤¾¯¢hïCOÚúÍv/DÏâòx¦n÷Tcþ 7^¬TþÃ*üd7-ÃPûaNb®œ%ÔøÉnZ–«…¶n’uÖÚBÍÌ–c8®AD¡mÚu¬Q€_ö÷Ó?ëÿùøñãÇ÷ïß+Ýò´8xxxxx—ƒŸØ0TÙçT‰©ÃÓN‡¸\‰RâÆLúzø³< –µˆ]÷“ÍñôdÔaÆÄO¢»x)K ̶¢(Š¢”ŸãQ÷ƒ¾ºJ ¼µÝW$îVC5Ó±þw»ÜŸ<¨·‰Sð+¹éÀWñ(8žÙV¥Ýæ§Kw‡Ê‰zR³#1ƘÔiR~L)ä-æ|WòL%!ãV…a½·çÅÅ'qzÝ®/wcó…Gr+õS[2y‹yQz<½_Ë£àDÑêu¹cw«Ãœ¹á½elDÓ6ýþd¶˜Ü²]((™{Î-WêO&}IÊdt˜Þ1äL¡ËeÐìˆDb§¶œ.Kë•Dg¥¿ìL¨§u<1ª*Wþøöݹ|^C&A³Ò;ÊË=EY!KÒÇMðQ SÁtÊ C÷V<ó!vÂüñ“MÚm—ˆ»ÝW­(=ø‚cR§)¤SîRÒZÄñ<¦'§sÃâÝc:g¬(Êý,JÍYƒâëf°³Ö!VmÚV¥mš5PãsÏÍ¥~8÷\T. še5—z›¯¦5ÐG׊r=ZEï“J6r׬ˆæÏv½w'î5K÷>e=Û’ôà§ó-cIǿԟ¸“·>™Ô±´­AnRcZ§‰å8yf<§.6ê$È–£eÌÞ†Hä~UÂÍÛöï·M(Ô®ˆVTT.Q°æ¶oÍî•1©Q§`‘[J°^E³E`ô6Ï>Q'u~Jò¿¹ù³2A«“¯‡<ý¯òÓ_˜´à§ÁKG¯Óf¦Z±Ka­.Íß:MÁ›Î¥_Ç£ØÚ¯ËP«]ùLº{ÔÈÖ•ÑŠxÄNo›l5ºVFñ.[wHÊýŒüu@áR¿Ù›ƒ/œåßz;+-÷(b£N´ã!¤ô: ½7ÿãîDiþõ­i ôÿÓ‰¢Ù"po»æVÌfCbŒ{8|6w´¢hÆ] ±Ñ¼)_ð)I~:9çQˆf²tpU(XûÜŒììxNÅíNî(š-<áèžÙîÀ½éh‘ÿº å4¦;0äpùêŸTîçÃÔaº6 6ê‡ië ±jþrgvÌüÙ®·ZÉ OÐwÚ}³6_x$÷3›yÊÓ€ŸËvB6× ¢LÀ=ÍÌ–_mÓ®÷bô.ÓÚúM»ÿ 7^,7Îh'f›r1ŠVã³æX®vR¹%ÿøÉnZ–«…¶n’u$}µü¹Aܳ ×5ˆñnïðuènHÚQêO²ŠHoÙUÑV?{ªË‘ýi¯h&õ_,MHÊÿLÎÄ`L8½Mr¤w®œÜaKj–w®ø‚EjNÞÏ"n/ZƒõåOFûIðzÅ&¬3 /àT¼ŸîÀ ³­Ì¸ÕþØ͘éu[o}÷Ûñà´»Ø7Låz–ÕϜĻ«¹Ô•kž; ÙVfQŽÿö¨ž'ìœT†õ^_šsñvÜž<9¥þÄÒÈÖÛ<½Ôö¥‡½{À—b?ê)ŠV¯ËpÏøKªI¨dÊþ“~?4HLn™ô¥Ü|¶ù•+õ'“¾$e2:LŸÎpï1¶Cª7IJ|N‘'Í/šk/Še*ÀóçY2 š•–µÈû÷ÄxªÙ½ÂÝ$ò_—¡ÐìˆDĘڒCûi¼Š"~Žx(÷RUçù%·šà™éyç¦Gr«KDW5‚µŸ{Ùúý,ÇÄï5²Ÿ{—ƒé4àâejš/'cê­&xæMêB¬F÷p'~1‚1©Ó¼Å<µà[‹¶¢(Š¢˜žl¼Üq{W¼{ÔSIHMLuè_7ÍtÓœeêMÛŠ¢´Í@³*c|޼¹ÔyzÝ®§ù•KD‚fYÍ¥Þn·u›´Gn1gËm›Þ)5?̧Džªù3uèu[«ÀCwV£kEQLB[O÷îÌXÿÝ–œü7Ž8"¡vUx§Ø¨S¸yÛ:“³…Gr«ÿÇpö¶¾pO`I·Y—1þIê?jd?s¼ù"Ðn³¾Y¡œb£Ni׿šG!ŽëºŽci™z«Ñuúÿùb×’–[*˱8íçdÎûÙ3iÒè#š/<¾V vš‚7M¬P<õâÙöÒrÓ°{ä•[D÷VÂå«_˜O‘<'æ¿SVKí§JóëQ´]+Êõ¨Ò–îÀØ-ܼ‰w×u¬æR×íìj“lä¬)ëÜâf÷JÛôx—È.¤ Zm‘xV±gHñÚEQ}çÏv½w'î6bžœW5O$À¯Æþ> âÁ9“ÆÎæÚŒ!˜z:M,Ç1( /6ê$È–£e,džHä~U2säDo›P¨]­¨¨Ü¬E³{eFDLjÔ)X”"ŽkÄ5ä óQ 䡲üaå]©?1äÐÖ3ÛèͲB¾1é.›ú`Q½!1–u*Rp­ð-à¼?øÜKÝ—ùÂ3z ‘ØÕШÛúCa}ýñ4xéˆãuÖ1)”ü¢ÅÖþ{]†ZíŠÈgÒÝ£Fv¼C—©C§·M¶]+£xw¯;$å~Fþ: p©ßìÍUî Øz;+-÷(b£N´9ô”ŽS OyþEÔâ6§q',쬶ß6!ÉAºAì4…pùV˜ƒ¿HÛÖ—1µ%ï;BQ4{Ð/V³#Ž}ŸˆvÄé¶ä]GR¶Í3ÛoÛL{Û5Èé/<Ãhui6à ðËs…Øi&†ô6FŸ1u·ã9»“;Šf OÐn»åEv†ìMG«ˆü×e(§ñ0Ý!ÇQIÇË=ðâ|˜:Ì®mT£Hž‚ü£hµâ½ÌLê¿$rr{›²nK³s¸ýºÒÎl¦÷Ý ¢h5šz$÷â½Ý[Màz.b¾ðH6’½.ûT;ýaùê'-zé^š×Ïì>ÝP£´ù6‘ö¾7¶ë­V¹œÉþ—t{†Ô–í,_€íÅ6:h{>ÀìÁl9ñõÐ6íz/¶}ίH¿¤7^,7Îh'Òf'úè~Æ-ïñYs,W;©Ü"ø÷‚š–åjD¡­›dõÎÑE±<…ùÏŸíže¸®A䙺Ý{Œ¯ïD…íž«àó¼øŒ~n$ÒQw"ñ²´íº@|”×~沑¶ —'Šfm8Éõ½s$(Û¾q`ÕNþ•Î-ñ_—uKKÂ׊äœÝ+oýIò…¶~o=|mØßOÿ¬ÿç_PÄŸŒÔŸXµéos 8›?~|ÿþ½Ò-AkÀ?Ùu£àÊøþáÚªƒ5 < < <Ц±Í€ß›oühµ«6m?Ì¢ˆøqNospôõ¯ˆ:twÎÇóLå~¶ÞÂY‡Bè·ÿÖÓì^™©C··Ùž1»í‡YñÓúÜI£Âam€„ãQOêÐMªI“ÔŸLú’ÔŸ¤g¦w’Õ©?q‡ê^žé-gç‘*~Þv(4;"_¯™¸YÐE8ߣú“Ö¢­(Š¢(¦'/w©/h–Õ\êív[·I{ìKŒ›û™<}ÛôxJP½±gýkÿù›f ºÐÿºL\ ñîQ âüE¹ŸaÕ€“< ÙpœxmÁÙn=X®S«z¾ðvn í8RÈ_DDĘڒCûy¾_ÈÛ&Lü“I_bLjÔ/ÿüÙå–zñß‘'¿)é>Šx_%;³ùÕýMÌáf{k°æ¢ˆïV &5ê,üƒBüu ´®HlÔ‚€:"½…›·óòdËÑ2nGC$Z]L«ÑµNËq ÂŽm*x90&Ý=jdëÊhEDL&ŽÆIˆ:QÆC;µÍó¢öØ×5!XøÕó÷×…Ë‹„Jì4…pùê§N…2¢xÇöàTPBù>Š«šox`Lì|‡5×â]†rïNäîAfíámÖ[·MZûóEмmÕã%ŠjùGÑlá Úm÷‚õgLXšàM÷¼”(Z½.CôÊùF¬Ì‚0[Žá¸…¶i×{åÿjüd7-ËÕˆB[7ÉêÅ×× Éžù@D‹À0do±Š"¢jùÑüAo¼X®kÄ LÚžG![®–M/<´‰È3ÛÊŒ‡{IwÙ¬ÐÖoæè"”Àþþ÷ÿ­ÿó_PàÇß¿¯tË_ÐàlàQàQàQàQàQ< < < < €Gx/ßø?Lê¿Xš_ôÌöÃ,Šp=½È…ýýïÿ[ÿç¿ À?¾ÿ^éD=ÎÀ£À£|y‚1u躮ëUö„cRâÆLúÒ/—©CwÒ—ûjm̘ԟ|•6ý¸ö-ÒÿÑvù‰úÉîQ0&5êáòÙÏþ<»Wf[)­áº}?‹â&¯Ûz{ì“x÷bYƒµr?‹øûj§[¬¹¦Šò)eÄF¼éü ó”»m§G-ð<¡~¼Ú%òä–Ë_6ÙVfïm•Ãú2&ݽl}9©?±ï^‹;ï‘Í¥®ŒVüÿ“þÛ#õùœ¡‡ÂvÉÓó˽H>L:Fݶ=ÒN퇹ý¹¬¿ÍžÑkHŒ­>Ì,x—y$6êaHÍŽ4ö};M ñJøñ.ó*ö†ãŸ_­Ì»ÃÐ0‚s‡ç°ÞëKó¯æEüŠú̾_¾šÞ¸lÙqûýÏË|áFKesÞŒ©-™<ó7Ñ~u½}Íñð#ôùž÷uj¨œÍ_'·ÇláQ½!&MÚOãUEÑjüd‡r¯/±= ¦%‡öó¼$Ÿ3¬¢’rÅ»GìçEÕ~v’<Ý[lý>Ï ãV\w»ÆP™«š@Á:öåü×e(Ô®2F­» "vš‚7û¼ÿ§žÐ윰F&&!“Xi¥ùdŠÝ®‚•è¡°ÔŠí’Sß³ôŸ{½Tþ}ý0&ÝõÈlߌ×úáEúÛ…_íRÿÅ1êöÓ;ì¤å2hvD"±S[N—yzNûIE–þ²Õg6ºìÄÕüÜ~˜ÛOxH!“ YÙØž’r¥þdÒ—¤LÃç”›²$}yÞã‘£guèû\–DÛ±ª+E71©?IÇ„ƒ?/^¯ù³&O@‘>Y6n·ÝŸëò~uz?!"¢`:åý]ú/ê?˜>wÞkE^G’èè¬ñá<½EÑêu¹µ³Šž—"½í^Odž/<’[Ý)’żp<)'KÞ_Eò|Ú¸ôôvr?ßö¥Jr–·Ë/ Ï³Þ×®›?>|”GÁ¤~OŽ­Oê¶äd H"2&plGödozhÁìäS¯ŸciɆãl+Y\.“úÙO¥ù«WQ¹Ü‚\Ò­{ÐÃâå9EQ¥mzçzϳg;”—¾ÄxìÙϱ¿ÎçÈõ6/"žü¾ªí„–½mÂ}õç VmÚV¥mš5P+ÉGêOZ‹¸PÓ“—;©T%êIþÎÁ IDAT=l—Âö-¨ïúϽ^.ÿ¡~¢h5º¾Ï™ù;¡ÿŸôe5ÿqî„:tx4Ñûf _A³#ŠÚæõ’úæö“DŸ½ÞVE·I»ížó¼çŸÛOV£kEQLB[ç×O™ì4Ëj.õv»­Û¤=ò.‘í?f Yƒnyú¦PϲV›¶Mk[·“˜«B·OëVþë2R3šÏ3|Ú$ý¡>·sÛŠ¢(Šn×ÓúVoÏé'óE Ýî½S«ê¿¤ÿü9úä³'²ámQ‹Ã?SQ®?yQˆ1©ÓR»5÷y)Ñ›x÷¨±ÞEáSTq}ÓXì4oÁ_%ý!wœ,xÊóiãÒ×Ñ[n}só?CÎJﯯ¦Ïªïk¾*w¶Sß'yW5!gBû4NÆÌ%" 7oâÝÄu«¹Ô÷¥`L½Õho"?ÊLl$>(¯_[·CòÌv;ÑUTRn÷V£ŠS°‡ò”•K‚V[$Oxlq­Ãñ}îÕèºmšå8ŽAã¯Û’ÃÃze|zS$œ¼Ê–Ü^oˆeùpSz[ZVü=[ÛÙ—¿DÏùõ­¨ÿÒv)”?G?%÷ÿÜö-éÿÉm¹£°D¹pÿžh1îyÎAóö¶¶yõwš² ŸgÆVþ: z£d®±¤?å_ÜOª“†±úë ¯ÿÌŸíPnmûùAú #‡sN…zíç9½mBJ¼Ô3–¿²ë“{o¸ {« árÛ‹õ™Y;%"Ÿ 'ÒãíYýdþl×{wâ^=]ÿGúÏŸ¤ÏÙ½Ò6=Ùpò–Ë®bwâsÃå¹0ŽciydT,Ð[’QN›ò7ªºû•÷‡Ãq²°]Êåùàqé«é­°¾§?k¥ržþþúrú<ë}})¾QñÞÓtP˜¥&Ñ0yþͲB[oVcÒ,PìïÈÈÏ'2¦]ë…ŽlRÉ+—©C£në~î”pfÌNóëUbw¤O8{‰¨Q§`áÓéåÕWº†ì™íöœºÇq[¦r?cLÊ- ŠfzãÅr\ƒˆBÛ´C£v´¾¹k#§åCnÊô°Ê/·¤]ŠüõB…VÒ¿XœMn>~õ禠ÿ—ôç’þ­F7íõÀqÜ‹nÒâ£3QXé¹È'X¯¢Ù"0z›gŸ¨“ŸO¶Ÿ\h '/ÿÒ~R ÞëEq1©Q'A¶œÌÞ™÷óÜô¥ºIÄ‚l¹Ú®ãZìúŽE­gþ,tıO&ÙOsºx½dƒ3Dž©\ÏÊô¯Ö„pó–³xZ­ÙÏî'þx¼tÄL°c5ý‹¥ýçÓ'—‚ïu't|4C£ðHW£Ë÷Ã4ÔŸdåÉŸIÉÕÛŠV£k&–ã{,ßÐ¥ù[§)$»ÂŠúƒ_ÒòÔ\,Ï'ŒK_HoÅõ-Ì¿’œþ¯ßÒû:ö(øªÇ˜©ç¥Q¼w3Ý?ê¿mB’ƒÄ‡#±ÓÂå[vBðÌBíC/<ÃØ> 9”Û½•w;žl9o°Sv¹œ¶/V(•,éÿ´ImÄ“Ê5jWD¾ÔïÉ¡­s«Ž[ù½¾4ÏL:îœÍŸ©C-Y%¬º«'7¾^AÉNq¦Þ=ä–Ûm¶K‰Hõüwo5ý_?žÏqJûÿÑþ|Øß˜Ô±jÓvûþ¢{+ãçZ:–SÛUûy»¿Ò‘{P-Y×*è'—ñˆJò¯—ØhïÄ_.ßýqŒôM÷ÎÏO}´ž)<0zñ•š´|*÷iÏ«×qWªÀR:|£–?ï§'Ç´¸·]óLýWì?¿½>3³WÍŽ8^­’)æõ­c½Ð¹Ú;Ÿ/ÿujÇìb½ñ”ÏÆºCÊzF«+6š‚7§i úé3éi»œÖŽ¿½ÞÊÉÍÿ=Ïů¨ÏŸò¾æüµg(•½Õz2÷ £Õhê‘Ü‹c¹º·Z6îV¼ë•,¥fóɹžXÆEæon¹³û4LiópÎö©£m‘<¯Ûm¹|æÕç}AæëãLZ'åÕ×_Ù¨—d?ßµ#õøÇd>TY¹‡[\vóÙî¨cL$þw¡Š ÐJíRTßÊú/¸~¢ü¹ú9¥žÒŸóûÛ™CÙ©ã][·ë†söæ`±Q/0,òûIIû®ƒ8ÌŽIý—ã_2(ê‡eÏÅÛ&ÜûPAÕr£h¶ð„‹­N5=Ÿ¹Jñl“öøØ ¦cÿ'W7óüÆÏcéó^Ô¾UûÉž6ê­Öyúÿrýç è3xÉŽ·Ñì^·I³^x|õñá]ˆæ‘Q·Ho﬽ #’{™ÐÁÊýá„÷Z‘<ŸÀ×Õ[iþçÉù+êóRïëó×(ʬÞÝI§Ÿ›?è+ž~ά¶0©ÿx°@Q”OIþ…¯½‚r+Ïù½§ÜmhÊjüd7-ËÕˆB[7ÉêSn2¯—ñÒ(©íªVîù¡­·OŠ“ÙY%Oô–›OÍÌ–cl£¡ê½r=\Œ¢úVÒ?ÿþRn»äæï±,בá8F ”Û/ØÏ/ìTÄQUý×K~”º¤Ÿ”˜­=Ëp]ƒÈ3u»÷xfþEý„ˆ|Þö»MS©Ü´Ÿ¸q‡¸Ø]?GÏ»ýÍp]ƒË_t=;W¦ÓŸþÒ(ZoÌš“Œ‡'Œ·Eí[µŸì¼Åë–Fáæ¼~þµúÏÏÑçAˆEÎû"Î0YD­úœžÁvœßŽÿùÏE‘ÞòÎ˜ï˜Æ†! ;®ÔŠÞ_Åíø|}½å_]ÎßBŸ—x_Ÿc]ÿýïÿ[ÿ翽?ÒÓÍ×ÿ7¿L:½Íg¾È}צïÛÄà·0E~üøñýû÷J9üE¥žñ:ø˜/Ίõò¸;>¼òscp üîÄ [޲õ4¶{–åjUv}Þ3%`ÈD¡­Ã”?©?±4È3ÛX%€?ÉÉF'¾;>“ýýïÖÿùÔ 8/ê ÎàË{Râr†êo¦AƤþÄu'}éc¾:öÑù£À{øvž{ðÜ#[úøÁ~RbÕ~IŸ¡j}+毣ž|Û[êO,lýÃ?ãû§µ#øÎ_£m]Q”v[·I³Ýß^SžÙέo­F׊r=:ßÇõ0¤fGbŒ1©Ó¤0sÚãòG;€#^£Ø®Ž&"©?y¤§'zä¿eÏÈLíÝ×e¨5cÜäÝ9†ù„¹ü¢ô™ãŠ·òìM«g¿r[µ\~Ko³_£râú&3ôɇ÷¿ºU¤Ïbù—Ë ÙÇ>ujËé²Ù+É?9‹Ê$ƒ×8L4þÌv?‘¿Ró®¹ÔEQE·ëÆË]µ/h–Õ\êm>‹ýØ—v¢ùSo5!\¾nÍP2y>fp|λ(½ÔŸ´íøº'§òˆwZ§WeÇ ­RîÙ0&uš‚·ˆOX®EÑíp/M‘>‹ä'¢×EÐìˆb§¶y}#jW…ùsd£·ÑÛŠ¢è6i·]´#øi‰¦àMÇ>¿ä§^öììtªØ_™;Ír]×q 2Û|›1µ%‡é±«óg;”[jñ~â’ô«ÑujeÎÞ®)½ŸgÕrÏC6×uÇÒóÈñåú<”íª&Ñ|4ook›Wÿ$y<3^—ð×ÕchGðù|‹-Úpó¶½ø¶ …Ú‹I厢ٽ2#"nãñÈuè­.ÍfDDb£N‚l9Ú6«°!&ùäXÞ…éwŽñ#¢pÃÿ]®ušXŽcP&¨b¹ÉAÅÜ®µ\èGŽ OHý‰;i”äë3_þXɳE`ô6Ï>Qç¼ÆüÃÚ|b×ò<´LK˜?Û=˪óûŸü—§¤¨ =cÒÝ£F¶®ð)suèô¶¿òÏ ñwHÊý¬j¹‰E}Î> "ò_—¡–U×¥úÌ‘?!‘‹Öó-¥?¬ÀWà¯ÄF–{wq|Lw`ÈáòÄØ›h5šz$÷î$Æ¢h¶ðÓÒ½…é¯jkŸø—UÓõ„{W¯ËðX>‚Øi1ÔOÐgV~±Q?Ì£Þ+K†vŸÎ7nÕoÌšÇÿTýÆÎüÙîYÚmw|?£ùƒÞx±\׈óLå~¶ûaÃu4Ð%7}ÍÌ–c8®AD¡mÚõQþù IÌ}^>—Õ”˳ÕOQ½ŠôY".%z+°ìÑŽà³aÿûŸõþE~üøñýû÷J·ü­Îàô(S‡®ëºîPe‡I™:t'}‰1h œAÚ“ú“ü>ökTäròg8+ž;tÒ›vÆÀÃ+¿¨¯½ýBúüœ÷ÎEêõSÆÕ¯ÙŽß²ù6êáòÙ?õá©Ûz{ì“x÷bYƒ5lf÷ÊlÛ¬áº}ŸØ‡oLj·ÃÎã·Þºyù_ð-˘t÷¨ž'ìžX}˜?×xs©+£ÿÿ¤ÿv=Z•È_B‘ü¹òt™me1©ÿb=ö_Ë^ê¿XZ`¶•Yô1=lë[Jý‰õx÷z3^EQ‘<œÇrŒºm{¤í\/êWE׋ä̽^Ô¾—’¿DŸ<Á|á½†ÄØêÃ̬w™Gb£†ÔìHcß'±Ó¤0üÅF¼éœþt.tªc´7<ýüjÅϦ:t g@_À©ÈžÃz¯/Í¿šQ¤O>^eßïo÷ùÂ3Œ–Êæ³dV«%“gΗÇë×ÖÛ×|®?BŸïy句ÁùŸõ>¨×ïûÚÉoǪßGÇe¸n:ÇʘڒCûi¼Š¢(ZŸìPîõ%¶Ûïg ê ñt‹§%‡öóüÄüKäEÏ{Éø_$ϧ=__Ao'÷ÿmß®$gy»üú<ë½ãºGó¯ÐßâzÕ'ØÕìÒÜ÷Z±~Îxry"^¶SEQÚ¦—Ìœ·äÄ@J"öM$&õ{rl¥fO÷do[<'ä_"§hÖ {ô%ú¨‘ýtš|WµÐ¦·Í¡E¸#ÿY6ß1y¶V)÷^–tëž·k†Š¢(Šn×sÇ«½ñ¤°óWí'óE ÝîE`WÕÕ÷ÑùO7“:M!µ[sÛ½DŸâÝ£Är*J<ÛÂggRÓXì4o1Kc ÇÜç=·½Jäù´çëëè-·¾¹ùŸ!g¥qø«é³ê{‡¯ŠÄ-Sö{úcê­&„Ë×Õ±v¬d—'EzøYãÉ;=Š«š°cz•Í»‡›7ñn⺎Õ\ê;=‡ûRÎÙ´:ð±So5Ú)¨8ÿÃ|öäœ?Û¡Ü*ߔӽը`Ê6GÎÌÜMXöå/}ƒê¡Dž8ÁÀØm A«-’‘:¶hK å¦nº8›Û¾Y½­F×m3Ð,Çq Ê3Fä9²îT´Ê–t|5«¨_]/—sçzAû–÷«JòÕ'´ÞsŽ%Ê-ªû÷DÇqk¾š··µÍ«¤_%ú‰#¬üuãÉUNòðÌv;K£’ü»-9|×’Ëîò6@ó×Á í~˜þÂÈÆáU¡žCûyNo›oúôeáì¼Tº.÷þ7ÊQºüíê?»fHD>N"ª»ýáØx¸ÿ¼¶W¹<ü|}5½Ö÷ô>S*çéãð—ÓçYïÊåèŸÛiÙ÷uÕçºÜ¾*”ÿ Ï/7ž”ò ö’&ËvÁÂ/šY m½‡¡g~I#ØÔ¡ëã˜àlH:t­ÊnþïzòÞŽ‚üsóñÅFÙr2±ëaC$ZímžŽ+ÈÔ¡Q·õ?߸ɑsö 7^,Ç5ˆ(´M;4jû »òç–[(?ßÿà—Ø…†Úzv»Lf¤æaøÅõ¥ÃMäá&iß|½©C×=³ÝžSwà8nk'¶ûPžürKúOurûUÉõ½å\ŠÚ·H?ÕkT®Ïh5ºi¯Žã^ts«…¹K=¹ý¤`½Šf‹Àèmž}¢N~>I¿ºà”I^þŒI—ìXÁ:í¼+1©ð¹ÈM_:AžDzÈ–«í:¢Eäí£øh=ó1¤#Ž}ê4É~šÓÅë%üù"òLåzV¦µ&„›·œEÃjÍ~v?ñÇÓà¥#f‚«é_,í?Ògš@êOÜI£lܸ*ÐçŠV£k&–ã{ï èÒü­Ó’ÝMUÇâñ¿XžOx¾¾ÞŠë[˜%9+vü¯¨Ï‹¼w*ö7n«ó}êÐ5Z]šÍÎx®éT»t+¾¾ÚxrÔ£à«Ec¦œ—FñÞM±Q'ÚÄ-Brø¾$všB¸|Ë}Iåì7/<ÃØ6(_WòÌLNÏߨ]ùþ: p™'yîî nKÞm Ùr´CgåÌæÃÔ¡–¬fæË_Pn‘üW¥ò|â óH«/Ÿw§dg0S‡N¿9óõƤ~Om[™ÜÚÞîVÌ•ç”únûÏŒŸ‚~•¹^ôiˆ£ò§íËŠôSqv \ŸÉøX›¶Û÷Ý«?×êбœÚn÷>o×\úÆ[T+éW—[-É¿^6–¿âñ¤šþ}½óóS­gŠ6Œ^G|¥&-ŸÊß%çÕë¸+UðÆ?´*'gô“(š-÷¶kž©ÿŠýçýÄ]†ZîÛà$}ò‘€Ï*ºCâF ïF«+6š‚7_vïwì‡Ú÷·×[9¹ù¿çyùõy™÷Î;ô3¶{–1Tç÷³¨r;>/eòçèá«'¥üµ«øÃ¼e¾þËÔaêSE«ÑÔ#¹Ç~uoµÃ¸[Ƥ»žœúû×3¹x×Û[­¿·˜EQÍžpzŒàì> TSÚ<œ¶½ÿ¶;”3ãÿ‘ù0ß] ¨¼%;›‰¹òWÍ¿ }ÜÛ´íúE ²£Ÿc¡2Q4{0[ޱê©ï¶ãžÞ’( x½Œ0‹¢yªöÚ‹‚H+{¾„á8yfûaNb¥~U$眺EòµoI¿:]þY>?m¬O¢ª.üµß’~U>å㺑gêvïñÌüwV‡w¿Èìó>·;tT*÷RãÉÏÒóîsa¸®Áå/ºž›Ò‚éOÿ¨k­Æ7fÍIž—íøVü>*hߪýdÇ ©[…›óúùÇ÷Ÿíø“觨}‹ô™÷úùŽil²°óAçJõ*ÿ‹Û÷3øúz+Ê¿ºœ¿…>ß=¾³¿ÍŸíž¥ÝvÇ÷³üv,W í«<ùKôðSÆ“óòaÿûŸõþ•Ô瑞n¾þ§¸ø`êÐém>óµ @®S›¾o?à•ú[ðãÇïß¿Wºå¯¬'·>æ‹3€bÄFý0Ž €Oí„ü<œ0ø¥Ý‰‚Ðhð ìD=ÍŸíže¹Z•]t€³Ç¾ø³ ¡­Ã”?©?±4È3Û˜Òüš/Óì”~jœêÝ iÔàç]QOPx€KxŒIý‰ëNúÒÇ|uì£ó¯„ÔŸ¸œ¡ŠNÀ<ŠKø êÐuÓƒ´Râ£ëû*)'8-«Ñµ¢(º¢ð¶ßzŠ"~þ÷;õ0¤fGûÿ?{o¯›:ó-þ/?zîE [ú^E ›÷.È–RœSzÙRªüü%÷Ám$ˆÎqeìä[ÀE Œ|[ûü/Æ66ÌŒ_€ì${}šg?Î0/kÖ¬y[3€ØïA”±_ ~.äõ"r«º=ÙáIAAù¸Ezàñ­[Ågü$=ï—»mv³ {}q@¿½yÝô†œøÓ7•,0ÉCÑc$ïë±Ò•Æ‹Gxz‚Gò·ü¨Ù¼åmé½®$ä&ÄÂsÈ^­P·'ƒòé-¼4^Ü®Õä»/—Ê¡nº‚ ‚ òµgtÄ~¯å¿ÎòÁìÕÏ¿©-ßjGÇnÚ-X­ÃÞý}{ÿTÊo%ûÁ.„NW„’t³%ÿ`æâié¶çy®k‚¥’ AÐnå({võâD§y.ÎFXá·ÏwÙlaµö‹S¢ã8릋 ‚ ‚ _—y¼i·¢ýûáÿß÷Q«}°…íó ÛuM(:J…»m¼\‡æpÿô›eŠnšD q¼|P–@ÆêÄJ›zæí–K±Û–l»ú!ª¨+¦ñPfPÌð…ç Ú“ÿÒåP7]AAùž3ŠâHþh¤OÎY%o ùÓÉH`B»A¦¸éòY½8CÛœj«‡%ÙĈ6éÙŒráA=êà ÙúЦîððWŠꦋ ‚ ‚ _®×Sð¶‰äá(ñ7LL9Úù2‘“Ðäßb·sG§+ÖÎT…tYÄÛçWäáH„8^®ý9›Qé·Ìð7í„»A›¤´Yr¨›.‚ ‚ ‚|]þ…ã ‹LÏ3‰Oog?¬¶k{Ä}'=ÀptÁDŽñcÅIƒ?{dOO·b‘V/ÎÐÖﳇ%¬~ݹíyfò7ßR–œüPÃÇñò§uëš®g@äXNgxZ®‚¨ñ ¶!‚ ‚ ßá?ÿïwÿߣ AAùýû÷¯_¿jý䔂 ‚ ‚ Á‚ ‚ ‚ 8£@AAg‚ ‚ ‚àŒAAA¿rF!ÚÔó<Ï›jÂiPA›z‹±$×Γ Hã=Èp%ù˜þ|!yæj;Òxá%,Æ Ûݵ[å'”Ì'¯¯O+7Ìg´é‡ö>?ÕО|K}.¼™=˜˜ôeh~ÇŸ† ðdDþ•†È1îÈ;Ó‚6q“—á²o‡*¼_ÁÈgÜ#Òx‘/qixN‹¢ÆÃÎ$]nuÓÍÇC”½çM•C¡óÈU ôª 5ß-¡+ÏiäIº¾¥þ\#?M—#g*ñòAY‚6õÌÉ`ùY_,!…ê¤b‘Æ [çÒO ‚6±õÐRï–ñ™v`¸¿Š…ù€>©ð¾ PTîŒ9Žä™¨}ÚüA›¸Ãý¥õáòò<Ã>0làé—/:6B¹}!y~Œ}¾H¹¶ÏwÊ3Hã…ÝÆzüÂõXiF!R·m^Êߦމ^ÌÁ.QIã¹­‡–ª‡ƒ‰ –ª,cAÏíÇñۅŤM=³ãê,q4·íÉŽož¨ù$’ím åyKþ½¿g]ééh›žE­ UnuÓi4?Ì¥ñÂ~½‘Ç õÅѼºå­.‡z•äêQ}¿Õi®?ËeyhÕöt§>,c²^èšúÉ¡W IDATÇñA‡ÒtYr.mQ«µo»’ l¯6|o›¨Õë‹§ñŸlèˆéÙb,¾ ‡$®IžfÛ8Žãíìɉäaþ'”‘:5Ÿb¿×ò_g©¿Ù«ßêõy{LuÃóÇ|'åbÊ­vº7í„»dd¼m¢Vû†[_tù7*/_8#à*zâèQçeÍL÷hW—õ=m·Ëµ®˜Ì†`©?f;¨–.SÎ}þˆÅiS­84¶ý¿¤<ãxû¶9ôÏ,ï –<‹ßS®Ö>È·ƒÂ”x½bê«]pì$+?¦‡ŸAn•õÿ Ûµòɯ—/ ÏFöÙóJ㯡oI¹:ݬÊê¶kVxªýgËá+Ûv=RãçèíëñÂ3Šd;RQEQ-Ÿ6®Êþ+©Qàî=^Ë,Œºòñ[¡nO23²i·_UEQT+ÔíIÖ9¹fÇ1Tò“d‘{p+§Q¦ž3…¡u´JÉçM»àró¾xÑÔÏNPÊÅ’[ÍtãxùâD²9Kñ-çeÇüú¢È¿~yKõ‡9Y«¦W‚4~ÔÁy:ÞÆ‹Ûu"LË—Íù(AR¿çßCù0kz¾{ ®SÓeÉ™£ÏI^o:¡M]âMtž#ÐÛ:ìõE±ßÞ¿½CÚ¾8ò”ÍáÞPE1Ð‰sm½²éºy#ÈŠŸÚ^¶ÏwŠ¢X>DŽA¾WYhié¶ÝÛªªèDµ8v‰þ8M÷°g¨(Š¢N'“OÝv×Dž«u¨ßÍÉ™ú ëíWÕò‰VNj88r¾ôÔïµ²q+)šáDGaXòGz˜äSQ’U²ú Å~¯å¯I{çök”vA­/N~>L?Ü¨å¥Æß ŸµìÕg“g]ûLV»eË¥}޾ ‚v¯·¢ÍÛ¶¬KÇ ùð,{’×¶'œz¤ÆÏÒÛ Öã™3Š›v+êSÖ‰©Có{ŠZz{ZÆ“‘À`bfQÅ¿zq"ù6 ŸíþÃ*[5†Á­Q—Z£ý»8Zxžk÷6Uò'ôi>sk'ħ¥UìÑç¾Üðœ‘ÁéšY.šÜ¤»}¾S­P·]×5¡0¸dÖEþ5ÓåëUuõjp¯Mnd*m²—~ÏÖ6Üâ4€»ÇEO—!gº>çÊG]ð=i¼ #Ňs'™äjöîïÛû·ªÈÓ·«`ò×<ˆ¹T 'ßRÕÔÆÆœøKÛK½í^â6ìB(·K§á¯Ä€˜ö€™Ïüž!dÓP,·Û”´šÈsõât†#±J»ƒÈyYÁû>‚4¿®X"ç !›®çy®kë¡UÒ òL#¢äXDMò#Œ²~í¸]0닟Ÿ+ëág“³¼Õu†›ÏêöêÓɳ‘}®.Cþ¤Ï÷wuÛ5' zžCO{ÆÙ6f†g¥K'(ô‘ÜâúéjSÏ”}KUW0˜¸®w›ƒ£ÕWÀÜ먗.»X yƵôJ¸™šÇøP÷ g\£=ÿ;äk*þg”çEìsM}#ãaâ¯M=óvËeƒvÍ ÏÊ?]ßÀž0ê±ö àEê±ñŒ‚ì‚Ímâλ쳛b·°?Úñ­#æ²~2,>>ºìBˆ6')òg•SyßG ‡é\ Ä~¯mÞ“±ã4 =Ÿùð‚6ÕÓÝØ£õÓL~Í _åMOÀ*Kn5Ó¤ñPŽƒŒɬ`8–V³€W_¬±oãòéUžµôjp+ ˆl»ºo©?WäŒCrBZЦîð°¯rú’î9ifºï#¶œKÇÍíWU}¸èܤ]kS×vÛECÙì”WÖ“ïBhW–ç93"Nü^u »T[þ©¼j]?UÚ¥±zŠÓž„ßîÊíj¹‚-סw?°ªÔ×ùrn&ÏCjo›HçZ;®zÿ1ÛÆ±8zÔCK¹[VŒAAA/Á? Ú­9/É[«'’o5A`ý†~û|—m>¬Ö~ág'qÖMAAAÏÆ¿b·-ÙvõÃ÷¨+l?b‡/<ÇÑžüwû|gÀÂv]rÏ4ÖMAAAÏ8£v!DãÇó¶ÚFxAF:8†ò¼A›ºÃÃ_·ÏwÊsr Â›‚ò°¬.‚ ‚ ‚ ŸŒ Ž—k¿¥ß*þ†þ¦Ý‚p€ h“ô€vñ·Û·MT‚ ‚ ‚ _á?ÿïwÿßÝÅD“Ž?ÂÁa‰ȾDvs“µé áéÇ,±8w§+AAA>žß¿ÿúõ«ÉŒe‡ ‚ ‚ Hƒ¾p‡ ‚ ‚ HspF ‚ ‚ Î(AAÁ‚ ‚ ‚ 8£@AA䯜Q‚6õ<Ïó¦šp½$mê-Æ’ \)žKÅÿíi¼hRׂ4^x ‹±ôÇóóIòßP‡“‡êú‰êåÛ´î#KxðTûòD;ÿeåI’Âþôké?ÚgÔŸRþ…\„ƒ‰ÙI_¼Îz üƒ‘cÜ‘÷°mrxx"ù˜Ée4·õ–o©?—q\jY²wðòñäã'X\ü]mj> ïcÇ8 /Hã£ç:òOmPaɳ’ˆŠ‘³¾3 [[ž¬øA›Øzh©wËøláSä|ÑÊ¥Öcüóå–(ë}•\º¬ö/”%hSÏœ –Ÿõ%’ùNšmi¼°u¸x“¼ˆ^½îË›Õç¤}¨ÅöùNyi¼°ÛÚSß5ªÔ5|À öHO’æ|xsI›¸Ãýºž¿Dž§Y½¸>Ÿ%· ý扢}Fýù,öäŸ|¼ÝN´y ŽBDŽ¡¤dz0˜˜`©Š¢¨†úãX²yë>ÂÆ¯˜üò!‰YU­P·ÉüL¤ÑÜì¤éè£ËÎ¥¨ù$’ím EQTÕp:f6§†·ÏwÊËw¥F•'oXßq UUUÃÝž øßÙSOž¼øÅnüõê\ ¦Êù‚-„åüóå&ÒèQ}¿Jº¬öBX­}èt¯ºÚ$¤+M~,v;Q½¾$‚ õ{EWÈâÙzõMð­êöCLÌÓÝó'éó|KMí¤lº“Ϻ)EaÃf‚òdÛáóõù²r«Òo~Ð>£þ|{ÒðÅòAy SÕàmµz}1± C°Ô³}J8¬ùú¿åÚ‡NW€›v Â]2· Þ6Q«}S?þcKGͧØïµü×Y@úãÙ«ßJ[)§\ÙˆíVŽœ—æ .Ëæ“Äù4ÛÆqogON$Ç’ÀúÎŽ›'O1ÝËLFœõã¯V®l‹!gV~²IGÛr=•[Yü•óÉ•›8zÔÁyY—뫽|\×.ç®ÙqžÎXµÚlÂ^_ûíÍë¦DoÉîmö—L¯ »ºw])õËв'oÊÐÒí¼o']i¼XŒ%)§p\}à…çáü93EÎÚÔ›ŽI^Ri'¢®åÝ$HãE®­ýïÅ˵zq¢ÔÒ³ä)ä,Šõ~jçùzU]O |} ©Í³–üYúó—ɳ`?Y}ñx‘ʨ‘}h&·8Þ¾m¢R;S”[Á#…]¿”þ«Šüó߫إâ8íó‡Úço§?t{òq3 êh5Ž·ÏwÔímꙬ½©–OíÕ†r6[¾8‘lÎÉèybëà¼hYñ”ƲzGËçM»àÂô¾'#HN¹Z0Êþkã›4^Ü®ssÄùHÜÊ©AN=¬Zí`~g–—)OM»ýª*Š¢Z¡nO4vºDA][olºn•ùz±ÂtNÏ33?dÈì8†z´ÖE—#~Nþ©ùäÈMÆ:8O³ ‚þpfw‡×›NhS—ìVŸ·@ø¶{}Qì·÷oïê]þI=÷†šìíÜøÊϪVüT} [†–ŸÛ¬àHÖÒm»·1’ݸdûˆ®·ìð׆)gYo¿ª–O¤m8©ÂQN5µ*ÎrɼøÃ\}Nåy´¶m8¬¼µì|C=Y­CýþȬՕ?Gþy’UBÙtY®á‰»Eh‘­´Þ?é÷ZÙ’;«~ÅÑ£ö –1¯_cô_ùsìg©]:ª/´ÏiŸ¿Ÿþ\ŠŠcÊ‚ÂaBI›°&féÒ<ýžÌ™Üâ0wû|§Z¡n»®kB:bÅsþþ@Ö—ø ßRc÷X<#Á-à½3@•'™²r‘íßÅÑÂó\»·1r#ÖwTyf»]IÒ«Ü.-~bTÉÀ·T5m#qÅz_½8‘|« _ÎÔü n制ÄN—#~Vþ™ùdËmp¯Ãi~*è³½\gßB/ˆe8Çù•Ì”Vë°wßÞçý"™z à[‰#o° ù>]½bÅÏÒ‡&dî°Á.äë-#ü…‘ÍÓ5-¶}p^Vð¾ Yi²¸t´›ï)¯Áà^oå½kOå™Û뀀dNlbçéÉêÅé Gb¹aÈ¿Dþ&y.ÕòeÓ¥m—Ý$Ó‰=?F2㺶ZYrìœÔ¿~)ýCþ%é–Ù%´ÏÄ>CýáÛ“šü ܳÔä$_6ô·ç?Œ"¦9FÙñ±Ûp0Î2›ZLû¢M=Sö-U]Á`âºÞ­¥<,™ñ°ã/ž.9qÇËŸFwn»ž ‘c9‘Yé£8ÊÑæ%(O—%ÏãCÞÑ>7ãŽC}ÞÆ‚ 3qÊwVºty–¬ ÐÒeM¨éŠÝ´dÛÕsjÞ!ÞÖ“³ H¬ ¦Ê­v=2ò [ºÜmjvãgPWXí%Þ>ÿPw×õ.zØ‹Œ¢3Û9 ´\‡æpÿô¹ò¿äR-~Ž>4!=ûlj)$¦>PÃstÓeÙöôb‡Á‚vQÁµå «µoûâ,€~œ§\¼\²IÚ€o)wKžüµv+Ú¿S6ûêU{c= f¯á¼/æœ\ëÉ_äêÏ_&O’ rzØ[t3ûÖÒM¢UƒËëa@/²ü°êwû|gÀÂv]3ß0Yõ°Wd¨òßrõŠn—âGûüQöù{êÇž4˜Q3|3A›¸ó.ûŠ€ÕÚ7MRdxÔìJ±ÛØ3:µ®$8Ê‘cQ¥ ÇÒjV-žü÷8?„¯bþrámªûëÒ3ï‚ Ýë-ß* «¤›É3¤Ñ£é-[‚6u‡©æ€¦k ö{­hó¬ïôtiL—'Kƒ™éV’[®gÞ…mhB•3¯sÒ' ,¹Õ­GF>YrÛÝÊEƒ Û®N ']N{¤ñÜn¿ªêÃEÑ&íZ›º¶Û.ʺíâhò¿ ¡Í•ÿ¥fDœø;œ1Ú™°õ¶ÁX ξÞäÚr†ÄAÅöÅ7èÁæ‰?§mV®ò©¥Ç¤÷¨|û_j7*I#ôîVCù×ÔŸo/ÏÜjK¯/ζÛt uwïÚshÚÐÎl_ÁÛ&Ò¹ýonQ•¬ºzSP–ÌþB¨'ÿ3Ûõi}¡}Fýi¬?5ì —Ž’<íÊŽmZc:¼m"™ì# ÚÔ¦9%ñ“yU° ó^ ™ç9+ž ñ7Zÿëgù ™þÔu¹*Êóà[/Ú$7ÇÛçWäaâ7¸×‰3ë;·Òäy´Û51åfñó†)-¾&G·üÄÛ·MÔ:qnfÉ­n=2óÉ[vAYâŒ9†zÜÒ-i/ô.þBƒÅåƒj8Óm|8Xìv‹rùÍpvaâ&HãyYxf»`êCj~‹ñë¦[Eo?–zrn¸Kñâ€þøØ _gÁ.nΞ'v€ìÂ3ì<«~ëêÉ‘4:··ÍäÿéôçÈ3[˜Ê{SÄËÃÝž'ïúöá,Ä~/µºåõ›?‰[±~³þ‹)ÿºíšQ_hŸÿßF.Ë¿%£ÞâeÒJö…)€~X§M'òïK˜®k&ß·³'§gÛž9†öºž‘ìC‘åŒeÐã‰ñs`ä3.< Ò(†¤ñãÉE]yÆñò§uëšo™Nšÿ„›lÈeî¬ïÜõ¡cy&g ó»]âç S~ݹí% ¢bÉ™•ŸÂîaz¯9Gnœø«ç“­‡ô*—.§½|ŒíK½ªÇo—¼¤œ#ΰuh›žgø–á ÆOÕ‡Äl[PTÝZérôöPWÎE;czžIòÏúž_sÓÃ×?þ²Aog?¬¶›¶»ƒ+ Óγ귮žzߎ­C´o¦çŸKþŒß)Ï M½áþðH;‚ ‚ ‚ _ˆœ×Sä$û Á.$_Ä~¯å¿ÎòÁìÕoõú"‚v+GµÞäÜÊ‘ó”ŸN ‚ ‚ ò ø =™îˆ{R/’͇v+Ú¿¿ï£Vûº×AÕ4Aª÷AAA¾ÿ窧l‘ps4à ˆÝN•”:]Å ‚ ‚ ßnFÁ!xÛDòp”ÌSŽ6oAá» M³ƒÛq¼Ý… ß@Æóôvoß6QK¿×h7Õ¾ï£V¯ÏY ‚ ‚ ÈWä_ˆ™gâx;ûaµÝô"¦ì lgON϶= r ìôdöêÅڦ百e8ÃÇäûöù΀…íºfSv; q¹:Þõ„ ‚ ‚ _ á?ÿó¿»é£ AAùýû÷¯_¿jý_¸CAA¤98£@AAg‚ ‚ ‚àŒAAAœQ ‚ ‚ òWÎ(A›zžçyS­Æë‚6õãï÷ Ä§-— HãE½:úœå¤ñÂKXŒ¥¿DßòùÏ58|¥¡> +qÔRžjhŸ‘?%O’ÊA¾ÿBîÑìÁÄì8†’¾Aæ7}©"Çøñ¼¹ÝØ!|þщlÄ0·õÖá]‹æ#£x´©—ÏåQº—2‚‡‚¥éÖ•4^de•ÿ冨ÇDìùpeYå”W´‰­‡–z·ŒóàQ–FµÇIÒŸø–ús™æ„Ïix~¹8õr¾ÆËe ÚÔ3'ƒåg}‰…(C'-Ž4^Ø:”ªP“TNô¡Y;î¯b>N{¹Ûç;å¤ñÂnèÜfTh`…¦ú§8Õ“¤9§æB´‰;Ü_ZÏÿyžfõJ]3‚ ŸgFqhüÝN´y Šæà0ÇÆ ûqôÆA&&Xª²Œi<·Ços,ÒèQ}¿Õ9ÛžžÄ³|P–‡Ñ=Ý©ËKZXmê™ÇPgˆ£¹mOvÊò|XV•ÿE{  ÔcLÆ#ù<îtæÃìEÄnü×Õq÷ÓÛÊó–ü{1~ç÷@‚6uÍŽãø WЇžS.N½\PWkßv%AØ^mXpÖðHìv¢z}i ö{EWÈâ‰>ü¥\è•Ïãæúç‹•´mê™î>Á ˜j®¢Îp,­>Û,â+Ê3o‡q¤… üs7í„»d޼m¢Vû&7ð"Ö¾–J2„ Þ6Q«×†Ñ£ÎËš2(ôh®‡ÏÅ­XV›ùPóÇÛ绊Àñ0óÊÅ®—ºzX"ç÷}tµ.hS—x·Rø¶{}Qì·÷oïÊA/n×Iãµ|Ùœ²€l÷†ª(Šá€~?h¢ìø©vcû|§(ŠåCää{•Åþ–nÛ½¡ªªá€þHF„œvG m˜r–õö«jùDÚ†“6" é¦VÅU2ÿ°EúSyÖ¶EQÃédå­`¯Êû—=Y­CýþȜ֕?Gþy’UÙtYGþŸ¨ÐR”»Ï¿)„ H­ÅM»u¥0Z IDATß©V¨Û®ëšJª¬O&f>Èà^ÚéàVŽN¾Å¿zq"ù–˜xV<ÙZ‹[aXÏŠŸW®hÿ.ŽžçÚ½qè©©ò)µø”µ+Zü9@n×Vå»2—ªÇL€÷:TÜ 8-/éTÉÀ·T5é‚âVkäÛAÚÙ<&þ·µ÷Cñ”÷—§åbÔ{-=¬”ÿuaô#Q28Ë ÌÐVë°wßÞ¿½Êb^­ #ßJ<¬‚].g­‘©ìø©v£!Ù1˜`By»; ad3¿˜\"çÈyYÁû>‚töZe{ödÅ¡°×ïµüõÏ÷z+Ú´èTž¹=FHæÄ&û¢ôdõât†#ñHE«Ë¿Dþ&y.ÕòeÓ¥m—Ý$Ó‰Ïz~ Aºü Œ³­‡µUÙ·Tuƒ‰ëz·–ò°±ÛpðÆ1¦9Fz|Y›šÇøœ.QÐ#»hɶ›óuº"7ôx2ãµÌ†tÓôt]áÐmZ@FüìrµtÛŽC}ÞÆ‚ øòa¥›žŒÌ~kÏÁø1 Xñ³òÉ–<+Ý‹Ôã¡~FC¹pâ¦^yY§âxùÓèÎm×3 r,'2Û©xù¬O§å¢Ö KŸ™zX–ÿxûüCÝM\×»è¡|2:ˆªë “p·—ëÐî_€>=€hYóDŸi7š&gfâ8©:Ab´»-=ÞØô4ÝKÕcšºv¯·|«0ð­U^Î>N>A›êþzÇ7ŸÕã)ë/OÊŨ—AM= Êò/Hã¹Ý~UÕ‡‹ž­LÚµ6um·]Ì^³S»ÙzB;ÝÿôÄ¿ MÝáE§Üø;œ1Ú™0Ú]mù§ò:óú©kËsØß ›'þœ¶Y¹Ê§R'#Þ4Þ›£qu;ÐLOâx¹½ûÕPþ5õçÛË3·ÊÓ닳í€l¥ìî]{Ÿï6-AšñOÑðœZÆœ7FæQ¼m"™ì Ú4¿v#hÓÓaèòAÉP‰»¥ª>,ã8Þ¾m¢Ö‰Ój/×~ëÔ÷šÏqï;”³uv÷I‰ŸU®xûüêƒåô7H·b»û@êɹ«ôÇÇ^XzíêäÚKb߈WË>3ê·®žI£s{ÛLþŸN><³™¼Ïd¼|0Ðí99ŒTß> ò¹ø·ÂºB²K––q °=9=ò=r ì!ä­­~X¿-q¨Ø>ß°°]×L +4«ŸFwn{ɶnÉŠ§—^—^WGŸÜçsZ®,|R¬43lùÔÎ'5~¾ »Þ\WÔ Ö£ O6(.X/…ÇÔYº}Ϭ—üû¦ëši>©ñp³ÊŪ—ZååäÿcH½ª ·9_ ÚxùÓºu̓wY§¬\«gh›žgø–á ÆÏ²Y«¬VºuíÏÕ«¯¦œ‹zhzžIòÏúžwêzøúÇ/!ãíì‡ÕvS{updÛgFýÖÕ“Â(¼cëí›éùçÒŸ?#ÏW1ŠKgaº‰Z·"ò©þó?ÿ·›þWÚ=ÂnA"ÈÇ6Bmê÷ÖAj(¡ ævûõÂ/ù ‚ _‘ß¿ÿúõ«ÖOþɯdìÂëÜ8ƒ ±Ûáûé!ÈÕ•¼¯‚eAFÞ£X½8 Ûù[ ¹ä¹(‡rÈŸƒ¼bë¡…»d‚ Hã!Mæõ„ ‚ ‚ È_ÎY^O‚ ‚ ‚ uÁ‚ ‚ ‚ šQÄ­ûä…@ÿ(žËõÐõä@…•–Xá?dDr\ÞãûÅ~Åø9ñÔ­GVøÂÕûÅËà OUä2Oɧ ñGl§õÎJ—¥ÿä¹\mꙓÁòÏÝX_EÈB¤¿~xöU—3¨þ³\»½“§Ò¥ñÂnèÜfThð•Låµ9Õ“Ä.¥ÍV´‰;Üã‹Î‚ ug‚ u;ÑæåäÁT±Ûÿuu¼>äšÇñA/~—Æs[-UYÍ1si¼°Go?fÛ˜ ¨²Q‹=Ý%·¡Sãáô²ƒ‰ –ª,cAÏí ¿äÕ@¨ùgÉáƒgüòÆd<’šC…;OëÄS·©áɦ·1”ç-ù÷bü~˜¹uC Žæ¶=Ù&´úe’hõÎJ—£ÿ䇫µo»’ \靈†Gb·EÐëK³ ±ßƒ(ú}ûK¹ÐkhñQ3ûóÅJlš6õLwŸ`RA5ÏQg8–V8‹@©K“s‚ †`©?f»“Þ½ŽqòFÒM»á.™«o›¨Õ¾9îÿ–k:]‘“僒Þ6Q¥W5ïØ1àð½Ì…#‡3Æ9ðµ^=1uyXŒ%¡T¥#Ô[9:ÿÎÓ£x8õHÍ'=¼Øïµü×Y@ÆU³W¿ÕëK‚¤õ4ÛÆ1yÇ:’‡yQT”³ÞéVÑÿ«"Hã¹kvœ§3ÆI›MØë‹b¿½yÝPÛQÚ^Ro±ì/¹å½È*z­°Úã©þ—S†–nç}{8éJãÅb,I¹ æë'<¿ç;qQä¬M½é˜ä%•v"êZÞM‚4^älÂÑÿ^¼\«'J-KžBÞ®X=?¾^U×__Cj÷QKþí*‚ È÷œQËëÚz dÓu3cÇÛ织Ó&2bÛÀ½WIÄñòʼnds>–âÛÎËÑÏi<”“Ñ+žjFoœ¾ÐKQEQ¬P·'ƒ¬È·k5ùîËæ|$¥[u9œ9øËäÀŸN¸fÇ1’¬VÙÄM»ýª*Š¢Z¡nOH§H•«¼…Áóh(û¯üêyñê‘U_ôð7í‚KÕû>Ánå4HêyÕjß”äóTnÌzg¤[®ÿïûèj-\Ц.ñ&:o§ëmöú¢ØoïßÞ’)µ½¤rî UQÃý~ÐLOXñSõû|§(ŠåCää{•Åþ–nÛ½¡ªªá€þHL GߨᯠSβÞ~U-ŸHÛpÒ©*…áTS«âìÌ‹?l‘þTž‡½>EQÅp:Yyóõ¢Z~%寫'«u¨ß™©ºò¯`¯A¾óŒâ¦Ý* ʼnåU 'ßRÕÄô–  [z{ZüÃÈuû|§Z¡n»®kBqpCÖrÜãa=ÒqÖ~³téüh]|õâDòm–ÏlcdµöϯåìœÈ~p+G5—œ3¯X%» ,9”–W´û ²O¾9õE¯÷ÕÚùv¬_Žóžéûwq´ð<×îmŒdÄÅÏç©ÜØ«¯Ìt9úŸVóU^«—Æ 2¢z8çp™)­Öaïþ¾½+šˆÓö’Ê-ñ° v!tºœeZŽüYñ7Ð&Ùqš`B}; ad3¿^"çÈyYÁû>‚t¢ÊöæÅ}<©ßkùë+:Ÿ îõV´9hÑ©\Ôƒ.i×ÚÔµÝvqÀ×ìÔn6‚ޅоh{a9ñw®7FkÔ.(òOåuæõS×–3$Ç·Ìa_|ƒlžøsÚfå*ŸJÌ ÒxoŽf‡q;”ϬèI/סw?°ÊÿBúƒ òµø§hÈÏï–@&>¦dú- 6çÕA;é Òh(“õ*f<§áý5Y[´iõ-ãx¹ö[4ßîCÎA›dëpWà(ÿT9°ÂÇñömµNœ}+2˜˜äè[ܸÑðG²éñPë‘“ÏÒz´©k‚õsE†Ñ¯9½ÜëµüÅ3¹Uªß\ºåúO2]h°¸|P §cº‹Ýc V¯½Äñv&na‚4ž—·/zü|ýßGéø†é6k×ä#ìÒêÅýñ±–âº:ÁÛ&’3¿£ÁÄLû‘ÃwA›f{¬ú­«'GÒèÜÞ6“ÿçÓAàßf?Ë¿Ÿ`º®™:N¬~ݹì÷\8Ⱥl²?Nþ°Œã£{Ê£ÜSÔxXá3+¯6šK9Hü^² ŸxÄñò§u뚯•ΰ¡˜³zþë}€íó ÛuÍôO¥+…¯ƒÔ­‚*Þ@Y?Ò6jO¨hñpꑚONøÂ£jîžÖ¼^U»¦“*7V½SÓM½¡ŽõÿÃyêUuá[•´—Õ‹3´MÏ3|Ëp† ãçè0{rzÇU\+ÝíâºÕWSÎE»azž™¶ú÷Ü8^×Ã×?~©kog?¬¶›¶—C?²%uëé‘cX`Kôª®žf5[‡hßLÏ?“þ ‚| ÂþçÿvÓÿJû¡Gx½ZùØF¨MÝáþ‚'(¤¶ Òhn·_Ï;Ä ‚| ~ÿþýëׯZ?9x=Åñv^çÆA؈ÝΩŸ‚|¨Ž+Ü߆ ‚ e3 H\iíü­…‚\òü–C9äÏAÞ±õÐÂ]2A¤ñ&ózBAAä/ç,¯'AAAºàŒAAA Í(âÖýÉÎQÚÔ[Œ%Ai¼¸|Þ²øÏ _7žò„®SÞ¿xˆ{žçy_c¸ºn' kù Ô×—”ê¥í’ÔwàÇÚá¬/lw¨?Ÿ_?‘/Dá=ŠÁÄìä= sŒ‰{¸aŸ\æ-HãÜ3 éßÒS}…+ù?ä¨??Ÿ i¼Èç4?úÇË-³w£¹­·Ž_ÒÈß^_å½ Fäý©Z¡E9Ô• h[-õnI/ÔÑ%ñ,9°òS&Ïã"S¿“燵©gNËÏzc=©ÄNšgê#å—I夾š jÏy úÏŽÁ €:R!íè³ÉäíÃìçuÁv‡úóØä;QrŽbù $o›ˆòZ… h·rDî¾Lþý4ÛÆqogON$Ç’ S >Bîù-×ì³›½NœšÀtv‘ENÍOé\™?+Ÿ¥áOòYc´Ç‘«¼¬|––7 /Òh–úc¶;™ ÜëàÔ·®ñTØúäè+žä^üuUùÔƒØïµü×Yä]ÞW¿Õëo:–¨ùá¤Ë’Gž1”Æs×ì8Og¬nn6a¯/ˆýöæuS¦Wã…7Õ²¿$z{´û_q÷¼ºÝ ¾¦ -ÝÎ~ÁOW/cIÊ50¾~rÂó‹ÐÀ™dõâDÐ銼t…¼ÃJ™½âË¿º<nÚ-wÖ1^¤²kTïÍäÇÛ·MTª?E¹‘ÊàV†pe[í€-HãÅíZUÒ¹ò|ôþc¶ãdmØPÆ:ÚÔ3ÁR”e²~<<üI6ídÙ[›ºödWœsòõ5ÔøYùd…çä³ÎJ>Cn³¼¬|²R`„ß>ß=€@›álö÷žwp3"”/¯6õê¬åô‡ upŒYp3)—C¯ ·×kº®™¬Ôü\Ý´ ®qïû¨Õ¾`É™†<P寑ç!'×›NhS×”³•ªÆ¼­ÃǾøíýÛôZùI;:†ú°¥ñ¾<³õ$ï ˜¯/ŽžSíÙª¬»«ÞÒm;r uˆ£l£çÔð`©OÓ%ÿÊö ¤ñÂ.³W\%©*Ï8^®}Ó4Ý)Е*q·-ån éq-R¿×Ê–ÜYú#޳ì±ú…£z?µÃ+`ÊŸcŸKõí¨¾°Ý}d»û~úóùí ò]÷(nÚ-æÂÓ`bž.ý ‚vô8W´G ÏsíÞÆ0œè LYß³Zûù±bt²tÊßg8ŒVk?s3óÃÙ £æ“¾ú~ÈQK¦¬a0äÆ*/Kž,ê†héíµ¢(Š¢¨V¨ÛMŽÊ»zq"ù¶â‘â¼þpâÜë@]J¯£WÛç;EQTÉÀ·TUQEyXÆ1¬Ö>È·ƒtÐóX<{CÍ6#?lyRäÆýžýî*¯ÕKã±ìgM'ÈLlµ{÷÷íý[PI¯|+éòƒ].gM”Y_5íFC2·é`B‰~RÃ_‰Á½ÞŠ6iŸ¦›Ûs€€lº‰MìR]y.ÕòeÓ¥-4Þ$Ó‰õ!™q][­LgxvïÄvñëb‡ò/I·Lß°Ýý‘v÷ õçëØä[íQpΤfãSŽãx\ 壓éÌ÷y ‚4Ê­õÎE{HNn¤‹Ï…xéŸK9ÍO1Ý´€ìø©ùd†¯ÏA°ùÚÔ³ç`µ%†Ü˜“"j>Ùé²Âóì}fAˆ›¿Ý´dÛÍ5ˆº"À–™.]Dz<ÂÍÔì8ÆÏ€³¢RE¯XÄñò§ÑÛ®g@äXNd¶ù -¬ü°Ó¥É-`|O—iâíóu7q]Éz@T]?™„»m¼\‡æpÿôÉ¿IjÙf„É™«˜“$¦žSÃów‡Òñ‡íéÅgdc&ÕRX=ͧÖnEû÷ÓM7¨'žfò$¹ §‡½E×8¬¡š:DÎSsÇüfrËHãE––~nŸï XØda>»¡a— `Ï´©òßrÛ]ßâÇv÷Qíî{êÏ×±'È÷™Q£Ö3A›¸óîÉÙMÖ‚ Ýë-ßÊ €Þ÷ÈaºFb¿×Š6ïÉz0¤·Híšuòc+ê<¡ÛØWÐã“üœÚæÇÏÏgi~òß«¤»Zû¦Ù¾rã´XV>©éV,×Ñž˜¶Á.„hC;ÝË)/Eñh÷rÑ Ë¶«û–ú³‘^чA¹| ÚT÷ל‘ôà–‘Ÿ•ÈN—&7Þ÷¬ÿn¿ªêÃEÏÌ%íZ›º¶Û.v¨Uô“º2Mz¤]í†zUoFtŽÝh[ÏŒ¹¡æõ&5üÓ#ú¥–ýl&Ït–Þ닳í6]úÜÝ»ö𠰙ܵ÷¶‰ôö @ÀÕÒÈjš7åaɪwæ*?Cþg¶‹ÓúÂv‡úÓX¾–=A¾:ÿŠ2ÝgÝX'ކG[añöùÕy8’R––ÿú¼ó>ô‚ MÒõr ª¥ß{€o›HŽÄ4tç”ÁÄ”“øéùáÚ jüô|2ÃWË'Ójeð×Ë8fËU^F>™Ô ÇËu.?dçç- ß[úý Örã©þ°âY>(ªåCäªú°ŒëêUåuP°~®øhj~ØúÌ‘å;{±ê’ÄËÕp:¦Ûø¦y±Ûatuõj» ·3AÏ›ê-ÓndÝb¯Ÿwö¨›n=ÿ3äìObˆ^1ìKuåy¬ý^«¨ÏñòÁp@·çäjýz? ±ßK[S¹~æOâV¬÷ƒfÉ¿®]bÔ¶»?·џ/jO/¿GÁYõ'Ú¬Öi A?žlÀê§ÑÛIðt70Ž—?­[׋(^âž]¿H•«¼y2×á<$‡ó’ú-ä'çŠC¾{I†Ž³Z]jÅÓ@¯XµPi]åPWž|¹~ÿR¯ª gl ÿÕ‹3´MÏ3|Ëp† ã§Ú¤[$m²¨*µÒ­«çŠ8ÞÎ~Xm7ñëÈéÓ.±äPKž'.}N"L7ÇêÊ¿;yÈ]hï¬Jëf‡Yò¯kŸëõ#Øî®Áߣ?ŸÍžà(ü;!üçþo7ý¯´<ÂÓ“¤Pæ‚ !ÿB®£LÌNî‘„tšAn_æÝÐ_Þ«å¯pο#!h÷ðB@viqþ}€ü÷ä'¶Zê]î%HV<Ü,Qâ9Êj+?ùtÉÎü»w¥ñâè&÷Ò¬2ÊK—ç%"%ò©ªÔð,9°ê‘#·Ó| ‚t†Š—«ð„Åy¼&ùçâ!Ï»jSÏœ –ŸõfnR)Tæ”GЯ©o µ Êýr.Þsg™Ü NNåyú´ÎW8‚ rÑE®“èv¢ÍKPè9´©kvÇýÜž²·1”ç-ù÷büNz ÁÄKU–± çöá®åƒ²<Œfìé.÷’¼Øí€ÿZ¼‡ZpÂSåÀ©G¦Ühù¬­¢Œ|jSÏì8†: @Ím{²kÁ¤‚cϱEAÿ”ôC°Ô³寉¿†ç¹ÅWâ³ï‡­v±ßkù¯³È»Œ¯~ö ûòAI¦ ÁÛ&¢¼†Ç˵nÉ+¥ñTdp¯ƒc<°WI‹ù¹i· Ü%s°àmµÚ7Mkâ ·R¶<Ä_=]¾>œþ‚õHÄ”úÐ,Æ’ÀÑOV>A»•#çi¶cò.i$³¨XúÏJW=ê༬ÿÄPÏ]³ã<±«°Ù„½¾ öÛ›× W¯Rï¯ì/‰޼Â*zã°ôSÈýxÚŸ)S†–ng¿à§+‹±$å*ŒgǸáùEhà ´zq¢ÔÒ°ÒòŽbŒvé'_þÕåyd÷Xm¼HeרÞA¯Å¿ÀcÄñöùîZi‚¥(KH^ü¥|צ^²¦{Ónåü¾'#ïâ½—¤ñPö_Pt}1“ÞOÊ{;V<+ÜÊÑfïy7—£AX>?q¼|q†¶9¿ÿ˜ƒ‰­ƒc4[P”Ƌ۵ª<¤k“óÑûä ù¬$Ïòø·q\+Ý%[êêOΩGÙ´“ímêÚ“úsÇTýdæsp+C¸ÒA¹­·ˆ YúÏJWÆ:8Æ,¸™PJò¾®ÖÂmêšr¶òݘ·uøØß ½{ƒ^¢pTý 9 C}ØÆÒxaßžÙ{;œvÍÒÛdOÉPóÍsû|§<×özjé¶9†: ÄQ¶-F·cìð`©OÓ%ÿÊö ¤ñÂNåÃÑO¶’T•g/×¾išîèJ•øD…–r·œ; ‚ü üSÃñ‡pY‡¡ÝÊÑé›\GßW/N$ßj‚«µòí ílõÖIœƒ‰y%Y2smœ2Lß>ß)Š¢N¾¥ªŠ¢(ÊÃÑþ4žS¸ñ´ôöZQEQT+ÔíI¶ìwškÜ­ü IDAT•j…ºíº® –ZesXðË-ÔmŸï²¾yµöKòYAž§E>¿vº—!ÊS§áùù<Œ¡WÉ®S?9Dûwq´ð<×îm ÉxúÏJ’½/îÁu^«—Æ 2R9L×ÚAì÷ZÑæ­ÿ ·rq€*Û®žY Òxn·_Uõá¢AI»Ö¦®í¶‹Ùfõ• w!´/ª?¬‘.'þNÉ\ð êê wÌ 5¯ŸªáŸvìîxs4訟çÈ3]ÝèõÅÙvK–dŒ»{מƒñAþ`‚ ÈgàŸ£Tµ÷mÉÑHºùl 1Ž—k¿¥ßx£mêš`%¾îäç¼á£ H£¡œ­Ïq¢=ÿFKrêZŽˆ‡†8Êy¯ƒÓü»0ï­RîùO28üR´ÉñI_¨.O¬ø›§{ÉQãA•õ!7#-e01eÿõyWÑÏ£iÒkN÷zË}ÞÆ,ýg¥»|P2TˇÈ1Ô⨑>¼Ð¼bù NÇt? v;Œh=ý‰ãí.LÜõi-ÿäߣX½8 Ûä©$ ‚\ò¬˜­C­»>AA>׿?ÿó»é¡ AAiäõ„»‚ ‚ ‚4å‚ ‚ ‚ —™QÄ­ûÃÏQÒxá%T¿5_ЦÞbœ¿(½Y<_S9|×|~¿úÍ58Ü+üKÛ/Ú‡¯*XA/>®¯üøzÄöˆúƒv©Eá=ŠÁÄìä.û?¾wèWŸ¶“ÑÜÖ[Ç/žPÈE"ÚÄÖCK½Ë½ù%ÚÄÍ^ˆª<]G‡˜þ]‚ü¥ì,¤ñâèfvþOꆿ ñ:<éP¼\?ÿ§|~¨õʼn§,9säC­_nxJ¹Xáë–‹ž­ç”vAžUÖ¦ž9,?ß‹ùÆÒ)ÈðÜ×$Ïl¿|Õªþ@õgaŒ ·á£:—åTžI¿ª½ hw¸ÿ|/d˳BçõEôÛ#êÏ÷·3ÈמQ‚ÔíD›—ÃÛ§ñöùNy. • üíj×ì8Žúɲãê,q4·íÉî0»ð_WG˜ÛHã…ý8z+mÌ'ñp[=þ僲hSo:&ùLµb‘Ù–}»¬<ãxû¶‰Jõª(Ï‚G ;Ÿb®Z„êõ’ÿ^E‹öÛㇶÇo§?_ÅÎ _Œ¡â k4”ýׇtD’¬¹j~ŒÇÛ绀“룷2„ëà°xÜh߀Ô?¸ ˜®k&3çŸËxùâ ms>~ÿ1 [Ç hmê™`)Jºî;Lš#ž˜a˜ñçaCÙýd-öv­*iæ£÷‹n€æË¥M½l-¤Ê÷LÜ TŽ÷=™A‚v+G›ý½çÜtž·1«¾èñl«•‹šÏ#9S©[¿«\5å³eè9Sž ¹Å[F»ÈÇp½é„6uM9[‘jÌÛ:|ì‹oÐÞ¿½A/µ]º”Í¡c¨ÛX/ìûÁ3{mS¿¬vG­²¥Zw÷¼¥Ûväê,Gsûqüöãy³Ú+üXêÓtÉ¿È^Y"ÆT>õìCÏKä¹Z‡î½6û¹*V%]d½m©Ö­kšCÇ0ö6ÑŽœ/Ü ©ßkeKî,½Gzh)wËŠödÓKU–± M]{²S®€Y/œ~¤TêÛãG¶Çï§?_ÆÎ _vâ¦ÝÊî‡h÷ÅG¸·rTkÉ3Ú¿‹£…ç¹vocN”j’¢¨†o©ª¢(Šò@†‰Ûç;Õ uÛu],5uv?Þ'9h$#Ôøósn×ÖÁyÉ"Ù>ße²ÕÚ?jµÖØNÕkõâDò­–­y—}¯°Ú¹öA¾$–qô˜?ÓÒÛkEQEQ­P·'Éò­¾¸ñTÚ×:]ã9’3U>üúeÈŸQ.jxv¹èzΑUn•:Ò«¼V/Ä‚Ÿ5 3¨Õ:ìÝß·÷oA¡±Ú…o%]{° ¡Óå4 ¾ Æ_ÛþðLSêìBšÞfíŽþJ îõV´9Hû4ÝÜ^d³L„Úö¡¡3Qæ0ñ?¢Õ'zy¹õE•3Kž¼ÎïTž¬rmYñÓËÅÒsžö œZ/[®|èzȨlÕ¿§þ|);ƒ|©Eâ­¤Ìmâλ'þ!‚ Ýë-ß:èt8¶ÿˆ÷}r˜®‘€Øïµ¢Í;Ïl åÈ1È(‡ŒÞ†ci5;í`_:VË.çÇTðì¼lâèQ‡ô$7kwoµöM3ñ–¡¦Ë »¢ EòŒïS¬tóßmªûëe Ðü–ØõEª•—U_¬sÉyyV܉Iä ŒrU«¯¢|èzN_SÏ‹ýtûUU.z6.i×ÚÔµÝv±ã¬R_œIà.„vº?SÚ.Ιqâ¯aj”éí®¶üSyÕÚЯáŸvìvx<©n'›É3Ž—ëлXUêë|97“ç!µ·M¤oO^>IË «lÞ”‡%0ís½z9³½œÖ#¶ÇhßU¾A¾ÿ $…­-rJ©¥ßWÜ¡Ž·Ï¯>ÈÃQv¸å¿ò¬C° ó^ Ù9êàmÉdŸ]Цv‰Ó ÔŽÿÈzåt¾~!Ú„vÂ) ŸŽ°+Yç4|/×~K¿P:lÚ÷sä hS×ëç*ÿP/d'ê-¨T_ùx¸6¸$ŸE9_Pž”rU‰¿(Ÿr=/„¯«çÌE©K/TÃé˜nãåÅn‡Ñ1”·‹£Î.LÜÅiÒ6( »{¹9óï ˜®k¦«ŸFwn'û~e÷ý§^%ɾ$ñì[Æ1ÀvöäôÈ÷È1,°­Á°â?º§9JâxùÓºu̓—Kg˜ŽJiáy£^Fx"/Ù†<ˆˆúÜ#TK…GÔCmê%çrê/V<¬‹šO–..Ï£rq³ÊU®çyRåVlÓÈS¯ª [dµ «gh›žgø–á ÆÏªˆÎ« Vºœöø©ˆãíì‡ÕvS;vh¿LûÀ’C]yF[‡hßL®/çƒwÇA>•ì9DŽñcUšÏ‚÷ÈÃ’[/õäS×Îc{¼þ|j;ƒ|„ÿLþo7ù¯´=<ÂÓ<7ƒ Úµ©;Üç“Aùêüþýûׯ_µ~òO~&º ¯sã ‚ lÄnçÔï AAä«Pxánõâ€n½Ì‚ È• ÏýØÅ«™AA¾Ø&ózBAAä/ç,¯'AAAºàŒAAA Í(âÖç(¾‚ ¨ÓKŃDzhp(U¤ñÂKhüzBѱÅ/zGA®Já=ŠÁÄìœ<Þ²\¸A¿põr…˃9áóÊî'–Æ‹ÂcóNá…Ëä-úÜû üð´üh÷ð¢@¸n¹ê¦Ë _7–ÜêOèr`Éùú“ŸC~ÈÅ×ü»ŒYrË¿ÿpT®ÂSÊEÕŽük}'ϸjSÏœ –ŸõnR)‚®‚c\øšiAÐ&¶ZêÝò¬h›=ˆûIfþ…ûì?öÑ’êòLÌBjA›¸Ã½×Ž#‚àŒ"íϺhóRx[XЦ®Ùqô㞯·1”ç-ù÷büÎéÂ9áIçZªr2Œ`’É[ô¡ï·:•ÂSLL°Te Òxn'/Õ-Wƒt9ákÅÑ[-¨ràËùÊ#ªÃœV/ìÇÑ[Ù Ÿ*·åƒ²<Œ†íéN}XÆÉt¢ãê,q4·íÉŽ?i¤ê?Gþu¿Vkßv%A¸Þ´í¬aŸØíDôúÒ,@ì÷ Š®E±Ûÿõ¯¿òÊ·rºêNàL*¨Í.ê ÇÒ g‚ ្Þ,õÇlwÔ÷÷{-ÿuyOñÕÏ?Ÿžøqä}9Øá÷:8ÆCa±8zÔÁyYŸUì僒$¼m¢ôŽŠåúX!'Ð̤ÜÄÔ§d1–žJäL‰‡Kn=›v Â]2§ Þ6Q«}ÕC)q¼\ûÐéŠdT}+GÎÿÏÞû„¸îeyžG?Þ&û—ÓEg'…ËvVÇtY9T6]+aCI½hkÛ©…ãA@Öt¢0½LÄêMÛ êå!ª¡! L©jk7TCH 6ZÍЋ®šÈþY.Qd×ðúå¯s©Y\I–l]Y²ñâ½÷ý¿§¸¾Î=W:çÞsï}w;CvŸh õ’&lׇ«ÿ|ùW}þB*$öï£a¿;Àþ›Íüöi‹¨uZŸ=Ìrû7%·þȪÉ_"=Ùˆš+“§'ùúÀb¦ ‰jš•ü¢¸\±?õE1¥ÐÅz[¾¸ {qMîì Ò\n¹B:P,+Ÿ¤þÉš_±üËË“ˆˆü‡?÷ò¢}P‡î°ÏêkÅ(gÜ!4 >W" ç7g—93d'õZúŸO«Äòû£ÎT‘eY–eÓ“Œû Qxé™…7£s×-k¡ bÿZ#ûÝíâhHY±üv©C× 3j—¯Yƒî‹¹ŽÑ°õH¤ÑÂNu¹‘dXõE–eÅô5k°¸Ÿ±æ ä¼+ŸMó"Wn¹z†ã;;Œû¾(°²ï˜úåÊ¡¤1Ý“"/‘º)nb¹AQsꓯÿ<ùW}žÑ´gU!MtX ÐãÔoŸ¶Z§õÕãÊ-Ö“ÞJWdYÖmÒÎ‹Æ ³\K«‘d8NÚå埫ó›3Y–M[gÏåd5ͲÚ3]QÝ&íšuMÁxÏMÿl—»^S•eY–u»‘È']Åôö{Ïìçdêkçß«’VPLi…nÇ/Öõ^ð¬ÅI½–˜”ųgS¤N7š÷º¸^‡þ2ljWz¢šVŸÆ_¾Œ…ºžKÍ]uÏ5âLµæ¦ßIw`HA|½§žÑÜv|ÙäΤNq=‹-ƒÜôùííHA~{¹rã‘DSÐ$™µçÈ¡PÎÅùl[Þ¹rË×¢ùÍ™búšå8ŽA)#˜/®<Ùܧ“rKˆˆ‚ÕSëb于՞éºêmq7æË¿êó$¿g¹­^옥xÐòó´'S¿}~^_=¦^róÌ(Âj±ô©Ñ,ÌrUt; ÏT”Èt Âü ô¡2ÉvšÅÒ/1Þ·Ó?Ýs­ÌÖÒÞ.7µ¦JD ¶¨ÚÚ¬©²ö‘çäÎnô.Z]™¯}7¡§U@q}ÍÖ9ø¤xCü½§<Âp|¥7ï-Ç5ˆ(°M;0ê‘Mý&’UqúèS“|‚Œ^³E4§ùÍ™|³6 ­{ÒßÞ.ØÎñ«E¾Q²•~†Ùúl6Pì )°õ(RŸ[ÏV³A5ÉrR±ôAA= Êå¥Ïo/µš ò§‹|;(OnÅí-°;ÓrÔ!OÎ\ç!·\¾Ürõ$š³”Û³Üxß-ÿX^•ŽŸÚérl{q¾'ÆÚn§ÝžÞò ÃñÔwÏ»f™þz>9x |•ý@Uÿê«CÇ 326×±ø‚ ’ù9Nz¶kVêE±¶­‹ž”^åO¾R=‰¼é8 Ç—r‚ÂÂ{•ͯo:}qµ‹OÀܪg­8¼d¹;Ó§Ÿ‡áüqÔ¶‚•ËÈ­€îÀ¼Öð\9”‘óF>\ƒ#_n=Y,ýtôOœŠ'‡ òô—ó0 ç7)¹uϵZTÿÝz[FþUŸgä‘c ɯ_*ºÝ0œ½oxh5´ªÜæK? +Äþý®ô¼ü‹õái¤TØ£Ü2ãýU°xœRwÔ‘^¥ž ê0Y3àÉ¡ª<3îúÝètvŒëO]ÎJð¦øÏésý Ç1â‰Ì¡þJ*jÈì8Æ:j¨ÑÛÊ'•žˆ&WzóÞŠ¦ÃãsÙƒ­+2r¬Éj飯¶ž‡/jWRO7 Gˆ®ª8V= ò™ßœé4²LjÿÄf:såVÜ•™hŠä yŽ*åS@®ÜxzGEñ!¬aI´Ø¶ögFnqýKémVÿyò¯úüeˆ£ª2§![¾Ü¸úpg÷,Ãu "ÏÔíÞõžùóÆ-nßÙíÍ.®T.Oo_Û»; ç·oͺ—µ^Í™ \(°u“¬ÞùW•gÆ«iX«ýôáS3€2?üérðDZvMï° ÀËBuèôV/ìfäòáÇ÷ïßWúÉ:ê) çKÿyNœði5ɾ[€OŽÌ}“;›4‹ÑðܰkÅ,*õ ðºLš$ê ð…sPÔTàH…Àº±ä!b´¿nêÐõEásS­CÚ•pq$ˆý‘±÷í =Z74s-ࡊ÷Ò§òø€þà; > 2÷QtF#ïRAìß[Z-{Ž~æê†øyúüþäPóèçéS?‰oXß„P^ìÒY‘¾‚ Ä¡æ¹õd>ÕÀY7«Ì5·¹ràÁ«?ïC“CÕöVMÿb_Êm½JW5-žœ·õgïþÊ­Ïß§òõa×*«C×tǯõ$~6(q_ä^6œR,Í7•³ñAÙVº úµQéý°ìªt±?²ê/j±¥ï‡yùûX0.>éqñùêO¶]Töö¬|ÒïOðŠ< A›`v·ÈÑûkÍ÷¼Z#k–5l]¹]PëâÞ²Kf¤Ž/åñúmk —Êå8 Ùw,ý[ŠÎÊÔ¡c4lÛ#-efñÓç~eÙÈlÏt9¾õlÔ*¹õd>™Š<±oí¾Œ'‡Ê[ <9Tmïòy¡÷é–^1›Þ79ûùäÉ9Wöë¯Üúì1?T©>ŒÉÔ3zMQžï3)êÀé­ö3wZÍFPûT¼],¨uÚ¦ x†*¶š òpäÕ‘þÍ×îÇo–r9ŽÞ]eÞ“ŸП#µ+ú69zN{³{EëâZ#ûnš±N:R`¿»‡aÎoßÙÔë‹Bö{6žzÔh¶¶-›Ž°³2A¼è‘©¼½]YBIz~OÛ5ïávÁ¾£·^­}ZrÕo£žãK™ oZ<΂ÌíQ|Š»$(#‡Jsn‘°UùŒª¶·0ýv»ãØÍ_joÅkð£2M²q’U!Ž^Q÷\#[¿Ìº<9ë϶<‹û+·>òÙnWÕú¼œó&öï£a¿;`öt6óÛ§-¢Öi}ö0Û!gEü%Ò“è‚’«ó<¹më'‹ 1$ªiV:¶§ \±?õE1¥ÐÅý^¾¸ ‡«äÈYºÃ>«K,íHÔ•¢›±?Jå·]a8œ;ûWHúd£pyã1÷½”Í'£?¹å–чì{ ãâEÇÅg§?©y¥;;ˆ-^>eê“þèUy=àh… ö¯5²ßÝfV.º‰üåb=©\£Zýdó‡=)²b³vdOò˜e†ó›³Ëb<žËI=ô´ 6«SÔÀÜz²L£FFë¦,˲,›¾f º%åP±?êL•(O2î/6¾(9Tm/?}~»’¹v=ªRzAC2¬úƒ"˲búš5(6ŽÓù+¦W¬WÌâŸÑ¹›ýâòä\ ?ùòä÷W¾žóå“Û®ÊõI÷Èó¹êÐaQ‡­J=Nýöi«uZ_=>Q,·‚vIFo¥+²,ë6içÝbåw]×±´I†ã¤?–¼üsõs~s&˲éQ`ëìy™‰ÌšfYí™®(Šn“vÍTŽ×ï¼ôÏ WÎ’VPLI[·ãÍD¡ÛåÔ*;{ÂæŽº“ãâi»æM'Åíj]\k~$Y–“)†‚~Ù~/­×feY–eÝn$ùèíN}H¿Ç0.^x\|~úS^þõ©šÿõžGqR¯%&tzÎ8oj3X=µ.F®ëX홞ùr1××±4²ï6¬+APÏ«\æ•›~í@'÷dê‘ÔéÆóÄ×Ù½…s<¹õ$¢îÀH–6ÖI&wv u"Kš/‡‚7Åö 3E£ü§Þ9Tm/'}A»º)àLi'«´4É]…*µ¾ÄÕ+ªiõiüfLy,åÌ•''ŸÜúðäSjݬd}Öí~–ÛêÅþˆ}9. Âfédê·ÏÏë«ÇE©vyfaµXúÔh̹²/™¢Ûy¦¢Ä6@X~V& Ë^,ý¾y¦ IDAT]ã"7ý‘‘Œí¹U¾>ÛwzZÏŠ Hþ¼o²n¹a±µYŽëºŽci¾™´¥h\¤e^¢_rÞK©µY"Z°F¶v•»K0.>ʸø õgë›X fk-Ú–'§>{|Ž©'¤xCü½¤‚:4¶~µàxЭ+7óPÄ‹Ô_’- êÐu‡™˜àÖEOÊÙ©Q¸@‘MŸ V‡®uOúÛÛy8¾Ò›÷–ãDئõd.<µ÷)ÓÀ‚zŠý‘!¶oÿj5T“,'DnŽxårênnÂVr+¶—›žÓ.a!6äOåÕ(¿½­ülŠôŠ‚ŒãdôŠå\®>)yæåí·ß«Š‡Šû7œß¼U–ÇqºéY‡DAÙþ*À_ÎÃñÔ7z«»Ñi¹v¡ yù BeýÜÕ4¶d½‘Óïóüô…q„ƒd¹ZÖpá‘·â¹åÌÆÚiëvA§m²ßMèèíJˆý‘;j¿÷æ7g:,Ç1ÒáÇßVOëÇ‹³óByæëGÝ0.^j\|žúÃ<%#øgã¢|TN}*~ެ'lxlß­ œûfjïf·#e d9šg*WO«€$?žk¡Öi»Ìžr?Rëý¦‚ žk5Ï,k0íL?™z†Áxf³ 5oÊ,¤2»7ê™sdÇbéS0ÛÚÕ*WeË5ê'D A¼¸Ö(>eKP‡No‡ª¶77½Àk—Lv–“)ÓÞV³A´*Ò« EaÅ–Ó·ô3_žœ|ºç¼úpä#æ·«r}2öAýAQ.º'/×êбœzöƒ½ß®ÝÄRXúT/Ñ®Ã=¢‚ü+ég58㢲ücyx|ÊsË™¢ídFï´õHmš½+~EØ®Åã,Ðv¿÷˜†²Ù.wHòå˜ ßW9¬-ÀŒ…x <·Ç;ÆÅKŽ‹ÏLvO1ìªÏNýÌåõ|Á|µa¸m¼(^§Dg7=x$õ¢¾î¹¶w+âEOJüìx¢½ÂÒ\qú(ÿØ’ÎÎyU>°*SOAnŸ†ã©WÛŽy-#‡åFõ_ïÙu°¹ƒ¹PUÛ›JÏmW8œ5í¼xDw`ìØâ²xœRï¢ ¶¶C¯Âñ4%O¶2ó¸ØCÎùòäåSXŸùðÚUµ>üɰ£‹ãKE·†³÷æàV³Áù íj×–^-ý(üNû÷»Òsû±P?ŸVÁÆAUËåöûG£šœ÷crg“v}Ýöó6•“Öi;ÖöÝíJïÄ-Ù/ë÷RjœFÏ£¨’ŠòäŒwŒ‹Âg£?Õ=:N}8ùóú·ªžPž7{~~®ôæ½MëÆ«Šç+©«-±·à¾Âp#ÀÈM_æ’eÇ2/Ÿäí ­ç«£ú°öºÑòäºÉ¹r¨ZnŽ¯ÌŽc¬£’=*+·í-HÏkWfÕ8{âmf•¶°½ì<¥¶e¹Q`ë&Y½*z• âÉ™£?\yVê/ž| ÚUµ>/CUµû4äªÛUÛ5¹³{–ẑgêvïzÏü ôsÁú&ÛÅ•Ê-…ªrξg ×5XýyÏÓs¿šÿðL‡W®ßëqß®¼û&;ûeû½†óÛ·f݉âjv–{¬÷ÆôçÀï 7ŸüúpóçõoU= ìœõºüq<¯éÝ[ì×àE¡:tz«ÏãÚ&ð©*¡ ^Ü[õ‡Ã6ñø,øðáÃû÷ï+ýä«´¼ôŸçÄŸV³‘Žà#(!»7 Ø‹Ì}“;›4kãFÀ3Á®²`Ê»÷ÀÒ|«dö6i’¨'ÀÎAQOPx€#y ëæì£Ô¡;ê—9¨X`‘¹®ëºîÞ§à1ŸOAû#ìiáÊá3OjÀaçÒ—>ÞŸcÔl¼±×ª€#’¹¢;0©KÊX÷–VKÝÀÜ’¥ù¦rvØ)„ÇÊ§à‹›œZÍÎcŽš“N7MÔ³¾ÑaãšÏ\9ä"öGé’¬Šó?Fc×ù³V±3‚yÏiã ‹l»Ê·—׮̑áÏpèuú^ˆCú«@q¢KÖÙVÑv}¬:tAwüZOþf•oÄÕιTþX¥c¼xAõÇ…÷~8ì"a±?²êøö8&_¥-ìf#º²¬E~­ùž·ù‡V³AÞôð£kŽ•Ïr¬úƒÂnJfŸíp~s¶¾=Y6=JÎôì 2Y–Ý&íº/ »åÀ!°õ¤ˆÄ\(ÈÿH¾“шËÕmÒ®/DAà=Ì膭+Š¢è6iÖ »_{sÛÅlñöL—eYQt»a}B:¹[QL_³Òë•êÏ“ƒ çšf™LöÓ"šL=j4ŸõšR!^aÜs$µO™Îœ¶)ž¡ŠÏ9Þ?%«K,íHÔ•¢›±?JGð(Ô¡kP4g¦˜^ú#t­‘ýî6½¢Á>cŽ¥ÕH2gããç [W6æÞÄþ¨3Uâ9]ɸ¿£øßü|Òõ1ýõœ1ïyµ7£s×åZ¢­‹žä=äY`+wB:,šè°ø™Ç©ß>mµNë«Ç'Šõ!_nQ¿ôVº­}égÁxç埫'l‰ÈôRË€%Éjšeµgz´ -Œ¯ÜôÏ WÎ’VPLI[·ãÄD¡ÛåÔ*ëå2ÿ¸!m¾(â¤^˘:ó¬iºçmMy²Ï˜¢Ûy¦¢DŸtõÑ‘‚¼)Rfš±ÿžL½â|6ê3¹³©£ ïùC¢>=¥MKTÔsÎ¥cÝ‘I®vZ0k ù‘g*‘ˆ&Ñlûíßœ)¦¯YŽã”2.yÏ)X=µ.F®ëX홾¶LöhoN»&S¤N—‰Z¼¸^GŽóô$WÅÏÙœ­“¸I…õÏÕÃ9ì0âKëOJ-žeÝBì˜å}yÈææN¦~ûü¼¾JÇEråFä™ÑF‹ÅÒ/Žé*xoðò/ГÊ$ÛcK?ï½·9¾¶ÒÉH/FìÒOûnBO«€b¯9½W’ô:¡ ˆ§í‚ÏTå åí1h5äO·¦£uh4lýªÂŽ‹üŒ¶7³«¢ŒZÍÕ$ËÑR_Ôf‹ˆ8Ïçù§˜2ë&SÏè±ôQ9=)˜Ý-rì3C l=Úž[ ^¹lgdbòZ÷”Þ컑e9ðÛ«]CòLE™Pwà8n'Ú Í{N5Ͳ[Wnæ¡ ˆ»ú/çœv…áøJoÞ[ŽkQ`›v`Ô‹õdÆ—ò8q-†¦|9.]ÿ”æÉa'%õgmÒÍoÞ*Ëã¸å6‹—w§íQPµ¿rð—óp<õÞênAtºßøÝ£ yùWO’½.a©Œ rÇWnznåÕá:RT²\-ëóÈ;¨à¹åÌÞ§­Û¶É~‡ÀÛÃw+¨ç¾É?Â¥Õl­ˆ¨Û‘²\Ér´_ÊFÚf-ž‹kâÓ¥uèô +»Xú̶j(ùÏ)Ú,{“—W­~²Q$7õ\«yæ¦a·}ÄMøåf?âÆºŽÐYTk¯ ö{R`ëÌjdÖ|¯/Nné"ÿùÓ* É瘩uÚ®³'êžïÓÞÜv¥Ó êPó¦ã0d³ÀÛzr c©)  §¿®&­|=äÈa—_V²öbýAQ.º‰6×êбœzvx–ÑÏ'méS}ñ[Ý#*ÈÿèzRf|U“,¯Ÿzn9S´ÝÈè¶©M³wØÌ  *騧­hîÅã,z-öKæÈ’ƒt¢Í­+EîDÎgAM;ߊÌYÇ” ‚:Èîüæ|öjÛ1Ù¼ç…ùÔ‹b‘ÙŒrËѺèmÇ¥êpÛ¬*‡M+¡'‘7ÇË;݉îÀ`¡ùUÛK‹¥ŸŽªI¤ÎyÎoRòéžk,®zöîl— ƒÌ«I¡žäË¡ÌóHÎþrÔ?_yr(vyýÉŒƒ`õô<#<_*ºÝ0œ½7·š ÎAÕñ;_úQ¸› öïw¥çöK¡ž<­‚ôFÿ=Ê­<¾žjrÞÓñ¾³I»¾nûGÜà‹áM±pûÎn[–«¶n’µïÜØüæL§‘å8Å笇áøÊì8Æ:ú¥±+ÿɕ޼·\׈þGðžçMW§BM±½5Áœ|ŵõüv‰@‘\/"»$“Žœü™Q*EòÉ4ªR{ã(£(î‚58µ.±ý<+Ÿ}ï‹([æ’e-í\=!¾rŸóä\$Žòä¾ïÂp#n׳êÏþNEUuÝ<æ%ûŒß;»g®ky¦n÷®÷Ì¿@Oì•í²Jåîñ>yÞî«(ç¬þ®k°úóž§æ4ÍcÀVߺüqüº¦woqÊ/:Õ¡Ó[qÕýñâÞª?¶‰ÀgÁ‡Þ¿_é'먧0œ/ý—>)Ðj6’ý¾|%d÷´`O6€½ÈÜG1¹³I³Ò§žvM›S|<Ø} –æ›X%°·I“D=¾pŠz€ªÀ£É£XX÷GÚG!¨CwÔ…×»…C`Ç®ëºîÞ§û¿|¹‚ öG¡O?V¹/¦‡‡ëCjÀaçÒG_ŸkAÕ×Þé¬×«¿ù_ÿ÷¾(2÷QtFc×áý ™+’ûÒGžsÎïOÏ3,R× ðŠÈœè/öGé_Qœn»˜»5°4ßTÎâÓ‹óá}éyõÌmWn¹› ÙUèÎ/zÒe»»FPNº':k¸dý÷s¾ñä\]n9ý“¯ßÙµÊêÐ5ÝñÇ»ñ`wK£×¹ÂåîËs¼ ú£{åßc°+ÑÅþȪ¿ Ÿ!9—6}dã¬døl= A›`vWêÂTuè [WnÔº¸·¬Á2¹A©=Óå›9ûïQÿ‰}Ç—òxmXÃeáÛì{˜.‹¢³5uè ÛöHÛüUù¯oAþ¹íŠÒµš ò&¥ò) ·žíÚ.wý¾Ö|Ï«5øØ‹ý{KóME.aÆ ‚xq¿ö9ÅþȺ¾xÜ×2(_ÿ=äÌÓ·"9We«_ŠåS Ÿ“©gôš¢ <Ÿ%êÀé­ösZÍFPûT¼],¨uÚ¦ x†*rôü‹ã£Þ¦÷ܰ3jÓîD¸1¼|šìÞGÅe¸ë…iAP;R`¿»‡!»W;z}Q Öi»æ=Ü.ØwâöÁ«µO7V¥Ãp<õ¨Ñlm翞ÛÍZB)`gk ‚xÑ#Sy{»<¦¥•ÊŸÓ®Šùìc[WoWt~ü´Â/âPˆQÒ¨î¹F¶žëÝ ëŽOBPNê5ò—‘ϹxœµúÉ^åÔ?UlN,жœ·õ“§oÅræéaqþYö“ϳ#ˆý{ÇhØïXU˜Íüöi‹¨uZŸ=Ì û+ŽvKþõûF\ɨž>lë' õ1$ªiV:æ§ \±?õE1¥ Åý^¾¸ ‡qñäɧ;²Ú–göy$«#Ê-ÑÃëìµ¹ÑYÅí-þ^x¥…:t 2eY–eÙô5kÐ%"êv¤ØpŠ#RjõfP¥xZm[T‚ØïI‘×±‘¿bzyvsOò˜%†ó›³ËñQçqÓùóÚÅ>{Ž¥ÕH2'רÍäS^» ÊÄþµFö»ÛEéR$ê?(²,+¦¯YU˜u>£swëËÍåë ëš(D'ßÙdÜ÷EŨ}·³;¶Ë-¨¿Øu¦Q¡¦'÷b¡œóõ“£oúÃÓÃÜüyý²Ÿ|¢‘ò|î„:tX4Ña@S¿}ÚjÖWODÑÀ.è/Éè­tE–eÝ&í¼[sžóòÏÕÏùÍ™,˦G­³çe&ûkšeµgº¢(ºMÚ5z•›þÈ•g®Šf.ز,˲¬ÛDž­‹kÍÚ+Ër™k³«Ê-.$jȺL·ƒ’íÝù½ð<Š“z-™`e_ Ì|ðäΤNdI«§ÖÅÈu«=Óã/Âdê‘ÔéÆ_¯ëLì;›[rRfÖÎy}APÏK_þµž˜,½]/'ÿ¼v±Ïž¢Ûy¦¢ÄßÜð¹ëYPn÷\£ŠSΞGšMÒ«D5­>¿Ðk‹¿Û‘‚¼üç7gŠék–ã8•3RsËåÕŸ™ü”hSaégž¾¨A®òò/è—ùìè÷ç¹­^ì˜ÅyyÈæ6S0™úíóóúêqQª¿<3аZ,}j4 T½Xž¹ùóôs’=9‹¥_B¯¶ÓÝuØX+(g59¤Ö‰hÁ‘­K·ñä&V{¦—×Ãíö² à¹yC{U[ÍÕ$ËIÅœ‘%ZÓ,+°uåf ‚xADDa8¾Ò›÷–ãDئëIh»:tÝ¡)_Ž©Õl?åO³·.zR¹éP\uèZ÷ľvÙM½› ÌÉ?¯]»¿Ô[ùðÊåÕ³RŸ êÐhØúUŽ\ŠÛ›o$_hÎß"a!ò:Fº†ä™Š2¡îÀqÜNó]©ÜÒõ' VEræé眣oÜþã4·0ÿ\xò)î÷p~óVYÇ Ž¹cUÄÓv(8‚žøËy8žúFou· :-×_GhB^þ‚ îxqTÂöä„a¤2‚XØï[é ô|™#Y®–u°ù¦tÉ}•åpR¯«§õ¿ãEä9ÍoÎtYŽc”¯@y¹-X³L2vì Ûõž=f·8¾GÁöÆÝ êÀ¹o¦÷n.–>³­Ýœ­’üx‰Z§íZ0{Šl£ÄtÔ¡æM·-•dêbû{A«Ô÷R=×jžYÙÀšL=Ã`J*Øõ—“ÿ·]…ßõœz–Ùm˜®g%º)ûá–,Gc–Jõ]Ž5^ [¶³ ö{R`ëÌúdÞc¯/NnæÕÊåÕÿjíÛd;›uèô åÌÑOž¾•T¤µ–Ë¿Œ|Šû]û÷VýAQ.ÑkuèXN=kÈî·6± —>ÕãuH^Å#*È¿QìÛBÅ~çÊ?–׳?UAk"ÇÃ`Áf—Ü!í³;œ÷½ˆV)&WfÇ1îûOÇ9%lã{àãòUöƒ³a‘Œ§^m;:œßù4Žj‹â7ØJìÌÎ)—g‘\™ÇXGÍ5z¥úkC?«Ë™«‡<ýçÖ?O>õy!Ó3Šªºî?ó‰‚þâ¾Oîìže¸®A䙺ݻÞ3ÿ\ýŒÌNÖ—Ù!\©Üªý~d$#)wç‰Ø¹rÈê›áº«Îoßšu'ÖÏx¼oè'¶þv²G•‘ëÃÐæ{{]ÏŠ7N|t„ þt9øãØÞ½¦wG¾¸ °cªC§·Â_x |øðáýû÷•~²Žz ÃùÒžg|ZÍÆŽ¸,€WLæ>ŠÉMšµqj!à™`דY¥x&MõøÂ9(ê ªp$B`aÝŸì> Aì܈2§ïWM¿ùsAìŽ)«ës`=uèŽú¢ðÚ»þXõ<<ŸÃû+5à°sé¹ô<Óûêµ½O ?П׬Ÿ|ödî£èŒFê°|Aìßo\#´>Â\8ÉÉþ½ë~Ù‚ô™+,Þ&WXä§Ïž=¼\Ô¥ù¦rVî®·ÜôérÓ‡Á§ï1ØyH|b”–h:+^ýã&oÞP‘Å!U*þBß[Z-Õ/Õ?BùòäÉ¡ «Ö“'7îÏóô¼ºþp럫çìZeuèƒîøÅn<Ø«q‰ý‘¥‘­ù˜éªã—dz^PýÜÛÖ½ŸÃÂÐèÏ1Ûõ —5}QzÀóñUzÜ6™;TÃùÍÙúvcÙôˆq)âŽѰuö\·I»¾(ðõ Ò«C×hغ¢( »Mµ[ž½YÚ3]–eEÑ톑™ˆj5äMË™“—¾;0ÈTdYVt›´ë¾5*¹æYQL_³™§ âvɲœyåÕGP‡ŽsM3/Çv¬úƒ’“Ïßãךïy‡d“g®vôo•zòä– OÏ«êOAýsõÔ@êõE›ë´]ónÄîO}ðjíÓãÚaãKù’Mq-gAÎía8žzÔh¦Ÿ·âµö}-¶[¹G¦òöv¹e©Ÿkdë—Õfãòë™ô×zMˆ¥¾¸ÖȾ›æÌͬ v §<³,êßòõ,[A>yzÎÓçjõçëùKMò‰ý{ÇhØï˜žÍüöi‹¨uZŸ=Ìrõ!–O‘ü%Ò·¨‰’Q<ù ©?0ÏżÕ4+ùEq¹b4ê‹bj`ëyAúâ&T  Ãùã,Ø)!è“VåÓœ÷@6ŸŒœsË-#·ìø‚þ@ÑŸÔüËÄ_`^>eê“ä_¬WåõPÖ£h]ô$ïY$a8¾³ɸï‹[k&ûŽ­BŠýQgššK¸¿›¾Û‘bÇ!Ž<©ÕOøùŸÔ3¡)O+æi°îXZ$ÃqvåÒg¼š´qÖ“"«‘!ÑZbúšµÏÜ ¯>a8¿9»Ü^ÞeêŒÎ]·‚'“[OuèdFË/¦—næµFö»ÛÅÖ·-Ioú•æÀò幕$§«Ö“'·‚|rõœ§Ï\ýáÕŸ£ç™”ÏçN¨C‡Eƒ¶õ8õÛ§­Öi}õøDqýsåë[o¥+Ñãywý/È_P‡ŽÑ°õÌ[j2½ÔòW‰@²šfYí™­EËhzž›þø½&ˆ§íZ2åΓCëâZóÍdy-™b(¨ÿö{ ½¶&˲n7’ü úw§Ü6Æôúsˆþ”—A}ªæD=àKó(Nêµ|“OÔóì%\ó›3Åô5ËqƒRÆ 3墹„©·3=«§ÖÅÈu«=Óu;(L?™z$uºñ<ôuZÉF¸¢Ûy¦¢ÄïFîtl™ôÝ‘ªŽæ6œ”ûÄðL%jòdkõ‚óܘû©Zÿ(›ú4~3–òd¶ë¹=Ÿ]ÙœÒÞH?¹³©SÒƒÚ–ç¶xý[µžåÖÙÊêù¶>sû‹Sÿ=O‰ãYn«û#öE¼<$¸œyJ“©ß>?¯¯Rq‘ÜñNDžÊ/–~qLWþóòïv¤à %—I¸ùbé—Ñó­ôÇE2×uÇÒ|3i{œik ×?ç}•Z[#¢[\kí*w—Ü ?ПãèÏÖ;¿–ŽÎÞ–'§>%óÏ”uD=à à •ØÃÚºèIÁìn‘ë•çR¤Ÿ=~8¿y«,Žãu3%›§$ Ž0.üå<O}£·º[î%Ÿ}š—¿ ˆ;:²ñž™0Œ†¸ rõ<7}ág¹!Y®–5Èvyþb䎚ÑI9ÏoÎtYŽcPê$Þ8]ð=Æ`õ´þw¼¸6/ìß|¹qºúý9D˜§dD-”ÏÆEù¨œúPµn?²žð…xa8¿9“ouàÜ7·Žàõ\«yfê±ß“[gV³¢z}qr»`1íÑiQ‚:tz…éŸVI~<E­Óv-˜=qÓßÌYMo’®æM‹-¡túòÓºÅG‘0º ð IDAT^„ÅaåN¦ža°ïÔr¹G{3ö6­ØÄLöƒ"YŽæ™ÊÕdéS0Ë“LA¹¥äË!·…Šõ,9ŸäÃÕsAÌÕç’ú¶®?GϳvOýAQ.z€I4®Õ¡c9õ¬XöÓ“ÄZúTßK>U=¢‚ükíØ,¸z^Mþ±¼ö;fñ8 ´ú Ñ¢P¬'Ù¬;$ùrÌ«?w–mf,Äûwc|A ?‡èOù;¯>;õ3—gÔ>;¾ÊDÞÄmv©p±ôÓQëùõ ‚:HæW8éÃù̓GR/Šqìžk5ÂÎÍcþ†Ì«#í!¨ÃbóWÄ‹ž”{Pw`¤Cðwízíòˆ8–âxš’›Y,=“²®çâqH½‹VÜðZòáOPX¸¨¢\ŽÃ0O½ZqLóžòÜ’C¦+Ö³ð›“WÏyú\®áIý¹zÎä;&áøRÑí†áì}B«Ùà|h«É' çK? ÄþýnyæçÏvÖ´ó܈»§U°q`CÕr÷Ðóç uÚŽµb·œÓ;qKÖ?÷==Þ'õŸ3¾ ?ПCô§ºGÇ©'^ÿVÕÀ›†‘Ø¿ÎNÜÒ:º#Z‡e‘ŒÑz‚ÙqŒuÔG£Wœž&ìN´ ËV] ÒgõWŽ|ØvòÖÓÖóÞž©\M¨•>;H]ÙA«±Å¡PÙsµ7òÉ%}†á8F ’‘[¹P™íz²s‡ÚLέ›díšCbåºQF;š¼‡$^tªC§·ú<®£À§Î‡Þ¿_é'_¥=û¥ÿ<'Îø´šRwê¼J2÷QLîlÒ¬›nÏ»>ÉÊY ð‰™4IÔà ç ¨'¨ < À‘< …uWÜG!¨CwÔ?ü`æcåó©”»…±?ªÐG%Ó¿~9bäFìq»BU¹½îE;—í_|,Þ¤oÂìŒÆÖ% éS«Óç1WBìÒÖìÌ'sTvê°ç‚|âŸD76ìgÐÜo\«eÍ+·j»rÓ”{,ùïAæj…¼ÊrwU¹1ww`i¾©œÃ2úYIn‚ œusYâ‚~ÉMOÙ{0¶ 2LÉ“]‹«]cпÖÍYc/ºµpÿRòúw½Ýã‚aæQ¤,³f#˜Ý-¶m ßTäñ¡ÖCyk˜Ù‚í™.ßÌÙúOÉosóÔ¡c4lÛ#mÿ†ó›3ù&cRÓúLO^ý«ZùÛé Ê=¢ü+» [WnÔº¸·¬ÁR¾÷ËóéCD«Ù ïaBeô³ªÜºƒLE‡‚Ø¿·¢›à ú%7=/åñÚ ·†ËõÞ‚ ^\k¾çÕ¶nžL=£×áùuàôVû¹­f#¨}*Þ.Ô:mS/B““Oë´]ónÄî¿|ðŠ¯»ñ¢G¦òövYÍhέbu¤àåÏôÜ(·@þD­8FdÔËÄÌä§ß–CT‡w·ó0d÷˜R¯/ Uû%qöÜ*¡,•ÒóäSUnãK9J¼xœ9·²lôËÎôa8žzÔh®Ÿ·.®5²ï¦a bÿÞ1ö»Vf3¿}Ú"jÖg³ÜñÇnÅÑeÉ_"9oD•Œ²ÛΟ§',fʨ¦YÉ/ŠËû£Q_S Qü+H|ɼÙiÝÎVç®»aF‰:t 2eyLÑ¿ëÏpò\ºln»Ø\ÈÉ礞 5yZµú oB; ç7g—DT>Wÿ”ýד¼‡Ë¿><]nü‰H2¬hš\:Ö`¹+Ö+7}¾º‰üé"6F-­FT?!¢jýBÉÚ‘®”´esÓ§C¤ Ç1ˆˆ‡Éí¤^#¹(­ùé±ß“¼‡«EòÏklývq2ÈiöÓ*x6Ô¡cHž©\¶Ìõ8õ¯O[T_=>R;êx±?êLù2~'Ü_<½½]DrîÙºr9ÅþÈ:ïÞðß¼þ‡anþó0ÌÕ¶¤T5ꩦYV`ëÊí¢u‘,7¼ÇrÓãCà 竬m´mÕ´úT–eY–Ó׬*¼ùûç“;;:É´âzâ0žóã®L¦I.ËS¼¸ÎIJoçSÙÆÚµþ êùÖ¥c¼r«Ö§ }^¹9ògXÛˆ“Ùð|¶ÓÉ!X=µ.F®ëXí™®ÛÁî~É¥Û‘ÎÔx®rÓÏoÎdYVt; ÏTY–eù2vxòÙSnݱ-’\}à¥gsÛŽ¥‘}—ø*ÝsŠ—žç¶z±?b–÷Aîóð'S¿}~^_=.2]“ä<™zY}‹LíÅÒ§F³`hô//ÿ½ªL²­e±ôK¼Ç¶Óà íØÓœ2—X¸w‹ˆšx;C«Ù šd9©½ A³E4§tDº:t­{ÒßÞ.Zùù„áøJoÞ[ŽkQ`›v`ÔׯÇV>ÑçÙ͵q9å®ÛqÑ“²;Jxåòžç—»«þÛåæËŸ_s^¹œvòåÏÄÞÌCA/võKn¹‚ ò Èׇ“kæË§šÜûÛ[ßÔ¨¼~á¦O¶R¨CךòåXP‡FÃÖ¯¸Å‡ó›·Êrà8njÓöá‚xÚ®‡ê ùËy8žúFou· :Íχ(X÷õ”›^íƒí èë‘ûËM€7E É·‚:pî››{7wŵ0»”VDl62˜íÚý9™z†Q?Ù2ðÖùd7+ êPó¦ÛO’OAí66×î¨d‡©çZÍ3¹†¯Üôó2ånäÃ)w·ü«¶w·žVI~<ÇL­Óv-˜=õKA¹µ-VJv¦ßp}8ò©&7âaÄÓ‡G%û­O:RÖ@•,GK‡! bÿÞª?(ÊåQwfGãZ:–SÏF=í§'‰½ô©¯SQ|:\nôàQAþõ¤ åÞcHHG=mFs³Ý¥Rï‚E,°™ÚÇÛ*õ.Zì3ŸÌ!†áxêÕ´óî+¡'³D9ùdÒ«CÇ ójR”Oe‹¡¨ÜÖE¯ $ŠWnÕúl§ß.—+ÿ,Ý!yå­Ÿuz^?ÎoRåvϵÚVþ¼~Ù°hgAM;WwìªäP&}ýÜCn‚:ä¹¹úP>Ó.9Ãñ¥œ ˜¶®dÃNêµ`õô<#<_*ºÝ0œ½··š Ž£¸ÞC"ê  }XúQØœ öïw¥çå_¬'O«`ãÀ€ªå–y Í›â?O®ôæ½M¯®C2æ·ïì¶e¹Q`ë&Y½Lz×5¢{frêhb·ñ¤#;G(7ŸÌeJÞe©|({€á8Fa GA¹‚ؿޚæ•[P®•ÉIŸ[.OþÌX’ 's¹{!¶ÓÈ!Sn*ÿÜ~)`~s¦ÓÈŠ¶ÛÆ÷oð图º~V“[b­jëu„8p+_òÓO¨UI^Œ8ªêÈÛˆÃp|evcרµF1¹³{–ẑgêvïzÏü ôdÁt:«º•Êå½Çðµ¸Vî†öòßü4¶w¯éÖúxÙA¨Þêˆ;(öæÃ‡ïß¿¯ô“uÔSΗþóœ8àÓj6’ý¾Ÿ™î&w6i»J ¢à¹a×´Yœ£i> “&‰z|áõUG€G€G€G€Gð(ð(ð(ð(À£À£À£À£HP‡î¨/~Q’±?rÝQ_„䡨¹Œ¡ú¼¥‹ýQ¶èã5jÔ…Jý+¬›ýŒj °J¯>¬G†ªP¾¿öþ˶÷X¼˜~¾Ä|ùàÈ… ÝÒfÓäΦöéÆ×]H[^ÌŠùÔ>ÿ Ø)‡ùÍ™,˺<³3£,͸‡áΔÃLõ…ÜÞIÚ†óÛ_³=•Û¿™Ê˜Š,˲,ŸÝÌ_ _ êóüžäGhﱜö—ÑÏWÅ8Ù¼Bì£aë‘ÙôŽÎ·ç­3,g¤wsþâŶ—éI†3øäœ òL9&m>†áüæL–ÏnvšõG§;0¶~9ÞéNˆ÷FÃÖYåu›´ë f… bÿÞ±ê9fq8¾Ôí†1è–ì_j5äM'/+‚‚úp¨Ô_Eóߥ½Ÿ–G!ê¹VóÌ·7óÈöšß\&ÿ=†óÛOê]LOîì€ÍV䱌úb2IžL"f—5Ök[ËQp /"JMÎGùêÐöYÚ¡Êþ>Úá)qØ#zd]Ÿ”è6ÚÅ‹ØÉ»b¿'y·‹ô“Qªíê0iòI½Fþr‘áA­~Â|’sø>ÉâöÁ“ziÉ”éߌp²b‰íóLTRbµÇÏ e:%yêR¹ú´Rê ôWA¹áøÒô5«œüÜíe•7$ªiÖF+Ê—ƒÖ·õ–¯o\=çy¼ÇÒ‡Üñ^ ·JõÀ>Åž³°“©WkŸ¶Ê&¯i–՞銢è6i×}QñâÞjÏ’9õ†q™ŒÝ¥ù¦,ËŠ¢Û)kx;fLt¦©µ‘8’´úƒbz$½•®Û‰‰]‘ªÑ#êÐ5(Zë0}ÍŠWZ׬]ŒËDÔ:m×¼é85ÑÎoÞ&2û=)°õ«q†áøÎ$ã¾/ ,V‡ì»q ‚Ú‘‚çFCQŽsúr«™ÅæXZ$Ãqcn±ô©ÑܰÒdž‡dd*k)i¹)¦WFß$#Z{QÌuøVAIFo¥Çå¦V=Æ—²n7 ç~Ã-yùö²Ê›ñr“|9¦h *g¼Œ£=܉m½åé[žïA%}Èï<¹·žàx'õ?/z$ ÇwvQ$J÷\«³ÇÄÐ lýíí< i±ôSÆr2û¾¸}`&£ ˆ‰‹†óÔT;'¢ùÍYbO¦^:ñÝ„žVÅåD‹&…¦M¥5„m˜oßE.ÚäΤÎzÚ8ýß»³OÛµ`õ´Ù)ó›w6i׃ÁµFö»ÔâÒ™búšå8ŽA¦’Šnªiõil™ålœxZµìF…íþe›¢Ûy¦¢Ä>QHO« ¶óØT}³±»iž©°.Kõ{Fn[z˜¯oI>4™z%º7Y‹Ûv é±~ÿ¸íå9—Ûã…ûüxz›«o;ô¼"•äÃï{ŒGÌ›C~¼xœVG&étÉp\#²ä³ñ:µ¿d>IŽ/å1 j=k,?­‚Zý„Âù|é“ÖéÒx™Ô³§‚|ˆí°´µ_¬öo•g&S›{Òj6¨&YŽ–òlš-¢9ÍoÎtYŽcT)(wþ{qûÎn[Ùz* Jº†ä™Š2¡îÀqÜNRDÊ2›L=£Çê³ÎméS»TÿæéÁÒ¯uN¨Õ¬û>¶è‘hÛ *)7º¨¦oGæi$õ.Ä üKµw““üñBÄy>?šÞæë[aú#À—OµñþÜõàQÄ&¦ÑéÒ¸¢!Îo<·wѧvú®çŒËXoË'k1I†Ë\Ï” ØñâZ#[gÉuèô>ªPKŸ‚™¾Þ—’f~s&ßD,îÊ8<ó‡P™dXƒe“‰H_éÍ{«×'· "Úab2ãms*¯ùYœÖWwÓúõikY¯U1”ñµõ˜Vû×§:bdidëJnǽ|{K—Âqt$½ÝÖ·âôGp$8ò©<ÞŸ©žˆùŠ¢0zbñ÷±i5Lþ»ø\ܾçNÇÅã,z‘áÐR0{\°(…$ÎD.as¯w$ ‚:Èîà|yض„ⓉX4W‰¬¶‚¾X§¨CKóÍ«ÉäÊô$#ŠÎZ,}JÅ»ÄR‰ú7ÞÖܺèIéh´B#´\ÿ>­‚Fç¼MËÅdê·Ï;`õD†ó¥OR§Ë¼{N¿tϵh£HJu˜žƒ.SŸîÀ¼‡½­FuèZšo*eìΗhïvo¼pŸ4¼Þc] ·¹úVFÏ7”ùHò)ï¹ñ{•ê €ª¼¡ˆh|)?õGVØúYIËlrg÷¬óîíBa8¿}kÖËMJe$h|g÷¢Ð Fa€PŽ¯ÌŽE[¶i7ŽºFÁÖb{Æp]ƒÕ‡÷œˆ&WzóÞŠÖXâúgÓ³ößõÔ¡cHž©DkfÇ1œ!)—c¶.Ë“[Ë~ÅêuïZÎkã²#³»Å~ý†ó¥_Ó$ϼ"¢©o’-wLîìžÅÖš¼³Û¬þ­›dõv×g;Ÿ‚~áv±:4¶®Ü–9jöeÚË¢Œ¢.óLùrÌ/ùÏ÷C¾Þrõ-ÌM¿ëmq¨|ŠÇû¶ÜxíÂÛ8šµü£½üô ,Ô¡Ó[m©e(öï­v¥ êÀ1¨|0Õg‡:t{+ýùîVû#«þÀ³±Žß¿‚xqoÕöìУ×çÙÇØaíýüßAðšøðáÃû÷ï+ýäÍᥦ6H‰³§ZÍeöfiL®ÌŽc —ÏbrE-Êäåú÷µé8€7¯°NáøRo®C°XÇ—¼«2 ÇWzóþºð¢ýÜ A¼è5’à(*›”‡G=>öˆzú Rì < À«ô(u˜¾Å¢ø^ ð¹"öG.c«ëÅþ(ºIcë'xŽçxþÂÏsÞáBføâm`‡G±aýpL³ocÏñÏ_þ90œßœÉ²,›Þb '¡ØÎìç8ãÿ“»7<¼Ë.Ô¡Ë.;ìÌUxŽçxþòÏ‹yî+q¼*Ž¿3;½êíºC5µˆ¡w­ôs^Ø ø$(è>±?:_¾ÝöñÏñüåŸO ê ¾öˆz*ô(²Ó€ öï-­–$ lýíí"rbd_b†ÑZÇå8Y'ÁÇìóó(Ÿð(E1o¨`{ÂÓ*¨ÕOˆâÏÈI½¬žHÄ‹kl]¾™Çž@ί[ÍÑjÿÖH†ëDDž)ãKÀ«¤pgöâqH½‹ûWw`HÁìqÁ\ ò— b¡PÉìTzA¦×0*!jGòL%Ú§±^ÓP‡l¸:d»¸£ÿG/ð±xCI˜SM³-úïÀÖßÞÎÃùí[³îX®–~¯ÌŽc8®ADmÚQÎoßÙmËr5¢ÀÖM²zÌC/Ö1R†ëä™a0a8¾³{–ãÉ£íôÌ¡|T„ íå¿ùé몓ؿ·Ú³d‡· ƒLÇ=}Š`Ÿ:ØG_ûÜGñoy8©gâ¥ZÍ«'ô/¯Ž7¯°NáøRoެl ÖÆ‘µàS‚m²_GÍ>Ò«Á òÀÿdü/ÿÖþæÿ)Ø'ê Rì < <Š*‚عCU(ø«;ê‹ÂÇßµ®?Â…ÅòùÔû÷U!öG.c¨VÕC)dziéù¿ªqô©óQÞ/£ç>" é'?ìþ§ßû­ôÓÿÞýõ7² ¾+þ§N÷¯ÿPüqlÂo]þa÷¯;ñÿâ?E)“ ¿ûï;tù]ˆ~òÃTú8MAz"þá&é7ªWÖ¦YÇF}ö Œª×ÂüæL–eݾ¹}êýøYêág ç>.o( ÿê7ßþà;ÿQþŒ?øÞ÷)øùøí“î׋ÿòëõóo‚Ù?ÿ¯ïá·þ·Öþóòëü—CDDß~ó“ËïÃ_gŽôù³¿œüÑO~ØýÙofÿü¿¦ ÊOÿãßû£?¯ÑøÏÿ‘=ÿÉÿð'Âÿóg ç7gò m¦^™=·ó/–Ûn9Ðþ'†æÖ¿j? êÐé¬ì†¦ÕÈ3Y/é±ü_^·ïvL¦6róg?¹¦wïè:QÜïJíâåŸ[Ÿ²ï±”þì¬ùñàÓâ+"úù·ÑÒÃïX|Ñïçëèï_ÿÎéw~ùG?ÿö×?øíßùñÖô¾ üî¿®}ýͯþö¿$Òßýò›Úÿü“òÁÓ[éÿà{ßÿÁo~9ùöy[.VýA‘eY·I;ﲇ¼ÕùÖŵæ›rL™Û»%à 3Îÿú‚E“‹ýQgª°LLO2î£ç‚:tŒ†­GÊ7#|S–ÏË 7Ÿî€%“E· l}íNPTÓ׬Aw?ùðÊÝK>½•žÉŸ5³=ÓY&ºÝ`ù³N1= l=) |ýÓ׬*r(Ïv=¹,gA­}ÚŠtæ´] f‹"¹ñàéaºžŠé¥×íü äVFéü«’[ÿ}úQÒêŠé±^Ðí V?áËs[þÞÃNóôùôp±ô©ÑÜèh¹àåÏÔ4ËjÏtEQt›´ët”æö{ êû'7ÿâúäæS0^ ê¿{¼”è/¯×£ "úÎw,üV÷;D±ÛðÍ·ÿ=2îõË? Cú»_ºßù~÷ëõ/Pkÿu§ûWí? ¿úÙ@¦àÿ¾û³üýÒu঄ßý÷ÑVŠõþ M!ŸÝxWZ2žûáÏûDwÔ*{L×ùß>xñws~s–XÛ“éÚbëv¤À~ÇùšžDfDÊ|ÉÍGÄfƒ¼é„IéqÙ_‚ v¤À¾›Déïì D[råëÿ^ò‰&#×ù3›âvÙ)¹Ò¿4™zÔh¶¸r(–ON=9D2oŸŠ‚ "k ÓÒ"¹•f£žÆzùüKÊáUØwzZëE£Ùâµ7 çLeÖòŸN>¦>­‚ØßõE6<×¶3/ÿdʱôw½ª¾òó/¬Ïv>;Þ'Üú猗=ú Àë$õôõïœÒÓŸüæ¤ûõâÿ,ßêþö×î* ¢0ü›¿øÕ¤Ã“Ø>ŠŸü°;ø5úo“Îôçß<}óÏ~ç¾ùuÉJðÒ‡áßü«éßÂïþIûäãŠi~s¦ÓÈrƒJ†ÜpŒ9±Ÿ J V‰'àO¹?©i†Fýn²3Ÿ0œ/}Ò:]Ù:˜=µš ªIë;ȉ(h¶ˆæÇ©ÿÑäsR¯«§ŒEV«ŸìSÏ|xr ÎóEå·žÖ9!Z0+m²CnÕ럯&Õò/’O _ÜöN¦žÑ;mÝ.è´MƒæÅõp±ôkj5ë¾O§-z$ VO»ò÷—Ì Ãñ¥<.~ìÃVþ‚ʩϢ¢þÖ?Ÿ£ô€×âQüo¿ùÎ׿ÿ÷è›ÿoñçôý¿ø^¾óí/þ y¢üþ¿øëß“ÿæw~üÍÿyê÷îÿâgÿôþý? þÕ ÓžÀ_ü¦û¯¿÷ó’•ØHÿó¿ûå7µïolÿè°}¦,BÀR£96A¼¸ÖÈÖe»¬Þ:Uƒcã¶þvyîX÷”òæçC’áº1ËžEO-–>‡(—»¿|¸–Û¦åu(9ïyåÂp<õŒN—&T¯yÓõ&“‚þ:Ä?¢Õ>ùóä æçÿêÜ ~{™ü{§­GjÓìݞ˘ÇÕÃÖi}u7­_Ÿ¶–õsØöÉû=p¸#Z­½ë|Žó>9Zx$÷Q|÷guú‹¿c»NNÙ³¿÷õ~ó‹9ûÿx:ùÇÓÉïÿç_|“ |"¢ð×ó?ùÉõÖÆ‹?÷ñƒðýòõH§=ÿ“_}ýÓúÏr7c|ÜsîYdK¥Ÿtϵظ<©×È_.ˆHÔA¼±4Š–ÑÎyQCáøR·I³îã äü|AíHž©(AÞa8žzµ{J‘_îòIYº³@ê]DñÝ!Åû"³§}ºG§w†ä=ÜÌCžŽœiÞW IDAT'"¢ÉÔ“:Ýn§‘ Ú-·ªòÔaj޾(ÿm¹qÛËÍ?Ò®!»Pà°q·w?–ÔÃÉMÚõuÛOÂx>ž>­‚Fç¼MËÅdê·Ï; f¨æ_ôòÙ|T}ÿìÓÞí|Ž<^8ýu,}ð2¼¡0¤oý ý#™~ùWDD~ñ›¯åï|KDÿò|ÿ›_ÍÖ[®¿ýÛÇßü£Óïýýûí¦3ð³úþõ÷?û©§ßþíãwÚ?¥oADD?ùawðÛìí¿®ýêçñi³ùéÿì/'ôÃî ý/ÑŸùß>»,²ž®k°žÍ”[»{y^2¶TD…áøÊì8ÑóÀ6íF<·š‰Ê;#%J`9uS¹çç†ã;»—d’.zr¥7ï­hí‚ö LâÕ?ùäz#·oͺ¤“=ƒhqûÎn[Q¨E‰úoË¿@G‘ÏÚDêYÙú¢’Üxú¶~†s&W# lÝ$«GÅzÅ“[n{yù§üC’;ãåxõ߯ËëaÊ>Ö4ÿa¼ï„÷±ô0 çK¿¦IžyEDSß0$o:C¢¢ü‹É¾ªú_©½Ì’¯4ŽöЇÃû Àk@øÑ¿ù?—Ãÿ‚8ºgRPÊytÌr7NcÔcùÒÕŸ+bdiþΣo¿ÌÑ÷ùIàe$‰þ€WȇÞ¿_é'o µÏ‡“z&N¥ÕlP´7€ƒU­Vâ&N‹í±ÀßOô|À£ø|Ç—zsd­Ï`Ùq#eUëS¸ÿ:¾Î3•[ìñ}ý ¿àsQO€ˆ=¢ž¾‚Ô{v±¾">ωgÊ—cˆðzDØúÙÍBž A:ˆðùzћΠàè¶þv 1>w‚«±Àñû£kHð9‚Ùxxxxâ• ¨CwÔáÊû#—1T?a‰­›áŽú"´ÿÓëAA~úz€ÏŒ7û¦÷VýA¹‡!³rNo¥¿½™‡Ÿ–CâûïÀÖÏnæ‰ç7gò ‰ý‘U?È ?–ÜAĵßYùõO,Í7•³ñËuÓõT‡îúooça¸«ñâÞÒje‹P‡noUª&«]‚Øÿ$ Ç—ò˜5Ù5ËÈà¹ùB£žÄþÈ1¶®È²,Ëò;:ï‹Â'TÿîÀ S‘eYÑmÒ®KU¾Õl7¼êzªC×hغ¢(Šn“f º»4Ǫ?Dýx ŸðJÚÎoÎä5¦Gä/ÓžCÎoßš^M;ï⠀ϛ#æÅ¦Në&l66°ã øôD2Ã3•Ëq(öG×ôî]³¿¥§{×sºä™ñ¼~ö9Q°Êm%ç€õ\«y¦’,Ìo.ç©&ÄõÌ”[ÐäíôíâËM¹‡ÄOz+ÖžÜÆ—ò˜ý{ñ8 ´öiëf¾1Í«gn»uètVvCÓj䙬—×]œK¥z ‚Ú‘[g}7¿}g·­^_œäß=×ÈÖ/Ë­ºˆýQÔ$Érµ¨eòåx~îvmývÛ|ZÔn¶ˆæx‹àãrü5 Ɉ¦ou›´ë ¶Ï¡;°4ß”eYQt;  eÖ4ËjÏôhö6šîû£ÎT‰çh%ã>ÊGº™ì¹bzw"~nú»æ¶9³õ̬lÏt–n7’r <^úÜvU¥@n1'õùËE±%íº®ci5’ ljÂðùõ,j—¤ÕÓ#Éè­tÝjõ“’MÙ]Oêv¤8IùS”?³¶gtžÚR$d6õozØQÓÒîDù~îveTõ¢'yy¾G‰r>Q‚M¢ÑâöÁ«µO[$bb‡áüq–µC“%…ÅÒO‰é<™zi Ò¾›äZ–ÉóÉHµÀ"<©×òÍ·ÓvÍ{¸¬´¤þ|s¯0}^»hmӻ޳^káy,Er‹\#O$9–´¢Ûy¦¢D¦t2¿]Ï‚vöÝ„žVÅo4[å\£Ýõ$" VO­‹‘ë:V{¦ëv°3ãšVŸÆ¦¯YuúUûý%Ú)ö¹F¹³íõŽcHžy9Æ& ||Þ¼@a8_ú¤uº4 ‚xÚ®³§õŸãñ0˳¹9•E7µš ò§ÛÓ²­fƒj’åh) nŸx“z-X¥jö´b6ü|¯ô[íDJGÔ°ÙûËHìŒu8;l×Så´k_*Ô³¦YV`ëÊÍ<ñbwÞ)s~2õŒÞKôûK´‹ˆ-P³» …_,}òvÅe|ņ ®ky¦\¸ƒVÄ‹kl%Ôa®áÝj6ˆV±ÌÊš4™z†ÑéÒxœ}¾iInZš›TM¿¹‰ý‘¥‘ýGlñÚµ—SQ¡žO«€$ߌS¶¶}¨S½Œé¿g{?f»’Ý>›îJÚñÀÿßÞýë&Îm ^~õÞŠ +° Jè]HiŽ>9T§q‘HTÑ 7§"´‘BtäÊ@ò-@‘/`4௰Í_Û`‡d2™ßSͳ½÷ò¶½—½mð¤šõ4_,EmÞøóB¼+©¯·ê×ZÆž \WQeu=߯Ÿ¼k3G\QëÉAóשã¯WitÖ÷0\w0Iòâ×LlQçõ´ûb«Ó**Ûå‹?¡%¤][qH¸|d}f‹¥¨åºx7g‚öÆÄMit>*8_»âëü´Âæ‰wÖ}±EmÏÞ\k™­GB–÷¶c°|T? IJµÇ$’·÷CÛµµ[…̧*¶úGŸøLiîQ¸îà®*£à…9ÁD|ÿÙ]cä]R_¿HÇuOV³7ë"‚?E–o–G~9ŽeZ¹¦?ò~¼·J½ÞXq,Ý”^pïbx§çŸ{þµücå‹Èà¶òÖê÷‚‰RŽ¥_Î\‘Ùã•™ i×ö‹€ŒñØðÊ÷^ây¸|ŠŸ¬fÏ»a›ºÕlKLÜÖY–¶™èuüõD‰2œð8$M'’×ÓÛŽþâÇ6âþò§Åîõ¡­U$ÝŽŸÐ.¥Øj‡Ý ø‚”ýó¿Eçß¼Hôjq=*ON¸:ÒÚy êöðè¹WšnÞ$ÛxbVy®ôè°’¸À·ñþó,|‚Ÿ?þøñ#ÑWþùøé{ïV*äsòÏ|?Ä _Þ§¼ëip«ç7SŒäØÏ¢¸àwÍ( ù\ú°;ë^VBÿ4ë^VºD;1â€/î/B€Œ€ß-£àI_é3 £ðéÂ~B5ÆcƒÐp^ŽE |ÏŒbç7³ù>ŽÒ ¾aF±£Øê÷´ qàìK¿Zß=£±ÍÊí€ÐpFÅV¿M|G<™ €Œ2 dpBF±ó{óÅ’ 8=£2 ¿"£pÏU–¢[ýq§¡ÄüuÜoåÓšWlõÇžNãë]ÙTkÜoa|þ(ñý“íø•·ñü‚ýÿ‹¯ð™þN‘Q(ÅÖsOËì|æXúÕãüË5oÖ½¬t¥Øê÷²_ç¼ÞxèiK³z9pé‡qs¥_vg_¤¶ã¯3èÔ~‹Œ"1×§|ã¯!ºë¯ýá ùœØ/CâÃvÄ Gâ À·Ì(޵Z}ï†céWÝ™+"Åà#±ÍÊí`½¨¢o¶îwØfõ6öŠo±ÕoËý½´×+Xç3ÎØPEüÕ>ÎÜ#WŽ£–ß|.¶Y½¸n|=“®7ª]‡ññîIÐÆFgl¨›*…®7Q<½…³/¦^I;Ûëô8+ÅÖs¯4 ¾»÷ßå#Ö» ‚Úk²DíŠ)'ª~èvŒŠ§ÒèŒÊ++§i±M/Ž¥_½Öž{Ù—í¯o§î¡ýó\ûE¢r¢âl_)Ψ¹Z%$±ý0d¿ˆ^~¯ŸœrH9ŒgLùI'QñŒú|·þ'mßðNÖ®“õÿ¤qø„ý.Ùq)bÿêÎÜÐ8Ç×ÿ,ÇøÆÎÿd¶jô²/ÕJ¥¢[¢]×½gÝËJ¥¢[Î~òqÓÖ–f%p{Â’ŒÖ땦zµZÕ-ÑÚ­¢âîÅ/Ç\j½‡úñÓØòÅV¿<©úŸÛªñ|ãÍÞŽªg¢õz³îG=-#ª1ùw(JT|ÜY÷*h£Rl5UÇÒ·Ò‰°õ¦ˆ§jbÛ«}³ž­ž Îóש“)Õ ~j¥Œ3}‰j4WúN?ñ‚`ÚâXúº)ÚSNTÿüÐíOQµìKÕ´½hè–“É^È|±”\~ï±åbÓ?ϵ_$*'>ΉÊíññ ]¾þÐóúIµª[Ž8–~ô ET™½ˆ/$&ž§×çH|"ÚòyÁÛuü¡ÿÑý1RtÿIÑÿ臨ߥ韡ûW|œ“n_€ˆ|̬§fÝK]ú½ÑÈý‰"‘– ën+QŠùœdÔÞHÛ:íä "Qó ‘Ëï?tî¬âêYH¸ÞTæ÷V©§‰¥¯Ÿ|^ošxž#ÎóÇ[+_ˆÌ½³õ0¶žóOé'§û”íϨáî|±Ì”/¤Ï.—R+È«ˆ³z‘Èþyžýâ¼åœ^þ™Æf®;[,E+×e0ð2[gúÿ•¸xž«ÿ´72lÆßÎëÌ “½8VN’þ“¬þý¦Â÷Ï(±qþUÇ £xRQéúw¢ÇI3Xœ/–âLO˜ë¿¼¢oÚšXzÅ›ÓÜ茚±õLºÞt§Ý›¶¶4M1z ?8±ëMÏB>'ËÉÀWô÷(v_¬aŒÇÆö;a6sK¿¦[ÅðNÏ?÷ük“"aåoÖµ¼ëîÌòÈ q,ÓÊ5ë¿SÏÐrΟFgd¨¶Y¸®ˆ_·ŽToî í=5žªßÞ“*×Þá“ÕìbéóÓ–:Ý?Þ[¥ž?Åàí:,'e»Þ½Sd׋eFSmóND&KÃPíÉl«ìõÏnW|9¡qöúÂxlˆØ¦n5ÛçÄ´7*>|eiîQ¸î`b‹jŒ<ŠP¨•2öd(óÅ2SªyŸ\wÖ½¼$Ñí2Ä ÚÕöË©?ô¼íX­ê–#Ž¥¯7eèv:>Dµ7¦_ðÝ2 ÜVª¦­£ñx<î4vNÄ£‘÷pÅh´¹ç%וáÄÎìL|Jj}}w{ ï¥&ÞÃÉîˆD-‡>¡ZN¸·•ä-ýVQQŠùÜfp“t½Ñíò¯ƒ®ë£(²êXOC¿ü'ËQË EqÝ™—R³ƒ2ö‹wA=|yëKžóÅò¤!Õ~ýãË?¬âMëýæñp²,]_gW¯óÞóòèÿþø²Ó…Æ9ùÐ2Y"ã|r=÷ÊßKË#ãpr¿JWÎûû퉟§wævLJ oÇ·'CïjÈëÔÉd/6_Ûî¡Ç‡¨öéW|aéáÎÜV¢(‡‘1îçõ+ïL¾=#¥ñ0jJ0åÉ~ñÎă‰m4ƒ‰Oç¢[;“‚Y³î¥.ýÞhdÈ)Sn"ÌËLùB ùìr)µ‚¼Š8«·_o!Ÿ“Œºù `qò‘™Ì_l­|!2/¬C·¼ˆÈráÍ p½-+¼þQåÏÏ·!—‹™;˜,æêi.R[g~¼×^&{!2;SœÏ‡è8'«g!Ÿ“åd–qEÄ!qæv–r’ö[‰ù<¬½òµÚ庳ÅR´r]ï˜æLßv»îþv?>Dmßø~À·Ì(‚³ìàNÏ?÷¼¹ïQ#ŒZ)#c46‚Orµâãü\S„¥xÓÖÄÒ+ÞœõFgÔÜVºþŒˆqGÒ: µìêi’m× ‹lf9™üzç‹¥8Ó°g<¼Ä¬\—¡dý›?±Ë§Lï×?¢ü³_E]ÈKɆŒ wFŽgÛ¾ïŽCârNŸËJŽÅ!s•“°ß*JÄçňö~‘ví%ª1öd¶Y9ú0Rôñ!¤½gÝøLgø=Šíyüá.²ÇÒ«þ|âªn9ï›ø¶ÿÁEim6õOWþÛÊY?9³;ÞjtÆ;o .¬'“mÉå 2:jóÆï1õC=èo‡qŽZohKQËuñ.*ÛŽï‰óIýa«½J£³¹Æ}BNñÇ•szÜ’öÛȸEµ7i¿JŸS¶»ˆÔ¯5/iW”FYµÍà8vBNq|ˆho|¿:Ü_ø:ÒÜ£8¸•¼ó'bùzYu¦O›;óש£•j…îl¶ýÂc42޽ž%jsg–Gþ=Ç2­\Ó;リűô«4sµ]w¶Xf4Õ6ïDd²4 ÕžÌ\Wäc×+"Ã;=ÿܯoîlO˜>YÍž!–>?mùÓ·otýS”?¼·J=*Gªú¸îìñÊÌŽ‚ûì¾c**ΉÖëÅr<6DlS·šísÆ!ipÝ™W÷±&âXº)½f\ÎÏsm¯˜ø„~ÕÞ¤ý*E|B·»G]ßS *ﺃ'«éO];a»G—bÚ{–ý€Ï§üëŸÿ-:ÿ–àÅ—W‹ëQyÂi ÀzLÜ}«ìî•”­·6+‡‘!'¼Xã< à·ðóçÏ?~$úÊ_D ŽóÞE¶VÈçä¬ÏŸðÛú›ÀQîàVÏ÷{›w1ùeLÈ(àÌÜY÷²ú'ï…]`³žQ £@F€ŒÈ(Q £@FdÈ(Q £@FdÈ(Q £€_Q[ý±§ÓøÓB¬4:ã~«¨(gŒñ<=žï‰¿ˆ(J±Õ¿§„?n)ÅVÜi(_¿>ºÅÇ!´_}…ýšý.£pÓŸxýV1~áY÷²R©è–ó®S‚V‚S—êKuÚÛÔþh塨¸%ž¿–·ñØÙ^_pÿ¢>ì¿€DþN÷µb«ßÓÄÒ«Ý™+"ÅV§U¼óþý[¨?bV+W)¶ž{íÖëÕoTy|&×u/+]úû àŒ…¢4®µŒmV×£ðY÷væÿ©xóÜÓ2ÞǶY½¸nlQáË[ý¶ÜßKÛû›cé—ÝY|!Ù—êíÀ¥Ñ5WúÕãÌuwË÷×q;p·•÷ÿùëÔÑJµBw6‹ou£36Ôzë5ÅðþâXú&3ÙZ^ÄYêaùçÝÒJ£3 V°Žghü£Úuj{Ë ~Ôz‹­¾¿Jµ7Öü5Wnñå¼#ž;Ë–¯[ϽÒ4hËö7UݪaL<£ê¿×½N·¥Ê++§i±M/Ú›P‡ôÃèúÇî/!Û1´?Äo¯h…à{'õ“÷ì×ÛñŒÞÂës¼ÿ«g±Õïe_¶Ò茛+ýê1r½1ýêýõ‰ŠÛ\ )ŽW‡ñŒé¡í:¹¿ß5_Vªç( ùœØ“aø™¬4Õ+•J¥RÑ­œñ|3›6~ùŒÖ땦zµZÕ-ÑÚéf7ÕzÚÒ¬T*Õªn9âXúÁéê"›‘åb_N±Õ/Oª^=M[Ý®§j4WzµR©è–h×õÍiXLoùªi­gLùçK'r–î¯b;ˆŠh»Ni¯¹Ôzõ˜õzS;L[Ë_õN:VNÒxF-^þüuêdJµ‚ß»k¥Œ3}‹DÏB mWLý 7m¯Í=6fRµìKÕ´½hë–“É^D÷“ÃúÛ/GĠÛ1ª?Äl¯Øò{Ù—j¥R©šK­÷àMVŒŠOâ#PD¾qFq‘Í„Ÿçk¥ŒýòèŸJç/öfœ“|ùõ¥¸ùb¹}b¼çF£Í5µ¨Œeú¸îìuŒËvRCu¬§á‘Ϻ—ëSÝp²3¢µMÿzÛz`¡(ò)…žVþYÔ˪cÝï2cãØ®Û;|²µìØÂ×¹½ÂËIϨå£Ê÷ûF©VTE)zA™ÅÞ# mWLDD¶ÿ}”c= åmåH°}rùBT?qÝ™·é6õŸUÈöMºÿ)?¸Ø<œØ’ËŽÅ'ñÐ8ä»±ûé~}’öÃÈ/¼­œ`üÝo½ÃÎázC÷ìïÜ.ëí³ñ¸vÜ8~|ü.þ>gaÙŒ³zÛ9Óf²"³TË/ިΠæ()Eٞɠ(‡Q3¦:®;[,E+×e0ðFZÎôm{b«o¨Ž¥>Š?“dëæ|ü¬›B>'ËÉ<É !Qù‰G$Åð EÅžp…|N2jo¤mˆóQæÅdˆ(G$a<£âUþLæ/¶V¾™{£êaªxF—?ë^êÒïF†$˜è’ Ÿ '¶Ñ¬çR+‰u?ü”ýWÎÓOR”ÏóìGIë9_,3å )ä³Ë¥Ô ò*²ǯ·¤Ç«=.=>¾{F1œØ†Q®Ë`o\´?Ù¡ìKº|:ª1â<¶Æ.?>£ZQŠ7mM,ÝûºÒèÄ&2açkY}\ù'ÊŽAÎÿùb)ÎáÄ}ï*jîô±OT9ÅdñŒŒDù^Ö:±r]†’ÍØ“Sb iWtùÞ ¸Òõg;’"©ˆé'^ý›µÂ«”dz?K÷ÎGï±ñI‘TìÅólûQºzjÙÕÓ$Û®ÙL² Ÿ·D¢¾¿|Îq)êø ø½¤™õ亃‰-ªñ¼~¶¡Øê´ŠÞ|nµy㟠ê†ÌG8S&\>²>³ÅRÔr]¼‹jÁd(Ei”UÛ¬V'+ΉéD0´ò'F+JãáÈd«v)Î΃‡g)?y|^§NF»ÞŸ)6§‘F‹IDATq¶ø&vfûY‹#ë]_KµíI Qå$ŽgÄò‘åi²Z®×˹£Ó«¢Ú_þöwÓnɸ~2|²Dk·KËõ´¥ä#׸þp¸½NT0Tû¥;sO‰Ï)ûut"n"R¿Öüä9rÿŠ‹gêþ°•!GŸeó‚ïNƒßµ€ßÁß"iŽ×ƒÛÊ[«ß nÄ;–~9sEfWfv¼dý.zã±áMTpÝðåSÔgød5{Þµ.ÛÔ­fÛ??YMj„Ç6+·ƒõÙQÛL$ˆ{½’ëîÌòÈ q,ÓÊ5œÑï­R¯7ÖDK7¥wdùðò£âõyÌ*vf‰ï&ŠŠŠ3øðNÏ?÷ük²™ˆº^øêÅÈÛÁò¡å$gäòQõÜô!±ôùzLç¨v…–¿ÿBÇÒ¯ÒLK:Òç¯SGÓ–/©ß¿?†n¯ª_Ï…ãâò~ÏøøÖ'fû&ª§ëÎËŒ¦ÚæˆL–†¡Ú“˜ý(Å~}–¸EÅ!j‰çaHz\Š:>sV€ß‘ò¯þ·èü[‚¶^-®GåÉ÷8¬ï¿USi<Œ áí„ø–{òî;C/ÞCþÌãó÷;Ïø–~þüùãÇD_ùë;ÇcïT…|N>à9 à (Ü´5Iô‚1€ã3à<þþÆms·z~35ëÄ_¶~/ÁÏŠÙf5å¤A€ã3€Œ"’÷B€NŽ?}ïκ—º.à#üEQ £ð»e<  }Fd>]ØÛcUc<6 çåXÄÀQ¸ƒÛÊ€°ð!”1ð­3ŠŒÖkÄ€dOˆ€ï™Qðó¥’âÉlïÊ(ø= é3 £@F€ŒQ ^±Õï·ŠÄ_%£PŠ­þØ÷ÇU/ ýVQQÞQH£ãűÓ8==Ø„þ«ŽkÔŸ½e°íoo˜xóÜÓ2ëm³z7pSþN…¢4zÚÒ¬^þˆ_ºhtÆÍ•~Ù}Dá®;¸­ ¼ 4îçõ«ÇÙ)ÛÅ6+·ƒupóÜÓ–›O¼:9ëÔÒvêãÿ´z£3n²÷`û…mV+•J¥R1mÕ=4R_Þ.äsbO†zd½Áwå²;sÝ÷õxeÚíºžê»÷–£6[Eƒ*ÅVSu¬ûÇ÷W øûð£á“ÕTKù‚ÈLÄ»ž­Šˆˆ³¹ª]lõÛr/mïÖ†c¹H¿{ds$´¥Ñ•WVNÓ2b›¦†*Ž¥_ugn¢ò½³/^ ~Ö…$m×fù üb«ï¯Rí5ÿ/Þ­€ÍŸì›)êx[9²Ù*‰’ŠY÷Þ*õÚ7¯Ws)Ü´5±ô£Á<4UÛ<é!™¨úD¯âl³¼ðEüç?ÿ9úÉfÖ“jŒÆ†?–¬\DD)æs’Q{#m³¼³™w³\xcGïéḊ\d3Îêmóÿ·•wÏ!¢å"aC£Êº_HÜ.¥ØÚyvÝY¥Ù Iëég‹¥Ø/ïxV~“Àh†êXú] ’Šÿþ÷¿QéÄNF±¾–¿;–u¦'Ì¡?b7ƒ8Y¿×‰åò9YNæ"’¬]ŠRôž=¨tg"¢4:£æçÅáxÂv×Ll#·zåR$¡é„Ä?GẃIº 홿NµyãÏï©?ª3}Ÿ¯‰§•_¿Ö2ödàºÉÛµy†AQÁÚ›4¡T;é—RÅaý«_¼Ÿå÷7ð¥ÞX²öw|Ã;=ÿÜûÓ¡öß\t"ïå§ÙQðB$ç̯Š*ßÙîÌæ *Ÿ¨]®;¸3Ë#¿Ç2­ÜÖ=Šùã½UêùS¨l³r;Ø}¡“1Þçñõ¾fR“Nˆˆò¯^ÿû–Þ‚Z½üAó|Š­~/û’(ñóÞ™›èGú>ôwýð«üüùóLJŸo?M±ç/¢ ^T:AFñM©Æxü!4øKŒÇ»Ï’àÏõg=H$jÖS îQHŒ@zÿ-A-…cp‰¥IEND®B`‚fwupd-2.0.10/docs/win32-uac.png000066400000000000000000000530071501337203100160650ustar00rootroot00000000000000‰PNG  IHDRÈoAÚÌëÐeXIfII* Èo†Œ”(1 œ2ªi‡¾ÎÎGIMP 2.10.362023:11:28 15:30:21 IÈäò„iCCPICC profilexœ}‘=HÃ@Å_[EÑJ‡v©¡:YqÔ*¡B¨Zu0¹ô š4$).Ž‚kÁÁŪƒ‹³®®‚ øâêâ¤è"%þ/)´ˆñà¸ïî=îÞþF…©f×8 j–‘N&„lnUèyE?¢aa‰™úœ(¦à9¾îáãë]œgyŸûs (y“>x–é†E¼A<½iéœ÷‰#¬$)ÄçÄc]ø‘ë²Ëoœ‹ûyfÄȤç‰#ÄB±ƒåf%C%ž"Ž)ªFùþ¬Ë ç-Îj¥ÆZ÷ä/ 浕e®ÓŒ"‰E,A„5”Q…8­)&Ò´Ÿðð9~‘\2¹Ê`äX@*$Çþ¿»5 “nR0t¿ØöÇг 4ë¶ý}lÛÍ ð \imµÌ|’^ok±# ´ \\·5y¸ÜŸtÉ)@Ó_(ïgôM9 | ô­¹½µöqúd¨«Ô ppŒ){Ýãݽ½ý{¦Õßpør¦Ù‡– xiTXtXML:com.adobe.xmp ]kûÙbKGDÿÿÿ ½§“ pHYsÃÃÇo¨dtIMEç JÄ¥ IDATxÚí½{T”×Ùÿ½i ¶**¢qf êR¤ÅQ`ÑVDì XõáV'‰i 4É‹Oâa’iú>éD&þŒeŒFÍôEt¢V¦¿(Š-ÂI§xª K׌è±ŠŠ¦•$ÍûÇÕìîÜs`8¨€ßÏâæž}ï{Ÿîï}íkï¹/¶æÐs `Œ}ûì;hè|v¾ù´ô,V€°„ ¬ ¬a++@XÂú=|" Å ™e•'ZºŸmBfY¶ÞÚkw‰Í'¢þz¤jîÈÖ[w—Ø0:x¤®¬8}¾Õùøéó­©++ú­ÅÚp¹­´ºù¡]®òDKBf™óñüíõºN{&ƒþŠvNø •ôN´/NY²âs‰¶ž>ߺdÅçÚ§ôOaÝ]b{iiX|tУU¥KWînÙÛÐp0‰)Ð)1"èLè¿oýQ[IU÷­Ÿ3u¢ïÖüíõ4}æ3ú°D3!¹Ü]bËÖ[ÃÍΦâÇæKÑÓF¥'*>6_rž’‡%𹻀Žäo¯w9gíPþ?ý“­·RÊKWîVžh‰Õ”®rð²{Kí/- sç‘ÔŽŒS:HÊÖ[ ¦ i«*É[–h¦6¡²ñÖpi)¦¶v_U½6ð!ØzßžÉàG2Ë>ÖG©¦’¦¤.T0Æ ¦ %ÉãÆ ‘œÛp¹R6\n»tåî¸1Cv—ØhJ.f8O9úÐæy\7ÓVUR‚KWî†.,n,IvW¼ÃUŽ]ùª2{ý{Ÿ-Ð)+Œñú-u<7ÎØ ÁΧ‡%šw嫨 ™eùÛëW½ÎãK4Wžh!óvÎŒ@Jɳ9îQ‚„̲—–†ÑYÙzk¶Þ [€G¨­Œ±nªêðXI+E[ìp•#VSJ6ZcSYmY)$ª*±_Z¶·ÔÎûüx‹þ•©q$a"ªO]Ï[ñ4¿zVÊ뙿»+Þ‚‰]ô´Q—®ÜõP‘ËÍ÷œŸŒ1®•º—¦”Y¯Ñÿ»òUôO|tЕk_:çöús“ ÿús“¦7ð€èI‹54ÄŒJúØp¹M8ˆ1Öp0©òD‹ODaVÊ2ÇD{“fî.3ܲ·¡±©mõú“<Q@½gÌèAݬZô´QÏéj:¼ºó³¡³„õààáÃ=Œ±î»zÒbzïã³\+ÃÆúq¡QMl,I&slAŒŒ<¡ž!K–¯Â“Wžh™3#P÷Ái‰Õ)f=m×âKWîLTÓeƒW9è ç}Yà †Ëm’ƒªéacýD»;[o¥Úñ§‚~KÝœî²UÈ;Û¼’¿÷ñÙyÊÑâ<*U:Ñßy-ë[¬:%-"ÑGÑËIG*ŒñŒ±C›çùD’ö…†ø‰Kí"Em’õ¢—–†´è”—›ïÑUèôC›ç…%š)üO¯z!|W¾Šƒ¬ä ƒY):´:}" ¹ó”8´y-s‰Ù6Lò‰(L[UI—Ó;Û¼±šÒÕëOJ¬u±ðY)ºf’zDUé×Ö.Û­>lÍ9ļ<¶¤®¬Ð¾8ÅY@OŸoÍý¨n÷ºØN«êÎ7!¬Ð“ ˜ ô<V€°„ ¬ ¬a++@X  V€°„„ ¬a%ùÛë}" )’#cL bÖÊãÌéó­#b>ysã©>ÔȧϷÎL-ñ‰(L]Y!×/éÉ`‚’;Ш¯2bä⟄,?6`ø@´5Œ±·îÏ}áHhˆß®|Õí»_¡A ¬âÇC«Þ¾û•é=óícÚ 'ÿôZ5=ÍíŽÊ-+ò¯_5ÃC+]ºr÷ÿsrú¤ý8’ë‡{.®ÛqÖ]à^—Lè³ê¿úP÷¹Üz§Ýs_ë÷ë'Þöï¼:­òDË3¯XžyÅòÙöù] $û8P}êzmý Ïi-_šÙ§OÑÛaÿ_š›Úúw_ßnkgŒAUû7ÜǪšøÙöù­wÚs?ªCs ¬=ÃÔ‰þ) rÓ!»xð`ÅUZñ‰( K4gë­7nÝwyúéó­>…Ùz«äxêÊŠ1ŸðùÛëiAÀ'¢pfjIþöz1qBfYBf™döíQ(IÆ'Ý>…’õ1ŸÌL-ˆiV\M]YÁ«óឋΪ<ÑB%ó‰X_ŸˆÂÕëO2Æb5¥>…’ròòÇjJc«×Ÿ¤«ˆíCMA9§®¬8}¾µÃ·ØV–†\v‡»Ëí.±ùD¬¸*é;±%©5Ä4âñÃUjç®ùpÏŰD3]TüʹÅ¡53µÄùZbñ²õV^—77žâ³¥úRn’¶å k+®ºìY—u¤¾ëH~°â*Uw‡çžå%ä'†%š©²üˆ¤­\Ò¡ ¥°2Ææ*Ÿ¢~¢Ùzë¢ìÏc›ßšu `nJ‚Ü`ºYæ²Ã¦Nô Ø]b“hŸé=+ecìÆ­û3SKV¯?9sJÀ®|Õ®|UhÈÕëO:k±—Œ3$2< ´ºY|­wÚkëoðÒ^¨’Q½ev«í«ÍoÍÚ•¯òê›ùö1Éý\f½öÌ+–—„(˜d0]øß?œ£¯*ŒñT‘Íoͪ0Æç­xÚ¹Hy+žÞüÖ,ÆXVÊ„ c|…1žkÙ´¥jëo®Y^aŒ_³<¼´ºyî GRZÝœ’ wÎ<`ø@Õô@ž­äÛÆ¦»ü¢Ô¼›/¹ìÙ;Ïûõݽ.–>þjÙxæ•d66Ý5˜.ðÂSùy[©¦.ˆ‘M[z ÷£:ž?ÕèXQ•-16xlÐàÌ·í=rÙ¹=©ŽÔ×’:66µ5–$ÓðèTÏZÏüýâ$J:,VSšùö± c<åOmU\Öäá.èÎÐØb•<ác¿{yªÄ+1EæGùõý´ü?Òu;Î.ˆ‘QgoÙÛ :¤¹ŸoéZ!ÄÈhHñ›ç¹¤q‘á¼ ¥ÕÍüŠªéß“ï€á#Ãnµ}o'MêB¿mH¸i®×–`<Øà­·&d–…%š×n­ë°×¦O!_ç²)#FzÐ2w#¼k=;lÈ“ž³êG2t!¬=ÞÒË¡!~âs¯³üjÙøÖ;ítg~¸ç¢ÿPß®Íñ½'>:ˆœSŸ¶G† ¸P%#{„ƒD„Üa¡ ‹·ìm ñ›§] S’¯°w’­·ÆjJ÷”^ž§=O9ú@ÁÜÌ|ܘ!¡!~ÅeMŒ±ö5ÄGÑ#“HŸo‘˜f=θ1C&íÊW ˜¶ªrVú!w¶Õéó­ã™µN2Ææ)G¯|~2y?=ãÍ–þ>´¶Ó·†.„UÚyµõ7ô¯ü{î?ÜïIrº¹_&ddxݱ»Klâ=à?ÔwOéeçÁ}¸Ê!Þà —;·AògqÁ­wÚOŸo-­n~qIÍ›ü‡ú©iþüx‹ht§­ªLI7LzçÕi«^Њ/zKÙwËh"´@Dóg[Õ`º°ù­YݽpÕ á«^Ÿåµýèååh²L³þÿZ '¿JcSµ¤óÜâAºPqhó¼ cüÍÛ÷Ýíó[½þäˆa/H*Ð)W½NSøe´´ºY2Jï|ßv£ŠwóáÑ…žíjèBX»Ë‡{.¦­ª\#ã}örÚDÆØo6–Üðµõ7ø¯¶\òâ’0Ó!ûî[ëvr¡k–‡×Öß,}nûcc,=QÁ º·y‚M»Î{.9MÄ>Üs±¶þ7N㣃>³~QZݼø'ßsŠ7Õ¥S­4eüpç[T¤qÃ$·úÒùcý‡úþö÷ŸR7nÝ7˜.p´KBž$i(oðòr U²Ö;í¿Ùt:4Ä”S'úûõýä°½±©œ×î˜1yD7->ñ\Õô@ϳrÿ¡¾üéH‘$0˜.ˆ’‰ ¥w~o ÑÀæ¯kt¹g»@7‡.pIï h¸ÜÆ÷ÍÙ÷H³R&ˆKUªé´+åÒ•»‹òÔ ª“× ¦ ‘ឬ¹tþØÌ·é>8-[˶§ôrÚªÊÏ·ßóóã-Ó…7~9…{ñÓÓ…%+>_ùüä§ýáO—›:v¿’ñ%Î^“ç…è>8M.WqhÒ=¹P%+©tÐ)j·àÀAŒ±ßþþoŒ±¦/¾t¹@0| ÿP_ƒéBÄøá—›ï­z!<`øÀ’&¼/- ‹ž6ªúÔõ-{˜Ó² ‡ŒWsk›¾ø2ä©AŸ–_½yÛ[óòrd×KVÿS*L‡ì\jÝ16h0cì×ïXñÌ8—NÆyvMÕ¸1CæÌ:äɪ“×W9\n_£gíá*G¶Þú³¸à¦/¾\·ãl|td·uhˆß¬ôC4fªN^_»µ.2<@|¨“Þ%d–‘Ç¿¸¬‰vvó‡U]èÙ.«j7‡.xÂÚØÔFûŸ©ÏR*Ò΃¬@§œ3#ð}ã¹Ì·1Æ"ÃÖ,ïð'ð´*j0]à^~üÐæyÛþذeo’1²sÅÛR5=ð@ÁÜWsk3ß>æ?Ô7+eÂ/žG{i=ßj0]0²ïÊWua.þIȺgiD†ˆ{‰$ÐÃ~w‰ê룪O]—4Ųø±SÆ5·¶±©7—¤²ëWͨ>u=[om½Óâ—·âéy™Cg{¶ktè—ø°5ç¾}ö¾RÜl½uw‰­o½tôE*O´ÄjJ=«dþöúÕëOòM£ü[Uw¾Ù—ÞÇJ>&o–nàÒ—„•–Y$.èm è¥ÌÖ[ý‡ú®ÝZ÷Æ/§<Ð]ð¸«ÁtÁ¨ï¿œ‚_(z?}lñ z»ªö­Å+è@X V€°è/Âê!Àßc…»‚îêôùVŠ…×ýœû7<üF2€Å Vðp™:ÑÿfÕõž_O\ºr7ueìµþ…G;@XÁ#ÃÑò¥÷¯"}‚ýir’@X O «³·^²VÀ?Ò´Â'¢0,Ñìy˜YæQHaWø‚@剖™©%>…#b>ÉÖ[%‘ßêQ(.ûÐU( ]ýÆ­ûonméÚú›k–‡Wã×,/­nžûºs(žJÕÉë<ý'‡íŒ11R<Åk£<»¦ª±©­ðݘÍoÍjljËXSå9ª6Åê`ŒÑ)Œ±EÙŸ9'Ït'“77žZ½þd|tP…1>oÅÓ —ÛÄ€É7nÝé/¡!~Æø7~9åp•cÉŠÏEí‹L9XZÝœ›ó4µpæÛÇ$£sËÞ›ãÞÅIßžÉp¾zÞŠ§©ÀY)*ŒñÆxoºÆÏ®©òê{ `nJ‚œº;!³ìæíû æ:—¿òDË¢ìÏnµ}µù­Y4À2ß>ær€ñþlû| CGÞÝVŸ’ ¯0ÆS¸Šg×Ty([¶Þš¶ª24dÈ®|Õ®|UJ‚¼é‹ÿ4õ‰s7ùX¥Â‹¡Þž]Se0]HISEL‡ì)¯W8×ÝÝØ£ÒÒŠÌ]ùüä-{(|‹—½y°âê¢ìφû=y `îæ·f Xwñ–s'VaŒ§-Ô¡KãåT€™©%«×Ÿœ9%€ª2dõú“îb,?–1Fñ@ŶõN;E⡸¹»KlÔ&›ßšuãÖý.Ü;Œ±{T÷Ò”oÏdôH4‡.ðÞnuãÖý¿î^HÿÏ »gãÎóÎѲõVƒé‚sh 뙿ךé剱ÁMw×n­£uzÜE†Ú<"X¨¦.ÿyØøEæ%+>o8˜0|à‚™é¯ó˜ÙÄÈ(j," ÖÄ›1yOòÔ EÙŸí-µ{è°Úú›ßšÅãV-?vVú¡l½µS!0»“ …Ù 2«¦®z!\4”jëoðV¥W߯ÝZWy¢…þ§Ø¥ÇЍ5¨_V¯?¹üçabP‘—¦Nô§ ó Ù`þjý»ÆCuæ)GSk'ÆßjûjíÖº1²ÝëbyñÖn­;}¾•GÓÇ °?üé’d€‘*56µqUeŒmûcCmýS{ÑÕôÀˆñÃÓVU¬¸ê2ú…u»)uá÷”V7óüi¬LxÓ͘–ŠËšBCüxÁÄç)!³,mU¥2bd§ÞúšfÒ£ ëð,Ö×4“Ķ^#³žù»$Mþöz—ªJ'61…S&«çHMsëöÿùõE  îllj£Gý⟄ðÿ/]¹ÛØÔFÁæŽÔ4³ïb'Ïû^VñÕÚ4Nœ»é¡‚¡!~b4À€áW>?™"i{ßJÝÉ$4į¶þ¦hV‹ B±ÈøÇ…*c¬úÔujÃUŽ•ÏOÓSÀÑs—nó#’7xÓ5îÛžr4cìÕg'JÊßö]€[ÕôïÝÒÃF†8j%KP¼‡ÉOI‹G(+—vcL÷ÁéÈð—‘ùébn4Vù¤õW§‰­A!ÂÚ¾©×ÃØÛ²·!2<@Tü€áŲöæ¸1CZï´‹~ƒNÔ¢HîP*ðçÇ]嶺ӽF%4²S™©´Y)$á&©¯%Â3b¬ÏÇÅb3Z½õN»øqOéåÚúy+žvi)dƒÒp¼Ü|¹ í¹P%[»µÎÑòå¸1C¨Å­gþ>nÌ#5Í‘áS'úG†|~¼%u¡‚º\1R<ÝùQé!¢2c,l¬ŸäHxè0ç{Æ3ÝÉd£62cMUèÂâ”ù/ž'içœ9ä1È|ûytǰ!Ov¶Ó½éwç:ßíC{*À¥+w÷–ÚOœ»y«í+뙿·Þi—ÄÜ^‘¼¶þF…1^r76µ56µy¿Ÿ¡±©M 4éåX•ؼg.Þ:~ö&=Ñx{^½ÃÞ|ý¹É¥ÕͱšÒÈð€×4“æGuJX¹Q"é¬12wOJÕôÀп͗èÖ¦Û TZzLŠÐ˜¡ñã==r±o«ׯ¦¶=¥—%ÓÏaܘ!¡!~ÅeM© íkˆ"S‚œSŸoƒ]÷w:#©ÝÏ$16øâ¤½G.¯Ûq–||Êé ›ßšE".q´±¾ÀîE±MIòäsIãÞ7ž“¤™9% ¶þFÑA›ól1+eYU"3¼§ž1¢/¢¶þFJ‚œÌð—„y~žuíêzsܘ! “v—Ø>6_¢FÛ·~ŽçøäÝgåó“3ß>véÊÝqc†¬Ûq6%AÞ/c‚ô°°Ú÷ºŸÉôI#(6uBfwÉy5Îü|É%/±Œh=Šß´Œ@6ÂGÿ3›1¶ F¶zýÉÓç[K«›ÅÉT—ïÉZ.ïÏ+×¾ì~&žŸØ¿Z6þWËÆ¸çbæÛÇö¹ìaÆÊñûÎìqÿ”—]Ó}ÒVU¦$ȹß1æ,¬é‰ Šô#qûõ½yû¾÷u÷ê»§ôr×–G$þ\jœÎfRf½&¹úí»_u¶7S*R**O´<óŠ%÷£:±éºPý·îÓŒÞÝYKçÍ|ûØ‘šæÙ?ÙØÔ¶Q)–¶¤Ò!)-5ËØ Á=+2}ÉÇâWZÝÌáÆ­û’5JïIŒ Þ•¯ª­¿A[—¼ßJ{Y¼ÏÄ{Q&=½ÝÖîMɧNô ñ[·ã¬÷mî2ˆÄ›ÜË®é©qÈÿ¯<ÑârŠýΫÓh!E\ÂN]¨0²{ï O]¨¨­¿Ñ…5k—n±;ÏwêÜ”ùá*‡XZ$Þ÷¦x\5=P1ÒƒkÆä’SÖ,w®>Eüt¶úÅG~J‚ü£} Ÿ¶óe+*í‚™ÁtAÒþ´ÂÆ}¦=(2}Æb}iiØêõ'Ÿ]Sõ\Ò8²ü‡úv97ò¤­ªüͦÓÖ %}V S¦­ªœ•~襥aÑÓFUŸºNí.æ šè?ÔW²úO7—ÚnÞØÏéjRä U²êS×ßÝVÏãÞ¨qc†Ð΄Ãþ,.¸é‹/×í8â'ùY‹çL<3~‘9+eBÌÓ£îÜýŠ*VSʰ¾ñöåæ{­wÚ½ìÞþC} ¦ ã‡_n¾·ê…p/»¦GT•Äe¡JVRé(­n¦}BÎÐu ¦ | ûw/OÝ]b›ûÂÞz—›ïí)½Ì7±HøÝËSÿZw#mUåçÇ[æÌ$WÒÔ‰þÞLÈã7ËÇæKÃý:çUXûÿ»¦jܘ!sfòdÕÉ뇫ø×ïXikTblðòŸ‡í)½,©¾Átá_Nñl#Ó|´õN»dv˜·âé¹/áíßôÅ—ûÿÒt¸Êq `.Ÿ¶ö¬Èô a¥IÁ»ÛêW9BCüV>?ùv[»K{Á{m=sñ–ó”Íó)cFÚ´ë>Ë[oAŒìÅ%až‡6ÏÛödž-{HÍÄÈ<k’>ºÜ|oËÞj¢•ÏOÖ©ß3ä³íóye#ÃþôºúÔuQX=÷æâŸ„¬Ûq–JàrŽ8õ!‹ØtȾ+_å®úâö/óQ²$hÙJ4±Åö§ÒŠÞ’!2ìé(\#;´yš<|ò·×¯^š IDAT²±$¹—¯…%š#ÃGxïÏíc"€`‚ô'ö”^öêÛËUõ`ÅÕÆ¦¶.LƒúVú »Klµõ7:õ¿GÂÆçÅe«~É Gúè¬ÿĹ›Ó' 6Ç=rÙÿî婽³À§Ï·ÒÆZêß½a O=mÔ‰s7ßÝVO?\Œ È[ñôƒøMMOÑv﫵[ëBCüvå«ú·¹Ê°x=¬ªX¼€ V€°„„auÙeœc¬@/äqüÀ[÷·ý±À0ÆRä/§M|´¡Ç°X © ßžÉèþo6Î]º½eoòø± ææ­xšÂþxŽg°X=1iÜ0o9168zÚ¨XM©çˆÖÐ?-ÖžBòcê¾&ð˜ ë[÷ßÜxjDÌ'>…a‰æüíõÎi.]¹›­·ò4î¹HÇ?ÜsÑymêÒ•»>…on<Å\­ƒÝ¸u?{}X¢Ù'¢Ð'¢pfj ŸÑ‹_ˆùäͧÜEþ¡(=’—™@¯pðˆ¾oür …¬Y·ã¬$Íéó­s_82bØÀÜœ§CžôiùÕÌ·Ýnk_õB8…oü´ü{<)òøòŸ‡ys¹Óç[)B=}ÕØÔ&†:~ö¦ó‹ýw—ØV¯?¹+_Õ/cðú¼°RD_1ÂÒÒùcg¥Ó¬^rİNJh>NºzýIzÝYJ‚|w‰MŒƒ´nÇYwQ<³é´$€°¤$ü+ÕôÀˆñÃÓVUJÂ/S zÏ¡~àQº¶ìmˆ ÷- (b¼tåîá*ÇÊç'‹^N ¾vîÒmÆØ/ž×z§¬T2o›Ú(£³?âAºŒ¯·eoCJ‚\üФ³îâ-1‡´U•y+ž†ªz¯ÅÚØÔöÆ/§xHàhù’1–ùö±Ì·¹L@á‹ËšHì>ÜsѨ¯KᣬÄ0«’’46µyŽyI9¸t2@oVÆØ°!ÇFßüÖ¬ðPéB<_šOI¯ÝZ÷û7•Ãî.±yŽG=t°ÛËe¥LpD, $9rîÒmü4Ð{…Õ¨o™õšd7èí»_ñÿý¾ÓAZ¶üçak·Ö©i:äÉÖ;íî,Jʪ¤Òá2+ÿ¡¾7oß÷¬˜ªéßžÉÀô8=écM]¨8\å8}¾•9}¾Õ`ºÀ?Nèâ·nÇYw;ŸcãÆ ‰ xßxîºänÙŠge0]/'–ÄtÈîò+xÐ<ÁT/ÿ¿?®è‘¼&öÿýßKFó¥;÷¾øä>ÜsñåµUFŒllj{aqèØ ÁŒ±é“F¼o<·ïÏMÿ¼ÿÍ·ß²’JÇþ¿4í;ryÑœ`q‚ÿAÑyGË?Þ|iÊ”ñÃùñª“×ÿ|ô žU\äh£ùß—»ßþM™õÚ{Ÿ}jäÆ žýã‘[÷5𯎟½y°âjNÞñ—–ŽErÒÿ½s﫟Ìz ãÐSüÏ™9=é 7fÈgÛçç~T·vkÝÚ­u‘áú@]}êúá*‡8¯0ÆoÚu~õú“4gúÅ3ß[÷ŸÄ O—›:ѿ֔øÞÇg ¦ k·ÖQVäE >PüŠ1¶ Föâ¬SˆÒ =ªªˆÒ =„ ¬a++@X  V€°€°„ ¬aa€^ËÆXs@>zˆÛ°X®€°„„ ¬aa+@X@X V€°€°„ ¬ ¬a++@X  V€°Æ d2Y¿¬Ztt´V«í…&“Éd2Yzzú£j»Ý.“ÉÌf3Æ?„µ‡±Z­²ï#gÑÑÑÞßTZ­6::úA·Ezzº—bAtY…î£Õj]êݱPz-Ôòv»ÝÜk4‡ÃQTTÔWj¤Õj=«°V«uw¹áf[X£££“““kjjß¡Óé$iV¯^­×ë{[[äääFw7íC#33Ób±X­V—ß ‡€^¯—Édî÷?”J¥Z­Þ¼y³Ke±Ùl‹/îCÕ‘ÉdF£Ñóþ¼¼œúº¸¸8++Ë¥¶Z­Ö¨¨(>6 ´µ_ kzzºB¡p8r¹œÌÎÎNJJ“ÑÇÞ6]R*• …âÀÞ¤t8ÙÙÙ¢r¹\­Vï߿ߛćC£Ñ$''?>C6%%Å¥]»v­oùR:ôxX­V‹Å²k×.>êt:]^^žsÊ 6h4~—Ùl6¸#ú‰°šÍf‹Å’››ëMbFc2™z[sdddöZíp 5øããèOåÎb³Ù CMM‡4µµµ …B´Q"##m6›ó¤Êb±Ìž=[<¢V«= ëÂj2™48$ •æ­‹/¶X,]›w›Ífwî$Ñÿ(NÉ?•žž.“ɸ#ULL¥Z´h‘Íf“̬©ØÜ‡eµZ].Pxö)»,­è> ÜYíP«Õ•••î Ã5×¹ØTÑD’TêNxî,^G±…ûËÙq!zäËbø·ÎOåôôt2Û“““e2Ùûï¿/¹PzzºØøT$»ÝÎÿqþŠ·dAAXgG¼Xw/ g‡Ã!™ÆyÃèÑ£c§NòfÞS^^ëÂj±XBBBz|Þí|§eeeqn\\œ¨:ŽŽk4ÉT+///%%ÅápäææJ †§áiii”X©T:OÙÄÜŠ‹‹%·k×.‡ÃQSSc³Ùømi·Ûív;÷–ªÕjñÎï”E¯R©l6›ËÂÔÔÔèõz’$ªhÈнLjL$^Á¬¬,’*^TT”Ë«ScÆÅÅñºF±.yyy¢Ã=99™Ë–ÙlNNN...¦oU*•è|×jµz½žŸËŸÙÎO墢"jvÊêµ×^S(µµµâàm½£GªÕjF€HaaáÑ£Gy{FQ[ź×ÔÔô [&88XbŸº“T…B!±O¡ªýGXi(xø6;;[¦Õ«WwaÞÝÔÔ$ÞÜó ÕjÕj5÷{:Oãââ¸@fÅ¢E‹¸…ÈKEKX.ÝXœœ¬Ñhø¥•J¥hŒSiår¹N§ã™Ëårqñ:''G¼‹h « }NNŽX¹\^\\ÌWÃT*/Q«Õ\€*++ùƒŠÄ]Òž.hjyÑDž¸ººZ¢@deeét:ÞøÙÙÙjµZ”lF#qz°ï–°Õ%K‡O$ɺôÒ¢çÕt¹2N õŒŒŒäe®­­‹‹S©T\€,‹x®ØbŒ;ç–—ËåJœï3Æ®^½Ê‹ÁoÜúóõBâ†öüTž={6P=zT¥RÅÅÅÑ$€ZIr]dddˆ§M›Æ{ª¼¼Üå·.}2]X©¯®®V(tzTT-d9ßJ´g€R†„„ÄÅÅ) gOEÝXÝŸ…Õy>Ò!ƃ%èÎì5 ´ÍˆTºøÀ"£ì‚½Ì‹-9·S;IIþº° Gn ñžqiÑw¸„•––¦V«I5H%%%—̸éavàÀ.£qqqG-//ïÚ­¨V«%…”x$㢸¸¢î–,ȸœà“ïR²’ÓáÆ’”””òòòýû÷s%‡²Åb!—ŸV‹Zél•Kv\ìß¿Ÿ/ñ9ïÇèÚHð½^¿zõê“(Š.ì:½ÔÇZ\\,Y¦n&³KÜn%λù½DË;z½^4Ó¢££Õj5%â”Ĉ†ø† ŒF£è{MOOww×™Íf~ R‰¦tj'©¤ÌV«Õ›R …‚Ïô­V«ËY¿gí Ç¥B¡ sZÇ·XÑÚ7s¨¹ y•/^l4m6›Ä?è%999‹E| FEEq­'Ÿ8ïtqÑ\.—k4š¬¬,þ­V«k±¯=*1Ö:ÜX2mÚ4›ÍV^^Îe”{W¸Çƒ!úôÍf³Ñhäê&©»»~ôɯ¥ÅV•Ü´UŽ»•xšÍf½^ßYèYôlväè¡í¢âq¡-Z¤×ë­V+ ñìììààବ,ÚëCV›¸â,—ËÅ̹ÏH©T'''ó‘­ÓéÜMš‚‚‚’““ù% ƒóoÃèçƒÞ<ö³³³###Å =oüæ¾3þóµZM…wgÑóÕy±eèB’:&%%M›6-**Š·ƒsí4Ñhä¹ñÕµÙ:uºøëLNÇ œÝÔÔÄ«F)%³þ­Á` µ‰'V(Înz*»û Í<Èã!ÚÈGMMMTTïƒÁ ¶0U§©©‰—¤¸¸XÔeê8ª»Z­¦Üzê†*//ç×[ÕqðÀ‘úÈñakÎ9^öh Ae/²õÐ(((¨¬¬|ä¯ó0›Íyyy][÷xÉd¢Ò=ˆü=+Ò1³ñv¯xm MEùÛOœMi/Cyht¸üý˜Ó…%ô1W@w½­ihKVo( fvèm^t ýÓb ×‚é€Å V€°€°„ ¬}1ø‡ËŸˆ¯³ôò÷bÌ—Ѩhm^l„êà1ÖèèèG¯  @¯×Ó«B Crr²(ô.ÑŒŒ þBo~:-Æ\qùkHüXë¤ÃW?Pôz=WKRR’Z­%O«ÕŠáI¼TjƘç7 P8Û]µêêj¼a¸6ôv1\Š\.£òY,1WÜ)u‡oÌÌËËëÁÈH€~.¬¢oQ|•¤ËÇäÜ$1âqVøAÑäGx|fñBž_PB§ˆ…á/²$CU|±±ª`ÿþý}“•Äó›ɤõœ†Ü¯¼ŽÔ2<þ¶³sÖe³§§§;G“ô1aMOO///w­# ql³Ù膧¨Œ1ŠÕìý¼5''göìÙèfôa%ÃÐáp<‘#•¢`ò˜òžwwv o4Ún·ëõzµZ““#}jµZLFá£)n¨\.ïlPƒàà`wÊK¦®ø€aß­Úu?t‚s³ú°òɾN§#¥(Uîmbßm„’Ìy9’HÑ………’ðpÎhµZµZMÆ)7ëBBB$:Haéa0{ölÉnÓ£GJ„X٧⬜*®T*«««%öݪ]×Bøynv@?VÑVª¬¬$ ¢9¸â˜1&þÔR¡Pˆ‘ÙI›6oÞLi-¹èR¤€Éžw›p×*E‡æž_›Í&NŸ“““yÜfú½/*•D4x¡Ê&''ÓƒÁn·ÓÂ7u—'ò.›ðéá]|Ã)7ÙwË5bx^ÑÑ™››KdŠo,—ËÅx†ÖÄÅ çääpcÓó# ³.þ¼Êd2%''Sàh‡ÃÁcP3§PÛEEEb(oçXÓÎPeù6çÐÓ=Ž»f<*zEøkï±Z­´%öÁ…;€îÐ[Â_\5|Û¿„‡0ë@Xû'ä™E;ú‰°*•Jˆ —+@X VV€°„„ ¬aa+@X  V€°€°„ ¬ ¬a++@XàqÖôôt™€çÄv»Ý]šèèh«ÕJ¦§§wöô®AÚívñ L&£’¸C«Õ`$8zÑùE*b„Iæ<ê‰V«MOO§€(âüš®% "+FFéšÓCœ&SÙèVÎ×rçÍpNóJOOçpŠ #z9øGž‰;g‹X`±ÅÜ…ágéõzz¿-ÿÖ9ŽÕjåÿó2îÜÎ!vÄSøU$i(D$8 ¥áÇ]–Gl(z?oVV/?—qîGú†Åê!zЍ,ƒ^¨*yÛ©Ë+ååå”~öìÙô®hg(\ŠÃáßàçþŠ,‚‡0°Ûí*•ŠòÑh4b>Gu8¹¹¹bÎ⮜ӘL&ÉûÂ333F#ÿh4333ÙwQa(½^ïü JOO—Ë唀^ýÅ¿rB†STT¤Óé4øZ[çp8âsB¯×ó 9W9::š—„BìÒ‘ššš¬¬,^~F§Ó%''SÛJ‚ÓX,:îp8 …gG6õ;+¹\nµZ³²²jjj¨´iiiÎý½BXùë=˜i¢§p•äqPc¹¹¹‹…›?<ŠHaaᆠèÿ¤¤$µZíòÒªÕjJCo­¥¶•§Q(º3OñäÿþôGŒ± ó½ÀÁ? ÿùé¶}=eÔ”óºy?ºÚö/ÊP¼ åIÿOõDiÚ~0gæÀ ½O‰wÖµ¯,û%[7ïGÎ1Ÿœ™%¦zË—ßòëòzÉ6Þ¦+Š™SM]ÖHl(Æo@¢‡}¬û– Þð×ûܻʩ»þ cÌñê°}K—Ù¿ÞY×ÎUÕñê0úÛ}¶=¿æŸQÁ¦Œz¢âò×ܦsceö¯—MzRrEžsêdß•eÿø[Ë7’«®›÷#º É?Ò& ®¹úõʲP²uó~ÄEP„„˜ò)ªoçÇãwÝ2ê :^wýz´ì[2˜ F:^Òø%Ø·dðî³í’öáì¬kß}¶RJ´ð˜ kTðR„%ûî‰ Vƒ|È.#ݼÚö/ÆXQ}»¨93þÙö5cì§Škù†1¶çÜW93’tî¬käãl¾MõåLšûëÈ’«ü¾öþ<ùn~þÏœ–Ùÿ#j”^’ìÙ)¾SF=Aº,’SF=Áñ•³È¥°îú7Ü:N÷¥ÂKà ¢‚ò¹Ôú/— 8Îÿô0`Œ­Šú!ÌUwWgUÔWEýpÚGwþûÏÿ Õ ü=ÿâÞ·ôO°ßáÝ+ŒIDATŽûý åÞ¿c±cÐ<ºîú7Ë&ýð϶¯wÖµÿâ›yŠ'»Vñê$UÜ`•«Ìþµlãmþ‘tù{ù òqw ñÄ)£žè0sæ¼<ëæýˆ93ºtªGa%æ)ž$¡ô€¨/WÛþE H6]~Í?[îý+*xÀOŽñMÝõo4]\ ‹A’<Ày2Î]ŸÞ –¼äT*±âIeŒÑÄŸRî¬k§e.wmÈ/8ÈÇa x\,Ög§ø®,»ÍW´;4é[>Ag¾3žzb÷ÙöO=Ágëîæ×Þ”Š1¶²ì´%î$»Úö/¾èïl„FÈ™9ç³nÞÖý'}uêÅ¡²ÿ©8Y¦QÁæÉ,Ùwv”4~E52ê .IQá  /âÃ֜󼉀÷È6ÞÆ» ‡°„ ¬aa+@X@X VV€°„ ¬ ¬a++@X 2Mú2™ ‡+Àb «Ü»wƒîuÂúî»ï¢cÀƒ`Íš5üÿŠŠ 4xÄÆÆöTVp@¯w$&&¢YAOqðàA4èsÀb+@X  V€°€°„µŸñæ›onÚ´Éózd¤%$$477‹êëëÑ8ÖþÆË/¿üé§ŸòÁ]__ïp8^~ùe´ èq‚‚‚ð̆°>.c}ùòåEEEô±¨¨è¹çžC³€ÁsÏ=wüøñòòr4Eo¯ |°,[¶lÛ¶md´:ޏ¸8ÆXyyynn.%رcGPPclÓ¦MŸ~ú)>^üøÞ{ïámô°Xû¡AÑÒÒB³~ò¼óÎ;Œ±çŸžŽ,_¾®ÐhŸw7ÑôH`ðã?h|ØšsŽW‡õH^LCЃÐ2Ëš5kd2cìÞ½{&±±±¥Õápt'ÙÆÛp@a+@X  V€°ðŠžÿå^œ€Å  ·Z¬kÖ¬Aƒ‚Mll,ºùÂ!`±„„ ¬a++@X  V€°€°„ ¬ ¬a+@X@X 71€1&Ûx =Åÿn|Úd=&IEND®B`‚fwupd-2.0.10/docs/win32-uac2.png000066400000000000000000000450671501337203100161560ustar00rootroot00000000000000‰PNG  IHDRÈ:™U¯ÐeXIfII* È:†Œ”(1 œ2ªi‡¾ÎÎGIMP 2.10.362023:11:28 15:29:51 ÛnžN„iCCPICC profilexœ}‘=HÃ@Å_[EÑJ‡v©¡:YqÔ*¡B¨Zu0¹ô š4$).Ž‚kÁÁŪƒ‹³®®‚ øâêâ¤è"%þ/)´ˆñà¸ïî=îÞþF…©f×8 j–‘N&„lnUèyE?¢aa‰™úœ(¦à9¾îáãë]œgyŸûs (y“>x–é†E¼A<½iéœ÷‰#¬$)ÄçÄc]ø‘ë²Ëoœ‹ûyfÄȤç‰#ÄB±ƒåf%C%ž"Ž)ªFùþ¬Ë ç-Îj¥ÆZ÷ä/ 浕e®ÓŒ"‰E,A„5”Q…8­)&Ò´Ÿðð9~‘\2¹Ê`äX@*$Çþ¿»5 “nR0t¿ØöÇг 4ë¶ý}lÛÍ ð \imµÌ|’^ok±# ´ \\·5y¸ÜŸtÉ)@Ó_(ïgôM9 | ô­¹½µöqúd¨«Ô ppŒ){Ýãݽ½ý{¦Õßpør¦Ù‡– xiTXtXML:com.adobe.xmp 3bKGDÿÿÿ ½§“ pHYsÃÃÇo¨dtIMEç 3áj› IDATxÚí}\”UúÿŠÏH¬0` –E:*"ðRg䛿O4@cQ¢@ía!ókk–­/‹bÕusÑ µ| 4R@Áo+®Î¤/Á u…ÅMEDøýqÕÙÓ=Ãð¨~ÞñÊ™{îû<\çÜŸû:×93Ç‚­.d:KÆØ;3FÀÐ)¬?ZÑ V€Î V€°„„ ¬aa+@XÅú熊o“ß,ËÏîx²Éïfn^ÕŒU¨Þ·þ¹¡ô×)UkŽÌÍ« ÕûÐ;xìÿ䕊â|ããÅùû?y¥Çz¬7ËKJr>´ìÊò³“ß4>~òÛØc;?yç?Ñ_áѽÐJzž W¤~&ÑÖŠâüÔÃ<®è™ÂZ¨Þ7~ÎËN“fü¶ªTiÐ=øÕk;sø‘™on@ 0b´›ÿŸvŠÚJªêÿ§#F»u]a=ùm, Ÿùˆ~k˜;!¹,TïËܼjk˜»±«˜Ÿ™dÿ”‡ëŒù™IÆCò­aî<\@GN~krÌ.ú¡ü5½ÈܼŠÎ¬4èÊò³Wú–äáe#.Û?~ÎËÍ@$µ#ç”RF™›WIß~ æUŠ–l s'›PÙ¸5LzÊ€‡©­WUF?ø|½wþñ?’ü~àœ··8¸y’¦¸ªæ3ÆÎ¤omg®\ríÍò:ófyI¥Agc'/Tï£!¹˜àãã§~œÌuó@Ì«tB¥A·5lÒk;s›+^IΑy‘_Ì|sÃÉocO}÷ÙÌ77oÌÈJÚÈSãXp0¾|k˜û¼È/¨ Éïžü6vÊï—3Æx¶†¹—åg“{ëðŒÉ«ºv…NH~?püœ—éªÌÍ«27¯‚/ Ào¨­Œ±ªêÃðXI+E_¬$çHâJß_œÄòÚ&Ì]"QU‰Ÿ8~ÎËŽígŒ•ý˜5-ì=‰8’0åZåÒ5<÷ s—.œn®xNî>$vöOyT^Õ™©HUE™ñ3ƒ1ƵÒkÑÊËgÑëy‘_üœþ¤·ÿ£7Nmò o~ò o<Ìh2àÑ™«9•ôöfy‰ÕP[ÆØk;sÊò³×?7tÂÜ%䎉þ&ÜM&xöàW•†Ͷµ<}Q@[Ïàa²VÍþ)ƒYÖbî6¶òfô;{'tJ><Àëx( 3=V§I3N}÷×ÊßÙ;q‘upó|mg.¹cNî> 5y²|ž´¸,?Ûá¯c;?‘xb‚öOyp-®4èΤowpó´j[’sä粙зj{³¼DrÐÁÍówöN¢ß¹yÕŽ?²’6:<ãÕ\²Ö#Gû¼’Ÿúî³ÇÇOCà·Rգ݌ç²~cuæ›h‰ÞŠQN:¼1ƒ1øqòú熒öÙØ9‰Sí¿rcî•ÌŸóráѽ3ßÜPUQF¹Ðå'o s§•K×Lùýòy‘_ðbP€•btpÂÜ%-Æ.Ö?7”O‰À“išKLöµ9ëŸz æUÊZ<ߨçM\é«Ù¶Vâ­‹…Ÿ0wIû\r@§¨*áÚÚn¿Õ‚­.ÄžW€G–ýŸ¼â¹p…±€VçgïÞôü{_¶5ÁõG+ ¬Ð™`3Aè| ¬a+@X@X VV€°„„ ¬a++@X IN~»þ¹¡´“#cLÜĬ+”ǘŠâüØOü°3m×A~ó¶îItæf‚’Véoecç2qŒ·ïØiϰ[à±¶;'åÒ5ô§˜·øfyɡؕ_,žlÆãŒ±²üì¯ÞüóVª4èöòJkvï¾äeìÜæÞ¦KFŒv[¾÷ßÓâzpÁ#í±2Æ~gï$nà<=,ª,?{ßš÷­yqᆴöm$û(P^ ½z1Ïü9w~ºZ¤Iùä¸l‡K'2* %=»­…:‚cupó\¸!­öNeöîM07kç0b´›‹2 H“"îÐ]¬ÍÌMÛZ’s„N“fL 4Š­(Îß®œ0wÉÌ77ˆÇ÷òŠ.÷èò½ÿ¦·'¿½p,ü>[gÅØi~¢ïœü~ c,ðãdqô¸ÒW¹txto ›ä¹è-q€»à ™üåÍÿäGÖ?7”ŸS¬ÍÌ?¼§H“BÕñ\¦ð “ddÿ”Ç‘¿¿õb^+WÕ|^_›N\éËsr÷ËÉËO¶Òl[«Ù¶–1öÎ?~âöÉÞ½I—{´öNe+ù¤&7I—ŒFÏ}¿‹ÛÊçõÜ<[Ùšf²+Tï;óê íí1Sl;Ñ’d ññ87,cLÒ4y;µÉ[* %ý­l¦ý/ÿȸÅ®e묘úÒjI^b]Îf$ª÷Q]óOžA“ur÷ÉÞ½‰ZÖE Úöä·±šmkƒ7fÔß½}|ן[Ö8àc¦Žæ;°É¤”K× —»Ž‹¬4”ØØ9=3Úcf±6“ŽH Å«©ºqj_\‘:•\feÀôÅQ6vr3YØ:+?N¦êTt§¾ûŒÌ%éäà7VÆØãã§iRÊò³éÎܼêLúv'wŸYË7Z·/+8™ô©áâiÞ„]¶uVª÷‰ÂZiÐiR<½EÝ%ùýÀ«ó&Ì]2yÁŒ±K'j¶­­ºvE¢Å­ÄÆNnë¬ÐåårP¬Í¬½Syõb^MÕ *a±6“16ÚýYê‚ß}°ªÓoõ©½ŸŠ]i=Ü^¼Ÿ/Ÿ=vrÏߦ/ù`êK«óï9“¾½ßàÇ(ýà…G÷žIß>kùÆaŽ.}ZI¹tÍoßC±+'Ì]â:c?NZfcç4%èíŸò(/ОÜó7]îQ3’²U]uÕÛ?Ú·æÅ—·ü³¹LÄ|vò‰*ÆXYÁI^ñ¢Rc¿²dÎaƘ±Ò }Ü9xc=x‚7f0Ƭ†ÚòOOïÿ²ÒP2-ì=ÆØ©½Ÿi¶­µáચo\Âbmæw,tQ¼ðÑîªëå—Nd\×6'¬;ÕT—ár×üÃ{²“>¥ø?áÚ¿Î=ø•‹Ê?xcFs¶-<º·$÷¨G಩/­6ß“›«c»;ðå³Ç.K›öµcúº?L_òÁÛ?š¾äê‡C¥¯{ÝPtZ1o±ÃSS¨¨•†ÑW9ûÖ¼(ªjEqþîU~ýÿnú’¬‡Ûÿûä¡C±+몫̨?„õ3ÌÑE|žIß.z £=fŽv6q¥ïñ„“=iò‚7ļZ¬Íäwˆî´š16î¹ÆØ¹ï¿¾z1o^ä¼¹ªæ|rœfÛZ‡g¼LÞ~-2všŸfÛÚJƒŽäæß'9¹û”ä1åRÊ Nö·²áŽž˜»|¢jsà˜üÃ{ÄûÙPtš‹×h™•†’ì¤Oévpó,/Ð’•šóGŒv«¿{‡1f=r?§¦êFææUbïwpó7;ä‹Å“S? {mgŽÉ¤ú ²‹×pi°nÿÝ u§Õ-z-f7Àzˆ“»O‘:•kS‘:•ìÆHºÜ£.ÊãÄXqpóäÉ!Jx¦?›73ÉdËæ¦míoeÃÇF ß°šªÍÕHÒ + %yvˆÂZ¤Iá¶rpótr÷Ù®ÌÞ½I{•ä})ö•m´ÇLëá‡bW^8¶ßØžÍÕ±ÝØPtúÕ§(ÁaŽ.‰+}Å® Þ˜A‰“¡.8ÈS9füÜwÿ΋ÊËNú´¢8_ò & µ‘ÉÅǃfÛÚþƒ'֔ޛ‚5?¿AŒÕ„×st/cljh¤$kë¬(É=jòùDU+›Ÿ<Äh“·8¹ûN=ø•­³BÒÿÆÍaŒ•ý˜Õ¾B:¹û0Æ Nó›Çmæ"[g/ƒ.÷(ÏÑÁÍSÌ}€õ[gE]õ-1AWÕ|Ñ%;ÍzpG,©;­®½S9õ¥ÕbÏ`=D1oq¥¡¤Ò 3y•Â7L¼‘ì\&1Æêª«:%»1Þ¾üu¥AWi(Q.]Ä5U7®^Ìã=§­5¼à ž)É7ôMŒ6låµw*Åõfn{ÉS\>iFíJ‰òж¢ –î×½tZØ{b ß°þV6—Ïo}íÚÝóó¬ILm\²ÉPb?œ%Õá©)Œ1z`‹OcU­4èJrŽx./§‘ÓO—/BF3•<2øT^Õ9¹ûwwrM^>Àzˆ«j~¡zE¯*ŠóùÀÜåœ5Æ—8¹ûT^Õµ;.lcçTöc–«j>e'Ÿ¨º®+(R§R?»z1Fm¼ç]8¶ÿÚ¿ÎÕUß2®½SIÒ̱9Ê8IŸn+Ue&‡Õ£ÝŸÍNúôÎOWÍ„ÏÊ ´—ÏcŒ5§PíËŽ¢† §mìäºÓj[gErÈ’$¯vc'¶µ¦ƒ‡ÉZ+Á/¼Q’{”‚ƒ“¼!Ÿ¨2#¬5U7.Û½ø|åUÝÍòãÉzãVùä8Š·š)›ËDÉcÕ<íîÀýZ_Õb0纮àÚ¥³ô3>ÏFˆIÝùé*cìPìÊC±+!š]HX/K³±sjM¯9Æû†žIß®;­vUÍ?›‘@³´ÌN“fP`·è‡T[gÅë!¤ •y²$"bäÑEåßo µÛÌE§ö~Öe›œÜ4=›ä÷Úw,ì¬Ämìä6vN4ü<÷ý.ù¤ôÈ<{ð+ò¿lé­)Àk;s Õûò3“¨Qüÿ´Ód¸™¼4˜x|ü´1޾׋ϟIßn>ýº»-»ö<îÑ¥à‘\eÀãã§1ÆÆÍ~ÉX(íœ'^½˜G7šä#š0CFa-Tï£Ò/1¾ÇŠ4)ÆÄ׌ i묠;¶P½O1o1ÿ¨¿•Í…ci’ zMÕ’œ#æ.áGn–·mñàSfIß^Qœ¯Ë=:nöK4Úêoe£;­¾^|^tºÄ¼ê¢ ãnGXû ²fŒ‰¡g‚&ˆÄÉI€{Öò<h&Ù¾ì\TþyvC4û­¿Q\E³mmEq~IîÑñs^~–qUÍwUͧ5Ô’(G4dŒ/ÖXFu¹G%½´þîmIÇ£i¨Ö—¶•¸ƒP$W ¯Ó¬qxÄpñô˜WÉŒtO¨¶~õb¬–¼Œb^ur÷á4ñùWcÇb$7üÕ‹yæïºq³_*Ò¤Ðjš¶"¦ýïÕ‹y…ê}’nÄÃ@z«4”ˆ1ÍÓû¿4_r’³ W/æqçT>iÆå³ÇKrŽñöý•£$“KêÒ&+ —»ߢ&]ñV;íùþV6ÇwýYÇšªyvð´I¬‡ÛK ÕZ™Ýh÷gkïTOˆ±±s¢{xÄh·þV6E?¤VJ$é@{Ìø6i½I¿ì¿1D7Oó£òþƒãªJ‘öÞ;ÄÉEôÒÜ´­â[êØâÊëØšÜYˆ IÉ9'Û:+ļʋD‘1mò–Ž4 <Öös³¼„ç²êÚÁ s—ˆSUnžæ.9“¾½òªnŒ·/-·Ê;°ÃÖYa~鯨iÏŠ]ylç'Õ7;ä±´1¯–ý˜åðŒ 9Ϥo÷\ôÀºÎXp&}{ê‡aˬ‡ÛçÞÓšo¿¸(JrŠ£×1ÞsŽíü„B®ÂðÓ‰îÉÑîÏçÖåµuV´ÉnVÃìcÇwý™1Vu½Üäýë!ý­lòì.ªª¢lÊï—°2óÍ b^ݵ|Öø9/Óú'tÏ\fz™ù•‡ã"=®—Ó¢™šÛ7[YÈVfG~}IîQ•¿èB©S¹Ô6¯øŒ±Ì-ï¸=d2žÛ"éë^·±•;<ãÕoàಂ“%9GhöÌDÐÀV^’s$sóª'¦Ìªº^®MÞ"Ÿ4C?µ‘Éw-ŸE}†Ö'Ù:+hZI2Äv›¹ˆ1véÄÁ"MÊ„¹KÌxvÆulMî8Ö#È>TÔṳ̈~ƒk®¡?NN~?0sóª¡;S“Íy{KâJ_Þôÿ)-ªº^VwûVûV4BXÛF¥¡„O@ÙØ9¹ªæ»ÎX`Ü9f¾¹Áá/ZïɳuVHV27×Þ¤È|ÚJìç¾ÿúìÁ¯(Fæäî#Y‚îàæùÂG»ÇEŠ]IKÁÝž j1¶8Æ{N‘&ÅSõÖ#Pc'ÒblQÙýÿ´3{÷¦ì¤O³“>uQ<ÿÞ¶Ì-«Úd·£Ýf-ßøÃö¾û`¡™Ñßüµßü˲ŸW’þ~9 Öàa²Óû¿$³›ÿª… _øh÷ñ]æv˜)QóCìÖd窚&};M:ÿlÿg¼hùô¾aè,Ò¤ðØQ›ãí«MÞBÝÀÖY!®a’@ûBõ>Š8Ïy{KyVbбÓüø üþV6ä"H*ëóúÇåÚÌÍ«hټɯœ˜¯ck:p§„Gª*ÊÎüŠúGà²aŽ.Í5ýë!Ï¿·í«eÿ³{•­Ûupó Þ˜Á›ž¾BÏ`Œ[]øÎŒÝ¥¸™›Wª÷ño[ð€0óÅ<ÿæ"@dýÑŠîô{¬5U7Τo§­  Ò„•Âùâ´tA,»E)37¯ê7ø±ì¤O=½õ@WAÀ£"¬gÒ·÷·²‘üÜtMºÙätqºÙät ¬a+@X@XàQÖ²üìõÏ å?|ºëŸJ¿'ßA*Šócò÷÷éSÉï±×TÝ8µ/®HJž ‹2`úâ(þ[VÉï–äy3ùÒ©}qyvÔÞ©´uVL}iµ˜xMÕã 1´ŒíM$ù)⚪ôSíô›ðŠy‹'Ïàe(Öfæ¦må!A¾Y±¤x¼HôcÌ/|´ûß'Ißîäî#ž,©oe€çÂæWl ØOØÈä/oþ§dØ~&}û›É—~º|ÑøG£Íô“ær1.°¸I%=VXé^rQL^ðc캮 êz9ÿôÚ¿ÎÜó·éK>˜úÒêüÃ{h ¾gNúº× E§ó;<5…öª4”HîÕôu¯÷ôØÜw?¯º^þÃöÒ×ýáå-ÿ¤»Žïîë¹è-‡§¦Ð.F’âÑ9•zbÞbÚŸ*ïÀŽk—Î’Ök3¿û`¡‹2à…vW]/¿t"㺮ÐXX‡>î¼1ƒ4(xcûe#)^€ s—Põ/8¨Ù¶¶êÚ“ûÑ^´ï¬(7µw*iÇ‹¶&h†Ãq‘ËÞùÇOf‡b^µuVÌZ¾‘öÂJý0ÌdãN˜»dêK«É¼_-û›+æ-¦ÁE,Tï›0w‰ÉMbÌô3¹ˆ)Tçï[óℹK=\Xiû qkeWö«N¯Ë=Jûç0ÆF{̬4”œIßÎ5bä˜ñsßý;Ý?$gÙIŸVç‹ÛÏ3žÿr õpûï>XxáØ~òƒhw_q§ ±Óžßµ|–XÉÀnžÃåOˆy•¶tÎMÛÚßʆ»Š ß0“{R°âàæIå÷ä ÄEÔU5ä“ã4ÛÖ:<ãe|ÿó-¼f.[ÏUãÒ‰ƒ6vNT…¶&h§I3LnPÈŸ7™›W‰Îìh™äP‹î¡¤qåU[Ã&Ú7=,ÊeºvÒ§¼-øÂä&£fú‰ù\DUݽÊÏU5;Ù®IgÆXíüÄÖYaævUÍUrì4?º—èíô°(Ñ+¡­èêïÞS· õ¹ö¯sôöìÁ¯l¢ƒ9Àzˆd§â³¿rQˆe yº®+dŒÙØÊkïTòòH"z-BèíèYöc–iƒÌXÀÓVÓÛJƒ®H“ÂËÜŽ›ã‰)³Ì|ª;­®½S9Éï5ñ ¤ îíoe#´±“;¹û\»t–16b´›­³‚vlŸ&7ƒ2ÓOÌçÂIý0ÌÎe"T<k¥¡DÜÐÔë‘£Zôy¯ë ®]:[SuƒBlŒcm|×øs§s* %ÍíL9ù…7Jr&®ô¥’ò‰ª6 k¥¡D9g±[êäîSyUgò7O;'>¯M ;vÚóíN°9úlæÓªŠ2ƘË$sµ»ª«½SI1å_ùÂî>?[oÁb^¥=!f-ߨÖ~Òb.<…g#bp÷‚GBXcýZ·ïBOtQ<>~clÜì—Ä¡hgå>aîãñ)Imìä¯íÌ¡%è´E°ÿŸvŠîíƒÀ#p١ؕÔ&oqQüV{Ï´ø±uVø¼þ±‘d[ñ1;c¬è‡Ô£Ý$Oˆ6µ”ù\8&Ãßô@aíoesáXšùMÕ›CýdŒk3ÛšÈå³Ç$¹×Ý­’”°æöM󛻪滪æ—ågï[óböîM<äÚ¾ê×TÝ(É92aî’æ®¢),ÝiµÌÕ]∵2Á›å]NÛo5ÅdDËHâËý=f(:mÆt2.R§N‹:÷ý®æ¦­Ì÷“s!ÌÌÂÐèÌ««jþÕ‹y…ê}íNað0›¶µM׺(JrŽTçó#ÅùyvHJX¤IÏ‘h–8H·s™Èã ÆŒ3^rÉ” ÿ5®>í,kr‡ë‘‹2àÜ÷»Š~HåÓV­OÐÆV^i(ktzÿ—m5;9›’ 'üj¬=Æ{NíʼŒfÒybʬJCÉÉoc¯^Ì3Se3ý¤5¹ðhy¬SC# OˆyµìÇ,‡g¼ce?f ý´™é,ŽõÆXúº×Ýf.bŒåg&õôX›rŸ¾8J—{t÷*?q)•ËDZ”ÊKX¨ÞGç8<5¥îîíªŠ² ÇÒhQWúº×mlåÏxõ8¸¬àdIÎåÒ5Íx¸c,sË;´4j´ÇÌq³C.K“TÿLúvÏEo™wÁÜž úµ·oI¦ÚZ“ ëŒgÒ·§~æ¸Ìz¸}þá=íø>˜ÜsÑ[ÙIŸÖUßâö7VÃṳ̈C±+¯Ÿ']×^û×¹‰Ï¿Â 3Úc¦ÓÉ=knÚªÅ~Òš\c[ÃÜû~L²€ž)¬¬‡~œLËïiª“»ÏErßVU”=ø7=— stin–©9uX¸!-{÷¦ì¤O³“>µuVÌ_ûMyVÖÖC^ÞòÏSß}–w`GvÒ§TÂq³_úÅ]òÕ&o¡’Û:+Œ—î‹(|îŸ?“¾½H“BkÔMV_\þÕ¤G•†IP²5 :¸y¾ðÑîÃq‘‡bWÒ÷H¦ÛÚvÓâú ´&ûóïMl#ž3÷Ý¿‹…±uVÈ'Íú¸ó¯Æ *ÿì¤O§/ù Ýý¤5¹ÐÅéá»´Ò÷Ž^Û™û[͵’­aî¶ÎŠÖÇs]–ž¿Kë…ciý­lº¸ªk3+ %Rôz²°ª÷]½˜×õ¿ò˜›¶U2mèÖôœŸ <ùmìµùä8z[uíÊ™ôí¶ÎŠ©¡‘]³ÀÅùE?¤Vêu%9G^øh7ú"Ö.‡ýSô#/µw*c¶Î åÒ5ãf‡´éÛS“ú»w²“>µ±sšùÜUz=|ò 2=ò >V€°„ ¬ ¬a++@X @Š%clÅ3u0t ëÂc„ VV€°„„ ¬aa+@X  V€°€°„ ¬ ¬a€nŽe'¦uõjEé•+¬éa”Û^f;lذ¾}ûXXXXXX !=SXuW®”•_kjjd'®wªkï54üî1«ÿܨ²±T[wÞÞ»woàÀ½{[õêÕ«wïÞhH@ÏÖ‚¿ ›šš;+Í9úkÿQùLÿ2éÿ”“¼.•”ÓÛô´}¶#‡÷íÛ§_¿~V@ÖÁƒ¬jnßø¯Ì5™t\›~ýQÓ/‡X/faaÁ˜cŒ55±¦&ÖÄš&Œµïl[[]4Û½wï†qOŽp{bxÍí¬©©¶¶¶®®®W¯^}úôACz¦°Zöé»cû—¬‰þkjlljjjÿm¤ÿ76555565M¿ýå4:¥ñ~Ãý††††††{ ÷êjkëêë›äÀ àû÷ïß¿¿±±­è±ÂjaaqÕpõþý†¦¦&ÆØ/bù_Qåˆoïß¿OoI%ÅîÝ»×ÐÐP___SSS[[Ëez è²`¹taa%G ¬ صk×Þ½{Ÿþy“§ïß¿̘1h„µ·•þqppøë_ÿúÉ'Ÿ¬^½zèС«V­¢ãï½÷ÞÌ™3éõ€zõB,amž††îµöë×ïÉ'ŸÔét—.]ÊÉÉÉÍÍ¥ã§Nº|ù2¿dáÂ…'Nä‘>[E/êêêêëëïÝ»GsYÝ˲¥¥¥2™,-- ìÁ‘––&“ÉJKKa дZ ±ÈIDATc…µ¹ë?þxäÈz™™yáÂZPPP T*•Jåã?^SSS-p÷jjjêêêH[;Ãõöö–Œ‹‹3¾-é^ÕjµŒ1™Lf|U7‚*(òHi¤îÞÞÞ¸áÁÃÁòágÙØØxïÞ½»wï.Z´(%%eÑ¢EcÇŽ2eJCCÃËT©T&$$H?~œ1–‘‘Áfgg3Æ<<<ºu»–––zyyÉår½^Ïvë‡DûˆŠŠâëíí-“ÉDƒÐ <Öæ [·n]uuõåË—7lØ0iÒ¤œœœóçÏ{zznÞ¼™‚´ Œ1òC¹ô¨Õj.¯F£R©èµ^¯OLLìŽíêååuâÄ ñ`bb¢££ã#Û×Éqqq¸íAwòXÅ¡zYYÙÿøÇ?ü°OŸ>ß~ûíÆ ^~ùåŸ~úéþýûtZuuu}}=¿j×®]®®®Í¥ÿÒK/¶¯l䦤¤pW4//<šèèhQmu:Ý»ï¾Û­522R.—‹n8 ärù•+W`ÐxŒŒŒäjEA:³ãqh°IHªÉ~™Îâðy-ãID˜B¥âk¢¹¡}BBBkž biźði·àà`1#­Vk\xnñS²'¿\ܤúJN Þ\Fv$f*©#¯~smÔ\áM—t n™LÆýeIFbëK"ã’¤‚ƒƒy^ЯGÂce?‘µ©®®®¦¦æ?ÿùÏ–-[jkkÍO:‰b÷õ×_?¸ªN:U­V—––ÒpX£Ñ„„„›““C/²³³årysãeNǃtZ­ÖßßÔ¨QÜ1”Éd¡¡¡4Þ¤§ä† ÷óóã—gggÇÄÄøùù…‡‡çååQ¾ä5ët:~íñãÇCCCIÂx”ÐXÙùCÂÎÎμ¬xyyñÒRá5:OMMMLL¤“IÅ(븸¸ððp…BÁ ¥Óé6mÚDŸFFFúûûËåòwß}—)¤5111tòºuëxâââüýý³²²xR ¥¥¥bFvvvÔ:d4nC2i›ÄT2Y·n/¤ù6âOHHà¥åö‹‹‹ŽŽæÇIòè*oooé.--ÍÈÈ0®ˆV«ÕbËd²ÔÔT>ÒÒét{öìA¤øòX‰úúúòòòüüü‚‚‚ššš®óu,___î“jµZNGîªJ¥âaVF£T*Í$’••Åc ¡¡¡üI@£o~:::ÆÇÇ‹†‡‡GEEÑD—ÇÇÇ“ˆPöìÙCeddŒrWN­V{zzÒI<;cFŽi>V R©Ä˳²²t:è …††ÒÍìèèJÒIѳ„,i\˜?üá4èæ•ŠŠŸ¢|'%—˹ÒEDDÈåò””z»bÅŠÐÐPž¬ŸŸŸhñòòR©TürƘR©ßšo£´´´„„„ÔÔTþ  *—––FGGÇÇÇóãâU:.((ˆ÷ ª¯Á` òójÒµiiijµš÷1ÊB¥RqËS‚Ý4îam?·nÝÊÍͽpáÂÍ›7»ZUår9MúçääÈårŽ   šÅ"†$¬¹èÌŽ5Jô»CBBÄ“yköÿk¬o`½Ùý¶|«7cÌ¢·Eï^½c÷›k¸ßÈï7‰íÈ‘ýúõíÝ»7öyôda}â §áÇ޽{÷!ü\@¯^½ зo_KKK´" Ç kïÞ½<`À€î¨ÚJaµ´´„Ç èáÂÚ«W¯>}ú ù …•GWcôdaõY0Ž+@X  V€°€°„ ¬ ¬a+@X@X VV€°„„ ¬aa+@XÂÚq‚ƒƒ###%½½½ãââZ¼¶´´T&“¥¥¥ÑÛ´´4™LFGd2Yiii+Ëg\†¶B…1ù‘···V«¥Ê›>^¯×gee…‡‡óÛ8<<\¯×‹A¬¬,ÆXVV–dxH÷ ?ŸDD«Õ†‡‡geeéõú¤¤$QzDÔjuvv6]+—Ë[ž[´hEZo*Bu:ÏN­VÓ}[ZZêïšª×ëSSSI˜ˆ={öèõzóÃa…B¡R©¸ddd47 í ƒëõúQ£F[;88˜Â/z½>>>ž*XZZÊ™ššÊÍ^ZZJgR±KKKýüüè½^¿e˲*UMRwwwFã …bêÔ©$µZ­V.—›ôy7mÚ$ihÑzz½><<\TÕÞX¤Å<_žh7žõ7º®°zxxÈårêÁ)))J¥’n •JE¡RGGÇÐÐPî°DEEµ>>CoW¬Xqüøqº£¢¢èætttl.A¹\ί‰‰Q«Õ-昔”D/"""x¥Œ±··çN1šÌ³ -//gŒ}þùç¡¡¡ô¤ñððP©TxzzR—0 T¤®ÇÖ_…)@qrù°+!!„†T©M‰{yyQ"þþþ\ÎZ“H;b‘棇??¿É€Z’IÙ„ÛA­VÓÍZP¦û_«Õ¦¥¥©TªæJxåÊî¥JžÍÄÑÑ‘%++‹FôbTAÌhÔ¨Q¼:­D¥RåääpåRË5μñ¹UMVʸ.\1)*­Õj•J¥»»;=‰Õj5™ñĉáááÍ-· #XvzŠþþþr¹œû¡¡¡ÜéÆ#McMi1‘k×®µUgu:틈ˆˆˆˆ è°ùFEEµf„BBBRRR4ͦM›:bq9‰i_žáíímooOPq5Bsªm†   ={öÐ ÞrrrÔju»ðg³q]t:cÌÓÓsÏž=W®\ñôôäjN‹Äî×ÁvàÁz¬ X±b8úúú&$$47”n½ÿ¨R©Œ‹   nÓjµ<Â+:iÜMe£›@%×ò¼"##͸‡Ü³#å5³ä6 €Ö™©¦™É+.âTNÉÂ&IF¢Á×S‰s;äÇ•––r+Éåòòòr‰Ùɾ¾¾&ó帅B¡Ó鸫H’÷õ×_‹ך6·­ôâ^¼N§ãu¤|¡žZ­Öh4”¯R©\±bw~‰££c[}p¶ÇJ^Utt4¿ý%s5-:ž&ILL¤É_eóóóóóóËÎΦƒ*•***ÊäM¢R©²³³I‚CCC¹{.—Ë%sAžžž”¬\.7¿DT,’££#Å(M>rø4Qk’mŽÐÐÐæFÐ<#Ñà<)râÄ Ñ˜¼EÈd1²Rbb"­ øÂU ï¾û.]kܸt¾(£$µüÑÛÊ6ŸE”ËåñññäSáe2I­Äªb¦žžž äƒ3Æ¢££M^@DZ`« õËëÙ•LKKÛ³g– ²Ø[ø­èd ¬€P €°„ ¬ ¬a++@X VV€°„„ ¬aa+@X@X V€°è¢Âªø²J{‹þ²Êcë³jg%Ýéa†û&¿^ñeUÇm’VÝY©º–®ªÁO÷}Ç«?‰Å±Ë ^ö–¦e³Êì­Ö/ÌÌ9y¯X£ amAJ*î6‘ª2Æ^të û ¬‚œÓõYµ\[Eþx¸fwA=cÌÇÑòk¿Atp}Ví¦SuôzáS}ÿúìrôö.D©)¾¬ò‘÷ùë³c!iÕ#õ¢×üòú·á½)å >Êo7R‚b.”&½vÞûÐ"+~pÅä~›NÕÑÉßäׯ:RC§mð`ü`ÓY1¹ŸÄU¯¸ÛÄóåõ’ÅÞ¢ÅÄ©¦&k$Š1ÆíèFtrŒuï‚A›NÕñè*'ÿú}Ƙ~ùc{ :RÚðM~=WUýòÇèowAýú¬Z/{K·á½]nà.0]Ë;RÚèÚG’#OyáS}W©9Wq_’ ©áŸ” É¿œÎÿÚoPVyê#5tÚŸ\EHˆ)Äóõüø¬¤;nÃ{Óñüë÷éѲwÁ *éøÿýû°wÁ Ýõûp¾É¯ß]POgJ´ðˆ «—½%)‚½Õâ„Ո䗑n–ßndŒ%ž¯µcÅä~‡u Œ±gå–ç*î3Æ’ ﭘܤó›üú-ŒÝ7·á½)eÒÜ7ÜûIrù,§ÎÇÑ’»Ÿk§÷?Rú_Q£ó%§½èÖ×mxoÒeÑ‘tÞ›;ã«<ûs)Ì¿~Ÿ{ÇÁO÷¥ÂKà'xÙ[ŽhQ|³Ñ¤Gÿ®= cïxõ‡» À£ à¼ãÕÿ¯þŠ/«þx¸†ToÄ _)øÕê&za?ø¿Çí÷ª¨ndŒM{Ü’ÆÑù×ïºö?¬kø&¿>÷ê}yŸö•G̤Š;Œ¢r)mÅÞâoI—•Î@‹æ²/tÞ»ÅsŒçåÙà3€+&÷3T<ŠÂJøÈûPšAÔ—òÛ¤€äӭϪ­¨nô²·|Vn™{õ~þõû¡Ï´s6L,Iª—½¥ñ`œ‡>[ƒXò-ÌÏïópyÄæWV½èÖ—N“ÅÞ²Ü s€<Ò¡€oòëÿxø¿¡É#º{GÕXyùÌclÓ©ºgå–ü£Äóõä¢N{Üòˆî^Eucû$æÿ=чÇ[ck~¨õq´4yš™Ð'…ÄtxÉ©TbÅ%’Ê£?ùM~=Ms5gCžÅˆÍ9¶€GÅc}Ñ­ïª#·øŒv‹ }ÊÈâÈw’mïÝõ“l{óÑzsãëÖ”Š1¶êH ÍG‰«$§•ßnä“þÆN¨—½åŠÉýx:|lÈ®¥ò^±–Åþ·âä™zÙ[ú8Z.Ø[M«þïß÷¨¦nÃ{› )HŠŠPÝ ¶ºÐü"v­G{ ¿ „ ¬a++@X  V€°€°„ ¬aa+@X@X =K˜t d2Œz½+Àc ½TWWÃàÁ1hР.'¬çÎCÀÁ¸qãøëcÇŽÁ àA0mÚ´ÎJ ¡èd ¬a+@X@X VV€°„˜ãý÷ßß²e‹ù#tJO›={¶Á`Ξ=ûüùó0„µ§±lÙ²ôôt޹ϟ?¯×ë—-[Ë€NÇÎÎÏlë£Ò×—.]š˜˜HoÃÂÂ`ð  ËÍÍÕh40EW?ø` ܶm9­z½^©T2Æ4MLL °cÇ;;;ÆØ–-[ÒÓÓéà÷ßÓ6QQQC}LÂùóçW®\I¯ç΋a„µÛCN«^¯ûí·©‹ÇÄÄžj4š÷Þ{oÇŽ&77—ô499Fí@©Tþøã[¶l‘è&©*„/^¼899900C( {;­z½^&“=ýôÓXºt)uqr. ðaÃøÌz!v9ÆØÛo¿}ðàAØ k·gÒ¤I#GŽäo·mÛ¶mÛ6þöÆO?ýtddäìÙ³c7n$  DFFþå/Ù±c‡xpĈüõ!C$넵‡t}ã@˜R©T*•ƒañâÅ|Ô@;‡JNN»PEE…ø GïB( §1kÖ,>sÅÑh4äDP¿qã :ضmwKgÍš%¾ýË_þ2gÎX kOs(***hÔOQ‚?þ˜1¶xñb:²téR„@G u~<ÜDÃ#±ƒ!Žÿ ±`« õËë”´°™ x@Œ7N&“1ƪ««±™ x@L›6viÕëõIG{ ¡èd ¬a+@X@X VV€°@ 3+`ܸq0(xÐL›6 F°𠡯r€PÀc ëÑÁ+@X@X V€°€°„ ¬ ¬a++@X  V€°„„º–Œ1Yì-:‹ÿg.,‘}IEND®B`‚fwupd-2.0.10/docs/win32.md000066400000000000000000000031271501337203100151310ustar00rootroot00000000000000--- title: Windows Support --- ## Introduction The Windows MSI package is built automatically in continuous integration. Only some plugins are included, any plugins that use Linux-specific features like `udev`, `efivarfs` or `devfs` are disabled. However, some USB devices updatable in Linux are also updatable in Microsoft Windows too. ## Installation First, install the `.msi` package from the [release announcements](https://github.com/fwupd/fwupd/releases/), or any of the CI artifacts such as `dist/setup/fwupd-1.9.9-setup-x86_64.msi`. Then click **More info** and **Run anyway** buttons: ![run anyway](win32-run-anyway.png) If not already an administrator, click "yes" on the User Account Control (UAC) dialog: ![uac](win32-uac.png) There are currently no start menu or desktop installed, although this is something we want to add in the future. In the meantime, use the start menu to open a *Command Prompt*: ![start-menu](win32-start-menu.png) Then click **yes** on the next UAC dialog if you chose to run the Command Prompt as the Administrator: ![uac again](win32-uac2.png) Then navigate to `C:\Program Files (x86)\fwupd\bin` ![term](win32-term1.png) Then `fwupdtool.exe` can be used just like `fwupdtool` on Linux or macOS. ![term again](win32-term2.png) Note: the `fwupd.exe` process also works, and you can use another Command Prompt window to run `fwupdmgr.exe` from the same location. There is no dbus-daemon on Windows, and so any local D-Bus client should also then connect to IP address 127.0.0.1 with TCP port 1341 rather than resolving the well-known `org.freedesktop.fwupd` bus name. fwupd-2.0.10/generate-build/000077500000000000000000000000001501337203100156015ustar00rootroot00000000000000fwupd-2.0.10/generate-build/README.md000066400000000000000000000001071501337203100170560ustar00rootroot00000000000000Build Tools =========== These scripts are only useful to build fwupd. fwupd-2.0.10/generate-build/fix_translations.py000077500000000000000000000024311501337203100215450ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2019 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later import sys import os import subprocess def _do_msgattrib(fn): argv = [ "msgattrib", "--no-location", "--translated", "--no-wrap", "--sort-output", fn, "--output-file=" + fn, ] ret = subprocess.run(argv) if ret.returncode != 0: return def _do_nukeheader(fn): clean_lines = [] with open(fn) as f: lines = f.readlines() for line in lines: if line.startswith('"POT-Creation-Date:'): continue if line.startswith('"PO-Revision-Date:'): continue if line.startswith('"Last-Translator:'): continue clean_lines.append(line) with open(fn, "w") as f: f.writelines(clean_lines) def _process_file(fn): _do_msgattrib(fn) _do_nukeheader(fn) if __name__ == "__main__": if len(sys.argv) == 1: print("path required") sys.exit(1) try: dirname = sys.argv[1] for fn in os.listdir(dirname): if fn.endswith(".po"): _process_file(os.path.join(dirname, fn)) except NotADirectoryError: print("path required") sys.exit(2) fwupd-2.0.10/generate-build/generate-dbus-interface.py000077500000000000000000000031101501337203100226340ustar00rootroot00000000000000#!/usr/bin/env python3 # pylint: disable=invalid-name,missing-docstring # # Copyright 2022 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later import sys import argparse import xml.etree.ElementTree as ET def _remove_docs(parent): namespaces = {"doc": "http://www.freedesktop.org/dbus/1.0/doc.dtd"} for node in parent.findall("doc:doc", namespaces): parent.remove(node) parent.text = "" if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("src", action="store", type=str, help="metainfo source") parser.add_argument("dst", action="store", type=str, help="metainfo destination") args = parser.parse_args() tree = ET.parse(args.src) tree.text = "" for node in tree.findall("interface"): for node_prop in node.findall("property"): _remove_docs(node_prop) for node_signal in node.findall("signal"): for node_arg in node_signal.findall("arg"): _remove_docs(node_arg) _remove_docs(node_signal) for node_method in node.findall("method"): for node_arg in node_method.findall("arg"): _remove_docs(node_arg) _remove_docs(node_method) _remove_docs(node) try: ET.indent(tree, space=" ", level=0) except AttributeError: print( f"WARNING: indenting of {args.dst} disabled as python is too old", file=sys.stderr, ) pass with open(args.dst, "wb") as f: tree.write(f, encoding="UTF-8", xml_declaration=True) fwupd-2.0.10/generate-build/generate-index.py000077500000000000000000000022221501337203100210530ustar00rootroot00000000000000#!/usr/bin/env python3 # pylint: disable=invalid-name,missing-docstring,consider-using-f-string # # Copyright 2023 Richard Hughes # Copyright 2023 Mario Limonciello # # SPDX-License-Identifier: LGPL-2.1-or-later import os import sys import argparse from jinja2 import Environment, FileSystemLoader if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("-o", "--output") parser.add_argument("-m", "--man", action="append", nargs=1) args, argv = parser.parse_known_args() if len(argv) != 1: print(f"usage: {sys.argv[0]} IN_HTML [-o OUT_HTML]\n") sys.exit(1) # strip the suffix of all args man = [] if args.man: for obj in args.man: man += [obj[0].strip('" ').split(".md")[0]] subst = {"man": man} env = Environment( loader=FileSystemLoader(os.path.dirname(argv[0])), ) template = env.get_template(os.path.basename(argv[0])) out = template.render(subst) # success if args.output: with open(args.output, "wb") as f_out: f_out.write(out.encode()) else: print(out) fwupd-2.0.10/generate-build/generate-man.py000077500000000000000000000146371501337203100205340ustar00rootroot00000000000000#!/usr/bin/env python3 # pylint: disable=invalid-name,missing-docstring,consider-using-f-string # # Copyright 2023 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later import os import sys import argparse from typing import List, Dict from jinja2 import Environment, FileSystemLoader, select_autoescape def _replace_bookend(line: str, search: str, replace_l: str, replace_r: str) -> str: try: while line.find(search) != -1: it = iter(line.split(search, maxsplit=2)) line_tmp: str = "" for token_before in it: line_tmp += token_before line_tmp += replace_l line_tmp += next(it) # token_mid line_tmp += replace_r line_tmp += next(it) # token_after line = line_tmp except StopIteration: pass return line def _strip_md(data: str) -> str: content = "" for line in data.split("\n"): # skip the man page header if line.startswith("%"): continue if line.startswith("|"): line = line[2:] # create links to other "man" pages if line.startswith("<") and (line.endswith("(1)>") or line.endswith("(5)>")): line = line.strip("<>") name = line.split("(")[0] line = f"[`{line}`](./{name}.html)" content += f"{line}\n" return content def _convert_md_to_man(data: str) -> str: sections = data.split("\n\n") troff_lines: List[str] = [] # ignore the docgen header if sections[0].startswith("---"): sections = sections[1:] # header split = sections[0].split(" ", maxsplit=4) if split[0] != "%" or split[3] != "|": print( "no man header detected, expected something like " "'% fwupdagent(1) 1.2.5 | fwupdagent man page' and got {}".format( sections[0] ) ) sys.exit(1) man_cmd = split[1][:-3] man_sect = int(split[1][-2:-1]) troff_lines.append(f'.TH "{man_cmd}" "{man_sect}" "" {split[2]} "{split[4]}"') troff_lines.append(".hy") # hyphenate # content for section in sections[1:]: lines = section.split("\n") sectkind: str = ".PP" # begin a new paragraph sectalign: int = 4 # convert markdown headers to section headers if lines[-1].startswith("##"): lines = [lines[-1].strip("#").strip()] sectkind = ".SH" # join long lines line = "" indent = False for line_tmp in lines: if not line_tmp: continue if line_tmp.startswith("```"): indent = not indent line_tmp = "```" # strip the language if line_tmp.startswith("| "): line_tmp = line_tmp[2:] if indent: line += ".nf\n" line += line_tmp + "\n" line += ".fi\n" continue elif line_tmp.startswith("* "): line += ".IP \\[bu] 2\n" line += line_tmp[2:] continue line_tmp = line_tmp.replace("\\-", "-") line += line_tmp if line_tmp.endswith("."): line += " " line += " " # make bold, and add smart quotes line = _replace_bookend(line, "**", "\\f[B]", "\\f[R]") line = _replace_bookend(line, "`", "\\f[B]", "\\f[R]") line = _replace_bookend(line, '"', "“", "â€") # add troff if sectalign != 4: troff_lines.append(f".RS {sectalign}") troff_lines.append(sectkind) troff_lines.append(line) if sectalign != 4: troff_lines.append(".RE") # success return "\n".join(troff_lines) def _add_defines(defines: Dict[str, str], fn: str) -> None: with open(fn, "rb") as f: for line in f.read().decode().split("\n"): if ( line.find("set_config_default") == -1 and line.find("config_set_default") == -1 ): continue try: _, wrapped_key, wrapped_value = line.split(",", maxsplit=2) wrapped_value = wrapped_value.rsplit(")", maxsplit=1)[0] except ValueError: print(f"failed to define value for {line} in {fn}") continue try: _, key, _ = wrapped_key.split('"', maxsplit=2) except ValueError: continue try: _, value, _ = wrapped_value.split('"', maxsplit=2) except ValueError: value = wrapped_value.strip() source_prefix: str = ( os.path.basename(os.path.dirname(fn)).replace("-", "_") + "_" ) if source_prefix == "src_": source_prefix = "" defines[f"{source_prefix}{key}"] = {"NULL": ""}.get(value, value) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("-o", "--output") parser.add_argument( "-r", "--replace", action="append", nargs=2, metavar=("symbol", "version") ) parser.add_argument("-d", "--defines", action="append") parser.add_argument("--md", action="store_true") args, argv = parser.parse_known_args() if len(argv) != 1: print(f"usage: {sys.argv[0]} MARKDOWN [-o TROFF]\n") sys.exit(1) # load in #defines to populate the defaults subst: Dict[str, str] = {} if args.defines: for fn_define in args.defines: try: _add_defines(subst, fn_define) except FileNotFoundError: print(f"{fn_define} not found") sys.exit(1) # static defines if args.replace: for key, value in args.replace: subst[key] = value # use Jinja2 to process as a template env = Environment( loader=FileSystemLoader(os.path.dirname(argv[0])), autoescape=select_autoescape(), keep_trailing_newline=True, ) template = env.get_template(os.path.basename(argv[0])) rendered = template.render(subst) # Stripped markdown mode is used for HTML docs if args.md: out = _strip_md(rendered) else: out = _convert_md_to_man(rendered) # success if args.output: with open(args.output, "wb") as f_out: f_out.write(out.encode()) else: print(out) fwupd-2.0.10/generate-build/generate-metainfo.py000077500000000000000000000016411501337203100215520ustar00rootroot00000000000000#!/usr/bin/env python3 # pylint: disable=invalid-name,missing-docstring # # Copyright 2022 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later import argparse import xml.etree.ElementTree as ET if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( "-r", "--releases", type=int, default=5, ) parser.add_argument( "filename_src", action="store", type=str, help="metainfo source" ) parser.add_argument( "filename_dst", action="store", type=str, help="metainfo destination" ) args = parser.parse_args() tree = ET.parse(args.filename_src) root = tree.getroot().findall("releases")[0] for release in root.findall("release")[args.releases :]: root.remove(release) with open(args.filename_dst, "wb") as f: tree.write(f, encoding="UTF-8", xml_declaration=True) fwupd-2.0.10/generate-build/generate-plugins-header.py000066400000000000000000000022151501337203100226520ustar00rootroot00000000000000#!/usr/bin/env python3 # pylint: disable=invalid-name,missing-docstring # # Copyright 2022 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later import sys if len(sys.argv) < 3: print("not enough arguments") sys.exit(1) with open(sys.argv[1], "w") as f: # empty argument is no plugins plugin_names = [] if sys.argv[3]: for fullpath in sys.argv[3].split(","): parts = fullpath.split("/") name = parts[-1] if name.startswith("libfu_plugin_"): name = name[13:] if name.endswith(".a"): name = name[:-2] plugin_names.append((parts[-2], name)) # includes for dirname, name in plugin_names: f.write( '#include "{srcdir}/plugins/{dirname}/fu-{name}-plugin.h"\n'.format( srcdir=sys.argv[2], dirname=dirname, name=name.replace("_", "-") ) ) # GTypes gtypes = [f"fu_{name}_plugin_get_type" for _, name in plugin_names] f.write( "GType (*fu_plugin_externals[])(void) = { %s };\n" % ", ".join(gtypes + ["NULL"]) ) sys.exit(0) fwupd-2.0.10/generate-build/generate-quirk-builtin.py000077500000000000000000000015401501337203100225450ustar00rootroot00000000000000#!/usr/bin/env python3 # pylint: disable=invalid-name,missing-docstring # # Copyright 2023 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later import argparse import gzip from typing import List if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("output", action="store", type=str, help="output") parser.add_argument("input", nargs="*", help="input") args = parser.parse_args() lines: List[str] = [] for fn in args.input: with open(fn, "rb") as f: for line in f.read().decode().split("\n"): if not line: continue if line.startswith("#"): continue lines.append(line) with gzip.GzipFile(args.output, "wb", mtime=0) as f: f.write("\n".join(lines).encode()) fwupd-2.0.10/generate-build/generate-version-script.py000077500000000000000000000111301501337203100227310ustar00rootroot00000000000000#!/usr/bin/env python3 # pylint: disable=invalid-name,missing-docstring # # Copyright 2017 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later import sys import argparse import xml.etree.ElementTree as ET XMLNS = "{http://www.gtk.org/introspection/core/1.0}" XMLNS_C = "{http://www.gtk.org/introspection/c/1.0}" def parse_version(ver): return tuple(map(int, ver.split("."))) def usage(return_code): """print usage and exit with the supplied return code""" if return_code == 0: out = sys.stdout else: out = sys.stderr out.write(f"usage: {sys.argv[0]} \n") sys.exit(return_code) class LdVersionScript: """Rasterize some text""" def __init__(self, library_name): self.library_name = library_name self.releases = {} self.overrides = {} def _add_node(self, node): identifier = node.attrib[XMLNS_C + "identifier"] introspectable = int(node.get("introspectable", 1)) version = node.get("version", None) if introspectable and not version: print("No version for", identifier) sys.exit(1) if not version: return None version = node.attrib["version"] if version not in self.releases: self.releases[version] = [] release = self.releases[version] if identifier not in release: release.append(identifier) return version def _add_cls(self, cls): # add all class functions for node in cls.findall(XMLNS + "function"): self._add_node(node) # choose the lowest version method for the _get_type symbol version_lowest = None # add all class methods for node in cls.findall(XMLNS + "method"): version_tmp = self._add_node(node) if version_tmp: if not version_lowest or parse_version(version_tmp) < parse_version( version_lowest ): version_lowest = version_tmp # add the constructor for node in cls.findall(XMLNS + "constructor"): version_tmp = self._add_node(node) if version_tmp: if not version_lowest or parse_version(version_tmp) < parse_version( version_lowest ): version_lowest = version_tmp if "{http://www.gtk.org/introspection/glib/1.0}get-type" not in cls.attrib: return type_name = cls.attrib["{http://www.gtk.org/introspection/glib/1.0}get-type"] # finally add the get_type symbol version = self.overrides.get(type_name, version_lowest) if version: self.releases[version].append(type_name) def import_gir(self, filename): tree = ET.parse(filename) root = tree.getroot() for ns in root.findall(XMLNS + "namespace"): for node in ns.findall(XMLNS + "function"): self._add_node(node) for cls in ns.findall(XMLNS + "record"): self._add_cls(cls) for cls in ns.findall(XMLNS + "class"): self._add_cls(cls) for cls in ns.findall(XMLNS + "interface"): self._add_cls(cls) def render(self): # get a sorted list of all the versions versions = [] for version in self.releases: versions.append(version) # output the version data to a file verout = "# generated automatically, do not edit!\n" oldversion = None for version in sorted(versions, key=parse_version): symbols = sorted(self.releases[version]) verout += "\n{}_{} {{\n".format(self.library_name, version) verout += " global:\n" for symbol in symbols: verout += f" {symbol};\n" verout += " local: *;\n" if oldversion: verout += "}} {}_{};\n".format(self.library_name, oldversion) else: verout += "};\n" oldversion = version return verout if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( "-r", "--override", action="append", nargs=2, metavar=("symbol", "version") ) args, argv = parser.parse_known_args() if len(argv) != 3: usage(1) ld = LdVersionScript(library_name=argv[0]) if args.override: for override_symbol, override_version in args.override: ld.overrides[override_symbol] = override_version ld.import_gir(argv[1]) open(argv[2], "w").write(ld.render()) fwupd-2.0.10/generate-build/meson.build000066400000000000000000000013131501337203100177410ustar00rootroot00000000000000generate_metainfo = [python3, files('generate-metainfo.py')] generate_version_script = [python3, files('generate-version-script.py')] generate_plugins_header = [python3, files('generate-plugins-header.py')] generate_quirk_builtin = [python3, files('generate-quirk-builtin.py')] generate_dbus_interface = [python3, files('generate-dbus-interface.py')] generate_man = [python3, files('generate-man.py')] generate_index = [python3, files('generate-index.py')] fix_translations = [python3, files('fix_translations.py')] if umockdev_integration_tests.allowed() unittest_inspector = [python3, files('unittest_inspector.py')] install_data(files('unittest_inspector.py'), install_dir: installed_test_datadir) endif fwupd-2.0.10/generate-build/unittest_inspector.py000077500000000000000000000020131501337203100221170ustar00rootroot00000000000000#! /usr/bin/env python3 # SPDX-License-Identifier: LGPL-2.1-or-later # # Copyright 2020 Canonical Ltd # # Authors: # Marco Trevisan import argparse import importlib.util import inspect import os import unittest def list_tests(module): tests = [] for name, obj in inspect.getmembers(module): if inspect.isclass(obj) and issubclass(obj, unittest.TestCase): cases = unittest.defaultTestLoader.getTestCaseNames(obj) tests += [(obj, f"{name}.{t}") for t in cases] return tests if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("unittest_source", type=argparse.FileType("r")) args = parser.parse_args() source_path = args.unittest_source.name spec = importlib.util.spec_from_file_location( os.path.basename(source_path), source_path ) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) for machine, human in list_tests(module): print(human) fwupd-2.0.10/libfwupd/000077500000000000000000000000001501337203100145265ustar00rootroot00000000000000fwupd-2.0.10/libfwupd/README.md000066400000000000000000000101141501337203100160020ustar00rootroot00000000000000# libfwupd ## Planned API/ABI changes for next release * Typedef `FwupdFeatureFlags` to `guint64` so it's the same size on all platforms * Remove `FWUPD_INSTALL_FLAG_IGNORE_CHECKSUM` * Remove `FWUPD_INSTALL_FLAG_IGNORE_VID_PID` * Remove `FWUPD_INSTALL_FLAG_NO_SEARCH` ## Migration from Version 2.0.0 * Migrate from `fu_firmware_parse_full()` to `fu_firmware_parse_bytes()` * Migrate from `fu_firmware_parse()` to `fu_firmware_parse_bytes()` by adding an offset of 0x0 ## Migration from Version 1.9.x * Migrate from `fwupd_build_machine_id()` to `fwupd_client_get_host_machine_id()` * Migrate from `fwupd_build_history_report_json()` to `fwupd_client_build_report_history()` * Migrate from `fwupd_get_os_release()` to `fwupd_client_get_report_metadata()` * Convert `fwupd_client_emulation_load()` to take a readable file rather than taking bytes * Convert `fwupd_client_emulation_save()` to take a writable file rather than returning bytes * Drop use of FWUPD_DEVICE_FLAG_ONLY_OFFLINE * Drop use of `fwupd_device_get_update_message()` and `fwupd_device_get_update_image()` ## Migration from Version 1.x.x * Add section to `fwupd_client_modify_config()` * Migrate from `fwupd_client_install_release2()` to `fwupd_client_install_release()` * Migrate from `fwupd_client_install_release2_async()` to `fwupd_client_install_release_async()` * Migrate from `fwupd_client_refresh_remote2()` to `fwupd_client_refresh_remote()` * Migrate from `fwupd_client_refresh_remote2_async()` to `fwupd_client_refresh_remote_async()` * Migrate from `fwupd_remote_get_enabled()` to `fwupd_remote_has_flag` * Migrate from `fwupd_remote_get_approval_required()` to `fwupd_remote_has_flag` * Migrate from `fwupd_remote_get_automatic_reports()` to `fwupd_remote_has_flag` * Migrate from `fwupd_remote_get_automatic_security_reports()` to `fwupd_remote_has_flag` * Migrate from `fwupd_device_get_protocol()` to `fwupd_device_get_protocols()` * Migrate from `fwupd_device_get_vendor_id()` to `fwupd_device_get_vendor_ids()` * Migrate from `fwupd_remote_set_checksum()` to `fwupd_remote_set_checksum_sig()` * Migrate from `fwupd_device_set_protocol()` to `fwupd_device_add_protocol()` * Migrate from `fwupd_device_set_vendor_id()` to `fwupd_device_add_vendor_id()` * Migrate from `fwupd_release_get_trust_flags()` to `fwupd_release_get_flags()` * Migrate from `fwupd_release_get_uri()` to `fwupd_release_get_locations()` * Migrate from `fwupd_release_set_trust_flags()` to `fwupd_release_set_flags()` * Migrate from `fwupd_release_set_uri()` to `fwupd_release_add_location()` * Remove use of flags like `FWUPD_DEVICE_FLAG_MD_SET_ICON` * Remove use of flags `FWUPD_INSTALL_FLAG_IGNORE_POWER` * Remove the `soup-session` property in `FwupdClient`. * Rename the flag `FWUPD_CLIENT_DOWNLOAD_FLAG_ONLY_IPFS` to `_ONLY_P2P` * Rename the SMAP attribute from `FWUPD_SECURITY_ATTR_ID_INTEL_SMAP` to `FWUPD_SECURITY_ATTR_ID_SMAP` * Rename the CET enabled attribute from `FWUPD_SECURITY_ATTR_ID_INTEL_CET_ENABLED` to `FWUPD_SECURITY_ATTR_ID_CET_ENABLED` * Rename the CET active attribute `FWUPD_SECURITY_ATTR_ID_INTEL_CET_ACTIVE` to `FWUPD_SECURITY_ATTR_ID_CET_ACTIVE` ## Migration from Version 0.9.x * Rename FU_DEVICE_FLAG -> FWUPD_DEVICE_FLAG * Rename FWUPD_DEVICE_FLAG_ALLOW_ONLINE -> FWUPD_DEVICE_FLAG_UPDATABLE * Rename FWUPD_DEVICE_FLAG_ALLOW_OFFLINE -> FWUPD_DEVICE_FLAG_ONLY_OFFLINE * Rename fwupd_client_get_devices_simple -> fwupd_client_get_devices * Rename fwupd_client_get_details_local -> fwupd_client_get_details * Rename fwupd_client_update_metadata_with_id -> fwupd_client_update_metadata * Rename fwupd_remote_get_uri -> fwupd_remote_get_metadata_uri * Rename fwupd_remote_get_uri_asc -> fwupd_remote_get_metadata_uri_sig * Rename fwupd_remote_build_uri -> fwupd_remote_build_firmware_uri * Switch FWUPD_RESULT_KEY_DEVICE_CHECKSUM_KIND to fwupd_checksum_guess_kind() * Rename fwupd_result_update_*() to fwupd_release_*() * Rename fwupd_result_*() to fwupd_device_*() * Convert FwupdResult to FwupdDevice in all callbacks * Rename fwupd_device_**provider -> fwupd_device**_plugin * Convert hash types sa{sv} -> a{sv} * Convert fwupd_client_get_updates() -> fwupd_client_get_upgrades() fwupd-2.0.10/libfwupd/fwupd-bios-setting.c000066400000000000000000000756711501337203100204440ustar00rootroot00000000000000/* * Copyright 2022 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fwupd-bios-setting.h" #include "fwupd-codec.h" #include "fwupd-common-private.h" #include "fwupd-enums-private.h" #include "fwupd-error.h" /** * FwupdBiosSetting: * * A BIOS setting that represents a setting in the firmware. */ static void fwupd_bios_setting_finalize(GObject *object); typedef struct { FwupdBiosSettingKind kind; gchar *id; gchar *name; gchar *description; gchar *path; gchar *current_value; guint64 lower_bound; guint64 upper_bound; guint64 scalar_increment; gboolean read_only; GPtrArray *possible_values; } FwupdBiosSettingPrivate; static void fwupd_bios_setting_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_EXTENDED(FwupdBiosSetting, fwupd_bios_setting, G_TYPE_OBJECT, 0, G_ADD_PRIVATE(FwupdBiosSetting) G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fwupd_bios_setting_codec_iface_init)); #define GET_PRIVATE(o) (fwupd_bios_setting_get_instance_private(o)) /** * fwupd_bios_setting_get_id * @self: a #FwupdBiosSetting * * Gets the unique attribute identifier for this attribute/driver * * Returns: attribute ID if set otherwise NULL * * Since: 1.8.4 **/ const gchar * fwupd_bios_setting_get_id(FwupdBiosSetting *self) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_BIOS_SETTING(self), NULL); return priv->id; } /** * fwupd_bios_setting_set_id * @self: a #FwupdBiosSetting * * Sets the unique attribute identifier for this attribute * * Since: 1.8.4 **/ void fwupd_bios_setting_set_id(FwupdBiosSetting *self, const gchar *id) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_BIOS_SETTING(self)); /* not changed */ if (g_strcmp0(priv->id, id) == 0) return; g_free(priv->id); priv->id = g_strdup(id); } /** * fwupd_bios_setting_get_read_only: * @self: a #FwupdBiosSetting * * Determines if a BIOS setting is read only * * Returns: gboolean * * Since: 1.8.4 **/ gboolean fwupd_bios_setting_get_read_only(FwupdBiosSetting *self) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_BIOS_SETTING(self), FALSE); return priv->read_only; } /** * fwupd_bios_setting_set_read_only: * @self: a #FwupdBiosSetting * * Configures whether an attribute is read only * maximum length for string attributes. * * * Since: 1.8.4 **/ void fwupd_bios_setting_set_read_only(FwupdBiosSetting *self, gboolean val) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_BIOS_SETTING(self)); priv->read_only = val; } /** * fwupd_bios_setting_get_lower_bound: * @self: a #FwupdBiosSetting * * Gets the lower bound for integer attributes or * minimum length for string attributes. * * Returns: guint64 * * Since: 1.8.4 **/ guint64 fwupd_bios_setting_get_lower_bound(FwupdBiosSetting *self) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_BIOS_SETTING(self), 0); return priv->lower_bound; } /** * fwupd_bios_setting_get_upper_bound: * @self: a #FwupdBiosSetting * * Gets the upper bound for integer attributes or * maximum length for string attributes. * * Returns: guint64 * * Since: 1.8.4 **/ guint64 fwupd_bios_setting_get_upper_bound(FwupdBiosSetting *self) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_BIOS_SETTING(self), 0); return priv->upper_bound; } /** * fwupd_bios_setting_get_scalar_increment: * @self: a #FwupdBiosSetting * * Gets the scalar increment used for integer attributes. * * Returns: guint64 * * Since: 1.8.4 **/ guint64 fwupd_bios_setting_get_scalar_increment(FwupdBiosSetting *self) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_BIOS_SETTING(self), 0); return priv->scalar_increment; } /** * fwupd_bios_setting_set_upper_bound: * @self: a #FwupdBiosSetting * @val: a guint64 value to set bound to * * Sets the upper bound used for BIOS integer attributes or max * length for string attributes. * * Since: 1.8.4 **/ void fwupd_bios_setting_set_upper_bound(FwupdBiosSetting *self, guint64 val) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_BIOS_SETTING(self)); priv->upper_bound = val; } /** * fwupd_bios_setting_set_lower_bound: * @self: a #FwupdBiosSetting * @val: a guint64 value to set bound to * * Sets the lower bound used for BIOS integer attributes or max * length for string attributes. * * Since: 1.8.4 **/ void fwupd_bios_setting_set_lower_bound(FwupdBiosSetting *self, guint64 val) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_BIOS_SETTING(self)); priv->lower_bound = val; } /** * fwupd_bios_setting_set_scalar_increment: * @self: a #FwupdBiosSetting * @val: a guint64 value to set increment to * * Sets the scalar increment used for BIOS integer attributes. * * Since: 1.8.4 **/ void fwupd_bios_setting_set_scalar_increment(FwupdBiosSetting *self, guint64 val) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_BIOS_SETTING(self)); priv->scalar_increment = val; } /** * fwupd_bios_setting_get_kind: * @self: a #FwupdBiosSetting * * Gets the BIOS setting type used by the kernel interface. * * Returns: the bios setting type, or %FWUPD_BIOS_SETTING_KIND_UNKNOWN if unset. * * Since: 1.8.4 **/ FwupdBiosSettingKind fwupd_bios_setting_get_kind(FwupdBiosSetting *self) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_BIOS_SETTING(self), 0); return priv->kind; } /** * fwupd_bios_setting_set_kind: * @self: a #FwupdBiosSetting * @type: a bios setting type, e.g. %FWUPD_BIOS_SETTING_KIND_ENUMERATION * * Sets the BIOS setting type used by the kernel interface. * * Since: 1.8.4 **/ void fwupd_bios_setting_set_kind(FwupdBiosSetting *self, FwupdBiosSettingKind type) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_BIOS_SETTING(self)); priv->kind = type; } /** * fwupd_bios_setting_set_name: * @self: a #FwupdBiosSetting * @name: (nullable): the attribute name * * Sets the attribute name provided by a kernel driver. * * Since: 1.8.4 **/ void fwupd_bios_setting_set_name(FwupdBiosSetting *self, const gchar *name) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_BIOS_SETTING(self)); /* not changed */ if (g_strcmp0(priv->name, name) == 0) return; g_free(priv->name); priv->name = g_strdup(name); } /** * fwupd_bios_setting_set_path: * @self: a #FwupdBiosSetting * @path: (nullable): the path the driver providing the attribute uses * * Sets path to the attribute. * * Since: 1.8.4 **/ void fwupd_bios_setting_set_path(FwupdBiosSetting *self, const gchar *path) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_BIOS_SETTING(self)); /* not changed */ if (g_strcmp0(priv->path, path) == 0) return; g_free(priv->path); priv->path = g_strdup(path); } /** * fwupd_bios_setting_set_description: * @self: a #FwupdBiosSetting * @description: (nullable): the attribute description * * Sets the attribute description. * * Since: 1.8.4 **/ void fwupd_bios_setting_set_description(FwupdBiosSetting *self, const gchar *description) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_BIOS_SETTING(self)); /* not changed */ if (g_strcmp0(priv->description, description) == 0) return; g_free(priv->description); priv->description = g_strdup(description); } /* determine if key is supposed to be positive */ static gboolean fwupd_bios_setting_key_is_positive(const gchar *key) { if (g_strrstr(key, "enable")) return TRUE; if (g_strcmp0(key, "true") == 0) return TRUE; if (g_strcmp0(key, "1") == 0) return TRUE; if (g_strcmp0(key, "on") == 0) return TRUE; return FALSE; } /* determine if key is supposed to be negative */ static gboolean fwupd_bios_setting_key_is_negative(const gchar *key) { if (g_strrstr(key, "disable")) return TRUE; if (g_strcmp0(key, "false") == 0) return TRUE; if (g_strcmp0(key, "0") == 0) return TRUE; if (g_strcmp0(key, "off") == 0) return TRUE; return FALSE; } /** * fwupd_bios_setting_map_possible_value: * @self: a #FwupdBiosSetting * @key: the string to try to map * @error: (nullable): optional return location for an error * * Attempts to map a user provided string into strings that a #FwupdBiosSetting can * support. The following heuristics are used: * - Ignore case sensitivity * - Map obviously "positive" phrases into a value that turns on the #FwupdBiosSetting * - Map obviously "negative" phrases into a value that turns off the #FwupdBiosSetting * * Returns: (transfer none): the possible value that maps or NULL if none if found * * Since: 1.8.4 **/ const gchar * fwupd_bios_setting_map_possible_value(FwupdBiosSetting *self, const gchar *key, GError **error) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); gboolean positive_key = FALSE; gboolean negative_key = FALSE; g_autofree gchar *lower_key = NULL; g_return_val_if_fail(FWUPD_IS_BIOS_SETTING(self), NULL); g_return_val_if_fail(priv->kind == FWUPD_BIOS_SETTING_KIND_ENUMERATION, NULL); if (priv->possible_values->len == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "%s doesn't contain any possible values", priv->name); return NULL; } lower_key = g_utf8_strdown(key, -1); positive_key = fwupd_bios_setting_key_is_positive(lower_key); negative_key = fwupd_bios_setting_key_is_negative(lower_key); for (guint i = 0; i < priv->possible_values->len; i++) { const gchar *possible = g_ptr_array_index(priv->possible_values, i); g_autofree gchar *lower_possible = g_utf8_strdown(possible, -1); gboolean positive_possible; gboolean negative_possible; /* perfect match */ if (g_strcmp0(lower_possible, lower_key) == 0) return possible; /* fuzzy match */ positive_possible = fwupd_bios_setting_key_is_positive(lower_possible); negative_possible = fwupd_bios_setting_key_is_negative(lower_possible); if ((positive_possible && positive_key) || (negative_possible && negative_key)) return possible; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "%s doesn't map to any possible values for %s", key, priv->name); return NULL; } /** * fwupd_bios_setting_has_possible_value: * @self: a #FwupdBiosSetting * @val: the possible value string * * Finds out if a specific possible value was added to the attribute. * * Returns: %TRUE if the self matches. * * Since: 1.8.4 **/ gboolean fwupd_bios_setting_has_possible_value(FwupdBiosSetting *self, const gchar *val) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_BIOS_SETTING(self), FALSE); g_return_val_if_fail(val != NULL, FALSE); if (priv->possible_values->len == 0) return TRUE; for (guint i = 0; i < priv->possible_values->len; i++) { const gchar *tmp = g_ptr_array_index(priv->possible_values, i); if (g_strcmp0(tmp, val) == 0) return TRUE; } return FALSE; } /** * fwupd_bios_setting_add_possible_value: * @self: a #FwupdBiosSetting * @possible_value: the possible * * Adds a possible value to the attribute. This indicates one of the values the * kernel driver will accept from userspace. * * Since: 1.8.4 **/ void fwupd_bios_setting_add_possible_value(FwupdBiosSetting *self, const gchar *possible_value) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_BIOS_SETTING(self)); if (priv->possible_values->len > 0 && fwupd_bios_setting_has_possible_value(self, possible_value)) return; g_ptr_array_add(priv->possible_values, g_strdup(possible_value)); } /** * fwupd_bios_setting_get_possible_values: * @self: a #FwupdBiosSetting * * Find all possible values for an enumeration attribute. * * Returns: (transfer container) (element-type gchar*): all possible values. * * Since: 1.8.4 **/ GPtrArray * fwupd_bios_setting_get_possible_values(FwupdBiosSetting *self) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_BIOS_SETTING(self), NULL); g_return_val_if_fail(priv->kind == FWUPD_BIOS_SETTING_KIND_ENUMERATION, NULL); return priv->possible_values; } /** * fwupd_bios_setting_get_name: * @self: a #FwupdBiosSetting * * Gets the attribute name. * * Returns: the attribute name, or %NULL if unset. * * Since: 1.8.4 **/ const gchar * fwupd_bios_setting_get_name(FwupdBiosSetting *self) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_BIOS_SETTING(self), NULL); return priv->name; } /** * fwupd_bios_setting_get_path: * @self: a #FwupdBiosSetting * * Gets the path for the driver providing the attribute. * * Returns: (nullable): the driver, or %NULL if unfound. * * Since: 1.8.4 **/ const gchar * fwupd_bios_setting_get_path(FwupdBiosSetting *self) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_BIOS_SETTING(self), NULL); return priv->path; } /** * fwupd_bios_setting_get_description: * @self: a #FwupdBiosSetting * * Gets the attribute description which is provided by some drivers to explain * what they change. * * Returns: the attribute description, or %NULL if unset. * * Since: 1.8.4 **/ const gchar * fwupd_bios_setting_get_description(FwupdBiosSetting *self) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_BIOS_SETTING(self), NULL); return priv->description; } /** * fwupd_bios_setting_get_current_value: * @self: a #FwupdBiosSetting * * Gets the string representation of the current_value stored in an attribute * from the kernel. This value is cached; so changing it outside of fwupd may * may put it out of sync. * * Returns: the current value of the attribute. * * Since: 1.8.4 **/ const gchar * fwupd_bios_setting_get_current_value(FwupdBiosSetting *self) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_BIOS_SETTING(self), NULL); return priv->current_value; } /** * fwupd_bios_setting_set_current_value: * @self: a #FwupdBiosSetting * @value: (nullable): The string to set an attribute to * * Sets the string stored in an attribute. * This doesn't change the representation in the kernel. * * Since: 1.8.4 **/ void fwupd_bios_setting_set_current_value(FwupdBiosSetting *self, const gchar *value) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); /* not changed */ if (g_strcmp0(priv->current_value, value) == 0) return; g_free(priv->current_value); priv->current_value = g_strdup(value); } static gboolean _fu_strtoull_simple(const gchar *str, guint64 *value, GError **error) { gchar *endptr = NULL; guint base = 10; /* convert */ if (g_str_has_prefix(str, "0x")) { str += 2; base = 16; } *value = g_ascii_strtoull(str, &endptr, base); /* nocheck:blocked */ if ((gsize)(endptr - str) != strlen(str) && *endptr != '\n') { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "cannot parse %s", str); return FALSE; } return TRUE; } static gboolean fwupd_bios_setting_validate_value(FwupdBiosSetting *self, const gchar *value, GError **error) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); if (priv->kind == FWUPD_BIOS_SETTING_KIND_INTEGER) { guint64 tmp = 0; if (!_fu_strtoull_simple(value, &tmp, error)) return FALSE; if (tmp < priv->lower_bound) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "%s is too small (%" G_GUINT64_FORMAT "); expected at least %" G_GUINT64_FORMAT, value, tmp, priv->lower_bound); return FALSE; } if (tmp > priv->upper_bound) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "%s is too big (%" G_GUINT64_FORMAT "); expected no more than %" G_GUINT64_FORMAT, value, tmp, priv->upper_bound); return FALSE; } return TRUE; } if (priv->kind == FWUPD_BIOS_SETTING_KIND_STRING) { gsize tmp = strlen(value); if (tmp < priv->lower_bound) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "%s is too short (%" G_GSIZE_FORMAT "); expected at least %" G_GUINT64_FORMAT, value, tmp, priv->lower_bound); return FALSE; } if (tmp > priv->upper_bound) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "%s is too long (%" G_GSIZE_FORMAT "); expected no more than %" G_GUINT64_FORMAT, value, tmp, priv->upper_bound); return FALSE; } return TRUE; } if (priv->kind == FWUPD_BIOS_SETTING_KIND_ENUMERATION) return TRUE; /* not supported */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unknown attribute type"); return FALSE; } /** * fwupd_bios_setting_write_value: * @self: a #FwupdBiosSetting * @value: (not nullable): The string to write * @error: (nullable): optional return location for an error * * Writes a new value into the setting if it is different from the current value. * * NOTE: A subclass should handle the `->write_value()` vfunc and actually write the value to the * firmware. * * Returns: %TRUE for success * * Since: 1.9.4 **/ gboolean fwupd_bios_setting_write_value(FwupdBiosSetting *self, const gchar *value, GError **error) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); FwupdBiosSettingClass *klass = FWUPD_BIOS_SETTING_GET_CLASS(self); g_return_val_if_fail(FWUPD_IS_BIOS_SETTING(self), FALSE); g_return_val_if_fail(value != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* not changed */ if (g_strcmp0(priv->current_value, value) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "%s is already set to %s", priv->id, value); return FALSE; } /* sanity check */ if (fwupd_bios_setting_get_read_only(self)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "%s is read only", priv->name); return FALSE; } /* convert the value */ if (priv->kind == FWUPD_BIOS_SETTING_KIND_ENUMERATION) { value = fwupd_bios_setting_map_possible_value(self, value, error); if (value == NULL) return FALSE; } /* also done by the kernel or firmware, doing it here too allows for better errors */ if (!fwupd_bios_setting_validate_value(self, value, error)) return FALSE; /* not implemented */ if (klass->write_value == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported"); return FALSE; } /* proxy */ return klass->write_value(self, value, error); } static gboolean fwupd_bios_setting_trusted(FwupdBiosSetting *self, gboolean trusted) { g_return_val_if_fail(FWUPD_IS_BIOS_SETTING(self), FALSE); if (trusted) return TRUE; if (g_strcmp0(fwupd_bios_setting_get_name(self), "pending_reboot") == 0) return TRUE; return FALSE; } static void fwupd_bios_setting_add_variant(FwupdCodec *codec, GVariantBuilder *builder, FwupdCodecFlags flags) { FwupdBiosSetting *self = FWUPD_BIOS_SETTING(codec); FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_BIOS_SETTING_TYPE, g_variant_new_uint64(priv->kind)); if (priv->id != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_BIOS_SETTING_ID, g_variant_new_string(priv->id)); } if (priv->name != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_NAME, g_variant_new_string(priv->name)); } if (priv->path != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_FILENAME, g_variant_new_string(priv->path)); } if (priv->description != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_DESCRIPTION, g_variant_new_string(priv->description)); } g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_BIOS_SETTING_READ_ONLY, g_variant_new_boolean(priv->read_only)); if (priv->current_value != NULL && fwupd_bios_setting_trusted(self, flags & FWUPD_CODEC_FLAG_TRUSTED)) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_BIOS_SETTING_CURRENT_VALUE, g_variant_new_string(priv->current_value)); } if (priv->kind == FWUPD_BIOS_SETTING_KIND_INTEGER || priv->kind == FWUPD_BIOS_SETTING_KIND_STRING) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_BIOS_SETTING_LOWER_BOUND, g_variant_new_uint64(priv->lower_bound)); g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_BIOS_SETTING_UPPER_BOUND, g_variant_new_uint64(priv->upper_bound)); if (priv->kind == FWUPD_BIOS_SETTING_KIND_INTEGER) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_BIOS_SETTING_SCALAR_INCREMENT, g_variant_new_uint64(priv->scalar_increment)); } } else if (priv->kind == FWUPD_BIOS_SETTING_KIND_ENUMERATION) { if (priv->possible_values->len > 0) { g_autofree const gchar **strv = g_new0(const gchar *, priv->possible_values->len + 1); for (guint i = 0; i < priv->possible_values->len; i++) strv[i] = (const gchar *)g_ptr_array_index(priv->possible_values, i); g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_BIOS_SETTING_POSSIBLE_VALUES, g_variant_new_strv(strv, -1)); } } } static void fwupd_bios_setting_from_key_value(FwupdBiosSetting *self, const gchar *key, GVariant *value) { if (g_strcmp0(key, FWUPD_RESULT_KEY_BIOS_SETTING_TYPE) == 0) { fwupd_bios_setting_set_kind(self, g_variant_get_uint64(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_BIOS_SETTING_ID) == 0) { fwupd_bios_setting_set_id(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_NAME) == 0) { fwupd_bios_setting_set_name(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_FILENAME) == 0) { fwupd_bios_setting_set_path(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_BIOS_SETTING_CURRENT_VALUE) == 0) { fwupd_bios_setting_set_current_value(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_DESCRIPTION) == 0) { fwupd_bios_setting_set_description(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_BIOS_SETTING_POSSIBLE_VALUES) == 0) { g_autofree const gchar **strv = g_variant_get_strv(value, NULL); for (guint i = 0; strv[i] != NULL; i++) fwupd_bios_setting_add_possible_value(self, strv[i]); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_BIOS_SETTING_LOWER_BOUND) == 0) { fwupd_bios_setting_set_lower_bound(self, g_variant_get_uint64(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_BIOS_SETTING_UPPER_BOUND) == 0) { fwupd_bios_setting_set_upper_bound(self, g_variant_get_uint64(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_BIOS_SETTING_SCALAR_INCREMENT) == 0) { fwupd_bios_setting_set_scalar_increment(self, g_variant_get_uint64(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_BIOS_SETTING_READ_ONLY) == 0) { fwupd_bios_setting_set_read_only(self, g_variant_get_boolean(value)); return; } } static gboolean fwupd_bios_setting_from_json(FwupdCodec *codec, JsonNode *json_node, GError **error) { FwupdBiosSetting *self = FWUPD_BIOS_SETTING(codec); JsonObject *obj; /* sanity check */ if (!JSON_NODE_HOLDS_OBJECT(json_node)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not JSON object"); return FALSE; } obj = json_node_get_object(json_node); fwupd_bios_setting_set_kind( self, json_object_get_int_member_with_default(obj, FWUPD_RESULT_KEY_BIOS_SETTING_TYPE, 0)); fwupd_bios_setting_set_id( self, json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_BIOS_SETTING_ID, NULL)); fwupd_bios_setting_set_name( self, json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_NAME, NULL)); fwupd_bios_setting_set_description( self, json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_DESCRIPTION, NULL)); fwupd_bios_setting_set_path( self, json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_FILENAME, NULL)); fwupd_bios_setting_set_current_value( self, json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_BIOS_SETTING_CURRENT_VALUE, NULL)); if (json_object_has_member(obj, FWUPD_RESULT_KEY_BIOS_SETTING_POSSIBLE_VALUES)) { JsonArray *array = json_object_get_array_member(obj, FWUPD_RESULT_KEY_BIOS_SETTING_POSSIBLE_VALUES); for (guint i = 0; i < json_array_get_length(array); i++) { const gchar *tmp = json_array_get_string_element(array, i); fwupd_bios_setting_add_possible_value(self, tmp); } } fwupd_bios_setting_set_lower_bound( self, json_object_get_int_member_with_default(obj, FWUPD_RESULT_KEY_BIOS_SETTING_LOWER_BOUND, 0)); fwupd_bios_setting_set_upper_bound( self, json_object_get_int_member_with_default(obj, FWUPD_RESULT_KEY_BIOS_SETTING_UPPER_BOUND, 0)); fwupd_bios_setting_set_scalar_increment( self, json_object_get_int_member_with_default(obj, FWUPD_RESULT_KEY_BIOS_SETTING_SCALAR_INCREMENT, 0)); fwupd_bios_setting_set_read_only( self, json_object_get_boolean_member_with_default(obj, FWUPD_RESULT_KEY_BIOS_SETTING_READ_ONLY, FALSE)); /* success */ return TRUE; } static void fwupd_bios_setting_add_json(FwupdCodec *codec, JsonBuilder *builder, FwupdCodecFlags flags) { FwupdBiosSetting *self = FWUPD_BIOS_SETTING(codec); FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_NAME, priv->name); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_DESCRIPTION, priv->description); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_FILENAME, priv->path); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_BIOS_SETTING_ID, priv->id); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_BIOS_SETTING_CURRENT_VALUE, priv->current_value); fwupd_codec_json_append_bool(builder, FWUPD_RESULT_KEY_BIOS_SETTING_READ_ONLY, priv->read_only); fwupd_codec_json_append_int(builder, FWUPD_RESULT_KEY_BIOS_SETTING_TYPE, priv->kind); if (priv->kind == FWUPD_BIOS_SETTING_KIND_ENUMERATION) { if (priv->possible_values->len > 0) { json_builder_set_member_name(builder, FWUPD_RESULT_KEY_BIOS_SETTING_POSSIBLE_VALUES); json_builder_begin_array(builder); for (guint i = 0; i < priv->possible_values->len; i++) { const gchar *tmp = g_ptr_array_index(priv->possible_values, i); json_builder_add_string_value(builder, tmp); } json_builder_end_array(builder); } } if (priv->kind == FWUPD_BIOS_SETTING_KIND_INTEGER || priv->kind == FWUPD_BIOS_SETTING_KIND_STRING) { fwupd_codec_json_append_int(builder, FWUPD_RESULT_KEY_BIOS_SETTING_LOWER_BOUND, priv->lower_bound); fwupd_codec_json_append_int(builder, FWUPD_RESULT_KEY_BIOS_SETTING_UPPER_BOUND, priv->upper_bound); if (priv->kind == FWUPD_BIOS_SETTING_KIND_INTEGER) { fwupd_codec_json_append_int(builder, FWUPD_RESULT_KEY_BIOS_SETTING_SCALAR_INCREMENT, priv->scalar_increment); } } } static void fwupd_bios_setting_add_string(FwupdCodec *codec, guint idt, GString *str) { FwupdBiosSetting *self = FWUPD_BIOS_SETTING(codec); FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_NAME, priv->name); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_BIOS_SETTING_ID, priv->id); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_DESCRIPTION, priv->description); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_FILENAME, priv->path); fwupd_codec_string_append_int(str, idt, FWUPD_RESULT_KEY_BIOS_SETTING_TYPE, priv->kind); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_BIOS_SETTING_CURRENT_VALUE, priv->current_value); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_BIOS_SETTING_READ_ONLY, priv->read_only ? "True" : "False"); if (priv->kind == FWUPD_BIOS_SETTING_KIND_ENUMERATION) { for (guint i = 0; i < priv->possible_values->len; i++) { const gchar *tmp = g_ptr_array_index(priv->possible_values, i); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_BIOS_SETTING_POSSIBLE_VALUES, tmp); } } if (priv->kind == FWUPD_BIOS_SETTING_KIND_INTEGER || priv->kind == FWUPD_BIOS_SETTING_KIND_STRING) { fwupd_codec_string_append_int(str, idt, FWUPD_RESULT_KEY_BIOS_SETTING_LOWER_BOUND, priv->lower_bound); fwupd_codec_string_append_int(str, idt, FWUPD_RESULT_KEY_BIOS_SETTING_UPPER_BOUND, priv->upper_bound); if (priv->kind == FWUPD_BIOS_SETTING_KIND_INTEGER) { fwupd_codec_string_append_int( str, idt, FWUPD_RESULT_KEY_BIOS_SETTING_SCALAR_INCREMENT, priv->scalar_increment); } } } static void fwupd_bios_setting_class_init(FwupdBiosSettingClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fwupd_bios_setting_finalize; } static void fwupd_bios_setting_init(FwupdBiosSetting *self) { FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); priv->possible_values = g_ptr_array_new_with_free_func(g_free); } static void fwupd_bios_setting_finalize(GObject *object) { FwupdBiosSetting *self = FWUPD_BIOS_SETTING(object); FwupdBiosSettingPrivate *priv = GET_PRIVATE(self); g_free(priv->current_value); g_free(priv->id); g_free(priv->name); g_free(priv->description); g_free(priv->path); g_ptr_array_unref(priv->possible_values); G_OBJECT_CLASS(fwupd_bios_setting_parent_class)->finalize(object); } static void fwupd_bios_setting_from_variant_iter(FwupdCodec *codec, GVariantIter *iter) { FwupdBiosSetting *self = FWUPD_BIOS_SETTING(codec); GVariant *value; const gchar *key; while (g_variant_iter_next(iter, "{&sv}", &key, &value)) { fwupd_bios_setting_from_key_value(self, key, value); g_variant_unref(value); } } static void fwupd_bios_setting_codec_iface_init(FwupdCodecInterface *iface) { iface->add_string = fwupd_bios_setting_add_string; iface->add_json = fwupd_bios_setting_add_json; iface->from_json = fwupd_bios_setting_from_json; iface->add_variant = fwupd_bios_setting_add_variant; iface->from_variant_iter = fwupd_bios_setting_from_variant_iter; } /** * fwupd_bios_setting_new: * @name: (nullable): the attribute name * @path: (nullable): the path the driver providing this attribute uses * * Creates a new bios setting. * * Returns: a new #FwupdBiosSetting. * * Since: 1.8.4 **/ FwupdBiosSetting * fwupd_bios_setting_new(const gchar *name, const gchar *path) { FwupdBiosSetting *self; self = g_object_new(FWUPD_TYPE_BIOS_SETTING, NULL); if (name != NULL) fwupd_bios_setting_set_name(self, name); if (path != NULL) fwupd_bios_setting_set_path(self, path); return FWUPD_BIOS_SETTING(self); } fwupd-2.0.10/libfwupd/fwupd-bios-setting.h000066400000000000000000000104451501337203100204350ustar00rootroot00000000000000/* * Copyright 2022 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fwupd-build.h" #include "fwupd-enums.h" G_BEGIN_DECLS #define FWUPD_TYPE_BIOS_SETTING (fwupd_bios_setting_get_type()) G_DECLARE_DERIVABLE_TYPE(FwupdBiosSetting, fwupd_bios_setting, FWUPD, BIOS_SETTING, GObject) struct _FwupdBiosSettingClass { GObjectClass parent_class; gboolean (*write_value)(FwupdBiosSetting *self, const gchar *value, GError **error); /*< private >*/ void (*_fwupd_reserved2)(void); void (*_fwupd_reserved3)(void); void (*_fwupd_reserved4)(void); void (*_fwupd_reserved5)(void); void (*_fwupd_reserved6)(void); void (*_fwupd_reserved7)(void); }; /* special attributes */ #define FWUPD_BIOS_SETTING_PENDING_REBOOT "pending_reboot" #define FWUPD_BIOS_SETTING_RESET_BIOS "reset_bios" #define FWUPD_BIOS_SETTING_DEBUG_CMD "debug_cmd" #define FWUPD_BIOS_SETTING_SELF_TEST "fwupd_self_test" /** * FwupdBiosSettingKind: * * The type of BIOS setting. **/ typedef enum { /** * FWUPD_BIOS_SETTING_KIND_UNKNOWN: * * BIOS setting type is unknown. * * Since: 1.8.4 */ FWUPD_BIOS_SETTING_KIND_UNKNOWN = 0, /** * FWUPD_BIOS_SETTING_KIND_ENUMERATION: * * BIOS setting that has enumerated possible values. * * Since: 1.8.4 */ FWUPD_BIOS_SETTING_KIND_ENUMERATION = 1, /** * FWUPD_BIOS_SETTING_KIND_INTEGER: * * BIOS setting that is an integer. * * Since: 1.8.4 */ FWUPD_BIOS_SETTING_KIND_INTEGER = 2, /** * FWUPD_BIOS_SETTING_KIND_STRING: * * BIOS setting that accepts a string. * * Since: 1.8.4 */ FWUPD_BIOS_SETTING_KIND_STRING = 3, /*< private >*/ FWUPD_BIOS_SETTING_KIND_LAST = 4 } FwupdBiosSettingKind; FwupdBiosSetting * fwupd_bios_setting_new(const gchar *name, const gchar *path); gboolean fwupd_bios_setting_get_read_only(FwupdBiosSetting *self) G_GNUC_NON_NULL(1); void fwupd_bios_setting_set_read_only(FwupdBiosSetting *self, gboolean val) G_GNUC_NON_NULL(1); guint64 fwupd_bios_setting_get_upper_bound(FwupdBiosSetting *self) G_GNUC_NON_NULL(1); guint64 fwupd_bios_setting_get_lower_bound(FwupdBiosSetting *self) G_GNUC_NON_NULL(1); guint64 fwupd_bios_setting_get_scalar_increment(FwupdBiosSetting *self) G_GNUC_NON_NULL(1); void fwupd_bios_setting_set_upper_bound(FwupdBiosSetting *self, guint64 val) G_GNUC_NON_NULL(1); void fwupd_bios_setting_set_lower_bound(FwupdBiosSetting *self, guint64 val) G_GNUC_NON_NULL(1); void fwupd_bios_setting_set_scalar_increment(FwupdBiosSetting *self, guint64 val) G_GNUC_NON_NULL(1); void fwupd_bios_setting_set_kind(FwupdBiosSetting *self, FwupdBiosSettingKind type) G_GNUC_NON_NULL(1); void fwupd_bios_setting_set_name(FwupdBiosSetting *self, const gchar *name) G_GNUC_NON_NULL(1); void fwupd_bios_setting_set_path(FwupdBiosSetting *self, const gchar *path) G_GNUC_NON_NULL(1); void fwupd_bios_setting_set_description(FwupdBiosSetting *self, const gchar *description) G_GNUC_NON_NULL(1); FwupdBiosSettingKind fwupd_bios_setting_get_kind(FwupdBiosSetting *self) G_GNUC_NON_NULL(1); const gchar * fwupd_bios_setting_get_name(FwupdBiosSetting *self) G_GNUC_NON_NULL(1); const gchar * fwupd_bios_setting_get_path(FwupdBiosSetting *self) G_GNUC_NON_NULL(1); const gchar * fwupd_bios_setting_get_description(FwupdBiosSetting *self) G_GNUC_NON_NULL(1); const gchar * fwupd_bios_setting_map_possible_value(FwupdBiosSetting *self, const gchar *key, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fwupd_bios_setting_has_possible_value(FwupdBiosSetting *self, const gchar *val) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_bios_setting_add_possible_value(FwupdBiosSetting *self, const gchar *possible_value) G_GNUC_NON_NULL(1, 2); GPtrArray * fwupd_bios_setting_get_possible_values(FwupdBiosSetting *self) G_GNUC_NON_NULL(1); const gchar * fwupd_bios_setting_get_current_value(FwupdBiosSetting *self) G_GNUC_NON_NULL(1); void fwupd_bios_setting_set_current_value(FwupdBiosSetting *self, const gchar *value) G_GNUC_NON_NULL(1); gboolean fwupd_bios_setting_write_value(FwupdBiosSetting *self, const gchar *value, GError **error) G_GNUC_NON_NULL(1, 2); const gchar * fwupd_bios_setting_get_id(FwupdBiosSetting *self) G_GNUC_NON_NULL(1); void fwupd_bios_setting_set_id(FwupdBiosSetting *self, const gchar *id) G_GNUC_NON_NULL(1); G_END_DECLS fwupd-2.0.10/libfwupd/fwupd-build.h000066400000000000000000000007601501337203100171240ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include /* see https://bugzilla.gnome.org/show_bug.cgi?id=113075 */ #ifndef G_GNUC_NON_NULL #if !defined(SUPPORTED_BUILD) && !defined(_WIN32) && (__GNUC__ > 3) || \ (__GNUC__ == 3 && __GNUC_MINOR__ >= 3) #define G_GNUC_NON_NULL(params...) __attribute__((nonnull(params))) #else #define G_GNUC_NON_NULL(params...) #endif #endif fwupd-2.0.10/libfwupd/fwupd-client-private.h000066400000000000000000000024641501337203100207560ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fwupd-client.h" #ifdef HAVE_GIO_UNIX #include #endif void fwupd_client_download_bytes2_async(FwupdClient *self, GPtrArray *urls, FwupdClientDownloadFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); #ifdef HAVE_GIO_UNIX void fwupd_client_get_details_stream_async(FwupdClient *self, GUnixInputStream *istr, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); void fwupd_client_install_stream_async(FwupdClient *self, const gchar *device_id, GUnixInputStream *istr, const gchar *filename_hint, FwupdInstallFlags install_flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2, 3); void fwupd_client_update_metadata_stream_async(FwupdClient *self, const gchar *remote_id, GUnixInputStream *istr, GUnixInputStream *istr_sig, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2, 3, 4); #endif fwupd-2.0.10/libfwupd/fwupd-client-sync.c000066400000000000000000002512341501337203100202540ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #ifdef HAVE_GIO_UNIX #include #endif #include "fwupd-client-private.h" #include "fwupd-client-sync.h" #include "fwupd-client.h" #include "fwupd-common-private.h" #include "fwupd-error.h" typedef struct { gboolean ret; gchar *str; GError *error; GPtrArray *array; GMainContext *context; GMainLoop *loop; GVariant *val; GHashTable *hash; GBytes *bytes; FwupdDevice *device; } FwupdClientHelper; static void fwupd_client_helper_free(FwupdClientHelper *helper) { if (helper->val != NULL) g_variant_unref(helper->val); if (helper->error != NULL) g_error_free(helper->error); if (helper->array != NULL) g_ptr_array_unref(helper->array); if (helper->hash != NULL) g_hash_table_unref(helper->hash); if (helper->bytes != NULL) g_bytes_unref(helper->bytes); if (helper->device != NULL) g_object_unref(helper->device); g_free(helper->str); g_main_loop_unref(helper->loop); g_main_context_unref(helper->context); g_main_context_pop_thread_default(helper->context); g_free(helper); } static FwupdClientHelper * fwupd_client_helper_new(FwupdClient *self) { FwupdClientHelper *helper; helper = g_new0(FwupdClientHelper, 1); helper->context = fwupd_client_get_main_context(self); helper->loop = g_main_loop_new(helper->context, FALSE); g_main_context_push_thread_default(helper->context); return helper; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTOPTR_CLEANUP_FUNC(FwupdClientHelper, fwupd_client_helper_free) #pragma clang diagnostic pop static void fwupd_client_connect_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_connect_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_connect: (skip) * @self: a #FwupdClient * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Sets up the client ready for use. Most other methods call this * for you, and do you only need to call this if you are just watching * the client. * * Returns: %TRUE for success * * Since: 0.7.1 **/ gboolean fwupd_client_connect(FwupdClient *self, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_connect_async(self, cancellable, fwupd_client_connect_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } static void fwupd_client_quit_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_quit_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_quit: (skip) * @self: a #FwupdClient * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Asks the daemon to quit. This can only be called by the root user. * * NOTE: This will only actually quit if an install is not already in progress. * * Returns: %TRUE for success * * Since: 1.8.11 **/ gboolean fwupd_client_quit(FwupdClient *self, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_quit_async(self, cancellable, fwupd_client_quit_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } static void fwupd_client_get_devices_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->array = fwupd_client_get_devices_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_get_devices: * @self: a #FwupdClient * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Gets all the devices registered with the daemon. * * Returns: (element-type FwupdDevice) (transfer container): results * * Since: 0.9.2 **/ GPtrArray * fwupd_client_get_devices(FwupdClient *self, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return NULL; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_get_devices_async(self, cancellable, fwupd_client_get_devices_cb, helper); g_main_loop_run(helper->loop); if (helper->array == NULL) { g_propagate_error(error, g_steal_pointer(&helper->error)); return NULL; } return g_steal_pointer(&helper->array); } static void fwupd_client_get_plugins_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->array = fwupd_client_get_plugins_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_get_plugins: * @self: a #FwupdClient * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Gets all the plugins being used the daemon. * * Returns: (element-type FwupdPlugin) (transfer container): results * * Since: 1.5.0 **/ GPtrArray * fwupd_client_get_plugins(FwupdClient *self, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return NULL; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_get_plugins_async(self, cancellable, fwupd_client_get_plugins_cb, helper); g_main_loop_run(helper->loop); if (helper->array == NULL) { g_propagate_error(error, g_steal_pointer(&helper->error)); return NULL; } return g_steal_pointer(&helper->array); } static void fwupd_client_get_history_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->array = fwupd_client_get_history_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_get_history: * @self: a #FwupdClient * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Gets all the history. * * Returns: (element-type FwupdDevice) (transfer container): results * * Since: 1.0.4 **/ GPtrArray * fwupd_client_get_history(FwupdClient *self, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return NULL; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_get_history_async(self, cancellable, fwupd_client_get_history_cb, helper); g_main_loop_run(helper->loop); if (helper->array == NULL) { g_propagate_error(error, g_steal_pointer(&helper->error)); return NULL; } return g_steal_pointer(&helper->array); } static void fwupd_client_get_releases_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->array = fwupd_client_get_releases_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_get_releases: * @self: a #FwupdClient * @device_id: (not nullable): the device ID * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Gets all the releases for a specific device * * Returns: (element-type FwupdRelease) (transfer container): results * * Since: 0.9.3 **/ GPtrArray * fwupd_client_get_releases(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(device_id != NULL, NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return NULL; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_get_releases_async(self, device_id, cancellable, fwupd_client_get_releases_cb, helper); g_main_loop_run(helper->loop); if (helper->array == NULL) { g_propagate_error(error, g_steal_pointer(&helper->error)); return NULL; } return g_steal_pointer(&helper->array); } static void fwupd_client_get_downgrades_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->array = fwupd_client_get_downgrades_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_get_downgrades: * @self: a #FwupdClient * @device_id: (not nullable): the device ID * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Gets all the downgrades for a specific device. * * Returns: (element-type FwupdRelease) (transfer container): results * * Since: 0.9.8 **/ GPtrArray * fwupd_client_get_downgrades(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(device_id != NULL, NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return NULL; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_get_downgrades_async(self, device_id, cancellable, fwupd_client_get_downgrades_cb, helper); g_main_loop_run(helper->loop); if (helper->array == NULL) { g_propagate_error(error, g_steal_pointer(&helper->error)); return NULL; } return g_steal_pointer(&helper->array); } static void fwupd_client_get_upgrades_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->array = fwupd_client_get_upgrades_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_get_upgrades: * @self: a #FwupdClient * @device_id: (not nullable): the device ID * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Gets all the upgrades for a specific device. * * Returns: (element-type FwupdRelease) (transfer container): results * * Since: 0.9.8 **/ GPtrArray * fwupd_client_get_upgrades(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(device_id != NULL, NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return NULL; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_get_upgrades_async(self, device_id, cancellable, fwupd_client_get_upgrades_cb, helper); g_main_loop_run(helper->loop); if (helper->array == NULL) { g_propagate_error(error, g_steal_pointer(&helper->error)); return NULL; } return g_steal_pointer(&helper->array); } static void fwupd_client_get_details_bytes_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->array = fwupd_client_get_details_bytes_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_get_details_bytes: * @self: a #FwupdClient * @bytes: the firmware archive * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Gets details about a specific firmware file. * * Returns: (transfer container) (element-type FwupdDevice): an array of results * * Since: 1.5.0 **/ GPtrArray * fwupd_client_get_details_bytes(FwupdClient *self, GBytes *bytes, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(bytes != NULL, NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return NULL; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_get_details_bytes_async(self, bytes, cancellable, fwupd_client_get_details_bytes_cb, helper); g_main_loop_run(helper->loop); if (helper->array == NULL) { g_propagate_error(error, g_steal_pointer(&helper->error)); return NULL; } return g_steal_pointer(&helper->array); } #ifdef HAVE_GIO_UNIX static void fwupd_client_get_details_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->array = fwupd_client_get_details_bytes_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } #endif /** * fwupd_client_get_details: * @self: a #FwupdClient * @filename: the firmware filename, e.g. `firmware.cab` * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Gets details about a specific firmware file. * * Returns: (transfer container) (element-type FwupdDevice): an array of results * * Since: 1.0.0 **/ GPtrArray * fwupd_client_get_details(FwupdClient *self, const gchar *filename, GCancellable *cancellable, GError **error) { #ifdef HAVE_GIO_UNIX g_autoptr(GUnixInputStream) istr = NULL; g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(filename != NULL, NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return NULL; /* call async version and run loop until complete */ istr = fwupd_unix_input_stream_from_fn(filename, error); if (istr == NULL) return NULL; helper = fwupd_client_helper_new(self); fwupd_client_get_details_stream_async(self, istr, cancellable, fwupd_client_get_details_cb, helper); g_main_loop_run(helper->loop); if (helper->array == NULL) { g_propagate_error(error, g_steal_pointer(&helper->error)); return NULL; } return g_steal_pointer(&helper->array); #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Get Details only supported on Linux"); return NULL; #endif } static void fwupd_client_verify_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_verify_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_verify: * @self: a #FwupdClient * @device_id: (not nullable): the device ID * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Verify a specific device. * * Returns: %TRUE for verification success * * Since: 0.7.0 **/ gboolean fwupd_client_verify(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(device_id != NULL, FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return FALSE; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_verify_async(self, device_id, cancellable, fwupd_client_verify_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } static void fwupd_client_verify_update_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_verify_update_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_verify_update: * @self: a #FwupdClient * @device_id: (not nullable): the device ID * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Update the verification record for a specific device. * * Returns: %TRUE for verification success * * Since: 0.8.0 **/ gboolean fwupd_client_verify_update(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(device_id != NULL, FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return FALSE; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_verify_update_async(self, device_id, cancellable, fwupd_client_verify_update_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } static void fwupd_client_unlock_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_unlock_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_unlock: * @self: a #FwupdClient * @device_id: (not nullable): the device ID * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Unlocks a specific device so firmware can be read or wrote. * * Returns: %TRUE for success * * Since: 0.7.0 **/ gboolean fwupd_client_unlock(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(device_id != NULL, FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return FALSE; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_unlock_async(self, device_id, cancellable, fwupd_client_unlock_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } static void fwupd_client_inhibit_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->str = fwupd_client_inhibit_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_inhibit: * @self: a #FwupdClient * @reason: (not nullable): the inhibit reason, e.g. `user active` * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Marks all devices as unavailable for update. Update is only available if there is no other * inhibit imposed by other applications or by the system (e.g. low power state). * * The same application can inhibit the system multiple times. * * Returns: (transfer full): a string to use for [method@FwupdClient.uninhibit_async], * or %NULL for failure * * Since: 1.8.11 **/ gchar * fwupd_client_inhibit(FwupdClient *self, const gchar *reason, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(reason != NULL, NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return NULL; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_inhibit_async(self, reason, cancellable, fwupd_client_inhibit_cb, helper); g_main_loop_run(helper->loop); if (helper->str == NULL) { g_propagate_error(error, g_steal_pointer(&helper->error)); return NULL; } return g_steal_pointer(&helper->str); } static void fwupd_client_uninhibit_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_uninhibit_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_uninhibit: * @self: a #FwupdClient * @inhibit_id: (not nullable): the inhibit ID * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Removes the inhibit token added by the application. * * Returns: %TRUE for success * * Since: 1.8.11 **/ gboolean fwupd_client_uninhibit(FwupdClient *self, const gchar *inhibit_id, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(inhibit_id != NULL, FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return FALSE; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_uninhibit_async(self, inhibit_id, cancellable, fwupd_client_uninhibit_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } static void fwupd_client_modify_config_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_modify_config_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_modify_config * @self: a #FwupdClient * @section: config section, e.g. `redfish` * @key: config key, e.g. `DisabledPlugins` * @value: config value, e.g. `*` * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Modifies a daemon config option. * The daemon will only respond to this request with proper permissions. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fwupd_client_modify_config(FwupdClient *self, const gchar *section, const gchar *key, const gchar *value, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(section != NULL, FALSE); g_return_val_if_fail(key != NULL, FALSE); g_return_val_if_fail(value != NULL, FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return FALSE; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_modify_config_async(self, section, key, value, cancellable, fwupd_client_modify_config_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } static void fwupd_client_reset_config_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_reset_config_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_reset_config * @self: a #FwupdClient * @section: config section, e.g. `redfish` * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Resets a daemon config section. * The daemon will only respond to this request with proper permissions. * * Returns: %TRUE for success * * Since: 1.9.15 **/ gboolean fwupd_client_reset_config(FwupdClient *self, const gchar *section, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(section != NULL, FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return FALSE; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_reset_config_async(self, section, cancellable, fwupd_client_reset_config_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } static void fwupd_client_activate_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_activate_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_activate: * @self: a #FwupdClient * @cancellable: (nullable): optional #GCancellable * @device_id: a device * @error: (nullable): optional return location for an error * * Activates up a device, which normally means the device switches to a new * firmware version. This should only be called when data loss cannot occur. * * Returns: %TRUE for success * * Since: 1.2.6 **/ gboolean fwupd_client_activate(FwupdClient *self, GCancellable *cancellable, const gchar *device_id, /* yes, this is the wrong way around :/ */ GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(device_id != NULL, FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return FALSE; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_activate_async(self, device_id, cancellable, fwupd_client_activate_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } static void fwupd_client_clear_results_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_clear_results_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_clear_results: * @self: a #FwupdClient * @device_id: (not nullable): the device ID * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Clears the results for a specific device. * * Returns: %TRUE for success * * Since: 0.7.0 **/ gboolean fwupd_client_clear_results(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(device_id != NULL, FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return FALSE; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_clear_results_async(self, device_id, cancellable, fwupd_client_clear_results_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } static void fwupd_client_get_results_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->device = fwupd_client_get_results_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_get_results: * @self: a #FwupdClient * @device_id: (not nullable): the device ID * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Gets the results of a previous firmware update for a specific device. * * Returns: (transfer full): a device, or %NULL for failure * * Since: 0.7.0 **/ FwupdDevice * fwupd_client_get_results(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(device_id != NULL, NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return NULL; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_get_results_async(self, device_id, cancellable, fwupd_client_get_results_cb, helper); g_main_loop_run(helper->loop); if (helper->device == NULL) { g_propagate_error(error, g_steal_pointer(&helper->error)); return NULL; } return g_steal_pointer(&helper->device); } static void fwupd_client_modify_bios_setting_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_modify_bios_setting_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_modify_bios_setting * @self: a #FwupdClient * @settings: (transfer container): BIOS settings * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Modifies a BIOS setting using kernel API. * The daemon will only respond to this request with proper permissions. * * Returns: %TRUE for success * * Since: 1.8.4 **/ gboolean fwupd_client_modify_bios_setting(FwupdClient *self, GHashTable *settings, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(settings != NULL, FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return FALSE; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_modify_bios_setting_async(self, settings, cancellable, fwupd_client_modify_bios_setting_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } static void fwupd_client_get_bios_settings_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->array = fwupd_client_get_bios_settings_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_get_bios_settings: * @self: a #FwupdClient * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Gets all the BIOS settings from the daemon. * * Returns: (element-type FwupdBiosSetting) (transfer container): attributes * * Since: 1.8.4 **/ GPtrArray * fwupd_client_get_bios_settings(FwupdClient *self, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return NULL; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_get_bios_settings_async(self, cancellable, fwupd_client_get_bios_settings_cb, helper); g_main_loop_run(helper->loop); if (helper->array == NULL) { g_propagate_error(error, g_steal_pointer(&helper->error)); return NULL; } return g_steal_pointer(&helper->array); } static void fwupd_client_get_host_security_attrs_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->array = fwupd_client_get_host_security_attrs_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_get_host_security_attrs: * @self: a #FwupdClient * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Gets all the host security attributes from the daemon. * * Returns: (element-type FwupdSecurityAttr) (transfer container): attributes * * Since: 1.5.0 **/ GPtrArray * fwupd_client_get_host_security_attrs(FwupdClient *self, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return NULL; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_get_host_security_attrs_async(self, cancellable, fwupd_client_get_host_security_attrs_cb, helper); g_main_loop_run(helper->loop); if (helper->array == NULL) { g_propagate_error(error, g_steal_pointer(&helper->error)); return NULL; } return g_steal_pointer(&helper->array); } static void fwupd_client_get_host_security_events_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->array = fwupd_client_get_host_security_events_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_get_host_security_events: * @self: a #FwupdClient * @limit: maximum number of events, or 0 for no limit * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Gets all the host security events from the daemon. * * Returns: (element-type FwupdSecurityAttr) (transfer container): attributes * * Since: 1.7.1 **/ GPtrArray * fwupd_client_get_host_security_events(FwupdClient *self, guint limit, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return NULL; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_get_host_security_events_async(self, limit, cancellable, fwupd_client_get_host_security_events_cb, helper); g_main_loop_run(helper->loop); if (helper->array == NULL) { g_propagate_error(error, g_steal_pointer(&helper->error)); return NULL; } return g_steal_pointer(&helper->array); } static void fwupd_client_get_device_by_id_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->device = fwupd_client_get_device_by_id_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_get_device_by_id: * @self: a #FwupdClient * @device_id: the device ID, e.g. `usb:00:01:03:03` * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Gets a device by its device ID. * * Returns: (transfer full): a device or %NULL * * Since: 0.9.3 **/ FwupdDevice * fwupd_client_get_device_by_id(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(device_id != NULL, NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return NULL; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_get_device_by_id_async(self, device_id, cancellable, fwupd_client_get_device_by_id_cb, helper); g_main_loop_run(helper->loop); if (helper->device == NULL) { g_propagate_error(error, g_steal_pointer(&helper->error)); return NULL; } return g_steal_pointer(&helper->device); } static void fwupd_client_get_devices_by_guid_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->array = fwupd_client_get_devices_by_guid_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_get_devices_by_guid: * @self: a #FwupdClient * @guid: the GUID, e.g. `e22c4520-43dc-5bb3-8245-5787fead9b63` * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Gets any devices that provide a specific GUID. An error is returned if no * devices contains this GUID. * * Returns: (element-type FwupdDevice) (transfer container): devices or %NULL * * Since: 1.4.1 **/ GPtrArray * fwupd_client_get_devices_by_guid(FwupdClient *self, const gchar *guid, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(guid != NULL, NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return NULL; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_get_devices_by_guid_async(self, guid, cancellable, fwupd_client_get_devices_by_guid_cb, helper); g_main_loop_run(helper->loop); if (helper->array == NULL) { g_propagate_error(error, g_steal_pointer(&helper->error)); return NULL; } return g_steal_pointer(&helper->array); } #ifdef HAVE_GIO_UNIX static void fwupd_client_install_fd_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_install_bytes_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } #endif /** * fwupd_client_install: * @self: a #FwupdClient * @device_id: (not nullable): the device ID * @filename: the filename to install * @install_flags: install flags, e.g. %FWUPD_INSTALL_FLAG_ALLOW_REINSTALL * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Install a file onto a specific device. * * Returns: %TRUE for success * * Since: 0.7.0 **/ gboolean fwupd_client_install(FwupdClient *self, const gchar *device_id, const gchar *filename, FwupdInstallFlags install_flags, GCancellable *cancellable, GError **error) { #ifdef HAVE_GIO_UNIX g_autoptr(GUnixInputStream) istr = NULL; g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(device_id != NULL, FALSE); g_return_val_if_fail(filename != NULL, FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return FALSE; /* move to a thread if this ever takes more than a few ms */ istr = fwupd_unix_input_stream_from_fn(filename, error); if (istr == NULL) return FALSE; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_install_stream_async(self, device_id, istr, filename, install_flags, cancellable, fwupd_client_install_fd_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Install CAB only supported on Linux"); return FALSE; #endif } static void fwupd_client_install_bytes_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_install_bytes_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_install_bytes: * @self: a #FwupdClient * @device_id: (not nullable): the device ID * @bytes: cabinet archive * @install_flags: install flags, e.g. %FWUPD_INSTALL_FLAG_ALLOW_REINSTALL * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Install firmware onto a specific device. * * Returns: %TRUE for success * * Since: 1.4.5 **/ gboolean fwupd_client_install_bytes(FwupdClient *self, const gchar *device_id, GBytes *bytes, FwupdInstallFlags install_flags, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(device_id != NULL, FALSE); g_return_val_if_fail(bytes != NULL, FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return FALSE; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_install_bytes_async(self, device_id, bytes, install_flags, cancellable, fwupd_client_install_bytes_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } static void fwupd_client_install_release_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_install_release_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_install_release: * @self: a #FwupdClient * @device: a device * @release: a release * @install_flags: install flags, e.g. %FWUPD_INSTALL_FLAG_ALLOW_REINSTALL * @download_flags: download flags, e.g. %FWUPD_CLIENT_DOWNLOAD_FLAG_NONE * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Installs a new release on a device, downloading the firmware if required. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fwupd_client_install_release(FwupdClient *self, FwupdDevice *device, FwupdRelease *release, FwupdInstallFlags install_flags, FwupdClientDownloadFlags download_flags, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(FWUPD_IS_DEVICE(device), FALSE); g_return_val_if_fail(FWUPD_IS_RELEASE(release), FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return FALSE; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_install_release_async(self, device, release, install_flags, download_flags, cancellable, fwupd_client_install_release_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } #ifdef HAVE_GIO_UNIX static void fwupd_client_update_metadata_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_update_metadata_bytes_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } #endif /** * fwupd_client_update_metadata: * @self: a #FwupdClient * @remote_id: the remote ID, e.g. `lvfs-testing` * @metadata_fn: the XML metadata filename * @signature_fn: the GPG signature file * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Updates the metadata. This allows a session process to download the metadata * and metadata signing file to be passed into the daemon to be checked and * parsed. * * The @remote_id allows the firmware to be tagged so that the remote can be * matched when the firmware is downloaded. * * Returns: %TRUE for success * * Since: 1.0.0 **/ gboolean fwupd_client_update_metadata(FwupdClient *self, const gchar *remote_id, const gchar *metadata_fn, const gchar *signature_fn, GCancellable *cancellable, GError **error) { #ifdef HAVE_GIO_UNIX g_autoptr(GUnixInputStream) istr = NULL; g_autoptr(GUnixInputStream) istr_sig = NULL; g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(remote_id != NULL, FALSE); g_return_val_if_fail(metadata_fn != NULL, FALSE); g_return_val_if_fail(signature_fn != NULL, FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return FALSE; istr = fwupd_unix_input_stream_from_fn(metadata_fn, error); if (istr == NULL) return FALSE; istr_sig = fwupd_unix_input_stream_from_fn(signature_fn, error); if (istr_sig == NULL) return FALSE; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_update_metadata_stream_async(self, remote_id, istr, istr_sig, cancellable, fwupd_client_update_metadata_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Update metadata only supported on Linux"); return FALSE; #endif } static void fwupd_client_update_metadata_bytes_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_update_metadata_bytes_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_update_metadata_bytes: * @self: a #FwupdClient * @remote_id: remote ID, e.g. `lvfs-testing` * @metadata: XML metadata data * @signature: signature data * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Updates the metadata. This allows a session process to download the metadata * and metadata signing file to be passed into the daemon to be checked and * parsed. * * The @remote_id allows the firmware to be tagged so that the remote can be * matched when the firmware is downloaded. * * Returns: %TRUE for success * * Since: 1.4.5 **/ gboolean fwupd_client_update_metadata_bytes(FwupdClient *self, const gchar *remote_id, GBytes *metadata, GBytes *signature, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(remote_id != NULL, FALSE); g_return_val_if_fail(metadata != NULL, FALSE); g_return_val_if_fail(signature != NULL, FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return FALSE; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_update_metadata_bytes_async(self, remote_id, metadata, signature, cancellable, fwupd_client_update_metadata_bytes_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } static void fwupd_client_refresh_remote_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_refresh_remote_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_refresh_remote: * @self: a #FwupdClient * @remote: a #FwupdRemote * @download_flags: download flags, e.g. %FWUPD_CLIENT_DOWNLOAD_FLAG_ONLY_P2P * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Refreshes a remote by downloading new metadata. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fwupd_client_refresh_remote(FwupdClient *self, FwupdRemote *remote, FwupdClientDownloadFlags download_flags, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(FWUPD_IS_REMOTE(remote), FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_refresh_remote_async(self, remote, download_flags, cancellable, fwupd_client_refresh_remote_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } static void fwupd_client_modify_remote_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_modify_remote_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_modify_remote: * @self: a #FwupdClient * @remote_id: the remote ID, e.g. `lvfs-testing` * @key: the key, e.g. `Enabled` * @value: the key, e.g. `true` * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Modifies a system remote in a specific way. * * NOTE: User authentication may be required to complete this action. * * Returns: %TRUE for success * * Since: 0.9.8 **/ gboolean fwupd_client_modify_remote(FwupdClient *self, const gchar *remote_id, const gchar *key, const gchar *value, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(remote_id != NULL, FALSE); g_return_val_if_fail(key != NULL, FALSE); g_return_val_if_fail(value != NULL, FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return FALSE; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_modify_remote_async(self, remote_id, key, value, cancellable, fwupd_client_modify_remote_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } static void fwupd_client_get_report_metadata_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->hash = fwupd_client_get_report_metadata_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_get_report_metadata: * @self: a #FwupdClient * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Gets all the report metadata from the daemon. * * Returns: (transfer container): attributes * * Since: 1.5.0 **/ GHashTable * fwupd_client_get_report_metadata(FwupdClient *self, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return NULL; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_get_report_metadata_async(self, cancellable, fwupd_client_get_report_metadata_cb, helper); g_main_loop_run(helper->loop); if (helper->hash == NULL) { g_propagate_error(error, g_steal_pointer(&helper->error)); return NULL; } return g_steal_pointer(&helper->hash); } static void fwupd_client_modify_device_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_modify_device_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_modify_device: * @self: a #FwupdClient * @device_id: (not nullable): the device ID * @key: (not nullable): the key, e.g. `Flags` * @value: (not nullable): the key, e.g. `reported` * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Modifies a device in a specific way. Not all properties on the #FwupdDevice * are settable by the client, and some may have other restrictions on @value. * * NOTE: User authentication may be required to complete this action. * * Returns: %TRUE for success * * Since: 1.0.4 **/ gboolean fwupd_client_modify_device(FwupdClient *self, const gchar *device_id, const gchar *key, const gchar *value, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(device_id != NULL, FALSE); g_return_val_if_fail(key != NULL, FALSE); g_return_val_if_fail(value != NULL, FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return FALSE; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_modify_device_async(self, device_id, key, value, cancellable, fwupd_client_modify_device_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } static void fwupd_client_get_remotes_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->array = fwupd_client_get_remotes_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_get_remotes: * @self: a #FwupdClient * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Gets the list of remotes that have been configured for the system. * * Returns: (element-type FwupdRemote) (transfer container): list of remotes, or %NULL * * Since: 0.9.3 **/ GPtrArray * fwupd_client_get_remotes(FwupdClient *self, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return NULL; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_get_remotes_async(self, cancellable, fwupd_client_get_remotes_cb, helper); g_main_loop_run(helper->loop); if (helper->array == NULL) { g_propagate_error(error, g_steal_pointer(&helper->error)); return NULL; } return g_steal_pointer(&helper->array); } static FwupdRemote * fwupd_client_get_remote_by_id_noref(GPtrArray *remotes, const gchar *remote_id) { for (guint i = 0; i < remotes->len; i++) { FwupdRemote *remote = g_ptr_array_index(remotes, i); if (g_strcmp0(remote_id, fwupd_remote_get_id(remote)) == 0) return remote; } return NULL; } /** * fwupd_client_get_remote_by_id: * @self: a #FwupdClient * @remote_id: the remote ID, e.g. `lvfs-testing` * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Gets a specific remote that has been configured for the system. * * Returns: (transfer full): a #FwupdRemote, or %NULL if not found * * Since: 0.9.3 **/ FwupdRemote * fwupd_client_get_remote_by_id(FwupdClient *self, const gchar *remote_id, GCancellable *cancellable, GError **error) { FwupdRemote *remote; g_autoptr(GPtrArray) remotes = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(remote_id != NULL, NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* find remote in list */ remotes = fwupd_client_get_remotes(self, cancellable, error); if (remotes == NULL) return NULL; remote = fwupd_client_get_remote_by_id_noref(remotes, remote_id); if (remote == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "No remote '%s' found in search paths", remote_id); return NULL; } /* success */ return g_object_ref(remote); } static void fwupd_client_get_approved_firmware_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->array = fwupd_client_get_approved_firmware_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_get_approved_firmware: * @self: a #FwupdClient * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Gets the list of approved firmware. * * Returns: (transfer full): checksums, or %NULL for error * * Since: 1.2.6 **/ gchar ** fwupd_client_get_approved_firmware(FwupdClient *self, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; gchar **argv; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return NULL; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_get_approved_firmware_async(self, cancellable, fwupd_client_get_approved_firmware_cb, helper); g_main_loop_run(helper->loop); if (helper->array == NULL) { g_propagate_error(error, g_steal_pointer(&helper->error)); return NULL; } argv = g_new0(gchar *, helper->array->len + 1); for (guint i = 0; i < helper->array->len; i++) { const gchar *tmp = g_ptr_array_index(helper->array, i); argv[i] = g_strdup(tmp); } return argv; } static void fwupd_client_set_approved_firmware_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_set_approved_firmware_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_set_approved_firmware: * @self: a #FwupdClient * @checksums: (not nullable): Array of checksums * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Sets the list of approved firmware. * * Returns: %TRUE for success * * Since: 1.2.6 **/ gboolean fwupd_client_set_approved_firmware(FwupdClient *self, gchar **checksums, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_autoptr(GPtrArray) array = g_ptr_array_new_with_free_func(g_free); g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(checksums != NULL, FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return FALSE; /* convert */ for (guint i = 0; checksums[i] != NULL; i++) g_ptr_array_add(array, g_strdup(checksums[i])); /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_set_approved_firmware_async(self, array, cancellable, fwupd_client_set_approved_firmware_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } static void fwupd_client_get_blocked_firmware_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->array = fwupd_client_get_blocked_firmware_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_get_blocked_firmware: * @self: a #FwupdClient * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Gets the list of blocked firmware. * * Returns: (transfer full): checksums, or %NULL for error * * Since: 1.4.6 **/ gchar ** fwupd_client_get_blocked_firmware(FwupdClient *self, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; gchar **argv; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return NULL; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_get_blocked_firmware_async(self, cancellable, fwupd_client_get_blocked_firmware_cb, helper); g_main_loop_run(helper->loop); if (helper->array == NULL) { g_propagate_error(error, g_steal_pointer(&helper->error)); return NULL; } argv = g_new0(gchar *, helper->array->len + 1); for (guint i = 0; i < helper->array->len; i++) { const gchar *tmp = g_ptr_array_index(helper->array, i); argv[i] = g_strdup(tmp); } return argv; } static void fwupd_client_set_blocked_firmware_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_set_blocked_firmware_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_set_blocked_firmware: * @self: a #FwupdClient * @checksums: Array of checksums * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Sets the list of approved firmware. * * Returns: %TRUE for success * * Since: 1.4.6 **/ gboolean fwupd_client_set_blocked_firmware(FwupdClient *self, gchar **checksums, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_autoptr(GPtrArray) array = g_ptr_array_new_with_free_func(g_free); g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(checksums != NULL, FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return FALSE; for (guint i = 0; checksums[i] != NULL; i++) g_ptr_array_add(array, g_strdup(checksums[i])); /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_set_blocked_firmware_async(self, array, cancellable, fwupd_client_set_blocked_firmware_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } static void fwupd_client_set_feature_flags_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_set_feature_flags_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_set_feature_flags: * @self: a #FwupdClient * @feature_flags: feature flags, e.g. %FWUPD_FEATURE_FLAG_UPDATE_TEXT * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Sets the features the client supports. This allows firmware to depend on * specific front-end features, for instance showing the user an image on * how to detach the hardware. * * Clients can call this none or multiple times. * * Returns: %TRUE for success * * Since: 1.4.5 **/ gboolean fwupd_client_set_feature_flags(FwupdClient *self, FwupdFeatureFlags feature_flags, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return FALSE; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_set_feature_flags_async(self, feature_flags, cancellable, fwupd_client_set_feature_flags_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } static void fwupd_client_self_sign_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->str = fwupd_client_self_sign_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_self_sign: * @self: a #FwupdClient * @value: (not nullable): a string to sign, typically a JSON blob * @flags: signing flags, e.g. %FWUPD_SELF_SIGN_FLAG_ADD_TIMESTAMP * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Signs the data using the client self-signed certificate. * * Returns: a signature, or %NULL for failure * * Since: 1.2.6 **/ gchar * fwupd_client_self_sign(FwupdClient *self, const gchar *value, FwupdSelfSignFlags flags, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(value != NULL, NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return NULL; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_self_sign_async(self, value, flags, cancellable, fwupd_client_self_sign_cb, helper); g_main_loop_run(helper->loop); if (helper->str == NULL) { g_propagate_error(error, g_steal_pointer(&helper->error)); return NULL; } return g_steal_pointer(&helper->str); } static void fwupd_client_download_bytes_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->bytes = fwupd_client_download_bytes_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_download_bytes: * @self: a #FwupdClient * @url: (not nullable): the remote URL * @flags: download flags, e.g. %FWUPD_CLIENT_DOWNLOAD_FLAG_NONE * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Downloads data from a remote server. The [method@Client.set_user_agent] function * should be called before this method is used. * * Returns: (transfer full): downloaded data, or %NULL for error * * Since: 1.4.5 **/ GBytes * fwupd_client_download_bytes(FwupdClient *self, const gchar *url, FwupdClientDownloadFlags flags, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(url != NULL, NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); g_return_val_if_fail(fwupd_client_get_user_agent(self) != NULL, NULL); /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_download_bytes_async(self, url, flags, cancellable, fwupd_client_download_bytes_cb, helper); g_main_loop_run(helper->loop); if (helper->bytes == NULL) { g_propagate_error(error, g_steal_pointer(&helper->error)); return NULL; } return g_steal_pointer(&helper->bytes); } /** * fwupd_client_download_file: * @self: a #FwupdClient * @url: (not nullable): the remote URL * @file: (not nullable): a file * @flags: download flags, e.g. %FWUPD_CLIENT_DOWNLOAD_FLAG_NONE * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Downloads data from a remote server. The [method@Client.set_user_agent] function * should be called before this method is used. * * Returns: %TRUE if the file was written * * Since: 1.5.2 **/ gboolean fwupd_client_download_file(FwupdClient *self, const gchar *url, GFile *file, FwupdClientDownloadFlags flags, GCancellable *cancellable, GError **error) { gssize size; g_autoptr(GBytes) bytes = NULL; g_autoptr(GOutputStream) ostream = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(url != NULL, FALSE); g_return_val_if_fail(G_IS_FILE(file), FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); g_return_val_if_fail(fwupd_client_get_user_agent(self) != NULL, FALSE); /* download then write */ bytes = fwupd_client_download_bytes(self, url, flags, cancellable, error); if (bytes == NULL) return FALSE; ostream = G_OUTPUT_STREAM(g_file_replace(file, NULL, FALSE, G_FILE_CREATE_NONE, NULL, error)); if (ostream == NULL) return FALSE; size = g_output_stream_write_bytes(ostream, bytes, NULL, error); if (size < 0) return FALSE; /* success */ return TRUE; } static void fwupd_client_upload_bytes_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->bytes = fwupd_client_upload_bytes_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_upload_bytes: * @self: a #FwupdClient * @url: (not nullable): the remote URL * @payload: (not nullable): payload string * @signature: (nullable): signature string * @flags: download flags, e.g. %FWUPD_CLIENT_DOWNLOAD_FLAG_NONE * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Uploads data to a remote server. The [method@Client.set_user_agent] function * should be called before this method is used. * * Returns: (transfer full): response data, or %NULL for error * * Since: 1.4.5 **/ GBytes * fwupd_client_upload_bytes(FwupdClient *self, const gchar *url, const gchar *payload, const gchar *signature, FwupdClientUploadFlags flags, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(url != NULL, NULL); g_return_val_if_fail(payload != NULL, NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return NULL; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_upload_bytes_async(self, url, payload, signature, flags, cancellable, fwupd_client_upload_bytes_cb, helper); g_main_loop_run(helper->loop); if (helper->bytes == NULL) { g_propagate_error(error, g_steal_pointer(&helper->error)); return NULL; } return g_steal_pointer(&helper->bytes); } static void fwupd_client_upload_report_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->str = fwupd_client_upload_report_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_upload_report: * @self: a #FwupdClient * @url: (not nullable): the remote URL * @payload: (not nullable): payload string * @signature: (nullable): signature string * @flags: download flags, e.g. %FWUPD_CLIENT_DOWNLOAD_FLAG_NONE * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Uploads a report to a remote server. The [method@Client.set_user_agent] function * should be called before this method is used. * * Returns: (transfer full): a URI (perhaps an empty string), or %NULL for error * * Since: 1.9.20 **/ gchar * fwupd_client_upload_report(FwupdClient *self, const gchar *url, const gchar *payload, const gchar *signature, FwupdClientUploadFlags flags, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(url != NULL, NULL); g_return_val_if_fail(payload != NULL, NULL); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return NULL; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_upload_report_async(self, url, payload, signature, flags, cancellable, fwupd_client_upload_report_cb, helper); g_main_loop_run(helper->loop); if (helper->str == NULL) { g_propagate_error(error, g_steal_pointer(&helper->error)); return NULL; } return g_steal_pointer(&helper->str); } static void fwupd_client_emulation_load_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_emulation_load_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_emulation_load * @self: a #FwupdClient * @filename: archive data of JSON files * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Loads an emulated device into the daemon backend that has the phases set by the JSON data, * for instance, having one USB device emulated for the bootloader and another emulated for the * runtime interface. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fwupd_client_emulation_load(FwupdClient *self, const gchar *filename, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(filename != NULL, FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return FALSE; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_emulation_load_async(self, filename, cancellable, fwupd_client_emulation_load_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } static void fwupd_client_emulation_save_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_emulation_save_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_emulation_save: * @self: a #FwupdClient * @filename: archive data of JSON files * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Gets the captured data from all filtered devices for all recorded phases. The data is returned * in a ZIP archive of JSON output. * * NOTE: Device events are not automatically recorded for all devices. You must call something * like `ModifyDevice(device_id, 'flags','emulation-tag')` to start the recording the backend. * * Once the device has been re-inserted then the emulation data will be available using * this API call. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fwupd_client_emulation_save(FwupdClient *self, const gchar *filename, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* connect */ if (!fwupd_client_connect(self, cancellable, error)) return FALSE; /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_emulation_save_async(self, filename, cancellable, fwupd_client_emulation_save_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } static void fwupd_client_fix_host_security_attr_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_fix_host_security_attr_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_fix_host_security_attr: * @self: a #FwupdClient * @appstream_id: the HSI AppStream ID * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Fix one specific security attribute. * * Returns: %TRUE for success * * Since: 1.9.6 **/ gboolean fwupd_client_fix_host_security_attr(FwupdClient *self, const gchar *appstream_id, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(appstream_id != NULL, FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_fix_host_security_attr_async(self, appstream_id, cancellable, fwupd_client_fix_host_security_attr_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } static void fwupd_client_undo_host_security_attr_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClientHelper *helper = (FwupdClientHelper *)user_data; helper->ret = fwupd_client_undo_host_security_attr_finish(FWUPD_CLIENT(source), res, &helper->error); g_main_loop_quit(helper->loop); } /** * fwupd_client_undo_host_security_attr: * @self: a #FwupdClient * @appstream_id: the HSI AppStream ID * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Revert the fix to one specific security attribute. * * Returns: %TRUE for success * * Since: 1.9.6 **/ gboolean fwupd_client_undo_host_security_attr(FwupdClient *self, const gchar *appstream_id, GCancellable *cancellable, GError **error) { g_autoptr(FwupdClientHelper) helper = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(appstream_id != NULL, FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* call async version and run loop until complete */ helper = fwupd_client_helper_new(self); fwupd_client_undo_host_security_attr_async(self, appstream_id, cancellable, fwupd_client_undo_host_security_attr_cb, helper); g_main_loop_run(helper->loop); if (!helper->ret) { g_propagate_error(error, g_steal_pointer(&helper->error)); return FALSE; } return TRUE; } fwupd-2.0.10/libfwupd/fwupd-client-sync.h000066400000000000000000000242441501337203100202600ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fwupd-client.h" G_BEGIN_DECLS gboolean fwupd_client_connect(FwupdClient *self, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fwupd_client_quit(FwupdClient *self, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); GPtrArray * fwupd_client_get_devices(FwupdClient *self, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); GPtrArray * fwupd_client_get_plugins(FwupdClient *self, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); GPtrArray * fwupd_client_get_history(FwupdClient *self, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); GPtrArray * fwupd_client_get_releases(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); GPtrArray * fwupd_client_get_downgrades(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); GPtrArray * fwupd_client_get_upgrades(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); GPtrArray * fwupd_client_get_details(FwupdClient *self, const gchar *filename, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); GPtrArray * fwupd_client_get_details_bytes(FwupdClient *self, GBytes *bytes, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_verify(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_verify_update(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_unlock(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gchar * fwupd_client_inhibit(FwupdClient *self, const gchar *reason, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_uninhibit(FwupdClient *self, const gchar *inhibit_id, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_modify_config(FwupdClient *self, const gchar *section, const gchar *key, const gchar *value, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_reset_config(FwupdClient *self, const gchar *section, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_activate(FwupdClient *self, GCancellable *cancellable, const gchar *device_id, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 3); gboolean fwupd_client_clear_results(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); FwupdDevice * fwupd_client_get_results(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_modify_bios_setting(FwupdClient *self, GHashTable *settings, GCancellable *cancellable, GError **error) G_GNUC_NON_NULL(1, 2); GPtrArray * fwupd_client_get_bios_settings(FwupdClient *self, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); GPtrArray * fwupd_client_get_host_security_attrs(FwupdClient *self, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); GPtrArray * fwupd_client_get_host_security_events(FwupdClient *self, guint limit, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); FwupdDevice * fwupd_client_get_device_by_id(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); GPtrArray * fwupd_client_get_devices_by_guid(FwupdClient *self, const gchar *guid, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_install(FwupdClient *self, const gchar *device_id, const gchar *filename, FwupdInstallFlags install_flags, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 3); gboolean fwupd_client_install_bytes(FwupdClient *self, const gchar *device_id, GBytes *bytes, FwupdInstallFlags install_flags, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 3); gboolean fwupd_client_install_release(FwupdClient *self, FwupdDevice *device, FwupdRelease *release, FwupdInstallFlags install_flags, FwupdClientDownloadFlags download_flags, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); gboolean fwupd_client_update_metadata(FwupdClient *self, const gchar *remote_id, const gchar *metadata_fn, const gchar *signature_fn, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3, 4); gboolean fwupd_client_update_metadata_bytes(FwupdClient *self, const gchar *remote_id, GBytes *metadata, GBytes *signature, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3, 4); gboolean fwupd_client_refresh_remote(FwupdClient *self, FwupdRemote *remote, FwupdClientDownloadFlags download_flags, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_modify_remote(FwupdClient *self, const gchar *remote_id, const gchar *key, const gchar *value, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); gboolean fwupd_client_modify_device(FwupdClient *self, const gchar *device_id, const gchar *key, const gchar *value, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); GHashTable * fwupd_client_get_report_metadata(FwupdClient *self, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); GPtrArray * fwupd_client_get_remotes(FwupdClient *self, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); FwupdRemote * fwupd_client_get_remote_by_id(FwupdClient *self, const gchar *remote_id, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gchar ** fwupd_client_get_approved_firmware(FwupdClient *self, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fwupd_client_set_approved_firmware(FwupdClient *self, gchar **checksums, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gchar ** fwupd_client_get_blocked_firmware(FwupdClient *self, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fwupd_client_set_blocked_firmware(FwupdClient *self, gchar **checksums, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gchar * fwupd_client_self_sign(FwupdClient *self, const gchar *value, FwupdSelfSignFlags flags, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_set_feature_flags(FwupdClient *self, FwupdFeatureFlags feature_flags, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); GBytes * fwupd_client_download_bytes(FwupdClient *self, const gchar *url, FwupdClientDownloadFlags flags, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_download_file(FwupdClient *self, const gchar *url, GFile *file, FwupdClientDownloadFlags flags, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); GBytes * fwupd_client_upload_bytes(FwupdClient *self, const gchar *url, const gchar *payload, const gchar *signature, FwupdClientUploadFlags flags, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); gchar * fwupd_client_upload_report(FwupdClient *self, const gchar *url, const gchar *payload, const gchar *signature, FwupdClientUploadFlags flags, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); gboolean fwupd_client_emulation_load(FwupdClient *self, const gchar *filename, GCancellable *cancellable, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_emulation_save(FwupdClient *self, const gchar *filename, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_fix_host_security_attr(FwupdClient *self, const gchar *appstream_id, GCancellable *cancellable, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_undo_host_security_attr(FwupdClient *self, const gchar *appstream_id, GCancellable *cancellable, GError **error) G_GNUC_NON_NULL(1, 2); G_END_DECLS fwupd-2.0.10/libfwupd/fwupd-client.c000066400000000000000000007047051501337203100173100ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #ifdef HAVE_GIO_UNIX #include #endif #ifdef HAVE_UTSNAME_H #include #endif #include #include #include #include #include #include "fwupd-bios-setting.h" #include "fwupd-client-private.h" #include "fwupd-client-sync.h" #include "fwupd-codec.h" #include "fwupd-common-private.h" #include "fwupd-device-private.h" #include "fwupd-enums-private.h" #include "fwupd-error.h" #include "fwupd-plugin.h" #include "fwupd-release.h" #include "fwupd-remote-private.h" #include "fwupd-request-private.h" #include "fwupd-security-attr-private.h" static void fwupd_client_fixup_dbus_error(GError *error); typedef GObject *(*FwupdClientObjectNewFunc)(void); #define FWUPD_CLIENT_DBUS_PROXY_TIMEOUT 180000 /* ms */ /** * FwupdClient: * * Allow client code to call the daemon methods. * * See also: [class@FwupdDevice] */ static void fwupd_client_finalize(GObject *object); typedef struct { GMainContext *main_ctx; FwupdStatus status; gboolean tainted; gboolean interactive; guint percentage; guint32 battery_level; guint32 battery_threshold; guint download_retries; GMutex idle_mutex; /* for @idle_id and @idle_sources */ guint idle_id; GPtrArray *idle_sources; /* element-type FwupdClientContextHelper */ gchar *daemon_version; gchar *host_bkc; gchar *host_product; gchar *host_vendor; gchar *host_machine_id; gchar *host_security_id; gboolean only_trusted; GMutex proxy_mutex; /* for @proxy */ GDBusProxy *proxy; gchar *proxy_name_owner; GProxyResolver *proxy_resolver; gchar *package_name; gchar *package_version; gchar *user_agent; GHashTable *hints; /* str:str */ GHashTable *immediate_requests; /* str:FwupdRequest */ } FwupdClientPrivate; typedef struct { GPtrArray *urls; CURL *curl; curl_mime *mime; struct curl_slist *headers; } FwupdCurlHelper; enum { SIGNAL_CHANGED, SIGNAL_STATUS_CHANGED, SIGNAL_DEVICE_ADDED, SIGNAL_DEVICE_REMOVED, SIGNAL_DEVICE_CHANGED, SIGNAL_DEVICE_REQUEST, SIGNAL_LAST }; enum { PROP_0, PROP_STATUS, PROP_PERCENTAGE, PROP_DAEMON_VERSION, PROP_TAINTED, PROP_HOST_PRODUCT, PROP_HOST_VENDOR, PROP_HOST_MACHINE_ID, PROP_HOST_SECURITY_ID, PROP_HOST_BKC, PROP_INTERACTIVE, PROP_ONLY_TRUSTED, PROP_BATTERY_LEVEL, PROP_BATTERY_THRESHOLD, PROP_LAST }; static guint signals[SIGNAL_LAST] = {0}; G_DEFINE_TYPE_WITH_PRIVATE(FwupdClient, fwupd_client, G_TYPE_OBJECT) #define GET_PRIVATE(o) (fwupd_client_get_instance_private(o)) G_DEFINE_AUTOPTR_CLEANUP_FUNC(CURLU, curl_url_cleanup) typedef char CURLSTR; G_DEFINE_AUTOPTR_CLEANUP_FUNC(CURLSTR, curl_free) static void fwupd_client_curl_helper_free(FwupdCurlHelper *helper) { if (helper->curl != NULL) curl_easy_cleanup(helper->curl); if (helper->mime != NULL) curl_mime_free(helper->mime); if (helper->headers != NULL) curl_slist_free_all(helper->headers); if (helper->urls != NULL) g_ptr_array_unref(helper->urls); g_free(helper); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(FwupdCurlHelper, fwupd_client_curl_helper_free) typedef struct { FwupdClient *self; gchar *property_name; guint signal_id; GObject *payload; } FwupdClientContextHelper; static void fwupd_client_context_helper_free(FwupdClientContextHelper *helper) { g_clear_object(&helper->payload); g_object_unref(helper->self); g_free(helper->property_name); g_free(helper); } /* always executed in the main context given by priv->main_ctx */ static void fwupd_client_context_object_notify(FwupdClient *self, const gchar *property_name) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_return_if_fail(g_main_context_is_owner(priv->main_ctx)); /* property */ g_object_notify(G_OBJECT(self), property_name); /* legacy signal name */ if (g_strcmp0(property_name, "status") == 0) g_signal_emit(self, signals[SIGNAL_STATUS_CHANGED], 0, priv->status); } /* emits all pending context helpers in the correct GMainContext */ static gboolean fwupd_client_context_idle_cb(gpointer user_data) { FwupdClient *self = FWUPD_CLIENT(user_data); FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GMutexLocker) locker = g_mutex_locker_new(&priv->idle_mutex); g_return_val_if_fail(locker != NULL, FALSE); for (guint i = 0; i < priv->idle_sources->len; i++) { FwupdClientContextHelper *helper = g_ptr_array_index(priv->idle_sources, i); /* property */ if (helper->property_name != NULL) fwupd_client_context_object_notify(self, helper->property_name); if (g_strcmp0(helper->property_name, "FwupdRequest") == 0) fwupd_request_emit_invalidate(FWUPD_REQUEST(helper->payload)); /* signal */ if (helper->signal_id != 0) { if (helper->payload == NULL) g_signal_emit(self, signals[helper->signal_id], 0); else g_signal_emit(self, signals[helper->signal_id], 0, helper->payload); } } /* all done */ g_ptr_array_set_size(priv->idle_sources, 0); priv->idle_id = 0; return G_SOURCE_REMOVE; } static void fwupd_client_context_helper(FwupdClient *self, FwupdClientContextHelper *helper) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GMutexLocker) locker = g_mutex_locker_new(&priv->idle_mutex); g_return_if_fail(locker != NULL); /* no source already attached to the context */ if (priv->idle_id == 0) { g_autoptr(GSource) source = g_idle_source_new(); g_source_set_callback(source, fwupd_client_context_idle_cb, self, NULL); priv->idle_id = g_source_attach(source, priv->main_ctx); } /* run in the correct GMainContext and thread */ g_ptr_array_add(priv->idle_sources, helper); } /* run callback in the correct thread */ static void fwupd_client_object_notify(FwupdClient *self, const gchar *property_name) { FwupdClientPrivate *priv = GET_PRIVATE(self); FwupdClientContextHelper *helper = NULL; /* shortcut */ if (g_main_context_is_owner(priv->main_ctx)) { fwupd_client_context_object_notify(self, property_name); return; } /* run in the correct GMainContext and thread */ helper = g_new0(FwupdClientContextHelper, 1); helper->self = g_object_ref(self); helper->property_name = g_strdup(property_name); fwupd_client_context_helper(self, helper); } /* run callback in the correct thread */ static void fwupd_client_request_invalidate(FwupdClient *self, FwupdRequest *request) { FwupdClientPrivate *priv = GET_PRIVATE(self); FwupdClientContextHelper *helper = NULL; /* shortcut */ if (g_main_context_is_owner(priv->main_ctx)) { fwupd_request_emit_invalidate(request); return; } /* run in the correct GMainContext and thread */ helper = g_new0(FwupdClientContextHelper, 1); helper->self = g_object_ref(self); helper->property_name = g_strdup("FwupdRequest"); helper->payload = G_OBJECT(g_object_ref(request)); fwupd_client_context_helper(self, helper); } /* run callback in the correct thread */ static void fwupd_client_signal_emit_object(FwupdClient *self, guint signal_id, GObject *payload) { FwupdClientPrivate *priv = GET_PRIVATE(self); FwupdClientContextHelper *helper = NULL; /* shortcut */ if (g_main_context_is_owner(priv->main_ctx)) { g_signal_emit(self, signals[signal_id], 0, payload); return; } /* run in the correct GMainContext and thread */ helper = g_new0(FwupdClientContextHelper, 1); helper->self = g_object_ref(self); helper->signal_id = signal_id; helper->payload = g_object_ref(payload); fwupd_client_context_helper(self, helper); } /* run callback in the correct thread */ static void fwupd_client_signal_emit_changed(FwupdClient *self) { FwupdClientPrivate *priv = GET_PRIVATE(self); FwupdClientContextHelper *helper = NULL; /* shortcut */ if (g_main_context_is_owner(priv->main_ctx)) { g_signal_emit(self, signals[SIGNAL_CHANGED], 0); return; } /* run in the correct GMainContext and thread */ helper = g_new0(FwupdClientContextHelper, 1); helper->self = g_object_ref(self); helper->signal_id = SIGNAL_CHANGED; fwupd_client_context_helper(self, helper); } static gchar * fwupd_client_build_user_agent_os_release(void) { const gchar *keys[] = {G_OS_INFO_KEY_NAME, G_OS_INFO_KEY_VERSION_ID, "VARIANT", NULL}; g_autoptr(GPtrArray) ids_os = g_ptr_array_new_with_free_func(g_free); /* create an array of the keys that exist */ for (guint i = 0; keys[i] != NULL; i++) { g_autofree gchar *value = g_get_os_info(keys[i]); if (value != NULL) g_ptr_array_add(ids_os, g_steal_pointer(&value)); } if (ids_os->len == 0) return NULL; g_ptr_array_add(ids_os, NULL); return g_strjoinv(" ", (gchar **)ids_os->pdata); } static gchar * fwupd_client_build_user_agent_system(void) { #ifdef HAVE_UTSNAME_H struct utsname name_tmp; #endif g_autofree gchar *locale = NULL; g_autofree gchar *os_release = NULL; g_autoptr(GPtrArray) ids = g_ptr_array_new_with_free_func(g_free); /* system, architecture and kernel, e.g. "Linux i686 4.14.5" */ #ifdef HAVE_UTSNAME_H memset(&name_tmp, 0, sizeof(struct utsname)); if (uname(&name_tmp) >= 0) { g_ptr_array_add(ids, g_strdup_printf("%s %s %s", name_tmp.sysname, name_tmp.machine, name_tmp.release)); } #endif /* current locale, e.g. "en-gb" */ #ifdef HAVE_LC_MESSAGES locale = g_strdup(setlocale(LC_MESSAGES, NULL)); #endif if (locale != NULL) { g_strdelimit(locale, ".", '\0'); g_strdelimit(locale, "_", '-'); g_ptr_array_add(ids, g_steal_pointer(&locale)); } /* OS release, e.g. "Fedora 27 Workstation" */ os_release = fwupd_client_build_user_agent_os_release(); if (os_release != NULL) g_ptr_array_add(ids, g_steal_pointer(&os_release)); /* convert to string */ if (ids->len == 0) return NULL; g_ptr_array_add(ids, NULL); return g_strjoinv("; ", (gchar **)ids->pdata); } static void fwupd_client_rebuild_user_agent(FwupdClient *self) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GString) str = g_string_new(NULL); g_autofree gchar *system = NULL; /* application name and version */ if (priv->package_name != NULL && priv->package_version != NULL) g_string_append_printf(str, "%s/%s ", priv->package_name, priv->package_version); /* system information */ system = fwupd_client_build_user_agent_system(); if (system != NULL) g_string_append_printf(str, "(%s) ", system); /* am running in CI */ if (g_getenv("CI") != NULL) g_string_append_printf(str, "ci/%s ", g_getenv("CI")); /* platform, unless the application name is fwupd itself */ if (priv->daemon_version != NULL && g_strcmp0(priv->package_name, "fwupd") != 0) g_string_append_printf(str, "fwupd/%s", priv->daemon_version); /* success */ g_free(priv->user_agent); priv->user_agent = g_string_free(g_steal_pointer(&str), FALSE); } static void fwupd_client_set_host_vendor(FwupdClient *self, const gchar *host_vendor) { FwupdClientPrivate *priv = GET_PRIVATE(self); /* not changed */ if (g_strcmp0(priv->host_vendor, host_vendor) == 0) return; g_free(priv->host_vendor); priv->host_vendor = g_strdup(host_vendor); fwupd_client_object_notify(self, "host-vendor"); } static void fwupd_client_set_host_product(FwupdClient *self, const gchar *host_product) { FwupdClientPrivate *priv = GET_PRIVATE(self); /* not changed */ if (g_strcmp0(priv->host_product, host_product) == 0) return; g_free(priv->host_product); priv->host_product = g_strdup(host_product); fwupd_client_object_notify(self, "host-product"); } static void fwupd_client_set_host_machine_id(FwupdClient *self, const gchar *host_machine_id) { FwupdClientPrivate *priv = GET_PRIVATE(self); /* not changed */ if (g_strcmp0(priv->host_machine_id, host_machine_id) == 0) return; g_free(priv->host_machine_id); priv->host_machine_id = g_strdup(host_machine_id); fwupd_client_object_notify(self, "host-machine-id"); } static void fwupd_client_set_host_security_id(FwupdClient *self, const gchar *host_security_id) { FwupdClientPrivate *priv = GET_PRIVATE(self); /* not changed */ if (g_strcmp0(priv->host_security_id, host_security_id) == 0) return; g_free(priv->host_security_id); priv->host_security_id = g_strdup(host_security_id); fwupd_client_object_notify(self, "host-security-id"); } /** * fwupd_client_set_daemon_version: * @self: a #FwupdClient * @daemon_version: A semantic version, e.g. "1.2.3" * * Sets the daemon version number. * * Since: 1.8.11 **/ void fwupd_client_set_daemon_version(FwupdClient *self, const gchar *daemon_version) { FwupdClientPrivate *priv = GET_PRIVATE(self); /* not changed */ if (g_strcmp0(priv->daemon_version, daemon_version) == 0) return; g_free(priv->daemon_version); priv->daemon_version = g_strdup(daemon_version); fwupd_client_object_notify(self, "daemon-version"); fwupd_client_rebuild_user_agent(self); } /** * fwupd_client_download_set_retries: * @self: a #FwupdClient * @retries: number of tries, defaulting to 0 * * Sets the number of retries should be attempted on transient download errors. * * Since: 1.9.19 **/ void fwupd_client_download_set_retries(FwupdClient *self, guint retries) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_CLIENT(self)); priv->download_retries = retries; } static void fwupd_client_set_host_bkc(FwupdClient *self, const gchar *host_bkc) { FwupdClientPrivate *priv = GET_PRIVATE(self); /* emulate a D-Bus maybe type */ if (g_strcmp0(host_bkc, "") == 0) host_bkc = NULL; /* not changed */ if (g_strcmp0(priv->host_bkc, host_bkc) == 0) return; g_free(priv->host_bkc); priv->host_bkc = g_strdup(host_bkc); fwupd_client_object_notify(self, "host-bkc"); } static void fwupd_client_set_status(FwupdClient *self, FwupdStatus status) { FwupdClientPrivate *priv = GET_PRIVATE(self); if (priv->status == status) return; priv->status = status; g_debug("Emitting ::status-changed() [%s]", fwupd_status_to_string(priv->status)); fwupd_client_object_notify(self, "status"); } static void fwupd_client_set_percentage(FwupdClient *self, guint percentage) { FwupdClientPrivate *priv = GET_PRIVATE(self); if (priv->percentage == percentage) return; priv->percentage = percentage; fwupd_client_object_notify(self, "percentage"); } static void fwupd_client_set_battery_level(FwupdClient *self, guint32 battery_level) { FwupdClientPrivate *priv = GET_PRIVATE(self); if (priv->battery_level == battery_level) return; priv->battery_level = battery_level; g_object_notify(G_OBJECT(self), "battery-level"); } static void fwupd_client_set_battery_threshold(FwupdClient *self, guint32 battery_threshold) { FwupdClientPrivate *priv = GET_PRIVATE(self); if (priv->battery_threshold == battery_threshold) return; priv->battery_threshold = battery_threshold; g_object_notify(G_OBJECT(self), "battery-threshold"); } static void fwupd_client_properties_changed_cb(GDBusProxy *proxy, GVariant *changed_properties, GStrv invalidated_properties, FwupdClient *self) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GVariantDict) dict = NULL; /* print to the console */ dict = g_variant_dict_new(changed_properties); if (g_variant_dict_contains(dict, "Status")) { g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_get_cached_property(proxy, "Status"); if (val != NULL) fwupd_client_set_status(self, g_variant_get_uint32(val)); } if (g_variant_dict_contains(dict, "Tainted")) { g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_get_cached_property(proxy, "Tainted"); if (val != NULL) { priv->tainted = g_variant_get_boolean(val); fwupd_client_object_notify(self, "tainted"); } } if (g_variant_dict_contains(dict, "Interactive")) { g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_get_cached_property(proxy, "Interactive"); if (val != NULL) { priv->interactive = g_variant_get_boolean(val); fwupd_client_object_notify(self, "interactive"); } } if (g_variant_dict_contains(dict, "Percentage")) { g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_get_cached_property(proxy, "Percentage"); if (val != NULL) fwupd_client_set_percentage(self, g_variant_get_uint32(val)); } if (g_variant_dict_contains(dict, FWUPD_RESULT_KEY_BATTERY_LEVEL)) { g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_get_cached_property(proxy, FWUPD_RESULT_KEY_BATTERY_LEVEL); if (val != NULL) fwupd_client_set_battery_level(self, g_variant_get_uint32(val)); } if (g_variant_dict_contains(dict, FWUPD_RESULT_KEY_BATTERY_THRESHOLD)) { g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_get_cached_property(proxy, FWUPD_RESULT_KEY_BATTERY_THRESHOLD); if (val != NULL) fwupd_client_set_battery_threshold(self, g_variant_get_uint32(val)); } if (g_variant_dict_contains(dict, "DaemonVersion")) { g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_get_cached_property(proxy, "DaemonVersion"); if (val != NULL) fwupd_client_set_daemon_version(self, g_variant_get_string(val, NULL)); } if (g_variant_dict_contains(dict, "HostBkc")) { g_autoptr(GVariant) val = g_dbus_proxy_get_cached_property(proxy, "HostBkc"); if (val != NULL) fwupd_client_set_host_bkc(self, g_variant_get_string(val, NULL)); } if (g_variant_dict_contains(dict, "HostVendor")) { g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_get_cached_property(proxy, "HostVendor"); if (val != NULL) fwupd_client_set_host_vendor(self, g_variant_get_string(val, NULL)); } if (g_variant_dict_contains(dict, "HostProduct")) { g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_get_cached_property(proxy, "HostProduct"); if (val != NULL) fwupd_client_set_host_product(self, g_variant_get_string(val, NULL)); } if (g_variant_dict_contains(dict, "HostMachineId")) { g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_get_cached_property(proxy, "HostMachineId"); if (val != NULL) fwupd_client_set_host_machine_id(self, g_variant_get_string(val, NULL)); } if (g_variant_dict_contains(dict, "HostSecurityId")) { g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_get_cached_property(proxy, "HostSecurityId"); if (val != NULL) fwupd_client_set_host_security_id(self, g_variant_get_string(val, NULL)); } if (g_variant_dict_contains(dict, "OnlyTrusted")) { g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_get_cached_property(proxy, "OnlyTrusted"); if (val != NULL) { priv->only_trusted = g_variant_get_boolean(val); fwupd_client_object_notify(self, "only-trusted"); } } } static void fwupd_client_update_proxy_name_owner(FwupdClient *self) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autofree gchar *name_owner = g_dbus_proxy_get_name_owner(priv->proxy); /* same */ if (g_strcmp0(priv->proxy_name_owner, name_owner) == 0) return; /* fwupd replaced, started, or quit */ if (name_owner != NULL && priv->proxy_name_owner != NULL) { fwupd_client_set_status(self, FWUPD_STATUS_SHUTDOWN); fwupd_client_signal_emit_changed(self); } else if (name_owner != NULL && priv->proxy_name_owner == NULL) { fwupd_client_signal_emit_changed(self); } else if (name_owner == NULL && priv->proxy_name_owner != NULL) { fwupd_client_set_status(self, FWUPD_STATUS_SHUTDOWN); } /* save so we can detect when the daemon is replaced */ g_free(priv->proxy_name_owner); priv->proxy_name_owner = g_steal_pointer(&name_owner); } static void fwupd_client_name_owner_changed_cb(GDBusProxy *proxy, GParamSpec *pspec, FwupdClient *self) { fwupd_client_update_proxy_name_owner(self); } static void fwupd_client_signal_cb(GDBusProxy *proxy, const gchar *sender_name, const gchar *signal_name, GVariant *parameters, FwupdClient *self) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(FwupdDevice) dev = NULL; g_autoptr(GError) error = NULL; if (g_strcmp0(signal_name, "Changed") == 0) { g_debug("Emitting ::changed()"); fwupd_client_signal_emit_changed(self); return; } if (g_strcmp0(signal_name, "DeviceAdded") == 0) { dev = fwupd_device_new(); if (!fwupd_codec_from_variant(FWUPD_CODEC(dev), parameters, &error)) { g_warning("failed to build FwupdDevice[DeviceAdded]: %s", error->message); return; } g_debug("Emitting ::device-added(%s)", fwupd_device_get_id(dev)); fwupd_client_signal_emit_object(self, SIGNAL_DEVICE_ADDED, G_OBJECT(dev)); return; } if (g_strcmp0(signal_name, "DeviceRemoved") == 0) { dev = fwupd_device_new(); if (!fwupd_codec_from_variant(FWUPD_CODEC(dev), parameters, &error)) { g_warning("failed to build FwupdDevice[DeviceRemoved]: %s", error->message); return; } g_debug("Emitting ::device-removed(%s)", fwupd_device_get_id(dev)); fwupd_client_signal_emit_object(self, SIGNAL_DEVICE_REMOVED, G_OBJECT(dev)); return; } if (g_strcmp0(signal_name, "DeviceChanged") == 0) { dev = fwupd_device_new(); if (!fwupd_codec_from_variant(FWUPD_CODEC(dev), parameters, &error)) { g_warning("failed to build FwupdDevice[DeviceChanged]: %s", error->message); return; } g_debug("Emitting ::device-changed(%s)", fwupd_device_get_id(dev)); fwupd_client_signal_emit_object(self, SIGNAL_DEVICE_CHANGED, G_OBJECT(dev)); /* invalidate request */ if (fwupd_device_get_status(dev) != FWUPD_STATUS_WAITING_FOR_USER) { FwupdRequest *req = g_hash_table_lookup(priv->immediate_requests, fwupd_device_get_id(dev)); if (req != NULL) { fwupd_client_request_invalidate(self, req); g_hash_table_remove(priv->immediate_requests, fwupd_device_get_id(dev)); } } return; } if (g_strcmp0(signal_name, "DeviceRequest") == 0) { g_autoptr(FwupdRequest) req = fwupd_request_new(); if (!fwupd_codec_from_variant(FWUPD_CODEC(req), parameters, &error)) { g_warning("failed to convert DeviceRequest: %s", error->message); return; } g_debug("Emitting ::device-request(%s)", fwupd_request_get_id(req)); fwupd_client_signal_emit_object(self, SIGNAL_DEVICE_REQUEST, G_OBJECT(req)); /* we may need to invalidate this later */ if (fwupd_request_get_kind(req) == FWUPD_REQUEST_KIND_IMMEDIATE && fwupd_request_get_device_id(req) != NULL) { g_hash_table_insert(priv->immediate_requests, g_strdup(fwupd_request_get_device_id(req)), g_object_ref(req)); } return; } g_debug("Unknown signal name '%s' from %s", signal_name, sender_name); } /** * fwupd_client_get_main_context: * @self: a #FwupdClient * * Gets the internal #GMainContext to use for synchronous methods. * By default the value is set a new #GMainContext. * * Returns: (transfer full): the main context * * Since: 1.5.3 **/ GMainContext * fwupd_client_get_main_context(FwupdClient *self) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); if (priv->main_ctx != NULL) return g_main_context_ref(priv->main_ctx); return g_main_context_new(); } /** * fwupd_client_set_main_context: * @self: a #FwupdClient * @main_ctx: (nullable): the global default main context to use * * Sets the internal main context to use for returning progress signals. * * Since: 1.5.3 **/ void fwupd_client_set_main_context(FwupdClient *self, GMainContext *main_ctx) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_CLIENT(self)); if (main_ctx == priv->main_ctx) return; g_clear_pointer(&priv->main_ctx, g_main_context_unref); if (main_ctx != NULL) priv->main_ctx = g_main_context_ref(main_ctx); } /** * fwupd_client_ensure_networking: * @self: a #FwupdClient * @error: (nullable): optional return location for an error * * Sets up the client networking support ready for use. Most other download and * upload methods call this automatically, and do you only need to call this if * the session is being used outside the [class@FwupdClient]. * * Returns: %TRUE for success * * Since: 1.4.5 **/ gboolean fwupd_client_ensure_networking(FwupdClient *self, GError **error) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* check the user agent is sane */ if (priv->user_agent == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "user agent unset"); return FALSE; } if (g_strstr_len(priv->user_agent, -1, "fwupd/") == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "user agent unsuitable; fwupd version required"); return FALSE; } return TRUE; } static int fwupd_client_progress_callback_cb(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { FwupdClient *self = FWUPD_CLIENT(clientp); FwupdClientPrivate *priv = GET_PRIVATE(self); /* calculate percentage */ if (dltotal > 0 && dlnow >= 0 && dlnow <= dltotal) { guint percentage = (guint)((100 * dlnow) / dltotal); if (priv->percentage != percentage) g_info("download progress: %u%%", percentage); fwupd_client_set_percentage(self, percentage); } else if (ultotal > 0 && ulnow >= 0 && ulnow <= ultotal) { guint percentage = (guint)((100 * ulnow) / ultotal); if (priv->percentage != percentage) g_info("upload progress: %u%%", percentage); fwupd_client_set_percentage(self, percentage); } return 0; } static gboolean fwupd_client_curl_helper_set_proxy(FwupdClient *self, FwupdCurlHelper *helper, const gchar *url, GError **error) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_auto(GStrv) proxies = NULL; proxies = g_proxy_resolver_lookup(priv->proxy_resolver, url, NULL, error); if (proxies == NULL) { g_prefix_error(error, "failed to lookup proxy for %s: ", url); return FALSE; } if (g_strcmp0(proxies[0], "direct://") != 0) (void)curl_easy_setopt(helper->curl, CURLOPT_PROXY, proxies[0]); /* success */ return TRUE; } static FwupdCurlHelper * fwupd_client_curl_new(FwupdClient *self, GError **error) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(FwupdCurlHelper) helper = g_new0(FwupdCurlHelper, 1); /* check the user agent is sane */ if (!fwupd_client_ensure_networking(self, error)) return NULL; /* create the session */ helper->curl = curl_easy_init(); if (helper->curl == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to setup networking"); return NULL; } if (g_getenv("FWUPD_CURL_VERBOSE") != NULL) (void)curl_easy_setopt(helper->curl, CURLOPT_VERBOSE, 1L); (void)curl_easy_setopt(helper->curl, CURLOPT_XFERINFOFUNCTION, fwupd_client_progress_callback_cb); (void)curl_easy_setopt(helper->curl, CURLOPT_XFERINFODATA, self); (void)curl_easy_setopt(helper->curl, CURLOPT_USERAGENT, priv->user_agent); (void)curl_easy_setopt(helper->curl, CURLOPT_CONNECTTIMEOUT, 60L); (void)curl_easy_setopt(helper->curl, CURLOPT_NOPROGRESS, 0L); (void)curl_easy_setopt(helper->curl, CURLOPT_FOLLOWLOCATION, 1L); (void)curl_easy_setopt(helper->curl, CURLOPT_MAXREDIRS, 5L); #ifdef _WIN32 (void)curl_easy_setopt(helper->curl, CURLOPT_CAINFO, "ca-bundle.crt"); #endif #if CURL_AT_LEAST_VERSION(7, 71, 0) (void)curl_easy_setopt(helper->curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); #endif /* this disables the double-compression of the firmware.xml.gz file */ (void)curl_easy_setopt(helper->curl, CURLOPT_HTTP_CONTENT_DECODING, 0L); return g_steal_pointer(&helper); } static void fwupd_client_set_hints_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { /* new libfwupd and old daemon, just swallow the error */ if (g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD)) { g_debug("ignoring %s", error->message); g_task_return_boolean(task, TRUE); return; } g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } static void fwupd_client_connect_get_proxy_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); GVariantBuilder builder; GHashTableIter iter; gpointer key, value; FwupdClient *self = g_task_get_source_object(task); FwupdClientPrivate *priv = GET_PRIVATE(self); GCancellable *cancellable = g_task_get_cancellable(task); g_autoptr(GDBusProxy) proxy = NULL; g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; g_autoptr(GVariant) val2 = NULL; g_autoptr(GVariant) val3 = NULL; g_autoptr(GVariant) val4 = NULL; g_autoptr(GVariant) val5 = NULL; g_autoptr(GVariant) val6 = NULL; g_autoptr(GVariant) val7 = NULL; g_autoptr(GVariant) val8 = NULL; g_autoptr(GVariant) val9 = NULL; g_autoptr(GVariant) val10 = NULL; g_autoptr(GMutexLocker) locker = NULL; proxy = g_dbus_proxy_new_finish(res, &error); if (proxy == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } /* another thread did this for us */ locker = g_mutex_locker_new(&priv->proxy_mutex); if (locker == NULL || priv->proxy != NULL) { g_task_return_boolean(task, TRUE); return; } priv->proxy = g_steal_pointer(&proxy); fwupd_client_update_proxy_name_owner(self); /* connect signals, etc. */ g_signal_connect(G_DBUS_PROXY(priv->proxy), "g-properties-changed", G_CALLBACK(fwupd_client_properties_changed_cb), self); g_signal_connect(G_DBUS_PROXY(priv->proxy), "g-signal", G_CALLBACK(fwupd_client_signal_cb), self); g_signal_connect(G_DBUS_PROXY(priv->proxy), "notify::g-name-owner", G_CALLBACK(fwupd_client_name_owner_changed_cb), self); val = g_dbus_proxy_get_cached_property(priv->proxy, "DaemonVersion"); if (val != NULL) fwupd_client_set_daemon_version(self, g_variant_get_string(val, NULL)); val2 = g_dbus_proxy_get_cached_property(priv->proxy, "Tainted"); if (val2 != NULL) priv->tainted = g_variant_get_boolean(val2); val3 = g_dbus_proxy_get_cached_property(priv->proxy, "Status"); if (val3 != NULL) fwupd_client_set_status(self, g_variant_get_uint32(val3)); val4 = g_dbus_proxy_get_cached_property(priv->proxy, "Interactive"); if (val4 != NULL) priv->interactive = g_variant_get_boolean(val4); val5 = g_dbus_proxy_get_cached_property(priv->proxy, "HostProduct"); if (val5 != NULL) fwupd_client_set_host_product(self, g_variant_get_string(val5, NULL)); val10 = g_dbus_proxy_get_cached_property(priv->proxy, "HostVendor"); if (val10 != NULL) fwupd_client_set_host_vendor(self, g_variant_get_string(val10, NULL)); val6 = g_dbus_proxy_get_cached_property(priv->proxy, "HostMachineId"); if (val6 != NULL) fwupd_client_set_host_machine_id(self, g_variant_get_string(val6, NULL)); val7 = g_dbus_proxy_get_cached_property(priv->proxy, "HostSecurityId"); if (val7 != NULL) fwupd_client_set_host_security_id(self, g_variant_get_string(val7, NULL)); val8 = g_dbus_proxy_get_cached_property(priv->proxy, "HostBkc"); if (val8 != NULL) fwupd_client_set_host_bkc(self, g_variant_get_string(val8, NULL)); val9 = g_dbus_proxy_get_cached_property(priv->proxy, "OnlyTrusted"); if (val9 != NULL) priv->only_trusted = g_variant_get_boolean(val9); /* build client hints */ g_variant_builder_init(&builder, G_VARIANT_TYPE("a{ss}")); g_hash_table_iter_init(&iter, priv->hints); while (g_hash_table_iter_next(&iter, &key, &value)) { if (value == NULL) continue; g_variant_builder_add(&builder, "{ss}", (const gchar *)key, (const gchar *)value); } /* only supported on fwupd >= 1.7.1 */ g_dbus_proxy_call(priv->proxy, "SetHints", g_variant_new("(a{ss})", &builder), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_set_hints_cb, g_steal_pointer(&task)); } static void fwupd_client_connect_get_connection_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); GCancellable *cancellable = g_task_get_cancellable(task); g_autoptr(GDBusConnection) connection = NULL; g_autoptr(GError) error = NULL; connection = g_dbus_connection_new_for_address_finish(res, &error); if (connection == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } g_dbus_proxy_new(connection, G_DBUS_PROXY_FLAGS_NONE, NULL, NULL, /* bus_name */ FWUPD_DBUS_PATH, FWUPD_DBUS_INTERFACE, cancellable, fwupd_client_connect_get_proxy_cb, g_steal_pointer(&task)); } /** * fwupd_client_connect_async: * @self: a #FwupdClient * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Sets up the client ready for use. This is probably the first method you call * when wanting to use libfwupd in an asynchronous manner. * * Other methods such as [method@FwupdClient.get_devices_async] should only be called * after [method@FwupdClient.connect_finish] has been called without an error. * * Since: 1.5.0 **/ void fwupd_client_connect_async(FwupdClient *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); const gchar *socket_filename = g_getenv("FWUPD_DBUS_SOCKET"); g_autofree gchar *socket_address = NULL; g_autoptr(GTask) task = g_task_new(self, cancellable, callback, callback_data); g_autoptr(GMutexLocker) locker = g_mutex_locker_new(&priv->proxy_mutex); g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(locker != NULL); /* nothing to do */ if (priv->proxy != NULL) { g_task_return_boolean(task, TRUE); return; } #ifdef FWUPD_DBUS_SOCKET_ADDRESS /* this is set for macOS and Windows */ if (socket_filename == NULL) socket_filename = g_strdup(FWUPD_DBUS_SOCKET_ADDRESS); #endif /* convert from filename to address, if required */ if (socket_filename != NULL) { if (g_strrstr(socket_filename, "=") == NULL) { socket_address = g_strdup_printf("unix:path=%s", socket_filename); } else { socket_address = g_strdup(socket_filename); } } /* use peer-to-peer only if the env variable is set */ if (socket_address != NULL) { g_dbus_connection_new_for_address(socket_address, G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT, NULL, cancellable, fwupd_client_connect_get_connection_cb, g_steal_pointer(&task)); return; } /* typical case */ g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, NULL, FWUPD_DBUS_SERVICE, FWUPD_DBUS_PATH, FWUPD_DBUS_INTERFACE, cancellable, fwupd_client_connect_get_proxy_cb, g_steal_pointer(&task)); } /** * fwupd_client_connect_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@Client.connect_async]. * * Returns: %TRUE for success * * Since: 1.5.0 **/ gboolean fwupd_client_connect_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } /** * fwupd_client_disconnect: (skip) * @self: a #FwupdClient * @error: (nullable): optional return location for an error * * Tears down client after use. You only need to call this method if you are: * * - connecting to the daemon in one thread and finalizing the client in another one * - to change the `FWUPD_DBUS_SOCKET` for a different peer-to-peer connection * - to add or change connection hints as specified by [method@FwupdClient.add_hint]. * * Returns: %TRUE for success * * Since: 1.8.0 **/ gboolean fwupd_client_disconnect(FwupdClient *self, GError **error) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GMutexLocker) locker = g_mutex_locker_new(&priv->proxy_mutex); g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); g_return_val_if_fail(locker != NULL, FALSE); /* sanity check */ if (priv->proxy == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "not connected"); return FALSE; } g_signal_handlers_disconnect_by_data(priv->proxy, self); g_clear_object(&priv->proxy); /* success */ return TRUE; } static void fwupd_client_quit_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdClient *self = FWUPD_CLIENT(g_task_get_source_object(G_TASK(user_data))); FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_clear_object(&priv->proxy); g_task_return_boolean(task, TRUE); } /** * fwupd_client_quit_async: * @self: a #FwupdClient * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Asks the daemon to quit. This can only be called by the root user. * * NOTE: This will only actually quit if an install is not already in progress. * * Since: 1.8.11 **/ void fwupd_client_quit_async(FwupdClient *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "Quit", NULL, G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_quit_cb, g_steal_pointer(&task)); } /** * fwupd_client_quit_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.quit_async]. * * Returns: %TRUE for success * * Since: 1.8.11 **/ gboolean fwupd_client_quit_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } static void fwupd_client_fixup_dbus_error(GError *error) { g_autofree gchar *name = NULL; g_return_if_fail(error != NULL); /* is a remote error? */ if (!g_dbus_error_is_remote_error(error)) return; /* parse the remote error */ name = g_dbus_error_get_remote_error(error); if (name == NULL) return; if (g_str_has_prefix(name, FWUPD_DBUS_INTERFACE)) { error->domain = FWUPD_ERROR; error->code = fwupd_error_from_string(name); } else if (g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN) || g_error_matches(error, G_IO_ERROR, G_IO_ERROR_DBUS_ERROR)) { error->domain = FWUPD_ERROR; error->code = FWUPD_ERROR_NOT_SUPPORTED; } else { error->domain = FWUPD_ERROR; error->code = FWUPD_ERROR_INTERNAL; } g_dbus_error_strip_remote_error(error); } static void fwupd_client_get_host_security_attrs_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) array = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } array = fwupd_codec_array_from_variant(val, FWUPD_TYPE_SECURITY_ATTR, &error); if (array == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_pointer(task, g_steal_pointer(&array), (GDestroyNotify)g_ptr_array_unref); } /** * fwupd_client_get_host_security_attrs_async: * @self: a #FwupdClient * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Gets all the host security attributes from the daemon. * * You must have called [method@Client.connect_async] on @self before using * this method. * * Since: 1.5.0 **/ void fwupd_client_get_host_security_attrs_async(FwupdClient *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "GetHostSecurityAttrs", NULL, G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_get_host_security_attrs_cb, g_steal_pointer(&task)); } /** * fwupd_client_get_host_security_attrs_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.get_host_security_attrs_async]. * * Returns: (element-type FwupdSecurityAttr) (transfer container): attributes * * Since: 1.5.0 **/ GPtrArray * fwupd_client_get_host_security_attrs_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } static void fwupd_client_modify_bios_setting_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } /** * fwupd_client_modify_bios_setting_async: * @self: a #FwupdClient * @settings: (transfer container): BIOS settings * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Modifies a BIOS setting using kernel API. * The daemon will only respond to this request with proper permissions. * * Since: 1.8.4 **/ void fwupd_client_modify_bios_setting_async(FwupdClient *self, GHashTable *settings, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); GVariantBuilder builder; GHashTableIter iter; gpointer key, value; g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(settings != NULL); g_return_if_fail(g_hash_table_size(settings) > 0); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_variant_builder_init(&builder, G_VARIANT_TYPE("a{ss}")); g_hash_table_iter_init(&iter, settings); while (g_hash_table_iter_next(&iter, &key, &value)) { if (value == NULL) continue; g_variant_builder_add(&builder, "{ss}", (const gchar *)key, (const gchar *)value); } g_dbus_proxy_call(priv->proxy, "SetBiosSettings", g_variant_new("(a{ss})", &builder), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_modify_bios_setting_cb, g_steal_pointer(&task)); } /** * fwupd_client_modify_bios_setting_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.modify_bios_setting_async]. * * Returns: %TRUE for success * * Since: 1.8.4 **/ gboolean fwupd_client_modify_bios_setting_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } static void fwupd_client_get_bios_settings_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) array = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } array = fwupd_codec_array_from_variant(val, FWUPD_TYPE_BIOS_SETTING, &error); if (array == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_pointer(task, g_steal_pointer(&array), (GDestroyNotify)g_ptr_array_unref); } /** * fwupd_client_get_bios_settings_async: * @self: a #FwupdClient * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Gets all the host security attributes from the daemon. * * You must have called [method@Client.connect_async] on @self before using * this method. * * Since: 1.8.4 **/ void fwupd_client_get_bios_settings_async(FwupdClient *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "GetBiosSettings", NULL, G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_get_bios_settings_cb, g_steal_pointer(&task)); } /** * fwupd_client_get_bios_settings_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.get_bios_settings_async]. * * Returns: (element-type FwupdBiosSetting) (transfer container): attributes * * Since: 1.8.4 **/ GPtrArray * fwupd_client_get_bios_settings_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } static void fwupd_client_get_host_security_events_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) array = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } array = fwupd_codec_array_from_variant(val, FWUPD_TYPE_SECURITY_ATTR, &error); if (array == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_pointer(task, g_steal_pointer(&array), (GDestroyNotify)g_ptr_array_unref); } /** * fwupd_client_get_host_security_events_async: * @self: a #FwupdClient * @limit: maximum number of events, or 0 for no limit * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Gets all the host security events from the daemon. * * You must have called [method@Client.connect_async] on @self before using * this method. * * Since: 1.7.1 **/ void fwupd_client_get_host_security_events_async(FwupdClient *self, guint limit, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "GetHostSecurityEvents", g_variant_new("(u)", limit), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_get_host_security_events_cb, g_steal_pointer(&task)); } /** * fwupd_client_get_host_security_events_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.get_host_security_events_async]. * * Returns: (element-type FwupdSecurityAttr) (transfer container): attributes * * Since: 1.7.1 **/ GPtrArray * fwupd_client_get_host_security_events_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } static GHashTable * fwupd_client_report_metadata_hash_from_variant(GVariant *value) { GHashTable *hash; gsize sz; g_autoptr(GVariant) untuple = NULL; hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); untuple = g_variant_get_child_value(value, 0); sz = g_variant_n_children(untuple); for (guint i = 0; i < sz; i++) { g_autoptr(GVariant) data = NULL; const gchar *key = NULL; const gchar *val = NULL; data = g_variant_get_child_value(untuple, i); g_variant_get(data, "{&s&s}", &key, &val); g_hash_table_insert(hash, g_strdup(key), g_strdup(val)); } return hash; } static void fwupd_client_get_report_metadata_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_pointer(task, fwupd_client_report_metadata_hash_from_variant(val), (GDestroyNotify)g_hash_table_unref); } /** * fwupd_client_get_report_metadata_async: * @self: a #FwupdClient * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Gets all the report metadata from the daemon. * * You must have called [method@Client.connect_async] on @self before using * this method. * * Since: 1.5.0 **/ void fwupd_client_get_report_metadata_async(FwupdClient *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "GetReportMetadata", NULL, G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_get_report_metadata_cb, g_steal_pointer(&task)); } /** * fwupd_client_get_report_metadata_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.get_report_metadata_async]. * * Returns: (transfer container): attributes * * Since: 1.5.0 **/ GHashTable * fwupd_client_get_report_metadata_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } static void fwupd_client_get_devices_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) array = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } array = fwupd_codec_array_from_variant(val, FWUPD_TYPE_DEVICE, &error); if (array == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } fwupd_device_array_ensure_parents(array); /* success */ g_task_return_pointer(task, g_steal_pointer(&array), (GDestroyNotify)g_ptr_array_unref); } /** * fwupd_client_get_devices_async: * @self: a #FwupdClient * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Gets all the devices registered with the daemon. * * You must have called [method@Client.connect_async] on @self before using * this method. * * Since: 1.5.0 **/ void fwupd_client_get_devices_async(FwupdClient *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "GetDevices", NULL, G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_get_devices_cb, g_steal_pointer(&task)); } /** * fwupd_client_get_devices_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.get_devices_async]. * * Returns: (element-type FwupdDevice) (transfer container): results * * Since: 1.5.0 **/ GPtrArray * fwupd_client_get_devices_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } static void fwupd_client_get_plugins_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) array = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } array = fwupd_codec_array_from_variant(val, FWUPD_TYPE_PLUGIN, &error); if (array == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_pointer(task, g_steal_pointer(&array), (GDestroyNotify)g_ptr_array_unref); } /** * fwupd_client_get_plugins_async: * @self: a #FwupdClient * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Gets all the plugins being used by the daemon. * * You must have called [method@Client.connect_async] on @self before using * this method. * * Since: 1.5.0 **/ void fwupd_client_get_plugins_async(FwupdClient *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "GetPlugins", NULL, G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_get_plugins_cb, g_steal_pointer(&task)); } /** * fwupd_client_get_plugins_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.get_plugins_async]. * * Returns: (element-type FwupdDevice) (transfer container): results * * Since: 1.5.0 **/ GPtrArray * fwupd_client_get_plugins_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } static void fwupd_client_get_history_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) array = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } array = fwupd_codec_array_from_variant(val, FWUPD_TYPE_DEVICE, &error); if (array == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } fwupd_device_array_ensure_parents(array); /* success */ g_task_return_pointer(task, g_steal_pointer(&array), (GDestroyNotify)g_ptr_array_unref); } /** * fwupd_client_get_history_async: * @self: a #FwupdClient * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Gets all the history. * * You must have called [method@Client.connect_async] on @self before using * this method. * * Since: 1.5.0 **/ void fwupd_client_get_history_async(FwupdClient *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "GetHistory", NULL, G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_get_history_cb, g_steal_pointer(&task)); } /** * fwupd_client_get_history_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.get_history_async]. * * Returns: (element-type FwupdDevice) (transfer container): results * * Since: 1.5.0 **/ GPtrArray * fwupd_client_get_history_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } static void fwupd_client_get_device_by_id_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdDevice *device_result = NULL; gsize device_id_len; g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) devices = NULL; const gchar *device_id = g_task_get_task_data(task); devices = fwupd_client_get_devices_finish(FWUPD_CLIENT(source), res, &error); if (devices == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } /* support abbreviated hashes (client side) */ device_id_len = strlen(device_id); for (guint i = 0; i < devices->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices, i); if (strncmp(fwupd_device_get_id(dev), device_id, device_id_len) == 0) { if (device_result != NULL) { g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "more than one matching ID prefix '%s'", device_id); return; } device_result = dev; } } /* one result */ if (device_result != NULL) { g_task_return_pointer(task, g_object_ref(device_result), (GDestroyNotify)g_object_unref); return; } /* failed */ g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "failed to find %s", device_id); } /** * fwupd_client_get_device_by_id_async: * @self: a #FwupdClient * @device_id: (not nullable): the device ID * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Gets a device by it's device ID. * * You must have called [method@Client.connect_async] on @self before using * this method. * * Since: 1.5.0 **/ void fwupd_client_get_device_by_id_async(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(device_id != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_task_set_task_data(task, g_strdup(device_id), g_free); fwupd_client_get_devices_async(self, cancellable, fwupd_client_get_device_by_id_cb, g_steal_pointer(&task)); } /** * fwupd_client_get_device_by_id_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.get_device_by_id_async]. * * Returns: (transfer full): a device, or %NULL for failure * * Since: 1.5.0 **/ FwupdDevice * fwupd_client_get_device_by_id_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } static void fwupd_client_get_devices_by_guid_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(GPtrArray) devices_tmp = NULL; const gchar *guid = g_task_get_task_data(task); /* get all the devices */ devices_tmp = fwupd_client_get_devices_finish(FWUPD_CLIENT(source), res, &error); if (devices_tmp == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } /* find the devices by GUID (client side) */ devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); for (guint i = 0; i < devices_tmp->len; i++) { FwupdDevice *dev_tmp = g_ptr_array_index(devices_tmp, i); if (fwupd_device_has_guid(dev_tmp, guid)) g_ptr_array_add(devices, g_object_ref(dev_tmp)); } /* nothing */ if (devices->len == 0) { g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "failed to find any device providing %s", guid); return; } /* success */ g_task_return_pointer(task, g_steal_pointer(&devices), (GDestroyNotify)g_ptr_array_unref); } /** * fwupd_client_get_devices_by_guid_async: * @self: a #FwupdClient * @guid: the GUID, e.g. `e22c4520-43dc-5bb3-8245-5787fead9b63` * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Gets any devices that provide a specific GUID. An error is returned if no * devices contains this GUID. * * You must have called [method@Client.connect_async] on @self before using * this method. * * Since: 1.5.0 **/ void fwupd_client_get_devices_by_guid_async(FwupdClient *self, const gchar *guid, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(guid != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_task_set_task_data(task, g_strdup(guid), g_free); fwupd_client_get_devices_async(self, cancellable, fwupd_client_get_devices_by_guid_cb, g_steal_pointer(&task)); } /** * fwupd_client_get_devices_by_guid_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.get_devices_by_guid_async]. * * Returns: (element-type FwupdRelease) (transfer container): results * * Since: 1.5.0 **/ GPtrArray * fwupd_client_get_devices_by_guid_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } static void fwupd_client_get_releases_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) array = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } array = fwupd_codec_array_from_variant(val, FWUPD_TYPE_RELEASE, &error); if (array == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_pointer(task, g_steal_pointer(&array), (GDestroyNotify)g_ptr_array_unref); } /** * fwupd_client_get_releases_async: * @self: a #FwupdClient * @device_id: (not nullable): the device ID * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Gets all the releases for a specific device * * You must have called [method@Client.connect_async] on @self before using * this method. * * Since: 1.5.0 **/ void fwupd_client_get_releases_async(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(device_id != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "GetReleases", g_variant_new("(s)", device_id), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_get_releases_cb, g_steal_pointer(&task)); } /** * fwupd_client_get_releases_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.get_releases_async]. * * Returns: (element-type FwupdRelease) (transfer container): results * * Since: 1.5.0 **/ GPtrArray * fwupd_client_get_releases_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } static void fwupd_client_get_downgrades_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) array = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } array = fwupd_codec_array_from_variant(val, FWUPD_TYPE_RELEASE, &error); if (array == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_pointer(task, g_steal_pointer(&array), (GDestroyNotify)g_ptr_array_unref); } /** * fwupd_client_get_downgrades_async: * @self: a #FwupdClient * @device_id: (not nullable): the device ID * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Gets all the downgrades for a specific device. * * You must have called [method@Client.connect_async] on @self before using * this method. * * Since: 1.5.0 **/ void fwupd_client_get_downgrades_async(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(device_id != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "GetDowngrades", g_variant_new("(s)", device_id), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_get_downgrades_cb, g_steal_pointer(&task)); } /** * fwupd_client_get_downgrades_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.get_downgrades_async]. * * Returns: (element-type FwupdRelease) (transfer container): results * * Since: 1.5.0 **/ GPtrArray * fwupd_client_get_downgrades_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } static void fwupd_client_get_upgrades_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) array = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } array = fwupd_codec_array_from_variant(val, FWUPD_TYPE_RELEASE, &error); if (array == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_pointer(task, g_steal_pointer(&array), (GDestroyNotify)g_ptr_array_unref); } /** * fwupd_client_get_upgrades_async: * @self: a #FwupdClient * @device_id: (not nullable): the device ID * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Gets all the upgrades for a specific device. * * You must have called [method@Client.connect_async] on @self before using * this method. * * Since: 1.5.0 **/ void fwupd_client_get_upgrades_async(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(device_id != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "GetUpgrades", g_variant_new("(s)", device_id), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_get_upgrades_cb, g_steal_pointer(&task)); } /** * fwupd_client_get_upgrades_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.get_upgrades_async]. * * Returns: (element-type FwupdRelease) (transfer container): results * * Since: 1.5.0 **/ GPtrArray * fwupd_client_get_upgrades_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } static void fwupd_client_modify_config_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } /** * fwupd_client_modify_config_async: * @self: a #FwupdClient * @section: config section, e.g. `redfish` * @key: config key, e.g. `DisabledPlugins` * @value: config value, e.g. `*` * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Modifies a daemon config option. * The daemon will only respond to this request with proper permissions. * * Since: 2.0.0 **/ void fwupd_client_modify_config_async(FwupdClient *self, const gchar *section, const gchar *key, const gchar *value, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(section != NULL); g_return_if_fail(key != NULL); g_return_if_fail(value != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "ModifyConfig", g_variant_new("(sss)", section, key, value), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_modify_config_cb, g_steal_pointer(&task)); } /** * fwupd_client_modify_config_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.modify_config_async]. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fwupd_client_modify_config_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } static void fwupd_client_reset_config_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } /** * fwupd_client_reset_config_async: * @self: a #FwupdClient * @section: config section, e.g. `redfish` * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Resets a daemon config section. * The daemon will only respond to this request with proper permissions. * * Since: 1.9.15 **/ void fwupd_client_reset_config_async(FwupdClient *self, const gchar *section, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(section != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "ResetConfig", g_variant_new("(s)", section), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_reset_config_cb, g_steal_pointer(&task)); } /** * fwupd_client_reset_config_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.reset_config_async]. * * Returns: %TRUE for success * * Since: 1.9.15 **/ gboolean fwupd_client_reset_config_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } static void fwupd_client_activate_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } /** * fwupd_client_activate_async: * @self: a #FwupdClient * @device_id: a device * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Activates up a device, which normally means the device switches to a new * firmware version. This should only be called when data loss cannot occur. * * Since: 1.5.0 **/ void fwupd_client_activate_async(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(device_id != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "Activate", g_variant_new("(s)", device_id), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_activate_cb, g_steal_pointer(&task)); } /** * fwupd_client_activate_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.activate_async]. * * Returns: %TRUE for success * * Since: 1.5.0 **/ gboolean fwupd_client_activate_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } static void fwupd_client_verify_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } /** * fwupd_client_verify_async: * @self: a #FwupdClient * @device_id: (not nullable): the device ID * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Verify a specific device. * * Since: 1.5.0 **/ void fwupd_client_verify_async(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(device_id != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "Verify", g_variant_new("(s)", device_id), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_verify_cb, g_steal_pointer(&task)); } /** * fwupd_client_verify_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.verify_async]. * * Returns: %TRUE for success * * Since: 1.5.0 **/ gboolean fwupd_client_verify_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } static void fwupd_client_verify_update_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } /** * fwupd_client_verify_update_async: * @self: a #FwupdClient * @device_id: (not nullable): the device ID * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Update the verification record for a specific device. * * Since: 1.5.0 **/ void fwupd_client_verify_update_async(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(device_id != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "VerifyUpdate", g_variant_new("(s)", device_id), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_verify_update_cb, g_steal_pointer(&task)); } /** * fwupd_client_verify_update_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.verify_update_async]. * * Returns: %TRUE for success * * Since: 1.5.0 **/ gboolean fwupd_client_verify_update_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } static void fwupd_client_unlock_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } /** * fwupd_client_unlock_async: * @self: a #FwupdClient * @device_id: (not nullable): the device ID * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Unlocks a specific device so firmware can be read or wrote. * * Since: 1.5.0 **/ void fwupd_client_unlock_async(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(device_id != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "Unlock", g_variant_new("(s)", device_id), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_unlock_cb, g_steal_pointer(&task)); } /** * fwupd_client_unlock_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.unlock_async]. * * Returns: %TRUE for success * * Since: 1.5.0 **/ gboolean fwupd_client_unlock_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } static void fwupd_client_clear_results_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } /** * fwupd_client_clear_results_async: * @self: a #FwupdClient * @device_id: a device * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Clears the results for a specific device. * * Since: 1.5.0 **/ void fwupd_client_clear_results_async(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(device_id != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "ClearResults", g_variant_new("(s)", device_id), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_clear_results_cb, g_steal_pointer(&task)); } /** * fwupd_client_clear_results_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.clear_results_async]. * * Returns: %TRUE for success * * Since: 1.5.0 **/ gboolean fwupd_client_clear_results_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } static void fwupd_client_get_results_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(FwupdDevice) device = fwupd_device_new(); g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } if (!fwupd_codec_from_variant(FWUPD_CODEC(device), val, &error)) { g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_pointer(task, g_steal_pointer(&device), (GDestroyNotify)g_ptr_array_unref); } /** * fwupd_client_get_results_async: * @self: a #FwupdClient * @device_id: (not nullable): the device ID * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Gets the results of a previous firmware update for a specific device. * * You must have called [method@Client.connect_async] on @self before using * this method. * * Since: 1.5.0 **/ void fwupd_client_get_results_async(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(device_id != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "GetResults", g_variant_new("(s)", device_id), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_get_results_cb, g_steal_pointer(&task)); } /** * fwupd_client_get_results_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.get_results_async]. * * Returns: (transfer full): a device, or %NULL for failure * * Since: 1.5.0 **/ FwupdDevice * fwupd_client_get_results_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } #ifdef HAVE_GIO_UNIX static void fwupd_client_install_stream_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GDBusMessage) msg = NULL; g_autoptr(GError) error = NULL; g_autoptr(GTask) task = G_TASK(user_data); msg = g_dbus_connection_send_message_with_reply_finish(G_DBUS_CONNECTION(source), res, &error); if (msg == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } if (g_dbus_message_to_gerror(msg, &error)) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } void fwupd_client_install_stream_async(FwupdClient *self, const gchar *device_id, GUnixInputStream *istr, const gchar *filename_hint, FwupdInstallFlags install_flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); GVariantBuilder builder; g_autoptr(GDBusMessage) request = NULL; g_autoptr(GUnixFDList) fd_list = NULL; g_autoptr(GTask) task = g_task_new(self, cancellable, callback, callback_data); /* set options */ g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); g_variant_builder_add(&builder, "{sv}", "reason", g_variant_new_string("user-action")); if (filename_hint != NULL) { g_variant_builder_add(&builder, "{sv}", "filename", g_variant_new_string(filename_hint)); } g_variant_builder_add(&builder, "{sv}", "install-flags", g_variant_new_uint64(install_flags)); /* set out of band file descriptor */ fd_list = g_unix_fd_list_new(); g_unix_fd_list_append(fd_list, g_unix_input_stream_get_fd(istr), NULL); request = g_dbus_message_new_method_call(FWUPD_DBUS_SERVICE, FWUPD_DBUS_PATH, FWUPD_DBUS_INTERFACE, "Install"); g_dbus_message_set_unix_fd_list(request, fd_list); /* call into daemon */ g_dbus_message_set_body( request, g_variant_new("(sha{sv})", device_id, g_unix_input_stream_get_fd(istr), &builder)); g_dbus_connection_send_message_with_reply(g_dbus_proxy_get_connection(priv->proxy), request, G_DBUS_SEND_MESSAGE_FLAGS_NONE, G_MAXINT, NULL, cancellable, fwupd_client_install_stream_cb, g_steal_pointer(&task)); } #endif /** * fwupd_client_install_bytes_async: * @self: a #FwupdClient * @device_id: (not nullable): the device ID * @bytes: cabinet archive * @install_flags: install flags, e.g. %FWUPD_INSTALL_FLAG_ALLOW_REINSTALL * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Install firmware onto a specific device. * * NOTE: This method is thread-safe, but progress signals will be * emitted in the global default main context, if not explicitly set with * [method@Client.set_main_context]. * * Since: 1.5.0 **/ void fwupd_client_install_bytes_async(FwupdClient *self, const gchar *device_id, GBytes *bytes, FwupdInstallFlags install_flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { #ifdef HAVE_GIO_UNIX FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GError) error = NULL; g_autoptr(GUnixInputStream) istr = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* move to a thread if this ever takes more than a few ms */ istr = fwupd_unix_input_stream_from_bytes(bytes, &error); if (istr == NULL) { g_autoptr(GTask) task = g_task_new(self, cancellable, callback, callback_data); g_task_return_error(task, g_steal_pointer(&error)); return; } /* call into daemon */ fwupd_client_install_stream_async(self, device_id, istr, NULL, install_flags, cancellable, callback, callback_data); #else g_autoptr(GTask) task = g_task_new(self, cancellable, callback, callback_data); g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Install CAB only supported on Linux"); #endif } /** * fwupd_client_install_bytes_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.install_bytes_async]. * * Returns: %TRUE for success * * Since: 1.5.0 **/ gboolean fwupd_client_install_bytes_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } /** * fwupd_client_install_async: * @self: a #FwupdClient * @device_id: (not nullable): the device ID * @filename: the filename to install * @install_flags: install flags, e.g. %FWUPD_INSTALL_FLAG_ALLOW_REINSTALL * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Install firmware onto a specific device. * * NOTE: This method is thread-safe, but progress signals will be * emitted in the global default main context, if not explicitly set with * [method@Client.set_main_context]. * * Since: 1.5.0 **/ void fwupd_client_install_async(FwupdClient *self, const gchar *device_id, const gchar *filename, FwupdInstallFlags install_flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { #ifdef HAVE_GIO_UNIX FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GError) error = NULL; g_autoptr(GUnixInputStream) istr = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(device_id != NULL); g_return_if_fail(filename != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* move to a thread if this ever takes more than a few ms */ istr = fwupd_unix_input_stream_from_fn(filename, &error); if (istr == NULL) { g_autoptr(GTask) task = g_task_new(self, cancellable, callback, callback_data); g_task_return_error(task, g_steal_pointer(&error)); return; } /* call into daemon */ fwupd_client_install_stream_async(self, device_id, istr, NULL, install_flags, cancellable, callback, callback_data); #else g_autoptr(GTask) task = g_task_new(self, cancellable, callback, callback_data); g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Install CAB async only supported on Linux"); #endif } /** * fwupd_client_install_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.install_async]. * * Returns: %TRUE for success * * Since: 1.5.0 **/ gboolean fwupd_client_install_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } typedef struct { FwupdDevice *device; FwupdRelease *release; FwupdInstallFlags install_flags; FwupdClientDownloadFlags download_flags; } FwupdClientInstallReleaseData; static void fwupd_client_install_release_data_free(FwupdClientInstallReleaseData *data) { g_object_unref(data->device); g_object_unref(data->release); g_free(data); } static void fwupd_client_install_release_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GError) error = NULL; g_autoptr(GTask) task = G_TASK(user_data); if (!fwupd_client_install_release_finish(FWUPD_CLIENT(source), res, &error)) { g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } static void fwupd_client_install_release_bytes_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GError) error = NULL; g_autoptr(GTask) task = G_TASK(user_data); if (!fwupd_client_install_bytes_finish(FWUPD_CLIENT(source), res, &error)) { g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } static void fwupd_client_install_release_download_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GBytes) blob = NULL; g_autoptr(GError) error = NULL; g_autoptr(GTask) task = G_TASK(user_data); FwupdClientInstallReleaseData *data = g_task_get_task_data(task); GChecksumType checksum_type; GCancellable *cancellable = g_task_get_cancellable(task); const gchar *checksum_expected; g_autofree gchar *checksum_actual = NULL; blob = fwupd_client_download_bytes_finish(FWUPD_CLIENT(source), res, &error); if (blob == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } /* verify checksum */ checksum_expected = fwupd_checksum_get_best(fwupd_release_get_checksums(data->release)); checksum_type = fwupd_checksum_guess_kind(checksum_expected); checksum_actual = g_compute_checksum_for_bytes(checksum_type, blob); if (g_strcmp0(checksum_expected, checksum_actual) != 0) { g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "checksum invalid, expected %s got %s", checksum_expected, checksum_actual); return; } fwupd_client_install_bytes_async(FWUPD_CLIENT(source), fwupd_device_get_id(data->device), blob, data->install_flags, cancellable, fwupd_client_install_release_bytes_cb, g_steal_pointer(&task)); } static gboolean fwupd_client_is_url_http(const gchar *perhaps_url) { g_autoptr(CURLU) h = curl_url(); return curl_url_set(h, CURLUPART_URL, perhaps_url, 0) == CURLUE_OK; } static gboolean fwupd_client_is_url_ipfs(const gchar *perhaps_url) { if (perhaps_url == NULL) return FALSE; return g_str_has_prefix(perhaps_url, "ipfs://") || g_str_has_prefix(perhaps_url, "ipns://"); } static gboolean fwupd_client_is_localhost(const gchar *url) { g_autoptr(CURLU) h = curl_url(); g_autoptr(CURLSTR) hostname = NULL; if (curl_url_set(h, CURLUPART_URL, url, 0) != CURLUE_OK) return FALSE; (void)curl_url_get(h, CURLUPART_HOST, &hostname, 0); return g_strcmp0(hostname, "localhost") == 0; } static gboolean fwupd_client_is_url_p2p(const gchar *perhaps_url) { if (perhaps_url == NULL) return FALSE; if (fwupd_client_is_url_ipfs(perhaps_url)) return TRUE; if (fwupd_client_is_localhost(perhaps_url)) return TRUE; return FALSE; } static void fwupd_client_install_release_remote_cb(GObject *source, GAsyncResult *res, gpointer user_data) { GPtrArray *locations; const gchar *uri_tmp; g_autofree gchar *fn = NULL; g_autoptr(FwupdRemote) remote = NULL; g_autoptr(GError) error = NULL; g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GPtrArray) uris_built = g_ptr_array_new_with_free_func(g_free); FwupdClientInstallReleaseData *data = g_task_get_task_data(task); GCancellable *cancellable = g_task_get_cancellable(task); /* if a remote-id was specified, the remote has to exist */ remote = fwupd_client_get_remote_by_id_finish(FWUPD_CLIENT(source), res, &error); if (remote == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } /* get the default release only until other parts of fwupd can cope */ locations = fwupd_release_get_locations(data->release); if (locations->len == 0) { g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "release missing URI"); return; } uri_tmp = g_ptr_array_index(locations, 0); /* local and directory remotes may have the firmware already */ if (fwupd_remote_get_kind(remote) == FWUPD_REMOTE_KIND_LOCAL && !fwupd_client_is_url_http(uri_tmp)) { const gchar *fn_cache = fwupd_remote_get_filename_cache(remote); g_autofree gchar *path = g_path_get_dirname(fn_cache); fn = g_build_filename(path, uri_tmp, NULL); } else if (fwupd_remote_get_kind(remote) == FWUPD_REMOTE_KIND_DIRECTORY) { fn = g_strdup(uri_tmp + 7); } /* install with flags chosen by the user */ if (fn != NULL) { fwupd_client_install_async(FWUPD_CLIENT(source), fwupd_device_get_id(data->device), fn, data->install_flags, cancellable, fwupd_client_install_release_cb, g_steal_pointer(&task)); return; } /* maybe get payload from Passim */ if (fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_ALLOW_P2P_FIRMWARE)) { const gchar *checksum_sha256 = fwupd_checksum_get_by_kind(fwupd_release_get_checksums(data->release), G_CHECKSUM_SHA256); if (checksum_sha256 != NULL) { g_autofree gchar *basename = g_path_get_basename(fwupd_release_get_filename(data->release)); g_ptr_array_add(uris_built, g_strdup_printf("https://localhost:27500/%s?sha256=%s", basename, checksum_sha256)); } } /* remote file */ for (guint i = 0; i < locations->len; i++) { uri_tmp = g_ptr_array_index(locations, i); if (fwupd_client_is_url_p2p(uri_tmp)) { g_ptr_array_add(uris_built, g_strdup(uri_tmp)); } else if (fwupd_client_is_url_http(uri_tmp)) { g_autofree gchar *uri_str = NULL; uri_str = fwupd_remote_build_firmware_uri(remote, uri_tmp, &error); if (uri_str == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } g_ptr_array_add(uris_built, g_steal_pointer(&uri_str)); } else { g_debug("do not how to handle URI %s", uri_tmp); } } if (uris_built->len == 0) { g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "No URIs to download"); return; } /* download file */ fwupd_client_download_bytes2_async(FWUPD_CLIENT(source), uris_built, data->download_flags, cancellable, fwupd_client_install_release_download_cb, g_steal_pointer(&task)); } static GPtrArray * fwupd_client_filter_locations(GPtrArray *locations, FwupdClientDownloadFlags download_flags, GError **error) { g_autoptr(GPtrArray) uris_filtered = g_ptr_array_new_with_free_func(g_free); g_return_val_if_fail(locations != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); for (guint i = 0; i < locations->len; i++) { const gchar *uri = g_ptr_array_index(locations, i); if ((download_flags & FWUPD_CLIENT_DOWNLOAD_FLAG_ONLY_P2P) > 0 && !fwupd_client_is_url_p2p(uri)) continue; g_ptr_array_add(uris_filtered, g_strdup(uri)); } if (uris_filtered->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no valid release URIs"); return NULL; } return g_steal_pointer(&uris_filtered); } /** * fwupd_client_install_release_async: * @self: a #FwupdClient * @device: (not nullable): a device * @release: (not nullable): a release * @install_flags: install flags, e.g. %FWUPD_INSTALL_FLAG_ALLOW_REINSTALL * @download_flags: download flags, e.g. %FWUPD_CLIENT_DOWNLOAD_FLAG_ONLY_P2P * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Installs a new release on a device, downloading the firmware if required. * * NOTE: This method is thread-safe, but progress signals will be * emitted in the global default main context, if not explicitly set with * [method@Client.set_main_context]. * * Since: 2.0.0 **/ void fwupd_client_install_release_async(FwupdClient *self, FwupdDevice *device, FwupdRelease *release, FwupdInstallFlags install_flags, FwupdClientDownloadFlags download_flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; FwupdClientInstallReleaseData *data; const gchar *remote_id; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(FWUPD_IS_DEVICE(device)); g_return_if_fail(FWUPD_IS_RELEASE(release)); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); data = g_new0(FwupdClientInstallReleaseData, 1); data->device = g_object_ref(device); data->release = g_object_ref(release); data->download_flags = download_flags; data->install_flags = install_flags; g_task_set_task_data(task, data, (GDestroyNotify)fwupd_client_install_release_data_free); /* work out what remote-specific URI fields this should use */ remote_id = fwupd_release_get_remote_id(release); if (remote_id == NULL) { fwupd_client_download_bytes2_async(self, fwupd_release_get_locations(release), download_flags, cancellable, fwupd_client_install_release_download_cb, g_steal_pointer(&task)); return; } /* if a remote-id was specified, the remote has to exist */ fwupd_client_get_remote_by_id_async(self, remote_id, cancellable, fwupd_client_install_release_remote_cb, g_steal_pointer(&task)); } /** * fwupd_client_install_release_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.install_release_async]. * * Returns: %TRUE for success * * Since: 1.5.0 **/ gboolean fwupd_client_install_release_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } #ifdef HAVE_GIO_UNIX static void fwupd_client_get_details_stream_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GDBusMessage) msg = NULL; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) array = NULL; g_autoptr(GTask) task = G_TASK(user_data); msg = g_dbus_connection_send_message_with_reply_finish(G_DBUS_CONNECTION(source), res, &error); if (msg == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } if (g_dbus_message_to_gerror(msg, &error)) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } array = fwupd_codec_array_from_variant(g_dbus_message_get_body(msg), FWUPD_TYPE_DEVICE, &error); if (array == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_pointer(task, g_steal_pointer(&array), (GDestroyNotify)g_ptr_array_unref); } void fwupd_client_get_details_stream_async(FwupdClient *self, GUnixInputStream *istr, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); gint fd = g_unix_input_stream_get_fd(istr); g_autoptr(GDBusMessage) request = NULL; g_autoptr(GUnixFDList) fd_list = NULL; g_autoptr(GTask) task = g_task_new(self, cancellable, callback, callback_data); /* set out of band file descriptor */ fd_list = g_unix_fd_list_new(); g_unix_fd_list_append(fd_list, fd, NULL); request = g_dbus_message_new_method_call(FWUPD_DBUS_SERVICE, FWUPD_DBUS_PATH, FWUPD_DBUS_INTERFACE, "GetDetails"); g_dbus_message_set_unix_fd_list(request, fd_list); /* call into daemon */ g_dbus_message_set_body(request, g_variant_new("(h)", fd)); g_dbus_connection_send_message_with_reply(g_dbus_proxy_get_connection(priv->proxy), request, G_DBUS_SEND_MESSAGE_FLAGS_NONE, G_MAXINT, NULL, cancellable, fwupd_client_get_details_stream_cb, g_steal_pointer(&task)); } #endif /** * fwupd_client_get_details_bytes_async: * @self: a #FwupdClient * @bytes: firmware archive * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Gets details about a specific firmware file. * * Since: 1.5.0 **/ void fwupd_client_get_details_bytes_async(FwupdClient *self, GBytes *bytes, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { #ifdef HAVE_GIO_UNIX FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GError) error = NULL; g_autoptr(GUnixInputStream) istr = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* move to a thread if this ever takes more than a few ms */ istr = fwupd_unix_input_stream_from_bytes(bytes, &error); if (istr == NULL) { g_autoptr(GTask) task = g_task_new(self, cancellable, callback, callback_data); g_task_return_error(task, g_steal_pointer(&error)); return; } /* call into daemon */ fwupd_client_get_details_stream_async(self, istr, cancellable, callback, callback_data); #else g_autoptr(GTask) task = g_task_new(self, cancellable, callback, callback_data); g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Get Details only supported on Linux"); #endif } /** * fwupd_client_get_details_bytes_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.get_details_bytes_async]. * * Returns: (transfer container) (element-type FwupdDevice): an array of results * * Since: 1.5.0 **/ GPtrArray * fwupd_client_get_details_bytes_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } /** * fwupd_client_get_details_async: * @self: a #FwupdClient * @filename: firmware archive * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Gets details about a specific firmware file. * * Since: 2.0.1 **/ void fwupd_client_get_details_async(FwupdClient *self, const gchar *filename, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { #ifdef HAVE_GIO_UNIX FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GError) error = NULL; g_autoptr(GUnixInputStream) istr = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(filename != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* move to a thread if this ever takes more than a few ms */ istr = fwupd_unix_input_stream_from_fn(filename, &error); if (istr == NULL) { g_autoptr(GTask) task = g_task_new(self, cancellable, callback, callback_data); g_task_return_error(task, g_steal_pointer(&error)); return; } /* call into daemon */ fwupd_client_get_details_stream_async(self, istr, cancellable, callback, callback_data); #else g_autoptr(GTask) task = g_task_new(self, cancellable, callback, callback_data); g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Get Details only supported on Linux"); #endif } /** * fwupd_client_get_details_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.get_details_async]. * * Returns: (transfer container) (element-type FwupdDevice): an array of results * * Since: 2.0.1 **/ GPtrArray * fwupd_client_get_details_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } /** * fwupd_client_get_percentage: * @self: a #FwupdClient * * Gets the last returned percentage value. * * Returns: a percentage, or 0 for unknown. * * Since: 0.7.3 **/ guint fwupd_client_get_percentage(FwupdClient *self) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_CLIENT(self), 0); return priv->percentage; } /** * fwupd_client_get_daemon_version: * @self: a #FwupdClient * * Gets the daemon version number. * * Returns: a string, or %NULL for unknown. * * Since: 0.9.6 **/ const gchar * fwupd_client_get_daemon_version(FwupdClient *self) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); return priv->daemon_version; } /** * fwupd_client_get_host_bkc: * @self: a #FwupdClient * * Gets the host best known configuration, e.g. `vendor-factory-2021q1,mycompany-2023`. * * Returns: a string, or %NULL for unknown. * * Since: 1.7.3 **/ const gchar * fwupd_client_get_host_bkc(FwupdClient *self) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); return priv->host_bkc; } /** * fwupd_client_get_host_product: * @self: a #FwupdClient * * Gets the string that represents the host running fwupd * * Returns: a string, or %NULL for unknown. * * Since: 1.3.1 **/ const gchar * fwupd_client_get_host_product(FwupdClient *self) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); return priv->host_product; } /** * fwupd_client_get_host_vendor: * @self: a #FwupdClient * * Gets the string that represents the vendor of the host running fwupd * * Returns: a string, or %NULL for unknown. * * Since: 1.8.2 **/ const gchar * fwupd_client_get_host_vendor(FwupdClient *self) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); return priv->host_vendor; } /** * fwupd_client_get_host_machine_id: * @self: a #FwupdClient * * Gets the string that represents the host machine ID * * Returns: a string, or %NULL for unknown. * * Since: 1.3.2 **/ const gchar * fwupd_client_get_host_machine_id(FwupdClient *self) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); return priv->host_machine_id; } /** * fwupd_client_get_host_security_id: * @self: a #FwupdClient * * Gets the string that represents the host machine ID * * Returns: a string, or %NULL for unknown. * * Since: 1.5.0 **/ const gchar * fwupd_client_get_host_security_id(FwupdClient *self) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); return priv->host_security_id; } /** * fwupd_client_get_battery_level: * @self: a #FwupdClient * * Returns the system battery level. * * Returns: value in percent * * Since: 1.8.1 **/ guint32 fwupd_client_get_battery_level(FwupdClient *self) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_CLIENT(self), FWUPD_BATTERY_LEVEL_INVALID); return priv->battery_level; } /** * fwupd_client_get_battery_threshold: * @self: a #FwupdClient * * Returns the system battery threshold under which a firmware update cannot be * performed. * * Returns: value in percent * * Since: 1.8.1 **/ guint32 fwupd_client_get_battery_threshold(FwupdClient *self) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_CLIENT(self), FWUPD_BATTERY_LEVEL_INVALID); return priv->battery_threshold; } /** * fwupd_client_get_status: * @self: a #FwupdClient * * Gets the last returned status value. * * Returns: a #FwupdStatus, or %FWUPD_STATUS_UNKNOWN for unknown. * * Since: 0.7.3 **/ FwupdStatus fwupd_client_get_status(FwupdClient *self) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_CLIENT(self), FWUPD_STATUS_UNKNOWN); return priv->status; } /** * fwupd_client_get_tainted: * @self: a #FwupdClient * * Gets if the daemon has been tainted by 3rd party code. * * Returns: %TRUE if the daemon is unsupported * * Since: 1.2.4 **/ gboolean fwupd_client_get_tainted(FwupdClient *self) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); return priv->tainted; } /** * fwupd_client_get_only_trusted: * @self: a #FwupdClient * * Gets if the daemon is verifying signatures from a trusted authority. * * Returns: %TRUE if the daemon is checking signatures * * Since: 1.8.0 **/ gboolean fwupd_client_get_only_trusted(FwupdClient *self) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); return priv->only_trusted; } /** * fwupd_client_get_daemon_interactive: * @self: a #FwupdClient * * Gets if the daemon is running in an interactive terminal. * * Returns: %TRUE if the daemon is running in an interactive terminal * * Since: 1.3.4 **/ gboolean fwupd_client_get_daemon_interactive(FwupdClient *self) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); return priv->interactive; } #ifdef HAVE_GIO_UNIX static void fwupd_client_update_metadata_stream_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GDBusMessage) msg = NULL; g_autoptr(GError) error = NULL; g_autoptr(GTask) task = G_TASK(user_data); msg = g_dbus_connection_send_message_with_reply_finish(G_DBUS_CONNECTION(source), res, &error); if (msg == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } if (g_dbus_message_to_gerror(msg, &error)) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } void fwupd_client_update_metadata_stream_async(FwupdClient *self, const gchar *remote_id, GUnixInputStream *istr, GUnixInputStream *istr_sig, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GDBusMessage) request = NULL; g_autoptr(GUnixFDList) fd_list = NULL; g_autoptr(GTask) task = g_task_new(self, cancellable, callback, callback_data); /* set out of band file descriptor */ fd_list = g_unix_fd_list_new(); g_unix_fd_list_append(fd_list, g_unix_input_stream_get_fd(istr), NULL); g_unix_fd_list_append(fd_list, g_unix_input_stream_get_fd(istr_sig), NULL); request = g_dbus_message_new_method_call(FWUPD_DBUS_SERVICE, FWUPD_DBUS_PATH, FWUPD_DBUS_INTERFACE, "UpdateMetadata"); g_dbus_message_set_unix_fd_list(request, fd_list); /* call into daemon */ g_dbus_message_set_body(request, g_variant_new("(shh)", remote_id, g_unix_input_stream_get_fd(istr), g_unix_input_stream_get_fd(istr_sig))); g_dbus_connection_send_message_with_reply(g_dbus_proxy_get_connection(priv->proxy), request, G_DBUS_SEND_MESSAGE_FLAGS_NONE, G_MAXINT, NULL, cancellable, fwupd_client_update_metadata_stream_cb, g_steal_pointer(&task)); } #endif /** * fwupd_client_update_metadata_bytes_async: * @self: a #FwupdClient * @remote_id: remote ID, e.g. `lvfs-testing` * @metadata: XML metadata data * @signature: signature data * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Updates the metadata. This allows a session process to download the metadata * and metadata signing file to be passed into the daemon to be checked and * parsed. * * The @remote_id allows the firmware to be tagged so that the remote can be * matched when the firmware is downloaded. * * NOTE: This method is thread-safe, but progress signals will be * emitted in the global default main context, if not explicitly set with * [method@Client.set_main_context]. * * Since: 1.5.0 **/ void fwupd_client_update_metadata_bytes_async(FwupdClient *self, const gchar *remote_id, GBytes *metadata, GBytes *signature, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { #ifdef HAVE_GIO_UNIX FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GError) error = NULL; g_autoptr(GUnixInputStream) istr = NULL; g_autoptr(GUnixInputStream) istr_sig = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(remote_id != NULL); g_return_if_fail(metadata != NULL); g_return_if_fail(signature != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* move to a thread if this ever takes more than a few ms */ istr = fwupd_unix_input_stream_from_bytes(metadata, &error); if (istr == NULL) { g_autoptr(GTask) task = g_task_new(self, cancellable, callback, callback_data); g_task_return_error(task, g_steal_pointer(&error)); return; } istr_sig = fwupd_unix_input_stream_from_bytes(signature, &error); if (istr_sig == NULL) { g_autoptr(GTask) task = g_task_new(self, cancellable, callback, callback_data); g_task_return_error(task, g_steal_pointer(&error)); return; } /* call into daemon */ fwupd_client_update_metadata_stream_async(self, remote_id, istr, istr_sig, cancellable, callback, callback_data); #else g_autoptr(GTask) task = g_task_new(self, cancellable, callback, callback_data); g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Update metadata only supported on Linux"); #endif } /** * fwupd_client_update_metadata_bytes_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.update_metadata_bytes_async]. * * Returns: %TRUE for success * * Since: 1.5.0 **/ gboolean fwupd_client_update_metadata_bytes_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } typedef struct { FwupdRemote *remote; FwupdClientDownloadFlags download_flags; GBytes *signature; GBytes *metadata; } FwupdClientRefreshRemoteData; static void fwupd_client_refresh_remote_data_free(FwupdClientRefreshRemoteData *data) { if (data->signature != NULL) g_bytes_unref(data->signature); if (data->metadata != NULL) g_bytes_unref(data->metadata); g_object_unref(data->remote); g_free(data); } static void fwupd_client_refresh_remote_update_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GError) error = NULL; g_autoptr(GTask) task = G_TASK(user_data); /* save metadata */ if (!fwupd_client_update_metadata_bytes_finish(FWUPD_CLIENT(source), res, &error)) { g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } static void fwupd_client_refresh_remote_metadata_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GBytes) bytes = NULL; g_autoptr(GError) error = NULL; g_autoptr(GTask) task = G_TASK(user_data); FwupdClientRefreshRemoteData *data = g_task_get_task_data(task); FwupdClient *self = g_task_get_source_object(task); GCancellable *cancellable = g_task_get_cancellable(task); /* save metadata */ bytes = fwupd_client_download_bytes_finish(FWUPD_CLIENT(source), res, &error); if (bytes == NULL) { g_prefix_error(&error, "Failed to download metadata for %s: ", fwupd_remote_get_id(data->remote)); g_task_return_error(task, g_steal_pointer(&error)); return; } data->metadata = g_steal_pointer(&bytes); /* verify this was what we expected */ if (fwupd_remote_get_checksum_metadata(data->remote) != NULL) { GChecksumType checksum_kind = fwupd_checksum_guess_kind(fwupd_remote_get_checksum_metadata(data->remote)); g_autofree gchar *checksum = g_compute_checksum_for_bytes(checksum_kind, data->metadata); if (g_strcmp0(checksum, fwupd_remote_get_checksum_metadata(data->remote)) != 0) { g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "metadata checksum expected %s and got %s", fwupd_remote_get_checksum_metadata(data->remote), checksum); return; } } /* send all this to fwupd */ fwupd_client_update_metadata_bytes_async(self, fwupd_remote_get_id(data->remote), data->metadata, data->signature, cancellable, fwupd_client_refresh_remote_update_cb, g_steal_pointer(&task)); } static void fwupd_client_refresh_remote_signature_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GBytes) bytes = NULL; g_autoptr(GError) error = NULL; g_autoptr(GTask) task = G_TASK(user_data); FwupdClientRefreshRemoteData *data = g_task_get_task_data(task); FwupdClient *self = g_task_get_source_object(task); GCancellable *cancellable = g_task_get_cancellable(task); g_autoptr(GPtrArray) urls = g_ptr_array_new_with_free_func(g_free); /* save signature */ bytes = fwupd_client_download_bytes_finish(FWUPD_CLIENT(source), res, &error); if (bytes == NULL) { g_prefix_error(&error, "Failed to download metadata for %s: ", fwupd_remote_get_id(data->remote)); g_task_return_error(task, g_steal_pointer(&error)); return; } data->signature = g_steal_pointer(&bytes); if (!fwupd_remote_load_signature_bytes(data->remote, data->signature, &error)) { g_prefix_error(&error, "Failed to load signature: "); g_task_return_error(task, g_steal_pointer(&error)); return; } /* is the signature checksum the same? */ if (fwupd_remote_get_checksum(data->remote) != NULL) { GChecksumType checksum_kind = fwupd_checksum_guess_kind(fwupd_remote_get_checksum(data->remote)); g_autofree gchar *checksum = g_compute_checksum_for_data( checksum_kind, (const guchar *)g_bytes_get_data(data->signature, NULL), g_bytes_get_size(data->signature)); if (g_strcmp0(checksum, fwupd_remote_get_checksum(data->remote)) == 0) { g_info("metadata signature of %s is unchanged, skipping", fwupd_remote_get_id(data->remote)); g_task_return_boolean(task, TRUE); return; } } /* maybe get metadata from Passim */ if (fwupd_remote_has_flag(data->remote, FWUPD_REMOTE_FLAG_ALLOW_P2P_METADATA) && fwupd_remote_get_checksum_metadata(data->remote) != NULL && fwupd_remote_get_username(data->remote) == NULL && fwupd_remote_get_password(data->remote) == NULL) { g_autofree gchar *basename = g_path_get_basename(fwupd_remote_get_metadata_uri(data->remote)); g_ptr_array_add(urls, g_strdup_printf("https://localhost:27500/%s?sha256=%s", basename, fwupd_remote_get_checksum_metadata(data->remote))); } if ((data->download_flags & FWUPD_CLIENT_DOWNLOAD_FLAG_ONLY_P2P) == 0) { g_autofree gchar *uri = fwupd_remote_build_metadata_uri(data->remote, &error); if (uri == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } g_ptr_array_add(urls, g_steal_pointer(&uri)); } fwupd_client_download_bytes2_async(self, urls, FWUPD_CLIENT_DOWNLOAD_FLAG_NONE, cancellable, fwupd_client_refresh_remote_metadata_cb, g_steal_pointer(&task)); } /** * fwupd_client_refresh_remote_async: * @self: a #FwupdClient * @remote: a #FwupdRemote * @download_flags: download flags, e.g. %FWUPD_CLIENT_DOWNLOAD_FLAG_ONLY_P2P * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Refreshes a remote by downloading new metadata. * * NOTE: This method is thread-safe, but progress signals will be * emitted in the global default main context, if not explicitly set with * [method@Client.set_main_context]. * * Since: 2.0.0 **/ void fwupd_client_refresh_remote_async(FwupdClient *self, FwupdRemote *remote, FwupdClientDownloadFlags download_flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientRefreshRemoteData *data; g_autofree gchar *uri = NULL; g_autoptr(GTask) task = NULL; g_autoptr(GError) error = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(FWUPD_IS_REMOTE(remote)); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); task = g_task_new(self, cancellable, callback, callback_data); data = g_new0(FwupdClientRefreshRemoteData, 1); data->download_flags = download_flags; data->remote = g_object_ref(remote); g_task_set_task_data(task, g_steal_pointer(&data), (GDestroyNotify)fwupd_client_refresh_remote_data_free); /* nothing to do */ if (fwupd_remote_get_kind(remote) != FWUPD_REMOTE_KIND_DOWNLOAD) { g_debug("ignoring %s as %s", fwupd_remote_get_id(remote), fwupd_remote_kind_to_string(fwupd_remote_get_kind(remote))); g_task_return_boolean(task, TRUE); return; } /* sanity check */ if (fwupd_remote_get_metadata_uri_sig(remote) == NULL || fwupd_remote_get_metadata_uri(remote) == NULL) { g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no metadata URIs for %s", fwupd_remote_get_id(remote)); return; } /* download signature */ uri = fwupd_remote_build_metadata_sig_uri(remote, &error); if (uri == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } fwupd_client_download_bytes_async(self, uri, download_flags & ~FWUPD_CLIENT_DOWNLOAD_FLAG_ONLY_P2P, cancellable, fwupd_client_refresh_remote_signature_cb, g_steal_pointer(&task)); } /** * fwupd_client_refresh_remote_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.refresh_remote_async]. * * Returns: %TRUE for success * * Since: 1.5.0 **/ gboolean fwupd_client_refresh_remote_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } static void fwupd_client_get_remotes_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) array = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } array = fwupd_codec_array_from_variant(val, FWUPD_TYPE_REMOTE, &error); if (array == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_pointer(task, g_steal_pointer(&array), (GDestroyNotify)g_ptr_array_unref); } /** * fwupd_client_get_remotes_async: * @self: a #FwupdClient * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Gets the list of remotes that have been configured for the system. * * You must have called [method@Client.connect_async] on @self before using * this method. * * Since: 1.5.0 **/ void fwupd_client_get_remotes_async(FwupdClient *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "GetRemotes", NULL, G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_get_remotes_cb, g_steal_pointer(&task)); } /** * fwupd_client_get_remotes_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.get_remotes_async]. * * Returns: (element-type FwupdRemote) (transfer container): results * * Since: 1.5.0 **/ GPtrArray * fwupd_client_get_remotes_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } static void fwupd_client_get_approved_firmware_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_auto(GStrv) strv = NULL; g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) array = g_ptr_array_new_with_free_func(g_free); g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } g_variant_get(val, "(^as)", &strv); for (guint i = 0; strv[i] != NULL; i++) g_ptr_array_add(array, g_strdup(strv[i])); /* success */ g_task_return_pointer(task, g_steal_pointer(&array), (GDestroyNotify)g_ptr_array_unref); } /** * fwupd_client_get_approved_firmware_async: * @self: a #FwupdClient * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Gets the list of approved firmware. * * You must have called [method@Client.connect_async] on @self before using * this method. * * Since: 1.5.0 **/ void fwupd_client_get_approved_firmware_async(FwupdClient *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "GetApprovedFirmware", NULL, G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_get_approved_firmware_cb, g_steal_pointer(&task)); } /** * fwupd_client_get_approved_firmware_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.get_approved_firmware_async]. * * Returns: (element-type utf8) (transfer container): checksums, or %NULL for error * * Since: 1.5.0 **/ GPtrArray * fwupd_client_get_approved_firmware_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } static void fwupd_client_set_approved_firmware_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } /** * fwupd_client_set_approved_firmware_async: * @self: a #FwupdClient * @checksums: (element-type utf8): firmware checksums * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Sets the list of approved firmware. * * Since: 1.5.0 **/ void fwupd_client_set_approved_firmware_async(FwupdClient *self, GPtrArray *checksums, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_auto(GStrv) strv = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); strv = g_new0(gchar *, checksums->len + 1); for (guint i = 0; i < checksums->len; i++) { const gchar *tmp = g_ptr_array_index(checksums, i); strv[i] = g_strdup(tmp); } g_dbus_proxy_call(priv->proxy, "SetApprovedFirmware", g_variant_new("(^as)", strv), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_set_approved_firmware_cb, g_steal_pointer(&task)); } /** * fwupd_client_set_approved_firmware_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.set_approved_firmware_async]. * * Returns: %TRUE for success * * Since: 1.5.0 **/ gboolean fwupd_client_set_approved_firmware_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } static void fwupd_client_get_blocked_firmware_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_auto(GStrv) strv = NULL; g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) array = g_ptr_array_new_with_free_func(g_free); g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } g_variant_get(val, "(^as)", &strv); for (guint i = 0; strv[i] != NULL; i++) g_ptr_array_add(array, g_strdup(strv[i])); /* success */ g_task_return_pointer(task, g_steal_pointer(&array), (GDestroyNotify)g_ptr_array_unref); } /** * fwupd_client_get_blocked_firmware_async: * @self: a #FwupdClient * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Gets the list of blocked firmware. * * You must have called [method@Client.connect_async] on @self before using * this method. * * Since: 1.5.0 **/ void fwupd_client_get_blocked_firmware_async(FwupdClient *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "GetBlockedFirmware", NULL, G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_get_blocked_firmware_cb, g_steal_pointer(&task)); } /** * fwupd_client_get_blocked_firmware_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.get_blocked_firmware_async]. * * Returns: (element-type utf8) (transfer container): checksums, or %NULL for error * * Since: 1.5.0 **/ GPtrArray * fwupd_client_get_blocked_firmware_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } static void fwupd_client_set_blocked_firmware_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } /** * fwupd_client_set_blocked_firmware_async: * @self: a #FwupdClient * @checksums: (element-type utf8): firmware checksums * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Sets the list of blocked firmware. * * Since: 1.5.0 **/ void fwupd_client_set_blocked_firmware_async(FwupdClient *self, GPtrArray *checksums, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_auto(GStrv) strv = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); strv = g_new0(gchar *, checksums->len + 1); for (guint i = 0; i < checksums->len; i++) { const gchar *tmp = g_ptr_array_index(checksums, i); strv[i] = g_strdup(tmp); } g_dbus_proxy_call(priv->proxy, "SetBlockedFirmware", g_variant_new("(^as)", strv), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_set_blocked_firmware_cb, g_steal_pointer(&task)); } /** * fwupd_client_set_blocked_firmware_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.set_blocked_firmware_async]. * * Returns: %TRUE for success * * Since: 1.5.0 **/ gboolean fwupd_client_set_blocked_firmware_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } static void fwupd_client_set_feature_flags_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } /** * fwupd_client_set_feature_flags_async: * @self: a #FwupdClient * @feature_flags: feature flags, e.g. %FWUPD_FEATURE_FLAG_UPDATE_TEXT * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Sets the features the client supports. This allows firmware to depend on * specific front-end features, for instance showing the user an image on * how to detach the hardware. * * Since: 1.5.0 **/ void fwupd_client_set_feature_flags_async(FwupdClient *self, FwupdFeatureFlags feature_flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "SetFeatureFlags", g_variant_new("(t)", (guint64)feature_flags), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_set_feature_flags_cb, g_steal_pointer(&task)); } /** * fwupd_client_set_feature_flags_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.set_feature_flags_async]. * * Returns: %TRUE for success * * Since: 1.5.0 **/ gboolean fwupd_client_set_feature_flags_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } static void fwupd_client_self_sign_cb(GObject *source, GAsyncResult *res, gpointer user_data) { gchar *str = NULL; g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_variant_get(val, "(s)", &str); g_task_return_pointer(task, g_steal_pointer(&str), (GDestroyNotify)g_free); } /** * fwupd_client_self_sign_async: * @self: a #FwupdClient * @value: a string to sign, typically a JSON blob * @flags: signing flags, e.g. %FWUPD_SELF_SIGN_FLAG_ADD_TIMESTAMP * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Signs the data using the client self-signed certificate. * * You must have called [method@Client.connect_async] on @self before using * this method. * * Since: 1.5.0 **/ void fwupd_client_self_sign_async(FwupdClient *self, const gchar *value, FwupdSelfSignFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); GVariantBuilder builder; g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(value != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* set options */ g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); if (flags & FWUPD_SELF_SIGN_FLAG_ADD_TIMESTAMP) { g_variant_builder_add(&builder, "{sv}", "add-timestamp", g_variant_new_boolean(TRUE)); } if (flags & FWUPD_SELF_SIGN_FLAG_ADD_CERT) { g_variant_builder_add(&builder, "{sv}", "add-cert", g_variant_new_boolean(TRUE)); } /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "SelfSign", g_variant_new("(sa{sv})", value, &builder), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_self_sign_cb, g_steal_pointer(&task)); } /** * fwupd_client_self_sign_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.self_sign_async]. * * Returns: a signature, or %NULL for failure * * Since: 1.5.0 **/ gchar * fwupd_client_self_sign_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } static void fwupd_client_modify_remote_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } /** * fwupd_client_modify_remote_async: * @self: a #FwupdClient * @remote_id: the remote ID, e.g. `lvfs-testing` * @key: the key, e.g. `Enabled` * @value: the key, e.g. `true` * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Modifies a system remote in a specific way. * * Since: 1.5.0 **/ void fwupd_client_modify_remote_async(FwupdClient *self, const gchar *remote_id, const gchar *key, const gchar *value, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(remote_id != NULL); g_return_if_fail(key != NULL); g_return_if_fail(value != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "ModifyRemote", g_variant_new("(sss)", remote_id, key, value), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_modify_remote_cb, g_steal_pointer(&task)); } /** * fwupd_client_modify_remote_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.modify_remote_async]. * * Returns: %TRUE for success * * Since: 1.5.0 **/ gboolean fwupd_client_modify_remote_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } static void fwupd_client_modify_device_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } /** * fwupd_client_modify_device_async: * @self: a #FwupdClient * @device_id: (not nullable): the device ID * @key: (not nullable): the key, e.g. `Flags` * @value: (not nullable): the value, e.g. `reported` * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Modifies a device in a specific way. Not all properties on the #FwupdDevice * are settable by the client, and some may have other restrictions on @value. * * Since: 1.5.0 **/ void fwupd_client_modify_device_async(FwupdClient *self, const gchar *device_id, const gchar *key, const gchar *value, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(device_id != NULL); g_return_if_fail(key != NULL); g_return_if_fail(value != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "ModifyDevice", g_variant_new("(sss)", device_id, key, value), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_modify_device_cb, g_steal_pointer(&task)); } /** * fwupd_client_modify_device_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.modify_device_async]. * * Returns: %TRUE for success * * Since: 1.5.0 **/ gboolean fwupd_client_modify_device_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } static FwupdRemote * fwupd_client_get_remote_by_id_noref(GPtrArray *remotes, const gchar *remote_id) { for (guint i = 0; i < remotes->len; i++) { FwupdRemote *remote = g_ptr_array_index(remotes, i); if (g_strcmp0(remote_id, fwupd_remote_get_id(remote)) == 0) return remote; } return NULL; } static void fwupd_client_get_remote_by_id_cb(GObject *source, GAsyncResult *res, gpointer user_data) { FwupdRemote *remote_tmp; g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) remotes = NULL; const gchar *remote_id = g_task_get_task_data(task); remotes = fwupd_client_get_remotes_finish(FWUPD_CLIENT(source), res, &error); if (remotes == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } remote_tmp = fwupd_client_get_remote_by_id_noref(remotes, remote_id); if (remote_tmp == NULL) { g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no remote '%s' found in search paths", remote_id); return; } /* success */ g_task_return_pointer(task, g_object_ref(remote_tmp), (GDestroyNotify)g_object_unref); } /** * fwupd_client_get_remote_by_id_async: * @self: a #FwupdClient * @remote_id: (not nullable): the remote ID, e.g. `lvfs-testing` * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Gets a specific remote that has been configured for the system. * * Since: 1.5.0 **/ void fwupd_client_get_remote_by_id_async(FwupdClient *self, const gchar *remote_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(remote_id != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_task_set_task_data(task, g_strdup(remote_id), g_free); fwupd_client_get_remotes_async(self, cancellable, fwupd_client_get_remote_by_id_cb, g_steal_pointer(&task)); } /** * fwupd_client_get_remote_by_id_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.get_remote_by_id_async]. * * Returns: (transfer full): a #FwupdRemote, or %NULL if not found * * Since: 1.5.0 **/ FwupdRemote * fwupd_client_get_remote_by_id_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } /** * fwupd_client_set_user_agent: * @self: a #FwupdClient * @user_agent: the user agent ID, e.g. `gnome-software/3.34.1` * * Manually sets the user agent that is used for downloading. The user agent * should contain the runtime version of fwupd somewhere in the provided string. * * Since: 1.4.5 **/ void fwupd_client_set_user_agent(FwupdClient *self, const gchar *user_agent) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(user_agent != NULL); /* not changed */ if (g_strcmp0(priv->user_agent, user_agent) == 0) return; g_free(priv->user_agent); priv->user_agent = g_strdup(user_agent); } /** * fwupd_client_get_user_agent: * @self: a #FwupdClient * * Gets the string that represents the user agent that is used for * uploading and downloading. The user agent will contain the runtime * version of fwupd somewhere in the provided string. * * Returns: a string, or %NULL for unknown. * * Since: 1.5.2 **/ const gchar * fwupd_client_get_user_agent(FwupdClient *self) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); return priv->user_agent; } /** * fwupd_client_set_user_agent_for_package: * @self: a #FwupdClient * @package_name: (not nullable): client program name, e.g. `gnome-software` * @package_version: (not nullable): client program version, e.g. `3.28.1` * * Builds a user-agent to use for the download. * * Supplying harmless details to the server means it knows more about each * client. This allows the web service to respond in a different way, for * instance sending a different metadata file for old versions of fwupd, or * returning an error for Solaris machines. * * Before freaking out about theoretical privacy implications, much more data * than this is sent to each and every website you visit. * * Since: 1.4.5 **/ void fwupd_client_set_user_agent_for_package(FwupdClient *self, const gchar *package_name, const gchar *package_version) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(package_name != NULL); g_return_if_fail(package_version != NULL); g_free(priv->package_name); g_free(priv->package_version); priv->package_name = g_path_get_basename(package_name); priv->package_version = g_strdup(package_version); fwupd_client_rebuild_user_agent(self); } static size_t fwupd_client_download_write_callback_cb(char *ptr, size_t size, size_t nmemb, void *userdata) { GByteArray *buf = (GByteArray *)userdata; gsize realsize = size * nmemb; g_byte_array_append(buf, (const guint8 *)ptr, realsize); return realsize; } static GBytes * fwupd_client_download_ipfs(FwupdClient *self, const gchar *url, GCancellable *cancellable, GError **error) { GSubprocessFlags flags = G_SUBPROCESS_FLAGS_STDOUT_PIPE | G_SUBPROCESS_FLAGS_STDERR_PIPE; g_autofree gchar *path = NULL; g_autoptr(GBytes) bstdout = NULL; g_autoptr(GBytes) bstderr = NULL; g_autoptr(GSubprocess) subprocess = NULL; /* we get no detailed progress details */ fwupd_client_set_status(self, FWUPD_STATUS_DOWNLOADING); fwupd_client_set_percentage(self, 0); /* convert from URI to path */ if (g_str_has_prefix(url, "ipfs://")) { path = g_strdup_printf("/ipfs/%s", url + 7); } else if (g_str_has_prefix(url, "ipns://")) { path = g_strdup_printf("/ipns/%s", url + 7); } else { path = g_strdup(url); } /* run sync */ subprocess = g_subprocess_new(flags, error, "ipfs", "cat", path, NULL); if (subprocess == NULL) return NULL; if (!g_subprocess_communicate(subprocess, NULL, cancellable, &bstdout, &bstderr, error)) return NULL; fwupd_client_set_status(self, FWUPD_STATUS_IDLE); if (g_subprocess_get_exit_status(subprocess) != 0) { const gchar *msg = g_bytes_get_data(bstderr, NULL); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to download file: %s", msg); return NULL; } return g_steal_pointer(&bstdout); } static GBytes * fwupd_client_download_http(FwupdClient *self, CURL *curl, const gchar *url, GError **error) { CURLcode res; gchar errbuf[CURL_ERROR_SIZE] = {'\0'}; glong status_code = 0; g_autoptr(GByteArray) buf = g_byte_array_new(); /* relax the SSL checks on localhost URLs and broken corporate proxies */ if (fwupd_client_is_localhost(url) || g_getenv("DISABLE_SSL_STRICT") != NULL) { (void)curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); (void)curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); } else { (void)curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); (void)curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 1L); } fwupd_client_set_status(self, FWUPD_STATUS_DOWNLOADING); (void)curl_easy_setopt(curl, CURLOPT_URL, url); (void)curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf); (void)curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwupd_client_download_write_callback_cb); (void)curl_easy_setopt(curl, CURLOPT_WRITEDATA, buf); res = curl_easy_perform(curl); fwupd_client_set_status(self, FWUPD_STATUS_IDLE); fwupd_client_set_percentage(self, 100); if (res != CURLE_OK) { if (errbuf[0] != '\0') { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to download file: %s", errbuf); return NULL; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to download file: %s", curl_easy_strerror(res)); return NULL; } /* check for server limit */ curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status_code); g_info("status-code was %ld", status_code); if (status_code == 429) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "Failed to download due to server limit"); return NULL; } if (status_code == 502 || status_code == 503 || status_code == 504) { g_autofree gchar *str = g_strndup((const gchar *)buf->data, MIN(buf->len, 4000)); if (g_str_is_ascii(str)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "Transient failure to download, server response was %u: %s", (guint)status_code, str); return NULL; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "Transient failure to download, server response was %u", (guint)status_code); return NULL; } if (status_code >= 400) { g_autofree gchar *str = g_strndup((const gchar *)buf->data, MIN(buf->len, 4000)); if (g_str_is_ascii(str)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "Failed to download, server response was %u: %s", (guint)status_code, str); return NULL; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "Failed to download, server response was %u", (guint)status_code); return NULL; } return g_bytes_new(buf->data, buf->len); } static gboolean fwupd_client_download_error_is_fatal(const GError *error) { if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT)) return FALSE; return TRUE; } static GBytes * fwupd_client_download_http_retry(FwupdClient *self, CURL *curl, const gchar *url, GError **error) { FwupdClientPrivate *priv = GET_PRIVATE(self); GNetworkMonitor *monitor = g_network_monitor_get_default(); gulong delay_ms = 2500; g_autoptr(GError) error_monitor = NULL; g_autoptr(GSocketConnectable) address = NULL; g_autoptr(GUri) uri = NULL; /* test if we can reach this network */ uri = g_uri_parse(url, G_URI_FLAGS_NONE, error); if (uri == NULL) return NULL; address = g_network_address_parse(g_uri_get_host(uri), g_uri_get_port(uri), error); if (address == NULL) return NULL; if (!g_network_monitor_can_reach(monitor, address, NULL, &error_monitor)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_REACHABLE, "network is unreachable: %s", error_monitor->message); return NULL; } for (guint i = 0;; i++, delay_ms *= 2) { g_autoptr(GBytes) blob = NULL; g_autoptr(GError) error_local = NULL; blob = fwupd_client_download_http(self, curl, url, &error_local); if (blob != NULL) return g_steal_pointer(&blob); if (i >= priv->download_retries || fwupd_client_download_error_is_fatal(error_local)) { g_propagate_error(error, g_steal_pointer(&error_local)); break; } g_debug("ignoring and trying again: %s", error_local->message); g_usleep(delay_ms * 1000); } return NULL; } static void fwupd_client_download_bytes_thread_cb(GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { FwupdClient *self = FWUPD_CLIENT(source_object); FwupdCurlHelper *helper = g_task_get_task_data(task); g_autoptr(GBytes) blob = NULL; for (guint i = 0; i < helper->urls->len; i++) { const gchar *url = g_ptr_array_index(helper->urls, i); g_autoptr(GError) error = NULL; g_info("downloading %s", url); if (!fwupd_client_curl_helper_set_proxy(self, helper, url, &error)) { g_task_return_error(task, g_steal_pointer(&error)); return; } if (fwupd_client_is_url_http(url)) { blob = fwupd_client_download_http_retry(self, helper->curl, url, &error); if (blob != NULL) break; } else if (fwupd_client_is_url_ipfs(url)) { blob = fwupd_client_download_ipfs(self, url, cancellable, &error); if (blob != NULL) break; } else { g_set_error(&error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "not sure how to handle: %s", url); } if (i == helper->urls->len - 1) { g_task_return_error(task, g_steal_pointer(&error)); return; } fwupd_client_set_percentage(self, 0); fwupd_client_set_status(self, FWUPD_STATUS_IDLE); g_info("failed to download %s: %s, trying next URI…", url, error->message); } g_task_return_pointer(task, g_steal_pointer(&blob), (GDestroyNotify)g_bytes_unref); } /* private */ void fwupd_client_download_bytes2_async(FwupdClient *self, GPtrArray *urls, FwupdClientDownloadFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { g_autoptr(GTask) task = NULL; g_autoptr(GError) error = NULL; g_autoptr(FwupdCurlHelper) helper = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(urls != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); /* ensure networking set up */ task = g_task_new(self, cancellable, callback, callback_data); helper = fwupd_client_curl_new(self, &error); if (helper == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } helper->urls = fwupd_client_filter_locations(urls, flags, &error); if (helper->urls == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } g_task_set_task_data(task, g_steal_pointer(&helper), (GDestroyNotify)fwupd_client_curl_helper_free); /* download data */ g_task_run_in_thread(task, fwupd_client_download_bytes_thread_cb); } /** * fwupd_client_download_bytes_async: * @self: a #FwupdClient * @url: (not nullable): the remote URL * @flags: download flags, e.g. %FWUPD_CLIENT_DOWNLOAD_FLAG_NONE * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Downloads data from a remote server. The [method@Client.set_user_agent] function * should be called before this method is used. * * You must have called [method@Client.connect_async] on @self before using * this method. * * NOTE: This method is thread-safe, but progress signals will be * emitted in the global default main context, if not explicitly set with * [method@Client.set_main_context]. * * Since: 1.5.0 **/ void fwupd_client_download_bytes_async(FwupdClient *self, const gchar *url, FwupdClientDownloadFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { g_autoptr(GPtrArray) urls = g_ptr_array_new_with_free_func(g_free); g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(url != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); /* just proxy */ g_ptr_array_add(urls, g_strdup(url)); fwupd_client_download_bytes2_async(self, urls, flags, cancellable, callback, callback_data); } /** * fwupd_client_download_bytes_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.download_bytes_async]. * * Returns: (transfer full): downloaded data, or %NULL for error * * Since: 1.5.0 **/ GBytes * fwupd_client_download_bytes_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } static void fwupd_client_upload_bytes_thread_cb(GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { FwupdClient *self = FWUPD_CLIENT(source_object); FwupdCurlHelper *helper = g_task_get_task_data(task); CURLcode res; gchar errbuf[CURL_ERROR_SIZE] = {'\0'}; g_autoptr(GByteArray) buf = g_byte_array_new(); (void)curl_easy_setopt(helper->curl, CURLOPT_ERRORBUFFER, errbuf); (void)curl_easy_setopt(helper->curl, CURLOPT_WRITEFUNCTION, fwupd_client_download_write_callback_cb); (void)curl_easy_setopt(helper->curl, CURLOPT_WRITEDATA, buf); res = curl_easy_perform(helper->curl); fwupd_client_set_status(self, FWUPD_STATUS_IDLE); if (res != CURLE_OK) { glong status_code = 0; curl_easy_getinfo(helper->curl, CURLINFO_RESPONSE_CODE, &status_code); g_info("status-code was %ld", status_code); if (errbuf[0] != '\0') { g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to upload file: %s", errbuf); return; } g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to upload file: %s", curl_easy_strerror(res)); return; } g_task_return_pointer(task, g_bytes_new(buf->data, buf->len), (GDestroyNotify)g_bytes_unref); } /** * fwupd_client_upload_bytes_async: * @self: a #FwupdClient * @url: (not nullable): the remote URL * @payload: (not nullable): payload string * @signature: (nullable): signature string * @flags: download flags, e.g. %FWUPD_CLIENT_DOWNLOAD_FLAG_NONE * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Uploads data to a remote server. The [method@Client.set_user_agent] function * should be called before this method is used. * * You must have called [method@Client.connect_async] on @self before using * this method. * * NOTE: This method is thread-safe, but progress signals will be * emitted in the global default main context, if not explicitly set with * [method@Client.set_main_context]. * * Since: 1.5.0 **/ void fwupd_client_upload_bytes_async(FwupdClient *self, const gchar *url, const gchar *payload, const gchar *signature, FwupdClientUploadFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_autoptr(FwupdCurlHelper) helper = NULL; g_autoptr(GError) error = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(url != NULL); g_return_if_fail(payload != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* ensure networking set up */ task = g_task_new(self, cancellable, callback, callback_data); helper = fwupd_client_curl_new(self, &error); if (helper == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } /* build message */ if ((flags & FWUPD_CLIENT_UPLOAD_FLAG_ALWAYS_MULTIPART) > 0 || signature != NULL) { curl_mimepart *part; helper->mime = curl_mime_init(helper->curl); (void)curl_easy_setopt(helper->curl, CURLOPT_MIMEPOST, helper->mime); part = curl_mime_addpart(helper->mime); (void)curl_mime_data(part, payload, CURL_ZERO_TERMINATED); curl_mime_name(part, "payload"); if (signature != NULL) { part = curl_mime_addpart(helper->mime); (void)curl_mime_data(part, signature, CURL_ZERO_TERMINATED); curl_mime_name(part, "signature"); } } else { helper->headers = curl_slist_append(helper->headers, "Content-Type: text/plain"); (void)curl_easy_setopt(helper->curl, CURLOPT_HTTPHEADER, helper->headers); (void)curl_easy_setopt(helper->curl, CURLOPT_POST, 1L); (void)curl_easy_setopt(helper->curl, CURLOPT_POSTFIELDSIZE, strlen(payload)); (void)curl_easy_setopt(helper->curl, CURLOPT_COPYPOSTFIELDS, payload); } /* relax the SSL checks on localhost URLs and broken corporate proxies */ if (fwupd_client_is_localhost(url) || g_getenv("DISABLE_SSL_STRICT") != NULL) { (void)curl_easy_setopt(helper->curl, CURLOPT_SSL_VERIFYPEER, 0L); (void)curl_easy_setopt(helper->curl, CURLOPT_SSL_VERIFYHOST, 0L); } else { (void)curl_easy_setopt(helper->curl, CURLOPT_SSL_VERIFYPEER, 1L); (void)curl_easy_setopt(helper->curl, CURLOPT_SSL_VERIFYHOST, 1L); } fwupd_client_set_status(self, FWUPD_STATUS_IDLE); g_info("uploading to %s", url); (void)curl_easy_setopt(helper->curl, CURLOPT_URL, url); g_task_set_task_data(task, g_steal_pointer(&helper), (GDestroyNotify)fwupd_client_curl_helper_free); g_task_run_in_thread(task, fwupd_client_upload_bytes_thread_cb); } /** * fwupd_client_upload_bytes_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.upload_bytes_async]. * * Returns: (transfer full): response data, or %NULL for error * * Since: 1.5.0 **/ GBytes * fwupd_client_upload_bytes_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } static void fwupd_client_upload_report_cb(GObject *source, GAsyncResult *res, gpointer user_data) { const gchar *server_msg = NULL; JsonNode *json_root; JsonObject *json_object; g_autofree gchar *str = NULL; g_autofree gchar *uri = NULL; g_autoptr(GBytes) bytes = NULL; g_autoptr(GError) error = NULL; g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(JsonParser) json_parser = NULL; /* parse */ bytes = fwupd_client_upload_bytes_finish(FWUPD_CLIENT(source), res, &error); if (bytes == NULL) { g_prefix_error(&error, "failed to upload report: "); g_task_return_error(task, g_steal_pointer(&error)); return; } /* server returned nothing, and probably exploded in a ball of flames */ if (g_bytes_get_size(bytes) == 0) { g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to upload, zero length data"); return; } /* parse JSON reply */ json_parser = json_parser_new(); str = g_strndup(g_bytes_get_data(bytes, NULL), g_bytes_get_size(bytes)); if (!json_parser_load_from_data(json_parser, str, -1, &error)) { g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to parse JSON response from '%s': %s", str, error->message); return; } json_root = json_parser_get_root(json_parser); if (json_root == NULL) { g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "JSON response was malformed: '%s'", str); return; } json_object = json_node_get_object(json_root); if (json_object == NULL) { g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "JSON response object was malformed: '%s'", str); return; } /* get any optional server message */ if (json_object_has_member(json_object, "msg")) server_msg = json_object_get_string_member(json_object, "msg"); /* server reported failed */ if (!json_object_get_boolean_member(json_object, "success")) { g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_PERMISSION_DENIED, "server rejected report: %s", server_msg != NULL ? server_msg : "unspecified"); return; } /* server wanted us to see the message */ if (server_msg != NULL) { g_info("server message: %s", server_msg); if (json_object_has_member(json_object, "uri")) uri = g_strdup(json_object_get_string_member(json_object, "uri")); } /* fallback */ if (uri == NULL) uri = g_strdup(""); /* success */ g_task_return_pointer(task, g_steal_pointer(&uri), g_free); } /** * fwupd_client_upload_report_async: * @self: a #FwupdClient * @url: (not nullable): the remote URL * @payload: (not nullable): payload string * @signature: (nullable): signature string * @flags: download flags, e.g. %FWUPD_CLIENT_DOWNLOAD_FLAG_NONE * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Uploads a report to a remote server. The [method@Client.set_user_agent] function * should be called before this method is used. * * You must have called [method@Client.connect_async] on @self before using * this method. * * NOTE: This method is thread-safe, but progress signals will be * emitted in the global default main context, if not explicitly set with * [method@Client.set_main_context]. * * Since: 1.9.20 **/ void fwupd_client_upload_report_async(FwupdClient *self, const gchar *url, const gchar *payload, const gchar *signature, FwupdClientUploadFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(url != NULL); g_return_if_fail(payload != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); task = g_task_new(self, cancellable, callback, callback_data); fwupd_client_upload_bytes_async(self, url, payload, signature, flags, cancellable, fwupd_client_upload_report_cb, g_steal_pointer(&task)); } /** * fwupd_client_upload_report_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.upload_report_async]. * * Returns: (transfer full): a URI (perhaps an empty string), or %NULL for error * * Since: 1.9.20 **/ gchar * fwupd_client_upload_report_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } static void fwupd_client_inhibit_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autofree gchar *inhibit_id = NULL; g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_variant_get(val, "(s)", &inhibit_id); g_task_return_pointer(task, g_steal_pointer(&inhibit_id), g_free); } /** * fwupd_client_inhibit_async: * @self: a #FwupdClient * @reason: (not nullable): the inhibit reason, e.g. `user active` * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Marks all devices as unavailable for update. Update is only available if there is no other * inhibit imposed by other applications or by the system (e.g. low power state). * * The same application can inhibit the system multiple times. * * Since: 1.8.11 **/ void fwupd_client_inhibit_async(FwupdClient *self, const gchar *reason, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(reason != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "Inhibit", g_variant_new("(s)", reason), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_inhibit_cb, g_steal_pointer(&task)); } /** * fwupd_client_inhibit_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.inhibit_async]. * * Returns: (transfer full): a string to use for [method@FwupdClient.uninhibit_async], * or %NULL for failure * * Since: 1.8.11 **/ gchar * fwupd_client_inhibit_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } static void fwupd_client_uninhibit_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } /** * fwupd_client_uninhibit_async: * @self: a #FwupdClient * @inhibit_id: (not nullable): the inhibit ID * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Removes the inhibit token added by the application. * * Since: 1.8.11 **/ void fwupd_client_uninhibit_async(FwupdClient *self, const gchar *inhibit_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(inhibit_id != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "Uninhibit", g_variant_new("(s)", inhibit_id), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_uninhibit_cb, g_steal_pointer(&task)); } /** * fwupd_client_uninhibit_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.uninhibit_async]. * * Returns: %TRUE for success * * Since: 1.8.11 **/ gboolean fwupd_client_uninhibit_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } /** * fwupd_client_add_hint: * @self: a #FwupdClient * @key: (not nullable): the key, e.g. `locale` * @value: (nullable): the value @key should be set * * Sets optional hints from the client that may affect the list of devices. * * Since: 1.7.1 **/ void fwupd_client_add_hint(FwupdClient *self, const gchar *key, const gchar *value) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(key != NULL); g_hash_table_insert(priv->hints, g_strdup(key), g_strdup(value)); } #ifdef HAVE_GIO_UNIX static void fwupd_client_emulation_load_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GDBusMessage) msg = NULL; g_autoptr(GError) error = NULL; msg = g_dbus_connection_send_message_with_reply_finish(G_DBUS_CONNECTION(source), res, &error); if (msg == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } if (g_dbus_message_to_gerror(msg, &error)) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } #endif /** * fwupd_client_emulation_load_async: * @self: a #FwupdClient * @filename: archive data of JSON files * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Loads an emulated device into the daemon backend that has the phases set by the JSON data, * for instance, having one USB device emulated for the bootloader and another emulated for the * runtime interface. * * Since: 2.0.0 **/ void fwupd_client_emulation_load_async(FwupdClient *self, const gchar *filename, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { #ifdef HAVE_GIO_UNIX FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GDBusMessage) request = NULL; g_autoptr(GError) error = NULL; g_autoptr(GTask) task = g_task_new(self, cancellable, callback, callback_data); g_autoptr(GUnixFDList) fd_list = NULL; g_autoptr(GUnixInputStream) istr = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(filename != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); istr = fwupd_unix_input_stream_from_fn(filename, &error); if (istr == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } /* set out of band file descriptor */ fd_list = g_unix_fd_list_new(); g_unix_fd_list_append(fd_list, g_unix_input_stream_get_fd(istr), NULL); request = g_dbus_message_new_method_call(FWUPD_DBUS_SERVICE, FWUPD_DBUS_PATH, FWUPD_DBUS_INTERFACE, "EmulationLoad"); g_dbus_message_set_unix_fd_list(request, fd_list); /* call into daemon */ g_dbus_message_set_body(request, g_variant_new("(h)", g_unix_input_stream_get_fd(istr))); g_dbus_connection_send_message_with_reply(g_dbus_proxy_get_connection(priv->proxy), request, G_DBUS_SEND_MESSAGE_FLAGS_NONE, G_MAXINT, NULL, cancellable, fwupd_client_emulation_load_cb, g_steal_pointer(&task)); #else g_autoptr(GTask) task = g_task_new(self, cancellable, callback, callback_data); g_task_return_new_error_literal(task, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported as not found"); #endif } /** * fwupd_client_emulation_load_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.emulation_load_async]. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fwupd_client_emulation_load_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } #ifdef HAVE_GIO_UNIX static void fwupd_client_emulation_save_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GDBusMessage) msg = NULL; g_autoptr(GError) error = NULL; msg = g_dbus_connection_send_message_with_reply_finish(G_DBUS_CONNECTION(source), res, &error); if (msg == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } if (g_dbus_message_to_gerror(msg, &error)) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } #endif /** * fwupd_client_emulation_save_async: * @self: a #FwupdClient * @filename: archive data of JSON files * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Gets the captured data from all filtered devices for all recorded phases. The data is returned * in a ZIP archive of JSON output. * * NOTE: Device events are not automatically recorded for all devices. You must call something * like `ModifyDevice(device_id, 'flags','emulation-tag')` to start the recording the backend. * * Once the device has been re-inserted then the emulation data will be available using * this API call. * * You must have called [method@Client.connect_async] on @self before using * this method. * * Since: 2.0.0 **/ void fwupd_client_emulation_save_async(FwupdClient *self, const gchar *filename, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { #ifdef HAVE_GIO_UNIX FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GDBusMessage) request = NULL; g_autoptr(GError) error = NULL; g_autoptr(GTask) task = g_task_new(self, cancellable, callback, callback_data); g_autoptr(GUnixFDList) fd_list = NULL; g_autoptr(GUnixOutputStream) istr = NULL; g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(filename != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); istr = fwupd_unix_output_stream_from_fn(filename, &error); if (istr == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } /* set out of band file descriptor */ fd_list = g_unix_fd_list_new(); g_unix_fd_list_append(fd_list, g_unix_output_stream_get_fd(istr), NULL); request = g_dbus_message_new_method_call(FWUPD_DBUS_SERVICE, FWUPD_DBUS_PATH, FWUPD_DBUS_INTERFACE, "EmulationSave"); g_dbus_message_set_unix_fd_list(request, fd_list); /* call into daemon */ g_dbus_message_set_body(request, g_variant_new("(h)", g_unix_output_stream_get_fd(istr))); g_dbus_connection_send_message_with_reply(g_dbus_proxy_get_connection(priv->proxy), request, G_DBUS_SEND_MESSAGE_FLAGS_NONE, G_MAXINT, NULL, cancellable, fwupd_client_emulation_save_cb, g_steal_pointer(&task)); #else g_autoptr(GTask) task = g_task_new(self, cancellable, callback, callback_data); g_task_return_new_error_literal(task, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported as not found"); #endif } /** * fwupd_client_emulation_save_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.emulation_save_async]. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fwupd_client_emulation_save_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } static void fwupd_client_fix_host_security_attr_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } /** * fwupd_client_fix_host_security_attr_async: * @self: a #FwupdClient * @appstream_id: HSI AppStream ID * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Fix one specific security attribute. * * Since: 1.9.6 **/ void fwupd_client_fix_host_security_attr_async(FwupdClient *self, const gchar *appstream_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(appstream_id != NULL); g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "FixHostSecurityAttr", g_variant_new("(s)", appstream_id), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_fix_host_security_attr_cb, g_steal_pointer(&task)); } /** * fwupd_client_fix_host_security_attr_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.fix_host_security_attr_async]. * * Returns: %TRUE for success * * Since: 1.9.6 **/ gboolean fwupd_client_fix_host_security_attr_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } static void fwupd_client_undo_host_security_attr_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { fwupd_client_fixup_dbus_error(error); g_task_return_error(task, g_steal_pointer(&error)); return; } /* success */ g_task_return_boolean(task, TRUE); } /** * fwupd_client_undo_host_security_attr_async: * @self: a #FwupdClient * @appstream_id: HSI AppStream ID * @cancellable: (nullable): optional #GCancellable * @callback: (scope async) (closure callback_data): the function to run on completion * @callback_data: the data to pass to @callback * * Reverts the fix to one specific security attribute. * * Since: 1.9.6 **/ void fwupd_client_undo_host_security_attr_async(FwupdClient *self, const gchar *appstream_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GTask) task = NULL; g_return_if_fail(appstream_id != NULL); g_return_if_fail(FWUPD_IS_CLIENT(self)); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(priv->proxy != NULL); /* call into daemon */ task = g_task_new(self, cancellable, callback, callback_data); g_dbus_proxy_call(priv->proxy, "UndoHostSecurityAttr", g_variant_new("(s)", appstream_id), G_DBUS_CALL_FLAGS_NONE, FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, cancellable, fwupd_client_undo_host_security_attr_cb, g_steal_pointer(&task)); } /** * fwupd_client_undo_host_security_attr_finish: * @self: a #FwupdClient * @res: (not nullable): the asynchronous result * @error: (nullable): optional return location for an error * * Gets the result of [method@FwupdClient.undo_host_security_attr_async]. * * Returns: %TRUE for success * * Since: 1.9.6 **/ gboolean fwupd_client_undo_host_security_attr_finish(FwupdClient *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } static void fwupd_client_build_report_metadata(JsonBuilder *builder, GHashTable *metadata) { GHashTableIter iter; const gchar *key; const gchar *value; g_hash_table_iter_init(&iter, metadata); while (g_hash_table_iter_next(&iter, (gpointer *)&key, (gpointer *)&value)) { json_builder_set_member_name(builder, key); json_builder_add_string_value(builder, value); } } /** * fwupd_client_build_report_devices: * @self: a #FwupdClient * @devices: (element-type FwupdDevice): devices * @metadata: (element-type utf8 utf8): attributes * @error: (nullable): optional return location for an error * * Builds a JSON report for the list of devices. * * This function should be called *before* asking the interactive user if they want to upload a * report -- as this function filters devices and may return an error if there is nothing to do. * * You must have called [method@Client.connect_async] on @self before using * this method. * * Returns: a string, or %NULL if the ID is not present * * Since: 1.9.20 **/ gchar * fwupd_client_build_report_devices(FwupdClient *self, GPtrArray *devices, GHashTable *metadata, GError **error) { FwupdClientPrivate *priv = GET_PRIVATE(self); guint cnt = 0; g_autofree gchar *data = NULL; g_autoptr(JsonBuilder) builder = json_builder_new(); g_autoptr(JsonGenerator) json_generator = NULL; g_autoptr(JsonNode) json_root = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(devices != NULL, NULL); g_return_val_if_fail(metadata != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); json_builder_begin_object(builder); json_builder_set_member_name(builder, "ReportType"); json_builder_add_string_value(builder, "device-list"); json_builder_set_member_name(builder, "ReportVersion"); json_builder_add_int_value(builder, 2); if (priv->host_machine_id != NULL) { json_builder_set_member_name(builder, "MachineId"); json_builder_add_string_value(builder, priv->host_machine_id); } /* this is system metadata not stored in the database */ if (g_hash_table_size(metadata) > 0) { json_builder_set_member_name(builder, "Metadata"); json_builder_begin_object(builder); fwupd_client_build_report_metadata(builder, metadata); json_builder_end_object(builder); } /* devices */ json_builder_set_member_name(builder, "Devices"); json_builder_begin_array(builder); for (guint i = 0; i < devices->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices, i); if (!fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE) && !fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN)) { g_debug("ignoring %s as not updatable", fwupd_device_get_id(dev)); continue; } json_builder_begin_object(builder); fwupd_codec_to_json(FWUPD_CODEC(dev), builder, FWUPD_CODEC_FLAG_TRUSTED); json_builder_end_object(builder); cnt++; } json_builder_end_array(builder); json_builder_end_object(builder); /* nothing to do */ if (cnt == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "no devices to upload"); return NULL; } /* export as a string */ json_root = json_builder_get_root(builder); json_generator = json_generator_new(); json_generator_set_pretty(json_generator, TRUE); json_generator_set_root(json_generator, json_root); data = json_generator_to_data(json_generator, NULL); if (data == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to convert to JSON string"); return NULL; } return g_steal_pointer(&data); } static void fwupd_client_build_report_history_device(JsonBuilder *builder, FwupdDevice *dev) { FwupdRelease *rel = fwupd_device_get_release_default(dev); GChecksumType checksum_types[] = {G_CHECKSUM_SHA256, G_CHECKSUM_SHA1, 0}; GHashTable *metadata = fwupd_release_get_metadata(rel); GPtrArray *checksums; GPtrArray *guids; /* identify the firmware used */ checksums = fwupd_release_get_checksums(rel); for (guint i = 0; checksum_types[i] != 0; i++) { const gchar *checksum = fwupd_checksum_get_by_kind(checksums, checksum_types[i]); if (checksum != NULL) { json_builder_set_member_name(builder, "Checksum"); json_builder_add_string_value(builder, checksum); break; } } /* identify the firmware written */ checksums = fwupd_device_get_checksums(dev); if (checksums->len > 0) { json_builder_set_member_name(builder, "ChecksumDevice"); json_builder_begin_array(builder); for (guint i = 0; i < checksums->len; i++) { const gchar *checksum = g_ptr_array_index(checksums, i); json_builder_add_string_value(builder, checksum); } json_builder_end_array(builder); } /* allow matching the specific component */ json_builder_set_member_name(builder, "ReleaseId"); json_builder_add_string_value(builder, fwupd_release_get_id(rel)); /* include the protocol used */ if (fwupd_release_get_protocol(rel) != NULL) { json_builder_set_member_name(builder, "Protocol"); json_builder_add_string_value(builder, fwupd_release_get_protocol(rel)); } /* set the error state of the report */ json_builder_set_member_name(builder, "UpdateState"); json_builder_add_int_value(builder, fwupd_device_get_update_state(dev)); if (fwupd_device_get_update_error(dev) != NULL) { json_builder_set_member_name(builder, "UpdateError"); json_builder_add_string_value(builder, fwupd_device_get_update_error(dev)); } if (fwupd_release_get_update_message(rel) != NULL) { json_builder_set_member_name(builder, "UpdateMessage"); json_builder_add_string_value(builder, fwupd_release_get_update_message(rel)); } /* find out if the predicted duration was accurate */ if (fwupd_device_get_install_duration(dev) != 0) { json_builder_set_member_name(builder, "InstallDuration"); json_builder_add_int_value(builder, fwupd_device_get_install_duration(dev)); } /* map back to the dev type on the LVFS */ guids = fwupd_device_get_guids(dev); if (guids->len > 0) { json_builder_set_member_name(builder, "Guid"); json_builder_begin_array(builder); for (guint i = 0; i < guids->len; i++) { const gchar *guid = g_ptr_array_index(guids, i); json_builder_add_string_value(builder, guid); } json_builder_end_array(builder); } json_builder_set_member_name(builder, "Plugin"); json_builder_add_string_value(builder, fwupd_device_get_plugin(dev)); /* report what we're trying to update *from* and *to* */ json_builder_set_member_name(builder, "VersionOld"); json_builder_add_string_value(builder, fwupd_device_get_version(dev)); json_builder_set_member_name(builder, "VersionNew"); json_builder_add_string_value(builder, fwupd_release_get_version(rel)); /* to know the state of the dev we're trying to update */ json_builder_set_member_name(builder, "Flags"); json_builder_add_int_value(builder, fwupd_device_get_flags(dev)); /* to know when the update tried to happen, and how soon after boot */ json_builder_set_member_name(builder, "Created"); json_builder_add_int_value(builder, fwupd_device_get_created(dev)); json_builder_set_member_name(builder, "Modified"); json_builder_add_int_value(builder, fwupd_device_get_modified(dev)); /* add saved metadata to the report */ if (g_hash_table_size(metadata) > 0) { json_builder_set_member_name(builder, "Metadata"); json_builder_begin_object(builder); fwupd_client_build_report_metadata(builder, metadata); json_builder_end_object(builder); } } /** * fwupd_client_build_report_history: * @self: a #FwupdClient * @devices: (element-type FwupdDevice): devices * @remote: (nullable): optional #FwupdRemote * @metadata: (element-type utf8 utf8): attributes * @error: (nullable): optional return location for an error * * Builds a JSON report for the list of devices. * * This function should be called *before* asking the interactive user if they want to upload a * report -- as this function filters devices and may return an error if there is nothing to do. * * You must have called [method@Client.connect_async] on @self before using * this method. * * Returns: a string, or %NULL on error * * Since: 2.0.0 **/ gchar * fwupd_client_build_report_history(FwupdClient *self, GPtrArray *devices, FwupdRemote *remote, GHashTable *metadata, GError **error) { FwupdClientPrivate *priv = GET_PRIVATE(self); guint cnt = 0; g_autofree gchar *data = NULL; g_autoptr(JsonBuilder) builder = json_builder_new(); g_autoptr(JsonGenerator) json_generator = json_generator_new(); g_autoptr(JsonNode) json_root = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(devices != NULL, NULL); g_return_val_if_fail(FWUPD_IS_REMOTE(remote) || remote == NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* sanity check */ if (remote != NULL && fwupd_remote_get_report_uri(remote) == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "%s has no ReportURI", fwupd_remote_get_id(remote)); return NULL; } /* create header */ json_builder_begin_object(builder); json_builder_set_member_name(builder, "ReportType"); json_builder_add_string_value(builder, "history"); json_builder_set_member_name(builder, "ReportVersion"); json_builder_add_int_value(builder, 2); if (priv->host_machine_id != NULL) { json_builder_set_member_name(builder, "MachineId"); json_builder_add_string_value(builder, priv->host_machine_id); } /* this is system metadata not stored in the database */ if (g_hash_table_size(metadata) > 0) { json_builder_set_member_name(builder, "Metadata"); json_builder_begin_object(builder); fwupd_client_build_report_metadata(builder, metadata); json_builder_end_object(builder); } /* add each device */ json_builder_set_member_name(builder, "Reports"); json_builder_begin_array(builder); for (guint i = 0; i < devices->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices, i); if (fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_REPORTED)) { g_debug("ignoring %s as already reported", fwupd_device_get_id(dev)); continue; } if (fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION)) { g_debug("ignoring %s as needs-activation", fwupd_device_get_id(dev)); continue; } if (fwupd_device_get_update_state(dev) != FWUPD_UPDATE_STATE_FAILED && fwupd_device_get_update_state(dev) != FWUPD_UPDATE_STATE_SUCCESS) { g_debug("ignoring %s with UpdateState %s", fwupd_device_get_id(dev), fwupd_update_state_to_string(fwupd_device_get_update_state(dev))); continue; } /* is this the correct remote */ if (remote != NULL) { FwupdRelease *rel = fwupd_device_get_release_default(dev); if (fwupd_release_get_remote_id(rel) == NULL) { g_debug("%s has no RemoteID", fwupd_device_get_id(dev)); continue; } if (g_strcmp0(fwupd_release_get_remote_id(rel), fwupd_remote_get_id(remote)) != 0) { g_debug("%s has incorrect RemoteID: %s != %s", fwupd_device_get_id(dev), fwupd_release_get_remote_id(rel), fwupd_remote_get_id(remote)); continue; } } json_builder_begin_object(builder); fwupd_client_build_report_history_device(builder, dev); json_builder_end_object(builder); cnt++; } json_builder_end_array(builder); json_builder_end_object(builder); /* nothing to do */ if (cnt == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "no history to upload"); return NULL; } /* export as a string */ json_root = json_builder_get_root(builder); json_generator_set_pretty(json_generator, TRUE); json_generator_set_root(json_generator, json_root); data = json_generator_to_data(json_generator, NULL); if (data == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to convert to JSON string"); return NULL; } return g_steal_pointer(&data); } /** * fwupd_client_build_report_security: * @self: a #FwupdClient * @attrs: (element-type FwupdSecurityAttr): attributes * @metadata: (element-type utf8 utf8): attributes * @error: (nullable): optional return location for an error * * Builds a JSON security report. * * You must have called [method@Client.connect_async] on @self before using * this method. * * Returns: a string, or %NULL on error * * Since: 2.0.0 **/ gchar * fwupd_client_build_report_security(FwupdClient *self, GPtrArray *attrs, GHashTable *metadata, GError **error) { FwupdClientPrivate *priv = GET_PRIVATE(self); guint cnt = 0; g_autofree gchar *data = NULL; g_autoptr(JsonBuilder) builder = json_builder_new(); g_autoptr(JsonGenerator) json_generator = json_generator_new(); g_autoptr(JsonNode) json_root = NULL; g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); g_return_val_if_fail(attrs != NULL, NULL); g_return_val_if_fail(metadata != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* create header */ json_builder_begin_object(builder); json_builder_set_member_name(builder, "ReportType"); json_builder_add_string_value(builder, "hsi"); json_builder_set_member_name(builder, "ReportVersion"); json_builder_add_int_value(builder, 2); if (priv->host_machine_id != NULL) { json_builder_set_member_name(builder, "MachineId"); json_builder_add_string_value(builder, priv->host_machine_id); } /* this is system metadata not stored in the database */ if (g_hash_table_size(metadata) > 0 || fwupd_client_get_host_security_id(self) != NULL) { json_builder_set_member_name(builder, "Metadata"); json_builder_begin_object(builder); fwupd_client_build_report_metadata(builder, metadata); json_builder_set_member_name(builder, "HostSecurityId"); json_builder_add_string_value(builder, fwupd_client_get_host_security_id(self)); json_builder_end_object(builder); } /* attrs */ json_builder_set_member_name(builder, "SecurityAttributes"); json_builder_begin_array(builder); for (guint i = 0; i < attrs->len; i++) { FwupdSecurityAttr *attr = g_ptr_array_index(attrs, i); json_builder_begin_object(builder); fwupd_codec_to_json(FWUPD_CODEC(attr), builder, FWUPD_CODEC_FLAG_TRUSTED); json_builder_end_object(builder); cnt++; } json_builder_end_array(builder); json_builder_end_object(builder); /* nothing to do */ if (cnt == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "no attributes to upload"); return NULL; } /* export as a string */ json_root = json_builder_get_root(builder); json_generator_set_pretty(json_generator, TRUE); json_generator_set_root(json_generator, json_root); data = json_generator_to_data(json_generator, NULL); if (data == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to convert to JSON string"); return NULL; } return g_steal_pointer(&data); } static void fwupd_client_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FwupdClient *self = FWUPD_CLIENT(object); FwupdClientPrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_STATUS: g_value_set_uint(value, priv->status); break; case PROP_TAINTED: g_value_set_boolean(value, priv->tainted); break; case PROP_PERCENTAGE: g_value_set_uint(value, priv->percentage); break; case PROP_DAEMON_VERSION: g_value_set_string(value, priv->daemon_version); break; case PROP_HOST_BKC: g_value_set_string(value, priv->host_bkc); break; case PROP_HOST_VENDOR: g_value_set_string(value, priv->host_vendor); break; case PROP_HOST_PRODUCT: g_value_set_string(value, priv->host_product); break; case PROP_HOST_MACHINE_ID: g_value_set_string(value, priv->host_machine_id); break; case PROP_HOST_SECURITY_ID: g_value_set_string(value, priv->host_security_id); break; case PROP_ONLY_TRUSTED: g_value_set_boolean(value, priv->only_trusted); break; case PROP_INTERACTIVE: g_value_set_boolean(value, priv->interactive); break; case PROP_BATTERY_LEVEL: g_value_set_uint(value, priv->battery_level); break; case PROP_BATTERY_THRESHOLD: g_value_set_uint(value, priv->battery_threshold); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fwupd_client_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FwupdClient *self = FWUPD_CLIENT(object); FwupdClientPrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_STATUS: priv->status = g_value_get_uint(value); break; case PROP_PERCENTAGE: priv->percentage = g_value_get_uint(value); break; case PROP_BATTERY_LEVEL: fwupd_client_set_battery_level(self, g_value_get_uint(value)); break; case PROP_BATTERY_THRESHOLD: fwupd_client_set_battery_threshold(self, g_value_get_uint(value)); break; case PROP_HOST_BKC: fwupd_client_set_host_bkc(self, g_value_get_string(value)); break; case PROP_HOST_VENDOR: fwupd_client_set_host_vendor(self, g_value_get_string(value)); break; case PROP_HOST_PRODUCT: fwupd_client_set_host_product(self, g_value_get_string(value)); break; case PROP_HOST_MACHINE_ID: fwupd_client_set_host_machine_id(self, g_value_get_string(value)); break; case PROP_HOST_SECURITY_ID: fwupd_client_set_host_security_id(self, g_value_get_string(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fwupd_client_class_init(FwupdClientClass *klass) { GParamSpec *pspec; GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fwupd_client_finalize; object_class->get_property = fwupd_client_get_property; object_class->set_property = fwupd_client_set_property; /** * FwupdClient::changed: * @self: the #FwupdClient instance that emitted the signal * * The ::changed signal is emitted when the daemon internal has * changed, for instance when a device has been added or removed. * * Since: 0.7.0 **/ signals[SIGNAL_CHANGED] = g_signal_new("changed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(FwupdClientClass, changed), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); /** * FwupdClient::state-changed: * @self: the #FwupdClient instance that emitted the signal * @status: the #FwupdStatus * * The ::state-changed signal is emitted when the daemon status has * changed, e.g. going from %FWUPD_STATUS_IDLE to %FWUPD_STATUS_DEVICE_WRITE. * * Since: 0.7.0 **/ signals[SIGNAL_STATUS_CHANGED] = g_signal_new("status-changed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(FwupdClientClass, status_changed), NULL, NULL, g_cclosure_marshal_VOID__UINT, G_TYPE_NONE, 1, G_TYPE_UINT); /** * FwupdClient::device-added: * @self: the #FwupdClient instance that emitted the signal * @result: the #FwupdDevice * * The ::device-added signal is emitted when a device has been * added. * * Since: 0.7.1 **/ signals[SIGNAL_DEVICE_ADDED] = g_signal_new("device-added", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(FwupdClientClass, device_added), NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 1, FWUPD_TYPE_DEVICE); /** * FwupdClient::device-removed: * @self: the #FwupdClient instance that emitted the signal * @result: the #FwupdDevice * * The ::device-removed signal is emitted when a device has been * removed. * * Since: 0.7.1 **/ signals[SIGNAL_DEVICE_REMOVED] = g_signal_new("device-removed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(FwupdClientClass, device_removed), NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 1, FWUPD_TYPE_DEVICE); /** * FwupdClient::device-changed: * @self: the #FwupdClient instance that emitted the signal * @result: the #FwupdDevice * * The ::device-changed signal is emitted when a device has been * changed in some way, e.g. the version number is updated. * * Since: 0.7.1 **/ signals[SIGNAL_DEVICE_CHANGED] = g_signal_new("device-changed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(FwupdClientClass, device_changed), NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 1, FWUPD_TYPE_DEVICE); /** * FwupdClient::device-request: * @self: the #FwupdClient instance that emitted the signal * @msg: the #FwupdRequest * * The ::device-request signal is emitted when a device has been * emitted some kind of event, e.g. a manual action is required. * * Since: 1.6.2 **/ signals[SIGNAL_DEVICE_REQUEST] = g_signal_new("device-request", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(FwupdClientClass, device_request), NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 1, FWUPD_TYPE_REQUEST); /** * FwupdClient:status: * * The last-reported status of the daemon. * * Since: 0.7.0 */ pspec = g_param_spec_uint("status", NULL, NULL, 0, FWUPD_STATUS_LAST, FWUPD_STATUS_UNKNOWN, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_STATUS, pspec); /** * FwupdClient:tainted: * * If the daemon is tainted by 3rd party code. * * Since: 1.2.4 */ pspec = g_param_spec_boolean("tainted", NULL, NULL, FALSE, G_PARAM_READABLE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_TAINTED, pspec); /** * FwupdClient:interactive: * * If the daemon is running in an interactive terminal * * Since: 1.3.4 */ pspec = g_param_spec_boolean("interactive", NULL, NULL, FALSE, G_PARAM_READABLE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_INTERACTIVE, pspec); /** * FwupdClient:percentage: * * The last-reported percentage of the daemon. * * Since: 0.7.3 */ pspec = g_param_spec_uint("percentage", NULL, NULL, 0, 100, 0, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_PERCENTAGE, pspec); /** * FwupdClient:daemon-version: * * The daemon version number. * * Since: 0.9.6 */ pspec = g_param_spec_string("daemon-version", NULL, NULL, NULL, G_PARAM_READABLE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_DAEMON_VERSION, pspec); /** * FwupdClient:host-bkc: * * The host best known configuration. * * Since: 1.7.3 */ pspec = g_param_spec_string("host-bkc", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_HOST_BKC, pspec); /** * FwupdClient:host-vendor: * * The host vendor string * * Since: 1.8.2 */ pspec = g_param_spec_string("host-vendor", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_HOST_VENDOR, pspec); /** * FwupdClient:host-product: * * The host product string * * Since: 1.3.1 */ pspec = g_param_spec_string("host-product", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_HOST_PRODUCT, pspec); /** * FwupdClient:host-machine-id: * * The host machine-id string * * Since: 1.3.2 */ pspec = g_param_spec_string("host-machine-id", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_HOST_MACHINE_ID, pspec); /** * FwupdClient:host-security-id: * * The host machine-id string * * Since: 1.5.0 */ pspec = g_param_spec_string("host-security-id", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_HOST_SECURITY_ID, pspec); /** * FwupdClient:only-trusted: * * If the daemon is verifying signatures from a trusted authority. * * Since: 1.8.0 */ pspec = g_param_spec_boolean("only-trusted", NULL, NULL, TRUE, G_PARAM_READABLE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_ONLY_TRUSTED, pspec); /** * FwupdClient:battery-level: * * The system battery level in percent. * * Since: 1.8.1 */ pspec = g_param_spec_uint("battery-level", NULL, NULL, 0, FWUPD_BATTERY_LEVEL_INVALID, FWUPD_BATTERY_LEVEL_INVALID, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_BATTERY_LEVEL, pspec); /** * FwupdClient:battery-threshold: * * The system battery threshold in percent. * * Since: 1.8.1 */ pspec = g_param_spec_uint("battery-threshold", NULL, NULL, 0, FWUPD_BATTERY_LEVEL_INVALID, FWUPD_BATTERY_LEVEL_INVALID, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_BATTERY_THRESHOLD, pspec); } static void fwupd_client_init(FwupdClient *self) { FwupdClientPrivate *priv = GET_PRIVATE(self); g_mutex_init(&priv->proxy_mutex); g_mutex_init(&priv->idle_mutex); priv->idle_sources = g_ptr_array_new_with_free_func((GDestroyNotify)fwupd_client_context_helper_free); priv->proxy_resolver = g_proxy_resolver_get_default(); priv->hints = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); priv->battery_level = FWUPD_BATTERY_LEVEL_INVALID; priv->battery_threshold = FWUPD_BATTERY_LEVEL_INVALID; priv->immediate_requests = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_object_unref); /* we get this one for free */ fwupd_client_add_hint(self, "locale", g_getenv("LANG")); } static void fwupd_client_finalize(GObject *object) { FwupdClient *self = FWUPD_CLIENT(object); FwupdClientPrivate *priv = GET_PRIVATE(self); g_clear_pointer(&priv->main_ctx, g_main_context_unref); g_free(priv->user_agent); g_free(priv->package_name); g_free(priv->package_version); g_free(priv->daemon_version); g_free(priv->host_bkc); g_free(priv->host_vendor); g_free(priv->host_product); g_free(priv->host_machine_id); g_free(priv->host_security_id); g_free(priv->proxy_name_owner); g_hash_table_unref(priv->hints); g_hash_table_unref(priv->immediate_requests); g_mutex_clear(&priv->idle_mutex); if (priv->idle_id != 0) g_source_remove(priv->idle_id); g_ptr_array_unref(priv->idle_sources); g_mutex_clear(&priv->proxy_mutex); if (priv->proxy != NULL) g_object_unref(priv->proxy); G_OBJECT_CLASS(fwupd_client_parent_class)->finalize(object); } /** * fwupd_client_new: * * Creates a new client. * * Returns: a new #FwupdClient * * Since: 0.7.0 **/ FwupdClient * fwupd_client_new(void) { FwupdClient *self; self = g_object_new(FWUPD_TYPE_CLIENT, NULL); return FWUPD_CLIENT(self); } fwupd-2.0.10/libfwupd/fwupd-client.h000066400000000000000000000554361501337203100173150ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fwupd-build.h" #include "fwupd-device.h" #include "fwupd-enums.h" #include "fwupd-plugin.h" #include "fwupd-remote.h" #include "fwupd-request.h" G_BEGIN_DECLS #define FWUPD_TYPE_CLIENT (fwupd_client_get_type()) G_DECLARE_DERIVABLE_TYPE(FwupdClient, fwupd_client, FWUPD, CLIENT, GObject) struct _FwupdClientClass { GObjectClass parent_class; void (*changed)(FwupdClient *client); void (*status_changed)(FwupdClient *client, FwupdStatus status); void (*device_added)(FwupdClient *client, FwupdDevice *result); void (*device_removed)(FwupdClient *client, FwupdDevice *result); void (*device_changed)(FwupdClient *client, FwupdDevice *result); void (*device_request)(FwupdClient *client, FwupdRequest *request); /*< private >*/ void (*_fwupd_reserved1)(void); void (*_fwupd_reserved2)(void); void (*_fwupd_reserved3)(void); void (*_fwupd_reserved4)(void); void (*_fwupd_reserved5)(void); void (*_fwupd_reserved6)(void); }; /** * FwupdClientDownloadFlags: * * The options to use for downloading. **/ typedef enum { /** * FWUPD_CLIENT_DOWNLOAD_FLAG_NONE: * * No flags set. * * Since: 1.4.5 */ FWUPD_CLIENT_DOWNLOAD_FLAG_NONE = 0, /** * FWUPD_CLIENT_DOWNLOAD_FLAG_ONLY_P2P: * * Only use peer-to-peer when downloading URIs. * * Since: 1.9.4 */ FWUPD_CLIENT_DOWNLOAD_FLAG_ONLY_P2P = 1 << 0, /*< private >*/ FWUPD_CLIENT_DOWNLOAD_FLAG_LAST } FwupdClientDownloadFlags; /** * FwupdClientUploadFlags: * * The options to use for uploading. **/ typedef enum { /** * FWUPD_CLIENT_UPLOAD_FLAG_NONE: * * No flags set. * * Since: 1.4.5 */ FWUPD_CLIENT_UPLOAD_FLAG_NONE = 0, /** * FWUPD_CLIENT_UPLOAD_FLAG_ALWAYS_MULTIPART: * * Always use multipart/form-data. * * Since: 1.4.5 */ FWUPD_CLIENT_UPLOAD_FLAG_ALWAYS_MULTIPART = 1 << 0, /*< private >*/ FWUPD_CLIENT_UPLOAD_FLAG_LAST } FwupdClientUploadFlags; FwupdClient * fwupd_client_new(void); GMainContext * fwupd_client_get_main_context(FwupdClient *self) G_GNUC_NON_NULL(1); void fwupd_client_set_main_context(FwupdClient *self, GMainContext *main_ctx) G_GNUC_NON_NULL(1); void fwupd_client_connect_async(FwupdClient *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1); gboolean fwupd_client_connect_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_disconnect(FwupdClient *self, GError **error) G_GNUC_NON_NULL(1); void fwupd_client_quit_async(FwupdClient *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1); gboolean fwupd_client_quit_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_get_devices_async(FwupdClient *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1); GPtrArray * fwupd_client_get_devices_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_get_plugins_async(FwupdClient *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1); GPtrArray * fwupd_client_get_plugins_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_get_history_async(FwupdClient *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1); GPtrArray * fwupd_client_get_history_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_get_releases_async(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); GPtrArray * fwupd_client_get_releases_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_get_downgrades_async(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); GPtrArray * fwupd_client_get_downgrades_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_get_upgrades_async(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); GPtrArray * fwupd_client_get_upgrades_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_get_details_bytes_async(FwupdClient *self, GBytes *bytes, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); GPtrArray * fwupd_client_get_details_bytes_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_get_details_async(FwupdClient *self, const gchar *filename, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); GPtrArray * fwupd_client_get_details_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_verify_async(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_verify_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_verify_update_async(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_verify_update_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_unlock_async(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1); gboolean fwupd_client_unlock_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_modify_config_async(FwupdClient *self, const gchar *section, const gchar *key, const gchar *value, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_modify_config_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_reset_config_async(FwupdClient *self, const gchar *section, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_reset_config_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_activate_async(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_activate_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_clear_results_async(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_clear_results_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_get_results_async(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); FwupdDevice * fwupd_client_get_results_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_modify_bios_setting_async(FwupdClient *self, GHashTable *settings, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_modify_bios_setting_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_get_bios_settings_async(FwupdClient *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1); GPtrArray * fwupd_client_get_bios_settings_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_get_host_security_attrs_async(FwupdClient *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1); GPtrArray * fwupd_client_get_host_security_attrs_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_get_host_security_events_async(FwupdClient *self, guint limit, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1); GPtrArray * fwupd_client_get_host_security_events_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_get_device_by_id_async(FwupdClient *self, const gchar *device_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); FwupdDevice * fwupd_client_get_device_by_id_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_get_devices_by_guid_async(FwupdClient *self, const gchar *guid, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1); GPtrArray * fwupd_client_get_devices_by_guid_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_install_async(FwupdClient *self, const gchar *device_id, const gchar *filename, FwupdInstallFlags install_flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2, 3); gboolean fwupd_client_install_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_install_bytes_async(FwupdClient *self, const gchar *device_id, GBytes *bytes, FwupdInstallFlags install_flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2, 3); gboolean fwupd_client_install_bytes_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_install_release_async(FwupdClient *self, FwupdDevice *device, FwupdRelease *release, FwupdInstallFlags install_flags, FwupdClientDownloadFlags download_flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2, 3); gboolean fwupd_client_install_release_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_update_metadata_bytes_async(FwupdClient *self, const gchar *remote_id, GBytes *metadata, GBytes *signature, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2, 3, 4); gboolean fwupd_client_update_metadata_bytes_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_refresh_remote_async(FwupdClient *self, FwupdRemote *remote, FwupdClientDownloadFlags download_flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_refresh_remote_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_modify_remote_async(FwupdClient *self, const gchar *remote_id, const gchar *key, const gchar *value, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2, 3); gboolean fwupd_client_modify_remote_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_modify_device_async(FwupdClient *self, const gchar *device_id, const gchar *key, const gchar *value, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2, 3); gboolean fwupd_client_modify_device_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_get_report_metadata_async(FwupdClient *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1); GHashTable * fwupd_client_get_report_metadata_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_inhibit_async(FwupdClient *self, const gchar *reason, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); gchar * fwupd_client_inhibit_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_uninhibit_async(FwupdClient *self, const gchar *inhibit_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_uninhibit_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_emulation_load_async(FwupdClient *self, const gchar *filename, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_emulation_load_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_NON_NULL(1, 2); void fwupd_client_emulation_save_async(FwupdClient *self, const gchar *filename, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1); gboolean fwupd_client_emulation_save_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_fix_host_security_attr_async(FwupdClient *self, const gchar *appstream_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_fix_host_security_attr_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_NON_NULL(1); void fwupd_client_undo_host_security_attr_async(FwupdClient *self, const gchar *appstream_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_undo_host_security_attr_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_NON_NULL(1, 2); FwupdStatus fwupd_client_get_status(FwupdClient *self) G_GNUC_NON_NULL(1); gboolean fwupd_client_get_tainted(FwupdClient *self) G_GNUC_NON_NULL(1); gboolean fwupd_client_get_only_trusted(FwupdClient *self) G_GNUC_NON_NULL(1); gboolean fwupd_client_get_daemon_interactive(FwupdClient *self) G_GNUC_NON_NULL(1); guint fwupd_client_get_percentage(FwupdClient *self) G_GNUC_NON_NULL(1); const gchar * fwupd_client_get_daemon_version(FwupdClient *self) G_GNUC_NON_NULL(1); void fwupd_client_set_daemon_version(FwupdClient *self, const gchar *daemon_version) G_GNUC_NON_NULL(1); const gchar * fwupd_client_get_host_bkc(FwupdClient *self) G_GNUC_NON_NULL(1); const gchar * fwupd_client_get_host_vendor(FwupdClient *self) G_GNUC_NON_NULL(1); const gchar * fwupd_client_get_host_product(FwupdClient *self) G_GNUC_NON_NULL(1); const gchar * fwupd_client_get_host_machine_id(FwupdClient *self) G_GNUC_NON_NULL(1); const gchar * fwupd_client_get_host_security_id(FwupdClient *self) G_GNUC_NON_NULL(1); guint32 fwupd_client_get_battery_level(FwupdClient *self) G_GNUC_NON_NULL(1); guint32 fwupd_client_get_battery_threshold(FwupdClient *self) G_GNUC_NON_NULL(1); void fwupd_client_get_remotes_async(FwupdClient *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1); GPtrArray * fwupd_client_get_remotes_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_get_remote_by_id_async(FwupdClient *self, const gchar *remote_id, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); FwupdRemote * fwupd_client_get_remote_by_id_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_get_approved_firmware_async(FwupdClient *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1); GPtrArray * fwupd_client_get_approved_firmware_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_set_approved_firmware_async(FwupdClient *self, GPtrArray *checksums, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_set_approved_firmware_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_get_blocked_firmware_async(FwupdClient *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); GPtrArray * fwupd_client_get_blocked_firmware_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_set_blocked_firmware_async(FwupdClient *self, GPtrArray *checksums, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_set_blocked_firmware_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_self_sign_async(FwupdClient *self, const gchar *value, FwupdSelfSignFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); gchar * fwupd_client_self_sign_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_set_feature_flags_async(FwupdClient *self, FwupdFeatureFlags feature_flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1); gboolean fwupd_client_set_feature_flags_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); const gchar * fwupd_client_get_user_agent(FwupdClient *self) G_GNUC_NON_NULL(1); void fwupd_client_set_user_agent(FwupdClient *self, const gchar *user_agent) G_GNUC_NON_NULL(1); void fwupd_client_set_user_agent_for_package(FwupdClient *self, const gchar *package_name, const gchar *package_version) G_GNUC_NON_NULL(1, 2, 3); void fwupd_client_download_bytes_async(FwupdClient *self, const gchar *url, FwupdClientDownloadFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2); GBytes * fwupd_client_download_bytes_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_download_set_retries(FwupdClient *self, guint retries) G_GNUC_NON_NULL(1); void fwupd_client_upload_bytes_async(FwupdClient *self, const gchar *url, const gchar *payload, const gchar *signature, FwupdClientUploadFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2, 3); GBytes * fwupd_client_upload_bytes_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fwupd_client_upload_report_async(FwupdClient *self, const gchar *url, const gchar *payload, const gchar *signature, FwupdClientUploadFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) G_GNUC_NON_NULL(1, 2, 3); gchar * fwupd_client_upload_report_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fwupd_client_ensure_networking(FwupdClient *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); void fwupd_client_add_hint(FwupdClient *self, const gchar *key, const gchar *value) G_GNUC_NON_NULL(1, 2, 3); gchar * fwupd_client_build_report_devices(FwupdClient *self, GPtrArray *devices, GHashTable *metadata, GError **error) G_GNUC_NON_NULL(1, 2, 3); gchar * fwupd_client_build_report_history(FwupdClient *self, GPtrArray *devices, FwupdRemote *remote, GHashTable *metadata, GError **error) G_GNUC_NON_NULL(1, 2, 4); gchar * fwupd_client_build_report_security(FwupdClient *self, GPtrArray *attrs, GHashTable *metadata, GError **error) G_GNUC_NON_NULL(1, 2, 3); G_END_DECLS fwupd-2.0.10/libfwupd/fwupd-codec.c000066400000000000000000000466571501337203100171140ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fwupd-codec.h" #include "fwupd-error.h" /** * FwupdCodec: * * A codec that can serialize and deserialize objects to formats such as text, JSON or #GVariant. */ G_DEFINE_INTERFACE(FwupdCodec, fwupd_codec, G_TYPE_OBJECT) static void fwupd_codec_default_init(FwupdCodecInterface *iface) { } static void fwupd_codec_add_string_from_json_node(FwupdCodec *self, const gchar *member_name, JsonNode *json_node, guint idt, GString *str) { JsonNodeType node_type = json_node_get_node_type(json_node); if (node_type == JSON_NODE_VALUE) { GType gtype = json_node_get_value_type(json_node); if (gtype == G_TYPE_STRING) { fwupd_codec_string_append(str, idt, member_name, json_node_get_string(json_node)); } else if (gtype == G_TYPE_INT64) { fwupd_codec_string_append_hex(str, idt, member_name, json_node_get_int(json_node)); } else if (gtype == G_TYPE_BOOLEAN) { fwupd_codec_string_append_bool(str, idt, member_name, json_node_get_boolean(json_node)); } else { fwupd_codec_string_append(str, idt, member_name, "GType value unknown"); } } else if (node_type == JSON_NODE_ARRAY) { JsonArray *json_array = json_node_get_array(json_node); g_autoptr(GList) json_nodes = json_array_get_elements(json_array); if (g_strcmp0(member_name, "") != 0) fwupd_codec_string_append(str, idt, member_name, ""); for (GList *l = json_nodes; l != NULL; l = l->next) fwupd_codec_add_string_from_json_node(self, "", l->data, idt + 1, str); } else if (node_type == JSON_NODE_OBJECT) { JsonObjectIter iter; json_object_iter_init(&iter, json_node_get_object(json_node)); if (g_strcmp0(member_name, "") != 0) fwupd_codec_string_append(str, idt, member_name, ""); while (json_object_iter_next(&iter, &member_name, &json_node)) { fwupd_codec_add_string_from_json_node(self, member_name, json_node, idt + 1, str); } } } /** * fwupd_codec_add_string: * @self: a #FwupdCodec * @idt: the indent * @str: (not nullable): a string to append to * * Converts an object that implements #FwupdCodec to a debug string, appending it to @str. * * Since: 2.0.0 */ void fwupd_codec_add_string(FwupdCodec *self, guint idt, GString *str) { FwupdCodecInterface *iface; g_return_if_fail(FWUPD_IS_CODEC(self)); g_return_if_fail(str != NULL); fwupd_codec_string_append(str, idt, G_OBJECT_TYPE_NAME(self), ""); iface = FWUPD_CODEC_GET_IFACE(self); if (iface->add_string != NULL) { iface->add_string(self, idt + 1, str); return; } if (iface->add_json != NULL) { g_autoptr(JsonBuilder) builder = json_builder_new(); g_autoptr(JsonNode) root_node = NULL; json_builder_begin_object(builder); iface->add_json(self, builder, FWUPD_CODEC_FLAG_TRUSTED); json_builder_end_object(builder); root_node = json_builder_get_root(builder); fwupd_codec_add_string_from_json_node(self, "", root_node, idt + 1, str); return; } g_critical("FwupdCodec->add_string or iface->add_json not implemented"); } /** * fwupd_codec_to_string: * @self: a #FwupdCodec * * Converts an object that implements #FwupdCodec to a debug string. * * Returns: (transfer full): a string * * Since: 2.0.0 */ gchar * fwupd_codec_to_string(FwupdCodec *self) { FwupdCodecInterface *iface; g_return_val_if_fail(FWUPD_IS_CODEC(self), NULL); iface = FWUPD_CODEC_GET_IFACE(self); if (iface->to_string != NULL) return iface->to_string(self); if (iface->add_string != NULL || iface->add_json != NULL) { GString *str = g_string_new(NULL); fwupd_codec_add_string(self, 0, str); return g_string_free(str, FALSE); } g_critical("FwupdCodec->to_string and iface->add_string not implemented"); return NULL; } /** * fwupd_codec_from_json: * @self: a #FwupdCodec * @json_node: (not nullable): a JSON node * @error: (nullable): optional return location for an error * * Converts an object that implements #FwupdCodec from a JSON object. * * Returns: %TRUE on success * * Since: 2.0.0 */ gboolean fwupd_codec_from_json(FwupdCodec *self, JsonNode *json_node, GError **error) { FwupdCodecInterface *iface; g_return_val_if_fail(FWUPD_IS_CODEC(self), FALSE); g_return_val_if_fail(json_node != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); iface = FWUPD_CODEC_GET_IFACE(self); if (iface->from_json == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "FwupdCodec->from_json not implemented"); return FALSE; } return (*iface->from_json)(self, json_node, error); } /** * fwupd_codec_from_json_string: * @self: a #FwupdCodec * @json: (not nullable): JSON text * @error: (nullable): optional return location for an error * * Converts an object that implements #FwupdCodec from a JSON string. * * Returns: %TRUE on success * * Since: 2.0.0 */ gboolean fwupd_codec_from_json_string(FwupdCodec *self, const gchar *json, GError **error) { g_autoptr(JsonParser) parser = json_parser_new(); g_return_val_if_fail(FWUPD_IS_CODEC(self), FALSE); g_return_val_if_fail(json != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!json_parser_load_from_data(parser, json, -1, error)) { g_prefix_error(error, "failed to load '%s': ", json); return FALSE; } return fwupd_codec_from_json(self, json_parser_get_root(parser), error); } /** * fwupd_codec_to_json: * @self: a #FwupdCodec * @builder: (not nullable): a JSON builder * @flags: a #FwupdCodecFlags, e.g. %FWUPD_CODEC_FLAG_TRUSTED * * Converts an object that implements #FwupdCodec to a JSON builder object. * * Since: 2.0.0 */ void fwupd_codec_to_json(FwupdCodec *self, JsonBuilder *builder, FwupdCodecFlags flags) { FwupdCodecInterface *iface; g_return_if_fail(FWUPD_IS_CODEC(self)); g_return_if_fail(builder != NULL); iface = FWUPD_CODEC_GET_IFACE(self); if (iface->add_json == NULL) { g_critical("FwupdCodec->add_json not implemented"); return; } iface->add_json(self, builder, flags); } /** * fwupd_codec_to_json_string: * @self: a #FwupdCodec * @flags: a #FwupdCodecFlags, e.g. %FWUPD_CODEC_FLAG_TRUSTED * @error: (nullable): optional return location for an error * * Converts an object that implements #FwupdCodec to a JSON string. * * Returns: (transfer full): a string * * Since: 2.0.0 */ gchar * fwupd_codec_to_json_string(FwupdCodec *self, FwupdCodecFlags flags, GError **error) { g_autofree gchar *data = NULL; g_autoptr(JsonGenerator) json_generator = NULL; g_autoptr(JsonBuilder) builder = json_builder_new(); g_autoptr(JsonNode) json_root = NULL; g_return_val_if_fail(FWUPD_IS_CODEC(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); json_builder_begin_object(builder); fwupd_codec_to_json(self, builder, flags); json_builder_end_object(builder); json_root = json_builder_get_root(builder); json_generator = json_generator_new(); json_generator_set_pretty(json_generator, TRUE); json_generator_set_root(json_generator, json_root); data = json_generator_to_data(json_generator, NULL); if (data == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to convert to json"); return NULL; } return g_steal_pointer(&data); } /** * fwupd_codec_from_variant: * @self: a #FwupdCodec * @value: (not nullable): a JSON node * @error: (nullable): optional return location for an error * * Converts an object that implements #FwupdCodec from a #GVariant value. * * Returns: %TRUE on success * * Since: 2.0.0 */ gboolean fwupd_codec_from_variant(FwupdCodec *self, GVariant *value, GError **error) { FwupdCodecInterface *iface; g_return_val_if_fail(FWUPD_IS_CODEC(self), FALSE); g_return_val_if_fail(value != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); iface = FWUPD_CODEC_GET_IFACE(self); if (iface->from_variant != NULL) return (*iface->from_variant)(self, value, error); if (iface->from_variant_iter != NULL) { const gchar *type_string; type_string = g_variant_get_type_string(value); if (g_strcmp0(type_string, "(a{sv})") == 0) { g_autoptr(GVariantIter) iter = NULL; g_variant_get(value, "(a{sv})", &iter); iface->from_variant_iter(self, iter); } else if (g_strcmp0(type_string, "a{sv}") == 0) { g_autoptr(GVariantIter) iter = NULL; g_variant_get(value, "a{sv}", &iter); iface->from_variant_iter(self, iter); } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "GVariant type %s not known", type_string); return FALSE; } return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "FwupdCodec->from_variant not implemented"); return FALSE; } /** * fwupd_codec_array_from_variant: * @value: a JSON node * @gtype: a #GType that implements `FwupdCodec` * @error: (nullable): optional return location for an error * * Converts an array of objects, each deserialized from a #GVariant value. * * Returns: (element-type GObject) (transfer container): %TRUE on success * * Since: 2.0.0 */ GPtrArray * fwupd_codec_array_from_variant(GVariant *value, GType gtype, GError **error) { gsize sz; g_autoptr(GPtrArray) array = NULL; g_autoptr(GVariant) untuple = NULL; g_return_val_if_fail(value != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); array = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); untuple = g_variant_get_child_value(value, 0); sz = g_variant_n_children(untuple); for (guint i = 0; i < sz; i++) { g_autoptr(GObject) gobj = g_object_new(gtype, NULL); g_autoptr(GVariant) data = g_variant_get_child_value(untuple, i); if (!fwupd_codec_from_variant(FWUPD_CODEC(gobj), data, error)) return NULL; g_ptr_array_add(array, g_steal_pointer(&gobj)); } return g_steal_pointer(&array); } /** * fwupd_codec_array_to_json: * @array: (element-type GObject): (not nullable): array of objects that much implement `FwupdCodec` * @member_name: (not nullable): member name of the array * @builder: (not nullable): a #JsonBuilder * @flags: a #FwupdCodecFlags, e.g. %FWUPD_CODEC_FLAG_TRUSTED * * Converts an array of objects into a #GVariant value. * * Since: 2.0.0 */ void fwupd_codec_array_to_json(GPtrArray *array, const gchar *member_name, JsonBuilder *builder, FwupdCodecFlags flags) { g_return_if_fail(array != NULL); g_return_if_fail(member_name != NULL); g_return_if_fail(JSON_IS_BUILDER(builder)); json_builder_set_member_name(builder, member_name); json_builder_begin_array(builder); for (guint i = 0; i < array->len; i++) { FwupdCodec *codec = FWUPD_CODEC(g_ptr_array_index(array, i)); json_builder_begin_object(builder); fwupd_codec_to_json(codec, builder, flags); json_builder_end_object(builder); } json_builder_end_array(builder); } /** * fwupd_codec_array_to_variant: * @array: (element-type GObject): (not nullable): array of objects that much implement `FwupdCodec` * @flags: a #FwupdCodecFlags, e.g. %FWUPD_CODEC_FLAG_TRUSTED * * Converts an array of objects into a #GVariant value. * * Returns: (transfer full): a #GVariant * * Since: 2.0.0 */ GVariant * fwupd_codec_array_to_variant(GPtrArray *array, FwupdCodecFlags flags) { GVariantBuilder builder; g_return_val_if_fail(array != NULL, NULL); g_variant_builder_init(&builder, G_VARIANT_TYPE("aa{sv}")); for (guint i = 0; i < array->len; i++) { FwupdCodec *codec = FWUPD_CODEC(g_ptr_array_index(array, i)); g_variant_builder_add_value(&builder, fwupd_codec_to_variant(codec, flags)); } return g_variant_new("(aa{sv})", &builder); } /** * fwupd_codec_to_variant: * @self: a #FwupdCodec * @flags: a #FwupdCodecFlags, e.g. %FWUPD_CODEC_FLAG_TRUSTED * * Converts an object that implements #FwupdCodec to a #GVariant. * * Returns: (transfer full): a #GVariant * * Since: 2.0.0 */ GVariant * fwupd_codec_to_variant(FwupdCodec *self, FwupdCodecFlags flags) { FwupdCodecInterface *iface; g_return_val_if_fail(FWUPD_IS_CODEC(self), NULL); iface = FWUPD_CODEC_GET_IFACE(self); if (iface->to_variant != NULL) return iface->to_variant(self, flags); if (iface->add_variant != NULL) { GVariantBuilder builder; g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); iface->add_variant(self, &builder, flags); return g_variant_new("a{sv}", &builder); } g_critical("FwupdCodec->add_variant and iface->add_variant not implemented"); return NULL; } static gsize _fu_strwidth(const gchar *text) { const gchar *p = text; gsize width = 0; g_return_val_if_fail(text != NULL, 0); while (*p) { gunichar c = g_utf8_get_char(p); if (g_unichar_iswide(c)) width += 2; else if (!g_unichar_iszerowidth(c)) width += 1; p = g_utf8_next_char(p); } return width; } /** * fwupd_codec_string_append: * @str: (not nullable): a #GString * @idt: the indent * @key: (not nullable): a string to append * @value: a string to append * * Appends a key and value to a string. * * Since: 2.0.0 */ void fwupd_codec_string_append(GString *str, guint idt, const gchar *key, const gchar *value) { const guint align = 24; gsize keysz; g_return_if_fail(str != NULL); g_return_if_fail(key != NULL); g_return_if_fail(idt * 2 < align); /* ignore */ if (value == NULL) return; for (gsize i = 0; i < idt; i++) g_string_append(str, " "); if (key[0] != '\0') { g_string_append_printf(str, "%s:", key); keysz = (idt * 2) + _fu_strwidth(key) + 1; } else { keysz = idt * 2; } if (value != NULL) { g_auto(GStrv) split = NULL; split = g_strsplit(value, "\n", -1); for (guint i = 0; split[i] != NULL; i++) { if (i == 0) { g_string_append(str, " "); for (gsize j = keysz + 1; j < align; j++) g_string_append(str, " "); } else { g_string_append(str, "\n"); for (gsize j = 0; j < idt; j++) g_string_append(str, " "); } g_string_append(str, split[i]); } } g_string_append(str, "\n"); } /** * fwupd_codec_string_append_int: * @str: (not nullable): a #GString * @idt: the indent * @key: (not nullable): a string to append * @value: guint64 * * Appends a key and unsigned integer to a string. * * Since: 2.0.0 */ void fwupd_codec_string_append_int(GString *str, guint idt, const gchar *key, guint64 value) { g_autofree gchar *tmp = NULL; g_return_if_fail(str != NULL); g_return_if_fail(key != NULL); /* ignore */ if (value == 0) return; tmp = g_strdup_printf("%" G_GUINT64_FORMAT, value); fwupd_codec_string_append(str, idt, key, tmp); } /** * fwupd_codec_string_append_hex: * @str: (not nullable): a #GString * @idt: the indent * @key: (not nullable): a string to append * @value: guint64 * * Appends a key and hex integer to a string. * * Since: 2.0.0 */ void fwupd_codec_string_append_hex(GString *str, guint idt, const gchar *key, guint64 value) { g_autofree gchar *tmp = NULL; g_return_if_fail(str != NULL); g_return_if_fail(key != NULL); /* ignore */ if (value == 0) return; tmp = g_strdup_printf("0x%x", (guint)value); fwupd_codec_string_append(str, idt, key, tmp); } /** * fwupd_codec_string_append_bool: * @str: (not nullable): a #GString * @idt: the indent * @key: (not nullable): a string to append * @value: Boolean * * Appends a key and boolean value to a string. * * Since: 2.0.0 */ void fwupd_codec_string_append_bool(GString *str, guint idt, const gchar *key, gboolean value) { g_return_if_fail(str != NULL); g_return_if_fail(key != NULL); fwupd_codec_string_append(str, idt, key, value ? "true" : "false"); } /** * fwupd_codec_string_append_time: * @str: (not nullable): a #GString * @idt: the indent * @key: (not nullable): a string to append * @value: guint64 UNIX time * * Appends a key and time value to a string. * * Since: 2.0.0 */ void fwupd_codec_string_append_time(GString *str, guint idt, const gchar *key, guint64 value) { g_autofree gchar *tmp = NULL; g_autoptr(GDateTime) date = NULL; g_return_if_fail(str != NULL); g_return_if_fail(key != NULL); /* ignore */ if (value == 0) return; date = g_date_time_new_from_unix_utc((gint64)value); tmp = g_date_time_format(date, "%F"); fwupd_codec_string_append(str, idt, key, tmp); } /** * fwupd_codec_string_append_size: * @str: (not nullable): a #GString * @idt: the indent * @key: (not nullable): a string to append * @value: guint64 * * Appends a key and size in bytes to a string. * * Since: 2.0.0 */ void fwupd_codec_string_append_size(GString *str, guint idt, const gchar *key, guint64 value) { g_autofree gchar *tmp = NULL; g_return_if_fail(str != NULL); g_return_if_fail(key != NULL); /* ignore */ if (value == 0) return; tmp = g_format_size(value); fwupd_codec_string_append(str, idt, key, tmp); } /** * fwupd_codec_json_append: * @builder: (not nullable): a #JsonBuilder * @key: (not nullable): a string * @value: a string to append * * Appends a key and value to a JSON builder. * * Since: 2.0.0 */ void fwupd_codec_json_append(JsonBuilder *builder, const gchar *key, const gchar *value) { g_return_if_fail(JSON_IS_BUILDER(builder)); g_return_if_fail(key != NULL); if (value == NULL) return; json_builder_set_member_name(builder, key); json_builder_add_string_value(builder, value); } /** * fwupd_codec_json_append_int: * @builder: (not nullable): a #JsonBuilder * @key: (not nullable): a string * @value: guint64 * * Appends a key and unsigned integer to a JSON builder. * * Since: 2.0.0 */ void fwupd_codec_json_append_int(JsonBuilder *builder, const gchar *key, guint64 value) { g_return_if_fail(JSON_IS_BUILDER(builder)); g_return_if_fail(key != NULL); json_builder_set_member_name(builder, key); json_builder_add_int_value(builder, value); } /** * fwupd_codec_json_append_bool: * @builder: (not nullable): a #JsonBuilder * @key: (not nullable): a string * @value: boolean * * Appends a key and boolean value to a JSON builder. * * Since: 2.0.0 */ void fwupd_codec_json_append_bool(JsonBuilder *builder, const gchar *key, gboolean value) { g_return_if_fail(JSON_IS_BUILDER(builder)); g_return_if_fail(key != NULL); json_builder_set_member_name(builder, key); json_builder_add_boolean_value(builder, value); } /** * fwupd_codec_json_append_strv: * @builder: (not nullable): a #JsonBuilder * @key: (not nullable): a string * @value: a #GStrv * * Appends a key and string array to a JSON builder. * * Since: 2.0.0 */ void fwupd_codec_json_append_strv(JsonBuilder *builder, const gchar *key, gchar **value) { g_return_if_fail(JSON_IS_BUILDER(builder)); g_return_if_fail(key != NULL); if (value == NULL) return; json_builder_set_member_name(builder, key); json_builder_begin_array(builder); for (guint i = 0; value[i] != NULL; i++) json_builder_add_string_value(builder, value[i]); json_builder_end_array(builder); } /** * fwupd_codec_json_append_map: * @builder: (not nullable): a #JsonBuilder * @key: (not nullable): a string * @value: (element-type utf8 utf8): a hash table * * Appends a key and string hash map to a JSON builder. * * Since: 2.0.10 */ void fwupd_codec_json_append_map(JsonBuilder *builder, const gchar *key, GHashTable *value) { GHashTableIter iter; gpointer hash_key, hash_value; g_return_if_fail(JSON_IS_BUILDER(builder)); g_return_if_fail(key != NULL); if (value == NULL) return; json_builder_set_member_name(builder, key); json_builder_begin_object(builder); g_hash_table_iter_init(&iter, value); while (g_hash_table_iter_next(&iter, &hash_key, &hash_value)) { fwupd_codec_json_append(builder, (const gchar *)hash_key, (const gchar *)hash_value); } json_builder_end_object(builder); } fwupd-2.0.10/libfwupd/fwupd-codec.h000066400000000000000000000101771501337203100171050ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include #include "fwupd-build.h" #define FWUPD_TYPE_CODEC (fwupd_codec_get_type()) G_DECLARE_INTERFACE(FwupdCodec, fwupd_codec, FWUPD, CODEC, GObject) /** * FwupdCodecFlags: * * The flags to use when converting data from one form to another. **/ typedef enum { /** * FWUPD_CODEC_FLAG_NONE: * * No flags set. * * Since: 2.0.0 */ FWUPD_CODEC_FLAG_NONE = 0, /** * FWUPD_CODEC_FLAG_TRUSTED: * * Include values that may be regarded as trusted or sensitive. * * Since: 2.0.0 */ FWUPD_CODEC_FLAG_TRUSTED = 1 << 0, /** * FWUPD_CODEC_FLAG_COMPRESSED: * * Compress values to the smallest possible size. * * Since: 2.0.8 */ FWUPD_CODEC_FLAG_COMPRESSED = 1 << 1, } FwupdCodecFlags; struct _FwupdCodecInterface { GTypeInterface g_iface; void (*add_string)(FwupdCodec *self, guint idt, GString *str); gchar *(*to_string)(FwupdCodec *self); void (*add_json)(FwupdCodec *self, JsonBuilder *builder, FwupdCodecFlags flags); gboolean (*from_json)(FwupdCodec *self, JsonNode *json_node, GError **error); void (*add_variant)(FwupdCodec *self, GVariantBuilder *builder, FwupdCodecFlags flags); GVariant *(*to_variant)(FwupdCodec *self, FwupdCodecFlags flags); gboolean (*from_variant)(FwupdCodec *self, GVariant *value, GError **error); void (*from_variant_iter)(FwupdCodec *self, GVariantIter *iter); /*< private >*/ void (*_fwupd_reserved1)(void); void (*_fwupd_reserved2)(void); void (*_fwupd_reserved3)(void); void (*_fwupd_reserved4)(void); void (*_fwupd_reserved5)(void); void (*_fwupd_reserved6)(void); void (*_fwupd_reserved7)(void); }; gchar * fwupd_codec_to_string(FwupdCodec *self) G_GNUC_NON_NULL(1); void fwupd_codec_add_string(FwupdCodec *self, guint idt, GString *str) G_GNUC_NON_NULL(1, 3); gboolean fwupd_codec_from_json(FwupdCodec *self, JsonNode *json_node, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fwupd_codec_from_json_string(FwupdCodec *self, const gchar *json, GError **error) G_GNUC_NON_NULL(1, 2); void fwupd_codec_to_json(FwupdCodec *self, JsonBuilder *builder, FwupdCodecFlags flags) G_GNUC_NON_NULL(1, 2); gchar * fwupd_codec_to_json_string(FwupdCodec *self, FwupdCodecFlags flags, GError **error); void fwupd_codec_array_to_json(GPtrArray *array, const gchar *member_name, JsonBuilder *builder, FwupdCodecFlags flags); GVariant * fwupd_codec_to_variant(FwupdCodec *self, FwupdCodecFlags flags) G_GNUC_NON_NULL(1); gboolean fwupd_codec_from_variant(FwupdCodec *self, GVariant *value, GError **error) G_GNUC_NON_NULL(1, 2); GVariant * fwupd_codec_array_to_variant(GPtrArray *array, FwupdCodecFlags flags) G_GNUC_NON_NULL(1); GPtrArray * fwupd_codec_array_from_variant(GVariant *value, GType gtype, GError **error) G_GNUC_NON_NULL(1); void fwupd_codec_string_append(GString *str, guint idt, const gchar *key, const gchar *value) G_GNUC_NON_NULL(1, 3); void fwupd_codec_string_append_int(GString *str, guint idt, const gchar *key, guint64 value) G_GNUC_NON_NULL(1, 3); void fwupd_codec_string_append_hex(GString *str, guint idt, const gchar *key, guint64 value) G_GNUC_NON_NULL(1, 3); void fwupd_codec_string_append_bool(GString *str, guint idt, const gchar *key, gboolean value) G_GNUC_NON_NULL(1, 3); void fwupd_codec_string_append_time(GString *str, guint idt, const gchar *key, guint64 value) G_GNUC_NON_NULL(1, 3); void fwupd_codec_string_append_size(GString *str, guint idt, const gchar *key, guint64 value) G_GNUC_NON_NULL(1, 3); void fwupd_codec_json_append(JsonBuilder *builder, const gchar *key, const gchar *value) G_GNUC_NON_NULL(1, 2); void fwupd_codec_json_append_strv(JsonBuilder *builder, const gchar *key, gchar **value) G_GNUC_NON_NULL(1, 2); void fwupd_codec_json_append_map(JsonBuilder *builder, const gchar *key, GHashTable *value) G_GNUC_NON_NULL(1, 2); void fwupd_codec_json_append_int(JsonBuilder *builder, const gchar *key, guint64 value) G_GNUC_NON_NULL(1, 2); void fwupd_codec_json_append_bool(JsonBuilder *builder, const gchar *key, gboolean value) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupd/fwupd-common-private.h000066400000000000000000000016251501337203100207660ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #ifdef HAVE_GIO_UNIX #include #include #endif #include #include "fwupd-common.h" G_BEGIN_DECLS GVariant * fwupd_hash_kv_to_variant(GHashTable *hash) G_GNUC_NON_NULL(1); GHashTable * fwupd_variant_to_hash_kv(GVariant *dict) G_GNUC_NON_NULL(1); #ifdef HAVE_GIO_UNIX GUnixInputStream * fwupd_unix_input_stream_from_bytes(GBytes *bytes, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); GUnixInputStream * fwupd_unix_input_stream_from_fn(const gchar *fn, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); GUnixOutputStream * fwupd_unix_output_stream_from_fn(const gchar *fn, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); #endif G_END_DECLS fwupd-2.0.10/libfwupd/fwupd-common.c000066400000000000000000000354631501337203100173200ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fwupd-common-private.h" #include "fwupd-device.h" #include "fwupd-error.h" #include "fwupd-release.h" #ifdef HAVE_GIO_UNIX #include #include #include #include #endif #ifdef HAVE_MEMFD_CREATE #include #endif #include #include /** * fwupd_checksum_guess_kind: * @checksum: (nullable): a checksum * * Guesses the checksum kind based on the length of the hash. * * Returns: a checksum type, e.g. %G_CHECKSUM_SHA1 * * Since: 0.9.3 **/ GChecksumType fwupd_checksum_guess_kind(const gchar *checksum) { guint len; g_return_val_if_fail(checksum != NULL, G_CHECKSUM_SHA1); len = strlen(checksum); if (len == 32) return G_CHECKSUM_MD5; if (len == 40) return G_CHECKSUM_SHA1; if (len == 64) return G_CHECKSUM_SHA256; if (len == 96) return G_CHECKSUM_SHA384; if (len == 128) return G_CHECKSUM_SHA512; return G_CHECKSUM_SHA1; } /** * fwupd_checksum_type_to_string_display: * @checksum_type: a #GChecksumType, e.g. %G_CHECKSUM_SHA1 * * Formats a checksum type for display. * * Returns: text, or %NULL for invalid * * Since: 1.9.6 **/ const gchar * fwupd_checksum_type_to_string_display(GChecksumType checksum_type) { if (checksum_type == G_CHECKSUM_MD5) return "MD5"; if (checksum_type == G_CHECKSUM_SHA1) return "SHA1"; if (checksum_type == G_CHECKSUM_SHA256) return "SHA256"; if (checksum_type == G_CHECKSUM_SHA384) return "SHA384"; if (checksum_type == G_CHECKSUM_SHA512) return "SHA512"; return NULL; } /** * fwupd_checksum_format_for_display: * @checksum: (nullable): a checksum * * Formats a checksum for display. * * Returns: text, or %NULL for invalid * * Since: 0.9.3 **/ gchar * fwupd_checksum_format_for_display(const gchar *checksum) { GChecksumType kind = fwupd_checksum_guess_kind(checksum); return g_strdup_printf("%s(%s)", fwupd_checksum_type_to_string_display(kind), checksum); } /** * fwupd_checksum_get_by_kind: * @checksums: (element-type utf8): checksums * @kind: a checksum type, e.g. %G_CHECKSUM_SHA512 * * Gets a specific checksum kind. * * Returns: a checksum from the array, or %NULL if not found * * Since: 0.9.4 **/ const gchar * fwupd_checksum_get_by_kind(GPtrArray *checksums, GChecksumType kind) { g_return_val_if_fail(checksums != NULL, NULL); for (guint i = 0; i < checksums->len; i++) { const gchar *checksum = g_ptr_array_index(checksums, i); if (fwupd_checksum_guess_kind(checksum) == kind) return checksum; } return NULL; } /** * fwupd_checksum_get_best: * @checksums: (element-type utf8): checksums * * Gets a the best possible checksum kind. * * Returns: a checksum from the array, or %NULL if nothing was suitable * * Since: 0.9.4 **/ const gchar * fwupd_checksum_get_best(GPtrArray *checksums) { GChecksumType checksum_types[] = {G_CHECKSUM_SHA512, G_CHECKSUM_SHA256, G_CHECKSUM_SHA384, G_CHECKSUM_SHA1, 0}; g_return_val_if_fail(checksums != NULL, NULL); for (guint i = 0; checksum_types[i] != 0; i++) { for (guint j = 0; j < checksums->len; j++) { const gchar *checksum = g_ptr_array_index(checksums, j); if (fwupd_checksum_guess_kind(checksum) == checksum_types[i]) return checksum; } } return NULL; } #define FWUPD_GUID_NAMESPACE_DEFAULT "6ba7b810-9dad-11d1-80b4-00c04fd430c8" #define FWUPD_GUID_NAMESPACE_MICROSOFT "70ffd812-4c7f-4c7d-0000-000000000000" typedef struct __attribute__((packed)) { /* nocheck:blocked */ guint32 a; guint16 b; guint16 c; guint16 d; guint8 e[6]; } fwupd_guid_native_t; /** * fwupd_guid_to_string: * @guid: a #fwupd_guid_t to read * @flags: GUID flags, e.g. %FWUPD_GUID_FLAG_MIXED_ENDIAN * * Returns a text GUID of mixed or BE endian for a packed buffer. * * Returns: a new GUID string * * Since: 1.2.5 **/ gchar * fwupd_guid_to_string(const fwupd_guid_t *guid, FwupdGuidFlags flags) { fwupd_guid_native_t gnat; g_return_val_if_fail(guid != NULL, NULL); /* copy to avoid issues with aligning */ memcpy(&gnat, guid, sizeof(gnat)); /* nocheck:blocked */ /* mixed is bizaar, but specified as the DCE encoding */ if (flags & FWUPD_GUID_FLAG_MIXED_ENDIAN) { return g_strdup_printf("%08x-%04x-%04x-%04x-%02x%02x%02x%02x%02x%02x", (guint)GUINT32_FROM_LE(gnat.a), /* nocheck:blocked */ (guint)GUINT16_FROM_LE(gnat.b), /* nocheck:blocked */ (guint)GUINT16_FROM_LE(gnat.c), /* nocheck:blocked */ (guint)GUINT16_FROM_BE(gnat.d), /* nocheck:blocked */ gnat.e[0], gnat.e[1], gnat.e[2], gnat.e[3], gnat.e[4], gnat.e[5]); } return g_strdup_printf("%08x-%04x-%04x-%04x-%02x%02x%02x%02x%02x%02x", (guint)GUINT32_FROM_BE(gnat.a), /* nocheck:blocked */ (guint)GUINT16_FROM_BE(gnat.b), /* nocheck:blocked */ (guint)GUINT16_FROM_BE(gnat.c), /* nocheck:blocked */ (guint)GUINT16_FROM_BE(gnat.d), /* nocheck:blocked */ gnat.e[0], gnat.e[1], gnat.e[2], gnat.e[3], gnat.e[4], gnat.e[5]); } /** * fwupd_guid_from_string: * @guidstr: (not nullable): a GUID, e.g. `00112233-4455-6677-8899-aabbccddeeff` * @guid: (nullable): a #fwupd_guid_t, or NULL to just check the GUID * @flags: GUID flags, e.g. %FWUPD_GUID_FLAG_MIXED_ENDIAN * @error: (nullable): optional return location for an error * * Converts a string GUID into its binary encoding. All string GUIDs are * formatted as big endian but on-disk can be encoded in different ways. * * Returns: %TRUE for success * * Since: 1.2.5 **/ gboolean fwupd_guid_from_string(const gchar *guidstr, fwupd_guid_t *guid, FwupdGuidFlags flags, GError **error) { fwupd_guid_native_t gu = {0x0}; gboolean mixed_endian = flags & FWUPD_GUID_FLAG_MIXED_ENDIAN; guint64 tmp; g_auto(GStrv) split = NULL; g_return_val_if_fail(guidstr != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* split into sections */ if (strlen(guidstr) != 36) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "GUID is not valid format"); return FALSE; } split = g_strsplit(guidstr, "-", 5); if (g_strv_length(split) != 5) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "GUID is not valid format, no dashes"); return FALSE; } if (strlen(split[0]) != 8 || strlen(split[1]) != 4 || strlen(split[2]) != 4 || strlen(split[3]) != 4 || strlen(split[4]) != 12) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "GUID is not valid format, not GUID"); return FALSE; } /* parse */ if (!g_ascii_string_to_unsigned(split[0], 16, 0, 0xffffffff, &tmp, error)) return FALSE; gu.a = mixed_endian ? GUINT32_TO_LE(tmp) : GUINT32_TO_BE(tmp); /* nocheck:blocked */ if (!g_ascii_string_to_unsigned(split[1], 16, 0, 0xffff, &tmp, error)) return FALSE; gu.b = mixed_endian ? GUINT16_TO_LE(tmp) : GUINT16_TO_BE(tmp); /* nocheck:blocked */ if (!g_ascii_string_to_unsigned(split[2], 16, 0, 0xffff, &tmp, error)) return FALSE; gu.c = mixed_endian ? GUINT16_TO_LE(tmp) : GUINT16_TO_BE(tmp); /* nocheck:blocked */ if (!g_ascii_string_to_unsigned(split[3], 16, 0, 0xffff, &tmp, error)) return FALSE; gu.d = GUINT16_TO_BE(tmp); /* nocheck:blocked */ for (guint i = 0; i < 6; i++) { gchar buffer[3] = {0x0}; memcpy(buffer, split[4] + (i * 2), 2); /* nocheck:blocked */ if (!g_ascii_string_to_unsigned(buffer, 16, 0, 0xff, &tmp, error)) return FALSE; gu.e[i] = tmp; } if (guid != NULL) memcpy(guid, &gu, sizeof(gu)); /* nocheck:blocked */ /* success */ return TRUE; } /** * fwupd_guid_hash_data: * @data: data to hash * @datasz: length of @data * @flags: GUID flags, e.g. %FWUPD_GUID_FLAG_NAMESPACE_MICROSOFT * * Returns a GUID for some data. This uses a hash and so even small * differences in the @data will produce radically different return values. * * The implementation is taken from RFC4122, Section 4.1.3; specifically * using a type-5 SHA-1 hash. * * Returns: a new GUID, or %NULL for internal error * * Since: 1.2.5 **/ gchar * fwupd_guid_hash_data(const guint8 *data, gsize datasz, FwupdGuidFlags flags) { gsize digestlen = 20; guint8 hash[20]; fwupd_guid_t uu_new; g_autoptr(GChecksum) csum = NULL; const fwupd_guid_t uu_default = {0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8}; const fwupd_guid_t uu_microso = {0x70, 0xff, 0xd8, 0x12, 0x4c, 0x7f, 0x4c, 0x7d}; const fwupd_guid_t *uu_namespace = &uu_default; g_return_val_if_fail(data != NULL, NULL); g_return_val_if_fail(datasz != 0, NULL); /* old MS GUID */ if (flags & FWUPD_GUID_FLAG_NAMESPACE_MICROSOFT) uu_namespace = &uu_microso; /* hash the namespace and then the string */ csum = g_checksum_new(G_CHECKSUM_SHA1); g_checksum_update(csum, (guchar *)uu_namespace, sizeof(*uu_namespace)); g_checksum_update(csum, (guchar *)data, (gssize)datasz); g_checksum_get_digest(csum, hash, &digestlen); /* copy most parts of the hash 1:1 */ memcpy(uu_new, hash, sizeof(uu_new)); /* nocheck:blocked */ /* set specific bits according to Section 4.1.3 */ uu_new[6] = (guint8)((uu_new[6] & 0x0f) | (5 << 4)); uu_new[8] = (guint8)((uu_new[8] & 0x3f) | 0x80); return fwupd_guid_to_string((const fwupd_guid_t *)&uu_new, flags); } /** * fwupd_device_id_is_valid: * @device_id: string to check, e.g. `d3fae86d95e5d56626129d00e332c4b8dac95442` * * Checks the string is a valid non-partial device ID. It is important to note * that the wildcard ID of `*` is not considered a valid ID in this function and * the client must check for this manually if this should be allowed. * * Returns: %TRUE if @guid was a fwupd device ID, %FALSE otherwise * * Since: 1.4.1 **/ gboolean fwupd_device_id_is_valid(const gchar *device_id) { if (device_id == NULL) return FALSE; if (strlen(device_id) != 40) return FALSE; for (guint i = 0; device_id[i] != '\0'; i++) { gchar tmp = device_id[i]; /* isalnum isn't case specific */ if ((tmp < 'a' || tmp > 'f') && (tmp < '0' || tmp > '9')) return FALSE; } return TRUE; } /** * fwupd_guid_is_valid: * @guid: string to check, e.g. `00112233-4455-6677-8899-aabbccddeeff` * * Checks the string is a valid GUID. * * Returns: %TRUE if @guid was a valid GUID, %FALSE otherwise * * Since: 1.2.5 **/ gboolean fwupd_guid_is_valid(const gchar *guid) { const gchar zeroguid[] = {"00000000-0000-0000-0000-000000000000"}; /* sanity check */ if (guid == NULL) return FALSE; /* check for dashes and hexdigits in the right place */ for (guint i = 0; i < sizeof(zeroguid) - 1; i++) { if (guid[i] == '\0') return FALSE; if (zeroguid[i] == '-') { if (guid[i] != '-') return FALSE; continue; } if (!g_ascii_isxdigit(guid[i])) return FALSE; } /* longer than required */ if (guid[sizeof(zeroguid) - 1] != '\0') return FALSE; /* not valid */ return g_strcmp0(guid, zeroguid) != 0; } /** * fwupd_guid_hash_string: * @str: (nullable): a source string to use as a key * * Returns a GUID for a given string. This uses a hash and so even small * differences in the @str will produce radically different return values. * * The default implementation is taken from RFC4122, Section 4.1.3; specifically * using a type-5 SHA-1 hash with a DNS namespace. * The same result can be obtained with this simple python program: * * #!/usr/bin/python * import uuid * print uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org') * * Returns: a new GUID, or %NULL if the string was invalid * * Since: 1.2.5 **/ gchar * fwupd_guid_hash_string(const gchar *str) { if (str == NULL || str[0] == '\0') return NULL; return fwupd_guid_hash_data((const guint8 *)str, strlen(str), FWUPD_GUID_FLAG_NONE); } /** * fwupd_hash_kv_to_variant: (skip): **/ GVariant * fwupd_hash_kv_to_variant(GHashTable *hash) { GVariantBuilder builder; g_autoptr(GList) keys = g_hash_table_get_keys(hash); g_variant_builder_init(&builder, G_VARIANT_TYPE_ARRAY); for (GList *l = keys; l != NULL; l = l->next) { const gchar *key = l->data; const gchar *value = g_hash_table_lookup(hash, key); g_variant_builder_add(&builder, "{ss}", key, value); } return g_variant_builder_end(&builder); } /** * fwupd_variant_to_hash_kv: (skip): **/ GHashTable * fwupd_variant_to_hash_kv(GVariant *dict) { GHashTable *hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); GVariantIter iter; const gchar *key; const gchar *value; g_variant_iter_init(&iter, dict); while (g_variant_iter_loop(&iter, "{&s&s}", &key, &value)) g_hash_table_insert(hash, g_strdup(key), g_strdup(value)); return hash; } #ifdef HAVE_GIO_UNIX /** * fwupd_unix_input_stream_from_bytes: (skip): **/ GUnixInputStream * fwupd_unix_input_stream_from_bytes(GBytes *bytes, GError **error) { gint fd; gssize rc; #ifndef HAVE_MEMFD_CREATE gchar tmp_file[] = "/tmp/fwupd.XXXXXX"; #endif #ifdef HAVE_MEMFD_CREATE fd = memfd_create("fwupd", MFD_CLOEXEC); #else /* emulate in-memory file by an unlinked temporary file */ fd = g_mkstemp(tmp_file); if (fd != -1) { rc = g_unlink(tmp_file); if (rc != 0) { if (!g_close(fd, error)) { g_prefix_error(error, "failed to close temporary file: "); return NULL; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to unlink temporary file"); return NULL; } } #endif if (fd < 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to create memfd"); return NULL; } rc = write(fd, g_bytes_get_data(bytes, NULL), g_bytes_get_size(bytes)); if (rc < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to write %" G_GSSIZE_FORMAT, rc); return NULL; } if (lseek(fd, 0, SEEK_SET) < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to seek: %s", g_strerror(errno)); return NULL; } return G_UNIX_INPUT_STREAM(g_unix_input_stream_new(fd, TRUE)); } /** * fwupd_unix_input_stream_from_fn: (skip): **/ GUnixInputStream * fwupd_unix_input_stream_from_fn(const gchar *fn, GError **error) { gint fd = open(fn, O_RDONLY); if (fd < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to open %s", fn); return NULL; } return G_UNIX_INPUT_STREAM(g_unix_input_stream_new(fd, TRUE)); } /** * fwupd_unix_output_stream_from_fn: (skip): **/ GUnixOutputStream * fwupd_unix_output_stream_from_fn(const gchar *fn, GError **error) { gint fd = g_open(fn, O_RDWR | O_CREAT, S_IRWXU); if (fd < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to open %s", fn); return NULL; } return G_UNIX_OUTPUT_STREAM(g_unix_output_stream_new(fd, TRUE)); } #endif fwupd-2.0.10/libfwupd/fwupd-common.h000066400000000000000000000047601501337203100173210ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fwupd-build.h" G_BEGIN_DECLS /** * FWUPD_DBUS_PATH: * * The dbus path **/ #define FWUPD_DBUS_PATH "/" /** * FWUPD_DBUS_SERVICE: * * The dbus service **/ #define FWUPD_DBUS_SERVICE "org.freedesktop.fwupd" /** * FWUPD_DBUS_INTERFACE: * * The dbus interface **/ #define FWUPD_DBUS_INTERFACE "org.freedesktop.fwupd" /** * FWUPD_DEVICE_ID_ANY: * * Wildcard used for matching all device ids in fwupd **/ #define FWUPD_DEVICE_ID_ANY "*" /** * FwupdGuidFlags: * * The flags to show how the data should be converted. **/ typedef enum { /** * FWUPD_GUID_FLAG_NONE: * * No endian swapping. * * Since: 1.2.5 */ FWUPD_GUID_FLAG_NONE = 0, /** * FWUPD_GUID_FLAG_NAMESPACE_MICROSOFT: * * Use the Microsoft-compatible namespace. * * Since: 1.2.5 */ FWUPD_GUID_FLAG_NAMESPACE_MICROSOFT = 1 << 0, /** * FWUPD_GUID_FLAG_MIXED_ENDIAN: * * Use EFI mixed endian representation, as used in EFI. * * Since: 1.2.5 */ FWUPD_GUID_FLAG_MIXED_ENDIAN = 1 << 1, /*< private >*/ FWUPD_GUID_FLAG_LAST } FwupdGuidFlags; /* GObject Introspection does not understand typedefs with sizes */ #ifdef __GI_SCANNER__ typedef guint8 *fwupd_guid_t; #else typedef guint8 fwupd_guid_t[16]; #endif const gchar * fwupd_checksum_get_best(GPtrArray *checksums) G_GNUC_NON_NULL(1); const gchar * fwupd_checksum_get_by_kind(GPtrArray *checksums, GChecksumType kind) G_GNUC_NON_NULL(1); GChecksumType fwupd_checksum_guess_kind(const gchar *checksum) G_GNUC_NON_NULL(1); const gchar * fwupd_checksum_type_to_string_display(GChecksumType checksum_type); gchar * fwupd_checksum_format_for_display(const gchar *checksum) G_GNUC_NON_NULL(1); gboolean fwupd_device_id_is_valid(const gchar *device_id); #ifndef __GI_SCANNER__ gchar * fwupd_guid_to_string(const fwupd_guid_t *guid, FwupdGuidFlags flags) G_GNUC_NON_NULL(1); gboolean fwupd_guid_from_string(const gchar *guidstr, fwupd_guid_t *guid, FwupdGuidFlags flags, GError **error) G_GNUC_NON_NULL(1); #else gchar * fwupd_guid_to_string(const guint8 guid[16], FwupdGuidFlags flags); gboolean fwupd_guid_from_string(const gchar *guidstr, guint8 guid[16], FwupdGuidFlags flags, GError **error); #endif gboolean fwupd_guid_is_valid(const gchar *guid); gchar * fwupd_guid_hash_string(const gchar *str); gchar * fwupd_guid_hash_data(const guint8 *data, gsize datasz, FwupdGuidFlags flags) G_GNUC_NON_NULL(1); G_END_DECLS fwupd-2.0.10/libfwupd/fwupd-context-test.c000066400000000000000000000063341501337203100204640ustar00rootroot00000000000000/* * Copyright 2020 Philip Withnall * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include typedef struct { GApplication *app; FwupdClient *client; GThread *main_thread; GThread *worker_thread; } FuThreadTestSelf; static gboolean fwupd_thread_test_exit_idle_cb(gpointer user_data) { FuThreadTestSelf *self = user_data; g_application_release(self->app); return G_SOURCE_REMOVE; } static gpointer fwupd_thread_test_thread_cb(gpointer user_data) { FuThreadTestSelf *self = user_data; g_autoptr(GError) error_local = NULL; g_autoptr(GMainContext) context = g_main_context_new(); g_autoptr(GMainContextPusher) pusher = g_main_context_pusher_new(context); g_assert_nonnull(pusher); g_message("Calling fwupd_client_get_devices() in thread %p with main context %p", g_thread_self(), g_main_context_get_thread_default()); if (!fwupd_client_connect(self->client, NULL, &error_local)) g_warning("%s", error_local->message); g_idle_add(fwupd_thread_test_exit_idle_cb, self); return NULL; } static gboolean fwupd_thread_test_idle_cb(gpointer user_data) { FuThreadTestSelf *self = user_data; g_message("fwupd_thread_test_idle_cb() in thread %p with main context %p", g_thread_self(), g_main_context_get_thread_default()); self->worker_thread = g_thread_new("worker00", fwupd_thread_test_thread_cb, self); return G_SOURCE_REMOVE; } static void fwupd_thread_test_activate_cb(GApplication *app, gpointer user_data) { FuThreadTestSelf *self = user_data; g_application_hold(self->app); g_idle_add(fwupd_thread_test_idle_cb, self); } static void fwupd_thread_test_notify_cb(GObject *object, GParamSpec *pspec, gpointer user_data) { FuThreadTestSelf *self = user_data; g_message("fwupd_thread_test_notify_cb() in thread %p with main context %p", g_thread_self(), g_main_context_get_thread_default()); g_assert_true(g_thread_self() == self->main_thread); g_assert_null(g_main_context_get_thread_default()); } static gboolean fwupd_thread_test_has_system_bus(void) { g_autoptr(GDBusConnection) conn = NULL; conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, NULL); return conn != NULL; } int main(void) { gint retval; g_autoptr(FwupdClient) client = fwupd_client_new(); g_autoptr(GApplication) app = g_application_new("org.fwupd.ContextTest", G_APPLICATION_NON_UNIQUE); g_autoptr(GThread) worker_thread = NULL; FuThreadTestSelf self = { .app = app, .client = client, .worker_thread = worker_thread, .main_thread = g_thread_self(), }; /* only some of the CI targets have a DBus daemon */ if (!fwupd_thread_test_has_system_bus()) { g_message("D-Bus system bus unavailable, skipping tests."); return 0; } g_message("Created FwupdClient in thread %p with main context %p", g_thread_self(), g_main_context_get_thread_default()); g_signal_connect(FWUPD_CLIENT(client), "notify::status", G_CALLBACK(fwupd_thread_test_notify_cb), &self); g_signal_connect(G_APPLICATION(app), "activate", G_CALLBACK(fwupd_thread_test_activate_cb), &self); retval = g_application_run(app, 0, NULL); if (self.worker_thread != NULL) g_thread_join(g_steal_pointer(&self.worker_thread)); return retval; } fwupd-2.0.10/libfwupd/fwupd-device-private.h000066400000000000000000000005741501337203100207370ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fwupd-device.h" G_BEGIN_DECLS void fwupd_device_incorporate(FwupdDevice *self, FwupdDevice *donor) G_GNUC_NON_NULL(1, 2); void fwupd_device_remove_children(FwupdDevice *self) G_GNUC_NON_NULL(1); G_END_DECLS fwupd-2.0.10/libfwupd/fwupd-device.c000066400000000000000000003371031501337203100172630ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fwupd-codec.h" #include "fwupd-common-private.h" #include "fwupd-device-private.h" #include "fwupd-enums-private.h" #include "fwupd-error.h" /** * FwupdDevice: * * A physical device on the host with optionally updatable firmware. * * See also: [class@FwupdRelease] */ static void fwupd_device_finalize(GObject *object); typedef struct { gchar *id; gchar *parent_id; gchar *composite_id; guint64 created; guint64 modified; guint64 flags; guint64 request_flags; guint64 problems; GPtrArray *guids; /* (nullable) (element-type utf-8) */ GPtrArray *vendor_ids; /* (nullable) (element-type utf-8) */ GPtrArray *protocols; /* (nullable) (element-type utf-8) */ GPtrArray *instance_ids; /* (nullable) (element-type utf-8) */ GPtrArray *icons; /* (nullable) (element-type utf-8) */ GPtrArray *issues; /* (nullable) (element-type utf-8) */ gchar *name; gchar *serial; gchar *summary; gchar *branch; gchar *vendor; gchar *homepage; gchar *plugin; gchar *version; gchar *version_lowest; gchar *version_bootloader; FwupdVersionFormat version_format; guint64 version_raw; guint64 version_build_date; guint64 version_lowest_raw; guint64 version_bootloader_raw; GPtrArray *checksums; /* (nullable) (element-type utf-8) */ GPtrArray *children; /* (nullable) (element-type FuDevice) */ guint32 flashes_left; guint32 battery_level; guint32 battery_threshold; guint32 install_duration; FwupdUpdateState update_state; gchar *update_error; FwupdStatus status; guint percentage; GPtrArray *releases; /* (nullable) (element-type FwupdRelease) */ FwupdDevice *parent; /* noref */ } FwupdDevicePrivate; enum { PROP_0, PROP_ID, PROP_VERSION, PROP_VERSION_FORMAT, PROP_FLAGS, PROP_REQUEST_FLAGS, PROP_STATUS, PROP_PERCENTAGE, PROP_PARENT, PROP_UPDATE_STATE, PROP_UPDATE_ERROR, PROP_BATTERY_LEVEL, PROP_BATTERY_THRESHOLD, PROP_PROBLEMS, PROP_LAST }; static void fwupd_device_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_EXTENDED(FwupdDevice, fwupd_device, G_TYPE_OBJECT, 0, G_ADD_PRIVATE(FwupdDevice) G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fwupd_device_codec_iface_init)); #define GET_PRIVATE(o) (fwupd_device_get_instance_private(o)) #define FWUPD_BATTERY_THRESHOLD_DEFAULT 10 /* % */ static void fwupd_device_ensure_checksums(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); if (priv->checksums == NULL) priv->checksums = g_ptr_array_new_with_free_func(g_free); } /** * fwupd_device_get_checksums: * @self: a #FwupdDevice * * Gets the device checksums. * * Returns: (element-type utf8) (transfer none): the checksums, which may be empty * * Since: 0.9.3 **/ GPtrArray * fwupd_device_get_checksums(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); fwupd_device_ensure_checksums(self); return priv->checksums; } /** * fwupd_device_has_checksum: * @self: a #FwupdDevice * @checksum: (not nullable): the device checksum * * Finds out if the device has this specific checksum. * * Returns: %TRUE if the checksum name is found * * Since: 1.8.7 **/ gboolean fwupd_device_has_checksum(FwupdDevice *self, const gchar *checksum) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), FALSE); g_return_val_if_fail(checksum != NULL, FALSE); if (priv->checksums == NULL) return FALSE; for (guint i = 0; i < priv->checksums->len; i++) { const gchar *checksum_tmp = g_ptr_array_index(priv->checksums, i); if (g_strcmp0(checksum, checksum_tmp) == 0) return TRUE; } return FALSE; } /** * fwupd_device_add_checksum: * @self: a #FwupdDevice * @checksum: (not nullable): the device checksum * * Adds a device checksum. * * Since: 0.9.3 **/ void fwupd_device_add_checksum(FwupdDevice *self, const gchar *checksum) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); g_return_if_fail(checksum != NULL); if (fwupd_device_has_checksum(self, checksum)) return; fwupd_device_ensure_checksums(self); g_ptr_array_add(priv->checksums, g_strdup(checksum)); } static void fwupd_device_ensure_issues(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); if (priv->issues == NULL) priv->issues = g_ptr_array_new_with_free_func(g_free); } /** * fwupd_device_get_issues: * @self: a #FwupdDevice * * Gets the list of issues currently affecting this device. * * Returns: (element-type utf8) (transfer none): the issues, which may be empty * * Since: 1.7.6 **/ GPtrArray * fwupd_device_get_issues(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); fwupd_device_ensure_issues(self); return priv->issues; } /** * fwupd_device_add_issue: * @self: a #FwupdDevice * @issue: (not nullable): the update issue, e.g. `CVE-2019-12345` * * Adds an current issue to this device. * * Since: 1.7.6 **/ void fwupd_device_add_issue(FwupdDevice *self, const gchar *issue) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); g_return_if_fail(issue != NULL); fwupd_device_ensure_issues(self); for (guint i = 0; i < priv->issues->len; i++) { const gchar *issue_tmp = g_ptr_array_index(priv->issues, i); if (g_strcmp0(issue_tmp, issue) == 0) return; } g_ptr_array_add(priv->issues, g_strdup(issue)); } static void fwupd_device_ensure_children(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); if (priv->children == NULL) priv->children = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); } /** * fwupd_device_get_children: * @self: a #FwupdDevice * * Gets the device children. These can only be assigned using fwupd_device_set_parent(). * * Returns: (element-type FwupdDevice) (transfer none): the children, which may be empty * * Since: 1.3.7 **/ GPtrArray * fwupd_device_get_children(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); fwupd_device_ensure_children(self); return priv->children; } /** * fwupd_device_get_summary: * @self: a #FwupdDevice * * Gets the device summary. * * Returns: the device summary, or %NULL if unset * * Since: 0.9.3 **/ const gchar * fwupd_device_get_summary(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); return priv->summary; } /** * fwupd_device_set_summary: * @self: a #FwupdDevice * @summary: (nullable): the device one line summary * * Sets the device summary. * * Since: 0.9.3 **/ void fwupd_device_set_summary(FwupdDevice *self, const gchar *summary) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); /* not changed */ if (g_strcmp0(priv->summary, summary) == 0) return; g_free(priv->summary); priv->summary = g_strdup(summary); } /** * fwupd_device_get_branch: * @self: a #FwupdDevice * * Gets the current device branch. * * Returns: the device branch, or %NULL if unset * * Since: 1.5.0 **/ const gchar * fwupd_device_get_branch(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); return priv->branch; } /** * fwupd_device_set_branch: * @self: a #FwupdDevice * @branch: (nullable): the device one line branch * * Sets the current device branch. * * Since: 1.5.0 **/ void fwupd_device_set_branch(FwupdDevice *self, const gchar *branch) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); /* not changed */ if (g_strcmp0(priv->branch, branch) == 0) return; g_free(priv->branch); priv->branch = g_strdup(branch); } /** * fwupd_device_get_serial: * @self: a #FwupdDevice * * Gets the serial number for the device. * * Returns: a string value, or %NULL if never set. * * Since: 1.1.2 **/ const gchar * fwupd_device_get_serial(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); return priv->serial; } /** * fwupd_device_set_serial: * @self: a #FwupdDevice * @serial: (nullable): the device serial number * * Sets the serial number for the device. * * Since: 1.1.2 **/ void fwupd_device_set_serial(FwupdDevice *self, const gchar *serial) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); /* not changed */ if (g_strcmp0(priv->serial, serial) == 0) return; g_free(priv->serial); priv->serial = g_strdup(serial); } /** * fwupd_device_get_id: * @self: a #FwupdDevice * * Gets the ID. * * Returns: the ID, or %NULL if unset * * Since: 0.9.3 **/ const gchar * fwupd_device_get_id(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); return priv->id; } /** * fwupd_device_set_id: * @self: a #FwupdDevice * @id: (nullable): the device ID, usually a SHA1 hash * * Sets the ID. * * Since: 0.9.3 **/ void fwupd_device_set_id(FwupdDevice *self, const gchar *id) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); /* not changed */ if (g_strcmp0(priv->id, id) == 0) return; /* sanity check */ if (!fwupd_device_id_is_valid(id)) { g_critical("%s is not a valid device ID", id); return; } g_free(priv->id); priv->id = g_strdup(id); g_object_notify(G_OBJECT(self), "id"); } /** * fwupd_device_get_parent_id: * @self: a #FwupdDevice * * Gets the parent ID. * * Returns: the parent ID, or %NULL if unset * * Since: 1.0.8 **/ const gchar * fwupd_device_get_parent_id(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); return priv->parent_id; } /** * fwupd_device_set_parent_id: * @self: a #FwupdDevice * @parent_id: (nullable): the device ID, usually a SHA1 hash * * Sets the parent ID. * * Since: 1.0.8 **/ void fwupd_device_set_parent_id(FwupdDevice *self, const gchar *parent_id) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); /* not changed */ if (g_strcmp0(priv->parent_id, parent_id) == 0) return; /* sanity check */ if (parent_id != NULL && !fwupd_device_id_is_valid(parent_id)) { g_critical("%s is not a valid device ID", parent_id); return; } g_free(priv->parent_id); priv->parent_id = g_strdup(parent_id); } /** * fwupd_device_get_composite_id: * @self: a #FwupdDevice * * Gets the composite ID, falling back to the device ID if unset. * * The composite ID will be the same value for all parent, child and sibling * devices. * * Returns: (nullable): the composite ID * * Since: 1.6.0 **/ const gchar * fwupd_device_get_composite_id(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); if (priv->composite_id != NULL) return priv->composite_id; return priv->id; } /** * fwupd_device_set_composite_id: * @self: a #FwupdDevice * @composite_id: (nullable): a device ID * * Sets the composite ID, which is usually a SHA1 hash of a grandparent or * parent device. * * Since: 1.6.0 **/ void fwupd_device_set_composite_id(FwupdDevice *self, const gchar *composite_id) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); /* not changed */ if (g_strcmp0(priv->composite_id, composite_id) == 0) return; /* sanity check */ if (!fwupd_device_id_is_valid(composite_id)) { g_critical("%s is not a valid device ID", composite_id); return; } g_free(priv->composite_id); priv->composite_id = g_strdup(composite_id); } /** * fwupd_device_get_parent: * @self: a #FwupdDevice * * Gets the parent. * * Returns: (transfer none): the parent device, or %NULL if unset * * Since: 1.0.8 **/ FwupdDevice * fwupd_device_get_parent(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); return priv->parent; } /** * fwupd_device_get_root: * @self: a #FwupdDevice * * Gets the device root. * * Returns: (transfer none): the root device, or %NULL if unset * * Since: 1.7.4 **/ FwupdDevice * fwupd_device_get_root(FwupdDevice *self) { g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); while (1) { FwupdDevicePrivate *priv = GET_PRIVATE(self); if (priv->parent == NULL) break; self = priv->parent; } return self; } /** * fwupd_device_set_parent: * @self: a #FwupdDevice * @parent: (nullable): another #FwupdDevice * * Sets the parent. Only used internally. * * Since: 1.0.8 **/ void fwupd_device_set_parent(FwupdDevice *self, FwupdDevice *parent) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); g_return_if_fail(self != parent); if (priv->parent != NULL) g_object_remove_weak_pointer(G_OBJECT(priv->parent), (gpointer *)&priv->parent); if (parent != NULL) g_object_add_weak_pointer(G_OBJECT(parent), (gpointer *)&priv->parent); priv->parent = parent; /* this is what goes over D-Bus */ fwupd_device_set_parent_id(self, parent != NULL ? fwupd_device_get_id(parent) : NULL); } static void fwupd_device_child_finalized_cb(gpointer data, GObject *where_the_object_was) { FwupdDevice *self = FWUPD_DEVICE(data); g_critical("FuDevice child %p was finalized while still having parent %s [%s]!", where_the_object_was, fwupd_device_get_name(self), fwupd_device_get_id(self)); } /** * fwupd_device_add_child: * @self: a #FwupdDevice * @child: (not nullable): Another #FwupdDevice * * Adds a child device. An child device is logically linked to the primary * device in some way. * * NOTE: You should never call this function from user code, it is for daemon * use only. Only use fwupd_device_set_parent() to set up a logical tree. * * Since: 1.5.1 **/ void fwupd_device_add_child(FwupdDevice *self, FwupdDevice *child) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); g_return_if_fail(FWUPD_IS_DEVICE(child)); g_return_if_fail(self != child); /* add if the child does not already exist */ fwupd_device_ensure_children(self); for (guint i = 0; i < priv->children->len; i++) { FwupdDevice *devtmp = g_ptr_array_index(priv->children, i); if (devtmp == child) return; } g_object_weak_ref(G_OBJECT(child), fwupd_device_child_finalized_cb, self); g_ptr_array_add(priv->children, g_object_ref(child)); } /** * fwupd_device_remove_child: * @self: a #FwupdDevice * @child: Another #FwupdDevice * * Removes a child device. * * NOTE: You should never call this function from user code, it is for daemon * use only. * * Since: 1.6.2 **/ void fwupd_device_remove_child(FwupdDevice *self, FwupdDevice *child) { FwupdDevicePrivate *priv = GET_PRIVATE(self); /* remove if the child exists */ if (priv->children == NULL) return; for (guint i = 0; i < priv->children->len; i++) { FwupdDevice *child_tmp = g_ptr_array_index(priv->children, i); if (child_tmp == child) { g_object_weak_unref(G_OBJECT(child), fwupd_device_child_finalized_cb, self); g_ptr_array_remove_index(priv->children, i); return; } } } /** * fwupd_device_remove_children: * @self: a #FwupdDevice * * Removes all child devices. * * NOTE: You should never call this function from user code, it is for daemon * use only. * * Since: 2.0.0 **/ void fwupd_device_remove_children(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); if (priv->children == NULL) return; for (guint i = 0; i < priv->children->len; i++) { FwupdDevice *child = g_ptr_array_index(priv->children, i); g_object_weak_unref(G_OBJECT(child), fwupd_device_child_finalized_cb, self); } g_ptr_array_set_size(priv->children, 0); } static void fwupd_device_ensure_guids(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); if (priv->guids == NULL) priv->guids = g_ptr_array_new_with_free_func(g_free); } /** * fwupd_device_get_guids: * @self: a #FwupdDevice * * Gets the GUIDs. * * Returns: (element-type utf8) (transfer none): the GUIDs * * Since: 0.9.3 **/ GPtrArray * fwupd_device_get_guids(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); fwupd_device_ensure_guids(self); return priv->guids; } /** * fwupd_device_has_guid: * @self: a #FwupdDevice * @guid: (not nullable): the GUID, e.g. `2082b5e0-7a64-478a-b1b2-e3404fab6dad` * * Finds out if the device has this specific GUID. * * Returns: %TRUE if the GUID is found * * Since: 0.9.3 **/ gboolean fwupd_device_has_guid(FwupdDevice *self, const gchar *guid) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), FALSE); g_return_val_if_fail(guid != NULL, FALSE); if (priv->guids == NULL) return FALSE; for (guint i = 0; i < priv->guids->len; i++) { const gchar *guid_tmp = g_ptr_array_index(priv->guids, i); if (g_strcmp0(guid, guid_tmp) == 0) return TRUE; } return FALSE; } /** * fwupd_device_add_guid: * @self: a #FwupdDevice * @guid: the GUID, e.g. `2082b5e0-7a64-478a-b1b2-e3404fab6dad` * * Adds the GUID if it does not already exist. * * Since: 0.9.3 **/ void fwupd_device_add_guid(FwupdDevice *self, const gchar *guid) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); g_return_if_fail(guid != NULL); if (fwupd_device_has_guid(self, guid)) return; fwupd_device_ensure_guids(self); g_ptr_array_add(priv->guids, g_strdup(guid)); } /** * fwupd_device_get_guid_default: * @self: a #FwupdDevice * * Gets the default GUID. * * Returns: the GUID, or %NULL if unset * * Since: 0.9.3 **/ const gchar * fwupd_device_get_guid_default(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); if (priv->guids == NULL || priv->guids->len == 0) return NULL; return g_ptr_array_index(priv->guids, 0); } static void fwupd_device_ensure_instance_ids(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); if (priv->instance_ids == NULL) priv->instance_ids = g_ptr_array_new_with_free_func(g_free); } /** * fwupd_device_get_instance_ids: * @self: a #FwupdDevice * * Gets the instance IDs. * * Returns: (element-type utf8) (transfer none): the instance IDs * * Since: 1.2.5 **/ GPtrArray * fwupd_device_get_instance_ids(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); fwupd_device_ensure_instance_ids(self); return priv->instance_ids; } /** * fwupd_device_has_instance_id: * @self: a #FwupdDevice * @instance_id: (not nullable): the instance ID, e.g. `PCI\VEN_10EC&DEV_525A` * * Finds out if the device has this specific instance ID. * * Returns: %TRUE if the instance ID is found * * Since: 1.2.5 **/ gboolean fwupd_device_has_instance_id(FwupdDevice *self, const gchar *instance_id) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), FALSE); g_return_val_if_fail(instance_id != NULL, FALSE); if (priv->instance_ids == NULL) return FALSE; for (guint i = 0; i < priv->instance_ids->len; i++) { const gchar *instance_id_tmp = g_ptr_array_index(priv->instance_ids, i); if (g_strcmp0(instance_id, instance_id_tmp) == 0) return TRUE; } return FALSE; } /** * fwupd_device_add_instance_id: * @self: a #FwupdDevice * @instance_id: (not nullable): the instance ID, e.g. `PCI\VEN_10EC&DEV_525A` * * Adds the instance ID if it does not already exist. * * Since: 1.2.5 **/ void fwupd_device_add_instance_id(FwupdDevice *self, const gchar *instance_id) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); g_return_if_fail(instance_id != NULL); if (fwupd_device_has_instance_id(self, instance_id)) return; fwupd_device_ensure_instance_ids(self); g_ptr_array_add(priv->instance_ids, g_strdup(instance_id)); } static void fwupd_device_ensure_icons(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); if (priv->icons == NULL) priv->icons = g_ptr_array_new_with_free_func(g_free); } /** * fwupd_device_get_icons: * @self: a #FwupdDevice * * Gets the icon names to use for the device. * * NOTE: Icons specified without a full path are stock icons and should * be loaded from the users icon theme. * * Returns: (element-type utf8) (transfer none): an array of icon names * * Since: 0.9.8 **/ GPtrArray * fwupd_device_get_icons(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); fwupd_device_ensure_icons(self); return priv->icons; } /** * fwupd_device_has_icon: * @self: a #FwupdDevice * @icon: the icon name, e.g. `input-mouse` or `/usr/share/icons/foo.png` * * Finds out if the device has this specific icon. * * Returns: %TRUE if the icon name is found * * Since: 1.6.2 **/ gboolean fwupd_device_has_icon(FwupdDevice *self, const gchar *icon) { FwupdDevicePrivate *priv = GET_PRIVATE(self); if (priv->icons == NULL) return FALSE; for (guint i = 0; i < priv->icons->len; i++) { const gchar *icon_tmp = g_ptr_array_index(priv->icons, i); if (g_strcmp0(icon, icon_tmp) == 0) return TRUE; } return FALSE; } /** * fwupd_device_add_icon: * @self: a #FwupdDevice * @icon: (not nullable): the icon name, e.g. `input-mouse` or `/usr/share/icons/foo.png` * * Adds the icon name if it does not already exist. * * Since: 0.9.8 **/ void fwupd_device_add_icon(FwupdDevice *self, const gchar *icon) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); g_return_if_fail(icon != NULL); if (fwupd_device_has_icon(self, icon)) return; fwupd_device_ensure_icons(self); g_ptr_array_add(priv->icons, g_strdup(icon)); } /** * fwupd_device_get_name: * @self: a #FwupdDevice * * Gets the device name. * * Returns: the device name, or %NULL if unset * * Since: 0.9.3 **/ const gchar * fwupd_device_get_name(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); return priv->name; } /** * fwupd_device_set_name: * @self: a #FwupdDevice * @name: (nullable): the device name, e.g. `ColorHug2` * * Sets the device name. * * Since: 0.9.3 **/ void fwupd_device_set_name(FwupdDevice *self, const gchar *name) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); /* not changed */ if (g_strcmp0(priv->name, name) == 0) return; g_free(priv->name); priv->name = g_strdup(name); } /** * fwupd_device_get_vendor: * @self: a #FwupdDevice * * Gets the device vendor. * * Returns: the device vendor, or %NULL if unset * * Since: 0.9.3 **/ const gchar * fwupd_device_get_vendor(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); return priv->vendor; } /** * fwupd_device_set_vendor: * @self: a #FwupdDevice * @vendor: (nullable): the vendor * * Sets the device vendor. * * Since: 0.9.3 **/ void fwupd_device_set_vendor(FwupdDevice *self, const gchar *vendor) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); /* not changed */ if (g_strcmp0(priv->vendor, vendor) == 0) return; g_free(priv->vendor); priv->vendor = g_strdup(vendor); } static void fwupd_device_ensure_vendor_ids(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); if (priv->vendor_ids == NULL) priv->vendor_ids = g_ptr_array_new_with_free_func(g_free); } /** * fwupd_device_get_vendor_ids: * @self: a #FwupdDevice * * Gets the device vendor ID. * * Returns: (element-type utf8) (transfer none): the device vendor ID * * Since: 1.5.5 **/ GPtrArray * fwupd_device_get_vendor_ids(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); fwupd_device_ensure_vendor_ids(self); return priv->vendor_ids; } /** * fwupd_device_has_vendor_id: * @self: a #FwupdDevice * @vendor_id: (not nullable): the vendor ID, e.g. 'USB:0x1234' * * Finds out if the device has this specific vendor ID. * * Returns: %TRUE if the vendor ID is found * * Since: 1.5.5 **/ gboolean fwupd_device_has_vendor_id(FwupdDevice *self, const gchar *vendor_id) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), FALSE); g_return_val_if_fail(vendor_id != NULL, FALSE); if (priv->vendor_ids == NULL) return FALSE; for (guint i = 0; i < priv->vendor_ids->len; i++) { const gchar *vendor_id_tmp = g_ptr_array_index(priv->vendor_ids, i); if (g_strcmp0(vendor_id, vendor_id_tmp) == 0) return TRUE; } return FALSE; } /** * fwupd_device_add_vendor_id: * @self: a #FwupdDevice * @vendor_id: (not nullable): the ID, e.g. 'USB:0x1234' * * Adds a device vendor ID. * * Since: 1.5.5 **/ void fwupd_device_add_vendor_id(FwupdDevice *self, const gchar *vendor_id) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); g_return_if_fail(vendor_id != NULL); if (fwupd_device_has_vendor_id(self, vendor_id)) return; fwupd_device_ensure_vendor_ids(self); g_ptr_array_add(priv->vendor_ids, g_strdup(vendor_id)); } /** * fwupd_device_get_version: * @self: a #FwupdDevice * * Gets the device version. * * Returns: the device version, or %NULL if unset * * Since: 0.9.3 **/ const gchar * fwupd_device_get_version(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); return priv->version; } /** * fwupd_device_set_version: * @self: a #FwupdDevice * @version: (nullable): the device version, e.g. `1.2.3` * * Sets the device version. * * Since: 0.9.3 **/ void fwupd_device_set_version(FwupdDevice *self, const gchar *version) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); /* not changed */ if (g_strcmp0(priv->version, version) == 0) return; g_free(priv->version); priv->version = g_strdup(version); g_object_notify(G_OBJECT(self), "version"); } /** * fwupd_device_get_version_lowest: * @self: a #FwupdDevice * * Gets the lowest version of firmware the device will accept. * * Returns: the device version_lowest, or %NULL if unset * * Since: 0.9.3 **/ const gchar * fwupd_device_get_version_lowest(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); return priv->version_lowest; } /** * fwupd_device_set_version_lowest: * @self: a #FwupdDevice * @version_lowest: (nullable): the version * * Sets the lowest version of firmware the device will accept. * * Since: 0.9.3 **/ void fwupd_device_set_version_lowest(FwupdDevice *self, const gchar *version_lowest) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); /* not changed */ if (g_strcmp0(priv->version_lowest, version_lowest) == 0) return; g_free(priv->version_lowest); priv->version_lowest = g_strdup(version_lowest); } /** * fwupd_device_get_version_lowest_raw: * @self: a #FwupdDevice * * Gets the lowest version of firmware the device will accept in raw format. * * Returns: integer version number, or %0 if unset * * Since: 1.4.0 **/ guint64 fwupd_device_get_version_lowest_raw(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), 0); return priv->version_lowest_raw; } /** * fwupd_device_set_version_lowest_raw: * @self: a #FwupdDevice * @version_lowest_raw: the raw hardware version * * Sets the raw lowest version number from the hardware before converted to a string. * * Since: 1.4.0 **/ void fwupd_device_set_version_lowest_raw(FwupdDevice *self, guint64 version_lowest_raw) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); priv->version_lowest_raw = version_lowest_raw; } /** * fwupd_device_get_version_bootloader: * @self: a #FwupdDevice * * Gets the version of the bootloader. * * Returns: the device version_bootloader, or %NULL if unset * * Since: 0.9.3 **/ const gchar * fwupd_device_get_version_bootloader(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); return priv->version_bootloader; } /** * fwupd_device_set_version_bootloader: * @self: a #FwupdDevice * @version_bootloader: (nullable): the version * * Sets the bootloader version. * * Since: 0.9.3 **/ void fwupd_device_set_version_bootloader(FwupdDevice *self, const gchar *version_bootloader) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); /* not changed */ if (g_strcmp0(priv->version_bootloader, version_bootloader) == 0) return; g_free(priv->version_bootloader); priv->version_bootloader = g_strdup(version_bootloader); } /** * fwupd_device_get_version_bootloader_raw: * @self: a #FwupdDevice * * Gets the bootloader version of firmware the device will accept in raw format. * * Returns: integer version number, or %0 if unset * * Since: 1.4.0 **/ guint64 fwupd_device_get_version_bootloader_raw(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), 0); return priv->version_bootloader_raw; } /** * fwupd_device_set_version_bootloader_raw: * @self: a #FwupdDevice * @version_bootloader_raw: the raw hardware version * * Sets the raw bootloader version number from the hardware before converted to a string. * * Since: 1.4.0 **/ void fwupd_device_set_version_bootloader_raw(FwupdDevice *self, guint64 version_bootloader_raw) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); priv->version_bootloader_raw = version_bootloader_raw; } /** * fwupd_device_get_flashes_left: * @self: a #FwupdDevice * * Gets the number of flash cycles left on the device * * Returns: the flash cycles left, or %NULL if unset * * Since: 0.9.3 **/ guint32 fwupd_device_get_flashes_left(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), 0); return priv->flashes_left; } /** * fwupd_device_set_flashes_left: * @self: a #FwupdDevice * @flashes_left: the description * * Sets the number of flash cycles left on the device * * Since: 0.9.3 **/ void fwupd_device_set_flashes_left(FwupdDevice *self, guint32 flashes_left) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); priv->flashes_left = flashes_left; } /** * fwupd_device_get_battery_level: * @self: a #FwupdDevice * * Returns the battery level. * * Returns: value in percent * * Since: 1.8.1 **/ guint32 fwupd_device_get_battery_level(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), G_MAXUINT); return priv->battery_level; } /** * fwupd_device_set_battery_level: * @self: a #FwupdDevice * @battery_level: the percentage value * * Sets the battery level, or %FWUPD_BATTERY_LEVEL_INVALID. * * Setting this allows fwupd to show a warning if the device change is too low * to perform the update. * * Since: 1.8.1 **/ void fwupd_device_set_battery_level(FwupdDevice *self, guint32 battery_level) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); g_return_if_fail(battery_level <= FWUPD_BATTERY_LEVEL_INVALID); if (priv->battery_level == battery_level) return; priv->battery_level = battery_level; g_object_notify(G_OBJECT(self), "battery-level"); } /** * fwupd_device_get_battery_threshold: * @self: a #FwupdDevice * * Returns the battery threshold under which a firmware update cannot be * performed. * * If fwupd_device_set_battery_threshold() has not been used, a default value is * used instead. * * Returns: value in percent * * Since: 1.8.1 **/ guint32 fwupd_device_get_battery_threshold(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), FWUPD_BATTERY_LEVEL_INVALID); /* default value */ if (priv->battery_threshold == FWUPD_BATTERY_LEVEL_INVALID) return FWUPD_BATTERY_THRESHOLD_DEFAULT; return priv->battery_threshold; } /** * fwupd_device_set_battery_threshold: * @self: a #FwupdDevice * @battery_threshold: the percentage value * * Sets the battery level, or %FWUPD_BATTERY_LEVEL_INVALID for the default. * * Setting this allows fwupd to show a warning if the device change is too low * to perform the update. * * Since: 1.8.1 **/ void fwupd_device_set_battery_threshold(FwupdDevice *self, guint32 battery_threshold) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); g_return_if_fail(battery_threshold <= FWUPD_BATTERY_LEVEL_INVALID); if (priv->battery_threshold == battery_threshold) return; priv->battery_threshold = battery_threshold; g_object_notify(G_OBJECT(self), "battery-threshold"); } /** * fwupd_device_get_install_duration: * @self: a #FwupdDevice * * Gets the time estimate for firmware installation (in seconds) * * Returns: the estimated time to flash this device (or 0 if unset) * * Since: 1.1.3 **/ guint32 fwupd_device_get_install_duration(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), 0); return priv->install_duration; } /** * fwupd_device_set_install_duration: * @self: a #FwupdDevice * @duration: the amount of time * * Sets the time estimate for firmware installation (in seconds) * * Since: 1.1.3 **/ void fwupd_device_set_install_duration(FwupdDevice *self, guint32 duration) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); priv->install_duration = duration; } /** * fwupd_device_get_plugin: * @self: a #FwupdDevice * * Gets the plugin that created the device. * * Returns: the plugin name, or %NULL if unset * * Since: 1.0.0 **/ const gchar * fwupd_device_get_plugin(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); return priv->plugin; } /** * fwupd_device_set_plugin: * @self: a #FwupdDevice * @plugin: (nullable): the plugin name, e.g. `hughski_colorhug` * * Sets the plugin that created the device. * * Since: 1.0.0 **/ void fwupd_device_set_plugin(FwupdDevice *self, const gchar *plugin) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); /* not changed */ if (g_strcmp0(priv->plugin, plugin) == 0) return; g_free(priv->plugin); priv->plugin = g_strdup(plugin); } static void fwupd_device_ensure_protocols(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); if (priv->protocols == NULL) priv->protocols = g_ptr_array_new_with_free_func(g_free); } /** * fwupd_device_get_protocols: * @self: a #FwupdDevice * * Gets the device protocol names. * * Returns: (element-type utf8) (transfer none): the device protocol names * * Since: 1.5.8 **/ GPtrArray * fwupd_device_get_protocols(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); fwupd_device_ensure_protocols(self); return priv->protocols; } /** * fwupd_device_has_protocol: * @self: a #FwupdDevice * @protocol: (not nullable): the protocol name, e.g. `com.hughski.colorhug` * * Finds out if the device has this specific protocol name. * * Returns: %TRUE if the protocol name is found * * Since: 1.5.8 **/ gboolean fwupd_device_has_protocol(FwupdDevice *self, const gchar *protocol) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), FALSE); g_return_val_if_fail(protocol != NULL, FALSE); if (priv->protocols == NULL) return FALSE; for (guint i = 0; i < priv->protocols->len; i++) { const gchar *protocol_tmp = g_ptr_array_index(priv->protocols, i); if (g_strcmp0(protocol, protocol_tmp) == 0) return TRUE; } return FALSE; } /** * fwupd_device_add_protocol: * @self: a #FwupdDevice * @protocol: (not nullable): the protocol name, e.g. `com.hughski.colorhug` * * Adds a device protocol name. * * Since: 1.5.8 **/ void fwupd_device_add_protocol(FwupdDevice *self, const gchar *protocol) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); g_return_if_fail(protocol != NULL); if (fwupd_device_has_protocol(self, protocol)) return; fwupd_device_ensure_protocols(self); g_ptr_array_add(priv->protocols, g_strdup(protocol)); } /** * fwupd_device_get_flags: * @self: a #FwupdDevice * * Gets device flags. * * Returns: device flags, or 0 if unset * * Since: 0.9.3 **/ guint64 fwupd_device_get_flags(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), 0); return priv->flags; } /** * fwupd_device_set_flags: * @self: a #FwupdDevice * @flags: device flags, e.g. %FWUPD_DEVICE_FLAG_REQUIRE_AC * * Sets device flags. * * Since: 0.9.3 **/ void fwupd_device_set_flags(FwupdDevice *self, guint64 flags) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); if (priv->flags == flags) return; priv->flags = flags; g_object_notify(G_OBJECT(self), "flags"); } /** * fwupd_device_add_flag: * @self: a #FwupdDevice * @flag: the #FwupdDeviceFlags * * Adds a specific device flag to the device. * * Since: 0.9.3 **/ void fwupd_device_add_flag(FwupdDevice *self, FwupdDeviceFlags flag) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); if (flag == 0) return; if ((priv->flags | flag) == priv->flags) return; priv->flags |= flag; g_object_notify(G_OBJECT(self), "flags"); } /** * fwupd_device_remove_flag: * @self: a #FwupdDevice * @flag: the #FwupdDeviceFlags * * Removes a specific device flag from the device. * * Since: 0.9.3 **/ void fwupd_device_remove_flag(FwupdDevice *self, FwupdDeviceFlags flag) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); if (flag == 0) return; if ((priv->flags & flag) == 0) return; priv->flags &= ~flag; g_object_notify(G_OBJECT(self), "flags"); } /** * fwupd_device_has_flag: * @self: a #FwupdDevice * @flag: the #FwupdDeviceFlags * * Finds if the device has a specific device flag. * * Returns: %TRUE if the flag is set * * Since: 0.9.3 **/ gboolean fwupd_device_has_flag(FwupdDevice *self, FwupdDeviceFlags flag) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), FALSE); return (priv->flags & flag) > 0; } /** * fwupd_device_get_problems: * @self: a #FwupdDevice * * Gets device problems. * * Returns: device problems, or 0 if unset * * Since: 1.8.1 **/ guint64 fwupd_device_get_problems(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), 0); return priv->problems; } /** * fwupd_device_set_problems: * @self: a #FwupdDevice * @problems: device problems, e.g. %FWUPD_DEVICE_PROBLEM_SYSTEM_POWER_TOO_LOW * * Sets device problems. * * Since: 1.8.1 **/ void fwupd_device_set_problems(FwupdDevice *self, guint64 problems) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); if (priv->problems == problems) return; priv->problems = problems; g_object_notify(G_OBJECT(self), "problems"); } /** * fwupd_device_add_problem: * @self: a #FwupdDevice * @problem: the #FwupdDeviceProblem, e.g. #FWUPD_DEVICE_PROBLEM_SYSTEM_POWER_TOO_LOW * * Adds a specific device problem kind to the device. * * Since: 1.8.1 **/ void fwupd_device_add_problem(FwupdDevice *self, FwupdDeviceProblem problem) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); if (problem == FWUPD_DEVICE_PROBLEM_NONE) return; if (fwupd_device_has_problem(self, problem)) return; priv->problems |= problem; g_object_notify(G_OBJECT(self), "problems"); } /** * fwupd_device_remove_problem: * @self: a #FwupdDevice * @problem: the #FwupdDeviceProblem, e.g. #FWUPD_DEVICE_PROBLEM_SYSTEM_POWER_TOO_LOW * * Removes a specific device problem kind from the device. * * Since: 1.8.1 **/ void fwupd_device_remove_problem(FwupdDevice *self, FwupdDeviceProblem problem) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); if (problem == FWUPD_DEVICE_PROBLEM_NONE) return; if (!fwupd_device_has_problem(self, problem)) return; priv->problems &= ~problem; g_object_notify(G_OBJECT(self), "problems"); } /** * fwupd_device_has_problem: * @self: a #FwupdDevice * @problem: the #FwupdDeviceProblem * * Finds if the device has a specific device problem kind. * * Returns: %TRUE if the problem is set * * Since: 1.8.1 **/ gboolean fwupd_device_has_problem(FwupdDevice *self, FwupdDeviceProblem problem) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), FALSE); return (priv->problems & problem) > 0; } /** * fwupd_device_get_request_flags: * @self: a #FwupdDevice * * Gets device request flags. * * Returns: device request flags, or 0 if unset * * Since: 1.9.10 **/ guint64 fwupd_device_get_request_flags(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), 0); return priv->request_flags; } /** * fwupd_device_set_request_flags: * @self: a #FwupdDevice * @request_flags: device request flags, e.g. %FWUPD_DEVICE_REQUEST_FLAG_REQUIRE_AC * * Sets device request flags. * * Since: 1.9.10 **/ void fwupd_device_set_request_flags(FwupdDevice *self, guint64 request_flags) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); if (priv->request_flags == request_flags) return; priv->request_flags = request_flags; g_object_notify(G_OBJECT(self), "request-flags"); } /** * fwupd_device_add_request_flag: * @self: a #FwupdDevice * @request_flag: the #FwupdRequestFlags * * Adds a specific device request flag to the device. * * Since: 1.9.10 **/ void fwupd_device_add_request_flag(FwupdDevice *self, FwupdRequestFlags request_flag) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); if (request_flag == 0) return; if ((priv->request_flags | request_flag) == priv->request_flags) return; priv->request_flags |= request_flag; g_object_notify(G_OBJECT(self), "request-flags"); } /** * fwupd_device_remove_request_flag: * @self: a #FwupdDevice * @request_flag: the #FwupdRequestFlags * * Removes a specific device request flag from the device. * * Since: 1.9.10 **/ void fwupd_device_remove_request_flag(FwupdDevice *self, FwupdRequestFlags request_flag) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); if (request_flag == 0) return; if ((priv->request_flags & request_flag) == 0) return; priv->request_flags &= ~request_flag; g_object_notify(G_OBJECT(self), "request-flags"); } /** * fwupd_device_has_request_flag: * @self: a #FwupdDevice * @request_flag: the #FwupdRequestFlags * * Finds if the device has a specific device request flag. * * Returns: %TRUE if the request_flag is set * * Since: 1.9.10 **/ gboolean fwupd_device_has_request_flag(FwupdDevice *self, FwupdRequestFlags request_flag) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), FALSE); return (priv->request_flags & request_flag) > 0; } /** * fwupd_device_get_created: * @self: a #FwupdDevice * * Gets when the device was created. * * Returns: the UNIX time, or 0 if unset * * Since: 0.9.3 **/ guint64 fwupd_device_get_created(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), 0); return priv->created; } /** * fwupd_device_set_created: * @self: a #FwupdDevice * @created: the UNIX time * * Sets when the device was created. * * Since: 0.9.3 **/ void fwupd_device_set_created(FwupdDevice *self, guint64 created) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); priv->created = created; } /** * fwupd_device_get_modified: * @self: a #FwupdDevice * * Gets when the device was modified. * * Returns: the UNIX time, or 0 if unset * * Since: 0.9.3 **/ guint64 fwupd_device_get_modified(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), 0); return priv->modified; } /** * fwupd_device_set_modified: * @self: a #FwupdDevice * @modified: the UNIX time * * Sets when the device was modified. * * Since: 0.9.3 **/ void fwupd_device_set_modified(FwupdDevice *self, guint64 modified) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); priv->modified = modified; } /** * fwupd_device_incorporate: * @self: a #FwupdDevice * @donor: Another #FwupdDevice * * Copy all properties from the donor object if they have not already been set. * * Since: 1.1.0 **/ void fwupd_device_incorporate(FwupdDevice *self, FwupdDevice *donor) { FwupdDevicePrivate *priv = GET_PRIVATE(self); FwupdDevicePrivate *priv_donor = GET_PRIVATE(donor); g_return_if_fail(FWUPD_IS_DEVICE(self)); g_return_if_fail(FWUPD_IS_DEVICE(donor)); fwupd_device_add_flag(self, priv_donor->flags); fwupd_device_add_request_flag(self, priv_donor->request_flags); fwupd_device_add_problem(self, priv_donor->problems); if (priv->created == 0) fwupd_device_set_created(self, priv_donor->created); if (priv->modified == 0) fwupd_device_set_modified(self, priv_donor->modified); if (priv->version_build_date == 0) fwupd_device_set_version_build_date(self, priv_donor->version_build_date); if (priv->flashes_left == 0) fwupd_device_set_flashes_left(self, priv_donor->flashes_left); if (priv->battery_level == FWUPD_BATTERY_LEVEL_INVALID) fwupd_device_set_battery_level(self, priv_donor->battery_level); if (priv->battery_threshold == FWUPD_BATTERY_LEVEL_INVALID) fwupd_device_set_battery_threshold(self, priv_donor->battery_threshold); if (priv->install_duration == 0) fwupd_device_set_install_duration(self, priv_donor->install_duration); if (priv->update_state == FWUPD_UPDATE_STATE_UNKNOWN) fwupd_device_set_update_state(self, priv_donor->update_state); if (priv->id == NULL) fwupd_device_set_id(self, priv_donor->id); if (priv->parent_id == NULL) fwupd_device_set_parent_id(self, priv_donor->parent_id); if (priv->composite_id == NULL) fwupd_device_set_composite_id(self, priv_donor->composite_id); if (priv->name == NULL) fwupd_device_set_name(self, priv_donor->name); if (priv->serial == NULL) fwupd_device_set_serial(self, priv_donor->serial); if (priv->summary == NULL) fwupd_device_set_summary(self, priv_donor->summary); if (priv->branch == NULL) fwupd_device_set_branch(self, priv_donor->branch); if (priv->vendor == NULL) fwupd_device_set_vendor(self, priv_donor->vendor); if (priv_donor->vendor_ids != NULL) { for (guint i = 0; i < priv_donor->vendor_ids->len; i++) { const gchar *tmp = g_ptr_array_index(priv_donor->vendor_ids, i); fwupd_device_add_vendor_id(self, tmp); } } if (priv->plugin == NULL) fwupd_device_set_plugin(self, priv_donor->plugin); if (priv_donor->protocols != NULL) { for (guint i = 0; i < priv_donor->protocols->len; i++) { const gchar *tmp = g_ptr_array_index(priv_donor->protocols, i); fwupd_device_add_protocol(self, tmp); } } if (priv->update_error == NULL) fwupd_device_set_update_error(self, priv_donor->update_error); if (priv->version == NULL) fwupd_device_set_version(self, priv_donor->version); if (priv->version_lowest == NULL) fwupd_device_set_version_lowest(self, priv_donor->version_lowest); if (priv->version_bootloader == NULL) fwupd_device_set_version_bootloader(self, priv_donor->version_bootloader); if (priv->version_format == FWUPD_VERSION_FORMAT_UNKNOWN) fwupd_device_set_version_format(self, priv_donor->version_format); if (priv->version_raw == 0) fwupd_device_set_version_raw(self, priv_donor->version_raw); if (priv->version_lowest_raw == 0) fwupd_device_set_version_lowest_raw(self, priv_donor->version_lowest_raw); if (priv->version_bootloader_raw == 0) fwupd_device_set_version_bootloader_raw(self, priv_donor->version_bootloader_raw); if (priv_donor->guids != NULL) { for (guint i = 0; i < priv_donor->guids->len; i++) { const gchar *tmp = g_ptr_array_index(priv_donor->guids, i); fwupd_device_add_guid(self, tmp); } } if (priv_donor->instance_ids != NULL) { for (guint i = 0; i < priv_donor->instance_ids->len; i++) { const gchar *tmp = g_ptr_array_index(priv_donor->instance_ids, i); fwupd_device_add_instance_id(self, tmp); } } if (priv_donor->icons != NULL) { for (guint i = 0; i < priv_donor->icons->len; i++) { const gchar *tmp = g_ptr_array_index(priv_donor->icons, i); fwupd_device_add_icon(self, tmp); } } if (priv_donor->checksums != NULL) { for (guint i = 0; i < priv_donor->checksums->len; i++) { const gchar *tmp = g_ptr_array_index(priv_donor->checksums, i); fwupd_device_add_checksum(self, tmp); } } if (priv_donor->releases != NULL) { for (guint i = 0; i < priv_donor->releases->len; i++) { FwupdRelease *tmp = g_ptr_array_index(priv_donor->releases, i); fwupd_device_add_release(self, tmp); } } } static void fwupd_device_add_variant(FwupdCodec *codec, GVariantBuilder *builder, FwupdCodecFlags flags) { FwupdDevice *self = FWUPD_DEVICE(codec); FwupdDevicePrivate *priv = GET_PRIVATE(self); if (priv->id != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_DEVICE_ID, g_variant_new_string(priv->id)); } if (priv->parent_id != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_PARENT_DEVICE_ID, g_variant_new_string(priv->parent_id)); } if (priv->composite_id != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_COMPOSITE_ID, g_variant_new_string(priv->composite_id)); } if (priv->guids != NULL && priv->guids->len > 0) { const gchar *const *tmp = (const gchar *const *)priv->guids->pdata; g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_GUID, g_variant_new_strv(tmp, priv->guids->len)); } if (priv->icons != NULL && priv->icons->len > 0) { const gchar *const *tmp = (const gchar *const *)priv->icons->pdata; g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_ICON, g_variant_new_strv(tmp, priv->icons->len)); } if (priv->name != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_NAME, g_variant_new_string(priv->name)); } if (priv->vendor != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_VENDOR, g_variant_new_string(priv->vendor)); } if (priv->vendor_ids != NULL && priv->vendor_ids->len > 0) { g_autoptr(GString) str = g_string_new(NULL); for (guint i = 0; i < priv->vendor_ids->len; i++) { const gchar *tmp = g_ptr_array_index(priv->vendor_ids, i); g_string_append_printf(str, "%s|", tmp); } if (str->len > 0) g_string_truncate(str, str->len - 1); g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_VENDOR_ID, g_variant_new_string(str->str)); } if (priv->flags > 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_FLAGS, g_variant_new_uint64(priv->flags)); } if (priv->request_flags > 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_REQUEST_FLAGS, g_variant_new_uint64(priv->request_flags)); } if (priv->problems > 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_PROBLEMS, g_variant_new_uint64(priv->problems)); } if (priv->created > 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_CREATED, g_variant_new_uint64(priv->created)); } if (priv->modified > 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_MODIFIED, g_variant_new_uint64(priv->modified)); } if (priv->version_build_date > 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_VERSION_BUILD_DATE, g_variant_new_uint64(priv->version_build_date)); } if (priv->summary != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_SUMMARY, g_variant_new_string(priv->summary)); } if (priv->branch != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_BRANCH, g_variant_new_string(priv->branch)); } if (priv->checksums != NULL && priv->checksums->len > 0) { g_autoptr(GString) str = g_string_new(""); for (guint i = 0; i < priv->checksums->len; i++) { const gchar *checksum = g_ptr_array_index(priv->checksums, i); g_string_append_printf(str, "%s,", checksum); } if (str->len > 0) g_string_truncate(str, str->len - 1); g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_CHECKSUM, g_variant_new_string(str->str)); } if (priv->plugin != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_PLUGIN, g_variant_new_string(priv->plugin)); } if (priv->protocols != NULL && priv->protocols->len > 0) { g_autoptr(GString) str = g_string_new(NULL); for (guint i = 0; i < priv->protocols->len; i++) { const gchar *tmp = g_ptr_array_index(priv->protocols, i); g_string_append_printf(str, "%s|", tmp); } if (str->len > 0) g_string_truncate(str, str->len - 1); g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_PROTOCOL, g_variant_new_string(str->str)); } if (priv->issues != NULL && priv->issues->len > 0) { g_autofree const gchar **strv = g_new0(const gchar *, priv->issues->len + 1); for (guint i = 0; i < priv->issues->len; i++) strv[i] = (const gchar *)g_ptr_array_index(priv->issues, i); g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_ISSUES, g_variant_new_strv(strv, -1)); } if (priv->version != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_VERSION, g_variant_new_string(priv->version)); } if (priv->version_lowest != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_VERSION_LOWEST, g_variant_new_string(priv->version_lowest)); } if (priv->version_bootloader != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_VERSION_BOOTLOADER, g_variant_new_string(priv->version_bootloader)); } if (priv->version_raw > 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_VERSION_RAW, g_variant_new_uint64(priv->version_raw)); } if (priv->version_lowest_raw > 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_VERSION_LOWEST_RAW, g_variant_new_uint64(priv->version_lowest_raw)); } if (priv->version_bootloader_raw > 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_VERSION_BOOTLOADER_RAW, g_variant_new_uint64(priv->version_raw)); } if (priv->flashes_left > 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_FLASHES_LEFT, g_variant_new_uint32(priv->flashes_left)); } if (priv->battery_level != FWUPD_BATTERY_LEVEL_INVALID) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_BATTERY_LEVEL, g_variant_new_uint32(priv->battery_level)); } if (priv->battery_threshold != FWUPD_BATTERY_LEVEL_INVALID) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_BATTERY_THRESHOLD, g_variant_new_uint32(priv->battery_threshold)); } if (priv->install_duration > 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_INSTALL_DURATION, g_variant_new_uint32(priv->install_duration)); } if (priv->update_error != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_UPDATE_ERROR, g_variant_new_string(priv->update_error)); } if (priv->update_state != FWUPD_UPDATE_STATE_UNKNOWN) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_UPDATE_STATE, g_variant_new_uint32(priv->update_state)); } if (priv->status != FWUPD_STATUS_UNKNOWN) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_STATUS, g_variant_new_uint32(priv->status)); } if (priv->percentage != 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_PERCENTAGE, g_variant_new_uint32(priv->percentage)); } if (priv->version_format != FWUPD_VERSION_FORMAT_UNKNOWN) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_VERSION_FORMAT, g_variant_new_uint32(priv->version_format)); } if (priv->instance_ids != NULL && (flags & FWUPD_CODEC_FLAG_TRUSTED) > 0) { if (priv->serial != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_SERIAL, g_variant_new_string(priv->serial)); } if (priv->instance_ids->len > 0) { const gchar *const *tmp = (const gchar *const *)priv->instance_ids->pdata; g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_INSTANCE_IDS, g_variant_new_strv(tmp, priv->instance_ids->len)); } } /* create an array with all the metadata in */ if (priv->releases != NULL && priv->releases->len > 0) { g_autofree GVariant **children = NULL; children = g_new0(GVariant *, priv->releases->len); for (guint i = 0; i < priv->releases->len; i++) { FwupdRelease *release = g_ptr_array_index(priv->releases, i); children[i] = fwupd_codec_to_variant(FWUPD_CODEC(release), FWUPD_CODEC_FLAG_NONE); } g_variant_builder_add( builder, "{sv}", FWUPD_RESULT_KEY_RELEASE, g_variant_new_array(G_VARIANT_TYPE("a{sv}"), children, priv->releases->len)); } } static void fwupd_device_from_key_value(FwupdDevice *self, const gchar *key, GVariant *value) { if (g_strcmp0(key, FWUPD_RESULT_KEY_RELEASE) == 0) { GVariantIter iter; GVariant *child; g_variant_iter_init(&iter, value); while ((child = g_variant_iter_next_value(&iter))) { g_autoptr(FwupdRelease) release = fwupd_release_new(); if (fwupd_codec_from_variant(FWUPD_CODEC(release), child, NULL)) fwupd_device_add_release(self, release); g_variant_unref(child); } return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_DEVICE_ID) == 0) { fwupd_device_set_id(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_PARENT_DEVICE_ID) == 0) { fwupd_device_set_parent_id(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_COMPOSITE_ID) == 0) { fwupd_device_set_composite_id(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_FLAGS) == 0) { fwupd_device_set_flags(self, g_variant_get_uint64(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_PROBLEMS) == 0) { fwupd_device_set_problems(self, g_variant_get_uint64(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_REQUEST_FLAGS) == 0) { fwupd_device_set_request_flags(self, g_variant_get_uint64(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_CREATED) == 0) { fwupd_device_set_created(self, g_variant_get_uint64(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_MODIFIED) == 0) { fwupd_device_set_modified(self, g_variant_get_uint64(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_VERSION_BUILD_DATE) == 0) { fwupd_device_set_version_build_date(self, g_variant_get_uint64(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_GUID) == 0) { g_autofree const gchar **guids = g_variant_get_strv(value, NULL); for (guint i = 0; guids != NULL && guids[i] != NULL; i++) fwupd_device_add_guid(self, guids[i]); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_INSTANCE_IDS) == 0) { g_autofree const gchar **instance_ids = g_variant_get_strv(value, NULL); for (guint i = 0; instance_ids != NULL && instance_ids[i] != NULL; i++) fwupd_device_add_instance_id(self, instance_ids[i]); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_ICON) == 0) { g_autofree const gchar **icons = g_variant_get_strv(value, NULL); for (guint i = 0; icons != NULL && icons[i] != NULL; i++) fwupd_device_add_icon(self, icons[i]); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_NAME) == 0) { fwupd_device_set_name(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_VENDOR) == 0) { fwupd_device_set_vendor(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_VENDOR_ID) == 0) { g_auto(GStrv) vendor_ids = NULL; vendor_ids = g_strsplit(g_variant_get_string(value, NULL), "|", -1); for (guint i = 0; vendor_ids[i] != NULL; i++) fwupd_device_add_vendor_id(self, vendor_ids[i]); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_SERIAL) == 0) { fwupd_device_set_serial(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_SUMMARY) == 0) { fwupd_device_set_summary(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_BRANCH) == 0) { fwupd_device_set_branch(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_CHECKSUM) == 0) { const gchar *checksums = g_variant_get_string(value, NULL); if (checksums != NULL) { g_auto(GStrv) split = g_strsplit(checksums, ",", -1); for (guint i = 0; split[i] != NULL; i++) fwupd_device_add_checksum(self, split[i]); } return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_PLUGIN) == 0) { fwupd_device_set_plugin(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_PROTOCOL) == 0) { g_auto(GStrv) protocols = NULL; protocols = g_strsplit(g_variant_get_string(value, NULL), "|", -1); for (guint i = 0; protocols[i] != NULL; i++) fwupd_device_add_protocol(self, protocols[i]); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_ISSUES) == 0) { g_autofree const gchar **strv = g_variant_get_strv(value, NULL); for (guint i = 0; strv[i] != NULL; i++) fwupd_device_add_issue(self, strv[i]); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_VERSION) == 0) { fwupd_device_set_version(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_VERSION_LOWEST) == 0) { fwupd_device_set_version_lowest(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_VERSION_BOOTLOADER) == 0) { fwupd_device_set_version_bootloader(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_FLASHES_LEFT) == 0) { fwupd_device_set_flashes_left(self, g_variant_get_uint32(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_BATTERY_LEVEL) == 0) { fwupd_device_set_battery_level(self, g_variant_get_uint32(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_BATTERY_THRESHOLD) == 0) { fwupd_device_set_battery_threshold(self, g_variant_get_uint32(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_INSTALL_DURATION) == 0) { fwupd_device_set_install_duration(self, g_variant_get_uint32(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_UPDATE_ERROR) == 0) { fwupd_device_set_update_error(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_UPDATE_STATE) == 0) { fwupd_device_set_update_state(self, g_variant_get_uint32(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_STATUS) == 0) { fwupd_device_set_status(self, g_variant_get_uint32(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_PERCENTAGE) == 0) { fwupd_device_set_percentage(self, g_variant_get_uint32(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_VERSION_FORMAT) == 0) { fwupd_device_set_version_format(self, g_variant_get_uint32(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_VERSION_RAW) == 0) { fwupd_device_set_version_raw(self, g_variant_get_uint64(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_VERSION_LOWEST_RAW) == 0) { fwupd_device_set_version_lowest_raw(self, g_variant_get_uint64(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_VERSION_BOOTLOADER_RAW) == 0) { fwupd_device_set_version_bootloader_raw(self, g_variant_get_uint64(value)); return; } } static void fwupd_device_string_append_flags(GString *str, guint idt, const gchar *key, guint64 device_flags) { g_autoptr(GString) tmp = g_string_new(""); for (guint i = 0; i < 64; i++) { if ((device_flags & ((guint64)1 << i)) == 0) continue; g_string_append_printf(tmp, "%s|", fwupd_device_flag_to_string((guint64)1 << i)); } if (tmp->len == 0) { g_string_append(tmp, fwupd_device_flag_to_string(0)); } else { g_string_truncate(tmp, tmp->len - 1); } fwupd_codec_string_append(str, idt, key, tmp->str); } static void fwupd_device_string_append_request_flags(GString *str, guint idt, const gchar *key, guint64 request_flags) { g_autoptr(GString) tmp = g_string_new(""); for (guint i = 0; i < 64; i++) { if ((request_flags & ((guint64)1 << i)) == 0) continue; g_string_append_printf(tmp, "%s|", fwupd_request_flag_to_string((guint64)1 << i)); } if (tmp->len == 0) { g_string_append(tmp, fwupd_request_flag_to_string(0)); } else { g_string_truncate(tmp, tmp->len - 1); } fwupd_codec_string_append(str, idt, key, tmp->str); } static void fwupd_device_string_append_problems(GString *str, guint idt, const gchar *key, guint64 device_problems) { g_autoptr(GString) tmp = g_string_new(""); for (guint i = 0; i < 64; i++) { if ((device_problems & ((guint64)1 << i)) == 0) continue; g_string_append_printf(tmp, "%s|", fwupd_device_problem_to_string((guint64)1 << i)); } if (tmp->len == 0) { g_string_append(tmp, fwupd_device_problem_to_string(0)); } else { g_string_truncate(tmp, tmp->len - 1); } fwupd_codec_string_append(str, idt, key, tmp->str); } /** * fwupd_device_get_update_state: * @self: a #FwupdDevice * * Gets the update state. * * Returns: the update state, or %FWUPD_UPDATE_STATE_UNKNOWN if unset * * Since: 0.9.8 **/ FwupdUpdateState fwupd_device_get_update_state(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), FWUPD_UPDATE_STATE_UNKNOWN); return priv->update_state; } /** * fwupd_device_set_update_state: * @self: a #FwupdDevice * @update_state: the state, e.g. %FWUPD_UPDATE_STATE_PENDING * * Sets the update state. * * Since: 0.9.8 **/ void fwupd_device_set_update_state(FwupdDevice *self, FwupdUpdateState update_state) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); if (priv->update_state == update_state) return; priv->update_state = update_state; g_object_notify(G_OBJECT(self), "update-state"); } /** * fwupd_device_get_version_format: * @self: a #FwupdDevice * * Gets the version format. * * Returns: the version format, or %FWUPD_VERSION_FORMAT_UNKNOWN if unset * * Since: 1.2.9 **/ FwupdVersionFormat fwupd_device_get_version_format(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), FWUPD_VERSION_FORMAT_UNKNOWN); return priv->version_format; } /** * fwupd_device_set_version_format: * @self: a #FwupdDevice * @version_format: the version format, e.g. %FWUPD_VERSION_FORMAT_NUMBER * * Sets the version format. * * Since: 1.2.9 **/ void fwupd_device_set_version_format(FwupdDevice *self, FwupdVersionFormat version_format) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); priv->version_format = version_format; } /** * fwupd_device_get_version_raw: * @self: a #FwupdDevice * * Gets the raw version number from the hardware before converted to a string. * * Returns: the hardware version, or 0 if unset * * Since: 1.3.6 **/ guint64 fwupd_device_get_version_raw(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), 0); return priv->version_raw; } /** * fwupd_device_set_version_raw: * @self: a #FwupdDevice * @version_raw: the raw hardware version * * Sets the raw version number from the hardware before converted to a string. * * Since: 1.3.6 **/ void fwupd_device_set_version_raw(FwupdDevice *self, guint64 version_raw) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); priv->version_raw = version_raw; } /** * fwupd_device_get_version_build_date: * @self: a #FwupdDevice * * Gets the date when the firmware was built. * * Returns: the UNIX time, or 0 if unset * * Since: 1.6.2 **/ guint64 fwupd_device_get_version_build_date(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), 0); return priv->version_build_date; } /** * fwupd_device_set_version_build_date: * @self: a #FwupdDevice * @version_build_date: the UNIX time * * Sets the date when the firmware was built. * * Since: 1.6.2 **/ void fwupd_device_set_version_build_date(FwupdDevice *self, guint64 version_build_date) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); priv->version_build_date = version_build_date; } /** * fwupd_device_get_update_error: * @self: a #FwupdDevice * * Gets the update error string. * * Returns: the update error string, or %NULL if unset * * Since: 0.9.8 **/ const gchar * fwupd_device_get_update_error(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); return priv->update_error; } /** * fwupd_device_set_update_error: * @self: a #FwupdDevice * @update_error: (nullable): the update error string * * Sets the update error string. * * Since: 0.9.8 **/ void fwupd_device_set_update_error(FwupdDevice *self, const gchar *update_error) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); /* not changed */ if (g_strcmp0(priv->update_error, update_error) == 0) return; g_free(priv->update_error); priv->update_error = g_strdup(update_error); g_object_notify(G_OBJECT(self), "update-error"); } /** * fwupd_device_get_release_default: * @self: a #FwupdDevice * * Gets the default release for this device. * * Returns: (transfer none): the #FwupdRelease, or %NULL if not set * * Since: 0.9.8 **/ FwupdRelease * fwupd_device_get_release_default(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); if (priv->releases == NULL || priv->releases->len == 0) return NULL; return FWUPD_RELEASE(g_ptr_array_index(priv->releases, 0)); } static void fwupd_device_ensure_releases(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); if (priv->releases == NULL) priv->releases = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); } /** * fwupd_device_get_releases: * @self: a #FwupdDevice * * Gets all the releases for this device. * * Returns: (transfer none) (element-type FwupdRelease): array of releases * * Since: 0.9.8 **/ GPtrArray * fwupd_device_get_releases(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), NULL); fwupd_device_ensure_releases(self); return priv->releases; } /** * fwupd_device_add_release: * @self: a #FwupdDevice * @release: (not nullable): a release * * Adds a release for this device. * * Since: 0.9.8 **/ void fwupd_device_add_release(FwupdDevice *self, FwupdRelease *release) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); g_return_if_fail(FWUPD_IS_RELEASE(release)); fwupd_device_ensure_releases(self); g_ptr_array_add(priv->releases, g_object_ref(release)); } /** * fwupd_device_get_status: * @self: a #FwupdDevice * * Returns what the device is currently doing. * * Returns: the status value, e.g. %FWUPD_STATUS_DEVICE_WRITE * * Since: 1.4.0 **/ FwupdStatus fwupd_device_get_status(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), 0); return priv->status; } /** * fwupd_device_set_status: * @self: a #FwupdDevice * @status: the status value, e.g. %FWUPD_STATUS_DEVICE_WRITE * * Sets what the device is currently doing. * * Since: 1.4.0 **/ void fwupd_device_set_status(FwupdDevice *self, FwupdStatus status) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); if (priv->status == status) return; priv->status = status; g_object_notify(G_OBJECT(self), "status"); } /** * fwupd_device_get_percentage: * @self: a #FwupdDevice * * Returns the percentage completion of the device. * * Returns: the percentage value * * Since: 1.8.11 **/ guint fwupd_device_get_percentage(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_DEVICE(self), 0); return priv->percentage; } /** * fwupd_device_set_percentage: * @self: a #FwupdDevice * @percentage: the percentage value * * Sets the percentage completion of the device. * * Since: 1.8.11 **/ void fwupd_device_set_percentage(FwupdDevice *self, guint percentage) { FwupdDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_DEVICE(self)); if (priv->percentage == percentage) return; priv->percentage = percentage; g_object_notify(G_OBJECT(self), "percentage"); } static void fwupd_device_string_append_update_state(GString *str, guint idt, const gchar *key, FwupdUpdateState value) { if (value == FWUPD_UPDATE_STATE_UNKNOWN) return; fwupd_codec_string_append(str, idt, key, fwupd_update_state_to_string(value)); } static void fwupd_device_add_json(FwupdCodec *codec, JsonBuilder *builder, FwupdCodecFlags flags) { FwupdDevice *self = FWUPD_DEVICE(codec); FwupdDevicePrivate *priv = GET_PRIVATE(self); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_NAME, priv->name); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_DEVICE_ID, priv->id); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_PARENT_DEVICE_ID, priv->parent_id); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_COMPOSITE_ID, priv->composite_id); if ((flags & FWUPD_CODEC_FLAG_TRUSTED) > 0 && priv->instance_ids != NULL && priv->instance_ids->len > 0) { json_builder_set_member_name(builder, FWUPD_RESULT_KEY_INSTANCE_IDS); json_builder_begin_array(builder); for (guint i = 0; i < priv->instance_ids->len; i++) { const gchar *instance_id = g_ptr_array_index(priv->instance_ids, i); json_builder_add_string_value(builder, instance_id); } json_builder_end_array(builder); } if (priv->guids != NULL && priv->guids->len > 0) { json_builder_set_member_name(builder, FWUPD_RESULT_KEY_GUID); json_builder_begin_array(builder); for (guint i = 0; i < priv->guids->len; i++) { const gchar *guid = g_ptr_array_index(priv->guids, i); json_builder_add_string_value(builder, guid); } json_builder_end_array(builder); } if (flags & FWUPD_CODEC_FLAG_TRUSTED) fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_SERIAL, priv->serial); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_SUMMARY, priv->summary); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_BRANCH, priv->branch); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_PLUGIN, priv->plugin); if (priv->protocols != NULL && priv->protocols->len > 0) { json_builder_set_member_name(builder, "Protocols"); json_builder_begin_array(builder); for (guint i = 0; i < priv->protocols->len; i++) { const gchar *tmp = g_ptr_array_index(priv->protocols, i); json_builder_add_string_value(builder, tmp); } json_builder_end_array(builder); } if (priv->issues != NULL && priv->issues->len > 0) { json_builder_set_member_name(builder, FWUPD_RESULT_KEY_ISSUES); json_builder_begin_array(builder); for (guint i = 0; i < priv->issues->len; i++) { const gchar *tmp = g_ptr_array_index(priv->issues, i); json_builder_add_string_value(builder, tmp); } json_builder_end_array(builder); } if (priv->flags != FWUPD_DEVICE_FLAG_NONE) { json_builder_set_member_name(builder, FWUPD_RESULT_KEY_FLAGS); json_builder_begin_array(builder); for (guint i = 0; i < 64; i++) { const gchar *tmp; if ((priv->flags & ((guint64)1 << i)) == 0) continue; tmp = fwupd_device_flag_to_string((guint64)1 << i); json_builder_add_string_value(builder, tmp); } json_builder_end_array(builder); } if (priv->request_flags != FWUPD_REQUEST_FLAG_NONE) { json_builder_set_member_name(builder, FWUPD_RESULT_KEY_REQUEST_FLAGS); json_builder_begin_array(builder); for (guint i = 0; i < 64; i++) { const gchar *tmp; if ((priv->request_flags & ((guint64)1 << i)) == 0) continue; tmp = fwupd_request_flag_to_string((guint64)1 << i); json_builder_add_string_value(builder, tmp); } json_builder_end_array(builder); } if (priv->problems != FWUPD_DEVICE_PROBLEM_NONE) { json_builder_set_member_name(builder, FWUPD_RESULT_KEY_PROBLEMS); json_builder_begin_array(builder); for (guint i = 0; i < 64; i++) { const gchar *tmp; if ((priv->problems & ((guint64)1 << i)) == 0) continue; tmp = fwupd_device_problem_to_string((guint64)1 << i); json_builder_add_string_value(builder, tmp); } json_builder_end_array(builder); } if (priv->checksums != NULL && priv->checksums->len > 0) { json_builder_set_member_name(builder, "Checksums"); json_builder_begin_array(builder); for (guint i = 0; i < priv->checksums->len; i++) { const gchar *checksum = g_ptr_array_index(priv->checksums, i); json_builder_add_string_value(builder, checksum); } json_builder_end_array(builder); } fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_VENDOR, priv->vendor); if (priv->vendor_ids != NULL && priv->vendor_ids->len > 0) { json_builder_set_member_name(builder, "VendorIds"); json_builder_begin_array(builder); for (guint i = 0; i < priv->vendor_ids->len; i++) { const gchar *tmp = g_ptr_array_index(priv->vendor_ids, i); json_builder_add_string_value(builder, tmp); } json_builder_end_array(builder); } fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_VERSION, priv->version); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_VERSION_LOWEST, priv->version_lowest); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_VERSION_BOOTLOADER, priv->version_bootloader); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_VERSION_FORMAT, fwupd_version_format_to_string(priv->version_format)); if (priv->flashes_left > 0) { fwupd_codec_json_append_int(builder, FWUPD_RESULT_KEY_FLASHES_LEFT, priv->flashes_left); } if (priv->battery_level != FWUPD_BATTERY_LEVEL_INVALID) { fwupd_codec_json_append_int(builder, FWUPD_RESULT_KEY_BATTERY_LEVEL, priv->battery_level); } if (priv->battery_threshold != FWUPD_BATTERY_LEVEL_INVALID) { fwupd_codec_json_append_int(builder, FWUPD_RESULT_KEY_BATTERY_THRESHOLD, priv->battery_threshold); } if (priv->version_raw > 0) { fwupd_codec_json_append_int(builder, FWUPD_RESULT_KEY_VERSION_RAW, priv->version_raw); } if (priv->version_lowest_raw > 0) fwupd_codec_json_append_int(builder, FWUPD_RESULT_KEY_VERSION_LOWEST_RAW, priv->version_lowest_raw); if (priv->version_bootloader_raw > 0) fwupd_codec_json_append_int(builder, FWUPD_RESULT_KEY_VERSION_BOOTLOADER_RAW, priv->version_bootloader_raw); if (priv->version_build_date > 0) { fwupd_codec_json_append_int(builder, FWUPD_RESULT_KEY_VERSION_BUILD_DATE, priv->version_build_date); } if (priv->icons != NULL && priv->icons->len > 0) { json_builder_set_member_name(builder, "Icons"); json_builder_begin_array(builder); for (guint i = 0; i < priv->icons->len; i++) { const gchar *icon = g_ptr_array_index(priv->icons, i); json_builder_add_string_value(builder, icon); } json_builder_end_array(builder); } if (priv->install_duration > 0) { fwupd_codec_json_append_int(builder, FWUPD_RESULT_KEY_INSTALL_DURATION, priv->install_duration); } if (priv->created > 0) fwupd_codec_json_append_int(builder, FWUPD_RESULT_KEY_CREATED, priv->created); if (priv->modified > 0) fwupd_codec_json_append_int(builder, FWUPD_RESULT_KEY_MODIFIED, priv->modified); if (priv->update_state > 0) { fwupd_codec_json_append_int(builder, FWUPD_RESULT_KEY_UPDATE_STATE, priv->update_state); } if (priv->status > 0) fwupd_codec_json_append_int(builder, FWUPD_RESULT_KEY_STATUS, priv->status); if (priv->percentage > 0) fwupd_codec_json_append_int(builder, FWUPD_RESULT_KEY_PERCENTAGE, priv->percentage); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_UPDATE_ERROR, priv->update_error); if (priv->releases != NULL && priv->releases->len > 0) fwupd_codec_array_to_json(priv->releases, "Releases", builder, flags); } static gboolean fwupd_device_from_json(FwupdCodec *codec, JsonNode *json_node, GError **error) { FwupdDevice *self = FWUPD_DEVICE(codec); JsonObject *obj; g_return_val_if_fail(FWUPD_IS_DEVICE(self), FALSE); g_return_val_if_fail(json_node != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* sanity check */ if (!JSON_NODE_HOLDS_OBJECT(json_node)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not JSON object"); return FALSE; } obj = json_node_get_object(json_node); /* this has to exist */ if (!json_object_has_member(obj, FWUPD_RESULT_KEY_DEVICE_ID)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no %s property in object", FWUPD_RESULT_KEY_DEVICE_ID); return FALSE; } fwupd_device_set_id(self, json_object_get_string_member(obj, FWUPD_RESULT_KEY_DEVICE_ID)); /* also optional */ if (json_object_has_member(obj, FWUPD_RESULT_KEY_NAME)) { const gchar *tmp = json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_NAME, NULL); fwupd_device_set_name(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_PARENT_DEVICE_ID)) { const gchar *tmp = json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_PARENT_DEVICE_ID, NULL); fwupd_device_set_parent_id(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_COMPOSITE_ID)) { const gchar *tmp = json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_COMPOSITE_ID, NULL); fwupd_device_set_composite_id(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_PROTOCOL)) { const gchar *tmp = json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_PROTOCOL, NULL); fwupd_device_add_protocol(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_SERIAL)) { const gchar *tmp = json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_SERIAL, NULL); fwupd_device_set_serial(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_SUMMARY)) { const gchar *tmp = json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_SUMMARY, NULL); fwupd_device_set_summary(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_BRANCH)) { const gchar *tmp = json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_BRANCH, NULL); fwupd_device_set_branch(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_PLUGIN)) { const gchar *tmp = json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_PLUGIN, NULL); fwupd_device_set_plugin(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_VENDOR)) { const gchar *tmp = json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_VENDOR, NULL); fwupd_device_set_vendor(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_VENDOR_ID)) { const gchar *tmp = json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_VENDOR_ID, NULL); if (tmp != NULL) { g_auto(GStrv) split = g_strsplit(tmp, "|", -1); for (guint i = 0; split[i] != NULL; i++) fwupd_device_add_vendor_id(self, split[i]); } } if (json_object_has_member(obj, FWUPD_RESULT_KEY_VERSION)) { const gchar *tmp = json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_VERSION, NULL); fwupd_device_set_version(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_VERSION_LOWEST)) { const gchar *tmp = json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_VERSION_LOWEST, NULL); fwupd_device_set_version_lowest(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_VERSION_BOOTLOADER)) { const gchar *tmp = json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_VERSION_BOOTLOADER, NULL); fwupd_device_set_version_bootloader(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_VERSION_FORMAT)) { const gchar *tmp = json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_VERSION_FORMAT, NULL); fwupd_device_set_version_format(self, fwupd_version_format_from_string(tmp)); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_FLASHES_LEFT)) { gint64 tmp = json_object_get_int_member_with_default(obj, FWUPD_RESULT_KEY_FLASHES_LEFT, 0); fwupd_device_set_flashes_left(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_BATTERY_LEVEL)) { gint64 tmp = json_object_get_int_member_with_default(obj, FWUPD_RESULT_KEY_BATTERY_LEVEL, 0); fwupd_device_set_battery_level(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_BATTERY_THRESHOLD)) { gint64 tmp = json_object_get_int_member_with_default(obj, FWUPD_RESULT_KEY_BATTERY_THRESHOLD, 0); fwupd_device_set_battery_threshold(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_VERSION_RAW)) { gint64 tmp = json_object_get_int_member_with_default(obj, FWUPD_RESULT_KEY_VERSION_RAW, 0); fwupd_device_set_version_raw(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_VERSION_LOWEST_RAW)) { gint64 tmp = json_object_get_int_member_with_default(obj, FWUPD_RESULT_KEY_VERSION_LOWEST_RAW, 0); fwupd_device_set_version_lowest_raw(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_VERSION_BOOTLOADER_RAW)) { gint64 tmp = json_object_get_int_member_with_default(obj, FWUPD_RESULT_KEY_VERSION_BOOTLOADER_RAW, 0); fwupd_device_set_version_bootloader_raw(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_VERSION_BUILD_DATE)) { gint64 tmp = json_object_get_int_member_with_default(obj, FWUPD_RESULT_KEY_VERSION_BUILD_DATE, 0); fwupd_device_set_version_build_date(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_INSTALL_DURATION)) { gint64 tmp = json_object_get_int_member_with_default(obj, FWUPD_RESULT_KEY_INSTALL_DURATION, 0); fwupd_device_set_install_duration(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_CREATED)) { gint64 tmp = json_object_get_int_member_with_default(obj, FWUPD_RESULT_KEY_CREATED, 0); fwupd_device_set_created(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_MODIFIED)) { gint64 tmp = json_object_get_int_member_with_default(obj, FWUPD_RESULT_KEY_MODIFIED, 0); fwupd_device_set_modified(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_UPDATE_STATE)) { const gchar *tmp = json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_UPDATE_STATE, NULL); fwupd_device_set_update_state(self, fwupd_update_state_from_string(tmp)); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_STATUS)) { const gchar *tmp = json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_STATUS, NULL); fwupd_device_set_status(self, fwupd_status_from_string(tmp)); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_PERCENTAGE)) { gint64 tmp = json_object_get_int_member_with_default(obj, FWUPD_RESULT_KEY_PERCENTAGE, 0); fwupd_device_set_percentage(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_UPDATE_ERROR)) { const gchar *tmp = json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_UPDATE_ERROR, NULL); fwupd_device_set_update_error(self, tmp); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_INSTANCE_IDS)) { JsonArray *array = json_object_get_array_member(obj, FWUPD_RESULT_KEY_INSTANCE_IDS); for (guint i = 0; i < json_array_get_length(array); i++) fwupd_device_add_instance_id(self, json_array_get_string_element(array, i)); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_GUID)) { JsonArray *array = json_object_get_array_member(obj, FWUPD_RESULT_KEY_GUID); for (guint i = 0; i < json_array_get_length(array); i++) fwupd_device_add_guid(self, json_array_get_string_element(array, i)); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_ISSUES)) { JsonArray *array = json_object_get_array_member(obj, FWUPD_RESULT_KEY_ISSUES); for (guint i = 0; i < json_array_get_length(array); i++) fwupd_device_add_issue(self, json_array_get_string_element(array, i)); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_FLAGS)) { JsonArray *array = json_object_get_array_member(obj, FWUPD_RESULT_KEY_FLAGS); for (guint i = 0; i < json_array_get_length(array); i++) { const gchar *tmp = json_array_get_string_element(array, i); fwupd_device_add_flag(self, fwupd_device_flag_from_string(tmp)); } } if (json_object_has_member(obj, FWUPD_RESULT_KEY_PROBLEMS)) { JsonArray *array = json_object_get_array_member(obj, FWUPD_RESULT_KEY_PROBLEMS); for (guint i = 0; i < json_array_get_length(array); i++) { const gchar *tmp = json_array_get_string_element(array, i); fwupd_device_add_problem(self, fwupd_device_problem_from_string(tmp)); } } if (json_object_has_member(obj, FWUPD_RESULT_KEY_REQUEST_FLAGS)) { JsonArray *array = json_object_get_array_member(obj, FWUPD_RESULT_KEY_REQUEST_FLAGS); for (guint i = 0; i < json_array_get_length(array); i++) { const gchar *tmp = json_array_get_string_element(array, i); fwupd_device_add_request_flag(self, fwupd_request_flag_from_string(tmp)); } } if (json_object_has_member(obj, "VendorIds")) { JsonArray *array = json_object_get_array_member(obj, "VendorIds"); for (guint i = 0; i < json_array_get_length(array); i++) fwupd_device_add_vendor_id(self, json_array_get_string_element(array, i)); } if (json_object_has_member(obj, "Protocols")) { JsonArray *array = json_object_get_array_member(obj, "Protocols"); for (guint i = 0; i < json_array_get_length(array); i++) fwupd_device_add_protocol(self, json_array_get_string_element(array, i)); } if (json_object_has_member(obj, "Icons")) { JsonArray *array = json_object_get_array_member(obj, "Icons"); for (guint i = 0; i < json_array_get_length(array); i++) fwupd_device_add_icon(self, json_array_get_string_element(array, i)); } if (json_object_has_member(obj, "Checksums")) { JsonArray *array = json_object_get_array_member(obj, "Checksums"); for (guint i = 0; i < json_array_get_length(array); i++) fwupd_device_add_checksum(self, json_array_get_string_element(array, i)); } /* success */ return TRUE; } static gchar * fwupd_device_verstr_raw(guint64 value_raw) { if (value_raw > 0xffffffff) { return g_strdup_printf("0x%08x%08x", (guint)(value_raw >> 32), (guint)(value_raw & 0xffffffff)); } return g_strdup_printf("0x%08x", (guint)value_raw); } typedef struct { gchar *guid; gchar *instance_id; } FwupdDeviceGuidHelper; static void fwupd_device_guid_helper_new(FwupdDeviceGuidHelper *helper) { g_free(helper->guid); g_free(helper->instance_id); g_free(helper); } static FwupdDeviceGuidHelper * fwupd_device_guid_helper_array_find(GPtrArray *array, const gchar *guid) { for (guint i = 0; i < array->len; i++) { FwupdDeviceGuidHelper *helper = g_ptr_array_index(array, i); if (g_strcmp0(helper->guid, guid) == 0) return helper; } return NULL; } static void fwupd_device_add_string(FwupdCodec *codec, guint idt, GString *str) { FwupdDevice *self = FWUPD_DEVICE(codec); FwupdDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(GPtrArray) guid_helpers = NULL; fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_DEVICE_ID, priv->id); if (g_strcmp0(priv->composite_id, priv->parent_id) != 0) { fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_PARENT_DEVICE_ID, priv->parent_id); } fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_COMPOSITE_ID, priv->composite_id); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_NAME, priv->name); if (priv->status != FWUPD_STATUS_UNKNOWN) { fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_STATUS, fwupd_status_to_string(priv->status)); } fwupd_codec_string_append_int(str, idt, FWUPD_RESULT_KEY_PERCENTAGE, priv->percentage); /* show instance IDs optionally mapped to GUIDs, and also "standalone" GUIDs */ guid_helpers = g_ptr_array_new_with_free_func((GDestroyNotify)fwupd_device_guid_helper_new); if (priv->instance_ids != NULL) { for (guint i = 0; i < priv->instance_ids->len; i++) { FwupdDeviceGuidHelper *helper = g_new0(FwupdDeviceGuidHelper, 1); const gchar *instance_id = g_ptr_array_index(priv->instance_ids, i); helper->guid = fwupd_guid_hash_string(instance_id); helper->instance_id = g_strdup(instance_id); g_ptr_array_add(guid_helpers, helper); } } if (priv->guids != NULL) { for (guint i = 0; i < priv->guids->len; i++) { const gchar *guid = g_ptr_array_index(priv->guids, i); if (fwupd_device_guid_helper_array_find(guid_helpers, guid) == NULL) { FwupdDeviceGuidHelper *helper = g_new0(FwupdDeviceGuidHelper, 1); helper->guid = g_strdup(guid); g_ptr_array_add(guid_helpers, helper); } } } for (guint i = 0; i < guid_helpers->len; i++) { FwupdDeviceGuidHelper *helper = g_ptr_array_index(guid_helpers, i); g_autoptr(GString) tmp = g_string_new(helper->guid); if (helper->instance_id != NULL) g_string_append_printf(tmp, " ↠%s", helper->instance_id); if (!fwupd_device_has_guid(self, helper->guid)) g_string_append(tmp, " ⚠"); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_GUID, tmp->str); } fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_SERIAL, priv->serial); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_SUMMARY, priv->summary); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_BRANCH, priv->branch); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_PLUGIN, priv->plugin); if (priv->protocols != NULL) { for (guint i = 0; i < priv->protocols->len; i++) { const gchar *tmp = g_ptr_array_index(priv->protocols, i); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_PROTOCOL, tmp); } } if (priv->issues != NULL) { for (guint i = 0; i < priv->issues->len; i++) { const gchar *tmp = g_ptr_array_index(priv->issues, i); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_ISSUES, tmp); } } fwupd_device_string_append_flags(str, idt, FWUPD_RESULT_KEY_FLAGS, priv->flags); if (priv->problems != FWUPD_DEVICE_PROBLEM_NONE) { fwupd_device_string_append_problems(str, idt, FWUPD_RESULT_KEY_PROBLEMS, priv->problems); } if (priv->request_flags > 0) { fwupd_device_string_append_request_flags(str, idt, FWUPD_RESULT_KEY_REQUEST_FLAGS, priv->request_flags); } if (priv->checksums != NULL) { for (guint i = 0; i < priv->checksums->len; i++) { const gchar *checksum = g_ptr_array_index(priv->checksums, i); g_autofree gchar *checksum_display = fwupd_checksum_format_for_display(checksum); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_CHECKSUM, checksum_display); } } fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_VENDOR, priv->vendor); if (priv->vendor_ids != NULL) { for (guint i = 0; i < priv->vendor_ids->len; i++) { const gchar *tmp = g_ptr_array_index(priv->vendor_ids, i); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_VENDOR_ID, tmp); } } fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_VERSION, priv->version); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_VERSION_LOWEST, priv->version_lowest); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_VERSION_BOOTLOADER, priv->version_bootloader); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_VERSION_FORMAT, fwupd_version_format_to_string(priv->version_format)); if (priv->flashes_left < 2) { fwupd_codec_string_append_int(str, idt, FWUPD_RESULT_KEY_FLASHES_LEFT, priv->flashes_left); } if (priv->battery_level != FWUPD_BATTERY_LEVEL_INVALID) { fwupd_codec_string_append_int(str, idt, FWUPD_RESULT_KEY_BATTERY_LEVEL, priv->battery_level); } if (priv->battery_threshold != FWUPD_BATTERY_LEVEL_INVALID) { fwupd_codec_string_append_int(str, idt, FWUPD_RESULT_KEY_BATTERY_THRESHOLD, priv->battery_threshold); } if (priv->version_raw > 0) { g_autofree gchar *tmp = fwupd_device_verstr_raw(priv->version_raw); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_VERSION_RAW, tmp); } if (priv->version_lowest_raw > 0) { g_autofree gchar *tmp = fwupd_device_verstr_raw(priv->version_lowest_raw); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_VERSION_LOWEST_RAW, tmp); } fwupd_codec_string_append_time(str, idt, FWUPD_RESULT_KEY_VERSION_BUILD_DATE, priv->version_build_date); if (priv->version_bootloader_raw > 0) { g_autofree gchar *tmp = fwupd_device_verstr_raw(priv->version_bootloader_raw); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_VERSION_BOOTLOADER_RAW, tmp); } if (priv->icons != NULL && priv->icons->len > 0) { g_autoptr(GString) tmp = g_string_new(NULL); for (guint i = 0; i < priv->icons->len; i++) { const gchar *icon = g_ptr_array_index(priv->icons, i); g_string_append_printf(tmp, "%s,", icon); } if (tmp->len > 1) g_string_truncate(tmp, tmp->len - 1); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_ICON, tmp->str); } fwupd_codec_string_append_int(str, idt, FWUPD_RESULT_KEY_INSTALL_DURATION, priv->install_duration); fwupd_codec_string_append_time(str, idt, FWUPD_RESULT_KEY_CREATED, priv->created); fwupd_codec_string_append_time(str, idt, FWUPD_RESULT_KEY_MODIFIED, priv->modified); fwupd_device_string_append_update_state(str, idt, FWUPD_RESULT_KEY_UPDATE_STATE, priv->update_state); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_UPDATE_ERROR, priv->update_error); if (priv->releases != NULL) { for (guint i = 0; i < priv->releases->len; i++) { FwupdRelease *release = g_ptr_array_index(priv->releases, i); fwupd_codec_add_string(FWUPD_CODEC(release), idt, str); } } } static void fwupd_device_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FwupdDevice *self = FWUPD_DEVICE(object); FwupdDevicePrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_ID: g_value_set_string(value, priv->id); break; case PROP_VERSION: g_value_set_string(value, priv->version); break; case PROP_VERSION_FORMAT: g_value_set_uint(value, priv->version_format); break; case PROP_FLAGS: g_value_set_uint64(value, priv->flags); break; case PROP_PROBLEMS: g_value_set_uint64(value, priv->problems); break; case PROP_REQUEST_FLAGS: g_value_set_uint64(value, priv->request_flags); break; case PROP_UPDATE_ERROR: g_value_set_string(value, priv->update_error); break; case PROP_STATUS: g_value_set_uint(value, priv->status); break; case PROP_PERCENTAGE: g_value_set_uint(value, priv->percentage); break; case PROP_PARENT: g_value_set_object(value, priv->parent); break; case PROP_UPDATE_STATE: g_value_set_uint(value, priv->update_state); break; case PROP_BATTERY_LEVEL: g_value_set_uint(value, priv->battery_level); break; case PROP_BATTERY_THRESHOLD: g_value_set_uint(value, priv->battery_threshold); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fwupd_device_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FwupdDevice *self = FWUPD_DEVICE(object); switch (prop_id) { case PROP_VERSION: fwupd_device_set_version(self, g_value_get_string(value)); break; case PROP_ID: fwupd_device_set_id(self, g_value_get_string(value)); break; case PROP_VERSION_FORMAT: fwupd_device_set_version_format(self, g_value_get_uint(value)); break; case PROP_FLAGS: fwupd_device_set_flags(self, g_value_get_uint64(value)); break; case PROP_PROBLEMS: fwupd_device_set_problems(self, g_value_get_uint64(value)); break; case PROP_REQUEST_FLAGS: fwupd_device_set_request_flags(self, g_value_get_uint64(value)); break; case PROP_UPDATE_ERROR: fwupd_device_set_update_error(self, g_value_get_string(value)); break; case PROP_STATUS: fwupd_device_set_status(self, g_value_get_uint(value)); break; case PROP_PERCENTAGE: fwupd_device_set_percentage(self, g_value_get_uint(value)); break; case PROP_PARENT: fwupd_device_set_parent(self, g_value_get_object(value)); break; case PROP_UPDATE_STATE: fwupd_device_set_update_state(self, g_value_get_uint(value)); break; case PROP_BATTERY_LEVEL: fwupd_device_set_battery_level(self, g_value_get_uint(value)); break; case PROP_BATTERY_THRESHOLD: fwupd_device_set_battery_threshold(self, g_value_get_uint(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fwupd_device_class_init(FwupdDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); GParamSpec *pspec; object_class->finalize = fwupd_device_finalize; object_class->get_property = fwupd_device_get_property; object_class->set_property = fwupd_device_set_property; /** * FwupdDevice:version: * * The device version. * * Since: 1.8.15 */ pspec = g_param_spec_string("version", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_VERSION, pspec); /** * FwupdDevice:id: * * The device ID. * * Since: 2.0.0 */ pspec = g_param_spec_string("id", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_ID, pspec); /** * FwupdDevice:version-format: * * The version format of the device. * * Since: 1.2.9 */ pspec = g_param_spec_uint("version-format", NULL, NULL, FWUPD_VERSION_FORMAT_UNKNOWN, FWUPD_VERSION_FORMAT_LAST, FWUPD_VERSION_FORMAT_UNKNOWN, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_VERSION_FORMAT, pspec); /** * FwupdDevice:flags: * * The device flags. * * Since: 0.9.3 */ pspec = g_param_spec_uint64("flags", NULL, NULL, FWUPD_DEVICE_FLAG_NONE, FWUPD_DEVICE_FLAG_UNKNOWN, FWUPD_DEVICE_FLAG_NONE, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_FLAGS, pspec); /** * FwupdDevice:problems: * * The problems with the device that the user could fix, e.g. "lid open". * * Since: 1.8.1 */ pspec = g_param_spec_uint64("problems", NULL, NULL, FWUPD_DEVICE_PROBLEM_NONE, FWUPD_DEVICE_PROBLEM_UNKNOWN, FWUPD_DEVICE_PROBLEM_NONE, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_PROBLEMS, pspec); /** * FwupdDevice:request-flags: * * The device request flags. * * Since: 1.9.10 */ pspec = g_param_spec_uint64("request-flags", NULL, NULL, FWUPD_REQUEST_FLAG_NONE, FWUPD_REQUEST_FLAG_UNKNOWN, FWUPD_REQUEST_FLAG_NONE, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_REQUEST_FLAGS, pspec); /** * FwupdDevice:status: * * The current device status. * * Since: 1.4.0 */ pspec = g_param_spec_uint("status", NULL, NULL, FWUPD_STATUS_UNKNOWN, FWUPD_STATUS_LAST, FWUPD_STATUS_UNKNOWN, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_STATUS, pspec); /** * FwupdDevice:percentage: * * The current device percentage. * * Since: 1.8.11 */ pspec = g_param_spec_uint("percentage", NULL, NULL, 0, 100, 0, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_PERCENTAGE, pspec); /** * FwupdDevice:parent: * * The device parent. * * Since: 1.0.8 */ pspec = g_param_spec_object("parent", NULL, NULL, FWUPD_TYPE_DEVICE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_PARENT, pspec); /** * FwupdDevice:update-state: * * The device update state. * * Since: 0.9.8 */ pspec = g_param_spec_uint("update-state", NULL, NULL, FWUPD_UPDATE_STATE_UNKNOWN, FWUPD_UPDATE_STATE_LAST, FWUPD_UPDATE_STATE_UNKNOWN, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_UPDATE_STATE, pspec); /** * FwupdDevice:update-error: * * The device update error. * * Since: 0.9.8 */ pspec = g_param_spec_string("update-error", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_UPDATE_ERROR, pspec); /** * FwupdDevice:battery-level: * * The device battery level in percent. * * Since: 1.5.8 */ pspec = g_param_spec_uint("battery-level", NULL, NULL, 0, FWUPD_BATTERY_LEVEL_INVALID, FWUPD_BATTERY_LEVEL_INVALID, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_BATTERY_LEVEL, pspec); /** * FwupdDevice:battery-threshold: * * The device battery threshold in percent. * * Since: 1.5.8 */ pspec = g_param_spec_uint("battery-threshold", NULL, NULL, 0, FWUPD_BATTERY_LEVEL_INVALID, FWUPD_BATTERY_LEVEL_INVALID, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_BATTERY_THRESHOLD, pspec); } static void fwupd_device_init(FwupdDevice *self) { FwupdDevicePrivate *priv = GET_PRIVATE(self); priv->battery_level = FWUPD_BATTERY_LEVEL_INVALID; priv->battery_threshold = FWUPD_BATTERY_LEVEL_INVALID; } static void fwupd_device_finalize(GObject *object) { FwupdDevice *self = FWUPD_DEVICE(object); FwupdDevicePrivate *priv = GET_PRIVATE(self); if (priv->parent != NULL) g_object_remove_weak_pointer(G_OBJECT(priv->parent), (gpointer *)&priv->parent); if (priv->children != NULL) { for (guint i = 0; i < priv->children->len; i++) { FwupdDevice *child = g_ptr_array_index(priv->children, i); g_object_weak_unref(G_OBJECT(child), fwupd_device_child_finalized_cb, self); } } g_free(priv->id); g_free(priv->parent_id); g_free(priv->composite_id); g_free(priv->name); g_free(priv->serial); g_free(priv->summary); g_free(priv->branch); g_free(priv->vendor); g_free(priv->plugin); g_free(priv->update_error); g_free(priv->version); g_free(priv->version_lowest); g_free(priv->version_bootloader); if (priv->guids != NULL) g_ptr_array_unref(priv->guids); if (priv->vendor_ids != NULL) g_ptr_array_unref(priv->vendor_ids); if (priv->protocols != NULL) g_ptr_array_unref(priv->protocols); if (priv->instance_ids != NULL) g_ptr_array_unref(priv->instance_ids); if (priv->icons != NULL) g_ptr_array_unref(priv->icons); if (priv->checksums != NULL) g_ptr_array_unref(priv->checksums); if (priv->children != NULL) g_ptr_array_unref(priv->children); if (priv->releases != NULL) g_ptr_array_unref(priv->releases); if (priv->issues != NULL) g_ptr_array_unref(priv->issues); G_OBJECT_CLASS(fwupd_device_parent_class)->finalize(object); } static void fwupd_device_from_variant_iter(FwupdCodec *codec, GVariantIter *iter) { FwupdDevice *self = FWUPD_DEVICE(codec); GVariant *value; const gchar *key; while (g_variant_iter_next(iter, "{&sv}", &key, &value)) { fwupd_device_from_key_value(self, key, value); g_variant_unref(value); } } static void fwupd_device_codec_iface_init(FwupdCodecInterface *iface) { iface->add_string = fwupd_device_add_string; iface->add_json = fwupd_device_add_json; iface->from_json = fwupd_device_from_json; iface->add_variant = fwupd_device_add_variant; iface->from_variant_iter = fwupd_device_from_variant_iter; } /** * fwupd_device_array_ensure_parents: * @devices: (not nullable) (element-type FwupdDevice): devices * * Sets the parent object on all devices in the array using the parent ID. * * Since: 1.3.7 **/ void fwupd_device_array_ensure_parents(GPtrArray *devices) { g_autoptr(GHashTable) devices_by_id = NULL; g_return_if_fail(devices != NULL); /* create hash of ID->FwupdDevice */ devices_by_id = g_hash_table_new(g_str_hash, g_str_equal); for (guint i = 0; i < devices->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices, i); if (fwupd_device_get_id(dev) == NULL) continue; g_hash_table_insert(devices_by_id, (gpointer)fwupd_device_get_id(dev), (gpointer)dev); } /* set the parent on each child */ for (guint i = 0; i < devices->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices, i); const gchar *parent_id = fwupd_device_get_parent_id(dev); if (parent_id != NULL) { FwupdDevice *dev_tmp; dev_tmp = g_hash_table_lookup(devices_by_id, parent_id); if (dev_tmp != NULL) fwupd_device_set_parent(dev, dev_tmp); } } } /** * fwupd_device_compare: * @self1: (not nullable): a device * @self2: (not nullable): a different device * * Comparison function for comparing two device objects. * * Returns: negative, 0 or positive * * Since: 1.1.1 **/ gint fwupd_device_compare(FwupdDevice *self1, FwupdDevice *self2) { FwupdDevicePrivate *priv1 = GET_PRIVATE(self1); FwupdDevicePrivate *priv2 = GET_PRIVATE(self2); g_return_val_if_fail(FWUPD_IS_DEVICE(self1), 0); g_return_val_if_fail(FWUPD_IS_DEVICE(self2), 0); return g_strcmp0(priv1->id, priv2->id); } /** * fwupd_device_match_flags: * @include: #FwupdDeviceFlags, or %FWUPD_DEVICE_FLAG_NONE * @exclude: #FwupdDeviceFlags, or %FWUPD_DEVICE_FLAG_NONE * * Check if the device flags match. * * Returns: %TRUE if the device flags match * * Since: 1.9.3 **/ gboolean fwupd_device_match_flags(FwupdDevice *self, FwupdDeviceFlags include, FwupdDeviceFlags exclude) { g_return_val_if_fail(FWUPD_IS_DEVICE(self), FALSE); for (guint i = 0; i < 64; i++) { FwupdDeviceFlags flag = 1LLU << i; if ((include & flag) > 0) { if (!fwupd_device_has_flag(self, flag)) return FALSE; } if ((exclude & flag) > 0) { if (fwupd_device_has_flag(self, flag)) return FALSE; } } return TRUE; } /** * fwupd_device_array_filter_flags: * @devices: (not nullable) (element-type FwupdDevice): devices * @include: #FwupdDeviceFlags, or %FWUPD_DEVICE_FLAG_NONE * @exclude: #FwupdDeviceFlags, or %FWUPD_DEVICE_FLAG_NONE * @error: (nullable): optional return location for an error * * Creates an array of new devices that match using fwupd_device_match_flags(). * * Returns: (transfer container) (element-type FwupdDevice): devices * * Since: 1.9.3 **/ GPtrArray * fwupd_device_array_filter_flags(GPtrArray *devices, FwupdDeviceFlags include, FwupdDeviceFlags exclude, GError **error) { g_autoptr(GPtrArray) devices_filtered = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_return_val_if_fail(devices != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); for (guint i = 0; i < devices->len; i++) { FwupdDevice *device = g_ptr_array_index(devices, i); if (!fwupd_device_match_flags(device, include, exclude)) continue; g_ptr_array_add(devices_filtered, g_object_ref(device)); } if (devices_filtered->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "no devices"); return NULL; } return g_steal_pointer(&devices_filtered); } /** * fwupd_device_new: * * Creates a new device. * * Returns: a new #FwupdDevice * * Since: 0.9.3 **/ FwupdDevice * fwupd_device_new(void) { FwupdDevice *self; self = g_object_new(FWUPD_TYPE_DEVICE, NULL); return FWUPD_DEVICE(self); } fwupd-2.0.10/libfwupd/fwupd-device.h000066400000000000000000000240411501337203100172620ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fwupd-enums.h" #include "fwupd-release.h" #include "fwupd-request.h" G_BEGIN_DECLS #define FWUPD_TYPE_DEVICE (fwupd_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FwupdDevice, fwupd_device, FWUPD, DEVICE, GObject) struct _FwupdDeviceClass { GObjectClass parent_class; /*< private >*/ void (*_fwupd_reserved1)(void); void (*_fwupd_reserved2)(void); void (*_fwupd_reserved3)(void); void (*_fwupd_reserved4)(void); void (*_fwupd_reserved5)(void); void (*_fwupd_reserved6)(void); void (*_fwupd_reserved7)(void); }; FwupdDevice * fwupd_device_new(void); const gchar * fwupd_device_get_id(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_id(FwupdDevice *self, const gchar *id) G_GNUC_NON_NULL(1); const gchar * fwupd_device_get_parent_id(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_parent_id(FwupdDevice *self, const gchar *parent_id) G_GNUC_NON_NULL(1); const gchar * fwupd_device_get_composite_id(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_composite_id(FwupdDevice *self, const gchar *composite_id) G_GNUC_NON_NULL(1); FwupdDevice * fwupd_device_get_root(FwupdDevice *self) G_GNUC_NON_NULL(1); FwupdDevice * fwupd_device_get_parent(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_parent(FwupdDevice *self, FwupdDevice *parent) G_GNUC_NON_NULL(1); void fwupd_device_add_child(FwupdDevice *self, FwupdDevice *child) G_GNUC_NON_NULL(1, 2); void fwupd_device_remove_child(FwupdDevice *self, FwupdDevice *child) G_GNUC_NON_NULL(1, 2); GPtrArray * fwupd_device_get_children(FwupdDevice *self) G_GNUC_NON_NULL(1); const gchar * fwupd_device_get_name(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_name(FwupdDevice *self, const gchar *name) G_GNUC_NON_NULL(1); const gchar * fwupd_device_get_serial(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_serial(FwupdDevice *self, const gchar *serial) G_GNUC_NON_NULL(1); const gchar * fwupd_device_get_summary(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_summary(FwupdDevice *self, const gchar *summary) G_GNUC_NON_NULL(1); const gchar * fwupd_device_get_branch(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_branch(FwupdDevice *self, const gchar *branch) G_GNUC_NON_NULL(1); const gchar * fwupd_device_get_version(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_version(FwupdDevice *self, const gchar *version) G_GNUC_NON_NULL(1); const gchar * fwupd_device_get_version_lowest(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_version_lowest(FwupdDevice *self, const gchar *version_lowest) G_GNUC_NON_NULL(1); guint64 fwupd_device_get_version_lowest_raw(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_version_lowest_raw(FwupdDevice *self, guint64 version_lowest_raw) G_GNUC_NON_NULL(1); const gchar * fwupd_device_get_version_bootloader(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_version_bootloader(FwupdDevice *self, const gchar *version_bootloader) G_GNUC_NON_NULL(1); guint64 fwupd_device_get_version_bootloader_raw(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_version_bootloader_raw(FwupdDevice *self, guint64 version_bootloader_raw) G_GNUC_NON_NULL(1); guint64 fwupd_device_get_version_raw(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_version_raw(FwupdDevice *self, guint64 version_raw) G_GNUC_NON_NULL(1); guint64 fwupd_device_get_version_build_date(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_version_build_date(FwupdDevice *self, guint64 version_build_date) G_GNUC_NON_NULL(1); FwupdVersionFormat fwupd_device_get_version_format(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_version_format(FwupdDevice *self, FwupdVersionFormat version_format) G_GNUC_NON_NULL(1); guint32 fwupd_device_get_flashes_left(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_flashes_left(FwupdDevice *self, guint32 flashes_left) G_GNUC_NON_NULL(1); guint32 fwupd_device_get_battery_level(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_battery_level(FwupdDevice *self, guint32 battery_level) G_GNUC_NON_NULL(1); guint32 fwupd_device_get_battery_threshold(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_battery_threshold(FwupdDevice *self, guint32 battery_threshold) G_GNUC_NON_NULL(1); guint32 fwupd_device_get_install_duration(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_install_duration(FwupdDevice *self, guint32 duration) G_GNUC_NON_NULL(1); guint64 fwupd_device_get_flags(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_flags(FwupdDevice *self, guint64 flags) G_GNUC_NON_NULL(1); void fwupd_device_add_flag(FwupdDevice *self, FwupdDeviceFlags flag) G_GNUC_NON_NULL(1); void fwupd_device_remove_flag(FwupdDevice *self, FwupdDeviceFlags flag) G_GNUC_NON_NULL(1); gboolean fwupd_device_has_flag(FwupdDevice *self, FwupdDeviceFlags flag) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); guint64 fwupd_device_get_problems(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_problems(FwupdDevice *self, guint64 problems) G_GNUC_NON_NULL(1); void fwupd_device_add_problem(FwupdDevice *self, FwupdDeviceProblem problem) G_GNUC_NON_NULL(1); void fwupd_device_remove_problem(FwupdDevice *self, FwupdDeviceProblem problem) G_GNUC_NON_NULL(1); gboolean fwupd_device_has_problem(FwupdDevice *self, FwupdDeviceProblem problem) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); guint64 fwupd_device_get_request_flags(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_request_flags(FwupdDevice *self, guint64 request_flags) G_GNUC_NON_NULL(1); void fwupd_device_add_request_flag(FwupdDevice *self, FwupdRequestFlags request_flag) G_GNUC_NON_NULL(1); void fwupd_device_remove_request_flag(FwupdDevice *self, FwupdRequestFlags request_flag) G_GNUC_NON_NULL(1); gboolean fwupd_device_has_request_flag(FwupdDevice *self, FwupdRequestFlags request_flag) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); guint64 fwupd_device_get_created(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_created(FwupdDevice *self, guint64 created) G_GNUC_NON_NULL(1); guint64 fwupd_device_get_modified(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_modified(FwupdDevice *self, guint64 modified) G_GNUC_NON_NULL(1); GPtrArray * fwupd_device_get_checksums(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_add_checksum(FwupdDevice *self, const gchar *checksum) G_GNUC_NON_NULL(1, 2); gboolean fwupd_device_has_checksum(FwupdDevice *self, const gchar *checksum) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); const gchar * fwupd_device_get_plugin(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_plugin(FwupdDevice *self, const gchar *plugin) G_GNUC_NON_NULL(1); void fwupd_device_add_protocol(FwupdDevice *self, const gchar *protocol) G_GNUC_NON_NULL(1, 2); gboolean fwupd_device_has_protocol(FwupdDevice *self, const gchar *protocol) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); GPtrArray * fwupd_device_get_protocols(FwupdDevice *self) G_GNUC_NON_NULL(1); const gchar * fwupd_device_get_vendor(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_vendor(FwupdDevice *self, const gchar *vendor) G_GNUC_NON_NULL(1); void fwupd_device_add_vendor_id(FwupdDevice *self, const gchar *vendor_id) G_GNUC_NON_NULL(1, 2); gboolean fwupd_device_has_vendor_id(FwupdDevice *self, const gchar *vendor_id) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); GPtrArray * fwupd_device_get_vendor_ids(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_add_guid(FwupdDevice *self, const gchar *guid) G_GNUC_NON_NULL(1, 2); gboolean fwupd_device_has_guid(FwupdDevice *self, const gchar *guid) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); GPtrArray * fwupd_device_get_guids(FwupdDevice *self) G_GNUC_NON_NULL(1); const gchar * fwupd_device_get_guid_default(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_add_instance_id(FwupdDevice *self, const gchar *instance_id) G_GNUC_NON_NULL(1, 2); gboolean fwupd_device_has_instance_id(FwupdDevice *self, const gchar *instance_id) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); GPtrArray * fwupd_device_get_instance_ids(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_add_icon(FwupdDevice *self, const gchar *icon) G_GNUC_NON_NULL(1, 2); gboolean fwupd_device_has_icon(FwupdDevice *self, const gchar *icon) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); GPtrArray * fwupd_device_get_icons(FwupdDevice *self) G_GNUC_NON_NULL(1); GPtrArray * fwupd_device_get_issues(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_add_issue(FwupdDevice *self, const gchar *issue) G_GNUC_NON_NULL(1, 2); FwupdUpdateState fwupd_device_get_update_state(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_update_state(FwupdDevice *self, FwupdUpdateState update_state) G_GNUC_NON_NULL(1); const gchar * fwupd_device_get_update_error(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_update_error(FwupdDevice *self, const gchar *update_error) G_GNUC_NON_NULL(1); FwupdStatus fwupd_device_get_status(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_status(FwupdDevice *self, FwupdStatus status) G_GNUC_NON_NULL(1); guint fwupd_device_get_percentage(FwupdDevice *self) G_GNUC_NON_NULL(1); void fwupd_device_set_percentage(FwupdDevice *self, guint percentage) G_GNUC_NON_NULL(1); void fwupd_device_add_release(FwupdDevice *self, FwupdRelease *release) G_GNUC_NON_NULL(1); GPtrArray * fwupd_device_get_releases(FwupdDevice *self) G_GNUC_NON_NULL(1); FwupdRelease * fwupd_device_get_release_default(FwupdDevice *self) G_GNUC_NON_NULL(1); gint fwupd_device_compare(FwupdDevice *self1, FwupdDevice *self2) G_GNUC_NON_NULL(1, 2); gboolean fwupd_device_match_flags(FwupdDevice *self, FwupdDeviceFlags include, FwupdDeviceFlags exclude) G_GNUC_NON_NULL(1); void fwupd_device_array_ensure_parents(GPtrArray *devices) G_GNUC_NON_NULL(1); GPtrArray * fwupd_device_array_filter_flags(GPtrArray *devices, FwupdDeviceFlags include, FwupdDeviceFlags exclude, GError **error) G_GNUC_NON_NULL(1); G_END_DECLS fwupd-2.0.10/libfwupd/fwupd-enums-private.h000066400000000000000000000426051501337203100206300ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_BEGIN_DECLS /** * FWUPD_RESULT_KEY_APPSTREAM_ID: * * Result key to represent AppstreamId * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_APPSTREAM_ID "AppstreamId" /** * FWUPD_RESULT_KEY_RELEASE_ID: * * Result key to represent the release ID. * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_RELEASE_ID "ReleaseId" /** * FWUPD_RESULT_KEY_CHECKSUM: * * Result key to represent Checksum * * The D-Bus type signature string is 'as' i.e. an array of strings. **/ #define FWUPD_RESULT_KEY_CHECKSUM "Checksum" /** * FWUPD_RESULT_KEY_TAGS: * * Result key to represent release tags * * The D-Bus type signature string is 'as' i.e. an array of strings. **/ #define FWUPD_RESULT_KEY_TAGS "Tags" /** * FWUPD_RESULT_KEY_CREATED: * * Result key to represent Created * * The D-Bus type signature string is 't' i.e. a unsigned 64 bit integer. **/ #define FWUPD_RESULT_KEY_CREATED "Created" /** * FWUPD_RESULT_KEY_DESCRIPTION: * * Result key to represent Description * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_DESCRIPTION "Description" /** * FWUPD_RESULT_KEY_DETACH_CAPTION: * * Result key to represent DetachCaption * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_DETACH_CAPTION "DetachCaption" /** * FWUPD_RESULT_KEY_DETACH_IMAGE: * * Result key to represent DetachImage * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_DETACH_IMAGE "DetachImage" /** * FWUPD_RESULT_KEY_DEVICE_ID: * * Result key to represent DeviceId * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_DEVICE_ID "DeviceId" /** * FWUPD_RESULT_KEY_PARENT_DEVICE_ID: * * Result key to represent ParentDeviceId * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_PARENT_DEVICE_ID "ParentDeviceId" /** * FWUPD_RESULT_KEY_COMPOSITE_ID: * * Result key to represent CompositeId * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_COMPOSITE_ID "CompositeId" /** * FWUPD_RESULT_KEY_FILENAME: * * Result key to represent Filename * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_FILENAME "Filename" /** * FWUPD_RESULT_KEY_PROTOCOL: * * Result key to represent Protocol * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_PROTOCOL "Protocol" /** * FWUPD_RESULT_KEY_CATEGORIES: * * Result key to represent Categories * * The D-Bus type signature string is 'as' i.e. an array of strings. **/ #define FWUPD_RESULT_KEY_CATEGORIES "Categories" /** * FWUPD_RESULT_KEY_ISSUES: * * Result key to represent Issues * * The D-Bus type signature string is 'as' i.e. an array of strings. **/ #define FWUPD_RESULT_KEY_ISSUES "Issues" /** * FWUPD_RESULT_KEY_FLAGS: * * Result key to represent Flags * * The D-Bus type signature string is 't' i.e. a unsigned 64 bit integer. **/ #define FWUPD_RESULT_KEY_FLAGS "Flags" /** * FWUPD_RESULT_KEY_REQUEST_FLAGS: * * Result key to represent RequestFlags * * The D-Bus type signature string is 't' i.e. a unsigned 64 bit integer. **/ #define FWUPD_RESULT_KEY_REQUEST_FLAGS "RequestFlags" /** * FWUPD_RESULT_KEY_FLASHES_LEFT: * * Result key to represent FlashesLeft * * The D-Bus type signature string is 'u' i.e. a unsigned 32 bit integer. **/ #define FWUPD_RESULT_KEY_FLASHES_LEFT "FlashesLeft" /** * FWUPD_RESULT_KEY_URGENCY: * * Result key to represent Urgency * * The D-Bus type signature string is 'u' i.e. a unsigned 32 bit integer. **/ #define FWUPD_RESULT_KEY_URGENCY "Urgency" /** * FWUPD_RESULT_KEY_REQUEST_KIND: * * Result key to represent RequestKind * * The D-Bus type signature string is 'u' i.e. a unsigned 32 bit integer. **/ #define FWUPD_RESULT_KEY_REQUEST_KIND "RequestKind" /** * FWUPD_RESULT_KEY_HSI_LEVEL: * * Result key to represent HsiLevel * * The D-Bus type signature string is 'u' i.e. a unsigned 32 bit integer. **/ #define FWUPD_RESULT_KEY_HSI_LEVEL "HsiLevel" /** * FWUPD_RESULT_KEY_HSI_RESULT: * * Result key to represent HsiResult * * The D-Bus type signature string is 'u' i.e. a unsigned 32 bit integer. **/ #define FWUPD_RESULT_KEY_HSI_RESULT "HsiResult" /** * FWUPD_RESULT_KEY_HSI_RESULT_FALLBACK: * * Result key to represent the fallback HsiResult * * The D-Bus type signature string is 'u' i.e. a unsigned 32 bit integer. **/ #define FWUPD_RESULT_KEY_HSI_RESULT_FALLBACK "HsiResultFallback" /** * FWUPD_RESULT_KEY_HSI_RESULT_SUCCESS: * * Result key to represent the desired HsiResult * * The D-Bus type signature string is 'u' i.e. a unsigned 32 bit integer. **/ #define FWUPD_RESULT_KEY_HSI_RESULT_SUCCESS "HsiResultSuccess" /** * FWUPD_RESULT_KEY_INSTALL_DURATION: * * Result key to represent InstallDuration * * The D-Bus type signature string is 'u' i.e. a unsigned 32 bit integer. **/ #define FWUPD_RESULT_KEY_INSTALL_DURATION "InstallDuration" /** * FWUPD_RESULT_KEY_GUID: * * Result key to represent Guid * * The D-Bus type signature string is 'as' i.e. an array of strings. **/ #define FWUPD_RESULT_KEY_GUID "Guid" /** * FWUPD_RESULT_KEY_INSTANCE_IDS: * * Result key to represent InstanceIds * * The D-Bus type signature string is 'as' i.e. an array of strings. **/ #define FWUPD_RESULT_KEY_INSTANCE_IDS "InstanceIds" /** * FWUPD_RESULT_KEY_HOMEPAGE: * * Result key to represent Homepage * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_HOMEPAGE "Homepage" /** * FWUPD_RESULT_KEY_DETAILS_URL: * * Result key to represent DetailsUrl * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_DETAILS_URL "DetailsUrl" /** * FWUPD_RESULT_KEY_SOURCE_URL: * * Result key to represent SourceUrl * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_SOURCE_URL "SourceUrl" /** * FWUPD_RESULT_KEY_SBOM_URL: * * Result key to represent SourceUrl * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_SBOM_URL "SbomUrl" /** * FWUPD_RESULT_KEY_ICON: * * Result key to represent Icon * * The D-Bus type signature string is 'as' i.e. an array of strings. **/ #define FWUPD_RESULT_KEY_ICON "Icon" /** * FWUPD_RESULT_KEY_LICENSE: * * Result key to represent License * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_LICENSE "License" /** * FWUPD_RESULT_KEY_MODIFIED: * * Result key to represent Modified * * The D-Bus type signature string is 't' i.e. a unsigned 64 bit integer. **/ #define FWUPD_RESULT_KEY_MODIFIED "Modified" /** * FWUPD_RESULT_KEY_VERSION_BUILD_DATE: * * Result key to represent VersionBuildDate * * The D-Bus type signature string is 't' i.e. a unsigned 64 bit integer. **/ #define FWUPD_RESULT_KEY_VERSION_BUILD_DATE "VersionBuildDate" /** * FWUPD_RESULT_KEY_METADATA: * * Result key to represent Metadata * * The D-Bus type signature string is 'a{ss}' i.e. a string dictionary. **/ #define FWUPD_RESULT_KEY_METADATA "Metadata" /** * FWUPD_RESULT_KEY_NAME: * * Result key to represent Name * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_NAME "Name" /** * FWUPD_RESULT_KEY_NAME_VARIANT_SUFFIX: * * Result key to represent NameVariantSuffix * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_NAME_VARIANT_SUFFIX "NameVariantSuffix" /** * FWUPD_RESULT_KEY_PLUGIN: * * Result key to represent Plugin * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_PLUGIN "Plugin" /** * FWUPD_RESULT_KEY_RELEASE: * * Result key to represent Release * * The D-Bus type signature string is 'a{sv}' i.e. a variant dictionary. **/ #define FWUPD_RESULT_KEY_RELEASE "Release" /** * FWUPD_RESULT_KEY_REMOTE_ID: * * Result key to represent RemoteId * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_REMOTE_ID "RemoteId" /** * FWUPD_RESULT_KEY_SERIAL: * * Result key to represent Serial * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_SERIAL "Serial" /** * FWUPD_RESULT_KEY_SIZE: * * Result key to represent Size * * The D-Bus type signature string is 't' i.e. a unsigned 64 bit integer. **/ #define FWUPD_RESULT_KEY_SIZE "Size" /** * FWUPD_RESULT_KEY_STATUS: * * Result key to represent Status * * The D-Bus type signature string is 'u' i.e. a unsigned 32 bit integer. **/ #define FWUPD_RESULT_KEY_STATUS "Status" /** * FWUPD_RESULT_KEY_PERCENTAGE: * * Result key to represent progress percentage, typically installation or verification * * The D-Bus type signature string is 'u' i.e. a unsigned 32 bit integer. **/ #define FWUPD_RESULT_KEY_PERCENTAGE "Percentage" /** * FWUPD_RESULT_KEY_SUMMARY: * * Result key to represent Summary * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_SUMMARY "Summary" /** * FWUPD_RESULT_KEY_BRANCH: * * Result key to represent Branch * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_BRANCH "Branch" /** * FWUPD_RESULT_KEY_TRUST_FLAGS: * * Result key to represent TrustFlags * * The D-Bus type signature string is 't' i.e. a unsigned 64 bit integer. **/ #define FWUPD_RESULT_KEY_TRUST_FLAGS "TrustFlags" /** * FWUPD_RESULT_KEY_PROBLEMS: * * Result key to represent problems * * The D-Bus type signature string is 't' i.e. a unsigned 64 bit integer. **/ #define FWUPD_RESULT_KEY_PROBLEMS "Problems" /** * FWUPD_RESULT_KEY_UPDATE_MESSAGE: * * Result key to represent UpdateMessage * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_UPDATE_MESSAGE "UpdateMessage" /** * FWUPD_RESULT_KEY_UPDATE_IMAGE: * * Result key to represent UpdateImage * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_UPDATE_IMAGE "UpdateImage" /** * FWUPD_RESULT_KEY_UPDATE_ERROR: * * Result key to represent UpdateError * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_UPDATE_ERROR "UpdateError" /** * FWUPD_RESULT_KEY_UPDATE_STATE: * * Result key to represent UpdateState * * The D-Bus type signature string is 'u' i.e. a unsigned 32 bit integer. **/ #define FWUPD_RESULT_KEY_UPDATE_STATE "UpdateState" /** * FWUPD_RESULT_KEY_URI: * * Result key to represent Uri * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_URI "Uri" /** * FWUPD_RESULT_KEY_LOCATIONS: * * Result key to represent Locations * * The D-Bus type signature string is 'as' i.e. an array of strings. **/ #define FWUPD_RESULT_KEY_LOCATIONS "Locations" /** * FWUPD_RESULT_KEY_VENDOR_ID: * * Result key to represent VendorId * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_VENDOR_ID "VendorId" /** * FWUPD_RESULT_KEY_VENDOR: * * Result key to represent Vendor * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_VENDOR "Vendor" /** * FWUPD_RESULT_KEY_VERSION_BOOTLOADER: * * Result key to represent VersionBootloader * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_VERSION_BOOTLOADER "VersionBootloader" /** * FWUPD_RESULT_KEY_VERSION_BOOTLOADER_RAW: * * Result key to represent VersionBootloaderRaw * * The D-Bus type signature string is 't' i.e. a unsigned 64 bit integer. **/ #define FWUPD_RESULT_KEY_VERSION_BOOTLOADER_RAW "VersionBootloaderRaw" /** * FWUPD_RESULT_KEY_VERSION_FORMAT: * * Result key to represent VersionFormat * * The D-Bus type signature string is 'u' i.e. a unsigned 32 bit integer. **/ #define FWUPD_RESULT_KEY_VERSION_FORMAT "VersionFormat" /** * FWUPD_RESULT_KEY_VERSION_RAW: * * Result key to represent VersionRaw * * The D-Bus type signature string is 't' i.e. a unsigned 64 bit integer. **/ #define FWUPD_RESULT_KEY_VERSION_RAW "VersionRaw" /** * FWUPD_RESULT_KEY_VERSION_LOWEST: * * Result key to represent VersionLowest * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_VERSION_LOWEST "VersionLowest" /** * FWUPD_RESULT_KEY_VERSION_LOWEST_RAW: * * Result key to represent VersionLowestRaw * * The D-Bus type signature string is 't' i.e. a unsigned 64 bit integer. **/ #define FWUPD_RESULT_KEY_VERSION_LOWEST_RAW "VersionLowestRaw" /** * FWUPD_RESULT_KEY_VERSION: * * Result key to represent Version * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_VERSION "Version" /** * FWUPD_RESULT_KEY_VERSION_OLD: * * Result key to represent the old version string. * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_VERSION_OLD "VersionOld" /** * FWUPD_RESULT_KEY_BATTERY_LEVEL: * * Result key to represent the current battery level in percent. * Expressed from 0-100%, or 101 for invalid or unset. * * The D-Bus type signature string is 'u' i.e. a unsigned 32 bit integer. **/ #define FWUPD_RESULT_KEY_BATTERY_LEVEL "BatteryLevel" /** * FWUPD_RESULT_KEY_BATTERY_THRESHOLD: * * Result key to represent the minimum battery level required to perform an update. * Expressed from 0-100%, or 101 for invalid or unset. * * The D-Bus type signature string is 'u' i.e. a unsigned 32 bit integer. **/ #define FWUPD_RESULT_KEY_BATTERY_THRESHOLD "BatteryThreshold" /** * FWUPD_RESULT_KEY_BIOS_SETTING_ID: * * Result key to represent the unique identifier of the BIOS setting. * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_BIOS_SETTING_ID "BiosSettingId" /** * FWUPD_RESULT_KEY_BIOS_SETTING_TARGET_VALUE: * * Result key to represent the value that would enable this attribute. * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_BIOS_SETTING_TARGET_VALUE "BiosSettingTargetValue" /** * FWUPD_RESULT_KEY_BIOS_SETTING_CURRENT_VALUE: * * Result key to represent the current value of BIOS setting. * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_BIOS_SETTING_CURRENT_VALUE "BiosSettingCurrentValue" /** * FWUPD_RESULT_KEY_BIOS_SETTING_TYPE: * * Result key to represent the type of BIOS setting. * 0 is invalid, 1+ represent an attribute type * * The D-Bus type signature string is 't' i.e. a unsigned 64 bit integer. **/ #define FWUPD_RESULT_KEY_BIOS_SETTING_TYPE "BiosSettingType" /** * FWUPD_RESULT_KEY_BIOS_SETTING_POSSIBLE_VALUES: * * Result key to represent possible values * * The D-Bus type signature string is 'as' i.e. an array of strings. **/ #define FWUPD_RESULT_KEY_BIOS_SETTING_POSSIBLE_VALUES "BiosSettingPossibleValues" /** * FWUPD_RESULT_KEY_BIOS_SETTING_LOWER_BOUND: * * Result key to represent the upper bound for an integer BIOS setting. * or minimum length for string BIOS setting. * The D-Bus type signature string is 't' i.e. a unsigned 64 bit integer. **/ #define FWUPD_RESULT_KEY_BIOS_SETTING_LOWER_BOUND "BiosSettingLowerBound" /** * FWUPD_RESULT_KEY_BIOS_SETTING_UPPER_BOUND: * * Result key to represent the lower bound for an integer BIOS setting * or maximum length for string BIOS setting. * * The D-Bus type signature string is 't' i.e. a unsigned 64 bit integer. **/ #define FWUPD_RESULT_KEY_BIOS_SETTING_UPPER_BOUND "BiosSettingUpperBound" /** * FWUPD_RESULT_KEY_BIOS_SETTING_SCALAR_INCREMENT: * * Result key to represent the scalar increment for an integer BIOS setting. * * The D-Bus type signature string is 't' i.e. a unsigned 64 bit integer. **/ #define FWUPD_RESULT_KEY_BIOS_SETTING_SCALAR_INCREMENT "BiosSettingScalarIncrement" /** * FWUPD_RESULT_KEY_BIOS_SETTING_READ_ONLY: * * Result key to represent whether BIOS setting is read only * * The D-Bus type signature string is 'b' i.e. a boolean. **/ #define FWUPD_RESULT_KEY_BIOS_SETTING_READ_ONLY "BiosSettingReadOnly" /** * FWUPD_RESULT_KEY_KERNEL_CURRENT_VALUE: * * Result key to represent the current kernel setting. * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_KERNEL_CURRENT_VALUE "KernelCurrentValue" /** * FWUPD_RESULT_KEY_KERNEL_TARGET_VALUE: * * Result key to represent the target kernel setting. * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_KERNEL_TARGET_VALUE "KernelTargetValue" /** * FWUPD_RESULT_KEY_DISTRO_ID: * * Result key to represent the distribution ID. * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_DISTRO_ID "DistroId" /** * FWUPD_RESULT_KEY_DISTRO_VARIANT: * * Result key to represent the distribution variant. * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_DISTRO_VARIANT "DistroVariant" /** * FWUPD_RESULT_KEY_DISTRO_VERSION: * * Result key to represent the distribution version. * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_DISTRO_VERSION "DistroVersion" /** * FWUPD_RESULT_KEY_REPORTS: * * Result key to represent an array of reports. * * The D-Bus type signature string is 'a{sv}' i.e. a variant dictionary. **/ #define FWUPD_RESULT_KEY_REPORTS "Reports" /** * FWUPD_RESULT_KEY_DEVICE_NAME: * * Result key to represent the device name. * * The D-Bus type signature string is 's' i.e. a string. **/ #define FWUPD_RESULT_KEY_DEVICE_NAME "DeviceName" G_END_DECLS fwupd-2.0.10/libfwupd/fwupd-enums.c000066400000000000000000000730611501337203100171530ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fwupd-enums.h" /** * fwupd_status_to_string: * @status: a status, e.g. %FWUPD_STATUS_DECOMPRESSING * * Converts an enumerated status to a string. * * Returns: identifier string * * Since: 0.1.1 **/ const gchar * fwupd_status_to_string(FwupdStatus status) { if (status == FWUPD_STATUS_UNKNOWN) return "unknown"; if (status == FWUPD_STATUS_IDLE) return "idle"; if (status == FWUPD_STATUS_DECOMPRESSING) return "decompressing"; if (status == FWUPD_STATUS_LOADING) return "loading"; if (status == FWUPD_STATUS_DEVICE_RESTART) return "device-restart"; if (status == FWUPD_STATUS_DEVICE_WRITE) return "device-write"; if (status == FWUPD_STATUS_DEVICE_READ) return "device-read"; if (status == FWUPD_STATUS_DEVICE_ERASE) return "device-erase"; if (status == FWUPD_STATUS_DEVICE_VERIFY) return "device-verify"; if (status == FWUPD_STATUS_DEVICE_BUSY) return "device-busy"; if (status == FWUPD_STATUS_SCHEDULING) return "scheduling"; if (status == FWUPD_STATUS_DOWNLOADING) return "downloading"; if (status == FWUPD_STATUS_WAITING_FOR_AUTH) return "waiting-for-auth"; if (status == FWUPD_STATUS_SHUTDOWN) return "shutdown"; if (status == FWUPD_STATUS_WAITING_FOR_USER) return "waiting-for-user"; return NULL; } /** * fwupd_status_from_string: * @status: (nullable): a string, e.g. `decompressing` * * Converts a string to an enumerated status. * * Returns: enumerated value * * Since: 0.1.1 **/ FwupdStatus fwupd_status_from_string(const gchar *status) { if (g_strcmp0(status, "unknown") == 0) return FWUPD_STATUS_UNKNOWN; if (g_strcmp0(status, "idle") == 0) return FWUPD_STATUS_IDLE; if (g_strcmp0(status, "decompressing") == 0) return FWUPD_STATUS_DECOMPRESSING; if (g_strcmp0(status, "loading") == 0) return FWUPD_STATUS_LOADING; if (g_strcmp0(status, "device-restart") == 0) return FWUPD_STATUS_DEVICE_RESTART; if (g_strcmp0(status, "device-write") == 0) return FWUPD_STATUS_DEVICE_WRITE; if (g_strcmp0(status, "device-verify") == 0) return FWUPD_STATUS_DEVICE_VERIFY; if (g_strcmp0(status, "scheduling") == 0) return FWUPD_STATUS_SCHEDULING; if (g_strcmp0(status, "downloading") == 0) return FWUPD_STATUS_DOWNLOADING; if (g_strcmp0(status, "device-read") == 0) return FWUPD_STATUS_DEVICE_READ; if (g_strcmp0(status, "device-erase") == 0) return FWUPD_STATUS_DEVICE_ERASE; if (g_strcmp0(status, "device-busy") == 0) return FWUPD_STATUS_DEVICE_BUSY; if (g_strcmp0(status, "waiting-for-auth") == 0) return FWUPD_STATUS_WAITING_FOR_AUTH; if (g_strcmp0(status, "shutdown") == 0) return FWUPD_STATUS_SHUTDOWN; if (g_strcmp0(status, "waiting-for-user") == 0) return FWUPD_STATUS_WAITING_FOR_USER; return FWUPD_STATUS_LAST; } /** * fwupd_device_flag_to_string: * @device_flag: a device flag, e.g. %FWUPD_DEVICE_FLAG_REQUIRE_AC * * Converts a device flag to a string. * * Returns: identifier string * * Since: 0.7.0 **/ const gchar * fwupd_device_flag_to_string(FwupdDeviceFlags device_flag) { if (device_flag == FWUPD_DEVICE_FLAG_NONE) return "none"; if (device_flag == FWUPD_DEVICE_FLAG_INTERNAL) return "internal"; if (device_flag == FWUPD_DEVICE_FLAG_UPDATABLE) return "updatable"; if (device_flag == FWUPD_DEVICE_FLAG_REQUIRE_AC) return "require-ac"; if (device_flag == FWUPD_DEVICE_FLAG_LOCKED) return "locked"; if (device_flag == FWUPD_DEVICE_FLAG_SUPPORTED) return "supported"; if (device_flag == FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER) return "needs-bootloader"; if (device_flag == FWUPD_DEVICE_FLAG_NEEDS_REBOOT) return "needs-reboot"; if (device_flag == FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN) return "needs-shutdown"; if (device_flag == FWUPD_DEVICE_FLAG_REPORTED) return "reported"; if (device_flag == FWUPD_DEVICE_FLAG_NOTIFIED) return "notified"; if (device_flag == FWUPD_DEVICE_FLAG_IS_BOOTLOADER) return "is-bootloader"; if (device_flag == FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG) return "wait-for-replug"; if (device_flag == FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED) return "another-write-required"; if (device_flag == FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION) return "needs-activation"; if (device_flag == FWUPD_DEVICE_FLAG_HISTORICAL) return "historical"; if (device_flag == FWUPD_DEVICE_FLAG_WILL_DISAPPEAR) return "will-disappear"; if (device_flag == FWUPD_DEVICE_FLAG_CAN_VERIFY) return "can-verify"; if (device_flag == FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE) return "can-verify-image"; if (device_flag == FWUPD_DEVICE_FLAG_DUAL_IMAGE) return "dual-image"; if (device_flag == FWUPD_DEVICE_FLAG_SELF_RECOVERY) return "self-recovery"; if (device_flag == FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE) return "usable-during-update"; if (device_flag == FWUPD_DEVICE_FLAG_VERSION_CHECK_REQUIRED) return "version-check-required"; if (device_flag == FWUPD_DEVICE_FLAG_INSTALL_ALL_RELEASES) return "install-all-releases"; if (device_flag == FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN) return "updatable-hidden"; if (device_flag == FWUPD_DEVICE_FLAG_HAS_MULTIPLE_BRANCHES) return "has-multiple-branches"; if (device_flag == FWUPD_DEVICE_FLAG_BACKUP_BEFORE_INSTALL) return "backup-before-install"; if (device_flag == FWUPD_DEVICE_FLAG_WILDCARD_INSTALL) return "wildcard-install"; if (device_flag == FWUPD_DEVICE_FLAG_ONLY_VERSION_UPGRADE) return "only-version-upgrade"; if (device_flag == FWUPD_DEVICE_FLAG_UNREACHABLE) return "unreachable"; if (device_flag == FWUPD_DEVICE_FLAG_AFFECTS_FDE) return "affects-fde"; if (device_flag == FWUPD_DEVICE_FLAG_END_OF_LIFE) return "end-of-life"; if (device_flag == FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD) return "signed-payload"; if (device_flag == FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD) return "unsigned-payload"; if (device_flag == FWUPD_DEVICE_FLAG_EMULATED) return "emulated"; if (device_flag == FWUPD_DEVICE_FLAG_EMULATION_TAG) return "emulation-tag"; if (device_flag == FWUPD_DEVICE_FLAG_ONLY_EXPLICIT_UPDATES) return "only-explicit-updates"; if (device_flag == FWUPD_DEVICE_FLAG_CAN_EMULATION_TAG) return "can-emulation-tag"; if (device_flag == FWUPD_DEVICE_FLAG_INSTALL_SKIP_VERSION_CHECK) return "install-skip-version-check"; return NULL; } /** * fwupd_device_flag_from_string: * @device_flag: (nullable): a string, e.g. `require-ac` * * Converts a string to an enumerated device flag. * * Returns: enumerated value * * Since: 0.7.0 **/ FwupdDeviceFlags fwupd_device_flag_from_string(const gchar *device_flag) { if (g_strcmp0(device_flag, "none") == 0) return FWUPD_DEVICE_FLAG_NONE; if (g_strcmp0(device_flag, "internal") == 0) return FWUPD_DEVICE_FLAG_INTERNAL; if (g_strcmp0(device_flag, "updatable") == 0 || g_strcmp0(device_flag, "allow-online") == 0) return FWUPD_DEVICE_FLAG_UPDATABLE; if (g_strcmp0(device_flag, "require-ac") == 0) return FWUPD_DEVICE_FLAG_REQUIRE_AC; if (g_strcmp0(device_flag, "locked") == 0) return FWUPD_DEVICE_FLAG_LOCKED; if (g_strcmp0(device_flag, "supported") == 0) return FWUPD_DEVICE_FLAG_SUPPORTED; if (g_strcmp0(device_flag, "needs-bootloader") == 0) return FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER; if (g_strcmp0(device_flag, "needs-reboot") == 0) return FWUPD_DEVICE_FLAG_NEEDS_REBOOT; if (g_strcmp0(device_flag, "needs-shutdown") == 0) return FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN; if (g_strcmp0(device_flag, "reported") == 0) return FWUPD_DEVICE_FLAG_REPORTED; if (g_strcmp0(device_flag, "notified") == 0) return FWUPD_DEVICE_FLAG_NOTIFIED; if (g_strcmp0(device_flag, "is-bootloader") == 0) return FWUPD_DEVICE_FLAG_IS_BOOTLOADER; if (g_strcmp0(device_flag, "wait-for-replug") == 0) return FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG; if (g_strcmp0(device_flag, "another-write-required") == 0) return FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED; if (g_strcmp0(device_flag, "needs-activation") == 0) return FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION; if (g_strcmp0(device_flag, "historical") == 0) return FWUPD_DEVICE_FLAG_HISTORICAL; if (g_strcmp0(device_flag, "will-disappear") == 0) return FWUPD_DEVICE_FLAG_WILL_DISAPPEAR; if (g_strcmp0(device_flag, "can-verify") == 0) return FWUPD_DEVICE_FLAG_CAN_VERIFY; if (g_strcmp0(device_flag, "can-verify-image") == 0) return FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE; if (g_strcmp0(device_flag, "dual-image") == 0) return FWUPD_DEVICE_FLAG_DUAL_IMAGE; if (g_strcmp0(device_flag, "self-recovery") == 0) return FWUPD_DEVICE_FLAG_SELF_RECOVERY; if (g_strcmp0(device_flag, "usable-during-update") == 0) return FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE; if (g_strcmp0(device_flag, "version-check-required") == 0) return FWUPD_DEVICE_FLAG_VERSION_CHECK_REQUIRED; if (g_strcmp0(device_flag, "install-all-releases") == 0) return FWUPD_DEVICE_FLAG_INSTALL_ALL_RELEASES; if (g_strcmp0(device_flag, "updatable-hidden") == 0) return FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN; if (g_strcmp0(device_flag, "has-multiple-branches") == 0) return FWUPD_DEVICE_FLAG_HAS_MULTIPLE_BRANCHES; if (g_strcmp0(device_flag, "backup-before-install") == 0) return FWUPD_DEVICE_FLAG_BACKUP_BEFORE_INSTALL; if (g_strcmp0(device_flag, "wildcard-install") == 0) return FWUPD_DEVICE_FLAG_WILDCARD_INSTALL; if (g_strcmp0(device_flag, "only-version-upgrade") == 0) return FWUPD_DEVICE_FLAG_ONLY_VERSION_UPGRADE; if (g_strcmp0(device_flag, "unreachable") == 0) return FWUPD_DEVICE_FLAG_UNREACHABLE; if (g_strcmp0(device_flag, "affects-fde") == 0) return FWUPD_DEVICE_FLAG_AFFECTS_FDE; if (g_strcmp0(device_flag, "end-of-life") == 0) return FWUPD_DEVICE_FLAG_END_OF_LIFE; if (g_strcmp0(device_flag, "signed-payload") == 0) return FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD; if (g_strcmp0(device_flag, "unsigned-payload") == 0) return FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD; if (g_strcmp0(device_flag, "emulated") == 0) return FWUPD_DEVICE_FLAG_EMULATED; if (g_strcmp0(device_flag, "emulation-tag") == 0) return FWUPD_DEVICE_FLAG_EMULATION_TAG; if (g_strcmp0(device_flag, "only-explicit-updates") == 0) return FWUPD_DEVICE_FLAG_ONLY_EXPLICIT_UPDATES; if (g_strcmp0(device_flag, "can-emulation-tag") == 0) return FWUPD_DEVICE_FLAG_CAN_EMULATION_TAG; if (g_strcmp0(device_flag, "install-skip-version-check") == 0) return FWUPD_DEVICE_FLAG_INSTALL_SKIP_VERSION_CHECK; return FWUPD_DEVICE_FLAG_UNKNOWN; } /** * fwupd_device_problem_to_string: * @device_problem: a device inhibit kind, e.g. %FWUPD_DEVICE_PROBLEM_SYSTEM_POWER_TOO_LOW * * Converts a device inhibit kind to a string. * * Returns: identifier string * * Since: 1.8.1 **/ const gchar * fwupd_device_problem_to_string(FwupdDeviceProblem device_problem) { if (device_problem == FWUPD_DEVICE_PROBLEM_NONE) return "none"; if (device_problem == FWUPD_DEVICE_PROBLEM_SYSTEM_POWER_TOO_LOW) return "system-power-too-low"; if (device_problem == FWUPD_DEVICE_PROBLEM_UNREACHABLE) return "unreachable"; if (device_problem == FWUPD_DEVICE_PROBLEM_POWER_TOO_LOW) return "power-too-low"; if (device_problem == FWUPD_DEVICE_PROBLEM_UPDATE_PENDING) return "update-pending"; if (device_problem == FWUPD_DEVICE_PROBLEM_REQUIRE_AC_POWER) return "require-ac-power"; if (device_problem == FWUPD_DEVICE_PROBLEM_LID_IS_CLOSED) return "lid-is-closed"; if (device_problem == FWUPD_DEVICE_PROBLEM_IS_EMULATED) return "is-emulated"; if (device_problem == FWUPD_DEVICE_PROBLEM_MISSING_LICENSE) return "missing-license"; if (device_problem == FWUPD_DEVICE_PROBLEM_SYSTEM_INHIBIT) return "system-inhibit"; if (device_problem == FWUPD_DEVICE_PROBLEM_UPDATE_IN_PROGRESS) return "update-in-progress"; if (device_problem == FWUPD_DEVICE_PROBLEM_IN_USE) return "in-use"; if (device_problem == FWUPD_DEVICE_PROBLEM_DISPLAY_REQUIRED) return "display-required"; if (device_problem == FWUPD_DEVICE_PROBLEM_LOWER_PRIORITY) return "lower-priority"; if (device_problem == FWUPD_DEVICE_PROBLEM_UNKNOWN) return "unknown"; return NULL; } /** * fwupd_device_problem_from_string: * @device_problem: (nullable): a string, e.g. `require-ac` * * Converts a string to a enumerated device inhibit kind. * * Returns: enumerated value * * Since: 1.8.1 **/ FwupdDeviceProblem fwupd_device_problem_from_string(const gchar *device_problem) { if (g_strcmp0(device_problem, "none") == 0) return FWUPD_DEVICE_PROBLEM_NONE; if (g_strcmp0(device_problem, "system-power-too-low") == 0) return FWUPD_DEVICE_PROBLEM_SYSTEM_POWER_TOO_LOW; if (g_strcmp0(device_problem, "unreachable") == 0) return FWUPD_DEVICE_PROBLEM_UNREACHABLE; if (g_strcmp0(device_problem, "power-too-low") == 0) return FWUPD_DEVICE_PROBLEM_POWER_TOO_LOW; if (g_strcmp0(device_problem, "update-pending") == 0) return FWUPD_DEVICE_PROBLEM_UPDATE_PENDING; if (g_strcmp0(device_problem, "require-ac-power") == 0) return FWUPD_DEVICE_PROBLEM_REQUIRE_AC_POWER; if (g_strcmp0(device_problem, "lid-is-closed") == 0) return FWUPD_DEVICE_PROBLEM_LID_IS_CLOSED; if (g_strcmp0(device_problem, "is-emulated") == 0) return FWUPD_DEVICE_PROBLEM_IS_EMULATED; if (g_strcmp0(device_problem, "missing-license") == 0) return FWUPD_DEVICE_PROBLEM_MISSING_LICENSE; if (g_strcmp0(device_problem, "system-inhibit") == 0) return FWUPD_DEVICE_PROBLEM_SYSTEM_INHIBIT; if (g_strcmp0(device_problem, "update-in-progress") == 0) return FWUPD_DEVICE_PROBLEM_UPDATE_IN_PROGRESS; if (g_strcmp0(device_problem, "in-use") == 0) return FWUPD_DEVICE_PROBLEM_IN_USE; if (g_strcmp0(device_problem, "display-required") == 0) return FWUPD_DEVICE_PROBLEM_DISPLAY_REQUIRED; if (g_strcmp0(device_problem, "lower-priority") == 0) return FWUPD_DEVICE_PROBLEM_LOWER_PRIORITY; return FWUPD_DEVICE_PROBLEM_UNKNOWN; } /** * fwupd_plugin_flag_to_string: * @plugin_flag: plugin flags, e.g. %FWUPD_PLUGIN_FLAG_CLEAR_UPDATABLE * * Converts an enumerated plugin flag to a string. * * Returns: identifier string * * Since: 1.5.0 **/ const gchar * fwupd_plugin_flag_to_string(FwupdPluginFlags plugin_flag) { if (plugin_flag == FWUPD_PLUGIN_FLAG_NONE) return "none"; if (plugin_flag == FWUPD_PLUGIN_FLAG_DISABLED) return "disabled"; if (plugin_flag == FWUPD_PLUGIN_FLAG_USER_WARNING) return "user-warning"; if (plugin_flag == FWUPD_PLUGIN_FLAG_CLEAR_UPDATABLE) return "clear-updatable"; if (plugin_flag == FWUPD_PLUGIN_FLAG_NO_HARDWARE) return "no-hardware"; if (plugin_flag == FWUPD_PLUGIN_FLAG_CAPSULES_UNSUPPORTED) return "capsules-unsupported"; if (plugin_flag == FWUPD_PLUGIN_FLAG_UNLOCK_REQUIRED) return "unlock-required"; if (plugin_flag == FWUPD_PLUGIN_FLAG_EFIVAR_NOT_MOUNTED) return "efivar-not-mounted"; if (plugin_flag == FWUPD_PLUGIN_FLAG_ESP_NOT_FOUND) return "esp-not-found"; if (plugin_flag == FWUPD_PLUGIN_FLAG_ESP_NOT_VALID) return "esp-not-valid"; if (plugin_flag == FWUPD_PLUGIN_FLAG_LEGACY_BIOS) return "legacy-bios"; if (plugin_flag == FWUPD_PLUGIN_FLAG_FAILED_OPEN) return "failed-open"; if (plugin_flag == FWUPD_PLUGIN_FLAG_REQUIRE_HWID) return "require-hwid"; if (plugin_flag == FWUPD_PLUGIN_FLAG_KERNEL_TOO_OLD) return "kernel-too-old"; if (plugin_flag == FWUPD_PLUGIN_FLAG_UNKNOWN) return "unknown"; if (plugin_flag == FWUPD_PLUGIN_FLAG_AUTH_REQUIRED) return "auth-required"; if (plugin_flag == FWUPD_PLUGIN_FLAG_SECURE_CONFIG) return "secure-config"; if (plugin_flag == FWUPD_PLUGIN_FLAG_MODULAR) return "modular"; if (plugin_flag == FWUPD_PLUGIN_FLAG_MEASURE_SYSTEM_INTEGRITY) return "measure-system-integrity"; if (plugin_flag == FWUPD_PLUGIN_FLAG_READY) return "ready"; if (plugin_flag == FWUPD_PLUGIN_FLAG_TEST_ONLY) return "test-only"; return NULL; } /** * fwupd_plugin_flag_from_string: * @plugin_flag: (nullable): a string, e.g. `require-ac` * * Converts a string to an enumerated plugin flag. * * Returns: enumerated value * * Since: 1.5.0 **/ FwupdPluginFlags fwupd_plugin_flag_from_string(const gchar *plugin_flag) { if (g_strcmp0(plugin_flag, "none") == 0) return FWUPD_PLUGIN_FLAG_NONE; if (g_strcmp0(plugin_flag, "disabled") == 0) return FWUPD_PLUGIN_FLAG_DISABLED; if (g_strcmp0(plugin_flag, "user-warning") == 0) return FWUPD_PLUGIN_FLAG_USER_WARNING; if (g_strcmp0(plugin_flag, "clear-updatable") == 0) return FWUPD_PLUGIN_FLAG_CLEAR_UPDATABLE; if (g_strcmp0(plugin_flag, "no-hardware") == 0) return FWUPD_PLUGIN_FLAG_NO_HARDWARE; if (g_strcmp0(plugin_flag, "capsules-unsupported") == 0) return FWUPD_PLUGIN_FLAG_CAPSULES_UNSUPPORTED; if (g_strcmp0(plugin_flag, "unlock-required") == 0) return FWUPD_PLUGIN_FLAG_UNLOCK_REQUIRED; if (g_strcmp0(plugin_flag, "efivar-not-mounted") == 0) return FWUPD_PLUGIN_FLAG_EFIVAR_NOT_MOUNTED; if (g_strcmp0(plugin_flag, "esp-not-found") == 0) return FWUPD_PLUGIN_FLAG_ESP_NOT_FOUND; if (g_strcmp0(plugin_flag, "esp-not-valid") == 0) return FWUPD_PLUGIN_FLAG_ESP_NOT_VALID; if (g_strcmp0(plugin_flag, "legacy-bios") == 0) return FWUPD_PLUGIN_FLAG_LEGACY_BIOS; if (g_strcmp0(plugin_flag, "failed-open") == 0) return FWUPD_PLUGIN_FLAG_FAILED_OPEN; if (g_strcmp0(plugin_flag, "require-hwid") == 0) return FWUPD_PLUGIN_FLAG_REQUIRE_HWID; if (g_strcmp0(plugin_flag, "kernel-too-old") == 0) return FWUPD_PLUGIN_FLAG_KERNEL_TOO_OLD; if (g_strcmp0(plugin_flag, "auth-required") == 0) return FWUPD_PLUGIN_FLAG_AUTH_REQUIRED; if (g_strcmp0(plugin_flag, "secure-config") == 0) return FWUPD_PLUGIN_FLAG_SECURE_CONFIG; if (g_strcmp0(plugin_flag, "modular") == 0) return FWUPD_PLUGIN_FLAG_MODULAR; if (g_strcmp0(plugin_flag, "measure-system-integrity") == 0) return FWUPD_PLUGIN_FLAG_MEASURE_SYSTEM_INTEGRITY; if (g_strcmp0(plugin_flag, "ready") == 0) return FWUPD_PLUGIN_FLAG_READY; if (g_strcmp0(plugin_flag, "test-only") == 0) return FWUPD_PLUGIN_FLAG_TEST_ONLY; return FWUPD_PLUGIN_FLAG_UNKNOWN; } /** * fwupd_update_state_to_string: * @update_state: the update state, e.g. %FWUPD_UPDATE_STATE_PENDING * * Converts an enumerated update state to a string. * * Returns: identifier string * * Since: 0.7.0 **/ const gchar * fwupd_update_state_to_string(FwupdUpdateState update_state) { if (update_state == FWUPD_UPDATE_STATE_UNKNOWN) return "unknown"; if (update_state == FWUPD_UPDATE_STATE_PENDING) return "pending"; if (update_state == FWUPD_UPDATE_STATE_SUCCESS) return "success"; if (update_state == FWUPD_UPDATE_STATE_FAILED) return "failed"; if (update_state == FWUPD_UPDATE_STATE_FAILED_TRANSIENT) return "failed-transient"; if (update_state == FWUPD_UPDATE_STATE_NEEDS_REBOOT) return "needs-reboot"; return NULL; } /** * fwupd_update_state_from_string: * @update_state: (nullable): a string, e.g. `pending` * * Converts a string to an enumerated update state. * * Returns: enumerated value * * Since: 0.7.0 **/ FwupdUpdateState fwupd_update_state_from_string(const gchar *update_state) { if (g_strcmp0(update_state, "unknown") == 0) return FWUPD_UPDATE_STATE_UNKNOWN; if (g_strcmp0(update_state, "pending") == 0) return FWUPD_UPDATE_STATE_PENDING; if (g_strcmp0(update_state, "success") == 0) return FWUPD_UPDATE_STATE_SUCCESS; if (g_strcmp0(update_state, "failed") == 0) return FWUPD_UPDATE_STATE_FAILED; if (g_strcmp0(update_state, "failed-transient") == 0) return FWUPD_UPDATE_STATE_FAILED_TRANSIENT; if (g_strcmp0(update_state, "needs-reboot") == 0) return FWUPD_UPDATE_STATE_NEEDS_REBOOT; return FWUPD_UPDATE_STATE_UNKNOWN; } /** * fwupd_feature_flag_to_string: * @feature_flag: a single feature flag, e.g. %FWUPD_FEATURE_FLAG_DETACH_ACTION * * Converts a feature flag to a string. * * Returns: identifier string * * Since: 1.4.5 **/ const gchar * fwupd_feature_flag_to_string(FwupdFeatureFlags feature_flag) { if (feature_flag == FWUPD_FEATURE_FLAG_NONE) return "none"; if (feature_flag == FWUPD_FEATURE_FLAG_CAN_REPORT) return "can-report"; if (feature_flag == FWUPD_FEATURE_FLAG_DETACH_ACTION) return "detach-action"; if (feature_flag == FWUPD_FEATURE_FLAG_UPDATE_ACTION) return "update-action"; if (feature_flag == FWUPD_FEATURE_FLAG_SWITCH_BRANCH) return "switch-branch"; if (feature_flag == FWUPD_FEATURE_FLAG_REQUESTS) return "requests"; if (feature_flag == FWUPD_FEATURE_FLAG_FDE_WARNING) return "fde-warning"; if (feature_flag == FWUPD_FEATURE_FLAG_COMMUNITY_TEXT) return "community-text"; if (feature_flag == FWUPD_FEATURE_FLAG_SHOW_PROBLEMS) return "show-problems"; if (feature_flag == FWUPD_FEATURE_FLAG_ALLOW_AUTHENTICATION) return "allow-authentication"; if (feature_flag == FWUPD_FEATURE_FLAG_REQUESTS_NON_GENERIC) return "requests-non-generic"; return NULL; } /** * fwupd_feature_flag_from_string: * @feature_flag: (nullable): a string, e.g. `detach-action` * * Converts a string to an enumerated feature flag. * * Returns: enumerated value * * Since: 1.4.5 **/ FwupdFeatureFlags fwupd_feature_flag_from_string(const gchar *feature_flag) { if (g_strcmp0(feature_flag, "none") == 0) return FWUPD_FEATURE_FLAG_NONE; if (g_strcmp0(feature_flag, "can-report") == 0) return FWUPD_FEATURE_FLAG_CAN_REPORT; if (g_strcmp0(feature_flag, "detach-action") == 0) return FWUPD_FEATURE_FLAG_DETACH_ACTION; if (g_strcmp0(feature_flag, "update-action") == 0) return FWUPD_FEATURE_FLAG_UPDATE_ACTION; if (g_strcmp0(feature_flag, "switch-branch") == 0) return FWUPD_FEATURE_FLAG_SWITCH_BRANCH; if (g_strcmp0(feature_flag, "requests") == 0) return FWUPD_FEATURE_FLAG_REQUESTS; if (g_strcmp0(feature_flag, "fde-warning") == 0) return FWUPD_FEATURE_FLAG_FDE_WARNING; if (g_strcmp0(feature_flag, "community-text") == 0) return FWUPD_FEATURE_FLAG_COMMUNITY_TEXT; if (g_strcmp0(feature_flag, "show-problems") == 0) return FWUPD_FEATURE_FLAG_SHOW_PROBLEMS; if (g_strcmp0(feature_flag, "allow-authentication") == 0) return FWUPD_FEATURE_FLAG_ALLOW_AUTHENTICATION; if (g_strcmp0(feature_flag, "requests-non-generic") == 0) return FWUPD_FEATURE_FLAG_REQUESTS_NON_GENERIC; return FWUPD_FEATURE_FLAG_UNKNOWN; } /** * fwupd_release_flag_to_string: * @release_flag: a release flag, e.g. %FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD * * Converts an enumerated release flag to a string. * * Returns: identifier string * * Since: 1.2.6 **/ const gchar * fwupd_release_flag_to_string(FwupdReleaseFlags release_flag) { if (release_flag == FWUPD_RELEASE_FLAG_NONE) return "none"; if (release_flag == FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD) return "trusted-payload"; if (release_flag == FWUPD_RELEASE_FLAG_TRUSTED_METADATA) return "trusted-metadata"; if (release_flag == FWUPD_RELEASE_FLAG_IS_UPGRADE) return "is-upgrade"; if (release_flag == FWUPD_RELEASE_FLAG_IS_DOWNGRADE) return "is-downgrade"; if (release_flag == FWUPD_RELEASE_FLAG_BLOCKED_VERSION) return "blocked-version"; if (release_flag == FWUPD_RELEASE_FLAG_BLOCKED_APPROVAL) return "blocked-approval"; if (release_flag == FWUPD_RELEASE_FLAG_IS_ALTERNATE_BRANCH) return "is-alternate-branch"; if (release_flag == FWUPD_RELEASE_FLAG_IS_COMMUNITY) return "is-community"; if (release_flag == FWUPD_RELEASE_FLAG_TRUSTED_REPORT) return "trusted-report"; return NULL; } /** * fwupd_release_flag_from_string: * @release_flag: (nullable): a string, e.g. `trusted-payload` * * Converts a string to an enumerated release flag. * * Returns: enumerated value * * Since: 1.2.6 **/ FwupdReleaseFlags fwupd_release_flag_from_string(const gchar *release_flag) { if (g_strcmp0(release_flag, "trusted-payload") == 0) return FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD; if (g_strcmp0(release_flag, "trusted-metadata") == 0) return FWUPD_RELEASE_FLAG_TRUSTED_METADATA; if (g_strcmp0(release_flag, "is-upgrade") == 0) return FWUPD_RELEASE_FLAG_IS_UPGRADE; if (g_strcmp0(release_flag, "is-downgrade") == 0) return FWUPD_RELEASE_FLAG_IS_DOWNGRADE; if (g_strcmp0(release_flag, "blocked-version") == 0) return FWUPD_RELEASE_FLAG_BLOCKED_VERSION; if (g_strcmp0(release_flag, "blocked-approval") == 0) return FWUPD_RELEASE_FLAG_BLOCKED_APPROVAL; if (g_strcmp0(release_flag, "is-alternate-branch") == 0) return FWUPD_RELEASE_FLAG_IS_ALTERNATE_BRANCH; if (g_strcmp0(release_flag, "is-community") == 0) return FWUPD_RELEASE_FLAG_IS_COMMUNITY; if (g_strcmp0(release_flag, "trusted-report") == 0) return FWUPD_RELEASE_FLAG_TRUSTED_REPORT; return FWUPD_RELEASE_FLAG_NONE; } /** * fwupd_release_urgency_to_string: * @release_urgency: a release urgency, e.g. %FWUPD_RELEASE_URGENCY_HIGH * * Converts an enumerated release urgency to a string. * * Returns: identifier string * * Since: 1.4.0 **/ const gchar * fwupd_release_urgency_to_string(FwupdReleaseUrgency release_urgency) { if (release_urgency == FWUPD_RELEASE_URGENCY_LOW) return "low"; if (release_urgency == FWUPD_RELEASE_URGENCY_MEDIUM) return "medium"; if (release_urgency == FWUPD_RELEASE_URGENCY_HIGH) return "high"; if (release_urgency == FWUPD_RELEASE_URGENCY_CRITICAL) return "critical"; return NULL; } /** * fwupd_release_urgency_from_string: * @release_urgency: (nullable): a string, e.g. `low` * * Converts a string to an enumerated release urgency value. * * Returns: enumerated value * * Since: 1.4.0 **/ FwupdReleaseUrgency fwupd_release_urgency_from_string(const gchar *release_urgency) { if (g_strcmp0(release_urgency, "low") == 0) return FWUPD_RELEASE_URGENCY_LOW; if (g_strcmp0(release_urgency, "medium") == 0) return FWUPD_RELEASE_URGENCY_MEDIUM; if (g_strcmp0(release_urgency, "high") == 0) return FWUPD_RELEASE_URGENCY_HIGH; if (g_strcmp0(release_urgency, "critical") == 0) return FWUPD_RELEASE_URGENCY_CRITICAL; return FWUPD_RELEASE_URGENCY_UNKNOWN; } /** * fwupd_version_format_from_string: * @str: (nullable): a string, e.g. `quad` * * Converts text to a display version type. * * Returns: an enumerated version format, e.g. %FWUPD_VERSION_FORMAT_TRIPLET * * Since: 1.2.9 **/ FwupdVersionFormat fwupd_version_format_from_string(const gchar *str) { if (g_strcmp0(str, "plain") == 0) return FWUPD_VERSION_FORMAT_PLAIN; if (g_strcmp0(str, "pair") == 0) return FWUPD_VERSION_FORMAT_PAIR; if (g_strcmp0(str, "number") == 0) return FWUPD_VERSION_FORMAT_NUMBER; if (g_strcmp0(str, "triplet") == 0) return FWUPD_VERSION_FORMAT_TRIPLET; if (g_strcmp0(str, "quad") == 0) return FWUPD_VERSION_FORMAT_QUAD; if (g_strcmp0(str, "bcd") == 0) return FWUPD_VERSION_FORMAT_BCD; if (g_strcmp0(str, "intel-me") == 0) return FWUPD_VERSION_FORMAT_INTEL_ME; if (g_strcmp0(str, "intel-me2") == 0) return FWUPD_VERSION_FORMAT_INTEL_ME2; if (g_strcmp0(str, "intel-csme19") == 0) return FWUPD_VERSION_FORMAT_INTEL_CSME19; if (g_strcmp0(str, "surface-legacy") == 0) return FWUPD_VERSION_FORMAT_SURFACE_LEGACY; if (g_strcmp0(str, "surface") == 0) return FWUPD_VERSION_FORMAT_SURFACE; if (g_strcmp0(str, "dell-bios") == 0) return FWUPD_VERSION_FORMAT_DELL_BIOS; if (g_strcmp0(str, "hex") == 0) return FWUPD_VERSION_FORMAT_HEX; if (g_strcmp0(str, "dell-bios-msb") == 0) return FWUPD_VERSION_FORMAT_DELL_BIOS_MSB; return FWUPD_VERSION_FORMAT_UNKNOWN; } /** * fwupd_version_format_to_string: * @kind: a version format, e.g. %FWUPD_VERSION_FORMAT_TRIPLET * * Converts an enumerated version format to text. * * Returns: a string, e.g. `quad`, or %NULL if not known * * Since: 1.2.9 **/ const gchar * fwupd_version_format_to_string(FwupdVersionFormat kind) { if (kind == FWUPD_VERSION_FORMAT_PLAIN) return "plain"; if (kind == FWUPD_VERSION_FORMAT_NUMBER) return "number"; if (kind == FWUPD_VERSION_FORMAT_PAIR) return "pair"; if (kind == FWUPD_VERSION_FORMAT_TRIPLET) return "triplet"; if (kind == FWUPD_VERSION_FORMAT_QUAD) return "quad"; if (kind == FWUPD_VERSION_FORMAT_BCD) return "bcd"; if (kind == FWUPD_VERSION_FORMAT_INTEL_ME) return "intel-me"; if (kind == FWUPD_VERSION_FORMAT_INTEL_ME2) return "intel-me2"; if (kind == FWUPD_VERSION_FORMAT_INTEL_CSME19) return "intel-csme19"; if (kind == FWUPD_VERSION_FORMAT_SURFACE_LEGACY) return "surface-legacy"; if (kind == FWUPD_VERSION_FORMAT_SURFACE) return "surface"; if (kind == FWUPD_VERSION_FORMAT_DELL_BIOS) return "dell-bios"; if (kind == FWUPD_VERSION_FORMAT_HEX) return "hex"; if (kind == FWUPD_VERSION_FORMAT_DELL_BIOS_MSB) return "dell-bios-msb"; return NULL; } /** * fwupd_install_flags_from_string: * @str: (nullable): a string, e.g. `allow-reinstall` * * Converts text to an install flag * * Returns: an enumerated install flag, e.g. %FWUPD_INSTALL_FLAG_ALLOW_REINSTALL * * Since: 2.0.4 **/ FwupdInstallFlags fwupd_install_flags_from_string(const gchar *str) { if (g_strcmp0(str, "none") == 0) return FWUPD_INSTALL_FLAG_NONE; if (g_strcmp0(str, "allow-reinstall") == 0) return FWUPD_INSTALL_FLAG_ALLOW_REINSTALL; if (g_strcmp0(str, "allow-older") == 0) return FWUPD_INSTALL_FLAG_ALLOW_OLDER; if (g_strcmp0(str, "force") == 0) return FWUPD_INSTALL_FLAG_FORCE; if (g_strcmp0(str, "no-history") == 0) return FWUPD_INSTALL_FLAG_NO_HISTORY; if (g_strcmp0(str, "allow-branch-switch") == 0) return FWUPD_INSTALL_FLAG_ALLOW_BRANCH_SWITCH; if (g_strcmp0(str, "ignore-requirements") == 0) return FWUPD_INSTALL_FLAG_IGNORE_REQUIREMENTS; if (g_strcmp0(str, "only-emulated") == 0) return FWUPD_INSTALL_FLAG_ONLY_EMULATED; return FWUPD_INSTALL_FLAG_UNKNOWN; } /** * fwupd_install_flags_to_string: * @install_flags: a #FwupdInstallFlags, e.g. %FWUPD_INSTALL_FLAG_FORCE * * Converts an install flag to text. * * Returns: a string, e.g. `force`, or %NULL if not known * * Since: 2.0.0 **/ const gchar * fwupd_install_flags_to_string(FwupdInstallFlags install_flags) { if (install_flags == FWUPD_INSTALL_FLAG_NONE) return "none"; if (install_flags == FWUPD_INSTALL_FLAG_ALLOW_REINSTALL) return "allow-reinstall"; if (install_flags == FWUPD_INSTALL_FLAG_ALLOW_OLDER) return "allow-older"; if (install_flags == FWUPD_INSTALL_FLAG_FORCE) return "force"; if (install_flags == FWUPD_INSTALL_FLAG_NO_HISTORY) return "no-history"; if (install_flags == FWUPD_INSTALL_FLAG_ALLOW_BRANCH_SWITCH) return "allow-branch-switch"; if (install_flags == FWUPD_INSTALL_FLAG_IGNORE_REQUIREMENTS) return "ignore-requirements"; if (install_flags == FWUPD_INSTALL_FLAG_ONLY_EMULATED) return "only-emulated"; return NULL; } fwupd-2.0.10/libfwupd/fwupd-enums.h000066400000000000000000000755001501337203100171600ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_BEGIN_DECLS /** * FwupdStatus: * * The flags to show daemon status. **/ typedef enum { /** * FWUPD_STATUS_UNKNOWN: * * Unknown state. * * Since: 0.1.1 */ FWUPD_STATUS_UNKNOWN, /** * FWUPD_STATUS_IDLE: * * Idle. * * Since: 0.1.1 */ FWUPD_STATUS_IDLE, /** * FWUPD_STATUS_LOADING: * * Loading a resource. * * Since: 0.1.1 */ FWUPD_STATUS_LOADING, /** * FWUPD_STATUS_DECOMPRESSING: * * Decompressing firmware. * * Since: 0.1.1 */ FWUPD_STATUS_DECOMPRESSING, /** * FWUPD_STATUS_DEVICE_RESTART: * * Restarting the device. * * Since: 0.1.1 */ FWUPD_STATUS_DEVICE_RESTART, /** * FWUPD_STATUS_DEVICE_WRITE: * * Writing to a device. * * Since: 0.1.1 */ FWUPD_STATUS_DEVICE_WRITE, /** * FWUPD_STATUS_DEVICE_VERIFY: * * Verifying (reading) a device. * * Since: 0.1.1 */ FWUPD_STATUS_DEVICE_VERIFY, /** * FWUPD_STATUS_SCHEDULING: * * Scheduling an update for installation on reboot. * * Since: 0.1.1 */ FWUPD_STATUS_SCHEDULING, /** * FWUPD_STATUS_DOWNLOADING: * * A file is downloading. * * Since: 0.9.4 */ FWUPD_STATUS_DOWNLOADING, /** * FWUPD_STATUS_DEVICE_READ: * * Reading from a device. * * Since: 1.0.0 */ FWUPD_STATUS_DEVICE_READ, /** * FWUPD_STATUS_DEVICE_ERASE: * * Erasing a device. * * Since: 1.0.0 */ FWUPD_STATUS_DEVICE_ERASE, /** * FWUPD_STATUS_WAITING_FOR_AUTH: * * Waiting for authentication. * * Since: 1.0.0 */ FWUPD_STATUS_WAITING_FOR_AUTH, /** * FWUPD_STATUS_DEVICE_BUSY: * * The device is busy. * * Since: 1.0.1 */ FWUPD_STATUS_DEVICE_BUSY, /** * FWUPD_STATUS_SHUTDOWN: * * The daemon is shutting down. * * Since: 1.2.1 */ FWUPD_STATUS_SHUTDOWN, /** * FWUPD_STATUS_WAITING_FOR_USER: * * Waiting for an interactive user action. * * Since: 1.9.8 */ FWUPD_STATUS_WAITING_FOR_USER, /*< private >*/ FWUPD_STATUS_LAST } FwupdStatus; /** * FwupdFeatureFlags: * * The flags to the feature capabilities of the front-end client. **/ typedef enum { /** * FWUPD_FEATURE_FLAG_NONE: * * No trust. * * Since: 1.4.5 */ FWUPD_FEATURE_FLAG_NONE = 0, /** * FWUPD_FEATURE_FLAG_CAN_REPORT: * * Can upload a report of the update back to the server. * * Since: 1.4.5 */ FWUPD_FEATURE_FLAG_CAN_REPORT = 1 << 0, /** * FWUPD_FEATURE_FLAG_DETACH_ACTION: * * Can perform detach action, typically showing text. * * Since: 1.4.5 */ FWUPD_FEATURE_FLAG_DETACH_ACTION = 1 << 1, /** * FWUPD_FEATURE_FLAG_UPDATE_ACTION: * * Can perform update action, typically showing text. * * Since: 1.4.5 */ FWUPD_FEATURE_FLAG_UPDATE_ACTION = 1 << 2, /** * FWUPD_FEATURE_FLAG_SWITCH_BRANCH: * * Can switch the firmware branch. * * Since: 1.5.0 */ FWUPD_FEATURE_FLAG_SWITCH_BRANCH = 1 << 3, /** * FWUPD_FEATURE_FLAG_REQUESTS: * * Can show interactive requests. * * Since: 1.6.2 */ FWUPD_FEATURE_FLAG_REQUESTS = 1 << 4, /** * FWUPD_FEATURE_FLAG_FDE_WARNING: * * Can warn about full disk encryption. * * Since: 1.7.1 */ FWUPD_FEATURE_FLAG_FDE_WARNING = 1 << 5, /** * FWUPD_FEATURE_FLAG_COMMUNITY_TEXT: * * Can show information about community supported. * * Since: 1.7.5 */ FWUPD_FEATURE_FLAG_COMMUNITY_TEXT = 1 << 6, /** * FWUPD_FEATURE_FLAG_SHOW_PROBLEMS: * * Can show problems when getting the update list. * * Since: 1.8.1 */ FWUPD_FEATURE_FLAG_SHOW_PROBLEMS = 1 << 7, /** * FWUPD_FEATURE_FLAG_ALLOW_AUTHENTICATION: * * Can authenticate with PolicyKit for requests. * * Since: 1.8.4 */ FWUPD_FEATURE_FLAG_ALLOW_AUTHENTICATION = 1 << 8, /** * FWUPD_FEATURE_FLAG_REQUESTS_NON_GENERIC: * * Can handle showing non-generic request message text. * * Since: 1.9.8 */ FWUPD_FEATURE_FLAG_REQUESTS_NON_GENERIC = 1 << 9, /* Since: 1.9.8 */ /*< private >*/ FWUPD_FEATURE_FLAG_UNKNOWN = G_MAXUINT64, } FwupdFeatureFlags; /** * FwupdDeviceFlags: * * Flags used to represent device attributes */ typedef enum { /** * FWUPD_DEVICE_FLAG_NONE: * * No flags set * * Since: 0.1.3 */ FWUPD_DEVICE_FLAG_NONE = 0ull, /** * FWUPD_DEVICE_FLAG_INTERNAL: * * Device is internal to the platform and cannot be removed easily. * * Since: 0.1.3 */ FWUPD_DEVICE_FLAG_INTERNAL = 1ull << 0, /** * FWUPD_DEVICE_FLAG_UPDATABLE: * * Device has the ability to be updated in this or any other mode. * * Since: 0.9.7 */ FWUPD_DEVICE_FLAG_UPDATABLE = 1ull << 1, /** * FWUPD_DEVICE_FLAG_REQUIRE_AC: * * Device requires an external power source to be connected or the battery * level at a minimum threshold to update. * * Since: 0.6.3 */ FWUPD_DEVICE_FLAG_REQUIRE_AC = 1ull << 3, /** * FWUPD_DEVICE_FLAG_LOCKED: * * The device can not be updated without manual user interaction. * * Since: 0.6.3 */ FWUPD_DEVICE_FLAG_LOCKED = 1ull << 4, /** * FWUPD_DEVICE_FLAG_SUPPORTED: * * The device is found in metadata loaded into the daemon. * * Since: 0.7.1 */ FWUPD_DEVICE_FLAG_SUPPORTED = 1ull << 5, /** * FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER: * * The device requires entering a bootloader mode to be manually. * * Since: 0.7.3 */ FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER = 1ull << 6, /** * FWUPD_DEVICE_FLAG_NEEDS_REBOOT: * * The device requires a system reboot to apply firmware or to reload hardware. * * Since: 0.9.7 */ FWUPD_DEVICE_FLAG_NEEDS_REBOOT = 1ull << 8, /** * FWUPD_DEVICE_FLAG_REPORTED: * * The success or failure of a previous update has been reported to a metadata server. * * Since: 1.0.4 */ FWUPD_DEVICE_FLAG_REPORTED = 1ull << 9, /** * FWUPD_DEVICE_FLAG_NOTIFIED: * * The user has been notified about a change in the device state. * * Since: 1.0.5 */ FWUPD_DEVICE_FLAG_NOTIFIED = 1ull << 10, /** * FWUPD_DEVICE_FLAG_IS_BOOTLOADER: * * The device is currently in a read-only bootloader mode and not running application code. * * Since: 1.0.8 */ FWUPD_DEVICE_FLAG_IS_BOOTLOADER = 1ull << 13, /** * FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG: * * The device is in the middle of and update and the hardware is waiting to be probed or * replugged. * * Since: 1.1.2 */ FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG = 1ull << 14, /** * FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN: * * The device requires the system to be shutdown to finish application of new firmware. * * Since: 1.2.4 */ FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN = 1ull << 17, /** * FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED: * * The device requires the update to be retried, possibly with a different plugin. * * Since: 1.2.5 */ FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED = 1ull << 18, /** * FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION: * * The device update needs to be separately activated. * This process may occur automatically on shutdown in some operating systems or when the * device is unplugged with some devices. * * Since: 1.2.6 */ FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION = 1ull << 20, /** * FWUPD_DEVICE_FLAG_HISTORICAL: * * The device is used for historical data only. * * Since: 1.3.2 */ FWUPD_DEVICE_FLAG_HISTORICAL = 1ull << 22, /** * FWUPD_DEVICE_FLAG_WILL_DISAPPEAR: * * The device will disappear after the update is complete and success or failure can't be * verified. * * Since: 1.3.3 */ FWUPD_DEVICE_FLAG_WILL_DISAPPEAR = 1ull << 24, /** * FWUPD_DEVICE_FLAG_CAN_VERIFY: * * The device checksums can be compared against metadata. * * Since: 1.3.3 */ FWUPD_DEVICE_FLAG_CAN_VERIFY = 1ull << 25, /** * FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE: * * The device application firmware image can be dumped from device for verification. * * Since: 1.3.3 */ FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE = 1ull << 26, /** * FWUPD_DEVICE_FLAG_DUAL_IMAGE: * * The device firmware update architecture uses a redundancy mechanism such as A/B * partitions for updates. * * Since: 1.3.3 */ FWUPD_DEVICE_FLAG_DUAL_IMAGE = 1ull << 27, /** * FWUPD_DEVICE_FLAG_SELF_RECOVERY: * * In flashing mode, the device will only accept intended payloads and will revert back to * a valid firmware image if an invalid or incomplete payload was sent. * * Since: 1.3.3 */ FWUPD_DEVICE_FLAG_SELF_RECOVERY = 1ull << 28, /** * FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE: * * The device remains usable while the update flashes or schedules the update. * The update will implicitly be applied next time the device is power cycled or possibly * activated. * * Since: 1.3.3 */ FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE = 1ull << 29, /** * FWUPD_DEVICE_FLAG_VERSION_CHECK_REQUIRED: * * All firmware updates for this device require a firmware version check. * * Since: 1.3.7 */ FWUPD_DEVICE_FLAG_VERSION_CHECK_REQUIRED = 1ull << 30, /** * FWUPD_DEVICE_FLAG_INSTALL_ALL_RELEASES: * * Install each intermediate releases for the device rather than jumping directly to the * newest. * * Since: 1.3.7 */ FWUPD_DEVICE_FLAG_INSTALL_ALL_RELEASES = 1ull << 31, /** * FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN: * * The device is updatable but is currently inhibited from updates in the client. * Reasons include but are not limited to low power or requiring reboot from a previous * update. * * Since: 1.4.1 */ FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN = 1ull << 37, /** * FWUPD_DEVICE_FLAG_HAS_MULTIPLE_BRANCHES: * * The device supports switching to a different stream of firmware. * * Since: 1.5.0 */ FWUPD_DEVICE_FLAG_HAS_MULTIPLE_BRANCHES = 1ull << 39, /** * FWUPD_DEVICE_FLAG_BACKUP_BEFORE_INSTALL: * * The device firmware should be saved before installing firmware. * * Since: 1.5.0 */ FWUPD_DEVICE_FLAG_BACKUP_BEFORE_INSTALL = 1ull << 40, /** * FWUPD_DEVICE_FLAG_WILDCARD_INSTALL: * * All devices with matching GUIDs will be updated at the same time. * * For some devices it is not possible to have different versions of firmware * for hardware of the same type. Updating one device will force update of * others with exactly the same instance IDs. * * Since: 1.6.2 */ FWUPD_DEVICE_FLAG_WILDCARD_INSTALL = 1ull << 42, /** * FWUPD_DEVICE_FLAG_ONLY_VERSION_UPGRADE: * * The device firmware can only be updated to a newer version and never downgraded or * reinstalled. * * Since: 1.6.2 */ FWUPD_DEVICE_FLAG_ONLY_VERSION_UPGRADE = 1ull << 43, /** * FWUPD_DEVICE_FLAG_UNREACHABLE: * * The device is currently unreachable, perhaps because it is in a lower power state or is * out of wireless range. * * Since: 1.7.0 */ FWUPD_DEVICE_FLAG_UNREACHABLE = 1ull << 44, /** * FWUPD_DEVICE_FLAG_AFFECTS_FDE: * * The device is warning that a volume with full-disk-encryption was found on this machine, * typically a Windows NTFS partition with BitLocker. * Updating the firmware on this device may invalidate secrets used to decrypt the volume, * and the recovery key may be required. * * Supported clients will display this information as a warning to the user. * * Since: 1.7.1 */ FWUPD_DEVICE_FLAG_AFFECTS_FDE = 1ull << 45, /** * FWUPD_DEVICE_FLAG_END_OF_LIFE: * * The device is no longer supported by the original hardware vendor as it is considered * end-of-life. It it unlikely to receive firmware updates, even for security issues. * * Since: 1.7.5 */ FWUPD_DEVICE_FLAG_END_OF_LIFE = 1ull << 46, /** * FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD: * * The firmware payload is verified on-device the payload using strong cryptography such * as RSA, AES or ECC. * * It is usually not possible to modify or flash custom firmware not provided by the vendor. * * Since: 1.7.6 */ FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD = 1ull << 47, /** * FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD: * * The firmware payload is unsigned and it is possible to modify and flash custom firmware. * * Since: 1.7.6 */ FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD = 1ull << 48, /** * FWUPD_DEVICE_FLAG_EMULATED: * * The device is emulated and should not be recorded by the backend. * * Since: 1.8.11 */ FWUPD_DEVICE_FLAG_EMULATED = 1ull << 49, /** * FWUPD_DEVICE_FLAG_EMULATION_TAG: * * The device should be recorded by the backend, allowing emulation. * * Since: 1.8.11 */ FWUPD_DEVICE_FLAG_EMULATION_TAG = 1ull << 50, /** * FWUPD_DEVICE_FLAG_ONLY_EXPLICIT_UPDATES: * * The device should stay on one firmware version unless the new version is explicitly * specified. * * This can either be done using `fwupdmgr install`, using GNOME Firmware, or using a BKC * config. * * Since: 1.9.3 */ FWUPD_DEVICE_FLAG_ONLY_EXPLICIT_UPDATES = 1ull << 51, /** * FWUPD_DEVICE_FLAG_CAN_EMULATION_TAG: * * The device can be recorded by the backend, allowing emulation. * * Since: 2.0.1 */ FWUPD_DEVICE_FLAG_CAN_EMULATION_TAG = 1ull << 52, /** * FWUPD_DEVICE_FLAG_INSTALL_SKIP_VERSION_CHECK: * * The device doesn't require verification of the newly installed version. * * Since: 2.0.2 */ FWUPD_DEVICE_FLAG_INSTALL_SKIP_VERSION_CHECK = 1ull << 53, /** * FWUPD_DEVICE_FLAG_UNKNOWN: * * This flag is not defined, this typically will happen from mismatched fwupd library and * clients. * * Since: 0.7.3 */ /*< private >*/ FWUPD_DEVICE_FLAG_UNKNOWN = G_MAXUINT64, } FwupdDeviceFlags; /** * FwupdDeviceProblem: * * Problems are reasons why the device is not updatable. * * All problems have to be fixable by the user, rather than the plugin author. */ typedef enum { /** * FWUPD_DEVICE_PROBLEM_NONE: * * No device problems detected. * * Since: 1.8.1 */ FWUPD_DEVICE_PROBLEM_NONE = 0u, /** * FWUPD_DEVICE_PROBLEM_SYSTEM_POWER_TOO_LOW: * * The system power is too low to perform the update. * * Since: 1.8.1 */ FWUPD_DEVICE_PROBLEM_SYSTEM_POWER_TOO_LOW = 1ull << 0, /** * FWUPD_DEVICE_PROBLEM_UNREACHABLE: * * The device is unreachable, or out of wireless range. * * Since: 1.8.1 */ FWUPD_DEVICE_PROBLEM_UNREACHABLE = 1ull << 1, /** * FWUPD_DEVICE_PROBLEM_POWER_TOO_LOW: * * The device battery power is too low. * * Since: 1.8.1 */ FWUPD_DEVICE_PROBLEM_POWER_TOO_LOW = 1ull << 2, /** * FWUPD_DEVICE_PROBLEM_UPDATE_PENDING: * * The device is waiting for the update to be applied. * * Since: 1.8.1 */ FWUPD_DEVICE_PROBLEM_UPDATE_PENDING = 1ull << 3, /** * FWUPD_DEVICE_PROBLEM_REQUIRE_AC_POWER: * * The device requires AC power to be connected. * * Since: 1.8.1 */ FWUPD_DEVICE_PROBLEM_REQUIRE_AC_POWER = 1ull << 4, /** * FWUPD_DEVICE_PROBLEM_LID_IS_CLOSED: * * The device cannot be used while the laptop lid is closed. * * Since: 1.8.1 */ FWUPD_DEVICE_PROBLEM_LID_IS_CLOSED = 1ull << 5, /** * FWUPD_DEVICE_PROBLEM_IS_EMULATED: * * The device is emulated from a different host. * * Since: 1.8.3 */ FWUPD_DEVICE_PROBLEM_IS_EMULATED = 1ull << 6, /** * FWUPD_DEVICE_PROBLEM_MISSING_LICENSE: * * The device cannot be updated due to missing vendor's license. * * Since: 1.8.6 */ FWUPD_DEVICE_PROBLEM_MISSING_LICENSE = 1ull << 7, /** * FWUPD_DEVICE_PROBLEM_SYSTEM_INHIBIT: * * The device cannot be updated due to a system-wide inhibit. * * Since: 1.8.10 */ FWUPD_DEVICE_PROBLEM_SYSTEM_INHIBIT = 1ull << 8, /** * FWUPD_DEVICE_PROBLEM_UPDATE_IN_PROGRESS: * * The device cannot be updated as it is already being updated. * * Since: 1.8.11 */ FWUPD_DEVICE_PROBLEM_UPDATE_IN_PROGRESS = 1ull << 9, /** * FWUPD_DEVICE_PROBLEM_IN_USE: * * The device is in use and cannot be interrupted, for instance taking a phone call. * * Since: 1.9.1 */ FWUPD_DEVICE_PROBLEM_IN_USE = 1ull << 10, /** * FWUPD_DEVICE_PROBLEM_DISPLAY_REQUIRED: * * The device cannot be used while there are no displays plugged in. * * Since: 1.9.6 */ FWUPD_DEVICE_PROBLEM_DISPLAY_REQUIRED = 1ull << 11, /** * FWUPD_DEVICE_PROBLEM_LOWER_PRIORITY: * * We have two ways of communicating with one physical device, so we hide the worse one. * * Since: 2.0.0 */ FWUPD_DEVICE_PROBLEM_LOWER_PRIORITY = 1ull << 12, /** * FWUPD_DEVICE_PROBLEM_UNKNOWN: * * This problem is not defined, this typically will happen from mismatched * fwupd library and clients. * * Since: 1.8.1 */ FWUPD_DEVICE_PROBLEM_UNKNOWN = G_MAXUINT64, } FwupdDeviceProblem; /** * FwupdReleaseFlags: * * Flags used to represent release attributes */ typedef enum { /** * FWUPD_RELEASE_FLAG_NONE: * * No flags are set. * * Since: 1.2.6 */ FWUPD_RELEASE_FLAG_NONE = 0u, /** * FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD: * * The payload binary is trusted. * * Since: 1.2.6 */ FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD = 1ull << 0, /** * FWUPD_RELEASE_FLAG_TRUSTED_METADATA: * * The payload metadata is trusted. * * Since: 1.2.6 */ FWUPD_RELEASE_FLAG_TRUSTED_METADATA = 1ull << 1, /** * FWUPD_RELEASE_FLAG_IS_UPGRADE: * * The release is newer than the device version. * * Since: 1.2.6 */ FWUPD_RELEASE_FLAG_IS_UPGRADE = 1ull << 2, /** * FWUPD_RELEASE_FLAG_IS_DOWNGRADE: * * The release is older than the device version. * * Since: 1.2.6 */ FWUPD_RELEASE_FLAG_IS_DOWNGRADE = 1ull << 3, /** * FWUPD_RELEASE_FLAG_BLOCKED_VERSION: * * The installation of the release is blocked as below device version-lowest. * * Since: 1.2.6 */ FWUPD_RELEASE_FLAG_BLOCKED_VERSION = 1ull << 4, /** * FWUPD_RELEASE_FLAG_BLOCKED_APPROVAL: * * The installation of the release is blocked as release not approved by an administrator. * * Since: 1.2.6 */ FWUPD_RELEASE_FLAG_BLOCKED_APPROVAL = 1ull << 5, /** * FWUPD_RELEASE_FLAG_IS_ALTERNATE_BRANCH: * * The release is an alternate branch of firmware. * * Since: 1.5.0 */ FWUPD_RELEASE_FLAG_IS_ALTERNATE_BRANCH = 1ull << 6, /** * FWUPD_RELEASE_FLAG_IS_COMMUNITY: * * The release is supported by the community and not the hardware vendor. * * Since: 1.7.5 */ FWUPD_RELEASE_FLAG_IS_COMMUNITY = 1ull << 7, /** * FWUPD_RELEASE_FLAG_TRUSTED_REPORT: * * The payload has been tested by a report we trust. * * Since: 1.9.1 */ FWUPD_RELEASE_FLAG_TRUSTED_REPORT = 1ull << 8, /** * FWUPD_RELEASE_FLAG_UNKNOWN: * * The release flag is unknown, typically caused by using mismatched client and daemon. * * Since: 1.2.6 */ FWUPD_RELEASE_FLAG_UNKNOWN = G_MAXUINT64, } FwupdReleaseFlags; /** * FwupdReleaseUrgency: * @FWUPD_RELEASE_URGENCY_UNKNOWN: Unknown * @FWUPD_RELEASE_URGENCY_LOW: Low * @FWUPD_RELEASE_URGENCY_MEDIUM: Medium * @FWUPD_RELEASE_URGENCY_HIGH: High * @FWUPD_RELEASE_URGENCY_CRITICAL: Critical, e.g. a security fix * * The release urgency. **/ typedef enum { FWUPD_RELEASE_URGENCY_UNKNOWN, /* Since: 1.4.0 */ FWUPD_RELEASE_URGENCY_LOW, /* Since: 1.4.0 */ FWUPD_RELEASE_URGENCY_MEDIUM, /* Since: 1.4.0 */ FWUPD_RELEASE_URGENCY_HIGH, /* Since: 1.4.0 */ FWUPD_RELEASE_URGENCY_CRITICAL, /* Since: 1.4.0 */ /*< private >*/ FWUPD_RELEASE_URGENCY_LAST } FwupdReleaseUrgency; /** * FwupdPluginFlags: * * Flags used to represent plugin attributes */ typedef enum { /** * FWUPD_PLUGIN_FLAG_NONE: * * No plugin flags are set. * * Since: 1.5.0 */ FWUPD_PLUGIN_FLAG_NONE = 0u, /** * FWUPD_PLUGIN_FLAG_DISABLED: * * The plugin has been disabled, either by daemon configuration or a problem. * * Since: 1.5.0 */ FWUPD_PLUGIN_FLAG_DISABLED = 1ull << 0, /** * FWUPD_PLUGIN_FLAG_USER_WARNING: * * The plugin has a problem and would like to show a user warning to a supported client. * * Since: 1.5.0 */ FWUPD_PLUGIN_FLAG_USER_WARNING = 1ull << 1, /** * FWUPD_PLUGIN_FLAG_CLEAR_UPDATABLE: * * When the plugin loads it should clear the UPDATABLE flag from any devices. * This typically happens when the device requires a system restart. * * Since: 1.5.0 */ FWUPD_PLUGIN_FLAG_CLEAR_UPDATABLE = 1ull << 2, /** * FWUPD_PLUGIN_FLAG_NO_HARDWARE: * * The plugin won't load because no supported hardware was found. * This typically happens with plugins designed for a specific platform design * = such as the dell plugin only works on Dell systems,. * * Since: 1.5.0 */ FWUPD_PLUGIN_FLAG_NO_HARDWARE = 1ull << 3, /** * FWUPD_PLUGIN_FLAG_CAPSULES_UNSUPPORTED: * * The plugin discovered that UEFI UpdateCapsule are unsupported. * Supported clients will display this information to a user. * * Since: 1.5.0 */ FWUPD_PLUGIN_FLAG_CAPSULES_UNSUPPORTED = 1ull << 4, /** * FWUPD_PLUGIN_FLAG_UNLOCK_REQUIRED: * * The plugin discovered that hardware unlock is required. * Supported clients will display this information to a user. * * Since: 1.5.0 */ FWUPD_PLUGIN_FLAG_UNLOCK_REQUIRED = 1ull << 5, /** * FWUPD_PLUGIN_FLAG_EFIVAR_NOT_MOUNTED: * * The plugin discovered the efivar filesystem is not found and is required for this plugin. * Supported clients will display this information to a user. * * Since: 1.5.0 */ FWUPD_PLUGIN_FLAG_EFIVAR_NOT_MOUNTED = 1ull << 6, /** * FWUPD_PLUGIN_FLAG_ESP_NOT_FOUND: * * The plugins discovered that the EFI system partition was not found. * Supported clients will display this information to a user. * * Since: 1.5.0 */ FWUPD_PLUGIN_FLAG_ESP_NOT_FOUND = 1ull << 7, /** * FWUPD_PLUGIN_FLAG_LEGACY_BIOS: * * The plugin discovered the system is running in legacy CSM mode. * Supported clients will display this information to a user. * * Since: 1.5.0 */ FWUPD_PLUGIN_FLAG_LEGACY_BIOS = 1ull << 8, /** * FWUPD_PLUGIN_FLAG_FAILED_OPEN: * * Failed to open plugin = missing dependency,. * Supported clients will display this information to a user. * * Since: 1.5.0 */ FWUPD_PLUGIN_FLAG_FAILED_OPEN = 1ull << 9, /** * FWUPD_PLUGIN_FLAG_REQUIRE_HWID: * * A specific HWID is required to use this plugin. * * Since: 1.5.8 */ FWUPD_PLUGIN_FLAG_REQUIRE_HWID = 1ull << 10, /** * FWUPD_PLUGIN_FLAG_KERNEL_TOO_OLD: * * The feature is not supported as the kernel is too old. * * Since: 1.6.2 */ FWUPD_PLUGIN_FLAG_KERNEL_TOO_OLD = 1ull << 11, /** * FWUPD_PLUGIN_FLAG_AUTH_REQUIRED: * * The plugin requires the user to provide authentication details. * Supported clients will display this information to a user. * * Since: 1.6.2 */ FWUPD_PLUGIN_FLAG_AUTH_REQUIRED = 1ull << 12, /** * FWUPD_PLUGIN_FLAG_SECURE_CONFIG: * * The plugin requires the config file to be saved with permissions that only allow the * root user to read. * * Since: 1.8.5 */ FWUPD_PLUGIN_FLAG_SECURE_CONFIG = 1ull << 13, /** * FWUPD_PLUGIN_FLAG_MODULAR: * * The plugin is loaded from an external module. * * Since: 1.8.6 */ FWUPD_PLUGIN_FLAG_MODULAR = 1ull << 14, /** * FWUPD_PLUGIN_FLAG_MEASURE_SYSTEM_INTEGRITY: * * The plugin will be checked that it preserves system state such as `KEK`, `PK`, * `BOOT####` etc. * * Since: 1.8.7 */ FWUPD_PLUGIN_FLAG_MEASURE_SYSTEM_INTEGRITY = 1ull << 15, /** * FWUPD_PLUGIN_FLAG_ESP_NOT_VALID: * * The plugins discovered that the EFI system partition may not be valid. * Supported clients will display this information to a user. * * Since: 1.9.3 */ FWUPD_PLUGIN_FLAG_ESP_NOT_VALID = 1ull << 16, /** * FWUPD_PLUGIN_FLAG_READY: * * The plugin is ready for use and all devices have been coldplugged. * * Since: 1.9.6 */ FWUPD_PLUGIN_FLAG_READY = 1ull << 17, /** * FWUPD_PLUGIN_FLAG_TEST_ONLY: * * The plugin is used for virtual devices that exercising daemon flows. * * Since: 2.0.0 */ FWUPD_PLUGIN_FLAG_TEST_ONLY = 1ull << 18, /** * FWUPD_PLUGIN_FLAG_UNKNOWN: * * The plugin flag is unknown. * This is usually caused by a mismatched libfwupdplugin and daemon. * * Since: 1.5.0 */ FWUPD_PLUGIN_FLAG_UNKNOWN = G_MAXUINT64 } FwupdPluginFlags; /** * FwupdInstallFlags: * * Flags to set when performing the firmware update or install. **/ typedef enum { /** * FWUPD_INSTALL_FLAG_NONE: * * No flags set. * * Since: 0.7.0 */ FWUPD_INSTALL_FLAG_NONE = 0, /** * FWUPD_INSTALL_FLAG_ALLOW_REINSTALL: * * Allow reinstalling the same version. * * Since: 0.7.0 */ FWUPD_INSTALL_FLAG_ALLOW_REINSTALL = 1 << 1, /** * FWUPD_INSTALL_FLAG_ALLOW_OLDER: * * Allow downgrading firmware. * * Since: 0.7.0 */ FWUPD_INSTALL_FLAG_ALLOW_OLDER = 1 << 2, /** * FWUPD_INSTALL_FLAG_FORCE: * * Force the update even if not a good idea. * * Since: 0.7.1 */ FWUPD_INSTALL_FLAG_FORCE = 1 << 3, /** * FWUPD_INSTALL_FLAG_NO_HISTORY: * * Do not write to the history database. * * Since: 1.0.8 */ FWUPD_INSTALL_FLAG_NO_HISTORY = 1 << 4, /** * FWUPD_INSTALL_FLAG_ALLOW_BRANCH_SWITCH: * * Allow firmware branch switching. * * Since: 1.5.0 */ FWUPD_INSTALL_FLAG_ALLOW_BRANCH_SWITCH = 1 << 5, /** * FWUPD_INSTALL_FLAG_IGNORE_CHECKSUM: * * This is now unused; see #FuFirmwareParseFlags. * * Since: 1.5.0 */ FWUPD_INSTALL_FLAG_IGNORE_CHECKSUM = 1 << 6, /** * FWUPD_INSTALL_FLAG_IGNORE_VID_PID: * * This is now unused; see #FuFirmwareParseFlags. * * Since: 1.5.0 */ FWUPD_INSTALL_FLAG_IGNORE_VID_PID = 1 << 7, /** * FWUPD_INSTALL_FLAG_NO_SEARCH: * * This is now unused; see #FuFirmwareParseFlags. * * Since: 1.5.0 */ FWUPD_INSTALL_FLAG_NO_SEARCH = 1 << 8, /** * FWUPD_INSTALL_FLAG_IGNORE_REQUIREMENTS: * * Ignore version requirement checks. * * Since: 1.9.21 */ FWUPD_INSTALL_FLAG_IGNORE_REQUIREMENTS = 1 << 9, /** * FWUPD_INSTALL_FLAG_ONLY_EMULATED: * * Only install to emulated devices. * * Since: 2.0.10 */ FWUPD_INSTALL_FLAG_ONLY_EMULATED = 1 << 10, /*< private >*/ FWUPD_INSTALL_FLAG_UNKNOWN = G_MAXUINT64, } FwupdInstallFlags; /** * FwupdSelfSignFlags: * * Flags to set when performing the firmware update or install. **/ typedef enum { /** * FWUPD_SELF_SIGN_FLAG_NONE: * * No flags set. * * Since: 1.2.6 */ FWUPD_SELF_SIGN_FLAG_NONE = 0, /** * FWUPD_SELF_SIGN_FLAG_ADD_TIMESTAMP: * * Add the timestamp to the detached signature. * * Since: 1.2.6 */ FWUPD_SELF_SIGN_FLAG_ADD_TIMESTAMP = 1 << 0, /** * FWUPD_SELF_SIGN_FLAG_ADD_CERT: * * Add the certificate to the detached signature. * * Since: 1.2.6 */ FWUPD_SELF_SIGN_FLAG_ADD_CERT = 1 << 1, /*< private >*/ FWUPD_SELF_SIGN_FLAG_UNKNOWN = G_MAXUINT64, } FwupdSelfSignFlags; /** * FwupdUpdateState: * * The update state. **/ typedef enum { /** * FWUPD_UPDATE_STATE_UNKNOWN: * * Unknown. * * Since: 0.7.0 */ FWUPD_UPDATE_STATE_UNKNOWN, /** * FWUPD_UPDATE_STATE_PENDING: * * Update is pending. * * Since: 0.7.0 */ FWUPD_UPDATE_STATE_PENDING, /** * FWUPD_UPDATE_STATE_SUCCESS: * * Update was successful. * * Since: 0.7.0 */ FWUPD_UPDATE_STATE_SUCCESS, /** * FWUPD_UPDATE_STATE_FAILED: * * Update failed. * * Since: 0.7.0 */ FWUPD_UPDATE_STATE_FAILED, /** * FWUPD_UPDATE_STATE_NEEDS_REBOOT: * * Waiting for a reboot to apply. * * Since: 1.0.4 */ FWUPD_UPDATE_STATE_NEEDS_REBOOT, /** * FWUPD_UPDATE_STATE_FAILED_TRANSIENT: * * Update failed due to transient issue, e.g. AC power required. * * Since: 1.2.7 */ FWUPD_UPDATE_STATE_FAILED_TRANSIENT, /*< private >*/ FWUPD_UPDATE_STATE_LAST } FwupdUpdateState; /** * FwupdVersionFormat: * * The flags used when parsing version numbers. * * If no verification is required then %FWUPD_VERSION_FORMAT_PLAIN should * be used to signify an unparsable text string. **/ typedef enum { /** * FWUPD_VERSION_FORMAT_UNKNOWN: * * Unknown version format. * * Since: 1.2.9 */ FWUPD_VERSION_FORMAT_UNKNOWN, /** * FWUPD_VERSION_FORMAT_PLAIN: * * An unidentified format text string. * * Since: 1.2.9 */ FWUPD_VERSION_FORMAT_PLAIN, /** * FWUPD_VERSION_FORMAT_NUMBER: * * A single integer version number. * * Since: 1.2.9 */ FWUPD_VERSION_FORMAT_NUMBER, /** * FWUPD_VERSION_FORMAT_PAIR: * * Two AABB.CCDD version numbers. * * Since: 1.2.9 */ FWUPD_VERSION_FORMAT_PAIR, /** * FWUPD_VERSION_FORMAT_TRIPLET: * * Microsoft-style AA.BB.CCDD version numbers. * * Since: 1.2.9 */ FWUPD_VERSION_FORMAT_TRIPLET, /** * FWUPD_VERSION_FORMAT_QUAD: * * UEFI-style AA.BB.CC.DD version numbers. * * Since: 1.2.9 */ FWUPD_VERSION_FORMAT_QUAD, /** * FWUPD_VERSION_FORMAT_BCD: * * Binary coded decimal notation. * * Since: 1.2.9 */ FWUPD_VERSION_FORMAT_BCD, /** * FWUPD_VERSION_FORMAT_INTEL_ME: * * Intel ME-style bitshifted notation. * * Since: 1.2.9 */ FWUPD_VERSION_FORMAT_INTEL_ME, /** * FWUPD_VERSION_FORMAT_INTEL_ME2: * * Intel ME-style A.B.CC.DDDD notation notation, with offset 11. * * Since: 1.2.9 */ FWUPD_VERSION_FORMAT_INTEL_ME2, /** * FWUPD_VERSION_FORMAT_SURFACE_LEGACY: * * Legacy Microsoft Surface 10b.12b.10b. * * Since: 1.3.4 */ FWUPD_VERSION_FORMAT_SURFACE_LEGACY, /** * FWUPD_VERSION_FORMAT_SURFACE: * * Microsoft Surface 8b.16b.8b. * * Since: 1.3.4 */ FWUPD_VERSION_FORMAT_SURFACE, /** * FWUPD_VERSION_FORMAT_DELL_BIOS: * * Dell BIOS BB.CC.DD style. * * Since: 1.3.6 */ FWUPD_VERSION_FORMAT_DELL_BIOS, /** * FWUPD_VERSION_FORMAT_HEX: * * Hexadecimal 0xAABCCDD style. * * Since: 1.4.0 */ FWUPD_VERSION_FORMAT_HEX, /** * FWUPD_VERSION_FORMAT_DELL_BIOS_MSB: * * Dell BIOS AA.BB.CC style. * * Since: 1.9.24 */ FWUPD_VERSION_FORMAT_DELL_BIOS_MSB, /** * FWUPD_VERSION_FORMAT_INTEL_CSME19: * * Intel ME-style bitshifted notation, with offset 19. * * Since: 2.0.4 */ FWUPD_VERSION_FORMAT_INTEL_CSME19, /*< private >*/ FWUPD_VERSION_FORMAT_LAST } FwupdVersionFormat; /** * FWUPD_BATTERY_LEVEL_INVALID: * * This value signifies the battery level is either unset, or the value cannot * be discovered. */ #define FWUPD_BATTERY_LEVEL_INVALID 101 const gchar * fwupd_status_to_string(FwupdStatus status); FwupdStatus fwupd_status_from_string(const gchar *status); const gchar * fwupd_device_flag_to_string(FwupdDeviceFlags device_flag); FwupdDeviceFlags fwupd_device_flag_from_string(const gchar *device_flag); const gchar * fwupd_device_problem_to_string(FwupdDeviceProblem device_problem); FwupdDeviceProblem fwupd_device_problem_from_string(const gchar *device_problem); const gchar * fwupd_plugin_flag_to_string(FwupdPluginFlags plugin_flag); FwupdPluginFlags fwupd_plugin_flag_from_string(const gchar *plugin_flag); const gchar * fwupd_release_flag_to_string(FwupdReleaseFlags release_flag); FwupdReleaseFlags fwupd_release_flag_from_string(const gchar *release_flag); const gchar * fwupd_release_urgency_to_string(FwupdReleaseUrgency release_urgency); FwupdReleaseUrgency fwupd_release_urgency_from_string(const gchar *release_urgency); const gchar * fwupd_update_state_to_string(FwupdUpdateState update_state); FwupdUpdateState fwupd_update_state_from_string(const gchar *update_state); const gchar * fwupd_feature_flag_to_string(FwupdFeatureFlags feature_flag); FwupdFeatureFlags fwupd_feature_flag_from_string(const gchar *feature_flag); FwupdVersionFormat fwupd_version_format_from_string(const gchar *str); const gchar * fwupd_version_format_to_string(FwupdVersionFormat kind); FwupdInstallFlags fwupd_install_flags_from_string(const gchar *str); const gchar * fwupd_install_flags_to_string(FwupdInstallFlags install_flags); G_END_DECLS fwupd-2.0.10/libfwupd/fwupd-error.c000066400000000000000000000232731501337203100171550ustar00rootroot00000000000000/* * Copyright 2015-2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fwupd-common.h" #include "fwupd-enums.h" #include "fwupd-error.h" /** * fwupd_error_to_string: * @error: an enumerated error, e.g. %FWUPD_ERROR_VERSION_NEWER * * Converts an enumerated error to a string. * * Returns: identifier string * * Since: 0.7.0 **/ const gchar * fwupd_error_to_string(FwupdError error) { if (error == FWUPD_ERROR_INTERNAL) return FWUPD_DBUS_INTERFACE ".Internal"; if (error == FWUPD_ERROR_VERSION_NEWER) return FWUPD_DBUS_INTERFACE ".VersionNewer"; if (error == FWUPD_ERROR_VERSION_SAME) return FWUPD_DBUS_INTERFACE ".VersionSame"; if (error == FWUPD_ERROR_ALREADY_PENDING) return FWUPD_DBUS_INTERFACE ".AlreadyPending"; if (error == FWUPD_ERROR_AUTH_FAILED) return FWUPD_DBUS_INTERFACE ".AuthFailed"; if (error == FWUPD_ERROR_READ) return FWUPD_DBUS_INTERFACE ".Read"; if (error == FWUPD_ERROR_WRITE) return FWUPD_DBUS_INTERFACE ".Write"; if (error == FWUPD_ERROR_INVALID_FILE) return FWUPD_DBUS_INTERFACE ".InvalidFile"; if (error == FWUPD_ERROR_NOT_FOUND) return FWUPD_DBUS_INTERFACE ".NotFound"; if (error == FWUPD_ERROR_NOTHING_TO_DO) return FWUPD_DBUS_INTERFACE ".NothingToDo"; if (error == FWUPD_ERROR_NOT_SUPPORTED) return FWUPD_DBUS_INTERFACE ".NotSupported"; if (error == FWUPD_ERROR_SIGNATURE_INVALID) return FWUPD_DBUS_INTERFACE ".SignatureInvalid"; if (error == FWUPD_ERROR_AC_POWER_REQUIRED) return FWUPD_DBUS_INTERFACE ".AcPowerRequired"; if (error == FWUPD_ERROR_PERMISSION_DENIED) return FWUPD_DBUS_INTERFACE ".PermissionDenied"; if (error == FWUPD_ERROR_BROKEN_SYSTEM) return FWUPD_DBUS_INTERFACE ".BrokenSystem"; if (error == FWUPD_ERROR_BATTERY_LEVEL_TOO_LOW) return FWUPD_DBUS_INTERFACE ".BatteryLevelTooLow"; if (error == FWUPD_ERROR_NEEDS_USER_ACTION) return FWUPD_DBUS_INTERFACE ".NeedsUserAction"; if (error == FWUPD_ERROR_AUTH_EXPIRED) return FWUPD_DBUS_INTERFACE ".AuthExpired"; if (error == FWUPD_ERROR_INVALID_DATA) return FWUPD_DBUS_INTERFACE ".InvalidData"; if (error == FWUPD_ERROR_TIMED_OUT) return FWUPD_DBUS_INTERFACE ".TimedOut"; if (error == FWUPD_ERROR_BUSY) return FWUPD_DBUS_INTERFACE ".Busy"; if (error == FWUPD_ERROR_NOT_REACHABLE) return FWUPD_DBUS_INTERFACE ".NotReachable"; return NULL; } /** * fwupd_error_from_string: * @error: (nullable): a string, e.g. `org.freedesktop.fwupd.VersionNewer` * * Converts a string to an enumerated error. * * Returns: enumerated value * * Since: 0.7.0 **/ FwupdError fwupd_error_from_string(const gchar *error) { if (g_strcmp0(error, FWUPD_DBUS_INTERFACE ".Internal") == 0) return FWUPD_ERROR_INTERNAL; if (g_strcmp0(error, FWUPD_DBUS_INTERFACE ".VersionNewer") == 0) return FWUPD_ERROR_VERSION_NEWER; if (g_strcmp0(error, FWUPD_DBUS_INTERFACE ".VersionSame") == 0) return FWUPD_ERROR_VERSION_SAME; if (g_strcmp0(error, FWUPD_DBUS_INTERFACE ".AlreadyPending") == 0) return FWUPD_ERROR_ALREADY_PENDING; if (g_strcmp0(error, FWUPD_DBUS_INTERFACE ".AuthFailed") == 0) return FWUPD_ERROR_AUTH_FAILED; if (g_strcmp0(error, FWUPD_DBUS_INTERFACE ".Read") == 0) return FWUPD_ERROR_READ; if (g_strcmp0(error, FWUPD_DBUS_INTERFACE ".Write") == 0) return FWUPD_ERROR_WRITE; if (g_strcmp0(error, FWUPD_DBUS_INTERFACE ".InvalidFile") == 0) return FWUPD_ERROR_INVALID_FILE; if (g_strcmp0(error, FWUPD_DBUS_INTERFACE ".InvalidData") == 0) return FWUPD_ERROR_INVALID_DATA; if (g_strcmp0(error, FWUPD_DBUS_INTERFACE ".NotFound") == 0) return FWUPD_ERROR_NOT_FOUND; if (g_strcmp0(error, FWUPD_DBUS_INTERFACE ".NothingToDo") == 0) return FWUPD_ERROR_NOTHING_TO_DO; if (g_strcmp0(error, FWUPD_DBUS_INTERFACE ".NotSupported") == 0) return FWUPD_ERROR_NOT_SUPPORTED; if (g_strcmp0(error, FWUPD_DBUS_INTERFACE ".SignatureInvalid") == 0) return FWUPD_ERROR_SIGNATURE_INVALID; if (g_strcmp0(error, FWUPD_DBUS_INTERFACE ".AcPowerRequired") == 0) return FWUPD_ERROR_AC_POWER_REQUIRED; if (g_strcmp0(error, FWUPD_DBUS_INTERFACE ".PermissionDenied") == 0) return FWUPD_ERROR_PERMISSION_DENIED; if (g_strcmp0(error, FWUPD_DBUS_INTERFACE ".BrokenSystem") == 0) return FWUPD_ERROR_BROKEN_SYSTEM; if (g_strcmp0(error, FWUPD_DBUS_INTERFACE ".BatteryLevelTooLow") == 0) return FWUPD_ERROR_BATTERY_LEVEL_TOO_LOW; if (g_strcmp0(error, FWUPD_DBUS_INTERFACE ".NeedsUserAction") == 0) return FWUPD_ERROR_NEEDS_USER_ACTION; if (g_strcmp0(error, FWUPD_DBUS_INTERFACE ".AuthExpired") == 0) return FWUPD_ERROR_AUTH_EXPIRED; if (g_strcmp0(error, FWUPD_DBUS_INTERFACE ".TimedOut") == 0) return FWUPD_ERROR_TIMED_OUT; if (g_strcmp0(error, FWUPD_DBUS_INTERFACE ".Busy") == 0) return FWUPD_ERROR_BUSY; if (g_strcmp0(error, FWUPD_DBUS_INTERFACE ".NotReachable") == 0) return FWUPD_ERROR_NOT_REACHABLE; return FWUPD_ERROR_LAST; } /** * fwupd_error_quark: * * An error quark. * * Returns: an error quark * * Since: 0.1.1 **/ GQuark fwupd_error_quark(void) { static GQuark quark = 0; if (!quark) { quark = g_quark_from_static_string("FwupdError"); for (gint i = 0; i < FWUPD_ERROR_LAST; i++) { g_dbus_error_register_error(quark, i, fwupd_error_to_string(i)); } } return quark; } /** * fwupd_error_convert: * @perror: (nullable): A #GError, perhaps with domain #GIOError * * Convert the error to a #FwupdError, if required. * * Since: 2.0.0 **/ void fwupd_error_convert(GError **perror) { GError *error = (perror != NULL) ? *perror : NULL; struct { GQuark domain; gint code; FwupdError fwupd_code; } map[] = { {G_FILE_ERROR, G_FILE_ERROR_ACCES, FWUPD_ERROR_PERMISSION_DENIED}, {G_FILE_ERROR, G_FILE_ERROR_AGAIN, FWUPD_ERROR_BUSY}, {G_FILE_ERROR, G_FILE_ERROR_BADF, FWUPD_ERROR_INTERNAL}, {G_FILE_ERROR, G_FILE_ERROR_EXIST, FWUPD_ERROR_PERMISSION_DENIED}, {G_FILE_ERROR, G_FILE_ERROR_FAILED, FWUPD_ERROR_INTERNAL}, {G_FILE_ERROR, G_FILE_ERROR_FAULT, FWUPD_ERROR_INTERNAL}, {G_FILE_ERROR, G_FILE_ERROR_INTR, FWUPD_ERROR_BUSY}, {G_FILE_ERROR, G_FILE_ERROR_INVAL, FWUPD_ERROR_INVALID_DATA}, {G_FILE_ERROR, G_FILE_ERROR_IO, FWUPD_ERROR_INTERNAL}, {G_FILE_ERROR, G_FILE_ERROR_ISDIR, FWUPD_ERROR_INVALID_FILE}, {G_FILE_ERROR, G_FILE_ERROR_LOOP, FWUPD_ERROR_NOT_SUPPORTED}, {G_FILE_ERROR, G_FILE_ERROR_MFILE, FWUPD_ERROR_INTERNAL}, {G_FILE_ERROR, G_FILE_ERROR_NAMETOOLONG, FWUPD_ERROR_INVALID_DATA}, {G_FILE_ERROR, G_FILE_ERROR_NFILE, FWUPD_ERROR_BROKEN_SYSTEM}, {G_FILE_ERROR, G_FILE_ERROR_NODEV, FWUPD_ERROR_NOT_SUPPORTED}, {G_FILE_ERROR, G_FILE_ERROR_NOENT, FWUPD_ERROR_INVALID_FILE}, {G_FILE_ERROR, G_FILE_ERROR_NOSPC, FWUPD_ERROR_BROKEN_SYSTEM}, {G_FILE_ERROR, G_FILE_ERROR_NOSYS, FWUPD_ERROR_NOT_SUPPORTED}, {G_FILE_ERROR, G_FILE_ERROR_NOTDIR, FWUPD_ERROR_INVALID_FILE}, {G_FILE_ERROR, G_FILE_ERROR_NXIO, FWUPD_ERROR_NOT_FOUND}, {G_FILE_ERROR, G_FILE_ERROR_PERM, FWUPD_ERROR_PERMISSION_DENIED}, {G_FILE_ERROR, G_FILE_ERROR_PIPE, FWUPD_ERROR_NOT_SUPPORTED}, {G_FILE_ERROR, G_FILE_ERROR_ROFS, FWUPD_ERROR_NOT_SUPPORTED}, {G_FILE_ERROR, G_FILE_ERROR_TXTBSY, FWUPD_ERROR_BUSY}, {G_IO_ERROR, G_IO_ERROR_BUSY, FWUPD_ERROR_TIMED_OUT}, {G_IO_ERROR, G_IO_ERROR_CANCELLED, FWUPD_ERROR_INTERNAL}, {G_IO_ERROR, G_IO_ERROR_FAILED, FWUPD_ERROR_INTERNAL}, {G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, FWUPD_ERROR_INVALID_DATA}, {G_IO_ERROR, G_IO_ERROR_INVALID_DATA, FWUPD_ERROR_INVALID_DATA}, {G_IO_ERROR, G_IO_ERROR_NOT_CONNECTED, FWUPD_ERROR_NOT_FOUND}, {G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY, FWUPD_ERROR_NOT_SUPPORTED}, {G_IO_ERROR, G_IO_ERROR_NOT_FOUND, FWUPD_ERROR_NOT_FOUND}, {G_IO_ERROR, G_IO_ERROR_NOT_INITIALIZED, FWUPD_ERROR_INTERNAL}, {G_IO_ERROR, G_IO_ERROR_NOT_REGULAR_FILE, FWUPD_ERROR_INVALID_DATA}, {G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, FWUPD_ERROR_NOT_SUPPORTED}, {G_IO_ERROR, G_IO_ERROR_PARTIAL_INPUT, FWUPD_ERROR_READ}, {G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED, FWUPD_ERROR_PERMISSION_DENIED}, {G_IO_ERROR, G_IO_ERROR_TIMED_OUT, FWUPD_ERROR_TIMED_OUT}, {G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK, FWUPD_ERROR_TIMED_OUT}, {G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY, FWUPD_ERROR_NOT_SUPPORTED}, {G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_UNKNOWN_ENCODING, FWUPD_ERROR_INVALID_DATA}, {G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_PARSE, FWUPD_ERROR_INVALID_DATA}, {G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_NOT_FOUND, FWUPD_ERROR_INVALID_DATA}, {G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND, FWUPD_ERROR_INVALID_DATA}, {G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_GROUP_NOT_FOUND, FWUPD_ERROR_INVALID_DATA}, {G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE, FWUPD_ERROR_INVALID_DATA}, {G_MARKUP_ERROR, G_MARKUP_ERROR_BAD_UTF8, FWUPD_ERROR_INVALID_DATA}, {G_MARKUP_ERROR, G_MARKUP_ERROR_EMPTY, FWUPD_ERROR_INVALID_DATA}, {G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, FWUPD_ERROR_INVALID_DATA}, {G_MARKUP_ERROR, G_MARKUP_ERROR_MISSING_ATTRIBUTE, FWUPD_ERROR_INVALID_DATA}, {G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, FWUPD_ERROR_INVALID_DATA}, {G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE, FWUPD_ERROR_NOT_SUPPORTED}, {G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT, FWUPD_ERROR_NOT_SUPPORTED}, }; /* sanity check */ if (error == NULL) return; if (error->domain == FWUPD_ERROR) return; /* correct some company names */ for (guint i = 0; i < G_N_ELEMENTS(map); i++) { if (g_error_matches(error, map[i].domain, map[i].code)) { error->domain = FWUPD_ERROR; error->code = map[i].fwupd_code; return; } } /* fallback */ #ifndef SUPPORTED_BUILD g_critical("GError %s:%i sending over D-Bus was not converted to FwupdError", g_quark_to_string(error->domain), error->code); #endif error->domain = FWUPD_ERROR; error->code = FWUPD_ERROR_INTERNAL; } fwupd-2.0.10/libfwupd/fwupd-error.h000066400000000000000000000063121501337203100171550ustar00rootroot00000000000000/* * Copyright 2015-2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_BEGIN_DECLS #define FWUPD_ERROR fwupd_error_quark() /** * FwupdError: * * The error code. **/ typedef enum { /** * FWUPD_ERROR_INTERNAL: * * Internal error. * * Since: 0.1.1 */ FWUPD_ERROR_INTERNAL, /** * FWUPD_ERROR_VERSION_NEWER: * * Installed newer firmware version. * * Since: 0.1.1 */ FWUPD_ERROR_VERSION_NEWER, /** * FWUPD_ERROR_VERSION_SAME: * * Installed same firmware version. * * Since: 0.1.1 */ FWUPD_ERROR_VERSION_SAME, /** * FWUPD_ERROR_ALREADY_PENDING: * * Already set to be installed offline. * * Since: 0.1.1 */ FWUPD_ERROR_ALREADY_PENDING, /** * FWUPD_ERROR_AUTH_FAILED: * * Failed to get authentication. * * Since: 0.1.1 */ FWUPD_ERROR_AUTH_FAILED, /** * FWUPD_ERROR_READ: * * Failed to read from device. * * Since: 0.1.1 */ FWUPD_ERROR_READ, /** * FWUPD_ERROR_WRITE: * * Failed to write to the device. * * Since: 0.1.1 */ FWUPD_ERROR_WRITE, /** * FWUPD_ERROR_INVALID_FILE: * * Invalid file format. * * Since: 0.1.1 */ FWUPD_ERROR_INVALID_FILE, /** * FWUPD_ERROR_NOT_FOUND: * * No matching device exists. * * Since: 0.1.1 */ FWUPD_ERROR_NOT_FOUND, /** * FWUPD_ERROR_NOTHING_TO_DO: * * Nothing to do. * * Since: 0.1.1 */ FWUPD_ERROR_NOTHING_TO_DO, /** * FWUPD_ERROR_NOT_SUPPORTED: * * Action was not possible. * * Since: 0.1.1 */ FWUPD_ERROR_NOT_SUPPORTED, /** * FWUPD_ERROR_SIGNATURE_INVALID: * * Signature was invalid. * * Since: 0.1.2 */ FWUPD_ERROR_SIGNATURE_INVALID, /** * FWUPD_ERROR_AC_POWER_REQUIRED: * * AC power was required. * * Since: 0.8.0 */ FWUPD_ERROR_AC_POWER_REQUIRED, /** * FWUPD_ERROR_PERMISSION_DENIED: * * Permission was denied. * * Since: 0.9.8 */ FWUPD_ERROR_PERMISSION_DENIED, /** * FWUPD_ERROR_BROKEN_SYSTEM: * * User has configured their system in a broken way. * * Since: 1.2.8 */ FWUPD_ERROR_BROKEN_SYSTEM, /** * FWUPD_ERROR_BATTERY_LEVEL_TOO_LOW: * * The system battery level is too low. * * Since: 1.2.10 */ FWUPD_ERROR_BATTERY_LEVEL_TOO_LOW, /** * FWUPD_ERROR_NEEDS_USER_ACTION: * * User needs to do an action to complete the update. * * Since: 1.3.3 */ FWUPD_ERROR_NEEDS_USER_ACTION, /** * FWUPD_ERROR_AUTH_EXPIRED: * * Failed to get auth as credentials have expired. * * Since: 1.7.5 */ FWUPD_ERROR_AUTH_EXPIRED, /** * FWUPD_ERROR_INVALID_DATA: * * Invalid data. * * Since: 2.0.0 */ FWUPD_ERROR_INVALID_DATA, /** * FWUPD_ERROR_TIMED_OUT: * * The request timed out. * * Since: 2.0.0 */ FWUPD_ERROR_TIMED_OUT, /** * FWUPD_ERROR_BUSY: * * The device is busy. * * Since: 2.0.0 */ FWUPD_ERROR_BUSY, /** * FWUPD_ERROR_NOT_REACHABLE: * * The network is not reachable. * * Since: 2.0.4 */ FWUPD_ERROR_NOT_REACHABLE, /*< private >*/ FWUPD_ERROR_LAST } FwupdError; GQuark fwupd_error_quark(void); const gchar * fwupd_error_to_string(FwupdError error); FwupdError fwupd_error_from_string(const gchar *error); void fwupd_error_convert(GError **perror); G_END_DECLS fwupd-2.0.10/libfwupd/fwupd-plugin.c000066400000000000000000000220521501337203100173140ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fwupd-codec.h" #include "fwupd-common-private.h" #include "fwupd-enums-private.h" #include "fwupd-plugin.h" /** * FwupdPlugin: * * A plugin which is used by fwupd to enumerate and update devices. * * See also: [class@FwupdRelease] */ static void fwupd_plugin_finalize(GObject *object); typedef struct { gchar *name; guint64 flags; } FwupdPluginPrivate; enum { PROP_0, PROP_NAME, PROP_FLAGS, PROP_LAST }; static void fwupd_plugin_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_EXTENDED(FwupdPlugin, fwupd_plugin, G_TYPE_OBJECT, 0, G_ADD_PRIVATE(FwupdPlugin) G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fwupd_plugin_codec_iface_init)); #define GET_PRIVATE(o) (fwupd_plugin_get_instance_private(o)) /** * fwupd_plugin_get_name: * @self: a #FwupdPlugin * * Gets the plugin name. * * Returns: the plugin name, or %NULL if unset * * Since: 1.5.0 **/ const gchar * fwupd_plugin_get_name(FwupdPlugin *self) { FwupdPluginPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_PLUGIN(self), NULL); return priv->name; } /** * fwupd_plugin_set_name: * @self: a #FwupdPlugin * @name: the plugin name, e.g. `bios` * * Sets the plugin name. * * Since: 1.5.0 **/ void fwupd_plugin_set_name(FwupdPlugin *self, const gchar *name) { FwupdPluginPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_PLUGIN(self)); g_return_if_fail(name != NULL); /* not changed */ if (g_strcmp0(priv->name, name) == 0) return; g_free(priv->name); priv->name = g_strdup(name); g_object_notify(G_OBJECT(self), "name"); } /** * fwupd_plugin_get_flags: * @self: a #FwupdPlugin * * Gets the plugin flags. * * Returns: plugin flags, or 0 if unset * * Since: 1.5.0 **/ guint64 fwupd_plugin_get_flags(FwupdPlugin *self) { FwupdPluginPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_PLUGIN(self), 0); return priv->flags; } /** * fwupd_plugin_set_flags: * @self: a #FwupdPlugin * @flags: plugin flags, e.g. %FWUPD_PLUGIN_FLAG_CAPSULES_UNSUPPORTED * * Sets the plugin flags. * * Since: 1.5.0 **/ void fwupd_plugin_set_flags(FwupdPlugin *self, guint64 flags) { FwupdPluginPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_PLUGIN(self)); if (priv->flags == flags) return; priv->flags = flags; g_object_notify(G_OBJECT(self), "flags"); } /** * fwupd_plugin_add_flag: * @self: a #FwupdPlugin * @flag: the #FwupdPluginFlags * * Adds a specific plugin flag to the plugin. * * Since: 1.5.0 **/ void fwupd_plugin_add_flag(FwupdPlugin *self, FwupdPluginFlags flag) { FwupdPluginPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_PLUGIN(self)); if (flag == 0) return; if ((priv->flags & flag) > 0) return; priv->flags |= flag; g_object_notify(G_OBJECT(self), "flags"); } /** * fwupd_plugin_remove_flag: * @self: a #FwupdPlugin * @flag: a plugin flag * * Removes a specific plugin flag from the plugin. * * Since: 1.5.0 **/ void fwupd_plugin_remove_flag(FwupdPlugin *self, FwupdPluginFlags flag) { FwupdPluginPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_PLUGIN(self)); if (flag == 0) return; if ((priv->flags & flag) == 0) return; priv->flags &= ~flag; g_object_notify(G_OBJECT(self), "flags"); } /** * fwupd_plugin_has_flag: * @self: a #FwupdPlugin * @flag: a plugin flag * * Finds if the plugin has a specific plugin flag. * * Returns: %TRUE if the flag is set * * Since: 1.5.0 **/ gboolean fwupd_plugin_has_flag(FwupdPlugin *self, FwupdPluginFlags flag) { FwupdPluginPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_PLUGIN(self), FALSE); return (priv->flags & flag) > 0; } static void fwupd_plugin_add_variant(FwupdCodec *codec, GVariantBuilder *builder, FwupdCodecFlags flags) { FwupdPlugin *self = FWUPD_PLUGIN(codec); FwupdPluginPrivate *priv = GET_PRIVATE(self); if (priv->name != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_NAME, g_variant_new_string(priv->name)); } if (priv->flags > 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_FLAGS, g_variant_new_uint64(priv->flags)); } } static void fwupd_plugin_from_key_value(FwupdPlugin *self, const gchar *key, GVariant *value) { if (g_strcmp0(key, FWUPD_RESULT_KEY_NAME) == 0) { fwupd_plugin_set_name(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_FLAGS) == 0) { fwupd_plugin_set_flags(self, g_variant_get_uint64(value)); return; } } static void fwupd_plugin_string_append_flags(GString *str, guint idt, const gchar *key, guint64 plugin_flags) { g_autoptr(GString) tmp = g_string_new(""); for (guint i = 0; i < 64; i++) { if ((plugin_flags & ((guint64)1 << i)) == 0) continue; g_string_append_printf(tmp, "%s|", fwupd_plugin_flag_to_string((guint64)1 << i)); } if (tmp->len == 0) { g_string_append(tmp, fwupd_plugin_flag_to_string(0)); } else { g_string_truncate(tmp, tmp->len - 1); } fwupd_codec_string_append(str, idt, key, tmp->str); } static void fwupd_plugin_add_json(FwupdCodec *codec, JsonBuilder *builder, FwupdCodecFlags flags) { FwupdPlugin *self = FWUPD_PLUGIN(codec); FwupdPluginPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_PLUGIN(self)); g_return_if_fail(builder != NULL); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_NAME, priv->name); if (priv->flags != FWUPD_PLUGIN_FLAG_NONE) { json_builder_set_member_name(builder, FWUPD_RESULT_KEY_FLAGS); json_builder_begin_array(builder); for (guint i = 0; i < 64; i++) { const gchar *tmp; if ((priv->flags & ((guint64)1 << i)) == 0) continue; tmp = fwupd_plugin_flag_to_string((guint64)1 << i); json_builder_add_string_value(builder, tmp); } json_builder_end_array(builder); } } static void fwupd_plugin_add_string(FwupdCodec *codec, guint idt, GString *str) { FwupdPlugin *self = FWUPD_PLUGIN(codec); FwupdPluginPrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_NAME, priv->name); if (priv->flags != FWUPD_PLUGIN_FLAG_NONE) fwupd_plugin_string_append_flags(str, idt, FWUPD_RESULT_KEY_FLAGS, priv->flags); } static void fwupd_plugin_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FwupdPlugin *self = FWUPD_PLUGIN(object); FwupdPluginPrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_NAME: g_value_set_string(value, priv->name); break; case PROP_FLAGS: g_value_set_uint64(value, priv->flags); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fwupd_plugin_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FwupdPlugin *self = FWUPD_PLUGIN(object); switch (prop_id) { case PROP_NAME: fwupd_plugin_set_name(self, g_value_get_string(value)); break; case PROP_FLAGS: fwupd_plugin_set_flags(self, g_value_get_uint64(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fwupd_plugin_class_init(FwupdPluginClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); GParamSpec *pspec; object_class->finalize = fwupd_plugin_finalize; object_class->get_property = fwupd_plugin_get_property; object_class->set_property = fwupd_plugin_set_property; /** * FwupdPlugin:name: * * The plugin name. * * Since: 1.5.0 */ pspec = g_param_spec_string("name", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_NAME, pspec); /** * FwupdPlugin:flags: * * The plugin flags. * * Since: 1.5.0 */ pspec = g_param_spec_uint64("flags", NULL, NULL, FWUPD_PLUGIN_FLAG_NONE, FWUPD_PLUGIN_FLAG_UNKNOWN, FWUPD_PLUGIN_FLAG_NONE, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_FLAGS, pspec); } static void fwupd_plugin_init(FwupdPlugin *self) { } static void fwupd_plugin_finalize(GObject *object) { FwupdPlugin *self = FWUPD_PLUGIN(object); FwupdPluginPrivate *priv = GET_PRIVATE(self); g_free(priv->name); G_OBJECT_CLASS(fwupd_plugin_parent_class)->finalize(object); } static void fwupd_plugin_from_variant_iter(FwupdCodec *codec, GVariantIter *iter) { FwupdPlugin *self = FWUPD_PLUGIN(codec); GVariant *value; const gchar *key; while (g_variant_iter_next(iter, "{&sv}", &key, &value)) { fwupd_plugin_from_key_value(self, key, value); g_variant_unref(value); } } static void fwupd_plugin_codec_iface_init(FwupdCodecInterface *iface) { iface->add_string = fwupd_plugin_add_string; iface->add_json = fwupd_plugin_add_json; iface->add_variant = fwupd_plugin_add_variant; iface->from_variant_iter = fwupd_plugin_from_variant_iter; } /** * fwupd_plugin_new: * * Creates a new plugin. * * Returns: a new #FwupdPlugin * * Since: 1.5.0 **/ FwupdPlugin * fwupd_plugin_new(void) { FwupdPlugin *self; self = g_object_new(FWUPD_TYPE_PLUGIN, NULL); return FWUPD_PLUGIN(self); } fwupd-2.0.10/libfwupd/fwupd-plugin.h000066400000000000000000000024211501337203100173170ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fwupd-enums.h" G_BEGIN_DECLS #define FWUPD_TYPE_PLUGIN (fwupd_plugin_get_type()) G_DECLARE_DERIVABLE_TYPE(FwupdPlugin, fwupd_plugin, FWUPD, PLUGIN, GObject) struct _FwupdPluginClass { GObjectClass parent_class; /*< private >*/ void (*_fwupd_reserved1)(void); void (*_fwupd_reserved2)(void); void (*_fwupd_reserved3)(void); void (*_fwupd_reserved4)(void); void (*_fwupd_reserved5)(void); void (*_fwupd_reserved6)(void); void (*_fwupd_reserved7)(void); }; FwupdPlugin * fwupd_plugin_new(void); const gchar * fwupd_plugin_get_name(FwupdPlugin *self) G_GNUC_NON_NULL(1); void fwupd_plugin_set_name(FwupdPlugin *self, const gchar *name) G_GNUC_NON_NULL(1); guint64 fwupd_plugin_get_flags(FwupdPlugin *self) G_GNUC_NON_NULL(1); void fwupd_plugin_set_flags(FwupdPlugin *self, guint64 flags) G_GNUC_NON_NULL(1); void fwupd_plugin_add_flag(FwupdPlugin *self, FwupdPluginFlags flag) G_GNUC_NON_NULL(1); void fwupd_plugin_remove_flag(FwupdPlugin *self, FwupdPluginFlags flag) G_GNUC_NON_NULL(1); gboolean fwupd_plugin_has_flag(FwupdPlugin *self, FwupdPluginFlags flag) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); G_END_DECLS fwupd-2.0.10/libfwupd/fwupd-release.c000066400000000000000000001775271501337203100174600ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fwupd-codec.h" #include "fwupd-common-private.h" #include "fwupd-enums-private.h" #include "fwupd-error.h" #include "fwupd-release.h" #include "fwupd-report.h" /** * FwupdRelease: * * A firmware release with a specific version. * * Devices can have more than one release, and the releases are typically * ordered by their version. * * See also: [class@FwupdDevice] */ static void fwupd_release_finalize(GObject *object); typedef struct { GPtrArray *checksums; /* (nullable) (element-type utf-8) */ GPtrArray *tags; /* (nullable) (element-type utf-8) */ GPtrArray *categories; /* (nullable) (element-type utf-8) */ GPtrArray *issues; /* (nullable) (element-type utf-8) */ GHashTable *metadata; /* (nullable) (element-type utf-8 utf-8) */ gchar *description; gchar *filename; gchar *protocol; gchar *homepage; gchar *details_url; gchar *source_url; gchar *sbom_url; gchar *appstream_id; gchar *id; gchar *detach_caption; gchar *detach_image; gchar *license; gchar *name; gchar *name_variant_suffix; gchar *summary; gchar *branch; GPtrArray *locations; /* (nullable) (element-type utf-8) */ gchar *vendor; gchar *version; gchar *remote_id; guint64 size; guint64 created; guint32 install_duration; FwupdReleaseFlags flags; FwupdReleaseUrgency urgency; gchar *update_message; gchar *update_image; GPtrArray *reports; /* (nullable) (element-type FwupdReport) */ } FwupdReleasePrivate; enum { PROP_0, PROP_REMOTE_ID, PROP_LAST }; static void fwupd_release_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_EXTENDED(FwupdRelease, fwupd_release, G_TYPE_OBJECT, 0, G_ADD_PRIVATE(FwupdRelease) G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fwupd_release_codec_iface_init)); #define GET_PRIVATE(o) (fwupd_release_get_instance_private(o)) /** * fwupd_release_get_remote_id: * @self: a #FwupdRelease * * Gets the remote ID that can be used for downloading. * * Returns: the ID, or %NULL if unset * * Since: 0.9.3 **/ const gchar * fwupd_release_get_remote_id(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); return priv->remote_id; } /** * fwupd_release_set_remote_id: * @self: a #FwupdRelease * @remote_id: the release ID, e.g. `USB:foo` * * Sets the remote ID that can be used for downloading. * * Since: 0.9.3 **/ void fwupd_release_set_remote_id(FwupdRelease *self, const gchar *remote_id) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); /* not changed */ if (g_strcmp0(priv->remote_id, remote_id) == 0) return; g_free(priv->remote_id); priv->remote_id = g_strdup(remote_id); g_object_notify(G_OBJECT(self), "remote-id"); } /** * fwupd_release_get_version: * @self: a #FwupdRelease * * Gets the update version. * * Returns: the update version, or %NULL if unset * * Since: 0.9.3 **/ const gchar * fwupd_release_get_version(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); return priv->version; } /** * fwupd_release_set_version: * @self: a #FwupdRelease * @version: (nullable): the update version, e.g. `1.2.4` * * Sets the update version. * * Since: 0.9.3 **/ void fwupd_release_set_version(FwupdRelease *self, const gchar *version) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); /* not changed */ if (g_strcmp0(priv->version, version) == 0) return; g_free(priv->version); priv->version = g_strdup(version); } /** * fwupd_release_get_filename: * @self: a #FwupdRelease * * Gets the update filename. * * Returns: the update filename, or %NULL if unset * * Since: 0.9.3 **/ const gchar * fwupd_release_get_filename(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); return priv->filename; } /** * fwupd_release_set_filename: * @self: a #FwupdRelease * @filename: (nullable): the update filename on disk * * Sets the update filename. * * Since: 0.9.3 **/ void fwupd_release_set_filename(FwupdRelease *self, const gchar *filename) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); /* not changed */ if (g_strcmp0(priv->filename, filename) == 0) return; g_free(priv->filename); priv->filename = g_strdup(filename); } /** * fwupd_release_get_update_message: * @self: a #FwupdRelease * * Gets the update message. * * Returns: the update message, or %NULL if unset * * Since: 1.2.4 **/ const gchar * fwupd_release_get_update_message(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); return priv->update_message; } /** * fwupd_release_set_update_message: * @self: a #FwupdRelease * @update_message: (nullable): the update message string * * Sets the update message. * * Since: 1.2.4 **/ void fwupd_release_set_update_message(FwupdRelease *self, const gchar *update_message) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); /* not changed */ if (g_strcmp0(priv->update_message, update_message) == 0) return; g_free(priv->update_message); priv->update_message = g_strdup(update_message); } /** * fwupd_release_get_update_image: * @self: a #FwupdRelease * * Gets the update image. * * Returns: the update image URL, or %NULL if unset * * Since: 1.4.5 **/ const gchar * fwupd_release_get_update_image(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); return priv->update_image; } /** * fwupd_release_set_update_image: * @self: a #FwupdRelease * @update_image: (nullable): the update image URL * * Sets the update image. * * Since: 1.4.5 **/ void fwupd_release_set_update_image(FwupdRelease *self, const gchar *update_image) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); /* not changed */ if (g_strcmp0(priv->update_image, update_image) == 0) return; g_free(priv->update_image); priv->update_image = g_strdup(update_image); } /** * fwupd_release_get_protocol: * @self: a #FwupdRelease * * Gets the update protocol. * * Returns: the update protocol, or %NULL if unset * * Since: 1.2.2 **/ const gchar * fwupd_release_get_protocol(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); return priv->protocol; } /** * fwupd_release_set_protocol: * @self: a #FwupdRelease * @protocol: (nullable): the update protocol, e.g. `org.usb.dfu` * * Sets the update protocol. * * Since: 1.2.2 **/ void fwupd_release_set_protocol(FwupdRelease *self, const gchar *protocol) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); /* not changed */ if (g_strcmp0(priv->protocol, protocol) == 0) return; g_free(priv->protocol); priv->protocol = g_strdup(protocol); } static void fwupd_release_ensure_issues(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); if (priv->issues == NULL) priv->issues = g_ptr_array_new_with_free_func(g_free); } /** * fwupd_release_get_issues: * @self: a #FwupdRelease * * Gets the list of issues fixed in this release. * * Returns: (element-type utf8) (transfer none): the issues, which may be empty * * Since: 1.3.2 **/ GPtrArray * fwupd_release_get_issues(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); fwupd_release_ensure_issues(self); return priv->issues; } /** * fwupd_release_add_issue: * @self: a #FwupdRelease * @issue: (not nullable): the update issue, e.g. `CVE-2019-12345` * * Adds an resolved issue to this release. * * Since: 1.3.2 **/ void fwupd_release_add_issue(FwupdRelease *self, const gchar *issue) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); g_return_if_fail(issue != NULL); fwupd_release_ensure_issues(self); for (guint i = 0; i < priv->issues->len; i++) { const gchar *issue_tmp = g_ptr_array_index(priv->issues, i); if (g_strcmp0(issue_tmp, issue) == 0) return; } g_ptr_array_add(priv->issues, g_strdup(issue)); } static void fwupd_release_ensure_categories(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); if (priv->categories == NULL) priv->categories = g_ptr_array_new_with_free_func(g_free); } /** * fwupd_release_get_categories: * @self: a #FwupdRelease * * Gets the release categories. * * Returns: (element-type utf8) (transfer none): the categories, which may be empty * * Since: 1.2.7 **/ GPtrArray * fwupd_release_get_categories(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); fwupd_release_ensure_categories(self); return priv->categories; } /** * fwupd_release_add_category: * @self: a #FwupdRelease * @category: (not nullable): the update category, e.g. `X-EmbeddedController` * * Adds the update category. * * Since: 1.2.7 **/ void fwupd_release_add_category(FwupdRelease *self, const gchar *category) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); g_return_if_fail(category != NULL); if (fwupd_release_has_category(self, category)) return; fwupd_release_ensure_categories(self); g_ptr_array_add(priv->categories, g_strdup(category)); } /** * fwupd_release_has_category: * @self: a #FwupdRelease * @category: (not nullable): the update category, e.g. `X-EmbeddedController` * * Finds out if the release has the update category. * * Returns: %TRUE if the release matches * * Since: 1.2.7 **/ gboolean fwupd_release_has_category(FwupdRelease *self, const gchar *category) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), FALSE); g_return_val_if_fail(category != NULL, FALSE); if (priv->categories == NULL) return FALSE; for (guint i = 0; i < priv->categories->len; i++) { const gchar *category_tmp = g_ptr_array_index(priv->categories, i); if (g_strcmp0(category_tmp, category) == 0) return TRUE; } return FALSE; } static void fwupd_release_ensure_checksums(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); if (priv->checksums == NULL) priv->checksums = g_ptr_array_new_with_free_func(g_free); } /** * fwupd_release_get_checksums: * @self: a #FwupdRelease * * Gets the release container checksums. * * Returns: (element-type utf8) (transfer none): the checksums, which may be empty * * Since: 0.9.3 **/ GPtrArray * fwupd_release_get_checksums(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); fwupd_release_ensure_checksums(self); return priv->checksums; } /** * fwupd_release_add_checksum: * @self: a #FwupdRelease * @checksum: (not nullable): the update container checksum * * Sets the update checksum. * * Since: 0.9.3 **/ void fwupd_release_add_checksum(FwupdRelease *self, const gchar *checksum) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); g_return_if_fail(checksum != NULL); if (fwupd_release_has_checksum(self, checksum)) return; fwupd_release_ensure_checksums(self); g_ptr_array_add(priv->checksums, g_strdup(checksum)); } /** * fwupd_release_has_checksum: * @self: a #FwupdRelease * @checksum: (not nullable): the update checksum * * Finds out if the release has the update container checksum. * * Returns: %TRUE if the release matches * * Since: 1.2.6 **/ gboolean fwupd_release_has_checksum(FwupdRelease *self, const gchar *checksum) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), FALSE); g_return_val_if_fail(checksum != NULL, FALSE); if (priv->checksums == NULL) return FALSE; for (guint i = 0; i < priv->checksums->len; i++) { const gchar *checksum_tmp = g_ptr_array_index(priv->checksums, i); if (g_strcmp0(checksum_tmp, checksum) == 0) return TRUE; } return FALSE; } static void fwupd_release_ensure_tags(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); if (priv->tags == NULL) priv->tags = g_ptr_array_new_with_free_func(g_free); } /** * fwupd_release_get_tags: * @self: a #FwupdRelease * * Gets the release tags. * * Returns: (element-type utf8) (transfer none): the tags, which may be empty * * Since: 1.7.3 **/ GPtrArray * fwupd_release_get_tags(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); fwupd_release_ensure_tags(self); return priv->tags; } /** * fwupd_release_add_tag: * @self: a #FwupdRelease * @tag: (not nullable): the update tag, e.g. `vendor-factory-2021q1` * * Adds a specific release tag. * * Since: 1.7.3 **/ void fwupd_release_add_tag(FwupdRelease *self, const gchar *tag) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); g_return_if_fail(tag != NULL); if (fwupd_release_has_tag(self, tag)) return; fwupd_release_ensure_tags(self); g_ptr_array_add(priv->tags, g_strdup(tag)); } /** * fwupd_release_has_tag: * @self: a #FwupdRelease * @tag: (not nullable): the update tag, e.g. `vendor-factory-2021q1` * * Finds out if the release has a specific tag. * * Returns: %TRUE if the release matches * * Since: 1.7.3 **/ gboolean fwupd_release_has_tag(FwupdRelease *self, const gchar *tag) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), FALSE); g_return_val_if_fail(tag != NULL, FALSE); if (priv->tags == NULL) return FALSE; for (guint i = 0; i < priv->tags->len; i++) { const gchar *tag_tmp = g_ptr_array_index(priv->tags, i); if (g_strcmp0(tag_tmp, tag) == 0) return TRUE; } return FALSE; } static void fwupd_release_ensure_metadata(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); if (priv->metadata == NULL) priv->metadata = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); } /** * fwupd_release_get_metadata: * @self: a #FwupdRelease * * Gets the release metadata. * * Returns: (transfer none): the metadata, which may be empty * * Since: 1.0.4 **/ GHashTable * fwupd_release_get_metadata(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); fwupd_release_ensure_metadata(self); return priv->metadata; } /** * fwupd_release_add_metadata_item: * @self: a #FwupdRelease * @key: (not nullable): the key * @value: (not nullable): the value * * Sets a release metadata item. * * Since: 1.0.4 **/ void fwupd_release_add_metadata_item(FwupdRelease *self, const gchar *key, const gchar *value) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); g_return_if_fail(key != NULL); g_return_if_fail(value != NULL); fwupd_release_ensure_metadata(self); g_hash_table_insert(priv->metadata, g_strdup(key), g_strdup(value)); } /** * fwupd_release_add_metadata: * @self: a #FwupdRelease * @hash: (not nullable): the key-values * * Sets multiple release metadata items. * * Since: 1.0.4 **/ void fwupd_release_add_metadata(FwupdRelease *self, GHashTable *hash) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_autoptr(GList) keys = NULL; g_return_if_fail(FWUPD_IS_RELEASE(self)); g_return_if_fail(hash != NULL); /* deep copy the whole map */ fwupd_release_ensure_metadata(self); keys = g_hash_table_get_keys(hash); for (GList *l = keys; l != NULL; l = l->next) { const gchar *key = l->data; const gchar *value = g_hash_table_lookup(hash, key); g_hash_table_insert(priv->metadata, g_strdup(key), g_strdup(value)); } } /** * fwupd_release_get_metadata_item: * @self: a #FwupdRelease * @key: (not nullable): the key * * Gets a release metadata item. * * Returns: the value, or %NULL if unset * * Since: 1.0.4 **/ const gchar * fwupd_release_get_metadata_item(FwupdRelease *self, const gchar *key) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); g_return_val_if_fail(key != NULL, NULL); if (priv->metadata == NULL) return NULL; return g_hash_table_lookup(priv->metadata, key); } static void fwupd_release_ensure_locations(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); if (priv->locations == NULL) priv->locations = g_ptr_array_new_with_free_func(g_free); } /** * fwupd_release_get_locations: * @self: a #FwupdRelease * * Gets the update URI, i.e. where you can download the firmware from. * * Typically the first URI will be the main HTTP mirror, but all URIs may not * be valid HTTP URIs. For example, "ipns://QmSrPmba" is valid here. * * Returns: (element-type utf8) (transfer none): the URIs * * Since: 1.5.6 **/ GPtrArray * fwupd_release_get_locations(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); fwupd_release_ensure_locations(self); return priv->locations; } /** * fwupd_release_add_location: * @self: a #FwupdRelease * @location: (not nullable): the update URI * * Adds an update URI, i.e. where you can download the firmware from. * * Since: 1.5.6 **/ void fwupd_release_add_location(FwupdRelease *self, const gchar *location) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); g_return_if_fail(location != NULL); fwupd_release_ensure_locations(self); for (guint i = 0; i < priv->locations->len; i++) { const gchar *location_tmp = g_ptr_array_index(priv->locations, i); if (g_strcmp0(location_tmp, location) == 0) return; } g_ptr_array_add(priv->locations, g_strdup(location)); } /** * fwupd_release_get_homepage: * @self: a #FwupdRelease * * Gets the update homepage. * * Returns: the update homepage, or %NULL if unset * * Since: 0.9.3 **/ const gchar * fwupd_release_get_homepage(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); return priv->homepage; } /** * fwupd_release_set_homepage: * @self: a #FwupdRelease * @homepage: (nullable): the URL * * Sets the update homepage URL. * * Since: 0.9.3 **/ void fwupd_release_set_homepage(FwupdRelease *self, const gchar *homepage) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); /* not changed */ if (g_strcmp0(priv->homepage, homepage) == 0) return; g_free(priv->homepage); priv->homepage = g_strdup(homepage); } /** * fwupd_release_get_details_url: * @self: a #FwupdRelease * * Gets the URL for the online update notes. * * Returns: the update URL, or %NULL if unset * * Since: 1.2.4 **/ const gchar * fwupd_release_get_details_url(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); return priv->details_url; } /** * fwupd_release_set_details_url: * @self: a #FwupdRelease * @details_url: (nullable): the URL * * Sets the URL for the online update notes. * * Since: 1.2.4 **/ void fwupd_release_set_details_url(FwupdRelease *self, const gchar *details_url) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); /* not changed */ if (g_strcmp0(priv->details_url, details_url) == 0) return; g_free(priv->details_url); priv->details_url = g_strdup(details_url); } /** * fwupd_release_get_source_url: * @self: a #FwupdRelease * * Gets the URL of the source code used to build this release. * * Returns: the update source_url, or %NULL if unset * * Since: 1.2.4 **/ const gchar * fwupd_release_get_source_url(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); return priv->source_url; } /** * fwupd_release_set_source_url: * @self: a #FwupdRelease * @source_url: (nullable): the URL * * Sets the URL of the source code used to build this release. * * Since: 1.2.4 **/ void fwupd_release_set_source_url(FwupdRelease *self, const gchar *source_url) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); /* not changed */ if (g_strcmp0(priv->source_url, source_url) == 0) return; g_free(priv->source_url); priv->source_url = g_strdup(source_url); } /** * fwupd_release_set_sbom_url: * @self: a #FwupdRelease * @sbom_url: (nullable): the URL * * Sets the URL of the SBOM for this release. * * Since: 2.0.7 **/ void fwupd_release_set_sbom_url(FwupdRelease *self, const gchar *sbom_url) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); /* not changed */ if (g_strcmp0(priv->sbom_url, sbom_url) == 0) return; g_free(priv->sbom_url); priv->sbom_url = g_strdup(sbom_url); } /** * fwupd_release_get_sbom_url: * @self: a #FwupdRelease * * Gets the URL of the SBOM for this release. * * Returns: a URL, or %NULL if unset * * Since: 2.0.7 **/ const gchar * fwupd_release_get_sbom_url(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); return priv->sbom_url; } /** * fwupd_release_get_description: * @self: a #FwupdRelease * * Gets the update description in AppStream markup format. * * Returns: the update description, or %NULL if unset * * Since: 0.9.3 **/ const gchar * fwupd_release_get_description(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); return priv->description; } /** * fwupd_release_set_description: * @self: a #FwupdRelease * @description: (nullable): the update description in AppStream markup format * * Sets the update description. * * Since: 0.9.3 **/ void fwupd_release_set_description(FwupdRelease *self, const gchar *description) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); /* not changed */ if (g_strcmp0(priv->description, description) == 0) return; g_free(priv->description); priv->description = g_strdup(description); } /** * fwupd_release_get_appstream_id: * @self: a #FwupdRelease * * Gets the AppStream ID. * * Returns: the AppStream ID, or %NULL if unset * * Since: 0.9.3 **/ const gchar * fwupd_release_get_appstream_id(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); return priv->appstream_id; } /** * fwupd_release_set_appstream_id: * @self: a #FwupdRelease * @appstream_id: (nullable): the AppStream component ID, e.g. `org.hughski.ColorHug2.firmware` * * Sets the AppStream ID. * * Since: 0.9.3 **/ void fwupd_release_set_appstream_id(FwupdRelease *self, const gchar *appstream_id) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); /* not changed */ if (g_strcmp0(priv->appstream_id, appstream_id) == 0) return; g_free(priv->appstream_id); priv->appstream_id = g_strdup(appstream_id); } /** * fwupd_release_get_id: * @self: a #FwupdRelease * * Gets the release ID, which allows identifying the specific uploaded component. * * Returns: the ID, or %NULL if unset * * Since: 1.7.2 **/ const gchar * fwupd_release_get_id(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); return priv->id; } /** * fwupd_release_set_id: * @self: a #FwupdRelease * @id: (nullable): the AppStream component ID, e.g. `component:1234` * * Sets the ID, which allows identifying the specific uploaded component. * * Since: 1.7.2 **/ void fwupd_release_set_id(FwupdRelease *self, const gchar *id) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); /* not changed */ if (g_strcmp0(priv->id, id) == 0) return; g_free(priv->id); priv->id = g_strdup(id); } /** * fwupd_release_get_detach_caption: * @self: a #FwupdRelease * * Gets the optional text caption used to manually detach the device. * * Returns: the string caption, or %NULL if unset * * Since: 1.3.3 **/ const gchar * fwupd_release_get_detach_caption(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); return priv->detach_caption; } /** * fwupd_release_set_detach_caption: * @self: a #FwupdRelease * @detach_caption: (nullable): string caption * * Sets the optional text caption used to manually detach the device. * * Since: 1.3.3 **/ void fwupd_release_set_detach_caption(FwupdRelease *self, const gchar *detach_caption) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); /* not changed */ if (g_strcmp0(priv->detach_caption, detach_caption) == 0) return; g_free(priv->detach_caption); priv->detach_caption = g_strdup(detach_caption); } /** * fwupd_release_get_detach_image: * @self: a #FwupdRelease * * Gets the optional image used to manually detach the device. * * Returns: the URI, or %NULL if unset * * Since: 1.3.3 **/ const gchar * fwupd_release_get_detach_image(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); return priv->detach_image; } /** * fwupd_release_set_detach_image: * @self: a #FwupdRelease * @detach_image: (nullable): a fully qualified URI * * Sets the optional image used to manually detach the device. * * Since: 1.3.3 **/ void fwupd_release_set_detach_image(FwupdRelease *self, const gchar *detach_image) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); /* not changed */ if (g_strcmp0(priv->detach_image, detach_image) == 0) return; g_free(priv->detach_image); priv->detach_image = g_strdup(detach_image); } /** * fwupd_release_get_size: * @self: a #FwupdRelease * * Gets the update size. * * Returns: the update size in bytes, or 0 if unset * * Since: 0.9.3 **/ guint64 fwupd_release_get_size(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), 0); return priv->size; } /** * fwupd_release_set_size: * @self: a #FwupdRelease * @size: the update size in bytes * * Sets the update size. * * Since: 0.9.3 **/ void fwupd_release_set_size(FwupdRelease *self, guint64 size) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); priv->size = size; } /** * fwupd_release_get_created: * @self: a #FwupdRelease * * Gets when the update was created. * * Returns: UTC timestamp in UNIX format, or 0 if unset * * Since: 1.4.0 **/ guint64 fwupd_release_get_created(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), 0); return priv->created; } /** * fwupd_release_set_created: * @self: a #FwupdRelease * @created: UTC timestamp in UNIX format * * Sets when the update was created. * * Since: 1.4.0 **/ void fwupd_release_set_created(FwupdRelease *self, guint64 created) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); priv->created = created; } /** * fwupd_release_get_summary: * @self: a #FwupdRelease * * Gets the update summary. * * Returns: the update summary, or %NULL if unset * * Since: 0.9.3 **/ const gchar * fwupd_release_get_summary(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); return priv->summary; } /** * fwupd_release_set_summary: * @self: a #FwupdRelease * @summary: (nullable): the update one line summary * * Sets the update summary. * * Since: 0.9.3 **/ void fwupd_release_set_summary(FwupdRelease *self, const gchar *summary) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); /* not changed */ if (g_strcmp0(priv->summary, summary) == 0) return; g_free(priv->summary); priv->summary = g_strdup(summary); } /** * fwupd_release_get_branch: * @self: a #FwupdRelease * * Gets the update branch. * * Returns: the alternate branch, or %NULL if unset * * Since: 1.5.0 **/ const gchar * fwupd_release_get_branch(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); return priv->branch; } /** * fwupd_release_set_branch: * @self: a #FwupdRelease * @branch: (nullable): the update one line branch * * Sets the alternate branch. * * Since: 1.5.0 **/ void fwupd_release_set_branch(FwupdRelease *self, const gchar *branch) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); /* not changed */ if (g_strcmp0(priv->branch, branch) == 0) return; g_free(priv->branch); priv->branch = g_strdup(branch); } /** * fwupd_release_get_vendor: * @self: a #FwupdRelease * * Gets the update vendor. * * Returns: the update vendor, or %NULL if unset * * Since: 0.9.3 **/ const gchar * fwupd_release_get_vendor(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); return priv->vendor; } /** * fwupd_release_set_vendor: * @self: a #FwupdRelease * @vendor: (nullable): the vendor name, e.g. `Hughski Limited` * * Sets the update vendor. * * Since: 0.9.3 **/ void fwupd_release_set_vendor(FwupdRelease *self, const gchar *vendor) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); /* not changed */ if (g_strcmp0(priv->vendor, vendor) == 0) return; g_free(priv->vendor); priv->vendor = g_strdup(vendor); } /** * fwupd_release_get_license: * @self: a #FwupdRelease * * Gets the update license. * * Returns: the update license, or %NULL if unset * * Since: 0.9.3 **/ const gchar * fwupd_release_get_license(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); return priv->license; } /** * fwupd_release_set_license: * @self: a #FwupdRelease * @license: (nullable): the update license. * * Sets the update license. * * Since: 0.9.3 **/ void fwupd_release_set_license(FwupdRelease *self, const gchar *license) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); /* not changed */ if (g_strcmp0(priv->license, license) == 0) return; g_free(priv->license); priv->license = g_strdup(license); } /** * fwupd_release_get_name: * @self: a #FwupdRelease * * Gets the update name. * * Returns: the update name, or %NULL if unset * * Since: 0.9.3 **/ const gchar * fwupd_release_get_name(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); return priv->name; } /** * fwupd_release_set_name: * @self: a #FwupdRelease * @name: (nullable): the update name. * * Sets the update name. * * Since: 0.9.3 **/ void fwupd_release_set_name(FwupdRelease *self, const gchar *name) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); /* not changed */ if (g_strcmp0(priv->name, name) == 0) return; g_free(priv->name); priv->name = g_strdup(name); } /** * fwupd_release_get_name_variant_suffix: * @self: a #FwupdRelease * * Gets the update variant suffix. * * Returns: the update variant, or %NULL if unset * * Since: 1.3.2 **/ const gchar * fwupd_release_get_name_variant_suffix(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); return priv->name_variant_suffix; } /** * fwupd_release_set_name_variant_suffix: * @self: a #FwupdRelease * @name_variant_suffix: (nullable): the description * * Sets the update variant suffix. * * Since: 1.3.2 **/ void fwupd_release_set_name_variant_suffix(FwupdRelease *self, const gchar *name_variant_suffix) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); /* not changed */ if (g_strcmp0(priv->name_variant_suffix, name_variant_suffix) == 0) return; g_free(priv->name_variant_suffix); priv->name_variant_suffix = g_strdup(name_variant_suffix); } /** * fwupd_release_get_flags: * @self: a #FwupdRelease * * Gets the release flags. * * Returns: release flags, or 0 if unset * * Since: 1.2.6 **/ FwupdReleaseFlags fwupd_release_get_flags(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), 0); return priv->flags; } /** * fwupd_release_set_flags: * @self: a #FwupdRelease * @flags: release flags, e.g. %FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD * * Sets the release flags. * * Since: 1.2.6 **/ void fwupd_release_set_flags(FwupdRelease *self, FwupdReleaseFlags flags) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); priv->flags = flags; } /** * fwupd_release_add_flag: * @self: a #FwupdRelease * @flag: the #FwupdReleaseFlags * * Adds a specific release flag to the release. * * Since: 1.2.6 **/ void fwupd_release_add_flag(FwupdRelease *self, FwupdReleaseFlags flag) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); priv->flags |= flag; } /** * fwupd_release_remove_flag: * @self: a #FwupdRelease * @flag: the #FwupdReleaseFlags * * Removes a specific release flag from the release. * * Since: 1.2.6 **/ void fwupd_release_remove_flag(FwupdRelease *self, FwupdReleaseFlags flag) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); priv->flags &= ~flag; } /** * fwupd_release_has_flag: * @self: a #FwupdRelease * @flag: the #FwupdReleaseFlags * * Finds if the release has a specific release flag. * * Returns: %TRUE if the flag is set * * Since: 1.2.6 **/ gboolean fwupd_release_has_flag(FwupdRelease *self, FwupdReleaseFlags flag) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), FALSE); return (priv->flags & flag) > 0; } /** * fwupd_release_get_urgency: * @self: a #FwupdRelease * * Gets the release urgency. * * Returns: the release urgency, or 0 if unset * * Since: 1.4.0 **/ FwupdReleaseUrgency fwupd_release_get_urgency(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), 0); return priv->urgency; } /** * fwupd_release_set_urgency: * @self: a #FwupdRelease * @urgency: the release urgency, e.g. %FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD * * Sets the release urgency. * * Since: 1.4.0 **/ void fwupd_release_set_urgency(FwupdRelease *self, FwupdReleaseUrgency urgency) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); priv->urgency = urgency; } /** * fwupd_release_get_install_duration: * @self: a #FwupdRelease * * Gets the time estimate for firmware installation (in seconds) * * Returns: the estimated time to flash this release (or 0 if unset) * * Since: 1.2.1 **/ guint32 fwupd_release_get_install_duration(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), 0); return priv->install_duration; } static void fwupd_release_ensure_reports(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); if (priv->reports == NULL) priv->reports = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); } /** * fwupd_release_get_reports: * @self: a #FwupdRelease * * Gets all the reports for this release. * * Returns: (transfer none) (element-type FwupdReport): array of reports * * Since: 1.8.8 **/ GPtrArray * fwupd_release_get_reports(FwupdRelease *self) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_RELEASE(self), NULL); fwupd_release_ensure_reports(self); return priv->reports; } /** * fwupd_release_add_report: * @self: a #FwupdRelease * @report: (not nullable): a #FwupdReport * * Adds a report for this release. * * Since: 1.8.8 **/ void fwupd_release_add_report(FwupdRelease *self, FwupdReport *report) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); g_return_if_fail(FWUPD_IS_REPORT(report)); fwupd_release_ensure_reports(self); g_ptr_array_add(priv->reports, g_object_ref(report)); } /** * fwupd_release_set_install_duration: * @self: a #FwupdRelease * @duration: amount of time in seconds * * Sets the time estimate for firmware installation (in seconds) * * Since: 1.2.1 **/ void fwupd_release_set_install_duration(FwupdRelease *self, guint32 duration) { FwupdReleasePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_RELEASE(self)); priv->install_duration = duration; } static void fwupd_release_add_variant(FwupdCodec *codec, GVariantBuilder *builder, FwupdCodecFlags flags) { FwupdRelease *self = FWUPD_RELEASE(codec); FwupdReleasePrivate *priv = GET_PRIVATE(self); if (priv->remote_id != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_REMOTE_ID, g_variant_new_string(priv->remote_id)); } if (priv->appstream_id != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_APPSTREAM_ID, g_variant_new_string(priv->appstream_id)); } if (priv->id != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_RELEASE_ID, g_variant_new_string(priv->id)); } if (priv->detach_caption != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_DETACH_CAPTION, g_variant_new_string(priv->detach_caption)); } if (priv->detach_image != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_DETACH_IMAGE, g_variant_new_string(priv->detach_image)); } if (priv->update_message != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_UPDATE_MESSAGE, g_variant_new_string(priv->update_message)); } if (priv->update_image != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_UPDATE_IMAGE, g_variant_new_string(priv->update_image)); } if (priv->filename != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_FILENAME, g_variant_new_string(priv->filename)); } if (priv->protocol != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_PROTOCOL, g_variant_new_string(priv->protocol)); } if (priv->license != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_LICENSE, g_variant_new_string(priv->license)); } if (priv->name != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_NAME, g_variant_new_string(priv->name)); } if (priv->name_variant_suffix != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_NAME_VARIANT_SUFFIX, g_variant_new_string(priv->name_variant_suffix)); } if (priv->size != 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_SIZE, g_variant_new_uint64(priv->size)); } if (priv->created != 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_CREATED, g_variant_new_uint64(priv->created)); } if (priv->summary != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_SUMMARY, g_variant_new_string(priv->summary)); } if (priv->branch != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_BRANCH, g_variant_new_string(priv->branch)); } if (priv->description != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_DESCRIPTION, g_variant_new_string(priv->description)); } if (priv->categories != NULL && priv->categories->len > 0) { g_autofree const gchar **strv = g_new0(const gchar *, priv->categories->len + 1); for (guint i = 0; i < priv->categories->len; i++) strv[i] = (const gchar *)g_ptr_array_index(priv->categories, i); g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_CATEGORIES, g_variant_new_strv(strv, -1)); } if (priv->issues != NULL && priv->issues->len > 0) { g_autofree const gchar **strv = g_new0(const gchar *, priv->issues->len + 1); for (guint i = 0; i < priv->issues->len; i++) strv[i] = (const gchar *)g_ptr_array_index(priv->issues, i); g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_ISSUES, g_variant_new_strv(strv, -1)); } if (priv->checksums != NULL && priv->checksums->len > 0) { g_autoptr(GString) str = g_string_new(""); for (guint i = 0; i < priv->checksums->len; i++) { const gchar *checksum = g_ptr_array_index(priv->checksums, i); g_string_append_printf(str, "%s,", checksum); } if (str->len > 0) g_string_truncate(str, str->len - 1); g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_CHECKSUM, g_variant_new_string(str->str)); } if (priv->locations != NULL && priv->locations->len > 0) { g_variant_builder_add( builder, "{sv}", FWUPD_RESULT_KEY_LOCATIONS, g_variant_new_strv((const gchar *const *)priv->locations->pdata, priv->locations->len)); } if (priv->tags != NULL && priv->tags->len > 0) { g_variant_builder_add( builder, "{sv}", FWUPD_RESULT_KEY_TAGS, g_variant_new_strv((const gchar *const *)priv->tags->pdata, priv->tags->len)); } if (priv->homepage != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_HOMEPAGE, g_variant_new_string(priv->homepage)); } if (priv->details_url != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_DETAILS_URL, g_variant_new_string(priv->details_url)); } if (priv->source_url != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_SOURCE_URL, g_variant_new_string(priv->source_url)); } if (priv->sbom_url != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_SBOM_URL, g_variant_new_string(priv->sbom_url)); } if (priv->version != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_VERSION, g_variant_new_string(priv->version)); } if (priv->vendor != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_VENDOR, g_variant_new_string(priv->vendor)); } if (priv->flags != 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_TRUST_FLAGS, g_variant_new_uint64(priv->flags)); } if (priv->urgency != 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_URGENCY, g_variant_new_uint32(priv->urgency)); } if (priv->metadata != NULL && g_hash_table_size(priv->metadata) > 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_METADATA, fwupd_hash_kv_to_variant(priv->metadata)); } if (priv->install_duration > 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_INSTALL_DURATION, g_variant_new_uint32(priv->install_duration)); } if (priv->reports != NULL && priv->reports->len > 0) { g_autofree GVariant **children = NULL; children = g_new0(GVariant *, priv->reports->len); for (guint i = 0; i < priv->reports->len; i++) { FwupdReport *report = g_ptr_array_index(priv->reports, i); children[i] = fwupd_codec_to_variant(FWUPD_CODEC(report), flags); } g_variant_builder_add( builder, "{sv}", FWUPD_RESULT_KEY_REPORTS, g_variant_new_array(G_VARIANT_TYPE("a{sv}"), children, priv->reports->len)); } } static void fwupd_release_from_key_value(FwupdRelease *self, const gchar *key, GVariant *value) { FwupdReleasePrivate *priv = GET_PRIVATE(self); if (g_strcmp0(key, FWUPD_RESULT_KEY_REMOTE_ID) == 0) { fwupd_release_set_remote_id(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_APPSTREAM_ID) == 0) { fwupd_release_set_appstream_id(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_RELEASE_ID) == 0) { fwupd_release_set_id(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_DETACH_CAPTION) == 0) { fwupd_release_set_detach_caption(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_DETACH_IMAGE) == 0) { fwupd_release_set_detach_image(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_FILENAME) == 0) { fwupd_release_set_filename(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_PROTOCOL) == 0) { fwupd_release_set_protocol(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_LICENSE) == 0) { fwupd_release_set_license(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_NAME) == 0) { fwupd_release_set_name(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_NAME_VARIANT_SUFFIX) == 0) { fwupd_release_set_name_variant_suffix(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_SIZE) == 0) { fwupd_release_set_size(self, g_variant_get_uint64(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_CREATED) == 0) { fwupd_release_set_created(self, g_variant_get_uint64(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_SUMMARY) == 0) { fwupd_release_set_summary(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_BRANCH) == 0) { fwupd_release_set_branch(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_DESCRIPTION) == 0) { fwupd_release_set_description(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_CATEGORIES) == 0) { g_autofree const gchar **strv = g_variant_get_strv(value, NULL); for (guint i = 0; strv[i] != NULL; i++) fwupd_release_add_category(self, strv[i]); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_ISSUES) == 0) { g_autofree const gchar **strv = g_variant_get_strv(value, NULL); for (guint i = 0; strv[i] != NULL; i++) fwupd_release_add_issue(self, strv[i]); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_CHECKSUM) == 0) { const gchar *checksums = g_variant_get_string(value, NULL); g_auto(GStrv) split = g_strsplit(checksums, ",", -1); for (guint i = 0; split[i] != NULL; i++) fwupd_release_add_checksum(self, split[i]); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_LOCATIONS) == 0) { g_autofree const gchar **strv = g_variant_get_strv(value, NULL); for (guint i = 0; strv[i] != NULL; i++) fwupd_release_add_location(self, strv[i]); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_TAGS) == 0) { g_autofree const gchar **strv = g_variant_get_strv(value, NULL); for (guint i = 0; strv[i] != NULL; i++) fwupd_release_add_tag(self, strv[i]); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_URI) == 0) { fwupd_release_add_location(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_HOMEPAGE) == 0) { fwupd_release_set_homepage(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_DETAILS_URL) == 0) { fwupd_release_set_details_url(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_SOURCE_URL) == 0) { fwupd_release_set_source_url(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_SBOM_URL) == 0) { fwupd_release_set_sbom_url(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_VERSION) == 0) { fwupd_release_set_version(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_VENDOR) == 0) { fwupd_release_set_vendor(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_TRUST_FLAGS) == 0) { fwupd_release_set_flags(self, g_variant_get_uint64(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_URGENCY) == 0) { fwupd_release_set_urgency(self, g_variant_get_uint32(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_INSTALL_DURATION) == 0) { fwupd_release_set_install_duration(self, g_variant_get_uint32(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_UPDATE_MESSAGE) == 0) { fwupd_release_set_update_message(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_UPDATE_IMAGE) == 0) { fwupd_release_set_update_image(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_METADATA) == 0) { if (priv->metadata != NULL) g_hash_table_unref(priv->metadata); priv->metadata = fwupd_variant_to_hash_kv(value); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_REPORTS) == 0) { GVariantIter iter; GVariant *child; g_variant_iter_init(&iter, value); while ((child = g_variant_iter_next_value(&iter))) { g_autoptr(FwupdReport) report = fwupd_report_new(); if (fwupd_codec_from_variant(FWUPD_CODEC(report), child, NULL)) fwupd_release_add_report(self, report); g_variant_unref(child); } return; } } static void fwupd_release_string_append_flags(GString *str, guint idt, const gchar *key, FwupdReleaseFlags release_flags) { g_autoptr(GString) tmp = g_string_new(""); for (guint i = 0; i < 64; i++) { if ((release_flags & ((guint64)1 << i)) == 0) continue; g_string_append_printf(tmp, "%s|", fwupd_release_flag_to_string((guint64)1 << i)); } if (tmp->len == 0) { g_string_append(tmp, fwupd_release_flag_to_string(0)); } else { g_string_truncate(tmp, tmp->len - 1); } fwupd_codec_string_append(str, idt, key, tmp->str); } static void fwupd_release_add_json(FwupdCodec *codec, JsonBuilder *builder, FwupdCodecFlags flags) { FwupdRelease *self = FWUPD_RELEASE(codec); FwupdReleasePrivate *priv = GET_PRIVATE(self); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_APPSTREAM_ID, priv->appstream_id); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_RELEASE_ID, priv->id); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_REMOTE_ID, priv->remote_id); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_NAME, priv->name); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_NAME_VARIANT_SUFFIX, priv->name_variant_suffix); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_SUMMARY, priv->summary); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_DESCRIPTION, priv->description); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_BRANCH, priv->branch); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_VERSION, priv->version); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_FILENAME, priv->filename); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_PROTOCOL, priv->protocol); if (priv->categories != NULL && priv->categories->len > 0) { json_builder_set_member_name(builder, FWUPD_RESULT_KEY_CATEGORIES); json_builder_begin_array(builder); for (guint i = 0; i < priv->categories->len; i++) { const gchar *tmp = g_ptr_array_index(priv->categories, i); json_builder_add_string_value(builder, tmp); } json_builder_end_array(builder); } if (priv->issues != NULL && priv->issues->len > 0) { json_builder_set_member_name(builder, FWUPD_RESULT_KEY_ISSUES); json_builder_begin_array(builder); for (guint i = 0; i < priv->issues->len; i++) { const gchar *tmp = g_ptr_array_index(priv->issues, i); json_builder_add_string_value(builder, tmp); } json_builder_end_array(builder); } if (priv->checksums != NULL && priv->checksums->len > 0) { json_builder_set_member_name(builder, FWUPD_RESULT_KEY_CHECKSUM); json_builder_begin_array(builder); for (guint i = 0; i < priv->checksums->len; i++) { const gchar *checksum = g_ptr_array_index(priv->checksums, i); json_builder_add_string_value(builder, checksum); } json_builder_end_array(builder); } if (priv->tags != NULL && priv->tags->len > 0) { json_builder_set_member_name(builder, FWUPD_RESULT_KEY_TAGS); json_builder_begin_array(builder); for (guint i = 0; i < priv->tags->len; i++) { const gchar *tag = g_ptr_array_index(priv->tags, i); json_builder_add_string_value(builder, tag); } json_builder_end_array(builder); } fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_LICENSE, priv->license); if (priv->size > 0) fwupd_codec_json_append_int(builder, FWUPD_RESULT_KEY_SIZE, priv->size); if (priv->created > 0) fwupd_codec_json_append_int(builder, FWUPD_RESULT_KEY_CREATED, priv->created); if (priv->locations != NULL && priv->locations->len > 0) { json_builder_set_member_name(builder, FWUPD_RESULT_KEY_LOCATIONS); json_builder_begin_array(builder); for (guint i = 0; i < priv->locations->len; i++) { const gchar *location = g_ptr_array_index(priv->locations, i); json_builder_add_string_value(builder, location); } json_builder_end_array(builder); } fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_HOMEPAGE, priv->homepage); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_DETAILS_URL, priv->details_url); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_SOURCE_URL, priv->source_url); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_SBOM_URL, priv->sbom_url); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_VENDOR, priv->vendor); if (priv->flags != FWUPD_RELEASE_FLAG_NONE) { json_builder_set_member_name(builder, FWUPD_RESULT_KEY_FLAGS); json_builder_begin_array(builder); for (guint i = 0; i < 64; i++) { const gchar *tmp; if ((priv->flags & ((guint64)1 << i)) == 0) continue; tmp = fwupd_release_flag_to_string((guint64)1 << i); json_builder_add_string_value(builder, tmp); } json_builder_end_array(builder); } if (priv->install_duration > 0) { fwupd_codec_json_append_int(builder, FWUPD_RESULT_KEY_INSTALL_DURATION, priv->install_duration); } fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_DETACH_CAPTION, priv->detach_caption); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_DETACH_IMAGE, priv->detach_image); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_UPDATE_MESSAGE, priv->update_message); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_UPDATE_IMAGE, priv->update_image); /* metadata */ if (priv->metadata != NULL) { g_autoptr(GList) keys = g_hash_table_get_keys(priv->metadata); for (GList *l = keys; l != NULL; l = l->next) { const gchar *key = l->data; const gchar *value = g_hash_table_lookup(priv->metadata, key); fwupd_codec_json_append(builder, key, value); } } /* reports */ if (priv->reports != NULL && priv->reports->len > 0) fwupd_codec_array_to_json(priv->reports, "Reports", builder, flags); } static void fwupd_release_add_string(FwupdCodec *codec, guint idt, GString *str) { FwupdRelease *self = FWUPD_RELEASE(codec); FwupdReleasePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_APPSTREAM_ID, priv->appstream_id); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_RELEASE_ID, priv->id); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_REMOTE_ID, priv->remote_id); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_NAME, priv->name); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_NAME_VARIANT_SUFFIX, priv->name_variant_suffix); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_SUMMARY, priv->summary); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_DESCRIPTION, priv->description); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_BRANCH, priv->branch); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_VERSION, priv->version); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_FILENAME, priv->filename); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_PROTOCOL, priv->protocol); if (priv->categories != NULL) { for (guint i = 0; i < priv->categories->len; i++) { const gchar *tmp = g_ptr_array_index(priv->categories, i); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_CATEGORIES, tmp); } } if (priv->issues != NULL) { for (guint i = 0; i < priv->issues->len; i++) { const gchar *tmp = g_ptr_array_index(priv->issues, i); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_ISSUES, tmp); } } if (priv->checksums != NULL) { for (guint i = 0; i < priv->checksums->len; i++) { const gchar *checksum = g_ptr_array_index(priv->checksums, i); g_autofree gchar *checksum_display = fwupd_checksum_format_for_display(checksum); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_CHECKSUM, checksum_display); } } if (priv->tags != NULL) { for (guint i = 0; i < priv->tags->len; i++) { const gchar *tag = g_ptr_array_index(priv->tags, i); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_TAGS, tag); } } fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_LICENSE, priv->license); fwupd_codec_string_append_size(str, idt, FWUPD_RESULT_KEY_SIZE, priv->size); fwupd_codec_string_append_time(str, idt, FWUPD_RESULT_KEY_CREATED, priv->created); if (priv->locations != NULL) { for (guint i = 0; i < priv->locations->len; i++) { const gchar *location = g_ptr_array_index(priv->locations, i); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_URI, location); } } fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_HOMEPAGE, priv->homepage); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_DETAILS_URL, priv->details_url); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_SOURCE_URL, priv->source_url); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_SBOM_URL, priv->sbom_url); if (priv->urgency != FWUPD_RELEASE_URGENCY_UNKNOWN) { fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_URGENCY, fwupd_release_urgency_to_string(priv->urgency)); } fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_VENDOR, priv->vendor); fwupd_release_string_append_flags(str, idt, FWUPD_RESULT_KEY_FLAGS, priv->flags); fwupd_codec_string_append_int(str, idt, FWUPD_RESULT_KEY_INSTALL_DURATION, priv->install_duration); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_DETACH_CAPTION, priv->detach_caption); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_DETACH_IMAGE, priv->detach_image); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_UPDATE_MESSAGE, priv->update_message); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_UPDATE_IMAGE, priv->update_image); /* metadata */ if (priv->metadata != NULL) { g_autoptr(GList) keys = g_hash_table_get_keys(priv->metadata); for (GList *l = keys; l != NULL; l = l->next) { const gchar *key = l->data; const gchar *value = g_hash_table_lookup(priv->metadata, key); fwupd_codec_string_append(str, idt, key, value); } } if (priv->reports != NULL) { for (guint i = 0; i < priv->reports->len; i++) { FwupdReport *report = g_ptr_array_index(priv->reports, i); fwupd_codec_add_string(FWUPD_CODEC(report), idt, str); } } } static void fwupd_release_get_property(GObject *obj, guint prop_id, GValue *value, GParamSpec *pspec) { FwupdRelease *self = FWUPD_RELEASE(obj); FwupdReleasePrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_REMOTE_ID: g_value_set_string(value, priv->remote_id); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec); break; } } static void fwupd_release_set_property(GObject *obj, guint prop_id, const GValue *value, GParamSpec *pspec) { FwupdRelease *self = FWUPD_RELEASE(obj); switch (prop_id) { case PROP_REMOTE_ID: fwupd_release_set_remote_id(self, g_value_get_string(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec); break; } } static void fwupd_release_class_init(FwupdReleaseClass *klass) { GParamSpec *pspec; GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fwupd_release_finalize; object_class->get_property = fwupd_release_get_property; object_class->set_property = fwupd_release_set_property; /** * FwupdRelease:remote-id: * * The remote ID. * * Since: 1.8.0 */ pspec = g_param_spec_string("remote-id", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_REMOTE_ID, pspec); } static void fwupd_release_init(FwupdRelease *self) { } static void fwupd_release_finalize(GObject *object) { FwupdRelease *self = FWUPD_RELEASE(object); FwupdReleasePrivate *priv = GET_PRIVATE(self); g_free(priv->description); g_free(priv->filename); g_free(priv->protocol); g_free(priv->appstream_id); g_free(priv->id); g_free(priv->detach_caption); g_free(priv->detach_image); g_free(priv->license); g_free(priv->name); g_free(priv->name_variant_suffix); g_free(priv->summary); g_free(priv->branch); if (priv->locations != NULL) g_ptr_array_unref(priv->locations); g_free(priv->homepage); g_free(priv->details_url); g_free(priv->source_url); g_free(priv->sbom_url); g_free(priv->vendor); g_free(priv->version); g_free(priv->remote_id); g_free(priv->update_message); g_free(priv->update_image); if (priv->categories != NULL) g_ptr_array_unref(priv->categories); if (priv->issues != NULL) g_ptr_array_unref(priv->issues); if (priv->checksums != NULL) g_ptr_array_unref(priv->checksums); if (priv->tags != NULL) g_ptr_array_unref(priv->tags); if (priv->reports != NULL) g_ptr_array_unref(priv->reports); if (priv->metadata != NULL) g_hash_table_unref(priv->metadata); G_OBJECT_CLASS(fwupd_release_parent_class)->finalize(object); } static void fwupd_release_from_variant_iter(FwupdCodec *codec, GVariantIter *iter) { FwupdRelease *self = FWUPD_RELEASE(codec); GVariant *value; const gchar *key; while (g_variant_iter_next(iter, "{&sv}", &key, &value)) { fwupd_release_from_key_value(self, key, value); g_variant_unref(value); } } static void fwupd_release_codec_iface_init(FwupdCodecInterface *iface) { iface->add_string = fwupd_release_add_string; iface->add_json = fwupd_release_add_json; iface->add_variant = fwupd_release_add_variant; iface->from_variant_iter = fwupd_release_from_variant_iter; } /** * fwupd_release_match_flags: * @include: #FwupdReleaseFlags, or %FWUPD_RELEASE_FLAG_NONE * @exclude: #FwupdReleaseFlags, or %FWUPD_RELEASE_FLAG_NONE * * Check if the release flags match. * * Returns: %TRUE if the release flags match * * Since: 1.9.3 **/ gboolean fwupd_release_match_flags(FwupdRelease *self, FwupdReleaseFlags include, FwupdReleaseFlags exclude) { g_return_val_if_fail(FWUPD_IS_RELEASE(self), FALSE); for (guint i = 0; i < 64; i++) { FwupdReleaseFlags flag = 1LLU << i; if ((include & flag) > 0) { if (!fwupd_release_has_flag(self, flag)) return FALSE; } if ((exclude & flag) > 0) { if (fwupd_release_has_flag(self, flag)) return FALSE; } } return TRUE; } /** * fwupd_release_array_filter_flags: * @rels: (not nullable) (element-type FwupdRelease): releases * @include: #FwupdReleaseFlags, or %FWUPD_RELEASE_FLAG_NONE * @exclude: #FwupdReleaseFlags, or %FWUPD_RELEASE_FLAG_NONE * @error: (nullable): optional return location for an error * * Creates an array of new releases that match using fwupd_release_match_flags(). * * Returns: (transfer container) (element-type FwupdRelease): releases * * Since: 1.9.3 **/ GPtrArray * fwupd_release_array_filter_flags(GPtrArray *rels, FwupdReleaseFlags include, FwupdReleaseFlags exclude, GError **error) { g_autoptr(GPtrArray) rels_filtered = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_return_val_if_fail(rels != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); for (guint i = 0; i < rels->len; i++) { FwupdRelease *rel = g_ptr_array_index(rels, i); if (!fwupd_release_match_flags(rel, include, exclude)) continue; g_ptr_array_add(rels_filtered, g_object_ref(rel)); } if (rels_filtered->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "no releases"); return NULL; } return g_steal_pointer(&rels_filtered); } /** * fwupd_release_new: * * Creates a new release. * * Returns: a new #FwupdRelease * * Since: 0.9.3 **/ FwupdRelease * fwupd_release_new(void) { FwupdRelease *self; self = g_object_new(FWUPD_TYPE_RELEASE, NULL); return FWUPD_RELEASE(self); } fwupd-2.0.10/libfwupd/fwupd-release.h000066400000000000000000000171111501337203100174430ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fwupd-enums.h" #include "fwupd-report.h" G_BEGIN_DECLS #define FWUPD_TYPE_RELEASE (fwupd_release_get_type()) G_DECLARE_DERIVABLE_TYPE(FwupdRelease, fwupd_release, FWUPD, RELEASE, GObject) struct _FwupdReleaseClass { GObjectClass parent_class; /*< private >*/ void (*_fwupd_reserved1)(void); void (*_fwupd_reserved2)(void); void (*_fwupd_reserved3)(void); void (*_fwupd_reserved4)(void); void (*_fwupd_reserved5)(void); void (*_fwupd_reserved6)(void); void (*_fwupd_reserved7)(void); }; FwupdRelease * fwupd_release_new(void); const gchar * fwupd_release_get_version(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_version(FwupdRelease *self, const gchar *version) G_GNUC_NON_NULL(1); GPtrArray * fwupd_release_get_locations(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_add_location(FwupdRelease *self, const gchar *location) G_GNUC_NON_NULL(1, 2); GPtrArray * fwupd_release_get_issues(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_add_issue(FwupdRelease *self, const gchar *issue) G_GNUC_NON_NULL(1, 2); GPtrArray * fwupd_release_get_categories(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_add_category(FwupdRelease *self, const gchar *category) G_GNUC_NON_NULL(1, 2); gboolean fwupd_release_has_category(FwupdRelease *self, const gchar *category) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); GPtrArray * fwupd_release_get_checksums(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_add_checksum(FwupdRelease *self, const gchar *checksum) G_GNUC_NON_NULL(1, 2); gboolean fwupd_release_has_checksum(FwupdRelease *self, const gchar *checksum) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); GPtrArray * fwupd_release_get_tags(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_add_tag(FwupdRelease *self, const gchar *tag) G_GNUC_NON_NULL(1, 2); gboolean fwupd_release_has_tag(FwupdRelease *self, const gchar *tag) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); GHashTable * fwupd_release_get_metadata(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_add_metadata(FwupdRelease *self, GHashTable *hash) G_GNUC_NON_NULL(1, 2); void fwupd_release_add_metadata_item(FwupdRelease *self, const gchar *key, const gchar *value) G_GNUC_NON_NULL(1, 2); const gchar * fwupd_release_get_metadata_item(FwupdRelease *self, const gchar *key) G_GNUC_NON_NULL(1, 2); const gchar * fwupd_release_get_filename(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_filename(FwupdRelease *self, const gchar *filename) G_GNUC_NON_NULL(1); const gchar * fwupd_release_get_protocol(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_protocol(FwupdRelease *self, const gchar *protocol) G_GNUC_NON_NULL(1); const gchar * fwupd_release_get_id(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_id(FwupdRelease *self, const gchar *id) G_GNUC_NON_NULL(1); const gchar * fwupd_release_get_appstream_id(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_appstream_id(FwupdRelease *self, const gchar *appstream_id) G_GNUC_NON_NULL(1); const gchar * fwupd_release_get_detach_caption(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_detach_caption(FwupdRelease *self, const gchar *detach_caption) G_GNUC_NON_NULL(1); const gchar * fwupd_release_get_detach_image(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_detach_image(FwupdRelease *self, const gchar *detach_image) G_GNUC_NON_NULL(1); const gchar * fwupd_release_get_remote_id(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_remote_id(FwupdRelease *self, const gchar *remote_id) G_GNUC_NON_NULL(1); const gchar * fwupd_release_get_vendor(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_vendor(FwupdRelease *self, const gchar *vendor) G_GNUC_NON_NULL(1); const gchar * fwupd_release_get_name(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_name(FwupdRelease *self, const gchar *name) G_GNUC_NON_NULL(1); const gchar * fwupd_release_get_name_variant_suffix(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_name_variant_suffix(FwupdRelease *self, const gchar *name_variant_suffix) G_GNUC_NON_NULL(1); const gchar * fwupd_release_get_summary(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_summary(FwupdRelease *self, const gchar *summary) G_GNUC_NON_NULL(1); const gchar * fwupd_release_get_branch(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_branch(FwupdRelease *self, const gchar *branch) G_GNUC_NON_NULL(1); const gchar * fwupd_release_get_description(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_description(FwupdRelease *self, const gchar *description) G_GNUC_NON_NULL(1); const gchar * fwupd_release_get_homepage(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_homepage(FwupdRelease *self, const gchar *homepage) G_GNUC_NON_NULL(1); const gchar * fwupd_release_get_details_url(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_details_url(FwupdRelease *self, const gchar *details_url) G_GNUC_NON_NULL(1); const gchar * fwupd_release_get_source_url(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_source_url(FwupdRelease *self, const gchar *source_url) G_GNUC_NON_NULL(1); const gchar * fwupd_release_get_sbom_url(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_sbom_url(FwupdRelease *self, const gchar *sbom_url) G_GNUC_NON_NULL(1); guint64 fwupd_release_get_size(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_size(FwupdRelease *self, guint64 size) G_GNUC_NON_NULL(1); guint64 fwupd_release_get_created(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_created(FwupdRelease *self, guint64 created) G_GNUC_NON_NULL(1); const gchar * fwupd_release_get_license(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_license(FwupdRelease *self, const gchar *license) G_GNUC_NON_NULL(1); FwupdReleaseFlags fwupd_release_get_flags(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_flags(FwupdRelease *self, FwupdReleaseFlags flags) G_GNUC_NON_NULL(1); void fwupd_release_add_flag(FwupdRelease *self, FwupdReleaseFlags flag) G_GNUC_NON_NULL(1); void fwupd_release_remove_flag(FwupdRelease *self, FwupdReleaseFlags flag) G_GNUC_NON_NULL(1); gboolean fwupd_release_has_flag(FwupdRelease *self, FwupdReleaseFlags flag) G_GNUC_WARN_UNUSED_RESULT; FwupdReleaseUrgency fwupd_release_get_urgency(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_urgency(FwupdRelease *self, FwupdReleaseUrgency urgency) G_GNUC_NON_NULL(1); guint32 fwupd_release_get_install_duration(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_install_duration(FwupdRelease *self, guint32 duration) G_GNUC_NON_NULL(1); const gchar * fwupd_release_get_update_message(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_update_message(FwupdRelease *self, const gchar *update_message) G_GNUC_NON_NULL(1); const gchar * fwupd_release_get_update_image(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_set_update_image(FwupdRelease *self, const gchar *update_image) G_GNUC_NON_NULL(1); GPtrArray * fwupd_release_get_reports(FwupdRelease *self) G_GNUC_NON_NULL(1); void fwupd_release_add_report(FwupdRelease *self, FwupdReport *report) G_GNUC_NON_NULL(1); gboolean fwupd_release_match_flags(FwupdRelease *self, FwupdReleaseFlags include, FwupdReleaseFlags exclude) G_GNUC_NON_NULL(1); GPtrArray * fwupd_release_array_filter_flags(GPtrArray *rels, FwupdReleaseFlags include, FwupdReleaseFlags exclude, GError **error) G_GNUC_NON_NULL(1); G_END_DECLS fwupd-2.0.10/libfwupd/fwupd-remote-private.h000066400000000000000000000046641501337203100207770ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fwupd-build.h" #include "fwupd-remote.h" G_BEGIN_DECLS void fwupd_remote_set_kind(FwupdRemote *self, FwupdRemoteKind kind) G_GNUC_NON_NULL(1); void fwupd_remote_set_id(FwupdRemote *self, const gchar *id) G_GNUC_NON_NULL(1); void fwupd_remote_set_title(FwupdRemote *self, const gchar *title) G_GNUC_NON_NULL(1); void fwupd_remote_set_privacy_uri(FwupdRemote *self, const gchar *privacy_uri) G_GNUC_NON_NULL(1); void fwupd_remote_set_priority(FwupdRemote *self, gint priority) G_GNUC_NON_NULL(1); void fwupd_remote_set_agreement(FwupdRemote *self, const gchar *agreement) G_GNUC_NON_NULL(1); void fwupd_remote_set_checksum_sig(FwupdRemote *self, const gchar *checksum_sig) G_GNUC_NON_NULL(1); void fwupd_remote_set_filename_cache(FwupdRemote *self, const gchar *filename) G_GNUC_NON_NULL(1); void fwupd_remote_set_metadata_uri(FwupdRemote *self, const gchar *metadata_uri) G_GNUC_NON_NULL(1); void fwupd_remote_set_mtime(FwupdRemote *self, guint64 mtime) G_GNUC_NON_NULL(1); gchar ** fwupd_remote_get_order_after(FwupdRemote *self) G_GNUC_NON_NULL(1); gchar ** fwupd_remote_get_order_before(FwupdRemote *self) G_GNUC_NON_NULL(1); void fwupd_remote_set_order_after(FwupdRemote *self, const gchar *ids) G_GNUC_NON_NULL(1); void fwupd_remote_set_order_before(FwupdRemote *self, const gchar *ids) G_GNUC_NON_NULL(1); void fwupd_remote_set_refresh_interval(FwupdRemote *self, guint64 refresh_interval) G_GNUC_NON_NULL(1); void fwupd_remote_set_username(FwupdRemote *self, const gchar *username) G_GNUC_NON_NULL(1); void fwupd_remote_set_password(FwupdRemote *self, const gchar *password) G_GNUC_NON_NULL(1); void fwupd_remote_set_report_uri(FwupdRemote *self, const gchar *report_uri) G_GNUC_NON_NULL(1); void fwupd_remote_set_firmware_base_uri(FwupdRemote *self, const gchar *firmware_base_uri) G_GNUC_NON_NULL(1); void fwupd_remote_set_remotes_dir(FwupdRemote *self, const gchar *directory) G_GNUC_NON_NULL(1); void fwupd_remote_set_filename_source(FwupdRemote *self, const gchar *filename_source) G_GNUC_NON_NULL(1); gboolean fwupd_remote_setup(FwupdRemote *self, GError **error) G_GNUC_NON_NULL(1); gchar * fwupd_remote_build_metadata_sig_uri(FwupdRemote *self, GError **error) G_GNUC_NON_NULL(1); gchar * fwupd_remote_build_metadata_uri(FwupdRemote *self, GError **error) G_GNUC_NON_NULL(1); G_END_DECLS fwupd-2.0.10/libfwupd/fwupd-remote.c000066400000000000000000001466521501337203100173260ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fwupd-codec.h" #include "fwupd-common-private.h" #include "fwupd-enums-private.h" #include "fwupd-error.h" #include "fwupd-remote-private.h" /** * FwupdRemote: * * A source of metadata that provides firmware. * * Remotes can be local (e.g. folders on a disk) or remote (e.g. downloaded * over HTTP or IPFS). * * See also: [class@FwupdClient] */ static void fwupd_remote_finalize(GObject *obj); typedef struct { FwupdRemoteKind kind; FwupdRemoteFlags flags; gchar *id; gchar *firmware_base_uri; gchar *report_uri; gchar *metadata_uri; gchar *metadata_uri_sig; gchar *username; gchar *password; gchar *title; gchar *privacy_uri; gchar *agreement; gchar *checksum; /* of metadata */ gchar *checksum_sig; /* of the signature */ gchar *filename_cache; gchar *filename_cache_sig; gchar *filename_source; gint priority; guint64 mtime; guint64 refresh_interval; gchar **order_after; gchar **order_before; gchar *remotes_dir; } FwupdRemotePrivate; enum { PROP_0, PROP_ID, PROP_ENABLED, PROP_APPROVAL_REQUIRED, PROP_AUTOMATIC_REPORTS, PROP_AUTOMATIC_SECURITY_REPORTS, PROP_FLAGS, PROP_LAST }; static void fwupd_remote_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_EXTENDED(FwupdRemote, fwupd_remote, G_TYPE_OBJECT, 0, G_ADD_PRIVATE(FwupdRemote) G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fwupd_remote_codec_iface_init)); #define GET_PRIVATE(o) (fwupd_remote_get_instance_private(o)) typedef gchar curlptr; G_DEFINE_AUTOPTR_CLEANUP_FUNC(curlptr, curl_free) G_DEFINE_AUTOPTR_CLEANUP_FUNC(CURLU, curl_url_cleanup) /** * fwupd_remote_flag_to_string: * @flag: remote attribute flags, e.g. %FWUPD_REMOTE_FLAG_ENABLED * * Returns the printable string for the flag. * * Returns: string, or %NULL * * Since: 1.9.4 **/ const gchar * fwupd_remote_flag_to_string(FwupdRemoteFlags flag) { if (flag == FWUPD_REMOTE_FLAG_NONE) return "none"; if (flag == FWUPD_REMOTE_FLAG_ENABLED) return "enabled"; if (flag == FWUPD_REMOTE_FLAG_APPROVAL_REQUIRED) return "approval-required"; if (flag == FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS) return "automatic-reports"; if (flag == FWUPD_REMOTE_FLAG_AUTOMATIC_SECURITY_REPORTS) return "automatic-security-reports"; if (flag == FWUPD_REMOTE_FLAG_ALLOW_P2P_METADATA) return "allow-p2p-metadata"; if (flag == FWUPD_REMOTE_FLAG_ALLOW_P2P_FIRMWARE) return "allow-p2p-firmware"; return NULL; } /** * fwupd_remote_flag_from_string: * @flag: (nullable): a string, e.g. `enabled` * * Converts a string to an enumerated flag. * * Returns: enumerated value * * Since: 1.9.4 **/ FwupdRemoteFlags fwupd_remote_flag_from_string(const gchar *flag) { if (g_strcmp0(flag, "enabled") == 0) return FWUPD_REMOTE_FLAG_ENABLED; if (g_strcmp0(flag, "approval-required") == 0) return FWUPD_REMOTE_FLAG_APPROVAL_REQUIRED; if (g_strcmp0(flag, "automatic-reports") == 0) return FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS; if (g_strcmp0(flag, "automatic-security-reports") == 0) return FWUPD_REMOTE_FLAG_AUTOMATIC_SECURITY_REPORTS; if (g_strcmp0(flag, "allow-p2p-metadata") == 0) return FWUPD_REMOTE_FLAG_ALLOW_P2P_METADATA; if (g_strcmp0(flag, "allow-p2p-firmware") == 0) return FWUPD_REMOTE_FLAG_ALLOW_P2P_FIRMWARE; return FWUPD_REMOTE_FLAG_NONE; } static void fwupd_remote_add_json(FwupdCodec *codec, JsonBuilder *builder, FwupdCodecFlags flags) { FwupdRemote *self = FWUPD_REMOTE(codec); FwupdRemotePrivate *priv = GET_PRIVATE(self); fwupd_codec_json_append(builder, "Id", priv->id); if (priv->kind != FWUPD_REMOTE_KIND_UNKNOWN) { fwupd_codec_json_append(builder, "Kind", fwupd_remote_kind_to_string(priv->kind)); } fwupd_codec_json_append(builder, "ReportUri", priv->report_uri); fwupd_codec_json_append(builder, "MetadataUri", priv->metadata_uri); fwupd_codec_json_append(builder, "MetadataUriSig", priv->metadata_uri_sig); fwupd_codec_json_append(builder, "FirmwareBaseUri", priv->firmware_base_uri); fwupd_codec_json_append(builder, "Username", priv->username); fwupd_codec_json_append(builder, "Password", priv->password); fwupd_codec_json_append(builder, "Title", priv->title); fwupd_codec_json_append(builder, "PrivacyUri", priv->privacy_uri); fwupd_codec_json_append(builder, "Agreement", priv->agreement); fwupd_codec_json_append(builder, "Checksum", priv->checksum); fwupd_codec_json_append(builder, "ChecksumSig", priv->checksum_sig); fwupd_codec_json_append(builder, "FilenameCache", priv->filename_cache); fwupd_codec_json_append(builder, "FilenameCacheSig", priv->filename_cache_sig); fwupd_codec_json_append(builder, "FilenameSource", priv->filename_source); fwupd_codec_json_append_int(builder, "Flags", priv->flags); fwupd_codec_json_append_bool(builder, "Enabled", fwupd_remote_has_flag(self, FWUPD_REMOTE_FLAG_ENABLED)); fwupd_codec_json_append_bool( builder, "ApprovalRequired", fwupd_remote_has_flag(self, FWUPD_REMOTE_FLAG_APPROVAL_REQUIRED)); fwupd_codec_json_append_bool( builder, "AutomaticReports", fwupd_remote_has_flag(self, FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS)); fwupd_codec_json_append_bool( builder, "AutomaticSecurityReports", fwupd_remote_has_flag(self, FWUPD_REMOTE_FLAG_AUTOMATIC_SECURITY_REPORTS)); fwupd_codec_json_append_int(builder, "Priority", priv->priority); fwupd_codec_json_append_int(builder, "Mtime", priv->mtime); fwupd_codec_json_append_int(builder, "RefreshInterval", priv->refresh_interval); fwupd_codec_json_append(builder, "RemotesDir", priv->remotes_dir); fwupd_codec_json_append_strv(builder, "OrderAfter", priv->order_after); fwupd_codec_json_append_strv(builder, "OrderBefore", priv->order_before); } /** * fwupd_remote_get_flags: * @self: a #FwupdRemote * * Gets the self flags. * * Returns: remote attribute flags, or 0 if unset * * Since: 1.9.4 **/ FwupdRemoteFlags fwupd_remote_get_flags(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), 0); return priv->flags; } /** * fwupd_remote_set_flags: * @self: a #FwupdRemote * @flags: remote attribute flags, e.g. %FWUPD_REMOTE_FLAG_APPROVAL_REQUIRED * * Sets the attribute flags. * * Since: 1.9.4 **/ void fwupd_remote_set_flags(FwupdRemote *self, FwupdRemoteFlags flags) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REMOTE(self)); if (flags == priv->flags) return; priv->flags = flags; g_object_notify(G_OBJECT(self), "flags"); } /** * fwupd_remote_add_flag: * @self: a #FwupdRemote * @flag: the #FwupdRemoteFlags, e.g. %FWUPD_REMOTE_FLAG_APPROVAL_REQUIRED * * Adds a specific attribute flag to the attribute. * * Since: 1.9.4 **/ void fwupd_remote_add_flag(FwupdRemote *self, FwupdRemoteFlags flag) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REMOTE(self)); priv->flags |= flag; g_object_notify(G_OBJECT(self), "flags"); } /** * fwupd_remote_remove_flag: * @self: a #FwupdRemote * @flag: the #FwupdRemoteFlags, e.g. %FWUPD_REMOTE_FLAG_APPROVAL_REQUIRED * * Removes a specific attribute flag from the remote. * * Since: 1.9.4 **/ void fwupd_remote_remove_flag(FwupdRemote *self, FwupdRemoteFlags flag) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REMOTE(self)); priv->flags &= ~flag; g_object_notify(G_OBJECT(self), "flags"); } /** * fwupd_remote_has_flag: * @self: a #FwupdRemote * @flag: the remote flag, e.g. %FWUPD_REMOTE_FLAG_APPROVAL_REQUIRED * * Finds if the remote has a specific flag. * * Returns: %TRUE if the flag is set * * Since: 1.9.4 **/ gboolean fwupd_remote_has_flag(FwupdRemote *self, FwupdRemoteFlags flag) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), FALSE); return (priv->flags & flag) > 0; } static gchar * fwupd_remote_strdup_nonempty(const gchar *text) { if (text == NULL || text[0] == '\0') return NULL; return g_strdup(text); } /** * fwupd_remote_set_username: * @self: a #FwupdRemote * @username: (nullable): an optional username * * Sets the remote username. * * Since: 2.0.0 **/ void fwupd_remote_set_username(FwupdRemote *self, const gchar *username) { FwupdRemotePrivate *priv = GET_PRIVATE(self); /* not changed */ if (g_strcmp0(priv->username, username) == 0) return; g_free(priv->username); priv->username = g_strdup(username); } /** * fwupd_remote_set_title: * @self: a #FwupdRemote * @title: (nullable): title text, e.g. "Backup" * * Sets the remote title. * * Since: 1.8.13 **/ void fwupd_remote_set_title(FwupdRemote *self, const gchar *title) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REMOTE(self)); /* not changed */ if (g_strcmp0(priv->title, title) == 0) return; g_free(priv->title); priv->title = g_strdup(title); } /** * fwupd_remote_set_privacy_uri: * @self: a #FwupdRemote * @privacy_uri: (nullable): privacy URL, e.g. "https://lvfs.readthedocs.io/en/latest/privacy.html" * * Sets the remote privacy policy URL. * * Since: 2.0.0 **/ void fwupd_remote_set_privacy_uri(FwupdRemote *self, const gchar *privacy_uri) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REMOTE(self)); /* not changed */ if (g_strcmp0(priv->privacy_uri, privacy_uri) == 0) return; g_free(priv->privacy_uri); priv->privacy_uri = g_strdup(privacy_uri); } /** * fwupd_remote_set_agreement: * @self: a #FwupdRemote * @agreement: (nullable): agreement markup text * * Sets the remote agreement in AppStream markup format * * Since: 1.0.7 **/ void fwupd_remote_set_agreement(FwupdRemote *self, const gchar *agreement) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REMOTE(self)); /* not changed */ if (g_strcmp0(priv->agreement, agreement) == 0) return; g_free(priv->agreement); priv->agreement = g_strdup(agreement); } /** * fwupd_remote_set_checksum_sig: * @self: a #FwupdRemote * @checksum_sig: (nullable): checksum string * * Sets the remote signature checksum, typically only useful in the self tests. * * Since: 2.0.0 **/ void fwupd_remote_set_checksum_sig(FwupdRemote *self, const gchar *checksum_sig) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REMOTE(self)); /* not changed */ if (g_strcmp0(priv->checksum_sig, checksum_sig) == 0) return; g_free(priv->checksum_sig); priv->checksum_sig = g_strdup(checksum_sig); } static void fwupd_remote_set_checksum_sig_metadata(FwupdRemote *self, const gchar *checksum) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REMOTE(self)); /* not changed */ if (g_strcmp0(priv->checksum, checksum) == 0) return; g_free(priv->checksum); priv->checksum = g_strdup(checksum); } /** * fwupd_remote_set_password: * @self: a #FwupdRemote * @password: (nullable): an optional password * * Sets the remote password. * * Since: 2.0.0 **/ void fwupd_remote_set_password(FwupdRemote *self, const gchar *password) { FwupdRemotePrivate *priv = GET_PRIVATE(self); /* not changed */ if (g_strcmp0(priv->password, password) == 0) return; g_free(priv->password); priv->password = g_strdup(password); } /** * fwupd_remote_set_kind: * @self: a #FwupdRemote * @kind: a #FwupdRemoteKind, e.g. #FWUPD_REMOTE_KIND_LOCAL * * Sets the kind of the remote. * * Since: 2.0.0 **/ void fwupd_remote_set_kind(FwupdRemote *self, FwupdRemoteKind kind) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REMOTE(self)); priv->kind = kind; } /** * fwupd_remote_set_id: * @self: a #FwupdRemote * @id: (nullable): remote ID, e.g. "lvfs" * * Sets the remote ID. * * NOTE: the ID has to be set before the URL. * * Since: 1.9.3 **/ void fwupd_remote_set_id(FwupdRemote *self, const gchar *id) { FwupdRemotePrivate *priv = GET_PRIVATE(self); /* not changed */ if (g_strcmp0(priv->id, id) == 0) return; g_free(priv->id); priv->id = g_strdup(id); g_strdelimit(priv->id, ".", '\0'); } /** * fwupd_remote_set_filename_source: * @self: a #FwupdRemote * @filename_source: (nullable): filename * * Sets the source filename. This is typically a file in `/etc/fwupd/remotes/`. * * Since: 1.6.1 **/ void fwupd_remote_set_filename_source(FwupdRemote *self, const gchar *filename_source) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REMOTE(self)); if (priv->filename_source == filename_source) return; g_free(priv->filename_source); priv->filename_source = g_strdup(filename_source); } static gchar * fwupd_remote_build_uri(FwupdRemote *self, const gchar *base_uri, const gchar *url_noauth, GError **error) { FwupdRemotePrivate *priv = GET_PRIVATE(self); const gchar *path_suffix = NULL; g_autoptr(curlptr) tmp_uri = NULL; g_autoptr(CURLU) uri = curl_url(); /* sanity check */ if (url_noauth == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "no URI set"); return NULL; } /* the LVFS can't accept basic auth on an endpoint not expecting authentication */ if (!g_str_has_suffix(url_noauth, "/auth") && (priv->username != NULL || priv->password != NULL)) { path_suffix = "auth"; } /* create URI, substituting if required */ if (base_uri != NULL) { g_autofree gchar *basename = NULL; g_autofree gchar *path_new = NULL; g_autoptr(curlptr) path = NULL; g_autoptr(CURLU) uri_tmp = curl_url(); if (curl_url_set(uri_tmp, CURLUPART_URL, url_noauth, 0) != CURLUE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to parse url '%s'", url_noauth); return NULL; } (void)curl_url_get(uri_tmp, CURLUPART_PATH, &path, 0); basename = g_path_get_basename(path); path_new = g_build_filename(priv->firmware_base_uri, basename, path_suffix, NULL); (void)curl_url_set(uri, CURLUPART_URL, path_new, 0); } else if (g_strstr_len(url_noauth, -1, "/") == NULL) { g_autofree gchar *basename = NULL; g_autofree gchar *path_new = NULL; g_autoptr(curlptr) path = NULL; /* use the base URI of the metadata to build the full path */ if (curl_url_set(uri, CURLUPART_URL, priv->metadata_uri, 0) != CURLUE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to parse url '%s'", priv->metadata_uri); return NULL; } (void)curl_url_get(uri, CURLUPART_PATH, &path, 0); basename = g_path_get_dirname(path); path_new = g_build_filename(basename, url_noauth, NULL); (void)curl_url_set(uri, CURLUPART_URL, path_new, 0); } else { g_autofree gchar *url = g_build_filename(url_noauth, path_suffix, NULL); /* a normal URI */ if (curl_url_set(uri, CURLUPART_URL, url, 0) != CURLUE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to parse URI '%s'", url); return NULL; } } /* set the escaped username and password */ if (priv->username != NULL) { g_autofree gchar *user_escaped = g_uri_escape_string(priv->username, NULL, FALSE); (void)curl_url_set(uri, CURLUPART_USER, user_escaped, 0); } if (priv->password != NULL) { g_autofree gchar *pass_escaped = g_uri_escape_string(priv->password, NULL, FALSE); (void)curl_url_set(uri, CURLUPART_PASSWORD, pass_escaped, 0); } (void)curl_url_get(uri, CURLUPART_URL, &tmp_uri, 0); return g_strdup(tmp_uri); } /** * fwupd_remote_set_metadata_uri: * @self: a #FwupdRemote * @metadata_uri: (nullable): metadata URI * * Sets the remote metadata URI. * * NOTE: This has to be set before the username and password. * * Since: 1.8.13 **/ void fwupd_remote_set_metadata_uri(FwupdRemote *self, const gchar *metadata_uri) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REMOTE(self)); /* not changed */ if (g_strcmp0(priv->metadata_uri, metadata_uri) == 0) return; /* save this so we can export the object as a GVariant */ g_free(priv->metadata_uri); priv->metadata_uri = g_strdup(metadata_uri); /* generate the signature URI too */ g_free(priv->metadata_uri_sig); priv->metadata_uri_sig = g_strconcat(metadata_uri, ".jcat", NULL); } /** * fwupd_remote_set_firmware_base_uri: * @self: a #FwupdRemote * @firmware_base_uri: (nullable): base URI for firmware * * Sets the firmware base URI. * * NOTE: This has to be set after MetadataURI. * * Since: 2.0.2 **/ void fwupd_remote_set_firmware_base_uri(FwupdRemote *self, const gchar *firmware_base_uri) { FwupdRemotePrivate *priv = GET_PRIVATE(self); /* not changed */ if (g_strcmp0(priv->firmware_base_uri, firmware_base_uri) == 0) return; g_free(priv->firmware_base_uri); priv->firmware_base_uri = g_strdup(firmware_base_uri); } /** * fwupd_remote_set_report_uri: * @self: a #FwupdRemote * @report_uri: (nullable): report URI * * Sets the report URI. * * Since: 2.0.0 **/ void fwupd_remote_set_report_uri(FwupdRemote *self, const gchar *report_uri) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_autofree gchar *report_uri_safe = fwupd_remote_strdup_nonempty(report_uri); /* not changed */ if (g_strcmp0(priv->report_uri, report_uri_safe) == 0) return; g_free(priv->report_uri); priv->report_uri = g_steal_pointer(&report_uri_safe); } /** * fwupd_remote_kind_from_string: * @kind: (nullable): a string, e.g. `download` * * Converts an printable string to an enumerated type. * * Returns: a #FwupdRemoteKind, e.g. %FWUPD_REMOTE_KIND_DOWNLOAD * * Since: 0.9.6 **/ FwupdRemoteKind fwupd_remote_kind_from_string(const gchar *kind) { if (g_strcmp0(kind, "download") == 0) return FWUPD_REMOTE_KIND_DOWNLOAD; if (g_strcmp0(kind, "local") == 0) return FWUPD_REMOTE_KIND_LOCAL; if (g_strcmp0(kind, "directory") == 0) return FWUPD_REMOTE_KIND_DIRECTORY; return FWUPD_REMOTE_KIND_UNKNOWN; } /** * fwupd_remote_kind_to_string: * @kind: a #FwupdRemoteKind, e.g. %FWUPD_REMOTE_KIND_DOWNLOAD * * Converts an enumerated type to a printable string. * * Returns: a string, e.g. `download` * * Since: 0.9.6 **/ const gchar * fwupd_remote_kind_to_string(FwupdRemoteKind kind) { if (kind == FWUPD_REMOTE_KIND_DOWNLOAD) return "download"; if (kind == FWUPD_REMOTE_KIND_LOCAL) return "local"; if (kind == FWUPD_REMOTE_KIND_DIRECTORY) return "directory"; return NULL; } /** * fwupd_remote_set_filename_cache: * @self: a #FwupdRemote * @filename: (nullable): filename string * * Sets the remote filename cache filename, typically only useful in the self tests. * * Since: 1.8.2 **/ void fwupd_remote_set_filename_cache(FwupdRemote *self, const gchar *filename) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REMOTE(self)); /* not changed */ if (g_strcmp0(priv->filename_cache, filename) == 0) return; g_free(priv->filename_cache); priv->filename_cache = g_strdup(filename); /* create for all non-local remote types */ if (priv->kind != FWUPD_REMOTE_KIND_LOCAL) { g_free(priv->filename_cache_sig); priv->filename_cache_sig = g_strconcat(filename, ".jcat", NULL); } } /** * fwupd_remote_set_order_before: * @self: a #FwupdRemote * @ids: (nullable): optional remote IDs * * Sets any remotes that should be ordered before this one. * * Since: 2.0.0 **/ void fwupd_remote_set_order_before(FwupdRemote *self, const gchar *ids) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_clear_pointer(&priv->order_before, g_strfreev); if (ids != NULL) priv->order_before = g_strsplit_set(ids, ",:;", -1); } /** * fwupd_remote_set_order_after: * @self: a #FwupdRemote * @ids: (nullable): optional remote IDs * * Sets any remotes that should be ordered after this one. * * Since: 2.0.0 **/ void fwupd_remote_set_order_after(FwupdRemote *self, const gchar *ids) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_clear_pointer(&priv->order_after, g_strfreev); if (ids != NULL) priv->order_after = g_strsplit_set(ids, ",:;", -1); } /** * fwupd_remote_setup: * @self: a #FwupdRemote * @error: (nullable): optional return location for an error * * Sets up the remote ready for use, checking that required parameters have * been set. Calling this method multiple times has no effect. * * Returns: %TRUE for success * * Since: 1.6.1 **/ gboolean fwupd_remote_setup(FwupdRemote *self, GError **error) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* we can override, hence the extra section */ if (priv->kind == FWUPD_REMOTE_KIND_UNKNOWN) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "metadata kind invalid"); return FALSE; } /* some validation for DOWNLOAD types */ if (priv->kind == FWUPD_REMOTE_KIND_DOWNLOAD) { g_autofree gchar *filename_cache = NULL; if (priv->remotes_dir == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "remotes directory not set"); return FALSE; } /* set cache to /var/lib... */ if (priv->metadata_uri == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "metadata URI not set"); return FALSE; } if (g_str_has_suffix(priv->metadata_uri, ".xml.zst")) { filename_cache = g_build_filename(priv->remotes_dir, priv->id, "firmware.xml.zst", NULL); } else if (g_str_has_suffix(priv->metadata_uri, ".xml.xz")) { filename_cache = g_build_filename(priv->remotes_dir, priv->id, "firmware.xml.xz", NULL); } else { filename_cache = g_build_filename(priv->remotes_dir, priv->id, "firmware.xml.gz", NULL); } fwupd_remote_set_filename_cache(self, filename_cache); } /* some validation for DIRECTORY types */ if (priv->kind == FWUPD_REMOTE_KIND_DIRECTORY) { if (priv->firmware_base_uri != NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "Directory remotes don't support firmware base URI"); return FALSE; } } /* load the signature checksum */ if (priv->filename_cache_sig != NULL && g_file_test(priv->filename_cache_sig, G_FILE_TEST_EXISTS)) { gsize sz = 0; g_autofree gchar *buf = NULL; g_autoptr(GChecksum) checksum_sig = g_checksum_new(G_CHECKSUM_SHA256); if (!g_file_get_contents(priv->filename_cache_sig, &buf, &sz, error)) { g_prefix_error(error, "failed to get signature checksum: "); return FALSE; } g_checksum_update(checksum_sig, (guchar *)buf, (gssize)sz); fwupd_remote_set_checksum_sig(self, g_checksum_get_string(checksum_sig)); } else { fwupd_remote_set_checksum_sig(self, NULL); } /* success */ return TRUE; } /** * fwupd_remote_get_order_after: * @self: a #FwupdRemote * * Gets the list of remotes this plugin should be ordered after. * * Returns: (transfer none): an array * * Since: 0.9.5 **/ gchar ** fwupd_remote_get_order_after(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), NULL); return priv->order_after; } /** * fwupd_remote_get_order_before: * @self: a #FwupdRemote * * Gets the list of remotes this plugin should be ordered before. * * Returns: (transfer none): an array * * Since: 0.9.5 **/ gchar ** fwupd_remote_get_order_before(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), NULL); return priv->order_before; } /** * fwupd_remote_get_filename_cache: * @self: a #FwupdRemote * * Gets the path and filename that the remote is using for a cache. * * Returns: a string, or %NULL for unset * * Since: 0.9.6 **/ const gchar * fwupd_remote_get_filename_cache(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), NULL); return priv->filename_cache; } /** * fwupd_remote_get_filename_cache_sig: * @self: a #FwupdRemote * * Gets the path and filename that the remote is using for a signature cache. * * Returns: a string, or %NULL for unset * * Since: 0.9.7 **/ const gchar * fwupd_remote_get_filename_cache_sig(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), NULL); return priv->filename_cache_sig; } /** * fwupd_remote_get_filename_source: * @self: a #FwupdRemote * * Gets the path and filename of the remote itself, typically a `.conf` file. * * Returns: a string, or %NULL for unset * * Since: 0.9.8 **/ const gchar * fwupd_remote_get_filename_source(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), NULL); return priv->filename_source; } /** * fwupd_remote_get_priority: * @self: a #FwupdRemote * * Gets the priority of the remote, where bigger numbers are better. * * Returns: a priority, or 0 for the default value * * Since: 0.9.5 **/ gint fwupd_remote_get_priority(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), 0); return priv->priority; } /** * fwupd_remote_get_kind: * @self: a #FwupdRemote * * Gets the kind of the remote. * * Returns: a #FwupdRemoteKind, e.g. #FWUPD_REMOTE_KIND_LOCAL * * Since: 0.9.6 **/ FwupdRemoteKind fwupd_remote_get_kind(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), 0); return priv->kind; } /** * fwupd_remote_get_age: * @self: a #FwupdRemote * * Gets the age of the remote in seconds. * * Returns: a age, or %G_MAXUINT64 for unavailable * * Since: 0.9.5 **/ guint64 fwupd_remote_get_age(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); guint64 now; g_return_val_if_fail(FWUPD_IS_REMOTE(self), 0); now = (guint64)g_get_real_time() / G_USEC_PER_SEC; if (priv->mtime > now) return G_MAXUINT64; return now - priv->mtime; } /** * fwupd_remote_set_remotes_dir: * @self: a #FwupdRemote * @directory: (nullable): Remotes directory * * Sets the directory to store remote data * * Since: 1.3.1 **/ void fwupd_remote_set_remotes_dir(FwupdRemote *self, const gchar *directory) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REMOTE(self)); /* not changed */ if (g_strcmp0(priv->remotes_dir, directory) == 0) return; g_free(priv->remotes_dir); priv->remotes_dir = g_strdup(directory); } /** * fwupd_remote_set_priority: * @self: a #FwupdRemote * @priority: an integer, where 1 is better * * Sets the plugin priority. * * Since: 0.9.5 **/ void fwupd_remote_set_priority(FwupdRemote *self, gint priority) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REMOTE(self)); priv->priority = priority; } /** * fwupd_remote_set_mtime: * @self: a #FwupdRemote * @mtime: a UNIX timestamp * * Sets the plugin modification time. * * Since: 0.9.5 **/ void fwupd_remote_set_mtime(FwupdRemote *self, guint64 mtime) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REMOTE(self)); priv->mtime = mtime; } /** * fwupd_remote_get_refresh_interval: * @self: a #FwupdRemote * * Gets the plugin refresh interval in seconds. * * Returns: value in seconds * * Since: 1.9.4 **/ guint64 fwupd_remote_get_refresh_interval(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), G_MAXUINT64); return priv->refresh_interval; } /** * fwupd_remote_set_refresh_interval: * @self: a #FwupdRemote * @refresh_interval: value in seconds * * Sets the plugin refresh interval in seconds. * * Since: 2.0.0 **/ void fwupd_remote_set_refresh_interval(FwupdRemote *self, guint64 refresh_interval) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REMOTE(self)); priv->refresh_interval = refresh_interval; } /** * fwupd_remote_get_username: * @self: a #FwupdRemote * * Gets the username configured for the remote. * * Returns: a string, or %NULL for unset * * Since: 0.9.5 **/ const gchar * fwupd_remote_get_username(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), NULL); return priv->username; } /** * fwupd_remote_get_password: * @self: a #FwupdRemote * * Gets the password configured for the remote. * * Returns: a string, or %NULL for unset * * Since: 0.9.5 **/ const gchar * fwupd_remote_get_password(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), NULL); return priv->password; } /** * fwupd_remote_get_title: * @self: a #FwupdRemote * * Gets the remote title, e.g. `Linux Vendor Firmware Service`. * * Returns: a string, or %NULL if unset * * Since: 0.9.8 **/ const gchar * fwupd_remote_get_title(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), NULL); return priv->title; } /** * fwupd_remote_get_privacy_uri: * @self: a #FwupdRemote * * Gets the remote privacy policy URL, e.g. `https://lvfs.readthedocs.io/en/latest/privacy.html` * * Returns: a string, or %NULL if unset * * Since: 2.0.0 **/ const gchar * fwupd_remote_get_privacy_uri(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), NULL); return priv->privacy_uri; } /** * fwupd_remote_get_agreement: * @self: a #FwupdRemote * * Gets the remote agreement in AppStream markup format * * Returns: a string, or %NULL if unset * * Since: 1.0.7 **/ const gchar * fwupd_remote_get_agreement(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), NULL); return priv->agreement; } /** * fwupd_remote_get_remotes_dir: * @self: a #FwupdRemote * * Gets the base directory for storing remote metadata * * Returns: a string, or %NULL if unset * * Since: 1.3.1 **/ const gchar * fwupd_remote_get_remotes_dir(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), NULL); return priv->remotes_dir; } /** * fwupd_remote_get_checksum: * @self: a #FwupdRemote * * Gets the remote signature checksum. * * Returns: a string, or %NULL if unset * * Since: 1.0.0 **/ const gchar * fwupd_remote_get_checksum(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), NULL); return priv->checksum_sig; } /** * fwupd_remote_get_checksum_metadata: * @self: a #FwupdRemote * * Gets the remote metadata checksum. * * Returns: a string, or %NULL if unset * * Since: 1.9.4 **/ const gchar * fwupd_remote_get_checksum_metadata(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), NULL); return priv->checksum; } /** * fwupd_remote_build_firmware_uri: * @self: a #FwupdRemote * @url: (not nullable): the URL to use * @error: (nullable): optional return location for an error * * Builds a URI for the URL using the username and password set for the remote, * including any basename URI substitution. * * Returns: (transfer full): a URI, or %NULL for error * * Since: 0.9.7 **/ gchar * fwupd_remote_build_firmware_uri(FwupdRemote *self, const gchar *url, GError **error) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), NULL); g_return_val_if_fail(url != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return fwupd_remote_build_uri(self, priv->firmware_base_uri, url, error); } /** * fwupd_remote_build_report_uri: * @self: a #FwupdRemote * @error: (nullable): optional return location for an error * * Builds a URI for the URL using the username and password set for the remote. * * Returns: (transfer full): a URI, or %NULL for error * * Since: 1.9.1 **/ gchar * fwupd_remote_build_report_uri(FwupdRemote *self, GError **error) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return fwupd_remote_build_uri(self, NULL, priv->report_uri, error); } /** * fwupd_remote_build_metadata_sig_uri: * @self: a #FwupdRemote * @error: (nullable): optional return location for an error * * Builds a URI for the metadata using the username and password set for the remote. * * Returns: (transfer full): a URI, or %NULL for error * * Since: 1.9.8 **/ gchar * fwupd_remote_build_metadata_sig_uri(FwupdRemote *self, GError **error) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return fwupd_remote_build_uri(self, NULL, priv->metadata_uri_sig, error); } /** * fwupd_remote_build_metadata_uri: * @self: a #FwupdRemote * @error: (nullable): optional return location for an error * * Builds a URI for the metadata signature using the username and password set for the remote. * * Returns: (transfer full): a URI, or %NULL for error * * Since: 1.9.8 **/ gchar * fwupd_remote_build_metadata_uri(FwupdRemote *self, GError **error) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return fwupd_remote_build_uri(self, NULL, priv->metadata_uri, error); } /** * fwupd_remote_get_report_uri: * @self: a #FwupdRemote * * Gets the URI for the remote reporting. * * Returns: (transfer none): a URI, or %NULL for invalid. * * Since: 1.0.4 **/ const gchar * fwupd_remote_get_report_uri(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), NULL); return priv->report_uri; } /** * fwupd_remote_get_metadata_uri: * @self: a #FwupdRemote * * Gets the URI for the remote metadata. * * Returns: (transfer none): a URI, or %NULL for invalid. * * Since: 0.9.7 **/ const gchar * fwupd_remote_get_metadata_uri(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), NULL); return priv->metadata_uri; } static gboolean fwupd_remote_load_signature_jcat(FwupdRemote *self, JcatFile *jcat_file, GError **error) { FwupdRemotePrivate *priv = GET_PRIVATE(self); const gchar *id; g_autofree gchar *basename = NULL; g_autofree gchar *baseuri = NULL; g_autofree gchar *metadata_uri = NULL; g_autoptr(GPtrArray) jcat_blobs = NULL; g_autoptr(JcatItem) jcat_item = NULL; /* this seems pointless to get the item by ID then just read the ID, * but _get_item_by_id() uses the AliasIds as a fallback */ basename = g_path_get_basename(priv->metadata_uri); jcat_item = jcat_file_get_item_by_id(jcat_file, basename, NULL); if (jcat_item == NULL) { /* if we're using libjcat 0.1.0 just get the default item */ jcat_item = jcat_file_get_item_default(jcat_file, error); if (jcat_item == NULL) return FALSE; } id = jcat_item_get_id(jcat_item); if (id == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "No ID for JCat item"); return FALSE; } /* replace the URI if required */ baseuri = g_path_get_dirname(priv->metadata_uri); metadata_uri = g_build_path("/", baseuri, id, NULL); if (g_strcmp0(metadata_uri, priv->metadata_uri) != 0) { g_info("changing metadata URI from %s to %s", priv->metadata_uri, metadata_uri); g_free(priv->metadata_uri); priv->metadata_uri = g_steal_pointer(&metadata_uri); } /* look for the metadata hash */ jcat_blobs = jcat_item_get_blobs_by_kind(jcat_item, JCAT_BLOB_KIND_SHA256); if (jcat_blobs->len == 1) { JcatBlob *blob = g_ptr_array_index(jcat_blobs, 0); g_autofree gchar *hash = jcat_blob_get_data_as_string(blob); fwupd_remote_set_checksum_sig_metadata(self, hash); } /* success */ return TRUE; } /** * fwupd_remote_load_signature_bytes: * @self: a #FwupdRemote * @bytes: (not nullable): data blob * @error: (nullable): optional return location for an error * * Parses the signature, updating the metadata URI as appropriate. * * Returns: %TRUE for success * * Since: 1.4.5 **/ gboolean fwupd_remote_load_signature_bytes(FwupdRemote *self, GBytes *bytes, GError **error) { g_autoptr(GInputStream) istr = NULL; g_autoptr(JcatFile) jcat_file = jcat_file_new(); g_return_val_if_fail(FWUPD_IS_REMOTE(self), FALSE); g_return_val_if_fail(bytes != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); istr = g_memory_input_stream_new_from_bytes(bytes); if (!jcat_file_import_stream(jcat_file, istr, JCAT_IMPORT_FLAG_NONE, NULL, error)) return FALSE; return fwupd_remote_load_signature_jcat(self, jcat_file, error); } /** * fwupd_remote_load_signature: * @self: a #FwupdRemote * @filename: (not nullable): a filename * @error: (nullable): optional return location for an error * * Parses the signature, updating the metadata URI as appropriate. * * Returns: %TRUE for success * * Since: 1.4.0 **/ gboolean fwupd_remote_load_signature(FwupdRemote *self, const gchar *filename, GError **error) { g_autoptr(GFile) gfile = NULL; g_autoptr(JcatFile) jcat_file = jcat_file_new(); g_return_val_if_fail(FWUPD_IS_REMOTE(self), FALSE); g_return_val_if_fail(filename != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* load JCat file */ gfile = g_file_new_for_path(filename); if (!jcat_file_import_file(jcat_file, gfile, JCAT_IMPORT_FLAG_NONE, NULL, error)) { fwupd_error_convert(error); return FALSE; } return fwupd_remote_load_signature_jcat(self, jcat_file, error); } /** * fwupd_remote_get_metadata_uri_sig: * @self: a #FwupdRemote * * Gets the URI for the remote metadata signature. * * Returns: (transfer none): a URI, or %NULL for invalid. * * Since: 0.9.7 **/ const gchar * fwupd_remote_get_metadata_uri_sig(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), NULL); return priv->metadata_uri_sig; } /** * fwupd_remote_get_firmware_base_uri: * @self: a #FwupdRemote * * Gets the base URI for firmware. * * Returns: (transfer none): a URI, or %NULL for unset. * * Since: 2.0.2 **/ const gchar * fwupd_remote_get_firmware_base_uri(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), NULL); return priv->firmware_base_uri; } /** * fwupd_remote_needs_refresh: * @self: a #FwupdRemote * * Gets if the metadata remote needs re-downloading. * * Returns: a #TRUE if the remote contents are considered old * * Since: 1.9.4 **/ gboolean fwupd_remote_needs_refresh(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), FALSE); if (!fwupd_remote_has_flag(self, FWUPD_REMOTE_FLAG_ENABLED)) return FALSE; if (priv->kind != FWUPD_REMOTE_KIND_DOWNLOAD) return FALSE; return fwupd_remote_get_age(self) > priv->refresh_interval; } /** * fwupd_remote_get_id: * @self: a #FwupdRemote * * Gets the remote ID, e.g. `lvfs-testing`. * * Returns: a string, or %NULL if unset * * Since: 0.9.3 **/ const gchar * fwupd_remote_get_id(FwupdRemote *self) { FwupdRemotePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REMOTE(self), NULL); return priv->id; } static void fwupd_remote_from_variant_iter(FwupdCodec *codec, GVariantIter *iter) { FwupdRemote *self = FWUPD_REMOTE(codec); FwupdRemotePrivate *priv = GET_PRIVATE(self); GVariant *value; const gchar *key; g_autoptr(GVariantIter) iter2 = g_variant_iter_copy(iter); g_autoptr(GVariantIter) iter3 = g_variant_iter_copy(iter); /* three passes, as we have to construct Id -> Url -> * */ while (g_variant_iter_loop(iter, "{&sv}", &key, &value)) { if (g_strcmp0(key, FWUPD_RESULT_KEY_REMOTE_ID) == 0) fwupd_remote_set_id(self, g_variant_get_string(value, NULL)); if (g_strcmp0(key, "Type") == 0) fwupd_remote_set_kind(self, g_variant_get_uint32(value)); if (g_strcmp0(key, FWUPD_RESULT_KEY_FLAGS) == 0) fwupd_remote_set_flags(self, g_variant_get_uint64(value)); } while (g_variant_iter_loop(iter2, "{&sv}", &key, &value)) { if (g_strcmp0(key, FWUPD_RESULT_KEY_URI) == 0) fwupd_remote_set_metadata_uri(self, g_variant_get_string(value, NULL)); if (g_strcmp0(key, "FilenameCache") == 0) fwupd_remote_set_filename_cache(self, g_variant_get_string(value, NULL)); if (g_strcmp0(key, "FilenameSource") == 0) fwupd_remote_set_filename_source(self, g_variant_get_string(value, NULL)); if (g_strcmp0(key, "ReportUri") == 0) fwupd_remote_set_report_uri(self, g_variant_get_string(value, NULL)); } while (g_variant_iter_loop(iter3, "{&sv}", &key, &value)) { if (g_strcmp0(key, "Username") == 0) { fwupd_remote_set_username(self, g_variant_get_string(value, NULL)); } else if (g_strcmp0(key, "Password") == 0) { fwupd_remote_set_password(self, g_variant_get_string(value, NULL)); } else if (g_strcmp0(key, "Title") == 0) { fwupd_remote_set_title(self, g_variant_get_string(value, NULL)); } else if (g_strcmp0(key, "PrivacyUri") == 0) { fwupd_remote_set_privacy_uri(self, g_variant_get_string(value, NULL)); } else if (g_strcmp0(key, "Agreement") == 0) { fwupd_remote_set_agreement(self, g_variant_get_string(value, NULL)); } else if (g_strcmp0(key, FWUPD_RESULT_KEY_CHECKSUM) == 0) { fwupd_remote_set_checksum_sig(self, g_variant_get_string(value, NULL)); } else if (g_strcmp0(key, "Enabled") == 0) { if (g_variant_get_boolean(value)) fwupd_remote_add_flag(self, FWUPD_REMOTE_FLAG_ENABLED); } else if (g_strcmp0(key, "ApprovalRequired") == 0) { if (g_variant_get_boolean(value)) fwupd_remote_add_flag(self, FWUPD_REMOTE_FLAG_APPROVAL_REQUIRED); } else if (g_strcmp0(key, "Priority") == 0) { priv->priority = g_variant_get_int32(value); } else if (g_strcmp0(key, "ModificationTime") == 0) { priv->mtime = g_variant_get_uint64(value); } else if (g_strcmp0(key, "RefreshInterval") == 0) { priv->refresh_interval = g_variant_get_uint64(value); } else if (g_strcmp0(key, "FirmwareBaseUri") == 0) { fwupd_remote_set_firmware_base_uri(self, g_variant_get_string(value, NULL)); } else if (g_strcmp0(key, "AutomaticReports") == 0) { /* we can probably stop doing proxying flags when we next branch */ if (g_variant_get_boolean(value)) fwupd_remote_add_flag(self, FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS); } else if (g_strcmp0(key, "AutomaticSecurityReports") == 0) { if (g_variant_get_boolean(value)) fwupd_remote_add_flag(self, FWUPD_REMOTE_FLAG_AUTOMATIC_SECURITY_REPORTS); } } } static void fwupd_remote_add_variant(FwupdCodec *codec, GVariantBuilder *builder, FwupdCodecFlags flags) { FwupdRemote *self = FWUPD_REMOTE(codec); FwupdRemotePrivate *priv = GET_PRIVATE(self); /* create an array with all the metadata in */ if (priv->id != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_REMOTE_ID, g_variant_new_string(priv->id)); } if (priv->flags != 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_FLAGS, g_variant_new_uint64(priv->flags)); } if (priv->username != NULL) { g_variant_builder_add(builder, "{sv}", "Username", g_variant_new_string(priv->username)); } if (priv->password != NULL) { g_variant_builder_add(builder, "{sv}", "Password", g_variant_new_string(priv->password)); } if (priv->title != NULL) { g_variant_builder_add(builder, "{sv}", "Title", g_variant_new_string(priv->title)); } if (priv->privacy_uri != NULL) { g_variant_builder_add(builder, "{sv}", "PrivacyUri", g_variant_new_string(priv->privacy_uri)); } if (priv->agreement != NULL) { g_variant_builder_add(builder, "{sv}", "Agreement", g_variant_new_string(priv->agreement)); } if (priv->checksum_sig != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_CHECKSUM, g_variant_new_string(priv->checksum_sig)); } if (priv->metadata_uri != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_URI, g_variant_new_string(priv->metadata_uri)); } if (priv->report_uri != NULL) { g_variant_builder_add(builder, "{sv}", "ReportUri", g_variant_new_string(priv->report_uri)); } if (priv->firmware_base_uri != NULL) { g_variant_builder_add(builder, "{sv}", "FirmwareBaseUri", g_variant_new_string(priv->firmware_base_uri)); } if (priv->priority != 0) { g_variant_builder_add(builder, "{sv}", "Priority", g_variant_new_int32(priv->priority)); } if (priv->kind != FWUPD_REMOTE_KIND_UNKNOWN) { g_variant_builder_add(builder, "{sv}", "Type", g_variant_new_uint32(priv->kind)); } if (priv->mtime != 0) { g_variant_builder_add(builder, "{sv}", "ModificationTime", g_variant_new_uint64(priv->mtime)); } if (priv->refresh_interval != 0) { g_variant_builder_add(builder, "{sv}", "RefreshInterval", g_variant_new_uint64(priv->refresh_interval)); } if (priv->filename_cache != NULL) { g_variant_builder_add(builder, "{sv}", "FilenameCache", g_variant_new_string(priv->filename_cache)); } if (priv->filename_source != NULL) { g_variant_builder_add(builder, "{sv}", "FilenameSource", g_variant_new_string(priv->filename_source)); } if (priv->remotes_dir != NULL) { g_variant_builder_add(builder, "{sv}", "RemotesDir", g_variant_new_string(priv->remotes_dir)); } /* we can probably stop doing proxying flags when we next branch */ g_variant_builder_add( builder, "{sv}", "Enabled", g_variant_new_boolean(fwupd_remote_has_flag(self, FWUPD_REMOTE_FLAG_ENABLED))); g_variant_builder_add( builder, "{sv}", "ApprovalRequired", g_variant_new_boolean( fwupd_remote_has_flag(self, FWUPD_REMOTE_FLAG_APPROVAL_REQUIRED))); g_variant_builder_add( builder, "{sv}", "AutomaticReports", g_variant_new_boolean( fwupd_remote_has_flag(self, FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS))); g_variant_builder_add( builder, "{sv}", "AutomaticSecurityReports", g_variant_new_boolean( fwupd_remote_has_flag(self, FWUPD_REMOTE_FLAG_AUTOMATIC_SECURITY_REPORTS))); } static void fwupd_remote_get_property(GObject *obj, guint prop_id, GValue *value, GParamSpec *pspec) { FwupdRemote *self = FWUPD_REMOTE(obj); FwupdRemotePrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_ENABLED: g_value_set_boolean(value, fwupd_remote_has_flag(self, FWUPD_REMOTE_FLAG_ENABLED)); break; case PROP_APPROVAL_REQUIRED: g_value_set_boolean( value, fwupd_remote_has_flag(self, FWUPD_REMOTE_FLAG_APPROVAL_REQUIRED)); break; case PROP_ID: g_value_set_string(value, priv->id); break; case PROP_AUTOMATIC_REPORTS: g_value_set_boolean( value, fwupd_remote_has_flag(self, FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS)); break; case PROP_AUTOMATIC_SECURITY_REPORTS: g_value_set_boolean( value, fwupd_remote_has_flag(self, FWUPD_REMOTE_FLAG_AUTOMATIC_SECURITY_REPORTS)); break; case PROP_FLAGS: g_value_set_uint64(value, priv->flags); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec); break; } } static void fwupd_remote_set_property(GObject *obj, guint prop_id, const GValue *value, GParamSpec *pspec) { FwupdRemote *self = FWUPD_REMOTE(obj); switch (prop_id) { case PROP_ENABLED: if (g_value_get_boolean(value)) fwupd_remote_add_flag(self, FWUPD_REMOTE_FLAG_ENABLED); else fwupd_remote_remove_flag(self, FWUPD_REMOTE_FLAG_ENABLED); break; case PROP_APPROVAL_REQUIRED: if (g_value_get_boolean(value)) fwupd_remote_add_flag(self, FWUPD_REMOTE_FLAG_APPROVAL_REQUIRED); else fwupd_remote_remove_flag(self, FWUPD_REMOTE_FLAG_APPROVAL_REQUIRED); break; case PROP_ID: fwupd_remote_set_id(self, g_value_get_string(value)); break; case PROP_AUTOMATIC_REPORTS: if (g_value_get_boolean(value)) fwupd_remote_add_flag(self, FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS); else fwupd_remote_remove_flag(self, FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS); break; case PROP_AUTOMATIC_SECURITY_REPORTS: if (g_value_get_boolean(value)) fwupd_remote_add_flag(self, FWUPD_REMOTE_FLAG_AUTOMATIC_SECURITY_REPORTS); else fwupd_remote_remove_flag(self, FWUPD_REMOTE_FLAG_AUTOMATIC_SECURITY_REPORTS); break; case PROP_FLAGS: fwupd_remote_set_flags(self, g_value_get_uint64(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec); break; } } static void fwupd_remote_class_init(FwupdRemoteClass *klass) { GParamSpec *pspec; GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fwupd_remote_finalize; object_class->get_property = fwupd_remote_get_property; object_class->set_property = fwupd_remote_set_property; /** * FwupdRemote:id: * * The remote ID. * * Since: 0.9.3 */ pspec = g_param_spec_string("id", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_ID, pspec); /** * FwupdRemote:enabled: * * If the remote is enabled and should be used. * * Since: 0.9.3 */ pspec = g_param_spec_boolean("enabled", NULL, NULL, FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_ENABLED, pspec); /** * FwupdRemote:approval-required: * * If firmware from the remote should be checked against the system * list of approved firmware. * * Since: 1.2.6 */ pspec = g_param_spec_boolean("approval-required", NULL, NULL, FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_APPROVAL_REQUIRED, pspec); /** * FwupdRemote:automatic-reports: * * The behavior for auto-uploading reports. * * Since: 1.3.3 */ pspec = g_param_spec_boolean("automatic-reports", NULL, NULL, FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_AUTOMATIC_REPORTS, pspec); /** * FwupdRemote:automatic-security-reports: * * The behavior for auto-uploading security reports. * * Since: 1.5.0 */ pspec = g_param_spec_boolean("automatic-security-reports", NULL, NULL, FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_AUTOMATIC_SECURITY_REPORTS, pspec); /** * FwupdRemote:flags: * * The remote flags. * * Since: 1.9.4 */ pspec = g_param_spec_uint64("flags", NULL, NULL, 0, G_MAXUINT64, 0, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_FLAGS, pspec); } static void fwupd_remote_init(FwupdRemote *self) { } static void fwupd_remote_finalize(GObject *obj) { FwupdRemote *self = FWUPD_REMOTE(obj); FwupdRemotePrivate *priv = GET_PRIVATE(self); g_free(priv->id); g_free(priv->metadata_uri); g_free(priv->metadata_uri_sig); g_free(priv->firmware_base_uri); g_free(priv->report_uri); g_free(priv->username); g_free(priv->password); g_free(priv->title); g_free(priv->privacy_uri); g_free(priv->agreement); g_free(priv->remotes_dir); g_free(priv->checksum); g_free(priv->checksum_sig); g_free(priv->filename_cache); g_free(priv->filename_cache_sig); g_free(priv->filename_source); g_strfreev(priv->order_after); g_strfreev(priv->order_before); G_OBJECT_CLASS(fwupd_remote_parent_class)->finalize(obj); } static void fwupd_remote_codec_iface_init(FwupdCodecInterface *iface) { iface->add_json = fwupd_remote_add_json; iface->add_variant = fwupd_remote_add_variant; iface->from_variant_iter = fwupd_remote_from_variant_iter; } /** * fwupd_remote_new: * * Creates a new fwupd remote. * * Returns: a new #FwupdRemote * * Since: 0.9.3 **/ FwupdRemote * fwupd_remote_new(void) { FwupdRemote *self; self = g_object_new(FWUPD_TYPE_REMOTE, NULL); return FWUPD_REMOTE(self); } fwupd-2.0.10/libfwupd/fwupd-remote.h000066400000000000000000000125241501337203100173210ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fwupd-enums.h" G_BEGIN_DECLS #define FWUPD_TYPE_REMOTE (fwupd_remote_get_type()) G_DECLARE_DERIVABLE_TYPE(FwupdRemote, fwupd_remote, FWUPD, REMOTE, GObject) struct _FwupdRemoteClass { GObjectClass parent_class; /*< private >*/ void (*_fwupd_reserved1)(void); void (*_fwupd_reserved2)(void); void (*_fwupd_reserved3)(void); void (*_fwupd_reserved4)(void); void (*_fwupd_reserved5)(void); void (*_fwupd_reserved6)(void); void (*_fwupd_reserved7)(void); }; /** * FwupdRemoteKind: * * The kind of remote. **/ typedef enum { /** * FWUPD_REMOTE_KIND_UNKNOWN: * * Unknown kind. */ FWUPD_REMOTE_KIND_UNKNOWN, /** * FWUPD_REMOTE_KIND_DOWNLOAD: * * Requires files to be downloaded. */ FWUPD_REMOTE_KIND_DOWNLOAD, /** * FWUPD_REMOTE_KIND_LOCAL: * * Reads files from the local machine. */ FWUPD_REMOTE_KIND_LOCAL, /** * FWUPD_REMOTE_KIND_DIRECTORY: * * Reads directory from the local machine. * * Since: 1.2.4 */ FWUPD_REMOTE_KIND_DIRECTORY, /*< private >*/ FWUPD_REMOTE_KIND_LAST } FwupdRemoteKind; /** * FwupdRemoteFlags: * * The flags available for the remote. **/ typedef enum { /** * FWUPD_REMOTE_FLAG_NONE: * * No flags set. * * Since: 1.9.4 */ FWUPD_REMOTE_FLAG_NONE = 0, /** * FWUPD_REMOTE_FLAG_ENABLED: * * Is enabled. * * Since: 1.9.4 */ FWUPD_REMOTE_FLAG_ENABLED = 1 << 0, /** * FWUPD_REMOTE_FLAG_APPROVAL_REQUIRED: * * Requires approval for each firmware. * * Since: 1.9.4 */ FWUPD_REMOTE_FLAG_APPROVAL_REQUIRED = 1 << 1, /** * FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS: * * Send firmware reports automatically. * * Since: 1.9.4 */ FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS = 1 << 2, /** * FWUPD_REMOTE_FLAG_AUTOMATIC_SECURITY_REPORTS: * * Send security reports automatically. * * Since: 1.9.4 */ FWUPD_REMOTE_FLAG_AUTOMATIC_SECURITY_REPORTS = 1 << 3, /** * FWUPD_REMOTE_FLAG_ALLOW_P2P_METADATA: * * Use peer-to-peer locations for metadata. * * Since: 1.9.5 */ FWUPD_REMOTE_FLAG_ALLOW_P2P_METADATA = 1 << 4, /** * FWUPD_REMOTE_FLAG_ALLOW_P2P_FIRMWARE: * * Use peer-to-peer locations for firmware. * * Since: 1.9.5 */ FWUPD_REMOTE_FLAG_ALLOW_P2P_FIRMWARE = 1 << 5, } FwupdRemoteFlags; FwupdRemoteKind fwupd_remote_kind_from_string(const gchar *kind); const gchar * fwupd_remote_kind_to_string(FwupdRemoteKind kind); const gchar * fwupd_remote_flag_to_string(FwupdRemoteFlags flag); FwupdRemoteFlags fwupd_remote_flag_from_string(const gchar *flag); FwupdRemote * fwupd_remote_new(void); const gchar * fwupd_remote_get_id(FwupdRemote *self) G_GNUC_NON_NULL(1); const gchar * fwupd_remote_get_title(FwupdRemote *self) G_GNUC_NON_NULL(1); const gchar * fwupd_remote_get_privacy_uri(FwupdRemote *self) G_GNUC_NON_NULL(1); const gchar * fwupd_remote_get_agreement(FwupdRemote *self) G_GNUC_NON_NULL(1); const gchar * fwupd_remote_get_remotes_dir(FwupdRemote *self) G_GNUC_NON_NULL(1); const gchar * fwupd_remote_get_checksum(FwupdRemote *self) G_GNUC_NON_NULL(1); const gchar * fwupd_remote_get_checksum_metadata(FwupdRemote *self) G_GNUC_NON_NULL(1); const gchar * fwupd_remote_get_username(FwupdRemote *self) G_GNUC_NON_NULL(1); const gchar * fwupd_remote_get_password(FwupdRemote *self) G_GNUC_NON_NULL(1); const gchar * fwupd_remote_get_filename_cache(FwupdRemote *self) G_GNUC_NON_NULL(1); const gchar * fwupd_remote_get_filename_cache_sig(FwupdRemote *self) G_GNUC_NON_NULL(1); const gchar * fwupd_remote_get_filename_source(FwupdRemote *self) G_GNUC_NON_NULL(1); const gchar * fwupd_remote_get_firmware_base_uri(FwupdRemote *self) G_GNUC_NON_NULL(1); const gchar * fwupd_remote_get_report_uri(FwupdRemote *self) G_GNUC_NON_NULL(1); const gchar * fwupd_remote_get_metadata_uri(FwupdRemote *self) G_GNUC_NON_NULL(1); const gchar * fwupd_remote_get_metadata_uri_sig(FwupdRemote *self) G_GNUC_NON_NULL(1); guint64 fwupd_remote_get_refresh_interval(FwupdRemote *self) G_GNUC_NON_NULL(1); FwupdRemoteFlags fwupd_remote_get_flags(FwupdRemote *self) G_GNUC_NON_NULL(1); void fwupd_remote_set_flags(FwupdRemote *self, FwupdRemoteFlags flags) G_GNUC_NON_NULL(1); void fwupd_remote_add_flag(FwupdRemote *self, FwupdRemoteFlags flag) G_GNUC_NON_NULL(1); void fwupd_remote_remove_flag(FwupdRemote *self, FwupdRemoteFlags flag) G_GNUC_NON_NULL(1); gboolean fwupd_remote_has_flag(FwupdRemote *self, FwupdRemoteFlags flag) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fwupd_remote_needs_refresh(FwupdRemote *self) G_GNUC_NON_NULL(1); gint fwupd_remote_get_priority(FwupdRemote *self) G_GNUC_NON_NULL(1); guint64 fwupd_remote_get_age(FwupdRemote *self) G_GNUC_NON_NULL(1); FwupdRemoteKind fwupd_remote_get_kind(FwupdRemote *self) G_GNUC_NON_NULL(1); gchar * fwupd_remote_build_firmware_uri(FwupdRemote *self, const gchar *url, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gchar * fwupd_remote_build_report_uri(FwupdRemote *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fwupd_remote_load_signature(FwupdRemote *self, const gchar *filename, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fwupd_remote_load_signature_bytes(FwupdRemote *self, GBytes *bytes, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); G_END_DECLS fwupd-2.0.10/libfwupd/fwupd-report.c000066400000000000000000000557151501337203100173450ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fwupd-codec.h" #include "fwupd-common-private.h" #include "fwupd-enums-private.h" #include "fwupd-error.h" #include "fwupd-report.h" /** * FwupdReport: * * A firmware report from a vendor. * * This is the LVFS formatted report that the fwupd user consumes, NOT the thing that gets uploaded. * * See also: [class@FwupdRelease] */ typedef struct { guint64 created; gchar *version_old; gchar *vendor; guint32 vendor_id; gchar *device_name; gchar *distro_id; gchar *distro_version; GHashTable *metadata; gchar *distro_variant; gchar *remote_id; FwupdReportFlags flags; } FwupdReportPrivate; enum { PROP_0, PROP_FLAGS, PROP_LAST }; static void fwupd_report_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_EXTENDED(FwupdReport, fwupd_report, G_TYPE_OBJECT, 0, G_ADD_PRIVATE(FwupdReport) G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fwupd_report_codec_iface_init)); #define GET_PRIVATE(o) (fwupd_report_get_instance_private(o)) /** * fwupd_report_get_created: * @self: a #FwupdReport * * Gets when the report was created. * * Returns: UTC timestamp in UNIX format, or 0 if unset * * Since: 1.8.8 **/ guint64 fwupd_report_get_created(FwupdReport *self) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REPORT(self), 0); return priv->created; } /** * fwupd_report_set_created: * @self: a #FwupdReport * @created: UTC timestamp in UNIX format * * Sets when the report was created. * * Since: 1.8.8 **/ void fwupd_report_set_created(FwupdReport *self, guint64 created) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REPORT(self)); priv->created = created; } /** * fwupd_report_get_version_old: * @self: a #FwupdReport * * Gets the old version, i.e. what the upser was upgrading *from*. * * Returns: the version, or %NULL if unset * * Since: 1.8.8 **/ const gchar * fwupd_report_get_version_old(FwupdReport *self) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REPORT(self), NULL); return priv->version_old; } /** * fwupd_report_set_version_old: * @self: a #FwupdReport * @version_old: (nullable): the version, e.g. `1.2.3` * * Sets the old version, i.e. what the upser was upgrading *from*. * * Since: 1.8.8 **/ void fwupd_report_set_version_old(FwupdReport *self, const gchar *version_old) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REPORT(self)); /* not changed */ if (g_strcmp0(priv->version_old, version_old) == 0) return; g_free(priv->version_old); priv->version_old = g_strdup(version_old); } /** * fwupd_report_get_vendor: * @self: a #FwupdReport * * Gets the vendor that uploaded the test result. * * Returns: the test vendor, or %NULL if unset * * Since: 1.8.8 **/ const gchar * fwupd_report_get_vendor(FwupdReport *self) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REPORT(self), NULL); return priv->vendor; } /** * fwupd_report_set_vendor: * @self: a #FwupdReport * @vendor: (nullable): the vendor name * * Sets the vendor that uploaded the test result. * * Since: 1.8.8 **/ void fwupd_report_set_vendor(FwupdReport *self, const gchar *vendor) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REPORT(self)); /* not changed */ if (g_strcmp0(priv->vendor, vendor) == 0) return; g_free(priv->vendor); priv->vendor = g_strdup(vendor); } /** * fwupd_report_get_vendor_id: * @self: a #FwupdReport * * Gets the vendor identifier. The mapping is only known on the remote server, and this can be * useful to filter on different QA teams that work for the same OEM. * * Returns: the vendor ID, or 0 if unset * * Since: 1.8.8 **/ guint32 fwupd_report_get_vendor_id(FwupdReport *self) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REPORT(self), 0); return priv->vendor_id; } /** * fwupd_report_set_vendor_id: * @self: a #FwupdReport * @vendor_id: the vendor ID, or 0 * * Sets the vendor identifier. The mapping is only known on the remote server, and this can be * useful to filter on different QA teams that work for the same OEM. * * Since: 1.8.8 **/ void fwupd_report_set_vendor_id(FwupdReport *self, guint32 vendor_id) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REPORT(self)); priv->vendor_id = vendor_id; } /** * fwupd_report_get_device_name: * @self: a #FwupdReport * * Gets the name of the device the update was performed on. * * Returns: the name, or %NULL if unset * * Since: 1.8.8 **/ const gchar * fwupd_report_get_device_name(FwupdReport *self) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REPORT(self), NULL); return priv->device_name; } /** * fwupd_report_set_device_name: * @self: a #FwupdReport * @device_name: (nullable): the name, e.g. `LENOVO ThinkPad P1 Gen 3` * * Sets the name of the device the update was performed on. * * Since: 1.8.8 **/ void fwupd_report_set_device_name(FwupdReport *self, const gchar *device_name) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REPORT(self)); /* not changed */ if (g_strcmp0(priv->device_name, device_name) == 0) return; g_free(priv->device_name); priv->device_name = g_strdup(device_name); } /** * fwupd_report_get_distro_id: * @self: a #FwupdReport * * Gets the distribution name. * * Returns: the name, or %NULL if unset * * Since: 1.8.8 **/ const gchar * fwupd_report_get_distro_id(FwupdReport *self) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REPORT(self), NULL); return priv->distro_id; } /** * fwupd_report_set_distro_id: * @self: a #FwupdReport * @distro_id: (nullable): the name, e.g. `fedora` * * Sets the distribution name. * * Since: 1.8.8 **/ void fwupd_report_set_distro_id(FwupdReport *self, const gchar *distro_id) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REPORT(self)); /* not changed */ if (g_strcmp0(priv->distro_id, distro_id) == 0) return; g_free(priv->distro_id); priv->distro_id = g_strdup(distro_id); } /** * fwupd_report_get_distro_variant: * @self: a #FwupdReport * * Gets the distribution variant. * * Returns: variant, or %NULL if unset * * Since: 1.8.8 **/ const gchar * fwupd_report_get_distro_variant(FwupdReport *self) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REPORT(self), NULL); return priv->distro_variant; } /** * fwupd_report_set_distro_variant: * @self: a #FwupdReport * @distro_variant: (nullable): the variant, e.g. `workstation` * * Sets the distribution variant. * * Since: 1.8.8 **/ void fwupd_report_set_distro_variant(FwupdReport *self, const gchar *distro_variant) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REPORT(self)); /* not changed */ if (g_strcmp0(priv->distro_variant, distro_variant) == 0) return; g_free(priv->distro_variant); priv->distro_variant = g_strdup(distro_variant); } /** * fwupd_report_get_remote_id: * @self: a #FwupdReport * * Gets the remote ID. * * Returns: ID, or %NULL if unset * * Since: 1.9.3 **/ const gchar * fwupd_report_get_remote_id(FwupdReport *self) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REPORT(self), NULL); return priv->remote_id; } /** * fwupd_report_set_remote_id: * @self: a #FwupdReport * @remote_id: (nullable): the remote, e.g. `lvfs` * * Sets the remote ID. * * Since: 1.9.3 **/ void fwupd_report_set_remote_id(FwupdReport *self, const gchar *remote_id) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REPORT(self)); /* not changed */ if (g_strcmp0(priv->remote_id, remote_id) == 0) return; g_free(priv->remote_id); priv->remote_id = g_strdup(remote_id); } /** * fwupd_report_get_distro_version: * @self: a #FwupdReport * * Gets the distribution version. * * Returns: a string, or %NULL if unset * * Since: 1.8.8 **/ const gchar * fwupd_report_get_distro_version(FwupdReport *self) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REPORT(self), NULL); return priv->distro_version; } /** * fwupd_report_set_distro_version: * @self: a #FwupdReport * @distro_version: (nullable): a string * * Sets the distribution version. * * Since: 1.8.8 **/ void fwupd_report_set_distro_version(FwupdReport *self, const gchar *distro_version) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REPORT(self)); /* not changed */ if (g_strcmp0(priv->distro_version, distro_version) == 0) return; g_free(priv->distro_version); priv->distro_version = g_strdup(distro_version); } /** * fwupd_report_get_metadata: * @self: a #FwupdReport * * Gets the report metadata. * * Returns: (transfer none): the metadata, which may be empty * * Since: 1.8.8 **/ GHashTable * fwupd_report_get_metadata(FwupdReport *self) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REPORT(self), NULL); return priv->metadata; } /** * fwupd_report_add_metadata_item: * @self: a #FwupdReport * @key: (not nullable): the key * @value: (not nullable): the value * * Sets a report metadata item. * * Since: 1.8.8 **/ void fwupd_report_add_metadata_item(FwupdReport *self, const gchar *key, const gchar *value) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REPORT(self)); g_return_if_fail(key != NULL); g_return_if_fail(value != NULL); g_hash_table_insert(priv->metadata, g_strdup(key), g_strdup(value)); } /** * fwupd_report_get_metadata_item: * @self: a #FwupdReport * @key: (not nullable): the key * * Gets a report metadata item. * * Returns: the value, or %NULL if unset * * Since: 1.8.8 **/ const gchar * fwupd_report_get_metadata_item(FwupdReport *self, const gchar *key) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REPORT(self), NULL); g_return_val_if_fail(key != NULL, NULL); return g_hash_table_lookup(priv->metadata, key); } static void fwupd_report_add_variant(FwupdCodec *codec, GVariantBuilder *builder, FwupdCodecFlags flags) { FwupdReport *self = FWUPD_REPORT(codec); FwupdReportPrivate *priv = GET_PRIVATE(self); if (priv->distro_id != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_DISTRO_ID, g_variant_new_string(priv->distro_id)); } if (priv->distro_variant != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_DISTRO_VARIANT, g_variant_new_string(priv->distro_variant)); } if (priv->distro_version != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_DISTRO_VERSION, g_variant_new_string(priv->distro_version)); } if (priv->vendor != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_VENDOR, g_variant_new_string(priv->vendor)); } if (priv->device_name != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_DEVICE_NAME, g_variant_new_string(priv->device_name)); } if (priv->created != 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_CREATED, g_variant_new_uint64(priv->created)); } if (priv->version_old != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_VERSION_OLD, g_variant_new_string(priv->version_old)); } if (priv->vendor_id > 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_VENDOR_ID, g_variant_new_uint32(priv->vendor_id)); } if (priv->remote_id != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_REMOTE_ID, g_variant_new_string(priv->remote_id)); } if (g_hash_table_size(priv->metadata) > 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_METADATA, fwupd_hash_kv_to_variant(priv->metadata)); } if (priv->flags > 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_FLAGS, g_variant_new_uint64(priv->flags)); } } static void fwupd_report_from_key_value(FwupdReport *self, const gchar *key, GVariant *value) { FwupdReportPrivate *priv = GET_PRIVATE(self); if (g_strcmp0(key, FWUPD_RESULT_KEY_DISTRO_ID) == 0) { fwupd_report_set_distro_id(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_DISTRO_VARIANT) == 0) { fwupd_report_set_distro_variant(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_DISTRO_VERSION) == 0) { fwupd_report_set_distro_version(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_VENDOR) == 0) { fwupd_report_set_vendor(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_VENDOR_ID) == 0) { fwupd_report_set_vendor_id(self, g_variant_get_uint32(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_DEVICE_NAME) == 0) { fwupd_report_set_device_name(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_CREATED) == 0) { fwupd_report_set_created(self, g_variant_get_uint64(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_VERSION_OLD) == 0) { fwupd_report_set_version_old(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_REMOTE_ID) == 0) { fwupd_report_set_remote_id(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_FLAGS) == 0) { fwupd_report_set_flags(self, g_variant_get_uint64(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_METADATA) == 0) { g_hash_table_unref(priv->metadata); priv->metadata = fwupd_variant_to_hash_kv(value); return; } } static void fwupd_report_add_json(FwupdCodec *codec, JsonBuilder *builder, FwupdCodecFlags flags) { FwupdReport *self = FWUPD_REPORT(codec); FwupdReportPrivate *priv = GET_PRIVATE(self); g_autoptr(GList) keys = NULL; fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_DEVICE_NAME, priv->device_name); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_DISTRO_ID, priv->distro_id); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_DISTRO_VARIANT, priv->distro_variant); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_DISTRO_VERSION, priv->distro_version); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_VERSION_OLD, priv->version_old); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_VENDOR, priv->vendor); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_REMOTE_ID, priv->remote_id); if (priv->vendor_id > 0) { fwupd_codec_json_append_int(builder, FWUPD_RESULT_KEY_VENDOR_ID, priv->vendor_id); } if (priv->flags != FWUPD_REPORT_FLAG_NONE) { json_builder_set_member_name(builder, FWUPD_RESULT_KEY_FLAGS); json_builder_begin_array(builder); for (guint i = 0; i < 64; i++) { const gchar *tmp; if ((priv->flags & ((guint64)1 << i)) == 0) continue; tmp = fwupd_report_flag_to_string((guint64)1 << i); json_builder_add_string_value(builder, tmp); } json_builder_end_array(builder); } /* metadata */ keys = g_hash_table_get_keys(priv->metadata); for (GList *l = keys; l != NULL; l = l->next) { const gchar *key = l->data; const gchar *value = g_hash_table_lookup(priv->metadata, key); fwupd_codec_json_append(builder, key, value); } } static void fwupd_report_string_append_flags(GString *str, guint idt, const gchar *key, guint64 report_flags) { g_autoptr(GString) tmp = g_string_new(""); for (guint i = 0; i < 64; i++) { if ((report_flags & ((guint64)1 << i)) == 0) continue; g_string_append_printf(tmp, "%s|", fwupd_report_flag_to_string((guint64)1 << i)); } if (tmp->len == 0) { g_string_append(tmp, fwupd_report_flag_to_string(0)); } else { g_string_truncate(tmp, tmp->len - 1); } fwupd_codec_string_append(str, idt, key, tmp->str); } static void fwupd_report_add_string(FwupdCodec *codec, guint idt, GString *str) { FwupdReport *self = FWUPD_REPORT(codec); FwupdReportPrivate *priv = GET_PRIVATE(self); g_autoptr(GList) keys = NULL; fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_DEVICE_NAME, priv->device_name); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_DISTRO_ID, priv->distro_id); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_DISTRO_VARIANT, priv->distro_variant); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_DISTRO_VERSION, priv->distro_version); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_VERSION_OLD, priv->version_old); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_VENDOR, priv->vendor); fwupd_codec_string_append_int(str, idt, FWUPD_RESULT_KEY_VENDOR_ID, priv->vendor_id); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_REMOTE_ID, priv->remote_id); fwupd_report_string_append_flags(str, idt, FWUPD_RESULT_KEY_FLAGS, priv->flags); /* metadata */ keys = g_hash_table_get_keys(priv->metadata); for (GList *l = keys; l != NULL; l = l->next) { const gchar *key = l->data; const gchar *value = g_hash_table_lookup(priv->metadata, key); fwupd_codec_string_append(str, idt, key, value); } } /** * fwupd_report_get_flags: * @self: a #FwupdReport * * Gets the report flags. * * Returns: report flags, or 0 if unset * * Since: 1.9.1 **/ guint64 fwupd_report_get_flags(FwupdReport *self) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REPORT(self), 0); return priv->flags; } /** * fwupd_report_set_flags: * @self: a #FwupdReport * @flags: report flags, e.g. %FWUPD_REPORT_FLAG_FROM_OEM * * Sets the report flags. * * Since: 1.9.1 **/ void fwupd_report_set_flags(FwupdReport *self, guint64 flags) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REPORT(self)); if (priv->flags == flags) return; priv->flags = flags; g_object_notify(G_OBJECT(self), "flags"); } /** * fwupd_report_add_flag: * @self: a #FwupdReport * @flag: the #FwupdReportFlags * * Adds a specific report flag to the report. * * Since: 1.9.1 **/ void fwupd_report_add_flag(FwupdReport *self, FwupdReportFlags flag) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REPORT(self)); if (flag == 0) return; if ((priv->flags & flag) > 0) return; priv->flags |= flag; g_object_notify(G_OBJECT(self), "flags"); } /** * fwupd_report_remove_flag: * @self: a #FwupdReport * @flag: a report flag * * Removes a specific report flag from the report. * * Since: 1.9.1 **/ void fwupd_report_remove_flag(FwupdReport *self, FwupdReportFlags flag) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REPORT(self)); if (flag == 0) return; if ((priv->flags & flag) == 0) return; priv->flags &= ~flag; g_object_notify(G_OBJECT(self), "flags"); } /** * fwupd_report_has_flag: * @self: a #FwupdReport * @flag: a report flag * * Finds if the report has a specific report flag. * * Returns: %TRUE if the flag is set * * Since: 1.9.1 **/ gboolean fwupd_report_has_flag(FwupdReport *self, FwupdReportFlags flag) { FwupdReportPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REPORT(self), FALSE); return (priv->flags & flag) > 0; } /** * fwupd_report_flag_to_string: * @report_flag: report flags, e.g. %FWUPD_REPORT_FLAG_FROM_OEM * * Converts an enumerated report flag to a string. * * Returns: identifier string * * Since: 1.9.1 **/ const gchar * fwupd_report_flag_to_string(FwupdReportFlags report_flag) { if (report_flag == FWUPD_REPORT_FLAG_NONE) return "none"; if (report_flag == FWUPD_REPORT_FLAG_FROM_OEM) return "from-oem"; if (report_flag == FWUPD_REPORT_FLAG_IS_UPGRADE) return "is-upgrade"; return NULL; } /** * fwupd_report_flag_from_string: * @report_flag: (nullable): a string, e.g. `from-oem` * * Converts a string to an enumerated report flag. * * Returns: enumerated value * * Since: 1.9.1 **/ FwupdReportFlags fwupd_report_flag_from_string(const gchar *report_flag) { if (g_strcmp0(report_flag, "none") == 0) return FWUPD_REPORT_FLAG_NONE; if (g_strcmp0(report_flag, "from-oem") == 0) return FWUPD_REPORT_FLAG_FROM_OEM; if (g_strcmp0(report_flag, "is-upgrade") == 0) return FWUPD_REPORT_FLAG_IS_UPGRADE; return FWUPD_REPORT_FLAG_UNKNOWN; } static void fwupd_report_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FwupdReport *self = FWUPD_REPORT(object); FwupdReportPrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_FLAGS: g_value_set_uint64(value, priv->flags); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fwupd_report_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FwupdReport *self = FWUPD_REPORT(object); switch (prop_id) { case PROP_FLAGS: fwupd_report_set_flags(self, g_value_get_uint64(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fwupd_report_init(FwupdReport *self) { FwupdReportPrivate *priv = GET_PRIVATE(self); priv->metadata = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); } static void fwupd_report_finalize(GObject *object) { FwupdReport *self = FWUPD_REPORT(object); FwupdReportPrivate *priv = GET_PRIVATE(self); g_free(priv->vendor); g_free(priv->device_name); g_free(priv->distro_id); g_free(priv->distro_version); g_free(priv->distro_variant); g_free(priv->remote_id); g_free(priv->version_old); g_hash_table_unref(priv->metadata); G_OBJECT_CLASS(fwupd_report_parent_class)->finalize(object); } static void fwupd_report_class_init(FwupdReportClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); GParamSpec *pspec; object_class->finalize = fwupd_report_finalize; object_class->get_property = fwupd_report_get_property; object_class->set_property = fwupd_report_set_property; /** * FwupdReport:flags: * * The report flags. * * Since: 1.9.1 */ pspec = g_param_spec_uint64("flags", NULL, NULL, FWUPD_REPORT_FLAG_NONE, FWUPD_REPORT_FLAG_UNKNOWN, FWUPD_REPORT_FLAG_NONE, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_FLAGS, pspec); } static void fwupd_report_from_variant_iter(FwupdCodec *codec, GVariantIter *iter) { FwupdReport *self = FWUPD_REPORT(codec); GVariant *value; const gchar *key; while (g_variant_iter_next(iter, "{&sv}", &key, &value)) { fwupd_report_from_key_value(self, key, value); g_variant_unref(value); } } static void fwupd_report_codec_iface_init(FwupdCodecInterface *iface) { iface->add_string = fwupd_report_add_string; iface->add_json = fwupd_report_add_json; iface->add_variant = fwupd_report_add_variant; iface->from_variant_iter = fwupd_report_from_variant_iter; } /** * fwupd_report_new: * * Creates a new report. * * Returns: a new #FwupdReport * * Since: 1.8.8 **/ FwupdReport * fwupd_report_new(void) { FwupdReport *self; self = g_object_new(FWUPD_TYPE_REPORT, NULL); return FWUPD_REPORT(self); } fwupd-2.0.10/libfwupd/fwupd-report.h000066400000000000000000000075051501337203100173440ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_BEGIN_DECLS #define FWUPD_TYPE_REPORT (fwupd_report_get_type()) G_DECLARE_DERIVABLE_TYPE(FwupdReport, fwupd_report, FWUPD, REPORT, GObject) struct _FwupdReportClass { GObjectClass parent_class; /*< private >*/ void (*_fwupd_reserved1)(void); void (*_fwupd_reserved2)(void); void (*_fwupd_reserved3)(void); void (*_fwupd_reserved4)(void); void (*_fwupd_reserved5)(void); void (*_fwupd_reserved6)(void); void (*_fwupd_reserved7)(void); }; /** * FwupdReportFlags: * * Flags used to represent report attributes */ typedef enum { /** * FWUPD_REPORT_FLAG_NONE: * * No report flags are set. * * Since: 1.9.1 */ FWUPD_REPORT_FLAG_NONE = 0u, /** * FWUPD_REPORT_FLAG_FROM_OEM: * * The report was generated by the OEM. * * Since: 1.9.1 */ FWUPD_REPORT_FLAG_FROM_OEM = 1ull << 0, /** * FWUPD_REPORT_FLAG_IS_UPGRADE: * * The new firmware was newer than the old firmware. * * Since: 1.9.14 */ FWUPD_REPORT_FLAG_IS_UPGRADE = 1ull << 1, /** * FWUPD_REPORT_FLAG_UNKNOWN: * * The report flag is unknown. * * This is usually caused by a mismatched libfwupdplugin and daemon. * * Since: 1.9.1 */ FWUPD_REPORT_FLAG_UNKNOWN = G_MAXUINT64, } FwupdReportFlags; FwupdReport * fwupd_report_new(void); guint64 fwupd_report_get_created(FwupdReport *self) G_GNUC_NON_NULL(1); void fwupd_report_set_created(FwupdReport *self, guint64 created) G_GNUC_NON_NULL(1); const gchar * fwupd_report_get_version_old(FwupdReport *self) G_GNUC_NON_NULL(1); void fwupd_report_set_version_old(FwupdReport *self, const gchar *version_old) G_GNUC_NON_NULL(1); const gchar * fwupd_report_get_vendor(FwupdReport *self) G_GNUC_NON_NULL(1); void fwupd_report_set_vendor(FwupdReport *self, const gchar *vendor) G_GNUC_NON_NULL(1); guint32 fwupd_report_get_vendor_id(FwupdReport *self) G_GNUC_NON_NULL(1); void fwupd_report_set_vendor_id(FwupdReport *self, guint32 vendor_id) G_GNUC_NON_NULL(1); const gchar * fwupd_report_get_device_name(FwupdReport *self) G_GNUC_NON_NULL(1); void fwupd_report_set_device_name(FwupdReport *self, const gchar *device_name) G_GNUC_NON_NULL(1); const gchar * fwupd_report_get_distro_id(FwupdReport *self) G_GNUC_NON_NULL(1); void fwupd_report_set_distro_id(FwupdReport *self, const gchar *distro_id) G_GNUC_NON_NULL(1); const gchar * fwupd_report_get_distro_version(FwupdReport *self) G_GNUC_NON_NULL(1); void fwupd_report_set_distro_version(FwupdReport *self, const gchar *distro_version) G_GNUC_NON_NULL(1); const gchar * fwupd_report_get_distro_variant(FwupdReport *self) G_GNUC_NON_NULL(1); void fwupd_report_set_distro_variant(FwupdReport *self, const gchar *distro_variant) G_GNUC_NON_NULL(1); const gchar * fwupd_report_get_remote_id(FwupdReport *self) G_GNUC_NON_NULL(1); void fwupd_report_set_remote_id(FwupdReport *self, const gchar *remote_id) G_GNUC_NON_NULL(1); GHashTable * fwupd_report_get_metadata(FwupdReport *self) G_GNUC_NON_NULL(1); void fwupd_report_add_metadata_item(FwupdReport *self, const gchar *key, const gchar *value) G_GNUC_NON_NULL(1, 2); const gchar * fwupd_report_get_metadata_item(FwupdReport *self, const gchar *key) G_GNUC_NON_NULL(1, 2); guint64 fwupd_report_get_flags(FwupdReport *self) G_GNUC_NON_NULL(1); void fwupd_report_set_flags(FwupdReport *self, guint64 flags) G_GNUC_NON_NULL(1); void fwupd_report_add_flag(FwupdReport *self, FwupdReportFlags flag) G_GNUC_NON_NULL(1); void fwupd_report_remove_flag(FwupdReport *self, FwupdReportFlags flag) G_GNUC_NON_NULL(1); gboolean fwupd_report_has_flag(FwupdReport *self, FwupdReportFlags flag) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); const gchar * fwupd_report_flag_to_string(FwupdReportFlags report_flag); FwupdReportFlags fwupd_report_flag_from_string(const gchar *report_flag); G_END_DECLS fwupd-2.0.10/libfwupd/fwupd-request-private.h000066400000000000000000000004011501337203100211550ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fwupd-request.h" G_BEGIN_DECLS void fwupd_request_emit_invalidate(FwupdRequest *self) G_GNUC_NON_NULL(1); G_END_DECLS fwupd-2.0.10/libfwupd/fwupd-request.c000066400000000000000000000502731501337203100175140ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fwupd-codec.h" #include "fwupd-common-private.h" #include "fwupd-enums-private.h" #include "fwupd-request-private.h" /** * FwupdRequest: * * A user request from the device. * * See also: [class@FwupdDevice] */ typedef struct { gchar *id; FwupdRequestKind kind; FwupdRequestFlags flags; guint64 created; gchar *device_id; gchar *message; gchar *image; } FwupdRequestPrivate; enum { SIGNAL_INVALIDATE, SIGNAL_LAST }; enum { PROP_0, PROP_ID, PROP_KIND, PROP_FLAGS, PROP_MESSAGE, PROP_IMAGE, PROP_DEVICE_ID, PROP_LAST }; static guint signals[SIGNAL_LAST] = {0}; static void fwupd_request_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_EXTENDED(FwupdRequest, fwupd_request, G_TYPE_OBJECT, 0, G_ADD_PRIVATE(FwupdRequest) G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fwupd_request_codec_iface_init)); #define GET_PRIVATE(o) (fwupd_request_get_instance_private(o)) /** * fwupd_request_kind_to_string: * @kind: a update message kind, e.g. %FWUPD_REQUEST_KIND_IMMEDIATE * * Converts an enumerated update message kind to a string. * * Returns: identifier string * * Since: 1.6.2 **/ const gchar * fwupd_request_kind_to_string(FwupdRequestKind kind) { if (kind == FWUPD_REQUEST_KIND_UNKNOWN) return "unknown"; if (kind == FWUPD_REQUEST_KIND_POST) return "post"; if (kind == FWUPD_REQUEST_KIND_IMMEDIATE) return "immediate"; return NULL; } /** * fwupd_request_kind_from_string: * @kind: (nullable): a string, e.g. `immediate` * * Converts a string to an enumerated update message kind. * * Returns: enumerated value * * Since: 1.6.2 **/ FwupdRequestKind fwupd_request_kind_from_string(const gchar *kind) { if (g_strcmp0(kind, "unknown") == 0) return FWUPD_REQUEST_KIND_UNKNOWN; if (g_strcmp0(kind, "post") == 0) return FWUPD_REQUEST_KIND_POST; if (g_strcmp0(kind, "immediate") == 0) return FWUPD_REQUEST_KIND_IMMEDIATE; return FWUPD_REQUEST_KIND_LAST; } /** * fwupd_request_flag_to_string: * @flag: a request flag, e.g. %FWUPD_REQUEST_FLAG_NONE * * Converts an enumerated request flag to a string. * * Returns: identifier string * * Since: 1.8.6 **/ const gchar * fwupd_request_flag_to_string(FwupdRequestFlags flag) { if (flag == FWUPD_REQUEST_FLAG_NONE) return "none"; if (flag == FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE) return "allow-generic-message"; if (flag == FWUPD_REQUEST_FLAG_ALLOW_GENERIC_IMAGE) return "allow-generic-image"; if (flag == FWUPD_REQUEST_FLAG_NON_GENERIC_MESSAGE) return "non-generic-message"; if (flag == FWUPD_REQUEST_FLAG_NON_GENERIC_IMAGE) return "non-generic-image"; return NULL; } /** * fwupd_request_flag_from_string: * @flag: (nullable): a string, e.g. `none` * * Converts a string to an enumerated request flag. * * Returns: enumerated value * * Since: 1.8.6 **/ FwupdRequestFlags fwupd_request_flag_from_string(const gchar *flag) { if (g_strcmp0(flag, "allow-generic-message") == 0) return FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE; if (g_strcmp0(flag, "allow-generic-image") == 0) return FWUPD_REQUEST_FLAG_ALLOW_GENERIC_IMAGE; if (g_strcmp0(flag, "non-generic-message") == 0) return FWUPD_REQUEST_FLAG_NON_GENERIC_MESSAGE; if (g_strcmp0(flag, "non-generic-image") == 0) return FWUPD_REQUEST_FLAG_NON_GENERIC_IMAGE; return FWUPD_REQUEST_FLAG_NONE; } /** * fwupd_request_emit_invalidate: * @self: a #FwupdRequest * * Emits an `invalidate` signal to signify that the request is no longer valid, and any visible * UI components should be hidden. * * Since: 1.9.17 **/ void fwupd_request_emit_invalidate(FwupdRequest *self) { g_return_if_fail(FWUPD_IS_REQUEST(self)); g_debug("emitting FwupdRequest::invalidate()"); g_signal_emit(self, signals[SIGNAL_INVALIDATE], 0); } /** * fwupd_request_get_id: * @self: a #FwupdRequest * * Gets the ID. * * Returns: the ID, or %NULL if unset * * Since: 1.6.2 **/ const gchar * fwupd_request_get_id(FwupdRequest *self) { FwupdRequestPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REQUEST(self), NULL); return priv->id; } /** * fwupd_request_set_id: * @self: a #FwupdRequest * @id: (nullable): the request ID, e.g. `USB:foo` * * Sets the ID. * * Since: 1.6.2 **/ void fwupd_request_set_id(FwupdRequest *self, const gchar *id) { FwupdRequestPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REQUEST(self)); /* not changed */ if (g_strcmp0(priv->id, id) == 0) return; g_free(priv->id); priv->id = g_strdup(id); } /** * fwupd_request_get_device_id: * @self: a #FwupdRequest * * Gets the device_id that created the request. * * Returns: the device_id, or %NULL if unset * * Since: 1.6.2 **/ const gchar * fwupd_request_get_device_id(FwupdRequest *self) { FwupdRequestPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REQUEST(self), NULL); return priv->device_id; } /** * fwupd_request_set_device_id: * @self: a #FwupdRequest * @device_id: (nullable): the device_id, e.g. `colorhug` * * Sets the device_id that created the request. * * Since: 1.6.2 **/ void fwupd_request_set_device_id(FwupdRequest *self, const gchar *device_id) { FwupdRequestPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REQUEST(self)); /* not changed */ if (g_strcmp0(priv->device_id, device_id) == 0) return; g_free(priv->device_id); priv->device_id = g_strdup(device_id); } /** * fwupd_request_get_created: * @self: a #FwupdRequest * * Gets when the request was created. * * Returns: the UNIX time, or 0 if unset * * Since: 1.6.2 **/ guint64 fwupd_request_get_created(FwupdRequest *self) { FwupdRequestPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REQUEST(self), 0); return priv->created; } /** * fwupd_request_set_created: * @self: a #FwupdRequest * @created: the UNIX time * * Sets when the request was created. * * Since: 1.6.2 **/ void fwupd_request_set_created(FwupdRequest *self, guint64 created) { FwupdRequestPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REQUEST(self)); priv->created = created; } static void fwupd_request_add_variant(FwupdCodec *codec, GVariantBuilder *builder, FwupdCodecFlags flags) { FwupdRequest *self = FWUPD_REQUEST(codec); FwupdRequestPrivate *priv = GET_PRIVATE(self); if (priv->id != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_APPSTREAM_ID, g_variant_new_string(priv->id)); } if (priv->created > 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_CREATED, g_variant_new_uint64(priv->created)); } if (priv->device_id != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_DEVICE_ID, g_variant_new_string(priv->device_id)); } if (priv->message != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_UPDATE_MESSAGE, g_variant_new_string(priv->message)); } if (priv->image != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_UPDATE_IMAGE, g_variant_new_string(priv->image)); } if (priv->kind != FWUPD_REQUEST_KIND_UNKNOWN) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_REQUEST_KIND, g_variant_new_uint32(priv->kind)); } if (priv->flags != FWUPD_REQUEST_FLAG_NONE) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_FLAGS, g_variant_new_uint64(priv->flags)); } } static void fwupd_request_from_key_value(FwupdRequest *self, const gchar *key, GVariant *value) { if (g_strcmp0(key, FWUPD_RESULT_KEY_APPSTREAM_ID) == 0) { fwupd_request_set_id(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_CREATED) == 0) { fwupd_request_set_created(self, g_variant_get_uint64(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_DEVICE_ID) == 0) { fwupd_request_set_device_id(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_UPDATE_MESSAGE) == 0) { fwupd_request_set_message(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_UPDATE_IMAGE) == 0) { fwupd_request_set_image(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_REQUEST_KIND) == 0) { fwupd_request_set_kind(self, g_variant_get_uint32(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_FLAGS) == 0) { fwupd_request_set_flags(self, g_variant_get_uint64(value)); return; } } /** * fwupd_request_get_message: * @self: a #FwupdRequest * * Gets the update message, generating a generic one using the request ID if possible. * * Returns: the update message, or %NULL if unset * * Since: 1.6.2 **/ const gchar * fwupd_request_get_message(FwupdRequest *self) { FwupdRequestPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REQUEST(self), NULL); /* something custom */ if (priv->message != NULL) return priv->message; /* untranslated canned messages */ if (fwupd_request_has_flag(self, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE)) { if (g_strcmp0(priv->id, FWUPD_REQUEST_ID_REMOVE_REPLUG) == 0) return "Please unplug and then re-insert the device USB cable."; if (g_strcmp0(priv->id, FWUPD_REQUEST_ID_INSERT_USB_CABLE) == 0) return "Please re-insert the device USB cable."; if (g_strcmp0(priv->id, FWUPD_REQUEST_ID_REMOVE_USB_CABLE) == 0) return "Please unplug the device USB cable."; if (g_strcmp0(priv->id, FWUPD_REQUEST_ID_REPLUG_POWER) == 0) return "Please unplug and then re-insert the device power cable."; if (g_strcmp0(priv->id, FWUPD_REQUEST_ID_PRESS_UNLOCK) == 0) return "Press unlock on the device."; if (g_strcmp0(priv->id, FWUPD_REQUEST_ID_DO_NOT_POWER_OFF) == 0) return "Do not turn off your computer or remove the AC adaptor."; if (g_strcmp0(priv->id, FWUPD_REQUEST_ID_RESTART_DAEMON) == 0) return "Please restart the fwupd service."; } /* unknown */ return NULL; } /** * fwupd_request_set_message: * @self: a #FwupdRequest * @message: (nullable): the update message string * * Sets the update message. * * Since: 1.6.2 **/ void fwupd_request_set_message(FwupdRequest *self, const gchar *message) { FwupdRequestPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REQUEST(self)); /* not changed */ if (g_strcmp0(priv->message, message) == 0) return; g_free(priv->message); priv->message = g_strdup(message); g_object_notify(G_OBJECT(self), "message"); } /** * fwupd_request_get_image: * @self: a #FwupdRequest * * Gets the update image. * * Returns: the update image URL, or %NULL if unset * * Since: 1.6.2 **/ const gchar * fwupd_request_get_image(FwupdRequest *self) { FwupdRequestPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REQUEST(self), NULL); return priv->image; } /** * fwupd_request_set_image: * @self: a #FwupdRequest * @image: (nullable): the update image URL * * Sets the update image. * * Since: 1.6.2 **/ void fwupd_request_set_image(FwupdRequest *self, const gchar *image) { FwupdRequestPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REQUEST(self)); /* not changed */ if (g_strcmp0(priv->image, image) == 0) return; g_free(priv->image); priv->image = g_strdup(image); g_object_notify(G_OBJECT(self), "image"); } /** * fwupd_request_get_kind: * @self: a #FwupdRequest * * Returns what the request is currently doing. * * Returns: the kind value, e.g. %FWUPD_STATUS_REQUEST_WRITE * * Since: 1.6.2 **/ FwupdRequestKind fwupd_request_get_kind(FwupdRequest *self) { FwupdRequestPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REQUEST(self), 0); return priv->kind; } /** * fwupd_request_set_kind: * @self: a #FwupdRequest * @kind: the kind value, e.g. %FWUPD_STATUS_REQUEST_WRITE * * Sets what the request is currently doing. * * Since: 1.6.2 **/ void fwupd_request_set_kind(FwupdRequest *self, FwupdRequestKind kind) { FwupdRequestPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REQUEST(self)); if (priv->kind == kind) return; priv->kind = kind; g_object_notify(G_OBJECT(self), "kind"); } /** * fwupd_request_get_flags: * @self: a #FwupdRequest * * Gets the request flags. * * Returns: request flags, or 0 if unset * * Since: 1.8.6 **/ FwupdRequestFlags fwupd_request_get_flags(FwupdRequest *self) { FwupdRequestPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REQUEST(self), 0); return priv->flags; } /** * fwupd_request_set_flags: * @self: a #FwupdRequest * @flags: request flags, e.g. %FWUPD_REQUEST_FLAG_NONE * * Sets the request flags. * * Since: 1.8.6 **/ void fwupd_request_set_flags(FwupdRequest *self, FwupdRequestFlags flags) { FwupdRequestPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REQUEST(self)); /* not changed */ if (priv->flags == flags) return; priv->flags = flags; g_object_notify(G_OBJECT(self), "flags"); } /** * fwupd_request_add_flag: * @self: a #FwupdRequest * @flag: the #FwupdRequestFlags * * Adds a specific flag to the request. * * Since: 1.8.6 **/ void fwupd_request_add_flag(FwupdRequest *self, FwupdRequestFlags flag) { FwupdRequestPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REQUEST(self)); priv->flags |= flag; } /** * fwupd_request_remove_flag: * @self: a #FwupdRequest * @flag: the #FwupdRequestFlags * * Removes a specific flag from the request. * * Since: 1.8.6 **/ void fwupd_request_remove_flag(FwupdRequest *self, FwupdRequestFlags flag) { FwupdRequestPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_REQUEST(self)); priv->flags &= ~flag; } /** * fwupd_request_has_flag: * @self: a #FwupdRequest * @flag: the #FwupdRequestFlags * * Finds if the request has a specific flag. * * Returns: %TRUE if the flag is set * * Since: 1.8.6 **/ gboolean fwupd_request_has_flag(FwupdRequest *self, FwupdRequestFlags flag) { FwupdRequestPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_REQUEST(self), FALSE); return (priv->flags & flag) > 0; } static void fwupd_request_add_string(FwupdCodec *codec, guint idt, GString *str) { FwupdRequest *self = FWUPD_REQUEST(codec); FwupdRequestPrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_APPSTREAM_ID, priv->id); if (priv->kind != FWUPD_REQUEST_KIND_UNKNOWN) { fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_REQUEST_KIND, fwupd_request_kind_to_string(priv->kind)); } fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_FLAGS, fwupd_request_flag_to_string(priv->flags)); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_DEVICE_ID, priv->device_id); fwupd_codec_string_append_time(str, idt, FWUPD_RESULT_KEY_CREATED, priv->created); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_UPDATE_MESSAGE, priv->message); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_UPDATE_IMAGE, priv->image); } static void fwupd_request_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FwupdRequest *self = FWUPD_REQUEST(object); FwupdRequestPrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_ID: g_value_set_string(value, priv->id); break; case PROP_MESSAGE: g_value_set_string(value, priv->message); break; case PROP_IMAGE: g_value_set_string(value, priv->image); break; case PROP_DEVICE_ID: g_value_set_string(value, priv->device_id); break; case PROP_KIND: g_value_set_uint(value, priv->kind); break; case PROP_FLAGS: g_value_set_uint64(value, priv->flags); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fwupd_request_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FwupdRequest *self = FWUPD_REQUEST(object); switch (prop_id) { case PROP_ID: fwupd_request_set_id(self, g_value_get_string(value)); break; case PROP_MESSAGE: fwupd_request_set_message(self, g_value_get_string(value)); break; case PROP_IMAGE: fwupd_request_set_image(self, g_value_get_string(value)); break; case PROP_DEVICE_ID: fwupd_request_set_device_id(self, g_value_get_string(value)); break; case PROP_KIND: fwupd_request_set_kind(self, g_value_get_uint(value)); break; case PROP_FLAGS: fwupd_request_set_flags(self, g_value_get_uint64(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fwupd_request_finalize(GObject *object) { FwupdRequest *self = FWUPD_REQUEST(object); FwupdRequestPrivate *priv = GET_PRIVATE(self); g_free(priv->id); g_free(priv->device_id); g_free(priv->message); g_free(priv->image); G_OBJECT_CLASS(fwupd_request_parent_class)->finalize(object); } static void fwupd_request_init(FwupdRequest *self) { FwupdRequestPrivate *priv = GET_PRIVATE(self); priv->created = g_get_real_time() / G_USEC_PER_SEC; } static void fwupd_request_class_init(FwupdRequestClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); GParamSpec *pspec; object_class->finalize = fwupd_request_finalize; object_class->get_property = fwupd_request_get_property; object_class->set_property = fwupd_request_set_property; /** * FwupdRequest::invalidate: * @self: the #FwupdRequest instance that emitted the signal * * The ::invalidate signal is emitted when the request is no longer valid, and any visible * UI components should be hidden. * * Since: 1.9.17 **/ signals[SIGNAL_INVALIDATE] = g_signal_new("invalidate", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(FwupdRequestClass, invalidate), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); /** * FwupdRequest:id: * * The request identifier. * * Since: 1.6.2 */ pspec = g_param_spec_string("id", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_ID, pspec); /** * FwupdRequest:kind: * * The kind of the request. * * Since: 1.6.2 */ pspec = g_param_spec_uint("kind", NULL, NULL, FWUPD_REQUEST_KIND_UNKNOWN, FWUPD_REQUEST_KIND_LAST, FWUPD_REQUEST_KIND_UNKNOWN, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_KIND, pspec); /** * FwupdRequest:flags: * * The flags for the request. * * Since: 1.8.6 */ pspec = g_param_spec_uint64("flags", NULL, NULL, FWUPD_REQUEST_FLAG_NONE, FWUPD_REQUEST_FLAG_UNKNOWN, FWUPD_REQUEST_FLAG_UNKNOWN, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_FLAGS, pspec); /** * FwupdRequest:message: * * The message text in the request. * * Since: 1.6.2 */ pspec = g_param_spec_string("message", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_MESSAGE, pspec); /** * FwupdRequest:image: * * The image link for the request. * * Since: 1.6.2 */ pspec = g_param_spec_string("image", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_IMAGE, pspec); /** * FwupdRequest:device-id: * * The device ID for the request. * * Since: 1.8.2 */ pspec = g_param_spec_string("device-id", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_DEVICE_ID, pspec); } static void fwupd_request_from_variant_iter(FwupdCodec *codec, GVariantIter *iter) { FwupdRequest *self = FWUPD_REQUEST(codec); GVariant *value; const gchar *key; while (g_variant_iter_next(iter, "{&sv}", &key, &value)) { fwupd_request_from_key_value(self, key, value); g_variant_unref(value); } } static void fwupd_request_codec_iface_init(FwupdCodecInterface *iface) { iface->add_string = fwupd_request_add_string; iface->add_variant = fwupd_request_add_variant; iface->from_variant_iter = fwupd_request_from_variant_iter; } /** * fwupd_request_new: * * Creates a new request. * * Returns: a new #FwupdRequest * * Since: 1.6.2 **/ FwupdRequest * fwupd_request_new(void) { FwupdRequest *self; self = g_object_new(FWUPD_TYPE_REQUEST, NULL); return FWUPD_REQUEST(self); } fwupd-2.0.10/libfwupd/fwupd-request.h000066400000000000000000000146561501337203100175260ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_BEGIN_DECLS #define FWUPD_TYPE_REQUEST (fwupd_request_get_type()) G_DECLARE_DERIVABLE_TYPE(FwupdRequest, fwupd_request, FWUPD, REQUEST, GObject) struct _FwupdRequestClass { GObjectClass parent_class; void (*invalidate)(FwupdRequest *client); /*< private >*/ void (*_fwupd_reserved2)(void); void (*_fwupd_reserved3)(void); void (*_fwupd_reserved4)(void); void (*_fwupd_reserved5)(void); void (*_fwupd_reserved6)(void); void (*_fwupd_reserved7)(void); }; /** * FwupdRequestKind: * * The kind of request we are asking of the user. **/ typedef enum { /** * FWUPD_REQUEST_KIND_UNKNOWN: * * Unknown kind. * * Since: 1.6.2 */ FWUPD_REQUEST_KIND_UNKNOWN, /** * FWUPD_REQUEST_KIND_POST: * * After the update. * * Since: 1.6.2 */ FWUPD_REQUEST_KIND_POST, /** * FWUPD_REQUEST_KIND_IMMEDIATE: * * Immediately. * * Since: 1.6.2 */ FWUPD_REQUEST_KIND_IMMEDIATE, /*< private >*/ FWUPD_REQUEST_KIND_LAST } FwupdRequestKind; /** * FWUPD_REQUEST_ID_REMOVE_REPLUG: * * The user needs to remove and reinsert the device to complete the update, e.g. * "The update will continue when the device USB cable has been unplugged and then re-inserted." * * Since 1.6.2 */ #define FWUPD_REQUEST_ID_REMOVE_REPLUG "org.freedesktop.fwupd.request.remove-replug" /** * FWUPD_REQUEST_ID_PRESS_UNLOCK: * * The user needs to press unlock on the device to continue, e.g. * "Press unlock on the device to continue the update process." * * Since 1.6.2 */ #define FWUPD_REQUEST_ID_PRESS_UNLOCK "org.freedesktop.fwupd.request.press-unlock" /** * FWUPD_REQUEST_ID_REMOVE_USB_CABLE: * * The user needs to remove the device to complete the update, e.g. * "The update will continue when the device USB cable has been unplugged." * * Since 1.8.6 */ #define FWUPD_REQUEST_ID_REMOVE_USB_CABLE "org.freedesktop.fwupd.request.remove-usb-cable" /** * FWUPD_REQUEST_ID_INSERT_USB_CABLE: * * The user needs to insert the cable to complete the update, e.g. * "The update will continue when the device USB cable has been re-inserted." * * Since 1.8.9 */ #define FWUPD_REQUEST_ID_INSERT_USB_CABLE "org.freedesktop.fwupd.request.insert-usb-cable" /** * FWUPD_REQUEST_ID_DO_NOT_POWER_OFF: * * Show the user a message not to unplug the machine from the AC power, e.g. * "Do not turn off your computer or remove the AC adaptor until you are sure the update has * completed." * * Since 1.8.6 */ #define FWUPD_REQUEST_ID_DO_NOT_POWER_OFF "org.freedesktop.fwupd.request.do-not-power-off" /** * FWUPD_REQUEST_ID_REPLUG_INSTALL: * * Show the user a message to replug the device and then install the firmware, e.g. * "Unplug and replug the device, to continue the update process." * * Since 1.8.11 */ #define FWUPD_REQUEST_ID_REPLUG_INSTALL "org.freedesktop.fwupd.replug-install" /** * FWUPD_REQUEST_ID_REPLUG_POWER: * * Show the user a message to replug the power connector, e.g. * "The update will continue when the device power cable has been unplugged and then re-inserted." * * Since 1.9.9 */ #define FWUPD_REQUEST_ID_REPLUG_POWER "org.freedesktop.fwupd.replug-power" /** * FWUPD_REQUEST_ID_RESTART_DAEMON: * * Show the user a message that they need to restart the daemon, e.g. * "Please restart the fwupd service." * * Since 2.0.1 */ #define FWUPD_REQUEST_ID_RESTART_DAEMON "org.freedesktop.fwupd.restart-daemon" /** * FwupdRequestFlags: * * Flags used to represent request attributes */ typedef enum { /** * FWUPD_REQUEST_FLAG_NONE: * * No flags are set. * * Since: 1.8.6 */ FWUPD_REQUEST_FLAG_NONE = 0u, /** * FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE: * * Use a generic (translated) request message. * * Since: 1.8.6 */ FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE = 1u << 0, /** * FWUPD_REQUEST_FLAG_ALLOW_GENERIC_IMAGE: * * Use a generic (translated) request image. * * Since: 1.8.6 */ FWUPD_REQUEST_FLAG_ALLOW_GENERIC_IMAGE = 1u << 1, /** * FWUPD_REQUEST_FLAG_NON_GENERIC_MESSAGE: * * Device requires a non-generic interaction with custom non-translatable text. * * Since: 1.9.10 */ FWUPD_REQUEST_FLAG_NON_GENERIC_MESSAGE = 1ull << 2, /** * FWUPD_REQUEST_FLAG_NON_GENERIC_IMAGE: * * Device requires to show the user a custom image for the action to make sense. * * Since: 1.9.10 */ FWUPD_REQUEST_FLAG_NON_GENERIC_IMAGE = 1ull << 3, /** * FWUPD_REQUEST_FLAG_UNKNOWN: * * The request flag is unknown, typically caused by using mismatched client and daemon. * * Since: 1.8.6 */ FWUPD_REQUEST_FLAG_UNKNOWN = G_MAXUINT64, } FwupdRequestFlags; const gchar * fwupd_request_kind_to_string(FwupdRequestKind kind); FwupdRequestKind fwupd_request_kind_from_string(const gchar *kind); const gchar * fwupd_request_flag_to_string(FwupdRequestFlags flag); FwupdRequestFlags fwupd_request_flag_from_string(const gchar *flag); FwupdRequest * fwupd_request_new(void); const gchar * fwupd_request_get_id(FwupdRequest *self) G_GNUC_NON_NULL(1); void fwupd_request_set_id(FwupdRequest *self, const gchar *id) G_GNUC_NON_NULL(1); guint64 fwupd_request_get_created(FwupdRequest *self) G_GNUC_NON_NULL(1); void fwupd_request_set_created(FwupdRequest *self, guint64 created) G_GNUC_NON_NULL(1); const gchar * fwupd_request_get_device_id(FwupdRequest *self) G_GNUC_NON_NULL(1); void fwupd_request_set_device_id(FwupdRequest *self, const gchar *device_id) G_GNUC_NON_NULL(1); const gchar * fwupd_request_get_message(FwupdRequest *self) G_GNUC_NON_NULL(1); void fwupd_request_set_message(FwupdRequest *self, const gchar *message) G_GNUC_NON_NULL(1); const gchar * fwupd_request_get_image(FwupdRequest *self) G_GNUC_NON_NULL(1); void fwupd_request_set_image(FwupdRequest *self, const gchar *image) G_GNUC_NON_NULL(1); FwupdRequestKind fwupd_request_get_kind(FwupdRequest *self) G_GNUC_NON_NULL(1); void fwupd_request_set_kind(FwupdRequest *self, FwupdRequestKind kind) G_GNUC_NON_NULL(1); FwupdRequestFlags fwupd_request_get_flags(FwupdRequest *self) G_GNUC_NON_NULL(1); void fwupd_request_set_flags(FwupdRequest *self, FwupdRequestFlags flags) G_GNUC_NON_NULL(1); void fwupd_request_add_flag(FwupdRequest *self, FwupdRequestFlags flag) G_GNUC_NON_NULL(1); void fwupd_request_remove_flag(FwupdRequest *self, FwupdRequestFlags flag) G_GNUC_NON_NULL(1); gboolean fwupd_request_has_flag(FwupdRequest *self, FwupdRequestFlags flag) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); G_END_DECLS fwupd-2.0.10/libfwupd/fwupd-security-attr-private.h000066400000000000000000000251161501337203100223160ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fwupd-build.h" #include "fwupd-security-attr.h" G_BEGIN_DECLS /** * FWUPD_SECURITY_ATTR_ID_PREBOOT_DMA_PROTECTION: * * Host Security ID attribute for Pre-boot DMA protection * * This was previously known as org.fwupd.hsi.AcpiDmar for Intel from 1.5.0+. * * Since: 1.8.0 **/ #define FWUPD_SECURITY_ATTR_ID_PREBOOT_DMA_PROTECTION "org.fwupd.hsi.PrebootDma" /** * FWUPD_SECURITY_ATTR_ID_ENCRYPTED_RAM: * * Host Security ID attribute indicating encrypted RAM available * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_ENCRYPTED_RAM "org.fwupd.hsi.EncryptedRam" /** * FWUPD_SECURITY_ATTR_ID_FWUPD_ATTESTATION: * * Host Security ID attribute for attestation * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_FWUPD_ATTESTATION "org.fwupd.hsi.Fwupd.Attestation" /** * FWUPD_SECURITY_ATTR_ID_FWUPD_PLUGINS: * * Host Security ID attribute for plugins * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_FWUPD_PLUGINS "org.fwupd.hsi.Fwupd.Plugins" /** * FWUPD_SECURITY_ATTR_ID_FWUPD_UPDATES: * * Host Security ID attribute for updates * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_FWUPD_UPDATES "org.fwupd.hsi.Fwupd.Updates" /** * FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_ENABLED: * * Host Security ID attribute for Intel Bootguard enabled * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_ENABLED "org.fwupd.hsi.IntelBootguard.Enabled" /** * FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_VERIFIED: * * Host Security ID attribute for Intel Bootguard verified * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_VERIFIED "org.fwupd.hsi.IntelBootguard.Verified" /** * FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_ACM: * * Host Security ID attribute for Intel Bootguard ACM * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_ACM "org.fwupd.hsi.IntelBootguard.Acm" /** * FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_POLICY: * * Host Security ID attribute for Intel Bootguard policy * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_POLICY "org.fwupd.hsi.IntelBootguard.Policy" /** * FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_OTP: * * Host Security ID attribute for Intel Bootguard OTP fuse * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_OTP "org.fwupd.hsi.IntelBootguard.Otp" /** * FWUPD_SECURITY_ATTR_ID_IOMMU: * * Host Security ID attribute for IOMMU * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_IOMMU "org.fwupd.hsi.Iommu" /** * FWUPD_SECURITY_ATTR_ID_KERNEL_LOCKDOWN: * * Host Security ID attribute for kernel lockdown * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_KERNEL_LOCKDOWN "org.fwupd.hsi.Kernel.Lockdown" /** * FWUPD_SECURITY_ATTR_ID_KERNEL_SWAP: * * Host Security ID attribute for kernel swap * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_KERNEL_SWAP "org.fwupd.hsi.Kernel.Swap" /** * FWUPD_SECURITY_ATTR_ID_KERNEL_TAINTED: * * Host Security ID attribute for kernel taint * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_KERNEL_TAINTED "org.fwupd.hsi.Kernel.Tainted" /** * FWUPD_SECURITY_ATTR_ID_MEI_MANUFACTURING_MODE: * * Host Security ID attribute for Intel ME manufacturing mode * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_MEI_MANUFACTURING_MODE "org.fwupd.hsi.Mei.ManufacturingMode" /** * FWUPD_SECURITY_ATTR_ID_MEI_OVERRIDE_STRAP: * * Host Security ID attribute for Intel ME override strap * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_MEI_OVERRIDE_STRAP "org.fwupd.hsi.Mei.OverrideStrap" /** * FWUPD_SECURITY_ATTR_ID_MEI_KEY_MANIFEST: * * Host Security ID attribute for Intel ME Key Manifest * * Since: 1.8.7 **/ #define FWUPD_SECURITY_ATTR_ID_MEI_KEY_MANIFEST "org.fwupd.hsi.Mei.KeyManifest" /** * FWUPD_SECURITY_ATTR_ID_MEI_VERSION: * * Host Security ID attribute for Intel ME version * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_MEI_VERSION "org.fwupd.hsi.Mei.Version" /** * FWUPD_SECURITY_ATTR_ID_SPI_BIOSWE: * * Host Security ID attribute for Intel SPI BIOSWE configuration * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_SPI_BIOSWE "org.fwupd.hsi.Spi.Bioswe" /** * FWUPD_SECURITY_ATTR_ID_SPI_BLE: * * Host Security ID attribute for Intel SPI BLE configuration * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_SPI_BLE "org.fwupd.hsi.Spi.Ble" /** * FWUPD_SECURITY_ATTR_ID_SPI_SMM_BWP: * * Host Security ID attribute for Intel SPI SMM BWP * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_SPI_SMM_BWP "org.fwupd.hsi.Spi.SmmBwp" /** * FWUPD_SECURITY_ATTR_ID_SPI_DESCRIPTOR: * * Host Security ID attribute for Intel SPI descriptor * * Since: 1.6.0 **/ #define FWUPD_SECURITY_ATTR_ID_SPI_DESCRIPTOR "org.fwupd.hsi.Spi.Descriptor" /** * FWUPD_SECURITY_ATTR_ID_SUSPEND_TO_IDLE: * * Host Security ID attribute for Suspend to Idle * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_SUSPEND_TO_IDLE "org.fwupd.hsi.SuspendToIdle" /** * FWUPD_SECURITY_ATTR_ID_SUSPEND_TO_RAM: * * Host Security ID attribute for Suspend to RAM * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_SUSPEND_TO_RAM "org.fwupd.hsi.SuspendToRam" /** * FWUPD_SECURITY_ATTR_ID_TPM_EMPTY_PCR: * * Host Security ID attribute for empty PCR * * Since: 1.7.2 **/ #define FWUPD_SECURITY_ATTR_ID_TPM_EMPTY_PCR "org.fwupd.hsi.Tpm.EmptyPcr" /** * FWUPD_SECURITY_ATTR_ID_TPM_RECONSTRUCTION_PCR0: * * Host Security ID attribute for TPM PCR0 reconstruction * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_TPM_RECONSTRUCTION_PCR0 "org.fwupd.hsi.Tpm.ReconstructionPcr0" /** * FWUPD_SECURITY_ATTR_ID_TPM_VERSION_20: * * Host Security ID attribute for TPM 2.0 * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_TPM_VERSION_20 "org.fwupd.hsi.Tpm.Version20" /** * FWUPD_SECURITY_ATTR_ID_UEFI_SECUREBOOT: * * Host Security ID attribute for UEFI secure boot * * Since: 1.5.0 **/ #define FWUPD_SECURITY_ATTR_ID_UEFI_SECUREBOOT "org.fwupd.hsi.Uefi.SecureBoot" /** * FWUPD_SECURITY_ATTR_ID_UEFI_BOOTSERVICE_VARS: * * Host Security ID attribute indicating if Bootservice-only variables are hidden. * * Since: 1.9.3 **/ #define FWUPD_SECURITY_ATTR_ID_UEFI_BOOTSERVICE_VARS "org.fwupd.hsi.Uefi.BootserviceVars" /** * FWUPD_SECURITY_ATTR_ID_PLATFORM_DEBUG_ENABLED: * * Host Security ID attribute for parts with debugging capabilities enabled * * This was previously known as org.fwupd.hsi.PlatformDebugEnabled for Intel 1.5.0+ * It was renamed for all vendor support in 1.8.0. * * * Since: 1.8.0 **/ #define FWUPD_SECURITY_ATTR_ID_PLATFORM_DEBUG_ENABLED "org.fwupd.hsi.PlatformDebugEnabled" /** * FWUPD_SECURITY_ATTR_ID_PLATFORM_FUSED: * * Host Security ID attribute for fused parts * * Since: 1.8.0 **/ #define FWUPD_SECURITY_ATTR_ID_PLATFORM_FUSED "org.fwupd.hsi.PlatformFused" /** * FWUPD_SECURITY_ATTR_ID_PLATFORM_DEBUG_LOCKED: * * Host Security ID attribute for parts locked from debugging * * This was previously known as org.fwupd.hsi.IntelDci.Locked for Intel 1.5.0+ * It was renamed for all vendor support in 1.8.0. * * Since: 1.8.0 **/ #define FWUPD_SECURITY_ATTR_ID_PLATFORM_DEBUG_LOCKED "org.fwupd.hsi.PlatformDebugLocked" /** * FWUPD_SECURITY_ATTR_ID_UEFI_PK: * * Host Security ID attribute for UEFI PK * * Since: 1.5.5 **/ #define FWUPD_SECURITY_ATTR_ID_UEFI_PK "org.fwupd.hsi.Uefi.Pk" /** * FWUPD_SECURITY_ATTR_ID_SUPPORTED_CPU: * * Host Security ID attribute for Supported CPU * * Since: 1.8.0 **/ #define FWUPD_SECURITY_ATTR_ID_SUPPORTED_CPU "org.fwupd.hsi.SupportedCpu" /** * FWUPD_SECURITY_ATTR_ID_AMD_ROLLBACK_PROTECTION: * * Host Security ID attribute for Rollback protection of AMD platform * firmware * * Since: 1.8.0 **/ #define FWUPD_SECURITY_ATTR_ID_AMD_ROLLBACK_PROTECTION "org.fwupd.hsi.Amd.RollbackProtection" /** * FWUPD_SECURITY_ATTR_ID_AMD_SPI_WRITE_PROTECTION: * * Host Security ID attribute for SPI Write protection * * Since: 1.8.0 **/ #define FWUPD_SECURITY_ATTR_ID_AMD_SPI_WRITE_PROTECTION "org.fwupd.hsi.Amd.SpiWriteProtection" /** * FWUPD_SECURITY_ATTR_ID_AMD_SPI_REPLAY_PROTECTION: * * Host Security ID attribute for SPI replay protection * * Since: 1.8.0 **/ #define FWUPD_SECURITY_ATTR_ID_AMD_SPI_REPLAY_PROTECTION "org.fwupd.hsi.Amd.SpiReplayProtection" /** * FWUPD_SECURITY_ATTR_ID_HOST_EMULATION: * * Host Security ID attribute for host emulation * * Since: 1.8.3 **/ #define FWUPD_SECURITY_ATTR_ID_HOST_EMULATION "org.fwupd.hsi.HostEmulation" /** * FWUPD_SECURITY_ATTR_ID_BIOS_ROLLBACK_PROTECTION: * * Host Security ID attribute for Rollback protection of BIOS firmware * * Since: 1.8.8 **/ #define FWUPD_SECURITY_ATTR_ID_BIOS_ROLLBACK_PROTECTION "org.fwupd.hsi.Bios.RollbackProtection" /** * FWUPD_SECURITY_ATTR_ID_INTEL_GDS: * * Host Security ID attribute indicating the processor is safe against Gather Data Sampling. * * Since: 1.9.4 **/ #define FWUPD_SECURITY_ATTR_ID_INTEL_GDS "org.fwupd.hsi.IntelGds" /** * FWUPD_SECURITY_ATTR_ID_BIOS_CAPSULE_UPDATES: * * Host Security ID attribute indicating Capsule updates are supported by the BIOS. * * Since: 1.9.6 **/ #define FWUPD_SECURITY_ATTR_ID_BIOS_CAPSULE_UPDATES "org.fwupd.hsi.Bios.CapsuleUpdates" /** * FWUPD_SECURITY_ATTR_ID_SMAP: * * Host Security ID attribute for SMAP * * NOTE: This attribute use to be known as org.fwupd.hsi.IntelSmap before fwupd 2.0.0 * * Since: 2.0.0 **/ #define FWUPD_SECURITY_ATTR_ID_SMAP "org.fwupd.hsi.Smap" /** * FWUPD_SECURITY_ATTR_ID_CET_ENABLED: * * Host Security ID attribute for Intel CET enabled * * NOTE: This used to be known as org.fwupd.hsi.IntelCet.Enabled before fwupd 2.0.0 * * Since: 2.0.0 **/ #define FWUPD_SECURITY_ATTR_ID_CET_ENABLED "org.fwupd.hsi.Cet.Enabled" /** * FWUPD_SECURITY_ATTR_ID_CET_ACTIVE: * * Host Security ID attribute for Intel CET active * * NOTE: This used to be known as org.fwupd.hsi.IntelCet.Active before fwupd 2.0.0 * * Since: 2.0.0 **/ #define FWUPD_SECURITY_ATTR_ID_CET_ACTIVE "org.fwupd.hsi.Cet.Active" /** * FWUPD_SECURITY_ATTR_ID_AMD_SMM_LOCKED: * * Host Security ID attribute for AMD SMM locked * * Since: 2.0.2 **/ #define FWUPD_SECURITY_ATTR_ID_AMD_SMM_LOCKED "org.fwupd.hsi.Amd.SmmLocked" /** * FWUPD_SECURITY_ATTR_ID_UEFI_MEMORY_PROTECTION: * * Host Security ID attribute for UEFI memory protection * * Since: 2.0.7 **/ #define FWUPD_SECURITY_ATTR_ID_UEFI_MEMORY_PROTECTION "org.fwupd.hsi.Uefi.MemoryProtection" /** * FWUPD_SECURITY_ATTR_ID_UEFI_DB: * * Host Security ID attribute for UEFI db certificate store * * Since: 2.0.8 **/ #define FWUPD_SECURITY_ATTR_ID_UEFI_DB "org.fwupd.hsi.Uefi.Db" FwupdSecurityAttr * fwupd_security_attr_copy(FwupdSecurityAttr *self) G_GNUC_NON_NULL(1); G_END_DECLS fwupd-2.0.10/libfwupd/fwupd-security-attr.c000066400000000000000000001564271501337203100206530ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fwupd-codec.h" #include "fwupd-common-private.h" #include "fwupd-enums-private.h" #include "fwupd-error.h" #include "fwupd-security-attr-private.h" /** * FwupdSecurityAttr: * * A Host Security ID attribute that represents something that was measured. */ static void fwupd_security_attr_finalize(GObject *object); typedef struct { gchar *appstream_id; GPtrArray *obsoletes; GPtrArray *guids; GHashTable *metadata; /* (nullable) */ gchar *name; gchar *title; gchar *description; gchar *plugin; gchar *fwupd_version; gchar *url; guint64 created; FwupdSecurityAttrLevel level; FwupdSecurityAttrResult result; FwupdSecurityAttrResult result_fallback; FwupdSecurityAttrResult result_success; FwupdSecurityAttrFlags flags; gchar *bios_setting_id; gchar *bios_setting_target_value; gchar *bios_setting_current_value; gchar *kernel_current_value; gchar *kernel_target_value; } FwupdSecurityAttrPrivate; static void fwupd_security_attr_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_EXTENDED(FwupdSecurityAttr, fwupd_security_attr, G_TYPE_OBJECT, 0, G_ADD_PRIVATE(FwupdSecurityAttr) G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fwupd_security_attr_codec_iface_init)); #define GET_PRIVATE(o) (fwupd_security_attr_get_instance_private(o)) /** * fwupd_security_attr_flag_to_string: * @flag: security attribute flags, e.g. %FWUPD_SECURITY_ATTR_FLAG_SUCCESS * * Returns the printable string for the flag. * * Returns: string, or %NULL * * Since: 1.5.0 **/ const gchar * fwupd_security_attr_flag_to_string(FwupdSecurityAttrFlags flag) { if (flag == FWUPD_SECURITY_ATTR_FLAG_NONE) return "none"; if (flag == FWUPD_SECURITY_ATTR_FLAG_SUCCESS) return "success"; if (flag == FWUPD_SECURITY_ATTR_FLAG_OBSOLETED) return "obsoleted"; if (flag == FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA) return "missing-data"; if (flag == FWUPD_SECURITY_ATTR_FLAG_RUNTIME_UPDATES) return "runtime-updates"; if (flag == FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ATTESTATION) return "runtime-attestation"; if (flag == FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE) return "runtime-issue"; if (flag == FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM) return "action-contact-oem"; if (flag == FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW) return "action-config-fw"; if (flag == FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_OS) return "action-config-os"; if (flag == FWUPD_SECURITY_ATTR_FLAG_CAN_FIX) return "can-fix"; if (flag == FWUPD_SECURITY_ATTR_FLAG_CAN_UNDO) return "can-undo"; return NULL; } /** * fwupd_security_attr_flag_from_string: * @flag: (nullable): a string, e.g. `success` * * Converts a string to an enumerated flag. * * Returns: enumerated value * * Since: 1.7.1 **/ FwupdSecurityAttrFlags fwupd_security_attr_flag_from_string(const gchar *flag) { if (g_strcmp0(flag, "success") == 0) return FWUPD_SECURITY_ATTR_FLAG_SUCCESS; if (g_strcmp0(flag, "obsoleted") == 0) return FWUPD_SECURITY_ATTR_FLAG_OBSOLETED; if (g_strcmp0(flag, "missing-data") == 0) return FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA; if (g_strcmp0(flag, "runtime-updates") == 0) return FWUPD_SECURITY_ATTR_FLAG_RUNTIME_UPDATES; if (g_strcmp0(flag, "runtime-attestation") == 0) return FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ATTESTATION; if (g_strcmp0(flag, "runtime-issue") == 0) return FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE; if (g_strcmp0(flag, "action-contact-oem") == 0) return FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM; if (g_strcmp0(flag, "action-config-fw") == 0) return FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW; if (g_strcmp0(flag, "action-config-os") == 0) return FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_OS; if (g_strcmp0(flag, "can-fix") == 0) return FWUPD_SECURITY_ATTR_FLAG_CAN_FIX; if (g_strcmp0(flag, "can-undo") == 0) return FWUPD_SECURITY_ATTR_FLAG_CAN_UNDO; return FWUPD_SECURITY_ATTR_FLAG_NONE; } /** * fwupd_security_attr_result_to_string: * @result: security attribute result, e.g. %FWUPD_SECURITY_ATTR_RESULT_ENABLED * * Returns the printable string for the result enum. * * Returns: string, or %NULL * * Since: 1.5.0 **/ const gchar * fwupd_security_attr_result_to_string(FwupdSecurityAttrResult result) { if (result == FWUPD_SECURITY_ATTR_RESULT_VALID) return "valid"; if (result == FWUPD_SECURITY_ATTR_RESULT_NOT_VALID) return "not-valid"; if (result == FWUPD_SECURITY_ATTR_RESULT_ENABLED) return "enabled"; if (result == FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED) return "not-enabled"; if (result == FWUPD_SECURITY_ATTR_RESULT_LOCKED) return "locked"; if (result == FWUPD_SECURITY_ATTR_RESULT_NOT_LOCKED) return "not-locked"; if (result == FWUPD_SECURITY_ATTR_RESULT_ENCRYPTED) return "encrypted"; if (result == FWUPD_SECURITY_ATTR_RESULT_NOT_ENCRYPTED) return "not-encrypted"; if (result == FWUPD_SECURITY_ATTR_RESULT_TAINTED) return "tainted"; if (result == FWUPD_SECURITY_ATTR_RESULT_NOT_TAINTED) return "not-tainted"; if (result == FWUPD_SECURITY_ATTR_RESULT_FOUND) return "found"; if (result == FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND) return "not-found"; if (result == FWUPD_SECURITY_ATTR_RESULT_SUPPORTED) return "supported"; if (result == FWUPD_SECURITY_ATTR_RESULT_NOT_SUPPORTED) return "not-supported"; return NULL; } /** * fwupd_security_attr_result_from_string: * @result: (nullable): a string, e.g. `not-encrypted` * * Converts a string to an enumerated result. * * Returns: enumerated value * * Since: 1.7.1 **/ FwupdSecurityAttrResult fwupd_security_attr_result_from_string(const gchar *result) { if (g_strcmp0(result, "valid") == 0) return FWUPD_SECURITY_ATTR_RESULT_VALID; if (g_strcmp0(result, "not-valid") == 0) return FWUPD_SECURITY_ATTR_RESULT_NOT_VALID; if (g_strcmp0(result, "enabled") == 0) return FWUPD_SECURITY_ATTR_RESULT_ENABLED; if (g_strcmp0(result, "not-enabled") == 0) return FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED; if (g_strcmp0(result, "locked") == 0) return FWUPD_SECURITY_ATTR_RESULT_LOCKED; if (g_strcmp0(result, "not-locked") == 0) return FWUPD_SECURITY_ATTR_RESULT_NOT_LOCKED; if (g_strcmp0(result, "encrypted") == 0) return FWUPD_SECURITY_ATTR_RESULT_ENCRYPTED; if (g_strcmp0(result, "not-encrypted") == 0) return FWUPD_SECURITY_ATTR_RESULT_NOT_ENCRYPTED; if (g_strcmp0(result, "tainted") == 0) return FWUPD_SECURITY_ATTR_RESULT_TAINTED; if (g_strcmp0(result, "not-tainted") == 0) return FWUPD_SECURITY_ATTR_RESULT_NOT_TAINTED; if (g_strcmp0(result, "found") == 0) return FWUPD_SECURITY_ATTR_RESULT_FOUND; if (g_strcmp0(result, "not-found") == 0) return FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND; if (g_strcmp0(result, "supported") == 0) return FWUPD_SECURITY_ATTR_RESULT_SUPPORTED; if (g_strcmp0(result, "not-supported") == 0) return FWUPD_SECURITY_ATTR_RESULT_NOT_SUPPORTED; return FWUPD_SECURITY_ATTR_RESULT_UNKNOWN; } /** * fwupd_security_attr_flag_to_suffix: * @flag: security attribute flags, e.g. %FWUPD_SECURITY_ATTR_FLAG_RUNTIME_UPDATES * * Returns the string suffix for the flag. * * Returns: string, or %NULL * * Since: 1.5.0 **/ const gchar * fwupd_security_attr_flag_to_suffix(FwupdSecurityAttrFlags flag) { if (flag == FWUPD_SECURITY_ATTR_FLAG_RUNTIME_UPDATES) return "U"; if (flag == FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ATTESTATION) return "A"; if (flag == FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE) return "!"; return NULL; } /** * fwupd_security_attr_get_bios_setting_id: * @self: a #FwupdSecurityAttr * * Gets the #FwupdBiosSetting that can be used to improve this * #FwupdSecurityAttr. * * Returns: The unique ID used for #FwupdBiosSetting or NULL * * Since: 1.8.4 **/ const gchar * fwupd_security_attr_get_bios_setting_id(FwupdSecurityAttr *self) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), NULL); return priv->bios_setting_id; } /** * fwupd_security_attr_set_bios_setting_id: * @self: a #FwupdSecurityAttr * @id: (nullable): Unique identifier used for #FwupdBiosSetting * * Sets the #FwupdBiosSetting that can be used to improve this * #FwupdSecurityAttr. * * Since: 1.8.4 **/ void fwupd_security_attr_set_bios_setting_id(FwupdSecurityAttr *self, const gchar *id) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_SECURITY_ATTR(self)); if (priv->bios_setting_id == id) return; g_free(priv->bios_setting_id); priv->bios_setting_id = g_strdup(id); } /** * fwupd_security_attr_get_obsoletes: * @self: a #FwupdSecurityAttr * * Gets the list of attribute obsoletes. The obsoleted attributes will not * contribute to the calculated HSI value or be visible in command line tools. * * Returns: (element-type utf8) (transfer none): the obsoletes, which may be empty * * Since: 1.5.0 **/ GPtrArray * fwupd_security_attr_get_obsoletes(FwupdSecurityAttr *self) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), NULL); return priv->obsoletes; } /** * fwupd_security_attr_add_obsolete: * @self: a #FwupdSecurityAttr * @appstream_id: the appstream_id or plugin name * * Adds an attribute appstream_id to obsolete. The obsoleted attribute will not * contribute to the calculated HSI value or be visible in command line tools. * * Since: 1.5.0 **/ void fwupd_security_attr_add_obsolete(FwupdSecurityAttr *self, const gchar *appstream_id) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_SECURITY_ATTR(self)); g_return_if_fail(appstream_id != NULL); if (fwupd_security_attr_has_obsolete(self, appstream_id)) return; g_ptr_array_add(priv->obsoletes, g_strdup(appstream_id)); } /** * fwupd_security_attr_has_obsolete: * @self: a #FwupdSecurityAttr * @appstream_id: the attribute appstream_id * * Finds out if the attribute obsoletes a specific appstream_id. * * Returns: %TRUE if the self matches * * Since: 1.5.0 **/ gboolean fwupd_security_attr_has_obsolete(FwupdSecurityAttr *self, const gchar *appstream_id) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), FALSE); g_return_val_if_fail(appstream_id != NULL, FALSE); for (guint i = 0; i < priv->obsoletes->len; i++) { const gchar *obsolete_tmp = g_ptr_array_index(priv->obsoletes, i); if (g_strcmp0(obsolete_tmp, appstream_id) == 0) return TRUE; } return FALSE; } /** * fwupd_security_attr_get_guids: * @self: a #FwupdSecurityAttr * * Gets the list of attribute GUIDs. The GUID values will not modify the calculated HSI value. * * Returns: (element-type utf8) (transfer none): the GUIDs, which may be empty * * Since: 1.7.0 **/ GPtrArray * fwupd_security_attr_get_guids(FwupdSecurityAttr *self) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), NULL); return priv->guids; } /** * fwupd_security_attr_add_guid: * @self: a #FwupdSecurityAttr * @guid: (not nullable): the GUID * * Adds a device GUID to the attribute. This indicates the GUID in some way contributed to the * result decided. * * Since: 1.7.0 **/ void fwupd_security_attr_add_guid(FwupdSecurityAttr *self, const gchar *guid) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_SECURITY_ATTR(self)); g_return_if_fail(fwupd_guid_is_valid(guid)); if (fwupd_security_attr_has_guid(self, guid)) return; g_ptr_array_add(priv->guids, g_strdup(guid)); } /** * fwupd_security_attr_add_guids: * @self: a #FwupdSecurityAttr * @guids: (element-type utf8): the GUIDs * * Adds device GUIDs to the attribute. This indicates the GUIDs in some way contributed to the * result decided. * * Since: 1.7.0 **/ void fwupd_security_attr_add_guids(FwupdSecurityAttr *self, GPtrArray *guids) { g_return_if_fail(FWUPD_IS_SECURITY_ATTR(self)); g_return_if_fail(guids != NULL); for (guint i = 0; i < guids->len; i++) { const gchar *guid = g_ptr_array_index(guids, i); fwupd_security_attr_add_guid(self, guid); } } /** * fwupd_security_attr_has_guid: * @self: a #FwupdSecurityAttr * @guid: the attribute guid * * Finds out if a specific GUID was added to the attribute. * * Returns: %TRUE if the self matches * * Since: 1.7.0 **/ gboolean fwupd_security_attr_has_guid(FwupdSecurityAttr *self, const gchar *guid) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), FALSE); g_return_val_if_fail(guid != NULL, FALSE); for (guint i = 0; i < priv->guids->len; i++) { const gchar *guid_tmp = g_ptr_array_index(priv->guids, i); if (g_strcmp0(guid_tmp, guid) == 0) return TRUE; } return FALSE; } /** * fwupd_security_attr_get_appstream_id: * @self: a #FwupdSecurityAttr * * Gets the AppStream ID. * * Returns: the AppStream ID, or %NULL if unset * * Since: 1.5.0 **/ const gchar * fwupd_security_attr_get_appstream_id(FwupdSecurityAttr *self) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), NULL); return priv->appstream_id; } /** * fwupd_security_attr_set_appstream_id: * @self: a #FwupdSecurityAttr * @appstream_id: (nullable): the AppStream component ID, e.g. `com.intel.BiosGuard` * * Sets the AppStream ID. * * Since: 1.5.0 **/ void fwupd_security_attr_set_appstream_id(FwupdSecurityAttr *self, const gchar *appstream_id) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_SECURITY_ATTR(self)); /* not changed */ if (g_strcmp0(priv->appstream_id, appstream_id) == 0) return; /* sanity check */ if (appstream_id != NULL && !g_str_has_prefix(appstream_id, "org.fwupd.hsi.")) g_critical("HSI attributes need to have a 'org.fwupd.hsi.' prefix"); g_free(priv->appstream_id); priv->appstream_id = g_strdup(appstream_id); } /** * fwupd_security_attr_get_url: * @self: a #FwupdSecurityAttr * * Gets the attribute URL. * * Returns: the attribute result, or %NULL if unset * * Since: 1.5.0 **/ const gchar * fwupd_security_attr_get_url(FwupdSecurityAttr *self) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), NULL); return priv->url; } /** * fwupd_security_attr_set_name: * @self: a #FwupdSecurityAttr * @name: (nullable): the attribute name * * Sets the attribute name. * * Since: 1.5.0 **/ void fwupd_security_attr_set_name(FwupdSecurityAttr *self, const gchar *name) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_SECURITY_ATTR(self)); /* not changed */ if (g_strcmp0(priv->name, name) == 0) return; g_free(priv->name); priv->name = g_strdup(name); } /** * fwupd_security_attr_get_bios_setting_target_value: * @self: a #FwupdSecurityAttr * * Gets the value that when written to an attribute would activate it or satisfy * a security requirement. * * Returns: the target value of the attribute. * * Since: 1.8.4 **/ const gchar * fwupd_security_attr_get_bios_setting_target_value(FwupdSecurityAttr *self) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), NULL); return priv->bios_setting_target_value; } /** * fwupd_security_attr_set_bios_setting_target_value: * @self: a #FwupdSecurityAttr * @value: (nullable): The string to set target value to * * Sets the string used for the target value of an attribute. * * Since: 1.8.4 **/ void fwupd_security_attr_set_bios_setting_target_value(FwupdSecurityAttr *self, const gchar *value) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); /* not changed */ if (g_strcmp0(priv->bios_setting_target_value, value) == 0) return; g_free(priv->bios_setting_target_value); priv->bios_setting_target_value = g_strdup(value); } /** * fwupd_security_attr_get_bios_setting_current_value: * @self: a #FwupdSecurityAttr * * Gets the current value of the BIOS setting that can be changed. * * Returns: the current value of the attribute. * * Since: 1.8.4 **/ const gchar * fwupd_security_attr_get_bios_setting_current_value(FwupdSecurityAttr *self) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), NULL); return priv->bios_setting_current_value; } /** * fwupd_security_attr_set_bios_setting_current_value: * @self: a #FwupdSecurityAttr * @value: (nullable): The string to set current value to * * Sets the current value of the BIOS setting that can be changed. * * Since: 1.8.4 **/ void fwupd_security_attr_set_bios_setting_current_value(FwupdSecurityAttr *self, const gchar *value) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); /* not changed */ if (g_strcmp0(priv->bios_setting_current_value, value) == 0) return; g_free(priv->bios_setting_current_value); priv->bios_setting_current_value = g_strdup(value); } /** * fwupd_security_attr_get_kernel_current_value: * @self: a #FwupdSecurityAttr * * Gets the current value of the BIOS setting that can be changed. * * Returns: the current value of the attribute. * * Since: 1.9.6 **/ const gchar * fwupd_security_attr_get_kernel_current_value(FwupdSecurityAttr *self) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), NULL); return priv->kernel_current_value; } /** * fwupd_security_attr_set_kernel_current_value: * @self: a #FwupdSecurityAttr * @value: (nullable): The string to set current value to * * Sets the current value of the BIOS setting that can be changed. * * Since: 1.9.6 **/ void fwupd_security_attr_set_kernel_current_value(FwupdSecurityAttr *self, const gchar *value) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); /* not changed */ if (g_strcmp0(priv->kernel_current_value, value) == 0) return; g_free(priv->kernel_current_value); priv->kernel_current_value = g_strdup(value); } /** * fwupd_security_attr_get_kernel_target_value: * @self: a #FwupdSecurityAttr * * Gets the target value of the kernel setting that can be changed. * * Returns: the current value of the attribute. * * Since: 1.9.6 **/ const gchar * fwupd_security_attr_get_kernel_target_value(FwupdSecurityAttr *self) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), NULL); return priv->kernel_target_value; } /** * fwupd_security_attr_set_kernel_target_value: * @self: a #FwupdSecurityAttr * @value: (nullable): The string to set current value to * * Sets the target value of the kernel setting that can be changed. * * Since: 1.9.6 **/ void fwupd_security_attr_set_kernel_target_value(FwupdSecurityAttr *self, const gchar *value) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); /* not changed */ if (g_strcmp0(priv->kernel_target_value, value) == 0) return; g_free(priv->kernel_target_value); priv->kernel_target_value = g_strdup(value); } /** * fwupd_security_attr_set_title: * @self: a #FwupdSecurityAttr * @title: (nullable): the attribute title * * Sets the attribute title. * * Since: 1.8.2 **/ void fwupd_security_attr_set_title(FwupdSecurityAttr *self, const gchar *title) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_SECURITY_ATTR(self)); /* not changed */ if (g_strcmp0(priv->title, title) == 0) return; g_free(priv->title); priv->title = g_strdup(title); } /** * fwupd_security_attr_set_description: * @self: a #FwupdSecurityAttr * @description: (nullable): the attribute description * * Sets the attribute description. * * Since: 1.8.2 **/ void fwupd_security_attr_set_description(FwupdSecurityAttr *self, const gchar *description) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_SECURITY_ATTR(self)); /* not changed */ if (g_strcmp0(priv->description, description) == 0) return; g_free(priv->description); priv->description = g_strdup(description); } /** * fwupd_security_attr_set_plugin: * @self: a #FwupdSecurityAttr * @plugin: (nullable): the plugin name * * Sets the plugin that created the attribute. * * Since: 1.5.0 **/ void fwupd_security_attr_set_plugin(FwupdSecurityAttr *self, const gchar *plugin) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_SECURITY_ATTR(self)); /* not changed */ if (g_strcmp0(priv->plugin, plugin) == 0) return; g_free(priv->plugin); priv->plugin = g_strdup(plugin); } /** * fwupd_security_attr_set_fwupd_version: * @self: a #FwupdSecurityAttr * @fwupd_version: (nullable): the fwupd version, e.g. `2.0.7` * * Sets the fwupd version the attribute was added. * * Since: 2.0.7 **/ void fwupd_security_attr_set_fwupd_version(FwupdSecurityAttr *self, const gchar *fwupd_version) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_SECURITY_ATTR(self)); /* not changed */ if (g_strcmp0(priv->fwupd_version, fwupd_version) == 0) return; g_free(priv->fwupd_version); priv->fwupd_version = g_strdup(fwupd_version); } /** * fwupd_security_attr_set_url: * @self: a #FwupdSecurityAttr * @url: (nullable): the attribute URL * * Sets the attribute result. * * Since: 1.5.0 **/ void fwupd_security_attr_set_url(FwupdSecurityAttr *self, const gchar *url) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_SECURITY_ATTR(self)); /* not changed */ if (g_strcmp0(priv->url, url) == 0) return; g_free(priv->url); priv->url = g_strdup(url); } /** * fwupd_security_attr_get_created: * @self: a #FwupdSecurityAttr * * Gets when the attribute was created. * * Returns: the UNIX time, or 0 if unset * * Since: 1.7.1 **/ guint64 fwupd_security_attr_get_created(FwupdSecurityAttr *self) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), 0); return priv->created; } /** * fwupd_security_attr_set_created: * @self: a #FwupdSecurityAttr * @created: the UNIX time * * Sets when the attribute was created. * * Since: 1.7.1 **/ void fwupd_security_attr_set_created(FwupdSecurityAttr *self, guint64 created) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_SECURITY_ATTR(self)); priv->created = created; } /** * fwupd_security_attr_get_name: * @self: a #FwupdSecurityAttr * * Gets the attribute name. * * Returns: the attribute name, or %NULL if unset * * Since: 1.5.0 **/ const gchar * fwupd_security_attr_get_name(FwupdSecurityAttr *self) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), NULL); return priv->name; } /** * fwupd_security_attr_get_title: * @self: a #FwupdSecurityAttr * * Gets the attribute title, which is typically a two word title. * * The fwupd client program may be able to get translations for this value using a method call * like `dgettext("fwupd",str)`. * * Returns: the attribute title, or %NULL if unset * * Since: 1.8.2 **/ const gchar * fwupd_security_attr_get_title(FwupdSecurityAttr *self) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), NULL); return priv->title; } /** * fwupd_security_attr_get_description: * @self: a #FwupdSecurityAttr * * Gets the attribute description which is a few lines of prose that normal users will understand. * * The fwupd client program may be able to get translations for this value using a method call * like `dgettext("fwupd",str)`. * * NOTE: The returned string may contain placeholders such as `$HostVendor$` or `$HostProduct$` * and these should be replaced with the values from [method@FwupdClient.get_host_vendor] and * [method@FwupdClient.get_host_product]. * * Returns: the attribute description, or %NULL if unset * * Since: 1.8.2 **/ const gchar * fwupd_security_attr_get_description(FwupdSecurityAttr *self) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), NULL); return priv->description; } /** * fwupd_security_attr_get_plugin: * @self: a #FwupdSecurityAttr * * Gets the plugin that created the attribute. * * Returns: the plugin name, or %NULL if unset * * Since: 1.5.0 **/ const gchar * fwupd_security_attr_get_plugin(FwupdSecurityAttr *self) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), NULL); return priv->plugin; } /** * fwupd_security_attr_get_fwupd_version: * @self: a #FwupdSecurityAttr * * Gets the fwupd version the attribute was added. * * Returns: the fwupd version, or %NULL if unset * * Since: 2.0.7 **/ const gchar * fwupd_security_attr_get_fwupd_version(FwupdSecurityAttr *self) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), NULL); return priv->fwupd_version; } /** * fwupd_security_attr_get_flags: * @self: a #FwupdSecurityAttr * * Gets the self flags. * * Returns: security attribute flags, or 0 if unset * * Since: 1.5.0 **/ FwupdSecurityAttrFlags fwupd_security_attr_get_flags(FwupdSecurityAttr *self) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), 0); return priv->flags; } /** * fwupd_security_attr_set_flags: * @self: a #FwupdSecurityAttr * @flags: security attribute flags, e.g. %FWUPD_SECURITY_ATTR_FLAG_OBSOLETED * * Sets the attribute flags. * * Since: 1.5.0 **/ void fwupd_security_attr_set_flags(FwupdSecurityAttr *self, FwupdSecurityAttrFlags flags) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_SECURITY_ATTR(self)); priv->flags = flags; } /* copy over the success result if not already set */ static void fwupd_security_attr_ensure_result(FwupdSecurityAttr *self) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); if (!fwupd_security_attr_has_flag(self, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)) return; if (fwupd_security_attr_has_flag(self, FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA)) fwupd_security_attr_remove_flag(self, FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA); if (priv->result == FWUPD_SECURITY_ATTR_RESULT_UNKNOWN && priv->result_success != FWUPD_SECURITY_ATTR_RESULT_UNKNOWN) { g_debug("auto-setting %s result %s", priv->appstream_id, fwupd_security_attr_result_to_string(priv->result_success)); priv->result = priv->result_success; } } /** * fwupd_security_attr_add_flag: * @self: a #FwupdSecurityAttr * @flag: the #FwupdSecurityAttrFlags, e.g. %FWUPD_SECURITY_ATTR_FLAG_OBSOLETED * * Adds a specific attribute flag to the attribute. * * Since: 1.5.0 **/ void fwupd_security_attr_add_flag(FwupdSecurityAttr *self, FwupdSecurityAttrFlags flag) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_SECURITY_ATTR(self)); priv->flags |= flag; fwupd_security_attr_ensure_result(self); } /** * fwupd_security_attr_remove_flag: * @self: a #FwupdSecurityAttr * @flag: the #FwupdSecurityAttrFlags, e.g. %FWUPD_SECURITY_ATTR_FLAG_OBSOLETED * * Removes a specific attribute flag from the attribute. * * Since: 1.8.3 **/ void fwupd_security_attr_remove_flag(FwupdSecurityAttr *self, FwupdSecurityAttrFlags flag) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_SECURITY_ATTR(self)); priv->flags &= ~flag; } /** * fwupd_security_attr_has_flag: * @self: a #FwupdSecurityAttr * @flag: the attribute flag, e.g. %FWUPD_SECURITY_ATTR_FLAG_OBSOLETED * * Finds if the attribute has a specific attribute flag. * * Returns: %TRUE if the flag is set * * Since: 1.5.0 **/ gboolean fwupd_security_attr_has_flag(FwupdSecurityAttr *self, FwupdSecurityAttrFlags flag) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), FALSE); return (priv->flags & flag) > 0; } /** * fwupd_security_attr_get_level: * @self: a #FwupdSecurityAttr * * Gets the HSI level. * * Returns: the security attribute level, or %FWUPD_SECURITY_ATTR_LEVEL_NONE if unset * * Since: 1.5.0 **/ FwupdSecurityAttrLevel fwupd_security_attr_get_level(FwupdSecurityAttr *self) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), 0); return priv->level; } /** * fwupd_security_attr_set_level: * @self: a #FwupdSecurityAttr * @level: a security attribute level, e.g. %FWUPD_SECURITY_ATTR_LEVEL_IMPORTANT * * Sets the HSI level. A @level of %FWUPD_SECURITY_ATTR_LEVEL_NONE is not used * for the HSI calculation. * * Since: 1.5.0 **/ void fwupd_security_attr_set_level(FwupdSecurityAttr *self, FwupdSecurityAttrLevel level) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_SECURITY_ATTR(self)); priv->level = level; } /** * fwupd_security_attr_set_result: * @self: a #FwupdSecurityAttr * @result: a security attribute result, e.g. %FWUPD_SECURITY_ATTR_LEVEL_LOCKED * * Sets the optional HSI result. This is required because some attributes may * be a "success" when something is `locked` or may be "failed" if `found`. * * Since: 1.5.0 **/ void fwupd_security_attr_set_result(FwupdSecurityAttr *self, FwupdSecurityAttrResult result) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_SECURITY_ATTR(self)); /* fixup any legacy attributes to the 'modern' value */ if (g_strcmp0(priv->appstream_id, FWUPD_SECURITY_ATTR_ID_ENCRYPTED_RAM) == 0 && result == FWUPD_SECURITY_ATTR_RESULT_NOT_ENCRYPTED) { result = FWUPD_SECURITY_ATTR_RESULT_NOT_SUPPORTED; } priv->result = result; } /** * fwupd_security_attr_get_result: * @self: a #FwupdSecurityAttr * * Gets the optional HSI result. * * Returns: the #FwupdSecurityAttrResult, e.g %FWUPD_SECURITY_ATTR_LEVEL_LOCKED * * Since: 1.5.0 **/ FwupdSecurityAttrResult fwupd_security_attr_get_result(FwupdSecurityAttr *self) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), 0); return priv->result; } /** * fwupd_security_attr_set_result_fallback: * @self: a #FwupdSecurityAttr * @result: a security attribute, e.g. %FWUPD_SECURITY_ATTR_LEVEL_LOCKED * * Sets the optional fallback HSI result. The fallback may represent the old state, or a state * that may be considered equivalent. * * Since: 1.7.1 **/ void fwupd_security_attr_set_result_fallback(FwupdSecurityAttr *self, FwupdSecurityAttrResult result) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_SECURITY_ATTR(self)); priv->result_fallback = result; } /** * fwupd_security_attr_get_result_fallback: * @self: a #FwupdSecurityAttr * * Gets the optional fallback HSI result. * * Returns: the #FwupdSecurityAttrResult, e.g %FWUPD_SECURITY_ATTR_LEVEL_LOCKED * * Since: 1.7.1 **/ FwupdSecurityAttrResult fwupd_security_attr_get_result_fallback(FwupdSecurityAttr *self) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), 0); return priv->result_fallback; } /** * fwupd_security_attr_set_result_success: * @self: a #FwupdSecurityAttr * @result: a security attribute, e.g. %FWUPD_SECURITY_ATTR_LEVEL_LOCKED * * Sets the desired HSI result. * * Since: 1.9.3 **/ void fwupd_security_attr_set_result_success(FwupdSecurityAttr *self, FwupdSecurityAttrResult result) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_SECURITY_ATTR(self)); priv->result_success = result; fwupd_security_attr_ensure_result(self); } /** * fwupd_security_attr_get_result_success: * @self: a #FwupdSecurityAttr * * Gets the desired HSI result. * * Returns: the #FwupdSecurityAttrResult, e.g %FWUPD_SECURITY_ATTR_LEVEL_LOCKED * * Since: 1.9.3 **/ FwupdSecurityAttrResult fwupd_security_attr_get_result_success(FwupdSecurityAttr *self) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), 0); return priv->result_success; } static void fwupd_security_attr_add_variant(FwupdCodec *codec, GVariantBuilder *builder, FwupdCodecFlags flags) { FwupdSecurityAttr *self = FWUPD_SECURITY_ATTR(codec); FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); if (priv->appstream_id != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_APPSTREAM_ID, g_variant_new_string(priv->appstream_id)); } if (priv->created > 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_CREATED, g_variant_new_uint64(priv->created)); } if (priv->name != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_NAME, g_variant_new_string(priv->name)); } if (priv->title != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_SUMMARY, g_variant_new_string(priv->title)); } if (priv->description != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_DESCRIPTION, g_variant_new_string(priv->description)); } if (priv->plugin != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_PLUGIN, g_variant_new_string(priv->plugin)); } if (priv->fwupd_version != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_VERSION, g_variant_new_string(priv->fwupd_version)); } if (priv->url != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_URI, g_variant_new_string(priv->url)); } if (priv->obsoletes->len > 0) { g_autofree const gchar **strv = g_new0(const gchar *, priv->obsoletes->len + 1); for (guint i = 0; i < priv->obsoletes->len; i++) strv[i] = (const gchar *)g_ptr_array_index(priv->obsoletes, i); g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_CATEGORIES, g_variant_new_strv(strv, -1)); } if (priv->guids->len > 0) { g_autofree const gchar **strv = g_new0(const gchar *, priv->guids->len + 1); for (guint i = 0; i < priv->guids->len; i++) strv[i] = (const gchar *)g_ptr_array_index(priv->guids, i); g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_GUID, g_variant_new_strv(strv, -1)); } if (priv->flags != 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_FLAGS, g_variant_new_uint64(priv->flags)); } if (priv->level > 0) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_HSI_LEVEL, g_variant_new_uint32(priv->level)); } if (priv->result != FWUPD_SECURITY_ATTR_RESULT_UNKNOWN) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_HSI_RESULT, g_variant_new_uint32(priv->result)); } if (priv->result_fallback != FWUPD_SECURITY_ATTR_RESULT_UNKNOWN) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_HSI_RESULT_FALLBACK, g_variant_new_uint32(priv->result_fallback)); } if (priv->result_success != FWUPD_SECURITY_ATTR_RESULT_UNKNOWN) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_HSI_RESULT_SUCCESS, g_variant_new_uint32(priv->result_success)); } if (priv->metadata != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_METADATA, fwupd_hash_kv_to_variant(priv->metadata)); } if (priv->bios_setting_id != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_BIOS_SETTING_ID, g_variant_new_string(priv->bios_setting_id)); } if (priv->bios_setting_target_value != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_BIOS_SETTING_TARGET_VALUE, g_variant_new_string(priv->bios_setting_target_value)); } if (priv->bios_setting_current_value != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_BIOS_SETTING_CURRENT_VALUE, g_variant_new_string(priv->bios_setting_current_value)); } if (priv->kernel_current_value != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_KERNEL_CURRENT_VALUE, g_variant_new_string(priv->kernel_current_value)); } if (priv->kernel_target_value != NULL) { g_variant_builder_add(builder, "{sv}", FWUPD_RESULT_KEY_KERNEL_TARGET_VALUE, g_variant_new_string(priv->kernel_target_value)); } } /** * fwupd_security_attr_get_metadata: * @self: a #FwupdSecurityAttr * @key: metadata key * * Gets private metadata from the attribute which may be used in the name. * * Returns: (nullable): the metadata value, or %NULL if unfound * * Since: 1.5.0 **/ const gchar * fwupd_security_attr_get_metadata(FwupdSecurityAttr *self, const gchar *key) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), NULL); g_return_val_if_fail(key != NULL, NULL); if (priv->metadata == NULL) return NULL; return g_hash_table_lookup(priv->metadata, key); } /** * fwupd_security_attr_add_metadata: * @self: a #FwupdSecurityAttr * @key: metadata key * @value: (nullable): metadata value * * Adds metadata to the attribute which may be used in the name. * * Since: 1.5.0 **/ void fwupd_security_attr_add_metadata(FwupdSecurityAttr *self, const gchar *key, const gchar *value) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FWUPD_IS_SECURITY_ATTR(self)); g_return_if_fail(key != NULL); if (priv->metadata == NULL) { priv->metadata = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); } g_hash_table_insert(priv->metadata, g_strdup(key), g_strdup(value)); } static void fwupd_security_attr_from_key_value(FwupdSecurityAttr *self, const gchar *key, GVariant *value) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); if (g_strcmp0(key, FWUPD_RESULT_KEY_APPSTREAM_ID) == 0) { fwupd_security_attr_set_appstream_id(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_CREATED) == 0) { fwupd_security_attr_set_created(self, g_variant_get_uint64(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_NAME) == 0) { fwupd_security_attr_set_name(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_SUMMARY) == 0) { fwupd_security_attr_set_title(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_DESCRIPTION) == 0) { fwupd_security_attr_set_description(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_PLUGIN) == 0) { fwupd_security_attr_set_plugin(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_VERSION) == 0) { fwupd_security_attr_set_fwupd_version(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_URI) == 0) { fwupd_security_attr_set_url(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_FLAGS) == 0) { fwupd_security_attr_set_flags(self, g_variant_get_uint64(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_HSI_LEVEL) == 0) { fwupd_security_attr_set_level(self, g_variant_get_uint32(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_HSI_RESULT) == 0) { fwupd_security_attr_set_result(self, g_variant_get_uint32(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_HSI_RESULT_FALLBACK) == 0) { fwupd_security_attr_set_result_fallback(self, g_variant_get_uint32(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_HSI_RESULT_SUCCESS) == 0) { fwupd_security_attr_set_result_success(self, g_variant_get_uint32(value)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_GUID) == 0) { g_autofree const gchar **strv = g_variant_get_strv(value, NULL); for (guint i = 0; strv[i] != NULL; i++) fwupd_security_attr_add_guid(self, strv[i]); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_METADATA) == 0) { if (priv->metadata != NULL) g_hash_table_unref(priv->metadata); priv->metadata = fwupd_variant_to_hash_kv(value); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_BIOS_SETTING_ID) == 0) { fwupd_security_attr_set_bios_setting_id(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_BIOS_SETTING_TARGET_VALUE) == 0) { fwupd_security_attr_set_bios_setting_target_value( self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_BIOS_SETTING_CURRENT_VALUE) == 0) { fwupd_security_attr_set_bios_setting_current_value( self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_KERNEL_CURRENT_VALUE) == 0) { fwupd_security_attr_set_kernel_current_value(self, g_variant_get_string(value, NULL)); return; } if (g_strcmp0(key, FWUPD_RESULT_KEY_KERNEL_TARGET_VALUE) == 0) { fwupd_security_attr_set_kernel_target_value(self, g_variant_get_string(value, NULL)); return; } } static void fwupd_security_attr_string_append_tfl(GString *str, guint idt, const gchar *key, FwupdSecurityAttrFlags security_attr_flags) { g_autoptr(GString) tmp = g_string_new(""); for (guint i = 0; i < 64; i++) { if ((security_attr_flags & ((guint64)1 << i)) == 0) continue; g_string_append_printf(tmp, "%s|", fwupd_security_attr_flag_to_string((guint64)1 << i)); } if (tmp->len == 0) { g_string_append(tmp, fwupd_security_attr_flag_to_string(0)); } else { g_string_truncate(tmp, tmp->len - 1); } fwupd_codec_string_append(str, idt, key, tmp->str); } static gboolean fwupd_security_attr_from_json(FwupdCodec *codec, JsonNode *json_node, GError **error) { FwupdSecurityAttr *self = FWUPD_SECURITY_ATTR(codec); JsonObject *obj; /* sanity check */ if (!JSON_NODE_HOLDS_OBJECT(json_node)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not JSON object"); return FALSE; } obj = json_node_get_object(json_node); /* this has to exist */ if (!json_object_has_member(obj, FWUPD_RESULT_KEY_APPSTREAM_ID)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no %s property in object", FWUPD_RESULT_KEY_APPSTREAM_ID); return FALSE; } /* all optional */ fwupd_security_attr_set_appstream_id( self, json_object_get_string_member(obj, FWUPD_RESULT_KEY_APPSTREAM_ID)); fwupd_security_attr_set_name( self, json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_NAME, NULL)); fwupd_security_attr_set_title( self, json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_SUMMARY, NULL)); fwupd_security_attr_set_description( self, json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_DESCRIPTION, NULL)); fwupd_security_attr_set_plugin( self, json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_PLUGIN, NULL)); fwupd_security_attr_set_fwupd_version( self, json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_VERSION, NULL)); fwupd_security_attr_set_url( self, json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_URI, NULL)); fwupd_security_attr_set_level( self, json_object_get_int_member_with_default(obj, FWUPD_RESULT_KEY_HSI_LEVEL, 0)); fwupd_security_attr_set_created( self, json_object_get_int_member_with_default(obj, FWUPD_RESULT_KEY_CREATED, 0)); fwupd_security_attr_set_bios_setting_id( self, json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_BIOS_SETTING_ID, NULL)); fwupd_security_attr_set_bios_setting_target_value( self, json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_BIOS_SETTING_TARGET_VALUE, NULL)); fwupd_security_attr_set_bios_setting_current_value( self, json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_BIOS_SETTING_CURRENT_VALUE, NULL)); fwupd_security_attr_set_kernel_current_value( self, json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_KERNEL_CURRENT_VALUE, NULL)); fwupd_security_attr_set_kernel_target_value( self, json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_KERNEL_TARGET_VALUE, NULL)); /* also optional */ if (json_object_has_member(obj, FWUPD_RESULT_KEY_HSI_RESULT)) { const gchar *tmp = json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_HSI_RESULT, NULL); fwupd_security_attr_set_result(self, fwupd_security_attr_result_from_string(tmp)); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_HSI_RESULT_FALLBACK)) { const gchar *tmp = json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_HSI_RESULT_FALLBACK, NULL); fwupd_security_attr_set_result_fallback( self, fwupd_security_attr_result_from_string(tmp)); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_HSI_RESULT_SUCCESS)) { const gchar *tmp = json_object_get_string_member_with_default(obj, FWUPD_RESULT_KEY_HSI_RESULT_SUCCESS, NULL); fwupd_security_attr_set_result_success(self, fwupd_security_attr_result_from_string(tmp)); } if (json_object_has_member(obj, FWUPD_RESULT_KEY_FLAGS)) { JsonArray *array = json_object_get_array_member(obj, FWUPD_RESULT_KEY_FLAGS); for (guint i = 0; i < json_array_get_length(array); i++) { const gchar *tmp = json_array_get_string_element(array, i); FwupdSecurityAttrFlags flag = fwupd_security_attr_flag_from_string(tmp); if (flag != FWUPD_SECURITY_ATTR_FLAG_NONE) fwupd_security_attr_add_flag(self, flag); } } if (json_object_has_member(obj, FWUPD_RESULT_KEY_GUID)) { JsonArray *array = json_object_get_array_member(obj, FWUPD_RESULT_KEY_GUID); for (guint i = 0; i < json_array_get_length(array); i++) { const gchar *tmp = json_array_get_string_element(array, i); fwupd_security_attr_add_guid(self, tmp); } } /* success */ return TRUE; } static void fwupd_security_attr_add_json(FwupdCodec *codec, JsonBuilder *builder, FwupdCodecFlags flags) { FwupdSecurityAttr *self = FWUPD_SECURITY_ATTR(codec); FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_APPSTREAM_ID, priv->appstream_id); if (priv->created > 0) fwupd_codec_json_append_int(builder, FWUPD_RESULT_KEY_CREATED, priv->created); fwupd_codec_json_append_int(builder, FWUPD_RESULT_KEY_HSI_LEVEL, priv->level); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_HSI_RESULT, fwupd_security_attr_result_to_string(priv->result)); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_HSI_RESULT_FALLBACK, fwupd_security_attr_result_to_string(priv->result_fallback)); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_HSI_RESULT_SUCCESS, fwupd_security_attr_result_to_string(priv->result_success)); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_NAME, priv->name); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_SUMMARY, priv->title); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_DESCRIPTION, priv->description); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_PLUGIN, priv->plugin); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_VERSION, priv->fwupd_version); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_URI, priv->url); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_BIOS_SETTING_TARGET_VALUE, priv->bios_setting_target_value); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_BIOS_SETTING_CURRENT_VALUE, priv->bios_setting_current_value); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_BIOS_SETTING_ID, priv->bios_setting_id); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_KERNEL_CURRENT_VALUE, priv->kernel_current_value); fwupd_codec_json_append(builder, FWUPD_RESULT_KEY_KERNEL_TARGET_VALUE, priv->kernel_target_value); if (priv->flags != FWUPD_SECURITY_ATTR_FLAG_NONE) { json_builder_set_member_name(builder, FWUPD_RESULT_KEY_FLAGS); json_builder_begin_array(builder); for (guint i = 0; i < 64; i++) { const gchar *tmp; if ((priv->flags & ((guint64)1 << i)) == 0) continue; tmp = fwupd_security_attr_flag_to_string((guint64)1 << i); json_builder_add_string_value(builder, tmp); } json_builder_end_array(builder); } if (priv->guids->len > 0) { json_builder_set_member_name(builder, FWUPD_RESULT_KEY_GUID); json_builder_begin_array(builder); for (guint i = 0; i < priv->guids->len; i++) { const gchar *guid = g_ptr_array_index(priv->guids, i); json_builder_add_string_value(builder, guid); } json_builder_end_array(builder); } if (priv->metadata != NULL) { g_autoptr(GList) keys = g_hash_table_get_keys(priv->metadata); for (GList *l = keys; l != NULL; l = l->next) { const gchar *key = l->data; const gchar *value = g_hash_table_lookup(priv->metadata, key); fwupd_codec_json_append(builder, key, value); } } } static void fwupd_security_attr_add_string(FwupdCodec *codec, guint idt, GString *str) { FwupdSecurityAttr *self = FWUPD_SECURITY_ATTR(codec); FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_APPSTREAM_ID, priv->appstream_id); fwupd_codec_string_append_time(str, idt, FWUPD_RESULT_KEY_CREATED, priv->created); fwupd_codec_string_append_int(str, idt, FWUPD_RESULT_KEY_HSI_LEVEL, priv->level); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_HSI_RESULT, fwupd_security_attr_result_to_string(priv->result)); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_HSI_RESULT_FALLBACK, fwupd_security_attr_result_to_string(priv->result_fallback)); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_HSI_RESULT_SUCCESS, fwupd_security_attr_result_to_string(priv->result_success)); if (priv->flags != FWUPD_SECURITY_ATTR_FLAG_NONE) fwupd_security_attr_string_append_tfl(str, idt, FWUPD_RESULT_KEY_FLAGS, priv->flags); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_NAME, priv->name); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_SUMMARY, priv->title); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_DESCRIPTION, priv->description); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_PLUGIN, priv->plugin); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_VERSION, priv->fwupd_version); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_URI, priv->url); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_BIOS_SETTING_ID, priv->bios_setting_id); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_BIOS_SETTING_TARGET_VALUE, priv->bios_setting_target_value); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_BIOS_SETTING_CURRENT_VALUE, priv->bios_setting_current_value); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_KERNEL_CURRENT_VALUE, priv->kernel_current_value); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_KERNEL_TARGET_VALUE, priv->kernel_target_value); for (guint i = 0; i < priv->obsoletes->len; i++) { const gchar *appstream_id = g_ptr_array_index(priv->obsoletes, i); fwupd_codec_string_append(str, idt, "Obsolete", appstream_id); } for (guint i = 0; i < priv->guids->len; i++) { const gchar *guid = g_ptr_array_index(priv->guids, i); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_GUID, guid); } if (priv->metadata != NULL) { g_autoptr(GList) keys = g_list_sort(g_hash_table_get_keys(priv->metadata), (GCompareFunc)g_strcmp0); for (GList *l = keys; l != NULL; l = l->next) { const gchar *key = l->data; const gchar *value = g_hash_table_lookup(priv->metadata, key); fwupd_codec_string_append(str, idt, key, value); } } } static void fwupd_security_attr_class_init(FwupdSecurityAttrClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fwupd_security_attr_finalize; } static void fwupd_security_attr_init(FwupdSecurityAttr *self) { FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); priv->level = FWUPD_SECURITY_ATTR_LEVEL_NONE; priv->obsoletes = g_ptr_array_new_with_free_func(g_free); priv->guids = g_ptr_array_new_with_free_func(g_free); priv->created = (guint64)g_get_real_time() / G_USEC_PER_SEC; } static void fwupd_security_attr_finalize(GObject *object) { FwupdSecurityAttr *self = FWUPD_SECURITY_ATTR(object); FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); if (priv->metadata != NULL) g_hash_table_unref(priv->metadata); g_free(priv->bios_setting_id); g_free(priv->bios_setting_target_value); g_free(priv->bios_setting_current_value); g_free(priv->kernel_current_value); g_free(priv->kernel_target_value); g_free(priv->appstream_id); g_free(priv->name); g_free(priv->title); g_free(priv->description); g_free(priv->plugin); g_free(priv->fwupd_version); g_free(priv->url); g_ptr_array_unref(priv->obsoletes); g_ptr_array_unref(priv->guids); G_OBJECT_CLASS(fwupd_security_attr_parent_class)->finalize(object); } static void fwupd_security_attr_from_variant_iter(FwupdCodec *codec, GVariantIter *iter) { FwupdSecurityAttr *self = FWUPD_SECURITY_ATTR(codec); GVariant *value; const gchar *key; while (g_variant_iter_next(iter, "{&sv}", &key, &value)) { fwupd_security_attr_from_key_value(self, key, value); g_variant_unref(value); } } static void fwupd_security_attr_codec_iface_init(FwupdCodecInterface *iface) { iface->add_string = fwupd_security_attr_add_string; iface->add_json = fwupd_security_attr_add_json; iface->from_json = fwupd_security_attr_from_json; iface->add_variant = fwupd_security_attr_add_variant; iface->from_variant_iter = fwupd_security_attr_from_variant_iter; } /** * fwupd_security_attr_copy: * @self: (nullable): a #FwupdSecurityAttr * * Makes a full (deep) copy of a security attribute. * * Returns: (transfer full): a new #FwupdSecurityAttr * * Since: 1.7.1 **/ FwupdSecurityAttr * fwupd_security_attr_copy(FwupdSecurityAttr *self) { g_autoptr(FwupdSecurityAttr) new = g_object_new(FWUPD_TYPE_SECURITY_ATTR, NULL); FwupdSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(self), NULL); fwupd_security_attr_set_appstream_id(new, priv->appstream_id); fwupd_security_attr_set_name(new, priv->name); fwupd_security_attr_set_title(new, priv->title); fwupd_security_attr_set_description(new, priv->description); fwupd_security_attr_set_plugin(new, priv->plugin); fwupd_security_attr_set_fwupd_version(new, priv->fwupd_version); fwupd_security_attr_set_url(new, priv->url); fwupd_security_attr_set_level(new, priv->level); fwupd_security_attr_set_flags(new, priv->flags); fwupd_security_attr_set_result(new, priv->result); fwupd_security_attr_set_created(new, priv->created); fwupd_security_attr_set_bios_setting_id(new, priv->bios_setting_id); for (guint i = 0; i < priv->guids->len; i++) { const gchar *guid = g_ptr_array_index(priv->guids, i); fwupd_security_attr_add_guid(new, guid); } for (guint i = 0; i < priv->obsoletes->len; i++) { const gchar *obsolete = g_ptr_array_index(priv->obsoletes, i); fwupd_security_attr_add_obsolete(new, obsolete); } if (priv->metadata != NULL) { GHashTableIter iter; gpointer key, value; g_hash_table_iter_init(&iter, priv->metadata); while (g_hash_table_iter_next(&iter, &key, &value)) { fwupd_security_attr_add_metadata(new, (const gchar *)key, (const gchar *)value); } } return g_steal_pointer(&new); } /** * fwupd_security_attr_new: * @appstream_id: (nullable): the AppStream component ID, e.g. `com.intel.BiosGuard` * * Creates a new security attribute. * * Plugins should not use this method, and should instead use `fu_plugin_security_attr_new()` or * `fu_security_attr_new()`. * * Returns: a new #FwupdSecurityAttr * * Since: 1.5.0 **/ FwupdSecurityAttr * fwupd_security_attr_new(const gchar *appstream_id) { FwupdSecurityAttr *self; self = g_object_new(FWUPD_TYPE_SECURITY_ATTR, NULL); if (appstream_id != NULL) fwupd_security_attr_set_appstream_id(self, appstream_id); return FWUPD_SECURITY_ATTR(self); } fwupd-2.0.10/libfwupd/fwupd-security-attr.h000066400000000000000000000262751501337203100206550ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fwupd-build.h" #include "fwupd-enums.h" G_BEGIN_DECLS #define FWUPD_TYPE_SECURITY_ATTR (fwupd_security_attr_get_type()) G_DECLARE_DERIVABLE_TYPE(FwupdSecurityAttr, fwupd_security_attr, FWUPD, SECURITY_ATTR, GObject) struct _FwupdSecurityAttrClass { GObjectClass parent_class; /*< private >*/ void (*_fwupd_reserved1)(void); void (*_fwupd_reserved2)(void); void (*_fwupd_reserved3)(void); void (*_fwupd_reserved4)(void); void (*_fwupd_reserved5)(void); void (*_fwupd_reserved6)(void); void (*_fwupd_reserved7)(void); }; /** * FwupdSecurityAttrFlags: * * The flags available for HSI attributes. **/ typedef enum { /** * FWUPD_SECURITY_ATTR_FLAG_NONE: * * No flags set. */ FWUPD_SECURITY_ATTR_FLAG_NONE = 0, /** * FWUPD_SECURITY_ATTR_FLAG_SUCCESS: * * Success. */ FWUPD_SECURITY_ATTR_FLAG_SUCCESS = 1 << 0, /** * FWUPD_SECURITY_ATTR_FLAG_OBSOLETED: * * Obsoleted by another attribute. */ FWUPD_SECURITY_ATTR_FLAG_OBSOLETED = 1 << 1, /** * FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA: * * Missing data. */ FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA = 1 << 2, /** * FWUPD_SECURITY_ATTR_FLAG_RUNTIME_UPDATES: * * Suffix `U`. */ FWUPD_SECURITY_ATTR_FLAG_RUNTIME_UPDATES = 1 << 8, /** * FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ATTESTATION: * * Suffix `A`. */ FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ATTESTATION = 1 << 9, /** * FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE: * * Suffix `!`. */ FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE = 1 << 10, /** * FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM: * * Contact the firmware vendor for a update. */ FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM = 1 << 11, /** * FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW: * * Failure may be fixed by changing FW config. */ FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW = 1 << 12, /** * FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_OS: * * Failure may be fixed by changing OS config. */ FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_OS = 1 << 13, /** * FWUPD_SECURITY_ATTR_FLAG_CAN_FIX: * * The failure can be automatically fixed. */ FWUPD_SECURITY_ATTR_FLAG_CAN_FIX = 1 << 14, /** * FWUPD_SECURITY_ATTR_FLAG_CAN_UNDO: * * The fix can be automatically reverted. */ FWUPD_SECURITY_ATTR_FLAG_CAN_UNDO = 1 << 15, } FwupdSecurityAttrFlags; /** * FwupdSecurityAttrLevel: * * The HSI level. **/ typedef enum { /** * FWUPD_SECURITY_ATTR_LEVEL_NONE: * * Very few detected firmware protections. * * Since: 1.5.0 */ FWUPD_SECURITY_ATTR_LEVEL_NONE = 0, /** * FWUPD_SECURITY_ATTR_LEVEL_CRITICAL: * * The most basic of security protections. * * Since: 1.5.0 */ FWUPD_SECURITY_ATTR_LEVEL_CRITICAL = 1, /** * FWUPD_SECURITY_ATTR_LEVEL_IMPORTANT: * * Firmware security issues considered important. * * Since: 1.5.0 */ FWUPD_SECURITY_ATTR_LEVEL_IMPORTANT = 2, /** * FWUPD_SECURITY_ATTR_LEVEL_THEORETICAL: * * Firmware security issues that pose a theoretical concern. * * Since: 1.5.0 */ FWUPD_SECURITY_ATTR_LEVEL_THEORETICAL = 3, /** * FWUPD_SECURITY_ATTR_LEVEL_SYSTEM_PROTECTION: * * Out-of-band protection of the system firmware. * * Since: 1.5.0 */ FWUPD_SECURITY_ATTR_LEVEL_SYSTEM_PROTECTION = 4, /** * FWUPD_SECURITY_ATTR_LEVEL_SYSTEM_ATTESTATION: * * Out-of-band attestation of the system firmware. * * Since: 1.5.0 */ FWUPD_SECURITY_ATTR_LEVEL_SYSTEM_ATTESTATION = 5, /*< private >*/ FWUPD_SECURITY_ATTR_LEVEL_LAST = 6 } FwupdSecurityAttrLevel; /** * FwupdSecurityAttrResult: * * The HSI result. **/ typedef enum { /** * FWUPD_SECURITY_ATTR_RESULT_UNKNOWN: * * Not known. * * Since: 1.5.0 */ FWUPD_SECURITY_ATTR_RESULT_UNKNOWN, /** * FWUPD_SECURITY_ATTR_RESULT_ENABLED: * * Enabled. * * Since: 1.5.0 */ FWUPD_SECURITY_ATTR_RESULT_ENABLED, /** * FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED: * * Not enabled. * * Since: 1.5.0 */ FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED, /** * FWUPD_SECURITY_ATTR_RESULT_VALID: * * Valid. * * Since: 1.5.0 */ FWUPD_SECURITY_ATTR_RESULT_VALID, /** * FWUPD_SECURITY_ATTR_RESULT_NOT_VALID: * * Not valid. * * Since: 1.5.0 */ FWUPD_SECURITY_ATTR_RESULT_NOT_VALID, /** * FWUPD_SECURITY_ATTR_RESULT_LOCKED: * * Locked. * * Since: 1.5.0 */ FWUPD_SECURITY_ATTR_RESULT_LOCKED, /** * FWUPD_SECURITY_ATTR_RESULT_NOT_LOCKED: * * Not locked. * * Since: 1.5.0 */ FWUPD_SECURITY_ATTR_RESULT_NOT_LOCKED, /** * FWUPD_SECURITY_ATTR_RESULT_ENCRYPTED: * * Encrypted. * * Since: 1.5.0 */ FWUPD_SECURITY_ATTR_RESULT_ENCRYPTED, /** * FWUPD_SECURITY_ATTR_RESULT_NOT_ENCRYPTED: * * Not encrypted. * * Since: 1.5.0 */ FWUPD_SECURITY_ATTR_RESULT_NOT_ENCRYPTED, /** * FWUPD_SECURITY_ATTR_RESULT_TAINTED: * * Tainted. * * Since: 1.5.0 */ FWUPD_SECURITY_ATTR_RESULT_TAINTED, /** * FWUPD_SECURITY_ATTR_RESULT_NOT_TAINTED: * * Not tainted. * * Since: 1.5.0 */ FWUPD_SECURITY_ATTR_RESULT_NOT_TAINTED, /** * FWUPD_SECURITY_ATTR_RESULT_FOUND: * * Found. * * Since: 1.5.0 */ FWUPD_SECURITY_ATTR_RESULT_FOUND, /** * FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND: * * Not found. * * Since: 1.5.0 */ FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND, /** * FWUPD_SECURITY_ATTR_RESULT_SUPPORTED: * * Supported. * * Since: 1.5.0 */ FWUPD_SECURITY_ATTR_RESULT_SUPPORTED, /** * FWUPD_SECURITY_ATTR_RESULT_NOT_SUPPORTED: * * Not supported. * * Since: 1.5.0 */ FWUPD_SECURITY_ATTR_RESULT_NOT_SUPPORTED, /*< private >*/ FWUPD_SECURITY_ATTR_RESULT_LAST } FwupdSecurityAttrResult; FwupdSecurityAttr * fwupd_security_attr_new(const gchar *appstream_id); const gchar * fwupd_security_attr_get_bios_setting_id(FwupdSecurityAttr *self) G_GNUC_NON_NULL(1); void fwupd_security_attr_set_bios_setting_id(FwupdSecurityAttr *self, const gchar *id) G_GNUC_NON_NULL(1); const gchar * fwupd_security_attr_get_bios_setting_target_value(FwupdSecurityAttr *self) G_GNUC_NON_NULL(1); void fwupd_security_attr_set_bios_setting_target_value(FwupdSecurityAttr *self, const gchar *value) G_GNUC_NON_NULL(1); const gchar * fwupd_security_attr_get_bios_setting_current_value(FwupdSecurityAttr *self) G_GNUC_NON_NULL(1); void fwupd_security_attr_set_bios_setting_current_value(FwupdSecurityAttr *self, const gchar *value) G_GNUC_NON_NULL(1); const gchar * fwupd_security_attr_get_kernel_current_value(FwupdSecurityAttr *self) G_GNUC_NON_NULL(1); void fwupd_security_attr_set_kernel_current_value(FwupdSecurityAttr *self, const gchar *value) G_GNUC_NON_NULL(1); const gchar * fwupd_security_attr_get_kernel_target_value(FwupdSecurityAttr *self) G_GNUC_NON_NULL(1); void fwupd_security_attr_set_kernel_target_value(FwupdSecurityAttr *self, const gchar *value) G_GNUC_NON_NULL(1); const gchar * fwupd_security_attr_get_appstream_id(FwupdSecurityAttr *self) G_GNUC_NON_NULL(1); void fwupd_security_attr_set_appstream_id(FwupdSecurityAttr *self, const gchar *appstream_id) G_GNUC_NON_NULL(1); FwupdSecurityAttrLevel fwupd_security_attr_get_level(FwupdSecurityAttr *self) G_GNUC_NON_NULL(1); void fwupd_security_attr_set_level(FwupdSecurityAttr *self, FwupdSecurityAttrLevel level) G_GNUC_NON_NULL(1); FwupdSecurityAttrResult fwupd_security_attr_get_result(FwupdSecurityAttr *self) G_GNUC_NON_NULL(1); void fwupd_security_attr_set_result(FwupdSecurityAttr *self, FwupdSecurityAttrResult result) G_GNUC_NON_NULL(1); FwupdSecurityAttrResult fwupd_security_attr_get_result_fallback(FwupdSecurityAttr *self) G_GNUC_NON_NULL(1); void fwupd_security_attr_set_result_fallback(FwupdSecurityAttr *self, FwupdSecurityAttrResult result) G_GNUC_NON_NULL(1); FwupdSecurityAttrResult fwupd_security_attr_get_result_success(FwupdSecurityAttr *self) G_GNUC_NON_NULL(1); void fwupd_security_attr_set_result_success(FwupdSecurityAttr *self, FwupdSecurityAttrResult result) G_GNUC_NON_NULL(1); const gchar * fwupd_security_attr_get_name(FwupdSecurityAttr *self) G_GNUC_NON_NULL(1); void fwupd_security_attr_set_name(FwupdSecurityAttr *self, const gchar *name) G_GNUC_NON_NULL(1); const gchar * fwupd_security_attr_get_title(FwupdSecurityAttr *self) G_GNUC_NON_NULL(1); void fwupd_security_attr_set_title(FwupdSecurityAttr *self, const gchar *title) G_GNUC_NON_NULL(1); const gchar * fwupd_security_attr_get_description(FwupdSecurityAttr *self) G_GNUC_NON_NULL(1); void fwupd_security_attr_set_description(FwupdSecurityAttr *self, const gchar *description) G_GNUC_NON_NULL(1); const gchar * fwupd_security_attr_get_plugin(FwupdSecurityAttr *self) G_GNUC_NON_NULL(1); void fwupd_security_attr_set_plugin(FwupdSecurityAttr *self, const gchar *plugin) G_GNUC_NON_NULL(1); const gchar * fwupd_security_attr_get_fwupd_version(FwupdSecurityAttr *self) G_GNUC_NON_NULL(1); void fwupd_security_attr_set_fwupd_version(FwupdSecurityAttr *self, const gchar *fwupd_version) G_GNUC_NON_NULL(1); const gchar * fwupd_security_attr_get_url(FwupdSecurityAttr *self) G_GNUC_NON_NULL(1); void fwupd_security_attr_set_url(FwupdSecurityAttr *self, const gchar *url) G_GNUC_NON_NULL(1); guint64 fwupd_security_attr_get_created(FwupdSecurityAttr *self) G_GNUC_NON_NULL(1); void fwupd_security_attr_set_created(FwupdSecurityAttr *self, guint64 created) G_GNUC_NON_NULL(1); GPtrArray * fwupd_security_attr_get_obsoletes(FwupdSecurityAttr *self) G_GNUC_NON_NULL(1); void fwupd_security_attr_add_obsolete(FwupdSecurityAttr *self, const gchar *appstream_id) G_GNUC_NON_NULL(1, 2); gboolean fwupd_security_attr_has_obsolete(FwupdSecurityAttr *self, const gchar *appstream_id) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); GPtrArray * fwupd_security_attr_get_guids(FwupdSecurityAttr *self) G_GNUC_NON_NULL(1); void fwupd_security_attr_add_guid(FwupdSecurityAttr *self, const gchar *guid) G_GNUC_NON_NULL(1, 2); void fwupd_security_attr_add_guids(FwupdSecurityAttr *self, GPtrArray *guids) G_GNUC_NON_NULL(1, 2); gboolean fwupd_security_attr_has_guid(FwupdSecurityAttr *self, const gchar *guid) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); const gchar * fwupd_security_attr_get_metadata(FwupdSecurityAttr *self, const gchar *key) G_GNUC_NON_NULL(1, 2); void fwupd_security_attr_add_metadata(FwupdSecurityAttr *self, const gchar *key, const gchar *value) G_GNUC_NON_NULL(1, 2); FwupdSecurityAttrFlags fwupd_security_attr_get_flags(FwupdSecurityAttr *self) G_GNUC_NON_NULL(1); void fwupd_security_attr_set_flags(FwupdSecurityAttr *self, FwupdSecurityAttrFlags flags) G_GNUC_NON_NULL(1); void fwupd_security_attr_add_flag(FwupdSecurityAttr *self, FwupdSecurityAttrFlags flag) G_GNUC_NON_NULL(1); void fwupd_security_attr_remove_flag(FwupdSecurityAttr *self, FwupdSecurityAttrFlags flag) G_GNUC_NON_NULL(1); gboolean fwupd_security_attr_has_flag(FwupdSecurityAttr *self, FwupdSecurityAttrFlags flag) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); const gchar * fwupd_security_attr_flag_to_string(FwupdSecurityAttrFlags flag); FwupdSecurityAttrFlags fwupd_security_attr_flag_from_string(const gchar *flag); const gchar * fwupd_security_attr_flag_to_suffix(FwupdSecurityAttrFlags flag); const gchar * fwupd_security_attr_result_to_string(FwupdSecurityAttrResult result); FwupdSecurityAttrResult fwupd_security_attr_result_from_string(const gchar *result); G_END_DECLS fwupd-2.0.10/libfwupd/fwupd-self-test.c000066400000000000000000001615051501337203100177330ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fwupd-bios-setting.h" #include "fwupd-client-private.h" #include "fwupd-client-sync.h" #include "fwupd-codec.h" #include "fwupd-common.h" #include "fwupd-device-private.h" #include "fwupd-enums.h" #include "fwupd-error.h" #include "fwupd-plugin.h" #include "fwupd-release.h" #include "fwupd-remote-private.h" #include "fwupd-report.h" #include "fwupd-request-private.h" #include "fwupd-security-attr-private.h" static gboolean fu_test_compare_lines(const gchar *txt1, const gchar *txt2, GError **error) { g_autofree gchar *output = NULL; /* exactly the same */ if (g_strcmp0(txt1, txt2) == 0) return TRUE; /* matches a pattern */ if (g_pattern_match_simple(txt2, txt1)) return TRUE; /* save temp files and diff them */ if (!g_file_set_contents("/tmp/a", txt1, -1, error)) return FALSE; if (!g_file_set_contents("/tmp/b", txt2, -1, error)) return FALSE; if (!g_spawn_command_line_sync("diff -urNp /tmp/b /tmp/a", &output, NULL, NULL, error)) return FALSE; /* just output the diff */ g_set_error_literal(error, 1, 0, output); return FALSE; } static void fwupd_enums_func(void) { /* enums */ for (guint i = 0; i < FWUPD_ERROR_LAST; i++) { const gchar *tmp = fwupd_error_to_string(i); g_assert_cmpstr(tmp, !=, NULL); g_assert_cmpint(fwupd_error_from_string(tmp), ==, i); } for (guint i = 0; i < FWUPD_STATUS_LAST; i++) { const gchar *tmp = fwupd_status_to_string(i); g_assert_cmpstr(tmp, !=, NULL); g_assert_cmpint(fwupd_status_from_string(tmp), ==, i); } for (guint i = 0; i < FWUPD_UPDATE_STATE_LAST; i++) { const gchar *tmp = fwupd_update_state_to_string(i); g_assert_cmpstr(tmp, !=, NULL); g_assert_cmpint(fwupd_update_state_from_string(tmp), ==, i); } for (guint i = 0; i < FWUPD_REQUEST_KIND_LAST; i++) { const gchar *tmp = fwupd_request_kind_to_string(i); g_assert_cmpstr(tmp, !=, NULL); g_assert_cmpint(fwupd_request_kind_from_string(tmp), ==, i); } for (guint i = FWUPD_RELEASE_URGENCY_UNKNOWN + 1; i < FWUPD_RELEASE_URGENCY_LAST; i++) { const gchar *tmp = fwupd_release_urgency_to_string(i); g_assert_cmpstr(tmp, !=, NULL); g_assert_cmpint(fwupd_release_urgency_from_string(tmp), ==, i); } for (guint i = 1; i < FWUPD_VERSION_FORMAT_LAST; i++) { const gchar *tmp = fwupd_version_format_to_string(i); g_assert_cmpstr(tmp, !=, NULL); g_assert_cmpint(fwupd_version_format_from_string(tmp), ==, i); } for (guint i = 1; i < FWUPD_REMOTE_KIND_LAST; i++) { const gchar *tmp = fwupd_remote_kind_to_string(i); g_assert_cmpstr(tmp, !=, NULL); g_assert_cmpint(fwupd_remote_kind_from_string(tmp), ==, i); } /* bitfield */ for (guint64 i = 1; i <= FWUPD_DEVICE_FLAG_INSTALL_SKIP_VERSION_CHECK; i *= 2) { const gchar *tmp = fwupd_device_flag_to_string(i); if (tmp == NULL) continue; g_assert_cmpint(fwupd_device_flag_from_string(tmp), ==, i); } for (guint64 i = 1; i <= FWUPD_DEVICE_PROBLEM_IN_USE; i *= 2) { const gchar *tmp = fwupd_device_problem_to_string(i); g_assert_cmpstr(tmp, !=, NULL); g_assert_cmpint(fwupd_device_problem_from_string(tmp), ==, i); } for (guint64 i = 1; i <= FWUPD_PLUGIN_FLAG_TEST_ONLY; i *= 2) { const gchar *tmp = fwupd_plugin_flag_to_string(i); g_assert_cmpstr(tmp, !=, NULL); g_assert_cmpint(fwupd_plugin_flag_from_string(tmp), ==, i); } for (guint64 i = 1; i <= FWUPD_FEATURE_FLAG_REQUESTS_NON_GENERIC; i *= 2) { const gchar *tmp = fwupd_feature_flag_to_string(i); g_assert_cmpstr(tmp, !=, NULL); g_assert_cmpint(fwupd_feature_flag_from_string(tmp), ==, i); } for (guint64 i = 1; i <= FWUPD_RELEASE_FLAG_TRUSTED_REPORT; i *= 2) { const gchar *tmp = fwupd_release_flag_to_string(i); g_assert_cmpstr(tmp, !=, NULL); g_assert_cmpint(fwupd_release_flag_from_string(tmp), ==, i); } for (guint64 i = 1; i <= FWUPD_REQUEST_FLAG_NON_GENERIC_IMAGE; i *= 2) { const gchar *tmp = fwupd_request_flag_to_string(i); g_assert_cmpstr(tmp, !=, NULL); g_assert_cmpint(fwupd_request_flag_from_string(tmp), ==, i); } for (guint64 i = 1; i < FWUPD_REMOTE_FLAG_ALLOW_P2P_FIRMWARE; i *= 2) { const gchar *tmp = fwupd_remote_flag_to_string(i); if (tmp == NULL) break; g_assert_cmpint(fwupd_remote_flag_from_string(tmp), ==, i); } for (guint64 i = 1; i <= FWUPD_INSTALL_FLAG_ONLY_EMULATED; i *= 2) { const gchar *tmp = fwupd_install_flags_to_string(i); if (tmp == NULL) continue; g_assert_cmpint(fwupd_install_flags_from_string(tmp), ==, i); } } static void fwupd_release_func(void) { gboolean ret; g_autofree gchar *json1 = NULL; g_autofree gchar *json2 = NULL; g_autofree gchar *str = NULL; g_autoptr(FwupdRelease) release1 = NULL; g_autoptr(FwupdRelease) release2 = fwupd_release_new(); g_autoptr(GError) error = NULL; g_autoptr(GVariant) data = NULL; release1 = fwupd_release_new(); fwupd_release_add_metadata_item(release1, "foo", "bar"); fwupd_release_add_metadata_item(release1, "baz", "bam"); fwupd_release_set_remote_id(release1, "remote-id"); fwupd_release_set_appstream_id(release1, "appstream-id"); fwupd_release_set_id(release1, "id"); fwupd_release_set_detach_caption(release1, "detach_caption"); fwupd_release_set_detach_image(release1, "detach_image"); fwupd_release_set_update_message(release1, "update_message"); fwupd_release_set_update_image(release1, "update_image"); fwupd_release_set_filename(release1, "filename"); fwupd_release_set_protocol(release1, "protocol"); fwupd_release_set_license(release1, "license"); fwupd_release_set_name(release1, "name"); fwupd_release_set_name_variant_suffix(release1, "name_variant_suffix"); fwupd_release_set_summary(release1, "summary"); fwupd_release_set_branch(release1, "branch"); fwupd_release_set_description(release1, "description"); fwupd_release_set_homepage(release1, "homepage"); fwupd_release_set_details_url(release1, "details_url"); fwupd_release_set_source_url(release1, "source_url"); fwupd_release_set_sbom_url(release1, "sbom_url"); fwupd_release_set_version(release1, "version"); fwupd_release_set_vendor(release1, "vendor"); fwupd_release_set_size(release1, 1234); fwupd_release_set_created(release1, 5678); fwupd_release_set_install_duration(release1, 2468); fwupd_release_add_category(release1, "category"); fwupd_release_add_category(release1, "category"); fwupd_release_add_issue(release1, "issue"); fwupd_release_add_issue(release1, "issue"); fwupd_release_add_location(release1, "location"); fwupd_release_add_location(release1, "location"); fwupd_release_add_tag(release1, "tag"); fwupd_release_add_tag(release1, "tag"); fwupd_release_add_checksum(release1, "checksum"); fwupd_release_add_checksum(release1, "checksum"); fwupd_release_add_flag(release1, FWUPD_RELEASE_FLAG_IS_UPGRADE); fwupd_release_add_flag(release1, FWUPD_RELEASE_FLAG_IS_UPGRADE); fwupd_release_add_flag(release1, FWUPD_RELEASE_FLAG_BLOCKED_APPROVAL); fwupd_release_remove_flag(release1, FWUPD_RELEASE_FLAG_BLOCKED_APPROVAL); fwupd_release_set_urgency(release1, FWUPD_RELEASE_URGENCY_MEDIUM); data = fwupd_codec_to_variant(FWUPD_CODEC(release1), FWUPD_CODEC_FLAG_NONE); ret = fwupd_codec_from_variant(FWUPD_CODEC(release2), data, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpstr(fwupd_release_get_metadata_item(release2, "foo"), ==, "bar"); g_assert_cmpstr(fwupd_release_get_metadata_item(release2, "baz"), ==, "bam"); g_assert_cmpstr(fwupd_release_get_remote_id(release2), ==, "remote-id"); g_assert_cmpstr(fwupd_release_get_appstream_id(release2), ==, "appstream-id"); g_assert_cmpstr(fwupd_release_get_id(release2), ==, "id"); g_assert_cmpstr(fwupd_release_get_detach_caption(release2), ==, "detach_caption"); g_assert_cmpstr(fwupd_release_get_detach_image(release2), ==, "detach_image"); g_assert_cmpstr(fwupd_release_get_update_message(release2), ==, "update_message"); g_assert_cmpstr(fwupd_release_get_update_image(release2), ==, "update_image"); g_assert_cmpstr(fwupd_release_get_filename(release2), ==, "filename"); g_assert_cmpstr(fwupd_release_get_protocol(release2), ==, "protocol"); g_assert_cmpstr(fwupd_release_get_license(release2), ==, "license"); g_assert_cmpstr(fwupd_release_get_name(release2), ==, "name"); g_assert_cmpstr(fwupd_release_get_name_variant_suffix(release2), ==, "name_variant_suffix"); g_assert_cmpstr(fwupd_release_get_summary(release2), ==, "summary"); g_assert_cmpstr(fwupd_release_get_branch(release2), ==, "branch"); g_assert_cmpstr(fwupd_release_get_description(release2), ==, "description"); g_assert_cmpstr(fwupd_release_get_homepage(release2), ==, "homepage"); g_assert_cmpstr(fwupd_release_get_details_url(release2), ==, "details_url"); g_assert_cmpstr(fwupd_release_get_source_url(release2), ==, "source_url"); g_assert_cmpstr(fwupd_release_get_sbom_url(release2), ==, "sbom_url"); g_assert_cmpstr(fwupd_release_get_version(release2), ==, "version"); g_assert_cmpstr(fwupd_release_get_vendor(release2), ==, "vendor"); g_assert_cmpint(fwupd_release_get_size(release2), ==, 1234); g_assert_cmpint(fwupd_release_get_created(release2), ==, 5678); g_assert_true(fwupd_release_has_category(release2, "category")); g_assert_true(fwupd_release_has_tag(release2, "tag")); g_assert_true(fwupd_release_has_checksum(release2, "checksum")); g_assert_true(fwupd_release_has_flag(release2, FWUPD_RELEASE_FLAG_IS_UPGRADE)); g_assert_false(fwupd_release_has_flag(release2, FWUPD_RELEASE_FLAG_IS_COMMUNITY)); g_assert_cmpint(fwupd_release_get_issues(release2)->len, ==, 1); g_assert_cmpint(fwupd_release_get_locations(release2)->len, ==, 1); g_assert_cmpint(fwupd_release_get_categories(release2)->len, ==, 1); g_assert_cmpint(fwupd_release_get_tags(release2)->len, ==, 1); g_assert_cmpint(fwupd_release_get_checksums(release2)->len, ==, 1); g_assert_cmpint(fwupd_release_get_urgency(release2), ==, FWUPD_RELEASE_URGENCY_MEDIUM); g_assert_cmpint(fwupd_release_get_install_duration(release2), ==, 2468); /* to JSON */ json1 = fwupd_codec_to_json_string(FWUPD_CODEC(release1), FWUPD_CODEC_FLAG_NONE, &error); g_assert_no_error(error); g_assert_nonnull(json1); json2 = fwupd_codec_to_json_string(FWUPD_CODEC(release2), FWUPD_CODEC_FLAG_NONE, &error); g_assert_no_error(error); g_assert_nonnull(json2); ret = fu_test_compare_lines(json1, json2, &error); g_assert_no_error(error); g_assert_true(ret); /* to string */ str = fwupd_codec_to_string(FWUPD_CODEC(release2)); ret = fu_test_compare_lines(str, "FwupdRelease:\n" " AppstreamId: appstream-id\n" " ReleaseId: id\n" " RemoteId: remote-id\n" " Name: name\n" " NameVariantSuffix: name_variant_suffix\n" " Summary: summary\n" " Description: description\n" " Branch: branch\n" " Version: version\n" " Filename: filename\n" " Protocol: protocol\n" " Categories: category\n" " Issues: issue\n" " Checksum: SHA1(checksum)\n" " Tags: tag\n" " License: license\n" " Size: 1.2 kB\n" " Created: 1970-01-01\n" " Uri: location\n" " Homepage: homepage\n" " DetailsUrl: details_url\n" " SourceUrl: source_url\n" " SbomUrl: sbom_url\n" " Urgency: medium\n" " Vendor: vendor\n" " Flags: is-upgrade\n" " InstallDuration: 2468\n" " DetachCaption: detach_caption\n" " DetachImage: detach_image\n" " UpdateMessage: update_message\n" " UpdateImage: update_image\n" " foo: bar\n" " baz: bam\n", &error); g_assert_no_error(error); g_assert_true(ret); } static void fwupd_report_func(void) { gboolean ret; g_autofree gchar *json1 = NULL; g_autofree gchar *json2 = NULL; g_autofree gchar *str = NULL; g_autoptr(FwupdReport) report1 = NULL; g_autoptr(FwupdReport) report2 = fwupd_report_new(); g_autoptr(GError) error = NULL; g_autoptr(GVariant) data = NULL; report1 = fwupd_report_new(); fwupd_report_add_metadata_item(report1, "foo", "bar"); fwupd_report_add_metadata_item(report1, "baz", "bam"); fwupd_report_set_version_old(report1, "1.2.3"); fwupd_report_set_created(report1, 5678); fwupd_report_set_vendor(report1, "acme"); fwupd_report_set_vendor_id(report1, 2468); fwupd_report_set_device_name(report1, "name"); fwupd_report_set_distro_id(report1, "distro_id"); fwupd_report_set_distro_version(report1, "distro_version"); fwupd_report_set_remote_id(report1, "lvfs"); data = fwupd_codec_to_variant(FWUPD_CODEC(report1), FWUPD_CODEC_FLAG_NONE); ret = fwupd_codec_from_variant(FWUPD_CODEC(report2), data, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpstr(fwupd_report_get_metadata_item(report2, "foo"), ==, "bar"); g_assert_cmpstr(fwupd_report_get_metadata_item(report2, "baz"), ==, "bam"); g_assert_cmpstr(fwupd_report_get_version_old(report2), ==, "1.2.3"); g_assert_cmpstr(fwupd_report_get_vendor(report2), ==, "acme"); g_assert_cmpint(fwupd_report_get_vendor_id(report2), ==, 2468); g_assert_cmpstr(fwupd_report_get_device_name(report2), ==, "name"); g_assert_cmpstr(fwupd_report_get_distro_id(report2), ==, "distro_id"); g_assert_cmpstr(fwupd_report_get_distro_version(report2), ==, "distro_version"); g_assert_cmpint(fwupd_report_get_created(report2), ==, 5678); /* to JSON */ json1 = fwupd_codec_to_json_string(FWUPD_CODEC(report1), FWUPD_CODEC_FLAG_NONE, &error); g_assert_no_error(error); g_assert_nonnull(json1); json2 = fwupd_codec_to_json_string(FWUPD_CODEC(report2), FWUPD_CODEC_FLAG_NONE, &error); g_assert_no_error(error); g_assert_nonnull(json2); ret = fu_test_compare_lines(json1, json2, &error); g_assert_no_error(error); g_assert_true(ret); /* to string */ str = fwupd_codec_to_string(FWUPD_CODEC(report2)); ret = fu_test_compare_lines(str, "FwupdReport:\n" " DeviceName: name\n" " DistroId: distro_id\n" " DistroVersion: distro_version\n" " VersionOld: 1.2.3\n" " Vendor: acme\n" " VendorId: 2468\n" " RemoteId: lvfs\n" " Flags: none\n" " foo: bar\n" " baz: bam\n", &error); g_assert_no_error(error); g_assert_true(ret); } static void fwupd_plugin_func(void) { gboolean ret; g_autofree gchar *str = NULL; g_autoptr(FwupdPlugin) plugin1 = fwupd_plugin_new(); g_autoptr(FwupdPlugin) plugin2 = fwupd_plugin_new(); g_autoptr(GError) error = NULL; g_autoptr(GVariant) data = NULL; fwupd_plugin_set_name(plugin1, "foo"); fwupd_plugin_set_flags(plugin1, FWUPD_PLUGIN_FLAG_USER_WARNING); fwupd_plugin_add_flag(plugin1, FWUPD_PLUGIN_FLAG_CLEAR_UPDATABLE); fwupd_plugin_add_flag(plugin1, FWUPD_PLUGIN_FLAG_CLEAR_UPDATABLE); fwupd_plugin_add_flag(plugin1, FWUPD_PLUGIN_FLAG_NO_HARDWARE); fwupd_plugin_remove_flag(plugin1, FWUPD_PLUGIN_FLAG_NO_HARDWARE); fwupd_plugin_remove_flag(plugin1, FWUPD_PLUGIN_FLAG_NO_HARDWARE); data = fwupd_codec_to_variant(FWUPD_CODEC(plugin1), FWUPD_CODEC_FLAG_NONE); ret = fwupd_codec_from_variant(FWUPD_CODEC(plugin2), data, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpstr(fwupd_plugin_get_name(plugin2), ==, "foo"); g_assert_cmpint(fwupd_plugin_get_flags(plugin2), ==, FWUPD_PLUGIN_FLAG_USER_WARNING | FWUPD_PLUGIN_FLAG_CLEAR_UPDATABLE); g_assert_true(fwupd_plugin_has_flag(plugin2, FWUPD_PLUGIN_FLAG_USER_WARNING)); g_assert_true(fwupd_plugin_has_flag(plugin2, FWUPD_PLUGIN_FLAG_CLEAR_UPDATABLE)); g_assert_false(fwupd_plugin_has_flag(plugin2, FWUPD_PLUGIN_FLAG_NO_HARDWARE)); str = fwupd_codec_to_string(FWUPD_CODEC(plugin2)); ret = fu_test_compare_lines(str, "FwupdPlugin:\n" " Name: foo\n" " Flags: user-warning|clear-updatable\n", &error); g_assert_no_error(error); g_assert_true(ret); } static void fwupd_remote_func(void) { g_autofree gchar *uri1 = NULL; g_autofree gchar *uri2 = NULL; g_autofree gchar *uri3 = NULL; g_autoptr(FwupdRemote) remote = fwupd_remote_new(); g_autoptr(GError) error = NULL; uri1 = fwupd_remote_build_firmware_uri(remote, "https://example.org/downloads/foo.cab", &error); g_assert_no_error(error); g_assert_cmpstr(uri1, ==, "https://example.org/downloads/foo.cab"); fwupd_remote_set_firmware_base_uri(remote, "https://example.org/mirror"); uri2 = fwupd_remote_build_firmware_uri(remote, "https://example.org/downloads/foo.cab", &error); g_assert_no_error(error); g_assert_cmpstr(uri2, ==, "https://example.org/mirror/foo.cab"); fwupd_remote_set_username(remote, "admin"); uri3 = fwupd_remote_build_firmware_uri(remote, "https://example.org/downloads/foo.cab", &error); g_assert_no_error(error); g_assert_cmpstr(uri3, ==, "https://admin@example.org/mirror/foo.cab/auth"); } static void fwupd_request_func(void) { gboolean ret; g_autofree gchar *str = NULL; g_autoptr(FwupdRequest) request = fwupd_request_new(); g_autoptr(FwupdRequest) request2 = fwupd_request_new(); g_autoptr(GError) error = NULL; g_autoptr(GVariant) data = NULL; /* create dummy */ fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_IMMEDIATE); fwupd_request_set_id(request, FWUPD_REQUEST_ID_DO_NOT_POWER_OFF); fwupd_request_add_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); g_assert_cmpstr(fwupd_request_get_message(request), ==, "Do not turn off your computer or remove the AC adaptor."); fwupd_request_set_message(request, "foo"); fwupd_request_set_image(request, "bar"); fwupd_request_set_device_id(request, "950da62d4c753a26e64f7f7d687104ce38e32ca5"); str = fwupd_codec_to_string(FWUPD_CODEC(request)); g_debug("%s", str); g_assert_true(fwupd_request_has_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE)); g_assert_cmpint(fwupd_request_get_flags(request), ==, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); fwupd_request_remove_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); g_assert_false(fwupd_request_has_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE)); g_assert_cmpint(fwupd_request_get_flags(request), ==, FWUPD_REQUEST_FLAG_NONE); /* set in init */ g_assert_cmpint(fwupd_request_get_created(request), >, 0); /* to serialized and back again */ data = fwupd_codec_to_variant(FWUPD_CODEC(request), FWUPD_CODEC_FLAG_NONE); ret = fwupd_codec_from_variant(FWUPD_CODEC(request2), data, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(fwupd_request_get_kind(request2), ==, FWUPD_REQUEST_KIND_IMMEDIATE); g_assert_cmpint(fwupd_request_get_created(request2), >, 0); g_assert_cmpstr(fwupd_request_get_id(request2), ==, FWUPD_REQUEST_ID_DO_NOT_POWER_OFF); g_assert_cmpstr(fwupd_request_get_message(request2), ==, "foo"); g_assert_cmpstr(fwupd_request_get_image(request2), ==, "bar"); g_assert_cmpstr(fwupd_request_get_device_id(request2), ==, "950da62d4c753a26e64f7f7d687104ce38e32ca5"); } static void fwupd_device_filter_func(void) { g_autoptr(FwupdDevice) dev = fwupd_device_new(); fwupd_device_add_flag(dev, FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fwupd_device_add_flag(dev, FWUPD_DEVICE_FLAG_SUPPORTED); /* none */ g_assert_true( fwupd_device_match_flags(dev, FWUPD_DEVICE_FLAG_NONE, FWUPD_DEVICE_FLAG_NONE)); /* include */ g_assert_true(fwupd_device_match_flags(dev, FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD, FWUPD_DEVICE_FLAG_NONE)); g_assert_true( fwupd_device_match_flags(dev, FWUPD_DEVICE_FLAG_SUPPORTED, FWUPD_DEVICE_FLAG_NONE)); g_assert_true( fwupd_device_match_flags(dev, FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD | FWUPD_DEVICE_FLAG_SUPPORTED, FWUPD_DEVICE_FLAG_NONE)); g_assert_false(fwupd_device_match_flags(dev, FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED, FWUPD_DEVICE_FLAG_NONE)); /* exclude, i.e. ~flag */ g_assert_false(fwupd_device_match_flags(dev, FWUPD_DEVICE_FLAG_NONE, FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD)); g_assert_false( fwupd_device_match_flags(dev, FWUPD_DEVICE_FLAG_NONE, FWUPD_DEVICE_FLAG_SUPPORTED)); g_assert_false(fwupd_device_match_flags(dev, FWUPD_DEVICE_FLAG_NONE, FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD | FWUPD_DEVICE_FLAG_SUPPORTED)); g_assert_true(fwupd_device_match_flags(dev, FWUPD_DEVICE_FLAG_NONE, FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED)); } static void fwupd_client_api_undefined_setter(void) { #if GLIB_CHECK_VERSION(2, 74, 0) if (g_test_subprocess()) { g_autoptr(FwupdClient) client = fwupd_client_new(); GValue value_bool = G_VALUE_INIT; g_value_init(&value_bool, G_TYPE_BOOLEAN); g_object_set_property(G_OBJECT(client), "battery-adapter", &value_bool); } else { g_test_trap_subprocess("/fwupd/client_api{undefined_setter}", 0, G_TEST_SUBPROCESS_DEFAULT); g_test_trap_assert_failed(); g_test_trap_assert_stderr( "*GLib-GObject-CRITICAL*has no property named 'battery-adapter'*"); } #else g_test_skip("Trap handling requires glib 2.74"); #endif } static void fwupd_client_api_undefined_getter(void) { #if GLIB_CHECK_VERSION(2, 74, 0) if (g_test_subprocess()) { g_autoptr(FwupdClient) client = fwupd_client_new(); GValue value_bool = G_VALUE_INIT; g_value_init(&value_bool, G_TYPE_BOOLEAN); g_object_get_property(G_OBJECT(client), "battery-adapter", &value_bool); } else { g_test_trap_subprocess("/fwupd/client_api{undefined_getter}", 0, G_TEST_SUBPROCESS_DEFAULT); g_test_trap_assert_failed(); g_test_trap_assert_stderr( "*GLib-GObject-CRITICAL*has no property named 'battery-adapter'*"); } #else g_test_skip("Trap handling requires glib 2.74"); #endif } static void fwupd_client_api_ro_props(void) { #if GLIB_CHECK_VERSION(2, 74, 0) const gchar *props[] = {"daemon-version", "tainted", "interactive", "only-trusted", NULL}; for (guint i = 0; i < G_N_ELEMENTS(props); i++) { if (g_test_subprocess()) { g_autoptr(FwupdClient) client = fwupd_client_new(); GValue value_bool = G_VALUE_INIT; g_value_init(&value_bool, G_TYPE_BOOLEAN); g_object_set_property(G_OBJECT(client), props[i], &value_bool); } else { g_test_trap_subprocess("/fwupd/client_api{ro_props}", 0, G_TEST_SUBPROCESS_DEFAULT); g_test_trap_assert_failed(); g_test_trap_assert_stderr( "*GLib-GObject-CRITICAL*property*is not writable*"); } } #else g_test_skip("Trap handling requires glib 2.74"); #endif } static void fwupd_client_api(void) { gboolean ret; const gchar *tmp = "1234567890abcdef"; GValue value_str = G_VALUE_INIT; GValue value_int = G_VALUE_INIT; GValue value_bool = G_VALUE_INIT; g_autoptr(FwupdClient) client = fwupd_client_new(); g_autoptr(GError) error = NULL; g_value_init(&value_str, G_TYPE_STRING); g_value_init(&value_int, G_TYPE_INT); g_value_init(&value_bool, G_TYPE_BOOLEAN); g_value_set_string(&value_str, tmp); ret = fwupd_client_get_only_trusted(client); g_assert_false(ret); ret = fwupd_client_get_daemon_interactive(client); g_assert_false(ret); ret = fwupd_client_get_tainted(client); g_assert_false(ret); /* set the version multiple times */ fwupd_client_set_daemon_version(client, "1.2.3"); g_assert_cmpstr(fwupd_client_get_daemon_version(client), ==, "1.2.3"); fwupd_client_set_daemon_version(client, "1.2.4"); g_assert_cmpstr(fwupd_client_get_daemon_version(client), ==, "1.2.4"); fwupd_client_set_daemon_version(client, "1.2.4"); /* set host security ID multiple times */ g_object_set_property(G_OBJECT(client), "host-security-id", &value_str); g_assert_cmpstr(fwupd_client_get_host_security_id(client), ==, tmp); g_object_set_property(G_OBJECT(client), "host-security-id", &value_str); /* set host machine ID multiple times */ g_object_set_property(G_OBJECT(client), "host-machine-id", &value_str); g_assert_cmpstr(fwupd_client_get_host_machine_id(client), ==, tmp); g_object_set_property(G_OBJECT(client), "host-machine-id", &value_str); /* set host product ID and product vendor multiple times */ tmp = "Acme"; g_value_set_string(&value_str, tmp); g_object_set_property(G_OBJECT(client), "host-vendor", &value_str); g_assert_cmpstr(fwupd_client_get_host_vendor(client), ==, tmp); g_object_set_property(G_OBJECT(client), "host-vendor", &value_str); tmp = "Anvil"; g_value_set_string(&value_str, tmp); g_object_set_property(G_OBJECT(client), "host-product", &value_str); g_assert_cmpstr(fwupd_client_get_host_product(client), ==, tmp); g_object_set_property(G_OBJECT(client), "host-product", &value_str); /* set BKC */ tmp = "BKC-123"; g_value_set_string(&value_str, tmp); g_object_set_property(G_OBJECT(client), "host-bkc", &value_str); g_assert_cmpstr(fwupd_client_get_host_bkc(client), ==, tmp); g_object_set_property(G_OBJECT(client), "host-bkc", &value_str); /* verify experience with no user agent explicitly */ ret = fwupd_client_ensure_networking(client, &error); g_assert_true(ret); g_assert_no_error(error); /* verify experience with a good user agent*/ fwupd_client_set_user_agent_for_package(client, "fwupd", "2.0.0"); ret = fwupd_client_ensure_networking(client, &error); g_assert_true(ret); g_assert_no_error(error); /* set same battery level multiple times */ g_value_set_int(&value_int, 50); g_object_set_property(G_OBJECT(client), "battery-level", &value_int); g_assert_cmpint(fwupd_client_get_battery_level(client), ==, 50); g_object_set_property(G_OBJECT(client), "battery-level", &value_int); /* set same battery threshold multiple times */ g_value_set_int(&value_int, 20); g_object_set_property(G_OBJECT(client), "battery-threshold", &value_int); g_assert_cmpint(fwupd_client_get_battery_threshold(client), ==, 20); g_object_set_property(G_OBJECT(client), "battery-threshold", &value_int); /* set same status multiple times */ g_value_set_int(&value_int, FWUPD_STATUS_IDLE); g_object_set_property(G_OBJECT(client), "status", &value_int); g_assert_cmpint(fwupd_client_get_status(client), ==, FWUPD_STATUS_IDLE); g_object_set_property(G_OBJECT(client), "status", &value_int); /* set same percentage multiple times */ g_value_set_int(&value_int, 50); g_object_set_property(G_OBJECT(client), "percentage", &value_int); g_assert_cmpint(fwupd_client_get_percentage(client), ==, 50); g_object_set_property(G_OBJECT(client), "percentage", &value_int); /* set all properties */ g_value_set_int(&value_int, 0); g_object_set_property(G_OBJECT(client), "status", &value_int); g_object_set_property(G_OBJECT(client), "percentage", &value_int); g_object_set_property(G_OBJECT(client), "host-bkc", &value_str); g_object_set_property(G_OBJECT(client), "host-vendor", &value_str); g_object_set_property(G_OBJECT(client), "host-product", &value_str); g_object_set_property(G_OBJECT(client), "host-machine-id", &value_str); g_object_set_property(G_OBJECT(client), "host-security-id", &value_str); g_object_set_property(G_OBJECT(client), "battery-level", &value_int); g_object_set_property(G_OBJECT(client), "battery-threshold", &value_int); /* read all properties */ g_object_get_property(G_OBJECT(client), "status", &value_int); g_object_get_property(G_OBJECT(client), "tainted", &value_bool); g_object_get_property(G_OBJECT(client), "interactive", &value_bool); g_object_get_property(G_OBJECT(client), "percentage", &value_int); g_object_get_property(G_OBJECT(client), "daemon-version", &value_str); g_object_get_property(G_OBJECT(client), "host-bkc", &value_str); g_object_get_property(G_OBJECT(client), "host-vendor", &value_str); g_object_get_property(G_OBJECT(client), "host-product", &value_str); g_object_get_property(G_OBJECT(client), "host-machine-id", &value_str); g_object_get_property(G_OBJECT(client), "host-security-id", &value_str); g_object_get_property(G_OBJECT(client), "only-trusted", &value_bool); g_object_get_property(G_OBJECT(client), "battery-level", &value_int); g_object_get_property(G_OBJECT(client), "battery-threshold", &value_int); g_value_unset(&value_str); g_value_unset(&value_int); g_value_unset(&value_bool); } static void fwupd_common_history_report_func(void) { g_autofree gchar *json = NULL; g_autoptr(FwupdClient) client = fwupd_client_new(); g_autoptr(FwupdDevice) dev = fwupd_device_new(); g_autoptr(FwupdRelease) rel = fwupd_release_new(); g_autoptr(GError) error = NULL; g_autoptr(GHashTable) metadata = g_hash_table_new(g_str_hash, g_str_equal); g_autoptr(GPtrArray) devs = g_ptr_array_new(); fwupd_device_set_id(dev, "0000000000000000000000000000000000000000"); fwupd_device_set_update_state(dev, FWUPD_UPDATE_STATE_FAILED); fwupd_device_add_checksum(dev, "beefdead"); fwupd_device_add_guid(dev, "2082b5e0-7a64-478a-b1b2-e3404fab6dad"); fwupd_device_add_protocol(dev, "org.hughski.colorhug"); fwupd_device_set_plugin(dev, "hughski_colorhug"); fwupd_device_set_update_error(dev, "device dead"); fwupd_device_set_version(dev, "1.2.3"); fwupd_release_add_checksum(rel, "beefdead"); fwupd_release_set_id(rel, "123"); fwupd_release_set_update_message(rel, "oops"); fwupd_release_set_version(rel, "1.2.4"); fwupd_device_add_release(dev, rel); /* metadata */ g_hash_table_insert(metadata, (gpointer) "DistroId", (gpointer) "generic"); g_hash_table_insert(metadata, (gpointer) "DistroVersion", (gpointer) "39"); g_hash_table_insert(metadata, (gpointer) "DistroVariant", (gpointer) "workstation"); g_ptr_array_add(devs, dev); json = fwupd_client_build_report_history(client, devs, NULL, metadata, &error); g_assert_no_error(error); g_assert_nonnull(json); g_assert_cmpstr(json, ==, "{\n" " \"ReportType\" : \"history\",\n" " \"ReportVersion\" : 2,\n" " \"Metadata\" : {\n" " \"DistroId\" : \"generic\",\n" " \"DistroVariant\" : \"workstation\",\n" " \"DistroVersion\" : \"39\"\n" " },\n" " \"Reports\" : [\n" " {\n" " \"Checksum\" : \"beefdead\",\n" " \"ChecksumDevice\" : [\n" " \"beefdead\"\n" " ],\n" " \"ReleaseId\" : \"123\",\n" " \"UpdateState\" : 3,\n" " \"UpdateError\" : \"device dead\",\n" " \"UpdateMessage\" : \"oops\",\n" " \"Guid\" : [\n" " \"2082b5e0-7a64-478a-b1b2-e3404fab6dad\"\n" " ],\n" " \"Plugin\" : \"hughski_colorhug\",\n" " \"VersionOld\" : \"1.2.3\",\n" " \"VersionNew\" : \"1.2.4\",\n" " \"Flags\" : 0,\n" " \"Created\" : 0,\n" " \"Modified\" : 0\n" " }\n" " ]\n" "}"); } static void fwupd_device_func(void) { gboolean ret; g_autofree gchar *data = NULL; g_autofree gchar *str = NULL; g_autoptr(FwupdDevice) dev = NULL; g_autoptr(FwupdDevice) dev2 = fwupd_device_new(); g_autoptr(FwupdDevice) dev_new = fwupd_device_new(); g_autoptr(FwupdRelease) rel = NULL; g_autoptr(GError) error = NULL; g_autoptr(GString) str_ascii = NULL; /* create dummy object */ dev = fwupd_device_new(); fwupd_device_add_checksum(dev, "beefdead"); fwupd_device_set_created(dev, 1); fwupd_device_add_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE); fwupd_device_set_id(dev, "0000000000000000000000000000000000000000"); fwupd_device_set_modified(dev, 60 * 60 * 24); fwupd_device_set_name(dev, "ColorHug2"); fwupd_device_set_branch(dev, "community"); fwupd_device_add_guid(dev, "2082b5e0-7a64-478a-b1b2-e3404fab6dad"); fwupd_device_add_guid(dev, "00000000-0000-0000-0000-000000000000"); fwupd_device_add_instance_id(dev, "USB\\VID_1234&PID_0001"); fwupd_device_add_icon(dev, "input-gaming"); fwupd_device_add_icon(dev, "input-mouse"); fwupd_device_add_vendor_id(dev, "USB:0x1234"); fwupd_device_add_vendor_id(dev, "PCI:0x5678"); fwupd_device_add_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE | FWUPD_DEVICE_FLAG_REQUIRE_AC); g_assert_true(fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_REQUIRE_AC)); g_assert_true(fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE)); g_assert_false(fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_HISTORICAL)); rel = fwupd_release_new(); fwupd_release_add_flag(rel, FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD); fwupd_release_add_checksum(rel, "deadbeef"); fwupd_release_set_description(rel, "

    Hi there!

    "); fwupd_release_set_filename(rel, "firmware.bin"); fwupd_release_set_appstream_id(rel, "org.dave.ColorHug.firmware"); fwupd_release_set_size(rel, 1024); fwupd_release_add_location(rel, "http://foo.com"); fwupd_release_add_location(rel, "ftp://foo.com"); fwupd_release_add_tag(rel, "vendor-2021q1"); fwupd_release_add_tag(rel, "vendor-2021q2"); fwupd_release_set_version(rel, "1.2.3"); fwupd_device_add_release(dev, rel); str = fwupd_codec_to_string(FWUPD_CODEC(dev)); g_print("\n%s", str); /* check GUIDs */ g_assert_true(fwupd_device_has_guid(dev, "2082b5e0-7a64-478a-b1b2-e3404fab6dad")); g_assert_true(fwupd_device_has_guid(dev, "00000000-0000-0000-0000-000000000000")); g_assert_false(fwupd_device_has_guid(dev, "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")); /* convert the new non-breaking space back into a normal space: * https://gitlab.gnome.org/GNOME/glib/commit/76af5dabb4a25956a6c41a75c0c7feeee74496da */ str_ascii = g_string_new(str); g_string_replace(str_ascii, " ", " ", 0); ret = fu_test_compare_lines( str_ascii->str, "FwupdDevice:\n" " DeviceId: 0000000000000000000000000000000000000000\n" " Name: ColorHug2\n" " Guid: 18f514d2-c12e-581f-a696-cc6d6c271699 " "↠USB\\VID_1234&PID_0001 ⚠\n" " Guid: 2082b5e0-7a64-478a-b1b2-e3404fab6dad\n" " Guid: 00000000-0000-0000-0000-000000000000\n" " Branch: community\n" " Flags: updatable|require-ac\n" " Checksum: SHA1(beefdead)\n" " VendorId: USB:0x1234\n" " VendorId: PCI:0x5678\n" " Icon: input-gaming,input-mouse\n" " Created: 1970-01-01\n" " Modified: 1970-01-02\n" " FwupdRelease:\n" " AppstreamId: org.dave.ColorHug.firmware\n" " Description:

    Hi there!

    \n" " Version: 1.2.3\n" " Filename: firmware.bin\n" " Checksum: SHA1(deadbeef)\n" " Tags: vendor-2021q1\n" " Tags: vendor-2021q2\n" " Size: 1.0 kB\n" " Uri: http://foo.com\n" " Uri: ftp://foo.com\n" " Flags: trusted-payload\n", &error); g_assert_no_error(error); g_assert_true(ret); /* export to json */ data = fwupd_codec_to_json_string(FWUPD_CODEC(dev), FWUPD_CODEC_FLAG_TRUSTED, &error); g_assert_no_error(error); g_assert_nonnull(data); ret = fu_test_compare_lines(data, "{\n" " \"Name\" : \"ColorHug2\",\n" " \"DeviceId\" : \"0000000000000000000000000000000000000000\",\n" " \"InstanceIds\" : [\n" " \"USB\\\\VID_1234&PID_0001\"\n" " ],\n" " \"Guid\" : [\n" " \"2082b5e0-7a64-478a-b1b2-e3404fab6dad\",\n" " \"00000000-0000-0000-0000-000000000000\"\n" " ],\n" " \"Branch\" : \"community\",\n" " \"Flags\" : [\n" " \"updatable\",\n" " \"require-ac\"\n" " ],\n" " \"Checksums\" : [\n" " \"beefdead\"\n" " ],\n" " \"VendorIds\" : [\n" " \"USB:0x1234\",\n" " \"PCI:0x5678\"\n" " ],\n" " \"Icons\" : [\n" " \"input-gaming\",\n" " \"input-mouse\"\n" " ],\n" " \"Created\" : 1,\n" " \"Modified\" : 86400,\n" " \"Releases\" : [\n" " {\n" " \"AppstreamId\" : \"org.dave.ColorHug.firmware\",\n" " \"Description\" : \"

    Hi there!

    \",\n" " \"Version\" : \"1.2.3\",\n" " \"Filename\" : \"firmware.bin\",\n" " \"Checksum\" : [\n" " \"deadbeef\"\n" " ],\n" " \"Tags\" : [\n" " \"vendor-2021q1\",\n" " \"vendor-2021q2\"\n" " ],\n" " \"Size\" : 1024,\n" " \"Locations\" : [\n" " \"http://foo.com\",\n" " \"ftp://foo.com\"\n" " ],\n" " \"Flags\" : [\n" " \"trusted-payload\"\n" " ]\n" " }\n" " ]\n" "}", &error); g_assert_no_error(error); g_assert_true(ret); /* incorporate */ fwupd_device_incorporate(dev_new, dev); g_assert_true(fwupd_device_has_vendor_id(dev_new, "USB:0x1234")); g_assert_true(fwupd_device_has_vendor_id(dev_new, "PCI:0x5678")); g_assert_true(fwupd_device_has_instance_id(dev_new, "USB\\VID_1234&PID_0001")); /* from JSON */ ret = fwupd_codec_from_json_string(FWUPD_CODEC(dev2), data, &error); if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_test_skip(error->message); return; } g_assert_no_error(error); g_assert_true(ret); g_assert_true(fwupd_device_has_vendor_id(dev2, "USB:0x1234")); g_assert_true(fwupd_device_has_instance_id(dev2, "USB\\VID_1234&PID_0001")); g_assert_true(fwupd_device_has_flag(dev2, FWUPD_DEVICE_FLAG_UPDATABLE)); g_assert_false(fwupd_device_has_flag(dev2, FWUPD_DEVICE_FLAG_LOCKED)); } static void fwupd_client_devices_func(void) { FwupdDevice *dev; gboolean ret; g_autoptr(FwupdClient) client = NULL; g_autoptr(GPtrArray) array = NULL; g_autoptr(GError) error = NULL; client = fwupd_client_new(); /* only run if running fwupd is new enough */ ret = fwupd_client_connect(client, NULL, &error); if (ret == FALSE && (g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_TIMED_OUT) || g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_NAME_HAS_NO_OWNER) || g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN))) { g_debug("%s", error->message); g_test_skip("timeout connecting to daemon"); return; } g_assert_no_error(error); g_assert_true(ret); if (fwupd_client_get_daemon_version(client) == NULL) { g_test_skip("no enabled fwupd daemon"); return; } if (!g_str_has_prefix(fwupd_client_get_daemon_version(client), "1.")) { g_test_skip("running fwupd is too old"); return; } array = fwupd_client_get_devices(client, NULL, &error); if (array == NULL && g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) { g_test_skip("no available fwupd devices"); return; } if (array == NULL && g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_test_skip("no available fwupd daemon"); return; } g_assert_no_error(error); g_assert_nonnull(array); g_assert_cmpint(array->len, >, 0); /* check device */ dev = g_ptr_array_index(array, 0); g_assert_true(FWUPD_IS_DEVICE(dev)); g_assert_cmpstr(fwupd_device_get_guid_default(dev), !=, NULL); g_assert_cmpstr(fwupd_device_get_id(dev), !=, NULL); } static void fwupd_client_remotes_func(void) { gboolean ret; g_autofree gchar *remotesdir = NULL; g_autoptr(FwupdClient) client = NULL; g_autoptr(FwupdRemote) remote2 = NULL; g_autoptr(FwupdRemote) remote3 = NULL; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) array = NULL; remotesdir = g_test_build_filename(G_TEST_DIST, "tests", "remotes.d", NULL); (void)g_setenv("FU_SELF_TEST_REMOTES_DIR", remotesdir, TRUE); client = fwupd_client_new(); /* only run if running fwupd is new enough */ ret = fwupd_client_connect(client, NULL, &error); if (ret == FALSE && (g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_TIMED_OUT) || g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_NAME_HAS_NO_OWNER) || g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN))) { g_debug("%s", error->message); g_test_skip("timeout connecting to daemon"); return; } g_assert_no_error(error); g_assert_true(ret); if (fwupd_client_get_daemon_version(client) == NULL) { g_test_skip("no enabled fwupd daemon"); return; } if (!g_str_has_prefix(fwupd_client_get_daemon_version(client), "1.")) { g_test_skip("running fwupd is too old"); return; } array = fwupd_client_get_remotes(client, NULL, &error); if (array == NULL && g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) { g_test_skip("no available fwupd remotes"); return; } if (array == NULL && g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_test_skip("no available fwupd daemon"); return; } g_assert_no_error(error); g_assert_nonnull(array); g_assert_cmpint(array->len, >, 0); /* check we can find the right thing */ remote2 = fwupd_client_get_remote_by_id(client, "lvfs", NULL, &error); g_assert_no_error(error); g_assert_nonnull(remote2); g_assert_cmpstr(fwupd_remote_get_id(remote2), ==, "lvfs"); g_assert_nonnull(fwupd_remote_get_metadata_uri(remote2)); /* check we set an error when unfound */ remote3 = fwupd_client_get_remote_by_id(client, "XXXX", NULL, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND); g_assert_null(remote3); } static gboolean fwupd_has_system_bus(void) { g_autoptr(GDBusConnection) conn = NULL; conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, NULL); if (conn != NULL) return TRUE; g_debug("D-Bus system bus unavailable, skipping tests."); return FALSE; } static void fwupd_common_device_id_func(void) { g_assert_false(fwupd_device_id_is_valid(NULL)); g_assert_false(fwupd_device_id_is_valid("")); g_assert_false(fwupd_device_id_is_valid("1ff60ab2-3905-06a1-b476-0371f00c9e9b")); g_assert_false(fwupd_device_id_is_valid("aaaaaad3fae86d95e5d56626129d00e332c4b8dac95442")); g_assert_false(fwupd_device_id_is_valid("x3fae86d95e5d56626129d00e332c4b8dac95442")); g_assert_false(fwupd_device_id_is_valid("D3FAE86D95E5D56626129D00E332C4B8DAC95442")); g_assert_false(fwupd_device_id_is_valid(FWUPD_DEVICE_ID_ANY)); g_assert_true(fwupd_device_id_is_valid("d3fae86d95e5d56626129d00e332c4b8dac95442")); } static void fwupd_common_guid_func(void) { const guint8 msbuf[] = "hello world!"; g_autofree gchar *guid1 = NULL; g_autofree gchar *guid2 = NULL; g_autofree gchar *guid3 = NULL; g_autofree gchar *guid_be = NULL; g_autofree gchar *guid_me = NULL; fwupd_guid_t buf = {0x0}; gboolean ret; g_autoptr(GError) error = NULL; /* invalid */ g_assert_false(fwupd_guid_is_valid(NULL)); g_assert_false(fwupd_guid_is_valid("")); g_assert_false(fwupd_guid_is_valid("1ff60ab2-3905-06a1-b476")); g_assert_false(fwupd_guid_is_valid("1ff60ab2-XXXX-XXXX-XXXX-0371f00c9e9b")); g_assert_false(fwupd_guid_is_valid("1ff60ab2-XXXX-XXXX-XXXX-0371f00c9e9bf")); g_assert_false(fwupd_guid_is_valid(" 1ff60ab2-3905-06a1-b476-0371f00c9e9b")); g_assert_false(fwupd_guid_is_valid("00000000-0000-0000-0000-000000000000")); /* valid */ g_assert_true(fwupd_guid_is_valid("1ff60ab2-3905-06a1-b476-0371f00c9e9b")); /* make valid */ guid1 = fwupd_guid_hash_string("python.org"); g_assert_cmpstr(guid1, ==, "886313e1-3b8a-5372-9b90-0c9aee199e5d"); guid2 = fwupd_guid_hash_string("8086:0406"); g_assert_cmpstr(guid2, ==, "1fbd1f2c-80f4-5d7c-a6ad-35c7b9bd5486"); guid3 = fwupd_guid_hash_data(msbuf, sizeof(msbuf), FWUPD_GUID_FLAG_NAMESPACE_MICROSOFT); g_assert_cmpstr(guid3, ==, "6836cfac-f77a-527f-b375-4f92f01449c5"); /* round-trip BE */ ret = fwupd_guid_from_string("00112233-4455-6677-8899-aabbccddeeff", &buf, FWUPD_GUID_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(memcmp(buf, "\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff", sizeof(buf)), ==, 0); guid_be = fwupd_guid_to_string((const fwupd_guid_t *)&buf, FWUPD_GUID_FLAG_NONE); g_assert_cmpstr(guid_be, ==, "00112233-4455-6677-8899-aabbccddeeff"); /* round-trip mixed encoding */ ret = fwupd_guid_from_string("00112233-4455-6677-8899-aabbccddeeff", &buf, FWUPD_GUID_FLAG_MIXED_ENDIAN, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(memcmp(buf, "\x33\x22\x11\x00\x55\x44\x77\x66\x88\x99\xaa\xbb\xcc\xdd\xee\xff", sizeof(buf)), ==, 0); guid_me = fwupd_guid_to_string((const fwupd_guid_t *)&buf, FWUPD_GUID_FLAG_MIXED_ENDIAN); g_assert_cmpstr(guid_me, ==, "00112233-4455-6677-8899-aabbccddeeff"); /* check failure */ g_assert_false( fwupd_guid_from_string("001122334455-6677-8899-aabbccddeeff", NULL, 0, NULL)); g_assert_false( fwupd_guid_from_string("0112233-4455-6677-8899-aabbccddeeff", NULL, 0, NULL)); } static void fwupd_security_attr_func(void) { gboolean ret; g_autofree gchar *str1 = NULL; g_autofree gchar *str2 = NULL; g_autofree gchar *str3 = NULL; g_autofree gchar *json = NULL; g_autoptr(FwupdSecurityAttr) attr1 = fwupd_security_attr_new("org.fwupd.hsi.bar"); g_autoptr(FwupdSecurityAttr) attr2 = fwupd_security_attr_new(NULL); g_autoptr(FwupdSecurityAttr) attr3 = fwupd_security_attr_new(NULL); g_autoptr(GError) error = NULL; g_autoptr(GVariant) data = NULL; for (guint i = 1; i < FWUPD_SECURITY_ATTR_RESULT_LAST; i++) { const gchar *tmp = fwupd_security_attr_result_to_string(i); g_assert_cmpstr(tmp, !=, NULL); g_assert_cmpint(fwupd_security_attr_result_from_string(tmp), ==, i); } g_assert_cmpstr(fwupd_security_attr_get_appstream_id(attr1), ==, "org.fwupd.hsi.bar"); fwupd_security_attr_set_appstream_id(attr1, "org.fwupd.hsi.baz"); fwupd_security_attr_set_fwupd_version(attr1, "2.0.7"); g_assert_cmpstr(fwupd_security_attr_get_appstream_id(attr1), ==, "org.fwupd.hsi.baz"); fwupd_security_attr_set_level(attr1, FWUPD_SECURITY_ATTR_LEVEL_IMPORTANT); g_assert_cmpint(fwupd_security_attr_get_level(attr1), ==, FWUPD_SECURITY_ATTR_LEVEL_IMPORTANT); fwupd_security_attr_set_result(attr1, FWUPD_SECURITY_ATTR_RESULT_ENABLED); g_assert_cmpint(fwupd_security_attr_get_result(attr1), ==, FWUPD_SECURITY_ATTR_RESULT_ENABLED); fwupd_security_attr_add_flag(attr1, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); fwupd_security_attr_add_flag(attr1, FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA); fwupd_security_attr_remove_flag(attr1, FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA); g_assert_true(fwupd_security_attr_has_flag(attr1, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)); g_assert_false(fwupd_security_attr_has_flag(attr1, FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA)); g_assert_false(fwupd_security_attr_has_flag(attr1, FWUPD_SECURITY_ATTR_FLAG_OBSOLETED)); fwupd_security_attr_set_name(attr1, "DCI"); g_assert_cmpstr(fwupd_security_attr_get_name(attr1), ==, "DCI"); fwupd_security_attr_set_plugin(attr1, "uefi-capsule"); g_assert_cmpstr(fwupd_security_attr_get_plugin(attr1), ==, "uefi-capsule"); g_assert_cmpstr(fwupd_security_attr_get_fwupd_version(attr1), ==, "2.0.7"); fwupd_security_attr_set_url(attr1, "https://foo.bar"); g_assert_cmpstr(fwupd_security_attr_get_url(attr1), ==, "https://foo.bar"); fwupd_security_attr_add_guid(attr1, "af3fc12c-d090-5783-8a67-845b90d3cfec"); g_assert_true(fwupd_security_attr_has_guid(attr1, "af3fc12c-d090-5783-8a67-845b90d3cfec")); g_assert_false(fwupd_security_attr_has_guid(attr1, "NOT_GOING_TO_EXIST")); fwupd_security_attr_add_metadata(attr1, "KEY", "VALUE"); g_assert_cmpstr(fwupd_security_attr_get_metadata(attr1, "KEY"), ==, "VALUE"); /* remove this from the output */ fwupd_security_attr_set_created(attr1, 0); str1 = fwupd_codec_to_string(FWUPD_CODEC(attr1)); ret = fu_test_compare_lines(str1, "FwupdSecurityAttr:\n" " AppstreamId: org.fwupd.hsi.baz\n" " HsiLevel: 2\n" " HsiResult: enabled\n" " Flags: success\n" " Name: DCI\n" " Plugin: uefi-capsule\n" " Version: 2.0.7\n" " Uri: https://foo.bar\n" " Guid: af3fc12c-d090-5783-8a67-845b90d3cfec\n" " KEY: VALUE\n", &error); g_assert_no_error(error); g_assert_true(ret); /* roundtrip GVariant */ data = fwupd_codec_to_variant(FWUPD_CODEC(attr1), FWUPD_CODEC_FLAG_NONE); ret = fwupd_codec_from_variant(FWUPD_CODEC(attr3), data, &error); g_assert_no_error(error); g_assert_true(ret); fwupd_security_attr_set_created(attr3, 0); str3 = fwupd_codec_to_string(FWUPD_CODEC(attr3)); ret = fu_test_compare_lines(str3, "FwupdSecurityAttr:\n" " AppstreamId: org.fwupd.hsi.baz\n" " HsiLevel: 2\n" " HsiResult: enabled\n" " Flags: success\n" " Name: DCI\n" " Plugin: uefi-capsule\n" " Version: 2.0.7\n" " Uri: https://foo.bar\n" " Guid: af3fc12c-d090-5783-8a67-845b90d3cfec\n" " KEY: VALUE\n", &error); g_assert_no_error(error); g_assert_true(ret); /* to JSON */ json = fwupd_codec_to_json_string(FWUPD_CODEC(attr1), FWUPD_CODEC_FLAG_NONE, &error); g_assert_no_error(error); g_assert_nonnull(json); ret = fu_test_compare_lines(json, "{\n" " \"AppstreamId\" : \"org.fwupd.hsi.baz\",\n" " \"HsiLevel\" : 2,\n" " \"HsiResult\" : \"enabled\",\n" " \"Name\" : \"DCI\",\n" " \"Plugin\" : \"uefi-capsule\",\n" " \"Version\" : \"2.0.7\",\n" " \"Uri\" : \"https://foo.bar\",\n" " \"Flags\" : [\n" " \"success\"\n" " ],\n" " \"Guid\" : [\n" " \"af3fc12c-d090-5783-8a67-845b90d3cfec\"\n" " ],\n" " \"KEY\" : \"VALUE\"\n" "}", &error); g_assert_no_error(error); g_assert_true(ret); /* from JSON */ ret = fwupd_codec_from_json_string(FWUPD_CODEC(attr2), json, &error); if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_test_skip(error->message); return; } g_assert_no_error(error); g_assert_true(ret); /* we don't load unconditionally load metadata from the JSON */ fwupd_security_attr_add_metadata(attr2, "KEY", "VALUE"); str2 = fwupd_codec_to_string(FWUPD_CODEC(attr2)); ret = fu_test_compare_lines(str2, str1, &error); g_assert_no_error(error); g_assert_true(ret); } static void fwupd_bios_settings_func(void) { gboolean ret; g_autofree gchar *str1 = NULL; g_autofree gchar *str2 = NULL; g_autofree gchar *str3 = NULL; g_autofree gchar *str4 = NULL; g_autofree gchar *json1 = NULL; g_autofree gchar *json2 = NULL; g_autoptr(FwupdBiosSetting) attr1 = fwupd_bios_setting_new("foo", "/path/to/bar"); g_autoptr(FwupdBiosSetting) attr2 = fwupd_bios_setting_new(NULL, NULL); g_autoptr(FwupdBiosSetting) attr3 = fwupd_bios_setting_new(NULL, NULL); g_autoptr(GError) error = NULL; g_autoptr(GVariant) data1 = NULL; g_autoptr(GVariant) data2 = NULL; g_assert_cmpstr(fwupd_bios_setting_get_name(attr1), ==, "foo"); fwupd_bios_setting_set_name(attr1, "UEFISecureBoot"); g_assert_cmpstr(fwupd_bios_setting_get_name(attr1), ==, "UEFISecureBoot"); fwupd_bios_setting_set_kind(attr1, FWUPD_BIOS_SETTING_KIND_ENUMERATION); g_assert_cmpint(fwupd_bios_setting_get_kind(attr1), ==, FWUPD_BIOS_SETTING_KIND_ENUMERATION); fwupd_bios_setting_set_description(attr1, "Controls Secure boot"); g_assert_cmpstr(fwupd_bios_setting_get_description(attr1), ==, "Controls Secure boot"); fwupd_bios_setting_set_current_value(attr1, "Disabled"); g_assert_cmpstr(fwupd_bios_setting_get_current_value(attr1), ==, "Disabled"); fwupd_bios_setting_add_possible_value(attr1, "Disabled"); fwupd_bios_setting_add_possible_value(attr1, "Enabled"); g_assert_true(fwupd_bios_setting_has_possible_value(attr1, "Disabled")); g_assert_false(fwupd_bios_setting_has_possible_value(attr1, "NOT_GOING_TO_EXIST")); str1 = fwupd_codec_to_string(FWUPD_CODEC(attr1)); ret = fu_test_compare_lines(str1, "FwupdBiosSetting:\n" " Name: UEFISecureBoot\n" " Description: Controls Secure boot\n" " Filename: /path/to/bar\n" " BiosSettingType: 1\n" " BiosSettingCurrentValue: Disabled\n" " BiosSettingReadOnly: False\n" " BiosSettingPossibleValues: Disabled\n" " BiosSettingPossibleValues: Enabled\n", &error); g_assert_no_error(error); g_assert_true(ret); /* roundtrip GVariant */ data1 = fwupd_codec_to_variant(FWUPD_CODEC(attr1), FWUPD_CODEC_FLAG_TRUSTED); ret = fwupd_codec_from_variant(FWUPD_CODEC(attr2), data1, &error); g_assert_no_error(error); g_assert_true(ret); str2 = fwupd_codec_to_string(FWUPD_CODEC(attr2)); ret = fu_test_compare_lines(str2, "FwupdBiosSetting:\n" " Name: UEFISecureBoot\n" " Description: Controls Secure boot\n" " Filename: /path/to/bar\n" " BiosSettingType: 1\n" " BiosSettingCurrentValue: Disabled\n" " BiosSettingReadOnly: False\n" " BiosSettingPossibleValues: Disabled\n" " BiosSettingPossibleValues: Enabled\n", &error); g_assert_no_error(error); g_assert_true(ret); /* to JSON */ json1 = fwupd_codec_to_json_string(FWUPD_CODEC(attr1), FWUPD_CODEC_FLAG_NONE, &error); g_assert_no_error(error); g_assert_nonnull(json1); ret = fu_test_compare_lines(json1, "{\n" " \"Name\" : \"UEFISecureBoot\",\n" " \"Description\" : \"Controls Secure boot\",\n" " \"Filename\" : \"/path/to/bar\",\n" " \"BiosSettingCurrentValue\" : \"Disabled\",\n" " \"BiosSettingReadOnly\" : false,\n" " \"BiosSettingType\" : 1,\n" " \"BiosSettingPossibleValues\" : [\n" " \"Disabled\",\n" " \"Enabled\"\n" " ]\n" "}", &error); g_assert_no_error(error); g_assert_true(ret); /* from JSON */ ret = fwupd_codec_from_json_string(FWUPD_CODEC(attr2), json1, &error); if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_test_skip(error->message); return; } g_assert_no_error(error); g_assert_true(ret); str3 = fwupd_codec_to_string(FWUPD_CODEC(attr2)); ret = fu_test_compare_lines(str3, str1, &error); g_assert_no_error(error); g_assert_true(ret); /* make sure we filter CurrentValue if not trusted */ data2 = fwupd_codec_to_variant(FWUPD_CODEC(attr1), FWUPD_CODEC_FLAG_NONE); ret = fwupd_codec_from_variant(FWUPD_CODEC(attr3), data2, &error); g_assert_no_error(error); g_assert_true(ret); str4 = fwupd_codec_to_string(FWUPD_CODEC(attr3)); ret = fu_test_compare_lines(str4, "FwupdBiosSetting:\n" " Name: UEFISecureBoot\n" " Description: Controls Secure boot\n" " Filename: /path/to/bar\n" " BiosSettingType: 1\n" " BiosSettingReadOnly: False\n" " BiosSettingPossibleValues: Disabled\n" " BiosSettingPossibleValues: Enabled\n", &error); g_assert_no_error(error); g_assert_true(ret); /* convert to JSON */ json2 = fwupd_codec_to_json_string(FWUPD_CODEC(attr1), FWUPD_CODEC_FLAG_NONE, &error); g_assert_no_error(error); g_assert_nonnull(json2); ret = fu_test_compare_lines(json2, "{\n" " \"Name\" : \"UEFISecureBoot\",\n" " \"Description\" : \"Controls Secure boot\",\n" " \"Filename\" : \"/path/to/bar\",\n" " \"BiosSettingCurrentValue\" : \"Disabled\",\n" " \"BiosSettingReadOnly\" : false,\n" " \"BiosSettingType\" : 1,\n" " \"BiosSettingPossibleValues\" : [\n" " \"Disabled\",\n" " \"Enabled\"\n" " ]\n" "}", &error); g_assert_no_error(error); g_assert_true(ret); } int main(int argc, char **argv) { g_autofree gchar *testdatadir = NULL; setlocale(LC_ALL, ""); (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); (void)g_setenv("G_MESSAGES_DEBUG", "all", TRUE); testdatadir = g_test_build_filename(G_TEST_DIST, "tests", NULL); (void)g_setenv("FWUPD_SYSCONFDIR", testdatadir, TRUE); g_assert_cmpint(sizeof(FwupdDeviceFlags), ==, sizeof(guint64)); g_assert_cmpint(sizeof(FwupdStatus), ==, sizeof(guint32)); /* tests go here */ g_test_add_func("/fwupd/enums", fwupd_enums_func); g_test_add_func("/fwupd/common{device-id}", fwupd_common_device_id_func); g_test_add_func("/fwupd/common{guid}", fwupd_common_guid_func); g_test_add_func("/fwupd/common{history-report}", fwupd_common_history_report_func); g_test_add_func("/fwupd/release", fwupd_release_func); g_test_add_func("/fwupd/report", fwupd_report_func); g_test_add_func("/fwupd/plugin", fwupd_plugin_func); g_test_add_func("/fwupd/remote", fwupd_remote_func); g_test_add_func("/fwupd/request", fwupd_request_func); g_test_add_func("/fwupd/device", fwupd_device_func); g_test_add_func("/fwupd/device{filter}", fwupd_device_filter_func); g_test_add_func("/fwupd/security-attr", fwupd_security_attr_func); g_test_add_func("/fwupd/bios-attrs", fwupd_bios_settings_func); g_test_add_func("/fwupd/client_api", fwupd_client_api); if (g_test_undefined()) { g_test_add_func("/fwupd/client_api{undefined_setter}", fwupd_client_api_undefined_setter); g_test_add_func("/fwupd/client_api{undefined_getter}", fwupd_client_api_undefined_getter); g_test_add_func("/fwupd/client_api{ro_props}", fwupd_client_api_ro_props); } if (fwupd_has_system_bus()) { g_test_add_func("/fwupd/client{remotes}", fwupd_client_remotes_func); g_test_add_func("/fwupd/client{devices}", fwupd_client_devices_func); } return g_test_run(); } fwupd-2.0.10/libfwupd/fwupd-thread-test.c000066400000000000000000000061161501337203100202450ustar00rootroot00000000000000/* * Copyright 2020 Philip Withnall * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include typedef struct { GApplication *app; FwupdClient *client; GPtrArray *worker_threads; } FuThreadTestSelf; static gboolean fwupd_thread_test_exit_idle_cb(gpointer user_data) { FuThreadTestSelf *self = user_data; g_application_release(self->app); return G_SOURCE_REMOVE; } static gpointer fwupd_thread_test_thread_cb(gpointer user_data) { FuThreadTestSelf *self = user_data; g_autoptr(GError) error_local = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(GMainContext) context = g_main_context_new(); g_autoptr(GMainContextPusher) pusher = g_main_context_pusher_new(context); g_assert_nonnull(pusher); g_message("Calling fwupd_client_get_devices() in thread %p with main context %p", g_thread_self(), g_main_context_get_thread_default()); devices = fwupd_client_get_devices(self->client, NULL, &error_local); if (devices == NULL) g_warning("%s", error_local->message); g_idle_add(fwupd_thread_test_exit_idle_cb, self); return NULL; } static gboolean fwupd_thread_test_idle_cb(gpointer user_data) { FuThreadTestSelf *self = user_data; g_message("fwupd_thread_test_idle_cb() in thread %p with main context %p", g_thread_self(), g_main_context_get_thread_default()); /* create 'n' threads with a small delay, and 'n-1' references on the app */ for (guint i = 0; i < 30; i++) { g_autofree gchar *thread_str = g_strdup_printf("worker%02u", i); GThread *thread = g_thread_new(thread_str, fwupd_thread_test_thread_cb, self); g_usleep(g_random_int_range(0, 1000)); /* nocheck:blocked */ g_ptr_array_add(self->worker_threads, thread); if (i > 0) g_application_hold(self->app); } return G_SOURCE_REMOVE; } static void fwupd_thread_test_activate_cb(GApplication *app, gpointer user_data) { FuThreadTestSelf *self = user_data; g_application_hold(self->app); g_idle_add(fwupd_thread_test_idle_cb, self); } static gboolean fwupd_thread_test_has_system_bus(void) { g_autoptr(GDBusConnection) conn = NULL; conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, NULL); return conn != NULL; } int main(void) { gint retval; g_autoptr(FwupdClient) client = fwupd_client_new(); g_autoptr(GApplication) app = g_application_new("org.test.Test", G_APPLICATION_NON_UNIQUE); g_autoptr(GPtrArray) worker_threads = g_ptr_array_new(); FuThreadTestSelf self = {app, client, worker_threads}; /* only some of the CI targets have a DBus daemon */ if (!fwupd_thread_test_has_system_bus()) { g_message("D-Bus system bus unavailable, skipping tests."); return 0; } g_message("Created FwupdClient in thread %p with main context %p", g_thread_self(), g_main_context_get_thread_default()); g_signal_connect(G_APPLICATION(app), "activate", G_CALLBACK(fwupd_thread_test_activate_cb), &self); retval = g_application_run(app, 0, NULL); for (guint i = 0; i < self.worker_threads->len; i++) { GThread *thread = g_ptr_array_index(self.worker_threads, i); g_thread_join(thread); } return retval; } fwupd-2.0.10/libfwupd/fwupd-version.c000066400000000000000000000011041501337203100174760ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fwupd-version.h" /** * fwupd_version_string: * * Gets the libfwupd installed runtime version. * * This may be different to the *build-time* version if the daemon and library * objects somehow get out of sync. * * Returns: version string * * Since: 1.6.1 **/ const gchar * fwupd_version_string(void) { return G_STRINGIFY(FWUPD_MAJOR_VERSION) "." G_STRINGIFY( FWUPD_MINOR_VERSION) "." G_STRINGIFY(FWUPD_MICRO_VERSION); } fwupd-2.0.10/libfwupd/fwupd-version.h.in000066400000000000000000000026561501337203100201250ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #if !defined(__FWUPD_H_INSIDE__) && !defined(FWUPD_COMPILATION) #error "Only can be included directly." #endif /* clang-format off */ /** * FWUPD_MAJOR_VERSION: * * The compile-time major version */ #define FWUPD_MAJOR_VERSION @MAJOR_VERSION@ /** * FWUPD_MINOR_VERSION: * * The compile-time minor version */ #define FWUPD_MINOR_VERSION @MINOR_VERSION@ /** * FWUPD_MICRO_VERSION: * * The compile-time micro version */ #define FWUPD_MICRO_VERSION @MICRO_VERSION@ /* clang-format on */ /** * FWUPD_CHECK_VERSION: * @major: Major version number * @minor: Minor version number * @micro: Micro version number * * Check whether a fwupd version equal to or greater than * major.minor.micro. * * These compile time macros allow the user to enable parts of client code * depending on the version of libfwupd installed. */ #define FWUPD_CHECK_VERSION(major, minor, micro) \ (FWUPD_MAJOR_VERSION > major || \ (FWUPD_MAJOR_VERSION == major && FWUPD_MINOR_VERSION > minor) || \ (FWUPD_MAJOR_VERSION == major && FWUPD_MINOR_VERSION == minor && \ FWUPD_MICRO_VERSION >= micro)) const gchar * fwupd_version_string(void); fwupd-2.0.10/libfwupd/fwupd.h000066400000000000000000000013661501337203100160320ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #define __FWUPD_H_INSIDE__ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #undef __FWUPD_H_INSIDE__ fwupd-2.0.10/libfwupd/fwupd.map000066400000000000000000000631221501337203100163560ustar00rootroot00000000000000# generated automatically, do not edit! LIBFWUPD_0.1.1 { global: fwupd_error_quark; fwupd_status_from_string; fwupd_status_to_string; local: *; }; LIBFWUPD_0.7.0 { global: fwupd_client_clear_results; fwupd_client_get_results; fwupd_client_get_type; fwupd_client_install; fwupd_client_new; fwupd_client_unlock; fwupd_client_verify; fwupd_device_flag_from_string; fwupd_device_flag_to_string; fwupd_error_from_string; fwupd_error_to_string; fwupd_update_state_from_string; fwupd_update_state_to_string; local: *; } LIBFWUPD_0.1.1; LIBFWUPD_0.7.1 { global: fwupd_client_connect; local: *; } LIBFWUPD_0.7.0; LIBFWUPD_0.7.3 { global: fwupd_client_get_percentage; fwupd_client_get_status; local: *; } LIBFWUPD_0.7.1; LIBFWUPD_0.8.0 { global: fwupd_client_verify_update; local: *; } LIBFWUPD_0.7.3; LIBFWUPD_0.9.2 { global: fwupd_client_get_devices; local: *; } LIBFWUPD_0.8.0; LIBFWUPD_0.9.3 { global: fwupd_checksum_format_for_display; fwupd_checksum_guess_kind; fwupd_client_get_device_by_id; fwupd_client_get_releases; fwupd_client_get_remote_by_id; fwupd_client_get_remotes; fwupd_device_add_checksum; fwupd_device_add_flag; fwupd_device_add_guid; fwupd_device_get_checksums; fwupd_device_get_created; fwupd_device_get_flags; fwupd_device_get_flashes_left; fwupd_device_get_guid_default; fwupd_device_get_guids; fwupd_device_get_id; fwupd_device_get_modified; fwupd_device_get_name; fwupd_device_get_summary; fwupd_device_get_type; fwupd_device_get_vendor; fwupd_device_get_version; fwupd_device_get_version_bootloader; fwupd_device_get_version_lowest; fwupd_device_has_flag; fwupd_device_has_guid; fwupd_device_new; fwupd_device_remove_flag; fwupd_device_set_created; fwupd_device_set_flags; fwupd_device_set_flashes_left; fwupd_device_set_id; fwupd_device_set_modified; fwupd_device_set_name; fwupd_device_set_summary; fwupd_device_set_vendor; fwupd_device_set_version; fwupd_device_set_version_bootloader; fwupd_device_set_version_lowest; fwupd_release_add_checksum; fwupd_release_get_appstream_id; fwupd_release_get_checksums; fwupd_release_get_description; fwupd_release_get_filename; fwupd_release_get_homepage; fwupd_release_get_license; fwupd_release_get_name; fwupd_release_get_remote_id; fwupd_release_get_size; fwupd_release_get_summary; fwupd_release_get_type; fwupd_release_get_vendor; fwupd_release_get_version; fwupd_release_new; fwupd_release_set_appstream_id; fwupd_release_set_description; fwupd_release_set_filename; fwupd_release_set_homepage; fwupd_release_set_license; fwupd_release_set_name; fwupd_release_set_remote_id; fwupd_release_set_size; fwupd_release_set_summary; fwupd_release_set_vendor; fwupd_release_set_version; fwupd_remote_get_id; fwupd_remote_get_type; fwupd_remote_new; local: *; } LIBFWUPD_0.9.2; LIBFWUPD_0.9.4 { global: fwupd_checksum_get_best; fwupd_checksum_get_by_kind; local: *; } LIBFWUPD_0.9.3; LIBFWUPD_0.9.5 { global: fwupd_remote_get_age; fwupd_remote_get_order_after; fwupd_remote_get_order_before; fwupd_remote_get_password; fwupd_remote_get_priority; fwupd_remote_get_username; fwupd_remote_set_mtime; fwupd_remote_set_priority; local: *; } LIBFWUPD_0.9.4; LIBFWUPD_0.9.6 { global: fwupd_client_get_daemon_version; fwupd_remote_get_filename_cache; fwupd_remote_get_kind; fwupd_remote_kind_from_string; fwupd_remote_kind_to_string; local: *; } LIBFWUPD_0.9.5; LIBFWUPD_0.9.7 { global: fwupd_remote_build_firmware_uri; fwupd_remote_get_filename_cache_sig; fwupd_remote_get_metadata_uri; fwupd_remote_get_metadata_uri_sig; local: *; } LIBFWUPD_0.9.6; LIBFWUPD_0.9.8 { global: fwupd_client_get_downgrades; fwupd_client_get_upgrades; fwupd_client_modify_remote; fwupd_device_add_icon; fwupd_device_add_release; fwupd_device_get_icons; fwupd_device_get_release_default; fwupd_device_get_releases; fwupd_device_get_update_error; fwupd_device_get_update_state; fwupd_device_set_update_error; fwupd_device_set_update_state; fwupd_remote_get_filename_source; fwupd_remote_get_title; local: *; } LIBFWUPD_0.9.7; LIBFWUPD_1.0.0 { global: fwupd_client_get_details; fwupd_client_update_metadata; fwupd_device_get_plugin; fwupd_device_set_plugin; fwupd_remote_get_checksum; local: *; } LIBFWUPD_0.9.8; LIBFWUPD_1.0.4 { global: fwupd_client_get_history; fwupd_client_modify_device; fwupd_release_add_metadata; fwupd_release_add_metadata_item; fwupd_release_get_metadata; fwupd_release_get_metadata_item; fwupd_remote_get_report_uri; local: *; } LIBFWUPD_1.0.0; LIBFWUPD_1.0.7 { global: fwupd_remote_get_agreement; fwupd_remote_set_agreement; local: *; } LIBFWUPD_1.0.4; LIBFWUPD_1.0.8 { global: fwupd_device_get_parent; fwupd_device_get_parent_id; fwupd_device_set_parent; fwupd_device_set_parent_id; local: *; } LIBFWUPD_1.0.7; LIBFWUPD_1.1.0 { global: fwupd_device_incorporate; local: *; } LIBFWUPD_1.0.8; LIBFWUPD_1.1.1 { global: fwupd_device_compare; local: *; } LIBFWUPD_1.1.0; LIBFWUPD_1.1.2 { global: fwupd_device_get_serial; fwupd_device_set_serial; local: *; } LIBFWUPD_1.1.1; LIBFWUPD_1.1.3 { global: fwupd_device_get_install_duration; fwupd_device_set_install_duration; local: *; } LIBFWUPD_1.1.2; LIBFWUPD_1.2.1 { global: fwupd_release_get_install_duration; fwupd_release_set_install_duration; local: *; } LIBFWUPD_1.1.3; LIBFWUPD_1.2.2 { global: fwupd_release_get_protocol; fwupd_release_set_protocol; local: *; } LIBFWUPD_1.2.1; LIBFWUPD_1.2.4 { global: fwupd_client_get_tainted; fwupd_release_get_details_url; fwupd_release_get_source_url; fwupd_release_get_update_message; fwupd_release_set_details_url; fwupd_release_set_source_url; fwupd_release_set_update_message; local: *; } LIBFWUPD_1.2.2; LIBFWUPD_1.2.5 { global: fwupd_device_add_instance_id; fwupd_device_get_instance_ids; fwupd_device_has_instance_id; fwupd_guid_from_string; fwupd_guid_hash_data; fwupd_guid_hash_string; fwupd_guid_is_valid; fwupd_guid_to_string; local: *; } LIBFWUPD_1.2.4; LIBFWUPD_1.2.6 { global: fwupd_client_activate; fwupd_client_get_approved_firmware; fwupd_client_self_sign; fwupd_client_set_approved_firmware; fwupd_release_add_flag; fwupd_release_flag_from_string; fwupd_release_flag_to_string; fwupd_release_get_flags; fwupd_release_has_checksum; fwupd_release_has_flag; fwupd_release_remove_flag; fwupd_release_set_flags; local: *; } LIBFWUPD_1.2.5; LIBFWUPD_1.2.7 { global: fwupd_release_add_category; fwupd_release_get_categories; fwupd_release_has_category; local: *; } LIBFWUPD_1.2.6; LIBFWUPD_1.2.9 { global: fwupd_device_get_version_format; fwupd_device_set_version_format; fwupd_version_format_from_string; fwupd_version_format_to_string; local: *; } LIBFWUPD_1.2.7; LIBFWUPD_1.3.1 { global: fwupd_client_get_host_product; fwupd_remote_get_remotes_dir; fwupd_remote_set_remotes_dir; local: *; } LIBFWUPD_1.2.9; LIBFWUPD_1.3.2 { global: fwupd_client_get_host_machine_id; fwupd_release_add_issue; fwupd_release_get_issues; fwupd_release_get_name_variant_suffix; fwupd_release_set_name_variant_suffix; local: *; } LIBFWUPD_1.3.1; LIBFWUPD_1.3.3 { global: fwupd_release_get_detach_caption; fwupd_release_get_detach_image; fwupd_release_set_detach_caption; fwupd_release_set_detach_image; local: *; } LIBFWUPD_1.3.2; LIBFWUPD_1.3.4 { global: fwupd_client_get_daemon_interactive; local: *; } LIBFWUPD_1.3.3; LIBFWUPD_1.3.6 { global: fwupd_device_get_version_raw; fwupd_device_set_version_raw; local: *; } LIBFWUPD_1.3.4; LIBFWUPD_1.3.7 { global: fwupd_device_array_ensure_parents; fwupd_device_get_children; local: *; } LIBFWUPD_1.3.6; LIBFWUPD_1.4.0 { global: fwupd_device_get_status; fwupd_device_get_version_bootloader_raw; fwupd_device_get_version_lowest_raw; fwupd_device_set_status; fwupd_device_set_version_bootloader_raw; fwupd_device_set_version_lowest_raw; fwupd_release_get_created; fwupd_release_get_urgency; fwupd_release_set_created; fwupd_release_set_urgency; fwupd_release_urgency_from_string; fwupd_release_urgency_to_string; fwupd_remote_load_signature; local: *; } LIBFWUPD_1.3.7; LIBFWUPD_1.4.1 { global: fwupd_client_get_devices_by_guid; fwupd_device_id_is_valid; local: *; } LIBFWUPD_1.4.0; LIBFWUPD_1.4.5 { global: fwupd_client_download_bytes; fwupd_client_ensure_networking; fwupd_client_install_bytes; fwupd_client_set_feature_flags; fwupd_client_set_user_agent; fwupd_client_set_user_agent_for_package; fwupd_client_update_metadata_bytes; fwupd_client_upload_bytes; fwupd_feature_flag_from_string; fwupd_feature_flag_to_string; fwupd_release_get_update_image; fwupd_release_set_update_image; fwupd_remote_load_signature_bytes; local: *; } LIBFWUPD_1.4.1; LIBFWUPD_1.4.6 { global: fwupd_client_get_blocked_firmware; fwupd_client_set_blocked_firmware; local: *; } LIBFWUPD_1.4.5; LIBFWUPD_1.5.0 { global: fwupd_client_activate_async; fwupd_client_activate_finish; fwupd_client_clear_results_async; fwupd_client_clear_results_finish; fwupd_client_connect_async; fwupd_client_connect_finish; fwupd_client_download_bytes_async; fwupd_client_download_bytes_finish; fwupd_client_get_approved_firmware_async; fwupd_client_get_approved_firmware_finish; fwupd_client_get_blocked_firmware_async; fwupd_client_get_blocked_firmware_finish; fwupd_client_get_details_bytes; fwupd_client_get_details_bytes_async; fwupd_client_get_details_bytes_finish; fwupd_client_get_device_by_id_async; fwupd_client_get_device_by_id_finish; fwupd_client_get_devices_async; fwupd_client_get_devices_by_guid_async; fwupd_client_get_devices_by_guid_finish; fwupd_client_get_devices_finish; fwupd_client_get_downgrades_async; fwupd_client_get_downgrades_finish; fwupd_client_get_history_async; fwupd_client_get_history_finish; fwupd_client_get_host_security_attrs; fwupd_client_get_host_security_attrs_async; fwupd_client_get_host_security_attrs_finish; fwupd_client_get_host_security_id; fwupd_client_get_plugins; fwupd_client_get_plugins_async; fwupd_client_get_plugins_finish; fwupd_client_get_releases_async; fwupd_client_get_releases_finish; fwupd_client_get_remote_by_id_async; fwupd_client_get_remote_by_id_finish; fwupd_client_get_remotes_async; fwupd_client_get_remotes_finish; fwupd_client_get_report_metadata; fwupd_client_get_report_metadata_async; fwupd_client_get_report_metadata_finish; fwupd_client_get_results_async; fwupd_client_get_results_finish; fwupd_client_get_upgrades_async; fwupd_client_get_upgrades_finish; fwupd_client_install_async; fwupd_client_install_bytes_async; fwupd_client_install_bytes_finish; fwupd_client_install_finish; fwupd_client_install_release_finish; fwupd_client_modify_device_async; fwupd_client_modify_device_finish; fwupd_client_modify_remote_async; fwupd_client_modify_remote_finish; fwupd_client_refresh_remote_finish; fwupd_client_self_sign_async; fwupd_client_self_sign_finish; fwupd_client_set_approved_firmware_async; fwupd_client_set_approved_firmware_finish; fwupd_client_set_blocked_firmware_async; fwupd_client_set_blocked_firmware_finish; fwupd_client_set_feature_flags_async; fwupd_client_set_feature_flags_finish; fwupd_client_unlock_async; fwupd_client_unlock_finish; fwupd_client_update_metadata_bytes_async; fwupd_client_update_metadata_bytes_finish; fwupd_client_upload_bytes_async; fwupd_client_upload_bytes_finish; fwupd_client_verify_async; fwupd_client_verify_finish; fwupd_client_verify_update_async; fwupd_client_verify_update_finish; fwupd_device_get_branch; fwupd_device_set_branch; fwupd_plugin_add_flag; fwupd_plugin_flag_from_string; fwupd_plugin_flag_to_string; fwupd_plugin_get_flags; fwupd_plugin_get_name; fwupd_plugin_get_type; fwupd_plugin_has_flag; fwupd_plugin_new; fwupd_plugin_remove_flag; fwupd_plugin_set_flags; fwupd_plugin_set_name; fwupd_release_get_branch; fwupd_release_set_branch; fwupd_security_attr_add_flag; fwupd_security_attr_add_metadata; fwupd_security_attr_add_obsolete; fwupd_security_attr_flag_to_string; fwupd_security_attr_flag_to_suffix; fwupd_security_attr_get_appstream_id; fwupd_security_attr_get_flags; fwupd_security_attr_get_level; fwupd_security_attr_get_metadata; fwupd_security_attr_get_name; fwupd_security_attr_get_obsoletes; fwupd_security_attr_get_plugin; fwupd_security_attr_get_result; fwupd_security_attr_get_type; fwupd_security_attr_get_url; fwupd_security_attr_has_flag; fwupd_security_attr_has_obsolete; fwupd_security_attr_new; fwupd_security_attr_result_to_string; fwupd_security_attr_set_appstream_id; fwupd_security_attr_set_flags; fwupd_security_attr_set_level; fwupd_security_attr_set_name; fwupd_security_attr_set_plugin; fwupd_security_attr_set_result; fwupd_security_attr_set_url; local: *; } LIBFWUPD_1.4.6; LIBFWUPD_1.5.1 { global: fwupd_device_add_child; local: *; } LIBFWUPD_1.5.0; LIBFWUPD_1.5.2 { global: fwupd_client_download_file; fwupd_client_get_user_agent; local: *; } LIBFWUPD_1.5.1; LIBFWUPD_1.5.3 { global: fwupd_client_get_main_context; fwupd_client_set_main_context; local: *; } LIBFWUPD_1.5.2; LIBFWUPD_1.5.5 { global: fwupd_device_add_vendor_id; fwupd_device_get_vendor_ids; fwupd_device_has_vendor_id; local: *; } LIBFWUPD_1.5.3; LIBFWUPD_1.5.6 { global: fwupd_release_add_location; fwupd_release_get_locations; local: *; } LIBFWUPD_1.5.5; LIBFWUPD_1.5.8 { global: fwupd_device_add_protocol; fwupd_device_get_protocols; fwupd_device_has_protocol; local: *; } LIBFWUPD_1.5.6; LIBFWUPD_1.6.0 { global: fwupd_device_get_composite_id; fwupd_device_set_composite_id; local: *; } LIBFWUPD_1.5.8; LIBFWUPD_1.6.1 { global: fwupd_remote_set_filename_source; fwupd_remote_setup; fwupd_version_string; local: *; } LIBFWUPD_1.6.0; LIBFWUPD_1.6.2 { global: fwupd_device_get_version_build_date; fwupd_device_has_icon; fwupd_device_remove_child; fwupd_device_set_version_build_date; fwupd_request_get_created; fwupd_request_get_device_id; fwupd_request_get_id; fwupd_request_get_image; fwupd_request_get_kind; fwupd_request_get_message; fwupd_request_get_type; fwupd_request_kind_from_string; fwupd_request_kind_to_string; fwupd_request_new; fwupd_request_set_created; fwupd_request_set_device_id; fwupd_request_set_id; fwupd_request_set_image; fwupd_request_set_kind; fwupd_request_set_message; local: *; } LIBFWUPD_1.6.1; LIBFWUPD_1.7.0 { global: fwupd_security_attr_add_guid; fwupd_security_attr_add_guids; fwupd_security_attr_get_guids; fwupd_security_attr_has_guid; local: *; } LIBFWUPD_1.6.2; LIBFWUPD_1.7.1 { global: fwupd_client_add_hint; fwupd_client_get_host_security_events; fwupd_client_get_host_security_events_async; fwupd_client_get_host_security_events_finish; fwupd_security_attr_copy; fwupd_security_attr_flag_from_string; fwupd_security_attr_get_created; fwupd_security_attr_get_result_fallback; fwupd_security_attr_result_from_string; fwupd_security_attr_set_created; fwupd_security_attr_set_result_fallback; local: *; } LIBFWUPD_1.7.0; LIBFWUPD_1.7.2 { global: fwupd_release_get_id; fwupd_release_set_id; local: *; } LIBFWUPD_1.7.1; LIBFWUPD_1.7.3 { global: fwupd_client_get_host_bkc; fwupd_release_add_tag; fwupd_release_get_tags; fwupd_release_has_tag; local: *; } LIBFWUPD_1.7.2; LIBFWUPD_1.7.4 { global: fwupd_device_get_root; local: *; } LIBFWUPD_1.7.3; LIBFWUPD_1.7.6 { global: fwupd_device_add_issue; fwupd_device_get_issues; local: *; } LIBFWUPD_1.7.4; LIBFWUPD_1.8.0 { global: fwupd_client_disconnect; fwupd_client_get_only_trusted; local: *; } LIBFWUPD_1.7.6; LIBFWUPD_1.8.1 { global: fwupd_client_get_battery_level; fwupd_client_get_battery_threshold; fwupd_device_add_problem; fwupd_device_get_battery_level; fwupd_device_get_battery_threshold; fwupd_device_get_problems; fwupd_device_has_problem; fwupd_device_problem_from_string; fwupd_device_problem_to_string; fwupd_device_remove_problem; fwupd_device_set_battery_level; fwupd_device_set_battery_threshold; fwupd_device_set_problems; local: *; } LIBFWUPD_1.8.0; LIBFWUPD_1.8.2 { global: fwupd_client_get_host_vendor; fwupd_remote_set_filename_cache; fwupd_security_attr_get_description; fwupd_security_attr_get_title; fwupd_security_attr_set_description; fwupd_security_attr_set_title; local: *; } LIBFWUPD_1.8.1; LIBFWUPD_1.8.3 { global: fwupd_security_attr_remove_flag; local: *; } LIBFWUPD_1.8.2; LIBFWUPD_1.8.4 { global: fwupd_bios_setting_add_possible_value; fwupd_bios_setting_get_current_value; fwupd_bios_setting_get_description; fwupd_bios_setting_get_id; fwupd_bios_setting_get_kind; fwupd_bios_setting_get_lower_bound; fwupd_bios_setting_get_name; fwupd_bios_setting_get_path; fwupd_bios_setting_get_possible_values; fwupd_bios_setting_get_read_only; fwupd_bios_setting_get_scalar_increment; fwupd_bios_setting_get_type; fwupd_bios_setting_get_upper_bound; fwupd_bios_setting_has_possible_value; fwupd_bios_setting_map_possible_value; fwupd_bios_setting_new; fwupd_bios_setting_set_current_value; fwupd_bios_setting_set_description; fwupd_bios_setting_set_id; fwupd_bios_setting_set_kind; fwupd_bios_setting_set_lower_bound; fwupd_bios_setting_set_name; fwupd_bios_setting_set_path; fwupd_bios_setting_set_read_only; fwupd_bios_setting_set_scalar_increment; fwupd_bios_setting_set_upper_bound; fwupd_client_get_bios_settings; fwupd_client_get_bios_settings_async; fwupd_client_get_bios_settings_finish; fwupd_client_modify_bios_setting; fwupd_client_modify_bios_setting_async; fwupd_client_modify_bios_setting_finish; fwupd_security_attr_get_bios_setting_current_value; fwupd_security_attr_get_bios_setting_id; fwupd_security_attr_get_bios_setting_target_value; fwupd_security_attr_set_bios_setting_current_value; fwupd_security_attr_set_bios_setting_id; fwupd_security_attr_set_bios_setting_target_value; local: *; } LIBFWUPD_1.8.3; LIBFWUPD_1.8.6 { global: fwupd_request_add_flag; fwupd_request_flag_from_string; fwupd_request_flag_to_string; fwupd_request_get_flags; fwupd_request_has_flag; fwupd_request_remove_flag; fwupd_request_set_flags; local: *; } LIBFWUPD_1.8.4; LIBFWUPD_1.8.7 { global: fwupd_device_has_checksum; local: *; } LIBFWUPD_1.8.6; LIBFWUPD_1.8.8 { global: fwupd_release_add_report; fwupd_release_get_reports; fwupd_report_add_metadata_item; fwupd_report_get_created; fwupd_report_get_device_name; fwupd_report_get_distro_id; fwupd_report_get_distro_variant; fwupd_report_get_distro_version; fwupd_report_get_metadata; fwupd_report_get_metadata_item; fwupd_report_get_type; fwupd_report_get_vendor; fwupd_report_get_vendor_id; fwupd_report_get_version_old; fwupd_report_new; fwupd_report_set_created; fwupd_report_set_device_name; fwupd_report_set_distro_id; fwupd_report_set_distro_variant; fwupd_report_set_distro_version; fwupd_report_set_vendor; fwupd_report_set_vendor_id; fwupd_report_set_version_old; local: *; } LIBFWUPD_1.8.7; LIBFWUPD_1.8.11 { global: fwupd_client_inhibit; fwupd_client_inhibit_async; fwupd_client_inhibit_finish; fwupd_client_quit; fwupd_client_quit_async; fwupd_client_quit_finish; fwupd_client_set_daemon_version; fwupd_client_uninhibit; fwupd_client_uninhibit_async; fwupd_client_uninhibit_finish; fwupd_device_get_percentage; fwupd_device_set_percentage; local: *; } LIBFWUPD_1.8.8; LIBFWUPD_1.8.13 { global: fwupd_remote_set_metadata_uri; fwupd_remote_set_title; local: *; } LIBFWUPD_1.8.11; LIBFWUPD_1.9.1 { global: fwupd_remote_build_report_uri; fwupd_report_add_flag; fwupd_report_flag_from_string; fwupd_report_flag_to_string; fwupd_report_get_flags; fwupd_report_has_flag; fwupd_report_remove_flag; fwupd_report_set_flags; local: *; } LIBFWUPD_1.8.13; LIBFWUPD_1.9.3 { global: fwupd_device_array_filter_flags; fwupd_device_match_flags; fwupd_release_array_filter_flags; fwupd_release_match_flags; fwupd_remote_set_id; fwupd_report_get_remote_id; fwupd_report_set_remote_id; fwupd_security_attr_get_result_success; fwupd_security_attr_set_result_success; local: *; } LIBFWUPD_1.9.1; LIBFWUPD_1.9.4 { global: fwupd_bios_setting_write_value; fwupd_remote_add_flag; fwupd_remote_flag_from_string; fwupd_remote_flag_to_string; fwupd_remote_get_checksum_metadata; fwupd_remote_get_flags; fwupd_remote_get_refresh_interval; fwupd_remote_has_flag; fwupd_remote_needs_refresh; fwupd_remote_remove_flag; fwupd_remote_set_flags; local: *; } LIBFWUPD_1.9.3; LIBFWUPD_1.9.6 { global: fwupd_checksum_type_to_string_display; fwupd_client_fix_host_security_attr; fwupd_client_fix_host_security_attr_async; fwupd_client_fix_host_security_attr_finish; fwupd_client_undo_host_security_attr; fwupd_client_undo_host_security_attr_async; fwupd_client_undo_host_security_attr_finish; fwupd_security_attr_get_kernel_current_value; fwupd_security_attr_get_kernel_target_value; fwupd_security_attr_set_kernel_current_value; fwupd_security_attr_set_kernel_target_value; local: *; } LIBFWUPD_1.9.4; LIBFWUPD_1.9.8 { global: fwupd_remote_build_metadata_sig_uri; fwupd_remote_build_metadata_uri; local: *; } LIBFWUPD_1.9.6; LIBFWUPD_1.9.10 { global: fwupd_device_add_request_flag; fwupd_device_get_request_flags; fwupd_device_has_request_flag; fwupd_device_remove_request_flag; fwupd_device_set_request_flags; local: *; } LIBFWUPD_1.9.8; LIBFWUPD_1.9.15 { global: fwupd_client_reset_config; fwupd_client_reset_config_async; fwupd_client_reset_config_finish; local: *; } LIBFWUPD_1.9.10; LIBFWUPD_1.9.17 { global: fwupd_request_emit_invalidate; local: *; } LIBFWUPD_1.9.15; LIBFWUPD_1.9.19 { global: fwupd_client_download_set_retries; local: *; } LIBFWUPD_1.9.17; LIBFWUPD_1.9.20 { global: fwupd_client_build_report_devices; fwupd_client_upload_report; fwupd_client_upload_report_async; fwupd_client_upload_report_finish; local: *; } LIBFWUPD_1.9.19; LIBFWUPD_2.0.0 { global: fwupd_client_build_report_history; fwupd_client_build_report_security; fwupd_client_emulation_load; fwupd_client_emulation_load_async; fwupd_client_emulation_load_finish; fwupd_client_emulation_save; fwupd_client_emulation_save_async; fwupd_client_emulation_save_finish; fwupd_client_install_release; fwupd_client_install_release_async; fwupd_client_modify_config; fwupd_client_modify_config_async; fwupd_client_modify_config_finish; fwupd_client_refresh_remote; fwupd_client_refresh_remote_async; fwupd_codec_add_string; fwupd_codec_array_from_variant; fwupd_codec_array_to_json; fwupd_codec_array_to_variant; fwupd_codec_from_json; fwupd_codec_from_json_string; fwupd_codec_from_variant; fwupd_codec_get_type; fwupd_codec_json_append; fwupd_codec_json_append_bool; fwupd_codec_json_append_int; fwupd_codec_json_append_strv; fwupd_codec_string_append; fwupd_codec_string_append_bool; fwupd_codec_string_append_hex; fwupd_codec_string_append_int; fwupd_codec_string_append_size; fwupd_codec_string_append_time; fwupd_codec_to_json; fwupd_codec_to_json_string; fwupd_codec_to_string; fwupd_codec_to_variant; fwupd_device_remove_children; fwupd_error_convert; fwupd_install_flags_to_string; fwupd_remote_get_privacy_uri; fwupd_remote_set_checksum_sig; fwupd_remote_set_kind; fwupd_remote_set_order_after; fwupd_remote_set_order_before; fwupd_remote_set_password; fwupd_remote_set_privacy_uri; fwupd_remote_set_refresh_interval; fwupd_remote_set_report_uri; fwupd_remote_set_username; local: *; } LIBFWUPD_1.9.20; LIBFWUPD_2.0.1 { global: fwupd_client_get_details_async; fwupd_client_get_details_finish; local: *; } LIBFWUPD_2.0.0; LIBFWUPD_2.0.2 { global: fwupd_remote_get_firmware_base_uri; fwupd_remote_set_firmware_base_uri; local: *; } LIBFWUPD_2.0.1; LIBFWUPD_2.0.4 { global: fwupd_install_flags_from_string; local: *; } LIBFWUPD_2.0.2; LIBFWUPD_2.0.7 { global: fwupd_release_get_sbom_url; fwupd_release_set_sbom_url; fwupd_security_attr_get_fwupd_version; fwupd_security_attr_set_fwupd_version; local: *; } LIBFWUPD_2.0.4; LIBFWUPD_2.0.10 { global: fwupd_codec_json_append_map; local: *; } LIBFWUPD_2.0.7; fwupd-2.0.10/libfwupd/meson.build000066400000000000000000000134231501337203100166730ustar00rootroot00000000000000if get_option('tests') subdir('tests') endif fwupd_version_h = configure_file( input: 'fwupd-version.h.in', output: 'fwupd-version.h', configuration: conf ) base_dir = 'fwupd-' + libfwupd_lt_current install_headers( 'fwupd.h', subdir: base_dir, ) install_headers([ 'fwupd-build.h', 'fwupd-client.h', 'fwupd-client-sync.h', 'fwupd-common.h', 'fwupd-codec.h', 'fwupd-device.h', 'fwupd-enums.h', 'fwupd-error.h', 'fwupd-remote.h', 'fwupd-report.h', 'fwupd-request.h', 'fwupd-bios-setting.h', 'fwupd-security-attr.h', 'fwupd-release.h', 'fwupd-plugin.h', fwupd_version_h, ], subdir: join_paths(base_dir, 'libfwupd'), ) libfwupd_deps = [ giounix, libjcat, libjsonglib, libcurl, ] libfwupd_src = [ 'fwupd-client.c', 'fwupd-client-sync.c', 'fwupd-common.c', # fuzzing 'fwupd-codec.c', # fuzzing 'fwupd-device.c', # fuzzing 'fwupd-enums.c', # fuzzing 'fwupd-error.c', # fuzzing 'fwupd-bios-setting.c', # fuzzing 'fwupd-security-attr.c', # fuzzing 'fwupd-release.c', # fuzzing 'fwupd-plugin.c', 'fwupd-remote.c', 'fwupd-report.c', # fuzzing 'fwupd-request.c', # fuzzing 'fwupd-version.c', ] fwupd_mapfile = 'fwupd.map' vflag = '-Wl,--version-script,@0@/@1@'.format(meson.current_source_dir(), fwupd_mapfile) fwupd = library( 'fwupd', sources: libfwupd_src, soversion: libfwupd_lt_current, version: libfwupd_lt_version, dependencies: libfwupd_deps, c_args: [ '-DG_LOG_DOMAIN="Fwupd"', '-DLOCALSTATEDIR="' + localstatedir + '"', ], include_directories: root_incdir, link_args: cc.get_supported_link_arguments([vflag]), link_depends: fwupd_mapfile, install: true ) libfwupd_dep = declare_dependency( link_with: fwupd, include_directories: [root_incdir, include_directories('.')], dependencies: libfwupd_deps ) pkgg = import('pkgconfig') pkgg.generate( fwupd, requires: [ 'gio-2.0', 'json-glib-1.0' ], subdirs: base_dir, version: meson.project_version(), name: 'fwupd', filebase: 'fwupd', description: 'fwupd is a system daemon for installing device firmware', ) if introspection.allowed() fwupd_gir_deps = [ giounix, libcurl, ] fwupd_gir = gnome.generate_gir(fwupd, sources: [ 'fwupd-client.c', 'fwupd-client.h', 'fwupd-client-sync.c', 'fwupd-client-sync.h', 'fwupd-common.c', 'fwupd-common.h', 'fwupd-common-private.h', 'fwupd-codec.c', 'fwupd-codec.h', 'fwupd-device.c', 'fwupd-device.h', 'fwupd-device-private.h', 'fwupd-enums.c', 'fwupd-enums.h', 'fwupd-enums-private.h', 'fwupd-error.c', 'fwupd-error.h', 'fwupd-bios-setting.c', 'fwupd-bios-setting.h', 'fwupd-security-attr.c', 'fwupd-security-attr.h', 'fwupd-security-attr-private.h', 'fwupd-release.c', 'fwupd-release.h', 'fwupd-plugin.c', 'fwupd-plugin.h', 'fwupd-remote.c', 'fwupd-remote.h', 'fwupd-remote-private.h', 'fwupd-report.c', 'fwupd-report.h', 'fwupd-request.c', 'fwupd-request.h', 'fwupd-request-private.h', 'fwupd-version.c', fwupd_version_h, ], nsversion: '2.0', namespace: 'Fwupd', symbol_prefix: 'fwupd', identifier_prefix: ['Fwupd', 'fwupd'], export_packages: 'fwupd', header: 'fwupd.h', dependencies: fwupd_gir_deps, includes: [ 'Gio-2.0', 'GObject-2.0', 'Json-1.0', ], install: true ) gnome.generate_vapi('fwupd', sources: fwupd_gir[0], packages: ['gio-2.0', 'json-glib-1.0'], install: true, ) # Verify the map file is correct -- note we can't actually use the generated # file for two reasons: # # 1. We don't hard depend on GObject Introspection # 2. The map file is required to build the lib that the GIR is built from # # To avoid the circular dep, and to ensure we don't change exported API # accidentally actually check in a version of the version script to git. mapfile_target = custom_target('fwupd_mapfile', input: fwupd_gir[0], output: 'fwupd.map', command: [ generate_version_script, 'LIBFWUPD', '@INPUT@', '@OUTPUT@', ], ) test('fwupd-exported-api', diffcmd, args: [ '-urNp', files('fwupd.map'), mapfile_target, ], ) endif if get_option('tests') env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) e = executable( 'fwupd-self-test', sources: [ 'fwupd-self-test.c' ], include_directories: [ root_incdir, ], dependencies: [ libfwupd_deps, ], link_with: fwupd, c_args: [ '-DG_LOG_DOMAIN="Fwupd"', '-DSRCDIR="' + meson.current_source_dir() + '"', '-DLOCALSTATEDIR="' + localstatedir + '"', ], ) test('fwupd-self-test', e, timeout: 60, env: env) if run_sanitize_unsafe_tests and gio.version().version_compare ('>= 2.64.0') e = executable( 'fwupd-thread-test', sources: [ 'fwupd-thread-test.c' ], include_directories: [ root_incdir, ], dependencies: [ libfwupd_deps, ], link_with: fwupd, c_args: [ '-DG_LOG_DOMAIN="Fwupd"', ], ) test('fwupd-thread-test', e, timeout: 60) e = executable( 'fwupd-context-test', sources: [ 'fwupd-context-test.c' ], include_directories: [ root_incdir, ], dependencies: [ libfwupd_deps, ], link_with: fwupd, c_args: [ '-DG_LOG_DOMAIN="Fwupd"', ], ) test('fwupd-context-test', e, timeout: 60) endif endif fwupd_incdir = include_directories('.') fwupd-2.0.10/libfwupd/tests/000077500000000000000000000000001501337203100156705ustar00rootroot00000000000000fwupd-2.0.10/libfwupd/tests/meson.build000066400000000000000000000000011501337203100200210ustar00rootroot00000000000000 fwupd-2.0.10/libfwupd/tests/os-release000066400000000000000000000001051501337203100176460ustar00rootroot00000000000000NAME="Generic Linux" ID=generic VERSION_ID=39 VARIANT_ID=workstation fwupd-2.0.10/libfwupdplugin/000077500000000000000000000000001501337203100157455ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/README.md000066400000000000000000000262471501337203100172370ustar00rootroot00000000000000# libfwupdplugin This library is only partially API and ABI stable. Keeping unused, unsafe and deprecated functions around forever is a maintenance burden and so symbols are removed when branching for new minor versions. Use `./contrib/migrate.py` to migrate up out-of-tree plugins to the new API. Remember: Plugins should be upstream! ## 1.5.5 * `fu_common_is_cpu_intel()`: Use `fu_common_get_cpu_vendor()` instead. * `fu_firmware_strparse_uintXX()`: Use `fu_firmware_strparse_uintXX_safe()` instead. * `fu_plugin_get_usb_context()`: Remove, as no longer required. * `fu_plugin_set_usb_context()`: Remove, as no longer required. * `fu_plugin_runner_usb_device_added()`: Use `fu_plugin_runner_backend_device_added()` instead. * `fu_plugin_runner_udev_device_added()`: Use `fu_plugin_runner_backend_device_added()` instead. * `fu_plugin_runner_udev_device_changed()`: Use `fu_plugin_runner_backend_device_added()` instead. * `FuHidDevice->open()`: Use the `FuDevice` superclass instead. * `FuHidDevice->close()`: Use the `FuDevice` superclass instead. * `FuUsbDevice->probe()`: Use the `FuDevice` superclass instead. * `FuUsbDevice->open()`: Use the `FuDevice` superclass instead. * `FuUsbDevice->close()`: Use the `FuDevice` superclass instead. * `FuUdevDevice->to_string()`: Use the `FuDevice` superclass instead. * `FuUdevDevice->probe()`: Use the `FuDevice` superclass instead. * `FuUdevDevice->open()`: Use the `FuDevice` superclass instead. * `FuUdevDevice->close()`: Use the `FuDevice` superclass instead. ## 1.5.6 * `fu_device_get_protocol()`: Use `fu_device_get_protocols()` instead. * `fu_device_set_protocol()`: Use `fu_device_add_protocol()` instead. ## 1.6.2 * `fu_device_has_custom_flag()`: Use `fu_device_has_private_flag()` instead. ## 1.6.3 * `fu_device_sleep_with_progress()`: Use `fu_progress_sleep()` instead -- but be aware the unit of time has changed from *seconds* to *milliseconds*. * `fu_device_get_status()`: Use `fu_progress_get_status()` instead. * `fu_device_set_status()`: Use `fu_progress_set_status()` instead. * `fu_device_get_progress()`: Use `fu_progress_get_percentage()` instead. * `fu_device_set_progress_full()`: Use `fu_progress_set_percentage_full()` instead. * `fu_device_set_progress()`: Use `fu_progress_set_steps()`, `fu_progress_add_step()` and `fu_progress_done()` -- see the `FuProgress` docs for more details! ## 1.8.2 * `fu_udev_device_pread_full()`: Use `fu_udev_device_pread()` instead -- as the latter now specifies the buffer length. * `fu_udev_device_pread_full()`: Use `fu_udev_device_pwrite()` instead -- as the latter now specifies the buffer length. * `fu_udev_device_ioctl_full()`: Use `fu_udev_device_ioctl()` instead -- as the latter now always specifies the timeout. * `fu_udev_device_new_full()`: Use `fu_udev_device_new()` instead -- as the latter always specifies the context. * `fu_usb_device_new_full()`: Use `fu_usb_device_new()` instead -- as the latter always specifies the context. * `fu_device_new_with_context()`: Use `fu_device_new()` instead -- as the latter always specifies the context. * `fu_plugin_has_custom_flag()`: Use `fu_plugin_has_private_flag()` instead. * `fu_efivar_secure_boot_enabled_full()`: Use `fu_efivars_get_secure_boot()` instead -- as the latter always specifies the error. * `fu_progress_add_step()`: Add a 4th parameter to the function to specify the nice name for the step, or NULL. * `fu_backend_setup()`: Now requires a `FuProgress`, although it can be ignored. * `fu_backend_coldplug`: Now requires a `FuProgress`, although it can be ignored. * `FuPluginVfuncs->setup`: Now requires a `FuProgress`, although it can be ignored. * `FuPluginVfuncs->coldplug`: Now requires a `FuProgress`, although it can be ignored. * `fu_common_crc*`: Use `fu_crc` prefix, i.e. remove the `_common` * `fu_common_sum*`: Use `fu_sum` prefix, i.e. remove the `_common` * `fu_byte_array_set_size_full()`: Use `fu_byte_array_set_size` instead -- as the latter now always specifies the fill char. * `fu_common_string*`: Use `fu_string` prefix, i.e. remove the `_common` * `fu_common_bytes*`: Use `fu_bytes` prefix, i.e. remove the `_common` * `fu_common_set_contents_bytes()`: Use `fu_bytes_set_contents()` instead * `fu_common_get_contents_bytes()`: Use `fu_bytes_get_contents()` instead * `fu_common_read*`: Use `fu_memread` prefix, i.e. replace the `_common` with `_mem` * `fu_common_write*`: Use `fu_memwrite` prefix, i.e. replace the `_common` with `_mem` * `fu_common_bytes_compare_raw()`: Use `fu_memcmp_safe()` instead * `fu_common_spawn_sync()`: Use `g_spawn_sync()` instead, or ideally not at all! * `fu_common_extract_archive()`: Use `FuArchiveFirmware()` instead. * `fu_common_instance_id_strsafe()`: Use `fu_device_add_instance_strsafe()` instead. * `fu_common_kernel_locked_down()`: Use `fu_kernel_locked_down` instead. * `fu_common_check_kernel_version()`: Use `fu_kernel_check_version` instead. * `fu_common_get_firmware_search_path()`: Use `fu_kernel_get_firmware_search_path` instead. * `fu_common_set_firmware_search_path()`: Use `fu_kernel_set_firmware_search_path` instead. * `fu_common_reset_firmware_search_path()`: Use `fu_kernel_reset_firmware_search_path` instead. * `fu_common_firmware_builder()`: You should not be using this. * `fu_common_realpath()`: You should not be using this. * `fu_common_uri_get_scheme()`: You should not be using this. * `fu_common_dump*`: Use `fu_dump` prefix, i.e. remove the `_common` * `fu_common_error_array_get_best()`: You should not be using this. * `fu_common_cpuid()`: Use `fu_cpuid` instead. * `fu_common_get_cpu_vendor()`: Use `fu_cpu_get_vendor` instead. * `fu_common_vercmp_full()`: Use `fu_version_compare()` instead. * `fu_version_ensure_semver()`: Use `fu_version_ensure_semver()` instead. * `fu_common_version_from_uint*()`: Use `fu_version_from_uint*()` instead. * `fu_common_strtoull()`: Use `fu_strtoull()` instead -- as the latter always specifies the error. * `fu_smbios_to_string()`: Use `fu_firmware_to_string()` instead -- as `FuSmbios` is a `FuFirmware` superclass. * `fu_common_cab_build_silo()`: You should not be using this. * `fu_i2c_device_read_full()`: Use `fu_i2c_device_read` instead. * `fu_i2c_device_write_full()`: Use `fu_i2c_device_write` instead. * `fu_firmware_parse_bytes()`: Remove the `addr_end` parameter, and ensure that `offset` is a `gsize`. ## 1.8.5 * `fu_volume_new_esp_default()`: Use `fu_context_get_esp_volumes()` instead. * `fu_plugin_set_secure_config_value()`: Set `FWUPD_PLUGIN_FLAG_SECURE_CONFIG` and use `fu_plugin_set_config_value()` ## 1.8.7 * `fu_mei_device_connect()`: Drop the explicit GUID parameter and match in the quirk file instead. * `fu_context_get_smbios_data()`: Add a `GError` ## 1.9.1 * `fu_plugin_get_config_value()`: Add the default value as the last parameter * `fu_plugin_get_config_value_boolean()`: Add the default value as the last parameter ## 1.9.6 * `fu_security_attrs_get_by_appstream_id()`: Add a `GError` ## 1.9.8 * `fu_device_build_instance_id_quirk": rename to`fu_device_build_instance_id_full()` * `fu_smbios_get_data()`: Now returns a array of `GByte`s * `fu_udev_device_get_fd()`: Use `fu_udev_device_get_io_channel()` instead * `fu_device_emit_request()`: Add `FuProgress` and `GError` * `fu_device_set_version_from_uint16()`: Use `fu_version_from_uint16()` from `FuDeviceClass->convert_version` instead * `fu_device_set_version_from_uint24()`: Use `fu_version_from_uint24()` from `FuDeviceClass->convert_version` instead * `fu_device_set_version_from_uint32()`: Use `fu_version_from_uint32()` from `FuDeviceClass->convert_version` instead * `fu_device_set_version_from_uint64()`: Use `fu_version_from_uint64()` from `FuDeviceClass->convert_version` instead ## 2.0.0 * `fu_hid_device_parse_descriptor()`: Use `fu_hid_device_parse_descriptors()` instead * `fu_io_channel_new_file()`: Add some `FuIoChannelOpenFlag`, e.g. `FU_IO_CHANNEL_OPEN_FLAG_READ|FU_IO_CHANNEL_OPEN_FLAG_WRITE` * `fu_udev_device_set_flags()`: Use `fu_udev_device_add_flag()` instead * `fu_udev_device_get_slot_depth()`: Use `fu_udev_device_get_subsystem_depth()` instead * `fu_usb_device_is_open()`: Use `fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_IS_OPEN)` instead * `fu_udev_device_ioctl`: Add some `FuIoctlFlags`, e.g. `FU_IOCTL_FLAG_RETRY` * `fu_udev_device_write_sysfs()`: Add a timeout in milliseconds * `fu_udev_device_get_sysfs_attr_uint64()`: Use `fu_udev_device_read_sysfs()` instead * `fu_udev_device_get_sysfs_attr`: Use `fu_udev_device_read_sysfs()` instead * `fu_i2c_device_get_bus_number()`: Use `fu_udev_device_get_number()` instead * `fu_usb_device_set_open_retry_count()`: Use `FU_DEVICE_PRIVATE_FLAG_RETRY_OPEN` instead * `FWUPD_DEVICE_FLAG_INSTALL_PARENT_FIRST`: Use `FU_DEVICE_PRIVATE_FLAG_INSTALL_PARENT_FIRST` instead * `FWUPD_DEVICE_FLAG_REGISTERED`: Use `FU_DEVICE_PRIVATE_FLAG_REGISTERED` instead * `FWUPD_DEVICE_FLAG_ADD_COUNTERPART_GUIDS`: Use `FU_DEVICE_PRIVATE_FLAG_ADD_COUNTERPART_GUIDS` instead * `FWUPD_DEVICE_FLAG_USE_RUNTIME_VERSION`: Use `FU_DEVICE_PRIVATE_FLAG_USE_RUNTIME_VERSION` instead * `FWUPD_DEVICE_FLAG_SKIPS_RESTART`: Use `FU_DEVICE_PRIVATE_FLAG_SKIPS_RESTART` instead * `FU_UDEV_DEVICE_FLAG_VENDOR_FROM_PARENT`: Use `fu_device_get_backend_parent_with_subsystem()` and copy properties instead * `fu_udev_device_is_pci_base_cls`: Put logic in `FuPciDevice` if needed, but consider what you are doing * `fu_udev_device_get_cls`: Put logic in `FuPciDevice` if needed, or use a quirk instead * `fu_udev_device_get_vendor`: Use `fu_device_get_vid()` instead * `fu_udev_device_set_vendor`: Use `fu_device_set_vid()` instead * `fu_udev_device_get_model`: Use `fu_device_get_pid()` instead * `fu_udev_device_set_model`: Use `fu_device_set_pid()` instead * `fu_udev_device_get_subsystem_vendor`: Use `fu_pci_device_get_subsystem_vid()` instead * `fu_udev_device_get_subsystem_model`: Use `fu_pci_device_get_subsystem_pid()` instead * `fu_udev_device_get_revision`: Use `fu_pci_device_get_revision()` instead * `fu_udev_device_set_revision`: Use `fu_pci_device_set_revision()` instead ## 2.0.2 * `fu_chunk_array_new_from_bytes()`: Add a page size, typically `FU_CHUNK_PAGESZ_NONE` * `fu_chunk_array_new_from_stream()`: Add a page size, typically `FU_CHUNK_PAGESZ_NONE` * `fu_udev_device_ioctl()`: Use `fu_udev_device_ioctl_new()` instead * `FuUdevDeviceIoctlFlags`: Use `FuIoctlFlags` instead ## 2.0.6 * `fu_kernel_get_firmware_search_path()`: Use `FuKernelSearchPathLocker` instead. * `fu_kernel_set_firmware_search_path()`: Use `FuKernelSearchPathLocker` instead. * `fu_kernel_reset_firmware_search_path()`: Use `FuKernelSearchPathLocker` instead. ## 2.0.7 * `fu_device_write_firmware()`: Use `FuFirmware` rather than a `GInputStream`. * `fu_bytes_pad()`: Add a data byte, typically `0xFF`. * `fu_smbios_get_string()`: Add an expected structure length, typically `FU_SMBIOS_STRUCTURE_LENGTH_ANY`. * `fu_smbios_get_integer()`: Add an expected structure length, typically `FU_SMBIOS_STRUCTURE_LENGTH_ANY`. * `fu_smbios_get_data()`: Add an expected structure length, typically `FU_SMBIOS_STRUCTURE_LENGTH_ANY`. * `fu_context_get_smbios_string()`: Add an expected structure length, typically `FU_SMBIOS_STRUCTURE_LENGTH_ANY`. * `fu_context_get_smbios_integer()`: Add an expected structure length, typically `FU_SMBIOS_STRUCTURE_LENGTH_ANY`. * `fu_context_get_smbios_data()`: Add an expected structure length, typically `FU_SMBIOS_STRUCTURE_LENGTH_ANY`. fwupd-2.0.10/libfwupdplugin/fu-acpi-table.c000066400000000000000000000117431501337203100205300ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-acpi-table-struct.h" #include "fu-acpi-table.h" #include "fu-common.h" #include "fu-input-stream.h" #include "fu-sum.h" /** * FuAcpiTable: * * An generic ACPI table. * * See also: [class@FuFirmware] */ typedef struct { guint8 revision; gchar *oem_id; gchar *oem_table_id; guint32 oem_revision; } FuAcpiTablePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuAcpiTable, fu_acpi_table, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_acpi_table_get_instance_private(o)) static void fu_acpi_table_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuAcpiTable *self = FU_ACPI_TABLE(firmware); FuAcpiTablePrivate *priv = GET_PRIVATE(self); fu_xmlb_builder_insert_kx(bn, "revision", priv->revision); fu_xmlb_builder_insert_kv(bn, "oem_id", priv->oem_id); fu_xmlb_builder_insert_kv(bn, "oem_table_id", priv->oem_table_id); fu_xmlb_builder_insert_kx(bn, "oem_revision", priv->oem_revision); } /** * fu_acpi_table_get_revision: * @self: a #FuAcpiTable * * Gets the revision of the table. * * Returns: integer, default 0x0 * * Since: 1.8.11 **/ guint8 fu_acpi_table_get_revision(FuAcpiTable *self) { FuAcpiTablePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_ACPI_TABLE(self), G_MAXUINT8); return priv->revision; } /** * fu_acpi_table_get_oem_id: * @self: a #FuAcpiTable * * Gets an optional OEM ID. * * Returns: a string, or %NULL * * Since: 1.8.11 **/ const gchar * fu_acpi_table_get_oem_id(FuAcpiTable *self) { FuAcpiTablePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_ACPI_TABLE(self), NULL); return priv->oem_id; } /** * fu_acpi_table_get_oem_table_id: * @self: a #FuAcpiTable * * Gets an optional OEM table ID. * * Returns: a string, or %NULL * * Since: 1.8.11 **/ const gchar * fu_acpi_table_get_oem_table_id(FuAcpiTable *self) { FuAcpiTablePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_ACPI_TABLE(self), NULL); return priv->oem_table_id; } /** * fu_acpi_table_get_oem_revision: * @self: a #FuAcpiTable * * Gets the OEM revision. * * Returns: integer, default 0x0 * * Since: 1.8.11 **/ guint32 fu_acpi_table_get_oem_revision(FuAcpiTable *self) { FuAcpiTablePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_ACPI_TABLE(self), G_MAXUINT32); return priv->oem_revision; } static gboolean fu_acpi_table_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuAcpiTable *self = FU_ACPI_TABLE(firmware); FuAcpiTablePrivate *priv = GET_PRIVATE(self); gsize streamsz = 0; guint32 length; g_autofree gchar *id = NULL; g_autoptr(FuStructAcpiTable) st = NULL; /* parse */ st = fu_struct_acpi_table_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; id = fu_struct_acpi_table_get_signature(st); fu_firmware_set_id(FU_FIRMWARE(self), id); priv->revision = fu_struct_acpi_table_get_revision(st); priv->oem_id = fu_struct_acpi_table_get_oem_id(st); priv->oem_table_id = fu_struct_acpi_table_get_oem_table_id(st); priv->oem_revision = fu_struct_acpi_table_get_oem_revision(st); /* length */ length = fu_struct_acpi_table_get_length(st); if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (length > streamsz || length < st->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "table length not valid: got 0x%x but expected 0x%x", (guint)streamsz, (guint)length); return FALSE; } fu_firmware_set_size(firmware, length); /* checksum */ if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { guint8 checksum_actual = 0; if (!fu_input_stream_compute_sum8(stream, &checksum_actual, error)) return FALSE; if (checksum_actual != 0x0) { guint8 checksum = fu_struct_acpi_table_get_checksum(st); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "CRC failed, expected 0x%02x, got 0x%02x", (guint)checksum - checksum_actual, checksum); return FALSE; } } /* success */ return TRUE; } static void fu_acpi_table_init(FuAcpiTable *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); } static void fu_acpi_table_finalize(GObject *object) { FuAcpiTable *self = FU_ACPI_TABLE(object); FuAcpiTablePrivate *priv = GET_PRIVATE(self); g_free(priv->oem_table_id); g_free(priv->oem_id); G_OBJECT_CLASS(fu_acpi_table_parent_class)->finalize(object); } static void fu_acpi_table_class_init(FuAcpiTableClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_acpi_table_finalize; firmware_class->parse = fu_acpi_table_parse; firmware_class->export = fu_acpi_table_export; } /** * fu_acpi_table_new: * * Creates a new #FuFirmware * * Since: 1.8.11 **/ FuFirmware * fu_acpi_table_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_ACPI_TABLE, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-acpi-table.h000066400000000000000000000012751501337203100205340ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_ACPI_TABLE (fu_acpi_table_get_type()) G_DECLARE_DERIVABLE_TYPE(FuAcpiTable, fu_acpi_table, FU, ACPI_TABLE, FuFirmware) struct _FuAcpiTableClass { FuFirmwareClass parent_class; }; FuFirmware * fu_acpi_table_new(void); guint8 fu_acpi_table_get_revision(FuAcpiTable *self) G_GNUC_NON_NULL(1); const gchar * fu_acpi_table_get_oem_id(FuAcpiTable *self) G_GNUC_NON_NULL(1); const gchar * fu_acpi_table_get_oem_table_id(FuAcpiTable *self) G_GNUC_NON_NULL(1); guint32 fu_acpi_table_get_oem_revision(FuAcpiTable *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-acpi-table.rs000066400000000000000000000006241501337203100207260ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructAcpiTable { signature: [char; 4], length: u32le, revision: u8, checksum: u8, oem_id: [char; 6], oem_table_id: [char; 8], oem_revision: u32be, _asl_compiler_id: [char; 4], _asl_compiler_revision: u32le, } fwupd-2.0.10/libfwupdplugin/fu-archive-firmware.c000066400000000000000000000176361501337203100217710ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * Copyright 2022 Gaël PORTAY * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include "fu-archive-firmware.h" #include "fu-archive.h" #include "fu-common.h" /** * FuArchiveFirmware: * * An archive firmware image, typically for nested firmware volumes. * * See also: [class@FuFirmware] */ typedef struct { FuArchiveFormat format; FuArchiveCompression compression; } FuArchiveFirmwarePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuArchiveFirmware, fu_archive_firmware, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_archive_firmware_get_instance_private(o)) static void fu_archive_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuArchiveFirmware *self = FU_ARCHIVE_FIRMWARE(firmware); FuArchiveFirmwarePrivate *priv = GET_PRIVATE(self); fu_xmlb_builder_insert_kv(bn, "format", fu_archive_format_to_string(priv->format)); fu_xmlb_builder_insert_kv(bn, "compression", fu_archive_compression_to_string(priv->compression)); } static gboolean fu_archive_firmware_parse_cb(FuArchive *self, const gchar *filename, GBytes *bytes, gpointer user_data, GError **error) { FuFirmware *firmware = FU_FIRMWARE(user_data); g_autoptr(FuFirmware) img = fu_firmware_new_from_bytes(bytes); fu_firmware_set_id(img, filename); return fu_firmware_add_image_full(firmware, img, error); } static gboolean fu_archive_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { g_autoptr(FuArchive) archive = NULL; /* load archive */ archive = fu_archive_new_stream(stream, FU_ARCHIVE_FLAG_IGNORE_PATH, error); if (archive == NULL) return FALSE; /* decompress each image in the archive */ return fu_archive_iterate(archive, fu_archive_firmware_parse_cb, firmware, error); } /** * fu_archive_firmware_get_format: * @self: a #FuArchiveFirmware * * Gets the archive format. * * Returns: format * * Since: 1.8.1 **/ FuArchiveFormat fu_archive_firmware_get_format(FuArchiveFirmware *self) { FuArchiveFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_ARCHIVE_FIRMWARE(self), FU_ARCHIVE_FORMAT_UNKNOWN); return priv->format; } /** * fu_archive_firmware_set_format: * @self: a #FuArchiveFirmware * @format: the archive format, e.g. %FU_ARCHIVE_FORMAT_ZIP * * Sets the archive format. * * Since: 1.8.1 **/ void fu_archive_firmware_set_format(FuArchiveFirmware *self, FuArchiveFormat format) { FuArchiveFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_ARCHIVE_FIRMWARE(self)); priv->format = format; } /** * fu_archive_firmware_get_compression: * @self: A #FuArchiveFirmware * * Returns the compression. * * Returns: compression * * Since: 1.8.1 **/ FuArchiveCompression fu_archive_firmware_get_compression(FuArchiveFirmware *self) { FuArchiveFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_ARCHIVE_FIRMWARE(self), FU_ARCHIVE_COMPRESSION_UNKNOWN); return priv->compression; } /** * fu_archive_firmware_set_compression: * @self: A #FuArchiveFirmware * @compression: the compression, e.g. %FU_ARCHIVE_COMPRESSION_NONE * * Sets the compression. * * Since: 1.8.1 **/ void fu_archive_firmware_set_compression(FuArchiveFirmware *self, FuArchiveCompression compression) { FuArchiveFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_ARCHIVE_FIRMWARE(self)); priv->compression = compression; } /** * fu_archive_firmware_get_image_fnmatch: * @self: a #FuPlugin * @pattern: (not nullable): a glob pattern, e.g. `*foo*` * @error: (nullable): optional return location for an error * * Gets a single firmware image using the image ID pattern. It is also an error for multiple images * to match. * * Returns: (transfer full): a #FuFirmware, or %NULL if the image is not found * * Since: 1.8.9 **/ FuFirmware * fu_archive_firmware_get_image_fnmatch(FuArchiveFirmware *self, const gchar *pattern, GError **error) { g_autoptr(FuFirmware) img_match = NULL; g_autoptr(GPtrArray) imgs = fu_firmware_get_images(FU_FIRMWARE(self)); g_return_val_if_fail(FU_IS_ARCHIVE_FIRMWARE(self), NULL); g_return_val_if_fail(pattern != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); const gchar *fn = fu_firmware_get_id(img); if (!g_pattern_match_simple(pattern, fn)) continue; if (img_match != NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "multiple images matched %s", pattern); return NULL; } img_match = g_object_ref(img); } if (img_match == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no image matched %s", pattern); return NULL; } return g_steal_pointer(&img_match); } static GByteArray * fu_archive_firmware_write(FuFirmware *firmware, GError **error) { FuArchiveFirmware *self = FU_ARCHIVE_FIRMWARE(firmware); FuArchiveFirmwarePrivate *priv = GET_PRIVATE(self); g_autoptr(FuArchive) archive = NULL; g_autoptr(GPtrArray) images = fu_firmware_get_images(firmware); /* sanity check */ if (priv->format == FU_ARCHIVE_FORMAT_UNKNOWN) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "firmware archive format unspecified"); return NULL; } if (priv->compression == FU_ARCHIVE_COMPRESSION_UNKNOWN) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "firmware archive compression unspecified"); return NULL; } /* save archive and compress each image to the archive */ archive = fu_archive_new(NULL, FU_ARCHIVE_FLAG_NONE, NULL); for (guint i = 0; i < images->len; i++) { FuFirmware *img = g_ptr_array_index(images, i); g_autoptr(GBytes) blob = NULL; if (fu_firmware_get_id(img) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "image has no ID"); return NULL; } blob = fu_firmware_get_bytes(img, error); if (blob == NULL) return NULL; fu_archive_add_entry(archive, fu_firmware_get_id(img), blob); } return fu_archive_write(archive, priv->format, priv->compression, error); } static gboolean fu_archive_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { FuArchiveFirmware *self = FU_ARCHIVE_FIRMWARE(firmware); const gchar *tmp; /* simple properties */ tmp = xb_node_query_text(n, "format", NULL); if (tmp != NULL) { FuArchiveFormat format = fu_archive_format_from_string(tmp); if (format == FU_ARCHIVE_FORMAT_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "format %s not supported", tmp); return FALSE; } fu_archive_firmware_set_format(self, format); } tmp = xb_node_query_text(n, "compression", NULL); if (tmp != NULL) { FuArchiveCompression compression = fu_archive_compression_from_string(tmp); if (compression == FU_ARCHIVE_COMPRESSION_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "compression %s not supported", tmp); return FALSE; } fu_archive_firmware_set_compression(self, compression); } /* success */ return TRUE; } static void fu_archive_firmware_init(FuArchiveFirmware *self) { fu_firmware_set_images_max(FU_FIRMWARE(self), 10000); } static void fu_archive_firmware_class_init(FuArchiveFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_archive_firmware_parse; firmware_class->write = fu_archive_firmware_write; firmware_class->build = fu_archive_firmware_build; firmware_class->export = fu_archive_firmware_export; } /** * fu_archive_firmware_new: * * Creates a new archive #FuFirmware * * Since: 1.7.3 **/ FuFirmware * fu_archive_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_ARCHIVE_FIRMWARE, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-archive-firmware.h000066400000000000000000000022051501337203100217600ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * Copyright 2022 Gaël PORTAY * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-archive.h" #include "fu-firmware.h" #define FU_TYPE_ARCHIVE_FIRMWARE (fu_archive_firmware_get_type()) #define FU_TYPE_ARCHIVE_FIRMWARE_RECORD (fu_archive_firmware_record_get_type()) G_DECLARE_DERIVABLE_TYPE(FuArchiveFirmware, fu_archive_firmware, FU, ARCHIVE_FIRMWARE, FuFirmware) struct _FuArchiveFirmwareClass { FuFirmwareClass parent_class; }; FuFirmware * fu_archive_firmware_new(void); FuArchiveFormat fu_archive_firmware_get_format(FuArchiveFirmware *self) G_GNUC_NON_NULL(1); void fu_archive_firmware_set_format(FuArchiveFirmware *self, FuArchiveFormat format) G_GNUC_NON_NULL(1); FuArchiveCompression fu_archive_firmware_get_compression(FuArchiveFirmware *self) G_GNUC_NON_NULL(1); void fu_archive_firmware_set_compression(FuArchiveFirmware *self, FuArchiveCompression compression) G_GNUC_NON_NULL(1); FuFirmware * fu_archive_firmware_get_image_fnmatch(FuArchiveFirmware *self, const gchar *pattern, GError **error) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-archive.c000066400000000000000000000415451501337203100201530ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2022 Gaël PORTAY * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuArchive" #include "config.h" #include #ifdef HAVE_LIBARCHIVE #include #include #endif #include "fwupd-error.h" #include "fu-archive.h" #include "fu-bytes.h" #include "fu-input-stream.h" /** * FuArchive: * * An in-memory archive decompressor */ struct _FuArchive { GObject parent_instance; GHashTable *entries; /* str:GBytes */ }; G_DEFINE_TYPE(FuArchive, fu_archive, G_TYPE_OBJECT) static void fu_archive_finalize(GObject *obj) { FuArchive *self = FU_ARCHIVE(obj); g_hash_table_unref(self->entries); G_OBJECT_CLASS(fu_archive_parent_class)->finalize(obj); } static void fu_archive_class_init(FuArchiveClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_archive_finalize; } static void fu_archive_init(FuArchive *self) { self->entries = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_bytes_unref); } /** * fu_archive_add_entry: * @self: a #FuArchive * @fn: (not nullable): a filename * @blob: (not nullable): a #GBytes * * Adds, or replaces an entry to an archive. * * Since: 1.8.1 **/ void fu_archive_add_entry(FuArchive *self, const gchar *fn, GBytes *blob) { g_return_if_fail(FU_IS_ARCHIVE(self)); g_return_if_fail(fn != NULL); g_return_if_fail(blob != NULL); g_hash_table_insert(self->entries, g_strdup(fn), g_bytes_ref(blob)); } /** * fu_archive_lookup_by_fn: * @self: a #FuArchive * @fn: a filename * @error: (nullable): optional return location for an error * * Finds the blob referenced by filename * * Returns: (transfer full): a #GBytes, or %NULL if the filename was not found * * Since: 1.2.2 **/ GBytes * fu_archive_lookup_by_fn(FuArchive *self, const gchar *fn, GError **error) { GBytes *bytes; g_return_val_if_fail(FU_IS_ARCHIVE(self), NULL); g_return_val_if_fail(fn != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); bytes = g_hash_table_lookup(self->entries, fn); if (bytes == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no blob for %s", fn); return NULL; } return g_bytes_ref(bytes); } /** * fu_archive_iterate: * @self: a #FuArchive * @callback: (scope call) (closure user_data): a #FuArchiveIterateFunc. * @user_data: user data * @error: (nullable): optional return location for an error * * Iterates over the archive contents, calling the given function for each * of the files found. If any @callback returns %FALSE scanning is aborted. * * Returns: True if no @callback returned FALSE * * Since: 1.3.4 */ gboolean fu_archive_iterate(FuArchive *self, FuArchiveIterateFunc callback, gpointer user_data, GError **error) { GHashTableIter iter; gpointer key, value; g_return_val_if_fail(FU_IS_ARCHIVE(self), FALSE); g_return_val_if_fail(callback != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); g_hash_table_iter_init(&iter, self->entries); while (g_hash_table_iter_next(&iter, &key, &value)) { if (!callback(self, (const gchar *)key, (GBytes *)value, user_data, error)) return FALSE; } return TRUE; } #ifdef HAVE_LIBARCHIVE /* workaround the struct types of libarchive */ typedef struct archive _archive_read_ctx; static void fu_archive_read_ctx_free(_archive_read_ctx *arch) { archive_read_close(arch); archive_read_free(arch); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(_archive_read_ctx, fu_archive_read_ctx_free) typedef struct archive _archive_write_ctx; static void fu_archive_write_ctx_free(_archive_write_ctx *arch) { archive_write_close(arch); archive_write_free(arch); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(_archive_write_ctx, fu_archive_write_ctx_free) typedef struct archive_entry _archive_entry_ctx; static void fu_archive_entry_ctx_free(_archive_entry_ctx *entry) { archive_entry_free(entry); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(_archive_entry_ctx, fu_archive_entry_ctx_free) static void fu_archive_set_format(_archive_write_ctx *arch, FuArchiveFormat format) { if (format == FU_ARCHIVE_FORMAT_CPIO) archive_write_set_format_cpio(arch); if (format == FU_ARCHIVE_FORMAT_SHAR) archive_write_set_format_shar(arch); if (format == FU_ARCHIVE_FORMAT_TAR) archive_write_set_format_pax_restricted(arch); if (format == FU_ARCHIVE_FORMAT_USTAR) archive_write_set_format_ustar(arch); if (format == FU_ARCHIVE_FORMAT_PAX) archive_write_set_format_pax(arch); if (format == FU_ARCHIVE_FORMAT_GNUTAR) archive_write_set_format_gnutar(arch); if (format == FU_ARCHIVE_FORMAT_ISO9660) archive_write_set_format_iso9660(arch); if (format == FU_ARCHIVE_FORMAT_ZIP) archive_write_set_format_zip(arch); if (format == FU_ARCHIVE_FORMAT_AR) archive_write_set_format_ar_bsd(arch); if (format == FU_ARCHIVE_FORMAT_AR_SVR4) archive_write_set_format_ar_svr4(arch); if (format == FU_ARCHIVE_FORMAT_MTREE) archive_write_set_format_mtree(arch); if (format == FU_ARCHIVE_FORMAT_RAW) archive_write_set_format_raw(arch); if (format == FU_ARCHIVE_FORMAT_XAR) archive_write_set_format_xar(arch); if (format == FU_ARCHIVE_FORMAT_7ZIP) archive_write_set_format_7zip(arch); if (format == FU_ARCHIVE_FORMAT_WARC) archive_write_set_format_warc(arch); } static void fu_archive_set_compression(_archive_write_ctx *arch, FuArchiveCompression compression) { if (compression == FU_ARCHIVE_COMPRESSION_BZIP2) archive_write_add_filter_bzip2(arch); if (compression == FU_ARCHIVE_COMPRESSION_COMPRESS) archive_write_add_filter_compress(arch); if (compression == FU_ARCHIVE_COMPRESSION_GRZIP) archive_write_add_filter_grzip(arch); if (compression == FU_ARCHIVE_COMPRESSION_GZIP) archive_write_add_filter_gzip(arch); if (compression == FU_ARCHIVE_COMPRESSION_LRZIP) archive_write_add_filter_lrzip(arch); if (compression == FU_ARCHIVE_COMPRESSION_LZ4) archive_write_add_filter_lz4(arch); if (compression == FU_ARCHIVE_COMPRESSION_LZIP) archive_write_add_filter_lzip(arch); if (compression == FU_ARCHIVE_COMPRESSION_LZMA) archive_write_add_filter_lzma(arch); if (compression == FU_ARCHIVE_COMPRESSION_LZOP) archive_write_add_filter_lzop(arch); if (compression == FU_ARCHIVE_COMPRESSION_UU) archive_write_add_filter_uuencode(arch); if (compression == FU_ARCHIVE_COMPRESSION_XZ) archive_write_add_filter_xz(arch); #ifdef HAVE_LIBARCHIVE_WRITE_ADD_COMPRESSION_ZSTD if (compression == FU_ARCHIVE_COMPRESSION_ZSTD) archive_write_add_filter_zstd(arch); #endif } static _archive_read_ctx * fu_archive_read_new(GError **error) { g_autoptr(_archive_read_ctx) arch = NULL; arch = archive_read_new(); if (arch == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "libarchive startup failed"); return NULL; } archive_read_support_format_all(arch); archive_read_support_filter_all(arch); return g_steal_pointer(&arch); } static gboolean fu_archive_read(FuArchive *self, _archive_read_ctx *arch, FuArchiveFlags flags, GError **error) { int r; while (TRUE) { const gchar *fn; gint64 bufsz; gssize rc; struct archive_entry *entry; g_autofree gchar *fn_key = NULL; g_autofree guint8 *buf = NULL; g_autoptr(GBytes) bytes = NULL; r = archive_read_next_header(arch, &entry); if (r == ARCHIVE_EOF) break; if (r != ARCHIVE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "cannot read header: %s", archive_error_string(arch)); return FALSE; } /* only extract if valid */ fn = archive_entry_pathname(entry); if (fn == NULL) continue; if (!archive_entry_size_is_set(entry)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "%s entry does not have size set", fn); return FALSE; } bufsz = archive_entry_size(entry); if (bufsz > 1024 * 1024 * 1024) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot read huge files"); return FALSE; } buf = g_malloc(bufsz); rc = archive_read_data(arch, buf, (gsize)bufsz); if (rc < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "cannot read data: %s", archive_error_string(arch)); return FALSE; } if (rc != bufsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "read %" G_GSSIZE_FORMAT " of %" G_GINT64_FORMAT, rc, bufsz); return FALSE; } if (flags & FU_ARCHIVE_FLAG_IGNORE_PATH) { fn_key = g_path_get_basename(fn); } else { fn_key = g_strdup(fn); } g_debug("adding %s [%" G_GINT64_FORMAT "]", fn_key, bufsz); bytes = g_bytes_new_take(g_steal_pointer(&buf), bufsz); fu_archive_add_entry(self, fn_key, bytes); } /* success */ return TRUE; } #endif /** * fu_archive_new: * @data: (nullable): archive contents * @flags: archive flags, e.g. %FU_ARCHIVE_FLAG_NONE * @error: (nullable): optional return location for an error * * Parses @data as an archive and decompresses all files to memory blobs. * * If @data is unspecified then a new empty archive is created. * * Returns: a #FuArchive, or %NULL if the archive was invalid in any way. * * Since: 1.2.2 **/ FuArchive * fu_archive_new(GBytes *data, FuArchiveFlags flags, GError **error) { #ifdef HAVE_LIBARCHIVE g_autoptr(FuArchive) self = g_object_new(FU_TYPE_ARCHIVE, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); if (data != NULL) { int r; g_autoptr(_archive_read_ctx) arch = NULL; arch = fu_archive_read_new(error); if (arch == NULL) return NULL; r = archive_read_open_memory(arch, (void *)g_bytes_get_data(data, NULL), (size_t)g_bytes_get_size(data)); if (r != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot open: %s", archive_error_string(arch)); return NULL; } if (!fu_archive_read(self, arch, flags, error)) return NULL; } return g_steal_pointer(&self); #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "missing libarchive support"); return NULL; #endif } #ifdef HAVE_LIBARCHIVE typedef struct { GInputStream *stream; guint8 buf[0x8000]; } FuArchiveStreamHelper; static gint64 fu_archive_skip_cb(struct archive *arch, void *client_data, off_t request) { FuArchiveStreamHelper *helper = (FuArchiveStreamHelper *)client_data; gssize cnt; g_autoptr(GError) error_local = NULL; cnt = g_input_stream_skip(helper->stream, request, NULL, &error_local); if (cnt < 0) { archive_set_error(arch, ARCHIVE_FAILED, "failed to read from stream: %s", error_local->message); return -1; } return cnt; } static gssize fu_archive_read_cb(struct archive *arch, void *client_data, const void **buffer) { FuArchiveStreamHelper *helper = (FuArchiveStreamHelper *)client_data; gssize cnt; g_autoptr(GError) error_local = NULL; cnt = g_input_stream_read(helper->stream, helper->buf, sizeof(helper->buf), NULL, &error_local); if (cnt < 0) { archive_set_error(arch, ARCHIVE_FAILED, "failed to read from stream: %s", error_local->message); return -1; } if (cnt > 0) *buffer = helper->buf; return cnt; } static GSeekType fu_archive_whence_to_seek_type(gint whence) { if (whence == SEEK_SET) return G_SEEK_SET; if (whence == SEEK_END) return G_SEEK_END; return G_SEEK_CUR; } static gint64 fu_archive_seek_cb(struct archive *arch, void *client_data, gint64 offset, gint whence) { FuArchiveStreamHelper *helper = (FuArchiveStreamHelper *)client_data; g_autoptr(GError) error_local = NULL; if (!g_seekable_seek(G_SEEKABLE(helper->stream), offset, fu_archive_whence_to_seek_type(whence), NULL, &error_local)) { archive_set_error(arch, ARCHIVE_FAILED, "failed to read from stream: %s", error_local->message); return -1; } return g_seekable_tell(G_SEEKABLE(helper->stream)); } #endif /** * fu_archive_new_stream: * @stream: a #GInputStream * @flags: archive flags, e.g. %FU_ARCHIVE_FLAG_NONE * @error: (nullable): optional return location for an error * * Parses @stream as an archive and decompresses all files to memory blobs. * * Returns: a #FuArchive, or %NULL if the archive was invalid in any way. * * Since: 2.0.0 **/ FuArchive * fu_archive_new_stream(GInputStream *stream, FuArchiveFlags flags, GError **error) { #ifdef HAVE_LIBARCHIVE g_autoptr(FuArchive) self = g_object_new(FU_TYPE_ARCHIVE, NULL); g_autoptr(_archive_read_ctx) arch = NULL; FuArchiveStreamHelper helper = {.stream = stream}; int r; g_return_val_if_fail(G_INPUT_STREAM(stream), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); if (!g_seekable_seek(G_SEEKABLE(stream), 0x0, G_SEEK_SET, NULL, error)) return NULL; arch = fu_archive_read_new(error); if (arch == NULL) return NULL; archive_read_set_seek_callback(arch, fu_archive_seek_cb); archive_read_set_read_callback(arch, fu_archive_read_cb); archive_read_set_skip_callback(arch, fu_archive_skip_cb); archive_read_set_callback_data(arch, &helper); r = archive_read_open1(arch); if (r != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot open: %s", archive_error_string(arch)); return NULL; } if (!fu_archive_read(self, arch, flags, error)) return NULL; return g_steal_pointer(&self); #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "missing libarchive support"); return NULL; #endif } #ifdef HAVE_LIBARCHIVE static gssize fu_archive_write_cb(struct archive *arch, void *user_data, const void *buf, gsize bufsz) { GByteArray *blob = (GByteArray *)user_data; g_byte_array_append(blob, buf, bufsz); return (gssize)bufsz; } #endif /** * fu_archive_write: * @self: a #FuArchive * @format: a compression, e.g. `FU_ARCHIVE_FORMAT_ZIP` * @compression: a compression, e.g. `FU_ARCHIVE_COMPRESSION_NONE` * @error: (nullable): optional return location for an error * * Writes an archive with specified @format and @compression. * * Returns: (transfer full): the archive blob * * Since: 1.8.1 **/ GByteArray * fu_archive_write(FuArchive *self, FuArchiveFormat format, FuArchiveCompression compression, GError **error) { #ifdef HAVE_LIBARCHIVE int r; g_autoptr(_archive_write_ctx) arch = NULL; g_autoptr(GByteArray) blob = g_byte_array_new(); g_autoptr(GList) keys = NULL; g_return_val_if_fail(FU_IS_ARCHIVE(self), NULL); g_return_val_if_fail(format != FU_ARCHIVE_FORMAT_UNKNOWN, NULL); g_return_val_if_fail(compression != FU_ARCHIVE_COMPRESSION_UNKNOWN, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* sanity check */ #ifndef HAVE_LIBARCHIVE_WRITE_ADD_COMPRESSION_ZSTD if (compression == FU_ARCHIVE_COMPRESSION_ZSTD) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "archive_write_add_filter_zstd() not supported"); return NULL; } #endif /* compress anything matching either glob */ arch = archive_write_new(); if (arch == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "libarchive startup failed"); return NULL; } fu_archive_set_format(arch, format); if (format == FU_ARCHIVE_FORMAT_ZIP) { if (compression != FU_ARCHIVE_COMPRESSION_NONE) archive_write_set_options(arch, "zip:compression=deflate"); } else { fu_archive_set_compression(arch, compression); } r = archive_write_open(arch, blob, NULL, fu_archive_write_cb, NULL); if (r != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot open: %s", archive_error_string(arch)); return NULL; } keys = g_hash_table_get_keys(self->entries); for (GList *l = keys; l != NULL; l = l->next) { const gchar *fn = l->data; GBytes *bytes = g_hash_table_lookup(self->entries, fn); gssize rc; g_autoptr(_archive_entry_ctx) entry = NULL; entry = archive_entry_new(); archive_entry_set_pathname(entry, fn); archive_entry_set_filetype(entry, AE_IFREG); archive_entry_set_perm(entry, 0644); archive_entry_set_size(entry, g_bytes_get_size(bytes)); r = archive_write_header(arch, entry); if (r != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot write header: %s", archive_error_string(arch)); return NULL; } rc = archive_write_data(arch, g_bytes_get_data(bytes, NULL), g_bytes_get_size(bytes)); if (rc < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "cannot write data: %s", archive_error_string(arch)); return NULL; } } r = archive_write_close(arch); if (r != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot close: %s", archive_error_string(arch)); return NULL; } /* success */ return g_steal_pointer(&blob); #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "missing libarchive support"); return NULL; #endif } fwupd-2.0.10/libfwupdplugin/fu-archive.h000066400000000000000000000036021501337203100201500ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2022 Gaël PORTAY * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-archive-struct.h" #define FU_TYPE_ARCHIVE (fu_archive_get_type()) G_DECLARE_FINAL_TYPE(FuArchive, fu_archive, FU, ARCHIVE, GObject) /** * FuArchiveFlags: * @FU_ARCHIVE_FLAG_NONE: No flags set * @FU_ARCHIVE_FLAG_IGNORE_PATH: Ignore any path component * * The flags to use when loading the archive. **/ typedef enum { FU_ARCHIVE_FLAG_NONE = 0, FU_ARCHIVE_FLAG_IGNORE_PATH = 1 << 0, /*< private >*/ FU_ARCHIVE_FLAG_LAST } FuArchiveFlags; /** * FuArchiveIterateFunc: * @self: a #FuArchive * @filename: a filename * @bytes: the blob referenced by @filename * @user_data: user data * @error: a #GError or NULL * * The archive iteration callback. */ typedef gboolean (*FuArchiveIterateFunc)(FuArchive *self, const gchar *filename, GBytes *bytes, gpointer user_data, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); FuArchive * fu_archive_new(GBytes *data, FuArchiveFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT; FuArchive * fu_archive_new_stream(GInputStream *stream, FuArchiveFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT; void fu_archive_add_entry(FuArchive *self, const gchar *fn, GBytes *blob) G_GNUC_NON_NULL(1, 2); GBytes * fu_archive_lookup_by_fn(FuArchive *self, const gchar *fn, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); GByteArray * fu_archive_write(FuArchive *self, FuArchiveFormat format, FuArchiveCompression compression, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_archive_iterate(FuArchive *self, FuArchiveIterateFunc callback, gpointer user_data, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-archive.rs000066400000000000000000000021221501337203100203410ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(FromString, ToString)] enum FuArchiveFormat { Unknown, // Since: 1.8.1 Cpio, // Since: 1.8.1 Shar, // Since: 1.8.1 Tar, // Since: 1.8.1 Ustar, // Since: 1.8.1 Pax, // Since: 1.8.1 Gnutar, // Since: 1.8.1 Iso9660, // Since: 1.8.1 Zip, // Since: 1.8.1 Ar, // Since: 1.8.1 ArSvr4, // Since: 1.8.1 Mtree, // Since: 1.8.1 Raw, // Since: 1.8.1 Xar, // Since: 1.8.1 7zip, // Since: 1.8.1 Warc, // Since: 1.8.1 } #[derive(FromString, ToString)] enum FuArchiveCompression { Unknown, // Since: 1.8.1 None, // Since: 1.8.1 Gzip, // Since: 1.8.1 Bzip2, // Since: 1.8.1 Compress, // Since: 1.8.1 Lzma, // Since: 1.8.1 Xz, // Since: 1.8.1 Uu, // Since: 1.8.1 Lzip, // Since: 1.8.1 Lrzip, // Since: 1.8.1 Lzop, // Since: 1.8.1 Grzip, // Since: 1.8.1 Lz4, // Since: 1.8.1 Zstd, // Since: 1.8.1 } fwupd-2.0.10/libfwupdplugin/fu-backend-private.h000066400000000000000000000003341501337203100215650ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-backend.h" gchar * fu_backend_get_emulation_array_member_name(FuBackend *self); fwupd-2.0.10/libfwupdplugin/fu-backend.c000066400000000000000000000613571501337203100201240ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuBackend" #include "config.h" #include "fu-backend-private.h" #include "fu-device-private.h" #include "fu-string.h" /** * FuBackend: * * An device discovery backend, for instance USB, BlueZ or UDev. * * See also: [class@FuDevice] */ typedef struct { FuContext *ctx; gchar *name; gboolean enabled; gboolean done_setup; gboolean can_invalidate; GType device_gtype; GHashTable *devices; /* device_id : * FuDevice */ GThread *thread_init; } FuBackendPrivate; enum { SIGNAL_ADDED, SIGNAL_REMOVED, SIGNAL_CHANGED, SIGNAL_LAST }; enum { PROP_0, PROP_NAME, PROP_CAN_INVALIDATE, PROP_CONTEXT, PROP_DEVICE_GTYPE, PROP_LAST }; static guint signals[SIGNAL_LAST] = {0}; static void fu_backend_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_EXTENDED(FuBackend, fu_backend, G_TYPE_OBJECT, 0, G_ADD_PRIVATE(FuBackend) G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fu_backend_codec_iface_init)); #define GET_PRIVATE(o) (fu_backend_get_instance_private(o)) /** * fu_backend_device_added: * @self: a #FuBackend * @device: a device * * Emits a signal that indicates the device has been added. * * Since: 1.6.1 **/ void fu_backend_device_added(FuBackend *self, FuDevice *device) { FuBackendPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_BACKEND(self)); g_return_if_fail(FU_IS_DEVICE(device)); g_return_if_fail(priv->thread_init == g_thread_self()); /* assign context if set */ if (priv->ctx != NULL) fu_device_set_context(device, priv->ctx); /* we set this here to be able to get the parent in plugins */ fu_device_set_backend(device, self); /* set backend ID if required */ if (fu_device_get_backend_id(device) == NULL) fu_device_set_backend_id(device, priv->name); /* set created to *now* */ if (fu_device_get_created_usec(device) == 0) fu_device_set_created_usec(device, g_get_real_time()); /* sanity check */ if ((g_getenv("FWUPD_UEFI_TEST") == NULL) && g_hash_table_contains(priv->devices, fu_device_get_backend_id(device))) { g_warning("replacing existing device with backend_id %s", fu_device_get_backend_id(device)); } /* add */ g_hash_table_insert(priv->devices, g_strdup(fu_device_get_backend_id(device)), g_object_ref(device)); g_signal_emit(self, signals[SIGNAL_ADDED], 0, device); } /** * fu_backend_device_removed: * @self: a #FuBackend * @device: a device * * Emits a signal that indicates the device has been removed. * * Since: 1.6.1 **/ void fu_backend_device_removed(FuBackend *self, FuDevice *device) { FuBackendPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_BACKEND(self)); g_return_if_fail(FU_IS_DEVICE(device)); g_return_if_fail(priv->thread_init == g_thread_self()); g_signal_emit(self, signals[SIGNAL_REMOVED], 0, device); g_hash_table_remove(priv->devices, fu_device_get_backend_id(device)); } /** * fu_backend_device_changed: * @self: a #FuBackend * @device: a device * * Emits a signal that indicates the device has been changed. * * Since: 1.6.1 **/ void fu_backend_device_changed(FuBackend *self, FuDevice *device) { FuBackendPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_BACKEND(self)); g_return_if_fail(FU_IS_DEVICE(device)); g_return_if_fail(priv->thread_init == g_thread_self()); g_signal_emit(self, signals[SIGNAL_CHANGED], 0, device); } /** * fu_backend_registered: * @self: a #FuBackend * @device: a device * * Calls the ->registered() vfunc for the backend. This allows the backend to perform shared * backend actions on superclassed devices. * * Since: 1.7.4 **/ void fu_backend_registered(FuBackend *self, FuDevice *device) { FuBackendClass *klass = FU_BACKEND_GET_CLASS(self); g_return_if_fail(FU_IS_BACKEND(self)); g_return_if_fail(FU_IS_DEVICE(device)); if (klass->registered != NULL) klass->registered(self, device); } /** * fu_backend_get_device_parent: * @self: a #FuBackend * @device: a #FuDevice * @subsystem: (nullable): an optional device subsystem, e.g. "usb:usb_device" * @error: (nullable): optional return location for an error * * Asks the backend to create the parent device (of the correct type) for a given device subsystem. * * Returns: (transfer full): a #FuDevice or %NULL if not found or unimplemented * * Since: 2.0.0 **/ FuDevice * fu_backend_get_device_parent(FuBackend *self, FuDevice *device, const gchar *subsystem, GError **error) { FuBackendClass *klass = FU_BACKEND_GET_CLASS(self); g_return_val_if_fail(FU_IS_BACKEND(self), NULL); g_return_val_if_fail(FU_IS_DEVICE(device), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); if (klass->get_device_parent == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "->get_device_parent is not implemented in %s", G_OBJECT_TYPE_NAME(self)); return NULL; } return klass->get_device_parent(self, device, subsystem, error); } /** * fu_backend_create_device: * @self: a #FuBackend * @backend_id: a backend ID, typically a sysfs path * @error: (nullable): optional return location for an error * * Asks the backend to create a device (of the correct type) for a given device backend ID. * * Returns: (transfer full): a #FuDevice or %NULL if not found or unimplemented * * Since: 2.0.0 **/ FuDevice * fu_backend_create_device(FuBackend *self, const gchar *backend_id, GError **error) { FuBackendClass *klass = FU_BACKEND_GET_CLASS(self); g_autoptr(FuDevice) device = NULL; g_return_val_if_fail(FU_IS_BACKEND(self), NULL); g_return_val_if_fail(backend_id != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); if (klass->create_device == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "->create_device is not implemented in %s", G_OBJECT_TYPE_NAME(self)); return NULL; } device = klass->create_device(self, backend_id, error); if (device == NULL) return NULL; fu_device_set_backend(device, self); /* success */ return g_steal_pointer(&device); } /** * fu_backend_create_device_for_donor: * @self: a #FuBackend * @donor: a donor #FuDevice * @error: (nullable): optional return location for an error * * Asks the backend to create a device (of the correct type) for a given donor device. * * Returns: (transfer full): a #FuDevice or %NULL if not found or unimplemented * * Since: 2.0.0 **/ FuDevice * fu_backend_create_device_for_donor(FuBackend *self, FuDevice *donor, GError **error) { FuBackendClass *klass = FU_BACKEND_GET_CLASS(self); g_autoptr(FuDevice) device = NULL; g_return_val_if_fail(FU_IS_BACKEND(self), NULL); g_return_val_if_fail(FU_IS_DEVICE(donor), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* no impl */ if (klass->create_device_for_donor == NULL) return g_object_ref(donor); /* impl */ device = klass->create_device_for_donor(self, donor, error); if (device == NULL) return NULL; fu_device_set_backend(device, self); /* success */ return g_steal_pointer(&device); } /** * fu_backend_invalidate: * @self: a #FuBackend * * Normally when calling [method@FuBackend.setup] multiple times it is only actually done once. * Calling this method causes the next requests to [method@FuBackend.setup] to actually probe the * hardware. * * Only subclassed backends setting `can-invalidate=TRUE` at construction time can use this * method, as it is not always safe to call for backends shared between multiple plugins. * * This should be done in case the backing information source has changed, for instance if * a platform subsystem has been updated. * * This also optionally calls the ->invalidate() vfunc for the backend. This allows the backend * to perform shared backend actions on superclassed devices. * * Since: 1.8.0 **/ void fu_backend_invalidate(FuBackend *self) { FuBackendClass *klass = FU_BACKEND_GET_CLASS(self); FuBackendPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_BACKEND(self)); g_return_if_fail(priv->can_invalidate); priv->done_setup = FALSE; if (klass->invalidate != NULL) klass->invalidate(self); } /** * fu_backend_add_string: * @self: a #FuBackend * @idt: indent level * @str: a string to append to * * Add backend-specific device metadata to an existing string. * * Since: 1.8.4 **/ void fu_backend_add_string(FuBackend *self, guint idt, GString *str) { FuBackendClass *klass = FU_BACKEND_GET_CLASS(self); FuBackendPrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append(str, idt, G_OBJECT_TYPE_NAME(self), ""); fwupd_codec_string_append(str, idt + 1, "Name", priv->name); fwupd_codec_string_append_bool(str, idt + 1, "Enabled", priv->enabled); fwupd_codec_string_append_bool(str, idt + 1, "DoneSetup", priv->done_setup); fwupd_codec_string_append_bool(str, idt + 1, "CanInvalidate", priv->can_invalidate); /* subclassed */ if (klass->to_string != NULL) klass->to_string(self, idt, str); } /** * fu_backend_setup: * @self: a #FuBackend * @flags: some #FuBackendSetupFlags, e.g. %FU_BACKEND_SETUP_FLAG_USE_HOTPLUG * @progress: a #FuProgress * @error: (nullable): optional return location for an error * * Sets up the backend ready for use, which typically calls the subclassed setup * function. No devices should be added or removed at this point. * * Returns: %TRUE for success * * Since: 1.6.1 **/ gboolean fu_backend_setup(FuBackend *self, FuBackendSetupFlags flags, FuProgress *progress, GError **error) { FuBackendClass *klass = FU_BACKEND_GET_CLASS(self); FuBackendPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_BACKEND(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (priv->done_setup) return TRUE; if (klass->setup != NULL) { if (!klass->setup(self, flags, progress, error)) { priv->enabled = FALSE; return FALSE; } } priv->done_setup = TRUE; return TRUE; } static gboolean fu_backend_from_json(FwupdCodec *codec, JsonNode *json_node, GError **error) { FuBackend *self = FU_BACKEND(codec); FuBackendPrivate *priv = GET_PRIVATE(self); JsonArray *json_array; JsonObject *json_object; const gchar *fwupd_version; g_autoptr(GPtrArray) devices_added = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_autoptr(GPtrArray) devices_remove = NULL; /* no registered specialized GType */ if (priv->device_gtype == FU_TYPE_DEVICE) return TRUE; /* sanity check */ if (!JSON_NODE_HOLDS_OBJECT(json_node)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not JSON object"); return FALSE; } json_object = json_node_get_object(json_node); /* remain compatible with all the old emulation files */ if (!json_object_has_member(json_object, "UsbDevices")) return TRUE; /* if recorded */ fwupd_version = json_object_get_string_member_with_default(json_object, "FwupdVersion", NULL); /* four steps: * * 1. store all the existing devices matching the tag in devices_remove * 2. read the devices in the array: * - if the platform-id exists: replace the event data & remove from devices_remove * - otherwise add to devices_added * 3. emit devices in devices_remove * 4. emit devices in devices_added */ devices_remove = fu_backend_get_devices(self); json_array = json_object_get_array_member(json_object, "UsbDevices"); for (guint i = 0; i < json_array_get_length(json_array); i++) { JsonNode *node_tmp = json_array_get_element(json_array, i); JsonObject *object_tmp; FuDevice *device_old; g_autoptr(FuDevice) device_tmp = NULL; const gchar *device_gtypestr; GType device_gtype; /* sanity check */ if (!JSON_NODE_HOLDS_OBJECT(node_tmp)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not JSON object"); return FALSE; } object_tmp = json_node_get_object(node_tmp); /* get the GType */ device_gtypestr = json_object_get_string_member_with_default(object_tmp, "GType", "FuUsbDevice"); device_gtype = g_type_from_name(device_gtypestr); if (device_gtype == G_TYPE_INVALID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unknown GType name %s", device_gtypestr); return FALSE; } if (!g_type_is_a(device_gtype, priv->device_gtype)) continue; /* create device */ device_tmp = g_object_new(device_gtype, "context", priv->ctx, NULL); fu_device_add_flag(device_tmp, FWUPD_DEVICE_FLAG_EMULATED); if (fwupd_version != NULL) fu_device_set_fwupd_version(device_tmp, fwupd_version); if (!fu_device_from_json(device_tmp, object_tmp, error)) return FALSE; if (fu_device_get_backend_id(device_tmp) == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no backend specified %s", device_gtypestr); return FALSE; } /* does a device with this platform ID [and the same created date] already exist */ device_old = fu_backend_lookup_by_id(self, fu_device_get_backend_id(device_tmp)); /* yes, and it has the same timestamp */ if (device_old != NULL) { g_debug("created timestamp %" G_GINT64_FORMAT "->%" G_GINT64_FORMAT, fu_device_get_created_usec(device_old), fu_device_get_created_usec(device_tmp)); } if (device_old != NULL && fu_device_get_created_usec(device_old) == fu_device_get_created_usec(device_tmp)) { GPtrArray *events = fu_device_get_events(device_tmp); fu_device_clear_events(device_old); for (guint j = 0; j < events->len; j++) { FuDeviceEvent *event = g_ptr_array_index(events, j); fu_device_add_event(device_old, event); } g_debug("changed %s [%s]", fu_device_get_name(device_tmp), fu_device_get_backend_id(device_tmp)); fu_backend_device_changed(self, device_old); g_ptr_array_remove(devices_remove, device_old); continue; } /* new to us! */ g_debug("not found %s [%s], adding", fu_device_get_name(device_tmp), fu_device_get_backend_id(device_tmp)); g_ptr_array_add(devices_added, g_object_ref(device_tmp)); } /* emit removes then adds */ for (guint i = 0; i < devices_remove->len; i++) { FuDevice *device = g_ptr_array_index(devices_remove, i); if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) continue; fu_backend_device_removed(self, device); } for (guint i = 0; i < devices_added->len; i++) { FuDevice *donor = g_ptr_array_index(devices_added, i); g_autoptr(FuDevice) device = NULL; g_autoptr(GError) error_local = NULL; /* convert from FuUdevDevice to the superclass, e.g. FuHidrawDevice */ if (!fu_device_probe(donor, &error_local)) { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } device = fu_backend_create_device_for_donor(self, donor, error); if (device == NULL) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_EMULATED); /* some devices only add plugin-matching instance IDs in FuDevice->setup() */ if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_EMULATED_REQUIRE_SETUP)) { g_autoptr(FuDeviceLocker) locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; } fu_backend_device_added(self, device); } /* success */ return TRUE; } static void fu_backend_add_json(FwupdCodec *codec, JsonBuilder *builder, FwupdCodecFlags flags) { FuBackend *self = FU_BACKEND(codec); FuBackendPrivate *priv = GET_PRIVATE(self); g_autoptr(GList) devices = NULL; /* remain compatible with all the old emulation files */ fwupd_codec_json_append(builder, "FwupdVersion", PACKAGE_VERSION); json_builder_set_member_name(builder, "UsbDevices"); json_builder_begin_array(builder); devices = g_hash_table_get_values(priv->devices); for (GList *l = devices; l != NULL; l = l->next) { FuDevice *device = FU_DEVICE(l->data); if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATION_TAG)) continue; json_builder_begin_object(builder); fu_device_add_json(device, builder, FWUPD_CODEC_FLAG_NONE); json_builder_end_object(builder); } json_builder_end_array(builder); } /** * fu_backend_coldplug: * @self: a #FuBackend * @progress: a #FuProgress * @error: (nullable): optional return location for an error * * Adds devices using the subclassed backend. If [method@FuBackend.setup] has not * already been called then it is run before this function automatically. * * Returns: %TRUE for success * * Since: 1.6.1 **/ gboolean fu_backend_coldplug(FuBackend *self, FuProgress *progress, GError **error) { FuBackendClass *klass = FU_BACKEND_GET_CLASS(self); g_return_val_if_fail(FU_IS_BACKEND(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_backend_setup(self, FU_BACKEND_SETUP_FLAG_NONE, progress, error)) return FALSE; if (klass->coldplug == NULL) return TRUE; return klass->coldplug(self, progress, error); } /** * fu_backend_get_name: * @self: a #FuBackend * * Return the name of the backend, which is normally set by the subclass. * * Returns: backend name * * Since: 1.6.1 **/ const gchar * fu_backend_get_name(FuBackend *self) { FuBackendPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_BACKEND(self), NULL); return priv->name; } /** * fu_backend_get_context: * @self: a #FuBackend * * Gets the context for a backend. * * Returns: (transfer none): a #FuContext or %NULL if not set * * Since: 1.6.1 **/ FuContext * fu_backend_get_context(FuBackend *self) { FuBackendPrivate *priv = GET_PRIVATE(self); return priv->ctx; } /** * fu_backend_get_enabled: * @self: a #FuBackend * * Return the boolean value of a key if it's been configured * * Returns: %TRUE if the backend is enabled * * Since: 1.6.1 **/ gboolean fu_backend_get_enabled(FuBackend *self) { FuBackendPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_BACKEND(self), FALSE); return priv->enabled; } /** * fu_backend_set_enabled: * @self: a #FuBackend * @enabled: enabled state * * Sets the backend enabled state. * * Since: 1.6.1 **/ void fu_backend_set_enabled(FuBackend *self, gboolean enabled) { FuBackendPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_BACKEND(self)); priv->enabled = FALSE; } /** * fu_backend_lookup_by_id: * @self: a #FuBackend * @backend_id: a device backend ID * * Gets a device previously added by the backend. * * Returns: (transfer none): device, or %NULL if not found * * Since: 1.6.1 **/ FuDevice * fu_backend_lookup_by_id(FuBackend *self, const gchar *backend_id) { FuBackendPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_BACKEND(self), NULL); g_return_val_if_fail(backend_id != NULL, NULL); return g_hash_table_lookup(priv->devices, backend_id); } static gint fu_backend_get_devices_sort_cb(gconstpointer a, gconstpointer b) { FuDevice *deva = *((FuDevice **)a); FuDevice *devb = *((FuDevice **)b); return g_strcmp0(fu_device_get_backend_id(deva), fu_device_get_backend_id(devb)); } /** * fu_backend_get_devices: * @self: a #FuBackend * * Gets all the devices added by the backend. * * Returns: (transfer container) (element-type FuDevice): devices * * Since: 1.6.1 **/ GPtrArray * fu_backend_get_devices(FuBackend *self) { FuBackendPrivate *priv = GET_PRIVATE(self); g_autoptr(GList) values = NULL; g_autoptr(GPtrArray) devices = NULL; g_return_val_if_fail(FU_IS_BACKEND(self), NULL); devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); values = g_hash_table_get_values(priv->devices); for (GList *l = values; l != NULL; l = l->next) g_ptr_array_add(devices, g_object_ref(l->data)); g_ptr_array_sort(devices, fu_backend_get_devices_sort_cb); return g_steal_pointer(&devices); } static void fu_backend_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuBackend *self = FU_BACKEND(object); FuBackendPrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_NAME: g_value_set_string(value, priv->name); break; case PROP_CAN_INVALIDATE: g_value_set_boolean(value, priv->can_invalidate); break; case PROP_CONTEXT: g_value_set_object(value, priv->ctx); break; case PROP_DEVICE_GTYPE: g_value_set_gtype(value, priv->device_gtype); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_backend_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuBackend *self = FU_BACKEND(object); FuBackendPrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_NAME: priv->name = g_value_dup_string(value); break; case PROP_CAN_INVALIDATE: priv->can_invalidate = g_value_get_boolean(value); break; case PROP_CONTEXT: g_set_object(&priv->ctx, g_value_get_object(value)); break; case PROP_DEVICE_GTYPE: priv->device_gtype = g_value_get_gtype(value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_backend_codec_iface_init(FwupdCodecInterface *iface) { iface->add_json = fu_backend_add_json; iface->from_json = fu_backend_from_json; } static void fu_backend_init(FuBackend *self) { FuBackendPrivate *priv = GET_PRIVATE(self); priv->enabled = TRUE; priv->device_gtype = FU_TYPE_DEVICE; priv->thread_init = g_thread_self(); priv->devices = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_object_unref); } static void fu_backend_dispose(GObject *object) { FuBackend *self = FU_BACKEND(object); FuBackendPrivate *priv = GET_PRIVATE(self); g_hash_table_remove_all(priv->devices); g_clear_object(&priv->ctx); G_OBJECT_CLASS(fu_backend_parent_class)->dispose(object); } static void fu_backend_finalize(GObject *object) { FuBackend *self = FU_BACKEND(object); FuBackendPrivate *priv = GET_PRIVATE(self); g_free(priv->name); g_hash_table_unref(priv->devices); G_OBJECT_CLASS(fu_backend_parent_class)->finalize(object); } static void fu_backend_class_init(FuBackendClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); GParamSpec *pspec; object_class->get_property = fu_backend_get_property; object_class->set_property = fu_backend_set_property; object_class->finalize = fu_backend_finalize; object_class->dispose = fu_backend_dispose; /** * FuBackend:name: * * The backend name. * * Since: 1.6.1 */ pspec = g_param_spec_string("name", NULL, NULL, NULL, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_NAME, pspec); /** * FuBackend:can-invalidate: * * If the backend can be invalidated. * * Since: 1.8.0 */ pspec = g_param_spec_boolean("can-invalidate", NULL, NULL, FALSE, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_CAN_INVALIDATE, pspec); /** * FuBackend:context: * * The #FuContent to use. * * Since: 1.6.1 */ pspec = g_param_spec_object("context", NULL, NULL, FU_TYPE_CONTEXT, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_CONTEXT, pspec); /** * FuBackend:device-gtype: * * The #GType to use when creating emulated devices. * * Since: 2.0.0 */ pspec = g_param_spec_gtype("device-gtype", NULL, NULL, FU_TYPE_DEVICE, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_DEVICE_GTYPE, pspec); /** * FuBackend::device-added: * @self: the #FuBackend instance that emitted the signal * @device: the #FuDevice * * The ::device-added signal is emitted when a device has been added. * * Since: 1.6.1 **/ signals[SIGNAL_ADDED] = g_signal_new("device-added", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, FU_TYPE_DEVICE); /** * FuBackend::device-removed: * @self: the #FuBackend instance that emitted the signal * @device: the #FuDevice * * The ::device-removed signal is emitted when a device has been removed. * * Since: 1.6.1 **/ signals[SIGNAL_REMOVED] = g_signal_new("device-removed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, FU_TYPE_DEVICE); /** * FuBackend::device-changed: * @self: the #FuBackend instance that emitted the signal * @device: the #FuDevice * * The ::device-changed signal is emitted when a device has been changed. * * Since: 1.6.1 **/ signals[SIGNAL_CHANGED] = g_signal_new("device-changed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, FU_TYPE_DEVICE); } fwupd-2.0.10/libfwupdplugin/fu-backend.h000066400000000000000000000062671501337203100201300ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-context.h" #include "fu-device.h" #define FU_TYPE_BACKEND (fu_backend_get_type()) G_DECLARE_DERIVABLE_TYPE(FuBackend, fu_backend, FU, BACKEND, GObject) typedef enum { /** * FU_BACKEND_SETUP_FLAG_NONE: * * No flags set. * * Since: 2.0.0 **/ FU_BACKEND_SETUP_FLAG_NONE = 0, /** * FU_BACKEND_SETUP_FLAG_USE_HOTPLUG: * * Set up hotplug events for updates (not used in tests). * * Since: 2.0.0 **/ FU_BACKEND_SETUP_FLAG_USE_HOTPLUG = 1 << 0, } FuBackendSetupFlags; struct _FuBackendClass { GObjectClass parent_class; /* signals */ gboolean (*setup)(FuBackend *self, FuBackendSetupFlags flags, FuProgress *progress, GError **error) G_GNUC_WARN_UNUSED_RESULT; gboolean (*coldplug)(FuBackend *self, FuProgress *progress, GError **error) G_GNUC_WARN_UNUSED_RESULT; void (*registered)(FuBackend *self, FuDevice *device); void (*invalidate)(FuBackend *self); void (*to_string)(FuBackend *self, guint indent, GString *str); FuDevice *(*get_device_parent)(FuBackend *self, FuDevice *device, const gchar *subsystem, GError **error)G_GNUC_WARN_UNUSED_RESULT; FuDevice *(*create_device)(FuBackend *self, const gchar *backend_id, GError **error)G_GNUC_WARN_UNUSED_RESULT; FuDevice *(*create_device_for_donor)(FuBackend *self, FuDevice *donor, GError **error)G_GNUC_WARN_UNUSED_RESULT; }; const gchar * fu_backend_get_name(FuBackend *self) G_GNUC_NON_NULL(1); FuContext * fu_backend_get_context(FuBackend *self) G_GNUC_NON_NULL(1); gboolean fu_backend_get_enabled(FuBackend *self) G_GNUC_NON_NULL(1); void fu_backend_set_enabled(FuBackend *self, gboolean enabled) G_GNUC_NON_NULL(1); GPtrArray * fu_backend_get_devices(FuBackend *self) G_GNUC_NON_NULL(1); FuDevice * fu_backend_lookup_by_id(FuBackend *self, const gchar *backend_id) G_GNUC_NON_NULL(1, 2); gboolean fu_backend_setup(FuBackend *self, FuBackendSetupFlags flags, FuProgress *progress, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_backend_coldplug(FuBackend *self, FuProgress *progress, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); void fu_backend_device_added(FuBackend *self, FuDevice *device) G_GNUC_NON_NULL(1, 2); void fu_backend_device_removed(FuBackend *self, FuDevice *device) G_GNUC_NON_NULL(1, 2); void fu_backend_device_changed(FuBackend *self, FuDevice *device) G_GNUC_NON_NULL(1, 2); void fu_backend_registered(FuBackend *self, FuDevice *device) G_GNUC_NON_NULL(1, 2); void fu_backend_invalidate(FuBackend *self) G_GNUC_NON_NULL(1); void fu_backend_add_string(FuBackend *self, guint idt, GString *str) G_GNUC_NON_NULL(1, 3); FuDevice * fu_backend_get_device_parent(FuBackend *self, FuDevice *device, const gchar *subsystem, GError **error) G_GNUC_NON_NULL(1, 2); FuDevice * fu_backend_create_device(FuBackend *self, const gchar *backend_id, GError **error) G_GNUC_NON_NULL(1, 2); FuDevice * fu_backend_create_device_for_donor(FuBackend *self, FuDevice *donor, GError **error) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-bios-setting.c000066400000000000000000000027341501337203100211360ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuBiosSettings" #include "config.h" #include #include "fu-bios-setting.h" #include "fu-io-channel.h" struct _FuBiosSetting { FwupdBiosSetting parent_instance; }; G_DEFINE_TYPE(FuBiosSetting, fu_bios_setting, FWUPD_TYPE_BIOS_SETTING) static gboolean fu_bios_setting_write_value(FwupdBiosSetting *self, const gchar *value, GError **error) { g_autofree gchar *fn = NULL; g_autoptr(FuIOChannel) io = NULL; if (fwupd_bios_setting_get_path(self) == NULL) return TRUE; fn = g_build_filename(fwupd_bios_setting_get_path(self), "current_value", NULL); io = fu_io_channel_new_file(fn, FU_IO_CHANNEL_OPEN_FLAG_WRITE, error); if (io == NULL) return FALSE; if (!fu_io_channel_write_raw(io, (const guint8 *)value, strlen(value), 1000, FU_IO_CHANNEL_FLAG_NONE, error)) return FALSE; /* success */ fwupd_bios_setting_set_current_value(self, value); g_debug("set %s to %s", fwupd_bios_setting_get_id(self), value); return TRUE; } static void fu_bios_setting_class_init(FuBiosSettingClass *klass) { FwupdBiosSettingClass *bios_setting_class = FWUPD_BIOS_SETTING_CLASS(klass); bios_setting_class->write_value = fu_bios_setting_write_value; } static void fu_bios_setting_init(FuBiosSetting *self) { } FwupdBiosSetting * fu_bios_setting_new(void) { return g_object_new(FU_TYPE_BIOS_SETTING, NULL); } fwupd-2.0.10/libfwupdplugin/fu-bios-setting.h000066400000000000000000000005251501337203100211370ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_BIOS_SETTING (fu_bios_setting_get_type()) G_DECLARE_FINAL_TYPE(FuBiosSetting, fu_bios_setting, FU, BIOS_SETTING, FwupdBiosSetting) FwupdBiosSetting * fu_bios_setting_new(void); fwupd-2.0.10/libfwupdplugin/fu-bios-settings-private.h000066400000000000000000000010571501337203100227730ustar00rootroot00000000000000/* * Copyright 2022 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-bios-settings.h" gboolean fu_bios_settings_setup(FuBiosSettings *self, GError **error) G_GNUC_NON_NULL(1); GPtrArray * fu_bios_settings_get_all(FuBiosSettings *self) G_GNUC_NON_NULL(1); GHashTable * fu_bios_settings_to_hash_kv(FuBiosSettings *self) G_GNUC_NON_NULL(1); void fu_bios_settings_add_attribute(FuBiosSettings *self, FwupdBiosSetting *attr) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-bios-settings.c000066400000000000000000000440411501337203100213160ustar00rootroot00000000000000/* * Copyright 2022 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuBiosSettings" #include "config.h" #include #include "fwupd-error.h" #include "fu-bios-setting.h" #include "fu-bios-settings-private.h" #include "fu-common.h" #include "fu-path.h" #include "fu-string.h" #define LENOVO_READ_ONLY_NEEDLE "[Status:ShowOnly]" struct _FuBiosSettings { GObject parent_instance; GHashTable *descriptions; GHashTable *read_only; GPtrArray *attrs; }; static void fu_bios_settings_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_WITH_CODE(FuBiosSettings, fu_bios_settings, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fu_bios_settings_codec_iface_init)) static void fu_bios_settings_finalize(GObject *obj) { FuBiosSettings *self = FU_BIOS_SETTINGS(obj); g_ptr_array_unref(self->attrs); g_hash_table_unref(self->descriptions); g_hash_table_unref(self->read_only); G_OBJECT_CLASS(fu_bios_settings_parent_class)->finalize(obj); } static void fu_bios_settings_class_init(FuBiosSettingsClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_bios_settings_finalize; } static gboolean fu_bios_setting_get_key(FwupdBiosSetting *attr, /* nocheck:name */ const gchar *key, gchar **value_out, GError **error) { g_autofree gchar *tmp = NULL; g_return_val_if_fail(FWUPD_IS_BIOS_SETTING(attr), FALSE); g_return_val_if_fail(&value_out != NULL, FALSE); tmp = g_build_filename(fwupd_bios_setting_get_path(attr), key, NULL); if (!g_file_get_contents(tmp, value_out, NULL, error)) { g_prefix_error(error, "failed to load %s: ", key); fu_error_convert(error); return FALSE; } g_strchomp(*value_out); return TRUE; } static gboolean fu_bios_settings_set_description(FuBiosSettings *self, FwupdBiosSetting *attr, GError **error) { g_autofree gchar *data = NULL; const gchar *value; g_return_val_if_fail(FWUPD_IS_BIOS_SETTING(attr), FALSE); /* Try ID, then name, and then key */ value = g_hash_table_lookup(self->descriptions, fwupd_bios_setting_get_id(attr)); if (value != NULL) { fwupd_bios_setting_set_description(attr, value); return TRUE; } value = g_hash_table_lookup(self->descriptions, fwupd_bios_setting_get_name(attr)); if (value != NULL) { fwupd_bios_setting_set_description(attr, value); return TRUE; } if (!fu_bios_setting_get_key(attr, "display_name", &data, error)) return FALSE; fwupd_bios_setting_set_description(attr, data); return TRUE; } static guint64 fu_bios_setting_get_key_as_integer(FwupdBiosSetting *attr, /* nocheck:name */ const gchar *key, GError **error) { g_autofree gchar *str = NULL; guint64 tmp; if (!fu_bios_setting_get_key(attr, key, &str, error)) return G_MAXUINT64; if (!fu_strtoull(str, &tmp, 0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "failed to convert %s to integer: ", key); return G_MAXUINT64; } return tmp; } static gboolean fu_bios_setting_set_enumeration_attrs(FwupdBiosSetting *attr, GError **error) /* nocheck:name */ { const gchar *delimiters[] = {",", ";", NULL}; g_autofree gchar *str = NULL; if (!fu_bios_setting_get_key(attr, "possible_values", &str, error)) return FALSE; for (guint j = 0; delimiters[j] != NULL; j++) { g_auto(GStrv) vals = NULL; if (g_strrstr(str, delimiters[j]) == NULL) continue; vals = fu_strsplit(str, strlen(str), delimiters[j], -1); if (vals[0] != NULL) fwupd_bios_setting_set_kind(attr, FWUPD_BIOS_SETTING_KIND_ENUMERATION); for (guint i = 0; vals[i] != NULL && vals[i][0] != '\0'; i++) fwupd_bios_setting_add_possible_value(attr, vals[i]); } return TRUE; } static gboolean fu_bios_setting_set_string_attrs(FwupdBiosSetting *attr, GError **error) /* nocheck:name */ { guint64 tmp; tmp = fu_bios_setting_get_key_as_integer(attr, "min_length", error); if (tmp == G_MAXUINT64) return FALSE; fwupd_bios_setting_set_lower_bound(attr, tmp); tmp = fu_bios_setting_get_key_as_integer(attr, "max_length", error); if (tmp == G_MAXUINT64) return FALSE; fwupd_bios_setting_set_upper_bound(attr, tmp); fwupd_bios_setting_set_kind(attr, FWUPD_BIOS_SETTING_KIND_STRING); return TRUE; } static gboolean fu_bios_setting_set_integer_attrs(FwupdBiosSetting *attr, GError **error) /* nocheck:name */ { guint64 tmp; tmp = fu_bios_setting_get_key_as_integer(attr, "min_value", error); if (tmp == G_MAXUINT64) return FALSE; fwupd_bios_setting_set_lower_bound(attr, tmp); tmp = fu_bios_setting_get_key_as_integer(attr, "max_value", error); if (tmp == G_MAXUINT64) return FALSE; fwupd_bios_setting_set_upper_bound(attr, tmp); tmp = fu_bios_setting_get_key_as_integer(attr, "scalar_increment", error); if (tmp == G_MAXUINT64) return FALSE; fwupd_bios_setting_set_scalar_increment(attr, tmp); fwupd_bios_setting_set_kind(attr, FWUPD_BIOS_SETTING_KIND_INTEGER); return TRUE; } static gboolean fu_bios_setting_set_current_value(FwupdBiosSetting *attr, GError **error) /* nocheck:name */ { g_autofree gchar *str = NULL; if (!fu_bios_setting_get_key(attr, "current_value", &str, error)) return FALSE; fwupd_bios_setting_set_current_value(attr, str); return TRUE; } static void fu_bios_settings_set_read_only(FuBiosSettings *self, FwupdBiosSetting *attr) { const gchar *tmp; if (fwupd_bios_setting_get_kind(attr) == FWUPD_BIOS_SETTING_KIND_ENUMERATION) { const gchar *value = g_hash_table_lookup(self->read_only, fwupd_bios_setting_get_id(attr)); if (g_strcmp0(value, fwupd_bios_setting_get_current_value(attr)) == 0) fwupd_bios_setting_set_read_only(attr, TRUE); } tmp = g_strrstr(fwupd_bios_setting_get_current_value(attr), LENOVO_READ_ONLY_NEEDLE); if (tmp != NULL) fwupd_bios_setting_set_read_only(attr, TRUE); } static gboolean fu_bios_settings_set_type(FuBiosSettings *self, FwupdBiosSetting *attr, GError **error) { gboolean kernel_bug = FALSE; g_autofree gchar *data = NULL; g_autoptr(GError) error_key = NULL; g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_BIOS_SETTINGS(self), FALSE); g_return_val_if_fail(FWUPD_IS_BIOS_SETTING(attr), FALSE); if (!fu_bios_setting_get_key(attr, "type", &data, &error_key)) { g_debug("%s", error_key->message); g_propagate_error(error, g_steal_pointer(&error_key)); return FALSE; } if (g_strcmp0(data, "enumeration") == 0 || kernel_bug) { if (!fu_bios_setting_set_enumeration_attrs(attr, &error_local)) g_debug("failed to add enumeration attrs: %s", error_local->message); } else if (g_strcmp0(data, "integer") == 0) { if (!fu_bios_setting_set_integer_attrs(attr, &error_local)) g_debug("failed to add integer attrs: %s", error_local->message); } else if (g_strcmp0(data, "string") == 0) { if (!fu_bios_setting_set_string_attrs(attr, &error_local)) g_debug("failed to add string attrs: %s", error_local->message); } return TRUE; } /* Special case attribute that is a file not a folder * https://github.com/torvalds/linux/blob/v5.18/Documentation/ABI/testing/sysfs-class-firmware-attributes#L300 */ static gboolean fu_bios_settings_set_file_attributes(FuBiosSettings *self, FwupdBiosSetting *attr, GError **error) { g_autofree gchar *value = NULL; if (g_strcmp0(fwupd_bios_setting_get_name(attr), FWUPD_BIOS_SETTING_PENDING_REBOOT) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "%s attribute is not supported", fwupd_bios_setting_get_name(attr)); return FALSE; } if (!fu_bios_settings_set_description(self, attr, error)) return FALSE; if (!fu_bios_setting_get_key(attr, NULL, &value, error)) return FALSE; fwupd_bios_setting_set_current_value(attr, value); fwupd_bios_setting_set_read_only(attr, TRUE); return TRUE; } static gboolean fu_bios_settings_set_folder_attributes(FuBiosSettings *self, FwupdBiosSetting *attr, GError **error) { g_autoptr(GError) error_local = NULL; if (!fu_bios_settings_set_type(self, attr, error)) return FALSE; if (!fu_bios_setting_set_current_value(attr, error)) return FALSE; if (!fu_bios_settings_set_description(self, attr, &error_local)) g_debug("%s", error_local->message); fu_bios_settings_set_read_only(self, attr); return TRUE; } void fu_bios_settings_add_attribute(FuBiosSettings *self, FwupdBiosSetting *attr) { g_return_if_fail(FU_IS_BIOS_SETTINGS(self)); g_return_if_fail(FU_IS_BIOS_SETTING(attr)); g_ptr_array_add(self->attrs, g_object_ref(attr)); } static gboolean fu_bios_settings_populate_attribute(FuBiosSettings *self, const gchar *driver, const gchar *path, const gchar *name, GError **error) { g_autoptr(FwupdBiosSetting) attr = NULL; g_autofree gchar *id = NULL; g_return_val_if_fail(FU_IS_BIOS_SETTINGS(self), FALSE); g_return_val_if_fail(name != NULL, FALSE); g_return_val_if_fail(path != NULL, FALSE); g_return_val_if_fail(driver != NULL, FALSE); attr = fu_bios_setting_new(); id = g_strdup_printf("com.%s.%s", driver, name); fwupd_bios_setting_set_name(attr, name); fwupd_bios_setting_set_path(attr, path); fwupd_bios_setting_set_id(attr, id); if (g_file_test(path, G_FILE_TEST_IS_DIR)) { if (!fu_bios_settings_set_folder_attributes(self, attr, error)) return FALSE; } else { if (!fu_bios_settings_set_file_attributes(self, attr, error)) return FALSE; } fu_bios_settings_add_attribute(self, attr); return TRUE; } static void fu_bios_settings_populate_descriptions(FuBiosSettings *self) { g_return_if_fail(FU_IS_BIOS_SETTINGS(self)); g_hash_table_insert(self->descriptions, g_strdup("pending_reboot"), /* TRANSLATORS: description of a BIOS setting */ g_strdup(_("Settings will apply after system reboots"))); g_hash_table_insert(self->descriptions, g_strdup("com.thinklmi.WindowsUEFIFirmwareUpdate"), /* TRANSLATORS: description of a BIOS setting */ g_strdup(_("BIOS updates delivered via LVFS or Windows Update"))); } static void fu_bios_settings_populate_read_only(FuBiosSettings *self) { g_return_if_fail(FU_IS_BIOS_SETTINGS(self)); g_hash_table_insert(self->read_only, g_strdup("com.thinklmi.SecureBoot"), g_strdup(_("Enable"))); g_hash_table_insert(self->read_only, g_strdup("com.dell-wmi-sysman.SecureBoot"), g_strdup(_("Enabled"))); } static void fu_bios_settings_combination_fixups(FuBiosSettings *self) { FwupdBiosSetting *thinklmi_sb = fu_bios_settings_get_attr(self, "com.thinklmi.SecureBoot"); FwupdBiosSetting *thinklmi_3rd = fu_bios_settings_get_attr(self, "com.thinklmi.Allow3rdPartyUEFICA"); if (thinklmi_sb != NULL && thinklmi_3rd != NULL) { const gchar *val = fwupd_bios_setting_get_current_value(thinklmi_3rd); if (g_strcmp0(val, "Disable") == 0) { g_info("Disabling changing %s since %s is %s", fwupd_bios_setting_get_name(thinklmi_sb), fwupd_bios_setting_get_name(thinklmi_3rd), val); fwupd_bios_setting_set_read_only(thinklmi_sb, TRUE); } } } /** * fu_bios_settings_setup: * @self: a #FuBiosSettings * * Clears all attributes and re-initializes them. * Mostly used for the test suite, but could potentially be connected to udev * events for drivers being loaded or unloaded too. * * Since: 1.8.4 **/ gboolean fu_bios_settings_setup(FuBiosSettings *self, GError **error) { guint count = 0; g_autofree gchar *sysfsfwdir = NULL; g_autoptr(GDir) class_dir = NULL; g_return_val_if_fail(FU_IS_BIOS_SETTINGS(self), FALSE); if (self->attrs->len > 0) { g_debug("re-initializing attributes"); g_ptr_array_set_size(self->attrs, 0); } if (g_hash_table_size(self->descriptions) == 0) fu_bios_settings_populate_descriptions(self); if (g_hash_table_size(self->read_only) == 0) fu_bios_settings_populate_read_only(self); sysfsfwdir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR_FW_ATTRIB); class_dir = g_dir_open(sysfsfwdir, 0, error); if (class_dir == NULL) { fu_error_convert(error); return FALSE; } do { g_autofree gchar *path = NULL; g_autoptr(GDir) driver_dir = NULL; const gchar *driver = g_dir_read_name(class_dir); if (driver == NULL) break; path = g_build_filename(sysfsfwdir, driver, "attributes", NULL); if (!g_file_test(path, G_FILE_TEST_IS_DIR)) { g_debug("skipping non-directory %s", path); continue; } driver_dir = g_dir_open(path, 0, error); if (driver_dir == NULL) { fu_error_convert(error); return FALSE; } do { const gchar *name = g_dir_read_name(driver_dir); g_autofree gchar *full_path = NULL; g_autoptr(GError) error_local = NULL; if (name == NULL) break; full_path = g_build_filename(path, name, NULL); if (!fu_bios_settings_populate_attribute(self, driver, full_path, name, &error_local)) { g_debug("%s is not supported: %s", name, error_local->message); continue; } } while (++count); } while (TRUE); g_info("loaded %u BIOS settings", count); fu_bios_settings_combination_fixups(self); return TRUE; } static void fu_bios_settings_init(FuBiosSettings *self) { self->attrs = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); self->descriptions = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); self->read_only = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); } /** * fu_bios_settings_get_attr: * @self: a #FuBiosSettings * @val: the attribute ID or name to check for * * Returns: (transfer none): the attribute with the given ID or name or NULL if it doesn't exist. * * Since: 1.8.4 **/ FwupdBiosSetting * fu_bios_settings_get_attr(FuBiosSettings *self, const gchar *val) { g_return_val_if_fail(FU_IS_BIOS_SETTINGS(self), NULL); g_return_val_if_fail(val != NULL, NULL); for (guint i = 0; i < self->attrs->len; i++) { FwupdBiosSetting *attr = g_ptr_array_index(self->attrs, i); const gchar *tmp_id = fwupd_bios_setting_get_id(attr); const gchar *tmp_name = fwupd_bios_setting_get_name(attr); if (g_strcmp0(val, tmp_id) == 0 || g_strcmp0(val, tmp_name) == 0) return attr; } return NULL; } /** * fu_bios_settings_get_all: * @self: a #FuBiosSettings * * Gets all the attributes in the object. * * Returns: (transfer container) (element-type FwupdBiosSetting): attributes * * Since: 1.8.4 **/ GPtrArray * fu_bios_settings_get_all(FuBiosSettings *self) { g_return_val_if_fail(FU_IS_BIOS_SETTINGS(self), NULL); return g_ptr_array_ref(self->attrs); } /** * fu_bios_settings_get_pending_reboot: * @self: a #FuBiosSettings * @result: (out): Whether a reboot is pending * @error: (nullable): optional return location for an error * * Determines if the system will apply changes to attributes upon reboot * * Since: 1.8.4 **/ gboolean fu_bios_settings_get_pending_reboot(FuBiosSettings *self, gboolean *result, GError **error) { FwupdBiosSetting *attr = NULL; g_autofree gchar *data = NULL; guint64 val = 0; g_return_val_if_fail(result != NULL, FALSE); g_return_val_if_fail(FU_IS_BIOS_SETTINGS(self), FALSE); for (guint i = 0; i < self->attrs->len; i++) { FwupdBiosSetting *attr_tmp = g_ptr_array_index(self->attrs, i); const gchar *tmp = fwupd_bios_setting_get_name(attr_tmp); if (g_strcmp0(tmp, FWUPD_BIOS_SETTING_PENDING_REBOOT) == 0) { attr = attr_tmp; break; } } if (attr == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "failed to find pending reboot attribute"); return FALSE; } /* refresh/re-read */ if (!fu_bios_setting_get_key(attr, NULL, &data, error)) return FALSE; fwupd_bios_setting_set_current_value(attr, data); if (!fu_strtoull(data, &val, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; *result = (val == 1); return TRUE; } static GVariant * fu_bios_settings_to_variant(FwupdCodec *codec, FwupdCodecFlags flags) { FuBiosSettings *self = FU_BIOS_SETTINGS(codec); GVariantBuilder builder; g_variant_builder_init(&builder, G_VARIANT_TYPE("aa{sv}")); for (guint i = 0; i < self->attrs->len; i++) { FwupdBiosSetting *bios_setting = g_ptr_array_index(self->attrs, i); GVariant *value = fwupd_codec_to_variant(FWUPD_CODEC(bios_setting), flags); g_variant_builder_add_value(&builder, value); } return g_variant_new("(aa{sv})", &builder); } static gboolean fu_bios_settings_from_json(FwupdCodec *codec, JsonNode *json_node, GError **error) { FuBiosSettings *self = FU_BIOS_SETTINGS(codec); JsonArray *array; JsonObject *obj; /* sanity check */ if (!JSON_NODE_HOLDS_OBJECT(json_node)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not JSON object"); return FALSE; } obj = json_node_get_object(json_node); /* this has to exist */ if (!json_object_has_member(obj, "BiosSettings")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no BiosSettings property in object"); return FALSE; } array = json_object_get_array_member(obj, "BiosSettings"); for (guint i = 0; i < json_array_get_length(array); i++) { JsonNode *node_tmp = json_array_get_element(array, i); g_autoptr(FwupdBiosSetting) attr = fwupd_bios_setting_new(NULL, NULL); if (!fwupd_codec_from_json(FWUPD_CODEC(attr), node_tmp, error)) return FALSE; g_ptr_array_add(self->attrs, g_steal_pointer(&attr)); } /* success */ return TRUE; } static void fu_bios_settings_codec_iface_init(FwupdCodecInterface *iface) { iface->to_variant = fu_bios_settings_to_variant; iface->from_json = fu_bios_settings_from_json; } /** * fu_bios_settings_to_hash_kv: * @self: A #FuBiosSettings * * Creates a #GHashTable with the name and current value of * all BIOS settings. * * Returns: (transfer full): name/value pairs * Since: 1.8.4 **/ GHashTable * fu_bios_settings_to_hash_kv(FuBiosSettings *self) { GHashTable *bios_settings = NULL; g_return_val_if_fail(self != NULL, NULL); bios_settings = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); for (guint i = 0; i < self->attrs->len; i++) { FwupdBiosSetting *item_setting = g_ptr_array_index(self->attrs, i); g_hash_table_insert(bios_settings, g_strdup(fwupd_bios_setting_get_id(item_setting)), g_strdup(fwupd_bios_setting_get_current_value(item_setting))); } return bios_settings; } /** * fu_bios_settings_new: * * Returns: #FuBiosSettings * * Since: 1.8.4 **/ FuBiosSettings * fu_bios_settings_new(void) { return g_object_new(FU_TYPE_FIRMWARE_ATTRS, NULL); } fwupd-2.0.10/libfwupdplugin/fu-bios-settings.h000066400000000000000000000011311501337203100213140ustar00rootroot00000000000000/* * Copyright 2022 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_FIRMWARE_ATTRS (fu_bios_settings_get_type()) G_DECLARE_FINAL_TYPE(FuBiosSettings, fu_bios_settings, FU, BIOS_SETTINGS, GObject) FuBiosSettings * fu_bios_settings_new(void); gboolean fu_bios_settings_get_pending_reboot(FuBiosSettings *self, gboolean *result, GError **error) G_GNUC_NON_NULL(1); FwupdBiosSetting * fu_bios_settings_get_attr(FuBiosSettings *self, const gchar *val) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-block-device.c000066400000000000000000000217461501337203100210620ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuBlockDevice" #include "config.h" #ifdef HAVE_SCSI_SG_H #include #include #endif #include "fu-block-device.h" #include "fu-dump.h" #include "fu-usb-device.h" /** * FuBlockDevice * * See also: #FuUdevDevice */ G_DEFINE_TYPE(FuBlockDevice, fu_block_device, FU_TYPE_UDEV_DEVICE) #define FU_BLOCK_DEVICE_SG_IO_SENSE_BUFFER_LEN 32 /* bytes */ #define FU_BLOCK_DEVICE_SG_IO_TIMEOUT 20000 /* ms */ static gboolean fu_block_device_probe(FuDevice *device, GError **error) { g_autoptr(FuDevice) usb_device = NULL; /* block devices are weird in that the vendor and model are generic */ usb_device = fu_device_get_backend_parent_with_subsystem(device, "usb:usb_device", NULL); if (usb_device != NULL) { g_autofree gchar *devpath = fu_udev_device_get_devpath(FU_UDEV_DEVICE(usb_device)); /* copy the VID and PID, and reconstruct compatible IDs */ if (!fu_device_probe(usb_device, error)) return FALSE; fu_device_add_instance_u16(device, "VEN", fu_device_get_vid(usb_device)); fu_device_add_instance_u16(device, "DEV", fu_device_get_pid(usb_device)); if (!fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, error, "BLOCK", "VEN", NULL)) return FALSE; if (!fu_device_build_instance_id(device, error, "BLOCK", "VEN", "DEV", NULL)) return FALSE; fu_device_incorporate(device, usb_device, FU_DEVICE_INCORPORATE_FLAG_VENDOR_IDS | FU_DEVICE_INCORPORATE_FLAG_VID | FU_DEVICE_INCORPORATE_FLAG_PID); /* USB devpath as physical ID */ if (devpath != NULL) { g_autofree gchar *physical_id = g_strdup_printf("DEVPATH=%s", devpath); fu_device_set_physical_id(device, physical_id); } } /* success */ return TRUE; } #ifdef HAVE_SCSI_SG_H static gboolean fu_block_device_ioctl_buf_cb(FuIoctl *self, gpointer ptr, guint8 *buf, gsize bufsz, GError **error) { struct sg_io_hdr *io_hdr = (struct sg_io_hdr *)ptr; io_hdr->dxferp = buf; io_hdr->dxfer_len = bufsz; return TRUE; } static gboolean fu_block_device_ioctl_cdb_cb(FuIoctl *self, gpointer ptr, guint8 *buf, gsize bufsz, GError **error) { struct sg_io_hdr *io_hdr = (struct sg_io_hdr *)ptr; io_hdr->cmdp = buf; io_hdr->cmd_len = bufsz; return TRUE; } static gboolean fu_block_device_ioctl_sense_cb(FuIoctl *self, gpointer ptr, guint8 *buf, gsize bufsz, GError **error) { struct sg_io_hdr *io_hdr = (struct sg_io_hdr *)ptr; io_hdr->sbp = buf; io_hdr->mx_sb_len = bufsz; return TRUE; } #endif /** * fu_block_device_sg_io_cmd_none: * @self: a #FuBlockDevice * @cdb: a cdb command * @cdbsz: sizeof @cdb * @error: (nullable): optional return location for an error * * Performs a SCSI IO command with no parameters. * * Returns: %TRUE on success * * Since: 2.0.0 **/ gboolean fu_block_device_sg_io_cmd_none(FuBlockDevice *self, const guint8 *cdb, guint8 cdbsz, GError **error) { #ifdef HAVE_SCSI_SG_H guint8 sense_buffer[FU_BLOCK_DEVICE_SG_IO_SENSE_BUFFER_LEN] = {0}; struct sg_io_hdr io_hdr = { .interface_id = 'S', .dxfer_direction = SG_DXFER_NONE, .timeout = FU_BLOCK_DEVICE_SG_IO_TIMEOUT, .flags = SG_FLAG_DIRECT_IO, }; gint rc = 0; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); fu_dump_raw(G_LOG_DOMAIN, "cmd", cdb, cdbsz); /* include these when generating the emulation event */ fu_ioctl_add_key_as_u16(ioctl, "Request", SG_IO); fu_ioctl_add_key_as_u8(ioctl, "DxferDirection", io_hdr.dxfer_direction); fu_ioctl_add_const_buffer(ioctl, "Cdb", cdb, cdbsz, fu_block_device_ioctl_cdb_cb); fu_ioctl_add_mutable_buffer(ioctl, "Sense", sense_buffer, sizeof(sense_buffer), fu_block_device_ioctl_sense_cb); if (!fu_ioctl_execute(ioctl, SG_IO, (guint8 *)&io_hdr, sizeof(io_hdr), &rc, 5 * FU_BLOCK_DEVICE_SG_IO_TIMEOUT, FU_IOCTL_FLAG_RETRY, error)) return FALSE; if (io_hdr.status) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "command fail with status %x, senseKey 0x%02x, asc 0x%02x, ascq 0x%02x", io_hdr.status, sense_buffer[2], sense_buffer[12], sense_buffer[13]); return FALSE; } /* success */ return TRUE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported as scsi/sg.h not found"); return FALSE; #endif } /** * fu_block_device_sg_io_cmd_read: * @self: a #FuBlockDevice * @cdb: a cdb command * @cdbsz: sizeof @cdb * @buf: buffer to read into * @bufsz: sizeof @buf * @error: (nullable): optional return location for an error * * Performs a SCSI IO read command. * * Returns: %TRUE on success * * Since: 2.0.0 **/ gboolean fu_block_device_sg_io_cmd_read(FuBlockDevice *self, const guint8 *cdb, gsize cdbsz, guint8 *buf, gsize bufsz, GError **error) { #ifdef HAVE_SCSI_SG_H guint8 sense_buffer[FU_BLOCK_DEVICE_SG_IO_SENSE_BUFFER_LEN] = {0}; struct sg_io_hdr io_hdr = { .interface_id = 'S', .dxfer_direction = SG_DXFER_FROM_DEV, .timeout = FU_BLOCK_DEVICE_SG_IO_TIMEOUT, .flags = SG_FLAG_DIRECT_IO, }; gint rc = 0; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); fu_dump_raw(G_LOG_DOMAIN, "cmd", cdb, cdbsz); /* include these when generating the emulation event */ fu_ioctl_add_key_as_u16(ioctl, "Request", SG_IO); fu_ioctl_add_key_as_u8(ioctl, "DxferDirection", io_hdr.dxfer_direction); fu_ioctl_add_mutable_buffer(ioctl, NULL, buf, bufsz, fu_block_device_ioctl_buf_cb); fu_ioctl_add_const_buffer(ioctl, "Cdb", cdb, cdbsz, fu_block_device_ioctl_cdb_cb); fu_ioctl_add_mutable_buffer(ioctl, "Sense", sense_buffer, sizeof(sense_buffer), fu_block_device_ioctl_sense_cb); if (!fu_ioctl_execute(ioctl, SG_IO, (guint8 *)&io_hdr, sizeof(io_hdr), &rc, 5 * FU_BLOCK_DEVICE_SG_IO_TIMEOUT, FU_IOCTL_FLAG_RETRY, error)) return FALSE; if (io_hdr.status) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "command fail with status %x, senseKey 0x%02x, asc 0x%02x, ascq 0x%02x", io_hdr.status, sense_buffer[2], sense_buffer[12], sense_buffer[13]); return FALSE; } if (bufsz > 0) fu_dump_raw(G_LOG_DOMAIN, "cmd data", buf, bufsz); /* success */ return TRUE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported as scsi/sg.h not found"); return FALSE; #endif } /** * fu_block_device_sg_io_cmd_write: * @self: a #FuBlockDevice * @cdb: a cdb command * @cdbsz: sizeof @cdb * @buf: buffer to read from * @bufsz: sizeof @buf * @error: (nullable): optional return location for an error * * Performs a SCSI IO write command. * * Returns: %TRUE on success * * Since: 2.0.0 **/ gboolean fu_block_device_sg_io_cmd_write(FuBlockDevice *self, const guint8 *cdb, gsize cdbsz, const guint8 *buf, gsize bufsz, GError **error) { #ifdef HAVE_SCSI_SG_H guint8 sense_buffer[FU_BLOCK_DEVICE_SG_IO_SENSE_BUFFER_LEN] = {0}; struct sg_io_hdr io_hdr = { .interface_id = 'S', .dxfer_direction = SG_DXFER_TO_DEV, .timeout = FU_BLOCK_DEVICE_SG_IO_TIMEOUT, .flags = SG_FLAG_DIRECT_IO, }; gint rc = 0; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); fu_dump_raw(G_LOG_DOMAIN, "cmd", cdb, cdbsz); /* include these when generating the emulation event */ fu_ioctl_add_key_as_u16(ioctl, "Request", SG_IO); fu_ioctl_add_key_as_u8(ioctl, "DxferDirection", io_hdr.dxfer_direction); fu_ioctl_add_const_buffer(ioctl, NULL, buf, bufsz, fu_block_device_ioctl_buf_cb); fu_ioctl_add_const_buffer(ioctl, "Cdb", cdb, cdbsz, fu_block_device_ioctl_cdb_cb); fu_ioctl_add_mutable_buffer(ioctl, "Sense", sense_buffer, sizeof(sense_buffer), fu_block_device_ioctl_sense_cb); if (!fu_ioctl_execute(ioctl, SG_IO, (guint8 *)&io_hdr, sizeof(io_hdr), &rc, 5 * FU_BLOCK_DEVICE_SG_IO_TIMEOUT, FU_IOCTL_FLAG_RETRY, error)) return FALSE; if (io_hdr.status) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "command fail with status %x, senseKey 0x%02x, asc 0x%02x, ascq 0x%02x", io_hdr.status, sense_buffer[2], sense_buffer[12], sense_buffer[13]); return FALSE; } if (bufsz > 0) fu_dump_raw(G_LOG_DOMAIN, "cmd data", buf, bufsz); /* success */ return TRUE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported as scsi/sg.h not found"); return FALSE; #endif } static void fu_block_device_init(FuBlockDevice *self) { } static void fu_block_device_class_init(FuBlockDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_block_device_probe; } fwupd-2.0.10/libfwupdplugin/fu-block-device.h000066400000000000000000000016111501337203100210540ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-udev-device.h" #define FU_TYPE_BLOCK_DEVICE (fu_block_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuBlockDevice, fu_block_device, FU, BLOCK_DEVICE, FuUdevDevice) struct _FuBlockDeviceClass { FuUdevDeviceClass parent_class; }; gboolean fu_block_device_sg_io_cmd_none(FuBlockDevice *self, const guint8 *cdb, guint8 cdbsz, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_block_device_sg_io_cmd_read(FuBlockDevice *self, const guint8 *cdb, gsize cdbsz, guint8 *buf, gsize bufsz, GError **error) G_GNUC_NON_NULL(1, 2, 4); gboolean fu_block_device_sg_io_cmd_write(FuBlockDevice *self, const guint8 *cdb, gsize cdbsz, const guint8 *buf, gsize bufsz, GError **error) G_GNUC_NON_NULL(1, 2, 4); fwupd-2.0.10/libfwupdplugin/fu-block-partition.c000066400000000000000000000236431501337203100216320ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuBlockPartition" #include "config.h" #ifdef HAVE_BLKID #include #endif #include "fu-block-partition.h" #include "fu-device-event.h" #include "fu-string.h" #include "fu-volume.h" /** * FuBlockPartition * * See also: #FuBlockDevice */ typedef struct { gchar *fs_type; gchar *fs_uuid; gchar *fs_label; } FuBlockPartitionPrivate; #define GET_PRIVATE(o) (fu_block_partition_get_instance_private(o)) G_DEFINE_TYPE_WITH_PRIVATE(FuBlockPartition, fu_block_partition, FU_TYPE_BLOCK_DEVICE) #ifdef HAVE_BLKID G_DEFINE_AUTO_CLEANUP_FREE_FUNC(blkid_probe, blkid_free_probe, NULL) #endif static void fu_block_partition_to_string(FuDevice *device, guint idt, GString *str) { FuBlockPartition *self = FU_BLOCK_PARTITION(device); FuBlockPartitionPrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append(str, idt, "FsType", priv->fs_type); fwupd_codec_string_append(str, idt, "FsUuid", priv->fs_uuid); fwupd_codec_string_append(str, idt, "FsLabel", priv->fs_label); } #ifdef HAVE_BLKID static void fu_block_partition_set_fs_type(FuBlockPartition *self, const gchar *fs_type, gsize fs_typelen) { FuBlockPartitionPrivate *priv = GET_PRIVATE(self); /* not changed */ if (g_strcmp0(priv->fs_type, fs_type) == 0) return; g_free(priv->fs_type); priv->fs_type = fu_strsafe(fs_type, fs_typelen); } static void fu_block_partition_set_fs_uuid(FuBlockPartition *self, const gchar *fs_uuid, gsize fs_uuidlen) { FuBlockPartitionPrivate *priv = GET_PRIVATE(self); /* not changed */ if (g_strcmp0(priv->fs_uuid, fs_uuid) == 0) return; g_free(priv->fs_uuid); priv->fs_uuid = fu_strsafe(fs_uuid, fs_uuidlen); } static void fu_block_partition_set_fs_label(FuBlockPartition *self, const gchar *fs_label, gsize fs_labellen) { FuBlockPartitionPrivate *priv = GET_PRIVATE(self); /* not changed */ if (g_strcmp0(priv->fs_label, fs_label) == 0) return; g_free(priv->fs_label); priv->fs_label = fu_strsafe(fs_label, fs_labellen); } #endif static void fu_block_partition_incorporate(FuDevice *self, FuDevice *donor) { FuBlockPartition *uself = FU_BLOCK_PARTITION(self); FuBlockPartition *udonor = FU_BLOCK_PARTITION(donor); FuBlockPartitionPrivate *priv = GET_PRIVATE(uself); g_return_if_fail(FU_IS_BLOCK_PARTITION(self)); g_return_if_fail(FU_IS_BLOCK_PARTITION(donor)); if (priv->fs_type == NULL) priv->fs_type = g_strdup(fu_block_partition_get_fs_type(udonor)); if (priv->fs_uuid == NULL) priv->fs_uuid = g_strdup(fu_block_partition_get_fs_uuid(udonor)); if (priv->fs_label == NULL) priv->fs_label = g_strdup(fu_block_partition_get_fs_label(udonor)); } /** * fu_block_partition_get_fs_type: * @self: a #FuBlockPartition * * Returns the filesystem type, e.g. `msdos`. * * Returns: string, or %NULL for unset * * Since: 2.0.2 **/ const gchar * fu_block_partition_get_fs_type(FuBlockPartition *self) { FuBlockPartitionPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_BLOCK_PARTITION(self), NULL); return priv->fs_type; } /** * fu_block_partition_get_fs_uuid: * @self: a #FuBlockPartition * * Returns the filesystem UUID. * * Returns: string, or %NULL for unset * * Since: 2.0.2 **/ const gchar * fu_block_partition_get_fs_uuid(FuBlockPartition *self) { FuBlockPartitionPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_BLOCK_PARTITION(self), NULL); return priv->fs_uuid; } /** * fu_block_partition_get_fs_label: * @self: a #FuBlockPartition * * Returns the filesystem label. * * Returns: string, or %NULL for unset * * Since: 2.0.2 **/ const gchar * fu_block_partition_get_fs_label(FuBlockPartition *self) { FuBlockPartitionPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_BLOCK_PARTITION(self), NULL); return priv->fs_label; } /** * fu_block_partition_get_mount_point: * @self: a #FuBlockPartition * @error: (nullable): optional return location for an error * * Returns the filesystem label. * * Returns: string, or %NULL for unset * * Since: 2.0.2 **/ gchar * fu_block_partition_get_mount_point(FuBlockPartition *self, GError **error) { const gchar *devfile = fu_udev_device_get_device_file(FU_UDEV_DEVICE(self)); FuDeviceEvent *event = NULL; g_autofree gchar *event_id = NULL; g_autofree gchar *mount_point = NULL; g_autoptr(FuVolume) volume = NULL; g_return_val_if_fail(FU_IS_BLOCK_PARTITION(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* sanity check */ if (devfile == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "invalid path: no devfile"); return NULL; } /* build event key either for load or save */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_strdup_printf("GetMountPoint:Devfile=%s", devfile); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { const gchar *tmp; event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return NULL; tmp = fu_device_event_get_str(event, "Data", error); if (tmp == NULL) return NULL; return g_strdup(tmp); } /* save */ if (fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event = fu_device_save_event(FU_DEVICE(self), event_id); } /* find volume */ volume = fu_volume_new_by_device(devfile, error); if (volume == NULL) return NULL; /* success */ mount_point = fu_volume_get_mount_point(volume); /* save */ if (event != NULL) fu_device_event_set_str(event, "Data", mount_point); /* success */ return g_steal_pointer(&mount_point); } static gboolean fu_block_partition_setup(FuDevice *device, GError **error) { FuBlockPartition *self = FU_BLOCK_PARTITION(device); FuBlockPartitionPrivate *priv = GET_PRIVATE(self); FuDeviceEvent *event = NULL; FuIOChannel *io_channel = fu_udev_device_get_io_channel(FU_UDEV_DEVICE(device)); g_autofree gchar *event_id = NULL; #ifdef HAVE_BLKID gint rc; const gchar *data; gsize datalen = 0; g_auto(blkid_probe) pr = NULL; #endif /* need event ID */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_strdup_printf("FuBlockPartitionSetup:DeviceFile=%s", fu_udev_device_get_device_file(FU_UDEV_DEVICE(self))); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return FALSE; priv->fs_type = g_strdup(fu_device_event_get_str(event, "FsType", NULL)); priv->fs_uuid = g_strdup(fu_device_event_get_str(event, "FsUuid", NULL)); priv->fs_label = g_strdup(fu_device_event_get_str(event, "FsLabel", NULL)); return TRUE; } /* save */ if (event_id != NULL) event = fu_device_save_event(FU_DEVICE(self), event_id); /* sanity check */ if (io_channel == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no device"); return FALSE; } #ifdef HAVE_BLKID /* probe */ pr = blkid_new_probe(); if (pr == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to create blkid prober"); return FALSE; } blkid_probe_set_superblocks_flags(pr, BLKID_SUBLKS_UUID | BLKID_SUBLKS_TYPE | BLKID_SUBLKS_LABEL); rc = blkid_probe_set_device(pr, fu_io_channel_unix_get_fd(io_channel), 0x0, 0x0); if (rc < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to setup device: %i", rc); return FALSE; } rc = blkid_do_safeprobe(pr); if (rc < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to probe device: %i", rc); return FALSE; } if (g_getenv("FWUPD_VERBOSE") != NULL) { gint nvals = blkid_probe_numof_values(pr); for (gint i = 0; i < nvals; i++) { const gchar *name = NULL; if (blkid_probe_get_value(pr, i, &name, &data, &datalen) == 0) g_debug("%s=%s", name, data); } } /* extract block attributes */ if (blkid_probe_lookup_value(pr, "TYPE", &data, &datalen) == 0) fu_block_partition_set_fs_type(self, data, datalen); if (blkid_probe_lookup_value(pr, "UUID", &data, &datalen) == 0) fu_block_partition_set_fs_uuid(self, data, datalen); if (blkid_probe_lookup_value(pr, "LABEL", &data, &datalen) == 0) fu_block_partition_set_fs_label(self, data, datalen); #else g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported as not found"); return FALSE; #endif /* save response */ if (event != NULL) { if (priv->fs_type != NULL) fu_device_event_set_str(event, "FsType", priv->fs_type); if (priv->fs_uuid != NULL) fu_device_event_set_str(event, "FsUuid", priv->fs_uuid); if (priv->fs_label != NULL) fu_device_event_set_str(event, "FsLabel", priv->fs_label); } /* success */ return TRUE; } static void fu_block_partition_finalize(GObject *object) { FuBlockPartition *self = FU_BLOCK_PARTITION(object); FuBlockPartitionPrivate *priv = GET_PRIVATE(self); g_free(priv->fs_type); g_free(priv->fs_uuid); g_free(priv->fs_label); G_OBJECT_CLASS(fu_block_partition_parent_class)->finalize(object); } static void fu_block_partition_init(FuBlockPartition *self) { fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_NONBLOCK); } static void fu_block_partition_class_init(FuBlockPartitionClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_block_partition_finalize; device_class->to_string = fu_block_partition_to_string; device_class->setup = fu_block_partition_setup; device_class->incorporate = fu_block_partition_incorporate; } fwupd-2.0.10/libfwupdplugin/fu-block-partition.h000066400000000000000000000013711501337203100216310ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-block-device.h" #define FU_TYPE_BLOCK_PARTITION (fu_block_partition_get_type()) G_DECLARE_DERIVABLE_TYPE(FuBlockPartition, fu_block_partition, FU, BLOCK_PARTITION, FuBlockDevice) struct _FuBlockPartitionClass { FuBlockDeviceClass parent_class; }; const gchar * fu_block_partition_get_fs_type(FuBlockPartition *self) G_GNUC_NON_NULL(1); const gchar * fu_block_partition_get_fs_uuid(FuBlockPartition *self) G_GNUC_NON_NULL(1); const gchar * fu_block_partition_get_fs_label(FuBlockPartition *self) G_GNUC_NON_NULL(1); gchar * fu_block_partition_get_mount_point(FuBlockPartition *self, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-bluez-device.c000066400000000000000000001012351501337203100211010ustar00rootroot00000000000000/* * Copyright 2021 Ricardo Cañuelo * Copyright 2024 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuBluezDevice" #include "config.h" #include #include #include "fu-bluez-device.h" #include "fu-dump.h" #include "fu-firmware-common.h" #include "fu-string.h" #define DEFAULT_PROXY_TIMEOUT 5000 /** * FuBluezDevice: * * A BlueZ Bluetooth device. * * See also: [class@FuDevice] */ typedef struct { GDBusObjectManager *object_manager; GDBusProxy *proxy; GHashTable *uuids; /* utf8 : FuBluezDeviceUuidHelper */ } FuBluezDevicePrivate; typedef struct { FuBluezDevice *self; gchar *uuid; gchar *path; gulong signal_id; GDBusProxy *proxy; } FuBluezDeviceUuidHelper; enum { PROP_0, PROP_OBJECT_MANAGER, PROP_PROXY, PROP_LAST }; enum { SIGNAL_CHANGED, SIGNAL_LAST }; static guint signals[SIGNAL_LAST] = {0}; G_DEFINE_TYPE_WITH_PRIVATE(FuBluezDevice, fu_bluez_device, FU_TYPE_DEVICE) #define GET_PRIVATE(o) (fu_bluez_device_get_instance_private(o)) static void fu_bluez_device_uuid_free(FuBluezDeviceUuidHelper *uuid_helper) { if (uuid_helper->path != NULL) g_free(uuid_helper->path); if (uuid_helper->proxy != NULL) g_object_unref(uuid_helper->proxy); g_free(uuid_helper->uuid); g_object_unref(uuid_helper->self); g_free(uuid_helper); } /* * Looks up a UUID in the FuBluezDevice uuids table. */ static FuBluezDeviceUuidHelper * fu_bluez_device_get_uuid_helper(FuBluezDevice *self, const gchar *uuid, GError **error) { FuBluezDevicePrivate *priv = GET_PRIVATE(self); FuBluezDeviceUuidHelper *uuid_helper; uuid_helper = g_hash_table_lookup(priv->uuids, uuid); if (uuid_helper == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "UUID %s not supported", uuid); return NULL; } return uuid_helper; } static void fu_bluez_device_signal_cb(GDBusProxy *proxy, GVariant *changed_properties, GStrv invalidated_properties, FuBluezDeviceUuidHelper *uuid_helper) { g_signal_emit(uuid_helper->self, signals[SIGNAL_CHANGED], 0, uuid_helper->uuid); } /* * Builds the GDBusProxy of the BlueZ object identified by a UUID * string. If the object doesn't have a dedicated proxy yet, this * creates it and saves it in the FuBluezDeviceUuidHelper object. * * NOTE: Currently limited to GATT characteristics. */ static gboolean fu_bluez_device_ensure_uuid_helper_proxy(FuBluezDeviceUuidHelper *uuid_helper, GError **error) { if (uuid_helper->proxy != NULL) return TRUE; uuid_helper->proxy = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, NULL, "org.bluez", uuid_helper->path, "org.bluez.GattCharacteristic1", NULL, error); if (uuid_helper->proxy == NULL) { g_prefix_error(error, "Failed to create GDBusProxy for uuid_helper: "); return FALSE; } g_dbus_proxy_set_default_timeout(uuid_helper->proxy, DEFAULT_PROXY_TIMEOUT); uuid_helper->signal_id = g_signal_connect(G_DBUS_PROXY(uuid_helper->proxy), "g-properties-changed", G_CALLBACK(fu_bluez_device_signal_cb), uuid_helper); if (uuid_helper->signal_id <= 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot connect to signal of UUID %s", uuid_helper->uuid); return FALSE; } return TRUE; } static void fu_bluez_device_add_uuid_path(FuBluezDevice *self, const gchar *uuid, const gchar *path) { FuBluezDevicePrivate *priv = GET_PRIVATE(self); FuBluezDeviceUuidHelper *uuid_helper; g_return_if_fail(FU_IS_BLUEZ_DEVICE(self)); g_return_if_fail(uuid != NULL); g_return_if_fail(path != NULL); uuid_helper = g_new0(FuBluezDeviceUuidHelper, 1); uuid_helper->self = g_object_ref(self); uuid_helper->uuid = g_strdup(uuid); uuid_helper->path = g_strdup(path); g_hash_table_insert(priv->uuids, g_strdup(uuid), uuid_helper); } static void fu_bluez_device_set_modalias(FuBluezDevice *self, const gchar *modalias) { gsize modaliaslen; guint16 vid = 0x0; guint16 pid = 0x0; guint16 rev = 0x0; g_return_if_fail(modalias != NULL); /* usb:v0461p4EEFd0001 */ modaliaslen = strlen(modalias); if (g_str_has_prefix(modalias, "usb:")) { fu_firmware_strparse_uint16_safe(modalias, modaliaslen, 5, &vid, NULL); fu_firmware_strparse_uint16_safe(modalias, modaliaslen, 10, &pid, NULL); fu_firmware_strparse_uint16_safe(modalias, modaliaslen, 15, &rev, NULL); /* bluetooth:v000ApFFFFdFFFF */ } else if (g_str_has_prefix(modalias, "bluetooth:")) { fu_firmware_strparse_uint16_safe(modalias, modaliaslen, 11, &vid, NULL); fu_firmware_strparse_uint16_safe(modalias, modaliaslen, 16, &pid, NULL); fu_firmware_strparse_uint16_safe(modalias, modaliaslen, 21, &rev, NULL); } /* add generated IDs */ if (vid != 0x0) { fu_device_set_vid(FU_DEVICE(self), vid); fu_device_add_instance_u16(FU_DEVICE(self), "VID", vid); } if (pid != 0x0) { fu_device_set_pid(FU_DEVICE(self), pid); fu_device_add_instance_u16(FU_DEVICE(self), "PID", pid); } fu_device_add_instance_u16(FU_DEVICE(self), "REV", rev); fu_device_build_instance_id_full(FU_DEVICE(self), FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "BLUETOOTH", "VID", NULL); fu_device_build_instance_id_full(FU_DEVICE(self), FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_VISIBLE | FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "BLUETOOTH", "VID", "PID", NULL); if (fu_device_has_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_INSTANCE_ID_REV)) { fu_device_build_instance_id_full(FU_DEVICE(self), FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_VISIBLE | FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "BLUETOOTH", "VID", "PID", "REV", NULL); } /* set vendor ID */ if (vid != 0x0) { g_autofree gchar *vendor_id = g_strdup_printf("%04X", vid); fu_device_build_vendor_id(FU_DEVICE(self), "BLUETOOTH", vendor_id); /* compat */ fu_device_build_vendor_id_u16(FU_DEVICE(self), "BLUETOOTH", vid); } /* set version if the revision has been set */ if (rev != 0x0 && fu_device_get_version_format(FU_DEVICE(self)) == FWUPD_VERSION_FORMAT_UNKNOWN) { fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_BCD); fu_device_set_version_raw(FU_DEVICE(self), rev); } } static void fu_bluez_device_to_string(FuDevice *device, guint idt, GString *str) { FuBluezDevice *self = FU_BLUEZ_DEVICE(device); FuBluezDevicePrivate *priv = GET_PRIVATE(self); if (priv->uuids != NULL) { GHashTableIter iter; gpointer key, value; g_hash_table_iter_init(&iter, priv->uuids); while (g_hash_table_iter_next(&iter, &key, &value)) { FuBluezDeviceUuidHelper *uuid_helper = (FuBluezDeviceUuidHelper *)value; fwupd_codec_string_append(str, idt + 1, (const gchar *)key, uuid_helper->path); } } } /* * Returns the value of a property of an object specified by its path as * a GVariant, or NULL if the property wasn't found. */ static GVariant * fu_bluez_device_get_ble_property(const gchar *obj_path, const gchar *iface, const gchar *prop_name, GError **error) { g_autoptr(GDBusProxy) proxy = NULL; g_autoptr(GVariant) val = NULL; proxy = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, NULL, "org.bluez", obj_path, iface, NULL, error); if (proxy == NULL) { g_prefix_error(error, "failed to connect to %s: ", iface); return NULL; } g_dbus_proxy_set_default_timeout(proxy, DEFAULT_PROXY_TIMEOUT); val = g_dbus_proxy_get_cached_property(proxy, prop_name); if (val == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "property %s not found in %s: ", prop_name, obj_path); return NULL; } return g_steal_pointer(&val); } /* * Returns the value of the string property of an object specified by * its path, or NULL if the property wasn't found. * * The returned string must be freed using g_free(). */ static gchar * fu_bluez_device_get_ble_string_property(const gchar *obj_path, const gchar *iface, const gchar *prop_name, GError **error) { g_autoptr(GVariant) val = NULL; val = fu_bluez_device_get_ble_property(obj_path, iface, prop_name, error); if (val == NULL) return NULL; return g_variant_dup_string(val, NULL); } static gchar * fu_bluez_device_get_interface_uuid(FuBluezDevice *self, GDBusObject *obj, const gchar *obj_path, const gchar *iface_name, GError **error) { g_autofree gchar *obj_uuid = NULL; g_autoptr(GDBusInterface) iface = NULL; iface = g_dbus_object_get_interface(obj, iface_name); if (iface == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no %s interface", iface_name); return NULL; } obj_uuid = fu_bluez_device_get_ble_string_property(obj_path, iface_name, "UUID", error); if (obj_uuid == NULL) { g_prefix_error(error, "failed to get %s property: ", iface_name); return NULL; } /* success */ return g_steal_pointer(&obj_uuid); } /* * Populates the {uuid_helper : object_path} entry of a device for its * characteristic. */ static gboolean fu_bluez_device_add_characteristic_uuid(FuBluezDevice *self, GDBusObject *obj, const gchar *obj_path, const gchar *iface_name, GError **error) { g_autofree gchar *obj_uuid = NULL; obj_uuid = fu_bluez_device_get_interface_uuid(self, obj, obj_path, iface_name, error); if (obj_uuid == NULL) return FALSE; fu_bluez_device_add_uuid_path(self, obj_uuid, obj_path); return TRUE; } static gboolean fu_bluez_device_add_instance_by_service_uuid(FuBluezDevice *self, GDBusObject *obj, const gchar *obj_path, const gchar *iface_name, GError **error) { g_autofree gchar *obj_uuid = NULL; /* register device by service UUID */ obj_uuid = fu_bluez_device_get_interface_uuid(self, obj, obj_path, iface_name, error); if (obj_uuid == NULL) return FALSE; fu_device_add_instance_str(FU_DEVICE(self), "GATT", obj_uuid); if (!fu_device_build_instance_id_full(FU_DEVICE(self), FU_DEVICE_INSTANCE_FLAG_QUIRKS, error, "BLUETOOTH", "GATT", NULL)) { g_prefix_error(error, "failed to register %s service: ", obj_uuid); return FALSE; } /* success */ return TRUE; } static gboolean fu_bluez_device_read_battery_interface(FuBluezDevice *self, GDBusObject *obj, const gchar *obj_path, const gchar *iface_name, GError **error) { guint8 percentage = FWUPD_BATTERY_LEVEL_INVALID; g_autoptr(GDBusInterface) iface = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(GVariant) obj_percentage = NULL; iface = g_dbus_object_get_interface(obj, iface_name); if (iface == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no %s interface", iface_name); return FALSE; } /* sometimes battery service announced but has no value, no error in that case */ obj_percentage = fu_bluez_device_get_ble_property(obj_path, iface_name, "Percentage", &error_local); if (obj_percentage == NULL) { g_debug("failed to get battery percentage from org.bluez.Battery1: %s", error_local->message); /* return TRUE since that situation should not affect to further interaction */ return TRUE; } percentage = g_variant_get_byte(obj_percentage); fu_device_set_battery_level(FU_DEVICE(self), percentage); /* success */ return TRUE; } /* see https://www.bluetooth.com/specifications/dis-1-2/ spec */ static gboolean fu_bluez_device_parse_device_information_service(FuBluezDevice *self, GError **error) { g_autofree gchar *model_number = NULL; g_autofree gchar *serial_number = NULL; g_autofree gchar *fw_revision = NULL; g_autofree gchar *manufacturer = NULL; model_number = fu_bluez_device_read_string(self, FU_BLUEZ_DEVICE_UUID_DI_MODEL_NUMBER, NULL); if (model_number != NULL) { fu_device_add_instance_str(FU_DEVICE(self), "MODEL", model_number); if (!fu_device_build_instance_id_full(FU_DEVICE(self), FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_QUIRKS, error, "BLUETOOTH", "MODEL", NULL)) { g_prefix_error(error, "failed to register model %s: ", model_number); return FALSE; } manufacturer = fu_bluez_device_read_string(self, FU_BLUEZ_DEVICE_UUID_DI_MANUFACTURER_NAME, NULL); if (manufacturer != NULL) { fu_device_add_instance_str(FU_DEVICE(self), "MANUFACTURER", manufacturer); if (!fu_device_build_instance_id_full(FU_DEVICE(self), FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_QUIRKS, error, "BLUETOOTH", "MANUFACTURER", "MODEL", NULL)) { g_prefix_error(error, "failed to register manufacturer %s: ", manufacturer); return FALSE; } } } serial_number = fu_bluez_device_read_string(self, FU_BLUEZ_DEVICE_UUID_DI_SERIAL_NUMBER, NULL); if (serial_number != NULL) fu_device_set_serial(FU_DEVICE(self), serial_number); fw_revision = fu_bluez_device_read_string(self, FU_BLUEZ_DEVICE_UUID_DI_FIRMWARE_REVISION, NULL); if (fw_revision != NULL) { fu_device_set_version_format(FU_DEVICE(self), fu_version_guess_format(fw_revision)); fu_device_set_version(FU_DEVICE(self), fw_revision); /* nocheck:set-version */ } /* success */ return TRUE; } /* * Populates the {uuid_helper : object_path} entries of a device for all its * characteristics. */ static gboolean fu_bluez_device_ensure_gatt_interfaces(FuBluezDevice *self, GError **error) { FuBluezDevicePrivate *priv = GET_PRIVATE(self); guint valid = 0; g_autolist(GDBusObject) obj_list = NULL; obj_list = g_dbus_object_manager_get_objects(priv->object_manager); for (GList *l = obj_list; l != NULL; l = l->next) { GDBusObject *obj = G_DBUS_OBJECT(l->data); const gchar *obj_path = g_dbus_object_get_object_path(obj); /* not us */ if (!g_str_has_prefix(g_dbus_object_get_object_path(obj), g_dbus_proxy_get_object_path(priv->proxy))) continue; /* add characteristics UUID for reading and writing */ if (g_dbus_object_get_interface(obj, "org.bluez.GattCharacteristic1")) { if (!fu_bluez_device_add_characteristic_uuid( self, obj, obj_path, "org.bluez.GattCharacteristic1", error)) { g_prefix_error(error, "failed to add characteristic uuid: "); return FALSE; } valid += 1; } if (g_dbus_object_get_interface(obj, "org.bluez.GattService1")) { if (!fu_bluez_device_add_instance_by_service_uuid(self, obj, obj_path, "org.bluez.GattService1", error)) { g_prefix_error(error, "failed to add service uuid: "); return FALSE; } valid += 1; } /* battery level is optional */ if (g_dbus_object_get_interface(obj, "org.bluez.Battery1")) { if (!fu_bluez_device_read_battery_interface(self, obj, obj_path, "org.bluez.Battery1", error)) { g_prefix_error(error, "failed to add battery: "); return FALSE; } } } /* found nothing */ if (valid == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no supported GATT characteristic or service"); return FALSE; } /* success */ return TRUE; } static gboolean fu_bluez_device_probe(FuDevice *device, GError **error) { FuBluezDevice *self = FU_BLUEZ_DEVICE(device); FuBluezDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(GVariant) val_adapter = NULL; g_autoptr(GVariant) val_address = NULL; g_autoptr(GVariant) val_icon = NULL; g_autoptr(GVariant) val_modalias = NULL; g_autoptr(GVariant) val_name = NULL; g_autoptr(GVariant) val_alias = NULL; /* sanity check */ if (priv->proxy == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy set"); return FALSE; } val_address = g_dbus_proxy_get_cached_property(priv->proxy, "Address"); if (val_address == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "No required BLE address"); return FALSE; } fu_device_set_logical_id(device, g_variant_get_string(val_address, NULL)); val_adapter = g_dbus_proxy_get_cached_property(priv->proxy, "Adapter"); if (val_adapter != NULL) fu_device_set_physical_id(device, g_variant_get_string(val_adapter, NULL)); val_name = g_dbus_proxy_get_cached_property(priv->proxy, "Name"); if (val_name != NULL) { fu_device_set_name(device, g_variant_get_string(val_name, NULL)); /* register the device by its alias, since modalias could be absent */ fu_device_add_instance_str(FU_DEVICE(self), "NAME", g_variant_get_string(val_name, NULL)); fu_device_build_instance_id_full(FU_DEVICE(self), FU_DEVICE_INSTANCE_FLAG_VISIBLE | FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "BLUETOOTH", "NAME", NULL); } val_alias = g_dbus_proxy_get_cached_property(priv->proxy, "Alias"); if (val_alias != NULL) { fu_device_set_name(device, g_variant_get_string(val_alias, NULL)); /* register the device by its alias, since modalias could be absent */ fu_device_add_instance_str(FU_DEVICE(self), "ALIAS", g_variant_get_string(val_alias, NULL)); fu_device_build_instance_id_full(FU_DEVICE(self), FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "BLUETOOTH", "ALIAS", NULL); } val_icon = g_dbus_proxy_get_cached_property(priv->proxy, "Icon"); if (val_icon != NULL) fu_device_add_icon(device, g_variant_get_string(val_icon, NULL)); val_modalias = g_dbus_proxy_get_cached_property(priv->proxy, "Modalias"); if (val_modalias != NULL) fu_bluez_device_set_modalias(self, g_variant_get_string(val_modalias, NULL)); /* success, if we added one service or characteristic */ if (!fu_bluez_device_ensure_gatt_interfaces(self, error)) return FALSE; /* try to parse Device Information service if available */ return fu_bluez_device_parse_device_information_service(self, error); } static gboolean fu_bluez_device_reload(FuDevice *device, GError **error) { FuBluezDevice *self = FU_BLUEZ_DEVICE(device); return fu_bluez_device_parse_device_information_service(self, error); } /** * fu_bluez_device_read: * @self: a #FuBluezDevice * @uuid: the UUID, e.g. `00cde35c-7062-11eb-9439-0242ac130002` * @error: (nullable): optional return location for an error * * Reads from a UUID on the device. * * Returns: (transfer full): data, or %NULL for error * * Since: 1.5.7 **/ GByteArray * fu_bluez_device_read(FuBluezDevice *self, const gchar *uuid, GError **error) { FuBluezDeviceUuidHelper *uuid_helper; guint8 byte; g_autofree gchar *title = NULL; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GVariantBuilder) builder = NULL; g_autoptr(GVariantIter) iter = NULL; g_autoptr(GVariant) val = NULL; g_return_val_if_fail(FU_IS_BLUEZ_DEVICE(self), NULL); g_return_val_if_fail(uuid != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); uuid_helper = fu_bluez_device_get_uuid_helper(self, uuid, error); if (uuid_helper == NULL) return NULL; if (!fu_bluez_device_ensure_uuid_helper_proxy(uuid_helper, error)) return NULL; /* * Call the "ReadValue" method through the proxy synchronously. * * The method takes one argument: an array of dicts of * {string:value} specifying the options (here the option is * "offset":0. * The result is a byte array. */ builder = g_variant_builder_new(G_VARIANT_TYPE("a{sv}")); g_variant_builder_add(builder, "{sv}", "offset", g_variant_new("q", 0)); val = g_dbus_proxy_call_sync(uuid_helper->proxy, "ReadValue", g_variant_new("(a{sv})", builder), G_DBUS_CALL_FLAGS_NONE, -1, NULL, error); if (val == NULL) { g_prefix_error(error, "Failed to read GattCharacteristic1: "); return NULL; } g_variant_get(val, "(ay)", &iter); while (g_variant_iter_loop(iter, "y", &byte)) g_byte_array_append(buf, &byte, 1); /* debug a bit */ title = g_strdup_printf("ReadValue[%s]", uuid); fu_dump_raw(G_LOG_DOMAIN, title, buf->data, buf->len); /* success */ return g_steal_pointer(&buf); } /** * fu_bluez_device_read_string: * @self: a #FuBluezDevice * @uuid: the UUID, e.g. `00cde35c-7062-11eb-9439-0242ac130002` * @error: (nullable): optional return location for an error * * Reads a string from a UUID on the device. * * Returns: (transfer full): NUL-terminated string, or %NULL for error * * Since: 1.5.7 **/ gchar * fu_bluez_device_read_string(FuBluezDevice *self, const gchar *uuid, GError **error) { g_autoptr(GByteArray) buf = fu_bluez_device_read(self, uuid, error); if (buf == NULL) return NULL; if (!g_utf8_validate((const gchar *)buf->data, buf->len, NULL)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "UUID %s did not return a valid UTF-8 string", uuid); return NULL; } return g_strndup((const gchar *)buf->data, buf->len); } /** * fu_bluez_device_write: * @self: a #FuBluezDevice * @uuid: the UUID, e.g. `00cde35c-7062-11eb-9439-0242ac130002` * @buf: data array * @error: (nullable): optional return location for an error * * Writes to a UUID on the device. * * Returns: %TRUE if all the data was written * * Since: 1.5.7 **/ gboolean fu_bluez_device_write(FuBluezDevice *self, const gchar *uuid, GByteArray *buf, GError **error) { FuBluezDeviceUuidHelper *uuid_helper; g_autofree gchar *title = NULL; g_autoptr(GVariantBuilder) opt_builder = NULL; g_autoptr(GVariantBuilder) val_builder = NULL; g_autoptr(GVariant) ret = NULL; GVariant *opt_variant = NULL; GVariant *val_variant = NULL; g_return_val_if_fail(FU_IS_BLUEZ_DEVICE(self), FALSE); g_return_val_if_fail(uuid != NULL, FALSE); g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); uuid_helper = fu_bluez_device_get_uuid_helper(self, uuid, error); if (uuid_helper == NULL) return FALSE; if (!fu_bluez_device_ensure_uuid_helper_proxy(uuid_helper, error)) return FALSE; /* debug a bit */ title = g_strdup_printf("WriteValue[%s]", uuid); fu_dump_raw(G_LOG_DOMAIN, title, buf->data, buf->len); /* build the value variant */ val_builder = g_variant_builder_new(G_VARIANT_TYPE("ay")); for (gsize i = 0; i < buf->len; i++) g_variant_builder_add(val_builder, "y", buf->data[i]); val_variant = g_variant_new("ay", val_builder); /* build the options variant (offset = 0) */ opt_builder = g_variant_builder_new(G_VARIANT_TYPE("a{sv}")); g_variant_builder_add(opt_builder, "{sv}", "offset", g_variant_new_uint16(0)); opt_variant = g_variant_new("a{sv}", opt_builder); ret = g_dbus_proxy_call_sync(uuid_helper->proxy, "WriteValue", g_variant_new("(@ay@a{sv})", val_variant, opt_variant), G_DBUS_CALL_FLAGS_NONE, -1, NULL, error); if (ret == NULL) { g_prefix_error(error, "Failed to write GattCharacteristic1: "); return FALSE; } /* success */ return TRUE; } /** * fu_bluez_device_notify_start: * @self: a #FuBluezDevice * @uuid: the UUID, e.g. `00cde35c-7062-11eb-9439-0242ac130002` * @error: (nullable): optional return location for an error * * Enables notifications for property changes in a UUID (StartNotify * method). * * Returns: %TRUE if the method call completed successfully. * * Since: 1.5.8 **/ gboolean fu_bluez_device_notify_start(FuBluezDevice *self, const gchar *uuid, GError **error) { FuBluezDeviceUuidHelper *uuid_helper; g_autoptr(GVariant) retval = NULL; g_return_val_if_fail(FU_IS_BLUEZ_DEVICE(self), FALSE); g_return_val_if_fail(uuid != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); uuid_helper = fu_bluez_device_get_uuid_helper(self, uuid, error); if (uuid_helper == NULL) return FALSE; if (!fu_bluez_device_ensure_uuid_helper_proxy(uuid_helper, error)) return FALSE; retval = g_dbus_proxy_call_sync(uuid_helper->proxy, "StartNotify", NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, error); if (retval == NULL) { g_prefix_error(error, "Failed to enable notifications: "); return FALSE; } return TRUE; } /** * fu_bluez_device_notify_stop: * @self: a #FuBluezDevice * @uuid: the UUID, e.g. `00cde35c-7062-11eb-9439-0242ac130002` * @error: (nullable): optional return location for an error * * Disables notifications for property changes in a UUID (StopNotify * method). * * Returns: %TRUE if the method call completed successfully. * * Since: 1.5.8 **/ gboolean fu_bluez_device_notify_stop(FuBluezDevice *self, const gchar *uuid, GError **error) { FuBluezDeviceUuidHelper *uuid_helper; g_autoptr(GVariant) retval = NULL; g_return_val_if_fail(FU_IS_BLUEZ_DEVICE(self), FALSE); g_return_val_if_fail(uuid != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); uuid_helper = fu_bluez_device_get_uuid_helper(self, uuid, error); if (uuid_helper == NULL) return FALSE; if (!fu_bluez_device_ensure_uuid_helper_proxy(uuid_helper, error)) return FALSE; retval = g_dbus_proxy_call_sync(uuid_helper->proxy, "StopNotify", NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, error); if (retval == NULL) { g_prefix_error(error, "Failed to enable notifications: "); return FALSE; } return TRUE; } static FuIOChannel * fu_bluez_device_method_acquire(FuBluezDevice *self, const gchar *method, const gchar *uuid, gint32 *mtu, GError **error) { #ifdef HAVE_GIO_UNIX FuBluezDeviceUuidHelper *uuid_helper; GVariant *opt_variant = NULL; gint fd_id; g_autoptr(GVariantBuilder) opt_builder = NULL; g_autoptr(GVariant) val = NULL; g_autoptr(GUnixFDList) out_fd_list = NULL; uuid_helper = fu_bluez_device_get_uuid_helper(self, uuid, error); if (uuid_helper == NULL) return NULL; if (!fu_bluez_device_ensure_uuid_helper_proxy(uuid_helper, error)) return NULL; opt_builder = g_variant_builder_new(G_VARIANT_TYPE("a{sv}")); opt_variant = g_variant_new("a{sv}", opt_builder); val = g_dbus_proxy_call_with_unix_fd_list_sync(uuid_helper->proxy, method, g_variant_new("(@a{sv})", opt_variant), G_DBUS_CALL_FLAGS_NONE, -1, NULL, // fd list &out_fd_list, NULL, error); if (val == NULL) { g_prefix_error(error, "failed to call %s: ", method); return NULL; } g_variant_get_child(val, 0, "h", &fd_id); if (mtu != NULL) g_variant_get_child(val, 1, "q", mtu); return fu_io_channel_unix_new(g_unix_fd_list_get(out_fd_list, fd_id, NULL)); #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported as not available"); return NULL; #endif } /** * fu_bluez_device_notify_acquire: * @self: a #FuBluezDevice * @uuid: the UUID, e.g. `00cde35c-7062-11eb-9439-0242ac130002` * @mtu: (out) (optional): MTU of the channel on success * @error: (nullable): optional return location for an error * * Acquire notifications for property changes in a UUID (AcquireNotify * method). Closing IO channel releases the notify. * * Returns: (transfer full): a #FuIOChannel or %NULL on error * * Since: 2.0.0 **/ FuIOChannel * fu_bluez_device_notify_acquire(FuBluezDevice *self, const gchar *uuid, gint32 *mtu, GError **error) { g_return_val_if_fail(FU_IS_BLUEZ_DEVICE(self), NULL); g_return_val_if_fail(uuid != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return fu_bluez_device_method_acquire(self, "AcquireNotify", uuid, mtu, error); } /** * fu_bluez_device_write_acquire: * @self: a #FuBluezDevice * @uuid: the UUID, e.g. `00cde35c-7062-11eb-9439-0242ac130002` * @mtu: (out) (optional): MTU of the channel * @error: (nullable): optional return location for an error * * Acquire notifications for property changes in a UUID (AcquireNotify * method). Closing IO channel releases the notify. * * Returns: (transfer full): a #FuIOChannel or %NULL on error * * Since: 2.0.0 **/ FuIOChannel * fu_bluez_device_write_acquire(FuBluezDevice *self, const gchar *uuid, gint32 *mtu, GError **error) { g_return_val_if_fail(FU_IS_BLUEZ_DEVICE(self), NULL); g_return_val_if_fail(uuid != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return fu_bluez_device_method_acquire(self, "AcquireWrite", uuid, mtu, error); } static void fu_bluez_device_incorporate(FuDevice *self, FuDevice *donor) { FuBluezDevice *uself = FU_BLUEZ_DEVICE(self); FuBluezDevice *udonor = FU_BLUEZ_DEVICE(donor); FuBluezDevicePrivate *priv = GET_PRIVATE(uself); FuBluezDevicePrivate *privdonor = GET_PRIVATE(udonor); g_return_if_fail(FU_IS_BLUEZ_DEVICE(self)); g_return_if_fail(FU_IS_BLUEZ_DEVICE(donor)); if (g_hash_table_size(priv->uuids) == 0) { GHashTableIter iter; gpointer key, value; g_hash_table_iter_init(&iter, privdonor->uuids); while (g_hash_table_iter_next(&iter, &key, &value)) { FuBluezDeviceUuidHelper *uuid_helper = (FuBluezDeviceUuidHelper *)value; fu_bluez_device_add_uuid_path(uself, (const gchar *)key, uuid_helper->path); } } if (priv->object_manager == NULL) priv->object_manager = g_object_ref(privdonor->object_manager); if (priv->proxy == NULL) priv->proxy = g_object_ref(privdonor->proxy); } static gchar * fu_bluez_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint16(version_raw, fu_device_get_version_format(device)); } static void fu_bluez_device_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuBluezDevice *self = FU_BLUEZ_DEVICE(object); FuBluezDevicePrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_OBJECT_MANAGER: g_value_set_object(value, priv->object_manager); break; case PROP_PROXY: g_value_set_object(value, priv->proxy); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_bluez_device_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuBluezDevice *self = FU_BLUEZ_DEVICE(object); FuBluezDevicePrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_OBJECT_MANAGER: priv->object_manager = g_value_dup_object(value); break; case PROP_PROXY: priv->proxy = g_value_dup_object(value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_bluez_device_finalize(GObject *object) { FuBluezDevice *self = FU_BLUEZ_DEVICE(object); FuBluezDevicePrivate *priv = GET_PRIVATE(self); g_hash_table_unref(priv->uuids); if (priv->proxy != NULL) g_object_unref(priv->proxy); if (priv->object_manager != NULL) g_object_unref(priv->object_manager); G_OBJECT_CLASS(fu_bluez_device_parent_class)->finalize(object); } static void fu_bluez_device_init(FuBluezDevice *self) { FuBluezDevicePrivate *priv = GET_PRIVATE(self); priv->uuids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)fu_bluez_device_uuid_free); } static void fu_bluez_device_class_init(FuBluezDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); GParamSpec *pspec; object_class->get_property = fu_bluez_device_get_property; object_class->set_property = fu_bluez_device_set_property; object_class->finalize = fu_bluez_device_finalize; device_class->probe = fu_bluez_device_probe; device_class->reload = fu_bluez_device_reload; device_class->to_string = fu_bluez_device_to_string; device_class->incorporate = fu_bluez_device_incorporate; device_class->convert_version = fu_bluez_device_convert_version; /** * FuBluezDevice::changed: * @self: the #FuBluezDevice instance that emitted the signal * @uuid: the UUID that changed * * The ::changed signal is emitted when a service with a specific UUID changed. * * Since: 1.5.8 **/ signals[SIGNAL_CHANGED] = g_signal_new("changed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__STRING, G_TYPE_NONE, 1, G_TYPE_STRING); /** * FuBluezDevice:object-manager: * * The object manager instance for all devices. * * Since: 1.5.8 */ pspec = g_param_spec_object("object-manager", NULL, NULL, G_TYPE_DBUS_OBJECT_MANAGER, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_OBJECT_MANAGER, pspec); /** * FuBluezDevice:proxy: * * The D-Bus proxy for the object. * * Since: 1.5.8 */ pspec = g_param_spec_object("proxy", NULL, NULL, G_TYPE_DBUS_PROXY, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_PROXY, pspec); } fwupd-2.0.10/libfwupdplugin/fu-bluez-device.h000066400000000000000000000037541501337203100211150ustar00rootroot00000000000000/* * Copyright 2021 Ricardo Cañuelo * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-device.h" #include "fu-io-channel.h" #define FU_TYPE_BLUEZ_DEVICE (fu_bluez_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuBluezDevice, fu_bluez_device, FU, BLUEZ_DEVICE, FuDevice) struct _FuBluezDeviceClass { FuDeviceClass parent_class; }; /* Device Information service attributes */ #define FU_BLUEZ_DEVICE_UUID_DI_SYSTEM_ID "00002a23-0000-1000-8000-00805f9b34fb" #define FU_BLUEZ_DEVICE_UUID_DI_MODEL_NUMBER "00002a24-0000-1000-8000-00805f9b34fb" #define FU_BLUEZ_DEVICE_UUID_DI_SERIAL_NUMBER "00002a25-0000-1000-8000-00805f9b34fb" #define FU_BLUEZ_DEVICE_UUID_DI_FIRMWARE_REVISION "00002a26-0000-1000-8000-00805f9b34fb" #define FU_BLUEZ_DEVICE_UUID_DI_HARDWARE_REVISION "00002a27-0000-1000-8000-00805f9b34fb" #define FU_BLUEZ_DEVICE_UUID_DI_SOFTWARE_REVISION "00002a28-0000-1000-8000-00805f9b34fb" #define FU_BLUEZ_DEVICE_UUID_DI_MANUFACTURER_NAME "00002a29-0000-1000-8000-00805f9b34fb" #define FU_BLUEZ_DEVICE_UUID_DI_PNP_UID "00002a50-0000-1000-8000-00805f9b34fb" GByteArray * fu_bluez_device_read(FuBluezDevice *self, const gchar *uuid, GError **error) G_GNUC_NON_NULL(1, 2); gchar * fu_bluez_device_read_string(FuBluezDevice *self, const gchar *uuid, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_bluez_device_write(FuBluezDevice *self, const gchar *uuid, GByteArray *buf, GError **error) G_GNUC_NON_NULL(1, 2, 3); gboolean fu_bluez_device_notify_start(FuBluezDevice *self, const gchar *uuid, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_bluez_device_notify_stop(FuBluezDevice *self, const gchar *uuid, GError **error) G_GNUC_NON_NULL(1, 2); FuIOChannel * fu_bluez_device_notify_acquire(FuBluezDevice *self, const gchar *uuid, gint32 *mtu, GError **error) G_GNUC_NON_NULL(1, 2, 3); FuIOChannel * fu_bluez_device_write_acquire(FuBluezDevice *self, const gchar *uuid, gint32 *mtu, GError **error) G_GNUC_NON_NULL(1, 2, 3); fwupd-2.0.10/libfwupdplugin/fu-byte-array.c000066400000000000000000000123551501337203100206060ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuCommon" #include "config.h" #include "fu-byte-array.h" #include "fu-common.h" #include "fu-firmware-common.h" #include "fu-mem.h" /** * fu_byte_array_to_string: * @array: a #GByteArray * * Converts the byte array to a lowercase hex string. * * Returns: (transfer full): a string, which may be zero length * * Since: 1.8.9 **/ gchar * fu_byte_array_to_string(GByteArray *array) { g_autoptr(GString) str = g_string_new(NULL); g_return_val_if_fail(array != NULL, NULL); for (guint i = 0; i < array->len; i++) g_string_append_printf(str, "%02x", array->data[i]); return g_string_free(g_steal_pointer(&str), FALSE); } /** * fu_byte_array_from_string: * @str: a hex string * @error: (nullable): optional return location for an error * * Converts a lowercase hex string to a byte array. * * Returns: (transfer full): a #GByteArray, or %NULL on error * * Since: 1.9.6 **/ GByteArray * fu_byte_array_from_string(const gchar *str, GError **error) { gsize strsz; g_autoptr(GByteArray) buf = g_byte_array_new(); g_return_val_if_fail(str != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); strsz = strlen(str); for (guint i = 0; i < strsz; i += 2) { guint8 value = 0; if (!fu_firmware_strparse_uint8_safe(str, strsz, i, &value, error)) return NULL; fu_byte_array_append_uint8(buf, value); } return g_steal_pointer(&buf); } /** * fu_byte_array_append_uint8: * @array: a #GByteArray * @data: value * * Adds a 8 bit integer to a byte array. * * Since: 1.3.1 **/ void fu_byte_array_append_uint8(GByteArray *array, guint8 data) { g_byte_array_append(array, &data, sizeof(data)); } /** * fu_byte_array_append_uint16: * @array: a #GByteArray * @data: value * @endian: endian type, e.g. #G_LITTLE_ENDIAN * * Adds a 16 bit integer to a byte array. * * Since: 1.3.1 **/ void fu_byte_array_append_uint16(GByteArray *array, guint16 data, FuEndianType endian) { guint8 buf[2]; fu_memwrite_uint16(buf, data, endian); g_byte_array_append(array, buf, sizeof(buf)); } /** * fu_byte_array_append_uint24: * @array: a #GByteArray * @data: value * @endian: endian type, e.g. #G_LITTLE_ENDIAN * * Adds a 24 bit integer to a byte array. * * Since: 1.8.13 **/ void fu_byte_array_append_uint24(GByteArray *array, guint32 data, FuEndianType endian) { guint8 buf[3]; fu_memwrite_uint24(buf, data, endian); g_byte_array_append(array, buf, sizeof(buf)); } /** * fu_byte_array_append_uint32: * @array: a #GByteArray * @data: value * @endian: endian type, e.g. #G_LITTLE_ENDIAN * * Adds a 32 bit integer to a byte array. * * Since: 1.3.1 **/ void fu_byte_array_append_uint32(GByteArray *array, guint32 data, FuEndianType endian) { guint8 buf[4]; fu_memwrite_uint32(buf, data, endian); g_byte_array_append(array, buf, sizeof(buf)); } /** * fu_byte_array_append_uint64: * @array: a #GByteArray * @data: value * @endian: endian type, e.g. #G_LITTLE_ENDIAN * * Adds a 64 bit integer to a byte array. * * Since: 1.5.8 **/ void fu_byte_array_append_uint64(GByteArray *array, guint64 data, FuEndianType endian) { guint8 buf[8]; fu_memwrite_uint64(buf, data, endian); g_byte_array_append(array, buf, sizeof(buf)); } /** * fu_byte_array_append_bytes: * @array: a #GByteArray * @bytes: data blob * * Adds the contents of a GBytes to a byte array. * * Since: 1.5.8 **/ void fu_byte_array_append_bytes(GByteArray *array, GBytes *bytes) { g_byte_array_append(array, g_bytes_get_data(bytes, NULL), g_bytes_get_size(bytes)); } /** * fu_byte_array_set_size: * @array: a #GByteArray * @length: the new size of the GByteArray * @data: the byte used to pad the array * * Sets the size of the GByteArray, expanding with @data as required. * * Since: 1.8.2 **/ void fu_byte_array_set_size(GByteArray *array, gsize length, guint8 data) { guint oldlength = array->len; g_return_if_fail(array != NULL); g_return_if_fail(length < G_MAXUINT); g_byte_array_set_size(array, length); if (length > oldlength) memset(array->data + oldlength, data, length - oldlength); } /** * fu_byte_array_align_up: * @array: a #GByteArray * @alignment: align to this power of 2 * @data: the byte used to pad the array * * Align a byte array length to a power of 2 boundary, where @alignment is the * bit position to align to. If @alignment is zero then @array is unchanged. * * Since: 1.6.0 **/ void fu_byte_array_align_up(GByteArray *array, guint8 alignment, guint8 data) { fu_byte_array_set_size(array, fu_common_align_up(array->len, alignment), data); } /** * fu_byte_array_compare: * @buf1: a data blob * @buf2: another #GByteArray * @error: (nullable): optional return location for an error * * Compares two buffers for equality. * * Returns: %TRUE if @buf1 and @buf2 are identical * * Since: 1.8.0 **/ gboolean fu_byte_array_compare(GByteArray *buf1, GByteArray *buf2, GError **error) { g_return_val_if_fail(buf1 != NULL, FALSE); g_return_val_if_fail(buf2 != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return fu_memcmp_safe(buf1->data, buf1->len, 0x0, buf2->data, buf2->len, 0x0, MAX(buf1->len, buf2->len), error); } fwupd-2.0.10/libfwupdplugin/fu-byte-array.h000066400000000000000000000023411501337203100206050ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-endian.h" gchar * fu_byte_array_to_string(GByteArray *array) G_GNUC_NON_NULL(1); GByteArray * fu_byte_array_from_string(const gchar *str, GError **error) G_GNUC_NON_NULL(1); void fu_byte_array_set_size(GByteArray *array, gsize length, guint8 data) G_GNUC_NON_NULL(1); void fu_byte_array_align_up(GByteArray *array, guint8 alignment, guint8 data) G_GNUC_NON_NULL(1); void fu_byte_array_append_uint8(GByteArray *array, guint8 data) G_GNUC_NON_NULL(1); void fu_byte_array_append_uint16(GByteArray *array, guint16 data, FuEndianType endian) G_GNUC_NON_NULL(1); void fu_byte_array_append_uint24(GByteArray *array, guint32 data, FuEndianType endian) G_GNUC_NON_NULL(1); void fu_byte_array_append_uint32(GByteArray *array, guint32 data, FuEndianType endian) G_GNUC_NON_NULL(1); void fu_byte_array_append_uint64(GByteArray *array, guint64 data, FuEndianType endian) G_GNUC_NON_NULL(1); void fu_byte_array_append_bytes(GByteArray *array, GBytes *bytes) G_GNUC_NON_NULL(1, 2); gboolean fu_byte_array_compare(GByteArray *buf1, GByteArray *buf2, GError **error) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-bytes.c000066400000000000000000000205671501337203100176610ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuCommon" #include "config.h" #include "fwupd-error.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-input-stream.h" #include "fu-mem.h" /** * fu_bytes_set_contents: * @filename: a filename * @bytes: data to write * @error: (nullable): optional return location for an error * * Writes a blob of data to a filename, creating the parent directories as * required. * * Returns: %TRUE for success * * Since: 1.8.2 **/ gboolean fu_bytes_set_contents(const gchar *filename, GBytes *bytes, GError **error) { const gchar *data; gsize size; g_autoptr(GFile) file = NULL; g_autoptr(GFile) file_parent = NULL; g_return_val_if_fail(filename != NULL, FALSE); g_return_val_if_fail(bytes != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); file = g_file_new_for_path(filename); file_parent = g_file_get_parent(file); if (!g_file_query_exists(file_parent, NULL)) { if (!g_file_make_directory_with_parents(file_parent, NULL, error)) return FALSE; } data = g_bytes_get_data(bytes, &size); g_debug("writing %s with 0x%x bytes", filename, (guint)size); return g_file_set_contents(filename, data, size, error); } /** * fu_bytes_get_contents: * @filename: a filename * @error: (nullable): optional return location for an error * * Reads a blob of data from a file. * * Returns: a #GBytes, or %NULL for failure * * Since: 1.8.2 **/ GBytes * fu_bytes_get_contents(const gchar *filename, GError **error) { g_autoptr(GError) error_local = NULL; g_autoptr(GMappedFile) mapped_file = NULL; g_return_val_if_fail(filename != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* try as a mapped file, falling back to reading it as a blob instead */ mapped_file = g_mapped_file_new(filename, FALSE, &error_local); if (mapped_file == NULL || g_mapped_file_get_length(mapped_file) == 0) { gchar *data = NULL; gsize len = 0; if (!g_file_get_contents(filename, &data, &len, error)) { fwupd_error_convert(error); return NULL; } g_debug("failed to read as mapped file, " "so reading %s of size 0x%x: %s", filename, (guint)len, error_local != NULL ? error_local->message : "zero size"); return g_bytes_new_take(data, len); } g_debug("mapped file %s of size 0x%x", filename, (guint)g_mapped_file_get_length(mapped_file)); return g_mapped_file_get_bytes(mapped_file); } /** * fu_bytes_align: * @bytes: data blob * @blksz: block size in bytes * @padval: the byte used to pad the byte buffer * * Aligns a block of memory to @blksize using the @padval value; if * the block is already aligned then the original @bytes is returned. * * Returns: (transfer full): a #GBytes, possibly @bytes * * Since: 1.8.2 **/ GBytes * fu_bytes_align(GBytes *bytes, gsize blksz, gchar padval) { const guint8 *data; gsize sz; g_return_val_if_fail(bytes != NULL, NULL); g_return_val_if_fail(blksz > 0, NULL); /* pad */ data = g_bytes_get_data(bytes, &sz); if (sz % blksz != 0) { gsize sz_align = ((sz / blksz) + 1) * blksz; guint8 *data_align = g_malloc(sz_align); memcpy(data_align, data, sz); /* nocheck:blocked */ memset(data_align + sz, padval, sz_align - sz); g_debug("aligning 0x%x bytes to 0x%x", (guint)sz, (guint)sz_align); return g_bytes_new_take(data_align, sz_align); } /* perfectly aligned */ return g_bytes_ref(bytes); } /** * fu_bytes_is_empty: * @bytes: data blob * * Checks if a byte array are just empty (0xff) bytes. * * Returns: %TRUE if @bytes is empty * * Since: 1.8.2 **/ gboolean fu_bytes_is_empty(GBytes *bytes) { gsize sz = 0; const guint8 *buf = g_bytes_get_data(bytes, &sz); for (gsize i = 0; i < sz; i++) { if (buf[i] != 0xff) return FALSE; } return TRUE; } /** * fu_bytes_compare: * @bytes1: a data blob * @bytes2: another #GBytes * @error: (nullable): optional return location for an error * * Compares the buffers for equality. * * Returns: %TRUE if @bytes1 and @bytes2 are identical * * Since: 1.8.2 **/ gboolean fu_bytes_compare(GBytes *bytes1, GBytes *bytes2, GError **error) { const guint8 *buf1; const guint8 *buf2; gsize bufsz1; gsize bufsz2; g_return_val_if_fail(bytes1 != NULL, FALSE); g_return_val_if_fail(bytes2 != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); buf1 = g_bytes_get_data(bytes1, &bufsz1); buf2 = g_bytes_get_data(bytes2, &bufsz2); return fu_memcmp_safe(buf1, bufsz1, 0x0, buf2, bufsz2, 0x0, MAX(bufsz1, bufsz2), error); } /** * fu_bytes_pad: * @bytes: data blob * @sz: the desired size in bytes * @data: the byte used to pad the array * * Pads a GBytes to a minimum @sz with `0xff`. * * Returns: (transfer full): a data blob * * Since: 2.0.7 **/ GBytes * fu_bytes_pad(GBytes *bytes, gsize sz, guint8 data) { gsize bytes_sz; g_return_val_if_fail(bytes != NULL, NULL); g_return_val_if_fail(sz != 0, NULL); /* pad */ bytes_sz = g_bytes_get_size(bytes); if (bytes_sz < sz) { const guint8 *data_old = g_bytes_get_data(bytes, NULL); guint8 *data_new = g_malloc(sz); if (data_old != NULL) memcpy(data_new, data_old, bytes_sz); /* nocheck:blocked */ memset(data_new + bytes_sz, data, sz - bytes_sz); return g_bytes_new_take(data_new, sz); } /* not required */ return g_bytes_ref(bytes); } /** * fu_bytes_new_offset: * @bytes: data blob * @offset: where subsection starts at * @length: length of subsection * @error: (nullable): optional return location for an error * * Creates a #GBytes which is a subsection of another #GBytes. * * Returns: (transfer full): a #GBytes, or #NULL if range is invalid * * Since: 1.8.2 **/ GBytes * fu_bytes_new_offset(GBytes *bytes, gsize offset, gsize length, GError **error) { g_return_val_if_fail(bytes != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* optimize */ if (offset == 0x0 && g_bytes_get_size(bytes) == length) return g_bytes_ref(bytes); /* sanity check */ if (offset + length < length || offset + length > g_bytes_get_size(bytes)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "cannot create bytes @0x%02x for 0x%02x " "as buffer only 0x%04x bytes in size", (guint)offset, (guint)length, (guint)g_bytes_get_size(bytes)); return NULL; } return g_bytes_new_from_bytes(bytes, offset, length); } /** * fu_bytes_get_data_safe: * @bytes: data blob * @bufsz: (out) (optional): location to return size of byte data * @error: (nullable): optional return location for an error * * Get the byte data in the #GBytes. This data should not be modified. * This function will always return the same pointer for a given #GBytes. * * If the size of @bytes is zero, then %NULL is returned and the @error is set, * which differs in behavior to that of g_bytes_get_data(). * * This may be useful when calling g_mapped_file_new() on a zero-length file. * * Returns: a pointer to the byte data, or %NULL. * * Since: 1.6.0 **/ const guint8 * fu_bytes_get_data_safe(GBytes *bytes, gsize *bufsz, GError **error) { const guint8 *buf = g_bytes_get_data(bytes, bufsz); if (buf == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid data"); return NULL; } return buf; } /** * fu_bytes_to_string: * @bytes: data blob * * Converts @bytes to a lowercase hex string. * * Returns: (transfer full): a string, which may be zero length * * Since: 2.0.4 **/ gchar * fu_bytes_to_string(GBytes *bytes) { const guint8 *buf; gsize bufsz = 0; g_autoptr(GString) str = g_string_new(NULL); g_return_val_if_fail(bytes != NULL, NULL); buf = g_bytes_get_data(bytes, &bufsz); for (gsize i = 0; i < bufsz; i++) g_string_append_printf(str, "%02x", buf[i]); return g_string_free(g_steal_pointer(&str), FALSE); } /** * fu_bytes_from_string: * @str: a hex string * @error: (nullable): optional return location for an error * * Converts a lowercase hex string to a #GBytes. * * Returns: (transfer full): a #GBytes, or %NULL on error * * Since: 2.0.5 **/ GBytes * fu_bytes_from_string(const gchar *str, GError **error) { g_autoptr(GByteArray) buf = NULL; g_return_val_if_fail(str != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); buf = fu_byte_array_from_string(str, error); if (buf == NULL) return NULL; return g_byte_array_free_to_bytes(g_steal_pointer(&buf)); /* nocheck:blocked */ } fwupd-2.0.10/libfwupdplugin/fu-bytes.h000066400000000000000000000022401501337203100176520ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include gboolean fu_bytes_set_contents(const gchar *filename, GBytes *bytes, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); GBytes * fu_bytes_get_contents(const gchar *filename, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); GBytes * fu_bytes_align(GBytes *bytes, gsize blksz, gchar padval) G_GNUC_NON_NULL(1); const guint8 * fu_bytes_get_data_safe(GBytes *bytes, gsize *bufsz, GError **error) G_GNUC_NON_NULL(1); gboolean fu_bytes_is_empty(GBytes *bytes) G_GNUC_NON_NULL(1); gboolean fu_bytes_compare(GBytes *bytes1, GBytes *bytes2, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); GBytes * fu_bytes_pad(GBytes *bytes, gsize sz, guint8 data) G_GNUC_NON_NULL(1); GBytes * fu_bytes_new_offset(GBytes *bytes, gsize offset, gsize length, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gchar * fu_bytes_to_string(GBytes *bytes) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); GBytes * fu_bytes_from_string(const gchar *str, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-cab-firmware-private.h000066400000000000000000000004121501337203100225320ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-cab-firmware.h" gboolean fu_cab_firmware_compute_checksum(const guint8 *buf, gsize bufsz, guint32 *checksum, GError **error); fwupd-2.0.10/libfwupdplugin/fu-cab-firmware.c000066400000000000000000000671151501337203100210720ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuCabFirmware" #include "config.h" #include #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-cab-firmware-private.h" #include "fu-cab-image.h" #include "fu-cab-struct.h" #include "fu-chunk-array.h" #include "fu-common.h" #include "fu-composite-input-stream.h" #include "fu-input-stream.h" #include "fu-mem-private.h" #include "fu-partial-input-stream.h" #include "fu-string.h" typedef struct { gboolean compressed; gboolean only_basename; } FuCabFirmwarePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuCabFirmware, fu_cab_firmware, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_cab_firmware_get_instance_private(o)) #define FU_CAB_FIRMWARE_MAX_FILES 1024 #define FU_CAB_FIRMWARE_MAX_FOLDERS 64 #define FU_CAB_FIRMWARE_DECOMPRESS_BUFSZ 0x4000 /* bytes */ /** * fu_cab_firmware_get_compressed: * @self: a #FuCabFirmware * * Gets if the cabinet archive should be compressed. * * Returns: boolean * * Since: 1.9.7 **/ gboolean fu_cab_firmware_get_compressed(FuCabFirmware *self) { FuCabFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CAB_FIRMWARE(self), FALSE); return priv->compressed; } /** * fu_cab_firmware_set_compressed: * @self: a #FuCabFirmware * @compressed: boolean * * Sets if the cabinet archive should be compressed. * * Since: 1.9.7 **/ void fu_cab_firmware_set_compressed(FuCabFirmware *self, gboolean compressed) { FuCabFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CAB_FIRMWARE(self)); priv->compressed = compressed; } /** * fu_cab_firmware_get_only_basename: * @self: a #FuCabFirmware * * Gets if the cabinet archive filenames should have the path component removed. * * Returns: boolean * * Since: 1.9.7 **/ gboolean fu_cab_firmware_get_only_basename(FuCabFirmware *self) { FuCabFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CAB_FIRMWARE(self), FALSE); return priv->only_basename; } /** * fu_cab_firmware_set_only_basename: * @self: a #FuCabFirmware * @only_basename: boolean * * Sets if the cabinet archive filenames should have the path component removed. * * Since: 1.9.7 **/ void fu_cab_firmware_set_only_basename(FuCabFirmware *self, gboolean only_basename) { FuCabFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CAB_FIRMWARE(self)); priv->only_basename = only_basename; } typedef struct { GInputStream *stream; FuFirmwareParseFlags parse_flags; gsize rsvd_folder; gsize rsvd_block; gsize size_total; FuCabCompression compression; GPtrArray *folder_data; /* of FuCompositeInputStream */ z_stream zstrm; guint8 *decompress_buf; gsize decompress_bufsz; gsize ndatabsz; } FuCabFirmwareParseHelper; static void fu_cab_firmware_parse_helper_free(FuCabFirmwareParseHelper *helper) { inflateEnd(&helper->zstrm); if (helper->stream != NULL) g_object_unref(helper->stream); if (helper->folder_data != NULL) g_ptr_array_unref(helper->folder_data); g_free(helper->decompress_buf); g_free(helper); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuCabFirmwareParseHelper, fu_cab_firmware_parse_helper_free) /* compute the MS cabinet checksum */ gboolean fu_cab_firmware_compute_checksum(const guint8 *buf, gsize bufsz, guint32 *checksum, GError **error) { guint32 tmp = *checksum; for (gsize i = 0; i < bufsz; i += 4) { gsize chunksz = bufsz - i; if (G_LIKELY(chunksz >= 4)) { /* 3,2,1,0 */ tmp ^= ((guint32)buf[i + 3] << 24) | ((guint32)buf[i + 2] << 16) | ((guint32)buf[i + 1] << 8) | (guint32)buf[i + 0]; } else if (chunksz == 3) { /* 0,1,2 -- yes, weird */ tmp ^= ((guint32)buf[i + 0] << 16) | ((guint32)buf[i + 1] << 8) | (guint32)buf[i + 2]; } else if (chunksz == 2) { /* 0,1 -- yes, weird */ tmp ^= ((guint32)buf[i + 0] << 8) | (guint32)buf[i + 1]; } else { /* 0 */ tmp ^= (guint32)buf[i + 0]; } } *checksum = tmp; return TRUE; } static gboolean fu_cab_firmware_compute_checksum_stream_cb(const guint8 *buf, gsize bufsz, gpointer user_data, GError **error) { guint32 *checksum = (guint32 *)user_data; return fu_cab_firmware_compute_checksum(buf, bufsz, checksum, error); } static voidpf fu_cab_firmware_zalloc(voidpf opaque, uInt items, uInt size) { return g_malloc0_n(items, size); } static void fu_cab_firmware_zfree(voidpf opaque, voidpf address) { g_free(address); } typedef z_stream z_stream_deflater; static void fu_cab_firmware_zstream_deflater_free(z_stream_deflater *zstrm) { deflateEnd(zstrm); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(z_stream_deflater, fu_cab_firmware_zstream_deflater_free) static gboolean fu_cab_firmware_parse_data(FuCabFirmware *self, FuCabFirmwareParseHelper *helper, gsize *offset, GInputStream *folder_data, GError **error) { gsize blob_comp; gsize blob_uncomp; gsize hdr_sz; gsize size_max = fu_firmware_get_size_max(FU_FIRMWARE(self)); g_autoptr(FuStructCabData) st = NULL; g_autoptr(GInputStream) partial_stream = NULL; /* parse header */ st = fu_struct_cab_data_parse_stream(helper->stream, *offset, error); if (st == NULL) return FALSE; /* sanity check */ blob_comp = fu_struct_cab_data_get_comp(st); blob_uncomp = fu_struct_cab_data_get_uncomp(st); if (helper->compression == FU_CAB_COMPRESSION_NONE && blob_comp != blob_uncomp) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "mismatched compressed data"); return FALSE; } helper->size_total += blob_uncomp; if (size_max > 0 && helper->size_total > size_max) { g_autofree gchar *sz_val = g_format_size(helper->size_total); g_autofree gchar *sz_max = g_format_size(size_max); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "uncompressed data too large (%s, limit %s)", sz_val, sz_max); return FALSE; } hdr_sz = st->len + helper->rsvd_block; /* verify checksum */ partial_stream = fu_partial_input_stream_new(helper->stream, *offset + hdr_sz, blob_comp, error); if (partial_stream == NULL) { g_prefix_error(error, "failed to cut cabinet checksum: "); return FALSE; } if ((helper->parse_flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { guint32 checksum = fu_struct_cab_data_get_checksum(st); if (checksum != 0) { guint32 checksum_actual = 0; g_autoptr(GByteArray) hdr = g_byte_array_new(); if (!fu_input_stream_chunkify(partial_stream, fu_cab_firmware_compute_checksum_stream_cb, &checksum_actual, error)) return FALSE; fu_byte_array_append_uint16(hdr, blob_comp, G_LITTLE_ENDIAN); fu_byte_array_append_uint16(hdr, blob_uncomp, G_LITTLE_ENDIAN); if (!fu_cab_firmware_compute_checksum(hdr->data, hdr->len, &checksum_actual, error)) return FALSE; if (checksum_actual != checksum) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid checksum at 0x%x, expected 0x%x, got 0x%x", (guint)*offset, checksum, checksum_actual); return FALSE; } } } /* decompress Zlib data after removing *another *header... */ if (helper->compression == FU_CAB_COMPRESSION_MSZIP) { int zret; g_autofree gchar *kind = NULL; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GBytes) bytes_comp = NULL; g_autoptr(GBytes) bytes_uncomp = NULL; /* check compressed header */ bytes_comp = fu_input_stream_read_bytes(helper->stream, *offset + hdr_sz, blob_comp, NULL, error); if (bytes_comp == NULL) return FALSE; kind = fu_memstrsafe(g_bytes_get_data(bytes_comp, NULL), g_bytes_get_size(bytes_comp), 0x0, 2, error); if (kind == NULL) return FALSE; if (g_strcmp0(kind, "CK") != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "compressed header invalid: %s", kind); return FALSE; } if (helper->decompress_buf == NULL) helper->decompress_buf = g_malloc0(helper->decompress_bufsz); helper->zstrm.avail_in = g_bytes_get_size(bytes_comp) - 2; helper->zstrm.next_in = (z_const Bytef *)g_bytes_get_data(bytes_comp, NULL) + 2; while (1) { helper->zstrm.avail_out = helper->decompress_bufsz; helper->zstrm.next_out = helper->decompress_buf; zret = inflate(&helper->zstrm, Z_BLOCK); if (zret == Z_STREAM_END) break; g_byte_array_append(buf, helper->decompress_buf, helper->decompress_bufsz - helper->zstrm.avail_out); if (zret != Z_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "inflate error @0x%x: %s", (guint)*offset, zError(zret)); return FALSE; } } zret = inflateReset(&helper->zstrm); if (zret != Z_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to reset inflate: %s", zError(zret)); return FALSE; } zret = inflateSetDictionary(&helper->zstrm, buf->data, buf->len); if (zret != Z_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to set inflate dictionary: %s", zError(zret)); return FALSE; } bytes_uncomp = g_byte_array_free_to_bytes(g_steal_pointer(&buf)); /* nocheck:blocked */ fu_composite_input_stream_add_bytes(FU_COMPOSITE_INPUT_STREAM(folder_data), bytes_uncomp); } else { fu_composite_input_stream_add_partial_stream( FU_COMPOSITE_INPUT_STREAM(folder_data), FU_PARTIAL_INPUT_STREAM(partial_stream)); } /* success */ *offset += blob_comp + hdr_sz; return TRUE; } static gboolean fu_cab_firmware_parse_folder(FuCabFirmware *self, FuCabFirmwareParseHelper *helper, guint idx, gsize offset, GInputStream *folder_data, GError **error) { FuCabFirmwarePrivate *priv = GET_PRIVATE(self); g_autoptr(GByteArray) st = NULL; /* parse header */ st = fu_struct_cab_folder_parse_stream(helper->stream, offset, error); if (st == NULL) return FALSE; /* sanity check */ if (fu_struct_cab_folder_get_ndatab(st) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no CFDATA blocks"); return FALSE; } helper->compression = fu_struct_cab_folder_get_compression(st); if (helper->compression != FU_CAB_COMPRESSION_NONE) priv->compressed = TRUE; if (helper->compression != FU_CAB_COMPRESSION_NONE && helper->compression != FU_CAB_COMPRESSION_MSZIP) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "compression %s not supported", fu_cab_compression_to_string(helper->compression)); return FALSE; } /* parse CDATA, either using the stream offset or the per-spec FuStructCabFolder.ndatab */ if (helper->ndatabsz > 0) { for (gsize off = fu_struct_cab_folder_get_offset(st); off < helper->ndatabsz;) { if (!fu_cab_firmware_parse_data(self, helper, &off, folder_data, error)) return FALSE; } } else { gsize off = fu_struct_cab_folder_get_offset(st); for (guint16 i = 0; i < fu_struct_cab_folder_get_ndatab(st); i++) { if (!fu_cab_firmware_parse_data(self, helper, &off, folder_data, error)) return FALSE; } } /* success */ return TRUE; } static gboolean fu_cab_firmware_parse_file(FuCabFirmware *self, FuCabFirmwareParseHelper *helper, gsize *offset, GError **error) { FuCabFirmwarePrivate *priv = GET_PRIVATE(self); GInputStream *folder_data; guint16 date; guint16 index; guint16 time; g_autoptr(FuCabImage) img = fu_cab_image_new(); g_autoptr(GByteArray) st = NULL; g_autoptr(GDateTime) created = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(GString) filename = g_string_new(NULL); g_autoptr(GTimeZone) tz_utc = g_time_zone_new_utc(); /* parse header */ st = fu_struct_cab_file_parse_stream(helper->stream, *offset, error); if (st == NULL) return FALSE; fu_firmware_set_offset(FU_FIRMWARE(img), fu_struct_cab_file_get_uoffset(st)); fu_firmware_set_size(FU_FIRMWARE(img), fu_struct_cab_file_get_usize(st)); /* sanity check */ index = fu_struct_cab_file_get_index(st); if (index >= helper->folder_data->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to get folder data for 0x%x", index); return FALSE; } folder_data = g_ptr_array_index(helper->folder_data, index); /* parse filename */ *offset += FU_STRUCT_CAB_FILE_SIZE; for (guint i = 0; i < 255; i++) { guint8 value = 0; if (!fu_input_stream_read_u8(helper->stream, *offset + i, &value, error)) return FALSE; if (value == 0) break; if (!g_ascii_isprint((gchar)value)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "non-ASCII filenames are not supported: 0x%02x", value); return FALSE; } /* convert to UNIX path */ if (value == '\\') value = '/'; g_string_append_c(filename, (gchar)value); } /* add image */ if (priv->only_basename) { g_autofree gchar *id = g_path_get_basename(filename->str); fu_firmware_set_id(FU_FIRMWARE(img), id); } else { fu_firmware_set_id(FU_FIRMWARE(img), filename->str); } stream = fu_partial_input_stream_new(folder_data, fu_struct_cab_file_get_uoffset(st), fu_struct_cab_file_get_usize(st), error); if (stream == NULL) { g_prefix_error(error, "failed to cut cabinet image: "); return FALSE; } if (!fu_firmware_parse_stream(FU_FIRMWARE(img), stream, 0x0, helper->parse_flags, error)) return FALSE; if (!fu_firmware_add_image_full(FU_FIRMWARE(self), FU_FIRMWARE(img), error)) return FALSE; /* set created date time */ date = fu_struct_cab_file_get_date(st); time = fu_struct_cab_file_get_time(st); created = g_date_time_new(tz_utc, 1980 + ((date & 0xFE00) >> 9), (date & 0x01E0) >> 5, date & 0x001F, (time & 0xF800) >> 11, (time & 0x07E0) >> 5, (time & 0x001F) * 2); fu_cab_image_set_created(img, created); /* offset to next entry */ *offset += filename->len + 1; return TRUE; } static gboolean fu_cab_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_cab_header_validate_stream(stream, offset, error); } static FuCabFirmwareParseHelper * fu_cab_firmware_parse_helper_new(GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { int zret; g_autoptr(FuCabFirmwareParseHelper) helper = g_new0(FuCabFirmwareParseHelper, 1); /* zlib */ helper->zstrm.zalloc = fu_cab_firmware_zalloc; helper->zstrm.zfree = fu_cab_firmware_zfree; zret = inflateInit2(&helper->zstrm, -MAX_WBITS); if (zret != Z_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to initialize inflate: %s", zError(zret)); return NULL; } helper->stream = g_object_ref(stream); helper->parse_flags = flags; helper->folder_data = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); helper->decompress_bufsz = FU_CAB_FIRMWARE_DECOMPRESS_BUFSZ; return g_steal_pointer(&helper); } static gboolean fu_cab_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuCabFirmware *self = FU_CAB_FIRMWARE(firmware); gsize off_cffile = 0; gsize offset = 0; gsize streamsz = 0; g_autoptr(GByteArray) st = NULL; g_autoptr(FuCabFirmwareParseHelper) helper = NULL; /* get size */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; /* parse header */ st = fu_struct_cab_header_parse_stream(stream, offset, error); if (st == NULL) return FALSE; /* sanity checks */ if (fu_struct_cab_header_get_size(st) < streamsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "buffer size 0x%x is less than stream size 0x%x", (guint)streamsz, fu_struct_cab_header_get_size(st)); return FALSE; } if (fu_struct_cab_header_get_idx_cabinet(st) != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "chained archive not supported"); return FALSE; } if (fu_struct_cab_header_get_nr_folders(st) == 0 || fu_struct_cab_header_get_nr_files(st) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "archive is empty"); return FALSE; } if (fu_struct_cab_header_get_nr_folders(st) > FU_CAB_FIRMWARE_MAX_FOLDERS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "too many CFFOLDERS, parsed %u and limit was %u", fu_struct_cab_header_get_nr_folders(st), (guint)FU_CAB_FIRMWARE_MAX_FOLDERS); return FALSE; } if (fu_struct_cab_header_get_nr_files(st) > FU_CAB_FIRMWARE_MAX_FILES) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "too many CFFILES, parsed %u and limit was %u", fu_struct_cab_header_get_nr_files(st), (guint)FU_CAB_FIRMWARE_MAX_FILES); return FALSE; } off_cffile = fu_struct_cab_header_get_off_cffile(st); if (off_cffile > streamsz) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "archive is corrupt"); return FALSE; } /* create helper */ helper = fu_cab_firmware_parse_helper_new(stream, flags, error); if (helper == NULL) return FALSE; /* if the only folder is >= 2GB then FuStructCabFolder.ndatab will overflow */ if (streamsz >= 0x8000 * 0xFFFF && fu_struct_cab_header_get_nr_folders(st) == 1) helper->ndatabsz = streamsz; /* reserved sizes */ offset += st->len; if (fu_struct_cab_header_get_flags(st) & 0x0004) { g_autoptr(GByteArray) st2 = NULL; st2 = fu_struct_cab_header_reserve_parse_stream(stream, offset, error); if (st2 == NULL) return FALSE; offset += st2->len; offset += fu_struct_cab_header_reserve_get_rsvd_hdr(st2); helper->rsvd_block = fu_struct_cab_header_reserve_get_rsvd_block(st2); helper->rsvd_folder = fu_struct_cab_header_reserve_get_rsvd_folder(st2); } /* parse CFFOLDER */ for (guint i = 0; i < fu_struct_cab_header_get_nr_folders(st); i++) { g_autoptr(GInputStream) folder_data = fu_composite_input_stream_new(); if (!fu_cab_firmware_parse_folder(self, helper, i, offset, folder_data, error)) return FALSE; if (!fu_input_stream_size(folder_data, &streamsz, error)) return FALSE; if (streamsz == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no folder data"); return FALSE; } g_ptr_array_add(helper->folder_data, g_steal_pointer(&folder_data)); offset += FU_STRUCT_CAB_FOLDER_SIZE + helper->rsvd_folder; } /* parse CFFILEs */ for (guint i = 0; i < fu_struct_cab_header_get_nr_files(st); i++) { if (!fu_cab_firmware_parse_file(self, helper, &off_cffile, error)) return FALSE; } /* success */ return TRUE; } static GByteArray * fu_cab_firmware_write(FuFirmware *firmware, GError **error) { FuCabFirmware *self = FU_CAB_FIRMWARE(firmware); FuCabFirmwarePrivate *priv = GET_PRIVATE(self); gsize archive_size; gsize offset; guint32 index_into = 0; g_autoptr(GByteArray) st_hdr = fu_struct_cab_header_new(); g_autoptr(GByteArray) st_folder = fu_struct_cab_folder_new(); g_autoptr(GPtrArray) imgs = fu_firmware_get_images(firmware); g_autoptr(GByteArray) cfdata_linear = g_byte_array_new(); g_autoptr(GBytes) cfdata_linear_blob = NULL; g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(GPtrArray) chunks_zlib = g_ptr_array_new_with_free_func((GDestroyNotify)g_byte_array_unref); /* create linear CFDATA block */ for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); const gchar *filename_win32 = fu_cab_image_get_win32_filename(FU_CAB_IMAGE(img)); g_autoptr(GBytes) img_blob = NULL; if (filename_win32 == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no image filename"); return NULL; } img_blob = fu_firmware_get_bytes(img, error); if (img_blob == NULL) return NULL; fu_byte_array_append_bytes(cfdata_linear, img_blob); } /* chunkify and compress with a fixed size */ if (cfdata_linear->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no data to compress"); return NULL; } cfdata_linear_blob = g_byte_array_free_to_bytes(g_steal_pointer(&cfdata_linear)); /* nocheck:blocked */ chunks = fu_chunk_array_new_from_bytes(cfdata_linear_blob, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 0x8000); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GByteArray) chunk_zlib = g_byte_array_new(); g_autoptr(GByteArray) buf = g_byte_array_new(); chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return NULL; fu_byte_array_set_size(chunk_zlib, fu_chunk_get_data_sz(chk) * 2, 0x0); if (priv->compressed) { int zret; z_stream zstrm = { .zalloc = fu_cab_firmware_zalloc, .zfree = fu_cab_firmware_zfree, .opaque = Z_NULL, .next_in = (guint8 *)fu_chunk_get_data(chk), .avail_in = fu_chunk_get_data_sz(chk), .next_out = chunk_zlib->data, .avail_out = chunk_zlib->len, }; g_autoptr(z_stream_deflater) zstrm_deflater = &zstrm; zret = deflateInit2(zstrm_deflater, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -15, 8, Z_DEFAULT_STRATEGY); if (zret != Z_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to initialize deflate: %s", zError(zret)); return NULL; } zret = deflate(zstrm_deflater, Z_FINISH); if (zret != Z_OK && zret != Z_STREAM_END) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "zlib deflate failed: %s", zError(zret)); return NULL; } fu_byte_array_append_uint8(buf, (guint8)'C'); fu_byte_array_append_uint8(buf, (guint8)'K'); g_byte_array_append(buf, chunk_zlib->data, zstrm.total_out); } else { g_byte_array_append(buf, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); } g_ptr_array_add(chunks_zlib, g_steal_pointer(&buf)); } /* create header */ archive_size = FU_STRUCT_CAB_HEADER_SIZE; archive_size += FU_STRUCT_CAB_FOLDER_SIZE; for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); const gchar *filename_win32 = fu_cab_image_get_win32_filename(FU_CAB_IMAGE(img)); archive_size += FU_STRUCT_CAB_FILE_SIZE + strlen(filename_win32) + 1; } for (guint i = 0; i < chunks_zlib->len; i++) { GByteArray *chunk = g_ptr_array_index(chunks_zlib, i); archive_size += FU_STRUCT_CAB_DATA_SIZE + chunk->len; } offset = FU_STRUCT_CAB_HEADER_SIZE; offset += FU_STRUCT_CAB_FOLDER_SIZE; fu_struct_cab_header_set_size(st_hdr, archive_size); fu_struct_cab_header_set_off_cffile(st_hdr, offset); fu_struct_cab_header_set_nr_files(st_hdr, imgs->len); /* create folder */ for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); const gchar *filename_win32 = fu_cab_image_get_win32_filename(FU_CAB_IMAGE(img)); offset += FU_STRUCT_CAB_FILE_SIZE; offset += strlen(filename_win32) + 1; } fu_struct_cab_folder_set_offset(st_folder, offset); fu_struct_cab_folder_set_ndatab(st_folder, fu_chunk_array_length(chunks)); fu_struct_cab_folder_set_compression(st_folder, priv->compressed ? FU_CAB_COMPRESSION_MSZIP : FU_CAB_COMPRESSION_NONE); g_byte_array_append(st_hdr, st_folder->data, st_folder->len); /* create each CFFILE */ for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); FuCabFileAttribute fattr = FU_CAB_FILE_ATTRIBUTE_NONE; GDateTime *created = fu_cab_image_get_created(FU_CAB_IMAGE(img)); const gchar *filename_win32 = fu_cab_image_get_win32_filename(FU_CAB_IMAGE(img)); g_autoptr(GByteArray) st_file = fu_struct_cab_file_new(); g_autoptr(GBytes) img_blob = fu_firmware_get_bytes(img, NULL); if (!g_str_is_ascii(filename_win32)) fattr |= FU_CAB_FILE_ATTRIBUTE_NAME_UTF8; fu_struct_cab_file_set_fattr(st_file, fattr); fu_struct_cab_file_set_usize(st_file, g_bytes_get_size(img_blob)); fu_struct_cab_file_set_uoffset(st_file, index_into); if (created != NULL) { fu_struct_cab_file_set_date(st_file, ((g_date_time_get_year(created) - 1980) << 9) + (g_date_time_get_month(created) << 5) + g_date_time_get_day_of_month(created)); fu_struct_cab_file_set_time(st_file, (g_date_time_get_hour(created) << 11) + (g_date_time_get_minute(created) << 5) + (g_date_time_get_second(created) / 2)); } g_byte_array_append(st_hdr, st_file->data, st_file->len); g_byte_array_append(st_hdr, (const guint8 *)filename_win32, strlen(filename_win32)); fu_byte_array_append_uint8(st_hdr, 0x0); index_into += g_bytes_get_size(img_blob); } /* create each CFDATA */ for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { guint32 checksum = 0; GByteArray *chunk_zlib = g_ptr_array_index(chunks_zlib, i); g_autoptr(FuChunk) chk = NULL; g_autoptr(GByteArray) hdr = g_byte_array_new(); g_autoptr(GByteArray) st_data = fu_struct_cab_data_new(); /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return NULL; /* first do the 'checksum' on the data, then the partial header -- slightly crazy */ if (!fu_cab_firmware_compute_checksum(chunk_zlib->data, chunk_zlib->len, &checksum, error)) return NULL; fu_byte_array_append_uint16(hdr, chunk_zlib->len, G_LITTLE_ENDIAN); fu_byte_array_append_uint16(hdr, fu_chunk_get_data_sz(chk), G_LITTLE_ENDIAN); if (!fu_cab_firmware_compute_checksum(hdr->data, hdr->len, &checksum, error)) return NULL; fu_struct_cab_data_set_checksum(st_data, checksum); fu_struct_cab_data_set_comp(st_data, chunk_zlib->len); fu_struct_cab_data_set_uncomp(st_data, fu_chunk_get_data_sz(chk)); g_byte_array_append(st_hdr, st_data->data, st_data->len); g_byte_array_append(st_hdr, chunk_zlib->data, chunk_zlib->len); } /* success */ return g_steal_pointer(&st_hdr); } static gboolean fu_cab_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { FuCabFirmware *self = FU_CAB_FIRMWARE(firmware); FuCabFirmwarePrivate *priv = GET_PRIVATE(self); const gchar *tmp; /* simple properties */ tmp = xb_node_query_text(n, "compressed", NULL); if (tmp != NULL) { if (!fu_strtobool(tmp, &priv->compressed, error)) return FALSE; } tmp = xb_node_query_text(n, "only_basename", NULL); if (tmp != NULL) { if (!fu_strtobool(tmp, &priv->only_basename, error)) return FALSE; } /* success */ return TRUE; } static void fu_cab_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuCabFirmware *self = FU_CAB_FIRMWARE(firmware); FuCabFirmwarePrivate *priv = GET_PRIVATE(self); fu_xmlb_builder_insert_kb(bn, "compressed", priv->compressed); fu_xmlb_builder_insert_kb(bn, "only_basename", priv->only_basename); } static void fu_cab_firmware_class_init(FuCabFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_cab_firmware_validate; firmware_class->parse = fu_cab_firmware_parse; firmware_class->write = fu_cab_firmware_write; firmware_class->build = fu_cab_firmware_build; firmware_class->export = fu_cab_firmware_export; } static void fu_cab_firmware_init(FuCabFirmware *self) { g_type_ensure(FU_TYPE_CAB_IMAGE); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_STORED_SIZE); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_DEDUPE_ID); fu_firmware_set_images_max(FU_FIRMWARE(self), G_MAXUINT16); } /** * fu_cab_firmware_new: * * Returns: (transfer full): a #FuCabFirmware * * Since: 1.9.7 **/ FuCabFirmware * fu_cab_firmware_new(void) { return g_object_new(FU_TYPE_CAB_FIRMWARE, NULL); } fwupd-2.0.10/libfwupdplugin/fu-cab-firmware.h000066400000000000000000000014401501337203100210640ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_CAB_FIRMWARE (fu_cab_firmware_get_type()) G_DECLARE_DERIVABLE_TYPE(FuCabFirmware, fu_cab_firmware, FU, CAB_FIRMWARE, FuFirmware) struct _FuCabFirmwareClass { FuFirmwareClass parent_class; }; gboolean fu_cab_firmware_get_compressed(FuCabFirmware *self) G_GNUC_NON_NULL(1); void fu_cab_firmware_set_compressed(FuCabFirmware *self, gboolean compressed) G_GNUC_NON_NULL(1); gboolean fu_cab_firmware_get_only_basename(FuCabFirmware *self) G_GNUC_NON_NULL(1); void fu_cab_firmware_set_only_basename(FuCabFirmware *self, gboolean only_basename) G_GNUC_NON_NULL(1); FuCabFirmware * fu_cab_firmware_new(void) G_GNUC_WARN_UNUSED_RESULT; fwupd-2.0.10/libfwupdplugin/fu-cab-image.c000066400000000000000000000077621501337203100203420ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuCabFirmware" #include "config.h" #include "fu-cab-image.h" #include "fu-common.h" #include "fu-string.h" struct _FuCabImage { FuFirmware parent_instance; gchar *win32_filename; GDateTime *created; }; G_DEFINE_TYPE(FuCabImage, fu_cab_image, FU_TYPE_FIRMWARE) /** * fu_cab_image_get_win32_filename: * @self: a #FuCabImage * * Gets the in-archive Windows filename, with a possible path component -- creating from the * firmware ID if required. * * Returns: filename, or %NULL * * Since: 1.9.7 **/ const gchar * fu_cab_image_get_win32_filename(FuCabImage *self) { g_return_val_if_fail(FU_IS_CAB_IMAGE(self), NULL); /* generate from the id */ if (self->win32_filename == NULL) { g_autoptr(GString) str = g_string_new(fu_firmware_get_id(FU_FIRMWARE(self))); g_string_replace(str, "/", "\\", 0); if (str->len == 0) return NULL; fu_cab_image_set_win32_filename(self, str->str); } return self->win32_filename; } /** * fu_cab_image_set_win32_filename: * @self: a #FuCabImage * @win32_filename: filename, or %NULL * * Sets the in-archive Windows filename, with a possible path component. * * Since: 1.9.7 **/ void fu_cab_image_set_win32_filename(FuCabImage *self, const gchar *win32_filename) { g_return_if_fail(FU_IS_CAB_IMAGE(self)); g_free(self->win32_filename); self->win32_filename = g_strdup(win32_filename); } /** * fu_cab_image_get_created: * @self: a #FuCabImage * * Gets the created timestamp. * * Returns: (transfer none): a #GDateTime, or %NULL * * Since: 1.9.7 **/ GDateTime * fu_cab_image_get_created(FuCabImage *self) { g_return_val_if_fail(FU_IS_CAB_IMAGE(self), NULL); return self->created; } /** * fu_cab_image_set_created: * @self: a #FuCabImage * @created: a #GDateTime, or %NULL * * Sets the created timestamp. * * Since: 1.9.7 **/ void fu_cab_image_set_created(FuCabImage *self, GDateTime *created) { g_return_if_fail(FU_IS_CAB_IMAGE(self)); if (self->created != NULL) { g_date_time_unref(self->created); self->created = NULL; } if (created != NULL) self->created = g_date_time_ref(created); } static gboolean fu_cab_image_build(FuFirmware *firmware, XbNode *n, GError **error) { FuCabImage *self = FU_CAB_IMAGE(firmware); const gchar *tmp; /* simple properties */ tmp = xb_node_query_text(n, "win32_filename", NULL); if (tmp != NULL) fu_cab_image_set_win32_filename(self, tmp); tmp = xb_node_query_text(n, "created", NULL); if (tmp != NULL) { g_autoptr(GDateTime) created = g_date_time_new_from_iso8601(tmp, NULL); if (created == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not iso8601: %s", tmp); return FALSE; } fu_cab_image_set_created(self, created); } /* success */ return TRUE; } static void fu_cab_image_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuCabImage *self = FU_CAB_IMAGE(firmware); fu_xmlb_builder_insert_kv(bn, "win32_filename", self->win32_filename); if (self->created != NULL) { g_autofree gchar *str = g_date_time_format_iso8601(self->created); fu_xmlb_builder_insert_kv(bn, "created", str); } } static void fu_cab_image_finalize(GObject *object) { FuCabImage *self = FU_CAB_IMAGE(object); g_free(self->win32_filename); if (self->created != NULL) g_date_time_unref(self->created); G_OBJECT_CLASS(fu_cab_image_parent_class)->finalize(object); } static void fu_cab_image_class_init(FuCabImageClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_cab_image_finalize; firmware_class->build = fu_cab_image_build; firmware_class->export = fu_cab_image_export; } static void fu_cab_image_init(FuCabImage *self) { } /** * fu_cab_image_new: * * Returns: (transfer full): a #FuCabImage * * Since: 1.9.7 **/ FuCabImage * fu_cab_image_new(void) { return g_object_new(FU_TYPE_CAB_IMAGE, NULL); } fwupd-2.0.10/libfwupdplugin/fu-cab-image.h000066400000000000000000000012671501337203100203410ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_CAB_IMAGE (fu_cab_image_get_type()) G_DECLARE_FINAL_TYPE(FuCabImage, fu_cab_image, FU, CAB_IMAGE, FuFirmware) const gchar * fu_cab_image_get_win32_filename(FuCabImage *self) G_GNUC_NON_NULL(1); void fu_cab_image_set_win32_filename(FuCabImage *self, const gchar *win32_filename) G_GNUC_NON_NULL(1); GDateTime * fu_cab_image_get_created(FuCabImage *self) G_GNUC_NON_NULL(1); void fu_cab_image_set_created(FuCabImage *self, GDateTime *created) G_GNUC_NON_NULL(1); FuCabImage * fu_cab_image_new(void) G_GNUC_WARN_UNUSED_RESULT; fwupd-2.0.10/libfwupdplugin/fu-cab.rs000066400000000000000000000027751501337203100174630ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ParseStream, New)] #[repr(C, packed)] struct FuStructCabData { checksum: u32le, comp: u16le, uncomp: u16le, } #[repr(u16le)] #[derive(ToString)] enum FuCabCompression { None = 0x0000, Mszip = 0x0001, Quantum = 0x0002, Lzx = 0x0003, } #[repr(u16le)] enum FuCabFileAttribute { None = 0x00, Readonly = 0x01, Hidden = 0x02, System = 0x04, Arch = 0x20, Exec = 0x40, NameUtf8 = 0x80, } #[derive(ParseStream, New)] #[repr(C, packed)] struct FuStructCabFile { usize: u32le, // uncompressed uoffset: u32le, // uncompressed index: u16le, date: u16le, time: u16le, fattr: FuCabFileAttribute, } #[derive(ParseStream, New)] #[repr(C, packed)] struct FuStructCabFolder { offset: u32le, ndatab: u16le, compression: FuCabCompression, } #[derive(ParseStream, ValidateStream, New, Default)] #[repr(C, packed)] struct FuStructCabHeader { signature: [char; 4] == "MSCF", _reserved1: [u8; 4], size: u32le, // in bytes _reserved2: [u8; 4], off_cffile: u32le, // to the first CabFile entry _reserved3: [u8; 4], version_minor: u8 == 3, version_major: u8 == 1, nr_folders: u16le = 1, nr_files: u16le, flags: u16le, set_id: u16le, idx_cabinet: u16le, } #[derive(ParseStream, New)] #[repr(C, packed)] struct FuStructCabHeaderReserve { rsvd_hdr: u16le, rsvd_folder: u8, rsvd_block: u8, } fwupd-2.0.10/libfwupdplugin/fu-cfi-device.c000066400000000000000000000674251501337203100205350ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuCfiDevice" #include "config.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-cfi-device.h" #include "fu-chunk-array.h" #include "fu-dump.h" #include "fu-mem.h" #include "fu-quirks.h" #include "fu-string.h" /** * FuCfiDevice: * * A chip conforming to the Common Flash Memory Interface, typically a SPI flash chip. * * Where required, the quirks instance IDs will be added in ->setup(). * * The defaults are set as follows, and can be overridden in quirk files: * * * `PageSize`: 0x100 * * `SectorSize`: 0x1000 * * `BlockSize`: 0x10000 * * See also: [class@FuDevice] */ typedef struct { gchar *flash_id; guint8 cmd_read_id_sz; guint32 page_size; guint32 sector_size; guint32 block_size; FuCfiDeviceCmd cmds[FU_CFI_DEVICE_CMD_LAST]; } FuCfiDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuCfiDevice, fu_cfi_device, FU_TYPE_DEVICE) enum { PROP_0, PROP_FLASH_ID, PROP_LAST }; #define GET_PRIVATE(o) (fu_cfi_device_get_instance_private(o)) #define FU_CFI_DEVICE_PAGE_SIZE_DEFAULT 0x100 #define FU_CFI_DEVICE_SECTOR_SIZE_DEFAULT 0x1000 #define FU_CFI_DEVICE_BLOCK_SIZE_DEFAULT 0x10000 /** * fu_cfi_device_get_size: * @self: a #FuCfiDevice * * Gets the chip maximum size. * * This is typically set with the `FirmwareSizeMax` quirk key. * * Returns: size in bytes, or 0 if unknown * * Since: 1.7.1 **/ guint64 fu_cfi_device_get_size(FuCfiDevice *self) { g_return_val_if_fail(FU_IS_CFI_DEVICE(self), G_MAXUINT64); return fu_device_get_firmware_size_max(FU_DEVICE(self)); } /** * fu_cfi_device_set_size: * @self: a #FuCfiDevice * @size: maximum size in bytes, or 0 if unknown * * Sets the chip maximum size. * * Since: 1.7.1 **/ void fu_cfi_device_set_size(FuCfiDevice *self, guint64 size) { g_return_if_fail(FU_IS_CFI_DEVICE(self)); fu_device_set_firmware_size_max(FU_DEVICE(self), size); } /** * fu_cfi_device_get_flash_id: * @self: a #FuCfiDevice * * Gets the chip ID used to identify the device. * * Returns: the ID, or %NULL * * Since: 1.7.1 **/ const gchar * fu_cfi_device_get_flash_id(FuCfiDevice *self) { FuCfiDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CFI_DEVICE(self), NULL); return priv->flash_id; } /** * fu_cfi_device_set_flash_id: * @self: a #FuCfiDevice * @flash_id: (nullable): The chip ID * * Sets the chip ID used to identify the device. * * Since: 1.7.1 **/ void fu_cfi_device_set_flash_id(FuCfiDevice *self, const gchar *flash_id) { FuCfiDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CFI_DEVICE(self)); if (g_strcmp0(flash_id, priv->flash_id) == 0) return; g_free(priv->flash_id); priv->flash_id = g_strdup(flash_id); } static void fu_cfi_device_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuCfiDevice *self = FU_CFI_DEVICE(object); FuCfiDevicePrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_FLASH_ID: g_value_set_object(value, priv->flash_id); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_cfi_device_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuCfiDevice *self = FU_CFI_DEVICE(object); switch (prop_id) { case PROP_FLASH_ID: fu_cfi_device_set_flash_id(self, g_value_get_string(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_cfi_device_finalize(GObject *object) { FuCfiDevice *self = FU_CFI_DEVICE(object); FuCfiDevicePrivate *priv = GET_PRIVATE(self); g_free(priv->flash_id); G_OBJECT_CLASS(fu_cfi_device_parent_class)->finalize(object); } typedef struct { guint8 mask; guint8 value; } FuCfiDeviceHelper; static gboolean fu_cfi_device_wait_for_status_cb(FuDevice *device, gpointer user_data, GError **error) { FuCfiDeviceHelper *helper = (FuCfiDeviceHelper *)user_data; FuCfiDevice *self = FU_CFI_DEVICE(device); guint8 buf[2] = {0x0}; g_autoptr(FuDeviceLocker) cslocker = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); /* enable chip */ cslocker = fu_cfi_device_chip_select_locker_new(self, error); if (cslocker == NULL) return FALSE; if (!fu_cfi_device_get_cmd(self, FU_CFI_DEVICE_CMD_READ_STATUS, &buf[0], error)) return FALSE; if (!fu_cfi_device_send_command(self, buf, sizeof(buf), buf, sizeof(buf), progress, error)) { g_prefix_error(error, "failed to want to status: "); return FALSE; } if ((buf[0x1] & helper->mask) != helper->value) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "wanted 0x%x, got 0x%x", helper->value, (guint)(buf[0x1] & helper->mask)); return FALSE; } /* success */ return TRUE; } static gboolean fu_cfi_device_wait_for_status(FuCfiDevice *self, guint8 mask, guint8 value, guint count, guint delay, GError **error) { FuCfiDeviceHelper helper = {.mask = mask, .value = value}; return fu_device_retry_full(FU_DEVICE(self), fu_cfi_device_wait_for_status_cb, count, delay, &helper, error); } static gboolean fu_cfi_device_read_jedec(FuCfiDevice *self, GError **error) { FuCfiDeviceClass *klass = FU_CFI_DEVICE_GET_CLASS(self); guint8 buf_res[] = {0x9F}; guint8 buf_req[3] = {0x0}; g_autoptr(FuDeviceLocker) cslocker = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GString) flash_id = g_string_new(NULL); /* has to be handled by the baseclass */ if (klass->read_jedec != NULL) return klass->read_jedec(self, error); /* enable chip */ cslocker = fu_cfi_device_chip_select_locker_new(self, error); if (cslocker == NULL) return FALSE; /* read JEDEC ID */ if (!fu_cfi_device_send_command(self, buf_res, sizeof(buf_res), buf_req, sizeof(buf_req), progress, error)) { g_prefix_error(error, "failed to request JEDEC ID: "); return FALSE; } if ((buf_req[0] == 0x0 && buf_req[1] == 0x0 && buf_req[2] == 0x0) || (buf_req[0] == 0xFF && buf_req[1] == 0xFF && buf_req[2] == 0xFF)) { fu_cfi_device_set_flash_id(self, NULL); return TRUE; } g_string_append_printf(flash_id, "%02X", buf_req[0]); g_string_append_printf(flash_id, "%02X", buf_req[1]); g_string_append_printf(flash_id, "%02X", buf_req[2]); fu_cfi_device_set_flash_id(self, flash_id->str); /* success */ return TRUE; } static gboolean fu_cfi_device_setup(FuDevice *device, GError **error) { gsize flash_idsz = 0; FuCfiDevice *self = FU_CFI_DEVICE(device); FuCfiDevicePrivate *priv = GET_PRIVATE(self); /* setup SPI chip */ if (priv->flash_id == NULL) { if (!fu_cfi_device_read_jedec(self, error)) return FALSE; } if (priv->flash_id == NULL) return TRUE; /* sanity check */ if (priv->flash_id != NULL) flash_idsz = strlen(priv->flash_id); if (flash_idsz == 0 || flash_idsz % 2 != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not a valid flash-id"); return FALSE; } /* typically this will add quirk strings of 2, 4, then 6 bytes */ for (guint i = 0; i < flash_idsz; i += 2) { g_autofree gchar *flash_id = g_strndup(priv->flash_id, i + 2); fu_device_add_instance_str(device, "FLASHID", flash_id); if (!fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_QUIRKS, error, "CFI", "FLASHID", NULL)) return FALSE; } /* success */ fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); return TRUE; } /** * fu_cfi_device_get_cmd: * @self: a #FuCfiDevice * @cmd: a #FuCfiDeviceCmd, e.g. %FU_CFI_DEVICE_CMD_CHIP_ERASE * @value: the API command value to use * @error: (nullable): optional return location for an error * * Gets the self vendor code. * * Returns: %TRUE on success * * Since: 1.7.1 **/ gboolean fu_cfi_device_get_cmd(FuCfiDevice *self, FuCfiDeviceCmd cmd, guint8 *value, GError **error) { FuCfiDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CFI_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (cmd >= FU_CFI_DEVICE_CMD_LAST) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "CFI cmd invalid"); return FALSE; } if (priv->cmds[cmd] == 0x0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "No defined CFI cmd for %s", fu_cfi_device_cmd_to_string(cmd)); return FALSE; } if (value != NULL) *value = priv->cmds[cmd]; return TRUE; } /** * fu_cfi_device_get_page_size: * @self: a #FuCfiDevice * * Gets the chip page size. This is typically the largest writable block size. * * This is typically set with the `CfiDevicePageSize` quirk key. * * Returns: page size in bytes * * Since: 1.7.3 **/ guint32 fu_cfi_device_get_page_size(FuCfiDevice *self) { FuCfiDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CFI_DEVICE(self), G_MAXUINT32); return priv->page_size; } /** * fu_cfi_device_set_page_size: * @self: a #FuCfiDevice * @page_size: page size in bytes, or 0 if unknown * * Sets the chip page size. This is typically the largest writable block size. * * Since: 1.7.3 **/ void fu_cfi_device_set_page_size(FuCfiDevice *self, guint32 page_size) { FuCfiDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CFI_DEVICE(self)); priv->page_size = page_size; } /** * fu_cfi_device_get_sector_size: * @self: a #FuCfiDevice * * Gets the chip sector size. This is typically the smallest erasable page size. * * This is typically set with the `CfiDeviceSectorSize` quirk key. * * Returns: sector size in bytes * * Since: 1.7.3 **/ guint32 fu_cfi_device_get_sector_size(FuCfiDevice *self) { FuCfiDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CFI_DEVICE(self), G_MAXUINT32); return priv->sector_size; } /** * fu_cfi_device_set_block_size: * @self: a #FuCfiDevice * @block_size: block size in bytes, or 0 if unknown * * Sets the chip block size. This is typically the largest erasable chunk size. * * Since: 1.7.4 **/ void fu_cfi_device_set_block_size(FuCfiDevice *self, guint32 block_size) { FuCfiDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CFI_DEVICE(self)); priv->block_size = block_size; } /** * fu_cfi_device_get_block_size: * @self: a #FuCfiDevice * * Gets the chip block size. This is typically the largest erasable block size. * * This is typically set with the `CfiDeviceBlockSize` quirk key. * * Returns: block size in bytes * * Since: 1.7.4 **/ guint32 fu_cfi_device_get_block_size(FuCfiDevice *self) { FuCfiDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CFI_DEVICE(self), G_MAXUINT32); return priv->block_size; } /** * fu_cfi_device_set_sector_size: * @self: a #FuCfiDevice * @sector_size: sector size in bytes, or 0 if unknown * * Sets the chip sector size. This is typically the smallest erasable page size. * * Since: 1.7.3 **/ void fu_cfi_device_set_sector_size(FuCfiDevice *self, guint32 sector_size) { FuCfiDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CFI_DEVICE(self)); priv->sector_size = sector_size; } static gboolean fu_cfi_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuCfiDevice *self = FU_CFI_DEVICE(device); FuCfiDevicePrivate *priv = GET_PRIVATE(self); guint64 tmp; if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_CMD_READ_ID) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->cmds[FU_CFI_DEVICE_CMD_READ_ID] = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_CMD_READ_ID_SZ) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->cmd_read_id_sz = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_CMD_CHIP_ERASE) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->cmds[FU_CFI_DEVICE_CMD_CHIP_ERASE] = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_CMD_BLOCK_ERASE) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->cmds[FU_CFI_DEVICE_CMD_BLOCK_ERASE] = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_CMD_SECTOR_ERASE) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->cmds[FU_CFI_DEVICE_CMD_SECTOR_ERASE] = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_CMD_WRITE_STATUS) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->cmds[FU_CFI_DEVICE_CMD_WRITE_STATUS] = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_CMD_PAGE_PROG) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->cmds[FU_CFI_DEVICE_CMD_PAGE_PROG] = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_CMD_READ_DATA) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->cmds[FU_CFI_DEVICE_CMD_READ_DATA] = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_CMD_READ_STATUS) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->cmds[FU_CFI_DEVICE_CMD_READ_STATUS] = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_CMD_WRITE_EN) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->cmds[FU_CFI_DEVICE_CMD_WRITE_EN] = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_PAGE_SIZE) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->page_size = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_SECTOR_SIZE) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->sector_size = tmp; return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CFI_DEVICE_BLOCK_SIZE) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->block_size = tmp; return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_cfi_device_to_string(FuDevice *device, guint idt, GString *str) { FuCfiDevice *self = FU_CFI_DEVICE(device); FuCfiDevicePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append(str, idt, "FlashId", priv->flash_id); for (guint i = 0; i < FU_CFI_DEVICE_CMD_LAST; i++) { fwupd_codec_string_append_hex(str, idt, fu_cfi_device_cmd_to_string(i), priv->cmds[i]); } fwupd_codec_string_append_hex(str, idt, "PageSize", priv->page_size); fwupd_codec_string_append_hex(str, idt, "SectorSize", priv->sector_size); fwupd_codec_string_append_hex(str, idt, "BlockSize", priv->block_size); } /** * fu_cfi_device_send_command: * @self: a #FuCfiDevice * @wbuf: buffer * @wbufsz: size of @wbuf, possibly zero * @rbuf: buffer * @rbufsz: size of @rbuf, possibly zero * @progress: a #FuProgress * @error: (nullable): optional return location for an error * * Sends an unspecified command stream to the CFI device. * * Returns: %TRUE on success * * Since: 1.9.1 **/ gboolean fu_cfi_device_send_command(FuCfiDevice *self, const guint8 *wbuf, gsize wbufsz, guint8 *rbuf, gsize rbufsz, FuProgress *progress, GError **error) { FuCfiDeviceClass *klass = FU_CFI_DEVICE_GET_CLASS(self); g_return_val_if_fail(FU_IS_CFI_DEVICE(self), FALSE); g_return_val_if_fail(FU_IS_PROGRESS(progress), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (klass->send_command == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "send_command is not implemented on this device"); return FALSE; } if (wbufsz > 0) fu_dump_raw(G_LOG_DOMAIN, "SPI write", wbuf, wbufsz); if (!klass->send_command(self, wbuf, wbufsz, rbuf, rbufsz, progress, error)) return FALSE; if (rbufsz > 0) fu_dump_raw(G_LOG_DOMAIN, "SPI read", rbuf, rbufsz); return TRUE; } /** * fu_cfi_device_chip_select: * @self: a #FuCfiDevice * @value: boolean * @error: (nullable): optional return location for an error * * Sets the chip select value. * * Returns: %TRUE on success * * Since: 1.8.0 **/ gboolean fu_cfi_device_chip_select(FuCfiDevice *self, gboolean value, GError **error) { FuCfiDeviceClass *klass = FU_CFI_DEVICE_GET_CLASS(self); g_return_val_if_fail(FU_IS_CFI_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (klass->chip_select == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "chip select is not implemented on this device"); return FALSE; } return klass->chip_select(self, value, error); } static gboolean fu_cfi_device_chip_select_assert(GObject *device, GError **error) { return fu_cfi_device_chip_select(FU_CFI_DEVICE(device), TRUE, error); } static gboolean fu_cfi_device_chip_select_deassert(GObject *device, GError **error) { return fu_cfi_device_chip_select(FU_CFI_DEVICE(device), FALSE, error); } /** * fu_cfi_device_chip_select_locker_new: * @self: a #FuCfiDevice * * Creates a custom device locker that asserts and deasserts the chip select signal. * * Returns: (transfer full): (nullable): a #FuDeviceLocker * * Since: 1.8.0 **/ FuDeviceLocker * fu_cfi_device_chip_select_locker_new(FuCfiDevice *self, GError **error) { g_return_val_if_fail(FU_IS_CFI_DEVICE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return fu_device_locker_new_full(self, fu_cfi_device_chip_select_assert, fu_cfi_device_chip_select_deassert, error); } static gboolean fu_cfi_device_write_enable(FuCfiDevice *self, GError **error) { guint8 buf[1] = {0x0}; g_autoptr(FuDeviceLocker) cslocker = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); /* write enable */ if (!fu_cfi_device_get_cmd(self, FU_CFI_DEVICE_CMD_WRITE_EN, &buf[0], error)) return FALSE; cslocker = fu_cfi_device_chip_select_locker_new(self, error); if (cslocker == NULL) return FALSE; if (!fu_cfi_device_send_command(self, buf, sizeof(buf), NULL, 0, progress, error)) return FALSE; if (!fu_device_locker_close(cslocker, error)) return FALSE; /* check that WEL is now set */ return fu_cfi_device_wait_for_status(self, 0b10, 0b10, 10, 5, error); } static gboolean fu_cfi_device_chip_erase(FuCfiDevice *self, GError **error) { guint8 buf[] = {0x0}; g_autoptr(FuDeviceLocker) cslocker = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); /* enable chip */ cslocker = fu_cfi_device_chip_select_locker_new(self, error); if (cslocker == NULL) return FALSE; /* erase */ if (!fu_cfi_device_get_cmd(self, FU_CFI_DEVICE_CMD_CHIP_ERASE, &buf[0], error)) return FALSE; if (!fu_cfi_device_send_command(self, buf, sizeof(buf), NULL, 0, progress, error)) return FALSE; if (!fu_device_locker_close(cslocker, error)) return FALSE; /* poll Read Status register BUSY */ return fu_cfi_device_wait_for_status(self, 0b1, 0b0, 100, 500, error); } static gboolean fu_cfi_device_write_page(FuCfiDevice *self, FuChunk *page, FuProgress *progress, GError **error) { guint8 cmd = 0x0; g_autoptr(FuDeviceLocker) cslocker = NULL; g_autoptr(GByteArray) buf = g_byte_array_new(); if (!fu_cfi_device_write_enable(self, error)) return FALSE; cslocker = fu_cfi_device_chip_select_locker_new(self, error); if (cslocker == NULL) return FALSE; /* cmd, 24 bit starting address, then data */ if (!fu_cfi_device_get_cmd(self, FU_CFI_DEVICE_CMD_PAGE_PROG, &cmd, error)) return FALSE; fu_byte_array_append_uint8(buf, cmd); fu_byte_array_append_uint24(buf, fu_chunk_get_address(page), G_BIG_ENDIAN); g_byte_array_append(buf, fu_chunk_get_data(page), fu_chunk_get_data_sz(page)); g_debug("writing page at 0x%x", (guint)fu_chunk_get_address(page)); if (!fu_cfi_device_send_command(self, buf->data, buf->len, NULL, 0, progress, error)) return FALSE; if (!fu_device_locker_close(cslocker, error)) return FALSE; /* poll Read Status register BUSY */ return fu_cfi_device_wait_for_status(self, 0b1, 0b0, 100, 50, error); } static gboolean fu_cfi_device_write_pages(FuCfiDevice *self, FuChunkArray *pages, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(pages)); for (guint i = 0; i < fu_chunk_array_length(pages); i++) { g_autoptr(FuChunk) page = NULL; /* prepare chunk */ page = fu_chunk_array_index(pages, i, error); if (page == NULL) return FALSE; if (!fu_cfi_device_write_page(self, page, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_cfi_device_read_block(FuCfiDevice *self, FuChunk *block, FuProgress *progress, GError **error) { guint8 buf_req[4] = {0x0}; /* cmd, then 24 bit starting address */ g_autoptr(FuDeviceLocker) cslocker = NULL; /* enable chip */ cslocker = fu_cfi_device_chip_select_locker_new(self, error); if (cslocker == NULL) return FALSE; if (!fu_cfi_device_get_cmd(self, FU_CFI_DEVICE_CMD_READ_DATA, &buf_req[0], error)) return FALSE; fu_memwrite_uint24(buf_req + 0x1, fu_chunk_get_address(block), G_BIG_ENDIAN); return fu_cfi_device_send_command(self, buf_req, sizeof(buf_req), fu_chunk_get_data_out(block), fu_chunk_get_data_sz(block), progress, error); } static GBytes * fu_cfi_device_read_firmware(FuCfiDevice *self, gsize bufsz, FuProgress *progress, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GPtrArray) pages = NULL; /* progress */ fu_byte_array_set_size(buf, bufsz, 0x0); pages = fu_chunk_array_mutable_new(buf->data, buf->len, 0x0, 0x0, fu_cfi_device_get_block_size(self)); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, pages->len); for (guint i = 0; i < pages->len; i++) { FuChunk *block = g_ptr_array_index(pages, i); if (!fu_cfi_device_read_block(self, block, fu_progress_get_child(progress), error)) return NULL; fu_progress_step_done(progress); } /* success */ return g_bytes_new(buf->data, buf->len); } static GBytes * fu_cfi_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuCfiDevice *self = FU_CFI_DEVICE(device); gsize bufsz = fu_device_get_firmware_size_max(device); g_autoptr(FuDeviceLocker) locker = NULL; /* open programmer */ locker = fu_device_locker_new(device, error); if (locker == NULL) return NULL; /* sanity check */ if (bufsz == 0x0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_READ, "device firmware size not set"); return NULL; } return fu_cfi_device_read_firmware(self, bufsz, progress, error); } static gboolean fu_cfi_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuCfiDevice *self = FU_CFI_DEVICE(device); g_autoptr(GBytes) fw = NULL; g_autoptr(GBytes) fw_verify = NULL; g_autoptr(FuChunkArray) pages = NULL; g_autoptr(FuDeviceLocker) locker = NULL; /* open programmer */ locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 10, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 85, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 5, NULL); /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* erase */ if (!fu_cfi_device_write_enable(self, error)) { g_prefix_error(error, "failed to enable writes: "); return FALSE; } if (!fu_cfi_device_chip_erase(self, error)) { g_prefix_error(error, "failed to erase: "); return FALSE; } fu_progress_step_done(progress); /* write each block */ pages = fu_chunk_array_new_from_bytes(fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, fu_cfi_device_get_page_size(self)); if (!fu_cfi_device_write_pages(self, pages, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to write pages: "); return FALSE; } fu_progress_step_done(progress); /* verify each block */ fw_verify = fu_cfi_device_read_firmware(self, g_bytes_get_size(fw), fu_progress_get_child(progress), error); if (fw_verify == NULL) { g_prefix_error(error, "failed to verify blocks: "); return FALSE; } if (!fu_bytes_compare(fw_verify, fw, error)) { g_prefix_error(error, "verify failed: "); return FALSE; } fu_progress_step_done(progress); /* success! */ return TRUE; } static void fu_cfi_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_cfi_device_init(FuCfiDevice *self) { FuCfiDevicePrivate *priv = GET_PRIVATE(self); priv->page_size = FU_CFI_DEVICE_PAGE_SIZE_DEFAULT; priv->sector_size = FU_CFI_DEVICE_SECTOR_SIZE_DEFAULT; priv->block_size = FU_CFI_DEVICE_BLOCK_SIZE_DEFAULT; priv->cmds[FU_CFI_DEVICE_CMD_WRITE_STATUS] = 0x01; priv->cmds[FU_CFI_DEVICE_CMD_PAGE_PROG] = 0x02; priv->cmds[FU_CFI_DEVICE_CMD_READ_DATA] = 0x03; priv->cmds[FU_CFI_DEVICE_CMD_READ_STATUS] = 0x05; priv->cmds[FU_CFI_DEVICE_CMD_WRITE_EN] = 0x06; priv->cmds[FU_CFI_DEVICE_CMD_SECTOR_ERASE] = 0x20; priv->cmds[FU_CFI_DEVICE_CMD_CHIP_ERASE] = 0x60; priv->cmds[FU_CFI_DEVICE_CMD_READ_ID] = 0x9f; fu_device_add_protocol(FU_DEVICE(self), "org.jedec.cfi"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_OPEN); fu_device_build_vendor_id(FU_DEVICE(self), "SPI", "*"); fu_device_set_summary(FU_DEVICE(self), "CFI flash chip"); } static void fu_cfi_device_constructed(GObject *obj) { FuCfiDevice *self = FU_CFI_DEVICE(obj); fu_device_add_instance_id(FU_DEVICE(self), "SPI"); } static void fu_cfi_device_class_init(FuCfiDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GParamSpec *pspec; object_class->finalize = fu_cfi_device_finalize; object_class->get_property = fu_cfi_device_get_property; object_class->set_property = fu_cfi_device_set_property; object_class->constructed = fu_cfi_device_constructed; device_class->setup = fu_cfi_device_setup; device_class->to_string = fu_cfi_device_to_string; device_class->set_quirk_kv = fu_cfi_device_set_quirk_kv; device_class->write_firmware = fu_cfi_device_write_firmware; device_class->dump_firmware = fu_cfi_device_dump_firmware; device_class->set_progress = fu_cfi_device_set_progress; /** * FuCfiDevice:flash-id: * * The CCI JEDEC flash ID. * * Since: 1.7.1 */ pspec = g_param_spec_string("flash-id", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_FLASH_ID, pspec); } /** * fu_cfi_device_new: * @ctx: a #FuContext * * Creates a new #FuCfiDevice. * * Returns: (transfer full): a #FuCfiDevice * * Since: 1.7.1 **/ FuCfiDevice * fu_cfi_device_new(FuContext *ctx, const gchar *flash_id) { return g_object_new(FU_TYPE_CFI_DEVICE, "context", ctx, "flash-id", flash_id, NULL); } fwupd-2.0.10/libfwupdplugin/fu-cfi-device.h000066400000000000000000000041571501337203100205330ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-cfi-struct.h" #include "fu-device.h" #define FU_TYPE_CFI_DEVICE (fu_cfi_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuCfiDevice, fu_cfi_device, FU, CFI_DEVICE, FuDevice) struct _FuCfiDeviceClass { FuDeviceClass parent_class; gboolean (*chip_select)(FuCfiDevice *self, gboolean value, GError **error); gboolean (*send_command)(FuCfiDevice *self, const guint8 *wbuf, gsize wbufsz, guint8 *rbuf, gsize rbufsz, FuProgress *progress, GError **error); gboolean (*read_jedec)(FuCfiDevice *self, GError **error); }; FuCfiDevice * fu_cfi_device_new(FuContext *ctx, const gchar *flash_id) G_GNUC_NON_NULL(1); const gchar * fu_cfi_device_get_flash_id(FuCfiDevice *self) G_GNUC_NON_NULL(1); void fu_cfi_device_set_flash_id(FuCfiDevice *self, const gchar *flash_id) G_GNUC_NON_NULL(1); guint64 fu_cfi_device_get_size(FuCfiDevice *self) G_GNUC_NON_NULL(1); void fu_cfi_device_set_size(FuCfiDevice *self, guint64 size) G_GNUC_NON_NULL(1); guint32 fu_cfi_device_get_page_size(FuCfiDevice *self) G_GNUC_NON_NULL(1); void fu_cfi_device_set_page_size(FuCfiDevice *self, guint32 page_size) G_GNUC_NON_NULL(1); guint32 fu_cfi_device_get_sector_size(FuCfiDevice *self) G_GNUC_NON_NULL(1); void fu_cfi_device_set_sector_size(FuCfiDevice *self, guint32 sector_size) G_GNUC_NON_NULL(1); guint32 fu_cfi_device_get_block_size(FuCfiDevice *self) G_GNUC_NON_NULL(1); void fu_cfi_device_set_block_size(FuCfiDevice *self, guint32 block_size) G_GNUC_NON_NULL(1); gboolean fu_cfi_device_get_cmd(FuCfiDevice *self, FuCfiDeviceCmd cmd, guint8 *value, GError **error) G_GNUC_NON_NULL(1); gboolean fu_cfi_device_send_command(FuCfiDevice *self, const guint8 *wbuf, gsize wbufsz, guint8 *rbuf, gsize rbufsz, FuProgress *progress, GError **error) G_GNUC_NON_NULL(1); gboolean fu_cfi_device_chip_select(FuCfiDevice *self, gboolean value, GError **error) G_GNUC_NON_NULL(1); FuDeviceLocker * fu_cfi_device_chip_select_locker_new(FuCfiDevice *self, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-cfi.rs000066400000000000000000000004301501337203100174610ustar00rootroot00000000000000// Copyright 2025 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] enum FuCfiDeviceCmd { ReadId, PageProg, ChipErase, ReadData, ReadStatus, SectorErase, WriteEn, WriteStatus, BlockErase, } fwupd-2.0.10/libfwupdplugin/fu-cfu-firmware.rs000066400000000000000000000012271501337203100213140ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructCfuPayload { addr: u32le, size: u8, } #[repr(u8)] enum FuCfuOfferComponentId { NotUsed = 0, // values in between are either valid or reserved OfferInformation = 0xFF, OfferInformation2 = 0xFE, } #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructCfuOffer { segment_number: u8, flags1: u8, component_id: FuCfuOfferComponentId, token: u8, version: u32le, compat_variant_mask: u32le, flags2: u8, flags3: u8, product_id: u16le, } fwupd-2.0.10/libfwupdplugin/fu-cfu-offer.c000066400000000000000000000350031501337203100203760ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include "fu-byte-array.h" #include "fu-cfu-firmware-struct.h" #include "fu-cfu-offer.h" #include "fu-common.h" #include "fu-string.h" #include "fu-version-common.h" /** * FuCfuOffer: * * A CFU offer. This is a 16 byte blob which contains enough data for the device to either accept * or refuse a firmware payload. The offer may be loaded from disk, network, or even constructed * manually. There is much left to how the specific firmware implements CFU, and it's expected * that multiple different plugins will use this offer in different ways. * * Documented: https://docs.microsoft.com/en-us/windows-hardware/drivers/cfu/cfu-specification * * See also: [class@FuFirmware] */ typedef struct { guint8 segment_number; gboolean force_immediate_reset; gboolean force_ignore_version; guint8 component_id; guint8 token; guint32 hw_variant; guint8 protocol_revision; guint8 bank; guint8 milestone; guint16 product_id; } FuCfuOfferPrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuCfuOffer, fu_cfu_offer, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_cfu_offer_get_instance_private(o)) static void fu_cfu_offer_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuCfuOffer *self = FU_CFU_OFFER(firmware); FuCfuOfferPrivate *priv = GET_PRIVATE(self); fu_xmlb_builder_insert_kx(bn, "segment_number", priv->segment_number); fu_xmlb_builder_insert_kb(bn, "force_immediate_reset", priv->force_immediate_reset); fu_xmlb_builder_insert_kb(bn, "force_ignore_version", priv->force_ignore_version); fu_xmlb_builder_insert_kx(bn, "component_id", priv->component_id); fu_xmlb_builder_insert_kx(bn, "token", priv->token); fu_xmlb_builder_insert_kx(bn, "hw_variant", priv->hw_variant); fu_xmlb_builder_insert_kx(bn, "protocol_revision", priv->protocol_revision); fu_xmlb_builder_insert_kx(bn, "bank", priv->bank); fu_xmlb_builder_insert_kx(bn, "milestone", priv->milestone); fu_xmlb_builder_insert_kx(bn, "product_id", priv->product_id); } /** * fu_cfu_offer_get_segment_number: * @self: a #FuCfuOffer * * Gets the part of the firmware that is being transferred. * * Returns: integer * * Since: 1.7.0 **/ guint8 fu_cfu_offer_get_segment_number(FuCfuOffer *self) { FuCfuOfferPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CFU_OFFER(self), 0x0); return priv->segment_number; } /** * fu_cfu_offer_get_force_immediate_reset: * @self: a #FuCfuOffer * * Gets if the in-situ firmware should reset into the new firmware immediately, rather than waiting * for the next time the device is replugged. * * Returns: boolean * * Since: 1.7.0 **/ gboolean fu_cfu_offer_get_force_immediate_reset(FuCfuOffer *self) { FuCfuOfferPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CFU_OFFER(self), FALSE); return priv->force_immediate_reset; } /** * fu_cfu_offer_get_force_ignore_version: * @self: a #FuCfuOffer * * Gets if the in-situ firmware should ignore version mismatch (e.g. downgrade). * * Returns: boolean * * Since: 1.7.0 **/ gboolean fu_cfu_offer_get_force_ignore_version(FuCfuOffer *self) { FuCfuOfferPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CFU_OFFER(self), FALSE); return priv->force_ignore_version; } /** * fu_cfu_offer_get_component_id: * @self: a #FuCfuOffer * * Gets the component in the device to apply the firmware update. * * Returns: integer * * Since: 1.7.0 **/ guint8 fu_cfu_offer_get_component_id(FuCfuOffer *self) { FuCfuOfferPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CFU_OFFER(self), 0x0); return priv->component_id; } /** * fu_cfu_offer_get_token: * @self: a #FuCfuOffer * * Gets the token to identify the user specific software making the offer. * * Returns: integer * * Since: 1.7.0 **/ guint8 fu_cfu_offer_get_token(FuCfuOffer *self) { FuCfuOfferPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CFU_OFFER(self), 0x0); return priv->token; } /** * fu_cfu_offer_get_hw_variant: * @self: a #FuCfuOffer * * Gets the hardware variant bitmask corresponding with compatible firmware. * * Returns: integer * * Since: 1.7.0 **/ guint32 fu_cfu_offer_get_hw_variant(FuCfuOffer *self) { FuCfuOfferPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CFU_OFFER(self), 0x0); return priv->hw_variant; } /** * fu_cfu_offer_get_protocol_revision: * @self: a #FuCfuOffer * * Gets the CFU protocol version. * * Returns: integer * * Since: 1.7.0 **/ guint8 fu_cfu_offer_get_protocol_revision(FuCfuOffer *self) { FuCfuOfferPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CFU_OFFER(self), 0x0); return priv->protocol_revision; } /** * fu_cfu_offer_get_bank: * @self: a #FuCfuOffer * * Gets the bank register, used if multiple banks are supported. * * Returns: integer * * Since: 1.7.0 **/ guint8 fu_cfu_offer_get_bank(FuCfuOffer *self) { FuCfuOfferPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CFU_OFFER(self), 0x0); return priv->bank; } /** * fu_cfu_offer_get_milestone: * @self: a #FuCfuOffer * * Gets the milestone, which can be used as a version for example EV1, EVT etc. * * Returns: integer * * Since: 1.7.0 **/ guint8 fu_cfu_offer_get_milestone(FuCfuOffer *self) { FuCfuOfferPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CFU_OFFER(self), 0x0); return priv->milestone; } /** * fu_cfu_offer_get_product_id: * @self: a #FuCfuOffer * * Gets the product ID for this CFU image. * * Returns: integer * * Since: 1.7.0 **/ guint16 fu_cfu_offer_get_product_id(FuCfuOffer *self) { FuCfuOfferPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CFU_OFFER(self), 0x0); return priv->product_id; } /** * fu_cfu_offer_set_segment_number: * @self: a #FuCfuOffer * @segment_number: integer * * Sets the part of the firmware that is being transferred. * * Since: 1.7.0 **/ void fu_cfu_offer_set_segment_number(FuCfuOffer *self, guint8 segment_number) { FuCfuOfferPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CFU_OFFER(self)); priv->segment_number = segment_number; } /** * fu_cfu_offer_set_force_immediate_reset: * @self: a #FuCfuOffer * @force_immediate_reset: boolean * * Sets if the in-situ firmware should reset into the new firmware immediately, rather than waiting * for the next time the device is replugged. * * Since: 1.7.0 **/ void fu_cfu_offer_set_force_immediate_reset(FuCfuOffer *self, gboolean force_immediate_reset) { FuCfuOfferPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CFU_OFFER(self)); priv->force_immediate_reset = force_immediate_reset; } /** * fu_cfu_offer_set_force_ignore_version: * @self: a #FuCfuOffer * @force_ignore_version: boolean * * Sets if the in-situ firmware should ignore version mismatch (e.g. downgrade). * * Since: 1.7.0 **/ void fu_cfu_offer_set_force_ignore_version(FuCfuOffer *self, gboolean force_ignore_version) { FuCfuOfferPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CFU_OFFER(self)); priv->force_ignore_version = force_ignore_version; } /** * fu_cfu_offer_set_component_id: * @self: a #FuCfuOffer * @component_id: integer * * Sets the component in the device to apply the firmware update. * * Since: 1.7.0 **/ void fu_cfu_offer_set_component_id(FuCfuOffer *self, guint8 component_id) { FuCfuOfferPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CFU_OFFER(self)); priv->component_id = component_id; } /** * fu_cfu_offer_set_token: * @self: a #FuCfuOffer * @token: integer * * Sets the token to identify the user specific software making the offer. * * Since: 1.7.0 **/ void fu_cfu_offer_set_token(FuCfuOffer *self, guint8 token) { FuCfuOfferPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CFU_OFFER(self)); priv->token = token; } /** * fu_cfu_offer_set_hw_variant: * @self: a #FuCfuOffer * @hw_variant: integer * * Sets the hardware variant bitmask corresponding with compatible firmware. * * Since: 1.7.0 **/ void fu_cfu_offer_set_hw_variant(FuCfuOffer *self, guint32 hw_variant) { FuCfuOfferPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CFU_OFFER(self)); priv->hw_variant = hw_variant; } /** * fu_cfu_offer_set_protocol_revision: * @self: a #FuCfuOffer * @protocol_revision: integer * * Sets the CFU protocol version. * * Since: 1.7.0 **/ void fu_cfu_offer_set_protocol_revision(FuCfuOffer *self, guint8 protocol_revision) { FuCfuOfferPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CFU_OFFER(self)); g_return_if_fail(protocol_revision <= 0b1111); priv->protocol_revision = protocol_revision; } /** * fu_cfu_offer_set_bank: * @self: a #FuCfuOffer * @bank: integer * * Sets bank register, used if multiple banks are supported. * * Since: 1.7.0 **/ void fu_cfu_offer_set_bank(FuCfuOffer *self, guint8 bank) { FuCfuOfferPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CFU_OFFER(self)); g_return_if_fail(bank <= 0b11); priv->bank = bank; } /** * fu_cfu_offer_set_milestone: * @self: a #FuCfuOffer * @milestone: integer * * Sets the milestone, which can be used as a version for example EV1, EVT etc. * * Since: 1.7.0 **/ void fu_cfu_offer_set_milestone(FuCfuOffer *self, guint8 milestone) { FuCfuOfferPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CFU_OFFER(self)); g_return_if_fail(milestone <= 0b111); priv->milestone = milestone; } /** * fu_cfu_offer_set_product_id: * @self: a #FuCfuOffer * @product_id: integer * * Sets the product ID for this CFU image. * * Since: 1.7.0 **/ void fu_cfu_offer_set_product_id(FuCfuOffer *self, guint16 product_id) { FuCfuOfferPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CFU_OFFER(self)); priv->product_id = product_id; } static gboolean fu_cfu_offer_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuCfuOffer *self = FU_CFU_OFFER(firmware); FuCfuOfferPrivate *priv = GET_PRIVATE(self); guint8 flags1; guint8 flags2; guint8 flags3; g_autoptr(GByteArray) st = NULL; /* parse */ st = fu_struct_cfu_offer_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; priv->segment_number = fu_struct_cfu_offer_get_segment_number(st); priv->component_id = fu_struct_cfu_offer_get_component_id(st); priv->token = fu_struct_cfu_offer_get_token(st); priv->hw_variant = fu_struct_cfu_offer_get_compat_variant_mask(st); priv->product_id = fu_struct_cfu_offer_get_product_id(st); /* AA.BBCC.DD */ fu_firmware_set_version_raw(firmware, fu_struct_cfu_offer_get_version(st)); /* component info */ flags1 = fu_struct_cfu_offer_get_flags1(st); priv->force_ignore_version = (flags1 & 0b10000000) > 0; priv->force_immediate_reset = (flags1 & 0b01000000) > 0; /* product info */ flags2 = fu_struct_cfu_offer_get_flags2(st); priv->protocol_revision = (flags2 >> 4) & 0b1111; priv->bank = (flags2 >> 2) & 0b11; flags3 = fu_struct_cfu_offer_get_flags3(st); priv->milestone = (flags3 >> 5) & 0b111; /* success */ return TRUE; } static GByteArray * fu_cfu_offer_write(FuFirmware *firmware, GError **error) { FuCfuOffer *self = FU_CFU_OFFER(firmware); FuCfuOfferPrivate *priv = GET_PRIVATE(self); g_autoptr(GByteArray) st = fu_struct_cfu_offer_new(); /* component info */ fu_struct_cfu_offer_set_segment_number(st, priv->segment_number); fu_struct_cfu_offer_set_flags1(st, priv->force_ignore_version << 7 | (priv->force_immediate_reset << 6)); fu_struct_cfu_offer_set_component_id(st, priv->component_id); fu_struct_cfu_offer_set_token(st, priv->token); /* version */ fu_struct_cfu_offer_set_version(st, fu_firmware_get_version_raw(firmware)); fu_struct_cfu_offer_set_compat_variant_mask(st, priv->hw_variant); /* product info */ fu_struct_cfu_offer_set_flags2(st, (priv->protocol_revision << 4) | (priv->bank << 2)); fu_struct_cfu_offer_set_flags3(st, priv->milestone << 5); fu_struct_cfu_offer_set_product_id(st, priv->product_id); /* success */ return g_steal_pointer(&st); } static gboolean fu_cfu_offer_build(FuFirmware *firmware, XbNode *n, GError **error) { FuCfuOffer *self = FU_CFU_OFFER(firmware); FuCfuOfferPrivate *priv = GET_PRIVATE(self); guint64 tmp; const gchar *tmpb; /* optional properties */ tmp = xb_node_query_text_as_uint(n, "segment_number", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT8) priv->segment_number = tmp; tmpb = xb_node_query_text(n, "force_immediate_reset", NULL); if (tmpb != NULL) { if (!fu_strtobool(tmpb, &priv->force_immediate_reset, error)) return FALSE; } tmpb = xb_node_query_text(n, "force_ignore_version", NULL); if (tmpb != NULL) { if (!fu_strtobool(tmpb, &priv->force_ignore_version, error)) return FALSE; } tmp = xb_node_query_text_as_uint(n, "component_id", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT8) priv->component_id = tmp; tmp = xb_node_query_text_as_uint(n, "token", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT8) priv->token = tmp; tmp = xb_node_query_text_as_uint(n, "hw_variant", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT32) priv->hw_variant = tmp; tmp = xb_node_query_text_as_uint(n, "protocol_revision", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT8) priv->protocol_revision = tmp; tmp = xb_node_query_text_as_uint(n, "bank", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT8) priv->bank = tmp; tmp = xb_node_query_text_as_uint(n, "milestone", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT8) priv->milestone = tmp; tmp = xb_node_query_text_as_uint(n, "product_id", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT16) priv->product_id = tmp; /* success */ return TRUE; } static gchar * fu_cfu_offer_convert_version(FuFirmware *firmware, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_firmware_get_version_format(firmware)); } static void fu_cfu_offer_init(FuCfuOffer *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_VID_PID); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_NO_AUTO_DETECTION); fu_firmware_set_version_format(FU_FIRMWARE(self), FWUPD_VERSION_FORMAT_SURFACE); } static void fu_cfu_offer_class_init(FuCfuOfferClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->convert_version = fu_cfu_offer_convert_version; firmware_class->export = fu_cfu_offer_export; firmware_class->parse = fu_cfu_offer_parse; firmware_class->write = fu_cfu_offer_write; firmware_class->build = fu_cfu_offer_build; } /** * fu_cfu_offer_new: * * Creates a new #FuFirmware for a CFU offer * * Since: 1.7.0 **/ FuFirmware * fu_cfu_offer_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_CFU_OFFER, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-cfu-offer.h000066400000000000000000000040621501337203100204040ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_CFU_OFFER (fu_cfu_offer_get_type()) G_DECLARE_DERIVABLE_TYPE(FuCfuOffer, fu_cfu_offer, FU, CFU_OFFER, FuFirmware) struct _FuCfuOfferClass { FuFirmwareClass parent_class; }; FuFirmware * fu_cfu_offer_new(void); guint8 fu_cfu_offer_get_segment_number(FuCfuOffer *self) G_GNUC_NON_NULL(1); gboolean fu_cfu_offer_get_force_immediate_reset(FuCfuOffer *self) G_GNUC_NON_NULL(1); gboolean fu_cfu_offer_get_force_ignore_version(FuCfuOffer *self) G_GNUC_NON_NULL(1); guint8 fu_cfu_offer_get_component_id(FuCfuOffer *self) G_GNUC_NON_NULL(1); guint8 fu_cfu_offer_get_token(FuCfuOffer *self) G_GNUC_NON_NULL(1); guint32 fu_cfu_offer_get_hw_variant(FuCfuOffer *self) G_GNUC_NON_NULL(1); guint8 fu_cfu_offer_get_protocol_revision(FuCfuOffer *self) G_GNUC_NON_NULL(1); guint8 fu_cfu_offer_get_bank(FuCfuOffer *self) G_GNUC_NON_NULL(1); guint8 fu_cfu_offer_get_milestone(FuCfuOffer *self) G_GNUC_NON_NULL(1); guint16 fu_cfu_offer_get_product_id(FuCfuOffer *self) G_GNUC_NON_NULL(1); void fu_cfu_offer_set_segment_number(FuCfuOffer *self, guint8 segment_number) G_GNUC_NON_NULL(1); void fu_cfu_offer_set_force_immediate_reset(FuCfuOffer *self, gboolean force_immediate_reset) G_GNUC_NON_NULL(1); void fu_cfu_offer_set_force_ignore_version(FuCfuOffer *self, gboolean force_ignore_version) G_GNUC_NON_NULL(1); void fu_cfu_offer_set_component_id(FuCfuOffer *self, guint8 component_id) G_GNUC_NON_NULL(1); void fu_cfu_offer_set_token(FuCfuOffer *self, guint8 token) G_GNUC_NON_NULL(1); void fu_cfu_offer_set_hw_variant(FuCfuOffer *self, guint32 hw_variant) G_GNUC_NON_NULL(1); void fu_cfu_offer_set_protocol_revision(FuCfuOffer *self, guint8 protocol_revision) G_GNUC_NON_NULL(1); void fu_cfu_offer_set_bank(FuCfuOffer *self, guint8 bank) G_GNUC_NON_NULL(1); void fu_cfu_offer_set_milestone(FuCfuOffer *self, guint8 milestone) G_GNUC_NON_NULL(1); void fu_cfu_offer_set_product_id(FuCfuOffer *self, guint16 product_id) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-cfu-payload.c000066400000000000000000000060051501337203100207260ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-cfu-firmware-struct.h" #include "fu-cfu-payload.h" #include "fu-common.h" #include "fu-input-stream.h" /** * FuCfuPayload: * * A CFU payload. This contains of a variable number of blocks, each containing the address, size * and the chunk data. The chunks do not have to be the same size, and the address ranges do not * have to be continuous. * * Documented: https://docs.microsoft.com/en-us/windows-hardware/drivers/cfu/cfu-specification * * See also: [class@FuFirmware] */ G_DEFINE_TYPE(FuCfuPayload, fu_cfu_payload, FU_TYPE_FIRMWARE) static gboolean fu_cfu_payload_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { gsize offset = 0; gsize streamsz = 0; /* process into chunks */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; while (offset < streamsz) { guint8 chunk_size = 0; g_autoptr(FuChunk) chk = NULL; g_autoptr(GBytes) blob = NULL; g_autoptr(GByteArray) st = NULL; st = fu_struct_cfu_payload_parse_stream(stream, offset, error); if (st == NULL) return FALSE; offset += st->len; chunk_size = fu_struct_cfu_payload_get_size(st); if (chunk_size == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "payload size was invalid"); return FALSE; } blob = fu_input_stream_read_bytes(stream, offset, chunk_size, NULL, error); if (blob == NULL) return FALSE; chk = fu_chunk_bytes_new(blob); fu_chunk_set_address(chk, fu_struct_cfu_payload_get_addr(st)); fu_firmware_add_chunk(firmware, chk); /* next! */ offset += chunk_size; } /* success */ return TRUE; } static GByteArray * fu_cfu_payload_write(FuFirmware *firmware, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GPtrArray) chunks = NULL; chunks = fu_firmware_get_chunks(firmware, error); if (chunks == NULL) return NULL; for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); g_autoptr(GByteArray) st = fu_struct_cfu_payload_new(); fu_struct_cfu_payload_set_addr(st, fu_chunk_get_address(chk)); fu_struct_cfu_payload_set_size(st, fu_chunk_get_data_sz(chk)); g_byte_array_append(buf, st->data, st->len); g_byte_array_append(buf, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); } return g_steal_pointer(&buf); } static void fu_cfu_payload_init(FuCfuPayload *self) { } static void fu_cfu_payload_class_init(FuCfuPayloadClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_cfu_payload_parse; firmware_class->write = fu_cfu_payload_write; } /** * fu_cfu_payload_new: * * Creates a new #FuFirmware for a CFU payload * * Since: 1.7.0 **/ FuFirmware * fu_cfu_payload_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_CFU_PAYLOAD, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-cfu-payload.h000066400000000000000000000006131501337203100207320ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_CFU_PAYLOAD (fu_cfu_payload_get_type()) G_DECLARE_DERIVABLE_TYPE(FuCfuPayload, fu_cfu_payload, FU, CFU_PAYLOAD, FuFirmware) struct _FuCfuPayloadClass { FuFirmwareClass parent_class; }; FuFirmware * fu_cfu_payload_new(void); fwupd-2.0.10/libfwupdplugin/fu-chunk-array.c000066400000000000000000000166751501337203100207640ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuChunkArray" #include "config.h" #include "fu-bytes.h" #include "fu-chunk-array.h" #include "fu-chunk-private.h" #include "fu-input-stream.h" /** * FuChunkArray: * * Create chunked data with address and index as required. */ struct _FuChunkArray { GObject parent_instance; GBytes *blob; GInputStream *stream; gsize addr_offset; gsize page_sz; gsize packet_sz; GArray *offsets; /* of gsize */ gsize total_size; }; G_DEFINE_TYPE(FuChunkArray, fu_chunk_array, G_TYPE_OBJECT) /** * fu_chunk_array_length: * @self: a #FuChunkArray * * Gets the number of chunks. * * Returns: integer * * Since: 1.9.6 **/ guint fu_chunk_array_length(FuChunkArray *self) { g_return_val_if_fail(FU_IS_CHUNK_ARRAY(self), G_MAXUINT); return self->offsets->len; } static void fu_chunk_array_calculate_chunk_for_offset(FuChunkArray *self, gsize offset, gsize *address, gsize *page, gsize *chunksz) { gsize chunksz_tmp = MIN(self->packet_sz, self->total_size - offset); gsize page_tmp = 0; gsize address_tmp = self->addr_offset + offset; /* if page_sz is not specified then all the pages are 0 */ if (self->page_sz > 0) { address_tmp %= self->page_sz; page_tmp = (offset + self->addr_offset) / self->page_sz; } /* cut the packet so it does not straddle multiple blocks */ if (self->page_sz != self->packet_sz && self->page_sz > 0) chunksz_tmp = MIN(chunksz_tmp, (offset + self->packet_sz) % self->page_sz); /* all optional */ if (address != NULL) *address = address_tmp; if (page != NULL) *page = page_tmp; if (chunksz != NULL) *chunksz = chunksz_tmp; } /** * fu_chunk_array_index: * @self: a #FuChunkArray * @idx: the chunk index * @error: (nullable): optional return location for an error * * Gets the next chunk. * * Returns: (transfer full): a #FuChunk or %NULL if not valid * * Since: 1.9.6 **/ FuChunk * fu_chunk_array_index(FuChunkArray *self, guint idx, GError **error) { gsize address = 0; gsize chunksz = 0; gsize offset; gsize page = 0; g_autoptr(FuChunk) chk = NULL; g_return_val_if_fail(FU_IS_CHUNK_ARRAY(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); if (idx >= self->offsets->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "idx %u invalid", idx); return NULL; } /* calculate address, page and chunk size from the offset */ offset = g_array_index(self->offsets, gsize, idx); fu_chunk_array_calculate_chunk_for_offset(self, offset, &address, &page, &chunksz); if (chunksz == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "idx %u zero sized", idx); return NULL; } /* create new chunk */ if (self->blob != NULL) { g_autoptr(GBytes) blob_chk = g_bytes_new_from_bytes(self->blob, offset, chunksz); chk = fu_chunk_bytes_new(blob_chk); } else if (self->stream != NULL) { g_autoptr(GBytes) blob_chk = fu_input_stream_read_bytes(self->stream, offset, chunksz, NULL, error); if (blob_chk == NULL) { g_prefix_error(error, "failed to get stream at 0x%x for 0x%x: ", (guint)offset, (guint)chunksz); return NULL; } chk = fu_chunk_bytes_new(blob_chk); } else { chk = fu_chunk_bytes_new(NULL); fu_chunk_set_data_sz(chk, chunksz); } fu_chunk_set_idx(chk, idx); fu_chunk_set_page(chk, page); fu_chunk_set_address(chk, address); return g_steal_pointer(&chk); } static void fu_chunk_array_ensure_offsets(FuChunkArray *self) { gsize offset = 0; while (offset < self->total_size) { gsize chunksz = 0; fu_chunk_array_calculate_chunk_for_offset(self, offset, NULL, NULL, &chunksz); g_array_append_val(self->offsets, offset); offset += chunksz; } } /** * fu_chunk_array_new_from_bytes: * @blob: data * @addr_offset: the hardware address offset, or %FU_CHUNK_ADDR_OFFSET_NONE * @page_sz: the hardware page size, typically %FU_CHUNK_PAGESZ_NONE * @packet_sz: the packet size, or 0x0 * * Chunks a linear blob of memory into packets, ensuring each packet is less that a specific * transfer size. * * Returns: (transfer full): a #FuChunkArray * * Since: 1.9.6 **/ FuChunkArray * fu_chunk_array_new_from_bytes(GBytes *blob, gsize addr_offset, gsize page_sz, gsize packet_sz) { g_autoptr(FuChunkArray) self = g_object_new(FU_TYPE_CHUNK_ARRAY, NULL); g_return_val_if_fail(blob != NULL, NULL); g_return_val_if_fail(page_sz == 0 || page_sz >= packet_sz, NULL); self->addr_offset = addr_offset; self->page_sz = page_sz; self->packet_sz = packet_sz; self->blob = g_bytes_ref(blob); self->total_size = g_bytes_get_size(self->blob); /* success */ fu_chunk_array_ensure_offsets(self); return g_steal_pointer(&self); } /** * fu_chunk_array_new_virtual: * @bufsz: size of the buffer * @addr_offset: the hardware address offset, or %FU_CHUNK_ADDR_OFFSET_NONE * @page_sz: the hardware page size, typically %FU_CHUNK_PAGESZ_NONE * @packet_sz: the packet size, or 0x0 * * Chunks a virtual buffer memory into packets, ensuring each packet is less that a specific * transfer size. * * Returns: (transfer full): a #FuChunkArray * * Since: 2.0.2 **/ FuChunkArray * fu_chunk_array_new_virtual(gsize bufsz, gsize addr_offset, gsize page_sz, gsize packet_sz) { g_autoptr(FuChunkArray) self = g_object_new(FU_TYPE_CHUNK_ARRAY, NULL); g_return_val_if_fail(page_sz == 0 || page_sz >= packet_sz, NULL); self->addr_offset = addr_offset; self->page_sz = page_sz; self->packet_sz = packet_sz; self->total_size = bufsz; /* success */ fu_chunk_array_ensure_offsets(self); return g_steal_pointer(&self); } /** * fu_chunk_array_new_from_stream: * @stream: a #GInputStream * @addr_offset: the hardware address offset, or %FU_CHUNK_ADDR_OFFSET_NONE * @page_sz: the hardware page size, typically %FU_CHUNK_PAGESZ_NONE * @packet_sz: the packet size, or 0x0 * @error: (nullable): optional return location for an error * * Chunks a linear stream into packets, ensuring each packet is less that a specific * transfer size. * * Returns: (transfer full): a #FuChunkArray, or #NULL on error * * Since: 2.0.2 **/ FuChunkArray * fu_chunk_array_new_from_stream(GInputStream *stream, gsize addr_offset, gsize page_sz, gsize packet_sz, GError **error) { g_autoptr(FuChunkArray) self = g_object_new(FU_TYPE_CHUNK_ARRAY, NULL); g_return_val_if_fail(G_IS_INPUT_STREAM(stream), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); g_return_val_if_fail(page_sz == 0 || page_sz >= packet_sz, NULL); if (!fu_input_stream_size(stream, &self->total_size, error)) return NULL; if (!g_seekable_seek(G_SEEKABLE(stream), 0x0, G_SEEK_SET, NULL, error)) return NULL; self->addr_offset = addr_offset; self->page_sz = page_sz; self->packet_sz = packet_sz; self->stream = g_object_ref(stream); /* success */ fu_chunk_array_ensure_offsets(self); return g_steal_pointer(&self); } static void fu_chunk_array_finalize(GObject *object) { FuChunkArray *self = FU_CHUNK_ARRAY(object); g_array_unref(self->offsets); if (self->blob != NULL) g_bytes_unref(self->blob); if (self->stream != NULL) g_object_unref(self->stream); G_OBJECT_CLASS(fu_chunk_array_parent_class)->finalize(object); } static void fu_chunk_array_class_init(FuChunkArrayClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_chunk_array_finalize; } static void fu_chunk_array_init(FuChunkArray *self) { self->offsets = g_array_new(FALSE, FALSE, sizeof(gsize)); } fwupd-2.0.10/libfwupdplugin/fu-chunk-array.h000066400000000000000000000016121501337203100207520ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-chunk.h" #define FU_TYPE_CHUNK_ARRAY (fu_chunk_array_get_type()) G_DECLARE_FINAL_TYPE(FuChunkArray, fu_chunk_array, FU, CHUNK_ARRAY, GObject) FuChunkArray * fu_chunk_array_new_virtual(gsize bufsz, gsize addr_offset, gsize page_sz, gsize packet_sz); FuChunkArray * fu_chunk_array_new_from_bytes(GBytes *blob, gsize addr_offset, gsize page_sz, gsize packet_sz) G_GNUC_NON_NULL(1); FuChunkArray * fu_chunk_array_new_from_stream(GInputStream *stream, gsize addr_offset, gsize page_sz, gsize packet_sz, GError **error) G_GNUC_NON_NULL(1); guint fu_chunk_array_length(FuChunkArray *self) G_GNUC_NON_NULL(1); FuChunk * fu_chunk_array_index(FuChunkArray *self, guint idx, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-chunk-private.h000066400000000000000000000007251501337203100213120ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-chunk.h" #include "fu-firmware.h" void fu_chunk_set_data_sz(FuChunk *self, gsize data_sz) G_GNUC_NON_NULL(1); void fu_chunk_export(FuChunk *self, FuFirmwareExportFlags flags, XbBuilderNode *bn) G_GNUC_NON_NULL(1, 3); gboolean fu_chunk_build(FuChunk *self, XbNode *n, GError **error) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-chunk.c000066400000000000000000000261631501337203100176410ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuChunk" #include "config.h" #include #include "fu-bytes.h" #include "fu-chunk-private.h" #include "fu-common.h" #include "fu-mem.h" /** * FuChunk: * * A optionally mutable packet of chunked data with address, page and index. */ struct _FuChunk { GObject parent_instance; guint idx; guint page; gsize address; const guint8 *data; gsize data_sz; gboolean is_mutable; GBytes *bytes; }; G_DEFINE_TYPE(FuChunk, fu_chunk, G_TYPE_OBJECT) /** * fu_chunk_set_idx: * @self: a #FuChunk * @idx: index, starting at 0 * * Sets the index of the chunk. * * Since: 1.5.6 **/ void fu_chunk_set_idx(FuChunk *self, guint idx) { g_return_if_fail(FU_IS_CHUNK(self)); self->idx = idx; } /** * fu_chunk_get_idx: * @self: a #FuChunk * * Gets the index of the chunk. * * Returns: index * * Since: 1.5.6 **/ guint fu_chunk_get_idx(FuChunk *self) { g_return_val_if_fail(FU_IS_CHUNK(self), G_MAXUINT32); return self->idx; } /** * fu_chunk_set_page: * @self: a #FuChunk * @page: page number, starting at 0 * * Sets the page of the chunk. * * Since: 1.5.6 **/ void fu_chunk_set_page(FuChunk *self, guint page) { g_return_if_fail(FU_IS_CHUNK(self)); self->page = page; } /** * fu_chunk_get_page: * @self: a #FuChunk * * Gets the page of the chunk. * * Returns: page * * Since: 1.5.6 **/ guint fu_chunk_get_page(FuChunk *self) { g_return_val_if_fail(FU_IS_CHUNK(self), G_MAXUINT32); return self->page; } /** * fu_chunk_set_address: * @self: a #FuChunk * @address: memory address * * Sets the address of the chunk. * * Since: 1.5.6 **/ void fu_chunk_set_address(FuChunk *self, gsize address) { g_return_if_fail(FU_IS_CHUNK(self)); self->address = address; } /** * fu_chunk_get_address: * @self: a #FuChunk * * Gets the address of the chunk. * * Returns: address * * Since: 1.5.6 **/ gsize fu_chunk_get_address(FuChunk *self) { g_return_val_if_fail(FU_IS_CHUNK(self), G_MAXUINT32); return self->address; } /** * fu_chunk_get_data: * @self: a #FuChunk * * Gets the data of the chunk. * * Returns: bytes * * Since: 1.5.6 **/ const guint8 * fu_chunk_get_data(FuChunk *self) { g_return_val_if_fail(FU_IS_CHUNK(self), NULL); return self->data; } /** * fu_chunk_get_data_out: * @self: a #FuChunk * * Gets the mutable data of the chunk. * * WARNING: At the moment fu_chunk_get_data_out() returns the same data as * fu_chunk_get_data() in all cases. The caller should verify the data passed to * fu_chunk_array_new() is also writable (i.e. not `const` or `mmap`) before * using this function. * * Returns: (transfer none): bytes * * Since: 1.5.6 **/ guint8 * fu_chunk_get_data_out(FuChunk *self) { g_return_val_if_fail(FU_IS_CHUNK(self), NULL); /* warn, but allow to proceed */ if (!self->is_mutable) { g_critical("calling fu_chunk_get_data_out() from immutable chunk"); self->is_mutable = TRUE; } return (guint8 *)self->data; } /** * fu_chunk_get_data_sz: * @self: a #FuChunk * * Gets the data size of the chunk. * * Returns: size in bytes * * Since: 1.5.6 **/ gsize fu_chunk_get_data_sz(FuChunk *self) { g_return_val_if_fail(FU_IS_CHUNK(self), G_MAXUINT32); return self->data_sz; } /* private */ void fu_chunk_set_data_sz(FuChunk *self, gsize data_sz) { g_return_if_fail(FU_IS_CHUNK(self)); self->data_sz = data_sz; } /** * fu_chunk_set_bytes: * @self: a #FuChunk * @bytes: (nullable): data * * Sets the data to use for the chunk. * * Since: 1.5.6 **/ void fu_chunk_set_bytes(FuChunk *self, GBytes *bytes) { g_return_if_fail(FU_IS_CHUNK(self)); /* not changed */ if (self->bytes == bytes) return; if (self->bytes != NULL) { g_bytes_unref(self->bytes); self->bytes = NULL; } if (bytes != NULL) { self->bytes = g_bytes_ref(bytes); self->data = g_bytes_get_data(bytes, NULL); self->data_sz = g_bytes_get_size(bytes); } } /** * fu_chunk_get_bytes: * @self: a #FuChunk * * Gets the data of the chunk. * * Returns: (transfer full): data * * Since: 1.5.6 **/ GBytes * fu_chunk_get_bytes(FuChunk *self) { g_return_val_if_fail(FU_IS_CHUNK(self), NULL); if (self->bytes != NULL) return g_bytes_ref(self->bytes); return g_bytes_new_static(self->data, self->data_sz); } /** * fu_chunk_new: * @idx: the packet number * @page: the hardware memory page * @address: the address *within* the page * @data: the data * @data_sz: size of @data_sz * * Creates a new packet of chunked data. * * Returns: (transfer full): a #FuChunk * * Since: 1.1.2 **/ FuChunk * fu_chunk_new(guint idx, guint page, gsize address, const guint8 *data, gsize data_sz) { FuChunk *self = g_object_new(FU_TYPE_CHUNK, NULL); self->idx = idx; self->page = page; self->address = address; self->data = data; self->data_sz = data_sz; return self; } /** * fu_chunk_bytes_new: * @bytes: (nullable): data * * Creates a new packet of data. * * Returns: (transfer full): a #FuChunk * * Since: 1.5.6 **/ FuChunk * fu_chunk_bytes_new(GBytes *bytes) { FuChunk *self = g_object_new(FU_TYPE_CHUNK, NULL); fu_chunk_set_bytes(self, bytes); return self; } void fu_chunk_export(FuChunk *self, FuFirmwareExportFlags flags, XbBuilderNode *bn) { fu_xmlb_builder_insert_kx(bn, "idx", self->idx); fu_xmlb_builder_insert_kx(bn, "page", self->page); fu_xmlb_builder_insert_kx(bn, "addr", self->address); if (self->data != NULL) { g_autofree gchar *datastr = NULL; g_autofree gchar *dataszstr = g_strdup_printf("0x%x", (guint)self->data_sz); if (flags & FU_FIRMWARE_EXPORT_FLAG_ASCII_DATA) { datastr = fu_memstrsafe(self->data, self->data_sz, 0x0, MIN(self->data_sz, 16), NULL); } else { datastr = g_base64_encode(self->data, self->data_sz); } xb_builder_node_insert_text(bn, "data", datastr, "size", dataszstr, NULL); } } /** * fu_chunk_to_string: * @self: a #FuChunk * * Converts the chunked packet to a string representation. * * Returns: (transfer full): a string * * Since: 1.1.2 **/ gchar * fu_chunk_to_string(FuChunk *self) { g_autoptr(XbBuilderNode) bn = xb_builder_node_new("chunk"); fu_chunk_export(self, FU_FIRMWARE_EXPORT_FLAG_ASCII_DATA, bn); return xb_builder_node_export(bn, XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE | XB_NODE_EXPORT_FLAG_COLLAPSE_EMPTY | XB_NODE_EXPORT_FLAG_FORMAT_INDENT, NULL); } /** * fu_chunk_array_to_string: * @chunks: (element-type FuChunk): array of chunks * * Converts all the chunked packets in an array to a string representation. * * Returns: (transfer full): a string * * Since: 1.0.1 **/ gchar * fu_chunk_array_to_string(GPtrArray *chunks) { g_autoptr(XbBuilderNode) bn = xb_builder_node_new("chunks"); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); g_autoptr(XbBuilderNode) bc = xb_builder_node_insert(bn, "chunk", NULL); fu_chunk_export(chk, FU_FIRMWARE_EXPORT_FLAG_ASCII_DATA, bc); } return xb_builder_node_export(bn, XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE | XB_NODE_EXPORT_FLAG_COLLAPSE_EMPTY | XB_NODE_EXPORT_FLAG_FORMAT_INDENT, NULL); } /** * fu_chunk_array_mutable_new: * @data: a mutable blob of memory * @data_sz: size of @data_sz * @addr_offset: the hardware address offset, or 0 * @page_sz: the hardware page size, or 0 * @packet_sz: the transfer size, or 0 * * Chunks a mutable blob of memory into packets, ensuring each packet does not * cross a package boundary and is less that a specific transfer size. * * Returns: (transfer container) (element-type FuChunk): array of packets * * Since: 1.5.6 **/ GPtrArray * fu_chunk_array_mutable_new(guint8 *data, gsize data_sz, gsize addr_offset, gsize page_sz, gsize packet_sz) { GPtrArray *chunks; g_return_val_if_fail(data != NULL, NULL); g_return_val_if_fail(data_sz > 0, NULL); chunks = fu_chunk_array_new(data, data_sz, addr_offset, page_sz, packet_sz); if (chunks == NULL) return NULL; for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); chk->is_mutable = TRUE; } return chunks; } /** * fu_chunk_array_new: * @data: (nullable): an optional linear blob of memory * @data_sz: size of @data_sz * @addr_offset: the hardware address offset, or 0 * @page_sz: the hardware page size, or 0 * @packet_sz: the transfer size, or 0 * * Chunks a linear blob of memory into packets, ensuring each packet does not * cross a package boundary and is less that a specific transfer size. * * Returns: (transfer container) (element-type FuChunk): array of packets * * Since: 1.1.2 **/ GPtrArray * fu_chunk_array_new(const guint8 *data, gsize data_sz, gsize addr_offset, gsize page_sz, gsize packet_sz) { GPtrArray *chunks = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); gsize offset = 0; g_return_val_if_fail(page_sz == 0 || page_sz >= packet_sz, NULL); while (offset < data_sz) { gsize chunksz = MIN(packet_sz, data_sz - offset); gsize page = 0; gsize address_offset = addr_offset + offset; /* if page_sz is not specified then all the pages are 0 */ if (page_sz > 0) { address_offset %= page_sz; page = (offset + addr_offset) / page_sz; } /* cut the packet so it does not straddle multiple blocks */ if (page_sz != packet_sz && page_sz > 0) chunksz = MIN(chunksz, (offset + packet_sz) % page_sz); g_ptr_array_add( chunks, fu_chunk_new(chunks->len, page, address_offset, data + offset, chunksz)); offset += chunksz; } #ifndef SUPPORTED_BUILD /* show the programmer a warning */ if (page_sz == 0x0 && chunks->len > 10000) { g_warning("fu_chunk_array_new() generated a lot of chunks (%u), " "maybe use FuChunkArray instead?", chunks->len); } #endif return chunks; } /* private */ gboolean fu_chunk_build(FuChunk *self, XbNode *n, GError **error) { guint64 tmp; g_autoptr(XbNode) data = NULL; g_return_val_if_fail(FU_IS_CHUNK(self), FALSE); g_return_val_if_fail(XB_IS_NODE(n), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* optional properties */ tmp = xb_node_query_text_as_uint(n, "idx", NULL); if (tmp != G_MAXUINT64) self->idx = tmp; tmp = xb_node_query_text_as_uint(n, "page", NULL); if (tmp != G_MAXUINT64) self->page = tmp; tmp = xb_node_query_text_as_uint(n, "addr", NULL); if (tmp != G_MAXUINT64) self->address = tmp; data = xb_node_query_first(n, "data", NULL); if (data != NULL && xb_node_get_text(data) != NULL) { gsize bufsz = 0; g_autofree guchar *buf = NULL; g_autoptr(GBytes) blob = NULL; buf = g_base64_decode(xb_node_get_text(data), &bufsz); blob = g_bytes_new(buf, bufsz); fu_chunk_set_bytes(self, blob); } else if (data != NULL) { g_autoptr(GBytes) blob = NULL; blob = g_bytes_new(NULL, 0); fu_chunk_set_bytes(self, blob); } /* success */ return TRUE; } static void fu_chunk_finalize(GObject *object) { FuChunk *self = FU_CHUNK(object); if (self->bytes != NULL) g_bytes_unref(self->bytes); G_OBJECT_CLASS(fu_chunk_parent_class)->finalize(object); } static void fu_chunk_class_init(FuChunkClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_chunk_finalize; } static void fu_chunk_init(FuChunk *self) { } fwupd-2.0.10/libfwupdplugin/fu-chunk.h000066400000000000000000000034141501337203100176400ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_CHUNK (fu_chunk_get_type()) G_DECLARE_FINAL_TYPE(FuChunk, fu_chunk, FU, CHUNK, GObject) /** * FU_CHUNK_PAGESZ_NONE: * * No page size is used. * * Since: 2.0.2 **/ #define FU_CHUNK_PAGESZ_NONE 0 /** * FU_CHUNK_ADDR_OFFSET_NONE: * * No address offset is used. * * Since: 2.0.2 **/ #define FU_CHUNK_ADDR_OFFSET_NONE 0 FuChunk * fu_chunk_bytes_new(GBytes *bytes); void fu_chunk_set_idx(FuChunk *self, guint idx) G_GNUC_NON_NULL(1); guint fu_chunk_get_idx(FuChunk *self) G_GNUC_NON_NULL(1); void fu_chunk_set_page(FuChunk *self, guint page) G_GNUC_NON_NULL(1); guint fu_chunk_get_page(FuChunk *self) G_GNUC_NON_NULL(1); void fu_chunk_set_address(FuChunk *self, gsize address) G_GNUC_NON_NULL(1); gsize fu_chunk_get_address(FuChunk *self) G_GNUC_NON_NULL(1); const guint8 * fu_chunk_get_data(FuChunk *self) G_GNUC_NON_NULL(1); guint8 * fu_chunk_get_data_out(FuChunk *self) G_GNUC_NON_NULL(1); gsize fu_chunk_get_data_sz(FuChunk *self) G_GNUC_NON_NULL(1); void fu_chunk_set_bytes(FuChunk *self, GBytes *bytes) G_GNUC_NON_NULL(1); GBytes * fu_chunk_get_bytes(FuChunk *self) G_GNUC_NON_NULL(1); FuChunk * fu_chunk_new(guint idx, guint page, gsize address, const guint8 *data, gsize data_sz); gchar * fu_chunk_to_string(FuChunk *self) G_GNUC_NON_NULL(1); gchar * fu_chunk_array_to_string(GPtrArray *chunks) G_GNUC_NON_NULL(1); GPtrArray * fu_chunk_array_new(const guint8 *data, gsize data_sz, gsize addr_offset, gsize page_sz, gsize packet_sz); GPtrArray * fu_chunk_array_mutable_new(guint8 *data, gsize data_sz, gsize addr_offset, gsize page_sz, gsize packet_sz) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-common-darwin.c000066400000000000000000000021721501337203100212750ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuCommon" #include "config.h" #include #include "fu-common-private.h" GPtrArray * fu_common_get_block_devices(GError **error) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "getting block devices is not supported on Darwin"); return NULL; } guint64 fu_common_get_memory_size_impl(void) { gint mib[] = {CTL_HW, HW_MEMSIZE}; gint64 physical_memory = 0; gsize length = sizeof(physical_memory); sysctl(mib, 2, &physical_memory, &length, NULL, 0); return (guint64)physical_memory; } gchar * fu_common_get_kernel_cmdline_impl(GError **error) { gchar cmdline[1024] = {0}; gsize cmdlinesz = sizeof(cmdline); sysctlbyname("kern.bootargs", cmdline, &cmdlinesz, NULL, 0); return g_strndup(cmdline, sizeof(cmdline)); } gchar * fu_common_get_olson_timezone_id_impl(GError **error) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "getting the Olson timezone ID is not supported on Darwin"); return NULL; } fwupd-2.0.10/libfwupdplugin/fu-common-freebsd.c000066400000000000000000000061031501337203100214210ustar00rootroot00000000000000/* * Copyright 2021 Sergii Dmytruk * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuCommon" #include "config.h" #include #include #include #include "fu-common-private.h" #include "fu-kenv.h" /* bsdisks doesn't provide Manager object */ #define UDISKS_DBUS_PATH "/org/freedesktop/UDisks2" #define UDISKS_DBUS_MANAGER_INTERFACE "org.freedesktop.DBus.ObjectManager" #define UDISKS_BLOCK_DEVICE_PATH "/org/freedesktop/UDisks2/block_devices/" GPtrArray * fu_common_get_block_devices(GError **error) { GVariant *ifaces; const size_t device_path_len = strlen(UDISKS_BLOCK_DEVICE_PATH); const gchar *obj; g_autoptr(GVariant) output = NULL; g_autoptr(GDBusProxy) proxy = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(GVariantIter) obj_iter = NULL; g_autoptr(GDBusConnection) connection = NULL; connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, error); if (connection == NULL) { g_prefix_error(error, "failed to get system bus: "); return NULL; } proxy = g_dbus_proxy_new_sync(connection, G_DBUS_PROXY_FLAGS_NONE, NULL, UDISKS_DBUS_SERVICE, UDISKS_DBUS_PATH, UDISKS_DBUS_MANAGER_INTERFACE, NULL, error); if (proxy == NULL) { g_prefix_error(error, "failed to find %s: ", UDISKS_DBUS_SERVICE); return NULL; } devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); output = g_dbus_proxy_call_sync(proxy, "GetManagedObjects", NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, error); if (output == NULL) { if (error != NULL) g_dbus_error_strip_remote_error(*error); g_prefix_error(error, "failed to call %s.%s(): ", UDISKS_DBUS_MANAGER_INTERFACE, "GetManagedObjects"); return NULL; } g_variant_get(output, "(a{oa{sa{sv}}})", &obj_iter); while (g_variant_iter_next(obj_iter, "{&o@a{sa{sv}}}", &obj, &ifaces)) { const gchar *iface; GVariant *props; GVariantIter iface_iter; if (strncmp(obj, UDISKS_BLOCK_DEVICE_PATH, device_path_len) != 0) continue; g_variant_iter_init(&iface_iter, ifaces); while (g_variant_iter_next(&iface_iter, "{&s@a{sv}}", &iface, &props)) { g_autoptr(GDBusProxy) proxy_blk = NULL; g_variant_unref(props); if (strcmp(iface, UDISKS_DBUS_INTERFACE_BLOCK) != 0) continue; proxy_blk = g_dbus_proxy_new_sync(connection, G_DBUS_PROXY_FLAGS_NONE, NULL, UDISKS_DBUS_SERVICE, obj, UDISKS_DBUS_INTERFACE_BLOCK, NULL, error); if (proxy_blk == NULL) { g_prefix_error(error, "failed to initialize d-bus proxy for %s: ", obj); return NULL; } g_ptr_array_add(devices, g_steal_pointer(&proxy_blk)); } g_variant_unref(ifaces); } return g_steal_pointer(&devices); } guint64 fu_common_get_memory_size_impl(void) { return (guint64)sysconf(_SC_PHYS_PAGES) * (guint64)sysconf(_SC_PAGE_SIZE); } gchar * fu_common_get_kernel_cmdline_impl(GError **error) { return fu_kenv_get_string("kernel_options", error); } fwupd-2.0.10/libfwupdplugin/fu-common-guid.c000066400000000000000000000012001501337203100207300ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuCommon" #include "config.h" #include "fu-common-guid.h" /** * fu_common_guid_is_plausible: * @buf: a buffer of data * * Checks whether a chunk of memory looks like it could be a GUID. * * Returns: TRUE if it looks like a GUID, FALSE if not * * Since: 1.2.5 **/ gboolean fu_common_guid_is_plausible(const guint8 *buf) { guint guint_sum = 0; for (guint i = 0; i < 16; i++) guint_sum += buf[i]; if (guint_sum == 0x00) return FALSE; if (guint_sum < 0xff) return FALSE; return TRUE; } fwupd-2.0.10/libfwupdplugin/fu-common-guid.h000066400000000000000000000003151501337203100207430ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include gboolean fu_common_guid_is_plausible(const guint8 *buf); fwupd-2.0.10/libfwupdplugin/fu-common-linux.c000066400000000000000000000135661501337203100211610ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuCommon" #include "config.h" #include #include #include "fu-common-private.h" #include "fu-kernel.h" #include "fu-path.h" #define UDISKS_DBUS_PATH "/org/freedesktop/UDisks2/Manager" #define UDISKS_DBUS_MANAGER_INTERFACE "org.freedesktop.UDisks2.Manager" GPtrArray * fu_common_get_block_devices(GError **error) { GVariantBuilder builder; const gchar *obj; g_autoptr(GVariant) output = NULL; g_autoptr(GDBusProxy) proxy = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(GVariantIter) obj_iter = NULL; g_autoptr(GDBusConnection) connection = NULL; connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, error); if (connection == NULL) { g_prefix_error(error, "failed to get system bus: "); return NULL; } proxy = g_dbus_proxy_new_sync(connection, G_DBUS_PROXY_FLAGS_NONE, NULL, UDISKS_DBUS_SERVICE, UDISKS_DBUS_PATH, UDISKS_DBUS_MANAGER_INTERFACE, NULL, error); if (proxy == NULL) { g_prefix_error(error, "failed to find %s: ", UDISKS_DBUS_SERVICE); return NULL; } devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); output = g_dbus_proxy_call_sync(proxy, "GetBlockDevices", g_variant_new("(a{sv})", &builder), G_DBUS_CALL_FLAGS_NONE, -1, NULL, error); if (output == NULL) { if (error != NULL) g_dbus_error_strip_remote_error(*error); g_prefix_error(error, "failed to call %s.%s(): ", UDISKS_DBUS_MANAGER_INTERFACE, "GetBlockDevices"); return NULL; } g_variant_get(output, "(ao)", &obj_iter); while (g_variant_iter_next(obj_iter, "&o", &obj)) { g_autoptr(GDBusProxy) proxy_blk = NULL; proxy_blk = g_dbus_proxy_new_sync(connection, G_DBUS_PROXY_FLAGS_NONE, NULL, UDISKS_DBUS_SERVICE, obj, UDISKS_DBUS_INTERFACE_BLOCK, NULL, error); if (proxy_blk == NULL) { g_prefix_error(error, "failed to initialize d-bus proxy for %s: ", obj); return NULL; } g_ptr_array_add(devices, g_steal_pointer(&proxy_blk)); } return g_steal_pointer(&devices); } guint64 fu_common_get_memory_size_impl(void) { glong phys_pages = sysconf(_SC_PHYS_PAGES); glong page_size = sysconf(_SC_PAGE_SIZE); if (phys_pages > 0 && page_size > 0) return (guint64)phys_pages * (guint64)page_size; return 0; } gchar * fu_common_get_kernel_cmdline_impl(GError **error) { GHashTableIter iter; gpointer key; gpointer value; g_autoptr(GHashTable) hash = NULL; g_autoptr(GString) cmdline_safe = g_string_new(NULL); const gchar *ignore[] = { "", "apparmor", "audit", "auto", "boot", "BOOT_IMAGE", "console", "crashkernel", "cryptdevice", "cryptkey", "dm", "earlycon", "earlyprintk", "ether", "initrd", "ip", "LANG", "loglevel", "luks.key", "luks.name", "luks.options", "luks.uuid", "mitigations", "mount.usr", "mount.usrflags", "mount.usrfstype", "netdev", "netroot", "nfsaddrs", "nfs.nfs4_unique_id", "nfsroot", "noplymouth", "ostree", "quiet", "rd.dm.uuid", "rd.luks.allow-discards", "rd.luks.key", "rd.luks.name", "rd.luks.options", "rd.luks.uuid", "rd.lvm.lv", "rd.lvm.vg", "rd.md.uuid", "rd.systemd.mask", "rd.systemd.wants", "resume", "resumeflags", "rhgb", "ro", "root", "rootflags", "roothash", "rw", "security", "showopts", "splash", "swap", "systemd.mask", "systemd.show_status", "systemd.unit", "systemd.verity_root_data", "systemd.verity_root_hash", "systemd.wants", "udev.log_priority", "verbose", "vt.handoff", "zfs", NULL, /* last entry */ }; /* get a PII-safe kernel command line */ hash = fu_kernel_get_cmdline(error); if (hash == NULL) return NULL; for (guint i = 0; ignore[i] != NULL; i++) g_hash_table_remove(hash, ignore[i]); g_hash_table_iter_init(&iter, hash); while (g_hash_table_iter_next(&iter, &key, &value)) { if (cmdline_safe->len > 0) g_string_append(cmdline_safe, " "); if (value == NULL) { g_string_append(cmdline_safe, (gchar *)key); continue; } g_string_append_printf(cmdline_safe, "%s=%s", (gchar *)key, (gchar *)value); } return g_string_free(g_steal_pointer(&cmdline_safe), FALSE); } gchar * fu_common_get_olson_timezone_id_impl(GError **error) { g_autofree gchar *fn_localtime = fu_path_from_kind(FU_PATH_KIND_LOCALTIME); g_autoptr(GFile) file_localtime = g_file_new_for_path(fn_localtime); /* use the last two sections of the symlink target */ g_debug("looking for timezone file %s", fn_localtime); if (g_file_query_file_type(file_localtime, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL) == G_FILE_TYPE_SYMBOLIC_LINK) { const gchar *target; g_autoptr(GFileInfo) info = NULL; info = g_file_query_info(file_localtime, G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, error); if (info == NULL) return NULL; target = g_file_info_get_symlink_target(info); if (target != NULL) { g_auto(GStrv) sections = g_strsplit(target, "/", -1); guint sections_len = g_strv_length(sections); if (sections_len < 2) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid symlink target: %s", target); return NULL; } return g_strdup_printf("%s/%s", sections[sections_len - 2], sections[sections_len - 1]); } } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no timezone or localtime is available"); return NULL; } fwupd-2.0.10/libfwupdplugin/fu-common-private.h000066400000000000000000000012171501337203100214670ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-common.h" #define UDISKS_DBUS_SERVICE "org.freedesktop.UDisks2" #define UDISKS_DBUS_INTERFACE_PARTITION "org.freedesktop.UDisks2.Partition" #define UDISKS_DBUS_INTERFACE_FILESYSTEM "org.freedesktop.UDisks2.Filesystem" #define UDISKS_DBUS_INTERFACE_BLOCK "org.freedesktop.UDisks2.Block" GPtrArray * fu_common_get_block_devices(GError **error); guint64 fu_common_get_memory_size_impl(void); gchar * fu_common_get_kernel_cmdline_impl(GError **error); gchar * fu_common_get_olson_timezone_id_impl(GError **error); fwupd-2.0.10/libfwupdplugin/fu-common-windows.c000066400000000000000000000163531501337203100215110ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuCommon" #include "config.h" #include #include #include #include #include "fu-common-private.h" GPtrArray * fu_common_get_block_devices(GError **error) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "getting block devices is not supported on Windows"); return NULL; } guint64 fu_common_get_memory_size_impl(void) { MEMORYSTATUSEX status; status.dwLength = sizeof(status); GlobalMemoryStatusEx(&status); return (guint64)status.ullTotalPhys; } gchar * fu_common_get_kernel_cmdline_impl(GError **error) { return g_strdup(""); } static gchar * fu_common_convert_tzinfo_to_olson_id(const gchar *tzinfo, GError **error) { struct { const gchar *tzinfo; const gchar *olson_id; } map[] = {{"Afghanistan", "Asia/Kabul"}, {"Alaskan", "America/Anchorage"}, {"Aleutian", "America/Adak"}, {"Altai", "Asia/Barnaul"}, {"Arab", "Asia/Riyadh"}, {"Arabian", "Asia/Dubai"}, {"Arabic", "Asia/Baghdad"}, {"Argentina", "America/Buenos_Aires"}, {"Astrakhan", "Europe/Astrakhan"}, {"Atlantic", "America/Halifax"}, {"AUS Central", "Australia/Darwin"}, {"Aus Central W.", "Australia/Eucla"}, {"AUS Eastern", "Australia/Sydney"}, {"Azerbaijan", "Asia/Baku"}, {"Azores", "Atlantic/Azores"}, {"Bahia", "America/Bahia"}, {"Bangladesh", "Asia/Dhaka"}, {"Belarus", "Europe/Minsk"}, {"Bougainville", "Pacific/Bougainville"}, {"Canada Central", "America/Regina"}, {"Cape Verde", "Atlantic/Cape_Verde"}, {"Caucasus", "Asia/Yerevan"}, {"Cen. Australia", "Australia/Adelaide"}, {"Central America", "America/Guatemala"}, {"Central", "America/Chicago"}, {"Central Asia", "Asia/Almaty"}, {"Central Brazilian", "America/Cuiaba"}, {"Central European", "Europe/Warsaw"}, {"Central Europe", "Europe/Budapest"}, {"Central Pacific", "Pacific/Guadalcanal"}, {"Central Standard Time (Mexico)", "America/Mexico_City"}, {"Chatham Islands", "Pacific/Chatham"}, {"China", "Asia/Shanghai"}, {"Cuba", "America/Havana"}, {"Dateline", "Etc/GMT+12"}, {"E. Africa", "Africa/Nairobi"}, {"Easter Island", "Pacific/Easter"}, {"Eastern", "America/New_York"}, {"Eastern Standard Time (Mexico)", "America/Cancun"}, {"E. Australia", "Australia/Brisbane"}, {"E. Europe", "Europe/Chisinau"}, {"Egypt", "Africa/Cairo"}, {"Ekaterinburg", "Asia/Yekaterinburg"}, {"E. South America", "America/Sao_Paulo"}, {"Fiji", "Pacific/Fiji"}, {"FILE", "Europe/Kiev"}, {"Georgian", "Asia/Tbilisi"}, {"GMT", "Europe/London"}, {"Greenland", "America/Godthab"}, {"Greenwich", "Atlantic/Reykjavik"}, {"GTB", "Europe/Bucharest"}, {"Haiti", "America/Port-au-Prince"}, {"Hawaiian", "Pacific/Honolulu"}, {"India", "Asia/Calcutta"}, {"Iran", "Asia/Tehran"}, {"Israel", "Asia/Jerusalem"}, {"Jordan", "Asia/Amman"}, {"Kaliningrad", "Europe/Kaliningrad"}, {"Korea", "Asia/Seoul"}, {"Libya", "Africa/Tripoli"}, {"Line Islands", "Pacific/Kiritimati"}, {"Lord Howe", "Australia/Lord_Howe"}, {"Magadan", "Asia/Magadan"}, {"Magallanes", "America/Punta_Arenas"}, {"Marquesas", "Pacific/Marquesas"}, {"Mauritius", "Indian/Mauritius"}, {"Middle East", "Asia/Beirut"}, {"Montevideo", "America/Montevideo"}, {"Morocco", "Africa/Casablanca"}, {"Mountain", "America/Denver"}, {"Mountain Standard Time (Mexico)", "America/Mazatlan"}, {"Myanmar", "Asia/Rangoon"}, {"Namibia", "Africa/Windhoek"}, {"N. Central Asia", "Asia/Novosibirsk"}, {"Nepal", "Asia/Katmandu"}, {"Newfoundland", "America/St_Johns"}, {"New Zealand", "Pacific/Auckland"}, {"Norfolk", "Pacific/Norfolk"}, {"North Asia", "Asia/Krasnoyarsk"}, {"North Asia East", "Asia/Irkutsk"}, {"North Korea", "Asia/Pyongyang"}, {"Omsk", "Asia/Omsk"}, {"Pacific", "America/Los_Angeles"}, {"Pacific SA", "America/Santiago"}, {"Pacific Standard Time (Mexico)", "America/Tijuana"}, {"Pakistan", "Asia/Karachi"}, {"Paraguay", "America/Asuncion"}, {"Qyzylorda", "Asia/Qyzylorda"}, {"Romance", "Europe/Paris"}, {"Russian", "Europe/Moscow"}, {"Russia Time Zone 10", "Asia/Srednekolymsk"}, {"Russia Time Zone 11", "Asia/Kamchatka"}, {"Russia Time Zone 3", "Europe/Samara"}, {"SA Eastern", "America/Cayenne"}, {"Saint Pierre", "America/Miquelon"}, {"Sakhalin", "Asia/Sakhalin"}, {"Samoa", "Pacific/Apia"}, {"Sao Tome", "Africa/Sao_Tome"}, {"SA Pacific", "America/Bogota"}, {"Saratov", "Europe/Saratov"}, {"SA Western", "America/La_Paz"}, {"SE Asia", "Asia/Bangkok"}, {"Singapore", "Asia/Singapore"}, {"South Africa", "Africa/Johannesburg"}, {"South Sudan", "Africa/Juba"}, {"Sri Lanka", "Asia/Colombo"}, {"Sudan", "Africa/Khartoum"}, {"Syria", "Asia/Damascus"}, {"Taipei", "Asia/Taipei"}, {"Tasmania", "Australia/Hobart"}, {"Tocantins", "America/Araguaina"}, {"Tokyo", "Asia/Tokyo"}, {"Tomsk", "Asia/Tomsk"}, {"Tonga", "Pacific/Tongatapu"}, {"Transbaikal", "Asia/Chita"}, {"Turkey", "Europe/Istanbul"}, {"Turks And Caicos", "America/Grand_Turk"}, {"Ulaanbaatar", "Asia/Ulaanbaatar"}, {"US Eastern", "America/Indianapolis"}, {"US Mountain", "America/Phoenix"}, {"UTC-02", "Etc/GMT+2"}, {"UTC-08", "Etc/GMT+8"}, {"UTC-09", "Etc/GMT+9"}, {"UTC-11", "Etc/GMT+11"}, {"UTC+12", "Etc/GMT-12"}, {"UTC+13", "Etc/GMT-13"}, {"UTC", "Etc/UTC"}, {"Venezuela", "America/Caracas"}, {"Vladivostok", "Asia/Vladivostok"}, {"Volgograd", "Europe/Volgograd"}, {"W. Australia", "Australia/Perth"}, {"W. Central Africa", "Africa/Lagos"}, {"West Asia", "Asia/Tashkent"}, {"West Bank", "Asia/Hebron"}, {"West Pacific", "Pacific/Port_Moresby"}, {"W. Europe", "Europe/Berlin"}, {"W. Mongolia", "Asia/Hovd"}, {"Yakutsk", "Asia/Yakutsk"}, {"Yukon", "America/Whitehorse"}, {NULL, NULL}}; for (guint i = 0; map[i].tzinfo != NULL; i++) { if (g_strcmp0(tzinfo, map[i].tzinfo) == 0) return g_strdup(map[i].olson_id); } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot map tzinfo '%s' to Olson ID", tzinfo); return NULL; } gchar * fu_common_get_olson_timezone_id_impl(GError **error) { DWORD rc; TIME_ZONE_INFORMATION tzinfo = {0}; gchar *suffix; g_autofree gchar *name = NULL; rc = GetTimeZoneInformation(&tzinfo); if (rc == TIME_ZONE_ID_INVALID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot get timezone information [%u]", (guint)GetLastError()); return NULL; } name = g_utf16_to_utf8(tzinfo.StandardName, -1, NULL, NULL, error); if (name == NULL) { g_prefix_error(error, "cannot convert timezone name to UTF-8: "); return NULL; } /* make the lookup key shorter, then convert */ suffix = g_strstr_len(name, -1, " Standard Time"); if (suffix != NULL) *suffix = '\0'; return fu_common_convert_tzinfo_to_olson_id(name, error); } fwupd-2.0.10/libfwupdplugin/fu-common.c000066400000000000000000000166451501337203100200250ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuCommon" #include "config.h" #ifdef HAVE_CPUID_H #include #endif #include "fu-common-private.h" #include "fu-firmware.h" #include "fu-path.h" #include "fu-string.h" /** * fu_cpuid: * @leaf: the CPUID level, now called the 'leaf' by Intel * @eax: (out) (nullable): EAX register * @ebx: (out) (nullable): EBX register * @ecx: (out) (nullable): ECX register * @edx: (out) (nullable): EDX register * @error: (nullable): optional return location for an error * * Calls CPUID and returns the registers for the given leaf. * * Returns: %TRUE if the registers are set. * * Since: 1.8.2 **/ gboolean fu_cpuid(guint32 leaf, guint32 *eax, guint32 *ebx, guint32 *ecx, guint32 *edx, GError **error) { #ifdef HAVE_CPUID_H guint eax_tmp = 0; guint ebx_tmp = 0; guint ecx_tmp = 0; guint edx_tmp = 0; g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* get vendor */ __get_cpuid_count(leaf, 0x0, &eax_tmp, &ebx_tmp, &ecx_tmp, &edx_tmp); if (eax != NULL) *eax = eax_tmp; if (ebx != NULL) *ebx = ebx_tmp; if (ecx != NULL) *ecx = ecx_tmp; if (edx != NULL) *edx = edx_tmp; return TRUE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no support"); return FALSE; #endif } /** * fu_cpu_get_attrs: * @error: (nullable): optional return location for an error * * Gets attributes for the first CPU listed in `/proc/cpuinfo`. * * Returns: (element-type utf8 utf8) (transfer full): CPU attributes * * Since: 2.0.7 **/ GHashTable * fu_cpu_get_attrs(GError **error) { gsize bufsz = 0; g_autofree gchar *buf = NULL; g_autofree gchar *procpath = fu_path_from_kind(FU_PATH_KIND_PROCFS); g_autofree gchar *fn = g_build_filename(procpath, "cpuinfo", NULL); g_autoptr(GHashTable) hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); g_return_val_if_fail(error == NULL || *error == NULL, NULL); if (!g_file_get_contents(fn, &buf, &bufsz, error)) return NULL; if (bufsz > 0) { g_auto(GStrv) lines = fu_strsplit(buf, bufsz, "\n", -1); for (guint i = 0; lines[i] != NULL; i++) { g_auto(GStrv) tokens = NULL; if (lines[i][0] == '\0') break; tokens = g_strsplit(lines[i], ": ", 2); for (guint j = 0; tokens[j] != NULL; j++) { g_hash_table_insert(hash, fu_strstrip(tokens[0]), g_strdup(tokens[1])); } } } /* success */ return g_steal_pointer(&hash); } /** * fu_cpu_get_vendor: * * Uses CPUID to discover the CPU vendor. * * Returns: a CPU vendor, e.g. %FU_CPU_VENDOR_AMD if the vendor was AMD. * * Since: 1.8.2 **/ FuCpuVendor fu_cpu_get_vendor(void) { #ifdef HAVE_CPUID_H guint ebx = 0; guint ecx = 0; guint edx = 0; if (fu_cpuid(0x0, NULL, &ebx, &ecx, &edx, NULL)) { if (ebx == signature_INTEL_ebx && edx == signature_INTEL_edx && ecx == signature_INTEL_ecx) { return FU_CPU_VENDOR_INTEL; } if (ebx == signature_AMD_ebx && edx == signature_AMD_edx && ecx == signature_AMD_ecx) { return FU_CPU_VENDOR_AMD; } } #endif /* failed */ return FU_CPU_VENDOR_UNKNOWN; } /** * fu_common_get_memory_size: * * Returns the size of physical memory. * * Returns: bytes * * Since: 1.5.6 **/ guint64 fu_common_get_memory_size(void) { return fu_common_get_memory_size_impl(); } /** * fu_common_get_kernel_cmdline: * @error: (nullable): optional return location for an error * * Returns the current kernel command line options. * * Returns: options as a string, or %NULL on error * * Since: 1.5.6 **/ gchar * fu_common_get_kernel_cmdline(GError **error) { return fu_common_get_kernel_cmdline_impl(error); } /** * fu_common_get_olson_timezone_id: * @error: (nullable): optional return location for an error * * Gets the system Olson timezone ID, as used in the CLDR and ICU specifications. * * Returns: timezone string, e.g. `Europe/London` or %NULL on error * * Since: 1.9.7 **/ gchar * fu_common_get_olson_timezone_id(GError **error) { return fu_common_get_olson_timezone_id_impl(error); } /** * fu_common_align_up: * @value: value to align * @alignment: align to this power of 2, where 0x1F is the maximum value of 2GB * * Align a value to a power of 2 boundary, where @alignment is the bit position * to align to. If @alignment is zero then @value is always returned unchanged. * * Returns: aligned value, which will be the same as @value if already aligned, * or %G_MAXSIZE if the value would overflow * * Since: 1.6.0 **/ gsize fu_common_align_up(gsize value, guint8 alignment) { gsize value_new; gsize mask = (gsize)1 << alignment; g_return_val_if_fail(alignment <= FU_FIRMWARE_ALIGNMENT_2G, G_MAXSIZE); /* no alignment required */ if ((value & (mask - 1)) == 0) return value; /* increment up to the next alignment value */ value_new = value + mask; value_new &= ~(mask - 1); /* overflow */ if (value_new < value) return G_MAXSIZE; /* success */ return value_new; } /** * fu_power_state_is_ac: * @power_state: a power state, e.g. %FU_POWER_STATE_AC_FULLY_CHARGED * * Determines if the power state can be considered "on AC power". * * Returns: %TRUE for success * * Since: 1.8.11 **/ gboolean fu_power_state_is_ac(FuPowerState power_state) { return power_state != FU_POWER_STATE_BATTERY; } /** * fu_error_convert: * @perror: (nullable): A #GError, perhaps with domain #GIOError * * Convert the error to a #FwupdError, if required. * * Since: 2.0.0 **/ void fu_error_convert(GError **perror) { GError *error = (perror != NULL) ? *perror : NULL; /* sanity check */ if (error == NULL) return; /* convert GIOError and GFileError */ fwupd_error_convert(perror); if (error->domain == FWUPD_ERROR) return; #ifndef SUPPORTED_BUILD /* fallback */ g_critical("GError %s:%i sending over D-Bus was not converted to FwupdError", g_quark_to_string(error->domain), error->code); #endif error->domain = FWUPD_ERROR; error->code = FWUPD_ERROR_INTERNAL; } /** * fu_xmlb_builder_insert_kv: * @bn: #XbBuilderNode * @key: string key * @value: string value * * Convenience function to add an XML node with a string value. If @value is %NULL * then no member is added. * * Since: 1.6.0 **/ void fu_xmlb_builder_insert_kv(XbBuilderNode *bn, const gchar *key, const gchar *value) { if (value == NULL) return; xb_builder_node_insert_text(bn, key, value, NULL); } /** * fu_xmlb_builder_insert_kx: * @bn: #XbBuilderNode * @key: string key * @value: integer value * * Convenience function to add an XML node with an integer value. If @value is 0 * then no member is added. * * Since: 1.6.0 **/ void fu_xmlb_builder_insert_kx(XbBuilderNode *bn, const gchar *key, guint64 value) { g_autofree gchar *value_hex = NULL; if (value == 0) return; value_hex = g_strdup_printf("0x%x", (guint)value); xb_builder_node_insert_text(bn, key, value_hex, NULL); } /** * fu_xmlb_builder_insert_kb: * @bn: #XbBuilderNode * @key: string key * @value: boolean value * * Convenience function to add an XML node with a boolean value. * * Since: 1.6.0 **/ void fu_xmlb_builder_insert_kb(XbBuilderNode *bn, const gchar *key, gboolean value) { xb_builder_node_insert_text(bn, key, value ? "true" : "false", NULL); } /** * fu_snap_is_in_snap: * * Check whether the current process is running inside a snap. * * Returns: TRUE if current process is running inside a snap. * * Since: 2.0.4 **/ gboolean fu_snap_is_in_snap(void) { return getenv("SNAP") != NULL; } fwupd-2.0.10/libfwupdplugin/fu-common.h000066400000000000000000000052571501337203100200270ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include #include "fu-common-struct.h" /** * FuCpuVendor: * * The CPU vendor. **/ /** * FuPowerState: * * The system power state. * * This does not have to be exactly what the battery is doing, but is supposed to represent the * 40,000ft view of the system power state. * * For example, it is perfectly correct to set %FU_POWER_STATE_AC if the system is connected to * AC power, but the battery cells are discharging for health or for other performance reasons. **/ /** * FuLidState: * * The device lid state. **/ /** * FuDisplayState: * * The device lid state. **/ /** * FU_BIT_SET: * @val: integer value * @pos: bit position, where 0 is the least significant byte * * Sets a bit in a value using a bitwise operation. * * Since: 2.0.0 **/ #define FU_BIT_SET(val, pos) (val |= (1ull << (pos))) /* nocheck:blocked */ /** * FU_BIT_CLEAR: * @val: integer value * @pos: bit position, where 0 is the least significant byte * * Clears a bit in a value using a bitwise operation. * * Since: 2.0.0 **/ #define FU_BIT_CLEAR(val, pos) (val &= ~(1ull << (pos))) /* nocheck:blocked */ /** * FU_BIT_IS_SET: * @val: integer value * @pos: bit position, where 0 is the least significant byte * * Checks a bit in a value using a bitwise operation. * * Returns: %TRUE if the bit is set. * * Since: 2.0.0 **/ #define FU_BIT_IS_SET(val, pos) (val & (1ull << (pos))) /** * FU_BIT_IS_CLEAR: * @val: integer value * @pos: bit position, where 0 is the least significant byte * * Checks a bit in a value using a bitwise operation. * * Returns: %TRUE if the bit is clear. * * Since: 2.0.0 **/ #define FU_BIT_IS_CLEAR(val, pos) (!FU_BIT_IS_SET(val, (pos))) gboolean fu_cpuid(guint32 leaf, guint32 *eax, guint32 *ebx, guint32 *ecx, guint32 *edx, GError **error) G_GNUC_WARN_UNUSED_RESULT; FuCpuVendor fu_cpu_get_vendor(void); GHashTable * fu_cpu_get_attrs(GError **error); guint64 fu_common_get_memory_size(void); gchar * fu_common_get_kernel_cmdline(GError **error); gchar * fu_common_get_olson_timezone_id(GError **error); gsize fu_common_align_up(gsize value, guint8 alignment); gboolean fu_power_state_is_ac(FuPowerState power_state); void fu_error_convert(GError **perror); void fu_xmlb_builder_insert_kv(XbBuilderNode *bn, const gchar *key, const gchar *value) G_GNUC_NON_NULL(1); void fu_xmlb_builder_insert_kx(XbBuilderNode *bn, const gchar *key, guint64 value) G_GNUC_NON_NULL(1); void fu_xmlb_builder_insert_kb(XbBuilderNode *bn, const gchar *key, gboolean value) G_GNUC_NON_NULL(1); gboolean fu_snap_is_in_snap(void); fwupd-2.0.10/libfwupdplugin/fu-common.rs000066400000000000000000000007431501337203100202170ustar00rootroot00000000000000// Copyright 2024 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later enum FuCpuVendor { Unknown, Intel, Amd, } #[derive(ToString)] enum FuPowerState { Unknown, Ac, // On AC power Battery, // On system battery } #[derive(ToString)] enum FuLidState { Unknown, Open, Closed, } #[derive(ToString, FromString)] enum FuDisplayState { Unknown, Connected, Disconnected, } fwupd-2.0.10/libfwupdplugin/fu-composite-input-stream.c000066400000000000000000000232601501337203100231540ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuCompositeInputStream" #include "config.h" #include "fwupd-codec.h" #include "fu-composite-input-stream.h" #include "fu-input-stream.h" #include "fu-partial-input-stream-private.h" /** * FuCompositeInputStream: * * A input stream that is made up of other partial streams, e.g. * * off sz off sz * [xxxxxxxxxxxx] [yyyyyyyy] * | 0x6 | |0x4| * \ \ / / * \ \ / / * \ | | * | | | * [xxxxxxyyyyyy] * * xxx offset: 2, sz: 6 * yyy offset: 0, sz: 4 */ typedef struct { FuPartialInputStream *partial_stream; gsize global_offset; } FuCompositeInputStreamItem; struct _FuCompositeInputStream { GInputStream parent_instance; GPtrArray *items; /* of FuCompositeInputStreamItem */ FuCompositeInputStreamItem *last_item; /* no-ref */ goffset pos; goffset pos_offset; gsize total_size; }; static void fu_composite_input_stream_seekable_iface_init(GSeekableIface *iface); static void fu_composite_input_stream_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_WITH_CODE(FuCompositeInputStream, fu_composite_input_stream, G_TYPE_INPUT_STREAM, G_IMPLEMENT_INTERFACE(G_TYPE_SEEKABLE, fu_composite_input_stream_seekable_iface_init) G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fu_composite_input_stream_codec_iface_init)) static void fu_composite_input_stream_add_string(FwupdCodec *codec, guint idt, GString *str) { FuCompositeInputStream *self = FU_COMPOSITE_INPUT_STREAM(codec); fwupd_codec_string_append_hex(str, idt, "Pos", self->pos); fwupd_codec_string_append_hex(str, idt, "PosOffset", self->pos_offset); fwupd_codec_string_append_hex(str, idt, "TotalSize", self->total_size); for (guint i = 0; i < self->items->len; i++) { FuCompositeInputStreamItem *item = g_ptr_array_index(self->items, i); fwupd_codec_add_string(FWUPD_CODEC(item->partial_stream), idt, str); fwupd_codec_string_append_hex(str, idt + 1, "GlobalOffset", item->global_offset); } } static void fu_composite_input_stream_codec_iface_init(FwupdCodecInterface *iface) { iface->add_string = fu_composite_input_stream_add_string; } static void fu_composite_input_stream_item_free(FuCompositeInputStreamItem *item) { g_object_unref(item->partial_stream); g_free(item); } /** * fu_composite_input_stream_add_bytes: * @self: a #FuCompositeInputStream * @bytes: a #GBytes * * Adds a bytes object. * * Since: 2.0.0 **/ void fu_composite_input_stream_add_bytes(FuCompositeInputStream *self, GBytes *bytes) { g_autoptr(GInputStream) stream = NULL; g_autoptr(GInputStream) partial_stream = NULL; g_return_if_fail(FU_IS_COMPOSITE_INPUT_STREAM(self)); g_return_if_fail(bytes != NULL); stream = g_memory_input_stream_new_from_bytes(bytes); partial_stream = fu_partial_input_stream_new(stream, 0x0, g_bytes_get_size(bytes), NULL); fu_composite_input_stream_add_partial_stream(self, FU_PARTIAL_INPUT_STREAM(partial_stream)); } /** * fu_composite_input_stream_add_partial_stream: * @self: a #FuCompositeInputStream * @partial_stream: a #FuPartialInputStream * * Adds a partial stream object. * * Since: 2.0.0 **/ void fu_composite_input_stream_add_partial_stream(FuCompositeInputStream *self, FuPartialInputStream *partial_stream) { FuCompositeInputStreamItem *item; gsize global_offset = 0; g_return_if_fail(FU_IS_COMPOSITE_INPUT_STREAM(self)); g_return_if_fail(FU_IS_PARTIAL_INPUT_STREAM(partial_stream)); g_return_if_fail(G_INPUT_STREAM(self) != G_INPUT_STREAM(partial_stream)); /* get the last-added item */ if (self->items->len > 0) { FuCompositeInputStreamItem *item_last = g_ptr_array_index(self->items, self->items->len - 1); global_offset = item_last->global_offset + fu_partial_input_stream_get_size( FU_PARTIAL_INPUT_STREAM(item_last->partial_stream)); } /* add a new item */ item = g_new0(FuCompositeInputStreamItem, 1); item->partial_stream = g_object_ref(partial_stream); item->global_offset = global_offset; g_debug("adding partial stream global_offset:0x%x", (guint)item->global_offset); self->total_size += fu_partial_input_stream_get_size(item->partial_stream); g_ptr_array_add(self->items, item); } /** * fu_composite_input_stream_add_stream: * @self: a #FuCompositeInputStream * @stream: a #GInputStream * @error: (nullable): optional return location for an error * * Adds a input stream object, which has to be seekable. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fu_composite_input_stream_add_stream(FuCompositeInputStream *self, GInputStream *stream, GError **error) { g_autoptr(GInputStream) partial_stream = NULL; g_return_val_if_fail(FU_IS_COMPOSITE_INPUT_STREAM(self), FALSE); g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(G_INPUT_STREAM(self) != stream, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* create a partial stream that is actually the size of the entire GInputStream */ partial_stream = fu_partial_input_stream_new(stream, 0x0, G_MAXSIZE, error); if (partial_stream == NULL) { g_prefix_error(error, "failed to add input stream: "); return FALSE; } fu_composite_input_stream_add_partial_stream(self, FU_PARTIAL_INPUT_STREAM(partial_stream)); /* success */ return TRUE; } static goffset fu_composite_input_stream_tell(GSeekable *seekable) { FuCompositeInputStream *self = FU_COMPOSITE_INPUT_STREAM(seekable); g_return_val_if_fail(FU_IS_COMPOSITE_INPUT_STREAM(self), -1); return self->pos; } static gboolean fu_composite_input_stream_can_seek(GSeekable *seekable) { return TRUE; } static FuCompositeInputStreamItem * fu_composite_input_stream_get_item_for_offset(FuCompositeInputStream *self, gsize offset, GError **error) { for (guint i = 0; i < self->items->len; i++) { FuCompositeInputStreamItem *item = g_ptr_array_index(self->items, i); gsize item_size = fu_partial_input_stream_get_size(item->partial_stream); if (offset < item->global_offset + item_size) return item; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "offset is 0x%x out of range", (guint)offset); return NULL; } static gboolean fu_composite_input_stream_seek(GSeekable *seekable, goffset offset, GSeekType type, GCancellable *cancellable, GError **error) { FuCompositeInputStream *self = FU_COMPOSITE_INPUT_STREAM(seekable); g_return_val_if_fail(FU_IS_COMPOSITE_INPUT_STREAM(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* reset */ self->pos_offset = 0; self->last_item = NULL; if (type == G_SEEK_CUR) { self->pos += offset; } else if (type == G_SEEK_END) { self->pos = self->total_size + offset; } else { self->pos = offset; } return TRUE; } static gboolean fu_composite_input_stream_can_truncate(GSeekable *seekable) { return FALSE; } static gboolean fu_composite_input_stream_truncate(GSeekable *seekable, goffset offset, GCancellable *cancellable, GError **error) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot truncate FuCompositeInputStream"); return FALSE; } static void fu_composite_input_stream_seekable_iface_init(GSeekableIface *iface) { iface->tell = fu_composite_input_stream_tell; iface->can_seek = fu_composite_input_stream_can_seek; iface->seek = fu_composite_input_stream_seek; iface->can_truncate = fu_composite_input_stream_can_truncate; iface->truncate_fn = fu_composite_input_stream_truncate; } /** * fu_composite_input_stream_new: * * Creates a composite input stream. * * Returns: (transfer full): a #FuCompositeInputStream * * Since: 2.0.0 **/ GInputStream * fu_composite_input_stream_new(void) { return G_INPUT_STREAM(g_object_new(FU_TYPE_COMPOSITE_INPUT_STREAM, NULL)); } static gssize fu_composite_input_stream_read(GInputStream *stream, void *buffer, gsize count, GCancellable *cancellable, GError **error) { FuCompositeInputStream *self = FU_COMPOSITE_INPUT_STREAM(stream); FuCompositeInputStreamItem *item; gssize rc; g_return_val_if_fail(FU_IS_COMPOSITE_INPUT_STREAM(self), -1); g_return_val_if_fail(error == NULL || *error == NULL, -1); item = fu_composite_input_stream_get_item_for_offset(self, self->pos + self->pos_offset, NULL); if (item == NULL) return 0; if (item != self->last_item) { if (!g_seekable_seek(G_SEEKABLE(item->partial_stream), self->pos + self->pos_offset - item->global_offset, G_SEEK_SET, cancellable, error)) return -1; self->last_item = item; } rc = g_input_stream_read(G_INPUT_STREAM(item->partial_stream), buffer, count, cancellable, error); if (rc < 0) return rc; /* we have to keep track of this in case we have to switch the FuCompositeInputStreamItem * without an explicit seek */ self->pos += rc; return rc; } static void fu_composite_input_stream_finalize(GObject *object) { FuCompositeInputStream *self = FU_COMPOSITE_INPUT_STREAM(object); g_ptr_array_unref(self->items); G_OBJECT_CLASS(fu_composite_input_stream_parent_class)->finalize(object); } static void fu_composite_input_stream_class_init(FuCompositeInputStreamClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); GInputStreamClass *istream_class = G_INPUT_STREAM_CLASS(klass); istream_class->read_fn = fu_composite_input_stream_read; object_class->finalize = fu_composite_input_stream_finalize; } static void fu_composite_input_stream_init(FuCompositeInputStream *self) { self->items = g_ptr_array_new_with_free_func((GDestroyNotify)fu_composite_input_stream_item_free); } fwupd-2.0.10/libfwupdplugin/fu-composite-input-stream.h000066400000000000000000000015351501337203100231620ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-partial-input-stream.h" #define FU_TYPE_COMPOSITE_INPUT_STREAM (fu_composite_input_stream_get_type()) G_DECLARE_FINAL_TYPE(FuCompositeInputStream, fu_composite_input_stream, FU, COMPOSITE_INPUT_STREAM, GInputStream) GInputStream * fu_composite_input_stream_new(void); void fu_composite_input_stream_add_bytes(FuCompositeInputStream *self, GBytes *bytes) G_GNUC_NON_NULL(1, 2); void fu_composite_input_stream_add_partial_stream(FuCompositeInputStream *self, FuPartialInputStream *partial_stream) G_GNUC_NON_NULL(1, 2); gboolean fu_composite_input_stream_add_stream(FuCompositeInputStream *self, GInputStream *stream, GError **error) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-config-private.h000066400000000000000000000005651501337203100214510ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-config.h" FuConfig * fu_config_new(void); gboolean fu_config_load(FuConfig *self, GError **error) G_GNUC_NON_NULL(1); gboolean fu_config_reset_defaults(FuConfig *self, const gchar *section, GError **error) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-config.c000066400000000000000000000565301501337203100177770ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuConfig" #include "config.h" #include #include #include #include #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-config-private.h" #include "fu-path.h" #include "fu-string.h" enum { SIGNAL_CHANGED, SIGNAL_LOADED, SIGNAL_LAST }; static guint signals[SIGNAL_LAST] = {0}; #define FU_CONFIG_FILE_MODE_SECURE 0640 typedef struct { gchar *filename; GFile *file; GFileMonitor *monitor; /* nullable */ gboolean is_writable; gboolean is_mutable; } FuConfigItem; typedef struct { GKeyFile *keyfile; GHashTable *default_values; GPtrArray *items; /* (element-type FuConfigItem) */ } FuConfigPrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuConfig, fu_config, G_TYPE_OBJECT) #define GET_PRIVATE(o) (fu_config_get_instance_private(o)) static void fu_config_item_free(FuConfigItem *item) { if (item->monitor != NULL) { g_file_monitor_cancel(item->monitor); g_object_unref(item->monitor); } g_object_unref(item->file); g_free(item->filename); g_free(item); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuConfigItem, fu_config_item_free) static void fu_config_emit_changed(FuConfig *self) { g_debug("::configuration changed"); g_signal_emit(self, signals[SIGNAL_CHANGED], 0); } static void fu_config_emit_loaded(FuConfig *self) { g_debug("::configuration loaded"); g_signal_emit(self, signals[SIGNAL_LOADED], 0); } static gchar * fu_config_build_section_key(const gchar *section, const gchar *key) { return g_strdup_printf("%s::%s", section, key); } static gboolean fu_config_load_bytes_replace(FuConfig *self, GBytes *blob, GError **error) { FuConfigPrivate *priv = GET_PRIVATE(self); g_auto(GStrv) groups = NULL; g_autoptr(GKeyFile) kf = g_key_file_new(); if (!g_key_file_load_from_data(kf, (const gchar *)g_bytes_get_data(blob, NULL), g_bytes_get_size(blob), G_KEY_FILE_KEEP_COMMENTS, error)) return FALSE; groups = g_key_file_get_groups(kf, NULL); for (guint i = 0; groups[i] != NULL; i++) { g_auto(GStrv) keys = NULL; g_autofree gchar *comment_group = NULL; keys = g_key_file_get_keys(kf, groups[i], NULL, error); if (keys == NULL) { g_prefix_error(error, "failed to get keys for [%s]: ", groups[i]); return FALSE; } for (guint j = 0; keys[j] != NULL; j++) { const gchar *default_value; g_autofree gchar *comment_key = NULL; g_autofree gchar *section_key = NULL; g_autofree gchar *value = NULL; value = g_key_file_get_string(kf, groups[i], keys[j], error); if (value == NULL) { g_prefix_error(error, "failed to get string for %s=%s: ", groups[i], keys[j]); return FALSE; } /* is the same as the default */ section_key = fu_config_build_section_key(groups[i], keys[j]); default_value = g_hash_table_lookup(priv->default_values, section_key); if (g_strcmp0(value, default_value) == 0) { g_debug("default config, ignoring [%s] %s=%s", groups[i], keys[j], value); continue; } g_debug("setting config [%s] %s=%s", groups[i], keys[j], value); g_key_file_set_string(priv->keyfile, groups[i], keys[j], value); comment_key = g_key_file_get_comment(kf, groups[i], keys[j], NULL); if (comment_key != NULL && comment_key[0] != '\0') { if (!g_key_file_set_comment(priv->keyfile, groups[i], keys[j], comment_key, error)) { g_prefix_error(error, "failed to set comment '%s' for %s=%s: ", comment_key, groups[i], keys[j]); return FALSE; } } } comment_group = g_key_file_get_comment(kf, groups[i], NULL, NULL); if (comment_group != NULL && comment_group[0] != '\0') { if (!g_key_file_set_comment(priv->keyfile, groups[i], NULL, comment_group, error)) { g_prefix_error(error, "failed to set comment '%s' for [%s]: ", comment_group, groups[i]); return FALSE; } } } /* success */ return TRUE; } static void fu_config_migrate_keyfile(FuConfig *self) { FuConfigPrivate *priv = GET_PRIVATE(self); struct { const gchar *group; const gchar *key; const gchar *value; } key_values[] = {{"fwupd", "ApprovedFirmware", NULL}, {"fwupd", "ArchiveSizeMax", "0"}, {"fwupd", "BlockedFirmware", NULL}, {"fwupd", "DisabledDevices", NULL}, {"fwupd", "EnumerateAllDevices", NULL}, {"fwupd", "EspLocation", NULL}, {"fwupd", "HostBkc", NULL}, {"fwupd", "IdleTimeout", "7200"}, {"fwupd", "IdleTimeout", NULL}, {"fwupd", "IgnorePower", NULL}, {"fwupd", "ShowDevicePrivate", NULL}, {"fwupd", "TrustedUids", NULL}, {"fwupd", "UpdateMotd", NULL}, {"fwupd", "UriSchemes", NULL}, {"fwupd", "VerboseDomains", NULL}, {"fwupd", "OnlyTrusted", NULL}, {"fwupd", "IgnorePower", NULL}, {"fwupd", "DisabledPlugins", "test;test_ble;invalid"}, {"fwupd", "DisabledPlugins", "test;test_ble"}, {"redfish", "IpmiDisableCreateUser", NULL}, {"redfish", "ManagerResetTimeout", NULL}, {"msr", "MinimumSmeKernelVersion", NULL}, {"thunderbolt", "MinimumKernelVersion", NULL}, {"thunderbolt", "DelayedActivation", NULL}, {NULL, NULL, NULL}}; for (guint i = 0; key_values[i].group != NULL; i++) { const gchar *default_value; g_autofree gchar *value = NULL; g_auto(GStrv) keys = NULL; value = g_key_file_get_value(priv->keyfile, key_values[i].group, key_values[i].key, NULL); if (value == NULL) continue; if (key_values[i].value == NULL) { g_autofree gchar *section_key = fu_config_build_section_key(key_values[i].group, key_values[i].key); default_value = g_hash_table_lookup(priv->default_values, section_key); } else { default_value = key_values[i].value; } if ((default_value != NULL && g_ascii_strcasecmp(value, default_value) == 0) || (key_values[i].value == NULL && g_strcmp0(value, "") == 0)) { g_debug("not migrating default value of [%s] %s=%s", key_values[i].group, key_values[i].key, default_value); g_key_file_remove_comment(priv->keyfile, key_values[i].group, key_values[i].key, NULL); g_key_file_remove_key(priv->keyfile, key_values[i].group, key_values[i].key, NULL); } /* remove the group if there are no keys left */ keys = g_key_file_get_keys(priv->keyfile, key_values[i].group, NULL, NULL); if (g_strv_length(keys) == 0) { g_key_file_remove_comment(priv->keyfile, key_values[i].group, NULL, NULL); g_key_file_remove_group(priv->keyfile, key_values[i].group, NULL); } } } static gboolean fu_config_reload(FuConfig *self, GError **error) { FuConfigPrivate *priv = GET_PRIVATE(self); g_autoptr(GPtrArray) legacy_cfg_files = g_ptr_array_new_with_free_func(g_free); const gchar *fn_merge[] = {"daemon.conf", "msr.conf", "redfish.conf", "thunderbolt.conf", "uefi_capsule.conf", NULL}; #ifndef _WIN32 /* ensure mutable config files are set to the correct permissions */ for (guint i = 0; i < priv->items->len; i++) { FuConfigItem *item = g_ptr_array_index(priv->items, i); guint32 st_mode; g_autoptr(GFileInfo) info = NULL; /* check permissions */ if (!item->is_writable) { g_debug("skipping mode check for %s as not writable", item->filename); continue; } info = g_file_query_info(item->file, G_FILE_ATTRIBUTE_UNIX_MODE, G_FILE_QUERY_INFO_NONE, NULL, error); if (info == NULL) { g_prefix_error(error, "failed to query info about %s", item->filename); return FALSE; } st_mode = g_file_info_get_attribute_uint32(info, G_FILE_ATTRIBUTE_UNIX_MODE) & 0777; if (st_mode != FU_CONFIG_FILE_MODE_SECURE) { g_info("fixing %s from mode 0%o to 0%o", item->filename, st_mode, (guint)FU_CONFIG_FILE_MODE_SECURE); g_file_info_set_attribute_uint32(info, G_FILE_ATTRIBUTE_UNIX_MODE, FU_CONFIG_FILE_MODE_SECURE); if (!g_file_set_attributes_from_info(item->file, info, G_FILE_QUERY_INFO_NONE, NULL, error)) { g_prefix_error(error, "failed to set mode attribute of %s: ", item->filename); return FALSE; } } } #endif /* we have to copy each group/key from a temporary GKeyFile as g_key_file_load_from_file() * clears all keys before loading each file, and we want to allow the mutable version to be * incomplete and just *override* a specific option */ if (!g_key_file_load_from_data(priv->keyfile, "", -1, G_KEY_FILE_NONE, error)) return FALSE; for (guint i = 0; i < priv->items->len; i++) { FuConfigItem *item = g_ptr_array_index(priv->items, i); g_autoptr(GError) error_load = NULL; g_autoptr(GBytes) blob_item = NULL; g_debug("trying to load config values from %s", item->filename); blob_item = fu_bytes_get_contents(item->filename, &error_load); if (blob_item == NULL) { if (g_error_matches(error_load, FWUPD_ERROR, FWUPD_ERROR_PERMISSION_DENIED)) { g_debug("ignoring config file %s: ", error_load->message); continue; } else if (g_error_matches(error_load, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE)) { g_debug("%s", error_load->message); continue; } g_propagate_error(error, g_steal_pointer(&error_load)); g_prefix_error(error, "failed to read %s: ", item->filename); return FALSE; } if (!fu_config_load_bytes_replace(self, blob_item, error)) { g_prefix_error(error, "failed to load %s: ", item->filename); return FALSE; } } /* are any of the legacy files found in this location? */ for (guint i = 0; i < priv->items->len; i++) { FuConfigItem *item = g_ptr_array_index(priv->items, i); g_autofree gchar *dirname = g_path_get_dirname(item->filename); for (guint j = 0; fn_merge[j] != NULL; j++) { g_autofree gchar *fncompat = g_build_filename(dirname, fn_merge[j], NULL); if (g_file_test(fncompat, G_FILE_TEST_EXISTS)) { g_autoptr(GBytes) blob_compat = fu_bytes_get_contents(fncompat, error); if (blob_compat == NULL) { g_prefix_error(error, "failed to read %s: ", fncompat); return FALSE; } if (!fu_config_load_bytes_replace(self, blob_compat, error)) { g_prefix_error(error, "failed to load %s: ", fncompat); return FALSE; } g_ptr_array_add(legacy_cfg_files, g_steal_pointer(&fncompat)); } } } /* migration needed */ if (legacy_cfg_files->len > 0) { FuConfigItem *item = g_ptr_array_index(priv->items, 0); const gchar *fn_default = item->filename; g_autofree gchar *data = NULL; /* do not write empty keys migrated from daemon.conf */ fu_config_migrate_keyfile(self); /* make sure we can save the new file first */ data = g_key_file_to_data(priv->keyfile, NULL, error); if (data == NULL) return FALSE; if (!g_file_set_contents_full( fn_default, data, -1, G_FILE_SET_CONTENTS_CONSISTENT, FU_CONFIG_FILE_MODE_SECURE, /* only readable by root */ error)) { g_prefix_error(error, "failed to save %s: ", fn_default); return FALSE; } /* give the legacy files a .old extension */ for (guint i = 0; i < legacy_cfg_files->len; i++) { const gchar *fn_old = g_ptr_array_index(legacy_cfg_files, i); g_autofree gchar *fn_new = g_strdup_printf("%s.old", fn_old); g_info("renaming legacy config file %s to %s", fn_old, fn_new); if (g_rename(fn_old, fn_new) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to change rename %s to %s", fn_old, fn_new); return FALSE; } } } /* success */ return TRUE; } static void fu_config_monitor_changed_cb(GFileMonitor *monitor, GFile *file, GFile *other_file, GFileMonitorEvent event_type, gpointer user_data) { FuConfig *self = FU_CONFIG(user_data); g_autoptr(GError) error = NULL; g_autofree gchar *fn = g_file_get_path(file); /* nothing we need to care about */ if (event_type == G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED) { g_debug("%s attributes changed, ignoring", fn); return; } /* reload everything */ g_info("%s changed, reloading all configs", fn); if (!fu_config_reload(self, &error)) g_warning("failed to rescan daemon config: %s", error->message); fu_config_emit_changed(self); } /** * fu_config_set_default: * @self: a #FuConfig * @section: a settings section * @key: a settings key * @value: (nullable): a settings value * * Sets a default config value. * * Since: 2.0.0 **/ void fu_config_set_default(FuConfig *self, const gchar *section, const gchar *key, const gchar *value) { FuConfigPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CONFIG(self)); g_return_if_fail(section != NULL); g_return_if_fail(key != NULL); g_hash_table_insert(priv->default_values, fu_config_build_section_key(section, key), g_strdup(value)); } static gboolean fu_config_save(FuConfig *self, GError **error) { FuConfigPrivate *priv = GET_PRIVATE(self); g_autofree gchar *data = NULL; data = g_key_file_to_data(priv->keyfile, NULL, error); if (data == NULL) return FALSE; for (guint i = 0; i < priv->items->len; i++) { FuConfigItem *item = g_ptr_array_index(priv->items, i); if (!item->is_mutable) continue; if (!fu_path_mkdir_parent(item->filename, error)) return FALSE; if (!g_file_set_contents_full(item->filename, data, -1, G_FILE_SET_CONTENTS_CONSISTENT, FU_CONFIG_FILE_MODE_SECURE, /* only for root */ error)) return FALSE; return fu_config_reload(self, error); } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no writable config"); return FALSE; } /** * fu_config_set_value: * @self: a #FuConfig * @section: a settings section * @key: a settings key * @value: (nullable): a settings value * @error: (nullable): optional return location for an error * * Sets a plugin config value, saving the new data back to the default config file. * * Returns: %TRUE for success * * Since: 1.9.1 **/ gboolean fu_config_set_value(FuConfig *self, const gchar *section, const gchar *key, const gchar *value, GError **error) { FuConfigPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONFIG(self), FALSE); g_return_val_if_fail(section != NULL, FALSE); g_return_val_if_fail(key != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* sanity check */ if (priv->items->len == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "no config to load"); return FALSE; } /* do not write default keys */ fu_config_migrate_keyfile(self); /* only write the file to a mutable location */ g_key_file_set_string(priv->keyfile, section, key, value); return fu_config_save(self, error); } /** * fu_config_reset_defaults: * @self: a #FuConfig * @section: a settings section * * Reset all the keys back to the default values. * * Since: 1.9.15 **/ gboolean fu_config_reset_defaults(FuConfig *self, const gchar *section, GError **error) { FuConfigPrivate *priv = GET_PRIVATE(self); g_autoptr(GError) error_keyfile = NULL; g_return_val_if_fail(FU_IS_CONFIG(self), FALSE); g_return_val_if_fail(section != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* remove all keys, and save */ if (!g_key_file_remove_group(priv->keyfile, section, &error_keyfile)) { /* allow user to reset config sections twice */ if (!g_error_matches(error_keyfile, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_GROUP_NOT_FOUND)) { g_propagate_error(error, g_steal_pointer(&error_keyfile)); fwupd_error_convert(error); return FALSE; } } return fu_config_save(self, error); } /** * fu_config_get_value: * @self: a #FuConfig * @section: a settings section * @key: a settings key * * Return the value of a key, falling back to the default value if missing. * * NOTE: this function will return an empty string for `key=`. * * Returns: (transfer full): key value * * Since: 1.9.1 **/ gchar * fu_config_get_value(FuConfig *self, const gchar *section, const gchar *key) { FuConfigPrivate *priv = GET_PRIVATE(self); g_autofree gchar *value = NULL; g_return_val_if_fail(FU_IS_CONFIG(self), NULL); g_return_val_if_fail(section != NULL, NULL); g_return_val_if_fail(key != NULL, NULL); value = g_key_file_get_string(priv->keyfile, section, key, NULL); if (value == NULL) { g_autofree gchar *section_key = fu_config_build_section_key(section, key); const gchar *value_tmp = g_hash_table_lookup(priv->default_values, section_key); return g_strdup(value_tmp); } return g_steal_pointer(&value); } /** * fu_config_get_value_strv: * @self: a #FuConfig * @section: a settings section * @key: a settings key * * Return the value of a key, falling back to the default value if missing. * * NOTE: this function will return an empty string for `key=`. * * Returns: (transfer full) (nullable): key value * * Since: 1.9.1 **/ gchar ** fu_config_get_value_strv(FuConfig *self, const gchar *section, const gchar *key) { g_autofree gchar *value = NULL; g_return_val_if_fail(FU_IS_CONFIG(self), NULL); g_return_val_if_fail(section != NULL, NULL); g_return_val_if_fail(key != NULL, NULL); value = fu_config_get_value(self, section, key); if (value == NULL) return NULL; return g_strsplit(value, ";", -1); } /** * fu_config_get_value_bool: * @self: a #FuConfig * @section: a settings section * @key: a settings key * * Return the value of a key, falling back to the default value if missing or empty. * * Returns: boolean * * Since: 1.9.1 **/ gboolean fu_config_get_value_bool(FuConfig *self, const gchar *section, const gchar *key) { FuConfigPrivate *priv = GET_PRIVATE(self); g_autofree gchar *value = fu_config_get_value(self, section, key); if (value == NULL || value[0] == '\0') { g_autofree gchar *section_key = fu_config_build_section_key(section, key); const gchar *value_tmp = g_hash_table_lookup(priv->default_values, section_key); if (value_tmp == NULL) { g_critical("no default for [%s] %s", section, key); return FALSE; } return g_ascii_strcasecmp(value_tmp, "true") == 0; } return g_ascii_strcasecmp(value, "true") == 0; } /** * fu_config_get_value_u64: * @self: a #FuConfig * @section: a settings section * @key: a settings key * * Return the value of a key, falling back to the default value if missing or empty. * * Returns: uint64 * * Since: 1.9.1 **/ guint64 fu_config_get_value_u64(FuConfig *self, const gchar *section, const gchar *key) { FuConfigPrivate *priv = GET_PRIVATE(self); guint64 value = 0; const gchar *value_tmp; g_autofree gchar *tmp = fu_config_get_value(self, section, key); g_autoptr(GError) error_local = NULL; if (tmp == NULL || tmp[0] == '\0') { g_autofree gchar *section_key = fu_config_build_section_key(section, key); value_tmp = g_hash_table_lookup(priv->default_values, section_key); if (value_tmp == NULL) { g_critical("no default for [%s] %s", section, key); return G_MAXUINT64; } } else { value_tmp = tmp; } if (!fu_strtoull(value_tmp, &value, 0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, &error_local)) { g_warning("failed to parse [%s] %s = %s as integer", section, key, value_tmp); return G_MAXUINT64; } return value; } static gboolean fu_config_add_location(FuConfig *self, const gchar *dirname, gboolean is_mutable, GError **error) { FuConfigPrivate *priv = GET_PRIVATE(self); g_autoptr(FuConfigItem) item = g_new0(FuConfigItem, 1); item->is_mutable = is_mutable; item->filename = g_build_filename(dirname, "fwupd.conf", NULL); item->file = g_file_new_for_path(item->filename); /* is writable */ if (g_file_query_exists(item->file, NULL)) { g_autoptr(GFileInfo) info = NULL; g_debug("loading config %s", item->filename); info = g_file_query_info(item->file, G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE, G_FILE_QUERY_INFO_NONE, NULL, error); if (info == NULL) return FALSE; item->is_writable = g_file_info_get_attribute_boolean(info, G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE); if (!item->is_writable) g_debug("config %s is immutable", item->filename); } else { g_debug("not loading config %s", item->filename); } /* success */ g_ptr_array_add(priv->items, g_steal_pointer(&item)); return TRUE; } /** * fu_config_load: * @self: a #FuConfig * @error: (nullable): optional return location for an error * * Loads the configuration files from all possible locations. * * Returns: %TRUE for success * * Since: 1.9.1 **/ gboolean fu_config_load(FuConfig *self, GError **error) { FuConfigPrivate *priv = GET_PRIVATE(self); g_autofree gchar *configdir_mut = fu_path_from_kind(FU_PATH_KIND_LOCALCONFDIR_PKG); g_autofree gchar *configdir = fu_path_from_kind(FU_PATH_KIND_SYSCONFDIR_PKG); g_return_val_if_fail(FU_IS_CONFIG(self), FALSE); g_return_val_if_fail(priv->items->len == 0, FALSE); /* load the main daemon config file */ if (!fu_config_add_location(self, configdir, FALSE, error)) return FALSE; if (!fu_config_add_location(self, configdir_mut, TRUE, error)) return FALSE; if (!fu_config_reload(self, error)) return FALSE; /* set up a notify watches */ for (guint i = 0; i < priv->items->len; i++) { FuConfigItem *item = g_ptr_array_index(priv->items, i); g_autoptr(GFile) file = g_file_new_for_path(item->filename); item->monitor = g_file_monitor(file, G_FILE_MONITOR_NONE, NULL, error); if (item->monitor == NULL) return FALSE; g_signal_connect(G_FILE_MONITOR(item->monitor), "changed", G_CALLBACK(fu_config_monitor_changed_cb), self); } /* success */ fu_config_emit_loaded(self); return TRUE; } static void fu_config_init(FuConfig *self) { FuConfigPrivate *priv = GET_PRIVATE(self); priv->keyfile = g_key_file_new(); priv->items = g_ptr_array_new_with_free_func((GDestroyNotify)fu_config_item_free); priv->default_values = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); } static void fu_config_finalize(GObject *obj) { FuConfig *self = FU_CONFIG(obj); FuConfigPrivate *priv = GET_PRIVATE(self); g_key_file_unref(priv->keyfile); g_ptr_array_unref(priv->items); g_hash_table_unref(priv->default_values); G_OBJECT_CLASS(fu_config_parent_class)->finalize(obj); } static void fu_config_class_init(FuConfigClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_config_finalize; /** * FuConfig::changed: * @self: the #FuConfig instance that emitted the signal * * The ::changed signal is emitted when the config file has * changed, for instance when a parameter has been modified. **/ signals[SIGNAL_CHANGED] = g_signal_new("changed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); /** * FuConfig::loaded: * @self: the #FuConfig instance that emitted the signal * * The ::loaded signal is emitted when the config file has * loaded, typically at startup. **/ signals[SIGNAL_LOADED] = g_signal_new("loaded", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); } /** * fu_config_new: * * Creates a new #FuConfig. * * Returns: (transfer full): a new #FuConfig * * Since: 1.9.1 **/ FuConfig * fu_config_new(void) { return FU_CONFIG(g_object_new(FU_TYPE_CONFIG, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-config.h000066400000000000000000000020561501337203100177760ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_CONFIG (fu_config_get_type()) G_DECLARE_DERIVABLE_TYPE(FuConfig, fu_config, FU, CONFIG, GObject) struct _FuConfigClass { GObjectClass parent_class; }; void fu_config_set_default(FuConfig *self, const gchar *section, const gchar *key, const gchar *value) G_GNUC_NON_NULL(1, 2); gboolean fu_config_set_value(FuConfig *self, const gchar *section, const gchar *key, const gchar *value, GError **error) G_GNUC_NON_NULL(1, 2); gchar * fu_config_get_value(FuConfig *self, const gchar *section, const gchar *key) G_GNUC_NON_NULL(1, 2); gchar ** fu_config_get_value_strv(FuConfig *self, const gchar *section, const gchar *key) G_GNUC_NON_NULL(1, 2); gboolean fu_config_get_value_bool(FuConfig *self, const gchar *section, const gchar *key) G_GNUC_NON_NULL(1, 2); guint64 fu_config_get_value_u64(FuConfig *self, const gchar *section, const gchar *key) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-context-helper.h000066400000000000000000000007351501337203100214740ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-backend.h" #include "fu-context.h" /* this header exists to prevent an #include loop between fu-context.h and fu-backend.h */ void fu_context_add_backend(FuContext *self, FuBackend *backend) G_GNUC_NON_NULL(1, 2); FuBackend * fu_context_get_backend_by_name(FuContext *self, const gchar *name, GError **error) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-context-private.h000066400000000000000000000051761501337203100216730ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-config.h" #include "fu-context.h" #include "fu-hwids.h" #include "fu-progress.h" #include "fu-quirks.h" #include "fu-volume.h" typedef enum { FU_CONTEXT_HWID_FLAG_NONE = 0, FU_CONTEXT_HWID_FLAG_LOAD_CONFIG = 1 << 0, FU_CONTEXT_HWID_FLAG_LOAD_SMBIOS = 1 << 1, FU_CONTEXT_HWID_FLAG_LOAD_FDT = 1 << 2, FU_CONTEXT_HWID_FLAG_LOAD_DMI = 1 << 3, FU_CONTEXT_HWID_FLAG_LOAD_KENV = 1 << 4, FU_CONTEXT_HWID_FLAG_LOAD_DARWIN = 1 << 5, FU_CONTEXT_HWID_FLAG_LOAD_ALL = G_MAXUINT, } FuContextHwidFlags; FuContext * fu_context_new(void); void fu_context_housekeeping(FuContext *self) G_GNUC_NON_NULL(1); gboolean fu_context_reload_bios_settings(FuContext *self, GError **error); gboolean fu_context_load_hwinfo(FuContext *self, FuProgress *progress, FuContextHwidFlags flags, GError **error) G_GNUC_NON_NULL(1); gboolean fu_context_load_quirks(FuContext *self, FuQuirksLoadFlags flags, GError **error) G_GNUC_NON_NULL(1); GHashTable * fu_context_get_runtime_versions(FuContext *self) G_GNUC_NON_NULL(1); GHashTable * fu_context_get_compile_versions(FuContext *self) G_GNUC_NON_NULL(1); void fu_context_add_firmware_gtype(FuContext *self, const gchar *id, GType gtype) G_GNUC_NON_NULL(1, 2); GPtrArray * fu_context_get_firmware_gtype_ids(FuContext *self) G_GNUC_NON_NULL(1); GArray * fu_context_get_firmware_gtypes(FuContext *self) G_GNUC_NON_NULL(1); GType fu_context_get_firmware_gtype_by_id(FuContext *self, const gchar *id) G_GNUC_NON_NULL(1, 2); void fu_context_add_udev_subsystem(FuContext *self, const gchar *subsystem, const gchar *plugin_name) G_GNUC_NON_NULL(1, 2); GPtrArray * fu_context_get_udev_subsystems(FuContext *self) G_GNUC_NON_NULL(1); GPtrArray * fu_context_get_backends(FuContext *self) G_GNUC_NON_NULL(1); gboolean fu_context_has_backend(FuContext *self, const gchar *name) G_GNUC_NON_NULL(1, 2); GPtrArray * fu_context_get_plugin_names_for_udev_subsystem(FuContext *self, const gchar *subsystem, GError **error) G_GNUC_NON_NULL(1, 2); void fu_context_add_esp_volume(FuContext *self, FuVolume *volume) G_GNUC_NON_NULL(1); FuSmbios * fu_context_get_smbios(FuContext *self) G_GNUC_NON_NULL(1); FuHwids * fu_context_get_hwids(FuContext *self) G_GNUC_NON_NULL(1); FuConfig * fu_context_get_config(FuContext *self) G_GNUC_NON_NULL(1); void fu_context_set_chassis_kind(FuContext *self, FuSmbiosChassisKind chassis_kind) G_GNUC_NON_NULL(1); gpointer fu_context_get_data(FuContext *self, const gchar *key); void fu_context_set_data(FuContext *self, const gchar *key, gpointer data); fwupd-2.0.10/libfwupdplugin/fu-context.c000066400000000000000000002070631501337203100202150ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuContext" #include "config.h" #include "fu-bios-settings-private.h" #include "fu-common-private.h" #include "fu-config-private.h" #include "fu-context-helper.h" #include "fu-context-private.h" #include "fu-dummy-efivars.h" #include "fu-efi-device-path-list.h" #include "fu-efi-file-path-device-path.h" #include "fu-efi-hard-drive-device-path.h" #include "fu-fdt-firmware.h" #include "fu-hwids-private.h" #include "fu-path.h" #include "fu-pefile-firmware.h" #include "fu-smbios-private.h" #include "fu-volume-private.h" /** * FuContext: * * A context that represents the shared system state. This object is shared * between the engine, the plugins and the devices. */ typedef struct { FuContextFlags flags; FuHwids *hwids; FuConfig *config; FuSmbios *smbios; FuSmbiosChassisKind chassis_kind; FuQuirks *quirks; FuEfivars *efivars; GPtrArray *backends; GHashTable *runtime_versions; GHashTable *compile_versions; GHashTable *udev_subsystems; /* utf8:GPtrArray */ GPtrArray *esp_volumes; GHashTable *firmware_gtypes; /* utf8:GType */ GHashTable *hwid_flags; /* str: */ FuPowerState power_state; FuLidState lid_state; FuDisplayState display_state; guint battery_level; guint battery_threshold; FuBiosSettings *host_bios_settings; FuFirmware *fdt; /* optional */ gchar *esp_location; } FuContextPrivate; enum { SIGNAL_SECURITY_CHANGED, SIGNAL_HOUSEKEEPING, SIGNAL_LAST }; enum { PROP_0, PROP_POWER_STATE, PROP_LID_STATE, PROP_DISPLAY_STATE, PROP_BATTERY_LEVEL, PROP_BATTERY_THRESHOLD, PROP_FLAGS, PROP_LAST }; static guint signals[SIGNAL_LAST] = {0}; G_DEFINE_TYPE_WITH_PRIVATE(FuContext, fu_context, G_TYPE_OBJECT) #define GET_PRIVATE(o) (fu_context_get_instance_private(o)) static GFile * fu_context_get_fdt_file(GError **error) { g_autofree gchar *fdtfn_local = NULL; g_autofree gchar *fdtfn_sys = NULL; g_autofree gchar *localstatedir_pkg = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR_PKG); g_autofree gchar *sysfsdir = NULL; /* look for override first, fall back to system value */ fdtfn_local = g_build_filename(localstatedir_pkg, "system.dtb", NULL); if (g_file_test(fdtfn_local, G_FILE_TEST_EXISTS)) return g_file_new_for_path(fdtfn_local); /* actual hardware value */ sysfsdir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR_FW); fdtfn_sys = g_build_filename(sysfsdir, "fdt", NULL); if (g_file_test(fdtfn_sys, G_FILE_TEST_EXISTS)) return g_file_new_for_path(fdtfn_sys); /* failed */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot find %s or override %s", fdtfn_sys, fdtfn_local); return NULL; } /** * fu_context_get_fdt: * @self: a #FuContext * @error: (nullable): optional return location for an error * * Gets and parses the system FDT, aka. the Flat Device Tree. * * The results are cached internally to the context, and subsequent calls to this function * returns the pre-parsed object. * * Returns: (transfer full): a #FuFdtFirmware, or %NULL * * Since: 1.8.10 **/ FuFirmware * fu_context_get_fdt(FuContext *self, GError **error) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* load if not already parsed */ if (priv->fdt == NULL) { g_autoptr(FuFirmware) fdt_tmp = fu_fdt_firmware_new(); g_autoptr(GFile) file = fu_context_get_fdt_file(error); if (file == NULL) return NULL; if (!fu_firmware_parse_file(fdt_tmp, file, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error)) { g_prefix_error(error, "failed to parse FDT: "); return NULL; } priv->fdt = g_steal_pointer(&fdt_tmp); } /* success */ return g_object_ref(priv->fdt); } /** * fu_context_get_efivars: * @self: a #FuContext * * Gets the EFI variable store. * * Returns: (transfer none): a #FuEfivars * * Since: 2.0.0 **/ FuEfivars * fu_context_get_efivars(FuContext *self) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); return priv->efivars; } /** * fu_context_get_smbios: * @self: a #FuContext * * Gets the SMBIOS store. * * Returns: (transfer none): a #FuSmbios * * Since: 1.8.10 **/ FuSmbios * fu_context_get_smbios(FuContext *self) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); return priv->smbios; } /** * fu_context_get_hwids: * @self: a #FuContext * * Gets the HWIDs store. * * Returns: (transfer none): a #FuHwids * * Since: 1.8.10 **/ FuHwids * fu_context_get_hwids(FuContext *self) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); return priv->hwids; } /** * fu_context_get_config: * @self: a #FuContext * * Gets the system config. * * Returns: (transfer none): a #FuHwids * * Since: 1.9.1 **/ FuConfig * fu_context_get_config(FuContext *self) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); return priv->config; } /** * fu_context_get_smbios_string: * @self: a #FuContext * @type: a SMBIOS structure type, e.g. %FU_SMBIOS_STRUCTURE_TYPE_BIOS * @length: expected length of the structure, or %FU_SMBIOS_STRUCTURE_LENGTH_ANY * @offset: a SMBIOS offset * @error: (nullable): optional return location for an error * * Gets a hardware SMBIOS string. * * The @type and @offset can be referenced from the DMTF SMBIOS specification: * https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.1.1.pdf * * Returns: a string, or %NULL * * Since: 2.0.7 **/ const gchar * fu_context_get_smbios_string(FuContext *self, guint8 type, guint8 length, guint8 offset, GError **error) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); if (!fu_context_has_flag(self, FU_CONTEXT_FLAG_LOADED_HWINFO)) { g_critical("cannot use SMBIOS before calling ->load_hwinfo()"); return NULL; } return fu_smbios_get_string(priv->smbios, type, length, offset, error); } /** * fu_context_get_smbios_data: * @self: a #FuContext * @type: a SMBIOS structure type, e.g. %FU_SMBIOS_STRUCTURE_TYPE_BIOS * @length: expected length of the structure, or %FU_SMBIOS_STRUCTURE_LENGTH_ANY * @error: (nullable): optional return location for an error * * Gets all hardware SMBIOS data for a specific type. * * Returns: (transfer container) (element-type GBytes): a #GBytes, or %NULL if not found * * Since: 2.0.7 **/ GPtrArray * fu_context_get_smbios_data(FuContext *self, guint8 type, guint8 length, GError **error) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); /* must be valid and non-zero length */ if (!fu_context_has_flag(self, FU_CONTEXT_FLAG_LOADED_HWINFO)) { g_critical("cannot use SMBIOS before calling ->load_hwinfo()"); g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "no data"); return NULL; } return fu_smbios_get_data(priv->smbios, type, length, error); } /** * fu_context_get_smbios_integer: * @self: a #FuContext * @type: a structure type, e.g. %FU_SMBIOS_STRUCTURE_TYPE_BIOS * @length: expected length of the structure, or %FU_SMBIOS_STRUCTURE_LENGTH_ANY * @offset: a structure offset * @error: (nullable): optional return location for an error * * Reads an integer value from the SMBIOS string table of a specific structure. * * The @type and @offset can be referenced from the DMTF SMBIOS specification: * https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.1.1.pdf * * Returns: an integer, or %G_MAXUINT if invalid or not found * * Since: 2.0.7 **/ guint fu_context_get_smbios_integer(FuContext *self, guint8 type, guint8 length, guint8 offset, GError **error) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), G_MAXUINT); if (!fu_context_has_flag(self, FU_CONTEXT_FLAG_LOADED_HWINFO)) { g_critical("cannot use SMBIOS before calling ->load_hwinfo()"); return G_MAXUINT; } return fu_smbios_get_integer(priv->smbios, type, length, offset, error); } /** * fu_context_reload_bios_settings: * @self: a #FuContext * @error: (nullable): optional return location for an error * * Refreshes the list of firmware attributes on the system. * * Since: 1.8.4 **/ gboolean fu_context_reload_bios_settings(FuContext *self, GError **error) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), FALSE); return fu_bios_settings_setup(priv->host_bios_settings, error); } /** * fu_context_get_bios_settings: * @self: a #FuContext * * Returns all the firmware attributes defined in the system. * * Returns: (transfer full): A #FuBiosSettings * * Since: 1.8.4 **/ FuBiosSettings * fu_context_get_bios_settings(FuContext *self) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); return g_object_ref(priv->host_bios_settings); } /** * fu_context_get_bios_setting: * @self: a #FuContext * @name: a BIOS setting title, e.g. `BootOrderLock` * * Finds out if a system supports a given BIOS setting. * * Returns: (transfer none): #FwupdBiosSetting if the attr exists. * * Since: 1.8.4 **/ FwupdBiosSetting * fu_context_get_bios_setting(FuContext *self, const gchar *name) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); g_return_val_if_fail(name != NULL, NULL); return fu_bios_settings_get_attr(priv->host_bios_settings, name); } /** * fu_context_get_bios_setting_pending_reboot: * @self: a #FuContext * * Determine if updates to BIOS settings are pending until next boot. * * Returns: %TRUE if updates are pending. * * Since: 1.8.4 **/ gboolean fu_context_get_bios_setting_pending_reboot(FuContext *self) { FuContextPrivate *priv = GET_PRIVATE(self); gboolean ret; g_return_val_if_fail(FU_IS_CONTEXT(self), FALSE); fu_bios_settings_get_pending_reboot(priv->host_bios_settings, &ret, NULL); return ret; } /** * fu_context_get_chassis_kind: * @self: a #FuContext * * Gets the chassis kind, if known. * * Returns: a #FuSmbiosChassisKind, e.g. %FU_SMBIOS_CHASSIS_KIND_LAPTOP * * Since: 1.8.10 **/ FuSmbiosChassisKind fu_context_get_chassis_kind(FuContext *self) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), FALSE); return priv->chassis_kind; } /** * fu_context_set_chassis_kind: * @self: a #FuContext * @chassis_kind: a #FuSmbiosChassisKind, e.g. %FU_SMBIOS_CHASSIS_KIND_TABLET * * Sets the chassis kind. * * Since: 1.8.10 **/ void fu_context_set_chassis_kind(FuContext *self, FuSmbiosChassisKind chassis_kind) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CONTEXT(self)); priv->chassis_kind = chassis_kind; } /** * fu_context_has_hwid_guid: * @self: a #FuContext * @guid: a GUID, e.g. `059eb22d-6dc7-59af-abd3-94bbe017f67c` * * Finds out if a hardware GUID exists. * * Returns: %TRUE if the GUID exists * * Since: 1.6.0 **/ gboolean fu_context_has_hwid_guid(FuContext *self, const gchar *guid) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), FALSE); if (!fu_context_has_flag(self, FU_CONTEXT_FLAG_LOADED_HWINFO)) { g_critical("cannot use HWIDs before calling ->load_hwinfo()"); return FALSE; } return fu_hwids_has_guid(priv->hwids, guid); } /** * fu_context_get_hwid_guids: * @self: a #FuContext * * Returns all the HWIDs defined in the system. All hardware IDs on a * specific system can be shown using the `fwupdmgr hwids` command. * * Returns: (transfer none) (element-type utf8): an array of GUIDs * * Since: 1.6.0 **/ GPtrArray * fu_context_get_hwid_guids(FuContext *self) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); if (!fu_context_has_flag(self, FU_CONTEXT_FLAG_LOADED_HWINFO)) { g_critical("cannot use HWIDs before calling ->load_hwinfo()"); return NULL; } return fu_hwids_get_guids(priv->hwids); } /** * fu_context_get_hwid_value: * @self: a #FuContext * @key: a DMI ID, e.g. `BiosVersion` * * Gets the cached value for one specific key that is valid ASCII and suitable * for display. * * Returns: the string, e.g. `1.2.3`, or %NULL if not found * * Since: 1.6.0 **/ const gchar * fu_context_get_hwid_value(FuContext *self, const gchar *key) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); g_return_val_if_fail(key != NULL, NULL); if (!fu_context_has_flag(self, FU_CONTEXT_FLAG_LOADED_HWINFO)) { g_critical("cannot use HWIDs before calling ->load_hwinfo()"); return NULL; } return fu_hwids_get_value(priv->hwids, key); } /** * fu_context_get_hwid_replace_value: * @self: a #FuContext * @keys: a key, e.g. `HardwareID-3` or %FU_HWIDS_KEY_PRODUCT_SKU * @error: (nullable): optional return location for an error * * Gets the replacement value for a specific key. All hardware IDs on a * specific system can be shown using the `fwupdmgr hwids` command. * * Returns: (transfer full): a string, or %NULL for error. * * Since: 1.6.0 **/ gchar * fu_context_get_hwid_replace_value(FuContext *self, const gchar *keys, GError **error) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); g_return_val_if_fail(keys != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); if (!fu_context_has_flag(self, FU_CONTEXT_FLAG_LOADED_HWINFO)) { g_critical("cannot use HWIDs before calling ->load_hwinfo()"); g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "no data"); return NULL; } return fu_hwids_get_replace_values(priv->hwids, keys, error); } /** * fu_context_add_runtime_version: * @self: a #FuContext * @component_id: an AppStream component id, e.g. `org.gnome.Software` * @version: a version string, e.g. `1.2.3` * * Sets a runtime version of a specific dependency. * * Since: 1.6.0 **/ void fu_context_add_runtime_version(FuContext *self, const gchar *component_id, const gchar *version) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CONTEXT(self)); g_return_if_fail(component_id != NULL); g_return_if_fail(version != NULL); if (priv->runtime_versions == NULL) return; g_hash_table_insert(priv->runtime_versions, g_strdup(component_id), g_strdup(version)); } /** * fu_context_get_runtime_version: * @self: a #FuContext * @component_id: an AppStream component id, e.g. `org.gnome.Software` * * Sets a runtime version of a specific dependency. * * Returns: a version string, e.g. `1.2.3`, or %NULL * * Since: 1.9.10 **/ const gchar * fu_context_get_runtime_version(FuContext *self, const gchar *component_id) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); g_return_val_if_fail(component_id != NULL, NULL); if (priv->runtime_versions == NULL) return NULL; return g_hash_table_lookup(priv->runtime_versions, component_id); } /** * fu_context_get_runtime_versions: * @self: a #FuContext * * Gets the runtime versions for the context. * * Returns: (transfer none) (element-type utf8 utf8): dictionary of versions * * Since: 1.9.10 **/ GHashTable * fu_context_get_runtime_versions(FuContext *self) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); return priv->runtime_versions; } /** * fu_context_add_compile_version: * @self: a #FuContext * @component_id: an AppStream component id, e.g. `org.gnome.Software` * @version: a version string, e.g. `1.2.3` * * Sets a compile-time version of a specific dependency. * * Since: 1.6.0 **/ void fu_context_add_compile_version(FuContext *self, const gchar *component_id, const gchar *version) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CONTEXT(self)); g_return_if_fail(component_id != NULL); g_return_if_fail(version != NULL); if (priv->compile_versions == NULL) return; g_hash_table_insert(priv->compile_versions, g_strdup(component_id), g_strdup(version)); } /** * fu_context_get_compile_versions: * @self: a #FuContext * * Gets the compile time versions for the context. * * Returns: (transfer none) (element-type utf8 utf8): dictionary of versions * * Since: 1.9.10 **/ GHashTable * fu_context_get_compile_versions(FuContext *self) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); return priv->compile_versions; } static gint fu_context_udev_plugin_names_sort_cb(gconstpointer a, gconstpointer b) { const gchar *str_a = *((const gchar **)a); const gchar *str_b = *((const gchar **)b); return g_strcmp0(str_a, str_b); } /** * fu_context_add_udev_subsystem: * @self: a #FuContext * @subsystem: a subsystem name, e.g. `pciport`, or `block:partition` * @plugin_name: (nullable): a plugin name, e.g. `iommu` * * Registers the udev subsystem to be watched by the daemon. * * Plugins can use this method only in fu_plugin_init() * * Since: 1.6.0 **/ void fu_context_add_udev_subsystem(FuContext *self, const gchar *subsystem, const gchar *plugin_name) { FuContextPrivate *priv = GET_PRIVATE(self); GPtrArray *plugin_names; g_auto(GStrv) subsystem_devtype = NULL; g_return_if_fail(FU_IS_CONTEXT(self)); g_return_if_fail(subsystem != NULL); /* add the base subsystem watch if passed a subsystem:devtype */ subsystem_devtype = g_strsplit(subsystem, ":", 2); if (g_strv_length(subsystem_devtype) > 1) fu_context_add_udev_subsystem(self, subsystem_devtype[0], NULL); /* already exists */ plugin_names = g_hash_table_lookup(priv->udev_subsystems, subsystem); if (plugin_names != NULL) { if (plugin_name != NULL) { for (guint i = 0; i < plugin_names->len; i++) { const gchar *tmp = g_ptr_array_index(plugin_names, i); if (g_strcmp0(tmp, plugin_name) == 0) return; } g_ptr_array_add(plugin_names, g_strdup(plugin_name)); g_ptr_array_sort(plugin_names, fu_context_udev_plugin_names_sort_cb); } return; } /* add */ plugin_names = g_ptr_array_new_with_free_func(g_free); if (plugin_name != NULL) g_ptr_array_add(plugin_names, g_strdup(plugin_name)); g_hash_table_insert(priv->udev_subsystems, g_strdup(subsystem), g_steal_pointer(&plugin_names)); if (plugin_name != NULL) g_info("added udev subsystem watch of %s for plugin %s", subsystem, plugin_name); else g_info("added udev subsystem watch of %s", subsystem); } /** * fu_context_get_plugin_names_for_udev_subsystem: * @self: a #FuContext * @subsystem: a subsystem name, e.g. `pciport`, or `block:partition` * @error: (nullable): optional return location for an error * * Gets the plugins which registered for a specific subsystem. * * Returns: (transfer container) (element-type utf8): List of plugin names * * Since: 1.9.3 **/ GPtrArray * fu_context_get_plugin_names_for_udev_subsystem(FuContext *self, const gchar *subsystem, GError **error) { FuContextPrivate *priv = GET_PRIVATE(self); GPtrArray *plugin_names_tmp; g_auto(GStrv) subsystem_devtype = NULL; g_autoptr(GPtrArray) plugin_names = g_ptr_array_new_with_free_func(g_free); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); g_return_val_if_fail(subsystem != NULL, NULL); /* add the base subsystem first */ subsystem_devtype = g_strsplit(subsystem, ":", 2); if (g_strv_length(subsystem_devtype) > 1) { plugin_names_tmp = g_hash_table_lookup(priv->udev_subsystems, subsystem_devtype[0]); if (plugin_names_tmp != NULL) g_ptr_array_extend(plugin_names, plugin_names_tmp, (GCopyFunc)g_strdup, NULL); } /* add the exact match */ plugin_names_tmp = g_hash_table_lookup(priv->udev_subsystems, subsystem); if (plugin_names_tmp != NULL) g_ptr_array_extend(plugin_names, plugin_names_tmp, (GCopyFunc)g_strdup, NULL); /* no matches */ if (plugin_names->len == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no plugins registered for %s", subsystem); return NULL; } /* success */ return g_steal_pointer(&plugin_names); } /** * fu_context_get_udev_subsystems: * @self: a #FuContext * * Gets the udev subsystems required by all plugins. * * Returns: (transfer container) (element-type utf8): List of subsystems * * Since: 1.6.0 **/ GPtrArray * fu_context_get_udev_subsystems(FuContext *self) { FuContextPrivate *priv = GET_PRIVATE(self); g_autoptr(GList) keys = g_hash_table_get_keys(priv->udev_subsystems); g_autoptr(GPtrArray) subsystems = g_ptr_array_new_with_free_func(g_free); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); for (GList *l = keys; l != NULL; l = l->next) { const gchar *subsystem = (const gchar *)l->data; g_ptr_array_add(subsystems, g_strdup(subsystem)); } return g_steal_pointer(&subsystems); } /** * fu_context_add_firmware_gtype: * @self: a #FuContext * @id: (nullable): an optional string describing the type, e.g. `ihex` * @gtype: a #GType e.g. `FU_TYPE_FOO_FIRMWARE` * * Adds a firmware #GType which is used when creating devices. If @id is not * specified then it is guessed using the #GType name. * * Plugins can use this method only in fu_plugin_init() * * Since: 1.6.0 **/ void fu_context_add_firmware_gtype(FuContext *self, const gchar *id, GType gtype) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CONTEXT(self)); g_return_if_fail(id != NULL); g_return_if_fail(gtype != G_TYPE_INVALID); g_type_ensure(gtype); g_hash_table_insert(priv->firmware_gtypes, g_strdup(id), GSIZE_TO_POINTER(gtype)); } /** * fu_context_get_firmware_gtype_by_id: * @self: a #FuContext * @id: an string describing the type, e.g. `ihex` * * Returns the #GType using the firmware @id. * * Returns: a #GType, or %G_TYPE_INVALID * * Since: 1.6.0 **/ GType fu_context_get_firmware_gtype_by_id(FuContext *self, const gchar *id) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), G_TYPE_INVALID); g_return_val_if_fail(id != NULL, G_TYPE_INVALID); return GPOINTER_TO_SIZE(g_hash_table_lookup(priv->firmware_gtypes, id)); } static gint fu_context_gtypes_sort_cb(gconstpointer a, gconstpointer b) { const gchar *stra = *((const gchar **)a); const gchar *strb = *((const gchar **)b); return g_strcmp0(stra, strb); } /** * fu_context_get_firmware_gtype_ids: * @self: a #FuContext * * Returns all the firmware #GType IDs. * * Returns: (transfer container) (element-type utf8): firmware IDs * * Since: 1.6.0 **/ GPtrArray * fu_context_get_firmware_gtype_ids(FuContext *self) { FuContextPrivate *priv = GET_PRIVATE(self); GPtrArray *firmware_gtypes = g_ptr_array_new_with_free_func(g_free); g_autoptr(GList) keys = g_hash_table_get_keys(priv->firmware_gtypes); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); for (GList *l = keys; l != NULL; l = l->next) { const gchar *id = l->data; g_ptr_array_add(firmware_gtypes, g_strdup(id)); } g_ptr_array_sort(firmware_gtypes, fu_context_gtypes_sort_cb); return firmware_gtypes; } /** * fu_context_get_firmware_gtypes: * @self: a #FuContext * * Returns all the firmware #GType's. * * Returns: (transfer container) (element-type GType): Firmware types * * Since: 1.9.1 **/ GArray * fu_context_get_firmware_gtypes(FuContext *self) { FuContextPrivate *priv = GET_PRIVATE(self); GArray *firmware_gtypes = g_array_new(FALSE, FALSE, sizeof(GType)); g_autoptr(GList) values = g_hash_table_get_values(priv->firmware_gtypes); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); for (GList *l = values; l != NULL; l = l->next) { GType gtype = GPOINTER_TO_SIZE(l->data); g_array_append_val(firmware_gtypes, gtype); } return firmware_gtypes; } /** * fu_context_add_quirk_key: * @self: a #FuContext * @key: a quirk string, e.g. `DfuVersion` * * Adds a possible quirk key. If added by a plugin it should be namespaced * using the plugin name, where possible. * * Plugins can use this method only in fu_plugin_init() * * Since: 1.6.0 **/ void fu_context_add_quirk_key(FuContext *self, const gchar *key) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CONTEXT(self)); g_return_if_fail(key != NULL); if (priv->quirks == NULL) return; fu_quirks_add_possible_key(priv->quirks, key); } /** * fu_context_lookup_quirk_by_id: * @self: a #FuContext * @guid: GUID to lookup * @key: an ID to match the entry, e.g. `Summary` * * Looks up an entry in the hardware database using a string value. * * Returns: (transfer none): values from the database, or %NULL if not found * * Since: 1.6.0 **/ const gchar * fu_context_lookup_quirk_by_id(FuContext *self, const gchar *guid, const gchar *key) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); g_return_val_if_fail(guid != NULL, NULL); g_return_val_if_fail(key != NULL, NULL); /* exact ID */ return fu_quirks_lookup_by_id(priv->quirks, guid, key); } typedef struct { FuContext *self; /* noref */ FuContextLookupIter iter_cb; gpointer user_data; } FuContextQuirkLookupHelper; static void fu_context_lookup_quirk_by_id_iter_cb(FuQuirks *self, const gchar *key, const gchar *value, FuContextQuirkSource source, gpointer user_data) { FuContextQuirkLookupHelper *helper = (FuContextQuirkLookupHelper *)user_data; helper->iter_cb(helper->self, key, value, source, helper->user_data); } /** * fu_context_lookup_quirk_by_id_iter: * @self: a #FuContext * @guid: GUID to lookup * @key: (nullable): an ID to match the entry, e.g. `Name`, or %NULL for all keys * @iter_cb: (scope call) (closure user_data): a function to call for each result * @user_data: user data passed to @iter_cb * * Looks up all entries in the hardware database using a GUID value. * * Returns: %TRUE if the ID was found, and @iter was called * * Since: 1.6.0 **/ gboolean fu_context_lookup_quirk_by_id_iter(FuContext *self, const gchar *guid, const gchar *key, FuContextLookupIter iter_cb, gpointer user_data) { FuContextPrivate *priv = GET_PRIVATE(self); FuContextQuirkLookupHelper helper = { .self = self, .iter_cb = iter_cb, .user_data = user_data, }; g_return_val_if_fail(FU_IS_CONTEXT(self), FALSE); g_return_val_if_fail(guid != NULL, FALSE); g_return_val_if_fail(iter_cb != NULL, FALSE); return fu_quirks_lookup_by_id_iter(priv->quirks, guid, key, fu_context_lookup_quirk_by_id_iter_cb, &helper); } /** * fu_context_security_changed: * @self: a #FuContext * * Informs the daemon that the HSI state may have changed. * * Since: 1.6.0 **/ void fu_context_security_changed(FuContext *self) { g_return_if_fail(FU_IS_CONTEXT(self)); g_signal_emit(self, signals[SIGNAL_SECURITY_CHANGED], 0); } /** * fu_context_housekeeping: * @self: a #FuContext * * Performs any housekeeping maintenance when the daemon is idle. * * Since: 2.0.0 **/ void fu_context_housekeeping(FuContext *self) { g_return_if_fail(FU_IS_CONTEXT(self)); g_signal_emit(self, signals[SIGNAL_HOUSEKEEPING], 0); } typedef gboolean (*FuContextHwidsSetupFunc)(FuContext *self, FuHwids *hwids, GError **error); static void fu_context_detect_full_disk_encryption(FuContext *self) { FuContextPrivate *priv = GET_PRIVATE(self); g_autoptr(GPtrArray) devices = NULL; g_autoptr(GError) error_local = NULL; g_return_if_fail(FU_IS_CONTEXT(self)); devices = fu_common_get_block_devices(&error_local); if (devices == NULL) { g_info("Failed to get block devices: %s", error_local->message); return; } for (guint i = 0; i < devices->len; i++) { GDBusProxy *proxy = g_ptr_array_index(devices, i); g_autoptr(GVariant) id_type = g_dbus_proxy_get_cached_property(proxy, "IdType"); g_autoptr(GVariant) device = g_dbus_proxy_get_cached_property(proxy, "Device"); g_autoptr(GVariant) id_label = g_dbus_proxy_get_cached_property(proxy, "IdLabel"); if (id_type != NULL && device != NULL && g_strcmp0(g_variant_get_string(id_type, NULL), "BitLocker") == 0) priv->flags |= FU_CONTEXT_FLAG_FDE_BITLOCKER; if (id_type != NULL && id_label != NULL && g_strcmp0(g_variant_get_string(id_label, NULL), "ubuntu-data-enc") == 0 && g_strcmp0(g_variant_get_string(id_type, NULL), "crypto_LUKS") == 0) { priv->flags |= FU_CONTEXT_FLAG_FDE_SNAPD; } } } static void fu_context_hwid_quirk_cb(FuContext *self, const gchar *key, const gchar *value, FuContextQuirkSource source, gpointer user_data) { FuContextPrivate *priv = GET_PRIVATE(self); if (value != NULL) { g_auto(GStrv) values = g_strsplit(value, ",", -1); for (guint j = 0; values[j] != NULL; j++) g_hash_table_add(priv->hwid_flags, g_strdup(values[j])); } } /** * fu_context_load_hwinfo: * @self: a #FuContext * @progress: a #FuProgress * @flags: a #FuContextHwidFlags, e.g. %FU_CONTEXT_HWID_FLAG_LOAD_SMBIOS * @error: (nullable): optional return location for an error * * Loads all hardware information parts of the context. * * Returns: %TRUE for success * * Since: 1.8.10 **/ gboolean fu_context_load_hwinfo(FuContext *self, FuProgress *progress, FuContextHwidFlags flags, GError **error) { FuContextPrivate *priv = GET_PRIVATE(self); GPtrArray *guids; g_autoptr(GError) error_hwids = NULL; g_autoptr(GError) error_bios_settings = NULL; struct { const gchar *name; FuContextHwidFlags flag; FuContextHwidsSetupFunc func; } hwids_setup_map[] = {{"config", FU_CONTEXT_HWID_FLAG_LOAD_CONFIG, fu_hwids_config_setup}, {"smbios", FU_CONTEXT_HWID_FLAG_LOAD_SMBIOS, fu_hwids_smbios_setup}, {"fdt", FU_CONTEXT_HWID_FLAG_LOAD_FDT, fu_hwids_fdt_setup}, {"kenv", FU_CONTEXT_HWID_FLAG_LOAD_KENV, fu_hwids_kenv_setup}, {"dmi", FU_CONTEXT_HWID_FLAG_LOAD_DMI, fu_hwids_dmi_setup}, {"darwin", FU_CONTEXT_HWID_FLAG_LOAD_DARWIN, fu_hwids_darwin_setup}, {NULL, FU_CONTEXT_HWID_FLAG_NONE, NULL}}; g_return_val_if_fail(FU_IS_CONTEXT(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "hwids-setup-funcs"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "hwids-setup"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 3, "set-flags"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "detect-fde"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 94, "reload-bios-settings"); /* required always */ if (!fu_config_load(priv->config, error)) return FALSE; /* run all the HWID setup funcs */ for (guint i = 0; hwids_setup_map[i].name != NULL; i++) { if ((flags & hwids_setup_map[i].flag) > 0) { g_autoptr(GError) error_local = NULL; if (!hwids_setup_map[i].func(self, priv->hwids, &error_local)) { g_info("failed to load %s: %s", hwids_setup_map[i].name, error_local->message); continue; } } } fu_context_add_flag(self, FU_CONTEXT_FLAG_LOADED_HWINFO); fu_progress_step_done(progress); if (!fu_hwids_setup(priv->hwids, &error_hwids)) g_warning("Failed to load HWIDs: %s", error_hwids->message); fu_progress_step_done(progress); /* set the hwid flags */ guids = fu_context_get_hwid_guids(self); for (guint i = 0; i < guids->len; i++) { const gchar *guid = g_ptr_array_index(guids, i); fu_context_lookup_quirk_by_id_iter(self, guid, FU_QUIRKS_FLAGS, fu_context_hwid_quirk_cb, NULL); } fu_progress_step_done(progress); fu_context_detect_full_disk_encryption(self); fu_progress_step_done(progress); fu_context_add_udev_subsystem(self, "firmware-attributes", NULL); if (!fu_context_reload_bios_settings(self, &error_bios_settings)) g_debug("%s", error_bios_settings->message); fu_progress_step_done(progress); /* always */ return TRUE; } /** * fu_context_has_hwid_flag: * @self: a #FuContext * @flag: flag, e.g. `use-legacy-bootmgr-desc` * * Returns if a HwId custom flag exists, typically added from a DMI quirk. * * Returns: %TRUE if the flag exists * * Since: 1.7.2 **/ gboolean fu_context_has_hwid_flag(FuContext *self, const gchar *flag) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), FALSE); g_return_val_if_fail(flag != NULL, FALSE); return g_hash_table_lookup(priv->hwid_flags, flag) != NULL; } /** * fu_context_load_quirks: * @self: a #FuContext * @flags: quirks load flags, e.g. %FU_QUIRKS_LOAD_FLAG_READONLY_FS * @error: (nullable): optional return location for an error * * Loads all quirks into the context. * * Returns: %TRUE for success * * Since: 1.6.0 **/ gboolean fu_context_load_quirks(FuContext *self, FuQuirksLoadFlags flags, GError **error) { FuContextPrivate *priv = GET_PRIVATE(self); g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_CONTEXT(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* rebuild silo if required */ if (!fu_quirks_load(priv->quirks, flags, &error_local)) g_warning("Failed to load quirks: %s", error_local->message); /* always */ return TRUE; } /** * fu_context_get_power_state: * @self: a #FuContext * * Gets if the system is on battery power, e.g. UPS or laptop battery. * * Returns: a power state, e.g. %FU_POWER_STATE_BATTERY_DISCHARGING * * Since: 1.8.11 **/ FuPowerState fu_context_get_power_state(FuContext *self) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), FALSE); return priv->power_state; } /** * fu_context_set_power_state: * @self: a #FuContext * @power_state: a power state, e.g. %FU_POWER_STATE_BATTERY_DISCHARGING * * Sets if the system is on battery power, e.g. UPS or laptop battery. * * Since: 1.8.11 **/ void fu_context_set_power_state(FuContext *self, FuPowerState power_state) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CONTEXT(self)); if (priv->power_state == power_state) return; priv->power_state = power_state; g_info("power state now %s", fu_power_state_to_string(power_state)); g_object_notify(G_OBJECT(self), "power-state"); } /** * fu_context_get_lid_state: * @self: a #FuContext * * Gets the laptop lid state, if applicable. * * Returns: a lid state, e.g. %FU_LID_STATE_CLOSED * * Since: 1.7.4 **/ FuLidState fu_context_get_lid_state(FuContext *self) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), FALSE); return priv->lid_state; } /** * fu_context_set_lid_state: * @self: a #FuContext * @lid_state: a lid state, e.g. %FU_LID_STATE_CLOSED * * Sets the laptop lid state, if applicable. * * Since: 1.7.4 **/ void fu_context_set_lid_state(FuContext *self, FuLidState lid_state) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CONTEXT(self)); if (priv->lid_state == lid_state) return; priv->lid_state = lid_state; g_info("lid state now %s", fu_lid_state_to_string(lid_state)); g_object_notify(G_OBJECT(self), "lid-state"); } /** * fu_context_get_display_state: * @self: a #FuContext * * Gets the display state, if applicable. * * Returns: a display state, e.g. %FU_DISPLAY_STATE_CONNECTED * * Since: 1.9.6 **/ FuDisplayState fu_context_get_display_state(FuContext *self) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), FALSE); return priv->display_state; } /** * fu_context_set_display_state: * @self: a #FuContext * @display_state: a display state, e.g. %FU_DISPLAY_STATE_CONNECTED * * Sets the display state, if applicable. * * Since: 1.9.6 **/ void fu_context_set_display_state(FuContext *self, FuDisplayState display_state) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CONTEXT(self)); if (priv->display_state == display_state) return; priv->display_state = display_state; g_info("display-state now %s", fu_display_state_to_string(display_state)); g_object_notify(G_OBJECT(self), "display-state"); } /** * fu_context_get_battery_level: * @self: a #FuContext * * Gets the system battery level in percent. * * Returns: percentage value, or %FWUPD_BATTERY_LEVEL_INVALID for unknown * * Since: 1.6.0 **/ guint fu_context_get_battery_level(FuContext *self) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), G_MAXUINT); return priv->battery_level; } /** * fu_context_set_battery_level: * @self: a #FuContext * @battery_level: value * * Sets the system battery level in percent. * * Since: 1.6.0 **/ void fu_context_set_battery_level(FuContext *self, guint battery_level) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CONTEXT(self)); g_return_if_fail(battery_level <= FWUPD_BATTERY_LEVEL_INVALID); if (priv->battery_level == battery_level) return; priv->battery_level = battery_level; g_info("battery level now %u", battery_level); g_object_notify(G_OBJECT(self), "battery-level"); } /** * fu_context_get_battery_threshold: * @self: a #FuContext * * Gets the system battery threshold in percent. * * Returns: percentage value, or %FWUPD_BATTERY_LEVEL_INVALID for unknown * * Since: 1.6.0 **/ guint fu_context_get_battery_threshold(FuContext *self) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), G_MAXUINT); return priv->battery_threshold; } /** * fu_context_set_battery_threshold: * @self: a #FuContext * @battery_threshold: value * * Sets the system battery threshold in percent. * * Since: 1.6.0 **/ void fu_context_set_battery_threshold(FuContext *self, guint battery_threshold) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CONTEXT(self)); g_return_if_fail(battery_threshold <= FWUPD_BATTERY_LEVEL_INVALID); if (priv->battery_threshold == battery_threshold) return; priv->battery_threshold = battery_threshold; g_info("battery threshold now %u", battery_threshold); g_object_notify(G_OBJECT(self), "battery-threshold"); } /** * fu_context_add_flag: * @context: a #FuContext * @flag: the context flag * * Adds a specific flag to the context. * * Since: 1.8.5 **/ void fu_context_add_flag(FuContext *context, FuContextFlags flag) { FuContextPrivate *priv = GET_PRIVATE(context); g_return_if_fail(FU_IS_CONTEXT(context)); if (priv->flags & flag) return; priv->flags |= flag; g_object_notify(G_OBJECT(context), "flags"); } /** * fu_context_remove_flag: * @context: a #FuContext * @flag: the context flag * * Removes a specific flag from the context. * * Since: 1.8.10 **/ void fu_context_remove_flag(FuContext *context, FuContextFlags flag) { FuContextPrivate *priv = GET_PRIVATE(context); g_return_if_fail(FU_IS_CONTEXT(context)); if ((priv->flags & flag) == 0) return; priv->flags &= ~flag; g_object_notify(G_OBJECT(context), "flags"); } /** * fu_context_has_flag: * @context: a #FuContext * @flag: the context flag * * Finds if the context has a specific flag. * * Returns: %TRUE if the flag is set * * Since: 1.8.5 **/ gboolean fu_context_has_flag(FuContext *context, FuContextFlags flag) { FuContextPrivate *priv = GET_PRIVATE(context); g_return_val_if_fail(FU_IS_CONTEXT(context), FALSE); return (priv->flags & flag) > 0; } /** * fu_context_add_esp_volume: * @self: a #FuContext * @volume: a #FuVolume * * Adds an ESP volume location. * * Since: 1.8.5 **/ void fu_context_add_esp_volume(FuContext *self, FuVolume *volume) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CONTEXT(self)); g_return_if_fail(FU_IS_VOLUME(volume)); /* check for dupes */ for (guint i = 0; i < priv->esp_volumes->len; i++) { FuVolume *volume_tmp = g_ptr_array_index(priv->esp_volumes, i); if (g_strcmp0(fu_volume_get_id(volume_tmp), fu_volume_get_id(volume)) == 0) { g_debug("not adding duplicate volume %s", fu_volume_get_id(volume)); return; } } /* add */ g_ptr_array_add(priv->esp_volumes, g_object_ref(volume)); } /** * fu_context_set_esp_location: * @self: A #FuContext object. * @location: The path to the preferred ESP. * * Sets the user's desired ESP (EFI System Partition) location for the given #FuContext. */ void fu_context_set_esp_location(FuContext *self, const gchar *location) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CONTEXT(self)); g_return_if_fail(location != NULL); g_free(priv->esp_location); priv->esp_location = g_strdup(location); } /** * fu_context_get_esp_location: * @self: The FuContext object. * * Retrieves the user's desired ESP (EFI System Partition) location for the given #FuContext * * Return: The preferred ESP location as a string. */ const gchar * fu_context_get_esp_location(FuContext *self) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); return priv->esp_location; } /** * fu_context_get_esp_volumes: * @self: a #FuContext * @error: (nullable): optional return location for an error * * Finds all volumes that could be an ESP. * * The volumes are cached and so subsequent calls to this function will be much faster. * * Returns: (transfer container) (element-type FuVolume): a #GPtrArray, or %NULL if no ESP was found * * Since: 1.8.5 **/ GPtrArray * fu_context_get_esp_volumes(FuContext *self, GError **error) { FuContextPrivate *priv = GET_PRIVATE(self); const gchar *path_tmp; g_autoptr(GError) error_bdp = NULL; g_autoptr(GError) error_esp = NULL; g_autoptr(GPtrArray) volumes_bdp = NULL; g_autoptr(GPtrArray) volumes_esp = NULL; g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* cached result */ if (priv->esp_volumes->len > 0) return g_ptr_array_ref(priv->esp_volumes); /* for the test suite use local directory for ESP */ path_tmp = g_getenv("FWUPD_UEFI_ESP_PATH"); if (path_tmp != NULL) { g_autoptr(FuVolume) vol = fu_volume_new_from_mount_path(path_tmp); fu_volume_set_partition_kind(vol, FU_VOLUME_KIND_ESP); fu_volume_set_partition_uuid(vol, "00000000-0000-0000-0000-000000000000"); fu_context_add_esp_volume(self, vol); return g_ptr_array_ref(priv->esp_volumes); } /* ESP */ volumes_esp = fu_volume_new_by_kind(FU_VOLUME_KIND_ESP, &error_esp); if (volumes_esp == NULL) { g_debug("%s", error_esp->message); } else { for (guint i = 0; i < volumes_esp->len; i++) { FuVolume *vol = g_ptr_array_index(volumes_esp, i); g_autofree gchar *type = fu_volume_get_id_type(vol); if (g_strcmp0(type, "vfat") != 0) continue; fu_context_add_esp_volume(self, vol); } } /* BDP */ volumes_bdp = fu_volume_new_by_kind(FU_VOLUME_KIND_BDP, &error_bdp); if (volumes_bdp == NULL) { g_debug("%s", error_bdp->message); } else { for (guint i = 0; i < volumes_bdp->len; i++) { FuVolume *vol = g_ptr_array_index(volumes_bdp, i); g_autofree gchar *type = fu_volume_get_id_type(vol); if (g_strcmp0(type, "vfat") != 0) continue; if (!fu_volume_is_internal(vol)) continue; fu_context_add_esp_volume(self, vol); } } /* nothing found */ if (priv->esp_volumes->len == 0) { g_autoptr(GPtrArray) devices = NULL; /* check if udisks2 is working */ devices = fu_common_get_block_devices(error); if (devices == NULL) return NULL; g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "No ESP or BDP found"); return NULL; } /* success */ return g_ptr_array_ref(priv->esp_volumes); } static gboolean fu_context_is_esp(FuVolume *esp) { g_autofree gchar *mount_point = fu_volume_get_mount_point(esp); g_autofree gchar *fn = NULL; g_autofree gchar *fn2 = NULL; if (mount_point == NULL) return FALSE; fn = g_build_filename(mount_point, "EFI", NULL); fn2 = g_build_filename(mount_point, "efi", NULL); return g_file_test(fn, G_FILE_TEST_IS_DIR) || g_file_test(fn2, G_FILE_TEST_IS_DIR); } static gboolean fu_context_is_esp_linux(FuVolume *esp, GError **error) { const gchar *prefixes[] = {"grub", "shim", "systemd-boot", "zfsbootmenu", NULL}; g_autofree gchar *prefixes_str = NULL; g_autofree gchar *mount_point = fu_volume_get_mount_point(esp); g_autoptr(GPtrArray) files = NULL; /* look for any likely basenames */ if (mount_point == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no mountpoint for ESP"); return FALSE; } files = fu_path_get_files(mount_point, error); if (files == NULL) return FALSE; for (guint i = 0; i < files->len; i++) { const gchar *fn = g_ptr_array_index(files, i); g_autofree gchar *basename = g_path_get_basename(fn); g_autofree gchar *basename_lower = g_utf8_strdown(basename, -1); for (guint j = 0; prefixes[j] != NULL; j++) { if (!g_str_has_prefix(basename_lower, prefixes[j])) continue; if (!g_str_has_suffix(basename_lower, ".efi")) continue; g_info("found %s which indicates a Linux ESP, using %s", fn, mount_point); return TRUE; } } /* failed */ prefixes_str = g_strjoinv("|", (gchar **)prefixes); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "did not any files with prefix %s in %s", prefixes_str, mount_point); return FALSE; } static gint fu_context_sort_esp_score_cb(gconstpointer a, gconstpointer b, gpointer user_data) { GHashTable *esp_scores = (GHashTable *)user_data; guint esp1_score = GPOINTER_TO_UINT(g_hash_table_lookup(esp_scores, *((FuVolume **)a))); guint esp2_score = GPOINTER_TO_UINT(g_hash_table_lookup(esp_scores, *((FuVolume **)b))); if (esp1_score < esp2_score) return 1; if (esp1_score > esp2_score) return -1; return 0; } /** * fu_context_get_default_esp: * @self: a #FuContext * @error: (nullable): optional return location for an error * * Finds the volume that represents the ESP that plugins should nominally * use for accessing storing data. * * Returns: (transfer full): a volume, or %NULL if no ESP was found * * Since: 2.0.0 **/ FuVolume * fu_context_get_default_esp(FuContext *self, GError **error) { FuContextPrivate *priv = GET_PRIVATE(self); g_autoptr(GPtrArray) esp_volumes = NULL; const gchar *user_esp_location = fu_context_get_esp_location(self); /* show which volumes we're choosing from */ esp_volumes = fu_context_get_esp_volumes(self, error); if (esp_volumes == NULL) return NULL; /* no mounting */ if (priv->flags & FU_CONTEXT_FLAG_INHIBIT_VOLUME_MOUNT) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot mount volume by policy"); return NULL; } /* we found more than one: lets look for the best one */ if (esp_volumes->len > 1) { g_autoptr(GString) str = g_string_new("more than one ESP possible:"); g_autoptr(GHashTable) esp_scores = g_hash_table_new(g_direct_hash, g_direct_equal); for (guint i = 0; i < esp_volumes->len; i++) { FuVolume *esp = g_ptr_array_index(esp_volumes, i); guint score = 0; g_autofree gchar *kind = NULL; g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GError) error_local = NULL; /* ignore the volume completely if we cannot mount it */ locker = fu_volume_locker(esp, &error_local); if (locker == NULL) { g_warning("failed to mount ESP: %s", error_local->message); continue; } /* if user specified, make sure that it matches */ if (user_esp_location != NULL) { g_autofree gchar *mount = fu_volume_get_mount_point(esp); if (g_strcmp0(mount, user_esp_location) != 0) { g_debug("skipping %s as it's not the user " "specified ESP", mount); continue; } } if (!fu_context_is_esp(esp)) { g_debug("not an ESP: %s", fu_volume_get_id(esp)); continue; } /* big partitions are better than small partitions */ score += fu_volume_get_size(esp) / (1024 * 1024); /* prefer partitions with the ESP flag set over msftdata */ kind = fu_volume_get_partition_kind(esp); if (g_strcmp0(kind, FU_VOLUME_KIND_ESP) == 0) score += 0x20000; /* prefer linux ESP */ if (!fu_context_is_esp_linux(esp, &error_local)) { g_debug("not a Linux ESP: %s", error_local->message); } else { score += 0x10000; } g_hash_table_insert(esp_scores, (gpointer)esp, GUINT_TO_POINTER(score)); } if (g_hash_table_size(esp_scores) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no EFI system partition found"); return NULL; } g_ptr_array_sort_with_data(esp_volumes, fu_context_sort_esp_score_cb, esp_scores); for (guint i = 0; i < esp_volumes->len; i++) { FuVolume *esp = g_ptr_array_index(esp_volumes, i); guint score = GPOINTER_TO_UINT(g_hash_table_lookup(esp_scores, esp)); g_string_append_printf(str, "\n - 0x%x:\t%s", score, fu_volume_get_id(esp)); } g_debug("%s", str->str); } if (esp_volumes->len == 1) { FuVolume *esp = g_ptr_array_index(esp_volumes, 0); g_autoptr(FuDeviceLocker) locker = NULL; /* ensure it can be mounted */ locker = fu_volume_locker(esp, error); if (locker == NULL) return NULL; /* if user specified, does it match mountpoints ? */ if (user_esp_location != NULL) { g_autofree gchar *mount = fu_volume_get_mount_point(esp); if (g_strcmp0(mount, user_esp_location) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "user specified ESP %s not found", user_esp_location); return NULL; } } } /* "success" */ return g_object_ref(g_ptr_array_index(esp_volumes, 0)); } /** * fu_context_get_esp_volume_by_hard_drive_device_path: * @self: a #FuContext * @dp: a #FuEfiHardDriveDevicePath * @error: (nullable): optional return location for an error * * Gets a volume that matches the EFI device path * * Returns: (transfer full): a volume, or %NULL if it was not found * * Since: 2.0.0 **/ FuVolume * fu_context_get_esp_volume_by_hard_drive_device_path(FuContext *self, FuEfiHardDriveDevicePath *dp, GError **error) { g_autoptr(GPtrArray) volumes = NULL; g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); g_return_val_if_fail(FU_IS_EFI_HARD_DRIVE_DEVICE_PATH(dp), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); volumes = fu_context_get_esp_volumes(self, error); if (volumes == NULL) return NULL; for (guint i = 0; i < volumes->len; i++) { FuVolume *volume = g_ptr_array_index(volumes, i); g_autoptr(GError) error_local = NULL; g_autoptr(FuEfiHardDriveDevicePath) dp_tmp = NULL; dp_tmp = fu_efi_hard_drive_device_path_new_from_volume(volume, &error_local); if (dp_tmp == NULL) { g_debug("%s", error_local->message); continue; } if (!fu_efi_hard_drive_device_path_compare(dp, dp_tmp)) continue; return g_object_ref(volume); } /* failed */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "could not find EFI DP"); return NULL; } static FuFirmware * fu_context_esp_load_pe_file(const gchar *filename, GError **error) { g_autoptr(FuFirmware) firmware = fu_pefile_firmware_new(); g_autoptr(GFile) file = g_file_new_for_path(filename); fu_firmware_set_filename(firmware, filename); if (!fu_firmware_parse_file(firmware, file, FU_FIRMWARE_PARSE_FLAG_NONE, error)) { g_prefix_error(error, "failed to load %s: ", filename); return NULL; } return g_steal_pointer(&firmware); } static gchar * fu_context_build_uefi_basename_for_arch(const gchar *app_name) { #if defined(__x86_64__) return g_strdup_printf("%sx64.efi", app_name); #endif #if defined(__aarch64__) return g_strdup_printf("%saa64.efi", app_name); #endif #if defined(__loongarch_lp64) return g_strdup_printf("%sloongarch64.efi", app_name); #endif #if (defined(__riscv) && __riscv_xlen == 64) return g_strdup_printf("%sriscv64.efi", app_name); #endif #if defined(__i386__) || defined(__i686__) return g_strdup_printf("%sia32.efi", app_name); #endif #if defined(__arm__) return g_strdup_printf("%sarm.efi", app_name); #endif return NULL; } static gboolean fu_context_get_esp_files_for_entry(FuContext *self, FuEfiLoadOption *entry, GPtrArray *files, FuContextEspFileFlags flags, GError **error) { FuContextPrivate *priv = GET_PRIVATE(self); g_autofree gchar *dp_filename = NULL; g_autofree gchar *filename = NULL; g_autofree gchar *mount_point = NULL; g_autofree gchar *shim_name = fu_context_build_uefi_basename_for_arch("shim"); g_autoptr(FuDeviceLocker) volume_locker = NULL; g_autoptr(FuEfiFilePathDevicePath) dp_path = NULL; g_autoptr(FuEfiHardDriveDevicePath) dp_hdd = NULL; g_autoptr(FuFirmware) dp_list = NULL; g_autoptr(FuVolume) volume = NULL; /* all entries should have a list */ dp_list = fu_firmware_get_image_by_gtype(FU_FIRMWARE(entry), FU_TYPE_EFI_DEVICE_PATH_LIST, NULL); if (dp_list == NULL) return TRUE; /* HDD */ dp_hdd = FU_EFI_HARD_DRIVE_DEVICE_PATH( fu_firmware_get_image_by_gtype(FU_FIRMWARE(dp_list), FU_TYPE_EFI_HARD_DRIVE_DEVICE_PATH, NULL)); if (dp_hdd == NULL) return TRUE; /* FILE */ dp_path = FU_EFI_FILE_PATH_DEVICE_PATH( fu_firmware_get_image_by_gtype(FU_FIRMWARE(dp_list), FU_TYPE_EFI_FILE_PATH_DEVICE_PATH, NULL)); if (dp_path == NULL) return TRUE; /* can we match the volume? */ volume = fu_context_get_esp_volume_by_hard_drive_device_path(self, dp_hdd, error); if (volume == NULL) return FALSE; if (priv->flags & FU_CONTEXT_FLAG_INHIBIT_VOLUME_MOUNT) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot mount volume by policy"); return FALSE; } volume_locker = fu_volume_locker(volume, error); if (volume_locker == NULL) return FALSE; dp_filename = fu_efi_file_path_device_path_get_name(dp_path, error); if (dp_filename == NULL) return FALSE; /* the file itself */ mount_point = fu_volume_get_mount_point(volume); filename = g_build_filename(mount_point, dp_filename, NULL); g_debug("check for 1st stage bootloader: %s", filename); if (flags & FU_CONTEXT_ESP_FILE_FLAG_INCLUDE_FIRST_STAGE) { g_autoptr(FuFirmware) firmware = NULL; g_autoptr(GError) error_local = NULL; /* ignore if the file cannot be loaded as a PE file */ firmware = fu_context_esp_load_pe_file(filename, &error_local); if (firmware == NULL) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE)) { g_debug("ignoring: %s", error_local->message); } else { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } else { fu_firmware_set_idx(firmware, fu_firmware_get_idx(FU_FIRMWARE(entry))); g_ptr_array_add(files, g_steal_pointer(&firmware)); } } /* the 2nd stage bootloader, typically grub */ if (flags & FU_CONTEXT_ESP_FILE_FLAG_INCLUDE_SECOND_STAGE && g_str_has_suffix(filename, shim_name)) { g_autoptr(FuFirmware) firmware = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(GString) filename2 = g_string_new(filename); const gchar *path; path = fu_efi_load_option_get_metadata(entry, FU_EFI_LOAD_OPTION_METADATA_PATH, NULL); if (path != NULL) { g_string_replace(filename2, shim_name, path, 1); } else { g_autofree gchar *grub_name = fu_context_build_uefi_basename_for_arch("grub"); g_string_replace(filename2, shim_name, grub_name, 1); } g_debug("check for 2nd stage bootloader: %s", filename2->str); /* ignore if the file cannot be loaded as a PE file */ firmware = fu_context_esp_load_pe_file(filename2->str, &error_local); if (firmware == NULL) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE)) { g_debug("ignoring: %s", error_local->message); } else { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } else { fu_firmware_set_idx(firmware, fu_firmware_get_idx(FU_FIRMWARE(entry))); g_ptr_array_add(files, g_steal_pointer(&firmware)); } } /* revocations, typically for SBAT */ if (flags & FU_CONTEXT_ESP_FILE_FLAG_INCLUDE_REVOCATIONS && g_str_has_suffix(filename, shim_name)) { g_autoptr(GString) filename2 = g_string_new(filename); g_autoptr(FuFirmware) firmware = NULL; g_autoptr(GError) error_local = NULL; g_string_replace(filename2, shim_name, "revocations.efi", 1); g_debug("check for revocation: %s", filename2->str); /* ignore if the file cannot be loaded as a PE file */ firmware = fu_context_esp_load_pe_file(filename2->str, &error_local); if (firmware == NULL) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE)) { g_debug("ignoring: %s", error_local->message); } else { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } else { fu_firmware_set_idx(firmware, fu_firmware_get_idx(FU_FIRMWARE(entry))); g_ptr_array_add(files, g_steal_pointer(&firmware)); } } /* success */ return TRUE; } /** * fu_context_get_esp_files: * @self: a #FuContext * @flags: some #FuContextEspFileFlags, e.g. #FU_CONTEXT_ESP_FILE_FLAG_INCLUDE_FIRST_STAGE * @error: #GError * * Gets the PE files for all the entries listed in `BootOrder`. * * Returns: (transfer full) (element-type FuPefileFirmware): PE firmware data * * Since: 2.0.0 **/ GPtrArray * fu_context_get_esp_files(FuContext *self, FuContextEspFileFlags flags, GError **error) { FuContextPrivate *priv = GET_PRIVATE(self); g_autoptr(GPtrArray) entries = NULL; g_autoptr(GPtrArray) files = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); entries = fu_efivars_get_boot_entries(priv->efivars, error); if (entries == NULL) return NULL; for (guint i = 0; i < entries->len; i++) { FuEfiLoadOption *entry = g_ptr_array_index(entries, i); g_autoptr(GError) error_local = NULL; if (!fu_context_get_esp_files_for_entry(self, entry, files, flags, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE)) { g_debug("ignoring %s: %s", fu_firmware_get_id(FU_FIRMWARE(entry)), error_local->message); continue; } g_propagate_error(error, g_steal_pointer(&error_local)); return NULL; } } /* success */ return g_steal_pointer(&files); } /** * fu_context_get_backends: * @self: a #FuContext * * Gets all the possible backends used by all plugins. * * Returns: (transfer none) (element-type FuBackend): List of backends * * Since: 2.0.0 **/ GPtrArray * fu_context_get_backends(FuContext *self) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); return priv->backends; } /** * fu_context_add_backend: * @self: a #FuContext * @backend: a #FuBackend * * Adds a backend to the context. * * Returns: (transfer full): a #FuBackend, or %NULL on error * * Since: 2.0.0 **/ void fu_context_add_backend(FuContext *self, FuBackend *backend) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CONTEXT(self)); g_return_if_fail(FU_IS_BACKEND(backend)); g_ptr_array_add(priv->backends, g_object_ref(backend)); } /** * fu_context_get_backend_by_name: * @self: a #FuContext * @name: backend name, e.g. `udev` or `bluez` * @error: (nullable): optional return location for an error * * Gets a specific backend added to the context. * * Returns: (transfer full): a #FuBackend, or %NULL on error * * Since: 2.0.0 **/ FuBackend * fu_context_get_backend_by_name(FuContext *self, const gchar *name, GError **error) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); g_return_val_if_fail(name != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); for (guint i = 0; i < priv->backends->len; i++) { FuBackend *backend = g_ptr_array_index(priv->backends, i); if (g_strcmp0(fu_backend_get_name(backend), name) == 0) return g_object_ref(backend); } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no backend with name %s", name); return NULL; } /* private */ gboolean fu_context_has_backend(FuContext *self, const gchar *name) { FuContextPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(self), FALSE); g_return_val_if_fail(name != NULL, FALSE); for (guint i = 0; i < priv->backends->len; i++) { FuBackend *backend = g_ptr_array_index(priv->backends, i); if (g_strcmp0(fu_backend_get_name(backend), name) == 0) return TRUE; } return FALSE; } /* private */ gpointer fu_context_get_data(FuContext *self, const gchar *key) { g_return_val_if_fail(FU_IS_CONTEXT(self), NULL); g_return_val_if_fail(key != NULL, NULL); return g_object_get_data(G_OBJECT(self), key); } /* private */ void fu_context_set_data(FuContext *self, const gchar *key, gpointer data) { g_return_if_fail(FU_IS_CONTEXT(self)); g_return_if_fail(key != NULL); g_object_set_data(G_OBJECT(self), key, data); } static void fu_context_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuContext *self = FU_CONTEXT(object); FuContextPrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_POWER_STATE: g_value_set_uint(value, priv->power_state); break; case PROP_LID_STATE: g_value_set_uint(value, priv->lid_state); break; case PROP_DISPLAY_STATE: g_value_set_uint(value, priv->display_state); break; case PROP_BATTERY_LEVEL: g_value_set_uint(value, priv->battery_level); break; case PROP_BATTERY_THRESHOLD: g_value_set_uint(value, priv->battery_threshold); break; case PROP_FLAGS: g_value_set_uint64(value, priv->flags); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_context_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuContext *self = FU_CONTEXT(object); FuContextPrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_POWER_STATE: fu_context_set_power_state(self, g_value_get_uint(value)); break; case PROP_LID_STATE: fu_context_set_lid_state(self, g_value_get_uint(value)); break; case PROP_DISPLAY_STATE: fu_context_set_display_state(self, g_value_get_uint(value)); break; case PROP_BATTERY_LEVEL: fu_context_set_battery_level(self, g_value_get_uint(value)); break; case PROP_BATTERY_THRESHOLD: fu_context_set_battery_threshold(self, g_value_get_uint(value)); break; case PROP_FLAGS: priv->flags = g_value_get_uint64(value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_context_dispose(GObject *object) { FuContext *self = FU_CONTEXT(object); FuContextPrivate *priv = GET_PRIVATE(self); g_ptr_array_set_size(priv->backends, 0); G_OBJECT_CLASS(fu_context_parent_class)->dispose(object); } static void fu_context_finalize(GObject *object) { FuContext *self = FU_CONTEXT(object); FuContextPrivate *priv = GET_PRIVATE(self); if (priv->fdt != NULL) g_object_unref(priv->fdt); if (priv->efivars != NULL) g_object_unref(priv->efivars); g_free(priv->esp_location); g_hash_table_unref(priv->runtime_versions); g_hash_table_unref(priv->compile_versions); g_object_unref(priv->hwids); g_object_unref(priv->config); g_hash_table_unref(priv->hwid_flags); g_object_unref(priv->quirks); g_object_unref(priv->smbios); g_object_unref(priv->host_bios_settings); g_hash_table_unref(priv->firmware_gtypes); g_hash_table_unref(priv->udev_subsystems); g_ptr_array_unref(priv->esp_volumes); g_ptr_array_unref(priv->backends); G_OBJECT_CLASS(fu_context_parent_class)->finalize(object); } static void fu_context_class_init(FuContextClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); GParamSpec *pspec; object_class->dispose = fu_context_dispose; object_class->get_property = fu_context_get_property; object_class->set_property = fu_context_set_property; /** * FuContext:power-state: * * The system power state. * * Since: 1.8.11 */ pspec = g_param_spec_uint("power-state", NULL, NULL, FU_POWER_STATE_UNKNOWN, FU_POWER_STATE_LAST, FU_POWER_STATE_UNKNOWN, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_POWER_STATE, pspec); /** * FuContext:lid-state: * * The system lid state. * * Since: 1.7.4 */ pspec = g_param_spec_uint("lid-state", NULL, NULL, FU_LID_STATE_UNKNOWN, FU_LID_STATE_LAST, FU_LID_STATE_UNKNOWN, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_LID_STATE, pspec); /** * FuContext:display-state: * * The display state. * * Since: 1.9.6 */ pspec = g_param_spec_uint("display-state", NULL, NULL, FU_DISPLAY_STATE_UNKNOWN, FU_DISPLAY_STATE_LAST, FU_DISPLAY_STATE_UNKNOWN, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_DISPLAY_STATE, pspec); /** * FuContext:battery-level: * * The system battery level in percent. * * Since: 1.6.0 */ pspec = g_param_spec_uint("battery-level", NULL, NULL, 0, FWUPD_BATTERY_LEVEL_INVALID, FWUPD_BATTERY_LEVEL_INVALID, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_BATTERY_LEVEL, pspec); /** * FuContext:battery-threshold: * * The system battery threshold in percent. * * Since: 1.6.0 */ pspec = g_param_spec_uint("battery-threshold", NULL, NULL, 0, FWUPD_BATTERY_LEVEL_INVALID, FWUPD_BATTERY_LEVEL_INVALID, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_BATTERY_THRESHOLD, pspec); /** * FuContext:flags: * * The context flags. * * Since: 1.8.10 */ pspec = g_param_spec_uint64("flags", NULL, NULL, FU_CONTEXT_FLAG_NONE, G_MAXUINT64, FU_CONTEXT_FLAG_NONE, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_FLAGS, pspec); /** * FuContext::security-changed: * @self: the #FuContext instance that emitted the signal * * The ::security-changed signal is emitted when some system state has changed that could * have affected the security level. * * Since: 1.6.0 **/ signals[SIGNAL_SECURITY_CHANGED] = g_signal_new("security-changed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(FuContextClass, security_changed), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); /** * FuContext::housekeeping: * @self: the #FuContext instance that emitted the signal * * The ::housekeeping signal is emitted when helper objects should do house-keeping actions * when the daemon is idle. * * Since: 2.0.0 **/ signals[SIGNAL_HOUSEKEEPING] = g_signal_new("housekeeping", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(FuContextClass, housekeeping), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); object_class->finalize = fu_context_finalize; } static void fu_context_init(FuContext *self) { FuContextPrivate *priv = GET_PRIVATE(self); priv->chassis_kind = FU_SMBIOS_CHASSIS_KIND_UNKNOWN; priv->battery_level = FWUPD_BATTERY_LEVEL_INVALID; priv->battery_threshold = FWUPD_BATTERY_LEVEL_INVALID; priv->smbios = fu_smbios_new(); priv->hwids = fu_hwids_new(); priv->config = fu_config_new(); priv->efivars = g_strcmp0(g_getenv("FWUPD_EFIVARS"), "dummy") == 0 ? fu_dummy_efivars_new() : fu_efivars_new(); priv->hwid_flags = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); priv->udev_subsystems = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_ptr_array_unref); priv->firmware_gtypes = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); priv->quirks = fu_quirks_new(self); priv->host_bios_settings = fu_bios_settings_new(); priv->esp_volumes = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); priv->runtime_versions = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); priv->compile_versions = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); priv->backends = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); } /** * fu_context_new: * * Creates a new #FuContext * * Returns: (transfer full): the object * * Since: 1.6.0 **/ FuContext * fu_context_new(void) { return FU_CONTEXT(g_object_new(FU_TYPE_CONTEXT, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-context.h000066400000000000000000000203521501337203100202140ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-bios-settings.h" #include "fu-common-struct.h" #include "fu-common.h" #include "fu-efi-hard-drive-device-path.h" #include "fu-efivars.h" #include "fu-firmware.h" #include "fu-smbios-struct.h" #define FU_TYPE_CONTEXT (fu_context_get_type()) G_DECLARE_DERIVABLE_TYPE(FuContext, fu_context, FU, CONTEXT, GObject) struct _FuContextClass { GObjectClass parent_class; /* signals */ void (*security_changed)(FuContext *self); void (*housekeeping)(FuContext *self); }; /** * FuContextQuirkSource: * * The source of the quirk data, ordered by how good the data is. **/ typedef enum { /** * FU_CONTEXT_QUIRK_SOURCE_DEVICE: * * From the device itself, perhaps a USB descriptor. * * Since: 2.0.2 **/ FU_CONTEXT_QUIRK_SOURCE_DEVICE, /** * FU_CONTEXT_QUIRK_SOURCE_FILE: * * From an internal `.quirk` file. * * Since: 2.0.2 **/ FU_CONTEXT_QUIRK_SOURCE_FILE, /** * FU_CONTEXT_QUIRK_SOURCE_DB: * * From the database, populated from `usb.ids` and `pci.ids`. * * Since: 2.0.2 **/ FU_CONTEXT_QUIRK_SOURCE_DB, /** * FU_CONTEXT_QUIRK_SOURCE_FALLBACK: * * A good fallback, perhaps from the PCI class information. * * Since: 2.0.2 **/ FU_CONTEXT_QUIRK_SOURCE_FALLBACK, } FuContextQuirkSource; /** * FuContextLookupIter: * @self: a #FuContext * @key: a key * @value: a value * @source: a #FuContextQuirkSource, e.g. %FU_CONTEXT_QUIRK_SOURCE_DB * @user_data: user data * * The context lookup iteration callback. */ typedef void (*FuContextLookupIter)(FuContext *self, const gchar *key, const gchar *value, FuContextQuirkSource source, gpointer user_data); /** * FuContextFlags: * * The context flags. **/ typedef enum { /** * FU_CONTEXT_FLAG_NONE: * * No flags set. * * Since: 1.8.5 **/ FU_CONTEXT_FLAG_NONE = 0, /** * FU_CONTEXT_FLAG_SAVE_EVENTS: * * Save events so that they can be replayed to emulate devices. * * Since: 1.8.5 **/ FU_CONTEXT_FLAG_SAVE_EVENTS = 1u << 0, /** * FU_CONTEXT_FLAG_SYSTEM_INHIBIT: * * All devices are not updatable due to a system-wide inhibit. * * Since: 1.8.10 **/ FU_CONTEXT_FLAG_SYSTEM_INHIBIT = 1u << 1, /** * FU_CONTEXT_FLAG_LOADED_HWINFO: * * Hardware information has been loaded with a call to fu_context_load_hwinfo(). * * Since: 1.9.10 **/ FU_CONTEXT_FLAG_LOADED_HWINFO = 1u << 2, /** * FU_CONTEXT_FLAG_INHIBIT_VOLUME_MOUNT: * * Do not allow mounting volumes, usually set in self tests. * * Since: 2.0.2 **/ FU_CONTEXT_FLAG_INHIBIT_VOLUME_MOUNT = 1u << 3, /** * FU_CONTEXT_FLAG_FDE_BITLOCKER: * * Bitlocker style full disk encryption is in use * * Since: 2.0.5 **/ FU_CONTEXT_FLAG_FDE_BITLOCKER = 1u << 4, /** * FU_CONTEXT_FLAG_FDE_SNAPD: * * Snapd style full disk encryption is in use * * Since: 2.0.5 **/ FU_CONTEXT_FLAG_FDE_SNAPD = 1u << 5, /** * FU_CONTEXT_FLAG_LOADED_UNKNOWN: * * Unknown flag value. * * Since: 2.0.0 **/ FU_CONTEXT_FLAG_LOADED_UNKNOWN = G_MAXUINT64, } FuContextFlags; void fu_context_add_flag(FuContext *context, FuContextFlags flag) G_GNUC_NON_NULL(1); void fu_context_remove_flag(FuContext *context, FuContextFlags flag) G_GNUC_NON_NULL(1); gboolean fu_context_has_flag(FuContext *context, FuContextFlags flag) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); const gchar * fu_context_get_smbios_string(FuContext *self, guint8 type, guint8 length, guint8 offset, GError **error) G_GNUC_NON_NULL(1); guint fu_context_get_smbios_integer(FuContext *self, guint8 type, guint8 length, guint8 offset, GError **error) G_GNUC_NON_NULL(1); GPtrArray * fu_context_get_smbios_data(FuContext *self, guint8 type, guint8 length, GError **error) G_GNUC_NON_NULL(1); gboolean fu_context_has_hwid_guid(FuContext *self, const gchar *guid) G_GNUC_NON_NULL(1); GPtrArray * fu_context_get_hwid_guids(FuContext *self) G_GNUC_NON_NULL(1); gboolean fu_context_has_hwid_flag(FuContext *self, const gchar *flag) G_GNUC_NON_NULL(1, 2); const gchar * fu_context_get_hwid_value(FuContext *self, const gchar *key) G_GNUC_NON_NULL(1, 2); gchar * fu_context_get_hwid_replace_value(FuContext *self, const gchar *keys, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); void fu_context_add_runtime_version(FuContext *self, const gchar *component_id, const gchar *version) G_GNUC_NON_NULL(1, 2, 3); const gchar * fu_context_get_runtime_version(FuContext *self, const gchar *component_id) G_GNUC_NON_NULL(1, 2); void fu_context_add_compile_version(FuContext *self, const gchar *component_id, const gchar *version) G_GNUC_NON_NULL(1, 2, 3); const gchar * fu_context_lookup_quirk_by_id(FuContext *self, const gchar *guid, const gchar *key) G_GNUC_NON_NULL(1, 2, 3); gboolean fu_context_lookup_quirk_by_id_iter(FuContext *self, const gchar *guid, const gchar *key, FuContextLookupIter iter_cb, gpointer user_data) G_GNUC_NON_NULL(1, 2); void fu_context_add_quirk_key(FuContext *self, const gchar *key) G_GNUC_NON_NULL(1, 2); void fu_context_security_changed(FuContext *self) G_GNUC_NON_NULL(1); FuPowerState fu_context_get_power_state(FuContext *self) G_GNUC_NON_NULL(1); void fu_context_set_power_state(FuContext *self, FuPowerState power_state) G_GNUC_NON_NULL(1); FuLidState fu_context_get_lid_state(FuContext *self) G_GNUC_NON_NULL(1); void fu_context_set_lid_state(FuContext *self, FuLidState lid_state) G_GNUC_NON_NULL(1); FuDisplayState fu_context_get_display_state(FuContext *self) G_GNUC_NON_NULL(1); void fu_context_set_display_state(FuContext *self, FuDisplayState display_state) G_GNUC_NON_NULL(1); guint fu_context_get_battery_level(FuContext *self) G_GNUC_NON_NULL(1); void fu_context_set_battery_level(FuContext *self, guint battery_level) G_GNUC_NON_NULL(1); guint fu_context_get_battery_threshold(FuContext *self) G_GNUC_NON_NULL(1); void fu_context_set_battery_threshold(FuContext *self, guint battery_threshold) G_GNUC_NON_NULL(1); FuBiosSettings * fu_context_get_bios_settings(FuContext *self) G_GNUC_NON_NULL(1); gboolean fu_context_get_bios_setting_pending_reboot(FuContext *self) G_GNUC_NON_NULL(1); FwupdBiosSetting * fu_context_get_bios_setting(FuContext *self, const gchar *name) G_GNUC_NON_NULL(1, 2); GPtrArray * fu_context_get_esp_volumes(FuContext *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); FuVolume * fu_context_get_default_esp(FuContext *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); FuVolume * fu_context_get_esp_volume_by_hard_drive_device_path(FuContext *self, FuEfiHardDriveDevicePath *dp, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); FuFirmware * fu_context_get_fdt(FuContext *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); FuSmbiosChassisKind fu_context_get_chassis_kind(FuContext *self) G_GNUC_NON_NULL(1); void fu_context_set_esp_location(FuContext *self, const gchar *location); const gchar * fu_context_get_esp_location(FuContext *self); FuEfivars * fu_context_get_efivars(FuContext *self) G_GNUC_NON_NULL(1); /** * FuContextEspFileFlags: * * The flags to use when loading files in the ESP. **/ typedef enum { /** * FU_CONTEXT_ESP_FILE_FLAG_NONE: * * No flags set. * * Since: 2.0.0 **/ FU_CONTEXT_ESP_FILE_FLAG_NONE = 0, /** * FU_CONTEXT_ESP_FILE_FLAG_INCLUDE_FIRST_STAGE: * * Include 1st stage bootloaders like shim. * * Since: 2.0.0 **/ FU_CONTEXT_ESP_FILE_FLAG_INCLUDE_FIRST_STAGE = 1 << 0, /** * FU_CONTEXT_ESP_FILE_FLAG_INCLUDE_SECOND_STAGE: * * Include 2nd stage bootloaders like shim. * * Since: 2.0.0 **/ FU_CONTEXT_ESP_FILE_FLAG_INCLUDE_SECOND_STAGE = 1 << 1, /** * FU_CONTEXT_ESP_FILE_FLAG_INCLUDE_REVOCATIONS: * * Include revokcations, for example the `revocations.efi` file used by shim. * * Since: 2.0.0 **/ FU_CONTEXT_ESP_FILE_FLAG_INCLUDE_REVOCATIONS = 1 << 2, /** * FU_CONTEXT_ESP_FILE_FLAG_UNKNOWN: * * Unknown flag value. * * Since: 2.0.0 **/ FU_CONTEXT_ESP_FILE_FLAG_UNKNOWN = G_MAXUINT64, } FuContextEspFileFlags; GPtrArray * fu_context_get_esp_files(FuContext *self, FuContextEspFileFlags flags, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-coswid-common.c000066400000000000000000000265111501337203100213040ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include #include "fu-coswid-common.h" #ifdef HAVE_CBOR /** * fu_coswid_read_string: * @item: a #cbor_item_t * @value: read value * @error: (nullable): optional return location for an error * * Reads a string value. If a bytestring is provided it is converted to a GUID. * * Returns: %TRUE for success * * Since: 1.9.17 **/ gchar * fu_coswid_read_string(cbor_item_t *item, GError **error) { g_return_val_if_fail(item != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); if (cbor_isa_string(item)) { if (cbor_string_handle(item) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "item has no string set"); return NULL; } return g_strndup((const gchar *)cbor_string_handle(item), cbor_string_length(item)); } if (cbor_isa_bytestring(item) && cbor_bytestring_length(item) == 16) { return fwupd_guid_to_string((const fwupd_guid_t *)cbor_bytestring_handle(item), FWUPD_GUID_FLAG_NONE); } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "item is not a string or GUID bytestring"); return NULL; } /** * fu_coswid_read_byte_array: * @item: a #cbor_item_t * @value: read value * @error: (nullable): optional return location for an error * * Reads a bytestring value as a #GByteArray. * * Returns: %TRUE for success * * Since: 1.9.17 **/ GByteArray * fu_coswid_read_byte_array(cbor_item_t *item, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_return_val_if_fail(item != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); if (!cbor_isa_bytestring(item)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "item is not a bytestring"); return NULL; } if (cbor_bytestring_handle(item) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "item has no bytestring set"); return NULL; } g_byte_array_append(buf, cbor_bytestring_handle(item), cbor_bytestring_length(item)); return g_steal_pointer(&buf); } /** * fu_coswid_read_tag: * @item: a #cbor_item_t * @value: read value * @error: (nullable): optional return location for an error * * Reads a tag value. * * Returns: %TRUE for success * * Since: 1.9.17 **/ gboolean fu_coswid_read_tag(cbor_item_t *item, FuCoswidTag *value, GError **error) { guint64 tmp; g_return_val_if_fail(item != NULL, FALSE); g_return_val_if_fail(value != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!cbor_isa_uint(item)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "tag item is not a uint"); return FALSE; } tmp = cbor_get_int(item); if (tmp > G_MAXUINT8) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "0x%x is too large for tag", (guint)tmp); return FALSE; } *value = (FuCoswidTag)tmp; return TRUE; } /** * fu_coswid_read_version_scheme: * @item: a #cbor_item_t * @value: read value * @error: (nullable): optional return location for an error * * Reads a version-scheme value. * * Returns: %TRUE for success * * Since: 1.9.17 **/ gboolean fu_coswid_read_version_scheme(cbor_item_t *item, FuCoswidVersionScheme *value, GError **error) { g_return_val_if_fail(item != NULL, FALSE); g_return_val_if_fail(value != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!cbor_isa_uint(item)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "version-scheme item is not a uint"); return FALSE; } *value = (FuCoswidVersionScheme)cbor_get_int(item); return TRUE; } /** * fu_coswid_read_u8: * @item: a #cbor_item_t * @value: read value * @error: (nullable): optional return location for an error * * Reads a #guint8 tag value. * * Returns: %TRUE for success * * Since: 1.9.17 **/ gboolean fu_coswid_read_u8(cbor_item_t *item, guint8 *value, GError **error) { guint64 tmp; g_return_val_if_fail(item != NULL, FALSE); g_return_val_if_fail(value != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!cbor_isa_uint(item)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "value item is not a uint"); return FALSE; } tmp = cbor_get_int(item); if (tmp > G_MAXUINT8) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "0x%x is too large for u8", (guint)tmp); return FALSE; } *value = (guint8)tmp; return TRUE; } /** * fu_coswid_read_s8: * @item: a #cbor_item_t * @value: read value * @error: (nullable): optional return location for an error * * Reads a #gint8 value. * * Returns: %TRUE for success * * Since: 1.9.17 **/ gboolean fu_coswid_read_s8(cbor_item_t *item, gint8 *value, GError **error) { guint64 tmp; g_return_val_if_fail(item != NULL, FALSE); g_return_val_if_fail(value != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!cbor_is_int(item)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "value item is not a int"); return FALSE; } tmp = cbor_get_int(item); if (tmp > 127) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "0x%x is too large for s8", (guint)tmp); return FALSE; } *value = cbor_isa_negint(item) ? (gint8)((-1) - tmp) : (gint8)tmp; return TRUE; } /** * fu_coswid_read_u64: * @item: a #cbor_item_t * @value: read value * @error: (nullable): optional return location for an error * * Reads a #guint64 tag value. * * Returns: %TRUE for success * * Since: 1.9.17 **/ gboolean fu_coswid_read_u64(cbor_item_t *item, guint64 *value, GError **error) { g_return_val_if_fail(item != NULL, FALSE); g_return_val_if_fail(value != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!cbor_isa_uint(item)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "value item is not a uint"); return FALSE; } *value = cbor_get_int(item); return TRUE; } /** * fu_coswid_write_tag_string: * @item: a #cbor_item_t * @tag: a #FuCoswidTag, e.g. %FU_COSWID_TAG_PAYLOAD * @value: string * * Writes a string tag value. * * Returns: %TRUE for success * * Since: 1.9.17 **/ void fu_coswid_write_tag_string(cbor_item_t *item, FuCoswidTag tag, const gchar *value) { g_autoptr(cbor_item_t) key = cbor_build_uint8(tag); g_autoptr(cbor_item_t) val = cbor_build_string(value); if (!cbor_map_add(item, (struct cbor_pair){.key = key, .value = val})) g_critical("failed to push string to indefinite map"); } /** * fu_coswid_write_tag_bytestring: * @item: a #cbor_item_t * @tag: a #FuCoswidTag, e.g. %FU_COSWID_TAG_PAYLOAD * @buf: a buffer of data * @bufsz: sizeof @buf * * Writes a bytestring tag value. * * Returns: %TRUE for success * * Since: 1.9.17 **/ void fu_coswid_write_tag_bytestring(cbor_item_t *item, FuCoswidTag tag, const guint8 *buf, gsize bufsz) { g_autoptr(cbor_item_t) key = cbor_build_uint8(tag); g_autoptr(cbor_item_t) val = cbor_build_bytestring((cbor_data)buf, bufsz); if (!cbor_map_add(item, (struct cbor_pair){.key = key, .value = val})) g_critical("failed to push bytestring to indefinite map"); } /** * fu_coswid_write_tag_bool: * @item: a #cbor_item_t * @tag: a #FuCoswidTag, e.g. %FU_COSWID_TAG_PAYLOAD * @value: boolean * * Writes a #gboolean tag value. * * Returns: %TRUE for success * * Since: 1.9.17 **/ void fu_coswid_write_tag_bool(cbor_item_t *item, FuCoswidTag tag, gboolean value) { g_autoptr(cbor_item_t) key = cbor_build_uint8(tag); g_autoptr(cbor_item_t) val = cbor_build_bool(value); if (!cbor_map_add(item, (struct cbor_pair){.key = key, .value = val})) g_critical("failed to push bool to indefinite map"); } /** * fu_coswid_write_tag_u16: * @item: a #cbor_item_t * @tag: a #FuCoswidTag, e.g. %FU_COSWID_TAG_PAYLOAD * @value: unsigned integer * * Writes a #guint16 tag value. * * Returns: %TRUE for success * * Since: 1.9.17 **/ void fu_coswid_write_tag_u16(cbor_item_t *item, FuCoswidTag tag, guint16 value) { g_autoptr(cbor_item_t) key = cbor_build_uint8(tag); g_autoptr(cbor_item_t) val = cbor_build_uint16(value); if (!cbor_map_add(item, (struct cbor_pair){.key = key, .value = val})) g_critical("failed to push u16 to indefinite map"); } /** * fu_coswid_write_tag_u64: * @item: a #cbor_item_t * @tag: a #FuCoswidTag, e.g. %FU_COSWID_TAG_PAYLOAD * @value: unsigned integer * * Writes a #guint64 tag value. * * Returns: %TRUE for success * * Since: 1.9.17 **/ void fu_coswid_write_tag_u64(cbor_item_t *item, FuCoswidTag tag, guint64 value) { g_autoptr(cbor_item_t) key = cbor_build_uint8(tag); g_autoptr(cbor_item_t) val = cbor_build_uint64(value); if (!cbor_map_add(item, (struct cbor_pair){.key = key, .value = val})) g_critical("failed to push u64 to indefinite map"); } /** * fu_coswid_write_tag_s8: * @item: a #cbor_item_t * @tag: a #FuCoswidTag, e.g. %FU_COSWID_TAG_PAYLOAD * @value: signed integer * * Writes a #gint8 tag value. * * Returns: %TRUE for success * * Since: 1.9.17 **/ void fu_coswid_write_tag_s8(cbor_item_t *item, FuCoswidTag tag, gint8 value) { g_autoptr(cbor_item_t) key = cbor_build_uint8(tag); g_autoptr(cbor_item_t) val = cbor_new_int8(); if (value >= 0) { cbor_set_uint8(val, value); } else { cbor_set_uint8(val, 0xFF - value); cbor_mark_negint(val); } if (!cbor_map_add(item, (struct cbor_pair){.key = key, .value = val})) g_critical("failed to push s8 to indefinite map"); } /** * fu_coswid_write_tag_item: * @item: a #cbor_item_t * @tag: a #FuCoswidTag, e.g. %FU_COSWID_TAG_PAYLOAD * @value: a #cbor_item_t * * Writes a raw tag value. * * Returns: %TRUE for success * * Since: 1.9.17 **/ void fu_coswid_write_tag_item(cbor_item_t *item, FuCoswidTag tag, cbor_item_t *value) { g_autoptr(cbor_item_t) key = cbor_build_uint8(tag); if (!cbor_map_add(item, (struct cbor_pair){.key = key, .value = value})) g_critical("failed to push item to indefinite map"); } /** * fu_coswid_parse_one_or_many: * @item: a #cbor_item_t * @func: a function to call with each map value * @user_data: pointer value to pass to @func * @error: (nullable): optional return location for an error * * Parses an item that *may* be an array, calling @func on each map value. * * Returns: %TRUE for success * * Since: 1.9.17 **/ gboolean fu_coswid_parse_one_or_many(cbor_item_t *item, FuCoswidItemFunc func, gpointer user_data, GError **error) { g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* one */ if (cbor_isa_map(item)) return func(item, user_data, error); /* many */ if (cbor_isa_array(item)) { for (guint j = 0; j < cbor_array_size(item); j++) { g_autoptr(cbor_item_t) value = cbor_array_get(item, j); if (!cbor_isa_map(value)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not an array of a map"); return FALSE; } if (!func(value, user_data, error)) return FALSE; } return TRUE; } /* not sure what to do */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "neither an array or map"); return FALSE; } #endif fwupd-2.0.10/libfwupdplugin/fu-coswid-common.h000066400000000000000000000040161501337203100213050ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #ifdef HAVE_CBOR #include #include #include "fu-coswid-struct.h" G_DEFINE_AUTOPTR_CLEANUP_FUNC(cbor_item_t, cbor_intermediate_decref) gchar * fu_coswid_read_string(cbor_item_t *item, GError **error) G_GNUC_NON_NULL(1); GByteArray * fu_coswid_read_byte_array(cbor_item_t *item, GError **error) G_GNUC_NON_NULL(1); gboolean fu_coswid_read_tag(cbor_item_t *item, FuCoswidTag *value, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_coswid_read_version_scheme(cbor_item_t *item, FuCoswidVersionScheme *value, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_coswid_read_u8(cbor_item_t *item, guint8 *value, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_coswid_read_s8(cbor_item_t *item, gint8 *value, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_coswid_read_u64(cbor_item_t *item, guint64 *value, GError **error) G_GNUC_NON_NULL(1, 2); void fu_coswid_write_tag_string(cbor_item_t *item, FuCoswidTag tag, const gchar *value) G_GNUC_NON_NULL(1); void fu_coswid_write_tag_bytestring(cbor_item_t *item, FuCoswidTag tag, const guint8 *buf, gsize bufsz) G_GNUC_NON_NULL(1, 3); void fu_coswid_write_tag_bool(cbor_item_t *item, FuCoswidTag tag, gboolean value) G_GNUC_NON_NULL(1); void fu_coswid_write_tag_u16(cbor_item_t *item, FuCoswidTag tag, guint16 value) G_GNUC_NON_NULL(1); void fu_coswid_write_tag_u64(cbor_item_t *item, FuCoswidTag tag, guint64 value) G_GNUC_NON_NULL(1); void fu_coswid_write_tag_s8(cbor_item_t *item, FuCoswidTag tag, gint8 value) G_GNUC_NON_NULL(1); void fu_coswid_write_tag_item(cbor_item_t *item, FuCoswidTag tag, cbor_item_t *value) G_GNUC_NON_NULL(1, 3); typedef gboolean (*FuCoswidItemFunc)(cbor_item_t *item, gpointer user_data, GError **error) G_GNUC_WARN_UNUSED_RESULT; gboolean fu_coswid_parse_one_or_many(cbor_item_t *item, FuCoswidItemFunc func, gpointer user_data, GError **error) G_GNUC_NON_NULL(1, 2); #endif fwupd-2.0.10/libfwupdplugin/fu-coswid-firmware.c000066400000000000000000001105351501337203100216300ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #ifdef HAVE_CBOR #include #endif #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-coswid-common.h" #include "fu-coswid-firmware.h" #include "fu-coswid-struct.h" #include "fu-input-stream.h" /** * FuCoswidFirmware: * * A coSWID SWID section. * * See also: [class@FuCoswidFirmware] */ typedef struct { gchar *product; gchar *summary; gchar *colloquial_version; FuCoswidVersionScheme version_scheme; GPtrArray *links; /* of FuCoswidFirmwareLink */ GPtrArray *entities; /* of FuCoswidFirmwareEntity */ GPtrArray *payloads; /* of FuCoswidFirmwarePayload */ } FuCoswidFirmwarePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuCoswidFirmware, fu_coswid_firmware, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_coswid_firmware_get_instance_private(o)) #define FU_COSWID_FIRMWARE_MAX_ALLOCATION 0x32000 typedef struct { gchar *name; gchar *regid; guint8 roles; /* bitfield of FuCoswidEntityRole */ } FuCoswidFirmwareEntity; typedef struct { gchar *href; FuCoswidLinkRel rel; } FuCoswidFirmwareLink; typedef struct { GByteArray *value; FuCoswidHashAlg alg_id; } FuCoswidFirmwareHash; typedef struct { gchar *name; guint64 size; GPtrArray *hashes; /* of FuCoswidFirmwareHash */ } FuCoswidFirmwarePayload; static void fu_coswid_firmware_entity_free(FuCoswidFirmwareEntity *entity) { g_free(entity->name); g_free(entity->regid); g_free(entity); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuCoswidFirmwareEntity, fu_coswid_firmware_entity_free) static void fu_coswid_firmware_link_free(FuCoswidFirmwareLink *link) { g_free(link->href); g_free(link); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuCoswidFirmwareLink, fu_coswid_firmware_link_free) static void fu_coswid_firmware_hash_free(FuCoswidFirmwareHash *hash) { if (hash->value != NULL) g_byte_array_unref(hash->value); g_free(hash); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuCoswidFirmwareHash, fu_coswid_firmware_hash_free) static FuCoswidFirmwarePayload * fu_coswid_firmware_payload_new(void) { FuCoswidFirmwarePayload *payload = g_new0(FuCoswidFirmwarePayload, 1); payload->hashes = g_ptr_array_new_with_free_func((GDestroyNotify)fu_coswid_firmware_hash_free); return payload; } static void fu_coswid_firmware_payload_free(FuCoswidFirmwarePayload *payload) { g_ptr_array_unref(payload->hashes); g_free(payload->name); g_free(payload); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuCoswidFirmwarePayload, fu_coswid_firmware_payload_free) #ifdef HAVE_CBOR /* @userdata: a #FuCoswidFirmware */ static gboolean fu_coswid_firmware_parse_meta(cbor_item_t *item, gpointer user_data, GError **error) { FuCoswidFirmware *self = FU_COSWID_FIRMWARE(user_data); FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self); struct cbor_pair *pairs = cbor_map_handle(item); for (gsize i = 0; i < cbor_map_size(item); i++) { FuCoswidTag tag_id = 0; if (!fu_coswid_read_tag(pairs[i].key, &tag_id, error)) return FALSE; if (tag_id == FU_COSWID_TAG_SUMMARY) { g_free(priv->summary); priv->summary = fu_coswid_read_string(pairs[i].value, error); if (priv->summary == NULL) { g_prefix_error(error, "failed to parse summary: "); return FALSE; } } else if (tag_id == FU_COSWID_TAG_COLLOQUIAL_VERSION) { g_free(priv->colloquial_version); priv->colloquial_version = fu_coswid_read_string(pairs[i].value, error); if (priv->colloquial_version == NULL) { g_prefix_error(error, "failed to parse colloquial-version: "); return FALSE; } } else { g_debug("unhandled tag %s from %s", fu_coswid_tag_to_string(tag_id), fu_coswid_tag_to_string(FU_COSWID_TAG_SOFTWARE_META)); } } /* success */ return TRUE; } /* @userdata: a #FuCoswidFirmware */ static gboolean fu_coswid_firmware_parse_link(cbor_item_t *item, gpointer user_data, GError **error) { FuCoswidFirmware *self = FU_COSWID_FIRMWARE(user_data); FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self); struct cbor_pair *pairs = cbor_map_handle(item); g_autoptr(FuCoswidFirmwareLink) link = g_new0(FuCoswidFirmwareLink, 1); for (gsize i = 0; i < cbor_map_size(item); i++) { FuCoswidTag tag_id = 0; if (!fu_coswid_read_tag(pairs[i].key, &tag_id, error)) return FALSE; if (tag_id == FU_COSWID_TAG_HREF) { g_free(link->href); link->href = fu_coswid_read_string(pairs[i].value, error); if (link->href == NULL) { g_prefix_error(error, "failed to parse link href: "); return FALSE; } } else if (tag_id == FU_COSWID_TAG_REL) { gint8 tmp = 0; if (!fu_coswid_read_s8(pairs[i].value, &tmp, error)) { g_prefix_error(error, "failed to parse link rel: "); return FALSE; } link->rel = tmp; } else { g_debug("unhandled tag %s from %s", fu_coswid_tag_to_string(tag_id), fu_coswid_tag_to_string(FU_COSWID_TAG_LINK)); } } /* success */ g_ptr_array_add(priv->links, g_steal_pointer(&link)); return TRUE; } /* @userdata: a #FuCoswidFirmwarePayload */ static gboolean fu_coswid_firmware_parse_hash(cbor_item_t *item, gpointer user_data, GError **error) { FuCoswidFirmwarePayload *payload = (FuCoswidFirmwarePayload *)user_data; guint8 alg_id8 = 0; g_autoptr(FuCoswidFirmwareHash) hash = g_new0(FuCoswidFirmwareHash, 1); g_autoptr(cbor_item_t) hash_item_alg_id = NULL; g_autoptr(cbor_item_t) hash_item_value = NULL; /* sanity check */ if (!cbor_isa_array(item)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "hash item is not an array"); return FALSE; } if (cbor_array_size(item) != 2) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "hash array has invalid size"); return FALSE; } hash_item_alg_id = cbor_array_get(item, 0); hash_item_value = cbor_array_get(item, 1); if (hash_item_alg_id == NULL || hash_item_value == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid hash item"); return FALSE; } if (!fu_coswid_read_u8(hash_item_alg_id, &alg_id8, error)) { g_prefix_error(error, "failed to parse hash alg-id: "); return FALSE; } /* success */ hash->alg_id = alg_id8; hash->value = fu_coswid_read_byte_array(hash_item_value, error); if (hash->value == NULL) { g_prefix_error(error, "failed to parse hash value: "); return FALSE; } g_ptr_array_add(payload->hashes, g_steal_pointer(&hash)); return TRUE; } /* @userdata: a #FuCoswidFirmwarePayload */ static gboolean fu_coswid_firmware_parse_hash_array(cbor_item_t *item, gpointer user_data, GError **error) { for (guint j = 0; j < cbor_array_size(item); j++) { g_autoptr(cbor_item_t) value = cbor_array_get(item, j); if (!fu_coswid_firmware_parse_hash(value, user_data, error)) return FALSE; } return TRUE; } /* @userdata: a #FuCoswidFirmware */ static gboolean fu_coswid_firmware_parse_file(cbor_item_t *item, gpointer user_data, GError **error) { FuCoswidFirmware *self = FU_COSWID_FIRMWARE(user_data); FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self); struct cbor_pair *pairs = cbor_map_handle(item); g_autoptr(FuCoswidFirmwarePayload) payload = fu_coswid_firmware_payload_new(); for (gsize i = 0; i < cbor_map_size(item); i++) { FuCoswidTag tag_id = 0; if (!fu_coswid_read_tag(pairs[i].key, &tag_id, error)) return FALSE; if (tag_id == FU_COSWID_TAG_FS_NAME) { g_free(payload->name); payload->name = fu_coswid_read_string(pairs[i].value, error); if (payload->name == NULL) { g_prefix_error(error, "failed to parse payload name: "); return FALSE; } } else if (tag_id == FU_COSWID_TAG_SIZE) { if (!fu_coswid_read_u64(pairs[i].value, &payload->size, error)) return FALSE; } else if (tag_id == FU_COSWID_TAG_HASH) { if (cbor_isa_array(pairs[i].value) && cbor_array_size(pairs[i].value) >= 1) { g_autoptr(cbor_item_t) value = cbor_array_get(pairs[i].value, 0); /* we can't use fu_coswid_parse_one_or_many() here as * the hash is an array, not a map -- for some reason */ if (cbor_isa_array(value)) { if (!fu_coswid_firmware_parse_hash_array(pairs[i].value, payload, error)) return FALSE; } else { if (!fu_coswid_firmware_parse_hash(pairs[i].value, payload, error)) return FALSE; } } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "hashes neither an array or array of array"); return FALSE; } } else { g_debug("unhandled tag %s from %s", fu_coswid_tag_to_string(tag_id), fu_coswid_tag_to_string(FU_COSWID_TAG_FILE)); } } /* success */ g_ptr_array_add(priv->payloads, g_steal_pointer(&payload)); return TRUE; } /* @userdata: a #FuCoswidFirmware */ static gboolean fu_coswid_firmware_parse_path_elements(cbor_item_t *item, gpointer user_data, GError **error) { FuCoswidFirmware *self = FU_COSWID_FIRMWARE(user_data); struct cbor_pair *pairs = cbor_map_handle(item); for (gsize i = 0; i < cbor_map_size(item); i++) { FuCoswidTag tag_id = 0; if (!fu_coswid_read_tag(pairs[i].key, &tag_id, error)) return FALSE; if (tag_id == FU_COSWID_TAG_FILE) { if (!fu_coswid_parse_one_or_many(pairs[i].value, fu_coswid_firmware_parse_file, self, /* user_data */ error)) return FALSE; } else { g_debug("unhandled tag %s from %s", fu_coswid_tag_to_string(tag_id), fu_coswid_tag_to_string(FU_COSWID_TAG_PATH_ELEMENTS)); } } /* success */ return TRUE; } /* @userdata: a #FuCoswidFirmware */ static gboolean fu_coswid_firmware_parse_directory(cbor_item_t *item, gpointer user_data, GError **error) { FuCoswidFirmware *self = FU_COSWID_FIRMWARE(user_data); struct cbor_pair *pairs = cbor_map_handle(item); for (gsize i = 0; i < cbor_map_size(item); i++) { FuCoswidTag tag_id = 0; if (!fu_coswid_read_tag(pairs[i].key, &tag_id, error)) return FALSE; if (tag_id == FU_COSWID_TAG_PATH_ELEMENTS) { if (!fu_coswid_parse_one_or_many(pairs[i].value, fu_coswid_firmware_parse_path_elements, self, /* user_data */ error)) return FALSE; } else { g_debug("unhandled tag %s from %s", fu_coswid_tag_to_string(tag_id), fu_coswid_tag_to_string(FU_COSWID_TAG_DIRECTORY)); } } /* success */ return TRUE; } /* @userdata: a #FuCoswidFirmware */ static gboolean fu_coswid_firmware_parse_payload(cbor_item_t *item, gpointer user_data, GError **error) { FuCoswidFirmware *self = FU_COSWID_FIRMWARE(user_data); struct cbor_pair *pairs = cbor_map_handle(item); for (gsize i = 0; i < cbor_map_size(item); i++) { FuCoswidTag tag_id = 0; if (!fu_coswid_read_tag(pairs[i].key, &tag_id, error)) return FALSE; if (tag_id == FU_COSWID_TAG_FILE) { if (!fu_coswid_parse_one_or_many(pairs[i].value, fu_coswid_firmware_parse_file, self, /* user_data */ error)) return FALSE; } else if (tag_id == FU_COSWID_TAG_DIRECTORY) { if (!fu_coswid_parse_one_or_many(pairs[i].value, fu_coswid_firmware_parse_directory, self, /* user_data */ error)) return FALSE; } else { g_debug("unhandled tag %s from %s", fu_coswid_tag_to_string(tag_id), fu_coswid_tag_to_string(FU_COSWID_TAG_PAYLOAD)); } } /* success */ return TRUE; } static gboolean fu_coswid_firmware_parse_entity_name(FuCoswidFirmwareEntity *entity, cbor_item_t *item, GError **error) { /* we might be calling this twice... */ g_free(entity->name); entity->name = fu_coswid_read_string(item, error); if (entity->name == NULL) { g_prefix_error(error, "failed to parse entity name: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_coswid_firmware_parse_entity_regid(FuCoswidFirmwareEntity *entity, cbor_item_t *item, GError **error) { /* we might be calling this twice... */ g_free(entity->regid); entity->regid = fu_coswid_read_string(item, error); if (entity->regid == NULL) { g_prefix_error(error, "failed to parse entity regid: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_coswid_firmware_parse_entity_role(FuCoswidFirmwareEntity *entity, cbor_item_t *item, GError **error) { if (cbor_isa_uint(item)) { guint8 role8 = 0; if (!fu_coswid_read_u8(item, &role8, error)) { g_prefix_error(error, "failed to parse entity role: "); return FALSE; } if (role8 >= FU_COSWID_ENTITY_ROLE_LAST) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid entity role 0x%x", role8); return FALSE; } FU_BIT_SET(entity->roles, role8); } else if (cbor_isa_array(item)) { for (guint j = 0; j < cbor_array_size(item); j++) { guint8 role8 = 0; g_autoptr(cbor_item_t) value = cbor_array_get(item, j); if (!fu_coswid_read_u8(value, &role8, error)) { g_prefix_error(error, "failed to parse entity role: "); return FALSE; } if (role8 >= FU_COSWID_ENTITY_ROLE_LAST) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid entity role 0x%x", role8); return FALSE; } FU_BIT_SET(entity->roles, role8); } } else { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "entity role item is not an uint or array"); return FALSE; } /* success */ return TRUE; } /* @userdata: a #FuCoswidFirmware */ static gboolean fu_coswid_firmware_parse_entity(cbor_item_t *item, gpointer user_data, GError **error) { FuCoswidFirmware *self = FU_COSWID_FIRMWARE(user_data); FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self); struct cbor_pair *pairs = cbor_map_handle(item); g_autoptr(FuCoswidFirmwareEntity) entity = g_new0(FuCoswidFirmwareEntity, 1); for (gsize i = 0; i < cbor_map_size(item); i++) { FuCoswidTag tag_id = 0; if (!fu_coswid_read_tag(pairs[i].key, &tag_id, error)) return FALSE; if (tag_id == FU_COSWID_TAG_ENTITY_NAME) { if (!fu_coswid_firmware_parse_entity_name(entity, pairs[i].value, error)) return FALSE; } else if (tag_id == FU_COSWID_TAG_REG_ID) { if (!fu_coswid_firmware_parse_entity_regid(entity, pairs[i].value, error)) return FALSE; } else if (tag_id == FU_COSWID_TAG_ROLE) { if (!fu_coswid_firmware_parse_entity_role(entity, pairs[i].value, error)) return FALSE; } else { g_debug("unhandled tag %s from %s", fu_coswid_tag_to_string(tag_id), fu_coswid_tag_to_string(FU_COSWID_TAG_ENTITY)); } } /* sanity check */ if (entity->name == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "entity does not have a name"); return FALSE; } if (entity->roles == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "entity has no roles"); return FALSE; } /* success */ g_ptr_array_add(priv->entities, g_steal_pointer(&entity)); return TRUE; } #endif #ifdef HAVE_CBOR_SET_ALLOCS static void * fu_coswid_firmware_malloc(size_t size) { if (size > FU_COSWID_FIRMWARE_MAX_ALLOCATION) { g_debug("failing CBOR allocation of 0x%x bytes", (guint)size); return NULL; } /* libcbor expects a valid pointer for a zero sized allocation */ return g_malloc0(MAX(size, 1)); } static void * fu_coswid_firmware_realloc(void *ptr, size_t size) { if (size > FU_COSWID_FIRMWARE_MAX_ALLOCATION) { g_debug("failing CBOR reallocation of 0x%x bytes", (guint)size); return NULL; } /* libcbor expects a valid pointer for a zero sized allocation */ return g_realloc(ptr, MAX(size, 1)); } static void fu_coswid_firmware_free(void *ptr) { g_free(ptr); } #endif static gboolean fu_coswid_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { #ifdef HAVE_CBOR FuCoswidFirmware *self = FU_COSWID_FIRMWARE(firmware); FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self); struct cbor_load_result result = {0x0}; struct cbor_pair *pairs = NULL; g_autoptr(cbor_item_t) item = NULL; g_autoptr(GBytes) fw = NULL; fw = fu_input_stream_read_bytes(stream, 0x0, G_MAXSIZE, NULL, error); if (fw == NULL) return FALSE; item = cbor_load(g_bytes_get_data(fw, NULL), g_bytes_get_size(fw), &result); if (item == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to parse CBOR at offset 0x%x: 0x%x", (guint)result.error.position, result.error.code); return FALSE; } fu_firmware_set_size(firmware, result.read); /* pretty-print the result */ if (g_getenv("FWUPD_CBOR_VERBOSE") != NULL) { cbor_describe(item, stdout); fflush(stdout); } /* sanity check */ if (!cbor_isa_map(item)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "root item is not a map"); return FALSE; } /* parse out anything interesting */ pairs = cbor_map_handle(item); for (gsize i = 0; i < cbor_map_size(item); i++) { FuCoswidTag tag_id = 0; if (!fu_coswid_read_tag(pairs[i].key, &tag_id, error)) return FALSE; /* identity can be specified as a string or in binary */ if (tag_id == FU_COSWID_TAG_TAG_ID) { g_autofree gchar *str = fu_coswid_read_string(pairs[i].value, error); if (str == NULL) { g_prefix_error(error, "failed to parse tag-id: "); return FALSE; } fu_firmware_set_id(firmware, str); } else if (tag_id == FU_COSWID_TAG_SOFTWARE_NAME) { g_free(priv->product); priv->product = fu_coswid_read_string(pairs[i].value, error); if (priv->product == NULL) { g_prefix_error(error, "failed to parse product: "); return FALSE; } } else if (tag_id == FU_COSWID_TAG_SOFTWARE_VERSION) { g_autofree gchar *str = fu_coswid_read_string(pairs[i].value, error); if (str == NULL) { g_prefix_error(error, "failed to parse software-version: "); return FALSE; } fu_firmware_set_version(firmware, str); } else if (tag_id == FU_COSWID_TAG_VERSION_SCHEME) { if (!fu_coswid_read_version_scheme(pairs[i].value, &priv->version_scheme, error)) return FALSE; } else if (tag_id == FU_COSWID_TAG_SOFTWARE_META) { if (!fu_coswid_parse_one_or_many(pairs[i].value, fu_coswid_firmware_parse_meta, self, /* user_data */ error)) return FALSE; } else if (tag_id == FU_COSWID_TAG_LINK) { if (!fu_coswid_parse_one_or_many(pairs[i].value, fu_coswid_firmware_parse_link, self, /* user_data */ error)) return FALSE; } else if (tag_id == FU_COSWID_TAG_PAYLOAD) { if (!fu_coswid_parse_one_or_many(pairs[i].value, fu_coswid_firmware_parse_payload, self, /* user_data */ error)) return FALSE; } else if (tag_id == FU_COSWID_TAG_ENTITY) { if (!fu_coswid_parse_one_or_many(pairs[i].value, fu_coswid_firmware_parse_entity, self, /* user_data */ error)) return FALSE; } else { g_debug("unhandled tag %s from root", fu_coswid_tag_to_string(tag_id)); } } /* device not supported */ if (fu_firmware_get_id(firmware) == NULL && fu_firmware_get_version(firmware) == NULL && priv->product == NULL && priv->entities->len == 0 && priv->links->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not enough SBOM data"); return FALSE; } /* success */ return TRUE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not compiled with CBOR support"); return FALSE; #endif } static gchar * fu_coswid_firmware_get_checksum(FuFirmware *firmware, GChecksumType csum_kind, GError **error) { FuCoswidFirmware *self = FU_COSWID_FIRMWARE(firmware); FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self); FuCoswidHashAlg alg_id = FU_COSWID_HASH_ALG_UNKNOWN; struct { GChecksumType kind; FuCoswidHashAlg alg_id; } csum_kinds[] = {{G_CHECKSUM_SHA256, FU_COSWID_HASH_ALG_SHA256}, {G_CHECKSUM_SHA384, FU_COSWID_HASH_ALG_SHA384}, {G_CHECKSUM_SHA512, FU_COSWID_HASH_ALG_SHA512}, {0, FU_COSWID_HASH_ALG_UNKNOWN}}; /* convert to FuCoswidHashAlg */ for (guint i = 0; csum_kinds[i].kind != 0; i++) { if (csum_kinds[i].kind == csum_kind) { alg_id = csum_kinds[i].alg_id; break; } } if (alg_id == FU_COSWID_HASH_ALG_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot convert %s", fwupd_checksum_type_to_string_display(csum_kind)); return NULL; } /* find the correct hash kind */ for (guint i = 0; i < priv->payloads->len; i++) { FuCoswidFirmwarePayload *payload = g_ptr_array_index(priv->payloads, i); for (guint j = 0; j < payload->hashes->len; j++) { FuCoswidFirmwareHash *hash = g_ptr_array_index(payload->hashes, j); if (hash->alg_id == alg_id) return fu_byte_array_to_string(hash->value); } } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no hash kind %s", fwupd_checksum_type_to_string_display(csum_kind)); return NULL; } #ifdef HAVE_CBOR static void fu_coswid_firmware_write_hash(cbor_item_t *root, FuCoswidFirmwareHash *hash) { g_autoptr(cbor_item_t) item_hash = cbor_new_definite_array(2); g_autoptr(cbor_item_t) item_hash_alg_id = cbor_build_uint8(hash->alg_id); g_autoptr(cbor_item_t) item_hash_value = cbor_build_bytestring(hash->value->data, hash->value->len); if (!cbor_array_push(item_hash, item_hash_alg_id)) g_critical("failed to push to definite array"); if (!cbor_array_push(item_hash, item_hash_value)) g_critical("failed to push to definite array"); if (!cbor_array_push(root, item_hash)) g_critical("failed to push to indefinite array"); } static void fu_coswid_firmware_write_payload(cbor_item_t *root, FuCoswidFirmwarePayload *payload) { g_autoptr(cbor_item_t) item_payload = cbor_new_indefinite_map(); g_autoptr(cbor_item_t) item_file = cbor_new_indefinite_map(); if (payload->name != NULL) { fu_coswid_write_tag_string(item_file, FU_COSWID_TAG_FS_NAME, payload->name); } if (payload->size != 0) { fu_coswid_write_tag_u64(item_file, FU_COSWID_TAG_SIZE, payload->size); } if (payload->hashes->len > 0) { g_autoptr(cbor_item_t) item_hashes = cbor_new_indefinite_array(); for (guint j = 0; j < payload->hashes->len; j++) { FuCoswidFirmwareHash *hash = g_ptr_array_index(payload->hashes, j); fu_coswid_firmware_write_hash(item_hashes, hash); } fu_coswid_write_tag_item(item_file, FU_COSWID_TAG_HASH, item_hashes); } fu_coswid_write_tag_item(item_payload, FU_COSWID_TAG_FILE, item_file); if (!cbor_array_push(root, item_payload)) g_critical("failed to push to indefinite array"); } #endif static GByteArray * fu_coswid_firmware_write(FuFirmware *firmware, GError **error) { #ifdef HAVE_CBOR FuCoswidFirmware *self = FU_COSWID_FIRMWARE(firmware); FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self); gsize buflen; gsize bufsz = 0; g_autofree guchar *buf = NULL; g_autoptr(cbor_item_t) root = cbor_new_indefinite_map(); g_autoptr(cbor_item_t) item_meta = cbor_new_indefinite_map(); /* preallocate the map structure */ fu_coswid_write_tag_string(root, FU_COSWID_TAG_LANG, "en-US"); if (fu_firmware_get_id(firmware) != NULL) { fwupd_guid_t uuid = {0}; if (fwupd_guid_from_string(fu_firmware_get_id(firmware), &uuid, FWUPD_GUID_FLAG_NONE, NULL)) { fu_coswid_write_tag_bytestring(root, FU_COSWID_TAG_TAG_ID, (const guint8 *)&uuid, sizeof(uuid)); } else { fu_coswid_write_tag_string(root, FU_COSWID_TAG_TAG_ID, fu_firmware_get_id(firmware)); } } fu_coswid_write_tag_bool(root, FU_COSWID_TAG_CORPUS, TRUE); if (priv->product != NULL) fu_coswid_write_tag_string(root, FU_COSWID_TAG_SOFTWARE_NAME, priv->product); if (fu_firmware_get_version(firmware) != NULL) { fu_coswid_write_tag_string(root, FU_COSWID_TAG_SOFTWARE_VERSION, fu_firmware_get_version(firmware)); } if (priv->version_scheme != FU_COSWID_VERSION_SCHEME_UNKNOWN) fu_coswid_write_tag_u16(root, FU_COSWID_TAG_VERSION_SCHEME, priv->version_scheme); fu_coswid_write_tag_item(root, FU_COSWID_TAG_SOFTWARE_META, item_meta); fu_coswid_write_tag_string(item_meta, FU_COSWID_TAG_GENERATOR, PACKAGE_NAME); if (priv->summary != NULL) fu_coswid_write_tag_string(item_meta, FU_COSWID_TAG_SUMMARY, priv->summary); if (priv->colloquial_version != NULL) { fu_coswid_write_tag_string(item_meta, FU_COSWID_TAG_COLLOQUIAL_VERSION, priv->colloquial_version); } /* add entities */ if (priv->entities->len > 0) { g_autoptr(cbor_item_t) item_entities = cbor_new_indefinite_array(); for (guint i = 0; i < priv->entities->len; i++) { FuCoswidFirmwareEntity *entity = g_ptr_array_index(priv->entities, i); g_autoptr(cbor_item_t) item_entity = cbor_new_indefinite_map(); g_autoptr(cbor_item_t) item_roles = cbor_new_indefinite_array(); if (entity->name != NULL) { fu_coswid_write_tag_string(item_entity, FU_COSWID_TAG_ENTITY_NAME, entity->name); } if (entity->regid != NULL) { fu_coswid_write_tag_string(item_entity, FU_COSWID_TAG_REG_ID, entity->regid); } for (guint j = 0; j < FU_COSWID_ENTITY_ROLE_LAST; j++) { if (FU_BIT_IS_SET(entity->roles, j)) { g_autoptr(cbor_item_t) item_role = cbor_build_uint8(j); if (!cbor_array_push(item_roles, item_role)) g_critical("failed to push to indefinite array"); } } fu_coswid_write_tag_item(item_entity, FU_COSWID_TAG_ROLE, item_roles); if (!cbor_array_push(item_entities, item_entity)) g_critical("failed to push to indefinite array"); } fu_coswid_write_tag_item(root, FU_COSWID_TAG_ENTITY, item_entities); } /* add links */ if (priv->links->len > 0) { g_autoptr(cbor_item_t) item_links = cbor_new_indefinite_array(); for (guint i = 0; i < priv->links->len; i++) { FuCoswidFirmwareLink *link = g_ptr_array_index(priv->links, i); g_autoptr(cbor_item_t) item_link = cbor_new_indefinite_map(); if (link->href != NULL) { fu_coswid_write_tag_string(item_link, FU_COSWID_TAG_HREF, link->href); } fu_coswid_write_tag_s8(item_link, FU_COSWID_TAG_REL, link->rel); if (!cbor_array_push(item_links, item_link)) g_critical("failed to push to indefinite array"); } fu_coswid_write_tag_item(root, FU_COSWID_TAG_LINK, item_links); } /* add payloads */ if (priv->payloads->len > 0) { g_autoptr(cbor_item_t) item_payloads = cbor_new_indefinite_array(); for (guint i = 0; i < priv->payloads->len; i++) { FuCoswidFirmwarePayload *payload = g_ptr_array_index(priv->payloads, i); fu_coswid_firmware_write_payload(item_payloads, payload); } fu_coswid_write_tag_item(root, FU_COSWID_TAG_PAYLOAD, item_payloads); } /* serialize */ buflen = cbor_serialize_alloc(root, &buf, &bufsz); if (buflen > bufsz) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "CBOR allocation failure"); return NULL; } return g_byte_array_new_take(g_steal_pointer(&buf), buflen); #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not compiled with CBOR support"); return NULL; #endif } static gboolean fu_coswid_firmware_build_entity(FuCoswidFirmware *self, XbNode *n, GError **error) { FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self); const gchar *tmp; FuCoswidEntityRole role; g_autoptr(GPtrArray) roles = NULL; g_autoptr(FuCoswidFirmwareEntity) entity = g_new0(FuCoswidFirmwareEntity, 1); /* these are required */ tmp = xb_node_query_text(n, "name", error); if (tmp == NULL) { fwupd_error_convert(error); return FALSE; } entity->name = g_strdup(tmp); tmp = xb_node_query_text(n, "regid", error); if (tmp == NULL) { fwupd_error_convert(error); return FALSE; } entity->regid = g_strdup(tmp); /* optional */ roles = xb_node_query(n, "role", 0, NULL); if (roles != NULL) { for (guint i = 0; i < roles->len; i++) { XbNode *c = g_ptr_array_index(roles, i); tmp = xb_node_get_text(c); role = fu_coswid_entity_role_from_string(tmp); if (role == FU_COSWID_ENTITY_ROLE_UNKNOWN || role >= FU_COSWID_ENTITY_ROLE_LAST) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to parse entity role %s", tmp); return FALSE; } FU_BIT_SET(entity->roles, role); } } /* success */ g_ptr_array_add(priv->entities, g_steal_pointer(&entity)); return TRUE; } static gboolean fu_coswid_firmware_build_link(FuCoswidFirmware *self, XbNode *n, GError **error) { FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self); const gchar *tmp; g_autoptr(FuCoswidFirmwareLink) link = g_new0(FuCoswidFirmwareLink, 1); /* required */ tmp = xb_node_query_text(n, "href", error); if (tmp == NULL) { fwupd_error_convert(error); return FALSE; } link->href = g_strdup(tmp); /* optional */ tmp = xb_node_query_text(n, "rel", NULL); if (tmp != NULL) { link->rel = fu_coswid_link_rel_from_string(tmp); if (link->rel == FU_COSWID_LINK_REL_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to parse link rel %s", tmp); return FALSE; } } /* success */ g_ptr_array_add(priv->links, g_steal_pointer(&link)); return TRUE; } static gboolean fu_coswid_firmware_build_hash(FuCoswidFirmware *self, XbNode *n, FuCoswidFirmwarePayload *payload, GError **error) { const gchar *tmp; g_autoptr(FuCoswidFirmwareHash) hash = g_new0(FuCoswidFirmwareHash, 1); /* required */ tmp = xb_node_query_text(n, "value", error); if (tmp == NULL) { fwupd_error_convert(error); return FALSE; } hash->value = fu_byte_array_from_string(tmp, error); if (hash->value == NULL) return FALSE; /* optional */ tmp = xb_node_query_text(n, "alg_id", NULL); if (tmp != NULL) { hash->alg_id = fu_coswid_hash_alg_from_string(tmp); if (hash->alg_id == FU_COSWID_HASH_ALG_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to parse alg_id %s", tmp); return FALSE; } } /* success */ g_ptr_array_add(payload->hashes, g_steal_pointer(&hash)); return TRUE; } static gboolean fu_coswid_firmware_build_payload(FuCoswidFirmware *self, XbNode *n, GError **error) { FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self); const gchar *tmp; guint64 tmp64; g_autoptr(FuCoswidFirmwarePayload) payload = fu_coswid_firmware_payload_new(); g_autoptr(GPtrArray) hashes = NULL; /* required */ tmp = xb_node_query_text(n, "name", NULL); if (tmp != NULL) payload->name = g_strdup(tmp); tmp64 = xb_node_query_text_as_uint(n, "size", NULL); if (tmp64 != G_MAXUINT64) payload->size = tmp64; /* multiple hashes allowed */ hashes = xb_node_query(n, "hash", 0, NULL); if (hashes != NULL) { for (guint i = 0; i < hashes->len; i++) { XbNode *c = g_ptr_array_index(hashes, i); if (!fu_coswid_firmware_build_hash(self, c, payload, error)) return FALSE; } } /* success */ g_ptr_array_add(priv->payloads, g_steal_pointer(&payload)); return TRUE; } static gboolean fu_coswid_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { FuCoswidFirmware *self = FU_COSWID_FIRMWARE(firmware); FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self); const gchar *tmp; g_autoptr(GPtrArray) links = NULL; g_autoptr(GPtrArray) payloads = NULL; g_autoptr(GPtrArray) entities = NULL; /* simple properties */ tmp = xb_node_query_text(n, "product", NULL); if (tmp != NULL) priv->product = g_strdup(tmp); tmp = xb_node_query_text(n, "summary", NULL); if (tmp != NULL) priv->summary = g_strdup(tmp); tmp = xb_node_query_text(n, "colloquial_version", NULL); if (tmp != NULL) priv->colloquial_version = g_strdup(tmp); tmp = xb_node_query_text(n, "version_scheme", NULL); if (tmp != NULL) { priv->version_scheme = fu_coswid_version_scheme_from_string(tmp); if (priv->version_scheme == FU_COSWID_VERSION_SCHEME_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to parse version_scheme %s", tmp); return FALSE; } } /* multiple links allowed */ links = xb_node_query(n, "link", 0, NULL); if (links != NULL) { for (guint i = 0; i < links->len; i++) { XbNode *c = g_ptr_array_index(links, i); if (!fu_coswid_firmware_build_link(self, c, error)) return FALSE; } } /* multiple payloads allowed */ payloads = xb_node_query(n, "payload", 0, NULL); if (payloads != NULL) { for (guint i = 0; i < payloads->len; i++) { XbNode *c = g_ptr_array_index(payloads, i); if (!fu_coswid_firmware_build_payload(self, c, error)) return FALSE; } } /* multiple entities allowed */ entities = xb_node_query(n, "entity", 0, NULL); if (entities != NULL) { for (guint i = 0; i < entities->len; i++) { XbNode *c = g_ptr_array_index(entities, i); if (!fu_coswid_firmware_build_entity(self, c, error)) return FALSE; } } /* success */ return TRUE; } static void fu_coswid_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuCoswidFirmware *self = FU_COSWID_FIRMWARE(firmware); FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self); if (priv->version_scheme != FU_COSWID_VERSION_SCHEME_UNKNOWN) { fu_xmlb_builder_insert_kv(bn, "version_scheme", fu_coswid_version_scheme_to_string(priv->version_scheme)); } fu_xmlb_builder_insert_kv(bn, "product", priv->product); fu_xmlb_builder_insert_kv(bn, "summary", priv->summary); fu_xmlb_builder_insert_kv(bn, "colloquial_version", priv->colloquial_version); for (guint i = 0; i < priv->links->len; i++) { FuCoswidFirmwareLink *link = g_ptr_array_index(priv->links, i); g_autoptr(XbBuilderNode) bc = xb_builder_node_insert(bn, "link", NULL); fu_xmlb_builder_insert_kv(bc, "href", link->href); if (link->rel != FU_COSWID_LINK_REL_UNKNOWN) { fu_xmlb_builder_insert_kv(bc, "rel", fu_coswid_link_rel_to_string(link->rel)); } } for (guint i = 0; i < priv->payloads->len; i++) { FuCoswidFirmwarePayload *payload = g_ptr_array_index(priv->payloads, i); g_autoptr(XbBuilderNode) bc = xb_builder_node_insert(bn, "payload", NULL); fu_xmlb_builder_insert_kv(bc, "name", payload->name); fu_xmlb_builder_insert_kx(bc, "size", payload->size); for (guint j = 0; j < payload->hashes->len; j++) { FuCoswidFirmwareHash *hash = g_ptr_array_index(payload->hashes, j); g_autoptr(XbBuilderNode) bh = xb_builder_node_insert(bc, "hash", NULL); g_autofree gchar *value = fu_byte_array_to_string(hash->value); fu_xmlb_builder_insert_kv(bh, "alg_id", fu_coswid_hash_alg_to_string(hash->alg_id)); fu_xmlb_builder_insert_kv(bh, "value", value); } } for (guint i = 0; i < priv->entities->len; i++) { FuCoswidFirmwareEntity *entity = g_ptr_array_index(priv->entities, i); g_autoptr(XbBuilderNode) bc = xb_builder_node_insert(bn, "entity", NULL); fu_xmlb_builder_insert_kv(bc, "name", entity->name); fu_xmlb_builder_insert_kv(bc, "regid", entity->regid); for (guint j = 0; j < FU_COSWID_ENTITY_ROLE_LAST; j++) { if (FU_BIT_IS_SET(entity->roles, j)) { fu_xmlb_builder_insert_kv(bc, "role", fu_coswid_entity_role_to_string(j)); } } } } static void fu_coswid_firmware_init(FuCoswidFirmware *self) { FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self); priv->version_scheme = FU_COSWID_VERSION_SCHEME_SEMVER; priv->links = g_ptr_array_new_with_free_func((GDestroyNotify)fu_coswid_firmware_link_free); priv->payloads = g_ptr_array_new_with_free_func((GDestroyNotify)fu_coswid_firmware_payload_free); priv->entities = g_ptr_array_new_with_free_func((GDestroyNotify)fu_coswid_firmware_entity_free); } static void fu_coswid_firmware_finalize(GObject *object) { FuCoswidFirmware *self = FU_COSWID_FIRMWARE(object); FuCoswidFirmwarePrivate *priv = GET_PRIVATE(self); g_free(priv->product); g_free(priv->summary); g_free(priv->colloquial_version); g_ptr_array_unref(priv->links); g_ptr_array_unref(priv->payloads); g_ptr_array_unref(priv->entities); G_OBJECT_CLASS(fu_coswid_firmware_parent_class)->finalize(object); } static void fu_coswid_firmware_class_init(FuCoswidFirmwareClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_coswid_firmware_finalize; firmware_class->parse = fu_coswid_firmware_parse; firmware_class->write = fu_coswid_firmware_write; firmware_class->build = fu_coswid_firmware_build; firmware_class->export = fu_coswid_firmware_export; firmware_class->get_checksum = fu_coswid_firmware_get_checksum; #ifdef HAVE_CBOR_SET_ALLOCS /* limit for protection, yes; this is global and there is no context */ cbor_set_allocs(fu_coswid_firmware_malloc, fu_coswid_firmware_realloc, fu_coswid_firmware_free); #endif } /** * fu_coswid_firmware_new: * * Creates a new #FuFirmware of sub type coSWID * * Since: 1.8.0 **/ FuFirmware * fu_coswid_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_COSWID_FIRMWARE, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-coswid-firmware.h000066400000000000000000000006471501337203100216370ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_COSWID_FIRMWARE (fu_coswid_firmware_get_type()) G_DECLARE_DERIVABLE_TYPE(FuCoswidFirmware, fu_coswid_firmware, FU, COSWID_FIRMWARE, FuFirmware) struct _FuCoswidFirmwareClass { FuFirmwareClass parent_class; }; FuFirmware * fu_coswid_firmware_new(void); fwupd-2.0.10/libfwupdplugin/fu-coswid.rs000066400000000000000000000034271501337203100202210ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString, FromString)] enum FuCoswidTag { TagId, SoftwareName, Entity, Evidence, Link, SoftwareMeta, Payload, Hash, Corpus, Patch, Media, Supplemental, TagVersion, SoftwareVersion, VersionScheme, Lang, Directory, File, Process, Resource, Size, FileVersion, Key, Location, FsName, Root, PathElements, ProcessName, Pid, Type, Missing30, // not in the spec! EntityName, RegId, Role, Thumbprint, Date, DeviceId, Artifact, Href, Ownership, Rel, MediaType, Use, ActivationStatus, ChannelType, ColloquialVersion, Description, Edition, EntitlementDataRequired, EntitlementKey, Generator, PersistentId, Product, ProductFamily, Revision, Summary, UnspscCode, UnspscVersion, } #[derive(ToString, FromString)] enum FuCoswidVersionScheme { Unknown, Multipartnumeric, MultipartnumericSuffix, Alphanumeric, Decimal, Semver = 16384, } #[derive(ToString, FromString)] enum FuCoswidLinkRel { License = -2, Compiler = -1, Unknown = 0, Ancestor = 1, Component = 2, Feature = 3, Installationmedia = 4, Packageinstaller = 5, Parent = 6, Patches = 7, Requires = 8, SeeAlso = 9, Supersedes = 10, Supplemental = 11, } #[derive(ToString, FromString)] enum FuCoswidEntityRole { Unknown, TagCreator, SoftwareCreator, Aggregator, Distributor, Licensor, Maintainer, } #[derive(ToString, FromString)] enum FuCoswidHashAlg { Unknown = 0, SHA256 = 1, SHA384 = 7, SHA512 = 8, } fwupd-2.0.10/libfwupdplugin/fu-crc-private.h000066400000000000000000000010471501337203100207470ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-crc.h" guint32 fu_crc32_step(FuCrcKind kind, const guint8 *buf, gsize bufsz, guint32 crc); guint32 fu_crc32_done(FuCrcKind kind, guint32 crc); guint16 fu_crc16_step(FuCrcKind kind, const guint8 *buf, gsize bufsz, guint16 crc); guint16 fu_crc16_done(FuCrcKind kind, guint16 crc); guint8 fu_crc8_step(FuCrcKind kind, const guint8 *buf, gsize bufsz, guint8 crc); guint8 fu_crc8_done(FuCrcKind kind, guint8 crc); fwupd-2.0.10/libfwupdplugin/fu-crc.c000066400000000000000000000277021501337203100173000ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuCommon" #include "config.h" #include "fu-common.h" #include "fu-crc-private.h" #include "fu-mem.h" const struct { FuCrcKind kind; guint bitwidth; guint32 poly; guint32 init; gboolean reflected; guint32 xorout; } crc_map[] = { {FU_CRC_KIND_UNKNOWN, 32, 0x00000000, 0x00000000, TRUE, 0xFFFFFFFF}, {FU_CRC_KIND_B32_STANDARD, 32, 0x04C11DB7, 0xFFFFFFFF, TRUE, 0xFFFFFFFF}, {FU_CRC_KIND_B32_BZIP2, 32, 0x04C11DB7, 0xFFFFFFFF, FALSE, 0xFFFFFFFF}, {FU_CRC_KIND_B32_JAMCRC, 32, 0x04C11DB7, 0xFFFFFFFF, TRUE, 0x00000000}, {FU_CRC_KIND_B32_MPEG2, 32, 0x04C11DB7, 0xFFFFFFFF, FALSE, 0x00000000}, {FU_CRC_KIND_B32_POSIX, 32, 0x04C11DB7, 0x00000000, FALSE, 0xFFFFFFFF}, {FU_CRC_KIND_B32_SATA, 32, 0x04C11DB7, 0x52325032, FALSE, 0x00000000}, {FU_CRC_KIND_B32_XFER, 32, 0x000000AF, 0x00000000, FALSE, 0x00000000}, {FU_CRC_KIND_B32_C, 32, 0x1EDC6F41, 0xFFFFFFFF, TRUE, 0xFFFFFFFF}, {FU_CRC_KIND_B32_D, 32, 0xA833982B, 0xFFFFFFFF, TRUE, 0xFFFFFFFF}, {FU_CRC_KIND_B32_Q, 32, 0x814141AB, 0x00000000, FALSE, 0x00000000}, {FU_CRC_KIND_B16_XMODEM, 16, 0x1021, 0x0000, FALSE, 0x0000}, {FU_CRC_KIND_B16_USB, 16, 0x8005, 0xFFFF, TRUE, 0xFFFF}, {FU_CRC_KIND_B16_UMTS, 16, 0x8005, 0x0000, FALSE, 0x0000}, {FU_CRC_KIND_B16_TMS37157, 16, 0x1021, 0x89ec, TRUE, 0x0000}, {FU_CRC_KIND_B16_BNR, 16, 0x8005, 0xFFFF, FALSE, 0x0000}, {FU_CRC_KIND_B8_WCDMA, 8, 0x9B, 0x00, TRUE, 0x00}, {FU_CRC_KIND_B8_TECH_3250, 8, 0x1D, 0xFF, TRUE, 0x00}, {FU_CRC_KIND_B8_STANDARD, 8, 0x07, 0x00, FALSE, 0x00}, {FU_CRC_KIND_B8_SAE_J1850, 8, 0x1D, 0xFF, FALSE, 0xFF}, {FU_CRC_KIND_B8_ROHC, 8, 0x07, 0xFF, TRUE, 0x00}, {FU_CRC_KIND_B8_OPENSAFETY, 8, 0x2F, 0x00, FALSE, 0x00}, {FU_CRC_KIND_B8_NRSC_5, 8, 0x31, 0xFF, FALSE, 0x00}, {FU_CRC_KIND_B8_MIFARE_MAD, 8, 0x1D, 0xC7, FALSE, 0x00}, {FU_CRC_KIND_B8_MAXIM_DOW, 8, 0x31, 0x00, TRUE, 0x00}, {FU_CRC_KIND_B8_LTE, 8, 0x9B, 0x00, FALSE, 0x00}, {FU_CRC_KIND_B8_I_CODE, 8, 0x1D, 0xFD, FALSE, 0x00}, {FU_CRC_KIND_B8_ITU, 8, 0x07, 0x00, FALSE, 0x55}, {FU_CRC_KIND_B8_HITAG, 8, 0x1D, 0xFF, FALSE, 0x00}, {FU_CRC_KIND_B8_GSM_B, 8, 0x49, 0x00, FALSE, 0xFF}, {FU_CRC_KIND_B8_GSM_A, 8, 0x1D, 0x00, FALSE, 0x00}, {FU_CRC_KIND_B8_DVB_S2, 8, 0xD5, 0x00, FALSE, 0x00}, {FU_CRC_KIND_B8_DARC, 8, 0x39, 0x00, TRUE, 0x00}, {FU_CRC_KIND_B8_CDMA2000, 8, 0x9B, 0xFF, FALSE, 0x00}, {FU_CRC_KIND_B8_BLUETOOTH, 8, 0xA7, 0x00, TRUE, 0x00}, {FU_CRC_KIND_B8_AUTOSAR, 8, 0x2F, 0xFF, FALSE, 0xFF}, }; static guint8 fu_crc_reflect8(guint8 data) { guint8 val = 0; for (guint8 bit = 0; bit < 8; bit++) { if (data & 0x01) FU_BIT_SET(val, 7 - bit); data = (data >> 1); } return val; } static guint32 fu_crc_reflect(guint32 data, guint bitwidth) { guint32 val = 0; for (guint bit = 0; bit < bitwidth; bit++) { if (data & 0x01) val |= 1ul << ((bitwidth - 1) - bit); data = (data >> 1); } return val; } /** * fu_crc8_step: * @kind: a #FuCrcKind, typically %FU_CRC_KIND_B8_MAXIM_DOW * @buf: memory buffer * @bufsz: size of @buf * @crc: initial CRC value * * Computes the cyclic redundancy check section value for the given memory buffer. * * NOTE: When all data has been added, you should call fu_crc8_done() to return the final value. * * Returns: CRC value * * Since: 2.0.0 **/ guint8 fu_crc8_step(FuCrcKind kind, const guint8 *buf, gsize bufsz, guint8 crc) { const guint bitwidth = sizeof(crc) * 8; g_return_val_if_fail(kind < FU_CRC_KIND_LAST, 0x0); g_return_val_if_fail(crc_map[kind].bitwidth == 8, 0x0); for (gsize i = 0; i < bufsz; ++i) { crc ^= crc_map[kind].reflected ? fu_crc_reflect8(buf[i]) : buf[i]; for (guint8 bit = 0; bit < 8; bit++) { if (crc & (1ul << (bitwidth - 1))) { crc = (crc << 1) ^ crc_map[kind].poly; } else { crc = (crc << 1); } } } return crc; } /** * fu_crc8_done: * @kind: a #FuCrcKind, typically %FU_CRC_KIND_B8_MAXIM_DOW * @crc: initial CRC value * * Returns the finished cyclic redundancy check value. * * Returns: CRC value * * Since: 2.0.0 **/ guint8 fu_crc8_done(FuCrcKind kind, guint8 crc) { g_return_val_if_fail(kind < FU_CRC_KIND_LAST, 0x0); g_return_val_if_fail(crc_map[kind].bitwidth == 8, 0x0); crc = crc_map[kind].reflected ? fu_crc_reflect(crc, crc_map[kind].bitwidth) : crc; return crc ^ crc_map[kind].xorout; } /** * fu_crc8: * @kind: a #FuCrcKind, typically %FU_CRC_KIND_B8_MAXIM_DOW * @buf: memory buffer * @bufsz: size of @buf * * Returns the cyclic redundancy check value for the given memory buffer. * * Returns: CRC value * * Since: 2.0.0 **/ guint8 fu_crc8(FuCrcKind kind, const guint8 *buf, gsize bufsz) { g_return_val_if_fail(kind < FU_CRC_KIND_LAST, 0x0); g_return_val_if_fail(crc_map[kind].bitwidth == 8, 0x0); return fu_crc8_done(kind, fu_crc8_step(kind, buf, bufsz, crc_map[kind].init)); } /** * fu_crc8_bytes: * @kind: a #FuCrcKind, typically %FU_CRC_KIND_B8_MAXIM_DOW * @blob: a #GBytes * * Returns the cyclic redundancy check value for the given memory buffer. * * Returns: CRC value * * Since: 2.0.2 **/ guint8 fu_crc8_bytes(FuCrcKind kind, GBytes *blob) { g_return_val_if_fail(kind < FU_CRC_KIND_LAST, 0x0); g_return_val_if_fail(blob != NULL, 0x0); return fu_crc8(kind, g_bytes_get_data(blob, NULL), g_bytes_get_size(blob)); } /** * fu_crc16_step: * @kind: a #FuCrcKind, typically %FU_CRC_KIND_B16_XMODEM * @buf: memory buffer * @bufsz: size of @buf * @crc: initial CRC value * * Computes the cyclic redundancy check section value for the given memory buffer. * * NOTE: When all data has been added, you should call fu_crc16_done() to return the final value. * * Returns: CRC value * * Since: 2.0.0 **/ guint16 fu_crc16_step(FuCrcKind kind, const guint8 *buf, gsize bufsz, guint16 crc) { const guint bitwidth = sizeof(crc) * 8; g_return_val_if_fail(kind < FU_CRC_KIND_LAST, 0x0); g_return_val_if_fail(crc_map[kind].bitwidth == 16, 0x0); for (gsize i = 0; i < bufsz; ++i) { guint16 tmp = crc_map[kind].reflected ? fu_crc_reflect8(buf[i]) : buf[i]; crc ^= tmp << (bitwidth - 8); for (guint8 bit = 0; bit < 8; bit++) { if (crc & (1ul << (bitwidth - 1))) { crc = (crc << 1) ^ crc_map[kind].poly; } else { crc = (crc << 1); } } } return crc; } /** * fu_crc16_done: * @kind: a #FuCrcKind, typically %FU_CRC_KIND_B16_XMODEM * @crc: initial CRC value * * Returns the finished cyclic redundancy check value. * * Returns: CRC value * * Since: 2.0.0 **/ guint16 fu_crc16_done(FuCrcKind kind, guint16 crc) { g_return_val_if_fail(kind < FU_CRC_KIND_LAST, 0x0); g_return_val_if_fail(crc_map[kind].bitwidth == 16, 0x0); crc = crc_map[kind].reflected ? fu_crc_reflect(crc, crc_map[kind].bitwidth) : crc; return crc ^ crc_map[kind].xorout; } /** * fu_crc16: * @kind: a #FuCrcKind, typically %FU_CRC_KIND_B16_XMODEM * @buf: memory buffer * @bufsz: size of @buf * * Returns the cyclic redundancy check value for the given memory buffer. * * Returns: CRC value * * Since: 2.0.0 **/ guint16 fu_crc16(FuCrcKind kind, const guint8 *buf, gsize bufsz) { g_return_val_if_fail(kind < FU_CRC_KIND_LAST, 0x0); g_return_val_if_fail(crc_map[kind].bitwidth == 16, 0x0); return fu_crc16_done(kind, fu_crc16_step(kind, buf, bufsz, crc_map[kind].init)); } /** * fu_crc16_bytes: * @kind: a #FuCrcKind, typically %FU_CRC_KIND_B16_XMODEM * @blob: a #GBytes * * Returns the cyclic redundancy check value for the given memory buffer. * * Returns: CRC value * * Since: 2.0.2 **/ guint16 fu_crc16_bytes(FuCrcKind kind, GBytes *blob) { g_return_val_if_fail(kind < FU_CRC_KIND_LAST, 0x0); g_return_val_if_fail(blob != NULL, 0x0); return fu_crc16(kind, g_bytes_get_data(blob, NULL), g_bytes_get_size(blob)); } /** * fu_crc32_step: * @kind: a #FuCrcKind, typically %FU_CRC_KIND_B32_STANDARD * @buf: memory buffer * @bufsz: size of @buf * @crc: initial CRC value * * Computes the cyclic redundancy check section value for the given memory buffer. * * NOTE: When all data has been added, you should call fu_crc32_done() to return the final value. * * Returns: CRC value * * Since: 2.0.0 **/ guint32 fu_crc32_step(FuCrcKind kind, const guint8 *buf, gsize bufsz, guint32 crc) { const guint bitwidth = sizeof(crc) * 8; g_return_val_if_fail(kind < FU_CRC_KIND_LAST, 0x0); g_return_val_if_fail(crc_map[kind].bitwidth == 32, 0x0); for (gsize i = 0; i < bufsz; ++i) { guint32 tmp = crc_map[kind].reflected ? fu_crc_reflect8(buf[i]) : buf[i]; crc ^= tmp << (bitwidth - 8); for (guint8 bit = 0; bit < 8; bit++) { if (crc & (1ul << (bitwidth - 1))) { crc = (crc << 1) ^ crc_map[kind].poly; } else { crc = (crc << 1); } } } return crc; } /** * fu_crc32_done: * @kind: a #FuCrcKind, typically %FU_CRC_KIND_B32_STANDARD * @crc: initial CRC value * * Returns the finished cyclic redundancy check value. * * Returns: CRC value * * Since: 2.0.0 **/ guint32 fu_crc32_done(FuCrcKind kind, guint32 crc) { g_return_val_if_fail(kind < FU_CRC_KIND_LAST, 0x0); g_return_val_if_fail(crc_map[kind].bitwidth == 32, 0x0); crc = crc_map[kind].reflected ? fu_crc_reflect(crc, crc_map[kind].bitwidth) : crc; return crc ^ crc_map[kind].xorout; } /** * fu_crc32: * @kind: a #FuCrcKind, typically %FU_CRC_KIND_B32_STANDARD * @buf: memory buffer * @bufsz: size of @buf * * Returns the cyclic redundancy check value for the given memory buffer. * * Returns: CRC value * * Since: 2.0.0 **/ guint32 fu_crc32(FuCrcKind kind, const guint8 *buf, gsize bufsz) { g_return_val_if_fail(kind < FU_CRC_KIND_LAST, 0x0); g_return_val_if_fail(crc_map[kind].bitwidth == 32, 0x0); return fu_crc32_done(kind, fu_crc32_step(kind, buf, bufsz, crc_map[kind].init)); } /** * fu_crc32_bytes: * @kind: a #FuCrcKind, typically %FU_CRC_KIND_B32_STANDARD * @blob: a #GBytes * * Returns the cyclic redundancy check value for the given memory buffer. * * Returns: CRC value * * Since: 2.0.2 **/ guint32 fu_crc32_bytes(FuCrcKind kind, GBytes *blob) { g_return_val_if_fail(kind < FU_CRC_KIND_LAST, 0x0); g_return_val_if_fail(blob != NULL, 0x0); return fu_crc32(kind, g_bytes_get_data(blob, NULL), g_bytes_get_size(blob)); } /** * fu_crc_find: * @buf: memory buffer * @bufsz: size of @buf * @crc_target: "correct" CRC value * * Returns the cyclic redundancy kind for the given memory buffer and target CRC. * * You can use a very simple buffer to discover most types of standard CRC-32: * * guint8 buf[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; * g_info("CRC:%u", fu_crc_find(buf, sizeof(buf), _custom_crc(buf, sizeof(buf)))); * * Returns: a #FuCrcKind, or %FU_CRC_KIND_UNKNOWN on error * * Since: 2.0.0 **/ FuCrcKind fu_crc_find(const guint8 *buf, gsize bufsz, guint32 crc_target) { for (guint i = 0; i < G_N_ELEMENTS(crc_map); i++) { if (crc_map[i].bitwidth == 32) { if (crc_target == fu_crc32(crc_map[i].kind, buf, bufsz)) return crc_map[i].kind; } if (crc_map[i].bitwidth == 16) { if ((guint16)crc_target == fu_crc16(crc_map[i].kind, buf, bufsz)) return crc_map[i].kind; } if (crc_map[i].bitwidth == 8) { if ((guint8)crc_target == fu_crc8(crc_map[i].kind, buf, bufsz)) return crc_map[i].kind; } } return FU_CRC_KIND_UNKNOWN; } static guint16 fu_crc_misr16_step(guint16 cur, guint16 new) { guint16 bit0; guint16 res; bit0 = cur ^ (new & 1); bit0 ^= cur >> 1; bit0 ^= cur >> 2; bit0 ^= cur >> 4; bit0 ^= cur >> 5; bit0 ^= cur >> 7; bit0 ^= cur >> 11; bit0 ^= cur >> 15; res = (cur << 1) ^ new; res = (res & ~1) | (bit0 & 1); return res; } /** * fu_crc_misr16: * @buf: memory buffer * @bufsz: size of @buf * @init: initial value, typically 0x0 * * Returns the MISR check value for the given memory buffer. * * Returns: value * * Since: 1.9.17 **/ guint16 fu_crc_misr16(guint16 init, const guint8 *buf, gsize bufsz) { g_return_val_if_fail(buf != NULL, G_MAXUINT16); g_return_val_if_fail(bufsz % 2 == 0, G_MAXUINT16); for (gsize i = 0; i < bufsz; i += 2) init = fu_crc_misr16_step(init, fu_memread_uint16(buf + i, G_LITTLE_ENDIAN)); return init; } fwupd-2.0.10/libfwupdplugin/fu-crc.h000066400000000000000000000031251501337203100172760ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include /** * FuCrcKind: * * The type of CRC. **/ typedef enum { FU_CRC_KIND_UNKNOWN, FU_CRC_KIND_B32_STANDARD, FU_CRC_KIND_B32_BZIP2, FU_CRC_KIND_B32_JAMCRC, FU_CRC_KIND_B32_MPEG2, FU_CRC_KIND_B32_POSIX, FU_CRC_KIND_B32_SATA, FU_CRC_KIND_B32_XFER, FU_CRC_KIND_B32_C, FU_CRC_KIND_B32_D, FU_CRC_KIND_B32_Q, FU_CRC_KIND_B16_XMODEM, FU_CRC_KIND_B16_USB, FU_CRC_KIND_B16_UMTS, FU_CRC_KIND_B16_TMS37157, FU_CRC_KIND_B16_BNR, FU_CRC_KIND_B8_WCDMA, FU_CRC_KIND_B8_TECH_3250, FU_CRC_KIND_B8_STANDARD, FU_CRC_KIND_B8_SAE_J1850, FU_CRC_KIND_B8_ROHC, FU_CRC_KIND_B8_OPENSAFETY, FU_CRC_KIND_B8_NRSC_5, FU_CRC_KIND_B8_MIFARE_MAD, FU_CRC_KIND_B8_MAXIM_DOW, FU_CRC_KIND_B8_LTE, FU_CRC_KIND_B8_I_CODE, FU_CRC_KIND_B8_ITU, FU_CRC_KIND_B8_HITAG, FU_CRC_KIND_B8_GSM_B, FU_CRC_KIND_B8_GSM_A, FU_CRC_KIND_B8_DVB_S2, FU_CRC_KIND_B8_DARC, FU_CRC_KIND_B8_CDMA2000, FU_CRC_KIND_B8_BLUETOOTH, FU_CRC_KIND_B8_AUTOSAR, /*< private >*/ FU_CRC_KIND_LAST, } FuCrcKind; guint32 fu_crc32(FuCrcKind kind, const guint8 *buf, gsize bufsz); guint32 fu_crc32_bytes(FuCrcKind kind, GBytes *blob); guint16 fu_crc16(FuCrcKind kind, const guint8 *buf, gsize bufsz); guint16 fu_crc16_bytes(FuCrcKind kind, GBytes *blob); guint8 fu_crc8(FuCrcKind kind, const guint8 *buf, gsize bufsz); guint8 fu_crc8_bytes(FuCrcKind kind, GBytes *blob); FuCrcKind fu_crc_find(const guint8 *buf, gsize bufsz, guint32 crc_target); guint16 fu_crc_misr16(guint16 init, const guint8 *buf, gsize bufsz); fwupd-2.0.10/libfwupdplugin/fu-csv-entry.c000066400000000000000000000163211501337203100204560ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-common.h" #include "fu-csv-entry.h" #include "fu-csv-firmware-private.h" #include "fu-string.h" /** * FuCsvEntry: * * A comma seporated value entry. * * See also: [class@FuFirmware] */ typedef struct { GPtrArray *values; /* element-type utf-8 */ } FuCsvEntryPrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuCsvEntry, fu_csv_entry, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_csv_entry_get_instance_private(o)) #define FU_CSV_ENTRY_COLUMNS_MAX 1000u /** * fu_csv_entry_add_value: * @self: a #FuFirmware * @value: (not nullable): string * * Adds a string value to the entry. * * Since: 1.9.3 **/ void fu_csv_entry_add_value(FuCsvEntry *self, const gchar *value) { FuCsvEntryPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CSV_ENTRY(self)); g_return_if_fail(value != NULL); g_ptr_array_add(priv->values, g_strdup(value)); } /** * fu_csv_entry_get_value_by_idx: * @self: a #FuFirmware * @idx: column ID idx * * Gets the entry value for a specific index. * * Returns: a string, or %NULL if unset * * Since: 1.9.3 **/ const gchar * fu_csv_entry_get_value_by_idx(FuCsvEntry *self, guint idx) { FuCsvEntryPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CSV_ENTRY(self), NULL); if (idx >= priv->values->len) return NULL; return g_ptr_array_index(priv->values, idx); } /** * fu_csv_entry_get_value_by_column_id: * @self: a #FuFirmware * @column_id: (not nullable): string, e.g. `component_generation` * * Gets the entry value for a specific column ID. * * Returns: a string, or %NULL if unset or the column ID cannot be found * * Since: 1.9.3 **/ const gchar * fu_csv_entry_get_value_by_column_id(FuCsvEntry *self, const gchar *column_id) { FuCsvEntryPrivate *priv = GET_PRIVATE(self); FuCsvFirmware *parent = FU_CSV_FIRMWARE(fu_firmware_get_parent(FU_FIRMWARE(self))); guint idx = fu_csv_firmware_get_idx_for_column_id(parent, column_id); g_return_val_if_fail(FU_IS_CSV_ENTRY(self), NULL); g_return_val_if_fail(FU_IS_CSV_FIRMWARE(parent), NULL); g_return_val_if_fail(idx != G_MAXUINT, NULL); g_return_val_if_fail(column_id != NULL, NULL); return g_ptr_array_index(priv->values, idx); } gboolean fu_csv_entry_get_value_by_column_id_uint64(FuCsvEntry *self, const gchar *column_id, guint64 *value, GError **error) { const gchar *str_value = fu_csv_entry_get_value_by_column_id(self, column_id); if (str_value == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "CSV value not found"); return FALSE; } return fu_strtoull(str_value, value, 0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error); } static void fu_csv_entry_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuCsvEntry *self = FU_CSV_ENTRY(firmware); FuCsvEntryPrivate *priv = GET_PRIVATE(self); FuCsvFirmware *parent = FU_CSV_FIRMWARE(fu_firmware_get_parent(firmware)); g_autoptr(XbBuilderNode) bc = xb_builder_node_insert(bn, "values", NULL); for (guint i = 0; i < priv->values->len; i++) { const gchar *value = g_ptr_array_index(priv->values, i); const gchar *key = fu_csv_firmware_get_column_id(parent, i); if (key != NULL) fu_xmlb_builder_insert_kv(bc, key, value); } } static gboolean fu_csv_entry_build(FuFirmware *firmware, XbNode *n, GError **error) { FuCsvEntry *self = FU_CSV_ENTRY(firmware); FuCsvFirmware *parent = FU_CSV_FIRMWARE(fu_firmware_get_parent(firmware)); gboolean add_columns = fu_csv_firmware_get_column_id(parent, 0) == NULL; g_autoptr(GPtrArray) values = NULL; values = xb_node_query(n, "values/*", 0, error); if (values == NULL) { fwupd_error_convert(error); return FALSE; } for (guint i = 0; i < values->len; i++) { XbNode *c = g_ptr_array_index(values, i); if (add_columns && xb_node_get_element(c) != NULL) fu_csv_firmware_add_column_id(parent, xb_node_get_element(c)); fu_csv_entry_add_value(self, xb_node_get_text(c)); } return TRUE; } static gboolean fu_csv_entry_parse_token_cb(GString *token, guint token_idx, gpointer user_data, GError **error) { FuCsvEntry *self = FU_CSV_ENTRY(user_data); FuCsvEntryPrivate *priv = GET_PRIVATE(self); FuCsvFirmware *parent = FU_CSV_FIRMWARE(fu_firmware_get_parent(FU_FIRMWARE(self))); const gchar *column_id = fu_csv_firmware_get_column_id(parent, token_idx); /* sanity check */ if (token_idx > FU_CSV_ENTRY_COLUMNS_MAX) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "too many columns, limit is %u", FU_CSV_ENTRY_COLUMNS_MAX); return FALSE; } if (g_strcmp0(column_id, "$id") == 0) { fu_firmware_set_id(FU_FIRMWARE(self), token->str); } else if (g_strcmp0(column_id, "$idx") == 0) { guint64 value = 0; if (!fu_strtoull(token->str, &value, 0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_firmware_set_idx(FU_FIRMWARE(self), value); } else if (g_strcmp0(column_id, "$version") == 0) { fu_firmware_set_version(FU_FIRMWARE(self), token->str); /* nocheck:set-version */ } else if (g_strcmp0(column_id, "$version_raw") == 0) { guint64 value = 0; if (!fu_strtoull(token->str, &value, 0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_firmware_set_version_raw(FU_FIRMWARE(self), value); } /* always save to value so we can write it back out */ g_ptr_array_add(priv->values, g_strdup(token->str)); return TRUE; } static gboolean fu_csv_entry_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuCsvEntry *self = FU_CSV_ENTRY(firmware); return fu_strsplit_stream(stream, 0x0, ",", fu_csv_entry_parse_token_cb, self, error); } static GByteArray * fu_csv_entry_write(FuFirmware *firmware, GError **error) { FuCsvEntry *self = FU_CSV_ENTRY(firmware); FuCsvEntryPrivate *priv = GET_PRIVATE(self); g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GString) str = g_string_new(NULL); /* single line */ for (guint i = 0; i < priv->values->len; i++) { const gchar *value = g_ptr_array_index(priv->values, i); if (str->len > 0) g_string_append(str, ","); if (value != NULL) g_string_append(str, value); } g_string_append(str, "\n"); g_byte_array_append(buf, (const guint8 *)str->str, str->len); /* success */ return g_steal_pointer(&buf); } static void fu_csv_entry_init(FuCsvEntry *self) { FuCsvEntryPrivate *priv = GET_PRIVATE(self); priv->values = g_ptr_array_new_with_free_func(g_free); } static void fu_csv_entry_finalize(GObject *object) { FuCsvEntry *self = FU_CSV_ENTRY(object); FuCsvEntryPrivate *priv = GET_PRIVATE(self); g_ptr_array_unref(priv->values); G_OBJECT_CLASS(fu_csv_entry_parent_class)->finalize(object); } static void fu_csv_entry_class_init(FuCsvEntryClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_csv_entry_finalize; firmware_class->parse = fu_csv_entry_parse; firmware_class->write = fu_csv_entry_write; firmware_class->build = fu_csv_entry_build; firmware_class->export = fu_csv_entry_export; } /** * fu_csv_entry_new: * * Creates a new #FuFirmware * * Since: 1.9.3 **/ FuFirmware * fu_csv_entry_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_CSV_ENTRY, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-csv-entry.h000066400000000000000000000015201501337203100204560ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_CSV_ENTRY (fu_csv_entry_get_type()) G_DECLARE_DERIVABLE_TYPE(FuCsvEntry, fu_csv_entry, FU, CSV_ENTRY, FuFirmware) struct _FuCsvEntryClass { FuFirmwareClass parent_class; }; FuFirmware * fu_csv_entry_new(void); void fu_csv_entry_add_value(FuCsvEntry *self, const gchar *value) G_GNUC_NON_NULL(1); const gchar * fu_csv_entry_get_value_by_idx(FuCsvEntry *self, guint idx) G_GNUC_NON_NULL(1); const gchar * fu_csv_entry_get_value_by_column_id(FuCsvEntry *self, const gchar *column_id) G_GNUC_NON_NULL(1, 2); gboolean fu_csv_entry_get_value_by_column_id_uint64(FuCsvEntry *self, const gchar *column_id, guint64 *value, GError **error) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-csv-firmware-private.h000066400000000000000000000004201501337203100225770ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-csv-firmware.h" guint fu_csv_firmware_get_idx_for_column_id(FuCsvFirmware *self, const gchar *column_id) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-csv-firmware.c000066400000000000000000000167301501337203100211350ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-byte-array.h" #include "fu-common.h" #include "fu-csv-entry.h" #include "fu-csv-firmware-private.h" #include "fu-string.h" /** * FuCsvFirmware: * * A comma seporated value file. * * See also: [class@FuFirmware] */ typedef struct { GPtrArray *column_ids; gboolean write_column_ids; } FuCsvFirmwarePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuCsvFirmware, fu_csv_firmware, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_csv_firmware_get_instance_private(o)) /** * fu_csv_firmware_add_column_id: * @self: a #FuFirmware * @column_id: (not nullable): string, e.g. `component_generation` * * Adds a column ID. * * There are several optional magic column IDs that map to #FuFirmware properties: * * * `$id` sets the firmware ID * * `$idx` sets the firmware index * * `$version` sets the firmware version * * `$version_raw` sets the raw firmware version * * Since: 1.9.3 **/ void fu_csv_firmware_add_column_id(FuCsvFirmware *self, const gchar *column_id) { FuCsvFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CSV_FIRMWARE(self)); g_return_if_fail(column_id != NULL); g_ptr_array_add(priv->column_ids, g_strdup(column_id)); } /** * fu_csv_firmware_get_column_id: * @self: a #FuFirmware * @idx: column ID idx * * Gets the column ID for a specific index position. * * Returns: a string, or %NULL if not found * * Since: 1.9.3 **/ const gchar * fu_csv_firmware_get_column_id(FuCsvFirmware *self, guint idx) { FuCsvFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CSV_FIRMWARE(self), NULL); if (idx >= priv->column_ids->len) return NULL; return g_ptr_array_index(priv->column_ids, idx); } /** * fu_csv_firmware_get_idx_for_column_id: * @self: a #FuFirmware * @column_id: (not nullable): string, e.g. `component_generation` * * Gets the column index for a given column ID. * * Returns: position, or %G_MAXUINT if unset * * Since: 1.9.3 **/ guint fu_csv_firmware_get_idx_for_column_id(FuCsvFirmware *self, const gchar *column_id) { FuCsvFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CSV_FIRMWARE(self), G_MAXUINT); g_return_val_if_fail(column_id != NULL, G_MAXUINT); for (guint i = 0; i < priv->column_ids->len; i++) { const gchar *column_id_tmp = g_ptr_array_index(priv->column_ids, i); if (g_strcmp0(column_id_tmp, column_id) == 0) return i; } return G_MAXUINT; } /** * fu_csv_firmware_set_write_column_ids: * @self: a #FuFirmware * @write_column_ids: boolean * * Sets if we should write the column ID headers on export. * * Since: 2.0.0 **/ void fu_csv_firmware_set_write_column_ids(FuCsvFirmware *self, gboolean write_column_ids) { FuCsvFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_CSV_FIRMWARE(self)); priv->write_column_ids = write_column_ids; } /** * fu_csv_firmware_get_write_column_ids: * @self: a #FuFirmware * * Gets if we should write the column ID headers on export. * * Returns: boolean. * * Since: 2.0.0 **/ gboolean fu_csv_firmware_get_write_column_ids(FuCsvFirmware *self) { FuCsvFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CSV_FIRMWARE(self), FALSE); return priv->write_column_ids; } static gboolean fu_csv_firmware_parse_token_cb(GString *token, guint token_idx, gpointer user_data, GError **error) { FuCsvFirmware *self = FU_CSV_FIRMWARE(user_data); fu_csv_firmware_add_column_id(self, token->str); return TRUE; } static gboolean fu_csv_firmware_parse_line_cb(GString *token, guint token_idx, gpointer user_data, GError **error) { FuCsvFirmware *self = FU_CSV_FIRMWARE(user_data); g_autoptr(FuFirmware) entry = fu_csv_entry_new(); g_autoptr(GBytes) fw = NULL; /* ignore blank lines */ if (token->len == 0) return TRUE; /* title */ if (g_str_has_prefix(token->str, "#")) { return fu_strsplit_full(token->str + 1, token->len - 1, ",", fu_csv_firmware_parse_token_cb, self, error); } /* parse entry */ fw = g_bytes_new(token->str, token->len); fu_firmware_set_idx(entry, token_idx); if (!fu_firmware_add_image_full(FU_FIRMWARE(self), entry, error)) return FALSE; if (!fu_firmware_parse_bytes(entry, fw, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return FALSE; return TRUE; } static gboolean fu_csv_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuCsvFirmware *self = FU_CSV_FIRMWARE(firmware); return fu_strsplit_stream(stream, 0x0, "\n", fu_csv_firmware_parse_line_cb, self, error); } static GByteArray * fu_csv_firmware_write(FuFirmware *firmware, GError **error) { FuCsvFirmware *self = FU_CSV_FIRMWARE(firmware); FuCsvFirmwarePrivate *priv = GET_PRIVATE(self); g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GPtrArray) imgs = fu_firmware_get_images(firmware); /* title section */ if (priv->write_column_ids) { g_autoptr(GString) str = g_string_new("#"); for (guint i = 0; i < priv->column_ids->len; i++) { const gchar *column_id = g_ptr_array_index(priv->column_ids, i); if (str->len > 1) g_string_append(str, ","); g_string_append(str, column_id); } g_string_append(str, "\n"); g_byte_array_append(buf, (const guint8 *)str->str, str->len); } /* each entry */ for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); g_autoptr(GBytes) img_blob = fu_firmware_write(img, error); if (img_blob == NULL) return NULL; fu_byte_array_append_bytes(buf, img_blob); } /* success */ return g_steal_pointer(&buf); } static void fu_csv_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuCsvFirmware *self = FU_CSV_FIRMWARE(firmware); FuCsvFirmwarePrivate *priv = GET_PRIVATE(self); fu_xmlb_builder_insert_kb(bn, "write_column_ids", priv->write_column_ids); } static gboolean fu_csv_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { FuCsvFirmware *self = FU_CSV_FIRMWARE(firmware); FuCsvFirmwarePrivate *priv = GET_PRIVATE(self); const gchar *tmp; /* optional properties */ tmp = xb_node_query_text(n, "write_column_ids", NULL); if (tmp != NULL) { if (!fu_strtobool(tmp, &priv->write_column_ids, error)) return FALSE; } /* success */ return TRUE; } static void fu_csv_firmware_init(FuCsvFirmware *self) { FuCsvFirmwarePrivate *priv = GET_PRIVATE(self); priv->column_ids = g_ptr_array_new_with_free_func(g_free); priv->write_column_ids = TRUE; fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_NO_AUTO_DETECTION); fu_firmware_set_images_max(FU_FIRMWARE(self), 10000); g_type_ensure(FU_TYPE_CSV_ENTRY); } static void fu_csv_firmware_finalize(GObject *object) { FuCsvFirmware *self = FU_CSV_FIRMWARE(object); FuCsvFirmwarePrivate *priv = GET_PRIVATE(self); g_ptr_array_unref(priv->column_ids); G_OBJECT_CLASS(fu_csv_firmware_parent_class)->finalize(object); } static void fu_csv_firmware_class_init(FuCsvFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_csv_firmware_finalize; firmware_class->parse = fu_csv_firmware_parse; firmware_class->write = fu_csv_firmware_write; firmware_class->export = fu_csv_firmware_export; firmware_class->build = fu_csv_firmware_build; } /** * fu_csv_firmware_new: * * Creates a new #FuFirmware * * Since: 1.9.3 **/ FuFirmware * fu_csv_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_CSV_FIRMWARE, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-csv-firmware.h000066400000000000000000000013701501337203100211340ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_CSV_FIRMWARE (fu_csv_firmware_get_type()) G_DECLARE_DERIVABLE_TYPE(FuCsvFirmware, fu_csv_firmware, FU, CSV_FIRMWARE, FuFirmware) struct _FuCsvFirmwareClass { FuFirmwareClass parent_class; }; FuFirmware * fu_csv_firmware_new(void); void fu_csv_firmware_add_column_id(FuCsvFirmware *self, const gchar *column_id) G_GNUC_NON_NULL(1, 2); const gchar * fu_csv_firmware_get_column_id(FuCsvFirmware *self, guint idx) G_GNUC_NON_NULL(1); void fu_csv_firmware_set_write_column_ids(FuCsvFirmware *self, gboolean write_column_ids); gboolean fu_csv_firmware_get_write_column_ids(FuCsvFirmware *self); fwupd-2.0.10/libfwupdplugin/fu-darwin-efivars.c000066400000000000000000000010721501337203100214420ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuDarwinEfivars" #include "config.h" #include "fu-darwin-efivars.h" struct _FuDarwinEfivars { FuEfivars parent_instance; }; G_DEFINE_TYPE(FuDarwinEfivars, fu_darwin_efivars, FU_TYPE_EFIVARS) static void fu_darwin_efivars_init(FuDarwinEfivars *self) { } static void fu_darwin_efivars_class_init(FuDarwinEfivarsClass *klass) { } FuEfivars * fu_efivars_new(void) { return FU_EFIVARS(g_object_new(FU_TYPE_DARWIN_EFIVARS, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-darwin-efivars.h000066400000000000000000000004551501337203100214530ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-efivars.h" #define FU_TYPE_DARWIN_EFIVARS (fu_darwin_efivars_get_type()) G_DECLARE_FINAL_TYPE(FuDarwinEfivars, fu_darwin_efivars, FU, DARWIN_EFIVARS, FuEfivars) fwupd-2.0.10/libfwupdplugin/fu-device-event-private.h000066400000000000000000000005471501337203100225620ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-device-event.h" FuDeviceEvent * fu_device_event_new(const gchar *id); const gchar * fu_device_event_get_id(FuDeviceEvent *self) G_GNUC_NON_NULL(1); gchar * fu_device_event_build_id(const gchar *id) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-device-event.c000066400000000000000000000341411501337203100211020ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuDeviceEvent" #include "config.h" #include "fu-device-event-private.h" #include "fu-mem.h" /** * FuDeviceEvent: * * A device event, used to enumulate hardware. * * See also: [class@FuDevice] */ typedef struct { GType gtype; gchar *key; GDestroyNotify key_destroy; gpointer data; GDestroyNotify data_destroy; } FuDeviceEventBlob; struct _FuDeviceEvent { GObject parent_instance; gchar *id; gchar *id_uncompressed; GPtrArray *values; /* element-type FuDeviceEventBlob */ }; static void fu_device_event_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_WITH_CODE(FuDeviceEvent, fu_device_event, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fu_device_event_codec_iface_init)) /* * NOTE: We use an event *counter* that gets the next event in the emulation, and this ID is only * used as a sanity check in case we have to skip an entry. */ #define FU_DEVICE_EVENT_KEY_HASH_PREFIX_SIZE 8 static void fu_device_event_blob_free(FuDeviceEventBlob *blob) { if (blob->key_destroy != NULL) g_free(blob->key); if (blob->data_destroy != NULL) blob->data_destroy(blob->data); g_free(blob); } static FuDeviceEventBlob * fu_device_event_blob_new(GType gtype, const gchar *key, gpointer data, GDestroyNotify data_destroy) { FuDeviceEventBlob *blob = g_new0(FuDeviceEventBlob, 1); const gchar *known_keys[] = { "Data", "DataOut", "Error", "ErrorMsg", "Rc", }; for (guint i = 0; i < G_N_ELEMENTS(known_keys); i++) { if (g_strcmp0(key, known_keys[i]) == 0) { blob->key = (gchar *)known_keys[i]; break; } } if (blob->key == NULL) { blob->key = g_strdup(key); blob->key_destroy = g_free; } blob->gtype = gtype; blob->data = data; blob->data_destroy = data_destroy; return blob; } /** * fu_device_event_build_id: * @id: a string * * Return the hash of the event ID. * * Returns: string hash prefix * * Since: 2.0.3 **/ gchar * fu_device_event_build_id(const gchar *id) { guint8 buf[20] = {0}; gsize bufsz = sizeof(buf); g_autoptr(GChecksum) csum = g_checksum_new(G_CHECKSUM_SHA1); g_autoptr(GString) id_hash = g_string_sized_new(FU_DEVICE_EVENT_KEY_HASH_PREFIX_SIZE + 1); g_return_val_if_fail(id != NULL, NULL); /* IMPORTANT: if you're reading this we're not using the SHA1 prefix for any kind of secure * hash, just because it is a tiny string that takes up less memory than the full ID. */ g_checksum_update(csum, (const guchar *)id, strlen(id)); g_checksum_get_digest(csum, buf, &bufsz); g_string_append_c(id_hash, '#'); for (guint i = 0; i < FU_DEVICE_EVENT_KEY_HASH_PREFIX_SIZE / 2; i++) g_string_append_printf(id_hash, "%02x", buf[i]); return g_string_free(g_steal_pointer(&id_hash), FALSE); } /** * fu_device_event_get_id: * @self: a #FuDeviceEvent * * Return the truncated SHA1 of the #FuDeviceEvent key, which is normally set when creating the * object. * * Returns: (nullable): string * * Since: 2.0.0 **/ const gchar * fu_device_event_get_id(FuDeviceEvent *self) { g_return_val_if_fail(FU_IS_DEVICE_EVENT(self), NULL); return self->id; } /** * fu_device_event_set_str: * @self: a #FuDeviceEvent * @key: (not nullable): a unique key, e.g. `Name` * @value: (nullable): a string * * Sets a string value on the event. * * Since: 2.0.0 **/ void fu_device_event_set_str(FuDeviceEvent *self, const gchar *key, const gchar *value) { g_return_if_fail(FU_IS_DEVICE_EVENT(self)); g_return_if_fail(key != NULL); g_ptr_array_add(self->values, fu_device_event_blob_new(G_TYPE_STRING, key, g_strdup(value), g_free)); } /** * fu_device_event_set_i64: * @self: a #FuDeviceEvent * @key: (not nullable): a unique key, e.g. `Name` * @value: a string * * Sets an integer value on the string. * * Since: 2.0.0 **/ void fu_device_event_set_i64(FuDeviceEvent *self, const gchar *key, gint64 value) { g_return_if_fail(FU_IS_DEVICE_EVENT(self)); g_return_if_fail(key != NULL); g_ptr_array_add( self->values, fu_device_event_blob_new(G_TYPE_INT, key, g_memdup2(&value, sizeof(value)), g_free)); } /** * fu_device_event_set_bytes: * @self: a #FuDeviceEvent * @key: (not nullable): a unique key, e.g. `Name` * @value: (not nullable): a #GBytes * * Sets a blob on the event. Note: blobs are stored internally as BASE-64 strings. * * Since: 2.0.0 **/ void fu_device_event_set_bytes(FuDeviceEvent *self, const gchar *key, GBytes *value) { g_return_if_fail(FU_IS_DEVICE_EVENT(self)); g_return_if_fail(key != NULL); g_return_if_fail(value != NULL); g_ptr_array_add(self->values, fu_device_event_blob_new( G_TYPE_STRING, key, g_base64_encode(g_bytes_get_data(value, NULL), g_bytes_get_size(value)), g_free)); } /** * fu_device_event_set_data: * @self: a #FuDeviceEvent * @key: (not nullable): a unique key, e.g. `Name` * @buf: (nullable): a buffer * @bufsz: size of @buf * * Sets a memory buffer on the event. Note: memory buffers are stored internally as BASE-64 strings. * * Since: 2.0.0 **/ void fu_device_event_set_data(FuDeviceEvent *self, const gchar *key, const guint8 *buf, gsize bufsz) { g_return_if_fail(FU_IS_DEVICE_EVENT(self)); g_return_if_fail(key != NULL); g_ptr_array_add( self->values, fu_device_event_blob_new(G_TYPE_STRING, key, g_base64_encode(buf, bufsz), g_free)); } /** * fu_device_event_set_error: * @self: a #FuDeviceEvent * @error: (not nullable): a #GError with domain #FwupdError * * Sets an error on the event. * * Since: 2.0.6 **/ void fu_device_event_set_error(FuDeviceEvent *self, const GError *error) { g_return_if_fail(FU_IS_DEVICE_EVENT(self)); g_return_if_fail(error != NULL); g_return_if_fail(error->domain == FWUPD_ERROR); fu_device_event_set_i64(self, "Error", error->code); fu_device_event_set_str(self, "ErrorMsg", error->message); } /** * fu_device_event_check_error: * @self: a #FuDeviceEvent * @error: (nullable): optional return location for an error * * Sets an error from the event if possible. * * Returns: %FALSE if @error was set * * Since: 2.0.6 **/ gboolean fu_device_event_check_error(FuDeviceEvent *self, GError **error) { gint64 code; const gchar *message; g_return_val_if_fail(FU_IS_DEVICE_EVENT(self), FALSE); /* nothing to do */ if (error == NULL) return TRUE; /* anything set */ code = fu_device_event_get_i64(self, "Error", NULL); if (code == G_MAXINT64) return TRUE; message = fu_device_event_get_str(self, "ErrorMsg", NULL); if (message == NULL) message = fwupd_error_to_string(code); /* success, in a way */ g_set_error_literal(error, FWUPD_ERROR, code, message); return FALSE; } static gpointer fu_device_event_lookup(FuDeviceEvent *self, const gchar *key, GType gtype, GError **error) { FuDeviceEventBlob *blob = NULL; for (guint i = 0; i < self->values->len; i++) { FuDeviceEventBlob *blob_tmp = g_ptr_array_index(self->values, i); if (g_strcmp0(blob_tmp->key, key) == 0) { blob = blob_tmp; break; } } if (blob == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no event for key %s", key); return NULL; } if (blob->gtype != gtype) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid event type for key %s", key); return NULL; } return blob->data; } /** * fu_device_event_get_str: * @self: a #FuDeviceEvent * @key: (not nullable): a unique key, e.g. `Name` * @error: (nullable): optional return location for an error * * Gets a string value from the event. * * Returns: (nullable): string, or %NULL on error * * Since: 2.0.0 **/ const gchar * fu_device_event_get_str(FuDeviceEvent *self, const gchar *key, GError **error) { g_return_val_if_fail(FU_IS_DEVICE_EVENT(self), NULL); g_return_val_if_fail(key != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return (const gchar *)fu_device_event_lookup(self, key, G_TYPE_STRING, error); } /** * fu_device_event_get_i64: * @self: a #FuDeviceEvent * @key: (not nullable): a unique key, e.g. `Name` * @error: (nullable): optional return location for an error * * Gets an integer value from the event. * * Returns: integer, or %G_MAXINT64 on error * * Since: 2.0.0 **/ gint64 fu_device_event_get_i64(FuDeviceEvent *self, const gchar *key, GError **error) { gint64 *val; g_return_val_if_fail(FU_IS_DEVICE_EVENT(self), G_MAXINT64); g_return_val_if_fail(key != NULL, G_MAXINT64); g_return_val_if_fail(error == NULL || *error == NULL, G_MAXINT64); val = fu_device_event_lookup(self, key, G_TYPE_INT, error); if (val == NULL) return G_MAXINT64; return *val; } /** * fu_device_event_get_bytes: * @self: a #FuDeviceEvent * @key: (not nullable): a unique key, e.g. `Name` * @error: (nullable): optional return location for an error * * Gets a memory blob from the event. * * Returns: (transfer full) (nullable): byes data, or %NULL on error * * Since: 2.0.0 **/ GBytes * fu_device_event_get_bytes(FuDeviceEvent *self, const gchar *key, GError **error) { const gchar *blobstr; gsize bufsz = 0; g_autofree guchar *buf = NULL; g_return_val_if_fail(FU_IS_DEVICE_EVENT(self), NULL); g_return_val_if_fail(key != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); blobstr = fu_device_event_lookup(self, key, G_TYPE_STRING, error); if (blobstr == NULL) return NULL; if (blobstr[0] == '\0') return g_bytes_new(NULL, 0); buf = g_base64_decode(blobstr, &bufsz); return g_bytes_new_take(g_steal_pointer(&buf), bufsz); } /** * fu_device_event_copy_data: * @self: a #FuDeviceEvent * @key: (not nullable): a unique key, e.g. `Name` * @buf: (nullable): a buffer * @bufsz: size of @buf * @actual_length: (out) (optional): the actual number of bytes sent, or %NULL * @error: (nullable): optional return location for an error * * Copies memory from the event. * * Returns: %TRUE if the buffer was copied * * Since: 2.0.0 **/ gboolean fu_device_event_copy_data(FuDeviceEvent *self, const gchar *key, guint8 *buf, gsize bufsz, gsize *actual_length, GError **error) { const gchar *blobstr; gsize bufsz_src = 0; g_autofree guchar *buf_src = NULL; g_return_val_if_fail(FU_IS_DEVICE_EVENT(self), FALSE); g_return_val_if_fail(key != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); blobstr = fu_device_event_lookup(self, key, G_TYPE_STRING, error); if (blobstr == NULL) return FALSE; buf_src = g_base64_decode(blobstr, &bufsz_src); if (actual_length != NULL) *actual_length = bufsz_src; if (buf != NULL) return fu_memcpy_safe(buf, bufsz, 0x0, buf_src, bufsz_src, 0x0, bufsz_src, error); return TRUE; } static void fu_device_event_add_json(FwupdCodec *codec, JsonBuilder *builder, FwupdCodecFlags flags) { FuDeviceEvent *self = FU_DEVICE_EVENT(codec); if (self->id_uncompressed != NULL && (flags & FWUPD_CODEC_FLAG_COMPRESSED) == 0) { json_builder_set_member_name(builder, "Id"); json_builder_add_string_value(builder, self->id_uncompressed); } else if (self->id != NULL) { json_builder_set_member_name(builder, "Id"); json_builder_add_string_value(builder, self->id); } for (guint i = 0; i < self->values->len; i++) { FuDeviceEventBlob *blob = g_ptr_array_index(self->values, i); if (blob->gtype == G_TYPE_INT) { json_builder_set_member_name(builder, blob->key); json_builder_add_int_value(builder, *((gint64 *)blob->data)); } else if (blob->gtype == G_TYPE_BYTES || blob->gtype == G_TYPE_STRING) { json_builder_set_member_name(builder, blob->key); json_builder_add_string_value(builder, (const gchar *)blob->data); } else { g_warning("invalid GType %s, ignoring", g_type_name(blob->gtype)); } } } static void fu_device_event_set_id(FuDeviceEvent *self, const gchar *id) { g_return_if_fail(FU_IS_DEVICE_EVENT(self)); g_return_if_fail(id != NULL); g_clear_pointer(&self->id, g_free); g_clear_pointer(&self->id_uncompressed, g_free); /* already a truncated SHA1 hash? */ if (g_str_has_prefix(id, "#")) { self->id = g_strdup(id); } else { self->id_uncompressed = g_strdup(id); self->id = fu_device_event_build_id(id); } } static gboolean fu_device_event_from_json(FwupdCodec *codec, JsonNode *json_node, GError **error) { FuDeviceEvent *self = FU_DEVICE_EVENT(codec); JsonNode *member_node; JsonObjectIter iter; JsonObject *json_object = json_node_get_object(json_node); const gchar *member_name; json_object_iter_init(&iter, json_object); while (json_object_iter_next(&iter, &member_name, &member_node)) { GType gtype; if (JSON_NODE_TYPE(member_node) != JSON_NODE_VALUE) continue; gtype = json_node_get_value_type(member_node); if (gtype == G_TYPE_STRING) { const gchar *str = json_node_get_string(member_node); if (g_strcmp0(member_name, "Id") == 0) { fu_device_event_set_id(self, str); } else { fu_device_event_set_str(self, member_name, str); } } else if (gtype == G_TYPE_INT64) { fu_device_event_set_i64(self, member_name, json_node_get_int(member_node)); } } /* we do not need this again, so avoid keeping all the tree data in memory */ json_node_init_null(json_node); /* success */ return TRUE; } static void fu_device_event_init(FuDeviceEvent *self) { self->values = g_ptr_array_new_with_free_func((GDestroyNotify)fu_device_event_blob_free); } static void fu_device_event_finalize(GObject *object) { FuDeviceEvent *self = FU_DEVICE_EVENT(object); g_free(self->id); g_free(self->id_uncompressed); g_ptr_array_unref(self->values); G_OBJECT_CLASS(fu_device_event_parent_class)->finalize(object); } static void fu_device_event_class_init(FuDeviceEventClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_device_event_finalize; } static void fu_device_event_codec_iface_init(FwupdCodecInterface *iface) { iface->add_json = fu_device_event_add_json; iface->from_json = fu_device_event_from_json; } /** * fu_device_event_new: * @id: a cache key, which is converted to a truncated SHA1 hash if required * * Return value: (transfer full): a new #FuDeviceEvent object. * * Since: 2.0.0 **/ FuDeviceEvent * fu_device_event_new(const gchar *id) { FuDeviceEvent *self = g_object_new(FU_TYPE_DEVICE_EVENT, NULL); if (id != NULL) fu_device_event_set_id(self, id); return FU_DEVICE_EVENT(self); } fwupd-2.0.10/libfwupdplugin/fu-device-event.h000066400000000000000000000027021501337203100211050ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_DEVICE_EVENT (fu_device_event_get_type()) G_DECLARE_FINAL_TYPE(FuDeviceEvent, fu_device_event, FU, DEVICE_EVENT, GObject) void fu_device_event_set_str(FuDeviceEvent *self, const gchar *key, const gchar *value) G_GNUC_NON_NULL(1, 2); const gchar * fu_device_event_get_str(FuDeviceEvent *self, const gchar *key, GError **error) G_GNUC_NON_NULL(1, 2); void fu_device_event_set_i64(FuDeviceEvent *self, const gchar *key, gint64 value) G_GNUC_NON_NULL(1, 2); gint64 fu_device_event_get_i64(FuDeviceEvent *self, const gchar *key, GError **error) G_GNUC_NON_NULL(1, 2); void fu_device_event_set_bytes(FuDeviceEvent *self, const gchar *key, GBytes *value) G_GNUC_NON_NULL(1, 2, 3); void fu_device_event_set_data(FuDeviceEvent *self, const gchar *key, const guint8 *buf, gsize bufsz) G_GNUC_NON_NULL(1, 2); GBytes * fu_device_event_get_bytes(FuDeviceEvent *self, const gchar *key, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_device_event_copy_data(FuDeviceEvent *self, const gchar *key, guint8 *buf, gsize bufsz, gsize *actual_length, GError **error) G_GNUC_NON_NULL(1, 2); void fu_device_event_set_error(FuDeviceEvent *self, const GError *error) G_GNUC_NON_NULL(1, 2); gboolean fu_device_event_check_error(FuDeviceEvent *self, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-device-locker.c000066400000000000000000000123221501337203100212350ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuDeviceLocker" #include "config.h" #include #include "fu-device-locker.h" #include "fu-device.h" /** * FuDeviceLocker: * * Easily close a shared resource (such as a device) when an object goes out of * scope. * * See also: [class@FuDevice] */ struct _FuDeviceLocker { GObject parent_instance; GObject *device; gboolean device_open; FuDeviceLockerFunc open_func; FuDeviceLockerFunc close_func; }; G_DEFINE_TYPE(FuDeviceLocker, fu_device_locker, G_TYPE_OBJECT) static void fu_device_locker_finalize(GObject *obj) { FuDeviceLocker *self = FU_DEVICE_LOCKER(obj); /* close device */ if (self->device_open) { g_autoptr(GError) error = NULL; if (!self->close_func(self->device, &error)) g_warning("failed to close device: %s", error->message); } if (self->device != NULL) g_object_unref(self->device); G_OBJECT_CLASS(fu_device_locker_parent_class)->finalize(obj); } static void fu_device_locker_class_init(FuDeviceLockerClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_device_locker_finalize; } static void fu_device_locker_init(FuDeviceLocker *self) { } /** * fu_device_locker_close: * @self: a #FuDeviceLocker * @error: (nullable): optional return location for an error * * Closes the locker before it gets cleaned up. * * This function can be used to manually close a device managed by a locker, * and allows the caller to properly handle the error. * * Returns: %TRUE for success * * Since: 1.4.0 **/ gboolean fu_device_locker_close(FuDeviceLocker *self, GError **error) { g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_DEVICE_LOCKER(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!self->device_open) return TRUE; if (!self->close_func(self->device, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_debug("ignoring: %s", error_local->message); return TRUE; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } self->device_open = FALSE; return TRUE; } /** * fu_device_locker_new: * @device: a #GObject * @error: (nullable): optional return location for an error * * Opens the device for use. When the #FuDeviceLocker is deallocated the device * will be closed and any error will just be directed to the console. * This object is typically called using g_autoptr() but the device can also be * manually closed using g_clear_object(). * * The functions used for opening and closing the device are set automatically. * If the @device is not a type or supertype of #FuDevice then this function will not work. * * For custom objects please use fu_device_locker_new_full(). * * NOTE: If the @open_func failed then the @close_func will not be called. * * Think of this object as the device ownership. * * Returns: a device locker, or %NULL if the @open_func failed. * * Since: 1.0.0 **/ FuDeviceLocker * fu_device_locker_new(gpointer device, GError **error) { g_return_val_if_fail(device != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* FuDevice */ if (FU_IS_DEVICE(device)) { return fu_device_locker_new_full(device, (FuDeviceLockerFunc)fu_device_open, (FuDeviceLockerFunc)fu_device_close, error); } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device object type not supported"); return NULL; } /** * fu_device_locker_new_full: * @device: a #GObject * @open_func: (scope async): a function to open the device * @close_func: (scope async): a function to close the device * @error: (nullable): optional return location for an error * * Opens the device for use. When the #FuDeviceLocker is deallocated the device * will be closed and any error will just be directed to the console. * This object is typically called using g_autoptr() but the device can also be * manually closed using g_clear_object(). * * NOTE: If the @open_func failed then the @close_func will not be called. * * Think of this object as the device ownership. * * Returns: a device locker, or %NULL if the @open_func failed. * * Since: 1.0.0 **/ FuDeviceLocker * fu_device_locker_new_full(gpointer device, FuDeviceLockerFunc open_func, FuDeviceLockerFunc close_func, GError **error) { g_autoptr(FuDeviceLocker) self = NULL; g_return_val_if_fail(device != NULL, NULL); g_return_val_if_fail(open_func != NULL, NULL); g_return_val_if_fail(close_func != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* create object */ self = g_object_new(FU_TYPE_DEVICE_LOCKER, NULL); self->device = g_object_ref(device); self->open_func = open_func; self->close_func = close_func; /* open device */ if (!self->open_func(device, error)) { g_autoptr(GError) error_local = NULL; if (!self->close_func(device, &error_local)) { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) { g_debug("ignoring close error on aborted open: %s", error_local->message); } } return NULL; } /* success */ self->device_open = TRUE; return g_steal_pointer(&self); } fwupd-2.0.10/libfwupdplugin/fu-device-locker.h000066400000000000000000000016231501337203100212440ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_DEVICE_LOCKER (fu_device_locker_get_type()) G_DECLARE_FINAL_TYPE(FuDeviceLocker, fu_device_locker, FU, DEVICE_LOCKER, GObject) /** * FuDeviceLockerFunc: * * Callback to use when opening and closing using [ctor@DeviceLocker.new_full]. **/ typedef gboolean (*FuDeviceLockerFunc)(GObject *device, GError **error); FuDeviceLocker * fu_device_locker_new(gpointer device, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); FuDeviceLocker * fu_device_locker_new_full(gpointer device, FuDeviceLockerFunc open_func, FuDeviceLockerFunc close_func, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_device_locker_close(FuDeviceLocker *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-device-metadata.h000066400000000000000000000033451501337203100215500ustar00rootroot00000000000000/* * Copyright 2017 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include /** * FU_DEVICE_METADATA_TBT_IS_SAFE_MODE: * * If the Thunderbolt hardware is stuck in safe mode. * Consumed by the thunderbolt plugin. */ #define FU_DEVICE_METADATA_TBT_IS_SAFE_MODE "Thunderbolt::IsSafeMode" /** * FU_DEVICE_METADATA_UEFI_DEVICE_KIND: * * The type of UEFI device, e.g. "system-firmware" or "device-firmware" * Consumed by the uefi plugin when other devices register fake devices that * need to be handled as a capsule update. */ #define FU_DEVICE_METADATA_UEFI_DEVICE_KIND "UefiDeviceKind" /** * FU_DEVICE_METADATA_UEFI_FW_VERSION: * * The firmware version of the UEFI device specified as a 32 bit unsigned * integer. * Consumed by the uefi plugin when other devices register fake devices that * need to be handled as a capsule update. */ #define FU_DEVICE_METADATA_UEFI_FW_VERSION "UefiFwVersion" /** * FU_DEVICE_METADATA_UEFI_CAPSULE_FLAGS: * * The capsule flags for the UEFI device, e.g. %EFI_CAPSULE_HEADER_FLAGS_PERSIST_ACROSS_RESET * Consumed by the uefi plugin when other devices register fake devices that * need to be handled as a capsule update. */ #define FU_DEVICE_METADATA_UEFI_CAPSULE_FLAGS "UefiCapsuleFlags" /** * FU_DEVICE_METADATA_CPU_MITIGATIONS_REQUIRED: * * CPU mitigations required. See the CPU plugin for more details. */ #define FU_DEVICE_METADATA_CPU_MITIGATIONS_REQUIRED "CpuMitigationsRequired" /** * FU_DEVICE_METADATA_CPU_SINKCLOSE_MICROCODE_VER: * * Microcode version required to mitigate Sinkclose. See the CPU plugin for more details. */ #define FU_DEVICE_METADATA_CPU_SINKCLOSE_MICROCODE_VER "CpuSinkcloseMicrocodeVersion" fwupd-2.0.10/libfwupdplugin/fu-device-private.h000066400000000000000000000067731501337203100214520ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-backend.h" #include "fu-device.h" #define fu_device_set_plugin(d, v) fwupd_device_set_plugin(FWUPD_DEVICE(d), v) void fu_device_remove_children(FuDevice *self) G_GNUC_NON_NULL(1); GPtrArray * fu_device_get_parent_guids(FuDevice *self) G_GNUC_NON_NULL(1); gboolean fu_device_has_parent_guid(FuDevice *self, const gchar *guid) G_GNUC_NON_NULL(1, 2); GPtrArray * fu_device_get_parent_physical_ids(FuDevice *self) G_GNUC_NON_NULL(1); gboolean fu_device_has_parent_physical_id(FuDevice *self, const gchar *physical_id) G_GNUC_NON_NULL(1, 2); GPtrArray * fu_device_get_parent_backend_ids(FuDevice *self) G_GNUC_NON_NULL(1); gboolean fu_device_has_parent_backend_id(FuDevice *self, const gchar *backend_id) G_GNUC_NON_NULL(1, 2); void fu_device_set_parent(FuDevice *self, FuDevice *parent) G_GNUC_NON_NULL(1); gint fu_device_get_order(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_set_order(FuDevice *self, gint order) G_GNUC_NON_NULL(1); const gchar * fu_device_get_update_request_id(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_set_update_request_id(FuDevice *self, const gchar *update_request_id) G_GNUC_NON_NULL(1); const gchar * fu_device_get_fwupd_version(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_set_fwupd_version(FuDevice *self, const gchar *fwupd_version) G_GNUC_NON_NULL(1, 2); gboolean fu_device_ensure_id(FuDevice *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); void fu_device_incorporate_from_component(FuDevice *self, XbNode *component) G_GNUC_NON_NULL(1, 2); void fu_device_replace(FuDevice *self, FuDevice *donor) G_GNUC_NON_NULL(1); void fu_device_ensure_from_component(FuDevice *self, XbNode *component) G_GNUC_NON_NULL(1, 2); void fu_device_ensure_from_release(FuDevice *self, XbNode *rel) G_GNUC_NON_NULL(1, 2); void fu_device_convert_instance_ids(FuDevice *self) G_GNUC_NON_NULL(1); GPtrArray * fu_device_get_possible_plugins(FuDevice *self) G_GNUC_NON_NULL(1); guint fu_device_get_request_cnt(FuDevice *self, FwupdRequestKind request_kind) G_GNUC_NON_NULL(1); void fu_device_set_progress(FuDevice *self, FuProgress *progress) G_GNUC_NON_NULL(1); gboolean fu_device_set_quirk_kv(FuDevice *self, const gchar *key, const gchar *value, FuContextQuirkSource source, GError **error) G_GNUC_NON_NULL(1, 2, 3); void fu_device_set_specialized_gtype(FuDevice *self, GType gtype) G_GNUC_NON_NULL(1); void fu_device_set_proxy_gtype(FuDevice *self, GType gtype) G_GNUC_NON_NULL(1); GPtrArray * fu_device_get_counterpart_guids(FuDevice *self) G_GNUC_NON_NULL(1); gboolean fu_device_is_updatable(FuDevice *self) G_GNUC_NON_NULL(1); const gchar * fu_device_get_custom_flags(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_set_custom_flags(FuDevice *self, const gchar *custom_flags) G_GNUC_NON_NULL(1); void fu_device_register_private_flag_safe(FuDevice *self, const gchar *flag); void fu_device_add_event(FuDevice *self, FuDeviceEvent *event); void fu_device_clear_events(FuDevice *self); GPtrArray * fu_device_get_events(FuDevice *self); void fu_device_set_target(FuDevice *self, FuDevice *target); FuBackend * fu_device_get_backend(FuDevice *self); void fu_device_set_backend(FuDevice *self, FuBackend *backend); void fu_device_add_json(FuDevice *self, JsonBuilder *builder, FwupdCodecFlags flags) G_GNUC_NON_NULL(1, 2); gboolean fu_device_from_json(FuDevice *self, JsonObject *json_object, GError **error) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-device-progress.c000066400000000000000000000054131501337203100216250ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuDeviceProgress" #include "config.h" #include #include "fu-device-progress.h" struct _FuDeviceProgress { GObject parent_instance; FuDevice *device; FuProgress *progress; guint percentage_changed_id; guint status_changed_id; }; G_DEFINE_TYPE(FuDeviceProgress, fu_device_progress, G_TYPE_OBJECT) static void fu_device_progress_percentage_changed_cb(FuProgress *progress, guint percentage, gpointer user_data) { FuDeviceProgress *self = FU_DEVICE_PROGRESS(user_data); fu_device_set_percentage(self->device, percentage); } static void fu_device_progress_status_changed_cb(FuProgress *progress, FwupdStatus status, gpointer user_data) { FuDeviceProgress *self = FU_DEVICE_PROGRESS(user_data); fu_device_set_status(self->device, status); } static void fu_device_progress_finalize(GObject *obj) { FuDeviceProgress *self = FU_DEVICE_PROGRESS(obj); g_signal_handler_disconnect(self->progress, self->percentage_changed_id); g_signal_handler_disconnect(self->progress, self->status_changed_id); fu_device_set_status(self->device, FWUPD_STATUS_IDLE); fu_device_set_percentage(self->device, 0); g_object_unref(self->device); g_object_unref(self->progress); G_OBJECT_CLASS(fu_device_progress_parent_class)->finalize(obj); } static void fu_device_progress_class_init(FuDeviceProgressClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_device_progress_finalize; } static void fu_device_progress_init(FuDeviceProgress *self) { } /** * fu_device_progress_new: * @device: a #FuDevice * @progress: a #FuProgress * * Binds the device to the progress object so that the status and percentage will be coped from * the progress all the time this object is alive. * * When this object is finalized the *device* status will be set to `idle` and the percentage reset * back to 0%. * * Returns: a #FuDeviceProgress * * Since: 1.8.11 **/ FuDeviceProgress * fu_device_progress_new(FuDevice *device, FuProgress *progress) { g_autoptr(FuDeviceProgress) self = g_object_new(FU_TYPE_DEVICE_PROGRESS, NULL); g_return_val_if_fail(FU_IS_DEVICE(device), NULL); g_return_val_if_fail(FU_IS_PROGRESS(progress), NULL); /* connect up to device */ self->percentage_changed_id = g_signal_connect(FU_PROGRESS(progress), "percentage-changed", G_CALLBACK(fu_device_progress_percentage_changed_cb), self); self->status_changed_id = g_signal_connect(FU_PROGRESS(progress), "status-changed", G_CALLBACK(fu_device_progress_status_changed_cb), self); self->device = g_object_ref(device); self->progress = g_object_ref(progress); /* success */ return g_steal_pointer(&self); } fwupd-2.0.10/libfwupdplugin/fu-device-progress.h000066400000000000000000000007211501337203100216270ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-device.h" #include "fu-progress.h" #define FU_TYPE_DEVICE_PROGRESS (fu_device_progress_get_type()) G_DECLARE_FINAL_TYPE(FuDeviceProgress, fu_device_progress, FU, DEVICE_PROGRESS, GObject) FuDeviceProgress * fu_device_progress_new(FuDevice *device, FuProgress *progress) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-device.c000066400000000000000000007043061501337203100177720ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuDevice" #include "config.h" #include #include #include "fwupd-common.h" #include "fwupd-device-private.h" #include "fwupd-enums-private.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-chunk-array.h" #include "fu-common.h" #include "fu-device-event-private.h" #include "fu-device-private.h" #include "fu-input-stream.h" #include "fu-quirks.h" #include "fu-security-attr.h" #include "fu-string.h" #include "fu-version-common.h" #define FU_DEVICE_RETRY_OPEN_COUNT 5 #define FU_DEVICE_RETRY_OPEN_DELAY 500 /* ms */ /** * FuDevice: * * A physical or logical device that is exported to the daemon. * * See also: [class@FuDeviceLocker], [class@Fwupd.Device] */ static void fu_device_finalize(GObject *object); static void fu_device_inhibit_full(FuDevice *self, FwupdDeviceProblem problem, const gchar *inhibit_id, const gchar *reason); typedef struct { gchar *equivalent_id; gchar *physical_id; gchar *logical_id; gchar *backend_id; gchar *update_request_id; gchar *update_message; gchar *update_image; gchar *fwupd_version; gchar *proxy_guid; FuDevice *proxy; /* noref */ FuDevice *target; /* ref */ FuBackend *backend; /* noref */ FuContext *ctx; gint64 created_usec; gint64 modified_usec; guint16 vid; guint16 pid; GHashTable *inhibits; /* (nullable) */ GHashTable *metadata; /* (nullable) */ GPtrArray *parent_guids; /* (nullable) (element-type utf-8) */ GPtrArray *parent_physical_ids; /* (nullable) */ GPtrArray *parent_backend_ids; /* (nullable) */ GPtrArray *events; /* (nullable) (element-type FuDeviceEvent) */ guint event_idx; guint remove_delay; /* ms */ guint acquiesce_delay; /* ms */ guint request_cnts[FWUPD_REQUEST_KIND_LAST]; gint order; guint priority; guint poll_id; gint poll_locker_cnt; gboolean done_probe; gboolean done_setup; gboolean device_id_valid; guint64 size_min; guint64 size_max; gint open_refcount; /* atomic */ GType specialized_gtype; GType proxy_gtype; GType firmware_gtype; GPtrArray *possible_plugins; /* (element-type utf-8) */ GPtrArray *instance_ids; /* (nullable) (element-type FuDeviceInstanceIdItem) */ GPtrArray *retry_recs; /* (nullable) (element-type FuDeviceRetryRecovery) */ guint retry_delay; GPtrArray *private_flags_registered; /* (nullable) (element-type GRefString) */ GPtrArray *private_flags; /* (nullable) (no-ref) (element-type GRefString) */ gchar *custom_flags; gulong notify_flags_handler_id; gulong notify_flags_proxy_id; GHashTable *instance_hash; /* (nullable) */ FuProgress *progress; /* provided for FuDevice notify callbacks */ } FuDevicePrivate; typedef struct { GQuark domain; gint code; FuDeviceRetryFunc recovery_func; } FuDeviceRetryRecovery; typedef struct { FwupdDeviceProblem problem; gchar *inhibit_id; gchar *reason; } FuDeviceInhibit; typedef struct { gchar *instance_id; gchar *guid; FuDeviceInstanceFlag flags; } FuDeviceInstanceIdItem; enum { PROP_0, PROP_PHYSICAL_ID, PROP_LOGICAL_ID, PROP_BACKEND_ID, PROP_EQUIVALENT_ID, PROP_UPDATE_MESSAGE, PROP_UPDATE_IMAGE, PROP_CONTEXT, PROP_BACKEND, PROP_PROXY, PROP_PARENT, PROP_PRIVATE_FLAGS, PROP_VID, PROP_PID, PROP_LAST }; enum { SIGNAL_CHILD_ADDED, SIGNAL_CHILD_REMOVED, SIGNAL_REQUEST, SIGNAL_LAST }; static guint signals[SIGNAL_LAST] = {0}; G_DEFINE_TYPE_WITH_PRIVATE(FuDevice, fu_device, FWUPD_TYPE_DEVICE); #define GET_PRIVATE(o) (fu_device_get_instance_private(o)) static void fu_device_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuDevice *self = FU_DEVICE(object); FuDevicePrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_PHYSICAL_ID: g_value_set_string(value, priv->physical_id); break; case PROP_LOGICAL_ID: g_value_set_string(value, priv->logical_id); break; case PROP_BACKEND_ID: g_value_set_string(value, priv->backend_id); break; case PROP_EQUIVALENT_ID: g_value_set_string(value, priv->equivalent_id); break; case PROP_UPDATE_MESSAGE: g_value_set_string(value, priv->update_message); break; case PROP_UPDATE_IMAGE: g_value_set_string(value, priv->update_image); break; case PROP_CONTEXT: g_value_set_object(value, priv->ctx); break; case PROP_BACKEND: g_value_set_object(value, priv->backend); break; case PROP_PROXY: g_value_set_object(value, priv->proxy); break; case PROP_PARENT: g_value_set_object(value, fu_device_get_parent(self)); break; case PROP_PRIVATE_FLAGS: g_value_set_uint64(value, priv->private_flags->len); break; case PROP_VID: g_value_set_uint(value, priv->vid); break; case PROP_PID: g_value_set_uint(value, priv->pid); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_device_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuDevice *self = FU_DEVICE(object); switch (prop_id) { case PROP_PHYSICAL_ID: fu_device_set_physical_id(self, g_value_get_string(value)); break; case PROP_LOGICAL_ID: fu_device_set_logical_id(self, g_value_get_string(value)); break; case PROP_BACKEND_ID: fu_device_set_backend_id(self, g_value_get_string(value)); break; case PROP_EQUIVALENT_ID: fu_device_set_equivalent_id(self, g_value_get_string(value)); break; case PROP_UPDATE_MESSAGE: fu_device_set_update_message(self, g_value_get_string(value)); break; case PROP_UPDATE_IMAGE: fu_device_set_update_image(self, g_value_get_string(value)); break; case PROP_CONTEXT: fu_device_set_context(self, g_value_get_object(value)); break; case PROP_BACKEND: fu_device_set_backend(self, g_value_get_object(value)); break; case PROP_PROXY: fu_device_set_proxy(self, g_value_get_object(value)); break; case PROP_PARENT: fu_device_set_parent(self, g_value_get_object(value)); break; case PROP_VID: fu_device_set_vid(self, g_value_get_uint(value)); break; case PROP_PID: fu_device_set_pid(self, g_value_get_uint(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } /* private */ void fu_device_register_private_flag_safe(FuDevice *self, const gchar *flag) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(flag != NULL); g_ptr_array_add(priv->private_flags_registered, g_ref_string_new_intern(flag)); } static void fu_device_register_flags(FuDevice *self) { fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_ICON); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_NAME); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_NAME_CATEGORY); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_VERFMT); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_ONLY_SUPPORTED); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_NO_AUTO_INSTANCE_IDS); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_ENSURE_SEMVER); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_RETRY_OPEN); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_INHERIT_ACTIVATION); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_IS_OPEN); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_NO_SERIAL_NUMBER); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_AUTO_PARENT_CHILDREN); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_ATTACH_EXTRA_RESET); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_INHIBIT_CHILDREN); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_NO_AUTO_REMOVE_CHILDREN); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_OPEN); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_USE_PROXY_FOR_OPEN); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_BATTERY); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_USE_PROXY_FALLBACK); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_NO_AUTO_REMOVE); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_VENDOR); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_NO_LID_CLOSED); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_NO_PROBE); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_SIGNED); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_AUTO_PAUSE_POLLING); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_DELAYED_REMOVAL); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_IGNORE_SYSTEM_POWER); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_SAVE_INTO_BACKUP_REMOTE); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_FLAGS); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_VERSION); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_MD_ONLY_CHECKSUM); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_ADD_INSTANCE_ID_REV); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_UNCONNECTED); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_DISPLAY_REQUIRED); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_UPDATE_PENDING); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_NO_GENERIC_GUIDS); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_ENFORCE_REQUIRES); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE_CHILD); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_HOST_CPU); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_HOST_CPU_CHILD); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_EXPLICIT_ORDER); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_REFCOUNTED_PROXY); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_INSTALL_PARENT_FIRST); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_REGISTERED); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_ADD_COUNTERPART_GUIDS); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_USE_RUNTIME_VERSION); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_SKIPS_RESTART); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_IS_FAKE); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_COUNTERPART_VISIBLE); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_DETACH_PREPARE_FIRMWARE); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_EMULATED_REQUIRE_SETUP); fu_device_register_private_flag_safe(self, FU_DEVICE_PRIVATE_FLAG_INSTALL_LOOP_RESTART); } static void fu_device_ensure_private_flags(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); if (priv->private_flags_registered != NULL) return; priv->private_flags_registered = g_ptr_array_new_with_free_func((GDestroyNotify)g_ref_string_release); priv->private_flags = g_ptr_array_new(); /* subclassed */ if (device_class->register_flags != NULL) device_class->register_flags(self); } static GRefString * fu_device_find_private_flag_registered(FuDevice *self, const gchar *flag) { FuDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(GRefString) flag_ref = g_ref_string_new_intern(flag); /* make sure base private flags are registered */ fu_device_ensure_private_flags(self); for (guint i = 0; i < priv->private_flags_registered->len; i++) { GRefString *flag_tmp = g_ptr_array_index(priv->private_flags_registered, i); if (flag_ref == flag_tmp) return flag_tmp; } return NULL; } /** * fu_device_add_private_flag: * @self: a #FuDevice * @flag: a device flag * * Adds a private flag that can be used by the plugin for any purpose. * * Since: 1.6.2 **/ void fu_device_add_private_flag(FuDevice *self, const gchar *flag) { FuDevicePrivate *priv = GET_PRIVATE(self); GRefString *flag_registered; g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(flag != NULL); /* do not let devices be updated until re-connected */ if (g_strcmp0(flag, FU_DEVICE_PRIVATE_FLAG_UNCONNECTED) == 0) fu_device_inhibit(self, "unconnected", "Device has been removed"); /* add counterpart GUIDs already added */ if (g_strcmp0(flag, FU_DEVICE_PRIVATE_FLAG_COUNTERPART_VISIBLE) == 0) { for (guint i = 0; priv->instance_ids != NULL && i < priv->instance_ids->len; i++) { FuDeviceInstanceIdItem *item = g_ptr_array_index(priv->instance_ids, i); if (item->flags & FU_DEVICE_INSTANCE_FLAG_COUNTERPART) item->flags |= FU_DEVICE_INSTANCE_FLAG_VISIBLE; } } /* reset this back to the default */ if (g_strcmp0(flag, FU_DEVICE_PRIVATE_FLAG_EXPLICIT_ORDER) == 0) { GPtrArray *children = fu_device_get_children(self); for (guint i = 0; i < children->len; i++) { FuDevice *child_tmp = g_ptr_array_index(children, i); fu_device_add_private_flag(child_tmp, FU_DEVICE_PRIVATE_FLAG_EXPLICIT_ORDER); } fu_device_set_order(self, G_MAXINT); } /* check exists */ flag_registered = fu_device_find_private_flag_registered(self, flag); if (flag_registered == NULL) { #ifndef SUPPORTED_BUILD g_critical("%s flag %s is unknown -- use fu_device_register_private_flag()", G_OBJECT_TYPE_NAME(self), flag); #endif return; } /* already set */ if (g_ptr_array_find(priv->private_flags, flag_registered, NULL)) return; /* add */ g_ptr_array_add(priv->private_flags, (gpointer)flag_registered); /* no ref */ g_object_notify(G_OBJECT(self), "private-flags"); } /** * fu_device_remove_private_flag: * @self: a #FuDevice * @flag: a device flag * * Removes a private flag that can be used by the plugin for any purpose. * * Since: 1.6.2 **/ void fu_device_remove_private_flag(FuDevice *self, const gchar *flag) { FuDevicePrivate *priv = GET_PRIVATE(self); GRefString *flag_registered; g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(flag != NULL); if (g_strcmp0(flag, FU_DEVICE_PRIVATE_FLAG_UNCONNECTED) == 0) fu_device_uninhibit(self, "unconnected"); flag_registered = fu_device_find_private_flag_registered(self, flag); if (flag_registered == NULL) { #ifndef SUPPORTED_BUILD g_critical("%s flag %s is unknown -- use fu_device_register_private_flag()", G_OBJECT_TYPE_NAME(self), flag); #endif return; } g_ptr_array_remove(priv->private_flags, (gpointer)flag_registered); g_object_notify(G_OBJECT(self), "private-flags"); } /** * fu_device_has_private_flag: * @self: a #FuDevice * @flag: a device flag * * Tests for a private flag that can be used by the plugin for any purpose. * * Since: 1.6.2 **/ gboolean fu_device_has_private_flag(FuDevice *self, const gchar *flag) { FuDevicePrivate *priv = GET_PRIVATE(self); GRefString *flag_registered; g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(flag != NULL, FALSE); flag_registered = fu_device_find_private_flag_registered(self, flag); if (flag_registered == NULL) { #ifndef SUPPORTED_BUILD g_critical("%s flag %s is unknown -- use fu_device_register_private_flag()", G_OBJECT_TYPE_NAME(self), flag); #endif return FALSE; } return g_ptr_array_find(priv->private_flags, flag_registered, NULL); } /** * fu_device_get_request_cnt: * @self: a #FuDevice * @request_kind: the type of request * * Returns the number of requests of a specific kind. This function is only * useful to the daemon, which uses it to synthesize artificial events for * plugins not yet ported to [class@Fwupd.Request]. * * Returns: integer, usually 0 * * Since: 1.6.2 **/ guint fu_device_get_request_cnt(FuDevice *self, FwupdRequestKind request_kind) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), G_MAXUINT); g_return_val_if_fail(request_kind < FWUPD_REQUEST_KIND_LAST, G_MAXUINT); return priv->request_cnts[request_kind]; } /** * fu_device_get_possible_plugins: * @self: a #FuDevice * * Gets the list of possible plugin names, typically added from quirk files. * * Returns: (element-type utf8) (transfer container): plugin names * * Since: 1.3.3 **/ GPtrArray * fu_device_get_possible_plugins(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); return g_ptr_array_ref(priv->possible_plugins); } /** * fu_device_add_possible_plugin: * @self: a #FuDevice * @plugin: a plugin name, e.g. `dfu` * * Adds a plugin name to the list of plugins that *might* be able to handle this * device. This is typically called from a quirk handler. * * Duplicate plugin names are ignored. * * Since: 1.5.1 **/ void fu_device_add_possible_plugin(FuDevice *self, const gchar *plugin) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(plugin != NULL); /* add if it does not already exist */ if (g_ptr_array_find_with_equal_func(priv->possible_plugins, plugin, g_str_equal, NULL)) return; g_ptr_array_add(priv->possible_plugins, g_strdup(plugin)); } /** * fu_device_retry_add_recovery: * @self: a #FuDevice * @domain: a #GQuark, or %0 for all domains * @code: a #GError code * @func: (scope async) (nullable): a function to recover the device * * Sets the optional function to be called when fu_device_retry() fails, which * is possibly a device reset. * * If @func is %NULL then recovery is not possible and an error is returned * straight away. * * Since: 1.4.0 **/ void fu_device_retry_add_recovery(FuDevice *self, GQuark domain, gint code, FuDeviceRetryFunc func) { FuDevicePrivate *priv = GET_PRIVATE(self); FuDeviceRetryRecovery *rec; g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(domain != 0); /* ensure */ if (priv->retry_recs == NULL) priv->retry_recs = g_ptr_array_new_with_free_func(g_free); rec = g_new(FuDeviceRetryRecovery, 1); rec->domain = domain; rec->code = code; rec->recovery_func = func; g_ptr_array_add(priv->retry_recs, rec); } /** * fu_device_retry_set_delay: * @self: a #FuDevice * @delay: delay in ms * * Sets the recovery delay between failed retries. * * Since: 1.4.0 **/ void fu_device_retry_set_delay(FuDevice *self, guint delay) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); priv->retry_delay = delay; } /** * fu_device_retry_full: * @self: a #FuDevice * @func: (scope async) (closure user_data): a function to execute * @count: the number of tries to try the function * @delay: the delay between each try in ms * @user_data: (nullable): a helper to pass to @func * @error: (nullable): optional return location for an error * * Calls a specific function a number of times, optionally handling the error * with a reset action. * * If fu_device_retry_add_recovery() has not been used then all errors are * considered non-fatal until the last try. * * If the reset function returns %FALSE, then the function returns straight away * without processing any pending retries. * * Since: 1.5.5 **/ gboolean fu_device_retry_full(FuDevice *self, FuDeviceRetryFunc func, guint count, guint delay, gpointer user_data, GError **error) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(func != NULL, FALSE); g_return_val_if_fail(count >= 1, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); for (guint i = 0;; i++) { g_autoptr(GError) error_local = NULL; /* delay */ if (i > 0) fu_device_sleep(self, delay); /* run function, if success return success */ if (func(self, user_data, &error_local)) break; /* sanity check */ if (error_local == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "exec failed but no error set!"); return FALSE; } /* too many retries */ if (i >= count - 1) { g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed after %u retries: ", count); return FALSE; } /* show recoverable error on the console */ if (priv->retry_recs == NULL || priv->retry_recs->len == 0) { g_info("failed on try %u of %u: %s", i + 1, count, error_local->message); continue; } /* find the condition that matches */ for (guint j = 0; j < priv->retry_recs->len; j++) { FuDeviceRetryRecovery *rec = g_ptr_array_index(priv->retry_recs, j); if (g_error_matches(error_local, rec->domain, rec->code)) { if (rec->recovery_func != NULL) { if (!rec->recovery_func(self, user_data, error)) return FALSE; } else { g_propagate_prefixed_error( error, g_steal_pointer(&error_local), "device recovery not possible: "); return FALSE; } } } } /* success */ return TRUE; } /** * fu_device_retry: * @self: a #FuDevice * @func: (scope async) (closure user_data): a function to execute * @count: the number of tries to try the function * @user_data: (nullable): a helper to pass to @func * @error: (nullable): optional return location for an error * * Calls a specific function a number of times, optionally handling the error * with a reset action. * * If fu_device_retry_add_recovery() has not been used then all errors are * considered non-fatal until the last try. * * If the reset function returns %FALSE, then the function returns straight away * without processing any pending retries. * * Since: 1.4.0 **/ gboolean fu_device_retry(FuDevice *self, FuDeviceRetryFunc func, guint count, gpointer user_data, GError **error) { FuDevicePrivate *priv = GET_PRIVATE(self); return fu_device_retry_full(self, func, count, priv->retry_delay, user_data, error); } /** * fu_device_sleep: * @self: a #FuDevice * @delay_ms: delay in milliseconds * * Delays program execution up to 100 seconds, unless the device is emulated where no delays is * performed. * * Long unavoidable delays (more than 1 second) should really use `fu_device_sleep_full()` so that * the percentage progress bar is updated. * * Since: 1.8.11 **/ void fu_device_sleep(FuDevice *self, guint delay_ms) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(delay_ms < 100000); if (fu_device_has_flag(self, FWUPD_DEVICE_FLAG_EMULATED)) return; if (priv->proxy != NULL && fu_device_has_flag(priv->proxy, FWUPD_DEVICE_FLAG_EMULATED)) return; if (delay_ms > 0) g_usleep(delay_ms * 1000); } /** * fu_device_sleep_full: * @self: a #FuDevice * @delay_ms: delay in milliseconds * @progress: a #FuProgress * * Delays program execution up to 1000 seconds, unless the device is emulated where no delays is * performed. * * Since: 1.8.11 **/ void fu_device_sleep_full(FuDevice *self, guint delay_ms, FuProgress *progress) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(delay_ms < 1000000); g_return_if_fail(FU_IS_PROGRESS(progress)); if (fu_device_has_flag(self, FWUPD_DEVICE_FLAG_EMULATED)) return; if (priv->proxy != NULL && fu_device_has_flag(priv->proxy, FWUPD_DEVICE_FLAG_EMULATED)) return; if (delay_ms > 0) fu_progress_sleep(progress, delay_ms); } /** * fu_device_set_contents: * @self: a #FuDevice * @filename: full path to a file * @stream: data to write * @progress: a #FuProgress * @error: (nullable): optional return location for an error * * Writes @stream to @filename, emulating if required. * * Returns: %TRUE on success * * Since: 2.0.2 **/ gboolean fu_device_set_contents(FuDevice *self, const gchar *filename, GInputStream *stream, FuProgress *progress, GError **error) { FuDeviceEvent *event = NULL; g_autofree gchar *event_id = NULL; g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(GByteArray) buf_tagged = g_byte_array_new(); g_autoptr(GFile) file = NULL; g_autoptr(GOutputStream) ostr = NULL; g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(filename != NULL, FALSE); g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(FU_IS_PROGRESS(progress), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* need event ID */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_strdup_printf("SetContents:Filename=%s", filename); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { g_autoptr(GBytes) blob1 = NULL; g_autoptr(GBytes) blob2 = NULL; event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return FALSE; blob1 = fu_device_event_get_bytes(event, "Data", error); if (blob1 == NULL) return FALSE; blob2 = fu_input_stream_read_bytes(stream, 0x0, G_MAXSIZE, progress, error); if (blob2 == NULL) return FALSE; return fu_bytes_compare(blob1, blob2, error); } /* save */ if (event_id != NULL) event = fu_device_save_event(FU_DEVICE(self), event_id); /* open file */ file = g_file_new_for_path(filename); ostr = G_OUTPUT_STREAM(g_file_replace(file, NULL, FALSE, G_FILE_CREATE_NONE, NULL, error)); if (ostr == NULL) return FALSE; /* write in 32k chunks */ chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 0x8000, error); if (chunks == NULL) return FALSE; fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { gssize wrote; g_autoptr(FuChunk) chk = NULL; g_autoptr(GBytes) blob = NULL; chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; blob = fu_chunk_get_bytes(chk); wrote = g_output_stream_write_bytes(ostr, blob, NULL, error); if (wrote < 0) return FALSE; if ((gsize)wrote != g_bytes_get_size(blob)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "only wrote 0x%x bytes of 0x%x", (guint)wrote, (guint)g_bytes_get_size(blob)); return FALSE; } /* save */ if (event != NULL) fu_byte_array_append_bytes(buf_tagged, blob); /* progress */ fu_progress_step_done(progress); } /* save response */ if (event != NULL) fu_device_event_set_data(event, "Data", buf_tagged->data, buf_tagged->len); /* success */ return TRUE; } /** * fu_device_set_contents_bytes: * @self: a #FuDevice * @filename: full path to a file * @blob: data to write * @progress: (nullable): optional #FuProgress * @error: (nullable): optional return location for an error * * Writes @blob to @filename, emulating if required. * * Returns: %TRUE on success * * Since: 2.0.2 **/ gboolean fu_device_set_contents_bytes(FuDevice *self, const gchar *filename, GBytes *blob, FuProgress *progress, GError **error) { g_autoptr(GInputStream) stream = NULL; g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(filename != NULL, FALSE); g_return_val_if_fail(blob != NULL, FALSE); g_return_val_if_fail(progress == NULL || FU_IS_PROGRESS(progress), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); stream = g_memory_input_stream_new_from_bytes(blob); return fu_device_set_contents(self, filename, stream, progress, error); } /** * fu_device_get_contents_bytes: * @self: a #FuDevice * @filename: full path to a file * @progress: (nullable): optional #FuProgress * @error: (nullable): optional return location for an error * * Writes @blob to @filename, emulating if required. * * Returns: (transfer full): a #GBytes, or %NULL on error * * Since: 2.0.2 **/ GBytes * fu_device_get_contents_bytes(FuDevice *self, const gchar *filename, FuProgress *progress, GError **error) { FuDeviceEvent *event = NULL; g_autofree gchar *event_id = NULL; g_autoptr(GBytes) blob = NULL; g_autoptr(GInputStream) istr = NULL; g_return_val_if_fail(FU_IS_DEVICE(self), NULL); g_return_val_if_fail(filename != NULL, NULL); g_return_val_if_fail(progress == NULL || FU_IS_PROGRESS(progress), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* need event ID */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_strdup_printf("GetContents:Filename=%s", filename); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return NULL; return fu_device_event_get_bytes(event, "Data", error); } /* save */ if (event_id != NULL) event = fu_device_save_event(FU_DEVICE(self), event_id); /* open for reading */ istr = fu_input_stream_from_path(filename, error); if (istr == NULL) return NULL; blob = fu_input_stream_read_bytes(istr, 0, G_MAXSIZE, progress, error); if (blob == NULL) return NULL; /* save response */ if (event != NULL) fu_device_event_set_bytes(event, "Data", blob); /* success */ return g_steal_pointer(&blob); } /** * fu_device_get_smbios_string: * @self: a #FuDevice * @type: a SMBIOS structure type, e.g. %FU_SMBIOS_STRUCTURE_TYPE_BIOS * @length: expected length of the structure, or %FU_SMBIOS_STRUCTURE_LENGTH_ANY * @offset: a SMBIOS offset * @error: (nullable): optional return location for an error * * Gets a hardware SMBIOS string. * * The @type and @offset can be referenced from the DMTF SMBIOS specification: * https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.1.1.pdf * * Returns: a string, or %NULL * * Since: 2.0.10 **/ const gchar * fu_device_get_smbios_string(FuDevice *self, guint8 type, guint8 length, guint8 offset, GError **error) { FuDevicePrivate *priv = GET_PRIVATE(self); FuDeviceEvent *event = NULL; const gchar *str; g_autofree gchar *event_id = NULL; g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_DEVICE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* need event ID */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_strdup_printf("GetSmbiosString:" "Type=0x%02x," "Length=0x%02x," "Offset=0x%02x", type, length, offset); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return NULL; if (!fu_device_event_check_error(event, error)) return NULL; return fu_device_event_get_str(event, "Data", error); } /* save */ if (event_id != NULL) event = fu_device_save_event(FU_DEVICE(self), event_id); /* use context */ str = fu_context_get_smbios_string(priv->ctx, type, length, offset, &error_local); if (str == NULL) { if (event != NULL) fu_device_event_set_error(event, error_local); g_propagate_error(error, g_steal_pointer(&error_local)); return NULL; } /* save response */ if (event != NULL) fu_device_event_set_str(event, "Data", str); /* success */ return str; } /** * fu_device_query_file_exists: * @self: a #FuDevice * @filename: filename * @exists: (out): if @filename exists * @error: (nullable): optional return location for an error * * Checks if a file exists. * * Returns: %TRUE for success * * Since: 2.0.2 **/ gboolean fu_device_query_file_exists(FuDevice *self, const gchar *filename, gboolean *exists, GError **error) { FuDeviceEvent *event = NULL; gint64 value; g_autofree gchar *event_id = NULL; g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(filename != NULL, FALSE); g_return_val_if_fail(exists != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_strdup_printf("FileExists:Filename=%s", filename); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return FALSE; value = fu_device_event_get_i64(event, "Exists", error); if (value == G_MAXINT64) return FALSE; *exists = value == 1; return TRUE; } /* save */ if (event_id != NULL) event = fu_device_save_event(FU_DEVICE(self), event_id); /* check, and save result */ *exists = g_file_test(filename, G_FILE_TEST_EXISTS); if (event != NULL) fu_device_event_set_i64(event, "Exists", *exists ? 1 : 0); /* success */ return TRUE; } static gboolean fu_device_poll_locker_open_cb(GObject *device, GError **error) { FuDevice *self = FU_DEVICE(device); FuDevicePrivate *priv = GET_PRIVATE(self); g_atomic_int_inc(&priv->poll_locker_cnt); return TRUE; } static gboolean fu_device_poll_locker_close_cb(GObject *device, GError **error) { FuDevice *self = FU_DEVICE(device); FuDevicePrivate *priv = GET_PRIVATE(self); g_atomic_int_dec_and_test(&priv->poll_locker_cnt); return TRUE; } /** * fu_device_poll_locker_new: * @self: a #FuDevice * @error: (nullable): optional return location for an error * * Returns a device locker that prevents polling on the device. If there are no open poll lockers * then the poll callback will be called. * * Use %FU_DEVICE_PRIVATE_FLAG_AUTO_PAUSE_POLLING to opt into this functionality. * * Returns: (transfer full): a #FuDeviceLocker * * Since: 1.8.1 **/ FuDeviceLocker * fu_device_poll_locker_new(FuDevice *self, GError **error) { g_return_val_if_fail(FU_IS_DEVICE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return fu_device_locker_new_full(self, fu_device_poll_locker_open_cb, fu_device_poll_locker_close_cb, error); } /** * fu_device_poll: * @self: a #FuDevice * @error: (nullable): optional return location for an error * * Polls a device, typically querying the hardware for status. * * Returns: %TRUE for success * * Since: 1.1.2 **/ gboolean fu_device_poll(FuDevice *self, GError **error) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* subclassed */ if (device_class->poll != NULL) { if (!device_class->poll(self, error)) return FALSE; } return TRUE; } static gboolean fu_device_poll_cb(gpointer user_data) { FuDevice *self = FU_DEVICE(user_data); FuDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(GError) error_local = NULL; /* device is being detached, written, read, or attached */ if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_AUTO_PAUSE_POLLING) && priv->poll_locker_cnt > 0) { g_debug("ignoring poll callback as an action is in progress"); return G_SOURCE_CONTINUE; } if (!fu_device_poll(self, &error_local)) { g_warning("disabling polling: %s", error_local->message); priv->poll_id = 0; return G_SOURCE_REMOVE; } return G_SOURCE_CONTINUE; } /** * fu_device_set_poll_interval: * @self: a #FuPlugin * @interval: duration in ms, or 0 to disable * * Polls the hardware every interval period. If the subclassed `->poll()` method * returns %FALSE then a warning is printed to the console and the poll is * disabled until the next call to fu_device_set_poll_interval(). * * Since: 1.1.2 **/ void fu_device_set_poll_interval(FuDevice *self, guint interval) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); if (priv->poll_id != 0) { g_source_remove(priv->poll_id); priv->poll_id = 0; } if (interval == 0) return; if (interval % 1000 == 0) { priv->poll_id = g_timeout_add_seconds(interval / 1000, fu_device_poll_cb, self); } else { priv->poll_id = g_timeout_add(interval, fu_device_poll_cb, self); } } /** * fu_device_get_order: * @self: a #FuPlugin * * Gets the device order, where higher numbers are installed after lower * numbers. * * Returns: the integer value * * Since: 1.0.8 **/ gint fu_device_get_order(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), 0); return priv->order; } /** * fu_device_set_order: * @self: a #FuDevice * @order: an integer value * * Sets the device order, where higher numbers are installed after lower * numbers. * * Since: 1.0.8 **/ void fu_device_set_order(FuDevice *self, gint order) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); priv->order = order; } /** * fu_device_get_priority: * @self: a #FuPlugin * * Gets the device priority, where higher numbers are better. * * Returns: the integer value * * Since: 1.1.1 **/ guint fu_device_get_priority(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), 0); return priv->priority; } /** * fu_device_set_priority: * @self: a #FuDevice * @priority: an integer value * * Sets the device priority, where higher numbers are better. * * Since: 1.1.1 **/ void fu_device_set_priority(FuDevice *self, guint priority) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); priv->priority = priority; } /** * fu_device_get_equivalent_id: * @self: a #FuDevice * * Gets any equivalent ID for a device * * Returns: (transfer none): a #gchar or NULL * * Since: 0.6.1 **/ const gchar * fu_device_get_equivalent_id(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), NULL); return priv->equivalent_id; } /** * fu_device_set_equivalent_id: * @self: a #FuDevice * @equivalent_id: (nullable): a string * * Sets any equivalent ID for a device * * Since: 0.6.1 **/ void fu_device_set_equivalent_id(FuDevice *self, const gchar *equivalent_id) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); /* not changed */ if (g_strcmp0(priv->equivalent_id, equivalent_id) == 0) return; /* sanity check */ if (!fwupd_device_id_is_valid(equivalent_id)) { g_critical("%s is not a valid device ID", equivalent_id); return; } if (g_strcmp0(equivalent_id, fu_device_get_id(self)) == 0) { g_critical("%s is the same as this device ID", equivalent_id); return; } g_free(priv->equivalent_id); priv->equivalent_id = g_strdup(equivalent_id); g_object_notify(G_OBJECT(self), "equivalent-id"); } /** * fu_device_get_parent: * @self: a #FuDevice * * Gets any parent device. An parent device is logically "above" the current * device and this may be reflected in client tools. * * This information also allows the plugin to optionally verify the parent * device, for instance checking the parent device firmware version. * * The parent object is not refcounted and if destroyed this function will then * return %NULL. * * Returns: (transfer none): a device or %NULL * * Since: 1.0.8 **/ FuDevice * fu_device_get_parent(FuDevice *self) { g_return_val_if_fail(FU_IS_DEVICE(self), NULL); return FU_DEVICE(fwupd_device_get_parent(FWUPD_DEVICE(self))); } /** * fu_device_get_root: * @self: a #FuDevice * * Gets the root parent device. A parent device is logically "above" the current * device and this may be reflected in client tools. * * If there is no parent device defined, then @self is returned. * * Returns: (transfer full): a device * * Since: 1.4.0 **/ FuDevice * fu_device_get_root(FuDevice *self) { FuDevice *parent; g_return_val_if_fail(FU_IS_DEVICE(self), NULL); do { parent = fu_device_get_parent(self); if (parent != NULL) self = parent; } while (parent != NULL); return g_object_ref(self); } static void fu_device_set_composite_id(FuDevice *self, const gchar *composite_id) { GPtrArray *children; /* subclassed simple setter */ fwupd_device_set_composite_id(FWUPD_DEVICE(self), composite_id); /* all children */ children = fu_device_get_children(self); for (guint i = 0; i < children->len; i++) { FuDevice *child_tmp = g_ptr_array_index(children, i); fu_device_set_composite_id(child_tmp, composite_id); } } /** * fu_device_set_parent: * @self: a #FuDevice * @parent: (nullable): a device * * Sets any parent device. An parent device is logically "above" the current * device and this may be reflected in client tools. * * This information also allows the plugin to optionally verify the parent * device, for instance checking the parent device firmware version. * * Since: 1.0.8 **/ void fu_device_set_parent(FuDevice *self, FuDevice *parent) { g_return_if_fail(FU_IS_DEVICE(self)); /* debug */ if (parent != NULL) { g_info("setting parent of %s [%s] to be %s [%s]", fu_device_get_name(self), fu_device_get_id(self), fu_device_get_name(parent), fu_device_get_id(parent)); } /* set the composite ID on the children and grandchildren */ if (parent != NULL) fu_device_set_composite_id(self, fu_device_get_composite_id(parent)); /* if the parent has a context, make the child inherit it */ if (parent != NULL) { if (fu_device_get_context(self) == NULL && fu_device_get_context(parent) != NULL) fu_device_set_context(self, fu_device_get_context(parent)); } fwupd_device_set_parent(FWUPD_DEVICE(self), FWUPD_DEVICE(parent)); g_object_notify(G_OBJECT(self), "parent"); } /* if the proxy sets this flag copy it to the logical device */ static void fu_device_incorporate_from_proxy_flags(FuDevice *self, FuDevice *proxy) { const FwupdDeviceFlags flags[] = {FWUPD_DEVICE_FLAG_EMULATED, FWUPD_DEVICE_FLAG_UNREACHABLE}; for (guint i = 0; i < G_N_ELEMENTS(flags); i++) { if (fu_device_has_flag(proxy, flags[i])) { g_debug("propagating %s from proxy", fwupd_device_flag_to_string(flags[i])); fu_device_add_flag(self, flags[i]); } } } static void fu_device_proxy_flags_notify_cb(FuDevice *proxy, GParamSpec *pspec, gpointer user_data) { FuDevice *self = FU_DEVICE(user_data); fu_device_incorporate_from_proxy_flags(self, proxy); } /** * fu_device_set_proxy: * @self: a #FuDevice * @proxy: a device * * Sets any proxy device. A proxy device can be used to perform an action on * behalf of another device, for instance attach()ing it after a successful * update. * * If the %FU_DEVICE_PRIVATE_FLAG_REFCOUNTED_PROXY flag is present on the device then a *strong* * reference is used, otherwise the proxy will be cleared if @proxy is destroyed. * * Since: 1.4.1 **/ void fu_device_set_proxy(FuDevice *self, FuDevice *proxy) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); /* unchanged */ if (proxy == priv->proxy) return; /* disconnect from old proxy */ if (priv->proxy != NULL && priv->notify_flags_proxy_id != 0) { g_signal_handler_disconnect(priv->proxy, priv->notify_flags_proxy_id); priv->notify_flags_proxy_id = 0; } /* copy from proxy */ if (proxy != NULL) { fu_device_incorporate(self, proxy, FU_DEVICE_INCORPORATE_FLAG_PHYSICAL_ID); priv->notify_flags_proxy_id = g_signal_connect(FWUPD_DEVICE(proxy), "notify::flags", G_CALLBACK(fu_device_proxy_flags_notify_cb), self); fu_device_incorporate_from_proxy_flags(self, proxy); } /* sometimes strong, sometimes weak */ if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_REFCOUNTED_PROXY)) { g_set_object(&priv->proxy, proxy); fu_device_set_target(self, proxy); } else { if (priv->proxy != NULL) g_object_remove_weak_pointer(G_OBJECT(priv->proxy), (gpointer *)&priv->proxy); if (proxy != NULL) g_object_add_weak_pointer(G_OBJECT(proxy), (gpointer *)&priv->proxy); priv->proxy = proxy; } g_object_notify(G_OBJECT(self), "proxy"); } /** * fu_device_get_proxy: * @self: a #FuDevice * * Gets any proxy device. A proxy device can be used to perform an action on * behalf of another device, for instance attach()ing it after a successful * update. * * Unless %FU_DEVICE_PRIVATE_FLAG_REFCOUNTED_PROXY is set, the proxy object is not refcounted and * if destroyed this function will then return %NULL. * * Returns: (transfer none): a device or %NULL * * Since: 1.4.1 **/ FuDevice * fu_device_get_proxy(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), NULL); return priv->proxy; } /** * fu_device_get_proxy_with_fallback: * @self: a #FuDevice * * Gets the proxy device if %FU_DEVICE_PRIVATE_FLAG_USE_PROXY_FALLBACK is set, falling back to the * device itself. * * Returns: (transfer none): a device * * Since: 1.6.2 **/ FuDevice * fu_device_get_proxy_with_fallback(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), NULL); if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_USE_PROXY_FALLBACK) && priv->proxy != NULL) return priv->proxy; return self; } /** * fu_device_get_children: * @self: a #FuDevice * * Gets any child devices. A child device is logically "below" the current * device and this may be reflected in client tools. * * Returns: (transfer none) (element-type FuDevice): child devices * * Since: 1.0.8 **/ GPtrArray * fu_device_get_children(FuDevice *self) { g_return_val_if_fail(FU_IS_DEVICE(self), NULL); return fwupd_device_get_children(FWUPD_DEVICE(self)); } /** * fu_device_add_child: * @self: a #FuDevice * @child: Another #FuDevice * * Sets any child device. An child device is logically linked to the primary * device in some way. * * Since: 1.0.8 **/ void fu_device_add_child(FuDevice *self, FuDevice *child) { FuDevicePrivate *priv = GET_PRIVATE(self); GPtrArray *children; g_autoptr(GError) error = NULL; g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(FU_IS_DEVICE(child)); /* if parent is emulated, child must be too */ if (fu_device_has_flag(self, FWUPD_DEVICE_FLAG_EMULATED)) fu_device_add_flag(child, FWUPD_DEVICE_FLAG_EMULATED); /* make tests easier */ fu_device_convert_instance_ids(child); /* add if the child does not already exist */ fwupd_device_add_child(FWUPD_DEVICE(self), FWUPD_DEVICE(child)); /* propagate inhibits to children */ if (priv->inhibits != NULL && fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_INHIBIT_CHILDREN)) { g_autoptr(GList) values = g_hash_table_get_values(priv->inhibits); for (GList *l = values; l != NULL; l = l->next) { FuDeviceInhibit *inhibit = (FuDeviceInhibit *)l->data; fu_device_inhibit_full(child, inhibit->problem, inhibit->inhibit_id, inhibit->reason); } } /* ensure the parent has the MAX() of the children's removal delay */ children = fu_device_get_children(self); for (guint i = 0; i < children->len; i++) { FuDevice *child_tmp = g_ptr_array_index(children, i); guint remove_delay = fu_device_get_remove_delay(child_tmp); if (remove_delay > priv->remove_delay) { g_debug("setting remove delay to %ums as child is greater than %ums", remove_delay, priv->remove_delay); priv->remove_delay = remove_delay; } } /* ensure the parent has the MAX() of the children's acquiesce delay */ children = fu_device_get_children(self); for (guint i = 0; i < children->len; i++) { FuDevice *child_tmp = g_ptr_array_index(children, i); guint acquiesce_delay = fu_device_get_acquiesce_delay(child_tmp); if (acquiesce_delay > priv->acquiesce_delay) { g_debug("setting acquiesce delay to %ums as child is greater than %ums", acquiesce_delay, priv->acquiesce_delay); priv->acquiesce_delay = acquiesce_delay; } } /* ensure child has the parent acquiesce delay */ for (guint i = 0; i < children->len; i++) { FuDevice *child_tmp = g_ptr_array_index(children, i); fu_device_set_acquiesce_delay(child_tmp, priv->acquiesce_delay); } /* copy from main device if unset */ fu_device_incorporate( child, self, FU_DEVICE_INCORPORATE_FLAG_PHYSICAL_ID | FU_DEVICE_INCORPORATE_FLAG_BACKEND_ID | FU_DEVICE_INCORPORATE_FLAG_REMOVE_DELAY | FU_DEVICE_INCORPORATE_FLAG_ACQUIESCE_DELAY | FU_DEVICE_INCORPORATE_FLAG_VENDOR_IDS | FU_DEVICE_INCORPORATE_FLAG_ICONS | FU_DEVICE_INCORPORATE_FLAG_VENDOR); /* ensure the ID is converted */ if (!fu_device_ensure_id(child, &error)) g_warning("failed to ensure child: %s", error->message); /* ensure the parent is also set on the child */ fu_device_set_parent(child, self); /* signal to the plugin in case this is done after setup */ g_signal_emit(self, signals[SIGNAL_CHILD_ADDED], 0, child); } /** * fu_device_remove_child: * @self: a #FuDevice * @child: Another #FuDevice * * Removes child device. * * Since: 1.6.2 **/ void fu_device_remove_child(FuDevice *self, FuDevice *child) { g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(FU_IS_DEVICE(child)); /* proxy */ fwupd_device_remove_child(FWUPD_DEVICE(self), FWUPD_DEVICE(child)); /* signal to the plugin */ g_signal_emit(self, signals[SIGNAL_CHILD_REMOVED], 0, child); } /** * fu_device_remove_children: * @self: a #FuDevice * * Removes all child devices. * * Since: 2.0.0 **/ void fu_device_remove_children(FuDevice *self) { GPtrArray *children; g_return_if_fail(FU_IS_DEVICE(self)); /* proxy */ fwupd_device_remove_children(FWUPD_DEVICE(self)); /* signal to the plugin */ children = fu_device_get_children(self); for (guint i = 0; i < children->len; i++) { FuDevice *child = g_ptr_array_index(children, i); g_signal_emit(self, signals[SIGNAL_CHILD_REMOVED], 0, child); } } static void fu_device_ensure_parent_guids(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); if (priv->parent_guids != NULL) return; priv->parent_guids = g_ptr_array_new_with_free_func(g_free); } /** * fu_device_get_parent_guids: * @self: a #FuDevice * * Gets any parent device GUIDs. If a device is added to the daemon that matches * any GUIDs added from fu_device_add_parent_guid() then this device is marked the parent of @self. * * Returns: (transfer none) (element-type utf8): a list of GUIDs * * Since: 1.0.8 **/ GPtrArray * fu_device_get_parent_guids(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), NULL); fu_device_ensure_parent_guids(self); return priv->parent_guids; } /** * fu_device_has_parent_guid: * @self: a #FuDevice * @guid: a GUID * * Searches the list of parent GUIDs for a string match. * * Returns: %TRUE if the parent GUID exists * * Since: 1.0.8 **/ gboolean fu_device_has_parent_guid(FuDevice *self, const gchar *guid) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(guid != NULL, FALSE); if (priv->parent_guids == NULL) return FALSE; for (guint i = 0; i < priv->parent_guids->len; i++) { const gchar *guid_tmp = g_ptr_array_index(priv->parent_guids, i); if (g_strcmp0(guid_tmp, guid) == 0) return TRUE; } return FALSE; } /** * fu_device_add_parent_guid: * @self: a #FuDevice * @guid: a GUID * * Sets any parent device using a GUID. An parent device is logically linked to * the primary device in some way and can be added before or after @self. * * The GUIDs are searched in order, and so the order of adding GUIDs may be * important if more than one parent device might match. * * If the parent device is removed, any children logically linked to it will * also be removed. * * Since: 1.0.8 **/ void fu_device_add_parent_guid(FuDevice *self, const gchar *guid) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(guid != NULL); /* ensure */ fu_device_ensure_parent_guids(self); /* make valid */ if (!fwupd_guid_is_valid(guid)) { g_autofree gchar *tmp = fwupd_guid_hash_string(guid); if (fu_device_has_parent_guid(self, tmp)) return; g_debug("using %s for %s", tmp, guid); g_ptr_array_add(priv->parent_guids, g_steal_pointer(&tmp)); return; } /* already valid */ if (fu_device_has_parent_guid(self, guid)) return; g_ptr_array_add(priv->parent_guids, g_strdup(guid)); } /** * fu_device_get_parent_physical_ids: * @self: a #FuDevice * * Gets any parent device IDs. If a device is added to the daemon that matches * the physical ID added from fu_device_add_parent_physical_id() then this * device is marked the parent of @self. * * Returns: (transfer none) (element-type utf8) (nullable): a list of IDs * * Since: 1.6.2 **/ GPtrArray * fu_device_get_parent_physical_ids(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), NULL); return priv->parent_physical_ids; } /** * fu_device_has_parent_physical_id: * @self: a #FuDevice * @physical_id: a device physical ID * * Searches the list of parent IDs for a string match. * * Returns: %TRUE if the parent ID exists * * Since: 1.6.2 **/ gboolean fu_device_has_parent_physical_id(FuDevice *self, const gchar *physical_id) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(physical_id != NULL, FALSE); if (priv->parent_physical_ids == NULL) return FALSE; for (guint i = 0; i < priv->parent_physical_ids->len; i++) { const gchar *tmp = g_ptr_array_index(priv->parent_physical_ids, i); if (g_strcmp0(tmp, physical_id) == 0) return TRUE; } return FALSE; } /** * fu_device_add_parent_physical_id: * @self: a #FuDevice * @physical_id: a device physical ID * * Sets any parent device using the physical ID. An parent device is logically * linked to the primary device in some way and can be added before or after @self. * * The IDs are searched in order, and so the order of adding IDs may be * important if more than one parent device might match. * * Since: 1.6.2 **/ void fu_device_add_parent_physical_id(FuDevice *self, const gchar *physical_id) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(physical_id != NULL); /* ensure exists */ if (priv->parent_physical_ids == NULL) priv->parent_physical_ids = g_ptr_array_new_with_free_func(g_free); /* already present */ if (fu_device_has_parent_physical_id(self, physical_id)) return; g_ptr_array_add(priv->parent_physical_ids, g_strdup(physical_id)); } /** * fu_device_get_parent_backend_ids: * @self: a #FuDevice * * Gets any parent device IDs. If a device is added to the daemon that matches * the physical ID added from fu_device_add_parent_backend_id() then this * device is marked the parent of @self. * * Returns: (transfer none) (element-type utf8) (nullable): a list of IDs * * Since: 1.9.7 **/ GPtrArray * fu_device_get_parent_backend_ids(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), NULL); return priv->parent_backend_ids; } /** * fu_device_has_parent_backend_id: * @self: a #FuDevice * @backend_id: a device physical ID * * Searches the list of parent IDs for a string match. * * Returns: %TRUE if the parent ID exists * * Since: 1.9.7 **/ gboolean fu_device_has_parent_backend_id(FuDevice *self, const gchar *backend_id) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(backend_id != NULL, FALSE); if (priv->parent_backend_ids == NULL) return FALSE; for (guint i = 0; i < priv->parent_backend_ids->len; i++) { const gchar *tmp = g_ptr_array_index(priv->parent_backend_ids, i); if (g_strcmp0(tmp, backend_id) == 0) return TRUE; } return FALSE; } /** * fu_device_add_parent_backend_id: * @self: a #FuDevice * @backend_id: a device physical ID * * Sets any parent device using the physical ID. An parent device is logically * linked to the primary device in some way and can be added before or after @self. * * The IDs are searched in order, and so the order of adding IDs may be * important if more than one parent device might match. * * Since: 1.9.7 **/ void fu_device_add_parent_backend_id(FuDevice *self, const gchar *backend_id) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(backend_id != NULL); /* ensure exists */ if (priv->parent_backend_ids == NULL) priv->parent_backend_ids = g_ptr_array_new_with_free_func(g_free); /* already present */ if (fu_device_has_parent_backend_id(self, backend_id)) return; g_ptr_array_add(priv->parent_backend_ids, g_strdup(backend_id)); } static gboolean fu_device_add_child_by_type_guid(FuDevice *self, GType type, const gchar *guid, GError **error) { FuDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(FuDevice) child = NULL; child = g_object_new(type, "context", priv->ctx, "logical-id", guid, NULL); fu_device_add_instance_id(child, guid); fu_device_incorporate(child, self, FU_DEVICE_INCORPORATE_FLAG_PHYSICAL_ID); if (!fu_device_ensure_id(self, error)) return FALSE; if (!fu_device_probe(child, error)) return FALSE; fu_device_convert_instance_ids(child); fu_device_add_child(self, child); return TRUE; } static gboolean fu_device_add_child_by_kv(FuDevice *self, const gchar *str, GError **error) { g_auto(GStrv) split = g_strsplit(str, "|", -1); /* type same as parent */ if (g_strv_length(split) == 1) { return fu_device_add_child_by_type_guid(self, G_OBJECT_TYPE(self), split[1], error); } /* type specified */ if (g_strv_length(split) == 2) { GType devtype = g_type_from_name(split[0]); if (devtype == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no GType registered"); return FALSE; } return fu_device_add_child_by_type_guid(self, devtype, split[1], error); } /* more than one '|' */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "unable to add parse child section"); return FALSE; } static gboolean fu_device_set_quirk_inhibit_section(FuDevice *self, const gchar *value, GError **error) { g_auto(GStrv) sections = NULL; g_return_val_if_fail(value != NULL, FALSE); /* sanity check */ sections = g_strsplit(value, ":", -1); if (g_strv_length(sections) != 2) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported, expected k1:v1[,k2:v2][,k3:]"); return FALSE; } /* allow empty string to unset quirk */ if (g_strcmp0(sections[1], "") != 0) fu_device_inhibit(self, sections[0], sections[1]); else fu_device_uninhibit(self, sections[0]); /* success */ return TRUE; } /** * fu_device_set_quirk_kv: * @self: a #FuDevice * @key: a string key * @value: a string value * @source: a #FuContextQuirkSource, e.g. %FU_CONTEXT_QUIRK_SOURCE_DB * @error: (nullable): optional return location for an error * * Sets a specific quirk on the device. * * Returns: %TRUE on success * * Since: 2.0.2 **/ gboolean fu_device_set_quirk_kv(FuDevice *self, const gchar *key, const gchar *value, FuContextQuirkSource source, GError **error) { FuDevicePrivate *priv = GET_PRIVATE(self); FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); guint64 tmp; g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(key != NULL, FALSE); g_return_val_if_fail(value != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (g_strcmp0(key, FU_QUIRKS_PLUGIN) == 0) { g_auto(GStrv) sections = g_strsplit(value, ",", -1); for (guint i = 0; sections[i] != NULL; i++) fu_device_add_possible_plugin(self, sections[i]); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_FLAGS) == 0) { fu_device_set_custom_flags(self, value); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_NAME) == 0) { if (fu_device_get_name(self) != NULL && source >= FU_CONTEXT_QUIRK_SOURCE_DB) return TRUE; fu_device_set_name(self, value); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_SUMMARY) == 0) { fu_device_set_summary(self, value); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_BRANCH) == 0) { fu_device_set_branch(self, value); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_VENDOR) == 0) { if (fu_device_get_vendor(self) != NULL && source >= FU_CONTEXT_QUIRK_SOURCE_DB) return TRUE; fu_device_set_vendor(self, value); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_VENDOR_ID) == 0) { g_auto(GStrv) sections = g_strsplit(value, ",", -1); for (guint i = 0; sections[i] != NULL; i++) fu_device_add_vendor_id(self, sections[i]); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_PROTOCOL) == 0) { g_auto(GStrv) sections = g_strsplit(value, ",", -1); for (guint i = 0; sections[i] != NULL; i++) fu_device_add_protocol(self, sections[i]); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_ISSUE) == 0) { g_auto(GStrv) sections = g_strsplit(value, ",", -1); for (guint i = 0; sections[i] != NULL; i++) fu_device_add_issue(self, sections[i]); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_VERSION) == 0) { fu_device_set_version(self, value); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_UPDATE_MESSAGE) == 0) { fu_device_set_update_message(self, value); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_UPDATE_IMAGE) == 0) { fu_device_set_update_image(self, value); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_ICON) == 0) { g_auto(GStrv) sections = g_strsplit(value, ",", -1); if (fu_device_get_icons(self)->len > 0 && source >= FU_CONTEXT_QUIRK_SOURCE_FALLBACK) return TRUE; for (guint i = 0; sections[i] != NULL; i++) fu_device_add_icon(self, sections[i]); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_GUID) == 0) { g_auto(GStrv) sections = g_strsplit(value, ",", -1); for (guint i = 0; sections[i] != NULL; i++) { fu_device_add_instance_id_full(self, sections[i], FU_DEVICE_INSTANCE_FLAG_VISIBLE | FU_DEVICE_INSTANCE_FLAG_QUIRKS); } return TRUE; } if (g_strcmp0(key, FU_QUIRKS_GUID_QUIRK) == 0) { g_auto(GStrv) sections = g_strsplit(value, ",", -1); for (guint i = 0; sections[i] != NULL; i++) { fu_device_add_instance_id_full(self, sections[i], FU_DEVICE_INSTANCE_FLAG_QUIRKS); } return TRUE; } if (g_strcmp0(key, FU_QUIRKS_COUNTERPART_GUID) == 0) { g_auto(GStrv) sections = g_strsplit(value, ",", -1); for (guint i = 0; sections[i] != NULL; i++) { fu_device_add_instance_id_full(self, sections[i], FU_DEVICE_INSTANCE_FLAG_COUNTERPART); } return TRUE; } if (g_strcmp0(key, FU_QUIRKS_PARENT_GUID) == 0) { g_auto(GStrv) sections = g_strsplit(value, ",", -1); for (guint i = 0; sections[i] != NULL; i++) fu_device_add_parent_guid(self, sections[i]); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_PROXY_GUID) == 0) { fu_device_set_proxy_guid(self, value); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_FIRMWARE_SIZE_MIN) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_device_set_firmware_size_min(self, tmp); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_FIRMWARE_SIZE_MAX) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_device_set_firmware_size_max(self, tmp); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_FIRMWARE_SIZE) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_device_set_firmware_size(self, tmp); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_INSTALL_DURATION) == 0) { if (!fu_strtoull(value, &tmp, 0, 60 * 60 * 24, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_device_set_install_duration(self, tmp); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_PRIORITY) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_device_set_priority(self, tmp); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_BATTERY_THRESHOLD) == 0) { if (!fu_strtoull(value, &tmp, 0, 100, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_device_set_battery_threshold(self, tmp); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_REMOVE_DELAY) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_device_set_remove_delay(self, tmp); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_ACQUIESCE_DELAY) == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_device_set_acquiesce_delay(self, tmp); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_VERSION_FORMAT) == 0) { fu_device_set_version_format(self, fwupd_version_format_from_string(value)); return TRUE; } if (g_strcmp0(key, FU_QUIRKS_INHIBIT) == 0) { g_auto(GStrv) sections = g_strsplit(value, ",", -1); for (guint i = 0; sections[i] != NULL; i++) { if (!fu_device_set_quirk_inhibit_section(self, sections[i], error)) return FALSE; } return TRUE; } if (g_strcmp0(key, FU_QUIRKS_GTYPE) == 0) { if (priv->specialized_gtype != G_TYPE_INVALID) { g_debug("already set GType to %s, ignoring %s", g_type_name(priv->specialized_gtype), value); return TRUE; } priv->specialized_gtype = g_type_from_name(value); if (priv->specialized_gtype == G_TYPE_INVALID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unknown GType name %s", value); return FALSE; } return TRUE; } if (g_strcmp0(key, FU_QUIRKS_PROXY_GTYPE) == 0) { if (priv->proxy_gtype != G_TYPE_INVALID) { g_debug("already set proxy GType to %s, ignoring %s", g_type_name(priv->proxy_gtype), value); return TRUE; } priv->proxy_gtype = g_type_from_name(value); if (priv->proxy_gtype == G_TYPE_INVALID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unknown GType name %s", value); return FALSE; } return TRUE; } if (g_strcmp0(key, FU_QUIRKS_FIRMWARE_GTYPE) == 0) { if (priv->firmware_gtype != G_TYPE_INVALID) { g_debug("already set firmware GType to %s, ignoring %s", g_type_name(priv->firmware_gtype), value); return TRUE; } priv->firmware_gtype = g_type_from_name(value); if (priv->firmware_gtype == G_TYPE_INVALID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unknown GType name %s", value); return FALSE; } return TRUE; } if (g_strcmp0(key, FU_QUIRKS_CHILDREN) == 0) { g_auto(GStrv) sections = g_strsplit(value, ",", -1); for (guint i = 0; sections[i] != NULL; i++) { if (!fu_device_add_child_by_kv(self, sections[i], error)) return FALSE; } return TRUE; } /* optional device-specific method */ if (device_class->set_quirk_kv != NULL) return device_class->set_quirk_kv(self, key, value, error); /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } /** * fu_device_get_specialized_gtype: * @self: a #FuDevice * * Gets the specialized type of the device * * Returns:#GType * * Since: 1.3.3 **/ GType fu_device_get_specialized_gtype(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); return priv->specialized_gtype; } /** * fu_device_set_specialized_gtype: * @self: a #FuDevice * @gtype: a #GType * * Sets the specialized type of the device * * Since: 2.0.0 **/ void fu_device_set_specialized_gtype(FuDevice *self, GType gtype) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(gtype != G_TYPE_INVALID); priv->specialized_gtype = gtype; } /** * fu_device_get_proxy_gtype: * @self: a #FuDevice * * Gets the specialized type of the device * * Returns:#GType * * Since: 1.9.15 **/ GType fu_device_get_proxy_gtype(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); return priv->proxy_gtype; } /** * fu_device_set_proxy_gtype: * @self: a #FuDevice * @gtype: a #GType * * Sets the specialized type of the device * * Since: 1.9.15 **/ void fu_device_set_proxy_gtype(FuDevice *self, GType gtype) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(gtype != G_TYPE_INVALID); priv->proxy_gtype = gtype; } /** * fu_device_get_firmware_gtype: * @self: a #FuDevice * * Gets the default firmware type for the device. * * Returns: #GType * * Since: 1.7.2 **/ GType fu_device_get_firmware_gtype(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), G_TYPE_INVALID); return priv->firmware_gtype; } /** * fu_device_set_firmware_gtype: * @self: a #FuDevice * @firmware_gtype: a #GType * * Sets the default firmware type for the device. * * Since: 1.7.2 **/ void fu_device_set_firmware_gtype(FuDevice *self, GType firmware_gtype) { FuDevicePrivate *priv = GET_PRIVATE(self); priv->firmware_gtype = firmware_gtype; } static void fu_device_quirks_iter_cb(FuContext *ctx, const gchar *key, const gchar *value, FuContextQuirkSource source, gpointer user_data) { FuDevice *self = FU_DEVICE(user_data); g_autoptr(GError) error = NULL; if (!fu_device_set_quirk_kv(self, key, value, source, &error)) { if (!g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) g_warning("failed to set quirk key %s=%s: %s", key, value, error->message); } } static void fu_device_add_guid_quirks(FuDevice *self, const gchar *guid) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(guid != NULL); if (priv->ctx == NULL) { g_autofree gchar *str = fu_device_to_string(self); g_critical("no FuContext assigned for %s", str); return; } /* run the query */ fu_context_lookup_quirk_by_id_iter(priv->ctx, guid, NULL, fu_device_quirks_iter_cb, self); } /** * fu_device_set_firmware_size: * @self: a #FuDevice * @size: Size in bytes * * Sets the exact allowed size of the firmware blob. * * Since: 1.2.6 **/ void fu_device_set_firmware_size(FuDevice *self, guint64 size) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); priv->size_min = size; priv->size_max = size; } /** * fu_device_set_firmware_size_min: * @self: a #FuDevice * @size_min: Size in bytes * * Sets the minimum allowed size of the firmware blob. * * Since: 1.1.2 **/ void fu_device_set_firmware_size_min(FuDevice *self, guint64 size_min) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); priv->size_min = size_min; } /** * fu_device_set_firmware_size_max: * @self: a #FuDevice * @size_max: Size in bytes * * Sets the maximum allowed size of the firmware blob. * * Since: 1.1.2 **/ void fu_device_set_firmware_size_max(FuDevice *self, guint64 size_max) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); priv->size_max = size_max; } /** * fu_device_get_firmware_size_min: * @self: a #FuDevice * * Gets the minimum size of the firmware blob. * * Returns: Size in bytes, or 0 if unset * * Since: 1.2.6 **/ guint64 fu_device_get_firmware_size_min(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), 0); return priv->size_min; } /** * fu_device_get_firmware_size_max: * @self: a #FuDevice * * Gets the maximum size of the firmware blob. * * Returns: Size in bytes, or 0 if unset * * Since: 1.2.6 **/ guint64 fu_device_get_firmware_size_max(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), 0); return priv->size_max; } /** * fu_device_has_guid: * @self: a #FuDevice * @guid: a GUID, e.g. `WacomAES` * * Finds out if the device has a specific GUID. * * Returns: %TRUE if the GUID is found * * Since: 1.2.2 **/ gboolean fu_device_has_guid(FuDevice *self, const gchar *guid) { g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(guid != NULL, FALSE); /* make valid */ if (!fwupd_guid_is_valid(guid)) { g_autofree gchar *tmp = fwupd_guid_hash_string(guid); return fwupd_device_has_guid(FWUPD_DEVICE(self), tmp); } /* already valid */ return fwupd_device_has_guid(FWUPD_DEVICE(self), guid); } static void fu_device_instance_id_free(FuDeviceInstanceIdItem *item) { g_free(item->instance_id); g_free(item->guid); g_free(item); } static FuDeviceInstanceIdItem * fu_device_get_instance_id(FuDevice *self, const gchar *instance_id) { FuDevicePrivate *priv = GET_PRIVATE(self); if (priv->instance_ids == NULL) return NULL; for (guint i = 0; i < priv->instance_ids->len; i++) { FuDeviceInstanceIdItem *item = g_ptr_array_index(priv->instance_ids, i); if (g_strcmp0(instance_id, item->instance_id) == 0) return item; if (g_strcmp0(instance_id, item->guid) == 0) return item; } return NULL; } /** * fu_device_has_instance_id: * @self: a #FuDevice * @instance_id: a instance ID, e.g. `WacomAES` * @flags: instance ID flags * * Finds out if the device has this specific instance ID. * * NOTE: The instance IDs are only added to the actual base #FwupdDevice after * fu_device_convert_instance_ids() has been called -- normally as part of `FuDevice->setup()`. * This ensures that incorporating a baseclass to a target device with the flag * %FU_DEVICE_PRIVATE_FLAG_NO_GENERIC_GUIDS does not have generic instance IDs added. * * Returns: %TRUE if the instance ID is found * * Since: 2.0.4 **/ gboolean fu_device_has_instance_id(FuDevice *self, const gchar *instance_id, FuDeviceInstanceFlag flags) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(instance_id != NULL, FALSE); for (guint i = 0; priv->instance_ids != NULL && i < priv->instance_ids->len; i++) { FuDeviceInstanceIdItem *item = g_ptr_array_index(priv->instance_ids, i); if ((item->flags & flags) == 0) continue; if (g_strcmp0(item->instance_id, instance_id) == 0 || g_strcmp0(item->guid, instance_id) == 0) return TRUE; } return FALSE; } /** * fu_device_add_instance_id_full: * @self: a #FuDevice * @instance_id: a instance ID or GUID, e.g. `WacomAES` * @flags: instance ID flags * * Adds an instance ID or GUID with all parameters set. * * A counterpart GUID is the GUID of the same device in bootloader or runtime mode, * if they have a different device PCI or USB ID. * * Since: 1.2.9 **/ void fu_device_add_instance_id_full(FuDevice *self, const gchar *instance_id, FuDeviceInstanceFlag flags) { FuDevicePrivate *priv = GET_PRIVATE(self); FuDeviceInstanceIdItem *item; g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(instance_id != NULL); /* some devices in recovery mode expect this to work */ if ((flags & FU_DEVICE_INSTANCE_FLAG_COUNTERPART) > 0 && fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_COUNTERPART_VISIBLE)) { g_debug("making %s also visible", instance_id); flags |= FU_DEVICE_INSTANCE_FLAG_VISIBLE; } /* add to cache */ item = fu_device_get_instance_id(self, instance_id); if (item != NULL) { if ((item->flags & FU_DEVICE_INSTANCE_FLAG_QUIRKS) == 0 && (flags & FU_DEVICE_INSTANCE_FLAG_QUIRKS) > 0) { /* visible -> visible+quirks */ fu_device_add_guid_quirks(self, item->guid); } item->flags |= flags; } else { item = g_new0(FuDeviceInstanceIdItem, 1); if (fwupd_guid_is_valid(instance_id)) { item->guid = g_strdup(instance_id); } else { item->instance_id = g_strdup(instance_id); item->guid = fwupd_guid_hash_string(instance_id); } item->flags |= flags; if (priv->instance_ids == NULL) priv->instance_ids = g_ptr_array_new_with_free_func( (GDestroyNotify)fu_device_instance_id_free); g_ptr_array_add(priv->instance_ids, item); /* we want the quirks to match so the plugin is set */ if (flags & FU_DEVICE_INSTANCE_FLAG_QUIRKS) fu_device_add_guid_quirks(self, item->guid); } /* already done by ->setup(), so this must be ->registered() */ if (priv->done_setup) { if (item->instance_id != NULL) fwupd_device_add_instance_id(FWUPD_DEVICE(self), item->instance_id); fwupd_device_add_guid(FWUPD_DEVICE(self), item->guid); } } /** * fu_device_add_instance_id: * @self: a #FuDevice * @instance_id: the instance ID, e.g. `PCI\VEN_10EC&DEV_525A` * * Adds an visible, quirked, instance ID to the device. * * Since: 1.2.5 **/ void fu_device_add_instance_id(FuDevice *self, const gchar *instance_id) { g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(instance_id != NULL); fu_device_add_instance_id_full(self, instance_id, FU_DEVICE_INSTANCE_FLAG_VISIBLE | FU_DEVICE_INSTANCE_FLAG_QUIRKS); } /** * fu_device_get_counterpart_guids: * @self: a #FuDevice * * Returns all the counterpart GUIDs. * * Returns: (transfer container) (element-type utf8): list of GUIDs * * Since: 1.9.21 **/ GPtrArray * fu_device_get_counterpart_guids(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(GPtrArray) guids = g_ptr_array_new_with_free_func(g_free); g_return_val_if_fail(FU_IS_DEVICE(self), NULL); for (guint i = 0; priv->instance_ids != NULL && i < priv->instance_ids->len; i++) { FuDeviceInstanceIdItem *item = g_ptr_array_index(priv->instance_ids, i); if (item->flags & FU_DEVICE_INSTANCE_FLAG_COUNTERPART) g_ptr_array_add(guids, g_strdup(item->guid)); } return g_steal_pointer(&guids); } /** * fu_device_get_metadata: * @self: a #FuDevice * @key: the key * * Gets an item of metadata from the device. * * Returns: a string value, or %NULL for unfound. * * Since: 0.1.0 **/ const gchar * fu_device_get_metadata(FuDevice *self, const gchar *key) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), NULL); g_return_val_if_fail(key != NULL, NULL); if (priv->metadata == NULL) return NULL; return g_hash_table_lookup(priv->metadata, key); } /** * fu_device_get_metadata_boolean: * @self: a #FuDevice * @key: the key * * Gets an item of metadata from the device. * * Returns: a boolean value, or %FALSE for unfound or failure to parse. * * Since: 0.9.7 **/ gboolean fu_device_get_metadata_boolean(FuDevice *self, const gchar *key) { FuDevicePrivate *priv = GET_PRIVATE(self); const gchar *tmp; g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(key != NULL, FALSE); if (priv->metadata == NULL) return FALSE; tmp = g_hash_table_lookup(priv->metadata, key); if (tmp == NULL) return FALSE; return g_strcmp0(tmp, "true") == 0; } /** * fu_device_get_metadata_integer: * @self: a #FuDevice * @key: the key * * Gets an item of metadata from the device. * * Returns: an integer value, or %G_MAXUINT for unfound or failure to parse. * * Since: 0.9.7 **/ guint fu_device_get_metadata_integer(FuDevice *self, const gchar *key) { FuDevicePrivate *priv = GET_PRIVATE(self); const gchar *tmp; guint64 val = 0; g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_DEVICE(self), G_MAXUINT); g_return_val_if_fail(key != NULL, G_MAXUINT); if (priv->metadata == NULL) return G_MAXUINT; tmp = g_hash_table_lookup(priv->metadata, key); if (tmp == NULL) return G_MAXUINT; if (!fu_strtoull(tmp, &val, 0, G_MAXUINT, FU_INTEGER_BASE_AUTO, &error_local)) { g_warning("could not convert %s to integer: %s", tmp, error_local->message); return G_MAXUINT; } return (guint)val; } /** * fu_device_remove_metadata: * @self: a #FuDevice * @key: the key * * Removes an item of metadata on the device. * * Since: 1.3.3 **/ void fu_device_remove_metadata(FuDevice *self, const gchar *key) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(key != NULL); if (priv->metadata == NULL) return; g_hash_table_remove(priv->metadata, key); } /** * fu_device_set_metadata: * @self: a #FuDevice * @key: the key * @value: the string value * * Sets an item of metadata on the device. * * Since: 0.1.0 **/ void fu_device_set_metadata(FuDevice *self, const gchar *key, const gchar *value) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(key != NULL); g_return_if_fail(value != NULL); if (priv->metadata == NULL) priv->metadata = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); g_hash_table_insert(priv->metadata, g_strdup(key), g_strdup(value)); } /** * fu_device_set_metadata_boolean: * @self: a #FuDevice * @key: the key * @value: the boolean value * * Sets an item of metadata on the device. When @value is set to %TRUE * the actual stored value is `true`. * * Since: 0.9.7 **/ void fu_device_set_metadata_boolean(FuDevice *self, const gchar *key, gboolean value) { g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(key != NULL); fu_device_set_metadata(self, key, value ? "true" : "false"); } /** * fu_device_set_metadata_integer: * @self: a #FuDevice * @key: the key * @value: the unsigned integer value * * Sets an item of metadata on the device. The integer is stored as a * base-10 string internally. * * Since: 0.9.7 **/ void fu_device_set_metadata_integer(FuDevice *self, const gchar *key, guint value) { g_autofree gchar *tmp = g_strdup_printf("%u", value); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(key != NULL); fu_device_set_metadata(self, key, tmp); } /* ensure the name does not have the vendor name as the prefix */ static void fu_device_fixup_vendor_name(FuDevice *self) { const gchar *name = fu_device_get_name(self); const gchar *vendor = fu_device_get_vendor(self); if (name != NULL && vendor != NULL) { g_autofree gchar *name_up = g_utf8_strup(name, -1); g_autofree gchar *vendor_up = g_utf8_strup(vendor, -1); if (g_strcmp0(name_up, vendor_up) == 0) { #ifndef SUPPORTED_BUILD g_warning("name and vendor are the same for %s [%s]", fu_device_get_name(self), fu_device_get_id(self)); #endif return; } if (g_str_has_prefix(name_up, vendor_up)) { gsize vendor_len = strlen(vendor); g_autofree gchar *name1 = g_strdup(name + vendor_len); g_autofree gchar *name2 = fu_strstrip(name1); g_debug("removing vendor prefix of '%s' from '%s'", vendor, name); fwupd_device_set_name(FWUPD_DEVICE(self), name2); } } } /** * fu_device_set_vendor: * @self: a #FuDevice * @vendor: a device vendor * * Sets the vendor name on the device. * * Since: 1.6.2 **/ void fu_device_set_vendor(FuDevice *self, const gchar *vendor) { g_autofree gchar *vendor_safe = NULL; /* trim any leading and trailing spaces */ if (vendor != NULL) vendor_safe = fu_strstrip(vendor); /* proxy */ fwupd_device_set_vendor(FWUPD_DEVICE(self), vendor_safe); fu_device_fixup_vendor_name(self); } static gchar * fu_device_sanitize_name(const gchar *value) { gboolean last_was_space = FALSE; guint last_non_space = 0; g_autoptr(GString) new = g_string_new(NULL); /* add each printable char with maximum of one whitespace char */ for (guint i = 0; value[i] != '\0'; i++) { const gchar tmp = value[i]; if (!g_ascii_isprint(tmp)) continue; if (g_ascii_isspace(tmp) || tmp == '_') { if (new->len == 0) continue; if (last_was_space) continue; last_was_space = TRUE; g_string_append_c(new, ' '); } else { last_was_space = FALSE; g_string_append_c(new, tmp); last_non_space = new->len; } } g_string_truncate(new, last_non_space); g_string_replace(new, "(TM)", "™", 0); g_string_replace(new, "(R)", "", 0); if (new->len == 0) return NULL; return g_string_free(g_steal_pointer(&new), FALSE); } /** * fu_device_set_name: * @self: a #FuDevice * @value: a device name * * Sets the name on the device. Any invalid parts will be converted or removed. * * Since: 0.7.1 **/ void fu_device_set_name(FuDevice *self, const gchar *value) { g_autofree gchar *value_safe = NULL; g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(value != NULL); /* overwriting? */ value_safe = fu_device_sanitize_name(value); if (value_safe == NULL) { g_info("ignoring name value: '%s'", value); return; } if (g_strcmp0(value_safe, fu_device_get_name(self)) == 0) return; /* changing */ if (fu_device_get_name(self) != NULL) { const gchar *id = fu_device_get_id(self); g_debug("%s device overwriting name value: %s->%s", id != NULL ? id : "unknown", fu_device_get_name(self), value_safe); } fwupd_device_set_name(FWUPD_DEVICE(self), value_safe); fu_device_fixup_vendor_name(self); } /** * fu_device_set_id: * @self: a #FuDevice * @id: a string, e.g. `tbt-port1` * * Sets the ID on the device. The ID should represent the *connection* of the * device, so that any similar device plugged into a different slot will * have a different @id string. * * The @id will be converted to a SHA1 hash if required before the device is * added to the daemon, and plugins should not assume that the ID that is set * here is the same as what is returned by fu_device_get_id(). * * Since: 0.7.1 **/ void fu_device_set_id(FuDevice *self, const gchar *id) { FuDevicePrivate *priv = GET_PRIVATE(self); GPtrArray *children; g_autofree gchar *id_hash = NULL; g_autofree gchar *id_hash_old = g_strdup(fwupd_device_get_id(FWUPD_DEVICE(self))); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(id != NULL); /* allow sane device-id to be set directly */ if (fwupd_device_id_is_valid(id)) { id_hash = g_strdup(id); } else { id_hash = g_compute_checksum_for_string(G_CHECKSUM_SHA1, id, -1); g_debug("using %s for %s", id_hash, id); } fwupd_device_set_id(FWUPD_DEVICE(self), id_hash); priv->device_id_valid = TRUE; /* ensure the parent ID is set */ children = fu_device_get_children(self); for (guint i = 0; i < children->len; i++) { FuDevice *devtmp = g_ptr_array_index(children, i); fwupd_device_set_parent_id(FWUPD_DEVICE(devtmp), id_hash); /* update the composite ID of the child with the new ID if required; this will * propagate to grandchildren and great-grandchildren as required */ if (id_hash_old != NULL && g_strcmp0(fu_device_get_composite_id(devtmp), id_hash_old) == 0) fu_device_set_composite_id(devtmp, id_hash); } } /** * fu_device_set_version_format: * @self: a #FuDevice * @fmt: the version format, e.g. %FWUPD_VERSION_FORMAT_PLAIN * * Sets the device version format. * * Since: 1.4.0 **/ void fu_device_set_version_format(FuDevice *self, FwupdVersionFormat fmt) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); /* same */ if (fu_device_get_version_format(self) == fmt) return; if (fu_device_get_version_format(self) != FWUPD_VERSION_FORMAT_UNKNOWN) { g_debug("changing verfmt for %s: %s->%s", fu_device_get_id(self), fwupd_version_format_to_string(fu_device_get_version_format(self)), fwupd_version_format_to_string(fmt)); } fwupd_device_set_version_format(FWUPD_DEVICE(self), fmt); /* convert this, now we know */ if (device_class->convert_version != NULL) { if (fu_device_get_version_raw(self) != 0) { g_autofree gchar *version = device_class->convert_version(self, fu_device_get_version_raw(self)); fu_device_set_version(self, version); } if (fu_device_get_version_lowest_raw(self) != 0) { g_autofree gchar *version = device_class->convert_version(self, fu_device_get_version_lowest_raw(self)); fu_device_set_version_lowest(self, version); } } } /** * fu_device_set_version: * @self: a #FuDevice * @version: (nullable): a string, e.g. `1.2.3` * * Sets the device version, sanitizing the string if required. * * Since: 1.2.9 **/ void fu_device_set_version(FuDevice *self, const gchar *version) { g_autofree gchar *version_safe = NULL; g_autoptr(GError) error = NULL; g_return_if_fail(FU_IS_DEVICE(self)); /* sanitize if required */ if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_ENSURE_SEMVER)) { version_safe = fu_version_ensure_semver(version, fu_device_get_version_format(self)); if (g_strcmp0(version, version_safe) != 0) g_debug("converted '%s' to '%s'", version, version_safe); } else { version_safe = g_strdup(version); } /* print a console warning for an invalid version, if semver */ if (version_safe != NULL && !fu_version_verify_format(version_safe, fu_device_get_version_format(self), &error)) #ifdef SUPPORTED_BUILD g_warning("%s", error->message); #else g_critical("%s", error->message); #endif /* if different */ if (g_strcmp0(fu_device_get_version(self), version_safe) != 0) { if (fu_device_get_version(self) != NULL) { g_debug("changing version for %s: %s->%s", fu_device_get_id(self), fu_device_get_version(self), version_safe); } fwupd_device_set_version(FWUPD_DEVICE(self), version_safe); } } /** * fu_device_set_version_lowest: * @self: a #FuDevice * @version: (nullable): a string, e.g. `1.2.3` * * Sets the device lowest version, sanitizing the string if required. * * Since: 1.4.0 **/ void fu_device_set_version_lowest(FuDevice *self, const gchar *version) { g_autofree gchar *version_safe = NULL; g_autoptr(GError) error = NULL; g_return_if_fail(FU_IS_DEVICE(self)); /* sanitize if required */ if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_ENSURE_SEMVER)) { version_safe = fu_version_ensure_semver(version, fu_device_get_version_format(self)); if (g_strcmp0(version, version_safe) != 0) g_debug("converted '%s' to '%s'", version, version_safe); } else { version_safe = g_strdup(version); } /* print a console warning for an invalid version, if semver */ if (version_safe != NULL && !fu_version_verify_format(version_safe, fu_device_get_version_format(self), &error)) #ifdef SUPPORTED_BUILD g_warning("%s", error->message); #else g_critical("%s", error->message); #endif /* if different */ if (g_strcmp0(fu_device_get_version_lowest(self), version_safe) != 0) { if (fu_device_get_version_lowest(self) != NULL) { g_debug("changing version lowest for %s: %s->%s", fu_device_get_id(self), fu_device_get_version_lowest(self), version_safe); } fwupd_device_set_version_lowest(FWUPD_DEVICE(self), version_safe); } } /** * fu_device_set_version_bootloader: * @self: a #FuDevice * @version: (nullable): a string, e.g. `1.2.3` * * Sets the device bootloader version, sanitizing the string if required. * * Since: 1.4.0 **/ void fu_device_set_version_bootloader(FuDevice *self, const gchar *version) { g_autofree gchar *version_safe = NULL; g_autoptr(GError) error = NULL; g_return_if_fail(FU_IS_DEVICE(self)); /* sanitize if required */ if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_ENSURE_SEMVER)) { version_safe = fu_version_ensure_semver(version, fu_device_get_version_format(self)); if (g_strcmp0(version, version_safe) != 0) g_debug("converted '%s' to '%s'", version, version_safe); } else { version_safe = g_strdup(version); } /* print a console warning for an invalid version, if semver */ if (version_safe != NULL && !fu_version_verify_format(version_safe, fu_device_get_version_format(self), &error)) #ifdef SUPPORTED_BUILD g_warning("%s", error->message); #else g_critical("%s", error->message); #endif /* if different */ if (g_strcmp0(fu_device_get_version_bootloader(self), version_safe) != 0) { if (fu_device_get_version_bootloader(self) != NULL) { g_debug("changing version for %s: %s->%s", fu_device_get_id(self), fu_device_get_version_bootloader(self), version_safe); } fwupd_device_set_version_bootloader(FWUPD_DEVICE(self), version_safe); } } /** * fu_device_set_version_raw: * @self: a #FuDevice * @version_raw: an integer * * Sets the raw device version from a integer value and the device version format. * * Since: 1.9.8 **/ void fu_device_set_version_raw(FuDevice *self, guint64 version_raw) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); g_return_if_fail(FU_IS_DEVICE(self)); fwupd_device_set_version_raw(FWUPD_DEVICE(self), version_raw); if (device_class->convert_version != NULL) { g_autofree gchar *version = device_class->convert_version(self, version_raw); if (version != NULL) fu_device_set_version(self, version); } } /** * fu_device_set_version_lowest_raw: * @self: a #FuDevice * @version_raw: an integer * * Sets the raw device version from a integer value and the device version format. * * Since: 2.0.7 **/ void fu_device_set_version_lowest_raw(FuDevice *self, guint64 version_raw) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); g_return_if_fail(FU_IS_DEVICE(self)); fwupd_device_set_version_lowest_raw(FWUPD_DEVICE(self), version_raw); if (device_class->convert_version != NULL) { g_autofree gchar *version = device_class->convert_version(self, version_raw); if (version != NULL) fu_device_set_version_lowest(self, version); } } /* private */ gboolean fu_device_is_updatable(FuDevice *self) { g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); return fu_device_has_flag(self, FWUPD_DEVICE_FLAG_UPDATABLE) || fu_device_has_flag(self, FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN); } static void fu_device_inhibit_free(FuDeviceInhibit *inhibit) { g_free(inhibit->inhibit_id); g_free(inhibit->reason); g_free(inhibit); } static void fu_device_ensure_inhibits(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); FwupdDeviceProblem problems = FWUPD_DEVICE_PROBLEM_NONE; guint nr_inhibits = g_hash_table_size(priv->inhibits); /* disable */ if (priv->notify_flags_handler_id != 0) g_signal_handler_block(self, priv->notify_flags_handler_id); /* was okay -> not okay */ if (nr_inhibits > 0) { g_autofree gchar *reasons_str = NULL; g_autoptr(GList) values = g_hash_table_get_values(priv->inhibits); g_autoptr(GPtrArray) reasons = g_ptr_array_new(); /* updatable -> updatable-hidden -- which is required as devices might have * inhibits and *not* be automatically updatable */ if (fu_device_has_flag(self, FWUPD_DEVICE_FLAG_UPDATABLE)) { fu_device_remove_flag(self, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(self, FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN); } /* update the update error */ for (GList *l = values; l != NULL; l = l->next) { FuDeviceInhibit *inhibit = (FuDeviceInhibit *)l->data; g_ptr_array_add(reasons, inhibit->reason); problems |= inhibit->problem; } reasons_str = fu_strjoin(", ", reasons); fu_device_set_update_error(self, reasons_str); } else { if (fu_device_has_flag(self, FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN)) { fu_device_remove_flag(self, FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN); fu_device_add_flag(self, FWUPD_DEVICE_FLAG_UPDATABLE); } fu_device_set_update_error(self, NULL); } /* sync with baseclass */ fwupd_device_set_problems(FWUPD_DEVICE(self), problems); /* enable */ if (priv->notify_flags_handler_id != 0) g_signal_handler_unblock(self, priv->notify_flags_handler_id); } static gchar * fu_device_problem_to_inhibit_reason(FuDevice *self, guint64 device_problem) { FuDevicePrivate *priv = GET_PRIVATE(self); if (device_problem == FWUPD_DEVICE_PROBLEM_UNREACHABLE) return g_strdup("Device is unreachable, or out of wireless range"); if (device_problem == FWUPD_DEVICE_PROBLEM_UPDATE_PENDING) return g_strdup("Device is waiting for the update to be applied"); if (device_problem == FWUPD_DEVICE_PROBLEM_REQUIRE_AC_POWER) return g_strdup("Device requires AC power to be connected"); if (device_problem == FWUPD_DEVICE_PROBLEM_LID_IS_CLOSED) return g_strdup("Device cannot be used while the lid is closed"); if (device_problem == FWUPD_DEVICE_PROBLEM_IS_EMULATED) return g_strdup("Device is emulated"); if (device_problem == FWUPD_DEVICE_PROBLEM_UPDATE_IN_PROGRESS) return g_strdup("An update is in progress"); if (device_problem == FWUPD_DEVICE_PROBLEM_IN_USE) return g_strdup("Device is in use"); if (device_problem == FWUPD_DEVICE_PROBLEM_DISPLAY_REQUIRED) return g_strdup("Device requires a display to be plugged in"); if (device_problem == FWUPD_DEVICE_PROBLEM_LOWER_PRIORITY) { if (fu_device_get_equivalent_id(self) == NULL) return g_strdup("Device is lower priority than an equivalent device"); return g_strdup_printf("Device is lower priority than equivalent device %s", fu_device_get_equivalent_id(self)); } if (device_problem == FWUPD_DEVICE_PROBLEM_MISSING_LICENSE) return g_strdup("Device does not have the necessary license installed"); if (device_problem == FWUPD_DEVICE_PROBLEM_SYSTEM_POWER_TOO_LOW) { if (priv->ctx == NULL) return g_strdup("System power is too low"); return g_strdup_printf("System power is too low (%u%%, requires %u%%)", fu_context_get_battery_level(priv->ctx), fu_context_get_battery_threshold(priv->ctx)); } if (device_problem == FWUPD_DEVICE_PROBLEM_POWER_TOO_LOW) { if (fu_device_get_battery_level(self) == FWUPD_BATTERY_LEVEL_INVALID || fu_device_get_battery_threshold(self) == FWUPD_BATTERY_LEVEL_INVALID) { return g_strdup_printf("Device battery power is too low"); } return g_strdup_printf("Device battery power is too low (%u%%, requires %u%%)", fu_device_get_battery_level(self), fu_device_get_battery_threshold(self)); } return NULL; } static void fu_device_inhibit_full(FuDevice *self, FwupdDeviceProblem problem, const gchar *inhibit_id, const gchar *reason) { FuDevicePrivate *priv = GET_PRIVATE(self); FuDeviceInhibit *inhibit; g_return_if_fail(FU_IS_DEVICE(self)); /* lazy create as most devices will not need this */ if (priv->inhibits == NULL) { priv->inhibits = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, (GDestroyNotify)fu_device_inhibit_free); } /* can fallback */ if (inhibit_id == NULL) inhibit_id = fwupd_device_problem_to_string(problem); /* already exists */ inhibit = g_hash_table_lookup(priv->inhibits, inhibit_id); if (inhibit != NULL) return; /* create new */ inhibit = g_new0(FuDeviceInhibit, 1); inhibit->problem = problem; inhibit->inhibit_id = g_strdup(inhibit_id); if (reason != NULL) { inhibit->reason = g_strdup(reason); } else { inhibit->reason = fu_device_problem_to_inhibit_reason(self, problem); } g_hash_table_insert(priv->inhibits, inhibit->inhibit_id, inhibit); /* refresh */ fu_device_ensure_inhibits(self); /* propagate to children */ if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_INHIBIT_CHILDREN)) { GPtrArray *children = fu_device_get_children(self); for (guint i = 0; i < children->len; i++) { FuDevice *child = g_ptr_array_index(children, i); fu_device_inhibit(child, inhibit_id, reason); } } } /** * fu_device_inhibit: * @self: a #FuDevice * @inhibit_id: an ID used for uninhibiting, e.g. `low-power` * @reason: (nullable): a string, e.g. `Cannot update as foo [bar] needs reboot` * * Prevent the device from being updated, changing it from %FWUPD_DEVICE_FLAG_UPDATABLE * to %FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN if not already inhibited. * * If the device already has an inhibit with the same @inhibit_id then the request * is ignored. * * Since: 1.6.0 **/ void fu_device_inhibit(FuDevice *self, const gchar *inhibit_id, const gchar *reason) { g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(inhibit_id != NULL); fu_device_inhibit_full(self, FWUPD_DEVICE_PROBLEM_NONE, inhibit_id, reason); } /** * fu_device_has_inhibit: * @self: a #FuDevice * @inhibit_id: an ID used for inhibiting, e.g. `low-power` * * Check if the device already has an inhibit with a specific ID. * * Returns: %TRUE if added * * Since: 1.8.0 **/ gboolean fu_device_has_inhibit(FuDevice *self, const gchar *inhibit_id) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(inhibit_id != NULL, FALSE); if (priv->inhibits == NULL) return FALSE; return g_hash_table_contains(priv->inhibits, inhibit_id); } /** * fu_device_remove_problem: * @self: a #FuDevice * @problem: a #FwupdDeviceProblem, e.g. %FWUPD_DEVICE_PROBLEM_SYSTEM_POWER_TOO_LOW * * Allow the device from being updated if there are no other inhibitors, * changing it from %FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN to %FWUPD_DEVICE_FLAG_UPDATABLE. * * If the device already has no inhibit with the @inhibit_id then the request * is ignored. * * Since: 1.8.1 **/ void fu_device_remove_problem(FuDevice *self, FwupdDeviceProblem problem) { g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(problem != FWUPD_DEVICE_PROBLEM_UNKNOWN); return fu_device_uninhibit(self, fwupd_device_problem_to_string(problem)); } /** * fu_device_has_problem: * @self: a #FuDevice * @problem: a #FwupdDeviceProblem, e.g. %FWUPD_DEVICE_PROBLEM_SYSTEM_POWER_TOO_LOW * * Query if a device has a specific problem. * * Returns: %TRUE if the device has this problem * * Since: 1.8.11 **/ gboolean fu_device_has_problem(FuDevice *self, FwupdDeviceProblem problem) { g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(problem != FWUPD_DEVICE_PROBLEM_UNKNOWN, FALSE); return fu_device_has_inhibit(self, fwupd_device_problem_to_string(problem)); } /** * fu_device_add_problem: * @self: a #FuDevice * @problem: a #FwupdDeviceProblem, e.g. %FWUPD_DEVICE_PROBLEM_SYSTEM_POWER_TOO_LOW * * Prevent the device from being updated, changing it from %FWUPD_DEVICE_FLAG_UPDATABLE * to %FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN if not already inhibited. * * If the device already has an inhibit with the same @problem then the request * is ignored. * * Since: 1.8.1 **/ void fu_device_add_problem(FuDevice *self, FwupdDeviceProblem problem) { g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(problem != FWUPD_DEVICE_PROBLEM_UNKNOWN); fu_device_inhibit_full(self, problem, NULL, NULL); } /** * fu_device_uninhibit: * @self: a #FuDevice * @inhibit_id: an ID used for uninhibiting, e.g. `low-power` * * Allow the device from being updated if there are no other inhibitors, * changing it from %FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN to %FWUPD_DEVICE_FLAG_UPDATABLE. * * If the device already has no inhibit with the @inhibit_id then the request * is ignored. * * Since: 1.6.0 **/ void fu_device_uninhibit(FuDevice *self, const gchar *inhibit_id) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(inhibit_id != NULL); if (priv->inhibits == NULL) return; if (g_hash_table_remove(priv->inhibits, inhibit_id)) fu_device_ensure_inhibits(self); /* propagate to children */ if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_INHIBIT_CHILDREN)) { GPtrArray *children = fu_device_get_children(self); for (guint i = 0; i < children->len; i++) { FuDevice *child = g_ptr_array_index(children, i); fu_device_uninhibit(child, inhibit_id); } } } /** * fu_device_ensure_id: * @self: a #FuDevice * @error: (nullable): optional return location for an error * * If not already set, generates a device ID with the optional physical and * logical IDs. * * Returns: %TRUE on success * * Since: 1.1.2 **/ gboolean fu_device_ensure_id(FuDevice *self, GError **error) { FuDevicePrivate *priv = GET_PRIVATE(self); g_autofree gchar *device_id = NULL; g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* already set */ if (priv->device_id_valid) return TRUE; /* nothing we can do! */ if (priv->physical_id == NULL) { g_autofree gchar *tmp = fu_device_to_string(self); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot ensure ID: %s", tmp); return FALSE; } /* logical may be NULL */ device_id = g_strjoin(":", fu_device_get_physical_id(self), fu_device_get_logical_id(self), NULL); fu_device_set_id(self, device_id); return TRUE; } /** * fu_device_get_logical_id: * @self: a #FuDevice * * Gets the logical ID set for the device, which disambiguates devices with the * same physical ID. * * Returns: a string value, or %NULL if never set. * * Since: 1.1.2 **/ const gchar * fu_device_get_logical_id(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), NULL); return priv->logical_id; } /** * fu_device_set_logical_id: * @self: a #FuDevice * @logical_id: a string, e.g. `dev2` * * Sets the logical ID on the device. This is designed to disambiguate devices * with the same physical ID. * * Since: 1.1.2 **/ void fu_device_set_logical_id(FuDevice *self, const gchar *logical_id) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); /* not changed */ if (g_strcmp0(priv->logical_id, logical_id) == 0) return; /* not allowed after ->probe() and ->setup() have completed */ if (priv->done_setup) { g_warning("cannot change %s logical ID from %s to %s as " "FuDevice->setup() has already completed", fu_device_get_id(self), priv->logical_id, logical_id); return; } g_free(priv->logical_id); priv->logical_id = g_strdup(logical_id); priv->device_id_valid = FALSE; g_object_notify(G_OBJECT(self), "logical-id"); } /** * fu_device_get_backend_id: * @self: a #FuDevice * * Gets the ID set for the device as recognized by the backend. This is typically * a Linux sysfs path or USB platform ID. If unset, it also falls back to the * physical ID as this may be the same value. * * Returns: a string value, or %NULL if never set. * * Since: 1.5.8 **/ const gchar * fu_device_get_backend_id(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), NULL); if (priv->backend_id != NULL) return priv->backend_id; return priv->physical_id; } /** * fu_device_set_backend_id: * @self: a #FuDevice * @backend_id: a string, e.g. `dev2` * * Sets the backend ID on the device. This is designed to disambiguate devices * with the same physical ID. This is typically a Linux sysfs path or USB * platform ID. * * Since: 1.5.8 **/ void fu_device_set_backend_id(FuDevice *self, const gchar *backend_id) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); /* not changed */ if (g_strcmp0(priv->backend_id, backend_id) == 0) return; g_free(priv->backend_id); priv->backend_id = g_strdup(backend_id); priv->device_id_valid = FALSE; g_object_notify(G_OBJECT(self), "backend-id"); } /** * fu_device_get_backend: * @self: a #FuDevice * * Gets the backend, if set with fu_device_set_backend(). * * Returns: (transfer none): a #FuBackend or %NULL if not sset * * Since: 2.0.0 **/ FuBackend * fu_device_get_backend(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), NULL); return priv->backend; } /** * fu_device_set_backend: * @self: a #FuDevice * @backend: a #FuBackend * * Sets the backend that created this device. * * Since: 2.0.0 **/ void fu_device_set_backend(FuDevice *self, FuBackend *backend) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(backend == NULL || FU_IS_BACKEND(backend)); /* same */ if (priv->backend == backend) return; /* not already set */ if (priv->ctx == NULL) fu_device_set_context(self, fu_backend_get_context(backend)); /* there is no ref on backend to prevent a loop */ if (priv->backend != NULL) g_object_remove_weak_pointer(G_OBJECT(priv->backend), (gpointer *)&priv->backend); if (backend != NULL) g_object_add_weak_pointer(G_OBJECT(backend), (gpointer *)&priv->backend); priv->backend = backend; /* no ref */ /* in case anything cares */ g_object_notify(G_OBJECT(self), "backend"); } /** * fu_device_get_backend_parent_with_subsystem: * @self: a #FuDevice * @subsystem: (nullable): an optional device subsystem, e.g. "usb:usb_device" * @error: (nullable): optional return location for an error * * Creates a device parent (of the correct type) using the current backend for a given device kind. * * NOTE: The backend must implement `FuBackendClass->get_device_parent` for this method to work -- * for cases where the plugin has created both parent and child, and used `fu_device_add_child()`, * using `fu_device_get_parent()` is probably more appropriate. * * Returns: (transfer full): a #FuDevice or %NULL if not found or unimplemented * * Since: 2.0.0 **/ FuDevice * fu_device_get_backend_parent_with_subsystem(FuDevice *self, const gchar *subsystem, GError **error) { FuDevicePrivate *priv = GET_PRIVATE(self); FuDeviceEvent *event = NULL; g_autofree gchar *event_id = NULL; g_autoptr(FuDevice) parent = NULL; g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_DEVICE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* sanity check */ if (priv->backend == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no backend set for device"); return NULL; } /* need event ID */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_strdup_printf("GetBackendParent:Subsystem=%s", subsystem); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { GType gtype; const gchar *gtype_str; const gchar *id; /* we might have to propagate this to preserve compat with older emulation files */ if (priv->fwupd_version != NULL && fu_version_compare(priv->fwupd_version, "2.0.8", FWUPD_VERSION_FORMAT_TRIPLET) >= 0) { event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return NULL; if (!fu_device_event_check_error(event, error)) return NULL; } else { event = fu_device_load_event(FU_DEVICE(self), event_id, NULL); if (event == NULL) { g_debug("falling back for emulation recorded by fwupd %s", priv->fwupd_version); parent = fu_backend_get_device_parent(priv->backend, self, subsystem, error); if (parent != self) fu_device_set_target(parent, self); return g_steal_pointer(&parent); } } /* missing GType is 'no parent found' */ gtype_str = fu_device_event_get_str(event, "GType", NULL); if (gtype_str == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no parent with subsystem %s", subsystem); return NULL; } gtype = g_type_from_name(gtype_str); if (gtype == G_TYPE_INVALID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no GType %s", gtype_str); return NULL; } parent = g_object_new(gtype, "context", fu_device_get_context(FU_DEVICE(self)), NULL); fu_device_add_flag(parent, FWUPD_DEVICE_FLAG_EMULATED); id = fu_device_event_get_str(event, "DeviceId", NULL); if (id != NULL) fu_device_set_id(parent, id); id = fu_device_event_get_str(event, "BackendId", NULL); if (id != NULL) fu_device_set_backend_id(parent, id); id = fu_device_event_get_str(event, "PhysicalId", NULL); if (id != NULL) fu_device_set_physical_id(parent, id); if (parent != self) fu_device_set_target(parent, self); return g_steal_pointer(&parent); } /* save */ if (event_id != NULL) event = fu_device_save_event(FU_DEVICE(self), event_id); /* call into the backend */ parent = fu_backend_get_device_parent(priv->backend, self, subsystem, &error_local); if (parent == NULL) { if (event != NULL) fu_device_event_set_error(event, error_local); g_propagate_error(error, g_steal_pointer(&error_local)); return NULL; } if (!fu_device_probe(parent, &error_local)) { if (event != NULL) fu_device_event_set_error(event, error_local); g_propagate_error(error, g_steal_pointer(&error_local)); return NULL; } /* save response */ if (event != NULL) { fu_device_event_set_str(event, "GType", G_OBJECT_TYPE_NAME(parent)); if (fu_device_get_id(self) != NULL) fu_device_event_set_str(event, "DeviceId", fu_device_get_id(parent)); if (fu_device_get_backend_id(parent) != NULL) { fu_device_event_set_str(event, "BackendId", fu_device_get_backend_id(parent)); } if (fu_device_get_physical_id(parent) != NULL) { fu_device_event_set_str(event, "PhysicalId", fu_device_get_physical_id(parent)); } } if (parent != self) fu_device_set_target(parent, self); return g_steal_pointer(&parent); } /** * fu_device_get_backend_parent: * @self: a #FuDevice * @error: (nullable): optional return location for an error * * Creates a device parent (of the correct type) using the current backend. * * NOTE: The backend must implement `FuBackendClass->get_device_parent` for this method to work -- * for cases where the plugin has created both parent and child, and used `fu_device_add_child()`, * using `fu_device_get_parent()` is probably more appropriate. * * Returns: (transfer full): a #FuDevice or %NULL if not found or unimplemented * * Since: 2.0.0 **/ FuDevice * fu_device_get_backend_parent(FuDevice *self, GError **error) { g_return_val_if_fail(FU_IS_DEVICE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return fu_device_get_backend_parent_with_subsystem(self, NULL, error); } /** * fu_device_get_update_request_id: * @self: a #FuDevice * * Gets the update request ID as specified from `LVFS::UpdateRequestId`. * * Returns: a string value, or %NULL if never set. * * Since: 1.8.6 **/ const gchar * fu_device_get_update_request_id(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), NULL); return priv->update_request_id; } /** * fu_device_set_update_request_id: * @self: a #FuDevice * @update_request_id: a string, e.g. `org.freedesktop.fwupd.request.do-not-power-off` * * Sets the update request ID as specified in `LVFS::UpdateRequestId`. * * Since: 1.8.6 **/ void fu_device_set_update_request_id(FuDevice *self, const gchar *update_request_id) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); /* not changed */ if (g_strcmp0(priv->update_request_id, update_request_id) == 0) return; g_free(priv->update_request_id); priv->update_request_id = g_strdup(update_request_id); } /** * fu_device_get_update_message: * @self: a #FuDevice * * Gets the update message string. * * Returns: the update message string, or %NULL if unset * * Since: 2.0.0 **/ const gchar * fu_device_get_update_message(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), NULL); return priv->update_message; } /** * fu_device_set_update_message: * @self: a #FuDevice * @update_message: (nullable): the update message string * * Sets the update message string. * * Since: 2.0.0 **/ void fu_device_set_update_message(FuDevice *self, const gchar *update_message) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); /* not changed */ if (g_strcmp0(priv->update_message, update_message) == 0) return; g_free(priv->update_message); priv->update_message = g_strdup(update_message); g_object_notify(G_OBJECT(self), "update-message"); } /** * fu_device_get_update_image: * @self: a #FuDevice * * Gets the update image URL. * * Returns: the update image URL, or %NULL if unset * * Since: 2.0.0 **/ const gchar * fu_device_get_update_image(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), NULL); return priv->update_image; } /** * fu_device_set_update_image: * @self: a #FuDevice * @update_image: (nullable): the update image URL * * Sets the update image URL. * * Since: 2.0.0 **/ void fu_device_set_update_image(FuDevice *self, const gchar *update_image) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); /* not changed */ if (g_strcmp0(priv->update_image, update_image) == 0) return; g_free(priv->update_image); priv->update_image = g_strdup(update_image); g_object_notify(G_OBJECT(self), "update-image"); } /** * fu_device_get_fwupd_version: * @self: a #FuDevice * * Gets the fwupd version that created the emulation. * * Returns: a version, e.g. `2.0.8`, or %NULL if unset * * Since: 2.0.8 **/ const gchar * fu_device_get_fwupd_version(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), NULL); return priv->fwupd_version; } /** * fu_device_set_fwupd_version: * @self: a #FuDevice * @fwupd_version: (nullable): the version, e.g. `2.0.8` * * Sets the fwupd version that created the emulation. * * NOTE: This can only be set on devices with the %FWUPD_DEVICE_FLAG_EMULATED flag set. * * Since: 2.0.8 **/ void fu_device_set_fwupd_version(FuDevice *self, const gchar *fwupd_version) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(fu_device_has_flag(self, FWUPD_DEVICE_FLAG_EMULATED)); /* not changed */ if (g_strcmp0(priv->fwupd_version, fwupd_version) == 0) return; g_free(priv->fwupd_version); priv->fwupd_version = g_strdup(fwupd_version); } /** * fu_device_get_proxy_guid: * @self: a #FuDevice * * Gets the proxy GUID device, which is set to let the engine match up the * proxy between plugins. * * Returns: a string value, or %NULL if never set. * * Since: 1.4.1 **/ const gchar * fu_device_get_proxy_guid(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), NULL); return priv->proxy_guid; } /** * fu_device_set_proxy_guid: * @self: a #FuDevice * @proxy_guid: a string, e.g. `USB\VID_413C&PID_B06E&hub` * * Sets the GUID of the proxy device. The proxy device may update @self. * * Since: 1.4.1 **/ void fu_device_set_proxy_guid(FuDevice *self, const gchar *proxy_guid) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); /* not changed */ if (g_strcmp0(priv->proxy_guid, proxy_guid) == 0) return; g_free(priv->proxy_guid); priv->proxy_guid = g_strdup(proxy_guid); } /** * fu_device_set_physical_id: * @self: a #FuDevice * @physical_id: a string that identifies the physical device connection * * Sets the physical ID on the device which represents the electrical connection * of the device to the system. Multiple #FuDevices can share a physical ID. * * The physical ID is used to remove logical devices when a physical device has * been removed from the system. * * A sysfs or devpath is not a physical ID, but could be something like * `PCI_SLOT_NAME=0000:3e:00.0`. * * Since: 1.1.2 **/ void fu_device_set_physical_id(FuDevice *self, const gchar *physical_id) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(physical_id != NULL); /* not changed */ if (g_strcmp0(priv->physical_id, physical_id) == 0) return; /* not allowed after ->probe() and ->setup() have completed */ if (priv->done_setup) { g_warning("cannot change %s physical ID from %s to %s as " "FuDevice->setup() has already completed", fu_device_get_id(self), priv->physical_id, physical_id); return; } g_free(priv->physical_id); priv->physical_id = g_strdup(physical_id); priv->device_id_valid = FALSE; g_object_notify(G_OBJECT(self), "physical-id"); } /** * fu_device_get_physical_id: * @self: a #FuDevice * * Gets the physical ID set for the device, which represents the electrical * connection used to compare devices. * * Multiple #FuDevices can share a single physical ID. * * Returns: a string value, or %NULL if never set. * * Since: 1.1.2 **/ const gchar * fu_device_get_physical_id(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), NULL); return priv->physical_id; } /** * fu_device_remove_flag: * @self: a #FuDevice * @flag: a device flag * * Removes a device flag from the device. * * Since: 1.6.0 **/ void fu_device_remove_flag(FuDevice *self, FwupdDeviceFlags flag) { /* proxy */ fwupd_device_remove_flag(FWUPD_DEVICE(self), flag); /* allow it to be updatable again */ if (flag & FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION) fu_device_uninhibit(self, "needs-activation"); if (flag & FWUPD_DEVICE_FLAG_UNREACHABLE) fu_device_uninhibit(self, "unreachable"); } /** * fu_device_add_flag: * @self: a #FuDevice * @flag: a device flag * * Adds a device flag to the device. * * Since: 0.1.0 **/ void fu_device_add_flag(FuDevice *self, FwupdDeviceFlags flag) { FuDevicePrivate *priv = GET_PRIVATE(self); /* none is not used as an "exported" flag */ if (flag == FWUPD_DEVICE_FLAG_NONE) return; /* an emulated device cannot be used to record emulation events, * and an emulated device cannot be tagged for emulation */ if (flag == FWUPD_DEVICE_FLAG_EMULATED) fu_device_remove_flag(self, FWUPD_DEVICE_FLAG_CAN_EMULATION_TAG); if (flag == FWUPD_DEVICE_FLAG_CAN_EMULATION_TAG && fu_device_has_flag(self, FWUPD_DEVICE_FLAG_EMULATED)) return; /* being both a bootloader and requiring a bootloader is invalid */ if (flag & FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER) fu_device_remove_flag(self, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); if (flag & FWUPD_DEVICE_FLAG_IS_BOOTLOADER) fu_device_remove_flag(self, FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER); /* being both a signed and unsigned is invalid */ if (flag & FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD) fu_device_remove_flag(self, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); if (flag & FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD) fu_device_remove_flag(self, FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); /* one implies the other */ if (flag & FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE) flag |= FWUPD_DEVICE_FLAG_CAN_VERIFY; if (flag & FWUPD_DEVICE_FLAG_INSTALL_ALL_RELEASES) flag |= FWUPD_DEVICE_FLAG_VERSION_CHECK_REQUIRED; fwupd_device_add_flag(FWUPD_DEVICE(self), flag); /* activatable devices shouldn't be allowed to update again until activated */ /* don't let devices be updated until activated */ if (flag & FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION) fu_device_inhibit(self, "needs-activation", "Pending activation"); /* do not let devices be updated until back in range */ if (flag & FWUPD_DEVICE_FLAG_UNREACHABLE) fu_device_add_problem(self, FWUPD_DEVICE_PROBLEM_UNREACHABLE); /* fixup and maybe show a warning if the remove delay was forgotten */ if ((flag & FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG) && priv->remove_delay == 0) { priv->remove_delay = FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE; #ifndef SUPPORTED_BUILD g_critical("FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG added but remove delay is unset! -- " "add something like fu_device_set_remove_delay(FU_DEVICE(self), " "FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE) to the %s _init()", G_OBJECT_TYPE_NAME(self)); #endif } } /** * fu_device_register_private_flag: * @self: a #FuDevice * @flag: a string * * Registers a private device flag so that it can be set from quirk files and printed * correctly in debug output. * * Since: 2.0.0 **/ void fu_device_register_private_flag(FuDevice *self, const gchar *flag) { GRefString *flag_registered; g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(flag != NULL); #ifndef SUPPORTED_BUILD /* ensure not already the name of an internal or exported flag */ if (fwupd_device_flag_from_string(flag) != FWUPD_DEVICE_FLAG_UNKNOWN) { g_critical("%s private flag %s already exists as an exported flag", G_OBJECT_TYPE_NAME(self), flag); return; } #endif /* sanity check */ flag_registered = fu_device_find_private_flag_registered(self, flag); if (flag_registered != NULL) { g_critical("already registered private %s flag %s", G_OBJECT_TYPE_NAME(self), flag); return; } /* add new */ fu_device_register_private_flag_safe(self, flag); } static void fu_device_set_custom_flag(FuDevice *self, const gchar *hint) { FwupdDeviceFlags flag; GRefString *private_flag; g_return_if_fail(hint != NULL); /* is this a negated device flag */ if (g_str_has_prefix(hint, "~")) { flag = fwupd_device_flag_from_string(hint + 1); if (flag != FWUPD_DEVICE_FLAG_UNKNOWN) { fu_device_remove_flag(self, flag); return; } private_flag = fu_device_find_private_flag_registered(self, hint + 1); if (private_flag != NULL) { fu_device_remove_private_flag(self, private_flag); return; } return; } /* is this a known device flag */ flag = fwupd_device_flag_from_string(hint); if (flag != FWUPD_DEVICE_FLAG_UNKNOWN) { fu_device_add_flag(self, flag); return; } private_flag = fu_device_find_private_flag_registered(self, hint); if (private_flag != NULL) { fu_device_add_private_flag(self, private_flag); return; } } /** * fu_device_set_custom_flags: * @self: a #FuDevice * @custom_flags: a string * * Sets the custom flags from the quirk system that can be used to * affect device matching. The actual string format is defined by the plugin. * * Since: 1.1.0 **/ void fu_device_set_custom_flags(FuDevice *self, const gchar *custom_flags) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(custom_flags != NULL); /* save what was set so we can use it for incorporating a superclass */ g_free(priv->custom_flags); priv->custom_flags = g_strdup(custom_flags); /* look for any standard FwupdDeviceFlags */ if (custom_flags != NULL) { g_auto(GStrv) hints = g_strsplit(custom_flags, ",", -1); for (guint i = 0; hints[i] != NULL; i++) fu_device_set_custom_flag(self, hints[i]); } } /** * fu_device_get_custom_flags: * @self: a #FuDevice * * Gets the custom flags for the device from the quirk system. * * Returns: a string value, or %NULL if never set. * * Since: 1.1.0 **/ const gchar * fu_device_get_custom_flags(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), NULL); return priv->custom_flags; } /** * fu_device_get_remove_delay: * @self: a #FuDevice * * Returns the maximum delay expected when replugging the device going into * bootloader mode. * * Returns: time in milliseconds * * Since: 1.0.2 **/ guint fu_device_get_remove_delay(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), 0); return priv->remove_delay; } /** * fu_device_set_remove_delay: * @self: a #FuDevice * @remove_delay: the value in milliseconds * * Sets the amount of time a device is allowed to return in bootloader mode. * * NOTE: this should be less than 3000ms for devices that just have to reset * and automatically re-enumerate, but significantly longer if it involves a * user removing a cable, pressing several buttons and removing a cable. * A suggested value for this would be 10,000ms. * * Since: 1.0.2 **/ void fu_device_set_remove_delay(FuDevice *self, guint remove_delay) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); priv->remove_delay = remove_delay; } /** * fu_device_get_acquiesce_delay: * @self: a #FuDevice * * Returns the time the daemon should wait for devices to finish hotplugging * after the update has completed. * * Returns: time in milliseconds * * Since: 1.8.3 **/ guint fu_device_get_acquiesce_delay(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), 0); return priv->acquiesce_delay; } /** * fu_device_set_acquiesce_delay: * @self: a #FuDevice * @acquiesce_delay: the value in milliseconds * * Sets the time the daemon should wait for devices to finish hotplugging * after the update has completed. * * Devices subclassing from [class@FuUsbDevice] and [class@FuUdevDevice] use * a value of 2,500ms, and other devices use 50ms by default. This can be also * be set using `AcquiesceDelay=` in a quirk file. * * Since: 1.8.3 **/ void fu_device_set_acquiesce_delay(FuDevice *self, guint acquiesce_delay) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); priv->acquiesce_delay = acquiesce_delay; } /** * fu_device_set_update_state: * @self: a #FuDevice * @update_state: the state, e.g. %FWUPD_UPDATE_STATE_PENDING * * Sets the update state, clearing the update error as required. * * Since: 1.6.2 **/ void fu_device_set_update_state(FuDevice *self, FwupdUpdateState update_state) { g_return_if_fail(FU_IS_DEVICE(self)); if (update_state == FWUPD_UPDATE_STATE_SUCCESS || update_state == FWUPD_UPDATE_STATE_PENDING || update_state == FWUPD_UPDATE_STATE_NEEDS_REBOOT) fu_device_set_update_error(self, NULL); if (update_state == FWUPD_UPDATE_STATE_NEEDS_REBOOT) { fu_device_add_problem(self, FWUPD_DEVICE_PROBLEM_UPDATE_IN_PROGRESS); } else { fu_device_remove_problem(self, FWUPD_DEVICE_PROBLEM_UPDATE_IN_PROGRESS); } fwupd_device_set_update_state(FWUPD_DEVICE(self), update_state); } static void fu_device_ensure_battery_inhibit(FuDevice *self) { if (fu_device_get_battery_level(self) == FWUPD_BATTERY_LEVEL_INVALID || fu_device_get_battery_level(self) >= fu_device_get_battery_threshold(self)) { fu_device_remove_problem(self, FWUPD_DEVICE_PROBLEM_POWER_TOO_LOW); return; } fu_device_add_problem(self, FWUPD_DEVICE_PROBLEM_POWER_TOO_LOW); } /** * fu_device_get_battery_level: * @self: a #FuDevice * * Returns the battery level. * * Returns: value in percent * * Since: 1.5.8 **/ guint fu_device_get_battery_level(FuDevice *self) { g_return_val_if_fail(FU_IS_DEVICE(self), G_MAXUINT); /* use the parent if the child is unset */ if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_BATTERY) && fwupd_device_get_battery_level(FWUPD_DEVICE(self)) == FWUPD_BATTERY_LEVEL_INVALID) { FuDevice *parent = fu_device_get_parent(self); if (parent != NULL) return fu_device_get_battery_level(parent); } return fwupd_device_get_battery_level(FWUPD_DEVICE(self)); } /** * fu_device_set_battery_level: * @self: a #FuDevice * @battery_level: the percentage value * * Sets the battery level, or %FWUPD_BATTERY_LEVEL_INVALID. * * Setting this allows fwupd to show a warning if the device change is too low * to perform the update. * * Since: 1.5.8 **/ void fu_device_set_battery_level(FuDevice *self, guint battery_level) { g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(battery_level <= FWUPD_BATTERY_LEVEL_INVALID); fwupd_device_set_battery_level(FWUPD_DEVICE(self), battery_level); fu_device_ensure_battery_inhibit(self); } /** * fu_device_get_battery_threshold: * @self: a #FuDevice * * Returns the battery threshold under which a firmware update cannot be * performed. * * If fu_device_set_battery_threshold() has not been used, a default value is * used instead. * * Returns: value in percent * * Since: 1.6.0 **/ guint fu_device_get_battery_threshold(FuDevice *self) { g_return_val_if_fail(FU_IS_DEVICE(self), FWUPD_BATTERY_LEVEL_INVALID); /* use the parent if the child is unset */ if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_BATTERY) && fwupd_device_get_battery_threshold(FWUPD_DEVICE(self)) == FWUPD_BATTERY_LEVEL_INVALID) { FuDevice *parent = fu_device_get_parent(self); if (parent != NULL) return fu_device_get_battery_threshold(parent); } return fwupd_device_get_battery_threshold(FWUPD_DEVICE(self)); } /** * fu_device_set_battery_threshold: * @self: a #FuDevice * @battery_threshold: the percentage value * * Sets the battery level, or %FWUPD_BATTERY_LEVEL_INVALID for the default. * * Setting this allows fwupd to show a warning if the device change is too low * to perform the update. * * Since: 1.6.0 **/ void fu_device_set_battery_threshold(FuDevice *self, guint battery_threshold) { g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(battery_threshold <= FWUPD_BATTERY_LEVEL_INVALID); fwupd_device_set_battery_threshold(FWUPD_DEVICE(self), battery_threshold); fu_device_ensure_battery_inhibit(self); } /** * fu_device_get_created_usec: * @self: a #FuDevice * * Gets when the device was created. * * If the usec-precision value has not been set with fu_device_set_created_usec(), * the exported seconds-precision fallback value is returned instead. * * Returns: value in microseconds, or -1 for invalid * * Since: 2.0.0 **/ gint64 fu_device_get_created_usec(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), -1); if (priv->created_usec > 0) return priv->created_usec; return fwupd_device_get_created(FWUPD_DEVICE(self)) * G_USEC_PER_SEC; } /** * fu_device_set_created_usec: * @self: a #FuDevice * @created_usec: value in microseconds * * Sets when the device was created. * * NOTE: This also sets the seconds-precision fallback value. * * Since: 2.0.0 **/ void fu_device_set_created_usec(FuDevice *self, gint64 created_usec) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(created_usec == 0 || created_usec > 10000000000); priv->created_usec = created_usec; fwupd_device_set_created(FWUPD_DEVICE(self), created_usec / G_USEC_PER_SEC); } /** * fu_device_get_modified_usec: * @self: a #FuDevice * * Gets when the device was modified. * * If the usec-precision value has not been set with fu_device_set_modified_usec(), * the exported seconds-precision fallback value is returned instead. * * Returns: value in microseconds, or -1 for invalid * * Since: 2.0.0 **/ gint64 fu_device_get_modified_usec(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), -1); if (priv->modified_usec > 0) return priv->modified_usec; return fwupd_device_get_modified(FWUPD_DEVICE(self)) * G_USEC_PER_SEC; } /** * fu_device_get_vid: * @self: a #FuUdevDevice * * Gets the device vendor code. * * Returns: a vendor code, or 0 if unset or invalid * * Since: 2.0.0 **/ guint16 fu_device_get_vid(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), 0x0000); return priv->vid; } /** * fu_device_set_vid: * @self: a #FuUdevDevice * @vid: an integer ID * * Sets the vendor ID. * * Since: 2.0.0 **/ void fu_device_set_vid(FuDevice *self, guint16 vid) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); if (priv->vid == vid) return; priv->vid = vid; g_object_notify(G_OBJECT(self), "vid"); } /** * fu_device_get_pid: * @self: a #FuUdevDevice * * Gets the device product code. * * Returns: a product code, or 0 if unset or invalid * * Since: 2.0.0 **/ guint16 fu_device_get_pid(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), 0x0000); return priv->pid; } /** * fu_device_set_pid: * @self: a #FuUdevDevice * @pid: an integer ID * * Sets the product ID. * * Since: 2.0.0 **/ void fu_device_set_pid(FuDevice *self, guint16 pid) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); if (priv->pid == pid) return; priv->pid = pid; g_object_notify(G_OBJECT(self), "pid"); } /** * fu_device_set_modified_usec: * @self: a #FuDevice * @modified_usec: value in microseconds * * Sets when the device was modified. * * NOTE: This also sets the seconds-precision fallback value. * * Since: 2.0.0 **/ void fu_device_set_modified_usec(FuDevice *self, gint64 modified_usec) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(modified_usec == 0 || modified_usec > 10000000000); priv->modified_usec = modified_usec; fwupd_device_set_modified(FWUPD_DEVICE(self), modified_usec / G_USEC_PER_SEC); } static gchar * fu_device_instance_flag_to_string_trunc(FuDeviceInstanceFlag flags) { g_autofree gchar *tmp = fu_device_instance_flag_to_string(flags); g_auto(GStrv) split = g_strsplit(tmp, ",", -1); for (guint i = 0; split[i] != NULL; i++) { if (strlen(split[i]) > 2) split[i][2] = '\0'; } return g_strjoinv(",", split); } static void fu_device_to_string_impl(FuDevice *self, guint idt, GString *str) { FuDevicePrivate *priv = GET_PRIVATE(self); for (guint i = 0; priv->instance_ids != NULL && i < priv->instance_ids->len; i++) { FuDeviceInstanceIdItem *item = g_ptr_array_index(priv->instance_ids, i); g_autofree gchar *flags_str = fu_device_instance_flag_to_string_trunc(item->flags); g_autofree gchar *title = g_strdup_printf("InstanceId[%s]", flags_str); if (item->instance_id != NULL) { g_autofree gchar *tmp2 = g_strdup_printf("%s ↠%s", item->guid, item->instance_id); fwupd_codec_string_append(str, idt, title, tmp2); } else { fwupd_codec_string_append(str, idt, title, item->guid); } } fwupd_codec_string_append(str, idt, "EquivalentId", priv->equivalent_id); fwupd_codec_string_append(str, idt, "PhysicalId", priv->physical_id); fwupd_codec_string_append(str, idt, "LogicalId", priv->logical_id); fwupd_codec_string_append(str, idt, "BackendId", priv->backend_id); fwupd_codec_string_append_hex(str, idt, "Vid", priv->vid); fwupd_codec_string_append_hex(str, idt, "Pid", priv->pid); fwupd_codec_string_append(str, idt, "UpdateRequestId", priv->update_request_id); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_UPDATE_MESSAGE, priv->update_message); fwupd_codec_string_append(str, idt, FWUPD_RESULT_KEY_UPDATE_IMAGE, priv->update_image); fwupd_codec_string_append(str, idt, "FwupdVersion", priv->fwupd_version); fwupd_codec_string_append(str, idt, "ProxyGuid", priv->proxy_guid); fwupd_codec_string_append_int(str, idt, "RemoveDelay", priv->remove_delay); fwupd_codec_string_append_int(str, idt, "AcquiesceDelay", priv->acquiesce_delay); fwupd_codec_string_append(str, idt, "CustomFlags", priv->custom_flags); if (priv->specialized_gtype != G_TYPE_INVALID) fwupd_codec_string_append(str, idt, "GType", g_type_name(priv->specialized_gtype)); if (priv->proxy_gtype != G_TYPE_INVALID) fwupd_codec_string_append(str, idt, "ProxyGType", g_type_name(priv->proxy_gtype)); if (priv->firmware_gtype != G_TYPE_INVALID) { fwupd_codec_string_append(str, idt, "FirmwareGType", g_type_name(priv->firmware_gtype)); } fwupd_codec_string_append_size(str, idt, "FirmwareSizeMin", priv->size_min); fwupd_codec_string_append_size(str, idt, "FirmwareSizeMax", priv->size_max); if (priv->order != G_MAXINT) { g_autofree gchar *order = g_strdup_printf("%i", priv->order); fwupd_codec_string_append(str, idt, "Order", order); } fwupd_codec_string_append_int(str, idt, "Priority", priv->priority); if (priv->metadata != NULL) { g_autoptr(GList) keys = g_hash_table_get_keys(priv->metadata); for (GList *l = keys; l != NULL; l = l->next) { const gchar *key = l->data; const gchar *value = g_hash_table_lookup(priv->metadata, key); fwupd_codec_string_append(str, idt, key, value); } } for (guint i = 0; i < priv->possible_plugins->len; i++) { const gchar *name = g_ptr_array_index(priv->possible_plugins, i); fwupd_codec_string_append(str, idt, "PossiblePlugin", name); } if (priv->parent_physical_ids != NULL && priv->parent_physical_ids->len > 0) { g_autofree gchar *flags = fu_strjoin(",", priv->parent_physical_ids); fwupd_codec_string_append(str, idt, "ParentPhysicalIds", flags); } if (priv->parent_backend_ids != NULL && priv->parent_backend_ids->len > 0) { g_autofree gchar *flags = fu_strjoin(",", priv->parent_backend_ids); fwupd_codec_string_append(str, idt, "ParentBackendIds", flags); } if (priv->private_flags != NULL && priv->private_flags->len != 0) { g_autoptr(GPtrArray) tmpv = g_ptr_array_new(); for (guint64 i = 0; i < priv->private_flags->len; i++) { GRefString *private_flag = g_ptr_array_index(priv->private_flags, i); g_ptr_array_add(tmpv, (gpointer)private_flag); } if (tmpv->len > 0) { g_autofree gchar *tmps = fu_strjoin(",", tmpv); fwupd_codec_string_append(str, idt, "PrivateFlags", tmps); } } if (priv->instance_hash != NULL) { GHashTableIter iter; gpointer key, value; g_hash_table_iter_init(&iter, priv->instance_hash); while (g_hash_table_iter_next(&iter, &key, &value)) { g_autofree gchar *title = g_strdup_printf("InstanceKey[%s]", (const gchar *)key); fwupd_codec_string_append(str, idt, title, value); } } if (priv->inhibits != NULL) { g_autoptr(GList) values = g_hash_table_get_values(priv->inhibits); for (GList *l = values; l != NULL; l = l->next) { FuDeviceInhibit *inhibit = (FuDeviceInhibit *)l->data; g_autofree gchar *val = g_strdup_printf("[%s] %s", inhibit->inhibit_id, inhibit->reason); fwupd_codec_string_append(str, idt, "Inhibit", val); } } if (priv->events != NULL) { fwupd_codec_string_append(str, idt, "Events", ""); for (guint i = 0; i < priv->events->len; i++) { FuDeviceEvent *event = g_ptr_array_index(priv->events, i); if (i > 10) { g_autofree gchar *msg = g_strdup_printf("…and %u more events", priv->events->len - 10); fwupd_codec_string_append(str, idt + 1, "", msg); break; } fwupd_codec_add_string(FWUPD_CODEC(event), idt + 1, str); } } if (priv->proxy != NULL) { fwupd_codec_string_append(str, idt, "Proxy", ""); fu_device_to_string_impl(priv->proxy, idt + 1, str); } } static GPtrArray * fu_device_get_common_class_parents(FuDevice *self, FuDevice *donor) { g_autoptr(GPtrArray) array = g_ptr_array_new(); for (GType gtype = G_OBJECT_TYPE(self); gtype != FWUPD_TYPE_DEVICE; gtype = g_type_parent(gtype)) { FuDeviceClass *device_class = g_type_class_peek(gtype); for (GType gtype_donor = G_OBJECT_TYPE(donor); gtype_donor != FWUPD_TYPE_DEVICE; gtype_donor = g_type_parent(gtype_donor)) { if (gtype == gtype_donor) g_ptr_array_add(array, device_class); } } return g_steal_pointer(&array); } /** * fu_device_add_string: * @self: a #FuDevice * @idt: indent level * @str: a string to append to * * Add daemon-specific device metadata to an existing string. * * Since: 1.7.1 **/ void fu_device_add_string(FuDevice *self, guint idt, GString *str) { GPtrArray *children; gpointer device_class_to_string_last = NULL; g_autoptr(GList) device_class_list = NULL; /* add baseclass */ fwupd_codec_add_string(FWUPD_CODEC(self), idt, str); /* run every unique ->to_string() in each subclass */ for (GType gtype = G_OBJECT_TYPE(self); gtype != G_TYPE_INVALID; gtype = g_type_parent(gtype)) { FuDeviceClass *device_class = g_type_class_peek(gtype); if (!FU_IS_DEVICE_CLASS(device_class)) break; device_class_list = g_list_prepend(device_class_list, device_class); } for (GList *l = device_class_list; l != NULL; l = l->next) { FuDeviceClass *device_class = FU_DEVICE_CLASS(l->data); if (device_class->to_string != NULL && device_class->to_string != device_class_to_string_last) { device_class->to_string(self, idt + 1, str); device_class_to_string_last = device_class->to_string; } } /* print children also */ children = fu_device_get_children(self); for (guint i = 0; i < children->len; i++) { FuDevice *child = g_ptr_array_index(children, i); fu_device_add_string(child, idt + 1, str); } } /** * fu_device_to_string: * @self: a #FuDevice * * This allows us to easily print the device, the release and the * daemon-specific metadata. * * Returns: a string value, or %NULL for invalid. * * Since: 0.9.8 **/ gchar * fu_device_to_string(FuDevice *self) { GString *str = g_string_new(NULL); fu_device_add_string(self, 0, str); return g_string_free(str, FALSE); } /** * fu_device_set_context: * @self: a #FuDevice * @ctx: (nullable): optional #FuContext * * Sets the optional context which may be useful to this device. * This is typically set after the device has been created, but before * the device has been opened or probed. * * Since: 1.6.0 **/ void fu_device_set_context(FuDevice *self, FuContext *ctx) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(FU_IS_CONTEXT(ctx) || ctx == NULL); #ifndef SUPPORTED_BUILD if (priv->ctx != NULL && ctx == NULL) { g_critical("clearing device context for %s [%s]", fu_device_get_name(self), fu_device_get_id(self)); return; } #endif if (g_set_object(&priv->ctx, ctx)) g_object_notify(G_OBJECT(self), "context"); } /** * fu_device_get_context: * @self: a #FuDevice * * Gets the context assigned for this device. * * Returns: (transfer none): the #FuContext object, or %NULL * * Since: 1.6.0 **/ FuContext * fu_device_get_context(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), NULL); return priv->ctx; } /** * fu_device_get_results: * @self: a #FuDevice * @error: (nullable): optional return location for an error * * Gets the results of the last update operation on the device by calling a vfunc. * * Returns: %TRUE on success * * Since: 1.6.2 **/ gboolean fu_device_get_results(FuDevice *self, GError **error) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* no plugin-specific method */ if (device_class->get_results == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "getting results not supported by device"); return FALSE; } /* call vfunc */ return device_class->get_results(self, error); } /** * fu_device_write_firmware: * @self: a #FuDevice * @firmware: a #FuFirmware * @progress: a #FuProgress * @flags: install flags, e.g. %FWUPD_INSTALL_FLAG_FORCE * @error: (nullable): optional return location for an error * * Writes firmware to the device by calling a plugin-specific vfunc. * * Returns: %TRUE on success * * Since: 2.0.7 **/ gboolean fu_device_write_firmware(FuDevice *self, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); FuDevicePrivate *priv = GET_PRIVATE(self); g_autofree gchar *str = NULL; g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(FU_IS_FIRMWARE(firmware), FALSE); g_return_val_if_fail(FU_IS_PROGRESS(progress), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* no plugin-specific method */ if (device_class->write_firmware == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "writing firmware not supported by device"); return FALSE; } /* call vfunc */ str = fu_firmware_to_string(firmware); g_info("installing onto %s:\n%s", fu_device_get_id(self), str); g_set_object(&priv->progress, progress); if (!device_class->write_firmware(self, firmware, priv->progress, flags, error)) return FALSE; /* the device set an UpdateMessage (possibly from a quirk, or XML file) * but did not do an event; guess something */ if (priv->request_cnts[FWUPD_REQUEST_KIND_POST] == 0 && fu_device_get_update_message(self) != NULL) { const gchar *update_request_id = fu_device_get_update_request_id(self); g_autoptr(FwupdRequest) request = fwupd_request_new(); fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_POST); if (update_request_id != NULL) { fwupd_request_set_id(request, update_request_id); fwupd_request_add_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); } else { fu_device_add_request_flag(self, FWUPD_REQUEST_FLAG_NON_GENERIC_MESSAGE); fwupd_request_set_id(request, FWUPD_REQUEST_ID_REMOVE_REPLUG); } fwupd_request_set_message(request, fu_device_get_update_message(self)); fwupd_request_set_image(request, fu_device_get_update_image(self)); if (!fu_device_emit_request(self, request, progress, error)) return FALSE; } /* success */ return TRUE; } /** * fu_device_prepare_firmware: * @self: a #FuDevice * @stream: a #GInputStream * @flags: #FuFirmwareParseFlags, e.g. %FWUPD_INSTALL_FLAG_FORCE * @error: (nullable): optional return location for an error * * Prepares the firmware by calling an optional device-specific vfunc for the * device, which can do things like decompressing or parsing of the firmware * data. * * For all firmware, this checks the size of the firmware if limits have been * set using fu_device_set_firmware_size_min(), fu_device_set_firmware_size_max() * or using a quirk entry. * * Returns: (transfer full): a new #GBytes, or %NULL for error * * Since: 1.1.2 **/ FuFirmware * fu_device_prepare_firmware(FuDevice *self, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); FuDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(FuFirmware) firmware = NULL; gsize fw_size; g_return_val_if_fail(FU_IS_DEVICE(self), NULL); g_return_val_if_fail(G_IS_INPUT_STREAM(stream), NULL); g_return_val_if_fail(FU_IS_PROGRESS(progress), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* optionally subclassed */ if (device_class->prepare_firmware != NULL) { firmware = device_class->prepare_firmware(self, stream, progress, flags, error); if (firmware == NULL) return NULL; } else if (priv->firmware_gtype != G_TYPE_INVALID) { firmware = g_object_new(priv->firmware_gtype, NULL); if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; } else { firmware = fu_firmware_new(); if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; } /* check size */ fw_size = fu_firmware_get_size(firmware); if (fw_size != 0) { if (priv->size_max > 0 && fw_size > priv->size_max) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware is 0x%04x bytes larger than the allowed " "maximum size of 0x%04x bytes", (guint)(fw_size - priv->size_max), (guint)priv->size_max); return NULL; } if (priv->size_min > 0 && fw_size < priv->size_min) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware is %04x bytes smaller than the allowed " "minimum size of %04x bytes", (guint)(priv->size_min - fw_size), (guint)priv->size_max); return NULL; } } /* success */ return g_steal_pointer(&firmware); } /** * fu_device_read_firmware: * @self: a #FuDevice * @progress: a #FuProgress * @error: (nullable): optional return location for an error * * Reads firmware from the device by calling a plugin-specific vfunc. * The device subclass should try to ensure the firmware does not contain any * serial numbers or user-configuration values and can be used to calculate the * device checksum. * * The return value can be converted to a blob of memory using fu_firmware_write(). * * Returns: (transfer full): a #FuFirmware, or %NULL for error * * Since: 1.0.8 **/ FuFirmware * fu_device_read_firmware(FuDevice *self, FuProgress *progress, GError **error) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); FuDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(GBytes) fw = NULL; g_return_val_if_fail(FU_IS_DEVICE(self), NULL); g_return_val_if_fail(FU_IS_PROGRESS(progress), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* device does not support reading for verification CRCs */ if (!fu_device_has_flag(self, FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "reading firmware is not supported by device"); return NULL; } /* call vfunc */ g_set_object(&priv->progress, progress); if (device_class->read_firmware != NULL) return device_class->read_firmware(self, progress, error); /* use the default FuFirmware when only ->dump_firmware is provided */ fw = fu_device_dump_firmware(self, progress, error); if (fw == NULL) return NULL; if (priv->firmware_gtype != G_TYPE_INVALID) { g_autoptr(FuFirmware) firmware = g_object_new(priv->firmware_gtype, NULL); if (!fu_firmware_parse_bytes(firmware, fw, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return NULL; return g_steal_pointer(&firmware); } return fu_firmware_new_from_bytes(fw); } /** * fu_device_dump_firmware: * @self: a #FuDevice * @progress: a #FuProgress * @error: (nullable): optional return location for an error * * Reads the raw firmware image from the device by calling a plugin-specific * vfunc. This raw firmware image may contain serial numbers or device-specific * configuration but should be a byte-for-byte match compared to using an * external SPI programmer. * * Returns: (transfer full): a #GBytes, or %NULL for error * * Since: 1.5.0 **/ GBytes * fu_device_dump_firmware(FuDevice *self, FuProgress *progress, GError **error) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), NULL); g_return_val_if_fail(FU_IS_PROGRESS(progress), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* use the default FuFirmware when only ->dump_firmware is provided */ if (device_class->dump_firmware == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "dumping firmware is not supported by device"); return NULL; } /* proxy */ g_set_object(&priv->progress, progress); return device_class->dump_firmware(self, progress, error); } /** * fu_device_detach: * @self: a #FuDevice * @error: (nullable): optional return location for an error * * Detaches a device from the application into bootloader mode. * * Returns: %TRUE on success * * Since: 1.0.8 **/ gboolean fu_device_detach(FuDevice *self, GError **error) { g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); return fu_device_detach_full(self, progress, error); } /** * fu_device_detach_full: * @self: a #FuDevice * @progress: a #FuProgress * @error: (nullable): optional return location for an error * * Detaches a device from the application into bootloader mode. * * Returns: %TRUE on success * * Since: 1.7.0 **/ gboolean fu_device_detach_full(FuDevice *self, FuProgress *progress, GError **error) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(FU_IS_PROGRESS(progress), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* no plugin-specific method */ if (device_class->detach == NULL) return TRUE; /* call vfunc */ g_set_object(&priv->progress, progress); return device_class->detach(self, progress, error); } /** * fu_device_attach: * @self: a #FuDevice * @error: (nullable): optional return location for an error * * Attaches a device from the bootloader into application mode. * * Returns: %TRUE on success * * Since: 1.0.8 **/ gboolean fu_device_attach(FuDevice *self, GError **error) { g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); return fu_device_attach_full(self, progress, error); } /** * fu_device_attach_full: * @self: a #FuDevice * @progress: a #FuProgress * @error: (nullable): optional return location for an error * * Attaches a device from the bootloader into application mode. * * Returns: %TRUE on success * * Since: 1.7.0 **/ gboolean fu_device_attach_full(FuDevice *self, FuProgress *progress, GError **error) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(FU_IS_PROGRESS(progress), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* no plugin-specific method */ if (device_class->attach == NULL) return TRUE; /* call vfunc */ g_set_object(&priv->progress, progress); return device_class->attach(self, progress, error); } /** * fu_device_reload: * @self: a #FuDevice * @error: (nullable): optional return location for an error * * Reloads a device that has just gone from bootloader into application mode. * * Returns: %TRUE on success * * Since: 1.3.3 **/ gboolean fu_device_reload(FuDevice *self, GError **error) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* no plugin-specific method */ if (device_class->reload == NULL) return TRUE; /* call vfunc */ return device_class->reload(self, error); } /** * fu_device_prepare: * @self: a #FuDevice * @progress: a #FuProgress * @flags: install flags * @error: (nullable): optional return location for an error * * Prepares a device for update. A different plugin can handle each of * FuDevice->prepare(), FuDevice->detach() and FuDevice->write_firmware(). * * Returns: %TRUE on success * * Since: 1.3.3 **/ gboolean fu_device_prepare(FuDevice *self, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(FU_IS_PROGRESS(progress), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* no plugin-specific method */ if (device_class->prepare == NULL) return TRUE; /* call vfunc */ g_set_object(&priv->progress, progress); return device_class->prepare(self, progress, flags, error); } /** * fu_device_cleanup: * @self: a #FuDevice * @progress: a #FuProgress * @flags: install flags * @error: (nullable): optional return location for an error * * Cleans up a device after an update. A different plugin can handle each of * FuDevice->write_firmware(), FuDevice->attach() and FuDevice->cleanup(). * * Returns: %TRUE on success * * Since: 1.3.3 **/ gboolean fu_device_cleanup(FuDevice *self, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(FU_IS_PROGRESS(progress), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* no plugin-specific method */ if (device_class->cleanup == NULL) return TRUE; /* call vfunc */ g_set_object(&priv->progress, progress); return device_class->cleanup(self, progress, flags, error); } static gboolean fu_device_open_cb(FuDevice *self, gpointer user_data, GError **error) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); return device_class->open(self, error); } static gboolean fu_device_open_internal(FuDevice *self, GError **error) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); FuDevicePrivate *priv = GET_PRIVATE(self); /* already open */ g_atomic_int_inc(&priv->open_refcount); if (priv->open_refcount > 1) return TRUE; /* probe */ if (!fu_device_probe(self, error)) { g_prefix_error(error, "failed to probe: "); return FALSE; } /* ensure the device ID is already setup */ if (!fu_device_ensure_id(self, error)) { g_prefix_error(error, "failed to ensure ID: "); return FALSE; } /* subclassed */ if (device_class->open != NULL) { if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_RETRY_OPEN)) { if (!fu_device_retry_full(self, fu_device_open_cb, FU_DEVICE_RETRY_OPEN_COUNT, FU_DEVICE_RETRY_OPEN_DELAY, NULL, error)) { g_prefix_error(error, "failed to retry subclass open: "); return FALSE; } } else { if (!device_class->open(self, error)) { g_prefix_error(error, "failed to subclass open: "); return FALSE; } } } /* setup */ if (!fu_device_setup(self, error)) { g_prefix_error(error, "failed to setup: "); return FALSE; } /* ensure the device ID is still valid */ if (!fu_device_ensure_id(self, error)) { g_prefix_error(error, "failed to ensure ID: "); return FALSE; } /* success */ fu_device_add_private_flag(self, FU_DEVICE_PRIVATE_FLAG_IS_OPEN); return TRUE; } /** * fu_device_open: * @self: a #FuDevice * @error: (nullable): optional return location for an error * * Opens a device, optionally running a object-specific vfunc. * * Plugins can call fu_device_open() multiple times without calling * fu_device_close(), but only the first call will actually invoke the vfunc. * * It is expected that plugins issue the same number of fu_device_open() and * fu_device_close() methods when using a specific @self. * * If the `->probe()`, `->open()` and `->setup()` actions all complete * successfully the internal device flag %FU_DEVICE_PRIVATE_FLAG_IS_OPEN will * be set. * * NOTE: It is important to still call fu_device_close() even if this function * fails as the device may still be partially initialized. * * Returns: %TRUE for success * * Since: 1.1.2 **/ gboolean fu_device_open(FuDevice *self, GError **error) { g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* skip */ if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_IS_FAKE)) { fu_device_add_private_flag(self, FU_DEVICE_PRIVATE_FLAG_IS_OPEN); if (!fu_device_probe(self, error)) return FALSE; if (!fu_device_setup(self, error)) return FALSE; return fu_device_ensure_id(self, error); } /* use parent */ if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_OPEN)) { FuDevice *parent = fu_device_get_parent(self); if (parent == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no parent device"); return FALSE; } return fu_device_open_internal(parent, error); } if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_USE_PROXY_FOR_OPEN)) { FuDevice *proxy = fu_device_get_proxy(self); if (proxy == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy device"); return FALSE; } if (!fu_device_open_internal(proxy, error)) return FALSE; } return fu_device_open_internal(self, error); } static gboolean fu_device_close_internal(FuDevice *self, GError **error) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); FuDevicePrivate *priv = GET_PRIVATE(self); /* not yet open */ if (priv->open_refcount == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "cannot close device, refcount already zero"); return FALSE; } if (!g_atomic_int_dec_and_test(&priv->open_refcount)) return TRUE; /* subclassed */ if (device_class->close != NULL) { if (!device_class->close(self, error)) return FALSE; } /* success */ fu_device_remove_private_flag(self, FU_DEVICE_PRIVATE_FLAG_IS_OPEN); return TRUE; } /** * fu_device_close: * @self: a #FuDevice * @error: (nullable): optional return location for an error * * Closes a device, optionally running a object-specific vfunc. * * Plugins can call fu_device_close() multiple times without calling * fu_device_open(), but only the last call will actually invoke the vfunc. * * It is expected that plugins issue the same number of fu_device_open() and * fu_device_close() methods when using a specific @self. * * An error is returned if this method is called without having used the * fu_device_open() method beforehand. * * If the close action completed successfully the internal device flag * %FU_DEVICE_PRIVATE_FLAG_IS_OPEN will be cleared. * * Returns: %TRUE for success * * Since: 1.1.2 **/ gboolean fu_device_close(FuDevice *self, GError **error) { g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* skip */ if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_IS_FAKE)) { fu_device_remove_private_flag(self, FU_DEVICE_PRIVATE_FLAG_IS_OPEN); return TRUE; } /* close the device first in case the plugin needs to use the proxy or parent */ if (!fu_device_close_internal(self, error)) return FALSE; /* use parent */ if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_OPEN)) { FuDevice *parent = fu_device_get_parent(self); if (parent == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no parent device"); return FALSE; } return fu_device_close_internal(parent, error); } if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_USE_PROXY_FOR_OPEN)) { FuDevice *proxy = fu_device_get_proxy(self); if (proxy == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy device"); return FALSE; } if (!fu_device_close_internal(proxy, error)) return FALSE; } /* success */ return TRUE; } /** * fu_device_probe: * @self: a #FuDevice * @error: (nullable): optional return location for an error * * Probes a device, setting parameters on the object that does not need * the device open or the interface claimed. * If the device is not compatible then an error should be returned. * * Returns: %TRUE for success * * Since: 1.1.2 **/ gboolean fu_device_probe(FuDevice *self, GError **error) { FuDevicePrivate *priv = GET_PRIVATE(self); FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* already done */ if (priv->done_probe) return TRUE; /* device self-assigned */ if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_NO_PROBE)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not probing"); return FALSE; } /* subclassed */ if (device_class->probe != NULL) { if (!device_class->probe(self, error)) return FALSE; } /* vfunc skipped device */ if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_NO_PROBE)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not probing"); return FALSE; } /* success */ priv->done_probe = TRUE; return TRUE; } /** * fu_device_probe_complete: * @self: a #FuDevice * * Tell the device that all probing has finished. This allows it to release any resources that are * only valid during coldplug or hotplug. * * Since: 1.8.12 **/ void fu_device_probe_complete(FuDevice *self) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); g_return_if_fail(FU_IS_DEVICE(self)); if (device_class->probe_complete != NULL) device_class->probe_complete(self); } /** * fu_device_rescan: * @self: a #FuDevice * @error: (nullable): optional return location for an error * * Rescans a device, re-adding GUIDs or flags based on some hardware change. * * Returns: %TRUE for success * * Since: 1.3.1 **/ gboolean fu_device_rescan(FuDevice *self, GError **error) { FuDevicePrivate *priv = GET_PRIVATE(self); FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* remove all GUIDs */ if (priv->instance_ids != NULL) g_ptr_array_set_size(priv->instance_ids, 0); g_ptr_array_set_size(fu_device_get_instance_ids(self), 0); g_ptr_array_set_size(fu_device_get_guids(self), 0); /* subclassed */ if (device_class->rescan != NULL) { if (!device_class->rescan(self, error)) { fu_device_convert_instance_ids(self); return FALSE; } } fu_device_convert_instance_ids(self); return TRUE; } /** * fu_device_set_progress: * @self: a #FuDevice * @progress: a #FuProgress * * Sets steps on the progress object used to write firmware. * * Since: 1.7.0 **/ void fu_device_set_progress(FuDevice *self, FuProgress *progress) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(FU_IS_PROGRESS(progress)); /* subclassed */ if (device_class->set_progress == NULL) return; device_class->set_progress(self, progress); } /** * fu_device_convert_instance_ids: * @self: a #FuDevice * * Converts all the Device instance IDs added using fu_device_add_instance_id() * into actual GUIDs, **unless** %FU_DEVICE_PRIVATE_FLAG_NO_AUTO_INSTANCE_IDS has * been set. * * Plugins will only need to need to call this manually when adding child * devices, as fu_device_setup() automatically calls this after the * fu_device_probe() and fu_device_setup() virtual functions have been run. * * Since: 1.2.5 **/ void fu_device_convert_instance_ids(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); /* already set */ if (fu_device_get_guids(self)->len > 0) return; /* convert the FuDevice IDs to FwupdDevice IDs */ if (priv->instance_ids != NULL) { for (guint i = 0; i < priv->instance_ids->len; i++) { FuDeviceInstanceIdItem *item = g_ptr_array_index(priv->instance_ids, i); if ((item->flags & FU_DEVICE_INSTANCE_FLAG_VISIBLE) == 0) continue; if ((item->flags & FU_DEVICE_INSTANCE_FLAG_GENERIC) > 0 && fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_NO_GENERIC_GUIDS)) continue; if (item->instance_id != NULL) fwupd_device_add_instance_id(FWUPD_DEVICE(self), item->instance_id); fwupd_device_add_guid(FWUPD_DEVICE(self), item->guid); } } /* OEM specific hardware */ if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_NO_AUTO_INSTANCE_IDS)) return; } /** * fu_device_setup: * @self: a #FuDevice * @error: (nullable): optional return location for an error * * Sets up a device, setting parameters on the object that requires * the device to be open and have the interface claimed. * If the device is not compatible then an error should be returned. * * Returns: %TRUE for success * * Since: 1.1.2 **/ gboolean fu_device_setup(FuDevice *self, GError **error) { FuDevicePrivate *priv = GET_PRIVATE(self); FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); GPtrArray *children; g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* skip */ if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_IS_FAKE)) { fu_device_convert_instance_ids(self); return TRUE; } /* should have already been called */ if (!fu_device_probe(self, error)) return FALSE; /* already done */ if (priv->done_setup) return TRUE; /* subclassed */ if (device_class->setup != NULL) { if (!device_class->setup(self, error)) return FALSE; } /* vfunc skipped device */ if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_NO_PROBE)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not probing"); return FALSE; } /* run setup on the children too (unless done already) */ children = fu_device_get_children(self); for (guint i = 0; i < children->len; i++) { FuDevice *child_tmp = g_ptr_array_index(children, i); if (!fu_device_setup(child_tmp, error)) return FALSE; } /* convert the instance IDs to GUIDs */ fu_device_convert_instance_ids(self); /* subclassed */ if (device_class->ready != NULL) { if (!device_class->ready(self, error)) return FALSE; } priv->done_setup = TRUE; return TRUE; } /** * fu_device_activate: * @self: a #FuDevice * @progress: a #FuProgress * @error: (nullable): optional return location for an error * * Activates up a device, which normally means the device switches to a new * firmware version. This should only be called when data loss cannot occur. * * Returns: %TRUE for success * * Since: 1.2.6 **/ gboolean fu_device_activate(FuDevice *self, FuProgress *progress, GError **error) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(FU_IS_PROGRESS(progress), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* subclassed */ if (device_class->activate != NULL) { g_set_object(&priv->progress, progress); if (!device_class->activate(self, progress, error)) return FALSE; } return TRUE; } /** * fu_device_probe_invalidate: * @self: a #FuDevice * * Normally when calling fu_device_probe() multiple times it is only done once. * Calling this method causes the next requests to fu_device_probe() and * fu_device_setup() actually probe the hardware. * * This should be done in case the backing device has changed, for instance if * a USB device has been replugged. * * Since: 1.1.2 **/ void fu_device_probe_invalidate(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); g_return_if_fail(FU_IS_DEVICE(self)); priv->done_probe = FALSE; priv->done_setup = FALSE; if (device_class->invalidate != NULL) device_class->invalidate(self); } /** * fu_device_report_metadata_pre: * @self: a #FuDevice * * Collects metadata that would be useful for debugging a failed update report. * * Returns: (transfer full) (nullable): a #GHashTable, or %NULL if there is no data * * Since: 1.5.0 **/ GHashTable * fu_device_report_metadata_pre(FuDevice *self) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); g_autoptr(GHashTable) metadata = NULL; g_return_val_if_fail(FU_IS_DEVICE(self), NULL); /* not implemented */ if (device_class->report_metadata_pre == NULL) return NULL; /* metadata for all devices */ metadata = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); device_class->report_metadata_pre(self, metadata); return g_steal_pointer(&metadata); } /** * fu_device_report_metadata_post: * @self: a #FuDevice * * Collects metadata that would be useful for debugging a failed update report. * * Returns: (transfer full) (nullable): a #GHashTable, or %NULL if there is no data * * Since: 1.5.0 **/ GHashTable * fu_device_report_metadata_post(FuDevice *self) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); g_autoptr(GHashTable) metadata = NULL; g_return_val_if_fail(FU_IS_DEVICE(self), NULL); /* not implemented */ if (device_class->report_metadata_post == NULL) return NULL; /* metadata for all devices */ metadata = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); device_class->report_metadata_post(self, metadata); return g_steal_pointer(&metadata); } /** * fu_device_add_security_attrs: * @self: a #FuDevice * @attrs: a security attribute * * Adds HSI security attributes. * * Since: 1.6.0 **/ void fu_device_add_security_attrs(FuDevice *self, FuSecurityAttrs *attrs) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); g_return_if_fail(FU_IS_DEVICE(self)); /* optional */ if (device_class->add_security_attrs != NULL) return device_class->add_security_attrs(self, attrs); } /** * fu_device_bind_driver: * @self: a #FuDevice * @subsystem: a subsystem string, e.g. `pci` * @driver: a kernel module name, e.g. `tg3` * @error: (nullable): optional return location for an error * * Binds a driver to the device, which normally means the kernel driver takes * control of the hardware. * * Returns: %TRUE if driver was bound. * * Since: 1.5.0 **/ gboolean fu_device_bind_driver(FuDevice *self, const gchar *subsystem, const gchar *driver, GError **error) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(subsystem != NULL, FALSE); g_return_val_if_fail(driver != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* not implemented */ if (device_class->bind_driver == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "binding drivers is not supported by device"); return FALSE; } /* subclass */ return device_class->bind_driver(self, subsystem, driver, error); } /** * fu_device_unbind_driver: * @self: a #FuDevice * @error: (nullable): optional return location for an error * * Unbinds the driver from the device, which normally means the kernel releases * the hardware so it can be used from userspace. * * If there is no driver bound then this function will return with success * without actually doing anything. * * Returns: %TRUE if driver was unbound. * * Since: 1.5.0 **/ gboolean fu_device_unbind_driver(FuDevice *self, GError **error) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* not implemented */ if (device_class->unbind_driver == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unbinding drivers is not supported by device"); return FALSE; } /* subclass */ return device_class->unbind_driver(self, error); } /** * fu_device_get_instance_str: * @self: a #FuDevice * @key: (not nullable): a key, e.g. `REV` * * Looks up an instance ID by a key. * * Returns: (nullable) (transfer none): The instance key, or %NULL. * * Since: 1.8.15 **/ const gchar * fu_device_get_instance_str(FuDevice *self, const gchar *key) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), NULL); g_return_val_if_fail(key != NULL, NULL); if (priv->instance_hash == NULL) return NULL; return g_hash_table_lookup(priv->instance_hash, key); } /** * fu_device_build_vendor_id: * @self: a #FuDevice * @prefix: (not nullable): a prefix string, e.g. `USB` * @value: (nullable): a value, e.g. `0x1234` * * Builds a device vendor ID, if @value is not %NULL. * * Since: 2.0.0 **/ void fu_device_build_vendor_id(FuDevice *self, const gchar *prefix, const gchar *value) { g_autofree gchar *vendor_id = NULL; g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(prefix != NULL); if (value == NULL) return; vendor_id = g_strdup_printf("%s:%s", prefix, value); fwupd_device_add_vendor_id(FWUPD_DEVICE(self), vendor_id); } /** * fu_device_build_vendor_id_u16: * @self: a #FuDevice * @prefix: (not nullable): a prefix string, e.g. `USB` * @value: a value, e.g. 0x1234 * * Builds a device vendor ID, if @value is not 0. * * Since: 2.0.0 **/ void fu_device_build_vendor_id_u16(FuDevice *self, const gchar *prefix, guint16 value) { g_autofree gchar *vendor_id = NULL; g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(prefix != NULL); if (value == 0x0) return; vendor_id = g_strdup_printf("%s:0x%04X", prefix, value); fwupd_device_add_vendor_id(FWUPD_DEVICE(self), vendor_id); } static void fu_device_incorporate_instance_ids(FuDevice *self, FuDevice *donor) { FuDevicePrivate *priv_donor = GET_PRIVATE(donor); if (priv_donor->instance_ids == NULL) return; for (guint i = 0; i < priv_donor->instance_ids->len; i++) { FuDeviceInstanceIdItem *item = g_ptr_array_index(priv_donor->instance_ids, i); if ((item->flags & FU_DEVICE_INSTANCE_FLAG_GENERIC) > 0 && fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_NO_GENERIC_GUIDS)) { continue; } if (item->instance_id != NULL) fu_device_add_instance_id_full(self, item->instance_id, item->flags); else fu_device_add_instance_id_full(self, item->guid, item->flags); } } /** * fu_device_incorporate: * @self: a #FuDevice * @donor: Another #FuDevice * @flag: Some #FuDeviceIncorporateFlags, e.g. %FU_DEVICE_INCORPORATE_FLAG_PHYSICAL_ID * * Copy some properties from the donor object if they have not already been set. * * Since: 2.0.0 **/ void fu_device_incorporate(FuDevice *self, FuDevice *donor, FuDeviceIncorporateFlags flag) { FuDevicePrivate *priv = GET_PRIVATE(self); FuDevicePrivate *priv_donor = GET_PRIVATE(donor); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(FU_IS_DEVICE(donor)); /* do these unconditionally */ if (priv->ctx == NULL && priv_donor->ctx != NULL) fu_device_set_context(self, priv_donor->ctx); if (priv->backend == NULL && priv_donor->backend != NULL) fu_device_set_backend(self, priv_donor->backend); /* bitflags */ if (flag & FU_DEVICE_INCORPORATE_FLAG_BASECLASS) { fwupd_device_incorporate(FWUPD_DEVICE(self), FWUPD_DEVICE(donor)); if (fu_device_get_id(self) != NULL) priv->device_id_valid = TRUE; /* remove the baseclass-added serial number and GUIDs if set */ if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_NO_SERIAL_NUMBER)) fwupd_device_set_serial(FWUPD_DEVICE(self), NULL); } if (flag & FU_DEVICE_INCORPORATE_FLAG_VENDOR) { if (fu_device_get_vendor(self) == NULL && fu_device_get_vendor(donor) != NULL) fu_device_set_vendor(self, fu_device_get_vendor(donor)); } if (flag & FU_DEVICE_INCORPORATE_FLAG_PHYSICAL_ID) { if (priv->physical_id == NULL && priv_donor->physical_id != NULL) fu_device_set_physical_id(self, priv_donor->physical_id); } if (flag & FU_DEVICE_INCORPORATE_FLAG_LOGICAL_ID) { if (priv->logical_id == NULL && priv_donor->logical_id != NULL) fu_device_set_logical_id(self, priv_donor->logical_id); } if (flag & FU_DEVICE_INCORPORATE_FLAG_BACKEND_ID) { if (priv->backend_id == NULL && priv_donor->backend_id != NULL) fu_device_set_backend_id(self, priv_donor->backend_id); } if (flag & FU_DEVICE_INCORPORATE_FLAG_VID) { if (priv->vid == 0x0 && priv_donor->vid != 0x0) fu_device_set_vid(self, priv_donor->vid); } if (flag & FU_DEVICE_INCORPORATE_FLAG_PID) { if (priv->pid == 0x0 && priv_donor->pid != 0x0) fu_device_set_pid(self, priv_donor->pid); } if (flag & FU_DEVICE_INCORPORATE_FLAG_VENDOR_IDS) { GPtrArray *vendor_ids = fu_device_get_vendor_ids(donor); for (guint i = 0; i < vendor_ids->len; i++) { const gchar *vendor_id = g_ptr_array_index(vendor_ids, i); fu_device_add_vendor_id(self, vendor_id); } } if (flag & FU_DEVICE_INCORPORATE_FLAG_REMOVE_DELAY) { if (priv->remove_delay == 0 && priv_donor->remove_delay != 0) fu_device_set_remove_delay(self, priv_donor->remove_delay); } if (flag & FU_DEVICE_INCORPORATE_FLAG_ACQUIESCE_DELAY) { if (priv->acquiesce_delay == 0 && priv_donor->acquiesce_delay != 0) fu_device_set_acquiesce_delay(self, priv_donor->acquiesce_delay); } if (flag & FU_DEVICE_INCORPORATE_FLAG_ICONS) { if (fu_device_get_icons(self)->len == 0) { GPtrArray *icons = fu_device_get_icons(donor); for (guint i = 0; i < icons->len; i++) { const gchar *icon_name = g_ptr_array_index(icons, i); fu_device_add_icon(self, icon_name); } } } if (flag & FU_DEVICE_INCORPORATE_FLAG_EVENTS) { if (priv_donor->events != NULL && donor != priv->proxy) { for (guint i = 0; i < priv_donor->events->len; i++) { FuDeviceEvent *event = g_ptr_array_index(priv_donor->events, i); fu_device_add_event(self, event); } } } if (flag & FU_DEVICE_INCORPORATE_FLAG_UPDATE_ERROR) { if (fu_device_get_update_error(self) == NULL && fu_device_get_update_error(donor) != NULL) { fu_device_set_update_error(self, fu_device_get_update_error(donor)); } } if (flag & FU_DEVICE_INCORPORATE_FLAG_UPDATE_STATE) { if (fu_device_get_update_state(self) == FWUPD_UPDATE_STATE_UNKNOWN && fu_device_get_update_state(donor) != FWUPD_UPDATE_STATE_UNKNOWN) { fu_device_set_update_state(self, fu_device_get_update_state(donor)); } } if (flag & FU_DEVICE_INCORPORATE_FLAG_SUPERCLASS) { gpointer device_class_incorporate_last = NULL; g_autoptr(GPtrArray) class_parents = fu_device_get_common_class_parents(self, donor); /* run every unique ->incorporate() in each subclass */ for (guint i = 0; i < class_parents->len; i++) { FuDeviceClass *device_class = g_ptr_array_index(class_parents, i); if (device_class->incorporate != NULL && device_class->incorporate != device_class_incorporate_last) { device_class->incorporate(self, donor); device_class_incorporate_last = device_class->incorporate; } } } if (flag & FU_DEVICE_INCORPORATE_FLAG_UPDATE_MESSAGE) { if (priv->update_message == NULL && priv_donor->update_message != NULL) fu_device_set_update_message(self, priv_donor->update_message); } if (flag & FU_DEVICE_INCORPORATE_FLAG_UPDATE_IMAGE) { if (priv->update_image == NULL && priv_donor->update_image != NULL) fu_device_set_update_image(self, priv_donor->update_image); } if (flag & FU_DEVICE_INCORPORATE_FLAG_INSTANCE_IDS) fu_device_incorporate_instance_ids(self, donor); if (flag & FU_DEVICE_INCORPORATE_FLAG_GTYPE) { if (priv->specialized_gtype == G_TYPE_INVALID && priv_donor->specialized_gtype != G_TYPE_INVALID) { fu_device_set_specialized_gtype(self, priv_donor->specialized_gtype); } } if (flag & FU_DEVICE_INCORPORATE_FLAG_POSSIBLE_PLUGINS) { for (guint i = 0; i < priv_donor->possible_plugins->len; i++) { const gchar *possible_plugin = g_ptr_array_index(priv_donor->possible_plugins, i); fu_device_add_possible_plugin(self, possible_plugin); } } if (flag & FU_DEVICE_INCORPORATE_FLAG_INSTANCE_KEYS) { if (priv_donor->instance_hash != NULL) { GHashTableIter iter; gpointer key, value; g_hash_table_iter_init(&iter, priv_donor->instance_hash); while (g_hash_table_iter_next(&iter, &key, &value)) { if (fu_device_get_instance_str(self, key) == NULL) fu_device_add_instance_str(self, key, value); } } } /* everything else */ if (flag == FU_DEVICE_INCORPORATE_FLAG_ALL) { GPtrArray *instance_ids = fu_device_get_instance_ids(donor); GPtrArray *parent_physical_ids = fu_device_get_parent_physical_ids(donor); GPtrArray *parent_backend_ids = fu_device_get_parent_backend_ids(donor); /* copy from donor FuDevice if has not already been set */ if (priv_donor->private_flags != NULL) { for (guint i = 0; i < priv_donor->private_flags->len; i++) { GRefString *flag_tmp = g_ptr_array_index(priv_donor->private_flags, i); if (g_ptr_array_find(priv->private_flags_registered, flag_tmp, NULL)) { fu_device_add_private_flag(self, flag_tmp); } } } if (priv->created_usec == 0 && priv_donor->created_usec != 0) fu_device_set_created_usec(self, priv_donor->created_usec); if (priv->modified_usec == 0 && priv_donor->modified_usec != 0) fu_device_set_modified_usec(self, priv_donor->modified_usec); if (priv->equivalent_id == NULL && fu_device_get_equivalent_id(donor) != NULL) fu_device_set_equivalent_id(self, fu_device_get_equivalent_id(donor)); if (priv->fwupd_version == NULL && fu_device_get_fwupd_version(donor) != NULL) fu_device_set_fwupd_version(self, fu_device_get_fwupd_version(donor)); if (priv->update_request_id == NULL && priv_donor->update_request_id != NULL) fu_device_set_update_request_id(self, priv_donor->update_request_id); if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_REFCOUNTED_PROXY) && fu_device_has_private_flag(donor, FU_DEVICE_PRIVATE_FLAG_REFCOUNTED_PROXY)) { if (priv->proxy == NULL && priv_donor->proxy != NULL) fu_device_set_proxy(self, priv_donor->proxy); } if (priv->proxy_guid == NULL && priv_donor->proxy_guid != NULL) fu_device_set_proxy_guid(self, priv_donor->proxy_guid); if (priv->custom_flags == NULL && priv_donor->custom_flags != NULL) fu_device_set_custom_flags(self, priv_donor->custom_flags); if (priv_donor->parent_guids != NULL) { for (guint i = 0; i < priv_donor->parent_guids->len; i++) { const gchar *guid = g_ptr_array_index(priv_donor->parent_guids, i); fu_device_add_parent_guid(self, guid); } } if (parent_physical_ids != NULL) { for (guint i = 0; i < parent_physical_ids->len; i++) { const gchar *tmp = g_ptr_array_index(parent_physical_ids, i); fu_device_add_parent_physical_id(self, tmp); } } if (parent_backend_ids != NULL) { for (guint i = 0; i < parent_backend_ids->len; i++) { const gchar *tmp = g_ptr_array_index(parent_backend_ids, i); fu_device_add_parent_backend_id(self, tmp); } } if (priv_donor->metadata != NULL) { GHashTableIter iter; gpointer key, value; g_hash_table_iter_init(&iter, priv_donor->metadata); while (g_hash_table_iter_next(&iter, &key, &value)) { if (fu_device_get_metadata(self, key) == NULL) fu_device_set_metadata(self, key, value); } } /* call the set_quirk_kv() vfunc for the superclassed object */ for (guint i = 0; i < instance_ids->len; i++) { const gchar *instance_id = g_ptr_array_index(instance_ids, i); g_autofree gchar *guid = fwupd_guid_hash_string(instance_id); fu_device_add_guid_quirks(self, guid); } } } /** * fu_device_replace: * @self: a #FuDevice * @donor: the old #FuDevice * * Copy properties from the old (no-longer-connected) device to the new (connected) device. * * This is typcically called from the daemon device list and should not be called from plugin code. * * Since: 1.9.2 **/ void fu_device_replace(FuDevice *self, FuDevice *donor) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(FU_IS_DEVICE(donor)); /* optional subclass */ if (device_class->replace != NULL) device_class->replace(self, donor); } /** * fu_device_incorporate_flag: * @self: a #FuDevice * @donor: another device * @flag: device flags * * Copy the value of a specific flag from the donor object. * * Since: 1.3.5 **/ void fu_device_incorporate_flag(FuDevice *self, FuDevice *donor, FwupdDeviceFlags flag) { if (fu_device_has_flag(donor, flag) && !fu_device_has_flag(self, flag)) { g_debug("donor set %s", fwupd_device_flag_to_string(flag)); fu_device_add_flag(self, flag); } else if (!fu_device_has_flag(donor, flag) && fu_device_has_flag(self, flag)) { g_debug("donor unset %s", fwupd_device_flag_to_string(flag)); fu_device_remove_flag(self, flag); } } /** * fu_device_incorporate_from_component: (skip): * @self: a device * @component: a Xmlb node * * Copy all properties from the donor AppStream component. * * Since: 1.2.4 **/ void fu_device_incorporate_from_component(FuDevice *self, XbNode *component) { const gchar *tmp; g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(XB_IS_NODE(component)); tmp = xb_node_query_text(component, "custom/value[@key='LVFS::UpdateMessage']", NULL); if (tmp != NULL) fu_device_set_update_message(self, tmp); tmp = xb_node_query_text(component, "custom/value[@key='LVFS::UpdateImage']", NULL); if (tmp != NULL) fu_device_set_update_image(self, tmp); } static void fu_device_ensure_from_component_name(FuDevice *self, XbNode *component) { const gchar *name = NULL; /* copy 1:1 */ name = xb_node_query_text(component, "name", NULL); if (name != NULL) { fu_device_set_name(self, name); fu_device_remove_private_flag(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_NAME); } } static void fu_device_ensure_from_component_vendor(FuDevice *self, XbNode *component) { const gchar *vendor = NULL; /* copy 1:1 */ vendor = xb_node_query_text(component, "developer_name", NULL); if (vendor != NULL) { fu_device_set_vendor(self, vendor); fu_device_remove_private_flag(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_VENDOR); } } static void fu_device_ensure_from_component_signed(FuDevice *self, XbNode *component) { const gchar *value = NULL; /* already set, possibly by a quirk */ if (fu_device_has_flag(self, FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD) || fu_device_has_flag(self, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD)) return; /* copy 1:1 */ value = xb_node_query_text(component, "custom/value[@key='LVFS::DeviceIntegrity']", NULL); if (value != NULL) { if (g_strcmp0(value, "signed") == 0) { fu_device_add_flag(self, FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); } else if (g_strcmp0(value, "unsigned") == 0) { fu_device_add_flag(self, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); } else { g_warning("payload value unexpected: %s, expected signed|unsigned", value); } fu_device_remove_private_flag(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_VENDOR); } } static void fu_device_ensure_from_component_icon(FuDevice *self, XbNode *component) { const gchar *icon = NULL; /* copy 1:1 */ icon = xb_node_query_text(component, "icon", NULL); if (icon != NULL) { fu_device_add_icon(self, icon); fu_device_remove_private_flag(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_ICON); } } static void fu_device_ensure_from_component_flags(FuDevice *self, XbNode *component) { const gchar *tmp = xb_node_query_text(component, "custom/value[@key='LVFS::DeviceFlags']", NULL); if (tmp != NULL) { g_auto(GStrv) hints = g_strsplit(tmp, ",", -1); for (guint i = 0; hints[i] != NULL; i++) fu_device_set_custom_flag(self, hints[i]); fu_device_remove_private_flag(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_FLAGS); } } static const gchar * fu_device_category_to_name(const gchar *cat) { if (g_strcmp0(cat, "X-EmbeddedController") == 0) return "Embedded Controller"; if (g_strcmp0(cat, "X-ManagementEngine") == 0) return "Intel Management Engine"; if (g_strcmp0(cat, "X-CorporateManagementEngine") == 0) return "Intel Management Engine"; if (g_strcmp0(cat, "X-ConsumerManagementEngine") == 0) return "Intel Management Engine"; if (g_strcmp0(cat, "X-ThunderboltController") == 0) return "Thunderbolt Controller"; if (g_strcmp0(cat, "X-PlatformSecurityProcessor") == 0) return "Platform Security Processor"; if (g_strcmp0(cat, "X-CpuMicrocode") == 0) return "CPU Microcode"; if (g_strcmp0(cat, "X-Battery") == 0) return "Battery"; if (g_strcmp0(cat, "X-Camera") == 0) return "Camera"; if (g_strcmp0(cat, "X-TPM") == 0) return "TPM"; if (g_strcmp0(cat, "X-Touchpad") == 0) return "Touchpad"; if (g_strcmp0(cat, "X-Mouse") == 0) return "Mouse"; if (g_strcmp0(cat, "X-Keyboard") == 0) return "Keyboard"; if (g_strcmp0(cat, "X-VideoDisplay") == 0) return "Display"; if (g_strcmp0(cat, "X-BaseboardManagementController") == 0) return "BMC"; if (g_strcmp0(cat, "X-UsbReceiver") == 0) return "USB Receiver"; if (g_strcmp0(cat, "X-Gpu") == 0) return "GPU"; if (g_strcmp0(cat, "X-Dock") == 0) return "Dock"; if (g_strcmp0(cat, "X-UsbDock") == 0) return "USB Dock"; if (g_strcmp0(cat, "X-FingerprintReader") == 0) return "Fingerprint Reader"; if (g_strcmp0(cat, "X-GraphicsTablet") == 0) return "Graphics Tablet"; if (g_strcmp0(cat, "X-InputController") == 0) return "Input Controller"; if (g_strcmp0(cat, "X-Headphones") == 0) return "Headphones"; if (g_strcmp0(cat, "X-Headset") == 0) return "Headset"; return NULL; } static void fu_device_ensure_from_component_name_category(FuDevice *self, XbNode *component) { const gchar *name = NULL; g_autoptr(GPtrArray) cats = NULL; /* get AppStream and safe-compat categories */ cats = xb_node_query(component, "categories/category|X-categories/category", 0, NULL); if (cats == NULL) return; for (guint i = 0; i < cats->len; i++) { XbNode *n = g_ptr_array_index(cats, i); name = fu_device_category_to_name(xb_node_get_text(n)); if (name != NULL) break; } if (name != NULL) { fu_device_set_name(self, name); fu_device_remove_private_flag(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_NAME_CATEGORY); } /* batteries updated using capsules should ignore the system power restriction */ if (g_strcmp0(fu_device_get_plugin(self), "uefi_capsule") == 0) { gboolean is_battery = FALSE; for (guint i = 0; i < cats->len; i++) { XbNode *n = g_ptr_array_index(cats, i); if (g_strcmp0(xb_node_get_text(n), "X-Battery") == 0) { is_battery = TRUE; break; } } if (is_battery) { g_info("ignoring system power for %s battery", fu_device_get_id(self)); fu_device_add_private_flag(self, FU_DEVICE_PRIVATE_FLAG_IGNORE_SYSTEM_POWER); } } } static void _g_ptr_array_reverse(GPtrArray *array) { guint last_idx = array->len - 1; for (guint i = 0; i < array->len / 2; i++) { gpointer tmp = array->pdata[i]; array->pdata[i] = array->pdata[last_idx - i]; array->pdata[last_idx - i] = tmp; } } static void fu_device_ensure_from_component_verfmt(FuDevice *self, XbNode *component) { FwupdVersionFormat verfmt = FWUPD_VERSION_FORMAT_UNKNOWN; g_autoptr(GPtrArray) verfmts = NULL; /* get metadata */ verfmts = xb_node_query(component, "custom/value[@key='LVFS::VersionFormat']", 0, NULL); if (verfmts == NULL) return; _g_ptr_array_reverse(verfmts); for (guint i = 0; i < verfmts->len; i++) { XbNode *value = g_ptr_array_index(verfmts, i); verfmt = fwupd_version_format_from_string(xb_node_get_text(value)); if (verfmt != FWUPD_VERSION_FORMAT_UNKNOWN) break; } /* found and different to existing */ if (verfmt != FWUPD_VERSION_FORMAT_UNKNOWN && fu_device_get_version_format(self) != verfmt) { fu_device_set_version_format(self, verfmt); if (fu_device_get_version_raw(self) != 0x0) { g_autofree gchar *version = NULL; version = fu_version_from_uint32(fu_device_get_version_raw(self), verfmt); fu_device_set_version(self, version); } if (fu_device_get_version_lowest_raw(self) != 0x0) { g_autofree gchar *version = NULL; version = fu_version_from_uint32(fu_device_get_version_lowest_raw(self), verfmt); fu_device_set_version_lowest(self, version); } if (fu_device_get_version_bootloader_raw(self) != 0x0) { g_autofree gchar *version = NULL; version = fu_version_from_uint32(fu_device_get_version_bootloader_raw(self), verfmt); fu_device_set_version_bootloader(self, version); } } /* do not try to do this again */ fu_device_remove_private_flag(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_VERFMT); } /** * fu_device_ensure_from_component: (skip): * @self: a device * @component: a #XbNode * * Ensure all properties from the donor AppStream component as required. * * Since: 1.8.13 **/ void fu_device_ensure_from_component(FuDevice *self, XbNode *component) { g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(XB_IS_NODE(component)); /* set the name */ if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_NAME)) fu_device_ensure_from_component_name(self, component); if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_NAME_CATEGORY)) fu_device_ensure_from_component_name_category(self, component); if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_ICON)) fu_device_ensure_from_component_icon(self, component); if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_VENDOR)) fu_device_ensure_from_component_vendor(self, component); if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_SIGNED)) fu_device_ensure_from_component_signed(self, component); if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_VERFMT)) fu_device_ensure_from_component_verfmt(self, component); if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_FLAGS)) fu_device_ensure_from_component_flags(self, component); } /** * fu_device_ensure_from_release: * @self: a #FuDevice * @rel: (not nullable): a #XbNode * * Ensure all properties from the donor AppStream release as required. * * Since: 2.0.5 **/ void fu_device_ensure_from_release(FuDevice *self, XbNode *rel) { g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(XB_IS_NODE(rel)); /* optionally filter by device checksum */ if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_MD_ONLY_CHECKSUM)) { gboolean valid = FALSE; g_autoptr(GPtrArray) device_checksums = NULL; if (fu_device_get_checksums(self)->len == 0) return; device_checksums = xb_node_query(rel, "checksum[@target='device']", 0, NULL); for (guint i = 0; device_checksums != NULL && i < device_checksums->len; i++) { XbNode *device_checksum = g_ptr_array_index(device_checksums, i); if (fu_device_has_checksum(self, xb_node_get_text(device_checksum))) { valid = TRUE; break; } } if (!valid) return; } /* set the version */ if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_VERSION)) { const gchar *version = xb_node_get_attr(rel, "version"); if (version != NULL) { fu_device_set_version(self, version); fu_device_remove_private_flag(self, FU_DEVICE_PRIVATE_FLAG_MD_SET_VERSION); } } } /** * fu_device_emit_request: * @self: a device * @request: a request * @progress: (nullable): a #FuProgress * @error: (nullable): optional return location for an error * * Emit a request from a plugin to the client. * * If the device is emulated then this request is ignored. * * Since: 1.9.8 **/ gboolean fu_device_emit_request(FuDevice *self, FwupdRequest *request, FuProgress *progress, GError **error) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(FWUPD_IS_REQUEST(request), FALSE); g_return_val_if_fail(progress == NULL || FU_IS_PROGRESS(progress), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); #ifndef SUPPORTED_BUILD /* nag the developer */ if (fwupd_request_has_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE) && !fu_device_has_request_flag(self, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "request %s emitted but device %s [%s] does not set " "FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE", fwupd_request_get_id(request), fu_device_get_id(self), fu_device_get_plugin(self)); return FALSE; } if (!fwupd_request_has_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE) && !fu_device_has_request_flag(self, FWUPD_REQUEST_FLAG_NON_GENERIC_MESSAGE)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "request %s is not a GENERIC_MESSAGE and device %s [%s] does not set " "FWUPD_REQUEST_FLAG_NON_GENERIC_MESSAGE", fwupd_request_get_id(request), fu_device_get_id(self), fu_device_get_plugin(self)); return FALSE; } #endif /* sanity check */ if (fwupd_request_get_kind(request) == FWUPD_REQUEST_KIND_UNKNOWN) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "a request must have an assigned kind"); return FALSE; } if (fwupd_request_get_id(request) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "a request must have an assigned ID"); return FALSE; } if (fwupd_request_get_kind(request) >= FWUPD_REQUEST_KIND_LAST) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid request kind"); return FALSE; } /* already cancelled */ if (progress != NULL && fu_progress_has_flag(progress, FU_PROGRESS_FLAG_NO_SENDER)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no sender, and so cannot process request"); return FALSE; } /* ignore */ if (fu_device_has_flag(self, FWUPD_DEVICE_FLAG_EMULATED)) { g_info("ignoring device %s request of %s as emulated", fu_device_get_id(self), fwupd_request_get_id(request)); return TRUE; } /* ensure set */ fwupd_request_set_device_id(request, fu_device_get_id(self)); /* for compatibility with older clients */ if (fwupd_request_get_kind(request) == FWUPD_REQUEST_KIND_POST) { fu_device_set_update_message(self, fwupd_request_get_message(request)); fu_device_set_update_image(self, fwupd_request_get_image(request)); } /* proxy to the engine */ if (progress != NULL) { fu_progress_set_status(progress, FWUPD_STATUS_WAITING_FOR_USER); } else if (priv->progress != NULL) { g_debug("using fallback progress"); fu_progress_set_status(priv->progress, FWUPD_STATUS_WAITING_FOR_USER); } else { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no progress"); return FALSE; } g_signal_emit(self, signals[SIGNAL_REQUEST], 0, request); if (fwupd_request_get_kind(request) < FWUPD_REQUEST_KIND_LAST) priv->request_cnts[fwupd_request_get_kind(request)]++; return TRUE; } static void fu_device_flags_notify_cb(FuDevice *self, GParamSpec *pspec, gpointer user_data) { FuDevicePrivate *priv = GET_PRIVATE(self); /* emulated device reinstalling do not need a replug or shutdown */ if (fu_device_has_flag(self, FWUPD_DEVICE_FLAG_EMULATED) && fu_device_has_flag(self, FWUPD_DEVICE_FLAG_NEEDS_REBOOT)) { g_debug("removing needs-reboot for emulated device"); fu_device_remove_flag(self, FWUPD_DEVICE_FLAG_NEEDS_REBOOT); } if (fu_device_has_flag(self, FWUPD_DEVICE_FLAG_EMULATED) && fu_device_has_flag(self, FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN)) { g_debug("removing needs-shutdown for emulated device"); fu_device_remove_flag(self, FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN); } /* we only inhibit when the flags contains UPDATABLE, and that might be discovered by * probing the hardware *after* the battery level has been set */ if (priv->inhibits != NULL) fu_device_ensure_inhibits(self); } static void fu_device_ensure_instance_hash(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); if (priv->instance_hash != NULL) return; priv->instance_hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); } /** * fu_device_add_instance_str: * @self: a #FuDevice * @key: (not nullable): string * @value: (nullable): value * * Assign a value for the @key. * * Since: 1.7.7 **/ void fu_device_add_instance_str(FuDevice *self, const gchar *key, const gchar *value) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(key != NULL); fu_device_ensure_instance_hash(self); g_hash_table_insert(priv->instance_hash, g_strdup(key), g_strdup(value)); } static gboolean fu_device_strsafe_instance_id_is_valid_char(gchar c) { switch (c) { case ' ': case '_': case '&': case '/': case '\\': case '-': case '(': case ')': case ',': return FALSE; default: break; } return g_ascii_isprint(c); } /* NOTE: we can't use fu_strsafe as this behavior is now effectively ABI */ static gchar * fu_device_strsafe_instance_id(const gchar *str) { g_autoptr(GString) tmp = g_string_new(NULL); gboolean has_content = FALSE; /* sanity check */ if (str == NULL) return NULL; /* use - to replace problematic chars -- but only once per section */ for (guint i = 0; str[i] != '\0'; i++) { gchar c = str[i]; if (!fu_device_strsafe_instance_id_is_valid_char(c)) { if (has_content) { g_string_append_c(tmp, '-'); has_content = FALSE; } } else { g_string_append_c(tmp, c); has_content = TRUE; } } /* remove any trailing replacements */ if (tmp->len > 0 && tmp->str[tmp->len - 1] == '-') g_string_truncate(tmp, tmp->len - 1); /* nothing left! */ if (tmp->len == 0) return NULL; /* success */ return g_string_free(g_steal_pointer(&tmp), FALSE); } /** * fu_device_add_instance_strsafe: * @self: a #FuDevice * @key: (not nullable): string * @value: (nullable): value * * Assign a sanitized value for the @key. * * Since: 1.7.7 **/ void fu_device_add_instance_strsafe(FuDevice *self, const gchar *key, const gchar *value) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(key != NULL); fu_device_ensure_instance_hash(self); g_hash_table_insert(priv->instance_hash, g_strdup(key), fu_device_strsafe_instance_id(value)); } /** * fu_device_add_instance_strup: * @self: a #FuDevice * @key: (not nullable): string * @value: (nullable): value * * Assign a uppercase value for the @key. * * Since: 1.7.7 **/ void fu_device_add_instance_strup(FuDevice *self, const gchar *key, const gchar *value) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(key != NULL); fu_device_ensure_instance_hash(self); g_hash_table_insert(priv->instance_hash, g_strdup(key), value != NULL ? g_utf8_strup(value, -1) : NULL); } /** * fu_device_add_instance_u4: * @self: a #FuDevice * @key: (not nullable): string * @value: value * * Assign a value to the @key, which is padded as %1X. * * Since: 1.7.7 **/ void fu_device_add_instance_u4(FuDevice *self, const gchar *key, guint8 value) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(key != NULL); fu_device_ensure_instance_hash(self); g_hash_table_insert(priv->instance_hash, g_strdup(key), g_strdup_printf("%01X", value)); } /** * fu_device_add_instance_u8: * @self: a #FuDevice * @key: (not nullable): string * @value: value * * Assign a value to the @key, which is padded as %2X. * * Since: 1.7.7 **/ void fu_device_add_instance_u8(FuDevice *self, const gchar *key, guint8 value) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(key != NULL); fu_device_ensure_instance_hash(self); g_hash_table_insert(priv->instance_hash, g_strdup(key), g_strdup_printf("%02X", value)); } /** * fu_device_add_instance_u16: * @self: a #FuDevice * @key: (not nullable): string * @value: value * * Assign a value to the @key, which is padded as %4X. * * Since: 1.7.7 **/ void fu_device_add_instance_u16(FuDevice *self, const gchar *key, guint16 value) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(key != NULL); fu_device_ensure_instance_hash(self); g_hash_table_insert(priv->instance_hash, g_strdup(key), g_strdup_printf("%04X", value)); } /** * fu_device_add_instance_u32: * @self: a #FuDevice * @key: (not nullable): string * @value: value * * Assign a value to the @key, which is padded as %8X. * * Since: 1.7.7 **/ void fu_device_add_instance_u32(FuDevice *self, const gchar *key, guint32 value) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(key != NULL); fu_device_ensure_instance_hash(self); g_hash_table_insert(priv->instance_hash, g_strdup(key), g_strdup_printf("%08X", value)); } /** * fu_device_build_instance_id: * @self: a #FuDevice * @error: (nullable): optional return location for an error * @subsystem: (not nullable): subsystem, e.g. `NVME` * @...: pairs of string key values, ending with %NULL * * Creates an instance ID from a prefix and some key values. * If the key value cannot be found, the parent and then proxy is also queried. * * If any of the key values remain unset then no instance ID is added. * * fu_device_add_instance_str(dev, "VID", "1234"); * fu_device_add_instance_u16(dev, "PID", 5678); * if (!fu_device_build_instance_id(dev, &error, "NVME", "VID", "PID", NULL)) * g_warning("failed to add ID: %s", error->message); * * Returns: %TRUE if the instance ID was added. * * Since: 1.7.7 **/ gboolean fu_device_build_instance_id(FuDevice *self, GError **error, const gchar *subsystem, ...) { FuDevice *parent = fu_device_get_parent(self); FuDevicePrivate *priv = GET_PRIVATE(self); gboolean ret = TRUE; va_list args; g_autoptr(GString) str = g_string_new(subsystem); g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(subsystem != NULL, FALSE); va_start(args, subsystem); for (guint i = 0;; i++) { const gchar *key = va_arg(args, const gchar *); const gchar *value; if (key == NULL) break; value = fu_device_get_instance_str(self, key); if (value == NULL && parent != NULL) value = fu_device_get_instance_str(parent, key); if (value == NULL && priv->proxy != NULL) value = fu_device_get_instance_str(priv->proxy, key); if (value == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no value for %s", key); ret = FALSE; break; } g_string_append(str, i == 0 ? "\\" : "&"); g_string_append_printf(str, "%s_%s", key, value); } va_end(args); /* we set an error above */ if (!ret) return FALSE; /* success */ fu_device_add_instance_id(self, str->str); return TRUE; } /** * fu_device_build_instance_id_full: * @self: a #FuDevice * @flags: instance ID flags, e.g. %FU_DEVICE_INSTANCE_FLAG_QUIRKS * @error: (nullable): optional return location for an error * @subsystem: (not nullable): subsystem, e.g. `NVME` * @...: pairs of string key values, ending with %NULL * * Creates an instance ID with specific flags from a prefix and some key values. If any of the key * values are unset then no instance ID is added. * * Returns: %TRUE if the instance ID was added. * * Since: 1.9.8 **/ gboolean fu_device_build_instance_id_full(FuDevice *self, FuDeviceInstanceFlag flags, GError **error, const gchar *subsystem, ...) { FuDevicePrivate *priv = GET_PRIVATE(self); gboolean ret = TRUE; va_list args; g_autoptr(GString) str = g_string_new(subsystem); g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(subsystem != NULL, FALSE); if (priv->instance_hash == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no instance hash values defined"); return FALSE; } va_start(args, subsystem); for (guint i = 0;; i++) { const gchar *key = va_arg(args, const gchar *); const gchar *value; if (key == NULL) break; value = g_hash_table_lookup(priv->instance_hash, key); if (value == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no value for %s", key); ret = FALSE; break; } g_string_append(str, i == 0 ? "\\" : "&"); g_string_append_printf(str, "%s_%s", key, value); } va_end(args); /* we set an error above */ if (!ret) return FALSE; /* success */ fu_device_add_instance_id_full(self, str->str, flags); return TRUE; } /** * fu_device_security_attr_new: * @self: a #FuDevice * @appstream_id: (nullable): the AppStream component ID, e.g. `com.intel.BiosGuard` * * Creates a new #FwupdSecurityAttr for this specific device. * * Returns: (transfer full): a #FwupdSecurityAttr * * Since: 1.8.4 **/ FwupdSecurityAttr * fu_device_security_attr_new(FuDevice *self, const gchar *appstream_id) { FuDevicePrivate *priv = fu_device_get_instance_private(self); g_autoptr(FwupdSecurityAttr) attr = NULL; g_return_val_if_fail(FU_IS_DEVICE(self), NULL); g_return_val_if_fail(appstream_id != NULL, NULL); attr = fu_security_attr_new(priv->ctx, appstream_id); fwupd_security_attr_set_plugin(attr, fu_device_get_plugin(FU_DEVICE(self))); fwupd_security_attr_add_guids(attr, fu_device_get_guids(FU_DEVICE(self))); /* if the device is a child of the host firmware then add those GUIDs too */ if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE_CHILD)) { FuDevice *msf_device = fu_device_get_parent(self); if (msf_device != NULL) { GPtrArray *guids = fu_device_get_guids(msf_device); for (guint i = 0; i < guids->len; i++) { const gchar *guid = g_ptr_array_index(guids, i); fwupd_security_attr_add_guid(attr, guid); } } } return g_steal_pointer(&attr); } static void fu_device_ensure_events(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); if (priv->events != NULL) return; priv->events = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); } /** * fu_device_add_event: * @self: a #FuDevice * @event: (not nullable): a #FuDeviceEvent * * Adds an event to the device. * * Since: 2.0.0 **/ void fu_device_add_event(FuDevice *self, FuDeviceEvent *event) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(FU_IS_DEVICE_EVENT(event)); /* redirect */ if (priv->target != NULL) { fu_device_add_event(priv->target, event); return; } fu_device_ensure_events(self); g_ptr_array_add(priv->events, g_object_ref(event)); } /** * fu_device_save_event: * @self: a #FuDevice * @id: (nullable): the event ID, e.g. `usb:AA:AA:06` * * Creates a new event with a specific ID and adds it to the device. * * Returns: (transfer none): a #FuDeviceEvent * * Since: 2.0.0 **/ FuDeviceEvent * fu_device_save_event(FuDevice *self, const gchar *id) { FuDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(FuDeviceEvent) event = NULL; g_return_val_if_fail(FU_IS_DEVICE(self), NULL); g_return_val_if_fail(id != NULL, NULL); /* redirect */ if (priv->target != NULL) return fu_device_save_event(priv->target, id); /* success */ event = fu_device_event_new(id); fu_device_add_event(self, event); g_debug("saved event %s", id); return event; } /** * fu_device_load_event: * @self: a #FuDevice * @id: (not nullable): the event ID, e.g. `usb:AA:AA:06` * @error: (nullable): optional return location for an error * * Loads a new event with a specific ID from the device. * * Returns: (transfer none) (nullable): a #FuDeviceEvent * * Since: 2.0.0 **/ FuDeviceEvent * fu_device_load_event(FuDevice *self, const gchar *id, GError **error) { FuDevicePrivate *priv = GET_PRIVATE(self); g_autofree gchar *id_hash = NULL; g_return_val_if_fail(FU_IS_DEVICE(self), NULL); g_return_val_if_fail(id != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* redirect */ if (priv->target != NULL) return fu_device_load_event(priv->target, id, error); /* sanity check */ if (priv->events == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no events loaded"); return NULL; } /* reset back to the beginning */ if (priv->event_idx >= priv->events->len) { g_debug("resetting event index"); priv->event_idx = 0; } /* look for the next event in the sequence */ id_hash = fu_device_event_build_id(id); for (guint i = priv->event_idx; i < priv->events->len; i++) { FuDeviceEvent *event = g_ptr_array_index(priv->events, i); if (g_strcmp0(fu_device_event_get_id(event), id_hash) == 0) { priv->event_idx = i + 1; return event; } } /* look for *any* event that matches */ for (guint i = 0; i < priv->events->len; i++) { FuDeviceEvent *event = g_ptr_array_index(priv->events, i); if (g_strcmp0(fu_device_event_get_id(event), id_hash) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "found out-of-order event %s at position %u", id, i); return NULL; } } /* nothing found */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no event with ID %s", id); return NULL; } /** * fu_device_get_events: * @self: a #FuDevice * * Gets all the #FuDeviceEvent objects added with fu_device_add_event(). * * These events should be added by #FuDevice subclasses to enable the daemon to emulate a specific * device type. * * Returns: (transfer none) (element-type FuDeviceEvent): events * * Since: 2.0.0 **/ GPtrArray * fu_device_get_events(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DEVICE(self), NULL); /* redirect */ if (priv->target != NULL) return fu_device_get_events(priv->target); fu_device_ensure_events(self); return priv->events; } /** * fu_device_clear_events: * @self: a #FuDevice * * Clears all the #FuDeviceEvent objects added with fu_device_add_event(), typically after saving * the device to an emulation. * * Since: 2.0.0 **/ void fu_device_clear_events(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); /* redirect */ if (priv->target != NULL) { fu_device_clear_events(priv->target); return; } if (priv->events == NULL) return; g_ptr_array_set_size(priv->events, 0); priv->event_idx = 0; } /** * fu_device_set_target: * @self: a #FuDevice * @target: a #FuDevice * * Sets the target device where #FuDeviceEvent objects added to @self should actually be added. * * Any existing events added to @self are added immediately to @target. * * Since: 2.0.0 **/ void fu_device_set_target(FuDevice *self, FuDevice *target) { FuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DEVICE(self)); g_return_if_fail(FU_IS_DEVICE(target)); fu_device_incorporate(target, self, FU_DEVICE_INCORPORATE_FLAG_EVENTS); g_set_object(&priv->target, target); } /* private; used to save an emulated device */ void fu_device_add_json(FuDevice *self, JsonBuilder *builder, FwupdCodecFlags flags) { FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); if (fu_device_get_created_usec(self) != 0) { #if GLIB_CHECK_VERSION(2, 80, 0) g_autoptr(GDateTime) dt = g_date_time_new_from_unix_utc_usec(fu_device_get_created_usec(self)); #else g_autoptr(GDateTime) dt = g_date_time_new_from_unix_utc( fu_device_get_created_usec(self) / G_USEC_PER_SEC); #endif g_autofree gchar *str = g_date_time_format_iso8601(dt); json_builder_set_member_name(builder, "Created"); json_builder_add_string_value(builder, str); } /* subclassed */ if (device_class->add_json != NULL) device_class->add_json(self, builder, flags); } /* private; used to load an emulated device */ gboolean fu_device_from_json(FuDevice *self, JsonObject *json_object, GError **error) { const gchar *tmp; FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(self); tmp = json_object_get_string_member_with_default(json_object, "Created", NULL); if (tmp != NULL) { g_autoptr(GDateTime) dt = g_date_time_new_from_iso8601(tmp, NULL); #if GLIB_CHECK_VERSION(2, 80, 0) if (dt != NULL) fu_device_set_created_usec(self, g_date_time_to_unix_usec(dt)); #else if (dt != NULL) { fu_device_set_created_usec(self, g_date_time_to_unix(dt) * G_USEC_PER_SEC); } #endif } /* subclassed */ if (device_class->from_json != NULL) { if (!device_class->from_json(self, json_object, error)) return FALSE; } /* success */ return TRUE; } static void fu_device_dispose(GObject *object) { FuDevice *self = FU_DEVICE(object); FuDevicePrivate *priv = GET_PRIVATE(self); g_clear_object(&priv->ctx); g_clear_object(&priv->target); G_OBJECT_CLASS(fu_device_parent_class)->dispose(object); } static void fu_device_class_init(FuDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GParamSpec *pspec; object_class->dispose = fu_device_dispose; object_class->finalize = fu_device_finalize; object_class->get_property = fu_device_get_property; object_class->set_property = fu_device_set_property; device_class->to_string = fu_device_to_string_impl; device_class->register_flags = fu_device_register_flags; /** * FuDevice::child-added: * @self: the #FuDevice instance that emitted the signal * @device: the #FuDevice child * * The ::child-added signal is emitted when a device has been added as a child. * * Since: 1.0.8 **/ signals[SIGNAL_CHILD_ADDED] = g_signal_new("child-added", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(FuDeviceClass, child_added), NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, FU_TYPE_DEVICE); /** * FuDevice::child-removed: * @self: the #FuDevice instance that emitted the signal * @device: the #FuDevice child * * The ::child-removed signal is emitted when a device has been removed as a child. * * Since: 1.0.8 **/ signals[SIGNAL_CHILD_REMOVED] = g_signal_new("child-removed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(FuDeviceClass, child_removed), NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, FU_TYPE_DEVICE); /** * FuDevice::request: * @self: the #FuDevice instance that emitted the signal * @request: the #FwupdRequest * * The ::request signal is emitted when the device needs interactive action from the user. * * Since: 1.6.2 **/ signals[SIGNAL_REQUEST] = g_signal_new("request", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(FuDeviceClass, request), NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, FWUPD_TYPE_REQUEST); /** * FuDevice:physical-id: * * The device physical ID. * * Since: 1.1.2 */ pspec = g_param_spec_string("physical-id", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_PHYSICAL_ID, pspec); /** * FuDevice:logical-id: * * The device logical ID. * * Since: 1.1.2 */ pspec = g_param_spec_string("logical-id", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_LOGICAL_ID, pspec); /** * FuDevice:backend-id: * * The device backend ID. * * Since: 1.5.8 */ pspec = g_param_spec_string("backend-id", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_BACKEND_ID, pspec); /** * FuDevice:equivalent-id: * * The device equivalent ID. * * Since: 2.0.0 */ pspec = g_param_spec_string("equivalent-id", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_EQUIVALENT_ID, pspec); /** * FuDevice:update-message: * * The device update message. * * Since: 2.0.0 */ pspec = g_param_spec_string("update-message", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_UPDATE_MESSAGE, pspec); /** * FuDevice:update-image: * * The update image for the device. * * Since: 2.0.0 */ pspec = g_param_spec_string("update-image", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_UPDATE_IMAGE, pspec); /** * FuDevice:context: * * The #FuContext to use. * * Since: 1.6.0 */ pspec = g_param_spec_object("context", NULL, NULL, FU_TYPE_CONTEXT, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_CONTEXT, pspec); /** * FuDevice:backend: * * The #FuBackend that created the device. * * Since: 2.0.0 */ pspec = g_param_spec_object("backend", NULL, NULL, FU_TYPE_BACKEND, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_BACKEND, pspec); /** * FuDevice:proxy: * * The device proxy to use. * * Since: 1.4.1 */ pspec = g_param_spec_object("proxy", NULL, NULL, FU_TYPE_DEVICE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_PROXY, pspec); /** * FuDevice:parent: * * The device parent. * * Since: 1.0.8 */ pspec = g_param_spec_object("parent", NULL, NULL, FU_TYPE_DEVICE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_PARENT, pspec); /** * FuDevice:private-flags: * * The device private flags. * * Since: 1.9.1 */ pspec = g_param_spec_uint64("private-flags", NULL, NULL, 0, G_MAXUINT64, 0, G_PARAM_READABLE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_PRIVATE_FLAGS, pspec); /** * FuDevice:vid: * * The device vendor ID. * * Since: 2.0.0 */ pspec = g_param_spec_uint("vid", NULL, NULL, 0, G_MAXUINT16, 0, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_VID, pspec); /** * FuDevice:pid: * * The device product ID. * * Since: 2.0.0 */ pspec = g_param_spec_uint("pid", NULL, NULL, 0, G_MAXUINT16, 0, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_PID, pspec); } static void fu_device_init(FuDevice *self) { FuDevicePrivate *priv = GET_PRIVATE(self); priv->order = G_MAXINT; priv->possible_plugins = g_ptr_array_new_with_free_func(g_free); priv->acquiesce_delay = 50; /* ms */ priv->notify_flags_handler_id = g_signal_connect(FWUPD_DEVICE(self), "notify::flags", G_CALLBACK(fu_device_flags_notify_cb), NULL); } static void fu_device_finalize(GObject *object) { FuDevice *self = FU_DEVICE(object); FuDevicePrivate *priv = GET_PRIVATE(self); if (priv->progress != NULL) g_object_unref(priv->progress); if (priv->proxy != NULL) { if (priv->notify_flags_proxy_id != 0) g_signal_handler_disconnect(priv->proxy, priv->notify_flags_proxy_id); if (fu_device_has_private_flag(self, FU_DEVICE_PRIVATE_FLAG_REFCOUNTED_PROXY)) { g_object_unref(priv->proxy); } else { g_object_remove_weak_pointer(G_OBJECT(priv->proxy), (gpointer *)&priv->proxy); } } if (priv->backend != NULL) g_object_remove_weak_pointer(G_OBJECT(priv->backend), (gpointer *)&priv->backend); if (priv->poll_id != 0) g_source_remove(priv->poll_id); if (priv->metadata != NULL) g_hash_table_unref(priv->metadata); if (priv->inhibits != NULL) g_hash_table_unref(priv->inhibits); if (priv->instance_hash != NULL) g_hash_table_unref(priv->instance_hash); if (priv->parent_physical_ids != NULL) g_ptr_array_unref(priv->parent_physical_ids); if (priv->parent_backend_ids != NULL) g_ptr_array_unref(priv->parent_backend_ids); if (priv->events != NULL) g_ptr_array_unref(priv->events); if (priv->retry_recs != NULL) g_ptr_array_unref(priv->retry_recs); if (priv->instance_ids != NULL) g_ptr_array_unref(priv->instance_ids); if (priv->parent_guids != NULL) g_ptr_array_unref(priv->parent_guids); if (priv->private_flags != NULL) g_ptr_array_unref(priv->private_flags); if (priv->private_flags_registered != NULL) g_ptr_array_unref(priv->private_flags_registered); g_ptr_array_unref(priv->possible_plugins); g_free(priv->equivalent_id); g_free(priv->physical_id); g_free(priv->logical_id); g_free(priv->backend_id); g_free(priv->update_request_id); g_free(priv->update_message); g_free(priv->update_image); g_free(priv->fwupd_version); g_free(priv->proxy_guid); g_free(priv->custom_flags); G_OBJECT_CLASS(fu_device_parent_class)->finalize(object); } /** * fu_device_new: * * Creates a new #Fudevice * * Since: 1.8.2 **/ FuDevice * fu_device_new(FuContext *ctx) { FuDevice *self = g_object_new(FU_TYPE_DEVICE, "context", ctx, NULL); return FU_DEVICE(self); } fwupd-2.0.10/libfwupdplugin/fu-device.h000066400000000000000000001300571501337203100177730ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-context.h" #include "fu-device-event.h" #include "fu-device-locker.h" #include "fu-device-struct.h" #include "fu-firmware.h" #include "fu-progress.h" #include "fu-security-attrs.h" #include "fu-version-common.h" #define FU_TYPE_DEVICE (fu_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuDevice, fu_device, FU, DEVICE, FwupdDevice) struct _FuDeviceClass { FwupdDeviceClass parent_class; #ifndef __GI_SCANNER__ void (*to_string)(FuDevice *self, guint indent, GString *str); gboolean (*write_firmware)(FuDevice *self, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT; FuFirmware *(*read_firmware)(FuDevice *self, FuProgress *progress, GError **error)G_GNUC_WARN_UNUSED_RESULT; gboolean (*detach)(FuDevice *self, FuProgress *progress, GError **error) G_GNUC_WARN_UNUSED_RESULT; gboolean (*attach)(FuDevice *self, FuProgress *progress, GError **error) G_GNUC_WARN_UNUSED_RESULT; gboolean (*open)(FuDevice *self, GError **error) G_GNUC_WARN_UNUSED_RESULT; gboolean (*close)(FuDevice *self, GError **error) G_GNUC_WARN_UNUSED_RESULT; gboolean (*probe)(FuDevice *self, GError **error) G_GNUC_WARN_UNUSED_RESULT; gboolean (*rescan)(FuDevice *self, GError **error) G_GNUC_WARN_UNUSED_RESULT; FuFirmware *(*prepare_firmware)(FuDevice *self, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error)G_GNUC_WARN_UNUSED_RESULT; gboolean (*set_quirk_kv)(FuDevice *self, const gchar *key, const gchar *value, GError **error) G_GNUC_WARN_UNUSED_RESULT; gboolean (*setup)(FuDevice *self, GError **error) G_GNUC_WARN_UNUSED_RESULT; void (*incorporate)(FuDevice *self, FuDevice *donor); void (*replace)(FuDevice *self, FuDevice *donor); void (*probe_complete)(FuDevice *self); gboolean (*poll)(FuDevice *self, GError **error) G_GNUC_WARN_UNUSED_RESULT; gboolean (*activate)(FuDevice *self, FuProgress *progress, GError **error) G_GNUC_WARN_UNUSED_RESULT; gboolean (*reload)(FuDevice *self, GError **error) G_GNUC_WARN_UNUSED_RESULT; gboolean (*prepare)(FuDevice *self, FuProgress *progress, FwupdInstallFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT; gboolean (*cleanup)(FuDevice *self, FuProgress *progress, FwupdInstallFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT; void (*report_metadata_pre)(FuDevice *self, GHashTable *metadata); void (*report_metadata_post)(FuDevice *self, GHashTable *metadata); gboolean (*bind_driver)(FuDevice *self, const gchar *subsystem, const gchar *driver, GError **error) G_GNUC_WARN_UNUSED_RESULT; gboolean (*unbind_driver)(FuDevice *self, GError **error) G_GNUC_WARN_UNUSED_RESULT; GBytes *(*dump_firmware)(FuDevice *self, FuProgress *progress, GError **error)G_GNUC_WARN_UNUSED_RESULT; void (*add_security_attrs)(FuDevice *self, FuSecurityAttrs *attrs); gboolean (*ready)(FuDevice *self, GError **error) G_GNUC_WARN_UNUSED_RESULT; void (*child_added)(FuDevice *self, /* signal */ FuDevice *child); void (*child_removed)(FuDevice *self, /* signal */ FuDevice *child); void (*request)(FuDevice *self, /* signal */ FwupdRequest *request); gboolean (*get_results)(FuDevice *self, GError **error) G_GNUC_WARN_UNUSED_RESULT; void (*set_progress)(FuDevice *self, FuProgress *progress); void (*invalidate)(FuDevice *self); gchar *(*convert_version)(FuDevice *self, guint64 version_raw); void (*register_flags)(FuDevice *self); void (*add_json)(FuDevice *self, JsonBuilder *builder, FwupdCodecFlags flags); gboolean (*from_json)(FuDevice *self, JsonObject *json_object, GError **error) G_GNUC_WARN_UNUSED_RESULT; #endif }; /** * FuDeviceIncorporateFlags: * * The flags to use when incorporating a device instance. **/ typedef enum { /** * FU_DEVICE_INCORPORATE_FLAG_BASECLASS: * * Set baseclass properties. * * Since: 2.0.0 **/ FU_DEVICE_INCORPORATE_FLAG_BASECLASS = 1ull << 0, /** * FU_DEVICE_INCORPORATE_FLAG_SUPERCLASS: * * Set superclass properties, implemented using `->incorporate()`. * * Since: 2.0.0 **/ FU_DEVICE_INCORPORATE_FLAG_SUPERCLASS = 1ull << 1, /** * FU_DEVICE_INCORPORATE_FLAG_VENDOR: * * Set vendor. * * Since: 2.0.0 **/ FU_DEVICE_INCORPORATE_FLAG_VENDOR = 1ull << 2, /** * FU_DEVICE_INCORPORATE_FLAG_VENDOR_IDS: * * Set vendor IDs. * * Since: 2.0.0 **/ FU_DEVICE_INCORPORATE_FLAG_VENDOR_IDS = 1ull << 3, /** * FU_DEVICE_INCORPORATE_FLAG_PHYSICAL_ID: * * Set the physical ID. * * Since: 2.0.0 **/ FU_DEVICE_INCORPORATE_FLAG_PHYSICAL_ID = 1ull << 4, /** * FU_DEVICE_INCORPORATE_FLAG_LOGICAL_ID: * * Set the logical ID. * * Since: 2.0.0 **/ FU_DEVICE_INCORPORATE_FLAG_LOGICAL_ID = 1ull << 5, /** * FU_DEVICE_INCORPORATE_FLAG_BACKEND_ID: * * Set the backend ID. * * Since: 2.0.0 **/ FU_DEVICE_INCORPORATE_FLAG_BACKEND_ID = 1ull << 6, /** * FU_DEVICE_INCORPORATE_FLAG_REMOVE_DELAY: * * Set the remove delay. * * Since: 2.0.0 **/ FU_DEVICE_INCORPORATE_FLAG_REMOVE_DELAY = 1ull << 7, /** * FU_DEVICE_INCORPORATE_FLAG_ACQUIESCE_DELAY: * * Set the acquiesce delay. * * Since: 2.0.0 **/ FU_DEVICE_INCORPORATE_FLAG_ACQUIESCE_DELAY = 1ull << 8, /** * FU_DEVICE_INCORPORATE_FLAG_ICONS: * * Set the icons. * * Since: 2.0.0 **/ FU_DEVICE_INCORPORATE_FLAG_ICONS = 1ull << 9, /** * FU_DEVICE_INCORPORATE_FLAG_UPDATE_ERROR: * * Set the update error. * * Since: 2.0.0 **/ FU_DEVICE_INCORPORATE_FLAG_UPDATE_ERROR = 1ull << 10, /** * FU_DEVICE_INCORPORATE_FLAG_UPDATE_STATE: * * Set the update state. * * Since: 2.0.0 **/ FU_DEVICE_INCORPORATE_FLAG_UPDATE_STATE = 1ull << 11, /** * FU_DEVICE_INCORPORATE_FLAG_VID: * * Set the vendor ID. * * Since: 2.0.0 **/ FU_DEVICE_INCORPORATE_FLAG_VID = 1ull << 12, /** * FU_DEVICE_INCORPORATE_FLAG_PID: * * Set the product ID. * * Since: 2.0.0 **/ FU_DEVICE_INCORPORATE_FLAG_PID = 1ull << 13, /** * FU_DEVICE_INCORPORATE_FLAG_UPDATE_MESSAGE: * * Set the update message. * * Since: 2.0.0 **/ FU_DEVICE_INCORPORATE_FLAG_UPDATE_MESSAGE = 1ull << 14, /** * FU_DEVICE_INCORPORATE_FLAG_UPDATE_IMAGE: * * Set the update image. * * Since: 2.0.0 **/ FU_DEVICE_INCORPORATE_FLAG_UPDATE_IMAGE = 1ull << 15, /** * FU_DEVICE_INCORPORATE_FLAG_EVENTS: * * Add the device events. * * Since: 2.0.0 **/ FU_DEVICE_INCORPORATE_FLAG_EVENTS = 1ull << 16, /** * FU_DEVICE_INCORPORATE_FLAG_INSTANCE_IDS: * * Set the instance IDs. * * Since: 2.0.4 **/ FU_DEVICE_INCORPORATE_FLAG_INSTANCE_IDS = 1ull << 17, /** * FU_DEVICE_INCORPORATE_FLAG_POSSIBLE_PLUGINS: * * Set the possible plugins. * * Since: 2.0.6 **/ FU_DEVICE_INCORPORATE_FLAG_POSSIBLE_PLUGINS = 1ull << 18, /** * FU_DEVICE_INCORPORATE_FLAG_GTYPE: * * Set the device GType. * * Since: 2.0.6 **/ FU_DEVICE_INCORPORATE_FLAG_GTYPE = 1ull << 19, /** * FU_DEVICE_INCORPORATE_FLAG_INSTANCE_KEYS: * * Set the device instance keys. * * Since: 2.0.9 **/ FU_DEVICE_INCORPORATE_FLAG_INSTANCE_KEYS = 1ull << 20, /*< private >*/ FU_DEVICE_INCORPORATE_FLAG_ALL = G_MAXUINT64, } FuDeviceIncorporateFlags; /** * FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE: * * The default removal delay for device re-enumeration taking into account a * chain of slow USB hubs. This should be used when the device is able to * reset itself between bootloader->runtime->bootloader. */ #define FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE 10000 /* ms */ /** * FU_DEVICE_REMOVE_DELAY_USER_REPLUG: * * The default removal delay for device re-plug taking into account humans * being slow and clumsy. This should be used when the user has to do something, * e.g. unplug, press a magic button and then replug. */ #define FU_DEVICE_REMOVE_DELAY_USER_REPLUG 40000 /* ms */ /** * FuDeviceRetryFunc: * @self: a #FuDevice * @user_data: (closure): user data * @error: (nullable): optional return location for an error * * The device retry iteration callback. * * Returns: %TRUE on success */ typedef gboolean (*FuDeviceRetryFunc)(FuDevice *self, gpointer user_data, GError **error) G_GNUC_WARN_UNUSED_RESULT; FuDevice * fu_device_new(FuContext *ctx); /* helpful casting macros */ #define fu_device_has_flag(d, v) fwupd_device_has_flag(FWUPD_DEVICE(d), v) #define fu_device_has_flag(d, v) fwupd_device_has_flag(FWUPD_DEVICE(d), v) #define fu_device_has_request_flag(d, v) fwupd_device_has_request_flag(FWUPD_DEVICE(d), v) #define fu_device_add_request_flag(d, v) fwupd_device_add_request_flag(FWUPD_DEVICE(d), v) #define fu_device_has_vendor_id(d, v) fwupd_device_has_vendor_id(FWUPD_DEVICE(d), v) #define fu_device_has_protocol(d, v) fwupd_device_has_protocol(FWUPD_DEVICE(d), v) #define fu_device_has_checksum(d, v) fwupd_device_has_checksum(FWUPD_DEVICE(d), v) #define fu_device_add_checksum(d, v) fwupd_device_add_checksum(FWUPD_DEVICE(d), v) #define fu_device_add_release(d, v) fwupd_device_add_release(FWUPD_DEVICE(d), v) #define fu_device_add_icon(d, v) fwupd_device_add_icon(FWUPD_DEVICE(d), v) #define fu_device_has_icon(d, v) fwupd_device_has_icon(FWUPD_DEVICE(d), v) #define fu_device_add_issue(d, v) fwupd_device_add_issue(FWUPD_DEVICE(d), v) #define fu_device_set_description(d, v) fwupd_device_set_description(FWUPD_DEVICE(d), v) #define fu_device_set_flags(d, v) fwupd_device_set_flags(FWUPD_DEVICE(d), v) #define fu_device_set_plugin(d, v) fwupd_device_set_plugin(FWUPD_DEVICE(d), v) #define fu_device_set_serial(d, v) fwupd_device_set_serial(FWUPD_DEVICE(d), v) #define fu_device_set_summary(d, v) fwupd_device_set_summary(FWUPD_DEVICE(d), v) #define fu_device_set_branch(d, v) fwupd_device_set_branch(FWUPD_DEVICE(d), v) #define fu_device_set_update_error(d, v) fwupd_device_set_update_error(FWUPD_DEVICE(d), v) #define fu_device_add_vendor_id(d, v) fwupd_device_add_vendor_id(FWUPD_DEVICE(d), v) #define fu_device_add_protocol(d, v) fwupd_device_add_protocol(FWUPD_DEVICE(d), v) #define fu_device_set_version_bootloader_raw(d, v) \ fwupd_device_set_version_bootloader_raw(FWUPD_DEVICE(d), v) #define fu_device_set_version_build_date(d, v) \ fwupd_device_set_version_build_date(FWUPD_DEVICE(d), v) #define fu_device_set_flashes_left(d, v) fwupd_device_set_flashes_left(FWUPD_DEVICE(d), v) #define fu_device_set_install_duration(d, v) fwupd_device_set_install_duration(FWUPD_DEVICE(d), v) #define fu_device_get_checksums(d) fwupd_device_get_checksums(FWUPD_DEVICE(d)) #define fu_device_get_flags(d) fwupd_device_get_flags(FWUPD_DEVICE(d)) #define fu_device_get_guids(d) fwupd_device_get_guids(FWUPD_DEVICE(d)) #define fu_device_get_guid_default(d) fwupd_device_get_guid_default(FWUPD_DEVICE(d)) #define fu_device_get_instance_ids(d) fwupd_device_get_instance_ids(FWUPD_DEVICE(d)) #define fu_device_get_icons(d) fwupd_device_get_icons(FWUPD_DEVICE(d)) #define fu_device_get_issues(d) fwupd_device_get_issues(FWUPD_DEVICE(d)) #define fu_device_get_name(d) fwupd_device_get_name(FWUPD_DEVICE(d)) #define fu_device_get_serial(d) fwupd_device_get_serial(FWUPD_DEVICE(d)) #define fu_device_get_summary(d) fwupd_device_get_summary(FWUPD_DEVICE(d)) #define fu_device_get_branch(d) fwupd_device_get_branch(FWUPD_DEVICE(d)) #define fu_device_get_id(d) fwupd_device_get_id(FWUPD_DEVICE(d)) #define fu_device_get_composite_id(d) fwupd_device_get_composite_id(FWUPD_DEVICE(d)) #define fu_device_get_plugin(d) fwupd_device_get_plugin(FWUPD_DEVICE(d)) #define fu_device_get_update_error(d) fwupd_device_get_update_error(FWUPD_DEVICE(d)) #define fu_device_get_update_state(d) fwupd_device_get_update_state(FWUPD_DEVICE(d)) #define fu_device_get_vendor(d) fwupd_device_get_vendor(FWUPD_DEVICE(d)) #define fu_device_get_version(d) fwupd_device_get_version(FWUPD_DEVICE(d)) #define fu_device_get_version_lowest(d) fwupd_device_get_version_lowest(FWUPD_DEVICE(d)) #define fu_device_get_version_bootloader(d) fwupd_device_get_version_bootloader(FWUPD_DEVICE(d)) #define fu_device_get_version_format(d) fwupd_device_get_version_format(FWUPD_DEVICE(d)) #define fu_device_get_version_raw(d) fwupd_device_get_version_raw(FWUPD_DEVICE(d)) #define fu_device_get_version_lowest_raw(d) fwupd_device_get_version_lowest_raw(FWUPD_DEVICE(d)) #define fu_device_get_version_bootloader_raw(d) \ fwupd_device_get_version_bootloader_raw(FWUPD_DEVICE(d)) #define fu_device_get_version_build_date(d) fwupd_device_get_version_build_date(FWUPD_DEVICE(d)) #define fu_device_get_vendor_ids(d) fwupd_device_get_vendor_ids(FWUPD_DEVICE(d)) #define fu_device_get_protocols(d) fwupd_device_get_protocols(FWUPD_DEVICE(d)) #define fu_device_get_flashes_left(d) fwupd_device_get_flashes_left(FWUPD_DEVICE(d)) #define fu_device_get_install_duration(d) fwupd_device_get_install_duration(FWUPD_DEVICE(d)) #define fu_device_get_release_default(d) fwupd_device_get_release_default(FWUPD_DEVICE(d)) #define fu_device_get_status(d) fwupd_device_get_status(FWUPD_DEVICE(d)) #define fu_device_set_status(d, v) fwupd_device_set_status(FWUPD_DEVICE(d), v) #define fu_device_get_percentage(d) fwupd_device_get_percentage(FWUPD_DEVICE(d)) #define fu_device_set_percentage(d, v) fwupd_device_set_percentage(FWUPD_DEVICE(d), v) /** * FU_DEVICE_PRIVATE_FLAG_NO_AUTO_INSTANCE_IDS: * * Do not add instance IDs from the device baseclass. * * Since: 1.5.5 */ #define FU_DEVICE_PRIVATE_FLAG_NO_AUTO_INSTANCE_IDS "no-auto-instance-ids" /** * FU_DEVICE_PRIVATE_FLAG_ENSURE_SEMVER: * * Ensure the version is a valid semantic version, e.g. numbers separated with dots. * * Since: 1.5.5 */ #define FU_DEVICE_PRIVATE_FLAG_ENSURE_SEMVER "ensure-semver" /** * FU_DEVICE_PRIVATE_FLAG_ONLY_SUPPORTED: * * Only devices supported in the metadata will be opened * * Since: 1.5.5 */ #define FU_DEVICE_PRIVATE_FLAG_ONLY_SUPPORTED "only-supported" /** * FU_DEVICE_PRIVATE_FLAG_MD_SET_NAME: * * Set the device name from the metadata `name` if available. * * Since: 1.5.5 */ #define FU_DEVICE_PRIVATE_FLAG_MD_SET_NAME "md-set-name" /** * FU_DEVICE_PRIVATE_FLAG_MD_SET_NAME_CATEGORY: * * Set the device name from the metadata `category` if available. * * Since: 1.5.5 */ #define FU_DEVICE_PRIVATE_FLAG_MD_SET_NAME_CATEGORY "md-set-name-category" /** * FU_DEVICE_PRIVATE_FLAG_MD_SET_VERFMT: * * Set the device version format from the metadata or history database if available. * * Since: 1.5.5 */ #define FU_DEVICE_PRIVATE_FLAG_MD_SET_VERFMT "md-set-verfmt" /** * FU_DEVICE_PRIVATE_FLAG_MD_SET_ICON: * * Set the device icon from the metadata if available. * * Since: 1.5.5 */ #define FU_DEVICE_PRIVATE_FLAG_MD_SET_ICON "md-set-icon" /** * FU_DEVICE_PRIVATE_FLAG_RETRY_OPEN: * * Retry the device open up to 5 times if it fails. * * Since: 1.5.5 */ #define FU_DEVICE_PRIVATE_FLAG_RETRY_OPEN "retry-open" /** * FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID: * * Match GUIDs on device replug where the physical and logical IDs will be different. * * Since: 1.5.8 */ #define FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID "replug-match-guid" /** * FU_DEVICE_PRIVATE_FLAG_INHERIT_ACTIVATION: * * Inherit activation status from the history database on startup. * * Since: 1.5.9 */ #define FU_DEVICE_PRIVATE_FLAG_INHERIT_ACTIVATION "inherit-activation" /** * FU_DEVICE_PRIVATE_FLAG_IS_OPEN: * * The device opened successfully and ready to use. * * Since: 1.6.1 */ #define FU_DEVICE_PRIVATE_FLAG_IS_OPEN "is-open" /** * FU_DEVICE_PRIVATE_FLAG_NO_SERIAL_NUMBER: * * Do not attempt to read the device serial number. * * Since: 1.6.2 */ #define FU_DEVICE_PRIVATE_FLAG_NO_SERIAL_NUMBER "no-serial-number" /** * FU_DEVICE_PRIVATE_FLAG_AUTO_PARENT_CHILDREN: * * Automatically assign the parent for children of this device. * * Since: 1.6.2 */ #define FU_DEVICE_PRIVATE_FLAG_AUTO_PARENT_CHILDREN "auto-parent-children" /** * FU_DEVICE_PRIVATE_FLAG_ATTACH_EXTRA_RESET: * * Device needs resetting twice for attach after the firmware update. * * Since: 1.6.2 */ #define FU_DEVICE_PRIVATE_FLAG_ATTACH_EXTRA_RESET "attach-extra-reset" /** * FU_DEVICE_PRIVATE_FLAG_INHIBIT_CHILDREN: * * Children of the device are inhibited by the parent. * * Since: 1.6.2 */ #define FU_DEVICE_PRIVATE_FLAG_INHIBIT_CHILDREN "inhibit-children" /** * FU_DEVICE_PRIVATE_FLAG_NO_AUTO_REMOVE_CHILDREN: * * Do not auto-remove children in the device list. * * Since: 1.6.2 */ #define FU_DEVICE_PRIVATE_FLAG_NO_AUTO_REMOVE_CHILDREN "no-auto-remove-children" /** * FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_OPEN: * * Use parent to open and close the device. * * Since: 1.6.2 */ #define FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_OPEN "use-parent-for-open" /** * FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_BATTERY: * * Use parent for the battery level and threshold. * * Since: 1.6.3 */ #define FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_BATTERY "use-parent-for-battery" /** * FU_DEVICE_PRIVATE_FLAG_USE_PROXY_FALLBACK: * * Use parent for the battery level and threshold. * * Since: 1.6.4 */ #define FU_DEVICE_PRIVATE_FLAG_USE_PROXY_FALLBACK "use-proxy-fallback" /** * FU_DEVICE_PRIVATE_FLAG_NO_AUTO_REMOVE: * * The device is not auto removed. * * Since 1.7.3 */ #define FU_DEVICE_PRIVATE_FLAG_NO_AUTO_REMOVE "no-auto-remove" /** * FU_DEVICE_PRIVATE_FLAG_MD_SET_VENDOR: * * Set the device vendor from the metadata `developer_name` if available. * * Since: 1.7.4 */ #define FU_DEVICE_PRIVATE_FLAG_MD_SET_VENDOR "md-set-vendor" /** * FU_DEVICE_PRIVATE_FLAG_NO_LID_CLOSED: * * Do not allow updating when the laptop lid is closed. * * Since: 1.7.4 */ #define FU_DEVICE_PRIVATE_FLAG_NO_LID_CLOSED "no-lid-closed" /** * FU_DEVICE_PRIVATE_FLAG_NO_PROBE: * * Do not probe this device. * * Since: 1.7.6 */ #define FU_DEVICE_PRIVATE_FLAG_NO_PROBE "no-probe" /** * FU_DEVICE_PRIVATE_FLAG_MD_SET_SIGNED: * * Set the signed/unsigned payload from the metadata if available. * * Since: 1.7.6 */ #define FU_DEVICE_PRIVATE_FLAG_MD_SET_SIGNED "md-set-signed" /** * FU_DEVICE_PRIVATE_FLAG_AUTO_PAUSE_POLLING: * * Pause polling when reading or writing to the device * * Since: 1.8.1 */ #define FU_DEVICE_PRIVATE_FLAG_AUTO_PAUSE_POLLING "auto-pause-polling" /** * FU_DEVICE_PRIVATE_FLAG_DELAYED_REMOVAL: * * Use the device removal delay every time the device is removed, rather than when explicitly * waiting for a replug. * * Since: 2.0.3 */ #define FU_DEVICE_PRIVATE_FLAG_DELAYED_REMOVAL "delayed-removal" /** * FU_DEVICE_PRIVATE_FLAG_IGNORE_SYSTEM_POWER: * * Allow updating firmware when the system power is otherwise too low. * This is only really useful when updating the system battery firmware. * * Since: 1.8.11 */ #define FU_DEVICE_PRIVATE_FLAG_IGNORE_SYSTEM_POWER "ignore-system-power" /** * FU_DEVICE_PRIVATE_FLAG_SAVE_INTO_BACKUP_REMOTE: * * Save the cabinet archive to persistent storage remote before starting the update process. * * This is useful when the network device is being updated, and different blobs inside the * archive could be required in different scenarios. For instance, if the user installs a * firmware update for a specific network device and then changes the SIM -- it might be * they need the archive again and have no internet access. * * Since: 1.8.13 */ #define FU_DEVICE_PRIVATE_FLAG_SAVE_INTO_BACKUP_REMOTE "save-into-backup-remote" /** * FU_DEVICE_PRIVATE_FLAG_MD_SET_FLAGS: * * Set the device flags from the metadata if available. * * NOTE: These flags should only affect device update, and should never be used to affect * enumeration. * * Since: 1.9.1 */ #define FU_DEVICE_PRIVATE_FLAG_MD_SET_FLAGS "md-set-flags" /** * FU_DEVICE_PRIVATE_FLAG_MD_SET_VERSION: * * Set the device version from the metadata if available. * * Since: 1.9.1 */ #define FU_DEVICE_PRIVATE_FLAG_MD_SET_VERSION "md-set-version" /** * FU_DEVICE_PRIVATE_FLAG_MD_ONLY_CHECKSUM: * * Only use the metadata *checksum* to set device attributes. * * NOTE: This will only work if the release and the device both have the same update protocol. * * Since: 1.9.1 */ #define FU_DEVICE_PRIVATE_FLAG_MD_ONLY_CHECKSUM "md-only-checksum" /** * FU_DEVICE_PRIVATE_FLAG_ADD_INSTANCE_ID_REV: * * Add the `_REV` instance ID suffix. * * Since: 1.9.3 */ #define FU_DEVICE_PRIVATE_FLAG_ADD_INSTANCE_ID_REV "add-instance-id-rev" /** * FU_DEVICE_PRIVATE_FLAG_UNCONNECTED: * * The device is not connected and is probably awaiting replug. * * Since: 1.9.4 */ #define FU_DEVICE_PRIVATE_FLAG_UNCONNECTED "unconnected" /** * FU_DEVICE_PRIVATE_FLAG_DISPLAY_REQUIRED: * * The device requires a display to be plugged in. * * Since: 1.9.6 */ #define FU_DEVICE_PRIVATE_FLAG_DISPLAY_REQUIRED "display-required" /** * FU_DEVICE_PRIVATE_FLAG_UPDATE_PENDING: * * The device has an update that is waiting to be applied. * * Since: 1.9.7 */ #define FU_DEVICE_PRIVATE_FLAG_UPDATE_PENDING "update-pending" /** * FU_DEVICE_PRIVATE_FLAG_NO_GENERIC_GUIDS: * * Do not add generic GUIDs from outside the plugin. * * Since: 1.9.8 */ #define FU_DEVICE_PRIVATE_FLAG_NO_GENERIC_GUIDS "no-generic-guids" /** * FU_DEVICE_PRIVATE_FLAG_ENFORCE_REQUIRES: * * The device uses a generic instance ID and firmware requires a parent, child, sibling or * CHID requirement. * * Since: 1.9.8 */ #define FU_DEVICE_PRIVATE_FLAG_ENFORCE_REQUIRES "enforce-requires" /** * FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE: * * The device represents the main system host firmware. * * Since: 1.9.10 */ #define FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE "host-firmware" /** * FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE_CHILD: * * The device should be a child of the main system host firmware device. * * Since: 1.9.10 */ #define FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE_CHILD "host-firmware-child" /** * FU_DEVICE_PRIVATE_FLAG_HOST_CPU: * * The device represents the main CPU device. * * Since: 1.9.10 */ #define FU_DEVICE_PRIVATE_FLAG_HOST_CPU "host-cpu" /** * FU_DEVICE_PRIVATE_FLAG_HOST_CPU_CHILD: * * The device should be a child of the main CPU device. * * Since: 1.9.10 */ #define FU_DEVICE_PRIVATE_FLAG_HOST_CPU_CHILD "host-cpu-child" /** * FU_DEVICE_PRIVATE_FLAG_EXPLICIT_ORDER: * * Do not automatically set the device order, e.g. updating the child before the parent. * * Since: 1.9.13 */ #define FU_DEVICE_PRIVATE_FLAG_EXPLICIT_ORDER "explicit-order" /** * FU_DEVICE_PRIVATE_FLAG_REFCOUNTED_PROXY: * * Reference-count the proxy -- which is useful when using `ProxyGType`. * * Since: 1.9.15 */ #define FU_DEVICE_PRIVATE_FLAG_REFCOUNTED_PROXY "refcounted-proxy" /** * FU_DEVICE_PRIVATE_FLAG_USE_PROXY_FOR_OPEN: * * Use proxy to open and close the device. * * Since: 1.9.16 */ #define FU_DEVICE_PRIVATE_FLAG_USE_PROXY_FOR_OPEN "use-proxy-for-open" /** * FU_DEVICE_PRIVATE_FLAG_INSTALL_PARENT_FIRST: * * The composite device requires installation of composite firmware on the parent before * the child. * Normally the child is installed before the parent. * * Since: 2.0.0 */ #define FU_DEVICE_PRIVATE_FLAG_INSTALL_PARENT_FIRST "install-parent-first" /** * FU_DEVICE_PRIVATE_FLAG_REGISTERED: * * The device has been registered with other plugins. * * Since: 2.0.0 */ #define FU_DEVICE_PRIVATE_FLAG_REGISTERED "registered" /** * FU_DEVICE_PRIVATE_FLAG_ADD_COUNTERPART_GUIDS: * * The device will add counterpart GUIDs from an alternate mode like bootloader. * This flag is typically specified in a quirk. * * Since: 2.0.0 */ #define FU_DEVICE_PRIVATE_FLAG_ADD_COUNTERPART_GUIDS "add-counterpart-guids" /** * FU_DEVICE_PRIVATE_FLAG_USE_RUNTIME_VERSION: * * The device will always display use the runtime version rather than the bootloader * version. * * Since: 2.0.0 */ #define FU_DEVICE_PRIVATE_FLAG_USE_RUNTIME_VERSION "use-runtime-version" /** * FU_DEVICE_PRIVATE_FLAG_SKIPS_RESTART: * * The device relies upon activation or power cycle to load firmware. * * Since: 2.0.0 */ #define FU_DEVICE_PRIVATE_FLAG_SKIPS_RESTART "skips-restart" /** * FU_DEVICE_PRIVATE_FLAG_IS_FAKE: * * The device is fake, and constructed from a fake sysfs test directory. * * Since: 2.0.0 */ #define FU_DEVICE_PRIVATE_FLAG_IS_FAKE "is-fake" /** * FU_DEVICE_PRIVATE_FLAG_COUNTERPART_VISIBLE: * * Add the counterpart instance IDs as visible GUID that can be matched in firmware. * * This was how the daemon worked in 1.9.x, and some devices in recovery mode still expect this. * * Since: 2.0.4 */ #define FU_DEVICE_PRIVATE_FLAG_COUNTERPART_VISIBLE "counterpart-visible" /** * FU_DEVICE_PRIVATE_FLAG_DETACH_PREPARE_FIRMWARE: * * Detach, then prepare firmware rather than parsing firmware from the runtime device. * For some devices the firmware GType isn't known until the bootloader gets added. * * Since: 2.0.7 */ #define FU_DEVICE_PRIVATE_FLAG_DETACH_PREPARE_FIRMWARE "detach-prepare-firmware" /** * FU_DEVICE_PRIVATE_FLAG_EMULATED_REQUIRE_SETUP: * * The device requires `FuDevice->setup()` rather than `FuDevice->probe()` to add instance IDs * during emulated device enumeration. * * Since: 2.0.7 */ #define FU_DEVICE_PRIVATE_FLAG_EMULATED_REQUIRE_SETUP "emulated-require-setup" /** * FU_DEVICE_PRIVATE_FLAG_INSTALL_LOOP_RESTART: * * Restart the `detach->write_firmware->attach->reload` loop without completing the sequence. * This can be set on any step, unlike `another-write-required` which does all steps. * * Since: 2.0.7 */ #define FU_DEVICE_PRIVATE_FLAG_INSTALL_LOOP_RESTART "install-loop-restart" /* standard icons */ /** * FU_DEVICE_ICON_COMPUTER: * * A generic icon of a computer. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_COMPUTER "computer" /** * FU_DEVICE_ICON_GPU: * * A generic icon of a gpu. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_GPU "gpu" /** * FU_DEVICE_ICON_AC_ADAPTER: * * A generic icon of an AC adapter. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_AC_ADAPTER "ac-adapter" /** * FU_DEVICE_ICON_DOCK: * * A generic icon of a dock. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_DOCK "dock" /** * FU_DEVICE_ICON_DOCK_USB: * * An icon of a dock attached via USB. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_DOCK_USB "dock-usb" /** * FU_DEVICE_ICON_USB_HUB: * * An icon of a hub of USB devices. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_USB_HUB "usb-hub" /** * FU_DEVICE_ICON_THUNDERBOLT: * * A generic icon of a thunderbolt device. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_THUNDERBOLT "thunderbolt" /** * FU_DEVICE_ICON_MEMORY: * * A generic icon of a memory card. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_MEMORY "media-memory" /** * FU_DEVICE_ICON_MODEM: * * An icon of a modem. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_MODEM "modem" /** * FU_DEVICE_ICON_NETWORK_WIRED: * * A generic icon of a wired connection. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_NETWORK_WIRED "network-wired" /** * FU_DEVICE_ICON_NETWORK_WIRELESS: * * A generic icon of a wireless connection. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_NETWORK_WIRELESS "network-wireless" /** * FU_DEVICE_ICON_DRIVE_HARDDISK: * * A generic icon of a hard disk drive. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_DRIVE_HARDDISK "drive-harddisk" /** * FU_DEVICE_ICON_DRIVE_SSD: * * A generic icon of a hard disk drive. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_DRIVE_SSD "drive-harddisk-solidstate" /** * FU_DEVICE_ICON_DRIVE_MULTIDISK: * * A generic icon of a multidisk drive. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_DRIVE_MULTIDISK "drive-multidisk" /** * FU_DEVICE_ICON_AUDIO_CARD: * * An icon of an audio card. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_AUDIO_CARD "audio-card" /** * FU_DEVICE_ICON_VIDEO_DISPLAY: * * A generic icon of a display. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_VIDEO_DISPLAY "video-display" /** * FU_DEVICE_ICON_INPUT_TOUCHPAD: * * A generic icon of a touchpad. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_INPUT_TOUCHPAD "input-touchpad" /** * FU_DEVICE_ICON_INPUT_KEYBOARD: * * A generic icon of a keyboard. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_INPUT_KEYBOARD "input-keyboard" /** * FU_DEVICE_ICON_INPUT_MOUSE: * * A generic icon of a mouse. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_INPUT_MOUSE "input-mouse" /** * FU_DEVICE_ICON_INPUT_DIALPAD: * * A generic icon of a dialpad. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_INPUT_DIALPAD "input-dialpad" /** * FU_DEVICE_ICON_INPUT_GAMING: * * A generic icon of a gaming controller. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_INPUT_GAMING "input-gaming" /** * FU_DEVICE_ICON_INPUT_TABLET: * * A generic icon of a drawing tablet. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_INPUT_TABLET "input-tablet" /** * FU_DEVICE_ICON_AUTH_FINGERPRINT: * * A generic icon of a fingerprint reader. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_AUTH_FINGERPRINT "auth-fingerprint" /** * FU_DEVICE_ICON_USB_RECEIVER: * * A generic icon of an USB receiver. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_USB_RECEIVER "usb-receiver" /** * FU_DEVICE_ICON_PDA: * * A generic icon of a PDA. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_PDA "pda" /** * FU_DEVICE_ICON_WEB_CAMERA: * * A generic icon of a web camera. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_WEB_CAMERA "camera-web" /** * FU_DEVICE_ICON_VIDEO_CAMERA: * * A generic icon of a video camera. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_VIDEO_CAMERA "camera-video" /** * FU_DEVICE_ICON_APPLICATION_CERTIFICATE: * * A generic icon of a certificate. * * Since: 2.0.10 */ #define FU_DEVICE_ICON_APPLICATION_CERTIFICATE "application-certificate" /* accessors */ gchar * fu_device_to_string(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_add_string(FuDevice *self, guint idt, GString *str) G_GNUC_NON_NULL(1, 3); const gchar * fu_device_get_equivalent_id(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_set_equivalent_id(FuDevice *self, const gchar *equivalent_id) G_GNUC_NON_NULL(1, 2); gboolean fu_device_has_guid(FuDevice *self, const gchar *guid) G_GNUC_NON_NULL(1); void fu_device_add_instance_id(FuDevice *self, const gchar *instance_id) G_GNUC_NON_NULL(1, 2); gboolean fu_device_has_instance_id(FuDevice *self, const gchar *instance_id, FuDeviceInstanceFlag flags) G_GNUC_NON_NULL(1, 2); void fu_device_add_instance_id_full(FuDevice *self, const gchar *instance_id, FuDeviceInstanceFlag flags) G_GNUC_NON_NULL(1, 2); FuDevice * fu_device_get_root(FuDevice *self) G_GNUC_NON_NULL(1); FuDevice * fu_device_get_parent(FuDevice *self) G_GNUC_NON_NULL(1); FuDevice * fu_device_get_backend_parent(FuDevice *self, GError **error) G_GNUC_NON_NULL(1); FuDevice * fu_device_get_backend_parent_with_subsystem(FuDevice *self, const gchar *subsystem, GError **error) G_GNUC_NON_NULL(1); GPtrArray * fu_device_get_children(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_add_child(FuDevice *self, FuDevice *child) G_GNUC_NON_NULL(1, 2); void fu_device_remove_child(FuDevice *self, FuDevice *child) G_GNUC_NON_NULL(1, 2); void fu_device_add_parent_guid(FuDevice *self, const gchar *guid) G_GNUC_NON_NULL(1, 2); void fu_device_add_parent_physical_id(FuDevice *self, const gchar *physical_id) G_GNUC_NON_NULL(1, 2); void fu_device_add_parent_backend_id(FuDevice *self, const gchar *backend_id) G_GNUC_NON_NULL(1, 2); FuDevice * fu_device_get_proxy(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_set_proxy(FuDevice *self, FuDevice *proxy) G_GNUC_NON_NULL(1); FuDevice * fu_device_get_proxy_with_fallback(FuDevice *self) G_GNUC_NON_NULL(1); const gchar * fu_device_get_metadata(FuDevice *self, const gchar *key) G_GNUC_NON_NULL(1, 2); gboolean fu_device_get_metadata_boolean(FuDevice *self, const gchar *key) G_GNUC_NON_NULL(1, 2); guint fu_device_get_metadata_integer(FuDevice *self, const gchar *key) G_GNUC_NON_NULL(1, 2); void fu_device_remove_metadata(FuDevice *self, const gchar *key) G_GNUC_NON_NULL(1, 2); void fu_device_set_metadata(FuDevice *self, const gchar *key, const gchar *value) G_GNUC_NON_NULL(1, 2); void fu_device_set_metadata_boolean(FuDevice *self, const gchar *key, gboolean value) G_GNUC_NON_NULL(1, 2); void fu_device_set_metadata_integer(FuDevice *self, const gchar *key, guint value) G_GNUC_NON_NULL(1, 2); void fu_device_set_id(FuDevice *self, const gchar *id) G_GNUC_NON_NULL(1); void fu_device_set_version_format(FuDevice *self, FwupdVersionFormat fmt) G_GNUC_NON_NULL(1); void fu_device_set_version(FuDevice *self, const gchar *version) G_GNUC_NON_NULL(1); void fu_device_set_version_lowest(FuDevice *self, const gchar *version) G_GNUC_NON_NULL(1); void fu_device_set_version_bootloader(FuDevice *self, const gchar *version) G_GNUC_NON_NULL(1); void fu_device_set_version_raw(FuDevice *self, guint64 version_raw) G_GNUC_NON_NULL(1); void fu_device_set_version_lowest_raw(FuDevice *self, guint64 version_raw) G_GNUC_NON_NULL(1); void fu_device_inhibit(FuDevice *self, const gchar *inhibit_id, const gchar *reason) G_GNUC_NON_NULL(1, 2); void fu_device_uninhibit(FuDevice *self, const gchar *inhibit_id) G_GNUC_NON_NULL(1, 2); void fu_device_add_problem(FuDevice *self, FwupdDeviceProblem problem) G_GNUC_NON_NULL(1); void fu_device_remove_problem(FuDevice *self, FwupdDeviceProblem problem) G_GNUC_NON_NULL(1); gboolean fu_device_has_problem(FuDevice *self, FwupdDeviceProblem problem) G_GNUC_NON_NULL(1); gboolean fu_device_has_inhibit(FuDevice *self, const gchar *inhibit_id) G_GNUC_NON_NULL(1, 2); const gchar * fu_device_get_physical_id(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_set_physical_id(FuDevice *self, const gchar *physical_id) G_GNUC_NON_NULL(1); const gchar * fu_device_get_logical_id(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_set_logical_id(FuDevice *self, const gchar *logical_id) G_GNUC_NON_NULL(1); const gchar * fu_device_get_backend_id(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_set_backend_id(FuDevice *self, const gchar *backend_id) G_GNUC_NON_NULL(1); const gchar * fu_device_get_proxy_guid(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_set_proxy_guid(FuDevice *self, const gchar *proxy_guid) G_GNUC_NON_NULL(1); guint fu_device_get_priority(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_set_priority(FuDevice *self, guint priority) G_GNUC_NON_NULL(1); void fu_device_add_flag(FuDevice *self, FwupdDeviceFlags flag) G_GNUC_NON_NULL(1); void fu_device_remove_flag(FuDevice *self, FwupdDeviceFlags flag) G_GNUC_NON_NULL(1); void fu_device_set_name(FuDevice *self, const gchar *value) G_GNUC_NON_NULL(1); void fu_device_set_vendor(FuDevice *self, const gchar *vendor) G_GNUC_NON_NULL(1); guint fu_device_get_remove_delay(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_set_remove_delay(FuDevice *self, guint remove_delay) G_GNUC_NON_NULL(1); guint fu_device_get_acquiesce_delay(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_set_acquiesce_delay(FuDevice *self, guint acquiesce_delay) G_GNUC_NON_NULL(1); void fu_device_set_firmware_size(FuDevice *self, guint64 size) G_GNUC_NON_NULL(1); void fu_device_set_firmware_size_min(FuDevice *self, guint64 size_min) G_GNUC_NON_NULL(1); void fu_device_set_firmware_size_max(FuDevice *self, guint64 size_max) G_GNUC_NON_NULL(1); guint64 fu_device_get_firmware_size_min(FuDevice *self) G_GNUC_NON_NULL(1); guint64 fu_device_get_firmware_size_max(FuDevice *self) G_GNUC_NON_NULL(1); guint fu_device_get_battery_level(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_set_battery_level(FuDevice *self, guint battery_level) G_GNUC_NON_NULL(1); guint fu_device_get_battery_threshold(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_set_battery_threshold(FuDevice *self, guint battery_threshold) G_GNUC_NON_NULL(1); const gchar * fu_device_get_update_message(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_set_update_message(FuDevice *self, const gchar *update_message) G_GNUC_NON_NULL(1); const gchar * fu_device_get_update_image(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_set_update_image(FuDevice *self, const gchar *update_image) G_GNUC_NON_NULL(1); gint64 fu_device_get_created_usec(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_set_created_usec(FuDevice *self, gint64 created_usec) G_GNUC_NON_NULL(1); gint64 fu_device_get_modified_usec(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_set_modified_usec(FuDevice *self, gint64 modified_usec) G_GNUC_NON_NULL(1); guint16 fu_device_get_vid(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_set_vid(FuDevice *self, guint16 vid); guint16 fu_device_get_pid(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_set_pid(FuDevice *self, guint16 pid); void fu_device_set_update_state(FuDevice *self, FwupdUpdateState update_state) G_GNUC_NON_NULL(1); void fu_device_set_context(FuDevice *self, FuContext *ctx) G_GNUC_NON_NULL(1); FuContext * fu_device_get_context(FuDevice *self) G_GNUC_NON_NULL(1); GType fu_device_get_specialized_gtype(FuDevice *self) G_GNUC_NON_NULL(1); GType fu_device_get_proxy_gtype(FuDevice *self) G_GNUC_NON_NULL(1); GType fu_device_get_firmware_gtype(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_set_firmware_gtype(FuDevice *self, GType firmware_gtype) G_GNUC_NON_NULL(1); gboolean fu_device_get_results(FuDevice *self, GError **error) G_GNUC_NON_NULL(1); gboolean fu_device_write_firmware(FuDevice *self, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); FuFirmware * fu_device_prepare_firmware(FuDevice *self, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); FuFirmware * fu_device_read_firmware(FuDevice *self, FuProgress *progress, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); GBytes * fu_device_dump_firmware(FuDevice *self, FuProgress *progress, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_device_attach(FuDevice *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_device_detach(FuDevice *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_device_attach_full(FuDevice *self, FuProgress *progress, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_device_detach_full(FuDevice *self, FuProgress *progress, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_device_reload(FuDevice *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_device_prepare(FuDevice *self, FuProgress *progress, FwupdInstallFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_device_cleanup(FuDevice *self, FuProgress *progress, FwupdInstallFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fu_device_incorporate(FuDevice *self, FuDevice *donor, FuDeviceIncorporateFlags flag) G_GNUC_NON_NULL(1, 2); void fu_device_incorporate_flag(FuDevice *self, FuDevice *donor, FwupdDeviceFlags flag) G_GNUC_NON_NULL(1); gboolean fu_device_open(FuDevice *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_device_close(FuDevice *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_device_probe(FuDevice *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_device_setup(FuDevice *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_device_rescan(FuDevice *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_device_activate(FuDevice *self, FuProgress *progress, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); void fu_device_probe_invalidate(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_probe_complete(FuDevice *self) G_GNUC_NON_NULL(1); gboolean fu_device_poll(FuDevice *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); void fu_device_set_poll_interval(FuDevice *self, guint interval) G_GNUC_NON_NULL(1); void fu_device_retry_set_delay(FuDevice *self, guint delay) G_GNUC_NON_NULL(1); void fu_device_retry_add_recovery(FuDevice *self, GQuark domain, gint code, FuDeviceRetryFunc func) G_GNUC_NON_NULL(1); gboolean fu_device_retry(FuDevice *self, FuDeviceRetryFunc func, guint count, gpointer user_data, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_device_retry_full(FuDevice *self, FuDeviceRetryFunc func, guint count, guint delay, gpointer user_data, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); void fu_device_sleep(FuDevice *self, guint delay_ms) G_GNUC_NON_NULL(1); void fu_device_sleep_full(FuDevice *self, guint delay_ms, FuProgress *progress) G_GNUC_NON_NULL(1); gboolean fu_device_bind_driver(FuDevice *self, const gchar *subsystem, const gchar *driver, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_device_unbind_driver(FuDevice *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); GHashTable * fu_device_report_metadata_pre(FuDevice *self) G_GNUC_NON_NULL(1); GHashTable * fu_device_report_metadata_post(FuDevice *self) G_GNUC_NON_NULL(1); void fu_device_add_security_attrs(FuDevice *self, FuSecurityAttrs *attrs) G_GNUC_NON_NULL(1, 2); void fu_device_register_private_flag(FuDevice *self, const gchar *flag) G_GNUC_NON_NULL(1, 2); void fu_device_add_private_flag(FuDevice *self, const gchar *flag) G_GNUC_NON_NULL(1, 2); void fu_device_remove_private_flag(FuDevice *self, const gchar *flag) G_GNUC_NON_NULL(1, 2); gboolean fu_device_has_private_flag(FuDevice *self, const gchar *flag) G_GNUC_NON_NULL(1, 2); gboolean fu_device_emit_request(FuDevice *self, FwupdRequest *request, FuProgress *progress, GError **error) G_GNUC_NON_NULL(1, 2); FwupdSecurityAttr * fu_device_security_attr_new(FuDevice *self, const gchar *appstream_id) G_GNUC_NON_NULL(1, 2); gboolean fu_device_set_contents(FuDevice *self, const gchar *filename, GInputStream *stream, FuProgress *progress, GError **error) G_GNUC_NON_NULL(1, 2, 3); gboolean fu_device_set_contents_bytes(FuDevice *self, const gchar *filename, GBytes *blob, FuProgress *progress, GError **error) G_GNUC_NON_NULL(1, 2, 3); GBytes * fu_device_get_contents_bytes(FuDevice *self, const gchar *filename, FuProgress *progress, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_device_query_file_exists(FuDevice *self, const gchar *filename, gboolean *exists, GError **error) G_GNUC_NON_NULL(1, 2, 3); const gchar * fu_device_get_smbios_string(FuDevice *self, guint8 type, guint8 length, guint8 offset, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); void fu_device_add_possible_plugin(FuDevice *self, const gchar *plugin) G_GNUC_NON_NULL(1, 2); const gchar * fu_device_get_instance_str(FuDevice *self, const gchar *key) G_GNUC_NON_NULL(1, 2); void fu_device_add_instance_str(FuDevice *self, const gchar *key, const gchar *value) G_GNUC_NON_NULL(1, 2); void fu_device_add_instance_strsafe(FuDevice *self, const gchar *key, const gchar *value) G_GNUC_NON_NULL(1, 2); void fu_device_add_instance_strup(FuDevice *self, const gchar *key, const gchar *value) G_GNUC_NON_NULL(1, 2); void fu_device_add_instance_u4(FuDevice *self, const gchar *key, guint8 value) G_GNUC_NON_NULL(1, 2); void fu_device_add_instance_u8(FuDevice *self, const gchar *key, guint8 value) G_GNUC_NON_NULL(1, 2); void fu_device_add_instance_u16(FuDevice *self, const gchar *key, guint16 value) G_GNUC_NON_NULL(1, 2); void fu_device_add_instance_u32(FuDevice *self, const gchar *key, guint32 value) G_GNUC_NON_NULL(1, 2); gboolean fu_device_build_instance_id(FuDevice *self, GError **error, const gchar *subsystem, ...) G_GNUC_NULL_TERMINATED G_GNUC_NON_NULL(1, 3); gboolean fu_device_build_instance_id_full(FuDevice *self, FuDeviceInstanceFlag flags, GError **error, const gchar *subsystem, ...) G_GNUC_NULL_TERMINATED G_GNUC_NON_NULL(1, 4); void fu_device_build_vendor_id(FuDevice *self, const gchar *prefix, const gchar *value); void fu_device_build_vendor_id_u16(FuDevice *self, const gchar *prefix, guint16 value); FuDeviceLocker * fu_device_poll_locker_new(FuDevice *self, GError **error) G_GNUC_NON_NULL(1); FuDeviceEvent * fu_device_save_event(FuDevice *self, const gchar *id); FuDeviceEvent * fu_device_load_event(FuDevice *self, const gchar *id, GError **error); fwupd-2.0.10/libfwupdplugin/fu-device.rs000066400000000000000000000004401501337203100201600ustar00rootroot00000000000000// Copyright 2024 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToBitString)] enum FuDeviceInstanceFlag { None = 0, Visible = 1 << 0, Quirks = 1 << 1, Generic = 1 << 2, // added by a baseclass Counterpart = 1 << 3, } fwupd-2.0.10/libfwupdplugin/fu-dfu-firmware-private.h000066400000000000000000000010241501337203100225630ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-dfu-firmware.h" guint8 fu_dfu_firmware_get_footer_len(FuDfuFirmware *self) G_GNUC_NON_NULL(1); GByteArray * fu_dfu_firmware_append_footer(FuDfuFirmware *self, GBytes *contents, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_dfu_firmware_parse_footer(FuDfuFirmware *self, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-dfu-firmware.c000066400000000000000000000235241501337203100211170ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-crc.h" #include "fu-dfu-firmware-private.h" #include "fu-dfu-firmware-struct.h" #include "fu-input-stream.h" /** * FuDfuFirmware: * * A DFU firmware image. * * See also: [class@FuFirmware] */ typedef struct { guint16 vid; guint16 pid; guint16 release; guint16 dfu_version; guint8 footer_len; } FuDfuFirmwarePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuDfuFirmware, fu_dfu_firmware, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_dfu_firmware_get_instance_private(o)) static void fu_dfu_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuDfuFirmware *self = FU_DFU_FIRMWARE(firmware); FuDfuFirmwarePrivate *priv = GET_PRIVATE(self); fu_xmlb_builder_insert_kx(bn, "vendor", priv->vid); fu_xmlb_builder_insert_kx(bn, "product", priv->pid); fu_xmlb_builder_insert_kx(bn, "release", priv->release); fu_xmlb_builder_insert_kx(bn, "dfu_version", priv->dfu_version); } /* private */ guint8 fu_dfu_firmware_get_footer_len(FuDfuFirmware *self) { FuDfuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_FIRMWARE(self), 0x0); return priv->footer_len; } /** * fu_dfu_firmware_get_vid: * @self: a #FuDfuFirmware * * Gets the vendor ID, or 0xffff for no restriction. * * Returns: integer * * Since: 1.3.3 **/ guint16 fu_dfu_firmware_get_vid(FuDfuFirmware *self) { FuDfuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_FIRMWARE(self), 0x0); return priv->vid; } /** * fu_dfu_firmware_get_pid: * @self: a #FuDfuFirmware * * Gets the product ID, or 0xffff for no restriction. * * Returns: integer * * Since: 1.3.3 **/ guint16 fu_dfu_firmware_get_pid(FuDfuFirmware *self) { FuDfuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_FIRMWARE(self), 0x0); return priv->pid; } /** * fu_dfu_firmware_get_release: * @self: a #FuDfuFirmware * * Gets the device ID, or 0xffff for no restriction. * * Returns: integer * * Since: 1.3.3 **/ guint16 fu_dfu_firmware_get_release(FuDfuFirmware *self) { FuDfuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_FIRMWARE(self), 0x0); return priv->release; } /** * fu_dfu_firmware_get_version: * @self: a #FuDfuFirmware * * Gets the file format version with is 0x0100 by default. * * Returns: integer * * Since: 1.3.3 **/ guint16 fu_dfu_firmware_get_version(FuDfuFirmware *self) { FuDfuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_FIRMWARE(self), 0x0); return priv->dfu_version; } /** * fu_dfu_firmware_set_vid: * @self: a #FuDfuFirmware * @vid: vendor ID, or 0xffff if the firmware should match any vendor * * Sets the vendor ID. * * Since: 1.3.3 **/ void fu_dfu_firmware_set_vid(FuDfuFirmware *self, guint16 vid) { FuDfuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DFU_FIRMWARE(self)); priv->vid = vid; } /** * fu_dfu_firmware_set_pid: * @self: a #FuDfuFirmware * @pid: product ID, or 0xffff if the firmware should match any product * * Sets the product ID. * * Since: 1.3.3 **/ void fu_dfu_firmware_set_pid(FuDfuFirmware *self, guint16 pid) { FuDfuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DFU_FIRMWARE(self)); priv->pid = pid; } /** * fu_dfu_firmware_set_release: * @self: a #FuDfuFirmware * @release: release, or 0xffff if the firmware should match any release * * Sets the release for the dfu firmware. * * Since: 1.3.3 **/ void fu_dfu_firmware_set_release(FuDfuFirmware *self, guint16 release) { FuDfuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DFU_FIRMWARE(self)); priv->release = release; } /** * fu_dfu_firmware_set_version: * @self: a #FuDfuFirmware * @version: integer * * Sets the file format version. * * Since: 1.3.3 **/ void fu_dfu_firmware_set_version(FuDfuFirmware *self, guint16 version) { FuDfuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DFU_FIRMWARE(self)); priv->dfu_version = version; } static gboolean fu_dfu_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { gsize streamsz = 0; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz < FU_STRUCT_DFU_FTR_SIZE) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "stream was too small"); return FALSE; } return fu_struct_dfu_ftr_validate_stream(stream, streamsz - FU_STRUCT_DFU_FTR_SIZE, error); } gboolean fu_dfu_firmware_parse_footer(FuDfuFirmware *self, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuDfuFirmwarePrivate *priv = GET_PRIVATE(self); gsize bufsz; const guint8 *buf; g_autoptr(GByteArray) st = NULL; g_autoptr(GBytes) fw = NULL; fw = fu_input_stream_read_bytes(stream, 0, G_MAXSIZE, NULL, error); if (fw == NULL) return FALSE; buf = g_bytes_get_data(fw, &bufsz); /* parse */ st = fu_struct_dfu_ftr_parse_stream(stream, bufsz - FU_STRUCT_DFU_FTR_SIZE, error); if (st == NULL) return FALSE; priv->vid = fu_struct_dfu_ftr_get_vid(st); priv->pid = fu_struct_dfu_ftr_get_pid(st); priv->release = fu_struct_dfu_ftr_get_release(st); priv->dfu_version = fu_struct_dfu_ftr_get_ver(st); priv->footer_len = fu_struct_dfu_ftr_get_len(st); /* verify the checksum */ if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { guint32 crc_new = fu_crc32(FU_CRC_KIND_B32_JAMCRC, buf, bufsz - 4); if (fu_struct_dfu_ftr_get_crc(st) != crc_new) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "CRC failed, expected 0x%04x, got 0x%04x", crc_new, fu_struct_dfu_ftr_get_crc(st)); return FALSE; } } /* check reported length */ if (priv->footer_len > bufsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "reported footer size 0x%04x larger than file 0x%04x", (guint)priv->footer_len, (guint)bufsz); return FALSE; } /* success */ return TRUE; } static gboolean fu_dfu_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuDfuFirmware *self = FU_DFU_FIRMWARE(firmware); FuDfuFirmwarePrivate *priv = GET_PRIVATE(self); gsize streamsz = 0; g_autoptr(GBytes) contents = NULL; /* parse footer */ if (!fu_dfu_firmware_parse_footer(self, stream, flags, error)) return FALSE; /* trim footer off */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; contents = fu_input_stream_read_bytes(stream, 0, streamsz - priv->footer_len, NULL, error); if (contents == NULL) return FALSE; fu_firmware_set_bytes(firmware, contents); return TRUE; } GByteArray * fu_dfu_firmware_append_footer(FuDfuFirmware *self, GBytes *contents, GError **error) { FuDfuFirmwarePrivate *priv = GET_PRIVATE(self); g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GByteArray) st = fu_struct_dfu_ftr_new(); /* add the raw firmware data, the footer-less-CRC, and only then the CRC */ fu_byte_array_append_bytes(buf, contents); fu_struct_dfu_ftr_set_release(st, priv->release); fu_struct_dfu_ftr_set_pid(st, priv->pid); fu_struct_dfu_ftr_set_vid(st, priv->vid); fu_struct_dfu_ftr_set_ver(st, priv->dfu_version); g_byte_array_append(buf, st->data, st->len - sizeof(guint32)); fu_byte_array_append_uint32(buf, fu_crc32(FU_CRC_KIND_B32_JAMCRC, buf->data, buf->len), G_LITTLE_ENDIAN); return g_steal_pointer(&buf); } static GByteArray * fu_dfu_firmware_write(FuFirmware *firmware, GError **error) { FuDfuFirmware *self = FU_DFU_FIRMWARE(firmware); g_autoptr(GPtrArray) images = fu_firmware_get_images(firmware); g_autoptr(GBytes) fw = NULL; /* can only contain one image */ if (images->len > 1) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "DFU only supports writing one image"); return NULL; } /* add footer */ fw = fu_firmware_get_bytes_with_patches(firmware, error); if (fw == NULL) return NULL; return fu_dfu_firmware_append_footer(self, fw, error); } static gboolean fu_dfu_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { FuDfuFirmware *self = FU_DFU_FIRMWARE(firmware); FuDfuFirmwarePrivate *priv = GET_PRIVATE(self); guint64 tmp; /* optional properties */ tmp = xb_node_query_text_as_uint(n, "vendor", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT16) priv->vid = tmp; tmp = xb_node_query_text_as_uint(n, "product", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT16) priv->pid = tmp; tmp = xb_node_query_text_as_uint(n, "release", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT16) priv->release = tmp; tmp = xb_node_query_text_as_uint(n, "dfu_version", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT16) priv->dfu_version = tmp; /* success */ return TRUE; } static void fu_dfu_firmware_init(FuDfuFirmware *self) { FuDfuFirmwarePrivate *priv = GET_PRIVATE(self); priv->vid = 0xffff; priv->pid = 0xffff; priv->release = 0xffff; priv->dfu_version = FU_DFU_FIRMARE_VERSION_DFU_1_0; fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_VID_PID); } static void fu_dfu_firmware_class_init(FuDfuFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_dfu_firmware_validate; firmware_class->export = fu_dfu_firmware_export; firmware_class->parse = fu_dfu_firmware_parse; firmware_class->write = fu_dfu_firmware_write; firmware_class->build = fu_dfu_firmware_build; } /** * fu_dfu_firmware_new: * * Creates a new #FuFirmware of sub type Dfu * * Since: 1.3.3 **/ FuFirmware * fu_dfu_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_DFU_FIRMWARE, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-dfu-firmware.h000066400000000000000000000035721501337203100211250ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_DFU_FIRMWARE (fu_dfu_firmware_get_type()) G_DECLARE_DERIVABLE_TYPE(FuDfuFirmware, fu_dfu_firmware, FU, DFU_FIRMWARE, FuFirmware) struct _FuDfuFirmwareClass { FuFirmwareClass parent_class; }; /** * FU_DFU_FIRMARE_VERSION_UNKNOWN: * * Unknown version of the DFU standard in BCD format. * * Since: 1.6.1 **/ #define FU_DFU_FIRMARE_VERSION_UNKNOWN (0u) /** * FU_DFU_FIRMARE_VERSION_DFU_1_0: * * The 1.0 version of the DFU standard in BCD format. * * Since: 1.6.1 **/ #define FU_DFU_FIRMARE_VERSION_DFU_1_0 (0x0100) /** * FU_DFU_FIRMARE_VERSION_DFU_1_1: * * The 1.1 version of the DFU standard in BCD format. * * Since: 1.6.1 **/ #define FU_DFU_FIRMARE_VERSION_DFU_1_1 (0x0110) /** * FU_DFU_FIRMARE_VERSION_DFUSE: * * The DfuSe version of the DFU standard in BCD format, defined by ST. * * Since: 1.6.1 **/ #define FU_DFU_FIRMARE_VERSION_DFUSE (0x011a) /** * FU_DFU_FIRMARE_VERSION_ATMEL_AVR: * * The Atmel AVR version of the DFU standard in BCD format. * * Since: 1.6.1 **/ #define FU_DFU_FIRMARE_VERSION_ATMEL_AVR (0xff01) FuFirmware * fu_dfu_firmware_new(void); guint16 fu_dfu_firmware_get_vid(FuDfuFirmware *self) G_GNUC_NON_NULL(1); guint16 fu_dfu_firmware_get_pid(FuDfuFirmware *self) G_GNUC_NON_NULL(1); guint16 fu_dfu_firmware_get_release(FuDfuFirmware *self) G_GNUC_NON_NULL(1); guint16 fu_dfu_firmware_get_version(FuDfuFirmware *self) G_GNUC_NON_NULL(1); void fu_dfu_firmware_set_vid(FuDfuFirmware *self, guint16 vid) G_GNUC_NON_NULL(1); void fu_dfu_firmware_set_pid(FuDfuFirmware *self, guint16 pid) G_GNUC_NON_NULL(1); void fu_dfu_firmware_set_release(FuDfuFirmware *self, guint16 release) G_GNUC_NON_NULL(1); void fu_dfu_firmware_set_version(FuDfuFirmware *self, guint16 version) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-dfu-firmware.rs000066400000000000000000000016031501337203100213130ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(New, ValidateStream, ParseStream, Default)] #[repr(C, packed)] struct FuStructDfuFtr { release: u16le, pid: u16le, vid: u16le, ver: u16le, sig: [char; 3] == "UFD", len: u8 = $struct_size, crc: u32le, } #[derive(New, ValidateStream, ParseStream, Default)] #[repr(C, packed)] struct FuStructDfuseHdr { sig: [char; 5] == "DfuSe", ver: u8 == 0x01, image_size: u32le, targets: u8, } #[derive(New, Validate, ParseStream, Default)] #[repr(C, packed)] struct FuStructDfuseImage { sig: [char; 6] == "Target", alt_setting: u8, target_named: u32le, target_name: [char; 255], target_size: u32le, chunks: u32le, } #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructDfuseElement { address: u32le, size: u32le, } fwupd-2.0.10/libfwupdplugin/fu-dfuse-firmware.c000066400000000000000000000200141501337203100214360ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-chunk-private.h" #include "fu-common.h" #include "fu-dfu-firmware-private.h" #include "fu-dfu-firmware-struct.h" #include "fu-dfuse-firmware.h" #include "fu-input-stream.h" /** * FuDfuseFirmware: * * A DfuSe firmware image. * * See also: [class@FuDfuFirmware] */ G_DEFINE_TYPE(FuDfuseFirmware, fu_dfuse_firmware, FU_TYPE_DFU_FIRMWARE) static FuChunk * fu_dfuse_firmware_image_chunk_parse(FuDfuseFirmware *self, GInputStream *stream, gsize *offset, GError **error) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GByteArray) st_ele = NULL; g_autoptr(GBytes) blob = NULL; /* create new chunk */ st_ele = fu_struct_dfuse_element_parse_stream(stream, *offset, error); if (st_ele == NULL) return NULL; *offset += st_ele->len; blob = fu_input_stream_read_bytes(stream, *offset, fu_struct_dfuse_element_get_size(st_ele), NULL, error); if (blob == NULL) return NULL; chk = fu_chunk_bytes_new(blob); fu_chunk_set_address(chk, fu_struct_dfuse_element_get_address(st_ele)); *offset += fu_chunk_get_data_sz(chk); /* success */ return g_steal_pointer(&chk); } static FuFirmware * fu_dfuse_firmware_image_parse_stream(FuDfuseFirmware *self, GInputStream *stream, gsize *offset, GError **error) { guint chunks; g_autoptr(FuFirmware) image = fu_firmware_new(); g_autoptr(GByteArray) st_img = NULL; /* verify image signature */ st_img = fu_struct_dfuse_image_parse_stream(stream, *offset, error); if (st_img == NULL) return NULL; /* set properties */ fu_firmware_set_idx(image, fu_struct_dfuse_image_get_alt_setting(st_img)); if (fu_struct_dfuse_image_get_target_named(st_img) == 0x01) { g_autofree gchar *target_name = fu_struct_dfuse_image_get_target_name(st_img); fu_firmware_set_id(image, target_name); } /* no chunks */ chunks = fu_struct_dfuse_image_get_chunks(st_img); if (chunks == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "DfuSe image has no chunks"); return NULL; } /* parse chunks */ *offset += st_img->len; for (guint j = 0; j < chunks; j++) { g_autoptr(FuChunk) chk = NULL; chk = fu_dfuse_firmware_image_chunk_parse(self, stream, offset, error); if (chk == NULL) return NULL; fu_firmware_add_chunk(image, chk); } /* success */ return g_steal_pointer(&image); } static gboolean fu_dfuse_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_dfuse_hdr_validate_stream(stream, offset, error); } static gboolean fu_dfuse_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuDfuFirmware *dfu_firmware = FU_DFU_FIRMWARE(firmware); gsize offset = 0; gsize streamsz = 0; guint8 targets = 0; g_autoptr(GByteArray) st_hdr = NULL; /* DFU footer first */ if (!fu_dfu_firmware_parse_footer(dfu_firmware, stream, flags, error)) return FALSE; /* parse */ st_hdr = fu_struct_dfuse_hdr_parse_stream(stream, offset, error); if (st_hdr == NULL) return FALSE; /* check image size */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (fu_struct_dfuse_hdr_get_image_size(st_hdr) != streamsz - fu_dfu_firmware_get_footer_len(dfu_firmware)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid DfuSe image size, " "got %" G_GUINT32_FORMAT ", " "expected %" G_GSIZE_FORMAT, fu_struct_dfuse_hdr_get_image_size(st_hdr), streamsz - fu_dfu_firmware_get_footer_len(dfu_firmware)); return FALSE; } /* parse the image targets */ targets = fu_struct_dfuse_hdr_get_targets(st_hdr); offset += st_hdr->len; for (guint i = 0; i < targets; i++) { g_autoptr(FuFirmware) image = NULL; image = fu_dfuse_firmware_image_parse_stream(FU_DFUSE_FIRMWARE(firmware), stream, &offset, error); if (image == NULL) return FALSE; if (!fu_firmware_add_image_full(firmware, image, error)) return FALSE; } return TRUE; } static GBytes * fu_dfuse_firmware_chunk_write(FuDfuseFirmware *self, FuChunk *chk) { g_autoptr(GByteArray) st_ele = fu_struct_dfuse_element_new(); fu_struct_dfuse_element_set_address(st_ele, fu_chunk_get_address(chk)); fu_struct_dfuse_element_set_size(st_ele, fu_chunk_get_data_sz(chk)); g_byte_array_append(st_ele, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); return g_bytes_new(st_ele->data, st_ele->len); } static GBytes * fu_dfuse_firmware_write_image(FuDfuseFirmware *self, FuFirmware *image, GError **error) { gsize totalsz = 0; g_autoptr(GByteArray) st_img = fu_struct_dfuse_image_new(); g_autoptr(GPtrArray) blobs = NULL; g_autoptr(GPtrArray) chunks = NULL; /* get total size */ blobs = g_ptr_array_new_with_free_func((GDestroyNotify)g_bytes_unref); chunks = fu_firmware_get_chunks(image, error); if (chunks == NULL) return NULL; for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); GBytes *bytes = fu_dfuse_firmware_chunk_write(self, chk); g_ptr_array_add(blobs, bytes); totalsz += g_bytes_get_size(bytes); } /* add prefix */ fu_struct_dfuse_image_set_alt_setting(st_img, fu_firmware_get_idx(image)); if (fu_firmware_get_id(image) != NULL) { fu_struct_dfuse_image_set_target_named(st_img, 0x01); if (!fu_struct_dfuse_image_set_target_name(st_img, fu_firmware_get_id(image), error)) return NULL; } fu_struct_dfuse_image_set_target_size(st_img, totalsz); fu_struct_dfuse_image_set_chunks(st_img, chunks->len); /* copy data */ for (guint i = 0; i < blobs->len; i++) { GBytes *blob = g_ptr_array_index(blobs, i); fu_byte_array_append_bytes(st_img, blob); } return g_bytes_new(st_img->data, st_img->len); } static GByteArray * fu_dfuse_firmware_write(FuFirmware *firmware, GError **error) { FuDfuseFirmware *self = FU_DFUSE_FIRMWARE(firmware); gsize totalsz = 0; g_autoptr(GByteArray) st_hdr = fu_struct_dfuse_hdr_new(); g_autoptr(GBytes) blob_noftr = NULL; g_autoptr(GPtrArray) blobs = NULL; g_autoptr(GPtrArray) images = NULL; /* create mutable output buffer */ blobs = g_ptr_array_new_with_free_func((GDestroyNotify)g_bytes_unref); images = fu_firmware_get_images(FU_FIRMWARE(firmware)); for (guint i = 0; i < images->len; i++) { FuFirmware *img = g_ptr_array_index(images, i); g_autoptr(GBytes) blob = NULL; blob = fu_dfuse_firmware_write_image(self, img, error); if (blob == NULL) return NULL; totalsz += g_bytes_get_size(blob); g_ptr_array_add(blobs, g_steal_pointer(&blob)); } /* DfuSe header */ fu_struct_dfuse_hdr_set_image_size(st_hdr, st_hdr->len + totalsz); if (images->len > G_MAXUINT8) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "too many (%u) images to write DfuSe file", images->len); return NULL; } fu_struct_dfuse_hdr_set_targets(st_hdr, (guint8)images->len); /* copy images */ for (guint i = 0; i < blobs->len; i++) { GBytes *blob = g_ptr_array_index(blobs, i); fu_byte_array_append_bytes(st_hdr, blob); } /* return blob */ blob_noftr = g_bytes_new(st_hdr->data, st_hdr->len); return fu_dfu_firmware_append_footer(FU_DFU_FIRMWARE(firmware), blob_noftr, error); } static void fu_dfuse_firmware_init(FuDfuseFirmware *self) { fu_dfu_firmware_set_version(FU_DFU_FIRMWARE(self), FU_DFU_FIRMARE_VERSION_DFUSE); fu_firmware_set_images_max(FU_FIRMWARE(self), 255); } static void fu_dfuse_firmware_class_init(FuDfuseFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_dfuse_firmware_validate; firmware_class->parse = fu_dfuse_firmware_parse; firmware_class->write = fu_dfuse_firmware_write; } /** * fu_dfuse_firmware_new: * * Creates a new #FuFirmware of sub type DfuSe * * Since: 1.5.6 **/ FuFirmware * fu_dfuse_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_DFUSE_FIRMWARE, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-dfuse-firmware.h000066400000000000000000000006521501337203100214510ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-dfu-firmware.h" #define FU_TYPE_DFUSE_FIRMWARE (fu_dfuse_firmware_get_type()) G_DECLARE_DERIVABLE_TYPE(FuDfuseFirmware, fu_dfuse_firmware, FU, DFUSE_FIRMWARE, FuDfuFirmware) struct _FuDfuseFirmwareClass { FuDfuFirmwareClass parent_class; }; FuFirmware * fu_dfuse_firmware_new(void); fwupd-2.0.10/libfwupdplugin/fu-dpaux-device.c000066400000000000000000000305131501337203100211010ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuDpauxDevice" #include "config.h" #include #include "fu-dpaux-device.h" #include "fu-dpaux-struct.h" #include "fu-dump.h" #include "fu-io-channel.h" #include "fu-string.h" /** * FuDpauxDevice * * A Display Port AUX device. * * See also: #FuUdevDevice */ typedef struct { guint32 dpcd_ieee_oui; guint8 dpcd_hw_rev; gchar *dpcd_dev_id; } FuDpauxDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuDpauxDevice, fu_dpaux_device, FU_TYPE_UDEV_DEVICE) enum { PROP_0, PROP_DPCD_IEEE_OUI, PROP_LAST }; #define GET_PRIVATE(o) (fu_dpaux_device_get_instance_private(o)) #define FU_DPAUX_DEVICE_READ_TIMEOUT 10 /* ms */ static void fu_dpaux_device_to_string(FuDevice *device, guint idt, GString *str) { FuDpauxDevice *self = FU_DPAUX_DEVICE(device); FuDpauxDevicePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append_hex(str, idt, "DpcdIeeeOui", priv->dpcd_ieee_oui); fwupd_codec_string_append_hex(str, idt, "DpcdHwRev", priv->dpcd_hw_rev); fwupd_codec_string_append(str, idt, "DpcdDevId", priv->dpcd_dev_id); } static void fu_dpaux_device_invalidate(FuDevice *device) { FuDpauxDevice *self = FU_DPAUX_DEVICE(device); FuDpauxDevicePrivate *priv = GET_PRIVATE(self); priv->dpcd_ieee_oui = 0; priv->dpcd_hw_rev = 0; g_clear_pointer(&priv->dpcd_dev_id, g_free); } static gboolean fu_dpaux_device_probe(FuDevice *device, GError **error) { g_autofree gchar *attr_name = NULL; /* FuUdevDevice->probe */ if (!FU_DEVICE_CLASS(fu_dpaux_device_parent_class)->probe(device, error)) return FALSE; /* get from sysfs if not set from tests */ if (fu_device_get_logical_id(device) == NULL && fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device)) != NULL) { g_autofree gchar *logical_id = NULL; logical_id = g_path_get_basename(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device))); fu_device_set_logical_id(device, logical_id); } if (fu_device_get_physical_id(device) == NULL) { if (!fu_udev_device_set_physical_id(FU_UDEV_DEVICE(device), "pci,drm_dp_aux_dev", error)) return FALSE; } /* only populated on real system, test suite won't have udev_device set */ attr_name = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(device), "name", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (attr_name != NULL) fu_device_set_name(device, attr_name); return TRUE; } static gboolean fu_dpaux_device_setup(FuDevice *device, GError **error) { FuContext *ctx = fu_device_get_context(device); FuDpauxDevice *self = FU_DPAUX_DEVICE(device); FuDpauxDevicePrivate *priv = GET_PRIVATE(self); guint8 buf[FU_STRUCT_DPAUX_DPCD_SIZE] = {0x0}; g_autoptr(GByteArray) st = NULL; /* ignore all Framework FRANDGCP07 BIOS version 3.02 */ if (fu_device_get_name(device) != NULL && g_str_has_prefix(fu_device_get_name(device), "AMDGPU DM") && fu_context_has_hwid_guid(ctx, "32d49d99-414b-55d5-813b-12aaf0335b58")) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "reading %s DPCD is broken on this hardware, " "you need to update the system BIOS", fu_device_get_name(device)); return FALSE; } if (!fu_dpaux_device_read(self, FU_DPAUX_DEVICE_DPCD_OFFSET_BRANCH_DEVICE, buf, sizeof(buf), FU_DPAUX_DEVICE_READ_TIMEOUT, error)) { g_prefix_error(error, "DPCD read failed: "); return FALSE; } st = fu_struct_dpaux_dpcd_parse(buf, sizeof(buf), 0x0, error); if (st == NULL) return FALSE; g_clear_pointer(&priv->dpcd_dev_id, g_free); priv->dpcd_ieee_oui = fu_struct_dpaux_dpcd_get_ieee_oui(st); priv->dpcd_hw_rev = fu_struct_dpaux_dpcd_get_hw_rev(st); priv->dpcd_dev_id = fu_struct_dpaux_dpcd_get_dev_id(st); fu_device_set_version_raw(device, fu_struct_dpaux_dpcd_get_fw_ver(st)); /* build some extra GUIDs */ if (priv->dpcd_ieee_oui != 0x0) fu_device_add_instance_u32(device, "OUI", priv->dpcd_ieee_oui); if (priv->dpcd_hw_rev != 0x0) fu_device_add_instance_u8(device, "HWREV", priv->dpcd_hw_rev); if (priv->dpcd_dev_id != 0x0) fu_device_add_instance_strup(device, "DEVID", priv->dpcd_dev_id); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "DPAUX", "OUI", NULL); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "DPAUX", "OUI", "HWREV", NULL); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "DPAUX", "OUI", "DEVID", NULL); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "DPAUX", "OUI", "HWREV", "DEVID", NULL); /* success */ return TRUE; } /** * fu_dpaux_device_get_dpcd_ieee_oui: * @self: a #FuDpauxDevice * * Gets the DPCD IEEE OUI. * * Returns: integer * * Since: 1.9.8 **/ guint32 fu_dpaux_device_get_dpcd_ieee_oui(FuDpauxDevice *self) { FuDpauxDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DPAUX_DEVICE(self), G_MAXUINT32); return priv->dpcd_ieee_oui; } /** * fu_dpaux_device_set_dpcd_ieee_oui: * @self: a #FuDpauxDevice * @dpcd_ieee_oui: integer * * Sets the DPCD IEEE OUI. * * Since: 1.9.8 **/ void fu_dpaux_device_set_dpcd_ieee_oui(FuDpauxDevice *self, guint32 dpcd_ieee_oui) { FuDpauxDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DPAUX_DEVICE(self)); if (priv->dpcd_ieee_oui == dpcd_ieee_oui) return; priv->dpcd_ieee_oui = dpcd_ieee_oui; g_object_notify(G_OBJECT(self), "dpcd-ieee-oui"); } /** * fu_dpaux_device_get_dpcd_hw_rev: * @self: a #FuDpauxDevice * * Gets the DPCD hardware revision number. * * Returns: integer * * Since: 1.9.8 **/ guint8 fu_dpaux_device_get_dpcd_hw_rev(FuDpauxDevice *self) { FuDpauxDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DPAUX_DEVICE(self), G_MAXUINT8); return priv->dpcd_hw_rev; } /** * fu_dpaux_device_set_dpcd_hw_rev: * @self: a #FuDpauxDevice * @dpcd_hw_rev: integer * * Sets the DPCD hardware revision number. * * Since: 1.9.8 **/ void fu_dpaux_device_set_dpcd_hw_rev(FuDpauxDevice *self, guint8 dpcd_hw_rev) { FuDpauxDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DPAUX_DEVICE(self)); priv->dpcd_hw_rev = dpcd_hw_rev; } /** * fu_dpaux_device_get_dpcd_dev_id: * @self: a #FuDpauxDevice * * Gets the DPCD device ID. * * Returns: integer * * Since: 1.9.8 **/ const gchar * fu_dpaux_device_get_dpcd_dev_id(FuDpauxDevice *self) { FuDpauxDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DPAUX_DEVICE(self), NULL); return priv->dpcd_dev_id; } /** * fu_dpaux_device_set_dpcd_dev_id: * @self: a #FuDpauxDevice * @dpcd_dev_id: integer * * Sets the DPCD device ID. * * Since: 1.9.8 **/ void fu_dpaux_device_set_dpcd_dev_id(FuDpauxDevice *self, const gchar *dpcd_dev_id) { FuDpauxDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DPAUX_DEVICE(self)); if (g_strcmp0(priv->dpcd_dev_id, dpcd_dev_id) == 0) return; g_free(priv->dpcd_dev_id); priv->dpcd_dev_id = g_strdup(dpcd_dev_id); } /** * fu_dpaux_device_write: * @self: a #FuDpauxDevice * @buf: (out): data * @bufsz: size of @data * @error: (nullable): optional return location for an error * * Write multiple bytes to the DP AUX device. * * Returns: %TRUE for success * * Since: 1.9.8 **/ gboolean fu_dpaux_device_write(FuDpauxDevice *self, goffset offset, const guint8 *buf, gsize bufsz, guint timeout_ms, GError **error) { g_autofree gchar *title = g_strdup_printf("DPAUX write @0x%x", (guint)offset); g_return_val_if_fail(FU_IS_DPAUX_DEVICE(self), FALSE); g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* seek, then write */ fu_dump_raw(G_LOG_DOMAIN, title, buf, bufsz); if (!fu_udev_device_seek(FU_UDEV_DEVICE(self), offset, error)) return FALSE; return fu_udev_device_write(FU_UDEV_DEVICE(self), buf, bufsz, timeout_ms, FU_IO_CHANNEL_FLAG_NONE, error); } /** * fu_dpaux_device_read: * @self: a #FuDpauxDevice * @buf: (out): data * @bufsz: size of @data * @error: (nullable): optional return location for an error * * Read multiple bytes from the DP AUX device. * * Returns: %TRUE for success * * Since: 1.9.8 **/ gboolean fu_dpaux_device_read(FuDpauxDevice *self, goffset offset, guint8 *buf, gsize bufsz, guint timeout_ms, GError **error) { g_autofree gchar *title = g_strdup_printf("DPAUX read @0x%x", (guint)offset); g_return_val_if_fail(FU_IS_DPAUX_DEVICE(self), FALSE); g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* seek, then read */ if (!fu_udev_device_seek(FU_UDEV_DEVICE(self), offset, error)) return FALSE; if (!fu_udev_device_read(FU_UDEV_DEVICE(self), buf, bufsz, NULL, timeout_ms, FU_IO_CHANNEL_FLAG_NONE, error)) return FALSE; fu_dump_raw(G_LOG_DOMAIN, title, buf, bufsz); /* success */ return TRUE; } static void fu_dpaux_device_incorporate(FuDevice *device, FuDevice *donor) { FuDpauxDevice *self = FU_DPAUX_DEVICE(device); FuDpauxDevicePrivate *priv = GET_PRIVATE(self); FuDpauxDevicePrivate *priv_donor = GET_PRIVATE(FU_DPAUX_DEVICE(donor)); g_return_if_fail(FU_IS_DPAUX_DEVICE(self)); g_return_if_fail(FU_IS_DPAUX_DEVICE(donor)); /* copy private instance data */ priv->dpcd_ieee_oui = priv_donor->dpcd_ieee_oui; priv->dpcd_hw_rev = priv_donor->dpcd_hw_rev; fu_dpaux_device_set_dpcd_dev_id(self, fu_dpaux_device_get_dpcd_dev_id(FU_DPAUX_DEVICE(donor))); } static gchar * fu_dpaux_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint24(version_raw, fu_device_get_version_format(device)); } static void fu_dpaux_device_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuDpauxDevice *self = FU_DPAUX_DEVICE(object); FuDpauxDevicePrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_DPCD_IEEE_OUI: g_value_set_uint(value, priv->dpcd_ieee_oui); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_dpaux_device_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuDpauxDevice *self = FU_DPAUX_DEVICE(object); switch (prop_id) { case PROP_DPCD_IEEE_OUI: fu_dpaux_device_set_dpcd_ieee_oui(self, g_value_get_uint(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_dpaux_device_init(FuDpauxDevice *self) { fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_NO_GENERIC_GUIDS); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_EMULATED_REQUIRE_SETUP); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_NONBLOCK); } static void fu_dpaux_device_finalize(GObject *object) { FuDpauxDevice *self = FU_DPAUX_DEVICE(object); FuDpauxDevicePrivate *priv = GET_PRIVATE(self); g_free(priv->dpcd_dev_id); G_OBJECT_CLASS(fu_dpaux_device_parent_class)->finalize(object); } static void fu_dpaux_device_class_init(FuDpauxDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); GParamSpec *pspec; object_class->finalize = fu_dpaux_device_finalize; object_class->get_property = fu_dpaux_device_get_property; object_class->set_property = fu_dpaux_device_set_property; device_class->probe = fu_dpaux_device_probe; device_class->setup = fu_dpaux_device_setup; device_class->invalidate = fu_dpaux_device_invalidate; device_class->to_string = fu_dpaux_device_to_string; device_class->incorporate = fu_dpaux_device_incorporate; device_class->convert_version = fu_dpaux_device_convert_version; /** * FuDpauxDevice:dpcd-ieee-oui: * * The DPCD IEEE OUI. * * Since: 1.9.11 */ pspec = g_param_spec_uint("dpcd-ieee-oui", NULL, NULL, 0x0, G_MAXUINT32, 0x0, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_DPCD_IEEE_OUI, pspec); } fwupd-2.0.10/libfwupdplugin/fu-dpaux-device.h000066400000000000000000000032451501337203100211100ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-udev-device.h" #define FU_TYPE_DPAUX_DEVICE (fu_dpaux_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuDpauxDevice, fu_dpaux_device, FU, DPAUX_DEVICE, FuUdevDevice) struct _FuDpauxDeviceClass { FuUdevDeviceClass parent_class; }; #define FU_DPAUX_DEVICE_DPCD_OFFSET_RECEIVER_CAPABILITY 0x0 #define FU_DPAUX_DEVICE_DPCD_OFFSET_LINK_CONFIGURATION 0x100 #define FU_DPAUX_DEVICE_DPCD_OFFSET_LINK_SINK_STATUS 0x200 #define FU_DPAUX_DEVICE_DPCD_OFFSET_SOURCE_DEVICE 0x300 #define FU_DPAUX_DEVICE_DPCD_OFFSET_SINK_DEVICE 0x400 #define FU_DPAUX_DEVICE_DPCD_OFFSET_BRANCH_DEVICE 0x500 guint32 fu_dpaux_device_get_dpcd_ieee_oui(FuDpauxDevice *self) G_GNUC_NON_NULL(1); void fu_dpaux_device_set_dpcd_ieee_oui(FuDpauxDevice *self, guint32 dpcd_ieee_oui) G_GNUC_NON_NULL(1); guint8 fu_dpaux_device_get_dpcd_hw_rev(FuDpauxDevice *self) G_GNUC_NON_NULL(1); void fu_dpaux_device_set_dpcd_hw_rev(FuDpauxDevice *self, guint8 dpcd_hw_rev) G_GNUC_NON_NULL(1); const gchar * fu_dpaux_device_get_dpcd_dev_id(FuDpauxDevice *self) G_GNUC_NON_NULL(1); void fu_dpaux_device_set_dpcd_dev_id(FuDpauxDevice *self, const gchar *dpcd_dev_id) G_GNUC_NON_NULL(1); gboolean fu_dpaux_device_read(FuDpauxDevice *self, goffset offset, guint8 *buf, gsize bufsz, guint timeout_ms, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_dpaux_device_write(FuDpauxDevice *self, goffset offset, const guint8 *buf, gsize bufsz, guint timeout_ms, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-dpaux.rs000066400000000000000000000004611501337203100200450ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(Parse)] #[repr(C, packed)] struct FuStructDpauxDpcd { ieee_oui: u24be, dev_id: [char; 6], hw_rev: u8, fw_ver: u24be, // technically this is u16be, but both MST vendors do this } fwupd-2.0.10/libfwupdplugin/fu-drm-device.c000066400000000000000000000250341501337203100205440ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuDrmDevice" #include "config.h" #ifdef HAVE_LIBDRM #include #endif #include "fu-bytes.h" #include "fu-common-struct.h" #include "fu-drm-device.h" #include "fu-string.h" /** * FuDrmDevice * * A DRM device. * * See also: #FuUdevDevice */ typedef struct { gchar *connector_id; gboolean enabled; FuDisplayState display_state; FuEdid *edid; guint32 crtc_x; guint32 crtc_y; guint32 crtc_width; guint32 crtc_height; } FuDrmDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuDrmDevice, fu_drm_device, FU_TYPE_UDEV_DEVICE) #define GET_PRIVATE(o) (fu_drm_device_get_instance_private(o)) #ifdef HAVE_LIBDRM G_DEFINE_AUTOPTR_CLEANUP_FUNC(drmModeCrtc, drmModeFreeCrtc) G_DEFINE_AUTOPTR_CLEANUP_FUNC(drmModeRes, drmModeFreeResources) #endif static void fu_drm_device_to_string(FuDevice *device, guint idt, GString *str) { FuDrmDevice *self = FU_DRM_DEVICE(device); FuDrmDevicePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append(str, idt, "ConnectorId", priv->connector_id); fwupd_codec_string_append_bool(str, idt, "Enabled", priv->enabled); fwupd_codec_string_append_int(str, idt, "CrctX", priv->crtc_x); fwupd_codec_string_append_int(str, idt, "CrctY", priv->crtc_y); fwupd_codec_string_append_int(str, idt, "CrctWidth", priv->crtc_width); fwupd_codec_string_append_int(str, idt, "CrctHeight", priv->crtc_height); fwupd_codec_string_append(str, idt, "State", fu_display_state_to_string(priv->display_state)); } /** * fu_drm_device_get_state: * @self: a #FuDrmDevice * * Gets the current status of the DRM device. * * Returns: a #FuDisplayState, e.g. %FU_DISPLAY_STATE_CONNECTED * * Since: 1.9.7 **/ FuDisplayState fu_drm_device_get_state(FuDrmDevice *self) { FuDrmDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DRM_DEVICE(self), FU_DISPLAY_STATE_UNKNOWN); return priv->display_state; } /** * fu_drm_device_get_enabled: * @self: a #FuDrmDevice * * Gets if the DRM device is currently enabled. * * Returns: %TRUE if enabled, %FALSE otherwise * * Since: 1.9.7 **/ gboolean fu_drm_device_get_enabled(FuDrmDevice *self) { FuDrmDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DRM_DEVICE(self), FALSE); return priv->enabled; } /** * fu_drm_device_get_connector_id: * @self: a #FuDrmDevice * * Gets the DRM device connector ID. * * Returns: a string, or %NULL if not found * * Since: 1.9.7 **/ const gchar * fu_drm_device_get_connector_id(FuDrmDevice *self) { FuDrmDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DRM_DEVICE(self), NULL); return priv->connector_id; } /** * fu_drm_device_get_crtc_x: * @self: a #FuDrmDevice * * Gets the X position of the preferred CRTC of the DRM device. * * Returns: pixels * * Since: 2.0.0 **/ guint32 fu_drm_device_get_crtc_x(FuDrmDevice *self) { FuDrmDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DRM_DEVICE(self), 0); return priv->crtc_x; } /** * fu_drm_device_get_crtc_y: * @self: a #FuDrmDevice * * Gets the Y position of the preferred CRTC of the DRM device. * * Returns: pixels * * Since: 2.0.0 **/ guint32 fu_drm_device_get_crtc_y(FuDrmDevice *self) { FuDrmDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DRM_DEVICE(self), 0); return priv->crtc_y; } /** * fu_drm_device_get_crtc_width: * @self: a #FuDrmDevice * * Gets the width of the preferred CRTC of the DRM device. * * Returns: pixels * * Since: 2.0.0 **/ guint32 fu_drm_device_get_crtc_width(FuDrmDevice *self) { FuDrmDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DRM_DEVICE(self), 0); return priv->crtc_width; } /** * fu_drm_device_get_crtc_height: * @self: a #FuDrmDevice * * Gets the height of the preferred CRTC of the DRM device. * * Returns: pixels * * Since: 2.0.0 **/ guint32 fu_drm_device_get_crtc_height(FuDrmDevice *self) { FuDrmDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DRM_DEVICE(self), 0); return priv->crtc_height; } /** * fu_drm_device_get_edid: * @self: a #FuDrmDevice * * Returns the cached EDID from the DRM device. * * Returns: (transfer none): a #FuEdid, or %NULL * * Since: 1.9.7 **/ FuEdid * fu_drm_device_get_edid(FuDrmDevice *self) { FuDrmDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DRM_DEVICE(self), NULL); return priv->edid; } static gboolean fu_drm_device_probe(FuDevice *device, GError **error) { g_autoptr(FuDevice) parent = NULL; FuDrmDevice *self = FU_DRM_DEVICE(device); FuDrmDevicePrivate *priv = GET_PRIVATE(self); const gchar *sysfs_path = fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device)); g_autofree gchar *attr_enabled = NULL; g_autofree gchar *attr_status = NULL; g_autofree gchar *attr_connector_id = NULL; g_autofree gchar *physical_id = g_path_get_basename(sysfs_path); /* check if "card" is in the sysfs_path string */ if (g_strstr_len(sysfs_path, -1, "card") == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not a DRM card device"); return FALSE; } /* basic properties */ attr_enabled = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(self), "enabled", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); priv->enabled = g_strcmp0(attr_enabled, "enabled") == 0; attr_status = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(self), "status", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); priv->display_state = fu_display_state_from_string(attr_status); attr_connector_id = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(self), "connector_id", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (attr_connector_id != NULL && attr_connector_id[0] != '\0') priv->connector_id = g_strdup(attr_connector_id); /* this is a heuristic */ if (physical_id != NULL) { g_auto(GStrv) parts = g_strsplit(physical_id, "-", -1); for (guint i = 0; parts[i] != NULL; i++) { if (g_strcmp0(parts[i], "eDP") == 0) fu_device_add_flag(device, FWUPD_DEVICE_FLAG_INTERNAL); } fu_device_set_physical_id(device, physical_id); } /* set the parent */ parent = fu_device_get_backend_parent_with_subsystem(FU_DEVICE(self), "pci", NULL); if (parent != NULL) { fu_device_add_parent_backend_id( device, fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(parent))); } /* read EDID and parse it */ if (priv->display_state == FU_DISPLAY_STATE_CONNECTED) { g_autofree gchar *edid_path = g_build_filename(sysfs_path, "edid", NULL); g_autoptr(FuEdid) edid = fu_edid_new(); g_autoptr(GBytes) blob = NULL; blob = fu_bytes_get_contents(edid_path, error); if (blob == NULL) return FALSE; if (!fu_firmware_parse_bytes(FU_FIRMWARE(edid), blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return FALSE; g_set_object(&priv->edid, edid); /* add instance ID */ fu_device_add_instance_str(device, "VEN", fu_edid_get_pnp_id(edid)); fu_device_add_instance_u16(device, "DEV", fu_edid_get_product_code(edid)); if (!fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_VISIBLE | FU_DEVICE_INSTANCE_FLAG_QUIRKS, error, "DRM", "VEN", "DEV", NULL)) return FALSE; if (fu_edid_get_eisa_id(edid) != NULL) fu_device_set_name(device, fu_edid_get_eisa_id(edid)); if (fu_edid_get_serial_number(edid) != NULL) fu_device_set_serial(device, fu_edid_get_serial_number(edid)); fu_device_build_vendor_id(device, "PNP", fu_edid_get_pnp_id(edid)); } /* success */ return TRUE; } static gboolean fu_drm_device_setup(FuDevice *device, GError **error) { #ifdef HAVE_LIBDRM FuDrmDevice *self = FU_DRM_DEVICE(device); FuDrmDevicePrivate *priv = GET_PRIVATE(self); FuIOChannel *io_channel = fu_udev_device_get_io_channel(FU_UDEV_DEVICE(device)); /* get crtcs */ if (io_channel != NULL) { gint fd = fu_io_channel_unix_get_fd(io_channel); g_autoptr(drmModeRes) res = drmModeGetResources(fd); if (res == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to get DRM resources"); return FALSE; } for (gint i = 0; i < res->count_crtcs; i++) { g_autoptr(drmModeCrtc) crtc = drmModeGetCrtc(fd, res->crtcs[i]); if (crtc == NULL) continue; if (crtc->mode_valid && (crtc->mode.type & DRM_MODE_TYPE_PREFERRED) > 0) { priv->crtc_x = crtc->x; priv->crtc_y = crtc->y; priv->crtc_width = crtc->width; priv->crtc_height = crtc->height; break; } } } #endif /* success */ return TRUE; } static void fu_drm_device_incorporate(FuDevice *device, FuDevice *donor) { FuDrmDevice *self = FU_DRM_DEVICE(device); FuDrmDevice *self_donor = FU_DRM_DEVICE(donor); FuDrmDevicePrivate *priv = GET_PRIVATE(self); FuDrmDevicePrivate *priv_donor = GET_PRIVATE(self_donor); if (priv->display_state == FU_DISPLAY_STATE_UNKNOWN && priv_donor->display_state != FU_DISPLAY_STATE_UNKNOWN) priv->display_state = priv_donor->display_state; if (!priv->enabled && priv_donor->enabled) priv->enabled = priv_donor->enabled; if (priv->connector_id == NULL && priv_donor->connector_id != NULL) priv->connector_id = g_strdup(priv_donor->connector_id); if (priv->edid == NULL && priv_donor->edid != NULL) priv->edid = g_object_ref(priv_donor->edid); if (priv->crtc_x == 0 && priv_donor->crtc_x > 0) priv->crtc_x = priv_donor->crtc_x; if (priv->crtc_y == 0 && priv_donor->crtc_y > 0) priv->crtc_y = priv_donor->crtc_y; if (priv->crtc_width == 0 && priv_donor->crtc_width > 0) priv->crtc_width = priv_donor->crtc_width; if (priv->crtc_height == 0 && priv_donor->crtc_height > 0) priv->crtc_height = priv_donor->crtc_height; } static void fu_drm_device_init(FuDrmDevice *self) { fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); } static void fu_drm_device_finalize(GObject *object) { FuDrmDevice *self = FU_DRM_DEVICE(object); FuDrmDevicePrivate *priv = GET_PRIVATE(self); g_free(priv->connector_id); if (priv->edid != NULL) g_object_unref(priv->edid); G_OBJECT_CLASS(fu_drm_device_parent_class)->finalize(object); } static void fu_drm_device_class_init(FuDrmDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_drm_device_finalize; device_class->probe = fu_drm_device_probe; device_class->incorporate = fu_drm_device_incorporate; device_class->setup = fu_drm_device_setup; device_class->to_string = fu_drm_device_to_string; } fwupd-2.0.10/libfwupdplugin/fu-drm-device.h000066400000000000000000000017261501337203100205530ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-edid.h" #include "fu-udev-device.h" #define FU_TYPE_DRM_DEVICE (fu_drm_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuDrmDevice, fu_drm_device, FU, DRM_DEVICE, FuUdevDevice) struct _FuDrmDeviceClass { FuUdevDeviceClass parent_class; }; gboolean fu_drm_device_get_enabled(FuDrmDevice *self) G_GNUC_NON_NULL(1); FuDisplayState fu_drm_device_get_state(FuDrmDevice *self) G_GNUC_NON_NULL(1); const gchar * fu_drm_device_get_connector_id(FuDrmDevice *self) G_GNUC_NON_NULL(1); guint32 fu_drm_device_get_crtc_x(FuDrmDevice *self) G_GNUC_NON_NULL(1); guint32 fu_drm_device_get_crtc_y(FuDrmDevice *self) G_GNUC_NON_NULL(1); guint32 fu_drm_device_get_crtc_width(FuDrmDevice *self) G_GNUC_NON_NULL(1); guint32 fu_drm_device_get_crtc_height(FuDrmDevice *self) G_GNUC_NON_NULL(1); FuEdid * fu_drm_device_get_edid(FuDrmDevice *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-dummy-efivars.c000066400000000000000000000134171501337203100213170ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuDummyEfivars" #include "config.h" #include "fu-dummy-efivars.h" typedef struct { gchar *guid; gchar *name; guint32 attr; GByteArray *buf; } FuDummyEfivarsKey; struct _FuDummyEfivars { FuEfivars parent_instance; GPtrArray *keys; /* of FuDummyEfivarsKey */ }; G_DEFINE_TYPE(FuDummyEfivars, fu_dummy_efivars, FU_TYPE_EFIVARS) static void fu_dummy_efivars_key_free(FuDummyEfivarsKey *key) { g_free(key->guid); g_free(key->name); g_byte_array_unref(key->buf); g_free(key); } static gboolean fu_dummy_efivars_supported(FuEfivars *efivars, GError **error) { return TRUE; } static FuDummyEfivarsKey * fu_dummy_efivars_find_by_guid_name(FuDummyEfivars *self, const gchar *guid, const gchar *name) { for (guint i = 0; i < self->keys->len; i++) { FuDummyEfivarsKey *key = g_ptr_array_index(self->keys, i); if (g_strcmp0(guid, key->guid) == 0 && g_strcmp0(name, key->name) == 0) return key; } return NULL; } static gboolean fu_dummy_efivars_delete(FuEfivars *efivars, const gchar *guid, const gchar *name, GError **error) { FuDummyEfivars *self = FU_DUMMY_EFIVARS(efivars); FuDummyEfivarsKey *key = fu_dummy_efivars_find_by_guid_name(self, guid, name); if (key == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no key to delete"); return FALSE; } g_ptr_array_remove(self->keys, key); return TRUE; } static gboolean fu_dummy_efivars_delete_with_glob(FuEfivars *efivars, const gchar *guid, const gchar *name_glob, GError **error) { FuDummyEfivars *self = FU_DUMMY_EFIVARS(efivars); g_autoptr(GPtrArray) keys_tmp = g_ptr_array_new(); for (guint i = 0; i < self->keys->len; i++) { FuDummyEfivarsKey *key = g_ptr_array_index(self->keys, i); if (g_pattern_match_simple(name_glob, key->name)) g_ptr_array_add(keys_tmp, key); } for (guint i = 0; i < keys_tmp->len; i++) { FuDummyEfivarsKey *key = g_ptr_array_index(keys_tmp, i); g_ptr_array_remove(self->keys, key); } return TRUE; } static gboolean fu_dummy_efivars_exists_guid(FuDummyEfivars *self, const gchar *guid) { for (guint i = 0; i < self->keys->len; i++) { FuDummyEfivarsKey *key = g_ptr_array_index(self->keys, i); if (g_strcmp0(guid, key->guid) == 0) return TRUE; } return FALSE; } static gboolean fu_dummy_efivars_exists(FuEfivars *efivars, const gchar *guid, const gchar *name) { FuDummyEfivars *self = FU_DUMMY_EFIVARS(efivars); if (name == NULL) return fu_dummy_efivars_exists_guid(self, guid); return fu_dummy_efivars_find_by_guid_name(self, guid, name) != NULL; } static gboolean fu_dummy_efivars_get_data(FuEfivars *efivars, const gchar *guid, const gchar *name, guint8 **data, gsize *data_sz, guint32 *attr, GError **error) { FuDummyEfivars *self = FU_DUMMY_EFIVARS(efivars); FuDummyEfivarsKey *key = fu_dummy_efivars_find_by_guid_name(self, guid, name); if (key == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "%s-%s not found", guid, name); return FALSE; } if (data != NULL) *data = g_memdup2(key->buf->data, key->buf->len); if (data_sz != NULL) *data_sz = key->buf->len; if (attr != NULL) *attr = key->attr; return TRUE; } static GPtrArray * fu_dummy_efivars_get_names(FuEfivars *efivars, const gchar *guid, GError **error) { FuDummyEfivars *self = FU_DUMMY_EFIVARS(efivars); g_autoptr(GPtrArray) names = g_ptr_array_new_with_free_func(g_free); for (guint i = 0; i < self->keys->len; i++) { FuDummyEfivarsKey *key = g_ptr_array_index(self->keys, i); if (g_strcmp0(guid, key->guid) == 0) g_ptr_array_add(names, g_strdup(key->name)); } return g_steal_pointer(&names); } static guint64 fu_dummy_efivars_space_used(FuEfivars *efivars, GError **error) { FuDummyEfivars *self = FU_DUMMY_EFIVARS(efivars); guint64 total = 0; for (guint i = 0; i < self->keys->len; i++) { FuDummyEfivarsKey *key = g_ptr_array_index(self->keys, i); total += 0x20 + strlen(key->name) + key->buf->len; } return total; } static gboolean fu_dummy_efivars_set_data(FuEfivars *efivars, const gchar *guid, const gchar *name, const guint8 *data, gsize sz, guint32 attr, GError **error) { FuDummyEfivars *self = FU_DUMMY_EFIVARS(efivars); FuDummyEfivarsKey *key = fu_dummy_efivars_find_by_guid_name(self, guid, name); if (key == NULL) { key = g_new0(FuDummyEfivarsKey, 1); key->guid = g_strdup(guid); key->name = g_strdup(name); key->buf = g_byte_array_new(); g_ptr_array_add(self->keys, key); } key->attr = attr; g_byte_array_set_size(key->buf, 0); g_byte_array_append(key->buf, data, sz); return TRUE; } static void fu_dummy_efivars_init(FuDummyEfivars *self) { self->keys = g_ptr_array_new_with_free_func((GDestroyNotify)fu_dummy_efivars_key_free); } static void fu_dummy_efivars_finalize(GObject *object) { FuDummyEfivars *self = FU_DUMMY_EFIVARS(object); g_ptr_array_unref(self->keys); G_OBJECT_CLASS(fu_dummy_efivars_parent_class)->finalize(object); } static void fu_dummy_efivars_class_init(FuDummyEfivarsClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuEfivarsClass *efivars_class = FU_EFIVARS_CLASS(klass); efivars_class->supported = fu_dummy_efivars_supported; efivars_class->space_used = fu_dummy_efivars_space_used; efivars_class->exists = fu_dummy_efivars_exists; efivars_class->get_data = fu_dummy_efivars_get_data; efivars_class->set_data = fu_dummy_efivars_set_data; efivars_class->delete = fu_dummy_efivars_delete; efivars_class->delete_with_glob = fu_dummy_efivars_delete_with_glob; efivars_class->get_names = fu_dummy_efivars_get_names; object_class->finalize = fu_dummy_efivars_finalize; } FuEfivars * fu_dummy_efivars_new(void) { return FU_EFIVARS(g_object_new(FU_TYPE_DUMMY_EFIVARS, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-dummy-efivars.h000066400000000000000000000005211501337203100213140ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-efivars.h" #define FU_TYPE_DUMMY_EFIVARS (fu_dummy_efivars_get_type()) G_DECLARE_FINAL_TYPE(FuDummyEfivars, fu_dummy_efivars, FU, DUMMY_EFIVARS, FuEfivars) FuEfivars * fu_dummy_efivars_new(void); fwupd-2.0.10/libfwupdplugin/fu-dump.c000066400000000000000000000064351501337203100174760ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuCommon" #include "config.h" #include "fu-dump.h" /** * fu_dump_full: * @log_domain: (nullable): optional log domain, typically %G_LOG_DOMAIN * @title: (nullable): optional prefix title * @data: buffer to print * @len: the size of @data * @columns: break new lines after this many bytes * @flags: dump flags, e.g. %FU_DUMP_FLAGS_SHOW_ASCII * * Dumps a raw buffer to the screen. * * Since: 1.8.2 **/ void fu_dump_full(const gchar *log_domain, const gchar *title, const guint8 *data, gsize len, guint columns, FuDumpFlags flags) { g_autoptr(GString) str = g_string_new(NULL); g_return_if_fail(columns > 0); /* this is CPU intensive enough to pre-filter here rather than building * the string and handling in a GLogFunc */ if (g_getenv("FWUPD_VERBOSE") == NULL) return; /* optional */ if (title != NULL) g_string_append_printf(str, "%s:", title); /* if more than can fit on one line then start afresh */ if (len > columns || flags & FU_DUMP_FLAGS_SHOW_ADDRESSES) { g_string_append(str, "\n"); } else { for (gsize i = str->len; i < 16; i++) g_string_append(str, " "); } /* offset line */ if (flags & FU_DUMP_FLAGS_SHOW_ADDRESSES) { g_string_append(str, " │ "); for (gsize i = 0; i < columns; i++) { g_string_append_printf(str, "%02x ", (guint)i); if (flags & FU_DUMP_FLAGS_SHOW_ASCII) g_string_append(str, " "); } g_string_append(str, "\n───────┼"); for (gsize i = 0; i < columns; i++) { g_string_append(str, "───"); if (flags & FU_DUMP_FLAGS_SHOW_ASCII) g_string_append(str, "────"); } g_string_append_printf(str, "\n0x%04x │ ", (guint)0); } /* print each row */ for (gsize i = 0; i < len; i++) { g_string_append_printf(str, "%02x ", data[i]); /* optionally print ASCII char */ if (flags & FU_DUMP_FLAGS_SHOW_ASCII) { if (g_ascii_isprint(data[i])) g_string_append_printf(str, "[%c] ", data[i]); else g_string_append(str, "[?] "); } /* new row required */ if (i > 0 && i != len - 1 && (i + 1) % columns == 0) { g_string_append(str, "\n"); if (flags & FU_DUMP_FLAGS_SHOW_ADDRESSES) g_string_append_printf(str, "0x%04x │ ", (guint)i + 1); } } g_log(log_domain, G_LOG_LEVEL_DEBUG, "%s", str->str); } /** * fu_dump_raw: * @log_domain: (nullable): optional log domain, typically %G_LOG_DOMAIN * @title: (nullable): optional prefix title * @data: buffer to print * @len: the size of @data * * Dumps a raw buffer to the screen. * * Since: 1.8.2 **/ void fu_dump_raw(const gchar *log_domain, const gchar *title, const guint8 *data, gsize len) { FuDumpFlags flags = FU_DUMP_FLAGS_NONE; if (len > 64) flags |= FU_DUMP_FLAGS_SHOW_ADDRESSES; fu_dump_full(log_domain, title, data, len, 32, flags); } /** * fu_dump_bytes: * @log_domain: (nullable): optional log domain, typically %G_LOG_DOMAIN * @title: (nullable): optional prefix title * @bytes: data blob * * Dumps a byte buffer to the screen. * * Since: 1.8.2 **/ void fu_dump_bytes(const gchar *log_domain, const gchar *title, GBytes *bytes) { gsize len = 0; const guint8 *data = g_bytes_get_data(bytes, &len); fu_dump_raw(log_domain, title, data, len); } fwupd-2.0.10/libfwupdplugin/fu-dump.h000066400000000000000000000016731501337203100175020ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include /** * FuDumpFlags: * @FU_DUMP_FLAGS_NONE: No flags set * @FU_DUMP_FLAGS_SHOW_ASCII: Show ASCII in debugging dumps * @FU_DUMP_FLAGS_SHOW_ADDRESSES: Show addresses in debugging dumps * * The flags to use when configuring debugging **/ typedef enum { FU_DUMP_FLAGS_NONE = 0, FU_DUMP_FLAGS_SHOW_ASCII = 1 << 0, FU_DUMP_FLAGS_SHOW_ADDRESSES = 1 << 1, /*< private >*/ FU_DUMP_FLAGS_LAST } FuDumpFlags; void fu_dump_raw(const gchar *log_domain, const gchar *title, const guint8 *data, gsize len) G_GNUC_NON_NULL(1); void fu_dump_full(const gchar *log_domain, const gchar *title, const guint8 *data, gsize len, guint columns, FuDumpFlags flags) G_GNUC_NON_NULL(1); void fu_dump_bytes(const gchar *log_domain, const gchar *title, GBytes *bytes) G_GNUC_NON_NULL(1, 3); fwupd-2.0.10/libfwupdplugin/fu-edid.c000066400000000000000000000261731501337203100174370ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuEdid" #include "config.h" #include "fu-byte-array.h" #include "fu-common.h" #include "fu-edid-struct.h" #include "fu-edid.h" #include "fu-string.h" struct _FuEdid { FuFirmware parent_instance; gchar *pnp_id; gchar *serial_number; gchar *product_name; gchar *eisa_id; guint16 product_code; }; G_DEFINE_TYPE(FuEdid, fu_edid, FU_TYPE_FIRMWARE) /** * fu_edid_get_pnp_id: * @self: a #FuEdid * * Gets the PNP ID, e.g. `IBM`. * * Returns: string value, or %NULL for unset * * Since: 1.9.6 **/ const gchar * fu_edid_get_pnp_id(FuEdid *self) { g_return_val_if_fail(FU_IS_EDID(self), NULL); return self->pnp_id; } /** * fu_edid_set_pnp_id: * @self: a #FuEdid * @pnp_id: (nullable): three digit string value, or %NULL * * Sets the PNP ID, which has a length equal to or less than 3 ASCII characters. * * Since: 1.9.6 **/ void fu_edid_set_pnp_id(FuEdid *self, const gchar *pnp_id) { g_return_if_fail(FU_IS_EDID(self)); if (g_strcmp0(self->pnp_id, pnp_id) == 0) return; g_free(self->pnp_id); self->pnp_id = g_strdup(pnp_id); } /** * fu_edid_get_eisa_id: * @self: a #FuEdid * * Gets the EISA ID, e.g. `LTN154P2-L05`. * * Returns: string value, or %NULL for unset * * Since: 1.9.6 **/ const gchar * fu_edid_get_eisa_id(FuEdid *self) { g_return_val_if_fail(FU_IS_EDID(self), NULL); return self->eisa_id; } /** * fu_edid_set_eisa_id: * @self: a #FuEdid * @eisa_id: (nullable): string value, or %NULL * * Sets the EISA ID, which has to be equal to or less than 13 ASCII characters long. * * Since: 1.9.6 **/ void fu_edid_set_eisa_id(FuEdid *self, const gchar *eisa_id) { g_return_if_fail(FU_IS_EDID(self)); if (g_strcmp0(self->eisa_id, eisa_id) == 0) return; g_free(self->eisa_id); self->eisa_id = g_strdup(eisa_id); } /** * fu_edid_get_serial_number: * @self: a #FuEdid * * Gets the serial number. * * Returns: string value, or %NULL for unset * * Since: 1.9.6 **/ const gchar * fu_edid_get_serial_number(FuEdid *self) { g_return_val_if_fail(FU_IS_EDID(self), NULL); return self->serial_number; } /** * fu_edid_set_serial_number: * @self: a #FuEdid * @serial_number: (nullable): string value, or %NULL * * Sets the serial number, which can either be a unsigned 32 bit integer, or a string equal to or * less than 13 ASCII characters long. * * Since: 1.9.6 **/ void fu_edid_set_serial_number(FuEdid *self, const gchar *serial_number) { g_return_if_fail(FU_IS_EDID(self)); if (g_strcmp0(self->serial_number, serial_number) == 0) return; g_free(self->serial_number); self->serial_number = g_strdup(serial_number); } /** * fu_edid_get_product_code: * @self: a #FuEdid * * Gets the product code. * * Returns: integer, or 0x0 for unset * * Since: 1.9.6 **/ guint16 fu_edid_get_product_code(FuEdid *self) { g_return_val_if_fail(FU_IS_EDID(self), G_MAXUINT16); return self->product_code; } /** * fu_edid_set_product_code: * @self: a #FuEdid * @product_code: integer, or 0x0 for unset * * Sets the product code. * * Since: 1.9.6 **/ void fu_edid_set_product_code(FuEdid *self, guint16 product_code) { g_return_if_fail(FU_IS_EDID(self)); self->product_code = product_code; } /* return as soon as the first non-printable char is encountered */ static gchar * fu_edid_strsafe(const guint8 *buf, gsize bufsz) { g_autoptr(GString) str = g_string_new(NULL); for (gsize i = 0; i < bufsz; i++) { if (!g_ascii_isprint((gchar)buf[i])) break; g_string_append_c(str, (gchar)buf[i]); } if (str->len == 0) return NULL; return g_string_free(g_steal_pointer(&str), FALSE); } static gboolean fu_edid_parse_descriptor(FuEdid *self, GInputStream *stream, gsize offset, GError **error) { gsize buf2sz = 0; const guint8 *buf2; g_autoptr(GByteArray) st = NULL; st = fu_struct_edid_descriptor_parse_stream(stream, offset, error); if (st == NULL) return FALSE; /* ignore pixel clock data */ if (fu_struct_edid_descriptor_get_kind(st) != 0x0 || fu_struct_edid_descriptor_get_subkind(st) != 0x0) return TRUE; buf2 = fu_struct_edid_descriptor_get_data(st, &buf2sz); if (fu_struct_edid_descriptor_get_tag(st) == FU_EDID_DESCRIPTOR_TAG_DISPLAY_PRODUCT_NAME) { g_free(self->product_name); self->product_name = fu_edid_strsafe(buf2, buf2sz); } else if (fu_struct_edid_descriptor_get_tag(st) == FU_EDID_DESCRIPTOR_TAG_DISPLAY_PRODUCT_SERIAL_NUMBER) { g_free(self->serial_number); self->serial_number = fu_edid_strsafe(buf2, buf2sz); } else if (fu_struct_edid_descriptor_get_tag(st) == FU_EDID_DESCRIPTOR_TAG_ALPHANUMERIC_DATA_STRING) { g_free(self->eisa_id); self->eisa_id = fu_edid_strsafe(buf2, buf2sz); } /* success */ return TRUE; } static gboolean fu_edid_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuEdid *self = FU_EDID(firmware); const guint8 *manu_id; gsize offset = 0; g_autofree gchar *pnp_id = NULL; g_autoptr(GByteArray) st = NULL; /* parse header */ st = fu_struct_edid_parse_stream(stream, offset, error); if (st == NULL) return FALSE; /* decode the PNP ID from three 5 bit words packed into 2 bytes * /--00--\/--01--\ * 7654321076543210 * |\---/\---/\---/ * R C1 C2 C3 */ manu_id = fu_struct_edid_get_manufacturer_name(st, NULL); pnp_id = g_strdup_printf( "%c%c%c", 'A' + ((manu_id[0] & 0b01111100) >> 2) - 1, 'A' + (((manu_id[0] & 0b00000011) << 3) + ((manu_id[1] & 0b11100000) >> 5)) - 1, 'A' + (manu_id[1] & 0b00011111) - 1); fu_edid_set_pnp_id(self, pnp_id); fu_edid_set_product_code(self, fu_struct_edid_get_product_code(st)); if (fu_struct_edid_get_serial_number(st) != 0x0) { g_autofree gchar *serial_number = g_strdup_printf("%u", fu_struct_edid_get_serial_number(st)); fu_edid_set_serial_number(self, serial_number); } /* parse 4x18 byte sections */ offset += FU_STRUCT_EDID_OFFSET_DATA_BLOCKS; for (guint i = 0; i < 4; i++) { if (!fu_edid_parse_descriptor(self, stream, offset, error)) return FALSE; offset += FU_STRUCT_EDID_DESCRIPTOR_SIZE; } /* success */ return TRUE; } static GByteArray * fu_edid_write(FuFirmware *firmware, GError **error) { FuEdid *self = FU_EDID(firmware); g_autoptr(GByteArray) st = fu_struct_edid_new(); guint64 serial_number = 0; gsize offset_desc = FU_STRUCT_EDID_OFFSET_DATA_BLOCKS; fu_struct_edid_set_product_code(st, self->product_code); /* if this is a integer, store it in the header rather than in a descriptor */ if (fu_strtoull(self->serial_number, &serial_number, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, NULL)) fu_struct_edid_set_serial_number(st, serial_number); /* store descriptors */ if (self->product_name != NULL) { g_autoptr(GByteArray) st_desc = fu_struct_edid_descriptor_new(); fu_struct_edid_descriptor_set_tag(st_desc, FU_EDID_DESCRIPTOR_TAG_DISPLAY_PRODUCT_NAME); if (!fu_struct_edid_descriptor_set_data(st_desc, (const guint8 *)self->product_name, strlen(self->product_name), error)) { g_prefix_error(error, "cannot write product name: "); return NULL; } memcpy(st->data + offset_desc, st_desc->data, st_desc->len); /* nocheck:blocked */ offset_desc += st_desc->len; } if (self->serial_number != NULL) { g_autoptr(GByteArray) st_desc = fu_struct_edid_descriptor_new(); fu_struct_edid_descriptor_set_tag( st_desc, FU_EDID_DESCRIPTOR_TAG_DISPLAY_PRODUCT_SERIAL_NUMBER); if (!fu_struct_edid_descriptor_set_data(st_desc, (const guint8 *)self->serial_number, strlen(self->serial_number), error)) { g_prefix_error(error, "cannot write serial number: "); return NULL; } memcpy(st->data + offset_desc, st_desc->data, st_desc->len); /* nocheck:blocked */ offset_desc += st_desc->len; } if (self->eisa_id != NULL) { g_autoptr(GByteArray) st_desc = fu_struct_edid_descriptor_new(); fu_struct_edid_descriptor_set_tag(st_desc, FU_EDID_DESCRIPTOR_TAG_ALPHANUMERIC_DATA_STRING); if (!fu_struct_edid_descriptor_set_data(st_desc, (const guint8 *)self->eisa_id, strlen(self->eisa_id), error)) { g_prefix_error(error, "cannot write EISA ID: "); return NULL; } memcpy(st->data + offset_desc, st_desc->data, st_desc->len); /* nocheck:blocked */ offset_desc += st_desc->len; } /* success */ return g_steal_pointer(&st); } static gboolean fu_edid_build(FuFirmware *firmware, XbNode *n, GError **error) { FuEdid *self = FU_EDID(firmware); const gchar *value; /* simple properties */ value = xb_node_query_text(n, "pnp_id", NULL); if (value != NULL) { gsize valuesz = strlen(value); if (valuesz != 3) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "pnp_id not supported, %u of %u bytes", (guint)valuesz, (guint)3); return FALSE; } fu_edid_set_pnp_id(self, value); } value = xb_node_query_text(n, "serial_number", NULL); if (value != NULL) { gsize valuesz = strlen(value); if (valuesz > FU_STRUCT_EDID_DESCRIPTOR_SIZE_DATA) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "serial_number not supported, %u of %u bytes", (guint)valuesz, (guint)FU_STRUCT_EDID_DESCRIPTOR_SIZE_DATA); return FALSE; } fu_edid_set_serial_number(self, value); } value = xb_node_query_text(n, "eisa_id", NULL); if (value != NULL) { gsize valuesz = strlen(value); if (valuesz > FU_STRUCT_EDID_DESCRIPTOR_SIZE_DATA) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "eisa_id not supported, %u of %u bytes", (guint)valuesz, (guint)FU_STRUCT_EDID_DESCRIPTOR_SIZE_DATA); return FALSE; } fu_edid_set_eisa_id(self, value); } value = xb_node_query_text(n, "product_code", NULL); if (value != NULL) { guint64 tmp = 0; if (!fu_strtoull(value, &tmp, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_edid_set_product_code(self, tmp); } /* success */ return TRUE; } static void fu_edid_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuEdid *self = FU_EDID(firmware); fu_xmlb_builder_insert_kv(bn, "pnp_id", self->pnp_id); fu_xmlb_builder_insert_kv(bn, "serial_number", self->serial_number); fu_xmlb_builder_insert_kv(bn, "product_name", self->product_name); fu_xmlb_builder_insert_kv(bn, "eisa_id", self->eisa_id); fu_xmlb_builder_insert_kx(bn, "product_code", self->product_code); } static void fu_edid_finalize(GObject *obj) { FuEdid *self = FU_EDID(obj); g_free(self->pnp_id); g_free(self->serial_number); g_free(self->product_name); g_free(self->eisa_id); G_OBJECT_CLASS(fu_edid_parent_class)->finalize(obj); } static void fu_edid_class_init(FuEdidClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_edid_finalize; firmware_class->parse = fu_edid_parse; firmware_class->write = fu_edid_write; firmware_class->build = fu_edid_build; firmware_class->export = fu_edid_export; } static void fu_edid_init(FuEdid *self) { } /** * fu_edid_new: * * Returns: (transfer full): a #FuEdid * * Since: 1.9.6 **/ FuEdid * fu_edid_new(void) { return g_object_new(FU_TYPE_EDID, NULL); } fwupd-2.0.10/libfwupdplugin/fu-edid.h000066400000000000000000000016361501337203100174410ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_EDID (fu_edid_get_type()) G_DECLARE_FINAL_TYPE(FuEdid, fu_edid, FU, EDID, FuFirmware) const gchar * fu_edid_get_pnp_id(FuEdid *self) G_GNUC_NON_NULL(1); void fu_edid_set_pnp_id(FuEdid *self, const gchar *pnp_id) G_GNUC_NON_NULL(1); const gchar * fu_edid_get_eisa_id(FuEdid *self) G_GNUC_NON_NULL(1); void fu_edid_set_eisa_id(FuEdid *self, const gchar *eisa_id) G_GNUC_NON_NULL(1); const gchar * fu_edid_get_serial_number(FuEdid *self) G_GNUC_NON_NULL(1); void fu_edid_set_serial_number(FuEdid *self, const gchar *serial_number) G_GNUC_NON_NULL(1); guint16 fu_edid_get_product_code(FuEdid *self) G_GNUC_NON_NULL(1); void fu_edid_set_product_code(FuEdid *self, guint16 product_code) G_GNUC_NON_NULL(1); FuEdid * fu_edid_new(void) G_GNUC_WARN_UNUSED_RESULT; fwupd-2.0.10/libfwupdplugin/fu-edid.rs000066400000000000000000000023551501337203100176350ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[repr(u8)] enum FuEdidDescriptorTag { DisplayProductSerialNumber = 0xFF, AlphanumericDataString = 0xFE, DisplayRangeLimits = 0xFD, DisplayProductName = 0xFC, ColorPointData = 0xFB, StandardTimingIdentifications = 0xFA, DisplayColorManagementData = 0xF9, CvtTimingCodes = 0xF8, EstablishedTimings = 0xF7, DummyDescriptor = 0x10, } #[derive(ParseStream, New)] #[repr(C, packed)] struct FuStructEdidDescriptor { kind: u16le, subkind: u8, tag: FuEdidDescriptorTag, _reserved: u8, data: [u8; 13], } #[derive(New, ParseStream, Default)] #[repr(C, packed)] struct FuStructEdid { header: [u8; 8] == 0x00FFFFFFFFFFFF00, manufacturer_name: [u8; 2], product_code: u16le, serial_number: u32le, week_of_manufacture: u8, year_of_manufacture: u8, edid_version_number: u8 == 0x1, revision_number: u8 = 0x3, _basic_display_parameters_and_features: [u8; 5], _color_characteristics: [u8; 10], _established_timings: [u8; 3], _standard_timings: [u8; 16], data_blocks: [u8; 72], // should be [FuEdidDescriptor: 4], extension_block_count: u8, checksum: u8, } fwupd-2.0.10/libfwupdplugin/fu-efi-common.c000066400000000000000000000074151501337203100205610ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-efi-common.h" #include "fu-efi-section.h" #include "fu-input-stream.h" #include "fu-lzma-common.h" #include "fu-partial-input-stream.h" /** * fu_efi_guid_to_name: * @guid: A lowercase GUID string, e.g. `8c8ce578-8a3d-4f1c-9935-896185c32dd3` * * Converts a GUID to the known nice name. * * Returns: identifier string, or %NULL if unknown * * Since: 1.6.2 **/ const gchar * fu_efi_guid_to_name(const gchar *guid) { if (g_strcmp0(guid, FU_EFI_VOLUME_GUID_FFS1) == 0) return "Volume:Ffs1"; if (g_strcmp0(guid, FU_EFI_VOLUME_GUID_FFS2) == 0) return "Volume:Ffs2"; if (g_strcmp0(guid, FU_EFI_VOLUME_GUID_FFS3) == 0) return "Volume:Ffs3"; if (g_strcmp0(guid, FU_EFI_VOLUME_GUID_NVRAM_EVSA) == 0) return "Volume:NvramEvsa"; if (g_strcmp0(guid, FU_EFI_VOLUME_GUID_NVRAM_NVAR) == 0) return "Volume:NvramNvar"; if (g_strcmp0(guid, FU_EFI_VOLUME_GUID_NVRAM_EVSA2) == 0) return "Volume:NvramEvsa2"; if (g_strcmp0(guid, FU_EFI_VOLUME_GUID_APPLE_BOOT) == 0) return "Volume:AppleBoot"; if (g_strcmp0(guid, FU_EFI_VOLUME_GUID_PFH1) == 0) return "Volume:Pfh1"; if (g_strcmp0(guid, FU_EFI_VOLUME_GUID_PFH2) == 0) return "Volume:Pfh2"; if (g_strcmp0(guid, FU_EFI_VOLUME_GUID_HP_FS) == 0) return "Volume:HpFs"; if (g_strcmp0(guid, FU_EFI_FILE_GUID_FV_IMAGE) == 0) return "File:FvImage"; if (g_strcmp0(guid, FU_EFI_FILE_GUID_MICROCODE) == 0) return "File:Microcode"; if (g_strcmp0(guid, FU_EFI_FILE_GUID_BIOS_GUARD) == 0) return "File:BiosGuard"; if (g_strcmp0(guid, FU_EFI_SECTION_GUID_LZMA_COMPRESS) == 0) return "Section:LzmaCompress"; if (g_strcmp0(guid, FU_EFI_SECTION_GUID_TIANO_COMPRESS) == 0) return "Section:TianoCompress"; if (g_strcmp0(guid, FU_EFI_SECTION_GUID_SMBIOS_TABLE) == 0) return "Section:SmbiosTable"; if (g_strcmp0(guid, FU_EFI_SECTION_GUID_ESRT_TABLE) == 0) return "Section:EsrtTable"; if (g_strcmp0(guid, FU_EFI_SECTION_GUID_ACPI1_TABLE) == 0) return "Section:Acpi1Table"; if (g_strcmp0(guid, FU_EFI_SECTION_GUID_ACPI2_TABLE) == 0) return "Section:Acpi2Table"; return NULL; } /** * fu_efi_parse_sections: * @firmware: #FuFirmware * @stream: a #GInputStream * @flags: #FuFirmwareParseFlags * @error: (nullable): optional return location for an error * * Parses a UEFI section. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fu_efi_parse_sections(FuFirmware *firmware, GInputStream *stream, gsize offset, FuFirmwareParseFlags flags, GError **error) { gsize streamsz = 0; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; while (offset < streamsz) { g_autoptr(FuFirmware) img = fu_efi_section_new(); g_autoptr(GInputStream) partial_stream = NULL; /* parse maximum payload */ partial_stream = fu_partial_input_stream_new(stream, offset, streamsz - offset, error); if (partial_stream == NULL) { g_prefix_error(error, "failed to cut payload: "); return FALSE; } if (!fu_firmware_parse_stream(img, partial_stream, 0x0, flags | FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error)) { g_prefix_error(error, "failed to parse section of size 0x%x: ", (guint)streamsz); return FALSE; } /* invalid */ if (fu_firmware_get_size(img) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "section had zero size"); return FALSE; } fu_firmware_set_offset(img, offset); if (!fu_firmware_add_image_full(firmware, img, error)) return FALSE; /* next! */ offset += fu_common_align_up(fu_firmware_get_size(img), FU_FIRMWARE_ALIGNMENT_4); } /* success */ return TRUE; } fwupd-2.0.10/libfwupdplugin/fu-efi-common.h000066400000000000000000000035451501337203100205660ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_EFI_VOLUME_GUID_FFS1 "7a9354d9-0468-444a-81ce-0bf617d890df" #define FU_EFI_VOLUME_GUID_FFS2 "8c8ce578-8a3d-4f1c-9935-896185c32dd3" #define FU_EFI_VOLUME_GUID_FFS3 "5473c07a-3dcb-4dca-bd6f-1e9689e7349a" #define FU_EFI_VOLUME_GUID_NVRAM_EVSA "fff12b8d-7696-4c8b-a985-2747075b4f50" #define FU_EFI_VOLUME_GUID_NVRAM_NVAR "cef5b9a3-476d-497f-9fdc-e98143e0422c" #define FU_EFI_VOLUME_GUID_NVRAM_EVSA2 "00504624-8a59-4eeb-bd0f-6b36e96128e0" #define FU_EFI_VOLUME_GUID_APPLE_BOOT "04adeead-61ff-4d31-b6ba-64f8bf901f5a" #define FU_EFI_VOLUME_GUID_PFH1 "16b45da2-7d70-4aea-a58d-760e9ecb841d" #define FU_EFI_VOLUME_GUID_PFH2 "e360bdba-c3ce-46be-8f37-b231e5cb9f35" #define FU_EFI_VOLUME_GUID_HP_FS "372b56df-cc9f-4817-ab97-0a10a92ceaa5" #define FU_EFI_FILE_GUID_FV_IMAGE "4e35fd93-9c72-4c15-8c4b-e77f1db2d792" #define FU_EFI_FILE_GUID_MICROCODE "197db236-f856-4924-90f8-cdf12fb875f3" #define FU_EFI_FILE_GUID_BIOS_GUARD "7934156d-cfce-460e-92f5-a07909a59eca" #define FU_EFI_SECTION_GUID_LZMA_COMPRESS "ee4e5898-3914-4259-9d6e-dc7bd79403cf" #define FU_EFI_SECTION_GUID_TIANO_COMPRESS "a31280ad-481e-41b6-95e8-127f4c984779" #define FU_EFI_SECTION_GUID_SMBIOS_TABLE "eb9d2d31-2d88-11d3-9a16-0090273fc14d" #define FU_EFI_SECTION_GUID_ESRT_TABLE "b122a263-3661-4f68-9929-78f8b0d62180" #define FU_EFI_SECTION_GUID_ACPI1_TABLE "eb9d2d30-2d88-11d3-9a16-0090273fc14d" #define FU_EFI_SECTION_GUID_ACPI2_TABLE "8868e871-e4f1-11d3-bc22-0080c73c8881" const gchar * fu_efi_guid_to_name(const gchar *guid); gboolean fu_efi_parse_sections(FuFirmware *firmware, GInputStream *stream, gsize offset, FuFirmwareParseFlags flags, GError **error) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-efi-device-path-list.c000066400000000000000000000120741501337203100224300ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuEfiDevicePath" #include "config.h" #include "fu-byte-array.h" #include "fu-efi-device-path-list.h" #include "fu-efi-file-path-device-path.h" #include "fu-efi-hard-drive-device-path.h" #include "fu-efi-struct.h" #include "fu-input-stream.h" struct _FuEfiDevicePathList { FuFirmware parent_instance; }; static void fu_efi_device_path_list_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_EXTENDED(FuEfiDevicePathList, fu_efi_device_path_list, FU_TYPE_FIRMWARE, 0, G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fu_efi_device_path_list_codec_iface_init)) #define FU_EFI_DEVICE_PATH_LIST_IMAGES_MAX 1000u static const gchar * fu_efi_device_path_list_gtype_to_member_name(GType gtype) { if (gtype == FU_TYPE_EFI_DEVICE_PATH) return "Dp"; if (gtype == FU_TYPE_EFI_FILE_PATH_DEVICE_PATH) return "Fp"; if (gtype == FU_TYPE_EFI_HARD_DRIVE_DEVICE_PATH) return "Hd"; return g_type_name(gtype); } static void fu_efi_device_path_list_add_json(FwupdCodec *codec, JsonBuilder *builder, FwupdCodecFlags flags) { FuFirmware *firmware = FU_FIRMWARE(codec); g_autoptr(GPtrArray) imgs = fu_firmware_get_images(firmware); json_builder_set_member_name(builder, "DPs"); json_builder_begin_array(builder); for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); json_builder_begin_object(builder); json_builder_set_member_name( builder, fu_efi_device_path_list_gtype_to_member_name(G_OBJECT_TYPE(img))); json_builder_begin_object(builder); fwupd_codec_to_json(FWUPD_CODEC(img), builder, flags); json_builder_end_object(builder); json_builder_end_object(builder); } json_builder_end_array(builder); } static gboolean fu_efi_device_path_list_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { gsize offset = 0; gsize streamsz = 0; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; while (offset < streamsz) { g_autoptr(FuEfiDevicePath) efi_dp = NULL; g_autoptr(GByteArray) st_dp = NULL; /* parse the header so we can work out what GType to create */ st_dp = fu_struct_efi_device_path_parse_stream(stream, offset, error); if (st_dp == NULL) return FALSE; if (fu_struct_efi_device_path_get_type(st_dp) == FU_EFI_DEVICE_PATH_TYPE_END) break; if (fu_struct_efi_device_path_get_type(st_dp) == FU_EFI_DEVICE_PATH_TYPE_MEDIA && fu_struct_efi_device_path_get_subtype(st_dp) == FU_EFI_HARD_DRIVE_DEVICE_PATH_SUBTYPE_FILE_PATH) { efi_dp = FU_EFI_DEVICE_PATH(fu_efi_file_path_device_path_new()); } else if (fu_struct_efi_device_path_get_type(st_dp) == FU_EFI_DEVICE_PATH_TYPE_MEDIA && fu_struct_efi_device_path_get_subtype(st_dp) == FU_EFI_HARD_DRIVE_DEVICE_PATH_SUBTYPE_HARD_DRIVE) { efi_dp = FU_EFI_DEVICE_PATH(fu_efi_hard_drive_device_path_new()); } else { efi_dp = fu_efi_device_path_new(); } fu_firmware_set_offset(FU_FIRMWARE(efi_dp), offset); if (!fu_firmware_parse_stream(FU_FIRMWARE(efi_dp), stream, offset, flags, error)) return FALSE; if (!fu_firmware_add_image_full(firmware, FU_FIRMWARE(efi_dp), error)) return FALSE; offset += fu_firmware_get_size(FU_FIRMWARE(efi_dp)); } /* success */ return TRUE; } static GByteArray * fu_efi_device_path_list_write(FuFirmware *firmware, GError **error) { g_autoptr(GPtrArray) imgs = fu_firmware_get_images(firmware); g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GByteArray) st_dp_end = NULL; /* add each image */ for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); g_autoptr(GBytes) dp_blob = fu_firmware_write(img, error); if (dp_blob == NULL) return NULL; fu_byte_array_append_bytes(buf, dp_blob); } /* add end marker */ st_dp_end = fu_struct_efi_device_path_new(); fu_struct_efi_device_path_set_type(st_dp_end, FU_EFI_DEVICE_PATH_TYPE_END); fu_struct_efi_device_path_set_subtype(st_dp_end, 0xFF); g_byte_array_append(buf, st_dp_end->data, st_dp_end->len); /* success */ return g_steal_pointer(&buf); } static void fu_efi_device_path_list_codec_iface_init(FwupdCodecInterface *iface) { iface->add_json = fu_efi_device_path_list_add_json; } static void fu_efi_device_path_list_class_init(FuEfiDevicePathListClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_efi_device_path_list_parse; firmware_class->write = fu_efi_device_path_list_write; } static void fu_efi_device_path_list_init(FuEfiDevicePathList *self) { g_type_ensure(FU_TYPE_EFI_FILE_PATH_DEVICE_PATH); g_type_ensure(FU_TYPE_EFI_HARD_DRIVE_DEVICE_PATH); fu_firmware_set_images_max(FU_FIRMWARE(self), FU_EFI_DEVICE_PATH_LIST_IMAGES_MAX); } /** * fu_efi_device_path_list_new: * * Creates a new EFI DEVICE_PATH list. * * Returns: (transfer full): a #FuEfiDevicePathList * * Since: 1.9.3 **/ FuEfiDevicePathList * fu_efi_device_path_list_new(void) { return g_object_new(FU_TYPE_EFI_DEVICE_PATH_LIST, NULL); } fwupd-2.0.10/libfwupdplugin/fu-efi-device-path-list.h000066400000000000000000000007341501337203100224350ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-efi-device-path.h" #include "fu-firmware.h" #define FU_TYPE_EFI_DEVICE_PATH_LIST (fu_efi_device_path_list_get_type()) G_DECLARE_FINAL_TYPE(FuEfiDevicePathList, fu_efi_device_path_list, FU, EFI_DEVICE_PATH_LIST, FuFirmware) FuEfiDevicePathList * fu_efi_device_path_list_new(void) G_GNUC_WARN_UNUSED_RESULT; fwupd-2.0.10/libfwupdplugin/fu-efi-device-path.c000066400000000000000000000125151501337203100214570ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuEfiDevicePath" #include "config.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-efi-device-path.h" #include "fu-efi-struct.h" #include "fu-input-stream.h" /** * FuEfiDevicePath: * * See also: [class@FuFirmware] */ typedef struct { guint8 subtype; } FuEfiDevicePathPrivate; static void fu_efi_device_path_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_EXTENDED(FuEfiDevicePath, fu_efi_device_path, FU_TYPE_FIRMWARE, 0, G_ADD_PRIVATE(FuEfiDevicePath) G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fu_efi_device_path_codec_iface_init)) #define GET_PRIVATE(o) (fu_efi_device_path_get_instance_private(o)) static void fu_efi_device_path_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuEfiDevicePath *self = FU_EFI_DEVICE_PATH(firmware); FuEfiDevicePathPrivate *priv = GET_PRIVATE(self); fu_xmlb_builder_insert_kx(bn, "subtype", priv->subtype); } static void fu_efi_device_path_add_json(FwupdCodec *codec, JsonBuilder *builder, FwupdCodecFlags flags) { FuEfiDevicePath *self = FU_EFI_DEVICE_PATH(codec); FuEfiDevicePathPrivate *priv = GET_PRIVATE(self); fwupd_codec_json_append_int(builder, "Subtype", priv->subtype); } /** * fu_efi_device_path_get_subtype: * @self: a #FuEfiDevicePath * * Gets the DEVICE_PATH subtype. * * Returns: integer * * Since: 1.9.3 **/ guint8 fu_efi_device_path_get_subtype(FuEfiDevicePath *self) { FuEfiDevicePathPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_EFI_DEVICE_PATH(self), 0x0); return priv->subtype; } /** * fu_efi_device_path_set_subtype: * @self: a #FuEfiDevicePath * @subtype: integer * * Sets the DEVICE_PATH subtype. * * Since: 1.9.3 **/ void fu_efi_device_path_set_subtype(FuEfiDevicePath *self, guint8 subtype) { FuEfiDevicePathPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_EFI_DEVICE_PATH(self)); priv->subtype = subtype; } static gboolean fu_efi_device_path_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuEfiDevicePath *self = FU_EFI_DEVICE_PATH(firmware); FuEfiDevicePathPrivate *priv = GET_PRIVATE(self); gsize dp_length; gsize streamsz = 0; g_autoptr(GByteArray) st = NULL; /* parse */ st = fu_struct_efi_device_path_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; if (fu_struct_efi_device_path_get_length(st) < FU_STRUCT_EFI_DEVICE_PATH_SIZE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "EFI DEVICE_PATH length invalid: 0x%x", fu_struct_efi_device_path_get_length(st)); return FALSE; } fu_firmware_set_idx(firmware, fu_struct_efi_device_path_get_type(st)); priv->subtype = fu_struct_efi_device_path_get_subtype(st); /* work around a efiboot bug */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; dp_length = fu_struct_efi_device_path_get_length(st); if (streamsz > 4 && dp_length > streamsz) { dp_length = streamsz - 0x4; g_debug("fixing up DP length from 0x%x to 0x%x, because of a bug in efiboot", fu_struct_efi_device_path_get_length(st), (guint)dp_length); } if (dp_length > st->len) { g_autoptr(GBytes) payload = fu_input_stream_read_bytes(stream, st->len, dp_length - st->len, NULL, error); if (payload == NULL) return FALSE; fu_firmware_set_bytes(firmware, payload); } fu_firmware_set_size(firmware, dp_length); /* success */ return TRUE; } static GByteArray * fu_efi_device_path_write(FuFirmware *firmware, GError **error) { FuEfiDevicePath *self = FU_EFI_DEVICE_PATH(firmware); FuEfiDevicePathPrivate *priv = GET_PRIVATE(self); g_autoptr(GByteArray) st = fu_struct_efi_device_path_new(); g_autoptr(GBytes) payload = NULL; /* required */ payload = fu_firmware_get_bytes(firmware, error); if (payload == NULL) return NULL; fu_struct_efi_device_path_set_type(st, fu_firmware_get_idx(firmware)); fu_struct_efi_device_path_set_subtype(st, priv->subtype); fu_struct_efi_device_path_set_length(st, st->len + g_bytes_get_size(payload)); fu_byte_array_append_bytes(st, payload); /* success */ return g_steal_pointer(&st); } static gboolean fu_efi_device_path_build(FuFirmware *firmware, XbNode *n, GError **error) { FuEfiDevicePath *self = FU_EFI_DEVICE_PATH(firmware); FuEfiDevicePathPrivate *priv = GET_PRIVATE(self); guint64 tmp; /* optional properties */ tmp = xb_node_query_text_as_uint(n, "subtype", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT8) priv->subtype = tmp; /* success */ return TRUE; } static void fu_efi_device_path_codec_iface_init(FwupdCodecInterface *iface) { iface->add_json = fu_efi_device_path_add_json; } static void fu_efi_device_path_init(FuEfiDevicePath *self) { } static void fu_efi_device_path_class_init(FuEfiDevicePathClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->export = fu_efi_device_path_export; firmware_class->parse = fu_efi_device_path_parse; firmware_class->write = fu_efi_device_path_write; firmware_class->build = fu_efi_device_path_build; } /** * fu_efi_device_path_new: * * Creates a new EFI DEVICE_PATH * * Since: 1.9.3 **/ FuEfiDevicePath * fu_efi_device_path_new(void) { return g_object_new(FU_TYPE_EFI_DEVICE_PATH, NULL); } fwupd-2.0.10/libfwupdplugin/fu-efi-device-path.h000066400000000000000000000011321501337203100214550ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_EFI_DEVICE_PATH (fu_efi_device_path_get_type()) G_DECLARE_DERIVABLE_TYPE(FuEfiDevicePath, fu_efi_device_path, FU, EFI_DEVICE_PATH, FuFirmware) struct _FuEfiDevicePathClass { FuFirmwareClass parent_class; }; FuEfiDevicePath * fu_efi_device_path_new(void); guint8 fu_efi_device_path_get_subtype(FuEfiDevicePath *self) G_GNUC_NON_NULL(1); void fu_efi_device_path_set_subtype(FuEfiDevicePath *self, guint8 subtype) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-efi-file-path-device-path.c000066400000000000000000000114071501337203100233250ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuEfiDevicePath" #include "config.h" #include "fu-common.h" #include "fu-efi-file-path-device-path.h" #include "fu-efi-struct.h" #include "fu-string.h" /** * FuEfiFilePathDevicePath: * * See also: [class@FuEfiDevicePath] */ struct _FuEfiFilePathDevicePath { FuEfiDevicePath parent_instance; }; static void fu_efi_file_path_device_path_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_EXTENDED(FuEfiFilePathDevicePath, fu_efi_file_path_device_path, FU_TYPE_EFI_DEVICE_PATH, 0, G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fu_efi_file_path_device_path_codec_iface_init)) /** * fu_efi_file_path_device_path_get_name: * @self: a #FuEfiFilePathDevicePath * @error: (nullable): optional return location for an error * * Gets the `DEVICE_PATH` name. * Any backslash characters are automatically converted to forward slashes. * * Returns: (transfer full): UTF-8 filename, or %NULL on error * * Since: 1.9.3 **/ gchar * fu_efi_file_path_device_path_get_name(FuEfiFilePathDevicePath *self, GError **error) { g_autofree gchar *name = NULL; g_autoptr(GBytes) blob = NULL; g_return_val_if_fail(FU_IS_EFI_FILE_PATH_DEVICE_PATH(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); blob = fu_firmware_get_bytes(FU_FIRMWARE(self), error); if (blob == NULL) return NULL; name = fu_utf16_to_utf8_bytes(blob, G_LITTLE_ENDIAN, error); if (name == NULL) return NULL; g_strdelimit(name, "\\", '/'); return g_steal_pointer(&name); } /** * fu_efi_file_path_device_path_set_name: * @self: a #FuEfiFilePathDevicePath * @name: (nullable): a path to a EFI binary, typically prefixed with a backslash * @error: (nullable): optional return location for an error * * Sets the `DEVICE_PATH` name. * Any forward slash characters are automatically converted to backslashes. * * Since: 1.9.3 **/ gboolean fu_efi_file_path_device_path_set_name(FuEfiFilePathDevicePath *self, const gchar *name, GError **error) { g_autoptr(GBytes) blob = NULL; g_return_val_if_fail(FU_IS_EFI_FILE_PATH_DEVICE_PATH(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (name != NULL) { g_autofree gchar *name_bs = g_strdup(name); g_autoptr(GByteArray) buf = NULL; g_strdelimit(name_bs, "/", '\\'); buf = fu_utf8_to_utf16_byte_array(name_bs, G_LITTLE_ENDIAN, FU_UTF_CONVERT_FLAG_APPEND_NUL, error); if (buf == NULL) return FALSE; blob = g_bytes_new(buf->data, buf->len); } else { blob = g_bytes_new(NULL, 0); } fu_firmware_set_bytes(FU_FIRMWARE(self), blob); return TRUE; } static void fu_efi_file_path_device_path_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuEfiFilePathDevicePath *self = FU_EFI_FILE_PATH_DEVICE_PATH(firmware); g_autofree gchar *name = fu_efi_file_path_device_path_get_name(self, NULL); fu_xmlb_builder_insert_kv(bn, "name", name); } static void fu_efi_file_path_device_path_add_json(FwupdCodec *codec, JsonBuilder *builder, FwupdCodecFlags flags) { FuEfiFilePathDevicePath *self = FU_EFI_FILE_PATH_DEVICE_PATH(codec); g_autofree gchar *name = fu_efi_file_path_device_path_get_name(self, NULL); fwupd_codec_json_append(builder, "Name", name); } static gboolean fu_efi_file_path_device_path_build(FuFirmware *firmware, XbNode *n, GError **error) { FuEfiFilePathDevicePath *self = FU_EFI_FILE_PATH_DEVICE_PATH(firmware); g_autoptr(XbNode) data = NULL; /* optional data */ data = xb_node_query_first(n, "name", NULL); if (data != NULL) { if (!fu_efi_file_path_device_path_set_name(self, xb_node_get_text(data), error)) return FALSE; } /* success */ return TRUE; } static void fu_efi_file_path_device_path_codec_iface_init(FwupdCodecInterface *iface) { iface->add_json = fu_efi_file_path_device_path_add_json; } static void fu_efi_file_path_device_path_init(FuEfiFilePathDevicePath *self) { fu_firmware_set_idx(FU_FIRMWARE(self), FU_EFI_DEVICE_PATH_TYPE_MEDIA); fu_efi_device_path_set_subtype(FU_EFI_DEVICE_PATH(self), FU_EFI_HARD_DRIVE_DEVICE_PATH_SUBTYPE_FILE_PATH); } static void fu_efi_file_path_device_path_class_init(FuEfiFilePathDevicePathClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->export = fu_efi_file_path_device_path_export; firmware_class->build = fu_efi_file_path_device_path_build; } /** * fu_efi_file_path_device_path_new: * * Creates a new EFI `DEVICE_PATH`. * * Returns: (transfer full): a #FuEfiFilePathDevicePath * * Since: 1.9.3 **/ FuEfiFilePathDevicePath * fu_efi_file_path_device_path_new(void) { return g_object_new(FU_TYPE_EFI_FILE_PATH_DEVICE_PATH, NULL); } fwupd-2.0.10/libfwupdplugin/fu-efi-file-path-device-path.h000066400000000000000000000013351501337203100233310ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-efi-device-path.h" #define FU_TYPE_EFI_FILE_PATH_DEVICE_PATH (fu_efi_file_path_device_path_get_type()) G_DECLARE_FINAL_TYPE(FuEfiFilePathDevicePath, fu_efi_file_path_device_path, FU, EFI_FILE_PATH_DEVICE_PATH, FuEfiDevicePath) FuEfiFilePathDevicePath * fu_efi_file_path_device_path_new(void); gchar * fu_efi_file_path_device_path_get_name(FuEfiFilePathDevicePath *self, GError **error) G_GNUC_NON_NULL(1); gboolean fu_efi_file_path_device_path_set_name(FuEfiFilePathDevicePath *self, const gchar *name, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-efi-file.c000066400000000000000000000213241501337203100202030ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-efi-common.h" #include "fu-efi-file.h" #include "fu-efi-section.h" #include "fu-efi-struct.h" #include "fu-input-stream.h" #include "fu-partial-input-stream.h" #include "fu-sum.h" /** * FuEfiFile: * * A UEFI file. * * See also: [class@FuFirmware] */ typedef struct { guint8 type; guint8 attrib; } FuEfiFilePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuEfiFile, fu_efi_file, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_efi_file_get_instance_private(o)) #define FU_EFI_FILE_SIZE_MAX 0x1000000 /* 16 MB */ static void fu_efi_file_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuEfiFile *self = FU_EFI_FILE(firmware); FuEfiFilePrivate *priv = GET_PRIVATE(self); fu_xmlb_builder_insert_kx(bn, "attrib", priv->attrib); fu_xmlb_builder_insert_kx(bn, "type", priv->type); if (flags & FU_FIRMWARE_EXPORT_FLAG_INCLUDE_DEBUG) { fu_xmlb_builder_insert_kv(bn, "name", fu_efi_guid_to_name(fu_firmware_get_id(firmware))); fu_xmlb_builder_insert_kv(bn, "type_name", fu_efi_file_type_to_string(priv->type)); } } static guint8 fu_efi_file_hdr_checksum8(GBytes *blob) { gsize bufsz = 0; guint8 checksum = 0; const guint8 *buf = g_bytes_get_data(blob, &bufsz); for (gsize i = 0; i < bufsz; i++) { if (i == FU_STRUCT_EFI_FILE_OFFSET_HDR_CHECKSUM) continue; if (i == FU_STRUCT_EFI_FILE_OFFSET_DATA_CHECKSUM) continue; if (i == FU_STRUCT_EFI_FILE_OFFSET_STATE) continue; checksum += buf[i]; } return (guint8)(0x100u - (guint)checksum); } static gboolean fu_efi_file_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuEfiFile *self = FU_EFI_FILE(firmware); FuEfiFilePrivate *priv = GET_PRIVATE(self); guint32 size = 0x0; g_autofree gchar *guid_str = NULL; g_autoptr(GByteArray) st = NULL; g_autoptr(GInputStream) partial_stream = NULL; /* parse */ st = fu_struct_efi_file_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; priv->type = fu_struct_efi_file_get_type(st); priv->attrib = fu_struct_efi_file_get_attrs(st); guid_str = fwupd_guid_to_string(fu_struct_efi_file_get_name(st), FWUPD_GUID_FLAG_MIXED_ENDIAN); fu_firmware_set_id(firmware, guid_str); /* extended size exists so size must be set to zero */ if (priv->attrib & FU_EFI_FILE_ATTRIB_LARGE_FILE) { if (fu_struct_efi_file_get_size(st) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid FFS size -- expected 0x0 and got 0x%x", (guint)fu_struct_efi_file_get_size(st)); return FALSE; } fu_struct_efi_file_unref(st); st = fu_struct_efi_file2_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; size = fu_struct_efi_file2_get_extended_size(st); } else { size = fu_struct_efi_file_get_size(st); } if (size < st->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid FFS length, got 0x%x", (guint)size); return FALSE; } /* verify header checksum */ if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { guint8 hdr_checksum_verify; g_autoptr(GBytes) hdr_blob = NULL; hdr_blob = fu_input_stream_read_bytes(stream, 0x0, st->len, NULL, error); if (hdr_blob == NULL) return FALSE; hdr_checksum_verify = fu_efi_file_hdr_checksum8(hdr_blob); if (hdr_checksum_verify != fu_struct_efi_file_get_hdr_checksum(st)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "checksum invalid, got %02x, expected %02x", hdr_checksum_verify, fu_struct_efi_file_get_hdr_checksum(st)); return FALSE; } } /* add simple blob */ partial_stream = fu_partial_input_stream_new(stream, st->len, size - st->len, error); if (partial_stream == NULL) { g_prefix_error(error, "failed to cut EFI blob: "); return FALSE; } /* verify data checksum */ if ((priv->attrib & FU_EFI_FILE_ATTRIB_CHECKSUM) > 0 && (flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { guint8 data_checksum_verify = 0; if (!fu_input_stream_compute_sum8(partial_stream, &data_checksum_verify, error)) return FALSE; if (0x100 - data_checksum_verify != fu_struct_efi_file_get_data_checksum(st)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "checksum invalid, got 0x%02x, expected 0x%02x", 0x100u - data_checksum_verify, fu_struct_efi_file_get_data_checksum(st)); return FALSE; } } /* add sections */ if (priv->type != FU_EFI_FILE_TYPE_FFS_PAD && priv->type != FU_EFI_FILE_TYPE_RAW) { if (!fu_efi_parse_sections(firmware, partial_stream, 0, flags, error)) { g_prefix_error(error, "failed to add firmware image: "); return FALSE; } } else { if (!fu_firmware_set_stream(firmware, partial_stream, error)) return FALSE; } /* align size for volume */ fu_firmware_set_size(firmware, fu_common_align_up(size, fu_firmware_get_alignment(firmware))); /* success */ return TRUE; } static GBytes * fu_efi_file_write_sections(FuFirmware *firmware, GError **error) { g_autoptr(GPtrArray) images = fu_firmware_get_images(firmware); g_autoptr(GByteArray) buf = g_byte_array_new(); /* sanity check */ if (fu_firmware_get_alignment(firmware) > FU_FIRMWARE_ALIGNMENT_1M) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "alignment invalid, got 0x%02x", fu_firmware_get_alignment(firmware)); return NULL; } /* no sections defined */ if (images->len == 0) return fu_firmware_get_bytes_with_patches(firmware, error); /* add each section */ for (guint i = 0; i < images->len; i++) { FuFirmware *img = g_ptr_array_index(images, i); g_autoptr(GBytes) blob = NULL; fu_firmware_set_offset(img, buf->len); blob = fu_firmware_write(img, error); if (blob == NULL) return NULL; fu_byte_array_append_bytes(buf, blob); fu_byte_array_align_up(buf, FU_FIRMWARE_ALIGNMENT_4, 0xFF); /* sanity check */ if (buf->len > FU_EFI_FILE_SIZE_MAX) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "EFI file too large, 0x%02x > 0x%02x", (guint)buf->len, (guint)FU_EFI_FILE_SIZE_MAX); return NULL; } } /* success */ return g_bytes_new(buf->data, buf->len); } static GByteArray * fu_efi_file_write(FuFirmware *firmware, GError **error) { FuEfiFile *self = FU_EFI_FILE(firmware); FuEfiFilePrivate *priv = GET_PRIVATE(self); fwupd_guid_t guid = {0x0}; g_autoptr(GByteArray) st = fu_struct_efi_file_new(); g_autoptr(GBytes) blob = NULL; g_autoptr(GBytes) hdr_blob = NULL; /* simple blob for now */ blob = fu_efi_file_write_sections(firmware, error); if (blob == NULL) return NULL; if (fu_firmware_get_id(firmware) != NULL) { if (!fwupd_guid_from_string(fu_firmware_get_id(firmware), &guid, FWUPD_GUID_FLAG_MIXED_ENDIAN, error)) return NULL; } fu_struct_efi_file_set_name(st, &guid); fu_struct_efi_file_set_hdr_checksum(st, 0x0); fu_struct_efi_file_set_data_checksum(st, 0x100 - fu_sum8_bytes(blob)); fu_struct_efi_file_set_type(st, priv->type); fu_struct_efi_file_set_attrs(st, priv->attrib); fu_struct_efi_file_set_size(st, g_bytes_get_size(blob) + st->len); /* fix up header checksum */ hdr_blob = g_bytes_new_static(st->data, st->len); fu_struct_efi_file_set_hdr_checksum(st, fu_efi_file_hdr_checksum8(hdr_blob)); /* success */ fu_byte_array_append_bytes(st, blob); return g_steal_pointer(&st); } static gboolean fu_efi_file_build(FuFirmware *firmware, XbNode *n, GError **error) { FuEfiFile *self = FU_EFI_FILE(firmware); FuEfiFilePrivate *priv = GET_PRIVATE(self); guint64 tmp; /* simple properties */ tmp = xb_node_query_text_as_uint(n, "type", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT8) priv->type = tmp; tmp = xb_node_query_text_as_uint(n, "attrib", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT8) priv->attrib = tmp; /* success */ return TRUE; } static void fu_efi_file_init(FuEfiFile *self) { FuEfiFilePrivate *priv = GET_PRIVATE(self); priv->attrib = FU_EFI_FILE_ATTRIB_NONE; priv->type = FU_EFI_FILE_TYPE_RAW; fu_firmware_set_alignment(FU_FIRMWARE(self), FU_FIRMWARE_ALIGNMENT_8); g_type_ensure(FU_TYPE_EFI_SECTION); } static void fu_efi_file_class_init(FuEfiFileClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_efi_file_parse; firmware_class->write = fu_efi_file_write; firmware_class->build = fu_efi_file_build; firmware_class->export = fu_efi_file_export; } /** * fu_efi_file_new: * * Creates a new #FuFirmware * * Since: 2.0.0 **/ FuFirmware * fu_efi_file_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_EFI_FILE, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-efi-file.h000066400000000000000000000005661501337203100202150ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_EFI_FILE (fu_efi_file_get_type()) G_DECLARE_DERIVABLE_TYPE(FuEfiFile, fu_efi_file, FU, EFI_FILE, FuFirmware) struct _FuEfiFileClass { FuFirmwareClass parent_class; }; FuFirmware * fu_efi_file_new(void); fwupd-2.0.10/libfwupdplugin/fu-efi-filesystem.c000066400000000000000000000077151501337203100214600ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-efi-file.h" #include "fu-efi-filesystem.h" #include "fu-input-stream.h" #include "fu-partial-input-stream.h" /** * FuEfiFilesystem: * * A UEFI filesystem. * * See also: [class@FuFirmware] */ G_DEFINE_TYPE(FuEfiFilesystem, fu_efi_filesystem, FU_TYPE_FIRMWARE) #define FU_EFI_FILESYSTEM_FILES_MAX 10000 #define FU_EFI_FILESYSTEM_SIZE_MAX 0x10000000 /* 256 MB */ static gboolean fu_efi_filesystem_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { gsize offset = 0; gsize streamsz = 0; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; while (offset < streamsz) { g_autoptr(FuFirmware) img = fu_efi_file_new(); g_autoptr(GInputStream) stream_tmp = NULL; gboolean is_freespace = TRUE; /* ignore free space */ for (guint i = 0; i < 0x18; i++) { guint8 tmp = 0; if (!fu_input_stream_read_u8(stream, offset + i, &tmp, error)) return FALSE; if (tmp != 0xff) { is_freespace = FALSE; break; } } if (is_freespace) { g_debug("ignoring free space @0x%x of 0x%x", (guint)offset, (guint)streamsz); break; } stream_tmp = fu_partial_input_stream_new(stream, offset, streamsz - offset, error); if (stream_tmp == NULL) { g_prefix_error(error, "failed to cut EFI file: "); return FALSE; } if (!fu_firmware_parse_stream(img, stream_tmp, 0x0, flags | FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error)) { g_prefix_error(error, "failed to parse EFI file at 0x%x: ", (guint)offset); return FALSE; } fu_firmware_set_offset(firmware, offset); if (!fu_firmware_add_image_full(firmware, img, error)) return FALSE; /* next! */ offset += fu_firmware_get_size(img); } /* success */ return TRUE; } static GByteArray * fu_efi_filesystem_write(FuFirmware *firmware, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GPtrArray) images = fu_firmware_get_images(firmware); /* sanity check */ if (fu_firmware_get_alignment(firmware) > FU_FIRMWARE_ALIGNMENT_1M) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "alignment invalid, got 0x%02x", fu_firmware_get_alignment(firmware)); return NULL; } /* add each file */ for (guint i = 0; i < images->len; i++) { FuFirmware *img = g_ptr_array_index(images, i); g_autoptr(GBytes) blob = NULL; fu_firmware_set_offset(img, buf->len); blob = fu_firmware_write(img, error); if (blob == NULL) return NULL; fu_byte_array_append_bytes(buf, blob); fu_byte_array_align_up(buf, fu_firmware_get_alignment(firmware), 0xFF); /* sanity check */ if (buf->len > FU_EFI_FILESYSTEM_SIZE_MAX) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "EFI filesystem too large, 0x%02x > 0x%02x", (guint)buf->len, (guint)FU_EFI_FILESYSTEM_SIZE_MAX); return NULL; } } /* success */ return g_steal_pointer(&buf); } static void fu_efi_filesystem_init(FuEfiFilesystem *self) { /* if fuzzing, artificially limit the number of files to avoid using large amounts of RSS * when printing the FuEfiFilesystem XML output */ fu_firmware_set_images_max( FU_FIRMWARE(self), g_getenv("FWUPD_FUZZER_RUNNING") == NULL ? FU_EFI_FILESYSTEM_FILES_MAX : 50); fu_firmware_set_alignment(FU_FIRMWARE(self), FU_FIRMWARE_ALIGNMENT_8); g_type_ensure(FU_TYPE_EFI_FILE); } static void fu_efi_filesystem_class_init(FuEfiFilesystemClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_efi_filesystem_parse; firmware_class->write = fu_efi_filesystem_write; } /** * fu_efi_filesystem_new: * * Creates a new #FuFirmware * * Since: 2.0.0 **/ FuFirmware * fu_efi_filesystem_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_EFI_FILESYSTEM, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-efi-filesystem.h000066400000000000000000000006401501337203100214530ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_EFI_FILESYSTEM (fu_efi_filesystem_get_type()) G_DECLARE_DERIVABLE_TYPE(FuEfiFilesystem, fu_efi_filesystem, FU, EFI_FILESYSTEM, FuFirmware) struct _FuEfiFilesystemClass { FuFirmwareClass parent_class; }; FuFirmware * fu_efi_filesystem_new(void); fwupd-2.0.10/libfwupdplugin/fu-efi-hard-drive-device-path.c000066400000000000000000000321541501337203100235030ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuEfiDevicePath" #include "config.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-efi-hard-drive-device-path.h" #include "fu-efi-struct.h" #include "fu-firmware-common.h" #include "fu-mem.h" #include "fu-string.h" /** * FuEfiHardDriveDevicePath: * * See also: [class@FuEfiDevicePath] */ struct _FuEfiHardDriveDevicePath { FuEfiDevicePath parent_instance; guint32 partition_number; guint64 partition_start; /* blocks */ guint64 partition_size; /* blocks */ fwupd_guid_t partition_signature; FuEfiHardDriveDevicePathPartitionFormat partition_format; FuEfiHardDriveDevicePathSignatureType signature_type; }; static void fu_efi_hard_drive_device_path_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_EXTENDED(FuEfiHardDriveDevicePath, fu_efi_hard_drive_device_path, FU_TYPE_EFI_DEVICE_PATH, 0, G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fu_efi_hard_drive_device_path_codec_iface_init)) #define BLOCK_SIZE_FALLBACK 0x200 static void fu_efi_hard_drive_device_path_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuEfiHardDriveDevicePath *self = FU_EFI_HARD_DRIVE_DEVICE_PATH(firmware); g_autofree gchar *partition_signature = fwupd_guid_to_string(&self->partition_signature, FWUPD_GUID_FLAG_MIXED_ENDIAN); fu_xmlb_builder_insert_kx(bn, "partition_number", self->partition_number); fu_xmlb_builder_insert_kx(bn, "partition_start", self->partition_start); fu_xmlb_builder_insert_kx(bn, "partition_size", self->partition_size); fu_xmlb_builder_insert_kv(bn, "partition_signature", partition_signature); fu_xmlb_builder_insert_kv( bn, "partition_format", fu_efi_hard_drive_device_path_partition_format_to_string(self->partition_format)); fu_xmlb_builder_insert_kv( bn, "signature_type", fu_efi_hard_drive_device_path_signature_type_to_string(self->signature_type)); } static void fu_efi_hard_drive_device_path_add_json(FwupdCodec *codec, JsonBuilder *builder, FwupdCodecFlags flags) { FuEfiHardDriveDevicePath *self = FU_EFI_HARD_DRIVE_DEVICE_PATH(codec); g_autofree gchar *partition_signature = fwupd_guid_to_string(&self->partition_signature, FWUPD_GUID_FLAG_MIXED_ENDIAN); fwupd_codec_json_append_int(builder, "PartitionNumber", self->partition_number); fwupd_codec_json_append_int(builder, "PartitionStart", self->partition_start); fwupd_codec_json_append_int(builder, "PartitionSize", self->partition_size); fwupd_codec_json_append(builder, "PartitionSignature", partition_signature); fwupd_codec_json_append( builder, "PartitionFormat", fu_efi_hard_drive_device_path_partition_format_to_string(self->partition_format)); fwupd_codec_json_append( builder, "SignatureType", fu_efi_hard_drive_device_path_signature_type_to_string(self->signature_type)); } static gboolean fu_efi_hard_drive_device_path_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuEfiHardDriveDevicePath *self = FU_EFI_HARD_DRIVE_DEVICE_PATH(firmware); g_autoptr(GByteArray) st = NULL; /* re-parse */ st = fu_struct_efi_hard_drive_device_path_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; self->partition_number = fu_struct_efi_hard_drive_device_path_get_partition_number(st); self->partition_start = fu_struct_efi_hard_drive_device_path_get_partition_start(st); self->partition_size = fu_struct_efi_hard_drive_device_path_get_partition_size(st); memcpy(self->partition_signature, /* nocheck:blocked */ fu_struct_efi_hard_drive_device_path_get_partition_signature(st), sizeof(self->partition_signature)); self->partition_format = fu_struct_efi_hard_drive_device_path_get_partition_format(st); self->signature_type = fu_struct_efi_hard_drive_device_path_get_signature_type(st); /* success */ fu_firmware_set_size(firmware, fu_struct_efi_device_path_get_length(st)); return TRUE; } static GByteArray * fu_efi_hard_drive_device_path_write(FuFirmware *firmware, GError **error) { FuEfiHardDriveDevicePath *self = FU_EFI_HARD_DRIVE_DEVICE_PATH(firmware); g_autoptr(GByteArray) st = fu_struct_efi_hard_drive_device_path_new(); /* required */ fu_struct_efi_hard_drive_device_path_set_partition_number(st, self->partition_number); fu_struct_efi_hard_drive_device_path_set_partition_start(st, self->partition_start); fu_struct_efi_hard_drive_device_path_set_partition_size(st, self->partition_size); fu_struct_efi_hard_drive_device_path_set_partition_signature(st, &self->partition_signature); fu_struct_efi_hard_drive_device_path_set_partition_format(st, self->partition_format); fu_struct_efi_hard_drive_device_path_set_signature_type(st, self->signature_type); /* success */ return g_steal_pointer(&st); } static gboolean fu_efi_hard_drive_device_path_build(FuFirmware *firmware, XbNode *n, GError **error) { FuEfiHardDriveDevicePath *self = FU_EFI_HARD_DRIVE_DEVICE_PATH(firmware); const gchar *tmp; guint64 value = 0; /* optional data */ tmp = xb_node_query_text(n, "partition_number", NULL); if (tmp != NULL) { if (!fu_strtoull(tmp, &value, 0x0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->partition_number = value; } tmp = xb_node_query_text(n, "partition_start", NULL); if (tmp != NULL) { if (!fu_strtoull(tmp, &value, 0x0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->partition_start = value; } tmp = xb_node_query_text(n, "partition_size", NULL); if (tmp != NULL) { if (!fu_strtoull(tmp, &value, 0x0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->partition_size = value; } tmp = xb_node_query_text(n, "partition_signature", NULL); if (tmp != NULL) { if (!fwupd_guid_from_string(tmp, &self->partition_signature, FWUPD_GUID_FLAG_MIXED_ENDIAN, error)) return FALSE; } tmp = xb_node_query_text(n, "partition_format", NULL); if (tmp != NULL) { self->partition_format = fu_efi_hard_drive_device_path_partition_format_from_string(tmp); } tmp = xb_node_query_text(n, "signature_type", NULL); if (tmp != NULL) { self->signature_type = fu_efi_hard_drive_device_path_signature_type_from_string(tmp); } /* success */ return TRUE; } static void fu_efi_hard_drive_device_path_init(FuEfiHardDriveDevicePath *self) { fu_firmware_set_idx(FU_FIRMWARE(self), FU_EFI_DEVICE_PATH_TYPE_MEDIA); fu_efi_device_path_set_subtype(FU_EFI_DEVICE_PATH(self), FU_EFI_HARD_DRIVE_DEVICE_PATH_SUBTYPE_HARD_DRIVE); } static void fu_efi_hard_drive_device_path_class_init(FuEfiHardDriveDevicePathClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->export = fu_efi_hard_drive_device_path_export; firmware_class->parse = fu_efi_hard_drive_device_path_parse; firmware_class->write = fu_efi_hard_drive_device_path_write; firmware_class->build = fu_efi_hard_drive_device_path_build; } /** * fu_efi_hard_drive_device_path_get_partition_signature: * @self: a #FuEfiHardDriveDevicePath * * Gets the DP partition signature. * * Returns: a #fwupd_guid_t * * Since: 2.0.0 **/ const fwupd_guid_t * fu_efi_hard_drive_device_path_get_partition_signature(FuEfiHardDriveDevicePath *self) { g_return_val_if_fail(FU_IS_EFI_HARD_DRIVE_DEVICE_PATH(self), NULL); return &self->partition_signature; } /** * fu_efi_hard_drive_device_path_get_partition_size: * @self: a #FuEfiHardDriveDevicePath * * Gets the DP partition size. * * NOTE: This are multiples of the block size, which can be found using fu_volume_get_block_size() * * Returns: integer * * Since: 2.0.0 **/ guint64 fu_efi_hard_drive_device_path_get_partition_size(FuEfiHardDriveDevicePath *self) { g_return_val_if_fail(FU_IS_EFI_HARD_DRIVE_DEVICE_PATH(self), 0); return self->partition_size; } /** * fu_efi_hard_drive_device_path_get_partition_start: * @self: a #FuEfiHardDriveDevicePath * * Gets the DP partition start. * * NOTE: This are multiples of the block size, which can be found using fu_volume_get_block_size() * * Returns: integer * * Since: 2.0.0 **/ guint64 fu_efi_hard_drive_device_path_get_partition_start(FuEfiHardDriveDevicePath *self) { g_return_val_if_fail(FU_IS_EFI_HARD_DRIVE_DEVICE_PATH(self), 0); return self->partition_start; } /** * fu_efi_hard_drive_device_path_get_partition_number: * @self: a #FuEfiHardDriveDevicePath * * Gets the DP partition number. * * Returns: integer * * Since: 2.0.0 **/ guint32 fu_efi_hard_drive_device_path_get_partition_number(FuEfiHardDriveDevicePath *self) { g_return_val_if_fail(FU_IS_EFI_HARD_DRIVE_DEVICE_PATH(self), 0); return self->partition_number; } /** * fu_efi_hard_drive_device_path_compare: * @dp1: a #FuEfiHardDriveDevicePath * @dp2: a #FuEfiHardDriveDevicePath * * Compares two EFI HardDrive `DEVICE_PATH`s. * * Returns: %TRUE is considered equal * * Since: 2.0.0 **/ gboolean fu_efi_hard_drive_device_path_compare(FuEfiHardDriveDevicePath *dp1, FuEfiHardDriveDevicePath *dp2) { g_return_val_if_fail(FU_IS_EFI_HARD_DRIVE_DEVICE_PATH(dp1), FALSE); g_return_val_if_fail(FU_IS_EFI_HARD_DRIVE_DEVICE_PATH(dp2), FALSE); if (dp1->partition_format != dp2->partition_format) return FALSE; if (dp1->signature_type != dp2->signature_type) return FALSE; if (memcmp(dp1->partition_signature, dp2->partition_signature, sizeof(fwupd_guid_t)) != 0) return FALSE; if (dp1->partition_number != dp2->partition_number) return FALSE; if (dp1->partition_start != dp2->partition_start) return FALSE; if (dp1->partition_size != dp2->partition_size) return FALSE; return TRUE; } static void fu_efi_hard_drive_device_path_codec_iface_init(FwupdCodecInterface *iface) { iface->add_json = fu_efi_hard_drive_device_path_add_json; } /** * fu_efi_hard_drive_device_path_new: * * Creates a new EFI `DEVICE_PATH`. * * Returns: (transfer full): a #FuEfiHardDriveDevicePath * * Since: 1.9.3 **/ FuEfiHardDriveDevicePath * fu_efi_hard_drive_device_path_new(void) { return g_object_new(FU_TYPE_EFI_HARD_DRIVE_DEVICE_PATH, NULL); } /** * fu_efi_hard_drive_device_path_new_from_volume: * @volume: a #FuVolume * @error: (nullable): optional return location for an error * * Creates a new EFI `DEVICE_PATH` for a specific volume. * * Returns: (transfer full): a #FuEfiHardDriveDevicePath, or %NULL on error * * Since: 1.9.3 **/ FuEfiHardDriveDevicePath * fu_efi_hard_drive_device_path_new_from_volume(FuVolume *volume, GError **error) { guint16 block_size; g_autoptr(FuEfiHardDriveDevicePath) self = fu_efi_hard_drive_device_path_new(); g_autofree gchar *partition_kind = NULL; g_autofree gchar *partition_uuid = NULL; g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_VOLUME(volume), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* common to both */ block_size = fu_volume_get_block_size(volume, &error_local); if (block_size == 0) { g_debug("failed to get volume block size, falling back to 0x%x: %s", (guint)BLOCK_SIZE_FALLBACK, error_local->message); block_size = BLOCK_SIZE_FALLBACK; } self->partition_number = fu_volume_get_partition_number(volume); self->partition_start = fu_volume_get_partition_offset(volume) / block_size; self->partition_size = fu_volume_get_partition_size(volume) / block_size; /* set up the rest of the struct */ partition_kind = fu_volume_get_partition_kind(volume); if (partition_kind == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "partition kind required"); return NULL; } partition_uuid = fu_volume_get_partition_uuid(volume); if (partition_uuid == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "partition UUID required"); return NULL; } if (g_strcmp0(partition_kind, FU_VOLUME_KIND_ESP) == 0 || g_strcmp0(partition_kind, FU_VOLUME_KIND_BDP) == 0) { self->partition_format = FU_EFI_HARD_DRIVE_DEVICE_PATH_PARTITION_FORMAT_GUID_PARTITION_TABLE; self->signature_type = FU_EFI_HARD_DRIVE_DEVICE_PATH_SIGNATURE_TYPE_GUID; if (!fwupd_guid_from_string(partition_uuid, &self->partition_signature, FWUPD_GUID_FLAG_MIXED_ENDIAN, error)) return NULL; } else if (g_strcmp0(partition_kind, "0xef") == 0) { guint32 value = 0; g_auto(GStrv) parts = g_strsplit(partition_uuid, "-", -1); if (!fu_firmware_strparse_uint32_safe(parts[0], strlen(parts[0]), 0x0, &value, error)) { g_prefix_error(error, "failed to parse %s: ", parts[0]); return NULL; } if (!fu_memwrite_uint32_safe(self->partition_signature, sizeof(self->partition_signature), 0x0, value, G_LITTLE_ENDIAN, error)) return NULL; self->partition_format = FU_EFI_HARD_DRIVE_DEVICE_PATH_PARTITION_FORMAT_LEGACY_MBR; self->signature_type = FU_EFI_HARD_DRIVE_DEVICE_PATH_SIGNATURE_TYPE_ADDR1B8; } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "partition kind %s not supported", partition_kind); return NULL; } /* success */ return g_steal_pointer(&self); } fwupd-2.0.10/libfwupdplugin/fu-efi-hard-drive-device-path.h000066400000000000000000000023111501337203100235000ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-efi-device-path.h" #include "fu-volume.h" #define FU_TYPE_EFI_HARD_DRIVE_DEVICE_PATH (fu_efi_hard_drive_device_path_get_type()) G_DECLARE_FINAL_TYPE(FuEfiHardDriveDevicePath, fu_efi_hard_drive_device_path, FU, EFI_HARD_DRIVE_DEVICE_PATH, FuEfiDevicePath) FuEfiHardDriveDevicePath * fu_efi_hard_drive_device_path_new(void); FuEfiHardDriveDevicePath * fu_efi_hard_drive_device_path_new_from_volume(FuVolume *volume, GError **error) G_GNUC_NON_NULL(1); gboolean fu_efi_hard_drive_device_path_compare(FuEfiHardDriveDevicePath *dp1, FuEfiHardDriveDevicePath *dp2) G_GNUC_NON_NULL(1, 2); const fwupd_guid_t * fu_efi_hard_drive_device_path_get_partition_signature(FuEfiHardDriveDevicePath *self) G_GNUC_NON_NULL(1); guint64 fu_efi_hard_drive_device_path_get_partition_size(FuEfiHardDriveDevicePath *self) G_GNUC_NON_NULL(1); guint64 fu_efi_hard_drive_device_path_get_partition_start(FuEfiHardDriveDevicePath *self) G_GNUC_NON_NULL(1); guint32 fu_efi_hard_drive_device_path_get_partition_number(FuEfiHardDriveDevicePath *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-efi-load-option.c000066400000000000000000000442671501337203100215240ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuEfiLoadOption" #include "config.h" #include "fu-byte-array.h" #include "fu-common.h" #include "fu-efi-device-path-list.h" #include "fu-efi-file-path-device-path.h" #include "fu-efi-hard-drive-device-path.h" #include "fu-efi-load-option.h" #include "fu-input-stream.h" #include "fu-mem.h" #include "fu-string.h" struct _FuEfiLoadOption { FuFirmware parent_instance; guint32 attrs; FuEfiLoadOptionKind kind; GBytes *optional_data; /* only used when not a hive or path */ GHashTable *metadata; /* element-type: utf8:utf8 */ }; static void fu_efi_load_option_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_EXTENDED(FuEfiLoadOption, fu_efi_load_option, FU_TYPE_FIRMWARE, 0, G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fu_efi_load_option_codec_iface_init)) #define FU_EFI_LOAD_OPTION_DESCRIPTION_SIZE_MAX 0x1000u /* bytes */ #define FU_EFI_LOAD_OPTION_HIVE_HEADER_VERSION_MIN 1 static void fu_efi_load_option_set_optional_data(FuEfiLoadOption *self, GBytes *optional_data) { g_return_if_fail(FU_IS_EFI_LOAD_OPTION(self)); if (self->optional_data != NULL) { g_bytes_unref(self->optional_data); self->optional_data = NULL; } if (optional_data != NULL) self->optional_data = g_bytes_ref(optional_data); } /** * fu_efi_load_option_get_metadata: * @self: a #FuEfiLoadOption * @key: (not nullable): UTF-8 string * @error: (nullable): optional return location for an error * * Gets an optional attribute. * * Returns: UTF-8 string, or %NULL * * Since: 2.0.0 **/ const gchar * fu_efi_load_option_get_metadata(FuEfiLoadOption *self, const gchar *key, GError **error) { const gchar *value; g_return_val_if_fail(FU_IS_EFI_LOAD_OPTION(self), NULL); g_return_val_if_fail(key != NULL, NULL); value = g_hash_table_lookup(self->metadata, key); if (value == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no attribute value for %s", key); return NULL; } /* success */ return value; } /** * fu_efi_load_option_get_kind: * @self: a #FuEfiLoadOption * * Gets the loadopt kind. * * Returns: a #FuEfiLoadOptionKind, e.g. %FU_EFI_LOAD_OPTION_KIND_HIVE * * Since: 2.0.6 **/ FuEfiLoadOptionKind fu_efi_load_option_get_kind(FuEfiLoadOption *self) { g_return_val_if_fail(FU_IS_EFI_LOAD_OPTION(self), FU_EFI_LOAD_OPTION_KIND_UNKNOWN); return self->kind; } /** * fu_efi_load_option_set_kind: * @self: a #FuEfiLoadOption * @kind: a #FuEfiLoadOptionKind, e.g. %FU_EFI_LOAD_OPTION_KIND_HIVE * * Sets the loadopt kind. * * Since: 2.0.6 **/ void fu_efi_load_option_set_kind(FuEfiLoadOption *self, FuEfiLoadOptionKind kind) { g_return_if_fail(FU_IS_EFI_LOAD_OPTION(self)); g_return_if_fail(kind < FU_EFI_LOAD_OPTION_KIND_LAST); self->kind = kind; } /** * fu_efi_load_option_set_metadata: * @self: a #FuEfiLoadOption * @key: (not nullable): UTF-8 string * @value: (nullable): UTF-8 string, or %NULL * * Sets an optional attribute. If @value is %NULL then the key will be removed. * * NOTE: When the key is `Path`, any leading backslash will be stripped automatically and added * back as-required on export. * * Since: 2.0.0 **/ void fu_efi_load_option_set_metadata(FuEfiLoadOption *self, const gchar *key, const gchar *value) { g_return_if_fail(FU_IS_EFI_LOAD_OPTION(self)); g_return_if_fail(key != NULL); if (value == NULL) { g_hash_table_remove(self->metadata, key); return; } /* auto-set something sensible */ if (self->kind == FU_EFI_LOAD_OPTION_KIND_UNKNOWN && g_strcmp0(key, FU_EFI_LOAD_OPTION_METADATA_PATH) == 0) { self->kind = FU_EFI_LOAD_OPTION_KIND_PATH; } else { self->kind = FU_EFI_LOAD_OPTION_KIND_HIVE; } if (g_strcmp0(key, FU_EFI_LOAD_OPTION_METADATA_PATH) == 0 && value != NULL && g_str_has_prefix(value, "\\")) { value++; } g_hash_table_insert(self->metadata, g_strdup(key), g_strdup(value)); } static gboolean fu_efi_load_option_parse_optional_hive(FuEfiLoadOption *self, GInputStream *stream, gsize offset, GError **error) { g_autoptr(FuStructShimHive) st = NULL; guint8 items_count; st = fu_struct_shim_hive_parse_stream(stream, offset, error); if (st == NULL) return FALSE; if (fu_struct_shim_hive_get_header_version(st) < FU_EFI_LOAD_OPTION_HIVE_HEADER_VERSION_MIN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "header version %u is not supported", fu_struct_shim_hive_get_header_version(st)); return FALSE; } offset += fu_struct_shim_hive_get_items_offset(st); /* items */ items_count = fu_struct_shim_hive_get_items_count(st); for (guint i = 0; i < items_count; i++) { guint8 keysz; guint32 valuesz; g_autofree gchar *key = NULL; g_autofree gchar *value = NULL; g_autoptr(FuStructShimHiveItem) st_item = NULL; st_item = fu_struct_shim_hive_item_parse_stream(stream, offset, error); if (st_item == NULL) return FALSE; offset += st_item->len; /* key */ keysz = fu_struct_shim_hive_item_get_key_length(st_item); if (keysz == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "zero key size is not supported"); return FALSE; } key = fu_input_stream_read_string(stream, offset, keysz, error); if (key == NULL) return FALSE; offset += keysz; /* value */ valuesz = fu_struct_shim_hive_item_get_value_length(st_item); if (valuesz > 0) { value = fu_input_stream_read_string(stream, offset, valuesz, error); if (value == NULL) return FALSE; offset += valuesz; } fu_efi_load_option_set_metadata(self, key, value != NULL ? value : ""); } /* success */ return TRUE; } static gboolean fu_efi_load_option_parse_optional_path(FuEfiLoadOption *self, GBytes *opt_blob, GError **error) { g_autofree gchar *optional_path = NULL; /* convert to UTF-8 */ optional_path = fu_utf16_to_utf8_bytes(opt_blob, G_LITTLE_ENDIAN, error); if (optional_path == NULL) return FALSE; /* check is ASCII */ if (optional_path[0] == '\0' || !g_str_is_ascii(optional_path)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not ASCII data: %s", optional_path); return FALSE; } fu_efi_load_option_set_metadata(self, FU_EFI_LOAD_OPTION_METADATA_PATH, optional_path); /* success */ return TRUE; } static gboolean fu_efi_load_option_parse_optional(FuEfiLoadOption *self, GInputStream *stream, gsize offset, GError **error) { gsize streamsz = 0; g_autoptr(GBytes) opt_blob = NULL; g_autoptr(GError) error_hive = NULL; g_autoptr(GError) error_path = NULL; /* try hive structure first */ if (!fu_efi_load_option_parse_optional_hive(self, stream, offset, &error_hive)) { if (!g_error_matches(error_hive, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA)) { g_propagate_error(error, g_steal_pointer(&error_hive)); return FALSE; } g_debug("not a shim hive, ignoring: %s", error_hive->message); } else { self->kind = FU_EFI_LOAD_OPTION_KIND_HIVE; return TRUE; } /* then UCS-2 path, and on ASCII failure just treat as a raw data blob */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; opt_blob = fu_input_stream_read_bytes(stream, offset, streamsz - offset, NULL, error); if (opt_blob == NULL) return FALSE; if (!fu_efi_load_option_parse_optional_path(self, opt_blob, &error_path)) { g_debug("not a path, saving as raw blob: %s", error_path->message); fu_efi_load_option_set_optional_data(self, opt_blob); } else { self->kind = FU_EFI_LOAD_OPTION_KIND_PATH; return TRUE; } /* success */ self->kind = FU_EFI_LOAD_OPTION_KIND_DATA; return TRUE; } static gboolean fu_efi_load_option_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuEfiLoadOption *self = FU_EFI_LOAD_OPTION(firmware); gsize offset = 0; gsize streamsz = 0; g_autofree gchar *id = NULL; g_autoptr(FuEfiDevicePathList) device_path_list = fu_efi_device_path_list_new(); g_autoptr(GByteArray) buf_utf16 = g_byte_array_new(); g_autoptr(GByteArray) st = NULL; /* parse header */ st = fu_struct_efi_load_option_parse_stream(stream, offset, error); if (st == NULL) return FALSE; self->attrs = fu_struct_efi_load_option_get_attrs(st); offset += st->len; /* parse UTF-16 description */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; for (; offset < streamsz; offset += 2) { guint16 tmp = 0; if (buf_utf16->len > FU_EFI_LOAD_OPTION_DESCRIPTION_SIZE_MAX) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "description was too long, limit is 0x%x chars", FU_EFI_LOAD_OPTION_DESCRIPTION_SIZE_MAX / 2); return FALSE; } if (!fu_input_stream_read_u16(stream, offset, &tmp, G_LITTLE_ENDIAN, error)) return FALSE; if (tmp == 0) break; fu_byte_array_append_uint16(buf_utf16, tmp, G_LITTLE_ENDIAN); } id = fu_utf16_to_utf8_byte_array(buf_utf16, G_LITTLE_ENDIAN, error); if (id == NULL) return FALSE; fu_firmware_set_id(firmware, id); offset += 2; /* parse dp blob */ if (!fu_firmware_parse_stream(FU_FIRMWARE(device_path_list), stream, offset, flags, error)) return FALSE; if (!fu_firmware_add_image_full(firmware, FU_FIRMWARE(device_path_list), error)) return FALSE; offset += fu_struct_efi_load_option_get_dp_size(st); /* optional data */ if (offset < streamsz) { if (!fu_efi_load_option_parse_optional(self, stream, offset, error)) return FALSE; } /* success */ return TRUE; } static GByteArray * fu_efi_load_option_write_hive(FuEfiLoadOption *self, GError **error) { GHashTableIter iter; guint items_count = g_hash_table_size(self->metadata); const gchar *key; const gchar *value; g_autoptr(FuStructShimHive) st = fu_struct_shim_hive_new(); fu_struct_shim_hive_set_items_count(st, items_count); fu_struct_shim_hive_set_items_offset(st, FU_STRUCT_SHIM_HIVE_SIZE); g_hash_table_iter_init(&iter, self->metadata); while (g_hash_table_iter_next(&iter, (gpointer *)&key, (gpointer *)&value)) { guint32 keysz = strlen(key); g_autoptr(FuStructShimHiveItem) st_item = fu_struct_shim_hive_item_new(); g_autoptr(GString) value_safe = g_string_new(value); /* required prefix for a path */ if (g_strcmp0(key, FU_EFI_LOAD_OPTION_METADATA_PATH) == 0 && value_safe->len > 0 && !g_str_has_prefix(value_safe->str, "\\")) { g_string_prepend(value_safe, "\\"); } fu_struct_shim_hive_item_set_key_length(st_item, keysz); fu_struct_shim_hive_item_set_value_length(st_item, value_safe->len); if (keysz > 0) g_byte_array_append(st_item, (const guint8 *)key, keysz); if (value_safe->len > 0) { g_byte_array_append(st_item, (const guint8 *)value_safe->str, value_safe->len); } /* add to hive */ g_byte_array_append(st, st_item->data, st_item->len); } /* this covers all items, and so has to be done last */ fu_struct_shim_hive_set_crc32(st, fu_crc32(FU_CRC_KIND_B32_STANDARD, st->data, st->len)); /* success */ return g_steal_pointer(&st); } static GByteArray * fu_efi_load_option_write_path(FuEfiLoadOption *self, GError **error) { g_autoptr(GByteArray) buf = NULL; const gchar *path = g_hash_table_lookup(self->metadata, FU_EFI_LOAD_OPTION_METADATA_PATH); g_autoptr(GString) str = g_string_new(path); /* is required if a path */ if (!g_str_has_prefix(str->str, "\\")) g_string_prepend(str, "\\"); buf = fu_utf8_to_utf16_byte_array(str->str, G_LITTLE_ENDIAN, FU_UTF_CONVERT_FLAG_APPEND_NUL, error); if (buf == NULL) return NULL; return g_steal_pointer(&buf); } static GByteArray * fu_efi_load_option_write(FuFirmware *firmware, GError **error) { FuEfiLoadOption *self = FU_EFI_LOAD_OPTION(firmware); g_autoptr(GByteArray) buf_utf16 = NULL; g_autoptr(GByteArray) st = fu_struct_efi_load_option_new(); g_autoptr(GBytes) dpbuf = NULL; /* header */ fu_struct_efi_load_option_set_attrs(st, self->attrs); /* label */ if (fu_firmware_get_id(firmware) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "firmware ID required"); return NULL; } buf_utf16 = fu_utf8_to_utf16_byte_array(fu_firmware_get_id(firmware), G_LITTLE_ENDIAN, FU_UTF_CONVERT_FLAG_APPEND_NUL, error); if (buf_utf16 == NULL) return NULL; g_byte_array_append(st, buf_utf16->data, buf_utf16->len); /* dpbuf */ dpbuf = fu_firmware_get_image_by_gtype_bytes(firmware, FU_TYPE_EFI_DEVICE_PATH_LIST, error); if (dpbuf == NULL) return NULL; fu_struct_efi_load_option_set_dp_size(st, g_bytes_get_size(dpbuf)); fu_byte_array_append_bytes(st, dpbuf); /* hive, path or data */ if (self->kind == FU_EFI_LOAD_OPTION_KIND_HIVE) { g_autoptr(GByteArray) buf_hive = NULL; buf_hive = fu_efi_load_option_write_hive(self, error); if (buf_hive == NULL) return NULL; g_byte_array_append(st, buf_hive->data, buf_hive->len); fu_byte_array_align_up(st, FU_FIRMWARE_ALIGNMENT_512, 0x0); /* make atomic */ } else if (self->kind == FU_EFI_LOAD_OPTION_KIND_PATH) { g_autoptr(GByteArray) buf_path = NULL; buf_path = fu_efi_load_option_write_path(self, error); if (buf_path == NULL) return NULL; g_byte_array_append(st, buf_path->data, buf_path->len); } else if (self->kind == FU_EFI_LOAD_OPTION_KIND_DATA && self->optional_data != NULL) { fu_byte_array_append_bytes(st, self->optional_data); } /* success */ return g_steal_pointer(&st); } static gboolean fu_efi_load_option_build(FuFirmware *firmware, XbNode *n, GError **error) { FuEfiLoadOption *self = FU_EFI_LOAD_OPTION(firmware); const gchar *str; guint64 tmp; g_autoptr(GPtrArray) metadata = NULL; g_autoptr(XbNode) optional_data = NULL; /* simple properties */ tmp = xb_node_query_text_as_uint(n, "attrs", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT32) self->attrs = tmp; /* simple properties */ str = xb_node_query_text(n, "kind", NULL); if (str != NULL) { self->kind = fu_efi_load_option_kind_from_string(str); if (self->kind == FU_EFI_LOAD_OPTION_KIND_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid option kind type %s", str); return FALSE; } } /* optional data */ optional_data = xb_node_query_first(n, "optional_data", NULL); if (optional_data != NULL) { g_autoptr(GBytes) blob = NULL; if (xb_node_get_text(optional_data) != NULL) { gsize bufsz = 0; g_autofree guchar *buf = NULL; buf = g_base64_decode(xb_node_get_text(optional_data), &bufsz); blob = g_bytes_new(buf, bufsz); } else { blob = g_bytes_new(NULL, 0); } fu_efi_load_option_set_optional_data(self, blob); self->kind = FU_EFI_LOAD_OPTION_KIND_DATA; } metadata = xb_node_query(n, "metadata/*", 0, NULL); if (metadata != NULL) { for (guint i = 0; i < metadata->len; i++) { XbNode *c = g_ptr_array_index(metadata, i); const gchar *value = xb_node_get_text(c); if (xb_node_get_element(c) == NULL) continue; fu_efi_load_option_set_metadata(self, xb_node_get_element(c), value != NULL ? value : ""); } } /* success */ return TRUE; } static void fu_efi_load_option_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuEfiLoadOption *self = FU_EFI_LOAD_OPTION(firmware); fu_xmlb_builder_insert_kx(bn, "attrs", self->attrs); if (self->kind != FU_EFI_LOAD_OPTION_KIND_UNKNOWN) { fu_xmlb_builder_insert_kv(bn, "kind", fu_efi_load_option_kind_to_string(self->kind)); } if (g_hash_table_size(self->metadata) > 0) { GHashTableIter iter; const gchar *key; const gchar *value; g_autoptr(XbBuilderNode) bc = xb_builder_node_insert(bn, "metadata", NULL); g_hash_table_iter_init(&iter, self->metadata); while (g_hash_table_iter_next(&iter, (gpointer *)&key, (gpointer *)&value)) xb_builder_node_insert_text(bc, key, value, NULL); } if (self->optional_data != NULL) { gsize bufsz = 0; const guint8 *buf = g_bytes_get_data(self->optional_data, &bufsz); g_autofree gchar *datastr = g_base64_encode(buf, bufsz); xb_builder_node_insert_text(bn, "optional_data", datastr, NULL); } } static void fu_efi_load_option_add_json(FwupdCodec *codec, JsonBuilder *builder, FwupdCodecFlags flags) { FuEfiLoadOption *self = FU_EFI_LOAD_OPTION(codec); g_autoptr(FuFirmware) dp_list = NULL; fwupd_codec_json_append(builder, "Name", fu_firmware_get_id(FU_FIRMWARE(self))); if (self->kind != FU_EFI_LOAD_OPTION_KIND_UNKNOWN) { fwupd_codec_json_append(builder, "Kind", fu_efi_load_option_kind_to_string(self->kind)); } if (g_hash_table_size(self->metadata) > 0) { GHashTableIter iter; const gchar *key; const gchar *value; json_builder_set_member_name(builder, "Metadata"); json_builder_begin_object(builder); g_hash_table_iter_init(&iter, self->metadata); while (g_hash_table_iter_next(&iter, (gpointer *)&key, (gpointer *)&value)) fwupd_codec_json_append(builder, key, value); json_builder_end_object(builder); } dp_list = fu_firmware_get_image_by_gtype(FU_FIRMWARE(self), FU_TYPE_EFI_DEVICE_PATH_LIST, NULL); if (dp_list != NULL) fwupd_codec_to_json(FWUPD_CODEC(dp_list), builder, flags); } static void fu_efi_load_option_codec_iface_init(FwupdCodecInterface *iface) { iface->add_json = fu_efi_load_option_add_json; } static void fu_efi_load_option_finalize(GObject *obj) { FuEfiLoadOption *self = FU_EFI_LOAD_OPTION(obj); if (self->optional_data != NULL) g_bytes_unref(self->optional_data); g_hash_table_unref(self->metadata); G_OBJECT_CLASS(fu_efi_load_option_parent_class)->finalize(obj); } static void fu_efi_load_option_class_init(FuEfiLoadOptionClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_efi_load_option_finalize; firmware_class->parse = fu_efi_load_option_parse; firmware_class->write = fu_efi_load_option_write; firmware_class->build = fu_efi_load_option_build; firmware_class->export = fu_efi_load_option_export; } static void fu_efi_load_option_init(FuEfiLoadOption *self) { self->attrs = FU_EFI_LOAD_OPTION_ATTRS_ACTIVE; self->metadata = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); g_type_ensure(FU_TYPE_EFI_DEVICE_PATH_LIST); } /** * fu_efi_load_option_new: * * Returns: (transfer full): a #FuEfiLoadOption * * Since: 1.9.3 **/ FuEfiLoadOption * fu_efi_load_option_new(void) { return g_object_new(FU_TYPE_EFI_LOAD_OPTION, NULL); } fwupd-2.0.10/libfwupdplugin/fu-efi-load-option.h000066400000000000000000000022361501337203100215170ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-efi-struct.h" #include "fu-firmware.h" #define FU_TYPE_EFI_LOAD_OPTION (fu_efi_load_option_get_type()) G_DECLARE_FINAL_TYPE(FuEfiLoadOption, fu_efi_load_option, FU, EFI_LOAD_OPTION, FuFirmware) /** * FU_EFI_LOAD_OPTION_METADATA_PATH: * * The key for the 2nd-stage loader path. * * Since: 2.0.0 */ #define FU_EFI_LOAD_OPTION_METADATA_PATH "path" /** * FU_EFI_LOAD_OPTION_METADATA_CMDLINE: * * The key for the kernel command line. * * Since: 2.0.0 */ #define FU_EFI_LOAD_OPTION_METADATA_CMDLINE "cmdline" FuEfiLoadOptionKind fu_efi_load_option_get_kind(FuEfiLoadOption *self) G_GNUC_NON_NULL(1); void fu_efi_load_option_set_kind(FuEfiLoadOption *self, FuEfiLoadOptionKind kind) G_GNUC_NON_NULL(1); const gchar * fu_efi_load_option_get_metadata(FuEfiLoadOption *self, const gchar *key, GError **error) G_GNUC_NON_NULL(1, 2); void fu_efi_load_option_set_metadata(FuEfiLoadOption *self, const gchar *key, const gchar *value) G_GNUC_NON_NULL(1, 2); FuEfiLoadOption * fu_efi_load_option_new(void) G_GNUC_WARN_UNUSED_RESULT; fwupd-2.0.10/libfwupdplugin/fu-efi-lz77-decompressor.c000066400000000000000000000462261501337203100226020ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * Copyright 2018 LongSoft * Copyright 2008 Apple Inc * Copyright 2006 Intel Corporation * * SPDX-License-Identifier: BSD-2-Clause or LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include "fu-byte-array.h" #include "fu-efi-lz77-decompressor.h" #include "fu-input-stream.h" #include "fu-string.h" struct _FuEfiLz77Decompressor { FuFirmware parent_instance; }; /** * FuEfiLz77Decompressor: * * Funky LZ77 decompressor as specified by EFI. The compression design [and code] was designed for * a different era, and much better compression can be achieved using LZMA or zlib. * * My advice would be to not use this compression method in new designs. * * See also: [class@FuFirmware] */ G_DEFINE_TYPE(FuEfiLz77Decompressor, fu_efi_lz77_decompressor, FU_TYPE_FIRMWARE) #define BITBUFSIZ 32 #define MAXMATCH 256 #define THRESHOLD 3 #define CODE_BIT 16 /* c: char&len set; p: position set; t: extra set */ #define NC (0xff + MAXMATCH + 2 - THRESHOLD) #define CBIT 9 #define MAXPBIT 5 #define TBIT 5 #define MAXNP ((1U << MAXPBIT) - 1) #define NT (CODE_BIT + 3) #if NT > MAXNP #define NPT NT #else #define NPT MAXNP #endif typedef struct { GInputStream *stream; /* no-ref */ GByteArray *dst; /* no-ref */ guint16 bit_count; guint32 bit_buf; guint32 sub_bit_buf; guint16 block_size; guint16 left[2 * NC - 1]; guint16 right[2 * NC - 1]; guint8 c_len[NC]; guint8 pt_len[NPT]; guint16 c_table[4096]; guint16 pt_table[256]; guint8 p_bit; /* 'position set code length array size' in block header */ } FuEfiLz77DecompressHelper; static void fu_efi_lz77_decompressor_memset16(guint16 *buf, gsize length, guint16 value) { g_return_if_fail(length % 2 == 0); length /= sizeof(guint16); for (gsize i = 0; i < length; i++) buf[i] = value; } static gboolean fu_efi_lz77_decompressor_read_source_bits(FuEfiLz77DecompressHelper *helper, guint16 number_of_bits, GError **error) { /* left shift number_of_bits of bits in advance */ helper->bit_buf = (guint32)(((guint64)helper->bit_buf) << number_of_bits); /* copy data needed in bytes into sub_bit_buf */ while (number_of_bits > helper->bit_count) { gssize rc; guint8 sub_bit_buf = 0; number_of_bits = (guint16)(number_of_bits - helper->bit_count); helper->bit_buf |= (guint32)(((guint64)helper->sub_bit_buf) << number_of_bits); /* get 1 byte into sub_bit_buf */ rc = g_input_stream_read(helper->stream, &sub_bit_buf, sizeof(sub_bit_buf), NULL, error); if (rc < 0) return FALSE; if (rc == 0) { /* no more bits from the source, just pad zero bit */ helper->sub_bit_buf = 0; } else { helper->sub_bit_buf = sub_bit_buf; } helper->bit_count = 8; } /* calculate additional bit count read to update bit_count */ helper->bit_count = (guint16)(helper->bit_count - number_of_bits); /* copy number_of_bits of bits from sub_bit_buf into bit_buf */ helper->bit_buf |= helper->sub_bit_buf >> helper->bit_count; return TRUE; } static gboolean fu_efi_lz77_decompressor_get_bits(FuEfiLz77DecompressHelper *helper, guint16 number_of_bits, guint16 *value, GError **error) { /* pop number_of_bits of bits from left */ *value = (guint16)(helper->bit_buf >> (BITBUFSIZ - number_of_bits)); /* fill up bit_buf from source */ return fu_efi_lz77_decompressor_read_source_bits(helper, number_of_bits, error); } /* creates huffman code mapping table for extra set, char&len set and position set according to * code length array */ static gboolean fu_efi_lz77_decompressor_make_huffman_table(FuEfiLz77DecompressHelper *helper, guint16 number_of_symbols, guint8 *code_length_array, guint16 mapping_table_bits, guint16 *table, GError **error) { guint16 count[17] = {0}; guint16 weight[17] = {0}; guint16 start[18] = {0}; guint16 *pointer; guint16 index; guint16 c_char; guint16 ju_bits; guint16 avail_symbols; guint16 mask; guint16 max_table_length; /* the maximum mapping table width supported by this internal working function is 16 */ if (mapping_table_bits >= (sizeof(count) / sizeof(guint16))) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "bad table"); return FALSE; } for (index = 0; index < number_of_symbols; index++) { if (code_length_array[index] > 16) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "bad table"); return FALSE; } count[code_length_array[index]]++; } for (index = 1; index <= 16; index++) { guint16 WordOfStart = start[index]; guint16 WordOfCount = count[index]; start[index + 1] = (guint16)(WordOfStart + (WordOfCount << (16 - index))); } if (start[17] != 0) { /*(1U << 16)*/ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "bad table"); return FALSE; } ju_bits = (guint16)(16 - mapping_table_bits); for (index = 1; index <= mapping_table_bits; index++) { start[index] >>= ju_bits; weight[index] = (guint16)(1U << (mapping_table_bits - index)); } while (index <= 16) { weight[index] = (guint16)(1U << (16 - index)); index++; } index = (guint16)(start[mapping_table_bits + 1] >> ju_bits); if (index != 0) { guint16 index3 = (guint16)(1U << mapping_table_bits); if (index < index3) { fu_efi_lz77_decompressor_memset16(table + index, (index3 - index) * sizeof(*table), 0); } } avail_symbols = number_of_symbols; mask = (guint16)(1U << (15 - mapping_table_bits)); max_table_length = (guint16)(1U << mapping_table_bits); for (c_char = 0; c_char < number_of_symbols; c_char++) { guint16 len = code_length_array[c_char]; guint16 next_code; if (len == 0 || len >= 17) continue; next_code = (guint16)(start[len] + weight[len]); if (len <= mapping_table_bits) { for (index = start[len]; index < next_code; index++) { if (index >= max_table_length) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "bad table"); return FALSE; } table[index] = c_char; } } else { guint16 index3 = start[len]; pointer = &table[index3 >> ju_bits]; index = (guint16)(len - mapping_table_bits); while (index != 0) { if (*pointer == 0 && avail_symbols < (2 * NC - 1)) { helper->right[avail_symbols] = helper->left[avail_symbols] = 0; *pointer = avail_symbols++; } if (*pointer < (2 * NC - 1)) { if ((index3 & mask) != 0) pointer = &helper->right[*pointer]; else pointer = &helper->left[*pointer]; } index3 <<= 1; index--; } *pointer = c_char; } start[len] = next_code; } /* success */ return TRUE; } /* get a position value according to Position Huffman table */ static gboolean fu_efi_lz77_decompressor_decode_p(FuEfiLz77DecompressHelper *helper, guint32 *value, GError **error) { guint16 val; val = helper->pt_table[helper->bit_buf >> (BITBUFSIZ - 8)]; if (val >= MAXNP) { guint32 mask = 1U << (BITBUFSIZ - 1 - 8); do { if ((helper->bit_buf & mask) != 0) { val = helper->right[val]; } else { val = helper->left[val]; } mask >>= 1; } while (val >= MAXNP); } /* advance what we have read */ if (!fu_efi_lz77_decompressor_read_source_bits(helper, helper->pt_len[val], error)) return FALSE; if (val > 1) { guint16 char_c = 0; if (!fu_efi_lz77_decompressor_get_bits(helper, (guint16)(val - 1), &char_c, error)) return FALSE; *value = (guint32)((1U << (val - 1)) + char_c); return TRUE; } *value = val; return TRUE; } /* read in the extra set or position set length array, then generate the code mapping for them */ static gboolean fu_efi_lz77_decompressor_read_pt_len(FuEfiLz77DecompressHelper *helper, guint16 number_of_symbols, guint16 number_of_bits, guint16 special_symbol, GError **error) { guint16 number = 0; guint16 index = 0; /* read Extra Set Code Length Array size */ if (!fu_efi_lz77_decompressor_get_bits(helper, number_of_bits, &number, error)) return FALSE; /* fail if number or number_of_symbols is greater than size of pt_len */ if ((number > sizeof(helper->pt_len)) || (number_of_symbols > sizeof(helper->pt_len))) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "bad table"); return FALSE; } if (number == 0) { /* this represents only Huffman code used */ guint16 char_c = 0; if (!fu_efi_lz77_decompressor_get_bits(helper, number_of_bits, &char_c, error)) return FALSE; fu_efi_lz77_decompressor_memset16(&helper->pt_table[0], sizeof(helper->pt_table), (guint16)char_c); memset(helper->pt_len, 0, number_of_symbols); return TRUE; } while (index < number && index < NPT) { guint16 char_c = helper->bit_buf >> (BITBUFSIZ - 3); /* if a code length is less than 7, then it is encoded as a 3-bit value. * Or it is encoded as a series of "1"s followed by a terminating "0". * The number of "1"s = Code length - 4 */ if (char_c == 7) { guint32 mask = 1U << (BITBUFSIZ - 1 - 3); while (mask & helper->bit_buf) { mask >>= 1; char_c += 1; } } if (!fu_efi_lz77_decompressor_read_source_bits( helper, (guint16)((char_c < 7) ? 3 : char_c - 3), error)) return FALSE; helper->pt_len[index++] = (guint8)char_c; /* for code&len set, after the third length of the code length concatenation, * a 2-bit value is used to indicated the number of consecutive zero lengths after * the third length */ if (index == special_symbol) { if (!fu_efi_lz77_decompressor_get_bits(helper, 2, &char_c, error)) return FALSE; while ((gint16)(--char_c) >= 0 && index < NPT) { helper->pt_len[index++] = 0; } } } while (index < number_of_symbols && index < NPT) helper->pt_len[index++] = 0; return fu_efi_lz77_decompressor_make_huffman_table(helper, number_of_symbols, helper->pt_len, 8, helper->pt_table, error); } /* read in and decode the Char&len Set Code Length Array, then generate the Huffman Code mapping * table for the char&len set */ static gboolean fu_efi_lz77_decompressor_read_c_len(FuEfiLz77DecompressHelper *helper, GError **error) { guint16 number = 0; guint16 index = 0; if (!fu_efi_lz77_decompressor_get_bits(helper, CBIT, &number, error)) return FALSE; if (number == 0) { /* this represents only Huffman code used */ guint16 char_c = 0; if (!fu_efi_lz77_decompressor_get_bits(helper, CBIT, &char_c, error)) return FALSE; memset(helper->c_len, 0, sizeof(helper->c_len)); fu_efi_lz77_decompressor_memset16(&helper->c_table[0], sizeof(helper->c_table), char_c); return TRUE; } while (index < number && index < NC) { guint16 char_c = helper->pt_table[helper->bit_buf >> (BITBUFSIZ - 8)]; if (char_c >= NT) { guint32 mask = 1U << (BITBUFSIZ - 1 - 8); do { if (mask & helper->bit_buf) { char_c = helper->right[char_c]; } else { char_c = helper->left[char_c]; } mask >>= 1; } while (char_c >= NT); } /* advance what we have read */ if (!fu_efi_lz77_decompressor_read_source_bits(helper, helper->pt_len[char_c], error)) return FALSE; if (char_c <= 2) { if (char_c == 0) { char_c = 1; } else if (char_c == 1) { if (!fu_efi_lz77_decompressor_get_bits(helper, 4, &char_c, error)) return FALSE; char_c += 3; } else if (char_c == 2) { if (!fu_efi_lz77_decompressor_get_bits(helper, CBIT, &char_c, error)) return FALSE; char_c += 20; } while ((gint16)(--char_c) >= 0 && index < NC) helper->c_len[index++] = 0; } else { helper->c_len[index++] = (guint8)(char_c - 2); } } memset(helper->c_len + index, 0, sizeof(helper->c_len) - index); return fu_efi_lz77_decompressor_make_huffman_table(helper, NC, helper->c_len, 12, helper->c_table, error); } /* get one code. if it is at block boundary, generate huffman code mapping table for extra set, * code&len set and position set */ static gboolean fu_efi_lz77_decompressor_decode_c(FuEfiLz77DecompressHelper *helper, guint16 *value, GError **error) { guint16 index2; guint32 mask; if (helper->block_size == 0) { /* starting a new block, so read blocksize from block header */ if (!fu_efi_lz77_decompressor_get_bits(helper, 16, &helper->block_size, error)) return FALSE; /* read in the extra set code length array */ if (!fu_efi_lz77_decompressor_read_pt_len(helper, NT, TBIT, 3, error)) { g_prefix_error( error, "failed to generate the Huffman code mapping table for extra set: "); return FALSE; } /* read in and decode the char&len set code length array */ if (!fu_efi_lz77_decompressor_read_c_len(helper, error)) { g_prefix_error(error, "failed to generate the code mapping table for char&len: "); return FALSE; } /* read in the position set code length array */ if (!fu_efi_lz77_decompressor_read_pt_len(helper, MAXNP, helper->p_bit, (guint16)(-1), error)) { g_prefix_error(error, "failed to generate the Huffman code mapping table for the " "position set: "); return FALSE; } } /* get one code according to code&set huffman table */ if (helper->block_size == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no blocks remained"); return FALSE; } helper->block_size--; index2 = helper->c_table[helper->bit_buf >> (BITBUFSIZ - 12)]; if (index2 >= NC) { mask = 1U << (BITBUFSIZ - 1 - 12); do { if ((helper->bit_buf & mask) != 0) { index2 = helper->right[index2]; } else { index2 = helper->left[index2]; } mask >>= 1; } while (index2 >= NC); } /* advance what we have read */ if (!fu_efi_lz77_decompressor_read_source_bits(helper, helper->c_len[index2], error)) return FALSE; *value = index2; return TRUE; } static gboolean fu_efi_lz77_decompressor_internal(FuEfiLz77DecompressHelper *helper, FuEfiLz77DecompressorVersion version, GError **error) { gsize dst_offset = 0; /* position set code length array size in the block header */ switch (version) { case FU_EFI_LZ77_DECOMPRESSOR_VERSION_LEGACY: helper->p_bit = 4; break; case FU_EFI_LZ77_DECOMPRESSOR_VERSION_TIANO: helper->p_bit = 5; break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "unknown version 0x%x", version); return FALSE; } /* fill the first BITBUFSIZ bits */ if (!fu_efi_lz77_decompressor_read_source_bits(helper, BITBUFSIZ, error)) return FALSE; /* decode each char */ while (dst_offset < helper->dst->len) { guint16 char_c = 0; /* get one code */ if (!fu_efi_lz77_decompressor_decode_c(helper, &char_c, error)) return FALSE; if (char_c < 256) { /* write original character into dst_buf */ helper->dst->data[dst_offset++] = (guint8)char_c; } else { guint16 bytes_remaining; guint32 data_offset; guint32 tmp = 0; /* process a pointer, so get string length */ bytes_remaining = (guint16)(char_c - (0x00000100U - THRESHOLD)); if (!fu_efi_lz77_decompressor_decode_p(helper, &tmp, error)) return FALSE; data_offset = dst_offset - tmp - 1; /* write bytes_remaining of bytes into dst_buf */ bytes_remaining--; while ((gint16)(bytes_remaining) >= 0) { if (dst_offset >= helper->dst->len) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "bad pointer offset"); return FALSE; } if (data_offset >= helper->dst->len) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "bad table"); return FALSE; } helper->dst->data[dst_offset++] = helper->dst->data[data_offset++]; bytes_remaining--; } } } /* success */ return TRUE; } static gboolean fu_efi_lz77_decompressor_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { gsize streamsz = 0; guint32 dst_bufsz; guint32 src_bufsz; g_autoptr(GByteArray) st = NULL; g_autoptr(GError) error_all = NULL; g_autoptr(GByteArray) dst = g_byte_array_new(); FuEfiLz77DecompressorVersion decompressor_versions[] = { FU_EFI_LZ77_DECOMPRESSOR_VERSION_LEGACY, FU_EFI_LZ77_DECOMPRESSOR_VERSION_TIANO, }; /* parse header */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; st = fu_struct_efi_lz77_decompressor_header_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; src_bufsz = fu_struct_efi_lz77_decompressor_header_get_src_size(st); if (streamsz < src_bufsz + st->len) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "source buffer is truncated"); return FALSE; } dst_bufsz = fu_struct_efi_lz77_decompressor_header_get_dst_size(st); if (dst_bufsz == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "destination size is zero"); return FALSE; } if (dst_bufsz > fu_firmware_get_size_max(firmware)) { g_autofree gchar *sz_val = g_format_size(dst_bufsz); g_autofree gchar *sz_max = g_format_size(fu_firmware_get_size_max(firmware)); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "destination size is too large (%s, limit %s)", sz_val, sz_max); return FALSE; } fu_byte_array_set_size(dst, dst_bufsz, 0x0); /* try both position */ for (guint i = 0; i < G_N_ELEMENTS(decompressor_versions); i++) { FuEfiLz77DecompressHelper helper = { .dst = dst, .stream = stream, }; g_autoptr(GError) error_local = NULL; if (!g_seekable_seek(G_SEEKABLE(stream), st->len, G_SEEK_SET, NULL, error)) return FALSE; if (fu_efi_lz77_decompressor_internal(&helper, decompressor_versions[i], &error_local)) { g_autoptr(GBytes) blob = g_byte_array_free_to_bytes(g_steal_pointer(&dst)); /* nocheck:blocked */ if (!fu_firmware_set_stream(firmware, NULL, error)) return FALSE; fu_firmware_set_bytes(firmware, blob); fu_firmware_set_version_raw(firmware, decompressor_versions[i]); return TRUE; } if (error_all == NULL) { g_propagate_prefixed_error( &error_all, g_steal_pointer(&error_local), "failed to parse %s: ", fu_efi_lz77_decompressor_version_to_string(decompressor_versions[i])); continue; } g_prefix_error(&error_all, "failed to parse %s: %s: ", fu_efi_lz77_decompressor_version_to_string(decompressor_versions[i]), error_local->message); } /* success */ g_propagate_error(error, g_steal_pointer(&error_all)); return FALSE; } static void fu_efi_lz77_decompressor_init(FuEfiLz77Decompressor *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_STORED_SIZE); fu_firmware_set_size_max(FU_FIRMWARE(self), 64 * 1024 * 1024); } static void fu_efi_lz77_decompressor_class_init(FuEfiLz77DecompressorClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_efi_lz77_decompressor_parse; } /** * fu_efi_lz77_decompressor_new: * * Creates a new #FuFirmware that can be used to decompress LZ77. * * Since: 2.0.0 **/ FuFirmware * fu_efi_lz77_decompressor_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_EFI_LZ77_DECOMPRESSOR, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-efi-lz77-decompressor.h000066400000000000000000000006721501337203100226020ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-efi-struct.h" #include "fu-firmware.h" #define FU_TYPE_EFI_LZ77_DECOMPRESSOR (fu_efi_lz77_decompressor_get_type()) G_DECLARE_FINAL_TYPE(FuEfiLz77Decompressor, fu_efi_lz77_decompressor, FU, EFI_LZ77_DECOMPRESSOR, FuFirmware) FuFirmware * fu_efi_lz77_decompressor_new(void); fwupd-2.0.10/libfwupdplugin/fu-efi-section.c000066400000000000000000000366031501337203100207360ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuEfiSection" #include "config.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-efi-common.h" #include "fu-efi-lz77-decompressor.h" #include "fu-efi-section.h" #include "fu-efi-struct.h" #include "fu-efi-volume.h" #include "fu-input-stream.h" #include "fu-lzma-common.h" #include "fu-mem.h" #include "fu-partial-input-stream.h" #include "fu-string.h" /** * FuEfiSection: * * A UEFI firmware section. * * See also: [class@FuFirmware] */ typedef struct { guint8 type; gchar *user_interface; } FuEfiSectionPrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuEfiSection, fu_efi_section, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_efi_section_get_instance_private(o)) static void fu_efi_section_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuEfiSection *self = FU_EFI_SECTION(firmware); FuEfiSectionPrivate *priv = GET_PRIVATE(self); fu_xmlb_builder_insert_kx(bn, "type", priv->type); if (priv->user_interface != NULL) fu_xmlb_builder_insert_kv(bn, "user_interface", priv->user_interface); if (flags & FU_FIRMWARE_EXPORT_FLAG_INCLUDE_DEBUG) { fu_xmlb_builder_insert_kv(bn, "name", fu_efi_guid_to_name(fu_firmware_get_id(firmware))); fu_xmlb_builder_insert_kv(bn, "type_name", fu_efi_section_type_to_string(priv->type)); } } static gboolean fu_efi_section_parse_volume_image(FuEfiSection *self, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { g_autoptr(FuFirmware) img = fu_efi_volume_new(); if (!fu_firmware_parse_stream(img, stream, 0x0, flags | FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error)) { return FALSE; } fu_firmware_add_image(FU_FIRMWARE(self), img); return TRUE; } static gboolean fu_efi_section_parse_lzma_sections(FuEfiSection *self, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { g_autoptr(GBytes) blob = NULL; g_autoptr(GBytes) blob_uncomp = NULL; g_autoptr(GInputStream) stream_uncomp = NULL; /* parse all sections */ blob = fu_input_stream_read_bytes(stream, 0, G_MAXSIZE, NULL, error); if (blob == NULL) return FALSE; blob_uncomp = fu_lzma_decompress_bytes(blob, 128 * 1024 * 1024, error); if (blob_uncomp == NULL) { g_prefix_error(error, "failed to decompress: "); return FALSE; } stream_uncomp = g_memory_input_stream_new_from_bytes(blob_uncomp); if (!fu_efi_parse_sections(FU_FIRMWARE(self), stream_uncomp, 0, flags, error)) { g_prefix_error(error, "failed to parse sections: "); return FALSE; } return TRUE; } static gboolean fu_efi_section_parse_user_interface(FuEfiSection *self, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuEfiSectionPrivate *priv = GET_PRIVATE(self); g_autoptr(GByteArray) buf = NULL; if (priv->user_interface != NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "UI already set as %s for section", priv->user_interface); return FALSE; } buf = fu_input_stream_read_byte_array(stream, 0x0, G_MAXSIZE, NULL, error); if (buf == NULL) return FALSE; priv->user_interface = fu_utf16_to_utf8_byte_array(buf, G_LITTLE_ENDIAN, error); if (priv->user_interface == NULL) return FALSE; return TRUE; } static gboolean fu_efi_section_parse_version(FuEfiSection *self, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { guint16 version_raw = 0; g_autofree gchar *version = NULL; g_autoptr(GByteArray) buf = NULL; if (!fu_input_stream_read_u16(stream, 0x0, &version_raw, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to read raw version: "); return FALSE; } fu_firmware_set_version_raw(FU_FIRMWARE(self), version_raw); buf = fu_input_stream_read_byte_array(stream, sizeof(guint16), G_MAXSIZE, NULL, error); if (buf == NULL) { g_prefix_error(error, "failed to read version buffer: "); return FALSE; } version = fu_utf16_to_utf8_byte_array(buf, G_LITTLE_ENDIAN, error); if (version == NULL) { g_prefix_error(error, "failed to convert to UTF-16: "); return FALSE; } fu_firmware_set_version(FU_FIRMWARE(self), version); /* nocheck:set-version */ return TRUE; } static gboolean fu_efi_section_parse_compression_sections(FuEfiSection *self, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { g_autoptr(GByteArray) st = NULL; st = fu_struct_efi_section_compression_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; if (fu_struct_efi_section_compression_get_compression_type(st) == FU_EFI_COMPRESSION_TYPE_NOT_COMPRESSED) { if (!fu_efi_parse_sections(FU_FIRMWARE(self), stream, st->len, flags, error)) { g_prefix_error(error, "failed to parse sections: "); return FALSE; } } else { g_autoptr(FuFirmware) lz77_decompressor = fu_efi_lz77_decompressor_new(); g_autoptr(GInputStream) lz77_stream = NULL; if (!fu_firmware_parse_stream(lz77_decompressor, stream, st->len, flags, error)) return FALSE; lz77_stream = fu_firmware_get_stream(lz77_decompressor, error); if (lz77_stream == NULL) return FALSE; if (!fu_efi_parse_sections(FU_FIRMWARE(self), lz77_stream, 0, flags, error)) { g_prefix_error(error, "failed to parse sections: "); return FALSE; } } return TRUE; } static const gchar * fu_efi_section_freeform_subtype_guid_to_string(const gchar *guid) { struct { const gchar *guid; const gchar *str; } freeform_guids[] = { {"00781ca1-5de3-405f-abb8-379c3c076984", "AmiRomLayoutGuid"}, {"20feebde-e739-420e-ae31-77e2876508c0", "IntelRstOprom"}, {"224d6eb4-307f-45ba-9dc3-fe9fc6b38148", "IntelEntRaidController"}, {"2ebe0275-6458-4af9-91ed-d3f4edb100aa", "SignOn"}, {"380b6b4f-1454-41f2-a6d3-61d1333e8cb4", "IntelGop"}, {"50339d20-c90a-4bb2-9aff-d8a11b23bc15", "I219?Oprom"}, {"88a15a4f-977d-4682-b17c-da1f316c1f32", "RomLayout"}, {"9bec7109-6d7a-413a-8e4b-019ced0503e1", "AmiBoardInfoSectionGuid"}, {"ab56dc60-0057-11da-a8db-000102eee626", "?BuildData"}, {"c5a4306e-e247-4ecd-a9d8-5b1985d3dcda", "?Oprom"}, {"c9352cc3-a354-44e5-8776-b2ed8dd781ec", "IntelEntRaidController"}, {"d46346ca-82a1-4cde-9546-77c86f893888", "?Oprom"}, {"e095affe-d4cd-4289-9b48-28f64e3d781d", "IntelRstOprom"}, {"fe612b72-203c-47b1-8560-a66d946eb371", "setupdata"}, {NULL, NULL}, }; for (guint i = 0; freeform_guids[i].guid != NULL; i++) { if (g_strcmp0(guid, freeform_guids[i].guid) == 0) return freeform_guids[i].str; } return NULL; } static gboolean fu_efi_section_parse_freeform_subtype_guid(FuEfiSection *self, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { const gchar *guid_ui; g_autofree gchar *guid_str = NULL; g_autoptr(GByteArray) st = NULL; st = fu_struct_efi_section_freeform_subtype_guid_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; /* no idea */ guid_str = fwupd_guid_to_string(fu_struct_efi_section_freeform_subtype_guid_get_guid(st), FWUPD_GUID_FLAG_MIXED_ENDIAN); guid_ui = fu_efi_section_freeform_subtype_guid_to_string(guid_str); if (guid_ui != NULL) { g_debug("ignoring FREEFORM_SUBTYPE_GUID %s [%s]", guid_str, guid_ui); return TRUE; } g_debug("unknown FREEFORM_SUBTYPE_GUID %s", guid_str); return TRUE; } static gboolean fu_efi_section_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuEfiSection *self = FU_EFI_SECTION(firmware); FuEfiSectionPrivate *priv = GET_PRIVATE(self); gsize offset = 0; gsize streamsz = 0; guint32 size; g_autoptr(GByteArray) st = NULL; g_autoptr(GInputStream) partial_stream = NULL; /* parse */ st = fu_struct_efi_section_parse_stream(stream, offset, error); if (st == NULL) return FALSE; /* use extended size */ if (fu_struct_efi_section_get_size(st) == 0xFFFFFF) { fu_struct_efi_section_unref(st); st = fu_struct_efi_section2_parse_stream(stream, offset, error); if (st == NULL) return FALSE; size = fu_struct_efi_section2_get_extended_size(st); } else { size = fu_struct_efi_section_get_size(st); } if (size < FU_STRUCT_EFI_SECTION_SIZE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid section size, got 0x%x", (guint)size); return FALSE; } /* sanity check */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (size > streamsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid section size, got 0x%x from stream of size 0x%x", (guint)size, (guint)streamsz); return FALSE; } /* name */ priv->type = fu_struct_efi_section_get_type(st); if (priv->type == FU_EFI_SECTION_TYPE_GUID_DEFINED) { g_autofree gchar *guid_str = NULL; g_autoptr(GByteArray) st_def = NULL; st_def = fu_struct_efi_section_guid_defined_parse_stream(stream, st->len, error); if (st_def == NULL) return FALSE; guid_str = fwupd_guid_to_string(fu_struct_efi_section_guid_defined_get_name(st_def), FWUPD_GUID_FLAG_MIXED_ENDIAN); fu_firmware_set_id(firmware, guid_str); if (fu_struct_efi_section_guid_defined_get_offset(st_def) < st_def->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid section size, got 0x%x", (guint)fu_struct_efi_section_guid_defined_get_offset(st_def)); return FALSE; } offset += fu_struct_efi_section_guid_defined_get_offset(st_def) - st->len; } /* create blob */ offset += st->len; partial_stream = fu_partial_input_stream_new(stream, offset, size - offset, error); if (partial_stream == NULL) { g_prefix_error(error, "failed to cut data: "); return FALSE; } fu_firmware_set_offset(firmware, offset); fu_firmware_set_size(firmware, size); if (!fu_firmware_set_stream(firmware, partial_stream, error)) return FALSE; /* nested volume */ if (priv->type == FU_EFI_SECTION_TYPE_VOLUME_IMAGE) { if (!fu_efi_section_parse_volume_image(self, partial_stream, flags, error)) { g_prefix_error(error, "failed to parse nested volume: "); return FALSE; } } else if (priv->type == FU_EFI_SECTION_TYPE_GUID_DEFINED && g_strcmp0(fu_firmware_get_id(firmware), FU_EFI_SECTION_GUID_LZMA_COMPRESS) == 0) { if (!fu_efi_section_parse_lzma_sections(self, partial_stream, flags, error)) { g_prefix_error(error, "failed to parse lzma section: "); return FALSE; } } else if (priv->type == FU_EFI_SECTION_TYPE_GUID_DEFINED && g_strcmp0(fu_firmware_get_id(firmware), "ced4eac6-49f3-4c12-a597-fc8c33447691") == 0) { g_debug("ignoring %s [0x%x] EFI section as self test", fu_efi_section_type_to_string(priv->type), priv->type); } else if (priv->type == FU_EFI_SECTION_TYPE_GUID_DEFINED) { g_warning("no idea how to decompress encapsulation section of type %s", fu_firmware_get_id(firmware)); } else if (priv->type == FU_EFI_SECTION_TYPE_USER_INTERFACE) { if (!fu_efi_section_parse_user_interface(self, partial_stream, flags, error)) { g_prefix_error(error, "failed to parse user interface: "); return FALSE; } } else if (priv->type == FU_EFI_SECTION_TYPE_VERSION) { if (!fu_efi_section_parse_version(self, partial_stream, flags, error)) { g_prefix_error(error, "failed to parse version: "); return FALSE; } } else if (priv->type == FU_EFI_SECTION_TYPE_COMPRESSION) { if (!fu_efi_section_parse_compression_sections(self, partial_stream, flags, error)) { g_prefix_error(error, "failed to parse compression: "); return FALSE; } } else if (priv->type == FU_EFI_SECTION_TYPE_FREEFORM_SUBTYPE_GUID) { if (!fu_efi_section_parse_freeform_subtype_guid(self, partial_stream, flags, error)) { g_prefix_error(error, "failed to parse compression: "); return FALSE; } } else if (priv->type == FU_EFI_SECTION_TYPE_PEI_DEPEX || priv->type == FU_EFI_SECTION_TYPE_DXE_DEPEX || priv->type == FU_EFI_SECTION_TYPE_MM_DEPEX || priv->type == FU_EFI_SECTION_TYPE_PE32 || priv->type == FU_EFI_SECTION_TYPE_TE || priv->type == FU_EFI_SECTION_TYPE_RAW) { g_debug("ignoring %s [0x%x] EFI section", fu_efi_section_type_to_string(priv->type), priv->type); } else { g_warning("no idea how to parse %s [0x%x] EFI section", fu_efi_section_type_to_string(priv->type), priv->type); } /* success */ return TRUE; } static GByteArray * fu_efi_section_write(FuFirmware *firmware, GError **error) { FuEfiSection *self = FU_EFI_SECTION(firmware); FuEfiSectionPrivate *priv = GET_PRIVATE(self); g_autoptr(GByteArray) buf = fu_struct_efi_section_new(); g_autoptr(GBytes) blob = NULL; /* simple blob for now */ blob = fu_firmware_get_bytes_with_patches(firmware, error); if (blob == NULL) return NULL; /* header */ if (priv->type == FU_EFI_SECTION_TYPE_GUID_DEFINED) { fwupd_guid_t guid = {0x0}; g_autoptr(GByteArray) st_def = fu_struct_efi_section_guid_defined_new(); if (!fwupd_guid_from_string(fu_firmware_get_id(firmware), &guid, FWUPD_GUID_FLAG_MIXED_ENDIAN, error)) return NULL; fu_struct_efi_section_guid_defined_set_name(st_def, &guid); fu_struct_efi_section_guid_defined_set_offset(st_def, buf->len + st_def->len); g_byte_array_append(buf, st_def->data, st_def->len); } fu_struct_efi_section_set_type(buf, priv->type); fu_struct_efi_section_set_size(buf, buf->len + g_bytes_get_size(blob)); /* blob */ fu_byte_array_append_bytes(buf, blob); return g_steal_pointer(&buf); } static gboolean fu_efi_section_build(FuFirmware *firmware, XbNode *n, GError **error) { FuEfiSection *self = FU_EFI_SECTION(firmware); FuEfiSectionPrivate *priv = GET_PRIVATE(self); const gchar *str; guint64 tmp; /* simple properties */ tmp = xb_node_query_text_as_uint(n, "type", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT8) priv->type = tmp; str = xb_node_query_text(n, "user_interface", NULL); if (str != NULL) { if (priv->user_interface != NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "UI already set as %s for section", priv->user_interface); return FALSE; } priv->user_interface = g_strdup(str); } /* success */ return TRUE; } static void fu_efi_section_init(FuEfiSection *self) { FuEfiSectionPrivate *priv = GET_PRIVATE(self); priv->type = FU_EFI_SECTION_TYPE_RAW; fu_firmware_set_images_max(FU_FIRMWARE(self), g_getenv("FWUPD_FUZZER_RUNNING") != NULL ? 10 : 2000); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_NO_AUTO_DETECTION); // fu_firmware_set_alignment (FU_FIRMWARE (self), FU_FIRMWARE_ALIGNMENT_8); g_type_ensure(FU_TYPE_EFI_VOLUME); } static void fu_efi_section_finalize(GObject *object) { FuEfiSection *self = FU_EFI_SECTION(object); FuEfiSectionPrivate *priv = GET_PRIVATE(self); g_free(priv->user_interface); G_OBJECT_CLASS(fu_efi_section_parent_class)->finalize(object); } static void fu_efi_section_class_init(FuEfiSectionClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_efi_section_finalize; firmware_class->parse = fu_efi_section_parse; firmware_class->write = fu_efi_section_write; firmware_class->build = fu_efi_section_build; firmware_class->export = fu_efi_section_export; } /** * fu_efi_section_new: * * Creates a new #FuFirmware * * Since: 2.0.0 **/ FuFirmware * fu_efi_section_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_EFI_SECTION, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-efi-section.h000066400000000000000000000006131501337203100207330ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_EFI_SECTION (fu_efi_section_get_type()) G_DECLARE_DERIVABLE_TYPE(FuEfiSection, fu_efi_section, FU, EFI_SECTION, FuFirmware) struct _FuEfiSectionClass { FuFirmwareClass parent_class; }; FuFirmware * fu_efi_section_new(void); fwupd-2.0.10/libfwupdplugin/fu-efi-signature-list.c000066400000000000000000000206611501337203100222410ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuEfiSignatureList" #include "config.h" #include #include #include "fu-byte-array.h" #include "fu-common.h" #include "fu-efi-signature-list.h" #include "fu-efi-signature-private.h" #include "fu-efi-struct.h" #include "fu-efi-x509-signature-private.h" #include "fu-input-stream.h" #include "fu-mem.h" /** * FuEfiSignatureList: * * A UEFI signature list typically found in the `PK` and `KEK` keys. * * See also: [class@FuFirmware] */ G_DEFINE_TYPE(FuEfiSignatureList, fu_efi_signature_list, FU_TYPE_FIRMWARE) const guint8 FU_EFI_SIGLIST_HEADER_MAGIC[] = {0x26, 0x16, 0xC4, 0xC1, 0x4C}; /** * fu_efi_signature_list_get_newest: * @self: a #FuEfiSignatureList * * Gets the deduplicated list of the newest EFI_SIGNATURE_LIST entries. * * Returns: (transfer container) (element-type FuEfiSignature): signatures * * Since: 2.0.8 **/ GPtrArray * fu_efi_signature_list_get_newest(FuEfiSignatureList *self) { g_autoptr(GHashTable) hash = NULL; g_autoptr(GList) sigs_values = NULL; g_autoptr(GPtrArray) sigs_newest = NULL; g_autoptr(GPtrArray) sigs = NULL; g_return_val_if_fail(FU_IS_EFI_SIGNATURE_LIST(self), NULL); /* dedupe the certificates either by the hash or by the subject vendor+name */ hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_object_unref); sigs = fu_firmware_get_images(FU_FIRMWARE(self)); for (guint i = 0; i < sigs->len; i++) { FuEfiSignature *sig = g_ptr_array_index(sigs, i); FuEfiSignature *sig_tmp; g_autofree gchar *key = NULL; if (fu_efi_signature_get_kind(sig) == FU_EFI_SIGNATURE_KIND_X509) { key = fu_efi_x509_signature_build_dedupe_key(FU_EFI_X509_SIGNATURE(sig)); } else { key = fu_firmware_get_checksum(FU_FIRMWARE(sig), G_CHECKSUM_SHA256, NULL); } sig_tmp = g_hash_table_lookup(hash, key); if (sig_tmp == NULL) { g_debug("adding %s", key); g_hash_table_insert(hash, g_steal_pointer(&key), g_object_ref(sig)); } else if (fu_firmware_get_version_raw(FU_FIRMWARE(sig)) > fu_firmware_get_version_raw(FU_FIRMWARE(sig_tmp))) { g_debug("replacing %s", key); g_hash_table_insert(hash, g_steal_pointer(&key), g_object_ref(sig)); } else { g_debug("ignoring %s", key); } } /* add the newest of each certificate */ sigs_newest = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); sigs_values = g_hash_table_get_values(hash); for (GList *l = sigs_values; l != NULL; l = l->next) { FuEfiSignature *sig = FU_EFI_SIGNATURE(l->data); g_ptr_array_add(sigs_newest, g_object_ref(sig)); } /* success */ return g_steal_pointer(&sigs_newest); } static gboolean fu_efi_signature_list_parse_list(FuEfiSignatureList *self, GInputStream *stream, gsize *offset, GError **error) { FuEfiSignatureKind sig_kind = FU_EFI_SIGNATURE_KIND_UNKNOWN; gsize offset_tmp; guint32 header_size; guint32 list_size; guint32 size; g_autofree gchar *sig_type = NULL; g_autoptr(GByteArray) st = NULL; /* read EFI_SIGNATURE_LIST */ st = fu_struct_efi_signature_list_parse_stream(stream, *offset, error); if (st == NULL) return FALSE; sig_type = fwupd_guid_to_string(fu_struct_efi_signature_list_get_type(st), FWUPD_GUID_FLAG_MIXED_ENDIAN); if (g_strcmp0(sig_type, "c1c41626-504c-4092-aca9-41f936934328") == 0) { sig_kind = FU_EFI_SIGNATURE_KIND_SHA256; } else if (g_strcmp0(sig_type, "a5c059a1-94e4-4aa7-87b5-ab155c2bf072") == 0) { sig_kind = FU_EFI_SIGNATURE_KIND_X509; } list_size = fu_struct_efi_signature_list_get_list_size(st); if (list_size < 0x1c || list_size > 1024 * 1024) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "SignatureListSize invalid: 0x%x", list_size); return FALSE; } header_size = fu_struct_efi_signature_list_get_header_size(st); if (header_size > 1024 * 1024) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "SignatureHeaderSize invalid: 0x%x", header_size); return FALSE; } size = fu_struct_efi_signature_list_get_size(st); if (size < sizeof(fwupd_guid_t) || size > 1024 * 1024) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "SignatureSize invalid: 0x%x", size); return FALSE; } /* header is typically unused */ offset_tmp = *offset + 0x1c + header_size; for (guint i = 0; i < (list_size - 0x1c) / size; i++) { g_autoptr(FuEfiSignature) sig = NULL; if (sig_kind == FU_EFI_SIGNATURE_KIND_X509) { sig = FU_EFI_SIGNATURE(fu_efi_x509_signature_new()); } else { sig = fu_efi_signature_new(sig_kind); } fu_firmware_set_size(FU_FIRMWARE(sig), size); if (!fu_firmware_parse_stream(FU_FIRMWARE(sig), stream, offset_tmp, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return FALSE; if (!fu_firmware_add_image_full(FU_FIRMWARE(self), FU_FIRMWARE(sig), error)) return FALSE; offset_tmp += size; } *offset += list_size; return TRUE; } static gboolean fu_efi_signature_list_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { fwupd_guid_t guid = {0x0}; g_autofree gchar *sig_type = NULL; if (!fu_input_stream_read_safe(stream, (guint8 *)&guid, sizeof(guid), 0, offset, /* seek */ sizeof(guid), error)) { g_prefix_error(error, "failed to read magic: "); return FALSE; } sig_type = fwupd_guid_to_string(&guid, FWUPD_GUID_FLAG_MIXED_ENDIAN); if (g_strcmp0(sig_type, "c1c41626-504c-4092-aca9-41f936934328") != 0 && g_strcmp0(sig_type, "a5c059a1-94e4-4aa7-87b5-ab155c2bf072") != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid magic for file"); return FALSE; } /* success */ return TRUE; } static gboolean fu_efi_signature_list_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuEfiSignatureList *self = FU_EFI_SIGNATURE_LIST(firmware); gsize offset = 0; gsize streamsz = 0; /* parse each EFI_SIGNATURE_LIST */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; while (offset < streamsz) { if (!fu_efi_signature_list_parse_list(self, stream, &offset, error)) return FALSE; } /* success */ return TRUE; } static GByteArray * fu_efi_signature_list_write(FuFirmware *firmware, GError **error) { fwupd_guid_t guid = {0}; g_autoptr(FuStructEfiSignatureList) st = fu_struct_efi_signature_list_new(); g_autoptr(GPtrArray) images = fu_firmware_get_images(firmware); /* entry */ if (!fwupd_guid_from_string("c1c41626-504c-4092-aca9-41f936934328", &guid, FWUPD_GUID_FLAG_MIXED_ENDIAN, error)) return NULL; fu_struct_efi_signature_list_set_type(st, &guid); fu_struct_efi_signature_list_set_header_size(st, 0); fu_struct_efi_signature_list_set_list_size(st, FU_STRUCT_EFI_SIGNATURE_LIST_SIZE + (images->len * (16 + 32))); fu_struct_efi_signature_list_set_size(st, sizeof(fwupd_guid_t) + 32); /* SHA256 */ /* SignatureOwner + SignatureData */ for (guint i = 0; i < images->len; i++) { FuFirmware *img = g_ptr_array_index(images, i); g_autoptr(GBytes) img_blob = NULL; img_blob = fu_firmware_write(img, error); if (img_blob == NULL) return NULL; if (g_bytes_get_size(img_blob) != sizeof(fwupd_guid_t) + 32) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "expected SHA256 hash as signature data, got 0x%x", (guint)(g_bytes_get_size(img_blob) - sizeof(fwupd_guid_t))); return NULL; } fu_byte_array_append_bytes(st, img_blob); } /* success */ return g_steal_pointer(&st); } /** * fu_efi_signature_list_new: * * Creates a new #FuFirmware that can parse an EFI_SIGNATURE_LIST * * Since: 1.5.5 **/ FuFirmware * fu_efi_signature_list_new(void) { return g_object_new(FU_TYPE_EFI_SIGNATURE_LIST, NULL); } static void fu_efi_signature_list_class_init(FuEfiSignatureListClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_efi_signature_list_validate; firmware_class->parse = fu_efi_signature_list_parse; firmware_class->write = fu_efi_signature_list_write; } static void fu_efi_signature_list_init(FuEfiSignatureList *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_ALWAYS_SEARCH); fu_firmware_set_images_max(FU_FIRMWARE(self), 2000); g_type_ensure(FU_TYPE_EFI_SIGNATURE); } fwupd-2.0.10/libfwupdplugin/fu-efi-signature-list.h000066400000000000000000000010451501337203100222410ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_EFI_SIGNATURE_LIST (fu_efi_signature_list_get_type()) G_DECLARE_DERIVABLE_TYPE(FuEfiSignatureList, fu_efi_signature_list, FU, EFI_SIGNATURE_LIST, FuFirmware) struct _FuEfiSignatureListClass { FuFirmwareClass parent_class; }; FuFirmware * fu_efi_signature_list_new(void); GPtrArray * fu_efi_signature_list_get_newest(FuEfiSignatureList *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-efi-signature-private.h000066400000000000000000000004541501337203100227430ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-efi-signature.h" FuEfiSignature * fu_efi_signature_new(FuEfiSignatureKind kind); void fu_efi_signature_set_kind(FuEfiSignature *self, FuEfiSignatureKind kind); fwupd-2.0.10/libfwupdplugin/fu-efi-signature.c000066400000000000000000000162011501337203100212630ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-efi-signature-private.h" /** * FuEfiSignature: * * A UEFI Signature as found in an `EFI_SIGNATURE_LIST`. * * See also: [class@FuFirmware] */ typedef struct { FuEfiSignatureKind kind; gchar *owner; } FuEfiSignaturePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuEfiSignature, fu_efi_signature, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_efi_signature_get_instance_private(o)) static void fu_efi_signature_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuEfiSignature *self = FU_EFI_SIGNATURE(firmware); FuEfiSignaturePrivate *priv = GET_PRIVATE(self); fu_xmlb_builder_insert_kv(bn, "kind", fu_efi_signature_kind_to_string(priv->kind)); fu_xmlb_builder_insert_kv(bn, "owner", priv->owner); /* special case: this is *literally* a hash */ if (priv->kind == FU_EFI_SIGNATURE_KIND_SHA256) { g_autoptr(GBytes) blob = fu_firmware_get_bytes(firmware, NULL); if (blob != NULL) { g_autofree gchar *str = fu_bytes_to_string(blob); fu_xmlb_builder_insert_kv(bn, "checksum", str); } } } /** * fu_efi_signature_new: (skip): * @kind: A #FuEfiSignatureKind * * Creates a new EFI_SIGNATURE. * * Returns: (transfer full): signature * * Since: 2.0.5 **/ FuEfiSignature * fu_efi_signature_new(FuEfiSignatureKind kind) { g_autoptr(FuEfiSignature) self = g_object_new(FU_TYPE_EFI_SIGNATURE, NULL); FuEfiSignaturePrivate *priv = GET_PRIVATE(self); priv->kind = kind; return g_steal_pointer(&self); } /** * fu_efi_signature_get_kind: * @self: A #FuEfiSignature * * Returns the signature kind. * * Returns: #FuEfiSignatureKind, e.g. %FU_EFI_SIGNATURE_KIND_SHA256 * * Since: 1.5.5 **/ FuEfiSignatureKind fu_efi_signature_get_kind(FuEfiSignature *self) { FuEfiSignaturePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_EFI_SIGNATURE(self), FU_EFI_SIGNATURE_KIND_UNKNOWN); return priv->kind; } /* private */ void fu_efi_signature_set_kind(FuEfiSignature *self, FuEfiSignatureKind kind) { FuEfiSignaturePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_EFI_SIGNATURE(self)); priv->kind = kind; } /** * fu_efi_signature_get_owner: * @self: A #FuEfiSignature * * Returns the GUID of the signature owner. * * Returns: GUID owner, perhaps %FU_EFI_SIGNATURE_GUID_MICROSOFT * * Since: 1.5.5 **/ const gchar * fu_efi_signature_get_owner(FuEfiSignature *self) { FuEfiSignaturePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_EFI_SIGNATURE(self), NULL); return priv->owner; } static gboolean fu_efi_signature_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuEfiSignature *self = FU_EFI_SIGNATURE(firmware); FuEfiSignaturePrivate *priv = GET_PRIVATE(self); fwupd_guid_t guid = {0}; gsize size = fu_firmware_get_size(firmware); g_autoptr(GBytes) data = NULL; /* sanity check */ if (size <= sizeof(fwupd_guid_t)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "SignatureSize invalid: 0x%x", (guint)size); return FALSE; } /* GUID */ if (!fu_input_stream_read_safe(stream, (guint8 *)&guid, sizeof(guid), 0x0, /* seek to */ 0x0, /* offset */ sizeof(guid), error)) { g_prefix_error(error, "failed to read signature GUID: "); return FALSE; } priv->owner = fwupd_guid_to_string(&guid, FWUPD_GUID_FLAG_MIXED_ENDIAN); /* if data size is unspecified, we'll use the stream size */ data = fu_input_stream_read_bytes(stream, sizeof(fwupd_guid_t), size - sizeof(fwupd_guid_t), NULL, error); if (data == NULL) { g_prefix_error(error, "failed to read signature data: "); return FALSE; } fu_firmware_set_bytes(firmware, data); /* success */ return TRUE; } static GByteArray * fu_efi_signature_write(FuFirmware *firmware, GError **error) { FuEfiSignature *self = FU_EFI_SIGNATURE(firmware); FuEfiSignaturePrivate *priv = GET_PRIVATE(self); fwupd_guid_t owner = {0}; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GBytes) data = NULL; /* optional owner */ if (priv->owner != NULL) { if (!fwupd_guid_from_string(priv->owner, &owner, FWUPD_GUID_FLAG_MIXED_ENDIAN, error)) return NULL; } g_byte_array_append(buf, owner, sizeof(owner)); /* data */ data = fu_firmware_get_bytes_with_patches(firmware, error); if (data == NULL) return NULL; fu_byte_array_append_bytes(buf, data); /* success */ return g_steal_pointer(&buf); } static gboolean fu_efi_signature_build(FuFirmware *firmware, XbNode *n, GError **error) { FuEfiSignature *self = FU_EFI_SIGNATURE(firmware); FuEfiSignaturePrivate *priv = GET_PRIVATE(self); const gchar *tmp; /* optional properties */ tmp = xb_node_query_text(n, "kind", NULL); if (tmp != NULL) { priv->kind = fu_efi_signature_kind_from_string(tmp); if (priv->kind == FU_EFI_SIGNATURE_KIND_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid kind: %s", tmp); return FALSE; } } tmp = xb_node_query_text(n, "owner", NULL); if (tmp != NULL) { if (!fwupd_guid_from_string(tmp, NULL, FWUPD_GUID_FLAG_MIXED_ENDIAN, error)) { g_prefix_error(error, "failed to parse owner %s, expected GUID: ", tmp); return FALSE; } g_free(priv->owner); priv->owner = g_strdup(tmp); } tmp = xb_node_query_text(n, "checksum", NULL); if (tmp != NULL) { g_autoptr(GBytes) data = NULL; data = fu_bytes_from_string(tmp, error); if (data == NULL) return FALSE; fu_firmware_set_bytes(firmware, data); } /* success */ return TRUE; } static gchar * fu_efi_signature_get_checksum(FuFirmware *firmware, GChecksumType csum_kind, GError **error) { FuEfiSignature *self = FU_EFI_SIGNATURE(firmware); FuEfiSignaturePrivate *priv = GET_PRIVATE(self); g_autoptr(GBytes) data = fu_firmware_get_bytes_with_patches(firmware, error); if (data == NULL) return NULL; /* special case: this is *literally* a hash */ if (priv->kind == FU_EFI_SIGNATURE_KIND_SHA256 && csum_kind == G_CHECKSUM_SHA256) return fu_bytes_to_string(data); /* fallback */ return g_compute_checksum_for_bytes(csum_kind, data); } static void fu_efi_signature_finalize(GObject *obj) { FuEfiSignature *self = FU_EFI_SIGNATURE(obj); FuEfiSignaturePrivate *priv = GET_PRIVATE(self); g_free(priv->owner); G_OBJECT_CLASS(fu_efi_signature_parent_class)->finalize(obj); } static void fu_efi_signature_class_init(FuEfiSignatureClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_efi_signature_finalize; firmware_class->export = fu_efi_signature_export; firmware_class->parse = fu_efi_signature_parse; firmware_class->write = fu_efi_signature_write; firmware_class->build = fu_efi_signature_build; firmware_class->get_checksum = fu_efi_signature_get_checksum; } static void fu_efi_signature_init(FuEfiSignature *self) { FuEfiSignaturePrivate *priv = GET_PRIVATE(self); priv->kind = FU_EFI_SIGNATURE_KIND_SHA256; } fwupd-2.0.10/libfwupdplugin/fu-efi-signature.h000066400000000000000000000016061501337203100212730ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-efi-struct.h" #include "fu-firmware.h" #define FU_TYPE_EFI_SIGNATURE (fu_efi_signature_get_type()) G_DECLARE_DERIVABLE_TYPE(FuEfiSignature, fu_efi_signature, FU, EFI_SIGNATURE, FuFirmware) struct _FuEfiSignatureClass { FuFirmwareClass parent_class; }; #define FU_EFI_SIGNATURE_GUID_ZERO "00000000-0000-0000-0000-000000000000" #define FU_EFI_SIGNATURE_GUID_MICROSOFT "77fa9abd-0359-4d32-bd60-28f4e78f784b" #define FU_EFI_SIGNATURE_GUID_OVMF "a0baa8a3-041d-48a8-bc87-c36d121b5e3d" #define FU_EFI_SIGNATURE_GUID_OVMF_LEGACY "d5c1df0b-1bac-4edf-ba48-08834009ca5a" FuEfiSignatureKind fu_efi_signature_get_kind(FuEfiSignature *self) G_GNUC_NON_NULL(1); const gchar * fu_efi_signature_get_owner(FuEfiSignature *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-efi-variable-authentication2.c000066400000000000000000000200711501337203100241460ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuEfiVariableAuthentication2" #include "config.h" #include "fu-byte-array.h" #include "fu-efi-struct.h" #include "fu-efi-variable-authentication2.h" #include "fu-mem.h" #include "fu-partial-input-stream.h" #include "fu-pkcs7.h" #include "fu-x509-certificate.h" /** * FuEfiVariableAuthentication2: * * A UEFI signature list typically found in the `KEKUpdate.bin` and `DBXUpdate.bin` files. * * See also: [class@FuFirmware] */ struct _FuEfiVariableAuthentication2 { FuEfiSignatureList parent_instance; GPtrArray *signers; }; G_DEFINE_TYPE(FuEfiVariableAuthentication2, fu_efi_variable_authentication2, FU_TYPE_EFI_SIGNATURE_LIST) /** * fu_efi_variable_authentication2_get_signers: * @self: A #FuEfiVariableAuthentication2 * * Returns the certificates that signed the variable. * * Returns: (transfer full) (element-type FuX509Certificate): certificates * * Since: 2.0.9 **/ GPtrArray * fu_efi_variable_authentication2_get_signers(FuEfiVariableAuthentication2 *self) { g_return_val_if_fail(FU_IS_EFI_VARIABLE_AUTHENTICATION2(self), NULL); return g_ptr_array_ref(self->signers); } static void fu_efi_variable_authentication2_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { g_autoptr(XbBuilderNode) bn_signers = NULL; FuEfiVariableAuthentication2 *self = FU_EFI_VARIABLE_AUTHENTICATION2(firmware); bn_signers = xb_builder_node_insert(bn, "signers", NULL); for (guint i = 0; i < self->signers->len; i++) { FuFirmware *img = g_ptr_array_index(self->signers, i); g_autoptr(XbBuilderNode) bn_firmware = NULL; bn_firmware = xb_builder_node_insert(bn_signers, "firmware", NULL); fu_firmware_export(img, flags, bn_firmware); } } static gboolean fu_efi_variable_authentication2_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_efi_variable_authentication2_validate_stream(stream, offset, error); } /* * with ContentInfo: * 30 82 05 90 -- SEQUENCE (1424 BYTES) -- ContentInfo * 06 09 -- OBJECT-IDENTIFIER (9 BYTES) -- ContentType * 2a 86 48 86 f7 0d 01 07 02 -- signedData [1.2.840.113549.1.7.2] * a0 82 05 81 -- CONTEXT-SPECIFIC CONSTRUCTED TAG 0 (1409 BYTES) -- content * * without ContentInfo: * 30 82 05 7d -- SEQUENCE (1405 BYTES) -- SignedData * 02 01 01 -- INTEGER 1 -- Version * 31 0f -- SET (1 element) (15 BYTES) -- DigestAlgorithmIdentifiers * 30 0d -- SEQUENCE (13 BYTES) -- AlgorithmIdentifier * 06 09 -- OBJECT-IDENTIFIER (9 BYTES) -- algorithm * 60 86 48 01 65 03 04 02 01 -- sha256 [2.16.840.1.101.3.4.2.1] * 05 00 -- NULL (0 BYTES) -- parameters */ static gboolean fu_efi_variable_authentication2_add_content_info_prefix(GByteArray *buf, GError **error) { guint16 sz = 0; const guint8 buf_algorithm[] = {0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x07, 0x02}; g_autoptr(GByteArray) buf_prefix = g_byte_array_new(); /* check is ASN.1 SEQUENCE */ if (!fu_memread_uint16_safe(buf->data, buf->len, 0x0, &sz, G_BIG_ENDIAN, error)) { g_prefix_error(error, "not ASN.1 SEQUENCE: "); return FALSE; } if (sz != 0x3082) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not ASN.1 SEQUENCE, got 0x%x", sz); return FALSE; } /* get size of SignedData */ if (!fu_memread_uint16_safe(buf->data, buf->len, 0x2, &sz, G_BIG_ENDIAN, error)) return FALSE; /* add SEQUENCE */ fu_byte_array_append_uint16(buf_prefix, 0x3082, G_BIG_ENDIAN); fu_byte_array_append_uint16(buf_prefix, sz + 19, G_BIG_ENDIAN); /* add OBJECT-IDENTIFIER */ fu_byte_array_append_uint16(buf_prefix, 0x0609, G_BIG_ENDIAN); g_byte_array_append(buf_prefix, buf_algorithm, sizeof(buf_algorithm)); /* add CONTEXT-SPECIFIC CONSTRUCTED TAG */ fu_byte_array_append_uint16(buf_prefix, 0xA082, G_BIG_ENDIAN); fu_byte_array_append_uint16(buf_prefix, sz + 4, G_BIG_ENDIAN); /* fix this up */ g_byte_array_prepend(buf, buf_prefix->data, buf_prefix->len); return TRUE; } static gboolean fu_efi_variable_authentication2_parse_pkcs7_certs(FuEfiVariableAuthentication2 *self, GByteArray *buf, GError **error) { g_autoptr(FuPkcs7) pkcs7 = fu_pkcs7_new(); g_autoptr(GBytes) blob = NULL; g_autoptr(GPtrArray) imgs = NULL; /* parse PKCS#7 blob */ blob = g_bytes_new(buf->data, buf->len); if (!fu_firmware_parse_bytes(FU_FIRMWARE(pkcs7), blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return FALSE; /* add certificates that signed this variable */ imgs = fu_firmware_get_images(FU_FIRMWARE(pkcs7)); for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); g_ptr_array_add(self->signers, g_object_ref(img)); } /* success */ return TRUE; } static gboolean fu_efi_variable_authentication2_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuEfiVariableAuthentication2 *self = FU_EFI_VARIABLE_AUTHENTICATION2(firmware); gsize offset = FU_STRUCT_EFI_TIME_SIZE; g_autoptr(FuStructEfiVariableAuthentication2) st = NULL; g_autoptr(FuStructEfiWinCertificate) st_wincert = NULL; g_autoptr(GInputStream) partial_stream = NULL; gboolean offset_tmp = 0; st = fu_struct_efi_variable_authentication2_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; /* parse the EFI_SIGNATURE_LIST blob past the EFI_TIME + WIN_CERTIFICATE */ st_wincert = fu_struct_efi_variable_authentication2_get_auth_info(st); if (fu_struct_efi_win_certificate_get_length(st_wincert) > st_wincert->len) { g_autoptr(GByteArray) buf = NULL; buf = fu_input_stream_read_byte_array( stream, offset + st_wincert->len + offset_tmp, fu_struct_efi_win_certificate_get_length(st_wincert) - st_wincert->len, NULL, error); if (buf == NULL) return FALSE; if (!fu_efi_variable_authentication2_add_content_info_prefix(buf, error)) return FALSE; if (!fu_efi_variable_authentication2_parse_pkcs7_certs(self, buf, error)) return FALSE; } offset += fu_struct_efi_win_certificate_get_length(st_wincert); partial_stream = fu_partial_input_stream_new(stream, offset, G_MAXSIZE, error); if (partial_stream == NULL) return FALSE; return FU_FIRMWARE_CLASS(fu_efi_variable_authentication2_parent_class) ->parse(firmware, partial_stream, flags, error); } static GByteArray * fu_efi_variable_authentication2_write(FuFirmware *firmware, GError **error) { g_autoptr(FuStructEfiVariableAuthentication2) st = fu_struct_efi_variable_authentication2_new(); g_autoptr(GByteArray) st_parent = NULL; /* append EFI_SIGNATURE_LIST */ st_parent = FU_FIRMWARE_CLASS(fu_efi_variable_authentication2_parent_class)->write(firmware, error); if (st_parent == NULL) return NULL; g_byte_array_append(st, st_parent->data, st_parent->len); /* success */ return g_steal_pointer(&st); } static void fu_efi_variable_authentication2_init(FuEfiVariableAuthentication2 *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_ALWAYS_SEARCH); g_type_ensure(FU_TYPE_EFI_SIGNATURE_LIST); self->signers = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); } static void fu_efi_variable_authentication2_finalize(GObject *obj) { FuEfiVariableAuthentication2 *self = FU_EFI_VARIABLE_AUTHENTICATION2(obj); g_ptr_array_unref(self->signers); G_OBJECT_CLASS(fu_efi_variable_authentication2_parent_class)->finalize(obj); } static void fu_efi_variable_authentication2_class_init(FuEfiVariableAuthentication2Class *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_efi_variable_authentication2_finalize; firmware_class->validate = fu_efi_variable_authentication2_validate; firmware_class->parse = fu_efi_variable_authentication2_parse; firmware_class->export = fu_efi_variable_authentication2_export; firmware_class->write = fu_efi_variable_authentication2_write; } fwupd-2.0.10/libfwupdplugin/fu-efi-variable-authentication2.h000066400000000000000000000010231501337203100241470ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-efi-signature-list.h" #define FU_TYPE_EFI_VARIABLE_AUTHENTICATION2 (fu_efi_variable_authentication2_get_type()) G_DECLARE_FINAL_TYPE(FuEfiVariableAuthentication2, fu_efi_variable_authentication2, FU, EFI_VARIABLE_AUTHENTICATION2, FuEfiSignatureList) GPtrArray * fu_efi_variable_authentication2_get_signers(FuEfiVariableAuthentication2 *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-efi-volume.c000066400000000000000000000236211501337203100205750ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuEfiVolume" #include "config.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-efi-common.h" #include "fu-efi-filesystem.h" #include "fu-efi-struct.h" #include "fu-efi-volume.h" #include "fu-input-stream.h" #include "fu-partial-input-stream.h" #include "fu-sum.h" /** * FuEfiVolume: * * A UEFI file volume. * * See also: [class@FuFirmware] */ typedef struct { guint16 attrs; } FuEfiVolumePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuEfiVolume, fu_efi_volume, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_efi_volume_get_instance_private(o)) static void fu_efi_volume_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuEfiVolume *self = FU_EFI_VOLUME(firmware); FuEfiVolumePrivate *priv = GET_PRIVATE(self); fu_xmlb_builder_insert_kx(bn, "attrs", priv->attrs); if (flags & FU_FIRMWARE_EXPORT_FLAG_INCLUDE_DEBUG) { fu_xmlb_builder_insert_kv(bn, "name", fu_efi_guid_to_name(fu_firmware_get_id(firmware))); } } static gboolean fu_efi_volume_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_efi_volume_validate_stream(stream, offset, error); } static gboolean fu_efi_volume_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuEfiVolume *self = FU_EFI_VOLUME(firmware); FuEfiVolumePrivate *priv = GET_PRIVATE(self); gsize blockmap_sz = 0; gsize offset = 0; gsize streamsz = 0; guint16 hdr_length = 0; guint32 attrs = 0; guint64 fv_length = 0; guint8 alignment; g_autofree gchar *guid_str = NULL; g_autoptr(GByteArray) st_hdr = NULL; g_autoptr(GInputStream) partial_stream = NULL; /* parse */ st_hdr = fu_struct_efi_volume_parse_stream(stream, 0x0, error); if (st_hdr == NULL) return FALSE; /* guid */ guid_str = fwupd_guid_to_string(fu_struct_efi_volume_get_guid(st_hdr), FWUPD_GUID_FLAG_MIXED_ENDIAN); g_debug("volume GUID: %s [%s]", guid_str, fu_efi_guid_to_name(guid_str)); /* length */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; fv_length = fu_struct_efi_volume_get_length(st_hdr); if (fv_length == 0x0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid volume length"); return FALSE; } fu_firmware_set_size(firmware, fv_length); attrs = fu_struct_efi_volume_get_attrs(st_hdr); alignment = (attrs & 0x00ff0000) >> 16; if (alignment > FU_FIRMWARE_ALIGNMENT_2G) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "0x%x invalid, maximum is 0x%x", (guint)alignment, (guint)FU_FIRMWARE_ALIGNMENT_2G); return FALSE; } fu_firmware_set_alignment(firmware, alignment); priv->attrs = attrs & 0xffff; hdr_length = fu_struct_efi_volume_get_hdr_len(st_hdr); if (hdr_length < st_hdr->len || hdr_length > fv_length || hdr_length > streamsz || hdr_length % 2 != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid volume header length 0x%x", (guint)hdr_length); return FALSE; } /* verify checksum */ if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { guint16 checksum_verify; g_autoptr(GBytes) blob_hdr = NULL; blob_hdr = fu_input_stream_read_bytes(stream, 0x0, hdr_length, NULL, error); if (blob_hdr == NULL) return FALSE; checksum_verify = fu_sum16w_bytes(blob_hdr, G_LITTLE_ENDIAN); if (checksum_verify != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "checksum invalid, got %02x, expected %02x", checksum_verify, fu_struct_efi_volume_get_checksum(st_hdr)); return FALSE; } } /* extended header items */ if (fu_struct_efi_volume_get_ext_hdr(st_hdr) != 0) { g_autoptr(GByteArray) st_ext_hdr = NULL; goffset offset_ext = fu_struct_efi_volume_get_ext_hdr(st_hdr); st_ext_hdr = fu_struct_efi_volume_ext_header_parse_stream(stream, offset_ext, error); if (st_ext_hdr == NULL) return FALSE; offset_ext += fu_struct_efi_volume_ext_header_get_size(st_ext_hdr); do { g_autoptr(GByteArray) st_ext_entry = NULL; st_ext_entry = fu_struct_efi_volume_ext_entry_parse_stream(stream, offset_ext, error); if (st_ext_entry == NULL) return FALSE; if (fu_struct_efi_volume_ext_entry_get_size(st_ext_entry) == 0x0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "EFI_VOLUME_EXT_ENTRY invalid size"); return FALSE; } if (fu_struct_efi_volume_ext_entry_get_size(st_ext_entry) == 0xFFFF) break; offset_ext += fu_struct_efi_volume_ext_entry_get_size(st_ext_entry); } while ((gsize)offset_ext < fv_length); } /* add image */ partial_stream = fu_partial_input_stream_new(stream, hdr_length, fv_length - hdr_length, error); if (partial_stream == NULL) { g_prefix_error(error, "failed to cut EFI volume: "); return FALSE; } fu_firmware_set_id(firmware, guid_str); fu_firmware_set_size(firmware, fv_length); /* parse, which might cascade and do something like FFS2 */ if (g_strcmp0(guid_str, FU_EFI_VOLUME_GUID_FFS2) == 0 || g_strcmp0(guid_str, FU_EFI_VOLUME_GUID_FFS3) == 0) { g_autoptr(FuFirmware) img = fu_efi_filesystem_new(); fu_firmware_set_alignment(img, fu_firmware_get_alignment(firmware)); if (!fu_firmware_parse_stream(img, partial_stream, 0x0, flags | FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error)) return FALSE; fu_firmware_add_image(firmware, img); } else if (g_strcmp0(guid_str, FU_EFI_VOLUME_GUID_NVRAM_EVSA) == 0 || g_strcmp0(guid_str, FU_EFI_VOLUME_GUID_NVRAM_EVSA2) == 0) { g_debug("ignoring %s [%s] EFI FV", guid_str, fu_efi_guid_to_name(guid_str)); if (!fu_firmware_set_stream(firmware, partial_stream, error)) return FALSE; } else { g_warning("no idea how to parse %s [%s] EFI volume", guid_str, fu_efi_guid_to_name(guid_str)); if (!fu_firmware_set_stream(firmware, partial_stream, error)) return FALSE; } /* skip the blockmap */ offset += st_hdr->len; while (offset < streamsz) { guint32 num_blocks; guint32 length; g_autoptr(GByteArray) st_blk = NULL; st_blk = fu_struct_efi_volume_block_map_parse_stream(stream, offset, error); if (st_blk == NULL) return FALSE; num_blocks = fu_struct_efi_volume_block_map_get_num_blocks(st_blk); length = fu_struct_efi_volume_block_map_get_length(st_blk); offset += st_blk->len; if (num_blocks == 0x0 && length == 0x0) break; blockmap_sz += (gsize)num_blocks * (gsize)length; } if (blockmap_sz < (gsize)fv_length) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "blocks allocated is less than volume length"); return FALSE; } /* success */ return TRUE; } static GByteArray * fu_efi_volume_write(FuFirmware *firmware, GError **error) { FuEfiVolume *self = FU_EFI_VOLUME(firmware); FuEfiVolumePrivate *priv = GET_PRIVATE(self); g_autoptr(GByteArray) buf = fu_struct_efi_volume_new(); g_autoptr(GByteArray) st_blk = fu_struct_efi_volume_block_map_new(); fwupd_guid_t guid = {0x0}; guint32 hdr_length = 0x48; guint64 fv_length; g_autoptr(GBytes) img_blob = NULL; g_autoptr(FuFirmware) img = NULL; /* sanity check */ if (fu_firmware_get_alignment(firmware) > FU_FIRMWARE_ALIGNMENT_1M) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "alignment invalid, got 0x%02x", fu_firmware_get_alignment(firmware)); return NULL; } /* GUID */ if (fu_firmware_get_id(firmware) == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "no GUID set for EFI FV"); return NULL; } if (!fwupd_guid_from_string(fu_firmware_get_id(firmware), &guid, FWUPD_GUID_FLAG_MIXED_ENDIAN, error)) return NULL; /* length */ img = fu_firmware_get_image_by_id(firmware, NULL, NULL); if (img != NULL) { img_blob = fu_firmware_write(img, error); if (img_blob == NULL) { g_prefix_error(error, "no EFI FV child payload: "); return NULL; } } else { img_blob = fu_firmware_get_bytes_with_patches(firmware, error); if (img_blob == NULL) { g_prefix_error(error, "no EFI FV payload: "); return NULL; } } /* pack */ fu_struct_efi_volume_set_guid(buf, &guid); fv_length = fu_common_align_up(hdr_length + g_bytes_get_size(img_blob), fu_firmware_get_alignment(firmware)); fu_struct_efi_volume_set_length(buf, fv_length); fu_struct_efi_volume_set_attrs(buf, priv->attrs | ((guint32)fu_firmware_get_alignment(firmware) << 16)); fu_struct_efi_volume_set_hdr_len(buf, hdr_length); /* blockmap */ fu_struct_efi_volume_block_map_set_num_blocks(st_blk, fv_length); fu_struct_efi_volume_block_map_set_length(st_blk, 0x1); g_byte_array_append(buf, st_blk->data, st_blk->len); fu_struct_efi_volume_block_map_set_num_blocks(st_blk, 0x0); fu_struct_efi_volume_block_map_set_length(st_blk, 0x0); g_byte_array_append(buf, st_blk->data, st_blk->len); /* fix up checksum */ fu_struct_efi_volume_set_checksum(buf, 0x10000 - fu_sum16w(buf->data, buf->len, G_LITTLE_ENDIAN)); /* pad contents to alignment */ fu_byte_array_append_bytes(buf, img_blob); fu_byte_array_set_size(buf, fv_length, 0xFF); /* success */ return g_steal_pointer(&buf); } static void fu_efi_volume_init(FuEfiVolume *self) { FuEfiVolumePrivate *priv = GET_PRIVATE(self); priv->attrs = 0xfeff; g_type_ensure(FU_TYPE_EFI_FILESYSTEM); } static void fu_efi_volume_class_init(FuEfiVolumeClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_efi_volume_validate; firmware_class->parse = fu_efi_volume_parse; firmware_class->write = fu_efi_volume_write; firmware_class->export = fu_efi_volume_export; } /** * fu_efi_volume_new: * * Creates a new #FuFirmware * * Since: 2.0.0 **/ FuFirmware * fu_efi_volume_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_EFI_VOLUME, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-efi-volume.h000066400000000000000000000006041501337203100205760ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_EFI_VOLUME (fu_efi_volume_get_type()) G_DECLARE_DERIVABLE_TYPE(FuEfiVolume, fu_efi_volume, FU, EFI_VOLUME, FuFirmware) struct _FuEfiVolumeClass { FuFirmwareClass parent_class; }; FuFirmware * fu_efi_volume_new(void); fwupd-2.0.10/libfwupdplugin/fu-efi-x509-device.c000066400000000000000000000153211501337203100212260ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuEfiX509Device" #include "config.h" #include "fu-archive-firmware.h" #include "fu-efi-variable-authentication2.h" #include "fu-efi-x509-device.h" #include "fu-version-common.h" /** * FuEfiX509Device * * See also: #FuUdevDevice */ typedef struct { FuEfiX509Signature *sig; } FuEfiX509DevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuEfiX509Device, fu_efi_x509_device, FU_TYPE_DEVICE) #define GET_PRIVATE(o) (fu_efi_x509_device_get_instance_private(o)) static gboolean fu_efi_x509_device_probe(FuDevice *device, GError **error) { FuEfiX509Device *self = FU_EFI_X509_DEVICE(device); FuEfiX509DevicePrivate *priv = GET_PRIVATE(self); const gchar *subject_name; const gchar *subject_vendor; g_autofree gchar *logical_id = NULL; /* sanity check */ if (priv->sig == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no sig"); return FALSE; } /* the O= key may not exist */ subject_name = fu_efi_x509_signature_get_subject_name(priv->sig); subject_vendor = fu_efi_x509_signature_get_subject_vendor(priv->sig); fu_device_add_instance_strsafe(device, "VENDOR", subject_vendor); fu_device_add_instance_strsafe(device, "NAME", subject_name); fu_device_build_instance_id(device, NULL, "UEFI", "VENDOR", "NAME", NULL); fu_device_set_name(device, subject_name != NULL ? subject_name : "Unknown"); fu_device_set_vendor(device, subject_vendor != NULL ? subject_vendor : "Unknown"); fu_device_set_version_raw(device, fu_firmware_get_version_raw(FU_FIRMWARE(priv->sig))); /* the device ID (and thus the logical ID) needs to stay the same between versions */ logical_id = g_strdup_printf("%s:%s", subject_name != NULL ? subject_name : "UNKNOWN", subject_vendor != NULL ? subject_vendor : "UNKNOWN"); fu_device_set_logical_id(device, logical_id); fu_device_build_vendor_id(device, "UEFI", subject_vendor != NULL ? subject_vendor : "UNKNOWN"); /* success */ fu_device_add_instance_strup(device, "CRT", fu_firmware_get_id(FU_FIRMWARE(priv->sig))); return fu_device_build_instance_id(device, error, "UEFI", "CRT", NULL); } static gchar * fu_efi_x509_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint64(version_raw, fu_device_get_version_format(device)); } static FuFirmware * fu_efi_x509_device_prepare_firmware(FuDevice *self, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { return fu_firmware_new_from_gtypes(stream, 0x0, flags, error, FU_TYPE_EFI_VARIABLE_AUTHENTICATION2, FU_TYPE_ARCHIVE_FIRMWARE, G_TYPE_INVALID); } static gboolean fu_efi_x509_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDeviceClass *device_class; FuDevice *proxy; g_autoptr(GPtrArray) imgs = NULL; /* not an archive */ if (FU_IS_EFI_VARIABLE_AUTHENTICATION2(firmware)) { imgs = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_ptr_array_add(imgs, g_object_ref(firmware)); } else { imgs = fu_firmware_get_images(firmware); } /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); fu_progress_set_steps(progress, imgs->len); /* process by the parent */ proxy = fu_device_get_proxy(device); if (proxy == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy device assigned"); return FALSE; } device_class = FU_DEVICE_GET_CLASS(proxy); /* install each blob */ for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); g_autoptr(GBytes) fw = NULL; g_debug("installing %s", fu_firmware_get_id(img)); fw = fu_firmware_get_bytes(img, error); if (fw == NULL) return FALSE; if (!device_class->write_firmware(proxy, img, progress, flags, error)) { g_prefix_error(error, "failed to write %s: ", fu_firmware_get_id(img)); return FALSE; } fu_progress_step_done(progress); } /* success! */ return TRUE; } static void fu_efi_x509_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 80, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 20, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_efi_x509_device_init(FuEfiX509Device *self) { fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_NUMBER); fu_device_add_protocol(FU_DEVICE(self), "org.uefi.dbx2"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_EMULATION_TAG); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_REBOOT); fu_device_add_icon(FU_DEVICE(self), "application-certificate"); } static void fu_efi_x509_device_finalize(GObject *obj) { FuEfiX509Device *self = FU_EFI_X509_DEVICE(obj); FuEfiX509DevicePrivate *priv = GET_PRIVATE(self); if (priv->sig != NULL) g_object_unref(priv->sig); G_OBJECT_CLASS(fu_efi_x509_device_parent_class)->finalize(obj); } static void fu_efi_x509_device_class_init(FuEfiX509DeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_efi_x509_device_finalize; device_class->probe = fu_efi_x509_device_probe; device_class->convert_version = fu_efi_x509_device_convert_version; device_class->prepare_firmware = fu_efi_x509_device_prepare_firmware; device_class->write_firmware = fu_efi_x509_device_write_firmware; device_class->set_progress = fu_efi_x509_device_set_progress; } /** * fu_efi_x509_device_new: * @ctx: (not nullable): a #FuContext * @sig: (not nullable): a #FuEfiX509Signature * * Creates a new X.509 EFI device. * * Returns: (transfer full): a #FuEfiX509Device * * Since: 2.0.8 **/ FuEfiX509Device * fu_efi_x509_device_new(FuContext *ctx, FuEfiX509Signature *sig) { g_autoptr(FuEfiX509Device) self = g_object_new(FU_TYPE_EFI_X509_DEVICE, "context", ctx, NULL); FuEfiX509DevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_EFI_X509_SIGNATURE(sig), NULL); priv->sig = g_object_ref(sig); return g_steal_pointer(&self); } fwupd-2.0.10/libfwupdplugin/fu-efi-x509-device.h000066400000000000000000000010001501337203100212200ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-device.h" #include "fu-efi-x509-signature.h" #define FU_TYPE_EFI_X509_DEVICE (fu_efi_x509_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuEfiX509Device, fu_efi_x509_device, FU, EFI_X509_DEVICE, FuDevice) struct _FuEfiX509DeviceClass { FuDeviceClass parent_class; }; FuEfiX509Device * fu_efi_x509_device_new(FuContext *ctx, FuEfiX509Signature *sig) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-efi-x509-signature-private.h000066400000000000000000000010151501337203100234400ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-efi-x509-signature.h" FuEfiX509Signature * fu_efi_x509_signature_new(void); void fu_efi_x509_signature_set_issuer(FuEfiX509Signature *self, const gchar *issuer) G_GNUC_NON_NULL(1); void fu_efi_x509_signature_set_subject(FuEfiX509Signature *self, const gchar *subject) G_GNUC_NON_NULL(1); gchar * fu_efi_x509_signature_build_dedupe_key(FuEfiX509Signature *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-efi-x509-signature.c000066400000000000000000000174261501337203100220000ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #ifdef HAVE_GNUTLS #include #include #endif #include "fu-common.h" #include "fu-efi-signature-private.h" #include "fu-efi-x509-signature-private.h" #include "fu-string.h" #include "fu-version-common.h" #include "fu-x509-certificate.h" /** * FuEfiX509Signature: * * A X.509 certificate as found in an `EFI_SIGNATURE_LIST`. * * See also: [class@FuFirmware] */ struct _FuEfiX509Signature { FuEfiSignature parent_instance; gchar *issuer; gchar *subject; gchar *subject_name; gchar *subject_vendor; }; G_DEFINE_TYPE(FuEfiX509Signature, fu_efi_x509_signature, FU_TYPE_EFI_SIGNATURE) static void fu_efi_x509_signature_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuEfiX509Signature *self = FU_EFI_X509_SIGNATURE(firmware); fu_xmlb_builder_insert_kv(bn, "issuer", self->issuer); fu_xmlb_builder_insert_kv(bn, "subject", self->subject); fu_xmlb_builder_insert_kv(bn, "subject_name", self->subject_name); fu_xmlb_builder_insert_kv(bn, "subject_vendor", self->subject_vendor); } /** * fu_efi_x509_signature_get_issuer: * @self: A #FuEfiX509Signature * * Returns the certificate issuer. * * Returns: string, or %NULL for unset * * Since: 2.0.8 **/ const gchar * fu_efi_x509_signature_get_issuer(FuEfiX509Signature *self) { g_return_val_if_fail(FU_IS_EFI_X509_SIGNATURE(self), NULL); return self->issuer; } /* private */ void fu_efi_x509_signature_set_issuer(FuEfiX509Signature *self, const gchar *issuer) { g_return_if_fail(FU_IS_EFI_X509_SIGNATURE(self)); if (g_strcmp0(issuer, self->issuer) == 0) return; g_free(self->issuer); self->issuer = g_strdup(issuer); } static gchar * fu_efi_x509_signature_normalize_vendor(const gchar *text) { GString *str = g_string_new(text); struct { const gchar *search; const gchar *replace; } dmi_map[] = { {"ASUSTeK MotherBoard", "ASUSTeK"}, {"ASUSTeK Notebook", "ASUSTeK"}, {"Canonical Ltd.", "Canonical"}, {"Dell Inc.", "Dell"}, {"Hughski Ltd.", "Hughski"}, {"Lenovo(Beijing) Ltd", "Lenovo"}, {"Lenovo Ltd.", "Lenovo"}, {"LG Electronics inc.", "LG"}, {"Microsoft Corporation", "Microsoft"}, {"KEK 2K CA", "KEK CA"}, {"KEK 3K CA", "KEK CA"}, }; /* make the certificate match DMI for LVFS permissions */ for (guint i = 0; i < G_N_ELEMENTS(dmi_map); i++) g_string_replace(str, dmi_map[i].search, dmi_map[i].replace, 0); return g_string_free(str, FALSE); } static void fu_efi_x509_signature_set_subject_vendor(FuEfiX509Signature *self, const gchar *vendor) { self->subject_vendor = fu_efi_x509_signature_normalize_vendor(vendor); } static void fu_efi_x509_signature_set_subject_name(FuEfiX509Signature *self, const gchar *name) { g_autoptr(GString) str = g_string_new(name); /* remove any year suffix */ if (str->len >= 5) { guint64 version_raw = 0; if (fu_strtoull(str->str + str->len - 4, &version_raw, 1982, 2099, FU_INTEGER_BASE_10, NULL)) { g_string_truncate(str, str->len - 5); fu_firmware_set_version_raw(FU_FIRMWARE(self), version_raw); } } self->subject_name = fu_efi_x509_signature_normalize_vendor(str->str); } /* private */ gchar * fu_efi_x509_signature_build_dedupe_key(FuEfiX509Signature *self) { g_return_val_if_fail(FU_IS_EFI_X509_SIGNATURE(self), NULL); /* in 2023 Microsoft renamed "Microsoft Windows Production PCA" -> "Windows UEFI CA" */ if (g_strcmp0(self->subject_vendor, "Microsoft") == 0 && g_strcmp0(self->subject_name, "Microsoft Windows Production PCA") == 0) { return g_strdup("Microsoft:Windows UEFI CA"); } return g_strdup_printf("%s:%s", self->subject_vendor, self->subject_name); } /* private */ void fu_efi_x509_signature_set_subject(FuEfiX509Signature *self, const gchar *subject) { g_return_if_fail(FU_IS_EFI_X509_SIGNATURE(self)); if (g_strcmp0(subject, self->subject) == 0) return; g_free(self->subject); self->subject = g_strdup(subject); /* parse out two keys things we need */ if (subject != NULL) { g_auto(GStrv) attrs = g_strsplit(subject, ",", -1); for (guint i = 0; attrs[i] != NULL; i++) { if (g_str_has_prefix(attrs[i], "O=")) { fu_efi_x509_signature_set_subject_vendor(self, attrs[i] + 2); continue; } if (g_str_has_prefix(attrs[i], "CN=")) { fu_efi_x509_signature_set_subject_name(self, attrs[i] + 3); continue; } } } } /** * fu_efi_x509_signature_get_subject: * @self: A #FuEfiX509Signature * * Returns the certificate subject. * * Returns: string, or %NULL for unset * * Since: 2.0.8 **/ const gchar * fu_efi_x509_signature_get_subject(FuEfiX509Signature *self) { g_return_val_if_fail(FU_IS_EFI_X509_SIGNATURE(self), NULL); return self->subject; } /** * fu_efi_x509_signature_get_subject_name: * @self: A #FuEfiX509Signature * * Returns the certificate subject name, with any suffixed version removed. * * Returns: string, or %NULL for unset * * Since: 2.0.8 **/ const gchar * fu_efi_x509_signature_get_subject_name(FuEfiX509Signature *self) { g_return_val_if_fail(FU_IS_EFI_X509_SIGNATURE(self), NULL); return self->subject_name; } /** * fu_efi_x509_signature_get_subject_vendor: * @self: A #FuEfiX509Signature * * Returns the certificate subject name, with any suffixed version removed. * * Returns: string, or %NULL for unset * * Since: 2.0.8 **/ const gchar * fu_efi_x509_signature_get_subject_vendor(FuEfiX509Signature *self) { g_return_val_if_fail(FU_IS_EFI_X509_SIGNATURE(self), NULL); return self->subject_vendor; } static gboolean fu_efi_x509_signature_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuEfiX509Signature *self = FU_EFI_X509_SIGNATURE(firmware); g_autoptr(FuX509Certificate) crt = fu_x509_certificate_new(); g_autoptr(GBytes) blob = NULL; /* set bytes */ if (!FU_FIRMWARE_CLASS(fu_efi_x509_signature_parent_class) ->parse(firmware, stream, flags, error)) return FALSE; /* parse certificate */ blob = fu_firmware_get_bytes(firmware, error); if (blob == NULL) return FALSE; if (!fu_firmware_parse_bytes(FU_FIRMWARE(crt), blob, 0x0, flags, error)) return FALSE; fu_firmware_set_id(firmware, fu_firmware_get_id(FU_FIRMWARE(crt))); fu_efi_x509_signature_set_issuer(self, fu_x509_certificate_get_issuer(crt)); fu_efi_x509_signature_set_subject(self, fu_x509_certificate_get_subject(crt)); /* success */ return TRUE; } static gchar * fu_efi_x509_signature_convert_version(FuFirmware *firmware, guint64 version_raw) { return fu_version_from_uint64(version_raw, fu_firmware_get_version_format(firmware)); } static void fu_efi_x509_signature_init(FuEfiX509Signature *self) { fu_efi_signature_set_kind(FU_EFI_SIGNATURE(self), FU_EFI_SIGNATURE_KIND_X509); fu_firmware_set_version_format(FU_FIRMWARE(self), FWUPD_VERSION_FORMAT_NUMBER); } static void fu_efi_x509_signature_finalize(GObject *obj) { FuEfiX509Signature *self = FU_EFI_X509_SIGNATURE(obj); g_free(self->issuer); g_free(self->subject); g_free(self->subject_name); g_free(self->subject_vendor); G_OBJECT_CLASS(fu_efi_x509_signature_parent_class)->finalize(obj); } static void fu_efi_x509_signature_class_init(FuEfiX509SignatureClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_efi_x509_signature_finalize; firmware_class->export = fu_efi_x509_signature_export; firmware_class->parse = fu_efi_x509_signature_parse; firmware_class->convert_version = fu_efi_x509_signature_convert_version; } /** * fu_efi_x509_signature_new: * * Creates a new #FuEfiX509Signature. * * Returns: (transfer full): object * * Since: 2.0.8 **/ FuEfiX509Signature * fu_efi_x509_signature_new(void) { return g_object_new(FU_TYPE_EFI_X509_SIGNATURE, NULL); } fwupd-2.0.10/libfwupdplugin/fu-efi-x509-signature.h000066400000000000000000000013531501337203100217750ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-efi-signature.h" #define FU_TYPE_EFI_X509_SIGNATURE (fu_efi_x509_signature_get_type()) G_DECLARE_FINAL_TYPE(FuEfiX509Signature, fu_efi_x509_signature, FU, EFI_X509_SIGNATURE, FuEfiSignature) const gchar * fu_efi_x509_signature_get_issuer(FuEfiX509Signature *self) G_GNUC_NON_NULL(1); const gchar * fu_efi_x509_signature_get_subject(FuEfiX509Signature *self) G_GNUC_NON_NULL(1); const gchar * fu_efi_x509_signature_get_subject_name(FuEfiX509Signature *self) G_GNUC_NON_NULL(1); const gchar * fu_efi_x509_signature_get_subject_vendor(FuEfiX509Signature *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-efi.rs000066400000000000000000000150301501337203100174650ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString, FromString)] enum FuEfiSignatureKind { Unknown, Sha256, X509, } #[repr(u8)] enum FuEfiFileAttrib { None = 0x00, LargeFile = 0x01, DataAlignment2 = 0x02, Fixed = 0x04, DataAlignment = 0x38, Checksum = 0x40, } #[repr(u8)] #[derive(ToString)] enum FuEfiFileType { All = 0x00, Raw = 0x01, Freeform = 0x02, SecurityCore = 0x03, PeiCore = 0x04, DxeCore = 0x05, Peim = 0x06, Driver = 0x07, CombinedPeimDriver = 0x08, Application = 0x09, Mm = 0x0A, FirmwareVolumeImage = 0x0B, CombinedMmDxe = 0x0C, MmCore = 0x0D, MmStandalone = 0x0E, MmCoreStandalone = 0x0F, FfsPad = 0xF0, } #[derive(New, Validate, ParseStream, Default)] #[repr(C, packed)] struct FuStructEfiFile { name: Guid, hdr_checksum: u8, data_checksum: u8, type: FuEfiFileType, attrs: u8, size: u24le, state: u8 = 0xF8, } #[derive(ParseStream)] #[repr(C, packed)] struct FuStructEfiFile2 { _base: FuStructEfiFile, extended_size: u64le, } #[repr(u8)] enum FuEfiCompressionType { NotCompressed = 0x00, StandardCompression = 0x01, } #[derive(ParseStream)] #[repr(C, packed)] struct FuStructEfiSectionCompression { uncompressed_length: u32le, compression_type: FuEfiCompressionType, } #[derive(ToString)] enum FuEfiLz77DecompressorVersion { None, Legacy, Tiano, } #[derive(ParseStream)] #[repr(C, packed)] struct FuStructEfiLz77DecompressorHeader { src_size: u32le, dst_size: u32le, } #[repr(u8)] #[derive(ToString)] enum FuEfiSectionType { // encapsulation section type values Compression = 0x01, GuidDefined = 0x02, Disposable = 0x03, // leaf section type values Pe32 = 0x10, Pic = 0x11, Te = 0x12, DxeDepex = 0x13, Version = 0x14, UserInterface = 0x15, Compatibility16 = 0x16, VolumeImage = 0x17, FreeformSubtypeGuid = 0x18, Raw = 0x19, PeiDepex = 0x1B, MmDepex = 0x1C, // vendor-specific PhoenixSectionPostcode = 0xF0, // Phoenix SCT InsydeSectionPostcode = 0x20, // Insyde H2O } #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructEfiSection { size: u24le, type: FuEfiSectionType, } #[derive(ParseStream)] #[repr(C, packed)] struct FuStructEfiSection2 { _base: FuStructEfiSection, extended_size: u32le, } #[derive(ParseStream)] #[repr(C, packed)] struct FuStructEfiSectionFreeformSubtypeGuid { guid: Guid, } #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructEfiSectionGuidDefined { name: Guid, offset: u16le, attr: u16le, } #[derive(New, ValidateStream, ParseStream, Default)] #[repr(C, packed)] struct FuStructEfiVolume { zero_vector: Guid, guid: Guid, length: u64le, signature: u32le == 0x4856465F, attrs: u32le, hdr_len: u16le, checksum: u16le, ext_hdr: u16le, reserved: u8, revision: u8 == 0x02, } #[derive(ParseStream)] #[repr(C, packed)] struct FuStructEfiVolumeExtHeader { fv_name: Guid, size: u32le, } #[repr(u16le)] #[derive(ToString)] enum FuEfiVolumeExtEntryType { Oem = 0x01, Guid = 0x02, Size = 0x03, } #[derive(ParseStream)] #[repr(C, packed)] struct FuStructEfiVolumeExtEntry { size: u16le, type: FuEfiVolumeExtEntryType, } #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructEfiVolumeBlockMap { num_blocks: u32le, length: u32le, } #[repr(C, packed)] struct FuStructEfiTime { year: u16le, month: u8, day: u8, hour: u8, minute: u8, second: u8, _pad1: u8, nanosecond: u32le, timezone: u16le, daylight: u8, _pad2: u8, } #[derive(Default, Setters)] #[repr(C, packed)] struct FuStructEfiWinCertificate { length: u32le = $struct_size, revision: u16le == 0x0200, certificate_type: u16le == 0x0EF1, guid: Guid == "4aafd29d-68df-49ee-8aa9-347d375665a7", } #[derive(New, ParseStream, ValidateStream)] #[repr(C, packed)] struct FuStructEfiVariableAuthentication2 { timestamp: FuStructEfiTime, auth_info: FuStructEfiWinCertificate, } #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructEfiSignatureList { type: Guid, list_size: u32le, header_size: u32le, size: u32le, } #[repr(u32le)] enum FuEfiLoadOptionAttrs { Active = 0x1, ForceReconnect = 0x2, Hidden = 0x8, Category = 0x1F00, CategoryBoot = 0x0, CategoryAp = 0x100, } #[derive(ToString, FromString)] enum FuEfiLoadOptionKind { Unknown, Path, Hive, Data, } #[derive(ParseStream, New)] #[repr(C, packed)] struct FuStructEfiLoadOption { attrs: FuEfiLoadOptionAttrs, dp_size: u16le, } #[repr(u8)] enum FuEfiDevicePathType { Hardware = 0x01, Acpi, Message, Media, BiosBoot, End = 0x7F, } #[derive(ParseStream, New, Default)] #[repr(C, packed)] struct FuStructEfiDevicePath { type: FuEfiDevicePathType, subtype: u8 = 0xFF, length: u16le = $struct_size, } #[repr(u8)] enum FuEfiHardDriveDevicePathSubtype { HardDrive = 0x01, Cdrom = 0x02, Vendor = 0x03, FilePath = 0x04, MediaProtocol = 0x05, PiwgFirmwareFile = 0x06, PiwgFirmwareVolume = 0x07, RelativeOffsetRange = 0x08, RamDiskDevicePath = 0x09, } #[repr(u8)] #[derive(ToString, FromString)] enum FuEfiHardDriveDevicePathPartitionFormat { LegacyMbr = 0x01, GuidPartitionTable = 0x02, } #[repr(u8)] #[derive(ToString, FromString)] enum FuEfiHardDriveDevicePathSignatureType { None, Addr1b8, Guid, } #[derive(ParseStream, New, Default)] #[repr(C, packed)] struct FuStructEfiHardDriveDevicePath { type: FuEfiDevicePathType == Media, subtype: FuEfiHardDriveDevicePathSubtype = HardDrive, length: u16le == $struct_size, partition_number: u32le, partition_start: u64le, partition_size: u64le, partition_signature: Guid, partition_format: FuEfiHardDriveDevicePathPartitionFormat = GuidPartitionTable, signature_type: FuEfiHardDriveDevicePathSignatureType = Guid, } #[derive(ParseStream, New, Default)] #[repr(C, packed)] struct FuStructShimHive { magic: [char; 4] == "HIVE", header_version: u8 = 0x1, items_count: u8, items_offset: u8, // for forwards and backwards compatibility crc32: u32le, // of the entire hive (excluding padding) //items: [ShimHiveItems; items_count] } #[derive(ParseStream, New)] #[repr(C, packed)] struct FuStructShimHiveItem { key_length: u8, value_length: u32le, // key string, no trailing NUL // value string, no trailing NUL } fwupd-2.0.10/libfwupdplugin/fu-efivars-private.h000066400000000000000000000007101501337203100216330ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-efivars.h" gboolean fu_efivars_set_secure_boot(FuEfivars *self, gboolean enabled, GError **error) G_GNUC_NON_NULL(1); gboolean fu_efivars_set_boot_current(FuEfivars *self, guint16 idx, GError **error) G_GNUC_NON_NULL(1); gboolean fu_efivars_build_boot_order(FuEfivars *self, GError **error, ...) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-efivars.c000066400000000000000000000563651501337203100201770ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2015 Peter Jones * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fwupd-error.h" #include "fu-byte-array.h" #include "fu-efi-device-path-list.h" #include "fu-efi-file-path-device-path.h" #include "fu-efi-hard-drive-device-path.h" #include "fu-efivars-private.h" #include "fu-mem.h" #include "fu-pefile-firmware.h" #include "fu-volume-private.h" G_DEFINE_TYPE(FuEfivars, fu_efivars, G_TYPE_OBJECT) /** * fu_efivars_supported: * @self: a #FuEfivars * @error: #GError * * Determines if the kernel supports EFI variables * * Returns: %TRUE on success * * Since: 2.0.0 **/ gboolean fu_efivars_supported(FuEfivars *self, GError **error) { FuEfivarsClass *efivars_class = FU_EFIVARS_GET_CLASS(self); g_return_val_if_fail(FU_IS_EFIVARS(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (efivars_class->supported == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported"); return FALSE; } return efivars_class->supported(self, error); } /** * fu_efivars_delete: * @self: a #FuEfivars * @guid: Globally unique identifier * @name: Variable name * @error: #GError * * Removes a variable from NVRAM, returning an error if it does not exist. * * Returns: %TRUE on success * * Since: 2.0.0 **/ gboolean fu_efivars_delete(FuEfivars *self, const gchar *guid, const gchar *name, GError **error) { FuEfivarsClass *efivars_class = FU_EFIVARS_GET_CLASS(self); g_return_val_if_fail(FU_IS_EFIVARS(self), FALSE); g_return_val_if_fail(guid != NULL, FALSE); g_return_val_if_fail(name != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (efivars_class->delete == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported"); return FALSE; } return efivars_class->delete(self, guid, name, error); } /** * fu_efivars_delete_with_glob: * @self: a #FuEfivars * @guid: Globally unique identifier * @name_glob: Variable name * @error: #GError * * Removes a group of variables from NVRAM * * Returns: %TRUE on success * * Since: 2.0.0 **/ gboolean fu_efivars_delete_with_glob(FuEfivars *self, const gchar *guid, const gchar *name_glob, GError **error) { FuEfivarsClass *efivars_class = FU_EFIVARS_GET_CLASS(self); g_return_val_if_fail(FU_IS_EFIVARS(self), FALSE); g_return_val_if_fail(guid != NULL, FALSE); g_return_val_if_fail(name_glob != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (efivars_class->delete_with_glob == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported"); return FALSE; } return efivars_class->delete_with_glob(self, guid, name_glob, error); } /** * fu_efivars_exists: * @self: a #FuEfivars * @guid: Globally unique identifier * @name: (nullable): Variable name * * Test if a variable exists * * Returns: %TRUE on success * * Since: 2.0.0 **/ gboolean fu_efivars_exists(FuEfivars *self, const gchar *guid, const gchar *name) { FuEfivarsClass *efivars_class = FU_EFIVARS_GET_CLASS(self); g_return_val_if_fail(FU_IS_EFIVARS(self), FALSE); g_return_val_if_fail(guid != NULL, FALSE); if (efivars_class->exists == NULL) return FALSE; return efivars_class->exists(self, guid, name); } /** * fu_efivars_get_data: * @self: a #FuEfivars * @guid: Globally unique identifier * @name: Variable name * @data: Data to set * @data_sz: size of data * @attr: Attributes * @error: (nullable): optional return location for an error * * Gets the data from a UEFI variable in NVRAM * * Returns: %TRUE on success * * Since: 2.0.0 **/ gboolean fu_efivars_get_data(FuEfivars *self, const gchar *guid, const gchar *name, guint8 **data, gsize *data_sz, guint32 *attr, GError **error) { FuEfivarsClass *efivars_class = FU_EFIVARS_GET_CLASS(self); g_return_val_if_fail(FU_IS_EFIVARS(self), FALSE); g_return_val_if_fail(guid != NULL, FALSE); g_return_val_if_fail(name != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (efivars_class->get_data == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported"); return FALSE; } return efivars_class->get_data(self, guid, name, data, data_sz, attr, error); } /** * fu_efivars_get_data_bytes: * @self: a #FuEfivars * @guid: Globally unique identifier * @name: Variable name * @attr: (nullable): Attributes * @error: (nullable): optional return location for an error * * Gets the data from a UEFI variable in NVRAM * * Returns: (transfer full): a #GBytes, or %NULL * * Since: 2.0.0 **/ GBytes * fu_efivars_get_data_bytes(FuEfivars *self, const gchar *guid, const gchar *name, guint32 *attr, GError **error) { guint8 *data = NULL; gsize datasz = 0; g_return_val_if_fail(FU_IS_EFIVARS(self), NULL); g_return_val_if_fail(guid != NULL, NULL); g_return_val_if_fail(name != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); if (!fu_efivars_get_data(self, guid, name, &data, &datasz, attr, error)) return NULL; return g_bytes_new_take(data, datasz); } /** * fu_efivars_get_names: * @self: a #FuEfivars * @guid: Globally unique identifier * @error: (nullable): optional return location for an error * * Gets the list of names where the GUID matches. An error is set if there are * no names matching the GUID. * * Returns: (transfer container) (element-type utf8): array of names * * Since: 2.0.0 **/ GPtrArray * fu_efivars_get_names(FuEfivars *self, const gchar *guid, GError **error) { FuEfivarsClass *efivars_class = FU_EFIVARS_GET_CLASS(self); g_return_val_if_fail(FU_IS_EFIVARS(self), NULL); g_return_val_if_fail(guid != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); if (efivars_class->get_names == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported"); return NULL; } return efivars_class->get_names(self, guid, error); } /** * fu_efivars_get_monitor: * @self: a #FuEfivars * @guid: Globally unique identifier * @name: Variable name * @error: (nullable): optional return location for an error * * Returns a file monitor for a specific key. * * Returns: (transfer full): a #GFileMonitor, or %NULL for an error * * Since: 2.0.0 **/ GFileMonitor * fu_efivars_get_monitor(FuEfivars *self, const gchar *guid, const gchar *name, GError **error) { FuEfivarsClass *efivars_class = FU_EFIVARS_GET_CLASS(self); g_return_val_if_fail(FU_IS_EFIVARS(self), NULL); g_return_val_if_fail(guid != NULL, NULL); g_return_val_if_fail(name != NULL, NULL); if (efivars_class->get_monitor == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported"); return NULL; } return efivars_class->get_monitor(self, guid, name, error); } /** * fu_efivars_space_used: * @self: a #FuEfivars * @error: (nullable): optional return location for an error * * Gets the total size used by all EFI variables. This may be less than the size reported by the * kernel as some (hopefully small) variables are hidden from userspace. * * Returns: total allocated size of all visible variables, or %G_MAXUINT64 on error * * Since: 2.0.0 **/ guint64 fu_efivars_space_used(FuEfivars *self, GError **error) { FuEfivarsClass *efivars_class = FU_EFIVARS_GET_CLASS(self); g_return_val_if_fail(FU_IS_EFIVARS(self), G_MAXUINT64); g_return_val_if_fail(error == NULL || *error == NULL, G_MAXUINT64); if (efivars_class->space_used == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported"); return G_MAXUINT64; } return efivars_class->space_used(self, error); } /** * fu_efivars_set_data: * @self: a #FuEfivars * @guid: Globally unique identifier * @name: Variable name * @data: Data to set * @sz: size of @data * @attr: Attributes * @error: (nullable): optional return location for an error * * Sets the data to a UEFI variable in NVRAM * * Returns: %TRUE on success * * Since: 2.0.0 **/ gboolean fu_efivars_set_data(FuEfivars *self, const gchar *guid, const gchar *name, const guint8 *data, gsize sz, guint32 attr, GError **error) { FuEfivarsClass *efivars_class = FU_EFIVARS_GET_CLASS(self); g_return_val_if_fail(FU_IS_EFIVARS(self), FALSE); g_return_val_if_fail(guid != NULL, FALSE); g_return_val_if_fail(name != NULL, FALSE); g_return_val_if_fail(data != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (efivars_class->set_data == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported"); return FALSE; } return efivars_class->set_data(self, guid, name, data, sz, attr, error); } /** * fu_efivars_set_data_bytes: * @self: a #FuEfivars * @guid: globally unique identifier * @name: variable name * @bytes: data blob * @attr: attributes * @error: (nullable): optional return location for an error * * Sets the data to a UEFI variable in NVRAM * * Returns: %TRUE on success * * Since: 2.0.0 **/ gboolean fu_efivars_set_data_bytes(FuEfivars *self, const gchar *guid, const gchar *name, GBytes *bytes, guint32 attr, GError **error) { gsize bufsz = 0; const guint8 *buf; g_return_val_if_fail(FU_IS_EFIVARS(self), FALSE); g_return_val_if_fail(guid != NULL, FALSE); g_return_val_if_fail(name != NULL, FALSE); g_return_val_if_fail(bytes != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); buf = g_bytes_get_data(bytes, &bufsz); return fu_efivars_set_data(self, guid, name, buf, bufsz, attr, error); } /** * fu_efivars_get_secure_boot: * @self: a #FuEfivars * @enabled: (out): SecureBoot value * @error: (nullable): optional return location for an error * * Determines if secure boot was enabled * * Returns: %TRUE on success * * Since: 2.0.0 **/ gboolean fu_efivars_get_secure_boot(FuEfivars *self, gboolean *enabled, GError **error) { gsize data_size = 0; g_autofree guint8 *data = NULL; g_return_val_if_fail(FU_IS_EFIVARS(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_efivars_get_data(self, FU_EFIVARS_GUID_EFI_GLOBAL, "SecureBoot", &data, &data_size, NULL, NULL)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "SecureBoot is not available"); return FALSE; } if (data_size == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "SecureBoot variable was empty"); return FALSE; } /* available, but not enabled */ if (enabled != NULL) *enabled = (data[0] & 0x01) > 0; /* success */ return TRUE; } /** * fu_efivars_set_secure_boot: (skip): **/ gboolean fu_efivars_set_secure_boot(FuEfivars *self, gboolean enabled, GError **error) { guint8 value = enabled ? 0x01 : 0x00; g_return_val_if_fail(FU_IS_EFIVARS(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return fu_efivars_set_data(self, FU_EFIVARS_GUID_EFI_GLOBAL, "SecureBoot", &value, sizeof(value), FU_EFIVARS_ATTR_BOOTSERVICE_ACCESS, error); } /** * fu_efivars_get_boot_next: * @self: a #FuEfivars * @idx: (out) (nullable): boot index, typically 0x0001 * @error: #GError * * Gets the index of the `BootNext` variable. * * Returns: %TRUE on success * * Since: 2.0.0 **/ gboolean fu_efivars_get_boot_next(FuEfivars *self, guint16 *idx, GError **error) { g_autofree guint8 *buf = NULL; gsize bufsz = 0; g_return_val_if_fail(FU_IS_EFIVARS(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_efivars_get_data(self, FU_EFIVARS_GUID_EFI_GLOBAL, "BootNext", &buf, &bufsz, NULL, error)) return FALSE; if (bufsz != sizeof(guint16)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid size"); return FALSE; } if (idx != NULL) *idx = fu_memread_uint16(buf, G_LITTLE_ENDIAN); /* success */ return TRUE; } /** * fu_efivars_set_boot_next: * @self: a #FuEfivars * @idx: boot index, typically 0x0001 * @error: #GError * * Sets the index of the `BootNext` variable. * * Returns: %TRUE on success * * Since: 2.0.0 **/ gboolean fu_efivars_set_boot_next(FuEfivars *self, guint16 idx, GError **error) { guint8 buf[2] = {0}; g_return_val_if_fail(FU_IS_EFIVARS(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); fu_memwrite_uint16(buf, idx, G_LITTLE_ENDIAN); return fu_efivars_set_data(self, FU_EFIVARS_GUID_EFI_GLOBAL, "BootNext", buf, sizeof(buf), FU_EFIVARS_ATTR_NON_VOLATILE | FU_EFIVARS_ATTR_BOOTSERVICE_ACCESS | FU_EFIVARS_ATTR_RUNTIME_ACCESS, error); } /** * fu_efivars_get_boot_current: * @self: a #FuEfivars * @idx: (out): boot index, typically 0x0001 * @error: #GError * * Gets the index of the `BootCurrent` variable. * * Returns: %TRUE on success * * Since: 2.0.0 **/ gboolean fu_efivars_get_boot_current(FuEfivars *self, guint16 *idx, GError **error) { g_autofree guint8 *buf = NULL; gsize bufsz = 0; g_return_val_if_fail(FU_IS_EFIVARS(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_efivars_get_data(self, FU_EFIVARS_GUID_EFI_GLOBAL, "BootCurrent", &buf, &bufsz, NULL, error)) return FALSE; if (bufsz != sizeof(guint16)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid size"); return FALSE; } if (idx != NULL) *idx = fu_memread_uint16(buf, G_LITTLE_ENDIAN); /* success */ return TRUE; } /** * fu_efivars_set_boot_current: (skip): **/ gboolean fu_efivars_set_boot_current(FuEfivars *self, guint16 idx, GError **error) { guint8 buf[2] = {0}; g_return_val_if_fail(FU_IS_EFIVARS(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); fu_memwrite_uint16(buf, idx, G_LITTLE_ENDIAN); return fu_efivars_set_data(self, FU_EFIVARS_GUID_EFI_GLOBAL, "BootCurrent", buf, sizeof(buf), FU_EFIVARS_ATTR_NON_VOLATILE | FU_EFIVARS_ATTR_RUNTIME_ACCESS, error); } /** * fu_efivars_get_boot_order: * @self: a #FuEfivars * @error: #GError * * Gets the indexes of the `BootOrder` variable. * * Returns: (transfer full) (element-type guint16): boot order, or %NULL on error * * Since: 2.0.0 **/ GArray * fu_efivars_get_boot_order(FuEfivars *self, GError **error) { gsize bufsz = 0; g_autofree guint8 *buf = NULL; g_autoptr(GArray) order = g_array_new(FALSE, FALSE, sizeof(guint16)); g_return_val_if_fail(FU_IS_EFIVARS(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); if (!fu_efivars_get_data(self, FU_EFIVARS_GUID_EFI_GLOBAL, "BootOrder", &buf, &bufsz, NULL, error)) return NULL; if (bufsz % sizeof(guint16) != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid size"); return NULL; } for (gsize i = 0; i < bufsz; i += sizeof(guint16)) { guint16 idx = fu_memread_uint16(buf + i, G_LITTLE_ENDIAN); g_array_append_val(order, idx); } /* success */ return g_steal_pointer(&order); } /** * fu_efivars_set_boot_order: * @self: a #FuEfivars * @order: (element-type guint16): boot order * @error: #GError * * Sets the index of the `BootNext` variable. * * Returns: %TRUE on success * * Since: 2.0.0 **/ gboolean fu_efivars_set_boot_order(FuEfivars *self, GArray *order, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_return_val_if_fail(FU_IS_EFIVARS(self), FALSE); g_return_val_if_fail(order != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); for (guint i = 0; i < order->len; i++) { guint16 idx = g_array_index(order, guint16, i); fu_byte_array_append_uint16(buf, idx, G_LITTLE_ENDIAN); } return fu_efivars_set_data(self, FU_EFIVARS_GUID_EFI_GLOBAL, "BootOrder", buf->data, buf->len, FU_EFIVARS_ATTR_NON_VOLATILE | FU_EFIVARS_ATTR_BOOTSERVICE_ACCESS | FU_EFIVARS_ATTR_RUNTIME_ACCESS, error); } /** * fu_efivars_build_boot_order: (skip) **/ gboolean fu_efivars_build_boot_order(FuEfivars *self, GError **error, ...) { va_list args; g_autoptr(GArray) order = g_array_new(FALSE, FALSE, sizeof(guint16)); g_return_val_if_fail(FU_IS_EFIVARS(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); va_start(args, error); while (TRUE) { guint16 idx = va_arg(args, guint); if (idx == G_MAXUINT16) break; g_array_append_val(order, idx); } va_end(args); /* success */ return fu_efivars_set_boot_order(self, order, error); } /** * fu_efivars_create_boot_entry_for_volume: * @self: a #FuEfivars * @idx: boot index, typically 0x0001 * @volume: a #FuVolume * @name: a display name, e.g. "Fedora" * @target: an EFI binary, e.g. "shim.efi" * @error: #GError * * Creates a BootXXXX variable for a given volume, name and target. * * If @target does not exist on the volume then a dummy file is created. * * Returns: %TRUE on success * * Since: 2.0.6 **/ gboolean fu_efivars_create_boot_entry_for_volume(FuEfivars *self, guint16 idx, FuVolume *volume, const gchar *name, const gchar *target, GError **error) { g_autoptr(FuEfiDevicePathList) devpath_list = fu_efi_device_path_list_new(); g_autoptr(FuEfiFilePathDevicePath) dp_fp = NULL; g_autoptr(FuEfiHardDriveDevicePath) dp_hdd = NULL; g_autoptr(FuEfiLoadOption) entry = fu_efi_load_option_new(); g_autoptr(GFile) file = NULL; g_autofree gchar *mount_point = NULL; g_return_val_if_fail(FU_IS_EFIVARS(self), FALSE); g_return_val_if_fail(FU_IS_VOLUME(volume), FALSE); g_return_val_if_fail(name != NULL, FALSE); g_return_val_if_fail(target != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* create plausible EFI file if not already exists */ mount_point = fu_volume_get_mount_point(volume); if (mount_point == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "volume has no mount point"); return FALSE; } file = g_file_new_build_filename(mount_point, target, NULL); if (!g_file_query_exists(file, NULL)) { g_autoptr(FuFirmware) img_text = fu_firmware_new(); g_autoptr(FuFirmware) pefile = fu_pefile_firmware_new(); g_autoptr(GBytes) img_blob = g_bytes_new_static("hello", 5); fu_firmware_set_id(img_text, ".text"); fu_firmware_set_bytes(img_text, img_blob); fu_firmware_add_image(pefile, img_text); if (!fu_firmware_write_file(pefile, file, error)) return FALSE; } dp_hdd = fu_efi_hard_drive_device_path_new_from_volume(volume, error); if (dp_hdd == NULL) return FALSE; dp_fp = fu_efi_file_path_device_path_new(); if (!fu_efi_file_path_device_path_set_name(dp_fp, target, error)) return FALSE; fu_firmware_add_image(FU_FIRMWARE(devpath_list), FU_FIRMWARE(dp_hdd)); fu_firmware_add_image(FU_FIRMWARE(devpath_list), FU_FIRMWARE(dp_fp)); fu_firmware_set_id(FU_FIRMWARE(entry), name); fu_firmware_add_image(FU_FIRMWARE(entry), FU_FIRMWARE(devpath_list)); return fu_efivars_set_boot_entry(self, idx, entry, error); } /** * fu_efivars_get_boot_data: * @self: a #FuEfivars * @idx: boot index, typically 0x0001 * @error: #GError * * Gets the raw data of the `BootXXXX` variable. * * Returns: (transfer full): boot data * * Since: 2.0.0 **/ GBytes * fu_efivars_get_boot_data(FuEfivars *self, guint16 idx, GError **error) { g_autofree gchar *name = g_strdup_printf("Boot%04X", idx); g_return_val_if_fail(FU_IS_EFIVARS(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return fu_efivars_get_data_bytes(self, FU_EFIVARS_GUID_EFI_GLOBAL, name, NULL, error); } /** * fu_efivars_set_boot_data: * @self: a #FuEfivars * @idx: boot index, typically 0x0001 * @blob: #GBytes * @error: #GError * * Sets the raw data of the `BootXXXX` variable. If @blob is %NULL then the boot entry is deleted. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fu_efivars_set_boot_data(FuEfivars *self, guint16 idx, GBytes *blob, GError **error) { g_autofree gchar *name = g_strdup_printf("Boot%04X", idx); g_return_val_if_fail(FU_IS_EFIVARS(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (blob == NULL) return fu_efivars_delete(self, FU_EFIVARS_GUID_EFI_GLOBAL, name, error); return fu_efivars_set_data_bytes(self, FU_EFIVARS_GUID_EFI_GLOBAL, name, blob, FU_EFIVARS_ATTR_NON_VOLATILE | FU_EFIVARS_ATTR_BOOTSERVICE_ACCESS | FU_EFIVARS_ATTR_RUNTIME_ACCESS, error); } /** * fu_efivars_get_boot_entry: * @self: a #FuEfivars * @idx: boot index, typically 0x0001 * @error: #GError * * Gets the loadopt data of the `BootXXXX` variable. * * Returns: (transfer full): a #FuEfiLoadOption, or %NULL * * Since: 2.0.0 **/ FuEfiLoadOption * fu_efivars_get_boot_entry(FuEfivars *self, guint16 idx, GError **error) { g_autofree gchar *name = g_strdup_printf("Boot%04X", idx); g_autoptr(FuEfiLoadOption) loadopt = fu_efi_load_option_new(); g_autoptr(GBytes) blob = NULL; g_return_val_if_fail(FU_IS_EFIVARS(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* get data */ blob = fu_efivars_get_data_bytes(self, FU_EFIVARS_GUID_EFI_GLOBAL, name, NULL, error); if (blob == NULL) return NULL; if (!fu_firmware_parse_bytes(FU_FIRMWARE(loadopt), blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return NULL; fu_firmware_set_idx(FU_FIRMWARE(loadopt), idx); return g_steal_pointer(&loadopt); } /** * fu_efivars_set_boot_entry: * @self: a #FuEfivars * @idx: boot index, typically 0x0001 * @entry: a #FuEfiLoadOption * @error: #GError * * Sets the loadopt data of the `BootXXXX` variable. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fu_efivars_set_boot_entry(FuEfivars *self, guint16 idx, FuEfiLoadOption *entry, GError **error) { g_autoptr(GBytes) blob = NULL; g_return_val_if_fail(FU_IS_EFIVARS(self), FALSE); g_return_val_if_fail(FU_IS_EFI_LOAD_OPTION(entry), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); blob = fu_firmware_write(FU_FIRMWARE(entry), error); if (blob == NULL) return FALSE; return fu_efivars_set_boot_data(self, idx, blob, error); } /** * fu_efivars_get_boot_entries: * @self: a #FuEfivars * @error: #GError * * Gets the loadopt data for all the entries listed in `BootOrder`. * * Returns: (transfer full) (element-type FuEfiLoadOption): boot data * * Since: 2.0.0 **/ GPtrArray * fu_efivars_get_boot_entries(FuEfivars *self, GError **error) { g_autoptr(GArray) order = NULL; g_autoptr(GPtrArray) array = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_return_val_if_fail(FU_IS_EFIVARS(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); order = fu_efivars_get_boot_order(self, error); if (order == NULL) return NULL; for (guint i = 0; i < order->len; i++) { guint16 idx = g_array_index(order, guint16, i); g_autoptr(FuEfiLoadOption) loadopt = NULL; loadopt = fu_efivars_get_boot_entry(self, idx, error); if (loadopt == NULL) { g_prefix_error(error, "failed to load Boot%04X: ", idx); return NULL; } g_ptr_array_add(array, g_steal_pointer(&loadopt)); } /* success */ return g_steal_pointer(&array); } static void fu_efivars_init(FuEfivars *self) { } static void fu_efivars_class_init(FuEfivarsClass *klass) { } fwupd-2.0.10/libfwupdplugin/fu-efivars.h000066400000000000000000000131731501337203100201720ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2015 Peter Jones * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-efi-load-option.h" #include "fu-volume.h" #define FU_TYPE_EFIVARS (fu_efivars_get_type()) G_DECLARE_DERIVABLE_TYPE(FuEfivars, fu_efivars, FU, EFIVARS, GObject) struct _FuEfivarsClass { GObjectClass parent_class; gboolean (*supported)(FuEfivars *self, GError **error) G_GNUC_NON_NULL(1); guint64 (*space_used)(FuEfivars *self, GError **error) G_GNUC_NON_NULL(1); gboolean (*exists)(FuEfivars *self, const gchar *guid, const gchar *name) G_GNUC_NON_NULL(1, 2); GFileMonitor *(*get_monitor)(FuEfivars *self, const gchar *guid, const gchar *name, GError **error)G_GNUC_NON_NULL(1, 2, 3); gboolean (*get_data)(FuEfivars *self, const gchar *guid, const gchar *name, guint8 **data, gsize *data_sz, guint32 *attr, GError **error) G_GNUC_NON_NULL(1, 2, 3); gboolean (*set_data)(FuEfivars *self, const gchar *guid, const gchar *name, const guint8 *data, gsize sz, guint32 attr, GError **error) G_GNUC_NON_NULL(1, 2, 3); gboolean (*delete)(FuEfivars *self, const gchar *guid, const gchar *name, GError **error) G_GNUC_NON_NULL(1, 2, 3); gboolean (*delete_with_glob)(FuEfivars *self, const gchar *guid, const gchar *name_glob, GError **error) G_GNUC_NON_NULL(1, 2, 3); GPtrArray *(*get_names)(FuEfivars *self, const gchar *guid, GError **error)G_GNUC_NON_NULL(1, 2); }; #define FU_EFIVARS_GUID_EFI_GLOBAL "8be4df61-93ca-11d2-aa0d-00e098032b8c" #define FU_EFIVARS_GUID_FWUPDATE "0abba7dc-e516-4167-bbf5-4d9d1c739416" #define FU_EFIVARS_GUID_UX_CAPSULE "3b8c8162-188c-46a4-aec9-be43f1d65697" #define FU_EFIVARS_GUID_SECURITY_DATABASE "d719b2cb-3d3a-4596-a3bc-dad00e67656f" #define FU_EFIVARS_GUID_UX_CAPSULE "3b8c8162-188c-46a4-aec9-be43f1d65697" #define FU_EFIVARS_GUID_EFI_CAPSULE_REPORT "39b68c46-f7fb-441b-b6ec-16b0f69821f3" #define FU_EFIVARS_GUID_SHIM "605dab50-e046-4300-abb6-3dd810dd8b23" #define FU_EFIVARS_ATTR_NON_VOLATILE (1 << 0) #define FU_EFIVARS_ATTR_BOOTSERVICE_ACCESS (1 << 1) #define FU_EFIVARS_ATTR_RUNTIME_ACCESS (1 << 2) #define FU_EFIVARS_ATTR_HARDWARE_ERROR_RECORD (1 << 3) #define FU_EFIVARS_ATTR_AUTHENTICATED_WRITE_ACCESS (1 << 4) #define FU_EFIVARS_ATTR_TIME_BASED_AUTHENTICATED_WRITE_ACCESS (1 << 5) #define FU_EFIVARS_ATTR_APPEND_WRITE (1 << 6) FuEfivars * fu_efivars_new(void); gboolean fu_efivars_supported(FuEfivars *self, GError **error) G_GNUC_NON_NULL(1); guint64 fu_efivars_space_used(FuEfivars *self, GError **error) G_GNUC_NON_NULL(1); gboolean fu_efivars_exists(FuEfivars *self, const gchar *guid, const gchar *name) G_GNUC_NON_NULL(1, 2); GFileMonitor * fu_efivars_get_monitor(FuEfivars *self, const gchar *guid, const gchar *name, GError **error) G_GNUC_NON_NULL(1, 2, 3); gboolean fu_efivars_get_data(FuEfivars *self, const gchar *guid, const gchar *name, guint8 **data, gsize *data_sz, guint32 *attr, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); GBytes * fu_efivars_get_data_bytes(FuEfivars *self, const gchar *guid, const gchar *name, guint32 *attr, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); gboolean fu_efivars_set_data(FuEfivars *self, const gchar *guid, const gchar *name, const guint8 *data, gsize sz, guint32 attr, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); gboolean fu_efivars_set_data_bytes(FuEfivars *self, const gchar *guid, const gchar *name, GBytes *bytes, guint32 attr, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); gboolean fu_efivars_delete(FuEfivars *self, const gchar *guid, const gchar *name, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); gboolean fu_efivars_delete_with_glob(FuEfivars *self, const gchar *guid, const gchar *name_glob, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); GPtrArray * fu_efivars_get_names(FuEfivars *self, const gchar *guid, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_efivars_get_secure_boot(FuEfivars *self, gboolean *enabled, GError **error) G_GNUC_NON_NULL(1); gboolean fu_efivars_get_boot_next(FuEfivars *self, guint16 *idx, GError **error) G_GNUC_NON_NULL(1); gboolean fu_efivars_set_boot_next(FuEfivars *self, guint16 idx, GError **error) G_GNUC_NON_NULL(1); gboolean fu_efivars_get_boot_current(FuEfivars *self, guint16 *idx, GError **error) G_GNUC_NON_NULL(1); GArray * fu_efivars_get_boot_order(FuEfivars *self, GError **error) G_GNUC_NON_NULL(1); gboolean fu_efivars_set_boot_order(FuEfivars *self, GArray *order, GError **error) G_GNUC_NON_NULL(1, 2); GBytes * fu_efivars_get_boot_data(FuEfivars *self, guint16 idx, GError **error) G_GNUC_NON_NULL(1); gboolean fu_efivars_set_boot_data(FuEfivars *self, guint16 idx, GBytes *blob, GError **error) G_GNUC_NON_NULL(1); FuEfiLoadOption * fu_efivars_get_boot_entry(FuEfivars *self, guint16 idx, GError **error) G_GNUC_NON_NULL(1); gboolean fu_efivars_set_boot_entry(FuEfivars *self, guint16 idx, FuEfiLoadOption *entry, GError **error) G_GNUC_NON_NULL(1, 3); GPtrArray * fu_efivars_get_boot_entries(FuEfivars *self, GError **error) G_GNUC_NON_NULL(1); gboolean fu_efivars_create_boot_entry_for_volume(FuEfivars *self, guint16 idx, FuVolume *volume, const gchar *name, const gchar *target, GError **error) G_GNUC_NON_NULL(1, 3, 4, 5); fwupd-2.0.10/libfwupdplugin/fu-elf-firmware.c000066400000000000000000000311221501337203100211000ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-elf-firmware.h" #include "fu-elf-struct.h" #include "fu-input-stream.h" #include "fu-partial-input-stream.h" #include "fu-string.h" /** * FuElfFirmware: * * Executable and Linkable Format is a common standard file format for executable files, * object code, shared libraries, core dumps -- and sometimes firmware. * * Documented: * https://en.wikipedia.org/wiki/Executable_and_Linkable_Format */ G_DEFINE_TYPE(FuElfFirmware, fu_elf_firmware, FU_TYPE_FIRMWARE) static gboolean fu_elf_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_elf_file_header64le_validate_stream(stream, offset, error); } static gboolean fu_elf_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { gsize offset_secthdr = 0; gsize offset_proghdr = 0; guint16 phentsize; guint16 phnum; guint16 shnum; g_autoptr(GByteArray) st_fhdr = NULL; g_autoptr(GByteArray) shstrndx_buf = NULL; g_autoptr(GPtrArray) sections = g_ptr_array_new_with_free_func((GDestroyNotify)g_byte_array_unref); /* file header */ st_fhdr = fu_struct_elf_file_header64le_parse_stream(stream, 0x0, error); if (st_fhdr == NULL) return FALSE; /* parse each program header, unused here */ offset_proghdr += fu_struct_elf_file_header64le_get_phoff(st_fhdr); phentsize = fu_struct_elf_file_header64le_get_phentsize(st_fhdr); phnum = fu_struct_elf_file_header64le_get_phnum(st_fhdr); for (guint i = 0; i < phnum; i++) { g_autoptr(GByteArray) st_phdr = fu_struct_elf_program_header64le_parse_stream(stream, offset_proghdr, error); if (st_phdr == NULL) return FALSE; offset_proghdr += phentsize; } /* parse all the sections ahead of time */ offset_secthdr += fu_struct_elf_file_header64le_get_shoff(st_fhdr); shnum = fu_struct_elf_file_header64le_get_shnum(st_fhdr); for (guint i = 0; i < shnum; i++) { g_autoptr(FuStructElfSectionHeader64le) st_shdr = fu_struct_elf_section_header64le_parse_stream(stream, offset_secthdr, error); if (st_shdr == NULL) return FALSE; g_ptr_array_add(sections, g_steal_pointer(&st_shdr)); offset_secthdr += fu_struct_elf_file_header64le_get_shentsize(st_fhdr); } /* add sections as images */ for (guint i = 0; i < sections->len; i++) { FuStructElfSectionHeader64le *st_shdr = g_ptr_array_index(sections, i); guint64 sect_offset = fu_struct_elf_section_header64le_get_offset(st_shdr); guint64 sect_size = fu_struct_elf_section_header64le_get_size(st_shdr); g_autoptr(FuFirmware) img = fu_firmware_new(); /* catch the strtab */ if (i == fu_struct_elf_file_header64le_get_shstrndx(st_fhdr)) { if (fu_struct_elf_section_header64le_get_type(st_shdr) != FU_ELF_SECTION_HEADER_TYPE_STRTAB) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "shstrndx section type was not strtab, was %s", fu_elf_section_header_type_to_string( fu_struct_elf_section_header64le_get_type(st_shdr))); return FALSE; } shstrndx_buf = fu_input_stream_read_byte_array(stream, sect_offset, sect_size, NULL, error); if (shstrndx_buf == NULL) return FALSE; continue; } if (fu_struct_elf_section_header64le_get_type(st_shdr) == FU_ELF_SECTION_HEADER_TYPE_NULL || fu_struct_elf_section_header64le_get_type(st_shdr) == FU_ELF_SECTION_HEADER_TYPE_STRTAB) continue; if (sect_size > 0) { g_autoptr(GInputStream) img_stream = fu_partial_input_stream_new(stream, sect_offset, sect_size, error); if (img_stream == NULL) { g_prefix_error(error, "failed to cut EFI image: "); return FALSE; } if (!fu_firmware_parse_stream(img, img_stream, 0x0, flags, error)) return FALSE; } fu_firmware_set_idx(img, i); if (!fu_firmware_add_image_full(firmware, img, error)) return FALSE; } /* no shstrndx found */ if (shstrndx_buf == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "shstrndx was invalid"); return FALSE; } /* fix up the section names */ for (guint i = 0; i < sections->len; i++) { FuStructElfSectionHeader64le *st_shdr = g_ptr_array_index(sections, i); guint32 sh_name = fu_struct_elf_section_header64le_get_name(st_shdr); g_autofree gchar *name = NULL; g_autoptr(FuFirmware) img = NULL; if (fu_struct_elf_section_header64le_get_type(st_shdr) == FU_ELF_SECTION_HEADER_TYPE_NULL || fu_struct_elf_section_header64le_get_type(st_shdr) == FU_ELF_SECTION_HEADER_TYPE_STRTAB) continue; if (sh_name > shstrndx_buf->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "offset into shstrndx invalid for section 0x%x", i); return FALSE; } img = fu_firmware_get_image_by_idx(firmware, i, error); if (img == NULL) return FALSE; name = g_strndup((const gchar *)shstrndx_buf->data + sh_name, shstrndx_buf->len - sh_name); if (name != NULL && name[0] != '\0') fu_firmware_set_id(img, name); } /* success */ return TRUE; } typedef struct { gchar *name; gsize namesz; gsize offset; } FuElfFirmwareStrtabEntry; static void fu_elf_firmware_strtab_entry_free(FuElfFirmwareStrtabEntry *entry) { g_free(entry->name); g_free(entry); } static void fu_elf_firmware_strtab_insert(GPtrArray *strtab, const gchar *name) { FuElfFirmwareStrtabEntry *entry = g_new0(FuElfFirmwareStrtabEntry, 1); gsize offset = 0; g_return_if_fail(name != NULL); /* get the previous entry */ if (strtab->len > 0) { FuElfFirmwareStrtabEntry *entry_old = g_ptr_array_index(strtab, strtab->len - 1); offset += entry_old->offset + entry_old->namesz; } entry->namesz = strlen(name) + 1; /* with NUL */ entry->name = g_strdup(name); entry->offset = offset; g_ptr_array_add(strtab, entry); } static GPtrArray * fu_elf_firmware_strtab_new(void) { g_autoptr(GPtrArray) strtab = g_ptr_array_new_with_free_func((GDestroyNotify)fu_elf_firmware_strtab_entry_free); fu_elf_firmware_strtab_insert(strtab, ""); fu_elf_firmware_strtab_insert(strtab, ".shstrtab"); return g_steal_pointer(&strtab); } static GByteArray * fu_elf_firmware_strtab_write(GPtrArray *strtab) { g_autoptr(GByteArray) buf = g_byte_array_new(); for (guint i = 0; i < strtab->len; i++) { FuElfFirmwareStrtabEntry *entry = g_ptr_array_index(strtab, i); g_byte_array_append(buf, (const guint8 *)entry->name, entry->namesz); } return g_steal_pointer(&buf); } static gsize fu_elf_firmware_strtab_get_offset_for_name(GPtrArray *strtab, const gchar *name) { for (guint i = 0; i < strtab->len; i++) { FuElfFirmwareStrtabEntry *entry = g_ptr_array_index(strtab, i); if (g_strcmp0(entry->name, name) == 0) return entry->offset; } return 0; } static GByteArray * fu_elf_firmware_write(FuFirmware *firmware, GError **error) { const gsize physical_addr = 0x80000000; gsize section_offset = 0; g_autoptr(FuStructElfFileHeader64le) st_filehdr = fu_struct_elf_file_header64le_new(); g_autoptr(FuStructElfProgramHeader64le) st_proghdr = fu_struct_elf_program_header64le_new(); g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GByteArray) section_data = g_byte_array_new(); g_autoptr(GByteArray) section_hdr = g_byte_array_new(); g_autoptr(GByteArray) shstrtab = NULL; g_autoptr(GPtrArray) imgs = NULL; g_autoptr(GPtrArray) strtab = fu_elf_firmware_strtab_new(); /* build the string table: * * \0 * .text\0 * .rodata\0 */ imgs = fu_firmware_get_images(firmware); for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); if (fu_firmware_get_id(img) == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "section 0x%x must have an ID", (guint)fu_firmware_get_idx(img)); return NULL; } fu_elf_firmware_strtab_insert(strtab, fu_firmware_get_id(img)); } shstrtab = fu_elf_firmware_strtab_write(strtab); /* build the section data: * * shstrtab * [img] * [img] * [img] * * NOTE: requires shstrtab to be set */ g_byte_array_append(section_data, shstrtab->data, shstrtab->len); for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); g_autoptr(GBytes) blob = fu_firmware_get_bytes(img, error); if (blob == NULL) return NULL; fu_byte_array_append_bytes(section_data, blob); } /* calculate the offset of each section */ section_offset = st_filehdr->len + st_proghdr->len + shstrtab->len; for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); fu_firmware_set_offset(img, section_offset); section_offset += fu_firmware_get_size(img); } /* build the section header: * 1. empty section header * 2. [image] section headers * 3. shstrtab * * NOTE: requires image offset to be set */ if (imgs->len > 0) { g_autoptr(FuStructElfSectionHeader64le) st_secthdr = fu_struct_elf_section_header64le_new(); g_byte_array_append(section_hdr, st_secthdr->data, st_secthdr->len); } for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); g_autoptr(FuStructElfSectionHeader64le) st_secthdr = fu_struct_elf_section_header64le_new(); gsize strtab_offset = fu_elf_firmware_strtab_get_offset_for_name(strtab, fu_firmware_get_id(img)); fu_struct_elf_section_header64le_set_name(st_secthdr, strtab_offset); fu_struct_elf_section_header64le_set_type(st_secthdr, FU_ELF_SECTION_HEADER_TYPE_PROGBITS); fu_struct_elf_section_header64le_set_flags(st_secthdr, 0x02); fu_struct_elf_section_header64le_set_addr(st_secthdr, physical_addr + fu_firmware_get_offset(img)); fu_struct_elf_section_header64le_set_offset(st_secthdr, fu_firmware_get_offset(img)); fu_struct_elf_section_header64le_set_size(st_secthdr, fu_firmware_get_size(img)); g_byte_array_append(section_hdr, st_secthdr->data, st_secthdr->len); } if (shstrtab->len > 0) { g_autoptr(FuStructElfSectionHeader64le) st_secthdr = fu_struct_elf_section_header64le_new(); fu_struct_elf_section_header64le_set_name(st_secthdr, 0x1); /* we made sure this was first */ fu_struct_elf_section_header64le_set_type(st_secthdr, FU_ELF_SECTION_HEADER_TYPE_STRTAB); fu_struct_elf_section_header64le_set_offset(st_secthdr, st_filehdr->len + st_proghdr->len); fu_struct_elf_section_header64le_set_size(st_secthdr, shstrtab->len); g_byte_array_append(section_hdr, st_secthdr->data, st_secthdr->len); } /* update with the new totals */ fu_struct_elf_file_header64le_set_entry(st_filehdr, physical_addr + 0x60); fu_struct_elf_file_header64le_set_shoff(st_filehdr, st_filehdr->len + st_proghdr->len + section_data->len); fu_struct_elf_file_header64le_set_phentsize(st_filehdr, FU_STRUCT_ELF_PROGRAM_HEADER64LE_SIZE); fu_struct_elf_file_header64le_set_phnum(st_filehdr, 1); fu_struct_elf_file_header64le_set_shentsize(st_filehdr, FU_STRUCT_ELF_SECTION_HEADER64LE_SIZE); fu_struct_elf_file_header64le_set_shnum(st_filehdr, 2 + imgs->len); /* & shstrtab */ fu_struct_elf_file_header64le_set_shstrndx(st_filehdr, imgs->len + 1); fu_struct_elf_program_header64le_set_vaddr(st_proghdr, physical_addr); fu_struct_elf_program_header64le_set_paddr(st_proghdr, physical_addr); fu_struct_elf_program_header64le_set_filesz(st_proghdr, st_filehdr->len + st_proghdr->len + section_data->len + section_hdr->len); fu_struct_elf_program_header64le_set_memsz(st_proghdr, st_filehdr->len + st_proghdr->len + section_data->len + section_hdr->len); /* add file header, sections, then section headers */ g_byte_array_append(buf, st_filehdr->data, st_filehdr->len); g_byte_array_append(buf, st_proghdr->data, st_proghdr->len); g_byte_array_append(buf, section_data->data, section_data->len); g_byte_array_append(buf, section_hdr->data, section_hdr->len); return g_steal_pointer(&buf); } static void fu_elf_firmware_init(FuElfFirmware *self) { fu_firmware_set_images_max(FU_FIRMWARE(self), 1024); } static void fu_elf_firmware_class_init(FuElfFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_elf_firmware_validate; firmware_class->parse = fu_elf_firmware_parse; firmware_class->write = fu_elf_firmware_write; } /** * fu_elf_firmware_new: * * Creates a new #FuElfFirmware * * Since: 1.9.3 **/ FuFirmware * fu_elf_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_ELF_FIRMWARE, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-elf-firmware.h000066400000000000000000000006221501337203100211060ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_ELF_FIRMWARE (fu_elf_firmware_get_type()) G_DECLARE_DERIVABLE_TYPE(FuElfFirmware, fu_elf_firmware, FU, ELF_FIRMWARE, FuFirmware) struct _FuElfFirmwareClass { FuFirmwareClass parent_class; }; FuFirmware * fu_elf_firmware_new(void); fwupd-2.0.10/libfwupdplugin/fu-elf.rs000066400000000000000000000034121501337203100174710ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[repr(u16le)] enum FuElfFileHeaderType { None = 0x00, Rel = 0x01, Exec = 0x02, Dyn = 0x03, Core = 0x04, } #[derive(ParseStream, ValidateStream, New, Default)] #[repr(C, packed)] struct FuStructElfFileHeader64le { ei_magic: [char; 4] == "\x7F\x45\x4C\x46", ei_class: u8 == 0x2, // 64-bit format ei_data: u8 == 0x1, // LE ei_version: u8 == 0x1, ei_osabi: u8 = 0x3, ei_abiversion: u8, _ei_padding: [u8; 7] = 0x00000000000000, type: FuElfFileHeaderType, machine: u16le, version: u32le == 0x1, entry: u64le, phoff: u64le = $struct_size, shoff: u64le, flags: u32le, ehsize: u16le = $struct_size, phentsize: u16le, phnum: u16le, shentsize: u16le, shnum: u16le, shstrndx: u16le, } #[derive(ParseStream, New)] #[repr(C, packed)] struct FuStructElfProgramHeader64le { flags: u32le, offset: u64le, vaddr: u64le, paddr: u64le, filesz: u64le, memsz: u64le, flags2: u32le, align: u64le, } #[repr(u32le)] #[derive(ToString)] enum FuElfSectionHeaderType { Null = 0x0, Progbits = 0x1, Symtab = 0x2, Strtab = 0x3, Rela = 0x4, Hash = 0x5, Dynamic = 0x6, Note = 0x7, Nobits = 0x8, Rel = 0x9, Shlib = 0x0a, Dynsym = 0x0b, InitArray = 0x0e, FiniArray = 0x0f, PreinitArray = 0x10, Group = 0x11, SymtabShndx = 0x12, Num = 0x13, } #[derive(ParseStream, New)] #[repr(C, packed)] struct FuStructElfSectionHeader64le { name: u32le, type: FuElfSectionHeaderType, flags: u64le, addr: u64le, offset: u64le, size: u64le, link: u32le, info: u32le, addralign: u64le, entsize: u64le, } fwupd-2.0.10/libfwupdplugin/fu-endian.h000066400000000000000000000003641501337203100177670ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include /** * FuEndianType: * * The endian type, e.g. %G_LITTLE_ENDIAN **/ typedef guint FuEndianType; fwupd-2.0.10/libfwupdplugin/fu-fdt-firmware.c000066400000000000000000000414321501337203100211140ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-crc.h" #include "fu-dump.h" #include "fu-fdt-firmware.h" #include "fu-fdt-image.h" #include "fu-fdt-struct.h" #include "fu-input-stream.h" #include "fu-mem.h" /** * FuFdtFirmware: * * A Flattened DeviceTree firmware image. * * Documented: * https://devicetree-specification.readthedocs.io/en/latest/chapter5-flattened-format.html * * See also: [class@FuFirmware] */ typedef struct { guint32 cpuid; } FuFdtFirmwarePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuFdtFirmware, fu_fdt_firmware, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_fdt_firmware_get_instance_private(o)) #define FDT_LAST_COMP_VERSION 2 #define FDT_DEPTH_MAX 128 static GString * fu_fdt_firmware_string_new_safe(const guint8 *buf, gsize bufsz, gsize offset, GError **error) { g_autoptr(GString) str = g_string_new(NULL); for (gsize i = offset; i < bufsz; i++) { if (buf[i] == '\0') return g_steal_pointer(&str); g_string_append_c(str, (gchar)buf[i]); } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "buffer not NULL terminated"); return NULL; } static void fu_fdt_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuFdtFirmware *self = FU_FDT_FIRMWARE(firmware); FuFdtFirmwarePrivate *priv = GET_PRIVATE(self); fu_xmlb_builder_insert_kx(bn, "cpuid", priv->cpuid); } /** * fu_fdt_firmware_get_cpuid: * @self: a #FuFdtFirmware * * Gets the CPUID. * * Returns: integer * * Since: 1.8.2 **/ guint32 fu_fdt_firmware_get_cpuid(FuFdtFirmware *self) { FuFdtFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FDT_FIRMWARE(self), 0x0); return priv->cpuid; } /** * fu_fdt_firmware_set_cpuid: * @self: a #FuFdtFirmware * @cpuid: integer value * * Sets the CPUID. * * Since: 1.8.2 **/ void fu_fdt_firmware_set_cpuid(FuFdtFirmware *self, guint32 cpuid) { FuFdtFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_FDT_FIRMWARE(self)); priv->cpuid = cpuid; } /** * fu_fdt_firmware_get_image_by_path: * @self: a #FuFdtFirmware * @path: ID path, e.g. `/images/firmware-1` * @error: (nullable): optional return location for an error * * Gets the FDT image for a specific path. * * Returns: (transfer full): a #FuFirmware, or %NULL * * Since: 1.8.2 **/ FuFdtImage * fu_fdt_firmware_get_image_by_path(FuFdtFirmware *self, const gchar *path, GError **error) { g_auto(GStrv) paths = NULL; g_autoptr(FuFirmware) img_current = g_object_ref(FU_FIRMWARE(self)); g_return_val_if_fail(FU_IS_FDT_FIRMWARE(self), NULL); g_return_val_if_fail(path != NULL, NULL); g_return_val_if_fail(path[0] != '\0', NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); paths = g_strsplit(path, "/", -1); for (guint i = 0; paths[i] != NULL; i++) { const gchar *id = paths[i]; g_autoptr(FuFirmware) img_tmp = NULL; /* special case for empty */ if (id[0] == '\0') id = NULL; img_tmp = fu_firmware_get_image_by_id(img_current, id, error); if (img_tmp == NULL) return NULL; g_set_object(&img_current, img_tmp); } /* success */ return FU_FDT_IMAGE(g_steal_pointer(&img_current)); } static gboolean fu_fdt_firmware_parse_dt_struct(FuFdtFirmware *self, GBytes *fw, GByteArray *strtab, GError **error) { gsize bufsz = 0; gsize offset = 0; guint depth = 0; gboolean has_end = FALSE; const guint8 *buf = g_bytes_get_data(fw, &bufsz); g_autoptr(FuFirmware) firmware_current = g_object_ref(FU_FIRMWARE(self)); /* debug */ fu_dump_bytes(G_LOG_DOMAIN, "dt_struct", fw); /* parse */ while (offset < bufsz) { guint32 token = 0; /* read tag from aligned offset */ offset = fu_common_align_up(offset, FU_FIRMWARE_ALIGNMENT_4); if (!fu_memread_uint32_safe(buf, bufsz, offset, &token, G_BIG_ENDIAN, error)) return FALSE; offset += sizeof(guint32); /* nothing to do */ if (token == FU_FDT_TOKEN_NOP) continue; /* END */ if (token == FU_FDT_TOKEN_END) { if (firmware_current != FU_FIRMWARE(self)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "got END with unclosed node"); return FALSE; } has_end = TRUE; break; } /* BEGIN NODE */ if (token == FU_FDT_TOKEN_BEGIN_NODE) { g_autoptr(GString) str = NULL; g_autoptr(FuFirmware) image = NULL; /* sanity check */ if (depth++ > FDT_DEPTH_MAX) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "node depth exceeded maximum: 0x%x", (guint)FDT_DEPTH_MAX); return FALSE; } str = fu_fdt_firmware_string_new_safe(buf, bufsz, offset, error); if (str == NULL) return FALSE; offset += str->len + 1; image = fu_fdt_image_new(); if (str->len > 0) fu_firmware_set_id(image, str->str); fu_firmware_set_offset(image, offset); if (!fu_firmware_add_image_full(firmware_current, image, error)) return FALSE; g_set_object(&firmware_current, image); continue; } /* END NODE */ if (token == FU_FDT_TOKEN_END_NODE) { if (firmware_current == FU_FIRMWARE(self)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "got END NODE with no node to end"); return FALSE; } g_set_object(&firmware_current, fu_firmware_get_parent(firmware_current)); if (depth > 0) depth--; continue; } /* PROP */ if (token == FU_FDT_TOKEN_PROP) { guint32 prop_len; guint32 prop_nameoff; g_autoptr(GBytes) blob = NULL; g_autoptr(GString) str = NULL; g_autoptr(GByteArray) st_prp = NULL; /* sanity check */ if (firmware_current == FU_FIRMWARE(self)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "got PROP with unopen node"); return FALSE; } /* parse */ st_prp = fu_struct_fdt_prop_parse(buf, bufsz, offset, error); if (st_prp == NULL) return FALSE; prop_len = fu_struct_fdt_prop_get_len(st_prp); prop_nameoff = fu_struct_fdt_prop_get_nameoff(st_prp); offset += st_prp->len; /* add property */ str = fu_fdt_firmware_string_new_safe(strtab->data, strtab->len, prop_nameoff, error); if (str == NULL) { g_prefix_error(error, "invalid strtab offset 0x%x: ", prop_nameoff); return FALSE; } blob = fu_bytes_new_offset(fw, offset, prop_len, error); if (blob == NULL) return FALSE; fu_fdt_image_set_attr(FU_FDT_IMAGE(firmware_current), str->str, blob); offset += prop_len; continue; } /* unknown token */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid token 0x%x @0%x", token, (guint)offset); return FALSE; } /* did not see FDT_END */ if (!has_end) g_warning("did not see FDT_END, perhaps size_dt_struct is invalid?"); /* success */ return TRUE; } static gboolean fu_fdt_firmware_parse_mem_rsvmap(FuFdtFirmware *self, GInputStream *stream, gsize offset, GError **error) { gsize streamsz = 0; /* parse */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; for (; offset < streamsz; offset += FU_STRUCT_FDT_RESERVE_ENTRY_SIZE) { guint64 address = 0; guint64 size = 0; g_autoptr(GByteArray) st_res = NULL; st_res = fu_struct_fdt_reserve_entry_parse_stream(stream, offset, error); if (st_res == NULL) return FALSE; address = fu_struct_fdt_reserve_entry_get_address(st_res); size = fu_struct_fdt_reserve_entry_get_size(st_res); g_debug("mem_rsvmap: 0x%x, 0x%x", (guint)address, (guint)size); if (address == 0x0 && size == 0x0) break; } /* success */ return TRUE; } static gboolean fu_fdt_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_fdt_validate_stream(stream, offset, error); } static gboolean fu_fdt_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuFdtFirmware *self = FU_FDT_FIRMWARE(firmware); FuFdtFirmwarePrivate *priv = GET_PRIVATE(self); guint32 totalsize; gsize streamsz = 0; guint32 off_mem_rsvmap = 0; g_autoptr(GByteArray) st_hdr = NULL; /* sanity check */ st_hdr = fu_struct_fdt_parse_stream(stream, 0x0, error); if (st_hdr == NULL) return FALSE; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; totalsize = fu_struct_fdt_get_totalsize(st_hdr); if (totalsize > streamsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "truncated image, got 0x%x, expected >= 0x%x", (guint)streamsz, (guint)totalsize); return FALSE; } fu_firmware_set_size(firmware, totalsize); /* read header */ priv->cpuid = fu_struct_fdt_get_boot_cpuid_phys(st_hdr); off_mem_rsvmap = fu_struct_fdt_get_off_mem_rsvmap(st_hdr); if (off_mem_rsvmap != 0x0) { if (!fu_fdt_firmware_parse_mem_rsvmap(self, stream, off_mem_rsvmap, error)) return FALSE; } if (fu_struct_fdt_get_last_comp_version(st_hdr) < FDT_LAST_COMP_VERSION) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid header version, got 0x%x, expected >= 0x%x", (guint)fu_struct_fdt_get_last_comp_version(st_hdr), (guint)FDT_LAST_COMP_VERSION); return FALSE; } fu_firmware_set_version_raw(firmware, fu_struct_fdt_get_version(st_hdr)); /* parse device tree struct */ if (fu_struct_fdt_get_size_dt_struct(st_hdr) != 0x0 && fu_struct_fdt_get_size_dt_strings(st_hdr) != 0x0) { g_autoptr(GByteArray) dt_strings = NULL; g_autoptr(GByteArray) dt_struct = NULL; g_autoptr(GBytes) dt_struct_buf = NULL; dt_strings = fu_input_stream_read_byte_array(stream, fu_struct_fdt_get_off_dt_strings(st_hdr), fu_struct_fdt_get_size_dt_strings(st_hdr), NULL, error); if (dt_strings == NULL) return FALSE; dt_struct = fu_input_stream_read_byte_array(stream, fu_struct_fdt_get_off_dt_struct(st_hdr), fu_struct_fdt_get_size_dt_struct(st_hdr), NULL, error); if (dt_struct == NULL) return FALSE; if (dt_struct->len != fu_struct_fdt_get_size_dt_struct(st_hdr)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid firmware -- dt_struct invalid"); return FALSE; } dt_struct_buf = g_byte_array_free_to_bytes(g_steal_pointer(&dt_struct)); /* nocheck:blocked */ if (!fu_fdt_firmware_parse_dt_struct(self, dt_struct_buf, dt_strings, error)) return FALSE; } /* success */ return TRUE; } typedef struct { GByteArray *dt_strings; GByteArray *dt_struct; GHashTable *strtab; } FuFdtFirmwareBuildHelper; static guint32 fu_fdt_firmware_append_to_strtab(FuFdtFirmwareBuildHelper *helper, const gchar *key) { gpointer tmp = NULL; guint32 offset; /* already exists */ if (g_hash_table_lookup_extended(helper->strtab, key, NULL, &tmp)) return GPOINTER_TO_UINT(tmp); g_debug("adding strtab: %s", key); offset = helper->dt_strings->len; g_byte_array_append(helper->dt_strings, (const guint8 *)key, strlen(key)); fu_byte_array_append_uint8(helper->dt_strings, 0x0); g_hash_table_insert(helper->strtab, g_strdup(key), GUINT_TO_POINTER(offset)); return offset; } static gboolean fu_fdt_firmware_write_image(FuFdtFirmware *self, FuFdtImage *img, FuFdtFirmwareBuildHelper *helper, guint depth, GError **error) { const gchar *id = fu_firmware_get_id(FU_FIRMWARE(img)); g_autoptr(GPtrArray) images = fu_firmware_get_images(FU_FIRMWARE(img)); g_autoptr(GPtrArray) attrs = fu_fdt_image_get_attrs(img); /* sanity check */ if (depth > 0 && id == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "child FuFdtImage requires ID"); return FALSE; } /* BEGIN_NODE, ID, NUL */ fu_byte_array_append_uint32(helper->dt_struct, FU_FDT_TOKEN_BEGIN_NODE, G_BIG_ENDIAN); if (id != NULL) { g_byte_array_append(helper->dt_struct, (const guint8 *)id, strlen(id) + 1); } else { fu_byte_array_append_uint8(helper->dt_struct, 0x0); } fu_byte_array_align_up(helper->dt_struct, FU_FIRMWARE_ALIGNMENT_4, 0x0); /* write properties */ for (guint i = 0; i < attrs->len; i++) { const gchar *key = g_ptr_array_index(attrs, i); g_autoptr(GBytes) blob = NULL; g_autoptr(GByteArray) st_prp = fu_struct_fdt_prop_new(); blob = fu_fdt_image_get_attr(img, key, error); if (blob == NULL) return FALSE; fu_byte_array_append_uint32(helper->dt_struct, FU_FDT_TOKEN_PROP, G_BIG_ENDIAN); fu_struct_fdt_prop_set_len(st_prp, g_bytes_get_size(blob)); fu_struct_fdt_prop_set_nameoff(st_prp, fu_fdt_firmware_append_to_strtab(helper, key)); g_byte_array_append(helper->dt_struct, st_prp->data, st_prp->len); fu_byte_array_append_bytes(helper->dt_struct, blob); fu_byte_array_align_up(helper->dt_struct, FU_FIRMWARE_ALIGNMENT_4, 0x0); } /* write children, recursively */ for (guint i = 0; i < images->len; i++) { FuFdtImage *img_child = g_ptr_array_index(images, i); if (!fu_fdt_firmware_write_image(self, img_child, helper, depth + 1, error)) return FALSE; } /* END_NODE */ fu_byte_array_append_uint32(helper->dt_struct, FU_FDT_TOKEN_END_NODE, G_BIG_ENDIAN); return TRUE; } static GByteArray * fu_fdt_firmware_write(FuFirmware *firmware, GError **error) { FuFdtFirmware *self = FU_FDT_FIRMWARE(firmware); FuFdtFirmwarePrivate *priv = GET_PRIVATE(self); guint32 off_dt_struct; guint32 off_dt_strings; guint32 off_mem_rsvmap; g_autoptr(GByteArray) dt_strings = g_byte_array_new(); g_autoptr(GByteArray) dt_struct = g_byte_array_new(); g_autoptr(GHashTable) strtab = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); g_autoptr(GPtrArray) images = fu_firmware_get_images(firmware); g_autoptr(GByteArray) st_hdr = fu_struct_fdt_new(); g_autoptr(GByteArray) mem_rsvmap = fu_struct_fdt_reserve_entry_new(); FuFdtFirmwareBuildHelper helper = { .dt_strings = dt_strings, .dt_struct = dt_struct, .strtab = strtab, }; /* empty mem_rsvmap */ off_mem_rsvmap = fu_common_align_up(st_hdr->len, FU_FIRMWARE_ALIGNMENT_4); /* dt_struct */ off_dt_struct = fu_common_align_up(off_mem_rsvmap + mem_rsvmap->len, FU_FIRMWARE_ALIGNMENT_4); /* only one root node supported */ if (images->len != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no root node"); return NULL; } if (!fu_fdt_firmware_write_image(self, FU_FDT_IMAGE(g_ptr_array_index(images, 0)), &helper, 0, error)) return NULL; fu_byte_array_append_uint32(dt_struct, FU_FDT_TOKEN_END, G_BIG_ENDIAN); /* dt_strings */ off_dt_strings = fu_common_align_up(off_dt_struct + dt_struct->len, FU_FIRMWARE_ALIGNMENT_4); /* write header */ fu_struct_fdt_set_totalsize(st_hdr, off_dt_strings + dt_strings->len); fu_struct_fdt_set_off_dt_struct(st_hdr, off_dt_struct); fu_struct_fdt_set_off_dt_strings(st_hdr, off_dt_strings); fu_struct_fdt_set_off_mem_rsvmap(st_hdr, off_mem_rsvmap); fu_struct_fdt_set_version(st_hdr, fu_firmware_get_version_raw(firmware)); fu_struct_fdt_set_boot_cpuid_phys(st_hdr, priv->cpuid); fu_struct_fdt_set_size_dt_strings(st_hdr, dt_strings->len); fu_struct_fdt_set_size_dt_struct(st_hdr, dt_struct->len); fu_byte_array_align_up(st_hdr, FU_FIRMWARE_ALIGNMENT_4, 0x0); /* write mem_rsvmap, dt_struct, dt_strings */ g_byte_array_append(st_hdr, mem_rsvmap->data, mem_rsvmap->len); fu_byte_array_align_up(st_hdr, FU_FIRMWARE_ALIGNMENT_4, 0x0); g_byte_array_append(st_hdr, dt_struct->data, dt_struct->len); fu_byte_array_align_up(st_hdr, FU_FIRMWARE_ALIGNMENT_4, 0x0); g_byte_array_append(st_hdr, dt_strings->data, dt_strings->len); fu_byte_array_align_up(st_hdr, FU_FIRMWARE_ALIGNMENT_4, 0x0); /* success */ return g_steal_pointer(&st_hdr); } static gboolean fu_fdt_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { FuFdtFirmware *self = FU_FDT_FIRMWARE(firmware); FuFdtFirmwarePrivate *priv = GET_PRIVATE(self); guint64 tmp; /* optional properties */ tmp = xb_node_query_text_as_uint(n, "cpuid", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT32) priv->cpuid = tmp; /* success */ return TRUE; } static void fu_fdt_firmware_init(FuFdtFirmware *self) { g_type_ensure(FU_TYPE_FDT_IMAGE); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_VID_PID); } static void fu_fdt_firmware_class_init(FuFdtFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_fdt_firmware_validate; firmware_class->export = fu_fdt_firmware_export; firmware_class->parse = fu_fdt_firmware_parse; firmware_class->write = fu_fdt_firmware_write; firmware_class->build = fu_fdt_firmware_build; } /** * fu_fdt_firmware_new: * * Creates a new #FuFirmware of sub type FDT * * Since: 1.8.2 **/ FuFirmware * fu_fdt_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_FDT_FIRMWARE, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-fdt-firmware.h000066400000000000000000000013151501337203100211150ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-fdt-image.h" #include "fu-firmware.h" #define FU_TYPE_FDT_FIRMWARE (fu_fdt_firmware_get_type()) G_DECLARE_DERIVABLE_TYPE(FuFdtFirmware, fu_fdt_firmware, FU, FDT_FIRMWARE, FuFirmware) struct _FuFdtFirmwareClass { FuFirmwareClass parent_class; }; FuFirmware * fu_fdt_firmware_new(void); guint32 fu_fdt_firmware_get_cpuid(FuFdtFirmware *self) G_GNUC_NON_NULL(1); void fu_fdt_firmware_set_cpuid(FuFdtFirmware *self, guint32 cpuid) G_GNUC_NON_NULL(1); FuFdtImage * fu_fdt_firmware_get_image_by_path(FuFdtFirmware *self, const gchar *path, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-fdt-image.c000066400000000000000000000430651501337203100203660ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include "fu-byte-array.h" #include "fu-common.h" #include "fu-fdt-image.h" #include "fu-mem.h" #include "fu-string.h" /** * FuFdtImage: * * A Flattened DeviceTree firmware image. * * See also: [class@FuFdtFirmware] */ typedef struct { GHashTable *hash_attrs; GHashTable *hash_attrs_format; } FuFdtImagePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuFdtImage, fu_fdt_image, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_fdt_image_get_instance_private(o)) #define FU_FDT_IMAGE_FORMAT_STR "str" #define FU_FDT_IMAGE_FORMAT_STRLIST "strlist" #define FU_FDT_IMAGE_FORMAT_UINT32 "uint32" #define FU_FDT_IMAGE_FORMAT_UINT64 "uint64" #define FU_FDT_IMAGE_FORMAT_DATA "data" static const gchar * fu_fdt_image_guess_format_from_key(const gchar *key) { struct { const gchar *key; const gchar *format; } key_format_map[] = {{"#address-cells", FU_FDT_IMAGE_FORMAT_UINT32}, {"algo", FU_FDT_IMAGE_FORMAT_STR}, {"arch", FU_FDT_IMAGE_FORMAT_STR}, {"compatible", FU_FDT_IMAGE_FORMAT_STRLIST}, {"compression", FU_FDT_IMAGE_FORMAT_STR}, {"creator", FU_FDT_IMAGE_FORMAT_STR}, {"data-offset", FU_FDT_IMAGE_FORMAT_UINT32}, {"data-size", FU_FDT_IMAGE_FORMAT_UINT32}, {"default", FU_FDT_IMAGE_FORMAT_STR}, {"description", FU_FDT_IMAGE_FORMAT_STR}, {"entry", FU_FDT_IMAGE_FORMAT_STR}, {"firmware", FU_FDT_IMAGE_FORMAT_STR}, {"load", FU_FDT_IMAGE_FORMAT_UINT32}, {"os", FU_FDT_IMAGE_FORMAT_STR}, {"timestamp", FU_FDT_IMAGE_FORMAT_UINT32}, {"type", FU_FDT_IMAGE_FORMAT_STR}, {"version", FU_FDT_IMAGE_FORMAT_STR}, {NULL, NULL}}; for (guint i = 0; key_format_map[i].key != NULL; i++) { if (g_strcmp0(key, key_format_map[i].key) == 0) return key_format_map[i].format; } return NULL; } static gchar ** fu_fdt_image_strlist_from_blob(GBytes *blob) { gchar **val; gsize bufsz = 0; const guint8 *buf = g_bytes_get_data(blob, &bufsz); g_autoptr(GPtrArray) strs = g_ptr_array_new(); /* delimit by NUL */ for (gsize i = 0; i < bufsz; i++) { const gchar *tmp = (const gchar *)buf + i; g_ptr_array_add(strs, (gpointer)tmp); i += strlen(tmp); } /* copy to GStrv */ val = g_new0(gchar *, strs->len + 1); for (guint i = 0; i < strs->len; i++) val[i] = g_strdup(g_ptr_array_index(strs, i)); return val; } static void fu_fdt_image_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuFdtImage *self = FU_FDT_IMAGE(firmware); FuFdtImagePrivate *priv = GET_PRIVATE(self); GHashTableIter iter; gpointer key, value; g_hash_table_iter_init(&iter, priv->hash_attrs); while (g_hash_table_iter_next(&iter, &key, &value)) { gsize bufsz = 0; const guint8 *buf = g_bytes_get_data(value, &bufsz); const gchar *format = g_hash_table_lookup(priv->hash_attrs_format, key); g_autofree gchar *str = NULL; g_autoptr(XbBuilderNode) bc = NULL; /* guess format based on key name to improve debugging experience */ if (format == NULL) format = fu_fdt_image_guess_format_from_key(key); if (g_strcmp0(format, FU_FDT_IMAGE_FORMAT_UINT32) == 0 && bufsz == 4) { guint64 tmp = fu_memread_uint32(buf, G_BIG_ENDIAN); str = g_strdup_printf("0x%x", (guint)tmp); } else if (g_strcmp0(format, FU_FDT_IMAGE_FORMAT_UINT64) == 0 && bufsz == 8) { guint64 tmp = fu_memread_uint64(buf, G_BIG_ENDIAN); str = g_strdup_printf("0x%x", (guint)tmp); } else if (g_strcmp0(format, FU_FDT_IMAGE_FORMAT_STR) == 0 && bufsz > 0) { str = g_strndup((const gchar *)buf, bufsz); } else if (g_strcmp0(format, FU_FDT_IMAGE_FORMAT_STRLIST) == 0 && bufsz > 0) { g_auto(GStrv) tmp = fu_fdt_image_strlist_from_blob(value); str = g_strjoinv(":", tmp); } else { str = g_base64_encode(buf, bufsz); } bc = xb_builder_node_insert(bn, "metadata", "key", key, NULL); if (str != NULL) xb_builder_node_set_text(bc, str, -1); if (format != NULL) xb_builder_node_set_attr(bc, "format", format); } } /** * fu_fdt_image_get_attrs: * @self: a #FuFdtImage * * Gets all the attributes stored on the image. * * Returns: (transfer container) (element-type utf8): keys * * Since: 1.8.2 **/ GPtrArray * fu_fdt_image_get_attrs(FuFdtImage *self) { FuFdtImagePrivate *priv = GET_PRIVATE(self); GPtrArray *array = g_ptr_array_new_with_free_func(g_free); g_autoptr(GList) keys = NULL; g_return_val_if_fail(FU_IS_FDT_IMAGE(self), NULL); keys = g_hash_table_get_keys(priv->hash_attrs); for (GList *l = keys; l != NULL; l = l->next) { const gchar *key = l->data; g_ptr_array_add(array, g_strdup(key)); } return array; } /** * fu_fdt_image_get_attr: * @self: a #FuFdtImage * @key: string, e.g. `creator` * @error: (nullable): optional return location for an error * * Gets a attribute from the image. * * Returns: (transfer full): blob * * Since: 1.8.2 **/ GBytes * fu_fdt_image_get_attr(FuFdtImage *self, const gchar *key, GError **error) { FuFdtImagePrivate *priv = GET_PRIVATE(self); GBytes *blob; g_return_val_if_fail(FU_IS_FDT_IMAGE(self), NULL); g_return_val_if_fail(key != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); blob = g_hash_table_lookup(priv->hash_attrs, key); if (blob == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no data for %s", key); return NULL; } /* success */ return g_bytes_ref(blob); } /** * fu_fdt_image_get_attr_u32: * @self: a #FuFdtImage * @key: string, e.g. `creator` * @val: (out) (nullable): value * @error: (nullable): optional return location for an error * * Gets a uint32 attribute from the image. * * Returns: %TRUE if @val was set. * * Since: 1.8.2 **/ gboolean fu_fdt_image_get_attr_u32(FuFdtImage *self, const gchar *key, guint32 *val, GError **error) { g_autoptr(GBytes) blob = NULL; g_return_val_if_fail(FU_IS_FDT_IMAGE(self), FALSE); g_return_val_if_fail(key != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); blob = fu_fdt_image_get_attr(self, key, error); if (blob == NULL) return FALSE; if (g_bytes_get_size(blob) != sizeof(guint32)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid data size for %s, got 0x%x, expected 0x%x", key, (guint)g_bytes_get_size(blob), (guint)sizeof(guint32)); return FALSE; } if (val != NULL) *val = fu_memread_uint32(g_bytes_get_data(blob, NULL), G_BIG_ENDIAN); return TRUE; } /** * fu_fdt_image_get_attr_u64: * @self: a #FuFdtImage * @key: string, e.g. `creator` * @val: (out) (nullable): value * @error: (nullable): optional return location for an error * * Gets a uint64 attribute from the image. * * Returns: %TRUE if @val was set. * * Since: 1.8.2 **/ gboolean fu_fdt_image_get_attr_u64(FuFdtImage *self, const gchar *key, guint64 *val, GError **error) { g_autoptr(GBytes) blob = NULL; g_return_val_if_fail(FU_IS_FDT_IMAGE(self), FALSE); g_return_val_if_fail(key != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); blob = fu_fdt_image_get_attr(self, key, error); if (blob == NULL) return FALSE; if (g_bytes_get_size(blob) != sizeof(guint64)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid data size for %s, got 0x%x, expected 0x%x", key, (guint)g_bytes_get_size(blob), (guint)sizeof(guint64)); return FALSE; } if (val != NULL) *val = fu_memread_uint64(g_bytes_get_data(blob, NULL), G_BIG_ENDIAN); return TRUE; } /** * fu_fdt_image_get_attr_strlist: * @self: a #FuFdtImage * @key: string, e.g. `compatible` * @val: (out) (nullable) (transfer full): values * @error: (nullable): optional return location for an error * * Gets a stringlist attribute from the image. @val is always `NUL` terminated. * * Returns: %TRUE if @val was set. * * Since: 1.8.2 **/ gboolean fu_fdt_image_get_attr_strlist(FuFdtImage *self, const gchar *key, gchar ***val, GError **error) { g_autoptr(GBytes) blob = NULL; const guint8 *buf; gsize bufsz = 0; g_return_val_if_fail(FU_IS_FDT_IMAGE(self), FALSE); g_return_val_if_fail(key != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); blob = fu_fdt_image_get_attr(self, key, error); if (blob == NULL) return FALSE; if (g_bytes_get_size(blob) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid data size for %s, got 0x%x", key, (guint)g_bytes_get_size(blob)); return FALSE; } /* sanity check */ buf = g_bytes_get_data(blob, &bufsz); for (gsize i = 0; i < bufsz; i++) { if (buf[i] != 0x0 && !g_ascii_isprint((gchar)buf[i])) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "nonprintable character 0x%02x at offset 0x%x in %s", buf[i], (guint)i, key); return FALSE; } } /* success */ if (val != NULL) *val = fu_fdt_image_strlist_from_blob(blob); return TRUE; } /** * fu_fdt_image_get_attr_str: * @self: a #FuFdtImage * @key: string, e.g. `creator` * @val: (out) (nullable) (transfer full): value * @error: (nullable): optional return location for an error * * Gets a string attribute from the image. @val is always `NUL` terminated. * * Returns: %TRUE if @val was set. * * Since: 1.8.2 **/ gboolean fu_fdt_image_get_attr_str(FuFdtImage *self, const gchar *key, gchar **val, GError **error) { g_autoptr(GBytes) blob = NULL; const guint8 *buf; gsize bufsz = 0; g_return_val_if_fail(FU_IS_FDT_IMAGE(self), FALSE); g_return_val_if_fail(key != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); blob = fu_fdt_image_get_attr(self, key, error); if (blob == NULL) return FALSE; if (g_bytes_get_size(blob) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid data size for %s, got 0x%x", key, (guint)g_bytes_get_size(blob)); return FALSE; } /* sanity check */ buf = g_bytes_get_data(blob, &bufsz); for (gsize i = 0; i < bufsz; i++) { if (buf[i] != 0x0 && !g_ascii_isprint((gchar)buf[i])) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "nonprintable character 0x%02x at offset 0x%x in %s", buf[i], (guint)i, key); return FALSE; } } /* success */ if (val != NULL) *val = g_strndup(g_bytes_get_data(blob, NULL), g_bytes_get_size(blob)); return TRUE; } /** * fu_fdt_image_set_attr: * @self: a #FuFdtImage * @key: string, e.g. `creator` * @blob: a #GBytes * * Sets a attribute for the image. * * Since: 1.8.2 **/ void fu_fdt_image_set_attr(FuFdtImage *self, const gchar *key, GBytes *blob) { FuFdtImagePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_FDT_IMAGE(self)); g_return_if_fail(key != NULL); g_hash_table_insert(priv->hash_attrs, g_strdup(key), g_bytes_ref(blob)); } static void fu_fdt_image_set_attr_format(FuFdtImage *self, const gchar *key, const gchar *format) { FuFdtImagePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_FDT_IMAGE(self)); g_return_if_fail(format != NULL); g_hash_table_insert(priv->hash_attrs_format, g_strdup(key), strdup(format)); } /** * fu_fdt_image_set_attr_uint32: * @self: a #FuFdtImage * @key: string, e.g. `creator` * @value: value to store * * Sets a uint32 attribute for the image. * * Since: 1.8.2 **/ void fu_fdt_image_set_attr_uint32(FuFdtImage *self, const gchar *key, guint32 value) { guint8 buf[4] = {0x0}; g_autoptr(GBytes) blob = NULL; g_return_if_fail(FU_IS_FDT_IMAGE(self)); g_return_if_fail(key != NULL); fu_memwrite_uint32(buf, value, G_BIG_ENDIAN); blob = g_bytes_new(buf, sizeof(buf)); fu_fdt_image_set_attr(self, key, blob); fu_fdt_image_set_attr_format(self, key, FU_FDT_IMAGE_FORMAT_UINT32); } /** * fu_fdt_image_set_attr_uint64: * @self: a #FuFdtImage * @key: string, e.g. `creator` * @value: value to store * * Sets a uint64 attribute for the image. * * Since: 1.8.2 **/ void fu_fdt_image_set_attr_uint64(FuFdtImage *self, const gchar *key, guint64 value) { guint8 buf[8] = {0x0}; g_autoptr(GBytes) blob = NULL; g_return_if_fail(FU_IS_FDT_IMAGE(self)); g_return_if_fail(key != NULL); fu_memwrite_uint64(buf, value, G_BIG_ENDIAN); blob = g_bytes_new(buf, sizeof(buf)); fu_fdt_image_set_attr(self, key, blob); fu_fdt_image_set_attr_format(self, key, FU_FDT_IMAGE_FORMAT_UINT64); } /** * fu_fdt_image_set_attr_str: * @self: a #FuFdtImage * @key: string, e.g. `creator` * @value: value to store * * Sets a string attribute for the image. * * Since: 1.8.2 **/ void fu_fdt_image_set_attr_str(FuFdtImage *self, const gchar *key, const gchar *value) { g_autoptr(GBytes) blob = NULL; g_return_if_fail(FU_IS_FDT_IMAGE(self)); g_return_if_fail(key != NULL); g_return_if_fail(value != NULL); blob = g_bytes_new((const guint8 *)value, strlen(value) + 1); fu_fdt_image_set_attr(self, key, blob); fu_fdt_image_set_attr_format(self, key, FU_FDT_IMAGE_FORMAT_STR); } /** * fu_fdt_image_set_attr_strlist: * @self: a #FuFdtImage * @key: string, e.g. `compatible` * @value: values to store * * Sets a stringlist attribute for the image. * * Since: 1.8.2 **/ void fu_fdt_image_set_attr_strlist(FuFdtImage *self, const gchar *key, gchar **value) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GBytes) blob = NULL; g_return_if_fail(FU_IS_FDT_IMAGE(self)); g_return_if_fail(key != NULL); g_return_if_fail(value != NULL); g_return_if_fail(value[0] != NULL); for (guint i = 0; value[i] != NULL; i++) { g_byte_array_append(buf, (const guint8 *)value[i], strlen(value[i])); fu_byte_array_append_uint8(buf, 0x0); } blob = g_bytes_new(buf->data, buf->len); fu_fdt_image_set_attr(self, key, blob); fu_fdt_image_set_attr_format(self, key, FU_FDT_IMAGE_FORMAT_STRLIST); } static gboolean fu_fdt_image_build_metadata_node(FuFdtImage *self, XbNode *n, GError **error) { const gchar *key; const gchar *format; const gchar *value = xb_node_get_text(n); key = xb_node_get_attr(n, "key"); if (key == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "key invalid"); return FALSE; } format = xb_node_get_attr(n, "format"); if (format == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "format unspecified for %s, expected uint64|uint32|str|strlist|data", key); return FALSE; } fu_fdt_image_set_attr_format(self, key, format); /* actually parse values */ if (g_strcmp0(format, FU_FDT_IMAGE_FORMAT_UINT32) == 0) { guint64 tmp = 0; if (value != NULL) { if (!fu_strtoull(value, &tmp, 0x0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; } fu_fdt_image_set_attr_uint32(self, key, tmp); return TRUE; } if (g_strcmp0(format, FU_FDT_IMAGE_FORMAT_UINT64) == 0) { guint64 tmp = 0; if (value != NULL) { if (!fu_strtoull(value, &tmp, 0x0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error)) return FALSE; } fu_fdt_image_set_attr_uint64(self, key, tmp); return TRUE; } if (g_strcmp0(format, FU_FDT_IMAGE_FORMAT_STR) == 0) { if (value != NULL) { fu_fdt_image_set_attr_str(self, key, value); } else { g_autoptr(GBytes) blob = g_bytes_new(NULL, 0); fu_fdt_image_set_attr(self, key, blob); } return TRUE; } if (g_strcmp0(format, FU_FDT_IMAGE_FORMAT_STRLIST) == 0) { if (value != NULL) { g_auto(GStrv) split = g_strsplit(value, ":", -1); fu_fdt_image_set_attr_strlist(self, key, split); } else { g_autoptr(GBytes) blob = g_bytes_new(NULL, 0); fu_fdt_image_set_attr(self, key, blob); } return TRUE; } if (g_strcmp0(format, FU_FDT_IMAGE_FORMAT_DATA) == 0) { g_autoptr(GBytes) blob = NULL; if (value != NULL) { gsize bufsz = 0; g_autofree guchar *buf = g_base64_decode(value, &bufsz); blob = g_bytes_new(buf, bufsz); } else { blob = g_bytes_new(NULL, 0); } fu_fdt_image_set_attr(self, key, blob); return TRUE; } /* failed */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "format for %s invalid, expected uint64|uint32|str|strlist|data", key); return FALSE; } static gboolean fu_fdt_image_build(FuFirmware *firmware, XbNode *n, GError **error) { FuFdtImage *self = FU_FDT_IMAGE(firmware); g_autoptr(GPtrArray) metadata = NULL; metadata = xb_node_query(n, "metadata", 0, NULL); if (metadata != NULL) { for (guint i = 0; i < metadata->len; i++) { XbNode *c = g_ptr_array_index(metadata, i); if (!fu_fdt_image_build_metadata_node(self, c, error)) return FALSE; } } /* success */ return TRUE; } static void fu_fdt_image_init(FuFdtImage *self) { FuFdtImagePrivate *priv = GET_PRIVATE(self); priv->hash_attrs = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_bytes_unref); priv->hash_attrs_format = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); fu_firmware_set_images_max(FU_FIRMWARE(self), 10000); } static void fu_fdt_image_finalize(GObject *object) { FuFdtImage *self = FU_FDT_IMAGE(object); FuFdtImagePrivate *priv = GET_PRIVATE(self); g_hash_table_unref(priv->hash_attrs); g_hash_table_unref(priv->hash_attrs_format); G_OBJECT_CLASS(fu_fdt_image_parent_class)->finalize(object); } static void fu_fdt_image_class_init(FuFdtImageClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_fdt_image_finalize; firmware_class->export = fu_fdt_image_export; firmware_class->build = fu_fdt_image_build; } /** * fu_fdt_image_new: * * Creates a new #FuFirmware of sub type FDT image * * Since: 1.8.2 **/ FuFirmware * fu_fdt_image_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_FDT_IMAGE, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-fdt-image.h000066400000000000000000000031331501337203100203630ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_FDT_IMAGE (fu_fdt_image_get_type()) G_DECLARE_DERIVABLE_TYPE(FuFdtImage, fu_fdt_image, FU, FDT_IMAGE, FuFirmware) struct _FuFdtImageClass { FuFirmwareClass parent_class; }; FuFirmware * fu_fdt_image_new(void); GBytes * fu_fdt_image_get_attr(FuFdtImage *self, const gchar *key, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_fdt_image_get_attr_u32(FuFdtImage *self, const gchar *key, guint32 *val, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_fdt_image_get_attr_u64(FuFdtImage *self, const gchar *key, guint64 *val, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_fdt_image_get_attr_str(FuFdtImage *self, const gchar *key, gchar **val, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_fdt_image_get_attr_strlist(FuFdtImage *self, const gchar *key, gchar ***val, GError **error) G_GNUC_NON_NULL(1, 2); void fu_fdt_image_set_attr(FuFdtImage *self, const gchar *key, GBytes *blob) G_GNUC_NON_NULL(1, 2); void fu_fdt_image_set_attr_uint32(FuFdtImage *self, const gchar *key, guint32 value) G_GNUC_NON_NULL(1, 2); void fu_fdt_image_set_attr_uint64(FuFdtImage *self, const gchar *key, guint64 value) G_GNUC_NON_NULL(1, 2); void fu_fdt_image_set_attr_str(FuFdtImage *self, const gchar *key, const gchar *value) G_GNUC_NON_NULL(1, 2); void fu_fdt_image_set_attr_strlist(FuFdtImage *self, const gchar *key, gchar **value) G_GNUC_NON_NULL(1, 2); GPtrArray * fu_fdt_image_get_attrs(FuFdtImage *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-fdt.rs000066400000000000000000000015401501337203100175000ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later enum FuFdtToken { BeginNode = 0x00000001, EndNode = 0x00000002, Prop = 0x00000003, Nop = 0x00000004, End = 0x00000009, } #[derive(New, ValidateStream, ParseStream, Default)] #[repr(C, packed)] struct FuStructFdt { magic: u32be == 0xD00DFEED, totalsize: u32be, off_dt_struct: u32be, off_dt_strings: u32be, off_mem_rsvmap: u32be, version: u32be, last_comp_version: u32be = 2, boot_cpuid_phys: u32be, size_dt_strings: u32be, size_dt_struct: u32be, } #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructFdtReserveEntry { address: u64be, size: u64be, } #[derive(New, Parse)] #[repr(C, packed)] struct FuStructFdtProp { len: u32be, nameoff: u32be, } fwupd-2.0.10/libfwupdplugin/fu-firmware-common.c000066400000000000000000000137571501337203100216400ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include #include "fu-common.h" #include "fu-firmware-common.h" #include "fu-mem.h" #include "fu-string.h" /** * fu_firmware_strparse_uint4_safe: * @data: destination buffer * @datasz: size of @data, typically the same as `strlen(data)` * @offset: offset in chars into @data to read * @value: (out) (nullable): parsed value * @error: (nullable): optional return location for an error * * Parses a base 16 number from a string of 1 character in length. * The returned @value will range from 0 to 0xf. * * Returns: %TRUE if parsed, %FALSE otherwise * * Since: 1.5.6 **/ gboolean fu_firmware_strparse_uint4_safe(const gchar *data, gsize datasz, gsize offset, guint8 *value, GError **error) { gchar buffer[2] = {'\0'}; guint64 valuetmp = 0; if (!fu_memcpy_safe((guint8 *)buffer, sizeof(buffer), 0x0, /* dst */ (const guint8 *)data, datasz, offset, /* src */ sizeof(buffer) - 1, error)) return FALSE; if (!fu_strtoull(buffer, &valuetmp, 0, 0xF, FU_INTEGER_BASE_16, error)) { g_autofree gchar *str = fu_strsafe(buffer, sizeof(buffer)); g_prefix_error(error, "cannot parse %s as hex: ", str); return FALSE; } if (value != NULL) *value = (guint8)valuetmp; return TRUE; } /** * fu_firmware_strparse_uint8_safe: * @data: destination buffer * @datasz: size of @data, typically the same as `strlen(data)` * @offset: offset in chars into @data to read * @value: (out) (nullable): parsed value * @error: (nullable): optional return location for an error * * Parses a base 16 number from a string of 2 characters in length. * The returned @value will range from 0 to 0xff. * * Returns: %TRUE if parsed, %FALSE otherwise * * Since: 1.5.6 **/ gboolean fu_firmware_strparse_uint8_safe(const gchar *data, gsize datasz, gsize offset, guint8 *value, GError **error) { gchar buffer[3] = {'\0'}; guint64 valuetmp = 0; if (!fu_memcpy_safe((guint8 *)buffer, sizeof(buffer), 0x0, /* dst */ (const guint8 *)data, datasz, offset, /* src */ sizeof(buffer) - 1, error)) return FALSE; if (!fu_strtoull(buffer, &valuetmp, 0, G_MAXUINT8, FU_INTEGER_BASE_16, error)) { g_autofree gchar *str = fu_strsafe(buffer, sizeof(buffer)); g_prefix_error(error, "cannot parse %s as hex: ", str); return FALSE; } if (value != NULL) *value = (guint8)valuetmp; return TRUE; } /** * fu_firmware_strparse_uint16_safe: * @data: destination buffer * @datasz: size of @data, typically the same as `strlen(data)` * @offset: offset in chars into @data to read * @value: (out) (nullable): parsed value * @error: (nullable): optional return location for an error * * Parses a base 16 number from a string of 4 characters in length. * The returned @value will range from 0 to 0xffff. * * Returns: %TRUE if parsed, %FALSE otherwise * * Since: 1.5.6 **/ gboolean fu_firmware_strparse_uint16_safe(const gchar *data, gsize datasz, gsize offset, guint16 *value, GError **error) { gchar buffer[5] = {'\0'}; guint64 valuetmp = 0; if (!fu_memcpy_safe((guint8 *)buffer, sizeof(buffer), 0x0, /* dst */ (const guint8 *)data, datasz, offset, /* src */ sizeof(buffer) - 1, error)) return FALSE; if (!fu_strtoull(buffer, &valuetmp, 0, G_MAXUINT16, FU_INTEGER_BASE_16, error)) { g_autofree gchar *str = fu_strsafe(buffer, sizeof(buffer)); g_prefix_error(error, "cannot parse %s as hex: ", str); return FALSE; } if (value != NULL) *value = (guint16)valuetmp; return TRUE; } /** * fu_firmware_strparse_uint24_safe: * @data: destination buffer * @datasz: size of @data, typically the same as `strlen(data)` * @offset: offset in chars into @data to read * @value: (out) (nullable): parsed value * @error: (nullable): optional return location for an error * * Parses a base 16 number from a string of 6 characters in length. * The returned @value will range from 0 to 0xffffff. * * Returns: %TRUE if parsed, %FALSE otherwise * * Since: 1.5.6 **/ gboolean fu_firmware_strparse_uint24_safe(const gchar *data, gsize datasz, gsize offset, guint32 *value, GError **error) { gchar buffer[7] = {'\0'}; guint64 valuetmp = 0; if (!fu_memcpy_safe((guint8 *)buffer, sizeof(buffer), 0x0, /* dst */ (const guint8 *)data, datasz, offset, /* src */ sizeof(buffer) - 1, error)) return FALSE; if (!fu_strtoull(buffer, &valuetmp, 0, G_MAXUINT32, FU_INTEGER_BASE_16, error)) { g_autofree gchar *str = fu_strsafe(buffer, sizeof(buffer)); g_prefix_error(error, "cannot parse %s as hex: ", str); return FALSE; } if (value != NULL) *value = (guint16)valuetmp; return TRUE; } /** * fu_firmware_strparse_uint32_safe: * @data: destination buffer * @datasz: size of @data, typically the same as `strlen(data)` * @offset: offset in chars into @data to read * @value: (out) (nullable): parsed value * @error: (nullable): optional return location for an error * * Parses a base 16 number from a string of 8 characters in length. * The returned @value will range from 0 to 0xffffffff. * * Returns: %TRUE if parsed, %FALSE otherwise * * Since: 1.5.6 **/ gboolean fu_firmware_strparse_uint32_safe(const gchar *data, gsize datasz, gsize offset, guint32 *value, GError **error) { gchar buffer[9] = {'\0'}; guint64 valuetmp = 0; if (!fu_memcpy_safe((guint8 *)buffer, sizeof(buffer), 0x0, /* dst */ (const guint8 *)data, datasz, offset, /* src */ sizeof(buffer) - 1, error)) return FALSE; if (!fu_strtoull(buffer, &valuetmp, 0, G_MAXUINT32, FU_INTEGER_BASE_16, error)) { g_autofree gchar *str = fu_strsafe(buffer, sizeof(buffer)); g_prefix_error(error, "cannot parse %s as hex: ", str); return FALSE; } if (value != NULL) *value = (guint32)valuetmp; return TRUE; } fwupd-2.0.10/libfwupdplugin/fu-firmware-common.h000066400000000000000000000016531501337203100216350ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include gboolean fu_firmware_strparse_uint4_safe(const gchar *data, gsize datasz, gsize offset, guint8 *value, GError **error) G_GNUC_NON_NULL(1); gboolean fu_firmware_strparse_uint8_safe(const gchar *data, gsize datasz, gsize offset, guint8 *value, GError **error) G_GNUC_NON_NULL(1); gboolean fu_firmware_strparse_uint16_safe(const gchar *data, gsize datasz, gsize offset, guint16 *value, GError **error) G_GNUC_NON_NULL(1); gboolean fu_firmware_strparse_uint24_safe(const gchar *data, gsize datasz, gsize offset, guint32 *value, GError **error) G_GNUC_NON_NULL(1); gboolean fu_firmware_strparse_uint32_safe(const gchar *data, gsize datasz, gsize offset, guint32 *value, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-firmware.c000066400000000000000000002104431501337203100203410ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-chunk-private.h" #include "fu-common.h" #include "fu-firmware.h" #include "fu-input-stream.h" #include "fu-mem.h" #include "fu-partial-input-stream.h" #include "fu-string.h" /** * FuFirmware: * * A firmware file which can have children which represent the images within. * * See also: [class@FuDfuFirmware], [class@FuIhexFirmware], [class@FuSrecFirmware] */ typedef struct { FuFirmwareFlags flags; FuFirmware *parent; /* noref */ GPtrArray *images; /* FuFirmware */ gchar *version; guint64 version_raw; FwupdVersionFormat version_format; GBytes *bytes; GInputStream *stream; gsize streamsz; FuFirmwareAlignment alignment; gchar *id; gchar *filename; guint64 idx; guint64 addr; guint64 offset; gsize size; gsize size_max; guint images_max; guint depth; GPtrArray *chunks; /* nullable, element-type FuChunk */ GPtrArray *patches; /* nullable, element-type FuFirmwarePatch */ } FuFirmwarePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuFirmware, fu_firmware, G_TYPE_OBJECT) #define GET_PRIVATE(o) (fu_firmware_get_instance_private(o)) enum { PROP_0, PROP_PARENT, PROP_LAST }; #define FU_FIRMWARE_IMAGE_DEPTH_MAX 50 /** * fu_firmware_flag_to_string: * @flag: a #FuFirmwareFlags, e.g. %FU_FIRMWARE_FLAG_DEDUPE_ID * * Converts a #FuFirmwareFlags to a string. * * Returns: identifier string * * Since: 1.5.0 **/ const gchar * fu_firmware_flag_to_string(FuFirmwareFlags flag) { if (flag == FU_FIRMWARE_FLAG_NONE) return "none"; if (flag == FU_FIRMWARE_FLAG_DEDUPE_ID) return "dedupe-id"; if (flag == FU_FIRMWARE_FLAG_DEDUPE_IDX) return "dedupe-idx"; if (flag == FU_FIRMWARE_FLAG_HAS_CHECKSUM) return "has-checksum"; if (flag == FU_FIRMWARE_FLAG_HAS_VID_PID) return "has-vid-pid"; if (flag == FU_FIRMWARE_FLAG_DONE_PARSE) return "done-parse"; if (flag == FU_FIRMWARE_FLAG_HAS_STORED_SIZE) return "has-stored-size"; if (flag == FU_FIRMWARE_FLAG_ALWAYS_SEARCH) return "always-search"; if (flag == FU_FIRMWARE_FLAG_NO_AUTO_DETECTION) return "no-auto-detection"; if (flag == FU_FIRMWARE_FLAG_HAS_CHECK_COMPATIBLE) return "has-check-compatible"; return NULL; } /** * fu_firmware_flag_from_string: * @flag: a string, e.g. `dedupe-id` * * Converts a string to a #FuFirmwareFlags. * * Returns: enumerated value * * Since: 1.5.0 **/ FuFirmwareFlags fu_firmware_flag_from_string(const gchar *flag) { if (g_strcmp0(flag, "dedupe-id") == 0) return FU_FIRMWARE_FLAG_DEDUPE_ID; if (g_strcmp0(flag, "dedupe-idx") == 0) return FU_FIRMWARE_FLAG_DEDUPE_IDX; if (g_strcmp0(flag, "has-checksum") == 0) return FU_FIRMWARE_FLAG_HAS_CHECKSUM; if (g_strcmp0(flag, "has-vid-pid") == 0) return FU_FIRMWARE_FLAG_HAS_VID_PID; if (g_strcmp0(flag, "done-parse") == 0) return FU_FIRMWARE_FLAG_DONE_PARSE; if (g_strcmp0(flag, "has-stored-size") == 0) return FU_FIRMWARE_FLAG_HAS_STORED_SIZE; if (g_strcmp0(flag, "always-search") == 0) return FU_FIRMWARE_FLAG_ALWAYS_SEARCH; if (g_strcmp0(flag, "no-auto-detection") == 0) return FU_FIRMWARE_FLAG_NO_AUTO_DETECTION; if (g_strcmp0(flag, "has-check-compatible") == 0) return FU_FIRMWARE_FLAG_HAS_CHECK_COMPATIBLE; return FU_FIRMWARE_FLAG_NONE; } typedef struct { gsize offset; GBytes *blob; } FuFirmwarePatch; static void fu_firmware_patch_free(FuFirmwarePatch *ptch) { g_bytes_unref(ptch->blob); g_free(ptch); } /** * fu_firmware_add_flag: * @firmware: a #FuFirmware * @flag: the firmware flag * * Adds a specific firmware flag to the firmware. * * Since: 1.5.0 **/ void fu_firmware_add_flag(FuFirmware *firmware, FuFirmwareFlags flag) { FuFirmwarePrivate *priv = GET_PRIVATE(firmware); g_return_if_fail(FU_IS_FIRMWARE(firmware)); priv->flags |= flag; } /** * fu_firmware_has_flag: * @firmware: a #FuFirmware * @flag: the firmware flag * * Finds if the firmware has a specific firmware flag. * * Returns: %TRUE if the flag is set * * Since: 1.5.0 **/ gboolean fu_firmware_has_flag(FuFirmware *firmware, FuFirmwareFlags flag) { FuFirmwarePrivate *priv = GET_PRIVATE(firmware); g_return_val_if_fail(FU_IS_FIRMWARE(firmware), FALSE); return (priv->flags & flag) > 0; } /** * fu_firmware_get_version: * @self: a #FuFirmware * * Gets an optional version that represents the firmware. * * Returns: a string, or %NULL * * Since: 1.3.3 **/ const gchar * fu_firmware_get_version(FuFirmware *self) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), NULL); return priv->version; } /** * fu_firmware_set_version: * @self: a #FuFirmware * @version: (nullable): optional string version * * Sets an optional version that represents the firmware. * * Since: 1.3.3 **/ void fu_firmware_set_version(FuFirmware *self, const gchar *version) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_FIRMWARE(self)); /* not changed */ if (g_strcmp0(priv->version, version) == 0) return; g_free(priv->version); priv->version = g_strdup(version); } /** * fu_firmware_get_version_raw: * @self: a #FuFirmware * * Gets an raw version that represents the firmware. This is most frequently * used when building firmware with `0x123456` in a * `firmware.builder.xml` file to avoid string splitting and sanity checks. * * Returns: an integer, or %G_MAXUINT64 for invalid * * Since: 1.5.7 **/ guint64 fu_firmware_get_version_raw(FuFirmware *self) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), G_MAXUINT64); return priv->version_raw; } /** * fu_firmware_set_version_raw: * @self: a #FuFirmware * @version_raw: a raw version, or %G_MAXUINT64 for invalid * * Sets an raw version that represents the firmware. * * This is optional, and is typically only used for debugging. * * Since: 1.5.7 **/ void fu_firmware_set_version_raw(FuFirmware *self, guint64 version_raw) { FuFirmwarePrivate *priv = GET_PRIVATE(self); FuFirmwareClass *klass = FU_FIRMWARE_GET_CLASS(self); g_return_if_fail(FU_IS_FIRMWARE(self)); priv->version_raw = version_raw; /* convert this */ if (klass->convert_version != NULL) { g_autofree gchar *version = klass->convert_version(self, version_raw); if (version != NULL) fu_firmware_set_version(self, version); } } /** * fu_firmware_get_version_format: * @self: a #FuFirmware * * Gets the version format. * * Returns: the version format, or %FWUPD_VERSION_FORMAT_UNKNOWN if unset * * Since: 2.0.0 **/ FwupdVersionFormat fu_firmware_get_version_format(FuFirmware *self) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), FWUPD_VERSION_FORMAT_UNKNOWN); return priv->version_format; } /** * fu_firmware_set_version_format: * @self: a #FuFirmware * @version_format: the version format, e.g. %FWUPD_VERSION_FORMAT_NUMBER * * Sets the version format. * * Since: 2.0.0 **/ void fu_firmware_set_version_format(FuFirmware *self, FwupdVersionFormat version_format) { FuFirmwarePrivate *priv = GET_PRIVATE(self); FuFirmwareClass *klass = FU_FIRMWARE_GET_CLASS(self); g_return_if_fail(FU_IS_FIRMWARE(self)); /* same */ if (priv->version_format == version_format) return; priv->version_format = version_format; /* convert this, now we know */ if (klass->convert_version != NULL && priv->version != NULL && priv->version_raw != 0) { g_autofree gchar *version = klass->convert_version(self, priv->version_raw); fu_firmware_set_version(self, version); } } /** * fu_firmware_get_filename: * @self: a #FuFirmware * * Gets an optional filename that represents the image source or destination. * * Returns: a string, or %NULL * * Since: 1.6.0 **/ const gchar * fu_firmware_get_filename(FuFirmware *self) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), NULL); return priv->filename; } /** * fu_firmware_set_filename: * @self: a #FuFirmware * @filename: (nullable): a string filename * * Sets an optional filename that represents the image source or destination. * * Since: 1.6.0 **/ void fu_firmware_set_filename(FuFirmware *self, const gchar *filename) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_FIRMWARE(self)); /* not changed */ if (g_strcmp0(priv->filename, filename) == 0) return; g_free(priv->filename); priv->filename = g_strdup(filename); } /** * fu_firmware_set_id: * @self: a #FuPlugin * @id: (nullable): image ID, e.g. `config` * * Since: 1.6.0 **/ void fu_firmware_set_id(FuFirmware *self, const gchar *id) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_FIRMWARE(self)); /* not changed */ if (g_strcmp0(priv->id, id) == 0) return; g_free(priv->id); priv->id = g_strdup(id); } /** * fu_firmware_get_id: * @self: a #FuPlugin * * Gets the image ID, typically set at construction. * * Returns: image ID, e.g. `config` * * Since: 1.6.0 **/ const gchar * fu_firmware_get_id(FuFirmware *self) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), NULL); return priv->id; } /** * fu_firmware_set_addr: * @self: a #FuPlugin * @addr: integer * * Sets the base address of the image. * * Since: 1.6.0 **/ void fu_firmware_set_addr(FuFirmware *self, guint64 addr) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_FIRMWARE(self)); priv->addr = addr; } /** * fu_firmware_get_addr: * @self: a #FuPlugin * * Gets the base address of the image. * * Returns: integer * * Since: 1.6.0 **/ guint64 fu_firmware_get_addr(FuFirmware *self) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), G_MAXUINT64); return priv->addr; } /** * fu_firmware_set_offset: * @self: a #FuPlugin * @offset: integer * * Sets the base offset of the image. * * Since: 1.6.0 **/ void fu_firmware_set_offset(FuFirmware *self, guint64 offset) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_FIRMWARE(self)); priv->offset = offset; } /** * fu_firmware_get_offset: * @self: a #FuPlugin * * Gets the base offset of the image. * * Returns: integer * * Since: 1.6.0 **/ guint64 fu_firmware_get_offset(FuFirmware *self) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), G_MAXUINT64); return priv->offset; } /** * fu_firmware_get_parent: * @self: a #FuFirmware * * Gets the parent. * * Returns: (transfer none): the parent firmware, or %NULL if unset * * Since: 1.8.2 **/ FuFirmware * fu_firmware_get_parent(FuFirmware *self) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), NULL); return priv->parent; } /** * fu_firmware_set_parent: * @self: a #FuFirmware * @parent: (nullable): another #FuFirmware * * Sets the parent. Only used internally. * * Since: 1.8.2 **/ void fu_firmware_set_parent(FuFirmware *self, FuFirmware *parent) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_FIRMWARE(self)); if (priv->parent != NULL) g_object_remove_weak_pointer(G_OBJECT(priv->parent), (gpointer *)&priv->parent); if (parent != NULL) g_object_add_weak_pointer(G_OBJECT(parent), (gpointer *)&priv->parent); priv->parent = parent; } /** * fu_firmware_set_size: * @self: a #FuPlugin * @size: integer * * Sets the total size of the image, which should be the same size as the * data from fu_firmware_write(). * * Since: 1.6.0 **/ void fu_firmware_set_size(FuFirmware *self, gsize size) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_FIRMWARE(self)); priv->size = size; } /** * fu_firmware_get_size: * @self: a #FuPlugin * * Gets the total size of the image, which is typically the same size as the * data from fu_firmware_write(). * * If the size has not been explicitly set, and fu_firmware_set_bytes() has been * used then the size of this is used instead. * * Returns: integer * * Since: 1.6.0 **/ gsize fu_firmware_get_size(FuFirmware *self) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), G_MAXSIZE); if (priv->size != 0) return priv->size; if (priv->stream != NULL && priv->streamsz != 0) return priv->streamsz; if (priv->bytes != NULL) return g_bytes_get_size(priv->bytes); return 0; } /** * fu_firmware_set_size_max: * @self: a #FuPlugin * @size_max: integer, or 0 for no limit * * Sets the maximum size of the image allowed during parsing. * Implementations should query fu_firmware_get_size_max() during parsing when adding images to * ensure the limit is not exceeded. * * Since: 1.9.7 **/ void fu_firmware_set_size_max(FuFirmware *self, gsize size_max) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_FIRMWARE(self)); priv->size_max = size_max; } /** * fu_firmware_get_size_max: * @self: a #FuPlugin * * Gets the maximum size of the image allowed during parsing. * * Returns: integer, or 0 if not set * * Since: 1.9.7 **/ gsize fu_firmware_get_size_max(FuFirmware *self) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), G_MAXSIZE); return priv->size_max; } /** * fu_firmware_set_idx: * @self: a #FuPlugin * @idx: integer * * Sets the index of the image which is used for ordering. * * Since: 1.6.0 **/ void fu_firmware_set_idx(FuFirmware *self, guint64 idx) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_FIRMWARE(self)); priv->idx = idx; } /** * fu_firmware_get_idx: * @self: a #FuPlugin * * Gets the index of the image which is used for ordering. * * Returns: integer * * Since: 1.6.0 **/ guint64 fu_firmware_get_idx(FuFirmware *self) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), G_MAXUINT64); return priv->idx; } /** * fu_firmware_set_bytes: * @self: a #FuPlugin * @bytes: data blob * * Sets the contents of the image if not created with fu_firmware_new_from_bytes(). * * Since: 1.6.0 **/ void fu_firmware_set_bytes(FuFirmware *self, GBytes *bytes) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_FIRMWARE(self)); g_return_if_fail(bytes != NULL); if (priv->bytes == bytes) return; if (priv->bytes != NULL) g_bytes_unref(priv->bytes); priv->bytes = g_bytes_ref(bytes); /* the input stream is no longer valid */ g_clear_object(&priv->stream); } /** * fu_firmware_get_bytes: * @self: a #FuPlugin * @error: (nullable): optional return location for an error * * Gets the firmware payload, which does not have any header or footer included. * * If there is more than one potential payload or image section then fu_firmware_add_image() * should be used instead. * * Returns: (transfer full): a #GBytes, or %NULL if the payload has never been set * * Since: 1.6.0 **/ GBytes * fu_firmware_get_bytes(FuFirmware *self, GError **error) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), NULL); if (priv->bytes != NULL) return g_bytes_ref(priv->bytes); if (priv->stream != NULL) { if (priv->streamsz == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "stream size unknown"); return NULL; } return fu_input_stream_read_bytes(priv->stream, 0x0, priv->streamsz, NULL, error); } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no payload set"); return NULL; } /** * fu_firmware_get_bytes_with_patches: * @self: a #FuPlugin * @error: (nullable): optional return location for an error * * Gets the firmware payload, with any defined patches applied. * * Returns: (transfer full): a #GBytes, or %NULL if the payload has never been set * * Since: 1.7.4 **/ GBytes * fu_firmware_get_bytes_with_patches(FuFirmware *self, GError **error) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_autoptr(GByteArray) buf = g_byte_array_new(); g_return_val_if_fail(FU_IS_FIRMWARE(self), NULL); if (priv->bytes == NULL) { if (priv->stream != NULL) return fu_firmware_get_bytes(self, error); g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no payload set"); return NULL; } /* usual case */ if (priv->patches == NULL) return fu_firmware_get_bytes(self, error); /* convert to a mutable buffer, apply each patch, aborting if the offset isn't valid */ fu_byte_array_append_bytes(buf, priv->bytes); for (guint i = 0; i < priv->patches->len; i++) { FuFirmwarePatch *ptch = g_ptr_array_index(priv->patches, i); if (!fu_memcpy_safe(buf->data, buf->len, ptch->offset, /* dst */ g_bytes_get_data(ptch->blob, NULL), g_bytes_get_size(ptch->blob), 0x0, /* src */ g_bytes_get_size(ptch->blob), error)) { g_prefix_error(error, "failed to apply patch @0x%x: ", (guint)ptch->offset); return NULL; } } /* success */ return g_bytes_new(buf->data, buf->len); } /** * fu_firmware_set_alignment: * @self: a #FuFirmware * @alignment: a #FuFirmwareAlignment * * Sets the alignment of the firmware. * * This allows a firmware to pad to a power of 2 boundary, where @alignment * is the bit position to align to. * * Since: 1.6.0 **/ void fu_firmware_set_alignment(FuFirmware *self, FuFirmwareAlignment alignment) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_FIRMWARE(self)); priv->alignment = alignment; } /** * fu_firmware_get_stream: * @self: a #FuPlugin * @error: (nullable): optional return location for an error * * Gets the input stream which was used to parse the firmware. * * Returns: (transfer full): a #GInputStream, or %NULL if the payload has never been set * * Since: 2.0.0 **/ GInputStream * fu_firmware_get_stream(FuFirmware *self, GError **error) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), NULL); if (priv->stream != NULL) return g_object_ref(priv->stream); if (priv->bytes != NULL) return g_memory_input_stream_new_from_bytes(priv->bytes); g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no stream or bytes set"); return NULL; } /** * fu_firmware_set_stream: * @self: a #FuPlugin * @stream: (nullable): #GInputStream * @error: (nullable): optional return location for an error * * Sets the input stream. * * Returns: %TRUE on success * * Since: 2.0.0 **/ gboolean fu_firmware_set_stream(FuFirmware *self, GInputStream *stream, GError **error) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), FALSE); g_return_val_if_fail(stream == NULL || G_IS_INPUT_STREAM(stream), FALSE); if (stream != NULL) { if (!fu_input_stream_size(stream, &priv->streamsz, error)) return FALSE; } else { priv->streamsz = 0; } g_set_object(&priv->stream, stream); return TRUE; } /** * fu_firmware_get_alignment: * @self: a #FuFirmware * * Gets the alignment of the firmware. * * This allows a firmware to pad to a power of 2 boundary, where @alignment * is the bit position to align to. * * Returns: a #FuFirmwareAlignment * * Since: 1.6.0 **/ FuFirmwareAlignment fu_firmware_get_alignment(FuFirmware *self) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), FU_FIRMWARE_ALIGNMENT_LAST); return priv->alignment; } /** * fu_firmware_get_chunks: * @self: a #FuFirmware * @error: (nullable): optional return location for an error * * Gets the optional image chunks. * * Returns: (transfer container) (element-type FuChunk) (nullable): chunk data, or %NULL * * Since: 1.6.0 **/ GPtrArray * fu_firmware_get_chunks(FuFirmware *self, GError **error) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* set */ if (priv->chunks != NULL) return g_ptr_array_ref(priv->chunks); /* lets build something plausible */ if (priv->bytes != NULL) { g_autoptr(GPtrArray) chunks = NULL; g_autoptr(FuChunk) chk = NULL; chunks = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); chk = fu_chunk_bytes_new(priv->bytes); fu_chunk_set_idx(chk, priv->idx); fu_chunk_set_address(chk, priv->addr); g_ptr_array_add(chunks, g_steal_pointer(&chk)); return g_steal_pointer(&chunks); } /* nothing to do */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no bytes or chunks found in firmware"); return NULL; } /** * fu_firmware_add_chunk: * @self: a #FuFirmware * @chk: a #FuChunk * * Adds a chunk to the image. * * Since: 1.6.0 **/ void fu_firmware_add_chunk(FuFirmware *self, FuChunk *chk) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_FIRMWARE(self)); g_return_if_fail(FU_IS_CHUNK(chk)); if (priv->chunks == NULL) priv->chunks = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_ptr_array_add(priv->chunks, g_object_ref(chk)); } /** * fu_firmware_get_checksum: * @self: a #FuPlugin * @csum_kind: a checksum type, e.g. %G_CHECKSUM_SHA256 * @error: (nullable): optional return location for an error * * Returns a checksum of the payload data. * * Returns: (transfer full): a checksum string, or %NULL if the checksum is not available * * Since: 1.6.0 **/ gchar * fu_firmware_get_checksum(FuFirmware *self, GChecksumType csum_kind, GError **error) { FuFirmwarePrivate *priv = GET_PRIVATE(self); FuFirmwareClass *klass = FU_FIRMWARE_GET_CLASS(self); g_autoptr(GBytes) blob = NULL; g_return_val_if_fail(FU_IS_FIRMWARE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* subclassed */ if (klass->get_checksum != NULL) { g_autoptr(GError) error_local = NULL; g_autofree gchar *checksum = klass->get_checksum(self, csum_kind, &error_local); if (checksum != NULL) return g_steal_pointer(&checksum); if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_propagate_error(error, g_steal_pointer(&error_local)); return NULL; } } /* internal data */ if (priv->bytes != NULL) return g_compute_checksum_for_bytes(csum_kind, priv->bytes); if (priv->stream != NULL) return fu_input_stream_compute_checksum(priv->stream, csum_kind, error); /* write */ blob = fu_firmware_write(self, error); if (blob == NULL) return NULL; return g_compute_checksum_for_bytes(csum_kind, blob); } /** * fu_firmware_tokenize: * @self: a #FuFirmware * @stream: a #GInputStream * @flags: #FuFirmwareParseFlags, e.g. %FWUPD_INSTALL_FLAG_FORCE * @error: (nullable): optional return location for an error * * Tokenizes a firmware, typically breaking the firmware into records. * * Records can be enumerated using subclass-specific functionality, for example * using fu_srec_firmware_get_records(). * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fu_firmware_tokenize(FuFirmware *self, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuFirmwareClass *klass = FU_FIRMWARE_GET_CLASS(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), FALSE); g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* optionally subclassed */ if (klass->tokenize != NULL) return klass->tokenize(self, stream, flags, error); return TRUE; } /** * fu_firmware_check_compatible: * @self: a #FuFirmware * @other: a #FuFirmware * @flags: #FuFirmwareParseFlags, e.g. %FWUPD_INSTALL_FLAG_FORCE * @error: (nullable): optional return location for an error * * Check a new firmware is compatible with the existing firmware. * * Returns: %TRUE for success * * Since: 1.8.4 **/ gboolean fu_firmware_check_compatible(FuFirmware *self, FuFirmware *other, FuFirmwareParseFlags flags, GError **error) { FuFirmwareClass *klass = FU_FIRMWARE_GET_CLASS(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), FALSE); g_return_val_if_fail(FU_IS_FIRMWARE(other), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* optionally subclassed */ if (klass->check_compatible == NULL) return TRUE; return klass->check_compatible(self, other, flags, error); } static gboolean fu_firmware_validate_for_offset(FuFirmware *self, GInputStream *stream, gsize *offset, FuFirmwareParseFlags flags, GError **error) { FuFirmwareClass *klass = FU_FIRMWARE_GET_CLASS(self); gsize streamsz = 0; /* not implemented */ if (klass->validate == NULL) return TRUE; /* fuzzing */ if (!fu_firmware_has_flag(self, FU_FIRMWARE_FLAG_ALWAYS_SEARCH) && (flags & FU_FIRMWARE_PARSE_FLAG_NO_SEARCH) > 0) { if (!klass->validate(self, stream, *offset, error)) return FALSE; return TRUE; } /* limit the size of firmware we search */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz > FU_FIRMWARE_SEARCH_MAGIC_BUFSZ_MAX) { if (!klass->validate(self, stream, *offset, error)) { g_prefix_error(error, "failed to search for magic as firmware size was 0x%x and " "limit was 0x%x: ", (guint)streamsz, (guint)FU_FIRMWARE_SEARCH_MAGIC_BUFSZ_MAX); return FALSE; } return TRUE; } /* increment the offset, looking for the magic */ for (gsize offset_tmp = *offset; offset_tmp < streamsz; offset_tmp++) { if (klass->validate(self, stream, offset_tmp, NULL)) { fu_firmware_set_offset(self, offset_tmp); *offset = offset_tmp; return TRUE; } } /* did not find what we were looking for */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "did not find magic"); return FALSE; } /** * fu_firmware_parse_stream: * @self: a #FuFirmware * @stream: input stream * @offset: start offset * @flags: #FuFirmwareParseFlags, e.g. %FWUPD_INSTALL_FLAG_FORCE * @error: (nullable): optional return location for an error * * Parses a firmware from a stream, typically breaking the firmware into images. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fu_firmware_parse_stream(FuFirmware *self, GInputStream *stream, gsize offset, FuFirmwareParseFlags flags, GError **error) { FuFirmwareClass *klass = FU_FIRMWARE_GET_CLASS(self); FuFirmwarePrivate *priv = GET_PRIVATE(self); gsize streamsz = 0; g_autoptr(GInputStream) partial_stream = NULL; g_return_val_if_fail(FU_IS_FIRMWARE(self), FALSE); g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* sanity check */ if (fu_firmware_has_flag(self, FU_FIRMWARE_FLAG_DONE_PARSE)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "firmware object cannot be reused"); return FALSE; } /* check size */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz <= offset) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "stream size 0x%x is smaller than offset 0x%x", (guint)streamsz, (guint)offset); return FALSE; } /* optional */ if (!fu_firmware_validate_for_offset(self, stream, &offset, flags, error)) return FALSE; /* save stream size */ priv->streamsz = streamsz - offset; if (priv->streamsz == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid firmware as zero sized"); return FALSE; } if (priv->size_max > 0 && priv->streamsz > priv->size_max) { g_autofree gchar *sz_val = g_format_size(priv->streamsz); g_autofree gchar *sz_max = g_format_size(priv->size_max); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware is too large (%s, limit %s)", sz_val, sz_max); return FALSE; } /* any FuFirmware subclass that gets past this point might have allocated memory in * ->tokenize() or ->parse() and needs to be destroyed before parsing again */ fu_firmware_add_flag(self, FU_FIRMWARE_FLAG_DONE_PARSE); /* this allows devices to skip reading the old firmware if the GType is unsuitable */ if (klass->check_compatible != NULL) fu_firmware_add_flag(self, FU_FIRMWARE_FLAG_HAS_CHECK_COMPATIBLE); /* save stream */ if (offset == 0) { partial_stream = g_object_ref(stream); } else { partial_stream = fu_partial_input_stream_new(stream, offset, priv->streamsz, error); if (partial_stream == NULL) { g_prefix_error(error, "failed to cut firmware: "); return FALSE; } } /* cache */ if (flags & FU_FIRMWARE_PARSE_FLAG_CACHE_BLOB) { g_autoptr(GBytes) blob = NULL; blob = fu_input_stream_read_bytes(partial_stream, 0x0, priv->streamsz, NULL, error); if (blob == NULL) return FALSE; fu_firmware_set_bytes(self, blob); } if (flags & FU_FIRMWARE_PARSE_FLAG_CACHE_STREAM) { g_set_object(&priv->stream, partial_stream); } /* optional */ if (klass->tokenize != NULL) { if (!klass->tokenize(self, partial_stream, flags, error)) return FALSE; } /* optional */ if (klass->parse != NULL) return klass->parse(self, partial_stream, flags, error); /* verify alignment */ if (streamsz % (1ull << priv->alignment) != 0) { g_autofree gchar *str = NULL; str = g_format_size_full(1ull << priv->alignment, G_FORMAT_SIZE_IEC_UNITS); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "raw firmware is not aligned to 0x%x (%s)", (guint)(1ull << priv->alignment), str); return FALSE; } /* success */ return TRUE; } /** * fu_firmware_parse_bytes: * @self: a #FuFirmware * @fw: firmware blob * @offset: start offset, useful for ignoring a bootloader * @flags: #FuFirmwareParseFlags, e.g. %FWUPD_INSTALL_FLAG_FORCE * @error: (nullable): optional return location for an error * * Parses a firmware, typically breaking the firmware into images. * * Returns: %TRUE for success * * Since: 2.0.1 **/ gboolean fu_firmware_parse_bytes(FuFirmware *self, GBytes *fw, gsize offset, FuFirmwareParseFlags flags, GError **error) { g_autoptr(GInputStream) stream = NULL; g_return_val_if_fail(FU_IS_FIRMWARE(self), FALSE); g_return_val_if_fail(fw != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); stream = g_memory_input_stream_new_from_bytes(fw); return fu_firmware_parse_stream(self, stream, offset, flags, error); } /** * fu_firmware_build: * @self: a #FuFirmware * @n: a Xmlb node * @error: (nullable): optional return location for an error * * Builds a firmware from an XML manifest. The manifest would typically have the * following form: * * |[ * * * 1.2.3 * * 7.8.9 * stage1 * 0x01 * stage1.bin * * * stage2 * * * * ape * 0x7 * aGVsbG8gd29ybGQ= * * * ]| * * This would be used in a build-system to merge images from generated files: * `fwupdtool firmware-build fw.builder.xml test.fw` * * Static binary content can be specified in the `/` section and * is encoded as base64 text if not empty. * * Additionally, extra nodes can be included under nested `` objects * which can be parsed by the subclassed objects. You should verify the * subclassed object `FuFirmware->build` vfunc for the specific additional * options supported. * * Plugins should manually g_type_ensure() subclassed image objects if not * constructed as part of the plugin fu_plugin_init() or fu_plugin_setup() * functions. * * Returns: %TRUE for success * * Since: 1.5.0 **/ gboolean fu_firmware_build(FuFirmware *self, XbNode *n, GError **error) { FuFirmwareClass *klass = FU_FIRMWARE_GET_CLASS(self); const gchar *tmp; guint64 tmpval; guint64 version_raw; g_autoptr(GPtrArray) chunks = NULL; g_autoptr(GPtrArray) xb_images = NULL; g_autoptr(XbNode) data = NULL; g_return_val_if_fail(FU_IS_FIRMWARE(self), FALSE); g_return_val_if_fail(XB_IS_NODE(n), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* set attributes */ tmp = xb_node_query_text(n, "version", NULL); if (tmp != NULL) fu_firmware_set_version(self, tmp); tmp = xb_node_query_text(n, "version_format", NULL); if (tmp != NULL) { FwupdVersionFormat version_format = fwupd_version_format_from_string(tmp); if (version_format == FWUPD_VERSION_FORMAT_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "%s is not a valid version format", tmp); return FALSE; } fu_firmware_set_version_format(self, version_format); } version_raw = xb_node_query_text_as_uint(n, "version_raw", NULL); if (version_raw != G_MAXUINT64) fu_firmware_set_version_raw(self, version_raw); tmp = xb_node_query_text(n, "id", NULL); if (tmp != NULL) fu_firmware_set_id(self, tmp); tmpval = xb_node_query_text_as_uint(n, "idx", NULL); if (tmpval != G_MAXUINT64) fu_firmware_set_idx(self, tmpval); tmpval = xb_node_query_text_as_uint(n, "addr", NULL); if (tmpval != G_MAXUINT64) fu_firmware_set_addr(self, tmpval); tmpval = xb_node_query_text_as_uint(n, "offset", NULL); if (tmpval != G_MAXUINT64) fu_firmware_set_offset(self, tmpval); tmpval = xb_node_query_text_as_uint(n, "size", NULL); if (tmpval != G_MAXUINT64) fu_firmware_set_size(self, tmpval); tmpval = xb_node_query_text_as_uint(n, "size_max", NULL); if (tmpval != G_MAXUINT64) fu_firmware_set_size_max(self, tmpval); tmpval = xb_node_query_text_as_uint(n, "alignment", NULL); if (tmpval != G_MAXUINT64) { if (tmpval > FU_FIRMWARE_ALIGNMENT_2G) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "0x%x invalid, maximum is 0x%x", (guint)tmpval, (guint)FU_FIRMWARE_ALIGNMENT_2G); return FALSE; } fu_firmware_set_alignment(self, (guint8)tmpval); } tmp = xb_node_query_text(n, "filename", NULL); if (tmp != NULL) { g_autoptr(GBytes) blob = NULL; blob = fu_bytes_get_contents(tmp, error); if (blob == NULL) return FALSE; fu_firmware_set_bytes(self, blob); fu_firmware_set_filename(self, tmp); } data = xb_node_query_first(n, "data", NULL); if (data != NULL) { guint64 sz = xb_node_get_attr_as_uint(data, "size"); g_autoptr(GBytes) blob = NULL; /* base64 encoded data */ if (xb_node_get_text(data) != NULL) { gsize bufsz = 0; g_autofree guchar *buf = NULL; buf = g_base64_decode(xb_node_get_text(data), &bufsz); blob = g_bytes_new(buf, bufsz); } else { blob = g_bytes_new(NULL, 0); } /* padding is optional */ if (sz == 0 || sz == G_MAXUINT64) { fu_firmware_set_bytes(self, blob); } else { g_autoptr(GBytes) blob_padded = fu_bytes_pad(blob, (gsize)sz, 0xFF); fu_firmware_set_bytes(self, blob_padded); } } /* optional chunks */ chunks = xb_node_query(n, "chunks/chunk", 0, NULL); if (chunks != NULL) { for (guint i = 0; i < chunks->len; i++) { XbNode *c = g_ptr_array_index(chunks, i); g_autoptr(FuChunk) chk = fu_chunk_bytes_new(NULL); fu_chunk_set_idx(chk, i); if (!fu_chunk_build(chk, c, error)) return FALSE; fu_firmware_add_chunk(self, chk); } } /* parse images */ xb_images = xb_node_query(n, "firmware", 0, NULL); if (xb_images != NULL) { for (guint i = 0; i < xb_images->len; i++) { XbNode *xb_image = g_ptr_array_index(xb_images, i); g_autoptr(FuFirmware) img = NULL; tmp = xb_node_get_attr(xb_image, "gtype"); if (tmp != NULL) { GType gtype = g_type_from_name(tmp); if (gtype == G_TYPE_INVALID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "GType %s not registered", tmp); return FALSE; } img = g_object_new(gtype, NULL); } else { img = fu_firmware_new(); } if (!fu_firmware_add_image_full(self, img, error)) return FALSE; if (!fu_firmware_build(img, xb_image, error)) return FALSE; } } /* subclassed */ if (klass->build != NULL) { if (!klass->build(self, n, error)) return FALSE; } /* success */ return TRUE; } /** * fu_firmware_build_from_xml: * @self: a #FuFirmware * @xml: XML text * @error: (nullable): optional return location for an error * * Builds a firmware from an XML manifest. The manifest would typically have the * following form: * * |[ * * * 1.2.3 * * 7.8.9 * stage1 * 0x01 * stage1.bin * * * stage2 * * * * ape * 0x7 * aGVsbG8gd29ybGQ= * * * ]| * * This would be used in a build-system to merge images from generated files: * `fwupdtool firmware-build fw.builder.xml test.fw` * * Static binary content can be specified in the `/` section and * is encoded as base64 text if not empty. * * Additionally, extra nodes can be included under nested `` objects * which can be parsed by the subclassed objects. You should verify the * subclassed object `FuFirmware->build` vfunc for the specific additional * options supported. * * Plugins should manually g_type_ensure() subclassed image objects if not * constructed as part of the plugin fu_plugin_init() or fu_plugin_setup() * functions. * * Returns: %TRUE for success * * Since: 1.6.0 **/ gboolean fu_firmware_build_from_xml(FuFirmware *self, const gchar *xml, GError **error) { g_autoptr(XbBuilder) builder = xb_builder_new(); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_autoptr(XbNode) n = NULL; g_autoptr(XbSilo) silo = NULL; /* parse XML */ if (!xb_builder_source_load_xml(source, xml, XB_BUILDER_SOURCE_FLAG_NONE, error)) { g_prefix_error(error, "could not parse XML: "); fwupd_error_convert(error); return FALSE; } xb_builder_import_source(builder, source); silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, error); if (silo == NULL) { fwupd_error_convert(error); return FALSE; } /* create FuFirmware of specific GType */ n = xb_silo_query_first(silo, "firmware", error); if (n == NULL) { fwupd_error_convert(error); return FALSE; } return fu_firmware_build(self, n, error); } /** * fu_firmware_build_from_filename: * @self: a #FuFirmware * @filename: filename of XML builder * @error: (nullable): optional return location for an error * * Builds a firmware from an XML manifest. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fu_firmware_build_from_filename(FuFirmware *self, const gchar *filename, GError **error) { g_autofree gchar *xml = NULL; g_return_val_if_fail(FU_IS_FIRMWARE(self), FALSE); g_return_val_if_fail(filename != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!g_file_get_contents(filename, &xml, NULL, error)) return FALSE; return fu_firmware_build_from_xml(self, xml, error); } /** * fu_firmware_parse_file: * @self: a #FuFirmware * @file: a file * @flags: #FuFirmwareParseFlags, e.g. %FWUPD_INSTALL_FLAG_FORCE * @error: (nullable): optional return location for an error * * Parses a firmware file, typically breaking the firmware into images. * * Returns: %TRUE for success * * Since: 1.3.3 **/ gboolean fu_firmware_parse_file(FuFirmware *self, GFile *file, FuFirmwareParseFlags flags, GError **error) { g_autoptr(GFileInputStream) stream = NULL; g_return_val_if_fail(FU_IS_FIRMWARE(self), FALSE); g_return_val_if_fail(G_IS_FILE(file), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); stream = g_file_read(file, NULL, error); if (stream == NULL) { fu_error_convert(error); return FALSE; } return fu_firmware_parse_stream(self, G_INPUT_STREAM(stream), 0, flags, error); } /** * fu_firmware_write: * @self: a #FuFirmware * @error: (nullable): optional return location for an error * * Writes a firmware, typically packing the images into a binary blob. * * Returns: (transfer full): a data blob * * Since: 1.3.1 **/ GBytes * fu_firmware_write(FuFirmware *self, GError **error) { FuFirmwareClass *klass = FU_FIRMWARE_GET_CLASS(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* subclassed */ if (klass->write != NULL) { g_autoptr(GByteArray) buf = klass->write(self, error); if (buf == NULL) return NULL; return g_bytes_new(buf->data, buf->len); } /* just add default blob */ return fu_firmware_get_bytes_with_patches(self, error); } /** * fu_firmware_add_patch: * @self: a #FuFirmware * @offset: an address smaller than fu_firmware_get_size() * @blob: (not nullable): bytes to replace * * Adds a byte patch at a specific offset. If a patch already exists at the specified address then * it is replaced. * * If the @address is larger than the size of the image then an error is returned. * * Since: 1.7.4 **/ void fu_firmware_add_patch(FuFirmware *self, gsize offset, GBytes *blob) { FuFirmwarePrivate *priv = GET_PRIVATE(self); FuFirmwarePatch *ptch; g_return_if_fail(FU_IS_FIRMWARE(self)); g_return_if_fail(blob != NULL); /* ensure exists */ if (priv->patches == NULL) { priv->patches = g_ptr_array_new_with_free_func((GDestroyNotify)fu_firmware_patch_free); } /* find existing of exact same size */ for (guint i = 0; i < priv->patches->len; i++) { ptch = g_ptr_array_index(priv->patches, i); if (ptch->offset == offset && g_bytes_get_size(ptch->blob) == g_bytes_get_size(blob)) { g_bytes_unref(ptch->blob); ptch->blob = g_bytes_ref(blob); return; } } /* add new */ ptch = g_new0(FuFirmwarePatch, 1); ptch->offset = offset; ptch->blob = g_bytes_ref(blob); g_ptr_array_add(priv->patches, ptch); } /** * fu_firmware_write_chunk: * @self: a #FuFirmware * @address: an address smaller than fu_firmware_get_addr() * @chunk_sz_max: the size of the new chunk * @error: (nullable): optional return location for an error * * Gets a block of data from the image. If the contents of the image is * smaller than the requested chunk size then the #GBytes will be smaller * than @chunk_sz_max. Use fu_bytes_pad() if padding is required. * * If the @address is larger than the size of the image then an error is returned. * * Returns: (transfer full): a #GBytes, or %NULL * * Since: 1.6.0 **/ GBytes * fu_firmware_write_chunk(FuFirmware *self, guint64 address, guint64 chunk_sz_max, GError **error) { FuFirmwarePrivate *priv = GET_PRIVATE(self); gsize chunk_left; guint64 offset; g_return_val_if_fail(FU_IS_FIRMWARE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* check address requested is larger than base address */ if (address < priv->addr) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "requested address 0x%x less than base address 0x%x", (guint)address, (guint)priv->addr); return NULL; } /* offset into data */ offset = address - priv->addr; if (offset > g_bytes_get_size(priv->bytes)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "offset 0x%x larger than data size 0x%x", (guint)offset, (guint)g_bytes_get_size(priv->bytes)); return NULL; } /* if we have less data than requested */ chunk_left = g_bytes_get_size(priv->bytes) - offset; if (chunk_sz_max > chunk_left) { return fu_bytes_new_offset(priv->bytes, offset, chunk_left, error); } /* check chunk */ return fu_bytes_new_offset(priv->bytes, offset, chunk_sz_max, error); } /** * fu_firmware_write_file: * @self: a #FuFirmware * @file: a file * @error: (nullable): optional return location for an error * * Writes a firmware, typically packing the images into a binary blob. * * Returns: %TRUE for success * * Since: 1.3.3 **/ gboolean fu_firmware_write_file(FuFirmware *self, GFile *file, GError **error) { g_autoptr(GBytes) blob = NULL; g_autoptr(GFile) parent = NULL; g_return_val_if_fail(FU_IS_FIRMWARE(self), FALSE); g_return_val_if_fail(G_IS_FILE(file), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); blob = fu_firmware_write(self, error); if (blob == NULL) return FALSE; parent = g_file_get_parent(file); if (!g_file_query_exists(parent, NULL)) { if (!g_file_make_directory_with_parents(parent, NULL, error)) return FALSE; } return g_file_replace_contents(file, g_bytes_get_data(blob, NULL), g_bytes_get_size(blob), NULL, FALSE, G_FILE_CREATE_NONE, NULL, NULL, error); } static void fu_firmware_set_depth(FuFirmware *self, guint depth) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_FIRMWARE(self)); priv->depth = depth; } /** * fu_firmware_get_depth: * @self: a #FuPlugin * * Gets the depth of this child image relative to the root. * * Returns: integer, or 0 for the root. * * Since: 1.9.14 **/ guint fu_firmware_get_depth(FuFirmware *self) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), G_MAXUINT); return priv->depth; } /** * fu_firmware_add_image_full: * @self: a #FuPlugin * @img: a child firmware image * @error: (nullable): optional return location for an error * * Adds an image to the firmware. This method will fail if the number of images would be * above the limit set by fu_firmware_set_images_max(). * * If %FU_FIRMWARE_FLAG_DEDUPE_ID is set, an image with the same ID is already * present it is replaced. * * Returns: %TRUE if the image was added * * Since: 1.9.3 **/ gboolean fu_firmware_add_image_full(FuFirmware *self, FuFirmware *img, GError **error) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), FALSE); g_return_val_if_fail(FU_IS_FIRMWARE(img), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* check depth */ if (priv->depth > FU_FIRMWARE_IMAGE_DEPTH_MAX) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "images are nested too deep, limit is %u", (guint)FU_FIRMWARE_IMAGE_DEPTH_MAX); return FALSE; } /* dedupe */ for (guint i = 0; i < priv->images->len; i++) { FuFirmware *img_tmp = g_ptr_array_index(priv->images, i); if (priv->flags & FU_FIRMWARE_FLAG_DEDUPE_ID) { if (g_strcmp0(fu_firmware_get_id(img_tmp), fu_firmware_get_id(img)) == 0) { g_ptr_array_remove_index(priv->images, i); break; } } if (priv->flags & FU_FIRMWARE_FLAG_DEDUPE_IDX) { if (fu_firmware_get_idx(img_tmp) == fu_firmware_get_idx(img)) { g_ptr_array_remove_index(priv->images, i); break; } } } /* sanity check */ if (priv->images_max > 0 && priv->images->len >= priv->images_max) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "too many images, limit is %u", priv->images_max); return FALSE; } g_ptr_array_add(priv->images, g_object_ref(img)); /* set the other way around */ fu_firmware_set_parent(img, self); fu_firmware_set_depth(img, priv->depth + 1); /* success */ return TRUE; } /** * fu_firmware_add_image: * @self: a #FuPlugin * @img: a child firmware image * * Adds an image to the firmware. * * NOTE: If adding images in a loop of any kind then fu_firmware_add_image_full() should be used * instead, and fu_firmware_set_images_max() should be set before adding images. * * If %FU_FIRMWARE_FLAG_DEDUPE_ID is set, an image with the same ID is already * present it is replaced. * * Since: 1.3.1 **/ void fu_firmware_add_image(FuFirmware *self, FuFirmware *img) { g_autoptr(GError) error_local = NULL; g_return_if_fail(FU_IS_FIRMWARE(self)); g_return_if_fail(FU_IS_FIRMWARE(img)); if (!fu_firmware_add_image_full(self, img, &error_local)) g_critical("failed to add image: %s", error_local->message); } /** * fu_firmware_set_images_max: * @self: a #FuPlugin * @images_max: integer, or 0 for unlimited * * Sets the maximum number of images this container can hold. * * Since: 1.9.3 **/ void fu_firmware_set_images_max(FuFirmware *self, guint images_max) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_FIRMWARE(self)); priv->images_max = images_max; } /** * fu_firmware_get_images_max: * @self: a #FuPlugin * * Gets the maximum number of images this container can hold. * * Returns: integer, or 0 for unlimited. * * Since: 1.9.3 **/ guint fu_firmware_get_images_max(FuFirmware *self) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), G_MAXUINT); return priv->images_max; } /** * fu_firmware_remove_image: * @self: a #FuPlugin * @img: a child firmware image * @error: (nullable): optional return location for an error * * Remove an image from the firmware. * * Returns: %TRUE if the image was removed * * Since: 1.5.0 **/ gboolean fu_firmware_remove_image(FuFirmware *self, FuFirmware *img, GError **error) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), FALSE); g_return_val_if_fail(FU_IS_FIRMWARE(img), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (g_ptr_array_remove(priv->images, img)) return TRUE; /* did not exist */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "image %s not found in firmware", fu_firmware_get_id(img)); return FALSE; } /** * fu_firmware_remove_image_by_idx: * @self: a #FuPlugin * @idx: index * @error: (nullable): optional return location for an error * * Removes the first image from the firmware matching the index. * * Returns: %TRUE if an image was removed * * Since: 1.5.0 **/ gboolean fu_firmware_remove_image_by_idx(FuFirmware *self, guint64 idx, GError **error) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_autoptr(FuFirmware) img = NULL; g_return_val_if_fail(FU_IS_FIRMWARE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); img = fu_firmware_get_image_by_idx(self, idx, error); if (img == NULL) return FALSE; g_ptr_array_remove(priv->images, img); return TRUE; } /** * fu_firmware_remove_image_by_id: * @self: a #FuPlugin * @id: (nullable): image ID, e.g. `config` * @error: (nullable): optional return location for an error * * Removes the first image from the firmware matching the ID. * * Returns: %TRUE if an image was removed * * Since: 1.5.0 **/ gboolean fu_firmware_remove_image_by_id(FuFirmware *self, const gchar *id, GError **error) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_autoptr(FuFirmware) img = NULL; g_return_val_if_fail(FU_IS_FIRMWARE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); img = fu_firmware_get_image_by_id(self, id, error); if (img == NULL) return FALSE; g_ptr_array_remove(priv->images, img); return TRUE; } /** * fu_firmware_get_images: * @self: a #FuFirmware * * Returns all the images in the firmware. * * Returns: (transfer container) (element-type FuFirmware): images * * Since: 1.3.1 **/ GPtrArray * fu_firmware_get_images(FuFirmware *self) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_autoptr(GPtrArray) imgs = NULL; g_return_val_if_fail(FU_IS_FIRMWARE(self), NULL); imgs = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); for (guint i = 0; i < priv->images->len; i++) { FuFirmware *img = g_ptr_array_index(priv->images, i); g_ptr_array_add(imgs, g_object_ref(img)); } return g_steal_pointer(&imgs); } /** * fu_firmware_get_image_by_id: * @self: a #FuPlugin * @id: (nullable): image ID, e.g. `config` or `*.mfg|*.elf` * @error: (nullable): optional return location for an error * * Gets the firmware image using the image ID. * * Returns: (transfer full): a #FuFirmware, or %NULL if the image is not found * * Since: 1.3.1 **/ FuFirmware * fu_firmware_get_image_by_id(FuFirmware *self, const gchar *id, GError **error) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* non-NULL */ if (id != NULL) { g_auto(GStrv) split = g_strsplit(id, "|", 0); for (guint i = 0; i < priv->images->len; i++) { FuFirmware *img = g_ptr_array_index(priv->images, i); for (guint j = 0; split[j] != NULL; j++) { if (g_pattern_match_simple(split[j], fu_firmware_get_id(img))) return g_object_ref(img); } } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no image id %s found in firmware", id); return NULL; } /* NULL */ for (guint i = 0; i < priv->images->len; i++) { FuFirmware *img = g_ptr_array_index(priv->images, i); if (fu_firmware_get_id(img) == NULL) return g_object_ref(img); } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no NULL image id found in firmware"); return NULL; } /** * fu_firmware_get_image_by_id_bytes: * @self: a #FuPlugin * @id: (nullable): image ID, e.g. `config` * @error: (nullable): optional return location for an error * * Gets the firmware image bytes using the image ID. * * Returns: (transfer full): a #GBytes of a #FuFirmware, or %NULL if the image is not found * * Since: 1.3.1 **/ GBytes * fu_firmware_get_image_by_id_bytes(FuFirmware *self, const gchar *id, GError **error) { g_autoptr(FuFirmware) img = fu_firmware_get_image_by_id(self, id, error); if (img == NULL) return NULL; return fu_firmware_write(img, error); } /** * fu_firmware_get_image_by_id_stream: * @self: a #FuPlugin * @id: (nullable): image ID, e.g. `config` * @error: (nullable): optional return location for an error * * Gets the firmware image stream using the image ID. * * Returns: (transfer full): a #GInputStream of a #FuFirmware, or %NULL if the image is not found * * Since: 2.0.0 **/ GInputStream * fu_firmware_get_image_by_id_stream(FuFirmware *self, const gchar *id, GError **error) { g_autoptr(FuFirmware) img = fu_firmware_get_image_by_id(self, id, error); if (img == NULL) return NULL; return fu_firmware_get_stream(img, error); } /** * fu_firmware_get_image_by_idx: * @self: a #FuPlugin * @idx: image index * @error: (nullable): optional return location for an error * * Gets the firmware image using the image index. * * Returns: (transfer full): a #FuFirmware, or %NULL if the image is not found * * Since: 1.3.1 **/ FuFirmware * fu_firmware_get_image_by_idx(FuFirmware *self, guint64 idx, GError **error) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); for (guint i = 0; i < priv->images->len; i++) { FuFirmware *img = g_ptr_array_index(priv->images, i); if (fu_firmware_get_idx(img) == idx) return g_object_ref(img); } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no image idx %" G_GUINT64_FORMAT " found in firmware", idx); return NULL; } /** * fu_firmware_get_image_by_checksum: * @self: a #FuPlugin * @checksum: checksum string of any format * @error: (nullable): optional return location for an error * * Gets the firmware image using the image checksum. The checksum type is guessed * based on the length of the input string. * * Returns: (transfer full): a #FuFirmware, or %NULL if the image is not found * * Since: 1.5.5 **/ FuFirmware * fu_firmware_get_image_by_checksum(FuFirmware *self, const gchar *checksum, GError **error) { FuFirmwarePrivate *priv = GET_PRIVATE(self); GChecksumType csum_kind; g_return_val_if_fail(FU_IS_FIRMWARE(self), NULL); g_return_val_if_fail(checksum != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); csum_kind = fwupd_checksum_guess_kind(checksum); for (guint i = 0; i < priv->images->len; i++) { FuFirmware *img = g_ptr_array_index(priv->images, i); g_autofree gchar *checksum_tmp = NULL; /* if this expensive then the subclassed FuFirmware can * cache the result as required */ checksum_tmp = fu_firmware_get_checksum(img, csum_kind, error); if (checksum_tmp == NULL) return NULL; if (g_strcmp0(checksum_tmp, checksum) == 0) return g_object_ref(img); } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no image with checksum %s found in firmware", checksum); return NULL; } /** * fu_firmware_get_image_by_idx_bytes: * @self: a #FuPlugin * @idx: image index * @error: (nullable): optional return location for an error * * Gets the firmware image bytes using the image index. * * Returns: (transfer full): a #GBytes of a #FuFirmware, or %NULL if the image is not found * * Since: 1.3.1 **/ GBytes * fu_firmware_get_image_by_idx_bytes(FuFirmware *self, guint64 idx, GError **error) { g_autoptr(FuFirmware) img = fu_firmware_get_image_by_idx(self, idx, error); if (img == NULL) return NULL; return fu_firmware_write(img, error); } /** * fu_firmware_get_image_by_idx_stream: * @self: a #FuPlugin * @idx: image index * @error: (nullable): optional return location for an error * * Gets the firmware image stream using the image index. * * Returns: (transfer full): a #GInputStream of a #FuFirmware, or %NULL if the image is not found * * Since: 2.0.0 **/ GInputStream * fu_firmware_get_image_by_idx_stream(FuFirmware *self, guint64 idx, GError **error) { g_autoptr(FuFirmware) img = fu_firmware_get_image_by_idx(self, idx, error); if (img == NULL) return NULL; return fu_firmware_get_stream(img, error); } /** * fu_firmware_get_image_by_gtype_bytes: * @self: a #FuPlugin * @gtype: an image #GType * @error: (nullable): optional return location for an error * * Gets the firmware image bytes using the image #GType. * * Returns: (transfer full): a #GBytes of a #FuFirmware, or %NULL if the image is not found * * Since: 1.9.3 **/ GBytes * fu_firmware_get_image_by_gtype_bytes(FuFirmware *self, GType gtype, GError **error) { g_autoptr(FuFirmware) img = fu_firmware_get_image_by_gtype(self, gtype, error); if (img == NULL) return NULL; return fu_firmware_write(img, error); } /** * fu_firmware_get_image_by_gtype: * @self: a #FuPlugin * @gtype: an image #GType * @error: (nullable): optional return location for an error * * Gets the firmware image using the image #GType. * * Returns: (transfer full): a #FuFirmware, or %NULL if the image is not found * * Since: 1.9.3 **/ FuFirmware * fu_firmware_get_image_by_gtype(FuFirmware *self, GType gtype, GError **error) { FuFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_FIRMWARE(self), NULL); g_return_val_if_fail(gtype != G_TYPE_INVALID, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); for (guint i = 0; i < priv->images->len; i++) { FuFirmware *img = g_ptr_array_index(priv->images, i); if (g_type_is_a(G_OBJECT_TYPE(img), gtype)) return g_object_ref(img); } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no image GType %s found in firmware", g_type_name(gtype)); return NULL; } /** * fu_firmware_export: * @self: a #FuFirmware * @flags: firmware export flags, e.g. %FU_FIRMWARE_EXPORT_FLAG_INCLUDE_DEBUG * @bn: a Xmlb builder node * * This allows us to build an XML object for the nested firmware. * * Since: 1.6.0 **/ void fu_firmware_export(FuFirmware *self, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuFirmwareClass *klass = FU_FIRMWARE_GET_CLASS(self); FuFirmwarePrivate *priv = GET_PRIVATE(self); const gchar *gtypestr = G_OBJECT_TYPE_NAME(self); /* object */ if (g_strcmp0(gtypestr, "FuFirmware") != 0) xb_builder_node_set_attr(bn, "gtype", gtypestr); /* subclassed type */ if (priv->flags != FU_FIRMWARE_FLAG_NONE) { g_autoptr(GString) tmp = g_string_new(""); for (guint i = 0; i < 64; i++) { guint64 flag = (guint64)1 << i; if (flag == FU_FIRMWARE_FLAG_DONE_PARSE) continue; if ((priv->flags & flag) == 0) continue; g_string_append_printf(tmp, "%s|", fu_firmware_flag_to_string(flag)); } if (tmp->len > 0) { g_string_truncate(tmp, tmp->len - 1); fu_xmlb_builder_insert_kv(bn, "flags", tmp->str); } } fu_xmlb_builder_insert_kv(bn, "id", priv->id); fu_xmlb_builder_insert_kx(bn, "idx", priv->idx); fu_xmlb_builder_insert_kv(bn, "version", priv->version); fu_xmlb_builder_insert_kx(bn, "version_raw", priv->version_raw); if (priv->version_format != FWUPD_VERSION_FORMAT_UNKNOWN) { fu_xmlb_builder_insert_kv(bn, "version_format", fwupd_version_format_to_string(priv->version_format)); } fu_xmlb_builder_insert_kx(bn, "addr", priv->addr); fu_xmlb_builder_insert_kx(bn, "offset", priv->offset); fu_xmlb_builder_insert_kx(bn, "alignment", priv->alignment); fu_xmlb_builder_insert_kx(bn, "size", priv->size); fu_xmlb_builder_insert_kx(bn, "size_max", priv->size_max); fu_xmlb_builder_insert_kv(bn, "filename", priv->filename); if (priv->stream != NULL) { g_autofree gchar *dataszstr = g_strdup_printf("0x%x", (guint)priv->streamsz); g_autofree gchar *datastr = NULL; if (priv->streamsz <= 0x100) { g_autoptr(GByteArray) buf = fu_input_stream_read_byte_array(priv->stream, 0x0, priv->streamsz, NULL, NULL); if (buf != NULL) { if (flags & FU_FIRMWARE_EXPORT_FLAG_ASCII_DATA) { datastr = fu_memstrsafe(buf->data, buf->len, 0x0, MIN(buf->len, 0x100), NULL); } else { datastr = g_base64_encode(buf->data, buf->len); } } } xb_builder_node_insert_text(bn, "data", datastr, "type", "GInputStream", "size", dataszstr, NULL); } else if (priv->bytes != NULL && g_bytes_get_size(priv->bytes) == 0) { xb_builder_node_insert_text(bn, "data", NULL, "type", "GBytes", NULL); } else if (priv->bytes != NULL) { gsize bufsz = 0; const guint8 *buf = g_bytes_get_data(priv->bytes, &bufsz); g_autofree gchar *datastr = NULL; g_autofree gchar *dataszstr = g_strdup_printf("0x%x", (guint)bufsz); if (flags & FU_FIRMWARE_EXPORT_FLAG_ASCII_DATA) { datastr = fu_memstrsafe(buf, bufsz, 0x0, MIN(bufsz, 0x100), NULL); } else { datastr = g_base64_encode(buf, bufsz); } xb_builder_node_insert_text(bn, "data", datastr, "type", "GBytes", "size", dataszstr, NULL); } /* chunks */ if (priv->chunks != NULL && priv->chunks->len > 0) { g_autoptr(XbBuilderNode) bp = xb_builder_node_insert(bn, "chunks", NULL); for (guint i = 0; i < priv->chunks->len; i++) { FuChunk *chk = g_ptr_array_index(priv->chunks, i); g_autoptr(XbBuilderNode) bc = xb_builder_node_insert(bp, "chunk", NULL); fu_chunk_export(chk, flags, bc); } } /* vfunc */ if (klass->export != NULL) klass->export(self, flags, bn); /* children */ if (priv->images->len > 0) { for (guint i = 0; i < priv->images->len; i++) { FuFirmware *img = g_ptr_array_index(priv->images, i); g_autoptr(XbBuilderNode) bc = xb_builder_node_insert(bn, "firmware", NULL); fu_firmware_export(img, flags, bc); } } } /** * fu_firmware_export_to_xml: * @self: a #FuFirmware * @flags: firmware export flags, e.g. %FU_FIRMWARE_EXPORT_FLAG_INCLUDE_DEBUG * @error: (nullable): optional return location for an error * * This allows us to build an XML object for the nested firmware. * * Returns: a string value, or %NULL for invalid. * * Since: 1.6.0 **/ gchar * fu_firmware_export_to_xml(FuFirmware *self, FuFirmwareExportFlags flags, GError **error) { g_autoptr(XbBuilderNode) bn = xb_builder_node_new("firmware"); fu_firmware_export(self, flags, bn); return xb_builder_node_export(bn, XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE | XB_NODE_EXPORT_FLAG_COLLAPSE_EMPTY | XB_NODE_EXPORT_FLAG_FORMAT_INDENT, error); } /** * fu_firmware_to_string: * @self: a #FuFirmware * * This allows us to easily print the object. * * Returns: a string value, or %NULL for invalid. * * Since: 1.3.1 **/ gchar * fu_firmware_to_string(FuFirmware *self) { g_autoptr(XbBuilderNode) bn = xb_builder_node_new("firmware"); fu_firmware_export(self, FU_FIRMWARE_EXPORT_FLAG_INCLUDE_DEBUG | FU_FIRMWARE_EXPORT_FLAG_ASCII_DATA, bn); return xb_builder_node_export(bn, XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE | XB_NODE_EXPORT_FLAG_COLLAPSE_EMPTY | XB_NODE_EXPORT_FLAG_FORMAT_INDENT, NULL); } static void fu_firmware_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuFirmware *self = FU_FIRMWARE(object); FuFirmwarePrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_PARENT: g_value_set_object(value, priv->parent); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_firmware_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuFirmware *self = FU_FIRMWARE(object); switch (prop_id) { case PROP_PARENT: fu_firmware_set_parent(self, g_value_get_object(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_firmware_init(FuFirmware *self) { FuFirmwarePrivate *priv = GET_PRIVATE(self); priv->images = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); } static void fu_firmware_finalize(GObject *object) { FuFirmware *self = FU_FIRMWARE(object); FuFirmwarePrivate *priv = GET_PRIVATE(self); g_free(priv->version); g_free(priv->id); g_free(priv->filename); if (priv->bytes != NULL) g_bytes_unref(priv->bytes); if (priv->stream != NULL) g_object_unref(priv->stream); if (priv->chunks != NULL) g_ptr_array_unref(priv->chunks); if (priv->patches != NULL) g_ptr_array_unref(priv->patches); if (priv->parent != NULL) g_object_remove_weak_pointer(G_OBJECT(priv->parent), (gpointer *)&priv->parent); g_ptr_array_unref(priv->images); G_OBJECT_CLASS(fu_firmware_parent_class)->finalize(object); } static void fu_firmware_class_init(FuFirmwareClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); GParamSpec *pspec; object_class->finalize = fu_firmware_finalize; object_class->get_property = fu_firmware_get_property; object_class->set_property = fu_firmware_set_property; /** * FuFirmware:parent: * * The firmware parent. * * Since: 1.8.2 */ pspec = g_param_spec_object("parent", NULL, NULL, FU_TYPE_FIRMWARE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_PARENT, pspec); } /** * fu_firmware_new: * * Creates an empty firmware object. * * Returns: a #FuFirmware * * Since: 1.3.1 **/ FuFirmware * fu_firmware_new(void) { FuFirmware *self = g_object_new(FU_TYPE_FIRMWARE, NULL); return FU_FIRMWARE(self); } /** * fu_firmware_new_from_bytes: * @fw: firmware blob image * * Creates a firmware object with the provided image set as default. * * Returns: a #FuFirmware * * Since: 1.3.1 **/ FuFirmware * fu_firmware_new_from_bytes(GBytes *fw) { FuFirmware *self = fu_firmware_new(); fu_firmware_set_bytes(self, fw); return self; } /** * fu_firmware_new_from_gtypes: * @stream: a #GInputStream * @offset: start offset, useful for ignoring a bootloader * @flags: install flags, e.g. %FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM * @error: (nullable): optional return location for an error * @...: an array of #GTypes, ending with %G_TYPE_INVALID * * Tries to parse the firmware with each #GType in order. * * Returns: (transfer full) (nullable): a #FuFirmware, or %NULL * * Since: 1.5.6 **/ FuFirmware * fu_firmware_new_from_gtypes(GInputStream *stream, gsize offset, FuFirmwareParseFlags flags, GError **error, ...) { va_list args; g_autoptr(GArray) gtypes = g_array_new(FALSE, FALSE, sizeof(GType)); g_autoptr(GError) error_all = NULL; g_return_val_if_fail(G_IS_INPUT_STREAM(stream), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* create array of GTypes */ va_start(args, error); while (TRUE) { GType gtype = va_arg(args, GType); if (gtype == G_TYPE_INVALID) break; g_array_append_val(gtypes, gtype); } va_end(args); /* invalid */ if (gtypes->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "no GTypes specified"); return NULL; } /* try each GType in turn */ for (guint i = 0; i < gtypes->len; i++) { GType gtype = g_array_index(gtypes, GType, i); g_autoptr(FuFirmware) firmware = g_object_new(gtype, NULL); g_autoptr(GError) error_local = NULL; if (!fu_firmware_parse_stream(firmware, stream, offset, flags, &error_local)) { g_debug("%s", error_local->message); if (error_all == NULL) { g_propagate_error(&error_all, g_steal_pointer(&error_local)); } else { g_prefix_error(&error_all, "%s: ", error_local->message); } continue; } return g_steal_pointer(&firmware); } /* failed */ g_propagate_error(error, g_steal_pointer(&error_all)); return NULL; } fwupd-2.0.10/libfwupdplugin/fu-firmware.h000066400000000000000000000337751501337203100203610ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include #include "fu-chunk.h" #include "fu-firmware.h" #define FU_TYPE_FIRMWARE (fu_firmware_get_type()) G_DECLARE_DERIVABLE_TYPE(FuFirmware, fu_firmware, FU, FIRMWARE, GObject) /** * FuFirmwareExportFlags: * * The firmware export flags. **/ typedef enum { /** * FU_FIRMWARE_EXPORT_FLAG_NONE: * * No flags set. * * Since: 1.6.0 **/ FU_FIRMWARE_EXPORT_FLAG_NONE = 0u, /** * FU_FIRMWARE_EXPORT_FLAG_INCLUDE_DEBUG: * * Include debug information when exporting. * * Since: 1.6.0 **/ FU_FIRMWARE_EXPORT_FLAG_INCLUDE_DEBUG = 1u << 0, /** * FU_FIRMWARE_EXPORT_FLAG_ASCII_DATA: * * Write the data as UTF-8 strings. * * Since: 1.6.0 **/ FU_FIRMWARE_EXPORT_FLAG_ASCII_DATA = 1u << 1, /** * FU_FIRMWARE_EXPORT_FLAG_UNKNOWN: * * Unknown flag value. * * Since: 2.0.0 */ FU_FIRMWARE_EXPORT_FLAG_UNKNOWN = G_MAXUINT64, } FuFirmwareExportFlags; /** * FuFirmwareParseFlags: * * The firmware parse flags. **/ typedef enum { /** * FWUPD_INSTALL_FLAG_NONE: * * No flags set. * * Since: 2.0.9 */ FU_FIRMWARE_PARSE_FLAG_NONE = 0, /** * FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM: * * Ignore firmware CRCs and checksums. * * Since: 2.0.9 */ FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM = 1 << 6, /** * FU_FIRMWARE_PARSE_FLAG_IGNORE_VID_PID: * * Ignore firmware vendor and project checks. * * Since: 2.0.9 */ FU_FIRMWARE_PARSE_FLAG_IGNORE_VID_PID = 1 << 7, /** * FU_FIRMWARE_PARSE_FLAG_NO_SEARCH: * * Do not use heuristics when parsing the image. * * Since: 2.0.9 */ FU_FIRMWARE_PARSE_FLAG_NO_SEARCH = 1 << 8, /** * FU_FIRMWARE_PARSE_FLAG_CACHE_STREAM: * * Keep a reference to the parsed stream. * * Since: 2.0.9 */ FU_FIRMWARE_PARSE_FLAG_CACHE_STREAM = 1 << 10, /** * FU_FIRMWARE_PARSE_FLAG_CACHE_BLOB: * * Keep a reference to the parsed blob in-memory. * * This allows the stream to be closed even when the firmware needs to be re-parsed. * * Since: 2.0.9 */ FU_FIRMWARE_PARSE_FLAG_CACHE_BLOB = 1 << 11, } FuFirmwareParseFlags; struct _FuFirmwareClass { GObjectClass parent_class; gboolean (*parse)(FuFirmware *self, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT; GByteArray *(*write)(FuFirmware *self, GError **error)G_GNUC_WARN_UNUSED_RESULT; void (*export)(FuFirmware *self, FuFirmwareExportFlags flags, XbBuilderNode *bn); gboolean (*tokenize)(FuFirmware *self, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT; gboolean (*build)(FuFirmware *self, XbNode *n, GError **error) G_GNUC_WARN_UNUSED_RESULT; gchar *(*get_checksum)(FuFirmware *self, GChecksumType csum_kind, GError **error)G_GNUC_WARN_UNUSED_RESULT; gboolean (*validate)(FuFirmware *self, GInputStream *stream, gsize offset, GError **error); gboolean (*check_compatible)(FuFirmware *self, FuFirmware *other, FuFirmwareParseFlags flags, GError **error); gchar *(*convert_version)(FuFirmware *self, guint64 version_raw); }; /** * FuFirmwareFlags: * * The firmware flags. **/ typedef enum { /** * FU_FIRMWARE_FLAG_NONE: * * No flags set. * * Since: 1.5.0 **/ FU_FIRMWARE_FLAG_NONE = 0u, /** * FU_FIRMWARE_FLAG_DEDUPE_ID: * * Dedupe images by ID. * * Since: 1.5.0 **/ FU_FIRMWARE_FLAG_DEDUPE_ID = 1u << 0, /** * FU_FIRMWARE_FLAG_DEDUPE_IDX: * * Dedupe images by IDX. * * Since: 1.5.0 **/ FU_FIRMWARE_FLAG_DEDUPE_IDX = 1u << 1, /** * FU_FIRMWARE_FLAG_HAS_CHECKSUM: * * Has a CRC or checksum to test internal consistency. * * Since: 1.5.6 **/ FU_FIRMWARE_FLAG_HAS_CHECKSUM = 1u << 2, /** * FU_FIRMWARE_FLAG_HAS_VID_PID: * * Has a vendor or product ID in the firmware. * * Since: 1.5.6 **/ FU_FIRMWARE_FLAG_HAS_VID_PID = 1u << 3, /** * FU_FIRMWARE_FLAG_DONE_PARSE: * * The firmware object has been used by fu_firmware_parse_bytes(). * * Since: 1.7.3 **/ FU_FIRMWARE_FLAG_DONE_PARSE = 1u << 4, /** * FU_FIRMWARE_FLAG_HAS_STORED_SIZE: * * Encodes the image size in the firmware. * * Since: 1.8.2 **/ FU_FIRMWARE_FLAG_HAS_STORED_SIZE = 1u << 5, /** * FU_FIRMWARE_FLAG_ALWAYS_SEARCH: * * Always searches for magic regardless of the install flags. * This is useful for firmware that always has an *unparsed* variable-length * header. * * Since: 1.8.6 **/ FU_FIRMWARE_FLAG_ALWAYS_SEARCH = 1u << 6, /** * FU_FIRMWARE_FLAG_NO_AUTO_DETECTION: * * Do not use this firmware type when auto-detecting firmware. * This should be used when there is no valid signature or CRC to check validity when *parsing. * * Since: 1.9.3 **/ FU_FIRMWARE_FLAG_NO_AUTO_DETECTION = 1u << 7, /** * FU_FIRMWARE_FLAG_HAS_CHECK_COMPATIBLE: * * The firmware subclass implements a compatibility check. * * Since: 1.9.20 **/ FU_FIRMWARE_FLAG_HAS_CHECK_COMPATIBLE = 1u << 8, /** * FU_FIRMWARE_FLAG_UNKNOWN: * * Unknown flag value. * * Since: 2.0.0 */ FU_FIRMWARE_FLAG_UNKNOWN = G_MAXUINT64, } FuFirmwareFlags; /** * FU_FIRMWARE_ID_PAYLOAD: * * The usual firmware ID string for the payload. * * Since: 1.6.0 **/ #define FU_FIRMWARE_ID_PAYLOAD "payload" /** * FU_FIRMWARE_ID_SIGNATURE: * * The usual firmware ID string for the signature. * * Since: 1.6.0 **/ #define FU_FIRMWARE_ID_SIGNATURE "signature" /** * FU_FIRMWARE_ID_HEADER: * * The usual firmware ID string for the header. * * Since: 1.6.0 **/ #define FU_FIRMWARE_ID_HEADER "header" /** * FuFirmwareAlignment: * * The firmware alignment position. **/ typedef enum { FU_FIRMWARE_ALIGNMENT_1 = 0x00, FU_FIRMWARE_ALIGNMENT_2 = 0x01, FU_FIRMWARE_ALIGNMENT_4 = 0x02, FU_FIRMWARE_ALIGNMENT_8 = 0x03, FU_FIRMWARE_ALIGNMENT_16 = 0x04, FU_FIRMWARE_ALIGNMENT_32 = 0x05, FU_FIRMWARE_ALIGNMENT_64 = 0x06, FU_FIRMWARE_ALIGNMENT_128 = 0x07, FU_FIRMWARE_ALIGNMENT_256 = 0x08, FU_FIRMWARE_ALIGNMENT_512 = 0x09, FU_FIRMWARE_ALIGNMENT_1K = 0x0A, FU_FIRMWARE_ALIGNMENT_2K = 0x0B, FU_FIRMWARE_ALIGNMENT_4K = 0x0C, FU_FIRMWARE_ALIGNMENT_8K = 0x0D, FU_FIRMWARE_ALIGNMENT_16K = 0x0E, FU_FIRMWARE_ALIGNMENT_32K = 0x0F, FU_FIRMWARE_ALIGNMENT_64K = 0x10, FU_FIRMWARE_ALIGNMENT_128K = 0x11, FU_FIRMWARE_ALIGNMENT_256K = 0x12, FU_FIRMWARE_ALIGNMENT_512K = 0x13, FU_FIRMWARE_ALIGNMENT_1M = 0x14, FU_FIRMWARE_ALIGNMENT_2M = 0x15, FU_FIRMWARE_ALIGNMENT_4M = 0x16, FU_FIRMWARE_ALIGNMENT_8M = 0x17, FU_FIRMWARE_ALIGNMENT_16M = 0x18, FU_FIRMWARE_ALIGNMENT_32M = 0x19, FU_FIRMWARE_ALIGNMENT_64M = 0x1A, FU_FIRMWARE_ALIGNMENT_128M = 0x1B, FU_FIRMWARE_ALIGNMENT_256M = 0x1C, FU_FIRMWARE_ALIGNMENT_512M = 0x1D, FU_FIRMWARE_ALIGNMENT_1G = 0x1E, FU_FIRMWARE_ALIGNMENT_2G = 0x1F, FU_FIRMWARE_ALIGNMENT_4G = 0x20, FU_FIRMWARE_ALIGNMENT_LAST, } FuFirmwareAlignment; #define FU_FIRMWARE_SEARCH_MAGIC_BUFSZ_MAX (32 * 1024 * 1024) const gchar * fu_firmware_flag_to_string(FuFirmwareFlags flag); FuFirmwareFlags fu_firmware_flag_from_string(const gchar *flag); FuFirmware * fu_firmware_new(void); FuFirmware * fu_firmware_new_from_bytes(GBytes *fw); FuFirmware * fu_firmware_new_from_gtypes(GInputStream *stream, gsize offset, FuFirmwareParseFlags flags, GError **error, ...) G_GNUC_NON_NULL(1); gchar * fu_firmware_to_string(FuFirmware *self) G_GNUC_NON_NULL(1); void fu_firmware_export(FuFirmware *self, FuFirmwareExportFlags flags, XbBuilderNode *bn) G_GNUC_NON_NULL(1, 3); gchar * fu_firmware_export_to_xml(FuFirmware *self, FuFirmwareExportFlags flags, GError **error) G_GNUC_NON_NULL(1); const gchar * fu_firmware_get_version(FuFirmware *self) G_GNUC_NON_NULL(1); void fu_firmware_set_version(FuFirmware *self, const gchar *version) G_GNUC_NON_NULL(1); guint64 fu_firmware_get_version_raw(FuFirmware *self) G_GNUC_NON_NULL(1); void fu_firmware_set_version_raw(FuFirmware *self, guint64 version_raw) G_GNUC_NON_NULL(1); void fu_firmware_set_version_format(FuFirmware *self, FwupdVersionFormat version_format) G_GNUC_NON_NULL(1); FwupdVersionFormat fu_firmware_get_version_format(FuFirmware *self) G_GNUC_NON_NULL(1); void fu_firmware_add_flag(FuFirmware *firmware, FuFirmwareFlags flag) G_GNUC_NON_NULL(1); gboolean fu_firmware_has_flag(FuFirmware *firmware, FuFirmwareFlags flag) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); const gchar * fu_firmware_get_filename(FuFirmware *self) G_GNUC_NON_NULL(1); void fu_firmware_set_filename(FuFirmware *self, const gchar *filename) G_GNUC_NON_NULL(1); const gchar * fu_firmware_get_id(FuFirmware *self) G_GNUC_NON_NULL(1); void fu_firmware_set_id(FuFirmware *self, const gchar *id) G_GNUC_NON_NULL(1); guint64 fu_firmware_get_addr(FuFirmware *self) G_GNUC_NON_NULL(1); void fu_firmware_set_addr(FuFirmware *self, guint64 addr) G_GNUC_NON_NULL(1); guint64 fu_firmware_get_offset(FuFirmware *self) G_GNUC_NON_NULL(1); void fu_firmware_set_offset(FuFirmware *self, guint64 offset) G_GNUC_NON_NULL(1); gsize fu_firmware_get_size(FuFirmware *self) G_GNUC_NON_NULL(1); void fu_firmware_set_size(FuFirmware *self, gsize size) G_GNUC_NON_NULL(1); void fu_firmware_set_size_max(FuFirmware *self, gsize size_max) G_GNUC_NON_NULL(1); gsize fu_firmware_get_size_max(FuFirmware *self) G_GNUC_NON_NULL(1); void fu_firmware_set_images_max(FuFirmware *self, guint images_max) G_GNUC_NON_NULL(1); guint fu_firmware_get_images_max(FuFirmware *self) G_GNUC_NON_NULL(1); guint fu_firmware_get_depth(FuFirmware *self) G_GNUC_NON_NULL(1); guint64 fu_firmware_get_idx(FuFirmware *self) G_GNUC_NON_NULL(1); void fu_firmware_set_idx(FuFirmware *self, guint64 idx) G_GNUC_NON_NULL(1); GBytes * fu_firmware_get_bytes(FuFirmware *self, GError **error) G_GNUC_NON_NULL(1); GBytes * fu_firmware_get_bytes_with_patches(FuFirmware *self, GError **error) G_GNUC_NON_NULL(1); void fu_firmware_set_bytes(FuFirmware *self, GBytes *bytes) G_GNUC_NON_NULL(1); gboolean fu_firmware_set_stream(FuFirmware *self, GInputStream *stream, GError **error) G_GNUC_NON_NULL(1); GInputStream * fu_firmware_get_stream(FuFirmware *self, GError **error) G_GNUC_NON_NULL(1); FuFirmwareAlignment fu_firmware_get_alignment(FuFirmware *self) G_GNUC_NON_NULL(1); void fu_firmware_set_alignment(FuFirmware *self, FuFirmwareAlignment alignment) G_GNUC_NON_NULL(1); void fu_firmware_add_chunk(FuFirmware *self, FuChunk *chk) G_GNUC_NON_NULL(1); GPtrArray * fu_firmware_get_chunks(FuFirmware *self, GError **error) G_GNUC_NON_NULL(1); FuFirmware * fu_firmware_get_parent(FuFirmware *self) G_GNUC_NON_NULL(1); void fu_firmware_set_parent(FuFirmware *self, FuFirmware *parent) G_GNUC_NON_NULL(1); gboolean fu_firmware_tokenize(FuFirmware *self, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_firmware_build(FuFirmware *self, XbNode *n, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_firmware_build_from_xml(FuFirmware *self, const gchar *xml, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_firmware_build_from_filename(FuFirmware *self, const gchar *filename, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_firmware_parse_stream(FuFirmware *self, GInputStream *stream, gsize offset, FuFirmwareParseFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_firmware_parse_file(FuFirmware *self, GFile *file, FuFirmwareParseFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_firmware_parse_bytes(FuFirmware *self, GBytes *fw, gsize offset, FuFirmwareParseFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); GBytes * fu_firmware_write(FuFirmware *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); GBytes * fu_firmware_write_chunk(FuFirmware *self, guint64 address, guint64 chunk_sz_max, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_firmware_write_file(FuFirmware *self, GFile *file, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gchar * fu_firmware_get_checksum(FuFirmware *self, GChecksumType csum_kind, GError **error) G_GNUC_NON_NULL(1); gboolean fu_firmware_check_compatible(FuFirmware *self, FuFirmware *other, FuFirmwareParseFlags flags, GError **error) G_GNUC_NON_NULL(1, 2); void fu_firmware_add_image(FuFirmware *self, FuFirmware *img) G_GNUC_NON_NULL(1, 2); gboolean fu_firmware_add_image_full(FuFirmware *self, FuFirmware *img, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_firmware_remove_image(FuFirmware *self, FuFirmware *img, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_firmware_remove_image_by_idx(FuFirmware *self, guint64 idx, GError **error) G_GNUC_NON_NULL(1); gboolean fu_firmware_remove_image_by_id(FuFirmware *self, const gchar *id, GError **error) G_GNUC_NON_NULL(1); GPtrArray * fu_firmware_get_images(FuFirmware *self) G_GNUC_NON_NULL(1); FuFirmware * fu_firmware_get_image_by_id(FuFirmware *self, const gchar *id, GError **error) G_GNUC_NON_NULL(1); GBytes * fu_firmware_get_image_by_id_bytes(FuFirmware *self, const gchar *id, GError **error) G_GNUC_NON_NULL(1); GInputStream * fu_firmware_get_image_by_id_stream(FuFirmware *self, const gchar *id, GError **error) G_GNUC_NON_NULL(1); FuFirmware * fu_firmware_get_image_by_idx(FuFirmware *self, guint64 idx, GError **error) G_GNUC_NON_NULL(1); GBytes * fu_firmware_get_image_by_idx_bytes(FuFirmware *self, guint64 idx, GError **error) G_GNUC_NON_NULL(1); GInputStream * fu_firmware_get_image_by_idx_stream(FuFirmware *self, guint64 idx, GError **error) G_GNUC_NON_NULL(1); FuFirmware * fu_firmware_get_image_by_gtype(FuFirmware *self, GType gtype, GError **error) G_GNUC_NON_NULL(1); GBytes * fu_firmware_get_image_by_gtype_bytes(FuFirmware *self, GType gtype, GError **error) G_GNUC_NON_NULL(1); FuFirmware * fu_firmware_get_image_by_checksum(FuFirmware *self, const gchar *checksum, GError **error) G_GNUC_NON_NULL(1, 2); void fu_firmware_add_patch(FuFirmware *self, gsize offset, GBytes *blob) G_GNUC_NON_NULL(1, 3); fwupd-2.0.10/libfwupdplugin/fu-fit-firmware.c000066400000000000000000000230211501337203100211130ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-crc.h" #include "fu-dump.h" #include "fu-fdt-image.h" #include "fu-fit-firmware.h" #include "fu-input-stream.h" #include "fu-mem.h" /** * FuFitFirmware: * * A Flat Image Tree. * * Documented: * https://github.com/u-boot/u-boot/blob/master/doc/uImage.FIT/source_file_format.txt * * See also: [class@FuFdtFirmware] */ G_DEFINE_TYPE(FuFitFirmware, fu_fit_firmware, FU_TYPE_FDT_FIRMWARE) static FuFdtImage * fu_fit_firmware_get_image_root(FuFitFirmware *self) { FuFirmware *img = fu_firmware_get_image_by_id(FU_FIRMWARE(self), NULL, NULL); if (img != NULL) return FU_FDT_IMAGE(img); img = fu_fdt_image_new(); fu_fdt_image_set_attr_uint32(FU_FDT_IMAGE(img), FU_FIT_FIRMWARE_ATTR_TIMESTAMP, 0x0); fu_fdt_image_set_attr_str(FU_FDT_IMAGE(img), "description", "Firmware image"); fu_fdt_image_set_attr_str(FU_FDT_IMAGE(img), "creator", "fwupd"); fu_firmware_add_image(FU_FIRMWARE(self), img); return FU_FDT_IMAGE(img); } /** * fu_fit_firmware_get_timestamp: * @self: a #FuFitFirmware * * Gets the creation timestamp. * * Returns: integer * * Since: 1.8.2 **/ guint32 fu_fit_firmware_get_timestamp(FuFitFirmware *self) { guint32 tmp = 0; g_autoptr(FuFdtImage) img_root = fu_fit_firmware_get_image_root(self); g_return_val_if_fail(FU_IS_FIT_FIRMWARE(self), 0x0); /* this has to exist */ (void)fu_fdt_image_get_attr_u32(img_root, FU_FIT_FIRMWARE_ATTR_TIMESTAMP, &tmp, NULL); return tmp; } /** * fu_fit_firmware_set_timestamp: * @self: a #FuFitFirmware * @timestamp: integer value * * Sets the creation timestamp. * * Since: 1.8.2 **/ void fu_fit_firmware_set_timestamp(FuFitFirmware *self, guint32 timestamp) { g_autoptr(FuFdtImage) img_root = fu_fit_firmware_get_image_root(self); g_return_if_fail(FU_IS_FIT_FIRMWARE(self)); fu_fdt_image_set_attr_uint32(img_root, FU_FIT_FIRMWARE_ATTR_TIMESTAMP, timestamp); } static gboolean fu_fit_firmware_verify_crc32(FuFirmware *firmware, FuFirmware *img, FuFirmware *img_hash, GBytes *blob, GError **error) { guint32 value = 0; guint32 value_calc; /* get value and verify */ if (!fu_fdt_image_get_attr_u32(FU_FDT_IMAGE(img_hash), FU_FIT_FIRMWARE_ATTR_VALUE, &value, error)) return FALSE; value_calc = fu_crc32_bytes(FU_CRC_KIND_B32_STANDARD, blob); if (value_calc != value) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "%s CRC did not match, got 0x%x, expected 0x%x", fu_firmware_get_id(img), value, value_calc); return FALSE; } /* success */ fu_firmware_add_flag(firmware, FU_FIRMWARE_FLAG_HAS_CHECKSUM); return TRUE; } static gboolean fu_fit_firmware_verify_checksum(FuFirmware *firmware, FuFirmware *img, FuFirmware *img_hash, GChecksumType checksum_type, GBytes *blob, GError **error) { gsize digest_len = g_checksum_type_get_length(checksum_type); g_autofree guint8 *buf = g_malloc0(digest_len); g_autoptr(GBytes) value = NULL; g_autoptr(GBytes) value_calc = NULL; g_autoptr(GChecksum) checksum = g_checksum_new(checksum_type); /* get value and verify */ value = fu_fdt_image_get_attr(FU_FDT_IMAGE(img_hash), FU_FIT_FIRMWARE_ATTR_VALUE, error); if (value == NULL) return FALSE; if (g_bytes_get_size(value) != digest_len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "%s invalid hash value size, got 0x%x, expected 0x%x", fu_firmware_get_id(img), (guint)g_bytes_get_size(value), (guint)digest_len); return FALSE; } g_checksum_update(checksum, (const guchar *)g_bytes_get_data(value, NULL), g_bytes_get_size(value)); g_checksum_get_digest(checksum, buf, &digest_len); value_calc = g_bytes_new(buf, digest_len); if (!fu_bytes_compare(value, value_calc, error)) return FALSE; /* success */ fu_firmware_add_flag(firmware, FU_FIRMWARE_FLAG_HAS_CHECKSUM); return TRUE; } static gboolean fu_fit_firmware_verify_hash(FuFirmware *firmware, FuFirmware *img, FuFirmware *img_hash, GBytes *blob, GError **error) { g_autofree gchar *algo = NULL; /* what is this */ if (!fu_fdt_image_get_attr_str(FU_FDT_IMAGE(img_hash), FU_FIT_FIRMWARE_ATTR_ALGO, &algo, error)) { g_prefix_error(error, "cannot get algo for %s: ", fu_firmware_get_id(img)); return FALSE; } if (g_strcmp0(algo, "crc32") == 0) return fu_fit_firmware_verify_crc32(firmware, img, img_hash, blob, error); if (g_strcmp0(algo, "md5") == 0) { return fu_fit_firmware_verify_checksum(firmware, img, img_hash, G_CHECKSUM_MD5, blob, error); } if (g_strcmp0(algo, "sha1") == 0) { return fu_fit_firmware_verify_checksum(firmware, img, img_hash, G_CHECKSUM_SHA1, blob, error); } if (g_strcmp0(algo, "sha256") == 0) { return fu_fit_firmware_verify_checksum(firmware, img, img_hash, G_CHECKSUM_SHA256, blob, error); } /* ignore any hashes we do not support: success */ return TRUE; } static gboolean fu_fit_firmware_verify_image(FuFirmware *firmware, GInputStream *stream, FuFirmware *img, FuFirmwareParseFlags flags, GError **error) { g_autoptr(GBytes) blob = NULL; /* sanity check */ if (!fu_fdt_image_get_attr_str(FU_FDT_IMAGE(img), "type", NULL, error)) return FALSE; if (!fu_fdt_image_get_attr_str(FU_FDT_IMAGE(img), "description", NULL, error)) return FALSE; /* if has data */ blob = fu_fdt_image_get_attr(FU_FDT_IMAGE(img), FU_FIT_FIRMWARE_ATTR_DATA, NULL); if (blob == NULL) { guint32 data_size = 0x0; guint32 data_offset = 0x0; /* extra data outside of FIT image */ if (!fu_fdt_image_get_attr_u32(FU_FDT_IMAGE(img), FU_FIT_FIRMWARE_ATTR_DATA_OFFSET, &data_offset, error)) return FALSE; if (!fu_fdt_image_get_attr_u32(FU_FDT_IMAGE(img), FU_FIT_FIRMWARE_ATTR_DATA_SIZE, &data_size, error)) return FALSE; blob = fu_input_stream_read_bytes(stream, data_offset, data_size, NULL, error); if (blob == NULL) return FALSE; } fu_dump_bytes(G_LOG_DOMAIN, "data", blob); /* verify any hashes we recognize */ if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { g_autoptr(GPtrArray) img_hashes = fu_firmware_get_images(img); for (guint i = 0; i < img_hashes->len; i++) { FuFirmware *img_hash = g_ptr_array_index(img_hashes, i); if (fu_firmware_get_id(img_hash) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no ID for image hash"); return FALSE; } if (g_str_has_prefix(fu_firmware_get_id(img_hash), "hash")) { if (!fu_fit_firmware_verify_hash(firmware, img, img_hash, blob, error)) return FALSE; } } } /* success */ return TRUE; } static gboolean fu_fit_firmware_verify_configuration(FuFirmware *firmware, FuFirmware *img, FuFirmwareParseFlags flags, GError **error) { /* sanity check */ if (!fu_fdt_image_get_attr_strlist(FU_FDT_IMAGE(img), FU_FIT_FIRMWARE_ATTR_COMPATIBLE, NULL, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_fit_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { g_autoptr(FuFirmware) img_cfgs = NULL; g_autoptr(FuFirmware) img_images = NULL; g_autoptr(FuFirmware) img_root = NULL; g_autoptr(GPtrArray) img_images_array = NULL; g_autoptr(GPtrArray) img_cfgs_array = NULL; /* FuFdtFirmware->parse */ if (!FU_FIRMWARE_CLASS(fu_fit_firmware_parent_class)->parse(firmware, stream, flags, error)) return FALSE; /* sanity check */ img_root = fu_firmware_get_image_by_id(firmware, NULL, error); if (img_root == NULL) return FALSE; if (!fu_fdt_image_get_attr_u32(FU_FDT_IMAGE(img_root), FU_FIT_FIRMWARE_ATTR_TIMESTAMP, NULL, error)) return FALSE; /* check the checksums of each image */ img_images = fu_firmware_get_image_by_id(img_root, FU_FIT_FIRMWARE_ID_IMAGES, error); if (img_images == NULL) return FALSE; img_images_array = fu_firmware_get_images(img_images); for (guint i = 0; i < img_images_array->len; i++) { FuFirmware *img = g_ptr_array_index(img_images_array, i); if (!fu_fit_firmware_verify_image(firmware, stream, img, flags, error)) return FALSE; } /* check the setup of each configuration */ img_cfgs = fu_firmware_get_image_by_id(img_root, FU_FIT_FIRMWARE_ID_CONFIGURATIONS, error); if (img_cfgs == NULL) return FALSE; img_cfgs_array = fu_firmware_get_images(img_cfgs); for (guint i = 0; i < img_cfgs_array->len; i++) { FuFirmware *img = g_ptr_array_index(img_cfgs_array, i); if (!fu_fit_firmware_verify_configuration(firmware, img, flags, error)) return FALSE; } /* success */ return TRUE; } static void fu_fit_firmware_init(FuFitFirmware *self) { fu_firmware_set_images_max(FU_FIRMWARE(self), 1024); } static void fu_fit_firmware_class_init(FuFitFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_fit_firmware_parse; } /** * fu_fit_firmware_new: * * Creates a new #FuFirmware of sub type FIT * * Since: 1.8.2 **/ FuFirmware * fu_fit_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_FIT_FIRMWARE, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-fit-firmware.h000066400000000000000000000053261501337203100211300ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-fdt-firmware.h" #define FU_TYPE_FIT_FIRMWARE (fu_fit_firmware_get_type()) G_DECLARE_DERIVABLE_TYPE(FuFitFirmware, fu_fit_firmware, FU, FIT_FIRMWARE, FuFdtFirmware) struct _FuFitFirmwareClass { FuFdtFirmwareClass parent_class; }; FuFirmware * fu_fit_firmware_new(void); guint32 fu_fit_firmware_get_timestamp(FuFitFirmware *self) G_GNUC_NON_NULL(1); void fu_fit_firmware_set_timestamp(FuFitFirmware *self, guint32 timestamp) G_GNUC_NON_NULL(1); /** * FU_FIT_FIRMWARE_ATTR_COMPATIBLE: * * The compatible metadata for the FIT image, typically a string list, e.g. *`pine64,rockpro64-v2.1:pine64,rockpro64`. * * Since: 1.8.2 **/ #define FU_FIT_FIRMWARE_ATTR_COMPATIBLE "compatible" /** * FU_FIT_FIRMWARE_ATTR_DATA: * * The raw data for the FIT image, typically a blob. * * Since: 1.8.2 **/ #define FU_FIT_FIRMWARE_ATTR_DATA "data" /** * FU_FIT_FIRMWARE_ATTR_ALGO: * * The checksum algorithm for the FIT image, typically a string, e.g. `crc32`. * * Since: 1.8.2 **/ #define FU_FIT_FIRMWARE_ATTR_ALGO "algo" /** * FU_FIT_FIRMWARE_ATTR_DATA_OFFSET: * * The external data offset after the FIT image, typically a uint32. * * Since: 1.8.2 **/ #define FU_FIT_FIRMWARE_ATTR_DATA_OFFSET "data-offset" /** * FU_FIT_FIRMWARE_ATTR_DATA_SIZE: * * The data size of the external image, typically a uint32. * * Since: 1.8.2 **/ #define FU_FIT_FIRMWARE_ATTR_DATA_SIZE "data-size" /** * FU_FIT_FIRMWARE_ATTR_STORE_OFFSET: * * The store offset for the FIT image, typically a uint32. * * Since: 1.8.2 **/ #define FU_FIT_FIRMWARE_ATTR_STORE_OFFSET "store-offset" /** * FU_FIT_FIRMWARE_ATTR_VALUE: * * The value of the checksum, which is typically a blob. * * Since: 1.8.2 **/ #define FU_FIT_FIRMWARE_ATTR_VALUE "value" /** * FU_FIT_FIRMWARE_ATTR_SKIP_OFFSET: * * The offset to skip when writing the FIT image, typically a uint32. * * Since: 1.8.2 **/ #define FU_FIT_FIRMWARE_ATTR_SKIP_OFFSET "skip-offset" /** * FU_FIT_FIRMWARE_ATTR_VERSION: * * The version of the FIT image, typically a string, e.g. `1.2.3`. * * Since: 1.8.2 **/ #define FU_FIT_FIRMWARE_ATTR_VERSION "version" /** * FU_FIT_FIRMWARE_ATTR_TIMESTAMP: * * The creation timestamp of FIT image, typically a uint32. * * Since: 1.8.2 **/ #define FU_FIT_FIRMWARE_ATTR_TIMESTAMP "timestamp" /** * FU_FIT_FIRMWARE_ID_IMAGES: * * The usual firmware ID string for the images. * * Since: 1.8.2 **/ #define FU_FIT_FIRMWARE_ID_IMAGES "images" /** * FU_FIT_FIRMWARE_ID_CONFIGURATIONS: * * The usual firmware ID string for the configurations. * * Since: 1.8.2 **/ #define FU_FIT_FIRMWARE_ID_CONFIGURATIONS "configurations" fwupd-2.0.10/libfwupdplugin/fu-fmap-firmware.c000066400000000000000000000130121501337203100212530ustar00rootroot00000000000000/* * Copyright 2020 Benson Leung * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-fmap-firmware.h" #include "fu-fmap-struct.h" #include "fu-input-stream.h" #include "fu-partial-input-stream.h" /** * FuFmapFirmware: * * A FMAP firmware image. * * See also: [class@FuFirmware] */ #define FMAP_AREANAME "FMAP" G_DEFINE_TYPE(FuFmapFirmware, fu_fmap_firmware, FU_TYPE_FIRMWARE) static gboolean fu_fmap_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { gsize offset = 0; gsize streamsz = 0; guint32 nareas; g_autoptr(GByteArray) st_hdr = NULL; /* find the magic token */ if (!fu_input_stream_find(stream, (const guint8 *)FU_STRUCT_FMAP_DEFAULT_SIGNATURE, FU_STRUCT_FMAP_SIZE_SIGNATURE, &offset, error)) return FALSE; /* parse */ st_hdr = fu_struct_fmap_parse_stream(stream, offset, error); if (st_hdr == NULL) return FALSE; fu_firmware_set_addr(firmware, fu_struct_fmap_get_base(st_hdr)); if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (fu_struct_fmap_get_size(st_hdr) != streamsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "file size incorrect, expected 0x%04x got 0x%04x", fu_struct_fmap_get_size(st_hdr), (guint)streamsz); return FALSE; } nareas = fu_struct_fmap_get_nareas(st_hdr); if (nareas < 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "number of areas invalid"); return FALSE; } offset += st_hdr->len; for (gsize i = 0; i < nareas; i++) { guint32 area_offset; guint32 area_size; g_autofree gchar *area_name = NULL; g_autoptr(FuFirmware) img = fu_firmware_new(); g_autoptr(GByteArray) st_area = NULL; g_autoptr(GInputStream) img_stream = NULL; /* load area */ st_area = fu_struct_fmap_area_parse_stream(stream, offset, error); if (st_area == NULL) return FALSE; area_size = fu_struct_fmap_area_get_size(st_area); if (area_size == 0) continue; area_offset = fu_struct_fmap_area_get_offset(st_area); img_stream = fu_partial_input_stream_new(stream, (gsize)area_offset, (gsize)area_size, error); if (img_stream == NULL) { g_prefix_error(error, "failed to cut FMAP area: "); return FALSE; } if (!fu_firmware_parse_stream(img, img_stream, 0x0, flags, error)) return FALSE; area_name = fu_struct_fmap_area_get_name(st_area); fu_firmware_set_id(img, area_name); fu_firmware_set_idx(img, i + 1); fu_firmware_set_addr(img, area_offset); if (!fu_firmware_add_image_full(firmware, img, error)) return FALSE; if (g_strcmp0(area_name, FMAP_AREANAME) == 0) { g_autofree gchar *version = NULL; version = g_strdup_printf("%d.%d", fu_struct_fmap_get_ver_major(st_hdr), fu_struct_fmap_get_ver_minor(st_hdr)); fu_firmware_set_version(img, version); } offset += st_area->len; } /* success */ return TRUE; } static GByteArray * fu_fmap_firmware_write(FuFirmware *firmware, GError **error) { gsize total_sz; gsize offset; g_autoptr(GPtrArray) images = fu_firmware_get_images(firmware); g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GByteArray) st_hdr = fu_struct_fmap_new(); /* pad to offset */ if (fu_firmware_get_offset(firmware) > 0) fu_byte_array_set_size(buf, fu_firmware_get_offset(firmware), 0x00); /* add header */ total_sz = offset = st_hdr->len + (FU_STRUCT_FMAP_AREA_SIZE * images->len); for (guint i = 0; i < images->len; i++) { FuFirmware *img = g_ptr_array_index(images, i); g_autoptr(GBytes) fw = fu_firmware_get_bytes_with_patches(img, error); if (fw == NULL) return NULL; total_sz += g_bytes_get_size(fw); } /* header */ fu_struct_fmap_set_base(st_hdr, fu_firmware_get_addr(firmware)); fu_struct_fmap_set_nareas(st_hdr, images->len); fu_struct_fmap_set_size(st_hdr, fu_firmware_get_offset(firmware) + total_sz); g_byte_array_append(buf, st_hdr->data, st_hdr->len); /* add each area */ for (guint i = 0; i < images->len; i++) { FuFirmware *img = g_ptr_array_index(images, i); g_autoptr(GBytes) fw = fu_firmware_get_bytes_with_patches(img, NULL); g_autoptr(GByteArray) st_area = fu_struct_fmap_area_new(); fu_struct_fmap_area_set_offset(st_area, fu_firmware_get_offset(firmware) + offset); fu_struct_fmap_area_set_size(st_area, g_bytes_get_size(fw)); if (fu_firmware_get_id(img) != NULL) { if (!fu_struct_fmap_area_set_name(st_area, fu_firmware_get_id(img), error)) return NULL; } g_byte_array_append(buf, st_area->data, st_area->len); offset += g_bytes_get_size(fw); } /* add the images */ for (guint i = 0; i < images->len; i++) { FuFirmware *img = g_ptr_array_index(images, i); g_autoptr(GBytes) fw = fu_firmware_get_bytes_with_patches(img, error); if (fw == NULL) return NULL; fu_byte_array_append_bytes(buf, fw); } /* success */ return g_steal_pointer(&buf); } static void fu_fmap_firmware_init(FuFmapFirmware *self) { fu_firmware_set_images_max(FU_FIRMWARE(self), 1024); } static void fu_fmap_firmware_class_init(FuFmapFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_fmap_firmware_parse; firmware_class->write = fu_fmap_firmware_write; } /** * fu_fmap_firmware_new * * Creates a new #FuFirmware of sub type fmap * * Since: 1.5.0 **/ FuFirmware * fu_fmap_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_FMAP_FIRMWARE, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-fmap-firmware.h000066400000000000000000000010041501337203100212560ustar00rootroot00000000000000/* * Copyright 2020 Benson Leung * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_FMAP_FIRMWARE_STRLEN 32 /* maximum length for strings, */ /* including null-terminator */ #define FU_TYPE_FMAP_FIRMWARE (fu_fmap_firmware_get_type()) G_DECLARE_DERIVABLE_TYPE(FuFmapFirmware, fu_fmap_firmware, FU, FMAP_FIRMWARE, FuFirmware) struct _FuFmapFirmwareClass { FuFirmwareClass parent_class; }; FuFirmware * fu_fmap_firmware_new(void); fwupd-2.0.10/libfwupdplugin/fu-fmap.rs000066400000000000000000000012461501337203100176510ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(New, ValidateStream, ParseStream, Default)] #[repr(C, packed)] struct FuStructFmap { signature: [char; 8] == "__FMAP__", ver_major: u8 = 0x1, ver_minor: u8 = 0x1, base: u64le, // address of the firmware binary size: u32le, // bytes name: [char; 32], nareas: u16le, // number of areas } #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructFmapArea { // area of volatile and static regions offset: u32le, // offset relative to base size: u32le, // bytes name: [char; 32], // descriptive name flags: u16le, } fwupd-2.0.10/libfwupdplugin/fu-freebsd-efivars.c000066400000000000000000000122071501337203100215720ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * Copyright 2021 Norbert KamiÅ„ski * Copyright 2021 MichaÅ‚ Kopeć * Copyright 2021 Sergii Dmytruk * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFreebsdEfivars" #include "config.h" #include #include "fwupd-error.h" #include "fu-common.h" #include "fu-freebsd-efivars.h" struct _FuFreebsdEfivars { FuEfivars parent_instance; }; G_DEFINE_TYPE(FuFreebsdEfivars, fu_freebsd_efivars, FU_TYPE_EFIVARS) static gboolean fu_freebsd_efivars_supported(FuEfivars *efivars, GError **error) { if (efi_variables_supported() == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "kernel efivars support missing"); return FALSE; } return TRUE; } static gboolean fu_freebsd_efivars_delete(FuEfivars *efivars, const gchar *guid, const gchar *name, GError **error) { efi_guid_t guidt; efi_str_to_guid(guid, &guidt); if (efi_del_variable(guidt, name) == 0) return TRUE; g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to delete efivars %s", name); return FALSE; } static gboolean fu_freebsd_efivars_delete_with_glob(FuEfivars *efivars, const gchar *guid, const gchar *name_glob, GError **error) { efi_guid_t *guidt = NULL; gchar *name = NULL; gboolean rv = FALSE; efi_guid_t guid_to_delete; efi_str_to_guid(guid, &guid_to_delete); while (efi_get_next_variable_name(&guidt, &name)) { if (memcmp(&guid_to_delete, guidt, sizeof(guid_to_delete)) != 0) continue; if (!g_pattern_match_simple(name, name_glob)) continue; rv = fu_freebsd_efivars_delete(efivars, guid, name, error); if (!rv) break; } return rv; } static gboolean fu_freebsd_efivars_exists_guid(const gchar *guid) { efi_guid_t *guidt = NULL; gchar *name = NULL; efi_guid_t test; efi_str_to_guid(guid, &test); while (efi_get_next_variable_name(&guidt, &name)) { if (memcmp(&test, guidt, sizeof(test)) == 0) { return TRUE; } } return FALSE; } static gboolean fu_freebsd_efivars_exists(FuEfivars *efivars, const gchar *guid, const gchar *name) { /* any name */ if (name == NULL) return fu_freebsd_efivars_exists_guid(guid); return fu_freebsd_efivars_get_data(efivars, guid, name, NULL, NULL, NULL, NULL); } static gboolean fu_freebsd_efivars_get_data(FuEfivars *efivars, const gchar *guid, const gchar *name, guint8 **data, gsize *data_sz, guint32 *attr, GError **error) { efi_guid_t guidt; efi_str_to_guid(guid, &guidt); return (efi_get_variable(guidt, name, data, data_sz, attr) != 0); } static GPtrArray * fu_freebsd_efivars_get_names(FuEfivars *efivars, const gchar *guid, GError **error) { g_autoptr(GPtrArray) names = g_ptr_array_new_with_free_func(g_free); efi_guid_t *guidt = NULL; gchar *name = NULL; efi_guid_t test; efi_str_to_guid(guid, &test); /* find names with matching GUID */ while (efi_get_next_variable_name(&guidt, &name)) { if (memcmp(&test, guidt, sizeof(test)) == 0) { g_ptr_array_add(names, g_strdup(name)); } } /* nothing found */ if (names->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no names for GUID %s", guid); return NULL; } /* success */ return g_steal_pointer(&names); } static guint64 fu_freebsd_efivars_space_used(FuEfivars *efivars, GError **error) { guint64 total = 0; efi_guid_t *guidt = NULL; char *name = NULL; while (efi_get_next_variable_name(&guidt, &name)) { size_t size = 0; if (efi_get_variable_size(*guidt, name, &size) < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to get efivars size"); return G_MAXUINT64; } total += size; } /* success */ return total; } static gboolean fu_freebsd_efivars_set_data(FuEfivars *efivars, const gchar *guid, const gchar *name, const guint8 *data, gsize sz, guint32 attr, GError **error) { efi_guid_t guidt; efi_str_to_guid(guid, &guidt); if (efi_set_variable(guidt, name, (guint8 *)data, sz, attr) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to write data to efivars %s", name); return FALSE; } /* success */ return TRUE; } static void fu_freebsd_efivars_init(FuFreebsdEfivars *self) { } static void fu_freebsd_efivars_class_init(FuFreebsdEfivarsClass *klass) { FuEfivarsClass *efivars_class = FU_EFIVARS_CLASS(klass); efivars_class->supported = fu_freebsd_efivars_supported; efivars_class->space_used = fu_freebsd_efivars_space_used; efivars_class->exists = fu_freebsd_efivars_exists; efivars_class->get_monitor = fu_freebsd_efivars_get_monitor; efivars_class->get_data = fu_freebsd_efivars_get_data; efivars_class->set_data = fu_freebsd_efivars_set_data; efivars_class->delete = fu_freebsd_efivars_delete; efivars_class->delete_with_glob = fu_freebsd_efivars_delete_with_glob; efivars_class->get_names = fu_freebsd_efivars_get_names; } FuEfivars * fu_efivars_new(void) { return FU_EFIVARS(g_object_new(FU_TYPE_FREEBSD_EFIVARS, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-freebsd-efivars.h000066400000000000000000000004621501337203100215770ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-efivars.h" #define FU_TYPE_FREEBSD_EFIVARS (fu_freebsd_efivars_get_type()) G_DECLARE_FINAL_TYPE(FuFreebsdEfivars, fu_freebsd_efivars, FU, FREEBSD_EFIVARS, FuEfivars) fwupd-2.0.10/libfwupdplugin/fu-fuzzer-firmware.c.in000066400000000000000000000017311501337203100222670ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "@INCLUDE@" int LLVMFuzzerTestOneInput(const guint8 *data, gsize size) { g_autoptr(FuFirmware) firmware = FU_FIRMWARE(@FIRMWARENEW@); g_autoptr(GBytes) fw = g_bytes_new(data, size); gboolean ret; (void)g_setenv("G_DEBUG", "fatal-criticals", TRUE); (void)g_setenv("FWUPD_FUZZER_RUNNING", "1", TRUE); ret = fu_firmware_parse_bytes(firmware, fw, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, NULL); if (!ret && fu_firmware_has_flag(firmware, FU_FIRMWARE_FLAG_HAS_CHECKSUM)) { g_clear_object(&firmware); firmware = FU_FIRMWARE(@FIRMWARENEW@); ret = fu_firmware_parse_bytes(firmware, fw, 0x0, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH | FU_FIRMWARE_PARSE_FLAG_IGNORE_VID_PID | FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM, NULL); } if (ret) { g_autoptr(GBytes) fw2 = fu_firmware_write(firmware, NULL); } return 0; } fwupd-2.0.10/libfwupdplugin/fu-fuzzer-generate.c.in000066400000000000000000000021241501337203100222420ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include #include "@INCLUDE@" int main(int argc, char **argv) { g_autoptr(FuFirmware) firmware = FU_FIRMWARE(@FIRMWARENEW@); g_autoptr(GBytes) blob_dst = NULL; g_autoptr(GError) error = NULL; /* do not use g_option_context_parse() here for speed */ if (argc != 3 || !g_str_has_suffix(argv[1], ".builder.xml") || !g_str_has_suffix(argv[2], ".bin")) { g_printerr("Invalid arguments, expected %s XML BIN\n", argv[0]); return EXIT_FAILURE; } if (!fu_firmware_build_from_filename(firmware, argv[1], &error)) { g_printerr("Failed to build: %s\n", error->message); return EXIT_FAILURE; } blob_dst = fu_firmware_write(firmware, &error); if (blob_dst == NULL) { g_printerr("Failed to write: %s\n", error->message); return EXIT_FAILURE; } if (!g_file_set_contents(argv[2], g_bytes_get_data(blob_dst, NULL), g_bytes_get_size(blob_dst), &error)) { g_printerr("Failed to save: %s\n", error->message); return EXIT_FAILURE; } return EXIT_SUCCESS; } fwupd-2.0.10/libfwupdplugin/fu-fuzzer-main.c000066400000000000000000000015311501337203100207700ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include __attribute__((weak)) extern int LLVMFuzzerTestOneInput(const unsigned char *data, size_t size); __attribute__((weak)) extern int LLVMFuzzerInitialize(int *argc, char ***argv); int main(int argc, char **argv) { g_assert_nonnull(LLVMFuzzerTestOneInput); if (LLVMFuzzerInitialize != NULL) LLVMFuzzerInitialize(&argc, &argv); for (int i = 1; i < argc; i++) { gsize bufsz = 0; g_autofree gchar *buf = NULL; g_autoptr(GError) error = NULL; g_printerr("Running: %s\n", argv[i]); if (!g_file_get_contents(argv[i], &buf, &bufsz, &error)) { g_printerr("Failed to load: %s\n", error->message); continue; } LLVMFuzzerTestOneInput((const guint8 *)buf, bufsz); g_printerr("Done\n"); } return EXIT_SUCCESS; } fwupd-2.0.10/libfwupdplugin/fu-gcab.c000066400000000000000000000060441501337203100174210ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #ifdef _WIN32 #include #endif int main(int argc, char **argv) { gboolean do_compression = FALSE; gboolean do_create = FALSE; gboolean do_extract = FALSE; gboolean do_list = FALSE; gboolean no_path = FALSE; gboolean verbose = FALSE; g_autoptr(FuCabFirmware) cab_firmware = fu_cab_firmware_new(); g_autoptr(GOptionContext) context = g_option_context_new(NULL); g_autoptr(GError) error = NULL; const GOptionEntry options[] = { {"verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose, "Be verbose", NULL}, {"create", 'c', 0, G_OPTION_ARG_NONE, &do_create, "Create archive", NULL}, {"extract", 'x', 0, G_OPTION_ARG_NONE, &do_extract, "Extract all files", NULL}, {"list", 'l', 0, G_OPTION_ARG_NONE, &do_list, "List contents", NULL}, {"zip", 'z', 0, G_OPTION_ARG_NONE, &do_compression, "Use zip compression", NULL}, {"nopath", 'n', 0, G_OPTION_ARG_NONE, &no_path, "Do not include path", NULL}, {NULL}}; #ifdef _WIN32 /* required for Windows */ SetConsoleOutputCP(CP_UTF8); SetConsoleCP(CP_UTF8); (void)g_setenv("LANG", "C.UTF-8", FALSE); #endif setlocale(LC_ALL, ""); g_option_context_add_main_entries(context, options, NULL); if (!g_option_context_parse(context, &argc, &argv, &error)) { g_printerr("Failed to parse arguments: %s\n", error->message); return EXIT_FAILURE; } if (verbose) (void)g_setenv("G_MESSAGES_DEBUG", "all", TRUE); if (do_create && argc > 1) { g_autoptr(GFile) file = g_file_new_for_path(argv[1]); for (gint i = 2; i < argc; i++) { g_autoptr(GBytes) img_blob = NULL; g_autoptr(FuCabImage) img = fu_cab_image_new(); if (no_path) { g_autofree gchar *basename = g_path_get_basename(argv[i]); fu_firmware_set_id(FU_FIRMWARE(img), basename); } else { fu_firmware_set_id(FU_FIRMWARE(img), argv[i]); } img_blob = fu_bytes_get_contents(argv[i], &error); if (img_blob == NULL) { g_printerr("Failed to load file %s: %s\n", argv[i], error->message); return EXIT_FAILURE; } fu_firmware_set_bytes(FU_FIRMWARE(img), img_blob); fu_firmware_add_image(FU_FIRMWARE(cab_firmware), FU_FIRMWARE(img)); } if (do_compression) fu_cab_firmware_set_compressed(cab_firmware, TRUE); if (!fu_firmware_write_file(FU_FIRMWARE(cab_firmware), file, &error)) { g_printerr("Failed to write file %s: %s\n", argv[1], error->message); return EXIT_FAILURE; } return EXIT_SUCCESS; } if (do_list && argc > 1) { g_autofree gchar *str = NULL; g_autoptr(GFile) file = g_file_new_for_path(argv[1]); if (!fu_firmware_parse_file(FU_FIRMWARE(cab_firmware), file, FU_FIRMWARE_PARSE_FLAG_NONE, &error)) { g_printerr("Failed to parse %s: %s\n", argv[1], error->message); return EXIT_FAILURE; } str = fu_firmware_to_string(FU_FIRMWARE(cab_firmware)); g_print("%s", str); return EXIT_SUCCESS; } g_printerr("Please specify a single operation\n"); return EXIT_FAILURE; } fwupd-2.0.10/libfwupdplugin/fu-heci-device.c000066400000000000000000000174731501337203100207020ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuHeciDevice" #include "config.h" #include "fu-byte-array.h" #include "fu-heci-device.h" /** * FuHeciDevice * * A HECI device. * * See also: #FuMeiDevice */ G_DEFINE_TYPE(FuHeciDevice, fu_heci_device, FU_TYPE_MEI_DEVICE) #define FU_HECI_DEVICE_TIMEOUT 200 /* ms */ static gboolean fu_heci_device_result_to_error(FuMkhiStatus result, GError **error) { if (result == FU_MKHI_STATUS_SUCCESS) return TRUE; switch (result) { case FU_MKHI_STATUS_NOT_SUPPORTED: case FU_MKHI_STATUS_NOT_AVAILABLE: case FU_MKHI_STATUS_NOT_SET: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported [0x%x]", result); break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "generic failure [0x%x]", result); break; } return FALSE; } /** * fu_heci_device_read_file: * @self: a #FuHeciDevice * @filename: (not nullable): MFS filename * @error: (nullable): optional return location for an error * * Reads a file from the MFS. * * Returns: (transfer container): file data * * Since: 2.0.9 **/ GByteArray * fu_heci_device_read_file(FuHeciDevice *self, const gchar *filename, GError **error) { guint32 data_size; guint datasz_req = 0x80; g_autoptr(GByteArray) bufout = g_byte_array_new(); g_autoptr(GByteArray) buf_res = g_byte_array_new(); g_autoptr(FuMkhiReadFileRequest) st_req = fu_mkhi_read_file_request_new(); g_autoptr(FuMkhiReadFileResponse) st_res = NULL; g_return_val_if_fail(FU_IS_HECI_DEVICE(self), NULL); g_return_val_if_fail(filename != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* request */ if (!fu_mkhi_read_file_request_set_filename(st_req, filename, error)) return NULL; fu_mkhi_read_file_request_set_data_size(st_req, datasz_req); fu_mkhi_read_file_request_set_flags(st_req, (1 << 3)); /* ?? */ if (!fu_mei_device_write(FU_MEI_DEVICE(self), st_req->data, st_req->len, FU_HECI_DEVICE_TIMEOUT, error)) return NULL; /* response */ fu_byte_array_set_size(buf_res, FU_MKHI_READ_FILE_RESPONSE_SIZE + datasz_req, 0x0); if (!fu_mei_device_read(FU_MEI_DEVICE(self), buf_res->data, buf_res->len, NULL, FU_HECI_DEVICE_TIMEOUT, error)) return NULL; st_res = fu_mkhi_read_file_response_parse(buf_res->data, buf_res->len, 0x0, error); if (st_res == NULL) return NULL; if (!fu_heci_device_result_to_error(fu_mkhi_read_file_response_get_result(st_res), error)) return NULL; /* verify we got what we asked for */ data_size = fu_mkhi_read_file_response_get_data_size(st_res); if (data_size > datasz_req) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid response data size, requested 0x%x and got 0x%x", datasz_req, data_size); return NULL; } /* success */ g_byte_array_append(bufout, &buf_res->data[st_res->len], data_size); return g_steal_pointer(&bufout); } /** * fu_heci_device_read_file_ex: * @self: a #FuHeciDevice * @file_id: MFS file ID * @section: MFS section * @datasz_req: the maximum size of data to request * @error: (nullable): optional return location for an error * * Reads a file from the MFS. * * Returns: (transfer container): file data * * Since: 2.0.9 **/ GByteArray * fu_heci_device_read_file_ex(FuHeciDevice *self, guint32 file_id, guint32 section, guint32 datasz_req, GError **error) { guint32 data_size; g_autoptr(FuMkhiReadFileExRequest) st_req = fu_mkhi_read_file_ex_request_new(); g_autoptr(FuMkhiReadFileExResponse) st_res = NULL; g_autoptr(GByteArray) bufout = g_byte_array_new(); g_autoptr(GByteArray) buf_res = g_byte_array_new(); g_return_val_if_fail(FU_IS_HECI_DEVICE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* request */ fu_mkhi_read_file_ex_request_set_file_id(st_req, file_id); fu_mkhi_read_file_ex_request_set_data_size(st_req, datasz_req); fu_mkhi_read_file_ex_request_set_flags(st_req, section); if (!fu_mei_device_write(FU_MEI_DEVICE(self), st_req->data, st_req->len, FU_HECI_DEVICE_TIMEOUT, error)) return NULL; /* response */ fu_byte_array_set_size(buf_res, FU_MKHI_READ_FILE_EX_REQUEST_SIZE + datasz_req, 0x0); if (!fu_mei_device_read(FU_MEI_DEVICE(self), buf_res->data, buf_res->len, NULL, FU_HECI_DEVICE_TIMEOUT, error)) return NULL; st_res = fu_mkhi_read_file_ex_response_parse(buf_res->data, buf_res->len, 0x0, error); if (st_res == NULL) return NULL; if (!fu_heci_device_result_to_error(fu_mkhi_read_file_ex_response_get_result(st_res), error)) return NULL; /* verify we got what we asked for */ data_size = fu_mkhi_read_file_ex_response_get_data_size(st_res); if (data_size > datasz_req) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid response data size, requested 0x%x and got 0x%x", datasz_req, data_size); return NULL; } /* success */ g_byte_array_append(bufout, &buf_res->data[st_res->len], data_size); return g_steal_pointer(&bufout); } /** * fu_heci_device_arbh_svn_get_info: * @self: a #FuHeciDevice * @usage_id: usage ID, e.g. %FU_MKHI_ARBH_SVN_INFO_ENTRY_USAGE_ID_CSE_RBE * @executing: (out) (nullable): currently executing SVN * @min_allowed: (out) (nullable): minimal allowed SVN * @error: (nullable): optional return location for an error * * Reads the ARBH SVN for a specific usage ID. * * Returns: %TRUE for success * * Since: 2.0.9 **/ gboolean fu_heci_device_arbh_svn_get_info(FuHeciDevice *self, guint8 usage_id, guint8 *executing, guint8 *min_allowed, GError **error) { guint32 num_entries; gsize offset = 0; gboolean found_usage = FALSE; g_autoptr(FuMkhiArbhSvnGetInfoRequest) st_req = fu_mkhi_arbh_svn_get_info_request_new(); g_autoptr(FuMkhiArbhSvnGetInfoResponse) st_res = NULL; g_autoptr(GByteArray) buf_res = g_byte_array_new(); g_return_val_if_fail(FU_IS_HECI_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* request */ if (!fu_mei_device_write(FU_MEI_DEVICE(self), st_req->data, st_req->len, FU_HECI_DEVICE_TIMEOUT, error)) return FALSE; /* response */ fu_byte_array_set_size(buf_res, fu_mei_device_get_max_msg_length(FU_MEI_DEVICE(self)), 0x0); if (!fu_mei_device_read(FU_MEI_DEVICE(self), buf_res->data, buf_res->len, NULL, FU_HECI_DEVICE_TIMEOUT, error)) return FALSE; st_res = fu_mkhi_arbh_svn_get_info_response_parse(buf_res->data, buf_res->len, 0x0, error); if (st_res == NULL) return FALSE; if (!fu_heci_device_result_to_error(fu_mkhi_arbh_svn_get_info_response_get_result(st_res), error)) return FALSE; /* verify we got what we asked for */ num_entries = fu_mkhi_arbh_svn_get_info_response_get_num_entries(st_res); offset += st_res->len; for (guint32 i = 0; i < num_entries; i++) { g_autoptr(FuMkhiArbhSvnInfoEntry) st_entry = NULL; /* parse each entry */ st_entry = fu_mkhi_arbh_svn_info_entry_parse(buf_res->data, buf_res->len, offset, error); if (st_entry == NULL) return FALSE; /* matches */ if (fu_mkhi_arbh_svn_info_entry_get_usage_id(st_entry) == usage_id) { found_usage = TRUE; if (executing != NULL) *executing = fu_mkhi_arbh_svn_info_entry_get_executing(st_entry); if (min_allowed != NULL) *min_allowed = fu_mkhi_arbh_svn_info_entry_get_min_allowed(st_entry); break; } /* next */ offset += st_entry->len; } /* did not find */ if (!found_usage) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no entry for usage ID 0x%x", usage_id); return FALSE; } /* success */ return TRUE; } static void fu_heci_device_init(FuHeciDevice *self) { } static void fu_heci_device_class_init(FuHeciDeviceClass *klass) { } fwupd-2.0.10/libfwupdplugin/fu-heci-device.h000066400000000000000000000030441501337203100206740ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-heci-struct.h" #include "fu-mei-device.h" #define FU_TYPE_HECI_DEVICE (fu_heci_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuHeciDevice, fu_heci_device, FU, HECI_DEVICE, FuMeiDevice) struct _FuHeciDeviceClass { FuMeiDeviceClass parent_class; }; /** * FU_HECI_DEVICE_UUID_MKHI: * * UUID for MKHI, usually a legacy interface. */ #define FU_HECI_DEVICE_UUID_MKHI "8e6a6715-9abc-4043-88ef-9e39c6f63e0f" /** * FU_HECI_DEVICE_UUID_MCHI: * * UUID for MCHI, commonly called MCA. */ #define FU_HECI_DEVICE_UUID_MCHI "dd17041c-09ea-4b17-a271-5b989867ec65" /** * FU_HECI_DEVICE_UUID_MCHI2: * * Another UUID for MCHI, commonly called MCA. */ #define FU_HECI_DEVICE_UUID_MCHI2 "fe2af7a6-ef22-4b45-872f-176b0bbc8b43" /** * FU_HECI_DEVICE_UUID_FWUPDATE: * * UUID for firmware updates. */ #define FU_HECI_DEVICE_UUID_FWUPDATE "87d90ca5-3495-4559-8105-3fbfa37b8b79" GByteArray * fu_heci_device_read_file(FuHeciDevice *self, const gchar *filename, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); GByteArray * fu_heci_device_read_file_ex(FuHeciDevice *self, guint32 file_id, guint32 section, guint32 datasz_req, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_heci_device_arbh_svn_get_info(FuHeciDevice *self, guint8 usage_id, guint8 *executing, guint8 *min_allowed, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-heci.rs000066400000000000000000000054721501337203100176430ustar00rootroot00000000000000// Copyright 2024 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[repr(u8)] enum FuMkhiGroupId { Cbm, Pm, // no longer used Pwd, Fwcaps, App, // no longer used Fwupdate, // for manufacturing downgrade FirmwareUpdate, Bist, Mdes, MeDbg, Mca, // sometimes called "FPF" Gen = 0xFF, } enum FuMkhiStatus { Success, InvalidState, MessageSkipped, SizeError = 0x05, NotSet = 0x0F, // guessed NotAvailable = 0x18, // guessed InvalidAccess = 0x84, InvalidParams = 0x85, NotReady = 0x88, NotSupported = 0x89, InvalidAddress = 0x8C, InvalidCommand = 0x8D, Failure = 0x9E, InvalidResource = 0xE4, ResourceInUse = 0xE5, NoResource = 0xE6, GeneralError = 0xFF, } #[repr(u8)] enum FuMkhiCommand { ReadFile = 0x02, ReadFileEx = 0x0A, ArbhSvnCommit = 0x1B, ArbhSvnGetInfo = 0x1C, // not real commands, but makes debugging easier ReadFileResponse = 0x82, ReadFileExResponse = 0x8A, ArbhSvnCommitResponse = 0x9B, ArbhSvnGetInfoResponse = 0x9C, } #[derive(New, Default)] #[repr(C, packed)] struct FuMkhiReadFileRequest { group_id: FuMkhiGroupId == Mca, command: FuMkhiCommand == ReadFile, _rsvd: u8, result: u8 == 0x0, filename: [char; 0x40], offset: u32le == 0x0, data_size: u32le, flags: u8, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuMkhiReadFileResponse { group_id: FuMkhiGroupId == Mca, command: FuMkhiCommand == ReadFileResponse, _rsvd: u8, result: u8, data_size: u32le, // payload here } #[derive(New, Default)] #[repr(C, packed)] struct FuMkhiReadFileExRequest { group_id: FuMkhiGroupId == Mca, command: FuMkhiCommand == ReadFileEx, _rsvd: u8, result: u8 == 0x0, file_id: u32le, offset: u32le == 0x0, data_size: u32le, flags: u8, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuMkhiReadFileExResponse { group_id: FuMkhiGroupId == Mca, command: FuMkhiCommand == ReadFileExResponse, _rsvd: u8, result: u8, data_size: u32le, // payload here } #[repr(u8)] enum FuMkhiArbhSvnInfoEntryUsageId { CseRbe = 3, } #[derive(Parse)] #[repr(C, packed)] struct FuMkhiArbhSvnInfoEntry { usage_id: FuMkhiArbhSvnInfoEntryUsageId, _flags: u8, executing: u8, min_allowed: u8, } #[derive(New, Default)] #[repr(C, packed)] struct FuMkhiArbhSvnGetInfoRequest { group_id: FuMkhiGroupId == Mca, command: FuMkhiCommand == ArbhSvnGetInfo, reserved: u8, result: u8 == 0x0, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuMkhiArbhSvnGetInfoResponse { group_id: FuMkhiGroupId == Mca, command: FuMkhiCommand == ArbhSvnGetInfoResponse, _rsvd: u8, result: u8, num_entries: u32le, // FuMkhiArbhSvnInfoEntry entries[] } fwupd-2.0.10/libfwupdplugin/fu-hid-descriptor.c000066400000000000000000000226541501337203100214520ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuHidDevice" #include "config.h" #include "fu-byte-array.h" #include "fu-hid-descriptor.h" #include "fu-hid-report-item.h" #include "fu-hid-struct.h" #include "fu-input-stream.h" #include "fu-mem.h" /** * FuHidDescriptor: * * A HID descriptor. * * Each report is a image of this firmware object and each report has children of #FuHidReportItem. * * Documented: https://www.usb.org/sites/default/files/hid1_11.pdf * * See also: [class@FuFirmware] */ G_DEFINE_TYPE(FuHidDescriptor, fu_hid_descriptor, FU_TYPE_FIRMWARE) #define FU_HID_DESCRIPTOR_TABLE_LOCAL_SIZE_MAX 1024 #define FU_HID_DESCRIPTOR_TABLE_LOCAL_DUPES_MAX 16 #define FU_HID_DESCRIPTOR_TABLE_GLOBAL_SIZE_MAX 1024 #define FU_HID_DESCRIPTOR_TABLE_GLOBAL_DUPES_MAX 64 static guint fu_hid_descriptor_count_table_dupes(GPtrArray *table, FuHidReportItem *item) { guint cnt = 0; for (guint i = 0; i < table->len; i++) { FuHidReportItem *item_tmp = g_ptr_array_index(table, i); if (fu_hid_report_item_get_kind(item) == fu_hid_report_item_get_kind(item_tmp) && fu_hid_report_item_get_value(item) == fu_hid_report_item_get_value(item_tmp) && fu_firmware_get_idx(FU_FIRMWARE(item)) == fu_firmware_get_idx(FU_FIRMWARE(item_tmp))) cnt++; } return cnt; } static gboolean fu_hid_descriptor_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { gsize offset = 0; gsize streamsz = 0; g_autoptr(GPtrArray) table_state = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_autoptr(GPtrArray) table_local = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; while (offset < streamsz) { g_autofree gchar *itemstr = NULL; g_autoptr(FuHidReportItem) item = fu_hid_report_item_new(); /* sanity check */ if (table_state->len > FU_HID_DESCRIPTOR_TABLE_GLOBAL_SIZE_MAX) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "HID table state too large, limit is %u", (guint)FU_HID_DESCRIPTOR_TABLE_GLOBAL_SIZE_MAX); return FALSE; } if (table_local->len > FU_HID_DESCRIPTOR_TABLE_LOCAL_SIZE_MAX) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "HID table state too large, limit is %u", (guint)FU_HID_DESCRIPTOR_TABLE_LOCAL_SIZE_MAX); return FALSE; } if (!fu_firmware_parse_stream(FU_FIRMWARE(item), stream, offset, flags, error)) return FALSE; offset += fu_firmware_get_size(FU_FIRMWARE(item)); /* only for debugging */ itemstr = fu_firmware_to_string(FU_FIRMWARE(item)); g_debug("add to table-state:\n%s", itemstr); /* if there is a sane number of duplicate tokens then add to table */ if (fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_GLOBAL) { if (fu_hid_descriptor_count_table_dupes(table_state, item) > FU_HID_DESCRIPTOR_TABLE_GLOBAL_DUPES_MAX) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "table invalid @0x%x, too many duplicate global %s tokens", (guint)offset, fu_firmware_get_id(FU_FIRMWARE(item))); return FALSE; } g_ptr_array_add(table_state, g_object_ref(item)); } else if (fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_LOCAL || fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_MAIN) { if (fu_hid_descriptor_count_table_dupes(table_local, item) > FU_HID_DESCRIPTOR_TABLE_LOCAL_DUPES_MAX) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "table invalid @0x%x, too many duplicate %s %s:0x%x tokens", (guint)offset, fu_hid_item_kind_to_string(fu_hid_report_item_get_kind(item)), fu_firmware_get_id(FU_FIRMWARE(item)), fu_hid_report_item_get_value(item)); return FALSE; } g_ptr_array_add(table_local, g_object_ref(item)); } /* add report */ if (fu_hid_report_item_get_kind(item) == FU_HID_ITEM_KIND_MAIN) { g_autoptr(FuHidReport) report = fu_hid_report_new(); /* copy the table state to the new report */ for (guint i = 0; i < table_state->len; i++) { FuHidReportItem *item_tmp = g_ptr_array_index(table_state, i); if (!fu_firmware_add_image_full(FU_FIRMWARE(report), FU_FIRMWARE(item_tmp), error)) return FALSE; } for (guint i = 0; i < table_local->len; i++) { FuHidReportItem *item_tmp = g_ptr_array_index(table_local, i); if (!fu_firmware_add_image_full(FU_FIRMWARE(report), FU_FIRMWARE(item_tmp), error)) return FALSE; } if (!fu_firmware_add_image_full(firmware, FU_FIRMWARE(report), error)) return FALSE; /* remove all the local items */ g_ptr_array_set_size(table_local, 0); } } /* success */ return TRUE; } static gboolean fu_hid_descriptor_write_report_item(FuFirmware *report_item, GByteArray *buf, GHashTable *globals, GError **error) { g_autoptr(GBytes) fw = NULL; /* dedupe any globals */ if (fu_hid_report_item_get_kind(FU_HID_REPORT_ITEM(report_item)) == FU_HID_ITEM_KIND_GLOBAL) { guint8 tag = fu_firmware_get_idx(report_item); FuFirmware *report_item_tmp = g_hash_table_lookup(globals, GUINT_TO_POINTER(tag)); if (report_item_tmp != NULL && fu_hid_report_item_get_value(FU_HID_REPORT_ITEM(report_item)) == fu_hid_report_item_get_value(FU_HID_REPORT_ITEM(report_item_tmp))) { g_debug("skipping duplicate global tag 0x%x", tag); return TRUE; } g_hash_table_insert(globals, GUINT_TO_POINTER(tag), report_item); } fw = fu_firmware_write(report_item, error); if (fw == NULL) return FALSE; fu_byte_array_append_bytes(buf, fw); /* success */ return TRUE; } static gboolean fu_hid_descriptor_write_report(FuFirmware *report, GByteArray *buf, GHashTable *globals, GError **error) { g_autoptr(GPtrArray) report_items = fu_firmware_get_images(report); /* for each item */ for (guint i = 0; i < report_items->len; i++) { FuFirmware *report_item = g_ptr_array_index(report_items, i); if (!fu_hid_descriptor_write_report_item(report_item, buf, globals, error)) return FALSE; } /* success */ return TRUE; } static GByteArray * fu_hid_descriptor_write(FuFirmware *firmware, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GHashTable) globals = g_hash_table_new(g_direct_hash, g_direct_equal); g_autoptr(GPtrArray) reports = fu_firmware_get_images(firmware); /* for each report */ for (guint i = 0; i < reports->len; i++) { FuFirmware *report = g_ptr_array_index(reports, i); if (!fu_hid_descriptor_write_report(report, buf, globals, error)) return NULL; } /* success */ return g_steal_pointer(&buf); } typedef struct { const gchar *id; guint32 value; } FuHidDescriptorCondition; /** * fu_hid_descriptor_find_report: * @self: a #FuHidDescriptor * @error: (nullable): optional return location for an error * @...: pairs of string-integer values, ending with %NULL * * Finds the first HID report that matches all the report attributes. * * Returns: (transfer full): A #FuHidReport, or %NULL if not found. * * Since: 1.9.4 **/ FuHidReport * fu_hid_descriptor_find_report(FuHidDescriptor *self, GError **error, ...) { va_list args; g_autoptr(GPtrArray) conditions = g_ptr_array_new_with_free_func(g_free); g_autoptr(GPtrArray) reports = fu_firmware_get_images(FU_FIRMWARE(self)); g_return_val_if_fail(FU_IS_HID_DESCRIPTOR(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* parse varargs */ va_start(args, error); for (guint i = 0; i < 1000; i++) { g_autofree FuHidDescriptorCondition *cond = g_new0(FuHidDescriptorCondition, 1); cond->id = va_arg(args, const gchar *); if (cond->id == NULL) break; cond->value = va_arg(args, guint32); g_ptr_array_add(conditions, g_steal_pointer(&cond)); } va_end(args); /* return the first report that matches *all* conditions */ for (guint i = 0; i < reports->len; i++) { FuHidReport *report = g_ptr_array_index(reports, i); gboolean matched = TRUE; for (guint j = 0; j < conditions->len; j++) { FuHidDescriptorCondition *cond = g_ptr_array_index(conditions, j); g_autoptr(FuFirmware) item = fu_firmware_get_image_by_id(FU_FIRMWARE(report), cond->id, NULL); if (item == NULL) { matched = FALSE; break; } if (fu_hid_report_item_get_value(FU_HID_REPORT_ITEM(item)) != cond->value) { matched = FALSE; break; } } if (matched) return g_object_ref(report); } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no report found"); return NULL; } static void fu_hid_descriptor_init(FuHidDescriptor *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_NO_AUTO_DETECTION); fu_firmware_set_size_max(FU_FIRMWARE(self), 64 * 1024); fu_firmware_set_images_max(FU_FIRMWARE(self), g_getenv("FWUPD_FUZZER_RUNNING") != NULL ? 10 : 1024); g_type_ensure(FU_TYPE_HID_REPORT); g_type_ensure(FU_TYPE_HID_REPORT_ITEM); } static void fu_hid_descriptor_class_init(FuHidDescriptorClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_hid_descriptor_parse; firmware_class->write = fu_hid_descriptor_write; } /** * fu_hid_descriptor_new: * * Creates a new #FuFirmware to parse a HID descriptor * * Since: 1.9.4 **/ FuFirmware * fu_hid_descriptor_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_HID_DESCRIPTOR, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-hid-descriptor.h000066400000000000000000000010521501337203100214440ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-hid-report.h" #define FU_TYPE_HID_DESCRIPTOR (fu_hid_descriptor_get_type()) G_DECLARE_DERIVABLE_TYPE(FuHidDescriptor, fu_hid_descriptor, FU, HID_DESCRIPTOR, FuFirmware) struct _FuHidDescriptorClass { FuFirmwareClass parent_class; }; FuFirmware * fu_hid_descriptor_new(void); FuHidReport * fu_hid_descriptor_find_report(FuHidDescriptor *self, GError **error, ...) G_GNUC_NULL_TERMINATED G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-hid-device.c000066400000000000000000000466741501337203100205430ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuHidDevice" #include "config.h" #include "fu-dump.h" #include "fu-hid-device.h" #include "fu-string.h" #include "fu-usb-device-private.h" #include "fu-usb-endpoint.h" #define FU_HID_REPORT_GET 0x01 #define FU_HID_REPORT_SET 0x09 #define FU_HID_REPORT_TYPE_INPUT 0x01 #define FU_HID_REPORT_TYPE_OUTPUT 0x02 #define FU_HID_REPORT_TYPE_FEATURE 0x03 #define FU_HID_DEVICE_RETRIES 10 /** * FuHidDevice: * * A Human Interface Device (HID) device. * * See also: [class@FuDevice], [class@FuUsbDevice] */ typedef struct { guint8 interface; guint8 ep_addr_in; /* only for _USE_INTERRUPT_TRANSFER */ guint8 ep_addr_out; /* only for _USE_INTERRUPT_TRANSFER */ gboolean interface_autodetect; FuHidDeviceFlags flags; } FuHidDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuHidDevice, fu_hid_device, FU_TYPE_USB_DEVICE) enum { PROP_0, PROP_INTERFACE, PROP_LAST }; #define GET_PRIVATE(o) (fu_hid_device_get_instance_private(o)) static void fu_hid_device_to_string(FuDevice *device, guint idt, GString *str) { FuHidDevice *self = FU_HID_DEVICE(device); FuHidDevicePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append_bool(str, idt, "InterfaceAutodetect", priv->interface_autodetect); fwupd_codec_string_append_hex(str, idt, "Interface", priv->interface); fwupd_codec_string_append_hex(str, idt, "EpAddrIn", priv->ep_addr_in); fwupd_codec_string_append_hex(str, idt, "EpAddrOut", priv->ep_addr_out); } static void fu_hid_device_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuHidDevice *device = FU_HID_DEVICE(object); FuHidDevicePrivate *priv = GET_PRIVATE(device); switch (prop_id) { case PROP_INTERFACE: g_value_set_uint(value, priv->interface); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_hid_device_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuHidDevice *device = FU_HID_DEVICE(object); switch (prop_id) { case PROP_INTERFACE: fu_hid_device_set_interface(device, g_value_get_uint(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } /** * fu_hid_device_parse_descriptors: * @self: a #FuHidDevice * @error: (nullable): optional return location for an error * * Parses the HID descriptors. * * Returns: (transfer container) (element-type FuHidDescriptor): descriptors, or %NULL for error * * Since: 2.0.0 **/ GPtrArray * fu_hid_device_parse_descriptors(FuHidDevice *self, GError **error) { g_autoptr(GPtrArray) fws = NULL; g_autoptr(GPtrArray) descriptors = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_return_val_if_fail(FU_HID_DEVICE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); fws = fu_usb_device_get_hid_descriptors(FU_USB_DEVICE(self), error); if (fws == NULL) return NULL; for (guint i = 0; i < fws->len; i++) { GBytes *fw = g_ptr_array_index(fws, i); g_autoptr(FuFirmware) descriptor = fu_hid_descriptor_new(); g_autofree gchar *title = g_strdup_printf("HidDescriptor:0x%x", i); fu_dump_bytes(G_LOG_DOMAIN, title, fw); if (!fu_firmware_parse_bytes(descriptor, fw, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return NULL; g_ptr_array_add(descriptors, g_steal_pointer(&descriptor)); } return g_steal_pointer(&descriptors); } static gboolean fu_hid_device_autodetect_eps(FuHidDevice *self, FuUsbInterface *iface, GError **error) { FuHidDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(GPtrArray) eps = fu_usb_interface_get_endpoints(iface); for (guint i = 0; eps != NULL && i < eps->len; i++) { FuUsbEndpoint *ep = g_ptr_array_index(eps, i); if (fu_usb_endpoint_get_direction(ep) == FU_USB_DIRECTION_DEVICE_TO_HOST && priv->ep_addr_in == 0) { priv->ep_addr_in = fu_usb_endpoint_get_address(ep); continue; } if (fu_usb_endpoint_get_direction(ep) == FU_USB_DIRECTION_HOST_TO_DEVICE && priv->ep_addr_out == 0) { priv->ep_addr_out = fu_usb_endpoint_get_address(ep); continue; } } if (priv->ep_addr_in == 0x0 && priv->ep_addr_out == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "could not autodetect EP addresses"); return FALSE; } return TRUE; } static gboolean fu_hid_device_setup(FuDevice *device, GError **error) { FuHidDevice *self = FU_HID_DEVICE(device); /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_hid_device_parent_class)->setup(device, error)) return FALSE; /* best effort, from HID */ if (fu_device_get_vendor(device) == NULL) { g_autofree gchar *manufacturer = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(self), "manufacturer", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (manufacturer != NULL) fu_device_set_vendor(device, manufacturer); } if (fu_device_get_name(device) == NULL) { g_autofree gchar *product = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(self), "product", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (product != NULL) fu_device_set_name(device, product); } /* success */ return TRUE; } static gboolean fu_hid_device_open(FuDevice *device, GError **error) { FuHidDevice *self = FU_HID_DEVICE(device); FuHidDevicePrivate *priv = GET_PRIVATE(self); FuUsbDeviceClaimFlags flags = 0; /* FuUsbDevice->open */ if (!FU_DEVICE_CLASS(fu_hid_device_parent_class)->open(device, error)) return FALSE; /* self tests */ if (fu_usb_device_get_spec(FU_USB_DEVICE(device)) == 0x0) return TRUE; /* auto-detect */ if (priv->interface_autodetect) { g_autoptr(GPtrArray) ifaces = NULL; ifaces = fu_usb_device_get_interfaces(FU_USB_DEVICE(self), error); if (ifaces == NULL) return FALSE; for (guint i = 0; i < ifaces->len; i++) { FuUsbInterface *iface = g_ptr_array_index(ifaces, i); if (fu_usb_interface_get_class(iface) == FU_USB_CLASS_HID) { priv->interface = fu_usb_interface_get_number(iface); priv->interface_autodetect = FALSE; if (priv->flags & FU_HID_DEVICE_FLAG_AUTODETECT_EPS) { if (!fu_hid_device_autodetect_eps(self, iface, error)) return FALSE; } break; } } if (priv->interface_autodetect) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "could not autodetect HID interface"); return FALSE; } } /* claim */ if ((priv->flags & FU_HID_DEVICE_FLAG_NO_KERNEL_UNBIND) == 0) flags |= FU_USB_DEVICE_CLAIM_FLAG_KERNEL_DRIVER; if (!fu_usb_device_claim_interface(FU_USB_DEVICE(self), priv->interface, flags, error)) { g_prefix_error(error, "failed to claim HID interface: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_hid_device_close(FuDevice *device, GError **error) { FuHidDevice *self = FU_HID_DEVICE(device); FuHidDevicePrivate *priv = GET_PRIVATE(self); FuUsbDeviceClaimFlags flags = 0; g_autoptr(GError) error_local = NULL; /* self tests */ if (fu_usb_device_get_spec(FU_USB_DEVICE(device)) == 0x0) return TRUE; /* release */ if ((priv->flags & FU_HID_DEVICE_FLAG_NO_KERNEL_REBIND) == 0) flags |= FU_USB_DEVICE_CLAIM_FLAG_KERNEL_DRIVER; if (!fu_usb_device_release_interface(FU_USB_DEVICE(self), priv->interface, flags, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL)) { g_debug("ignoring: %s", error_local->message); } else { g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to release HID interface: "); return FALSE; } } /* FuUsbDevice->close */ return FU_DEVICE_CLASS(fu_hid_device_parent_class)->close(device, error); } /** * fu_hid_device_set_interface: * @self: a #FuHidDevice * @interface_number: an interface number, e.g. `0x03` * * Sets the HID USB interface number. * * In most cases the HID interface is auto-detected, but this function can be * used where there are multiple HID interfaces or where the device USB * interface descriptor is invalid. * * Since: 1.4.0 **/ void fu_hid_device_set_interface(FuHidDevice *self, guint8 interface_number) { FuHidDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_HID_DEVICE(self)); priv->interface = interface_number; priv->interface_autodetect = FALSE; } /** * fu_hid_device_get_interface: * @self: a #FuHidDevice * * Gets the HID USB interface number. * * Returns: integer * * Since: 1.4.0 **/ guint8 fu_hid_device_get_interface(FuHidDevice *self) { FuHidDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_HID_DEVICE(self), 0xff); return priv->interface; } /** * fu_hid_device_set_ep_addr_in: * @self: a #FuHidDevice * @ep_addr_in: an endpoint, e.g. `0x03` * * Sets the HID USB interrupt in endpoint. * * In most cases the HID ep_addr_in is auto-detected, but this function can be * used where there are multiple HID EPss or where the device USB EP is invalid. * * Since: 1.9.4 **/ void fu_hid_device_set_ep_addr_in(FuHidDevice *self, guint8 ep_addr_in) { FuHidDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_HID_DEVICE(self)); priv->ep_addr_in = ep_addr_in; priv->interface_autodetect = FALSE; } /** * fu_hid_device_get_ep_addr_in: * @self: a #FuHidDevice * * Gets the HID USB in endpoint. * * Returns: integer * * Since: 1.9.4 **/ guint8 fu_hid_device_get_ep_addr_in(FuHidDevice *self) { FuHidDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_HID_DEVICE(self), 0xff); return priv->ep_addr_in; } /** * fu_hid_device_set_ep_addr_out: * @self: a #FuHidDevice * @ep_addr_out: an endpoint, e.g. `0x03` * * Sets the HID USB interrupt out endpoint. * * In most cases the HID EPs are auto-detected, but this function can be * used where there are multiple HID EPs or where the device USB EP is invalid. * * Since: 1.9.4 **/ void fu_hid_device_set_ep_addr_out(FuHidDevice *self, guint8 ep_addr_out) { FuHidDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_HID_DEVICE(self)); priv->ep_addr_out = ep_addr_out; priv->interface_autodetect = FALSE; } /** * fu_hid_device_get_ep_addr_out: * @self: a #FuHidDevice * * Gets the HID USB out endpoint. * * Returns: integer * * Since: 1.9.4 **/ guint8 fu_hid_device_get_ep_addr_out(FuHidDevice *self) { FuHidDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_HID_DEVICE(self), 0xff); return priv->ep_addr_out; } /** * fu_hid_device_add_flag: * @self: a #FuHidDevice * @flag: HID device flags, e.g. %FU_HID_DEVICE_FLAG_RETRY_FAILURE * * Adds a flag to be used for all set and get report messages. * * Since: 1.5.2 **/ void fu_hid_device_add_flag(FuHidDevice *self, FuHidDeviceFlags flag) { FuHidDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_HID_DEVICE(self)); priv->flags |= flag; } typedef struct { guint8 value; guint8 *buf; gsize bufsz; guint timeout; FuHidDeviceFlags flags; } FuHidDeviceRetryHelper; static gboolean fu_hid_device_set_report_internal(FuHidDevice *self, FuHidDeviceRetryHelper *helper, GError **error) { FuHidDevicePrivate *priv = GET_PRIVATE(self); gsize actual_len = 0; /* what method do we use? */ if (helper->flags & FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER) { g_autofree gchar *title = g_strdup_printf("HID::SetReport [EP=0x%02x]", priv->ep_addr_out); fu_dump_raw(G_LOG_DOMAIN, title, helper->buf, helper->bufsz); if (priv->ep_addr_out == 0x0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no EpAddrOut set"); return FALSE; } if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), priv->ep_addr_out, helper->buf, helper->bufsz, &actual_len, helper->timeout, NULL, /* cancellable */ error)) { g_prefix_error(error, "failed to SetReport [interrupt-transfer]: "); return FALSE; } } else { guint16 wvalue = (FU_HID_REPORT_TYPE_OUTPUT << 8) | helper->value; g_autofree gchar *title = NULL; /* special case */ if (helper->flags & FU_HID_DEVICE_FLAG_IS_FEATURE) wvalue = (FU_HID_REPORT_TYPE_FEATURE << 8) | helper->value; title = g_strdup_printf("HID::SetReport [wValue=0x%04x, wIndex=%u]", wvalue, priv->interface); fu_dump_raw(G_LOG_DOMAIN, title, helper->buf, helper->bufsz); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_CLASS, FU_USB_RECIPIENT_INTERFACE, FU_HID_REPORT_SET, wvalue, priv->interface, helper->buf, helper->bufsz, &actual_len, helper->timeout, NULL, error)) { g_prefix_error(error, "failed to SetReport: "); return FALSE; } } if ((helper->flags & FU_HID_DEVICE_FLAG_ALLOW_TRUNC) == 0 && actual_len != helper->bufsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "wrote %" G_GSIZE_FORMAT ", requested %" G_GSIZE_FORMAT " bytes", actual_len, helper->bufsz); return FALSE; } return TRUE; } static gboolean fu_hid_device_set_report_internal_cb(FuDevice *device, gpointer user_data, GError **error) { FuHidDevice *self = FU_HID_DEVICE(device); FuHidDeviceRetryHelper *helper = (FuHidDeviceRetryHelper *)user_data; return fu_hid_device_set_report_internal(self, helper, error); } /** * fu_hid_device_set_report: * @self: a #FuHidDevice * @value: low byte of wValue, but unused when using %FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER * @buf: (nullable): a mutable buffer of data to send * @bufsz: size of @buf * @timeout: timeout in ms * @flags: HID device flags e.g. %FU_HID_DEVICE_FLAG_ALLOW_TRUNC * @error: (nullable): optional return location for an error * * Calls SetReport on the hardware. * * Returns: %TRUE for success * * Since: 1.4.0 **/ gboolean fu_hid_device_set_report(FuHidDevice *self, guint8 value, guint8 *buf, gsize bufsz, guint timeout, FuHidDeviceFlags flags, GError **error) { FuHidDeviceRetryHelper helper; FuHidDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_HID_DEVICE(self), FALSE); g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(bufsz != 0, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* create helper */ helper.value = value; helper.buf = buf; helper.bufsz = bufsz; helper.timeout = timeout; helper.flags = priv->flags | flags; /* special case */ if (flags & FU_HID_DEVICE_FLAG_RETRY_FAILURE) { return fu_device_retry(FU_DEVICE(self), fu_hid_device_set_report_internal_cb, FU_HID_DEVICE_RETRIES, &helper, error); } /* just one */ return fu_hid_device_set_report_internal(self, &helper, error); } static gboolean fu_hid_device_get_report_internal(FuHidDevice *self, FuHidDeviceRetryHelper *helper, GError **error) { FuHidDevicePrivate *priv = GET_PRIVATE(self); gsize actual_len = 0; /* what method do we use? */ if (helper->flags & FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER) { g_autofree gchar *title = NULL; if (priv->ep_addr_in == 0x0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no EpAddrIn set"); return FALSE; } if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), priv->ep_addr_in, helper->buf, helper->bufsz, &actual_len, helper->timeout, NULL, /* cancellable */ error)) return FALSE; title = g_strdup_printf("HID::GetReport [EP=0x%02x]", priv->ep_addr_in); fu_dump_raw(G_LOG_DOMAIN, title, helper->buf, helper->bufsz); } else { guint16 wvalue = (FU_HID_REPORT_TYPE_INPUT << 8) | helper->value; g_autofree gchar *title = NULL; /* special case */ if (helper->flags & FU_HID_DEVICE_FLAG_IS_FEATURE) wvalue = (FU_HID_REPORT_TYPE_FEATURE << 8) | helper->value; title = g_strdup_printf("HID::GetReport [wValue=0x%04x, wIndex=%u]", wvalue, priv->interface); fu_dump_raw(G_LOG_DOMAIN, title, helper->buf, helper->bufsz); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_CLASS, FU_USB_RECIPIENT_INTERFACE, FU_HID_REPORT_GET, wvalue, priv->interface, helper->buf, helper->bufsz, &actual_len, /* actual length */ helper->timeout, NULL, error)) { g_prefix_error(error, "failed to GetReport: "); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, title, helper->buf, actual_len); } if ((helper->flags & FU_HID_DEVICE_FLAG_ALLOW_TRUNC) == 0 && actual_len != helper->bufsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "read %" G_GSIZE_FORMAT ", requested %" G_GSIZE_FORMAT " bytes", actual_len, helper->bufsz); return FALSE; } return TRUE; } static gboolean fu_hid_device_get_report_internal_cb(FuDevice *device, gpointer user_data, GError **error) { FuHidDevice *self = FU_HID_DEVICE(device); FuHidDeviceRetryHelper *helper = (FuHidDeviceRetryHelper *)user_data; return fu_hid_device_get_report_internal(self, helper, error); } /** * fu_hid_device_get_report: * @self: a #FuHidDevice * @value: low byte of wValue, but unused when using %FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER * @buf: (nullable): a mutable buffer of data to send * @bufsz: size of @buf * @timeout: timeout in ms * @flags: HID device flags e.g. %FU_HID_DEVICE_FLAG_ALLOW_TRUNC * @error: (nullable): optional return location for an error * * Calls GetReport on the hardware. * * Returns: %TRUE for success * * Since: 1.4.0 **/ gboolean fu_hid_device_get_report(FuHidDevice *self, guint8 value, guint8 *buf, gsize bufsz, guint timeout, FuHidDeviceFlags flags, GError **error) { FuHidDeviceRetryHelper helper; FuHidDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_HID_DEVICE(self), FALSE); g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(bufsz != 0, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* create helper */ helper.value = value; helper.buf = buf; helper.bufsz = bufsz; helper.timeout = timeout; helper.flags = priv->flags | flags; /* special case */ if (flags & FU_HID_DEVICE_FLAG_RETRY_FAILURE) { return fu_device_retry(FU_DEVICE(self), fu_hid_device_get_report_internal_cb, FU_HID_DEVICE_RETRIES, &helper, error); } /* just one */ return fu_hid_device_get_report_internal(self, &helper, error); } static void fu_hid_device_init(FuHidDevice *self) { FuHidDevicePrivate *priv = GET_PRIVATE(self); priv->interface_autodetect = TRUE; } static void fu_hid_device_class_init(FuHidDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); GParamSpec *pspec; object_class->get_property = fu_hid_device_get_property; object_class->set_property = fu_hid_device_set_property; device_class->open = fu_hid_device_open; device_class->setup = fu_hid_device_setup; device_class->close = fu_hid_device_close; device_class->to_string = fu_hid_device_to_string; /** * FuHidDevice:interface: * * The HID interface to use. * * Since: 1.4.0 */ pspec = g_param_spec_uint("interface", NULL, NULL, 0x00, 0xff, 0x00, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_INTERFACE, pspec); } fwupd-2.0.10/libfwupdplugin/fu-hid-device.h000066400000000000000000000051401501337203100205270ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-hid-descriptor.h" #include "fu-usb-device.h" #define FU_TYPE_HID_DEVICE (fu_hid_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuHidDevice, fu_hid_device, FU, HID_DEVICE, FuUsbDevice) struct _FuHidDeviceClass { FuUsbDeviceClass parent_class; }; /** * FuHidDeviceFlags: * @FU_HID_DEVICE_FLAG_NONE: No flags set * @FU_HID_DEVICE_FLAG_ALLOW_TRUNC: Allow truncated reads and writes * @FU_HID_DEVICE_FLAG_IS_FEATURE: Use %FU_HID_REPORT_TYPE_FEATURE for wValue * @FU_HID_DEVICE_FLAG_RETRY_FAILURE: Retry up to 10 times on failure * @FU_HID_DEVICE_FLAG_NO_KERNEL_UNBIND: Do not unbind the kernel driver on open * @FU_HID_DEVICE_FLAG_NO_KERNEL_REBIND: Do not rebind the kernel driver on close * @FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER: Use interrupt transfers, not control transfers * @FU_HID_DEVICE_FLAG_AUTODETECT_EPS: Autodetect interface endpoints * * Flags used when calling fu_hid_device_get_report() and fu_hid_device_set_report(). **/ typedef enum { FU_HID_DEVICE_FLAG_NONE = 0, FU_HID_DEVICE_FLAG_ALLOW_TRUNC = 1 << 0, FU_HID_DEVICE_FLAG_IS_FEATURE = 1 << 1, FU_HID_DEVICE_FLAG_RETRY_FAILURE = 1 << 2, FU_HID_DEVICE_FLAG_NO_KERNEL_UNBIND = 1 << 3, FU_HID_DEVICE_FLAG_NO_KERNEL_REBIND = 1 << 4, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER = 1 << 5, FU_HID_DEVICE_FLAG_AUTODETECT_EPS = 1 << 6, /*< private >*/ FU_HID_DEVICE_FLAG_LAST } FuHidDeviceFlags; void fu_hid_device_add_flag(FuHidDevice *self, FuHidDeviceFlags flag) G_GNUC_NON_NULL(1); void fu_hid_device_set_interface(FuHidDevice *self, guint8 interface_number) G_GNUC_NON_NULL(1); guint8 fu_hid_device_get_interface(FuHidDevice *self) G_GNUC_NON_NULL(1); void fu_hid_device_set_ep_addr_in(FuHidDevice *self, guint8 ep_addr_in) G_GNUC_NON_NULL(1); guint8 fu_hid_device_get_ep_addr_in(FuHidDevice *self) G_GNUC_NON_NULL(1); void fu_hid_device_set_ep_addr_out(FuHidDevice *self, guint8 ep_addr_out) G_GNUC_NON_NULL(1); guint8 fu_hid_device_get_ep_addr_out(FuHidDevice *self) G_GNUC_NON_NULL(1); GPtrArray * fu_hid_device_parse_descriptors(FuHidDevice *self, GError **error) G_GNUC_NON_NULL(1); gboolean fu_hid_device_set_report(FuHidDevice *self, guint8 value, guint8 *buf, gsize bufsz, guint timeout, FuHidDeviceFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_hid_device_get_report(FuHidDevice *self, guint8 value, guint8 *buf, gsize bufsz, guint timeout, FuHidDeviceFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-hid-report-item.c000066400000000000000000000125731501337203100215420ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuHidDevice" #include "config.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-hid-report-item.h" #include "fu-input-stream.h" #include "fu-mem-private.h" #include "fu-partial-input-stream.h" #include "fu-string.h" /** * FuHidReportItem: * * See also: [class@FuHidDescriptor] */ struct _FuHidReportItem { FuFirmware parent_instance; guint32 value; }; G_DEFINE_TYPE(FuHidReportItem, fu_hid_report_item, FU_TYPE_FIRMWARE) FuHidItemKind fu_hid_report_item_get_kind(FuHidReportItem *self) { g_return_val_if_fail(FU_IS_HID_REPORT_ITEM(self), 0); return fu_firmware_get_idx(FU_FIRMWARE(self)) & 0b11; } guint32 fu_hid_report_item_get_value(FuHidReportItem *self) { g_return_val_if_fail(FU_IS_HID_REPORT_ITEM(self), 0); return self->value; } static void fu_hid_report_item_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuHidReportItem *self = FU_HID_REPORT_ITEM(firmware); fu_xmlb_builder_insert_kv(bn, "kind", fu_hid_item_kind_to_string(fu_hid_report_item_get_kind(self))); fu_xmlb_builder_insert_kx(bn, "value", self->value); } static gboolean fu_hid_report_item_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuHidReportItem *self = FU_HID_REPORT_ITEM(firmware); const guint8 size_lookup[] = {0, 1, 2, 4}; guint8 data_size; guint8 tag; guint8 val = 0; if (!fu_input_stream_read_u8(stream, 0x0, &val, error)) return FALSE; data_size = size_lookup[val & 0b11]; tag = (val & 0b11111100) >> 2; fu_firmware_set_idx(firmware, tag); fu_firmware_set_id(firmware, fu_hid_item_tag_to_string(tag)); if (tag == FU_HID_ITEM_TAG_LONG && data_size == 2) { gsize streamsz = 0; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz < 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not enough data to read long tag"); return FALSE; } if (!fu_input_stream_read_u8(stream, 1, &data_size, error)) return FALSE; } else { g_autoptr(GInputStream) partial_stream = NULL; if (data_size == 1) { guint8 value = 0; if (!fu_input_stream_read_u8(stream, 1, &value, error)) return FALSE; self->value = value; } else if (data_size == 2) { guint16 value = 0; if (!fu_input_stream_read_u16(stream, 1, &value, G_LITTLE_ENDIAN, error)) return FALSE; self->value = value; } else if (data_size == 4) { if (!fu_input_stream_read_u32(stream, 1, &self->value, G_LITTLE_ENDIAN, error)) return FALSE; } partial_stream = fu_partial_input_stream_new(stream, 1, data_size, error); if (partial_stream == NULL) { g_prefix_error(error, "failed to cut HID payload: "); return FALSE; } if (!fu_firmware_set_stream(firmware, partial_stream, error)) return FALSE; } /* success */ fu_firmware_set_size(firmware, 1 + data_size); return TRUE; } static GByteArray * fu_hid_report_item_write(FuFirmware *firmware, GError **error) { FuHidReportItem *self = FU_HID_REPORT_ITEM(firmware); g_autoptr(GByteArray) st = g_byte_array_new(); guint8 tmp = fu_firmware_get_idx(firmware) << 2; if (self->value == 0) { fu_byte_array_append_uint8(st, tmp); } else if (self->value <= G_MAXUINT8) { tmp |= 0b01; fu_byte_array_append_uint8(st, tmp); fu_byte_array_append_uint8(st, self->value); } else if (self->value <= G_MAXUINT16) { tmp |= 0b10; fu_byte_array_append_uint8(st, tmp); fu_byte_array_append_uint16(st, self->value, G_LITTLE_ENDIAN); } else { tmp |= 0b11; fu_byte_array_append_uint8(st, tmp); fu_byte_array_append_uint32(st, self->value, G_LITTLE_ENDIAN); } /* success */ return g_steal_pointer(&st); } static gboolean fu_hid_report_item_build(FuFirmware *firmware, XbNode *n, GError **error) { FuHidReportItem *self = FU_HID_REPORT_ITEM(firmware); const gchar *tmp; guint64 value = 0; /* optional data */ tmp = xb_node_query_text(n, "idx", NULL); if (tmp != NULL) { if (!fu_strtoull(tmp, &value, 0x0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_firmware_set_idx(firmware, value); fu_firmware_set_id(firmware, fu_hid_item_tag_to_string(value)); } tmp = xb_node_query_text(n, "id", NULL); if (tmp != NULL) { fu_firmware_set_id(firmware, tmp); fu_firmware_set_idx(firmware, fu_hid_item_tag_from_string(tmp)); } tmp = xb_node_query_text(n, "value", NULL); if (tmp != NULL) { if (!fu_strtoull(tmp, &value, 0x0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->value = value; } /* success */ return TRUE; } static void fu_hid_report_item_init(FuHidReportItem *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_NO_AUTO_DETECTION); } static void fu_hid_report_item_class_init(FuHidReportItemClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->export = fu_hid_report_item_export; firmware_class->parse = fu_hid_report_item_parse; firmware_class->write = fu_hid_report_item_write; firmware_class->build = fu_hid_report_item_build; } /** * fu_hid_report_item_new: * * Creates a new HID report item * * Returns: (transfer full): a #FuHidReportItem * * Since: 1.9.4 **/ FuHidReportItem * fu_hid_report_item_new(void) { return g_object_new(FU_TYPE_HID_REPORT_ITEM, NULL); } fwupd-2.0.10/libfwupdplugin/fu-hid-report-item.h000066400000000000000000000010451501337203100215370ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #include "fu-hid-struct.h" #define FU_TYPE_HID_REPORT_ITEM (fu_hid_report_item_get_type()) G_DECLARE_FINAL_TYPE(FuHidReportItem, fu_hid_report_item, FU, HID_REPORT_ITEM, FuFirmware) FuHidReportItem * fu_hid_report_item_new(void); FuHidItemKind fu_hid_report_item_get_kind(FuHidReportItem *self) G_GNUC_NON_NULL(1); guint32 fu_hid_report_item_get_value(FuHidReportItem *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-hid-report.c000066400000000000000000000016441501337203100206030ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuHidDevice" #include "config.h" #include "fu-hid-report.h" /** * FuHidReport: * * See also: [class@FuHidDescriptor] */ struct _FuHidReport { FuFirmware parent_instance; }; G_DEFINE_TYPE(FuHidReport, fu_hid_report, FU_TYPE_FIRMWARE) static void fu_hid_report_init(FuHidReport *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_DEDUPE_IDX); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_NO_AUTO_DETECTION); fu_firmware_set_images_max(FU_FIRMWARE(self), G_MAXUINT8); } static void fu_hid_report_class_init(FuHidReportClass *klass) { } /** * fu_hid_report_new: * * Creates a new HID report item * * Returns: (transfer full): a #FuHidReport * * Since: 1.9.4 **/ FuHidReport * fu_hid_report_new(void) { return g_object_new(FU_TYPE_HID_REPORT, NULL); } fwupd-2.0.10/libfwupdplugin/fu-hid-report.h000066400000000000000000000005031501337203100206010ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_HID_REPORT (fu_hid_report_get_type()) G_DECLARE_FINAL_TYPE(FuHidReport, fu_hid_report, FU, HID_REPORT, FuFirmware) FuHidReport * fu_hid_report_new(void); fwupd-2.0.10/libfwupdplugin/fu-hid.rs000066400000000000000000000017231501337203100174720ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString, FromString)] enum FuHidItemTag { Unknown = 0b0, // Main Input = 0b1000_00, Output = 0b1001_00, Feature = 0b1011_00, Collection = 0b1010_00, EndCollection = 0b1100_00, // Global UsagePage = 0b0000_01, LogicalMinimum = 0b0001_01, LogicalMaximum = 0b0010_01, PhysicalMinimum = 0b0011_01, PhysicalMaximum = 0b0100_01, Unit = 0b0101_01, ReportSize = 0b0111_01, ReportId = 0b1000_01, ReportCount = 0b1001_01, Push = 0b1010_01, Pop = 0b1011_01, // Local Usage = 0b0000_10, UsageMinimum = 0b0001_10, UsageMaximum = 0b0010_10, DesignatorIndex = 0b0011_10, DesignatorMinimum = 0b0100_10, DesignatorMaximum = 0b0101_10, StringIndex = 0b0111_10, StringMinimum = 0b1000_10, StringMaximum = 0b1001_10, // 'just' supported Long = 0b1111, } #[derive(ToString)] enum FuHidItemKind { Main, Global, Local, } fwupd-2.0.10/libfwupdplugin/fu-hidraw-device.c000066400000000000000000000166161501337203100212460ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuHidrawDevice" #include "config.h" #ifdef HAVE_HIDRAW_H #include #include #endif #include "fu-dump.h" #include "fu-hidraw-device.h" #include "fu-mem.h" #include "fu-string.h" #include "fu-udev-device-private.h" /** * FuHidrawDevice * * See also: #FuUdevDevice */ G_DEFINE_TYPE(FuHidrawDevice, fu_hidraw_device, FU_TYPE_UDEV_DEVICE) #define FU_HIDRAW_DEVICE_IOCTL_TIMEOUT 2500 /* ms */ static gboolean fu_hidraw_device_probe_usb(FuHidrawDevice *self, GError **error) { g_autoptr(FuDevice) usb_device = NULL; usb_device = fu_device_get_backend_parent_with_subsystem(FU_DEVICE(self), "usb:usb_device", error); if (usb_device == NULL) return FALSE; fu_device_incorporate(FU_DEVICE(self), FU_DEVICE(usb_device), FU_DEVICE_INCORPORATE_FLAG_POSSIBLE_PLUGINS | FU_DEVICE_INCORPORATE_FLAG_GTYPE); /* success */ return TRUE; } static gboolean fu_hidraw_device_probe(FuDevice *device, GError **error) { FuHidrawDevice *self = FU_HIDRAW_DEVICE(device); g_autofree gchar *prop_id = NULL; g_auto(GStrv) split = NULL; g_autoptr(FuDevice) hid_device = NULL; /* get device */ if (!fu_udev_device_parse_number(FU_UDEV_DEVICE(self), error)) return FALSE; /* get parent */ hid_device = fu_device_get_backend_parent_with_subsystem(device, "hid", error); if (hid_device == NULL) return FALSE; /* ID */ prop_id = fu_udev_device_read_property(FU_UDEV_DEVICE(hid_device), "HID_ID", error); if (prop_id == NULL) return FALSE; split = g_strsplit(prop_id, ":", -1); if (g_strv_length(split) == 3) { if (fu_device_get_vendor(FU_DEVICE(self)) == NULL) { guint64 val = 0; if (!fu_strtoull(split[1], &val, 0, G_MAXUINT16, FU_INTEGER_BASE_16, error)) { g_prefix_error(error, "failed to parse HID_ID: "); return FALSE; } fu_device_set_vid(device, (guint16)val); } if (fu_device_get_pid(device) == 0x0) { guint64 val = 0; if (!fu_strtoull(split[2], &val, 0, G_MAXUINT16, FU_INTEGER_BASE_16, error)) { g_prefix_error(error, "failed to parse HID_ID: "); return FALSE; } fu_device_set_pid(device, (guint16)val); } } /* set name */ if (fu_device_get_name(FU_DEVICE(self)) == NULL) { g_autofree gchar *prop_name = fu_udev_device_read_property(FU_UDEV_DEVICE(hid_device), "HID_NAME", NULL); if (prop_name != NULL) fu_device_set_name(FU_DEVICE(self), prop_name); } /* set the logical ID */ if (fu_device_get_logical_id(FU_DEVICE(self)) == NULL) { g_autofree gchar *logical_id = fu_udev_device_read_property(FU_UDEV_DEVICE(hid_device), "HID_UNIQ", NULL); if (logical_id != NULL && logical_id[0] != '\0') fu_device_set_logical_id(FU_DEVICE(self), logical_id); } /* set the physical ID */ if (fu_device_get_physical_id(FU_DEVICE(self)) == NULL) { g_autofree gchar *physical_id = NULL; physical_id = fu_udev_device_read_property(FU_UDEV_DEVICE(hid_device), "HID_PHYS", error); if (physical_id == NULL) return FALSE; fu_device_set_physical_id(FU_DEVICE(self), physical_id); /* this is from a USB device, so try to use the DS-20 descriptor */ if (g_str_has_prefix(physical_id, "usb")) { if (!fu_hidraw_device_probe_usb(self, error)) return FALSE; } } /* set the hidraw device */ if (fu_udev_device_get_device_file(FU_UDEV_DEVICE(self)) == NULL) { g_autofree gchar *device_file = NULL; device_file = fu_udev_device_get_device_file_from_subsystem(FU_UDEV_DEVICE(hid_device), "hidraw", error); if (device_file == NULL) return FALSE; fu_udev_device_set_device_file(FU_UDEV_DEVICE(self), device_file); } /* USB\\VID_1234 */ fu_device_add_instance_u16(FU_DEVICE(self), "VEN", fu_device_get_vid(device)); fu_device_add_instance_u16(FU_DEVICE(self), "DEV", fu_device_get_pid(device)); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "HIDRAW", "VEN", NULL); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_VISIBLE | FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "HIDRAW", "VEN", "DEV", NULL); fu_device_build_vendor_id_u16(device, "HIDRAW", fu_device_get_vid(device)); /* success */ return TRUE; } /** * fu_hidraw_device_set_feature: * @self: a #FuHidrawDevice * @buf: (not nullable): a buffer to use, which *must* be large enough for the request * @bufsz: the size of @buf * @flags: some #FuIoctlFlags, e.g. %FU_IOCTL_FLAG_RETRY * @error: (nullable): optional return location for an error * * Do a HID SetFeature request. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fu_hidraw_device_set_feature(FuHidrawDevice *self, const guint8 *buf, gsize bufsz, FuIoctlFlags flags, GError **error) { #ifdef HAVE_HIDRAW_H g_autofree guint8 *buf_mut = NULL; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); #endif g_return_val_if_fail(FU_IS_HIDRAW_DEVICE(self), FALSE); g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); #ifdef HAVE_HIDRAW_H fu_dump_raw(G_LOG_DOMAIN, "SetFeature", buf, bufsz); buf_mut = fu_memdup_safe(buf, bufsz, error); if (buf_mut == NULL) return FALSE; return fu_ioctl_execute(ioctl, HIDIOCSFEATURE(bufsz), /* nocheck:blocked */ buf_mut, bufsz, NULL, FU_HIDRAW_DEVICE_IOCTL_TIMEOUT, flags, error); #else /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, " not available"); return FALSE; #endif } /** * fu_hidraw_device_get_feature: * @self: a #FuHidrawDevice * @buf: (not nullable): a buffer to use, which *must* be large enough for the request * @bufsz: the size of @buf * @flags: some #FuIoctlFlags, e.g. %FU_IOCTL_FLAG_RETRY * @error: (nullable): optional return location for an error * * Do a HID GetFeature request. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fu_hidraw_device_get_feature(FuHidrawDevice *self, guint8 *buf, gsize bufsz, FuIoctlFlags flags, GError **error) { #ifdef HAVE_HIDRAW_H g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); #endif g_return_val_if_fail(FU_IS_HIDRAW_DEVICE(self), FALSE); g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); #ifdef HAVE_HIDRAW_H fu_dump_raw(G_LOG_DOMAIN, "GetFeature[req]", buf, bufsz); if (!fu_ioctl_execute(ioctl, HIDIOCGFEATURE(bufsz), /* nocheck:blocked */ buf, bufsz, NULL, FU_HIDRAW_DEVICE_IOCTL_TIMEOUT, flags, error)) return FALSE; fu_dump_raw(G_LOG_DOMAIN, "GetFeature[res]", buf, bufsz); /* success */ return TRUE; #else /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, " not available"); return FALSE; #endif } static void fu_hidraw_device_init(FuHidrawDevice *self) { fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); } static void fu_hidraw_device_class_init(FuHidrawDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_hidraw_device_probe; } fwupd-2.0.10/libfwupdplugin/fu-hidraw-device.h000066400000000000000000000014231501337203100212410ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-udev-device.h" #define FU_TYPE_HIDRAW_DEVICE (fu_hidraw_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuHidrawDevice, fu_hidraw_device, FU, HIDRAW_DEVICE, FuUdevDevice) struct _FuHidrawDeviceClass { FuUdevDeviceClass parent_class; }; gboolean fu_hidraw_device_set_feature(FuHidrawDevice *self, const guint8 *buf, gsize bufsz, FuIoctlFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_hidraw_device_get_feature(FuHidrawDevice *self, guint8 *buf, gsize bufsz, FuIoctlFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-hwids-config.c000066400000000000000000000013501501337203100211010ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuContext" #include "config.h" #include "fu-config.h" #include "fu-context-private.h" #include "fu-hwids-private.h" #include "fu-path.h" gboolean fu_hwids_config_setup(FuContext *ctx, FuHwids *self, GError **error) { FuConfig *config = fu_context_get_config(ctx); g_autoptr(GPtrArray) keys = fu_hwids_get_keys(self); /* all keys are optional */ for (guint i = 0; i < keys->len; i++) { const gchar *key = g_ptr_array_index(keys, i); g_autofree gchar *value = fu_config_get_value(config, "fwupd", key); if (value != NULL) fu_hwids_add_value(self, key, value); } /* success */ return TRUE; } fwupd-2.0.10/libfwupdplugin/fu-hwids-darwin.c000066400000000000000000000034101501337203100211170ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuContext" #include "config.h" #include "fu-context-private.h" #include "fu-hwids-private.h" gboolean fu_hwids_darwin_setup(FuContext *ctx, FuHwids *self, GError **error) { #ifdef HOST_MACHINE_SYSTEM_DARWIN struct { const gchar *hwid; const gchar *key; } map[] = {{FU_HWIDS_KEY_BIOS_VERSION, "System Firmware Version"}, {FU_HWIDS_KEY_FAMILY, "Model Name"}, {FU_HWIDS_KEY_PRODUCT_NAME, "Model Identifier"}, {NULL}}; const gchar *family = NULL; g_autofree gchar *standard_output = NULL; g_auto(GStrv) lines = NULL; /* parse the profiler output */ if (!g_spawn_command_line_sync("system_profiler SPHardwareDataType", &standard_output, NULL, NULL, error)) return FALSE; lines = g_strsplit(standard_output, "\n", -1); for (guint j = 0; lines[j] != NULL; j++) { for (guint i = 0; map[i].key != NULL; i++) { g_auto(GStrv) chunks = g_strsplit(lines[j], ":", 2); if (g_strv_length(chunks) != 2) continue; if (g_strstr_len(chunks[0], -1, map[i].key) != NULL) fu_hwids_add_value(self, map[i].hwid, g_strstrip(chunks[1])); } } /* this has to be hardcoded */ fu_hwids_add_value(self, FU_HWIDS_KEY_BASEBOARD_MANUFACTURER, "Apple"); fu_hwids_add_value(self, FU_HWIDS_KEY_MANUFACTURER, "Apple"); fu_hwids_add_value(self, FU_HWIDS_KEY_BIOS_VENDOR, "Apple"); /* set the chassis kind using the family */ family = fu_hwids_get_value(self, FU_HWIDS_KEY_FAMILY); if (g_strcmp0(family, "MacBook Pro") == 0) { fu_hwids_add_value(self, FU_HWIDS_KEY_ENCLOSURE_KIND, "a"); fu_context_set_chassis_kind(ctx, FU_SMBIOS_CHASSIS_KIND_LAPTOP); } #endif /* success */ return TRUE; } fwupd-2.0.10/libfwupdplugin/fu-hwids-dmi.c000066400000000000000000000041211501337203100204040ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuContext" #include "config.h" #include "fu-context-private.h" #include "fu-hwids-private.h" #include "fu-path.h" #include "fu-smbios-struct.h" #include "fu-string.h" gboolean fu_hwids_dmi_setup(FuContext *ctx, FuHwids *self, GError **error) { g_autofree gchar *path = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR_DMI); struct { const gchar *hwid; const gchar *key; } map[] = {{FU_HWIDS_KEY_BASEBOARD_MANUFACTURER, "board_vendor"}, {FU_HWIDS_KEY_BASEBOARD_PRODUCT, "board_name"}, {FU_HWIDS_KEY_BIOS_VENDOR, "bios_vendor"}, {FU_HWIDS_KEY_BIOS_VERSION, "bios_version"}, {FU_HWIDS_KEY_FAMILY, "product_family"}, {FU_HWIDS_KEY_MANUFACTURER, "sys_vendor"}, {FU_HWIDS_KEY_PRODUCT_NAME, "product_name"}, {FU_HWIDS_KEY_PRODUCT_SKU, "product_sku"}, {FU_HWIDS_KEY_ENCLOSURE_KIND, "chassis_type"}, {NULL, NULL}}; /* the values the kernel parsed; these are world-readable */ if (!g_file_test(path, G_FILE_TEST_IS_DIR)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no %s", path); return FALSE; } for (guint i = 0; map[i].key != NULL; i++) { gsize bufsz = 0; g_autofree gchar *buf = NULL; g_autofree gchar *fn = g_build_filename(path, map[i].key, NULL); g_autoptr(GError) error_local = NULL; if (!g_file_get_contents(fn, &buf, &bufsz, &error_local)) { g_debug("unable to read SMBIOS data from %s: %s", fn, error_local->message); continue; } if (bufsz == 0) continue; /* trim trailing newline added by kernel */ if (buf[bufsz - 1] == '\n') buf[bufsz - 1] = 0; fu_hwids_add_value(self, map[i].hwid, buf); if (g_strcmp0(map[i].hwid, FU_HWIDS_KEY_ENCLOSURE_KIND) == 0) { guint64 val = 0; if (!fu_strtoull(buf, &val, FU_SMBIOS_CHASSIS_KIND_OTHER, FU_SMBIOS_CHASSIS_KIND_LAST, FU_INTEGER_BASE_AUTO, &error_local)) { g_warning("ignoring enclosure kind %s", buf); continue; } fu_context_set_chassis_kind(ctx, val); } } /* success */ return TRUE; } fwupd-2.0.10/libfwupdplugin/fu-hwids-fdt.c000066400000000000000000000106611501337203100204160ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuContext" #include "config.h" #include "fu-context-private.h" #include "fu-fdt-firmware.h" #include "fu-hwids-private.h" gboolean fu_hwids_fdt_setup(FuContext *ctx, FuHwids *self, GError **error) { g_autofree gchar *chassis_type = NULL; g_auto(GStrv) compatible = NULL; g_autoptr(FuFirmware) fdt_img = NULL; g_autoptr(FuFdtImage) fdt_img_baseb = NULL; g_autoptr(FuFdtImage) fdt_img_fwver = NULL; g_autoptr(FuFirmware) fdt = NULL; struct { const gchar *hwid; const gchar *key; } map[] = {{FU_HWIDS_KEY_MANUFACTURER, "vendor"}, {FU_HWIDS_KEY_FAMILY, "model-name"}, {FU_HWIDS_KEY_PRODUCT_NAME, "model"}, {NULL, NULL}}; /* adds compatible GUIDs */ fdt = fu_context_get_fdt(ctx, error); if (fdt == NULL) return FALSE; fdt_img = fu_firmware_get_image_by_id(fdt, NULL, error); if (fdt_img == NULL) return FALSE; if (!fu_fdt_image_get_attr_strlist(FU_FDT_IMAGE(fdt_img), "compatible", &compatible, error)) return FALSE; for (guint i = 0; compatible[i] != NULL; i++) { g_autofree gchar *guid = fwupd_guid_hash_string(compatible[i]); g_debug("using %s for DT compatible %s", guid, compatible[i]); fu_hwids_add_guid(self, guid); } /* root node */ for (guint i = 0; map[i].key != NULL; i++) { g_autofree gchar *tmp = NULL; fu_fdt_image_get_attr_str(FU_FDT_IMAGE(fdt_img), map[i].key, &tmp, NULL); if (tmp == NULL) continue; fu_hwids_add_value(self, map[i].hwid, tmp); } /* chassis kind */ fu_fdt_image_get_attr_str(FU_FDT_IMAGE(fdt_img), "chassis-type", &chassis_type, NULL); if (chassis_type != NULL) { struct { FuSmbiosChassisKind chassis_kind; const gchar *dt; } chassis_map[] = {{FU_SMBIOS_CHASSIS_KIND_CONVERTIBLE, "convertible"}, {FU_SMBIOS_CHASSIS_KIND_EMBEDDED_PC, "embedded"}, {FU_SMBIOS_CHASSIS_KIND_HAND_HELD, "handset"}, {FU_SMBIOS_CHASSIS_KIND_LAPTOP, "laptop"}, {FU_SMBIOS_CHASSIS_KIND_TABLET, "tablet"}, {FU_SMBIOS_CHASSIS_KIND_UNKNOWN, NULL}}; for (guint i = 0; chassis_map[i].dt != NULL; i++) { if (g_strcmp0(chassis_type, chassis_map[i].dt) == 0) { fu_context_set_chassis_kind(ctx, chassis_map[i].chassis_kind); break; } } } /* fallback */ if (g_strv_length(compatible) > 0) { g_auto(GStrv) compatible0 = g_strsplit(compatible[0], ",", -1); fu_hwids_add_value(self, FU_HWIDS_KEY_MANUFACTURER, compatible0[0]); if (g_strv_length(compatible0) > 1) fu_hwids_add_value(self, FU_HWIDS_KEY_PRODUCT_NAME, compatible0[1]); } if (g_strv_length(compatible) > 1) fu_hwids_add_value(self, FU_HWIDS_KEY_FAMILY, compatible[1]); if (fu_context_get_chassis_kind(ctx) == FU_SMBIOS_CHASSIS_KIND_UNKNOWN) { if (fu_fdt_image_get_attr_str(FU_FDT_IMAGE(fdt_img), "battery", NULL, NULL)) fu_context_set_chassis_kind(ctx, FU_SMBIOS_CHASSIS_KIND_PORTABLE); } fdt_img_fwver = fu_fdt_firmware_get_image_by_path(FU_FDT_FIRMWARE(fdt), "/ibm,firmware-versions", NULL); if (fdt_img_fwver != NULL) { g_autofree gchar *version = NULL; fu_fdt_image_get_attr_str(FU_FDT_IMAGE(fdt_img), "version", &version, NULL); fu_hwids_add_value(self, FU_HWIDS_KEY_BIOS_VERSION, version); } /* fall back to the firmware unix time */ if (fdt_img_fwver == NULL) { fdt_img_fwver = fu_fdt_firmware_get_image_by_path(FU_FDT_FIRMWARE(fdt), "/chosen/bootloader", NULL); } if (fdt_img_fwver != NULL) { guint32 timestamp = 0; fu_fdt_image_get_attr_u32(FU_FDT_IMAGE(fdt_img_fwver), "build-timestamp", ×tamp, NULL); if (timestamp != 0) { g_autoptr(GDateTime) dt = g_date_time_new_from_unix_utc(timestamp); g_autofree gchar *version = g_date_time_format(dt, "%Y%m%d"); fu_hwids_add_value(self, FU_HWIDS_KEY_BIOS_VERSION, version); } } fdt_img_baseb = fu_fdt_firmware_get_image_by_path( FU_FDT_FIRMWARE(fdt), "/vpd/root-node-vpd@a000/enclosure@1e00/backplane@800", NULL); if (fdt_img_baseb != NULL) { g_autofree gchar *vendor = NULL; g_autofree gchar *product = NULL; fu_fdt_image_get_attr_str(FU_FDT_IMAGE(fdt_img_baseb), "vendor", &vendor, NULL); fu_fdt_image_get_attr_str(FU_FDT_IMAGE(fdt_img_baseb), "part-number", &product, NULL); if (vendor != NULL) fu_hwids_add_value(self, FU_HWIDS_KEY_BASEBOARD_MANUFACTURER, vendor); if (product != NULL) fu_hwids_add_value(self, FU_HWIDS_KEY_BASEBOARD_PRODUCT, product); } /* success */ return TRUE; } fwupd-2.0.10/libfwupdplugin/fu-hwids-kenv.c000066400000000000000000000023201501337203100205750ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuContext" #include "config.h" #include "fu-context-private.h" #include "fu-hwids-private.h" #include "fu-kenv.h" gboolean fu_hwids_kenv_setup(FuContext *ctx, FuHwids *self, GError **error) { #ifdef HAVE_KENV_H struct { const gchar *hwid; const gchar *key; } map[] = {{FU_HWIDS_KEY_BASEBOARD_MANUFACTURER, "smbios.planar.maker"}, {FU_HWIDS_KEY_BASEBOARD_PRODUCT, "smbios.planar.product"}, {FU_HWIDS_KEY_BIOS_VENDOR, "smbios.bios.vendor"}, {FU_HWIDS_KEY_BIOS_VERSION, "smbios.bios.version"}, {FU_HWIDS_KEY_FAMILY, "smbios.system.family"}, {FU_HWIDS_KEY_MANUFACTURER, "smbios.system.maker"}, {FU_HWIDS_KEY_PRODUCT_NAME, "smbios.system.product"}, {FU_HWIDS_KEY_PRODUCT_SKU, "smbios.system.sku"}, {{NULL, NULL}}}; for (guint i = 0; map[i].key != NULL; i++) { g_autoptr(GError) error_local = NULL; g_autofree gchar *value = fu_kenv_get_string(map[i].key, error_local); if (value == NULL) { g_debug("ignoring: %s", error_local->message); continue; } fu_hwids_add_value(self, map[i].hwid, value); } #endif /* success */ return TRUE; } fwupd-2.0.10/libfwupdplugin/fu-hwids-private.h000066400000000000000000000017011501337203100213130ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-context.h" #include "fu-hwids.h" FuHwids * fu_hwids_new(void); void fu_hwids_add_chid(FuHwids *self, const gchar *key, const gchar *value); gboolean fu_hwids_setup(FuHwids *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_hwids_config_setup(FuContext *ctx, FuHwids *self, GError **error) G_GNUC_NON_NULL(1); gboolean fu_hwids_dmi_setup(FuContext *ctx, FuHwids *self, GError **error) G_GNUC_NON_NULL(1); gboolean fu_hwids_fdt_setup(FuContext *ctx, FuHwids *self, GError **error) G_GNUC_NON_NULL(1); gboolean fu_hwids_kenv_setup(FuContext *ctx, FuHwids *self, GError **error) G_GNUC_NON_NULL(1); gboolean fu_hwids_darwin_setup(FuContext *ctx, FuHwids *self, GError **error) G_GNUC_NON_NULL(1); gboolean fu_hwids_smbios_setup(FuContext *ctx, FuHwids *self, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-hwids-smbios.c000066400000000000000000000104011501337203100211250ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuContext" #include "config.h" #include "fu-context-private.h" #include "fu-hwids-private.h" #include "fu-smbios-private.h" #include "fu-string.h" typedef gchar *(*FuContextHwidConvertFunc)(FuSmbios *smbios, guint8 type, guint8 offset, GError **error); static gchar * fu_hwids_smbios_convert_string_table_cb(FuSmbios *smbios, guint8 type, guint8 offset, GError **error) { const gchar *tmp = fu_smbios_get_string(smbios, type, FU_SMBIOS_STRUCTURE_LENGTH_ANY, offset, error); if (tmp == NULL) return NULL; /* ComputerHardwareIds.exe seems to strip spaces */ return fu_strstrip(tmp); } static gchar * fu_hwids_smbios_convert_padded_integer_cb(FuSmbios *smbios, guint8 type, guint8 offset, GError **error) { guint tmp = fu_smbios_get_integer(smbios, type, FU_SMBIOS_STRUCTURE_LENGTH_ANY, offset, error); if (tmp == G_MAXUINT) return NULL; return g_strdup_printf("%02x", tmp); } static gchar * fu_hwids_smbios_convert_integer_cb(FuSmbios *smbios, guint8 type, guint8 offset, GError **error) { guint tmp = fu_smbios_get_integer(smbios, type, FU_SMBIOS_STRUCTURE_LENGTH_ANY, offset, error); if (tmp == G_MAXUINT) return NULL; return g_strdup_printf("%x", tmp); } gboolean fu_hwids_smbios_setup(FuContext *ctx, FuHwids *self, GError **error) { FuSmbios *smbios = fu_context_get_smbios(ctx); struct { const gchar *key; guint8 type; guint8 offset; FuContextHwidConvertFunc func; } map[] = {{FU_HWIDS_KEY_MANUFACTURER, FU_SMBIOS_STRUCTURE_TYPE_SYSTEM, 0x04, fu_hwids_smbios_convert_string_table_cb}, {FU_HWIDS_KEY_ENCLOSURE_KIND, FU_SMBIOS_STRUCTURE_TYPE_CHASSIS, 0x05, fu_hwids_smbios_convert_integer_cb}, {FU_HWIDS_KEY_FAMILY, FU_SMBIOS_STRUCTURE_TYPE_SYSTEM, 0x1a, fu_hwids_smbios_convert_string_table_cb}, {FU_HWIDS_KEY_PRODUCT_NAME, FU_SMBIOS_STRUCTURE_TYPE_SYSTEM, 0x05, fu_hwids_smbios_convert_string_table_cb}, {FU_HWIDS_KEY_PRODUCT_SKU, FU_SMBIOS_STRUCTURE_TYPE_SYSTEM, 0x19, fu_hwids_smbios_convert_string_table_cb}, {FU_HWIDS_KEY_BIOS_VENDOR, FU_SMBIOS_STRUCTURE_TYPE_BIOS, 0x04, fu_hwids_smbios_convert_string_table_cb}, {FU_HWIDS_KEY_BIOS_VERSION, FU_SMBIOS_STRUCTURE_TYPE_BIOS, 0x05, fu_hwids_smbios_convert_string_table_cb}, {FU_HWIDS_KEY_BIOS_MAJOR_RELEASE, FU_SMBIOS_STRUCTURE_TYPE_BIOS, 0x14, fu_hwids_smbios_convert_padded_integer_cb}, {FU_HWIDS_KEY_BIOS_MINOR_RELEASE, FU_SMBIOS_STRUCTURE_TYPE_BIOS, 0x15, fu_hwids_smbios_convert_padded_integer_cb}, {FU_HWIDS_KEY_FIRMWARE_MAJOR_RELEASE, FU_SMBIOS_STRUCTURE_TYPE_BIOS, 0x16, fu_hwids_smbios_convert_padded_integer_cb}, {FU_HWIDS_KEY_FIRMWARE_MINOR_RELEASE, FU_SMBIOS_STRUCTURE_TYPE_BIOS, 0x17, fu_hwids_smbios_convert_padded_integer_cb}, {FU_HWIDS_KEY_BASEBOARD_MANUFACTURER, FU_SMBIOS_STRUCTURE_TYPE_BASEBOARD, 0x04, fu_hwids_smbios_convert_string_table_cb}, {FU_HWIDS_KEY_BASEBOARD_PRODUCT, FU_SMBIOS_STRUCTURE_TYPE_BASEBOARD, 0x05, fu_hwids_smbios_convert_string_table_cb}, {NULL, 0x00, 0x00, NULL}}; if (!fu_smbios_setup(smbios, error)) return FALSE; /* get all DMI data from SMBIOS */ fu_context_set_chassis_kind(ctx, fu_smbios_get_integer(smbios, FU_SMBIOS_STRUCTURE_TYPE_CHASSIS, FU_SMBIOS_STRUCTURE_LENGTH_ANY, 0x05, NULL)); for (guint i = 0; map[i].key != NULL; i++) { const gchar *contents_hdr = NULL; g_autofree gchar *contents = NULL; g_autoptr(GError) error_local = NULL; contents = map[i].func(smbios, map[i].type, map[i].offset, &error_local); if (contents == NULL) { g_debug("ignoring %s: %s", map[i].key, error_local->message); continue; } g_info("SMBIOS %s=%s", map[i].key, contents); /* weirdly, remove leading zeros */ contents_hdr = contents; while (contents_hdr[0] == '0' && map[i].func != fu_hwids_smbios_convert_padded_integer_cb) contents_hdr++; fu_hwids_add_value(self, map[i].key, contents_hdr); } /* success */ return TRUE; } fwupd-2.0.10/libfwupdplugin/fu-hwids.c000066400000000000000000000311341501337203100176410ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuHwids" #include "config.h" #include #include #include "fwupd-common.h" #include "fwupd-error.h" #include "fu-common.h" #include "fu-hwids-private.h" #include "fu-path.h" #include "fu-string.h" /** * FuHwids: * * A the hardware IDs on the system. * * Note, these are called "CHIDs" in Microsoft Windows and the results here * will match that of `ComputerHardwareIds.exe`. * * See also: [class@FuSmbios] */ struct _FuHwids { GObject parent_instance; GHashTable *hash_values; /* BiosVersion->"1.2.3 " */ GHashTable *hash_values_display; /* BiosVersion->"1.2.3" */ GHashTable *hash_guid; /* a-c-b-d->1 */ GPtrArray *array_guids; /* a-c-b-d */ GHashTable *chids; /* "HardwareID-5"->"Manufacturer&ProductName" */ }; G_DEFINE_TYPE(FuHwids, fu_hwids, G_TYPE_OBJECT) /** * fu_hwids_get_value: * @self: a #FuHwids * @key: a DMI ID, e.g. `BiosVersion` * * Gets the cached value for one specific key that is valid ASCII and suitable * for display. * * Returns: the string, e.g. `1.2.3`, or %NULL if not found * * Since: 0.9.3 **/ const gchar * fu_hwids_get_value(FuHwids *self, const gchar *key) { return g_hash_table_lookup(self->hash_values_display, key); } /** * fu_hwids_has_guid: * @self: a #FuHwids * @guid: a GUID, e.g. `059eb22d-6dc7-59af-abd3-94bbe017f67c` * * Finds out if a hardware GUID exists. * * Returns: %TRUE if the GUID exists * * Since: 0.9.3 **/ gboolean fu_hwids_has_guid(FuHwids *self, const gchar *guid) { return g_hash_table_lookup(self->hash_guid, guid) != NULL; } /** * fu_hwids_get_guids: * @self: a #FuHwids * * Returns all the defined HWIDs * * Returns: (transfer none) (element-type utf8): an array of GUIDs * * Since: 0.9.3 **/ GPtrArray * fu_hwids_get_guids(FuHwids *self) { return self->array_guids; } /** * fu_hwids_get_keys: * @self: a #FuHwids * * Returns all the defined HWID keys. * * Returns: (transfer container) (element-type utf8): All the known keys, * e.g. %FU_HWIDS_KEY_FAMILY * * Since: 1.5.6 **/ GPtrArray * fu_hwids_get_keys(FuHwids *self) { GPtrArray *array = g_ptr_array_new(); const gchar *keys[] = {FU_HWIDS_KEY_BIOS_VENDOR, FU_HWIDS_KEY_BIOS_VERSION, FU_HWIDS_KEY_BIOS_MAJOR_RELEASE, FU_HWIDS_KEY_BIOS_MINOR_RELEASE, FU_HWIDS_KEY_FIRMWARE_MAJOR_RELEASE, FU_HWIDS_KEY_FIRMWARE_MINOR_RELEASE, FU_HWIDS_KEY_MANUFACTURER, FU_HWIDS_KEY_FAMILY, FU_HWIDS_KEY_PRODUCT_NAME, FU_HWIDS_KEY_PRODUCT_SKU, FU_HWIDS_KEY_ENCLOSURE_KIND, FU_HWIDS_KEY_BASEBOARD_MANUFACTURER, FU_HWIDS_KEY_BASEBOARD_PRODUCT, NULL}; g_return_val_if_fail(FU_IS_HWIDS(self), NULL); for (guint i = 0; keys[i] != NULL; i++) g_ptr_array_add(array, (gpointer)keys[i]); return array; } static gchar * fu_hwids_get_guid_for_str(const gchar *str, GError **error) { glong items_written = 0; g_autofree gunichar2 *data = NULL; /* convert to UTF-16 and convert to GUID using custom namespace */ data = g_utf8_to_utf16(str, -1, NULL, &items_written, error); if (data == NULL) return NULL; if (items_written == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no GUIDs in data"); return NULL; } /* ensure the data is in little endian format */ for (glong i = 0; i < items_written; i++) data[i] = GUINT16_TO_LE(data[i]); /* nocheck:blocked */ /* convert to a GUID */ return fwupd_guid_hash_data((guint8 *)data, items_written * 2, FWUPD_GUID_FLAG_NAMESPACE_MICROSOFT); } /** * fu_hwids_get_replace_keys: * @self: a #FuHwids * @key: a CHID key, e.g. `HardwareID-03` * * Gets the defined values for a well known value. * * Returns: the replacement value, e.g. `Manufacturer&ProductName`, or %NULL for error. * * Since: 0.9.3 **/ const gchar * fu_hwids_get_replace_keys(FuHwids *self, const gchar *key) { const gchar *value; g_return_val_if_fail(FU_IS_HWIDS(self), NULL); g_return_val_if_fail(key != NULL, NULL); value = g_hash_table_lookup(self->chids, key); if (value != NULL) return value; return key; } /** * fu_hwids_add_chid: * @self: a #FuHwids * @key: an textual ID, e.g. `HardwareID-05` * @value: a composite hardware key, e.g. `Manufacturer&ProductName` * * Defines a "Computer Hardware ID" in terms of a set of SMBIOS values. * * Since: 1.9.16 **/ void fu_hwids_add_chid(FuHwids *self, const gchar *key, const gchar *value) { g_return_if_fail(FU_IS_HWIDS(self)); g_return_if_fail(key != NULL); g_return_if_fail(value != NULL); g_hash_table_insert(self->chids, g_strdup(key), g_strdup(value)); } static gint fu_hwids_sort_keys_cb(gconstpointer a, gconstpointer b) { const gchar *key1 = *((gchar **)a); const gchar *key2 = *((gchar **)b); return g_strcmp0(key1, key2); } /** * fu_hwids_get_chid_keys: * @self: a #FuHwids * * Returns all the CHID keys added by fu_hwids_add_chid(). * * Returns: (transfer container) (element-type utf8): IDs * * Since: 1.9.16 **/ GPtrArray * fu_hwids_get_chid_keys(FuHwids *self) { GHashTableIter iter; gpointer key; g_autoptr(GPtrArray) keys = g_ptr_array_new_with_free_func(g_free); g_return_val_if_fail(FU_IS_HWIDS(self), NULL); g_hash_table_iter_init(&iter, self->chids); while (g_hash_table_iter_next(&iter, &key, NULL)) g_ptr_array_add(keys, g_strdup(key)); g_ptr_array_sort(keys, fu_hwids_sort_keys_cb); return g_steal_pointer(&keys); } /** * fu_hwids_add_value: * @self: a #FuHwids * @key: a key, e.g. %FU_HWIDS_KEY_PRODUCT_SKU * @value: (nullable): a new value, e.g. `ExampleModel` * * Sets override values so you can emulate another system. * * This function has no effect if called after fu_hwids_setup() * * Since: 1.8.10 **/ void fu_hwids_add_value(FuHwids *self, const gchar *key, const gchar *value) { g_return_if_fail(FU_IS_HWIDS(self)); g_return_if_fail(key != NULL); /* does not replace; first value set wins */ if (g_hash_table_contains(self->hash_values, key)) return; g_hash_table_insert(self->hash_values, g_strdup(key), g_strdup(value)); /* make suitable for display */ if (value != NULL) { g_autofree gchar *value_safe = g_str_to_ascii(value, "C"); g_strdelimit(value_safe, "\n\r", '\0'); g_strchomp(value_safe); g_hash_table_insert(self->hash_values_display, g_strdup(key), g_steal_pointer(&value_safe)); } else { g_hash_table_insert(self->hash_values_display, g_strdup(key), NULL); } } /** * fu_hwids_get_replace_values: * @self: a #FuHwids * @keys: a key, e.g. `HardwareID-3` or %FU_HWIDS_KEY_PRODUCT_SKU * @error: (nullable): optional return location for an error * * Gets the replacement values for a HardwareID key or plain key. * * Returns: a string, e.g. `LENOVO&ThinkPad T440s`, or %NULL for error. * * Since: 0.9.3 **/ gchar * fu_hwids_get_replace_values(FuHwids *self, const gchar *keys, GError **error) { g_auto(GStrv) split = NULL; g_autoptr(GString) str = g_string_new(NULL); g_return_val_if_fail(FU_IS_HWIDS(self), NULL); g_return_val_if_fail(keys != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* do any replacements */ keys = fu_hwids_get_replace_keys(self, keys); /* get each part of the HWID */ split = g_strsplit(keys, "&", -1); for (guint j = 0; split[j] != NULL; j++) { const gchar *tmp = g_hash_table_lookup(self->hash_values, split[j]); if (tmp == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "not available as '%s' unknown", split[j]); return NULL; } g_string_append_printf(str, "%s&", tmp); } if (str->len > 0) g_string_truncate(str, str->len - 1); return g_strdup(str->str); } /** * fu_hwids_get_guid: * @self: a #FuHwids * @keys: a key, e.g. `HardwareID-3` or %FU_HWIDS_KEY_PRODUCT_SKU * @error: (nullable): optional return location for an error * * Gets the GUID for a specific key. * * Returns: a string, or %NULL for error. * * Since: 0.9.3 **/ gchar * fu_hwids_get_guid(FuHwids *self, const gchar *keys, GError **error) { g_autofree gchar *tmp = NULL; g_return_val_if_fail(FU_IS_HWIDS(self), NULL); g_return_val_if_fail(keys != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); tmp = fu_hwids_get_replace_values(self, keys, error); if (tmp == NULL) return NULL; return fu_hwids_get_guid_for_str(tmp, error); } /** * fu_hwids_add_guid: * @self: a #FuHwids * @guid: a GUID * * Adds a HWID GUID value. * * Since: 1.8.10 **/ void fu_hwids_add_guid(FuHwids *self, const gchar *guid) { g_return_if_fail(FU_IS_HWIDS(self)); g_return_if_fail(guid != NULL); g_hash_table_insert(self->hash_guid, g_strdup(guid), GUINT_TO_POINTER(1)); g_ptr_array_add(self->array_guids, g_strdup(guid)); } /** * fu_hwids_setup: * @self: a #FuHwids * @error: (nullable): optional return location for an error * * Adds all the `HardwareID` GUIDs from the previously supplied data. * * Returns: %TRUE for success * * Since: 0.9.3 **/ gboolean fu_hwids_setup(FuHwids *self, GError **error) { g_autoptr(GPtrArray) chids = fu_hwids_get_chid_keys(self); g_return_val_if_fail(FU_IS_HWIDS(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* add GUIDs */ for (guint i = 0; i < chids->len; i++) { const gchar *key = g_ptr_array_index(chids, i); g_autofree gchar *guid = NULL; g_autoptr(GError) error_local = NULL; /* get the GUID and add to hash */ guid = fu_hwids_get_guid(self, key, &error_local); if (guid == NULL) { g_debug("%s is not available, %s", key, error_local->message); continue; } fu_hwids_add_guid(self, guid); } return TRUE; } static void fu_hwids_finalize(GObject *object) { FuHwids *self; g_return_if_fail(FU_IS_HWIDS(object)); self = FU_HWIDS(object); g_hash_table_unref(self->hash_values); g_hash_table_unref(self->hash_values_display); g_hash_table_unref(self->hash_guid); g_hash_table_unref(self->chids); g_ptr_array_unref(self->array_guids); G_OBJECT_CLASS(fu_hwids_parent_class)->finalize(object); } static void fu_hwids_class_init(FuHwidsClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_hwids_finalize; } static void fu_hwids_init(FuHwids *self) { self->hash_values = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); self->hash_values_display = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); self->hash_guid = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); self->chids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); self->array_guids = g_ptr_array_new_with_free_func(g_free); /* Windows 10 CHIDs */ fu_hwids_add_chid(self, "HardwareID-00", "Manufacturer&Family&ProductName&ProductSku&BiosVendor&BiosVersion&" "BiosMajorRelease&BiosMinorRelease"); fu_hwids_add_chid(self, "HardwareID-01", "Manufacturer&Family&ProductName&BiosVendor&BiosVersion&" "BiosMajorRelease&BiosMinorRelease"); fu_hwids_add_chid(self, "HardwareID-02", "Manufacturer&ProductName&BiosVendor&BiosVersion&" "BiosMajorRelease&BiosMinorRelease"); fu_hwids_add_chid(self, "HardwareID-03", "Manufacturer&Family&ProductName&ProductSku&" "BaseboardManufacturer&BaseboardProduct"); fu_hwids_add_chid(self, "HardwareID-04", "Manufacturer&Family&ProductName&ProductSku"); fu_hwids_add_chid(self, "HardwareID-05", "Manufacturer&Family&ProductName"); fu_hwids_add_chid(self, "HardwareID-06", "Manufacturer&ProductSku&BaseboardManufacturer&BaseboardProduct"); fu_hwids_add_chid(self, "HardwareID-07", "Manufacturer&ProductSku"); fu_hwids_add_chid(self, "HardwareID-08", "Manufacturer&ProductName&BaseboardManufacturer&BaseboardProduct"); fu_hwids_add_chid(self, "HardwareID-09", "Manufacturer&ProductName"); fu_hwids_add_chid(self, "HardwareID-10", "Manufacturer&Family&BaseboardManufacturer&BaseboardProduct"); fu_hwids_add_chid(self, "HardwareID-11", "Manufacturer&Family"); fu_hwids_add_chid(self, "HardwareID-12", "Manufacturer&EnclosureKind"); fu_hwids_add_chid(self, "HardwareID-13", "Manufacturer&BaseboardManufacturer&BaseboardProduct"); fu_hwids_add_chid(self, "HardwareID-14", "Manufacturer"); /* used by the flashrom plugin */ fu_hwids_add_chid(self, "fwupd-04", "Manufacturer&Family&ProductName&ProductSku&BiosVendor"); fu_hwids_add_chid(self, "fwupd-05", "Manufacturer&Family&ProductName&BiosVendor"); fu_hwids_add_chid(self, "fwupd-14", "Manufacturer&BiosVendor"); } /** * fu_hwids_new: * * Creates a new #FuHwids * * Since: 0.9.3 **/ FuHwids * fu_hwids_new(void) { FuHwids *self; self = g_object_new(FU_TYPE_HWIDS, NULL); return FU_HWIDS(self); } fwupd-2.0.10/libfwupdplugin/fu-hwids.h000066400000000000000000000063311501337203100176470ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-smbios.h" #define FU_TYPE_HWIDS (fu_hwids_get_type()) G_DECLARE_FINAL_TYPE(FuHwids, fu_hwids, FU, HWIDS, GObject) /** * FU_HWIDS_KEY_BASEBOARD_MANUFACTURER: * * The HwID key for the baseboard (motherboard) vendor. * * Since: 1.3.7 **/ #define FU_HWIDS_KEY_BASEBOARD_MANUFACTURER "BaseboardManufacturer" /** * FU_HWIDS_KEY_BASEBOARD_PRODUCT: * * The HwID key for baseboard (motherboard) product. * * Since: 1.3.7 **/ #define FU_HWIDS_KEY_BASEBOARD_PRODUCT "BaseboardProduct" /** * FU_HWIDS_KEY_BIOS_MAJOR_RELEASE: * * The HwID key for the BIOS major version. * * Since: 1.3.7 **/ #define FU_HWIDS_KEY_BIOS_MAJOR_RELEASE "BiosMajorRelease" /** * FU_HWIDS_KEY_BIOS_MINOR_RELEASE: * * The HwID key for the BIOS minor version. * * Since: 1.3.7 **/ #define FU_HWIDS_KEY_BIOS_MINOR_RELEASE "BiosMinorRelease" /** * FU_HWIDS_KEY_BIOS_VENDOR: * * The HwID key for the BIOS vendor. * * Since: 1.3.7 **/ #define FU_HWIDS_KEY_BIOS_VENDOR "BiosVendor" /** * FU_HWIDS_KEY_BIOS_VERSION: * * The HwID key for the BIOS version. * * Since: 1.3.7 **/ #define FU_HWIDS_KEY_BIOS_VERSION "BiosVersion" /** * FU_HWIDS_KEY_FIRMWARE_MAJOR_RELEASE: * * The HwID key for the firmware major version. * * Since: 1.6.1 **/ #define FU_HWIDS_KEY_FIRMWARE_MAJOR_RELEASE "FirmwareMajorRelease" /** * FU_HWIDS_KEY_FIRMWARE_MINOR_RELEASE: * * The HwID key for the firmware minor version. * * Since: 1.6.1 **/ #define FU_HWIDS_KEY_FIRMWARE_MINOR_RELEASE "FirmwareMinorRelease" /** * FU_HWIDS_KEY_ENCLOSURE_KIND: * * The HwID key for the enclosure kind. * * Since: 1.3.7 **/ #define FU_HWIDS_KEY_ENCLOSURE_KIND "EnclosureKind" /** * FU_HWIDS_KEY_FAMILY: * * The HwID key for the deice family. * * Since: 1.3.7 **/ #define FU_HWIDS_KEY_FAMILY "Family" /** * FU_HWIDS_KEY_MANUFACTURER: * * The HwID key for the top-level product vendor. * * Since: 1.3.7 **/ #define FU_HWIDS_KEY_MANUFACTURER "Manufacturer" /** * FU_HWIDS_KEY_PRODUCT_NAME: * * The HwID key for the top-level product name. * * Since: 1.3.7 **/ #define FU_HWIDS_KEY_PRODUCT_NAME "ProductName" /** * FU_HWIDS_KEY_PRODUCT_SKU: * * The HwID key for the top-level product SKU. * * Since: 1.3.7 **/ #define FU_HWIDS_KEY_PRODUCT_SKU "ProductSku" GPtrArray * fu_hwids_get_keys(FuHwids *self) G_GNUC_NON_NULL(1); const gchar * fu_hwids_get_value(FuHwids *self, const gchar *key) G_GNUC_NON_NULL(1, 2); void fu_hwids_add_value(FuHwids *self, const gchar *key, const gchar *value) G_GNUC_NON_NULL(1, 2); GPtrArray * fu_hwids_get_chid_keys(FuHwids *self); const gchar * fu_hwids_get_replace_keys(FuHwids *self, const gchar *key) G_GNUC_NON_NULL(1, 2); gchar * fu_hwids_get_replace_values(FuHwids *self, const gchar *keys, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gchar * fu_hwids_get_guid(FuHwids *self, const gchar *keys, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); GPtrArray * fu_hwids_get_guids(FuHwids *self) G_GNUC_NON_NULL(1); void fu_hwids_add_guid(FuHwids *self, const gchar *guid) G_GNUC_NON_NULL(1); gboolean fu_hwids_has_guid(FuHwids *self, const gchar *guid) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-i2c-device.c000066400000000000000000000142261501337203100204400ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuI2cDevice" #include "config.h" #include #include #include #include #ifdef HAVE_ERRNO_H #include #endif #ifdef HAVE_I2C_DEV_H #include #endif #include "fu-device-private.h" #include "fu-i2c-device.h" #include "fu-string.h" #include "fu-udev-device-private.h" #define FU_I2C_DEVICE_IOCTL_TIMEOUT 2000 /** * FuI2cDevice * * A I²C device with an assigned bus number. * * See also: #FuUdevDevice */ G_DEFINE_TYPE(FuI2cDevice, fu_i2c_device, FU_TYPE_UDEV_DEVICE) static gboolean fu_i2c_device_probe(FuDevice *device, GError **error) { FuI2cDevice *self = FU_I2C_DEVICE(device); g_autofree gchar *attr_name = NULL; /* FuUdevDevice */ if (!FU_DEVICE_CLASS(fu_i2c_device_parent_class)->probe(device, error)) return FALSE; /* set physical ID */ if (!fu_udev_device_set_physical_id(FU_UDEV_DEVICE(device), "i2c", error)) return FALSE; /* i2c devices all expose a name */ attr_name = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(self), "name", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, error); if (attr_name == NULL) return FALSE; fu_device_add_instance_strsafe(device, "NAME", attr_name); if (!fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_QUIRKS, error, "I2C", "NAME", NULL)) return FALSE; /* get bus number out of sysfs path */ if (g_strcmp0(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device)), "i2c") != 0) { g_autoptr(FuUdevDevice) udev_parent = FU_UDEV_DEVICE( fu_device_get_backend_parent_with_subsystem(device, "i2c", NULL)); if (udev_parent != NULL) { if (!fu_udev_device_parse_number(udev_parent, error)) return FALSE; fu_udev_device_set_number(FU_UDEV_DEVICE(self), fu_udev_device_get_number(udev_parent)); } } /* set the device file manually */ if (fu_udev_device_get_device_file(FU_UDEV_DEVICE(self)) == NULL) { const gchar *sysfs = fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(self)); g_auto(GStrv) tokens = fu_strsplit(sysfs, strlen(sysfs), "/", -1); g_autofree gchar *devfile = NULL; guint64 number = G_MAXUINT64; for (guint i = 0; tokens[i] != NULL; i++) { if (!g_str_has_prefix(tokens[i], "i2c-")) continue; if (!fu_strtoull(tokens[i] + 4, &number, 0x0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error)) return FALSE; break; } if (number == G_MAXUINT64) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Could not find i2c bus number in sysfs path"); return FALSE; } fu_udev_device_set_number(FU_UDEV_DEVICE(self), number); devfile = g_strdup_printf("/dev/i2c-%" G_GUINT64_FORMAT, number); fu_udev_device_set_device_file(FU_UDEV_DEVICE(self), devfile); } /* i2c devices are often tied to the platform, and usually have very unhelpful names */ if (!fu_device_has_private_flag(FU_DEVICE(self), FU_I2C_DEVICE_PRIVATE_FLAG_NO_HWID_GUIDS)) { GPtrArray *hwid_guids = fu_context_get_hwid_guids(fu_device_get_context(device)); for (guint i = 0; i < hwid_guids->len; i++) { const gchar *hwid_guid = g_ptr_array_index(hwid_guids, i); fu_device_add_instance_str(device, "HWID", hwid_guid); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "I2C", "NAME", "HWID", NULL); } } /* success */ return TRUE; } /** * fu_i2c_device_set_address: * @self: a #FuI2cDevice * @address: address * @force: Force the address, even if the device is device is busy (typically with a kernel driver) * @error: (nullable): optional return location for an error * * Sets the I²C device address. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fu_i2c_device_set_address(FuI2cDevice *self, guint8 address, gboolean force, GError **error) { #ifdef HAVE_I2C_DEV_H g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); if (!fu_ioctl_execute(ioctl, force ? I2C_SLAVE_FORCE : I2C_SLAVE, (guint8 *)(guintptr)address, sizeof(guintptr), NULL, FU_I2C_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_NONE, error)) { g_prefix_error(error, "failed to set address 0x%02x: ", address); return FALSE; } /* success */ return TRUE; #else g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Not supported as not found"); return FALSE; #endif } /** * fu_i2c_device_write: * @self: a #FuI2cDevice * @buf: (out): data * @bufsz: size of @data * @error: (nullable): optional return location for an error * * Write multiple bytes to the I²C device. * * Returns: %TRUE for success * * Since: 1.8.2 **/ gboolean fu_i2c_device_write(FuI2cDevice *self, const guint8 *buf, gsize bufsz, GError **error) { return fu_udev_device_pwrite(FU_UDEV_DEVICE(self), 0x0, buf, bufsz, error); } /** * fu_i2c_device_read: * @self: a #FuI2cDevice * @buf: (out): data * @bufsz: size of @data * @error: (nullable): optional return location for an error * * Read multiple bytes from the I²C device. * * Returns: %TRUE for success * * Since: 1.8.2 **/ gboolean fu_i2c_device_read(FuI2cDevice *self, guint8 *buf, gsize bufsz, GError **error) { return fu_udev_device_pread(FU_UDEV_DEVICE(self), 0x0, buf, bufsz, error); } static void fu_i2c_device_register_flags(FuDevice *device) { FU_DEVICE_CLASS(fu_i2c_device_parent_class)->register_flags(device); fu_device_register_private_flag_safe(device, FU_I2C_DEVICE_PRIVATE_FLAG_NO_HWID_GUIDS); } static void fu_i2c_device_init(FuI2cDevice *self) { fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); } static void fu_i2c_device_class_init(FuI2cDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_i2c_device_probe; device_class->register_flags = fu_i2c_device_register_flags; } fwupd-2.0.10/libfwupdplugin/fu-i2c-device.h000066400000000000000000000017221501337203100204420ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-udev-device.h" #define FU_TYPE_I2C_DEVICE (fu_i2c_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuI2cDevice, fu_i2c_device, FU, I2C_DEVICE, FuUdevDevice) struct _FuI2cDeviceClass { FuUdevDeviceClass parent_class; }; /** * FU_I2C_DEVICE_PRIVATE_FLAG_NO_HWID_GUIDS: * * Do not add the HWID instance IDs. * * Since: 2.0.0 */ #define FU_I2C_DEVICE_PRIVATE_FLAG_NO_HWID_GUIDS "no-hwid-guids" gboolean fu_i2c_device_set_address(FuI2cDevice *self, guint8 address, gboolean force, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_i2c_device_read(FuI2cDevice *self, guint8 *buf, gsize bufsz, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_i2c_device_write(FuI2cDevice *self, const guint8 *buf, gsize bufsz, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-ifd-bios.c000066400000000000000000000041551501337203100202220ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuIfdBios" #include "config.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-efi-volume.h" #include "fu-ifd-bios.h" #include "fu-input-stream.h" #include "fu-mem.h" /** * FuIfdBios: * * An Intel BIOS section. * * See also: [class@FuFirmware] */ G_DEFINE_TYPE(FuIfdBios, fu_ifd_bios, FU_TYPE_IFD_IMAGE) #define FU_IFD_BIOS_FIT_SIGNATURE 0x5449465F static gboolean fu_ifd_bios_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { gsize offset = 0; gsize streamsz = 0; guint img_cnt = 0; /* get size */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; /* read each volume in order */ while (offset < streamsz) { g_autoptr(FuFirmware) firmware_tmp = fu_efi_volume_new(); g_autoptr(GError) error_local = NULL; /* FV */ if (!fu_firmware_parse_stream(firmware_tmp, stream, offset, flags, &error_local)) { g_debug("failed to read volume @0x%x of 0x%x: %s", (guint)offset, (guint)streamsz, error_local->message); offset += 0x1000; continue; } fu_firmware_set_offset(firmware_tmp, offset); if (!fu_firmware_add_image_full(firmware, firmware_tmp, error)) return FALSE; /* next! */ offset += fu_firmware_get_size(firmware_tmp); img_cnt++; } /* found nothing */ if (img_cnt == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no EFI firmware volumes"); return FALSE; } /* success */ return TRUE; } static void fu_ifd_bios_init(FuIfdBios *self) { fu_firmware_set_alignment(FU_FIRMWARE(self), FU_FIRMWARE_ALIGNMENT_4K); fu_firmware_set_images_max(FU_FIRMWARE(self), 1024); } static void fu_ifd_bios_class_init(FuIfdBiosClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_ifd_bios_parse; } /** * fu_ifd_bios_new: * * Creates a new #FuFirmware * * Since: 1.6.2 **/ FuFirmware * fu_ifd_bios_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_IFD_BIOS, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-ifd-bios.h000066400000000000000000000005671501337203100202320ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-ifd-image.h" #define FU_TYPE_IFD_BIOS (fu_ifd_bios_get_type()) G_DECLARE_DERIVABLE_TYPE(FuIfdBios, fu_ifd_bios, FU, IFD_BIOS, FuIfdImage) struct _FuIfdBiosClass { FuIfdImageClass parent_class; }; FuFirmware * fu_ifd_bios_new(void); fwupd-2.0.10/libfwupdplugin/fu-ifd-common.c000066400000000000000000000052101501337203100205470ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-ifd-common.h" /** * fu_ifd_region_to_name: * @region: A #FuIfdRegion, e.g. %FU_IFD_REGION_BIOS * * Converts a #FuIfdRegion to a name the user might recognize. * * Returns: identifier string * * Since: 1.6.2 **/ const gchar * fu_ifd_region_to_name(FuIfdRegion region) { if (region == FU_IFD_REGION_DESC) return "IFD descriptor region"; if (region == FU_IFD_REGION_BIOS) return "BIOS"; if (region == FU_IFD_REGION_ME) return "Intel Management Engine"; if (region == FU_IFD_REGION_GBE) return "Gigabit Ethernet"; if (region == FU_IFD_REGION_PLATFORM) return "Platform firmware"; if (region == FU_IFD_REGION_DEVEXP) return "Device Firmware"; if (region == FU_IFD_REGION_BIOS2) return "BIOS Backup"; if (region == FU_IFD_REGION_EC) return "Embedded Controller"; if (region == FU_IFD_REGION_IE) return "Innovation Engine"; if (region == FU_IFD_REGION_10GBE) return "10 Gigabit Ethernet"; return NULL; } /** * fu_ifd_access_to_string: * @access: A #FuIfdAccess, e.g. %FU_IFD_ACCESS_READ * * Converts a #FuIfdAccess to a string. * * Returns: identifier string * * Since: 1.6.2 **/ const gchar * fu_ifd_access_to_string(FuIfdAccess access) { if (access == FU_IFD_ACCESS_NONE) return "--"; if (access == FU_IFD_ACCESS_READ) return "ro"; if (access == FU_IFD_ACCESS_WRITE) return "wr"; if (access == (FU_IFD_ACCESS_READ | FU_IFD_ACCESS_WRITE)) return "rw"; return NULL; } /** * fu_ifd_region_to_access: * @region: A #FuIfdRegion, e.g. %FU_IFD_REGION_BIOS * @flash_master: flash master number * @new_layout: if Skylake or newer * * Converts a #FuIfdRegion to an access level. * * Returns: access * * Since: 1.6.2 **/ FuIfdAccess fu_ifd_region_to_access(FuIfdRegion region, guint32 flash_master, gboolean new_layout) { guint8 bit_r = 0; guint8 bit_w = 0; /* new layout */ if (new_layout) { bit_r = (flash_master >> (region + 8)) & 0b1; bit_w = (flash_master >> (region + 20)) & 0b1; return (bit_r ? FU_IFD_ACCESS_READ : FU_IFD_ACCESS_NONE) | (bit_w ? FU_IFD_ACCESS_WRITE : FU_IFD_ACCESS_NONE); } /* old layout */ if (region == FU_IFD_REGION_DESC) { bit_r = 16; bit_w = 24; } else if (region == FU_IFD_REGION_BIOS) { bit_r = 17; bit_w = 25; } else if (region == FU_IFD_REGION_ME) { bit_r = 18; bit_w = 26; } else if (region == FU_IFD_REGION_GBE) { bit_r = 19; bit_w = 27; } return ((flash_master >> bit_r) & 0b1 ? FU_IFD_ACCESS_READ : FU_IFD_ACCESS_NONE) | ((flash_master >> bit_w) & 0b1 ? FU_IFD_ACCESS_WRITE : FU_IFD_ACCESS_NONE); } fwupd-2.0.10/libfwupdplugin/fu-ifd-common.h000066400000000000000000000012541501337203100205600ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-ifd-struct.h" /** * FuIfdAccess: * @FU_IFD_ACCESS_NONE: None * @FU_IFD_ACCESS_READ: Readable * @FU_IFD_ACCESS_WRITE: Writable * * The flags to use for IFD access permissions. **/ typedef enum { FU_IFD_ACCESS_NONE = 0, FU_IFD_ACCESS_READ = 1 << 0, FU_IFD_ACCESS_WRITE = 1 << 1, } FuIfdAccess; const gchar * fu_ifd_region_to_name(FuIfdRegion region); const gchar * fu_ifd_access_to_string(FuIfdAccess access); FuIfdAccess fu_ifd_region_to_access(FuIfdRegion region, guint32 flash_master, gboolean new_layout); fwupd-2.0.10/libfwupdplugin/fu-ifd-firmware.c000066400000000000000000000372601501337203100211050ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuIfdFirmware" #include "config.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-composite-input-stream.h" #include "fu-efi-volume.h" #include "fu-ifd-bios.h" #include "fu-ifd-common.h" #include "fu-ifd-firmware.h" #include "fu-ifd-image.h" #include "fu-input-stream.h" #include "fu-mem.h" #include "fu-partial-input-stream.h" /** * FuIfdFirmware: * * An Intel Flash Descriptor. * * See also: [class@FuFirmware] */ typedef struct { gboolean new_layout; guint32 descriptor_map0; guint32 descriptor_map1; guint32 descriptor_map2; guint8 num_regions; guint8 num_components; guint32 flash_region_base_addr; guint32 flash_component_base_addr; guint32 flash_master_base_addr; guint32 flash_master[4]; /* indexed from 1, ignore [0] */ guint32 flash_ich_strap_base_addr; guint32 flash_mch_strap_base_addr; guint32 components_rcd; guint32 illegal_jedec; guint32 illegal_jedec1; guint32 *flash_descriptor_regs; } FuIfdFirmwarePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuIfdFirmware, fu_ifd_firmware, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_ifd_firmware_get_instance_private(o)) #define FU_IFD_SIZE 0x1000 #define FU_IFD_FDBAR_FLASH_UPPER_MAP1 0x0EFC #define FU_IFD_FDBAR_OEM_SECTION 0x0F00 #define FU_IFD_FREG_BASE(freg) (((freg) << 12) & 0x07FFF000) #define FU_IFD_FREG_LIMIT(freg) ((((freg) >> 4) & 0x07FFF000) | 0x00000FFF) static void fu_ifd_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuIfdFirmware *self = FU_IFD_FIRMWARE(firmware); FuIfdFirmwarePrivate *priv = GET_PRIVATE(self); fu_xmlb_builder_insert_kx(bn, "descriptor_map0", priv->descriptor_map0); fu_xmlb_builder_insert_kx(bn, "descriptor_map1", priv->descriptor_map1); fu_xmlb_builder_insert_kx(bn, "descriptor_map2", priv->descriptor_map2); fu_xmlb_builder_insert_kx(bn, "num_regions", priv->num_regions); fu_xmlb_builder_insert_kx(bn, "num_components", priv->num_components + 1); fu_xmlb_builder_insert_kx(bn, "flash_region_base_addr", priv->flash_region_base_addr); fu_xmlb_builder_insert_kx(bn, "flash_component_base_addr", priv->flash_component_base_addr); fu_xmlb_builder_insert_kx(bn, "flash_master_base_addr", priv->flash_master_base_addr); fu_xmlb_builder_insert_kx(bn, "flash_ich_strap_base_addr", priv->flash_ich_strap_base_addr); fu_xmlb_builder_insert_kx(bn, "flash_mch_strap_base_addr", priv->flash_mch_strap_base_addr); fu_xmlb_builder_insert_kx(bn, "components_rcd", priv->components_rcd); fu_xmlb_builder_insert_kx(bn, "illegal_jedec", priv->illegal_jedec); fu_xmlb_builder_insert_kx(bn, "illegal_jedec1", priv->illegal_jedec1); if (flags & FU_FIRMWARE_EXPORT_FLAG_INCLUDE_DEBUG) { for (guint i = 1; i < 3; i++) { g_autofree gchar *title = g_strdup_printf("flash_master%x", i + 1); fu_xmlb_builder_insert_kx(bn, title, priv->flash_master[i]); } if (priv->flash_descriptor_regs != NULL) { for (guint i = 0; i < priv->num_regions; i++) { g_autofree gchar *title = g_strdup_printf("flash_descriptor_reg%x", i); fu_xmlb_builder_insert_kx(bn, title, priv->flash_descriptor_regs[i]); } } } } static gboolean fu_ifd_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_ifd_fdbar_validate_stream(stream, offset, error); } static GInputStream * fu_ifd_firmware_fixup_stream(GInputStream *stream, GError **error) { const guint8 buf[] = {0xFF}; gsize streamsz = 0; g_autoptr(GBytes) blob = g_bytes_new(buf, sizeof(buf)); g_autoptr(GInputStream) stream2 = fu_composite_input_stream_new(); /* already aligned */ if (!fu_input_stream_size(stream, &streamsz, error)) return NULL; if (((streamsz >> 1) << 1) == streamsz) return g_object_ref(stream); /* pad with one trailing byte */ if (!fu_composite_input_stream_add_stream(FU_COMPOSITE_INPUT_STREAM(stream2), stream, error)) return NULL; fu_composite_input_stream_add_bytes(FU_COMPOSITE_INPUT_STREAM(stream2), blob); return g_steal_pointer(&stream2); } static gboolean fu_ifd_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuIfdFirmware *self = FU_IFD_FIRMWARE(firmware); FuIfdFirmwarePrivate *priv = GET_PRIVATE(self); gsize streamsz = 0; g_autoptr(GByteArray) st_fcba = NULL; g_autoptr(GByteArray) st_fdbar = NULL; g_autoptr(GInputStream) stream2 = NULL; /* check size */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz < FU_IFD_SIZE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "file is too small, expected streamsz >= 0x%x", (guint)FU_IFD_SIZE); return FALSE; } /* some test IFD images were captured missing the final byte -- so align up */ stream2 = fu_ifd_firmware_fixup_stream(stream, error); if (stream2 == NULL) return FALSE; /* descriptor registers */ st_fdbar = fu_struct_ifd_fdbar_parse_stream(stream, 0x0, error); if (st_fdbar == NULL) return FALSE; priv->descriptor_map0 = fu_struct_ifd_fdbar_get_descriptor_map0(st_fdbar); priv->num_regions = (priv->descriptor_map0 >> 24) & 0b111; if (priv->num_regions == 0) priv->num_regions = 10; priv->num_components = (priv->descriptor_map0 >> 8) & 0b11; priv->flash_component_base_addr = (priv->descriptor_map0 << 4) & 0x00000FF0; priv->flash_region_base_addr = (priv->descriptor_map0 >> 12) & 0x00000FF0; priv->descriptor_map1 = fu_struct_ifd_fdbar_get_descriptor_map1(st_fdbar); priv->flash_master_base_addr = (priv->descriptor_map1 << 4) & 0x00000FF0; priv->flash_ich_strap_base_addr = (priv->descriptor_map1 >> 12) & 0x00000FF0; priv->descriptor_map2 = fu_struct_ifd_fdbar_get_descriptor_map2(st_fdbar); priv->flash_mch_strap_base_addr = (priv->descriptor_map2 << 4) & 0x00000FF0; /* FCBA */ st_fcba = fu_struct_ifd_fcba_parse_stream(stream, priv->flash_component_base_addr, error); if (st_fcba == NULL) return FALSE; priv->components_rcd = fu_struct_ifd_fcba_get_flcomp(st_fcba); priv->illegal_jedec = fu_struct_ifd_fcba_get_flill(st_fcba); priv->illegal_jedec1 = fu_struct_ifd_fcba_get_flill1(st_fcba); /* FMBA */ if (!fu_input_stream_read_u32(stream, priv->flash_master_base_addr + 0x0, &priv->flash_master[1], G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_input_stream_read_u32(stream, priv->flash_master_base_addr + 0x4, &priv->flash_master[2], G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_input_stream_read_u32(stream, priv->flash_master_base_addr + 0x8, &priv->flash_master[3], G_LITTLE_ENDIAN, error)) return FALSE; /* FRBA */ priv->flash_descriptor_regs = g_new0(guint32, priv->num_regions); for (guint i = 0; i < priv->num_regions; i++) { if (!fu_input_stream_read_u32(stream, priv->flash_region_base_addr + (i * sizeof(guint32)), &priv->flash_descriptor_regs[i], G_LITTLE_ENDIAN, error)) return FALSE; } for (guint i = 0; i < priv->num_regions; i++) { const gchar *freg_str = fu_ifd_region_to_string(i); guint32 freg_base = FU_IFD_FREG_BASE(priv->flash_descriptor_regs[i]); guint32 freg_limt = FU_IFD_FREG_LIMIT(priv->flash_descriptor_regs[i]); guint32 freg_size = (freg_limt - freg_base) + 1; g_autoptr(FuFirmware) img = NULL; g_autoptr(GInputStream) partial_stream = NULL; /* invalid */ if (freg_base > freg_limt) continue; /* create image */ g_debug("freg %s 0x%04x -> 0x%04x", freg_str, freg_base, freg_limt); partial_stream = fu_partial_input_stream_new(stream2, freg_base, freg_size, error); if (partial_stream == NULL) { g_prefix_error(error, "failed to cut IFD image: "); return FALSE; } if (i == FU_IFD_REGION_BIOS) { img = fu_ifd_bios_new(); } else { img = fu_ifd_image_new(); } if (!fu_firmware_parse_stream(img, partial_stream, 0x0, flags | FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error)) return FALSE; fu_firmware_set_addr(img, freg_base); fu_firmware_set_idx(img, i); if (freg_str != NULL) fu_firmware_set_id(img, freg_str); if (!fu_firmware_add_image_full(firmware, img, error)) return FALSE; /* is writable by anything other than the region itself */ for (FuIfdRegion r = 1; r <= 3; r++) { FuIfdAccess acc; acc = fu_ifd_region_to_access(i, priv->flash_master[r], priv->new_layout); fu_ifd_image_set_access(FU_IFD_IMAGE(img), r, acc); } } /* success */ return TRUE; } /** * fu_ifd_firmware_check_jedec_cmd: * @self: a #FuIfdFirmware * @cmd: a JEDEC command, e.g. 0x42 for "whole chip erase" * * Checks a JEDEC command to see if it has been put on the "illegal_jedec" list. * * Returns: %TRUE if the command is allowed * * Since: 1.6.2 **/ gboolean fu_ifd_firmware_check_jedec_cmd(FuIfdFirmware *self, guint8 cmd) { FuIfdFirmwarePrivate *priv = GET_PRIVATE(self); for (guint j = 0; j < 32; j += 8) { if (((priv->illegal_jedec >> j) & 0xff) == cmd) return FALSE; if (((priv->illegal_jedec1 >> j) & 0xff) == cmd) return FALSE; } return TRUE; } static GByteArray * fu_ifd_firmware_write(FuFirmware *firmware, GError **error) { FuIfdFirmware *self = FU_IFD_FIRMWARE(firmware); FuIfdFirmwarePrivate *priv = GET_PRIVATE(self); gsize bufsz_max = 0x0; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GByteArray) st_fcba = fu_struct_ifd_fcba_new(); g_autoptr(GByteArray) st_fdbar = fu_struct_ifd_fdbar_new(); g_autoptr(GHashTable) blobs = NULL; g_autoptr(FuFirmware) img_desc = NULL; /* if the descriptor does not exist, then add something plausible */ img_desc = fu_firmware_get_image_by_idx(firmware, FU_IFD_REGION_DESC, NULL); if (img_desc == NULL) { g_autoptr(GByteArray) buf_desc = g_byte_array_new(); g_autoptr(GBytes) blob_desc = NULL; fu_byte_array_set_size(buf_desc, FU_IFD_SIZE, 0x00); /* success */ blob_desc = g_bytes_new(buf_desc->data, buf_desc->len); img_desc = fu_firmware_new_from_bytes(blob_desc); fu_firmware_set_addr(img_desc, 0x0); fu_firmware_set_idx(img_desc, FU_IFD_REGION_DESC); fu_firmware_set_id(img_desc, "desc"); fu_firmware_add_image(firmware, img_desc); } /* generate ahead of time */ blobs = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)g_bytes_unref); for (guint i = 0; i < priv->num_regions; i++) { g_autoptr(FuFirmware) img = fu_firmware_get_image_by_idx(firmware, i, NULL); g_autoptr(GBytes) blob = NULL; if (img == NULL) continue; blob = fu_firmware_write(img, error); if (blob == NULL) { g_prefix_error(error, "failed to write %s: ", fu_firmware_get_id(img)); return NULL; } if (g_bytes_get_data(blob, NULL) == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to write %s", fu_firmware_get_id(img)); return NULL; } g_hash_table_insert(blobs, GUINT_TO_POINTER(i), g_bytes_ref(blob)); /* check total size */ bufsz_max = MAX(fu_firmware_get_addr(img) + g_bytes_get_size(blob), bufsz_max); } fu_byte_array_set_size(buf, bufsz_max, 0x00); /* descriptor map */ fu_struct_ifd_fdbar_set_descriptor_map0(st_fdbar, priv->descriptor_map0); fu_struct_ifd_fdbar_set_descriptor_map1(st_fdbar, priv->descriptor_map1); fu_struct_ifd_fdbar_set_descriptor_map2(st_fdbar, priv->descriptor_map2); if (!fu_memcpy_safe(buf->data, buf->len, 0x0, st_fdbar->data, st_fdbar->len, 0x0, st_fdbar->len, error)) return NULL; /* FCBA */ fu_struct_ifd_fcba_set_flcomp(st_fcba, priv->components_rcd); fu_struct_ifd_fcba_set_flill(st_fcba, priv->illegal_jedec); fu_struct_ifd_fcba_set_flill1(st_fcba, priv->illegal_jedec1); if (!fu_memcpy_safe(buf->data, buf->len, priv->flash_component_base_addr, st_fcba->data, st_fcba->len, 0x0, st_fcba->len, error)) return NULL; /* FRBA */ for (guint i = 0; i < priv->num_regions; i++) { guint32 freg_base = 0x7FFF000; guint32 freg_limt = 0x0; guint32 flreg; g_autoptr(FuFirmware) img = fu_firmware_get_image_by_idx(firmware, i, NULL); if (img != NULL) { GBytes *blob = g_hash_table_lookup(blobs, GUINT_TO_POINTER(fu_firmware_get_idx(img))); freg_base = fu_firmware_get_addr(img); freg_limt = (freg_base + g_bytes_get_size(blob)) - 1; } flreg = ((freg_limt << 4) & 0xFFFF0000) | (freg_base >> 12); g_debug("freg 0x%04x -> 0x%04x = 0x%08x", freg_base, freg_limt, flreg); if (!fu_memwrite_uint32_safe(buf->data, buf->len, priv->flash_region_base_addr + (i * sizeof(guint32)), flreg, G_LITTLE_ENDIAN, error)) return NULL; } /* write images at correct offsets */ for (guint i = 1; i < priv->num_regions; i++) { GBytes *blob; g_autoptr(FuFirmware) img = fu_firmware_get_image_by_idx(firmware, i, NULL); if (img == NULL) continue; blob = g_hash_table_lookup(blobs, GUINT_TO_POINTER(fu_firmware_get_idx(img))); if (!fu_memcpy_safe(buf->data, buf->len, fu_firmware_get_addr(img), g_bytes_get_data(blob, NULL), g_bytes_get_size(blob), 0x0, g_bytes_get_size(blob), error)) return NULL; } /* success */ return g_steal_pointer(&buf); } static gboolean fu_ifd_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { FuIfdFirmware *self = FU_IFD_FIRMWARE(firmware); FuIfdFirmwarePrivate *priv = GET_PRIVATE(self); guint64 tmp; /* optional properties */ tmp = xb_node_query_text_as_uint(n, "descriptor_map0", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT32) priv->descriptor_map0 = tmp; tmp = xb_node_query_text_as_uint(n, "descriptor_map1", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT32) priv->descriptor_map1 = tmp; tmp = xb_node_query_text_as_uint(n, "descriptor_map2", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT32) priv->descriptor_map2 = tmp; tmp = xb_node_query_text_as_uint(n, "components_rcd", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT32) priv->components_rcd = tmp; tmp = xb_node_query_text_as_uint(n, "illegal_jedec", NULL); if (tmp != G_MAXUINT64) { priv->illegal_jedec = tmp & 0xFFFFFFFF; priv->illegal_jedec1 = tmp >> 32; } /* success */ return TRUE; } static void fu_ifd_firmware_init(FuIfdFirmware *self) { FuIfdFirmwarePrivate *priv = GET_PRIVATE(self); /* some good defaults */ priv->new_layout = TRUE; priv->num_regions = 10; priv->flash_region_base_addr = 0x40; priv->flash_component_base_addr = 0x30; priv->flash_master_base_addr = 0x80; priv->flash_master[1] = 0x00A00F00; priv->flash_master[2] = 0x00400D00; priv->flash_master[3] = 0x00800900; priv->flash_ich_strap_base_addr = 0x100; priv->flash_mch_strap_base_addr = 0x300; g_type_ensure(FU_TYPE_IFD_BIOS); g_type_ensure(FU_TYPE_IFD_IMAGE); g_type_ensure(FU_TYPE_EFI_VOLUME); } static void fu_ifd_firmware_finalize(GObject *object) { FuIfdFirmware *self = FU_IFD_FIRMWARE(object); FuIfdFirmwarePrivate *priv = GET_PRIVATE(self); g_free(priv->flash_descriptor_regs); G_OBJECT_CLASS(fu_ifd_firmware_parent_class)->finalize(object); } static void fu_ifd_firmware_class_init(FuIfdFirmwareClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_ifd_firmware_finalize; firmware_class->validate = fu_ifd_firmware_validate; firmware_class->export = fu_ifd_firmware_export; firmware_class->parse = fu_ifd_firmware_parse; firmware_class->write = fu_ifd_firmware_write; firmware_class->build = fu_ifd_firmware_build; } /** * fu_ifd_firmware_new: * * Creates a new #FuFirmware of sub type Ifd * * Since: 1.6.2 **/ FuFirmware * fu_ifd_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_IFD_FIRMWARE, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-ifd-firmware.h000066400000000000000000000007601501337203100211050ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_IFD_FIRMWARE (fu_ifd_firmware_get_type()) G_DECLARE_DERIVABLE_TYPE(FuIfdFirmware, fu_ifd_firmware, FU, IFD_FIRMWARE, FuFirmware) struct _FuIfdFirmwareClass { FuFirmwareClass parent_class; }; FuFirmware * fu_ifd_firmware_new(void); gboolean fu_ifd_firmware_check_jedec_cmd(FuIfdFirmware *self, guint8 cmd) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-ifd-image.c000066400000000000000000000071661501337203100203550ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-byte-array.h" #include "fu-common.h" #include "fu-ifd-image.h" /** * FuIfdImage: * * An Intel Flash Descriptor image, e.g. BIOS. * * See also: [class@FuFirmware] */ typedef struct { FuIfdAccess access[FU_IFD_REGION_MAX]; } FuIfdImagePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuIfdImage, fu_ifd_image, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_ifd_image_get_instance_private(o)) static void fu_ifd_image_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuIfdImage *self = FU_IFD_IMAGE(firmware); FuIfdImagePrivate *priv = GET_PRIVATE(self); for (guint i = 0; i < FU_IFD_REGION_MAX; i++) { if (priv->access[i] == FU_IFD_ACCESS_NONE) continue; xb_builder_node_insert_text(bn, "access", fu_ifd_access_to_string(priv->access[i]), "region", fu_ifd_region_to_string(i), NULL); } } /** * fu_ifd_image_set_access: * @self: a #FuIfdImage * @region: a #FuIfdRegion, e.g. %FU_IFD_REGION_BIOS * @access: a #FuIfdAccess, e.g. %FU_IFD_ACCESS_NONE * * Sets the access control for a specific reason. * * Since: 1.6.2 **/ void fu_ifd_image_set_access(FuIfdImage *self, FuIfdRegion region, FuIfdAccess access) { FuIfdImagePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_IFD_IMAGE(self)); g_return_if_fail(region < FU_IFD_REGION_MAX); priv->access[region] = access; } /** * fu_ifd_image_get_access: * @self: a #FuIfdImage * @region: a #FuIfdRegion, e.g. %FU_IFD_REGION_BIOS * * Gets the access control for a specific reason. * * Returns: a #FuIfdAccess, e.g. %FU_IFD_ACCESS_NONE * * Since: 1.6.2 **/ FuIfdAccess fu_ifd_image_get_access(FuIfdImage *self, FuIfdRegion region) { FuIfdImagePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_IFD_IMAGE(self), FU_IFD_ACCESS_NONE); g_return_val_if_fail(region < FU_IFD_REGION_MAX, FU_IFD_ACCESS_NONE); return priv->access[region]; } static GByteArray * fu_ifd_image_write(FuFirmware *firmware, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GPtrArray) images = fu_firmware_get_images(firmware); /* sanity check */ if (fu_firmware_get_alignment(firmware) > FU_FIRMWARE_ALIGNMENT_1M) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "alignment invalid, got 0x%02x", fu_firmware_get_alignment(firmware)); return NULL; } /* add each volume */ if (images->len > 0) { for (guint i = 0; i < images->len; i++) { FuFirmware *img = g_ptr_array_index(images, i); g_autoptr(GBytes) bytes = fu_firmware_write(img, error); if (bytes == NULL) return NULL; fu_byte_array_append_bytes(buf, bytes); } } else { g_autoptr(GBytes) bytes = NULL; bytes = fu_firmware_get_bytes_with_patches(firmware, error); if (bytes == NULL) return NULL; fu_byte_array_append_bytes(buf, bytes); } /* align up */ fu_byte_array_set_size(buf, fu_common_align_up(buf->len, fu_firmware_get_alignment(firmware)), 0x00); /* success */ return g_steal_pointer(&buf); } static void fu_ifd_image_init(FuIfdImage *self) { fu_firmware_set_alignment(FU_FIRMWARE(self), FU_FIRMWARE_ALIGNMENT_4K); } static void fu_ifd_image_class_init(FuIfdImageClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->export = fu_ifd_image_export; firmware_class->write = fu_ifd_image_write; } /** * fu_ifd_image_new: * * Creates a new #FuFirmware * * Since: 1.6.2 **/ FuFirmware * fu_ifd_image_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_IFD_IMAGE, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-ifd-image.h000066400000000000000000000011451501337203100203510ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #include "fu-ifd-common.h" #define FU_TYPE_IFD_IMAGE (fu_ifd_image_get_type()) G_DECLARE_DERIVABLE_TYPE(FuIfdImage, fu_ifd_image, FU, IFD_IMAGE, FuFirmware) struct _FuIfdImageClass { FuFirmwareClass parent_class; }; FuFirmware * fu_ifd_image_new(void); void fu_ifd_image_set_access(FuIfdImage *self, FuIfdRegion region, FuIfdAccess access) G_GNUC_NON_NULL(1); FuIfdAccess fu_ifd_image_get_access(FuIfdImage *self, FuIfdRegion region) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-ifd.rs000066400000000000000000000013471501337203100174720ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] enum FuIfdRegion { Desc = 0x00, Bios = 0x01, Me = 0x02, Gbe = 0x03, Platform = 0x04, Devexp = 0x05, Bios2 = 0x06, Ec = 0x08, Ie = 0x0A, 10gbe = 0x0B, Max = 0x0F, } #[derive(ParseStream, New, ValidateStream, Default)] #[repr(C, packed)] struct FuStructIfdFdbar { reserved: [u8; 16] = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, signature: u32le == 0x0FF0A55A, descriptor_map0: u32le, descriptor_map1: u32le, descriptor_map2: u32le, } #[derive(ParseStream, New)] #[repr(C, packed)] struct FuStructIfdFcba { flcomp: u32le, flill: u32le, flill1: u32le, } fwupd-2.0.10/libfwupdplugin/fu-ifwi-cpd-firmware.c000066400000000000000000000243361501337203100220450ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * Copyright 2022 Intel * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-ifwi-cpd-firmware.h" #include "fu-ifwi-struct.h" #include "fu-input-stream.h" #include "fu-partial-input-stream.h" #include "fu-string.h" /** * FuIfwiCpdFirmware: * * An Intel Code Partition Directory (aka CPD) can be found in IFWI (Integrated Firmware Image) * firmware blobs which are used in various Intel products using an IPU (Infrastructure Processing * Unit). * * This could include hardware like SmartNICs, GPUs, camera and audio devices. * * See also: [class@FuFirmware] */ typedef struct { guint8 header_version; guint8 entry_version; } FuIfwiCpdFirmwarePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuIfwiCpdFirmware, fu_ifwi_cpd_firmware, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_ifwi_cpd_firmware_get_instance_private(o)) #define FU_IFWI_CPD_FIRMWARE_ENTRIES_MAX 1024 static void fu_ifwi_cpd_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuIfwiCpdFirmware *self = FU_IFWI_CPD_FIRMWARE(firmware); FuIfwiCpdFirmwarePrivate *priv = GET_PRIVATE(self); fu_xmlb_builder_insert_kx(bn, "header_version", priv->header_version); fu_xmlb_builder_insert_kx(bn, "entry_version", priv->entry_version); } static gboolean fu_ifwi_cpd_firmware_parse_manifest(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { gsize streamsz = 0; guint32 size; gsize offset = 0; g_autoptr(GByteArray) st_mhd = NULL; /* raw version */ st_mhd = fu_struct_ifwi_cpd_manifest_parse_stream(stream, offset, error); if (st_mhd == NULL) return FALSE; fu_firmware_set_version_raw(firmware, fu_struct_ifwi_cpd_manifest_get_version(st_mhd)); /* verify the size */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; size = fu_struct_ifwi_cpd_manifest_get_size(st_mhd); if (size * 4 != streamsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid manifest invalid length, got 0x%x, expected 0x%x", size * 4, (guint)streamsz); return FALSE; } /* parse extensions */ offset += fu_struct_ifwi_cpd_manifest_get_header_length(st_mhd) * 4; while (offset < streamsz) { guint32 extension_type = 0; guint32 extension_length = 0; g_autoptr(FuFirmware) img = fu_firmware_new(); g_autoptr(GByteArray) st_mex = NULL; g_autoptr(GInputStream) partial_stream = NULL; /* set the extension type as the index */ st_mex = fu_struct_ifwi_cpd_manifest_ext_parse_stream(stream, offset, error); if (st_mex == NULL) return FALSE; extension_type = fu_struct_ifwi_cpd_manifest_ext_get_extension_type(st_mex); if (extension_type == 0x0) break; fu_firmware_set_idx(img, extension_type); /* add data section */ extension_length = fu_struct_ifwi_cpd_manifest_ext_get_extension_length(st_mex); if (extension_length == 0x0) break; if (extension_length < st_mex->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid manifest extension header length 0x%x", (guint)extension_length); return FALSE; } partial_stream = fu_partial_input_stream_new(stream, offset + st_mex->len, extension_length - st_mex->len, error); if (partial_stream == NULL) { g_prefix_error(error, "failed to cut CPD extension: "); return FALSE; } if (!fu_firmware_parse_stream(img, partial_stream, 0x0, flags, error)) return FALSE; /* success */ fu_firmware_set_offset(img, offset); if (!fu_firmware_add_image_full(firmware, img, error)) return FALSE; offset += extension_length; } /* success */ return TRUE; } static gboolean fu_ifwi_cpd_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_ifwi_cpd_validate_stream(stream, offset, error); } static gboolean fu_ifwi_cpd_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuIfwiCpdFirmware *self = FU_IFWI_CPD_FIRMWARE(firmware); FuIfwiCpdFirmwarePrivate *priv = GET_PRIVATE(self); g_autoptr(GByteArray) st_hdr = NULL; gsize offset = 0; guint32 num_of_entries; /* other header fields */ st_hdr = fu_struct_ifwi_cpd_parse_stream(stream, offset, error); if (st_hdr == NULL) return FALSE; priv->header_version = fu_struct_ifwi_cpd_get_header_version(st_hdr); priv->entry_version = fu_struct_ifwi_cpd_get_entry_version(st_hdr); fu_firmware_set_idx(firmware, fu_struct_ifwi_cpd_get_partition_name(st_hdr)); /* read out entries */ num_of_entries = fu_struct_ifwi_cpd_get_num_of_entries(st_hdr); if (num_of_entries > FU_IFWI_CPD_FIRMWARE_ENTRIES_MAX) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "too many entries 0x%x, expected <= 0x%x", num_of_entries, (guint)FU_IFWI_CPD_FIRMWARE_ENTRIES_MAX); return FALSE; } offset += fu_struct_ifwi_cpd_get_header_length(st_hdr); for (guint32 i = 0; i < num_of_entries; i++) { guint32 img_offset = 0; g_autofree gchar *id = NULL; g_autoptr(FuFirmware) img = fu_firmware_new(); g_autoptr(GByteArray) st_ent = NULL; g_autoptr(GInputStream) partial_stream = NULL; /* the IDX is the position in the file */ fu_firmware_set_idx(img, i); st_ent = fu_struct_ifwi_cpd_entry_parse_stream(stream, offset, error); if (st_ent == NULL) return FALSE; /* copy name as id */ id = fu_struct_ifwi_cpd_entry_get_name(st_ent); fu_firmware_set_id(img, id); /* copy offset, ignoring huffman and reserved bits */ img_offset = fu_struct_ifwi_cpd_entry_get_offset(st_ent); img_offset &= 0x1FFFFFF; fu_firmware_set_offset(img, img_offset); /* copy data */ partial_stream = fu_partial_input_stream_new(stream, img_offset, fu_struct_ifwi_cpd_entry_get_length(st_ent), error); if (partial_stream == NULL) { g_prefix_error(error, "failed to cut IFD image: "); return FALSE; } if (!fu_firmware_parse_stream(img, partial_stream, 0x0, flags, error)) return FALSE; /* read the manifest */ if (i == FU_IFWI_CPD_FIRMWARE_IDX_MANIFEST && fu_struct_ifwi_cpd_entry_get_length(st_ent) > FU_STRUCT_IFWI_CPD_MANIFEST_SIZE) { if (!fu_ifwi_cpd_firmware_parse_manifest(img, partial_stream, flags, error)) return FALSE; } /* success */ if (!fu_firmware_add_image_full(firmware, img, error)) return FALSE; offset += st_ent->len; } /* success */ return TRUE; } static GByteArray * fu_ifwi_cpd_firmware_write(FuFirmware *firmware, GError **error) { FuIfwiCpdFirmware *self = FU_IFWI_CPD_FIRMWARE(firmware); FuIfwiCpdFirmwarePrivate *priv = GET_PRIVATE(self); gsize offset = 0; g_autoptr(GByteArray) buf = fu_struct_ifwi_cpd_new(); g_autoptr(GPtrArray) imgs = fu_firmware_get_images(firmware); /* write the header */ fu_struct_ifwi_cpd_set_num_of_entries(buf, imgs->len); fu_struct_ifwi_cpd_set_header_version(buf, priv->header_version); fu_struct_ifwi_cpd_set_entry_version(buf, priv->entry_version); fu_struct_ifwi_cpd_set_checksum(buf, 0x0); fu_struct_ifwi_cpd_set_partition_name(buf, fu_firmware_get_idx(firmware)); fu_struct_ifwi_cpd_set_crc32(buf, 0x0); /* fixup the image offsets */ offset += buf->len; offset += FU_STRUCT_IFWI_CPD_ENTRY_SIZE * imgs->len; for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); g_autoptr(GBytes) blob = NULL; blob = fu_firmware_get_bytes(img, error); if (blob == NULL) { g_prefix_error(error, "image 0x%x: ", i); return NULL; } fu_firmware_set_offset(img, offset); offset += g_bytes_get_size(blob); } /* add entry headers */ for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); g_autoptr(GByteArray) st_ent = fu_struct_ifwi_cpd_entry_new(); /* sanity check */ if (fu_firmware_get_id(img) == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "image 0x%x must have an ID", (guint)fu_firmware_get_idx(img)); return NULL; } if (!fu_struct_ifwi_cpd_entry_set_name(st_ent, fu_firmware_get_id(img), error)) return NULL; fu_struct_ifwi_cpd_entry_set_offset(st_ent, fu_firmware_get_offset(img)); fu_struct_ifwi_cpd_entry_set_length(st_ent, fu_firmware_get_size(img)); g_byte_array_append(buf, st_ent->data, st_ent->len); } /* add entry data */ for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); g_autoptr(GBytes) blob = NULL; blob = fu_firmware_get_bytes(img, error); if (blob == NULL) return NULL; fu_byte_array_append_bytes(buf, blob); } /* success */ return g_steal_pointer(&buf); } static gboolean fu_ifwi_cpd_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { FuIfwiCpdFirmware *self = FU_IFWI_CPD_FIRMWARE(firmware); FuIfwiCpdFirmwarePrivate *priv = GET_PRIVATE(self); const gchar *tmp; /* simple properties */ tmp = xb_node_query_text(n, "header_version", NULL); if (tmp != NULL) { guint64 val = 0; if (!fu_strtoull(tmp, &val, 0x0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->header_version = val; } tmp = xb_node_query_text(n, "entry_version", NULL); if (tmp != NULL) { guint64 val = 0; if (!fu_strtoull(tmp, &val, 0x0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->entry_version = val; } /* success */ return TRUE; } static void fu_ifwi_cpd_firmware_init(FuIfwiCpdFirmware *self) { fu_firmware_set_images_max(FU_FIRMWARE(self), FU_IFWI_CPD_FIRMWARE_ENTRIES_MAX); } static void fu_ifwi_cpd_firmware_class_init(FuIfwiCpdFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_ifwi_cpd_firmware_validate; firmware_class->export = fu_ifwi_cpd_firmware_export; firmware_class->parse = fu_ifwi_cpd_firmware_parse; firmware_class->write = fu_ifwi_cpd_firmware_write; firmware_class->build = fu_ifwi_cpd_firmware_build; } /** * fu_ifwi_cpd_firmware_new: * * Creates a new #FuFirmware of Intel Code Partition Directory format * * Since: 1.8.2 **/ FuFirmware * fu_ifwi_cpd_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_IFWI_CPD_FIRMWARE, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-ifwi-cpd-firmware.h000066400000000000000000000016641501337203100220510ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * Copyright 2022 Intel * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_IFWI_CPD_FIRMWARE (fu_ifwi_cpd_firmware_get_type()) G_DECLARE_DERIVABLE_TYPE(FuIfwiCpdFirmware, fu_ifwi_cpd_firmware, FU, IFWI_CPD_FIRMWARE, FuFirmware) struct _FuIfwiCpdFirmwareClass { FuFirmwareClass parent_class; }; /** * FU_IFWI_CPD_FIRMWARE_IDX_MANIFEST: * * The index for the IFWI manifest image. * * Since: 1.8.2 **/ #define FU_IFWI_CPD_FIRMWARE_IDX_MANIFEST 0x0 /** * FU_IFWI_CPD_FIRMWARE_IDX_METADATA: * * The index for the IFWI metadata image. * * Since: 1.8.2 **/ #define FU_IFWI_CPD_FIRMWARE_IDX_METADATA 0x1 /** * FU_IFWI_CPD_FIRMWARE_IDX_MODULEDATA_IDX: * * The index for the IFWI module data image. * * Since: 1.8.2 **/ #define FU_IFWI_CPD_FIRMWARE_IDX_MODULEDATA_IDX 0x2 FuFirmware * fu_ifwi_cpd_firmware_new(void); fwupd-2.0.10/libfwupdplugin/fu-ifwi-fpt-firmware.c000066400000000000000000000131111501337203100220550ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * Copyright 2022 Intel * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-ifwi-fpt-firmware.h" #include "fu-ifwi-struct.h" #include "fu-partial-input-stream.h" #include "fu-string.h" /** * FuIfwiFptFirmware: * * An Intel Flash Program Tool (aka FPT) header can be found in IFWI (Integrated Firmware Image) * firmware blobs which are used in various Intel products using an IPU (Infrastructure Processing * Unit). * * This could include hardware like SmartNICs, GPUs, camera and audio devices. * * See also: [class@FuFirmware] */ G_DEFINE_TYPE(FuIfwiFptFirmware, fu_ifwi_fpt_firmware, FU_TYPE_FIRMWARE) #define FU_IFWI_FPT_MAX_ENTRIES 56 static gboolean fu_ifwi_fpt_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_ifwi_fpt_validate_stream(stream, offset, error); } static gboolean fu_ifwi_fpt_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { guint32 num_of_entries; gsize offset = 0; g_autoptr(GByteArray) st_hdr = NULL; /* sanity check */ st_hdr = fu_struct_ifwi_fpt_parse_stream(stream, offset, error); if (st_hdr == NULL) return FALSE; num_of_entries = fu_struct_ifwi_fpt_get_num_of_entries(st_hdr); if (num_of_entries > FU_IFWI_FPT_MAX_ENTRIES) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid FPT number of entries %u", num_of_entries); return FALSE; } if (fu_struct_ifwi_fpt_get_header_version(st_hdr) < FU_STRUCT_IFWI_FPT_DEFAULT_HEADER_VERSION) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid FPT header version: 0x%x", fu_struct_ifwi_fpt_get_header_version(st_hdr)); return FALSE; } /* offset by header length */ offset += fu_struct_ifwi_fpt_get_header_length(st_hdr); /* read out entries */ for (guint i = 0; i < num_of_entries; i++) { guint32 data_length; guint32 partition_name; g_autofree gchar *id = NULL; g_autoptr(FuFirmware) img = fu_firmware_new(); g_autoptr(GByteArray) st_ent = NULL; /* read IDX */ st_ent = fu_struct_ifwi_fpt_entry_parse_stream(stream, offset, error); if (st_ent == NULL) return FALSE; partition_name = fu_struct_ifwi_fpt_entry_get_partition_name(st_ent); fu_firmware_set_idx(img, partition_name); /* convert to text form for convenience */ id = fu_strsafe((const gchar *)&partition_name, sizeof(partition_name)); if (id != NULL) fu_firmware_set_id(img, id); /* get data at offset using zero-copy */ data_length = fu_struct_ifwi_fpt_entry_get_length(st_ent); if (data_length != 0x0) { guint32 data_offset = fu_struct_ifwi_fpt_entry_get_offset(st_ent); g_autoptr(GInputStream) partial_stream = NULL; partial_stream = fu_partial_input_stream_new(stream, data_offset, data_length, error); if (partial_stream == NULL) { g_prefix_error(error, "failed to cut FPT image: "); return FALSE; } if (!fu_firmware_parse_stream(img, partial_stream, 0x0, flags, error)) return FALSE; fu_firmware_set_offset(img, data_offset); } if (!fu_firmware_add_image_full(firmware, img, error)) return FALSE; /* next */ offset += st_ent->len; } /* success */ return TRUE; } static GByteArray * fu_ifwi_fpt_firmware_write(FuFirmware *firmware, GError **error) { gsize offset = 0; g_autoptr(GByteArray) buf = fu_struct_ifwi_fpt_new(); g_autoptr(GPtrArray) imgs = fu_firmware_get_images(firmware); /* fixup the image offsets */ offset += buf->len; offset += FU_STRUCT_IFWI_FPT_ENTRY_SIZE * imgs->len; for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); g_autoptr(GBytes) blob = NULL; blob = fu_firmware_get_bytes(img, error); if (blob == NULL) { g_prefix_error(error, "image 0x%x: ", i); return NULL; } fu_firmware_set_offset(img, offset); offset += g_bytes_get_size(blob); } /* write the header */ fu_struct_ifwi_fpt_set_num_of_entries(buf, imgs->len); /* add entries */ for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); g_autoptr(GByteArray) st_ent = fu_struct_ifwi_fpt_entry_new(); fu_struct_ifwi_fpt_entry_set_partition_name(st_ent, fu_firmware_get_idx(img)); fu_struct_ifwi_fpt_entry_set_offset(st_ent, fu_firmware_get_offset(img)); fu_struct_ifwi_fpt_entry_set_length(st_ent, fu_firmware_get_size(img)); g_byte_array_append(buf, st_ent->data, st_ent->len); } /* add entry data */ for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); g_autoptr(GBytes) blob = NULL; blob = fu_firmware_get_bytes(img, error); if (blob == NULL) return NULL; fu_byte_array_append_bytes(buf, blob); } /* success */ return g_steal_pointer(&buf); } static void fu_ifwi_fpt_firmware_init(FuIfwiFptFirmware *self) { fu_firmware_set_images_max(FU_FIRMWARE(self), FU_IFWI_FPT_MAX_ENTRIES); } static void fu_ifwi_fpt_firmware_class_init(FuIfwiFptFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_ifwi_fpt_firmware_validate; firmware_class->parse = fu_ifwi_fpt_firmware_parse; firmware_class->write = fu_ifwi_fpt_firmware_write; } /** * fu_ifwi_fpt_firmware_new: * * Creates a new #FuFirmware of Intel Flash Program Tool format * * Since: 1.8.2 **/ FuFirmware * fu_ifwi_fpt_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_IFWI_FPT_FIRMWARE, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-ifwi-fpt-firmware.h000066400000000000000000000023351501337203100220700ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * Copyright 2022 Intel * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_IFWI_FPT_FIRMWARE (fu_ifwi_fpt_firmware_get_type()) G_DECLARE_DERIVABLE_TYPE(FuIfwiFptFirmware, fu_ifwi_fpt_firmware, FU, IFWI_FPT_FIRMWARE, FuFirmware) struct _FuIfwiFptFirmwareClass { FuFirmwareClass parent_class; }; /** * FU_IFWI_FPT_FIRMWARE_IDX_INFO: * * The index for the IFWI info image. * * Since: 1.8.2 **/ #define FU_IFWI_FPT_FIRMWARE_IDX_INFO 0x4f464e49 /** * FU_IFWI_FPT_FIRMWARE_IDX_FWIM: * * The index for the IFWI firmware image. * * Since: 1.8.2 **/ #define FU_IFWI_FPT_FIRMWARE_IDX_FWIM 0x4d495746 /** * FU_IFWI_FPT_FIRMWARE_IDX_IMGI: * * The index for the IFWI image instance. * * Since: 1.8.2 **/ #define FU_IFWI_FPT_FIRMWARE_IDX_IMGI 0x49474d49 /** * FU_IFWI_FPT_FIRMWARE_IDX_SDTA: * * The index for the IFWI firmware data image. * * Since: 1.8.2 **/ #define FU_IFWI_FPT_FIRMWARE_IDX_SDTA 0x41544447 /** * FU_IFWI_FPT_FIRMWARE_IDX_CKSM: * * The index for the IFWI checksum image. * * Since: 1.8.2 **/ #define FU_IFWI_FPT_FIRMWARE_IDX_CKSM 0x4d534b43 FuFirmware * fu_ifwi_fpt_firmware_new(void); fwupd-2.0.10/libfwupdplugin/fu-ifwi.rs000066400000000000000000000033661501337203100176710ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(New, ValidateStream, ParseStream, Default)] #[repr(C, packed)] struct FuStructIfwiCpd { header_marker: u32le == 0x44504324, num_of_entries: u32le, header_version: u8, entry_version: u8, header_length: u8 = $struct_size, checksum: u8, partition_name: u32le, crc32: u32le, } #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructIfwiCpdEntry { name: [char; 12], offset: u32le, length: u32le, _reserved1: [u8; 4], } #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructIfwiCpdManifest { header_type: u32le, header_length: u32le, // dwords header_version: u32le, flags: u32le, vendor: u32le, date: u32le, size: u32le, // dwords id: u32le, rsvd: u32le, version: u64le, svn: u32le, } #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructIfwiCpdManifestExt { extension_type: u32le, extension_length: u32le, } #[derive(New, ValidateStream, ParseStream, Default)] #[repr(C, packed)] struct FuStructIfwiFpt { header_marker: u32le == 0x54504624, num_of_entries: u32le, header_version: u8 = 0x20, entry_version: u8 == 0x10, header_length: u8 = $struct_size, flags: u8, ticks_to_add: u16le, tokens_to_add: u16le, uma_size: u32le, crc32: u32le, fitc_major: u16le, fitc_minor: u16le, fitc_hotfix: u16le, fitc_build: u16le, } #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructIfwiFptEntry { partition_name: u32le, _reserved1: [u8; 4], offset: u32le, length: u32le, // bytes _reserved2: [u8; 12], partition_type: u32le, // 0 for code, 1 for data, 2 for GLUT } fwupd-2.0.10/libfwupdplugin/fu-ihex-firmware.c000066400000000000000000000363741501337203100213050ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-firmware-common.h" #include "fu-ihex-firmware.h" #include "fu-mem.h" #include "fu-string.h" /** * FuIhexFirmware: * * A Intel hex (ihex) firmware image. * * See also: [class@FuFirmware] */ typedef struct { GPtrArray *records; guint8 padding_value; } FuIhexFirmwarePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuIhexFirmware, fu_ihex_firmware, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_ihex_firmware_get_instance_private(o)) #define FU_IHEX_FIRMWARE_TOKENS_MAX 100000 /* lines */ /** * fu_ihex_firmware_get_records: * @self: A #FuIhexFirmware * * Returns the raw lines from tokenization. * * This might be useful if the plugin is expecting the hex file to be a list * of operations, rather than a simple linear image with filled holes. * * Returns: (transfer none) (element-type FuIhexFirmwareRecord): records * * Since: 1.3.4 **/ GPtrArray * fu_ihex_firmware_get_records(FuIhexFirmware *self) { FuIhexFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_IHEX_FIRMWARE(self), NULL); return priv->records; } /** * fu_ihex_firmware_set_padding_value: * @self: A #FuIhexFirmware * @padding_value: the byte used to pad the image * * Set the padding value to fill incomplete address ranges. * * The default value of zero can be changed to `0xff` if functions like * fu_bytes_is_empty() are going to be used on subsections of the data. * * Since: 1.6.0 **/ void fu_ihex_firmware_set_padding_value(FuIhexFirmware *self, guint8 padding_value) { FuIhexFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_IHEX_FIRMWARE(self)); priv->padding_value = padding_value; } static void fu_ihex_firmware_record_free(FuIhexFirmwareRecord *rcd) { g_string_free(rcd->buf, TRUE); g_byte_array_unref(rcd->data); g_free(rcd); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuIhexFirmwareRecord, fu_ihex_firmware_record_free) static FuIhexFirmwareRecord * fu_ihex_firmware_record_new(guint ln, const gchar *line, FuFirmwareParseFlags flags, GError **error) { g_autoptr(FuIhexFirmwareRecord) rcd = NULL; gsize linesz = strlen(line); guint line_end; guint16 addr16 = 0; /* check starting token */ if (line[0] != ':') { g_autofree gchar *strsafe = fu_strsafe(line, 5); if (strsafe != NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid starting token: %s", strsafe); return NULL; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid starting token"); return NULL; } /* length, 16-bit address, type */ rcd = g_new0(FuIhexFirmwareRecord, 1); rcd->ln = ln; rcd->data = g_byte_array_new(); rcd->buf = g_string_new(line); if (!fu_firmware_strparse_uint8_safe(line, linesz, 1, &rcd->byte_cnt, error)) return NULL; if (!fu_firmware_strparse_uint16_safe(line, linesz, 3, &addr16, error)) return NULL; rcd->addr = addr16; if (!fu_firmware_strparse_uint8_safe(line, linesz, 7, &rcd->record_type, error)) return NULL; /* position of checksum */ line_end = 9 + rcd->byte_cnt * 2; if (line_end > (guint)rcd->buf->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "line malformed, length: %u", line_end); return NULL; } /* verify checksum */ if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { guint8 checksum = 0; for (guint i = 1; i < line_end + 2; i += 2) { guint8 data_tmp = 0; if (!fu_firmware_strparse_uint8_safe(line, linesz, i, &data_tmp, error)) return NULL; checksum += data_tmp; } if (checksum != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid checksum (0x%02x)", checksum); return NULL; } } /* add data */ for (guint i = 9; i < line_end; i += 2) { guint8 tmp_c = 0; if (!fu_firmware_strparse_uint8_safe(line, linesz, i, &tmp_c, error)) return NULL; fu_byte_array_append_uint8(rcd->data, tmp_c); } return g_steal_pointer(&rcd); } typedef struct { FuIhexFirmware *self; FuFirmwareParseFlags flags; } FuIhexFirmwareTokenHelper; static gboolean fu_ihex_firmware_tokenize_cb(GString *token, guint token_idx, gpointer user_data, GError **error) { FuIhexFirmwareTokenHelper *helper = (FuIhexFirmwareTokenHelper *)user_data; FuIhexFirmwarePrivate *priv = GET_PRIVATE(helper->self); g_autoptr(FuIhexFirmwareRecord) rcd = NULL; /* sanity check */ if (token_idx > FU_IHEX_FIRMWARE_TOKENS_MAX) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "file has too many lines"); return FALSE; } /* remove WIN32 line endings */ g_strdelimit(token->str, "\r\x1a", '\0'); token->len = strlen(token->str); /* ignore blank lines */ if (token->len == 0) return TRUE; /* ignore comments */ if (g_str_has_prefix(token->str, ";")) return TRUE; /* parse record */ rcd = fu_ihex_firmware_record_new(token_idx + 1, token->str, helper->flags, error); if (rcd == NULL) { g_prefix_error(error, "invalid line %u: ", token_idx + 1); return FALSE; } g_ptr_array_add(priv->records, g_steal_pointer(&rcd)); return TRUE; } static gboolean fu_ihex_firmware_tokenize(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuIhexFirmware *self = FU_IHEX_FIRMWARE(firmware); FuIhexFirmwareTokenHelper helper = {.self = self, .flags = flags}; return fu_strsplit_stream(stream, 0x0, "\n", fu_ihex_firmware_tokenize_cb, &helper, error); } static gboolean fu_ihex_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuIhexFirmware *self = FU_IHEX_FIRMWARE(firmware); FuIhexFirmwarePrivate *priv = GET_PRIVATE(self); gboolean got_eof = FALSE; gboolean got_sig = FALSE; guint32 abs_addr = 0x0; guint32 addr_last = 0x0; guint32 img_addr = G_MAXUINT32; guint32 seg_addr = 0x0; g_autoptr(GBytes) img_bytes = NULL; g_autoptr(GByteArray) buf = g_byte_array_new(); /* parse records */ for (guint k = 0; k < priv->records->len; k++) { FuIhexFirmwareRecord *rcd = g_ptr_array_index(priv->records, k); guint16 addr16 = 0; guint32 addr = rcd->addr + seg_addr + abs_addr; guint32 len_hole; /* debug */ g_debug("%s:", fu_ihex_firmware_record_type_to_string(rcd->record_type)); g_debug("length:\t0x%02x", rcd->data->len); g_debug("addr:\t0x%08x", addr); /* sanity check */ if (rcd->record_type != FU_IHEX_FIRMWARE_RECORD_TYPE_EOF && rcd->data->len == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "record 0x%x had zero size", k); return FALSE; } /* process different record types */ switch (rcd->record_type) { case FU_IHEX_FIRMWARE_RECORD_TYPE_DATA: /* does not make sense */ if (got_eof) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "cannot process data after EOF"); return FALSE; } if (rcd->data->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "cannot parse invalid data"); return FALSE; } /* base address for element */ if (img_addr == G_MAXUINT32) img_addr = addr; /* does not make sense */ if (addr < addr_last) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid address 0x%x, last was 0x%x on line %u", (guint)addr, (guint)addr_last, rcd->ln); return FALSE; } /* any holes in the hex record */ len_hole = addr - addr_last; if (addr_last > 0 && len_hole > 0x100000) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "hole of 0x%x bytes too large to fill on line %u", (guint)len_hole, rcd->ln); return FALSE; } if (addr_last > 0x0 && len_hole > 1) { g_debug("filling address 0x%08x to 0x%08x on line %u", addr_last + 1, addr_last + len_hole - 1, rcd->ln); for (guint j = 1; j < len_hole; j++) fu_byte_array_append_uint8(buf, priv->padding_value); } addr_last = addr + rcd->data->len - 1; if (addr_last < addr) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "overflow of address 0x%x on line %u", (guint)addr, rcd->ln); return FALSE; } /* write into buf */ g_byte_array_append(buf, rcd->data->data, rcd->data->len); break; case FU_IHEX_FIRMWARE_RECORD_TYPE_EOF: if (got_eof) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "duplicate EOF, perhaps " "corrupt file"); return FALSE; } got_eof = TRUE; break; case FU_IHEX_FIRMWARE_RECORD_TYPE_EXTENDED_LINEAR: if (!fu_memread_uint16_safe(rcd->data->data, rcd->data->len, 0x0, &addr16, G_BIG_ENDIAN, error)) return FALSE; abs_addr = (guint32)addr16 << 16; g_debug("abs_addr:\t0x%02x on line %u", abs_addr, rcd->ln); break; case FU_IHEX_FIRMWARE_RECORD_TYPE_START_LINEAR: if (!fu_memread_uint32_safe(rcd->data->data, rcd->data->len, 0x0, &abs_addr, G_BIG_ENDIAN, error)) return FALSE; g_debug("abs_addr:\t0x%08x on line %u", abs_addr, rcd->ln); break; case FU_IHEX_FIRMWARE_RECORD_TYPE_EXTENDED_SEGMENT: if (!fu_memread_uint16_safe(rcd->data->data, rcd->data->len, 0x0, &addr16, G_BIG_ENDIAN, error)) return FALSE; /* segment base address, so ~1Mb addressable */ seg_addr = (guint32)addr16 * 16; g_debug("seg_addr:\t0x%08x on line %u", seg_addr, rcd->ln); break; case FU_IHEX_FIRMWARE_RECORD_TYPE_START_SEGMENT: /* initial content of the CS:IP registers */ if (!fu_memread_uint32_safe(rcd->data->data, rcd->data->len, 0x0, &seg_addr, G_BIG_ENDIAN, error)) return FALSE; g_debug("seg_addr:\t0x%02x on line %u", seg_addr, rcd->ln); break; case FU_IHEX_FIRMWARE_RECORD_TYPE_SIGNATURE: if (got_sig) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "duplicate signature, perhaps " "corrupt file"); return FALSE; } if (rcd->data->len > 0) { g_autoptr(GBytes) data_sig = g_bytes_new(rcd->data->data, rcd->data->len); g_autoptr(FuFirmware) img_sig = fu_firmware_new_from_bytes(data_sig); fu_firmware_set_id(img_sig, FU_FIRMWARE_ID_SIGNATURE); if (!fu_firmware_add_image_full(firmware, img_sig, error)) return FALSE; } got_sig = TRUE; break; default: /* vendors sneak in nonstandard sections past the EOF */ if (got_eof) break; g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid ihex record type %i on line %u", rcd->record_type, rcd->ln); return FALSE; } } /* no EOF */ if (!got_eof) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no EOF, perhaps truncated file"); return FALSE; } /* add single image */ img_bytes = g_bytes_new(buf->data, buf->len); if (img_addr != G_MAXUINT32) fu_firmware_set_addr(firmware, img_addr); fu_firmware_set_bytes(firmware, img_bytes); return TRUE; } static void fu_ihex_firmware_emit_chunk(GString *str, guint16 address, guint8 record_type, const guint8 *data, gsize sz) { guint8 checksum = 0x00; g_string_append_printf(str, ":%02X%04X%02X", (guint)sz, (guint)address, (guint)record_type); for (gsize j = 0; j < sz; j++) g_string_append_printf(str, "%02X", data[j]); checksum = (guint8)sz; checksum += (guint8)((address & 0xff00) >> 8); checksum += (guint8)(address & 0xff); checksum += record_type; for (gsize j = 0; j < sz; j++) checksum += data[j]; g_string_append_printf(str, "%02X\n", (guint)(((~checksum) + 0x01) & 0xff)); } static gboolean fu_ihex_firmware_image_to_string(GBytes *bytes, guint32 addr, guint8 record_type, GString *str, GError **error) { const guint8 *data; const guint chunk_size = 16; gsize len; guint32 address_offset_last = 0x0; /* get number of chunks */ data = g_bytes_get_data(bytes, &len); for (gsize i = 0; i < len; i += chunk_size) { guint32 address_tmp = addr + i; guint32 address_offset = (address_tmp >> 16) & 0xffff; gsize chunk_len = MIN(len - i, 16); /* need to offset */ if (address_offset != address_offset_last) { guint8 buf[2]; fu_memwrite_uint16(buf, address_offset, G_BIG_ENDIAN); fu_ihex_firmware_emit_chunk(str, 0x0, FU_IHEX_FIRMWARE_RECORD_TYPE_EXTENDED_LINEAR, buf, 2); address_offset_last = address_offset; } address_tmp &= 0xffff; fu_ihex_firmware_emit_chunk(str, address_tmp, record_type, data + i, chunk_len); } return TRUE; } static GByteArray * fu_ihex_firmware_write(FuFirmware *firmware, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GBytes) fw = NULL; g_autoptr(FuFirmware) img_sig = NULL; g_autoptr(GString) str = g_string_new(""); /* payload */ fw = fu_firmware_get_bytes_with_patches(firmware, error); if (fw == NULL) return NULL; if (!fu_ihex_firmware_image_to_string(fw, fu_firmware_get_addr(firmware), FU_IHEX_FIRMWARE_RECORD_TYPE_DATA, str, error)) return NULL; /* signature */ img_sig = fu_firmware_get_image_by_id(firmware, FU_FIRMWARE_ID_SIGNATURE, NULL); if (img_sig != NULL) { g_autoptr(GBytes) img_fw = fu_firmware_get_bytes(img_sig, error); if (img_fw == NULL) return NULL; if (!fu_ihex_firmware_image_to_string(img_fw, 0, FU_IHEX_FIRMWARE_RECORD_TYPE_SIGNATURE, str, error)) return NULL; } /* add EOF */ fu_ihex_firmware_emit_chunk(str, 0x0, FU_IHEX_FIRMWARE_RECORD_TYPE_EOF, NULL, 0); g_byte_array_append(buf, (const guint8 *)str->str, str->len); return g_steal_pointer(&buf); } static void fu_ihex_firmware_finalize(GObject *object) { FuIhexFirmware *self = FU_IHEX_FIRMWARE(object); FuIhexFirmwarePrivate *priv = GET_PRIVATE(self); g_ptr_array_unref(priv->records); G_OBJECT_CLASS(fu_ihex_firmware_parent_class)->finalize(object); } static void fu_ihex_firmware_init(FuIhexFirmware *self) { FuIhexFirmwarePrivate *priv = GET_PRIVATE(self); priv->padding_value = 0x00; /* chosen as we can't write 0xffff to PIC14 */ priv->records = g_ptr_array_new_with_free_func((GFreeFunc)fu_ihex_firmware_record_free); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); fu_firmware_set_images_max(FU_FIRMWARE(self), 10); } static void fu_ihex_firmware_class_init(FuIhexFirmwareClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_ihex_firmware_finalize; firmware_class->parse = fu_ihex_firmware_parse; firmware_class->tokenize = fu_ihex_firmware_tokenize; firmware_class->write = fu_ihex_firmware_write; } /** * fu_ihex_firmware_new: * * Creates a new #FuFirmware of sub type Ihex * * Since: 1.3.1 **/ FuFirmware * fu_ihex_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_IHEX_FIRMWARE, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-ihex-firmware.h000066400000000000000000000015001501337203100212710ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #include "fu-ihex-struct.h" #define FU_TYPE_IHEX_FIRMWARE (fu_ihex_firmware_get_type()) G_DECLARE_DERIVABLE_TYPE(FuIhexFirmware, fu_ihex_firmware, FU, IHEX_FIRMWARE, FuFirmware) struct _FuIhexFirmwareClass { FuFirmwareClass parent_class; }; /** * FuIhexFirmwareRecord: * * A single Intel HEX record. **/ typedef struct { guint ln; GString *buf; guint8 byte_cnt; guint32 addr; guint8 record_type; GByteArray *data; } FuIhexFirmwareRecord; FuFirmware * fu_ihex_firmware_new(void); GPtrArray * fu_ihex_firmware_get_records(FuIhexFirmware *self) G_GNUC_NON_NULL(1); void fu_ihex_firmware_set_padding_value(FuIhexFirmware *self, guint8 padding_value) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-ihex.rs000066400000000000000000000004211501337203100176550ustar00rootroot00000000000000// Copyright 2025 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] enum FuIhexFirmwareRecordType { Data, Eof, ExtendedSegment, StartSegment, ExtendedLinear, StartLinear, Signature = 0xFD, } fwupd-2.0.10/libfwupdplugin/fu-input-stream.c000066400000000000000000000525621501337203100211630ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuInputStream" #include "config.h" #include "fu-chunk-array.h" #include "fu-crc-private.h" #include "fu-input-stream.h" #include "fu-mem-private.h" #include "fu-sum.h" /** * fu_input_stream_from_path: * @path: a filename * @error: (nullable): optional return location for an error * * Opens the file as n input stream. * * Returns: (transfer full): a #GInputStream, or %NULL on error * * Since: 2.0.0 **/ GInputStream * fu_input_stream_from_path(const gchar *path, GError **error) { g_autoptr(GFile) file = NULL; g_autoptr(GFileInputStream) stream = NULL; g_return_val_if_fail(path != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); file = g_file_new_for_path(path); stream = g_file_read(file, NULL, error); if (stream == NULL) return NULL; return G_INPUT_STREAM(g_steal_pointer(&stream)); } /** * fu_input_stream_read_safe: * @stream: a #GInputStream * @buf (not nullable): a buffer to read data into * @bufsz: size of @buf * @offset: offset in bytes into @buf to copy from * @seek_set: given offset to seek to * @count: the number of bytes that will be read from the stream * @error: (nullable): optional return location for an error * * Tries to read count bytes from the stream into the buffer starting at @buf. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fu_input_stream_read_safe(GInputStream *stream, guint8 *buf, gsize bufsz, gsize offset, gsize seek_set, gsize count, GError **error) { gssize rc; g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_memchk_write(bufsz, offset, count, error)) return FALSE; if (!g_seekable_seek(G_SEEKABLE(stream), seek_set, G_SEEK_SET, NULL, error)) { g_prefix_error(error, "seek to 0x%x: ", (guint)seek_set); return FALSE; } rc = g_input_stream_read(stream, buf + offset, count, NULL, error); if (rc == -1) { g_prefix_error(error, "failed read of 0x%x: ", (guint)count); return FALSE; } if ((gsize)rc != count) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "requested 0x%x and got 0x%x", (guint)count, (guint)rc); return FALSE; } return TRUE; } /** * fu_input_stream_read_u8: * @stream: a #GInputStream * @offset: offset in bytes into @stream to copy from * @value: (out) (not nullable): the parsed value * @error: (nullable): optional return location for an error * * Read a value from a stream using a specified endian in a safe way. * * Returns: %TRUE if @value was set, %FALSE otherwise * * Since: 2.0.0 **/ gboolean fu_input_stream_read_u8(GInputStream *stream, gsize offset, guint8 *value, GError **error) { guint8 buf = 0; g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(value != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_input_stream_read_safe(stream, &buf, sizeof(buf), 0x0, offset, sizeof(buf), error)) return FALSE; *value = buf; return TRUE; } /** * fu_input_stream_read_u16: * @stream: a #GInputStream * @offset: offset in bytes into @stream to copy from * @value: (out) (not nullable): the parsed value * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * @error: (nullable): optional return location for an error * * Read a value from a stream using a specified endian in a safe way. * * Returns: %TRUE if @value was set, %FALSE otherwise * * Since: 2.0.0 **/ gboolean fu_input_stream_read_u16(GInputStream *stream, gsize offset, guint16 *value, FuEndianType endian, GError **error) { guint8 buf[2] = {0}; g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(value != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_input_stream_read_safe(stream, buf, sizeof(buf), 0x0, offset, sizeof(buf), error)) return FALSE; *value = fu_memread_uint16(buf, endian); return TRUE; } /** * fu_input_stream_read_u24: * @stream: a #GInputStream * @offset: offset in bytes into @stream to copy from * @value: (out) (not nullable): the parsed value * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * @error: (nullable): optional return location for an error * * Read a value from a stream using a specified endian in a safe way. * * Returns: %TRUE if @value was set, %FALSE otherwise * * Since: 2.0.0 **/ gboolean fu_input_stream_read_u24(GInputStream *stream, gsize offset, guint32 *value, FuEndianType endian, GError **error) { guint8 buf[3] = {0}; g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(value != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_input_stream_read_safe(stream, buf, sizeof(buf), 0x0, offset, sizeof(buf), error)) return FALSE; *value = fu_memread_uint24(buf, endian); return TRUE; } /** * fu_input_stream_read_u32: * @stream: a #GInputStream * @offset: offset in bytes into @stream to copy from * @value: (out) (not nullable): the parsed value * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * @error: (nullable): optional return location for an error * * Read a value from a stream using a specified endian in a safe way. * * Returns: %TRUE if @value was set, %FALSE otherwise * * Since: 2.0.0 **/ gboolean fu_input_stream_read_u32(GInputStream *stream, gsize offset, guint32 *value, FuEndianType endian, GError **error) { guint8 buf[4] = {0}; g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(value != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_input_stream_read_safe(stream, buf, sizeof(buf), 0x0, offset, sizeof(buf), error)) return FALSE; *value = fu_memread_uint32(buf, endian); return TRUE; } /** * fu_input_stream_read_u64: * @stream: a #GInputStream * @offset: offset in bytes into @stream to copy from * @value: (out) (not nullable): the parsed value * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * @error: (nullable): optional return location for an error * * Read a value from a stream using a specified endian in a safe way. * * Returns: %TRUE if @value was set, %FALSE otherwise * * Since: 2.0.0 **/ gboolean fu_input_stream_read_u64(GInputStream *stream, gsize offset, guint64 *value, FuEndianType endian, GError **error) { guint8 buf[8] = {0}; g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(value != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_input_stream_read_safe(stream, buf, sizeof(buf), 0x0, offset, sizeof(buf), error)) return FALSE; *value = fu_memread_uint64(buf, endian); return TRUE; } /** * fu_input_stream_read_byte_array: * @stream: a #GInputStream * @offset: offset in bytes into @stream to copy from * @count: maximum number of bytes to read * @progress: (nullable): an optional #FuProgress * @error: (nullable): optional return location for an error * * Read a byte array from a stream in a safe way. * * NOTE: The returned buffer may be smaller than @count! * * Returns: (transfer full): buffer * * Since: 2.0.0 **/ GByteArray * fu_input_stream_read_byte_array(GInputStream *stream, gsize offset, gsize count, FuProgress *progress, GError **error) { guint8 tmp[0x8000]; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GError) error_local = NULL; g_return_val_if_fail(G_IS_INPUT_STREAM(stream), NULL); g_return_val_if_fail(progress == NULL || FU_IS_PROGRESS(progress), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* this is invalid */ if (count == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "read size must be non-zero"); return NULL; } /* seek back to start */ if (G_IS_SEEKABLE(stream) && g_seekable_can_seek(G_SEEKABLE(stream))) { if (!g_seekable_seek(G_SEEKABLE(stream), offset, G_SEEK_SET, NULL, error)) return NULL; } /* read from stream in 32kB chunks */ while (TRUE) { gssize sz; sz = g_input_stream_read(stream, tmp, MIN(count - buf->len, sizeof(tmp)), NULL, &error_local); if (sz == 0) break; if (sz < 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, error_local->message); return NULL; } /* update progress */ if (progress != NULL) fu_progress_set_percentage_full(progress, buf->len, count); g_byte_array_append(buf, tmp, sz); if (buf->len >= count) break; } /* no data was read */ if (buf->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no data could be read"); return NULL; } /* success */ return g_steal_pointer(&buf); } /** * fu_input_stream_read_bytes: * @stream: a #GInputStream * @offset: offset in bytes into @stream to copy from * @count: maximum number of bytes to read * @progress: (nullable): an optional #FuProgress * @error: (nullable): optional return location for an error * * Read a #GBytes from a stream in a safe way. * * NOTE: The returned buffer may be smaller than @count! * * Returns: (transfer full): buffer * * Since: 2.0.0 **/ GBytes * fu_input_stream_read_bytes(GInputStream *stream, gsize offset, gsize count, FuProgress *progress, GError **error) { g_autoptr(GByteArray) buf = NULL; g_return_val_if_fail(G_IS_INPUT_STREAM(stream), NULL); g_return_val_if_fail(progress == NULL || FU_IS_PROGRESS(progress), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); buf = fu_input_stream_read_byte_array(stream, offset, count, progress, error); if (buf == NULL) return NULL; return g_byte_array_free_to_bytes(g_steal_pointer(&buf)); /* nocheck:blocked */ } /** * fu_input_stream_read_string: * @stream: a #GInputStream * @offset: offset in bytes into @stream to copy from * @count: maximum number of bytes to read * @error: (nullable): optional return location for an error * * Read a UTF-8 string from a stream in a safe way. * * Returns: (transfer full): string * * Since: 2.0.0 **/ gchar * fu_input_stream_read_string(GInputStream *stream, gsize offset, gsize count, GError **error) { g_autoptr(GByteArray) buf = NULL; g_return_val_if_fail(G_IS_INPUT_STREAM(stream), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); buf = fu_input_stream_read_byte_array(stream, offset, count, NULL, error); if (buf == NULL) return NULL; if (!g_utf8_validate_len((const gchar *)buf->data, buf->len, NULL)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "non UTF-8 string"); return NULL; } return g_strndup((const gchar *)buf->data, buf->len); } /** * fu_input_stream_size: * @stream: a #GInputStream * @val: (out): size in bytes * @error: (nullable): optional return location for an error * * Reads the total possible of the stream. * * If @stream is not seekable, %G_MAXSIZE is used as the size. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fu_input_stream_size(GInputStream *stream, gsize *val, GError **error) { g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* streaming from unseekable stream */ if (!G_IS_SEEKABLE(stream) || !g_seekable_can_seek(G_SEEKABLE(stream))) { if (val != NULL) *val = G_MAXSIZE; return TRUE; } if (!g_seekable_seek(G_SEEKABLE(stream), 0, G_SEEK_END, NULL, error)) { g_prefix_error(error, "seek to end: "); return FALSE; } if (val != NULL) *val = g_seekable_tell(G_SEEKABLE(stream)); /* success */ return TRUE; } static gboolean fu_input_stream_compute_checksum_cb(const guint8 *buf, gsize bufsz, gpointer user_data, GError **error) { GChecksum *csum = (GChecksum *)user_data; g_checksum_update(csum, buf, bufsz); return TRUE; } /** * fu_input_stream_compute_checksum: * @stream: a #GInputStream * @checksum_type: a #GChecksumType * @error: (nullable): optional return location for an error * * Generates the checksum of the entire stream. * * Returns: the hexadecimal representation of the checksum, or %NULL on error * * Since: 2.0.0 **/ gchar * fu_input_stream_compute_checksum(GInputStream *stream, GChecksumType checksum_type, GError **error) { g_autoptr(GChecksum) csum = g_checksum_new(checksum_type); g_return_val_if_fail(G_IS_INPUT_STREAM(stream), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); if (!fu_input_stream_chunkify(stream, fu_input_stream_compute_checksum_cb, csum, error)) return NULL; return g_strdup(g_checksum_get_string(csum)); } static gboolean fu_input_stream_compute_sum8_cb(const guint8 *buf, gsize bufsz, gpointer user_data, GError **error) { guint8 *value = (guint8 *)user_data; *value += fu_sum8(buf, bufsz); return TRUE; } /** * fu_input_stream_compute_sum8: * @stream: a #GInputStream * @value: (out): value * @error: (nullable): optional return location for an error * * Returns the arithmetic sum of all bytes in the stream. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fu_input_stream_compute_sum8(GInputStream *stream, guint8 *value, GError **error) { g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(value != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return fu_input_stream_chunkify(stream, fu_input_stream_compute_sum8_cb, value, error); } static gboolean fu_input_stream_compute_sum16_cb(const guint8 *buf, gsize bufsz, gpointer user_data, GError **error) { guint16 *value = (guint16 *)user_data; if (bufsz % sizeof(*value) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "not aligned to %u bytes, got 0x%x", (guint)sizeof(*value), (guint)bufsz); return FALSE; } *value += fu_sum16(buf, bufsz); return TRUE; } /** * fu_input_stream_compute_sum16: * @stream: a #GInputStream * @value: (out): value * @error: (nullable): optional return location for an error * * Returns the arithmetic sum of all bytes in the stream. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fu_input_stream_compute_sum16(GInputStream *stream, guint16 *value, GError **error) { g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(value != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return fu_input_stream_chunkify(stream, fu_input_stream_compute_sum16_cb, value, error); } static gboolean fu_input_stream_compute_sum32_cb(const guint8 *buf, gsize bufsz, gpointer user_data, GError **error) { guint32 *value = (guint32 *)user_data; if (bufsz % sizeof(*value) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "not aligned to %u bytes, got 0x%x", (guint)sizeof(*value), (guint)bufsz); return FALSE; } *value += fu_sum32(buf, bufsz); return TRUE; } /** * fu_input_stream_compute_sum32: * @stream: a #GInputStream * @value: (out): value * @error: (nullable): optional return location for an error * * Returns the arithmetic sum of all bytes in the stream. * * Returns: %TRUE for success * * Since: 2.0.1 **/ gboolean fu_input_stream_compute_sum32(GInputStream *stream, guint32 *value, GError **error) { g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(value != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return fu_input_stream_chunkify(stream, fu_input_stream_compute_sum32_cb, value, error); } typedef struct { FuCrcKind kind; guint32 crc; } FuInputStreamComputeCrc32Helper; static gboolean fu_input_stream_compute_crc32_cb(const guint8 *buf, gsize bufsz, gpointer user_data, GError **error) { FuInputStreamComputeCrc32Helper *helper = (FuInputStreamComputeCrc32Helper *)user_data; helper->crc = fu_crc32_step(helper->kind, buf, bufsz, helper->crc); return TRUE; } /** * fu_input_stream_compute_crc32: * @stream: a #GInputStream * @kind: a #FuCrcKind, typically %FU_CRC_KIND_B32_STANDARD * @crc: (inout): initial and final CRC value * @error: (nullable): optional return location for an error * * Returns the cyclic redundancy check value for the given memory buffer. * * NOTE: The initial @crc differs from fu_crc32_step() in that it is inverted (to make it * symmetrical, and chainable), so for most uses you want to use the value of 0x0, not 0xFFFFFFFF. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fu_input_stream_compute_crc32(GInputStream *stream, FuCrcKind kind, guint32 *crc, GError **error) { FuInputStreamComputeCrc32Helper helper = {.crc = *crc, .kind = kind}; g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(crc != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_input_stream_chunkify(stream, fu_input_stream_compute_crc32_cb, &helper, error)) return FALSE; *crc = fu_crc32_done(kind, helper.crc); return TRUE; } typedef struct { FuCrcKind kind; guint16 crc; } FuInputStreamComputeCrc16Helper; static gboolean fu_input_stream_compute_crc16_cb(const guint8 *buf, gsize bufsz, gpointer user_data, GError **error) { FuInputStreamComputeCrc16Helper *helper = (FuInputStreamComputeCrc16Helper *)user_data; helper->crc = fu_crc16_step(helper->kind, buf, bufsz, helper->crc); return TRUE; } /** * fu_input_stream_compute_crc16: * @stream: a #GInputStream * @kind: a #FuCrcKind, typically %FU_CRC_KIND_B16_XMODEM * @crc: (inout): initial and final CRC value * @error: (nullable): optional return location for an error * * Returns the cyclic redundancy check value for the given memory buffer. * * NOTE: The initial @crc differs from fu_crc16() in that it is inverted (to make it * symmetrical, and chainable), so for most uses you want to use the value of 0x0, not 0xFFFF. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fu_input_stream_compute_crc16(GInputStream *stream, FuCrcKind kind, guint16 *crc, GError **error) { FuInputStreamComputeCrc16Helper helper = {.crc = *crc, .kind = kind}; g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(crc != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_input_stream_chunkify(stream, fu_input_stream_compute_crc16_cb, &helper, error)) return FALSE; *crc = fu_crc16_done(kind, helper.crc); return TRUE; } /** * fu_input_stream_chunkify: * @stream: a #GInputStream * @func_cb: (scope async): function to call with chunks * @user_data: user data to pass to @func_cb * @error: (nullable): optional return location for an error * * Split the stream into blocks and calls a function on each chunk. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fu_input_stream_chunkify(GInputStream *stream, FuInputStreamChunkifyFunc func_cb, gpointer user_data, GError **error) { g_autoptr(FuChunkArray) chunks = NULL; g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(func_cb != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 0x8000, error); if (chunks == NULL) return FALSE; for (gsize i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!func_cb(fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), user_data, error)) return FALSE; } return TRUE; } /** * fu_input_stream_find: * @stream: a #GInputStream * @buf: input buffer to look for * @bufsz: size of @buf * @offset: (nullable): found offset * @error: (nullable): optional return location for an error * * Find a memory buffer within an input stream, without loading the entire stream into a buffer. * * Returns: %TRUE if @buf was found * * Since: 2.0.0 **/ gboolean fu_input_stream_find(GInputStream *stream, const guint8 *buf, gsize bufsz, gsize *offset, GError **error) { g_autoptr(GByteArray) buf_acc = g_byte_array_new(); const gsize blocksz = 0x10000; gsize offset_add = 0; gsize offset_cur = 0; g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(bufsz != 0, FALSE); g_return_val_if_fail(bufsz < blocksz, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); while (offset_cur < bufsz) { g_autoptr(GByteArray) buf_tmp = NULL; g_autoptr(GError) error_local = NULL; /* read more data */ buf_tmp = fu_input_stream_read_byte_array(stream, offset_cur, blocksz, NULL, &error_local); if (buf_tmp == NULL) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE)) break; g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } g_byte_array_append(buf_acc, buf_tmp->data, buf_tmp->len); /* we found something */ if (fu_memmem_safe(buf_acc->data, buf_acc->len, buf, bufsz, offset, NULL)) { if (offset != NULL) *offset += offset_add; return TRUE; } /* truncate the buffer */ if (buf_acc->len > bufsz) { offset_add += buf_acc->len - bufsz; g_byte_array_remove_range(buf_acc, 0, buf_acc->len - bufsz); } /* move the offset */ offset_cur += buf_tmp->len; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "failed to find buffer of size 0x%x", (guint)bufsz); return FALSE; } fwupd-2.0.10/libfwupdplugin/fu-input-stream.h000066400000000000000000000070131501337203100211570ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-crc.h" #include "fu-endian.h" #include "fu-progress.h" GInputStream * fu_input_stream_from_path(const gchar *path, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_input_stream_size(GInputStream *stream, gsize *val, GError **error) G_GNUC_NON_NULL(1); gboolean fu_input_stream_read_safe(GInputStream *stream, guint8 *buf, gsize bufsz, gsize offset, gsize seek_set, gsize count, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_input_stream_read_u8(GInputStream *stream, gsize offset, guint8 *value, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 3); gboolean fu_input_stream_read_u16(GInputStream *stream, gsize offset, guint16 *value, FuEndianType endian, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 3); gboolean fu_input_stream_read_u24(GInputStream *stream, gsize offset, guint32 *value, FuEndianType endian, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 3); gboolean fu_input_stream_read_u32(GInputStream *stream, gsize offset, guint32 *value, FuEndianType endian, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 3); gboolean fu_input_stream_read_u64(GInputStream *stream, gsize offset, guint64 *value, FuEndianType endian, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 3); GByteArray * fu_input_stream_read_byte_array(GInputStream *stream, gsize offset, gsize count, FuProgress *progress, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); GBytes * fu_input_stream_read_bytes(GInputStream *stream, gsize offset, gsize count, FuProgress *progress, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gchar * fu_input_stream_read_string(GInputStream *stream, gsize offset, gsize count, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); typedef gboolean (*FuInputStreamChunkifyFunc)(const guint8 *buf, gsize bufsz, gpointer user_data, GError **error) G_GNUC_WARN_UNUSED_RESULT; gboolean fu_input_stream_chunkify(GInputStream *stream, FuInputStreamChunkifyFunc func_cb, gpointer user_data, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_input_stream_compute_sum8(GInputStream *stream, guint8 *value, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_input_stream_compute_sum16(GInputStream *stream, guint16 *value, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_input_stream_compute_sum32(GInputStream *stream, guint32 *value, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_input_stream_compute_crc16(GInputStream *stream, FuCrcKind kind, guint16 *crc, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 3); gboolean fu_input_stream_compute_crc32(GInputStream *stream, FuCrcKind kind, guint32 *crc, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 3); gchar * fu_input_stream_compute_checksum(GInputStream *stream, GChecksumType checksum_type, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_input_stream_find(GInputStream *stream, const guint8 *buf, gsize bufsz, gsize *offset, GError **error) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-intel-thunderbolt-firmware.c000066400000000000000000000071021501337203100237760ustar00rootroot00000000000000/* * Copyright 2021 Dell Inc. * Copyright 2020 Richard Hughes * Copyright 2020 Mario Limonciello * Copyright 2017 Intel Corporation. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include "fu-byte-array.h" #include "fu-input-stream.h" #include "fu-intel-thunderbolt-firmware.h" #include "fu-mem.h" #include "fu-partial-input-stream.h" /** * FuIntelThunderboltFirmware: * * The Non-Volatile-Memory file-format specification. This is what you would find as the update * payload. * * See also: [class@FuFirmware] */ G_DEFINE_TYPE(FuIntelThunderboltFirmware, fu_intel_thunderbolt_firmware, FU_TYPE_INTEL_THUNDERBOLT_NVM) static gboolean fu_intel_thunderbolt_firmware_nvm_valid_farb_pointer(guint32 pointer) { return pointer != 0 && pointer != 0xFFFFFF; } static gboolean fu_intel_thunderbolt_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { const guint32 farb_offsets[] = {0x0, 0x1000}; gboolean valid = FALSE; guint32 farb_pointer = 0x0; g_autoptr(GInputStream) partial_stream = NULL; /* get header offset */ for (guint i = 0; i < G_N_ELEMENTS(farb_offsets); i++) { if (!fu_input_stream_read_u24(stream, farb_offsets[i], &farb_pointer, G_LITTLE_ENDIAN, error)) return FALSE; if (fu_intel_thunderbolt_firmware_nvm_valid_farb_pointer(farb_pointer)) { valid = TRUE; break; } } if (!valid) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no valid farb pointer found"); return FALSE; } g_debug("detected digital section begins at 0x%x", farb_pointer); fu_firmware_set_offset(firmware, farb_pointer); /* FuIntelThunderboltNvm->parse */ partial_stream = fu_partial_input_stream_new(stream, farb_pointer, G_MAXSIZE, error); if (partial_stream == NULL) { g_prefix_error(error, "failed to cut from NVM: "); return FALSE; } return FU_FIRMWARE_CLASS(fu_intel_thunderbolt_firmware_parent_class) ->parse(firmware, partial_stream, flags, error); } static GByteArray * fu_intel_thunderbolt_firmware_write(FuFirmware *firmware, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GByteArray) buf_nvm = NULL; /* sanity check */ if (fu_firmware_get_offset(firmware) < 0x08) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "not valid offset"); return NULL; } /* offset */ fu_byte_array_append_uint32(buf, fu_firmware_get_offset(firmware), G_LITTLE_ENDIAN); fu_byte_array_set_size(buf, fu_firmware_get_offset(firmware), 0x00); /* FuIntelThunderboltNvm->write */ buf_nvm = FU_FIRMWARE_CLASS(fu_intel_thunderbolt_firmware_parent_class)->write(firmware, error); if (buf_nvm == NULL) return NULL; g_byte_array_append(buf, buf_nvm->data, buf_nvm->len); /* success */ return g_steal_pointer(&buf); } static void fu_intel_thunderbolt_firmware_init(FuIntelThunderboltFirmware *self) { } static void fu_intel_thunderbolt_firmware_class_init(FuIntelThunderboltFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_intel_thunderbolt_firmware_parse; firmware_class->write = fu_intel_thunderbolt_firmware_write; } /** * fu_intel_thunderbolt_firmware_new: * * Creates a new #FuFirmware of Intel NVM format * * Since: 1.8.5 **/ FuFirmware * fu_intel_thunderbolt_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_INTEL_THUNDERBOLT_FIRMWARE, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-intel-thunderbolt-firmware.h000066400000000000000000000012471501337203100240070ustar00rootroot00000000000000/* * Copyright 2021 Dell Inc. * Copyright 2020 Richard Hughes * Copyright 2020 Mario Limonciello * Copyright 2017 Intel Corporation. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-intel-thunderbolt-nvm.h" #define FU_TYPE_INTEL_THUNDERBOLT_FIRMWARE (fu_intel_thunderbolt_firmware_get_type()) G_DECLARE_DERIVABLE_TYPE(FuIntelThunderboltFirmware, fu_intel_thunderbolt_firmware, FU, INTEL_THUNDERBOLT_FIRMWARE, FuIntelThunderboltNvm) struct _FuIntelThunderboltFirmwareClass { FuIntelThunderboltNvmClass parent_class; }; FuFirmware * fu_intel_thunderbolt_firmware_new(void); fwupd-2.0.10/libfwupdplugin/fu-intel-thunderbolt-nvm.c000066400000000000000000000562541501337203100227760ustar00rootroot00000000000000/* * Copyright 2021 Dell Inc. * Copyright 2020 Richard Hughes * Copyright 2020 Mario Limonciello * Copyright 2017 Intel Corporation. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-input-stream.h" #include "fu-intel-thunderbolt-nvm.h" #include "fu-intel-thunderbolt-struct.h" #include "fu-mem.h" #include "fu-partial-input-stream.h" #include "fu-string.h" #include "fu-version-common.h" /** * FuIntelThunderboltNvm: * * The Non-Volatile-Memory device specification. This is what you would find on the device SPI chip. * * See also: [class@FuFirmware] */ typedef struct { guint32 sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_LAST]; FuIntelThunderboltNvmFamily family; gboolean is_host; gboolean is_native; gboolean has_pd; guint16 vendor_id; guint16 device_id; guint16 model_id; guint gen; guint ports; guint8 flash_size; } FuIntelThunderboltNvmPrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuIntelThunderboltNvm, fu_intel_thunderbolt_nvm, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_intel_thunderbolt_nvm_get_instance_private(o)) /** * fu_intel_thunderbolt_nvm_get_vendor_id: * @self: a #FuFirmware * * Gets the vendor ID. * * Returns: an integer, or 0x0 for unset * * Since: 1.8.5 **/ guint16 fu_intel_thunderbolt_nvm_get_vendor_id(FuIntelThunderboltNvm *self) { FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_INTEL_THUNDERBOLT_NVM(self), G_MAXUINT16); return priv->vendor_id; } /** * fu_intel_thunderbolt_nvm_get_device_id: * @self: a #FuFirmware * * Gets the device ID. * * Returns: an integer, or 0x0 for unset * * Since: 1.8.5 **/ guint16 fu_intel_thunderbolt_nvm_get_device_id(FuIntelThunderboltNvm *self) { FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self); return priv->device_id; } /** * fu_intel_thunderbolt_nvm_is_host: * @self: a #FuFirmware * * Gets if the firmware is designed for a host controller rather than a device. * * Returns: %TRUE for controller, %FALSE for device * * Since: 1.8.5 **/ gboolean fu_intel_thunderbolt_nvm_is_host(FuIntelThunderboltNvm *self) { FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_INTEL_THUNDERBOLT_NVM(self), FALSE); return priv->is_host; } /** * fu_intel_thunderbolt_nvm_is_native: * @self: a #FuFirmware * * Gets if the device is native, i.e. not in recovery mode. * * Returns: %TRUE if set * * Since: 1.8.5 **/ gboolean fu_intel_thunderbolt_nvm_is_native(FuIntelThunderboltNvm *self) { FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_INTEL_THUNDERBOLT_NVM(self), FALSE); return priv->is_native; } /** * fu_intel_thunderbolt_nvm_has_pd: * @self: a #FuFirmware * * Gets if the device has power delivery capability. * * Returns: %TRUE if set * * Since: 1.8.5 **/ gboolean fu_intel_thunderbolt_nvm_has_pd(FuIntelThunderboltNvm *self) { FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_INTEL_THUNDERBOLT_NVM(self), FALSE); return priv->has_pd; } /** * fu_intel_thunderbolt_nvm_get_model_id: * @self: a #FuFirmware * * Gets the model ID. * * Returns: an integer, or 0x0 for unset * * Since: 1.8.5 **/ guint16 fu_intel_thunderbolt_nvm_get_model_id(FuIntelThunderboltNvm *self) { FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_INTEL_THUNDERBOLT_NVM(self), 0x0); return priv->model_id; } /** * fu_intel_thunderbolt_nvm_get_flash_size: * @self: a #FuFirmware * * Gets the flash size. * * NOTE: This does not correspond to a size in bytes, or a power of 2 and is only useful for * comparison between firmware and device. * * Returns: an integer, or 0x0 for unset * * Since: 1.8.5 **/ guint8 fu_intel_thunderbolt_nvm_get_flash_size(FuIntelThunderboltNvm *self) { FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_INTEL_THUNDERBOLT_NVM(self), 0x0); return priv->flash_size; } static void fu_intel_thunderbolt_nvm_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuIntelThunderboltNvm *self = FU_INTEL_THUNDERBOLT_NVM(firmware); FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self); fu_xmlb_builder_insert_kx(bn, "vendor_id", priv->vendor_id); fu_xmlb_builder_insert_kx(bn, "device_id", priv->device_id); fu_xmlb_builder_insert_kx(bn, "model_id", priv->model_id); fu_xmlb_builder_insert_kv(bn, "family", fu_intel_thunderbolt_nvm_family_to_string(priv->family)); fu_xmlb_builder_insert_kb(bn, "is_host", priv->is_host); fu_xmlb_builder_insert_kb(bn, "is_native", priv->is_native); fu_xmlb_builder_insert_kx(bn, "flash_size", priv->flash_size); fu_xmlb_builder_insert_kx(bn, "generation", priv->gen); fu_xmlb_builder_insert_kx(bn, "ports", priv->ports); fu_xmlb_builder_insert_kb(bn, "has_pd", priv->has_pd); for (guint i = 0; i < FU_INTEL_THUNDERBOLT_NVM_SECTION_LAST; i++) { if (priv->sections[i] != 0x0) { g_autofree gchar *tmp = g_strdup_printf("0x%x", priv->sections[i]); g_autoptr(XbBuilderNode) bc = xb_builder_node_insert(bn, "section", "type", fu_intel_thunderbolt_nvm_section_to_string(i), "offset", tmp, NULL); g_return_if_fail(bc != NULL); } } } static inline gboolean fu_intel_thunderbolt_nvm_valid_pd_pointer(guint32 pointer) { return pointer != 0 && pointer != 0xFFFFFFFF; } /* * Size of ucode sections is uint16 value saved at the start of the section, * it's in DWORDS (4-bytes) units and it doesn't include itself. We need the * offset to the next section, so we translate it to bytes and add 2 for the * size field itself. * * offset parameter must be relative to digital section */ static gboolean fu_intel_thunderbolt_nvm_read_ucode_section_len(FuIntelThunderboltNvm *self, GInputStream *stream, guint32 offset, guint16 *value, GError **error) { FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self); if (!fu_input_stream_read_u16(stream, priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL] + offset, value, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to read ucode section len: "); return FALSE; } *value *= sizeof(guint32); *value += sizeof(guint16); return TRUE; } /* assumes sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL].offset is already set */ static gboolean fu_intel_thunderbolt_nvm_read_sections(FuIntelThunderboltNvm *self, GInputStream *stream, GError **error) { guint32 offset; FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self); if (priv->gen >= 3 || priv->gen == 0) { if (!fu_input_stream_read_u32( stream, priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL] + FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_DROM, &offset, G_LITTLE_ENDIAN, error)) return FALSE; priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DROM] = offset + priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL]; if (!fu_input_stream_read_u32( stream, priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL] + FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_ARC_PARAMS, &offset, G_LITTLE_ENDIAN, error)) return FALSE; priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_ARC_PARAMS] = offset + priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL]; } if (priv->is_host && priv->gen > 2) { /* * To find the DRAM section, we have to jump from section to * section in a chain of sections. * available_sections location tells what sections exist at all * (with a flag per section). * ee_ucode_start_addr location tells the offset of the first * section in the list relatively to the digital section start. * After having the offset of the first section, we have a loop * over the section list. If the section exists, we read its * length (2 bytes at section start) and add it to current * offset to find the start of the next section. Otherwise, we * already have the next section offset... */ guint16 ucode_offset; guint8 available_sections = 0; if (!fu_input_stream_read_u8( stream, priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL] + FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_AVAILABLE_SECTIONS, &available_sections, error)) { g_prefix_error(error, "failed to read available sections: "); return FALSE; } if (!fu_input_stream_read_u16( stream, priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL] + FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_UCODE, &ucode_offset, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to read ucode offset: "); return FALSE; } offset = ucode_offset; if ((available_sections & FU_INTEL_THUNDERBOLT_NVM_SECTION_FLAG_DRAM) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "cannot find needed FW sections in the FW image file"); return FALSE; } for (guint8 i = 1; i < FU_INTEL_THUNDERBOLT_NVM_SECTION_FLAG_DRAM; i <<= 1) { if (available_sections & i) { if (!fu_intel_thunderbolt_nvm_read_ucode_section_len(self, stream, offset, &ucode_offset, error)) return FALSE; offset += ucode_offset; } } priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DRAM_UCODE] = offset + priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL]; } return TRUE; } static gboolean fu_intel_thunderbolt_nvm_missing_needed_drom(FuIntelThunderboltNvm *self) { FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self); if (priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DROM] != 0) return FALSE; if (priv->is_host && priv->gen < 3) return FALSE; return TRUE; } static gboolean fu_intel_thunderbolt_nvm_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuIntelThunderboltNvm *self = FU_INTEL_THUNDERBOLT_NVM(firmware); FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self); guint8 tmp = 0; guint16 version_raw = 0; struct { guint16 device_id; guint gen; FuIntelThunderboltNvmFamily family; guint ports; } hw_info_arr[] = {{0x156D, 2, FU_INTEL_THUNDERBOLT_NVM_FAMILY_FALCON_RIDGE, 2}, {0x156B, 2, FU_INTEL_THUNDERBOLT_NVM_FAMILY_FALCON_RIDGE, 1}, {0x157E, 2, FU_INTEL_THUNDERBOLT_NVM_FAMILY_WIN_RIDGE, 1}, {0x1578, 3, FU_INTEL_THUNDERBOLT_NVM_FAMILY_ALPINE_RIDGE, 2}, {0x1576, 3, FU_INTEL_THUNDERBOLT_NVM_FAMILY_ALPINE_RIDGE, 1}, {0x15C0, 3, FU_INTEL_THUNDERBOLT_NVM_FAMILY_ALPINE_RIDGE, 1}, {0x15D3, 3, FU_INTEL_THUNDERBOLT_NVM_FAMILY_ALPINE_RIDGE_C, 2}, {0x15DA, 3, FU_INTEL_THUNDERBOLT_NVM_FAMILY_ALPINE_RIDGE_C, 1}, {0x15E7, 3, FU_INTEL_THUNDERBOLT_NVM_FAMILY_TITAN_RIDGE, 1}, {0x15EA, 3, FU_INTEL_THUNDERBOLT_NVM_FAMILY_TITAN_RIDGE, 2}, {0x15EF, 3, FU_INTEL_THUNDERBOLT_NVM_FAMILY_TITAN_RIDGE, 2}, {0x15EE, 3, FU_INTEL_THUNDERBOLT_NVM_FAMILY_BB, 0}, {0x0B26, 4, FU_INTEL_THUNDERBOLT_NVM_FAMILY_GOSHEN_RIDGE, 2}, {0x5786, 5, FU_INTEL_THUNDERBOLT_NVM_FAMILY_BARLOW_RIDGE, 2}, /* Maple ridge devices * NOTE: These are expected to be flashed via UEFI capsules *not* * Thunderbolt plugin Flashing via fwupd will require matching kernel * work. They're left here only for parsing the binaries */ {0x1136, 4, FU_INTEL_THUNDERBOLT_NVM_FAMILY_MAPLE_RIDGE, 2}, {0x1137, 4, FU_INTEL_THUNDERBOLT_NVM_FAMILY_MAPLE_RIDGE, 2}, {0}}; g_autoptr(FuFirmware) img_payload = fu_firmware_new(); gsize streamsz = 0; /* add this straight away */ priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL] = 0x0; /* is native */ if (!fu_input_stream_read_u8(stream, priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL] + FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_FLAGS_IS_NATIVE, &tmp, error)) { g_prefix_error(error, "failed to read native: "); return FALSE; } priv->is_native = tmp & 0x20; /* we're only reading the first chunk */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz == 0x80) return TRUE; /* host or device */ if (!fu_input_stream_read_u8(stream, priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL] + FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_FLAGS_HOST, &tmp, error)) { g_prefix_error(error, "failed to read is-host: "); return FALSE; } priv->is_host = tmp & (1 << 1); /* device ID */ if (!fu_input_stream_read_u16(stream, priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL] + FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_DEVICE_ID, &priv->device_id, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to read device-id: "); return FALSE; } /* this is best-effort */ for (guint i = 0; hw_info_arr[i].device_id != 0; i++) { if (hw_info_arr[i].device_id == priv->device_id) { priv->family = hw_info_arr[i].family; priv->gen = hw_info_arr[i].gen; priv->ports = hw_info_arr[i].ports; break; } } if (priv->family == FU_INTEL_THUNDERBOLT_NVM_FAMILY_UNKNOWN) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "unknown NVM family"); return FALSE; } if (priv->ports == 0 && priv->is_host) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unknown controller: %x", priv->device_id); return FALSE; } /* read sections from file */ if (!fu_intel_thunderbolt_nvm_read_sections(self, stream, error)) return FALSE; if (fu_intel_thunderbolt_nvm_missing_needed_drom(self)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_READ, "cannot find required drom section"); return FALSE; } /* vendor:model */ if (priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DROM] != 0x0) { if (!fu_input_stream_read_u16( stream, priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DROM] + FU_INTEL_THUNDERBOLT_NVM_DROM_OFFSET_VENDOR_ID, &priv->vendor_id, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to read vendor-id: "); return FALSE; } if (!fu_input_stream_read_u16( stream, priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DROM] + FU_INTEL_THUNDERBOLT_NVM_DROM_OFFSET_MODEL_ID, &priv->model_id, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to read model-id: "); return FALSE; } } /* versions */ switch (priv->family) { case FU_INTEL_THUNDERBOLT_NVM_FAMILY_TITAN_RIDGE: case FU_INTEL_THUNDERBOLT_NVM_FAMILY_GOSHEN_RIDGE: case FU_INTEL_THUNDERBOLT_NVM_FAMILY_BARLOW_RIDGE: if (!fu_input_stream_read_u16( stream, priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL] + FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_VERSION, &version_raw, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to read version: "); return FALSE; } fu_firmware_set_version_raw(FU_FIRMWARE(self), version_raw); break; default: break; } if (priv->is_host) { switch (priv->family) { case FU_INTEL_THUNDERBOLT_NVM_FAMILY_ALPINE_RIDGE: case FU_INTEL_THUNDERBOLT_NVM_FAMILY_ALPINE_RIDGE_C: case FU_INTEL_THUNDERBOLT_NVM_FAMILY_TITAN_RIDGE: /* used for comparison between old and new image, not a raw number */ if (!fu_input_stream_read_u8( stream, priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_DIGITAL] + FU_INTEL_THUNDERBOLT_NVM_DIGITAL_OFFSET_FLASH_SIZE, &tmp, error)) { g_prefix_error(error, "failed to read flash size: "); return FALSE; } priv->flash_size = tmp & 0x07; break; default: break; } } /* we're only reading enough to get the vendor-id and model-id */ if (streamsz < priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_ARC_PARAMS]) return TRUE; /* has PD */ if (priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_ARC_PARAMS] != 0x0) { guint32 pd_pointer = 0x0; if (!fu_input_stream_read_u32( stream, priv->sections[FU_INTEL_THUNDERBOLT_NVM_SECTION_ARC_PARAMS] + FU_INTEL_THUNDERBOLT_NVM_ARC_PARAMS_OFFSET_PD_POINTER, &pd_pointer, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to read pd-pointer: "); return FALSE; } priv->has_pd = fu_intel_thunderbolt_nvm_valid_pd_pointer(pd_pointer); } /* as as easy-to-grab payload blob */ if (!fu_firmware_parse_stream(img_payload, stream, 0x0, flags, error)) return FALSE; fu_firmware_set_id(img_payload, FU_FIRMWARE_ID_PAYLOAD); if (!fu_firmware_add_image_full(firmware, img_payload, error)) return FALSE; /* success */ return TRUE; } /* can only write version 3 NVM */ static GByteArray * fu_intel_thunderbolt_nvm_write(FuFirmware *firmware, GError **error) { FuIntelThunderboltNvm *self = FU_INTEL_THUNDERBOLT_NVM(firmware); FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self); g_autoptr(GByteArray) st = fu_intel_thunderbolt_nvm_digital_new(); g_autoptr(GByteArray) st_drom = fu_intel_thunderbolt_nvm_drom_new(); g_autoptr(GByteArray) st_arc = fu_intel_thunderbolt_nvm_arc_params_new(); g_autoptr(GByteArray) st_dram = fu_intel_thunderbolt_nvm_dram_new(); /* digital section */ fu_intel_thunderbolt_nvm_digital_set_available_sections( st, FU_INTEL_THUNDERBOLT_NVM_SECTION_FLAG_DRAM); fu_intel_thunderbolt_nvm_digital_set_device_id(st, priv->device_id); fu_intel_thunderbolt_nvm_digital_set_version(st, fu_firmware_get_version_raw(firmware)); fu_intel_thunderbolt_nvm_digital_set_flags_host(st, priv->is_host ? 0x2 : 0x0); fu_intel_thunderbolt_nvm_digital_set_flash_size(st, priv->flash_size); fu_intel_thunderbolt_nvm_digital_set_flags_is_native(st, priv->is_native ? 0x20 : 0x0); /* drom section */ fu_intel_thunderbolt_nvm_digital_set_drom(st, st->len); fu_intel_thunderbolt_nvm_drom_set_vendor_id(st_drom, priv->vendor_id); fu_intel_thunderbolt_nvm_drom_set_model_id(st_drom, priv->model_id); g_byte_array_append(st, st_drom->data, st_drom->len); /* ARC param section */ fu_intel_thunderbolt_nvm_digital_set_arc_params(st, st->len); fu_intel_thunderbolt_nvm_arc_params_set_pd_pointer(st_arc, priv->has_pd ? 0x1 : 0x0); g_byte_array_append(st, st_arc->data, st_arc->len); /* dram section */ fu_intel_thunderbolt_nvm_digital_set_ucode(st, st->len); g_byte_array_append(st, st_dram->data, st_dram->len); /* success */ return g_steal_pointer(&st); } static gboolean fu_intel_thunderbolt_nvm_check_compatible(FuFirmware *firmware, FuFirmware *firmware_other, FuFirmwareParseFlags flags, GError **error) { FuIntelThunderboltNvm *self = FU_INTEL_THUNDERBOLT_NVM(firmware); FuIntelThunderboltNvm *other = FU_INTEL_THUNDERBOLT_NVM(firmware_other); FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self); FuIntelThunderboltNvmPrivate *priv_other = GET_PRIVATE(other); if (priv->is_host != priv_other->is_host) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "incorrect firmware mode, got %s, expected %s", priv->is_host ? "host" : "device", priv_other->is_host ? "host" : "device"); return FALSE; } if (priv->vendor_id != priv_other->vendor_id) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "incorrect device vendor, got 0x%04x, expected 0x%04x", priv->vendor_id, priv_other->vendor_id); return FALSE; } if (priv->device_id != priv_other->device_id) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "incorrect device type, got 0x%04x, expected 0x%04x", priv->device_id, priv_other->device_id); return FALSE; } if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_VID_PID) == 0) { if (priv->model_id != priv_other->model_id) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "incorrect device model, got 0x%04x, expected 0x%04x", priv->model_id, priv_other->model_id); return FALSE; } /* old firmware has PD but new doesn't (we don't care about other way around) */ if (priv->has_pd && !priv_other->has_pd) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "incorrect PD section"); return FALSE; } if (priv->flash_size != priv_other->flash_size) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "incorrect flash size, got 0x%x and expected 0x%x", priv->flash_size, priv_other->flash_size); return FALSE; } } return TRUE; } static gboolean fu_intel_thunderbolt_nvm_build(FuFirmware *firmware, XbNode *n, GError **error) { FuIntelThunderboltNvm *self = FU_INTEL_THUNDERBOLT_NVM(firmware); FuIntelThunderboltNvmPrivate *priv = GET_PRIVATE(self); const gchar *tmp; /* simple properties */ tmp = xb_node_query_text(n, "vendor_id", NULL); if (tmp != NULL) { guint64 val = 0; if (!fu_strtoull(tmp, &val, 0x0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->vendor_id = val; } tmp = xb_node_query_text(n, "device_id", NULL); if (tmp != NULL) { guint64 val = 0; if (!fu_strtoull(tmp, &val, 0x0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->device_id = val; } tmp = xb_node_query_text(n, "model_id", NULL); if (tmp != NULL) { guint64 val = 0; if (!fu_strtoull(tmp, &val, 0x0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->model_id = val; } tmp = xb_node_query_text(n, "family", NULL); if (tmp != NULL) { priv->family = fu_intel_thunderbolt_nvm_family_from_string(tmp); if (priv->family == FU_INTEL_THUNDERBOLT_NVM_FAMILY_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "unknown family: %s", tmp); return FALSE; } } tmp = xb_node_query_text(n, "flash_size", NULL); if (tmp != NULL) { guint64 val = 0; if (!fu_strtoull(tmp, &val, 0x0, 0x07, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->flash_size = val; } tmp = xb_node_query_text(n, "is_host", NULL); if (tmp != NULL) { if (!fu_strtobool(tmp, &priv->is_host, error)) return FALSE; } tmp = xb_node_query_text(n, "is_native", NULL); if (tmp != NULL) { if (!fu_strtobool(tmp, &priv->is_native, error)) return FALSE; } /* success */ return TRUE; } static gchar * fu_intel_thunderbolt_nvm_convert_version(FuFirmware *firmware, guint64 version_raw) { return fu_version_from_uint16(version_raw, fu_firmware_get_version_format(firmware)); } static void fu_intel_thunderbolt_nvm_init(FuIntelThunderboltNvm *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_VID_PID); fu_firmware_set_images_max(FU_FIRMWARE(self), 1024); fu_firmware_set_version_format(FU_FIRMWARE(self), FWUPD_VERSION_FORMAT_BCD); } static void fu_intel_thunderbolt_nvm_class_init(FuIntelThunderboltNvmClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->convert_version = fu_intel_thunderbolt_nvm_convert_version; firmware_class->export = fu_intel_thunderbolt_nvm_export; firmware_class->parse = fu_intel_thunderbolt_nvm_parse; firmware_class->write = fu_intel_thunderbolt_nvm_write; firmware_class->build = fu_intel_thunderbolt_nvm_build; firmware_class->check_compatible = fu_intel_thunderbolt_nvm_check_compatible; } /** * fu_intel_thunderbolt_nvm_new: * * Creates a new #FuFirmware of Intel NVM format * * Since: 1.8.5 **/ FuFirmware * fu_intel_thunderbolt_nvm_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_INTEL_THUNDERBOLT_NVM, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-intel-thunderbolt-nvm.h000066400000000000000000000023631501337203100227730ustar00rootroot00000000000000/* * Copyright 2021 Dell Inc. * Copyright 2020 Richard Hughes * Copyright 2020 Mario Limonciello * Copyright 2017 Intel Corporation. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_INTEL_THUNDERBOLT_NVM (fu_intel_thunderbolt_nvm_get_type()) G_DECLARE_DERIVABLE_TYPE(FuIntelThunderboltNvm, fu_intel_thunderbolt_nvm, FU, INTEL_THUNDERBOLT_NVM, FuFirmware) struct _FuIntelThunderboltNvmClass { FuFirmwareClass parent_class; }; guint16 fu_intel_thunderbolt_nvm_get_vendor_id(FuIntelThunderboltNvm *self) G_GNUC_NON_NULL(1); guint16 fu_intel_thunderbolt_nvm_get_device_id(FuIntelThunderboltNvm *self) G_GNUC_NON_NULL(1); guint16 fu_intel_thunderbolt_nvm_get_model_id(FuIntelThunderboltNvm *self) G_GNUC_NON_NULL(1); gboolean fu_intel_thunderbolt_nvm_is_host(FuIntelThunderboltNvm *self) G_GNUC_NON_NULL(1); gboolean fu_intel_thunderbolt_nvm_is_native(FuIntelThunderboltNvm *self) G_GNUC_NON_NULL(1); gboolean fu_intel_thunderbolt_nvm_has_pd(FuIntelThunderboltNvm *self) G_GNUC_NON_NULL(1); guint8 fu_intel_thunderbolt_nvm_get_flash_size(FuIntelThunderboltNvm *self) G_GNUC_NON_NULL(1); FuFirmware * fu_intel_thunderbolt_nvm_new(void); fwupd-2.0.10/libfwupdplugin/fu-intel-thunderbolt.rs000066400000000000000000000025641501337203100223750ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] enum FuIntelThunderboltNvmSection { Digital, Drom, ArcParams, DramUcode, } #[repr(u8)] enum FuIntelThunderboltNvmSectionFlag { Dram = 1 << 6, } #[derive(ToString, FromString)] enum FuIntelThunderboltNvmFamily { Unknown, FalconRidge, WinRidge, AlpineRidge, AlpineRidgeC, TitanRidge, Bb, MapleRidge, GoshenRidge, BarlowRidge, } #[derive(New)] #[repr(C, packed)] struct FuIntelThunderboltNvmDigital { reserved: [u8; 2], available_sections: u8, // FuIntelThunderboltNvmSectionFlag ucode: u16le, // addr device_id: u32le, version: u16le, reserved: [u8; 5], flags_host: u8, reserved: [u8; 52], flash_size: u8, reserved: [u8; 47], arc_params: u32le, reserved: [u8; 2], flags_is_native: u8, reserved: [u8; 146], drom: u32le, reserved: [u8; 14], } #[derive(New)] #[repr(C, packed)] struct FuIntelThunderboltNvmDrom { reserved: [u8; 16], vendor_id: u16le, model_id: u16le, reserved: [u8; 12], } #[derive(New)] #[repr(C, packed)] struct FuIntelThunderboltNvmArcParams { reserved: [u8; 268], pd_pointer: u32le, reserved: [u8; 16], } #[derive(New)] #[repr(C, packed)] struct FuIntelThunderboltNvmDram { reserved: [u8; 16], } fwupd-2.0.10/libfwupdplugin/fu-io-channel.c000066400000000000000000000407131501337203100205430ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuIOChannel" #include "config.h" #include #include #include #include #ifdef HAVE_POLL_H #include #endif #include #include #ifdef HAVE_MEMFD_CREATE #include #endif #include "fwupd-error.h" #include "fu-common.h" #include "fu-input-stream.h" #include "fu-io-channel.h" #include "fu-string.h" /** * FuIOChannel: * * A bidirectional IO channel which can be read from and written to. */ struct _FuIOChannel { GObject parent_instance; gint fd; }; G_DEFINE_TYPE(FuIOChannel, fu_io_channel, G_TYPE_OBJECT) /** * fu_io_channel_unix_get_fd: * @self: a #FuIOChannel * * Gets the file descriptor for the device. * * Returns: fd, or -1 for not open. * * Since: 1.2.2 **/ gint fu_io_channel_unix_get_fd(FuIOChannel *self) { g_return_val_if_fail(FU_IS_IO_CHANNEL(self), -1); return self->fd; } /** * fu_io_channel_shutdown: * @self: a #FuIOChannel * @error: (nullable): optional return location for an error * * Closes the file descriptor for the device if open. * * Returns: %TRUE if all the FD was closed. * * Since: 1.2.2 **/ gboolean fu_io_channel_shutdown(FuIOChannel *self, GError **error) { g_return_val_if_fail(FU_IS_IO_CHANNEL(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (self->fd != -1) { if (!g_close(self->fd, error)) return FALSE; self->fd = -1; } return TRUE; } /** * fu_io_channel_seek: * @self: a #FuIOChannel * @offset: an absolute offset in bytes * @error: (nullable): optional return location for an error * * Seeks the file descriptor to a specific offset. * * Returns: %TRUE if all the seek worked. * * Since: 2.0.0 **/ gboolean fu_io_channel_seek(FuIOChannel *self, gsize offset, GError **error) { g_return_val_if_fail(FU_IS_IO_CHANNEL(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (self->fd == -1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "channel is not open"); return FALSE; } if (lseek(self->fd, offset, SEEK_SET) < 0) { g_set_error(error, G_IO_ERROR, /* nocheck:error */ #ifdef HAVE_ERRNO_H g_io_error_from_errno(errno), #else G_IO_ERROR_FAILED, /* nocheck:blocked */ #endif "failed to seek to 0x%04x: %s", (guint)offset, g_strerror(errno)); fwupd_error_convert(error); return FALSE; } /* success */ return TRUE; } static gboolean fu_io_channel_flush_input(FuIOChannel *self, GError **error) { GPollFD poll = { .fd = self->fd, .events = G_IO_IN | G_IO_ERR, }; while (g_poll(&poll, 1, 0) > 0) { gchar c; gint r = read(self->fd, &c, 1); if (r < 0 && errno != EINTR) break; } return TRUE; } /** * fu_io_channel_write_bytes: * @self: a #FuIOChannel * @bytes: buffer to write * @timeout_ms: timeout in ms * @flags: channel flags, e.g. %FU_IO_CHANNEL_FLAG_SINGLE_SHOT * @error: (nullable): optional return location for an error * * Writes bytes to the TTY, that will fail if exceeding @timeout_ms. * * Returns: %TRUE if all the bytes was written * * Since: 1.2.2 **/ gboolean fu_io_channel_write_bytes(FuIOChannel *self, GBytes *bytes, guint timeout_ms, FuIOChannelFlags flags, GError **error) { gsize bufsz = 0; const guint8 *buf = g_bytes_get_data(bytes, &bufsz); return fu_io_channel_write_raw(self, buf, bufsz, timeout_ms, flags, error); } typedef struct { FuIOChannel *self; guint timeout_ms; FuIOChannelFlags flags; } FuIOChannelWriteStreamHelper; static gboolean fu_io_channel_write_stream_cb(const guint8 *buf, gsize bufsz, gpointer user_data, GError **error) { FuIOChannelWriteStreamHelper *helper = (FuIOChannelWriteStreamHelper *)user_data; return fu_io_channel_write_raw(helper->self, buf, bufsz, helper->timeout_ms, helper->flags, error); } /** * fu_io_channel_write_stream: * @self: a #FuIOChannel * @stream: #GInputStream to write * @timeout_ms: timeout in ms * @flags: channel flags, e.g. %FU_IO_CHANNEL_FLAG_SINGLE_SHOT * @error: (nullable): optional return location for an error * * Writes the stream to the fd, chucking when required. * * Returns: %TRUE if all the bytes was written * * Since: 2.0.0 **/ gboolean fu_io_channel_write_stream(FuIOChannel *self, GInputStream *stream, guint timeout_ms, FuIOChannelFlags flags, GError **error) { FuIOChannelWriteStreamHelper helper = {.self = self, .timeout_ms = timeout_ms, .flags = flags}; g_return_val_if_fail(FU_IS_IO_CHANNEL(self), FALSE); g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return fu_input_stream_chunkify(stream, fu_io_channel_write_stream_cb, &helper, error); } /** * fu_io_channel_write_byte_array: * @self: a #FuIOChannel * @buf: buffer to write * @timeout_ms: timeout in ms * @flags: channel flags, e.g. %FU_IO_CHANNEL_FLAG_SINGLE_SHOT * @error: (nullable): optional return location for an error * * Writes bytes to the TTY, that will fail if exceeding @timeout_ms. * * Returns: %TRUE if all the bytes was written * * Since: 1.3.2 **/ gboolean fu_io_channel_write_byte_array(FuIOChannel *self, GByteArray *buf, guint timeout_ms, FuIOChannelFlags flags, GError **error) { return fu_io_channel_write_raw(self, buf->data, buf->len, timeout_ms, flags, error); } /** * fu_io_channel_write_raw: * @self: a #FuIOChannel * @data: buffer to write * @datasz: size of @data * @timeout_ms: timeout in ms * @flags: channel flags, e.g. %FU_IO_CHANNEL_FLAG_SINGLE_SHOT * @error: (nullable): optional return location for an error * * Writes bytes to the TTY, that will fail if exceeding @timeout_ms. * * Returns: %TRUE if all the bytes was written * * Since: 1.2.2 **/ gboolean fu_io_channel_write_raw(FuIOChannel *self, const guint8 *data, gsize datasz, guint timeout_ms, FuIOChannelFlags flags, GError **error) { gsize idx = 0; g_return_val_if_fail(FU_IS_IO_CHANNEL(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* flush pending reads */ if (flags & FU_IO_CHANNEL_FLAG_FLUSH_INPUT) { if (!fu_io_channel_flush_input(self, error)) return FALSE; } /* blocking IO */ if (flags & FU_IO_CHANNEL_FLAG_USE_BLOCKING_IO) { gssize wrote = write(self->fd, data, datasz); if (wrote != (gssize)datasz) { if (errno == EPROTO) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "failed to write: %s", g_strerror(errno)); return FALSE; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to write: " "wrote %" G_GSSIZE_FORMAT " of %" G_GSIZE_FORMAT, wrote, datasz); return FALSE; } return TRUE; } /* nonblocking IO */ while (idx < datasz) { gint rc; GPollFD fds = { .fd = self->fd, .events = G_IO_OUT | G_IO_ERR, }; /* wait for data to be allowed to write without blocking */ rc = g_poll(&fds, 1, (gint)timeout_ms); if (rc == 0) break; if (rc < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "failed to poll %i", self->fd); return FALSE; } /* we can write data */ if (fds.revents & G_IO_OUT) { gssize len = write(self->fd, data + idx, datasz - idx); if (len < 0) { if (errno == EAGAIN) { g_debug("got EAGAIN, trying harder"); continue; } if (errno == EPROTO) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "failed to write: %s", g_strerror(errno)); return FALSE; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to write %" G_GSIZE_FORMAT " bytes to %i: %s", datasz, self->fd, g_strerror(errno)); return FALSE; } if (flags & FU_IO_CHANNEL_FLAG_SINGLE_SHOT) break; idx += len; } } return TRUE; } /** * fu_io_channel_read_bytes: * @self: a #FuIOChannel * @count: number of bytes to read, or -1 for no limit * @timeout_ms: timeout in ms * @flags: channel flags, e.g. %FU_IO_CHANNEL_FLAG_SINGLE_SHOT * @error: (nullable): optional return location for an error * * Reads bytes from the TTY, that will fail if exceeding @timeout_ms. * * Returns: a #GBytes (which may be bigger than @count), or %NULL for error * * Since: 1.2.2 **/ GBytes * fu_io_channel_read_bytes(FuIOChannel *self, gssize count, guint timeout_ms, FuIOChannelFlags flags, GError **error) { g_autoptr(GByteArray) buf = fu_io_channel_read_byte_array(self, count, timeout_ms, flags, error); if (buf == NULL) return NULL; return g_bytes_new(buf->data, buf->len); } /** * fu_io_channel_read_byte_array: * @self: a #FuIOChannel * @count: number of bytes to read, or -1 for no limit * @timeout_ms: timeout in ms * @flags: channel flags, e.g. %FU_IO_CHANNEL_FLAG_SINGLE_SHOT * @error: (nullable): optional return location for an error * * Reads bytes from the TTY, that will fail if exceeding @timeout_ms. * * Returns: (transfer full): a #GByteArray (which may be bigger than @count), or %NULL for error * * Since: 1.3.2 **/ GByteArray * fu_io_channel_read_byte_array(FuIOChannel *self, gssize count, guint timeout_ms, FuIOChannelFlags flags, GError **error) { GPollFD fds = { .fd = self->fd, .events = G_IO_IN | G_IO_PRI | G_IO_ERR, }; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GByteArray) buf_tmp = g_byte_array_new(); g_return_val_if_fail(FU_IS_IO_CHANNEL(self), NULL); /* a temp buf of 1k or smaller size */ g_byte_array_set_size(buf_tmp, count >= 0 ? MIN(count, 1024) : 1024); /* blocking IO */ if (flags & FU_IO_CHANNEL_FLAG_USE_BLOCKING_IO) { do { gssize len = read(self->fd, buf_tmp->data, buf_tmp->len); if (len < 0) { g_set_error(error, G_IO_ERROR, /* nocheck:error */ #ifdef HAVE_ERRNO_H g_io_error_from_errno(errno), #else G_IO_ERROR_FAILED, /* nocheck:blocked */ #endif "failed to read %i: %s", self->fd, g_strerror(errno)); fwupd_error_convert(error); return NULL; } if (len == 0) break; if (flags & FU_IO_CHANNEL_FLAG_SINGLE_SHOT) break; g_byte_array_append(buf, buf_tmp->data, len); } while (count < 0 || buf->len < (gsize)count); return g_steal_pointer(&buf); } /* nonblocking IO */ while (TRUE) { /* wait for data to appear */ gint rc = g_poll(&fds, 1, (gint)timeout_ms); if (rc == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "timeout"); return NULL; } if (rc < 0) { if (errno == EINTR) continue; g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "failed to poll %i", self->fd); return NULL; } /* we have data to read */ if (fds.revents & G_IO_IN) { gssize len = read(self->fd, buf_tmp->data, buf_tmp->len); if (len < 0) { if (errno == EINTR) continue; if (errno == EAGAIN) continue; g_set_error(error, G_IO_ERROR, /* nocheck:error */ #ifdef HAVE_ERRNO_H g_io_error_from_errno(errno), #else G_IO_ERROR_FAILED, /* nocheck:blocked */ #endif "failed to read %i: %s", self->fd, g_strerror(errno)); fwupd_error_convert(error); return NULL; } if (len == 0) break; if (len > 0) g_byte_array_append(buf, buf_tmp->data, len); /* check maximum size */ if (count > 0 && buf->len >= (guint)count) break; if (flags & FU_IO_CHANNEL_FLAG_SINGLE_SHOT) break; continue; } if (fds.revents & G_IO_ERR) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_READ, "error condition"); return NULL; } if (fds.revents & G_IO_HUP) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_READ, "connection hung up"); return NULL; } if (fds.revents & G_IO_NVAL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_READ, "invalid request"); return NULL; } } /* no data */ if (buf->len == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "no data received from device in %ums", timeout_ms); return NULL; } /* return blob */ return g_steal_pointer(&buf); } /** * fu_io_channel_read_raw: * @self: a #FuIOChannel * @buf: (nullable): optional buffer * @bufsz: size of @buf * @bytes_read: (out) (nullable): data written to @buf * @timeout_ms: timeout in ms * @flags: channel flags, e.g. %FU_IO_CHANNEL_FLAG_SINGLE_SHOT * @error: (nullable): optional return location for an error * * Reads bytes from the TTY, that will fail if exceeding @timeout_ms. * * Returns: a #GBytes, or %NULL for error * * Since: 1.2.2 **/ gboolean fu_io_channel_read_raw(FuIOChannel *self, guint8 *buf, gsize bufsz, gsize *bytes_read, guint timeout_ms, FuIOChannelFlags flags, GError **error) { g_autoptr(GByteArray) tmp = NULL; g_return_val_if_fail(FU_IS_IO_CHANNEL(self), FALSE); tmp = fu_io_channel_read_byte_array(self, bufsz, timeout_ms, flags, error); if (tmp == NULL) return FALSE; if (buf != NULL) memcpy(buf, tmp->data, MIN(tmp->len, bufsz)); /* nocheck:blocked */ if (bytes_read != NULL) *bytes_read = tmp->len; return TRUE; } static void fu_io_channel_finalize(GObject *object) { FuIOChannel *self = FU_IO_CHANNEL(object); if (self->fd != -1) g_close(self->fd, NULL); G_OBJECT_CLASS(fu_io_channel_parent_class)->finalize(object); } static void fu_io_channel_class_init(FuIOChannelClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_io_channel_finalize; } static void fu_io_channel_init(FuIOChannel *self) { self->fd = -1; } /** * fu_io_channel_unix_new: * @fd: file descriptor * * Creates a new object to write and read from. * * Returns: a #FuIOChannel * * Since: 1.2.2 **/ FuIOChannel * fu_io_channel_unix_new(gint fd) { FuIOChannel *self; self = g_object_new(FU_TYPE_IO_CHANNEL, NULL); self->fd = fd; return FU_IO_CHANNEL(self); } /** * fu_io_channel_new_file: * @filename: device file * @open_flags: some #FuIoChannelOpenFlag typically %FU_IO_CHANNEL_OPEN_FLAG_READ * @error: (nullable): optional return location for an error * * Creates a new object to write and/or read from. * * Returns: a #FuIOChannel * * Since: 2.0.0 **/ FuIOChannel * fu_io_channel_new_file(const gchar *filename, FuIoChannelOpenFlag open_flags, GError **error) { gint fd; int flags = 0; g_return_val_if_fail(filename != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); if (open_flags & FU_IO_CHANNEL_OPEN_FLAG_READ && open_flags & FU_IO_CHANNEL_OPEN_FLAG_WRITE) { flags |= O_RDWR; } else if (open_flags & FU_IO_CHANNEL_OPEN_FLAG_READ) { flags |= O_RDONLY; } else if (open_flags & FU_IO_CHANNEL_OPEN_FLAG_WRITE) { flags |= O_WRONLY; } #ifdef O_NONBLOCK if (open_flags & FU_IO_CHANNEL_OPEN_FLAG_NONBLOCK) flags |= O_NONBLOCK; #endif #ifdef O_SYNC if (open_flags & FU_IO_CHANNEL_OPEN_FLAG_SYNC) flags |= O_SYNC; #endif fd = g_open(filename, flags, S_IRWXU); if (fd < 0) { g_set_error(error, G_IO_ERROR, /* nocheck:error */ #ifdef HAVE_ERRNO_H g_io_error_from_errno(errno), #else G_IO_ERROR_FAILED, /* nocheck:blocked */ #endif "failed to open %s: %s", filename, g_strerror(errno)); fwupd_error_convert(error); return NULL; } return fu_io_channel_unix_new(fd); } /** * fu_io_channel_virtual_new: * @name: (not nullable): memfd name * @error: (nullable): optional return location for an error * * Creates a new virtual object to write and/or read from. * * Returns: a #FuIOChannel * * Since: 2.0.0 **/ FuIOChannel * fu_io_channel_virtual_new(const gchar *name, GError **error) { #ifdef HAVE_MEMFD_CREATE gint fd; g_return_val_if_fail(name != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); fd = memfd_create(name, MFD_CLOEXEC); if (fd < 0) { g_set_error(error, G_IO_ERROR, /* nocheck:error */ #ifdef HAVE_ERRNO_H g_io_error_from_errno(errno), #else G_IO_ERROR_FAILED, /* nocheck:blocked */ #endif "failed to create %s: %s", name, g_strerror(errno)); fwupd_error_convert(error); return NULL; } return fu_io_channel_unix_new(fd); #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "memfd not supported"); return NULL; #endif } fwupd-2.0.10/libfwupdplugin/fu-io-channel.h000066400000000000000000000060741501337203100205520ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-io-channel-struct.h" #define FU_TYPE_IO_CHANNEL (fu_io_channel_get_type()) G_DECLARE_FINAL_TYPE(FuIOChannel, fu_io_channel, FU, IO_CHANNEL, GObject) /** * FuIOChannelFlags: * @FU_IO_CHANNEL_FLAG_NONE: No flags are set * @FU_IO_CHANNEL_FLAG_SINGLE_SHOT: Only one read or write is expected * @FU_IO_CHANNEL_FLAG_FLUSH_INPUT: Flush pending input before writing * @FU_IO_CHANNEL_FLAG_USE_BLOCKING_IO: Block waiting for the TTY * * The flags used when reading data from the TTY. **/ typedef enum { FU_IO_CHANNEL_FLAG_NONE = 0, /* Since: 1.2.2 */ FU_IO_CHANNEL_FLAG_SINGLE_SHOT = 1 << 0, /* Since: 1.2.2 */ FU_IO_CHANNEL_FLAG_FLUSH_INPUT = 1 << 1, /* Since: 1.2.2 */ FU_IO_CHANNEL_FLAG_USE_BLOCKING_IO = 1 << 2, /* Since: 1.2.2 */ /*< private >*/ FU_IO_CHANNEL_FLAG_LAST } FuIOChannelFlags; FuIOChannel * fu_io_channel_unix_new(gint fd); FuIOChannel * fu_io_channel_virtual_new(const gchar *name, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); FuIOChannel * fu_io_channel_new_file(const gchar *filename, FuIoChannelOpenFlag open_flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gint fu_io_channel_unix_get_fd(FuIOChannel *self) G_GNUC_NON_NULL(1); gboolean fu_io_channel_shutdown(FuIOChannel *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_io_channel_seek(FuIOChannel *self, gsize offset, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_io_channel_write_raw(FuIOChannel *self, const guint8 *data, gsize datasz, guint timeout_ms, FuIOChannelFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_io_channel_read_raw(FuIOChannel *self, guint8 *buf, gsize bufsz, gsize *bytes_read, guint timeout_ms, FuIOChannelFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_io_channel_write_bytes(FuIOChannel *self, GBytes *bytes, guint timeout_ms, FuIOChannelFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_io_channel_write_stream(FuIOChannel *self, GInputStream *stream, guint timeout_ms, FuIOChannelFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_io_channel_write_byte_array(FuIOChannel *self, GByteArray *buf, guint timeout_ms, FuIOChannelFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); GBytes * fu_io_channel_read_bytes(FuIOChannel *self, gssize count, guint timeout_ms, FuIOChannelFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); GByteArray * fu_io_channel_read_byte_array(FuIOChannel *self, gssize count, guint timeout_ms, FuIOChannelFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-io-channel.rs000066400000000000000000000003711501337203100207410ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToBitString)] enum FuIoChannelOpenFlag { None = 0, Read = 1 << 0, Write = 1 << 1, Nonblock = 1 << 2, Sync = 1 << 3, } fwupd-2.0.10/libfwupdplugin/fu-ioctl-private.h000066400000000000000000000003671501337203100213160ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-ioctl.h" #include "fu-udev-device.h" FuIoctl * fu_ioctl_new(FuUdevDevice *udev_device) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-ioctl.c000066400000000000000000000223751501337203100176440ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuIoctl" #include "config.h" #include "fu-ioctl-private.h" #include "fu-udev-device-private.h" struct _FuIoctl { GObject parent_instance; FuUdevDevice *udev_device; /* ref */ GString *event_id; GPtrArray *fixups; /* of FuIoctlFixup */ }; G_DEFINE_TYPE(FuIoctl, fu_ioctl, G_TYPE_OBJECT) typedef struct { gchar *key; gboolean is_mutable; guint8 *buf; gsize bufsz; FuIoctlFixupFunc fixup_cb; } FuIoctlFixup; static void fu_ioctl_fixup_free(FuIoctlFixup *fixup) { g_free(fixup->key); g_free(fixup); } static gchar * fu_ioctl_fixup_build_key(FuIoctlFixup *fixup, const gchar *suffix) { return g_strdup_printf("%s%s", fixup->key != NULL ? fixup->key : "", suffix); } /** * fu_ioctl_set_name: * @self: a #FuIoctl * @name: (nullable): a string, e.g. `Nvme` * * Adds a name for the ioctl, preserving compatibility with existing emulation data. * * NOTE: For new devices this is not required. * * Since: 2.0.2 **/ void fu_ioctl_set_name(FuIoctl *self, const gchar *name) { g_return_if_fail(FU_IS_IOCTL(self)); g_string_truncate(self->event_id, 0); g_string_append_printf(self->event_id, "%sIoctl:", name != NULL ? name : ""); } /* private */ FuIoctl * fu_ioctl_new(FuUdevDevice *udev_device) { g_autoptr(FuIoctl) self = g_object_new(FU_TYPE_IOCTL, NULL); g_return_val_if_fail(FU_IS_UDEV_DEVICE(udev_device), NULL); self->udev_device = g_object_ref(udev_device); return g_steal_pointer(&self); } static void fu_ioctl_append_key(GString *event_id, const gchar *key, const gchar *value) { if (event_id->len > 0 && !g_str_has_suffix(event_id->str, ":")) g_string_append_c(event_id, ','); g_string_append_printf(event_id, "%s=%s", key, value); } static void fu_ioctl_append_key_as_u8(GString *event_id, const gchar *key, gsize value) { g_autofree gchar *value2 = g_strdup_printf("0x%02x", (guint)value); fu_ioctl_append_key(event_id, key, value2); } static void fu_ioctl_append_key_as_u16(GString *event_id, const gchar *key, gsize value) { g_autofree gchar *value2 = g_strdup_printf("0x%04x", (guint)value); fu_ioctl_append_key(event_id, key, value2); } static void fu_ioctl_append_key_from_buf(GString *event_id, const gchar *key, const guint8 *buf, gsize bufsz) { g_autofree gchar *key_data = g_strdup_printf("%sData", key != NULL ? key : ""); g_autofree gchar *value_data = g_base64_encode(buf, bufsz); g_autofree gchar *key_length = g_strdup_printf("%sLength", key != NULL ? key : ""); g_autofree gchar *value_length = g_strdup_printf("0x%x", (guint)bufsz); fu_ioctl_append_key(event_id, key_data, value_data); fu_ioctl_append_key(event_id, key_length, value_length); } /** * fu_ioctl_add_key_as_u8: * @self: a #FuIoctl * @key: a string, e.g. `Opcode` * @value: a integer value * * Adds a key for the emulation, formatting it as `0x%02x`. * * Since: 2.0.2 **/ void fu_ioctl_add_key_as_u8(FuIoctl *self, const gchar *key, gsize value) { g_return_if_fail(FU_IS_IOCTL(self)); g_return_if_fail(key != NULL); fu_ioctl_append_key_as_u8(self->event_id, key, value); } /** * fu_ioctl_add_key_as_u16: * @self: a #FuIoctl * @key: a string, e.g. `Opcode` * @value: a integer value * * Adds a key for the emulation, formatting it as `0x%04x`. * * Since: 2.0.2 **/ void fu_ioctl_add_key_as_u16(FuIoctl *self, const gchar *key, gsize value) { g_return_if_fail(FU_IS_IOCTL(self)); g_return_if_fail(key != NULL); fu_ioctl_append_key_as_u16(self->event_id, key, value); } static void fu_ioctl_add_buffer(FuIoctl *self, const gchar *key, guint8 *buf, gsize bufsz, gboolean is_mutable, FuIoctlFixupFunc fixup_cb) { fu_ioctl_append_key_from_buf(self->event_id, key, buf, bufsz); if (fixup_cb != NULL) { FuIoctlFixup *fixup = g_new0(FuIoctlFixup, 1); fixup->key = g_strdup(key); fixup->is_mutable = is_mutable; fixup->buf = buf; fixup->bufsz = bufsz; fixup->fixup_cb = fixup_cb; g_ptr_array_add(self->fixups, fixup); } } /** * fu_ioctl_add_mutable_buffer: * @self: a #FuIoctl * @key: a string, e.g. `Cdb` * @buf: (nullable): an optional buffer * @bufsz: Size of @buf * @fixup_cb: (scope forever): a function to call on the structure * * Adds a mutable buffer that can be used to fix up the ioctl-defined structure with the buffer and * size, and adds a key for the emulation. * * Since: 2.0.2 **/ void fu_ioctl_add_mutable_buffer(FuIoctl *self, const gchar *key, guint8 *buf, gsize bufsz, FuIoctlFixupFunc fixup_cb) { fu_ioctl_add_buffer(self, key, buf, bufsz, TRUE, fixup_cb); } /** * fu_ioctl_add_const_buffer: * @self: a #FuIoctl * @key: a string, e.g. `Cdb` * @buf: (nullable): an optional buffer * @bufsz: Size of @buf * @fixup_cb: (scope forever): a function to call on the structure * * Adds a constant buffer that can be used to fix up the ioctl-defined structure with the buffer * and size, and adds a key for the emulation. * * Since: 2.0.2 **/ void fu_ioctl_add_const_buffer(FuIoctl *self, const gchar *key, const guint8 *buf, gsize bufsz, FuIoctlFixupFunc fixup_cb) { fu_ioctl_add_buffer(self, key, (guint8 *)buf, bufsz, FALSE, fixup_cb); } /** * fu_ioctl_execute: * @self: a #FuIoctl * @request: request number * @buf: a buffer to use, which *must* be large enough for the request * @bufsz: the size of @buf * @rc: (out) (nullable): the raw return value from the ioctl * @timeout: timeout in ms for the retry action, see %FU_IOCTL_FLAG_RETRY * @flags: some #FuIoctlFlags, e.g. %FU_IOCTL_FLAG_RETRY * @error: (nullable): optional return location for an error * * Executes the ioctl, emulating as required. Each fixup defined using fu_ioctl_add_mutable_buffer() * of fu_ioctl_add_const_buffer() is run before the ioctl is executed. * * If there are no fixups defined, the @buf is emulated, and so you must ensure that there are no * ioctl wrapper structures that use indirect pointer values. * * Returns: %TRUE for success * * Since: 2.0.2 **/ gboolean fu_ioctl_execute(FuIoctl *self, gulong request, gpointer buf, gsize bufsz, gint *rc, guint timeout, FuIoctlFlags flags, GError **error) { FuDeviceEvent *event = NULL; g_autoptr(GString) event_id = NULL; /* need event ID */ if (fu_device_has_flag(FU_DEVICE(self->udev_device), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self->udev_device)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_string_new(self->event_id->str); if (g_strcmp0(event_id->str, "Ioctl:") == 0) { fu_ioctl_append_key_as_u16(event_id, "Request", request); fu_ioctl_append_key_from_buf(event_id, NULL, buf, bufsz); } } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self->udev_device), FWUPD_DEVICE_FLAG_EMULATED)) { event = fu_device_load_event(FU_DEVICE(self->udev_device), event_id->str, error); if (event == NULL) return FALSE; if (self->fixups->len == 0) { if (!fu_device_event_copy_data(event, "DataOut", buf, bufsz, NULL, error)) return FALSE; } for (guint i = 0; i < self->fixups->len; i++) { FuIoctlFixup *fixup = g_ptr_array_index(self->fixups, i); g_autofree gchar *key = fu_ioctl_fixup_build_key(fixup, "DataOut"); if (!fixup->is_mutable) continue; if (!fu_device_event_copy_data(event, key, fixup->buf, fixup->bufsz, NULL, error)) return FALSE; } if (rc != NULL) { gint64 rc_tmp = fu_device_event_get_i64(event, "Rc", NULL); if (rc_tmp != G_MAXINT64) *rc = (gint)rc_tmp; } return TRUE; } /* save */ if (fu_context_has_flag(fu_device_get_context(FU_DEVICE(self->udev_device)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event = fu_device_save_event(FU_DEVICE(self->udev_device), event_id->str); } /* the buffer might be specified indirectly */ if (buf != NULL) { for (guint i = 0; i < self->fixups->len; i++) { FuIoctlFixup *fixup = g_ptr_array_index(self->fixups, i); if (!fixup->fixup_cb(self, buf, fixup->buf, fixup->bufsz, error)) return FALSE; } } if (!fu_udev_device_ioctl(self->udev_device, request, buf, bufsz, rc, timeout, flags, error)) return FALSE; /* save response */ if (event != NULL) { if (rc != NULL && *rc != 0) fu_device_event_set_i64(event, "Rc", *rc); if (self->fixups->len == 0) fu_device_event_set_data(event, "DataOut", buf, bufsz); for (guint i = 0; i < self->fixups->len; i++) { FuIoctlFixup *fixup = g_ptr_array_index(self->fixups, i); g_autofree gchar *key = fu_ioctl_fixup_build_key(fixup, "DataOut"); if (!fixup->is_mutable) continue; fu_device_event_set_data(event, key, fixup->buf, fixup->bufsz); } } /* success */ return TRUE; } static void fu_ioctl_init(FuIoctl *self) { self->event_id = g_string_new("Ioctl:"); self->fixups = g_ptr_array_new_with_free_func((GDestroyNotify)fu_ioctl_fixup_free); } static void fu_ioctl_finalize(GObject *object) { FuIoctl *self = FU_IOCTL(object); g_string_free(self->event_id, TRUE); g_ptr_array_unref(self->fixups); if (self->udev_device != NULL) g_object_unref(self->udev_device); G_OBJECT_CLASS(fu_ioctl_parent_class)->finalize(object); } static void fu_ioctl_class_init(FuIoctlClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_ioctl_finalize; } fwupd-2.0.10/libfwupdplugin/fu-ioctl.h000066400000000000000000000027661501337203100176530ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_IOCTL (fu_ioctl_get_type()) G_DECLARE_FINAL_TYPE(FuIoctl, fu_ioctl, FU, IOCTL, GObject) /** * FuIoctlFlags: * @FU_IOCTL_FLAG: No flags set * @FU_IOCTL_FLAG_RETRY: Retry the call on failure * * Flags used when calling fu_ioctl_execute() and fu_udev_device_ioctl(). **/ typedef enum { FU_IOCTL_FLAG_NONE = 0, FU_IOCTL_FLAG_RETRY = 1 << 0, /*< private >*/ FU_IOCTL_FLAG_LAST } FuIoctlFlags; typedef gboolean (*FuIoctlFixupFunc)(FuIoctl *self, gpointer ptr, guint8 *buf, gsize bufsz, GError **error) G_GNUC_WARN_UNUSED_RESULT; void fu_ioctl_set_name(FuIoctl *self, const gchar *name) G_GNUC_NON_NULL(1); void fu_ioctl_add_key_as_u8(FuIoctl *self, const gchar *key, gsize value) G_GNUC_NON_NULL(1, 2); void fu_ioctl_add_key_as_u16(FuIoctl *self, const gchar *key, gsize value) G_GNUC_NON_NULL(1, 2); void fu_ioctl_add_mutable_buffer(FuIoctl *self, const gchar *key, guint8 *buf, gsize bufsz, FuIoctlFixupFunc fixup_cb) G_GNUC_NON_NULL(1); void fu_ioctl_add_const_buffer(FuIoctl *self, const gchar *key, const guint8 *buf, gsize bufsz, FuIoctlFixupFunc fixup_cb) G_GNUC_NON_NULL(1); gboolean fu_ioctl_execute(FuIoctl *self, gulong request, gpointer buf, gsize bufsz, gint *rc, guint timeout, FuIoctlFlags flags, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-kenv.c000066400000000000000000000021271501337203100174660ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #ifdef HAVE_KENV_H #include #endif #include "fwupd-error.h" #include "fu-kenv.h" /** * fu_kenv_get_string: * @key: a kenv key, e.g. `smbios.bios.version` * @error: (nullable): optional return location for an error * * Gets a BSD kernel environment string. This will not work on Linux or * Windows. * * Returns: (transfer full): a string, or %NULL if the @key was not found * * Since: 1.6.1 **/ gchar * fu_kenv_get_string(const gchar *key, GError **error) { #ifdef HAVE_KENV_H gchar buf[128] = {'\0'}; g_return_val_if_fail(key != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); if (kenv(KENV_GET, key, buf, sizeof(buf)) == -1) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "cannot get kenv request for %s", key); return NULL; } return g_strndup(buf, sizeof(buf)); #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "kenv not supported"); return NULL; #endif } fwupd-2.0.10/libfwupdplugin/fu-kenv.h000066400000000000000000000003431501337203100174710ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include gchar * fu_kenv_get_string(const gchar *key, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-kernel-search-path-private.h000066400000000000000000000003351501337203100236540ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-kernel-search-path.h" gchar * fu_kernel_search_path_get_current(GError **error); fwupd-2.0.10/libfwupdplugin/fu-kernel-search-path.c000066400000000000000000000112171501337203100222000ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuKernel" #include "config.h" #include "fu-kernel-search-path-private.h" #include "fu-path.h" /** * FuKernelSearchPathLocker: * * Easily reset the firmware search path. */ struct _FuKernelSearchPathLocker { GObject parent_instance; gchar *path; gchar *old_path; }; G_DEFINE_TYPE(FuKernelSearchPathLocker, fu_kernel_search_path_locker, G_TYPE_OBJECT) /** * fu_kernel_search_path_locker_get_path: * @self: a #FuDfuFirmware * * Gets the kernel search path set using this locker. * * Returns: the path set with fu_kernel_search_path_locker_new() * * Since: 2.0.7 **/ const gchar * fu_kernel_search_path_locker_get_path(FuKernelSearchPathLocker *self) { g_return_val_if_fail(FU_IS_KERNEL_SEARCH_PATH_LOCKER(self), NULL); return self->path; } /* private */ gchar * fu_kernel_search_path_get_current(GError **error) { gsize sz = 0; g_autofree gchar *sys_fw_search_path = NULL; g_autofree gchar *contents = NULL; sys_fw_search_path = fu_path_from_kind(FU_PATH_KIND_FIRMWARE_SEARCH); if (!g_file_get_contents(sys_fw_search_path, &contents, &sz, error)) return NULL; /* sanity check */ if (contents == NULL || sz == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to get firmware search path from %s", sys_fw_search_path); return NULL; } /* remove newline character */ if (contents[sz - 1] == '\n') contents[sz - 1] = 0; g_debug("read firmware search path (%" G_GSIZE_FORMAT "): %s", sz, contents); return g_steal_pointer(&contents); } static gboolean fu_kernel_search_path_set_current(const gchar *path, GError **error) { g_autofree gchar *sys_fw_search_path_prm = NULL; g_return_val_if_fail(path != NULL, FALSE); g_return_val_if_fail(strlen(path) < PATH_MAX, FALSE); g_debug("writing firmware search path (%" G_GSIZE_FORMAT "): %s", strlen(path), path); sys_fw_search_path_prm = fu_path_from_kind(FU_PATH_KIND_FIRMWARE_SEARCH); return g_file_set_contents_full(sys_fw_search_path_prm, path, strlen(path), G_FILE_SET_CONTENTS_NONE, 0644, error); } static gboolean fu_kernel_search_path_locker_close(FuKernelSearchPathLocker *self, GError **error) { if (self->old_path == NULL) return TRUE; if (!fu_kernel_search_path_set_current(self->old_path, error)) return FALSE; g_clear_pointer(&self->old_path, g_free); return TRUE; } /** * fu_kernel_search_path_locker_new: * @path: the new devivce path * @error: (nullable): optional return location for an error * * Sets the kernel firmware search path. When the #FuKernelSearchPathLocker is deallocated path * is restored to the previous value. * * This object is typically called using g_autoptr() but the device can also be * manually closed using g_clear_object(). * * Returns: (transfer full): a #FuKernelSearchPathLocker, or %NULL on error * * Since: 2.0.6 **/ FuKernelSearchPathLocker * fu_kernel_search_path_locker_new(const gchar *path, GError **error) { g_autofree gchar *old_path = NULL; g_autoptr(FuKernelSearchPathLocker) self = NULL; g_return_val_if_fail(path != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* create object */ self = g_object_new(FU_TYPE_KERNEL_SEARCH_PATH_LOCKER, NULL); self->path = g_strdup(path); old_path = fu_kernel_search_path_get_current(error); if (old_path == NULL) return NULL; /* set the new path if different */ if (g_strcmp0(self->old_path, path) != 0) { self->old_path = g_steal_pointer(&old_path); if (!fu_kernel_search_path_set_current(path, error)) return NULL; } /* success */ return g_steal_pointer(&self); } static void fu_kernel_search_path_locker_dispose(GObject *obj) { FuKernelSearchPathLocker *self = FU_KERNEL_SEARCH_PATH_LOCKER(obj); if (self->old_path != NULL) { g_autoptr(GError) error = NULL; if (!fu_kernel_search_path_locker_close(self, &error)) g_warning("failed to restore path: %s", error->message); } G_OBJECT_CLASS(fu_kernel_search_path_locker_parent_class)->dispose(obj); } static void fu_kernel_search_path_locker_finalize(GObject *obj) { FuKernelSearchPathLocker *self = FU_KERNEL_SEARCH_PATH_LOCKER(obj); g_free(self->path); g_free(self->old_path); G_OBJECT_CLASS(fu_kernel_search_path_locker_parent_class)->finalize(obj); } static void fu_kernel_search_path_locker_class_init(FuKernelSearchPathLockerClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->dispose = fu_kernel_search_path_locker_dispose; object_class->finalize = fu_kernel_search_path_locker_finalize; } static void fu_kernel_search_path_locker_init(FuKernelSearchPathLocker *self) { } fwupd-2.0.10/libfwupdplugin/fu-kernel-search-path.h000066400000000000000000000011621501337203100222030ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_KERNEL_SEARCH_PATH_LOCKER (fu_kernel_search_path_locker_get_type()) G_DECLARE_FINAL_TYPE(FuKernelSearchPathLocker, fu_kernel_search_path_locker, FU, KERNEL_SEARCH_PATH_LOCKER, GObject) FuKernelSearchPathLocker * fu_kernel_search_path_locker_new(const gchar *path, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); const gchar * fu_kernel_search_path_locker_get_path(FuKernelSearchPathLocker *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-kernel.c000066400000000000000000000274211501337203100200070ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuCommon" #include "config.h" #include #include #ifdef HAVE_UTSNAME_H #include #endif #include "fu-common.h" #include "fu-input-stream.h" #include "fu-kernel.h" #include "fu-path.h" #include "fu-string.h" #include "fu-version-common.h" /** * fu_kernel_locked_down: * * Determines if kernel lockdown in effect * * Since: 1.8.2 **/ gboolean fu_kernel_locked_down(void) { #ifdef __linux__ gsize len = 0; g_autofree gchar *dir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR_SECURITY); g_autofree gchar *fname = g_build_filename(dir, "lockdown", NULL); g_autofree gchar *data = NULL; g_auto(GStrv) options = NULL; if (!g_file_test(fname, G_FILE_TEST_EXISTS)) return FALSE; if (!g_file_get_contents(fname, &data, &len, NULL)) return FALSE; if (len < 1) return FALSE; options = g_strsplit(data, " ", -1); for (guint i = 0; options[i] != NULL; i++) { if (g_strcmp0(options[i], "[none]") == 0) return FALSE; } return TRUE; #else return FALSE; #endif } /** * fu_kernel_check_version: * @minimum_kernel: (not nullable): The minimum kernel version to check against * @error: (nullable): optional return location for an error * * Determines if the system is running at least a certain required kernel version * * Since: 1.8.2 **/ gboolean fu_kernel_check_version(const gchar *minimum_kernel, GError **error) { #ifdef HAVE_UTSNAME_H struct utsname name_tmp; g_return_val_if_fail(error == NULL || *error == NULL, FALSE); g_return_val_if_fail(minimum_kernel != NULL, FALSE); memset(&name_tmp, 0, sizeof(struct utsname)); if (uname(&name_tmp) < 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to read kernel version"); return FALSE; } if (fu_version_compare(name_tmp.release, minimum_kernel, FWUPD_VERSION_FORMAT_TRIPLET) < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "kernel %s doesn't meet minimum %s", name_tmp.release, minimum_kernel); return FALSE; } return TRUE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "platform doesn't support checking for minimum Linux kernel"); return FALSE; #endif } typedef struct { GHashTable *hash; GHashTable *values; } FuKernelConfigHelper; static gboolean fu_kernel_parse_config_line_cb(GString *token, guint token_idx, gpointer user_data, GError **error) { g_auto(GStrv) kv = NULL; FuKernelConfigHelper *helper = (FuKernelConfigHelper *)user_data; GRefString *value; if (token->len == 0) return TRUE; if (token->str[0] == '#') return TRUE; kv = g_strsplit(token->str, "=", 2); if (g_strv_length(kv) != 2) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid format for '%s'", token->str); return FALSE; } value = g_hash_table_lookup(helper->values, kv[1]); if (value != NULL) { g_hash_table_insert(helper->hash, g_strdup(kv[0]), g_ref_string_acquire(value)); } else { g_hash_table_insert(helper->hash, g_strdup(kv[0]), g_ref_string_new(kv[1])); } return TRUE; } /** * fu_kernel_parse_config: * @buf: (not nullable): cmdline to parse * @bufsz: size of @bufsz * * Parses all the kernel options into a hash table. Commented out options are not included. * * Returns: (transfer container) (element-type utf8 utf8): config keys * * Since: 1.9.6 **/ GHashTable * fu_kernel_parse_config(const gchar *buf, gsize bufsz, GError **error) { g_autoptr(GHashTable) hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_ref_string_release); g_autoptr(GHashTable) values = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, (GDestroyNotify)g_ref_string_release); FuKernelConfigHelper helper = {.hash = hash, .values = values}; const gchar *value_keys[] = {"y", "m", "0", NULL}; g_return_val_if_fail(buf != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* add 99.9% of the most common keys to avoid thousands of small allocations */ for (guint i = 0; value_keys[i] != NULL; i++) { g_hash_table_insert(values, (gpointer)value_keys[i], g_ref_string_new(value_keys[i])); } if (!fu_strsplit_full(buf, bufsz, "\n", fu_kernel_parse_config_line_cb, &helper, error)) return NULL; return g_steal_pointer(&hash); } #ifdef __linux__ static gchar * fu_kernel_get_config_path(GError **error) { #ifdef HAVE_UTSNAME_H struct utsname name_tmp; g_autofree gchar *config_fn = NULL; g_autofree gchar *bootdir = fu_path_from_kind(FU_PATH_KIND_HOSTFS_BOOT); memset(&name_tmp, 0, sizeof(struct utsname)); if (uname(&name_tmp) < 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to read kernel version"); return NULL; } config_fn = g_strdup_printf("config-%s", name_tmp.release); return g_build_filename(bootdir, config_fn, NULL); #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "platform does not support uname"); return NULL; #endif } #endif /** * fu_kernel_get_config: * @error: (nullable): optional return location for an error * * Loads all the kernel options into a hash table. Commented out options are not included. * * Returns: (transfer container) (element-type utf8 utf8): options from the kernel * * Since: 1.8.5 **/ GHashTable * fu_kernel_get_config(GError **error) { #ifdef __linux__ gsize bufsz = 0; g_autofree gchar *buf = NULL; g_autofree gchar *fn = NULL; g_autofree gchar *procdir = fu_path_from_kind(FU_PATH_KIND_PROCFS); g_autofree gchar *config_fngz = g_build_filename(procdir, "config.gz", NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* try /proc/config.gz -- which will only work with CONFIG_IKCONFIG */ if (g_file_test(config_fngz, G_FILE_TEST_EXISTS)) { g_autoptr(GBytes) payload = NULL; g_autoptr(GConverter) conv = NULL; g_autoptr(GFile) file = g_file_new_for_path(config_fngz); g_autoptr(GInputStream) istream1 = NULL; g_autoptr(GInputStream) istream2 = NULL; istream1 = G_INPUT_STREAM(g_file_read(file, NULL, error)); if (istream1 == NULL) return NULL; conv = G_CONVERTER(g_zlib_decompressor_new(G_ZLIB_COMPRESSOR_FORMAT_GZIP)); istream2 = g_converter_input_stream_new(istream1, conv); payload = fu_input_stream_read_bytes(istream2, 0, G_MAXSIZE, NULL, error); if (payload == NULL) return NULL; return fu_kernel_parse_config(g_bytes_get_data(payload, NULL), g_bytes_get_size(payload), error); } /* fall back to /boot/config-$(uname -r) */ fn = fu_kernel_get_config_path(error); if (fn == NULL) return NULL; if (!g_file_get_contents(fn, &buf, &bufsz, error)) return NULL; return fu_kernel_parse_config(buf, bufsz, error); #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "platform does not support getting the kernel config"); return NULL; #endif } /** * fu_kernel_parse_cmdline: * @buf: (not nullable): cmdline to parse * @bufsz: size of @bufsz * * Parses all the kernel key/values into a hash table, respecting double quotes when required. * * Returns: (transfer container) (element-type utf8 utf8): keys from the cmdline * * Since: 1.9.1 **/ GHashTable * fu_kernel_parse_cmdline(const gchar *buf, gsize bufsz) { gboolean is_escape = FALSE; g_autoptr(GHashTable) hash = NULL; g_autoptr(GString) acc = g_string_new(NULL); g_return_val_if_fail(buf != NULL, NULL); hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); if (bufsz == 0) return g_steal_pointer(&hash); for (gsize i = 0; i < bufsz; i++) { if (!is_escape && (buf[i] == ' ' || buf[i] == '\n') && acc->len > 0) { g_auto(GStrv) kv = g_strsplit(acc->str, "=", 2); g_hash_table_insert(hash, g_strdup(kv[0]), g_strdup(kv[1])); g_string_set_size(acc, 0); continue; } if (buf[i] == '"') { is_escape = !is_escape; continue; } g_string_append_c(acc, buf[i]); } if (acc->len > 0) { g_auto(GStrv) kv = g_strsplit(acc->str, "=", 2); g_hash_table_insert(hash, g_strdup(kv[0]), g_strdup(kv[1])); } /* success */ return g_steal_pointer(&hash); } /** * fu_kernel_get_cmdline: * @error: (nullable): optional return location for an error * * Loads all the kernel /proc/cmdline key/values into a hash table. * * Returns: (transfer container) (element-type utf8 utf8): keys from the kernel command line * * Since: 1.8.5 **/ GHashTable * fu_kernel_get_cmdline(GError **error) { #ifdef __linux__ gsize bufsz = 0; g_autofree gchar *buf = NULL; g_return_val_if_fail(error == NULL || *error == NULL, NULL); if (!g_file_get_contents("/proc/cmdline", &buf, &bufsz, error)) return NULL; return fu_kernel_parse_cmdline(buf, bufsz); #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "platform does not support getting the kernel cmdline"); return NULL; #endif } gboolean fu_kernel_check_cmdline_mutable(GError **error) { g_autofree gchar *bootdir = fu_path_from_kind(FU_PATH_KIND_HOSTFS_BOOT); g_autofree gchar *grubby_path = NULL; g_autofree gchar *sysconfdir = fu_path_from_kind(FU_PATH_KIND_SYSCONFDIR); g_auto(GStrv) config_files = g_new0(gchar *, 3); /* not found */ grubby_path = fu_path_find_program("grubby", error); if (grubby_path == NULL) return FALSE; /* check all the config files are writable */ config_files[0] = g_build_filename(bootdir, "grub2", "grub.cfg", NULL); config_files[1] = g_build_filename(sysconfdir, "grub.cfg", NULL); for (guint i = 0; config_files[i] != NULL; i++) { g_autoptr(GFile) file = g_file_new_for_path(config_files[i]); g_autoptr(GFileInfo) info = NULL; g_autoptr(GError) error_local = NULL; if (!g_file_query_exists(file, NULL)) continue; info = g_file_query_info(file, G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE, G_FILE_QUERY_INFO_NONE, NULL, &error_local); if (info == NULL) { g_warning("failed to get info for %s: %s", config_files[i], error_local->message); continue; } if (!g_file_info_get_attribute_boolean(info, G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "%s is not writable", config_files[i]); return FALSE; } } /* success */ return TRUE; } static gboolean fu_kernel_set_commandline(const gchar *arg, gboolean enable, GError **error) { g_autofree gchar *output = NULL; g_autofree gchar *arg_string = NULL; g_autofree gchar *grubby_path = NULL; const gchar *argv_grubby[] = {"", "--update-kernel=DEFAULT", "", NULL}; grubby_path = fu_path_find_program("grubby", error); if (grubby_path == NULL) { g_prefix_error(error, "failed to find grubby: "); return FALSE; } if (enable) arg_string = g_strdup_printf("--args=%s", arg); else arg_string = g_strdup_printf("--remove-args=%s", arg); argv_grubby[0] = grubby_path; argv_grubby[2] = arg_string; return g_spawn_sync(NULL, (gchar **)argv_grubby, NULL, G_SPAWN_DEFAULT, NULL, NULL, &output, NULL, NULL, error); } /** * fu_kernel_add_cmdline_arg: * @arg: (not nullable): key to set * @error: (nullable): optional return location for an error * * Add a kernel command line argument. * * Returns: %TRUE if successful * * Since: 1.9.5 **/ gboolean fu_kernel_add_cmdline_arg(const gchar *arg, GError **error) { return fu_kernel_set_commandline(arg, TRUE, error); } /** * fu_kernel_remove_cmdline_arg: * @arg: (not nullable): key to set * @error: (nullable): optional return location for an error * * Remove a kernel command line argument. * * Returns: %TRUE if successful * * Since: 1.9.5 **/ gboolean fu_kernel_remove_cmdline_arg(const gchar *arg, GError **error) { return fu_kernel_set_commandline(arg, FALSE, error); } fwupd-2.0.10/libfwupdplugin/fu-kernel.h000066400000000000000000000014341501337203100200100ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include gboolean fu_kernel_locked_down(void); gboolean fu_kernel_check_version(const gchar *minimum_kernel, GError **error) G_GNUC_NON_NULL(1); GHashTable * fu_kernel_get_config(GError **error); GHashTable * fu_kernel_parse_config(const gchar *buf, gsize bufsz, GError **error); GHashTable * fu_kernel_get_cmdline(GError **error); GHashTable * fu_kernel_parse_cmdline(const gchar *buf, gsize bufsz) G_GNUC_NON_NULL(1); gboolean fu_kernel_check_cmdline_mutable(GError **error); gboolean fu_kernel_add_cmdline_arg(const gchar *arg, GError **error) G_GNUC_NON_NULL(1); gboolean fu_kernel_remove_cmdline_arg(const gchar *arg, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-linear-firmware.c000066400000000000000000000135611501337203100216130ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-input-stream.h" #include "fu-linear-firmware.h" #include "fu-partial-input-stream.h" /** * FuLinearFirmware: * * A firmware made up of concatenated blobs of a different firmware type. * * NOTE: All the child images will be of the specified `GType`. * * See also: [class@FuFirmware] */ typedef struct { GType image_gtype; } FuLinearFirmwarePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuLinearFirmware, fu_linear_firmware, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_linear_firmware_get_instance_private(o)) enum { PROP_0, PROP_IMAGE_GTYPE, PROP_LAST }; static void fu_linear_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuLinearFirmware *self = FU_LINEAR_FIRMWARE(firmware); FuLinearFirmwarePrivate *priv = GET_PRIVATE(self); fu_xmlb_builder_insert_kv(bn, "image_gtype", g_type_name(priv->image_gtype)); } /** * fu_linear_firmware_get_image_gtype: * @self: a #FuLinearFirmware * * Gets the image #GType to use when parsing a byte buffer. * * Returns: integer * * Since: 1.8.2 **/ GType fu_linear_firmware_get_image_gtype(FuLinearFirmware *self) { FuLinearFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_LINEAR_FIRMWARE(self), G_TYPE_INVALID); return priv->image_gtype; } static gboolean fu_linear_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { FuLinearFirmware *self = FU_LINEAR_FIRMWARE(firmware); FuLinearFirmwarePrivate *priv = GET_PRIVATE(self); const gchar *tmp; /* simple properties */ tmp = xb_node_query_text(n, "image_gtype", NULL); if (tmp != NULL) { priv->image_gtype = g_type_from_name(tmp); if (priv->image_gtype == G_TYPE_INVALID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "GType %s not registered", tmp); return FALSE; } } /* success */ return TRUE; } static gboolean fu_linear_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuLinearFirmware *self = FU_LINEAR_FIRMWARE(firmware); FuLinearFirmwarePrivate *priv = GET_PRIVATE(self); gsize offset = 0; gsize streamsz = 0; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; while (offset < streamsz) { g_autoptr(FuFirmware) img = g_object_new(priv->image_gtype, NULL); g_autoptr(GInputStream) stream_tmp = NULL; stream_tmp = fu_partial_input_stream_new(stream, offset, streamsz - offset, error); if (stream_tmp == NULL) { g_prefix_error(error, "failed to cut linear image: "); return FALSE; } if (!fu_firmware_parse_stream(img, stream_tmp, 0x0, flags | FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error)) { g_prefix_error(error, "failed to parse at 0x%x: ", (guint)offset); return FALSE; } fu_firmware_set_offset(firmware, offset); if (!fu_firmware_add_image_full(firmware, img, error)) return FALSE; /* next! */ offset += fu_firmware_get_size(img); } /* success */ return TRUE; } static GByteArray * fu_linear_firmware_write(FuFirmware *firmware, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GPtrArray) images = fu_firmware_get_images(firmware); /* add each file */ for (guint i = 0; i < images->len; i++) { FuFirmware *img = g_ptr_array_index(images, i); g_autoptr(GBytes) blob = NULL; fu_firmware_set_offset(img, buf->len); blob = fu_firmware_write(img, error); if (blob == NULL) return NULL; fu_byte_array_append_bytes(buf, blob); } /* success */ return g_steal_pointer(&buf); } static void fu_linear_firmware_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuLinearFirmware *self = FU_LINEAR_FIRMWARE(object); FuLinearFirmwarePrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_IMAGE_GTYPE: g_value_set_gtype(value, priv->image_gtype); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_linear_firmware_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuLinearFirmware *self = FU_LINEAR_FIRMWARE(object); FuLinearFirmwarePrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_IMAGE_GTYPE: priv->image_gtype = g_value_get_gtype(value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_linear_firmware_init(FuLinearFirmware *self) { fu_firmware_set_images_max(FU_FIRMWARE(self), 1024); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_NO_AUTO_DETECTION); } static void fu_linear_firmware_class_init(FuLinearFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); GParamSpec *pspec; object_class->get_property = fu_linear_firmware_get_property; object_class->set_property = fu_linear_firmware_set_property; firmware_class->parse = fu_linear_firmware_parse; firmware_class->write = fu_linear_firmware_write; firmware_class->export = fu_linear_firmware_export; firmware_class->build = fu_linear_firmware_build; /** * FuLinearFirmware:image-gtype: * * The image #GType * * Since: 1.8.2 */ pspec = g_param_spec_gtype("image-gtype", NULL, NULL, FU_TYPE_FIRMWARE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_IMAGE_GTYPE, pspec); } /** * fu_linear_firmware_new: * @image_gtype: a #GType, e.g. %FU_TYPE_OPROM_FIRMWARE * * Creates a new #FuFirmware made up of concatenated images. * * Since: 1.8.2 **/ FuFirmware * fu_linear_firmware_new(GType image_gtype) { return g_object_new(FU_TYPE_LINEAR_FIRMWARE, "image-gtype", image_gtype, NULL); } fwupd-2.0.10/libfwupdplugin/fu-linear-firmware.h000066400000000000000000000010111501337203100216030ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_LINEAR_FIRMWARE (fu_linear_firmware_get_type()) G_DECLARE_DERIVABLE_TYPE(FuLinearFirmware, fu_linear_firmware, FU, LINEAR_FIRMWARE, FuFirmware) struct _FuLinearFirmwareClass { FuFirmwareClass parent_class; }; FuFirmware * fu_linear_firmware_new(GType image_gtype); GType fu_linear_firmware_get_image_gtype(FuLinearFirmware *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-linux-efivars.c000066400000000000000000000312361501337203100213220ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2015 Peter Jones * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuLinuxEfivars" #include "config.h" #include #include #include #include #include #include #include #include "fwupd-error.h" #include "fu-common.h" #include "fu-linux-efivars.h" #include "fu-path.h" struct _FuLinuxEfivars { FuEfivars parent_instance; }; G_DEFINE_TYPE(FuLinuxEfivars, fu_linux_efivars, FU_TYPE_EFIVARS) static gchar * fu_linux_efivars_get_path(void) { g_autofree gchar *sysfsfwdir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR_FW); return g_build_filename(sysfsfwdir, "efi", "efivars", NULL); } static gchar * fu_linux_efivars_get_filename(const gchar *guid, const gchar *name) { g_autofree gchar *efivarsdir = fu_linux_efivars_get_path(); return g_strdup_printf("%s/%s-%s", efivarsdir, name, guid); } static gboolean fu_linux_efivars_supported(FuEfivars *efivars, GError **error) { g_autofree gchar *efivarsdir = fu_linux_efivars_get_path(); if (!g_file_test(efivarsdir, G_FILE_TEST_IS_DIR)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "kernel efivars support missing: %s", efivarsdir); return FALSE; } return TRUE; } static gboolean fu_linux_efivars_set_immutable_fd(int fd, gboolean value, gboolean *value_old, GError **error) { guint flags; gboolean is_immutable; int rc; /* get existing status */ rc = ioctl(fd, FS_IOC_GETFLAGS, &flags); /* nocheck:blocked */ if (rc < 0) { /* check for tmpfs */ if (errno == ENOTTY || errno == ENOSYS) { is_immutable = FALSE; } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to get flags: %s", g_strerror(errno)); return FALSE; } } else { is_immutable = (flags & FS_IMMUTABLE_FL) > 0; } /* save the old value */ if (value_old != NULL) *value_old = is_immutable; /* is this already correct */ if (value) { if (is_immutable) return TRUE; flags |= FS_IMMUTABLE_FL; } else { if (!is_immutable) return TRUE; flags &= ~FS_IMMUTABLE_FL; } /* set the new status */ rc = ioctl(fd, FS_IOC_SETFLAGS, &flags); /* nocheck:blocked */ if (rc < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to set flags: %s", g_strerror(errno)); return FALSE; } return TRUE; } static gboolean fu_linux_efivars_set_immutable(const gchar *fn, gboolean value, gboolean *value_old, GError **error) { gint fd; g_autoptr(GInputStream) istr = NULL; /* not bare-metal */ if (!g_str_has_prefix(fn, "/sys")) return TRUE; /* open file readonly */ fd = open(fn, O_RDONLY); if (fd < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "failed to open: %s", g_strerror(errno)); return FALSE; } istr = g_unix_input_stream_new(fd, TRUE); if (istr == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to create stream"); return FALSE; } return fu_linux_efivars_set_immutable_fd(fd, value, value_old, error); } static gboolean fu_linux_efivars_delete(FuEfivars *efivars, const gchar *guid, const gchar *name, GError **error) { g_autofree gchar *fn = NULL; g_autoptr(GFile) file = NULL; fn = fu_linux_efivars_get_filename(guid, name); file = g_file_new_for_path(fn); if (!g_file_query_exists(file, NULL)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no key to delete"); return FALSE; } if (!fu_linux_efivars_set_immutable(fn, FALSE, NULL, error)) { g_prefix_error(error, "failed to set %s as mutable: ", fn); return FALSE; } return g_file_delete(file, NULL, error); } static gboolean fu_linux_efivars_delete_with_glob(FuEfivars *efivars, const gchar *guid, const gchar *name_glob, GError **error) { const gchar *fn; g_autofree gchar *nameguid_glob = NULL; g_autofree gchar *efivarsdir = fu_linux_efivars_get_path(); g_autoptr(GDir) dir = NULL; dir = g_dir_open(efivarsdir, 0, error); if (dir == NULL) return FALSE; nameguid_glob = g_strdup_printf("%s-%s", name_glob, guid); while ((fn = g_dir_read_name(dir)) != NULL) { if (g_pattern_match_simple(nameguid_glob, fn)) { g_autofree gchar *keyfn = g_build_filename(efivarsdir, fn, NULL); g_autoptr(GFile) file = g_file_new_for_path(keyfn); if (!fu_linux_efivars_set_immutable(keyfn, FALSE, NULL, error)) { g_prefix_error(error, "failed to set %s as mutable: ", keyfn); return FALSE; } if (!g_file_delete(file, NULL, error)) return FALSE; } } return TRUE; } static gboolean fu_linux_efivars_exists_guid(const gchar *guid) { const gchar *fn; g_autofree gchar *efivarsdir = fu_linux_efivars_get_path(); g_autoptr(GDir) dir = NULL; dir = g_dir_open(efivarsdir, 0, NULL); if (dir == NULL) return FALSE; while ((fn = g_dir_read_name(dir)) != NULL) { if (g_str_has_suffix(fn, guid)) return TRUE; } return TRUE; } static gboolean fu_linux_efivars_exists(FuEfivars *efivars, const gchar *guid, const gchar *name) { g_autofree gchar *fn = NULL; /* any name */ if (name == NULL) return fu_linux_efivars_exists_guid(guid); fn = fu_linux_efivars_get_filename(guid, name); return g_file_test(fn, G_FILE_TEST_EXISTS); } static gboolean fu_linux_efivars_get_data(FuEfivars *efivars, const gchar *guid, const gchar *name, guint8 **data, gsize *data_sz, guint32 *attr, GError **error) { gssize attr_sz; gssize data_sz_tmp; guint32 attr_tmp; guint64 sz; g_autofree gchar *fn = NULL; g_autoptr(GFile) file = NULL; g_autoptr(GFileInfo) info = NULL; g_autoptr(GInputStream) istr = NULL; /* open file as stream */ fn = fu_linux_efivars_get_filename(guid, name); file = g_file_new_for_path(fn); istr = G_INPUT_STREAM(g_file_read(file, NULL, error)); if (istr == NULL) { fwupd_error_convert(error); return FALSE; } info = g_file_input_stream_query_info(G_FILE_INPUT_STREAM(istr), G_FILE_ATTRIBUTE_STANDARD_SIZE, NULL, error); if (info == NULL) { g_prefix_error(error, "failed to get stream info: "); fwupd_error_convert(error); return FALSE; } /* get total stream size */ sz = g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_STANDARD_SIZE); if (sz < 4) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "efivars file too small: %" G_GUINT64_FORMAT, sz); return FALSE; } /* read out the attributes */ attr_sz = g_input_stream_read(istr, &attr_tmp, sizeof(attr_tmp), NULL, error); if (attr_sz == -1) { g_prefix_error(error, "failed to read attr: "); fwupd_error_convert(error); return FALSE; } if (attr != NULL) *attr = attr_tmp; /* read out the data */ data_sz_tmp = sz - sizeof(attr_tmp); if (data_sz_tmp == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no data to read"); return FALSE; } if (data_sz != NULL) *data_sz = data_sz_tmp; if (data != NULL) { g_autofree guint8 *data_tmp = g_malloc0(data_sz_tmp); if (!g_input_stream_read_all(istr, data_tmp, data_sz_tmp, NULL, NULL, error)) { g_prefix_error(error, "failed to read data: "); return FALSE; } *data = g_steal_pointer(&data_tmp); } return TRUE; } static GPtrArray * fu_linux_efivars_get_names(FuEfivars *efivars, const gchar *guid, GError **error) { const gchar *name_guid; g_autofree gchar *path = fu_linux_efivars_get_path(); g_autoptr(GDir) dir = NULL; g_autoptr(GPtrArray) names = g_ptr_array_new_with_free_func(g_free); /* find names with matching GUID */ dir = g_dir_open(path, 0, error); if (dir == NULL) return NULL; while ((name_guid = g_dir_read_name(dir)) != NULL) { gsize name_guidsz = strlen(name_guid); if (name_guidsz < 38) continue; if (g_strcmp0(name_guid + name_guidsz - 36, guid) == 0) { g_ptr_array_add(names, g_strndup(name_guid, name_guidsz - 37)); } } /* nothing found */ if (names->len == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no names for GUID %s", guid); return NULL; } /* success */ return g_steal_pointer(&names); } static GFileMonitor * fu_linux_efivars_get_monitor(FuEfivars *efivars, const gchar *guid, const gchar *name, GError **error) { g_autofree gchar *fn = NULL; g_autoptr(GFile) file = NULL; g_autoptr(GFileMonitor) monitor = NULL; fn = fu_linux_efivars_get_filename(guid, name); file = g_file_new_for_path(fn); monitor = g_file_monitor_file(file, G_FILE_MONITOR_NONE, NULL, error); if (monitor == NULL) { fwupd_error_convert(error); return NULL; } g_file_monitor_set_rate_limit(monitor, 5000); return g_steal_pointer(&monitor); } static guint64 fu_linux_efivars_space_used(FuEfivars *efivars, GError **error) { const gchar *fn; guint64 total = 0; g_autoptr(GDir) dir = NULL; g_autofree gchar *path = fu_linux_efivars_get_path(); g_autoptr(GFile) file_fs = g_file_new_for_path(path); g_autoptr(GFileInfo) info_fs = NULL; g_autoptr(GError) error_local = NULL; /* this is only supported in new kernels */ info_fs = g_file_query_info(file_fs, G_FILE_ATTRIBUTE_FILESYSTEM_USED, G_FILE_QUERY_INFO_NONE, NULL, &error_local); if (info_fs == NULL) { g_debug("failed to get efivars used space: %s", error_local->message); } else { total = g_file_info_get_attribute_uint64(info_fs, G_FILE_ATTRIBUTE_FILESYSTEM_USED); if (total > 0) return total; } /* stat each file */ dir = g_dir_open(path, 0, error); if (dir == NULL) return G_MAXUINT64; while ((fn = g_dir_read_name(dir)) != NULL) { guint64 sz; g_autofree gchar *pathfn = g_build_filename(path, fn, NULL); g_autoptr(GFile) file = g_file_new_for_path(pathfn); g_autoptr(GFileInfo) info = NULL; info = g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_ALLOCATED_SIZE "," G_FILE_ATTRIBUTE_STANDARD_SIZE, G_FILE_QUERY_INFO_NONE, NULL, error); if (info == NULL) { fwupd_error_convert(error); return G_MAXUINT64; } sz = g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_STANDARD_ALLOCATED_SIZE); if (sz == 0) sz = g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_STANDARD_SIZE); total += sz; } /* success */ return total; } static gboolean fu_linux_efivars_set_data(FuEfivars *efivars, const gchar *guid, const gchar *name, const guint8 *data, gsize sz, guint32 attr, GError **error) { int fd; int open_wflags = O_WRONLY; gboolean was_immutable = TRUE; g_autofree gchar *fn = fu_linux_efivars_get_filename(guid, name); g_autofree guint8 *buf = g_malloc0(sizeof(guint32) + sz); g_autoptr(GOutputStream) ostr = NULL; /* clear the immutable bit before writing if required */ if (g_file_test(fn, G_FILE_TEST_EXISTS)) { if (!fu_linux_efivars_set_immutable(fn, FALSE, &was_immutable, error)) { g_prefix_error(error, "failed to set %s as mutable: ", fn); return FALSE; } } else { open_wflags |= O_CREAT; } /* open file for writing, optionally append */ if (attr & FU_EFIVARS_ATTR_APPEND_WRITE) open_wflags |= O_APPEND; fd = open(fn, open_wflags, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); if (fd < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to open %s: %s", fn, g_strerror(errno)); return FALSE; } ostr = g_unix_output_stream_new(fd, TRUE); memcpy(buf, &attr, sizeof(attr)); /* nocheck:blocked */ memcpy(buf + sizeof(attr), data, sz); /* nocheck:blocked */ if (g_output_stream_write(ostr, buf, sizeof(attr) + sz, NULL, error) < 0) { g_prefix_error(error, "failed to write data to efivarsfs: "); fwupd_error_convert(error); return FALSE; } /* set as immutable again */ if (was_immutable && !fu_linux_efivars_set_immutable(fn, TRUE, NULL, error)) { g_prefix_error(error, "failed to set %s as immutable: ", fn); return FALSE; } /* success */ return TRUE; } static void fu_linux_efivars_init(FuLinuxEfivars *self) { } static void fu_linux_efivars_class_init(FuLinuxEfivarsClass *klass) { FuEfivarsClass *efivars_class = FU_EFIVARS_CLASS(klass); efivars_class->supported = fu_linux_efivars_supported; efivars_class->space_used = fu_linux_efivars_space_used; efivars_class->exists = fu_linux_efivars_exists; efivars_class->get_monitor = fu_linux_efivars_get_monitor; efivars_class->get_data = fu_linux_efivars_get_data; efivars_class->set_data = fu_linux_efivars_set_data; efivars_class->delete = fu_linux_efivars_delete; efivars_class->delete_with_glob = fu_linux_efivars_delete_with_glob; efivars_class->get_names = fu_linux_efivars_get_names; } FuEfivars * fu_efivars_new(void) { return FU_EFIVARS(g_object_new(FU_TYPE_LINUX_EFIVARS, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-linux-efivars.h000066400000000000000000000004501501337203100213210ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-efivars.h" #define FU_TYPE_LINUX_EFIVARS (fu_linux_efivars_get_type()) G_DECLARE_FINAL_TYPE(FuLinuxEfivars, fu_linux_efivars, FU, LINUX_EFIVARS, FuEfivars) fwupd-2.0.10/libfwupdplugin/fu-lzma-common.c000066400000000000000000000055231501337203100207570ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-lzma-common.h" /** * fu_lzma_decompress_bytes: * @blob: data * @memlimit: decompression memory limit, in bytes * @error: (nullable): optional return location for an error * * Decompresses a LZMA stream. * * Returns: (transfer full): decompressed data * * Since: 2.0.7 **/ GBytes * fu_lzma_decompress_bytes(GBytes *blob, guint64 memlimit, GError **error) { const gsize tmpbufsz = 0x20000; lzma_ret rc; lzma_stream strm = LZMA_STREAM_INIT; g_autofree guint8 *tmpbuf = g_malloc0(tmpbufsz); g_autoptr(GByteArray) buf = g_byte_array_new(); strm.next_in = g_bytes_get_data(blob, NULL); strm.avail_in = g_bytes_get_size(blob); rc = lzma_auto_decoder(&strm, memlimit, LZMA_TELL_UNSUPPORTED_CHECK); if (rc != LZMA_OK) { lzma_end(&strm); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to set up LZMA decoder rc=%u", rc); return NULL; } do { strm.next_out = tmpbuf; strm.avail_out = tmpbufsz; rc = lzma_code(&strm, LZMA_RUN); if (rc != LZMA_OK && rc != LZMA_STREAM_END) break; g_byte_array_append(buf, tmpbuf, tmpbufsz - strm.avail_out); } while (rc == LZMA_OK); lzma_end(&strm); /* success */ if (rc != LZMA_OK && rc != LZMA_STREAM_END) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to decode LZMA data rc=%u", rc); return NULL; } return g_bytes_new(buf->data, buf->len); } /** * fu_lzma_compress_bytes: * @blob: data * @error: (nullable): optional return location for an error * * Compresses into a LZMA stream. * * Returns: (transfer full): compressed data * * Since: 1.9.8 **/ GBytes * fu_lzma_compress_bytes(GBytes *blob, GError **error) { const gsize tmpbufsz = 0x20000; lzma_ret rc; lzma_stream strm = LZMA_STREAM_INIT; g_autofree guint8 *tmpbuf = g_malloc0(tmpbufsz); g_autoptr(GByteArray) buf = g_byte_array_new(); strm.next_in = g_bytes_get_data(blob, NULL); strm.avail_in = g_bytes_get_size(blob); rc = lzma_easy_encoder(&strm, 9, LZMA_CHECK_CRC64); if (rc != LZMA_OK) { lzma_end(&strm); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to set up LZMA encoder rc=%u", rc); return NULL; } do { strm.next_out = tmpbuf; strm.avail_out = tmpbufsz; rc = lzma_code(&strm, LZMA_FINISH); if (rc != LZMA_OK && rc != LZMA_STREAM_END) break; g_byte_array_append(buf, tmpbuf, tmpbufsz - strm.avail_out); } while (rc == LZMA_OK); lzma_end(&strm); /* success */ if (rc != LZMA_OK && rc != LZMA_STREAM_END) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to encode LZMA data rc=%u", rc); return NULL; } return g_bytes_new(buf->data, buf->len); } fwupd-2.0.10/libfwupdplugin/fu-lzma-common.h000066400000000000000000000005121501337203100207550ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include GBytes * fu_lzma_decompress_bytes(GBytes *blob, guint64 memlimit, GError **error) G_GNUC_NON_NULL(1); GBytes * fu_lzma_compress_bytes(GBytes *blob, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-mei-device.c000066400000000000000000000256411501337203100205400ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuMeiDevice" #include "config.h" #include #ifdef HAVE_MEI_H #include #endif #ifdef HAVE_IOCTL_H #include #endif #include "fu-bytes.h" #include "fu-dump.h" #include "fu-mei-device.h" #include "fu-string.h" #define FU_MEI_DEVICE_IOCTL_TIMEOUT 5000 /* ms */ /** * FuMeiDevice * * The Intel proprietary Management Engine Interface. * * See also: #FuUdevDevice */ typedef struct { guint32 max_msg_length; guint8 protocol_version; gchar *uuid; } FuMeiDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuMeiDevice, fu_mei_device, FU_TYPE_UDEV_DEVICE) #define GET_PRIVATE(o) (fu_mei_device_get_instance_private(o)) static void fu_mei_device_to_string(FuDevice *device, guint idt, GString *str) { FuMeiDevice *self = FU_MEI_DEVICE(device); FuMeiDevicePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append(str, idt, "Uuid", priv->uuid); fwupd_codec_string_append_hex(str, idt, "MaxMsgLength", priv->max_msg_length); fwupd_codec_string_append_hex(str, idt, "ProtocolVer", priv->protocol_version); } #ifdef HAVE_MEI_H static gboolean fu_mei_device_set_uuid(FuMeiDevice *self, const gchar *uuid) { FuMeiDevicePrivate *priv = GET_PRIVATE(self); if (g_strcmp0(priv->uuid, uuid) == 0) return FALSE; g_free(priv->uuid); priv->uuid = g_strdup(uuid); return TRUE; } #endif static gboolean fu_mei_device_pci_probe(FuMeiDevice *self, GError **error) { g_autoptr(FuDevice) pci_donor = NULL; pci_donor = fu_device_get_backend_parent_with_subsystem(FU_DEVICE(self), "pci", error); if (pci_donor == NULL) return FALSE; if (!fu_device_probe(pci_donor, error)) return FALSE; fu_device_incorporate(FU_DEVICE(self), pci_donor, FU_DEVICE_INCORPORATE_FLAG_VENDOR_IDS | FU_DEVICE_INCORPORATE_FLAG_VID | FU_DEVICE_INCORPORATE_FLAG_PID | FU_DEVICE_INCORPORATE_FLAG_INSTANCE_KEYS | FU_DEVICE_INCORPORATE_FLAG_PHYSICAL_ID); /* success */ return TRUE; } static gboolean fu_mei_device_interfaces_probe(FuMeiDevice *self, GError **error) { gsize prefixlen; g_autofree gchar *prefix = NULL; g_autoptr(FuDevice) parent = NULL; g_autoptr(GPtrArray) attrs = NULL; /* all the interfaces are prefixed by the parent basename */ parent = fu_device_get_backend_parent(FU_DEVICE(self), error); if (parent == NULL) return FALSE; if (fu_device_get_backend_id(parent) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no parent backend-id"); return FALSE; } prefix = g_path_get_basename(fu_device_get_backend_id(parent)); prefixlen = strlen(prefix); /* add any instance IDs that match */ attrs = fu_udev_device_list_sysfs(FU_UDEV_DEVICE(parent), error); if (attrs == NULL) return FALSE; for (guint i = 0; i < attrs->len; i++) { const gchar *attr = g_ptr_array_index(attrs, i); if (g_str_has_prefix(attr, prefix)) { fu_device_add_instance_id_full(FU_DEVICE(self), attr + prefixlen + 1, FU_DEVICE_INSTANCE_FLAG_QUIRKS); } } /* success */ return TRUE; } static gboolean fu_mei_device_probe(FuDevice *device, GError **error) { FuMeiDevice *self = FU_MEI_DEVICE(device); /* copy the PCI-specific vendor */ if (!fu_mei_device_pci_probe(self, error)) return FALSE; /* add interfaces */ if (!fu_mei_device_interfaces_probe(self, error)) return FALSE; /* for quirk matches */ fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "PCI", "VEN", NULL); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "PCI", "VEN", "DEV", NULL); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "PCI", "DRIVER", NULL); /* success */ return TRUE; } static gchar * fu_mei_device_get_multiline_attr(FuMeiDevice *self, const gchar *attr, guint idx, GError **error) { g_auto(GStrv) lines = NULL; g_autoptr(GBytes) blob = NULL; /* load lines */ blob = fu_udev_device_read_sysfs_bytes(FU_UDEV_DEVICE(self), attr, -1, 500, error); if (blob == NULL) return NULL; lines = fu_strsplit_bytes(blob, "\n", -1); if (g_strv_length(lines) <= idx) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "requested line %u of %u", idx, g_strv_length(lines)); return NULL; } /* success */ return g_strdup(lines[idx]); } /** * fu_mei_device_get_fw_ver: * @self: a #FuMeiDevice * @idx: line index * @error: (nullable): optional return location for an error * * Gets the firmware version for a specific index. * * Returns: string value * * Since: 1.8.7 **/ gchar * fu_mei_device_get_fw_ver(FuMeiDevice *self, guint idx, GError **error) { g_return_val_if_fail(FU_IS_MEI_DEVICE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return fu_mei_device_get_multiline_attr(self, "fw_ver", idx, error); } /** * fu_mei_device_get_fw_status: * @self: a #FuMeiDevice * @idx: line index * @error: (nullable): optional return location for an error * * Gets the firmware status for a specific index. * * Returns: string value * * Since: 1.8.7 **/ gchar * fu_mei_device_get_fw_status(FuMeiDevice *self, guint idx, GError **error) { g_return_val_if_fail(FU_IS_MEI_DEVICE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return fu_mei_device_get_multiline_attr(self, "fw_status", idx, error); } /** * fu_mei_device_get_max_msg_length: * @self: a #FuMeiDevice * * Gets the maximum message length. * * Returns: integer * * Since: 1.8.2 **/ guint32 fu_mei_device_get_max_msg_length(FuMeiDevice *self) { FuMeiDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_MEI_DEVICE(self), G_MAXUINT32); return priv->max_msg_length; } /** * fu_mei_device_get_protocol_version: * @self: a #FuMeiDevice * * Gets the protocol version, or 0x for unset. * * Returns: integer * * Since: 1.8.2 **/ guint8 fu_mei_device_get_protocol_version(FuMeiDevice *self) { FuMeiDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_MEI_DEVICE(self), G_MAXUINT8); return priv->protocol_version; } /** * fu_mei_device_connect: * @self: a #FuMeiDevice * @uuid: interface UUID * @req_protocol_version: required protocol version, or 0 * @error: (nullable): optional return location for an error * * Connects to the MEI device. * * Returns: %TRUE for success * * Since: 1.8.2 **/ gboolean fu_mei_device_connect(FuMeiDevice *self, const gchar *uuid, guint8 req_protocol_version, GError **error) { #ifdef HAVE_MEI_H FuMeiDevicePrivate *priv = GET_PRIVATE(self); fwupd_guid_t guid_le = {0x0}; struct mei_client *cl; struct mei_connect_client_data data = {0x0}; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); g_return_val_if_fail(FU_IS_MEI_DEVICE(self), FALSE); g_return_val_if_fail(uuid != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* already using this UUID */ if (!fu_mei_device_set_uuid(self, uuid)) return TRUE; if (!fwupd_guid_from_string(priv->uuid, &guid_le, FWUPD_GUID_FLAG_MIXED_ENDIAN, error)) return FALSE; memcpy(&data.in_client_uuid, &guid_le, sizeof(guid_le)); /* nocheck:blocked */ g_debug("connecting to %s", priv->uuid); if (!fu_ioctl_execute(ioctl, IOCTL_MEI_CONNECT_CLIENT, (guint8 *)&data, sizeof(data), NULL, /* rc */ FU_MEI_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_NONE, error)) return FALSE; cl = &data.out_client_properties; if (req_protocol_version > 0 && cl->protocol_version != req_protocol_version) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Intel MEI protocol version not supported %i", cl->protocol_version); return FALSE; } /* success */ priv->max_msg_length = cl->max_msg_length; priv->protocol_version = cl->protocol_version; return TRUE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "linux/mei.h not supported"); return FALSE; #endif } /** * fu_mei_device_read: * @self: a #FuMeiDevice * @buf: (out): data * @bufsz: size of @data * @bytes_read: (nullable): bytes read * @timeout_ms: timeout * @error: (nullable): optional return location for an error * * Read raw bytes from the device. * * Returns: %TRUE for success * * Since: 1.8.2 **/ gboolean fu_mei_device_read(FuMeiDevice *self, guint8 *buf, gsize bufsz, gsize *bytes_read, guint timeout_ms, GError **error) { g_return_val_if_fail(FU_IS_MEI_DEVICE(self), FALSE); g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return fu_udev_device_read(FU_UDEV_DEVICE(self), buf, bufsz, bytes_read, timeout_ms, FU_IO_CHANNEL_FLAG_SINGLE_SHOT, error); } /** * fu_mei_device_write: * @self: a #FuMeiDevice * @buf: (out): data * @bufsz: size of @data * @timeout_ms: timeout * @error: (nullable): optional return location for an error * * Write raw bytes to the device. * * Returns: %TRUE for success * * Since: 1.8.2 **/ gboolean fu_mei_device_write(FuMeiDevice *self, const guint8 *buf, gsize bufsz, guint timeout_ms, GError **error) { g_return_val_if_fail(FU_IS_MEI_DEVICE(self), FALSE); g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return fu_udev_device_write(FU_UDEV_DEVICE(self), buf, bufsz, timeout_ms, FU_IO_CHANNEL_FLAG_SINGLE_SHOT, error); } static void fu_mei_device_incorporate(FuDevice *device, FuDevice *donor) { FuMeiDevice *self = FU_MEI_DEVICE(device); FuMeiDevicePrivate *priv = GET_PRIVATE(self); FuMeiDevicePrivate *priv_donor = GET_PRIVATE(FU_MEI_DEVICE(donor)); g_return_if_fail(FU_IS_MEI_DEVICE(self)); g_return_if_fail(FU_IS_MEI_DEVICE(donor)); /* copy private instance data */ priv->max_msg_length = priv_donor->max_msg_length; priv->protocol_version = priv_donor->protocol_version; } static void fu_mei_device_init(FuMeiDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); } static void fu_mei_device_finalize(GObject *object) { FuMeiDevice *self = FU_MEI_DEVICE(object); FuMeiDevicePrivate *priv = GET_PRIVATE(self); g_free(priv->uuid); G_OBJECT_CLASS(fu_mei_device_parent_class)->finalize(object); } static void fu_mei_device_class_init(FuMeiDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_mei_device_finalize; device_class->probe = fu_mei_device_probe; device_class->to_string = fu_mei_device_to_string; device_class->incorporate = fu_mei_device_incorporate; } fwupd-2.0.10/libfwupdplugin/fu-mei-device.h000066400000000000000000000022671501337203100205440ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-udev-device.h" #define FU_TYPE_MEI_DEVICE (fu_mei_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuMeiDevice, fu_mei_device, FU, MEI_DEVICE, FuUdevDevice) struct _FuMeiDeviceClass { FuUdevDeviceClass parent_class; }; gboolean fu_mei_device_connect(FuMeiDevice *self, const gchar *uuid, guint8 req_protocol_version, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_mei_device_read(FuMeiDevice *self, guint8 *buf, gsize bufsz, gsize *bytes_read, guint timeout_ms, GError **error) G_GNUC_NON_NULL(1); gboolean fu_mei_device_write(FuMeiDevice *self, const guint8 *buf, gsize bufsz, guint timeout_ms, GError **error) G_GNUC_NON_NULL(1); guint fu_mei_device_get_max_msg_length(FuMeiDevice *self) G_GNUC_NON_NULL(1); guint8 fu_mei_device_get_protocol_version(FuMeiDevice *self) G_GNUC_NON_NULL(1); gchar * fu_mei_device_get_fw_ver(FuMeiDevice *self, guint idx, GError **error) G_GNUC_NON_NULL(1); gchar * fu_mei_device_get_fw_status(FuMeiDevice *self, guint idx, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-mem-private.h000066400000000000000000000005011501337203100207500ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-mem.h" gboolean fu_memchk_read(gsize bufsz, gsize offset, gsize n, GError **error); gboolean fu_memchk_write(gsize bufsz, gsize offset, gsize n, GError **error); fwupd-2.0.10/libfwupdplugin/fu-mem.c000066400000000000000000000622011501337203100173000ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuCommon" #include "config.h" #include "fwupd-error.h" #include "fu-mem-private.h" #include "fu-string.h" /** * fu_memwrite_uint16: * @buf: a writable buffer * @val_native: a value in host byte-order * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * * Writes a value to a buffer using a specified endian. * * Since: 1.8.2 **/ void fu_memwrite_uint16(guint8 *buf, guint16 val_native, FuEndianType endian) { guint16 val_hw; switch (endian) { case G_BIG_ENDIAN: val_hw = GUINT16_TO_BE(val_native); /* nocheck:blocked */ break; case G_LITTLE_ENDIAN: val_hw = GUINT16_TO_LE(val_native); /* nocheck:blocked */ break; default: val_hw = val_native; break; } memcpy(buf, &val_hw, sizeof(val_hw)); /* nocheck:blocked */ } /** * fu_memwrite_uint24: * @buf: a writable buffer * @val_native: a value in host byte-order * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * * Writes a value to a buffer using a specified endian. * * Since: 1.8.2 **/ void fu_memwrite_uint24(guint8 *buf, guint32 val_native, FuEndianType endian) { guint32 val_hw; switch (endian) { case G_BIG_ENDIAN: val_hw = GUINT32_TO_BE(val_native); /* nocheck:blocked */ memcpy(buf, ((const guint8 *)&val_hw) + 0x1, 0x3); /* nocheck:blocked */ break; case G_LITTLE_ENDIAN: val_hw = GUINT32_TO_LE(val_native); /* nocheck:blocked */ memcpy(buf, &val_hw, 0x3); /* nocheck:blocked */ break; default: g_assert_not_reached(); } } /** * fu_memwrite_uint32: * @buf: a writable buffer * @val_native: a value in host byte-order * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * * Writes a value to a buffer using a specified endian. * * Since: 1.8.2 **/ void fu_memwrite_uint32(guint8 *buf, guint32 val_native, FuEndianType endian) { guint32 val_hw; switch (endian) { case G_BIG_ENDIAN: val_hw = GUINT32_TO_BE(val_native); /* nocheck:blocked */ break; case G_LITTLE_ENDIAN: val_hw = GUINT32_TO_LE(val_native); /* nocheck:blocked */ break; default: val_hw = val_native; break; } memcpy(buf, &val_hw, sizeof(val_hw)); /* nocheck:blocked */ } /** * fu_memwrite_uint64: * @buf: a writable buffer * @val_native: a value in host byte-order * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * * Writes a value to a buffer using a specified endian. * * Since: 1.8.2 **/ void fu_memwrite_uint64(guint8 *buf, guint64 val_native, FuEndianType endian) { guint64 val_hw; switch (endian) { case G_BIG_ENDIAN: val_hw = GUINT64_TO_BE(val_native); /* nocheck:blocked */ break; case G_LITTLE_ENDIAN: val_hw = GUINT64_TO_LE(val_native); /* nocheck:blocked */ break; default: val_hw = val_native; break; } memcpy(buf, &val_hw, sizeof(val_hw)); /* nocheck:blocked */ } /** * fu_memread_uint16: * @buf: a readable buffer * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * * Read a value from a buffer using a specified endian. * * Returns: a value in host byte-order * * Since: 1.8.2 **/ guint16 fu_memread_uint16(const guint8 *buf, FuEndianType endian) { guint16 val_hw, val_native; memcpy(&val_hw, buf, sizeof(val_hw)); /* nocheck:blocked */ switch (endian) { case G_BIG_ENDIAN: val_native = GUINT16_FROM_BE(val_hw); /* nocheck:blocked */ break; case G_LITTLE_ENDIAN: val_native = GUINT16_FROM_LE(val_hw); /* nocheck:blocked */ break; default: val_native = val_hw; break; } return val_native; } /** * fu_memread_uint24: * @buf: a readable buffer * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * * Read a value from a buffer using a specified endian. * * Returns: a value in host byte-order * * Since: 1.8.2 **/ guint32 fu_memread_uint24(const guint8 *buf, FuEndianType endian) { guint32 val_hw = 0; guint32 val_native; switch (endian) { case G_BIG_ENDIAN: memcpy(((guint8 *)&val_hw) + 0x1, buf, 0x3); /* nocheck:blocked */ val_native = GUINT32_FROM_BE(val_hw); /* nocheck:blocked */ break; case G_LITTLE_ENDIAN: memcpy(&val_hw, buf, 0x3); /* nocheck:blocked */ val_native = GUINT32_FROM_LE(val_hw); /* nocheck:blocked */ break; default: val_native = val_hw; break; } return val_native; } /** * fu_memread_uint32: * @buf: a readable buffer * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * * Read a value from a buffer using a specified endian. * * Returns: a value in host byte-order * * Since: 1.8.2 **/ guint32 fu_memread_uint32(const guint8 *buf, FuEndianType endian) { guint32 val_hw, val_native; memcpy(&val_hw, buf, sizeof(val_hw)); /* nocheck:blocked */ switch (endian) { case G_BIG_ENDIAN: val_native = GUINT32_FROM_BE(val_hw); /* nocheck:blocked */ break; case G_LITTLE_ENDIAN: val_native = GUINT32_FROM_LE(val_hw); /* nocheck:blocked */ break; default: val_native = val_hw; break; } return val_native; } /** * fu_memread_uint64: * @buf: a readable buffer * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * * Read a value from a buffer using a specified endian. * * Returns: a value in host byte-order * * Since: 1.8.2 **/ guint64 fu_memread_uint64(const guint8 *buf, FuEndianType endian) { guint64 val_hw, val_native; memcpy(&val_hw, buf, sizeof(val_hw)); /* nocheck:blocked */ switch (endian) { case G_BIG_ENDIAN: val_native = GUINT64_FROM_BE(val_hw); /* nocheck:blocked */ break; case G_LITTLE_ENDIAN: val_native = GUINT64_FROM_LE(val_hw); /* nocheck:blocked */ break; default: val_native = val_hw; break; } return val_native; } /** * fu_memcmp_safe: * @buf1: a buffer * @buf1_sz: sizeof @buf1 * @buf1_offset: offset into @buf1 * @buf2: another buffer * @buf2_sz: sizeof @buf2 * @buf2_offset: offset into @buf1 * @n: number of bytes to compare from @buf1+@buf1_offset from * @error: (nullable): optional return location for an error * * Compares the buffers for equality. * * Returns: %TRUE if @buf1 and @buf2 are identical * * Since: 1.8.2 **/ gboolean fu_memcmp_safe(const guint8 *buf1, gsize buf1_sz, gsize buf1_offset, const guint8 *buf2, gsize buf2_sz, gsize buf2_offset, gsize n, GError **error) { g_return_val_if_fail(buf1 != NULL, FALSE); g_return_val_if_fail(buf2 != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_memchk_read(buf1_sz, buf1_offset, n, error)) return FALSE; if (!fu_memchk_read(buf2_sz, buf2_offset, n, error)) return FALSE; /* check matches */ for (guint i = 0x0; i < n; i++) { if (buf1[buf1_offset + i] != buf2[buf2_offset + i]) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "got 0x%02x, expected 0x%02x @ 0x%04x", buf1[buf1_offset + i], buf2[buf2_offset + i], i); return FALSE; } } /* success */ return TRUE; } /** * fu_memchk_read: * @bufsz: maximum size of a buffer, typically `sizeof(buf)` * @offset: offset in bytes * @n: number of bytes * @error: (nullable): optional return location for an error * * Works out if reading from a buffer is safe. Providing the buffer sizes allows us to check for * buffer overflow. * * You don't need to use this function in "obviously correct" cases, nor should * you use it when performance is a concern. Only us it when you're not sure if * malicious data from a device or firmware could cause memory corruption. * * Returns: %TRUE if the access is safe, %FALSE otherwise * * Since: 1.9.1 **/ gboolean fu_memchk_read(gsize bufsz, gsize offset, gsize n, GError **error) { g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (n == 0) return TRUE; if (n > bufsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "attempted to read 0x%02x bytes from buffer of 0x%02x", (guint)n, (guint)bufsz); return FALSE; } if (offset > bufsz || n + offset > bufsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "attempted to read 0x%02x bytes at offset 0x%02x from buffer of 0x%02x", (guint)n, (guint)offset, (guint)bufsz); return FALSE; } return TRUE; } /** * fu_memchk_write: * @bufsz: maximum size of a buffer, typically `sizeof(buf)` * @offset: offset in bytes * @n: number of bytes * @error: (nullable): optional return location for an error * * Works out if writing to a buffer is safe. Providing the buffer sizes allows us to check for * buffer overflow. * * You don't need to use this function in "obviously correct" cases, nor should * you use it when performance is a concern. Only us it when you're not sure if * malicious data from a device or firmware could cause memory corruption. * * Returns: %TRUE if the access is safe, %FALSE otherwise * * Since: 1.9.1 **/ gboolean fu_memchk_write(gsize bufsz, gsize offset, gsize n, GError **error) { g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (n == 0) return TRUE; if (n > bufsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "attempted to write 0x%02x bytes to buffer of 0x%02x", (guint)n, (guint)bufsz); return FALSE; } if (offset > bufsz || n + offset > bufsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "attempted to write 0x%02x bytes at offset 0x%02x to buffer of 0x%02x", (guint)n, (guint)offset, (guint)bufsz); return FALSE; } return TRUE; } /** * fu_memcpy_safe: * @dst: destination buffer * @dst_sz: maximum size of @dst, typically `sizeof(dst)` * @dst_offset: offset in bytes into @dst to copy to * @src: source buffer * @src_sz: maximum size of @dst, typically `sizeof(src)` * @src_offset: offset in bytes into @src to copy from * @n: number of bytes to copy from @src+@offset from * @error: (nullable): optional return location for an error * * Copies some memory using memcpy in a safe way. Providing the buffer sizes * of both the destination and the source allows us to check for buffer overflow. * * Providing the buffer offsets also allows us to check reading past the end of * the source buffer. For this reason the caller should NEVER add an offset to * @src or @dst. * * You don't need to use this function in "obviously correct" cases, nor should * you use it when performance is a concern. Only us it when you're not sure if * malicious data from a device or firmware could cause memory corruption. * * Returns: %TRUE if the bytes were copied, %FALSE otherwise * * Since: 1.8.2 **/ gboolean fu_memcpy_safe(guint8 *dst, gsize dst_sz, gsize dst_offset, const guint8 *src, gsize src_sz, gsize src_offset, gsize n, GError **error) { g_return_val_if_fail(dst != NULL, FALSE); g_return_val_if_fail(src != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_memchk_read(src_sz, src_offset, n, error)) return FALSE; if (!fu_memchk_write(dst_sz, dst_offset, n, error)) return FALSE; memcpy(dst + dst_offset, src + src_offset, n); /* nocheck:blocked */ return TRUE; } /** * fu_memmem_safe: * @haystack: destination buffer * @haystack_sz: maximum size of @haystack, typically `sizeof(haystack)` * @needle: source buffer * @needle_sz: maximum size of @haystack, typically `sizeof(needle)` * @offset: (out) (nullable): offset in bytes @needle has been found in @haystack * @error: (nullable): optional return location for an error * * Finds a block of memory in another block of memory in a safe way. * * Returns: %TRUE if the needle was found in the haystack, %FALSE otherwise * * Since: 1.8.2 **/ gboolean fu_memmem_safe(const guint8 *haystack, gsize haystack_sz, const guint8 *needle, gsize needle_sz, gsize *offset, GError **error) { #ifdef HAVE_MEMMEM const guint8 *tmp; #endif g_return_val_if_fail(haystack != NULL, FALSE); g_return_val_if_fail(needle != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* nothing to find */ if (needle_sz == 0) { if (offset != NULL) *offset = 0; return TRUE; } /* impossible */ if (needle_sz > haystack_sz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "needle of 0x%02x bytes is larger than haystack of 0x%02x bytes", (guint)needle_sz, (guint)haystack_sz); return FALSE; } #ifdef HAVE_MEMMEM /* trust glibc to do a binary or linear search as appropriate */ tmp = memmem(haystack, haystack_sz, needle, needle_sz); if (tmp != NULL) { if (offset != NULL) *offset = tmp - haystack; return TRUE; } #else for (gsize i = 0; i < haystack_sz - needle_sz; i++) { if (memcmp(haystack + i, needle, needle_sz) == 0) { if (offset != NULL) *offset = i; return TRUE; } } #endif /* not found */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "needle of 0x%02x bytes was not found in haystack of 0x%02x bytes", (guint)needle_sz, (guint)haystack_sz); return FALSE; } /** * fu_memdup_safe: * @src: (nullable): source buffer * @n: number of bytes to copy from @src * @error: (nullable): optional return location for an error * * Duplicates some memory using memdup in a safe way. * * You don't need to use this function in "obviously correct" cases, nor should * you use it when performance is a concern. Only us it when you're not sure if * malicious data from a device or firmware could cause memory corruption. * * NOTE: This function intentionally limits allocation size to 1GB. * * Returns: (transfer full): block of allocated memory, or %NULL for an error. * * Since: 1.8.2 **/ guint8 * fu_memdup_safe(const guint8 *src, gsize n, GError **error) { /* sanity check */ if (n > 0x40000000) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot allocate %uGB of memory", (guint)(n / 0x40000000)); return NULL; } /* linear block of memory */ return g_memdup2(src, n); } /** * fu_memread_uint8_safe: * @buf: source buffer * @bufsz: maximum size of @buf, typically `sizeof(buf)` * @offset: offset in bytes into @buf to copy from * @value: (out) (nullable): the parsed value * @error: (nullable): optional return location for an error * * Read a value from a buffer in a safe way. * * You don't need to use this function in "obviously correct" cases, nor should * you use it when performance is a concern. Only us it when you're not sure if * malicious data from a device or firmware could cause memory corruption. * * Returns: %TRUE if @value was set, %FALSE otherwise * * Since: 1.8.2 **/ gboolean fu_memread_uint8_safe(const guint8 *buf, gsize bufsz, gsize offset, guint8 *value, GError **error) { guint8 tmp; g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_memcpy_safe(&tmp, sizeof(tmp), 0x0, /* dst */ buf, bufsz, offset, /* src */ sizeof(tmp), error)) return FALSE; if (value != NULL) *value = tmp; return TRUE; } /** * fu_memread_uint16_safe: * @buf: source buffer * @bufsz: maximum size of @buf, typically `sizeof(buf)` * @offset: offset in bytes into @buf to copy from * @value: (out) (nullable): the parsed value * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * @error: (nullable): optional return location for an error * * Read a value from a buffer using a specified endian in a safe way. * * You don't need to use this function in "obviously correct" cases, nor should * you use it when performance is a concern. Only us it when you're not sure if * malicious data from a device or firmware could cause memory corruption. * * Returns: %TRUE if @value was set, %FALSE otherwise * * Since: 1.8.2 **/ gboolean fu_memread_uint16_safe(const guint8 *buf, gsize bufsz, gsize offset, guint16 *value, FuEndianType endian, GError **error) { guint8 dst[2] = {0x0}; g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_memcpy_safe(dst, sizeof(dst), 0x0, /* dst */ buf, bufsz, offset, /* src */ sizeof(dst), error)) return FALSE; if (value != NULL) *value = fu_memread_uint16(dst, endian); return TRUE; } /** * fu_memread_uint24_safe: * @buf: source buffer * @bufsz: maximum size of @buf, typically `sizeof(buf)` * @offset: offset in bytes into @buf to copy from * @value: (out) (nullable): the parsed value * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * @error: (nullable): optional return location for an error * * Read a value from a buffer using a specified endian in a safe way. * * You don't need to use this function in "obviously correct" cases, nor should * you use it when performance is a concern. Only us it when you're not sure if * malicious data from a device or firmware could cause memory corruption. * * Returns: %TRUE if @value was set, %FALSE otherwise * * Since: 1.8.3 **/ gboolean fu_memread_uint24_safe(const guint8 *buf, gsize bufsz, gsize offset, guint32 *value, FuEndianType endian, GError **error) { guint8 dst[3] = {0x0}; g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_memcpy_safe(dst, sizeof(dst), 0x0, /* dst */ buf, bufsz, offset, /* src */ sizeof(dst), error)) return FALSE; if (value != NULL) *value = fu_memread_uint24(dst, endian); return TRUE; } /** * fu_memread_uint32_safe: * @buf: source buffer * @bufsz: maximum size of @buf, typically `sizeof(buf)` * @offset: offset in bytes into @buf to copy from * @value: (out) (nullable): the parsed value * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * @error: (nullable): optional return location for an error * * Read a value from a buffer using a specified endian in a safe way. * * You don't need to use this function in "obviously correct" cases, nor should * you use it when performance is a concern. Only us it when you're not sure if * malicious data from a device or firmware could cause memory corruption. * * Returns: %TRUE if @value was set, %FALSE otherwise * * Since: 1.8.2 **/ gboolean fu_memread_uint32_safe(const guint8 *buf, gsize bufsz, gsize offset, guint32 *value, FuEndianType endian, GError **error) { guint8 dst[4] = {0x0}; g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_memcpy_safe(dst, sizeof(dst), 0x0, /* dst */ buf, bufsz, offset, /* src */ sizeof(dst), error)) return FALSE; if (value != NULL) *value = fu_memread_uint32(dst, endian); return TRUE; } /** * fu_memread_uint64_safe: * @buf: source buffer * @bufsz: maximum size of @buf, typically `sizeof(buf)` * @offset: offset in bytes into @buf to copy from * @value: (out) (nullable): the parsed value * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * @error: (nullable): optional return location for an error * * Read a value from a buffer using a specified endian in a safe way. * * You don't need to use this function in "obviously correct" cases, nor should * you use it when performance is a concern. Only us it when you're not sure if * malicious data from a device or firmware could cause memory corruption. * * Returns: %TRUE if @value was set, %FALSE otherwise * * Since: 1.8.2 **/ gboolean fu_memread_uint64_safe(const guint8 *buf, gsize bufsz, gsize offset, guint64 *value, FuEndianType endian, GError **error) { guint8 dst[8] = {0x0}; g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_memcpy_safe(dst, sizeof(dst), 0x0, /* dst */ buf, bufsz, offset, /* src */ sizeof(dst), error)) return FALSE; if (value != NULL) *value = fu_memread_uint64(dst, endian); return TRUE; } /** * fu_memwrite_uint8_safe: * @buf: source buffer * @bufsz: maximum size of @buf, typically `sizeof(buf)` * @offset: offset in bytes into @buf to write to * @value: the value to write * @error: (nullable): optional return location for an error * * Write a value to a buffer in a safe way. * * You don't need to use this function in "obviously correct" cases, nor should * you use it when performance is a concern. Only us it when you're not sure if * malicious data from a device or firmware could cause memory corruption. * * Returns: %TRUE if @value was written, %FALSE otherwise * * Since: 1.8.2 **/ gboolean fu_memwrite_uint8_safe(guint8 *buf, gsize bufsz, gsize offset, guint8 value, GError **error) { g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return fu_memcpy_safe(buf, bufsz, offset, /* dst */ &value, sizeof(value), 0x0, /* src */ sizeof(value), error); } /** * fu_memwrite_uint16_safe: * @buf: source buffer * @bufsz: maximum size of @buf, typically `sizeof(buf)` * @offset: offset in bytes into @buf to write to * @value: the value to write * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * @error: (nullable): optional return location for an error * * Write a value to a buffer using a specified endian in a safe way. * * You don't need to use this function in "obviously correct" cases, nor should * you use it when performance is a concern. Only us it when you're not sure if * malicious data from a device or firmware could cause memory corruption. * * Returns: %TRUE if @value was written, %FALSE otherwise * * Since: 1.8.2 **/ gboolean fu_memwrite_uint16_safe(guint8 *buf, gsize bufsz, gsize offset, guint16 value, FuEndianType endian, GError **error) { guint8 tmp[2] = {0x0}; g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); fu_memwrite_uint16(tmp, value, endian); return fu_memcpy_safe(buf, bufsz, offset, /* dst */ tmp, sizeof(tmp), 0x0, /* src */ sizeof(tmp), error); } /** * fu_memwrite_uint32_safe: * @buf: source buffer * @bufsz: maximum size of @buf, typically `sizeof(buf)` * @offset: offset in bytes into @buf to write to * @value: the value to write * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * @error: (nullable): optional return location for an error * * Write a value to a buffer using a specified endian in a safe way. * * You don't need to use this function in "obviously correct" cases, nor should * you use it when performance is a concern. Only us it when you're not sure if * malicious data from a device or firmware could cause memory corruption. * * Returns: %TRUE if @value was written, %FALSE otherwise * * Since: 1.8.2 **/ gboolean fu_memwrite_uint32_safe(guint8 *buf, gsize bufsz, gsize offset, guint32 value, FuEndianType endian, GError **error) { guint8 tmp[4] = {0x0}; g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); fu_memwrite_uint32(tmp, value, endian); return fu_memcpy_safe(buf, bufsz, offset, /* dst */ tmp, sizeof(tmp), 0x0, /* src */ sizeof(tmp), error); } /** * fu_memwrite_uint64_safe: * @buf: source buffer * @bufsz: maximum size of @buf, typically `sizeof(buf)` * @offset: offset in bytes into @buf to write to * @value: the value to write * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * @error: (nullable): optional return location for an error * * Write a value to a buffer using a specified endian in a safe way. * * You don't need to use this function in "obviously correct" cases, nor should * you use it when performance is a concern. Only us it when you're not sure if * malicious data from a device or firmware could cause memory corruption. * * Returns: %TRUE if @value was written, %FALSE otherwise * * Since: 1.8.2 **/ gboolean fu_memwrite_uint64_safe(guint8 *buf, gsize bufsz, gsize offset, guint64 value, FuEndianType endian, GError **error) { guint8 tmp[8] = {0x0}; g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); fu_memwrite_uint64(tmp, value, endian); return fu_memcpy_safe(buf, bufsz, offset, /* dst */ tmp, sizeof(tmp), 0x0, /* src */ sizeof(tmp), error); } /** * fu_memstrsafe: * @buf: source buffer * @bufsz: maximum size of @buf, typically `sizeof(buf)` * @offset: offset in bytes into @buf to read from * @maxsz: maximum size of returned string * @error: (nullable): optional return location for an error * * Converts a byte buffer to a ASCII string. * * Returns: (transfer full): a string, or %NULL on error * * Since: 1.9.3 **/ gchar * fu_memstrsafe(const guint8 *buf, gsize bufsz, gsize offset, gsize maxsz, GError **error) { g_autofree gchar *str = NULL; g_return_val_if_fail(buf != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); if (!fu_memchk_read(bufsz, offset, maxsz, error)) return NULL; str = fu_strsafe((const gchar *)buf + offset, maxsz); if (str == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid ASCII string"); return NULL; } return g_steal_pointer(&str); } fwupd-2.0.10/libfwupdplugin/fu-mem.h000066400000000000000000000071101501337203100173030ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-endian.h" gboolean fu_memcmp_safe(const guint8 *buf1, gsize buf1_sz, gsize buf1_offset, const guint8 *buf2, gsize buf2_sz, gsize buf2_offset, gsize n, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 4); guint8 * fu_memdup_safe(const guint8 *src, gsize n, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC G_GNUC_ALLOC_SIZE(2); gboolean fu_memcpy_safe(guint8 *dst, gsize dst_sz, gsize dst_offset, const guint8 *src, gsize src_sz, gsize src_offset, gsize n, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 4); gboolean fu_memmem_safe(const guint8 *haystack, gsize haystack_sz, const guint8 *needle, gsize needle_sz, gsize *offset, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 3); gboolean fu_memread_uint8_safe(const guint8 *buf, gsize bufsz, gsize offset, guint8 *value, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_memread_uint16_safe(const guint8 *buf, gsize bufsz, gsize offset, guint16 *value, FuEndianType endian, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_memread_uint24_safe(const guint8 *buf, gsize bufsz, gsize offset, guint32 *value, FuEndianType endian, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_memread_uint32_safe(const guint8 *buf, gsize bufsz, gsize offset, guint32 *value, FuEndianType endian, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_memread_uint64_safe(const guint8 *buf, gsize bufsz, gsize offset, guint64 *value, FuEndianType endian, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_memwrite_uint8_safe(guint8 *buf, gsize bufsz, gsize offset, guint8 value, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_memwrite_uint16_safe(guint8 *buf, gsize bufsz, gsize offset, guint16 value, FuEndianType endian, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_memwrite_uint32_safe(guint8 *buf, gsize bufsz, gsize offset, guint32 value, FuEndianType endian, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_memwrite_uint64_safe(guint8 *buf, gsize bufsz, gsize offset, guint64 value, FuEndianType endian, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); void fu_memwrite_uint16(guint8 *buf, guint16 val_native, FuEndianType endian) G_GNUC_NON_NULL(1); void fu_memwrite_uint24(guint8 *buf, guint32 val_native, FuEndianType endian) G_GNUC_NON_NULL(1); void fu_memwrite_uint32(guint8 *buf, guint32 val_native, FuEndianType endian) G_GNUC_NON_NULL(1); void fu_memwrite_uint64(guint8 *buf, guint64 val_native, FuEndianType endian) G_GNUC_NON_NULL(1); guint16 fu_memread_uint16(const guint8 *buf, FuEndianType endian) G_GNUC_NON_NULL(1); guint32 fu_memread_uint24(const guint8 *buf, FuEndianType endian) G_GNUC_NON_NULL(1); guint32 fu_memread_uint32(const guint8 *buf, FuEndianType endian) G_GNUC_NON_NULL(1); guint64 fu_memread_uint64(const guint8 *buf, FuEndianType endian) G_GNUC_NON_NULL(1); gchar * fu_memstrsafe(const guint8 *buf, gsize bufsz, gsize offset, gsize maxsz, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-msgpack-item-private.h000066400000000000000000000005661501337203100225660ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-msgpack-item.h" gboolean fu_msgpack_item_append(FuMsgpackItem *self, GByteArray *buf, GError **error) G_GNUC_NON_NULL(1, 2); FuMsgpackItem * fu_msgpack_item_parse(GByteArray *buf, gsize *offset, GError **error) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-msgpack-item.c000066400000000000000000000532311501337203100211060ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuMsgpack" #include "config.h" #include #include "fu-byte-array.h" #include "fu-input-stream.h" #include "fu-mem-private.h" #include "fu-msgpack-item-private.h" struct _FuMsgpackItem { GObject parent_instance; FuMsgpackItemKind kind; GInputStream *stream; union { gint64 i64; gdouble f64; GByteArray *buf; GString *str; } value; }; G_DEFINE_TYPE(FuMsgpackItem, fu_msgpack_item, G_TYPE_OBJECT) /** * fu_msgpack_item_get_kind: * @self: a #FuMsgpackItem * * Gets the item kind. * * Returns: a #FuMsgpackItemKind, e.g. %FU_MSGPACK_ITEM_KIND_BOOLEAN * * Since: 2.0.0 **/ FuMsgpackItemKind fu_msgpack_item_get_kind(FuMsgpackItem *self) { g_return_val_if_fail(FU_IS_MSGPACK_ITEM(self), FU_MSGPACK_ITEM_KIND_UNKNOWN); return self->kind; } /** * fu_msgpack_item_get_boolean: * @self: a #FuMsgpackItem * * Reads a value from the item. * * Returns: an integer, or %G_MAXINT64 if invalid or not found * * Since: 2.0.0 **/ gboolean fu_msgpack_item_get_boolean(FuMsgpackItem *self) { g_return_val_if_fail(FU_IS_MSGPACK_ITEM(self), FALSE); g_return_val_if_fail(self->kind == FU_MSGPACK_ITEM_KIND_BOOLEAN, FALSE); return self->value.i64 > 0; } /** * fu_msgpack_item_get_integer: * @self: a #FuMsgpackItem * * Reads a value from the item. * * Returns: an integer, or %G_MAXINT64 if invalid or not found * * Since: 2.0.0 **/ gint64 fu_msgpack_item_get_integer(FuMsgpackItem *self) { g_return_val_if_fail(FU_IS_MSGPACK_ITEM(self), G_MAXINT64); g_return_val_if_fail(self->kind == FU_MSGPACK_ITEM_KIND_INTEGER, G_MAXINT64); return self->value.i64; } /** * fu_msgpack_item_get_float: * @self: a #FuMsgpackItem * * Reads a value from the item. * * Returns: an integer, or %G_MINDOUBLE if invalid or not found * * Since: 2.0.0 **/ gdouble fu_msgpack_item_get_float(FuMsgpackItem *self) { g_return_val_if_fail(FU_IS_MSGPACK_ITEM(self), G_MINDOUBLE); g_return_val_if_fail(self->kind == FU_MSGPACK_ITEM_KIND_FLOAT, G_MINDOUBLE); return self->value.f64; } /** * fu_msgpack_item_get_binary: * @self: a #FuMsgpackItem * * Reads a value from the item. * * Returns: (transfer none): a #GByteArray * * Since: 2.0.0 **/ GByteArray * fu_msgpack_item_get_binary(FuMsgpackItem *self) { g_return_val_if_fail(FU_IS_MSGPACK_ITEM(self), NULL); g_return_val_if_fail(self->kind == FU_MSGPACK_ITEM_KIND_BINARY, NULL); g_return_val_if_fail(self->stream == NULL, NULL); return self->value.buf; } /** * fu_msgpack_item_get_map: * @self: a #FuMsgpackItem * * Reads the number of items in the map. * * Returns: an integer, or %G_MAXINT64 if invalid or not found * * Since: 2.0.0 **/ guint64 fu_msgpack_item_get_map(FuMsgpackItem *self) { g_return_val_if_fail(FU_IS_MSGPACK_ITEM(self), G_MAXINT64); g_return_val_if_fail(self->kind == FU_MSGPACK_ITEM_KIND_MAP, G_MAXINT64); return self->value.i64; } /** * fu_msgpack_item_get_array: * @self: a #FuMsgpackItem * * Reads the number of items in the array. * * Returns: an integer, or %G_MAXINT64 if invalid or not found * * Since: 2.0.0 **/ guint64 fu_msgpack_item_get_array(FuMsgpackItem *self) { g_return_val_if_fail(FU_IS_MSGPACK_ITEM(self), G_MAXINT64); g_return_val_if_fail(self->kind == FU_MSGPACK_ITEM_KIND_ARRAY, G_MAXINT64); return self->value.i64; } /** * fu_msgpack_item_get_string: * @self: a #FuMsgpackItem * * Reads a value from the item. * * Returns: (transfer none): a #GString * * Since: 2.0.0 **/ GString * fu_msgpack_item_get_string(FuMsgpackItem *self) { g_return_val_if_fail(FU_IS_MSGPACK_ITEM(self), NULL); g_return_val_if_fail(self->kind == FU_MSGPACK_ITEM_KIND_STRING, NULL); return self->value.str; } /** * fu_msgpack_item_new_nil: * * Creates a new msgpack item. * * Returns: (transfer full): a #FuMsgpackItem * * Since: 2.0.0 **/ FuMsgpackItem * fu_msgpack_item_new_nil(void) { g_autoptr(FuMsgpackItem) self = g_object_new(FU_TYPE_MSGPACK_ITEM, NULL); self->kind = FU_MSGPACK_ITEM_KIND_NIL; return g_steal_pointer(&self); } /** * fu_msgpack_item_new_boolean: * @value: integer value * * Creates a new msgpack item. * * Returns: (transfer full): a #FuMsgpackItem * * Since: 2.0.0 **/ FuMsgpackItem * fu_msgpack_item_new_boolean(gboolean value) { g_autoptr(FuMsgpackItem) self = g_object_new(FU_TYPE_MSGPACK_ITEM, NULL); self->kind = FU_MSGPACK_ITEM_KIND_BOOLEAN; self->value.i64 = value ? 1 : 0; return g_steal_pointer(&self); } /** * fu_msgpack_item_new_integer: * @value: integer value * * Creates a new msgpack item. * * Returns: (transfer full): a #FuMsgpackItem * * Since: 2.0.0 **/ FuMsgpackItem * fu_msgpack_item_new_integer(gint64 value) { g_autoptr(FuMsgpackItem) self = g_object_new(FU_TYPE_MSGPACK_ITEM, NULL); g_return_val_if_fail(value < G_MAXINT64, NULL); self->kind = FU_MSGPACK_ITEM_KIND_INTEGER; self->value.i64 = value; return g_steal_pointer(&self); } /** * fu_msgpack_item_new_float: * @value: floating point value * * Creates a new msgpack item. * * Returns: (transfer full): a #FuMsgpackItem * * Since: 2.0.0 **/ FuMsgpackItem * fu_msgpack_item_new_float(gdouble value) { g_autoptr(FuMsgpackItem) self = g_object_new(FU_TYPE_MSGPACK_ITEM, NULL); g_return_val_if_fail(value < G_MAXDOUBLE, NULL); self->kind = FU_MSGPACK_ITEM_KIND_FLOAT; self->value.f64 = value; return g_steal_pointer(&self); } /** * fu_msgpack_item_new_binary: * @buf: (not nullable): a #GByteArray * * Creates a new msgpack item. * * Returns: (transfer full): a #FuMsgpackItem * * Since: 2.0.0 **/ FuMsgpackItem * fu_msgpack_item_new_binary(GByteArray *buf) { g_autoptr(FuMsgpackItem) self = g_object_new(FU_TYPE_MSGPACK_ITEM, NULL); g_return_val_if_fail(buf != NULL, NULL); self->kind = FU_MSGPACK_ITEM_KIND_BINARY; self->value.buf = g_byte_array_ref(buf); return g_steal_pointer(&self); } /** * fu_msgpack_item_new_binary_stream: * @stream: (not nullable): a #GInputStream * * Creates a new msgpack item. * * Returns: (transfer full): a #FuMsgpackItem * * Since: 2.0.0 **/ FuMsgpackItem * fu_msgpack_item_new_binary_stream(GInputStream *stream) { g_autoptr(FuMsgpackItem) self = g_object_new(FU_TYPE_MSGPACK_ITEM, NULL); g_return_val_if_fail(G_IS_INPUT_STREAM(stream), NULL); self->kind = FU_MSGPACK_ITEM_KIND_BINARY; self->stream = g_object_ref(stream); return g_steal_pointer(&self); } /** * fu_msgpack_item_new_string: * @str: (not nullable): string value * * Creates a new msgpack item. * * Returns: (transfer full): a #FuMsgpackItem * * Since: 2.0.0 **/ FuMsgpackItem * fu_msgpack_item_new_string(const gchar *str) { g_autoptr(FuMsgpackItem) self = g_object_new(FU_TYPE_MSGPACK_ITEM, NULL); g_return_val_if_fail(str != NULL, NULL); self->kind = FU_MSGPACK_ITEM_KIND_STRING; self->value.str = g_string_new(str); return g_steal_pointer(&self); } /** * fu_msgpack_item_new_map: * @items: number of items * * Creates a new msgpack item. * * Returns: (transfer full): a #FuMsgpackItem * * Since: 2.0.0 **/ FuMsgpackItem * fu_msgpack_item_new_map(guint64 items) { g_autoptr(FuMsgpackItem) self = g_object_new(FU_TYPE_MSGPACK_ITEM, NULL); self->kind = FU_MSGPACK_ITEM_KIND_MAP; self->value.i64 = items; return g_steal_pointer(&self); } /** * fu_msgpack_item_new_array: * @items: number of items * * Creates a new msgpack item. * * Returns: (transfer full): a #FuMsgpackItem * * Since: 2.0.0 **/ FuMsgpackItem * fu_msgpack_item_new_array(guint64 items) { g_autoptr(FuMsgpackItem) self = g_object_new(FU_TYPE_MSGPACK_ITEM, NULL); self->kind = FU_MSGPACK_ITEM_KIND_ARRAY; self->value.i64 = items; return g_steal_pointer(&self); } static gboolean fu_msgpack_item_append_integer(GByteArray *buf, gint64 val, GError **error) { if (val >= 0) { if (val <= FU_MSGPACK_CMD_POSITIVE_FIXINT_END) { fu_byte_array_append_uint8(buf, val); return TRUE; } if (val <= G_MAXUINT8) { fu_byte_array_append_uint8(buf, FU_MSGPACK_CMD_UINT8); fu_byte_array_append_uint8(buf, val); return TRUE; } if (val <= G_MAXUINT16) { fu_byte_array_append_uint8(buf, FU_MSGPACK_CMD_UINT16); fu_byte_array_append_uint16(buf, val, G_BIG_ENDIAN); return TRUE; } if (val <= G_MAXUINT32) { fu_byte_array_append_uint8(buf, FU_MSGPACK_CMD_UINT32); fu_byte_array_append_uint32(buf, val, G_BIG_ENDIAN); return TRUE; } fu_byte_array_append_uint8(buf, FU_MSGPACK_CMD_UINT64); fu_byte_array_append_uint64(buf, val, G_BIG_ENDIAN); return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "negint not supported"); return FALSE; } static gboolean fu_msgpack_item_append_double(GByteArray *buf, gdouble val, GError **error) { guint64 int_val = 0; fu_byte_array_append_uint8(buf, FU_MSGPACK_CMD_FLOAT64); if (!fu_memcpy_safe((guint8 *)&int_val, sizeof(int_val), 0, (guint8 *)&val, sizeof(val), 0, sizeof(val), error)) return FALSE; fu_byte_array_append_uint64(buf, int_val, G_BIG_ENDIAN); return TRUE; } static gboolean fu_msgpack_item_append_array(GByteArray *buf, gint64 val, GError **error) { if (val <= 15) { fu_byte_array_append_uint8(buf, FU_MSGPACK_CMD_FIXARRAY | val); return TRUE; } if (val <= G_MAXUINT16) { fu_byte_array_append_uint8(buf, FU_MSGPACK_CMD_ARRAY16); fu_byte_array_append_uint16(buf, val, G_BIG_ENDIAN); return TRUE; } if (val <= G_MAXUINT32) { fu_byte_array_append_uint8(buf, FU_MSGPACK_CMD_ARRAY32); fu_byte_array_append_uint32(buf, val, G_BIG_ENDIAN); return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "array too large"); return FALSE; } static gboolean fu_msgpack_item_append_map(GByteArray *buf, gint64 val, GError **error) { if (val <= 15) { fu_byte_array_append_uint8(buf, FU_MSGPACK_CMD_FIXMAP | val); return TRUE; } if (val <= G_MAXUINT16) { fu_byte_array_append_uint8(buf, FU_MSGPACK_CMD_MAP16); fu_byte_array_append_uint16(buf, val, G_BIG_ENDIAN); return TRUE; } if (val <= G_MAXUINT32) { fu_byte_array_append_uint8(buf, FU_MSGPACK_CMD_MAP32); fu_byte_array_append_uint32(buf, val, G_BIG_ENDIAN); return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "map too large"); return FALSE; } static gboolean fu_msgpack_item_append_string(GByteArray *buf, GString *str, GError **error) { if (str->len <= 31) { fu_byte_array_append_uint8(buf, FU_MSGPACK_CMD_FIXSTR | str->len); g_byte_array_append(buf, (const guint8 *)str->str, str->len); return TRUE; } if (str->len <= G_MAXUINT8) { fu_byte_array_append_uint8(buf, FU_MSGPACK_CMD_STR8); fu_byte_array_append_uint8(buf, str->len); g_byte_array_append(buf, (const guint8 *)str->str, str->len); return TRUE; } if (str->len <= G_MAXUINT16) { fu_byte_array_append_uint8(buf, FU_MSGPACK_CMD_STR16); fu_byte_array_append_uint16(buf, str->len, G_BIG_ENDIAN); g_byte_array_append(buf, (const guint8 *)str->str, str->len); return TRUE; } if (str->len <= G_MAXUINT32) { fu_byte_array_append_uint8(buf, FU_MSGPACK_CMD_STR32); fu_byte_array_append_uint32(buf, str->len, G_BIG_ENDIAN); g_byte_array_append(buf, (const guint8 *)str->str, str->len); return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "string too long"); return FALSE; } static gboolean fu_msgpack_item_append_binary_stream_chunk_cb(const guint8 *buf, gsize bufsz, gpointer user_data, GError **error) { GByteArray *data = (GByteArray *)user_data; g_byte_array_append(data, buf, bufsz); return TRUE; } static gboolean fu_msgpack_item_append_binary_stream(GByteArray *buf, GInputStream *stream, GError **error) { gsize streamsz = 0; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz <= G_MAXUINT8) { fu_byte_array_append_uint8(buf, FU_MSGPACK_CMD_BIN8); fu_byte_array_append_uint8(buf, streamsz); return fu_input_stream_chunkify(stream, fu_msgpack_item_append_binary_stream_chunk_cb, buf, error); } if (streamsz <= G_MAXUINT16) { fu_byte_array_append_uint8(buf, FU_MSGPACK_CMD_BIN16); fu_byte_array_append_uint16(buf, streamsz, G_BIG_ENDIAN); return fu_input_stream_chunkify(stream, fu_msgpack_item_append_binary_stream_chunk_cb, buf, error); } if (streamsz <= G_MAXUINT32) { fu_byte_array_append_uint8(buf, FU_MSGPACK_CMD_BIN32); fu_byte_array_append_uint32(buf, streamsz, G_BIG_ENDIAN); return fu_input_stream_chunkify(stream, fu_msgpack_item_append_binary_stream_chunk_cb, buf, error); } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "binary too large"); return FALSE; } static gboolean fu_msgpack_item_append_binary(GByteArray *buf, GByteArray *donor, GError **error) { if (donor->len <= G_MAXUINT8) { fu_byte_array_append_uint8(buf, FU_MSGPACK_CMD_BIN8); fu_byte_array_append_uint8(buf, donor->len); g_byte_array_append(buf, donor->data, donor->len); return TRUE; } if (donor->len <= G_MAXUINT16) { fu_byte_array_append_uint8(buf, FU_MSGPACK_CMD_BIN16); fu_byte_array_append_uint16(buf, donor->len, G_BIG_ENDIAN); g_byte_array_append(buf, donor->data, donor->len); return TRUE; } if (donor->len <= G_MAXUINT32) { fu_byte_array_append_uint8(buf, FU_MSGPACK_CMD_BIN32); fu_byte_array_append_uint32(buf, donor->len, G_BIG_ENDIAN); g_byte_array_append(buf, donor->data, donor->len); return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "binary too large"); return FALSE; } /* private */ gboolean fu_msgpack_item_append(FuMsgpackItem *self, GByteArray *buf, GError **error) { g_return_val_if_fail(FU_IS_MSGPACK_ITEM(self), FALSE); if (self->kind == FU_MSGPACK_ITEM_KIND_NIL) { fu_byte_array_append_uint8(buf, FU_MSGPACK_CMD_NIL); return TRUE; } if (self->kind == FU_MSGPACK_ITEM_KIND_BOOLEAN) { fu_byte_array_append_uint8(buf, self->value.i64 ? FU_MSGPACK_CMD_TRUE : FU_MSGPACK_CMD_FALSE); return TRUE; } if (self->kind == FU_MSGPACK_ITEM_KIND_FLOAT) return fu_msgpack_item_append_double(buf, self->value.f64, error); if (self->kind == FU_MSGPACK_ITEM_KIND_INTEGER) return fu_msgpack_item_append_integer(buf, self->value.i64, error); if (self->kind == FU_MSGPACK_ITEM_KIND_STRING) return fu_msgpack_item_append_string(buf, self->value.str, error); if (self->kind == FU_MSGPACK_ITEM_KIND_BINARY) { if (self->stream != NULL) return fu_msgpack_item_append_binary_stream(buf, self->stream, error); return fu_msgpack_item_append_binary(buf, self->value.buf, error); } if (self->kind == FU_MSGPACK_ITEM_KIND_ARRAY) return fu_msgpack_item_append_array(buf, self->value.i64, error); if (self->kind == FU_MSGPACK_ITEM_KIND_MAP) return fu_msgpack_item_append_map(buf, self->value.i64, error); /* failure */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "msgpack item %s not supported", fu_msgpack_item_kind_to_string(self->kind)); return FALSE; } static GByteArray * fu_msgpack_item_read_binary(GByteArray *buf, gsize offset, gsize n, GError **error) { g_autoptr(GByteArray) tmp = g_byte_array_new(); if (!fu_memchk_read(buf->len, offset, n, error)) return NULL; g_byte_array_append(tmp, buf->data + offset, n); return g_steal_pointer(&tmp); } static gchar * fu_msgpack_item_read_string(GByteArray *buf, gsize offset, gsize n, GError **error) { g_autofree gchar *tmp = NULL; if (!fu_memchk_read(buf->len, offset, n, error)) return NULL; tmp = g_strndup((const gchar *)buf->data + offset, n); if (!g_utf8_validate_len(tmp, n, NULL)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid UTF-8 string"); return NULL; } return g_steal_pointer(&tmp); } FuMsgpackItem * fu_msgpack_item_parse(GByteArray *buf, gsize *offset, GError **error) { guint8 cmd = 0; g_autofree gchar *tmp_string = NULL; g_autoptr(GByteArray) tmp_binary = NULL; g_return_val_if_fail(buf != NULL, NULL); g_return_val_if_fail(offset != NULL, NULL); /* first byte, normally a command */ if (!fu_memread_uint8_safe(buf->data, buf->len, *offset, &cmd, error)) return NULL; *offset += 1; /* nil */ if (cmd == FU_MSGPACK_CMD_NIL) return fu_msgpack_item_new_nil(); /* boolean */ if (cmd == FU_MSGPACK_CMD_FALSE) return fu_msgpack_item_new_boolean(FALSE); if (cmd == FU_MSGPACK_CMD_TRUE) return fu_msgpack_item_new_boolean(TRUE); /* integer */ if (cmd <= FU_MSGPACK_CMD_POSITIVE_FIXINT_END) return fu_msgpack_item_new_integer(cmd); if (cmd == FU_MSGPACK_CMD_UINT8) { guint8 v = 0; if (!fu_memread_uint8_safe(buf->data, buf->len, *offset, &v, error)) return NULL; *offset += sizeof(v); return fu_msgpack_item_new_integer(v); } if (cmd == FU_MSGPACK_CMD_UINT16) { guint16 v = 0; if (!fu_memread_uint16_safe(buf->data, buf->len, *offset, &v, G_BIG_ENDIAN, error)) return NULL; *offset += sizeof(v); return fu_msgpack_item_new_integer(v); } if (cmd == FU_MSGPACK_CMD_UINT32) { guint32 v = 0; if (!fu_memread_uint32_safe(buf->data, buf->len, *offset, &v, G_BIG_ENDIAN, error)) return NULL; *offset += sizeof(v); return fu_msgpack_item_new_integer(v); } if (cmd == FU_MSGPACK_CMD_UINT64) { guint64 v = 0; if (!fu_memread_uint64_safe(buf->data, buf->len, *offset, &v, G_BIG_ENDIAN, error)) return NULL; *offset += sizeof(v); return fu_msgpack_item_new_integer(v); } /* float */ if (cmd == FU_MSGPACK_CMD_FLOAT64) { gdouble v = 0.; if (!fu_memread_uint64_safe(buf->data, buf->len, *offset, (guint64 *)&v, G_BIG_ENDIAN, error)) return NULL; *offset += sizeof(v); return fu_msgpack_item_new_float(v); } /* string */ if (cmd >= FU_MSGPACK_CMD_FIXSTR && cmd <= FU_MSGPACK_CMD_FIXSTR_END) { gsize n = cmd & 0b00011111; tmp_string = fu_msgpack_item_read_string(buf, *offset, n, error); if (tmp_string == NULL) return NULL; *offset += n; return fu_msgpack_item_new_string(tmp_string); } if (cmd == FU_MSGPACK_CMD_STR8) { guint8 n = 0; if (!fu_memread_uint8_safe(buf->data, buf->len, *offset, &n, error)) return NULL; tmp_string = fu_msgpack_item_read_string(buf, (*offset) + sizeof(n), n, error); if (tmp_string == NULL) return NULL; *offset += sizeof(n) + n; return fu_msgpack_item_new_string(tmp_string); } if (cmd == FU_MSGPACK_CMD_STR16) { guint16 n = 0; if (!fu_memread_uint16_safe(buf->data, buf->len, *offset, &n, G_BIG_ENDIAN, error)) return NULL; tmp_string = fu_msgpack_item_read_string(buf, (*offset) + sizeof(n), n, error); if (tmp_string == NULL) return NULL; *offset += sizeof(n) + n; return fu_msgpack_item_new_string(tmp_string); } if (cmd == FU_MSGPACK_CMD_STR32) { guint32 n = 0; if (!fu_memread_uint32_safe(buf->data, buf->len, *offset, &n, G_BIG_ENDIAN, error)) return NULL; tmp_string = fu_msgpack_item_read_string(buf, (*offset) + sizeof(n), n, error); if (tmp_string == NULL) return NULL; *offset += sizeof(n) + n; return fu_msgpack_item_new_string(tmp_string); } /* binary */ if (cmd == FU_MSGPACK_CMD_BIN8) { guint8 n = 0; if (!fu_memread_uint8_safe(buf->data, buf->len, *offset, &n, error)) return NULL; tmp_binary = fu_msgpack_item_read_binary(buf, (*offset) + sizeof(n), n, error); if (tmp_binary == NULL) return NULL; *offset += sizeof(n) + n; return fu_msgpack_item_new_binary(tmp_binary); } if (cmd == FU_MSGPACK_CMD_BIN16) { guint16 n = 0; if (!fu_memread_uint16_safe(buf->data, buf->len, *offset, &n, G_BIG_ENDIAN, error)) return NULL; tmp_binary = fu_msgpack_item_read_binary(buf, (*offset) + sizeof(n), n, error); if (tmp_binary == NULL) return NULL; *offset += sizeof(n) + n; return fu_msgpack_item_new_binary(tmp_binary); } if (cmd == FU_MSGPACK_CMD_BIN32) { guint32 n = 0; if (!fu_memread_uint32_safe(buf->data, buf->len, *offset, &n, G_BIG_ENDIAN, error)) return NULL; tmp_binary = fu_msgpack_item_read_binary(buf, (*offset) + sizeof(n), n, error); if (tmp_binary == NULL) return NULL; *offset += sizeof(n) + n; return fu_msgpack_item_new_binary(tmp_binary); } /* array */ if (cmd >= FU_MSGPACK_CMD_FIXARRAY && cmd <= FU_MSGPACK_CMD_FIXARRAY_END) return fu_msgpack_item_new_array(cmd & 0b00001111); if (cmd == FU_MSGPACK_CMD_ARRAY16) { guint16 n = 0; if (!fu_memread_uint16_safe(buf->data, buf->len, *offset, &n, G_BIG_ENDIAN, error)) return NULL; *offset += sizeof(n); return fu_msgpack_item_new_array(n); } if (cmd == FU_MSGPACK_CMD_ARRAY32) { guint32 n = 0; if (!fu_memread_uint32_safe(buf->data, buf->len, *offset, &n, G_BIG_ENDIAN, error)) return NULL; *offset += sizeof(n); return fu_msgpack_item_new_array(n); } /* map */ if (cmd >= FU_MSGPACK_CMD_FIXMAP && cmd <= FU_MSGPACK_CMD_FIXMAP_END) return fu_msgpack_item_new_map(cmd & 0b00001111); if (cmd == FU_MSGPACK_CMD_MAP16) { guint16 n = 0; if (!fu_memread_uint16_safe(buf->data, buf->len, *offset, &n, G_BIG_ENDIAN, error)) return NULL; *offset += sizeof(n); return fu_msgpack_item_new_map(n); } if (cmd == FU_MSGPACK_CMD_MAP32) { guint32 n = 0; if (!fu_memread_uint32_safe(buf->data, buf->len, *offset, &n, G_BIG_ENDIAN, error)) return NULL; *offset += sizeof(n); return fu_msgpack_item_new_map(n); } /* failure */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to parse 0x%02X", cmd); return NULL; } static void fu_msgpack_item_init(FuMsgpackItem *self) { } static void fu_msgpack_item_finalize(GObject *object) { FuMsgpackItem *self = FU_MSGPACK_ITEM(object); if (self->stream != NULL) { g_object_unref(self->stream); } else { if (self->kind == FU_MSGPACK_ITEM_KIND_BINARY) g_byte_array_unref(self->value.buf); if (self->kind == FU_MSGPACK_ITEM_KIND_STRING) g_string_free(self->value.str, TRUE); } G_OBJECT_CLASS(fu_msgpack_item_parent_class)->finalize(object); } static void fu_msgpack_item_class_init(FuMsgpackItemClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_msgpack_item_finalize; } fwupd-2.0.10/libfwupdplugin/fu-msgpack-item.h000066400000000000000000000027471501337203100211210ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-msgpack-struct.h" #define FU_TYPE_MSGPACK_ITEM (fu_msgpack_item_get_type()) G_DECLARE_FINAL_TYPE(FuMsgpackItem, fu_msgpack_item, FU, MSGPACK_ITEM, GObject) FuMsgpackItem * fu_msgpack_item_new_nil(void); FuMsgpackItem * fu_msgpack_item_new_boolean(gboolean value); FuMsgpackItem * fu_msgpack_item_new_integer(gint64 value); FuMsgpackItem * fu_msgpack_item_new_float(gdouble value); FuMsgpackItem * fu_msgpack_item_new_binary(GByteArray *buf) G_GNUC_NON_NULL(1); FuMsgpackItem * fu_msgpack_item_new_binary_stream(GInputStream *stream) G_GNUC_NON_NULL(1); FuMsgpackItem * fu_msgpack_item_new_string(const gchar *str) G_GNUC_NON_NULL(1); FuMsgpackItem * fu_msgpack_item_new_map(guint64 items); FuMsgpackItem * fu_msgpack_item_new_array(guint64 items); FuMsgpackItemKind fu_msgpack_item_get_kind(FuMsgpackItem *self) G_GNUC_NON_NULL(1); gboolean fu_msgpack_item_get_boolean(FuMsgpackItem *self) G_GNUC_NON_NULL(1); gint64 fu_msgpack_item_get_integer(FuMsgpackItem *self) G_GNUC_NON_NULL(1); gdouble fu_msgpack_item_get_float(FuMsgpackItem *self) G_GNUC_NON_NULL(1); GByteArray * fu_msgpack_item_get_binary(FuMsgpackItem *self) G_GNUC_NON_NULL(1); GString * fu_msgpack_item_get_string(FuMsgpackItem *self) G_GNUC_NON_NULL(1); guint64 fu_msgpack_item_get_map(FuMsgpackItem *self) G_GNUC_NON_NULL(1); guint64 fu_msgpack_item_get_array(FuMsgpackItem *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-msgpack.c000066400000000000000000000076521501337203100201600ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuMsgpack" #include "config.h" #include "fu-msgpack-item-private.h" #include "fu-msgpack.h" /** * fu_msgpack_new: * @buf: data blob * @error: (nullable): optional return location for an error * * Parses a buffer into messagepack items. * * Returns: (transfer container) (element-type FuMsgpackItem): items, or %NULL on error * * Since: 2.0.0 **/ GPtrArray * fu_msgpack_parse(GByteArray *buf, GError **error) { gsize offset = 0; g_autoptr(GPtrArray) items = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_return_val_if_fail(buf != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); while (offset < buf->len) { g_autoptr(FuMsgpackItem) item = NULL; item = fu_msgpack_item_parse(buf, &offset, error); if (item == NULL) { g_prefix_error(error, "offset 0x%x: ", (guint)offset); return NULL; } g_ptr_array_add(items, g_steal_pointer(&item)); } return g_steal_pointer(&items); } /** * fu_msgpack_write: * @items: (element-type FuMsgpackItem): items * @error: (nullable): optional return location for an error * * Writes messagepack items into a buffer. * * Returns: (transfer container): buffer, or %NULL on error * * Since: 2.0.0 **/ GByteArray * fu_msgpack_write(GPtrArray *items, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_return_val_if_fail(items != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); for (guint i = 0; i < items->len; i++) { FuMsgpackItem *item = g_ptr_array_index(items, i); if (!fu_msgpack_item_append(item, buf, error)) return NULL; } /* success */ return g_steal_pointer(&buf); } /** * fu_msgpack_map_lookup: * @items: (element-type FuMsgpackItem): items * @idx: index into the items, usually 0 * @key: (not nullable): key to find * @error: (nullable): optional return location for an error * * Looks up an item from a map. This is similar in action to looking up an `a{sv}` dictionary * with g_variant_lookup(). * * Returns: (transfer full): a #FuMsgpackItem, or %NULL on error * * Since: 2.0.0 **/ FuMsgpackItem * fu_msgpack_map_lookup(GPtrArray *items, guint idx, const gchar *key, GError **error) { guint64 map_size = 0; FuMsgpackItem *item_map; g_return_val_if_fail(items != NULL, NULL); g_return_val_if_fail(key != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* sanity check */ if (idx >= items->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "index %u of %u would be invalid", idx, items->len); return NULL; } /* verify is a map */ item_map = g_ptr_array_index(items, idx); if (fu_msgpack_item_get_kind(item_map) != FU_MSGPACK_ITEM_KIND_MAP) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "is not a map"); return NULL; } /* read each {sv} */ map_size = fu_msgpack_item_get_map(item_map); if (idx + (map_size * 2) >= items->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "map %u with index %u of %u would be invalid", (guint)map_size, idx, items->len); return NULL; } for (guint i = idx + 1; i < idx + (map_size * 2); i += 2) { FuMsgpackItem *item_key = g_ptr_array_index(items, i); FuMsgpackItem *item_value = g_ptr_array_index(items, i + 1); FuMsgpackItemKind kind_key = fu_msgpack_item_get_kind(item_key); if (kind_key != FU_MSGPACK_ITEM_KIND_STRING) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "at index %u, key is not a string, got %s", i, fu_msgpack_item_kind_to_string(kind_key)); return NULL; } if (g_strcmp0(fu_msgpack_item_get_string(item_key)->str, key) == 0) return g_object_ref(item_value); } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no key %s in map", key); return NULL; } fwupd-2.0.10/libfwupdplugin/fu-msgpack.h000066400000000000000000000007301501337203100201530ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-msgpack-struct.h" GPtrArray * fu_msgpack_parse(GByteArray *buf, GError **error) G_GNUC_NON_NULL(1); GByteArray * fu_msgpack_write(GPtrArray *items, GError **error) G_GNUC_NON_NULL(1); FuMsgpackItem * fu_msgpack_map_lookup(GPtrArray *items, guint idx, const gchar *key, GError **error) G_GNUC_NON_NULL(1, 3); fwupd-2.0.10/libfwupdplugin/fu-msgpack.rs000066400000000000000000000027601501337203100203550ustar00rootroot00000000000000// Copyright 2024 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] enum FuMsgpackItemKind { Unknown, Integer, Nil, Boolean, Float, String, Binary, Array, Map, Extension, } enum FuMsgpackCmd { PositiveFixint = 0x00, PositiveFixintEnd = 0x7F, Fixmap = 0x80, FixmapEnd = 0x8F, Fixarray = 0x90, FixarrayEnd = 0x9F, Fixstr = 0xA0, FixstrEnd = 0xBF, Nil = 0xC0, False = 0xC2, True = 0xC3, Bin8 = 0xC4, Bin16 = 0xC5, Bin32 = 0xC6, Ext8 = 0xC7, Ext16 = 0xC8, Ext32 = 0xC9, Float32 = 0xCA, Float64 = 0xCB, Uint8 = 0xCC, Uint16 = 0xCD, Uint32 = 0xCE, Uint64 = 0xCF, Int8 = 0xD0, Int16 = 0xD1, Int32 = 0xD2, Int64 = 0xD3, Fixext1 = 0xD4, Fixext2 = 0xD5, Fixext4 = 0xD6, Fixext8 = 0xD7, Fixext16 = 0xD8, Str8 = 0xD9, Str16 = 0xDA, Str32 = 0xDB, Array16 = 0xDC, Array32 = 0xDD, Map16 = 0xDE, Map32 = 0xDF, NegativeFixint = 0xE0, NegativeFixintEnd = 0xFF, } fwupd-2.0.10/libfwupdplugin/fu-oprom-device.c000066400000000000000000000106241501337203100211150ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-oprom-device.h" G_DEFINE_TYPE(FuOpromDevice, fu_oprom_device, FU_TYPE_PCI_DEVICE) static gboolean fu_oprom_device_probe(FuDevice *device, GError **error) { FuOpromDevice *self = FU_OPROM_DEVICE(device); gboolean rom_exists = FALSE; g_autofree gchar *rom_fn = NULL; /* does the device even have ROM? */ rom_fn = g_build_filename(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(self)), "rom", NULL); if (!fu_device_query_file_exists(device, rom_fn, &rom_exists, error)) return FALSE; if (rom_exists) fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); return TRUE; } static gboolean fu_oprom_device_set_enabled(FuOpromDevice *self, gboolean value, GError **error) { g_autofree gchar *rom_fn = NULL; g_autoptr(GFile) file = NULL; g_autoptr(GFileOutputStream) output_stream = NULL; rom_fn = g_build_filename(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(self)), "rom", NULL); if (!g_str_has_prefix(rom_fn, "/sys")) return TRUE; file = g_file_new_for_path(rom_fn); output_stream = g_file_replace(file, NULL, FALSE, G_FILE_CREATE_NONE, NULL, error); if (output_stream == NULL) { fu_error_convert(error); return FALSE; } if (!g_output_stream_write_all(G_OUTPUT_STREAM(output_stream), value ? "1" : "0", 1, NULL, NULL, error)) { fu_error_convert(error); return FALSE; } /* success */ return TRUE; } static gboolean fu_oprom_device_dump_enable_cb(GObject *device, GError **error) { FuOpromDevice *self = FU_OPROM_DEVICE(device); return fu_oprom_device_set_enabled(self, TRUE, error); } static gboolean fu_oprom_device_dump_disable_cb(GObject *device, GError **error) { FuOpromDevice *self = FU_OPROM_DEVICE(device); return fu_oprom_device_set_enabled(self, FALSE, error); } static GBytes * fu_oprom_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuOpromDevice *self = FU_OPROM_DEVICE(device); guint number_reads = 0; g_autofree gchar *rom_fn = NULL; g_autoptr(FuDeviceLocker) locker_enable = NULL; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GError) error_local = NULL; g_autoptr(GFile) file = NULL; g_autoptr(GInputStream) stream = NULL; /* sanity check */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unable to read firmware from device, 'rom' does not exist"); return NULL; } /* open file */ rom_fn = g_build_filename(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(self)), "rom", NULL); file = g_file_new_for_path(rom_fn); stream = G_INPUT_STREAM(g_file_read(file, NULL, &error_local)); if (stream == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_AUTH_FAILED, error_local->message); return NULL; } /* we have to enable the read for devices */ locker_enable = fu_device_locker_new_full(device, fu_oprom_device_dump_enable_cb, fu_oprom_device_dump_disable_cb, error); if (locker_enable == NULL) return NULL; /* ensure we got enough data to fill the buffer */ while (TRUE) { gssize sz; guint8 tmp[32 * 1024] = {0x0}; sz = g_input_stream_read(stream, tmp, sizeof(tmp), NULL, error); if (sz == 0) break; g_debug("ROM returned 0x%04x bytes", (guint)sz); if (sz < 0) return NULL; g_byte_array_append(buf, tmp, sz); /* check the firmware isn't serving us small chunks */ if (number_reads++ > 1024) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware not fulfilling requests"); return NULL; } } if (buf->len < 512) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware too small: 0x%x bytes", (guint)buf->len); return NULL; } return g_byte_array_free_to_bytes(g_steal_pointer(&buf)); /* nocheck:blocked */ } static void fu_oprom_device_init(FuOpromDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); } static void fu_oprom_device_class_init(FuOpromDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->dump_firmware = fu_oprom_device_dump_firmware; device_class->probe = fu_oprom_device_probe; } fwupd-2.0.10/libfwupdplugin/fu-oprom-device.h000066400000000000000000000005561501337203100211250ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-pci-device.h" #define FU_TYPE_OPROM_DEVICE (fu_oprom_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuOpromDevice, fu_oprom_device, FU, OPROM_DEVICE, FuPciDevice) struct _FuOpromDeviceClass { FuPciDeviceClass parent_class; }; fwupd-2.0.10/libfwupdplugin/fu-oprom-firmware.c000066400000000000000000000217421501337203100214750ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * Copyright 2022 Intel * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-ifwi-cpd-firmware.h" #include "fu-oprom-firmware.h" #include "fu-string.h" /** * FuOpromFirmware: * * An OptionROM can be found in nearly every PCI device. Multiple OptionROM images may be appended. * * See also: [class@FuFirmware] */ typedef struct { FuOpromMachineType machine_type; FuOpromSubsystem subsystem; FuOpromCompressionType compression_type; guint16 vendor_id; guint16 device_id; } FuOpromFirmwarePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuOpromFirmware, fu_oprom_firmware, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_oprom_firmware_get_instance_private(o)) #define FU_OPROM_FIRMWARE_ALIGN_LEN 512u #define FU_OPROM_FIRMWARE_LAST_IMAGE_INDICATOR_BIT (1u << 7) /** * fu_oprom_firmware_get_machine_type: * @self: a #FuFirmware * * Gets the machine type. * * Returns: a #FuOpromMachineType * * Since: 1.8.2 **/ FuOpromMachineType fu_oprom_firmware_get_machine_type(FuOpromFirmware *self) { FuOpromFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_OPROM_FIRMWARE(self), G_MAXUINT16); return priv->machine_type; } /** * fu_oprom_firmware_get_subsystem: * @self: a #FuFirmware * * Gets the machine type. * * Returns: a #FuOpromSubsystem * * Since: 1.8.2 **/ FuOpromSubsystem fu_oprom_firmware_get_subsystem(FuOpromFirmware *self) { FuOpromFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_OPROM_FIRMWARE(self), G_MAXUINT16); return priv->subsystem; } /** * fu_oprom_firmware_get_compression_type: * @self: a #FuFirmware * * Gets the machine type. * * Returns: a #FuOpromCompressionType * * Since: 1.8.2 **/ FuOpromCompressionType fu_oprom_firmware_get_compression_type(FuOpromFirmware *self) { FuOpromFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_OPROM_FIRMWARE(self), G_MAXUINT16); return priv->compression_type; } static void fu_oprom_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuOpromFirmware *self = FU_OPROM_FIRMWARE(firmware); FuOpromFirmwarePrivate *priv = GET_PRIVATE(self); fu_xmlb_builder_insert_kx(bn, "machine_type", priv->machine_type); fu_xmlb_builder_insert_kx(bn, "subsystem", priv->subsystem); fu_xmlb_builder_insert_kx(bn, "compression_type", priv->compression_type); fu_xmlb_builder_insert_kx(bn, "vendor_id", priv->vendor_id); fu_xmlb_builder_insert_kx(bn, "device_id", priv->device_id); } static gboolean fu_oprom_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_oprom_validate_stream(stream, offset, error); } static gboolean fu_oprom_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuOpromFirmware *self = FU_OPROM_FIRMWARE(firmware); FuOpromFirmwarePrivate *priv = GET_PRIVATE(self); guint16 expansion_header_offset = 0; guint16 pci_header_offset; guint16 image_length = 0; g_autoptr(GByteArray) st_hdr = NULL; g_autoptr(GByteArray) st_pci = NULL; /* parse header */ st_hdr = fu_struct_oprom_parse_stream(stream, 0x0, error); if (st_hdr == NULL) return FALSE; priv->subsystem = fu_struct_oprom_get_subsystem(st_hdr); priv->compression_type = fu_struct_oprom_get_compression_type(st_hdr); priv->machine_type = fu_struct_oprom_get_machine_type(st_hdr); /* get PCI offset */ pci_header_offset = fu_struct_oprom_get_pci_header_offset(st_hdr); if (pci_header_offset == 0x0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no PCI data structure offset provided"); return FALSE; } /* verify signature */ st_pci = fu_struct_oprom_pci_parse_stream(stream, pci_header_offset, error); if (st_pci == NULL) return FALSE; priv->vendor_id = fu_struct_oprom_pci_get_vendor_id(st_pci); priv->device_id = fu_struct_oprom_pci_get_device_id(st_pci); /* get length */ image_length = fu_struct_oprom_pci_get_image_length(st_pci); if (image_length == 0x0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid image length"); return FALSE; } fu_firmware_set_size(firmware, image_length * FU_OPROM_FIRMWARE_ALIGN_LEN); fu_firmware_set_idx(firmware, fu_struct_oprom_pci_get_code_type(st_pci)); /* get CPD offset */ expansion_header_offset = fu_struct_oprom_get_expansion_header_offset(st_hdr); if (expansion_header_offset != 0x0) { g_autoptr(FuFirmware) img = NULL; img = fu_firmware_new_from_gtypes(stream, expansion_header_offset, flags, error, FU_TYPE_IFWI_CPD_FIRMWARE, FU_TYPE_FIRMWARE, G_TYPE_INVALID); if (img == NULL) { g_prefix_error(error, "failed to build firmware: "); return FALSE; } fu_firmware_set_id(img, "cpd"); fu_firmware_set_offset(img, expansion_header_offset); fu_firmware_add_image(firmware, img); } /* success */ return TRUE; } static GByteArray * fu_oprom_firmware_write(FuFirmware *firmware, GError **error) { FuOpromFirmware *self = FU_OPROM_FIRMWARE(firmware); FuOpromFirmwarePrivate *priv = GET_PRIVATE(self); gsize image_size = 0; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GByteArray) st_hdr = fu_struct_oprom_new(); g_autoptr(GByteArray) st_pci = fu_struct_oprom_pci_new(); g_autoptr(GBytes) blob_cpd = NULL; /* the smallest each image (and header) can be is 512 bytes */ image_size += fu_common_align_up(st_hdr->len, FU_FIRMWARE_ALIGNMENT_512); blob_cpd = fu_firmware_get_image_by_id_bytes(firmware, "cpd", NULL); if (blob_cpd != NULL) { image_size += fu_common_align_up(g_bytes_get_size(blob_cpd), FU_FIRMWARE_ALIGNMENT_512); } /* write the header */ fu_struct_oprom_set_image_size(st_hdr, image_size / FU_OPROM_FIRMWARE_ALIGN_LEN); fu_struct_oprom_set_subsystem(st_hdr, priv->subsystem); fu_struct_oprom_set_machine_type(st_hdr, priv->machine_type); fu_struct_oprom_set_compression_type(st_hdr, priv->compression_type); if (blob_cpd != NULL) { fu_struct_oprom_set_expansion_header_offset(st_hdr, image_size - FU_OPROM_FIRMWARE_ALIGN_LEN); } g_byte_array_append(buf, st_hdr->data, st_hdr->len); /* add PCI section */ fu_struct_oprom_pci_set_vendor_id(st_pci, priv->vendor_id); fu_struct_oprom_pci_set_device_id(st_pci, priv->device_id); fu_struct_oprom_pci_set_image_length(st_pci, image_size / FU_OPROM_FIRMWARE_ALIGN_LEN); fu_struct_oprom_pci_set_code_type(st_pci, fu_firmware_get_idx(firmware)); fu_struct_oprom_pci_set_indicator(st_pci, FU_OPROM_FIRMWARE_LAST_IMAGE_INDICATOR_BIT); g_byte_array_append(buf, st_pci->data, st_pci->len); fu_byte_array_align_up(buf, FU_FIRMWARE_ALIGNMENT_512, 0xFF); /* add CPD */ if (blob_cpd != NULL) { fu_byte_array_append_bytes(buf, blob_cpd); fu_byte_array_align_up(buf, FU_FIRMWARE_ALIGNMENT_512, 0xFF); } /* success */ return g_steal_pointer(&buf); } static gboolean fu_oprom_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { FuOpromFirmware *self = FU_OPROM_FIRMWARE(firmware); FuOpromFirmwarePrivate *priv = GET_PRIVATE(self); const gchar *tmp; /* simple properties */ tmp = xb_node_query_text(n, "machine_type", NULL); if (tmp != NULL) { guint64 val = 0; if (!fu_strtoull(tmp, &val, 0x0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->machine_type = val; } tmp = xb_node_query_text(n, "subsystem", NULL); if (tmp != NULL) { guint64 val = 0; if (!fu_strtoull(tmp, &val, 0x0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->subsystem = val; } tmp = xb_node_query_text(n, "compression_type", NULL); if (tmp != NULL) { guint64 val = 0; if (!fu_strtoull(tmp, &val, 0x0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->compression_type = val; } tmp = xb_node_query_text(n, "vendor_id", NULL); if (tmp != NULL) { guint64 val = 0; if (!fu_strtoull(tmp, &val, 0x0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->vendor_id = val; } tmp = xb_node_query_text(n, "device_id", NULL); if (tmp != NULL) { guint64 val = 0; if (!fu_strtoull(tmp, &val, 0x0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->device_id = val; } /* success */ return TRUE; } static void fu_oprom_firmware_init(FuOpromFirmware *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_STORED_SIZE); } static void fu_oprom_firmware_class_init(FuOpromFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_oprom_firmware_validate; firmware_class->export = fu_oprom_firmware_export; firmware_class->parse = fu_oprom_firmware_parse; firmware_class->write = fu_oprom_firmware_write; firmware_class->build = fu_oprom_firmware_build; } /** * fu_oprom_firmware_new: * * Creates a new #FuFirmware of OptionROM format * * Since: 1.8.2 **/ FuFirmware * fu_oprom_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_OPROM_FIRMWARE, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-oprom-firmware.h000066400000000000000000000013741501337203100215010ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * Copyright 2022 Intel * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #include "fu-oprom-struct.h" #define FU_TYPE_OPROM_FIRMWARE (fu_oprom_firmware_get_type()) G_DECLARE_DERIVABLE_TYPE(FuOpromFirmware, fu_oprom_firmware, FU, OPROM_FIRMWARE, FuFirmware) struct _FuOpromFirmwareClass { FuFirmwareClass parent_class; }; FuFirmware * fu_oprom_firmware_new(void); FuOpromMachineType fu_oprom_firmware_get_machine_type(FuOpromFirmware *self) G_GNUC_NON_NULL(1); FuOpromSubsystem fu_oprom_firmware_get_subsystem(FuOpromFirmware *self) G_GNUC_NON_NULL(1); FuOpromCompressionType fu_oprom_firmware_get_compression_type(FuOpromFirmware *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-oprom.rs000066400000000000000000000023201501337203100200540ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[repr(u16le)] enum FuOpromMachineType { X64, } #[repr(u16le)] enum FuOpromSubsystem { EfiBootSrvDrv, } #[repr(u16le)] enum FuOpromCompressionType { None, } #[derive(New, ValidateStream, ParseStream, Default)] #[repr(C, packed)] struct FuStructOprom { signature: u16le == 0xAA55, image_size: u16le, // of 512 bytes init_func_entry_point: u32le, subsystem: FuOpromSubsystem, machine_type: FuOpromMachineType, compression_type: FuOpromCompressionType, _reserved: [u8; 8], efi_image_offset: u16le, pci_header_offset: u16le = $struct_size, expansion_header_offset: u16le, } #[derive(New, ParseStream, Default)] #[repr(C, packed)] struct FuStructOpromPci { signature: u32le == 0x52494350, vendor_id: u16le, device_id: u16le, device_list_pointer: u16le, structure_length: u16le, structure_revision: u8, class_code: u24le, image_length: u16le, // of 512 bytes image_revision: u16le, code_type: u8, indicator: u8, max_runtime_image_length: u16le, conf_util_code_header_pointer: u16le, dmtf_clp_entry_point_pointer: u16le, } fwupd-2.0.10/libfwupdplugin/fu-partial-input-stream-private.h000066400000000000000000000005241501337203100242610ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-partial-input-stream.h" gsize fu_partial_input_stream_get_offset(FuPartialInputStream *self) G_GNUC_NON_NULL(1); gsize fu_partial_input_stream_get_size(FuPartialInputStream *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-partial-input-stream.c000066400000000000000000000163351501337203100226130ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuPartialInputStream" #include "config.h" #include "fwupd-codec.h" #include "fwupd-common-private.h" #include "fu-input-stream.h" #include "fu-partial-input-stream-private.h" /** * FuPartialInputStream: * * A input stream that is a slice of another input stream. * * off sz * [xxxxxxxxxxxx] * | 0x6 | * \ \ * \ \ * \ | * | | * [xxxxxx] * * xxx offset: 2, sz: 6 */ struct _FuPartialInputStream { GInputStream parent_instance; GInputStream *base_stream; gsize offset; gsize size; }; static void fu_partial_input_stream_seekable_iface_init(GSeekableIface *iface); static void fu_partial_input_stream_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_WITH_CODE(FuPartialInputStream, fu_partial_input_stream, G_TYPE_INPUT_STREAM, G_IMPLEMENT_INTERFACE(G_TYPE_SEEKABLE, fu_partial_input_stream_seekable_iface_init) G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fu_partial_input_stream_codec_iface_init)) static void fu_partial_input_stream_add_string(FwupdCodec *codec, guint idt, GString *str) { FuPartialInputStream *self = FU_PARTIAL_INPUT_STREAM(codec); fwupd_codec_string_append_hex(str, idt, "Offset", self->offset); fwupd_codec_string_append_hex(str, idt, "Size", self->size); } static void fu_partial_input_stream_codec_iface_init(FwupdCodecInterface *iface) { iface->add_string = fu_partial_input_stream_add_string; } static goffset fu_partial_input_stream_tell(GSeekable *seekable) { FuPartialInputStream *self = FU_PARTIAL_INPUT_STREAM(seekable); return g_seekable_tell(G_SEEKABLE(self->base_stream)) - self->offset; } static gboolean fu_partial_input_stream_can_seek(GSeekable *seekable) { FuPartialInputStream *self = FU_PARTIAL_INPUT_STREAM(seekable); return g_seekable_can_seek(G_SEEKABLE(self->base_stream)); } static gboolean fu_partial_input_stream_seek(GSeekable *seekable, goffset offset, GSeekType type, GCancellable *cancellable, GError **error) { FuPartialInputStream *self = FU_PARTIAL_INPUT_STREAM(seekable); g_return_val_if_fail(FU_IS_PARTIAL_INPUT_STREAM(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (type == G_SEEK_CUR) { goffset pos = g_seekable_tell(G_SEEKABLE(self->base_stream)); return g_seekable_seek(G_SEEKABLE(self->base_stream), self->offset + pos + offset, G_SEEK_SET, cancellable, error); } if (type == G_SEEK_END) { return g_seekable_seek(G_SEEKABLE(self->base_stream), self->offset + self->size + offset, G_SEEK_SET, cancellable, error); } return g_seekable_seek(G_SEEKABLE(self->base_stream), self->offset + offset, G_SEEK_SET, cancellable, error); } static gboolean fu_partial_input_stream_can_truncate(GSeekable *seekable) { return FALSE; } static gboolean fu_partial_input_stream_truncate(GSeekable *seekable, goffset offset, GCancellable *cancellable, GError **error) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot truncate FuPartialInputStream"); return FALSE; } static void fu_partial_input_stream_seekable_iface_init(GSeekableIface *iface) { iface->tell = fu_partial_input_stream_tell; iface->can_seek = fu_partial_input_stream_can_seek; iface->seek = fu_partial_input_stream_seek; iface->can_truncate = fu_partial_input_stream_can_truncate; iface->truncate_fn = fu_partial_input_stream_truncate; } /** * fu_partial_input_stream_new: * @stream: a base #GInputStream * @offset: offset into @stream * @size: size of @stream in bytes, or %G_MAXSIZE for the "rest" of the stream * @error: (nullable): optional return location for an error * * Creates a partial input stream where content is read from the donor stream. * * Returns: (transfer full): a #FuPartialInputStream, or %NULL on error * * Since: 2.0.0 **/ GInputStream * fu_partial_input_stream_new(GInputStream *stream, gsize offset, gsize size, GError **error) { gsize base_sz = 0; g_autoptr(FuPartialInputStream) self = g_object_new(FU_TYPE_PARTIAL_INPUT_STREAM, NULL); g_return_val_if_fail(G_IS_INPUT_STREAM(stream), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); self->base_stream = g_object_ref(stream); self->offset = offset; /* sanity check */ if (!fu_input_stream_size(stream, &base_sz, error)) { g_prefix_error(error, "failed to get size: "); return NULL; } if (size == G_MAXSIZE) { if (offset > base_sz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "base stream was 0x%x bytes in size " "and tried to create partial stream @0x%x", (guint)base_sz, (guint)offset); return NULL; } self->size = base_sz - offset; } else { if (offset + size > base_sz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "base stream was 0x%x bytes in size, and tried to create " "partial stream @0x%x of 0x%x bytes", (guint)base_sz, (guint)offset, (guint)size); return NULL; } self->size = size; } /* success */ return G_INPUT_STREAM(g_steal_pointer(&self)); } /** * fu_partial_input_stream_get_offset: * @self: a #FuPartialInputStream * * Gets the offset of the stream. * * Returns: integer * * Since: 2.0.0 **/ gsize fu_partial_input_stream_get_offset(FuPartialInputStream *self) { g_return_val_if_fail(FU_IS_PARTIAL_INPUT_STREAM(self), G_MAXSIZE); return self->offset; } /** * fu_partial_input_stream_get_size: * @self: a #FuPartialInputStream * * Gets the offset of the stream. * * Returns: integer * * Since: 2.0.0 **/ gsize fu_partial_input_stream_get_size(FuPartialInputStream *self) { g_return_val_if_fail(FU_IS_PARTIAL_INPUT_STREAM(self), G_MAXSIZE); return self->size; } static gssize fu_partial_input_stream_read(GInputStream *stream, void *buffer, gsize count, GCancellable *cancellable, GError **error) { FuPartialInputStream *self = FU_PARTIAL_INPUT_STREAM(stream); g_return_val_if_fail(FU_IS_PARTIAL_INPUT_STREAM(self), -1); g_return_val_if_fail(error == NULL || *error == NULL, -1); if (self->size < (gsize)g_seekable_tell(G_SEEKABLE(stream))) { g_warning("base stream is outside seekable range"); return 0; } count = MIN(count, self->size - g_seekable_tell(G_SEEKABLE(stream))); return g_input_stream_read(self->base_stream, buffer, count, cancellable, error); } static void fu_partial_input_stream_finalize(GObject *object) { FuPartialInputStream *self = FU_PARTIAL_INPUT_STREAM(object); if (self->base_stream != NULL) g_object_unref(self->base_stream); G_OBJECT_CLASS(fu_partial_input_stream_parent_class)->finalize(object); } static void fu_partial_input_stream_class_init(FuPartialInputStreamClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); GInputStreamClass *istream_class = G_INPUT_STREAM_CLASS(klass); istream_class->read_fn = fu_partial_input_stream_read; object_class->finalize = fu_partial_input_stream_finalize; } static void fu_partial_input_stream_init(FuPartialInputStream *self) { } fwupd-2.0.10/libfwupdplugin/fu-partial-input-stream.h000066400000000000000000000007511501337203100226130ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_PARTIAL_INPUT_STREAM (fu_partial_input_stream_get_type()) G_DECLARE_FINAL_TYPE(FuPartialInputStream, fu_partial_input_stream, FU, PARTIAL_INPUT_STREAM, GInputStream) GInputStream * fu_partial_input_stream_new(GInputStream *stream, gsize offset, gsize size, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-path.c000066400000000000000000000446721501337203100174720ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuCommon" #include "config.h" #include #include #ifdef _WIN32 #include #endif #include "fwupd-error.h" #include "fu-common.h" #include "fu-path.h" /** * fu_path_rmtree: * @directory: a directory name * @error: (nullable): optional return location for an error * * Recursively removes a directory. * * Returns: %TRUE for success, %FALSE otherwise * * Since: 1.8.2 **/ gboolean fu_path_rmtree(const gchar *directory, GError **error) { const gchar *filename; g_autoptr(GDir) dir = NULL; g_return_val_if_fail(directory != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* try to open */ g_debug("removing %s", directory); dir = g_dir_open(directory, 0, error); if (dir == NULL) return FALSE; /* find each */ while ((filename = g_dir_read_name(dir))) { g_autofree gchar *src = NULL; src = g_build_filename(directory, filename, NULL); if (g_file_test(src, G_FILE_TEST_IS_DIR)) { if (!fu_path_rmtree(src, error)) return FALSE; } else { if (g_unlink(src) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to delete: %s", src); return FALSE; } } } if (g_remove(directory) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to delete: %s", directory); return FALSE; } return TRUE; } static gboolean fu_path_get_file_list_internal(GPtrArray *files, const gchar *directory, GError **error) { const gchar *filename; g_autoptr(GDir) dir = NULL; /* try to open */ dir = g_dir_open(directory, 0, error); if (dir == NULL) return FALSE; /* find each */ while ((filename = g_dir_read_name(dir))) { g_autofree gchar *src = g_build_filename(directory, filename, NULL); if (g_file_test(src, G_FILE_TEST_IS_SYMLINK)) continue; if (g_file_test(src, G_FILE_TEST_IS_DIR)) { if (!fu_path_get_file_list_internal(files, src, error)) return FALSE; } else { g_ptr_array_add(files, g_steal_pointer(&src)); } } return TRUE; } /** * fu_path_get_files: * @path: a directory name * @error: (nullable): optional return location for an error * * Returns every file found under @directory, and any subdirectory. * If any path under @directory cannot be accessed due to permissions an error * will be returned. * * Returns: (transfer container) (element-type utf8): array of files, or %NULL for error * * Since: 1.8.2 **/ GPtrArray * fu_path_get_files(const gchar *path, GError **error) { g_autoptr(GPtrArray) files = g_ptr_array_new_with_free_func(g_free); g_return_val_if_fail(path != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); if (!fu_path_get_file_list_internal(files, path, error)) return NULL; return g_steal_pointer(&files); } /** * fu_path_mkdir: * @dirname: a directory name * @error: (nullable): optional return location for an error * * Creates any required directories, including any parent directories. * * Returns: %TRUE for success * * Since: 1.8.2 **/ gboolean fu_path_mkdir(const gchar *dirname, GError **error) { g_return_val_if_fail(dirname != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!g_file_test(dirname, G_FILE_TEST_IS_DIR)) g_debug("creating path %s", dirname); if (g_mkdir_with_parents(dirname, 0755) == -1) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to create '%s': %s", dirname, g_strerror(errno)); return FALSE; } return TRUE; } /** * fu_path_mkdir_parent: * @filename: a full pathname * @error: (nullable): optional return location for an error * * Creates any required directories, including any parent directories. * * Returns: %TRUE for success * * Since: 1.8.2 **/ gboolean fu_path_mkdir_parent(const gchar *filename, GError **error) { g_autofree gchar *parent = NULL; g_return_val_if_fail(filename != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); parent = g_path_get_dirname(filename); return fu_path_mkdir(parent, error); } /** * fu_path_find_program: * @basename: the program to search * @error: (nullable): optional return location for an error * * Looks for a program in the PATH variable * * Returns: a new #gchar, or %NULL for error * * Since: 1.8.2 **/ gchar * fu_path_find_program(const gchar *basename, GError **error) { gchar *fn = g_find_program_in_path(basename); if (fn == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "missing executable %s in PATH", basename); return NULL; } return fn; } /** * fu_path_get_win32_basedir: * * Gets the base directory that fwupd has been launched from on Windows. * This is the directory containing all subdirectories (IE 'C:\Program Files (x86)\fwupd\') * * Returns: The system path, or %NULL if invalid * * Since: 1.8.2 **/ static gchar * fu_path_get_win32_basedir(void) { #ifdef _WIN32 char drive_buf[_MAX_DRIVE]; char dir_buf[_MAX_DIR]; _splitpath(_pgmptr, drive_buf, dir_buf, NULL, NULL); return g_build_filename(drive_buf, dir_buf, "..", NULL); #endif return NULL; } /** * fu_path_from_kind: * @path_kind: a #FuPathKind e.g. %FU_PATH_KIND_DATADIR_PKG * * Gets a fwupd-specific system path. These can be overridden with various * environment variables, for instance %FWUPD_DATADIR. * * Returns: a system path, or %NULL if invalid * * Since: 1.8.2 **/ gchar * fu_path_from_kind(FuPathKind path_kind) { const gchar *tmp; g_autofree gchar *basedir = NULL; switch (path_kind) { /* /var */ case FU_PATH_KIND_LOCALSTATEDIR: tmp = g_getenv("FWUPD_LOCALSTATEDIR"); if (tmp != NULL) return g_strdup(tmp); #ifdef _WIN32 return g_build_filename(g_getenv("USERPROFILE"), PACKAGE_NAME, FWUPD_LOCALSTATEDIR, NULL); #else tmp = g_getenv("SNAP_COMMON"); if (tmp != NULL) return g_build_filename(tmp, FWUPD_LOCALSTATEDIR, NULL); return g_build_filename(FWUPD_LOCALSTATEDIR, NULL); #endif /* /proc */ case FU_PATH_KIND_PROCFS: tmp = g_getenv("FWUPD_PROCFS"); if (tmp != NULL) return g_strdup(tmp); return g_strdup("/proc"); /* /sys */ case FU_PATH_KIND_SYSFSDIR: tmp = g_getenv("FWUPD_SYSFSDIR"); if (tmp != NULL) return g_strdup(tmp); return g_strdup("/sys"); /* /sys/firmware */ case FU_PATH_KIND_SYSFSDIR_FW: tmp = g_getenv("FWUPD_SYSFSFWDIR"); if (tmp != NULL) return g_strdup(tmp); basedir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR); return g_build_filename(basedir, "firmware", NULL); /* /sys/class/tpm */ case FU_PATH_KIND_SYSFSDIR_TPM: tmp = g_getenv("FWUPD_SYSFSTPMDIR"); if (tmp != NULL) return g_strdup(tmp); basedir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR); return g_build_filename(basedir, "class", "tpm", NULL); /* /sys/bus/platform/drivers */ case FU_PATH_KIND_SYSFSDIR_DRIVERS: tmp = g_getenv("FWUPD_SYSFSDRIVERDIR"); if (tmp != NULL) return g_strdup(tmp); basedir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR); return g_build_filename(basedir, "bus", "platform", "drivers", NULL); /* /sys/kernel/security */ case FU_PATH_KIND_SYSFSDIR_SECURITY: tmp = g_getenv("FWUPD_SYSFSSECURITYDIR"); if (tmp != NULL) return g_strdup(tmp); basedir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR); return g_build_filename(basedir, "kernel", "security", NULL); /* /sys/class/dmi/id */ case FU_PATH_KIND_SYSFSDIR_DMI: tmp = g_getenv("FWUPD_SYSFSDMIDIR"); if (tmp != NULL) return g_strdup(tmp); basedir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR); return g_build_filename(basedir, "class", "dmi", "id", NULL); /* /sys/firmware/acpi/tables */ case FU_PATH_KIND_ACPI_TABLES: tmp = g_getenv("FWUPD_ACPITABLESDIR"); if (tmp != NULL) return g_strdup(tmp); basedir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR); return g_build_filename(basedir, "firmware", "acpi", "tables", NULL); /* /sys/module/firmware_class/parameters/path */ case FU_PATH_KIND_FIRMWARE_SEARCH: tmp = g_getenv("FWUPD_FIRMWARESEARCH"); if (tmp != NULL) return g_strdup(tmp); basedir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR); return g_build_filename(basedir, "module", "firmware_class", "parameters", "path", NULL); /* /etc */ case FU_PATH_KIND_SYSCONFDIR: tmp = g_getenv("FWUPD_SYSCONFDIR"); if (tmp != NULL) return g_strdup(tmp); tmp = g_getenv("SNAP"); if (tmp != NULL) return g_build_filename(tmp, FWUPD_SYSCONFDIR, NULL); basedir = fu_path_get_win32_basedir(); if (basedir != NULL) return g_build_filename(basedir, FWUPD_SYSCONFDIR, NULL); return g_strdup(FWUPD_SYSCONFDIR); /* /usr/libexec/ */ case FU_PATH_KIND_LIBEXECDIR: tmp = g_getenv("FWUPD_LIBEXECDIR"); if (tmp != NULL) return g_strdup(tmp); tmp = g_getenv("SNAP"); if (tmp != NULL) return g_build_filename(tmp, FWUPD_LIBEXECDIR, NULL); return g_strdup(FWUPD_LIBEXECDIR); /* /usr/lib//fwupd-#VERSION# */ case FU_PATH_KIND_LIBDIR_PKG: tmp = g_getenv("FWUPD_LIBDIR_PKG"); if (tmp != NULL) return g_strdup(tmp); tmp = g_getenv("SNAP"); if (tmp != NULL) return g_build_filename(tmp, FWUPD_LIBDIR_PKG, NULL); basedir = fu_path_get_win32_basedir(); if (basedir != NULL) return g_build_filename(basedir, FWUPD_LIBDIR_PKG, NULL); return g_build_filename(FWUPD_LIBDIR_PKG, NULL); /* /usr/share/fwupd */ case FU_PATH_KIND_DATADIR_PKG: tmp = g_getenv("FWUPD_DATADIR"); if (tmp != NULL) return g_strdup(tmp); tmp = g_getenv("SNAP"); if (tmp != NULL) return g_build_filename(tmp, FWUPD_DATADIR, PACKAGE_NAME, NULL); basedir = fu_path_get_win32_basedir(); if (basedir != NULL) return g_build_filename(basedir, FWUPD_DATADIR, PACKAGE_NAME, NULL); return g_build_filename(FWUPD_DATADIR, PACKAGE_NAME, NULL); /* /usr/libexec/fwupd */ case FU_PATH_KIND_LIBEXECDIR_PKG: tmp = g_getenv("FWUPD_LIBEXECDIR_PKG"); if (tmp != NULL) return g_strdup(tmp); tmp = g_getenv("SNAP"); if (tmp != NULL) return g_build_filename(tmp, FWUPD_LIBEXECDIR, PACKAGE_NAME, NULL); return g_build_filename(FWUPD_LIBEXECDIR, PACKAGE_NAME, NULL); /* /usr/share/hwdata */ case FU_PATH_KIND_DATADIR_VENDOR_IDS: tmp = g_getenv("FWUPD_DATADIR_VENDOR_IDS"); if (tmp != NULL) return g_strdup(tmp); tmp = g_getenv("SNAP"); if (tmp != NULL) return g_build_filename(tmp, FWUPD_DATADIR_VENDOR_IDS, NULL); return g_strdup(FWUPD_DATADIR_VENDOR_IDS); /* /usr/share/fwupd/quirks.d */ case FU_PATH_KIND_DATADIR_QUIRKS: tmp = g_getenv("FWUPD_DATADIR_QUIRKS"); if (tmp != NULL) return g_strdup(tmp); basedir = fu_path_from_kind(FU_PATH_KIND_DATADIR_PKG); return g_build_filename(basedir, "quirks.d", NULL); /* /usr/libexec/fwupd/efi */ case FU_PATH_KIND_EFIAPPDIR: tmp = g_getenv("FWUPD_EFIAPPDIR"); if (tmp != NULL) return g_strdup(tmp); #ifdef EFI_APP_LOCATION tmp = g_getenv("SNAP"); if (tmp != NULL) return g_build_filename(tmp, EFI_APP_LOCATION, NULL); return g_strdup(EFI_APP_LOCATION); #else return NULL; #endif /* /etc/fwupd */ case FU_PATH_KIND_SYSCONFDIR_PKG: tmp = g_getenv("CONFIGURATION_DIRECTORY"); if (tmp != NULL && g_file_test(tmp, G_FILE_TEST_EXISTS)) return g_build_filename(tmp, NULL); basedir = fu_path_from_kind(FU_PATH_KIND_SYSCONFDIR); return g_build_filename(basedir, PACKAGE_NAME, NULL); /* /var/lib/fwupd */ case FU_PATH_KIND_LOCALSTATEDIR_PKG: tmp = g_getenv("STATE_DIRECTORY"); if (tmp != NULL && g_file_test(tmp, G_FILE_TEST_EXISTS)) return g_build_filename(tmp, NULL); basedir = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR); return g_build_filename(basedir, "lib", PACKAGE_NAME, NULL); /* /var/lib/fwupd/quirks.d */ case FU_PATH_KIND_LOCALSTATEDIR_QUIRKS: tmp = g_getenv("FWUPD_LOCALSTATEDIR_QUIRKS"); if (tmp != NULL) return g_build_filename(tmp, NULL); basedir = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR_PKG); return g_build_filename(basedir, "quirks.d", NULL); /* /var/lib/fwupd/metadata */ case FU_PATH_KIND_LOCALSTATEDIR_METADATA: tmp = g_getenv("FWUPD_LOCALSTATEDIR_METADATA"); if (tmp != NULL) return g_build_filename(tmp, NULL); basedir = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR_PKG); return g_build_filename(basedir, "metadata", NULL); /* /var/lib/fwupd/remotes.d */ case FU_PATH_KIND_LOCALSTATEDIR_REMOTES: tmp = g_getenv("FWUPD_LOCALSTATEDIR_REMOTES"); if (tmp != NULL) return g_build_filename(tmp, NULL); basedir = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR_PKG); return g_build_filename(basedir, "remotes.d", NULL); /* /var/cache/fwupd */ case FU_PATH_KIND_CACHEDIR_PKG: tmp = g_getenv("CACHE_DIRECTORY"); if (tmp != NULL && g_file_test(tmp, G_FILE_TEST_EXISTS)) return g_build_filename(tmp, NULL); basedir = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR); return g_build_filename(basedir, "cache", PACKAGE_NAME, NULL); /* /var/etc/fwupd */ case FU_PATH_KIND_LOCALCONFDIR_PKG: tmp = g_getenv("LOCALCONF_DIRECTORY"); if (tmp != NULL && g_file_test(tmp, G_FILE_TEST_EXISTS)) return g_build_filename(tmp, NULL); basedir = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR); return g_build_filename(basedir, "etc", PACKAGE_NAME, NULL); /* /run/lock */ case FU_PATH_KIND_LOCKDIR: tmp = g_getenv("FWUPD_LOCKDIR"); if (tmp != NULL) return g_strdup(tmp); if (g_file_test("/run/lock", G_FILE_TEST_EXISTS)) return g_strdup("/run/lock"); return g_strdup("/var/run"); /* /sys/class/firmware-attributes */ case FU_PATH_KIND_SYSFSDIR_FW_ATTRIB: tmp = g_getenv("FWUPD_SYSFSFWATTRIBDIR"); if (tmp != NULL) return g_strdup(tmp); basedir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR); return g_build_filename(basedir, "class", "firmware-attributes", NULL); case FU_PATH_KIND_POLKIT_ACTIONS: #ifdef POLKIT_ACTIONDIR return g_strdup(POLKIT_ACTIONDIR); #else return NULL; #endif /* C:\Program Files (x86)\fwupd\ */ case FU_PATH_KIND_WIN32_BASEDIR: return fu_path_get_win32_basedir(); /* / */ case FU_PATH_KIND_HOSTFS_ROOT: tmp = g_getenv("FWUPD_HOSTFS_ROOT"); if (tmp != NULL) return g_strdup(tmp); return g_strdup("/"); /* /boot */ case FU_PATH_KIND_HOSTFS_BOOT: tmp = g_getenv("FWUPD_HOSTFS_BOOT"); if (tmp != NULL) return g_strdup(tmp); return g_strdup("/boot"); /* /dev */ case FU_PATH_KIND_DEVFS: tmp = g_getenv("FWUPD_DEVFS"); if (tmp != NULL) return g_strdup(tmp); return g_strdup("/dev"); /* /etc/localtime or /var/lib/timezone/localtime */ case FU_PATH_KIND_LOCALTIME: { g_autofree gchar *sysconfdir = fu_path_from_kind(FU_PATH_KIND_SYSCONFDIR); g_autofree gchar *localstatedir = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR); g_autofree gchar *localtime = NULL; tmp = g_getenv("FWUPD_LOCALTIME"); if (tmp != NULL) return g_strdup(tmp); basedir = g_build_filename(localstatedir, "lib", "timezone", "localtime", NULL); if (g_file_test(basedir, G_FILE_TEST_EXISTS)) return g_steal_pointer(&basedir); localtime = g_build_filename(sysconfdir, "localtime", NULL); if (g_file_test(localtime, G_FILE_TEST_EXISTS)) return g_steal_pointer(&localtime); return g_strdup("/etc/localtime"); } /* this shouldn't happen */ default: g_warning("cannot build path for unknown kind %u", path_kind); } return NULL; } static gint fu_path_glob_sort_cb(gconstpointer a, gconstpointer b) { return g_strcmp0(*(const gchar **)a, *(const gchar **)b); } /** * fu_path_glob: * @directory: a directory path * @pattern: a glob pattern, e.g. `*foo*` * @error: (nullable): optional return location for an error * * Returns all the filenames that match a specific glob pattern. * Any results are sorted. No matching files will set @error. * * Returns: (element-type utf8) (transfer container): matching files, or %NULL * * Since: 1.8.2 **/ GPtrArray * fu_path_glob(const gchar *directory, const gchar *pattern, GError **error) { const gchar *basename; g_autoptr(GDir) dir = NULL; g_autoptr(GPtrArray) files = g_ptr_array_new_with_free_func(g_free); g_return_val_if_fail(directory != NULL, NULL); g_return_val_if_fail(pattern != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); dir = g_dir_open(directory, 0, error); if (dir == NULL) return NULL; while ((basename = g_dir_read_name(dir)) != NULL) { if (!g_pattern_match_simple(pattern, basename)) continue; g_ptr_array_add(files, g_build_filename(directory, basename, NULL)); } if (files->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no files matched pattern"); return NULL; } g_ptr_array_sort(files, fu_path_glob_sort_cb); return g_steal_pointer(&files); } /** * fu_path_make_absolute: * @filename: a path to a filename, perhaps symlinked * @error: (nullable): optional return location for an error * * Returns the resolved absolute file name. * * Returns: (transfer full): path, or %NULL on error * * Since: 2.0.0 **/ gchar * fu_path_make_absolute(const gchar *filename, GError **error) { char full_tmp[PATH_MAX]; g_return_val_if_fail(filename != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); #ifdef HAVE_REALPATH if (realpath(filename, full_tmp) == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "cannot resolve path: %s", g_strerror(errno)); return NULL; } #else if (_fullpath(full_tmp, filename, sizeof(full_tmp)) == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "cannot resolve path: %s", g_strerror(errno)); return NULL; } #endif if (!g_file_test(full_tmp, G_FILE_TEST_EXISTS)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "cannot find path: %s", full_tmp); return NULL; } return g_strdup(full_tmp); } /** * fu_path_get_symlink_target: * @filename: a path to a symlink * @error: (nullable): optional return location for an error * * Returns the symlink target. * * Returns: (transfer full): path, or %NULL on error * * Since: 2.0.0 **/ gchar * fu_path_get_symlink_target(const gchar *filename, GError **error) { const gchar *target; g_autoptr(GFile) file = NULL; g_autoptr(GFileInfo) info = NULL; file = g_file_new_for_path(filename); info = g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, error); if (info == NULL) { fu_error_convert(error); return NULL; } target = g_file_info_get_attribute_byte_string(info, G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET); if (target == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no symlink target"); return NULL; } /* success */ return g_strdup(target); } fwupd-2.0.10/libfwupdplugin/fu-path.h000066400000000000000000000117401501337203100174650ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include /** * FuPathKind: * @FU_PATH_KIND_CACHEDIR_PKG: The cache directory (IE /var/cache/fwupd) * @FU_PATH_KIND_DATADIR_PKG: The non-volatile data store (IE /usr/share/fwupd) * @FU_PATH_KIND_EFIAPPDIR: The location to store EFI apps before install (IE * /usr/libexec/fwupd/efi) * @FU_PATH_KIND_LOCALSTATEDIR: The local state directory (IE /var) * @FU_PATH_KIND_LOCALSTATEDIR_PKG: The local state directory for the package (IE * /var/lib/fwupd) * @FU_PATH_KIND_LIBDIR_PKG: The location to look for plugins for package (IE * /usr/lib/[triplet]/fwupd-plugins-3) * @FU_PATH_KIND_SYSCONFDIR: The configuration location (IE /etc) * @FU_PATH_KIND_SYSCONFDIR_PKG: The package configuration location (IE /etc/fwupd) * @FU_PATH_KIND_SYSFSDIR: The sysfs base location (IE /sys) * @FU_PATH_KIND_SYSFSDIR_FW: The sysfs firmware location (IE /sys/firmware) * @FU_PATH_KIND_SYSFSDIR_DRIVERS: The platform sysfs directory (IE /sys/bus/platform/drivers) * @FU_PATH_KIND_SYSFSDIR_TPM: The TPM sysfs directory (IE /sys/class/tpm) * @FU_PATH_KIND_PROCFS: The procfs location (IE /proc) * @FU_PATH_KIND_POLKIT_ACTIONS: The directory for policy kit actions (IE * /usr/share/polkit-1/actions/) * @FU_PATH_KIND_SYSFSDIR_SECURITY: The sysfs security location (IE /sys/kernel/security) * @FU_PATH_KIND_ACPI_TABLES: The location of the ACPI tables * @FU_PATH_KIND_LOCKDIR: The lock directory (IE /run/lock) * @FU_PATH_KIND_SYSFSDIR_FW_ATTRIB The firmware attributes directory (IE * /sys/class/firmware-attributes) * @FU_PATH_KIND_FIRMWARE_SEARCH: The path to configure the kernel policy for runtime loading *other than /lib/firmware (IE /sys/module/firmware_class/parameters/path) * @FU_PATH_KIND_DATADIR_QUIRKS: The quirks data store (IE /usr/share/fwupd/quirks.d) * @FU_PATH_KIND_LOCALSTATEDIR_QUIRKS: The local state directory for quirks (IE * /var/lib/fwupd/quirks.d) * @FU_PATH_KIND_LOCALSTATEDIR_METADATA: The local state directory for metadata (IE * /var/lib/fwupd/metadata) * @FU_PATH_KIND_LOCALSTATEDIR_REMOTES: The local state directory for remotes (IE * /var/lib/fwupd/remotes.d) * @FU_PATH_KIND_WIN32_BASEDIR: The root of the install directory on Windows * @FU_PATH_KIND_LOCALCONFDIR_PKG: The package configuration override (IE /var/etc/fwupd) * @FU_PATH_KIND_SYSFSDIR_DMI: The sysfs DMI location, (IE /sys/class/dmi/id) * @FU_PATH_KIND_HOSTFS_ROOT: The root of the host filesystem (IE /) * @FU_PATH_KIND_HOSTFS_BOOT: The host boot directory, (IE /boot) * @FU_PATH_KIND_DEVFS: The host dev directory, (IE /dev) * @FU_PATH_KIND_LOCALTIME: The timezone symlink (IE /etc/localtime) * @FU_PATH_KIND_LIBEXECDIR: The directory to launch executables * @FU_PATH_KIND_LIBEXECDIR_PKG The directory launch executables packaged with daemon * @FU_PATH_KIND_DATADIR_VENDOR_IDS: The vendor ID store (IE /usr/share/hwdata) * * Path types to use when dynamically determining a path at runtime **/ typedef enum { FU_PATH_KIND_CACHEDIR_PKG, FU_PATH_KIND_DATADIR_PKG, FU_PATH_KIND_EFIAPPDIR, FU_PATH_KIND_LOCALSTATEDIR, FU_PATH_KIND_LOCALSTATEDIR_PKG, FU_PATH_KIND_LIBDIR_PKG, FU_PATH_KIND_SYSCONFDIR, FU_PATH_KIND_SYSCONFDIR_PKG, FU_PATH_KIND_SYSFSDIR, FU_PATH_KIND_SYSFSDIR_FW, FU_PATH_KIND_SYSFSDIR_DRIVERS, FU_PATH_KIND_SYSFSDIR_TPM, FU_PATH_KIND_PROCFS, FU_PATH_KIND_POLKIT_ACTIONS, FU_PATH_KIND_SYSFSDIR_SECURITY, FU_PATH_KIND_ACPI_TABLES, FU_PATH_KIND_LOCKDIR, FU_PATH_KIND_SYSFSDIR_FW_ATTRIB, FU_PATH_KIND_FIRMWARE_SEARCH, FU_PATH_KIND_DATADIR_QUIRKS, FU_PATH_KIND_LOCALSTATEDIR_QUIRKS, FU_PATH_KIND_LOCALSTATEDIR_METADATA, FU_PATH_KIND_LOCALSTATEDIR_REMOTES, FU_PATH_KIND_WIN32_BASEDIR, FU_PATH_KIND_LOCALCONFDIR_PKG, FU_PATH_KIND_SYSFSDIR_DMI, FU_PATH_KIND_HOSTFS_ROOT, FU_PATH_KIND_HOSTFS_BOOT, FU_PATH_KIND_DEVFS, FU_PATH_KIND_LOCALTIME, FU_PATH_KIND_LIBEXECDIR, FU_PATH_KIND_LIBEXECDIR_PKG, FU_PATH_KIND_DATADIR_VENDOR_IDS, /*< private >*/ FU_PATH_KIND_LAST } FuPathKind; gchar * fu_path_from_kind(FuPathKind path_kind); GPtrArray * fu_path_glob(const gchar *directory, const gchar *pattern, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_path_rmtree(const gchar *directory, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); GPtrArray * fu_path_get_files(const gchar *path, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_path_mkdir(const gchar *dirname, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_path_mkdir_parent(const gchar *filename, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gchar * fu_path_find_program(const gchar *basename, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gchar * fu_path_make_absolute(const gchar *filename, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gchar * fu_path_get_symlink_target(const gchar *filename, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-pci-device.c000066400000000000000000000403561501337203100205410ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuPciDevice" #include "config.h" #include "fu-device-private.h" #include "fu-pci-device.h" #include "fu-pci-struct.h" #include "fu-quirks.h" #include "fu-string.h" /** * FuPciDevice * * See also: #FuUdevDevice */ typedef struct { guint8 revision; guint32 class; guint16 subsystem_vid; guint16 subsystem_pid; } FuPciDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuPciDevice, fu_pci_device, FU_TYPE_UDEV_DEVICE) #define GET_PRIVATE(o) (fu_pci_device_get_instance_private(o)) static void fu_pci_device_to_string(FuDevice *device, guint idt, GString *str) { FuPciDevice *self = FU_PCI_DEVICE(device); FuPciDevicePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append_hex(str, idt, "Revision", priv->revision); fwupd_codec_string_append_hex(str, idt, "Class", priv->class); fwupd_codec_string_append_hex(str, idt, "SubsystemVendor", priv->subsystem_vid); fwupd_codec_string_append_hex(str, idt, "SubsystemModel", priv->subsystem_pid); } static void fu_pci_device_ensure_subsys_instance_id(FuPciDevice *self) { FuPciDevicePrivate *priv = GET_PRIVATE(self); g_autofree gchar *subsys = NULL; /* not usable */ if (priv->subsystem_vid == 0x0 || priv->subsystem_pid == 0x0) return; /* a weird format, but copy Windows 10... */ subsys = g_strdup_printf("%04X%04X", priv->subsystem_vid, priv->subsystem_pid); fu_device_add_instance_str(FU_DEVICE(self), "SUBSYS", subsys); } /** * fu_pci_device_set_subsystem_vid: * @self: a #FuPciDevice * @subsystem_vid: integer * * Sets the device subsystem vendor code. * * Since: 2.0.0 **/ void fu_pci_device_set_subsystem_vid(FuPciDevice *self, guint16 subsystem_vid) { FuPciDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_PCI_DEVICE(self)); if (priv->subsystem_vid == subsystem_vid) return; priv->subsystem_vid = subsystem_vid; fu_pci_device_ensure_subsys_instance_id(self); } /** * fu_pci_device_get_subsystem_vid: * @self: a #FuPciDevice * * Gets the device subsystem vendor code. * * Returns: a vendor code, or 0 if unset or invalid * * Since: 2.0.0 **/ guint16 fu_pci_device_get_subsystem_vid(FuPciDevice *self) { FuPciDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_PCI_DEVICE(self), 0x0000); return priv->subsystem_vid; } /** * fu_pci_device_set_subsystem_pid: * @self: a #FuPciDevice * @subsystem_pid: integer * * Sets the device subsystem model code. * * Since: 2.0.0 **/ void fu_pci_device_set_subsystem_pid(FuPciDevice *self, guint16 subsystem_pid) { FuPciDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_PCI_DEVICE(self)); if (priv->subsystem_pid == subsystem_pid) return; priv->subsystem_pid = subsystem_pid; fu_pci_device_ensure_subsys_instance_id(self); } /** * fu_pci_device_get_subsystem_pid: * @self: a #FuPciDevice * * Gets the device subsystem model code. * * Returns: a vendor code, or 0 if unset or invalid * * Since: 1.5.0 **/ guint16 fu_pci_device_get_subsystem_pid(FuPciDevice *self) { FuPciDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_PCI_DEVICE(self), 0x0000); return priv->subsystem_pid; } /** * fu_pci_device_set_revision: * @self: a #FuPciDevice * @revision: integer * * Sets the device revision. * * Since: 2.0.0 **/ void fu_pci_device_set_revision(FuPciDevice *self, guint8 revision) { FuPciDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_PCI_DEVICE(self)); priv->revision = revision; fu_device_add_instance_u8(FU_DEVICE(self), "REV", priv->revision); } /** * fu_pci_device_get_revision: * @self: a #FuPciDevice * * Gets the device revision. * * Returns: a vendor code, or 0 if unset or invalid * * Since: 1.1.2 **/ guint8 fu_pci_device_get_revision(FuPciDevice *self) { FuPciDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_PCI_DEVICE(self), 0x00); return priv->revision; } static void fu_pci_device_to_incorporate(FuDevice *self, FuDevice *donor) { FuPciDevice *uself = FU_PCI_DEVICE(self); FuPciDevice *udonor = FU_PCI_DEVICE(donor); FuPciDevicePrivate *priv = GET_PRIVATE(uself); FuPciDevicePrivate *priv_donor = GET_PRIVATE(udonor); g_return_if_fail(FU_IS_PCI_DEVICE(self)); g_return_if_fail(FU_IS_PCI_DEVICE(donor)); if (priv->class == 0x0) priv->class = priv_donor->class; if (priv->subsystem_vid == 0x0) fu_pci_device_set_subsystem_vid(uself, fu_pci_device_get_subsystem_vid(udonor)); if (priv->subsystem_pid == 0x0) fu_pci_device_set_subsystem_pid(uself, fu_pci_device_get_subsystem_pid(udonor)); if (priv->revision == 0x0) fu_pci_device_set_revision(uself, fu_pci_device_get_revision(udonor)); } static gboolean fu_pci_device_probe(FuDevice *device, GError **error) { FuPciDevice *self = FU_PCI_DEVICE(device); FuPciDevicePrivate *priv = GET_PRIVATE(self); g_autofree gchar *attr_class = NULL; g_autofree gchar *attr_revision = NULL; g_autofree gchar *attr_subsystem_device = NULL; g_autofree gchar *attr_subsystem_vendor = NULL; g_autofree gchar *physical_id = NULL; g_autofree gchar *prop_slot = NULL; g_autofree gchar *subsystem = NULL; /* FuUdevDevice->probe */ if (!FU_DEVICE_CLASS(fu_pci_device_parent_class)->probe(device, error)) return FALSE; /* needed for instance IDs further down */ subsystem = g_ascii_strup(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(self)), -1); /* PCI class code */ attr_class = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(self), "class", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (attr_class != NULL) { guint64 class_u64 = 0; g_autoptr(GError) error_local = NULL; if (!fu_strtoull(attr_class, &class_u64, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, &error_local)) { g_warning("reading class for %s was invalid: %s", attr_class, error_local->message); } else { priv->class = (guint32)class_u64; } } /* if the device is a GPU try to fetch it from vbios_version */ if ((priv->class >> 16) == FU_PCI_DEVICE_BASE_CLS_DISPLAY && fu_device_get_version(device) == NULL) { g_autofree gchar *version = NULL; version = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(self), "vbios_version", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (version != NULL) { fu_device_set_version(device, version); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_PLAIN); } } /* set the version if the revision has been set */ attr_revision = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(self), "revision", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (attr_revision != NULL) { guint64 tmp64 = 0; if (!fu_strtoull(attr_revision, &tmp64, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_pci_device_set_revision(self, (guint8)tmp64); } if (fu_device_get_version(device) == NULL && fu_device_get_version_format(device) == FWUPD_VERSION_FORMAT_UNKNOWN) { if (priv->revision != 0x00 && priv->revision != 0xFF) { g_autofree gchar *version = g_strdup_printf("%02x", priv->revision); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_version(device, version); } } if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_ADD_INSTANCE_ID_REV) && priv->revision != 0xFF) { if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_ADD_INSTANCE_ID_REV)) { fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_VISIBLE | FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, subsystem, "VEN", "DEV", "REV", NULL); } } /* subsystem IDs */ attr_subsystem_vendor = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(self), "subsystem_vendor", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (attr_subsystem_vendor != NULL) { guint64 tmp64 = 0; if (!fu_strtoull(attr_subsystem_vendor, &tmp64, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->subsystem_vid = (guint16)tmp64; } attr_subsystem_device = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(self), "subsystem_device", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (attr_subsystem_device != NULL) { guint64 tmp64 = 0; if (!fu_strtoull(attr_subsystem_device, &tmp64, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->subsystem_pid = (guint16)tmp64; } if (priv->subsystem_vid != 0x0000 || priv->subsystem_pid != 0x0000) { fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_VISIBLE | FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, subsystem, "VEN", "DEV", "SUBSYS", NULL); if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_ADD_INSTANCE_ID_REV)) { fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_VISIBLE | FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, subsystem, "VEN", "DEV", "SUBSYS", "REV", NULL); } } /* physical slot */ prop_slot = fu_udev_device_read_property(FU_UDEV_DEVICE(self), "PCI_SLOT_NAME", error); if (prop_slot == NULL) return FALSE; physical_id = g_strdup_printf("PCI_SLOT_NAME=%s", prop_slot); fu_device_set_physical_id(device, physical_id); /* success */ fu_pci_device_ensure_subsys_instance_id(self); return TRUE; } static void fu_pci_device_set_quirks_fallback(FuPciDevice *self, guint16 base) { if (base == FU_PCI_DEVICE_BASE_CLS_MASS_STORAGE) { fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_NAME, "Mass Storage Device", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_ICON, "drive-harddisk-solidstate", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); return; } if (base == FU_PCI_DEVICE_BASE_CLS_NETWORK) { fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_NAME, "Network Device", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_ICON, "network-wired", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); return; } if (base == FU_PCI_DEVICE_BASE_CLS_DISPLAY) { fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_NAME, "Display Device", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_ICON, "video-display", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); return; } if (base == FU_PCI_DEVICE_BASE_CLS_MULTIMEDIA) { fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_NAME, "Multimedia Device", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_ICON, "audio-card", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); return; } if (base == FU_PCI_DEVICE_BASE_CLS_MEMORY) { fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_NAME, "Memory Device", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_ICON, "drive-harddisk-solidstate", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); return; } if (base == FU_PCI_DEVICE_BASE_CLS_BRIDGE) { fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_NAME, "Bridge Device", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_ICON, "dock", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); return; } if (base == FU_PCI_DEVICE_BASE_CLS_SIMPLE_COMMUNICATION) { fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_NAME, "Simple Communication Device", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_ICON, "network-wired", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); return; } if (base == FU_PCI_DEVICE_BASE_CLS_BASE) { fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_NAME, "Base Device", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); return; } if (base == FU_PCI_DEVICE_BASE_CLS_INPUT) { fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_NAME, "Input Device", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); return; } if (base == FU_PCI_DEVICE_BASE_CLS_DOCKING) { fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_NAME, "Docking Device", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_ICON, "dock", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); return; } if (base == FU_PCI_DEVICE_BASE_CLS_PROCESSORS) { fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_NAME, "Processor Device", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); return; } if (base == FU_PCI_DEVICE_BASE_CLS_SERIAL_BUS) { fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_NAME, "Serial Bus Device", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); return; } if (base == FU_PCI_DEVICE_BASE_CLS_WIRELESS) { fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_NAME, "Wireless Device", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_ICON, "network-wireless", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); return; } if (base == FU_PCI_DEVICE_BASE_CLS_INTELLIGENT_IO) { fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_NAME, "Intelligent I/O Device", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); return; } if (base == FU_PCI_DEVICE_BASE_CLS_SATELLITE) { fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_NAME, "Satellite Device", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); return; } if (base == FU_PCI_DEVICE_BASE_CLS_ENCRYPTION) { fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_NAME, "Encryption Device", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_ICON, "auth-fingerprint", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); return; } if (base == FU_PCI_DEVICE_BASE_CLS_SIGNAL_PROCESSING) { fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_NAME, "Signal Processing Device", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); return; } if (base == FU_PCI_DEVICE_BASE_CLS_ACCELERATOR) { fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_NAME, "Accelerator Device", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_ICON, "gpu", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); return; } if (base == FU_PCI_DEVICE_BASE_CLS_NON_ESSENTIAL) { fu_device_set_quirk_kv(FU_DEVICE(self), FU_QUIRKS_NAME, "Non-essential Device", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, NULL); return; } } static void fu_pci_device_probe_complete(FuDevice *device) { FuPciDevice *self = FU_PCI_DEVICE(device); FuPciDevicePrivate *priv = GET_PRIVATE(self); /* FuUdevDevice->probe_complete */ FU_DEVICE_CLASS(fu_pci_device_parent_class)->probe_complete(device); /* "Display Adapter" is much better than "Unknown Device" */ fu_pci_device_set_quirks_fallback(self, priv->class >> 16); } static void fu_pci_device_init(FuPciDevice *self) { } static void fu_pci_device_class_init(FuPciDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_pci_device_to_string; device_class->probe = fu_pci_device_probe; device_class->probe_complete = fu_pci_device_probe_complete; device_class->incorporate = fu_pci_device_to_incorporate; } fwupd-2.0.10/libfwupdplugin/fu-pci-device.h000066400000000000000000000015521501337203100205410ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-udev-device.h" #define FU_TYPE_PCI_DEVICE (fu_pci_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuPciDevice, fu_pci_device, FU, PCI_DEVICE, FuUdevDevice) struct _FuPciDeviceClass { FuUdevDeviceClass parent_class; }; guint16 fu_pci_device_get_subsystem_vid(FuPciDevice *self) G_GNUC_NON_NULL(1); guint16 fu_pci_device_get_subsystem_pid(FuPciDevice *self) G_GNUC_NON_NULL(1); void fu_pci_device_set_subsystem_vid(FuPciDevice *self, guint16 subsystem_vid) G_GNUC_NON_NULL(1); void fu_pci_device_set_subsystem_pid(FuPciDevice *self, guint16 subsystem_pid) G_GNUC_NON_NULL(1); guint8 fu_pci_device_get_revision(FuPciDevice *self) G_GNUC_NON_NULL(1); void fu_pci_device_set_revision(FuPciDevice *self, guint8 revision) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-pci.rs000066400000000000000000000007071501337203100175020ustar00rootroot00000000000000// Copyright 2025 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later enum FuPciDeviceBaseCls { Old, MassStorage, Network, Display, Multimedia, Memory, Bridge, SimpleCommunication, Base, Input, Docking, Processors, SerialBus, Wireless, IntelligentIo, Satellite, Encryption, SignalProcessing, Accelerator, NonEssential, Undefined = 0xFF, } fwupd-2.0.10/libfwupdplugin/fu-pefile-firmware.c000066400000000000000000000411071501337203100216020ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-composite-input-stream.h" #include "fu-coswid-firmware.h" #include "fu-csv-firmware.h" #include "fu-input-stream.h" #include "fu-mem.h" #include "fu-partial-input-stream.h" #include "fu-pefile-firmware.h" #include "fu-pefile-struct.h" #include "fu-sbatlevel-section.h" #include "fu-string.h" /** * FuPefileFirmware: * * A PE file consists of a Microsoft MS-DOS stub, the PE signature, the COFF file header, and an * optional header, followed by section data. * * Documented: * https://learn.microsoft.com/en-gb/windows/win32/debug/pe-format */ typedef struct { gchar *authenticode_hash; guint16 subsystem_id; } FuPefileFirmwarePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuPefileFirmware, fu_pefile_firmware, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_pefile_firmware_get_instance_private(o)) #define FU_PEFILE_SECTION_ID_STRTAB_SIZE 16 static void fu_pefile_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuPefileFirmware *self = FU_PEFILE_FIRMWARE(firmware); FuPefileFirmwarePrivate *priv = GET_PRIVATE(self); fu_xmlb_builder_insert_kv(bn, "authenticode_hash", priv->authenticode_hash); fu_xmlb_builder_insert_kv(bn, "subsystem", fu_coff_subsystem_to_string(priv->subsystem_id)); } static gboolean fu_pefile_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_pe_dos_header_validate_stream(stream, offset, error); } typedef struct { gsize offset; gsize size; gchar *name; } FuPefileFirmwareRegion; static void fu_pefile_firmware_add_region(GPtrArray *regions, const gchar *name, gsize offset, gsize size) { FuPefileFirmwareRegion *r = g_new0(FuPefileFirmwareRegion, 1); r->name = g_strdup(name); r->offset = offset; r->size = size; g_ptr_array_add(regions, r); } static void fu_pefile_firmware_region_free(FuPefileFirmwareRegion *r) { g_free(r->name); g_free(r); } static gint fu_pefile_firmware_region_sort_cb(gconstpointer a, gconstpointer b) { const FuPefileFirmwareRegion *r1 = *((const FuPefileFirmwareRegion **)a); const FuPefileFirmwareRegion *r2 = *((const FuPefileFirmwareRegion **)b); if (r1->offset < r2->offset) return -1; if (r1->offset > r2->offset) return 1; return 0; } static gboolean fu_pefile_firmware_parse_section(FuFirmware *firmware, GInputStream *stream, guint idx, gsize hdr_offset, gsize strtab_offset, GPtrArray *regions, FuFirmwareParseFlags flags, GError **error) { g_autofree gchar *sect_id = NULL; g_autofree gchar *sect_id_tmp = NULL; g_autoptr(FuFirmware) img = NULL; g_autoptr(GByteArray) st = NULL; st = fu_struct_pe_coff_section_parse_stream(stream, hdr_offset, error); if (st == NULL) { g_prefix_error(error, "failed to read section: "); return FALSE; } sect_id_tmp = fu_struct_pe_coff_section_get_name(st); if (sect_id_tmp == NULL) { sect_id = g_strdup_printf(".nul%04x", idx); } else if (sect_id_tmp[0] == '/') { guint64 str_idx = 0x0; guint8 buf[FU_PEFILE_SECTION_ID_STRTAB_SIZE] = {0}; if (!fu_strtoull(sect_id_tmp + 1, &str_idx, 0, G_MAXUINT32, FU_INTEGER_BASE_10, error)) { g_prefix_error(error, "failed to parse section ID '%s': ", sect_id_tmp + 1); return FALSE; } if (!fu_input_stream_read_safe(stream, buf, sizeof(buf), 0x0, strtab_offset + str_idx, /* seek */ sizeof(buf), error)) return FALSE; sect_id = fu_strsafe((const gchar *)buf, sizeof(buf)); if (sect_id == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no section name"); return FALSE; } } else { sect_id = g_steal_pointer(§_id_tmp); } /* create new firmware */ if (g_strcmp0(sect_id, ".sbom") == 0) { img = fu_coswid_firmware_new(); } else if (g_strcmp0(sect_id, ".sbat") == 0 || g_strcmp0(sect_id, ".sbata") == 0 || g_strcmp0(sect_id, ".sbatl") == 0) { img = fu_csv_firmware_new(); fu_csv_firmware_add_column_id(FU_CSV_FIRMWARE(img), "$id"); fu_csv_firmware_add_column_id(FU_CSV_FIRMWARE(img), "$version_raw"); fu_csv_firmware_add_column_id(FU_CSV_FIRMWARE(img), "vendor_name"); fu_csv_firmware_add_column_id(FU_CSV_FIRMWARE(img), "vendor_package_name"); fu_csv_firmware_add_column_id(FU_CSV_FIRMWARE(img), "$version"); fu_csv_firmware_add_column_id(FU_CSV_FIRMWARE(img), "vendor_url"); fu_csv_firmware_set_write_column_ids(FU_CSV_FIRMWARE(img), FALSE); } else if (g_strcmp0(sect_id, ".sbatlevel") == 0) { img = fu_sbatlevel_section_new(); } else { img = fu_firmware_new(); } fu_firmware_set_id(img, sect_id); fu_firmware_set_idx(img, idx); /* add data */ if (fu_struct_pe_coff_section_get_size_of_raw_data(st) > 0) { guint32 sect_offset = fu_struct_pe_coff_section_get_pointer_to_raw_data(st); g_autoptr(GInputStream) img_stream = NULL; fu_firmware_set_offset(img, sect_offset); img_stream = fu_partial_input_stream_new(stream, sect_offset, fu_struct_pe_coff_section_get_size_of_raw_data(st), error); if (img_stream == NULL) { g_prefix_error(error, "failed to cut raw PE data: "); return FALSE; } if (!fu_firmware_parse_stream(img, img_stream, 0x0, flags, error)) { g_prefix_error(error, "failed to parse raw data %s: ", sect_id); return FALSE; } /* add region for Authenticode checksum */ fu_pefile_firmware_add_region(regions, sect_id, sect_offset, fu_struct_pe_coff_section_get_size_of_raw_data(st)); } /* success */ return fu_firmware_add_image_full(firmware, img, error); } static gboolean fu_pefile_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuPefileFirmware *self = FU_PEFILE_FIRMWARE(firmware); FuPefileFirmwarePrivate *priv = GET_PRIVATE(self); guint32 cert_table_sz = 0; gsize offset = 0; gsize streamsz = 0; gsize strtab_offset; guint32 nr_sections; g_autoptr(FuStructPeCoffFileHeader) st_coff = NULL; g_autoptr(FuStructPeDosHeader) st_doshdr = NULL; g_autoptr(GPtrArray) regions = NULL; g_autoptr(GInputStream) composite_stream = fu_composite_input_stream_new(); /* get size */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; /* parse the DOS header to get the COFF header */ st_doshdr = fu_struct_pe_dos_header_parse_stream(stream, offset, error); if (st_doshdr == NULL) { g_prefix_error(error, "failed to read DOS header: "); return FALSE; } offset += fu_struct_pe_dos_header_get_lfanew(st_doshdr); st_coff = fu_struct_pe_coff_file_header_parse_stream(stream, offset, error); if (st_coff == NULL) { g_prefix_error(error, "failed to read COFF header: "); return FALSE; } offset += st_coff->len; regions = g_ptr_array_new_with_free_func((GDestroyNotify)fu_pefile_firmware_region_free); /* 1st Authenticode region */ fu_pefile_firmware_add_region(regions, "pre-cksum", 0x0, offset + FU_STRUCT_PE_COFF_OPTIONAL_HEADER64_OFFSET_CHECKSUM); if (!fu_input_stream_read_safe( stream, (guint8 *)&priv->subsystem_id, sizeof(priv->subsystem_id), 0x0, offset + FU_STRUCT_PE_COFF_OPTIONAL_HEADER64_OFFSET_SUBSYSTEM, /* seek */ sizeof(priv->subsystem_id), error)) return FALSE; /* 2nd Authenticode region */ fu_pefile_firmware_add_region( regions, "chksum->cert-table", offset + FU_STRUCT_PE_COFF_OPTIONAL_HEADER64_OFFSET_SUBSYSTEM, FU_STRUCT_PE_COFF_OPTIONAL_HEADER64_OFFSET_CERTIFICATE_TABLE - FU_STRUCT_PE_COFF_OPTIONAL_HEADER64_OFFSET_SUBSYSTEM); // end /* verify optional extra header */ if (fu_struct_pe_coff_file_header_get_size_of_optional_header(st_coff) > 0) { g_autoptr(FuStructPeCoffOptionalHeader64) st_opt = fu_struct_pe_coff_optional_header64_parse_stream(stream, offset, error); if (st_opt == NULL) { g_prefix_error(error, "failed to read optional header: "); return FALSE; } /* 3rd Authenticode region */ if (fu_struct_pe_coff_optional_header64_get_size_of_headers(st_opt) > 0) { fu_pefile_firmware_add_region( regions, "cert-table->end-of-headers", offset + FU_STRUCT_PE_COFF_OPTIONAL_HEADER64_OFFSET_DEBUG_TABLE, fu_struct_pe_coff_optional_header64_get_size_of_headers(st_opt) - (offset + FU_STRUCT_PE_COFF_OPTIONAL_HEADER64_OFFSET_DEBUG_TABLE)); } /* 4th Authenticode region */ cert_table_sz = fu_struct_pe_coff_optional_header64_get_size_of_certificate_table(st_opt); offset += fu_struct_pe_coff_file_header_get_size_of_optional_header(st_coff); } /* read number of sections */ nr_sections = fu_struct_pe_coff_file_header_get_number_of_sections(st_coff); if (nr_sections == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid number of sections"); return FALSE; } strtab_offset = fu_struct_pe_coff_file_header_get_pointer_to_symbol_table(st_coff) + fu_struct_pe_coff_file_header_get_number_of_symbols(st_coff) * FU_STRUCT_PE_COFF_SYMBOL_SIZE; /* read out each section */ for (guint idx = 0; idx < nr_sections; idx++) { if (!fu_pefile_firmware_parse_section(firmware, stream, idx, offset, strtab_offset, regions, flags, error)) { g_prefix_error(error, "failed to read section 0x%x: ", idx); return FALSE; } offset += FU_STRUCT_PE_COFF_SECTION_SIZE; } /* make sure ordered by address */ g_ptr_array_sort(regions, fu_pefile_firmware_region_sort_cb); /* for the data at the end of the image */ if (regions->len > 0) { FuPefileFirmwareRegion *r = g_ptr_array_index(regions, regions->len - 1); gsize offset_end = r->offset + r->size; fu_pefile_firmware_add_region(regions, "tabledata->cert-table", offset_end, streamsz - (offset_end + cert_table_sz)); } /* calculate the checksum we would find in the dbx */ for (guint i = 0; i < regions->len; i++) { FuPefileFirmwareRegion *r = g_ptr_array_index(regions, i); g_autoptr(GInputStream) partial_stream = NULL; if (r->size == 0) continue; g_debug("Authenticode region %s: 0x%04x -> 0x%04x [0x%04x]", r->name, (guint)r->offset, (guint)(r->offset + r->size), (guint)r->size); partial_stream = fu_partial_input_stream_new(stream, r->offset, r->size, error); if (partial_stream == NULL) { g_prefix_error(error, "failed to cut Authenticode region: "); return FALSE; } fu_composite_input_stream_add_partial_stream( FU_COMPOSITE_INPUT_STREAM(composite_stream), FU_PARTIAL_INPUT_STREAM(partial_stream)); } priv->authenticode_hash = fu_input_stream_compute_checksum(composite_stream, G_CHECKSUM_SHA256, error); if (priv->authenticode_hash == NULL) return FALSE; /* success */ return TRUE; } typedef struct { GBytes *blob; gchar *id; gsize offset; gsize blobsz_aligned; } FuPefileSection; static void fu_pefile_firmware_section_free(FuPefileSection *section) { if (section->blob != NULL) g_bytes_unref(section->blob); g_free(section->id); g_free(section); } static GByteArray * fu_pefile_firmware_write(FuFirmware *firmware, GError **error) { gsize offset = 0; g_autoptr(GPtrArray) imgs = fu_firmware_get_images(firmware); g_autoptr(GByteArray) st = fu_struct_pe_dos_header_new(); g_autoptr(GByteArray) st_hdr = fu_struct_pe_coff_file_header_new(); g_autoptr(GByteArray) st_opt = fu_struct_pe_coff_optional_header64_new(); g_autoptr(GByteArray) strtab = g_byte_array_new(); g_autoptr(GPtrArray) sections = g_ptr_array_new_with_free_func((GDestroyNotify)fu_pefile_firmware_section_free); /* calculate the offset for each of the sections */ offset += st->len + st_hdr->len + st_opt->len; offset += FU_STRUCT_PE_COFF_SECTION_SIZE * imgs->len; for (guint i = 0; i < imgs->len; i++) { g_autofree FuPefileSection *section = g_new0(FuPefileSection, 1); FuFirmware *img = g_ptr_array_index(imgs, i); section->offset = offset; section->blob = fu_firmware_write(img, error); if (section->blob == NULL) return NULL; section->id = g_strdup(fu_firmware_get_id(img)); section->blobsz_aligned = fu_common_align_up(g_bytes_get_size(section->blob), 4); offset += section->blobsz_aligned; g_ptr_array_add(sections, g_steal_pointer(§ion)); } /* export_table -> architecture_table */ fu_struct_pe_coff_optional_header64_set_number_of_rva_and_sizes(st_opt, 7); /* COFF file header */ fu_struct_pe_coff_file_header_set_size_of_optional_header(st_hdr, st_opt->len); fu_struct_pe_coff_file_header_set_number_of_sections(st_hdr, sections->len); fu_struct_pe_coff_file_header_set_pointer_to_symbol_table(st_hdr, offset); g_byte_array_append(st, st_hdr->data, st_hdr->len); g_byte_array_append(st, st_opt->data, st_opt->len); /* add sections */ for (guint i = 0; i < sections->len; i++) { FuPefileSection *section = g_ptr_array_index(sections, i); g_autoptr(GByteArray) st_sect = fu_struct_pe_coff_section_new(); fu_struct_pe_coff_section_set_size_of_raw_data(st_sect, g_bytes_get_size(section->blob)); fu_struct_pe_coff_section_set_virtual_address(st_sect, 0x0); fu_struct_pe_coff_section_set_virtual_size(st_sect, section->blobsz_aligned); fu_struct_pe_coff_section_set_pointer_to_raw_data(st_sect, section->offset); /* set the name directly, or add to the string table */ if (section->id == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "image %u has no ID", i); return NULL; } if (strlen(section->id) <= 8) { if (!fu_struct_pe_coff_section_set_name(st_sect, section->id, error)) return NULL; } else { g_autofree gchar *name_tmp = g_strdup_printf("/%u", strtab->len); g_autoptr(GByteArray) strtab_buf = g_byte_array_new(); if (!fu_struct_pe_coff_section_set_name(st_sect, name_tmp, error)) return NULL; /* create a byte buffer of exactly the correct chunk size */ g_byte_array_append(strtab_buf, (const guint8 *)section->id, strlen(section->id)); if (strtab_buf->len > FU_PEFILE_SECTION_ID_STRTAB_SIZE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "image ID %s is too long", section->id); return NULL; } fu_byte_array_set_size(strtab_buf, FU_PEFILE_SECTION_ID_STRTAB_SIZE, 0x0); g_byte_array_append(strtab, strtab_buf->data, strtab_buf->len); } g_byte_array_append(st, st_sect->data, st_sect->len); } /* add the section data itself */ for (guint i = 0; i < sections->len; i++) { FuPefileSection *section = g_ptr_array_index(sections, i); g_autoptr(GBytes) blob_aligned = fu_bytes_pad(section->blob, section->blobsz_aligned, 0xFF); fu_byte_array_append_bytes(st, blob_aligned); } /* string table comes last */ g_byte_array_append(st, strtab->data, strtab->len); /* success */ return g_steal_pointer(&st); } static gchar * fu_pefile_firmware_get_checksum(FuFirmware *firmware, GChecksumType csum_kind, GError **error) { FuPefileFirmware *self = FU_PEFILE_FIRMWARE(firmware); FuPefileFirmwarePrivate *priv = GET_PRIVATE(self); if (csum_kind != G_CHECKSUM_SHA256) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Authenticode only supports SHA256"); return NULL; } if (priv->authenticode_hash == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "Authenticode checksum not set"); return NULL; } return g_strdup(priv->authenticode_hash); } static void fu_pefile_firmware_init(FuPefileFirmware *self) { fu_firmware_set_images_max(FU_FIRMWARE(self), 100); } static void fu_pefile_firmware_finalize(GObject *object) { FuPefileFirmware *self = FU_PEFILE_FIRMWARE(object); FuPefileFirmwarePrivate *priv = GET_PRIVATE(self); g_free(priv->authenticode_hash); G_OBJECT_CLASS(fu_pefile_firmware_parent_class)->finalize(object); } static void fu_pefile_firmware_class_init(FuPefileFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_pefile_firmware_finalize; firmware_class->validate = fu_pefile_firmware_validate; firmware_class->parse = fu_pefile_firmware_parse; firmware_class->write = fu_pefile_firmware_write; firmware_class->export = fu_pefile_firmware_export; firmware_class->get_checksum = fu_pefile_firmware_get_checksum; } /** * fu_pefile_firmware_new: * * Creates a new #FuPefileFirmware * * Since: 1.8.10 **/ FuFirmware * fu_pefile_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_PEFILE_FIRMWARE, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-pefile-firmware.h000066400000000000000000000006471501337203100216130ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_PEFILE_FIRMWARE (fu_pefile_firmware_get_type()) G_DECLARE_DERIVABLE_TYPE(FuPefileFirmware, fu_pefile_firmware, FU, PEFILE_FIRMWARE, FuFirmware) struct _FuPefileFirmwareClass { FuFirmwareClass parent_class; }; FuFirmware * fu_pefile_firmware_new(void); fwupd-2.0.10/libfwupdplugin/fu-pefile.rs000066400000000000000000000101551501337203100201710ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ParseStream, ValidateStream, New, Default)] #[repr(C, packed)] struct FuStructPeDosHeader { magic: u16le == 0x5A4D, cblp: u16le = 0x90, cp: u16le = 0x3, crlc: u16le, cparhdr: u16le = 0x4, minalloc: u16le, maxalloc: u16le = 0xFFFF, _ss: u16le, sp: u16le = 0xB8, _csum: u16le, _ip: u16le, _cs: u16le, lfarlc: u16le = 0x40, _ovno: u16le, _res: [u8; 8], _oemid: u16le, _oeminfo: u16le, _res2: [u8; 20], lfanew: u32le = 0x80, _res3: [u8; 64], } #[repr(u16le)] enum FuPeCoffMachine { Unknown, Alpha = 0x184, Alpha64 = 0x284, Am33 = 0x1d3, Amd64 = 0x8664, Arm = 0x1c0, Arm64 = 0xaa64, Armnt = 0x1c4, Ebc = 0xebc, I386 = 0x14c, Ia64 = 0x200, Loongarch32 = 0x6232, Loongarch64 = 0x6264, M32r = 0x9041, Mips16 = 0x266, Mipsfpu = 0x366, Mipsfpu16 = 0x466, Powerpc = 0x1f0, Powerpcfp = 0x1f1, R4000 = 0x166, Riscv32 = 0x5032, Riscv64 = 0x5064, Riscv128 = 0x5128, Sh3 = 0x1a2, Sh3dsp = 0x1a3, Sh4 = 0x1a6, Sh5 = 0x1a8, Thumb = 0x1c2, Wcemipsv2 = 0x169, } #[repr(u16le)] enum FuPeCoffMagic { Pe32 = 0x10b, Pe32Plus = 0x20b, } #[repr(u16le)] #[derive(ToString)] enum FuCoffSubsystem { Unknown, Native = 1, WindowsGui = 2, WindowsCui = 3, Os2Cui = 5, PosixCui = 7, NativeWindows = 8, WindowsCeGui = 9, EfiApplication = 10, EfiBootServiceDriver = 11, EfiRuntimeDriver = 12, EfiRom = 13, Xbox = 14, WindowsBootApplication = 16, } #[derive(ParseStream, New, Default)] #[repr(C, packed)] struct FuStructPeCoffFileHeader { signature: u32le == 0x4550, // "PE\0\0" machine: FuPeCoffMachine = Amd64, number_of_sections: u16le, _time_date_stamp: u32le, pointer_to_symbol_table: u32le, number_of_symbols: u32le, size_of_optional_header: u16le = 0xf0, characteristics: u16le = 0x2022, } #[derive(ParseStream, New, Default)] #[repr(C, packed)] struct FuStructPeCoffOptionalHeader64 { magic: FuPeCoffMagic = Pe32Plus, major_linker_version: u8 = 0x0e, minor_linker_version: u8 = 0x0e, size_of_code: u32le, size_of_initialized_data: u32le, size_of_uninitialized_data: u32le, addressofentrypoint: u32le, base_of_code: u32le, image_base: u64le, section_alignment: u32le = 0x200, file_alignment: u32le = 0x200, _major_operating_system_version: u16le, _minor_operating_system_version: u16le, _major_image_version: u16le, _minor_image_version: u16le, _major_subsystem_version: u16le, _minor_subsystem_version: u16le, _win32_versionvalue: u32le, size_of_image: u32le, size_of_headers: u32le, checksum: u32le, subsystem: FuCoffSubsystem = EfiApplication, _dll_characteristics: u16le, _size_of_stackreserve: u64le, _size_of_stack_commit: u64le, _size_of_heap_reserve: u64le, _size_of_heap_commit: u64le, loader_flags: u32le, number_of_rva_and_sizes: u32le, _export_table: u32le, _size_of_export_table: u32le, _import_table: u32le, _size_of_import_table: u32le, _resource_table: u32le, _size_of_resource: u32le, _exception_table: u32le, _size_of_exception_table: u32le, certificate_table: u32le, size_of_certificate_table: u32le, debug_table: u32le, size_of_debug_table: u32le, _architecture_table: u32le, _size_of_architecture_table: u32le, _global_ptr: u32le, _reserved: u32le, } #[repr(C, packed)] struct FuStructPeCoffSymbol { name: [char; 8], value: u32le, section_number: u16le, type: u16le, storage_class: u8, number_of_aux_symbols: u8, } #[derive(ParseStream, New)] #[repr(C, packed)] struct FuStructPeCoffSection { name: [char; 8], virtual_size: u32le, virtual_address: u32le, size_of_raw_data: u32le, pointer_to_raw_data: u32le, _pointer_to_relocations: u32le, _pointer_to_linenumbers: u32le, _number_of_relocations: u16le, _number_of_linenumbers: u16le, characteristics: u32le, } fwupd-2.0.10/libfwupdplugin/fu-pkcs7.c000066400000000000000000000065141501337203100175560ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #ifdef HAVE_GNUTLS #include #include #include #endif #include "fu-input-stream.h" #include "fu-pkcs7.h" #include "fu-string.h" #include "fu-x509-certificate.h" #ifdef HAVE_GNUTLS #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTO_CLEANUP_FREE_FUNC(gnutls_pkcs7_t, gnutls_pkcs7_deinit, NULL) #pragma clang diagnostic pop #endif /** * FuPkcs7: * * A PKCS#7 object, typically containing signed X.509 certificates. * * See also: [class@FuFirmware] */ struct _FuPkcs7 { FuFirmware parent_instance; }; G_DEFINE_TYPE(FuPkcs7, fu_pkcs7, FU_TYPE_FIRMWARE) #ifdef HAVE_GNUTLS static gboolean fu_pkcs7_parse_x509_certificate(FuPkcs7 *self, gnutls_datum_t *data, GError **error) { g_autoptr(FuX509Certificate) crt = fu_x509_certificate_new(); g_autoptr(GBytes) blob = NULL; /* parse as a X.509 certificate */ blob = g_bytes_new(data->data, data->size); if (!fu_firmware_parse_bytes(FU_FIRMWARE(crt), blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return FALSE; fu_firmware_add_image(FU_FIRMWARE(self), FU_FIRMWARE(crt)); /* success */ return TRUE; } #endif static gboolean fu_pkcs7_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { #ifdef HAVE_GNUTLS FuPkcs7 *self = FU_PKCS7(firmware); gnutls_datum_t datum = {0}; int rc; g_auto(gnutls_pkcs7_t) pkcs7 = NULL; g_autoptr(GByteArray) buf = NULL; /* load PKCS#7 cert */ buf = fu_input_stream_read_byte_array(stream, 0x0, G_MAXSIZE, NULL, error); if (buf == NULL) return FALSE; rc = gnutls_pkcs7_init(&pkcs7); if (rc != GNUTLS_E_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to init pkcs7: %s [%i]", gnutls_strerror(rc), rc); return FALSE; } datum.data = buf->data; datum.size = buf->len; rc = gnutls_pkcs7_import(pkcs7, &datum, GNUTLS_X509_FMT_DER); if (rc != GNUTLS_E_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to import the PKCS7 signature: %s [%i]", gnutls_strerror(rc), rc); return FALSE; } /* parse each X.509 certificate */ for (int i = 0; i < gnutls_pkcs7_get_crt_count(pkcs7); i++) { gnutls_datum_t out; rc = gnutls_pkcs7_get_crt_raw2(pkcs7, i, &out); if (rc < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to get raw crt: %s [%i]", gnutls_strerror(rc), rc); return FALSE; } if (!fu_pkcs7_parse_x509_certificate(self, &out, error)) { gnutls_free(out.data); return FALSE; } gnutls_free(out.data); } /* success */ return TRUE; #else g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no GnuTLS support"); return FALSE; #endif } static void fu_pkcs7_init(FuPkcs7 *self) { } static void fu_pkcs7_class_init(FuPkcs7Class *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_pkcs7_parse; } /** * fu_pkcs7_new: * * Creates a new #FuPkcs7. * * Returns: (transfer full): object * * Since: 2.0.9 **/ FuPkcs7 * fu_pkcs7_new(void) { return g_object_new(FU_TYPE_PKCS7, NULL); } fwupd-2.0.10/libfwupdplugin/fu-pkcs7.h000066400000000000000000000004421501337203100175550ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_PKCS7 (fu_pkcs7_get_type()) G_DECLARE_FINAL_TYPE(FuPkcs7, fu_pkcs7, FU, PKCS7, FuFirmware) FuPkcs7 * fu_pkcs7_new(void); fwupd-2.0.10/libfwupdplugin/fu-plugin-private.h000066400000000000000000000133601501337203100214770ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-context.h" #include "fu-plugin.h" #include "fu-security-attrs.h" FuPlugin * fu_plugin_new(FuContext *ctx); FuPlugin * fu_plugin_new_from_gtype(GType gtype, FuContext *ctx) G_GNUC_NON_NULL(2); void fu_plugin_set_context(FuPlugin *self, FuContext *ctx) G_GNUC_NON_NULL(1); gboolean fu_plugin_is_open(FuPlugin *self) G_GNUC_NON_NULL(1); guint fu_plugin_get_order(FuPlugin *self) G_GNUC_NON_NULL(1); void fu_plugin_set_order(FuPlugin *self, guint order) G_GNUC_NON_NULL(1); guint fu_plugin_get_priority(FuPlugin *self) G_GNUC_NON_NULL(1); void fu_plugin_set_priority(FuPlugin *self, guint priority) G_GNUC_NON_NULL(1); GArray * fu_plugin_get_device_gtypes(FuPlugin *self) G_GNUC_NON_NULL(1); gchar * fu_plugin_to_string(FuPlugin *self) G_GNUC_NON_NULL(1); void fu_plugin_add_string(FuPlugin *self, guint idt, GString *str) G_GNUC_NON_NULL(1); GPtrArray * fu_plugin_get_rules(FuPlugin *self, FuPluginRule rule) G_GNUC_NON_NULL(1); GHashTable * fu_plugin_get_report_metadata(FuPlugin *self) G_GNUC_NON_NULL(1); gboolean fu_plugin_open(FuPlugin *self, const gchar *filename, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_plugin_reset_config_values(FuPlugin *self, GError **error) G_GNUC_NON_NULL(1); void fu_plugin_runner_init(FuPlugin *self) G_GNUC_NON_NULL(1); gboolean fu_plugin_runner_startup(FuPlugin *self, FuProgress *progress, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_plugin_runner_ready(FuPlugin *self, FuProgress *progress, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_plugin_runner_coldplug(FuPlugin *self, FuProgress *progress, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_plugin_runner_prepare(FuPlugin *self, FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); gboolean fu_plugin_runner_cleanup(FuPlugin *self, FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); gboolean fu_plugin_runner_composite_prepare(FuPlugin *self, GPtrArray *devices, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_plugin_runner_composite_cleanup(FuPlugin *self, GPtrArray *devices, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_plugin_runner_attach(FuPlugin *self, FuDevice *device, FuProgress *progress, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); gboolean fu_plugin_runner_detach(FuPlugin *self, FuDevice *device, FuProgress *progress, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); gboolean fu_plugin_runner_reload(FuPlugin *self, FuDevice *device, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_plugin_runner_backend_device_added(FuPlugin *self, FuDevice *device, FuProgress *progress, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); gboolean fu_plugin_runner_backend_device_changed(FuPlugin *self, FuDevice *device, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_plugin_runner_device_created(FuPlugin *self, FuDevice *device, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fu_plugin_runner_device_added(FuPlugin *self, FuDevice *device) G_GNUC_NON_NULL(1, 2); void fu_plugin_runner_device_removed(FuPlugin *self, FuDevice *device) G_GNUC_NON_NULL(1, 2); void fu_plugin_runner_device_register(FuPlugin *self, FuDevice *device) G_GNUC_NON_NULL(1, 2); gboolean fu_plugin_runner_write_firmware(FuPlugin *self, FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3, 4); gboolean fu_plugin_runner_verify(FuPlugin *self, FuDevice *device, FuProgress *progress, FuPluginVerifyFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); gboolean fu_plugin_runner_activate(FuPlugin *self, FuDevice *device, FuProgress *progress, GError **error) G_GNUC_NON_NULL(1, 2, 3); gboolean fu_plugin_runner_unlock(FuPlugin *self, FuDevice *device, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_plugin_runner_clear_results(FuPlugin *self, FuDevice *device, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_plugin_runner_get_results(FuPlugin *self, FuDevice *device, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_plugin_runner_fix_host_security_attr(FuPlugin *self, FwupdSecurityAttr *attr, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_plugin_runner_undo_host_security_attr(FuPlugin *self, FwupdSecurityAttr *attr, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_plugin_runner_reboot_cleanup(FuPlugin *self, FuDevice *device, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); void fu_plugin_runner_add_security_attrs(FuPlugin *self, FuSecurityAttrs *attrs) G_GNUC_NON_NULL(1, 2); gboolean fu_plugin_runner_modify_config(FuPlugin *self, const gchar *key, const gchar *value, GError **error) G_GNUC_NON_NULL(1, 2, 3); gint fu_plugin_name_compare(FuPlugin *plugin1, FuPlugin *plugin2) G_GNUC_NON_NULL(1, 2); gint fu_plugin_order_compare(FuPlugin *plugin1, FuPlugin *plugin2) G_GNUC_NON_NULL(1, 2); /* utils */ gchar * fu_plugin_guess_name_from_fn(const gchar *filename) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-plugin-vfuncs.h000066400000000000000000000005241501337203100213270ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-plugin.h" /** * fu_plugin_init_vfuncs: * @vfuncs: #FuPluginVfuncs * * Initializes the plugin vfuncs. * * Since: 1.7.2 **/ void fu_plugin_init_vfuncs(FuPluginVfuncs *vfuncs) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-plugin.c000066400000000000000000002426221501337203100200270ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuPlugin" #include "config.h" #include #include #include #include #include #include #include "fu-bytes.h" #include "fu-config-private.h" #include "fu-context-private.h" #include "fu-device-private.h" #include "fu-kernel.h" #include "fu-path.h" #include "fu-plugin-private.h" #include "fu-security-attr.h" #include "fu-string.h" /** * FuPlugin: * * A plugin which is used by fwupd to enumerate and update devices. * * See also: [class@FuDevice], [class@Fwupd.Plugin] */ static void fu_plugin_finalize(GObject *object); typedef struct { GModule *module; guint order; guint priority; gboolean done_init; GPtrArray *rules[FU_PLUGIN_RULE_LAST]; GPtrArray *devices; /* (nullable) (element-type FuDevice) */ GHashTable *runtime_versions; GHashTable *compile_versions; FuContext *ctx; GArray *device_gtypes; /* (nullable): of #GType */ GType device_gtype_default; GHashTable *cache; /* (nullable): platform_id:GObject */ GHashTable *report_metadata; /* (nullable): key:value */ GFileMonitor *config_monitor; FuPluginData *data; FuPluginVfuncs vfuncs; } FuPluginPrivate; enum { PROP_0, PROP_CONTEXT, PROP_LAST }; enum { SIGNAL_DEVICE_ADDED, SIGNAL_DEVICE_REMOVED, SIGNAL_DEVICE_REGISTER, SIGNAL_RULES_CHANGED, SIGNAL_CHECK_SUPPORTED, SIGNAL_LAST }; static guint signals[SIGNAL_LAST] = {0}; G_DEFINE_TYPE_WITH_PRIVATE(FuPlugin, fu_plugin, FWUPD_TYPE_PLUGIN) #define GET_PRIVATE(o) (fu_plugin_get_instance_private(o)) typedef void (*FuPluginInitVfuncsFunc)(FuPluginVfuncs *vfuncs); typedef gboolean (*FuPluginDeviceFunc)(FuPlugin *self, FuDevice *device, GError **error); typedef gboolean (*FuPluginDeviceProgressFunc)(FuPlugin *self, FuDevice *device, FuProgress *progress, GError **error); typedef gboolean (*FuPluginFlaggedDeviceFunc)(FuPlugin *self, FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error); typedef gboolean (*FuPluginDeviceArrayFunc)(FuPlugin *self, GPtrArray *devices, GError **error); /** * fu_plugin_is_open: * @self: a #FuPlugin * * Determines if the plugin is opened * * Returns: TRUE for opened, FALSE for not * * Since: 1.3.5 **/ gboolean fu_plugin_is_open(FuPlugin *self) { FuPluginPrivate *priv = GET_PRIVATE(self); return priv->module != NULL; } /** * fu_plugin_get_name: * @self: a #FuPlugin * * Gets the plugin name. * * Returns: a plugin name, or %NULL for unknown. * * Since: 0.8.0 **/ const gchar * fu_plugin_get_name(FuPlugin *self) { g_return_val_if_fail(FU_IS_PLUGIN(self), NULL); return fwupd_plugin_get_name(FWUPD_PLUGIN(self)); } /** * fu_plugin_set_name: * @self: a #FuPlugin * @name: a string * * Sets the plugin name. * * Since: 0.8.0 **/ void fu_plugin_set_name(FuPlugin *self, const gchar *name) { FuPluginPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_PLUGIN(self)); g_return_if_fail(!priv->done_init); if (g_strcmp0(name, fwupd_plugin_get_name(FWUPD_PLUGIN(self))) == 0) { g_critical("plugin name set to original value: %s", name); return; } if (fwupd_plugin_get_name(FWUPD_PLUGIN(self)) != NULL) { g_debug("overwriting plugin name %s -> %s", fwupd_plugin_get_name(FWUPD_PLUGIN(self)), name); } fwupd_plugin_set_name(FWUPD_PLUGIN(self), name); } static FuPluginVfuncs * fu_plugin_get_vfuncs(FuPlugin *self) { FuPluginPrivate *priv = GET_PRIVATE(self); if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_MODULAR)) return &priv->vfuncs; return FU_PLUGIN_GET_CLASS(self); } /** * fu_plugin_cache_lookup: * @self: a #FuPlugin * @id: the key * * Finds an object in the per-plugin cache. * * Returns: (transfer none): a #GObject, or %NULL for unfound. * * Since: 0.8.0 **/ gpointer fu_plugin_cache_lookup(FuPlugin *self, const gchar *id) { FuPluginPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_PLUGIN(self), NULL); g_return_val_if_fail(id != NULL, NULL); if (priv->cache == NULL) return NULL; return g_hash_table_lookup(priv->cache, id); } /** * fu_plugin_cache_add: * @self: a #FuPlugin * @id: the key * @dev: a #GObject, typically a #FuDevice * * Adds an object to the per-plugin cache. * * Since: 0.8.0 **/ void fu_plugin_cache_add(FuPlugin *self, const gchar *id, gpointer dev) { FuPluginPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_PLUGIN(self)); g_return_if_fail(id != NULL); g_return_if_fail(G_IS_OBJECT(dev)); if (priv->cache == NULL) { priv->cache = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_object_unref); } g_hash_table_insert(priv->cache, g_strdup(id), g_object_ref(dev)); } /** * fu_plugin_cache_remove: * @self: a #FuPlugin * @id: the key * * Removes an object from the per-plugin cache. * * Since: 0.8.0 **/ void fu_plugin_cache_remove(FuPlugin *self, const gchar *id) { FuPluginPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_PLUGIN(self)); g_return_if_fail(id != NULL); if (priv->cache == NULL) return; if (g_hash_table_remove(priv->cache, id)) g_debug("removed %s object %s", fu_plugin_get_name(self), id); } /** * fu_plugin_get_data: * @self: a #FuPlugin * * Gets the per-plugin allocated private data. This will return %NULL unless * fu_plugin_alloc_data() has been called by the plugin. * * Returns: (transfer none): a pointer to a structure, or %NULL for unset. * * Since: 0.8.0 **/ FuPluginData * fu_plugin_get_data(FuPlugin *self) { FuPluginPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_PLUGIN(self), NULL); return priv->data; } /** * fu_plugin_alloc_data: (skip): * @self: a #FuPlugin * @data_sz: the size to allocate * * Allocates the per-plugin allocated private data. * * Returns: (transfer full): a pointer to a structure, or %NULL for unset. * * Since: 0.8.0 **/ FuPluginData * fu_plugin_alloc_data(FuPlugin *self, gsize data_sz) { FuPluginPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_PLUGIN(self), NULL); if (priv->data != NULL) { g_critical("fu_plugin_alloc_data() already used by plugin"); return priv->data; } priv->data = g_malloc0(data_sz); return priv->data; } /** * fu_plugin_guess_name_from_fn: * @filename: filename to guess * * Tries to guess the name of the plugin from a filename * * Returns: (transfer full): the guessed name of the plugin * * Since: 1.0.8 **/ gchar * fu_plugin_guess_name_from_fn(const gchar *filename) { const gchar *prefix = "libfu_plugin_"; gchar *name; gchar *str = g_strstr_len(filename, -1, prefix); if (str == NULL) return NULL; name = g_strdup(str + strlen(prefix)); g_strdelimit(name, ".", '\0'); return name; } /** * fu_plugin_open: * @self: a #FuPlugin * @filename: the shared object filename to open * @error: (nullable): optional return location for an error * * Opens the plugin module, and calls `->load()` on it. * * Returns: TRUE for success, FALSE for fail * * Since: 0.8.0 **/ gboolean fu_plugin_open(FuPlugin *self, const gchar *filename, GError **error) { FuPluginPrivate *priv = GET_PRIVATE(self); FuPluginVfuncs *vfuncs; FuPluginInitVfuncsFunc init_vfuncs = NULL; g_return_val_if_fail(FU_IS_PLUGIN(self), FALSE); g_return_val_if_fail(filename != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); priv->module = g_module_open(filename, 0); if (priv->module == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to open plugin %s: %s", filename, g_module_error()); fu_plugin_add_flag(self, FWUPD_PLUGIN_FLAG_FAILED_OPEN); fu_plugin_add_flag(self, FWUPD_PLUGIN_FLAG_USER_WARNING); return FALSE; } /* call the vfunc setup */ g_module_symbol(priv->module, "fu_plugin_init_vfuncs", (gpointer *)&init_vfuncs); if (init_vfuncs == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to init_vfuncs() on plugin %s", filename); fu_plugin_add_flag(self, FWUPD_PLUGIN_FLAG_FAILED_OPEN); fu_plugin_add_flag(self, FWUPD_PLUGIN_FLAG_USER_WARNING); return FALSE; } /* we can't "fallback" from modular to built-in so this is safe */ fu_plugin_add_flag(self, FWUPD_PLUGIN_FLAG_MODULAR); vfuncs = fu_plugin_get_vfuncs(self); init_vfuncs(vfuncs); /* set automatically */ if (fu_plugin_get_name(self) == NULL) { g_autofree gchar *str = fu_plugin_guess_name_from_fn(filename); fu_plugin_set_name(self, str); } /* optional */ if (vfuncs->load != NULL) { FuContext *ctx = fu_plugin_get_context(self); g_debug("load(%s)", filename); vfuncs->load(ctx); } return TRUE; } /** * fu_plugin_add_string: * @self: a #FuPlugin * @idt: indent level * @str: a string to append to * * Add daemon-specific device metadata to an existing string. * * Since: 1.8.4 **/ void fu_plugin_add_string(FuPlugin *self, guint idt, GString *str) { FuPluginPrivate *priv = GET_PRIVATE(self); FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); g_return_if_fail(FU_IS_PLUGIN(self)); g_return_if_fail(str != NULL); /* attributes */ fwupd_codec_add_string(FWUPD_CODEC(self), idt, str); fwupd_codec_string_append_int(str, idt + 1, "Order", priv->order); fwupd_codec_string_append_int(str, idt + 1, "Priority", priv->priority); if (priv->device_gtype_default != G_TYPE_INVALID) { fwupd_codec_string_append(str, idt + 1, "DeviceGTypeDefault", g_type_name(priv->device_gtype_default)); } /* optional */ if (vfuncs->to_string != NULL) vfuncs->to_string(self, idt + 1, str); } /** * fu_plugin_to_string: * @self: a #FuPlugin * * This allows us to easily print the plugin metadata. * * Returns: a string value, or %NULL for invalid. * * Since: 1.8.4 **/ gchar * fu_plugin_to_string(FuPlugin *self) { g_autoptr(GString) str = g_string_new(NULL); g_return_val_if_fail(FU_IS_PLUGIN(self), NULL); fu_plugin_add_string(self, 0, str); return g_string_free(g_steal_pointer(&str), FALSE); } /* order of usefulness to the user */ static const gchar * fu_plugin_build_device_update_error(FuPlugin *self) { if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_NO_HARDWARE)) return "Not updatable as required hardware was not found"; if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_LEGACY_BIOS)) return "Not updatable in legacy BIOS mode"; if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_CAPSULES_UNSUPPORTED)) return "Not updatable as UEFI capsule updates not enabled in firmware setup"; if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_UNLOCK_REQUIRED)) return "Not updatable as requires unlock"; if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_AUTH_REQUIRED)) return "Not updatable as requires authentication"; if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_EFIVAR_NOT_MOUNTED)) return "Not updatable as efivarfs was not found"; if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_ESP_NOT_FOUND)) return "Not updatable as UEFI ESP partition not detected"; if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_DISABLED)) return "Not updatable as plugin was disabled"; return NULL; } static void fu_plugin_ensure_devices(FuPlugin *self) { FuPluginPrivate *priv = GET_PRIVATE(self); if (priv->devices != NULL) return; priv->devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); } static void fu_plugin_device_child_added_cb(FuDevice *device, FuDevice *child, FuPlugin *self) { g_debug("child %s added to parent %s after setup, adding to daemon", fu_device_get_id(child), fu_device_get_id(device)); fu_plugin_device_add(self, child); } static void fu_plugin_device_child_removed_cb(FuDevice *device, FuDevice *child, FuPlugin *self) { g_debug("child %s removed from parent %s after setup, removing from daemon", fu_device_get_id(child), fu_device_get_id(device)); fu_plugin_device_remove(self, child); } /** * fu_plugin_device_add: * @self: a #FuPlugin * @device: a device * * Asks the daemon to add a device to the exported list. If this device ID * has already been added by a different plugin then this request will be * ignored. * * Since: 0.8.0 **/ void fu_plugin_device_add(FuPlugin *self, FuDevice *device) { FuPluginPrivate *priv = GET_PRIVATE(self); GPtrArray *children; g_autoptr(GError) error = NULL; g_return_if_fail(FU_IS_PLUGIN(self)); g_return_if_fail(FU_IS_DEVICE(device)); /* make tests easier */ fu_device_convert_instance_ids(device); /* ensure the device ID is set from the physical and logical IDs */ if (!fu_device_ensure_id(device, &error)) { g_warning("ignoring add: %s", error->message); return; } /* add to array */ fu_plugin_ensure_devices(self); g_ptr_array_add(priv->devices, g_object_ref(device)); /* proxy to device where required */ if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_CLEAR_UPDATABLE)) { if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_USER_WARNING)) { fu_device_inhibit(device, "clear-updatable", fu_plugin_build_device_update_error(self)); } else { fu_device_inhibit(device, "clear-updatable", "Plugin disallowed updates with no user warning"); } } g_debug("emit added from %s: %s", fu_plugin_get_name(self), fu_device_get_id(device)); if (fu_device_get_created_usec(device) == 0) fu_device_set_created_usec(device, g_get_real_time()); fu_device_set_plugin(device, fu_plugin_get_name(self)); g_signal_emit(self, signals[SIGNAL_DEVICE_ADDED], 0, device); /* add children if they have not already been added */ children = fu_device_get_children(device); for (guint i = 0; i < children->len; i++) { FuDevice *child = g_ptr_array_index(children, i); if (fu_device_get_created_usec(child) == 0) fu_plugin_device_add(self, child); } /* watch to see if children are added or removed at runtime */ g_signal_connect(FU_DEVICE(device), "child-added", G_CALLBACK(fu_plugin_device_child_added_cb), self); g_signal_connect(FU_DEVICE(device), "child-removed", G_CALLBACK(fu_plugin_device_child_removed_cb), self); } /** * fu_plugin_get_devices: * @self: a #FuPlugin * * Returns all devices added by the plugin using [method@FuPlugin.device_add] and * not yet removed with [method@FuPlugin.device_remove]. * * Returns: (transfer none) (element-type FuDevice): devices * * Since: 1.5.6 **/ GPtrArray * fu_plugin_get_devices(FuPlugin *self) { FuPluginPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_PLUGIN(self), NULL); fu_plugin_ensure_devices(self); return priv->devices; } /** * fu_plugin_device_register: * @self: a #FuPlugin * @device: a device * * Registers the device with other plugins so they can set metadata. * * Plugins do not have to call this manually as this is done automatically * when using [method@FuPlugin.device_add]. They may wish to use this manually * if for instance the coldplug should be ignored based on the metadata * set from other plugins. * * Since: 0.9.7 **/ void fu_plugin_device_register(FuPlugin *self, FuDevice *device) { g_autoptr(GError) error = NULL; g_return_if_fail(FU_IS_PLUGIN(self)); g_return_if_fail(FU_IS_DEVICE(device)); /* ensure the device ID is set from the physical and logical IDs */ if (!fu_device_ensure_id(device, &error)) { g_warning("ignoring registration: %s", error->message); return; } g_debug("emit device-register from %s: %s", fu_plugin_get_name(self), fu_device_get_id(device)); g_signal_emit(self, signals[SIGNAL_DEVICE_REGISTER], 0, device); } /** * fu_plugin_device_remove: * @self: a #FuPlugin * @device: a device * * Asks the daemon to remove a device from the exported list. * * Since: 0.8.0 **/ void fu_plugin_device_remove(FuPlugin *self, FuDevice *device) { FuPluginPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_PLUGIN(self)); g_return_if_fail(FU_IS_DEVICE(device)); g_debug("emit removed from %s: %s", fu_plugin_get_name(self), fu_device_get_id(device)); g_signal_emit(self, signals[SIGNAL_DEVICE_REMOVED], 0, device); /* remove from array */ if (priv->devices != NULL) g_ptr_array_remove(priv->devices, device); } /** * fu_plugin_check_supported: * @self: a #FuPlugin * @guid: a hardware ID GUID, e.g. `6de5d951-d755-576b-bd09-c5cf66b27234` * * Checks to see if a specific device GUID is supported, i.e. available in the * AppStream metadata. * * Returns: %TRUE if the device is supported. * * Since: 1.0.0 **/ static gboolean fu_plugin_check_supported(FuPlugin *self, const gchar *guid) { gboolean retval = FALSE; g_signal_emit(self, signals[SIGNAL_CHECK_SUPPORTED], 0, guid, &retval); return retval; } /** * fu_plugin_get_context: * @self: a #FuPlugin * * Gets the context for a plugin. * * Returns: (transfer none): a #FuContext or %NULL if not set * * Since: 1.6.0 **/ FuContext * fu_plugin_get_context(FuPlugin *self) { FuPluginPrivate *priv = GET_PRIVATE(self); return priv->ctx; } /** * fu_plugin_set_context: * @self: a #FuPlugin * @ctx: (nullable): optional #FuContext * * Sets the context for this plugin. * * Since: 1.8.6 **/ void fu_plugin_set_context(FuPlugin *self, FuContext *ctx) { FuPluginPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_PLUGIN(self)); g_return_if_fail(FU_IS_CONTEXT(ctx) || ctx == NULL); if (g_set_object(&priv->ctx, ctx)) g_object_notify(G_OBJECT(self), "context"); } static gboolean fu_plugin_device_attach(FuPlugin *self, FuDevice *device, FuProgress *progress, GError **error) { FuDevice *proxy = fu_device_get_proxy_with_fallback(device); FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(proxy); g_autoptr(FuDeviceLocker) locker = NULL; if (device_class->attach == NULL) return TRUE; locker = fu_device_locker_new(proxy, error); if (locker == NULL) return FALSE; return fu_device_attach_full(device, progress, error); } static gboolean fu_plugin_device_detach(FuPlugin *self, FuDevice *device, FuProgress *progress, GError **error) { FuDevice *proxy = fu_device_get_proxy_with_fallback(device); FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(proxy); g_autoptr(FuDeviceLocker) locker = NULL; if (device_class->detach == NULL) return TRUE; locker = fu_device_locker_new(proxy, error); if (locker == NULL) return FALSE; return fu_device_detach_full(device, progress, error); } static gboolean fu_plugin_device_activate(FuPlugin *self, FuDevice *device, FuProgress *progress, GError **error) { FuDevice *proxy = fu_device_get_proxy_with_fallback(device); FuDeviceClass *device_class = FU_DEVICE_GET_CLASS(proxy); g_autoptr(FuDeviceLocker) locker = NULL; if (device_class->activate == NULL) return TRUE; locker = fu_device_locker_new(proxy, error); if (locker == NULL) return FALSE; return fu_device_activate(device, progress, error); } static gboolean fu_plugin_device_write_firmware(FuPlugin *self, FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDevice *proxy = fu_device_get_proxy_with_fallback(device); g_autoptr(FuDeviceLocker) locker = NULL; locker = fu_device_locker_new(proxy, error); if (locker == NULL) { g_prefix_error(error, "failed to open device: "); return FALSE; } /* back the old firmware up to /var/lib/fwupd */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_BACKUP_BEFORE_INSTALL)) { g_autoptr(GBytes) fw_old = NULL; g_autofree gchar *path = NULL; g_autofree gchar *fn = NULL; g_autofree gchar *localstatedir = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_NO_PROFILE); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 25, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 75, NULL); fw_old = fu_device_dump_firmware(device, fu_progress_get_child(progress), error); if (fw_old == NULL) { g_prefix_error(error, "failed to backup old firmware: "); return FALSE; } localstatedir = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR_PKG); fn = g_strdup_printf("%s.bin", fu_device_get_version(device)); path = g_build_filename( localstatedir, "backup", fu_device_get_id(device), fu_device_get_serial(device) != NULL ? fu_device_get_serial(device) : "default", fn, NULL); fu_progress_step_done(progress); if (!fu_bytes_set_contents(path, fw_old, error)) return FALSE; if (!fu_device_write_firmware(device, firmware, fu_progress_get_child(progress), flags, error)) return FALSE; fu_progress_step_done(progress); return TRUE; } return fu_device_write_firmware(device, firmware, progress, flags, error); } static gboolean fu_plugin_device_get_results(FuPlugin *self, FuDevice *device, GError **error) { g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GError) error_local = NULL; locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; if (!fu_device_get_results(device, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) return TRUE; g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } return TRUE; } static gboolean fu_plugin_device_read_firmware(FuPlugin *self, FuDevice *device, FuProgress *progress, GError **error) { FuDevice *proxy = fu_device_get_proxy_with_fallback(device); g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(FuFirmware) firmware = NULL; g_autoptr(GBytes) fw = NULL; GChecksumType checksum_types[] = {G_CHECKSUM_SHA1, G_CHECKSUM_SHA256, 0}; locker = fu_device_locker_new(proxy, error); if (locker == NULL) return FALSE; if (!fu_device_detach_full(device, progress, error)) return FALSE; firmware = fu_device_read_firmware(device, progress, error); if (firmware == NULL) { g_autoptr(GError) error_local = NULL; if (!fu_device_attach_full(device, progress, &error_local)) g_debug("ignoring attach failure: %s", error_local->message); g_prefix_error(error, "failed to read firmware: "); return FALSE; } fw = fu_firmware_write(firmware, error); if (fw == NULL) { g_autoptr(GError) error_local = NULL; if (!fu_device_attach_full(device, progress, &error_local)) g_debug("ignoring attach failure: %s", error_local->message); g_prefix_error(error, "failed to write firmware: "); return FALSE; } for (guint i = 0; checksum_types[i] != 0; i++) { g_autofree gchar *hash = NULL; hash = g_compute_checksum_for_bytes(checksum_types[i], fw); fu_device_add_checksum(device, hash); } return fu_device_attach_full(device, progress, error); } /** * fu_plugin_runner_startup: * @self: a #FuPlugin * @error: (nullable): optional return location for an error * * Runs the startup routine for the plugin * * Returns: #TRUE for success, #FALSE for failure * * Since: 0.8.0 **/ gboolean fu_plugin_runner_startup(FuPlugin *self, FuProgress *progress, GError **error) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_PLUGIN(self), FALSE); /* progress */ fu_progress_set_name(progress, fu_plugin_get_name(self)); /* be helpful for unit tests */ fu_plugin_runner_init(self); /* not enabled */ if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_DISABLED)) return TRUE; /* optional */ if (vfuncs->startup != NULL) { g_debug("startup(%s)", fu_plugin_get_name(self)); if (!vfuncs->startup(self, progress, &error_local)) { if (error_local == NULL) { g_critical("unset plugin error in startup(%s)", fu_plugin_get_name(self)); g_set_error_literal(&error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unspecified error"); } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to startup using %s: ", fu_plugin_get_name(self)); return FALSE; } } /* success */ return TRUE; } /** * fu_plugin_runner_ready: * @self: a #FuPlugin * @error: (nullable): optional return location for an error * * Runs the ready routine for the plugin * * Returns: #TRUE for success, #FALSE for failure * * Since: 1.9.6 **/ gboolean fu_plugin_runner_ready(FuPlugin *self, FuProgress *progress, GError **error) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_PLUGIN(self), FALSE); /* progress */ fu_progress_set_name(progress, fu_plugin_get_name(self)); if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_DISABLED)) return TRUE; fu_plugin_add_flag(self, FWUPD_PLUGIN_FLAG_READY); if (vfuncs->ready == NULL) return TRUE; /* optional */ g_debug("ready(%s)", fu_plugin_get_name(self)); if (!vfuncs->ready(self, progress, &error_local)) { if (error_local == NULL) { g_critical("unset plugin error in ready(%s)", fu_plugin_get_name(self)); g_set_error_literal(&error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unspecified error"); } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to ready using %s: ", fu_plugin_get_name(self)); return FALSE; } /* success */ return TRUE; } /** * fu_plugin_runner_init: * @self: a #FuPlugin * * Runs the constructed routine for the plugin, if enabled. * * Since: 1.8.1 **/ void fu_plugin_runner_init(FuPlugin *self) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); FuPluginPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_PLUGIN(self)); /* already done */ if (priv->done_init) return; /* not enabled */ if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_DISABLED)) return; /* optional */ if (vfuncs->constructed != NULL) { g_debug("constructed(%s)", fu_plugin_get_name(self)); vfuncs->constructed(G_OBJECT(self)); priv->done_init = TRUE; } } static gboolean fu_plugin_runner_device_generic(FuPlugin *self, FuDevice *device, const gchar *symbol_name, FuPluginDeviceFunc device_func, GError **error) { g_autoptr(GError) error_local = NULL; /* not enabled */ if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_DISABLED)) return TRUE; /* optional */ if (device_func == NULL) return TRUE; g_debug("%s(%s)", symbol_name + 10, fu_plugin_get_name(self)); if (!device_func(self, device, &error_local)) { if (error_local == NULL) { g_critical("unset plugin error in %s(%s)", fu_plugin_get_name(self), symbol_name + 10); g_set_error_literal(&error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unspecified error"); } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to %s using %s: ", symbol_name + 10, fu_plugin_get_name(self)); return FALSE; } return TRUE; } static gboolean fu_plugin_runner_device_generic_progress(FuPlugin *self, FuDevice *device, FuProgress *progress, const gchar *symbol_name, FuPluginDeviceProgressFunc device_func, GError **error) { g_autoptr(GError) error_local = NULL; /* not enabled */ if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_DISABLED)) return TRUE; /* optional */ if (device_func == NULL) return TRUE; g_debug("%s(%s)", symbol_name + 10, fu_plugin_get_name(self)); if (!device_func(self, device, progress, &error_local)) { if (error_local == NULL) { g_critical("unset plugin error in %s(%s)", fu_plugin_get_name(self), symbol_name + 10); g_set_error_literal(&error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unspecified error"); } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to %s using %s: ", symbol_name + 10, fu_plugin_get_name(self)); return FALSE; } return TRUE; } static gboolean fu_plugin_runner_flagged_device_generic(FuPlugin *self, FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, const gchar *symbol_name, FuPluginFlaggedDeviceFunc func, GError **error) { g_autoptr(GError) error_local = NULL; /* not enabled */ if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_DISABLED)) return TRUE; /* optional */ if (func == NULL) return TRUE; g_debug("%s(%s)", symbol_name + 10, fu_plugin_get_name(self)); if (!func(self, device, progress, flags, &error_local)) { if (error_local == NULL) { g_critical("unset plugin error in %s(%s)", fu_plugin_get_name(self), symbol_name + 10); g_set_error_literal(&error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unspecified error"); } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to %s using %s: ", symbol_name + 10, fu_plugin_get_name(self)); return FALSE; } return TRUE; } static gboolean fu_plugin_runner_device_array_generic(FuPlugin *self, GPtrArray *devices, const gchar *symbol_name, FuPluginDeviceArrayFunc func, GError **error) { g_autoptr(GError) error_local = NULL; /* not enabled */ if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_DISABLED)) return TRUE; /* optional */ if (func == NULL) return TRUE; g_debug("%s(%s)", symbol_name + 10, fu_plugin_get_name(self)); if (!func(self, devices, &error_local)) { if (error_local == NULL) { g_critical("unset plugin error in for %s(%s)", fu_plugin_get_name(self), symbol_name + 10); g_set_error_literal(&error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unspecified error"); } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to %s using %s: ", symbol_name + 10, fu_plugin_get_name(self)); return FALSE; } return TRUE; } /** * fu_plugin_runner_coldplug: * @self: a #FuPlugin * @progress: a #FuProgress * @error: (nullable): optional return location for an error * * Runs the coldplug routine for the plugin * * Returns: #TRUE for success, #FALSE for failure * * Since: 0.8.0 **/ gboolean fu_plugin_runner_coldplug(FuPlugin *self, FuProgress *progress, GError **error) { FuPluginPrivate *priv = GET_PRIVATE(self); FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_PLUGIN(self), FALSE); /* progress */ fu_progress_set_name(progress, fu_plugin_get_name(self)); /* not enabled */ if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_DISABLED)) return TRUE; /* no HwId */ if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_REQUIRE_HWID)) return TRUE; /* optional */ if (vfuncs->coldplug == NULL) return TRUE; g_debug("coldplug(%s)", fu_plugin_get_name(self)); if (!vfuncs->coldplug(self, progress, &error_local)) { if (error_local == NULL) { g_critical("unset plugin error in coldplug(%s)", fu_plugin_get_name(self)); g_set_error_literal(&error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unspecified error"); } /* coldplug failed, but we might have already added devices to the daemon... */ if (priv->devices != NULL) { for (guint i = 0; i < priv->devices->len; i++) { FuDevice *device = g_ptr_array_index(priv->devices, i); g_warning("removing device %s due to failed coldplug", fu_device_get_id(device)); fu_plugin_device_remove(self, device); } } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to coldplug using %s: ", fu_plugin_get_name(self)); return FALSE; } return TRUE; } /** * fu_plugin_runner_composite_prepare: * @self: a #FuPlugin * @devices: (element-type FuDevice): an array of devices * @error: (nullable): optional return location for an error * * Runs the composite_prepare routine for the plugin * * Returns: #TRUE for success, #FALSE for failure * * Since: 1.0.9 **/ gboolean fu_plugin_runner_composite_prepare(FuPlugin *self, GPtrArray *devices, GError **error) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); return fu_plugin_runner_device_array_generic(self, devices, "fu_plugin_composite_prepare", vfuncs->composite_prepare, error); } /** * fu_plugin_runner_composite_cleanup: * @self: a #FuPlugin * @devices: (element-type FuDevice): an array of devices * @error: (nullable): optional return location for an error * * Runs the composite_cleanup routine for the plugin * * Returns: #TRUE for success, #FALSE for failure * * Since: 1.0.9 **/ gboolean fu_plugin_runner_composite_cleanup(FuPlugin *self, GPtrArray *devices, GError **error) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); return fu_plugin_runner_device_array_generic(self, devices, "fu_plugin_composite_cleanup", vfuncs->composite_cleanup, error); } /** * fu_plugin_runner_prepare: * @self: a #FuPlugin * @device: a device * @progress: a #FuProgress * @flags: install flags * @error: (nullable): optional return location for an error * * Runs the update_prepare routine for the plugin * * Returns: #TRUE for success, #FALSE for failure * * Since: 1.7.0 **/ gboolean fu_plugin_runner_prepare(FuPlugin *self, FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); return fu_plugin_runner_flagged_device_generic(self, device, progress, flags, "fu_plugin_prepare", vfuncs->prepare, error); } /** * fu_plugin_runner_cleanup: * @self: a #FuPlugin * @device: a device * @progress: a #FuProgress * @flags: install flags * @error: (nullable): optional return location for an error * * Runs the update_cleanup routine for the plugin * * Returns: #TRUE for success, #FALSE for failure * * Since: 1.7.0 **/ gboolean fu_plugin_runner_cleanup(FuPlugin *self, FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); return fu_plugin_runner_flagged_device_generic(self, device, progress, flags, "fu_plugin_cleanup", vfuncs->cleanup, error); } /** * fu_plugin_runner_attach: * @self: a #FuPlugin * @device: a device * @progress: a #FuProgress * @error: (nullable): optional return location for an error * * Runs the update_attach routine for the plugin * * Returns: #TRUE for success, #FALSE for failure * * Since: 1.7.0 **/ gboolean fu_plugin_runner_attach(FuPlugin *self, FuDevice *device, FuProgress *progress, GError **error) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); return fu_plugin_runner_device_generic_progress( self, device, progress, "fu_plugin_attach", vfuncs->attach != NULL ? vfuncs->attach : fu_plugin_device_attach, error); } /** * fu_plugin_runner_detach: * @self: a #FuPlugin * @device: a device * @progress: a #FuProgress * @error: (nullable): optional return location for an error * * Runs the update_detach routine for the plugin * * Returns: #TRUE for success, #FALSE for failure * * Since: 1.7.0 **/ gboolean fu_plugin_runner_detach(FuPlugin *self, FuDevice *device, FuProgress *progress, GError **error) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); return fu_plugin_runner_device_generic_progress( self, device, progress, "fu_plugin_detach", vfuncs->detach != NULL ? vfuncs->detach : fu_plugin_device_detach, error); } /** * fu_plugin_runner_reload: * @self: a #FuPlugin * @device: a device * @error: (nullable): optional return location for an error * * Runs reload routine for a device * * Returns: #TRUE for success, #FALSE for failure * * Since: 1.7.0 **/ gboolean fu_plugin_runner_reload(FuPlugin *self, FuDevice *device, GError **error) { FuDevice *proxy = fu_device_get_proxy_with_fallback(device); g_autoptr(FuDeviceLocker) locker = NULL; /* not enabled */ if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_DISABLED)) return TRUE; /* no object loaded */ locker = fu_device_locker_new(proxy, error); if (locker == NULL) return FALSE; return fu_device_reload(device, error); } /** * fu_plugin_runner_reboot_cleanup: * @self: a #FuPlugin * @device: a device * @error: (nullable): optional return location for an error * * Performs cleanup actions after the reboot has been performed. * * Returns: #TRUE for success, #FALSE for failure * * Since: 1.9.7 **/ gboolean fu_plugin_runner_reboot_cleanup(FuPlugin *self, FuDevice *device, GError **error) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); /* optional */ if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_DISABLED)) return TRUE; if (vfuncs->reboot_cleanup == NULL) return TRUE; g_debug("reboot_cleanup(%s)", fu_plugin_get_name(self)); return vfuncs->reboot_cleanup(self, device, error); } /** * fu_plugin_runner_add_security_attrs: * @self: a #FuPlugin * @attrs: a security attribute * * Runs the `add_security_attrs()` routine for the plugin * * Since: 1.5.0 **/ void fu_plugin_runner_add_security_attrs(FuPlugin *self, FuSecurityAttrs *attrs) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); /* optional, but gets called even for disabled plugins */ if (vfuncs->add_security_attrs == NULL) return; g_debug("add_security_attrs(%s)", fu_plugin_get_name(self)); vfuncs->add_security_attrs(self, attrs); } /** * fu_plugin_add_device_gtype: * @self: a #FuPlugin * @device_gtype: a #GType, e.g. `FU_TYPE_DEVICE` * * Adds the device #GType which is used when creating devices. * * If this method is used then fu_plugin_backend_device_added() is not called, and * instead the object is created in the daemon for the plugin. * * Plugins can use this method only in fu_plugin_init() * * See also: fu_plugin_set_device_gtype_default() * * Since: 1.6.0 **/ void fu_plugin_add_device_gtype(FuPlugin *self, GType device_gtype) { FuPluginPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_PLUGIN(self)); /* create as required */ if (priv->device_gtypes == NULL) priv->device_gtypes = g_array_new(FALSE, FALSE, sizeof(GType)); /* check for duplicates */ for (guint i = 0; i < priv->device_gtypes->len; i++) { GType device_gtype_tmp = g_array_index(priv->device_gtypes, GType, i); if (device_gtype == device_gtype_tmp) return; } /* ensure (to allow quirks to use it) then add */ g_type_ensure(device_gtype); g_array_append_val(priv->device_gtypes, device_gtype); } /** * fu_plugin_get_device_gtypes: * @self: a #FuPlugin * * Gets all device #GTypes. * * Returns: (element-type GType) (transfer none) (nullable): registered types, or %NULL * * Since: 2.0.2 **/ GArray * fu_plugin_get_device_gtypes(FuPlugin *self) { FuPluginPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_PLUGIN(self), NULL); return priv->device_gtypes; } /** * fu_plugin_get_device_gtype_default: * @self: a #FuPlugin * * Gets the default device #GType. * * If there is only one possible #GType added from fu_plugin_add_device_gtype() it will also be * returned here. * * Returns: a #GType, or %G_TYPE_INVALID on error * * Since: 1.9.14 **/ GType fu_plugin_get_device_gtype_default(FuPlugin *self) { FuPluginPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_PLUGIN(self), G_TYPE_INVALID); if (priv->device_gtype_default != G_TYPE_INVALID) return priv->device_gtype_default; if (priv->device_gtypes->len == 1) return g_array_index(priv->device_gtypes, GType, 0); return G_TYPE_INVALID; } /** * fu_plugin_set_device_gtype_default: * @self: a #FuPlugin * @device_gtype: a #GType, e.g. `FU_TYPE_DEVICE` * * Sets the default device #GType. * * This will also add the device #GType using fu_plugin_add_device_gtype(). * * Plugins can use this method only in fu_plugin_init() * * Since: 1.9.14 **/ void fu_plugin_set_device_gtype_default(FuPlugin *self, GType device_gtype) { FuPluginPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_PLUGIN(self)); fu_plugin_add_device_gtype(self, device_gtype); priv->device_gtype_default = device_gtype; } static gchar * fu_plugin_string_uncamelcase(const gchar *str) { GString *tmp = g_string_new(NULL); for (guint i = 0; str[i] != '\0'; i++) { if (g_ascii_islower(str[i]) || g_ascii_isdigit(str[i])) { g_string_append_c(tmp, str[i]); continue; } if (i > 0) g_string_append_c(tmp, '-'); g_string_append_c(tmp, g_ascii_tolower(str[i])); } return g_string_free(tmp, FALSE); } static gboolean fu_plugin_check_amdgpu_dpaux(FuPlugin *self, GError **error) { #ifdef __linux__ gsize bufsz = 0; g_autofree gchar *buf = NULL; g_auto(GStrv) lines = NULL; /* no module support in the kernel, we can't test for amdgpu module */ if (!g_file_test("/proc/modules", G_FILE_TEST_EXISTS)) return TRUE; if (!g_file_get_contents("/proc/modules", &buf, &bufsz, error)) return FALSE; lines = g_strsplit(buf, "\n", -1); for (guint i = 0; lines[i] != NULL; i++) { if (g_str_has_prefix(lines[i], "amdgpu ")) { /* released 2019! */ return fu_kernel_check_version("5.2.0", error); } } #endif return TRUE; } /** * fu_plugin_add_device_udev_subsystem: * @self: a #FuPlugin * @subsystem: a subsystem name, e.g. `pciport` * * Add this plugin as a possible handler of devices with this udev subsystem. * Use fu_plugin_add_udev_subsystem() if you just want to ensure the subsystem is watched. * * Plugins can use this method only in fu_plugin_init() * * Since: 1.9.3 **/ void fu_plugin_add_device_udev_subsystem(FuPlugin *self, const gchar *subsystem) { FuPluginPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_PLUGIN(self)); g_return_if_fail(subsystem != NULL); /* see https://github.com/fwupd/fwupd/issues/1121 for more details */ if (g_strcmp0(subsystem, "drm_dp_aux_dev") == 0) { g_autoptr(GError) error = NULL; if (!fu_plugin_check_amdgpu_dpaux(self, &error)) { g_warning("failed to add subsystem: %s", error->message); fu_plugin_add_flag(self, FWUPD_PLUGIN_FLAG_DISABLED); fu_plugin_add_flag(self, FWUPD_PLUGIN_FLAG_KERNEL_TOO_OLD); return; } } /* proxy */ fu_context_add_udev_subsystem(priv->ctx, subsystem, fu_plugin_get_name(self)); } /** * fu_plugin_add_udev_subsystem: * @self: a #FuPlugin * @subsystem: a subsystem name, e.g. `pciport` * * Registers the udev subsystem to be watched by the daemon. * * Plugins can use this method only in fu_plugin_init() * * Since: 1.6.2 **/ void fu_plugin_add_udev_subsystem(FuPlugin *self, const gchar *subsystem) { FuPluginPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_PLUGIN(self)); g_return_if_fail(subsystem != NULL); /* proxy */ fu_context_add_udev_subsystem(priv->ctx, subsystem, NULL); } /** * fu_plugin_add_firmware_gtype: * @self: a #FuPlugin * @id: (nullable): an optional string describing the type, e.g. `ihex` * @gtype: a #GType e.g. `FU_TYPE_FOO_FIRMWARE` * * Adds a firmware #GType which is used when creating devices. If @id is not * specified then it is guessed using the #GType name. * * Plugins can use this method only in fu_plugin_init() * * Since: 1.3.3 **/ void fu_plugin_add_firmware_gtype(FuPlugin *self, const gchar *id, GType gtype) { FuPluginPrivate *priv = GET_PRIVATE(self); g_autofree gchar *id_safe = NULL; if (id != NULL) { id_safe = g_strdup(id); } else { g_autoptr(GString) str = g_string_new(g_type_name(gtype)); if (g_str_has_prefix(str->str, "Fu")) g_string_erase(str, 0, 2); g_string_replace(str, "Firmware", "", 1); id_safe = fu_plugin_string_uncamelcase(str->str); } fu_context_add_firmware_gtype(priv->ctx, id_safe, gtype); } static gboolean fu_plugin_check_supported_device(FuPlugin *self, FuDevice *device) { GPtrArray *instance_ids = fu_device_get_instance_ids(device); for (guint i = 0; i < instance_ids->len; i++) { const gchar *instance_id = g_ptr_array_index(instance_ids, i); g_autofree gchar *guid = fwupd_guid_hash_string(instance_id); if (fu_plugin_check_supported(self, guid)) return TRUE; } return FALSE; } static gboolean fu_plugin_backend_device_added(FuPlugin *self, FuDevice *device, FuProgress *progress, GError **error) { FuDevice *proxy; FuPluginPrivate *priv = GET_PRIVATE(self); GType device_gtype = fu_device_get_specialized_gtype(FU_DEVICE(device)); GType proxy_gtype = fu_device_get_proxy_gtype(FU_DEVICE(device)); g_autoptr(FuDevice) dev = NULL; g_autoptr(FuDeviceLocker) locker = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_NO_PROFILE); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 4, "created"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 48, "open"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 48, "add"); /* fall back to plugin default */ if (device_gtype == G_TYPE_INVALID) device_gtype = fu_plugin_get_device_gtype_default(self); if (device_gtype == G_TYPE_INVALID) { if (priv->device_gtypes->len > 1) { g_autoptr(GString) str = g_string_new(NULL); for (guint i = 0; i < priv->device_gtypes->len; i++) { device_gtype = g_array_index(priv->device_gtypes, GType, i); if (str->len > 0) g_string_append(str, ","); g_string_append(str, g_type_name(device_gtype)); } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "too many GTypes to choose a default, got: %s", str->str); return FALSE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "no possible device GTypes"); return FALSE; } /* create new device and incorporate existing properties */ dev = g_object_new(device_gtype, "context", priv->ctx, NULL); fu_device_incorporate(dev, FU_DEVICE(device), FU_DEVICE_INCORPORATE_FLAG_ALL); /* any proxy device to create too? */ if (proxy_gtype != G_TYPE_INVALID) { g_autoptr(FuDevice) proxy_tmp = g_object_new(proxy_gtype, "context", priv->ctx, NULL); fu_device_incorporate(proxy_tmp, device, FU_DEVICE_INCORPORATE_FLAG_ALL); fu_device_add_private_flag(dev, FU_DEVICE_PRIVATE_FLAG_REFCOUNTED_PROXY); fu_device_set_proxy(dev, proxy_tmp); } /* notify plugins */ if (!fu_plugin_runner_device_created(self, dev, error)) return FALSE; fu_progress_step_done(progress); /* there are a lot of different devices that match, but not all respond * well to opening -- so limit some ones with issued updates */ if (fu_device_has_private_flag(dev, FU_DEVICE_PRIVATE_FLAG_ONLY_SUPPORTED)) { if (!fu_device_probe(dev, error)) return FALSE; fu_device_convert_instance_ids(dev); if (!fu_plugin_check_supported_device(self, dev)) { GPtrArray *guids = fu_device_get_guids(dev); g_autofree gchar *guids_str = fu_strjoin(",", guids); g_info("%s has no updates, so ignoring device", guids_str); fu_progress_finished(progress); return TRUE; } } /* open */ proxy = fu_device_get_proxy(dev); if (proxy != NULL) { g_autoptr(FuDeviceLocker) locker_proxy = NULL; locker_proxy = fu_device_locker_new(proxy, error); if (locker_proxy == NULL) return FALSE; fu_device_incorporate(dev, proxy, FU_DEVICE_INCORPORATE_FLAG_ALL); } locker = fu_device_locker_new(dev, error); if (locker == NULL) return FALSE; fu_progress_step_done(progress); /* add */ fu_plugin_device_add(self, dev); fu_plugin_runner_device_added(self, dev); fu_progress_step_done(progress); return TRUE; } /** * fu_plugin_runner_backend_device_added: * @self: a #FuPlugin * @device: a device * @progress: a #FuProgress * @error: (nullable): optional return location for an error * * Call the backend_device_added routine for the plugin * * Returns: #TRUE for success, #FALSE for failure * * Since: 1.5.6 **/ gboolean fu_plugin_runner_backend_device_added(FuPlugin *self, FuDevice *device, FuProgress *progress, GError **error) { FuPluginPrivate *priv = GET_PRIVATE(self); FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_PLUGIN(self), FALSE); g_return_val_if_fail(FU_IS_DEVICE(device), FALSE); g_return_val_if_fail(FU_IS_PROGRESS(progress), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* not enabled */ if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_DISABLED)) return TRUE; /* optional */ if (vfuncs->backend_device_added == NULL) { if (priv->device_gtypes != NULL || fu_device_get_specialized_gtype(device) != G_TYPE_INVALID) { return fu_plugin_backend_device_added(self, device, progress, error); } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "No device GType set"); return FALSE; } g_debug("backend_device_added(%s)", fu_plugin_get_name(self)); if (!vfuncs->backend_device_added(self, device, progress, &error_local)) { if (error_local == NULL) { g_critical("unset plugin error in backend_device_added(%s)", fu_plugin_get_name(self)); g_set_error_literal(&error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unspecified error"); } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to add device using on %s: ", fu_plugin_get_name(self)); return FALSE; } return TRUE; } /** * fu_plugin_runner_backend_device_changed: * @self: a #FuPlugin * @device: a device * @error: (nullable): optional return location for an error * * Call the backend_device_changed routine for the plugin * * Returns: #TRUE for success, #FALSE for failure * * Since: 1.5.6 **/ gboolean fu_plugin_runner_backend_device_changed(FuPlugin *self, FuDevice *device, GError **error) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_PLUGIN(self), FALSE); g_return_val_if_fail(FU_IS_DEVICE(device), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* not enabled */ if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_DISABLED)) return TRUE; /* optional */ if (vfuncs->backend_device_changed == NULL) return TRUE; g_debug("udev_device_changed(%s)", fu_plugin_get_name(self)); if (!vfuncs->backend_device_changed(self, device, &error_local)) { if (error_local == NULL) { g_critical("unset plugin error in udev_device_changed(%s)", fu_plugin_get_name(self)); g_set_error_literal(&error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unspecified error"); } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to change device on %s: ", fu_plugin_get_name(self)); return FALSE; } return TRUE; } /** * fu_plugin_runner_device_added: * @self: a #FuPlugin * @device: a device * * Call the device_added routine for the plugin * * Since: 1.5.0 **/ void fu_plugin_runner_device_added(FuPlugin *self, FuDevice *device) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); /* not enabled */ if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_DISABLED)) return; /* optional */ if (vfuncs->device_added == NULL) return; g_debug("fu_plugin_device_added(%s)", fu_plugin_get_name(self)); vfuncs->device_added(self, device); } /** * fu_plugin_runner_device_removed: * @self: a #FuPlugin * @device: a device * * Call the device_removed routine for the plugin * * Since: 1.1.2 **/ void fu_plugin_runner_device_removed(FuPlugin *self, FuDevice *device) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); g_autoptr(GError) error_local = NULL; if (!fu_plugin_runner_device_generic(self, device, "fu_plugin_backend_device_removed", vfuncs->backend_device_removed, &error_local)) g_warning("%s", error_local->message); } /** * fu_plugin_runner_device_register: * @self: a #FuPlugin * @device: a device * * Call the device_registered routine for the plugin * * Since: 0.9.7 **/ void fu_plugin_runner_device_register(FuPlugin *self, FuDevice *device) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); /* not enabled */ if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_DISABLED)) return; /* optional */ if (vfuncs->device_registered != NULL) { g_debug("fu_plugin_device_registered(%s)", fu_plugin_get_name(self)); vfuncs->device_registered(self, device); } } /** * fu_plugin_runner_device_created: * @self: a #FuPlugin * @device: a device * @error: (nullable): optional return location for an error * * Call the device_created routine for the plugin * * Returns: #TRUE for success, #FALSE for failure * * Since: 1.4.0 **/ gboolean fu_plugin_runner_device_created(FuPlugin *self, FuDevice *device, GError **error) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); g_return_val_if_fail(FU_IS_PLUGIN(self), FALSE); g_return_val_if_fail(FU_IS_DEVICE(device), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* not enabled */ if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_DISABLED)) return TRUE; /* optional */ if (vfuncs->device_created == NULL) return TRUE; g_debug("fu_plugin_device_created(%s)", fu_plugin_get_name(self)); return vfuncs->device_created(self, device, error); } /** * fu_plugin_runner_verify: * @self: a #FuPlugin * @device: a device * @progress: a #FuProgress * @flags: verify flags * @error: (nullable): optional return location for an error * * Call into the plugin's verify routine * * Returns: #TRUE for success, #FALSE for failure * * Since: 0.8.0 **/ gboolean fu_plugin_runner_verify(FuPlugin *self, FuDevice *device, FuProgress *progress, FuPluginVerifyFlags flags, GError **error) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); GPtrArray *checksums; g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_PLUGIN(self), FALSE); g_return_val_if_fail(FU_IS_DEVICE(device), FALSE); g_return_val_if_fail(FU_IS_PROGRESS(progress), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* not enabled */ if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_DISABLED)) return TRUE; /* optional */ if (vfuncs->verify == NULL) { if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_CAN_VERIFY)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device %s does not support verification", fu_device_get_id(device)); return FALSE; } return fu_plugin_device_read_firmware(self, device, progress, error); } /* clear any existing verification checksums */ checksums = fu_device_get_checksums(device); g_ptr_array_set_size(checksums, 0); /* run additional detach */ if (!fu_plugin_runner_device_generic_progress( self, device, progress, "fu_plugin_detach", vfuncs->detach != NULL ? vfuncs->detach : fu_plugin_device_detach, error)) return FALSE; /* run vfunc */ g_debug("verify(%s)", fu_plugin_get_name(self)); if (!vfuncs->verify(self, device, progress, flags, &error_local)) { g_autoptr(GError) error_attach = NULL; if (error_local == NULL) { g_critical("unset plugin error in verify(%s)", fu_plugin_get_name(self)); g_set_error_literal(&error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unspecified error"); } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to verify using %s: ", fu_plugin_get_name(self)); /* make the device "work" again, but don't prefix the error */ if (!fu_plugin_runner_device_generic_progress( self, device, progress, "fu_plugin_attach", vfuncs->attach != NULL ? vfuncs->attach : fu_plugin_device_attach, &error_attach)) { g_warning("failed to attach whilst aborting verify(): %s", error_attach->message); } return FALSE; } /* run optional attach */ if (!fu_plugin_runner_device_generic_progress( self, device, progress, "fu_plugin_attach", vfuncs->attach != NULL ? vfuncs->attach : fu_plugin_device_attach, error)) return FALSE; /* success */ return TRUE; } /** * fu_plugin_runner_activate: * @self: a #FuPlugin * @device: a device * @progress: a #FuProgress * @error: (nullable): optional return location for an error * * Call into the plugin's activate routine * * Returns: #TRUE for success, #FALSE for failure * * Since: 1.2.6 **/ gboolean fu_plugin_runner_activate(FuPlugin *self, FuDevice *device, FuProgress *progress, GError **error) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); guint64 flags; g_return_val_if_fail(FU_IS_PLUGIN(self), FALSE); g_return_val_if_fail(FU_IS_DEVICE(device), FALSE); g_return_val_if_fail(FU_IS_PROGRESS(progress), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* final check */ flags = fu_device_get_flags(device); if ((flags & FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Device %s does not need activation", fu_device_get_id(device)); return FALSE; } /* run vfunc */ if (!fu_plugin_runner_device_generic_progress( self, device, progress, "fu_plugin_activate", vfuncs->activate != NULL ? vfuncs->activate : fu_plugin_device_activate, error)) return FALSE; /* update with correct flags */ fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION); fu_device_set_modified_usec(device, g_get_real_time()); return TRUE; } /** * fu_plugin_runner_unlock: * @self: a #FuPlugin * @device: a device * @error: (nullable): optional return location for an error * * Call into the plugin's unlock routine * * Returns: #TRUE for success, #FALSE for failure * * Since: 0.8.0 **/ gboolean fu_plugin_runner_unlock(FuPlugin *self, FuDevice *device, GError **error) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); guint64 flags; g_return_val_if_fail(FU_IS_PLUGIN(self), FALSE); g_return_val_if_fail(FU_IS_DEVICE(device), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* final check */ flags = fu_device_get_flags(device); if ((flags & FWUPD_DEVICE_FLAG_LOCKED) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Device %s is not locked", fu_device_get_id(device)); return FALSE; } /* run vfunc */ if (!fu_plugin_runner_device_generic(self, device, "fu_plugin_unlock", vfuncs->unlock, error)) return FALSE; /* update with correct flags */ fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_LOCKED); fu_device_set_modified_usec(device, g_get_real_time()); return TRUE; } /** * fu_plugin_runner_write_firmware: * @self: a #FuPlugin * @device: a device * @firmware: a #FuFirmware * @progress: a #FuProgress * @flags: install flags * @error: (nullable): optional return location for an error * * Call into the plugin's write firmware routine * * Returns: #TRUE for success, #FALSE for failure * * Since: 2.0.7 **/ gboolean fu_plugin_runner_write_firmware(FuPlugin *self, FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_PLUGIN(self), FALSE); g_return_val_if_fail(FU_IS_DEVICE(device), FALSE); g_return_val_if_fail(FU_IS_FIRMWARE(firmware), FALSE); g_return_val_if_fail(FU_IS_PROGRESS(progress), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* not enabled */ if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_DISABLED)) { g_debug("plugin not enabled, skipping"); return TRUE; } /* optional */ if (vfuncs->write_firmware != NULL) { if (!vfuncs ->write_firmware(self, device, firmware, progress, flags, &error_local)) { if (error_local == NULL) { g_critical("unset plugin error in update(%s)", fu_plugin_get_name(self)); g_set_error_literal(&error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unspecified error"); return FALSE; } fu_device_set_update_error(device, error_local->message); g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } else { g_debug("superclassed write_firmware(%s)", fu_plugin_get_name(self)); return fu_plugin_device_write_firmware(self, device, firmware, progress, flags, error); } /* no longer valid */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN)) { GPtrArray *checksums = fu_device_get_checksums(device); g_ptr_array_set_size(checksums, 0); } /* success */ return TRUE; } /** * fu_plugin_runner_clear_results: * @self: a #FuPlugin * @device: a device * @error: (nullable): optional return location for an error * * Call into the plugin's clear results routine * * Returns: #TRUE for success, #FALSE for failure * * Since: 0.8.0 **/ gboolean fu_plugin_runner_clear_results(FuPlugin *self, FuDevice *device, GError **error) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_PLUGIN(self), FALSE); g_return_val_if_fail(FU_IS_DEVICE(device), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* not enabled */ if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_DISABLED)) return TRUE; /* optional */ if (vfuncs->clear_results == NULL) return TRUE; g_debug("clear_result(%s)", fu_plugin_get_name(self)); if (!vfuncs->clear_results(self, device, &error_local)) { if (error_local == NULL) { g_critical("unset plugin error in clear_result(%s)", fu_plugin_get_name(self)); g_set_error_literal(&error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unspecified error"); } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to clear_result using %s: ", fu_plugin_get_name(self)); return FALSE; } return TRUE; } /** * fu_plugin_runner_get_results: * @self: a #FuPlugin * @device: a device * @error: (nullable): optional return location for an error * * Call into the plugin's get results routine * * Returns: #TRUE for success, #FALSE for failure * * Since: 0.8.0 **/ gboolean fu_plugin_runner_get_results(FuPlugin *self, FuDevice *device, GError **error) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_PLUGIN(self), FALSE); g_return_val_if_fail(FU_IS_DEVICE(device), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* not enabled */ if (fu_plugin_has_flag(self, FWUPD_PLUGIN_FLAG_DISABLED)) return TRUE; /* optional */ if (vfuncs->get_results == NULL) { g_debug("superclassed get_results(%s)", fu_plugin_get_name(self)); return fu_plugin_device_get_results(self, device, error); } g_debug("get_results(%s)", fu_plugin_get_name(self)); if (!vfuncs->get_results(self, device, &error_local)) { if (error_local == NULL) { g_critical("unset plugin error in get_results(%s)", fu_plugin_get_name(self)); g_set_error_literal(&error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unspecified error"); } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to get_results using %s: ", fu_plugin_get_name(self)); return FALSE; } return TRUE; } /** * fu_plugin_runner_fix_host_security_attr: * @self: a #FuPlugin * @attr: (nullable): a security attribute * @error: (nullable): optional return location for an error * * Fix the specific security attribute. * * Returns: #TRUE for success, #FALSE for failure * * Since: 1.9.6 **/ gboolean fu_plugin_runner_fix_host_security_attr(FuPlugin *self, FwupdSecurityAttr *attr, GError **error) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); g_return_val_if_fail(FU_IS_PLUGIN(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (vfuncs->fix_host_security_attr == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "fix is not supported"); return FALSE; } return vfuncs->fix_host_security_attr(self, attr, error); } /** * fu_plugin_runner_undo_host_security_attr: * @self: a #FuPlugin * @attr: (nullable): a security attribute * @error: (nullable): optional return location for an error * * Fix the security issue of given security attribute. * * Returns: #TRUE for success, #FALSE for failure * * Since: 1.9.6 **/ gboolean fu_plugin_runner_undo_host_security_attr(FuPlugin *self, FwupdSecurityAttr *attr, GError **error) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); g_return_val_if_fail(FU_IS_PLUGIN(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (vfuncs->undo_host_security_attr == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "undo is not supported"); return FALSE; } return vfuncs->undo_host_security_attr(self, attr, error); } /** * fu_plugin_runner_modify_config: * @self: a #FuPlugin * @key: a config key * @value: a config value * @error: (nullable): optional return location for an error * * Sets a plugin config option, which may be allow-listed or value-checked. * * Returns: #TRUE for success, #FALSE for failure * * Since: 2.0.0 **/ gboolean fu_plugin_runner_modify_config(FuPlugin *self, const gchar *key, const gchar *value, GError **error) { FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); /* optional */ if (vfuncs->modify_config == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot modify %s=%s config", key, value); return FALSE; } g_debug("modify_config(%s)", fu_plugin_get_name(self)); return vfuncs->modify_config(self, key, value, error); } /** * fu_plugin_get_order: * @self: a #FuPlugin * * Gets the plugin order, where higher numbers are run after lower * numbers. * * Returns: the integer value * * Since: 1.0.0 **/ guint fu_plugin_get_order(FuPlugin *self) { FuPluginPrivate *priv = fu_plugin_get_instance_private(self); return priv->order; } /** * fu_plugin_set_order: * @self: a #FuPlugin * @order: an integer value * * Sets the plugin order, where higher numbers are run after lower * numbers. * * Since: 1.0.0 **/ void fu_plugin_set_order(FuPlugin *self, guint order) { FuPluginPrivate *priv = fu_plugin_get_instance_private(self); priv->order = order; } /** * fu_plugin_get_priority: * @self: a #FuPlugin * * Gets the plugin priority, where higher numbers are better. * * Returns: the integer value * * Since: 1.1.1 **/ guint fu_plugin_get_priority(FuPlugin *self) { FuPluginPrivate *priv = fu_plugin_get_instance_private(self); return priv->priority; } /** * fu_plugin_set_priority: * @self: a #FuPlugin * @priority: an integer value * * Sets the plugin priority, where higher numbers are better. * * Since: 1.0.0 **/ void fu_plugin_set_priority(FuPlugin *self, guint priority) { FuPluginPrivate *priv = fu_plugin_get_instance_private(self); priv->priority = priority; } /** * fu_plugin_add_rule: * @self: a #FuPlugin * @rule: a plugin rule, e.g. %FU_PLUGIN_RULE_CONFLICTS * @name: a plugin name, e.g. `upower` * * If the plugin name is found, the rule will be used to sort the plugin list, * for example the plugin specified by @name will be ordered after this plugin * when %FU_PLUGIN_RULE_RUN_AFTER is used. * * NOTE: The depsolver is iterative and may not solve overly-complicated rules; * If depsolving fails then fwupd will not start. * * Since: 1.0.0 **/ void fu_plugin_add_rule(FuPlugin *self, FuPluginRule rule, const gchar *name) { FuPluginPrivate *priv = fu_plugin_get_instance_private(self); if (priv->rules[rule] == NULL) priv->rules[rule] = g_ptr_array_new_with_free_func(g_free); g_ptr_array_add(priv->rules[rule], g_strdup(name)); g_signal_emit(self, signals[SIGNAL_RULES_CHANGED], 0); } /** * fu_plugin_get_rules: * @self: a #FuPlugin * @rule: a plugin rule, e.g. %FU_PLUGIN_RULE_CONFLICTS * * Gets the plugin IDs that should be run after this plugin. * * Returns: (element-type utf8) (transfer none) (nullable): the list of plugin names, e.g. *`['appstream']` * * Since: 1.0.0 **/ GPtrArray * fu_plugin_get_rules(FuPlugin *self, FuPluginRule rule) { FuPluginPrivate *priv = fu_plugin_get_instance_private(self); g_return_val_if_fail(rule < FU_PLUGIN_RULE_LAST, NULL); return priv->rules[rule]; } /** * fu_plugin_add_report_metadata: * @self: a #FuPlugin * @key: a string, e.g. `FwupdateVersion` * @value: a string, e.g. `10` * * Sets any additional metadata to be included in the firmware report to aid * debugging problems. * * Any data included here will be sent to the metadata server after user * confirmation. * * Since: 1.0.4 **/ void fu_plugin_add_report_metadata(FuPlugin *self, const gchar *key, const gchar *value) { FuPluginPrivate *priv = fu_plugin_get_instance_private(self); if (priv->report_metadata == NULL) { priv->report_metadata = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); } g_hash_table_insert(priv->report_metadata, g_strdup(key), g_strdup(value)); } /** * fu_plugin_get_report_metadata: * @self: a #FuPlugin * * Returns the list of additional metadata to be added when filing a report. * * Returns: (transfer none) (nullable): the map of report metadata * * Since: 1.0.4 **/ GHashTable * fu_plugin_get_report_metadata(FuPlugin *self) { FuPluginPrivate *priv = fu_plugin_get_instance_private(self); return priv->report_metadata; } /** * fu_plugin_get_config_value: * @self: a #FuPlugin * @key: a settings key * * Return the value of a key, falling back to the default value. * * Since: 1.0.6 **/ gchar * fu_plugin_get_config_value(FuPlugin *self, const gchar *key) { FuPluginPrivate *priv = fu_plugin_get_instance_private(self); FuConfig *config = fu_context_get_config(priv->ctx); const gchar *name; g_return_val_if_fail(FU_IS_PLUGIN(self), NULL); g_return_val_if_fail(key != NULL, NULL); if (config == NULL) { g_critical("cannot get config value with no loaded context!"); return NULL; } name = fu_plugin_get_name(self); if (name == NULL) { g_critical("cannot get config value with no plugin name!"); return NULL; } return fu_config_get_value(config, name, key); } /** * fu_plugin_set_config_default: * @self: a #FuPlugin * @key: a settings key * @value: the default value of the key if not found * * Sets the config default value. * * Since: 2.0.0 **/ void fu_plugin_set_config_default(FuPlugin *self, const gchar *key, const gchar *value) { FuPluginPrivate *priv = fu_plugin_get_instance_private(self); FuConfig *config = fu_context_get_config(priv->ctx); const gchar *name; g_return_if_fail(FU_IS_PLUGIN(self)); g_return_if_fail(key != NULL); if (config == NULL) { g_critical("cannot set config default with no loaded context!"); return; } name = fu_plugin_get_name(self); if (name == NULL) { g_critical("cannot set config default with no plugin name!"); return; } fu_config_set_default(config, name, key, value); } /** * fu_plugin_security_attr_new: * @self: a #FuPlugin * @appstream_id: (nullable): the AppStream component ID, e.g. `com.intel.BiosGuard` * * Creates a new #FwupdSecurityAttr for this specific plugin. * * Returns: (transfer full): a #FwupdSecurityAttr * * Since: 1.8.4 **/ FwupdSecurityAttr * fu_plugin_security_attr_new(FuPlugin *self, const gchar *appstream_id) { FuPluginPrivate *priv = fu_plugin_get_instance_private(self); g_autoptr(FwupdSecurityAttr) attr = NULL; g_return_val_if_fail(FU_IS_PLUGIN(self), NULL); g_return_val_if_fail(appstream_id != NULL, NULL); attr = fu_security_attr_new(priv->ctx, appstream_id); fwupd_security_attr_set_plugin(attr, fu_plugin_get_name(self)); return g_steal_pointer(&attr); } /** * fu_plugin_set_config_value: * @self: a #FuPlugin * @key: a settings key * @value: (nullable): a settings value * @error: (nullable): optional return location for an error * * Sets a plugin config value. * * Returns: %TRUE for success * * Since: 1.7.0 **/ gboolean fu_plugin_set_config_value(FuPlugin *self, const gchar *key, const gchar *value, GError **error) { FuPluginPrivate *priv = fu_plugin_get_instance_private(self); FuConfig *config = fu_context_get_config(priv->ctx); const gchar *name; g_return_val_if_fail(FU_IS_PLUGIN(self), FALSE); g_return_val_if_fail(key != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (config == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "cannot get config value with no loaded context"); return FALSE; } name = fu_plugin_get_name(self); if (name == NULL) { g_critical("cannot get config value with no plugin name!"); return FALSE; } return fu_config_set_value(config, name, key, value, error); } /** * fu_plugin_reset_config_values: * @self: a #FuPlugin * @error: (nullable): optional return location for an error * * Reset all the plugin keys back to the default values. * * Returns: %TRUE for success * * Since: 1.9.15 **/ gboolean fu_plugin_reset_config_values(FuPlugin *self, GError **error) { FuPluginPrivate *priv = fu_plugin_get_instance_private(self); FuConfig *config = fu_context_get_config(priv->ctx); const gchar *name; g_return_val_if_fail(FU_IS_PLUGIN(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (config == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "cannot reset config values with no loaded context"); return FALSE; } name = fu_plugin_get_name(self); if (name == NULL) { g_critical("cannot reset config values with no plugin name!"); return FALSE; } return fu_config_reset_defaults(config, name, error); } /** * fu_plugin_get_config_value_boolean: * @self: a #FuPlugin * @key: a settings key * * Return the boolean value of a key if it's been configured * * Returns: %TRUE if the value is `true` (case insensitive), %FALSE otherwise * * Since: 1.4.0 **/ gboolean fu_plugin_get_config_value_boolean(FuPlugin *self, const gchar *key) { FuPluginPrivate *priv = fu_plugin_get_instance_private(self); FuConfig *config = fu_context_get_config(priv->ctx); const gchar *name; g_return_val_if_fail(FU_IS_PLUGIN(self), FALSE); g_return_val_if_fail(key != NULL, FALSE); if (config == NULL) { g_critical("cannot get config value with no loaded context!"); return FALSE; } name = fu_plugin_get_name(self); if (name == NULL) { g_critical("cannot get config value with no plugin name!"); return FALSE; } return fu_config_get_value_bool(config, name, key); } /** * fu_plugin_name_compare: * @plugin1: first #FuPlugin to compare. * @plugin2: second #FuPlugin to compare. * * Compares two plugins by their names. * * Returns: 1, 0 or -1 if @plugin1 is greater, equal, or less than @plugin2. * * Since: 1.0.8 **/ gint fu_plugin_name_compare(FuPlugin *plugin1, FuPlugin *plugin2) { return g_strcmp0(fu_plugin_get_name(plugin1), fu_plugin_get_name(plugin2)); } /** * fu_plugin_order_compare: * @plugin1: first #FuPlugin to compare. * @plugin2: second #FuPlugin to compare. * * Compares two plugins by their depsolved order, and then by name. * * Returns: 1, 0 or -1 if @plugin1 is greater, equal, or less than @plugin2. * * Since: 1.0.8 **/ gint fu_plugin_order_compare(FuPlugin *plugin1, FuPlugin *plugin2) { FuPluginPrivate *priv1 = fu_plugin_get_instance_private(plugin1); FuPluginPrivate *priv2 = fu_plugin_get_instance_private(plugin2); if (priv1->order < priv2->order) return -1; if (priv1->order > priv2->order) return 1; return fu_plugin_name_compare(plugin1, plugin2); } static gchar * fu_plugin_convert_gtype_to_name(GType gtype) { const gchar *gtype_name = g_type_name(gtype); gsize len = strlen(gtype_name); g_autoptr(GString) str = g_string_new(NULL); g_return_val_if_fail(g_str_has_prefix(gtype_name, "Fu"), NULL); g_return_val_if_fail(g_str_has_suffix(gtype_name, "Plugin"), NULL); /* self tests */ if (g_strcmp0(gtype_name, "FuPlugin") == 0) return g_strdup("plugin"); /* normal plugins */ for (guint j = 2; j < len - 6; j++) { gchar tmp = gtype_name[j]; if (g_ascii_isupper(tmp)) { if (str->len > 0) g_string_append_c(str, '_'); g_string_append_c(str, g_ascii_tolower(tmp)); } else { g_string_append_c(str, tmp); } } if (str->len == 0) return NULL; return g_string_free(g_steal_pointer(&str), FALSE); } static void fu_plugin_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuPlugin *self = FU_PLUGIN(object); FuPluginPrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_CONTEXT: g_value_set_object(value, priv->ctx); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_plugin_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuPlugin *self = FU_PLUGIN(object); switch (prop_id) { case PROP_CONTEXT: fu_plugin_set_context(self, g_value_get_object(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_plugin_dispose(GObject *object) { FuPlugin *self = FU_PLUGIN(object); FuPluginPrivate *priv = GET_PRIVATE(self); if (priv->devices != NULL) g_ptr_array_set_size(priv->devices, 0); if (priv->cache != NULL) g_hash_table_remove_all(priv->cache); g_clear_object(&priv->ctx); G_OBJECT_CLASS(fu_plugin_parent_class)->dispose(object); } static void fu_plugin_class_init(FuPluginClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); GParamSpec *pspec; object_class->finalize = fu_plugin_finalize; object_class->dispose = fu_plugin_dispose; object_class->get_property = fu_plugin_get_property; object_class->set_property = fu_plugin_set_property; /** * FuPlugin::device-added: * @self: the #FuPlugin instance that emitted the signal * @device: the #FuDevice * * The ::device-added signal is emitted when a device has been added by the plugin. * * Since: 0.8.0 **/ signals[SIGNAL_DEVICE_ADDED] = g_signal_new("device-added", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(FuPluginClass, _device_added), NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, FU_TYPE_DEVICE); /** * FuPlugin::device-removed: * @self: the #FuPlugin instance that emitted the signal * @device: the #FuDevice * * The ::device-removed signal is emitted when a device has been removed by the plugin. * * Since: 0.8.0 **/ signals[SIGNAL_DEVICE_REMOVED] = g_signal_new("device-removed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(FuPluginClass, _device_removed), NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, FU_TYPE_DEVICE); /** * FuPlugin::device-register: * @self: the #FuPlugin instance that emitted the signal * @device: the #FuDevice * * The ::device-register signal is emitted when another plugin has added the device. * * Since: 0.9.7 **/ signals[SIGNAL_DEVICE_REGISTER] = g_signal_new("device-register", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(FuPluginClass, _device_register), NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, FU_TYPE_DEVICE); /** * FuPlugin::check-supported: * @self: the #FuPlugin instance that emitted the signal * @guid: a device GUID * * The ::check-supported signal is emitted when a plugin wants to ask the daemon if a * specific device GUID is supported in the existing system metadata. * * Returns: %TRUE if the GUID is found * * Since: 1.0.0 **/ signals[SIGNAL_CHECK_SUPPORTED] = g_signal_new("check-supported", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(FuPluginClass, _check_supported), NULL, NULL, g_cclosure_marshal_generic, G_TYPE_BOOLEAN, 1, G_TYPE_STRING); signals[SIGNAL_RULES_CHANGED] = g_signal_new("rules-changed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(FuPluginClass, _rules_changed), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); /** * FuPlugin:context: * * The #FuContext to use. * * Since: 1.8.6 */ pspec = g_param_spec_object("context", NULL, NULL, FU_TYPE_CONTEXT, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_CONTEXT, pspec); } static void fu_plugin_init(FuPlugin *self) { FuPluginPrivate *priv = GET_PRIVATE(self); priv->device_gtype_default = G_TYPE_INVALID; } static void fu_plugin_finalize(GObject *object) { FuPlugin *self = FU_PLUGIN(object); FuPluginPrivate *priv = GET_PRIVATE(self); FuPluginVfuncs *vfuncs = fu_plugin_get_vfuncs(self); /* optional */ if (priv->done_init && vfuncs->finalize != NULL) { g_debug("finalize(%s)", fu_plugin_get_name(self)); vfuncs->finalize(G_OBJECT(self)); } for (guint i = 0; i < FU_PLUGIN_RULE_LAST; i++) { if (priv->rules[i] != NULL) g_ptr_array_unref(priv->rules[i]); } if (priv->devices != NULL) g_ptr_array_unref(priv->devices); if (priv->runtime_versions != NULL) g_hash_table_unref(priv->runtime_versions); if (priv->compile_versions != NULL) g_hash_table_unref(priv->compile_versions); if (priv->report_metadata != NULL) g_hash_table_unref(priv->report_metadata); if (priv->cache != NULL) g_hash_table_unref(priv->cache); if (priv->device_gtypes != NULL) g_array_unref(priv->device_gtypes); if (priv->config_monitor != NULL) g_object_unref(priv->config_monitor); g_free(priv->data); G_OBJECT_CLASS(fu_plugin_parent_class)->finalize(object); } /** * fu_plugin_new_from_gtype: * @ctx: (nullable): a #FuContext * @gtype: a #GType, possibly even `G_TYPE_PLUGIN` * * Creates a new #FuPlugin * * Since: 1.8.6 **/ FuPlugin * fu_plugin_new_from_gtype(GType gtype, FuContext *ctx) { FuPlugin *self; g_return_val_if_fail(gtype != G_TYPE_INVALID, NULL); g_return_val_if_fail(ctx == NULL || FU_IS_CONTEXT(ctx), NULL); self = g_object_new(gtype, "context", ctx, NULL); if (fu_plugin_get_name(self) == NULL) { g_autofree gchar *name = fu_plugin_convert_gtype_to_name(gtype); fu_plugin_set_name(self, name); } return self; } /** * fu_plugin_new: * @ctx: (nullable): a #FuContext * * Creates a new #FuPlugin * * Since: 0.8.0 **/ FuPlugin * fu_plugin_new(FuContext *ctx) { FuPlugin *self = FU_PLUGIN(g_object_new(FU_TYPE_PLUGIN, NULL)); if (ctx != NULL) fu_plugin_set_context(self, ctx); return self; } fwupd-2.0.10/libfwupdplugin/fu-plugin.h000066400000000000000000000347111501337203100200320ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-bluez-device.h" #include "fu-common-guid.h" #include "fu-common.h" #include "fu-context.h" #include "fu-device-locker.h" #include "fu-device.h" #include "fu-plugin.h" #include "fu-quirks.h" #include "fu-security-attrs.h" #include "fu-version-common.h" /* only until HSI is declared stable */ #include "fwupd-security-attr-private.h" #define FU_TYPE_PLUGIN (fu_plugin_get_type()) G_DECLARE_DERIVABLE_TYPE(FuPlugin, fu_plugin, FU, PLUGIN, FwupdPlugin) #define fu_plugin_get_flags(p) fwupd_plugin_get_flags(FWUPD_PLUGIN(p)) #define fu_plugin_has_flag(p, f) fwupd_plugin_has_flag(FWUPD_PLUGIN(p), f) #define fu_plugin_add_flag(p, f) fwupd_plugin_add_flag(FWUPD_PLUGIN(p), f) #define fu_plugin_remove_flag(p, f) fwupd_plugin_remove_flag(FWUPD_PLUGIN(p), f) /** * FuPluginVerifyFlags: * @FU_PLUGIN_VERIFY_FLAG_NONE: No flags set * * Flags used when verifying, currently unused. **/ typedef enum { FU_PLUGIN_VERIFY_FLAG_NONE = 0, /*< private >*/ FU_PLUGIN_VERIFY_FLAG_LAST } FuPluginVerifyFlags; struct _FuPluginClass { FwupdPluginClass parent_class; /* signals */ void (*_device_added)(FuPlugin *self, FuDevice *device); void (*_device_removed)(FuPlugin *self, FuDevice *device); void (*_status_changed)(FuPlugin *self, FwupdStatus status); void (*_percentage_changed)(FuPlugin *self, guint percentage); void (*_device_register)(FuPlugin *self, FuDevice *device); gboolean (*_check_supported)(FuPlugin *self, const gchar *guid); void (*_rules_changed)(FuPlugin *self); /* vfuncs */ /** * init: * @self: A #FuPlugin * * Initializes the modular plugin. * Sets up any static data structures for the plugin. * * Since: 1.7.2 **/ void (*constructed)(GObject *obj); /** * finalize: * @self: a plugin * * Destroys the modular plugin. * Any allocated memory should be freed here. * * Since: 1.7.2 **/ void (*finalize)(GObject *obj); /** * startup: * @self: a #FuPlugin * @progress: a #FuProgress * @error: (nullable): optional return location for an error * * Tries to start the plugin. * Returns: TRUE for success or FALSE for failure. * * Any plugins not intended for the system or that have failure communicating * with the device should return FALSE. * Any allocated memory should be freed here. * * Since: 1.7.2 **/ gboolean (*startup)(FuPlugin *self, FuProgress *progress, GError **error); /** * ready: * @self: a #FuPlugin * @progress: a #FuProgress * @error: (nullable): optional return location for an error * * Tells the plugin that all devices have been coldplugged and the plugin is * ready to be used. * * Returns: TRUE for success or FALSE for failure. * * NOTE: Any plugins not intended for the system or that have failure communicating * with the device should return %FALSE and set @error. * * Since: 1.9.6 **/ gboolean (*ready)(FuPlugin *self, FuProgress *progress, GError **error); /** * coldplug: * @self: a #FuPlugin * @progress: a #FuProgress * @error: (nullable): optional return location for an error * * Probes for devices. * * Since: 1.7.2 **/ gboolean (*coldplug)(FuPlugin *self, FuProgress *progress, GError **error); /** * device_created * @self: a #FuPlugin * @dev: a device * @error: (nullable): optional return location for an error * * Function run when the subclassed device has been created. * * Since: 1.7.2 **/ gboolean (*device_created)(FuPlugin *self, FuDevice *device, GError **error); /** * device_registered * @self: a #FuPlugin * @dev: a device * * Function run when device registered from another plugin. * * Since: 1.7.2 **/ void (*device_registered)(FuPlugin *self, FuDevice *device); /** * device_added * @self: a #FuPlugin * @dev: a device * * Function run when the subclassed device has been added. * * Since: 1.7.2 **/ void (*device_added)(FuPlugin *self, FuDevice *device); /** * verify: * @self: a #FuPlugin * @dev: a device * @progress: a #FuProgress * @flags: verify flags * @error: (nullable): optional return location for an error * * Verifies the firmware on the device matches the value stored in the database * * Since: 1.7.2 **/ gboolean (*verify)(FuPlugin *self, FuDevice *device, FuProgress *progress, FuPluginVerifyFlags flags, GError **error); /** * get_results: * @self: a #FuPlugin * @dev: a device * @error: (nullable): optional return location for an error * * Obtains historical update results for the device. * * Since: 1.7.2 **/ gboolean (*get_results)(FuPlugin *self, FuDevice *device, GError **error); /** * clear_results: * @self: a #FuPlugin * @dev: a device * @error: (nullable): optional return location for an error * * Clears stored update results for the device. * * Since: 1.7.2 **/ gboolean (*clear_results)(FuPlugin *self, FuDevice *device, GError **error); /** * backend_device_added * @self: a #FuPlugin * @device: a device * @progress: a #FuProgress * @error: (nullable): optional return location for an error * * Function to run after a device is added by a backend, e.g. by USB or Udev. * * Since: 1.7.2 **/ gboolean (*backend_device_added)(FuPlugin *self, FuDevice *device, FuProgress *progress, GError **error); /** * backend_device_changed * @self: a #FuPlugin * @device: a device * @error: (nullable): optional return location for an error * * Function run when the device changed. * * Since: 1.7.2 **/ gboolean (*backend_device_changed)(FuPlugin *self, FuDevice *device, GError **error); /** * backend_device_removed * @self: a #FuPlugin * @device: a device * @error: (nullable): optional return location for an error * * Function to run when device is physically removed. * * Since: 1.7.2 **/ gboolean (*backend_device_removed)(FuPlugin *self, FuDevice *device, GError **error); /** * add_security_attrs * @self: a #FuPlugin * @attrs: a security attribute * * Function that asks plugins to add Host Security Attributes. * * Since: 1.7.2 **/ void (*add_security_attrs)(FuPlugin *self, FuSecurityAttrs *attrs); /** * write_firmware: * @self: a #FuPlugin * @dev: a device * @firmware: a #FuFirmware * @progress: a #FuProgress * @flags: install flags * @error: (nullable): optional return location for an error * * Updates the firmware on the device. * * Since: 2.0.7 **/ gboolean (*write_firmware)(FuPlugin *self, FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error); /** * unlock: * @self: a #FuPlugin * @dev: a device * @error: (nullable): optional return location for an error * * Unlocks the device for writes. * * Since: 1.7.2 **/ gboolean (*unlock)(FuPlugin *self, FuDevice *device, GError **error); /** * activate: * @self: a #FuPlugin * @dev: a device * @error: (nullable): optional return location for an error * * Activates the new firmware on the device. * * This is intended for devices that it is not safe to immediately activate * the firmware. It may be called at a more convenient time instead. * * Since: 1.7.2 **/ gboolean (*activate)(FuPlugin *self, FuDevice *device, FuProgress *progress, GError **error); /** * attach: * @self: a #FuPlugin * @dev: a device * @error: (nullable): optional return location for an error * * Swaps the device from bootloader mode to runtime mode. * * Since: 1.7.2 **/ gboolean (*attach)(FuPlugin *self, FuDevice *device, FuProgress *progress, GError **error); /** * detach: * @self: a #FuPlugin * @dev: a device * @error: (nullable): optional return location for an error * * Swaps the device from runtime mode to bootloader mode. * * Since: 1.7.2 **/ gboolean (*detach)(FuPlugin *self, FuDevice *device, FuProgress *progress, GError **error); /** * prepare: * @self: a #FuPlugin * @device: a device * @progress: a #FuProgress * @flags: install flags * @error: (nullable): optional return location for an error * * Prepares the device to receive an update. * * Since: 1.7.2 **/ gboolean (*prepare)(FuPlugin *self, FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error); /** * cleanup * @self: a #FuPlugin * @device: a device * @progress: a #FuProgress * @flags: install flags * @error: (nullable): optional return location for an error * * Cleans up the device after receiving an update. * * Since: 1.7.2 **/ gboolean (*cleanup)(FuPlugin *self, FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error); /** * composite_prepare * @self: a #FuPlugin * @devices: (element-type FuDevice): array of devices * @error: (nullable): optional return location for an error * * Function run before updating group of composite devices. * * Since: 1.7.2 **/ gboolean (*composite_prepare)(FuPlugin *self, GPtrArray *devices, GError **error); /** * composite_cleanup * @self: a #FuPlugin * @devices: (element-type FuDevice): array of devices * @error: (nullable): optional return location for an error * * Function run after updating group of composite devices. * * Since: 1.7.2 **/ gboolean (*composite_cleanup)(FuPlugin *self, GPtrArray *devices, GError **error); /** * load * @ctx: a #FuContext * * Function to register context attributes, run during early startup even on plugins which * will be later disabled. * * Since: 1.8.1 **/ void (*load)(FuContext *ctx); /** * to_string: * @self: A #FuPlugin * * Prints plugin private data to the console. * * Since: 1.8.4 **/ void (*to_string)(FuPlugin *self, guint idt, GString *str); /** * fix_host_security_attr: * @self: a #FuPlugin * @attr: a #FwupdSecurityAttr * @error: (nullable): optional return location for an error * * Fix a host security issue. * * Since: 1.9.6 **/ gboolean (*fix_host_security_attr)(FuPlugin *self, FwupdSecurityAttr *attr, GError **error); /** * undo_host_security_attr: * @self: a #FuPlugin * @attr: a #FwupdSecurityAttr * @error: (nullable): optional return location for an error * * Undo the fix for a host security issue. * * Since: 1.9.6 **/ gboolean (*undo_host_security_attr)(FuPlugin *self, FwupdSecurityAttr *attr, GError **error); /** * reboot_cleanup: * @self: a #FuPlugin * @device: a device * @error: (nullable): optional return location for an error * * Performs cleanup actions after the reboot has been performed. * * Since: 1.9.7 **/ gboolean (*reboot_cleanup)(FuPlugin *self, FuDevice *device, GError **error); /** * modify_config: * @self: a #FuPlugin * @key: a config key * @value: a config value * @error: (nullable): optional return location for an error * * Sets a plugin config option, which may be allow-listed or value-checked. * * Since: 2.0.0 **/ gboolean (*modify_config)(FuPlugin *self, const gchar *key, const gchar *value, GError **error); }; /** * FuPluginVfuncs: * * A subset of virtual functions that are implemented by modular plugins. **/ typedef struct _FuPluginClass FuPluginVfuncs; /** * FuPluginRule: * @FU_PLUGIN_RULE_CONFLICTS: The plugin conflicts with another * @FU_PLUGIN_RULE_RUN_AFTER: Order the plugin after another * @FU_PLUGIN_RULE_RUN_BEFORE: Order the plugin before another * @FU_PLUGIN_RULE_BETTER_THAN: Is better than another plugin * @FU_PLUGIN_RULE_INHIBITS_IDLE: The plugin inhibits the idle shutdown * @FU_PLUGIN_RULE_METADATA_SOURCE: Uses another plugin as a source of report metadata * * The rules used for ordering plugins. * Plugins are expected to add rules in fu_plugin_initialize(). **/ typedef enum { FU_PLUGIN_RULE_CONFLICTS, FU_PLUGIN_RULE_RUN_AFTER, FU_PLUGIN_RULE_RUN_BEFORE, FU_PLUGIN_RULE_BETTER_THAN, FU_PLUGIN_RULE_INHIBITS_IDLE, FU_PLUGIN_RULE_METADATA_SOURCE, /* Since: 1.3.6 */ /*< private >*/ FU_PLUGIN_RULE_LAST } FuPluginRule; /** * FuPluginData: * * The plugin-allocated private data. **/ typedef struct FuPluginData FuPluginData; /* for plugins to use */ const gchar * fu_plugin_get_name(FuPlugin *self) G_GNUC_NON_NULL(1); void fu_plugin_set_name(FuPlugin *self, const gchar *name) G_GNUC_NON_NULL(1); FuPluginData * fu_plugin_get_data(FuPlugin *self) G_GNUC_NON_NULL(1); FuPluginData * fu_plugin_alloc_data(FuPlugin *self, gsize data_sz) G_GNUC_NON_NULL(1); FuContext * fu_plugin_get_context(FuPlugin *self) G_GNUC_NON_NULL(1); void fu_plugin_device_add(FuPlugin *self, FuDevice *device) G_GNUC_NON_NULL(1, 2); void fu_plugin_device_remove(FuPlugin *self, FuDevice *device) G_GNUC_NON_NULL(1, 2); void fu_plugin_device_register(FuPlugin *self, FuDevice *device) G_GNUC_NON_NULL(1, 2); void fu_plugin_add_device_gtype(FuPlugin *self, GType device_gtype) G_GNUC_NON_NULL(1); GType fu_plugin_get_device_gtype_default(FuPlugin *self) G_GNUC_NON_NULL(1); void fu_plugin_set_device_gtype_default(FuPlugin *self, GType device_gtype) G_GNUC_NON_NULL(1); void fu_plugin_add_firmware_gtype(FuPlugin *self, const gchar *id, GType gtype) G_GNUC_NON_NULL(1); void fu_plugin_add_device_udev_subsystem(FuPlugin *self, const gchar *subsystem) G_GNUC_NON_NULL(1, 2); void fu_plugin_add_udev_subsystem(FuPlugin *self, const gchar *subsystem) G_GNUC_NON_NULL(1, 2); gpointer fu_plugin_cache_lookup(FuPlugin *self, const gchar *id) G_GNUC_NON_NULL(1, 2); void fu_plugin_cache_remove(FuPlugin *self, const gchar *id) G_GNUC_NON_NULL(1, 2); void fu_plugin_cache_add(FuPlugin *self, const gchar *id, gpointer dev) G_GNUC_NON_NULL(1, 2); GPtrArray * fu_plugin_get_devices(FuPlugin *self) G_GNUC_NON_NULL(1); void fu_plugin_add_rule(FuPlugin *self, FuPluginRule rule, const gchar *name) G_GNUC_NON_NULL(1, 3); void fu_plugin_add_report_metadata(FuPlugin *self, const gchar *key, const gchar *value) G_GNUC_NON_NULL(1, 2, 3); void fu_plugin_set_config_default(FuPlugin *self, const gchar *key, const gchar *value) G_GNUC_NON_NULL(1, 2); gchar * fu_plugin_get_config_value(FuPlugin *self, const gchar *key) G_GNUC_NON_NULL(1, 2); gboolean fu_plugin_get_config_value_boolean(FuPlugin *self, const gchar *key) G_GNUC_NON_NULL(1, 2); gboolean fu_plugin_set_config_value(FuPlugin *self, const gchar *key, const gchar *value, GError **error) G_GNUC_NON_NULL(1, 2); FwupdSecurityAttr * fu_plugin_security_attr_new(FuPlugin *self, const gchar *appstream_id) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-progress-private.h000066400000000000000000000003461501337203100220450ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-progress.h" gdouble fu_progress_get_global_fraction(FuProgress *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-progress.c000066400000000000000000000710471501337203100203760ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuProgress" #include "config.h" #include #include "fu-progress-private.h" #include "fu-string.h" /** * FuProgress: * * Objects can use fu_progress_set_percentage() if the absolute percentage * is known. Percentages should always go up, not down. * * Modules usually set the number of steps that are expected using * fu_progress_set_steps() and then after each section is completed, * the fu_progress_step_done() function should be called. This will automatically * call fu_progress_set_percentage() with the correct values. * * #FuProgress allows sub-modules to be "chained up" to the parent module * so that as the sub-module progresses, so does the parent. * The child can be reused for each section, and chains can be deep. * * To get a child object, you should use [method@FuProgress.get_child]. and then * use the result in any sub-process. You should ensure that the child * is not re-used without calling fu_progress_step_done(). * * There are a few nice touches in this module, so that if a module only has * one progress step, the child progress is used for parent updates. * * static void * _do_something(FuProgress *self) * { * // setup correct number of steps * fu_progress_set_steps(self, 2); * * // run a sub function * _do_something_else1(fu_progress_get_child(self)); * * // this section done * fu_progress_step_done(self); * * // run another sub function * _do_something_else2(fu_progress_get_child(self)); * * // this progress done (all complete) * fu_progress_step_done(self); * } * * See also: [class@FuDevice] */ struct _FuProgress { GObject parent_instance; gchar *id; gchar *name; FuProgressFlag flags; guint percentage; FwupdStatus status; GPtrArray *children; /* of FuProgress */ gboolean profile; gdouble duration; /* seconds */ gdouble global_fraction; guint step_weighting; GTimer *timer; GTimer *timer_child; guint step_now; guint step_done; guint step_scaling; FuProgress *parent; /* no-ref */ }; enum { SIGNAL_PERCENTAGE_CHANGED, SIGNAL_STATUS_CHANGED, SIGNAL_LAST }; static guint signals[SIGNAL_LAST] = {0}; static void fu_progress_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_WITH_CODE(FuProgress, fu_progress, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fu_progress_codec_iface_init)) #define FU_PROGRESS_STEPS_MAX 1000 /** * fu_progress_get_id: * @self: a #FuProgress * * Return the id of the progress, which is normally set by the caller. * * Returns: progress ID * * Since: 1.7.0 **/ const gchar * fu_progress_get_id(FuProgress *self) { g_return_val_if_fail(FU_IS_PROGRESS(self), NULL); return self->id; } /** * fu_progress_set_id: * @self: a #FuProgress * @id: progress ID, normally `G_STRLOC` * * Sets the id of the progress. * * Since: 1.7.0 **/ void fu_progress_set_id(FuProgress *self, const gchar *id) { g_return_if_fail(FU_IS_PROGRESS(self)); g_return_if_fail(id != NULL); /* not changed */ if (g_strcmp0(self->id, id) == 0) return; /* set id */ g_free(self->id); self->id = g_strdup(id); } /** * fu_progress_get_name: * @self: a #FuProgress * * Return the nice name of the progress, which is normally set by the caller. * * Returns: progress nice name, e.g. `add-devices` * * Since: 1.8.2 **/ const gchar * fu_progress_get_name(FuProgress *self) { g_return_val_if_fail(FU_IS_PROGRESS(self), NULL); return self->name; } static const gchar * fu_progress_get_name_fallback(FuProgress *self) { if (self->name != NULL) return self->name; return fwupd_status_to_string(self->status); } /** * fu_progress_set_name: * @self: a #FuProgress * @name: progress nice name, e.g. `add-devices`, or perhaps just `G_STRFUNC` * * Sets the nice name of the progress. * * Since: 1.8.2 **/ void fu_progress_set_name(FuProgress *self, const gchar *name) { g_return_if_fail(FU_IS_PROGRESS(self)); g_return_if_fail(name != NULL); /* not changed */ if (g_strcmp0(self->name, name) == 0) return; /* set name */ g_free(self->name); self->name = g_strdup(name); } /** * fu_progress_get_status: * @self: a #FuProgress * * Return the status of the progress, which is normally indirectly by fu_progress_add_step(). * * Returns: status * * Since: 1.7.0 **/ FwupdStatus fu_progress_get_status(FuProgress *self) { g_return_val_if_fail(FU_IS_PROGRESS(self), FWUPD_STATUS_UNKNOWN); return self->status; } /** * fu_progress_add_flag: * @self: a #FuProgress * @flag: an internal progress flag, e.g. %FU_PROGRESS_FLAG_GUESSED * * Adds a flag. * * Since: 1.7.0 **/ void fu_progress_add_flag(FuProgress *self, FuProgressFlag flag) { g_return_if_fail(FU_IS_PROGRESS(self)); self->flags |= flag; } /** * fu_progress_remove_flag: * @self: a #FuProgress * @flag: an internal progress flag, e.g. %FU_PROGRESS_FLAG_GUESSED * * Removes a flag. * * Since: 1.7.0 **/ void fu_progress_remove_flag(FuProgress *self, FuProgressFlag flag) { g_return_if_fail(FU_IS_PROGRESS(self)); self->flags &= ~flag; } /** * fu_progress_has_flag: * @self: a #FuProgress * @flag: an internal progress flag, e.g. %FU_PROGRESS_FLAG_GUESSED * * Tests for a flag. * * Since: 1.7.0 **/ gboolean fu_progress_has_flag(FuProgress *self, FuProgressFlag flag) { g_return_val_if_fail(FU_IS_PROGRESS(self), FALSE); return (self->flags & flag) > 0; } /** * fu_progress_set_status: * @self: a #FuProgress * @status: device status * * Sets the status of the progress. * * Since: 1.7.0 **/ void fu_progress_set_status(FuProgress *self, FwupdStatus status) { g_return_if_fail(FU_IS_PROGRESS(self)); /* not changed */ if (self->status == status) return; /* save */ self->status = status; g_signal_emit(self, signals[SIGNAL_STATUS_CHANGED], 0, status); } /** * fu_progress_get_percentage: * @self: a #FuProgress * * Get the last set progress percentage. * * Return value: The percentage value, or %G_MAXUINT for error * * Since: 1.7.0 **/ guint fu_progress_get_percentage(FuProgress *self) { g_return_val_if_fail(FU_IS_PROGRESS(self), G_MAXUINT); if (self->percentage == G_MAXUINT) return 0; return self->percentage; } static void fu_progress_set_parent(FuProgress *self, FuProgress *parent) { g_return_if_fail(FU_IS_PROGRESS(self)); g_return_if_fail(FU_IS_PROGRESS(parent)); self->parent = parent; /* no ref! */ self->profile = fu_progress_get_profile(parent); } /** * fu_progress_get_duration: * @self: a #FuProgress * * Get the duration of the step. * * Return value: The duration value in seconds * * Since: 1.8.2 **/ gdouble fu_progress_get_duration(FuProgress *self) { return self->duration; } static void fu_progress_set_duration(FuProgress *self, gdouble duration) { self->duration = duration; } static void fu_progress_build_parent_chain(FuProgress *self, GString *str, guint level) { if (self->parent != NULL) fu_progress_build_parent_chain(self->parent, str, level + 1); g_string_append_printf(str, "%u) %s (%u/%u)\n", level, self->id, self->step_now, self->children->len); } /** * fu_progress_set_percentage: * @self: a #FuProgress * @percentage: value between 0% and 100% * * Sets the progress percentage complete. * * NOTE: this must be above what was previously set, or it will be rejected. * * Since: 1.7.0 **/ void fu_progress_set_percentage(FuProgress *self, guint percentage) { g_return_if_fail(FU_IS_PROGRESS(self)); g_return_if_fail(percentage <= 100); /* is it the same */ if (percentage == self->percentage) return; /* is it less */ if (self->percentage != G_MAXUINT && percentage < self->percentage) { if (self->profile) { g_autoptr(GString) str = g_string_new(NULL); fu_progress_build_parent_chain(self, str, 0); g_warning("percentage should not go down from %u to %u: %s", self->percentage, percentage, str->str); } return; } /* done */ if (percentage == 100) { fu_progress_set_duration(self, g_timer_elapsed(self->timer, NULL)); for (guint i = 0; i < self->children->len; i++) { FuProgress *child = g_ptr_array_index(self->children, i); g_signal_handlers_disconnect_by_data(child, self); } } /* save */ self->percentage = percentage; g_signal_emit(self, signals[SIGNAL_PERCENTAGE_CHANGED], 0, percentage); } /** * fu_progress_set_percentage_full: * @self: a #FuDevice * @progress_done: the bytes already done * @progress_total: the total number of bytes * * Sets the progress completion using the raw progress values. * * Since: 1.7.0 **/ void fu_progress_set_percentage_full(FuProgress *self, gsize progress_done, gsize progress_total) { gdouble percentage = 0.f; g_return_if_fail(FU_IS_PROGRESS(self)); g_return_if_fail(progress_done <= progress_total); if (progress_total > 0) percentage = (100.f * (gdouble)progress_done) / (gdouble)progress_total; fu_progress_set_percentage(self, (guint)percentage); } /** * fu_progress_set_profile: * @self: A #FuProgress * @profile: if profiling should be enabled * * This enables profiling of FuProgress. This may be useful in development, * but be warned; enabling profiling makes #FuProgress very slow. * * Since: 1.7.0 **/ void fu_progress_set_profile(FuProgress *self, gboolean profile) { g_return_if_fail(FU_IS_PROGRESS(self)); self->profile = profile; } /** * fu_progress_get_profile: * @self: A #FuProgress * * Returns if the profile is enabled for this progress. * * Return value: if profiling has been enabled * * Since: 1.8.2 **/ gboolean fu_progress_get_profile(FuProgress *self) { g_return_val_if_fail(FU_IS_PROGRESS(self), FALSE); return self->profile; } /** * fu_progress_reset: * @self: A #FuProgress * * Resets the #FuProgress object to unset * * Since: 1.7.0 **/ void fu_progress_reset(FuProgress *self) { g_return_if_fail(FU_IS_PROGRESS(self)); /* reset values */ self->step_now = 0; self->percentage = G_MAXUINT; /* only use the timer if profiling; it's expensive */ if (self->profile) { g_timer_start(self->timer); g_timer_start(self->timer_child); } /* no more step data */ g_ptr_array_set_size(self->children, 0); } /** * fu_progress_set_steps: * @self: A #FuProgress * @step_max: The number of sub-tasks in this progress, can be 0 * * Sets the number of sub-tasks, i.e. how many times the fu_progress_step_done() * function will be called in the loop. * * The progress ID must be set fu_progress_set_id() before this method is used. * * Since: 1.7.0 **/ void fu_progress_set_steps(FuProgress *self, guint step_max) { g_return_if_fail(FU_IS_PROGRESS(self)); g_return_if_fail(self->id != NULL); /* if there is an insane number of steps, scale these */ if (step_max > FU_PROGRESS_STEPS_MAX) { self->step_scaling = step_max / 100; step_max = 100; } /* create fake steps */ for (guint i = 0; i < step_max; i++) fu_progress_add_step(self, self->status, 0, NULL); /* adjust global fraction */ for (guint i = 0; i < self->children->len; i++) { FuProgress *child = g_ptr_array_index(self->children, i); child->global_fraction = self->global_fraction / step_max; if (child->global_fraction < 0.01f) g_signal_handlers_disconnect_by_data(child, self); } /* show that the sub-progress has been created */ fu_progress_set_percentage(self, 0); fu_progress_add_flag(self, FU_PROGRESS_FLAG_NO_PROFILE); /* reset child timer */ g_timer_start(self->timer_child); } /** * fu_progress_get_global_fraction: * @self: A #FuProgress * * Gets the global percentage. * * Return value: fraction, where 1.0 is 100%. * * Since: 2.0.4 **/ gdouble fu_progress_get_global_fraction(FuProgress *self) { g_return_val_if_fail(FU_IS_PROGRESS(self), -1.f); return self->global_fraction; } /** * fu_progress_get_steps: * @self: A #FuProgress * * Gets the number of sub-tasks, i.e. how many times the fu_progress_step_done() * function will be called in the loop. * * Return value: number of sub-tasks in this progress * * Since: 1.7.0 **/ guint fu_progress_get_steps(FuProgress *self) { g_return_val_if_fail(FU_IS_PROGRESS(self), G_MAXUINT); return self->children->len; } static gdouble fu_progress_discrete_to_percent(guint discrete, guint step_max) { /* check we are in range */ if (discrete > step_max) return 100; if (step_max == 0) { g_warning("step_max is 0!"); return 0; } return ((gdouble)discrete * (100.0f / (gdouble)(step_max))); } static gdouble fu_progress_get_step_percentage(FuProgress *self, guint idx) { guint current = 0; guint total = 0; gboolean any_step_weighting = FALSE; /* we set the step weighting manually */ for (guint i = 0; i < self->children->len; i++) { FuProgress *child = g_ptr_array_index(self->children, i); if (child->step_weighting > 0) { any_step_weighting = TRUE; break; } } /* just use proportional */ if (!any_step_weighting) return -1; /* work out percentage */ for (guint i = 0; i < self->children->len; i++) { FuProgress *child = g_ptr_array_index(self->children, i); if (i <= idx) current += child->step_weighting; total += child->step_weighting; } if (total == 0) return -1; return ((gdouble)current * 100.f) / (gdouble)total; } static void fu_progress_child_status_changed_cb(FuProgress *child, FwupdStatus status, FuProgress *self) { fu_progress_set_status(self, status); } static void fu_progress_child_percentage_changed_cb(FuProgress *child, guint percentage, FuProgress *self) { gdouble offset; gdouble range; gdouble extra; guint parent_percentage = G_MAXUINT; /* propagate up the stack if FuProgress has only one priv */ if (self->children->len == 1) { fu_progress_set_percentage(self, percentage); return; } /* did we call done on a step that did not have a size set? */ if (self->children->len == 0) return; /* already at >= 100% */ if (self->step_now >= self->children->len) { g_warning("already at %u/%u step_max", self->step_now, self->children->len); return; } /* if the child finished, set the status back to the last parent status */ if (percentage == 100) { FuProgress *child_tmp = g_ptr_array_index(self->children, self->step_now); if (fu_progress_get_status(child_tmp) != FWUPD_STATUS_UNKNOWN) fu_progress_set_status(self, fu_progress_get_status(child_tmp)); } /* we don't store zero */ if (self->step_now == 0) { gdouble pc = fu_progress_get_step_percentage(self, 0); if (pc > 0) parent_percentage = percentage * pc / 100; } else { gdouble pc1 = fu_progress_get_step_percentage(self, self->step_now - 1); gdouble pc2 = fu_progress_get_step_percentage(self, self->step_now); /* bi-linearly interpolate */ if (pc1 >= 0 && pc2 >= 0) parent_percentage = (((100 - percentage) * pc1) + (percentage * pc2)) / 100; } if (parent_percentage != G_MAXUINT) { fu_progress_set_percentage(self, parent_percentage); return; } /* get the range between the parent priv and the next parent priv */ offset = fu_progress_discrete_to_percent(self->step_now, self->children->len); range = fu_progress_discrete_to_percent(self->step_now + 1, self->children->len) - offset; if (range < 0.01) return; /* get the extra contributed by the child */ extra = ((gdouble)percentage / 100.0f) * range; /* emit from the parent */ parent_percentage = (guint)(offset + extra); fu_progress_set_percentage(self, parent_percentage); } /** * fu_progress_add_step: * @self: A #FuProgress * @status: status value to use for this phase * @value: A step weighting variable argument array * @name: (nullable): Human readable name to identify the step * * This sets the step weighting, which you will want to do if one action * will take a bigger chunk of time than another. * * The progress ID must be set fu_progress_set_id() before this method is used. * * Since: 1.8.2 **/ void fu_progress_add_step(FuProgress *self, FwupdStatus status, guint value, const gchar *name) { g_autoptr(FuProgress) child = fu_progress_new(NULL); g_return_if_fail(FU_IS_PROGRESS(self)); g_return_if_fail(self->id != NULL); g_return_if_fail(self->children->len < 100 * 1000); /* save data */ fu_progress_set_status(child, status); child->step_weighting = value; /* adjust global percentage */ if (value > 0) child->global_fraction = self->global_fraction * (gdouble)value / 100.f; /* connect signals as required */ if (fu_progress_get_global_fraction(self) > 0.001f) { g_signal_connect(FU_PROGRESS(child), "percentage-changed", G_CALLBACK(fu_progress_child_percentage_changed_cb), self); } g_signal_connect(FU_PROGRESS(child), "status-changed", G_CALLBACK(fu_progress_child_status_changed_cb), self); fu_progress_set_parent(child, self); if (name != NULL) fu_progress_set_name(child, name); /* use first child status */ if (self->children->len == 0) fu_progress_set_status(self, status); /* add child */ g_ptr_array_add(self->children, g_steal_pointer(&child)); /* reset child timer */ g_timer_start(self->timer_child); } /** * fu_progress_finished: * @self: A #FuProgress * * Called when the step_now sub-task wants to finish early and still complete. * * Since: 1.7.0 **/ void fu_progress_finished(FuProgress *self) { g_return_if_fail(FU_IS_PROGRESS(self)); /* is already at 100%? */ if (self->step_now == self->children->len) return; /* all done */ self->step_now = self->children->len; fu_progress_set_percentage(self, 100); /* we finished early, so invalidate children */ for (guint i = 0; i < self->children->len; i++) { FuProgress *child = g_ptr_array_index(self->children, i); fu_progress_add_flag(child, FU_PROGRESS_FLAG_NO_TRACEBACK); } } /** * fu_progress_get_child: * @self: A #FuProgress * * Monitor a child and proxy back up to the parent with the correct percentage. * * Return value: (transfer none): A new %FuProgress or %NULL for failure * * Since: 1.7.0 **/ FuProgress * fu_progress_get_child(FuProgress *self) { guint step_now; g_return_val_if_fail(FU_IS_PROGRESS(self), NULL); g_return_val_if_fail(self->id != NULL, NULL); step_now = self->step_now / self->step_scaling; g_return_val_if_fail(self->children->len > 0, NULL); g_return_val_if_fail(self->children->len > step_now, NULL); /* all preallocated, nothing to do */ return FU_PROGRESS(g_ptr_array_index(self->children, step_now)); } static void fu_progress_show_profile(FuProgress *self) { gdouble division; gdouble total_time = 0.0f; gboolean close_enough = TRUE; g_autoptr(GString) str = NULL; /* not accurate enough for a profile result */ if (self->flags & FU_PROGRESS_FLAG_NO_PROFILE) return; /* get the total time so we can work out the divisor */ str = g_string_new("raw timing data was { "); for (guint i = 0; i < self->children->len; i++) { FuProgress *child = g_ptr_array_index(self->children, i); g_string_append_printf(str, "%.3f, ", fu_progress_get_duration(child)); } if (self->children->len > 0) g_string_set_size(str, str->len - 2); g_string_append(str, " } -- "); /* get the total time so we can work out the divisor */ for (guint i = 0; i < self->children->len; i++) { FuProgress *child = g_ptr_array_index(self->children, i); total_time += fu_progress_get_duration(child); } if (total_time < 0.001) return; division = total_time / 100.0f; /* what we set */ g_string_append(str, "steps were set as [ "); for (guint i = 0; i < self->children->len; i++) { FuProgress *child = g_ptr_array_index(self->children, i); g_string_append_printf(str, "%u ", child->step_weighting); } /* what we _should_ have set */ g_string_append_printf(str, "] but should have been [ "); for (guint i = 0; i < self->children->len; i++) { FuProgress *child = g_ptr_array_index(self->children, i); g_string_append_printf(str, "%.0f ", fu_progress_get_duration(child) / division); /* this is sufficiently different to what we guessed */ if (fabs((fu_progress_get_duration(child) / division) - (gdouble)child->step_weighting) > 5) { close_enough = FALSE; } } g_string_append(str, "]"); if (self->flags & FU_PROGRESS_FLAG_GUESSED) { #ifdef SUPPORTED_BUILD g_debug("%s at %s [%s]", str->str, self->id, fu_progress_get_name_fallback(self)); #else g_warning("%s at %s [%s]", str->str, self->id, fu_progress_get_name_fallback(self)); g_warning("Please see " "https://github.com/fwupd/fwupd/wiki/Daemon-Warning:-FuProgress-steps"); #endif } else if (!close_enough) { g_debug("%s at %s", str->str, self->id); } } /** * fu_progress_step_done: * @self: A #FuProgress * * Called when the step_now sub-task has finished. * * Since: 1.7.0 **/ void fu_progress_step_done(FuProgress *self) { FuProgress *child = NULL; gdouble percentage; g_return_if_fail(FU_IS_PROGRESS(self)); g_return_if_fail(self->id != NULL); /* ignore steps */ if (self->step_scaling > 1) { if (self->step_now >= self->children->len || self->step_done++ % self->step_scaling != 0) return; } /* did we call done when no size set? */ if (self->children->len == 0) { g_autoptr(GString) str = g_string_new(NULL); fu_progress_build_parent_chain(self, str, 0); g_warning("progress done when no size set! [%s]: %s", self->id, str->str); return; } /* get the active child */ if (self->children->len > 0) child = g_ptr_array_index(self->children, self->step_now); /* save the duration in the array */ if (self->profile) { if (child != NULL) fu_progress_set_duration(child, g_timer_elapsed(self->timer_child, NULL)); g_timer_start(self->timer_child); } /* is already at 100%? */ if (self->step_now >= self->children->len) { g_autoptr(GString) str = g_string_new(NULL); fu_progress_build_parent_chain(self, str, 0); g_warning("already at 100%% [%s]: %s", self->id, str->str); return; } /* is child not at 100%? */ if (!fu_progress_has_flag(self, FU_PROGRESS_FLAG_CHILD_FINISHED) && child != NULL) { if (child->step_now != child->children->len) { g_autoptr(GString) str = g_string_new(NULL); fu_progress_build_parent_chain(child, str, 0); g_warning("child is at %u/%u step_max and parent done [%s]\n%s", child->step_now, child->children->len, self->id, str->str); /* do not abort, as we want to clean this up */ } } /* another */ self->step_now++; /* update status */ if (self->step_now < self->children->len) { FuProgress *child_tmp = g_ptr_array_index(self->children, self->step_now); if (fu_progress_get_status(child_tmp) != FWUPD_STATUS_UNKNOWN) fu_progress_set_status(self, fu_progress_get_status(child_tmp)); } else if (self->parent != NULL) { fu_progress_set_status(self, fu_progress_get_status(self->parent)); } else { fu_progress_set_status(self, FWUPD_STATUS_UNKNOWN); } /* not interesting anymore */ if (self->global_fraction < 0.01) return; /* find new percentage */ percentage = fu_progress_get_step_percentage(self, self->step_now - 1); if (percentage < 0) percentage = fu_progress_discrete_to_percent(self->step_now, self->children->len); fu_progress_set_percentage(self, (guint)percentage); /* show any profiling stats */ if (self->profile && self->step_now == self->children->len) fu_progress_show_profile(self); } /** * fu_progress_sleep: * @self: a #FuProgress * @delay_ms: the delay in milliseconds * * Sleeps, setting the device progress from 0..100% as time continues. * * NOTE: You should try to avoid calling this function for emulated devices. * * Since: 1.7.0 **/ void fu_progress_sleep(FuProgress *self, guint delay_ms) { gulong delay_us_pc = (delay_ms * 1000) / 100; g_return_if_fail(FU_IS_PROGRESS(self)); g_return_if_fail(delay_ms > 0); fu_progress_set_percentage(self, 0); for (guint i = 0; i < 100; i++) { g_usleep(delay_us_pc); fu_progress_set_percentage(self, i + 1); } } static void fu_progress_traceback_cb(FuProgress *self, guint idt, guint child_idx, guint threshold_ms, GString *str) { if (self->flags & FU_PROGRESS_FLAG_NO_TRACEBACK) return; if (self->children->len == 0 && fu_progress_get_duration(self) < 0.0001) return; if (threshold_ms == 0 || fu_progress_get_duration(self) * 1000 > threshold_ms) { for (guint i = 0; i < idt; i++) g_string_append(str, " "); if (self->id != NULL) g_string_append(str, self->id); if (self->name != NULL) g_string_append_printf(str, ":%s", self->name); if (self->id == NULL && self->name == NULL && child_idx != G_MAXUINT) g_string_append_printf(str, "@%u", child_idx); g_string_append_printf(str, " [%.2fms]", fu_progress_get_duration(self) * 1000.f); g_string_append(str, self->children->len > 0 ? ":\n" : "\n"); } for (guint i = 0; i < self->children->len; i++) { FuProgress *child = g_ptr_array_index(self->children, i); fu_progress_traceback_cb(child, idt + 4, i, threshold_ms, str); } } /** * fu_progress_traceback: * @self: A #FuProgress * * Create a traceback used for profiling startup. * * Return value: (transfer full): string * * Since: 1.8.2 **/ gchar * fu_progress_traceback(FuProgress *self) { const gchar *tmp = g_getenv("FWUPD_PROFILE"); guint64 threshold_ms = 5000; g_autoptr(GString) str = g_string_new(NULL); /* allow override */ if (tmp != NULL) { g_autoptr(GError) error_local = NULL; if (!fu_strtoull(tmp, &threshold_ms, 0, G_MAXUINT, FU_INTEGER_BASE_AUTO, &error_local)) g_warning("invalid threshold value: %s", tmp); } fu_progress_traceback_cb(self, 0, G_MAXUINT, threshold_ms, str); if (str->len == 0) return NULL; return g_string_free(g_steal_pointer(&str), FALSE); } static void fu_progress_add_string(FwupdCodec *codec, guint idt, GString *str) { FuProgress *self = FU_PROGRESS(codec); /* not interesting */ if (self->id == NULL && self->name == NULL) return; fwupd_codec_string_append(str, idt, "Id", self->id); fwupd_codec_string_append(str, idt, "Name", self->name); if (self->percentage != G_MAXUINT) fwupd_codec_string_append_int(str, idt, "Percentage", self->percentage); if (self->status != FWUPD_STATUS_UNKNOWN) fwupd_codec_string_append(str, idt, "Status", fwupd_status_to_string(self->status)); if (self->duration > 0.0001) fwupd_codec_string_append_int(str, idt, "DurationMs", self->duration * 1000.f); fwupd_codec_string_append_int(str, idt, "StepWeighting", self->step_weighting); fwupd_codec_string_append_int(str, idt, "StepNow", self->step_now); for (guint i = 0; i < self->children->len; i++) { FuProgress *child = g_ptr_array_index(self->children, i); fwupd_codec_add_string(FWUPD_CODEC(child), idt + 1, str); } } static void fu_progress_codec_iface_init(FwupdCodecInterface *iface) { iface->add_string = fu_progress_add_string; } static void fu_progress_init(FuProgress *self) { self->status = FWUPD_STATUS_UNKNOWN; self->step_scaling = 1; self->percentage = G_MAXUINT; self->timer = g_timer_new(); self->timer_child = g_timer_new(); self->children = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); self->duration = 0.f; self->global_fraction = 1.f; } static void fu_progress_finalize(GObject *object) { FuProgress *self = FU_PROGRESS(object); /* show any profiling stats */ if (self->profile) fu_progress_show_profile(self); fu_progress_reset(self); g_free(self->id); g_free(self->name); g_ptr_array_unref(self->children); g_timer_destroy(self->timer); g_timer_destroy(self->timer_child); G_OBJECT_CLASS(fu_progress_parent_class)->finalize(object); } static void fu_progress_class_init(FuProgressClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_progress_finalize; /** * FuProgress::percentage-changed: * @self: the #FuProgress instance that emitted the signal * @percentage: the new value * * The ::percentage-changed signal is emitted when the tasks completion has changed. * * Since: 1.7.0 **/ signals[SIGNAL_PERCENTAGE_CHANGED] = g_signal_new("percentage-changed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__UINT, G_TYPE_NONE, 1, G_TYPE_UINT); /** * FuProgress::status-changed: * @self: the #FuProgress instance that emitted the signal * @status: the new #FwupdStatus * * The ::status-changed signal is emitted when the task status has changed. * * Since: 1.7.0 **/ signals[SIGNAL_STATUS_CHANGED] = g_signal_new("status-changed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__UINT, G_TYPE_NONE, 1, G_TYPE_UINT); } /** * fu_progress_new: * @id: (nullable): progress ID, normally `G_STRLOC` * * Return value: A new #FuProgress instance. * * Since: 1.7.0 **/ FuProgress * fu_progress_new(const gchar *id) { FuProgress *self; self = g_object_new(FU_TYPE_PROGRESS, NULL); if (id != NULL) fu_progress_set_id(self, id); return FU_PROGRESS(self); } fwupd-2.0.10/libfwupdplugin/fu-progress.h000066400000000000000000000044111501337203100203720ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include #include "fu-progress-struct.h" #define FU_TYPE_PROGRESS (fu_progress_get_type()) G_DECLARE_FINAL_TYPE(FuProgress, fu_progress, FU, PROGRESS, GObject) FuProgress * fu_progress_new(const gchar *id); const gchar * fu_progress_get_id(FuProgress *self) G_GNUC_NON_NULL(1); void fu_progress_set_id(FuProgress *self, const gchar *id) G_GNUC_NON_NULL(1, 2); const gchar * fu_progress_get_name(FuProgress *self) G_GNUC_NON_NULL(1); void fu_progress_set_name(FuProgress *self, const gchar *name) G_GNUC_NON_NULL(1, 2); void fu_progress_add_flag(FuProgress *self, FuProgressFlag flag) G_GNUC_NON_NULL(1); void fu_progress_remove_flag(FuProgress *self, FuProgressFlag flag) G_GNUC_NON_NULL(1); gboolean fu_progress_has_flag(FuProgress *self, FuProgressFlag flag) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); FwupdStatus fu_progress_get_status(FuProgress *self) G_GNUC_NON_NULL(1); void fu_progress_set_status(FuProgress *self, FwupdStatus status) G_GNUC_NON_NULL(1); void fu_progress_set_percentage(FuProgress *self, guint percentage) G_GNUC_NON_NULL(1); void fu_progress_set_percentage_full(FuProgress *self, gsize progress_done, gsize progress_total) G_GNUC_NON_NULL(1); guint fu_progress_get_percentage(FuProgress *self) G_GNUC_NON_NULL(1); gdouble fu_progress_get_duration(FuProgress *self) G_GNUC_NON_NULL(1); void fu_progress_set_profile(FuProgress *self, gboolean profile) G_GNUC_NON_NULL(1); gboolean fu_progress_get_profile(FuProgress *self) G_GNUC_NON_NULL(1); void fu_progress_reset(FuProgress *self) G_GNUC_NON_NULL(1); void fu_progress_set_steps(FuProgress *self, guint step_max) G_GNUC_NON_NULL(1); guint fu_progress_get_steps(FuProgress *self) G_GNUC_NON_NULL(1); void fu_progress_add_step(FuProgress *self, FwupdStatus status, guint value, const gchar *name) G_GNUC_NON_NULL(1); void fu_progress_finished(FuProgress *self) G_GNUC_NON_NULL(1); void fu_progress_step_done(FuProgress *self) G_GNUC_NON_NULL(1); FuProgress * fu_progress_get_child(FuProgress *self) G_GNUC_NON_NULL(1); void fu_progress_sleep(FuProgress *self, guint delay_ms) G_GNUC_NON_NULL(1); gchar * fu_progress_traceback(FuProgress *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-progress.rs000066400000000000000000000007171501337203100205740ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later enum FuProgressFlag { None = 0, // Since: 1.7.0 Guessed = 1 << 0, // Since: 1.7.0 NoProfile = 1 << 1, // Since: 1.7.0 ChildFinished = 1 << 2, // Since: 1.8.2 NoTraceback = 1 << 3, // Since: 1.8.2 NoSender = 1 << 4, // Since: 1.9.10 Unknown = u64::MAX, // Since: 1.7.0 } fwupd-2.0.10/libfwupdplugin/fu-quirks.c000066400000000000000000001027141501337203100200440ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuQuirks" #include "config.h" #include #include #ifdef HAVE_SQLITE #include #endif #include "fwupd-common.h" #include "fwupd-enums-private.h" #include "fwupd-error.h" #include "fwupd-remote-private.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-path.h" #include "fu-quirks.h" #include "fu-string.h" /** * FuQuirks: * * Quirks can be used to modify device behavior. * When fwupd is installed in long-term support distros it's very hard to * backport new versions as new hardware is released. * * There are several reasons why we can't just include the mapping and quirk * information in the AppStream metadata: * * * The extra data is hugely specific to the installed fwupd plugin versions * * The device-id is per-device, and the mapping is usually per-plugin * * Often the information is needed before the FuDevice is created * * There are security implications in allowing plugins to handle new devices * * The idea with quirks is that the end user can drop an additional (or replace * an existing) file in a .d director with a simple format and the hardware will * magically start working. This assumes no new quirks are required, as this would * obviously need code changes, but allows us to get most existing devices working * in an easy way without the user compiling anything. * * Plugins may add support for additional quirks that are relevant only for those plugins, * and should be documented in the per-plugin `README.md` files. * * You can add quirk files in `/usr/share/fwupd/quirks.d` or `/var/lib/fwupd/quirks.d/`. * * Here is an example as seen in the CSR plugin: * * |[ * [USB\VID_0A12&PID_1337] * Plugin = dfu_csr * Name = H05 * Summary = Bluetooth Headphones * Icon = audio-headphones * Vendor = AIAIAI * [USB\VID_0A12&PID_1337&REV_2520] * Version = 1.2 * ]| * * See also: [class@FuDevice], [class@FuPlugin] */ static void fu_quirks_finalize(GObject *obj); struct _FuQuirks { GObject parent_instance; FuContext *ctx; FuQuirksLoadFlags load_flags; GHashTable *possible_keys; GPtrArray *invalid_keys; XbSilo *silo; XbQuery *query_kv; XbQuery *query_vs; gboolean verbose; #ifdef HAVE_SQLITE sqlite3 *db; #endif }; G_DEFINE_TYPE(FuQuirks, fu_quirks, G_TYPE_OBJECT) #ifdef HAVE_SQLITE G_DEFINE_AUTOPTR_CLEANUP_FUNC(sqlite3_stmt, sqlite3_finalize); #endif static gchar * fu_quirks_build_group_key(const gchar *group) { if (fwupd_guid_is_valid(group)) return g_strdup(group); return fwupd_guid_hash_string(group); } static gboolean fu_quirks_validate_flags(const gchar *value, GError **error) { g_return_val_if_fail(value != NULL, FALSE); for (gsize i = 0; value[i] != '\0'; i++) { gchar tmp = value[i]; /* allowed special chars */ if (tmp == ',' || tmp == '~' || tmp == '-') continue; if (!g_ascii_isalnum(tmp)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "%c is not alphanumeric", tmp); return FALSE; } if (g_ascii_isalpha(tmp) && !g_ascii_islower(tmp)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "%c is not lowercase", tmp); return FALSE; } } /* success */ return TRUE; } typedef struct { GString *group; XbBuilderNode *bn; XbBuilderNode *root; } FuQuirksConvertHelper; static void fu_quirks_convert_helper_free(FuQuirksConvertHelper *helper) { g_string_free(helper->group, TRUE); g_object_unref(helper->root); if (helper->bn != NULL) g_object_unref(helper->bn); g_free(helper); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuQuirksConvertHelper, fu_quirks_convert_helper_free) static gboolean fu_quirks_convert_keyfile_to_xml_cb(GString *token, guint token_idx, gpointer user_data, GError **error) { FuQuirksConvertHelper *helper = (FuQuirksConvertHelper *)user_data; g_autofree gchar *key = NULL; g_autofree gchar *value = NULL; g_auto(GStrv) kv = NULL; /* blank line */ if (token->len == 0) return TRUE; /* comment */ if (token->str[0] == '#') return TRUE; /* neither a key=value or [group] */ if (token->len < 3) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid line: %s", token->str); return FALSE; } /* a group */ if (token->str[0] == '[' && token->str[token->len - 1] == ']') { g_autofree gchar *group_id = NULL; g_autofree gchar *group_tmp = NULL; g_autoptr(XbBuilderNode) bn_tmp = NULL; /* trim off the [] and convert to a GUID */ group_tmp = g_strndup(token->str + 1, token->len - 2); group_id = fu_quirks_build_group_key(group_tmp); bn_tmp = xb_builder_node_insert(helper->root, "device", "id", group_id, NULL); g_set_object(&helper->bn, bn_tmp); g_string_assign(helper->group, group_tmp); return TRUE; } /* no current group */ if (helper->bn == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid line when group unset: %s", token->str); return FALSE; } /* parse as key=value */ kv = g_strsplit(token->str, "=", 2); if (kv[1] == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid line: not key=value: %s", token->str); return FALSE; } /* sanity check flags */ key = fu_strstrip(kv[0]); value = fu_strstrip(kv[1]); if (g_strcmp0(key, FU_QUIRKS_FLAGS) == 0) { g_autoptr(GError) error_local = NULL; if (!fu_quirks_validate_flags(value, &error_local)) { g_warning("[%s] %s = %s is invalid: %s", helper->group->str, key, value, error_local->message); } } /* add */ xb_builder_node_insert_text(helper->bn, "value", value, "key", key, NULL); return TRUE; } static GBytes * fu_quirks_convert_keyfile_to_xml(FuQuirks *self, GBytes *bytes, GError **error) { gsize xmlsz; g_autofree gchar *xml = NULL; g_autoptr(FuQuirksConvertHelper) helper = g_new0(FuQuirksConvertHelper, 1); /* split into lines */ helper->root = xb_builder_node_new("quirk"); helper->group = g_string_new(NULL); if (!fu_strsplit_full((const gchar *)g_bytes_get_data(bytes, NULL), g_bytes_get_size(bytes), "\n", fu_quirks_convert_keyfile_to_xml_cb, helper, error)) return NULL; /* export as XML blob */ xml = xb_builder_node_export(helper->root, XB_NODE_EXPORT_FLAG_ADD_HEADER, error); if (xml == NULL) return NULL; xmlsz = strlen(xml); return g_bytes_new_take(g_steal_pointer(&xml), xmlsz); } static GInputStream * fu_quirks_convert_quirk_to_xml_cb(XbBuilderSource *source, XbBuilderSourceCtx *ctx, gpointer user_data, GCancellable *cancellable, GError **error) { FuQuirks *self = FU_QUIRKS(user_data); g_autoptr(GBytes) bytes = NULL; g_autoptr(GBytes) bytes_xml = NULL; bytes = xb_builder_source_ctx_get_bytes(ctx, cancellable, error); if (bytes == NULL) return NULL; bytes_xml = fu_quirks_convert_keyfile_to_xml(self, bytes, error); if (bytes_xml == NULL) return NULL; return g_memory_input_stream_new_from_bytes(bytes_xml); } static gint fu_quirks_filename_sort_cb(gconstpointer a, gconstpointer b) { const gchar *stra = *((const gchar **)a); const gchar *strb = *((const gchar **)b); return g_strcmp0(stra, strb); } static gboolean fu_quirks_add_quirks_for_path(FuQuirks *self, XbBuilder *builder, const gchar *path, GError **error) { const gchar *tmp; g_autoptr(GDir) dir = NULL; g_autoptr(GPtrArray) filenames = g_ptr_array_new_with_free_func(g_free); g_info("loading quirks from %s", path); /* add valid files to the array */ if (!g_file_test(path, G_FILE_TEST_EXISTS)) return TRUE; dir = g_dir_open(path, 0, error); if (dir == NULL) return FALSE; while ((tmp = g_dir_read_name(dir)) != NULL) { if (!g_str_has_suffix(tmp, ".quirk") && !g_str_has_suffix(tmp, ".quirk.gz")) { g_debug("skipping invalid file %s", tmp); continue; } g_ptr_array_add(filenames, g_build_filename(path, tmp, NULL)); } /* sort */ g_ptr_array_sort(filenames, fu_quirks_filename_sort_cb); /* process files */ for (guint i = 0; i < filenames->len; i++) { const gchar *filename = g_ptr_array_index(filenames, i); g_autoptr(GFile) file = g_file_new_for_path(filename); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); /* load from keyfile */ xb_builder_source_add_simple_adapter(source, "text/plain,application/octet-stream,.quirk", fu_quirks_convert_quirk_to_xml_cb, self, NULL); if (!xb_builder_source_load_file(source, file, XB_BUILDER_SOURCE_FLAG_WATCH_FILE | XB_BUILDER_SOURCE_FLAG_LITERAL_TEXT, NULL, error)) { g_prefix_error(error, "failed to load %s: ", filename); fwupd_error_convert(error); return FALSE; } /* watch the file for changes */ xb_builder_import_source(builder, source); } /* success */ return TRUE; } static gint fu_quirks_strcasecmp_cb(gconstpointer a, gconstpointer b) { const gchar *entry1 = *((const gchar **)a); const gchar *entry2 = *((const gchar **)b); return g_ascii_strcasecmp(entry1, entry2); } static gboolean fu_quirks_check_silo(FuQuirks *self, GError **error) { XbBuilderCompileFlags compile_flags = XB_BUILDER_COMPILE_FLAG_WATCH_BLOB; g_autofree gchar *datadir = NULL; g_autofree gchar *localstatedir = NULL; g_autoptr(GFile) file = NULL; g_autoptr(XbBuilder) builder = NULL; g_autoptr(XbNode) n_any = NULL; /* everything is okay */ if (self->silo != NULL && xb_silo_is_valid(self->silo)) return TRUE; /* system datadir */ builder = xb_builder_new(); datadir = fu_path_from_kind(FU_PATH_KIND_DATADIR_QUIRKS); if (!fu_quirks_add_quirks_for_path(self, builder, datadir, error)) return FALSE; /* something we can write when using Ostree */ localstatedir = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR_QUIRKS); if (!fu_quirks_add_quirks_for_path(self, builder, localstatedir, error)) return FALSE; /* load silo */ if (self->load_flags & FU_QUIRKS_LOAD_FLAG_NO_CACHE) { g_autoptr(GFileIOStream) iostr = NULL; file = g_file_new_tmp(NULL, &iostr, error); if (file == NULL) return FALSE; } else { g_autofree gchar *cachedirpkg = fu_path_from_kind(FU_PATH_KIND_CACHEDIR_PKG); g_autofree gchar *xmlbfn = g_build_filename(cachedirpkg, "quirks.xmlb", NULL); file = g_file_new_for_path(xmlbfn); } if (g_getenv("FWUPD_XMLB_VERBOSE") != NULL) { xb_builder_set_profile_flags(builder, XB_SILO_PROFILE_FLAG_XPATH | XB_SILO_PROFILE_FLAG_DEBUG); } if (self->load_flags & FU_QUIRKS_LOAD_FLAG_READONLY_FS) compile_flags |= XB_BUILDER_COMPILE_FLAG_IGNORE_GUID; self->silo = xb_builder_ensure(builder, file, compile_flags, NULL, error); if (self->silo == NULL) return FALSE; /* dump warnings to console, just once */ if (self->invalid_keys->len > 0) { g_autofree gchar *str = NULL; g_ptr_array_sort(self->invalid_keys, fu_quirks_strcasecmp_cb); str = fu_strjoin(",", self->invalid_keys); g_info("invalid key names: %s", str); } /* check if there is any quirk data to load, as older libxmlb versions will not be able to * create the prepared query with an unknown text ID */ n_any = xb_silo_query_first(self->silo, "quirk", NULL); if (n_any == NULL) { g_debug("no quirk data, not creating prepared queries"); return TRUE; } /* create prepared queries to save time later */ self->query_kv = xb_query_new_full(self->silo, "quirk/device[@id=?]/value[@key=?]", XB_QUERY_FLAG_OPTIMIZE, error); if (self->query_kv == NULL) { g_prefix_error(error, "failed to prepare query: "); return FALSE; } self->query_vs = xb_query_new_full(self->silo, "quirk/device[@id=?]/value", XB_QUERY_FLAG_OPTIMIZE, error); if (self->query_vs == NULL) { g_prefix_error(error, "failed to prepare query: "); return FALSE; } if (!xb_silo_query_build_index(self->silo, "quirk/device", "id", error)) { fwupd_error_convert(error); return FALSE; } if (!xb_silo_query_build_index(self->silo, "quirk/device/value", "key", error)) { fwupd_error_convert(error); return FALSE; } /* success */ return TRUE; } /** * fu_quirks_lookup_by_id: * @self: a #FuQuirks * @guid: GUID to lookup * @key: an ID to match the entry, e.g. `Name` * * Looks up an entry in the hardware database using a string value. * * Returns: (transfer none): values from the database, or %NULL if not found * * Since: 1.0.1 **/ const gchar * fu_quirks_lookup_by_id(FuQuirks *self, const gchar *guid, const gchar *key) { g_autoptr(GError) error = NULL; g_autoptr(XbNode) n = NULL; g_auto(XbQueryContext) context = XB_QUERY_CONTEXT_INIT(); g_return_val_if_fail(FU_IS_QUIRKS(self), NULL); g_return_val_if_fail(guid != NULL, NULL); g_return_val_if_fail(key != NULL, NULL); #ifdef HAVE_SQLITE /* this is generated from usb.ids and other static sources */ if (self->db != NULL && (self->load_flags & FU_QUIRKS_LOAD_FLAG_NO_CACHE) == 0) { g_autoptr(sqlite3_stmt) stmt = NULL; if (sqlite3_prepare_v2(self->db, "SELECT key, value FROM quirks WHERE guid = ?1 " "AND key = ?2 LIMIT 1", -1, &stmt, NULL) != SQLITE_OK) { g_warning("failed to prepare SQL: %s", sqlite3_errmsg(self->db)); return NULL; } sqlite3_bind_text(stmt, 1, guid, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, key, -1, SQLITE_STATIC); if (sqlite3_step(stmt) == SQLITE_ROW) { const gchar *value = (const gchar *)sqlite3_column_text(stmt, 1); if (value != NULL) return g_intern_string(value); } } #endif /* ensure up to date */ if (!fu_quirks_check_silo(self, &error)) { g_warning("failed to build silo: %s", error->message); return NULL; } /* no quirk data */ if (self->query_kv == NULL) return NULL; /* query */ xb_query_context_set_flags(&context, XB_QUERY_FLAG_USE_INDEXES); xb_value_bindings_bind_str(xb_query_context_get_bindings(&context), 0, guid, NULL); xb_value_bindings_bind_str(xb_query_context_get_bindings(&context), 1, key, NULL); n = xb_silo_query_first_with_context(self->silo, self->query_kv, &context, &error); if (n == NULL) { if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) return NULL; if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) return NULL; g_warning("failed to query: %s", error->message); return NULL; } if (self->verbose) g_debug("%s:%s → %s", guid, key, xb_node_get_text(n)); return xb_node_get_text(n); } /** * fu_quirks_lookup_by_id_iter: * @self: a #FuQuirks * @guid: GUID to lookup * @key: (nullable): an ID to match the entry, e.g. `Name`, or %NULL for all keys * @iter_cb: (scope call) (closure user_data): a function to call for each result * @user_data: user data passed to @iter_cb * * Looks up all entries in the hardware database using a GUID value. * * Returns: %TRUE if the ID was found, and @iter was called * * Since: 1.3.3 **/ gboolean fu_quirks_lookup_by_id_iter(FuQuirks *self, const gchar *guid, const gchar *key, FuQuirksIter iter_cb, gpointer user_data) { g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) results = NULL; g_auto(XbQueryContext) context = XB_QUERY_CONTEXT_INIT(); g_return_val_if_fail(FU_IS_QUIRKS(self), FALSE); g_return_val_if_fail(guid != NULL, FALSE); g_return_val_if_fail(iter_cb != NULL, FALSE); #ifdef HAVE_SQLITE /* this is generated from usb.ids and other static sources */ if (self->db != NULL && (self->load_flags & FU_QUIRKS_LOAD_FLAG_NO_CACHE) == 0) { g_autoptr(sqlite3_stmt) stmt = NULL; if (key == NULL) { if (sqlite3_prepare_v2(self->db, "SELECT key, value FROM quirks WHERE guid = ?1", -1, &stmt, NULL) != SQLITE_OK) { g_warning("failed to prepare SQL: %s", sqlite3_errmsg(self->db)); return FALSE; } sqlite3_bind_text(stmt, 1, guid, -1, SQLITE_STATIC); } else { if (sqlite3_prepare_v2(self->db, "SELECT key, value FROM quirks WHERE guid = ?1 " "AND key = ?2", -1, &stmt, NULL) != SQLITE_OK) { g_warning("failed to prepare SQL: %s", sqlite3_errmsg(self->db)); return FALSE; } sqlite3_bind_text(stmt, 1, guid, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, key, -1, SQLITE_STATIC); } while (sqlite3_step(stmt) == SQLITE_ROW) { const gchar *key_tmp = (const gchar *)sqlite3_column_text(stmt, 0); const gchar *value = (const gchar *)sqlite3_column_text(stmt, 1); iter_cb(self, key_tmp, value, FU_CONTEXT_QUIRK_SOURCE_DB, user_data); } } #endif /* ensure up to date */ if (!fu_quirks_check_silo(self, &error)) { g_warning("failed to build silo: %s", error->message); return FALSE; } /* no quirk data */ if (self->query_vs == NULL) { g_debug("no quirk data"); return FALSE; } /* query */ xb_query_context_set_flags(&context, XB_QUERY_FLAG_USE_INDEXES); xb_value_bindings_bind_str(xb_query_context_get_bindings(&context), 0, guid, NULL); if (key != NULL) { xb_value_bindings_bind_str(xb_query_context_get_bindings(&context), 1, key, NULL); results = xb_silo_query_with_context(self->silo, self->query_kv, &context, &error); } else { results = xb_silo_query_with_context(self->silo, self->query_vs, &context, &error); } if (results == NULL) { if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) return FALSE; if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) return FALSE; g_warning("failed to query: %s", error->message); return FALSE; } for (guint i = 0; i < results->len; i++) { XbNode *n = g_ptr_array_index(results, i); if (self->verbose) g_debug("%s → %s", guid, xb_node_get_text(n)); iter_cb(self, xb_node_get_attr(n, "key"), xb_node_get_text(n), FU_CONTEXT_QUIRK_SOURCE_FILE, user_data); } return TRUE; } #ifdef HAVE_SQLITE typedef struct { FuQuirks *self; sqlite3_stmt *stmt; const gchar *subsystem; const gchar *title_vid; const gchar *title_pid; GString *vid; } FuQuirksDbHelper; static void fu_quirks_db_helper_free(FuQuirksDbHelper *helper) { if (helper->vid != NULL) g_string_free(helper->vid, TRUE); g_free(helper); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuQuirksDbHelper, fu_quirks_db_helper_free) static gboolean fu_quirks_db_add_vendor_entry(FuQuirksDbHelper *helper, const gchar *vid, const gchar *name, GError **error) { FuQuirks *self = FU_QUIRKS(helper->self); g_autofree gchar *guid = NULL; g_autofree gchar *instance_id = NULL; g_autofree gchar *vid_strup = g_ascii_strup(vid, -1); instance_id = g_strdup_printf("%s\\%s_%s", helper->subsystem, helper->title_vid, vid_strup); guid = fwupd_guid_hash_string(instance_id); sqlite3_reset(helper->stmt); sqlite3_bind_text(helper->stmt, 1, guid, -1, SQLITE_STATIC); sqlite3_bind_text(helper->stmt, 2, FWUPD_RESULT_KEY_VENDOR, -1, SQLITE_STATIC); sqlite3_bind_text(helper->stmt, 3, name, -1, SQLITE_STATIC); if (sqlite3_step(helper->stmt) != SQLITE_DONE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to execute prepared statement: %s", sqlite3_errmsg(self->db)); return FALSE; } /* success */ return TRUE; } static gboolean fu_quirks_db_add_name_entry(FuQuirksDbHelper *helper, const gchar *vid, const gchar *pid, const gchar *name, GError **error) { FuQuirks *self = FU_QUIRKS(helper->self); g_autofree gchar *guid = NULL; g_autofree gchar *instance_id = NULL; g_autofree gchar *vid_strup = g_ascii_strup(vid, -1); g_autofree gchar *pid_strup = g_ascii_strup(pid, -1); instance_id = g_strdup_printf("%s\\%s_%s&%s_%s", helper->subsystem, helper->title_vid, vid_strup, helper->title_pid, pid_strup); guid = fwupd_guid_hash_string(instance_id); sqlite3_reset(helper->stmt); sqlite3_bind_text(helper->stmt, 1, guid, -1, SQLITE_STATIC); sqlite3_bind_text(helper->stmt, 2, FWUPD_RESULT_KEY_NAME, -1, SQLITE_STATIC); sqlite3_bind_text(helper->stmt, 3, name, -1, SQLITE_STATIC); if (sqlite3_step(helper->stmt) != SQLITE_DONE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to execute prepared statement: %s", sqlite3_errmsg(self->db)); return FALSE; } /* success */ return TRUE; } static gboolean _g_ascii_isxstrn(const gchar *str, gsize n) { for (gsize i = 0; i < n; i++) { if (!g_ascii_isxdigit(str[i])) return FALSE; } return TRUE; } static gboolean fu_quirks_db_add_usbids_cb(GString *token, guint token_idx, gpointer user_data, GError **error) { FuQuirksDbHelper *helper = (FuQuirksDbHelper *)user_data; /* not vendor lines */ if (token->len < 7) return TRUE; /* ignore the wrong ones */ if (g_strstr_len(token->str, -1, "Wrong ID") != NULL || g_strstr_len(token->str, -1, "wrong ID") != NULL) return TRUE; /* 4 hex digits */ if (_g_ascii_isxstrn(token->str, 4)) { g_string_set_size(helper->vid, 0); g_string_append_len(helper->vid, token->str, 4); return fu_quirks_db_add_vendor_entry(helper, helper->vid->str, token->str + 6, error); } /* tab, then 4 hex digits */ if (helper->vid->len > 0 && token->str[0] == '\t' && _g_ascii_isxstrn(token->str + 1, 4)) { g_autofree gchar *pid = g_strndup(token->str + 1, 4); return fu_quirks_db_add_name_entry(helper, helper->vid->str, pid, token->str + 7, error); } /* build into XML */ return TRUE; } static gboolean fu_quirks_db_add_ouitxt_cb(GString *token, guint token_idx, gpointer user_data, GError **error) { FuQuirksDbHelper *helper = (FuQuirksDbHelper *)user_data; g_autofree gchar *vid = NULL; /* not vendor lines */ if (token->len < 22) return TRUE; /* not 6 hex digits */ if (!_g_ascii_isxstrn(token->str, 6)) return TRUE; /* build into XML */ vid = g_strndup(token->str, 6); return fu_quirks_db_add_vendor_entry(helper, vid, token->str + 22, error); } static gboolean fu_quirks_db_add_pnpids_cb(GString *token, guint token_idx, gpointer user_data, GError **error) { FuQuirksDbHelper *helper = (FuQuirksDbHelper *)user_data; g_autofree gchar *vid = NULL; /* not vendor lines */ if (token->len < 5) return TRUE; /* ignore the wrong ones */ if (g_strstr_len(token->str, -1, "DO NOT USE") != NULL) return TRUE; /* build into XML */ vid = g_strndup(token->str, 3); return fu_quirks_db_add_vendor_entry(helper, vid, token->str + 4, error); } typedef struct { const gchar *fn; const gchar *subsystem; const gchar *title_vid; const gchar *title_pid; FuStrsplitFunc func; } FuQuirksDbItem; static gboolean fu_quirks_db_sqlite3_exec(FuQuirks *self, const gchar *sql, GError **error) { gint rc; /* if we're running all the tests in parallel it is possible to hit this... */ for (guint i = 0; i < 10; i++) { rc = sqlite3_exec(self->db, sql, NULL, NULL, NULL); if (rc != SQLITE_LOCKED) break; g_usleep(50 * 1000); } if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to run %s: %s", sql, sqlite3_errmsg(self->db)); return FALSE; } /* success */ return TRUE; } static gboolean fu_quirks_db_load(FuQuirks *self, FuQuirksLoadFlags load_flags, GError **error) { g_autofree gchar *vendor_ids_dir = fu_path_from_kind(FU_PATH_KIND_DATADIR_VENDOR_IDS); g_autoptr(sqlite3_stmt) stmt_insert = NULL; g_autoptr(sqlite3_stmt) stmt_query = NULL; g_autoptr(GString) fn_mtimes = g_string_new("quirks"); g_autofree gchar *guid_fwupd = fwupd_guid_hash_string("fwupd"); const FuQuirksDbItem map[] = { {"pci.ids", "PCI", "VEN", "DEV", fu_quirks_db_add_usbids_cb}, {"usb.ids", "USB", "VID", "PID", fu_quirks_db_add_usbids_cb}, {"pnp.ids", "PNP", "VID", "PID", fu_quirks_db_add_pnpids_cb}, {"oui.txt", "OUI", "VID", "PID", fu_quirks_db_add_ouitxt_cb}, }; /* nothing to do */ if (load_flags & FU_QUIRKS_LOAD_FLAG_NO_CACHE) return TRUE; /* create tables and indexes */ if (!fu_quirks_db_sqlite3_exec( self, "BEGIN TRANSACTION;" "CREATE TABLE IF NOT EXISTS quirks(guid, key, value);" "CREATE INDEX IF NOT EXISTS idx_quirks_guid ON quirks(guid);" "CREATE INDEX IF NOT EXISTS idx_quirks_guid_key ON quirks(guid, key);" "COMMIT;", error)) { return FALSE; } /* find out the mtimes of each of the files we want to load into the db */ for (guint i = 0; i < G_N_ELEMENTS(map); i++) { const FuQuirksDbItem *item = &map[i]; guint64 mtime; g_autofree gchar *fn = g_build_filename(vendor_ids_dir, item->fn, NULL); g_autoptr(GFile) file = g_file_new_for_path(fn); g_autoptr(GFileInfo) info = NULL; if (!g_file_query_exists(file, NULL)) continue; info = g_file_query_info(file, G_FILE_ATTRIBUTE_TIME_MODIFIED, G_FILE_QUERY_INFO_NONE, NULL, error); if (info == NULL) return FALSE; mtime = g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_TIME_MODIFIED); g_string_append_printf(fn_mtimes, ",%s:%" G_GUINT64_FORMAT, item->fn, mtime); } /* check if the mtimes match */ if (sqlite3_prepare_v2(self->db, "SELECT value FROM quirks WHERE guid = ?1 and key = ?2", -1, &stmt_query, NULL) != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to prepare SQL: %s", sqlite3_errmsg(self->db)); return FALSE; } sqlite3_bind_text(stmt_query, 1, guid_fwupd, -1, SQLITE_STATIC); sqlite3_bind_text(stmt_query, 2, FWUPD_RESULT_KEY_VERSION, -1, SQLITE_STATIC); while (sqlite3_step(stmt_query) == SQLITE_ROW) { const gchar *fn_mtimes_old = (const gchar *)sqlite3_column_text(stmt_query, 0); if (g_strcmp0(fn_mtimes->str, fn_mtimes_old) == 0) { g_debug("mtimes unchanged: %s, doing nothing", fn_mtimes->str); return TRUE; } g_debug("mtimes changed %s vs %s -- regenerating", fn_mtimes_old, fn_mtimes->str); } /* delete any existing data */ if (!fu_quirks_db_sqlite3_exec(self, "BEGIN TRANSACTION;", error)) return FALSE; if (!fu_quirks_db_sqlite3_exec(self, "DELETE FROM quirks;", error)) return FALSE; /* prepared statement for speed */ if (sqlite3_prepare_v3(self->db, "INSERT INTO quirks (guid, key, value) VALUES (?1,?2,?3)", -1, SQLITE_PREPARE_PERSISTENT, &stmt_insert, NULL) != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to prepare SQL to insert history: %s", sqlite3_errmsg(self->db)); return FALSE; } /* populate database */ for (guint i = 0; i < G_N_ELEMENTS(map); i++) { const FuQuirksDbItem *item = &map[i]; g_autofree gchar *fn = g_build_filename(vendor_ids_dir, item->fn, NULL); g_autoptr(FuQuirksDbHelper) helper = g_new0(FuQuirksDbHelper, 1); g_autoptr(GFile) file = g_file_new_for_path(fn); g_autoptr(GInputStream) stream = NULL; /* split into lines */ if (!g_file_query_exists(file, NULL)) { g_debug("%s not found", fn); continue; } g_debug("indexing vendor IDs from %s", fn); stream = G_INPUT_STREAM(g_file_read(file, NULL, error)); if (stream == NULL) return FALSE; helper->self = self; helper->subsystem = item->subsystem; helper->title_vid = item->title_vid; helper->title_pid = item->title_pid; helper->stmt = stmt_insert; helper->vid = g_string_new(NULL); if (!fu_strsplit_stream(stream, 0x0, "\n", item->func, helper, error)) return FALSE; } /* set schema */ sqlite3_reset(stmt_insert); sqlite3_bind_text(stmt_insert, 1, guid_fwupd, -1, SQLITE_STATIC); sqlite3_bind_text(stmt_insert, 2, FWUPD_RESULT_KEY_VERSION, -1, SQLITE_STATIC); sqlite3_bind_text(stmt_insert, 3, fn_mtimes->str, -1, SQLITE_STATIC); if (sqlite3_step(stmt_insert) != SQLITE_DONE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to execute prepared statement: %s", sqlite3_errmsg(self->db)); return FALSE; } if (!fu_quirks_db_sqlite3_exec(self, "COMMIT;", error)) return FALSE; /* success */ return TRUE; } #endif /** * fu_quirks_load: (skip) * @self: a #FuQuirks * @load_flags: load flags * @error: (nullable): optional return location for an error * * Loads the various files that define the hardware quirks used in plugins. * * Returns: %TRUE for success * * Since: 1.0.1 **/ gboolean fu_quirks_load(FuQuirks *self, FuQuirksLoadFlags load_flags, GError **error) { #ifdef HAVE_SQLITE g_autofree gchar *cachedirpkg = fu_path_from_kind(FU_PATH_KIND_CACHEDIR_PKG); g_autofree gchar *quirksdb = g_build_filename(cachedirpkg, "quirks.db", NULL); #endif g_return_val_if_fail(FU_IS_QUIRKS(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); self->load_flags = load_flags; self->verbose = g_getenv("FWUPD_XMLB_VERBOSE") != NULL; #ifdef HAVE_SQLITE if (self->db == NULL && (load_flags & FU_QUIRKS_LOAD_FLAG_NO_CACHE) == 0) { g_debug("open database %s", quirksdb); if (!fu_path_mkdir_parent(quirksdb, error)) return FALSE; if (sqlite3_open(quirksdb, &self->db) != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "cannot open %s: %s", quirksdb, sqlite3_errmsg(self->db)); return FALSE; } if (!fu_quirks_db_load(self, load_flags, error)) return FALSE; } #endif /* now silo */ return fu_quirks_check_silo(self, error); } /** * fu_quirks_add_possible_key: * @self: a #FuQuirks * @possible_key: a key name, e.g. `Flags` * * Adds a possible quirk key. If added by a plugin it should be namespaced * using the plugin name, where possible. * * Since: 1.5.8 **/ void fu_quirks_add_possible_key(FuQuirks *self, const gchar *possible_key) { g_return_if_fail(FU_IS_QUIRKS(self)); g_return_if_fail(possible_key != NULL); g_hash_table_add(self->possible_keys, g_strdup(possible_key)); } static void fu_quirks_housekeeping_cb(FuContext *ctx, FuQuirks *self) { #ifdef HAVE_SQLITE sqlite3_release_memory(G_MAXINT32); if (self->db != NULL) sqlite3_db_release_memory(self->db); #endif } static void fu_quirks_dispose(GObject *object) { FuQuirks *self = FU_QUIRKS(object); if (self->ctx != NULL) g_signal_handlers_disconnect_by_data(self->ctx, self); g_clear_object(&self->ctx); G_OBJECT_CLASS(fu_quirks_parent_class)->dispose(object); } static void fu_quirks_class_init(FuQuirksClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->dispose = fu_quirks_dispose; object_class->finalize = fu_quirks_finalize; } static void fu_quirks_init(FuQuirks *self) { self->possible_keys = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); self->invalid_keys = g_ptr_array_new_with_free_func(g_free); /* built in */ fu_quirks_add_possible_key(self, FU_QUIRKS_BRANCH); fu_quirks_add_possible_key(self, FU_QUIRKS_CHILDREN); fu_quirks_add_possible_key(self, FU_QUIRKS_COUNTERPART_GUID); fu_quirks_add_possible_key(self, FU_QUIRKS_FIRMWARE_SIZE); fu_quirks_add_possible_key(self, FU_QUIRKS_FIRMWARE_SIZE_MAX); fu_quirks_add_possible_key(self, FU_QUIRKS_FIRMWARE_SIZE_MIN); fu_quirks_add_possible_key(self, FU_QUIRKS_FLAGS); fu_quirks_add_possible_key(self, FU_QUIRKS_GTYPE); fu_quirks_add_possible_key(self, FU_QUIRKS_FIRMWARE_GTYPE); fu_quirks_add_possible_key(self, FU_QUIRKS_GUID); fu_quirks_add_possible_key(self, FU_QUIRKS_ICON); fu_quirks_add_possible_key(self, FU_QUIRKS_INHIBIT); fu_quirks_add_possible_key(self, FU_QUIRKS_INSTALL_DURATION); fu_quirks_add_possible_key(self, FU_QUIRKS_ISSUE); fu_quirks_add_possible_key(self, FU_QUIRKS_NAME); fu_quirks_add_possible_key(self, FU_QUIRKS_PARENT_GUID); fu_quirks_add_possible_key(self, FU_QUIRKS_PLUGIN); fu_quirks_add_possible_key(self, FU_QUIRKS_PRIORITY); fu_quirks_add_possible_key(self, FU_QUIRKS_PROTOCOL); fu_quirks_add_possible_key(self, FU_QUIRKS_PROXY_GUID); fu_quirks_add_possible_key(self, FU_QUIRKS_BATTERY_THRESHOLD); fu_quirks_add_possible_key(self, FU_QUIRKS_REMOVE_DELAY); fu_quirks_add_possible_key(self, FU_QUIRKS_SUMMARY); fu_quirks_add_possible_key(self, FU_QUIRKS_UPDATE_IMAGE); fu_quirks_add_possible_key(self, FU_QUIRKS_UPDATE_MESSAGE); fu_quirks_add_possible_key(self, FU_QUIRKS_VENDOR); fu_quirks_add_possible_key(self, FU_QUIRKS_VENDOR_ID); fu_quirks_add_possible_key(self, FU_QUIRKS_VERSION); fu_quirks_add_possible_key(self, FU_QUIRKS_VERSION_FORMAT); fu_quirks_add_possible_key(self, FU_QUIRKS_CFI_DEVICE_CMD_READ_ID); fu_quirks_add_possible_key(self, FU_QUIRKS_CFI_DEVICE_CMD_READ_ID_SZ); fu_quirks_add_possible_key(self, FU_QUIRKS_CFI_DEVICE_CMD_CHIP_ERASE); fu_quirks_add_possible_key(self, FU_QUIRKS_CFI_DEVICE_CMD_BLOCK_ERASE); fu_quirks_add_possible_key(self, FU_QUIRKS_CFI_DEVICE_CMD_SECTOR_ERASE); fu_quirks_add_possible_key(self, FU_QUIRKS_CFI_DEVICE_CMD_WRITE_STATUS); fu_quirks_add_possible_key(self, FU_QUIRKS_CFI_DEVICE_CMD_PAGE_PROG); fu_quirks_add_possible_key(self, FU_QUIRKS_CFI_DEVICE_CMD_READ_DATA); fu_quirks_add_possible_key(self, FU_QUIRKS_CFI_DEVICE_CMD_READ_STATUS); fu_quirks_add_possible_key(self, FU_QUIRKS_CFI_DEVICE_CMD_WRITE_EN); fu_quirks_add_possible_key(self, FU_QUIRKS_CFI_DEVICE_PAGE_SIZE); fu_quirks_add_possible_key(self, FU_QUIRKS_CFI_DEVICE_SECTOR_SIZE); fu_quirks_add_possible_key(self, FU_QUIRKS_CFI_DEVICE_BLOCK_SIZE); } static void fu_quirks_finalize(GObject *obj) { FuQuirks *self = FU_QUIRKS(obj); if (self->query_kv != NULL) g_object_unref(self->query_kv); if (self->query_vs != NULL) g_object_unref(self->query_vs); if (self->silo != NULL) g_object_unref(self->silo); #ifdef HAVE_SQLITE if (self->db != NULL) sqlite3_close(self->db); #endif g_hash_table_unref(self->possible_keys); g_ptr_array_unref(self->invalid_keys); G_OBJECT_CLASS(fu_quirks_parent_class)->finalize(obj); } /** * fu_quirks_new: (skip) * * Creates a new quirks object. * * Returns: a new #FuQuirks * * Since: 1.0.1 **/ FuQuirks * fu_quirks_new(FuContext *ctx) { FuQuirks *self; self = g_object_new(FU_TYPE_QUIRKS, NULL); self->ctx = g_object_ref(ctx); g_signal_connect(self->ctx, "housekeeping", G_CALLBACK(fu_quirks_housekeeping_cb), self); return FU_QUIRKS(self); } fwupd-2.0.10/libfwupdplugin/fu-quirks.h000066400000000000000000000245641501337203100200570ustar00rootroot00000000000000/* * Copyright 2016 Mario Limonciello * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-context.h" #define FU_TYPE_QUIRKS (fu_quirks_get_type()) G_DECLARE_FINAL_TYPE(FuQuirks, fu_quirks, FU, QUIRKS, GObject) /** * FuQuirksLoadFlags: * @FU_QUIRKS_LOAD_FLAG_NONE: No flags set * @FU_QUIRKS_LOAD_FLAG_READONLY_FS: Ignore readonly filesystem errors * @FU_QUIRKS_LOAD_FLAG_NO_CACHE: Do not save to a persistent cache * @FU_QUIRKS_LOAD_FLAG_NO_VERIFY: Do not check the key files for errors * * The flags to use when loading quirks. **/ typedef enum { FU_QUIRKS_LOAD_FLAG_NONE = 0, FU_QUIRKS_LOAD_FLAG_READONLY_FS = 1 << 0, FU_QUIRKS_LOAD_FLAG_NO_CACHE = 1 << 1, FU_QUIRKS_LOAD_FLAG_NO_VERIFY = 1 << 2, /*< private >*/ FU_QUIRKS_LOAD_FLAG_LAST } FuQuirksLoadFlags; /** * FuQuirksIter: * @self: a #FuQuirks * @key: a key * @value: a value * @user_data: (closure): user data * * The quirks iteration callback. */ typedef void (*FuQuirksIter)(FuQuirks *self, const gchar *key, const gchar *value, FuContextQuirkSource source, gpointer user_data); FuQuirks * fu_quirks_new(FuContext *ctx); gboolean fu_quirks_load(FuQuirks *self, FuQuirksLoadFlags load_flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); const gchar * fu_quirks_lookup_by_id(FuQuirks *self, const gchar *guid, const gchar *key) G_GNUC_NON_NULL(1, 2, 3); gboolean fu_quirks_lookup_by_id_iter(FuQuirks *self, const gchar *guid, const gchar *key, FuQuirksIter iter_cb, gpointer user_data) G_GNUC_NON_NULL(1, 2); void fu_quirks_add_possible_key(FuQuirks *self, const gchar *possible_key) G_GNUC_NON_NULL(1, 2); /** * FU_QUIRKS_PLUGIN: * * The quirk key for the plugin name, e.g. `csr`. * * Since: 1.3.7 **/ #define FU_QUIRKS_PLUGIN "Plugin" /** * FU_QUIRKS_FLAGS: * * The quirk key for either for public, internal or private flags, e.g. `is-bootloader`. * * Since: 1.3.7 **/ #define FU_QUIRKS_FLAGS "Flags" /** * FU_QUIRKS_SUMMARY: * * The quirk key for the summary, e.g. `An open source display colorimeter`. * * Since: 1.3.7 **/ #define FU_QUIRKS_SUMMARY "Summary" /** * FU_QUIRKS_ICON: * * The quirk key for the icon, e.g. `media-removable`. * * Since: 1.3.7 **/ #define FU_QUIRKS_ICON "Icon" /** * FU_QUIRKS_NAME: * * The quirk key for the name, e.g. `ColorHug`. * * Since: 1.3.7 **/ #define FU_QUIRKS_NAME "Name" /** * FU_QUIRKS_BRANCH: * * The quirk key for the firmware branch. * * Since: 1.5.0 **/ #define FU_QUIRKS_BRANCH "Branch" /** * FU_QUIRKS_GUID: * * The quirk key for the GUID, e.g. `537f7800-8529-5656-b2fa-b0901fe91696`. * * If the value provided is not already a suitable GUID, it will be converted to one. * * Since: 1.3.7 **/ #define FU_QUIRKS_GUID "Guid" /** * FU_QUIRKS_GUID_QUIRK: * * The quirk key for the GUID, only used for quirk matching, e.g. `SYNAPTICS_CAPE\CX31993`. * * If the value provided is not already a suitable GUID, it will be converted to one. * * Since: 1.9.6 **/ #define FU_QUIRKS_GUID_QUIRK "Guid[quirk]" /** * FU_QUIRKS_COUNTERPART_GUID: * * The quirk key for the counterpart GUID, e.g. `537f7800-8529-5656-b2fa-b0901fe91696`. * * A counterpart GUID is typically the GUID of the same device in bootloader or runtime mode, * if they have a different device PCI or USB ID. * Adding this type of GUID does not cause a "cascade" by matching using the quirk database. * * If the value provided is not already a suitable GUID, it will be converted to one. * * Since: 1.3.7 **/ #define FU_QUIRKS_COUNTERPART_GUID "CounterpartGuid" /** * FU_QUIRKS_PARENT_GUID: * * The quirk key for the parent GUID, e.g. `537f7800-8529-5656-b2fa-b0901fe91696`. * * If the value provided is not already a suitable GUID, it will be converted to one. * * Since: 1.3.7 **/ #define FU_QUIRKS_PARENT_GUID "ParentGuid" /** * FU_QUIRKS_PROXY_GUID: * * The quirk key for the proxy GUID, e.g. `537f7800-8529-5656-b2fa-b0901fe91696`. * * Since: 1.4.1 **/ #define FU_QUIRKS_PROXY_GUID "ProxyGuid" /** * FU_QUIRKS_CHILDREN: * * The quirk key for the children. This should contain the custom GType, e.g. * `FuRts54xxDeviceUSB\VID_0763&PID_2806&I2C_01`. * * This allows the quirk entry to adds one or more virtual devices to a physical device. * If the type of device is not specified the parent device type is used. * * Since: 1.3.7 **/ #define FU_QUIRKS_CHILDREN "Children" /** * FU_QUIRKS_VERSION: * * The quirk key for the version, e.g. `1.2.3`. * * Since: 1.3.7 **/ #define FU_QUIRKS_VERSION "Version" /** * FU_QUIRKS_VENDOR: * * The quirk key for the vendor name, e.g. `Hughski Limited`. * * Since: 1.3.7 **/ #define FU_QUIRKS_VENDOR "Vendor" /** * FU_QUIRKS_VENDOR_ID: * * The quirk key for the vendor ID, e.g. `USB:0x123A`. * * Since: 1.3.7 **/ #define FU_QUIRKS_VENDOR_ID "VendorId" /** * FU_QUIRKS_FIRMWARE_SIZE_MIN: * * The quirk key for the minimum firmware size in bytes, e.g. `512`. * * Since: 1.3.7 **/ #define FU_QUIRKS_FIRMWARE_SIZE_MIN "FirmwareSizeMin" /** * FU_QUIRKS_FIRMWARE_SIZE_MAX: * * The quirk key for the maximum firmware size in bytes, e.g. `1024`. * * Since: 1.3.7 **/ #define FU_QUIRKS_FIRMWARE_SIZE_MAX "FirmwareSizeMax" /** * FU_QUIRKS_FIRMWARE_SIZE: * * The quirk key for the exact required firmware size in bytes, e.g. `1024`. * * Since: 1.3.7 **/ #define FU_QUIRKS_FIRMWARE_SIZE "FirmwareSize" /** * FU_QUIRKS_INSTALL_DURATION: * * The quirk key for the install duration in seconds, e.g. `60`. * * Since: 1.3.7 **/ #define FU_QUIRKS_INSTALL_DURATION "InstallDuration" /** * FU_QUIRKS_VERSION_FORMAT: * * The quirk key for the version format, e.g. `quad`. * * Since: 1.3.7 **/ #define FU_QUIRKS_VERSION_FORMAT "VersionFormat" /** * FU_QUIRKS_GTYPE: * * The quirk key for the custom GType, e.g. `FuCcgxHidDevice`. * * Since: 1.3.7 **/ #define FU_QUIRKS_GTYPE "GType" /** * FU_QUIRKS_PROXY_GTYPE: * * The quirk key for the custom proxy GType, e.g. `FuCcgxHidDevice`. * * Since: 1.9.15 **/ #define FU_QUIRKS_PROXY_GTYPE "ProxyGType" /** * FU_QUIRKS_FIRMWARE_GTYPE: * * The quirk key for the custom firmware GType, e.g. `FuUswidFirmware`. * * Since: 1.7.2 **/ #define FU_QUIRKS_FIRMWARE_GTYPE "FirmwareGType" /** * FU_QUIRKS_PROTOCOL: * * The quirk key for the protocol, e.g. `org.usb.dfu`. * * Since: 1.3.7 **/ #define FU_QUIRKS_PROTOCOL "Protocol" /** * FU_QUIRKS_UPDATE_MESSAGE: * * The quirk key for the update message shown after the transaction has completed. * * Since: 1.4.0 **/ #define FU_QUIRKS_UPDATE_MESSAGE "UpdateMessage" /** * FU_QUIRKS_UPDATE_IMAGE: * * The quirk key for the update image shown before the update is performed. * * Since: 1.5.0 **/ #define FU_QUIRKS_UPDATE_IMAGE "UpdateImage" /** * FU_QUIRKS_PRIORITY: * * The quirk key for the device priority, e.g. `2`. * * Since: 1.4.1 **/ #define FU_QUIRKS_PRIORITY "Priority" /** * FU_QUIRKS_BATTERY_THRESHOLD: * * The quirk key for the battery threshold in percent, e.g. `80`. * * Since: 1.6.0 **/ #define FU_QUIRKS_BATTERY_THRESHOLD "BatteryThreshold" /** * FU_QUIRKS_REMOVE_DELAY: * * The quirk key for the device removal delay in milliseconds, e.g. `2500`. * * Since: 1.5.0 **/ #define FU_QUIRKS_REMOVE_DELAY "RemoveDelay" /** * FU_QUIRKS_ACQUIESCE_DELAY: * * The quirk key for the device removal delay in milliseconds, e.g. `2500`. * * Since: 1.8.3 **/ #define FU_QUIRKS_ACQUIESCE_DELAY "AcquiesceDelay" /** * FU_QUIRKS_INHIBIT: * * The quirk key to inhibit the UPDATABLE flag and to set an update error, e.g. `In safe mode`. * * Since: 1.6.2 **/ #define FU_QUIRKS_INHIBIT "Inhibit" /** * FU_QUIRKS_ISSUE: * * The quirk key to add security issues affecting a specific device, e.g. * `https://www.pugetsystems.com/support/guides/critical-samsung-ssd-firmware-update/`. * * Since: 1.7.6 **/ #define FU_QUIRKS_ISSUE "Issue" /** * FU_QUIRKS_CFI_DEVICE_CMD_READ_ID * * The quirk key to set the CFI read ID command, e.g. `0xF8`. * * Since: 1.8.2 **/ #define FU_QUIRKS_CFI_DEVICE_CMD_READ_ID "CfiDeviceCmdReadId" /** * FU_QUIRKS_CFI_DEVICE_CMD_READ_ID_SZ * * The quirk key to set the CFI read ID size, e.g. `0xF8`. * * Since: 1.8.2 **/ #define FU_QUIRKS_CFI_DEVICE_CMD_READ_ID_SZ "CfiDeviceCmdReadIdSz" /** * FU_QUIRKS_CFI_DEVICE_CMD_CHIP_ERASE * * The quirk key to set the CFI chip erase command, e.g. `0xF8`. * * Since: 1.8.2 **/ #define FU_QUIRKS_CFI_DEVICE_CMD_CHIP_ERASE "CfiDeviceCmdChipErase" /** * FU_QUIRKS_CFI_DEVICE_CMD_BLOCK_ERASE * * The quirk key to set the CFI block erase command, e.g. `0xF8`. * * Since: 1.8.2 **/ #define FU_QUIRKS_CFI_DEVICE_CMD_BLOCK_ERASE "CfiDeviceCmdBlockErase" /** * FU_QUIRKS_CFI_DEVICE_CMD_SECTOR_ERASE * * The quirk key to set the CFI sector erase command, e.g. `0xF8`. * * Since: 1.8.2 **/ #define FU_QUIRKS_CFI_DEVICE_CMD_SECTOR_ERASE "CfiDeviceCmdSectorErase" /** * FU_QUIRKS_CFI_DEVICE_CMD_WRITE_STATUS * * The quirk key to set the CFI write status command, e.g. `0xF8`. * * Since: 1.8.2 **/ #define FU_QUIRKS_CFI_DEVICE_CMD_WRITE_STATUS "CfiDeviceCmdWriteStatus" /** * FU_QUIRKS_CFI_DEVICE_CMD_PAGE_PROG * * The quirk key to set the CFI page program command, e.g. `0xF8`. * * Since: 1.8.2 **/ #define FU_QUIRKS_CFI_DEVICE_CMD_PAGE_PROG "CfiDeviceCmdPageProg" /** * FU_QUIRKS_CFI_DEVICE_CMD_READ_DATA * * The quirk key to set the CFI read data command, e.g. `0xF8`. * * Since: 1.8.2 **/ #define FU_QUIRKS_CFI_DEVICE_CMD_READ_DATA "CfiDeviceCmdReadData" /** * FU_QUIRKS_CFI_DEVICE_CMD_READ_STATUS * * The quirk key to set the CFI read status command, e.g. `0xF8`. * * Since: 1.8.2 **/ #define FU_QUIRKS_CFI_DEVICE_CMD_READ_STATUS "CfiDeviceCmdReadStatus" /** * FU_QUIRKS_CFI_DEVICE_CMD_WRITE_EN * * The quirk key to set the CFI write en command, e.g. `0xF8`. * * Since: 1.8.2 **/ #define FU_QUIRKS_CFI_DEVICE_CMD_WRITE_EN "CfiDeviceCmdWriteEn" /** * FU_QUIRKS_CFI_DEVICE_PAGE_SIZE * * The quirk key to set the CFI page size, e.g. `0xF8`. * * Since: 1.8.2 **/ #define FU_QUIRKS_CFI_DEVICE_PAGE_SIZE "CfiDevicePageSize" /** * FU_QUIRKS_CFI_DEVICE_SECTOR_SIZE * * The quirk key to set the CFI sector size, e.g. `0x100`. * * Since: 1.8.2 **/ #define FU_QUIRKS_CFI_DEVICE_SECTOR_SIZE "CfiDeviceSectorSize" /** * FU_QUIRKS_CFI_DEVICE_BLOCK_SIZE * * The quirk key to set the CFI block size, e.g. `0x100`. * * Since: 1.8.2 **/ #define FU_QUIRKS_CFI_DEVICE_BLOCK_SIZE "CfiDeviceBlockSize" fwupd-2.0.10/libfwupdplugin/fu-rustgen-enum.c.in000066400000000000000000000022501501337203100215560ustar00rootroot00000000000000 {%- set export = obj.export('ToString') %} {%- if export in [Export.PUBLIC, Export.PRIVATE] %} {{export.value}}const gchar * {{obj.c_method('ToString')}}({{obj.c_type}} val) { {%- for item in obj.items %} if (val == {{item.c_define}}) return "{{item.value}}"; {%- endfor %} return NULL; } {%- endif %} {%- set export = obj.export('ToBitString') %} {%- if export in [Export.PUBLIC, Export.PRIVATE] %} {{export.value}}gchar * {{obj.c_method('ToString')}}({{obj.c_type}} val) { const gchar *data[{{obj.items|length}}] = {0}; guint idx = 0; if (val == {{obj.items[0].c_define}}) return g_strdup("{{obj.items[0].value}}"); {%- for item in obj.items[1:] %} if (val & {{item.c_define}}) data[idx++] = "{{item.value}}"; {%- endfor %} return g_strjoinv(",", (gchar **)data); } {%- endif %} {%- set export = obj.export('FromString') %} {%- if export in [Export.PUBLIC, Export.PRIVATE] %} {{export.value}}{{obj.c_type}} {{obj.c_method('FromString')}}(const gchar *val) { {%- for item in obj.items %} if (g_strcmp0(val, "{{item.value}}") == 0) return {{item.c_define}}; {%- endfor %} return {{obj.items[0].c_define}}; } {%- endif %} fwupd-2.0.10/libfwupdplugin/fu-rustgen-enum.h.in000066400000000000000000000013371501337203100215700ustar00rootroot00000000000000typedef enum { {%- for item in obj.items %} {%- if item.default %} {{item.c_define}} = {{item.default}}, {%- else %} {{item.c_define}}, {%- endif %} {%- endfor %} } {{obj.c_type}}; {%- if not obj.items_any_defaults and not obj.item('Last') %} #define {{obj.c_define_last}} {{obj.items|length}} {%- endif %} {%- if obj.export('ToString') == Export.PUBLIC %} const gchar *{{obj.c_method('ToString')}}({{obj.c_type}} val) G_GNUC_CONST; {%- endif %} {%- if obj.export('ToBitString') == Export.PUBLIC %} gchar *{{obj.c_method('ToString')}}({{obj.c_type}} val) G_GNUC_CONST; {%- endif %} {%- if obj.export('FromString') == Export.PUBLIC %} {{obj.c_type}} {{obj.c_method('FromString')}}(const gchar *val) G_GNUC_CONST; {%- endif %} fwupd-2.0.10/libfwupdplugin/fu-rustgen-struct.c.in000066400000000000000000000446631501337203100221540ustar00rootroot00000000000000/* getters */ {%- for item in obj.items | selectattr('enabled') %} {%- set export = item.export('Getters') %} {%- if export in [Export.PUBLIC, Export.PRIVATE] %} /** * {{item.c_getter}}: (skip): **/ {%- if item.type == Type.STRING %} {{export.value}}gchar * {{item.c_getter}}(const {{obj.name}} *st) { g_return_val_if_fail(st != NULL, NULL); return fu_memstrsafe(st->data, st->len, {{item.offset}}, {{item.size}}, NULL); } {%- elif item.struct_obj %} {%- if item.n_elements %} {{export.value}}{{item.struct_obj.name}} * {{item.c_getter}}(const {{obj.name}} *st, guint idx) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_return_val_if_fail(st != NULL, NULL); g_return_val_if_fail(idx < {{item.n_elements}}, NULL); g_byte_array_append(buf, st->data + {{item.c_define('OFFSET')}} + ({{item.struct_obj.c_define('SIZE')}} * idx), {{item.struct_obj.size}}); return g_steal_pointer(&buf); } {%- else %} {{export.value}}{{item.struct_obj.name}} * {{item.c_getter}}(const {{obj.name}} *st) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_return_val_if_fail(st != NULL, NULL); g_byte_array_append(buf, st->data + {{item.c_define('OFFSET')}}, {{item.size}}); return g_steal_pointer(&buf); } {%- endif %} {%- elif item.type == Type.U8 and item.n_elements %} {{export.value}}const guint8 * {{item.c_getter}}(const {{obj.name}} *st, gsize *bufsz) { g_return_val_if_fail(st != NULL, NULL); if (bufsz != NULL) *bufsz = {{item.size}}; return st->data + {{item.offset}}; } {%- elif item.type == Type.GUID %} {{export.value}}const fwupd_guid_t * {{item.c_getter}}(const {{obj.name}} *st) { g_return_val_if_fail(st != NULL, NULL); return (const fwupd_guid_t *) (st->data + {{item.offset}}); } {%- elif item.type == Type.U8 %} {{export.value}}{{item.type_glib}} {{item.c_getter}}(const {{obj.name}} *st) { g_return_val_if_fail(st != NULL, 0x0); return st->data[{{item.offset}}]; } {%- elif item.type in [Type.U16, Type.U24, Type.U32, Type.U64, Type.I8, Type.I16, Type.I32, Type.I64] %} {%- if item.n_elements %} {{export.value}}{{item.type_glib}} {{item.c_getter}}(const {{obj.name}} *st, guint idx) { g_return_val_if_fail(st != NULL, 0x0); return fu_memread_{{item.type_mem}}(st->data + {{item.offset}} + (sizeof({{item.type_glib}}) * idx), {{item.endian_glib}}); } {%- else %} {{export.value}}{{item.type_glib}} {{item.c_getter}}(const {{obj.name}} *st) { g_return_val_if_fail(st != NULL, 0x0); return fu_memread_{{item.type_mem}}(st->data + {{item.offset}}, {{item.endian_glib}}); } {%- endif %} {%- elif item.type in [Type.B32] %} {{export.value}}{{item.type_glib}} {{item.c_getter}}(const {{obj.name}} *st) { guint32 val; g_return_val_if_fail(st != NULL, 0x0); g_return_val_if_fail(st->len >= sizeof({{item.type_glib}}), 0x0); val = fu_memread_{{item.type_mem}}(st->data + {{item.offset}}, {{item.endian_glib}}); return (val >> {{item.bits_offset}}) & {{item.bits_mask}}; } {%- endif %} {%- endif %} {%- endfor %} /* setters */ {%- for item in obj.items | selectattr('enabled') %} {%- set export = item.export('Setters') %} {%- if export in [Export.PUBLIC, Export.PRIVATE] %} /** * {{item.c_setter}}: (skip): **/ {%- if item.type == Type.STRING %} {{export.value}}gboolean {{item.c_setter}}({{obj.name}} *st, const gchar *value, GError **error) { gsize len; g_return_val_if_fail(st != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (value == NULL) { memset(st->data + {{item.offset}}, 0x0, {{item.size}}); return TRUE; } len = strlen(value); if (len > {{item.size}}) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "string '%s' (0x%x bytes) does not fit in {{obj.name}}.{{item.element_id}} (0x%x bytes)", value, (guint) len, (guint) {{item.size}}); return FALSE; } return fu_memcpy_safe(st->data, st->len, {{item.offset}}, (const guint8 *)value, len, 0x0, len, error); } {%- elif item.struct_obj %} {%- if item.n_elements %} {{export.value}}gboolean {{item.c_setter}}({{obj.name}} *st, guint idx, const {{item.struct_obj.name}} *st_donor, GError **error) { g_return_val_if_fail(st != NULL, FALSE); g_return_val_if_fail(st_donor != NULL, FALSE); g_return_val_if_fail(idx < {{item.n_elements}}, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (st_donor->len > {{item.struct_obj.c_define('SIZE')}}) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "donor '{{item.struct_obj.name}}' (0x%x bytes) does not fit in " "{{obj.name}}.{{item.element_id}} (0x%x bytes)", (guint) st_donor->len, (guint) {{item.struct_obj.c_define('SIZE')}}); return FALSE; } memcpy(st->data + {{item.c_define('OFFSET')}} + ({{item.struct_obj.c_define('SIZE')}} * idx), st_donor->data, st_donor->len); return TRUE; } {%- else %} {{export.value}}gboolean {{item.c_setter}}({{obj.name}} *st, const {{item.struct_obj.name}} *st_donor, GError **error) { g_return_val_if_fail(st != NULL, FALSE); g_return_val_if_fail(st_donor != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (st_donor->len > {{item.struct_obj.c_define('SIZE')}}) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "donor '{{item.struct_obj.name}}' (0x%x bytes) does not fit in " "{{obj.name}}.{{item.element_id}} (0x%x bytes)", (guint) st_donor->len, (guint) {{item.struct_obj.c_define('SIZE')}}); return FALSE; } memcpy(st->data + {{item.c_define('OFFSET')}}, st_donor->data, st_donor->len); return TRUE; } {%- endif %} {%- elif item.type == Type.U8 and item.n_elements %} {{export.value}}gboolean {{item.c_setter}}({{obj.name}} *st, const guint8 *buf, gsize bufsz, GError **error) { g_return_val_if_fail(st != NULL, FALSE); g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return fu_memcpy_safe(st->data, st->len, {{item.offset}}, buf, bufsz, 0x0, bufsz, error); } {%- elif item.type == Type.GUID %} {{export.value}}void {{item.c_setter}}({{obj.name}} *st, const fwupd_guid_t *value) { g_return_if_fail(st != NULL); g_return_if_fail(value != NULL); memcpy(st->data + {{item.offset}}, value, sizeof(*value)); /* nocheck:blocked */ } {%- elif item.type == Type.U8 %} {{export.value}}void {{item.c_setter}}({{obj.name}} *st, {{item.type_glib}} value) { g_return_if_fail(st != NULL); st->data[{{item.offset}}] = value; } {%- elif item.type == Type.B32 %} {{export.value}}void {{item.c_setter}}({{obj.name}} *st, {{item.type_glib}} value) { guint32 tmp; g_return_if_fail(st != NULL); g_return_if_fail(st->len >= sizeof({{item.type_glib}})); tmp = fu_memread_{{item.type_mem}}(st->data + {{item.offset}}, {{item.endian_glib}}); tmp &= ~({{item.bits_mask}} << {{item.bits_offset}}); tmp |= (value & {{item.bits_mask}}) << {{item.bits_offset}}; fu_memwrite_{{item.type_mem}}(st->data + {{item.offset}}, tmp, {{item.endian_glib}}); } {%- elif item.type in [Type.U16, Type.U24, Type.U32, Type.U64, Type.I8, Type.I16, Type.I32, Type.I64] %} {%- if item.n_elements %} {{export.value}}void {{item.c_setter}}({{obj.name}} *st, guint idx, {{item.type_glib}} value) { g_return_if_fail(st != NULL); g_return_if_fail(idx < {{item.n_elements}}); fu_memwrite_{{item.type_mem}}(st->data + {{item.offset}} + (sizeof({{item.type_glib}}) * idx), value, {{item.endian_glib}}); } {%- else %} {{export.value}}void {{item.c_setter}}({{obj.name}} *st, {{item.type_glib}} value) { g_return_if_fail(st != NULL); fu_memwrite_{{item.type_mem}}(st->data + {{item.offset}}, value, {{item.endian_glib}}); } {%- endif %} {%- endif %} {%- endif %} {%- endfor %} {%- set export = obj.export('New') %} {%- if export in [Export.PUBLIC, Export.PRIVATE] %} /** * {{obj.c_method('New')}}: (skip): **/ {{export.value}}{{obj.name}} * {{obj.c_method('New')}}(void) { {{obj.name}} *st = g_byte_array_sized_new({{obj.size}}); fu_byte_array_set_size(st, {{obj.size}}, 0x0); {%- for item in obj.items | selectattr('padding') %} memset(st->data + {{item.offset}}, {{item.padding}}, {{item.size}}); {%- endfor %} {%- for item in obj.items | selectattr('struct_obj') %} { g_autoptr(GByteArray) st_donor = {{item.struct_obj.c_method('New')}}(); memcpy(st->data + 0x{{'{:X}'.format(item.offset)}}, st_donor->data, st_donor->len); /* nocheck:blocked */ } {%- endfor %} {%- for item in obj.items | selectattr('default') %} {%- if item.type == Type.STRING %} {{item.c_setter}}(st, "{{item.default}}", NULL); {%- elif item.type == Type.GUID %} {{item.c_setter}}(st, (fwupd_guid_t *) "{{item.default}}"); {%- elif item.type == Type.U8 and item.n_elements %} memcpy(st->data + 0x{{'{:X}'.format(item.offset)}}, "{{item.default}}", {{item.size}}); /* nocheck:blocked */ {%- else %} {{item.c_setter}}(st, {{item.default}}); {%- endif %} {%- endfor %} return st; } {%- endif %} {%- set export = obj.export('ToString') %} {%- if export in [Export.PUBLIC, Export.PRIVATE] %} /** * {{obj.c_method('ToString')}}: (skip): **/ {{export.value}}gchar * {{obj.c_method('ToString')}}(const {{obj.name}} *st) { g_autoptr(GString) str = g_string_new("{{obj.name}}:\n"); g_return_val_if_fail(st != NULL, NULL); {%- for item in obj.items | selectattr('enabled') | rejectattr('constant') %} {%- if item.enum_obj %} { const gchar *tmp = {{item.enum_obj.c_method('ToString')}}({{item.c_getter}}(st)); if (tmp != NULL) { g_string_append_printf(str, " {{item.element_id}}: 0x%x [%s]\n", (guint) {{item.c_getter}}(st), tmp); } else { g_string_append_printf(str, " {{item.element_id}}: 0x%x\n", (guint) {{item.c_getter}}(st)); } } {%- elif item.type == Type.U8 %} {%- if item.n_elements %} { gsize bufsz = 0; const guint8 *buf = {{item.c_getter}}(st, &bufsz); g_autoptr(GString) tmp = g_string_new(NULL); for (gsize i = 0; i < bufsz; i++) g_string_append_printf(tmp, "%02X", buf[i]); g_string_append_printf(str, " {{item.element_id}}: 0x%s\n", tmp->str); } {%- else %} g_string_append_printf(str, " {{item.element_id}}: 0x%x\n", (guint) {{item.c_getter}}(st)); {%- endif %} {%- elif item.type in [Type.U16, Type.U24, Type.U32, Type.U64, Type.I8, Type.I16, Type.I32, Type.I64, Type.B32] %} {%- if item.n_elements %} for (guint i = 0; i < {{item.n_elements}}; i++) { g_string_append_printf(str, " {{item.element_id}}[%u]: 0x%x\n", i, (guint) {{item.c_getter}}(st, i)); } {%- else %} g_string_append_printf(str, " {{item.element_id}}: 0x%x\n", (guint) {{item.c_getter}}(st)); {%- endif %} {%- elif item.type == Type.GUID %} { g_autofree gchar *tmp = fwupd_guid_to_string({{item.c_getter}}(st), FWUPD_GUID_FLAG_MIXED_ENDIAN); g_string_append_printf(str, " {{item.element_id}}: %s\n", tmp); } {%- elif item.type == Type.STRING %} { g_autofree gchar *tmp = {{item.c_getter}}(st); if (tmp != NULL) g_string_append_printf(str, " {{item.element_id}}: %s\n", tmp); } {%- elif item.struct_obj %} {%- if item.n_elements %} for (guint i = 0; i < {{item.n_elements}}; i++) { g_autoptr(GByteArray) st_tmp = {{item.c_getter}}(st, i); g_autofree gchar *tmp = {{item.struct_obj.c_method('ToString')}}(st_tmp); g_string_append_printf(str, " {{item.element_id}}[%u]: %s\n", i, tmp); } {%- else %} { g_autoptr(GByteArray) st_tmp = {{item.c_getter}}(st); g_autofree gchar *tmp = {{item.struct_obj.c_method('ToString')}}(st_tmp); g_string_append_printf(str, " {{item.element_id}}: %s\n", tmp); } {%- endif %} {%- endif %} {%- endfor %} if (str->len > 0) g_string_set_size(str, str->len - 1); return g_string_free(g_steal_pointer(&str), FALSE); } {%- endif %} {%- set export = obj.export('ValidateInternal') %} {%- if export in [Export.PUBLIC, Export.PRIVATE] %} {{export.value}}gboolean {{obj.c_method('ValidateInternal')}}({{obj.name}} *st, GError **error) { g_return_val_if_fail(st != NULL, FALSE); {%- for item in obj.items | selectattr('constant') %} {%- if item.type == Type.STRING %} if (strncmp((const gchar *) (st->data + {{item.offset}}), "{{item.constant}}", {{item.size}}) != 0) { {%- elif item.type == Type.GUID %} if (memcmp({{item.c_getter}}(st), "{{item.constant}}", {{item.size}}) != 0) { {%- elif item.type == Type.U8 and item.n_elements %} if (memcmp(st->data + {{item.offset}}, "{{item.constant}}", {{item.size}}) != 0) { {%- else %} if ({{item.c_getter}}(st) != {{item.constant}}) { {%- endif %} g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "constant {{obj.name}}.{{item.element_id}} was not valid"); return FALSE; } {%- endfor %} {%- for item in obj.items | selectattr('struct_obj') %} { GByteArray st_tmp = { .data = (guint8*) st->data + 0x{{'{:X}'.format(item.offset)}}, .len = {{item.size}}, }; if (!{{item.struct_obj.c_method('ValidateInternal')}}(&st_tmp, error)) return FALSE; } {%- endfor %} return TRUE; } {%- endif %} {%- set export = obj.export('Validate') %} {%- if export in [Export.PUBLIC, Export.PRIVATE] %} /** * {{obj.c_method('Validate')}}: (skip): **/ {{export.value}}gboolean {{obj.c_method('Validate')}}(const guint8 *buf, gsize bufsz, gsize offset, GError **error) { GByteArray st = {.data = (guint8 *) buf + offset, .len = bufsz - offset, }; g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_memchk_read(bufsz, offset, {{obj.size}}, error)) { g_prefix_error(error, "invalid struct {{obj.name}}: "); return FALSE; } if (!{{obj.c_method('ValidateInternal')}}(&st, error)) return FALSE; return TRUE; } {%- endif %} {%- set export = obj.export('ValidateStream') %} {%- if export in [Export.PUBLIC, Export.PRIVATE] %} /** * {{obj.c_method('ValidateStream')}}: (skip): **/ {{export.value}}gboolean {{obj.c_method('ValidateStream')}}(GInputStream *stream, gsize offset, GError **error) { g_autoptr(GByteArray) st = NULL; g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); st = fu_input_stream_read_byte_array(stream, offset, {{obj.size}}, NULL, error); if (st == NULL) { g_prefix_error(error, "{{obj.name}} failed read of 0x%x: ", (guint) {{obj.size}}); return FALSE; } if (st->len != {{obj.size}}) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "{{obj.name}} requested 0x%x and got 0x%x", (guint) {{obj.size}}, (guint) st->len); return FALSE; } return {{obj.c_method('ValidateInternal')}}(st, error); } {%- endif %} {%- set export = obj.export('ValidateBytes') %} {%- if export in [Export.PUBLIC, Export.PRIVATE] %} /** * {{obj.c_method('ValidateBytes')}}: (skip): **/ {{export.value}}gboolean {{obj.c_method('ValidateBytes')}}(GBytes *blob, gsize offset, GError **error) { gsize bufsz = 0; const guint8 *buf = g_bytes_get_data(blob, &bufsz); return {{obj.c_method('Validate')}}(buf, bufsz, offset, error); } {%- endif %} {%- set export = obj.export('ParseInternal') %} {%- if export in [Export.PUBLIC, Export.PRIVATE] %} {{export.value}}gboolean {{obj.c_method('ParseInternal')}}({{obj.name}} *st, GError **error) { if (!{{obj.c_method('ValidateInternal')}}(st, error)) return FALSE; if (g_getenv("FWUPD_VERBOSE") != NULL) { g_autofree gchar *str = {{obj.c_method('ToString')}}(st); g_debug("%s", str); } return TRUE; } {%- endif %} {%- set export = obj.export('Parse') %} {%- if export in [Export.PUBLIC, Export.PRIVATE] %} /** * {{obj.c_method('Parse')}}: (skip): **/ {{export.value}}{{obj.name}} * {{obj.c_method('Parse')}}(const guint8 *buf, gsize bufsz, gsize offset, GError **error) { g_autoptr(GByteArray) st = g_byte_array_new(); g_return_val_if_fail(buf != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); if (!fu_memchk_read(bufsz, offset, {{obj.size}}, error)) { g_prefix_error(error, "invalid struct {{obj.name}}: "); return NULL; } g_byte_array_append(st, buf + offset, {{obj.size}}); if (!{{obj.c_method('ParseInternal')}}(st, error)) return NULL; return g_steal_pointer(&st); } {%- endif %} {%- set export = obj.export('ParseBytes') %} {%- if export in [Export.PUBLIC, Export.PRIVATE] %} /** * {{obj.c_method('ParseBytes')}}: (skip): **/ {{export.value}}{{obj.name}} * {{obj.c_method('ParseBytes')}}(GBytes *blob, gsize offset, GError **error) { gsize bufsz = 0; const guint8 *buf = g_bytes_get_data(blob, &bufsz); return {{obj.c_method('Parse')}}(buf, bufsz, offset, error); } {%- endif %} {%- set export = obj.export('ParseStream') %} {%- if export in [Export.PUBLIC, Export.PRIVATE] %} /** * {{obj.c_method('ParseStream')}}: (skip): **/ {{export.value}}{{obj.name}} * {{obj.c_method('ParseStream')}}(GInputStream *stream, gsize offset, GError **error) { g_autoptr(GByteArray) st = NULL; st = fu_input_stream_read_byte_array(stream, offset, {{obj.size}}, NULL, error); if (st == NULL) { g_prefix_error(error, "{{obj.name}} failed read of 0x%x: ", (guint) {{obj.size}}); return NULL; } if (st->len != {{obj.size}}) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "{{obj.name}} requested 0x%x and got 0x%x", (guint) {{obj.size}}, (guint) st->len); return NULL; } if (!{{obj.c_method('ParseInternal')}}(st, error)) return NULL; return g_steal_pointer(&st); } {%- endif %} fwupd-2.0.10/libfwupdplugin/fu-rustgen-struct.h.in000066400000000000000000000122071501337203100221460ustar00rootroot00000000000000typedef GByteArray {{obj.name}}; #define {{obj.c_method('Unref')}} g_byte_array_unref G_DEFINE_AUTOPTR_CLEANUP_FUNC({{obj.name}}, {{obj.c_method('Unref')}}) {%- if obj.export('New') == Export.PUBLIC %} {{obj.name}} *{{obj.c_method('New')}}(void) G_GNUC_WARN_UNUSED_RESULT; {%- endif %} {%- if obj.export('Parse') == Export.PUBLIC %} {{obj.name}} *{{obj.c_method('Parse')}}(const guint8 *buf, gsize bufsz, gsize offset, GError **error) G_GNUC_NON_NULL(1) G_GNUC_WARN_UNUSED_RESULT; {%- endif %} {%- if obj.export('ParseBytes') == Export.PUBLIC %} {{obj.name}} *{{obj.c_method('ParseBytes')}}(GBytes *blob, gsize offset, GError **error) G_GNUC_NON_NULL(1) G_GNUC_WARN_UNUSED_RESULT; {%- endif %} {%- if obj.export('ParseStream') == Export.PUBLIC %} {{obj.name}} *{{obj.c_method('ParseStream')}}(GInputStream *stream, gsize offset, GError **error) G_GNUC_NON_NULL(1) G_GNUC_WARN_UNUSED_RESULT; {%- endif %} {%- if obj.export('Validate') == Export.PUBLIC %} gboolean {{obj.c_method('Validate')}}(const guint8 *buf, gsize bufsz, gsize offset, GError **error) G_GNUC_NON_NULL(1) G_GNUC_WARN_UNUSED_RESULT; {%- endif %} {%- if obj.export('ValidateBytes') == Export.PUBLIC %} gboolean {{obj.c_method('ValidateBytes')}}(GBytes *blob, gsize offset, GError **error) G_GNUC_NON_NULL(1) G_GNUC_WARN_UNUSED_RESULT; {%- endif %} {%- if obj.export('ValidateStream') == Export.PUBLIC %} gboolean {{obj.c_method('ValidateStream')}}(GInputStream *stream, gsize offset, GError **error) G_GNUC_NON_NULL(1) G_GNUC_WARN_UNUSED_RESULT; {%- endif %} {%- if obj.export('ToString') == Export.PUBLIC %} gchar *{{obj.c_method('ToString')}}(const {{obj.name}} *st) G_GNUC_NON_NULL(1) G_GNUC_WARN_UNUSED_RESULT; {%- endif %} {%- for item in obj.items | selectattr('enabled') %} {%- if item.export('Getters') == Export.PUBLIC %} {%- if item.type == Type.STRING %} gchar *{{item.c_getter}}(const {{obj.name}} *st) G_GNUC_NON_NULL(1) G_GNUC_WARN_UNUSED_RESULT; {%- elif item.struct_obj %} {%- if item.n_elements %} {{item.struct_obj.name}} *{{item.c_getter}}(const {{obj.name}} *st, guint idx) G_GNUC_NON_NULL(1) G_GNUC_WARN_UNUSED_RESULT; {%- else %} {{item.struct_obj.name}} *{{item.c_getter}}(const {{obj.name}} *st) G_GNUC_NON_NULL(1) G_GNUC_WARN_UNUSED_RESULT; {%- endif %} {%- elif item.type == Type.U8 and item.n_elements %} const guint8 *{{item.c_getter}}(const {{obj.name}} *st, gsize *bufsz) G_GNUC_NON_NULL(1) G_GNUC_WARN_UNUSED_RESULT; {%- elif item.type == Type.GUID %} const fwupd_guid_t *{{item.c_getter}}(const {{obj.name}} *st) G_GNUC_NON_NULL(1) G_GNUC_WARN_UNUSED_RESULT; {%- elif item.type in [Type.U8, Type.U16, Type.U24, Type.U32, Type.U64, Type.I8, Type.I16, Type.I32, Type.I64, Type.B32] %} {%- if item.n_elements %} {{item.type_glib}} {{item.c_getter}}(const {{obj.name}} *st, guint idx) G_GNUC_NON_NULL(1) G_GNUC_WARN_UNUSED_RESULT; {%- else %} {{item.type_glib}} {{item.c_getter}}(const {{obj.name}} *st) G_GNUC_NON_NULL(1) G_GNUC_WARN_UNUSED_RESULT; {%- endif %} {%- endif %} {%- endif %} {%- endfor %} {%- for item in obj.items | selectattr('enabled') %} {%- if item.export('Setters') == Export.PUBLIC %} {%- if item.type == Type.STRING %} gboolean {{item.c_setter}}({{obj.name}} *st, const gchar *value, GError **error) G_GNUC_NON_NULL(1) G_GNUC_WARN_UNUSED_RESULT; {%- elif item.struct_obj %} {%- if item.n_elements %} gboolean {{item.c_setter}}({{obj.name}} *st, guint idx, const {{item.struct_obj.name}} *st_donor, GError **error) G_GNUC_NON_NULL(1, 3) G_GNUC_WARN_UNUSED_RESULT; {%- else %} gboolean {{item.c_setter}}({{obj.name}} *st, const {{item.struct_obj.name}} *st_donor, GError **error) G_GNUC_NON_NULL(1, 2) G_GNUC_WARN_UNUSED_RESULT; {%- endif %} {%- elif item.type == Type.U8 and item.n_elements %} gboolean {{item.c_setter}}({{obj.name}} *st, const guint8 *buf, gsize bufsz, GError **error) G_GNUC_NON_NULL(1, 2) G_GNUC_WARN_UNUSED_RESULT; {%- elif item.type == Type.GUID %} void {{item.c_setter}}({{obj.name}} *st, const fwupd_guid_t *value) G_GNUC_NON_NULL(1, 2); {%- elif item.type in [Type.U8, Type.U16, Type.U24, Type.U32, Type.U64, Type.I8, Type.I16, Type.I32, Type.I64, Type.B32] %} {%- if item.n_elements %} void {{item.c_setter}}({{obj.name}} *st, guint idx, {{item.type_glib}} value) G_GNUC_NON_NULL(1); {%- else %} void {{item.c_setter}}({{obj.name}} *st, {{item.type_glib}} value) G_GNUC_NON_NULL(1); {%- endif %} {%- endif %} {%- endif %} {%- endfor %} #ifndef __GI_SCANNER__ {%- for item in obj.items | selectattr('enabled') %} {%- if item.type != Type.B32 %} #define {{item.c_define('OFFSET')}} 0x{{'{:X}'.format(item.offset)}} {%- endif %} {%- endfor %} #endif #ifndef __GI_SCANNER__ {%- for item in obj.items | selectattr('enabled') | selectattr('n_elements') %} #define {{item.c_define('SIZE')}} 0x{{'{:X}'.format(item.size)}} #define {{item.c_define('N_ELEMENTS')}} {{item.n_elements}} {%- endfor %} #define {{obj.c_define('SIZE')}} 0x{{'{:X}'.format(obj.size)}} #endif #ifndef __GI_SCANNER__ {%- for item in obj.items | selectattr('enabled') | selectattr('default') %} {%- if item.type == Type.STRING or item.type == Type.GUID or item.n_elements %} #define {{item.c_define('DEFAULT')}} "{{item.default}}" {%- else %} #define {{item.c_define('DEFAULT')}} {{item.default}} {%- endif %} {%- endfor %} #endif fwupd-2.0.10/libfwupdplugin/fu-rustgen.c.in000066400000000000000000000004241501337203100206150ustar00rootroot00000000000000/* auto-generated, do not modify */ #include "config.h" #include "{{basename}}" {%- if struct_objs %} #include "fu-byte-array.h" #include "fu-mem-private.h" #include "fu-string.h" {%- endif %} #ifdef G_LOG_DOMAIN #undef G_LOG_DOMAIN #endif #define G_LOG_DOMAIN "FuStruct" fwupd-2.0.10/libfwupdplugin/fu-rustgen.h.in000066400000000000000000000002211501337203100206150ustar00rootroot00000000000000/* auto-generated, do not modify */ #pragma once {%- if struct_objs %} #include {%- else %} #include {%- endif %} fwupd-2.0.10/libfwupdplugin/fu-sbatlevel-section.c000066400000000000000000000105211501337203100221430ustar00rootroot00000000000000/* * Copyright 2023 Canonical Ltd. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-csv-firmware.h" #include "fu-input-stream.h" #include "fu-mem.h" #include "fu-partial-input-stream.h" #include "fu-sbatlevel-section-struct.h" #include "fu-sbatlevel-section.h" G_DEFINE_TYPE(FuSbatlevelSection, fu_sbatlevel_section, FU_TYPE_FIRMWARE); static gboolean fu_sbatlevel_section_add_entry(FuFirmware *firmware, GInputStream *stream, gsize offset, const gchar *entry_name, guint64 entry_idx, FuFirmwareParseFlags flags, GError **error) { gsize streamsz = 0; g_autoptr(FuFirmware) entry_fw = NULL; g_autoptr(GInputStream) partial_stream = NULL; /* stop at the null terminator */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; for (guint i = offset; i < streamsz; i++) { guint8 value = 0; if (!fu_input_stream_read_u8(stream, i, &value, error)) return FALSE; if (value == 0x0) { streamsz = i - 1; break; } } entry_fw = fu_csv_firmware_new(); fu_csv_firmware_add_column_id(FU_CSV_FIRMWARE(entry_fw), "$id"); fu_csv_firmware_add_column_id(FU_CSV_FIRMWARE(entry_fw), "component_generation"); fu_csv_firmware_add_column_id(FU_CSV_FIRMWARE(entry_fw), "date_stamp"); fu_csv_firmware_set_write_column_ids(FU_CSV_FIRMWARE(entry_fw), FALSE); fu_firmware_set_idx(entry_fw, entry_idx); fu_firmware_set_id(entry_fw, entry_name); fu_firmware_set_offset(entry_fw, offset); partial_stream = fu_partial_input_stream_new(stream, offset, streamsz - offset, error); if (partial_stream == NULL) { g_prefix_error(error, "failed to cut CSV section: "); return FALSE; } if (!fu_firmware_parse_stream(entry_fw, partial_stream, 0, flags, error)) { g_prefix_error(error, "failed to parse %s: ", entry_name); return FALSE; } if (!fu_firmware_add_image_full(firmware, entry_fw, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_sbatlevel_section_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { g_autoptr(GByteArray) st = NULL; st = fu_struct_sbat_level_section_header_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; if (!fu_sbatlevel_section_add_entry( firmware, stream, sizeof(guint32) + fu_struct_sbat_level_section_header_get_previous(st), "previous", 0, flags, error)) return FALSE; if (!fu_sbatlevel_section_add_entry(firmware, stream, sizeof(guint32) + fu_struct_sbat_level_section_header_get_latest(st), "latest", 1, flags, error)) return FALSE; return TRUE; } static GByteArray * fu_sbatlevel_section_write(FuFirmware *firmware, GError **error) { g_autoptr(FuFirmware) img_ltst = NULL; g_autoptr(FuFirmware) img_prev = NULL; g_autoptr(GByteArray) buf = fu_struct_sbat_level_section_header_new(); g_autoptr(GBytes) blob_ltst = NULL; g_autoptr(GBytes) blob_prev = NULL; /* previous */ fu_struct_sbat_level_section_header_set_previous(buf, sizeof(guint32) * 2); img_prev = fu_firmware_get_image_by_id(firmware, "previous", error); if (img_prev == NULL) return NULL; blob_prev = fu_firmware_write(img_prev, error); if (blob_prev == NULL) return NULL; fu_byte_array_append_bytes(buf, blob_prev); fu_byte_array_append_uint8(buf, 0x0); /* latest */ fu_struct_sbat_level_section_header_set_latest(buf, (sizeof(guint32) * 2) + g_bytes_get_size(blob_prev) + 1); img_ltst = fu_firmware_get_image_by_id(firmware, "latest", error); if (img_ltst == NULL) return NULL; blob_ltst = fu_firmware_write(img_ltst, error); if (blob_ltst == NULL) return NULL; fu_byte_array_append_bytes(buf, blob_ltst); fu_byte_array_append_uint8(buf, 0x0); /* success */ return g_steal_pointer(&buf); } static void fu_sbatlevel_section_init(FuSbatlevelSection *self) { fu_firmware_set_images_max(FU_FIRMWARE(self), 2); } static void fu_sbatlevel_section_class_init(FuSbatlevelSectionClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_sbatlevel_section_parse; firmware_class->write = fu_sbatlevel_section_write; } FuFirmware * fu_sbatlevel_section_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_SBATLEVEL_SECTION, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-sbatlevel-section.h000066400000000000000000000006571501337203100221610ustar00rootroot00000000000000/* * Copyright 2023 Canonical Ltd. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_SBATLEVEL_SECTION (fu_sbatlevel_section_get_type()) G_DECLARE_DERIVABLE_TYPE(FuSbatlevelSection, fu_sbatlevel_section, FU, SBATLEVEL_SECTION, FuFirmware) struct _FuSbatlevelSectionClass { FuFirmwareClass parent_class; }; FuFirmware * fu_sbatlevel_section_new(void); fwupd-2.0.10/libfwupdplugin/fu-sbatlevel-section.rs000066400000000000000000000003641501337203100223510ustar00rootroot00000000000000// Copyright 2023 Canonical Ltd // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ParseStream, New, Default)] #[repr(C, packed)] struct FuStructSbatLevelSectionHeader { version: u32le == 0x0, previous: u32le, latest: u32le, } fwupd-2.0.10/libfwupdplugin/fu-security-attr.c000066400000000000000000000100421501337203100213350ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FwupdSecurityAttr" #include "config.h" #include "fu-security-attr.h" #include "fu-version-common.h" typedef struct { FuContext *ctx; } FuSecurityAttrPrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuSecurityAttr, fu_security_attr, FWUPD_TYPE_SECURITY_ATTR) #define GET_PRIVATE(o) (fu_security_attr_get_instance_private(o)) /** * fu_security_attr_check_fwupd_version: * @attr: a #FwupdSecurityAttr * @fwupd_version: a fwupd version, e.g. `2.0.7` * * Checks if this attribute was available in a given fwupd release. * * If @fwupd_version is %NULL then expect %TRUE. * * Returns: %TRUE if the fwupd release contained this attribute * * Since: 2.0.7 **/ gboolean fu_security_attr_check_fwupd_version(FwupdSecurityAttr *attr, const gchar *fwupd_version) { g_return_val_if_fail(FWUPD_IS_SECURITY_ATTR(attr), FALSE); if (fwupd_version == NULL) return TRUE; if (fwupd_security_attr_get_fwupd_version(attr) == NULL) return TRUE; return fu_version_compare(fwupd_version, fwupd_security_attr_get_fwupd_version(attr), FWUPD_VERSION_FORMAT_UNKNOWN) >= 0; } /** * fu_security_attr_add_bios_target_value: * @attr: a #FwupdSecurityAttr * @id: a #FwupdBiosSetting ID or name * @needle: The substring of a target value * * Checks all configured possible values of an enumeration attribute and * if any match @needle then set as the target value. * * Since: 1.8.4 **/ void fu_security_attr_add_bios_target_value(FwupdSecurityAttr *attr, const gchar *id, const gchar *needle) { FuSecurityAttr *self = FU_SECURITY_ATTR(attr); FuSecurityAttrPrivate *priv = GET_PRIVATE(self); FwupdBiosSetting *bios_setting; GPtrArray *values; const gchar *current; bios_setting = fu_context_get_bios_setting(priv->ctx, id); if (bios_setting == NULL) return; current = fwupd_bios_setting_get_current_value(bios_setting); fwupd_security_attr_set_bios_setting_id(attr, fwupd_bios_setting_get_id(bios_setting)); fwupd_security_attr_set_bios_setting_current_value(attr, current); if (fwupd_bios_setting_get_kind(bios_setting) != FWUPD_BIOS_SETTING_KIND_ENUMERATION) return; if (fwupd_bios_setting_get_read_only(bios_setting)) return; values = fwupd_bios_setting_get_possible_values(bios_setting); for (guint i = 0; i < values->len; i++) { const gchar *possible = g_ptr_array_index(values, i); g_autofree gchar *lower = g_utf8_strdown(possible, -1); if (g_strrstr(lower, needle)) { fwupd_security_attr_set_bios_setting_target_value(attr, possible); /* this is built-in to the engine */ if (g_strcmp0(possible, current) != 0) { fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_CAN_FIX); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_CAN_UNDO); } return; } } } static void fu_security_attr_init(FuSecurityAttr *self) { } static void fu_security_attr_dispose(GObject *object) { FuSecurityAttr *self = FU_SECURITY_ATTR(object); FuSecurityAttrPrivate *priv = GET_PRIVATE(self); g_clear_object(&priv->ctx); G_OBJECT_CLASS(fu_security_attr_parent_class)->dispose(object); } static void fu_security_attr_class_init(FuSecurityAttrClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->dispose = fu_security_attr_dispose; } /** * fu_security_attr_new: * @ctx: a #FuContext * @appstream_id: (nullable): the AppStream component ID, e.g. `com.intel.BiosGuard` * * Creates a new #FwupdSecurityAttr with context set. * * Returns: (transfer full): a #FwupdSecurityAttr * * Since: 1.8.4 **/ FwupdSecurityAttr * fu_security_attr_new(FuContext *ctx, const gchar *appstream_id) { g_autoptr(FuSecurityAttr) self = g_object_new(FU_TYPE_SECURITY_ATTR, NULL); FuSecurityAttrPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_CONTEXT(ctx), NULL); if (appstream_id != NULL) fwupd_security_attr_set_appstream_id(FWUPD_SECURITY_ATTR(self), appstream_id); priv->ctx = g_object_ref(ctx); return FWUPD_SECURITY_ATTR(g_steal_pointer(&self)); } fwupd-2.0.10/libfwupdplugin/fu-security-attr.h000066400000000000000000000014431501337203100213470ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-context.h" #define FU_TYPE_SECURITY_ATTR (fu_security_attr_get_type()) G_DECLARE_DERIVABLE_TYPE(FuSecurityAttr, fu_security_attr, FU, SECURITY_ATTR, FwupdSecurityAttr) struct _FuSecurityAttrClass { FwupdSecurityAttrClass parent_class; }; FwupdSecurityAttr * fu_security_attr_new(FuContext *ctx, const gchar *appstream_id) G_GNUC_NON_NULL(1); void fu_security_attr_add_bios_target_value(FwupdSecurityAttr *attr, const gchar *id, const gchar *needle) G_GNUC_NON_NULL(1, 2); gboolean fu_security_attr_check_fwupd_version(FwupdSecurityAttr *attr, const gchar *fwupd_version) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-security-attrs-private.h000066400000000000000000000026061501337203100232040ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once /** * FuSecurityAttrsFlags: * @FU_SECURITY_ATTRS_FLAG_NONE: No flags set * @FU_SECURITY_ATTRS_FLAG_ADD_VERSION: Add the daemon version to the HSI string * * The flags to use when calculating an HSI version. **/ typedef enum { FU_SECURITY_ATTRS_FLAG_NONE = 0, FU_SECURITY_ATTRS_FLAG_ADD_VERSION = 1 << 0, /*< private >*/ FU_SECURITY_ATTRS_FLAG_LAST } FuSecurityAttrsFlags; #include "fu-security-attrs.h" FuSecurityAttrs * fu_security_attrs_new(void); gchar * fu_security_attrs_calculate_hsi(FuSecurityAttrs *self, const gchar *fwupd_version, FuSecurityAttrsFlags flags) G_GNUC_NON_NULL(1); void fu_security_attrs_depsolve(FuSecurityAttrs *self) G_GNUC_NON_NULL(1); GVariant * fu_security_attrs_to_variant(FuSecurityAttrs *self) G_GNUC_NON_NULL(1); GPtrArray * fu_security_attrs_get_all(FuSecurityAttrs *self, const gchar *fwupd_version) G_GNUC_NON_NULL(1); void fu_security_attrs_append_internal(FuSecurityAttrs *self, FwupdSecurityAttr *attr) G_GNUC_NON_NULL(1, 2); gboolean fu_security_attrs_is_valid(FuSecurityAttrs *self) G_GNUC_NON_NULL(1); gboolean fu_security_attrs_equal(FuSecurityAttrs *attrs1, FuSecurityAttrs *attrs2) G_GNUC_NON_NULL(1, 2); GPtrArray * fu_security_attrs_compare(FuSecurityAttrs *attrs1, FuSecurityAttrs *attrs2) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-security-attrs.c000066400000000000000000000647751501337203100215460ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuSecurityAttrs" #include "config.h" #include #include #include "fwupd-security-attr-private.h" #include "fu-security-attr.h" #include "fu-security-attrs-private.h" #include "fu-security-attrs.h" /** * FuSecurityAttrs: * * A set of Host Security ID attributes that represents the system state. */ struct _FuSecurityAttrs { GObject parent_instance; GPtrArray *attrs; }; /* probably sane to *not* make this part of the ABI */ #define FWUPD_SECURITY_ATTR_ID_DOC_URL "https://fwupd.github.io/libfwupdplugin/hsi.html" static void fu_security_attrs_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_WITH_CODE(FuSecurityAttrs, fu_security_attrs, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fu_security_attrs_codec_iface_init)) static void fu_security_attrs_finalize(GObject *obj) { FuSecurityAttrs *self = FU_SECURITY_ATTRS(obj); g_ptr_array_unref(self->attrs); G_OBJECT_CLASS(fu_security_attrs_parent_class)->finalize(obj); } static void fu_security_attrs_class_init(FuSecurityAttrsClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_security_attrs_finalize; } static void fu_security_attrs_init(FuSecurityAttrs *self) { self->attrs = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); } /** * fu_security_attrs_append_internal: * @self: a #FuSecurityAttrs * @attr: a #FwupdSecurityAttr * * Adds a #FwupdSecurityAttr to the array with no sanity checks. * * Since: 1.7.1 **/ void fu_security_attrs_append_internal(FuSecurityAttrs *self, FwupdSecurityAttr *attr) { g_return_if_fail(FU_IS_SECURITY_ATTRS(self)); g_return_if_fail(FWUPD_IS_SECURITY_ATTR(attr)); g_ptr_array_add(self->attrs, g_object_ref(attr)); } /** * fu_security_attrs_is_valid: * @self: a #FuSecurityAttrs * * Adds a #FwupdSecurityAttr to the array with no sanity checks. * * Returns: %TRUE if the collection is valid * * Since: 2.0.7 **/ gboolean fu_security_attrs_is_valid(FuSecurityAttrs *self) { g_return_val_if_fail(FU_IS_SECURITY_ATTRS(self), FALSE); return self->attrs->len > 0; } /** * fu_security_attrs_append: * @self: a #FuSecurityAttrs * @attr: a #FwupdSecurityAttr * * Adds a #FwupdSecurityAttr to the array. * * Since: 1.5.0 **/ void fu_security_attrs_append(FuSecurityAttrs *self, FwupdSecurityAttr *attr) { g_return_if_fail(FU_IS_SECURITY_ATTRS(self)); g_return_if_fail(FWUPD_IS_SECURITY_ATTR(attr)); /* sanity check */ if (fwupd_security_attr_get_plugin(attr) == NULL) { g_warning("%s has no plugin set", fwupd_security_attr_get_appstream_id(attr)); } /* sanity check, and correctly prefix the URLs with the current mirror */ if (fwupd_security_attr_get_url(attr) == NULL) { g_autofree gchar *url = NULL; url = g_strdup_printf("%s#%s", FWUPD_SECURITY_ATTR_ID_DOC_URL, fwupd_security_attr_get_appstream_id(attr)); fwupd_security_attr_set_url(attr, url); } else if (g_str_has_prefix(fwupd_security_attr_get_url(attr), "#")) { g_autofree gchar *url = NULL; url = g_strdup_printf("%s%s", FWUPD_SECURITY_ATTR_ID_DOC_URL, fwupd_security_attr_get_url(attr)); fwupd_security_attr_set_url(attr, url); } fu_security_attrs_append_internal(self, attr); } /** * fu_security_attrs_get_by_appstream_id: * @self: a #FuSecurityAttrs * @appstream_id: an ID, e.g. %FWUPD_SECURITY_ATTR_ID_ENCRYPTED_RAM * @error: (nullable): optional return location for an error * * Gets a specific #FwupdSecurityAttr from the array. * * Returns: (transfer full): a #FwupdSecurityAttr or %NULL * * Since: 1.9.6 **/ FwupdSecurityAttr * fu_security_attrs_get_by_appstream_id(FuSecurityAttrs *self, const gchar *appstream_id, GError **error) { g_return_val_if_fail(FU_IS_SECURITY_ATTRS(self), NULL); if (self->attrs->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no attributes are loaded"); return NULL; } for (guint i = 0; i < self->attrs->len; i++) { FwupdSecurityAttr *attr = g_ptr_array_index(self->attrs, i); if (g_strcmp0(fwupd_security_attr_get_appstream_id(attr), appstream_id) == 0) return g_object_ref(attr); } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no attr with ID %s", appstream_id); return NULL; } /** * fu_security_attrs_to_variant: * @self: a #FuSecurityAttrs * * Serializes the #FwupdSecurityAttr objects. * * Returns: a #GVariant or %NULL * * Since: 1.5.0 **/ GVariant * fu_security_attrs_to_variant(FuSecurityAttrs *self) { GVariantBuilder builder; g_return_val_if_fail(FU_IS_SECURITY_ATTRS(self), NULL); g_variant_builder_init(&builder, G_VARIANT_TYPE("aa{sv}")); for (guint i = 0; i < self->attrs->len; i++) { FwupdSecurityAttr *attr = g_ptr_array_index(self->attrs, i); if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_OBSOLETED)) continue; g_variant_builder_add_value( &builder, fwupd_codec_to_variant(FWUPD_CODEC(attr), FWUPD_CODEC_FLAG_NONE)); } return g_variant_new("(aa{sv})", &builder); } /** * fu_security_attrs_get_all: * @self: a #FuSecurityAttrs * @fwupd_version: (nullable): fwupd version string, e.g. `2.0.7` * * Gets all the non-obsoleted attributes in the object. * * Returns: (transfer container) (element-type FwupdSecurityAttr): attributes * * Since: 1.5.0 **/ GPtrArray * fu_security_attrs_get_all(FuSecurityAttrs *self, const gchar *fwupd_version) { g_autoptr(GPtrArray) all = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_return_val_if_fail(FU_IS_SECURITY_ATTRS(self), NULL); for (guint i = 0; i < self->attrs->len; i++) { FwupdSecurityAttr *attr = g_ptr_array_index(self->attrs, i); if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_OBSOLETED)) continue; if (!fu_security_attr_check_fwupd_version(attr, fwupd_version)) continue; g_ptr_array_add(all, g_object_ref(attr)); } return g_steal_pointer(&all); } /** * fu_security_attrs_remove_all: * @self: a #FuSecurityAttrs * * Removes all the attributes in the object. * * Since: 1.5.0 **/ void fu_security_attrs_remove_all(FuSecurityAttrs *self) { g_return_if_fail(FU_IS_SECURITY_ATTRS(self)); return g_ptr_array_set_size(self->attrs, 0); } /** * fu_security_attrs_calculate_hsi: * @self: a #FuSecurityAttrs * @fwupd_version: fwupd version, e.g. `2.0.7` * @flags: HSI attribute flags * * Calculates the HSI string from the appended attributes. * * Returns: (transfer full): a string or %NULL * * Since: 2.0.7 **/ gchar * fu_security_attrs_calculate_hsi(FuSecurityAttrs *self, const gchar *fwupd_version, FuSecurityAttrsFlags flags) { guint hsi_number = 0; FwupdSecurityAttrFlags attr_flags = FWUPD_SECURITY_ATTR_FLAG_NONE; g_autoptr(GString) str = g_string_new("HSI:"); const FwupdSecurityAttrFlags hpi_suffixes[] = { FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE, FWUPD_SECURITY_ATTR_FLAG_NONE, }; g_return_val_if_fail(FU_IS_SECURITY_ATTRS(self), NULL); /* find the highest HSI number where there are no failures and at least * one success */ for (guint j = 1; j <= FWUPD_SECURITY_ATTR_LEVEL_LAST; j++) { gboolean success_cnt = 0; gboolean failure_cnt = 0; for (guint i = 0; i < self->attrs->len; i++) { FwupdSecurityAttr *attr = g_ptr_array_index(self->attrs, i); if (fwupd_security_attr_get_level(attr) != j) continue; if (!fu_security_attr_check_fwupd_version(attr, fwupd_version)) continue; if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)) success_cnt++; else if (!fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_OBSOLETED)) failure_cnt++; } /* abort */ if (failure_cnt > 0) break; /* we matched at least one thing on this level */ if (success_cnt > 0) hsi_number = j; } /* get a logical OR of the runtime flags */ for (guint i = 0; i < self->attrs->len; i++) { FwupdSecurityAttr *attr = g_ptr_array_index(self->attrs, i); if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_OBSOLETED)) continue; if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE) && fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)) continue; if (!fu_security_attr_check_fwupd_version(attr, fwupd_version)) continue; attr_flags |= fwupd_security_attr_get_flags(attr); } g_string_append_printf(str, "%u", hsi_number); if (attr_flags & FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE) { for (guint j = 0; hpi_suffixes[j] != FWUPD_SECURITY_ATTR_FLAG_NONE; j++) { if (attr_flags & hpi_suffixes[j]) g_string_append( str, fwupd_security_attr_flag_to_suffix(hpi_suffixes[j])); } } if (flags & FU_SECURITY_ATTRS_FLAG_ADD_VERSION) { g_string_append_printf(str, " (v%d.%d.%d)", FWUPD_MAJOR_VERSION, FWUPD_MINOR_VERSION, FWUPD_MICRO_VERSION); } return g_string_free(g_steal_pointer(&str), FALSE); } static gchar * fu_security_attrs_get_sort_key(FwupdSecurityAttr *attr) { GString *str = g_string_new(NULL); /* level */ g_string_append_printf(str, "%u", fwupd_security_attr_get_level(attr)); /* success -> fail -> obsoletes */ if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)) { g_string_append(str, "0"); } else if (!fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS) && !fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_OBSOLETED)) { g_string_append(str, "1"); } else { g_string_append(str, "9"); } /* prefer name, but fallback to appstream-id for tests */ if (fwupd_security_attr_get_name(attr) != NULL) { g_string_append(str, fwupd_security_attr_get_name(attr)); } else { g_string_append(str, fwupd_security_attr_get_appstream_id(attr)); } return g_string_free(str, FALSE); } static gint fu_security_attrs_sort_cb(gconstpointer item1, gconstpointer item2) { FwupdSecurityAttr *attr1 = *((FwupdSecurityAttr **)item1); FwupdSecurityAttr *attr2 = *((FwupdSecurityAttr **)item2); g_autofree gchar *sort1 = fu_security_attrs_get_sort_key(attr1); g_autofree gchar *sort2 = fu_security_attrs_get_sort_key(attr2); return g_strcmp0(sort1, sort2); } static struct { const gchar *appstream_id; FwupdSecurityAttrLevel level; } appstream_id_level_map[] = { {FWUPD_SECURITY_ATTR_ID_AMD_ROLLBACK_PROTECTION, FWUPD_SECURITY_ATTR_LEVEL_SYSTEM_PROTECTION}, {FWUPD_SECURITY_ATTR_ID_AMD_SPI_REPLAY_PROTECTION, FWUPD_SECURITY_ATTR_LEVEL_THEORETICAL}, {FWUPD_SECURITY_ATTR_ID_AMD_SPI_WRITE_PROTECTION, FWUPD_SECURITY_ATTR_LEVEL_IMPORTANT}, {FWUPD_SECURITY_ATTR_ID_ENCRYPTED_RAM, FWUPD_SECURITY_ATTR_LEVEL_SYSTEM_PROTECTION}, {FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_ACM, FWUPD_SECURITY_ATTR_LEVEL_IMPORTANT}, {FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_ENABLED, FWUPD_SECURITY_ATTR_LEVEL_IMPORTANT}, {FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_OTP, FWUPD_SECURITY_ATTR_LEVEL_IMPORTANT}, {FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_POLICY, FWUPD_SECURITY_ATTR_LEVEL_THEORETICAL}, {FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_VERIFIED, FWUPD_SECURITY_ATTR_LEVEL_IMPORTANT}, {FWUPD_SECURITY_ATTR_ID_CET_ACTIVE, FWUPD_SECURITY_ATTR_LEVEL_THEORETICAL}, {FWUPD_SECURITY_ATTR_ID_CET_ENABLED, FWUPD_SECURITY_ATTR_LEVEL_THEORETICAL}, {FWUPD_SECURITY_ATTR_ID_INTEL_GDS, FWUPD_SECURITY_ATTR_LEVEL_IMPORTANT}, {FWUPD_SECURITY_ATTR_ID_SMAP, FWUPD_SECURITY_ATTR_LEVEL_SYSTEM_PROTECTION}, {FWUPD_SECURITY_ATTR_ID_IOMMU, FWUPD_SECURITY_ATTR_LEVEL_IMPORTANT}, {FWUPD_SECURITY_ATTR_ID_MEI_MANUFACTURING_MODE, FWUPD_SECURITY_ATTR_LEVEL_CRITICAL}, {FWUPD_SECURITY_ATTR_ID_MEI_OVERRIDE_STRAP, FWUPD_SECURITY_ATTR_LEVEL_CRITICAL}, {FWUPD_SECURITY_ATTR_ID_MEI_KEY_MANIFEST, FWUPD_SECURITY_ATTR_LEVEL_CRITICAL}, {FWUPD_SECURITY_ATTR_ID_MEI_VERSION, FWUPD_SECURITY_ATTR_LEVEL_CRITICAL}, {FWUPD_SECURITY_ATTR_ID_PLATFORM_DEBUG_ENABLED, FWUPD_SECURITY_ATTR_LEVEL_CRITICAL}, {FWUPD_SECURITY_ATTR_ID_PLATFORM_DEBUG_LOCKED, FWUPD_SECURITY_ATTR_LEVEL_IMPORTANT}, {FWUPD_SECURITY_ATTR_ID_PLATFORM_FUSED, FWUPD_SECURITY_ATTR_LEVEL_CRITICAL}, {FWUPD_SECURITY_ATTR_ID_PREBOOT_DMA_PROTECTION, FWUPD_SECURITY_ATTR_LEVEL_THEORETICAL}, {FWUPD_SECURITY_ATTR_ID_SPI_BIOSWE, FWUPD_SECURITY_ATTR_LEVEL_CRITICAL}, {FWUPD_SECURITY_ATTR_ID_SPI_BLE, FWUPD_SECURITY_ATTR_LEVEL_CRITICAL}, {FWUPD_SECURITY_ATTR_ID_SPI_DESCRIPTOR, FWUPD_SECURITY_ATTR_LEVEL_CRITICAL}, {FWUPD_SECURITY_ATTR_ID_SPI_SMM_BWP, FWUPD_SECURITY_ATTR_LEVEL_CRITICAL}, {FWUPD_SECURITY_ATTR_ID_SUPPORTED_CPU, FWUPD_SECURITY_ATTR_LEVEL_CRITICAL}, {FWUPD_SECURITY_ATTR_ID_SUSPEND_TO_IDLE, FWUPD_SECURITY_ATTR_LEVEL_THEORETICAL}, {FWUPD_SECURITY_ATTR_ID_SUSPEND_TO_RAM, FWUPD_SECURITY_ATTR_LEVEL_THEORETICAL}, {FWUPD_SECURITY_ATTR_ID_TPM_EMPTY_PCR, FWUPD_SECURITY_ATTR_LEVEL_CRITICAL}, {FWUPD_SECURITY_ATTR_ID_TPM_RECONSTRUCTION_PCR0, FWUPD_SECURITY_ATTR_LEVEL_IMPORTANT}, {FWUPD_SECURITY_ATTR_ID_TPM_VERSION_20, FWUPD_SECURITY_ATTR_LEVEL_CRITICAL}, {FWUPD_SECURITY_ATTR_ID_UEFI_PK, FWUPD_SECURITY_ATTR_LEVEL_CRITICAL}, {FWUPD_SECURITY_ATTR_ID_UEFI_SECUREBOOT, FWUPD_SECURITY_ATTR_LEVEL_CRITICAL}, {FWUPD_SECURITY_ATTR_ID_UEFI_BOOTSERVICE_VARS, FWUPD_SECURITY_ATTR_LEVEL_CRITICAL}, {FWUPD_SECURITY_ATTR_ID_BIOS_ROLLBACK_PROTECTION, FWUPD_SECURITY_ATTR_LEVEL_IMPORTANT}, {FWUPD_SECURITY_ATTR_ID_BIOS_CAPSULE_UPDATES, FWUPD_SECURITY_ATTR_LEVEL_CRITICAL}, {FWUPD_SECURITY_ATTR_ID_AMD_SMM_LOCKED, FWUPD_SECURITY_ATTR_LEVEL_CRITICAL}, {FWUPD_SECURITY_ATTR_ID_UEFI_MEMORY_PROTECTION, FWUPD_SECURITY_ATTR_LEVEL_IMPORTANT}, {FWUPD_SECURITY_ATTR_ID_UEFI_DB, FWUPD_SECURITY_ATTR_LEVEL_IMPORTANT}, }; static void fu_security_attrs_ensure_level(FwupdSecurityAttr *attr) { const gchar *appstream_id = fwupd_security_attr_get_appstream_id(attr); /* already set */ if (fwupd_security_attr_get_level(attr) != FWUPD_SECURITY_ATTR_LEVEL_NONE) return; /* not required */ if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE)) return; /* map ID to level in one place */ for (guint i = 0; i < G_N_ELEMENTS(appstream_id_level_map); i++) { if (g_strcmp0(appstream_id, appstream_id_level_map[i].appstream_id) == 0) { fwupd_security_attr_set_level(attr, appstream_id_level_map[i].level); return; } } /* somebody forgot to add to the level map... */ g_warning("cannot map %s to a HSI level, assuming critical", appstream_id); fwupd_security_attr_set_level(attr, FWUPD_SECURITY_ATTR_LEVEL_CRITICAL); } static struct { const gchar *appstream_id; const gchar *fwupd_version; } appstream_id_version_map[] = { {FWUPD_SECURITY_ATTR_ID_AMD_ROLLBACK_PROTECTION, "1.8.0"}, {FWUPD_SECURITY_ATTR_ID_AMD_SMM_LOCKED, "2.0.2"}, {FWUPD_SECURITY_ATTR_ID_AMD_SPI_REPLAY_PROTECTION, "1.8.0"}, {FWUPD_SECURITY_ATTR_ID_AMD_SPI_WRITE_PROTECTION, "1.8.0"}, {FWUPD_SECURITY_ATTR_ID_BIOS_CAPSULE_UPDATES, "1.9.6"}, {FWUPD_SECURITY_ATTR_ID_BIOS_ROLLBACK_PROTECTION, "1.8.8"}, {FWUPD_SECURITY_ATTR_ID_CET_ACTIVE, "2.0.0"}, {FWUPD_SECURITY_ATTR_ID_CET_ENABLED, "2.0.0"}, {FWUPD_SECURITY_ATTR_ID_ENCRYPTED_RAM, "1.5.0"}, {FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_ACM, "1.5.0"}, {FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_ENABLED, "1.5.0"}, {FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_OTP, "1.5.0"}, {FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_POLICY, "1.5.0"}, {FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_VERIFIED, "1.5.0"}, {FWUPD_SECURITY_ATTR_ID_INTEL_GDS, "1.9.4"}, {FWUPD_SECURITY_ATTR_ID_IOMMU, "1.5.0"}, {FWUPD_SECURITY_ATTR_ID_KERNEL_LOCKDOWN, "1.5.0"}, {FWUPD_SECURITY_ATTR_ID_KERNEL_TAINTED, "1.5.0"}, {FWUPD_SECURITY_ATTR_ID_MEI_KEY_MANIFEST, "1.8.7"}, {FWUPD_SECURITY_ATTR_ID_MEI_MANUFACTURING_MODE, "1.5.0"}, {FWUPD_SECURITY_ATTR_ID_MEI_OVERRIDE_STRAP, "1.5.0"}, {FWUPD_SECURITY_ATTR_ID_MEI_VERSION, "1.5.0"}, {FWUPD_SECURITY_ATTR_ID_PLATFORM_DEBUG_ENABLED, "1.5.0"}, {FWUPD_SECURITY_ATTR_ID_PLATFORM_DEBUG_LOCKED, "1.8.0"}, {FWUPD_SECURITY_ATTR_ID_PLATFORM_FUSED, "1.8.0"}, {FWUPD_SECURITY_ATTR_ID_PREBOOT_DMA_PROTECTION, "1.8.0"}, {FWUPD_SECURITY_ATTR_ID_SMAP, "2.0.0"}, {FWUPD_SECURITY_ATTR_ID_SPI_BIOSWE, "1.5.0"}, {FWUPD_SECURITY_ATTR_ID_SPI_BLE, "1.5.0"}, {FWUPD_SECURITY_ATTR_ID_SPI_DESCRIPTOR, "1.6.0"}, {FWUPD_SECURITY_ATTR_ID_SPI_SMM_BWP, "1.5.0"}, {FWUPD_SECURITY_ATTR_ID_SUPPORTED_CPU, "1.8.0"}, {FWUPD_SECURITY_ATTR_ID_SUSPEND_TO_IDLE, "1.5.0"}, {FWUPD_SECURITY_ATTR_ID_SUSPEND_TO_RAM, "1.5.0"}, {FWUPD_SECURITY_ATTR_ID_TPM_EMPTY_PCR, "1.7.2"}, {FWUPD_SECURITY_ATTR_ID_TPM_RECONSTRUCTION_PCR0, "1.5.0"}, {FWUPD_SECURITY_ATTR_ID_TPM_VERSION_20, "1.5.0"}, {FWUPD_SECURITY_ATTR_ID_UEFI_BOOTSERVICE_VARS, "1.9.3"}, {FWUPD_SECURITY_ATTR_ID_UEFI_DB, "2.0.8"}, {FWUPD_SECURITY_ATTR_ID_UEFI_MEMORY_PROTECTION, "2.0.7"}, {FWUPD_SECURITY_ATTR_ID_UEFI_PK, "1.5.0"}, {FWUPD_SECURITY_ATTR_ID_UEFI_SECUREBOOT, "1.5.0"}, }; static void fu_security_attrs_ensure_fwupd_version(FwupdSecurityAttr *attr) { const gchar *appstream_id = fwupd_security_attr_get_appstream_id(attr); /* already set */ if (fwupd_security_attr_get_fwupd_version(attr) != NULL) return; /* not required */ if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE)) return; /* map ID to fwupd version in one place */ for (guint i = 0; i < G_N_ELEMENTS(appstream_id_version_map); i++) { if (g_strcmp0(appstream_id, appstream_id_version_map[i].appstream_id) == 0) { fwupd_security_attr_set_fwupd_version( attr, appstream_id_version_map[i].fwupd_version); return; } } /* somebody forgot to add to the level map... */ g_warning("cannot map %s to a fwupd version", appstream_id); } /** * fu_security_attrs_depsolve: * @self: a #FuSecurityAttrs * * Marks any attributes with %FWUPD_SECURITY_ATTR_FLAG_OBSOLETED that have been * defined as obsoleted by other attributes. * * It is only required to call this function once, and should be done when all * attributes have been added. This will also sort the attrs. * * Since: 1.5.0 **/ void fu_security_attrs_depsolve(FuSecurityAttrs *self) { g_return_if_fail(FU_IS_SECURITY_ATTRS(self)); /* assign HSI levels if not already done */ for (guint i = 0; i < self->attrs->len; i++) { FwupdSecurityAttr *attr = g_ptr_array_index(self->attrs, i); fu_security_attrs_ensure_level(attr); fu_security_attrs_ensure_fwupd_version(attr); } /* set flat where required */ for (guint i = 0; i < self->attrs->len; i++) { FwupdSecurityAttr *attr = g_ptr_array_index(self->attrs, i); const gchar *attr_id = fwupd_security_attr_get_appstream_id(attr); const gchar *attr_plugin = fwupd_security_attr_get_plugin(attr); GPtrArray *obsoletes = fwupd_security_attr_get_obsoletes(attr); for (guint j = 0; j < self->attrs->len; j++) { FwupdSecurityAttr *attr_tmp = g_ptr_array_index(self->attrs, j); const gchar *attr_tmp_id = fwupd_security_attr_get_appstream_id(attr_tmp); const gchar *attr_tmp_plugin = fwupd_security_attr_get_plugin(attr_tmp); /* skip self */ if (g_strcmp0(attr_plugin, attr_tmp_plugin) == 0 && g_strcmp0(attr_id, attr_tmp_id) == 0) continue; /* add duplicate (negative) attributes when obsolete not explicitly set */ if (obsoletes->len == 0) { if (g_strcmp0(attr_id, attr_tmp_id) != 0) continue; if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)) continue; if (fwupd_security_attr_has_flag(attr_tmp, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)) continue; if (fwupd_security_attr_has_obsolete(attr, attr_id)) continue; if (fwupd_security_attr_has_obsolete(attr_tmp, attr_id)) continue; g_debug("duplicate security attr %s from plugin %s implicitly " "obsoleted by plugin %s", attr_id, attr_plugin, attr_tmp_plugin); fwupd_security_attr_add_obsolete(attr, attr_id); } /* walk all the obsoletes for matches appstream ID or plugin */ for (guint k = 0; k < obsoletes->len; k++) { const gchar *obsolete = g_ptr_array_index(obsoletes, k); if (g_strcmp0(attr_tmp_id, obsolete) == 0 || g_strcmp0(attr_tmp_plugin, obsolete) == 0) { g_debug("security attr %s:%s obsoleted by %s:%s", attr_tmp_id, attr_tmp_plugin, attr_id, attr_plugin); fwupd_security_attr_add_flag( attr_tmp, FWUPD_SECURITY_ATTR_FLAG_OBSOLETED); } } } } /* sort */ g_ptr_array_sort(self->attrs, fu_security_attrs_sort_cb); } static void fu_security_attrs_add_json(FwupdCodec *codec, JsonBuilder *builder, FwupdCodecFlags flags) { FuSecurityAttrs *self = FU_SECURITY_ATTRS(codec); g_autoptr(GPtrArray) items = NULL; json_builder_set_member_name(builder, "SecurityAttributes"); json_builder_begin_array(builder); items = fu_security_attrs_get_all(self, NULL); for (guint i = 0; i < items->len; i++) { FwupdSecurityAttr *attr = g_ptr_array_index(items, i); guint64 created = fwupd_security_attr_get_created(attr); if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_OBSOLETED)) continue; fwupd_security_attr_set_created(attr, 0); json_builder_begin_object(builder); fwupd_codec_to_json(FWUPD_CODEC(attr), builder, FWUPD_CODEC_FLAG_NONE); json_builder_end_object(builder); fwupd_security_attr_set_created(attr, created); } json_builder_end_array(builder); } static gboolean fu_security_attrs_from_json(FwupdCodec *codec, JsonNode *json_node, GError **error) { FuSecurityAttrs *self = FU_SECURITY_ATTRS(codec); JsonArray *array; JsonObject *obj; /* sanity check */ if (!JSON_NODE_HOLDS_OBJECT(json_node)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not JSON object"); return FALSE; } obj = json_node_get_object(json_node); /* this has to exist */ if (!json_object_has_member(obj, "SecurityAttributes")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no SecurityAttributes property in object"); return FALSE; } array = json_object_get_array_member(obj, "SecurityAttributes"); for (guint i = 0; i < json_array_get_length(array); i++) { JsonNode *node_tmp = json_array_get_element(array, i); g_autoptr(FwupdSecurityAttr) attr = fwupd_security_attr_new(NULL); if (!fwupd_codec_from_json(FWUPD_CODEC(attr), node_tmp, error)) return FALSE; if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_OBSOLETED)) continue; fu_security_attrs_append(self, attr); } /* success */ return TRUE; } static void fu_security_attrs_codec_iface_init(FwupdCodecInterface *iface) { iface->add_json = fu_security_attrs_add_json; iface->from_json = fu_security_attrs_from_json; } /** * fu_security_attrs_compare: * @attrs1: a #FuSecurityAttrs * @attrs2: another #FuSecurityAttrs, perhaps newer in some way * * Compares the two objects, returning the differences. * * If the two sets of attrs are considered the same then an empty array is returned. * Only the AppStream ID results are compared, extra metadata is ignored. * * Returns: (element-type FwupdSecurityAttr) (transfer container): differences * * Since: 1.9.2 */ GPtrArray * fu_security_attrs_compare(FuSecurityAttrs *attrs1, FuSecurityAttrs *attrs2) { g_autoptr(GHashTable) hash1 = g_hash_table_new(g_str_hash, g_str_equal); g_autoptr(GHashTable) hash2 = g_hash_table_new(g_str_hash, g_str_equal); g_autoptr(GPtrArray) array1 = fu_security_attrs_get_all(attrs1, NULL); g_autoptr(GPtrArray) array2 = fu_security_attrs_get_all(attrs2, NULL); g_autoptr(GPtrArray) results = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_return_val_if_fail(FU_IS_SECURITY_ATTRS(attrs1), NULL); g_return_val_if_fail(FU_IS_SECURITY_ATTRS(attrs2), NULL); /* create hash tables of appstream-id -> FwupdSecurityAttr */ for (guint i = 0; i < array1->len; i++) { FwupdSecurityAttr *attr1 = g_ptr_array_index(array1, i); g_hash_table_insert(hash1, (gpointer)fwupd_security_attr_get_appstream_id(attr1), (gpointer)attr1); } for (guint i = 0; i < array2->len; i++) { FwupdSecurityAttr *attr2 = g_ptr_array_index(array2, i); g_hash_table_insert(hash2, (gpointer)fwupd_security_attr_get_appstream_id(attr2), (gpointer)attr2); } /* present in attrs2, not present in attrs1 */ for (guint i = 0; i < array2->len; i++) { FwupdSecurityAttr *attr1; FwupdSecurityAttr *attr2 = g_ptr_array_index(array2, i); attr1 = g_hash_table_lookup(hash1, fwupd_security_attr_get_appstream_id(attr2)); if (attr1 == NULL) { g_autoptr(FwupdSecurityAttr) attr = fwupd_security_attr_copy(attr2); g_ptr_array_add(results, g_steal_pointer(&attr)); continue; } } /* present in attrs1, not present in attrs2 */ for (guint i = 0; i < array1->len; i++) { FwupdSecurityAttr *attr1 = g_ptr_array_index(array1, i); FwupdSecurityAttr *attr2; attr2 = g_hash_table_lookup(hash2, fwupd_security_attr_get_appstream_id(attr1)); if (attr2 == NULL) { g_autoptr(FwupdSecurityAttr) attr = fwupd_security_attr_copy(attr1); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_UNKNOWN); fwupd_security_attr_set_result_fallback( attr, /* flip these around */ fwupd_security_attr_get_result(attr1)); g_ptr_array_add(results, g_steal_pointer(&attr)); continue; } } /* find any attributes that differ */ for (guint i = 0; i < array2->len; i++) { FwupdSecurityAttr *attr1; FwupdSecurityAttr *attr2 = g_ptr_array_index(array2, i); attr1 = g_hash_table_lookup(hash1, fwupd_security_attr_get_appstream_id(attr2)); if (attr1 == NULL) continue; /* result of specific attr differed */ if (fwupd_security_attr_get_result(attr1) != fwupd_security_attr_get_result(attr2)) { g_autoptr(FwupdSecurityAttr) attr = fwupd_security_attr_copy(attr1); fwupd_security_attr_set_result(attr, fwupd_security_attr_get_result(attr2)); fwupd_security_attr_set_result_fallback( attr, fwupd_security_attr_get_result(attr1)); fwupd_security_attr_set_flags(attr, fwupd_security_attr_get_flags(attr2)); g_ptr_array_add(results, g_steal_pointer(&attr)); } } /* success */ return g_steal_pointer(&results); } /** * fu_security_attrs_equal: * @attrs1: a #FuSecurityAttrs * @attrs2: another #FuSecurityAttrs * * Tests the objects for equality. Only the AppStream ID results are compared, extra metadata * is ignored. * * Returns: %TRUE if the set of attrs can be considered equal * * Since: 1.9.2 */ gboolean fu_security_attrs_equal(FuSecurityAttrs *attrs1, FuSecurityAttrs *attrs2) { g_autoptr(GPtrArray) compare = fu_security_attrs_compare(attrs1, attrs2); return compare->len == 0; } /** * fu_security_attrs_new: * * Returns: a security attribute * * Since: 1.5.0 **/ FuSecurityAttrs * fu_security_attrs_new(void) { return g_object_new(FU_TYPE_SECURITY_ATTRS, NULL); } fwupd-2.0.10/libfwupdplugin/fu-security-attrs.h000066400000000000000000000012301501337203100215240ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_SECURITY_ATTRS (fu_security_attrs_get_type()) G_DECLARE_FINAL_TYPE(FuSecurityAttrs, fu_security_attrs, FU, SECURITY_ATTRS, GObject) void fu_security_attrs_append(FuSecurityAttrs *self, FwupdSecurityAttr *attr) G_GNUC_NON_NULL(1, 2); void fu_security_attrs_remove_all(FuSecurityAttrs *self) G_GNUC_NON_NULL(1); FwupdSecurityAttr * fu_security_attrs_get_by_appstream_id(FuSecurityAttrs *self, const gchar *appstream_id, GError **error) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-self-test.c000066400000000000000000007313221501337203100204370ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #define G_LOG_DOMAIN "FuSelfTest" #include #include #include #include "fwupd-enums-private.h" #include "fwupd-security-attr-private.h" #include "fu-backend-private.h" #include "fu-bios-settings-private.h" #include "fu-cab-firmware-private.h" #include "fu-common-private.h" #include "fu-config-private.h" #include "fu-context-private.h" #include "fu-coswid-firmware.h" #include "fu-device-event-private.h" #include "fu-device-private.h" #include "fu-device-progress.h" #include "fu-dummy-efivars.h" #include "fu-efi-lz77-decompressor.h" #include "fu-efi-x509-signature-private.h" #include "fu-efivars-private.h" #include "fu-kernel-search-path-private.h" #include "fu-lzma-common.h" #include "fu-plugin-private.h" #include "fu-progress-private.h" #include "fu-security-attrs-private.h" #include "fu-self-test-struct.h" #include "fu-smbios-private.h" #include "fu-test-device.h" #include "fu-udev-device-private.h" #include "fu-volume-private.h" static GMainLoop *_test_loop = NULL; static guint _test_loop_timeout_id = 0; static gboolean fu_test_hang_check_cb(gpointer user_data) { g_main_loop_quit(_test_loop); _test_loop_timeout_id = 0; return G_SOURCE_REMOVE; } static void fu_test_loop_run_with_timeout(guint timeout_ms) { g_assert_cmpint(_test_loop_timeout_id, ==, 0); g_assert_null(_test_loop); _test_loop = g_main_loop_new(NULL, FALSE); _test_loop_timeout_id = g_timeout_add(timeout_ms, fu_test_hang_check_cb, NULL); g_main_loop_run(_test_loop); } static void fu_test_loop_quit(void) { if (_test_loop_timeout_id > 0) { g_source_remove(_test_loop_timeout_id); _test_loop_timeout_id = 0; } if (_test_loop != NULL) { g_main_loop_quit(_test_loop); g_main_loop_unref(_test_loop); _test_loop = NULL; } } static void fu_msgpack_lookup_func(void) { g_autoptr(FuMsgpackItem) item1 = NULL; g_autoptr(FuMsgpackItem) item2 = NULL; g_autoptr(FuMsgpackItem) item3 = NULL; g_autoptr(FuMsgpackItem) item4 = NULL; g_autoptr(FuMsgpackItem) item5 = NULL; g_autoptr(FuMsgpackItem) item6 = NULL; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) items = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_autoptr(GPtrArray) items_invalid = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); /* empty */ item1 = fu_msgpack_map_lookup(items, 0, "foo", &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA); g_assert_null(item1); g_clear_error(&error); /* map of stuff */ g_ptr_array_add(items, fu_msgpack_item_new_string("offset")); g_ptr_array_add(items, fu_msgpack_item_new_map(2)); g_ptr_array_add(items, fu_msgpack_item_new_string("fixint")); g_ptr_array_add(items, fu_msgpack_item_new_integer(6)); g_ptr_array_add(items, fu_msgpack_item_new_string("uint8")); /* ...value is missing here */ /* not a map */ item2 = fu_msgpack_map_lookup(items, 0, "fixint", &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_null(item2); g_clear_error(&error); /* items too small */ item3 = fu_msgpack_map_lookup(items, 1, "fixint", &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA); g_assert_null(item3); g_clear_error(&error); /* add the missing value */ g_ptr_array_add(items, fu_msgpack_item_new_integer(256)); /* get valid */ item4 = fu_msgpack_map_lookup(items, 1, "fixint", &error); g_assert_no_error(error); g_assert_nonnull(item4); /* not found */ item5 = fu_msgpack_map_lookup(items, 1, "not-going-to-exist", &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND); g_assert_null(item5); g_clear_error(&error); /* not string key */ g_ptr_array_add(items_invalid, fu_msgpack_item_new_map(1)); g_ptr_array_add(items_invalid, fu_msgpack_item_new_integer(12)); g_ptr_array_add(items_invalid, fu_msgpack_item_new_integer(34)); /* get valid */ item6 = fu_msgpack_map_lookup(items_invalid, 0, "fixint", &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA); g_assert_null(item6); g_clear_error(&error); } static void fu_msgpack_binary_stream_func(void) { const gchar data[] = "hello"; g_autoptr(GByteArray) buf = NULL; g_autoptr(GBytes) blob = g_bytes_new(data, sizeof(data)); g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = G_INPUT_STREAM(g_memory_input_stream_new_from_bytes(blob)); g_autoptr(GPtrArray) items = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_ptr_array_add(items, fu_msgpack_item_new_binary_stream(stream)); buf = fu_msgpack_write(items, &error); g_assert_no_error(error); g_assert_nonnull(buf); fu_dump_raw(G_LOG_DOMAIN, "foo", buf->data, buf->len); g_assert_cmpint(buf->len, ==, 8); g_assert_cmpuint(buf->data[0], ==, FU_MSGPACK_CMD_BIN8); g_assert_cmpuint(buf->data[1], ==, sizeof(data)); g_assert_cmpuint(buf->data[2], ==, 'h'); g_assert_cmpuint(buf->data[3], ==, 'e'); g_assert_cmpuint(buf->data[4], ==, 'l'); g_assert_cmpuint(buf->data[5], ==, 'l'); g_assert_cmpuint(buf->data[6], ==, 'o'); g_assert_cmpuint(buf->data[7], ==, '\0'); } static void fu_msgpack_parse_binary_func(void) { // 64 bit float 100.0099 const guchar data[] = {0xCB, 0x40, 0x59, 0x00, 0xA2, 0x33, 0x9C, 0x0E, 0xBF}; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) items = NULL; g_byte_array_append(buf, data, sizeof(data)); items = fu_msgpack_parse(buf, &error); g_assert_no_error(error); g_assert_nonnull(items); g_assert_cmpint(items->len, ==, 1); g_assert_cmpfloat_with_epsilon(fu_msgpack_item_get_float(g_ptr_array_index(items, 0)), 100.0099, 0.00001); } static void fu_msgpack_func(void) { g_autoptr(GByteArray) buf1 = NULL; g_autoptr(GByteArray) buf2 = NULL; g_autoptr(GByteArray) buf_in = g_byte_array_new(); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) items_new = NULL; g_autoptr(GPtrArray) items = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); FuMsgpackItemKind kinds[] = { FU_MSGPACK_ITEM_KIND_MAP, FU_MSGPACK_ITEM_KIND_STRING, FU_MSGPACK_ITEM_KIND_INTEGER, FU_MSGPACK_ITEM_KIND_STRING, FU_MSGPACK_ITEM_KIND_INTEGER, FU_MSGPACK_ITEM_KIND_STRING, FU_MSGPACK_ITEM_KIND_FLOAT, FU_MSGPACK_ITEM_KIND_STRING, FU_MSGPACK_ITEM_KIND_ARRAY, FU_MSGPACK_ITEM_KIND_BINARY, }; /* empty */ buf1 = fu_msgpack_write(items, &error); g_assert_no_error(error); g_assert_nonnull(buf1); g_assert_cmpint(buf1->len, ==, 0); /* prepare */ fu_byte_array_append_uint24(buf_in, 0x1234, G_LITTLE_ENDIAN); /* map of stuff */ g_ptr_array_add(items, fu_msgpack_item_new_map(4)); g_ptr_array_add(items, fu_msgpack_item_new_string("fixint")); g_ptr_array_add(items, fu_msgpack_item_new_integer(6)); g_ptr_array_add(items, fu_msgpack_item_new_string("uint8")); g_ptr_array_add(items, fu_msgpack_item_new_integer(256)); g_ptr_array_add(items, fu_msgpack_item_new_string("float")); g_ptr_array_add(items, fu_msgpack_item_new_float(1.0)); g_ptr_array_add(items, fu_msgpack_item_new_string("array-of-data")); g_ptr_array_add(items, fu_msgpack_item_new_array(1)); g_ptr_array_add(items, fu_msgpack_item_new_binary(buf_in)); buf2 = fu_msgpack_write(items, &error); g_assert_no_error(error); g_assert_nonnull(buf2); g_assert_cmpint(buf2->len, ==, 53); /* parse it back */ items_new = fu_msgpack_parse(buf2, &error); g_assert_no_error(error); g_assert_nonnull(items_new); g_assert_cmpint(items_new->len, ==, 10); for (guint i = 0; i < G_N_ELEMENTS(kinds); i++) { FuMsgpackItem *item = g_ptr_array_index(items_new, i); g_assert_cmpint(fu_msgpack_item_get_kind(item), ==, kinds[i]); } g_assert_cmpint(fu_msgpack_item_get_map(g_ptr_array_index(items_new, 0)), ==, 4); g_assert_cmpint(fu_msgpack_item_get_integer(g_ptr_array_index(items_new, 2)), ==, 6); g_assert_cmpint(fu_msgpack_item_get_integer(g_ptr_array_index(items_new, 4)), ==, 256); g_assert_cmpfloat_with_epsilon(fu_msgpack_item_get_float(g_ptr_array_index(items_new, 6)), 1.0, 0.00001); g_assert_cmpint(fu_msgpack_item_get_array(g_ptr_array_index(items_new, 8)), ==, 1); } static void fu_archive_invalid_func(void) { g_autofree gchar *filename = NULL; g_autoptr(FuArchive) archive = NULL; g_autoptr(GBytes) data = NULL; g_autoptr(GError) error = NULL; #ifndef HAVE_LIBARCHIVE g_test_skip("no libarchive support"); return; #endif filename = g_test_build_filename(G_TEST_DIST, "tests", "metadata.xml", NULL); data = fu_bytes_get_contents(filename, &error); g_assert_no_error(error); g_assert_nonnull(data); archive = fu_archive_new(data, FU_ARCHIVE_FLAG_NONE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_null(archive); } static void fu_archive_cab_func(void) { g_autofree gchar *checksum1 = NULL; g_autofree gchar *checksum2 = NULL; g_autofree gchar *filename = NULL; g_autoptr(FuArchive) archive = NULL; g_autoptr(GBytes) data = NULL; g_autoptr(GBytes) data_tmp1 = NULL; g_autoptr(GBytes) data_tmp2 = NULL; g_autoptr(GBytes) data_tmp3 = NULL; g_autoptr(GError) error = NULL; #ifndef HAVE_LIBARCHIVE g_test_skip("no libarchive support"); return; #endif filename = g_test_build_filename(G_TEST_BUILT, "tests", "colorhug", "colorhug-als-3.0.2.cab", NULL); data = fu_bytes_get_contents(filename, &error); g_assert_no_error(error); g_assert_nonnull(data); archive = fu_archive_new(data, FU_ARCHIVE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_nonnull(archive); data_tmp1 = fu_archive_lookup_by_fn(archive, "firmware.metainfo.xml", &error); g_assert_no_error(error); g_assert_nonnull(data_tmp1); checksum1 = g_compute_checksum_for_bytes(G_CHECKSUM_SHA1, data_tmp1); g_assert_cmpstr(checksum1, ==, "f62ee340c27bbb80229c3dd3cb2e78bddfc82d4f"); data_tmp2 = fu_archive_lookup_by_fn(archive, "firmware.txt", &error); g_assert_no_error(error); g_assert_nonnull(data_tmp2); checksum2 = g_compute_checksum_for_bytes(G_CHECKSUM_SHA1, data_tmp2); g_assert_cmpstr(checksum2, ==, "22596363b3de40b06f981fb85d82312e8c0ed511"); data_tmp3 = fu_archive_lookup_by_fn(archive, "NOTGOINGTOEXIST.xml", &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND); g_assert_null(data_tmp3); } static void fu_volume_gpt_type_func(void) { g_assert_cmpstr(fu_volume_kind_convert_to_gpt("0xef"), ==, "c12a7328-f81f-11d2-ba4b-00a0c93ec93b"); g_assert_cmpstr(fu_volume_kind_convert_to_gpt("0x0b"), ==, "ebd0a0a2-b9e5-4433-87c0-68b6b72699c7"); g_assert_cmpstr(fu_volume_kind_convert_to_gpt("fat32lba"), ==, "ebd0a0a2-b9e5-4433-87c0-68b6b72699c7"); g_assert_cmpstr(fu_volume_kind_convert_to_gpt("0x00"), ==, "0x00"); } static void fu_common_align_up_func(void) { g_assert_cmpint(fu_common_align_up(0, 0), ==, 0); g_assert_cmpint(fu_common_align_up(5, 0), ==, 5); g_assert_cmpint(fu_common_align_up(5, 3), ==, 8); g_assert_cmpint(fu_common_align_up(1023, 10), ==, 1024); g_assert_cmpint(fu_common_align_up(1024, 10), ==, 1024); g_assert_cmpint(fu_common_align_up(G_MAXSIZE - 1, 10), ==, G_MAXSIZE); } static void fu_common_bitwise_func(void) { guint64 val = 0; g_assert_true(FU_BIT_IS_CLEAR(val, 1)); g_assert_true(FU_BIT_IS_CLEAR(val, 63)); g_assert_false(FU_BIT_IS_SET(val, 1)); g_assert_false(FU_BIT_IS_SET(val, 63)); FU_BIT_SET(val, 1); FU_BIT_SET(val, 63); g_assert_true(FU_BIT_IS_SET(val, 1)); g_assert_true(FU_BIT_IS_SET(val, 63)); g_assert_cmpint(val, ==, 0x8000000000000002ull); FU_BIT_CLEAR(val, 1); g_assert_cmpint(val, ==, 0x8000000000000000ull); FU_BIT_CLEAR(val, 63); g_assert_cmpint(val, ==, 0); } static void fu_common_byte_array_func(void) { g_autofree gchar *str = NULL; g_autoptr(GByteArray) array = g_byte_array_new(); g_autoptr(GByteArray) array2 = NULL; g_autoptr(GByteArray) array3 = NULL; g_autoptr(GError) error = NULL; fu_byte_array_append_uint8(array, (guint8)'h'); fu_byte_array_append_uint8(array, (guint8)'e'); fu_byte_array_append_uint8(array, (guint8)'l'); fu_byte_array_append_uint8(array, (guint8)'l'); fu_byte_array_append_uint8(array, (guint8)'o'); g_assert_cmpint(array->len, ==, 5); g_assert_cmpint(memcmp(array->data, "hello", array->len), ==, 0); fu_byte_array_set_size(array, 10, 0x00); g_assert_cmpint(array->len, ==, 10); g_assert_cmpint(memcmp(array->data, "hello\0\0\0\0\0", array->len), ==, 0); str = fu_byte_array_to_string(array); g_assert_cmpstr(str, ==, "68656c6c6f0000000000"); array2 = fu_byte_array_from_string(str, &error); g_assert_no_error(error); g_assert_nonnull(array2); g_assert_cmpint(array2->len, ==, 10); g_assert_cmpint(memcmp(array2->data, "hello\0\0\0\0\0", array2->len), ==, 0); array3 = fu_byte_array_from_string("ZZZ", &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA); g_assert_null(array3); } static void fu_common_crc_func(void) { guint8 buf[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09}; g_assert_cmpint(fu_crc8(FU_CRC_KIND_B8_STANDARD, buf, sizeof(buf)), ==, (guint8)~0x7A); g_assert_cmpint(fu_crc16(FU_CRC_KIND_B16_USB, buf, sizeof(buf)), ==, 0x4DF1); g_assert_cmpint(fu_crc_misr16(0, buf, (sizeof(buf) / 2) * 2), ==, 0x40D); g_assert_cmpint(fu_crc_misr16(0xFFFF, buf, (sizeof(buf) / 2) * 2), ==, 0xFBFA); /* all the CRC32 variants, verified using https://crccalc.com/?method=CRC-32 */ g_assert_cmpint(fu_crc32(FU_CRC_KIND_B32_STANDARD, buf, sizeof(buf)), ==, 0x40EFAB9E); g_assert_cmpint(fu_crc32(FU_CRC_KIND_B32_BZIP2, buf, sizeof(buf)), ==, 0x89AE7A5C); g_assert_cmpint(fu_crc32(FU_CRC_KIND_B32_JAMCRC, buf, sizeof(buf)), ==, 0xBF105461); g_assert_cmpint(fu_crc32(FU_CRC_KIND_B32_MPEG2, buf, sizeof(buf)), ==, 0x765185A3); g_assert_cmpint(fu_crc32(FU_CRC_KIND_B32_POSIX, buf, sizeof(buf)), ==, 0x037915C4); g_assert_cmpint(fu_crc32(FU_CRC_KIND_B32_SATA, buf, sizeof(buf)), ==, 0xBA55CCAC); g_assert_cmpint(fu_crc32(FU_CRC_KIND_B32_XFER, buf, sizeof(buf)), ==, 0x868E70FC); g_assert_cmpint(fu_crc32(FU_CRC_KIND_B32_C, buf, sizeof(buf)), ==, 0x5A14B9F9); g_assert_cmpint(fu_crc32(FU_CRC_KIND_B32_D, buf, sizeof(buf)), ==, 0x68AD8D3C); g_assert_cmpint(fu_crc32(FU_CRC_KIND_B32_Q, buf, sizeof(buf)), ==, 0xE955C875); } static void fu_common_guid_func(void) { gboolean ret; guint8 buf[16] = {0}; ret = fu_common_guid_is_plausible(buf); g_assert_false(ret); buf[0] = 0x5; ret = fu_common_guid_is_plausible(buf); g_assert_false(ret); for (guint i = 0; i < sizeof(buf); i++) buf[i] = 0xFF; ret = fu_common_guid_is_plausible(buf); g_assert_true(ret); } static void fu_string_append_func(void) { g_autoptr(GString) str = g_string_new(NULL); fwupd_codec_string_append(str, 0, "hdr", ""); fwupd_codec_string_append(str, 0, "key", "value"); fwupd_codec_string_append(str, 0, "key1", "value1"); fwupd_codec_string_append(str, 1, "key2", "value2"); fwupd_codec_string_append(str, 1, "", "value2"); fwupd_codec_string_append(str, 2, "key3", "value3"); g_assert_cmpstr(str->str, ==, "hdr:\n" "key: value\n" "key1: value1\n" " key2: value2\n" " value2\n" " key3: value3\n"); } static void fu_version_guess_format_func(void) { g_assert_cmpint(fu_version_guess_format(NULL), ==, FWUPD_VERSION_FORMAT_UNKNOWN); g_assert_cmpint(fu_version_guess_format(""), ==, FWUPD_VERSION_FORMAT_UNKNOWN); g_assert_cmpint(fu_version_guess_format("1234ac"), ==, FWUPD_VERSION_FORMAT_PLAIN); g_assert_cmpint(fu_version_guess_format("1.2"), ==, FWUPD_VERSION_FORMAT_PAIR); g_assert_cmpint(fu_version_guess_format("1.2.3"), ==, FWUPD_VERSION_FORMAT_TRIPLET); g_assert_cmpint(fu_version_guess_format("1.2.3.4"), ==, FWUPD_VERSION_FORMAT_QUAD); g_assert_cmpint(fu_version_guess_format("1.2.3.4.5"), ==, FWUPD_VERSION_FORMAT_UNKNOWN); g_assert_cmpint(fu_version_guess_format("1a.2b.3"), ==, FWUPD_VERSION_FORMAT_PLAIN); g_assert_cmpint(fu_version_guess_format("1"), ==, FWUPD_VERSION_FORMAT_NUMBER); g_assert_cmpint(fu_version_guess_format("0x10201"), ==, FWUPD_VERSION_FORMAT_NUMBER); } static void fu_device_version_format_func(void) { g_autoptr(FuDevice) device = fu_device_new(NULL); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_ENSURE_SEMVER); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "Ver1.2.3 RELEASE"); g_assert_cmpstr(fu_device_get_version(device), ==, "1.2.3"); } static void fu_device_version_format_raw_func(void) { g_autoptr(FuDevice) device = g_object_new(FU_TYPE_USB_DEVICE, NULL); /* like normal */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_BCD); fu_device_set_version_raw(device, 256); fu_device_set_version_lowest_raw(device, 257); g_assert_cmpstr(fu_device_get_version(device), ==, "1.0"); g_assert_cmpstr(fu_device_get_version_lowest(device), ==, "1.1"); /* ensure both are changed */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_PLAIN); g_assert_cmpstr(fu_device_get_version(device), ==, "256"); g_assert_cmpstr(fu_device_get_version_lowest(device), ==, "257"); } static void fu_device_open_refcount_func(void) { gboolean ret; g_autoptr(FuDevice) device = fu_device_new(NULL); g_autoptr(GError) error = NULL; fu_device_set_id(device, "test_device"); ret = fu_device_open(device, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_device_open(device, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_device_close(device, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_device_close(device, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_device_close(device, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO); g_assert_false(ret); } static void fu_device_rescan_func(void) { gboolean ret; g_autoptr(FuDevice) device = fu_device_new(NULL); g_autoptr(GError) error = NULL; /* no GUIDs! */ ret = fu_device_rescan(device, &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_device_name_func(void) { g_autoptr(FuDevice) device1 = fu_device_new(NULL); g_autoptr(FuDevice) device2 = fu_device_new(NULL); /* vendor then name */ fu_device_set_vendor(device1, " Hughski "); fu_device_set_name(device1, "HUGHSKI ColorHug(TM)__Pro "); g_assert_cmpstr(fu_device_get_vendor(device1), ==, "Hughski"); g_assert_cmpstr(fu_device_get_name(device1), ==, "ColorHugâ„¢ Pro"); /* name then vendor */ fu_device_set_name(device2, "Hughski ColorHug(TM)_Pro"); fu_device_set_vendor(device2, "Hughski"); g_assert_cmpstr(fu_device_get_vendor(device2), ==, "Hughski"); g_assert_cmpstr(fu_device_get_name(device2), ==, "ColorHugâ„¢ Pro"); /* a real example */ fu_device_set_name(device2, "Intel(R) Core(TM) i7-10850H CPU @ 2.70GHz"); fu_device_set_vendor(device2, "Intel"); g_assert_cmpstr(fu_device_get_name(device2), ==, "Coreâ„¢ i7-10850H CPU @ 2.70GHz"); /* name and vendor are the same */ #ifndef SUPPORTED_BUILD g_test_expect_message("FuDevice", G_LOG_LEVEL_WARNING, "name and vendor are the same*"); #endif fu_device_set_name(device2, "example"); fu_device_set_vendor(device2, "EXAMPLE"); g_assert_cmpstr(fu_device_get_name(device2), ==, "example"); g_assert_cmpstr(fu_device_get_vendor(device2), ==, "EXAMPLE"); } static void fu_device_cfi_device_func(void) { gboolean ret; guint8 cmd = 0; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuCfiDevice) cfi_device = NULL; g_autoptr(GError) error = NULL; ret = fu_context_load_quirks(ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE, &error); g_assert_no_error(error); g_assert_true(ret); cfi_device = fu_cfi_device_new(ctx, "3730"); ret = fu_device_setup(FU_DEVICE(cfi_device), &error); g_assert_no_error(error); g_assert_true(ret); /* fallback */ ret = fu_cfi_device_get_cmd(cfi_device, FU_CFI_DEVICE_CMD_READ_DATA, &cmd, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(cmd, ==, 0x03); /* from quirk */ ret = fu_cfi_device_get_cmd(cfi_device, FU_CFI_DEVICE_CMD_CHIP_ERASE, &cmd, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(cmd, ==, 0xC7); g_assert_cmpint(fu_cfi_device_get_size(cfi_device), ==, 0x10000); g_assert_cmpint(fu_cfi_device_get_page_size(cfi_device), ==, 0x200); g_assert_cmpint(fu_cfi_device_get_sector_size(cfi_device), ==, 0x2000); g_assert_cmpint(fu_cfi_device_get_block_size(cfi_device), ==, 0x8000); } static void fu_device_metadata_func(void) { g_autoptr(FuDevice) device = fu_device_new(NULL); /* string */ fu_device_set_metadata(device, "foo", "bar"); g_assert_cmpstr(fu_device_get_metadata(device, "foo"), ==, "bar"); fu_device_set_metadata(device, "foo", "baz"); g_assert_cmpstr(fu_device_get_metadata(device, "foo"), ==, "baz"); g_assert_null(fu_device_get_metadata(device, "unknown")); /* boolean */ fu_device_set_metadata_boolean(device, "baz", TRUE); g_assert_cmpstr(fu_device_get_metadata(device, "baz"), ==, "true"); g_assert_true(fu_device_get_metadata_boolean(device, "baz")); g_assert_false(fu_device_get_metadata_boolean(device, "unknown")); /* integer */ fu_device_set_metadata_integer(device, "bam", 12345); g_assert_cmpstr(fu_device_get_metadata(device, "bam"), ==, "12345"); g_assert_cmpint(fu_device_get_metadata_integer(device, "bam"), ==, 12345); g_assert_cmpint(fu_device_get_metadata_integer(device, "unknown"), ==, G_MAXUINT); } static void fu_string_utf16_func(void) { g_autofree gchar *str1 = NULL; g_autofree gchar *str2 = NULL; g_autoptr(GByteArray) buf = NULL; g_autoptr(GError) error = NULL; buf = fu_utf8_to_utf16_byte_array("hello world", G_LITTLE_ENDIAN, FU_UTF_CONVERT_FLAG_APPEND_NUL, &error); g_assert_no_error(error); g_assert_nonnull(buf); g_assert_cmpint(buf->len, ==, 24); g_assert_cmpint(buf->data[0], ==, 'h'); g_assert_cmpint(buf->data[1], ==, '\0'); g_assert_cmpint(buf->data[2], ==, 'e'); g_assert_cmpint(buf->data[3], ==, '\0'); str1 = fu_utf16_to_utf8_byte_array(buf, G_LITTLE_ENDIAN, &error); g_assert_no_error(error); g_assert_cmpstr(str1, ==, "hello world"); /* failure */ g_byte_array_set_size(buf, buf->len - 1); str2 = fu_utf16_to_utf8_byte_array(buf, G_LITTLE_ENDIAN, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA); g_assert_cmpstr(str2, ==, NULL); } static void fu_smbios_func(void) { const gchar *str; gboolean ret; g_autofree gchar *dump = NULL; g_autofree gchar *testdatadir = NULL; g_autofree gchar *full_path = NULL; g_autoptr(FuSmbios) smbios = NULL; g_autoptr(GError) error = NULL; #ifdef _WIN32 g_test_skip("Windows uses GetSystemFirmwareTable rather than parsing the fake test data"); return; #endif /* these tests will not write */ testdatadir = g_test_build_filename(G_TEST_DIST, "tests", NULL); (void)g_setenv("FWUPD_SYSFSFWDIR", testdatadir, TRUE); full_path = g_test_build_filename(G_TEST_DIST, "tests", "dmi", "tables", NULL); if (!g_file_test(full_path, G_FILE_TEST_IS_DIR)) { g_test_skip("no DMI tables found"); return; } smbios = fu_smbios_new(); ret = fu_smbios_setup(smbios, &error); g_assert_no_error(error); g_assert_true(ret); dump = fu_firmware_to_string(FU_FIRMWARE(smbios)); g_debug("%s", dump); /* test for missing table */ str = fu_smbios_get_string(smbios, 0xff, FU_SMBIOS_STRUCTURE_LENGTH_ANY, 0, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE); g_assert_null(str); g_clear_error(&error); /* check for invalid offset */ str = fu_smbios_get_string(smbios, FU_SMBIOS_STRUCTURE_TYPE_BIOS, FU_SMBIOS_STRUCTURE_LENGTH_ANY, 0xff, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE); g_assert_null(str); g_clear_error(&error); /* check for invalid length */ str = fu_smbios_get_string(smbios, FU_SMBIOS_STRUCTURE_TYPE_BIOS, 0x01, 0xff, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE); g_assert_null(str); g_clear_error(&error); /* get vendor -- explicit length */ str = fu_smbios_get_string(smbios, FU_SMBIOS_STRUCTURE_TYPE_BIOS, 0x18, 0x04, &error); g_assert_no_error(error); g_assert_cmpstr(str, ==, "LENOVO"); /* get vendor */ str = fu_smbios_get_string(smbios, FU_SMBIOS_STRUCTURE_TYPE_BIOS, FU_SMBIOS_STRUCTURE_LENGTH_ANY, 0x04, &error); g_assert_no_error(error); g_assert_cmpstr(str, ==, "LENOVO"); } static void fu_kernel_cmdline_func(void) { const gchar *buf = "key=val foo bar=\"baz baz baz\" tail\n"; g_autoptr(GHashTable) hash = NULL; hash = fu_kernel_parse_cmdline(buf, strlen(buf)); g_assert_nonnull(hash); g_assert_true(g_hash_table_contains(hash, "key")); g_assert_cmpstr(g_hash_table_lookup(hash, "key"), ==, "val"); g_assert_true(g_hash_table_contains(hash, "foo")); g_assert_cmpstr(g_hash_table_lookup(hash, "foo"), ==, NULL); g_assert_true(g_hash_table_contains(hash, "bar")); g_assert_cmpstr(g_hash_table_lookup(hash, "bar"), ==, "baz baz baz"); g_assert_true(g_hash_table_contains(hash, "tail")); g_assert_false(g_hash_table_contains(hash, "")); } static void fu_kernel_config_func(void) { const gchar *buf = "CONFIG_LOCK_DOWN_KERNEL_FORCE_NONE=y\n\n" "# CONFIG_LOCK_DOWN_KERNEL_FORCE_INTEGRITY is not set\n"; g_autoptr(GHashTable) hash = NULL; g_autoptr(GError) error = NULL; hash = fu_kernel_parse_config(buf, strlen(buf), &error); g_assert_no_error(error); g_assert_nonnull(hash); g_assert_true(g_hash_table_contains(hash, "CONFIG_LOCK_DOWN_KERNEL_FORCE_NONE")); g_assert_cmpstr(g_hash_table_lookup(hash, "CONFIG_LOCK_DOWN_KERNEL_FORCE_NONE"), ==, "y"); g_assert_false(g_hash_table_contains(hash, "CONFIG_LOCK_DOWN_KERNEL_FORCE_INTEGRITY")); } static void fu_smbios3_func(void) { const gchar *str; gboolean ret; g_autofree gchar *dump = NULL; g_autofree gchar *path = NULL; g_autoptr(FuSmbios) smbios = NULL; g_autoptr(GError) error = NULL; path = g_test_build_filename(G_TEST_DIST, "tests", "dmi", "tables64", NULL); if (!g_file_test(path, G_FILE_TEST_IS_DIR)) { g_test_skip("no DMI tables found"); return; } smbios = fu_smbios_new(); ret = fu_smbios_setup_from_path(smbios, path, &error); g_assert_no_error(error); g_assert_true(ret); dump = fu_firmware_to_string(FU_FIRMWARE(smbios)); g_debug("%s", dump); /* get vendor */ str = fu_smbios_get_string(smbios, FU_SMBIOS_STRUCTURE_TYPE_BIOS, 0x18, 0x04, &error); g_assert_no_error(error); g_assert_cmpstr(str, ==, "Dell Inc."); } static void fu_context_backends_func(void) { g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuBackend) backend2 = NULL; g_autoptr(FuBackend) backend = g_object_new(FU_TYPE_BACKEND, "name", "dummy", NULL); g_autoptr(GError) error = NULL; fu_context_add_backend(ctx, backend); backend2 = fu_context_get_backend_by_name(ctx, "dummy", &error); g_assert_no_error(error); g_assert_nonnull(backend2); } static void fu_context_flags_func(void) { g_autoptr(FuContext) ctx = fu_context_new(); g_assert_false(fu_context_has_flag(ctx, FU_CONTEXT_FLAG_SAVE_EVENTS)); fu_context_add_flag(ctx, FU_CONTEXT_FLAG_SAVE_EVENTS); g_assert_true(fu_context_has_flag(ctx, FU_CONTEXT_FLAG_SAVE_EVENTS)); fu_context_remove_flag(ctx, FU_CONTEXT_FLAG_SAVE_EVENTS); fu_context_remove_flag(ctx, FU_CONTEXT_FLAG_SAVE_EVENTS); g_assert_false(fu_context_has_flag(ctx, FU_CONTEXT_FLAG_SAVE_EVENTS)); fu_context_add_flag(ctx, FU_CONTEXT_FLAG_SAVE_EVENTS); fu_context_add_flag(ctx, FU_CONTEXT_FLAG_SAVE_EVENTS); g_assert_true(fu_context_has_flag(ctx, FU_CONTEXT_FLAG_SAVE_EVENTS)); } static void fu_context_udev_subsystems_func(void) { g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) plugin_names1 = NULL; g_autoptr(GPtrArray) plugin_names2 = NULL; g_autoptr(GPtrArray) udev_subsystems = NULL; /* ensure we add the base subsystem too */ fu_context_add_udev_subsystem(ctx, "usb", NULL); fu_context_add_udev_subsystem(ctx, "block:partition", "uf2"); udev_subsystems = fu_context_get_udev_subsystems(ctx); g_assert_nonnull(udev_subsystems); g_assert_cmpint(udev_subsystems->len, ==, 3); /* add another plugin that can handle *all* block devices */ fu_context_add_udev_subsystem(ctx, "block", "uf3"); /* both specified, so return uf2 and uf3 */ plugin_names1 = fu_context_get_plugin_names_for_udev_subsystem(ctx, "block:partition", &error); g_assert_no_error(error); g_assert_nonnull(plugin_names1); g_assert_cmpint(plugin_names1->len, ==, 2); /* devtype unset, so just uf3 */ plugin_names2 = fu_context_get_plugin_names_for_udev_subsystem(ctx, "block", &error); g_assert_no_error(error); g_assert_nonnull(plugin_names2); g_assert_cmpint(plugin_names2->len, ==, 1); } static void fu_context_state_func(void) { g_autoptr(FuContext) ctx = fu_context_new(); g_assert_cmpint(fu_context_get_power_state(ctx), ==, FU_POWER_STATE_UNKNOWN); g_assert_cmpint(fu_context_get_lid_state(ctx), ==, FU_LID_STATE_UNKNOWN); g_assert_cmpint(fu_context_get_display_state(ctx), ==, FU_DISPLAY_STATE_UNKNOWN); g_assert_cmpint(fu_context_get_battery_level(ctx), ==, FWUPD_BATTERY_LEVEL_INVALID); fu_context_set_power_state(ctx, FU_POWER_STATE_BATTERY); fu_context_set_power_state(ctx, FU_POWER_STATE_BATTERY); fu_context_set_lid_state(ctx, FU_LID_STATE_CLOSED); fu_context_set_lid_state(ctx, FU_LID_STATE_CLOSED); fu_context_set_display_state(ctx, FU_DISPLAY_STATE_CONNECTED); fu_context_set_display_state(ctx, FU_DISPLAY_STATE_CONNECTED); fu_context_set_battery_level(ctx, 50); fu_context_set_battery_level(ctx, 50); g_assert_cmpint(fu_context_get_power_state(ctx), ==, FU_POWER_STATE_BATTERY); g_assert_cmpint(fu_context_get_lid_state(ctx), ==, FU_LID_STATE_CLOSED); g_assert_cmpint(fu_context_get_display_state(ctx), ==, FU_DISPLAY_STATE_CONNECTED); g_assert_cmpint(fu_context_get_battery_level(ctx), ==, 50); } static void fu_context_firmware_gtypes_func(void) { g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(GArray) gtypes = NULL; g_autoptr(GPtrArray) gtype_ids = NULL; fu_context_add_firmware_gtype(ctx, "base", FU_TYPE_FIRMWARE); gtype_ids = fu_context_get_firmware_gtype_ids(ctx); g_assert_nonnull(gtype_ids); g_assert_cmpint(gtype_ids->len, ==, 1); g_assert_cmpstr(g_ptr_array_index(gtype_ids, 0), ==, "base"); gtypes = fu_context_get_firmware_gtypes(ctx); g_assert_nonnull(gtypes); g_assert_cmpint(gtypes->len, ==, 1); g_assert_cmpint(g_array_index(gtypes, GType, 0), ==, FU_TYPE_FIRMWARE); g_assert_cmpint(fu_context_get_firmware_gtype_by_id(ctx, "base"), ==, FU_TYPE_FIRMWARE); g_assert_cmpint(fu_context_get_firmware_gtype_by_id(ctx, "n/a"), ==, G_TYPE_INVALID); } static void fu_context_hwids_dmi_func(void) { g_autofree gchar *dump = NULL; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; gboolean ret; ret = fu_context_load_hwinfo(ctx, progress, FU_CONTEXT_HWID_FLAG_LOAD_DMI, &error); g_assert_no_error(error); g_assert_true(ret); dump = fu_firmware_to_string(FU_FIRMWARE(fu_context_get_smbios(ctx))); g_debug("%s", dump); g_assert_cmpstr(fu_context_get_hwid_value(ctx, FU_HWIDS_KEY_MANUFACTURER), ==, "FwupdTest"); g_assert_cmpuint(fu_context_get_chassis_kind(ctx), ==, 16); } static void fu_context_hwids_fdt_func(void) { gboolean ret; g_autofree gchar *dump = NULL; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FuFirmware) fdt_tmp = fu_fdt_firmware_new(); g_autoptr(GError) error = NULL; g_autoptr(GFile) file = g_file_new_for_path("/tmp/fwupd-self-test/var/lib/fwupd/system.dtb"); /* write file */ ret = fu_firmware_build_from_xml( FU_FIRMWARE(fdt_tmp), "\n" " \n" " pine64,rockpro64-v2.1\n" " tablet\n" " fwupd\n" " \n" " ibm,firmware-versions\n" " 1.2.3\n" " \n" " \n" " vpd\n" " \n" " root-node-vpd@a000\n" " \n" " enclosure@1e00\n" " \n" " backplane@800\n" " Tablet\n" " \n" " \n" " \n" " \n" " \n" "\n", &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_firmware_write_file(FU_FIRMWARE(fdt_tmp), file, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_context_load_hwinfo(ctx, progress, FU_CONTEXT_HWID_FLAG_LOAD_FDT, &error); g_assert_no_error(error); g_assert_true(ret); dump = fu_firmware_to_string(FU_FIRMWARE(fu_context_get_smbios(ctx))); g_debug("%s", dump); g_assert_cmpstr(fu_context_get_hwid_value(ctx, FU_HWIDS_KEY_MANUFACTURER), ==, "fwupd"); g_assert_cmpstr(fu_context_get_hwid_value(ctx, FU_HWIDS_KEY_BASEBOARD_PRODUCT), ==, "Tablet"); g_assert_cmpuint(fu_context_get_chassis_kind(ctx), ==, FU_SMBIOS_CHASSIS_KIND_TABLET); } static gboolean fu_test_strnsplit_add_cb(GString *token, guint token_idx, gpointer user_data, GError **error) { GPtrArray *array = (GPtrArray *)user_data; g_debug("TOKEN: [%s] (%u)", token->str, token_idx); g_ptr_array_add(array, g_strdup(token->str)); return TRUE; } static gboolean fu_test_strnsplit_nop_cb(GString *token, guint token_idx, gpointer user_data, GError **error) { guint *cnt = (guint *)user_data; (*cnt)++; return TRUE; } static void fu_common_memmem_func(void) { const guint8 haystack[] = {'H', 'A', 'Y', 'S'}; const guint8 needle[] = {'A', 'Y'}; gboolean ret; gsize offset = 0; g_autoptr(GError) error = NULL; ret = fu_memmem_safe(haystack, sizeof(haystack), needle, sizeof(needle), &offset, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(offset, ==, 0x1); ret = fu_memmem_safe(haystack + 2, sizeof(haystack) - 2, needle, sizeof(needle), &offset, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND); g_assert_false(ret); } static void fu_strpassmask_func(void) { struct { const gchar *in; const gchar *op; } strs[] = {{"foo https://test.com/auth bar", "foo https://test.com/auth bar"}, {"foo https://user%40host:SECRET@test.com/auth bar", "foo https://user%40host:XXXXXX@test.com/auth bar"}, {"foo https://user1%40host:SECRET@test.com/auth " "https://user2%40host:SECRET2@test.com/auth bar", "foo https://user1%40host:XXXXXX@test.com/auth " "https://user2%40host:XXXXXXX@test.com/auth bar"}, {NULL, NULL}}; for (guint i = 0; strs[i].in != NULL; i++) { g_autofree gchar *tmp = fu_strpassmask(strs[i].in); g_assert_cmpstr(tmp, ==, strs[i].op); } } static void fu_strsplit_func(void) { const gchar *str = "123foo123bar123"; const guint bigsz = 1024 * 1024; gboolean ret; guint cnt = 0; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) array = g_ptr_array_new_with_free_func(g_free); g_autoptr(GString) bigstr = g_string_sized_new(bigsz * 2); /* works for me */ ret = fu_strsplit_full(str, -1, "123", fu_test_strnsplit_add_cb, array, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(array->len, ==, 4); g_assert_cmpstr(g_ptr_array_index(array, 0), ==, ""); g_assert_cmpstr(g_ptr_array_index(array, 1), ==, "foo"); g_assert_cmpstr(g_ptr_array_index(array, 2), ==, "bar"); g_assert_cmpstr(g_ptr_array_index(array, 3), ==, ""); /* lets try something insane */ for (guint i = 0; i < bigsz; i++) g_string_append(bigstr, "X\n"); ret = fu_strsplit_full(bigstr->str, -1, "\n", fu_test_strnsplit_nop_cb, &cnt, &error); g_assert_no_error(error); g_assert_true(ret); /* we have an empty last section */ g_assert_cmpint(cnt, ==, bigsz + 1); } static void fu_common_olson_timezone_id_func(void) { g_autofree gchar *timezone_id = NULL; g_autoptr(GError) error = NULL; #ifdef HOST_MACHINE_SYSTEM_DARWIN g_test_skip("not supported on Darwin"); return; #endif timezone_id = fu_common_get_olson_timezone_id(&error); g_assert_no_error(error); #ifdef _WIN32 /* we do not emulate this on Windows, so just check for anything */ g_assert_nonnull(timezone_id); #else g_assert_cmpstr(timezone_id, ==, "America/New_York"); #endif } static void fu_cpuid_func(void) { g_autoptr(GError) error = NULL; g_autoptr(GHashTable) cpu_attrs = NULL; cpu_attrs = fu_cpu_get_attrs(&error); g_assert_no_error(error); g_assert_nonnull(cpu_attrs); g_assert_cmpstr(g_hash_table_lookup(cpu_attrs, "vendor_id"), ==, "AuthenticAMD"); g_assert_cmpstr(g_hash_table_lookup(cpu_attrs, "fpu_exception"), ==, "yes"); } static void fu_strsafe_func(void) { struct { const gchar *in; const gchar *op; } strs[] = {{"dave123", "dave123"}, {"dave123XXX", "dave123"}, {"dave\x03XXX", "dave.XX"}, {"dave\x03\x04XXX", "dave..X"}, {"\x03\x03", NULL}, {NULL, NULL}}; GPtrArray *instance_ids; gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuDevice) dev = fu_device_new(ctx); g_autoptr(GError) error = NULL; /* check bespoke legacy instance ID behavior */ fu_device_add_instance_strsafe(dev, "KEY", "_ _LEN&VO&\\&"); ret = fu_device_build_instance_id(dev, &error, "SUB", "KEY", NULL); g_assert_no_error(error); g_assert_true(ret); fu_device_convert_instance_ids(dev); instance_ids = fu_device_get_instance_ids(dev); g_assert_cmpint(instance_ids->len, ==, 1); g_assert_cmpstr(g_ptr_array_index(instance_ids, 0), ==, "SUB\\KEY_LEN-VO"); for (guint i = 0; strs[i].in != NULL; i++) { g_autofree gchar *tmp = fu_strsafe(strs[i].in, 7); g_assert_cmpstr(tmp, ==, strs[i].op); } } static void fu_hwids_func(void) { g_autofree gchar *testdatadir = NULL; g_autofree gchar *full_path = NULL; g_autoptr(FuContext) context = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; gboolean ret; struct { const gchar *key; const gchar *value; } guids[] = {{"Manufacturer", "6de5d951-d755-576b-bd09-c5cf66b27234"}, {"HardwareID-14", "6de5d951-d755-576b-bd09-c5cf66b27234"}, {"HardwareID-13", "f8e1de5f-b68c-5f52-9d1a-f1ba52f1f773"}, {"HardwareID-12", "e093d715-70f7-51f4-b6c8-b4a7e31def85"}, {"HardwareID-11", "db73af4c-4612-50f7-b8a7-787cf4871847"}, {"HardwareID-10", "f4275c1f-6130-5191-845c-3426247eb6a1"}, {"HardwareID-09", "0cf8618d-9eff-537c-9f35-46861406eb9c"}, {"HardwareID-08", "059eb22d-6dc7-59af-abd3-94bbe017f67c"}, {"HardwareID-07", "da1da9b6-62f5-5f22-8aaa-14db7eeda2a4"}, {"HardwareID-06", "178cd22d-ad9f-562d-ae0a-34009822cdbe"}, {"HardwareID-05", "8dc9b7c5-f5d5-5850-9ab3-bd6f0549d814"}, {"HardwareID-04", "660ccba8-1b78-5a33-80e6-9fb8354ee873"}, {"HardwareID-03", "3faec92a-3ae3-5744-be88-495e90a7d541"}, {"HardwareID-02", "f5ff077f-3eeb-5bae-be1c-e98ffe8ce5f8"}, {"HardwareID-01", "b7cceb67-774c-537e-bf8b-22c6107e9a74"}, {"HardwareID-00", "147efce9-f201-5fc8-ab0c-c859751c3440"}, {NULL, NULL}}; #ifdef _WIN32 g_test_skip("Windows uses GetSystemFirmwareTable rather than parsing the fake test data"); return; #endif /* these tests will not write */ testdatadir = g_test_build_filename(G_TEST_DIST, "tests", NULL); (void)g_setenv("FWUPD_SYSFSFWDIR", testdatadir, TRUE); /* DMI */ full_path = g_test_build_filename(G_TEST_DIST, "tests", "dmi", "tables", NULL); if (!g_file_test(full_path, G_FILE_TEST_IS_DIR)) { g_test_skip("no DMI tables found"); return; } context = fu_context_new(); ret = fu_context_load_hwinfo(context, progress, FU_CONTEXT_HWID_FLAG_LOAD_SMBIOS, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpstr(fu_context_get_hwid_value(context, FU_HWIDS_KEY_MANUFACTURER), ==, "LENOVO"); g_assert_cmpstr(fu_context_get_hwid_value(context, FU_HWIDS_KEY_ENCLOSURE_KIND), ==, "a"); g_assert_cmpstr(fu_context_get_hwid_value(context, FU_HWIDS_KEY_FAMILY), ==, "ThinkPad T440s"); g_assert_cmpstr(fu_context_get_hwid_value(context, FU_HWIDS_KEY_PRODUCT_NAME), ==, "20ARS19C0C"); g_assert_cmpstr(fu_context_get_hwid_value(context, FU_HWIDS_KEY_BIOS_VENDOR), ==, "LENOVO"); g_assert_cmpstr(fu_context_get_hwid_value(context, FU_HWIDS_KEY_BIOS_VERSION), ==, "GJET75WW (2.25 )"); g_assert_cmpstr(fu_context_get_hwid_value(context, FU_HWIDS_KEY_BIOS_MAJOR_RELEASE), ==, "02"); g_assert_cmpstr(fu_context_get_hwid_value(context, FU_HWIDS_KEY_BIOS_MINOR_RELEASE), ==, "19"); g_assert_cmpstr(fu_context_get_hwid_value(context, FU_HWIDS_KEY_PRODUCT_SKU), ==, "LENOVO_MT_20AR_BU_Think_FM_ThinkPad T440s"); for (guint i = 0; guids[i].key != NULL; i++) { FuHwids *hwids = fu_context_get_hwids(context); g_autofree gchar *guid = fu_hwids_get_guid(hwids, guids[i].key, &error); g_assert_no_error(error); g_assert_cmpstr(guid, ==, guids[i].value); } for (guint i = 0; guids[i].key != NULL; i++) g_assert_true(fu_context_has_hwid_guid(context, guids[i].value)); } static void fu_test_plugin_device_added_cb(FuPlugin *plugin, FuDevice *device, gpointer user_data) { FuDevice **dev = (FuDevice **)user_data; *dev = g_object_ref(device); fu_test_loop_quit(); } static void fu_config_func(void) { GStatBuf statbuf = {0}; gboolean ret; g_autofree gchar *composite_data = NULL; g_autoptr(FuConfig) config = fu_config_new(); g_autoptr(GError) error = NULL; g_autofree gchar *fn_imu = NULL; g_autofree gchar *fn_mut = NULL; #ifdef _WIN32 /* the Windows file permission model is different than a simple octal value */ g_test_skip("chmod not supported on Windows"); return; #endif /* immutable file */ (void)g_setenv("FWUPD_SYSCONFDIR", "/tmp/fwupd-self-test/etc/fwupd", TRUE); fn_imu = g_build_filename(g_getenv("FWUPD_SYSCONFDIR"), "fwupd.conf", NULL); g_assert_nonnull(fn_imu); ret = fu_path_mkdir_parent(fn_imu, &error); g_assert_no_error(error); g_assert_true(ret); g_remove(fn_imu); ret = g_file_set_contents(fn_imu, "[fwupd]\n" "Key=true\n", -1, &error); g_assert_no_error(error); g_assert_true(ret); g_chmod(fn_imu, 0640); ret = g_stat(fn_imu, &statbuf); g_assert_cmpint(ret, ==, 0); g_assert_cmpint(statbuf.st_mode & 0777, ==, 0640); /* mutable file */ (void)g_setenv("LOCALCONF_DIRECTORY", "/tmp/fwupd-self-test/var/etc/fwupd", TRUE); fn_mut = g_build_filename(g_getenv("LOCALCONF_DIRECTORY"), "fwupd.conf", NULL); g_assert_nonnull(fn_mut); ret = fu_path_mkdir_parent(fn_mut, &error); g_assert_no_error(error); g_assert_true(ret); g_remove(fn_mut); ret = g_file_set_contents(fn_mut, "# group comment\n" "[fwupd]\n" "# key comment\n" "Key=false\n", -1, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_config_load(config, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_config_set_value(config, "fwupd", "Key", "false", &error); g_assert_no_error(error); g_assert_true(ret); ret = g_file_get_contents(fn_mut, &composite_data, NULL, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_true(g_strstr_len(composite_data, -1, "Key=false") != NULL); g_assert_true(g_strstr_len(composite_data, -1, "Key=true") == NULL); g_assert_true(g_strstr_len(composite_data, -1, "# group comment") != NULL); g_assert_true(g_strstr_len(composite_data, -1, "# key comment") != NULL); g_remove(fn_mut); } static void fu_plugin_config_func(void) { GStatBuf statbuf = {0}; gboolean ret; gint rc; g_autofree gchar *conf_dir = NULL; g_autofree gchar *fn = NULL; g_autofree gchar *value = NULL; g_autofree gchar *value_missing = NULL; g_autofree gchar *fn_mut = NULL; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuPlugin) plugin = fu_plugin_new(ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; #ifdef _WIN32 /* the Windows file permission model is different than a simple octal value */ g_test_skip("chmod not supported on Windows"); return; #endif /* remove existing file */ (void)g_setenv("FWUPD_SYSCONFDIR", "/tmp/fwupd-self-test/etc/fwupd", TRUE); conf_dir = fu_path_from_kind(FU_PATH_KIND_SYSCONFDIR_PKG); fu_plugin_set_name(plugin, "test"); fn = g_build_filename(conf_dir, "fwupd.conf", NULL); ret = fu_path_mkdir_parent(fn, &error); g_assert_no_error(error); g_assert_true(ret); g_remove(fn); ret = g_file_set_contents(fn, "", -1, &error); g_assert_no_error(error); g_assert_true(ret); /* mutable file we'll be writing */ (void)g_setenv("LOCALCONF_DIRECTORY", "/tmp/fwupd-self-test/var/etc/fwupd", TRUE); fn_mut = g_build_filename(g_getenv("LOCALCONF_DIRECTORY"), "fwupd.conf", NULL); g_assert_nonnull(fn_mut); ret = fu_path_mkdir_parent(fn_mut, &error); g_assert_no_error(error); g_assert_true(ret); g_remove(fn_mut); ret = g_file_set_contents(fn_mut, "", -1, &error); g_assert_no_error(error); g_assert_true(ret); /* load context */ ret = fu_context_load_hwinfo(ctx, progress, FU_CONTEXT_HWID_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); /* set a value */ ret = fu_plugin_set_config_value(plugin, "Key", "True", &error); g_assert_no_error(error); g_assert_true(ret); g_assert_true(g_file_test(fn, G_FILE_TEST_EXISTS)); /* check it is only readable by the user/group */ rc = g_stat(fn_mut, &statbuf); g_assert_cmpint(rc, ==, 0); g_assert_cmpint(statbuf.st_mode & 0777, ==, 0640); /* read back the value */ fu_plugin_set_config_default(plugin, "NotGoingToExist", "Foo"); value_missing = fu_plugin_get_config_value(plugin, "NotGoingToExist"); g_assert_cmpstr(value_missing, ==, "Foo"); value = fu_plugin_get_config_value(plugin, "Key"); g_assert_cmpstr(value, ==, "True"); g_assert_true(fu_plugin_get_config_value_boolean(plugin, "Key")); } static void fu_plugin_devices_func(void) { g_autoptr(FuDevice) device = fu_device_new(NULL); g_autoptr(FuDevice) child = fu_device_new(NULL); g_autoptr(FuPlugin) plugin = fu_plugin_new(NULL); GPtrArray *devices; devices = fu_plugin_get_devices(plugin); g_assert_nonnull(devices); g_assert_cmpint(devices->len, ==, 0); fu_device_set_id(device, "testdev"); fu_device_set_name(device, "testdev"); fu_plugin_device_add(plugin, device); g_assert_cmpint(devices->len, ==, 1); fu_plugin_device_remove(plugin, device); g_assert_cmpint(devices->len, ==, 0); /* add a child after adding the parent to the plugin */ fu_device_set_id(child, "child"); fu_device_set_name(child, "child"); fu_device_add_child(device, child); g_assert_cmpint(devices->len, ==, 1); /* remove said child */ fu_device_remove_child(device, child); g_assert_cmpint(devices->len, ==, 0); } static void fu_plugin_device_inhibit_children_func(void) { g_autoptr(FuDevice) parent = fu_device_new(NULL); g_autoptr(FuDevice) child1 = fu_device_new(NULL); g_autoptr(FuDevice) child2 = fu_device_new(NULL); fu_device_set_id(parent, "testdev"); fu_device_set_name(parent, "testdev"); fu_device_add_flag(parent, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_set_id(child1, "child1"); fu_device_set_name(child1, "child1"); fu_device_add_flag(child1, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_child(parent, child1); /* inhibit the parent */ fu_device_inhibit(parent, "test", "because"); g_assert_false(fu_device_has_flag(parent, FWUPD_DEVICE_FLAG_UPDATABLE)); g_assert_true(fu_device_has_flag(child1, FWUPD_DEVICE_FLAG_UPDATABLE)); fu_device_uninhibit(parent, "test"); /* make the inhibit propagate to children */ fu_device_add_private_flag(parent, FU_DEVICE_PRIVATE_FLAG_INHIBIT_CHILDREN); fu_device_inhibit(parent, "test", "because"); g_assert_false(fu_device_has_flag(parent, FWUPD_DEVICE_FLAG_UPDATABLE)); g_assert_false(fu_device_has_flag(child1, FWUPD_DEVICE_FLAG_UPDATABLE)); /* add a child after the inhibit, which should also be inhibited too */ fu_device_set_id(child2, "child2"); fu_device_set_name(child2, "child2"); fu_device_add_flag(child2, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_child(parent, child2); g_assert_false(fu_device_has_flag(parent, FWUPD_DEVICE_FLAG_UPDATABLE)); g_assert_false(fu_device_has_flag(child1, FWUPD_DEVICE_FLAG_UPDATABLE)); g_assert_false(fu_device_has_flag(child2, FWUPD_DEVICE_FLAG_UPDATABLE)); } static void fu_plugin_delay_func(void) { FuDevice *device_tmp; g_autoptr(FuPlugin) plugin = NULL; g_autoptr(FuDevice) device = NULL; plugin = fu_plugin_new(NULL); g_signal_connect(FU_PLUGIN(plugin), "device-added", G_CALLBACK(fu_test_plugin_device_added_cb), &device_tmp); g_signal_connect(FU_PLUGIN(plugin), "device-removed", G_CALLBACK(fu_test_plugin_device_added_cb), &device_tmp); /* add device straight away */ device = fu_device_new(NULL); fu_device_set_id(device, "testdev"); fu_plugin_device_add(plugin, device); g_assert_nonnull(device_tmp); g_assert_cmpstr(fu_device_get_id(device_tmp), ==, "b7eccd0059d6d7dc2ef76c35d6de0048cc8c029d"); g_clear_object(&device_tmp); /* remove device */ fu_plugin_device_remove(plugin, device); g_assert_nonnull(device_tmp); g_assert_cmpstr(fu_device_get_id(device_tmp), ==, "b7eccd0059d6d7dc2ef76c35d6de0048cc8c029d"); g_clear_object(&device_tmp); } static void fu_plugin_fdt_func(void) { gboolean ret; g_autofree gchar *compatible = NULL; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuFirmware) fdt = NULL; g_autoptr(FuFirmware) fdt_root = NULL; g_autoptr(FuFirmware) fdt_tmp = fu_fdt_firmware_new(); g_autoptr(FuFirmware) img2 = NULL; g_autoptr(FuFirmware) img3 = NULL; g_autoptr(FuFirmware) img4 = NULL; g_autoptr(GError) error = NULL; g_autoptr(GFile) file = g_file_new_for_path("/tmp/fwupd-self-test/var/lib/fwupd/system.dtb"); /* write file */ ret = fu_firmware_build_from_xml( FU_FIRMWARE(fdt_tmp), "\n" " \n" " pine64,rockpro64-v2.1\n" " \n" "\n", &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_firmware_write_file(FU_FIRMWARE(fdt_tmp), file, &error); g_assert_no_error(error); g_assert_true(ret); /* get compatible from the context */ fdt = fu_context_get_fdt(ctx, &error); g_assert_no_error(error); g_assert_nonnull(fdt); fdt_root = fu_firmware_get_image_by_id(fdt, NULL, &error); g_assert_no_error(error); g_assert_nonnull(fdt_root); ret = fu_fdt_image_get_attr_str(FU_FDT_IMAGE(fdt_root), "compatible", &compatible, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpstr(compatible, ==, "pine64,rockpro64-v2.1"); /* get by GType */ img2 = fu_firmware_get_image_by_gtype(fdt, FU_TYPE_FIRMWARE, &error); g_assert_no_error(error); g_assert_nonnull(img2); img3 = fu_firmware_get_image_by_gtype(fdt, FU_TYPE_FDT_IMAGE, &error); g_assert_no_error(error); g_assert_nonnull(img3); img4 = fu_firmware_get_image_by_gtype(fdt, G_TYPE_STRING, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND); g_assert_null(img4); } static void fu_plugin_quirks_func(void) { const gchar *tmp; gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(GError) error = NULL; ret = fu_context_load_quirks(ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE, &error); g_assert_no_error(error); g_assert_true(ret); /* USB\\VID_0A5C&PID_6412 */ tmp = fu_context_lookup_quirk_by_id(ctx, "7a1ba7b9-6bcd-54a4-8a36-d60cc5ee935c", "Flags"); g_assert_cmpstr(tmp, ==, "ignore-runtime"); /* ACME Inc.=True */ tmp = fu_context_lookup_quirk_by_id(ctx, "ec77e295-7c63-5935-9957-be0472d9593a", "Name"); g_assert_cmpstr(tmp, ==, "awesome"); /* CORP* */ tmp = fu_context_lookup_quirk_by_id(ctx, "3731cce4-484c-521f-a652-892c8e0a65c7", "Name"); g_assert_cmpstr(tmp, ==, "town"); /* baz */ tmp = fu_context_lookup_quirk_by_id(ctx, "579a3b1c-d1db-5bdc-b6b9-e2c1b28d5b8a", "Unfound"); g_assert_cmpstr(tmp, ==, NULL); /* unfound */ tmp = fu_context_lookup_quirk_by_id(ctx, "8ff2ed23-b37e-5f61-b409-b7fe9563be36", "tests"); g_assert_cmpstr(tmp, ==, NULL); /* unfound */ tmp = fu_context_lookup_quirk_by_id(ctx, "8ff2ed23-b37e-5f61-b409-b7fe9563be36", "unfound"); g_assert_cmpstr(tmp, ==, NULL); /* GUID */ tmp = fu_context_lookup_quirk_by_id(ctx, "bb9ec3e2-77b3-53bc-a1f1-b05916715627", "Flags"); g_assert_cmpstr(tmp, ==, "clever"); } static void fu_plugin_quirks_performance_func(void) { gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuQuirks) quirks = fu_quirks_new(ctx); g_autoptr(GTimer) timer = g_timer_new(); g_autoptr(GError) error = NULL; const gchar *keys[] = {"Name", "Children", "Flags", NULL}; ret = fu_quirks_load(quirks, FU_QUIRKS_LOAD_FLAG_NO_CACHE, &error); g_assert_no_error(error); g_assert_true(ret); /* lookup */ g_timer_reset(timer); for (guint j = 0; j < 1000; j++) { const gchar *group = "bb9ec3e2-77b3-53bc-a1f1-b05916715627"; for (guint i = 0; keys[i] != NULL; i++) { const gchar *tmp = fu_quirks_lookup_by_id(quirks, group, keys[i]); g_assert_cmpstr(tmp, !=, NULL); } } g_print("lookup=%.3fms ", g_timer_elapsed(timer, NULL) * 1000.f); } typedef struct { gboolean seen_one; gboolean seen_two; } FuPluginQuirksAppendHelper; static void fu_plugin_quirks_append_cb(FuQuirks *quirks, const gchar *key, const gchar *value, FuContextQuirkSource source, gpointer user_data) { FuPluginQuirksAppendHelper *helper = (FuPluginQuirksAppendHelper *)user_data; g_debug("key=%s, value=%s", key, value); if (g_strcmp0(key, "Plugin") == 0 && g_strcmp0(value, "one") == 0) { helper->seen_one = TRUE; return; } if (g_strcmp0(key, "Plugin") == 0 && g_strcmp0(value, "two") == 0) { helper->seen_two = TRUE; return; } g_assert_not_reached(); } static void fu_plugin_device_progress_func(void) { g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuDevice) device = fu_device_new(ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FuDeviceProgress) device_progress = fu_device_progress_new(device, progress); /* proxy */ fu_progress_set_percentage(progress, 50); fu_progress_set_status(progress, FWUPD_STATUS_SHUTDOWN); g_assert_cmpint(fu_device_get_percentage(device), ==, 50); g_assert_cmpint(fu_device_get_status(device), ==, FWUPD_STATUS_SHUTDOWN); /* clear */ g_clear_object(&device_progress); g_assert_cmpint(fu_device_get_percentage(device), ==, 0); g_assert_cmpint(fu_device_get_status(device), ==, FWUPD_STATUS_IDLE); /* do not proxy */ fu_progress_set_percentage(progress, 100); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_BUSY); g_assert_cmpint(fu_device_get_percentage(device), ==, 0); g_assert_cmpint(fu_device_get_status(device), ==, FWUPD_STATUS_IDLE); } static void fu_plugin_quirks_append_func(void) { FuPluginQuirksAppendHelper helper = {0}; gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuQuirks) quirks = fu_quirks_new(ctx); g_autoptr(GError) error = NULL; /* lookup a duplicate group name */ ret = fu_quirks_load(quirks, FU_QUIRKS_LOAD_FLAG_NO_CACHE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_quirks_lookup_by_id_iter(quirks, "b19d1c67-a29a-51ce-9cae-f7b40fe5505b", NULL, fu_plugin_quirks_append_cb, &helper); g_assert_true(ret); g_assert_true(helper.seen_one); g_assert_true(helper.seen_two); } static void fu_quirks_vendor_ids_func(void) { gboolean ret; const gchar *tmp; g_autoptr(FuContext) ctx = fu_context_new(); g_autofree gchar *guid1 = fwupd_guid_hash_string("PCI\\VEN_8086"); g_autofree gchar *guid2 = fwupd_guid_hash_string("USB\\VID_8086"); g_autofree gchar *guid3 = fwupd_guid_hash_string("PNP\\VID_ICO"); g_autofree gchar *guid4 = fwupd_guid_hash_string("PCI\\VEN_8086&DEV_0007"); g_autofree gchar *guid5 = fwupd_guid_hash_string("USB\\VID_8086&PID_0001"); g_autofree gchar *datadata = fu_path_from_kind(FU_PATH_KIND_CACHEDIR_PKG); g_autofree gchar *quirksdb = g_build_filename(datadata, "quirks.db", NULL); g_autoptr(FuQuirks) quirks = fu_quirks_new(ctx); g_autoptr(GError) error = NULL; g_debug("deleting %s if exists", quirksdb); g_unlink(quirksdb); /* lookup a duplicate group name */ ret = fu_quirks_load(quirks, FU_QUIRKS_LOAD_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); tmp = fu_quirks_lookup_by_id(quirks, guid1, "Vendor"); g_assert_true(ret); g_assert_cmpstr(tmp, ==, "Intel Corporation"); tmp = fu_quirks_lookup_by_id(quirks, guid2, "Vendor"); g_assert_true(ret); g_assert_cmpstr(tmp, ==, "Intel Corp."); tmp = fu_quirks_lookup_by_id(quirks, guid3, FWUPD_RESULT_KEY_VENDOR); g_assert_true(ret); g_assert_cmpstr(tmp, ==, "Intel Corp"); tmp = fu_quirks_lookup_by_id(quirks, guid4, FWUPD_RESULT_KEY_NAME); g_assert_true(ret); g_assert_cmpstr(tmp, ==, "82379AB"); tmp = fu_quirks_lookup_by_id(quirks, guid5, FWUPD_RESULT_KEY_NAME); g_assert_true(ret); g_assert_cmpstr(tmp, ==, "AnyPoint (TM) Home Network 1.6 Mbps Wireless Adapter"); } static void fu_plugin_func(void) { GHashTable *metadata; GPtrArray *rules; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuPlugin) plugin = fu_plugin_new(ctx); fu_plugin_add_rule(plugin, FU_PLUGIN_RULE_CONFLICTS, "dave1"); fu_plugin_add_rule(plugin, FU_PLUGIN_RULE_CONFLICTS, "dave2"); rules = fu_plugin_get_rules(plugin, FU_PLUGIN_RULE_CONFLICTS); g_assert_nonnull(rules); g_assert_cmpint(rules->len, ==, 2); rules = fu_plugin_get_rules(plugin, FU_PLUGIN_RULE_RUN_AFTER); g_assert_null(rules); fu_plugin_add_report_metadata(plugin, "key", "value"); metadata = fu_plugin_get_report_metadata(plugin); g_assert_nonnull(metadata); g_assert_cmpint(g_hash_table_size(metadata), ==, 1); } static void fu_plugin_vfuncs_func(void) { gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuPlugin) plugin = fu_plugin_new(ctx); g_autoptr(GError) error = NULL; /* nop: error */ ret = fu_plugin_runner_modify_config(plugin, "foo", "bar", &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_false(ret); g_clear_error(&error); } static void fu_plugin_device_gtype_func(void) { g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuPlugin) plugin = fu_plugin_new(ctx); /* add the same gtype multiple times */ fu_plugin_add_device_gtype(plugin, FU_TYPE_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_DEVICE); g_assert_cmpint(fu_plugin_get_device_gtype_default(plugin), ==, FU_TYPE_DEVICE); /* now there's no explicit default */ fu_plugin_add_device_gtype(plugin, FU_TYPE_TEST_DEVICE); g_assert_cmpint(fu_plugin_get_device_gtype_default(plugin), ==, G_TYPE_INVALID); /* make it explicit */ fu_plugin_set_device_gtype_default(plugin, FU_TYPE_TEST_DEVICE); g_assert_cmpint(fu_plugin_get_device_gtype_default(plugin), ==, FU_TYPE_TEST_DEVICE); } static void fu_plugin_backend_device_func(void) { gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuDevice) device = fu_device_new(ctx); g_autoptr(FuPlugin) plugin = fu_plugin_new(ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRFUNC); g_autoptr(GError) error = NULL; ret = fu_plugin_runner_backend_device_changed(plugin, device, &error); g_assert_no_error(error); g_assert_true(ret); fu_device_set_specialized_gtype(device, FU_TYPE_DEVICE); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_ONLY_SUPPORTED); ret = fu_plugin_runner_backend_device_added(plugin, device, progress, &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_test_plugin_backend_proxy_device_added_cb(FuPlugin *plugin, FuDevice *device, gpointer user_data) { FuDevice **dev = (FuDevice **)user_data; *dev = g_object_ref(device); } static void fu_plugin_backend_proxy_device_func(void) { gboolean ret; FuDevice *proxy; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuDevice) device = fu_device_new(ctx); g_autoptr(FuDevice) device_new = NULL; g_autoptr(FuPlugin) plugin = fu_plugin_new(ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRFUNC); g_autoptr(GError) error = NULL; fu_device_set_id(device, "testdev"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_EMULATED); ret = fu_plugin_runner_backend_device_changed(plugin, device, &error); g_assert_no_error(error); g_assert_true(ret); /* watch for the new superclassed device */ g_signal_connect(plugin, "device-added", G_CALLBACK(fu_test_plugin_backend_proxy_device_added_cb), &device_new); fu_device_set_specialized_gtype(device, FU_TYPE_DEVICE); fu_device_set_proxy_gtype(device, FU_TYPE_TEST_DEVICE); ret = fu_plugin_runner_backend_device_added(plugin, device, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* check device was constructed */ g_assert_nonnull(device_new); g_assert_true(FU_IS_DEVICE(device_new)); /* check proxy was constructed */ proxy = fu_device_get_proxy(device_new); g_assert_nonnull(proxy); g_assert_true(FU_IS_TEST_DEVICE(proxy)); } static void fu_plugin_quirks_device_func(void) { FuDevice *device_tmp; GPtrArray *children; gboolean ret; g_autoptr(FuDevice) device = fu_device_new(NULL); g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(GError) error = NULL; ret = fu_context_load_quirks(ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE, &error); g_assert_no_error(error); g_assert_true(ret); /* use quirk file to set device attributes */ fu_device_set_physical_id(device, "usb:00:05"); fu_device_set_context(device, ctx); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_instance_id_full(device, "USB\\VID_0BDA&PID_1100", FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_QUIRKS); fu_device_add_instance_id(device, "USB\\VID_0BDA&PID_1100&CID_1234"); g_assert_cmpstr(fu_device_get_name(device), ==, "Hub"); /* ensure the non-customer-id instance ID is not available */ g_assert_true(fu_device_has_instance_id(device, "USB\\VID_0BDA&PID_1100&CID_1234", FU_DEVICE_INSTANCE_FLAG_QUIRKS)); g_assert_true(fu_device_has_instance_id(device, "USB\\VID_0BDA&PID_1100&CID_1234", FU_DEVICE_INSTANCE_FLAG_VISIBLE)); g_assert_true(fu_device_has_instance_id(device, "USB\\VID_0BDA&PID_1100", FU_DEVICE_INSTANCE_FLAG_QUIRKS)); g_assert_false(fu_device_has_instance_id(device, "USB\\VID_0BDA&PID_1100", FU_DEVICE_INSTANCE_FLAG_VISIBLE)); /* ensure children are created */ children = fu_device_get_children(device); g_assert_cmpint(children->len, ==, 1); device_tmp = g_ptr_array_index(children, 0); g_assert_cmpstr(fu_device_get_name(device_tmp), ==, "HDMI"); g_assert_true(fu_device_has_flag(device_tmp, FWUPD_DEVICE_FLAG_UPDATABLE)); } static void fu_common_kernel_lockdown_func(void) { gboolean ret; g_autofree gchar *locked_dir = NULL; g_autofree gchar *none_dir = NULL; g_autofree gchar *old_kernel_dir = NULL; #ifndef __linux__ g_test_skip("only works on Linux"); return; #endif old_kernel_dir = g_test_build_filename(G_TEST_DIST, "tests", "lockdown", NULL); (void)g_setenv("FWUPD_SYSFSSECURITYDIR", old_kernel_dir, TRUE); ret = fu_kernel_locked_down(); g_assert_false(ret); locked_dir = g_test_build_filename(G_TEST_DIST, "tests", "lockdown", "locked", NULL); (void)g_setenv("FWUPD_SYSFSSECURITYDIR", locked_dir, TRUE); ret = fu_kernel_locked_down(); g_assert_true(ret); none_dir = g_test_build_filename(G_TEST_DIST, "tests", "lockdown", "none", NULL); (void)g_setenv("FWUPD_SYSFSSECURITYDIR", none_dir, TRUE); ret = fu_kernel_locked_down(); g_assert_false(ret); } static void fu_common_kernel_search_func(void) { gboolean ret; g_autofree gchar *result1 = NULL; g_autofree gchar *result2 = NULL; g_autoptr(FuKernelSearchPathLocker) locker = NULL; g_autoptr(GError) error = NULL; #ifndef __linux__ g_test_skip("only works on Linux"); return; #endif (void)g_setenv("FWUPD_FIRMWARESEARCH", "/dev/null", TRUE); result1 = fu_kernel_search_path_get_current(&error); g_assert_null(result1); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL); g_clear_error(&error); ret = g_file_set_contents("/tmp/fwupd-self-test/search_path", "oldvalue", -1, &error); g_assert_no_error(error); g_assert_true(ret); (void)g_setenv("FWUPD_FIRMWARESEARCH", "/tmp/fwupd-self-test/search_path", TRUE); locker = fu_kernel_search_path_locker_new("/foo/bar", &error); g_assert_no_error(error); g_assert_nonnull(locker); g_assert_cmpstr(fu_kernel_search_path_locker_get_path(locker), ==, "/foo/bar"); result1 = fu_kernel_search_path_get_current(&error); g_assert_nonnull(result1); g_assert_cmpstr(result1, ==, "/foo/bar"); g_assert_no_error(error); g_clear_object(&locker); result2 = fu_kernel_search_path_get_current(&error); g_assert_nonnull(result2); g_assert_cmpstr(result2, ==, "oldvalue"); g_assert_no_error(error); } static gboolean fu_test_open_cb(GObject *device, GError **error) { g_assert_cmpstr(g_object_get_data(device, "state"), ==, "closed"); g_object_set_data(device, "state", (gpointer) "opened"); return TRUE; } static gboolean fu_test_close_cb(GObject *device, GError **error) { g_assert_cmpstr(g_object_get_data(device, "state"), ==, "opened"); g_object_set_data(device, "state", (gpointer) "closed-on-unref"); return TRUE; } static void fu_device_locker_func(void) { g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GError) error = NULL; g_autoptr(GObject) device = g_object_new(G_TYPE_OBJECT, NULL); g_object_set_data(device, "state", (gpointer) "closed"); locker = fu_device_locker_new_full(device, fu_test_open_cb, fu_test_close_cb, &error); g_assert_no_error(error); g_assert_nonnull(locker); g_clear_object(&locker); g_assert_cmpstr(g_object_get_data(device, "state"), ==, "closed-on-unref"); } static gboolean fu_test_fail_open_cb(FuDevice *device, GError **error) { fu_device_set_metadata_boolean(device, "Test::Open", TRUE); g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "fail"); return FALSE; } static gboolean fu_test_fail_close_cb(FuDevice *device, GError **error) { fu_device_set_metadata_boolean(device, "Test::Close", TRUE); g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "busy"); return FALSE; } static void fu_device_locker_fail_func(void) { g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GError) error = NULL; g_autoptr(FuDevice) device = fu_device_new(NULL); locker = fu_device_locker_new_full(device, (FuDeviceLockerFunc)fu_test_fail_open_cb, (FuDeviceLockerFunc)fu_test_fail_close_cb, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL); g_assert_null(locker); g_assert_true(fu_device_get_metadata_boolean(device, "Test::Open")); g_assert_true(fu_device_get_metadata_boolean(device, "Test::Close")); g_assert_false(fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_IS_OPEN)); } static void fu_common_endian_func(void) { guint8 buf[3]; fu_memwrite_uint16(buf, 0x1234, G_LITTLE_ENDIAN); g_assert_cmpint(buf[0], ==, 0x34); g_assert_cmpint(buf[1], ==, 0x12); g_assert_cmpint(fu_memread_uint16(buf, G_LITTLE_ENDIAN), ==, 0x1234); fu_memwrite_uint16(buf, 0x1234, G_BIG_ENDIAN); g_assert_cmpint(buf[0], ==, 0x12); g_assert_cmpint(buf[1], ==, 0x34); g_assert_cmpint(fu_memread_uint16(buf, G_BIG_ENDIAN), ==, 0x1234); fu_memwrite_uint24(buf, 0x123456, G_LITTLE_ENDIAN); g_assert_cmpint(buf[0], ==, 0x56); g_assert_cmpint(buf[1], ==, 0x34); g_assert_cmpint(buf[2], ==, 0x12); g_assert_cmpint(fu_memread_uint24(buf, G_LITTLE_ENDIAN), ==, 0x123456); fu_memwrite_uint24(buf, 0x123456, G_BIG_ENDIAN); g_assert_cmpint(buf[0], ==, 0x12); g_assert_cmpint(buf[1], ==, 0x34); g_assert_cmpint(buf[2], ==, 0x56); g_assert_cmpint(fu_memread_uint24(buf, G_BIG_ENDIAN), ==, 0x123456); } static void fu_common_bytes_get_data_func(void) { const gchar *fn = "/tmp/fwupdzero"; const guint8 *buf; gboolean ret; g_autoptr(GBytes) bytes1 = NULL; g_autoptr(GBytes) bytes2 = NULL; g_autoptr(GError) error = NULL; g_autoptr(GMappedFile) mmap = NULL; /* create file with zero size */ ret = g_file_set_contents(fn, NULL, 0, &error); g_assert_no_error(error); g_assert_true(ret); /* check we got zero sized data */ bytes1 = fu_bytes_get_contents(fn, &error); g_assert_no_error(error); g_assert_nonnull(bytes1); g_assert_cmpint(g_bytes_get_size(bytes1), ==, 0); g_assert_nonnull(g_bytes_get_data(bytes1, NULL)); /* do the same with an mmap mapping, which returns NULL on empty file */ mmap = g_mapped_file_new(fn, FALSE, &error); g_assert_no_error(error); g_assert_nonnull(mmap); bytes2 = g_mapped_file_get_bytes(mmap); g_assert_nonnull(bytes2); g_assert_cmpint(g_bytes_get_size(bytes2), ==, 0); g_assert_null(g_bytes_get_data(bytes2, NULL)); /* use the safe function */ buf = fu_bytes_get_data_safe(bytes2, NULL, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA); g_assert_null(buf); } static gboolean fu_device_poll_cb(FuDevice *device, GError **error) { guint64 cnt = fu_device_get_metadata_integer(device, "cnt"); g_debug("poll cnt=%" G_GUINT64_FORMAT, cnt); fu_device_set_metadata_integer(device, "cnt", cnt + 1); return TRUE; } static void fu_device_poll_func(void) { gboolean ret; g_autoptr(FuDevice) device = fu_device_new(NULL); g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GError) error = NULL; FuDeviceClass *klass = FU_DEVICE_GET_CLASS(device); guint cnt; klass->poll = fu_device_poll_cb; fu_device_set_metadata_integer(device, "cnt", 0); /* manual poll */ ret = fu_device_poll(device, &error); g_assert_no_error(error); g_assert_true(ret); cnt = fu_device_get_metadata_integer(device, "cnt"); g_assert_cmpint(cnt, ==, 1); /* set up a 10ms poll */ fu_device_set_poll_interval(device, 5); fu_test_loop_run_with_timeout(50); fu_test_loop_quit(); cnt = fu_device_get_metadata_integer(device, "cnt"); g_assert_cmpint(cnt, >=, 5); fu_test_loop_quit(); /* auto pause */ fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_AUTO_PAUSE_POLLING); locker = fu_device_poll_locker_new(device, &error); g_assert_no_error(error); g_assert_nonnull(locker); fu_test_loop_run_with_timeout(25); g_clear_object(&locker); g_assert_cmpint(fu_device_get_metadata_integer(device, "cnt"), ==, cnt); fu_test_loop_quit(); /* disable the poll manually */ fu_device_set_poll_interval(device, 0); fu_test_loop_run_with_timeout(25); fu_test_loop_quit(); g_assert_cmpint(fu_device_get_metadata_integer(device, "cnt"), ==, cnt); fu_test_loop_quit(); } static void fu_device_func(void) { g_autoptr(FuDevice) device = fu_device_new(NULL); g_autoptr(GPtrArray) possible_plugins = NULL; /* only add one plugin name of the same type */ fu_device_add_possible_plugin(device, "test"); fu_device_add_possible_plugin(device, "test"); possible_plugins = fu_device_get_possible_plugins(device); g_assert_cmpint(possible_plugins->len, ==, 1); } static void fu_device_event_donor_func(void) { g_autoptr(FuDevice) device1 = fu_device_new(NULL); g_autoptr(FuDevice) device2 = fu_device_new(NULL); g_autoptr(FuDeviceEvent) event1 = fu_device_event_new("foo:bar:baz"); g_autoptr(FuDeviceEvent) event2 = fu_device_event_new("aaa:bbb:ccc"); g_autoptr(FuDeviceEvent) event3 = fu_device_event_new("foo:111:222"); GPtrArray *events; fu_device_add_event(device1, event1); fu_device_add_event(device2, event2); fu_device_set_target(device1, device2); /* did we incorporate */ events = fu_device_get_events(device2); g_assert_nonnull(events); g_assert_cmpint(events->len, ==, 2); /* make sure it is redirected */ fu_device_add_event(device1, event3); events = fu_device_get_events(device2); g_assert_nonnull(events); g_assert_cmpint(events->len, ==, 3); } static void fu_device_event_func(void) { gboolean ret; const gchar *str; g_autofree gchar *json = NULL; g_autoptr(FuDeviceEvent) event1 = fu_device_event_new("foo:bar:baz"); g_autoptr(FuDeviceEvent) event2 = fu_device_event_new(NULL); g_autoptr(GBytes) blob1 = g_bytes_new_static("hello", 6); g_autoptr(GBytes) blob2 = NULL; g_autoptr(GBytes) blob3 = NULL; g_autoptr(GError) error = NULL; g_autoptr(GError) error_copy = NULL; fu_device_event_set_str(event1, "Name", "Richard"); fu_device_event_set_i64(event1, "Age", 123); fu_device_event_set_bytes(event1, "Blob", blob1); fu_device_event_set_data(event1, "Data", NULL, 0); /* no event set */ ret = fu_device_event_check_error(event1, &error); g_assert_no_error(error); g_assert_true(ret); json = fwupd_codec_to_json_string(FWUPD_CODEC(event1), FWUPD_CODEC_FLAG_COMPRESSED, &error); g_assert_no_error(error); g_assert_cmpstr(json, ==, "{\n" " \"Id\" : \"#f9f98a90\",\n" " \"Name\" : \"Richard\",\n" " \"Age\" : 123,\n" " \"Blob\" : \"aGVsbG8A\",\n" " \"Data\" : \"\"\n" "}"); ret = fwupd_codec_from_json_string(FWUPD_CODEC(event2), json, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpstr(fu_device_event_get_id(event2), ==, "#f9f98a90"); g_assert_cmpint(fu_device_event_get_i64(event2, "Age", NULL), ==, 123); g_assert_cmpstr(fu_device_event_get_str(event2, "Name", NULL), ==, "Richard"); blob2 = fu_device_event_get_bytes(event2, "Blob", &error); g_assert_nonnull(blob2); g_assert_cmpstr(g_bytes_get_data(blob2, NULL), ==, "hello"); blob3 = fu_device_event_get_bytes(event2, "Data", &error); g_assert_nonnull(blob3); g_assert_cmpstr(g_bytes_get_data(blob3, NULL), ==, NULL); /* invalid type */ str = fu_device_event_get_str(event2, "Age", &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA); g_assert_null(str); /* set error */ fu_device_event_set_error(event2, error); ret = fu_device_event_check_error(event2, &error_copy); g_assert_error(error_copy, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA); g_assert_cmpstr(error_copy->message, ==, "invalid event type for key Age"); g_assert_false(ret); } static void fu_device_event_uncompressed_func(void) { g_autofree gchar *json = NULL; g_autoptr(FuDeviceEvent) event = fu_device_event_new("foo:bar:baz"); g_autoptr(GError) error = NULL; /* uncompressed */ fu_device_event_set_str(event, "Name", "Richard"); json = fwupd_codec_to_json_string(FWUPD_CODEC(event), FWUPD_CODEC_FLAG_NONE, &error); g_assert_no_error(error); g_assert_cmpstr(json, ==, "{\n" " \"Id\" : \"foo:bar:baz\",\n" " \"Name\" : \"Richard\"\n" "}"); } static void fu_device_vfuncs_func(void) { gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuDevice) device = fu_device_new(ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FuFirmware) firmware_dummy = fu_firmware_new(); g_autoptr(FuFirmware) firmware = NULL; g_autoptr(GBytes) blob = NULL; g_autoptr(GError) error = NULL; /* nop: error */ ret = fu_device_get_results(device, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_false(ret); g_clear_error(&error); ret = fu_device_write_firmware(device, firmware_dummy, progress, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_false(ret); g_clear_error(&error); firmware = fu_device_read_firmware(device, progress, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_null(firmware); g_clear_error(&error); blob = fu_device_dump_firmware(device, progress, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_null(blob); g_clear_error(&error); ret = fu_device_unbind_driver(device, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_false(ret); g_clear_error(&error); ret = fu_device_bind_driver(device, "subsystem", "driver", &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_false(ret); g_clear_error(&error); /* nop: ignore */ ret = fu_device_detach(device, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_device_attach(device, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_device_activate(device, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* no-probe */ fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_NO_PROBE); ret = fu_device_probe(device, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_false(ret); g_clear_error(&error); } static void fu_device_instance_ids_func(void) { gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuDevice) device = fu_device_new(ctx); g_autoptr(GError) error = NULL; /* do not save silo */ ret = fu_context_load_quirks(ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE, &error); g_assert_no_error(error); g_assert_true(ret); /* sanity check */ g_assert_false(fu_device_has_guid(device, "c0a26214-223b-572a-9477-cde897fe8619")); /* add a deferred instance ID that only gets converted on ->setup */ fu_device_add_instance_id(device, "foobarbaz"); g_assert_false(fu_device_has_guid(device, "c0a26214-223b-572a-9477-cde897fe8619")); ret = fu_device_setup(device, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_true(fu_device_has_guid(device, "c0a26214-223b-572a-9477-cde897fe8619")); /* this gets added immediately */ fu_device_add_instance_id(device, "bazbarfoo"); g_assert_true(fu_device_has_guid(device, "77e49bb0-2cd6-5faf-bcee-5b7fbe6e944d")); } static void fu_device_composite_id_func(void) { g_autoptr(FuDevice) dev1 = fu_device_new(NULL); g_autoptr(FuDevice) dev2 = fu_device_new(NULL); g_autoptr(FuDevice) dev3 = fu_device_new(NULL); g_autoptr(FuDevice) dev4 = fu_device_new(NULL); /* single device */ fu_device_set_id(dev1, "dev1"); g_assert_cmpstr(fu_device_get_composite_id(dev1), ==, "3b42553c4e3241e8f3f8fbc19a69fa2f95708a9d"); fu_device_set_id(dev2, "dev2"); /* one child */ fu_device_add_child(dev1, dev2); g_assert_cmpstr(fu_device_get_composite_id(dev1), ==, "3b42553c4e3241e8f3f8fbc19a69fa2f95708a9d"); g_assert_cmpstr(fu_device_get_composite_id(dev2), ==, "3b42553c4e3241e8f3f8fbc19a69fa2f95708a9d"); /* add a different "family" */ fu_device_set_id(dev3, "dev3"); fu_device_set_id(dev4, "dev4"); fu_device_add_child(dev3, dev4); fu_device_add_child(dev2, dev3); g_assert_cmpstr(fu_device_get_composite_id(dev1), ==, "3b42553c4e3241e8f3f8fbc19a69fa2f95708a9d"); g_assert_cmpstr(fu_device_get_composite_id(dev2), ==, "3b42553c4e3241e8f3f8fbc19a69fa2f95708a9d"); g_assert_cmpstr(fu_device_get_composite_id(dev3), ==, "3b42553c4e3241e8f3f8fbc19a69fa2f95708a9d"); g_assert_cmpstr(fu_device_get_composite_id(dev4), ==, "3b42553c4e3241e8f3f8fbc19a69fa2f95708a9d"); /* change the parent ID */ fu_device_set_id(dev1, "dev1-NEW"); g_assert_cmpstr(fu_device_get_composite_id(dev1), ==, "a4c8efc6a0a58c2dc14c05fd33186703f7352997"); g_assert_cmpstr(fu_device_get_composite_id(dev2), ==, "a4c8efc6a0a58c2dc14c05fd33186703f7352997"); } static void fu_device_inhibit_func(void) { g_autoptr(FuDevice) device = fu_device_new(NULL); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_set_battery_threshold(device, 25); g_assert_true(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE)); g_assert_false(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN)); /* does not exist -> fine */ fu_device_uninhibit(device, "NOTGOINGTOEXIST"); g_assert_false(fu_device_has_inhibit(device, "NOTGOINGTOEXIST")); /* first one */ fu_device_inhibit(device, "needs-activation", "Device is pending activation"); g_assert_true(fu_device_has_inhibit(device, "needs-activation")); g_assert_true(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN)); g_assert_false(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE)); /* another */ fu_device_set_battery_level(device, 5); g_assert_true(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN)); g_assert_false(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE)); /* activated, power still too low */ fu_device_uninhibit(device, "needs-activation"); g_assert_false(fu_device_has_inhibit(device, "needs-activation")); g_assert_true(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN)); g_assert_false(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE)); /* we got some more power -> fine */ fu_device_set_battery_level(device, 95); g_assert_true(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE)); g_assert_false(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN)); } static void fu_device_inhibit_updateable_func(void) { g_autoptr(FuDevice) device = fu_device_new(NULL); g_assert_false(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE)); g_assert_false(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN)); g_assert_cmpstr(fu_device_get_update_error(device), ==, NULL); /* first one */ fu_device_inhibit(device, "needs-activation", "Device is pending activation"); g_assert_false(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN)); g_assert_false(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE)); g_assert_cmpstr(fu_device_get_update_error(device), ==, "Device is pending activation"); /* activated, but still not updatable */ fu_device_uninhibit(device, "needs-activation"); g_assert_false(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE)); g_assert_false(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN)); g_assert_cmpstr(fu_device_get_update_error(device), ==, NULL); } static void fu_device_custom_flags_func(void) { g_autofree gchar *tmp = NULL; g_autoptr(FuDevice) device = fu_device_new(NULL); fu_device_register_private_flag(device, "foo"); fu_device_register_private_flag(device, "bar"); fu_device_set_custom_flags(device, "foo"); g_assert_true(fu_device_has_private_flag(device, "foo")); fu_device_set_custom_flags(device, "bar"); g_assert_true(fu_device_has_private_flag(device, "foo")); g_assert_true(fu_device_has_private_flag(device, "bar")); fu_device_set_custom_flags(device, "~bar"); g_assert_true(fu_device_has_private_flag(device, "foo")); g_assert_false(fu_device_has_private_flag(device, "bar")); fu_device_set_custom_flags(device, "baz"); g_assert_true(fu_device_has_private_flag(device, "foo")); g_assert_false(fu_device_has_private_flag(device, "bar")); tmp = fu_device_to_string(device); g_assert_cmpstr(tmp, ==, "FuDevice:\n" " Flags: none\n" " AcquiesceDelay: 50\n" " CustomFlags: baz\n" " PrivateFlags: foo\n"); } static void fu_device_flags_func(void) { g_autoptr(FuDevice) device = fu_device_new(NULL); g_autoptr(FuDevice) proxy = fu_device_new(NULL); g_assert_cmpint(fu_device_get_flags(device), ==, FWUPD_DEVICE_FLAG_NONE); /* remove IS_BOOTLOADER if is a BOOTLOADER */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER); g_assert_cmpint(fu_device_get_flags(device), ==, FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER); fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER); /* check implication */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); g_assert_cmpint(fu_device_get_flags(device), ==, FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE | FWUPD_DEVICE_FLAG_CAN_VERIFY); fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_CAN_VERIFY | FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); /* negation */ fu_device_set_custom_flags(device, "is-bootloader,updatable"); g_assert_cmpint(fu_device_get_flags(device), ==, FWUPD_DEVICE_FLAG_IS_BOOTLOADER | FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_set_custom_flags(device, "~is-bootloader"); g_assert_cmpint(fu_device_get_flags(device), ==, FWUPD_DEVICE_FLAG_UPDATABLE); /* setting flags on the proxy should propagate to the device that *uses* the proxy */ fu_device_set_proxy(device, proxy); fu_device_add_flag(proxy, FWUPD_DEVICE_FLAG_EMULATED); g_assert_true(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)); } static void fu_device_children_func(void) { gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuDevice) child = fu_device_new(NULL); g_autoptr(FuDevice) parent = fu_device_new(ctx); g_autoptr(GError) error = NULL; /* do not save silo */ ret = fu_context_load_quirks(ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE, &error); g_assert_no_error(error); g_assert_true(ret); fu_device_set_physical_id(child, "dummy"); fu_device_set_physical_id(parent, "dummy"); /* set up family */ fu_device_add_child(parent, child); /* set an instance ID that will be converted to a GUID when the parent * calls ->setup */ fu_device_add_instance_id(child, "foo"); g_assert_false(fu_device_has_guid(child, "b84ed8ed-a7b1-502f-83f6-90132e68adef")); /* setup parent, which also calls setup on child too (and thus also * converts the instance ID to a GUID) */ ret = fu_device_setup(parent, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_true(fu_device_has_guid(child, "b84ed8ed-a7b1-502f-83f6-90132e68adef")); } static void fu_device_parent_func(void) { g_autoptr(FuDevice) child = fu_device_new(NULL); g_autoptr(FuDevice) child_root = NULL; g_autoptr(FuDevice) grandparent = fu_device_new(NULL); g_autoptr(FuDevice) grandparent_root = NULL; g_autoptr(FuDevice) parent = fu_device_new(NULL); g_autoptr(FuDevice) parent_root = NULL; fu_device_set_physical_id(child, "dummy"); fu_device_set_physical_id(grandparent, "dummy"); fu_device_set_physical_id(parent, "dummy"); /* set up three layer family */ fu_device_add_child(grandparent, parent); fu_device_add_child(parent, child); /* check parents */ g_assert_true(fu_device_get_parent(child) == parent); g_assert_true(fu_device_get_parent(parent) == grandparent); g_assert_true(fu_device_get_parent(grandparent) == NULL); /* check root */ child_root = fu_device_get_root(child); g_assert_true(child_root == grandparent); parent_root = fu_device_get_root(parent); g_assert_true(parent_root == grandparent); grandparent_root = fu_device_get_root(child); g_assert_true(grandparent_root == grandparent); } static void fu_device_incorporate_descendant_func(void) { g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuDevice) device = fu_device_new(ctx); g_autoptr(FuTestDevice) test_device = g_object_new(FU_TYPE_TEST_DEVICE, NULL); fu_device_set_name(device, "FuDevice"); fu_device_set_summary(FU_DEVICE(test_device), "FuTestDevice"); fu_device_incorporate(FU_DEVICE(test_device), device, FU_DEVICE_INCORPORATE_FLAG_ALL); g_assert_cmpstr(fu_device_get_name(FU_DEVICE(test_device)), ==, "FuDevice"); /* this won't explode as device_class->incorporate is checking types */ fu_device_incorporate(device, FU_DEVICE(test_device), FU_DEVICE_INCORPORATE_FLAG_ALL); g_assert_cmpstr(fu_device_get_summary(device), ==, "FuTestDevice"); } static void fu_device_incorporate_non_generic_func(void) { g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuDevice) device = fu_device_new(ctx); g_autoptr(FuDevice) donor = fu_device_new(ctx); fu_device_add_instance_id_full(donor, "USB\\VID_273F&PID_1004", FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_VISIBLE); fu_device_add_instance_id_full(donor, "USB\\VID_273F&PID_1004&CID_1234", FU_DEVICE_INSTANCE_FLAG_VISIBLE); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_NO_GENERIC_GUIDS); fu_device_incorporate(device, donor, FU_DEVICE_INCORPORATE_FLAG_INSTANCE_IDS); g_assert_false(fu_device_has_instance_id(device, "USB\\VID_273F&PID_1004", FU_DEVICE_INSTANCE_FLAG_VISIBLE)); g_assert_true(fu_device_has_instance_id(device, "USB\\VID_273F&PID_1004&CID_1234", FU_DEVICE_INSTANCE_FLAG_VISIBLE)); fu_device_convert_instance_ids(device); g_assert_false(fu_device_has_instance_id(device, "USB\\VID_273F&PID_1004", FU_DEVICE_INSTANCE_FLAG_VISIBLE)); g_assert_true(fu_device_has_instance_id(device, "USB\\VID_273F&PID_1004&CID_1234", FU_DEVICE_INSTANCE_FLAG_VISIBLE)); g_assert_false( fwupd_device_has_instance_id(FWUPD_DEVICE(device), "USB\\VID_273F&PID_1004")); g_assert_true( fwupd_device_has_instance_id(FWUPD_DEVICE(device), "USB\\VID_273F&PID_1004&CID_1234")); } static void fu_device_incorporate_flag_func(void) { g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuDevice) device = fu_device_new(ctx); g_autoptr(FuDevice) donor = fu_device_new(ctx); fu_device_set_logical_id(donor, "logi"); fu_device_set_physical_id(donor, "phys"); fu_device_add_vendor_id(donor, "PCI:0x1234"); fu_device_incorporate(device, donor, FU_DEVICE_INCORPORATE_FLAG_VENDOR_IDS | FU_DEVICE_INCORPORATE_FLAG_PHYSICAL_ID); g_assert_cmpstr(fu_device_get_physical_id(device), ==, "phys"); g_assert_cmpstr(fu_device_get_logical_id(device), ==, NULL); g_assert_true(fu_device_has_vendor_id(device, "PCI:0x1234")); fu_device_incorporate(device, donor, FU_DEVICE_INCORPORATE_FLAG_ALL); g_assert_cmpstr(fu_device_get_logical_id(device), ==, "logi"); } static void fu_device_incorporate_func(void) { gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuDevice) device = fu_device_new(ctx); g_autoptr(FuDevice) donor = fu_device_new(ctx); g_autoptr(GError) error = NULL; /* load quirks */ ret = fu_context_load_quirks(ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE, &error); g_assert_no_error(error); g_assert_true(ret); /* set up donor device */ fu_device_set_equivalent_id(donor, "0000000000000000000000000000000000000000"); fu_device_set_metadata(donor, "test", "me"); fu_device_set_metadata(donor, "test2", "me"); fu_device_add_instance_str(donor, "VID", "0A5C"); fu_device_add_instance_u16(donor, "PID", 0x6412); fu_device_add_instance_u32(donor, "BOARD_ID", 0x12345678); fu_device_register_private_flag(donor, "self-test"); fu_device_add_private_flag(donor, "self-test"); /* match a quirk entry, and then clear to ensure encorporate uses the quirk instance ID */ ret = fu_device_build_instance_id_full(donor, FU_DEVICE_INSTANCE_FLAG_QUIRKS, &error, "USB", "VID", "PID", NULL); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpstr(fu_device_get_custom_flags(donor), ==, "ignore-runtime"); fu_device_set_custom_flags(donor, "SHOULD_BE_REPLACED_WITH_QUIRK_VALUE"); /* base properties */ fu_device_add_flag(donor, FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_set_created_usec(donor, 1514338000ull * G_USEC_PER_SEC); fu_device_set_modified_usec(donor, 1514338999ull * G_USEC_PER_SEC); fu_device_add_icon(donor, "computer"); /* existing properties */ fu_device_set_equivalent_id(device, "ffffffffffffffffffffffffffffffffffffffff"); fu_device_set_metadata(device, "test2", "DO_NOT_OVERWRITE"); fu_device_set_modified_usec(device, 1514340000ull * G_USEC_PER_SEC); /* incorporate properties from donor to device */ fu_device_incorporate(device, donor, FU_DEVICE_INCORPORATE_FLAG_ALL); g_assert_cmpstr(fu_device_get_equivalent_id(device), ==, "ffffffffffffffffffffffffffffffffffffffff"); g_assert_cmpstr(fu_device_get_metadata(device, "test"), ==, "me"); g_assert_cmpstr(fu_device_get_metadata(device, "test2"), ==, "DO_NOT_OVERWRITE"); g_assert_true(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_REQUIRE_AC)); g_assert_cmpint(fu_device_get_created_usec(device), ==, 1514338000ull * G_USEC_PER_SEC); g_assert_cmpint(fu_device_get_modified_usec(device), ==, 1514340000ull * G_USEC_PER_SEC); g_assert_cmpint(fu_device_get_icons(device)->len, ==, 1); ret = fu_device_build_instance_id(device, &error, "USB", "VID", NULL); g_assert_no_error(error); g_assert_true(ret); g_assert_true( fu_device_has_instance_id(device, "USB\\VID_0A5C", FU_DEVICE_INSTANCE_FLAG_VISIBLE)); g_assert_cmpstr(fu_device_get_custom_flags(device), ==, "ignore-runtime"); } static void fu_backend_emulate_count_cb(FuBackend *backend, FuDevice *device, gpointer user_data) { guint *cnt = (guint *)user_data; (*cnt)++; } static void fu_backend_emulate_func(void) { gboolean ret; guint8 buf[] = {0x00, 0x00}; guint added_cnt = 0; guint changed_cnt = 0; guint removed_cnt = 0; FuDevice *device; g_autofree gchar *json3 = NULL; g_autoptr(FuBackend) backend = NULL; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuDevice) device2 = NULL; g_autoptr(FuIoctl) ioctl = NULL; g_autoptr(GError) error = NULL; const gchar *json1 = "{" " \"UsbDevices\" : [" " {" " \"Created\" : \"2023-02-01T16:35:03.302027Z\"," " \"GType\" : \"FuUdevDevice\",\n" " \"BackendId\" : \"foo:bar:baz\"," " \"Events\" : [" " {" " \"Id\" : \"Ioctl:Request=0x007b,Data=AAA=,Length=0x2\"," " \"Data\" : \"Aw==\"," " \"DataOut\" : \"Aw==\"" " }," " {" " \"Id\" : \"Ioctl:Request=0x007b,Data=AAA=,Length=0x2\"," " \"Data\" : \"Aw==\"," " \"DataOut\" : \"Aw==\"" " }" " ]" " }" " ]" "}"; const gchar *json2 = "{\n" " \"FwupdVersion\" : \"" PACKAGE_VERSION "\",\n" " \"UsbDevices\" : [\n" " {\n" " \"Created\" : \"2099-02-01T16:35:03Z\",\n" " \"GType\" : \"FuUdevDevice\",\n" " \"BackendId\" : \"usb:FF:FF:06\"\n" " }\n" " ]\n" "}"; /* watch events */ backend = g_object_new(FU_TYPE_BACKEND, "context", ctx, "name", "udev", "device-gtype", FU_TYPE_UDEV_DEVICE, NULL); g_signal_connect(FU_BACKEND(backend), "device-added", G_CALLBACK(fu_backend_emulate_count_cb), &added_cnt); g_signal_connect(FU_BACKEND(backend), "device-removed", G_CALLBACK(fu_backend_emulate_count_cb), &removed_cnt); g_signal_connect(FU_BACKEND(backend), "device-changed", G_CALLBACK(fu_backend_emulate_count_cb), &changed_cnt); /* parse */ ret = fwupd_codec_from_json_string(FWUPD_CODEC(backend), json1, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(added_cnt, ==, 1); g_assert_cmpint(removed_cnt, ==, 0); g_assert_cmpint(changed_cnt, ==, 0); /* get device */ device = fu_backend_lookup_by_id(backend, "foo:bar:baz"); g_assert_no_error(error); g_assert_nonnull(device); g_assert_true(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)); #ifndef HAVE_IOCTL_H g_test_skip("no support"); return; #endif /* in-order */ ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(device)); g_assert_nonnull(ioctl); ret = fu_ioctl_execute(ioctl, 123, buf, sizeof(buf), NULL, 0, FU_IOCTL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); /* in-order, repeat */ buf[0] = 0x00; buf[1] = 0x00; ret = fu_ioctl_execute(ioctl, 123, buf, sizeof(buf), NULL, 0, FU_IOCTL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); /* out-of-order */ buf[0] = 0x00; buf[1] = 0x00; ret = fu_ioctl_execute(ioctl, 123, buf, sizeof(buf), NULL, 0, FU_IOCTL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); /* load the same data */ ret = fwupd_codec_from_json_string(FWUPD_CODEC(backend), json1, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(added_cnt, ==, 1); g_assert_cmpint(removed_cnt, ==, 0); g_assert_cmpint(changed_cnt, ==, 1); device = fu_backend_lookup_by_id(backend, "foo:bar:baz"); g_assert_no_error(error); g_assert_nonnull(device); g_assert_true(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)); /* load a different device */ ret = fwupd_codec_from_json_string(FWUPD_CODEC(backend), json2, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(added_cnt, ==, 2); g_assert_cmpint(changed_cnt, ==, 1); g_assert_cmpint(removed_cnt, ==, 1); device = fu_backend_lookup_by_id(backend, "usb:FF:FF:06"); g_assert_no_error(error); g_assert_nonnull(device); /* save to string */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_EMULATION_TAG); json3 = fwupd_codec_to_json_string(FWUPD_CODEC(backend), FWUPD_CODEC_FLAG_NONE, &error); g_assert_no_error(error); g_assert_nonnull(json3); g_debug("%s", json3); g_assert_cmpstr(json3, ==, json2); /* missing event, new path */ fu_device_set_fwupd_version(device, PACKAGE_VERSION); device2 = fu_device_get_backend_parent_with_subsystem(device, "usb", &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND); g_assert_null(device2); } static void fu_backend_func(void) { FuDevice *dev; gboolean ret; g_autoptr(FuBackend) backend = g_object_new(FU_TYPE_BACKEND, NULL); g_autoptr(FuDevice) dev1 = fu_device_new(NULL); g_autoptr(FuDevice) dev2 = fu_device_new(NULL); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) devices = NULL; /* defaults */ g_assert_null(fu_backend_get_name(backend)); g_assert_true(fu_backend_get_enabled(backend)); /* load */ ret = fu_backend_setup(backend, FU_BACKEND_SETUP_FLAG_NONE, progress, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_backend_coldplug(backend, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* add two devices, then remove one of them */ fu_device_set_physical_id(dev1, "dev1"); fu_backend_device_added(backend, dev1); fu_device_set_physical_id(dev2, "dev2"); fu_backend_device_added(backend, dev2); fu_backend_device_changed(backend, dev2); fu_backend_device_removed(backend, dev2); dev = fu_backend_lookup_by_id(backend, "dev1"); g_assert_nonnull(dev); g_assert_true(dev == dev1); /* should have been removed */ dev = fu_backend_lookup_by_id(backend, "dev2"); g_assert_null(dev); /* get linear array */ devices = fu_backend_get_devices(backend); g_assert_nonnull(devices); g_assert_cmpint(devices->len, ==, 1); dev = g_ptr_array_index(devices, 0); g_assert_nonnull(dev); g_assert_true(dev == dev1); } static void fu_chunk_array_func(void) { g_autoptr(FuChunk) chk1 = NULL; g_autoptr(FuChunk) chk2 = NULL; g_autoptr(FuChunk) chk3 = NULL; g_autoptr(FuChunk) chk4 = NULL; g_autoptr(GError) error = NULL; g_autoptr(GBytes) fw = g_bytes_new_static("hello world", 11); g_autoptr(FuChunkArray) chunks = fu_chunk_array_new_from_bytes(fw, 100, FU_CHUNK_PAGESZ_NONE, 5); g_assert_cmpint(fu_chunk_array_length(chunks), ==, 3); chk1 = fu_chunk_array_index(chunks, 0, &error); g_assert_no_error(error); g_assert_nonnull(chk1); g_assert_cmpint(fu_chunk_get_idx(chk1), ==, 0x0); g_assert_cmpint(fu_chunk_get_address(chk1), ==, 100); g_assert_cmpint(fu_chunk_get_data_sz(chk1), ==, 0x5); g_assert_cmpint(strncmp((const gchar *)fu_chunk_get_data(chk1), "hello", 5), ==, 0); chk2 = fu_chunk_array_index(chunks, 1, &error); g_assert_no_error(error); g_assert_nonnull(chk2); g_assert_cmpint(fu_chunk_get_idx(chk2), ==, 0x1); g_assert_cmpint(fu_chunk_get_address(chk2), ==, 105); g_assert_cmpint(fu_chunk_get_data_sz(chk2), ==, 0x5); g_assert_cmpint(strncmp((const gchar *)fu_chunk_get_data(chk2), " world", 6), ==, 0); chk3 = fu_chunk_array_index(chunks, 2, &error); g_assert_no_error(error); g_assert_nonnull(chk3); g_assert_cmpint(fu_chunk_get_idx(chk3), ==, 0x2); g_assert_cmpint(fu_chunk_get_address(chk3), ==, 110); g_assert_cmpint(fu_chunk_get_data_sz(chk3), ==, 0x1); g_assert_cmpint(strncmp((const gchar *)fu_chunk_get_data(chk3), "d", 1), ==, 0); chk4 = fu_chunk_array_index(chunks, 3, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA); g_assert_null(chk4); chk4 = fu_chunk_array_index(chunks, 1024, NULL); g_assert_null(chk4); } static void fu_chunk_func(void) { g_autofree gchar *chunked1_str = NULL; g_autofree gchar *chunked2_str = NULL; g_autofree gchar *chunked3_str = NULL; g_autofree gchar *chunked4_str = NULL; g_autofree gchar *chunked5_str = NULL; g_autoptr(GPtrArray) chunked1 = NULL; g_autoptr(GPtrArray) chunked2 = NULL; g_autoptr(GPtrArray) chunked3 = NULL; g_autoptr(GPtrArray) chunked4 = NULL; g_autoptr(GPtrArray) chunked5 = NULL; chunked3 = fu_chunk_array_new((const guint8 *)"123456", 6, 0x0, 3, 3); chunked3_str = fu_chunk_array_to_string(chunked3); g_assert_cmpstr(chunked3_str, ==, "\n" " \n" " 123\n" " \n" " \n" " 0x1\n" " 0x1\n" " 456\n" " \n" "\n"); chunked4 = fu_chunk_array_new((const guint8 *)"123456", 6, 0x4, 4, 4); chunked4_str = fu_chunk_array_to_string(chunked4); g_assert_cmpstr(chunked4_str, ==, "\n" " \n" " 0x1\n" " 1234\n" " \n" " \n" " 0x1\n" " 0x2\n" " 56\n" " \n" "\n"); chunked5 = fu_chunk_array_new(NULL, 0, 0x0, 0x0, 4); g_assert_cmpint(chunked5->len, ==, 0); chunked5_str = fu_chunk_array_to_string(chunked5); if (fu_version_compare(xb_version_string(), "0.3.22", FWUPD_VERSION_FORMAT_TRIPLET) >= 0) { g_assert_cmpstr(chunked5_str, ==, "\n"); } else { g_assert_cmpstr(chunked5_str, ==, "\n\n"); } chunked1 = fu_chunk_array_new((const guint8 *)"0123456789abcdef", 16, 0x0, 10, 4); chunked1_str = fu_chunk_array_to_string(chunked1); g_assert_cmpstr(chunked1_str, ==, "\n" " \n" " 0123\n" " \n" " \n" " 0x1\n" " 0x4\n" " 4567\n" " \n" " \n" " 0x2\n" " 0x8\n" " 89\n" " \n" " \n" " 0x3\n" " 0x1\n" " abcd\n" " \n" " \n" " 0x4\n" " 0x1\n" " 0x4\n" " ef\n" " \n" "\n"); chunked2 = fu_chunk_array_new((const guint8 *)"XXXXXXYYYYYYZZZZZZ", 18, 0x0, 6, 4); chunked2_str = fu_chunk_array_to_string(chunked2); g_print("\n%s", chunked2_str); g_assert_cmpstr(chunked2_str, ==, "\n" " \n" " XXXX\n" " \n" " \n" " 0x1\n" " 0x4\n" " XX\n" " \n" " \n" " 0x2\n" " 0x1\n" " YYYY\n" " \n" " \n" " 0x3\n" " 0x1\n" " 0x4\n" " YY\n" " \n" " \n" " 0x4\n" " 0x2\n" " ZZZZ\n" " \n" " \n" " 0x5\n" " 0x2\n" " 0x4\n" " ZZ\n" " \n" "\n"); } static void fu_strstrip_func(void) { struct { const gchar *old; const gchar *new; } map[] = {{"same", "same"}, {" leading", "leading"}, {"tailing ", "tailing"}, {" b ", "b"}, {" ", ""}, {NULL, NULL}}; for (guint i = 0; map[i].old != NULL; i++) { g_autofree gchar *tmp = fu_strstrip(map[i].old); g_assert_cmpstr(tmp, ==, map[i].new); } } static void fu_version_semver_func(void) { struct { const gchar *old; const gchar *new; FwupdVersionFormat fmt; } map[] = {{"1.2.3", "1.2.3", FWUPD_VERSION_FORMAT_TRIPLET}, {"1.2.3.4", "1.2.3", FWUPD_VERSION_FORMAT_TRIPLET}, {"1.2", "0.1.2", FWUPD_VERSION_FORMAT_TRIPLET}, {"1", "0.0.1", FWUPD_VERSION_FORMAT_TRIPLET}, {"CBET1.2.3", "1.2.3", FWUPD_VERSION_FORMAT_TRIPLET}, {"4.11-1190-g12d8072e6b-dirty", "4.11.1190", FWUPD_VERSION_FORMAT_TRIPLET}, {"4.11-1190-g12d8072e6b-dirty", "4.11", FWUPD_VERSION_FORMAT_PAIR}, {NULL, NULL}}; for (guint i = 0; map[i].old != NULL; i++) { g_autofree gchar *tmp = fu_version_ensure_semver(map[i].old, map[i].fmt); g_assert_cmpstr(tmp, ==, map[i].new); } } static void fu_strtoull_func(void) { gboolean ret; guint64 val = 0; g_autoptr(GError) error = NULL; ret = fu_strtoull("123", &val, 123, 200, FU_INTEGER_BASE_AUTO, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(val, ==, 123); ret = fu_strtoull("123\n", &val, 0, 200, FU_INTEGER_BASE_AUTO, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(val, ==, 123); ret = fu_strtoull("0x123", &val, 0, 0x123, FU_INTEGER_BASE_AUTO, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(val, ==, 0x123); ret = fu_strtoull(NULL, &val, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, NULL); g_assert_false(ret); ret = fu_strtoull("", &val, 120, 123, FU_INTEGER_BASE_AUTO, NULL); g_assert_false(ret); ret = fu_strtoull("124", &val, 120, 123, FU_INTEGER_BASE_AUTO, NULL); g_assert_false(ret); ret = fu_strtoull("119", &val, 120, 123, FU_INTEGER_BASE_AUTO, NULL); g_assert_false(ret); } static void fu_strtoll_func(void) { gboolean ret; gint64 val = 0; g_autoptr(GError) error = NULL; ret = fu_strtoll("123", &val, 123, 200, FU_INTEGER_BASE_AUTO, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(val, ==, 123); ret = fu_strtoll("-123\n", &val, -123, 200, FU_INTEGER_BASE_AUTO, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(val, ==, -123); ret = fu_strtoll("0x123", &val, 0, 0x123, FU_INTEGER_BASE_AUTO, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(val, ==, 0x123); ret = fu_strtoll(NULL, &val, 0, G_MAXINT32, FU_INTEGER_BASE_AUTO, NULL); g_assert_false(ret); ret = fu_strtoll("", &val, 120, 123, FU_INTEGER_BASE_AUTO, NULL); g_assert_false(ret); ret = fu_strtoll("124", &val, 120, 123, FU_INTEGER_BASE_AUTO, NULL); g_assert_false(ret); ret = fu_strtoll("-124", &val, -123, 123, FU_INTEGER_BASE_AUTO, NULL); g_assert_false(ret); } static void fu_common_version_func(void) { guint i; struct { guint32 val; const gchar *ver; FwupdVersionFormat flags; } version_from_uint32[] = { {0x0, "0.0.0.0", FWUPD_VERSION_FORMAT_QUAD}, {0xff, "0.0.0.255", FWUPD_VERSION_FORMAT_QUAD}, {0xff01, "0.0.255.1", FWUPD_VERSION_FORMAT_QUAD}, {0xff0001, "0.255.0.1", FWUPD_VERSION_FORMAT_QUAD}, {0xff000100, "255.0.1.0", FWUPD_VERSION_FORMAT_QUAD}, {0x0, "0.0.0", FWUPD_VERSION_FORMAT_TRIPLET}, {0xff, "0.0.255", FWUPD_VERSION_FORMAT_TRIPLET}, {0xff01, "0.0.65281", FWUPD_VERSION_FORMAT_TRIPLET}, {0xff0001, "0.255.1", FWUPD_VERSION_FORMAT_TRIPLET}, {0xff000100, "255.0.256", FWUPD_VERSION_FORMAT_TRIPLET}, {0x0, "0", FWUPD_VERSION_FORMAT_NUMBER}, {0xff000100, "4278190336", FWUPD_VERSION_FORMAT_NUMBER}, {0x0, "11.0.0.0", FWUPD_VERSION_FORMAT_INTEL_ME}, {0xffffffff, "18.31.255.65535", FWUPD_VERSION_FORMAT_INTEL_ME}, {0x0b32057a, "11.11.50.1402", FWUPD_VERSION_FORMAT_INTEL_ME}, {0xb8320d84, "11.8.50.3460", FWUPD_VERSION_FORMAT_INTEL_ME2}, {0x00000741, "19.0.0.1857", FWUPD_VERSION_FORMAT_INTEL_CSME19}, {0x226a4b00, "137.2706.768", FWUPD_VERSION_FORMAT_SURFACE_LEGACY}, {0x6001988, "6.25.136", FWUPD_VERSION_FORMAT_SURFACE}, {0x00ff0001, "255.0.1", FWUPD_VERSION_FORMAT_DELL_BIOS}, {0x010f0201, "1.15.2", FWUPD_VERSION_FORMAT_DELL_BIOS_MSB}, {0xc8, "0x000000c8", FWUPD_VERSION_FORMAT_HEX}, }; struct { guint32 val; const gchar *ver; FwupdVersionFormat flags; } version_from_uint24[] = { {0x0, "0.0.0", FWUPD_VERSION_FORMAT_TRIPLET}, {0xff, "0.0.255", FWUPD_VERSION_FORMAT_TRIPLET}, {0x0, "0", FWUPD_VERSION_FORMAT_NUMBER}, {0xc8, "0x0000c8", FWUPD_VERSION_FORMAT_HEX}, }; struct { guint64 val; const gchar *ver; FwupdVersionFormat flags; } version_from_uint64[] = { {0x0, "0.0.0.0", FWUPD_VERSION_FORMAT_QUAD}, {0xff, "0.0.0.255", FWUPD_VERSION_FORMAT_QUAD}, {0xffffffffffffffff, "65535.65535.65535.65535", FWUPD_VERSION_FORMAT_QUAD}, {0xff, "0.255", FWUPD_VERSION_FORMAT_PAIR}, {0xffffffffffffffff, "4294967295.4294967295", FWUPD_VERSION_FORMAT_PAIR}, {0x0, "0", FWUPD_VERSION_FORMAT_NUMBER}, {0x11000000c8, "0x00000011000000c8", FWUPD_VERSION_FORMAT_HEX}, }; struct { guint16 val; const gchar *ver; FwupdVersionFormat flags; } version_from_uint16[] = { {0x0, "0.0", FWUPD_VERSION_FORMAT_PAIR}, {0xff, "0.255", FWUPD_VERSION_FORMAT_PAIR}, {0xff01, "255.1", FWUPD_VERSION_FORMAT_PAIR}, {0x0, "0.0", FWUPD_VERSION_FORMAT_BCD}, {0x0110, "1.10", FWUPD_VERSION_FORMAT_BCD}, {0x9999, "99.99", FWUPD_VERSION_FORMAT_BCD}, {0x0, "0", FWUPD_VERSION_FORMAT_NUMBER}, {0x1234, "4660", FWUPD_VERSION_FORMAT_NUMBER}, {0x1234, "1.2.52", FWUPD_VERSION_FORMAT_TRIPLET}, }; struct { const gchar *old; const gchar *new; } version_parse[] = { {"0", "0"}, {"0x1a", "0.0.26"}, {"257", "0.0.257"}, {"1.2.3", "1.2.3"}, {"0xff0001", "0.255.1"}, {"16711681", "0.255.1"}, {"20150915", "20150915"}, {"dave", "dave"}, {"0x1x", "0x1x"}, }; /* check version conversion */ for (i = 0; i < G_N_ELEMENTS(version_from_uint64); i++) { g_autofree gchar *ver = NULL; ver = fu_version_from_uint64(version_from_uint64[i].val, version_from_uint64[i].flags); g_assert_cmpstr(ver, ==, version_from_uint64[i].ver); } for (i = 0; i < G_N_ELEMENTS(version_from_uint32); i++) { g_autofree gchar *ver = NULL; ver = fu_version_from_uint32(version_from_uint32[i].val, version_from_uint32[i].flags); g_assert_cmpstr(ver, ==, version_from_uint32[i].ver); } for (i = 0; i < G_N_ELEMENTS(version_from_uint24); i++) { g_autofree gchar *ver = NULL; ver = fu_version_from_uint24(version_from_uint24[i].val, version_from_uint24[i].flags); g_assert_cmpstr(ver, ==, version_from_uint24[i].ver); } for (i = 0; i < G_N_ELEMENTS(version_from_uint16); i++) { g_autofree gchar *ver = NULL; ver = fu_version_from_uint16(version_from_uint16[i].val, version_from_uint16[i].flags); g_assert_cmpstr(ver, ==, version_from_uint16[i].ver); } /* check version parsing */ for (i = 0; i < G_N_ELEMENTS(version_parse); i++) { g_autofree gchar *ver = NULL; ver = fu_version_parse_from_format(version_parse[i].old, FWUPD_VERSION_FORMAT_TRIPLET); g_assert_cmpstr(ver, ==, version_parse[i].new); } } static void fu_common_vercmp_func(void) { /* same */ g_assert_cmpint(fu_version_compare("1.2.3", "1.2.3", FWUPD_VERSION_FORMAT_UNKNOWN), ==, 0); g_assert_cmpint( fu_version_compare("001.002.003", "001.002.003", FWUPD_VERSION_FORMAT_UNKNOWN), ==, 0); g_assert_cmpint(fu_version_compare("0x00000002", "0x2", FWUPD_VERSION_FORMAT_HEX), ==, 0); /* upgrade and downgrade */ g_assert_cmpint(fu_version_compare("1.2.3", "1.2.4", FWUPD_VERSION_FORMAT_UNKNOWN), <, 0); g_assert_cmpint( fu_version_compare("001.002.000", "001.002.009", FWUPD_VERSION_FORMAT_UNKNOWN), <, 0); g_assert_cmpint(fu_version_compare("1.2.3", "1.2.2", FWUPD_VERSION_FORMAT_UNKNOWN), >, 0); g_assert_cmpint( fu_version_compare("001.002.009", "001.002.000", FWUPD_VERSION_FORMAT_UNKNOWN), >, 0); /* unequal depth */ g_assert_cmpint(fu_version_compare("1.2.3", "1.2.3.1", FWUPD_VERSION_FORMAT_UNKNOWN), <, 0); g_assert_cmpint(fu_version_compare("1.2.3.1", "1.2.4", FWUPD_VERSION_FORMAT_UNKNOWN), <, 0); /* mixed-alpha-numeric */ g_assert_cmpint(fu_version_compare("1.2.3a", "1.2.3a", FWUPD_VERSION_FORMAT_UNKNOWN), ==, 0); g_assert_cmpint(fu_version_compare("1.2.3a", "1.2.3b", FWUPD_VERSION_FORMAT_UNKNOWN), <, 0); g_assert_cmpint(fu_version_compare("1.2.3b", "1.2.3a", FWUPD_VERSION_FORMAT_UNKNOWN), >, 0); /* alpha version append */ g_assert_cmpint(fu_version_compare("1.2.3", "1.2.3a", FWUPD_VERSION_FORMAT_UNKNOWN), <, 0); g_assert_cmpint(fu_version_compare("1.2.3a", "1.2.3", FWUPD_VERSION_FORMAT_UNKNOWN), >, 0); /* alpha only */ g_assert_cmpint(fu_version_compare("alpha", "alpha", FWUPD_VERSION_FORMAT_UNKNOWN), ==, 0); g_assert_cmpint(fu_version_compare("alpha", "beta", FWUPD_VERSION_FORMAT_UNKNOWN), <, 0); g_assert_cmpint(fu_version_compare("beta", "alpha", FWUPD_VERSION_FORMAT_UNKNOWN), >, 0); /* alpha-compare */ g_assert_cmpint(fu_version_compare("1.2a.3", "1.2a.3", FWUPD_VERSION_FORMAT_UNKNOWN), ==, 0); g_assert_cmpint(fu_version_compare("1.2a.3", "1.2b.3", FWUPD_VERSION_FORMAT_UNKNOWN), <, 0); g_assert_cmpint(fu_version_compare("1.2b.3", "1.2a.3", FWUPD_VERSION_FORMAT_UNKNOWN), >, 0); /* tilde is all-powerful */ g_assert_cmpint(fu_version_compare("1.2.3~rc1", "1.2.3~rc1", FWUPD_VERSION_FORMAT_UNKNOWN), ==, 0); g_assert_cmpint(fu_version_compare("1.2.3~rc1", "1.2.3", FWUPD_VERSION_FORMAT_UNKNOWN), <, 0); g_assert_cmpint(fu_version_compare("1.2.3", "1.2.3~rc1", FWUPD_VERSION_FORMAT_UNKNOWN), >, 0); g_assert_cmpint(fu_version_compare("1.2.3~rc2", "1.2.3~rc1", FWUPD_VERSION_FORMAT_UNKNOWN), >, 0); /* invalid */ g_assert_cmpint(fu_version_compare("1", NULL, FWUPD_VERSION_FORMAT_UNKNOWN), ==, G_MAXINT); g_assert_cmpint(fu_version_compare(NULL, "1", FWUPD_VERSION_FORMAT_UNKNOWN), ==, G_MAXINT); g_assert_cmpint(fu_version_compare(NULL, NULL, FWUPD_VERSION_FORMAT_UNKNOWN), ==, G_MAXINT); } static void fu_firmware_raw_aligned_func(void) { gboolean ret; g_autoptr(FuFirmware) firmware1 = fu_firmware_new(); g_autoptr(FuFirmware) firmware2 = fu_firmware_new(); g_autoptr(GError) error = NULL; g_autoptr(GBytes) blob = g_bytes_new_static("hello", 5); /* no alignment */ ret = fu_firmware_parse_bytes(firmware1, blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, &error); g_assert_no_error(error); g_assert_true(ret); /* invalid alignment */ fu_firmware_set_alignment(firmware2, FU_FIRMWARE_ALIGNMENT_4K); ret = fu_firmware_parse_bytes(firmware2, blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE); g_assert_false(ret); } static void fu_firmware_ihex_func(void) { const guint8 *data; gboolean ret; gsize len; g_autofree gchar *filename_hex = NULL; g_autofree gchar *str = NULL; g_autoptr(FuFirmware) firmware = fu_ihex_firmware_new(); g_autoptr(GBytes) data_fw = NULL; g_autoptr(GBytes) data_hex = NULL; g_autoptr(GError) error = NULL; /* load a Intel hex32 file */ filename_hex = g_test_build_filename(G_TEST_DIST, "tests", "ihex.builder.xml", NULL); ret = fu_firmware_build_from_filename(firmware, filename_hex, &error); g_assert_no_error(error); g_assert_true(ret); data_fw = fu_firmware_get_bytes(firmware, &error); g_assert_no_error(error); g_assert_nonnull(data_fw); g_assert_cmpint(g_bytes_get_size(data_fw), ==, 92); /* export a ihex file (which will be slightly different due to * non-continuous regions being expanded */ data_hex = fu_firmware_write(firmware, &error); g_assert_no_error(error); g_assert_nonnull(data_hex); data = g_bytes_get_data(data_hex, &len); str = g_strndup((const gchar *)data, len); g_assert_cmpstr(str, ==, ":100000004E6571756520706F72726F2071756973BE\n" ":100010007175616D206573742071756920646F6CF2\n" ":100020006F72656D20697073756D207175696120DF\n" ":10003000646F6C6F722073697420616D65742C201D\n" ":10004000636F6E73656374657475722C2061646987\n" ":0C00500070697363692076656C69740A3E\n" ":040000FD646176655F\n" ":00000001FF\n"); } static void fu_firmware_ihex_signed_func(void) { const guint8 *data; gboolean ret; gsize len; g_autofree gchar *filename = NULL; g_autoptr(FuFirmware) firmware = fu_ihex_firmware_new(); g_autoptr(GBytes) data_fw = NULL; g_autoptr(GBytes) data_sig = NULL; g_autoptr(GError) error = NULL; /* load a signed Intel hex32 file */ filename = g_test_build_filename(G_TEST_DIST, "tests", "ihex-signed.builder.xml", NULL); ret = fu_firmware_build_from_filename(firmware, filename, &error); g_assert_no_error(error); g_assert_true(ret); data_fw = fu_firmware_get_bytes(firmware, &error); g_assert_no_error(error); g_assert_nonnull(data_fw); g_assert_cmpint(g_bytes_get_size(data_fw), ==, 11); /* get the signed image */ data_sig = fu_firmware_get_image_by_id_bytes(firmware, FU_FIRMWARE_ID_SIGNATURE, &error); g_assert_no_error(error); g_assert_nonnull(data_sig); data = g_bytes_get_data(data_sig, &len); g_assert_cmpint(len, ==, 8); g_assert_nonnull(data); g_assert_cmpint(memcmp(data, "deadbeef", 8), ==, 0); } static void fu_firmware_ihex_offset_func(void) { const guint8 *data; gboolean ret; gsize len; g_autofree gchar *str = NULL; g_autoptr(FuFirmware) firmware = fu_ihex_firmware_new(); g_autoptr(FuFirmware) firmware_verify = fu_ihex_firmware_new(); g_autoptr(GBytes) data_bin = NULL; g_autoptr(GBytes) data_dummy = NULL; g_autoptr(GBytes) data_verify = NULL; g_autoptr(GError) error = NULL; /* add a 4 byte image in high memory */ data_dummy = g_bytes_new_static("foo", 4); fu_firmware_set_addr(firmware, 0x80000000); fu_firmware_set_bytes(firmware, data_dummy); data_bin = fu_firmware_write(firmware, &error); g_assert_no_error(error); g_assert_nonnull(data_bin); data = g_bytes_get_data(data_bin, &len); str = g_strndup((const gchar *)data, len); g_assert_cmpstr(str, ==, ":0200000480007A\n" ":04000000666F6F00B8\n" ":00000001FF\n"); /* check we can load it too */ ret = fu_firmware_parse_bytes(firmware_verify, data_bin, 0x0, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(fu_firmware_get_addr(firmware_verify), ==, 0x80000000); data_verify = fu_firmware_get_bytes(firmware_verify, &error); g_assert_no_error(error); g_assert_nonnull(data_verify); g_assert_cmpint(g_bytes_get_size(data_verify), ==, 0x4); } static void fu_firmware_srec_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autoptr(FuFirmware) firmware = fu_srec_firmware_new(); g_autoptr(GBytes) data_bin = NULL; g_autoptr(GError) error = NULL; filename = g_test_build_filename(G_TEST_DIST, "tests", "srec.builder.xml", NULL); ret = fu_firmware_build_from_filename(firmware, filename, &error); g_assert_no_error(error); g_assert_true(ret); data_bin = fu_firmware_get_bytes(firmware, &error); g_assert_no_error(error); g_assert_nonnull(data_bin); g_assert_cmpint(g_bytes_get_size(data_bin), ==, 11); } static void fu_firmware_fdt_func(void) { gboolean ret; guint32 val32 = 0; guint64 val64 = 0; g_autofree gchar *filename = NULL; g_autofree gchar *val = NULL; g_autofree gchar *str = NULL; g_autoptr(FuFirmware) firmware = fu_fdt_firmware_new(); g_autoptr(FuFirmware) img1 = NULL; g_autoptr(FuFdtImage) img2 = NULL; g_autoptr(GError) error = NULL; filename = g_test_build_filename(G_TEST_DIST, "tests", "fdt.builder.xml", NULL); ret = fu_firmware_build_from_filename(firmware, filename, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(fu_fdt_firmware_get_cpuid(FU_FDT_FIRMWARE(firmware)), ==, 0x0); str = fu_firmware_to_string(firmware); g_debug("%s", str); img1 = fu_firmware_get_image_by_id(firmware, NULL, &error); g_assert_no_error(error); g_assert_nonnull(img1); ret = fu_fdt_image_get_attr_str(FU_FDT_IMAGE(img1), "key", &val, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpstr(val, ==, "hello world"); /* get image, and get the uint32 attr */ img2 = fu_fdt_firmware_get_image_by_path(FU_FDT_FIRMWARE(firmware), "/images/firmware-1", &error); g_assert_no_error(error); g_assert_nonnull(img2); ret = fu_fdt_image_get_attr_u32(FU_FDT_IMAGE(img2), "key", &val32, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(val32, ==, 0x123); /* wrong type */ ret = fu_fdt_image_get_attr_u64(img2, "key", &val64, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA); g_assert_false(ret); } static void fu_firmware_fit_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autofree gchar *str = NULL; g_auto(GStrv) val = NULL; g_autoptr(FuFdtImage) img1 = NULL; g_autoptr(FuFirmware) firmware = fu_fit_firmware_new(); g_autoptr(GError) error = NULL; filename = g_test_build_filename(G_TEST_DIST, "tests", "fit.builder.xml", NULL); ret = fu_firmware_build_from_filename(firmware, filename, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(fu_fit_firmware_get_timestamp(FU_FIT_FIRMWARE(firmware)), ==, 0x629D4ABD); str = fu_firmware_to_string(firmware); g_debug("%s", str); img1 = fu_fdt_firmware_get_image_by_path(FU_FDT_FIRMWARE(firmware), "/configurations/conf-1", &error); g_assert_no_error(error); g_assert_nonnull(img1); ret = fu_fdt_image_get_attr_strlist(FU_FDT_IMAGE(img1), FU_FIT_FIRMWARE_ATTR_COMPATIBLE, &val, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_nonnull(val); g_assert_cmpstr(val[0], ==, "alice"); g_assert_cmpstr(val[1], ==, "bob"); g_assert_cmpstr(val[2], ==, "clara"); g_assert_cmpstr(val[3], ==, NULL); } static void fu_firmware_srec_tokenization_func(void) { FuSrecFirmwareRecord *rcd; GPtrArray *records; gboolean ret; g_autoptr(FuFirmware) firmware = fu_srec_firmware_new(); g_autoptr(GBytes) data_srec = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(GError) error = NULL; const gchar *buf = "S3060000001400E5\r\n" "S31000000002281102000000007F0304002C\r\n" "S306000000145095\r\n" "S70500000000FA\r\n"; data_srec = g_bytes_new_static(buf, strlen(buf)); g_assert_no_error(error); g_assert_nonnull(data_srec); stream = g_memory_input_stream_new_from_bytes(data_srec); ret = fu_firmware_tokenize(firmware, stream, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); records = fu_srec_firmware_get_records(FU_SREC_FIRMWARE(firmware)); g_assert_nonnull(records); g_assert_cmpint(records->len, ==, 4); rcd = g_ptr_array_index(records, 2); g_assert_nonnull(rcd); g_assert_cmpint(rcd->ln, ==, 0x3); g_assert_cmpint(rcd->kind, ==, 3); g_assert_cmpint(rcd->addr, ==, 0x14); g_assert_cmpint(rcd->buf->len, ==, 0x1); g_assert_cmpint(rcd->buf->data[0], ==, 0x50); } static void fu_firmware_build_func(void) { gboolean ret; g_autofree gchar *str = NULL; g_autoptr(FuFirmware) firmware = fu_firmware_new(); g_autoptr(FuFirmware) img = NULL; g_autoptr(GBytes) blob = NULL; g_autoptr(GBytes) blob2 = NULL; g_autoptr(GError) error = NULL; g_autoptr(XbBuilder) builder = xb_builder_new(); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_autoptr(XbNode) n = NULL; g_autoptr(XbSilo) silo = NULL; const gchar *buf = "\n" "\n" " 1.2.3\n" " \n" " 4.5.6\n" " header\n" " 456\n" " 0x456\n" " aGVsbG8=\n" " \n" " \n" " 7.8.9\n" " header\n" " 789\n" " 0x789\n" " \n" "\n"; blob = g_bytes_new_static(buf, strlen(buf)); g_assert_no_error(error); g_assert_nonnull(blob); /* parse XML */ ret = xb_builder_source_load_bytes(source, blob, XB_BUILDER_SOURCE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); xb_builder_import_source(builder, source); silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, &error); g_assert_no_error(error); g_assert_nonnull(silo); n = xb_silo_query_first(silo, "firmware", &error); g_assert_no_error(error); g_assert_nonnull(n); /* build object */ ret = fu_firmware_build(firmware, n, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpstr(fu_firmware_get_version(firmware), ==, "1.2.3"); /* verify image */ img = fu_firmware_get_image_by_id(firmware, "xxx|h?ad*", &error); g_assert_no_error(error); g_assert_nonnull(img); g_assert_cmpstr(fu_firmware_get_version(img), ==, "4.5.6"); g_assert_cmpint(fu_firmware_get_idx(img), ==, 456); g_assert_cmpint(fu_firmware_get_addr(img), ==, 0x456); blob2 = fu_firmware_write(img, &error); g_assert_no_error(error); g_assert_nonnull(blob2); g_assert_cmpint(g_bytes_get_size(blob2), ==, 5); str = g_strndup(g_bytes_get_data(blob2, NULL), g_bytes_get_size(blob2)); g_assert_cmpstr(str, ==, "hello"); } static gsize fu_test_firmware_dfuse_image_get_size(FuFirmware *self) { g_autoptr(GPtrArray) chunks = fu_firmware_get_chunks(self, NULL); gsize length = 0; for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); length += fu_chunk_get_data_sz(chk); } return length; } static gsize fu_test_firmware_dfuse_get_size(FuFirmware *firmware) { gsize length = 0; g_autoptr(GPtrArray) images = fu_firmware_get_images(firmware); for (guint i = 0; i < images->len; i++) { FuFirmware *image = g_ptr_array_index(images, i); length += fu_test_firmware_dfuse_image_get_size(image); } return length; } static void fu_firmware_dfuse_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autoptr(FuFirmware) firmware = fu_dfuse_firmware_new(); g_autoptr(GError) error = NULL; /* load a DfuSe firmware */ filename = g_test_build_filename(G_TEST_DIST, "tests", "dfuse.builder.xml", NULL); g_assert_nonnull(filename); ret = fu_firmware_build_from_filename(firmware, filename, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(fu_dfu_firmware_get_vid(FU_DFU_FIRMWARE(firmware)), ==, 0x1234); g_assert_cmpint(fu_dfu_firmware_get_pid(FU_DFU_FIRMWARE(firmware)), ==, 0x5678); g_assert_cmpint(fu_dfu_firmware_get_release(FU_DFU_FIRMWARE(firmware)), ==, 0x8642); g_assert_cmpint(fu_test_firmware_dfuse_get_size(firmware), ==, 0x21); } static void fu_firmware_fmap_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autofree gchar *csum = NULL; g_autofree gchar *img_str = NULL; g_autoptr(FuFirmware) firmware = fu_fmap_firmware_new(); g_autoptr(FuFirmware) img = NULL; g_autoptr(GBytes) img_blob = NULL; g_autoptr(GBytes) roundtrip = NULL; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) images = NULL; #ifndef HAVE_MEMMEM g_test_skip("no memmem()"); return; #endif /* load firmware */ filename = g_test_build_filename(G_TEST_DIST, "tests", "fmap-offset.builder.xml", NULL); g_assert_nonnull(filename); ret = fu_firmware_build_from_filename(firmware, filename, &error); g_assert_no_error(error); g_assert_true(ret); /* check image count */ images = fu_firmware_get_images(firmware); g_assert_cmpint(images->len, ==, 2); /* get a specific image */ img = fu_firmware_get_image_by_id(firmware, "FMAP", &error); g_assert_no_error(error); g_assert_nonnull(img); img_blob = fu_firmware_get_bytes(img, &error); g_assert_no_error(error); g_assert_nonnull(img_blob); g_assert_cmpint(g_bytes_get_size(img_blob), ==, 0xb); img_str = g_strndup(g_bytes_get_data(img_blob, NULL), g_bytes_get_size(img_blob)); g_assert_cmpstr(img_str, ==, "hello world"); /* can we roundtrip without losing data */ roundtrip = fu_firmware_write(firmware, &error); g_assert_no_error(error); g_assert_nonnull(roundtrip); csum = g_compute_checksum_for_bytes(G_CHECKSUM_SHA256, roundtrip); g_assert_cmpstr(csum, ==, "229fcd952264f42ae4853eda7e716cc5c1ae18e7f804a6ba39ab1dfde5737d7e"); } static void fu_firmware_new_from_gtypes_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autoptr(FuFirmware) firmware = fu_dfu_firmware_new(); g_autoptr(FuFirmware) firmware1 = NULL; g_autoptr(FuFirmware) firmware2 = NULL; g_autoptr(FuFirmware) firmware3 = NULL; g_autoptr(GBytes) fw = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(GError) error = NULL; filename = g_test_build_filename(G_TEST_DIST, "tests", "dfu.builder.xml", NULL); ret = fu_firmware_build_from_filename(firmware, filename, &error); g_assert_no_error(error); g_assert_true(ret); fw = fu_firmware_write(firmware, &error); g_assert_no_error(error); g_assert_nonnull(fw); stream = g_memory_input_stream_new_from_bytes(fw); g_assert_no_error(error); g_assert_nonnull(stream); /* dfu -> FuDfuFirmware */ firmware1 = fu_firmware_new_from_gtypes(stream, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error, FU_TYPE_SREC_FIRMWARE, FU_TYPE_DFUSE_FIRMWARE, FU_TYPE_DFU_FIRMWARE, G_TYPE_INVALID); g_assert_no_error(error); g_assert_nonnull(firmware1); g_assert_cmpstr(G_OBJECT_TYPE_NAME(firmware1), ==, "FuDfuFirmware"); /* dfu -> FuFirmware */ firmware2 = fu_firmware_new_from_gtypes(stream, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error, FU_TYPE_SREC_FIRMWARE, FU_TYPE_FIRMWARE, G_TYPE_INVALID); g_assert_no_error(error); g_assert_nonnull(firmware2); g_assert_cmpstr(G_OBJECT_TYPE_NAME(firmware2), ==, "FuFirmware"); /* dfu -> error */ firmware3 = fu_firmware_new_from_gtypes(stream, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error, FU_TYPE_SREC_FIRMWARE, G_TYPE_INVALID); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE); g_assert_null(firmware3); } static void fu_firmware_csv_func(void) { FuCsvEntry *entry_tmp; gboolean ret; g_autofree gchar *str = NULL; g_autoptr(FuFirmware) firmware = fu_csv_firmware_new(); g_autoptr(GBytes) blob = NULL; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) imgs = NULL; const gchar *data = "sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md\n" "grub,1,Free Software Foundation,grub,2.04,https://www.gnu.org/software/grub/\n"; fu_csv_firmware_add_column_id(FU_CSV_FIRMWARE(firmware), "$id"); fu_csv_firmware_add_column_id(FU_CSV_FIRMWARE(firmware), "component_generation"); fu_csv_firmware_add_column_id(FU_CSV_FIRMWARE(firmware), "vendor_name"); fu_csv_firmware_add_column_id(FU_CSV_FIRMWARE(firmware), "vendor_package_name"); fu_csv_firmware_add_column_id(FU_CSV_FIRMWARE(firmware), "vendor_version"); fu_csv_firmware_add_column_id(FU_CSV_FIRMWARE(firmware), "vendor_url"); g_assert_cmpstr(fu_csv_firmware_get_column_id(FU_CSV_FIRMWARE(firmware), 0), ==, "$id"); g_assert_cmpstr(fu_csv_firmware_get_column_id(FU_CSV_FIRMWARE(firmware), 1), ==, "component_generation"); g_assert_cmpstr(fu_csv_firmware_get_column_id(FU_CSV_FIRMWARE(firmware), 5), ==, "vendor_url"); g_assert_cmpstr(fu_csv_firmware_get_column_id(FU_CSV_FIRMWARE(firmware), 6), ==, NULL); blob = g_bytes_new(data, strlen(data)); ret = fu_firmware_parse_bytes(firmware, blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); str = fu_firmware_to_string(firmware); g_debug("%s", str); imgs = fu_firmware_get_images(firmware); g_assert_cmpint(imgs->len, ==, 2); entry_tmp = g_ptr_array_index(imgs, 1); g_assert_cmpstr(fu_firmware_get_id(FU_FIRMWARE(entry_tmp)), ==, "grub"); g_assert_cmpstr(fu_csv_entry_get_value_by_idx(entry_tmp, 0), ==, "grub"); g_assert_cmpstr(fu_csv_entry_get_value_by_idx(entry_tmp, 1), ==, "1"); g_assert_cmpstr(fu_csv_entry_get_value_by_column_id(entry_tmp, "vendor_version"), ==, "2.04"); } static void fu_firmware_archive_func(void) { gboolean ret; g_autofree gchar *fn = NULL; g_autoptr(FuFirmware) firmware = fu_archive_firmware_new(); g_autoptr(FuFirmware) img_asc = NULL; g_autoptr(FuFirmware) img_bin = NULL; g_autoptr(FuFirmware) img_both = NULL; g_autoptr(GError) error = NULL; g_autoptr(GFile) file = NULL; #ifndef HAVE_LIBARCHIVE g_test_skip("no libarchive support"); return; #endif fn = g_test_build_filename(G_TEST_BUILT, "tests", "firmware.zip", NULL); file = g_file_new_for_path(fn); ret = fu_firmware_parse_file(firmware, file, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(fu_archive_firmware_get_format(FU_ARCHIVE_FIRMWARE(firmware)), ==, FU_ARCHIVE_FORMAT_UNKNOWN); g_assert_cmpint(fu_archive_firmware_get_compression(FU_ARCHIVE_FIRMWARE(firmware)), ==, FU_ARCHIVE_COMPRESSION_UNKNOWN); img_bin = fu_archive_firmware_get_image_fnmatch(FU_ARCHIVE_FIRMWARE(firmware), "*.txt", &error); g_assert_no_error(error); g_assert_nonnull(img_bin); img_asc = fu_archive_firmware_get_image_fnmatch(FU_ARCHIVE_FIRMWARE(firmware), "*.txt.asc", &error); g_assert_no_error(error); g_assert_nonnull(img_asc); img_both = fu_archive_firmware_get_image_fnmatch(FU_ARCHIVE_FIRMWARE(firmware), "*.txt*", &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA); g_assert_null(img_both); } static void fu_firmware_linear_func(void) { gboolean ret; g_autoptr(FuFirmware) firmware1 = fu_linear_firmware_new(FU_TYPE_OPROM_FIRMWARE); g_autoptr(FuFirmware) firmware2 = fu_linear_firmware_new(FU_TYPE_OPROM_FIRMWARE); g_autoptr(GBytes) blob1 = g_bytes_new_static("XXXX", 4); g_autoptr(GBytes) blob2 = g_bytes_new_static("HELO", 4); g_autoptr(GBytes) blob3 = NULL; g_autoptr(FuFirmware) img1 = fu_oprom_firmware_new(); g_autoptr(FuFirmware) img2 = fu_oprom_firmware_new(); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) imgs = NULL; g_autofree gchar *str = NULL; /* add images then parse */ fu_firmware_set_bytes(img1, blob1); fu_firmware_add_image(firmware1, img1); fu_firmware_set_bytes(img2, blob2); fu_firmware_add_image(firmware1, img2); blob3 = fu_firmware_write(firmware1, &error); g_assert_no_error(error); g_assert_nonnull(blob3); g_assert_cmpint(g_bytes_get_size(blob3), ==, 1024); /* parse them back */ ret = fu_firmware_parse_bytes(firmware2, blob3, 0x0, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, &error); g_assert_no_error(error); g_assert_true(ret); str = fu_firmware_to_string(firmware2); g_debug("\n%s", str); /* verify we got both images */ imgs = fu_firmware_get_images(firmware2); g_assert_cmpint(imgs->len, ==, 2); } static void fu_firmware_dfu_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autoptr(FuFirmware) firmware = fu_dfu_firmware_new(); g_autoptr(GBytes) data_bin = NULL; g_autoptr(GError) error = NULL; filename = g_test_build_filename(G_TEST_DIST, "tests", "dfu.builder.xml", NULL); ret = fu_firmware_build_from_filename(firmware, filename, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(fu_dfu_firmware_get_vid(FU_DFU_FIRMWARE(firmware)), ==, 0x1234); g_assert_cmpint(fu_dfu_firmware_get_pid(FU_DFU_FIRMWARE(firmware)), ==, 0x4321); g_assert_cmpint(fu_dfu_firmware_get_release(FU_DFU_FIRMWARE(firmware)), ==, 0xdead); data_bin = fu_firmware_get_bytes(firmware, &error); g_assert_no_error(error); g_assert_nonnull(data_bin); g_assert_cmpint(g_bytes_get_size(data_bin), ==, 12); } static void fu_firmware_ifwi_cpd_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autoptr(FuFirmware) firmware = fu_ifwi_cpd_firmware_new(); g_autoptr(FuFirmware) img1 = NULL; g_autoptr(FuFirmware) img2 = NULL; g_autoptr(GBytes) data_bin = NULL; g_autoptr(GError) error = NULL; filename = g_test_build_filename(G_TEST_DIST, "tests", "ifwi-cpd.builder.xml", NULL); ret = fu_firmware_build_from_filename(firmware, filename, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(fu_firmware_get_idx(firmware), ==, 0x1234); data_bin = fu_firmware_write(firmware, &error); g_assert_no_error(error); g_assert_nonnull(data_bin); g_assert_cmpint(g_bytes_get_size(data_bin), ==, 90); img1 = fu_firmware_get_image_by_id(firmware, "one", &error); g_assert_no_error(error); g_assert_nonnull(img1); g_assert_cmpint(fu_firmware_get_offset(img1), ==, 68); g_assert_cmpint(fu_firmware_get_size(img1), ==, 11); img2 = fu_firmware_get_image_by_id(firmware, "two", &error); g_assert_no_error(error); g_assert_nonnull(img2); g_assert_cmpint(fu_firmware_get_offset(img2), ==, 79); g_assert_cmpint(fu_firmware_get_size(img2), ==, 11); } static void fu_firmware_ifwi_fpt_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autoptr(FuFirmware) firmware = fu_ifwi_fpt_firmware_new(); g_autoptr(FuFirmware) img1 = NULL; g_autoptr(FuFirmware) img2 = NULL; g_autoptr(GBytes) data_bin = NULL; g_autoptr(GError) error = NULL; filename = g_test_build_filename(G_TEST_DIST, "tests", "ifwi-fpt.builder.xml", NULL); ret = fu_firmware_build_from_filename(firmware, filename, &error); g_assert_no_error(error); g_assert_true(ret); data_bin = fu_firmware_write(firmware, &error); g_assert_no_error(error); g_assert_nonnull(data_bin); g_assert_cmpint(g_bytes_get_size(data_bin), ==, 118); img1 = fu_firmware_get_image_by_idx(firmware, 0x4f464e49, &error); g_assert_no_error(error); g_assert_nonnull(img1); g_assert_cmpint(fu_firmware_get_offset(img1), ==, 96); g_assert_cmpint(fu_firmware_get_size(img1), ==, 11); img2 = fu_firmware_get_image_by_idx(firmware, 0x4d495746, &error); g_assert_no_error(error); g_assert_nonnull(img2); g_assert_cmpint(fu_firmware_get_offset(img2), ==, 107); g_assert_cmpint(fu_firmware_get_size(img2), ==, 11); } static void fu_firmware_oprom_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autoptr(FuFirmware) firmware1 = fu_oprom_firmware_new(); g_autoptr(FuFirmware) firmware2 = fu_oprom_firmware_new(); g_autoptr(FuFirmware) img1 = NULL; g_autoptr(GBytes) data_bin = NULL; g_autoptr(GError) error = NULL; filename = g_test_build_filename(G_TEST_DIST, "tests", "oprom.builder.xml", NULL); ret = fu_firmware_build_from_filename(firmware1, filename, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(fu_firmware_get_idx(firmware1), ==, 0x1); data_bin = fu_firmware_write(firmware1, &error); g_assert_no_error(error); g_assert_nonnull(data_bin); g_assert_cmpint(g_bytes_get_size(data_bin), ==, 1024); /* re-parse to get the CPD image */ ret = fu_firmware_parse_bytes(firmware2, data_bin, 0x0, FU_FIRMWARE_PARSE_FLAG_CACHE_STREAM, &error); g_assert_no_error(error); g_assert_true(ret); img1 = fu_firmware_get_image_by_id(firmware2, "cpd", &error); g_assert_no_error(error); g_assert_nonnull(img1); g_assert_cmpint(fu_firmware_get_offset(img1), ==, 512); g_assert_cmpint(fu_firmware_get_size(img1), ==, 512); } static void fu_firmware_dfu_patch_func(void) { gboolean ret; g_autofree gchar *csum = NULL; g_autofree gchar *filename = NULL; g_autoptr(FuFirmware) firmware = fu_dfu_firmware_new(); g_autoptr(GBytes) data_new = NULL; g_autoptr(GBytes) data_patch0 = g_bytes_new_static("XXXX", 4); g_autoptr(GBytes) data_patch1 = g_bytes_new_static("HELO", 4); g_autoptr(GError) error = NULL; filename = g_test_build_filename(G_TEST_DIST, "tests", "dfu.builder.xml", NULL); ret = fu_firmware_build_from_filename(firmware, filename, &error); g_assert_no_error(error); g_assert_true(ret); /* add a couple of patches */ fu_firmware_add_patch(firmware, 0x0, data_patch0); fu_firmware_add_patch(firmware, 0x0, data_patch1); fu_firmware_add_patch(firmware, 0x8, data_patch1); data_new = fu_firmware_write(firmware, &error); g_assert_no_error(error); g_assert_nonnull(data_new); fu_dump_full(G_LOG_DOMAIN, "patch", g_bytes_get_data(data_new, NULL), g_bytes_get_size(data_new), 20, FU_DUMP_FLAGS_SHOW_ASCII | FU_DUMP_FLAGS_SHOW_ADDRESSES); csum = g_compute_checksum_for_bytes(G_CHECKSUM_SHA1, data_new); g_assert_cmpstr(csum, ==, "676c039e8cb1d2f51831fcb77be36db24bb8ecf8"); } static void fu_hid_descriptor_container_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autoptr(FuFirmware) firmware = fu_hid_descriptor_new(); g_autoptr(FuFirmware) item_id = NULL; g_autoptr(FuHidReport) report = NULL; g_autoptr(GError) error = NULL; filename = g_test_build_filename(G_TEST_DIST, "tests", "hid-descriptor2.builder.xml", NULL); ret = fu_firmware_build_from_filename(firmware, filename, &error); g_assert_no_error(error); g_assert_true(ret); /* find report-id from usage */ report = fu_hid_descriptor_find_report(FU_HID_DESCRIPTOR(firmware), &error, "usage-page", 0xFF02, "usage", 0x01, "feature", 0x02, NULL); g_assert_no_error(error); g_assert_nonnull(report); item_id = fu_firmware_get_image_by_id(FU_FIRMWARE(report), "report-id", &error); g_assert_no_error(error); g_assert_nonnull(item_id); g_assert_cmpint(fu_hid_report_item_get_value(FU_HID_REPORT_ITEM(item_id)), ==, 0x09); } static void fu_hid_descriptor_func(void) { gboolean ret; g_autoptr(FuFirmware) firmware = fu_hid_descriptor_new(); g_autoptr(FuHidReport) report1 = NULL; g_autoptr(FuHidReport) report2 = NULL; g_autoptr(FuHidReport) report3 = NULL; g_autoptr(FuHidReport) report4 = NULL; g_autoptr(FuFirmware) item_usage = NULL; g_autoptr(FuFirmware) item_id = NULL; g_autoptr(GError) error = NULL; g_autofree gchar *filename = NULL; filename = g_test_build_filename(G_TEST_DIST, "tests", "hid-descriptor.builder.xml", NULL); ret = fu_firmware_build_from_filename(firmware, filename, &error); g_assert_no_error(error); g_assert_true(ret); /* find report-id from usage */ report4 = fu_hid_descriptor_find_report(FU_HID_DESCRIPTOR(firmware), &error, "usage", 0xC8, NULL); g_assert_no_error(error); g_assert_nonnull(report4); item_id = fu_firmware_get_image_by_id(FU_FIRMWARE(report4), "report-id", &error); g_assert_no_error(error); g_assert_nonnull(item_id); g_assert_cmpint(fu_hid_report_item_get_value(FU_HID_REPORT_ITEM(item_id)), ==, 0xF1); /* find usage from report-id */ report1 = fu_hid_descriptor_find_report(FU_HID_DESCRIPTOR(firmware), &error, "report-id", 0xF1, NULL); g_assert_no_error(error); g_assert_nonnull(report1); report2 = fu_hid_descriptor_find_report(FU_HID_DESCRIPTOR(firmware), &error, "usage-page", 0xFF0B, "report-id", 0xF1, NULL); g_assert_no_error(error); g_assert_nonnull(report2); item_usage = fu_firmware_get_image_by_id(FU_FIRMWARE(report2), "usage", &error); g_assert_no_error(error); g_assert_nonnull(item_usage); g_assert_cmpint(fu_hid_report_item_get_value(FU_HID_REPORT_ITEM(item_usage)), ==, 0xC8); /* not found */ report3 = fu_hid_descriptor_find_report(FU_HID_DESCRIPTOR(firmware), &error, "usage-page", 0x1234, "report-id", 0xF1, NULL); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND); g_assert_null(report3); } static void fu_firmware_func(void) { gboolean ret; g_autoptr(FuFirmware) firmware = fu_firmware_new(); g_autoptr(FuFirmware) img1 = fu_firmware_new(); g_autoptr(FuFirmware) img2 = fu_firmware_new(); g_autoptr(FuFirmware) img_id = NULL; g_autoptr(FuFirmware) img_idx = NULL; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) images = NULL; g_autofree gchar *str = NULL; fu_firmware_set_addr(img1, 0x200); fu_firmware_set_idx(img1, 13); fu_firmware_set_id(img1, "primary"); fu_firmware_set_filename(img1, "BIOS.bin"); fu_firmware_add_image(firmware, img1); fu_firmware_set_addr(img2, 0x400); fu_firmware_set_idx(img2, 23); fu_firmware_set_id(img2, "secondary"); fu_firmware_add_image(firmware, img2); /* check depth */ g_assert_cmpint(fu_firmware_get_depth(firmware), ==, 0); g_assert_cmpint(fu_firmware_get_depth(img1), ==, 1); g_assert_cmpint(fu_firmware_get_depth(img2), ==, 1); img_id = fu_firmware_get_image_by_id(firmware, "NotGoingToExist", &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND); g_assert_null(img_id); g_clear_error(&error); img_id = fu_firmware_get_image_by_id(firmware, "primary", &error); g_assert_no_error(error); g_assert_nonnull(img_id); g_assert_cmpint(fu_firmware_get_addr(img_id), ==, 0x200); g_assert_cmpint(fu_firmware_get_idx(img_id), ==, 13); g_assert_cmpstr(fu_firmware_get_id(img_id), ==, "primary"); img_idx = fu_firmware_get_image_by_idx(firmware, 123456, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND); g_assert_null(img_idx); g_clear_error(&error); img_idx = fu_firmware_get_image_by_idx(firmware, 23, &error); g_assert_no_error(error); g_assert_nonnull(img_idx); g_assert_cmpint(fu_firmware_get_addr(img_idx), ==, 0x400); g_assert_cmpint(fu_firmware_get_idx(img_idx), ==, 23); g_assert_cmpstr(fu_firmware_get_id(img_idx), ==, "secondary"); str = fu_firmware_to_string(firmware); g_assert_cmpstr(str, ==, "\n" " \n" " primary\n" " 0xd\n" " 0x200\n" " BIOS.bin\n" " \n" " \n" " secondary\n" " 0x17\n" " 0x400\n" " \n" "\n"); ret = fu_firmware_remove_image_by_idx(firmware, 0xd, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_firmware_remove_image_by_id(firmware, "secondary", &error); g_assert_no_error(error); g_assert_true(ret); images = fu_firmware_get_images(firmware); g_assert_nonnull(images); g_assert_cmpint(images->len, ==, 0); ret = fu_firmware_remove_image_by_id(firmware, "NOTGOINGTOEXIST", &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND); g_assert_false(ret); } static void fu_firmware_convert_version_func(void) { g_autoptr(FuFirmware) firmware = fu_intel_thunderbolt_nvm_new(); fu_firmware_set_version_raw(firmware, 0x1234); g_assert_cmpstr(fu_firmware_get_version(firmware), ==, "12.34"); } static void fu_firmware_common_func(void) { gboolean ret; guint8 value = 0; g_autoptr(GError) error = NULL; ret = fu_firmware_strparse_uint8_safe("ff00XX", 6, 0, &value, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(value, ==, 0xFF); ret = fu_firmware_strparse_uint8_safe("ff00XX", 6, 2, &value, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(value, ==, 0x00); ret = fu_firmware_strparse_uint8_safe("ff00XX", 6, 4, &value, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA); g_assert_false(ret); } static void fu_firmware_dedupe_func(void) { gboolean ret; g_autoptr(FuFirmware) firmware = fu_firmware_new(); g_autoptr(FuFirmware) img1 = fu_firmware_new(); g_autoptr(FuFirmware) img1_old = fu_firmware_new(); g_autoptr(FuFirmware) img2 = fu_firmware_new(); g_autoptr(FuFirmware) img2_old = fu_firmware_new(); g_autoptr(FuFirmware) img3 = fu_firmware_new(); g_autoptr(FuFirmware) img_id = NULL; g_autoptr(FuFirmware) img_idx = NULL; g_autoptr(GError) error = NULL; fu_firmware_add_flag(firmware, FU_FIRMWARE_FLAG_DEDUPE_ID); fu_firmware_add_flag(firmware, FU_FIRMWARE_FLAG_DEDUPE_IDX); fu_firmware_set_images_max(firmware, 2); fu_firmware_set_idx(img1_old, 13); fu_firmware_set_id(img1_old, "DAVE"); fu_firmware_add_image(firmware, img1_old); g_assert_true(fu_firmware_get_parent(img1_old) == firmware); fu_firmware_set_idx(img1, 13); fu_firmware_set_id(img1, "primary"); fu_firmware_add_image(firmware, img1); fu_firmware_set_idx(img2_old, 123456); fu_firmware_set_id(img2_old, "secondary"); fu_firmware_add_image(firmware, img2_old); fu_firmware_set_idx(img2, 23); fu_firmware_set_id(img2, "secondary"); fu_firmware_add_image(firmware, img2); img_id = fu_firmware_get_image_by_id(firmware, "primary", &error); g_assert_no_error(error); g_assert_nonnull(img_id); g_assert_cmpint(fu_firmware_get_idx(img_id), ==, 13); g_assert_cmpstr(fu_firmware_get_id(img_id), ==, "primary"); img_idx = fu_firmware_get_image_by_idx(firmware, 23, &error); g_assert_no_error(error); g_assert_nonnull(img_idx); g_assert_cmpint(fu_firmware_get_idx(img_idx), ==, 23); g_assert_cmpstr(fu_firmware_get_id(img_idx), ==, "secondary"); ret = fu_firmware_add_image_full(firmware, img3, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA); g_assert_false(ret); } static void fu_efivar_func(void) { gboolean ret; gsize sz = 0; guint32 attr = 0; guint64 total; g_autofree guint8 *data = NULL; g_autoptr(FuEfivars) efivars = fu_dummy_efivars_new(); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) names = NULL; /* check supported */ ret = fu_efivars_supported(efivars, &error); g_assert_no_error(error); g_assert_true(ret); /* write and read a key */ ret = fu_efivars_set_data(efivars, FU_EFIVARS_GUID_EFI_GLOBAL, "Test", (guint8 *)"1", 1, FU_EFIVARS_ATTR_NON_VOLATILE | FU_EFIVARS_ATTR_RUNTIME_ACCESS, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_efivars_get_data(efivars, FU_EFIVARS_GUID_EFI_GLOBAL, "Test", &data, &sz, &attr, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(sz, ==, 1); g_assert_cmpint(attr, ==, FU_EFIVARS_ATTR_NON_VOLATILE | FU_EFIVARS_ATTR_RUNTIME_ACCESS); g_assert_cmpint(data[0], ==, '1'); /* check existing keys */ g_assert_false(fu_efivars_exists(efivars, FU_EFIVARS_GUID_EFI_GLOBAL, "NotGoingToExist")); g_assert_true(fu_efivars_exists(efivars, FU_EFIVARS_GUID_EFI_GLOBAL, "Test")); /* list a few keys */ names = fu_efivars_get_names(efivars, FU_EFIVARS_GUID_EFI_GLOBAL, &error); g_assert_no_error(error); g_assert_nonnull(names); g_assert_cmpint(names->len, ==, 1); /* check we can get the space used */ total = fu_efivars_space_used(efivars, &error); g_assert_no_error(error); g_assert_cmpint(total, >=, 0x10); /* delete single key */ ret = fu_efivars_delete(efivars, FU_EFIVARS_GUID_EFI_GLOBAL, "Test", &error); g_assert_no_error(error); g_assert_true(ret); g_assert_false(fu_efivars_exists(efivars, FU_EFIVARS_GUID_EFI_GLOBAL, "Test")); g_assert_false(fu_efivars_delete(efivars, FU_EFIVARS_GUID_EFI_GLOBAL, "Test", NULL)); /* delete multiple keys */ ret = fu_efivars_set_data(efivars, FU_EFIVARS_GUID_EFI_GLOBAL, "Test1", (guint8 *)"1", 1, 0, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_efivars_set_data(efivars, FU_EFIVARS_GUID_EFI_GLOBAL, "Test2", (guint8 *)"1", 1, 0, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_efivars_delete_with_glob(efivars, FU_EFIVARS_GUID_EFI_GLOBAL, "Test*", &error); g_assert_no_error(error); g_assert_true(ret); g_assert_false(fu_efivars_exists(efivars, FU_EFIVARS_GUID_EFI_GLOBAL, "Test1")); g_assert_false(fu_efivars_exists(efivars, FU_EFIVARS_GUID_EFI_GLOBAL, "Test2")); /* read a key that doesn't exist */ ret = fu_efivars_get_data(efivars, FU_EFIVARS_GUID_EFI_GLOBAL, "NotGoingToExist", NULL, NULL, NULL, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND); g_assert_false(ret); } static void fu_efivar_boot_func(void) { FuFirmware *firmware_tmp; gboolean ret; const gchar *tmpdir = g_getenv("FWUPD_LOCALSTATEDIR"); guint16 idx = 0; g_autofree gchar *pefile_fn = g_build_filename(tmpdir, "grubx64.efi", NULL); g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuEfiLoadOption) loadopt2 = NULL; g_autoptr(FuVolume) volume = fu_volume_new_from_mount_path(tmpdir); g_autoptr(GArray) bootorder2 = NULL; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) entries = NULL; g_autoptr(GPtrArray) esp_files = NULL; FuEfivars *efivars = fu_context_get_efivars(ctx); /* set and get BootCurrent */ ret = fu_efivars_set_boot_current(efivars, 0x0001, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_efivars_get_boot_current(efivars, &idx, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(idx, ==, 0x0001); /* set and get BootNext */ ret = fu_efivars_set_boot_next(efivars, 0x0002, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_efivars_get_boot_next(efivars, &idx, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(idx, ==, 0x0002); /* set and get BootOrder */ ret = fu_efivars_build_boot_order(efivars, &error, 0x0001, 0x0002, G_MAXUINT16); g_assert_no_error(error); g_assert_true(ret); bootorder2 = fu_efivars_get_boot_order(efivars, &error); g_assert_no_error(error); g_assert_nonnull(bootorder2); g_assert_cmpint(bootorder2->len, ==, 2); idx = g_array_index(bootorder2, guint16, 0); g_assert_cmpint(idx, ==, 0x0001); idx = g_array_index(bootorder2, guint16, 1); g_assert_cmpint(idx, ==, 0x0002); /* add a plausible ESP */ fu_volume_set_partition_kind(volume, FU_VOLUME_KIND_ESP); fu_volume_set_partition_uuid(volume, "41f5e9b7-eb4f-5c65-b8a6-f94b0ad54815"); fu_context_add_esp_volume(ctx, volume); /* create Boot0001 and Boot0002 */ ret = fu_efivars_create_boot_entry_for_volume(efivars, 0x0001, volume, "Fedora", "grubx64.efi", &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_efivars_create_boot_entry_for_volume(efivars, 0x0002, volume, "Firmware Update", "fwupdx64.efi", &error); g_assert_no_error(error); g_assert_true(ret); /* check BootXXXX exists */ loadopt2 = fu_efivars_get_boot_entry(efivars, 0x0001, &error); g_assert_no_error(error); g_assert_nonnull(loadopt2); entries = fu_efivars_get_boot_entries(efivars, &error); g_assert_no_error(error); g_assert_nonnull(bootorder2); g_assert_cmpint(bootorder2->len, ==, 2); /* check we detected something */ esp_files = fu_context_get_esp_files(ctx, FU_CONTEXT_ESP_FILE_FLAG_INCLUDE_FIRST_STAGE, &error); g_assert_no_error(error); g_assert_nonnull(esp_files); g_assert_cmpint(esp_files->len, ==, 2); firmware_tmp = g_ptr_array_index(esp_files, 0); g_assert_cmpstr(fu_firmware_get_filename(firmware_tmp), ==, pefile_fn); } typedef struct { guint cnt_success; guint cnt_failed; } FuDeviceRetryHelper; static gboolean fu_device_retry_success(FuDevice *device, gpointer user_data, GError **error) { FuDeviceRetryHelper *helper = (FuDeviceRetryHelper *)user_data; helper->cnt_success++; return TRUE; } static gboolean fu_device_retry_failed(FuDevice *device, gpointer user_data, GError **error) { FuDeviceRetryHelper *helper = (FuDeviceRetryHelper *)user_data; helper->cnt_failed++; g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed"); return FALSE; } static gboolean fu_device_retry_success_3rd_try(FuDevice *device, gpointer user_data, GError **error) { FuDeviceRetryHelper *helper = (FuDeviceRetryHelper *)user_data; if (helper->cnt_failed == 2) { helper->cnt_success++; return TRUE; } helper->cnt_failed++; g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed"); return FALSE; } static void fu_device_retry_success_func(void) { gboolean ret; g_autoptr(FuDevice) device = fu_device_new(NULL); g_autoptr(GError) error = NULL; FuDeviceRetryHelper helper = { .cnt_success = 0, .cnt_failed = 0, }; fu_device_retry_add_recovery(device, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, fu_device_retry_failed); ret = fu_device_retry(device, fu_device_retry_success, 3, &helper, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(helper.cnt_success, ==, 1); g_assert_cmpint(helper.cnt_failed, ==, 0); } static void fu_device_retry_failed_func(void) { gboolean ret; g_autoptr(FuDevice) device = fu_device_new(NULL); g_autoptr(GError) error = NULL; FuDeviceRetryHelper helper = { .cnt_success = 0, .cnt_failed = 0, }; fu_device_retry_add_recovery(device, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, fu_device_retry_success); ret = fu_device_retry(device, fu_device_retry_failed, 3, &helper, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL); g_assert_true(!ret); g_assert_cmpint(helper.cnt_success, ==, 2); /* do not reset for the last failure */ g_assert_cmpint(helper.cnt_failed, ==, 3); } static void fu_device_retry_hardware_func(void) { gboolean ret; g_autoptr(FuDevice) device = fu_device_new(NULL); g_autoptr(GError) error = NULL; FuDeviceRetryHelper helper = { .cnt_success = 0, .cnt_failed = 0, }; ret = fu_device_retry(device, fu_device_retry_success_3rd_try, 3, &helper, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(helper.cnt_success, ==, 1); g_assert_cmpint(helper.cnt_failed, ==, 2); } static void fu_bios_settings_load_func(void) { gboolean ret; gint integer; const gchar *tmp; GPtrArray *values; FwupdBiosSetting *setting; FwupdBiosSettingKind kind; g_autofree gchar *base_dir = NULL; g_autofree gchar *test_dir = NULL; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(GError) error = NULL; g_autoptr(FuBiosSettings) p620_6_3_settings = NULL; g_autoptr(GPtrArray) p620_6_3_items = NULL; #ifdef _WIN32 /* the "AlarmDate(MM\DD\YYYY)" setting really confuses wine for obvious reasons */ g_test_skip("BIOS settings not supported on Windows"); return; #endif /* ensure the data directory is actually present for the test */ base_dir = g_test_build_filename(G_TEST_DIST, "tests", "bios-attrs", NULL); if (!g_file_test(base_dir, G_FILE_TEST_EXISTS)) { g_test_skip("Missing test data"); return; } /* load BIOS settings from a Lenovo P620 (with thinklmi driver problems) */ test_dir = g_build_filename(base_dir, "lenovo-p620", NULL); if (g_file_test(test_dir, G_FILE_TEST_EXISTS)) { (void)g_setenv("FWUPD_SYSFSFWATTRIBDIR", test_dir, TRUE); ret = fu_context_reload_bios_settings(ctx, &error); g_assert_no_error(error); g_assert_true(ret); } g_free(test_dir); /* load BIOS settings from a Lenovo P620 running 6.3 */ test_dir = g_build_filename(base_dir, "lenovo-p620-6.3", NULL); if (g_file_test(test_dir, G_FILE_TEST_EXISTS)) { (void)g_setenv("FWUPD_SYSFSFWATTRIBDIR", test_dir, TRUE); ret = fu_context_reload_bios_settings(ctx, &error); g_assert_no_error(error); g_assert_true(ret); p620_6_3_settings = fu_context_get_bios_settings(ctx); p620_6_3_items = fu_bios_settings_get_all(p620_6_3_settings); g_assert_cmpint(p620_6_3_items->len, ==, 5); /* make sure nothing pending */ ret = fu_context_get_bios_setting_pending_reboot(ctx); g_assert_false(ret); /* check a BIOS setting reads from kernel 6.3 as expected by fwupd */ setting = fu_context_get_bios_setting(ctx, "com.thinklmi.AMDMemoryGuard"); g_assert_nonnull(setting); tmp = fwupd_bios_setting_get_name(setting); g_assert_cmpstr(tmp, ==, "AMDMemoryGuard"); tmp = fwupd_bios_setting_get_description(setting); g_assert_cmpstr(tmp, ==, "AMDMemoryGuard"); tmp = fwupd_bios_setting_get_current_value(setting); g_assert_cmpstr(tmp, ==, "Disable"); values = fwupd_bios_setting_get_possible_values(setting); for (guint i = 0; i < values->len; i++) { const gchar *possible = g_ptr_array_index(values, i); if (i == 0) g_assert_cmpstr(possible, ==, "Disable"); if (i == 1) g_assert_cmpstr(possible, ==, "Enable"); } /* try to read an BIOS setting known to have ][Status] to make sure we worked * around the thinklmi bug sufficiently */ setting = fu_context_get_bios_setting(ctx, "com.thinklmi.StartupSequence"); g_assert_nonnull(setting); tmp = fwupd_bios_setting_get_current_value(setting); g_assert_cmpstr(tmp, ==, "Primary"); values = fwupd_bios_setting_get_possible_values(setting); for (guint i = 0; i < values->len; i++) { const gchar *possible = g_ptr_array_index(values, i); if (i == 0) g_assert_cmpstr(possible, ==, "Primary"); if (i == 1) g_assert_cmpstr(possible, ==, "Automatic"); } /* check BIOS settings that should be read only */ for (guint i = 0; i < p620_6_3_items->len; i++) { const gchar *name; gboolean ro; setting = g_ptr_array_index(p620_6_3_items, i); ro = fwupd_bios_setting_get_read_only(setting); tmp = fwupd_bios_setting_get_current_value(setting); name = fwupd_bios_setting_get_name(setting); g_debug("%s: %s", name, tmp); if ((g_strcmp0(name, "pending_reboot") == 0) || (g_strrstr(tmp, "[Status") != NULL)) g_assert_true(ro); else g_assert_false(ro); } } g_free(test_dir); /* load BIOS settings from a Lenovo P14s Gen1 */ test_dir = g_build_filename(base_dir, "lenovo-p14s-gen1", NULL); if (g_file_test(test_dir, G_FILE_TEST_EXISTS)) { (void)g_setenv("FWUPD_SYSFSFWATTRIBDIR", test_dir, TRUE); ret = fu_context_reload_bios_settings(ctx, &error); g_assert_no_error(error); g_assert_true(ret); g_clear_error(&error); } g_free(test_dir); /* load BIOS settings from a Dell XPS 9310 */ test_dir = g_build_filename(base_dir, "dell-xps13-9310", NULL); if (g_file_test(test_dir, G_FILE_TEST_EXISTS)) { (void)g_setenv("FWUPD_SYSFSFWATTRIBDIR", test_dir, TRUE); ret = fu_context_reload_bios_settings(ctx, &error); g_assert_no_error(error); g_assert_true(ret); /* make sure that we DIDN'T parse reset_bios setting */ setting = fu_context_get_bios_setting(ctx, FWUPD_BIOS_SETTING_RESET_BIOS); g_assert_null(setting); /* look at a integer BIOS setting */ setting = fu_context_get_bios_setting(ctx, "com.dell-wmi-sysman.CustomChargeStop"); g_assert_nonnull(setting); kind = fwupd_bios_setting_get_kind(setting); g_assert_cmpint(kind, ==, FWUPD_BIOS_SETTING_KIND_INTEGER); integer = fwupd_bios_setting_get_lower_bound(setting); g_assert_cmpint(integer, ==, 55); integer = fwupd_bios_setting_get_upper_bound(setting); g_assert_cmpint(integer, ==, 100); integer = fwupd_bios_setting_get_scalar_increment(setting); g_assert_cmpint(integer, ==, 1); /* look at a string BIOS setting */ setting = fu_context_get_bios_setting(ctx, "com.dell-wmi-sysman.Asset"); g_assert_nonnull(setting); integer = fwupd_bios_setting_get_lower_bound(setting); g_assert_cmpint(integer, ==, 1); integer = fwupd_bios_setting_get_upper_bound(setting); g_assert_cmpint(integer, ==, 64); tmp = fwupd_bios_setting_get_description(setting); g_assert_cmpstr(tmp, ==, "Asset Tag"); /* look at a enumeration BIOS setting */ setting = fu_context_get_bios_setting(ctx, "com.dell-wmi-sysman.BiosRcvrFrmHdd"); g_assert_nonnull(setting); kind = fwupd_bios_setting_get_kind(setting); g_assert_cmpint(kind, ==, FWUPD_BIOS_SETTING_KIND_ENUMERATION); values = fwupd_bios_setting_get_possible_values(setting); for (guint i = 0; i < values->len; i++) { const gchar *possible = g_ptr_array_index(values, i); if (i == 0) g_assert_cmpstr(possible, ==, "Disabled"); if (i == 1) g_assert_cmpstr(possible, ==, "Enabled"); } /* make sure we defaulted UEFI Secure boot to read only if enabled */ setting = fu_context_get_bios_setting(ctx, "com.dell-wmi-sysman.SecureBoot"); g_assert_nonnull(setting); ret = fwupd_bios_setting_get_read_only(setting); g_assert_true(ret); } } static void fu_security_attrs_hsi_func(void) { g_autofree gchar *hsi1 = NULL; g_autofree gchar *hsi2 = NULL; g_autofree gchar *hsi3 = NULL; g_autofree gchar *hsi4 = NULL; g_autofree gchar *hsi5 = NULL; g_autofree gchar *hsi6 = NULL; g_autofree gchar *hsi7 = NULL; g_autofree gchar *hsi8 = NULL; g_autofree gchar *hsi9 = NULL; g_autofree gchar *expected_hsi9 = NULL; g_autoptr(FuSecurityAttrs) attrs = NULL; g_autoptr(FwupdSecurityAttr) attr = NULL; /* no attrs */ attrs = fu_security_attrs_new(); hsi1 = fu_security_attrs_calculate_hsi(attrs, NULL, FU_SECURITY_ATTRS_FLAG_NONE); g_assert_cmpstr(hsi1, ==, "HSI:0"); /* just success from HSI:1 */ attr = fwupd_security_attr_new(FWUPD_SECURITY_ATTR_ID_SPI_BIOSWE); fwupd_security_attr_set_plugin(attr, "test"); fwupd_security_attr_set_level(attr, FWUPD_SECURITY_ATTR_LEVEL_CRITICAL); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); fwupd_security_attr_set_url(attr, "http://test"); fu_security_attrs_append(attrs, attr); hsi2 = fu_security_attrs_calculate_hsi(attrs, NULL, FU_SECURITY_ATTRS_FLAG_NONE); g_assert_cmpstr(hsi2, ==, "HSI:1"); g_clear_object(&attr); /* add failed from HSI:2, so still HSI:1 */ attr = fwupd_security_attr_new("org.fwupd.hsi.PRX"); fwupd_security_attr_set_plugin(attr, "test"); fwupd_security_attr_set_fwupd_version(attr, "2.0.7"); fwupd_security_attr_set_level(attr, FWUPD_SECURITY_ATTR_LEVEL_IMPORTANT); fwupd_security_attr_set_url(attr, "http://test"); fu_security_attrs_append(attrs, attr); hsi3 = fu_security_attrs_calculate_hsi(attrs, NULL, FU_SECURITY_ATTRS_FLAG_NONE); g_assert_cmpstr(hsi3, ==, "HSI:1"); g_clear_object(&attr); /* add an implicit obsolete via duplication */ attr = fwupd_security_attr_new("org.fwupd.hsi.PRX"); fwupd_security_attr_set_plugin(attr, "other-plugin"); fwupd_security_attr_set_fwupd_version(attr, "2.0.7"); fwupd_security_attr_set_level(attr, FWUPD_SECURITY_ATTR_LEVEL_IMPORTANT); fwupd_security_attr_set_url(attr, "http://other-plugin"); fu_security_attrs_append(attrs, attr); fu_security_attrs_depsolve(attrs); hsi4 = fu_security_attrs_calculate_hsi(attrs, NULL, FU_SECURITY_ATTRS_FLAG_NONE); g_assert_cmpstr(hsi4, ==, "HSI:1"); g_assert_true(fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_OBSOLETED)); g_clear_object(&attr); /* add attr from HSI:3, obsoleting the failure */ attr = fwupd_security_attr_new("org.fwupd.hsi.BIOSGuard"); fwupd_security_attr_set_plugin(attr, "test"); fwupd_security_attr_set_fwupd_version(attr, "2.0.7"); fwupd_security_attr_set_level(attr, FWUPD_SECURITY_ATTR_LEVEL_THEORETICAL); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); fwupd_security_attr_add_obsolete(attr, "org.fwupd.hsi.PRX"); fwupd_security_attr_set_url(attr, "http://test"); fu_security_attrs_append(attrs, attr); fu_security_attrs_depsolve(attrs); hsi5 = fu_security_attrs_calculate_hsi(attrs, NULL, FU_SECURITY_ATTRS_FLAG_NONE); g_assert_cmpstr(hsi5, ==, "HSI:3"); g_clear_object(&attr); /* add taint that was fine */ attr = fwupd_security_attr_new(FWUPD_SECURITY_ATTR_ID_FWUPD_PLUGINS); fwupd_security_attr_set_plugin(attr, "test"); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE); fwupd_security_attr_set_url(attr, "http://test"); fu_security_attrs_append(attrs, attr); hsi6 = fu_security_attrs_calculate_hsi(attrs, NULL, FU_SECURITY_ATTRS_FLAG_NONE); g_assert_cmpstr(hsi6, ==, "HSI:3"); g_clear_object(&attr); /* add updates and attestation */ attr = fwupd_security_attr_new(FWUPD_SECURITY_ATTR_ID_FWUPD_UPDATES); fwupd_security_attr_set_plugin(attr, "test"); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); fwupd_security_attr_set_url(attr, "http://test"); fu_security_attrs_append(attrs, attr); hsi7 = fu_security_attrs_calculate_hsi(attrs, NULL, FU_SECURITY_ATTRS_FLAG_NONE); g_assert_cmpstr(hsi7, ==, "HSI:3"); g_clear_object(&attr); /* add issue that was uncool */ attr = fwupd_security_attr_new(FWUPD_SECURITY_ATTR_ID_KERNEL_SWAP); fwupd_security_attr_set_plugin(attr, "test"); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE); fwupd_security_attr_set_url(attr, "http://test"); fu_security_attrs_append(attrs, attr); hsi8 = fu_security_attrs_calculate_hsi(attrs, NULL, FU_SECURITY_ATTRS_FLAG_NONE); g_assert_cmpstr(hsi8, ==, "HSI:3!"); g_clear_object(&attr); /* show version in the attribute */ attr = fwupd_security_attr_new(FWUPD_SECURITY_ATTR_ID_KERNEL_SWAP); fwupd_security_attr_set_plugin(attr, "test"); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE); fwupd_security_attr_set_url(attr, "http://test"); fu_security_attrs_append(attrs, attr); hsi9 = fu_security_attrs_calculate_hsi(attrs, NULL, FU_SECURITY_ATTRS_FLAG_ADD_VERSION); expected_hsi9 = g_strdup_printf("HSI:3! (v%d.%d.%d)", FWUPD_MAJOR_VERSION, FWUPD_MINOR_VERSION, FWUPD_MICRO_VERSION); g_assert_cmpstr(hsi9, ==, expected_hsi9); g_clear_object(&attr); } static void fu_security_attrs_compare_func(void) { FwupdSecurityAttr *attr_tmp; g_autoptr(FuSecurityAttrs) attrs1 = fu_security_attrs_new(); g_autoptr(FuSecurityAttrs) attrs2 = fu_security_attrs_new(); g_autoptr(FwupdSecurityAttr) attr1 = fwupd_security_attr_new("org.fwupd.hsi.foo"); g_autoptr(FwupdSecurityAttr) attr2 = fwupd_security_attr_new("org.fwupd.hsi.bar"); g_autoptr(FwupdSecurityAttr) attr3 = fwupd_security_attr_new("org.fwupd.hsi.baz"); g_autoptr(FwupdSecurityAttr) attr4 = fwupd_security_attr_new("org.fwupd.hsi.baz"); g_autoptr(GPtrArray) results = NULL; /* attrs1 has foo and baz(enabled) */ fwupd_security_attr_set_plugin(attr1, "foo"); fwupd_security_attr_set_created(attr1, 0); fwupd_security_attr_set_result(attr1, FWUPD_SECURITY_ATTR_RESULT_ENCRYPTED); fu_security_attrs_append(attrs1, attr1); fwupd_security_attr_set_plugin(attr3, "baz"); fwupd_security_attr_set_created(attr3, 0); fwupd_security_attr_set_result(attr3, FWUPD_SECURITY_ATTR_RESULT_ENABLED); fu_security_attrs_append(attrs1, attr3); /* attrs2 has bar and baz(~enabled) */ fwupd_security_attr_set_plugin(attr2, "bar"); fwupd_security_attr_set_created(attr2, 0); fwupd_security_attr_set_result(attr2, FWUPD_SECURITY_ATTR_RESULT_LOCKED); fu_security_attrs_append(attrs2, attr2); fwupd_security_attr_set_plugin(attr4, "baz"); fwupd_security_attr_set_created(attr4, 0); fwupd_security_attr_set_result(attr4, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); fu_security_attrs_append(attrs2, attr4); results = fu_security_attrs_compare(attrs1, attrs2); g_assert_cmpint(results->len, ==, 3); attr_tmp = g_ptr_array_index(results, 0); g_assert_cmpstr(fwupd_security_attr_get_appstream_id(attr_tmp), ==, "org.fwupd.hsi.bar"); g_assert_cmpint(fwupd_security_attr_get_result_fallback(attr_tmp), ==, FWUPD_SECURITY_ATTR_RESULT_UNKNOWN); g_assert_cmpint(fwupd_security_attr_get_result(attr_tmp), ==, FWUPD_SECURITY_ATTR_RESULT_LOCKED); attr_tmp = g_ptr_array_index(results, 1); g_assert_cmpstr(fwupd_security_attr_get_appstream_id(attr_tmp), ==, "org.fwupd.hsi.foo"); g_assert_cmpint(fwupd_security_attr_get_result_fallback(attr_tmp), ==, FWUPD_SECURITY_ATTR_RESULT_ENCRYPTED); g_assert_cmpint(fwupd_security_attr_get_result(attr_tmp), ==, FWUPD_SECURITY_ATTR_RESULT_UNKNOWN); attr_tmp = g_ptr_array_index(results, 2); g_assert_cmpstr(fwupd_security_attr_get_appstream_id(attr_tmp), ==, "org.fwupd.hsi.baz"); g_assert_cmpint(fwupd_security_attr_get_result_fallback(attr_tmp), ==, FWUPD_SECURITY_ATTR_RESULT_ENABLED); g_assert_cmpint(fwupd_security_attr_get_result(attr_tmp), ==, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); g_assert_true(fu_security_attrs_equal(attrs1, attrs1)); g_assert_false(fu_security_attrs_equal(attrs1, attrs2)); g_assert_false(fu_security_attrs_equal(attrs2, attrs1)); } typedef enum { FU_FIRMWARE_BUILDER_FLAG_NONE, FU_FIRMWARE_BUILDER_FLAG_NO_BINARY_COMPARE = 1 << 0, } FuFirmwareBuilderFlags; static void fu_firmware_builder_round_trip_func(void) { struct { GType gtype; const gchar *xml_fn; const gchar *checksum; FuFirmwareBuilderFlags flags; } map[] = { { FU_TYPE_CAB_FIRMWARE, "cab.builder.xml", "a708f47b1a46377f1ea420597641ffe9a40abd75", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_CAB_FIRMWARE, "cab-compressed.builder.xml", NULL, /* not byte-identical */ FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_ELF_FIRMWARE, "elf.builder.xml", "99ea60b8dd46085dcbf1ecd5e72b4cb73a3b6faa", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_DFUSE_FIRMWARE, "dfuse.builder.xml", "c1ff429f0e381c8fe8e1b2ee41a5a9a79e2f2ff7", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_PEFILE_FIRMWARE, "pefile.builder.xml", "73b0e0dc9f6175b7bc27b77f20e0d9eca2d2d141", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_LINEAR_FIRMWARE, "linear.builder.xml", "18fa8201652c82dc717df1905d8ab72e46e3d82b", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_HID_REPORT_ITEM, "hid-report-item.builder.xml", "5b18c07399fc8968ce22127df38d8d923089ec92", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_HID_DESCRIPTOR, "hid-descriptor.builder.xml", "6bb23f7c9fedc21f05528b3b63ad5837f4a16a92", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_SBATLEVEL_SECTION, "sbatlevel.builder.xml", "8204ef9477b4305748a0de6e667547cb6ce5e426", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_CSV_FIRMWARE, "csv.builder.xml", "986cbf8cde5bc7d8b49ee94cceae3f92efbd2eef", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_FDT_FIRMWARE, "fdt.builder.xml", "40f7fbaff684a6bcf67c81b3079422c2529741e1", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_FIT_FIRMWARE, "fit.builder.xml", "293ce07351bb7d76631c4e2ba47243db1e150f3c", FU_FIRMWARE_BUILDER_FLAG_NO_BINARY_COMPARE, }, { FU_TYPE_SREC_FIRMWARE, "srec.builder.xml", "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed", FU_FIRMWARE_BUILDER_FLAG_NO_BINARY_COMPARE, }, { FU_TYPE_IHEX_FIRMWARE, "ihex.builder.xml", "a8d74f767f3fc992b413e5ba801cedc80a4cf013", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_FMAP_FIRMWARE, "fmap.builder.xml", "a0b9ffc10a586d217edf9e9bae7c1fe7c564ea01", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_EFI_LOAD_OPTION, "efi-load-option.builder.xml", "7ef696d22902ae97ef5f73ad9c85a28095ad56f1", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_EFI_LOAD_OPTION, "efi-load-option-hive.builder.xml", "76a378752b7ccdf3d68365d83784053356fa7e0a", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_EFI_LOAD_OPTION, "efi-load-option-data.builder.xml", "6e6190dc6b1bf45bc6e30ba7a6a98d891d692dd0", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_EDID, "edid.builder.xml", "64cef10b75ccce684a483d576dd4a4ce6bef8165", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_EFI_SECTION, "efi-section.builder.xml", "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_EFI_SECTION, "efi-section.builder.xml", "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_EFI_FILE, "efi-file.builder.xml", "90374d97cf6bc70059d24c816c188c10bd250ed7", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_EFI_FILESYSTEM, "efi-filesystem.builder.xml", "d6fbadc1c303a3b4eede9db7fb0ddb353efffc86", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_EFI_SIGNATURE, "efi-signature.builder.xml", "ff7b862504262ce4853db29690b683bb06ce7d1f", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_EFI_SIGNATURE_LIST, "efi-signature-list.builder.xml", "450111ea0f77a0ede5b6a6305cd2e02b44b5f1e9", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_EFI_VARIABLE_AUTHENTICATION2, "efi-variable-authentication2.builder.xml", "bd08e81e9c86490dc1ffb32b1e3332606eb0fa97", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_EFI_VOLUME, "efi-volume.builder.xml", "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_IFD_FIRMWARE, "ifd.builder.xml", "06ae066ea53cefe43fed2f1ca4fc7d8cccdbcf1e", FU_FIRMWARE_BUILDER_FLAG_NO_BINARY_COMPARE, }, { FU_TYPE_CFU_OFFER, "cfu-offer.builder.xml", "c10223887ff6cdf4475ad07c65b1f0f3a2d0d5ca", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_CFU_PAYLOAD, "cfu-payload.builder.xml", "5da829f5fd15a28970aed98ebb26ebf2f88ed6f2", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_IFWI_CPD_FIRMWARE, "ifwi-cpd.builder.xml", "91e348d17cb91ef7a528e85beb39d15a0532dca5", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_IFWI_FPT_FIRMWARE, "ifwi-fpt.builder.xml", "d1f0fb2c2a7a99441bf4a825d060642315a94d91", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_OPROM_FIRMWARE, "oprom.builder.xml", "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_INTEL_THUNDERBOLT_NVM, "intel-thunderbolt.builder.xml", "b3a73baf05078dfdd833b407a0a6afb239ec2f23", FU_FIRMWARE_BUILDER_FLAG_NO_BINARY_COMPARE, }, { FU_TYPE_INTEL_THUNDERBOLT_FIRMWARE, "intel-thunderbolt.builder.xml", "1a2dec48aab3e1e29907f2148ab16e4730387325", FU_FIRMWARE_BUILDER_FLAG_NO_BINARY_COMPARE, }, { FU_TYPE_USB_BOS_DESCRIPTOR, "usb-bos-descriptor.builder.xml", "a305749853781c6899c4b28039cb4c7d9059b910", FU_FIRMWARE_BUILDER_FLAG_NONE, }, #ifdef HAVE_CBOR { FU_TYPE_USWID_FIRMWARE, "uswid.builder.xml", "b473fbdbe00f860c4da43f9499569394bac81f14", FU_FIRMWARE_BUILDER_FLAG_NONE, }, { FU_TYPE_USWID_FIRMWARE, "uswid-compressed.builder.xml", NULL, /* not byte-identical */ FU_FIRMWARE_BUILDER_FLAG_NONE, }, #endif }; g_type_ensure(FU_TYPE_COSWID_FIRMWARE); for (guint i = 0; i < G_N_ELEMENTS(map); i++) { gboolean ret; g_autofree gchar *csum1 = NULL; g_autofree gchar *csum2 = NULL; g_autofree gchar *filename = NULL; g_autofree gchar *xml1 = NULL; g_autofree gchar *xml2 = NULL; g_autoptr(FuFirmware) firmware1 = g_object_new(map[i].gtype, NULL); g_autoptr(FuFirmware) firmware2 = g_object_new(map[i].gtype, NULL); g_autoptr(FuFirmware) firmware3 = g_object_new(map[i].gtype, NULL); g_autoptr(GError) error = NULL; g_autoptr(GBytes) blob = NULL; /* build and write */ g_debug("%s", map[i].xml_fn); filename = g_test_build_filename(G_TEST_DIST, "tests", map[i].xml_fn, NULL); ret = g_file_get_contents(filename, &xml1, NULL, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_firmware_build_from_xml(firmware1, xml1, &error); g_assert_no_error(error); g_assert_true(ret); csum1 = fu_firmware_get_checksum(firmware1, G_CHECKSUM_SHA1, &error); g_assert_no_error(error); g_assert_nonnull(csum1); if (map[i].checksum != NULL) g_assert_cmpstr(csum1, ==, map[i].checksum); /* ensure we can write and then parse what we just wrote */ blob = fu_firmware_write(firmware1, &error); g_assert_no_error(error); g_assert_nonnull(blob); ret = fu_firmware_parse_bytes(firmware3, blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH | FU_FIRMWARE_PARSE_FLAG_CACHE_STREAM, &error); if (!ret) g_prefix_error(&error, "%s: ", map[i].xml_fn); g_assert_no_error(error); g_assert_true(ret); /* ensure we can write back the binary blob */ if ((map[i].flags & FU_FIRMWARE_BUILDER_FLAG_NO_BINARY_COMPARE) == 0) { g_autoptr(GBytes) blob2 = fu_firmware_write(firmware3, &error); g_assert_nonnull(blob2); g_assert_no_error(error); ret = fu_bytes_compare(blob2, blob, &error); g_assert_no_error(error); g_assert_true(ret); } /* ensure we can round-trip to XML */ xml2 = fu_firmware_export_to_xml(firmware1, FU_FIRMWARE_EXPORT_FLAG_NONE, &error); g_assert_no_error(error); ret = fu_firmware_build_from_xml(firmware2, xml2, &error); g_assert_no_error(error); g_assert_true(ret); csum2 = fu_firmware_get_checksum(firmware2, G_CHECKSUM_SHA1, &error); g_assert_nonnull(csum2); g_assert_no_error(error); if (map[i].checksum != NULL) g_assert_cmpstr(csum2, ==, map[i].checksum); } } typedef struct { guint last_percentage; guint updates; } FuProgressHelper; static void fu_progress_percentage_changed_cb(FuProgress *progress, guint percentage, gpointer data) { FuProgressHelper *helper = (FuProgressHelper *)data; helper->last_percentage = percentage; helper->updates++; } static void fu_progress_func(void) { FuProgressHelper helper = {0}; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autofree gchar *str = NULL; g_signal_connect(FU_PROGRESS(progress), "percentage-changed", G_CALLBACK(fu_progress_percentage_changed_cb), &helper); g_assert_cmpfloat_with_epsilon(fu_progress_get_duration(progress), 0.f, 0.001); fu_progress_set_profile(progress, TRUE); fu_progress_set_steps(progress, 5); g_assert_cmpint(helper.last_percentage, ==, 0); g_usleep(20 * 1000); fu_progress_step_done(progress); g_assert_cmpint(helper.updates, ==, 2); g_assert_cmpint(helper.last_percentage, ==, 20); for (guint i = 0; i < 4; i++) { g_usleep(20 * 1000); fu_progress_step_done(progress); } g_assert_cmpint(helper.last_percentage, ==, 100); g_assert_cmpint(helper.updates, ==, 6); g_assert_cmpfloat_with_epsilon(fu_progress_get_duration(progress), 0.1f, 0.05); str = fu_progress_traceback(progress); g_debug("\n%s", str); } static void fu_progress_child_func(void) { FuProgressHelper helper = {0}; FuProgress *child; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); /* reset */ fu_progress_set_profile(progress, TRUE); fu_progress_set_steps(progress, 2); g_signal_connect(FU_PROGRESS(progress), "percentage-changed", G_CALLBACK(fu_progress_percentage_changed_cb), &helper); /* parent: |-----------------------|-----------------------| * step1: |-----------------------| * child: |-------------|---------| */ /* PARENT UPDATE */ g_debug("parent update #1"); fu_progress_step_done(progress); g_assert_cmpint(helper.updates, ==, 1); g_assert_cmpint(helper.last_percentage, ==, 50); /* now test with a child */ child = fu_progress_get_child(progress); fu_progress_set_id(child, G_STRLOC); fu_progress_set_steps(child, 2); g_debug("child update #1"); fu_progress_step_done(child); g_assert_cmpint(helper.updates, ==, 2); g_assert_cmpint(helper.last_percentage, ==, 75); /* child update */ g_debug("child update #2"); fu_progress_step_done(child); g_assert_cmpint(helper.updates, ==, 3); g_assert_cmpint(helper.last_percentage, ==, 100); /* parent update */ g_debug("parent update #2"); fu_progress_step_done(progress); /* ensure we ignored the duplicate */ g_assert_cmpint(helper.updates, ==, 3); g_assert_cmpint(helper.last_percentage, ==, 100); } static void fu_progress_scaling_func(void) { const guint insane_steps = 1000 * 1000; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); fu_progress_set_steps(progress, insane_steps); for (guint i = 0; i < insane_steps / 2; i++) fu_progress_step_done(progress); g_assert_cmpint(fu_progress_get_percentage(progress), ==, 50); for (guint i = 0; i < insane_steps / 2; i++) { FuProgress *progress_child = fu_progress_get_child(progress); fu_progress_set_percentage(progress_child, 0); fu_progress_set_percentage(progress_child, 100); fu_progress_step_done(progress); } g_assert_cmpint(fu_progress_get_percentage(progress), ==, 100); } static void fu_progress_parent_one_step_proxy_func(void) { FuProgressHelper helper = {0}; FuProgress *child; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); /* one step */ fu_progress_set_steps(progress, 1); g_signal_connect(FU_PROGRESS(progress), "percentage-changed", G_CALLBACK(fu_progress_percentage_changed_cb), &helper); /* now test with a child */ child = fu_progress_get_child(progress); fu_progress_set_id(child, G_STRLOC); fu_progress_set_steps(child, 2); /* child set value */ fu_progress_set_percentage(child, 33); /* ensure 1 updates for progress with one step and ensure using child value as parent */ g_assert_cmpint(helper.updates, ==, 1); g_assert_cmpint(helper.last_percentage, ==, 33); } static void fu_progress_non_equal_steps_func(void) { g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); FuProgress *child; FuProgress *grandchild; /* test non-equal steps */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 20, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 60, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 20, NULL); g_assert_cmpint(fu_progress_get_percentage(progress), ==, 0); g_assert_cmpint(fu_progress_get_status(progress), ==, FWUPD_STATUS_DEVICE_ERASE); /* child step should increment according to the custom steps */ child = fu_progress_get_child(progress); fu_progress_set_id(child, G_STRLOC); fu_progress_set_steps(child, 2); fu_progress_set_status(child, FWUPD_STATUS_DEVICE_BUSY); g_assert_cmpint(fu_progress_get_status(progress), ==, FWUPD_STATUS_DEVICE_BUSY); /* start child */ fu_progress_step_done(child); /* verify 10% */ g_assert_cmpint(fu_progress_get_percentage(progress), ==, 10); /* finish child */ fu_progress_step_done(child); /* ensure the parent is switched back to the status before the child took over */ g_assert_cmpint(fu_progress_get_status(progress), ==, FWUPD_STATUS_DEVICE_ERASE); fu_progress_step_done(progress); g_assert_cmpint(fu_progress_get_status(progress), ==, FWUPD_STATUS_DEVICE_WRITE); /* verify 20% */ g_assert_cmpint(fu_progress_get_percentage(progress), ==, 20); /* child step should increment according to the custom steps */ child = fu_progress_get_child(progress); fu_progress_set_id(child, G_STRLOC); fu_progress_set_id(child, G_STRLOC); fu_progress_add_step(child, FWUPD_STATUS_DEVICE_RESTART, 25, NULL); fu_progress_add_step(child, FWUPD_STATUS_DEVICE_WRITE, 75, NULL); g_assert_cmpint(fu_progress_get_status(progress), ==, FWUPD_STATUS_DEVICE_RESTART); /* start child */ fu_progress_step_done(child); g_assert_cmpint(fu_progress_get_status(progress), ==, FWUPD_STATUS_DEVICE_WRITE); /* verify bilinear interpolation is working */ g_assert_cmpint(fu_progress_get_percentage(progress), ==, 35); /* * 0 20 80 100 * |---------||----------------------------||---------| * | 35 | * |-------||-------------------| (25%) * | 75.5 | * |---------------||--| (90%) */ grandchild = fu_progress_get_child(child); fu_progress_set_id(grandchild, G_STRLOC); fu_progress_add_step(grandchild, FWUPD_STATUS_DEVICE_ERASE, 90, NULL); fu_progress_add_step(grandchild, FWUPD_STATUS_DEVICE_WRITE, 10, NULL); fu_progress_step_done(grandchild); /* verify bilinear interpolation (twice) is working for subpercentage */ g_assert_cmpint(fu_progress_get_percentage(progress), ==, 75); fu_progress_step_done(grandchild); /* finish child */ fu_progress_step_done(child); fu_progress_step_done(progress); g_assert_cmpint(fu_progress_get_status(progress), ==, FWUPD_STATUS_DEVICE_READ); /* verify 80% */ g_assert_cmpint(fu_progress_get_percentage(progress), ==, 80); fu_progress_step_done(progress); /* verify 100% */ g_assert_cmpint(fu_progress_get_percentage(progress), ==, 100); g_assert_cmpint(fu_progress_get_status(progress), ==, FWUPD_STATUS_UNKNOWN); } static void fu_progress_finish_func(void) { FuProgress *child; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); /* check straight finish */ fu_progress_set_steps(progress, 3); child = fu_progress_get_child(progress); fu_progress_set_id(child, G_STRLOC); fu_progress_set_steps(child, 3); fu_progress_finished(child); /* parent step done after child finish */ fu_progress_step_done(progress); } static void fu_progress_global_fraction_func(void) { FuProgress *child; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); /* sanity check */ fu_progress_set_steps(progress, 100); g_assert_cmpfloat_with_epsilon(fu_progress_get_global_fraction(progress), 1.f, 0.001); /* 1% */ child = fu_progress_get_child(progress); g_assert_cmpfloat_with_epsilon(fu_progress_get_global_fraction(child), 0.01f, 0.001); /* 0.01% */ fu_progress_set_id(child, G_STRLOC); fu_progress_set_steps(child, 100); fu_progress_step_done(child); fu_progress_step_done(child); fu_progress_step_done(child); fu_progress_finished(child); /* done */ fu_progress_finished(progress); } static void fu_progress_child_finished(void) { FuProgress *child; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); /* check straight finish */ fu_progress_set_steps(progress, 3); child = fu_progress_get_child(progress); fu_progress_set_id(child, G_STRLOC); fu_progress_set_steps(child, 3); /* some imaginary ignorable error */ /* parent step done after child finish */ fu_progress_add_flag(progress, FU_PROGRESS_FLAG_CHILD_FINISHED); fu_progress_step_done(progress); } static void fu_partial_input_stream_composite_func(void) { gboolean ret; gint rc; guint8 buf[4] = {0}; g_autoptr(GBytes) blob = g_bytes_new_static("12345678", 8); g_autoptr(GError) error = NULL; g_autoptr(GInputStream) composite_stream = fu_composite_input_stream_new(); g_autoptr(GInputStream) partial_stream = NULL; fu_composite_input_stream_add_bytes(FU_COMPOSITE_INPUT_STREAM(composite_stream), blob); /* limit to '34' */ partial_stream = fu_partial_input_stream_new(composite_stream, 2, 2, &error); g_assert_no_error(error); g_assert_nonnull(partial_stream); /* seek to the start of the partial input stream */ ret = g_seekable_seek(G_SEEKABLE(partial_stream), 0x0, G_SEEK_SET, NULL, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(composite_stream)), ==, 0x2); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(partial_stream)), ==, 0x0); /* read the 34 */ rc = g_input_stream_read(partial_stream, buf, sizeof(buf), NULL, &error); g_assert_no_error(error); g_assert_cmpint(rc, ==, 2); g_assert_cmpint(buf[0], ==, '3'); g_assert_cmpint(buf[1], ==, '4'); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(composite_stream)), ==, 0x4); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(partial_stream)), ==, 0x2); /* there is no more data to read */ rc = g_input_stream_read(partial_stream, buf, sizeof(buf), NULL, &error); g_assert_no_error(error); g_assert_cmpint(rc, ==, 0); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(composite_stream)), ==, 0x4); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(partial_stream)), ==, 0x2); } static void fu_partial_input_stream_simple_func(void) { gboolean ret; gssize rc; guint8 buf[2] = {0x0}; g_autoptr(GBytes) blob = g_bytes_new_static("12345678", 8); g_autoptr(GError) error = NULL; g_autoptr(GInputStream) base_stream = g_memory_input_stream_new_from_bytes(blob); g_autoptr(GInputStream) stream = NULL; /* use G_MAXSIZE for "rest of the stream" */ stream = fu_partial_input_stream_new(base_stream, 4, G_MAXSIZE, &error); g_assert_no_error(error); g_assert_nonnull(stream); ret = g_seekable_seek(G_SEEKABLE(stream), 0x2, G_SEEK_SET, NULL, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(stream)), ==, 0x2); /* read from offset */ rc = g_input_stream_read(stream, buf, 2, NULL, &error); g_assert_no_error(error); g_assert_cmpint(rc, ==, 2); g_assert_cmpint(buf[0], ==, '7'); g_assert_cmpint(buf[1], ==, '8'); } static void fu_partial_input_stream_closed_base_func(void) { gboolean ret; gssize rc; guint8 buf[2] = {0x0}; g_autoptr(GBytes) blob = g_bytes_new_static("12345678", 8); g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(GInputStream) base_stream = g_memory_input_stream_new_from_bytes(blob); stream = fu_partial_input_stream_new(base_stream, 2, 4, &error); g_assert_no_error(error); g_assert_nonnull(stream); ret = g_input_stream_close(base_stream, NULL, &error); g_assert_no_error(error); g_assert_true(ret); ret = g_seekable_seek(G_SEEKABLE(stream), 0x0, G_SEEK_SET, NULL, &error); g_assert_no_error(error); g_assert_true(ret); rc = g_input_stream_read(stream, buf, sizeof(buf), NULL, &error); g_assert_error(error, G_IO_ERROR, G_IO_ERROR_CLOSED); g_assert_cmpint(rc, ==, -1); } static void fu_partial_input_stream_func(void) { gboolean ret; gssize rc; guint8 buf[5] = {0x0}; goffset pos; g_autofree gchar *fn = NULL; g_autoptr(GError) error = NULL; g_autoptr(GBytes) blob = g_bytes_new_static("12345678", 8); /* \--/ */ g_autoptr(GBytes) blob2 = NULL; g_autoptr(GFile) file = NULL; g_autoptr(GInputStream) base_stream = g_memory_input_stream_new_from_bytes(blob); g_autoptr(GInputStream) stream_complete = NULL; g_autoptr(GInputStream) stream_error = NULL; g_autoptr(GInputStream) stream_file = NULL; g_autoptr(GInputStream) stream = NULL; /* check the behavior of GFileInputStream */ fn = g_test_build_filename(G_TEST_DIST, "tests", "dfu.builder.xml", NULL); g_assert_nonnull(fn); file = g_file_new_for_path(fn); stream_file = G_INPUT_STREAM(g_file_read(file, NULL, &error)); g_assert_no_error(error); g_assert_nonnull(stream_file); ret = g_seekable_seek(G_SEEKABLE(stream_file), 0x0, G_SEEK_SET, NULL, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(stream_file)), ==, 0x0); ret = g_seekable_seek(G_SEEKABLE(stream_file), 0x0, G_SEEK_END, NULL, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(stream_file)), ==, 216); rc = g_input_stream_read(stream_file, buf, 2, NULL, &error); g_assert_no_error(error); g_assert_cmpint(rc, ==, 0); pos = g_seekable_tell(G_SEEKABLE(stream_file)); g_assert_cmpint(pos, ==, 216); ret = g_seekable_seek(G_SEEKABLE(stream_file), pos, G_SEEK_SET, NULL, &error); g_assert_no_error(error); g_assert_true(ret); rc = g_input_stream_read(stream_file, buf, 2, NULL, &error); g_assert_no_error(error); g_assert_cmpint(rc, ==, 0); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(stream_file)), ==, 216); /* we CAN seek past the end... */ ret = g_seekable_seek(G_SEEKABLE(stream_file), pos + 10000, G_SEEK_SET, NULL, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(stream_file)), ==, 10216); /* reads all return zero */ rc = g_input_stream_read(stream_file, buf, 2, NULL, &error); g_assert_no_error(error); g_assert_cmpint(rc, ==, 0); /* END offset is negative */ ret = g_seekable_seek(G_SEEKABLE(stream_file), -0x1, G_SEEK_END, NULL, &error); g_assert_no_error(error); g_assert_true(ret); rc = g_input_stream_read(stream_file, buf, 1, NULL, &error); g_assert_no_error(error); g_assert_cmpint(rc, ==, 1); g_assert_cmpint(buf[0], ==, 10); /* check the behavior of GMemoryInputStream */ g_assert_no_error(error); g_assert_nonnull(stream_file); ret = g_seekable_seek(G_SEEKABLE(base_stream), 0x0, G_SEEK_SET, NULL, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(base_stream)), ==, 0x0); ret = g_seekable_seek(G_SEEKABLE(base_stream), 0x0, G_SEEK_END, NULL, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(base_stream)), ==, 8); rc = g_input_stream_read(base_stream, buf, 2, NULL, &error); g_assert_no_error(error); g_assert_cmpint(rc, ==, 0); pos = g_seekable_tell(G_SEEKABLE(base_stream)); g_assert_cmpint(pos, ==, 8); ret = g_seekable_seek(G_SEEKABLE(base_stream), pos, G_SEEK_SET, NULL, &error); g_assert_no_error(error); g_assert_true(ret); rc = g_input_stream_read(base_stream, buf, 2, NULL, &error); g_assert_no_error(error); g_assert_cmpint(rc, ==, 0); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(base_stream)), ==, 8); /* we CANNOT seek past the end... */ ret = g_seekable_seek(G_SEEKABLE(base_stream), pos + 10000, G_SEEK_SET, NULL, &error); g_assert_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT); g_assert_false(ret); g_clear_error(&error); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(base_stream)), ==, 8); /* END offset is negative */ ret = g_seekable_seek(G_SEEKABLE(base_stream), -1, G_SEEK_END, NULL, &error); g_assert_no_error(error); g_assert_true(ret); rc = g_input_stream_read(base_stream, buf, 1, NULL, &error); g_assert_no_error(error); g_assert_cmpint(rc, ==, 1); g_assert_cmpint(buf[0], ==, '8'); /* seek to non-start */ stream = fu_partial_input_stream_new(base_stream, 2, 4, &error); g_assert_no_error(error); g_assert_nonnull(stream); ret = g_seekable_seek(G_SEEKABLE(stream), 0x2, G_SEEK_SET, NULL, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(stream)), ==, 0x2); /* read from start */ rc = g_input_stream_read(stream, buf, 2, NULL, &error); g_assert_no_error(error); g_assert_cmpint(rc, ==, 2); g_assert_cmpint(buf[0], ==, '5'); g_assert_cmpint(buf[1], ==, '6'); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(stream)), ==, 0x4); rc = g_input_stream_read(stream, buf, 2, NULL, &error); g_assert_no_error(error); g_assert_cmpint(rc, ==, 0); /* convert back to bytes */ blob2 = fu_input_stream_read_bytes(stream, 0x0, G_MAXUINT32, NULL, &error); g_assert_no_error(error); g_assert_nonnull(blob2); g_assert_cmpint(g_bytes_get_size(blob2), ==, 4); /* seek to end of base stream */ ret = g_seekable_seek(G_SEEKABLE(base_stream), 0x0, G_SEEK_END, NULL, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(base_stream)), ==, 0x8); rc = g_input_stream_read(base_stream, buf, 1, NULL, &error); g_assert_no_error(error); g_assert_cmpint(rc, ==, 0); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(base_stream)), ==, 0x8); /* seek to end of partial stream */ ret = g_seekable_seek(G_SEEKABLE(stream), 0x0, G_SEEK_END, NULL, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(stream)), ==, 0x4); rc = g_input_stream_read(stream, buf, sizeof(buf), NULL, &error); g_assert_no_error(error); g_assert_cmpint(rc, ==, 0); /* seek to offset to end of partial stream */ ret = g_seekable_seek(G_SEEKABLE(stream), -1, G_SEEK_END, NULL, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(stream)), ==, 0x3); rc = g_input_stream_read(stream, buf, sizeof(buf), NULL, &error); g_assert_no_error(error); g_assert_cmpint(rc, ==, 1); g_assert_cmpint(buf[0], ==, '6'); /* attempt an overread of the base stream */ ret = g_seekable_seek(G_SEEKABLE(stream), 0x2, G_SEEK_SET, NULL, &error); g_assert_no_error(error); g_assert_true(ret); rc = g_input_stream_read(stream, buf, sizeof(buf), NULL, &error); g_assert_no_error(error); g_assert_cmpint(rc, ==, 2); /* attempt to seek way past the base stream */ ret = g_seekable_seek(G_SEEKABLE(stream), 0x1000, G_SEEK_SET, NULL, &error); g_assert_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT); g_assert_false(ret); g_clear_error(&error); /* read right up against the end of the base stream */ stream_complete = fu_partial_input_stream_new(base_stream, 0, 8, &error); g_assert_no_error(error); g_assert_nonnull(stream_complete); ret = g_seekable_seek(G_SEEKABLE(stream_complete), 0x8, G_SEEK_SET, NULL, &error); g_assert_no_error(error); g_assert_true(ret); rc = g_input_stream_read(stream_complete, buf, sizeof(buf), NULL, &error); g_assert_no_error(error); g_assert_cmpint(rc, ==, 0); /* try to create an out-of-range partial stream */ stream_error = fu_partial_input_stream_new(base_stream, 0, 9, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA); g_assert_null(stream_error); } static void fu_composite_input_stream_func(void) { gboolean ret; gsize streamsz = 0; gssize rc; guint8 buf[2] = {0x0}; g_autofree gchar *str = NULL; g_autoptr(GError) error = NULL; g_autoptr(GBytes) blob1 = g_bytes_new_static("ab", 2); g_autoptr(GBytes) blob2 = g_bytes_new_static("cde", 3); g_autoptr(GBytes) blob3 = g_bytes_new_static("xxxfgyyy", 8); g_autoptr(GInputStream) composite_stream = fu_composite_input_stream_new(); g_autoptr(GInputStream) stream3 = g_memory_input_stream_new_from_bytes(blob3); g_autoptr(GInputStream) stream4 = NULL; /* empty */ ret = fu_input_stream_size(composite_stream, &streamsz, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(streamsz, ==, 0); /* add bytes */ fu_composite_input_stream_add_bytes(FU_COMPOSITE_INPUT_STREAM(composite_stream), blob1); ret = fu_input_stream_size(composite_stream, &streamsz, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(streamsz, ==, 2); /* add bytes */ fu_composite_input_stream_add_bytes(FU_COMPOSITE_INPUT_STREAM(composite_stream), blob2); ret = fu_input_stream_size(composite_stream, &streamsz, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(streamsz, ==, 5); /* add partial stream */ stream4 = fu_partial_input_stream_new(stream3, 0x3, 2, &error); g_assert_no_error(error); g_assert_nonnull(stream4); fu_composite_input_stream_add_partial_stream(FU_COMPOSITE_INPUT_STREAM(composite_stream), FU_PARTIAL_INPUT_STREAM(stream4)); ret = fu_input_stream_size(composite_stream, &streamsz, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(streamsz, ==, 7); /* to string */ str = fwupd_codec_to_string(FWUPD_CODEC(composite_stream)); g_print("%s", str); /* first block */ ret = fu_input_stream_read_safe(composite_stream, buf, sizeof(buf), 0x0, /* offset */ 0x0, /* seek */ sizeof(buf), &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(buf[0], ==, 'a'); g_assert_cmpint(buf[1], ==, 'b'); /* indented into second block */ ret = fu_input_stream_read_safe(composite_stream, buf, sizeof(buf), 0x0, /* offset */ 0x3, /* seek */ sizeof(buf), &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(buf[0], ==, 'd'); g_assert_cmpint(buf[1], ==, 'e'); /* third input stream has an offset */ ret = fu_input_stream_read_safe(composite_stream, buf, sizeof(buf), 0x0, /* offset */ 0x5, /* seek */ sizeof(buf), &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(buf[0], ==, 'f'); g_assert_cmpint(buf[1], ==, 'g'); /* read across a boundary, so should return early */ ret = g_seekable_seek(G_SEEKABLE(composite_stream), 0x1, G_SEEK_SET, NULL, &error); g_assert_no_error(error); g_assert_true(ret); rc = g_input_stream_read(composite_stream, buf, 2, NULL, &error); g_assert_no_error(error); g_assert_cmpint(rc, ==, 1); g_assert_cmpint(buf[0], ==, 'b'); /* seek to end of composite stream */ ret = g_seekable_seek(G_SEEKABLE(composite_stream), 0x0, G_SEEK_END, NULL, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(composite_stream)), ==, 0x7); rc = g_input_stream_read(composite_stream, buf, sizeof(buf), NULL, &error); g_assert_no_error(error); g_assert_cmpint(rc, ==, 0); /* seek to the same place directly */ ret = g_seekable_seek(G_SEEKABLE(composite_stream), 0x7, G_SEEK_SET, NULL, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(composite_stream)), ==, 0x7); rc = g_input_stream_read(composite_stream, buf, sizeof(buf), NULL, &error); g_assert_no_error(error); g_assert_cmpint(rc, ==, 0); /* seek to offset to end of composite stream */ ret = g_seekable_seek(G_SEEKABLE(composite_stream), -1, G_SEEK_END, NULL, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(g_seekable_tell(G_SEEKABLE(composite_stream)), ==, 0x6); rc = g_input_stream_read(composite_stream, buf, sizeof(buf), NULL, &error); g_assert_no_error(error); g_assert_cmpint(rc, ==, 1); g_assert_cmpint(buf[0], ==, 'g'); } static gboolean fu_strsplit_stream_cb(GString *token, guint token_idx, gpointer user_data, GError **error) { guint *cnt = (guint *)user_data; g_debug(">%s<", token->str); (*cnt)++; return TRUE; } static void fu_strsplit_stream_func(void) { gboolean ret; guint cnt1 = 0; guint cnt2 = 0; guint cnt3 = 0; const gchar str1[] = "simple string"; const gchar str2[] = "123delimited123start123and123end123"; const gchar str3[] = "this|has|trailing|nuls\0\0\0\0"; g_autoptr(GInputStream) stream1 = NULL; g_autoptr(GInputStream) stream2 = NULL; g_autoptr(GInputStream) stream3 = NULL; g_autoptr(GError) error = NULL; /* check includes NUL */ g_assert_cmpint(sizeof(str1), ==, 14); stream1 = G_INPUT_STREAM(g_memory_input_stream_new_from_data(str1, strlen(str1), NULL)); ret = fu_strsplit_stream(stream1, 0x0, " ", fu_strsplit_stream_cb, &cnt1, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(cnt1, ==, 2); stream2 = G_INPUT_STREAM(g_memory_input_stream_new_from_data(str2, strlen(str2), NULL)); ret = fu_strsplit_stream(stream2, 0x0, "123", fu_strsplit_stream_cb, &cnt2, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(cnt2, ==, 6); stream3 = G_INPUT_STREAM(g_memory_input_stream_new_from_data(str3, sizeof(str3), NULL)); ret = fu_strsplit_stream(stream3, 0x0, "|", fu_strsplit_stream_cb, &cnt3, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(cnt3, ==, 4); } static void fu_input_stream_find_func(void) { const gchar *haystack = "I write free software. Firmware troublemaker."; const gchar *needle1 = "Firmware"; const gchar *needle2 = "XXX"; gboolean ret; gsize offset = 0; g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = NULL; stream = g_memory_input_stream_new_from_data((const guint8 *)haystack, strlen(haystack), NULL); ret = fu_input_stream_find(stream, (const guint8 *)needle1, strlen(needle1), &offset, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(offset, ==, 23); ret = fu_input_stream_find(stream, (const guint8 *)needle2, strlen(needle2), &offset, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND); g_assert_false(ret); } static void fu_input_stream_sum_overflow_func(void) { guint8 buf[3] = {0}; gboolean ret; guint32 sum32 = 0; g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = g_memory_input_stream_new_from_data(buf, sizeof(buf), NULL); ret = fu_input_stream_compute_sum32(stream, &sum32, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_READ); g_assert_false(ret); } static void fu_input_stream_chunkify_func(void) { gboolean ret; guint8 sum8 = 0; guint16 crc16 = 0x0; guint32 crc32 = 0xffffffff; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GInputStream) stream = NULL; g_autoptr(GError) error = NULL; g_autoptr(GBytes) blob = NULL; g_autofree gchar *checksum = NULL; g_autofree gchar *checksum2 = NULL; for (guint i = 0; i < 0x80000; i++) fu_byte_array_append_uint8(buf, i); blob = g_bytes_new(buf->data, buf->len); stream = g_memory_input_stream_new_from_bytes(blob); ret = fu_input_stream_compute_sum8(stream, &sum8, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(sum8, ==, fu_sum8_bytes(blob)); checksum = fu_input_stream_compute_checksum(stream, G_CHECKSUM_SHA1, &error); g_assert_no_error(error); g_assert_nonnull(checksum); checksum2 = g_compute_checksum_for_bytes(G_CHECKSUM_SHA1, blob); g_assert_cmpstr(checksum, ==, checksum2); ret = fu_input_stream_compute_crc16(stream, FU_CRC_KIND_B16_XMODEM, &crc16, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(crc16, ==, fu_crc16(FU_CRC_KIND_B16_XMODEM, buf->data, buf->len)); ret = fu_input_stream_compute_crc32(stream, FU_CRC_KIND_B32_STANDARD, &crc32, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(crc32, ==, fu_crc32(FU_CRC_KIND_B32_STANDARD, buf->data, buf->len)); } static void fu_lzma_func(void) { gboolean ret; g_autoptr(GByteArray) buf_in = g_byte_array_new(); g_autoptr(GBytes) blob_in = NULL; g_autoptr(GBytes) blob_orig = NULL; g_autoptr(GBytes) blob_out = NULL; g_autoptr(GError) error = NULL; /* create a repeating pattern */ for (guint i = 0; i < 10000; i++) { guint8 tmp = i % 8; g_byte_array_append(buf_in, &tmp, sizeof(tmp)); } blob_in = g_bytes_new(buf_in->data, buf_in->len); /* compress */ blob_out = fu_lzma_compress_bytes(blob_in, &error); g_assert_no_error(error); g_assert_nonnull(blob_out); g_assert_cmpint(g_bytes_get_size(blob_out), <, 500); /* decompress */ blob_orig = fu_lzma_decompress_bytes(blob_out, 128 * 1024 * 1024, &error); g_assert_no_error(error); g_assert_nonnull(blob_orig); ret = fu_bytes_compare(blob_in, blob_orig, &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_plugin_efi_x509_signature_func(void) { gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuEfiX509Signature) sig = fu_efi_x509_signature_new(); g_autoptr(FuEfiX509Device) device = fu_efi_x509_device_new(ctx, sig); g_autoptr(GError) error = NULL; fu_firmware_set_id(FU_FIRMWARE(sig), "0000000000000000000000000000000000000000"); fu_efi_x509_signature_set_issuer(sig, "C=UK,O=fwupd,CN=fwupd root CA 2012"); fu_efi_x509_signature_set_subject(sig, "C=UK,O=Hughski Ltd.,CN=Hughski Ltd. KEK CA 2012"); /* get issuer */ g_assert_cmpstr(fu_efi_x509_signature_get_issuer(sig), ==, "C=UK,O=fwupd,CN=fwupd root CA 2012"); g_assert_cmpstr(fu_efi_x509_signature_get_subject(sig), ==, "C=UK,O=Hughski Ltd.,CN=Hughski Ltd. KEK CA 2012"); g_assert_cmpstr(fu_efi_x509_signature_get_subject_name(sig), ==, "Hughski KEK CA"); g_assert_cmpstr(fu_efi_x509_signature_get_subject_vendor(sig), ==, "Hughski"); g_assert_cmpint(fu_firmware_get_version_raw(FU_FIRMWARE(sig)), ==, 2012); g_assert_cmpstr(fu_firmware_get_version(FU_FIRMWARE(sig)), ==, "2012"); /* create a device from the certificate */ ret = fu_device_probe(FU_DEVICE(device), &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(fu_device_get_version_raw(FU_DEVICE(device)), ==, 2012); g_assert_cmpstr(fu_device_get_version(FU_DEVICE(device)), ==, "2012"); g_assert_cmpstr(fu_device_get_name(FU_DEVICE(device)), ==, "KEK CA"); g_assert_cmpstr(fu_device_get_vendor(FU_DEVICE(device)), ==, "Hughski"); g_assert_true(fu_device_has_instance_id(FU_DEVICE(device), "UEFI\\VENDOR_Hughski&NAME_Hughski-KEK-CA", FU_DEVICE_INSTANCE_FLAG_VISIBLE)); g_assert_true( fu_device_has_instance_id(FU_DEVICE(device), "UEFI\\CRT_0000000000000000000000000000000000000000", FU_DEVICE_INSTANCE_FLAG_VISIBLE)); } static void fu_plugin_efi_variable_authentication2_func(void) { FuFirmware *signer; gboolean ret; g_autofree gchar *fn = NULL; g_autofree gchar *str = NULL; g_autoptr(FuFirmware) efi_x509 = NULL; g_autoptr(FuFirmware) firmware = g_object_new(FU_TYPE_EFI_VARIABLE_AUTHENTICATION2, NULL); g_autoptr(GError) error = NULL; g_autoptr(GFile) file = NULL; g_autoptr(GPtrArray) signers = NULL; /* parse file */ fn = g_test_build_filename(G_TEST_DIST, "tests", "KEKUpdate.bin", NULL); if (!g_file_test(fn, G_FILE_TEST_EXISTS)) { g_test_skip("Missing KEKUpdate.bin"); return; } file = g_file_new_for_path(fn); ret = fu_firmware_parse_file(firmware, file, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); str = fu_firmware_to_string(firmware); g_debug("%s", str); /* get EFI sig */ efi_x509 = fu_firmware_get_image_by_id(firmware, "dec64d7746d983db3774829a00bf829d9f19e9cf", &error); g_assert_no_error(error); g_assert_nonnull(efi_x509); g_assert_cmpstr("C=US,O=Microsoft Corporation,CN=Microsoft RSA Devices Root CA 2021", ==, fu_efi_x509_signature_get_issuer(FU_EFI_X509_SIGNATURE(efi_x509))); g_assert_cmpstr("C=US,O=Microsoft Corporation,CN=Microsoft Corporation KEK 2K CA 2023", ==, fu_efi_x509_signature_get_subject(FU_EFI_X509_SIGNATURE(efi_x509))); /* get signer */ signers = fu_efi_variable_authentication2_get_signers(FU_EFI_VARIABLE_AUTHENTICATION2(firmware)); g_assert_nonnull(signers); g_assert_cmpint(signers->len, ==, 1); signer = g_ptr_array_index(signers, 0); g_assert_cmpstr("CN=DO NOT TRUST - AMI Test PK", ==, fu_x509_certificate_get_issuer(FU_X509_CERTIFICATE(signer))); g_assert_cmpstr("CN=DO NOT TRUST - AMI Test PK", ==, fu_x509_certificate_get_subject(FU_X509_CERTIFICATE(signer))); } static void fu_plugin_efi_signature_list_func(void) { FuEfiX509Signature *sig; g_autoptr(FuEfiX509Signature) sig2022 = fu_efi_x509_signature_new(); g_autoptr(FuEfiX509Signature) sig2023 = fu_efi_x509_signature_new(); g_autoptr(FuEfiX509Signature) sig2024 = fu_efi_x509_signature_new(); g_autoptr(FuFirmware) siglist = fu_efi_signature_list_new(); g_autoptr(GPtrArray) sigs_newest = NULL; fu_efi_x509_signature_set_subject(sig2022, "C=UK,O=Hughski,CN=Hughski Ltd. KEK CA 2022"); fu_efi_x509_signature_set_subject(sig2023, "C=UK,O=Hughski,CN=Hughski Ltd. KEK CA 2023"); fu_efi_x509_signature_set_subject(sig2024, "C=UK,O=Hughski,CN=Hughski Ltd. KEK CA 2024"); /* 2022 -> 2024 -> 2023 */ fu_firmware_add_image(siglist, FU_FIRMWARE(sig2022)); fu_firmware_add_image(siglist, FU_FIRMWARE(sig2024)); fu_firmware_add_image(siglist, FU_FIRMWARE(sig2023)); /* only one */ sigs_newest = fu_efi_signature_list_get_newest(FU_EFI_SIGNATURE_LIST(siglist)); g_assert_cmpint(sigs_newest->len, ==, 1); sig = g_ptr_array_index(sigs_newest, 0); g_assert_cmpint(fu_firmware_get_version_raw(FU_FIRMWARE(sig)), ==, 2024); } static void fu_device_udev_func(void) { g_autofree gchar *prop = NULL; g_autofree gchar *sysfs_path = g_test_build_filename(G_TEST_DIST, "tests", NULL); g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuUdevDevice) udev_device = fu_udev_device_new(ctx, sysfs_path); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) attrs = NULL; prop = fu_udev_device_read_property(udev_device, "MODALIAS", &error); g_assert_no_error(error); g_assert_cmpstr(prop, ==, "hdaudio:v10EC0298r00100103a01"); /* list all the files in the directory */ attrs = fu_udev_device_list_sysfs(udev_device, &error); g_assert_no_error(error); g_assert_nonnull(attrs); g_assert_cmpint(attrs->len, >, 10); } static void fu_cab_checksum_func(void) { guint8 buf[] = {0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80}; guint32 checksums[] = { 0xc0404040, 0x40604060, 0x40307070, 0x40302040, 0x40302010, 0x102030, 0x1020, 0x10, 0x0, }; for (guint i = 0; i <= sizeof(buf); i++) { gboolean ret; guint32 checksum = 0x0; g_autoptr(GError) error = NULL; ret = fu_cab_firmware_compute_checksum(buf, sizeof(buf) - i, &checksum, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(checksum, ==, checksums[i]); } } static void fu_efi_lz77_decompressor_func(void) { gboolean ret; g_autofree gchar *csum_legacy = NULL; g_autofree gchar *csum_tiano = NULL; g_autofree gchar *filename_legacy = NULL; g_autofree gchar *filename_tiano = NULL; g_autoptr(FuFirmware) lz77_decompressor_legacy = fu_efi_lz77_decompressor_new(); g_autoptr(FuFirmware) lz77_decompressor_tiano = fu_efi_lz77_decompressor_new(); g_autoptr(GBytes) blob_legacy2 = NULL; g_autoptr(GBytes) blob_legacy = NULL; g_autoptr(GBytes) blob_tiano2 = NULL; g_autoptr(GBytes) blob_tiano = NULL; g_autoptr(GError) error = NULL; filename_tiano = g_test_build_filename(G_TEST_DIST, "tests", "efi-lz77-tiano.bin", NULL); if (!g_file_test(filename_tiano, G_FILE_TEST_EXISTS)) { g_test_skip("Missing efi-lz77-tiano.bin"); return; } blob_tiano = fu_bytes_get_contents(filename_tiano, &error); g_assert_no_error(error); g_assert_nonnull(blob_tiano); g_assert_cmpint(g_bytes_get_size(blob_tiano), ==, 144); ret = fu_firmware_parse_bytes(lz77_decompressor_tiano, blob_tiano, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); blob_tiano2 = fu_firmware_get_bytes(lz77_decompressor_tiano, &error); g_assert_no_error(error); g_assert_nonnull(blob_tiano2); g_assert_cmpint(g_bytes_get_size(blob_tiano2), ==, 276); csum_tiano = g_compute_checksum_for_bytes(G_CHECKSUM_SHA1, blob_tiano2); g_assert_cmpstr(csum_tiano, ==, "40f7fbaff684a6bcf67c81b3079422c2529741e1"); filename_legacy = g_test_build_filename(G_TEST_DIST, "tests", "efi-lz77-legacy.bin", NULL); if (!g_file_test(filename_tiano, G_FILE_TEST_EXISTS)) { g_test_skip("Missing efi-lz77-legacy.bin"); return; } blob_legacy = fu_bytes_get_contents(filename_legacy, &error); g_assert_no_error(error); g_assert_nonnull(blob_legacy); g_assert_cmpint(g_bytes_get_size(blob_legacy), ==, 144); ret = fu_firmware_parse_bytes(lz77_decompressor_legacy, blob_tiano, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); blob_legacy2 = fu_firmware_get_bytes(lz77_decompressor_legacy, &error); g_assert_no_error(error); g_assert_nonnull(blob_legacy2); g_assert_cmpint(g_bytes_get_size(blob_legacy2), ==, 276); csum_legacy = g_compute_checksum_for_bytes(G_CHECKSUM_SHA1, blob_legacy2); g_assert_cmpstr(csum_legacy, ==, "40f7fbaff684a6bcf67c81b3079422c2529741e1"); } static void fu_input_stream_func(void) { gboolean ret; gsize bufsz = 0; gsize streamsz = 0; g_autofree gchar *csum2 = NULL; g_autofree gchar *csum = NULL; g_autofree gchar *fn = NULL; g_autofree guint8 *buf2 = NULL; g_autofree guint8 *buf = NULL; g_autoptr(GError) error = NULL; g_autoptr(GFile) file = NULL; g_autoptr(GInputStream) stream = NULL; fn = g_test_build_filename(G_TEST_DIST, "tests", "dfu.builder.xml", NULL); g_assert_nonnull(fn); ret = g_file_get_contents(fn, (gchar **)&buf, &bufsz, &error); g_assert_no_error(error); g_assert_true(ret); fu_dump_raw(G_LOG_DOMAIN, "src", buf, bufsz); csum = g_compute_checksum_for_data(G_CHECKSUM_MD5, (const guchar *)buf, bufsz); file = g_file_new_for_path(fn); stream = G_INPUT_STREAM(g_file_read(file, NULL, &error)); g_assert_no_error(error); g_assert_nonnull(stream); /* verify size */ ret = fu_input_stream_size(stream, &streamsz, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(streamsz, ==, bufsz); /* verify checksum */ csum2 = fu_input_stream_compute_checksum(stream, G_CHECKSUM_MD5, &error); g_assert_no_error(error); g_assert_nonnull(csum2); g_assert_cmpstr(csum, ==, csum2); /* read first byte */ buf2 = g_malloc0(bufsz); ret = fu_input_stream_read_safe(stream, buf2, bufsz, 0x0, 0x0, 1, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(buf[0], ==, buf2[0]); fu_dump_raw(G_LOG_DOMAIN, "dst", buf2, bufsz); /* read bytes 2,3 */ ret = fu_input_stream_read_safe(stream, buf2, bufsz, 0x1, /* offset */ 0x1, /* seek */ 2, /* count */ &error); g_assert_no_error(error); g_assert_true(ret); fu_dump_raw(G_LOG_DOMAIN, "dst", buf2, bufsz); g_assert_cmpint(buf[1], ==, buf2[1]); g_assert_cmpint(buf[2], ==, buf2[2]); /* read past end of stream */ ret = fu_input_stream_read_safe(stream, buf2, bufsz, 0x0, /* offset */ 0x20, /* seek */ bufsz, /* count */ &error); fu_dump_raw(G_LOG_DOMAIN, "dst", buf2, bufsz); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_READ); g_assert_false(ret); } static void fu_plugin_struct_bits_func(void) { g_autofree gchar *str1 = NULL; g_autoptr(FuStructSelfTestBits) st2 = NULL; g_autoptr(FuStructSelfTestBits) st = fu_struct_self_test_bits_new(); g_autoptr(GError) error = NULL; /* 0b1111 + 0b1 + 0b0010 = 0b111110010 -> 0x1F2 */ g_assert_cmpint(st->len, ==, 4); fu_dump_raw(G_LOG_DOMAIN, "buf", st->data, st->len); g_assert_cmpint(st->data[0], ==, 0xF2); g_assert_cmpint(st->data[1], ==, 0x01); g_assert_cmpint(st->data[2], ==, 0x0); g_assert_cmpint(st->data[3], ==, 0x0); st2 = fu_struct_self_test_bits_parse(st->data, st->len, 0x0, &error); g_assert_no_error(error); g_assert_nonnull(st2); g_assert_cmpint(fu_struct_self_test_bits_get_lower(st2), ==, 0x2); g_assert_cmpint(fu_struct_self_test_bits_get_middle(st2), ==, 0b1); g_assert_cmpint(fu_struct_self_test_bits_get_upper(st2), ==, 0xF); str1 = fu_struct_self_test_bits_to_string(st2); g_assert_cmpstr(str1, ==, "FuStructSelfTestBits:\n" " lower: 0x2 [two]\n" " middle: 0x1\n" " upper: 0xf"); /* set all to maximum value */ fu_struct_self_test_bits_set_lower(st2, G_MAXUINT32); fu_struct_self_test_bits_set_middle(st2, G_MAXUINT32); fu_struct_self_test_bits_set_upper(st2, G_MAXUINT32); g_assert_cmpint(fu_struct_self_test_bits_get_lower(st2), ==, 0xF); g_assert_cmpint(fu_struct_self_test_bits_get_middle(st2), ==, 0x1); g_assert_cmpint(fu_struct_self_test_bits_get_upper(st2), ==, 0xF); } static void fu_plugin_struct_list_func(void) { g_autofree gchar *str = NULL; g_autoptr(FuStructSelfTestList) st = fu_struct_self_test_list_new(); for (guint i = 0; i < FU_STRUCT_SELF_TEST_LIST_N_ELEMENTS_BASIC; i++) { fu_struct_self_test_list_set_basic(st, i, i * 16); g_assert_cmpint(fu_struct_self_test_list_get_basic(st, i), ==, i * 16); } for (guint i = 0; i < FU_STRUCT_SELF_TEST_LIST_N_ELEMENTS_MEMBERS; i++) { gboolean ret; g_autoptr(FuStructSelfTestListMember) st2 = fu_struct_self_test_list_member_new(); g_autoptr(FuStructSelfTestListMember) st3 = NULL; g_autoptr(GError) error = NULL; fu_struct_self_test_list_member_set_data1(st2, i * 16); fu_struct_self_test_list_member_set_data2(st2, i * 32); ret = fu_struct_self_test_list_set_members(st, i, st2, &error); g_assert_no_error(error); g_assert_true(ret); st3 = fu_struct_self_test_list_get_members(st, i); g_assert_cmpint(fu_struct_self_test_list_member_get_data1(st3), ==, i * 16); g_assert_cmpint(fu_struct_self_test_list_member_get_data2(st3), ==, i * 32); } /* size */ str = fu_byte_array_to_string(st); g_assert_cmpstr( str, ==, "000000001000000020000000300000004000000050000000600000007000000000001020204030604080"); } static void fu_plugin_struct_func(void) { gboolean ret; g_autoptr(GByteArray) st = fu_struct_self_test_new(); g_autoptr(GByteArray) st2 = NULL; g_autoptr(GByteArray) st3 = NULL; g_autoptr(GError) error = NULL; g_autofree gchar *str1 = NULL; g_autofree gchar *str2 = NULL; g_autofree gchar *oem_table_id = NULL; /* size */ g_assert_cmpint(st->len, ==, 51); /* getters and setters */ fu_struct_self_test_set_revision(st, 0xFF); fu_struct_self_test_set_length(st, 0xDEAD); ret = fu_struct_self_test_set_oem_table_id(st, "X", &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(fu_struct_self_test_get_revision(st), ==, 0xFF); g_assert_cmpint(fu_struct_self_test_get_length(st), ==, 0xDEAD); /* pack */ str1 = fu_byte_array_to_string(st); g_assert_cmpstr(str1, ==, "12345678adde0000ff000000000000000000000000000000004142434445465800000000" "00000000000000dfdfdfdf00000000"); /* parse */ st2 = fu_struct_self_test_parse(st->data, st->len, 0x0, &error); g_assert_no_error(error); g_assert_nonnull(st2); g_assert_cmpint(fu_struct_self_test_get_revision(st2), ==, 0xFF); g_assert_cmpint(fu_struct_self_test_get_length(st2), ==, 0xDEAD); oem_table_id = fu_struct_self_test_get_oem_table_id(st2); g_assert_cmpstr(oem_table_id, ==, "X"); /* to string */ str2 = fu_struct_self_test_to_string(st); g_assert_cmpstr(str2, ==, "FuStructSelfTest:\n" " length: 0xdead\n" " revision: 0xff [all]\n" " owner: 00000000-0000-0000-0000-000000000000\n" " oem_table_id: X\n" " oem_revision: 0x0\n" " asl_compiler_id: 0xDFDFDFDF\n" " asl_compiler_revision: 0x0"); /* parse failing signature */ st->data[0] = 0xFF; st3 = fu_struct_self_test_parse(st->data, st->len, 0x0, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA); g_assert_null(st3); g_clear_error(&error); ret = fu_struct_self_test_validate(st->data, st->len, 0x0, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA); g_assert_false(ret); } static void fu_plugin_struct_wrapped_func(void) { gboolean ret; g_autofree gchar *str1 = NULL; g_autofree gchar *str2 = NULL; g_autofree gchar *str4 = NULL; g_autoptr(GByteArray) st2 = NULL; g_autoptr(GByteArray) st3 = NULL; g_autoptr(GByteArray) st_base2 = NULL; g_autoptr(GByteArray) st_base = fu_struct_self_test_new(); g_autoptr(GByteArray) st = fu_struct_self_test_wrapped_new(); g_autoptr(GError) error = NULL; /* size */ g_assert_cmpint(st->len, ==, 53); /* getters and setters */ fu_struct_self_test_wrapped_set_less(st, 0x99); fu_struct_self_test_wrapped_set_more(st, 0x12); g_assert_cmpint(fu_struct_self_test_wrapped_get_more(st), ==, 0x12); str1 = fu_byte_array_to_string(st); g_assert_cmpstr(str1, ==, "991234567833000000000000000000000000000000000000000041424344454600000000" "0000000000000000dfdfdfdf0000000012"); /* modify the base */ fu_struct_self_test_set_revision(st_base, 0xFE); ret = fu_struct_self_test_wrapped_set_base(st, st_base, &error); g_assert_no_error(error); g_assert_true(ret); str4 = fu_byte_array_to_string(st); g_assert_cmpstr(str4, ==, "991234567833000000fe0000000000000000000000000000000041424344454600000000" "0000000000000000dfdfdfdf0000000012"); /* parse */ st2 = fu_struct_self_test_wrapped_parse(st->data, st->len, 0x0, &error); g_assert_no_error(error); g_assert_nonnull(st2); g_assert_cmpint(fu_struct_self_test_wrapped_get_more(st), ==, 0x12); st_base2 = fu_struct_self_test_wrapped_get_base(st); g_assert_cmpint(fu_struct_self_test_get_revision(st_base2), ==, 0xFE); /* to string */ str2 = fu_struct_self_test_wrapped_to_string(st); g_debug("%s", str2); g_assert_cmpstr(str2, ==, "FuStructSelfTestWrapped:\n" " less: 0x99\n" " base: FuStructSelfTest:\n" " length: 0x33\n" " revision: 0xfe\n" " owner: 00000000-0000-0000-0000-000000000000\n" " oem_revision: 0x0\n" " asl_compiler_id: 0xDFDFDFDF\n" " asl_compiler_revision: 0x0\n" " more: 0x12"); /* parse failing signature */ st->data[FU_STRUCT_SELF_TEST_WRAPPED_OFFSET_BASE] = 0xFF; st3 = fu_struct_self_test_wrapped_parse(st->data, st->len, 0x0, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA); g_assert_null(st3); g_clear_error(&error); ret = fu_struct_self_test_wrapped_validate(st->data, st->len, 0x0, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA); g_assert_false(ret); } static void fu_efi_load_option_path_func(void) { const gchar *tmp; g_autofree gchar *blobstr = NULL; g_autoptr(FuEfiDevicePathList) devpathlist = fu_efi_device_path_list_new(); g_autoptr(FuEfiLoadOption) loadopt = fu_efi_load_option_new(); g_autoptr(GBytes) blob = NULL; g_autoptr(GError) error = NULL; g_assert_cmpint(fu_efi_load_option_get_kind(loadopt), ==, FU_EFI_LOAD_OPTION_KIND_UNKNOWN); fu_efi_load_option_set_metadata(loadopt, FU_EFI_LOAD_OPTION_METADATA_PATH, "/foo"); g_assert_cmpint(fu_efi_load_option_get_kind(loadopt), ==, FU_EFI_LOAD_OPTION_KIND_PATH); tmp = fu_efi_load_option_get_metadata(loadopt, FU_EFI_LOAD_OPTION_METADATA_PATH, &error); g_assert_no_error(error); g_assert_cmpstr(tmp, ==, "/foo"); fu_firmware_set_id(FU_FIRMWARE(loadopt), "id"); fu_firmware_add_image(FU_FIRMWARE(loadopt), FU_FIRMWARE(devpathlist)); blob = fu_firmware_write(FU_FIRMWARE(loadopt), &error); g_assert_no_error(error); g_assert_nonnull(blob); blobstr = fu_bytes_to_string(blob); g_assert_cmpstr(blobstr, ==, "0100000004006900640000007fff04005c002f0066006f006f000000"); } static void fu_efi_load_option_hive_func(void) { g_autofree gchar *blobstr = NULL; g_autoptr(FuEfiDevicePathList) devpathlist = fu_efi_device_path_list_new(); g_autoptr(FuEfiLoadOption) loadopt = fu_efi_load_option_new(); g_autoptr(GBytes) blob = NULL; g_autoptr(GError) error = NULL; g_assert_cmpint(fu_efi_load_option_get_kind(loadopt), ==, FU_EFI_LOAD_OPTION_KIND_UNKNOWN); fu_efi_load_option_set_metadata(loadopt, FU_EFI_LOAD_OPTION_METADATA_PATH, "/foo"); fu_efi_load_option_set_metadata(loadopt, FU_EFI_LOAD_OPTION_METADATA_CMDLINE, "noacpi"); g_assert_cmpint(fu_efi_load_option_get_kind(loadopt), ==, FU_EFI_LOAD_OPTION_KIND_HIVE); fu_firmware_set_id(FU_FIRMWARE(loadopt), "id"); fu_firmware_add_image(FU_FIRMWARE(loadopt), FU_FIRMWARE(devpathlist)); blob = fu_firmware_write(FU_FIRMWARE(loadopt), &error); g_assert_no_error(error); g_assert_nonnull(blob); g_assert_cmpint(g_bytes_get_size(blob), ==, 512); blobstr = fu_bytes_to_string(blob); /* get rid of extra NUL butes */ blobstr[120] = '\0'; g_assert_cmpstr(blobstr, ==, "0100000004006900640000007fff04004849564501020b0f4a6ea20405000000706174685c" "2f666f6f0706000000636d646c696e656e6f6163706900"); } static void fu_efi_load_option_func(void) { g_autoptr(FuEfivars) efivars = fu_efivars_new(); /* * 0000 = Linux-Firmware-Updater * 0001 = Fedora * 0002 = Windows Boot Manager */ for (guint16 i = 0; i < 3; i++) { g_autoptr(GError) error = NULL; g_autoptr(FuEfiLoadOption) load_option = fu_efivars_get_boot_entry(efivars, i, &error); g_autoptr(GBytes) fw = NULL; g_autofree gchar *str = NULL; if (load_option == NULL) { g_debug("failed: %s", error->message); continue; } str = fu_firmware_to_string(FU_FIRMWARE(load_option)); g_debug("%s", str); fw = fu_firmware_write(FU_FIRMWARE(load_option), &error); g_assert_no_error(error); g_assert_nonnull(fw); } } int main(int argc, char **argv) { g_autofree gchar *testdatadir = NULL; (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); g_type_ensure(FU_TYPE_IFD_BIOS); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); (void)g_setenv("G_MESSAGES_DEBUG", "all", TRUE); testdatadir = g_test_build_filename(G_TEST_DIST, "tests", NULL); (void)g_setenv("FWUPD_DATADIR", testdatadir, TRUE); (void)g_setenv("FWUPD_DATADIR_VENDOR_IDS", testdatadir, TRUE); (void)g_setenv("FWUPD_LIBDIR_PKG", testdatadir, TRUE); (void)g_setenv("FWUPD_SYSCONFDIR", testdatadir, TRUE); (void)g_setenv("FWUPD_SYSFSFWDIR", testdatadir, TRUE); (void)g_setenv("FWUPD_SYSFSFWATTRIBDIR", testdatadir, TRUE); (void)g_setenv("FWUPD_SYSFSDMIDIR", testdatadir, TRUE); (void)g_setenv("FWUPD_PROCFS", testdatadir, TRUE); (void)g_setenv("FWUPD_LOCALSTATEDIR", "/tmp/fwupd-self-test/var", TRUE); (void)g_setenv("FWUPD_PROFILE", "1", TRUE); (void)g_setenv("FWUPD_EFIVARS", "dummy", TRUE); (void)g_setenv("CACHE_DIRECTORY", "/tmp/fwupd-self-test/cache", TRUE); g_test_add_func("/fwupd/cab{checksum}", fu_cab_checksum_func); g_test_add_func("/fwupd/efi-lz77{decompressor}", fu_efi_lz77_decompressor_func); g_test_add_func("/fwupd/input-stream", fu_input_stream_func); g_test_add_func("/fwupd/input-stream{sum-overflow}", fu_input_stream_sum_overflow_func); g_test_add_func("/fwupd/input-stream{chunkify}", fu_input_stream_chunkify_func); g_test_add_func("/fwupd/input-stream{find}", fu_input_stream_find_func); g_test_add_func("/fwupd/partial-input-stream", fu_partial_input_stream_func); g_test_add_func("/fwupd/partial-input-stream{closed-base}", fu_partial_input_stream_closed_base_func); g_test_add_func("/fwupd/partial-input-stream{simple}", fu_partial_input_stream_simple_func); g_test_add_func("/fwupd/partial-input-stream{composite}", fu_partial_input_stream_composite_func); g_test_add_func("/fwupd/composite-input-stream", fu_composite_input_stream_func); g_test_add_func("/fwupd/struct", fu_plugin_struct_func); g_test_add_func("/fwupd/struct{bits}", fu_plugin_struct_bits_func); g_test_add_func("/fwupd/struct{list}", fu_plugin_struct_list_func); g_test_add_func("/fwupd/struct{wrapped}", fu_plugin_struct_wrapped_func); g_test_add_func("/fwupd/plugin{quirks-append}", fu_plugin_quirks_append_func); g_test_add_func("/fwupd/quirks{vendor-ids}", fu_quirks_vendor_ids_func); g_test_add_func("/fwupd/string{password-mask}", fu_strpassmask_func); g_test_add_func("/fwupd/string{strsplit-stream}", fu_strsplit_stream_func); g_test_add_func("/fwupd/lzma", fu_lzma_func); g_test_add_func("/fwupd/common{strnsplit}", fu_strsplit_func); g_test_add_func("/fwupd/common{olson-timezone-id}", fu_common_olson_timezone_id_func); g_test_add_func("/fwupd/common{memmem}", fu_common_memmem_func); if (g_test_slow()) g_test_add_func("/fwupd/progress", fu_progress_func); g_test_add_func("/fwupd/progress{scaling}", fu_progress_scaling_func); g_test_add_func("/fwupd/progress{child}", fu_progress_child_func); g_test_add_func("/fwupd/progress{child-finished}", fu_progress_child_finished); g_test_add_func("/fwupd/progress{parent-1-step}", fu_progress_parent_one_step_proxy_func); g_test_add_func("/fwupd/progress{no-equal}", fu_progress_non_equal_steps_func); g_test_add_func("/fwupd/progress{finish}", fu_progress_finish_func); g_test_add_func("/fwupd/progress{global-fraction}", fu_progress_global_fraction_func); g_test_add_func("/fwupd/bios-attrs{load}", fu_bios_settings_load_func); g_test_add_func("/fwupd/security-attrs{hsi}", fu_security_attrs_hsi_func); g_test_add_func("/fwupd/security-attrs{compare}", fu_security_attrs_compare_func); g_test_add_func("/fwupd/config", fu_config_func); g_test_add_func("/fwupd/plugin", fu_plugin_func); g_test_add_func("/fwupd/plugin{vfuncs}", fu_plugin_vfuncs_func); g_test_add_func("/fwupd/plugin{device-gtype}", fu_plugin_device_gtype_func); g_test_add_func("/fwupd/plugin{backend-device}", fu_plugin_backend_device_func); g_test_add_func("/fwupd/plugin{backend-proxy-device}", fu_plugin_backend_proxy_device_func); g_test_add_func("/fwupd/plugin{config}", fu_plugin_config_func); g_test_add_func("/fwupd/plugin{devices}", fu_plugin_devices_func); g_test_add_func("/fwupd/plugin{device-inhibit-children}", fu_plugin_device_inhibit_children_func); g_test_add_func("/fwupd/plugin{delay}", fu_plugin_delay_func); g_test_add_func("/fwupd/plugin{quirks}", fu_plugin_quirks_func); g_test_add_func("/fwupd/plugin{fdt}", fu_plugin_fdt_func); g_test_add_func("/fwupd/plugin{quirks-performance}", fu_plugin_quirks_performance_func); g_test_add_func("/fwupd/plugin{quirks-device}", fu_plugin_quirks_device_func); g_test_add_func("/fwupd/backend", fu_backend_func); g_test_add_func("/fwupd/backend{emulate}", fu_backend_emulate_func); g_test_add_func("/fwupd/chunk", fu_chunk_func); g_test_add_func("/fwupd/chunks", fu_chunk_array_func); g_test_add_func("/fwupd/common{align-up}", fu_common_align_up_func); g_test_add_func("/fwupd/volume{gpt-type}", fu_volume_gpt_type_func); g_test_add_func("/fwupd/common{bitwise}", fu_common_bitwise_func); g_test_add_func("/fwupd/common{byte-array}", fu_common_byte_array_func); g_test_add_func("/fwupd/common{crc}", fu_common_crc_func); g_test_add_func("/fwupd/common{guid}", fu_common_guid_func); g_test_add_func("/fwupd/common{string-append-kv}", fu_string_append_func); g_test_add_func("/fwupd/common{version-guess-format}", fu_version_guess_format_func); g_test_add_func("/fwupd/common{strtoull}", fu_strtoull_func); g_test_add_func("/fwupd/common{strtoll}", fu_strtoll_func); g_test_add_func("/fwupd/common{version}", fu_common_version_func); g_test_add_func("/fwupd/common{version-semver}", fu_version_semver_func); g_test_add_func("/fwupd/common{vercmp}", fu_common_vercmp_func); g_test_add_func("/fwupd/common{strstrip}", fu_strstrip_func); g_test_add_func("/fwupd/common{endian}", fu_common_endian_func); g_test_add_func("/fwupd/common{bytes-get-data}", fu_common_bytes_get_data_func); g_test_add_func("/fwupd/common{kernel-lockdown}", fu_common_kernel_lockdown_func); g_test_add_func("/fwupd/common{kernel-search}", fu_common_kernel_search_func); g_test_add_func("/fwupd/common{strsafe}", fu_strsafe_func); g_test_add_func("/fwupd/common{cpuid}", fu_cpuid_func); g_test_add_func("/fwupd/msgpack", fu_msgpack_func); g_test_add_func("/fwupd/msgpack{binary-stream}", fu_msgpack_binary_stream_func); g_test_add_func("/fwupd/msgpack{parse-binary}", fu_msgpack_parse_binary_func); g_test_add_func("/fwupd/msgpack{lookup}", fu_msgpack_lookup_func); g_test_add_func("/fwupd/efi-load-option", fu_efi_load_option_func); g_test_add_func("/fwupd/efi-load-option{path}", fu_efi_load_option_path_func); g_test_add_func("/fwupd/efi-load-option{hive}", fu_efi_load_option_hive_func); g_test_add_func("/fwupd/efi-x509-signature", fu_plugin_efi_x509_signature_func); g_test_add_func("/fwupd/efi-signature-list", fu_plugin_efi_signature_list_func); g_test_add_func("/fwupd/efi-variable-authentication2", fu_plugin_efi_variable_authentication2_func); g_test_add_func("/fwupd/efivar", fu_efivar_func); g_test_add_func("/fwupd/efivar{bootxxxx}", fu_efivar_boot_func); g_test_add_func("/fwupd/hwids", fu_hwids_func); g_test_add_func("/fwupd/context{flags}", fu_context_flags_func); g_test_add_func("/fwupd/context{backends}", fu_context_backends_func); g_test_add_func("/fwupd/context{hwids-dmi}", fu_context_hwids_dmi_func); g_test_add_func("/fwupd/context{hwids-fdt}", fu_context_hwids_fdt_func); g_test_add_func("/fwupd/context{firmware-gtypes}", fu_context_firmware_gtypes_func); g_test_add_func("/fwupd/context{state}", fu_context_state_func); g_test_add_func("/fwupd/context{udev-subsystems}", fu_context_udev_subsystems_func); g_test_add_func("/fwupd/string{utf16}", fu_string_utf16_func); g_test_add_func("/fwupd/smbios", fu_smbios_func); g_test_add_func("/fwupd/smbios3", fu_smbios3_func); g_test_add_func("/fwupd/kernel{cmdline}", fu_kernel_cmdline_func); g_test_add_func("/fwupd/kernel{config}", fu_kernel_config_func); g_test_add_func("/fwupd/hid{descriptor}", fu_hid_descriptor_func); g_test_add_func("/fwupd/hid{descriptor-container}", fu_hid_descriptor_container_func); g_test_add_func("/fwupd/firmware", fu_firmware_func); g_test_add_func("/fwupd/firmware{common}", fu_firmware_common_func); g_test_add_func("/fwupd/firmware{convert-version}", fu_firmware_convert_version_func); g_test_add_func("/fwupd/firmware{csv}", fu_firmware_csv_func); g_test_add_func("/fwupd/firmware{archive}", fu_firmware_archive_func); g_test_add_func("/fwupd/firmware{linear}", fu_firmware_linear_func); g_test_add_func("/fwupd/firmware{dedupe}", fu_firmware_dedupe_func); g_test_add_func("/fwupd/firmware{build}", fu_firmware_build_func); g_test_add_func("/fwupd/firmware{raw-aligned}", fu_firmware_raw_aligned_func); g_test_add_func("/fwupd/firmware{ihex}", fu_firmware_ihex_func); g_test_add_func("/fwupd/firmware{ihex-offset}", fu_firmware_ihex_offset_func); g_test_add_func("/fwupd/firmware{ihex-signed}", fu_firmware_ihex_signed_func); g_test_add_func("/fwupd/firmware{srec-tokenization}", fu_firmware_srec_tokenization_func); g_test_add_func("/fwupd/firmware{srec}", fu_firmware_srec_func); g_test_add_func("/fwupd/firmware{fdt}", fu_firmware_fdt_func); g_test_add_func("/fwupd/firmware{fit}", fu_firmware_fit_func); g_test_add_func("/fwupd/firmware{ifwi-cpd}", fu_firmware_ifwi_cpd_func); g_test_add_func("/fwupd/firmware{ifwi-fpt}", fu_firmware_ifwi_fpt_func); g_test_add_func("/fwupd/firmware{oprom}", fu_firmware_oprom_func); g_test_add_func("/fwupd/firmware{dfu}", fu_firmware_dfu_func); g_test_add_func("/fwupd/firmware{dfu-patch}", fu_firmware_dfu_patch_func); g_test_add_func("/fwupd/firmware{dfuse}", fu_firmware_dfuse_func); g_test_add_func("/fwupd/firmware{builder-round-trip}", fu_firmware_builder_round_trip_func); g_test_add_func("/fwupd/firmware{fmap}", fu_firmware_fmap_func); g_test_add_func("/fwupd/firmware{gtypes}", fu_firmware_new_from_gtypes_func); g_test_add_func("/fwupd/archive{invalid}", fu_archive_invalid_func); g_test_add_func("/fwupd/archive{cab}", fu_archive_cab_func); g_test_add_func("/fwupd/device", fu_device_func); g_test_add_func("/fwupd/device{udev}", fu_device_udev_func); g_test_add_func("/fwupd/device{event}", fu_device_event_func); g_test_add_func("/fwupd/device{event-uncompressed}", fu_device_event_uncompressed_func); g_test_add_func("/fwupd/device{event-donor}", fu_device_event_donor_func); g_test_add_func("/fwupd/device{vfuncs}", fu_device_vfuncs_func); g_test_add_func("/fwupd/device{instance-ids}", fu_device_instance_ids_func); g_test_add_func("/fwupd/device{composite-id}", fu_device_composite_id_func); g_test_add_func("/fwupd/device{flags}", fu_device_flags_func); g_test_add_func("/fwupd/device{private-flags}", fu_device_custom_flags_func); g_test_add_func("/fwupd/device{inhibit}", fu_device_inhibit_func); g_test_add_func("/fwupd/device{inhibit-updateable}", fu_device_inhibit_updateable_func); g_test_add_func("/fwupd/device{parent}", fu_device_parent_func); g_test_add_func("/fwupd/device{children}", fu_device_children_func); g_test_add_func("/fwupd/device{incorporate}", fu_device_incorporate_func); g_test_add_func("/fwupd/device{incorporate-flag}", fu_device_incorporate_flag_func); g_test_add_func("/fwupd/device{incorporate-non-generic}", fu_device_incorporate_non_generic_func); g_test_add_func("/fwupd/device{incorporate-descendant}", fu_device_incorporate_descendant_func); if (g_test_slow()) g_test_add_func("/fwupd/device{poll}", fu_device_poll_func); g_test_add_func("/fwupd/device-locker{success}", fu_device_locker_func); g_test_add_func("/fwupd/device-locker{fail}", fu_device_locker_fail_func); g_test_add_func("/fwupd/device{name}", fu_device_name_func); g_test_add_func("/fwupd/device{rescan}", fu_device_rescan_func); g_test_add_func("/fwupd/device{metadata}", fu_device_metadata_func); g_test_add_func("/fwupd/device{open-refcount}", fu_device_open_refcount_func); g_test_add_func("/fwupd/device{version-format}", fu_device_version_format_func); g_test_add_func("/fwupd/device{version-format-raw}", fu_device_version_format_raw_func); g_test_add_func("/fwupd/device{retry-success}", fu_device_retry_success_func); g_test_add_func("/fwupd/device{retry-failed}", fu_device_retry_failed_func); g_test_add_func("/fwupd/device{retry-hardware}", fu_device_retry_hardware_func); g_test_add_func("/fwupd/device{cfi-device}", fu_device_cfi_device_func); g_test_add_func("/fwupd/device{progress}", fu_plugin_device_progress_func); return g_test_run(); } fwupd-2.0.10/libfwupdplugin/fu-self-test.rs000066400000000000000000000023541501337203100206350ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[repr(u8)] enum FuSelfTestRevision { None = 0x0, All = 0xF_F, } #[derive(New, Validate, Parse, ToString, Default)] #[repr(C, packed)] struct FuStructSelfTest { signature: u32be == 0x1234_5678, length: u32le = $struct_size, // bytes revision: FuSelfTestRevision, owner: Guid, oem_id: [char; 6] == "ABCDEF", oem_table_id: [char; 8], oem_revision: u32le, asl_compiler_id: [u8; 4] = 0xDF, asl_compiler_revision: u32le, } #[derive(New, Validate, Parse, ToString)] #[repr(C, packed)] struct FuStructSelfTestWrapped { less: u8, base: FuStructSelfTest, more: u8, } #[repr(u4)] enum FuStructSelfTestLower { None = 0x0, One = 0x1, Two = 0x2, } #[derive(New, Parse, ToString, Default)] #[repr(C, packed)] struct FuStructSelfTestBits { lower: FuStructSelfTestLower = Two, middle: u1 = 0b1, upper: u4 = 0xF, } #[derive(New, Getters)] #[repr(C, packed)] struct FuStructSelfTestListMember { data1: u8, data2: u8, } #[derive(New, Setters, Getters, ToString)] #[repr(C, packed)] struct FuStructSelfTestList { basic: [u32le; 8], members: [FuStructSelfTestListMember; 5], } fwupd-2.0.10/libfwupdplugin/fu-serio-device.c000066400000000000000000000050431501337203100211010ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuSerioDevice" #include "config.h" #include "fu-serio-device.h" #include "fu-udev-device-private.h" /** * FuSerioDevice * * See also: #FuUdevDevice */ G_DEFINE_TYPE(FuSerioDevice, fu_serio_device, FU_TYPE_UDEV_DEVICE) static gboolean fu_serio_device_probe(FuDevice *device, GError **error) { FuSerioDevice *self = FU_SERIO_DEVICE(device); g_autofree gchar *devpath = fu_udev_device_get_devpath(FU_UDEV_DEVICE(self)); g_autofree gchar *summary = NULL; g_autofree gchar *firmware_id = NULL; /* FuUdevDevice */ if (!FU_DEVICE_CLASS(fu_serio_device_parent_class)->probe(device, error)) return FALSE; /* firmware ID */ firmware_id = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(self), "firmware_id", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (firmware_id != NULL && firmware_id[0] != '\0') { g_autofree gchar *firmware_id_strup = g_ascii_strup(firmware_id, -1); if (g_str_has_prefix(firmware_id, "PNP: ")) fu_device_add_instance_strsafe(device, "FWID", firmware_id_strup + 5); else fu_device_add_instance_strsafe(device, "FWID", firmware_id_strup); if (!fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_VISIBLE | FU_DEVICE_INSTANCE_FLAG_QUIRKS, error, "SERIO", "FWID", NULL)) return FALSE; } /* try to get one line summary */ summary = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(self), "description", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (summary != NULL) fu_device_set_summary(device, summary); /* fall back to the first thing handled by misc drivers */ if (fu_udev_device_get_device_file(FU_UDEV_DEVICE(self)) == NULL) { g_autofree gchar *device_file = fu_udev_device_get_device_file_from_subsystem(FU_UDEV_DEVICE(self), "misc", NULL); if (device_file != NULL) fu_udev_device_set_device_file(FU_UDEV_DEVICE(self), device_file); } /* we don't have anything better to use */ if (devpath != NULL) { g_autofree gchar *physical_id = g_strdup_printf("DEVPATH=%s", devpath); fu_device_set_physical_id(device, physical_id); } /* success */ return TRUE; } static void fu_serio_device_init(FuSerioDevice *self) { } static void fu_serio_device_class_init(FuSerioDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_serio_device_probe; } fwupd-2.0.10/libfwupdplugin/fu-serio-device.h000066400000000000000000000005601501337203100211050ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-udev-device.h" #define FU_TYPE_SERIO_DEVICE (fu_serio_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuSerioDevice, fu_serio_device, FU, SERIO_DEVICE, FuUdevDevice) struct _FuSerioDeviceClass { FuUdevDeviceClass parent_class; }; fwupd-2.0.10/libfwupdplugin/fu-smbios-private.h000066400000000000000000000010431501337203100214700ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-smbios.h" gboolean fu_smbios_setup(FuSmbios *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_smbios_setup_from_path(FuSmbios *self, const gchar *path, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_smbios_setup_from_file(FuSmbios *self, const gchar *filename, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-smbios.c000066400000000000000000000430301501337203100200150ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuSmbios" #include "config.h" #include #include #ifdef _WIN32 #include #include #endif #include "fwupd-error.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-input-stream.h" #include "fu-path.h" #include "fu-smbios-private.h" #include "fu-smbios-struct.h" #include "fu-string.h" /** * FuSmbios: * * Enumerate the SMBIOS data on the system. * * See also: [class@FuHwids] */ struct _FuSmbios { FuFirmware parent_instance; guint32 structure_table_len; GPtrArray *items; }; typedef struct { guint8 type; guint16 handle; GByteArray *buf; GPtrArray *strings; } FuSmbiosItem; G_DEFINE_TYPE(FuSmbios, fu_smbios, FU_TYPE_FIRMWARE) static FuSmbiosItem * fu_smbios_get_item_for_type_length(FuSmbios *self, guint8 type, guint8 length) { for (guint i = 0; i < self->items->len; i++) { FuSmbiosItem *item = g_ptr_array_index(self->items, i); if (item->type != type) continue; if (length != FU_SMBIOS_STRUCTURE_LENGTH_ANY && length != item->buf->len) { g_debug("filtering SMBIOS structure by length: 0x%x != 0x%x", length, item->buf->len); continue; } return item; } return NULL; } static gboolean fu_smbios_setup_from_data(FuSmbios *self, const guint8 *buf, gsize bufsz, GError **error) { /* go through each structure */ for (gsize i = 0; i < bufsz; i++) { FuSmbiosItem *item; guint8 length; g_autoptr(FuStructSmbiosStructure) st_str = NULL; /* sanity check */ st_str = fu_struct_smbios_structure_parse(buf, bufsz, i, error); if (st_str == NULL) return FALSE; length = fu_struct_smbios_structure_get_length(st_str); if (length < st_str->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "structure smaller than allowed @0x%x", (guint)i); return FALSE; } if (i + length >= bufsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "structure larger than available data @0x%x", (guint)i); return FALSE; } /* create a new result */ item = g_new0(FuSmbiosItem, 1); item->type = fu_struct_smbios_structure_get_type(st_str); item->handle = fu_struct_smbios_structure_get_handle(st_str); item->buf = g_byte_array_sized_new(length); item->strings = g_ptr_array_new_with_free_func(g_free); g_byte_array_append(item->buf, buf + i, length); g_ptr_array_add(self->items, item); /* jump to the end of the formatted area of the struct */ i += length; /* add strings from table */ while (i < bufsz) { GString *str; /* end of string section */ if (item->strings->len > 0 && buf[i] == 0x0) break; /* copy into string table */ str = fu_strdup((const gchar *)buf, bufsz, i); i += str->len + 1; g_ptr_array_add(item->strings, g_string_free(str, FALSE)); } } /* this has to exist */ if (fu_smbios_get_item_for_type_length(self, FU_SMBIOS_STRUCTURE_TYPE_SYSTEM, FU_SMBIOS_STRUCTURE_LENGTH_ANY) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no structure with required type SYSTEM"); return FALSE; } /* success */ return TRUE; } /** * fu_smbios_setup_from_file: * @self: a #FuSmbios * @filename: a filename * @error: (nullable): optional return location for an error * * Reads all the SMBIOS values from a DMI blob. * * Returns: %TRUE for success * * Since: 1.0.0 **/ gboolean fu_smbios_setup_from_file(FuSmbios *self, const gchar *filename, GError **error) { gsize sz = 0; g_autofree gchar *buf = NULL; g_return_val_if_fail(FU_IS_SMBIOS(self), FALSE); g_return_val_if_fail(filename != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* DMI blob */ if (!g_file_get_contents(filename, &buf, &sz, error)) return FALSE; return fu_smbios_setup_from_data(self, (guint8 *)buf, sz, error); } static gboolean fu_smbios_parse_ep32(FuSmbios *self, const guint8 *buf, gsize bufsz, GError **error) { guint8 csum = 0; g_autofree gchar *version_str = NULL; g_autofree gchar *intermediate_anchor_str = NULL; g_autoptr(GByteArray) st_ep32 = NULL; /* verify checksum */ st_ep32 = fu_struct_smbios_ep32_parse(buf, bufsz, 0x0, error); if (st_ep32 == NULL) return FALSE; for (guint i = 0; i < bufsz; i++) csum += buf[i]; if (csum != 0x00) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "entry point checksum invalid"); return FALSE; } /* verify intermediate section */ intermediate_anchor_str = fu_struct_smbios_ep32_get_intermediate_anchor_str(st_ep32); if (g_strcmp0(intermediate_anchor_str, "_DMI_") != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "intermediate anchor signature invalid, got %s", intermediate_anchor_str); return FALSE; } for (guint i = 10; i < bufsz; i++) csum += buf[i]; if (csum != 0x00) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "intermediate checksum invalid"); return FALSE; } self->structure_table_len = fu_struct_smbios_ep32_get_structure_table_len(st_ep32); version_str = g_strdup_printf("%u.%u", fu_struct_smbios_ep32_get_smbios_major_ver(st_ep32), fu_struct_smbios_ep32_get_smbios_minor_ver(st_ep32)); fu_firmware_set_version(FU_FIRMWARE(self), version_str); /* nocheck:set-version */ fu_firmware_set_version_raw( FU_FIRMWARE(self), (((guint16)fu_struct_smbios_ep32_get_smbios_major_ver(st_ep32)) << 8) + fu_struct_smbios_ep32_get_smbios_minor_ver(st_ep32)); return TRUE; } static gboolean fu_smbios_parse_ep64(FuSmbios *self, const guint8 *buf, gsize bufsz, GError **error) { guint8 csum = 0; g_autofree gchar *version_str = NULL; g_autoptr(GByteArray) st_ep64 = NULL; /* verify checksum */ st_ep64 = fu_struct_smbios_ep64_parse(buf, bufsz, 0x0, error); if (st_ep64 == NULL) return FALSE; for (guint i = 0; i < bufsz; i++) csum += buf[i]; if (csum != 0x00) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "entry point checksum invalid"); return FALSE; } self->structure_table_len = fu_struct_smbios_ep64_get_structure_table_len(st_ep64); version_str = g_strdup_printf("%u.%u", fu_struct_smbios_ep64_get_smbios_major_ver(st_ep64), fu_struct_smbios_ep64_get_smbios_minor_ver(st_ep64)); fu_firmware_set_version(FU_FIRMWARE(self), version_str); /* nocheck:set-version */ return TRUE; } /** * fu_smbios_setup_from_path: * @self: a #FuSmbios * @path: a path, e.g. `/sys/firmware/dmi/tables` * @error: (nullable): optional return location for an error * * Reads all the SMBIOS values from a specific path. * * Returns: %TRUE for success * * Since: 1.0.0 **/ gboolean fu_smbios_setup_from_path(FuSmbios *self, const gchar *path, GError **error) { gsize sz = 0; g_autofree gchar *dmi_fn = NULL; g_autofree gchar *dmi_raw = NULL; g_autofree gchar *ep_fn = NULL; g_autofree gchar *ep_raw = NULL; g_return_val_if_fail(FU_IS_SMBIOS(self), FALSE); g_return_val_if_fail(path != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* get the smbios entry point */ ep_fn = g_build_filename(path, "smbios_entry_point", NULL); if (!g_file_get_contents(ep_fn, &ep_raw, &sz, error)) { fu_error_convert(error); return FALSE; } /* check we got enough data to read the signature */ if (sz < 5) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid smbios entry point got 0x%x bytes, expected 0x%x or 0x%x", (guint)sz, (guint)FU_STRUCT_SMBIOS_EP32_SIZE, (guint)FU_STRUCT_SMBIOS_EP64_SIZE); return FALSE; } /* parse 32 bit structure */ if (memcmp(ep_raw, "_SM_", 4) == 0) { if (!fu_smbios_parse_ep32(self, (const guint8 *)ep_raw, sz, error)) return FALSE; } else if (memcmp(ep_raw, "_SM3_", 5) == 0) { if (!fu_smbios_parse_ep64(self, (const guint8 *)ep_raw, sz, error)) return FALSE; } else { g_autofree gchar *tmp = g_strndup(ep_raw, 4); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "SMBIOS signature invalid, got %s", tmp); return FALSE; } /* get the DMI data */ dmi_fn = g_build_filename(path, "DMI", NULL); if (!g_file_get_contents(dmi_fn, &dmi_raw, &sz, error)) { fu_error_convert(error); return FALSE; } if (sz > self->structure_table_len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid DMI data size, got %" G_GSIZE_FORMAT " bytes, expected %" G_GUINT32_FORMAT, sz, self->structure_table_len); return FALSE; } /* parse blob */ return fu_smbios_setup_from_data(self, (guint8 *)dmi_raw, sz, error); } static gboolean fu_smbios_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuSmbios *self = FU_SMBIOS(firmware); g_autoptr(GBytes) fw = NULL; fw = fu_input_stream_read_bytes(stream, 0x0, G_MAXSIZE, NULL, error); if (fw == NULL) return FALSE; return fu_smbios_setup_from_data(self, g_bytes_get_data(fw, NULL), g_bytes_get_size(fw), error); } #ifdef _WIN32 #define FU_SMBIOS_FT_SIG_ACPI 0x41435049 #define FU_SMBIOS_FT_SIG_FIRM 0x4649524D #define FU_SMBIOS_FT_SIG_RSMB 0x52534D42 #define FU_SMBIOS_FT_RAW_OFFSET 0x08 #endif /** * fu_smbios_setup: * @self: a #FuSmbios * @error: (nullable): optional return location for an error * * Reads all the SMBIOS values from the hardware. * * Returns: %TRUE for success * * Since: 1.0.0 **/ gboolean fu_smbios_setup(FuSmbios *self, GError **error) { #ifdef _WIN32 gsize bufsz; guint rc; g_autofree guint8 *buf = NULL; rc = GetSystemFirmwareTable(FU_SMBIOS_FT_SIG_RSMB, 0, 0, 0); if (rc <= 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to access RSMB [%u]", (guint)GetLastError()); return FALSE; } if (rc < FU_SMBIOS_FT_RAW_OFFSET || rc > 0x1000000) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "RSMB impossible size"); return FALSE; } bufsz = rc; buf = g_malloc0(bufsz); rc = GetSystemFirmwareTable(FU_SMBIOS_FT_SIG_RSMB, 0, buf, (DWORD)bufsz); if (rc <= 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to read RSMB [%u]", (guint)GetLastError()); return FALSE; } return fu_smbios_setup_from_data(self, buf + FU_SMBIOS_FT_RAW_OFFSET, bufsz - FU_SMBIOS_FT_RAW_OFFSET, error); #else g_autofree gchar *path = NULL; g_autofree gchar *sysfsfwdir = NULL; g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_SMBIOS(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* DMI */ sysfsfwdir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR_FW); path = g_build_filename(sysfsfwdir, "dmi", "tables", NULL); if (!g_file_test(path, G_FILE_TEST_EXISTS)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "SMBIOS tables not found at %s", path); return FALSE; } if (!fu_smbios_setup_from_path(self, path, &error_local)) { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE)) { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } g_debug("ignoring %s", error_local->message); } /* success */ return TRUE; #endif } static void fu_smbios_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuSmbios *self = FU_SMBIOS(firmware); for (guint i = 0; i < self->items->len; i++) { FuSmbiosItem *item = g_ptr_array_index(self->items, i); g_autoptr(XbBuilderNode) bc = xb_builder_node_insert(bn, "item", NULL); g_autofree gchar *buf = fu_byte_array_to_string(item->buf); fu_xmlb_builder_insert_kx(bc, "type", item->type); fu_xmlb_builder_insert_kx(bc, "length", item->buf->len); fu_xmlb_builder_insert_kx(bc, "handle", item->handle); fu_xmlb_builder_insert_kv(bc, "buf", buf); for (guint j = 0; j < item->strings->len; j++) { const gchar *tmp = g_ptr_array_index(item->strings, j); g_autofree gchar *title = g_strdup_printf("%02u", j); g_autofree gchar *value = fu_strsafe(tmp, 40); xb_builder_node_insert_text(bc, "string", value, "idx", title, NULL); } } } /** * fu_smbios_get_data: * @self: a #FuSmbios * @type: a structure type, e.g. %FU_SMBIOS_STRUCTURE_TYPE_BIOS * @length: expected length of the structure, or %FU_SMBIOS_STRUCTURE_LENGTH_ANY * @error: (nullable): optional return location for an error * * Reads all the SMBIOS data blobs of a specified type. * * Returns: (transfer container) (element-type GBytes): a #GBytes, or %NULL if invalid or not found * * Since: 2.0.7 **/ GPtrArray * fu_smbios_get_data(FuSmbios *self, guint8 type, guint8 length, GError **error) { g_autoptr(GPtrArray) array = g_ptr_array_new_with_free_func((GDestroyNotify)g_bytes_unref); g_return_val_if_fail(FU_IS_SMBIOS(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); for (guint i = 0; i < self->items->len; i++) { FuSmbiosItem *item = g_ptr_array_index(self->items, i); if (item->type != type) continue; if (length != FU_SMBIOS_STRUCTURE_LENGTH_ANY && length != item->buf->len) continue; if (item->buf->len == 0) continue; g_ptr_array_add(array, g_bytes_new(item->buf->data, item->buf->len)); } if (array->len == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no structures with type %02x", type); return NULL; } return g_steal_pointer(&array); } /** * fu_smbios_get_integer: * @self: a #FuSmbios * @type: a structure type, e.g. %FU_SMBIOS_STRUCTURE_TYPE_BIOS * @length: expected length of the structure, or %FU_SMBIOS_STRUCTURE_LENGTH_ANY * @offset: a structure offset * @error: (nullable): optional return location for an error * * Reads an integer value from the SMBIOS string table of a specific structure. * * The @type and @offset can be referenced from the DMTF SMBIOS specification: * https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.1.1.pdf * * Returns: an integer, or %G_MAXUINT if invalid or not found * * Since: 2.0.7 **/ guint fu_smbios_get_integer(FuSmbios *self, guint8 type, guint8 length, guint8 offset, GError **error) { FuSmbiosItem *item; g_return_val_if_fail(FU_IS_SMBIOS(self), 0); g_return_val_if_fail(error == NULL || *error == NULL, 0); /* get item */ item = fu_smbios_get_item_for_type_length(self, type, length); if (item == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no structure with type %02x", type); return G_MAXUINT; } /* check offset valid */ if (offset >= item->buf->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "offset bigger than size %u", item->buf->len); return G_MAXUINT; } /* success */ return item->buf->data[offset]; } /** * fu_smbios_get_string: * @self: a #FuSmbios * @type: a structure type, e.g. %FU_SMBIOS_STRUCTURE_TYPE_BIOS * @length: expected length of the structure, or %FU_SMBIOS_STRUCTURE_LENGTH_ANY * @offset: a structure offset * @error: (nullable): optional return location for an error * * Reads a string from the SMBIOS string table of a specific structure. * * The @type and @offset can be referenced from the DMTF SMBIOS specification: * https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.1.1.pdf * * Returns: a string, or %NULL if invalid or not found * * Since: 2.0.7 **/ const gchar * fu_smbios_get_string(FuSmbios *self, guint8 type, guint8 length, guint8 offset, GError **error) { FuSmbiosItem *item; g_return_val_if_fail(FU_IS_SMBIOS(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* get item */ item = fu_smbios_get_item_for_type_length(self, type, length); if (item == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no structure with type %02x", type); return NULL; } /* check offset valid */ if (offset >= item->buf->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "offset bigger than size %u", item->buf->len); return NULL; } if (item->buf->data[offset] == 0x00) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no data available"); return NULL; } /* check string index valid */ if (item->buf->data[offset] > item->strings->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "index larger than string table %u", item->strings->len); return NULL; } return g_ptr_array_index(item->strings, item->buf->data[offset] - 1); } static void fu_smbios_item_free(FuSmbiosItem *item) { g_byte_array_unref(item->buf); g_ptr_array_unref(item->strings); g_free(item); } static void fu_smbios_finalize(GObject *object) { FuSmbios *self = FU_SMBIOS(object); g_ptr_array_unref(self->items); G_OBJECT_CLASS(fu_smbios_parent_class)->finalize(object); } static void fu_smbios_class_init(FuSmbiosClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_smbios_finalize; firmware_class->parse = fu_smbios_parse; firmware_class->export = fu_smbios_export; } static void fu_smbios_init(FuSmbios *self) { self->items = g_ptr_array_new_with_free_func((GDestroyNotify)fu_smbios_item_free); } /** * fu_smbios_new: * * Creates a new object to parse SMBIOS data. * * Returns: a #FuSmbios * * Since: 1.0.0 **/ FuSmbios * fu_smbios_new(void) { FuSmbios *self; self = g_object_new(FU_TYPE_SMBIOS, NULL); return FU_SMBIOS(self); } fwupd-2.0.10/libfwupdplugin/fu-smbios.h000066400000000000000000000014641501337203100200270ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_SMBIOS (fu_smbios_get_type()) G_DECLARE_FINAL_TYPE(FuSmbios, fu_smbios, FU, SMBIOS, FuFirmware) /** * FU_SMBIOS_STRUCTURE_LENGTH_ANY: * * Accept any structure length. * * Since: 2.0.7 **/ #define FU_SMBIOS_STRUCTURE_LENGTH_ANY G_MAXUINT8 FuSmbios * fu_smbios_new(void); const gchar * fu_smbios_get_string(FuSmbios *self, guint8 type, guint8 length, guint8 offset, GError **error) G_GNUC_NON_NULL(1); guint fu_smbios_get_integer(FuSmbios *self, guint8 type, guint8 length, guint8 offset, GError **error) G_GNUC_NON_NULL(1); GPtrArray * fu_smbios_get_data(FuSmbios *self, guint8 type, guint8 length, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-smbios.rs000066400000000000000000000033501501337203100202200ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later enum FuSmbiosStructureType { Bios, System, Baseboard, // aka motherboard Chassis, } #[derive(ToString)] enum FuSmbiosChassisKind { Unset, // inferred Other, Unknown, Desktop, LowProfileDesktop, PizzaBox, MiniTower, Tower, Portable, Laptop, Notebook, HandHeld, DockingStation, AllInOne, SubNotebook, SpaceSaving, LunchBox, MainServer, Expansion, Subchassis, BusExpansion, Peripheral, Raid, RackMount, SealedCasePc, MultiSystem, CompactPci, AdvancedTca, Blade, Reserved, // 0x1D is missing! Tablet, Convertible, Detachable, IotGateway, EmbeddedPc, MiniPc, StickPc, } #[derive(New, Parse)] #[repr(C, packed)] struct FuStructSmbiosEp32 { anchor_str: [char; 4], entry_point_csum: u8, entry_point_len: u8, smbios_major_ver: u8, smbios_minor_ver: u8, max_structure_sz: u16le, entry_point_rev: u8, _formatted_area: [u8; 5], intermediate_anchor_str: [char; 5], intermediate_csum: u8, structure_table_len: u16le, structure_table_addr: u32le, number_smbios_structs: u16le, smbios_bcd_rev: u8, } #[derive(New, Parse)] #[repr(C, packed)] struct FuStructSmbiosEp64 { anchor_str: [char; 5], entry_point_csum: u8, entry_point_len: u8, smbios_major_ver: u8, smbios_minor_ver: u8, smbios_docrev: u8, entry_point_rev: u8, reserved0: u8, structure_table_len: u32le, structure_table_addr: u64le, } #[derive(New, Parse)] #[repr(C, packed)] struct FuStructSmbiosStructure { type: u8, length: u8, handle: u16le, } fwupd-2.0.10/libfwupdplugin/fu-srec-firmware.c000066400000000000000000000444131501337203100212750ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include #include "fu-byte-array.h" #include "fu-chunk-array.h" #include "fu-common.h" #include "fu-firmware-common.h" #include "fu-srec-firmware.h" #include "fu-string.h" /** * FuSrecFirmware: * * A SREC firmware image. * * See also: [class@FuFirmware] */ typedef struct { GPtrArray *records; guint32 addr_min; guint32 addr_max; } FuSrecFirmwarePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuSrecFirmware, fu_srec_firmware, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_srec_firmware_get_instance_private(o)) #define FU_SREC_FIRMWARE_TOKENS_MAX 100000 /* lines */ /** * fu_srec_firmware_get_records: * @self: A #FuSrecFirmware * * Returns the raw records from SREC tokenization. * * This might be useful if the plugin is expecting the SREC file to be a list * of operations, rather than a simple linear image with filled holes. * * Returns: (transfer none) (element-type FuSrecFirmwareRecord): records * * Since: 1.3.2 **/ GPtrArray * fu_srec_firmware_get_records(FuSrecFirmware *self) { FuSrecFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_SREC_FIRMWARE(self), NULL); return priv->records; } /** * fu_srec_firmware_set_addr_min: * @self: A #FuSrecFirmware * @addr_min: address, or 0x0 to disable * * Sets the minimum address allowed. This may be useful to ignore a bootloader section. * * Since: 1.9.3 **/ void fu_srec_firmware_set_addr_min(FuSrecFirmware *self, guint32 addr_min) { FuSrecFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_SREC_FIRMWARE(self)); priv->addr_min = addr_min; } /** * fu_srec_firmware_set_addr_max: * @self: A #FuSrecFirmware * @addr_max: address, or 0x0 to disable * * Sets the maximum address allowed. This may be useful to ignore a signature. * * Since: 1.9.3 **/ void fu_srec_firmware_set_addr_max(FuSrecFirmware *self, guint32 addr_max) { FuSrecFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_SREC_FIRMWARE(self)); priv->addr_max = addr_max; } static void fu_srec_firmware_record_free(FuSrecFirmwareRecord *rcd) { g_byte_array_unref(rcd->buf); g_free(rcd); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuSrecFirmwareRecord, fu_srec_firmware_record_free); #pragma clang diagnostic pop /** * fu_srec_firmware_record_new: (skip): * @ln: unsigned integer * @kind: a record kind, e.g. #FU_FIRMWARE_SREC_RECORD_KIND_S3_DATA_32 * @addr: unsigned integer * * Returns a single firmware record * * Returns: (transfer full): record * * Since: 1.3.2 **/ FuSrecFirmwareRecord * fu_srec_firmware_record_new(guint ln, FuFirmareSrecRecordKind kind, guint32 addr) { FuSrecFirmwareRecord *rcd = g_new0(FuSrecFirmwareRecord, 1); rcd->ln = ln; rcd->kind = kind; rcd->addr = addr; rcd->buf = g_byte_array_new(); return rcd; } static FuSrecFirmwareRecord * fu_srec_firmware_record_dup(const FuSrecFirmwareRecord *rcd) { FuSrecFirmwareRecord *dest; g_return_val_if_fail(rcd != NULL, NULL); dest = fu_srec_firmware_record_new(rcd->ln, rcd->kind, rcd->addr); dest->buf = g_byte_array_ref(rcd->buf); return dest; } /** * fu_srec_firmware_record_get_type: * * Gets a specific type. * * Return value: a #GType * * Since: 1.6.1 **/ GType fu_srec_firmware_record_get_type(void) { static GType type_id = 0; if (!type_id) { type_id = g_boxed_type_register_static("FuSrecFirmwareRecord", (GBoxedCopyFunc)fu_srec_firmware_record_dup, (GBoxedFreeFunc)fu_srec_firmware_record_free); } return type_id; } typedef struct { FuSrecFirmware *self; FuFirmwareParseFlags flags; gboolean got_eof; } FuSrecFirmwareTokenHelper; static gboolean fu_srec_firmware_tokenize_cb(GString *token, guint token_idx, gpointer user_data, GError **error) { FuSrecFirmwareTokenHelper *helper = (FuSrecFirmwareTokenHelper *)user_data; FuSrecFirmwarePrivate *priv = GET_PRIVATE(helper->self); g_autoptr(FuSrecFirmwareRecord) rcd = NULL; gboolean require_data = FALSE; guint32 rec_addr32; guint16 rec_addr16; guint8 addrsz = 0; /* bytes */ guint8 rec_count; /* words */ guint8 rec_kind; /* sanity check */ if (token_idx > FU_SREC_FIRMWARE_TOKENS_MAX) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "file has too many lines"); return FALSE; } /* remove WIN32 line endings */ g_strdelimit(token->str, "\r\x1a", '\0'); token->len = strlen(token->str); /* ignore blank lines */ if (token->len == 0) return TRUE; /* check starting token */ if (token->str[0] != 'S' || token->len < 3) { g_autofree gchar *strsafe = fu_strsafe(token->str, 3); if (strsafe != NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid starting token, got '%s' at line %u", strsafe, token_idx + 1); return FALSE; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid starting token at line %u", token_idx + 1); return FALSE; } /* kind, count, address, (data), checksum, linefeed */ rec_kind = token->str[1] - '0'; if (!fu_firmware_strparse_uint8_safe(token->str, token->len, 2, &rec_count, error)) return FALSE; if (rec_count * 2 != token->len - 4) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "count incomplete at line %u, " "length %u, expected %u", token_idx + 1, (guint)token->len - 4, (guint)rec_count * 2); return FALSE; } /* checksum check */ if ((helper->flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { guint8 rec_csum = 0; guint8 rec_csum_expected; for (guint8 i = 0; i < rec_count; i++) { guint8 csum_tmp = 0; if (!fu_firmware_strparse_uint8_safe(token->str, token->len, (i * 2) + 2, &csum_tmp, error)) return FALSE; rec_csum += csum_tmp; } rec_csum ^= 0xff; if (!fu_firmware_strparse_uint8_safe(token->str, token->len, (rec_count * 2) + 2, &rec_csum_expected, error)) return FALSE; if (rec_csum != rec_csum_expected) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "checksum incorrect line %u, " "expected %02x, got %02x", token_idx + 1, rec_csum_expected, rec_csum); return FALSE; } } /* set each command settings */ switch (rec_kind) { case FU_FIRMWARE_SREC_RECORD_KIND_S0_HEADER: case FU_FIRMWARE_SREC_RECORD_KIND_S1_DATA_16: addrsz = 2; require_data = TRUE; break; case FU_FIRMWARE_SREC_RECORD_KIND_S2_DATA_24: addrsz = 3; require_data = TRUE; break; case FU_FIRMWARE_SREC_RECORD_KIND_S3_DATA_32: addrsz = 4; require_data = TRUE; break; case FU_FIRMWARE_SREC_RECORD_KIND_S5_COUNT_16: addrsz = 2; helper->got_eof = TRUE; break; case FU_FIRMWARE_SREC_RECORD_KIND_S6_COUNT_24: addrsz = 3; break; case FU_FIRMWARE_SREC_RECORD_KIND_S7_COUNT_32: addrsz = 4; helper->got_eof = TRUE; break; case FU_FIRMWARE_SREC_RECORD_KIND_S8_TERMINATION_24: addrsz = 3; helper->got_eof = TRUE; break; case FU_FIRMWARE_SREC_RECORD_KIND_S9_TERMINATION_16: addrsz = 2; helper->got_eof = TRUE; break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid srec record type S%c at line %u", token->str[1], token_idx + 1); return FALSE; } /* parse address */ switch (addrsz) { case 2: if (!fu_firmware_strparse_uint16_safe(token->str, token->len, 4, &rec_addr16, error)) return FALSE; rec_addr32 = rec_addr16; break; case 3: if (!fu_firmware_strparse_uint24_safe(token->str, token->len, 4, &rec_addr32, error)) return FALSE; break; case 4: if (!fu_firmware_strparse_uint32_safe(token->str, token->len, 4, &rec_addr32, error)) return FALSE; break; default: g_assert_not_reached(); } g_debug("line %03u S%u addr:0x%04x datalen:0x%02x", token_idx + 1, rec_kind, rec_addr32, (guint)rec_count - addrsz - 1); if (require_data && rec_count == addrsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "S%u required data but not provided", rec_kind); return FALSE; } /* data */ rcd = fu_srec_firmware_record_new(token_idx + 1, rec_kind, rec_addr32); if (rec_kind == 1 || rec_kind == 2 || rec_kind == 3) { for (gsize i = 4 + (addrsz * 2); i <= rec_count * 2; i += 2) { guint8 tmp = 0; if (!fu_firmware_strparse_uint8_safe(token->str, token->len, i, &tmp, error)) return FALSE; fu_byte_array_append_uint8(rcd->buf, tmp); } } g_ptr_array_add(priv->records, g_steal_pointer(&rcd)); return TRUE; } static gboolean fu_srec_firmware_tokenize(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuSrecFirmware *self = FU_SREC_FIRMWARE(firmware); FuSrecFirmwareTokenHelper helper = {.self = self, .flags = flags, .got_eof = FALSE}; /* parse records */ if (!fu_strsplit_stream(stream, 0x0, "\n", fu_srec_firmware_tokenize_cb, &helper, error)) return FALSE; /* no EOF */ if (!helper.got_eof) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no EOF, perhaps truncated file"); return FALSE; } return TRUE; } static gboolean fu_srec_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuSrecFirmware *self = FU_SREC_FIRMWARE(firmware); FuSrecFirmwarePrivate *priv = GET_PRIVATE(self); gboolean got_hdr = FALSE; guint16 data_cnt = 0; guint32 addr32_last = 0; guint32 img_address = 0; g_autoptr(GBytes) img_bytes = NULL; g_autoptr(GByteArray) outbuf = g_byte_array_new(); /* parse records */ for (guint j = 0; j < priv->records->len; j++) { FuSrecFirmwareRecord *rcd = g_ptr_array_index(priv->records, j); /* header */ if (rcd->kind == FU_FIRMWARE_SREC_RECORD_KIND_S0_HEADER) { g_autoptr(GString) modname = g_string_new(NULL); /* check for duplicate */ if (got_hdr) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "duplicate header record at line %u", rcd->ln); return FALSE; } /* could be anything, lets assume text */ for (guint i = 0; i < rcd->buf->len; i++) { gchar tmp = rcd->buf->data[i]; if (!g_ascii_isgraph(tmp)) break; g_string_append_c(modname, tmp); } if (modname->len != 0) fu_firmware_set_id(firmware, modname->str); got_hdr = TRUE; continue; } /* verify we got all records */ if (rcd->kind == FU_FIRMWARE_SREC_RECORD_KIND_S5_COUNT_16) { if (rcd->addr != data_cnt) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "count record was not valid, got 0x%02x expected " "0x%02x at line %u", (guint)rcd->addr, (guint)data_cnt, rcd->ln); return FALSE; } continue; } /* data */ if (rcd->kind == FU_FIRMWARE_SREC_RECORD_KIND_S1_DATA_16 || rcd->kind == FU_FIRMWARE_SREC_RECORD_KIND_S2_DATA_24 || rcd->kind == FU_FIRMWARE_SREC_RECORD_KIND_S3_DATA_32) { /* invalid */ if (!got_hdr) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "missing header record at line %u", rcd->ln); return FALSE; } /* does not make sense */ if (rcd->addr < addr32_last) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid address 0x%x, last was 0x%x at line %u", (guint)rcd->addr, (guint)addr32_last, rcd->ln); return FALSE; } if (rcd->addr < priv->addr_min) { g_debug( "ignoring data at 0x%x as before start address 0x%x at line %u", (guint)rcd->addr, priv->addr_min, rcd->ln); } else if (priv->addr_max > 0 && rcd->addr < priv->addr_max) { g_debug( "ignoring data at 0x%x as after end address 0x%x at line %u", (guint)rcd->addr, priv->addr_max, rcd->ln); } else { guint32 len_hole = rcd->addr - addr32_last; /* fill any holes, but only up to 1Mb to avoid a DoS */ if (addr32_last > 0 && len_hole > 0x100000) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "hole of 0x%x bytes too large to fill at line %u", (guint)len_hole, rcd->ln); return FALSE; } if (addr32_last > 0x0 && len_hole > 1) { g_debug("filling address 0x%08x to 0x%08x at line %u", addr32_last + 1, addr32_last + len_hole - 1, rcd->ln); for (guint i = 0; i < len_hole; i++) fu_byte_array_append_uint8(outbuf, 0xff); } /* add data */ g_byte_array_append(outbuf, rcd->buf->data, rcd->buf->len); if (img_address == 0x0) img_address = rcd->addr; addr32_last = rcd->addr + rcd->buf->len; if (addr32_last < rcd->addr) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "overflow from address 0x%x at line %u", (guint)rcd->addr, rcd->ln); return FALSE; } } data_cnt++; } } /* add single image */ img_bytes = g_bytes_new(outbuf->data, outbuf->len); fu_firmware_set_bytes(firmware, img_bytes); fu_firmware_set_addr(firmware, img_address); return TRUE; } static void fu_srec_firmware_write_line(GString *str, FuFirmareSrecRecordKind kind, guint32 addr, const guint8 *buf, gsize bufsz) { guint8 csum = 0; g_autoptr(GByteArray) buf_addr = g_byte_array_new(); if (kind == FU_FIRMWARE_SREC_RECORD_KIND_S0_HEADER || kind == FU_FIRMWARE_SREC_RECORD_KIND_S1_DATA_16 || kind == FU_FIRMWARE_SREC_RECORD_KIND_S5_COUNT_16 || kind == FU_FIRMWARE_SREC_RECORD_KIND_S9_TERMINATION_16) { fu_byte_array_append_uint16(buf_addr, addr, G_BIG_ENDIAN); } else if (kind == FU_FIRMWARE_SREC_RECORD_KIND_S2_DATA_24 || kind == FU_FIRMWARE_SREC_RECORD_KIND_S6_COUNT_24 || kind == FU_FIRMWARE_SREC_RECORD_KIND_S8_TERMINATION_24) { fu_byte_array_append_uint32(buf_addr, addr, G_BIG_ENDIAN); g_byte_array_remove_index(buf_addr, 0); } else if (kind == FU_FIRMWARE_SREC_RECORD_KIND_S3_DATA_32 || kind == FU_FIRMWARE_SREC_RECORD_KIND_S7_COUNT_32) { fu_byte_array_append_uint32(buf_addr, addr, G_BIG_ENDIAN); } /* bytecount + address + data */ csum = buf_addr->len + bufsz + 1; for (guint i = 0; i < buf_addr->len; i++) csum += buf_addr->data[i]; for (guint i = 0; i < bufsz; i++) csum += buf[i]; csum ^= 0xff; /* output record */ g_string_append_printf(str, "S%X", kind); g_string_append_printf(str, "%02X", (guint)(buf_addr->len + bufsz + 1)); for (guint i = 0; i < buf_addr->len; i++) g_string_append_printf(str, "%02X", buf_addr->data[i]); for (guint i = 0; i < bufsz; i++) g_string_append_printf(str, "%02X", buf[i]); g_string_append_printf(str, "%02X\n", csum); } static GByteArray * fu_srec_firmware_write(FuFirmware *firmware, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GString) str = g_string_new(NULL); g_autoptr(GBytes) buf_blob = NULL; const gchar *id = fu_firmware_get_id(firmware); gsize id_strlen = id != NULL ? strlen(id) : 0; FuFirmareSrecRecordKind kind_data = FU_FIRMWARE_SREC_RECORD_KIND_S1_DATA_16; FuFirmareSrecRecordKind kind_coun = FU_FIRMWARE_SREC_RECORD_KIND_S5_COUNT_16; FuFirmareSrecRecordKind kind_term = FU_FIRMWARE_SREC_RECORD_KIND_S9_TERMINATION_16; /* upgrade to longer addresses? */ if (fu_firmware_get_addr(firmware) >= (1ull << 24)) { kind_data = FU_FIRMWARE_SREC_RECORD_KIND_S3_DATA_32; kind_term = FU_FIRMWARE_SREC_RECORD_KIND_S7_COUNT_32; /* intentional... */ } else if (fu_firmware_get_addr(firmware) >= (1ull << 16)) { kind_data = FU_FIRMWARE_SREC_RECORD_KIND_S2_DATA_24; kind_term = FU_FIRMWARE_SREC_RECORD_KIND_S8_TERMINATION_24; } /* main blob */ buf_blob = fu_firmware_get_bytes_with_patches(firmware, error); if (buf_blob == NULL) return NULL; /* header */ fu_srec_firmware_write_line(str, FU_FIRMWARE_SREC_RECORD_KIND_S0_HEADER, 0x0, (const guint8 *)id, id_strlen); /* payload */ if (g_bytes_get_size(buf_blob) > 0) { g_autoptr(FuChunkArray) chunks = fu_chunk_array_new_from_bytes(buf_blob, fu_firmware_get_addr(firmware), FU_CHUNK_PAGESZ_NONE, 64); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return NULL; fu_srec_firmware_write_line(str, kind_data, fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); } /* upgrade to longer format */ if (fu_chunk_array_length(chunks) > G_MAXUINT16) kind_coun = FU_FIRMWARE_SREC_RECORD_KIND_S6_COUNT_24; fu_srec_firmware_write_line(str, kind_coun, fu_chunk_array_length(chunks), NULL, 0); } /* EOF */ fu_srec_firmware_write_line(str, kind_term, 0x0, NULL, 0); /* success */ g_byte_array_append(buf, (const guint8 *)str->str, str->len); return g_steal_pointer(&buf); } static void fu_srec_firmware_finalize(GObject *object) { FuSrecFirmware *self = FU_SREC_FIRMWARE(object); FuSrecFirmwarePrivate *priv = GET_PRIVATE(self); g_ptr_array_unref(priv->records); G_OBJECT_CLASS(fu_srec_firmware_parent_class)->finalize(object); } static void fu_srec_firmware_init(FuSrecFirmware *self) { FuSrecFirmwarePrivate *priv = GET_PRIVATE(self); priv->records = g_ptr_array_new_with_free_func((GFreeFunc)fu_srec_firmware_record_free); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); } static void fu_srec_firmware_class_init(FuSrecFirmwareClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_srec_firmware_finalize; firmware_class->parse = fu_srec_firmware_parse; firmware_class->tokenize = fu_srec_firmware_tokenize; firmware_class->write = fu_srec_firmware_write; } /** * fu_srec_firmware_new: * * Creates a new #FuFirmware of type SREC * * Since: 1.3.2 **/ FuFirmware * fu_srec_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_SREC_FIRMWARE, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-srec-firmware.h000066400000000000000000000044031501337203100212750ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_SREC_FIRMWARE (fu_srec_firmware_get_type()) #define FU_TYPE_SREC_FIRMWARE_RECORD (fu_srec_firmware_record_get_type()) G_DECLARE_DERIVABLE_TYPE(FuSrecFirmware, fu_srec_firmware, FU, SREC_FIRMWARE, FuFirmware) struct _FuSrecFirmwareClass { FuFirmwareClass parent_class; }; /** * FuFirmareSrecRecordKind: * @FU_FIRMWARE_SREC_RECORD_KIND_S0_HEADER: Header * @FU_FIRMWARE_SREC_RECORD_KIND_S1_DATA_16: 16 bit data * @FU_FIRMWARE_SREC_RECORD_KIND_S2_DATA_24: 24 bit data * @FU_FIRMWARE_SREC_RECORD_KIND_S3_DATA_32: 32 bit data * @FU_FIRMWARE_SREC_RECORD_KIND_S4_RESERVED: Reserved value * @FU_FIRMWARE_SREC_RECORD_KIND_S5_COUNT_16: 16 bit count * @FU_FIRMWARE_SREC_RECORD_KIND_S6_COUNT_24: 24 bit count * @FU_FIRMWARE_SREC_RECORD_KIND_S7_COUNT_32: 32 bit count * @FU_FIRMWARE_SREC_RECORD_KIND_S8_TERMINATION_24: 24 bit termination * @FU_FIRMWARE_SREC_RECORD_KIND_S9_TERMINATION_16: 16 bit termination * * The kind of SREC record kind. **/ typedef enum { FU_FIRMWARE_SREC_RECORD_KIND_S0_HEADER, FU_FIRMWARE_SREC_RECORD_KIND_S1_DATA_16, FU_FIRMWARE_SREC_RECORD_KIND_S2_DATA_24, FU_FIRMWARE_SREC_RECORD_KIND_S3_DATA_32, FU_FIRMWARE_SREC_RECORD_KIND_S4_RESERVED, FU_FIRMWARE_SREC_RECORD_KIND_S5_COUNT_16, FU_FIRMWARE_SREC_RECORD_KIND_S6_COUNT_24, FU_FIRMWARE_SREC_RECORD_KIND_S7_COUNT_32, FU_FIRMWARE_SREC_RECORD_KIND_S8_TERMINATION_24, FU_FIRMWARE_SREC_RECORD_KIND_S9_TERMINATION_16, /*< private >*/ FU_FIRMWARE_SREC_RECORD_KIND_LAST } FuFirmareSrecRecordKind; /** * FuSrecFirmwareRecord: * * A single SREC record. **/ typedef struct { guint ln; FuFirmareSrecRecordKind kind; guint32 addr; GByteArray *buf; } FuSrecFirmwareRecord; FuFirmware * fu_srec_firmware_new(void); void fu_srec_firmware_set_addr_min(FuSrecFirmware *self, guint32 addr_min) G_GNUC_NON_NULL(1); void fu_srec_firmware_set_addr_max(FuSrecFirmware *self, guint32 addr_max) G_GNUC_NON_NULL(1); GPtrArray * fu_srec_firmware_get_records(FuSrecFirmware *self) G_GNUC_NON_NULL(1); GType fu_srec_firmware_record_get_type(void); FuSrecFirmwareRecord * fu_srec_firmware_record_new(guint ln, FuFirmareSrecRecordKind kind, guint32 addr); fwupd-2.0.10/libfwupdplugin/fu-string.c000066400000000000000000000533371501337203100200420ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuCommon" #include "config.h" #include "fu-byte-array.h" #include "fu-chunk-array.h" #include "fu-input-stream.h" #include "fu-mem.h" #include "fu-partial-input-stream.h" #include "fu-string.h" /** * fu_strtoull: * @str: a string, e.g. `0x1234` * @value: (out) (nullable): parsed value * @min: minimum acceptable value, typically 0 * @max: maximum acceptable value, typically G_MAXUINT64 * @base: default log base, usually %FU_INTEGER_BASE_AUTO * @error: (nullable): optional return location for an error * * Converts a string value to an integer. If the @value is prefixed with `0x` then the base is * set to 16 automatically. * * Returns: %TRUE if the value was parsed correctly, or %FALSE for error * * Since: 2.0.0 **/ gboolean fu_strtoull(const gchar *str, guint64 *value, guint64 min, guint64 max, FuIntegerBase base, GError **error) { gchar *endptr = NULL; guint64 value_tmp; /* sanity check */ if (str == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "cannot parse NULL"); return FALSE; } /* detect hex */ if (base == FU_INTEGER_BASE_AUTO) { if (g_str_has_prefix(str, "0x")) { str += 2; base = FU_INTEGER_BASE_16; } else { base = FU_INTEGER_BASE_10; } } else if (base == FU_INTEGER_BASE_16 && g_str_has_prefix(str, "0x")) { str += 2; } else if (base == FU_INTEGER_BASE_10 && g_str_has_prefix(str, "0x")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "cannot parse 0x-prefixed base-10 string"); return FALSE; } /* convert */ value_tmp = g_ascii_strtoull(str, &endptr, base); /* nocheck:blocked */ if ((gsize)(endptr - str) != strlen(str) && *endptr != '\n') { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "cannot parse datastream"); return FALSE; } /* overflow check */ if (value_tmp == G_MAXUINT64) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "parsing datastream caused overflow"); return FALSE; } /* range check */ if (value_tmp < min) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "value %" G_GUINT64_FORMAT " was below minimum %" G_GUINT64_FORMAT, value_tmp, min); return FALSE; } if (value_tmp > max) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "value %" G_GUINT64_FORMAT " was above maximum %" G_GUINT64_FORMAT, value_tmp, max); return FALSE; } /* success */ if (value != NULL) *value = value_tmp; return TRUE; } /** * fu_strtoll: * @str: a string, e.g. `0x1234`, `-12345` * @value: (out) (nullable): parsed value * @min: minimum acceptable value, typically 0 * @max: maximum acceptable value, typically G_MAXINT64 * @base: default log base, usually %FU_INTEGER_BASE_AUTO * @error: (nullable): optional return location for an error * * Converts a string value to an integer. Values are assumed base 10, unless * prefixed with "0x" where they are parsed as base 16. * * Returns: %TRUE if the value was parsed correctly, or %FALSE for error * * Since: 2.0.0 **/ gboolean fu_strtoll(const gchar *str, gint64 *value, gint64 min, gint64 max, FuIntegerBase base, GError **error) { gchar *endptr = NULL; gint64 value_tmp; /* sanity check */ if (str == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "cannot parse NULL"); return FALSE; } /* detect hex */ if (base == FU_INTEGER_BASE_AUTO) { if (g_str_has_prefix(str, "0x")) { str += 2; base = FU_INTEGER_BASE_16; } else { base = FU_INTEGER_BASE_10; } } else if (base == FU_INTEGER_BASE_16 && g_str_has_prefix(str, "0x")) { str += 2; } else if (base == FU_INTEGER_BASE_10 && g_str_has_prefix(str, "0x")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "cannot parse 0x-prefixed base-10 string"); return FALSE; } /* convert */ value_tmp = g_ascii_strtoll(str, &endptr, base); /* nocheck:blocked */ if ((gsize)(endptr - str) != strlen(str) && *endptr != '\n') { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "cannot parse %s", str); return FALSE; } /* overflow check */ if (value_tmp == G_MAXINT64) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "cannot parse %s as caused overflow", str); return FALSE; } /* range check */ if (value_tmp < min) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "value %" G_GINT64_FORMAT " was below minimum %" G_GINT64_FORMAT, value_tmp, min); return FALSE; } if (value_tmp > max) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "value %" G_GINT64_FORMAT " was above maximum %" G_GINT64_FORMAT, value_tmp, max); return FALSE; } /* success */ if (value != NULL) *value = value_tmp; return TRUE; } /** * fu_strtobool: * @str: a string, e.g. `true` * @value: (out) (nullable): parsed value * @error: (nullable): optional return location for an error * * Converts a string value to a boolean. Only `true` and `false` are accepted values. * * Returns: %TRUE if the value was parsed correctly, or %FALSE for error * * Since: 1.8.2 **/ gboolean fu_strtobool(const gchar *str, gboolean *value, GError **error) { /* sanity check */ if (str == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "cannot parse NULL"); return FALSE; } /* be super strict */ if (g_strcmp0(str, "true") == 0) { if (value != NULL) *value = TRUE; return TRUE; } if (g_strcmp0(str, "false") == 0) { if (value != NULL) *value = FALSE; return TRUE; } /* invalid */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "cannot parse %s as boolean, expected true|false", str); return FALSE; } /** * fu_strstrip: * @str: a string, e.g. ` test ` * * Removes leading and trailing whitespace from a constant string. * * Returns: newly allocated string * * Since: 1.8.2 **/ gchar * fu_strstrip(const gchar *str) { guint head = G_MAXUINT; guint tail = 0; g_return_val_if_fail(str != NULL, NULL); /* find first non-space char */ for (guint i = 0; str[i] != '\0'; i++) { if (str[i] != ' ') { head = i; break; } } if (head == G_MAXUINT) return g_strdup(""); /* find last non-space char */ for (guint i = head; str[i] != '\0'; i++) { if (!g_ascii_isspace(str[i])) tail = i; } return g_strndup(str + head, tail - head + 1); } /** * fu_strdup: * @str: a string, e.g. ` test ` * @bufsz: the maximum size of @str * @offset: the offset to start copying from * * Copies a string from a buffer of a specified size up to (but not including) `NUL`. * * Returns: (transfer full): a #GString, possibly of zero size. * * Since: 1.8.11 **/ GString * fu_strdup(const gchar *str, gsize bufsz, gsize offset) { GString *substr; g_return_val_if_fail(str != NULL, NULL); g_return_val_if_fail(offset < bufsz, NULL); substr = g_string_new(NULL); while (offset < bufsz) { if (str[offset] == '\0') break; g_string_append_c(substr, str[offset++]); } return substr; } /** * fu_strwidth: * @text: the string to operate on * * Returns the width of the string in displayed characters on the console. * * Returns: width of text * * Since: 1.8.2 **/ gsize fu_strwidth(const gchar *text) { const gchar *p = text; gsize width = 0; g_return_val_if_fail(text != NULL, 0); while (*p) { gunichar c = g_utf8_get_char(p); if (g_unichar_iswide(c)) width += 2; else if (!g_unichar_iszerowidth(c)) width += 1; p = g_utf8_next_char(p); } return width; } /** * fu_strsplit: * @str: (not nullable): a string to split * @sz: size of @str, which must be more than 0 * @delimiter: a string which specifies the places at which to split the string * @max_tokens: the maximum number of pieces to split @str into * * Splits a string into a maximum of @max_tokens pieces, using the given * delimiter. If @max_tokens is reached, the remainder of string is appended * to the last token. * * Returns: (transfer full): a newly-allocated NULL-terminated array of strings * * Since: 1.8.2 **/ gchar ** fu_strsplit(const gchar *str, gsize sz, const gchar *delimiter, gint max_tokens) { g_return_val_if_fail(str != NULL, NULL); g_return_val_if_fail(sz > 0, NULL); if (str[sz - 1] != '\0') { g_autofree gchar *str2 = g_strndup(str, sz); return g_strsplit(str2, delimiter, max_tokens); } return g_strsplit(str, delimiter, max_tokens); } /** * fu_strsplit_bytes: * @blob: (not nullable): a #GBytes * @delimiter: a string which specifies the places at which to split the string * @max_tokens: the maximum number of pieces to split @str into * * Splits a string into a maximum of @max_tokens pieces, using the given * delimiter. If @max_tokens is reached, the remainder of string is appended * to the last token. * * Returns: (transfer full): a newly-allocated NULL-terminated array of strings * * Since: 2.0.7 **/ gchar ** fu_strsplit_bytes(GBytes *blob, const gchar *delimiter, gint max_tokens) { g_return_val_if_fail(blob != NULL, NULL); return fu_strsplit(g_bytes_get_data(blob, NULL), g_bytes_get_size(blob), delimiter, max_tokens); } typedef struct { FuStrsplitFunc callback; gpointer user_data; guint token_idx; const gchar *delimiter; gsize delimiter_sz; gboolean detected_nul; gboolean more_chunks; } FuStrsplitHelper; static gboolean fu_strsplit_buffer_drain(GByteArray *buf, FuStrsplitHelper *helper, GError **error) { gsize buf_offset = 0; while (buf_offset <= buf->len) { gsize offset; g_autoptr(GString) token = g_string_new(NULL); /* find first match in buffer, starting at the buffer offset */ for (offset = buf_offset; offset < buf->len; offset++) { if (buf->data[offset] == 0x0) { helper->detected_nul = TRUE; break; } if (strncmp((const gchar *)buf->data + offset, helper->delimiter, helper->delimiter_sz) == 0) break; } /* no token found, keep going */ if (helper->more_chunks && offset == buf->len) break; /* sanity check is valid UTF-8 */ g_string_append_len(token, (const gchar *)buf->data + buf_offset, offset - buf_offset); if (!g_utf8_validate_len(token->str, token->len, NULL)) { g_debug("ignoring invalid UTF-8, got: %s", token->str); } else { if (!helper->callback(token, helper->token_idx++, helper->user_data, error)) return FALSE; } if (helper->detected_nul) { buf_offset = buf->len; break; } buf_offset = offset + helper->delimiter_sz; } g_byte_array_remove_range(buf, 0, MIN(buf_offset, buf->len)); return TRUE; } /** * fu_strsplit_stream: * @stream: a #GInputStream to split * @offset: offset into @stream * @delimiter: a string which specifies the places at which to split the string * @callback: (scope call) (closure user_data): a #FuStrsplitFunc. * @user_data: user data * @error: (nullable): optional return location for an error * * Splits the string, calling the given function for each * of the tokens found. If any @callback returns %FALSE scanning is aborted. * * Use this function in preference to fu_strsplit() when the input file is untrusted, * and you don't want to allocate a GStrv with billions of one byte items. * * Returns: %TRUE if no @callback returned FALSE * * Since: 2.0.0 */ gboolean fu_strsplit_stream(GInputStream *stream, gsize offset, const gchar *delimiter, FuStrsplitFunc callback, gpointer user_data, GError **error) { g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GInputStream) stream_partial = NULL; FuStrsplitHelper helper = { .callback = callback, .user_data = user_data, .delimiter = delimiter, .token_idx = 0, }; g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(delimiter != NULL && delimiter[0] != '\0', FALSE); g_return_val_if_fail(callback != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); helper.delimiter_sz = strlen(delimiter); if (offset > 0) { stream_partial = fu_partial_input_stream_new(stream, offset, G_MAXSIZE, error); if (stream_partial == NULL) { g_prefix_error(error, "failed to cut string: "); return FALSE; } } else { stream_partial = g_object_ref(stream); } chunks = fu_chunk_array_new_from_stream(stream_partial, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 0x8000, error); if (chunks == NULL) return FALSE; for (gsize i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; g_byte_array_append(buf, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); helper.more_chunks = i != fu_chunk_array_length(chunks) - 1; if (!fu_strsplit_buffer_drain(buf, &helper, error)) return FALSE; if (helper.detected_nul) break; } return TRUE; } /** * fu_strsplit_full: * @str: a string to split * @sz: size of @str, or -1 for unknown * @delimiter: a string which specifies the places at which to split the string * @callback: (scope call) (closure user_data): a #FuStrsplitFunc. * @user_data: user data * @error: (nullable): optional return location for an error * * Splits the string, calling the given function for each * of the tokens found. If any @callback returns %FALSE scanning is aborted. * * Use this function in preference to fu_strsplit() when the input file is untrusted, * and you don't want to allocate a GStrv with billions of one byte items. * * Returns: %TRUE if no @callback returned FALSE * * Since: 1.8.2 */ gboolean fu_strsplit_full(const gchar *str, gssize sz, const gchar *delimiter, FuStrsplitFunc callback, gpointer user_data, GError **error) { gsize delimiter_sz; gsize offset_old = 0; gsize str_sz; guint token_idx = 0; g_return_val_if_fail(str != NULL, FALSE); g_return_val_if_fail(delimiter != NULL && delimiter[0] != '\0', FALSE); g_return_val_if_fail(callback != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* make known */ str_sz = sz != -1 ? (gsize)sz : strlen(str); delimiter_sz = strlen(delimiter); /* cannot split */ if (delimiter_sz > str_sz) { g_autoptr(GString) token = g_string_new(str); return callback(token, token_idx, user_data, error); } /* start splittin' */ while (offset_old <= str_sz) { gsize offset; g_autoptr(GString) token = g_string_new(NULL); for (offset = offset_old; offset < str_sz; offset++) { if (strncmp(str + offset, delimiter, delimiter_sz) == 0) break; } g_string_append_len(token, str + offset_old, offset - offset_old); if (!callback(token, token_idx++, user_data, error)) return FALSE; offset_old = offset + delimiter_sz; } /* success */ return TRUE; } /** * fu_strsafe: * @str: (nullable): a string to make safe for printing * @maxsz: maximum size of returned string * * Converts a string into something that can be safely printed. * * Returns: (transfer full): safe string, or %NULL if there was nothing valid * * Since: 1.8.2 **/ gchar * fu_strsafe(const gchar *str, gsize maxsz) { gboolean valid = FALSE; g_autoptr(GString) tmp = NULL; /* sanity check */ if (str == NULL || maxsz == 0) return NULL; /* replace non-printable chars with '.' */ tmp = g_string_sized_new(maxsz); for (gsize i = 0; i < maxsz && str[i] != '\0'; i++) { if (!g_ascii_isprint(str[i])) { g_string_append_c(tmp, '.'); continue; } g_string_append_c(tmp, str[i]); if (!g_ascii_isspace(str[i])) valid = TRUE; } /* if just junk, don't return 'all dots' */ if (tmp->len == 0 || !valid) return NULL; return g_string_free(g_steal_pointer(&tmp), FALSE); } /** * fu_strsafe_bytes: * @blob: (not nullable): a #GBytes * @maxsz: maximum size of returned string * * Converts a #GBytes into something that can be safely printed. * * Returns: (transfer full): safe string, or %NULL if there was nothing valid * * Since: 2.0.2 **/ gchar * fu_strsafe_bytes(GBytes *blob, gsize maxsz) { g_return_val_if_fail(blob != NULL, NULL); return fu_strsafe((const gchar *)g_bytes_get_data(blob, NULL), MIN(g_bytes_get_size(blob), maxsz)); } /** * fu_strjoin: * @separator: (nullable): string to insert between each of the strings * @array: (element-type utf8): a #GPtrArray * * Joins an array of strings together to form one long string, with the optional * separator inserted between each of them. * * If @array has no items, the return value will be an empty string. * If @array contains a single item, separator will not appear in the resulting * string. * * Returns: a string * * Since: 1.8.2 **/ gchar * fu_strjoin(const gchar *separator, GPtrArray *array) { g_autofree const gchar **strv = NULL; g_return_val_if_fail(array != NULL, NULL); strv = g_new0(const gchar *, array->len + 1); for (guint i = 0; i < array->len; i++) strv[i] = g_ptr_array_index(array, i); return g_strjoinv(separator, (gchar **)strv); } /** * fu_strpassmask: * @str: (nullable): a string to make safe for printing * * Hides password strings encoded in HTTP requests. * * Returns: a string * * Since: 1.9.10 **/ gchar * fu_strpassmask(const gchar *str) { g_autoptr(GString) tmp = g_string_new(str); if (tmp->str != NULL && g_strstr_len(tmp->str, -1, "@") != NULL && g_strstr_len(tmp->str, -1, ":") != NULL) { gboolean is_password = FALSE; gboolean is_url = FALSE; for (guint i = 0; i < tmp->len; i++) { const gchar *url_prefixes[] = {"http://", "https://", NULL}; for (guint j = 0; url_prefixes[j] != NULL; j++) { if (g_str_has_prefix(tmp->str + i, url_prefixes[j])) { is_url = TRUE; i += strlen(url_prefixes[j]); break; } } if (tmp->str[i] == ' ' || tmp->str[i] == '@' || tmp->str[i] == '/') { is_url = FALSE; is_password = FALSE; continue; } if (is_url && tmp->str[i] == ':') { is_password = TRUE; continue; } if (is_url && is_password) { if (tmp->str[i] == '@') { is_password = FALSE; continue; } tmp->str[i] = 'X'; } } } return g_string_free(g_steal_pointer(&tmp), FALSE); } /** * fu_utf16_to_utf8_byte_array: * @array: a #GByteArray * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * @error: (nullable): optional return location for an error * * Converts a UTF-16 buffer to a UTF-8 string. * * Returns: (transfer full): a string, or %NULL on error * * Since: 1.9.3 **/ gchar * fu_utf16_to_utf8_byte_array(GByteArray *array, FuEndianType endian, GError **error) { g_autofree guint16 *buf16 = NULL; g_return_val_if_fail(array != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); if (array->len % 2 != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid UTF-16 buffer length"); return NULL; } buf16 = g_new0(guint16, (array->len / sizeof(guint16)) + 1); for (guint i = 0; i < array->len / 2; i++) { guint16 data = fu_memread_uint16(array->data + (i * 2), endian); fu_memwrite_uint16((guint8 *)(buf16 + i), data, G_BYTE_ORDER); } return g_utf16_to_utf8(buf16, array->len / sizeof(guint16), NULL, NULL, error); } /** * fu_utf8_to_utf16_byte_array: * @str: a UTF-8 string * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * @flags: a FuUtfConvertFlags, e.g. %FU_UTF_CONVERT_FLAG_APPEND_NUL * @error: (nullable): optional return location for an error * * Converts UTF-8 string to a buffer of UTF-16, optionially including the trailing NULw. * * Returns: (transfer full): a #GByteArray, or %NULL on error * * Since: 1.9.3 **/ GByteArray * fu_utf8_to_utf16_byte_array(const gchar *str, FuEndianType endian, FuUtfConvertFlags flags, GError **error) { glong buf_utf16sz = 0; g_autoptr(GByteArray) array = g_byte_array_new(); g_autofree gunichar2 *buf_utf16 = NULL; g_return_val_if_fail(str != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); buf_utf16 = g_utf8_to_utf16(str, (glong)-1, NULL, &buf_utf16sz, error); if (buf_utf16 == NULL) return NULL; if (flags & FU_UTF_CONVERT_FLAG_APPEND_NUL) buf_utf16sz += 1; for (glong i = 0; i < buf_utf16sz; i++) { guint16 data = fu_memread_uint16((guint8 *)(buf_utf16 + i), G_BYTE_ORDER); fu_byte_array_append_uint16(array, data, endian); } return g_steal_pointer(&array); } /** * fu_utf16_to_utf8_bytes: * @bytes: a #GBytes * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * @error: (nullable): optional return location for an error * * Converts a UTF-16 buffer to a UTF-8 string. * * Returns: (transfer full): a string, or %NULL on error * * Since: 1.9.3 **/ gchar * fu_utf16_to_utf8_bytes(GBytes *bytes, FuEndianType endian, GError **error) { GByteArray array = {0x0}; g_return_val_if_fail(bytes != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); array.data = (guint8 *)g_bytes_get_data(bytes, NULL); array.len = g_bytes_get_size(bytes); return fu_utf16_to_utf8_byte_array(&array, endian, error); } /** * fu_utf8_to_utf16_bytes: * @str: a UTF-8 string * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * @error: (nullable): optional return location for an error * * Converts UTF-8 string to a buffer of UTF-16, optionally including the trailing NULw. * * Returns: (transfer full): a #GBytes, or %NULL on error * * Since: 1.9.3 **/ GBytes * fu_utf8_to_utf16_bytes(const gchar *str, FuEndianType endian, FuUtfConvertFlags flags, GError **error) { g_autoptr(GByteArray) buf = NULL; g_return_val_if_fail(str != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); buf = fu_utf8_to_utf16_byte_array(str, endian, flags, error); if (buf == NULL) return NULL; return g_bytes_new(buf->data, buf->len); } fwupd-2.0.10/libfwupdplugin/fu-string.h000066400000000000000000000054561501337203100200460ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-endian.h" typedef enum { FU_INTEGER_BASE_AUTO = 0, FU_INTEGER_BASE_10 = 10, FU_INTEGER_BASE_16 = 16, } FuIntegerBase; gchar * fu_strsafe(const gchar *str, gsize maxsz); gchar * fu_strsafe_bytes(GBytes *blob, gsize maxsz); gchar * fu_strpassmask(const gchar *str) G_GNUC_NON_NULL(1); gboolean fu_strtoull(const gchar *str, guint64 *value, guint64 min, guint64 max, FuIntegerBase base, GError **error); gboolean fu_strtoll(const gchar *str, gint64 *value, gint64 min, gint64 max, FuIntegerBase base, GError **error); gboolean fu_strtobool(const gchar *str, gboolean *value, GError **error); gchar * fu_strstrip(const gchar *str) G_GNUC_NON_NULL(1); gsize fu_strwidth(const gchar *text) G_GNUC_NON_NULL(1); gchar ** fu_strsplit(const gchar *str, gsize sz, const gchar *delimiter, gint max_tokens) G_GNUC_NON_NULL(1); gchar ** fu_strsplit_bytes(GBytes *blob, const gchar *delimiter, gint max_tokens) G_GNUC_NON_NULL(1); gchar * fu_strjoin(const gchar *separator, GPtrArray *array) G_GNUC_NON_NULL(1, 2); GString * fu_strdup(const gchar *str, gsize bufsz, gsize offset) G_GNUC_NON_NULL(1); /** * FuStrsplitFunc: * @token: a #GString * @token_idx: the token number * @user_data: (closure): user data * @error: a #GError or NULL * * The fu_strsplit_full() iteration callback. */ typedef gboolean (*FuStrsplitFunc)(GString *token, guint token_idx, gpointer user_data, GError **error); gboolean fu_strsplit_full(const gchar *str, gssize sz, const gchar *delimiter, FuStrsplitFunc callback, gpointer user_data, GError **error) G_GNUC_NON_NULL(1, 3); gboolean fu_strsplit_stream(GInputStream *stream, gsize offset, const gchar *delimiter, FuStrsplitFunc callback, gpointer user_data, GError **error) G_GNUC_NON_NULL(1, 3); /** * FuUtfConvertFlags: * @FU_UTF_CONVERT_FLAG_NONE: No flags set * @FU_UTF_CONVERT_FLAG_APPEND_NUL: Include the trailing `NUL` or `NULw` in the buffer * * The flags to use when converting to and from UTF-8. **/ typedef enum { FU_UTF_CONVERT_FLAG_NONE = 0, FU_UTF_CONVERT_FLAG_APPEND_NUL = 1 << 0, } FuUtfConvertFlags; gchar * fu_utf16_to_utf8_byte_array(GByteArray *array, FuEndianType endian, GError **error) G_GNUC_NON_NULL(1); GByteArray * fu_utf8_to_utf16_byte_array(const gchar *str, FuEndianType endian, FuUtfConvertFlags flags, GError **error) G_GNUC_NON_NULL(1); gchar * fu_utf16_to_utf8_bytes(GBytes *bytes, FuEndianType endian, GError **error) G_GNUC_NON_NULL(1); GBytes * fu_utf8_to_utf16_bytes(const gchar *str, FuEndianType endian, FuUtfConvertFlags flags, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-sum.c000066400000000000000000000110331501337203100173230ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuCommon" #include "config.h" #include "fu-mem.h" #include "fu-sum.h" /** * fu_sum8: * @buf: memory buffer * @bufsz: size of @buf * * Returns the arithmetic sum of all bytes in @buf. * * Returns: sum value * * Since: 1.8.2 **/ guint8 fu_sum8(const guint8 *buf, gsize bufsz) { guint8 checksum = 0; g_return_val_if_fail(buf != NULL, G_MAXUINT8); for (gsize i = 0; i < bufsz; i++) checksum += buf[i]; return checksum; } /** * fu_sum8_bytes: * @blob: a #GBytes * * Returns the arithmetic sum of all bytes in @blob. * * Returns: sum value * * Since: 1.8.2 **/ guint8 fu_sum8_bytes(GBytes *blob) { g_return_val_if_fail(blob != NULL, G_MAXUINT8); if (g_bytes_get_size(blob) == 0) return 0; return fu_sum8(g_bytes_get_data(blob, NULL), g_bytes_get_size(blob)); } /** * fu_sum16: * @buf: memory buffer * @bufsz: size of @buf * * Returns the arithmetic sum of all bytes in @buf, adding them one byte at a time. * * Returns: sum value * * Since: 1.8.2 **/ guint16 fu_sum16(const guint8 *buf, gsize bufsz) { guint16 checksum = 0; g_return_val_if_fail(buf != NULL, G_MAXUINT16); for (gsize i = 0; i < bufsz; i++) checksum += buf[i]; return checksum; } /** * fu_sum16_bytes: * @blob: a #GBytes * * Returns the arithmetic sum of all bytes in @blob, adding them one byte at a time. * * Returns: sum value * * Since: 1.8.2 **/ guint16 fu_sum16_bytes(GBytes *blob) { g_return_val_if_fail(blob != NULL, G_MAXUINT16); return fu_sum16(g_bytes_get_data(blob, NULL), g_bytes_get_size(blob)); } /** * fu_sum16w: * @buf: memory buffer * @bufsz: size of @buf * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * * Returns the arithmetic sum of all bytes in @buf, adding them one word at a time. * The caller must ensure that @bufsz is a multiple of 2. * * Returns: sum value * * Since: 1.8.2 **/ guint16 fu_sum16w(const guint8 *buf, gsize bufsz, FuEndianType endian) { guint16 checksum = 0; g_return_val_if_fail(buf != NULL, G_MAXUINT16); g_return_val_if_fail(bufsz % 2 == 0, G_MAXUINT16); for (gsize i = 0; i < bufsz; i += 2) checksum += fu_memread_uint16(&buf[i], endian); return checksum; } /** * fu_sum16w_bytes: * @blob: a #GBytes * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * * Returns the arithmetic sum of all bytes in @blob, adding them one word at a time. * The caller must ensure that the size of @blob is a multiple of 2. * * Returns: sum value * * Since: 1.8.2 **/ guint16 fu_sum16w_bytes(GBytes *blob, FuEndianType endian) { g_return_val_if_fail(blob != NULL, G_MAXUINT16); return fu_sum16w(g_bytes_get_data(blob, NULL), g_bytes_get_size(blob), endian); } /** * fu_sum32: * @buf: memory buffer * @bufsz: size of @buf * * Returns the arithmetic sum of all bytes in @buf, adding them one byte at a time. * * Returns: sum value * * Since: 1.8.2 **/ guint32 fu_sum32(const guint8 *buf, gsize bufsz) { guint32 checksum = 0; g_return_val_if_fail(buf != NULL, G_MAXUINT32); for (gsize i = 0; i < bufsz; i++) checksum += buf[i]; return checksum; } /** * fu_sum32_bytes: * @blob: a #GBytes * * Returns the arithmetic sum of all bytes in @blob, adding them one byte at a time. * * Returns: sum value * * Since: 1.8.2 **/ guint32 fu_sum32_bytes(GBytes *blob) { g_return_val_if_fail(blob != NULL, G_MAXUINT32); return fu_sum32(g_bytes_get_data(blob, NULL), g_bytes_get_size(blob)); } /** * fu_sum32w: * @buf: memory buffer * @bufsz: size of @buf * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * * Returns the arithmetic sum of all bytes in @buf, adding them one dword at a time. * The caller must ensure that @bufsz is a multiple of 4. * * Returns: sum value * * Since: 1.8.2 **/ guint32 fu_sum32w(const guint8 *buf, gsize bufsz, FuEndianType endian) { guint32 checksum = 0; g_return_val_if_fail(buf != NULL, G_MAXUINT32); g_return_val_if_fail(bufsz % 4 == 0, G_MAXUINT32); for (gsize i = 0; i < bufsz; i += 4) checksum += fu_memread_uint32(&buf[i], endian); return checksum; } /** * fu_sum32w_bytes: * @blob: a #GBytes * @endian: an endian type, e.g. %G_LITTLE_ENDIAN * * Returns the arithmetic sum of all bytes in @blob, adding them one dword at a time. * The caller must ensure that the size of @blob is a multiple of 4. * * Returns: sum value * * Since: 1.8.2 **/ guint32 fu_sum32w_bytes(GBytes *blob, FuEndianType endian) { g_return_val_if_fail(blob != NULL, G_MAXUINT32); return fu_sum32w(g_bytes_get_data(blob, NULL), g_bytes_get_size(blob), endian); } fwupd-2.0.10/libfwupdplugin/fu-sum.h000066400000000000000000000012421501337203100173310ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-endian.h" guint8 fu_sum8(const guint8 *buf, gsize bufsz); guint8 fu_sum8_bytes(GBytes *blob); guint16 fu_sum16(const guint8 *buf, gsize bufsz); guint16 fu_sum16_bytes(GBytes *blob); guint16 fu_sum16w(const guint8 *buf, gsize bufsz, FuEndianType endian); guint16 fu_sum16w_bytes(GBytes *blob, FuEndianType endian); guint32 fu_sum32(const guint8 *buf, gsize bufsz); guint32 fu_sum32_bytes(GBytes *blob); guint32 fu_sum32w(const guint8 *buf, gsize bufsz, FuEndianType endian); guint32 fu_sum32w_bytes(GBytes *blob, FuEndianType endian); fwupd-2.0.10/libfwupdplugin/fu-test-device.c000066400000000000000000000006221501337203100207350ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-test-device.h" struct _FuTestDevice { FuDevice parent_instance; }; G_DEFINE_TYPE(FuTestDevice, fu_test_device, FU_TYPE_DEVICE) static void fu_test_device_init(FuTestDevice *self) { } static void fu_test_device_class_init(FuTestDeviceClass *klass) { } fwupd-2.0.10/libfwupdplugin/fu-test-device.h000066400000000000000000000004341501337203100207430ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-device.h" #define FU_TYPE_TEST_DEVICE (fu_test_device_get_type()) G_DECLARE_FINAL_TYPE(FuTestDevice, fu_test_device, FU, TEST_DEVICE, FuDevice) fwupd-2.0.10/libfwupdplugin/fu-udev-device-private.h000066400000000000000000000023771501337203100224070ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-udev-device.h" void fu_udev_device_emit_changed(FuUdevDevice *self) G_GNUC_NON_NULL(1); FuUdevDevice * fu_udev_device_new(FuContext *ctx, const gchar *sysfs_path); void fu_udev_device_set_number(FuUdevDevice *self, guint64 number) G_GNUC_NON_NULL(1); void fu_udev_device_set_subsystem(FuUdevDevice *self, const gchar *subsystem) G_GNUC_NON_NULL(1); void fu_udev_device_set_devtype(FuUdevDevice *self, const gchar *devtype) G_GNUC_NON_NULL(1); void fu_udev_device_add_property(FuUdevDevice *self, const gchar *key, const gchar *value); gboolean fu_udev_device_parse_number(FuUdevDevice *self, GError **error) G_GNUC_NON_NULL(1); gboolean fu_udev_device_match_subsystem(FuUdevDevice *self, const gchar *subsystem) G_GNUC_NON_NULL(1); gchar * fu_udev_device_get_device_file_from_subsystem(FuUdevDevice *self, const gchar *subsystem, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_udev_device_ioctl(FuUdevDevice *self, gulong request, guint8 *buf, gsize bufsz, gint *rc, guint timeout, FuIoctlFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-udev-device.c000066400000000000000000002130271501337203100207260ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuUdevDevice" #include "config.h" #include #include #ifdef HAVE_ERRNO_H #include #endif #ifdef HAVE_IOCTL_H #include #endif #include #include #include #include #include "fu-device-event-private.h" #include "fu-device-private.h" #include "fu-i2c-device.h" #include "fu-ioctl-private.h" #include "fu-path.h" #include "fu-string.h" #include "fu-udev-device-private.h" /** * FuUdevDevice: * * A UDev device, typically only available on Linux. * * See also: [class@FuDevice] */ typedef struct { gchar *subsystem; gchar *bind_id; gchar *driver; gchar *device_file; gchar *devtype; guint64 number; FuIOChannel *io_channel; FuIoChannelOpenFlag open_flags; GHashTable *properties; gboolean properties_valid; } FuUdevDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuUdevDevice, fu_udev_device, FU_TYPE_DEVICE); enum { PROP_0, PROP_SUBSYSTEM, PROP_DRIVER, PROP_DEVICE_FILE, PROP_BIND_ID, PROP_DEVTYPE, PROP_LAST }; enum { SIGNAL_CHANGED, SIGNAL_LAST }; static guint signals[SIGNAL_LAST] = {0}; #define GET_PRIVATE(o) (fu_udev_device_get_instance_private(o)) /** * fu_udev_device_emit_changed: * @self: a #FuUdevDevice * * Emits the ::changed signal for the object. * * Since: 1.1.2 **/ void fu_udev_device_emit_changed(FuUdevDevice *self) { g_autoptr(GError) error = NULL; g_return_if_fail(FU_IS_UDEV_DEVICE(self)); g_debug("FuUdevDevice emit changed"); if (!fu_device_rescan(FU_DEVICE(self), &error)) g_debug("%s", error->message); g_signal_emit(self, signals[SIGNAL_CHANGED], 0); } static void fu_udev_device_to_string(FuDevice *device, guint idt, GString *str) { FuUdevDevice *self = FU_UDEV_DEVICE(device); FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_autofree gchar *open_flags = fu_io_channel_open_flag_to_string(priv->open_flags); fwupd_codec_string_append_hex(str, idt, "Number", priv->number); fwupd_codec_string_append(str, idt, "Subsystem", priv->subsystem); fwupd_codec_string_append(str, idt, "Devtype", priv->devtype); fwupd_codec_string_append(str, idt, "Driver", priv->driver); fwupd_codec_string_append(str, idt, "BindId", priv->bind_id); fwupd_codec_string_append(str, idt, "DeviceFile", priv->device_file); fwupd_codec_string_append(str, idt, "OpenFlags", open_flags); } static gboolean fu_udev_device_ensure_bind_id(FuUdevDevice *self, GError **error) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); /* sanity check */ if (priv->bind_id != NULL) return TRUE; /* automatically set the bind ID from the subsystem */ if (g_strcmp0(priv->subsystem, "pci") == 0) { priv->bind_id = fu_udev_device_read_property(self, "PCI_SLOT_NAME", error); return priv->bind_id != NULL; } if (g_strcmp0(priv->subsystem, "hid") == 0) { priv->bind_id = fu_udev_device_read_property(self, "HID_PHYS", error); return priv->bind_id != NULL; } if (g_strcmp0(priv->subsystem, "usb") == 0) { priv->bind_id = g_path_get_basename(fu_udev_device_get_sysfs_path(self)); return TRUE; } /* nothing found automatically */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot derive bind-id from subsystem %s", priv->subsystem); return FALSE; } /* private */ void fu_udev_device_set_subsystem(FuUdevDevice *self, const gchar *subsystem) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); /* not changed */ if (g_strcmp0(priv->subsystem, subsystem) == 0) return; g_free(priv->subsystem); priv->subsystem = g_strdup(subsystem); g_object_notify(G_OBJECT(self), "subsystem"); } /** * fu_udev_device_set_bind_id: * @self: a #FuUdevDevice * @bind_id: a bind-id string, e.g. `pci:0:0:1` * * Sets the device ID used for binding the device, e.g. `pci:1:2:3` * * Since: 1.7.2 **/ void fu_udev_device_set_bind_id(FuUdevDevice *self, const gchar *bind_id) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); /* not changed */ if (g_strcmp0(priv->bind_id, bind_id) == 0) return; g_free(priv->bind_id); priv->bind_id = g_strdup(bind_id); g_object_notify(G_OBJECT(self), "bind-id"); } static void fu_udev_device_set_driver(FuUdevDevice *self, const gchar *driver) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); /* not changed */ if (g_strcmp0(priv->driver, driver) == 0) return; g_free(priv->driver); priv->driver = g_strdup(driver); g_object_notify(G_OBJECT(self), "driver"); } /** * fu_udev_device_set_device_file: * @self: a #FuUdevDevice * @device_file: (nullable): a device path * * Sets the device file to use for reading and writing. * * Since: 1.8.7 **/ void fu_udev_device_set_device_file(FuUdevDevice *self, const gchar *device_file) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); /* not changed */ if (g_strcmp0(priv->device_file, device_file) == 0) return; g_free(priv->device_file); priv->device_file = g_strdup(device_file); g_object_notify(G_OBJECT(self), "device-file"); } static gchar * fu_udev_device_get_symlink_target(FuUdevDevice *self, const gchar *attr, GError **error) { FuDeviceEvent *event = NULL; g_autofree gchar *event_id = NULL; g_autofree gchar *fn_attr = NULL; g_autofree gchar *symlink_target = NULL; g_autofree gchar *value = NULL; if (fu_udev_device_get_sysfs_path(self) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no sysfs path"); return NULL; } /* need event ID */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_strdup_printf("GetSymlinkTarget:Attr=%s", attr); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return NULL; return g_strdup(fu_device_event_get_str(event, "Data", error)); } /* save */ if (event_id != NULL) event = fu_device_save_event(FU_DEVICE(self), event_id); /* find target */ fn_attr = g_build_filename(fu_udev_device_get_sysfs_path(self), attr, NULL); symlink_target = fu_path_get_symlink_target(fn_attr, error); if (symlink_target == NULL) return NULL; value = g_path_get_basename(symlink_target); /* save response */ if (event != NULL) fu_device_event_set_str(event, "Data", value); /* success */ return g_steal_pointer(&value); } /* private */ void fu_udev_device_set_number(FuUdevDevice *self, guint64 number) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_UDEV_DEVICE(self)); priv->number = number; } /* private */ void fu_udev_device_set_devtype(FuUdevDevice *self, const gchar *devtype) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); /* not changed */ if (g_strcmp0(priv->devtype, devtype) == 0) return; g_free(priv->devtype); priv->devtype = g_strdup(devtype); g_object_notify(G_OBJECT(self), "devtype"); } /* private */ gboolean fu_udev_device_parse_number(FuUdevDevice *self, GError **error) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(GString) path = g_string_new(fu_udev_device_get_sysfs_path(self)); if (path->len == 0) return TRUE; for (guint i = path->len - 1; i > 0; i--) { if (!g_ascii_isdigit(path->str[i])) { g_string_erase(path, 0, i + 1); break; } } if (path->len > 0) { if (!fu_strtoull(path->str, &priv->number, 0x0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_udev_device_ensure_devtype_from_modalias(FuUdevDevice *self, GError **error) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); const gchar *devtype_modalias[] = {"mmc", "platform", NULL}; g_autofree gchar *prop_modalias = NULL; g_auto(GStrv) split_modalias = NULL; /* only some subsystems forget to set the DEVTYPE property */ if (!g_strv_contains(devtype_modalias, priv->subsystem)) return TRUE; /* parse out subsystem:devtype */ prop_modalias = fu_udev_device_read_property(self, "MODALIAS", error); if (prop_modalias == NULL) return FALSE; split_modalias = g_strsplit(prop_modalias, ":", 2); if (g_strv_length(split_modalias) >= 2) priv->devtype = g_strdup(split_modalias[1]); /* success */ return TRUE; } static gboolean fu_udev_device_probe(FuDevice *device, GError **error) { FuUdevDevice *self = FU_UDEV_DEVICE(device); FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_autofree gchar *subsystem = NULL; g_autofree gchar *attr_device = NULL; g_autofree gchar *attr_vendor = NULL; /* find the subsystem, driver and devtype */ if (priv->subsystem == NULL) { g_autofree gchar *subsystem_tmp = fu_udev_device_get_symlink_target(self, "subsystem", error); if (subsystem_tmp == NULL) { g_prefix_error(error, "failed to read subsystem: "); return FALSE; } fu_udev_device_set_subsystem(self, subsystem_tmp); } if (priv->driver == NULL) priv->driver = fu_udev_device_get_symlink_target(self, "driver", NULL); if (priv->devtype == NULL) { priv->devtype = fu_udev_device_read_property(self, "DEVTYPE", NULL); if (priv->devtype == NULL) { if (!fu_udev_device_ensure_devtype_from_modalias(self, error)) return FALSE; } } if (priv->device_file == NULL) { g_autofree gchar *prop_devname = fu_udev_device_read_property(self, "DEVNAME", NULL); if (prop_devname != NULL) { g_autofree gchar *device_file = g_strdup_printf("/dev/%s", prop_devname); fu_udev_device_set_device_file(self, device_file); } } /* get IDs */ attr_vendor = fu_udev_device_read_sysfs(self, "vendor", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (attr_vendor != NULL) { guint64 tmp64 = 0; if (!fu_strtoull(attr_vendor, &tmp64, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, NULL)) { fu_device_set_vendor(device, attr_vendor); } else { fu_device_set_vid(device, (guint16)tmp64); } } attr_device = fu_udev_device_read_sysfs(self, "device", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (attr_device != NULL) { guint64 tmp64 = 0; if (!fu_strtoull(attr_device, &tmp64, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_device_set_pid(device, (guint16)tmp64); } /* set number */ if (fu_udev_device_get_sysfs_path(self) != NULL) { g_autoptr(GError) error_local = NULL; if (!fu_udev_device_parse_number(self, &error_local)) g_debug("failed to convert udev number: %s", error_local->message); } /* set vendor ID */ if (priv->subsystem != NULL) subsystem = g_ascii_strup(priv->subsystem, -1); if (subsystem != NULL) fu_device_build_vendor_id_u16(device, subsystem, fu_device_get_vid(device)); /* add GUIDs in order of priority */ if (subsystem != NULL) { fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, subsystem, "VEN", NULL); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_VISIBLE | FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, subsystem, "VEN", "DEV", NULL); } /* add device class */ if (subsystem != NULL) { g_autofree gchar *cls = fu_udev_device_read_sysfs(self, "class", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); g_autofree gchar *devtype = fu_udev_device_read_property(self, "DEVTYPE", NULL); if (cls != NULL && g_str_has_prefix(cls, "0x")) fu_device_add_instance_strup(device, "CLASS", cls + 2); else fu_device_add_instance_strup(device, "CLASS", cls); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, subsystem, "VEN", "CLASS", NULL); /* add devtype */ fu_device_add_instance_strup(device, "TYPE", devtype); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, subsystem, "TYPE", NULL); /* add the driver */ fu_device_add_instance_str(device, "DRIVER", priv->driver); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, subsystem, "DRIVER", NULL); } /* determine if we're wired internally */ if (g_strcmp0(priv->subsystem, "i2c") != 0) { g_autoptr(FuDevice) parent_i2c = fu_device_get_backend_parent_with_subsystem(device, "i2c", NULL); if (parent_i2c != NULL) fu_device_add_flag(device, FWUPD_DEVICE_FLAG_INTERNAL); } /* success */ return TRUE; } /** * fu_udev_device_get_subsystem_depth: * @self: a #FuUdevDevice * @subsystem: a subsystem * * Determine how far up a chain a given device is * * Returns: unsigned integer * * Since: 2.0.0 **/ guint fu_udev_device_get_subsystem_depth(FuUdevDevice *self, const gchar *subsystem) { g_autoptr(FuDevice) device_tmp = NULL; device_tmp = fu_device_get_backend_parent_with_subsystem(FU_DEVICE(self), subsystem, NULL); if (device_tmp == NULL) return 0; if (g_strcmp0(fu_device_get_id(device_tmp), fu_device_get_id(FU_DEVICE(self))) == 0) return 0; for (guint i = 0;; i++) { g_autoptr(FuDevice) parent = fu_device_get_backend_parent(FU_DEVICE(device_tmp), NULL); if (parent == NULL) return i; g_set_object(&device_tmp, parent); } return 0; } static void fu_udev_device_probe_complete(FuDevice *device) { FuUdevDevice *self = FU_UDEV_DEVICE(device); FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_hash_table_remove_all(priv->properties); priv->properties_valid = FALSE; } static gboolean fu_udev_device_unbind_driver(FuDevice *device, GError **error) { FuUdevDevice *self = FU_UDEV_DEVICE(device); FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_autofree gchar *fn = NULL; g_autoptr(GFile) file = NULL; g_autoptr(GOutputStream) stream = NULL; /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) return TRUE; /* is already unbound */ if (fu_udev_device_get_sysfs_path(self) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "not initialized"); return FALSE; } fn = g_build_filename(fu_udev_device_get_sysfs_path(self), "driver", "unbind", NULL); if (!g_file_test(fn, G_FILE_TEST_EXISTS)) return TRUE; /* write bus ID to file */ if (!fu_udev_device_ensure_bind_id(self, error)) return FALSE; file = g_file_new_for_path(fn); stream = G_OUTPUT_STREAM(g_file_replace(file, NULL, FALSE, G_FILE_CREATE_NONE, NULL, error)); if (stream == NULL) return FALSE; return g_output_stream_write_all(stream, priv->bind_id, strlen(priv->bind_id), NULL, NULL, error); } static gboolean fu_udev_device_bind_driver(FuDevice *device, const gchar *subsystem, const gchar *driver, GError **error) { FuUdevDevice *self = FU_UDEV_DEVICE(device); FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_autofree gchar *driver_safe = g_strdup(driver); g_autofree gchar *fn = NULL; g_autoptr(GFile) file = NULL; g_autoptr(GOutputStream) stream = NULL; /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) return TRUE; /* copy the logic from modprobe */ g_strdelimit(driver_safe, "-", '_'); /* driver exists */ fn = g_strdup_printf("/sys/module/%s/drivers/%s:%s/bind", driver_safe, subsystem, driver_safe); if (!g_file_test(fn, G_FILE_TEST_EXISTS)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot bind with %s:%s", subsystem, driver); return FALSE; } /* write bus ID to file */ if (!fu_udev_device_ensure_bind_id(self, error)) return FALSE; if (priv->bind_id == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "bind-id not set for subsystem %s", priv->subsystem); return FALSE; } file = g_file_new_for_path(fn); stream = G_OUTPUT_STREAM(g_file_replace(file, NULL, FALSE, G_FILE_CREATE_NONE, NULL, error)); if (stream == NULL) return FALSE; return g_output_stream_write_all(stream, priv->bind_id, strlen(priv->bind_id), NULL, NULL, error); } static FuIoChannelOpenFlag fu_udev_device_get_open_flags(FuUdevDevice *self) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), 0); return priv->open_flags; } static void fu_udev_device_invalidate(FuDevice *device) { FuUdevDevice *self = FU_UDEV_DEVICE(device); FuUdevDevicePrivate *priv = GET_PRIVATE(self); priv->properties_valid = FALSE; g_hash_table_remove_all(priv->properties); } static void fu_udev_device_incorporate(FuDevice *self, FuDevice *donor) { FuUdevDevice *uself = FU_UDEV_DEVICE(self); FuUdevDevice *udonor = FU_UDEV_DEVICE(donor); FuUdevDevicePrivate *priv = GET_PRIVATE(uself); g_return_if_fail(FU_IS_UDEV_DEVICE(self)); g_return_if_fail(FU_IS_UDEV_DEVICE(donor)); if (priv->device_file == NULL) fu_udev_device_set_device_file(uself, fu_udev_device_get_device_file(udonor)); if (priv->subsystem == NULL) fu_udev_device_set_subsystem(uself, fu_udev_device_get_subsystem(udonor)); if (priv->bind_id == NULL) fu_udev_device_set_bind_id(uself, fu_udev_device_get_bind_id(udonor)); if (priv->driver == NULL) fu_udev_device_set_driver(uself, fu_udev_device_get_driver(udonor)); if (priv->devtype == NULL) fu_udev_device_set_devtype(uself, fu_udev_device_get_devtype(udonor)); if (priv->number == 0x0) fu_udev_device_set_number(uself, fu_udev_device_get_number(udonor)); if (priv->open_flags == FU_IO_CHANNEL_OPEN_FLAG_NONE) priv->open_flags = fu_udev_device_get_open_flags(udonor); } /** * fu_udev_device_get_subsystem: * @self: a #FuUdevDevice * * Gets the device subsystem, e.g. `pci` * * Returns: a subsystem, or NULL if unset or invalid * * Since: 1.1.2 **/ const gchar * fu_udev_device_get_subsystem(FuUdevDevice *self) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), NULL); return priv->subsystem; } /** * fu_udev_device_get_bind_id: * @self: a #FuUdevDevice * * Gets the device ID used for binding the device, e.g. `pci:1:2:3` * * Returns: a bind_id, or NULL if unset or invalid * * Since: 1.7.2 **/ const gchar * fu_udev_device_get_bind_id(FuUdevDevice *self) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), NULL); fu_udev_device_ensure_bind_id(self, NULL); return priv->bind_id; } /** * fu_udev_device_get_driver: * @self: a #FuUdevDevice * * Gets the device driver, e.g. `psmouse`. * * Returns: a subsystem, or NULL if unset or invalid * * Since: 1.5.3 **/ const gchar * fu_udev_device_get_driver(FuUdevDevice *self) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), NULL); return priv->driver; } /** * fu_udev_device_get_device_file: * @self: a #FuUdevDevice * * Gets the device node. * * Returns: a device file, or NULL if unset * * Since: 1.3.1 **/ const gchar * fu_udev_device_get_device_file(FuUdevDevice *self) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), NULL); return priv->device_file; } /** * fu_udev_device_get_sysfs_path: * @self: a #FuUdevDevice * * Gets the device sysfs path, e.g. `/sys/devices/pci0000:00/0000:00:14.0`. * * Returns: a local path, or NULL if unset or invalid * * Since: 1.1.2 **/ const gchar * fu_udev_device_get_sysfs_path(FuUdevDevice *self) { g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), NULL); return fu_device_get_backend_id(FU_DEVICE(self)); } /** * fu_udev_device_get_number: * @self: a #FuUdevDevice * * Gets the device number, if any. * * Returns: integer, 0 if the data is unavailable. * * Since: 1.5.0 **/ guint64 fu_udev_device_get_number(FuUdevDevice *self) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), 0); return priv->number; } static gchar * fu_udev_device_get_parent_subsystems(FuUdevDevice *self) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(GString) str = g_string_new(NULL); g_autoptr(FuUdevDevice) udev_device = g_object_ref(self); /* not true, but good enough for emulation */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) return g_strdup(priv->subsystem); /* find subsystems of self and all parent devices */ while (TRUE) { g_autoptr(FuUdevDevice) parent = NULL; if (fu_udev_device_get_devtype(udev_device) != NULL) { g_string_append_printf(str, "%s:%s,", fu_udev_device_get_subsystem(udev_device), fu_udev_device_get_devtype(udev_device)); } else { g_string_append_printf(str, "%s,", fu_udev_device_get_subsystem(udev_device)); } parent = FU_UDEV_DEVICE( fu_device_get_backend_parent_with_subsystem(FU_DEVICE(udev_device), NULL, NULL)); if (parent == NULL) break; g_set_object(&udev_device, parent); } if (str->len > 0) g_string_truncate(str, str->len - 1); return g_string_free(g_steal_pointer(&str), FALSE); } /* private */ gboolean fu_udev_device_match_subsystem(FuUdevDevice *self, const gchar *subsystem) { g_auto(GStrv) subsys_devtype = NULL; g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), FALSE); if (subsystem == NULL) return TRUE; subsys_devtype = g_strsplit(subsystem, ":", 2); if (g_strcmp0(fu_udev_device_get_subsystem(self), subsys_devtype[0]) != 0) return FALSE; if (subsys_devtype[1] != NULL && g_strcmp0(fu_udev_device_get_devtype(self), subsys_devtype[1]) != 0) { return FALSE; } return TRUE; } /* private */ gchar * fu_udev_device_get_device_file_from_subsystem(FuUdevDevice *self, const gchar *subsystem, GError **error) { const gchar *fn; g_autofree gchar *subsystem_dir = NULL; g_autoptr(GDir) dir = NULL; g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), NULL); g_return_val_if_fail(subsystem != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); subsystem_dir = g_build_filename(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(self)), subsystem, NULL); dir = g_dir_open(subsystem_dir, 0, &error_local); if (dir == NULL) { if (g_error_matches(error_local, G_FILE_ERROR_NOENT, G_FILE_ERROR_NOENT)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "failed to find subsystem directory %s", subsystem_dir); return NULL; } g_propagate_error(error, g_steal_pointer(&error_local)); fu_error_convert(error); return NULL; } fn = g_dir_read_name(dir); if (fn == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "failed to find subsystem device in %s", subsystem_dir); return NULL; } return g_strdup_printf("/dev/%s", fn); } /** * fu_udev_device_get_devpath: * @self: a #FuUdevDevice * * Sets the physical ID from the sysfs path, with the `/sys` prefixed removed. * * Returns: The udev-compatble device path, or %NULL on error. * * Since: 2.0.1 **/ gchar * fu_udev_device_get_devpath(FuUdevDevice *self) { gchar *full_devpath; if (fu_udev_device_get_sysfs_path(self) == NULL) return NULL; full_devpath = g_strrstr(fu_udev_device_get_sysfs_path(self), "/sys"); if (full_devpath == NULL) return NULL; return g_strdup(full_devpath + 4); } /** * fu_udev_device_set_physical_id: * @self: a #FuUdevDevice * @subsystems: a subsystem string, e.g. `pci,usb,scsi:scsi_target` * @error: (nullable): optional return location for an error * * Sets the physical ID from the device subsystem. Plugins should choose the * subsystem that is "deepest" in the udev tree, for instance choosing `usb` * over `pci` for a mouse device. * * The devtype can also be specified for a specific device, which is useful when the * subsystem alone is not enough to identify the physical device. e.g. ignoring the * specific LUNs for a SCSI device. * * Returns: %TRUE if the physical device was set. * * Since: 1.1.2 **/ gboolean fu_udev_device_set_physical_id(FuUdevDevice *self, const gchar *subsystems, GError **error) { const gchar *subsystem = NULL; g_autofree gchar *physical_id = NULL; g_auto(GStrv) split = NULL; g_autoptr(FuUdevDevice) udev_device = NULL; g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), FALSE); g_return_val_if_fail(subsystems != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* look for each subsystem[:devtype] in turn */ split = g_strsplit(subsystems, ",", -1); for (guint i = 0; split[i] != NULL; i++) { g_autoptr(FuUdevDevice) device_parent = NULL; /* do we match */ if (fu_udev_device_match_subsystem(self, split[i])) { udev_device = g_object_ref(self); break; } /* does a parent match? */ device_parent = FU_UDEV_DEVICE( fu_device_get_backend_parent_with_subsystem(FU_DEVICE(self), split[i], NULL)); if (device_parent != NULL) { udev_device = g_object_ref(device_parent); break; } } if (udev_device == NULL) { g_autofree gchar *str = fu_udev_device_get_parent_subsystems(self); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "failed to find device with subsystems %s, only got %s", subsystems, str); return FALSE; } subsystem = fu_udev_device_get_subsystem(udev_device); if (subsystem == NULL && fu_device_get_physical_id(FU_DEVICE(udev_device)) != NULL) { fu_device_set_physical_id(FU_DEVICE(self), fu_device_get_physical_id(FU_DEVICE(udev_device))); return TRUE; } if (g_strcmp0(subsystem, "pci") == 0) { g_autofree gchar *prop_id = fu_udev_device_read_property(udev_device, "PCI_SLOT_NAME", error); if (prop_id == NULL) return FALSE; physical_id = g_strdup_printf("PCI_SLOT_NAME=%s", prop_id); } else if (g_strcmp0(subsystem, "usb") == 0 || g_strcmp0(subsystem, "mmc") == 0 || g_strcmp0(subsystem, "i2c") == 0 || g_strcmp0(subsystem, "platform") == 0 || g_strcmp0(subsystem, "mtd") == 0 || g_strcmp0(subsystem, "block") == 0 || g_strcmp0(subsystem, "gpio") == 0 || g_strcmp0(subsystem, "video4linux") == 0) { g_auto(GStrv) sysfs_parts = g_strsplit(fu_udev_device_get_sysfs_path(udev_device), "/sys", 2); if (sysfs_parts[1] != NULL) physical_id = g_strdup_printf("DEVPATH=%s", sysfs_parts[1]); } else if (g_strcmp0(subsystem, "hid") == 0) { g_autofree gchar *prop_id = fu_udev_device_read_property(udev_device, "HID_PHYS", error); if (prop_id == NULL) return FALSE; physical_id = g_strdup_printf("HID_PHYS=%s", prop_id); } else if (g_strcmp0(subsystem, "drm_dp_aux_dev") == 0) { g_autofree gchar *prop_id = fu_udev_device_read_property(udev_device, "DEVNAME", error); if (prop_id == NULL) return FALSE; physical_id = g_strdup_printf("DEVNAME=%s", prop_id); } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot handle subsystem %s", subsystem); return FALSE; } /* success */ fu_device_set_physical_id(FU_DEVICE(self), physical_id); return TRUE; } /** * fu_udev_device_get_io_channel: * @self: a #FuUdevDevice * * Gets the IO channel. * * Returns: (transfer none): a #FuIOChannel, or %NULL if the device is not open * * Since: 1.9.8 **/ FuIOChannel * fu_udev_device_get_io_channel(FuUdevDevice *self) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), NULL); return priv->io_channel; } /** * fu_udev_device_set_io_channel: * @self: a #FuUdevDevice * @io_channel: a #FuIOChannel * * Replace the IO channel to use when the device has already been opened. * This object will automatically unref @io_channel when fu_device_close() is called. * * Since: 1.9.8 **/ void fu_udev_device_set_io_channel(FuUdevDevice *self, FuIOChannel *io_channel) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_UDEV_DEVICE(self)); g_return_if_fail(FU_IS_IO_CHANNEL(io_channel)); g_set_object(&priv->io_channel, io_channel); } /** * fu_udev_device_remove_open_flag: * @self: a #FuUdevDevice * @flag: udev device flag, e.g. %FU_IO_CHANNEL_OPEN_FLAG_READ * * Removes a open flag. * * Since: 2.0.0 **/ void fu_udev_device_remove_open_flag(FuUdevDevice *self, FuIoChannelOpenFlag flag) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_UDEV_DEVICE(self)); priv->open_flags &= ~flag; } /** * fu_udev_device_add_open_flag: * @self: a #FuUdevDevice * @flag: udev device flag, e.g. %FU_IO_CHANNEL_OPEN_FLAG_READ * * Sets the parameters to use when opening the device. * * For example %FU_IO_CHANNEL_OPEN_FLAG_READ means that fu_device_open() * would use `O_RDONLY` rather than `O_RDWR` which is the default. * * Since: 2.0.0 **/ void fu_udev_device_add_open_flag(FuUdevDevice *self, FuIoChannelOpenFlag flag) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_UDEV_DEVICE(self)); /* already set */ if (priv->open_flags & flag) return; priv->open_flags |= flag; } static gboolean fu_udev_device_open(FuDevice *device, GError **error) { FuUdevDevice *self = FU_UDEV_DEVICE(device); FuUdevDevicePrivate *priv = GET_PRIVATE(self); /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) return TRUE; /* old versions of fwupd used to start with OPEN_READ|OPEN_WRITE and then plugins * could add more flags, or set the flags back to NONE -- detect and fixup */ if (priv->device_file != NULL && priv->open_flags == FU_IO_CHANNEL_OPEN_FLAG_NONE) { #ifndef SUPPORTED_BUILD g_critical("%s [%s] forgot to call fu_udev_device_add_open_flag() with " "FU_IO_CHANNEL_OPEN_FLAG_READ and/or FU_IO_CHANNEL_OPEN_FLAG_WRITE", fu_device_get_name(device), fu_device_get_id(device)); #endif fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); } /* open device */ if (priv->device_file != NULL) { g_autoptr(FuIOChannel) io_channel = NULL; io_channel = fu_io_channel_new_file(priv->device_file, priv->open_flags, error); if (io_channel == NULL) return FALSE; g_set_object(&priv->io_channel, io_channel); } /* success */ return TRUE; } static gboolean fu_udev_device_rescan(FuDevice *device, GError **error) { if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) return TRUE; fu_device_probe_invalidate(device); return fu_device_probe(device, error); } static gboolean fu_udev_device_close(FuDevice *device, GError **error) { FuUdevDevice *self = FU_UDEV_DEVICE(device); FuUdevDevicePrivate *priv = GET_PRIVATE(self); /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) return TRUE; /* optional */ if (priv->io_channel != NULL) { if (!fu_io_channel_shutdown(priv->io_channel, error)) return FALSE; } /* success */ return TRUE; } /** * fu_udev_device_reopen: * @self: a #FuDevice * @error: (nullable): optional return location for an error * * Closes and opens the device, typically used to close() and open() the device-file which is * required by some ioctls. * * Returns: %TRUE for success * * Since: 2.0.9 **/ gboolean fu_udev_device_reopen(FuUdevDevice *self, GError **error) { g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_udev_device_close(FU_DEVICE(self), error)) return FALSE; return fu_udev_device_open(FU_DEVICE(self), error); } /** * fu_udev_device_ioctl_new: * @self: a #FuUdevDevice * * Build a helper to control a device using a low-level request. * * Returns: (transfer full): a #FuIoctl, or %NULL on error * * Since: 2.0.2 **/ FuIoctl * fu_udev_device_ioctl_new(FuUdevDevice *self) { g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), NULL); return fu_ioctl_new(self); } /* private */ gboolean fu_udev_device_ioctl(FuUdevDevice *self, gulong request, guint8 *buf, gsize bufsz, gint *rc, guint timeout, FuIoctlFlags flags, GError **error) { #ifdef HAVE_IOCTL_H FuUdevDevicePrivate *priv = GET_PRIVATE(self); gint rc_tmp; g_autoptr(GTimer) timer = g_timer_new(); g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), FALSE); g_return_val_if_fail(request != 0x0, FALSE); g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* not open! */ if (priv->io_channel == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "%s [%s] has not been opened", fu_device_get_id(FU_DEVICE(self)), fu_device_get_name(FU_DEVICE(self))); return FALSE; } /* poll if required up to the timeout */ do { rc_tmp = ioctl(fu_io_channel_unix_get_fd(priv->io_channel), /* nocheck:blocked */ request, buf); if (rc_tmp >= 0) break; } while ((flags & FU_IOCTL_FLAG_RETRY) && (errno == EINTR || errno == EAGAIN) && g_timer_elapsed(timer, NULL) < timeout * 1000.f); if (rc != NULL) *rc = rc_tmp; if (rc_tmp < 0) { #ifdef HAVE_ERRNO_H if (errno == EPERM) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_PERMISSION_DENIED, "permission denied"); return FALSE; } if (errno == ENOTTY) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "permission denied"); return FALSE; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "ioctl error: %s [%i]", g_strerror(errno), errno); #else g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unspecified ioctl error"); #endif return FALSE; } /* success */ return TRUE; #else g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Not supported as not found"); return FALSE; #endif } /** * fu_udev_device_pread: * @self: a #FuUdevDevice * @port: offset address * @buf: (in): data * @bufsz: size of @buf * @error: (nullable): optional return location for an error * * Read a buffer from a file descriptor at a given offset. * * Returns: %TRUE for success * * Since: 1.8.2 **/ gboolean fu_udev_device_pread(FuUdevDevice *self, goffset port, guint8 *buf, gsize bufsz, GError **error) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); FuDeviceEvent *event = NULL; g_autofree gchar *event_id = NULL; g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), FALSE); g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_strdup_printf("Pread:" "Port=0x%x," "Length=0x%x", (guint)port, (guint)bufsz); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return FALSE; return fu_device_event_copy_data(event, "Data", buf, bufsz, NULL, error); } /* save */ if (event_id != NULL) event = fu_device_save_event(FU_DEVICE(self), event_id); /* not open! */ if (priv->io_channel == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "%s [%s] has not been opened", fu_device_get_id(FU_DEVICE(self)), fu_device_get_name(FU_DEVICE(self))); return FALSE; } #ifdef HAVE_PWRITE if (pread(fu_io_channel_unix_get_fd(priv->io_channel), buf, bufsz, port) != (gssize)bufsz) { g_set_error(error, G_IO_ERROR, /* nocheck:error */ #ifdef HAVE_ERRNO_H g_io_error_from_errno(errno), #else G_IO_ERROR_FAILED, /* nocheck:error */ #endif "failed to read from port 0x%04x: %s", (guint)port, g_strerror(errno)); fwupd_error_convert(error); return FALSE; } /* save response */ if (event != NULL) fu_device_event_set_data(event, "Data", buf, bufsz); return TRUE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Not supported as pread() is unavailable"); return FALSE; #endif } /** * fu_udev_device_seek: * @self: a #FuUdevDevice * @offset: offset address * @error: (nullable): optional return location for an error * * Seeks a file descriptor to a given offset. * * Returns: %TRUE for success * * Since: 1.7.2 **/ gboolean fu_udev_device_seek(FuUdevDevice *self, goffset offset, GError **error) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) return TRUE; /* not open! */ if (priv->io_channel == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "%s [%s] has not been opened", fu_device_get_id(FU_DEVICE(self)), fu_device_get_name(FU_DEVICE(self))); return FALSE; } return fu_io_channel_seek(priv->io_channel, offset, error); } /** * fu_udev_device_pwrite: * @self: a #FuUdevDevice * @port: offset address * @buf: (out): data * @bufsz: size of @data * @error: (nullable): optional return location for an error * * Write a buffer to a file descriptor at a given offset. * * Returns: %TRUE for success * * Since: 1.8.2 **/ gboolean fu_udev_device_pwrite(FuUdevDevice *self, goffset port, const guint8 *buf, gsize bufsz, GError **error) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) return TRUE; /* not open! */ if (priv->io_channel == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "%s [%s] has not been opened", fu_device_get_id(FU_DEVICE(self)), fu_device_get_name(FU_DEVICE(self))); return FALSE; } #ifdef HAVE_PWRITE if (pwrite(fu_io_channel_unix_get_fd(priv->io_channel), buf, bufsz, port) != (gssize)bufsz) { g_set_error(error, G_IO_ERROR, /* nocheck:error */ #ifdef HAVE_ERRNO_H g_io_error_from_errno(errno), #else G_IO_ERROR_FAILED, /* nocheck:blocked */ #endif "failed to write to port %04x: %s", (guint)port, g_strerror(errno)); fwupd_error_convert(error); return FALSE; } return TRUE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Not supported as pwrite() is unavailable"); return FALSE; #endif } /** * fu_udev_device_read: * @self: a #FuUdevDevice * @buf: (in): data * @bufsz: size of @buf * @bytes_read: (out) (nullable): data written to @buf * @timeout_ms: timeout in ms * @flags: channel flags, e.g. %FU_IO_CHANNEL_FLAG_SINGLE_SHOT * @error: (nullable): optional return location for an error * * Read a buffer from a file descriptor. * * Returns: %TRUE for success * * Since: 2.0.4 **/ gboolean fu_udev_device_read(FuUdevDevice *self, guint8 *buf, gsize bufsz, gsize *bytes_read, guint timeout_ms, FuIOChannelFlags flags, GError **error) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); FuDeviceEvent *event = NULL; gsize buflen_tmp = 0; g_autofree gchar *event_id = NULL; g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), FALSE); g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_strdup_printf("Read:Length=0x%x", (guint)bufsz); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return FALSE; if (!fu_device_event_check_error(event, error)) return FALSE; return fu_device_event_copy_data(event, "Data", buf, bufsz, bytes_read, error); } /* save */ if (event_id != NULL) event = fu_device_save_event(FU_DEVICE(self), event_id); /* not open! */ if (priv->io_channel == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "%s [%s] has not been opened", fu_device_get_id(FU_DEVICE(self)), fu_device_get_name(FU_DEVICE(self))); return FALSE; } if (!fu_io_channel_read_raw(priv->io_channel, buf, bufsz, &buflen_tmp, timeout_ms, flags, &error_local)) { if (event != NULL) fu_device_event_set_error(event, error_local); g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } if (bytes_read != NULL) *bytes_read = buflen_tmp; /* save response */ if (event != NULL) fu_device_event_set_data(event, "Data", buf, buflen_tmp); return TRUE; } /** * fu_udev_device_read_bytes: * @self: a #FuUdevDevice * @count: bytes to read * @timeout_ms: timeout in ms * @flags: channel flags, e.g. %FU_IO_CHANNEL_FLAG_SINGLE_SHOT * @error: (nullable): optional return location for an error * * Read a buffer from a file descriptor. * * Returns: (transfer full): A #GBytes, or %NULL * * Since: 2.0.7 **/ GBytes * fu_udev_device_read_bytes(FuUdevDevice *self, gsize count, guint timeout_ms, FuIOChannelFlags flags, GError **error) { gsize bytes_read = 0; g_autofree guint8 *buf = NULL; g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), NULL); g_return_val_if_fail(count > 0, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); buf = g_malloc0(count); if (!fu_udev_device_read(self, buf, count, &bytes_read, timeout_ms, flags, error)) return NULL; return g_bytes_new(buf, bytes_read); } /** * fu_udev_device_write: * @self: a #FuUdevDevice * @buf: (out): data * @bufsz: size of @data * @timeout_ms: timeout in ms * @flags: channel flags, e.g. %FU_IO_CHANNEL_FLAG_SINGLE_SHOT * @error: (nullable): optional return location for an error * * Write a buffer to a file descriptor. * * Returns: %TRUE for success * * Since: 2.0.4 **/ gboolean fu_udev_device_write(FuUdevDevice *self, const guint8 *buf, gsize bufsz, guint timeout_ms, FuIOChannelFlags flags, GError **error) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); FuDeviceEvent *event = NULL; g_autofree gchar *event_id = NULL; g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), FALSE); g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { g_autofree gchar *data_base64 = g_base64_encode(buf, bufsz); event_id = g_strdup_printf("Write:Data=%s,Length=0x%x", data_base64, (guint)bufsz); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return FALSE; return event != NULL; } /* save */ if (event_id != NULL) event = fu_device_save_event(FU_DEVICE(self), event_id); /* not open! */ if (priv->io_channel == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "%s [%s] has not been opened", fu_device_get_id(FU_DEVICE(self)), fu_device_get_name(FU_DEVICE(self))); return FALSE; } if (!fu_io_channel_write_raw(priv->io_channel, buf, bufsz, timeout_ms, flags, error)) return FALSE; /* success */ return TRUE; } /** * fu_udev_device_write_bytes: * @self: a #FuUdevDevice * @blob: a #GBytes * @timeout_ms: timeout in ms * @flags: channel flags, e.g. %FU_IO_CHANNEL_FLAG_SINGLE_SHOT * @error: (nullable): optional return location for an error * * Write a buffer to a file descriptor. * * Returns: %TRUE for success * * Since: 2.0.7 **/ gboolean fu_udev_device_write_bytes(FuUdevDevice *self, GBytes *blob, guint timeout_ms, FuIOChannelFlags flags, GError **error) { g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), FALSE); g_return_val_if_fail(blob != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return fu_udev_device_write(self, g_bytes_get_data(blob, NULL), g_bytes_get_size(blob), timeout_ms, flags, error); } /** * fu_udev_device_list_sysfs: * @self: a #FuUdevDevice * @error: (nullable): optional return location for an error * * Lists all the sysfs attributes. * * Returns: (transfer container) (element-type utf8): basenames, or %NULL * * Since: 2.0.9 **/ GPtrArray * fu_udev_device_list_sysfs(FuUdevDevice *self, GError **error) { FuDeviceEvent *event = NULL; const gchar *basename; g_autofree gchar *event_id = NULL; g_autoptr(GDir) dir = NULL; g_autoptr(GPtrArray) attrs = g_ptr_array_new_with_free_func(g_free); g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* need event ID */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_strdup("ListAttr"); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { const gchar *value; g_auto(GStrv) attrs_strv = NULL; event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return NULL; value = fu_device_event_get_str(event, "Data", error); if (value == NULL) return NULL; attrs_strv = g_strsplit(value, "\n", -1); for (guint i = 0; attrs_strv[i] != NULL; i++) g_ptr_array_add(attrs, g_strdup(attrs_strv[i])); return g_steal_pointer(&attrs); } /* save */ if (event_id != NULL) event = fu_device_save_event(FU_DEVICE(self), event_id); /* list the files and directories */ if (fu_udev_device_get_sysfs_path(self) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "sysfs_path undefined"); return NULL; } dir = g_dir_open(fu_udev_device_get_sysfs_path(self), 0, error); if (dir == NULL) { fwupd_error_convert(error); return NULL; } while ((basename = g_dir_read_name(dir)) != NULL) g_ptr_array_add(attrs, g_strdup(basename)); /* save for emulation */ if (event != NULL) { g_autofree gchar *value = fu_strjoin("\n", attrs); fu_device_event_set_str(event, "Data", value); } /* success */ return g_steal_pointer(&attrs); } /** * fu_udev_device_read_sysfs: * @self: a #FuUdevDevice * @attr: sysfs attribute name * @timeout_ms: IO timeout in milliseconds * @error: (nullable): optional return location for an error * * Reads data from a sysfs attribute, removing any newline trailing chars. * * Returns: (transfer full): string value, or %NULL * * Since: 2.0.0 **/ gchar * fu_udev_device_read_sysfs(FuUdevDevice *self, const gchar *attr, guint timeout_ms, GError **error) { FuDeviceEvent *event = NULL; g_autofree gchar *event_id = NULL; g_autofree gchar *path = NULL; g_autofree gchar *value = NULL; g_autoptr(FuIOChannel) io_channel = NULL; g_autoptr(GByteArray) buf = NULL; g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), NULL); g_return_val_if_fail(attr != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* need event ID */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_strdup_printf("ReadAttr:Attr=%s", attr); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return NULL; return g_strdup(fu_device_event_get_str(event, "Data", error)); } /* save */ if (event_id != NULL) event = fu_device_save_event(FU_DEVICE(self), event_id); /* open the file */ if (fu_udev_device_get_sysfs_path(self) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "sysfs_path undefined"); return NULL; } path = g_build_filename(fu_udev_device_get_sysfs_path(self), attr, NULL); io_channel = fu_io_channel_new_file(path, FU_IO_CHANNEL_OPEN_FLAG_READ, error); if (io_channel == NULL) return NULL; buf = fu_io_channel_read_byte_array(io_channel, -1, timeout_ms, FU_IO_CHANNEL_FLAG_NONE, error); if (buf == NULL) return NULL; if (!g_utf8_validate((const gchar *)buf->data, buf->len, NULL)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "non UTF-8 data"); return NULL; } /* save response */ value = g_strndup((const gchar *)buf->data, buf->len); /* remove the trailing newline */ if (buf->len > 0) { if (value[buf->len - 1] == '\n') value[buf->len - 1] = '\0'; } /* save for emulation */ if (event != NULL) fu_device_event_set_str(event, "Data", value); /* success */ return g_steal_pointer(&value); } /** * fu_udev_device_read_sysfs_bytes: * @self: a #FuUdevDevice * @attr: sysfs attribute name * @count: maximum bytes to read, or -1 for no limit * @timeout_ms: IO timeout in milliseconds * @error: (nullable): optional return location for an error * * Reads raw data from a sysfs attribute. * * Returns: (transfer full): string value, or %NULL * * Since: 2.0.1 **/ GBytes * fu_udev_device_read_sysfs_bytes(FuUdevDevice *self, const gchar *attr, gssize count, guint timeout_ms, GError **error) { FuDeviceEvent *event = NULL; g_autofree gchar *event_id = NULL; g_autofree gchar *path = NULL; g_autoptr(FuIOChannel) io_channel = NULL; g_autoptr(GBytes) blob = NULL; g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), NULL); g_return_val_if_fail(attr != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* need event ID */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_strdup_printf("ReadAttr:Attr=%s", attr); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return NULL; return fu_device_event_get_bytes(event, "Data", error); } /* save */ if (event_id != NULL) event = fu_device_save_event(FU_DEVICE(self), event_id); /* open the file */ if (fu_udev_device_get_sysfs_path(self) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "sysfs_path undefined"); return NULL; } path = g_build_filename(fu_udev_device_get_sysfs_path(self), attr, NULL); io_channel = fu_io_channel_new_file(path, FU_IO_CHANNEL_OPEN_FLAG_READ, error); if (io_channel == NULL) return NULL; blob = fu_io_channel_read_bytes(io_channel, count, timeout_ms, FU_IO_CHANNEL_FLAG_NONE, error); if (blob == NULL) return NULL; /* save for emulation */ if (event != NULL) fu_device_event_set_bytes(event, "Data", blob); /* success */ return g_steal_pointer(&blob); } /** * fu_udev_device_write_sysfs: * @self: a #FuUdevDevice * @attr: sysfs attribute name * @val: data to write into the attribute * @timeout_ms: IO timeout in milliseconds * @error: (nullable): optional return location for an error * * Writes data into a sysfs attribute * * Returns: %TRUE for success * * Since: 2.0.0 **/ gboolean fu_udev_device_write_sysfs(FuUdevDevice *self, const gchar *attr, const gchar *val, guint timeout_ms, GError **error) { FuDeviceEvent *event = NULL; g_autofree gchar *event_id = NULL; g_autofree gchar *path = NULL; g_autoptr(FuIOChannel) io_channel = NULL; g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), FALSE); g_return_val_if_fail(attr != NULL, FALSE); g_return_val_if_fail(val != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* need event ID */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_strdup_printf("WriteAttr:Attr=%s,Data=%s", attr, val); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { event = fu_device_load_event(FU_DEVICE(self), event_id, error); return event != NULL; } /* open the file */ if (fu_udev_device_get_sysfs_path(self) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "sysfs_path undefined"); return FALSE; } path = g_build_filename(fu_udev_device_get_sysfs_path(self), attr, NULL); io_channel = fu_io_channel_new_file(path, FU_IO_CHANNEL_OPEN_FLAG_WRITE, error); if (io_channel == NULL) return FALSE; /* save */ if (event_id != NULL) event = fu_device_save_event(FU_DEVICE(self), event_id); return fu_io_channel_write_raw(io_channel, (const guint8 *)val, strlen(val), timeout_ms, FU_IO_CHANNEL_FLAG_NONE, error); } /** * fu_udev_device_write_sysfs_byte_array: * @self: a #FuUdevDevice * @attr: sysfs attribute name * @buf: data to write into the attribute * @timeout_ms: IO timeout in milliseconds * @error: (nullable): optional return location for an error * * Writes raw data into a sysfs attribute. * * Returns: %TRUE for success * * Since: 2.0.1 **/ gboolean fu_udev_device_write_sysfs_byte_array(FuUdevDevice *self, const gchar *attr, GByteArray *buf, guint timeout_ms, GError **error) { FuDeviceEvent *event = NULL; g_autofree gchar *event_id = NULL; g_autofree gchar *path = NULL; g_autoptr(FuIOChannel) io_channel = NULL; g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), FALSE); g_return_val_if_fail(attr != NULL, FALSE); g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* need event ID */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { g_autofree gchar *buf_base64 = g_base64_encode(buf->data, buf->len); event_id = g_strdup_printf("WriteAttr:Attr=%s,Data=%s", attr, buf_base64); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { event = fu_device_load_event(FU_DEVICE(self), event_id, error); return event != NULL; } /* open the file */ if (fu_udev_device_get_sysfs_path(self) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "sysfs_path undefined"); return FALSE; } path = g_build_filename(fu_udev_device_get_sysfs_path(self), attr, NULL); io_channel = fu_io_channel_new_file(path, FU_IO_CHANNEL_OPEN_FLAG_WRITE, error); if (io_channel == NULL) return FALSE; /* save */ if (event_id != NULL) event = fu_device_save_event(FU_DEVICE(self), event_id); return fu_io_channel_write_byte_array(io_channel, buf, timeout_ms, FU_IO_CHANNEL_FLAG_NONE, error); } /** * fu_udev_device_write_sysfs_bytes: * @self: a #FuUdevDevice * @attr: sysfs attribute name * @blob: data to write into the attribute * @timeout_ms: IO timeout in milliseconds * @error: (nullable): optional return location for an error * * Writes raw data into a sysfs attribute. * * Returns: %TRUE for success * * Since: 2.0.1 **/ gboolean fu_udev_device_write_sysfs_bytes(FuUdevDevice *self, const gchar *attr, GBytes *blob, guint timeout_ms, GError **error) { FuDeviceEvent *event = NULL; g_autofree gchar *event_id = NULL; g_autofree gchar *path = NULL; g_autoptr(FuIOChannel) io_channel = NULL; g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), FALSE); g_return_val_if_fail(attr != NULL, FALSE); g_return_val_if_fail(blob != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* need event ID */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { g_autofree gchar *buf_base64 = g_base64_encode(g_bytes_get_data(blob, NULL), g_bytes_get_size(blob)); event_id = g_strdup_printf("WriteAttr:Attr=%s,Data=%s", attr, buf_base64); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { event = fu_device_load_event(FU_DEVICE(self), event_id, error); return event != NULL; } /* open the file */ if (fu_udev_device_get_sysfs_path(self) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "sysfs_path undefined"); return FALSE; } path = g_build_filename(fu_udev_device_get_sysfs_path(self), attr, NULL); io_channel = fu_io_channel_new_file(path, FU_IO_CHANNEL_OPEN_FLAG_WRITE, error); if (io_channel == NULL) return FALSE; /* save */ if (event_id != NULL) event = fu_device_save_event(FU_DEVICE(self), event_id); return fu_io_channel_write_bytes(io_channel, blob, timeout_ms, FU_IO_CHANNEL_FLAG_NONE, error); } /** * fu_udev_device_get_devtype: * @self: a #FuUdevDevice * * Returns the Udev device type * * Returns: device type specified in the uevent * * Since: 1.4.5 **/ const gchar * fu_udev_device_get_devtype(FuUdevDevice *self) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), NULL); return priv->devtype; } /** * fu_udev_device_get_subsystem_devtype: * @self: a #FuUdevDevice * * Returns the Udev subsystem and device type, as a string. * * Returns: (transfer full): `subsystem:devtype`, or just `subsystem` if the latter is unset * * Since: 2.0.2 **/ gchar * fu_udev_device_get_subsystem_devtype(FuUdevDevice *self) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), NULL); if (priv->devtype != NULL) return g_strdup_printf("%s:%s", priv->subsystem, priv->devtype); return g_strdup(priv->subsystem); } /* private */ void fu_udev_device_add_property(FuUdevDevice *self, const gchar *key, const gchar *value) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_UDEV_DEVICE(self)); g_return_if_fail(key != NULL); /* this are explicit properties too */ if (g_strcmp0(key, "DEVNAME") == 0) fu_udev_device_set_device_file(self, value); if (g_strcmp0(key, "DEVTYPE") == 0) fu_udev_device_set_devtype(self, value); g_hash_table_insert(priv->properties, g_strdup(key), g_strdup(value)); } /** * fu_udev_device_read_property: * @self: a #FuUdevDevice * @key: uevent key name, e.g. `HID_PHYS` * @error: (nullable): optional return location for an error * * Gets a value from the `uevent` file. * * Returns: %TRUE for success * * Since: 2.0.0 **/ gchar * fu_udev_device_read_property(FuUdevDevice *self, const gchar *key, GError **error) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); FuDeviceEvent *event = NULL; g_autofree gchar *event_id = NULL; g_autofree gchar *value = NULL; g_return_val_if_fail(FU_IS_UDEV_DEVICE(self), NULL); g_return_val_if_fail(key != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* need event ID */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_strdup_printf("ReadProp:Key=%s", key); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return NULL; return g_strdup(fu_device_event_get_str(event, "Data", error)); } /* save */ if (event_id != NULL) event = fu_device_save_event(FU_DEVICE(self), event_id); /* parse key */ if (!priv->properties_valid) { g_autofree gchar *str = NULL; g_auto(GStrv) uevent_lines = NULL; str = fu_udev_device_read_sysfs(self, "uevent", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, error); if (str == NULL) return NULL; uevent_lines = g_strsplit(str, "\n", -1); for (guint i = 0; uevent_lines[i] != NULL; i++) { /* only split KEY=VALUE */ if (g_strstr_len(uevent_lines[i], -1, "=") != NULL) { g_autofree gchar **kvs = g_strsplit(uevent_lines[i], "=", 2); g_hash_table_insert(priv->properties, g_steal_pointer(&kvs[0]), g_steal_pointer(&kvs[1])); } } priv->properties_valid = TRUE; } value = g_strdup(g_hash_table_lookup(priv->properties, key)); if (value == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "property key %s was not found", key); return NULL; } /* save response */ if (event != NULL) fu_device_event_set_str(event, "Data", value); /* success */ return g_steal_pointer(&value); } static void fu_udev_device_add_json(FuDevice *device, JsonBuilder *builder, FwupdCodecFlags flags) { FuUdevDevice *self = FU_UDEV_DEVICE(device); FuUdevDevicePrivate *priv = GET_PRIVATE(self); GPtrArray *events = fu_device_get_events(device); /* optional properties */ fwupd_codec_json_append(builder, "GType", "FuUdevDevice"); if (fu_udev_device_get_sysfs_path(self) != NULL) fwupd_codec_json_append(builder, "BackendId", fu_udev_device_get_sysfs_path(self)); if (priv->device_file != NULL) fwupd_codec_json_append(builder, "DeviceFile", priv->device_file); if (priv->subsystem != NULL) fwupd_codec_json_append(builder, "Subsystem", priv->subsystem); if (priv->devtype != NULL) fwupd_codec_json_append(builder, "Devtype", priv->devtype); if (priv->driver != NULL) fwupd_codec_json_append(builder, "Driver", priv->driver); if (priv->bind_id != NULL) fwupd_codec_json_append(builder, "BindId", priv->bind_id); if (fu_device_get_vid(device) != 0) fwupd_codec_json_append_int(builder, "Vendor", fu_device_get_vid(device)); if (fu_device_get_pid(device) != 0) fwupd_codec_json_append_int(builder, "Model", fu_device_get_pid(device)); /* events */ if (events->len > 0) { json_builder_set_member_name(builder, "Events"); json_builder_begin_array(builder); for (guint i = 0; i < events->len; i++) { FuDeviceEvent *event = g_ptr_array_index(events, i); json_builder_begin_object(builder); fwupd_codec_to_json(FWUPD_CODEC(event), builder, events->len > 1000 ? flags | FWUPD_CODEC_FLAG_COMPRESSED : flags); json_builder_end_object(builder); } json_builder_end_array(builder); } } static gboolean fu_udev_device_from_json(FuDevice *device, JsonObject *json_object, GError **error) { FuUdevDevice *self = FU_UDEV_DEVICE(device); const gchar *tmp; gint64 tmp64; tmp = json_object_get_string_member_with_default(json_object, "BackendId", NULL); if (tmp != NULL) fu_device_set_backend_id(device, tmp); tmp = json_object_get_string_member_with_default(json_object, "Subsystem", NULL); if (tmp != NULL) fu_udev_device_set_subsystem(self, tmp); tmp = json_object_get_string_member_with_default(json_object, "Devtype", NULL); if (tmp != NULL) fu_udev_device_set_devtype(self, tmp); tmp = json_object_get_string_member_with_default(json_object, "Driver", NULL); if (tmp != NULL) fu_udev_device_set_driver(self, tmp); tmp = json_object_get_string_member_with_default(json_object, "BindId", NULL); if (tmp != NULL) fu_udev_device_set_bind_id(self, tmp); tmp = json_object_get_string_member_with_default(json_object, "DeviceFile", NULL); if (tmp != NULL) fu_udev_device_set_device_file(self, tmp); tmp64 = json_object_get_int_member_with_default(json_object, "Vendor", 0); if (tmp64 != 0) fu_device_set_vid(device, tmp64); tmp64 = json_object_get_int_member_with_default(json_object, "Model", 0); if (tmp64 != 0) fu_device_set_pid(device, tmp64); /* array of events */ if (json_object_has_member(json_object, "Events")) { JsonArray *json_array = json_object_get_array_member(json_object, "Events"); for (guint i = 0; i < json_array_get_length(json_array); i++) { JsonNode *node_tmp = json_array_get_element(json_array, i); g_autoptr(FuDeviceEvent) event = fu_device_event_new(NULL); if (!fwupd_codec_from_json(FWUPD_CODEC(event), node_tmp, error)) return FALSE; fu_device_add_event(device, event); } } /* success */ return TRUE; } static void fu_udev_device_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuUdevDevice *self = FU_UDEV_DEVICE(object); FuUdevDevicePrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_SUBSYSTEM: g_value_set_string(value, priv->subsystem); break; case PROP_BIND_ID: g_value_set_string(value, priv->bind_id); break; case PROP_DRIVER: g_value_set_string(value, priv->driver); break; case PROP_DEVICE_FILE: g_value_set_string(value, priv->device_file); break; case PROP_DEVTYPE: g_value_set_string(value, priv->devtype); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_udev_device_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuUdevDevice *self = FU_UDEV_DEVICE(object); switch (prop_id) { case PROP_SUBSYSTEM: fu_udev_device_set_subsystem(self, g_value_get_string(value)); break; case PROP_BIND_ID: fu_udev_device_set_bind_id(self, g_value_get_string(value)); break; case PROP_DRIVER: fu_udev_device_set_driver(self, g_value_get_string(value)); break; case PROP_DEVICE_FILE: fu_udev_device_set_device_file(self, g_value_get_string(value)); break; case PROP_DEVTYPE: fu_udev_device_set_devtype(self, g_value_get_string(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_udev_device_finalize(GObject *object) { FuUdevDevice *self = FU_UDEV_DEVICE(object); FuUdevDevicePrivate *priv = GET_PRIVATE(self); g_hash_table_unref(priv->properties); g_free(priv->subsystem); g_free(priv->devtype); g_free(priv->bind_id); g_free(priv->driver); g_free(priv->device_file); if (priv->io_channel != NULL) g_object_unref(priv->io_channel); G_OBJECT_CLASS(fu_udev_device_parent_class)->finalize(object); } static void fu_udev_device_vid_notify_cb(FuDevice *device, GParamSpec *pspec, gpointer user_data) { fu_device_add_instance_u16(device, "VEN", fu_device_get_vid(device)); } static void fu_udev_device_pid_notify_cb(FuDevice *device, GParamSpec *pspec, gpointer user_data) { fu_device_add_instance_u16(device, "DEV", fu_device_get_pid(device)); } static void fu_udev_device_init(FuUdevDevice *self) { FuUdevDevicePrivate *priv = GET_PRIVATE(self); priv->properties = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); fu_device_set_acquiesce_delay(FU_DEVICE(self), 2500); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_EMULATION_TAG); g_signal_connect(FU_DEVICE(self), "notify::vid", G_CALLBACK(fu_udev_device_vid_notify_cb), NULL); g_signal_connect(FU_DEVICE(self), "notify::pid", G_CALLBACK(fu_udev_device_pid_notify_cb), NULL); } static void fu_udev_device_class_init(FuUdevDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); GParamSpec *pspec; object_class->finalize = fu_udev_device_finalize; object_class->get_property = fu_udev_device_get_property; object_class->set_property = fu_udev_device_set_property; device_class->probe = fu_udev_device_probe; device_class->rescan = fu_udev_device_rescan; device_class->incorporate = fu_udev_device_incorporate; device_class->invalidate = fu_udev_device_invalidate; device_class->open = fu_udev_device_open; device_class->close = fu_udev_device_close; device_class->to_string = fu_udev_device_to_string; device_class->bind_driver = fu_udev_device_bind_driver; device_class->unbind_driver = fu_udev_device_unbind_driver; device_class->probe_complete = fu_udev_device_probe_complete; device_class->from_json = fu_udev_device_from_json; device_class->add_json = fu_udev_device_add_json; /** * FuUdevDevice::changed: * @self: the #FuUdevDevice instance that emitted the signal * * The ::changed signal is emitted when the low-level GUdevDevice has changed. * * Since: 1.1.2 **/ signals[SIGNAL_CHANGED] = g_signal_new("changed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); /** * FuUdevDevice:subsystem: * * The device subsystem. * * Since: 1.1.2 */ pspec = g_param_spec_string("subsystem", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_SUBSYSTEM, pspec); /** * FuUdevDevice:bind-id: * * The bind ID to use when binding a new driver. * * Since: 1.7.2 */ pspec = g_param_spec_string("bind-id", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_BIND_ID, pspec); /** * FuUdevDevice:driver: * * The driver being used for the device. * * Since: 1.5.3 */ pspec = g_param_spec_string("driver", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_DRIVER, pspec); /** * FuUdevDevice:device-file: * * The low level file to use for device access. * * Since: 1.3.1 */ pspec = g_param_spec_string("device-file", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_DEVICE_FILE, pspec); /** * FuUdevDevice:devtype: * * The device type. * * Since: 2.0.0 */ pspec = g_param_spec_string("devtype", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_DEVTYPE, pspec); } /** * fu_udev_device_new: * @ctx: (nullable): a #FuContext * @sysfs_path: a sysfs path * * Creates a new #FuUdevDevice. * * Returns: (transfer full): a #FuUdevDevice * * Since: 2.0.0 **/ FuUdevDevice * fu_udev_device_new(FuContext *ctx, const gchar *sysfs_path) { return g_object_new(FU_TYPE_UDEV_DEVICE, "context", ctx, "backend-id", sysfs_path, NULL); } fwupd-2.0.10/libfwupdplugin/fu-udev-device.h000066400000000000000000000115601501337203100207310ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-device.h" #include "fu-io-channel.h" #include "fu-ioctl.h" #define FU_TYPE_UDEV_DEVICE (fu_udev_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuUdevDevice, fu_udev_device, FU, UDEV_DEVICE, FuDevice) struct _FuUdevDeviceClass { FuDeviceClass parent_class; }; /** * FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT: * * The default IO timeout when reading sysfs attributes. */ #define FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT 50 /* ms */ const gchar * fu_udev_device_get_device_file(FuUdevDevice *self) G_GNUC_NON_NULL(1); void fu_udev_device_set_device_file(FuUdevDevice *self, const gchar *device_file) G_GNUC_NON_NULL(1); const gchar * fu_udev_device_get_sysfs_path(FuUdevDevice *self) G_GNUC_NON_NULL(1); gchar * fu_udev_device_get_devpath(FuUdevDevice *self) G_GNUC_NON_NULL(1); const gchar * fu_udev_device_get_subsystem(FuUdevDevice *self) G_GNUC_NON_NULL(1); const gchar * fu_udev_device_get_bind_id(FuUdevDevice *self) G_GNUC_NON_NULL(1); void fu_udev_device_set_bind_id(FuUdevDevice *self, const gchar *bind_id) G_GNUC_NON_NULL(1); const gchar * fu_udev_device_get_driver(FuUdevDevice *self) G_GNUC_NON_NULL(1); guint64 fu_udev_device_get_number(FuUdevDevice *self) G_GNUC_NON_NULL(1); guint fu_udev_device_get_subsystem_depth(FuUdevDevice *self, const gchar *subsystem) G_GNUC_NON_NULL(1); gboolean fu_udev_device_set_physical_id(FuUdevDevice *self, const gchar *subsystems, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); void fu_udev_device_add_open_flag(FuUdevDevice *self, FuIoChannelOpenFlag flag) G_GNUC_NON_NULL(1); void fu_udev_device_remove_open_flag(FuUdevDevice *self, FuIoChannelOpenFlag flag) G_GNUC_NON_NULL(1); FuIOChannel * fu_udev_device_get_io_channel(FuUdevDevice *self) G_GNUC_NON_NULL(1); void fu_udev_device_set_io_channel(FuUdevDevice *self, FuIOChannel *io_channel) G_GNUC_NON_NULL(1, 2); FuIoctl * fu_udev_device_ioctl_new(FuUdevDevice *self) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_udev_device_pwrite(FuUdevDevice *self, goffset port, const guint8 *buf, gsize bufsz, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_udev_device_pread(FuUdevDevice *self, goffset port, guint8 *buf, gsize bufsz, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_udev_device_write(FuUdevDevice *self, const guint8 *buf, gsize bufsz, guint timeout_ms, FuIOChannelFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_udev_device_write_bytes(FuUdevDevice *self, GBytes *blob, guint timeout_ms, FuIOChannelFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_udev_device_read(FuUdevDevice *self, guint8 *buf, gsize bufsz, gsize *bytes_read, guint timeout_ms, FuIOChannelFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); GBytes * fu_udev_device_read_bytes(FuUdevDevice *self, gsize count, guint timeout_ms, FuIOChannelFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_udev_device_seek(FuUdevDevice *self, goffset offset, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gchar * fu_udev_device_read_property(FuUdevDevice *self, const gchar *key, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); GPtrArray * fu_udev_device_list_sysfs(FuUdevDevice *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gchar * fu_udev_device_read_sysfs(FuUdevDevice *self, const gchar *attr, guint timeout_ms, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); GBytes * fu_udev_device_read_sysfs_bytes(FuUdevDevice *self, const gchar *attr, gssize count, guint timeout_ms, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2); gboolean fu_udev_device_write_sysfs(FuUdevDevice *self, const gchar *attr, const gchar *val, guint timeout_ms, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); gboolean fu_udev_device_write_sysfs_byte_array(FuUdevDevice *self, const gchar *attr, GByteArray *buf, guint timeout_ms, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); gboolean fu_udev_device_write_sysfs_bytes(FuUdevDevice *self, const gchar *attr, GBytes *blob, guint timeout_ms, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); const gchar * fu_udev_device_get_devtype(FuUdevDevice *self) G_GNUC_NON_NULL(1); gchar * fu_udev_device_get_subsystem_devtype(FuUdevDevice *self) G_GNUC_NON_NULL(1); gboolean fu_udev_device_reopen(FuUdevDevice *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-uefi-device-private.h000066400000000000000000000011061501337203100223610ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-uefi-device.h" FuUefiDevice * fu_uefi_device_new(const gchar *guid, const gchar *name) G_GNUC_NON_NULL(1, 2); void fu_uefi_device_set_guid(FuUefiDevice *self, const gchar *guid) G_GNUC_NON_NULL(1); const gchar * fu_uefi_device_get_guid(FuUefiDevice *self) G_GNUC_NON_NULL(1); void fu_uefi_device_set_name(FuUefiDevice *self, const gchar *name) G_GNUC_NON_NULL(1); const gchar * fu_uefi_device_get_name(FuUefiDevice *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-uefi-device.c000066400000000000000000000262521501337203100207150ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuUefiDevice" #include "config.h" #include "fu-bytes.h" #include "fu-device-event-private.h" #include "fu-device-private.h" #include "fu-uefi-device-private.h" /** * FuUefiDevice: * * A device that represents a UEFI EFI variable. * * See also: [class@FuDevice] */ typedef struct { gchar *guid; gchar *name; } FuUefiDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuUefiDevice, fu_uefi_device, FU_TYPE_DEVICE); #define GET_PRIVATE(o) (fu_uefi_device_get_instance_private(o)) /* private */ void fu_uefi_device_set_guid(FuUefiDevice *self, const gchar *guid) { FuUefiDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_UEFI_DEVICE(self)); /* same */ if (g_strcmp0(priv->guid, guid) == 0) return; g_free(priv->guid); priv->guid = g_strdup(guid); if (guid != NULL) fu_device_add_instance_str(FU_DEVICE(self), "GUID", guid); } /* private */ const gchar * fu_uefi_device_get_guid(FuUefiDevice *self) { FuUefiDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UEFI_DEVICE(self), NULL); return priv->guid; } /* private */ void fu_uefi_device_set_name(FuUefiDevice *self, const gchar *name) { FuUefiDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_UEFI_DEVICE(self)); /* same */ if (g_strcmp0(priv->name, name) == 0) return; g_free(priv->name); priv->name = g_strdup(name); if (name != NULL) fu_device_add_instance_str(FU_DEVICE(self), "NAME", name); } /* private */ const gchar * fu_uefi_device_get_name(FuUefiDevice *self) { FuUefiDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UEFI_DEVICE(self), NULL); return priv->name; } /** * fu_uefi_device_set_efivar_bytes: * @self: a #FuUefiDevice * @guid: globally unique identifier * @name: variable name * @bytes: data blob * @attr: attributes * @error: (nullable): optional return location for an error * * Sets the data to a UEFI variable in NVRAM, emulating if required. * * Returns: %TRUE on success * * Since: 2.0.5 **/ gboolean fu_uefi_device_set_efivar_bytes(FuUefiDevice *self, const gchar *guid, const gchar *name, GBytes *bytes, guint32 attr, GError **error) { FuContext *ctx = fu_device_get_context(FU_DEVICE(self)); FuDeviceEvent *event = NULL; g_autofree gchar *event_id = NULL; g_return_val_if_fail(FU_IS_UEFI_DEVICE(self), FALSE); g_return_val_if_fail(guid != NULL, FALSE); g_return_val_if_fail(name != NULL, FALSE); g_return_val_if_fail(bytes != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* need event ID */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_strdup_printf("SetEfivar:Guid=%s,Name=%s,Attr=0x%x", guid, name, attr); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { g_autoptr(GBytes) bytes_tmp = NULL; event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return FALSE; bytes_tmp = fu_device_event_get_bytes(event, "Data", error); if (bytes_tmp == NULL) return FALSE; return fu_bytes_compare(bytes, bytes_tmp, error); } /* save */ if (event_id != NULL) event = fu_device_save_event(FU_DEVICE(self), event_id); /* set */ if (!fu_efivars_set_data_bytes(fu_context_get_efivars(ctx), guid, name, bytes, attr, error)) return FALSE; /* save response */ if (event != NULL) fu_device_event_set_bytes(event, "Data", bytes); /* success */ return TRUE; } /** * fu_uefi_device_get_efivar_bytes: * @self: a #FuUefiDevice * @guid: Globally unique identifier * @name: Variable name * @attr: (nullable): Attributes * @error: (nullable): optional return location for an error * * Gets the data from a UEFI variable in NVRAM, emulating if required. * * Returns: (transfer full): a #GBytes, or %NULL on error * * Since: 2.0.5 **/ GBytes * fu_uefi_device_get_efivar_bytes(FuUefiDevice *self, const gchar *guid, const gchar *name, guint32 *attr, GError **error) { FuContext *ctx = fu_device_get_context(FU_DEVICE(self)); FuDeviceEvent *event = NULL; guint32 attr_tmp = 0; g_autofree gchar *event_id = NULL; g_autoptr(GBytes) blob = NULL; g_return_val_if_fail(FU_IS_UEFI_DEVICE(self), NULL); g_return_val_if_fail(guid != NULL, NULL); g_return_val_if_fail(name != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* need event ID */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_strdup_printf("GetEfivar:Guid=%s,Name=%s", guid, name); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return NULL; if (attr != NULL) { guint64 tmp = fu_device_event_get_i64(event, "Attr", error); if (tmp == G_MAXINT64) return NULL; *attr = (guint32)tmp; } return fu_device_event_get_bytes(event, "Data", error); } /* save */ if (event_id != NULL) event = fu_device_save_event(FU_DEVICE(self), event_id); /* read */ blob = fu_efivars_get_data_bytes(fu_context_get_efivars(ctx), guid, name, &attr_tmp, error); if (blob == NULL) return NULL; if (attr != NULL) *attr = attr_tmp; /* save response */ if (event != NULL) { fu_device_event_set_bytes(event, "Data", blob); fu_device_event_set_i64(event, "Attr", attr_tmp); } /* success */ return g_steal_pointer(&blob); } static void fu_uefi_device_to_string(FuDevice *device, guint idt, GString *str) { FuUefiDevice *self = FU_UEFI_DEVICE(device); FuUefiDevicePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append(str, idt, "Guid", priv->guid); fwupd_codec_string_append(str, idt, "Name", priv->name); } static void fu_uefi_device_incorporate(FuDevice *device, FuDevice *donor) { FuUefiDevice *self = FU_UEFI_DEVICE(device); FuUefiDevice *udonor = FU_UEFI_DEVICE(donor); fu_uefi_device_set_guid(self, fu_uefi_device_get_guid(udonor)); fu_uefi_device_set_name(self, fu_uefi_device_get_name(udonor)); } static gboolean fu_uefi_device_probe(FuDevice *device, GError **error) { return fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "UEFI", "GUID", "NAME", NULL); } static GBytes * fu_uefi_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuUefiDevice *self = FU_UEFI_DEVICE(device); FuUefiDevicePrivate *priv = GET_PRIVATE(self); /* sanity check */ if (priv->guid == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "no GUID"); return NULL; } if (priv->name == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "no name"); return NULL; } return fu_uefi_device_get_efivar_bytes(FU_UEFI_DEVICE(device), priv->guid, priv->name, NULL, error); } static void fu_uefi_device_add_json(FuDevice *device, JsonBuilder *builder, FwupdCodecFlags flags) { FuUefiDevice *self = FU_UEFI_DEVICE(device); FuUefiDevicePrivate *priv = GET_PRIVATE(self); GPtrArray *events = fu_device_get_events(device); /* optional properties */ fwupd_codec_json_append(builder, "GType", "FuUefiDevice"); if (fu_device_get_backend_id(device) != NULL) fwupd_codec_json_append(builder, "BackendId", fu_device_get_backend_id(device)); if (priv->guid != NULL) fwupd_codec_json_append(builder, "Guid", priv->guid); if (priv->name != NULL) fwupd_codec_json_append(builder, "Name", priv->name); #if GLIB_CHECK_VERSION(2, 80, 0) if (fu_device_get_created_usec(device) != 0) { g_autoptr(GDateTime) dt = g_date_time_new_from_unix_utc_usec(fu_device_get_created_usec(device)); g_autofree gchar *str = g_date_time_format_iso8601(dt); json_builder_set_member_name(builder, "Created"); json_builder_add_string_value(builder, str); } #endif /* events */ if (events->len > 0) { json_builder_set_member_name(builder, "Events"); json_builder_begin_array(builder); for (guint i = 0; i < events->len; i++) { FuDeviceEvent *event = g_ptr_array_index(events, i); json_builder_begin_object(builder); fwupd_codec_to_json(FWUPD_CODEC(event), builder, flags); json_builder_end_object(builder); } json_builder_end_array(builder); } } static gboolean fu_uefi_device_from_json(FuDevice *device, JsonObject *json_object, GError **error) { FuUefiDevice *self = FU_UEFI_DEVICE(device); const gchar *tmp; tmp = json_object_get_string_member_with_default(json_object, "Guid", NULL); if (tmp != NULL) fu_uefi_device_set_guid(self, tmp); tmp = json_object_get_string_member_with_default(json_object, "Name", NULL); if (tmp != NULL) fu_uefi_device_set_name(self, tmp); tmp = json_object_get_string_member_with_default(json_object, "BackendId", NULL); if (tmp != NULL) fu_device_set_backend_id(device, tmp); #if GLIB_CHECK_VERSION(2, 80, 0) tmp = json_object_get_string_member_with_default(json_object, "Created", NULL); if (tmp != NULL) { g_autoptr(GDateTime) dt = g_date_time_new_from_iso8601(tmp, NULL); if (dt != NULL) fu_device_set_created_usec(device, g_date_time_to_unix_usec(dt)); } #endif /* array of events */ if (json_object_has_member(json_object, "Events")) { JsonArray *json_array = json_object_get_array_member(json_object, "Events"); for (guint i = 0; i < json_array_get_length(json_array); i++) { JsonNode *node_tmp = json_array_get_element(json_array, i); g_autoptr(FuDeviceEvent) event = fu_device_event_new(NULL); if (!fwupd_codec_from_json(FWUPD_CODEC(event), node_tmp, error)) return FALSE; fu_device_add_event(device, event); } } /* success */ return TRUE; } static void fu_uefi_device_finalize(GObject *object) { FuUefiDevice *self = FU_UEFI_DEVICE(object); FuUefiDevicePrivate *priv = GET_PRIVATE(self); g_free(priv->guid); g_free(priv->name); G_OBJECT_CLASS(fu_uefi_device_parent_class)->finalize(object); } static void fu_uefi_device_init(FuUefiDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_EMULATION_TAG); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); } static void fu_uefi_device_class_init(FuUefiDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_uefi_device_finalize; device_class->to_string = fu_uefi_device_to_string; device_class->probe = fu_uefi_device_probe; device_class->dump_firmware = fu_uefi_device_dump_firmware; device_class->incorporate = fu_uefi_device_incorporate; device_class->from_json = fu_uefi_device_from_json; device_class->add_json = fu_uefi_device_add_json; } FuUefiDevice * fu_uefi_device_new(const gchar *guid, const gchar *name) { g_autofree gchar *backend_id = NULL; g_autoptr(FuUefiDevice) self = NULL; backend_id = g_strdup_printf("%s-%s", guid, name); self = g_object_new(FU_TYPE_UEFI_DEVICE, "backend-id", backend_id, NULL); fu_uefi_device_set_guid(self, guid); fu_uefi_device_set_name(self, name); return g_steal_pointer(&self); } fwupd-2.0.10/libfwupdplugin/fu-uefi-device.h000066400000000000000000000013451501337203100207160ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-device.h" #define FU_TYPE_UEFI_DEVICE (fu_uefi_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuUefiDevice, fu_uefi_device, FU, UEFI_DEVICE, FuDevice) struct _FuUefiDeviceClass { FuDeviceClass parent_class; }; gboolean fu_uefi_device_set_efivar_bytes(FuUefiDevice *self, const gchar *guid, const gchar *name, GBytes *bytes, guint32 attr, GError **error) G_GNUC_NON_NULL(1, 2, 3, 4); GBytes * fu_uefi_device_get_efivar_bytes(FuUefiDevice *self, const gchar *guid, const gchar *name, guint32 *attr, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); fwupd-2.0.10/libfwupdplugin/fu-usb-bos-descriptor-private.h000066400000000000000000000004721501337203100237270ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-usb-bos-descriptor.h" FuUsbBosDescriptor * fu_usb_bos_descriptor_new(const struct libusb_bos_dev_capability_descriptor *bos_cap) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-usb-bos-descriptor.c000066400000000000000000000166411501337203100222570ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ /** * FuUsbBosDescriptor: * * This object is a thin glib wrapper around a `libusb_bos_dev_capability_descriptor`. * * All the data is copied when the object is created and the original descriptor can be destroyed * at any point. */ #include "config.h" #include #include "fu-byte-array.h" #include "fu-common.h" #include "fu-partial-input-stream.h" #include "fu-usb-bos-descriptor-private.h" struct _FuUsbBosDescriptor { FuUsbDescriptor parent_instance; struct libusb_bos_dev_capability_descriptor bos_cap; }; static void fu_usb_bos_descriptor_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_EXTENDED(FuUsbBosDescriptor, fu_usb_bos_descriptor, FU_TYPE_USB_DESCRIPTOR, 0, G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fu_usb_bos_descriptor_codec_iface_init)); static void fu_usb_bos_descriptor_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuUsbBosDescriptor *self = FU_USB_BOS_DESCRIPTOR(firmware); fu_xmlb_builder_insert_kv( bn, "dev_capability_type", fu_usb_descriptor_kind_to_string(self->bos_cap.bDevCapabilityType)); } static gboolean fu_usb_bos_descriptor_build(FuFirmware *firmware, XbNode *n, GError **error) { FuUsbBosDescriptor *self = FU_USB_BOS_DESCRIPTOR(firmware); const gchar *str; /* simple properties */ str = xb_node_query_text(n, "dev_capability_type", NULL); if (str != NULL) { self->bos_cap.bDevCapabilityType = fu_usb_descriptor_kind_from_string(str); if (self->bos_cap.bDevCapabilityType == FU_USB_DESCRIPTOR_KIND_INVALID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid DevCapabilityType %s", str); return FALSE; } } /* success */ return TRUE; } static gboolean fu_usb_bos_descriptor_from_json(FwupdCodec *codec, JsonNode *json_node, GError **error) { FuUsbBosDescriptor *self = FU_USB_BOS_DESCRIPTOR(codec); const gchar *str; JsonObject *json_object; /* sanity check */ if (!JSON_NODE_HOLDS_OBJECT(json_node)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not JSON object"); return FALSE; } json_object = json_node_get_object(json_node); /* optional properties */ self->bos_cap.bDevCapabilityType = json_object_get_int_member_with_default(json_object, "DevCapabilityType", 0x0); /* data */ str = json_object_get_string_member_with_default(json_object, "ExtraData", NULL); if (str != NULL) { gsize bufsz = 0; g_autofree guchar *buf = g_base64_decode(str, &bufsz); g_autoptr(GInputStream) stream = NULL; g_autoptr(FuFirmware) img = fu_firmware_new(); /* create child */ stream = g_memory_input_stream_new_from_data(g_steal_pointer(&buf), bufsz, g_free); if (!fu_firmware_parse_stream(img, stream, 0x0, FU_FIRMWARE_PARSE_FLAG_CACHE_BLOB, error)) return FALSE; fu_firmware_set_id(img, FU_FIRMWARE_ID_PAYLOAD); if (!fu_firmware_add_image_full(FU_FIRMWARE(self), img, error)) return FALSE; } /* success */ return TRUE; } static void fu_usb_bos_descriptor_add_json(FwupdCodec *codec, JsonBuilder *builder, FwupdCodecFlags flags) { FuUsbBosDescriptor *self = FU_USB_BOS_DESCRIPTOR(codec); g_autoptr(GBytes) bytes = NULL; /* optional properties */ if (self->bos_cap.bDevCapabilityType != 0) { json_builder_set_member_name(builder, "DevCapabilityType"); json_builder_add_int_value(builder, self->bos_cap.bDevCapabilityType); } /* data */ bytes = fu_firmware_get_image_by_id_bytes(FU_FIRMWARE(self), FU_FIRMWARE_ID_PAYLOAD, NULL); if (bytes != NULL && g_bytes_get_size(bytes) > 0) { g_autofree gchar *str = g_base64_encode(g_bytes_get_data(bytes, NULL), g_bytes_get_size(bytes)); json_builder_set_member_name(builder, "ExtraData"); json_builder_add_string_value(builder, str); } } /** * fu_usb_bos_descriptor_get_capability: * @self: a #FuUsbBosDescriptor * * Gets the BOS descriptor capability. * * Return value: capability * * Since: 2.0.0 **/ guint8 fu_usb_bos_descriptor_get_capability(FuUsbBosDescriptor *self) { g_return_val_if_fail(FU_IS_USB_BOS_DESCRIPTOR(self), 0); return self->bos_cap.bDevCapabilityType; } static gboolean fu_usb_bos_descriptor_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuUsbBosDescriptor *self = FU_USB_BOS_DESCRIPTOR(firmware); g_autoptr(FuUsbBosHdr) st = NULL; /* FuUsbDescriptor */ if (!FU_FIRMWARE_CLASS(fu_usb_bos_descriptor_parent_class) ->parse(firmware, stream, flags, error)) return FALSE; /* parse */ st = fu_usb_bos_hdr_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; self->bos_cap.bLength = fu_usb_bos_hdr_get_length(st); self->bos_cap.bDevCapabilityType = fu_usb_bos_hdr_get_dev_capability_type(st); /* data */ if (self->bos_cap.bLength > st->len) { g_autoptr(FuFirmware) img = fu_firmware_new(); g_autoptr(GInputStream) img_stream = NULL; img_stream = fu_partial_input_stream_new(stream, st->len, self->bos_cap.bLength - st->len, error); if (img_stream == NULL) { g_prefix_error(error, "failed to cut BOS descriptor: "); return FALSE; } if (!fu_firmware_parse_stream(img, img_stream, 0x0, FU_FIRMWARE_PARSE_FLAG_CACHE_BLOB, error)) return FALSE; fu_firmware_set_id(img, FU_FIRMWARE_ID_PAYLOAD); if (!fu_firmware_add_image_full(FU_FIRMWARE(self), img, error)) return FALSE; } /* success */ return TRUE; } static GByteArray * fu_usb_bos_descriptor_write(FuFirmware *firmware, GError **error) { FuUsbBosDescriptor *self = FU_USB_BOS_DESCRIPTOR(firmware); g_autoptr(FuUsbBosHdr) st = fu_usb_bos_hdr_new(); g_autoptr(GBytes) blob = NULL; fu_usb_bos_hdr_set_dev_capability_type(st, self->bos_cap.bDevCapabilityType); blob = fu_firmware_get_image_by_id_bytes(firmware, FU_FIRMWARE_ID_PAYLOAD, NULL); if (blob != NULL) { fu_byte_array_append_bytes(st, blob); fu_usb_bos_hdr_set_length(st, st->len); } /* success */ return g_steal_pointer(&st); } static void fu_usb_bos_descriptor_codec_iface_init(FwupdCodecInterface *iface) { iface->add_json = fu_usb_bos_descriptor_add_json; iface->from_json = fu_usb_bos_descriptor_from_json; } static void fu_usb_bos_descriptor_class_init(FuUsbBosDescriptorClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_usb_bos_descriptor_parse; firmware_class->write = fu_usb_bos_descriptor_write; firmware_class->build = fu_usb_bos_descriptor_build; firmware_class->export = fu_usb_bos_descriptor_export; } static void fu_usb_bos_descriptor_init(FuUsbBosDescriptor *self) { } /** * fu_usb_bos_descriptor_new: * * Return value: a new #FuUsbBosDescriptor object. * * Since: 2.0.0 **/ FuUsbBosDescriptor * fu_usb_bos_descriptor_new(const struct libusb_bos_dev_capability_descriptor *bos_cap) { FuUsbBosDescriptor *self = g_object_new(FU_TYPE_USB_BOS_DESCRIPTOR, NULL); g_autoptr(FuFirmware) img = fu_firmware_new(); g_autoptr(GBytes) bytes = NULL; /* copy the data */ memcpy(&self->bos_cap, bos_cap, sizeof(*bos_cap)); /* nocheck:blocked */ bytes = g_bytes_new(bos_cap->dev_capability_data, bos_cap->bLength - FU_USB_BOS_HDR_SIZE); fu_firmware_set_bytes(img, bytes); fu_firmware_set_id(img, FU_FIRMWARE_ID_PAYLOAD); fu_firmware_add_image(FU_FIRMWARE(self), img); return FU_USB_BOS_DESCRIPTOR(self); } fwupd-2.0.10/libfwupdplugin/fu-usb-bos-descriptor.h000066400000000000000000000006611501337203100222570ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-usb-descriptor.h" #define FU_TYPE_USB_BOS_DESCRIPTOR (fu_usb_bos_descriptor_get_type()) G_DECLARE_FINAL_TYPE(FuUsbBosDescriptor, fu_usb_bos_descriptor, FU, USB_BOS_DESCRIPTOR, FuUsbDescriptor) guint8 fu_usb_bos_descriptor_get_capability(FuUsbBosDescriptor *self); fwupd-2.0.10/libfwupdplugin/fu-usb-config-descriptor-private.h000066400000000000000000000003661501337203100244130ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-usb-config-descriptor.h" FuUsbConfigDescriptor * fu_usb_config_descriptor_new(void); fwupd-2.0.10/libfwupdplugin/fu-usb-config-descriptor.c000066400000000000000000000101411501337203100227260ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ /** * FuUsbConfigDescriptor: * * This object is a thin glib wrapper around a `libusb_config_dev_capability_descriptor`. * * All the data is copied when the object is created and the original descriptor can be destroyed * at any point. */ #include "config.h" #include #include "fu-usb-config-descriptor-private.h" struct _FuUsbConfigDescriptor { FuUsbDescriptor parent_instance; guint8 configuration; guint8 configuration_value; }; static void fu_usb_config_descriptor_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_EXTENDED(FuUsbConfigDescriptor, fu_usb_config_descriptor, FU_TYPE_USB_DESCRIPTOR, 0, G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fu_usb_config_descriptor_codec_iface_init)); static gboolean fu_usb_config_descriptor_from_json(FwupdCodec *codec, JsonNode *json_node, GError **error) { FuUsbConfigDescriptor *self = FU_USB_CONFIG_DESCRIPTOR(codec); JsonObject *json_object; /* sanity check */ if (!JSON_NODE_HOLDS_OBJECT(json_node)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not JSON object"); return FALSE; } json_object = json_node_get_object(json_node); /* optional properties */ self->configuration = json_object_get_int_member_with_default(json_object, "Configuration", 0x0); self->configuration_value = json_object_get_int_member_with_default(json_object, "ConfigurationValue", 0x0); /* success */ return TRUE; } static void fu_usb_config_descriptor_add_json(FwupdCodec *codec, JsonBuilder *builder, FwupdCodecFlags flags) { FuUsbConfigDescriptor *self = FU_USB_CONFIG_DESCRIPTOR(codec); /* optional properties */ if (self->configuration != 0) { json_builder_set_member_name(builder, "Configuration"); json_builder_add_int_value(builder, self->configuration); } if (self->configuration_value != 0) { json_builder_set_member_name(builder, "ConfigurationValue"); json_builder_add_int_value(builder, self->configuration_value); } } /** * fu_usb_config_descriptor_get_configuration: * @self: a #FuUsbConfigDescriptor * * Gets the config descriptor configuration. * * Return value: integer * * Since: 2.0.0 **/ guint8 fu_usb_config_descriptor_get_configuration(FuUsbConfigDescriptor *self) { g_return_val_if_fail(FU_IS_USB_CONFIG_DESCRIPTOR(self), 0); return self->configuration; } /** * fu_usb_config_descriptor_get_configuration_value: * @self: a #FuUsbConfigDescriptor * * Gets the CONFIG descriptor configuration value. * * Return value: integer * * Since: 2.0.0 **/ guint8 fu_usb_config_descriptor_get_configuration_value(FuUsbConfigDescriptor *self) { g_return_val_if_fail(FU_IS_USB_CONFIG_DESCRIPTOR(self), 0); return self->configuration_value; } static gboolean fu_usb_config_descriptor_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuUsbConfigDescriptor *self = FU_USB_CONFIG_DESCRIPTOR(firmware); g_autoptr(FuUsbDescriptorHdr) st = NULL; /* parse */ st = fu_usb_descriptor_hdr_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; self->configuration = fu_usb_descriptor_hdr_get_configuration(st); self->configuration_value = fu_usb_descriptor_hdr_get_configuration_value(st); /* success */ return TRUE; } static void fu_usb_config_descriptor_codec_iface_init(FwupdCodecInterface *iface) { iface->add_json = fu_usb_config_descriptor_add_json; iface->from_json = fu_usb_config_descriptor_from_json; } static void fu_usb_config_descriptor_class_init(FuUsbConfigDescriptorClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_usb_config_descriptor_parse; } static void fu_usb_config_descriptor_init(FuUsbConfigDescriptor *self) { } /** * fu_usb_config_descriptor_new: * * Return value: a new #FuUsbConfigDescriptor object. * * Since: 2.0.0 **/ FuUsbConfigDescriptor * fu_usb_config_descriptor_new(void) { return FU_USB_CONFIG_DESCRIPTOR(g_object_new(FU_TYPE_USB_CONFIG_DESCRIPTOR, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-usb-config-descriptor.h000066400000000000000000000010371501337203100227370ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-usb-descriptor.h" #define FU_TYPE_USB_CONFIG_DESCRIPTOR (fu_usb_config_descriptor_get_type()) G_DECLARE_FINAL_TYPE(FuUsbConfigDescriptor, fu_usb_config_descriptor, FU, USB_CONFIG_DESCRIPTOR, FuUsbDescriptor) guint8 fu_usb_config_descriptor_get_configuration(FuUsbConfigDescriptor *self); guint8 fu_usb_config_descriptor_get_configuration_value(FuUsbConfigDescriptor *self); fwupd-2.0.10/libfwupdplugin/fu-usb-descriptor.c000066400000000000000000000016431501337203100214720ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-usb-descriptor.h" G_DEFINE_TYPE(FuUsbDescriptor, fu_usb_descriptor, FU_TYPE_FIRMWARE) static gboolean fu_usb_descriptor_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { g_autoptr(FuUsbBaseHdr) st = NULL; /* parse */ st = fu_usb_base_hdr_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; fu_firmware_set_size(firmware, fu_usb_base_hdr_get_length(st)); fu_firmware_set_idx(firmware, fu_usb_base_hdr_get_descriptor_type(st)); /* success */ return TRUE; } static void fu_usb_descriptor_class_init(FuUsbDescriptorClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_usb_descriptor_parse; } static void fu_usb_descriptor_init(FuUsbDescriptor *self) { } fwupd-2.0.10/libfwupdplugin/fu-usb-descriptor.h000066400000000000000000000006201501337203100214710ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #include "fu-usb-struct.h" #define FU_TYPE_USB_DESCRIPTOR (fu_usb_descriptor_get_type()) G_DECLARE_DERIVABLE_TYPE(FuUsbDescriptor, fu_usb_descriptor, FU, USB_DESCRIPTOR, FuFirmware) struct _FuUsbDescriptorClass { FuFirmwareClass parent_class; }; fwupd-2.0.10/libfwupdplugin/fu-usb-device-ds20.c000066400000000000000000000166451501337203100213310ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuUsbDeviceDs20" #include "config.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-dump.h" #include "fu-input-stream.h" #include "fu-usb-device-ds20-struct.h" #include "fu-usb-device-ds20.h" /** * FuUsbDeviceDs20: * * A USB DS20 BOS descriptor. * * See also: [class@FuUsbDevice] */ typedef struct { guint32 version_lowest; } FuUsbDeviceDs20Private; G_DEFINE_TYPE_WITH_PRIVATE(FuUsbDeviceDs20, fu_usb_device_ds20, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_usb_device_ds20_get_instance_private(o)) typedef struct { guint32 platform_ver; guint16 total_length; guint8 vendor_code; guint8 alt_code; } FuUsbDeviceDs20Item; /** * fu_usb_device_ds20_set_version_lowest: * @self: a #FuUsbDeviceDs20 * @version_lowest: version number * * Sets the lowest possible `platform_ver` for a DS20 descriptor. * * Since: 1.8.5 **/ void fu_usb_device_ds20_set_version_lowest(FuUsbDeviceDs20 *self, guint32 version_lowest) { FuUsbDeviceDs20Private *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_USB_DEVICE_DS20(self)); priv->version_lowest = version_lowest; } /** * fu_usb_device_ds20_apply_to_device: * @self: a #FuUsbDeviceDs20 * @device: a #FuUsbDevice * @error: (nullable): optional return location for an error * * Sets the DS20 descriptor onto @device. * * Returns: %TRUE for success * * Since: 1.8.5 **/ gboolean fu_usb_device_ds20_apply_to_device(FuUsbDeviceDs20 *self, FuUsbDevice *device, GError **error) { FuUsbDeviceDs20Class *klass = FU_USB_DEVICE_DS20_GET_CLASS(self); gsize actual_length = 0; gsize total_length = fu_firmware_get_size(FU_FIRMWARE(self)); guint8 vendor_code = fu_firmware_get_idx(FU_FIRMWARE(self)); g_autofree guint8 *buf = g_malloc0(total_length); g_autoptr(GInputStream) stream = NULL; g_return_val_if_fail(FU_IS_USB_DEVICE_DS20(self), FALSE); g_return_val_if_fail(FU_IS_USB_DEVICE(device), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_usb_device_control_transfer(device, FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, vendor_code, /* bRequest */ 0x0, /* wValue */ 0x07, /* wIndex */ buf, total_length, &actual_length, 500, NULL, /* cancellable */ error)) { g_prefix_error(error, "requested vendor code 0x%02x: ", vendor_code); return FALSE; } if (total_length != actual_length) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "expected 0x%x bytes from vendor code 0x%02x, but got 0x%x", (guint)total_length, vendor_code, (guint)actual_length); return FALSE; } /* debug */ fu_dump_raw(G_LOG_DOMAIN, "PlatformCapabilityOs20", buf, actual_length); /* FuUsbDeviceDs20->parse */ stream = g_memory_input_stream_new_from_data(buf, actual_length, NULL); return klass->parse(self, stream, device, error); } static gboolean fu_usb_device_ds20_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { g_autoptr(GByteArray) st = NULL; g_autofree gchar *guid_str = NULL; /* matches the correct UUID */ st = fu_struct_ds20_parse_stream(stream, offset, error); if (st == NULL) return FALSE; guid_str = fwupd_guid_to_string(fu_struct_ds20_get_guid(st), FWUPD_GUID_FLAG_MIXED_ENDIAN); if (g_strcmp0(guid_str, fu_firmware_get_id(firmware)) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid UUID for DS20, expected %s", fu_firmware_get_id(firmware)); return FALSE; } /* success */ return TRUE; } static gint fu_usb_device_ds20_sort_by_platform_ver_cb(gconstpointer a, gconstpointer b) { FuUsbDeviceDs20Item *ds1 = *((FuUsbDeviceDs20Item **)a); FuUsbDeviceDs20Item *ds2 = *((FuUsbDeviceDs20Item **)b); if (ds1->platform_ver < ds2->platform_ver) return -1; if (ds1->platform_ver > ds2->platform_ver) return 1; return 0; } static gboolean fu_usb_device_ds20_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuUsbDeviceDs20 *self = FU_USB_DEVICE_DS20(firmware); FuUsbDeviceDs20Private *priv = GET_PRIVATE(self); gsize streamsz = 0; guint version_lowest = fu_firmware_get_version_raw(firmware); g_autoptr(GPtrArray) dsinfos = g_ptr_array_new_with_free_func(g_free); if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; for (gsize off = 0; off < streamsz; off += FU_STRUCT_DS20_SIZE) { g_autofree FuUsbDeviceDs20Item *dsinfo = g_new0(FuUsbDeviceDs20Item, 1); g_autoptr(GByteArray) st = NULL; /* parse */ st = fu_struct_ds20_parse_stream(stream, off, error); if (st == NULL) return FALSE; dsinfo->platform_ver = fu_struct_ds20_get_platform_ver(st); dsinfo->total_length = fu_struct_ds20_get_total_length(st); dsinfo->vendor_code = fu_struct_ds20_get_vendor_code(st); dsinfo->alt_code = fu_struct_ds20_get_alt_code(st); g_debug("PlatformVersion=0x%08x, TotalLength=0x%04x, VendorCode=0x%02x, " "AltCode=0x%02x", dsinfo->platform_ver, dsinfo->total_length, dsinfo->vendor_code, dsinfo->alt_code); g_ptr_array_add(dsinfos, g_steal_pointer(&dsinfo)); } /* sort by platform_ver, highest first */ g_ptr_array_sort(dsinfos, fu_usb_device_ds20_sort_by_platform_ver_cb); /* find the newest info that's not newer than the lowest version */ for (guint i = 0; i < dsinfos->len; i++) { FuUsbDeviceDs20Item *dsinfo = g_ptr_array_index(dsinfos, i); /* not valid */ if (dsinfo->platform_ver == 0x0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid platform version 0x%08x", dsinfo->platform_ver); return FALSE; } if (dsinfo->platform_ver < priv->version_lowest) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid platform version 0x%08x, expected >= 0x%08x", dsinfo->platform_ver, priv->version_lowest); return FALSE; } /* dwVersion is effectively the minimum version */ if (dsinfo->platform_ver <= version_lowest) { fu_firmware_set_size(firmware, dsinfo->total_length); fu_firmware_set_idx(firmware, dsinfo->vendor_code); return TRUE; } } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no supported platform version"); return FALSE; } static GByteArray * fu_usb_device_ds20_write(FuFirmware *firmware, GError **error) { g_autoptr(FuStructDs20) st = fu_struct_ds20_new(); fwupd_guid_t guid = {0x0}; /* pack */ if (!fwupd_guid_from_string(fu_firmware_get_id(firmware), &guid, FWUPD_GUID_FLAG_MIXED_ENDIAN, error)) return NULL; fu_struct_ds20_set_guid(st, &guid); fu_struct_ds20_set_platform_ver(st, fu_firmware_get_version_raw(firmware)); fu_struct_ds20_set_total_length(st, fu_firmware_get_size(firmware)); fu_struct_ds20_set_vendor_code(st, fu_firmware_get_idx(firmware)); return g_steal_pointer(&st); } static void fu_usb_device_ds20_init(FuUsbDeviceDs20 *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_STORED_SIZE); } static void fu_usb_device_ds20_class_init(FuUsbDeviceDs20Class *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_usb_device_ds20_validate; firmware_class->parse = fu_usb_device_ds20_parse; firmware_class->write = fu_usb_device_ds20_write; } fwupd-2.0.10/libfwupdplugin/fu-usb-device-ds20.h000066400000000000000000000014311501337203100213210ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #include "fu-usb-device.h" #define FU_TYPE_USB_DEVICE_DS20 (fu_usb_device_ds20_get_type()) G_DECLARE_DERIVABLE_TYPE(FuUsbDeviceDs20, fu_usb_device_ds20, FU, USB_DEVICE_DS20, FuFirmware) struct _FuUsbDeviceDs20Class { FuFirmwareClass parent_class; gboolean (*parse)(FuUsbDeviceDs20 *self, GInputStream *stream, FuUsbDevice *device, GError **error) G_GNUC_WARN_UNUSED_RESULT; }; void fu_usb_device_ds20_set_version_lowest(FuUsbDeviceDs20 *self, guint32 version_lowest) G_GNUC_NON_NULL(1); gboolean fu_usb_device_ds20_apply_to_device(FuUsbDeviceDs20 *self, FuUsbDevice *device, GError **error) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-usb-device-ds20.rs000066400000000000000000000012541501337203100215210ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructDs20 { _reserved: u8, guid: Guid, platform_ver: u32le, total_length: u16le, vendor_code: u8, alt_code: u8, } #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructMsDs20 { size: u16le, type: u16le, } #[derive(ToString)] enum FuUsbDeviceMsDs20Desc { SetHeaderDescriptor, SubsetHeaderConfiguration, SubsetHeaderFunction, FeatureCompatibleId, FeatureRegProperty, FeatureMinResumeTime, FeatureModelId, FeatureCcgpDevice, FeatureVendorRevision, } fwupd-2.0.10/libfwupdplugin/fu-usb-device-fw-ds20.c000066400000000000000000000076311501337203100217360ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuUsbDeviceDs20" #include "config.h" #include "fu-bytes.h" #include "fu-device-private.h" #include "fu-input-stream.h" #include "fu-string.h" #include "fu-usb-device-fw-ds20.h" struct _FuUsbDeviceFwDs20 { FuUsbDeviceDs20 parent_instance; }; G_DEFINE_TYPE(FuUsbDeviceFwDs20, fu_usb_device_fw_ds20, FU_TYPE_USB_DEVICE_DS20) #define DS20_VERSION_LOWEST ((1u << 16) | (8u << 8) | 5u) #define DS20_VERSION_CURRENT \ ((((guint32)FWUPD_MAJOR_VERSION) << 16) | (((guint32)FWUPD_MINOR_VERSION) << 8) | \ ((guint)FWUPD_MICRO_VERSION)) static gboolean fu_usb_device_fw_ds20_parse(FuUsbDeviceDs20 *self, GInputStream *stream, FuUsbDevice *device, GError **error) { gsize bufsz = 0; gsize bufsz_safe = 0; const guint8 *buf; g_auto(GStrv) lines = NULL; g_autoptr(GBytes) blob = NULL; /* convert to blob */ blob = fu_input_stream_read_bytes(stream, 0, G_MAXSIZE, NULL, error); if (blob == NULL) return FALSE; /* only accept Linux line-endings */ buf = g_bytes_get_data(blob, &bufsz); if (g_strstr_len((const gchar *)buf, bufsz, "\r") != NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "Windows line endings are not supported"); return FALSE; } /* find the first NUL, if one exists */ for (gsize i = 1; i < bufsz; i++) { if (buf[i] == '\0') { bufsz_safe = i - 1; break; } } /* no NUL is unexpected, but fine */ if (bufsz_safe == 0) bufsz_safe = bufsz; if (!g_utf8_validate((const gchar *)buf, (gssize)bufsz_safe, NULL)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "DS20 descriptor is not valid UTF-8"); return FALSE; } /* add payload for ->export() */ fu_firmware_set_bytes(FU_FIRMWARE(self), blob); /* split into lines */ lines = fu_strsplit((const gchar *)buf, bufsz_safe, "\n", -1); for (guint i = 0; lines[i] != NULL; i++) { g_autofree gchar *key = NULL; g_autofree gchar *value = NULL; g_auto(GStrv) kv = NULL; if (lines[i][0] == '\0') continue; if (g_str_has_prefix(lines[i], "[") && g_str_has_suffix(lines[i], "]")) { g_debug("ignoring DS-20 group header: %s", lines[i]); continue; } kv = g_strsplit(lines[i], "=", 2); if (g_strv_length(kv) < 2) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "expected key=value for '%s'", lines[i]); return FALSE; } key = fu_strstrip(kv[0]); if (g_strcmp0(key, kv[0]) != 0) g_debug("removing DS-20 whitespace '%s'", kv[0]); value = fu_strstrip(kv[1]); if (g_strcmp0(value, kv[1]) != 0) g_debug("removing DS-20 whitespace '%s'", kv[1]); /* it's fine to be strict here, as we checked the fwupd version was new enough in * FuUsbDeviceDs20Item */ g_debug("setting ds20 device quirk '%s'='%s'", key, value); if (!fu_device_set_quirk_kv(FU_DEVICE(device), key, value, FU_CONTEXT_QUIRK_SOURCE_DEVICE, error)) return FALSE; } /* success */ return TRUE; } static void fu_usb_device_fw_ds20_class_init(FuUsbDeviceFwDs20Class *klass) { FuUsbDeviceDs20Class *usb_device_ds20_class = FU_USB_DEVICE_DS20_CLASS(klass); usb_device_ds20_class->parse = fu_usb_device_fw_ds20_parse; } static void fu_usb_device_fw_ds20_init(FuUsbDeviceFwDs20 *self) { fu_firmware_set_version_raw(FU_FIRMWARE(self), DS20_VERSION_CURRENT); fu_usb_device_ds20_set_version_lowest(FU_USB_DEVICE_DS20(self), DS20_VERSION_LOWEST); fu_firmware_set_id(FU_FIRMWARE(self), "010aec63-f574-52cd-9dda-2852550d94f0"); } /** * fu_usb_device_fw_ds20_new: * * Creates a new #FuUsbDeviceFwDs20. * * Returns: (transfer full): a #FuFirmware * * Since: 1.8.5 **/ FuFirmware * fu_usb_device_fw_ds20_new(void) { return g_object_new(FU_TYPE_USB_DEVICE_FW_DS20, NULL); } fwupd-2.0.10/libfwupdplugin/fu-usb-device-fw-ds20.h000066400000000000000000000006301501337203100217330ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-usb-device-ds20.h" #define FU_TYPE_USB_DEVICE_FW_DS20 (fu_usb_device_fw_ds20_get_type()) G_DECLARE_FINAL_TYPE(FuUsbDeviceFwDs20, fu_usb_device_fw_ds20, FU, USB_DEVICE_FW_DS20, FuUsbDeviceDs20) FuFirmware * fu_usb_device_fw_ds20_new(void); fwupd-2.0.10/libfwupdplugin/fu-usb-device-ms-ds20.c000066400000000000000000000036231501337203100217360ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuUsbDeviceDs20" #include "config.h" #include "fu-input-stream.h" #include "fu-usb-device-ds20-struct.h" #include "fu-usb-device-ms-ds20.h" struct _FuUsbDeviceMsDs20 { FuUsbDeviceDs20 parent_instance; }; G_DEFINE_TYPE(FuUsbDeviceMsDs20, fu_usb_device_ms_ds20, FU_TYPE_USB_DEVICE_DS20) static gboolean fu_usb_device_ms_ds20_parse(FuUsbDeviceDs20 *self, GInputStream *stream, FuUsbDevice *device, GError **error) { gsize streamsz = 0; /* get length and type only */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; for (gsize offset = 0; offset < streamsz;) { guint16 desc_sz; guint16 desc_type; g_autoptr(FuStructMsDs20) st = NULL; st = fu_struct_ms_ds20_parse_stream(stream, offset, error); if (st == NULL) return FALSE; desc_sz = fu_struct_ms_ds20_get_size(st); if (desc_sz == 0) break; desc_type = fu_struct_ms_ds20_get_type(st); g_debug("USB OS descriptor type 0x%04x [%s], length 0x%04x", desc_type, fu_usb_device_ms_ds20_desc_to_string(desc_type), desc_sz); offset += desc_sz; } /* success */ return TRUE; } static void fu_usb_device_ms_ds20_class_init(FuUsbDeviceMsDs20Class *klass) { FuUsbDeviceDs20Class *usb_device_ds20_class = FU_USB_DEVICE_DS20_CLASS(klass); usb_device_ds20_class->parse = fu_usb_device_ms_ds20_parse; } static void fu_usb_device_ms_ds20_init(FuUsbDeviceMsDs20 *self) { fu_firmware_set_version_raw(FU_FIRMWARE(self), 0x06030000); /* Windows 8.1 */ fu_firmware_set_id(FU_FIRMWARE(self), "d8dd60df-4589-4cc7-9cd2-659d9e648a9f"); } /** * fu_usb_device_ms_ds20_new: * * Creates a new #FuUsbDeviceMsDs20. * * Returns: (transfer full): a #FuFirmware * * Since: 1.8.5 **/ FuFirmware * fu_usb_device_ms_ds20_new(void) { return g_object_new(FU_TYPE_USB_DEVICE_MS_DS20, NULL); } fwupd-2.0.10/libfwupdplugin/fu-usb-device-ms-ds20.h000066400000000000000000000006301501337203100217360ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-usb-device-ds20.h" #define FU_TYPE_USB_DEVICE_MS_DS20 (fu_usb_device_ms_ds20_get_type()) G_DECLARE_FINAL_TYPE(FuUsbDeviceMsDs20, fu_usb_device_ms_ds20, FU, USB_DEVICE_MS_DS20, FuUsbDeviceDs20) FuFirmware * fu_usb_device_ms_ds20_new(void); fwupd-2.0.10/libfwupdplugin/fu-usb-device-private.h000066400000000000000000000006041501337203100222240ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-usb-device.h" G_DEFINE_AUTOPTR_CLEANUP_FUNC(libusb_context, libusb_exit) FuUsbDevice * fu_usb_device_new(FuContext *ctx, libusb_device *usb_device) G_GNUC_NON_NULL(1); libusb_device * fu_usb_device_get_dev(FuUsbDevice *self); fwupd-2.0.10/libfwupdplugin/fu-usb-device.c000066400000000000000000002715121501337203100205570ustar00rootroot00000000000000/* * Copyright 2010 Richard Hughes * Copyright 2011 Hans de Goede * Copyright 2011 Debarshi Ray * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuUsbDevice" #include "config.h" #include "fu-bytes.h" #include "fu-context-private.h" #include "fu-device-event-private.h" #include "fu-device-private.h" #include "fu-dump.h" #include "fu-input-stream.h" #include "fu-linear-firmware.h" #include "fu-mem.h" #include "fu-string.h" #include "fu-usb-bos-descriptor-private.h" #include "fu-usb-config-descriptor-private.h" #include "fu-usb-device-fw-ds20.h" #include "fu-usb-device-ms-ds20.h" #include "fu-usb-device-private.h" #include "fu-usb-hid-descriptor-private.h" #include "fu-usb-interface-private.h" /** * FuUsbDevice: * * A USB device. * * See also: [class@FuDevice], [class@FuHidDevice] */ typedef struct { libusb_device *usb_device; /* (nullable): only set from FuUsbBackend */ libusb_device_handle *handle; /* (nullable) */ struct libusb_device_descriptor desc; guint8 busnum; guint8 devnum; gboolean interfaces_valid; gboolean bos_descriptors_valid; GPtrArray *interfaces; /* (element-type FuUsbInterface) */ GPtrArray *bos_descriptors; /* (element-type FuUsbBosDescriptor) */ GPtrArray *cfg_descriptors; /* (element-type FuUsbConfigDescriptor) */ GPtrArray *hid_descriptors; /* (element-type FuUsbHidDescriptor) */ gint configuration; GPtrArray *device_interfaces; /* (nullable) (element-type FuUsbDeviceInterface) */ guint claim_retry_count; } FuUsbDevicePrivate; typedef struct { guint8 number; gboolean claimed; } FuUsbDeviceInterface; static gboolean fu_usb_device_ensure_interfaces(FuUsbDevice *self, GError **error); G_DEFINE_TYPE_WITH_PRIVATE(FuUsbDevice, fu_usb_device, FU_TYPE_UDEV_DEVICE); enum { PROP_0, PROP_LIBUSB_DEVICE, PROP_LAST }; #define GET_PRIVATE(o) (fu_usb_device_get_instance_private(o)) #define FU_DEVICE_CLAIM_INTERFACE_DELAY 500 /* ms */ #define FU_USB_DEVICE_OPEN_DELAY 50 /* ms */ static gboolean fu_usb_device_libusb_error_to_gerror(gint rc, GError **error) { gint error_code = FWUPD_ERROR_INTERNAL; /* Put the rc in libusb's error enum so that gcc warns us if we're missing an error code */ enum libusb_error result = rc; switch (result) { case LIBUSB_SUCCESS: return TRUE; case LIBUSB_ERROR_INVALID_PARAM: case LIBUSB_ERROR_NOT_FOUND: case LIBUSB_ERROR_NO_MEM: case LIBUSB_ERROR_OTHER: case LIBUSB_ERROR_INTERRUPTED: error_code = FWUPD_ERROR_INTERNAL; break; case LIBUSB_ERROR_IO: case LIBUSB_ERROR_OVERFLOW: case LIBUSB_ERROR_PIPE: error_code = FWUPD_ERROR_READ; break; case LIBUSB_ERROR_TIMEOUT: error_code = FWUPD_ERROR_TIMED_OUT; break; case LIBUSB_ERROR_NOT_SUPPORTED: error_code = FWUPD_ERROR_NOT_SUPPORTED; break; case LIBUSB_ERROR_ACCESS: error_code = FWUPD_ERROR_PERMISSION_DENIED; break; case LIBUSB_ERROR_NO_DEVICE: error_code = FWUPD_ERROR_NOT_FOUND; break; case LIBUSB_ERROR_BUSY: error_code = FWUPD_ERROR_BUSY; break; default: break; } g_set_error(error, FWUPD_ERROR, error_code, "USB error: %s [%i]", libusb_strerror(rc), rc); return FALSE; } static gboolean fu_usb_device_libusb_status_to_gerror(gint status, GError **error) { gboolean ret = FALSE; switch (status) { case LIBUSB_TRANSFER_COMPLETED: ret = TRUE; break; case LIBUSB_TRANSFER_ERROR: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "transfer failed"); break; case LIBUSB_TRANSFER_TIMED_OUT: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "transfer timed out"); break; case LIBUSB_TRANSFER_CANCELLED: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "transfer cancelled"); break; case LIBUSB_TRANSFER_STALL: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "endpoint stalled or request not supported"); break; case LIBUSB_TRANSFER_NO_DEVICE: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "device was disconnected"); break; case LIBUSB_TRANSFER_OVERFLOW: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "device sent more data than requested"); break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unknown status [%i]", status); } return ret; } /** * fu_usb_device_get_dev: (skip): **/ libusb_device * fu_usb_device_get_dev(FuUsbDevice *self) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_USB_DEVICE(self), NULL); return priv->usb_device; } static gboolean fu_usb_device_not_open_error(FuUsbDevice *self, GError **error) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "device %04x:%04x has not been opened", fu_device_get_vid(FU_DEVICE(self)), fu_device_get_pid(FU_DEVICE(self))); return FALSE; } static void fu_usb_device_invalidate(FuDevice *device) { FuUsbDevice *self = FU_USB_DEVICE(device); FuUsbDevicePrivate *priv = GET_PRIVATE(self); /* FuUdevDevice->invalidate */ FU_DEVICE_CLASS(fu_usb_device_parent_class)->invalidate(device); priv->interfaces_valid = FALSE; priv->bos_descriptors_valid = FALSE; g_ptr_array_set_size(priv->interfaces, 0); g_ptr_array_set_size(priv->bos_descriptors, 0); g_ptr_array_set_size(priv->hid_descriptors, 0); } static void fu_usb_device_set_dev(FuUsbDevice *self, struct libusb_device *usb_device) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); /* allow replacement */ g_clear_pointer(&priv->usb_device, libusb_unref_device); if (usb_device != NULL) priv->usb_device = libusb_ref_device(usb_device); } static void fu_usb_device_vid_notify_cb(FuDevice *device, GParamSpec *pspec, gpointer user_data) { fu_device_add_instance_u16(device, "VID", fu_device_get_vid(device)); } static void fu_usb_device_pid_notify_cb(FuDevice *device, GParamSpec *pspec, gpointer user_data) { fu_device_add_instance_u16(device, "PID", fu_device_get_pid(device)); } static void fu_usb_device_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuUsbDevice *device = FU_USB_DEVICE(object); FuUsbDevicePrivate *priv = GET_PRIVATE(device); switch (prop_id) { case PROP_LIBUSB_DEVICE: g_value_set_pointer(value, priv->usb_device); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_usb_device_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuUsbDevice *self = FU_USB_DEVICE(object); switch (prop_id) { case PROP_LIBUSB_DEVICE: fu_usb_device_set_dev(self, g_value_get_pointer(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_usb_device_finalize(GObject *object) { FuUsbDevice *self = FU_USB_DEVICE(object); FuUsbDevicePrivate *priv = GET_PRIVATE(self); if (priv->handle != NULL) libusb_close(priv->handle); if (priv->usb_device != NULL) libusb_unref_device(priv->usb_device); if (priv->device_interfaces != NULL) g_ptr_array_unref(priv->device_interfaces); g_ptr_array_unref(priv->interfaces); g_ptr_array_unref(priv->bos_descriptors); g_ptr_array_unref(priv->hid_descriptors); g_ptr_array_unref(priv->cfg_descriptors); G_OBJECT_CLASS(fu_usb_device_parent_class)->finalize(object); } static void fu_usb_device_init(FuUsbDevice *self) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); priv->configuration = -1; priv->interfaces = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); priv->bos_descriptors = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); priv->cfg_descriptors = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); priv->hid_descriptors = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); fu_device_set_acquiesce_delay(FU_DEVICE(self), 2500); fu_device_retry_add_recovery(FU_DEVICE(self), FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, NULL); fu_device_retry_add_recovery(FU_DEVICE(self), FWUPD_ERROR, FWUPD_ERROR_PERMISSION_DENIED, NULL); g_signal_connect(FU_DEVICE(self), "notify::vid", G_CALLBACK(fu_usb_device_vid_notify_cb), NULL); g_signal_connect(FU_DEVICE(self), "notify::pid", G_CALLBACK(fu_usb_device_pid_notify_cb), NULL); } /** * fu_usb_device_set_claim_retry_count: * @self: a #FuUsbDevice * @claim_retry_count: integer * * Sets the number of tries we should attempt when claiming the device. * Applies to all interfaces associated with this device. * * Since: 1.9.10 **/ void fu_usb_device_set_claim_retry_count(FuUsbDevice *self, guint claim_retry_count) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_USB_DEVICE(self)); priv->claim_retry_count = claim_retry_count; } /** * fu_usb_device_get_claim_retry_count: * @self: a #FuUsbDevice * * Gets the number of tries we should attempt when claiming the device. * * Returns: integer, or `0` if no attempt should be made. * * Since: 1.9.10 **/ guint fu_usb_device_get_claim_retry_count(FuUsbDevice *self) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_USB_DEVICE(self), G_MAXUINT); return priv->claim_retry_count; } /** * fu_usb_device_set_configuration: * @device: a #FuUsbDevice * @configuration: the configuration value to set * * Set the active bConfigurationValue for the device. * * Since: 1.7.4 **/ void fu_usb_device_set_configuration(FuUsbDevice *device, gint configuration) { FuUsbDevicePrivate *priv = GET_PRIVATE(device); g_return_if_fail(FU_IS_USB_DEVICE(device)); priv->configuration = configuration; } /** * fu_usb_device_add_interface: * @device: a #FuUsbDevice * @number: bInterfaceNumber of the interface * * Adds an interface that will be claimed on `->open()` and released on `->close()`. * * Since: 1.7.4 **/ void fu_usb_device_add_interface(FuUsbDevice *device, guint8 number) { FuUsbDevicePrivate *priv = GET_PRIVATE(device); FuUsbDeviceInterface *iface; g_return_if_fail(FU_IS_USB_DEVICE(device)); if (priv->device_interfaces == NULL) priv->device_interfaces = g_ptr_array_new_with_free_func(g_free); /* check for existing */ for (guint i = 0; i < priv->device_interfaces->len; i++) { iface = g_ptr_array_index(priv->device_interfaces, i); if (iface->number == number) return; } /* add new */ iface = g_new0(FuUsbDeviceInterface, 1); iface->number = number; g_ptr_array_add(priv->device_interfaces, iface); } static gboolean fu_usb_device_query_hub(FuUsbDevice *self, GError **error) { gsize sz = 0; guint16 value = 0x29; guint8 data[0x0c] = {0x0}; g_autoptr(GString) hub = g_string_new(NULL); /* longer descriptor for SuperSpeed */ if (fu_usb_device_get_spec(self) >= 0x0300) value = 0x2a; if (!fu_usb_device_control_transfer(self, FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_CLASS, FU_USB_RECIPIENT_DEVICE, 0x06, /* LIBUSB_REQUEST_GET_DESCRIPTOR */ value << 8, 0x00, data, sizeof(data), &sz, 1000, NULL, error)) { g_prefix_error(error, "failed to get USB descriptor: "); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "HUB_DT", data, sz); /* for USB 3: size is fixed as max ports is 15, * for USB 2: size is variable as max ports is 255 */ if (fu_usb_device_get_spec(self) >= 0x0300 && sz == 0x0C) { g_string_append_printf(hub, "%02X", data[0x0B]); g_string_append_printf(hub, "%02X", data[0x0A]); } else if (sz >= 9) { guint8 numbytes = fu_common_align_up(data[2] + 1, 0x03) / 8; for (guint i = 0; i < numbytes; i++) { guint8 tmp = 0x0; if (!fu_memread_uint8_safe(data, sz, 7 + i, &tmp, error)) return FALSE; g_string_append_printf(hub, "%02X", tmp); } } if (hub->len > 0) fu_device_add_instance_str(FU_DEVICE(self), "HUB", hub->str); return fu_device_build_instance_id_full(FU_DEVICE(self), FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_VISIBLE | FU_DEVICE_INSTANCE_FLAG_QUIRKS, error, "USB", "VID", "PID", "HUB", NULL); } static gboolean fu_usb_device_open_internal(FuUsbDevice *self, GError **error) { FuContext *ctx = fu_device_get_context(FU_DEVICE(self)); FuUsbDevicePrivate *priv = GET_PRIVATE(self); libusb_context *usb_ctx = fu_context_get_data(ctx, "libusb_context"); gint rc; /* sanity check */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) return TRUE; if (priv->handle != NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "device %04x:%04x is already open", fu_device_get_vid(FU_DEVICE(self)), fu_device_get_pid(FU_DEVICE(self))); return FALSE; } /* libusb or kernel */ if (priv->usb_device != NULL) { rc = libusb_open(priv->usb_device, &priv->handle); } else { gint fd; FuIOChannel *io_channel = fu_udev_device_get_io_channel(FU_UDEV_DEVICE(self)); if (io_channel == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "no io channel"); return FALSE; } fd = fu_io_channel_unix_get_fd(io_channel); rc = libusb_wrap_sys_device(usb_ctx, fd, &priv->handle); } if (!fu_usb_device_libusb_error_to_gerror(rc, error)) { if (priv->handle != NULL) libusb_close(priv->handle); priv->handle = NULL; return FALSE; } /* success */ return TRUE; } static gboolean fu_usb_device_close_internal(FuUsbDevice *self, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) return TRUE; if (priv->handle == NULL) return fu_usb_device_not_open_error(self, error); libusb_close(priv->handle); priv->handle = NULL; return TRUE; } static gboolean fu_usb_device_set_configuration_internal(FuUsbDevice *self, gint configuration, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); gint rc; gint config_tmp = 0; g_return_val_if_fail(FU_IS_USB_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) return TRUE; if (priv->handle == NULL) return fu_usb_device_not_open_error(self, error); /* verify we've not already set the same configuration */ rc = libusb_get_configuration(priv->handle, &config_tmp); if (rc != LIBUSB_SUCCESS) return fu_usb_device_libusb_error_to_gerror(rc, error); if (config_tmp == configuration) return TRUE; /* different, so change */ rc = libusb_set_configuration(priv->handle, configuration); return fu_usb_device_libusb_error_to_gerror(rc, error); } static gboolean fu_usb_device_open(FuDevice *device, GError **error) { FuUsbDevice *self = FU_USB_DEVICE(device); FuUsbDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_USB_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* self tests */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) return TRUE; /* FuUdevDevice->open */ if (!FU_DEVICE_CLASS(fu_usb_device_parent_class)->open(device, error)) return FALSE; /* open */ if (!fu_usb_device_open_internal(self, error)) { g_prefix_error(error, "failed to open device: "); return FALSE; } /* if set */ if (priv->configuration >= 0) { if (!fu_usb_device_set_configuration_internal(self, priv->configuration, error)) { g_prefix_error(error, "failed to set configuration: "); return FALSE; } } /* claim interfaces */ for (guint i = 0; priv->device_interfaces != NULL && i < priv->device_interfaces->len; i++) { FuUsbDeviceInterface *iface = g_ptr_array_index(priv->device_interfaces, i); if (!fu_usb_device_claim_interface(self, iface->number, FU_USB_DEVICE_CLAIM_FLAG_KERNEL_DRIVER, error)) { g_prefix_error(error, "failed to claim interface 0x%02x: ", iface->number); return FALSE; } iface->claimed = TRUE; } return TRUE; } /** * fu_usb_device_get_bus: * @self: a #FuUsbDevice * * Gets the USB bus number for the device. * * Return value: The 8-bit bus number * * Since: 2.0.0 **/ guint8 fu_usb_device_get_bus(FuUsbDevice *self) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) return 0x0; return priv->busnum; } /** * fu_usb_device_get_address: * @self: a #FuUsbDevice * * Gets the USB address for the device. * * Return value: The 8-bit address * * Since: 2.0.0 **/ guint8 fu_usb_device_get_address(FuUsbDevice *self) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) return 0x0; return priv->devnum; } /** * fu_usb_device_get_manufacturer_index: * @self: a #FuUsbDevice * * Gets the index for the iManufacturer string descriptor. * * Return value: a string descriptor index. * * Since: 2.0.4 **/ guint8 fu_usb_device_get_manufacturer_index(FuUsbDevice *self) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_USB_DEVICE(self), 0); return priv->desc.iManufacturer; } /** * fu_usb_device_get_product_index: * @self: a #FuUsbDevice * * Gets the index for the iProduct string descriptor. * * Return value: a string descriptor index. * * Since: 2.0.4 **/ guint8 fu_usb_device_get_product_index(FuUsbDevice *self) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_USB_DEVICE(self), 0); return priv->desc.iProduct; } /* not defined in FreeBSD */ #ifndef HAVE_LIBUSB_GET_PARENT static libusb_device * libusb_get_parent(libusb_device *dev) /* nocheck:name */ { return NULL; } #endif static void fu_usb_device_build_parent_port_number(GString *str, libusb_device *dev) { libusb_device *parent = libusb_get_parent(dev); if (parent != NULL) fu_usb_device_build_parent_port_number(str, parent); g_string_append_printf(str, "%02x:", libusb_get_port_number(dev)); } static gchar * fu_usb_device_build_physical_id(struct libusb_device *dev) { GString *platform_id; /* build a topology of the device */ platform_id = g_string_new("usb:"); g_string_append_printf(platform_id, "%02x:", libusb_get_bus_number(dev)); fu_usb_device_build_parent_port_number(platform_id, dev); g_string_truncate(platform_id, platform_id->len - 1); return g_string_free(platform_id, FALSE); } static gboolean fu_usb_device_setup(FuDevice *device, GError **error) { FuUsbDevice *self = FU_USB_DEVICE(device); g_return_val_if_fail(FU_IS_USB_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* get vendor */ if (fu_device_get_vendor(device) == NULL) { guint idx = fu_usb_device_get_manufacturer_index(self); if (idx != 0x00) { g_autofree gchar *tmp = NULL; g_autoptr(GError) error_local = NULL; tmp = fu_usb_device_get_string_descriptor(self, idx, &error_local); if (tmp != NULL) fu_device_set_vendor(device, g_strchomp(tmp)); else g_debug( "failed to load manufacturer string for usb device %u:%u: %s", fu_usb_device_get_bus(self), fu_usb_device_get_address(self), error_local->message); } } /* get product */ if (fu_device_get_name(device) == NULL) { guint idx = fu_usb_device_get_product_index(self); if (idx != 0x00) { g_autofree gchar *tmp = NULL; g_autoptr(GError) error_local = NULL; tmp = fu_usb_device_get_string_descriptor(self, idx, &error_local); if (tmp != NULL) fu_device_set_name(device, g_strchomp(tmp)); else g_debug("failed to load product string for usb device %u:%u: %s", fu_usb_device_get_bus(self), fu_usb_device_get_address(self), error_local->message); } } /* get serial number */ if (!fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_NO_SERIAL_NUMBER) && fu_device_get_serial(device) == NULL) { guint idx = fu_usb_device_get_serial_number_index(self); if (idx != 0x00) { g_autofree gchar *tmp = NULL; g_autoptr(GError) error_local = NULL; tmp = fu_usb_device_get_string_descriptor(self, idx, &error_local); if (tmp != NULL) fu_device_set_serial(device, g_strchomp(tmp)); else g_debug( "failed to load serial number string for usb device %u:%u: %s", fu_usb_device_get_bus(self), fu_usb_device_get_address(self), error_local->message); } } /* get the hub descriptor if this is a hub */ if (fu_usb_device_get_class(self) == FU_USB_CLASS_HUB) { if (!fu_usb_device_query_hub(self, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_usb_device_ready(FuDevice *device, GError **error) { FuUsbDevice *self = FU_USB_DEVICE(device); g_autoptr(GPtrArray) intfs = NULL; /* self tests */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) return TRUE; /* get the interface GUIDs */ intfs = fu_usb_device_get_interfaces(self, error); if (intfs == NULL) { g_prefix_error(error, "failed to get interfaces: "); return FALSE; } /* add fallback icon if there is nothing added already */ if (fu_device_get_icons(device)->len == 0) { for (guint i = 0; i < intfs->len; i++) { FuUsbInterface *intf = g_ptr_array_index(intfs, i); /* Video: Video Control: i.e. a webcam */ if (fu_usb_interface_get_class(intf) == FU_USB_CLASS_VIDEO && fu_usb_interface_get_subclass(intf) == 0x01) { fu_device_add_icon(device, "camera-web"); } /* Audio */ if (fu_usb_interface_get_class(intf) == FU_USB_CLASS_AUDIO) fu_device_add_icon(device, "audio-card"); /* Mass Storage */ if (fu_usb_interface_get_class(intf) == FU_USB_CLASS_MASS_STORAGE) fu_device_add_icon(device, "drive-harddisk"); /* Printer */ if (fu_usb_interface_get_class(intf) == FU_USB_CLASS_PRINTER) fu_device_add_icon(device, "printer"); } } /* success */ return TRUE; } static gboolean fu_usb_device_close(FuDevice *device, GError **error) { FuUsbDevice *self = FU_USB_DEVICE(device); FuUsbDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_USB_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* release interfaces, ignoring errors */ for (guint i = 0; priv->device_interfaces != NULL && i < priv->device_interfaces->len; i++) { FuUsbDeviceInterface *iface = g_ptr_array_index(priv->device_interfaces, i); FuUsbDeviceClaimFlags claim_flags = FU_USB_DEVICE_CLAIM_FLAG_NONE; g_autoptr(GError) error_local = NULL; if (!iface->claimed) continue; if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG)) { g_debug("re-binding kernel driver as not waiting for replug"); claim_flags |= FU_USB_DEVICE_CLAIM_FLAG_KERNEL_DRIVER; } if (!fu_usb_device_release_interface(self, iface->number, claim_flags, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL)) { g_debug("failed to release interface 0x%02x: %s", iface->number, error_local->message); } else { g_warning("failed to release interface 0x%02x: %s", iface->number, error_local->message); } } iface->claimed = FALSE; } /* success */ if (!fu_usb_device_close_internal(self, error)) return FALSE; /* FuUdevDevice->close */ return FU_DEVICE_CLASS(fu_usb_device_parent_class)->close(device, error); } static gboolean fu_usb_device_probe_bos_descriptor(FuUsbDevice *self, FuUsbBosDescriptor *bos, GError **error) { g_autofree gchar *str = NULL; g_autoptr(FuFirmware) ds20 = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(FuDeviceLocker) usb_locker = NULL; /* parse either type */ stream = fu_firmware_get_image_by_id_stream(FU_FIRMWARE(bos), FU_FIRMWARE_ID_PAYLOAD, error); if (stream == NULL) return FALSE; ds20 = fu_firmware_new_from_gtypes(stream, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, error, FU_TYPE_USB_DEVICE_FW_DS20, FU_TYPE_USB_DEVICE_MS_DS20, G_TYPE_INVALID); if (ds20 == NULL) { g_prefix_error(error, "failed to parse: "); return FALSE; } str = fu_firmware_to_string(ds20); g_debug("DS20: %s", str); /* Microsoft descriptors are not useful at the moment */ if (FU_IS_USB_DEVICE_MS_DS20(ds20)) return TRUE; /* set the quirks onto the device */ usb_locker = fu_device_locker_new_full(self, (FuDeviceLockerFunc)fu_usb_device_open, (FuDeviceLockerFunc)fu_usb_device_close, error); if (usb_locker == NULL) return FALSE; if (!fu_usb_device_ds20_apply_to_device(FU_USB_DEVICE_DS20(ds20), self, error)) { g_prefix_error(error, "failed to apply DS20 data: "); return FALSE; } /* success */ return TRUE; } static GInputStream * fu_usb_device_load_descriptor_stream(FuUsbDevice *self, const gchar *basename, GError **error) { FuDeviceEvent *event = NULL; g_autofree gchar *event_id = NULL; g_autofree gchar *fn = NULL; /* build event key either for load or save */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_strdup_printf("LoadDescriptor:basename=%s", basename); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { g_autoptr(GBytes) data = NULL; /* lots of old emulations don't have this, returning FWUPD_ERROR_NOT_FOUND */ event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return NULL; data = fu_device_event_get_bytes(event, "Data", error); if (data == NULL) return NULL; return g_memory_input_stream_new_from_bytes(data); } /* save */ if (fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event = fu_device_save_event(FU_DEVICE(self), event_id); } /* kernel weirdness -- fseek(fd, 0L, SEEK_END) always gives us 0x10011 */ fn = g_build_filename(fu_device_get_backend_id(FU_DEVICE(self)), basename, NULL); if (!g_file_test(fn, G_FILE_TEST_EXISTS)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no descriptors, expected %s", fn); return NULL; } /* save */ if (event != NULL) { g_autoptr(GBytes) data = fu_bytes_get_contents(fn, error); if (data == NULL) return NULL; fu_device_event_set_bytes(event, "Data", data); } /* success */ return fu_input_stream_from_path(fn, error); } static gboolean fu_usb_device_parse_bos_descriptor(FuUsbDevice *self, GInputStream *stream, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(FuFirmware) firmware = fu_linear_firmware_new(FU_TYPE_USB_BOS_DESCRIPTOR); g_autoptr(GError) error_local = NULL; g_autoptr(GPtrArray) imgs = NULL; if (!fu_firmware_parse_stream(firmware, stream, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA)) { g_debug("ignoring: %s", error_local->message); } else { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } /* copy from container */ imgs = fu_firmware_get_images(firmware); for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); g_ptr_array_add(priv->bos_descriptors, g_object_ref(img)); } /* success */ return TRUE; } static gboolean fu_usb_device_ensure_bos_descriptors(FuUsbDevice *self, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); /* already set */ if (priv->bos_descriptors_valid) return TRUE; /* libusb or kernel */ if (priv->usb_device != NULL) { gint rc; guint8 num_device_caps; struct libusb_bos_descriptor *bos = NULL; g_autoptr(FuDeviceLocker) usb_locker = NULL; /* not supported, so there is no point opening */ if (fu_usb_device_get_spec(self) <= 0x0200) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not available as bcdUSB 0x%04x <= 0x0200", fu_usb_device_get_spec(self)); return FALSE; } usb_locker = fu_device_locker_new(self, error); if (usb_locker == NULL) return FALSE; if (priv->handle == NULL) { fu_usb_device_not_open_error(self, error); return FALSE; } rc = libusb_get_bos_descriptor(priv->handle, &bos); if (!fu_usb_device_libusb_error_to_gerror(rc, error)) return FALSE; #ifdef __FreeBSD__ num_device_caps = bos->bNumDeviceCapabilities; #else num_device_caps = bos->bNumDeviceCaps; #endif for (guint i = 0; i < num_device_caps; i++) { FuUsbBosDescriptor *bos_descriptor = NULL; struct libusb_bos_dev_capability_descriptor *bos_cap = bos->dev_capability[i]; bos_descriptor = fu_usb_bos_descriptor_new(bos_cap); g_ptr_array_add(priv->bos_descriptors, bos_descriptor); } libusb_free_bos_descriptor(bos); } else { g_autoptr(GError) error_local = NULL; g_autoptr(GInputStream) stream = NULL; /* this is optional */ stream = fu_usb_device_load_descriptor_stream(self, "bos_descriptors", &error_local); if (stream == NULL) { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED) && !g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } else { if (!fu_usb_device_parse_bos_descriptor(self, stream, error)) return FALSE; } } priv->bos_descriptors_valid = TRUE; return TRUE; } static gboolean fu_usb_device_probe_bos_descriptors(FuUsbDevice *self, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(GError) error_local = NULL; /* already matched a quirk entry */ if (fu_device_has_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_NO_PROBE)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not probing"); return FALSE; } if (!fu_usb_device_ensure_bos_descriptors(self, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_READ) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL)) { g_debug("ignoring missing BOS descriptor: %s", error_local->message); return TRUE; } g_propagate_error(error, g_steal_pointer(&error_local)); fu_error_convert(error); return FALSE; } for (guint i = 0; i < priv->bos_descriptors->len; i++) { FuUsbBosDescriptor *bos = g_ptr_array_index(priv->bos_descriptors, i); g_autoptr(GError) error_loop = NULL; if (fu_usb_bos_descriptor_get_capability(bos) != 0x5) continue; if (!fu_usb_device_probe_bos_descriptor(self, bos, &error_loop)) { #ifdef SUPPORTED_BUILD g_debug("failed to parse platform BOS descriptor: %s", error_loop->message); #else g_warning("failed to parse platform BOS descriptor: %s", error_loop->message); #endif } } return TRUE; } static gboolean fu_usb_device_probe_internal(FuUsbDevice *self, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); /* libusb or kernel */ if (priv->usb_device != NULL) { gint rc; rc = libusb_get_device_descriptor(priv->usb_device, &priv->desc); if (rc != LIBUSB_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to get USB descriptor for device: %s", libusb_strerror(rc)); return FALSE; } priv->busnum = libusb_get_bus_number(priv->usb_device); priv->devnum = libusb_get_device_address(priv->usb_device); fu_device_set_vid(FU_DEVICE(self), priv->desc.idVendor); fu_device_set_pid(FU_DEVICE(self), priv->desc.idProduct); } else { guint64 busnum = 0; guint64 devnum = 0; g_autofree gchar *busnum_str = NULL; g_autofree gchar *devnum_str = NULL; g_autofree gchar *device_file = NULL; /* get bus number */ busnum_str = fu_udev_device_read_property(FU_UDEV_DEVICE(self), "BUSNUM", error); if (busnum_str == NULL) return FALSE; if (!fu_strtoull(busnum_str, &busnum, 0, G_MAXUINT8, FU_INTEGER_BASE_10, error)) return FALSE; priv->busnum = (guint8)busnum; /* get device address */ devnum_str = fu_udev_device_read_property(FU_UDEV_DEVICE(self), "DEVNUM", error); if (devnum_str == NULL) return FALSE; if (!fu_strtoull(devnum_str, &devnum, 0, G_MAXUINT8, FU_INTEGER_BASE_10, error)) return FALSE; priv->devnum = (guint8)devnum; /* load descriptors */ if (!fu_usb_device_ensure_interfaces(self, error)) return FALSE; if (!fu_usb_device_ensure_bos_descriptors(self, error)) return FALSE; /* we have to open a fd for libusb */ device_file = g_strdup_printf("/dev/bus/usb/%03u/%03u", priv->busnum, priv->devnum); fu_udev_device_set_device_file(FU_UDEV_DEVICE(self), device_file); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); } /* this does not change on plug->unplug->plug */ if (priv->usb_device != NULL) { g_autofree gchar *platform_id = fu_usb_device_build_physical_id(priv->usb_device); fu_device_set_physical_id(FU_DEVICE(self), platform_id); } else { g_autofree gchar *platform_id = g_path_get_basename(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(self))); fu_device_set_physical_id(FU_DEVICE(self), platform_id); } /* success */ return TRUE; } static gboolean fu_usb_device_probe(FuDevice *device, GError **error) { FuUsbDevice *self = FU_USB_DEVICE(device); guint16 release; g_autoptr(GError) error_bos = NULL; g_autoptr(GPtrArray) intfs = NULL; /* load hardware info */ if (!fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { if (!fu_usb_device_probe_internal(self, error)) return FALSE; } /* set vendor ID */ fu_device_build_vendor_id_u16(device, "USB", fu_device_get_vid(device)); /* set the version if the release has been set */ release = fu_usb_device_get_release(self); if (release != 0x0 && fu_device_get_version_format(device) == FWUPD_VERSION_FORMAT_UNKNOWN) { fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_BCD); fu_device_set_version_raw(device, release); } /* add GUIDs in order of priority */ fu_device_add_instance_u16(device, "REV", release); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "USB", "VID", NULL); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_VISIBLE | FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "USB", "VID", "PID", NULL); if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_ADD_INSTANCE_ID_REV)) { fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_VISIBLE | FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "USB", "VID", "PID", "REV", NULL); } /* add the interface GUIDs */ intfs = fu_usb_device_get_interfaces(self, error); if (intfs == NULL) { g_prefix_error(error, "failed to get interfaces: "); return FALSE; } for (guint i = 0; i < intfs->len; i++) { FuUsbInterface *intf = g_ptr_array_index(intfs, i); fu_device_add_instance_u8(device, "CLASS", fu_usb_interface_get_class(intf)); fu_device_add_instance_u8(device, "SUBCLASS", fu_usb_interface_get_subclass(intf)); fu_device_add_instance_u8(device, "PROT", fu_usb_interface_get_protocol(intf)); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "USB", "CLASS", NULL); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "USB", "CLASS", "SUBCLASS", NULL); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_GENERIC | FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "USB", "CLASS", "SUBCLASS", "PROT", NULL); } /* add 2 levels of parent IDs */ if (fu_device_get_physical_id(device) != NULL) { g_autofree gchar *platform_id_tmp = g_strdup(fu_device_get_physical_id(device)); for (guint i = 0; i < 2; i++) { gchar *tok = g_strrstr(platform_id_tmp, ":"); if (tok == NULL) break; *tok = '\0'; if (g_strcmp0(platform_id_tmp, "usb") == 0) break; fu_device_add_parent_physical_id(device, platform_id_tmp); } } /* parse the platform capability BOS descriptors for quirks */ if (!fu_usb_device_probe_bos_descriptors(self, &error_bos)) { if (g_error_matches(error_bos, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_debug("ignoring: %s", error_bos->message); } else { g_warning("failed to load BOS descriptor from USB device: %s", error_bos->message); } } /* success */ return TRUE; } /** * fu_usb_device_get_release: * @self: a #FuUsbDevice * * Gets the device release. * * Returns: integer, or 0x0 if unset or invalid * * Since: 2.0.0 **/ guint16 fu_usb_device_get_release(FuUsbDevice *self) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_USB_DEVICE(self), 0x0000); return priv->desc.bcdDevice; } /** * fu_usb_device_get_spec: * @self: a #FuUsbDevice * * Gets the string USB revision for the device. * * Returns: a specification revision in BCD format, or 0x0 if not supported * * Since: 1.3.4 **/ guint16 fu_usb_device_get_spec(FuUsbDevice *self) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_USB_DEVICE(self), 0x0); return priv->desc.bcdUSB; } /** * fu_usb_device_get_class: * @self: a #FuUsbDevice * * Gets the device class, typically a #FuUsbClass. * * Return value: a #FuUsbClass, e.g. %FU_USB_CLASS_HUB. * * Since: 2.0.0 **/ FuUsbClass fu_usb_device_get_class(FuUsbDevice *self) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_USB_DEVICE(self), 0x0); return priv->desc.bDeviceClass; } static void fu_usb_device_add_interface_internal(FuUsbDevice *self, FuUsbInterface *iface) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); g_ptr_array_add(priv->interfaces, g_object_ref(iface)); } static void fu_usb_device_incorporate(FuDevice *device, FuDevice *device_donor) { FuUsbDevice *self = FU_USB_DEVICE(device); FuUsbDevice *donor = FU_USB_DEVICE(device_donor); FuUsbDevicePrivate *priv = GET_PRIVATE(self); FuUsbDevicePrivate *priv_donor = GET_PRIVATE(donor); fu_usb_device_set_dev(self, priv_donor->usb_device); /* all descriptor fields */ if (priv->desc.bLength == 0x0) memcpy(&priv->desc, &priv_donor->desc, sizeof(priv->desc)); /* nocheck:blocked */ if (priv->interfaces->len == 0 && priv_donor->interfaces_valid) { for (guint i = 0; i < priv_donor->interfaces->len; i++) { FuUsbInterface *iface = g_ptr_array_index(priv_donor->interfaces, i); g_ptr_array_add(priv->interfaces, g_object_ref(iface)); } priv->interfaces_valid = TRUE; } if (priv->bos_descriptors->len == 0 && priv_donor->bos_descriptors_valid) { for (guint i = 0; i < priv_donor->bos_descriptors->len; i++) { FuUsbBosDescriptor *bos_descriptor = g_ptr_array_index(priv_donor->bos_descriptors, i); g_ptr_array_add(priv->bos_descriptors, g_object_ref(bos_descriptor)); } priv->bos_descriptors_valid = TRUE; } if (priv->hid_descriptors->len == 0) { for (guint i = 0; i < priv_donor->hid_descriptors->len; i++) { FuUsbHidDescriptor *hid_descriptor = g_ptr_array_index(priv_donor->hid_descriptors, i); g_ptr_array_add(priv->hid_descriptors, g_object_ref(hid_descriptor)); } } if (priv->cfg_descriptors->len == 0) { for (guint i = 0; i < priv_donor->cfg_descriptors->len; i++) { FuUsbConfigDescriptor *cfg_descriptor = g_ptr_array_index(priv_donor->cfg_descriptors, i); g_ptr_array_add(priv->cfg_descriptors, g_object_ref(cfg_descriptor)); } } } static gchar * fu_usb_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint16(version_raw, fu_device_get_version_format(device)); } /** * fu_usb_device_control_transfer: * @self: a #FuUsbDevice * @request_type: the request type field for the setup packet * @request: the request field for the setup packet * @value: the value field for the setup packet * @idx: the index field for the setup packet * @data: (array length=length): a suitably-sized data buffer for * either input or output * @length: the length field for the setup packet. * @actual_length: (out) (optional): the actual number of bytes sent, or %NULL * @timeout: timeout timeout (in milliseconds) that this function should wait * before giving up due to no response being received. For an unlimited * timeout, use 0. * @cancellable: a #GCancellable, or %NULL * @error: a #GError, or %NULL * * Perform a USB control transfer. * * Warning: this function is synchronous, and cannot be cancelled. * * Return value: %TRUE on success * * Since: 2.0.0 **/ gboolean fu_usb_device_control_transfer(FuUsbDevice *self, FuUsbDirection direction, FuUsbRequestType request_type, FuUsbRecipient recipient, guint8 request, guint16 value, guint16 idx, guint8 *data, gsize length, gsize *actual_length, guint timeout, GCancellable *cancellable, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); gint rc; guint8 request_type_raw = 0; FuDeviceEvent *event = NULL; g_autofree gchar *event_id = NULL; g_return_val_if_fail(FU_IS_USB_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* build event key either for load or save */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { g_autofree gchar *data_base64 = g_base64_encode(data, length); event_id = g_strdup_printf("ControlTransfer:" "Direction=0x%02x," "RequestType=0x%02x," "Recipient=0x%02x," "Request=0x%02x," "Value=0x%04x," "Idx=0x%04x," "Data=%s," "Length=0x%x", direction, request_type, recipient, request, value, idx, data_base64, (guint)length); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { gint64 rc_tmp; event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return FALSE; rc_tmp = fu_device_event_get_i64(event, "Error", NULL); if (rc_tmp != G_MAXINT64) return fu_usb_device_libusb_error_to_gerror(rc_tmp, error); rc_tmp = fu_device_event_get_i64(event, "Status", NULL); if (rc_tmp != G_MAXINT64) return fu_usb_device_libusb_status_to_gerror(rc_tmp, error); return fu_device_event_copy_data(event, "Data", data, length, actual_length, error); } /* sanity check */ if (priv->handle == NULL) return fu_usb_device_not_open_error(self, error); /* save */ if (fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event = fu_device_save_event(FU_DEVICE(self), event_id); } /* munge back to flags */ if (direction == FU_USB_DIRECTION_DEVICE_TO_HOST) request_type_raw |= 0x80; request_type_raw |= (request_type << 5); request_type_raw |= recipient; /* sync request */ rc = libusb_control_transfer(priv->handle, request_type_raw, request, value, idx, data, length, timeout); if (rc < 0) { if (!fu_usb_device_libusb_error_to_gerror(rc, error)) { if (event != NULL) fu_device_event_set_i64(event, "Error", rc); return FALSE; } } if (actual_length != NULL) *actual_length = rc; /* save */ if (event != NULL) fu_device_event_set_data(event, "Data", data, rc); /* success */ return TRUE; } /** * fu_usb_device_bulk_transfer: * @self: a #FuUsbDevice * @endpoint: the address of a valid endpoint to communicate with * @data: (array length=length): a suitably-sized data buffer for * either input or output * @length: the length field for the setup packet. * @actual_length: (out) (optional): the actual number of bytes sent, or %NULL * @timeout: timeout timeout (in milliseconds) that this function should wait * before giving up due to no response being received. For an unlimited * timeout, use 0. * @cancellable: a #GCancellable, or %NULL * @error: a #GError, or %NULL * * Perform a USB bulk transfer. * * Warning: this function is synchronous, and cannot be cancelled. * * Return value: %TRUE on success * * Since: 2.0.0 **/ gboolean fu_usb_device_bulk_transfer(FuUsbDevice *self, guint8 endpoint, guint8 *data, gsize length, gsize *actual_length, guint timeout, GCancellable *cancellable, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); gint rc; gint transferred = 0; FuDeviceEvent *event = NULL; g_autofree gchar *event_id = NULL; g_return_val_if_fail(FU_IS_USB_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* build event key either for load or save */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { g_autofree gchar *data_base64 = g_base64_encode(data, length); event_id = g_strdup_printf("BulkTransfer:" "Endpoint=0x%02x," "Data=%s," "Length=0x%x", endpoint, data_base64, (guint)length); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { gint64 rc_tmp; event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return FALSE; rc_tmp = fu_device_event_get_i64(event, "Error", NULL); if (rc_tmp != G_MAXINT64) return fu_usb_device_libusb_error_to_gerror(rc_tmp, error); rc_tmp = fu_device_event_get_i64(event, "Status", NULL); if (rc_tmp != G_MAXINT64) return fu_usb_device_libusb_status_to_gerror(rc_tmp, error); return fu_device_event_copy_data(event, "Data", data, length, actual_length, error); } /* sanity check */ if (priv->handle == NULL) return fu_usb_device_not_open_error(self, error); /* save */ if (fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event = fu_device_save_event(FU_DEVICE(self), event_id); } /* sync request */ rc = libusb_bulk_transfer(priv->handle, endpoint, data, length, &transferred, timeout); if (!fu_usb_device_libusb_error_to_gerror(rc, error)) { if (event != NULL) fu_device_event_set_i64(event, "Error", rc); return FALSE; } if (actual_length != NULL) *actual_length = transferred; /* save */ if (event != NULL) fu_device_event_set_data(event, "Data", data, transferred); /* success */ return TRUE; } /** * fu_usb_device_interrupt_transfer: * @self: a #FuUsbDevice * @endpoint: the address of a valid endpoint to communicate with * @data: (array length=length): a suitably-sized data buffer for either input or output * @length: the length field for the setup packet. * @actual_length: (out) (optional): the actual number of bytes sent, or %NULL * @timeout: timeout (in milliseconds) that this function should wait -- use 0 for unlimited * @cancellable: a #GCancellable, or %NULL * @error: a #GError, or %NULL * * Perform a USB interrupt transfer. * * Warning: this function is synchronous, and cannot be cancelled. * * Return value: %TRUE on success * * Since: 2.0.0 **/ gboolean fu_usb_device_interrupt_transfer(FuUsbDevice *self, guint8 endpoint, guint8 *data, gsize length, gsize *actual_length, guint timeout, GCancellable *cancellable, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); gint rc; gint transferred = 0; FuDeviceEvent *event = NULL; g_autofree gchar *event_id = NULL; g_return_val_if_fail(FU_IS_USB_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* build event key either for load or save */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { g_autofree gchar *data_base64 = g_base64_encode(data, length); event_id = g_strdup_printf("InterruptTransfer:" "Endpoint=0x%02x," "Data=%s," "Length=0x%x", endpoint, data_base64, (guint)length); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { gint64 rc_tmp; event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return FALSE; rc_tmp = fu_device_event_get_i64(event, "Error", NULL); if (rc_tmp != G_MAXINT64) return fu_usb_device_libusb_error_to_gerror(rc_tmp, error); rc_tmp = fu_device_event_get_i64(event, "Status", NULL); if (rc_tmp != G_MAXINT64) return fu_usb_device_libusb_status_to_gerror(rc_tmp, error); return fu_device_event_copy_data(event, "Data", data, length, actual_length, error); } /* sanity check */ if (priv->handle == NULL) return fu_usb_device_not_open_error(self, error); /* save */ if (fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event = fu_device_save_event(FU_DEVICE(self), event_id); } /* sync request */ rc = libusb_interrupt_transfer(priv->handle, endpoint, data, length, &transferred, timeout); if (!fu_usb_device_libusb_error_to_gerror(rc, error)) { if (event != NULL) fu_device_event_set_i64(event, "Error", rc); return FALSE; } if (actual_length != NULL) *actual_length = transferred; /* save */ if (event != NULL) fu_device_event_set_data(event, "Data", data, transferred); /* success */ return TRUE; } /** * fu_usb_device_reset: * @self: a #FuUsbDevice * @error: a #GError, or %NULL * * Perform a USB port reset to reinitialize a device. * * If the reset succeeds, the device will appear to disconnected and reconnected. * This means the @self will no longer be valid and should be closed and * rediscovered. * * This is a blocking function which usually incurs a noticeable delay. * * Return value: %TRUE on success * * Since: 2.0.0 **/ gboolean fu_usb_device_reset(FuUsbDevice *self, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); gint rc; g_return_val_if_fail(FU_IS_USB_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* emulating? */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) return TRUE; if (priv->handle == NULL) return fu_usb_device_not_open_error(self, error); rc = libusb_reset_device(priv->handle); if (rc == LIBUSB_ERROR_NOT_FOUND) return TRUE; return fu_usb_device_libusb_error_to_gerror(rc, error); } static gboolean fu_usb_device_parse_descriptor(FuUsbDevice *self, GInputStream *stream, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); gsize offset = 0; gsize streamsz = 0; g_autoptr(FuUsbDeviceHdr) st = NULL; g_autoptr(FuUsbInterface) iface_last = NULL; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; st = fu_usb_device_hdr_parse_stream(stream, offset, error); if (st == NULL) return FALSE; priv->desc.bLength = fu_usb_device_hdr_get_length(st); priv->desc.bDescriptorType = FU_USB_DEVICE_HDR_DEFAULT_DESCRIPTOR_TYPE; priv->desc.bcdUSB = fu_usb_device_hdr_get_usb(st); priv->desc.bDeviceClass = fu_usb_device_hdr_get_device_class(st); priv->desc.bDeviceSubClass = fu_usb_device_hdr_get_device_sub_class(st); priv->desc.bDeviceProtocol = fu_usb_device_hdr_get_device_protocol(st); priv->desc.bMaxPacketSize0 = fu_usb_device_hdr_get_max_packet_size0(st); fu_device_set_vid(FU_DEVICE(self), fu_usb_device_hdr_get_vendor(st)); fu_device_set_pid(FU_DEVICE(self), fu_usb_device_hdr_get_product(st)); priv->desc.bcdDevice = fu_usb_device_hdr_get_device(st); priv->desc.iManufacturer = fu_usb_device_hdr_get_manufacturer_idx(st); priv->desc.iProduct = fu_usb_device_hdr_get_product_idx(st); priv->desc.iSerialNumber = fu_usb_device_hdr_get_serial_number_idx(st); priv->desc.bNumConfigurations = fu_usb_device_hdr_get_num_configurations(st); offset += fu_usb_device_hdr_get_length(st); while (offset < streamsz) { FuUsbDescriptorKind descriptor_kind; g_autoptr(FuUsbBaseHdr) st_base = NULL; g_autoptr(GError) error_local = NULL; /* this is common to all descriptor types */ st_base = fu_usb_base_hdr_parse_stream(stream, offset, &error_local); if (st_base == NULL) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE)) break; if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA)) break; g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } /* config, interface or endpoint */ descriptor_kind = fu_usb_base_hdr_get_descriptor_type(st_base); if (descriptor_kind == FU_USB_DESCRIPTOR_KIND_CONFIG) { g_autoptr(FuUsbConfigDescriptor) cfg_descriptor = fu_usb_config_descriptor_new(); if (!fu_firmware_parse_stream(FU_FIRMWARE(cfg_descriptor), stream, offset, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return FALSE; g_ptr_array_add(priv->cfg_descriptors, g_steal_pointer(&cfg_descriptor)); } else if (descriptor_kind == FU_USB_DESCRIPTOR_KIND_INTERFACE) { g_autoptr(FuUsbInterface) iface = g_object_new(FU_TYPE_USB_INTERFACE, NULL); if (!fu_firmware_parse_stream(FU_FIRMWARE(iface), stream, offset, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return FALSE; fu_usb_device_add_interface_internal(self, iface); /* the next descriptor is the custom one, so just add as a child */ if (fu_usb_interface_get_class(iface) == FU_USB_CLASS_APPLICATION_SPECIFIC) { g_autoptr(FuUsbDescriptor) img = g_object_new(FU_TYPE_USB_DESCRIPTOR, NULL); if (!fu_firmware_parse_stream( FU_FIRMWARE(img), stream, offset + fu_usb_base_hdr_get_length(st_base), FU_FIRMWARE_PARSE_FLAG_CACHE_BLOB, error)) return FALSE; if (!fu_firmware_add_image_full(FU_FIRMWARE(iface), FU_FIRMWARE(img), error)) return FALSE; offset += fu_firmware_get_size(FU_FIRMWARE(img)); } g_set_object(&iface_last, iface); } else if (descriptor_kind == FU_USB_DESCRIPTOR_KIND_ENDPOINT) { g_autoptr(FuUsbEndpoint) ep = g_object_new(FU_TYPE_USB_ENDPOINT, NULL); if (!fu_firmware_parse_stream(FU_FIRMWARE(ep), stream, offset, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return FALSE; if (iface_last == NULL) { g_warning("endpoint 0x%x without prior interface, ignoring", fu_usb_endpoint_get_number(ep)); } else { fu_usb_interface_add_endpoint(iface_last, ep); } } else if (descriptor_kind == FU_USB_DESCRIPTOR_KIND_HID) { g_autoptr(FuUsbHidDescriptor) hid_descriptor = fu_usb_hid_descriptor_new(); if (!fu_firmware_parse_stream(FU_FIRMWARE(hid_descriptor), stream, offset, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return FALSE; if (iface_last == NULL) { g_warning("hid descriptor without prior interface, ignoring"); } else { fu_usb_hid_descriptor_set_iface_number( hid_descriptor, fu_usb_interface_get_number(iface_last)); g_ptr_array_add(priv->hid_descriptors, g_steal_pointer(&hid_descriptor)); } } else { const gchar *str = fu_usb_descriptor_kind_to_string(descriptor_kind); g_debug("usb descriptor type 0x%x [%s] not processed", descriptor_kind, str != NULL ? str : "unknown"); } offset += fu_usb_base_hdr_get_length(st_base); } /* success */ return TRUE; } static gboolean fu_usb_device_ensure_interfaces(FuUsbDevice *self, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); gint rc; /* sanity check */ if (priv->interfaces_valid) return TRUE; /* libusb or kernel */ if (priv->usb_device != NULL) { struct libusb_config_descriptor *config; rc = libusb_get_active_config_descriptor(priv->usb_device, &config); if (!fu_usb_device_libusb_error_to_gerror(rc, error)) return FALSE; for (guint i = 0; i < config->bNumInterfaces; i++) { for (guint j = 0; j < (guint)config->interface[i].num_altsetting; j++) { const struct libusb_interface_descriptor *ifp = &config->interface[i].altsetting[j]; g_autoptr(FuUsbInterface) iface = fu_usb_interface_new(ifp, error); if (iface == NULL) return FALSE; fu_usb_device_add_interface_internal(self, iface); } } libusb_free_config_descriptor(config); } else { g_autoptr(GError) error_local = NULL; g_autoptr(GInputStream) stream = NULL; stream = fu_usb_device_load_descriptor_stream(self, "descriptors", &error_local); if (stream == NULL) { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } else { if (!fu_usb_device_parse_descriptor(self, stream, error)) return FALSE; } } priv->interfaces_valid = TRUE; return TRUE; } /** * fu_usb_device_get_interfaces: * @self: a #FuUsbDevice * @error: a #GError, or %NULL * * Gets all the interfaces exported by the device. * * Return value: (transfer container) (element-type FuUsbInterface): an array of interfaces or %NULL * * Since: 2.0.0 **/ GPtrArray * fu_usb_device_get_interfaces(FuUsbDevice *self, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_USB_DEVICE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); if (!fu_usb_device_ensure_interfaces(self, error)) return NULL; return g_ptr_array_ref(priv->interfaces); } /** * fu_usb_device_get_interface: * @self: a #FuUsbDevice * @class_id: a device class, e.g. 0xff for VENDOR * @subclass_id: a device subclass * @protocol_id: a protocol number * @error: a #GError, or %NULL * * Gets the first interface that matches the vendor class interface descriptor. * If you want to find all the interfaces that match (there may be other * 'alternate' interfaces you have to use fu_usb_device_get_interfaces() and * check each one manally. * * Return value: (transfer full): a #FuUsbInterface or %NULL for not found * * Since: 0.2.8 **/ FuUsbInterface * fu_usb_device_get_interface(FuUsbDevice *self, guint8 class_id, guint8 subclass_id, guint8 protocol_id, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_USB_DEVICE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* find the right data */ if (!fu_usb_device_ensure_interfaces(self, error)) return NULL; for (guint i = 0; i < priv->interfaces->len; i++) { FuUsbInterface *iface = g_ptr_array_index(priv->interfaces, i); if (fu_usb_interface_get_class(iface) != class_id) continue; if (fu_usb_interface_get_subclass(iface) != subclass_id) continue; if (fu_usb_interface_get_protocol(iface) != protocol_id) continue; return g_object_ref(iface); } /* nothing matched */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no interface for class 0x%02x, " "subclass 0x%02x and protocol 0x%02x", class_id, subclass_id, protocol_id); return NULL; } /** * fu_usb_device_get_string_descriptor: * @desc_index: the index for the string descriptor to retrieve * @error: a #GError, or %NULL * * Get a string descriptor from the device. The returned string should be freed * with g_free() when no longer needed. * * Return value: a newly-allocated string holding the descriptor, or NULL on error. * * Since: 2.0.0 **/ gchar * fu_usb_device_get_string_descriptor(FuUsbDevice *self, guint8 desc_index, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); FuDeviceEvent *event; gint rc; unsigned char buf[128] = {0}; g_autofree gchar *event_id = NULL; g_return_val_if_fail(FU_IS_USB_DEVICE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* build event key either for load or save */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_strdup_printf("GetStringDescriptor:DescIndex=0x%02x", desc_index); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { gint64 rc_tmp; g_autoptr(GBytes) bytes = NULL; event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return NULL; rc_tmp = fu_device_event_get_i64(event, "Error", NULL); if (rc_tmp != G_MAXINT64) { fu_usb_device_libusb_error_to_gerror(rc_tmp, error); return NULL; } rc_tmp = fu_device_event_get_i64(event, "Status", NULL); if (rc_tmp != G_MAXINT64) { fu_usb_device_libusb_status_to_gerror(rc_tmp, error); return NULL; } bytes = fu_device_event_get_bytes(event, "Data", error); if (bytes == NULL) return NULL; return g_strndup(g_bytes_get_data(bytes, NULL), g_bytes_get_size(bytes)); } if (priv->handle == NULL) { fu_usb_device_not_open_error(self, error); return NULL; } rc = libusb_get_string_descriptor_ascii(priv->handle, desc_index, buf, sizeof(buf)); if (rc < 0) { fu_usb_device_libusb_error_to_gerror(rc, error); return NULL; } /* save */ if (fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event = fu_device_save_event(FU_DEVICE(self), event_id); fu_device_event_set_data(event, "Data", buf, sizeof(buf)); } return g_strndup((const gchar *)buf, sizeof(buf)); } /** * fu_usb_device_get_string_descriptor_bytes: * @desc_index: the index for the string descriptor to retrieve * @langid: the language ID * @error: a #GError, or %NULL * * Get a raw string descriptor from the device. The returned string should be freed * with g_bytes_unref() when no longer needed. * The descriptor will be at most 128 btes in length, if you need to * issue a request with either a smaller or larger descriptor, you can * use fu_usb_device_get_string_descriptor_bytes_full instead. * * Return value: (transfer full): a possibly UTF-16 string, or NULL on error. * * Since: 2.0.0 **/ GBytes * fu_usb_device_get_string_descriptor_bytes(FuUsbDevice *self, guint8 desc_index, guint16 langid, GError **error) { g_return_val_if_fail(FU_IS_USB_DEVICE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return fu_usb_device_get_string_descriptor_bytes_full(self, desc_index, langid, 128, error); } /** * fu_usb_device_get_string_descriptor_bytes_full: * @desc_index: the index for the string descriptor to retrieve * @langid: the language ID * @length: size of the request data buffer * @error: a #GError, or %NULL * * Get a raw string descriptor from the device. The returned string should be freed * with g_bytes_unref() when no longer needed. * * Return value: (transfer full): a possibly UTF-16 string, or NULL on error. * * Since: 2.0.0 **/ GBytes * fu_usb_device_get_string_descriptor_bytes_full(FuUsbDevice *self, guint8 desc_index, guint16 langid, gsize length, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); FuDeviceEvent *event; gint rc; g_autofree gchar *event_id = NULL; g_autofree guint8 *buf = g_malloc0(length); g_return_val_if_fail(FU_IS_USB_DEVICE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* emulating? */ /* build event key either for load or save */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_strdup_printf( "GetStringDescriptorBytes:DescIndex=0x%02x,Langid=0x%04x,Length=0x%x", desc_index, langid, (guint)length); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { gint64 rc_tmp; g_autoptr(GBytes) bytes = NULL; event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return NULL; rc_tmp = fu_device_event_get_i64(event, "Error", NULL); if (rc_tmp != G_MAXINT64) { fu_usb_device_libusb_error_to_gerror(rc_tmp, error); return NULL; } rc_tmp = fu_device_event_get_i64(event, "Status", NULL); if (rc_tmp != G_MAXINT64) { fu_usb_device_libusb_status_to_gerror(rc_tmp, error); return NULL; } bytes = fu_device_event_get_bytes(event, "Data", error); if (bytes == NULL) return NULL; return g_steal_pointer(&bytes); } if (priv->handle == NULL) { fu_usb_device_not_open_error(self, error); return NULL; } rc = libusb_get_string_descriptor(priv->handle, desc_index, langid, buf, length); if (rc < 0) { fu_usb_device_libusb_error_to_gerror(rc, error); return NULL; } /* save */ if (fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event = fu_device_save_event(FU_DEVICE(self), event_id); fu_device_event_set_data(event, "Data", buf, rc); } /* success */ return g_bytes_new(buf, rc); } static gboolean fu_usb_device_claim_interface_internal(FuUsbDevice *self, guint8 iface, FuUsbDeviceClaimFlags flags, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); gint rc; if (flags & FU_USB_DEVICE_CLAIM_FLAG_KERNEL_DRIVER) { rc = libusb_detach_kernel_driver(priv->handle, iface); if (rc != LIBUSB_SUCCESS && rc != LIBUSB_ERROR_NOT_FOUND && /* No driver attached */ rc != LIBUSB_ERROR_NOT_SUPPORTED && /* win32 */ rc != LIBUSB_ERROR_BUSY /* driver rebound already */) return fu_usb_device_libusb_error_to_gerror(rc, error); } rc = libusb_claim_interface(priv->handle, iface); return fu_usb_device_libusb_error_to_gerror(rc, error); } typedef struct { guint8 iface; FuUsbDeviceClaimFlags flags; } FuUsbDeviceClaimHelper; static gboolean fu_usb_device_claim_interface_cb(FuDevice *device, gpointer user_data, GError **error) { FuUsbDevice *self = FU_USB_DEVICE(device); FuUsbDeviceClaimHelper *helper = (FuUsbDeviceClaimHelper *)user_data; return fu_usb_device_claim_interface_internal(self, helper->iface, helper->flags, error); } /** * fu_usb_device_claim_interface: * @self: a #FuUsbDevice * @iface: bInterfaceNumber of the interface you wish to claim * @flags: #FuUsbDeviceClaimFlags * @error: a #GError, or %NULL * * Claim an interface of the device. * * Return value: %TRUE on success * * Since: 2.0.0 **/ gboolean fu_usb_device_claim_interface(FuUsbDevice *self, guint8 iface, FuUsbDeviceClaimFlags flags, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_USB_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* emulating? */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) return TRUE; if (priv->handle == NULL) return fu_usb_device_not_open_error(self, error); if (priv->claim_retry_count > 0) { FuUsbDeviceClaimHelper helper = {.iface = iface, .flags = flags}; return fu_device_retry_full(FU_DEVICE(self), fu_usb_device_claim_interface_cb, priv->claim_retry_count, FU_DEVICE_CLAIM_INTERFACE_DELAY, &helper, error); } return fu_usb_device_claim_interface_internal(self, iface, flags, error); } /** * fu_usb_device_release_interface: * @self: a #FuUsbDevice * @iface: bInterfaceNumber of the interface you wish to release * @flags: #FuUsbDeviceClaimFlags * @error: a #GError, or %NULL * * Release an interface of the device. * * Return value: %TRUE on success * * Since: 2.0.0 **/ gboolean fu_usb_device_release_interface(FuUsbDevice *self, guint8 iface, FuUsbDeviceClaimFlags flags, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); gint rc; g_return_val_if_fail(FU_IS_USB_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* emulating? */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) return TRUE; if (priv->handle == NULL) return fu_usb_device_not_open_error(self, error); rc = libusb_release_interface(priv->handle, iface); if (rc != LIBUSB_SUCCESS) return fu_usb_device_libusb_error_to_gerror(rc, error); if (flags & FU_USB_DEVICE_CLAIM_FLAG_KERNEL_DRIVER) { rc = libusb_attach_kernel_driver(priv->handle, iface); if (rc != LIBUSB_SUCCESS && rc != LIBUSB_ERROR_NOT_FOUND && /* No driver attached */ rc != LIBUSB_ERROR_NOT_SUPPORTED && /* win32 */ rc != LIBUSB_ERROR_BUSY /* driver rebound already */) return fu_usb_device_libusb_error_to_gerror(rc, error); } return TRUE; } static FuUsbConfigDescriptor * fu_usb_device_get_config_descriptor_for_value(FuUsbDevice *self, guint8 configuration_value, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); if (!fu_usb_device_ensure_interfaces(self, error)) return NULL; for (guint i = 0; i < priv->cfg_descriptors->len; i++) { FuUsbConfigDescriptor *cfg_descriptor = g_ptr_array_index(priv->cfg_descriptors, i); if (fu_usb_config_descriptor_get_configuration_value(cfg_descriptor) == configuration_value) return cfg_descriptor; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no configuration for value 0x%x", configuration_value); return NULL; } /** * fu_usb_device_get_configuration_index * @self: a #FuUsbDevice * @error: a #GError, or %NULL * * Get the index for the active Configuration string descriptor * ie, iConfiguration. * * Return value: a string descriptor index, or 0x0 on error * * Since: 2.0.0 **/ guint8 fu_usb_device_get_configuration_index(FuUsbDevice *self, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); FuDeviceEvent *event = NULL; guint8 index; g_autofree gchar *event_id = NULL; g_return_val_if_fail(FU_IS_USB_DEVICE(self), 0x0); /* build event key either for load or save */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_strdup_printf("GetConfigurationIndex"); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { g_autoptr(GBytes) bytes = NULL; event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return 0x0; bytes = fu_device_event_get_bytes(event, "Data", error); if (bytes == NULL) return 0x0; if (g_bytes_get_size(bytes) != 1) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no correct event data for %s", event_id); return 0x0; } return ((const guint8 *)g_bytes_get_data(bytes, NULL))[0]; } /* libusb or kernel */ if (priv->usb_device != NULL) { gint rc; struct libusb_config_descriptor *config; rc = libusb_get_active_config_descriptor(priv->usb_device, &config); if (rc != LIBUSB_SUCCESS) return fu_usb_device_libusb_error_to_gerror(rc, error); index = config->iConfiguration; libusb_free_config_descriptor(config); } else { FuUsbConfigDescriptor *cfg_descriptor; guint64 configuration = 0; g_autofree gchar *configuration_str = NULL; configuration_str = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(self), "bConfigurationValue", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, error); if (configuration_str == NULL) return 0x0; if (!fu_strtoull(configuration_str, &configuration, 0, G_MAXUINT8, FU_INTEGER_BASE_10, error)) return 0x0; /* lookup the correct configuration for the configuration_value */ cfg_descriptor = fu_usb_device_get_config_descriptor_for_value(self, configuration, error); if (cfg_descriptor == NULL) return 0x0; index = fu_usb_config_descriptor_get_configuration(cfg_descriptor); if (index == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid configuration for value 0x%x", (guint)configuration); return 0x0; } } /* save */ if (fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event = fu_device_save_event(FU_DEVICE(self), event_id); fu_device_event_set_data(event, "Data", &index, sizeof(index)); } return index; } /** * fu_usb_device_get_serial_number_index: * @self: a #FuUsbDevice * * Gets the index for the Serial Number string descriptor. * * Return value: a string descriptor index. * * Since: 2.0.0 **/ guint8 fu_usb_device_get_serial_number_index(FuUsbDevice *self) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_USB_DEVICE(self), 0x0); return priv->desc.iSerialNumber; } /** * fu_usb_device_get_custom_index: * @self: a #FuUsbDevice * @class_id: a device class, e.g. 0xff for VENDOR * @subclass_id: a device subclass * @protocol_id: a protocol number * @error: a #GError, or %NULL * * Gets the string index from the vendor class interface descriptor. * * Return value: a non-zero index, or 0x00 for failure * * Since: 2.0.0 **/ guint8 fu_usb_device_get_custom_index(FuUsbDevice *self, guint8 class_id, guint8 subclass_id, guint8 protocol_id, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); FuDeviceEvent *event; gint rc; guint8 idx = 0x00; g_autofree gchar *event_id = NULL; g_return_val_if_fail(FU_IS_USB_DEVICE(self), 0x0); g_return_val_if_fail(error == NULL || *error == NULL, 0x0); /* build event key either for load or save */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_strdup_printf( "GetCustomIndex:ClassId=0x%02x,SubclassId=0x%02x,ProtocolId=0x%02x", class_id, subclass_id, protocol_id); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { gint64 rc_tmp; g_autoptr(GBytes) bytes = NULL; event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return 0x00; rc_tmp = fu_device_event_get_i64(event, "Error", NULL); if (rc_tmp != G_MAXINT64) return fu_usb_device_libusb_error_to_gerror(rc_tmp, error); rc_tmp = fu_device_event_get_i64(event, "Status", NULL); if (rc_tmp != G_MAXINT64) return fu_usb_device_libusb_status_to_gerror(rc_tmp, error); bytes = fu_device_event_get_bytes(event, "Data", error); if (bytes == NULL) return 0x00; if (g_bytes_get_size(bytes) != 1) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no correct event data for %s", event_id); return 0x00; } return ((const guint8 *)g_bytes_get_data(bytes, NULL))[0]; } /* libusb or kernel */ if (priv->usb_device != NULL) { struct libusb_config_descriptor *config; rc = libusb_get_active_config_descriptor(priv->usb_device, &config); if (!fu_usb_device_libusb_error_to_gerror(rc, error)) return 0x00; /* find the right data */ for (guint i = 0; i < config->bNumInterfaces; i++) { const struct libusb_interface_descriptor *ifp = &config->interface[i].altsetting[0]; if (ifp->bInterfaceClass != class_id) continue; if (ifp->bInterfaceSubClass != subclass_id) continue; if (ifp->bInterfaceProtocol != protocol_id) continue; idx = ifp->iInterface; break; } libusb_free_config_descriptor(config); } else { g_autoptr(FuUsbInterface) iface = NULL; iface = fu_usb_device_get_interface(self, class_id, subclass_id, protocol_id, error); if (iface == NULL) return 0x00; idx = fu_usb_interface_get_index(iface); } /* nothing matched */ if (idx == 0x00) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no vendor descriptor for class 0x%02x, " "subclass 0x%02x and protocol 0x%02x", class_id, subclass_id, protocol_id); return 0x0; } /* save */ if (fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event = fu_device_save_event(FU_DEVICE(self), event_id); fu_device_event_set_data(event, "Data", &idx, sizeof(idx)); } /* success */ return idx; } /** * fu_usb_device_set_interface_alt: * @self: a #FuUsbDevice * @iface: bInterfaceNumber of the interface you wish to release * @alt: alternative setting number * @error: a #GError, or %NULL * * Sets an alternate setting on an interface. * * Return value: %TRUE on success * * Since: 2.0.0 **/ gboolean fu_usb_device_set_interface_alt(FuUsbDevice *self, guint8 iface, guint8 alt, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); guint8 rc; g_return_val_if_fail(FU_IS_USB_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* emulating? */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) return TRUE; if (priv->handle == NULL) return fu_usb_device_not_open_error(self, error); rc = libusb_set_interface_alt_setting(priv->handle, (gint)iface, (gint)alt); return fu_usb_device_libusb_error_to_gerror(rc, error); } static gboolean fu_usb_device_ensure_hid_descriptor(FuUsbDevice *self, FuUsbHidDescriptor *hid_descriptor, GError **error) { gsize actual_length = 0; gsize bufsz = fu_usb_hid_descriptor_get_descriptor_length(hid_descriptor); g_autofree guint8 *buf = NULL; g_autoptr(GBytes) blob = NULL; /* already set */ if (fu_usb_hid_descriptor_get_blob(hid_descriptor) != NULL) return TRUE; /* get HID descriptor */ buf = g_malloc0(bufsz); g_debug("get 0x%x bytes of HID descriptor on iface 0x%x", (guint)bufsz, fu_usb_hid_descriptor_get_iface_number(hid_descriptor)); if (!fu_usb_device_control_transfer(self, FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_STANDARD, FU_USB_RECIPIENT_INTERFACE, LIBUSB_REQUEST_GET_DESCRIPTOR, LIBUSB_DT_REPORT << 8, fu_usb_hid_descriptor_get_iface_number(hid_descriptor), buf, bufsz, &actual_length, 5000, NULL, error)) { g_prefix_error(error, "failed to get HID report descriptor: "); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "HidDescriptor", buf, bufsz); if (actual_length < bufsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "invalid data on HID interface 0x%x, got 0x%x and expected 0x%x", fu_usb_hid_descriptor_get_iface_number(hid_descriptor), (guint)actual_length, (guint)bufsz); return FALSE; } /* success */ blob = g_bytes_new_take(g_steal_pointer(&buf), actual_length); fu_usb_hid_descriptor_set_blob(hid_descriptor, blob); return TRUE; } static gboolean fu_usb_device_ensure_hid_descriptors(FuUsbDevice *self, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) return TRUE; if (priv->handle == NULL) { fu_usb_device_not_open_error(self, error); return FALSE; } if (!fu_usb_device_ensure_interfaces(self, error)) return FALSE; for (guint i = 0; i < priv->hid_descriptors->len; i++) { FuUsbHidDescriptor *hid_descriptor = g_ptr_array_index(priv->hid_descriptors, i); g_autoptr(GError) error_local = NULL; if (!fu_usb_device_ensure_hid_descriptor(self, hid_descriptor, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_READ)) { g_debug("ignoring: %s", error_local->message); } else { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } } return TRUE; } /** * fu_usb_device_get_hid_descriptors: * @self: a #FuUsbDevice * @error: a #GError, or %NULL * * Gets all the HID descriptors exported by the device. * * The first time this method is used the hardware is queried and then after that cached results * are returned. To invalidate the caches use fu_device_invalidate(). * * Return value: (transfer container) (element-type GBytes): an array of HID descriptors * * Since: 2.0.0 **/ GPtrArray * fu_usb_device_get_hid_descriptors(FuUsbDevice *self, GError **error) { FuUsbDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(GPtrArray) hid_descriptor_blobs = g_ptr_array_new_with_free_func((GDestroyNotify)g_bytes_unref); g_return_val_if_fail(FU_IS_USB_DEVICE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); if (!fu_usb_device_ensure_hid_descriptors(self, error)) return NULL; for (guint i = 0; i < priv->hid_descriptors->len; i++) { FuUsbHidDescriptor *hid_descriptor = g_ptr_array_index(priv->hid_descriptors, i); if (fu_usb_hid_descriptor_get_blob(hid_descriptor) == NULL) continue; g_ptr_array_add(hid_descriptor_blobs, g_bytes_ref(fu_usb_hid_descriptor_get_blob(hid_descriptor))); } return g_steal_pointer(&hid_descriptor_blobs); } static gboolean fu_usb_device_from_json(FuDevice *device, JsonObject *json_object, GError **error) { FuUsbDevice *self = FU_USB_DEVICE(device); FuUsbDevicePrivate *priv = GET_PRIVATE(self); const gchar *tmp; /* optional properties */ tmp = json_object_get_string_member_with_default(json_object, "PlatformId", NULL); if (tmp != NULL) fu_device_set_physical_id(FU_DEVICE(self), tmp); fu_device_set_vid(FU_DEVICE(self), json_object_get_int_member_with_default(json_object, "IdVendor", 0x0)); fu_device_set_pid(FU_DEVICE(self), json_object_get_int_member_with_default(json_object, "IdProduct", 0x0)); priv->desc.bcdDevice = json_object_get_int_member_with_default(json_object, "Device", 0x0); priv->desc.bcdUSB = json_object_get_int_member_with_default(json_object, "USB", 0x0); priv->desc.iManufacturer = json_object_get_int_member_with_default(json_object, "Manufacturer", 0x0); priv->desc.bDeviceClass = json_object_get_int_member_with_default(json_object, "DeviceClass", 0x0); priv->desc.bDeviceSubClass = json_object_get_int_member_with_default(json_object, "DeviceSubClass", 0x0); priv->desc.bDeviceProtocol = json_object_get_int_member_with_default(json_object, "DeviceProtocol", 0x0); priv->desc.iProduct = json_object_get_int_member_with_default(json_object, "Product", 0x0); priv->desc.iSerialNumber = json_object_get_int_member_with_default(json_object, "SerialNumber", 0x0); /* array of BOS descriptors */ if (json_object_has_member(json_object, "UsbBosDescriptors")) { JsonArray *json_array = json_object_get_array_member(json_object, "UsbBosDescriptors"); for (guint i = 0; i < json_array_get_length(json_array); i++) { JsonNode *node_tmp = json_array_get_element(json_array, i); g_autoptr(FuUsbBosDescriptor) bos_descriptor = g_object_new(FU_TYPE_USB_BOS_DESCRIPTOR, NULL); if (!fwupd_codec_from_json(FWUPD_CODEC(bos_descriptor), node_tmp, error)) return FALSE; g_ptr_array_add(priv->bos_descriptors, g_object_ref(bos_descriptor)); } } /* array of config descriptors */ if (json_object_has_member(json_object, "UsbConfigDescriptors")) { JsonArray *json_array = json_object_get_array_member(json_object, "UsbConfigDescriptors"); for (guint i = 0; i < json_array_get_length(json_array); i++) { JsonNode *node_tmp = json_array_get_element(json_array, i); g_autoptr(FuUsbConfigDescriptor) cfg_descriptor = g_object_new(FU_TYPE_USB_CONFIG_DESCRIPTOR, NULL); if (!fwupd_codec_from_json(FWUPD_CODEC(cfg_descriptor), node_tmp, error)) return FALSE; g_ptr_array_add(priv->cfg_descriptors, g_object_ref(cfg_descriptor)); } } /* array of HID descriptors */ if (json_object_has_member(json_object, "UsbHidDescriptors")) { JsonArray *json_array = json_object_get_array_member(json_object, "UsbHidDescriptors"); for (guint i = 0; i < json_array_get_length(json_array); i++) { JsonNode *node_tmp = json_array_get_element(json_array, i); g_autoptr(FuUsbHidDescriptor) hid_descriptor = g_object_new(FU_TYPE_USB_HID_DESCRIPTOR, NULL); if (!fwupd_codec_from_json(FWUPD_CODEC(hid_descriptor), node_tmp, error)) return FALSE; g_ptr_array_add(priv->hid_descriptors, g_object_ref(hid_descriptor)); } } /* array of interfaces */ if (json_object_has_member(json_object, "UsbInterfaces")) { JsonArray *json_array = json_object_get_array_member(json_object, "UsbInterfaces"); for (guint i = 0; i < json_array_get_length(json_array); i++) { JsonNode *node_tmp = json_array_get_element(json_array, i); g_autoptr(FuUsbInterface) iface = g_object_new(FU_TYPE_USB_INTERFACE, NULL); if (!fwupd_codec_from_json(FWUPD_CODEC(iface), node_tmp, error)) return FALSE; fu_usb_device_add_interface_internal(self, iface); } } /* array of events */ if (json_object_has_member(json_object, "UsbEvents")) { JsonArray *json_array = json_object_get_array_member(json_object, "UsbEvents"); for (guint i = 0; i < json_array_get_length(json_array); i++) { JsonNode *node_tmp = json_array_get_element(json_array, i); g_autoptr(FuDeviceEvent) event = fu_device_event_new(NULL); if (!fwupd_codec_from_json(FWUPD_CODEC(event), node_tmp, error)) return FALSE; fu_device_add_event(FU_DEVICE(self), event); } } /* success */ priv->interfaces_valid = TRUE; priv->bos_descriptors_valid = TRUE; return TRUE; } static void fu_usb_device_add_json(FuDevice *device, JsonBuilder *builder, FwupdCodecFlags flags) { FuUsbDevice *self = FU_USB_DEVICE(device); FuUsbDevicePrivate *priv = GET_PRIVATE(self); GPtrArray *events = fu_device_get_events(device); g_autoptr(GPtrArray) interfaces = NULL; g_autoptr(GError) error_bos = NULL; g_autoptr(GError) error_hid = NULL; g_autoptr(GError) error_interfaces = NULL; /* optional properties */ fwupd_codec_json_append(builder, "GType", "FuUsbDevice"); fwupd_codec_json_append(builder, "PlatformId", fu_device_get_physical_id(FU_DEVICE(self))); if (fu_device_get_created_usec(FU_DEVICE(self)) != 0) { #if GLIB_CHECK_VERSION(2, 80, 0) g_autoptr(GDateTime) dt = g_date_time_new_from_unix_utc_usec(fu_device_get_created_usec(FU_DEVICE(self))); #else g_autoptr(GDateTime) dt = g_date_time_new_from_unix_utc( fu_device_get_created_usec(FU_DEVICE(self)) / G_USEC_PER_SEC); #endif g_autofree gchar *str = g_date_time_format_iso8601(dt); fwupd_codec_json_append(builder, "Created", str); } if (fu_device_get_vid(FU_DEVICE(self)) != 0) { fwupd_codec_json_append_int(builder, "IdVendor", fu_device_get_vid(FU_DEVICE(self))); } if (fu_device_get_pid(FU_DEVICE(self)) != 0) { fwupd_codec_json_append_int(builder, "IdProduct", fu_device_get_pid(FU_DEVICE(self))); } if (priv->desc.bcdDevice != 0) fwupd_codec_json_append_int(builder, "Device", priv->desc.bcdDevice); if (priv->desc.bcdUSB != 0) fwupd_codec_json_append_int(builder, "USB", priv->desc.bcdUSB); if (priv->desc.iManufacturer != 0) fwupd_codec_json_append_int(builder, "Manufacturer", priv->desc.iManufacturer); if (priv->desc.bDeviceClass != 0) fwupd_codec_json_append_int(builder, "DeviceClass", priv->desc.bDeviceClass); if (priv->desc.bDeviceSubClass != 0) fwupd_codec_json_append_int(builder, "DeviceSubClass", priv->desc.bDeviceSubClass); if (priv->desc.bDeviceProtocol != 0) fwupd_codec_json_append_int(builder, "DeviceProtocol", priv->desc.bDeviceProtocol); if (priv->desc.iProduct != 0) fwupd_codec_json_append_int(builder, "Product", priv->desc.iProduct); if (priv->desc.iSerialNumber != 0) fwupd_codec_json_append_int(builder, "SerialNumber", priv->desc.iSerialNumber); /* array of BOS descriptors */ if (!fu_usb_device_ensure_bos_descriptors(self, &error_bos)) g_debug("%s", error_bos->message); if (priv->bos_descriptors->len > 0) { json_builder_set_member_name(builder, "UsbBosDescriptors"); json_builder_begin_array(builder); for (guint i = 0; i < priv->bos_descriptors->len; i++) { FuUsbBosDescriptor *bos_descriptor = g_ptr_array_index(priv->bos_descriptors, i); json_builder_begin_object(builder); fwupd_codec_to_json(FWUPD_CODEC(bos_descriptor), builder, flags); json_builder_end_object(builder); } json_builder_end_array(builder); } /* array of config descriptors */ if (priv->cfg_descriptors->len > 0) { json_builder_set_member_name(builder, "UsbConfigDescriptors"); json_builder_begin_array(builder); for (guint i = 0; i < priv->cfg_descriptors->len; i++) { FuUsbConfigDescriptor *cfg_descriptor = g_ptr_array_index(priv->cfg_descriptors, i); json_builder_begin_object(builder); fwupd_codec_to_json(FWUPD_CODEC(cfg_descriptor), builder, flags); json_builder_end_object(builder); } json_builder_end_array(builder); } /* array of HID descriptors */ if (!fu_usb_device_ensure_hid_descriptors(self, &error_hid)) { g_debug("%s", error_hid->message); } else if (priv->hid_descriptors->len > 0) { json_builder_set_member_name(builder, "UsbHidDescriptors"); json_builder_begin_array(builder); for (guint i = 0; i < priv->hid_descriptors->len; i++) { FuUsbHidDescriptor *hid_descriptor = g_ptr_array_index(priv->hid_descriptors, i); fwupd_codec_to_json(FWUPD_CODEC(hid_descriptor), builder, flags); } json_builder_end_array(builder); } /* array of interfaces */ interfaces = fu_usb_device_get_interfaces(self, &error_interfaces); if (interfaces == NULL) { g_debug("%s", error_interfaces->message); } else if (interfaces->len > 0) { json_builder_set_member_name(builder, "UsbInterfaces"); json_builder_begin_array(builder); for (guint i = 0; i < interfaces->len; i++) { FuUsbInterface *iface = g_ptr_array_index(interfaces, i); json_builder_begin_object(builder); fwupd_codec_to_json(FWUPD_CODEC(iface), builder, flags); json_builder_end_object(builder); } json_builder_end_array(builder); } /* events */ if (events->len > 0) { json_builder_set_member_name(builder, "UsbEvents"); json_builder_begin_array(builder); for (guint i = 0; i < events->len; i++) { FuDeviceEvent *event = g_ptr_array_index(events, i); json_builder_begin_object(builder); fwupd_codec_to_json(FWUPD_CODEC(event), builder, events->len > 1000 ? flags | FWUPD_CODEC_FLAG_COMPRESSED : flags); json_builder_end_object(builder); } json_builder_end_array(builder); } } /** * fu_usb_device_new: (skip): * @ctx: (nullable): a #FuContext * @usb_device: a #libusb_device * * Creates a new #FuUsbDevice. * * Returns: (transfer full): a #FuUsbDevice * * Since: 2.0.0 **/ FuUsbDevice * fu_usb_device_new(FuContext *ctx, libusb_device *usb_device) { return g_object_new(FU_TYPE_USB_DEVICE, "context", ctx, "libusb-device", usb_device, NULL); } static void fu_usb_device_to_string(FuDevice *device, guint idt, GString *str) { FuUsbDevice *self = FU_USB_DEVICE(device); FuUsbDevicePrivate *priv = GET_PRIVATE(self); if (priv->configuration >= 0) fwupd_codec_string_append_hex(str, idt, "Configuration", priv->configuration); fwupd_codec_string_append_hex(str, idt, "ClaimRetryCount", priv->claim_retry_count); fwupd_codec_string_append_hex(str, idt, "BusNum", priv->busnum); fwupd_codec_string_append_hex(str, idt, "DevNum", priv->devnum); for (guint i = 0; priv->device_interfaces != NULL && i < priv->device_interfaces->len; i++) { FuUsbDeviceInterface *iface = g_ptr_array_index(priv->device_interfaces, i); g_autofree gchar *tmp = g_strdup_printf("InterfaceNumber#%02x", iface->number); fwupd_codec_string_append(str, idt, tmp, iface->claimed ? "claimed" : "released"); } fwupd_codec_string_append(str, idt, "Class", fu_usb_class_to_string(fu_usb_device_get_class(self))); if (priv->interfaces->len > 0) { fwupd_codec_string_append(str, idt, "Interfaces", ""); for (guint i = 0; i < priv->interfaces->len; i++) { FuUsbInterface *iface = g_ptr_array_index(priv->interfaces, i); fwupd_codec_add_string(FWUPD_CODEC(iface), idt + 1, str); } } if (priv->bos_descriptors->len > 0) { fwupd_codec_string_append(str, idt, "BosDescriptors", ""); for (guint i = 0; i < priv->bos_descriptors->len; i++) { FuUsbBosDescriptor *bos_descriptor = g_ptr_array_index(priv->bos_descriptors, i); fwupd_codec_add_string(FWUPD_CODEC(bos_descriptor), idt + 1, str); } } if (priv->cfg_descriptors->len > 0) { fwupd_codec_string_append(str, idt, "ConfigDescriptors", ""); for (guint i = 0; i < priv->cfg_descriptors->len; i++) { FuUsbConfigDescriptor *cfg_descriptor = g_ptr_array_index(priv->cfg_descriptors, i); fwupd_codec_add_string(FWUPD_CODEC(cfg_descriptor), idt + 1, str); } } if (priv->hid_descriptors->len > 0) { fwupd_codec_string_append(str, idt, "HidDescriptors", ""); for (guint i = 0; i < priv->hid_descriptors->len; i++) { GBytes *hid_descriptor = g_ptr_array_index(priv->hid_descriptors, i); g_autofree gchar *key = g_strdup_printf("HidDescriptor0x%02u", i); fwupd_codec_string_append_hex(str, idt + 1, key, g_bytes_get_size(hid_descriptor)); } } } static void fu_usb_device_class_init(FuUsbDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GParamSpec *pspec; object_class->finalize = fu_usb_device_finalize; object_class->get_property = fu_usb_device_get_property; object_class->set_property = fu_usb_device_set_property; device_class->open = fu_usb_device_open; device_class->setup = fu_usb_device_setup; device_class->ready = fu_usb_device_ready; device_class->close = fu_usb_device_close; device_class->probe = fu_usb_device_probe; device_class->invalidate = fu_usb_device_invalidate; device_class->to_string = fu_usb_device_to_string; device_class->incorporate = fu_usb_device_incorporate; device_class->convert_version = fu_usb_device_convert_version; device_class->from_json = fu_usb_device_from_json; device_class->add_json = fu_usb_device_add_json; /** * FuUsbDevice:libusb-device: * * The low-level #libusb_device. * * Since: 2.0.0 */ pspec = g_param_spec_pointer("libusb-device", NULL, NULL, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE); g_object_class_install_property(object_class, PROP_LIBUSB_DEVICE, pspec); } fwupd-2.0.10/libfwupdplugin/fu-usb-device.h000066400000000000000000000103221501337203100205520ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-plugin.h" #include "fu-udev-device.h" #include "fu-usb-interface.h" #include "fu-usb-struct.h" #define FU_TYPE_USB_DEVICE (fu_usb_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuUsbDevice, fu_usb_device, FU, USB_DEVICE, FuUdevDevice) struct _FuUsbDeviceClass { FuUdevDeviceClass parent_class; }; /** * FuUsbDeviceClaimFlags: * * Flags for the fu_usb_device_claim_interface and * fu_usb_device_release_interface methods flags parameters. **/ typedef enum { FU_USB_DEVICE_CLAIM_FLAG_NONE = 0, FU_USB_DEVICE_CLAIM_FLAG_KERNEL_DRIVER = 1 << 0, } FuUsbDeviceClaimFlags; guint8 fu_usb_device_get_bus(FuUsbDevice *self) G_GNUC_NON_NULL(1); guint8 fu_usb_device_get_address(FuUsbDevice *self) G_GNUC_NON_NULL(1); guint16 fu_usb_device_get_release(FuUsbDevice *self) G_GNUC_NON_NULL(1); guint16 fu_usb_device_get_spec(FuUsbDevice *self) G_GNUC_NON_NULL(1); FuUsbClass fu_usb_device_get_class(FuUsbDevice *self) G_GNUC_NON_NULL(1); guint8 fu_usb_device_get_configuration_index(FuUsbDevice *self, GError **error) G_GNUC_NON_NULL(1); guint8 fu_usb_device_get_serial_number_index(FuUsbDevice *self) G_GNUC_NON_NULL(1); guint8 fu_usb_device_get_manufacturer_index(FuUsbDevice *self) G_GNUC_NON_NULL(1); guint8 fu_usb_device_get_product_index(FuUsbDevice *self) G_GNUC_NON_NULL(1); guint8 fu_usb_device_get_custom_index(FuUsbDevice *self, guint8 class_id, guint8 subclass_id, guint8 protocol_id, GError **error) G_GNUC_NON_NULL(1); void fu_usb_device_set_configuration(FuUsbDevice *device, gint configuration) G_GNUC_NON_NULL(1); void fu_usb_device_add_interface(FuUsbDevice *device, guint8 number) G_GNUC_NON_NULL(1); void fu_usb_device_set_claim_retry_count(FuUsbDevice *self, guint claim_retry_count) G_GNUC_NON_NULL(1); guint fu_usb_device_get_claim_retry_count(FuUsbDevice *self) G_GNUC_NON_NULL(1); gboolean fu_usb_device_control_transfer(FuUsbDevice *self, FuUsbDirection direction, FuUsbRequestType request_type, FuUsbRecipient recipient, guint8 request, guint16 value, guint16 idx, guint8 *data, gsize length, gsize *actual_length, guint timeout, GCancellable *cancellable, GError **error) G_GNUC_NON_NULL(1); gboolean fu_usb_device_bulk_transfer(FuUsbDevice *self, guint8 endpoint, guint8 *data, gsize length, gsize *actual_length, guint timeout, GCancellable *cancellable, GError **error) G_GNUC_NON_NULL(1); gboolean fu_usb_device_interrupt_transfer(FuUsbDevice *self, guint8 endpoint, guint8 *data, gsize length, gsize *actual_length, guint timeout, GCancellable *cancellable, GError **error) G_GNUC_NON_NULL(1); gboolean fu_usb_device_claim_interface(FuUsbDevice *self, guint8 iface, FuUsbDeviceClaimFlags flags, GError **error) G_GNUC_NON_NULL(1); gboolean fu_usb_device_release_interface(FuUsbDevice *self, guint8 iface, FuUsbDeviceClaimFlags flags, GError **error) G_GNUC_NON_NULL(1); gboolean fu_usb_device_reset(FuUsbDevice *self, GError **error) G_GNUC_NON_NULL(1); GPtrArray * fu_usb_device_get_interfaces(FuUsbDevice *self, GError **error) G_GNUC_NON_NULL(1); FuUsbInterface * fu_usb_device_get_interface(FuUsbDevice *self, guint8 class_id, guint8 subclass_id, guint8 protocol_id, GError **error) G_GNUC_NON_NULL(1); gboolean fu_usb_device_set_interface_alt(FuUsbDevice *self, guint8 iface, guint8 alt, GError **error) G_GNUC_NON_NULL(1); gchar * fu_usb_device_get_string_descriptor(FuUsbDevice *self, guint8 desc_index, GError **error) G_GNUC_NON_NULL(1); GBytes * fu_usb_device_get_string_descriptor_bytes(FuUsbDevice *self, guint8 desc_index, guint16 langid, GError **error) G_GNUC_NON_NULL(1); GBytes * fu_usb_device_get_string_descriptor_bytes_full(FuUsbDevice *self, guint8 desc_index, guint16 langid, gsize length, GError **error) G_GNUC_NON_NULL(1); GPtrArray * fu_usb_device_get_hid_descriptors(FuUsbDevice *self, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-usb-endpoint-private.h000066400000000000000000000005231501337203100226050ustar00rootroot00000000000000/* * Copyright 2020 Emmanuel Pacaud * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-usb-endpoint.h" FuUsbEndpoint * fu_usb_endpoint_new(const struct libusb_endpoint_descriptor *endpoint) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-usb-endpoint.c000066400000000000000000000161411501337203100211330ustar00rootroot00000000000000/* * Copyright 2020 Emmanuel Pacaud * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ /** * FuUsbEndpoint: * * This object is a thin glib wrapper around a libusb_endpoint_descriptor. * * All the data is copied when the object is created and the original * descriptor can be destroyed any at point. */ #include "config.h" #include #include "fu-usb-endpoint-private.h" struct _FuUsbEndpoint { FuUsbDescriptor parent_instance; struct libusb_endpoint_descriptor endpoint_descriptor; }; static void fu_usb_endpoint_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_EXTENDED(FuUsbEndpoint, fu_usb_endpoint, FU_TYPE_USB_DESCRIPTOR, 0, G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fu_usb_endpoint_codec_iface_init)); static gboolean fu_usb_endpoint_from_json(FwupdCodec *codec, JsonNode *json_node, GError **error) { FuUsbEndpoint *self = FU_USB_ENDPOINT(codec); JsonObject *json_object; /* sanity check */ if (!JSON_NODE_HOLDS_OBJECT(json_node)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not JSON object"); return FALSE; } json_object = json_node_get_object(json_node); /* optional properties */ self->endpoint_descriptor.bDescriptorType = json_object_get_int_member_with_default(json_object, "DescriptorType", 0x0); self->endpoint_descriptor.bEndpointAddress = json_object_get_int_member_with_default(json_object, "EndpointAddress", 0x0); self->endpoint_descriptor.bRefresh = json_object_get_int_member_with_default(json_object, "Refresh", 0x0); self->endpoint_descriptor.bInterval = json_object_get_int_member_with_default(json_object, "Interval", 0x0); self->endpoint_descriptor.bSynchAddress = json_object_get_int_member_with_default(json_object, "SynchAddress", 0x0); self->endpoint_descriptor.wMaxPacketSize = json_object_get_int_member_with_default(json_object, "MaxPacketSize", 0x0); /* success */ return TRUE; } static void fu_usb_endpoint_add_json(FwupdCodec *codec, JsonBuilder *builder, FwupdCodecFlags flags) { FuUsbEndpoint *self = FU_USB_ENDPOINT(codec); /* optional properties */ if (self->endpoint_descriptor.bDescriptorType != 0) { json_builder_set_member_name(builder, "DescriptorType"); json_builder_add_int_value(builder, self->endpoint_descriptor.bDescriptorType); } if (self->endpoint_descriptor.bEndpointAddress != 0) { json_builder_set_member_name(builder, "EndpointAddress"); json_builder_add_int_value(builder, self->endpoint_descriptor.bEndpointAddress); } if (self->endpoint_descriptor.bRefresh != 0) { json_builder_set_member_name(builder, "Refresh"); json_builder_add_int_value(builder, self->endpoint_descriptor.bRefresh); } if (self->endpoint_descriptor.bInterval != 0) { json_builder_set_member_name(builder, "Interval"); json_builder_add_int_value(builder, self->endpoint_descriptor.bInterval); } if (self->endpoint_descriptor.bSynchAddress != 0) { json_builder_set_member_name(builder, "SynchAddress"); json_builder_add_int_value(builder, self->endpoint_descriptor.bSynchAddress); } if (self->endpoint_descriptor.wMaxPacketSize != 0) { json_builder_set_member_name(builder, "MaxPacketSize"); json_builder_add_int_value(builder, self->endpoint_descriptor.wMaxPacketSize); } } /** * fu_usb_endpoint_get_maximum_packet_size: * @self: a #FuUsbEndpoint * * Gets the maximum packet size this endpoint is capable of sending/receiving. * * Return value: The maximum packet size * * Since: 2.0.0 **/ guint16 fu_usb_endpoint_get_maximum_packet_size(FuUsbEndpoint *self) { g_return_val_if_fail(FU_IS_USB_ENDPOINT(self), 0); return self->endpoint_descriptor.wMaxPacketSize; } /** * fu_usb_endpoint_get_polling_interval: * @self: a #FuUsbEndpoint * * Gets the endpoint polling interval. * * Return value: The endpoint polling interval * * Since: 2.0.0 **/ guint8 fu_usb_endpoint_get_polling_interval(FuUsbEndpoint *self) { g_return_val_if_fail(FU_IS_USB_ENDPOINT(self), 0); return self->endpoint_descriptor.bInterval; } /** * fu_usb_endpoint_get_address: * @self: a #FuUsbEndpoint * * Gets the address of the endpoint. * * Return value: The 4-bit endpoint address * * Since: 2.0.0 **/ guint8 fu_usb_endpoint_get_address(FuUsbEndpoint *self) { g_return_val_if_fail(FU_IS_USB_ENDPOINT(self), 0); return self->endpoint_descriptor.bEndpointAddress; } /** * fu_usb_endpoint_get_number: * @self: a #FuUsbEndpoint * * Gets the number part of endpoint address. * * Return value: The lower 4-bit of endpoint address * * Since: 2.0.0 **/ guint8 fu_usb_endpoint_get_number(FuUsbEndpoint *self) { g_return_val_if_fail(FU_IS_USB_ENDPOINT(self), 0); return (self->endpoint_descriptor.bEndpointAddress) & 0xf; } /** * fu_usb_endpoint_get_direction: * @self: a #FuUsbEndpoint * * Gets the direction of the endpoint. * * Return value: The endpoint direction * * Since: 2.0.0 **/ FuUsbDirection fu_usb_endpoint_get_direction(FuUsbEndpoint *self) { g_return_val_if_fail(FU_IS_USB_ENDPOINT(self), 0); return (self->endpoint_descriptor.bEndpointAddress & 0x80) ? FU_USB_DIRECTION_DEVICE_TO_HOST : FU_USB_DIRECTION_HOST_TO_DEVICE; } static gboolean fu_usb_endpoint_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuUsbEndpoint *self = FU_USB_ENDPOINT(firmware); g_autoptr(FuUsbEndpointHdr) st = NULL; /* FuUsbDescriptor */ if (!FU_FIRMWARE_CLASS(fu_usb_endpoint_parent_class)->parse(firmware, stream, flags, error)) return FALSE; /* parse */ st = fu_usb_endpoint_hdr_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; self->endpoint_descriptor.bLength = fu_usb_endpoint_hdr_get_length(st); self->endpoint_descriptor.bDescriptorType = fu_usb_endpoint_hdr_get_descriptor_type(st); self->endpoint_descriptor.bEndpointAddress = fu_usb_endpoint_hdr_get_endpoint_address(st); self->endpoint_descriptor.bmAttributes = fu_usb_endpoint_hdr_get_attributes(st); self->endpoint_descriptor.wMaxPacketSize = fu_usb_endpoint_hdr_get_max_packet_size(st); self->endpoint_descriptor.bInterval = fu_usb_endpoint_hdr_get_interval(st); self->endpoint_descriptor.bRefresh = 0; self->endpoint_descriptor.bSynchAddress = 0; /* success */ return TRUE; } static void fu_usb_endpoint_codec_iface_init(FwupdCodecInterface *iface) { iface->add_json = fu_usb_endpoint_add_json; iface->from_json = fu_usb_endpoint_from_json; } static void fu_usb_endpoint_init(FuUsbEndpoint *self) { } static void fu_usb_endpoint_class_init(FuUsbEndpointClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_usb_endpoint_parse; } /** * fu_usb_endpoint_new: * * Return value: a new #FuUsbEndpoint object. * * Since: 2.0.0 **/ FuUsbEndpoint * fu_usb_endpoint_new(const struct libusb_endpoint_descriptor *endpoint_descriptor) { FuUsbEndpoint *self = g_object_new(FU_TYPE_USB_ENDPOINT, NULL); /* copy the data */ memcpy(&self->endpoint_descriptor, /* nocheck:blocked */ endpoint_descriptor, sizeof(struct libusb_endpoint_descriptor)); return FU_USB_ENDPOINT(self); } fwupd-2.0.10/libfwupdplugin/fu-usb-endpoint.h000066400000000000000000000014031501337203100211330ustar00rootroot00000000000000/* * Copyright 2020 Emmanuel Pacaud * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-usb-descriptor.h" #define FU_TYPE_USB_ENDPOINT (fu_usb_endpoint_get_type()) G_DECLARE_FINAL_TYPE(FuUsbEndpoint, fu_usb_endpoint, FU, USB_ENDPOINT, FuUsbDescriptor) guint16 fu_usb_endpoint_get_maximum_packet_size(FuUsbEndpoint *self) G_GNUC_NON_NULL(1); guint8 fu_usb_endpoint_get_polling_interval(FuUsbEndpoint *self) G_GNUC_NON_NULL(1); guint8 fu_usb_endpoint_get_address(FuUsbEndpoint *self) G_GNUC_NON_NULL(1); guint8 fu_usb_endpoint_get_number(FuUsbEndpoint *self) G_GNUC_NON_NULL(1); FuUsbDirection fu_usb_endpoint_get_direction(FuUsbEndpoint *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-usb-hid-descriptor-private.h000066400000000000000000000013001501337203100236770ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-usb-hid-descriptor.h" FuUsbHidDescriptor * fu_usb_hid_descriptor_new(void); guint8 fu_usb_hid_descriptor_get_iface_number(FuUsbHidDescriptor *self) G_GNUC_NON_NULL(1); void fu_usb_hid_descriptor_set_iface_number(FuUsbHidDescriptor *self, guint8 iface_number) G_GNUC_NON_NULL(1); gsize fu_usb_hid_descriptor_get_descriptor_length(FuUsbHidDescriptor *self) G_GNUC_NON_NULL(1); GBytes * fu_usb_hid_descriptor_get_blob(FuUsbHidDescriptor *self) G_GNUC_NON_NULL(1); void fu_usb_hid_descriptor_set_blob(FuUsbHidDescriptor *self, GBytes *blob) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/libfwupdplugin/fu-usb-hid-descriptor.c000066400000000000000000000114411501337203100222310ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ /** * FuUsbHidDescriptor: * * This object is a placeholder for the HID descriptor, and is populated with data after the * device has been opened. */ #include "config.h" #include "fu-usb-hid-descriptor-private.h" struct _FuUsbHidDescriptor { FuUsbDescriptor parent_instance; guint8 iface_number; gsize descriptor_length; GBytes *blob; }; static void fu_usb_hid_descriptor_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_EXTENDED(FuUsbHidDescriptor, fu_usb_hid_descriptor, FU_TYPE_USB_DESCRIPTOR, 0, G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fu_usb_hid_descriptor_codec_iface_init)); static gboolean fu_usb_hid_descriptor_from_json(FwupdCodec *codec, JsonNode *json_node, GError **error) { FuUsbHidDescriptor *self = FU_USB_HID_DESCRIPTOR(codec); const gchar *tmp; tmp = json_node_get_string(json_node); if (tmp != NULL) { gsize bufsz = 0; g_autofree guchar *buf = g_base64_decode(tmp, &bufsz); g_autoptr(GBytes) blob = g_bytes_new_take(g_steal_pointer(&buf), bufsz); fu_usb_hid_descriptor_set_blob(self, blob); } /* success */ return TRUE; } static void fu_usb_hid_descriptor_add_json(FwupdCodec *codec, JsonBuilder *builder, FwupdCodecFlags flags) { FuUsbHidDescriptor *self = FU_USB_HID_DESCRIPTOR(codec); g_autofree gchar *str = NULL; if (self->blob == NULL) return; str = g_base64_encode(g_bytes_get_data(self->blob, NULL), g_bytes_get_size(self->blob)); json_builder_add_string_value(builder, str); } /** * fu_usb_hid_descriptor_get_iface_number: * @self: a #FuUsbHidDescriptor * * Gets the hid descriptor interface number. * * Return value: integer * * Since: 2.0.2 **/ guint8 fu_usb_hid_descriptor_get_iface_number(FuUsbHidDescriptor *self) { g_return_val_if_fail(FU_IS_USB_HID_DESCRIPTOR(self), 0); return self->iface_number; } /** * fu_usb_hid_descriptor_set_iface_number: * @self: a #FuUsbHidDescriptor * * Sets the hid descriptor interface number. * * Since: 2.0.2 **/ void fu_usb_hid_descriptor_set_iface_number(FuUsbHidDescriptor *self, guint8 iface_number) { g_return_if_fail(FU_IS_USB_HID_DESCRIPTOR(self)); self->iface_number = iface_number; } /** * fu_usb_hid_descriptor_get_descriptor_length: * @self: a #FuUsbHidDescriptor * * Gets the HID descriptor length. * * Return value: integer * * Since: 2.0.2 **/ gsize fu_usb_hid_descriptor_get_descriptor_length(FuUsbHidDescriptor *self) { g_return_val_if_fail(FU_IS_USB_HID_DESCRIPTOR(self), 0); return self->descriptor_length; } /** * fu_usb_hid_descriptor_get_blob: * @self: a #FuUsbHidDescriptor * * Gets the HID descriptor binary blob. * * Return value: (transfer none): The descriptor data * * Since: 2.0.2 **/ GBytes * fu_usb_hid_descriptor_get_blob(FuUsbHidDescriptor *self) { g_return_val_if_fail(FU_IS_USB_HID_DESCRIPTOR(self), NULL); return self->blob; } /** * fu_usb_hid_descriptor_set_blob: * @self: a #FuUsbHidDescriptor * * Sets the HID descriptor binary blob. * * Since: 2.0.2 **/ void fu_usb_hid_descriptor_set_blob(FuUsbHidDescriptor *self, GBytes *blob) { g_return_if_fail(FU_IS_USB_HID_DESCRIPTOR(self)); if (self->blob != NULL) g_bytes_unref(self->blob); self->blob = g_bytes_ref(blob); } static gboolean fu_usb_hid_descriptor_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuUsbHidDescriptor *self = FU_USB_HID_DESCRIPTOR(firmware); g_autoptr(FuUsbHidDescriptorHdr) st = NULL; /* parse */ st = fu_usb_hid_descriptor_hdr_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; self->descriptor_length = fu_usb_hid_descriptor_hdr_get_class_descriptor_length(st); /* success */ return TRUE; } static void fu_usb_hid_descriptor_codec_iface_init(FwupdCodecInterface *iface) { iface->add_json = fu_usb_hid_descriptor_add_json; iface->from_json = fu_usb_hid_descriptor_from_json; } static void fu_usb_hid_descriptor_init(FuUsbHidDescriptor *self) { } static void fu_usb_hid_descriptor_finalize(GObject *object) { FuUsbHidDescriptor *self = FU_USB_HID_DESCRIPTOR(object); if (self->blob != NULL) g_bytes_unref(self->blob); G_OBJECT_CLASS(fu_usb_hid_descriptor_parent_class)->finalize(object); } static void fu_usb_hid_descriptor_class_init(FuUsbHidDescriptorClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_usb_hid_descriptor_finalize; firmware_class->parse = fu_usb_hid_descriptor_parse; } /** * fu_usb_hid_descriptor_new: * * Return value: a new #FuUsbHidDescriptor object. * * Since: 2.0.2 **/ FuUsbHidDescriptor * fu_usb_hid_descriptor_new(void) { return FU_USB_HID_DESCRIPTOR(g_object_new(FU_TYPE_USB_HID_DESCRIPTOR, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-usb-hid-descriptor.h000066400000000000000000000005511501337203100222360ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-usb-descriptor.h" #define FU_TYPE_USB_HID_DESCRIPTOR (fu_usb_hid_descriptor_get_type()) G_DECLARE_FINAL_TYPE(FuUsbHidDescriptor, fu_usb_hid_descriptor, FU, USB_HID_DESCRIPTOR, FuUsbDescriptor) fwupd-2.0.10/libfwupdplugin/fu-usb-interface-private.h000066400000000000000000000006411501337203100227260ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-usb-endpoint.h" #include "fu-usb-interface.h" FuUsbInterface * fu_usb_interface_new(const struct libusb_interface_descriptor *iface, GError **error) G_GNUC_NON_NULL(1); void fu_usb_interface_add_endpoint(FuUsbInterface *self, FuUsbEndpoint *endpoint); fwupd-2.0.10/libfwupdplugin/fu-usb-interface.c000066400000000000000000000316371501337203100212620ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * Copyright 2020 Emmanuel Pacaud * * SPDX-License-Identifier: LGPL-2.1-or-later */ /** * FuUsbInterface: * * This object is a thin glib wrapper around a libusb_interface_descriptor. * * All the data is copied when the object is created and the original * descriptor can be destroyed any at point. */ #include "config.h" #include #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-input-stream.h" #include "fu-mem-private.h" #include "fu-usb-endpoint-private.h" #include "fu-usb-interface-private.h" struct _FuUsbInterface { FuUsbDescriptor parent_instance; struct libusb_interface_descriptor iface; GPtrArray *endpoints; /* element-type FuUsbEndpoint */ }; static void fu_usb_interface_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_EXTENDED(FuUsbInterface, fu_usb_interface, FU_TYPE_USB_DESCRIPTOR, 0, G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fu_usb_interface_codec_iface_init)); static void fu_usb_interface_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuUsbInterface *self = FU_USB_INTERFACE(firmware); fu_xmlb_builder_insert_kx(bn, "number", self->iface.bInterfaceNumber); fu_xmlb_builder_insert_kx(bn, "alternate", self->iface.bAlternateSetting); fu_xmlb_builder_insert_kx(bn, "class", self->iface.bInterfaceClass); fu_xmlb_builder_insert_kx(bn, "subclass", self->iface.bInterfaceSubClass); fu_xmlb_builder_insert_kx(bn, "protocol", self->iface.bInterfaceProtocol); fu_xmlb_builder_insert_kx(bn, "interface", self->iface.iInterface); } static gboolean fu_usb_interface_parse_extra(FuUsbInterface *self, const guint8 *buf, gsize bufsz, GError **error) { gsize offset = 0; g_autoptr(GBytes) bytes = g_bytes_new(buf, bufsz); /* this is common to all descriptor types */ while (offset < bufsz) { g_autoptr(FuUsbDescriptor) img = g_object_new(FU_TYPE_USB_DESCRIPTOR, NULL); if (!fu_firmware_parse_bytes(FU_FIRMWARE(img), bytes, offset, FU_FIRMWARE_PARSE_FLAG_CACHE_BLOB, error)) return FALSE; if (!fu_firmware_add_image_full(FU_FIRMWARE(self), FU_FIRMWARE(img), error)) return FALSE; offset += fu_firmware_get_size(FU_FIRMWARE(img)); } return TRUE; } static gboolean fu_usb_interface_from_json(FwupdCodec *codec, JsonNode *json_node, GError **error) { FuUsbInterface *self = FU_USB_INTERFACE(codec); const gchar *str; JsonObject *json_object; /* sanity check */ if (!JSON_NODE_HOLDS_OBJECT(json_node)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not JSON object"); return FALSE; } json_object = json_node_get_object(json_node); /* optional properties */ self->iface.bLength = json_object_get_int_member_with_default(json_object, "Length", 0x0); self->iface.bDescriptorType = json_object_get_int_member_with_default(json_object, "DescriptorType", 0x0); self->iface.bInterfaceNumber = json_object_get_int_member_with_default(json_object, "InterfaceNumber", 0x0); self->iface.bAlternateSetting = json_object_get_int_member_with_default(json_object, "AlternateSetting", 0x0); self->iface.bInterfaceClass = json_object_get_int_member_with_default(json_object, "InterfaceClass", 0x0); self->iface.bInterfaceSubClass = json_object_get_int_member_with_default(json_object, "InterfaceSubClass", 0x0); self->iface.bInterfaceProtocol = json_object_get_int_member_with_default(json_object, "InterfaceProtocol", 0x0); self->iface.iInterface = json_object_get_int_member_with_default(json_object, "Interface", 0x0); /* array of endpoints */ if (json_object_has_member(json_object, "UsbEndpoints")) { JsonArray *json_array = json_object_get_array_member(json_object, "UsbEndpoints"); for (guint i = 0; i < json_array_get_length(json_array); i++) { JsonNode *node_tmp = json_array_get_element(json_array, i); g_autoptr(FuUsbEndpoint) endpoint = g_object_new(FU_TYPE_USB_ENDPOINT, NULL); if (!fwupd_codec_from_json(FWUPD_CODEC(endpoint), node_tmp, error)) return FALSE; g_ptr_array_add(self->endpoints, g_object_ref(endpoint)); } } /* extra data */ str = json_object_get_string_member_with_default(json_object, "ExtraData", NULL); if (str != NULL) { gsize bufsz = 0; g_autofree guchar *buf = g_base64_decode(str, &bufsz); if (!fu_usb_interface_parse_extra(self, (const guint8 *)buf, bufsz, error)) return FALSE; } /* success */ return TRUE; } static void fu_usb_interface_add_json(FwupdCodec *codec, JsonBuilder *builder, FwupdCodecFlags flags) { FuUsbInterface *self = FU_USB_INTERFACE(codec); g_autoptr(GPtrArray) imgs = fu_firmware_get_images(FU_FIRMWARE(self)); /* optional properties */ if (self->iface.bLength != 0) { json_builder_set_member_name(builder, "Length"); json_builder_add_int_value(builder, self->iface.bLength); } if (self->iface.bDescriptorType != 0) { json_builder_set_member_name(builder, "DescriptorType"); json_builder_add_int_value(builder, self->iface.bDescriptorType); } if (self->iface.bInterfaceNumber != 0) { json_builder_set_member_name(builder, "InterfaceNumber"); json_builder_add_int_value(builder, self->iface.bInterfaceNumber); } if (self->iface.bAlternateSetting != 0) { json_builder_set_member_name(builder, "AlternateSetting"); json_builder_add_int_value(builder, self->iface.bAlternateSetting); } if (self->iface.bInterfaceClass != 0) { json_builder_set_member_name(builder, "InterfaceClass"); json_builder_add_int_value(builder, self->iface.bInterfaceClass); } if (self->iface.bInterfaceSubClass != 0) { json_builder_set_member_name(builder, "InterfaceSubClass"); json_builder_add_int_value(builder, self->iface.bInterfaceSubClass); } if (self->iface.bInterfaceProtocol != 0) { json_builder_set_member_name(builder, "InterfaceProtocol"); json_builder_add_int_value(builder, self->iface.bInterfaceProtocol); } if (self->iface.iInterface != 0) { json_builder_set_member_name(builder, "Interface"); json_builder_add_int_value(builder, self->iface.iInterface); } /* array of endpoints */ if (self->endpoints->len > 0) { json_builder_set_member_name(builder, "UsbEndpoints"); json_builder_begin_array(builder); for (guint i = 0; i < self->endpoints->len; i++) { FuUsbEndpoint *endpoint = g_ptr_array_index(self->endpoints, i); json_builder_begin_object(builder); fwupd_codec_to_json(FWUPD_CODEC(endpoint), builder, flags); json_builder_end_object(builder); } json_builder_end_array(builder); } /* extra data */ if (imgs->len > 0) { g_autofree gchar *str = NULL; g_autoptr(GByteArray) buf = g_byte_array_new(); for (guint i = 0; i < imgs->len; i++) { FuUsbDescriptor *img = g_ptr_array_index(imgs, i); g_autoptr(GBytes) blob = fu_firmware_get_bytes(FU_FIRMWARE(img), NULL); if (blob != NULL) fu_byte_array_append_bytes(buf, blob); } str = g_base64_encode(buf->data, buf->len); json_builder_set_member_name(builder, "ExtraData"); json_builder_add_string_value(builder, str); } } /** * fu_usb_interface_new: * * Return value: a new #FuUsbInterface object. * * Since: 2.0.0 **/ FuUsbInterface * fu_usb_interface_new(const struct libusb_interface_descriptor *iface, GError **error) { FuUsbInterface *self = g_object_new(FU_TYPE_USB_INTERFACE, NULL); /* copy the data */ memcpy(&self->iface, /* nocheck:blocked */ iface, sizeof(struct libusb_interface_descriptor)); if (!fu_usb_interface_parse_extra(self, iface->extra, iface->extra_length, error)) return NULL; for (guint i = 0; i < iface->bNumEndpoints; i++) g_ptr_array_add(self->endpoints, fu_usb_endpoint_new(&iface->endpoint[i])); return FU_USB_INTERFACE(self); } /** * fu_usb_interface_get_number: * @self: a #FuUsbInterface * * Gets the interface number. * * Return value: The interface ID * * Since: 2.0.0 **/ guint8 fu_usb_interface_get_number(FuUsbInterface *self) { g_return_val_if_fail(FU_IS_USB_INTERFACE(self), 0); return self->iface.bInterfaceNumber; } /** * fu_usb_interface_get_alternate: * @self: a #FuUsbInterface * * Gets the alternate setting for the interface. * * Return value: alt setting, typically zero. * * Since: 2.0.0 **/ guint8 fu_usb_interface_get_alternate(FuUsbInterface *self) { g_return_val_if_fail(FU_IS_USB_INTERFACE(self), 0); return self->iface.bAlternateSetting; } /** * fu_usb_interface_get_class: * @self: a #FuUsbInterface * * Gets the interface class, typically a #FuUsbInterfaceClassCode. * * Return value: a interface class number, e.g. 0x09 is a USB hub. * * Since: 2.0.0 **/ guint8 fu_usb_interface_get_class(FuUsbInterface *self) { g_return_val_if_fail(FU_IS_USB_INTERFACE(self), 0); return self->iface.bInterfaceClass; } /** * fu_usb_interface_get_subclass: * @self: a #FuUsbInterface * * Gets the interface subclass qualified by the class number. * See fu_usb_interface_get_class(). * * Return value: a interface subclass number. * * Since: 2.0.0 **/ guint8 fu_usb_interface_get_subclass(FuUsbInterface *self) { g_return_val_if_fail(FU_IS_USB_INTERFACE(self), 0); return self->iface.bInterfaceSubClass; } /** * fu_usb_interface_get_protocol: * @self: a #FuUsbInterface * * Gets the interface protocol qualified by the class and subclass numbers. * See fu_usb_interface_get_class() and fu_usb_interface_get_subclass(). * * Return value: a interface protocol number. * * Since: 2.0.0 **/ guint8 fu_usb_interface_get_protocol(FuUsbInterface *self) { g_return_val_if_fail(FU_IS_USB_INTERFACE(self), 0); return self->iface.bInterfaceProtocol; } /** * fu_usb_interface_get_index: * @self: a #FuUsbInterface * * Gets the index for the string descriptor. * * Return value: a string descriptor index. * * Since: 2.0.0 **/ guint8 fu_usb_interface_get_index(FuUsbInterface *self) { g_return_val_if_fail(FU_IS_USB_INTERFACE(self), 0); return self->iface.iInterface; } /** * fu_usb_interface_get_endpoints: * @self: a #FuUsbInterface * * Gets interface endpoints. * * Return value: (transfer container) (element-type FuUsbEndpoint): an array of endpoints. * * Since: 2.0.0 **/ GPtrArray * fu_usb_interface_get_endpoints(FuUsbInterface *self) { g_return_val_if_fail(FU_IS_USB_INTERFACE(self), NULL); return g_ptr_array_ref(self->endpoints); } /* private */ void fu_usb_interface_add_endpoint(FuUsbInterface *self, FuUsbEndpoint *endpoint) { g_return_if_fail(FU_IS_USB_INTERFACE(self)); g_return_if_fail(FU_IS_USB_ENDPOINT(endpoint)); g_ptr_array_add(self->endpoints, g_object_ref(endpoint)); } static gboolean fu_usb_interface_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuUsbInterface *self = FU_USB_INTERFACE(firmware); g_autoptr(FuUsbInterfaceHdr) st = NULL; /* FuUsbDescriptor */ if (!FU_FIRMWARE_CLASS(fu_usb_interface_parent_class) ->parse(firmware, stream, flags, error)) return FALSE; /* parse as proper interface with endpoints */ st = fu_usb_interface_hdr_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; self->iface.bLength = fu_usb_interface_hdr_get_length(st); self->iface.bDescriptorType = FU_USB_INTERFACE_HDR_DEFAULT_DESCRIPTOR_TYPE; self->iface.bInterfaceNumber = fu_usb_interface_hdr_get_interface_number(st); self->iface.bAlternateSetting = fu_usb_interface_hdr_get_alternate_setting(st); self->iface.bNumEndpoints = fu_usb_interface_hdr_get_num_endpoints(st); self->iface.bInterfaceClass = fu_usb_interface_hdr_get_interface_class(st); self->iface.bInterfaceSubClass = fu_usb_interface_hdr_get_interface_sub_class(st); self->iface.bInterfaceProtocol = fu_usb_interface_hdr_get_interface_protocol(st); self->iface.iInterface = fu_usb_interface_hdr_get_interface(st); fu_firmware_set_size(FU_FIRMWARE(self), self->iface.bLength); /* extra data */ if (self->iface.bLength > st->len) { g_autoptr(GByteArray) buf = NULL; buf = fu_input_stream_read_byte_array(stream, st->len, self->iface.bLength - st->len, NULL, error); if (buf == NULL) return FALSE; if (!fu_usb_interface_parse_extra(self, buf->data, buf->len, error)) return FALSE; } /* success */ return TRUE; } static void fu_usb_interface_codec_iface_init(FwupdCodecInterface *iface) { iface->add_json = fu_usb_interface_add_json; iface->from_json = fu_usb_interface_from_json; } static void fu_usb_interface_finalize(GObject *object) { FuUsbInterface *self = FU_USB_INTERFACE(object); g_ptr_array_unref(self->endpoints); G_OBJECT_CLASS(fu_usb_interface_parent_class)->finalize(object); } static void fu_usb_interface_init(FuUsbInterface *self) { self->endpoints = g_ptr_array_new_with_free_func(g_object_unref); } static void fu_usb_interface_class_init(FuUsbInterfaceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_usb_interface_finalize; firmware_class->parse = fu_usb_interface_parse; firmware_class->export = fu_usb_interface_export; } fwupd-2.0.10/libfwupdplugin/fu-usb-interface.h000066400000000000000000000016251501337203100212610ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * Copyright 2020 Emmanuel Pacaud * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-usb-descriptor.h" #define FU_TYPE_USB_INTERFACE (fu_usb_interface_get_type()) G_DECLARE_FINAL_TYPE(FuUsbInterface, fu_usb_interface, FU, USB_INTERFACE, FuUsbDescriptor) guint8 fu_usb_interface_get_number(FuUsbInterface *self) G_GNUC_NON_NULL(1); guint8 fu_usb_interface_get_alternate(FuUsbInterface *self) G_GNUC_NON_NULL(1); guint8 fu_usb_interface_get_class(FuUsbInterface *self) G_GNUC_NON_NULL(1); guint8 fu_usb_interface_get_subclass(FuUsbInterface *self) G_GNUC_NON_NULL(1); guint8 fu_usb_interface_get_protocol(FuUsbInterface *self) G_GNUC_NON_NULL(1); guint8 fu_usb_interface_get_index(FuUsbInterface *self) G_GNUC_NON_NULL(1); GPtrArray * fu_usb_interface_get_endpoints(FuUsbInterface *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-usb.rs000066400000000000000000000066251501337203100175250ustar00rootroot00000000000000// Copyright 2024 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later enum FuUsbDirection { DeviceToHost, // IN HostToDevice, // OUT } enum FuUsbRequestType { Standard, Class, Vendor, Reserved, } enum FuUsbRecipient { Device, Interface, Endpoint, Other, } #[derive(ToString)] #[repr(u8)] enum FuUsbClass { InterfaceDesc = 0x00, Audio = 0x01, Communications = 0x02, Hid = 0x03, Physical = 0x05, Image = 0x06, Printer = 0x07, MassStorage = 0x08, Hub = 0x09, Cdc_data = 0x0A, SmartCard = 0x0B, ContentSecurity = 0x0D, Video = 0x0E, PersonalHealthcare = 0x0F, AudioVideo = 0x10, Billboard = 0x11, Diagnostic = 0xDC, WirelessController = 0xE0, Miscellaneous = 0xEF, ApplicationSpecific = 0xFE, VendorSpecific = 0xFF, } enum FuUsbLangid { Invalid = 0x0000, EnglishUnitedStates = 0x0409, } #[derive(ToString, FromString)] #[repr(u8)] enum FuUsbDescriptorKind { Invalid = 0x00, Device = 0x01, Config = 0x02, String = 0x03, Interface = 0x04, Endpoint = 0x05, InterfaceAssociation = 0x0B, Bos = 0x0F, DeviceCapability = 0x10, Hid = 0x21, Report = 0x22, Physical = 0x23, Hub = 0x29, SuperspeedHub = 0x2A, SsEndpointCompanion = 0x30, } #[derive(ParseStream, Parse)] #[repr(C, packed)] struct FuUsbBaseHdr { length: u8, descriptor_type: FuUsbDescriptorKind, } #[derive(ParseStream, Default)] #[repr(C, packed)] struct FuUsbDeviceHdr { length: u8, descriptor_type: FuUsbDescriptorKind == Device, usb: u16le, device_class: FuUsbClass, device_sub_class: u8, device_protocol: u8, max_packet_size0: u8, vendor: u16le, product: u16le, device: u16le, manufacturer_idx: u8, product_idx: u8, serial_number_idx: u8, num_configurations: u8, }; #[derive(ParseStream, Default)] #[repr(C, packed)] struct FuUsbDescriptorHdr { length: u8, descriptor_type: FuUsbDescriptorKind == Config, total_length: u16le, num_interfaces: u8, configuration_value: u8, configuration: u8, attributes: u8, max_power: u8, } #[derive(ParseStream, Default)] #[repr(C, packed)] struct FuUsbHidDescriptorHdr { length: u8, descriptor_type: FuUsbDescriptorKind == Hid, hid: u16le, country_code: u8, num_descriptors: u8, class_descriptor_type: u8, class_descriptor_length: u16le, } #[derive(ParseBytes, Default)] #[repr(C, packed)] struct FuUsbDfuDescriptorHdr { length: u8, descriptor_type: FuUsbDescriptorKind == Hid, attributes: u8, detach_timeout: u16le, transfer_size: u16le, dfu_version: u16le, } #[derive(ParseStream, Default)] #[repr(C, packed)] struct FuUsbInterfaceHdr { length: u8, descriptor_type: FuUsbDescriptorKind == Interface, interface_number: u8, alternate_setting: u8, num_endpoints: u8, interface_class: FuUsbClass, interface_sub_class: u8, interface_protocol: u8, interface: u8, } #[derive(ParseStream)] #[repr(C, packed)] struct FuUsbEndpointHdr { length: u8, descriptor_type: FuUsbDescriptorKind, endpoint_address: u8, attributes: u8, max_packet_size: u16le, interval: u8, } #[derive(New, ParseStream, Default)] #[repr(C, packed)] struct FuUsbBosHdr { length: u8 = $struct_size, descriptor_type: FuUsbDescriptorKind = Bos, dev_capability_type: u8, } fwupd-2.0.10/libfwupdplugin/fu-uswid-firmware.c000066400000000000000000000237071501337203100214770ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuFirmware" #include "config.h" #include "fu-byte-array.h" #include "fu-bytes.h" #include "fu-common.h" #include "fu-coswid-firmware.h" #include "fu-input-stream.h" #include "fu-lzma-common.h" #include "fu-mem.h" #include "fu-partial-input-stream.h" #include "fu-string.h" #include "fu-uswid-firmware.h" #include "fu-uswid-struct.h" /** * FuUswidFirmware: * * A uSWID header with multiple optionally-compressed coSWID CBOR sections. * * See also: [class@FuCoswidFirmware] */ typedef struct { guint8 hdrver; FuUswidPayloadCompression compression; } FuUswidFirmwarePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuUswidFirmware, fu_uswid_firmware, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_uswid_firmware_get_instance_private(o)) #define FU_USWID_FIRMARE_MINIMUM_HDRVER 1 static void fu_uswid_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuUswidFirmware *self = FU_USWID_FIRMWARE(firmware); FuUswidFirmwarePrivate *priv = GET_PRIVATE(self); fu_xmlb_builder_insert_kx(bn, "hdrver", priv->hdrver); if (priv->compression != FU_USWID_PAYLOAD_COMPRESSION_NONE) { fu_xmlb_builder_insert_kv( bn, "compression", fu_uswid_payload_compression_to_string(priv->compression)); } } static gboolean fu_uswid_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_uswid_validate_stream(stream, offset, error); } static gboolean fu_uswid_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuUswidFirmware *self = FU_USWID_FIRMWARE(firmware); FuUswidFirmwarePrivate *priv = GET_PRIVATE(self); guint16 hdrsz; guint32 payloadsz; g_autoptr(FuStructUswid) st = NULL; g_autoptr(GBytes) payload = NULL; /* unpack */ st = fu_struct_uswid_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; /* hdrver */ priv->hdrver = fu_struct_uswid_get_hdrver(st); if (priv->hdrver < FU_USWID_FIRMARE_MINIMUM_HDRVER) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "header version was unsupported"); return FALSE; } /* hdrsz+payloadsz */ hdrsz = fu_struct_uswid_get_hdrsz(st); payloadsz = fu_struct_uswid_get_payloadsz(st); if (payloadsz == 0x0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "payload size is invalid"); return FALSE; } fu_firmware_set_size(firmware, hdrsz + payloadsz); /* flags */ if (priv->hdrver >= 0x03) { if (fu_struct_uswid_get_flags(st) & FU_USWID_HEADER_FLAG_COMPRESSED) { priv->compression = fu_struct_uswid_get_compression(st); } else { priv->compression = FU_USWID_PAYLOAD_COMPRESSION_NONE; } } else if (priv->hdrver >= 0x02) { priv->compression = fu_struct_uswid_get_flags(st) & FU_USWID_HEADER_FLAG_COMPRESSED ? FU_USWID_PAYLOAD_COMPRESSION_ZLIB : FU_USWID_PAYLOAD_COMPRESSION_NONE; } else { priv->compression = FU_USWID_PAYLOAD_COMPRESSION_NONE; } /* zlib stream */ if (priv->compression == FU_USWID_PAYLOAD_COMPRESSION_ZLIB) { g_autoptr(GConverter) conv = NULL; g_autoptr(GInputStream) istream1 = NULL; g_autoptr(GInputStream) istream2 = NULL; conv = G_CONVERTER(g_zlib_decompressor_new(G_ZLIB_COMPRESSOR_FORMAT_ZLIB)); istream1 = fu_partial_input_stream_new(stream, hdrsz, payloadsz, error); if (istream1 == NULL) { g_prefix_error(error, "failed to cut uSWID payload: "); return FALSE; } if (!g_seekable_seek(G_SEEKABLE(istream1), 0, G_SEEK_SET, NULL, error)) return FALSE; istream2 = g_converter_input_stream_new(istream1, conv); g_filter_input_stream_set_close_base_stream(G_FILTER_INPUT_STREAM(istream2), FALSE); payload = fu_input_stream_read_bytes(istream2, 0, G_MAXSIZE, NULL, error); if (payload == NULL) return FALSE; } else if (priv->compression == FU_USWID_PAYLOAD_COMPRESSION_LZMA) { g_autoptr(GBytes) payload_tmp = NULL; payload_tmp = fu_input_stream_read_bytes(stream, hdrsz, payloadsz, NULL, error); if (payload_tmp == NULL) return FALSE; payload = fu_lzma_decompress_bytes(payload_tmp, 16 * 1024 * 1024, error); if (payload == NULL) return FALSE; } else if (priv->compression == FU_USWID_PAYLOAD_COMPRESSION_NONE) { payload = fu_input_stream_read_bytes(stream, hdrsz, payloadsz, NULL, error); if (payload == NULL) return FALSE; } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "compression format 0x%x is not supported", priv->compression); return FALSE; } /* payload */ payloadsz = g_bytes_get_size(payload); for (gsize offset_tmp = 0; offset_tmp < payloadsz;) { g_autoptr(FuFirmware) firmware_coswid = fu_coswid_firmware_new(); g_autoptr(GBytes) fw2 = NULL; /* CBOR parse */ fw2 = fu_bytes_new_offset(payload, offset_tmp, payloadsz - offset_tmp, error); if (fw2 == NULL) return FALSE; if (!fu_firmware_parse_bytes(firmware_coswid, fw2, 0x0, flags | FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error)) return FALSE; if (!fu_firmware_add_image_full(firmware, firmware_coswid, error)) return FALSE; if (fu_firmware_get_size(firmware_coswid) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "coSWID read no bytes"); return FALSE; } offset_tmp += fu_firmware_get_size(firmware_coswid); } /* success */ return TRUE; } static GByteArray * fu_uswid_firmware_write(FuFirmware *firmware, GError **error) { FuUswidFirmware *self = FU_USWID_FIRMWARE(firmware); FuUswidFirmwarePrivate *priv = GET_PRIVATE(self); g_autoptr(FuStructUswid) buf = fu_struct_uswid_new(); g_autoptr(GByteArray) payload = g_byte_array_new(); g_autoptr(GBytes) payload_blob = NULL; g_autoptr(GPtrArray) images = fu_firmware_get_images(firmware); /* generate early so we know the size */ for (guint i = 0; i < images->len; i++) { FuFirmware *firmware_coswid = g_ptr_array_index(images, i); g_autoptr(GBytes) fw = fu_firmware_write(firmware_coswid, error); if (fw == NULL) return NULL; fu_byte_array_append_bytes(payload, fw); } /* zlibify */ if (priv->compression == FU_USWID_PAYLOAD_COMPRESSION_ZLIB) { g_autoptr(GConverter) conv = NULL; g_autoptr(GInputStream) istream1 = NULL; g_autoptr(GInputStream) istream2 = NULL; conv = G_CONVERTER(g_zlib_compressor_new(G_ZLIB_COMPRESSOR_FORMAT_ZLIB, -1)); istream1 = g_memory_input_stream_new_from_data(payload->data, payload->len, NULL); istream2 = g_converter_input_stream_new(istream1, conv); payload_blob = fu_input_stream_read_bytes(istream2, 0, G_MAXSIZE, NULL, error); if (payload_blob == NULL) return NULL; } else if (priv->compression == FU_USWID_PAYLOAD_COMPRESSION_LZMA) { g_autoptr(GBytes) payload_tmp = g_bytes_new(payload->data, payload->len); payload_blob = fu_lzma_compress_bytes(payload_tmp, error); if (payload_blob == NULL) return NULL; } else { payload_blob = g_bytes_new(payload->data, payload->len); } /* pack */ fu_struct_uswid_set_hdrver(buf, priv->hdrver); fu_struct_uswid_set_payloadsz(buf, g_bytes_get_size(payload_blob)); if (priv->hdrver >= 3) { guint8 flags = 0; if (priv->compression != FU_USWID_PAYLOAD_COMPRESSION_NONE) flags |= FU_USWID_HEADER_FLAG_COMPRESSED; fu_struct_uswid_set_flags(buf, flags); fu_struct_uswid_set_compression(buf, priv->compression); } else if (priv->hdrver >= 2) { guint8 flags = 0; if (priv->compression != FU_USWID_PAYLOAD_COMPRESSION_NONE) { if (priv->compression != FU_USWID_PAYLOAD_COMPRESSION_ZLIB) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "hdrver 0x02 only supports zlib compression"); return NULL; } flags |= FU_USWID_HEADER_FLAG_COMPRESSED; } fu_struct_uswid_set_flags(buf, flags); g_byte_array_set_size(buf, buf->len - 1); fu_struct_uswid_set_hdrsz(buf, buf->len); } else { g_byte_array_set_size(buf, buf->len - 2); fu_struct_uswid_set_hdrsz(buf, buf->len); } fu_byte_array_append_bytes(buf, payload_blob); /* success */ return g_steal_pointer(&buf); } static gboolean fu_uswid_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { FuUswidFirmware *self = FU_USWID_FIRMWARE(firmware); FuUswidFirmwarePrivate *priv = GET_PRIVATE(self); const gchar *str; guint64 tmp; /* simple properties */ tmp = xb_node_query_text_as_uint(n, "hdrver", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT8) priv->hdrver = tmp; /* simple properties */ str = xb_node_query_text(n, "compression", NULL); if (str != NULL) { priv->compression = fu_uswid_payload_compression_from_string(str); if (priv->compression == FU_USWID_PAYLOAD_COMPRESSION_NONE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid compression type %s", str); return FALSE; } } else { priv->compression = FU_USWID_PAYLOAD_COMPRESSION_NONE; } /* success */ return TRUE; } static void fu_uswid_firmware_init(FuUswidFirmware *self) { FuUswidFirmwarePrivate *priv = GET_PRIVATE(self); priv->hdrver = FU_USWID_FIRMARE_MINIMUM_HDRVER; priv->compression = FU_USWID_PAYLOAD_COMPRESSION_NONE; fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_STORED_SIZE); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_ALWAYS_SEARCH); fu_firmware_set_images_max(FU_FIRMWARE(self), 2000); g_type_ensure(FU_TYPE_COSWID_FIRMWARE); } static void fu_uswid_firmware_class_init(FuUswidFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_uswid_firmware_validate; firmware_class->parse = fu_uswid_firmware_parse; firmware_class->write = fu_uswid_firmware_write; firmware_class->build = fu_uswid_firmware_build; firmware_class->export = fu_uswid_firmware_export; } /** * fu_uswid_firmware_new: * * Creates a new #FuFirmware of sub type uSWID * * Since: 1.8.0 **/ FuFirmware * fu_uswid_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_USWID_FIRMWARE, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-uswid-firmware.h000066400000000000000000000006401501337203100214730ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_USWID_FIRMWARE (fu_uswid_firmware_get_type()) G_DECLARE_DERIVABLE_TYPE(FuUswidFirmware, fu_uswid_firmware, FU, USWID_FIRMWARE, FuFirmware) struct _FuUswidFirmwareClass { FuFirmwareClass parent_class; }; FuFirmware * fu_uswid_firmware_new(void); fwupd-2.0.10/libfwupdplugin/fu-uswid.rs000066400000000000000000000010461501337203100200570ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later enum FuUswidHeaderFlag { None = 0b0, Compressed = 0b1, } #[derive(ToString, FromString)] enum FuUswidPayloadCompression { None = 0x00, Zlib = 0x01, Lzma = 0x02, } #[derive(New, ValidateStream, ParseStream, Default)] #[repr(C, packed)] struct FuStructUswid { magic: Guid == "4d4f4253-bad6-ac2e-a3e6-7a52aaee3baf", hdrver: u8, hdrsz: u16le = $struct_size, payloadsz: u32le, flags: u8, compression: u8, } fwupd-2.0.10/libfwupdplugin/fu-v4l-device.c000066400000000000000000000125571501337203100204750ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuV4lDevice" #include "config.h" #ifdef HAVE_VIDEODEV2_H #include #endif #include "fu-string.h" #include "fu-usb-device.h" #include "fu-v4l-device.h" /** * FuV4lDevice * * See also: #FuUdevDevice */ typedef struct { guint8 index; FuV4lCap caps; } FuV4lDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuV4lDevice, fu_v4l_device, FU_TYPE_UDEV_DEVICE) #define GET_PRIVATE(o) (fu_v4l_device_get_instance_private(o)) static void fu_v4l_device_to_string(FuDevice *device, guint idt, GString *str) { FuV4lDevice *self = FU_V4L_DEVICE(device); FuV4lDevicePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append_hex(str, idt, "Index", priv->index); if (priv->caps != FU_V4L_CAP_NONE) { g_autofree gchar *caps = fu_v4l_cap_to_string(priv->caps); fwupd_codec_string_append(str, idt, "Caps", caps); } } /** * fu_v4l_device_get_index: * @self: a #FuV4lDevice * * Gets the video4linux device index. * * Returns: integer, or %G_MAXUINT8 on error * * Since: 2.0.0 **/ guint8 fu_v4l_device_get_index(FuV4lDevice *self) { FuV4lDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_V4L_DEVICE(self), G_MAXUINT8); return priv->index; } /** * fu_v4l_device_get_caps: * @self: a #FuV4lDevice * * Gets the video4linux device capabilities. * * NOTE: This property is only available after the device has been opened and is not available * during probe. * * Returns: integer, or 0 on error * * Since: 2.0.0 **/ FuV4lCap fu_v4l_device_get_caps(FuV4lDevice *self) { FuV4lDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_V4L_DEVICE(self), 0); return priv->caps; } static gboolean fu_v4l_device_usb_probe(FuV4lDevice *self, FuDevice *usb_device, GError **error) { /* copy the VID and PID, and reconstruct compatible IDs */ if (!fu_device_probe(usb_device, error)) return FALSE; fu_device_add_instance_str(FU_DEVICE(self), "VID", fu_device_get_instance_str(usb_device, "VID")); if (!fu_device_build_instance_id_full(FU_DEVICE(self), FU_DEVICE_INSTANCE_FLAG_QUIRKS, error, "USB", "VID", NULL)) return FALSE; fu_device_add_instance_str(FU_DEVICE(self), "VEN", fu_device_get_instance_str(usb_device, "VID")); fu_device_add_instance_str(FU_DEVICE(self), "DEV", fu_device_get_instance_str(usb_device, "PID")); if (!fu_device_build_instance_id(FU_DEVICE(self), error, "VIDEO4LINUX", "VEN", "DEV", NULL)) return FALSE; fu_device_incorporate(FU_DEVICE(self), usb_device, FU_DEVICE_INCORPORATE_FLAG_VENDOR_IDS | FU_DEVICE_INCORPORATE_FLAG_VID | FU_DEVICE_INCORPORATE_FLAG_PID | FU_DEVICE_INCORPORATE_FLAG_PHYSICAL_ID); /* success */ return TRUE; } static gboolean fu_v4l_device_probe(FuDevice *device, GError **error) { FuV4lDevice *self = FU_V4L_DEVICE(device); g_autofree gchar *attr_index = NULL; g_autofree gchar *attr_name = NULL; g_autoptr(FuDevice) usb_device = NULL; /* name */ attr_name = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(device), "name", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (attr_name != NULL) fu_device_set_name(device, attr_name); /* device index */ attr_index = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(device), "index", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (attr_index != NULL) { guint64 index64 = 0; if (!fu_strtoull(attr_index, &index64, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "failed to parse index: "); return FALSE; } } /* v4l devices are weird in that the vendor and model are generic */ usb_device = fu_device_get_backend_parent_with_subsystem(device, "usb:usb_device", NULL); if (usb_device != NULL) { if (!fu_v4l_device_usb_probe(self, usb_device, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_v4l_device_setup(FuDevice *device, GError **error) { #ifdef HAVE_VIDEODEV2_H FuV4lDevice *self = FU_V4L_DEVICE(device); FuV4lDevicePrivate *priv = GET_PRIVATE(self); struct v4l2_capability v2cap = {0}; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); if (!fu_ioctl_execute(ioctl, VIDIOC_QUERYCAP, (guint8 *)&v2cap, sizeof(v2cap), NULL, 50, /* ms */ FU_IOCTL_FLAG_NONE, error)) return FALSE; if (v2cap.capabilities & V4L2_CAP_DEVICE_CAPS) priv->caps = v2cap.device_caps; else priv->caps = v2cap.capabilities; #endif /* success */ return TRUE; } static void fu_v4l_device_incorporate(FuDevice *device, FuDevice *donor) { FuV4lDevice *self = FU_V4L_DEVICE(device); FuV4lDevicePrivate *priv = GET_PRIVATE(self); priv->index = fu_v4l_device_get_index(FU_V4L_DEVICE(donor)); priv->caps = fu_v4l_device_get_caps(FU_V4L_DEVICE(donor)); } static void fu_v4l_device_init(FuV4lDevice *self) { fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); } static void fu_v4l_device_class_init(FuV4lDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_v4l_device_probe; device_class->setup = fu_v4l_device_setup; device_class->to_string = fu_v4l_device_to_string; device_class->incorporate = fu_v4l_device_incorporate; } fwupd-2.0.10/libfwupdplugin/fu-v4l-device.h000066400000000000000000000010151501337203100204650ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-udev-device.h" #include "fu-v4l-struct.h" #define FU_TYPE_V4L_DEVICE (fu_v4l_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuV4lDevice, fu_v4l_device, FU, V4L_DEVICE, FuUdevDevice) struct _FuV4lDeviceClass { FuUdevDeviceClass parent_class; }; guint8 fu_v4l_device_get_index(FuV4lDevice *self) G_GNUC_NON_NULL(1); FuV4lCap fu_v4l_device_get_caps(FuV4lDevice *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-v4l.rs000066400000000000000000000040711501337203100174320ustar00rootroot00000000000000// Copyright 2024 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToBitString)] enum FuV4lCap { None = 0x00000000, VideoCapture = 0x00000001, // video capture device VideoOutput = 0x00000002, // video output device VideoOverlay = 0x00000004, // video overlay VbiCapture = 0x00000010, // raw VBI capture device VbiOutput = 0x00000020, // raw VBI output device SlicedVbiCapture = 0x00000040, // sliced VBI capture device SlicedVbiOutput = 0x00000080, // sliced VBI output device RdsCapture = 0x00000100, // RDS data capture VideoOutputOverlay = 0x00000200, // video output overlay HwFreqSeek = 0x00000400, // hardware frequency seek RdsOutput = 0x00000800, // RDS encoder VideoCaptureMplane = 0x00001000, // video capture device that supports multiplanar formats VideoOutputMplane = 0x00002000, // video output device that supports multiplanar formats VideoM2mMplane = 0x00004000, // video mem-to-mem device that supports multiplanar formats VideoM2m = 0x00008000, // video mem-to-mem device Tuner = 0x00010000, // has a tuner Audio = 0x00020000, // has audio support Radio = 0x00040000, // is a radio device Modulator = 0x00080000, // has a modulator SdrCapture = 0x00100000, // SDR capture device ExtPixFormat = 0x00200000, // supports the extended pixel format SdrOutput = 0x00400000, // SDR output device MetaCapture = 0x00800000, // metadata capture device Readwrite = 0x01000000, // read/write systemcalls Streaming = 0x04000000, // streaming I/O ioctls MetaOutput = 0x08000000, // metadata output device Touch = 0x10000000, // touch device IoMc = 0x20000000, // input/output controlled by the media controller DeviceCaps = 0x80000000, // sets device capabilities field } fwupd-2.0.10/libfwupdplugin/fu-version-common.c000066400000000000000000000520211501337203100214740ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuCommon" #include "config.h" #include #include "fwupd-enums.h" #include "fwupd-error.h" #include "fu-string.h" #include "fu-version-common.h" #define FU_COMMON_VERSION_DECODE_BCD(val) ((((val) >> 4) & 0x0f) * 10 + ((val) & 0x0f)) static gchar * fu_version_ensure_semver_internal(const gchar *version); /** * fu_version_from_uint64: * @val: a raw version number * @kind: version kind used for formatting, e.g. %FWUPD_VERSION_FORMAT_QUAD * * Returns a dotted decimal version string from a 64 bit number. * * Returns: a version number, e.g. `1.2.3.4`, or %NULL if not supported * * Since: 1.8.2 **/ gchar * fu_version_from_uint64(guint64 val, FwupdVersionFormat kind) { if (kind == FWUPD_VERSION_FORMAT_QUAD) { /* AABB.CCDD.EEFF.GGHH */ return g_strdup_printf("%" G_GUINT64_FORMAT "." "%" G_GUINT64_FORMAT "." "%" G_GUINT64_FORMAT "." "%" G_GUINT64_FORMAT "", (val >> 48) & 0xffff, (val >> 32) & 0xffff, (val >> 16) & 0xffff, val & 0xffff); } if (kind == FWUPD_VERSION_FORMAT_PAIR) { /* AABBCCDD.EEFFGGHH */ return g_strdup_printf("%" G_GUINT64_FORMAT ".%" G_GUINT64_FORMAT "", (val >> 32) & 0xffffffff, val & 0xffffffff); } if (kind == FWUPD_VERSION_FORMAT_NUMBER || kind == FWUPD_VERSION_FORMAT_PLAIN) { /* AABBCCDD */ return g_strdup_printf("%" G_GUINT64_FORMAT, val); } if (kind == FWUPD_VERSION_FORMAT_HEX) { /* 0xAABBCCDDEEFFGGHH */ return g_strdup_printf("0x%08x%08x", (guint32)(val >> 32), (guint32)(val & 0xffffffff)); } g_critical("failed to convert version format %s: %" G_GUINT64_FORMAT "", fwupd_version_format_to_string(kind), val); return NULL; } /** * fu_version_from_uint32: * @val: a uint32le version number * @kind: version kind used for formatting, e.g. %FWUPD_VERSION_FORMAT_TRIPLET * * Returns a dotted decimal version string from a 32 bit number. * * Returns: a version number, e.g. `1.0.3`, or %NULL if not supported * * Since: 1.8.2 **/ gchar * fu_version_from_uint32(guint32 val, FwupdVersionFormat kind) { if (kind == FWUPD_VERSION_FORMAT_QUAD) { /* AA.BB.CC.DD */ return g_strdup_printf("%u.%u.%u.%u", (val >> 24) & 0xff, (val >> 16) & 0xff, (val >> 8) & 0xff, val & 0xff); } if (kind == FWUPD_VERSION_FORMAT_TRIPLET) { /* AA.BB.CCDD */ return g_strdup_printf("%u.%u.%u", (val >> 24) & 0xff, (val >> 16) & 0xff, val & 0xffff); } if (kind == FWUPD_VERSION_FORMAT_PAIR) { /* AABB.CCDD */ return g_strdup_printf("%u.%u", (val >> 16) & 0xffff, val & 0xffff); } if (kind == FWUPD_VERSION_FORMAT_NUMBER || kind == FWUPD_VERSION_FORMAT_PLAIN) { /* AABBCCDD */ return g_strdup_printf("%" G_GUINT32_FORMAT, val); } if (kind == FWUPD_VERSION_FORMAT_BCD) { /* AA.BB.CC.DD, but BCD */ return g_strdup_printf("%u.%u.%u.%u", FU_COMMON_VERSION_DECODE_BCD(val >> 24), FU_COMMON_VERSION_DECODE_BCD(val >> 16), FU_COMMON_VERSION_DECODE_BCD(val >> 8), FU_COMMON_VERSION_DECODE_BCD(val)); } if (kind == FWUPD_VERSION_FORMAT_INTEL_ME) { /* aaa+11.bbbbb.cccccccc.dddddddddddddddd */ return g_strdup_printf("%u.%u.%u.%u", ((val >> 29) & 0x07) + 0x0b, (val >> 24) & 0x1f, (val >> 16) & 0xff, val & 0xffff); } if (kind == FWUPD_VERSION_FORMAT_INTEL_ME2) { /* A.B.CC.DDDD */ return g_strdup_printf("%u.%u.%u.%u", (val >> 28) & 0x0f, (val >> 24) & 0x0f, (val >> 16) & 0xff, val & 0xffff); } if (kind == FWUPD_VERSION_FORMAT_INTEL_CSME19) { /* aaa+19.bbbbb.cccccccc.dddddddddddddddd */ return g_strdup_printf("%u.%u.%u.%u", ((val >> 29) & 0x07) + 19, (val >> 24) & 0x1f, (val >> 16) & 0xff, val & 0xffff); } if (kind == FWUPD_VERSION_FORMAT_SURFACE_LEGACY) { /* 10b.12b.10b */ return g_strdup_printf("%u.%u.%u", (val >> 22) & 0x3ff, (val >> 10) & 0xfff, val & 0x3ff); } if (kind == FWUPD_VERSION_FORMAT_SURFACE) { /* 8b.16b.8b */ return g_strdup_printf("%u.%u.%u", (val >> 24) & 0xff, (val >> 8) & 0xffff, val & 0xff); } if (kind == FWUPD_VERSION_FORMAT_DELL_BIOS) { /* BB.CC.DD */ return g_strdup_printf("%u.%u.%u", (val >> 16) & 0xff, (val >> 8) & 0xff, val & 0xff); } if (kind == FWUPD_VERSION_FORMAT_HEX) { /* 0xAABBCCDD */ return g_strdup_printf("0x%08x", val); } if (kind == FWUPD_VERSION_FORMAT_DELL_BIOS_MSB) { /* AA.BB.CC */ return g_strdup_printf("%u.%u.%u", (val >> 24) & 0xff, (val >> 16) & 0xff, (val >> 8) & 0xff); } g_critical("failed to convert version format %s: %u", fwupd_version_format_to_string(kind), val); return NULL; } /** * fu_version_from_uint32_hex: * @val: a uint32le version number * @kind: version kind used for formatting, e.g. %FWUPD_VERSION_FORMAT_TRIPLET * * Returns a dotted decimal hex string from a 32 bit number. * * Returns: a version number, e.g. `1a.0.d3`, or %NULL if not supported * * Since: 2.0.0 **/ gchar * fu_version_from_uint32_hex(guint32 val, FwupdVersionFormat kind) { if (kind == FWUPD_VERSION_FORMAT_HEX) return g_strdup_printf("0x%x", val); if (kind == FWUPD_VERSION_FORMAT_NUMBER) return g_strdup_printf("%x", val); if (kind == FWUPD_VERSION_FORMAT_PAIR) return g_strdup_printf("%x.%x", (val >> 16) & 0xffff, val & 0xffff); if (kind == FWUPD_VERSION_FORMAT_TRIPLET) { return g_strdup_printf("%x.%x.%x", (val >> 24) & 0xff, (val >> 16) & 0xff, val & 0xffff); } if (kind == FWUPD_VERSION_FORMAT_DELL_BIOS) { return g_strdup_printf("%x.%x.%x", (val >> 16) & 0xff, (val >> 8) & 0xff, val & 0xff); } if (kind == FWUPD_VERSION_FORMAT_DELL_BIOS_MSB) { return g_strdup_printf("%x.%x.%x", (val >> 24) & 0xff, (val >> 16) & 0xff, (val >> 8) & 0xff); } if (kind == FWUPD_VERSION_FORMAT_QUAD) { return g_strdup_printf("%x.%x.%x.%x", (val >> 24) & 0xff, (val >> 16) & 0xff, (val >> 8) & 0xff, val & 0xff); } if (kind == FWUPD_VERSION_FORMAT_BCD) { return g_strdup_printf("%x.%x.%x.%x", FU_COMMON_VERSION_DECODE_BCD(val >> 24), FU_COMMON_VERSION_DECODE_BCD(val >> 16), FU_COMMON_VERSION_DECODE_BCD(val >> 8), FU_COMMON_VERSION_DECODE_BCD(val)); } g_critical("failed to convert version format %s: %u", fwupd_version_format_to_string(kind), val); return NULL; } /** * fu_version_from_uint24: * @val: a uint24le version number * @kind: version kind used for formatting, e.g. %FWUPD_VERSION_FORMAT_TRIPLET * * Returns a dotted decimal version string from a 24 bit number. * * Returns: a version number, e.g. `1.0.3`, or %NULL if not supported * * Since: 1.8.9 **/ gchar * fu_version_from_uint24(guint32 val, FwupdVersionFormat kind) { if (kind == FWUPD_VERSION_FORMAT_TRIPLET) { /* BB.CC.DD */ return g_strdup_printf("%u.%u.%u", (val >> 24) & 0xff, (val >> 16) & 0xff, val & 0xffff); } if (kind == FWUPD_VERSION_FORMAT_PAIR) { /* BB.CCDD */ return g_strdup_printf("%u.%u", (val >> 16) & 0xff, val & 0xffff); } if (kind == FWUPD_VERSION_FORMAT_NUMBER || kind == FWUPD_VERSION_FORMAT_PLAIN) { /* BBCCDD */ return g_strdup_printf("%" G_GUINT32_FORMAT, val); } if (kind == FWUPD_VERSION_FORMAT_HEX) { /* 0xBBCCDD */ return g_strdup_printf("0x%06x", val); } g_critical("failed to convert version format %s: %u", fwupd_version_format_to_string(kind), val); return NULL; } /** * fu_version_from_uint16: * @val: a uint16le version number * @kind: version kind used for formatting, e.g. %FWUPD_VERSION_FORMAT_TRIPLET * * Returns a dotted decimal version string from a 16 bit number. * * Returns: a version number, e.g. `1.3`, or %NULL if not supported * * Since: 1.8.2 **/ gchar * fu_version_from_uint16(guint16 val, FwupdVersionFormat kind) { if (kind == FWUPD_VERSION_FORMAT_BCD) { return g_strdup_printf("%i.%i", FU_COMMON_VERSION_DECODE_BCD(val >> 8), FU_COMMON_VERSION_DECODE_BCD(val)); } if (kind == FWUPD_VERSION_FORMAT_PAIR) { return g_strdup_printf("%u.%u", (guint)(val >> 8) & 0xff, (guint)val & 0xff); } if (kind == FWUPD_VERSION_FORMAT_QUAD) { return g_strdup_printf("%u.%u.%u.%u", (guint)(val >> 12) & 0xF, (guint)(val >> 8) & 0xF, (guint)(val >> 4) & 0xF, (guint)val & 0xF); } if (kind == FWUPD_VERSION_FORMAT_TRIPLET) { return g_strdup_printf("%u.%u.%u", (guint)(val >> 12) & 0xF, (guint)(val >> 8) & 0xF, (guint)val & 0xFF); } if (kind == FWUPD_VERSION_FORMAT_NUMBER || kind == FWUPD_VERSION_FORMAT_PLAIN) { return g_strdup_printf("%" G_GUINT16_FORMAT, val); } if (kind == FWUPD_VERSION_FORMAT_HEX) { /* 0xAABB */ return g_strdup_printf("0x%04x", val); } g_critical("failed to convert version format %s: %u", fwupd_version_format_to_string(kind), val); return NULL; } /** * fu_version_from_uint16_hex: * @val: a uint16le version number * @kind: version kind used for formatting, e.g. %FWUPD_VERSION_FORMAT_TRIPLET * * Returns a dotted hex version string from a 16 bit number. * * Returns: a version number, e.g. `1a.f3`, or %NULL if not supported * * Since: 2.0.0 **/ gchar * fu_version_from_uint16_hex(guint16 val, FwupdVersionFormat kind) { if (kind == FWUPD_VERSION_FORMAT_NUMBER) return g_strdup_printf("%x", val); if (kind == FWUPD_VERSION_FORMAT_HEX) return g_strdup_printf("0x%x", val); if (kind == FWUPD_VERSION_FORMAT_PAIR) return g_strdup_printf("%x.%x", (guint)(val >> 8) & 0xff, (guint)val & 0xff); if (kind == FWUPD_VERSION_FORMAT_BCD) { return g_strdup_printf("%x.%x", (guint)FU_COMMON_VERSION_DECODE_BCD(val >> 8), (guint)FU_COMMON_VERSION_DECODE_BCD(val)); } if (kind == FWUPD_VERSION_FORMAT_TRIPLET) { return g_strdup_printf("%x.%x.%x", (guint)(val >> 12) & 0xF, (guint)(val >> 8) & 0xF, (guint)val & 0xFF); } g_critical("failed to convert version format %s: %u", fwupd_version_format_to_string(kind), val); return NULL; } static gint fu_version_compare_char(gchar chr1, gchar chr2) { if (chr1 == chr2) return 0; if (chr1 == '~') return -1; if (chr2 == '~') return 1; return chr1 < chr2 ? -1 : 1; } static gint fu_version_compare_chunk(const gchar *str1, const gchar *str2) { guint i; /* trivial */ if (g_strcmp0(str1, str2) == 0) return 0; if (str1 == NULL) return 1; if (str2 == NULL) return -1; /* check each char of the chunk */ for (i = 0; str1[i] != '\0' && str2[i] != '\0'; i++) { gint rc = fu_version_compare_char(str1[i], str2[i]); if (rc != 0) return rc; } return fu_version_compare_char(str1[i], str2[i]); } static gboolean _g_ascii_is_digits(const gchar *str) { g_return_val_if_fail(str != NULL, FALSE); for (gsize i = 0; str[i] != '\0'; i++) { if (!g_ascii_isdigit(str[i])) return FALSE; } return TRUE; } static guint fu_version_format_number_sections(FwupdVersionFormat fmt) { if (fmt == FWUPD_VERSION_FORMAT_PLAIN || fmt == FWUPD_VERSION_FORMAT_NUMBER || fmt == FWUPD_VERSION_FORMAT_HEX) return 1; if (fmt == FWUPD_VERSION_FORMAT_PAIR || fmt == FWUPD_VERSION_FORMAT_BCD) return 2; if (fmt == FWUPD_VERSION_FORMAT_TRIPLET || fmt == FWUPD_VERSION_FORMAT_SURFACE_LEGACY || fmt == FWUPD_VERSION_FORMAT_SURFACE || fmt == FWUPD_VERSION_FORMAT_DELL_BIOS || fmt == FWUPD_VERSION_FORMAT_DELL_BIOS_MSB) return 3; if (fmt == FWUPD_VERSION_FORMAT_QUAD || fmt == FWUPD_VERSION_FORMAT_INTEL_ME || fmt == FWUPD_VERSION_FORMAT_INTEL_ME2 || fmt == FWUPD_VERSION_FORMAT_INTEL_CSME19) return 4; return 0; } /** * fu_version_ensure_semver: * @version: (nullable): a version number, e.g. ` V1.2.3 ` * @fmt: a version format, e.g. %FWUPD_VERSION_FORMAT_TRIPLET * * Builds a semver from the possibly crazy version number. Depending on the @semver value * the string will be split and a string in the correct format will be returned. * * Returns: a version number, e.g. `1.2.3`, or %NULL if the version was not valid * * Since: 1.8.2 */ gchar * fu_version_ensure_semver(const gchar *version, FwupdVersionFormat fmt) { guint sections_actual; guint sections_expected = fu_version_format_number_sections(fmt); g_autofree gchar *tmp = NULL; g_auto(GStrv) split = NULL; g_autoptr(GString) str = g_string_new(NULL); /* split into all sections */ tmp = fu_version_ensure_semver_internal(version); if (tmp == NULL) return NULL; if (fmt == FWUPD_VERSION_FORMAT_UNKNOWN) return g_steal_pointer(&tmp); split = g_strsplit(tmp, ".", -1); sections_actual = g_strv_length(split); /* add zero sections as required */ if (sections_actual < sections_expected) { for (guint i = 0; i < sections_expected - sections_actual; i++) { if (str->len > 0) g_string_append(str, "."); g_string_append(str, "0"); } } /* only add enough sections for the format */ for (guint i = 0; i < sections_actual && i < sections_expected; i++) { if (str->len > 0) g_string_append(str, "."); g_string_append(str, split[i]); } /* success */ return g_string_free(g_steal_pointer(&str), FALSE); } static gchar * fu_version_ensure_semver_internal(const gchar *version) { gboolean dot_valid = FALSE; guint digit_cnt = 0; g_autoptr(GString) version_safe = g_string_new(NULL); /* invalid */ if (version == NULL) return NULL; /* hex prefix */ if (g_str_has_prefix(version, "0x")) { return fu_version_parse_from_format(version, FWUPD_VERSION_FORMAT_TRIPLET); } /* make sane */ for (guint i = 0; version[i] != '\0'; i++) { if (g_ascii_isdigit(version[i])) { g_string_append_c(version_safe, version[i]); digit_cnt++; dot_valid = TRUE; continue; } if (version[i] == '-' || version[i] == '~') { g_string_append_c(version_safe, '.'); dot_valid = FALSE; continue; } if (version[i] == '.' && dot_valid && version[i + 1] != '\0') { g_string_append_c(version_safe, version[i]); dot_valid = FALSE; continue; } } /* remove any trailing dot */ if (version_safe->len > 0 && version_safe->str[version_safe->len - 1] == '.') g_string_truncate(version_safe, version_safe->len - 1); /* found no digits */ if (digit_cnt == 0) return NULL; return g_string_free(g_steal_pointer(&version_safe), FALSE); } /** * fu_version_parse_from_format * @version: (nullable): a version number * @fmt: a version format, e.g. %FWUPD_VERSION_FORMAT_TRIPLET * * Returns a dotted decimal version string from a version string using @fmt. * The supported formats are: * * - Dotted decimal, e.g. `1.2.3` * - Base 16, a hex number *with* a 0x prefix, e.g. `0x10203` * - Base 10, a string containing just [0-9], e.g. `66051` * - Date in YYYYMMDD format, e.g. `20150915` * * Anything with a `.` or that doesn't match `[0-9]` or `0x[a-f,0-9]` is considered * a string and returned without modification. * * Returns: a version number, e.g. `1.0.3`, or %NULL on error * * Since: 1.8.2 */ gchar * fu_version_parse_from_format(const gchar *version, FwupdVersionFormat fmt) { guint64 tmp = 0; /* sanity check */ if (version == NULL) return NULL; /* already dotted decimal */ if (g_strstr_len(version, -1, ".") != NULL) return g_strdup(version); /* is a date */ if (g_str_has_prefix(version, "20") && strlen(version) == 8) return g_strdup(version); /* convert 0x prefixed strings to dotted decimal */ if (!fu_strtoull(version, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, NULL)) return g_strdup(version); if (tmp == 0) return g_strdup(version); return fu_version_from_uint32((guint32)tmp, fmt); } /** * fu_version_guess_format: * @version: (nullable): a version number, e.g. `1.2.3` * * Guesses the version format from the version number. This is only a heuristic * and plugins and components should explicitly set the version format whenever * possible. * * If the version format cannot be guessed with any degree of accuracy, the * %FWUPD_VERSION_FORMAT_UNKNOWN constant is returned. * * Returns: a version format, e.g. %FWUPD_VERSION_FORMAT_QUAD * * Since: 1.8.2 */ FwupdVersionFormat fu_version_guess_format(const gchar *version) { guint sz; g_auto(GStrv) split = NULL; /* nothing to use */ if (version == NULL || version[0] == '\0') return FWUPD_VERSION_FORMAT_UNKNOWN; /* no dots, assume just text */ split = g_strsplit(version, ".", -1); sz = g_strv_length(split); if (sz == 1) { if (g_str_has_prefix(version, "0x") || _g_ascii_is_digits(version)) return FWUPD_VERSION_FORMAT_NUMBER; return FWUPD_VERSION_FORMAT_PLAIN; } /* check for only-digit semver version */ for (guint i = 0; split[i] != NULL; i++) { /* check sections are plain numbers */ if (!_g_ascii_is_digits(split[i])) return FWUPD_VERSION_FORMAT_PLAIN; } /* the most common formats */ if (sz == 2) return FWUPD_VERSION_FORMAT_PAIR; if (sz == 3) return FWUPD_VERSION_FORMAT_TRIPLET; if (sz == 4) return FWUPD_VERSION_FORMAT_QUAD; /* unknown! */ return FWUPD_VERSION_FORMAT_UNKNOWN; } static FwupdVersionFormat fu_version_format_convert_base(FwupdVersionFormat fmt) { if (fmt == FWUPD_VERSION_FORMAT_INTEL_ME || fmt == FWUPD_VERSION_FORMAT_INTEL_ME2 || fmt == FWUPD_VERSION_FORMAT_INTEL_CSME19) return FWUPD_VERSION_FORMAT_QUAD; if (fmt == FWUPD_VERSION_FORMAT_DELL_BIOS || fmt == FWUPD_VERSION_FORMAT_DELL_BIOS_MSB) return FWUPD_VERSION_FORMAT_TRIPLET; if (fmt == FWUPD_VERSION_FORMAT_BCD) return FWUPD_VERSION_FORMAT_PAIR; if (fmt == FWUPD_VERSION_FORMAT_HEX) return FWUPD_VERSION_FORMAT_NUMBER; return fmt; } /** * fu_version_verify_format: * @version: (not nullable): a string, e.g. `0x1234` * @fmt: a version format * @error: (nullable): optional return location for an error * * Verifies if a version matches the input format. * * Returns: TRUE or FALSE * * Since: 1.8.2 **/ gboolean fu_version_verify_format(const gchar *version, FwupdVersionFormat fmt, GError **error) { FwupdVersionFormat fmt_guess; g_return_val_if_fail(version != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* don't touch */ if (fmt == FWUPD_VERSION_FORMAT_PLAIN) return TRUE; /* nothing we can check for */ if (fmt == FWUPD_VERSION_FORMAT_UNKNOWN) return TRUE; /* check the base format */ fmt_guess = fu_version_guess_format(version); if (fmt == FWUPD_VERSION_FORMAT_BCD && (fmt_guess == FWUPD_VERSION_FORMAT_PAIR || fmt_guess == FWUPD_VERSION_FORMAT_QUAD)) return TRUE; if (fmt_guess != fu_version_format_convert_base(fmt)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "%s is not a valid %s (guessed %s)", version, fwupd_version_format_to_string(fmt), fwupd_version_format_to_string(fmt_guess)); return FALSE; } return TRUE; } static gint fu_version_compare_safe(const gchar *version_a, const gchar *version_b) { guint longest_split; g_auto(GStrv) split_a = NULL; g_auto(GStrv) split_b = NULL; /* sanity check */ if (version_a == NULL || version_b == NULL) return G_MAXINT; /* optimization */ if (g_strcmp0(version_a, version_b) == 0) return 0; /* split into sections, and try to parse */ split_a = g_strsplit(version_a, ".", -1); split_b = g_strsplit(version_b, ".", -1); longest_split = MAX(g_strv_length(split_a), g_strv_length(split_b)); for (guint i = 0; i < longest_split; i++) { gchar *endptr_a = NULL; gchar *endptr_b = NULL; gint64 ver_a; gint64 ver_b; /* we lost or gained a dot */ if (split_a[i] == NULL) return -1; if (split_b[i] == NULL) return 1; /* compare integers */ ver_a = g_ascii_strtoll(split_a[i], &endptr_a, 10); /* nocheck:blocked */ ver_b = g_ascii_strtoll(split_b[i], &endptr_b, 10); /* nocheck:blocked */ if (ver_a < ver_b) return -1; if (ver_a > ver_b) return 1; /* compare strings */ if ((endptr_a != NULL && endptr_a[0] != '\0') || (endptr_b != NULL && endptr_b[0] != '\0')) { gint rc = fu_version_compare_chunk(endptr_a, endptr_b); if (rc < 0) return -1; if (rc > 0) return 1; } } /* we really shouldn't get here */ return 0; } /** * fu_version_compare: * @version_a: (nullable): the semver release version, e.g. `1.2.3` * @version_b: (nullable): the semver release version, e.g. `1.2.3.1` * @fmt: a version format, e.g. %FWUPD_VERSION_FORMAT_PLAIN * * Compares version numbers for sorting taking into account the version format * if required. * * Returns: -1 if a < b, +1 if a > b, 0 if they are equal, and %G_MAXINT on error * * Since: 1.8.2 */ gint fu_version_compare(const gchar *version_a, const gchar *version_b, FwupdVersionFormat fmt) { if (fmt == FWUPD_VERSION_FORMAT_PLAIN) return g_strcmp0(version_a, version_b); if (fmt == FWUPD_VERSION_FORMAT_HEX) { g_autofree gchar *hex_a = NULL; g_autofree gchar *hex_b = NULL; hex_a = fu_version_parse_from_format(version_a, fmt); hex_b = fu_version_parse_from_format(version_b, fmt); return fu_version_compare_safe(hex_a, hex_b); } return fu_version_compare_safe(version_a, version_b); } fwupd-2.0.10/libfwupdplugin/fu-version-common.h000066400000000000000000000021031501337203100214750ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include gint fu_version_compare(const gchar *version_a, const gchar *version_b, FwupdVersionFormat fmt); gchar * fu_version_from_uint64(guint64 val, FwupdVersionFormat kind); gchar * fu_version_from_uint32(guint32 val, FwupdVersionFormat kind); gchar * fu_version_from_uint32_hex(guint32 val, FwupdVersionFormat kind); gchar * fu_version_from_uint24(guint32 val, FwupdVersionFormat kind); gchar * fu_version_from_uint16(guint16 val, FwupdVersionFormat kind); gchar * fu_version_from_uint16_hex(guint16 val, FwupdVersionFormat kind); gchar * fu_version_parse_from_format(const gchar *version, FwupdVersionFormat fmt); gchar * fu_version_ensure_semver(const gchar *version, FwupdVersionFormat fmt) G_GNUC_NON_NULL(1); FwupdVersionFormat fu_version_guess_format(const gchar *version); gboolean fu_version_verify_format(const gchar *version, FwupdVersionFormat fmt, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-volume-private.h000066400000000000000000000012011501337203100214770ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2019 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-volume.h" FuVolume * fu_volume_new_from_mount_path(const gchar *mount_path) G_GNUC_NON_NULL(1); void fu_volume_set_partition_kind(FuVolume *self, const gchar *partition_kind) G_GNUC_NON_NULL(1, 2); void fu_volume_set_partition_uuid(FuVolume *self, const gchar *partition_uuid) G_GNUC_NON_NULL(1, 2); /* for tests */ void fu_volume_set_filesystem_free(FuVolume *self, guint64 filesystem_free) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-volume.c000066400000000000000000000750261501337203100200420ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2019 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuVolume" #include "config.h" #include #include #if defined(HAVE_IOCTL_H) && defined(HAVE_BLKSSZGET) #include #include #include #endif #include "fwupd-error.h" #include "fu-common-private.h" #include "fu-volume-private.h" /** * FuVolume: * * Volume abstraction that uses UDisks */ struct _FuVolume { GObject parent_instance; GDBusProxy *proxy_blk; GDBusProxy *proxy_fs; GDBusProxy *proxy_part; gchar *mount_path; /* only when mounted ourselves */ gchar *partition_kind; /* only for tests */ gchar *partition_uuid; /* only for tests */ guint64 fs_free; /* only for tests */ }; enum { PROP_0, PROP_MOUNT_PATH, PROP_PROXY_BLOCK, PROP_PROXY_FILESYSTEM, PROP_PROXY_PARTITION, PROP_LAST }; static void fu_volume_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_EXTENDED(FuVolume, fu_volume, G_TYPE_OBJECT, 0, G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fu_volume_codec_iface_init)) static void fu_volume_add_json(FwupdCodec *codec, JsonBuilder *builder, FwupdCodecFlags flags) { FuVolume *self = FU_VOLUME(codec); g_autofree gchar *mount_point = fu_volume_get_mount_point(self); g_autofree gchar *partition_kind = fu_volume_get_partition_kind(self); g_autofree gchar *partition_name = fu_volume_get_partition_name(self); g_autofree gchar *partition_uuid = fu_volume_get_partition_uuid(self); fwupd_codec_json_append_bool(builder, "IsMounted", fu_volume_is_mounted(self)); fwupd_codec_json_append_bool(builder, "IsEncrypted", fu_volume_is_encrypted(self)); fwupd_codec_json_append_int(builder, "Size", fu_volume_get_size(self)); fwupd_codec_json_append_int(builder, "BlockSize", fu_volume_get_block_size(self, NULL)); fwupd_codec_json_append(builder, "MountPoint", mount_point); fwupd_codec_json_append(builder, "PartitionKind", partition_kind); fwupd_codec_json_append(builder, "PartitionName", partition_name); fwupd_codec_json_append_int(builder, "PartitionSize", fu_volume_get_partition_size(self)); fwupd_codec_json_append_int(builder, "PartitionOffset", fu_volume_get_partition_offset(self)); fwupd_codec_json_append_int(builder, "PartitionNumber", fu_volume_get_partition_number(self)); fwupd_codec_json_append(builder, "PartitionUuid", partition_uuid); } static void fu_volume_finalize(GObject *obj) { FuVolume *self = FU_VOLUME(obj); g_free(self->mount_path); g_free(self->partition_kind); g_free(self->partition_uuid); if (self->proxy_blk != NULL) g_object_unref(self->proxy_blk); if (self->proxy_fs != NULL) g_object_unref(self->proxy_fs); if (self->proxy_part != NULL) g_object_unref(self->proxy_part); G_OBJECT_CLASS(fu_volume_parent_class)->finalize(obj); } static void fu_volume_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuVolume *self = FU_VOLUME(object); switch (prop_id) { case PROP_MOUNT_PATH: g_value_set_string(value, self->mount_path); break; case PROP_PROXY_BLOCK: g_value_set_object(value, self->proxy_blk); break; case PROP_PROXY_FILESYSTEM: g_value_set_object(value, self->proxy_fs); break; case PROP_PROXY_PARTITION: g_value_set_object(value, self->proxy_part); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_volume_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuVolume *self = FU_VOLUME(object); switch (prop_id) { case PROP_MOUNT_PATH: self->mount_path = g_value_dup_string(value); break; case PROP_PROXY_BLOCK: self->proxy_blk = g_value_dup_object(value); break; case PROP_PROXY_FILESYSTEM: self->proxy_fs = g_value_dup_object(value); break; case PROP_PROXY_PARTITION: self->proxy_part = g_value_dup_object(value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_volume_class_init(FuVolumeClass *klass) { GParamSpec *pspec; GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_volume_finalize; object_class->get_property = fu_volume_get_property; object_class->set_property = fu_volume_set_property; /** * FuVolume:proxy-block: * * The proxy for the block interface. * * Since: 1.4.6 */ pspec = g_param_spec_object("proxy-block", NULL, NULL, G_TYPE_DBUS_PROXY, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_PROXY_BLOCK, pspec); /** * FuVolume:proxy-filesystem: * * The proxy for the filesystem interface. * * Since: 1.4.6 */ pspec = g_param_spec_object("proxy-filesystem", NULL, NULL, G_TYPE_DBUS_PROXY, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_PROXY_FILESYSTEM, pspec); /** * FuVolume:mount-path: * * The UNIX mount path. * * Since: 1.4.6 */ pspec = g_param_spec_string("mount-path", NULL, NULL, NULL, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_MOUNT_PATH, pspec); /** * FuVolume:proxy-partition: * * The proxy for the filesystem interface. * * Since: 1.9.3 */ pspec = g_param_spec_object("proxy-partition", NULL, NULL, G_TYPE_DBUS_PROXY, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_PROXY_PARTITION, pspec); } static void fu_volume_init(FuVolume *self) { } /** * fu_volume_get_id: * @self: a @FuVolume * * Gets the D-Bus path of the mount point. * * Returns: string ID, or %NULL * * Since: 1.4.6 **/ const gchar * fu_volume_get_id(FuVolume *self) { g_return_val_if_fail(FU_IS_VOLUME(self), NULL); if (self->proxy_fs != NULL) return g_dbus_proxy_get_object_path(self->proxy_fs); if (self->proxy_blk != NULL) return g_dbus_proxy_get_object_path(self->proxy_blk); if (self->proxy_part != NULL) return g_dbus_proxy_get_object_path(self->proxy_part); return NULL; } /** * fu_volume_get_size: * @self: a @FuVolume * * Gets the size of the block device pointed to by the volume. * * Returns: size in bytes, or 0 on error * * Since: 1.9.3 **/ guint64 fu_volume_get_size(FuVolume *self) { g_autoptr(GVariant) val = NULL; g_return_val_if_fail(FU_IS_VOLUME(self), 0); if (self->proxy_blk == NULL) return 0; val = g_dbus_proxy_get_cached_property(self->proxy_blk, "Size"); if (val == NULL) return 0; return g_variant_get_uint64(val); } /** * fu_volume_get_partition_size: * @self: a @FuVolume * * Gets the size of the partition. * * Returns: size in bytes, or 0 on error * * Since: 1.9.3 **/ guint64 fu_volume_get_partition_size(FuVolume *self) { g_autoptr(GVariant) val = NULL; g_return_val_if_fail(FU_IS_VOLUME(self), 0); if (self->proxy_part == NULL) return 0; val = g_dbus_proxy_get_cached_property(self->proxy_part, "Size"); if (val == NULL) return 0; return g_variant_get_uint64(val); } /** * fu_volume_get_partition_offset: * @self: a @FuVolume * * Gets the offset of the partition. * * Returns: offset in bytes, or 0 on error * * Since: 1.9.3 **/ guint64 fu_volume_get_partition_offset(FuVolume *self) { g_autoptr(GVariant) val = NULL; g_return_val_if_fail(FU_IS_VOLUME(self), 0); if (self->proxy_part == NULL) return 0; val = g_dbus_proxy_get_cached_property(self->proxy_part, "Offset"); if (val == NULL) return 0; return g_variant_get_uint64(val); } /** * fu_volume_get_partition_number: * @self: a @FuVolume * * Gets the number of the partition. * * Returns: size in bytes, or 0 on error * * Since: 1.9.3 **/ guint32 fu_volume_get_partition_number(FuVolume *self) { g_autoptr(GVariant) val = NULL; g_return_val_if_fail(FU_IS_VOLUME(self), 0); if (self->proxy_part == NULL) return 0; val = g_dbus_proxy_get_cached_property(self->proxy_part, "Number"); if (val == NULL) return 0; return g_variant_get_uint32(val); } /** * fu_volume_get_partition_uuid: * @self: a @FuVolume * * Gets the UUID of the partition. * * Returns: size in bytes, or 0 on error * * Since: 1.9.3 **/ gchar * fu_volume_get_partition_uuid(FuVolume *self) { g_autoptr(GVariant) val = NULL; g_return_val_if_fail(FU_IS_VOLUME(self), NULL); if (self->partition_uuid != NULL) return g_strdup(self->partition_uuid); if (self->proxy_part == NULL) return NULL; val = g_dbus_proxy_get_cached_property(self->proxy_part, "UUID"); if (val == NULL) return NULL; return g_variant_dup_string(val, NULL); } /** * fu_volume_get_partition_kind: * @self: a @FuVolume * * Gets the partition kind of the volume mount point. * * NOTE: If you want this to be converted to a GPT-style GUID then use * fu_volume_kind_convert_to_gpt() on the return value of this function. * * Returns: (transfer full): partition kind, e.g. `0x06`, `vfat` or a GUID like `FU_VOLUME_KIND_ESP` * * Since: 1.8.13 **/ gchar * fu_volume_get_partition_kind(FuVolume *self) { g_autoptr(GVariant) val = NULL; g_return_val_if_fail(FU_IS_VOLUME(self), NULL); if (self->partition_kind != NULL) return g_strdup(self->partition_kind); if (self->proxy_part == NULL) return NULL; val = g_dbus_proxy_get_cached_property(self->proxy_part, "Type"); if (val == NULL) return NULL; return g_variant_dup_string(val, NULL); } /** * fu_volume_set_partition_kind: * @self: a @FuVolume * @partition_kind: a partition kind, e.g. %FU_VOLUME_KIND_ESP * * Sets the partition name of the volume mount point. * * Since: 2.0.0 **/ void fu_volume_set_partition_kind(FuVolume *self, const gchar *partition_kind) { g_return_if_fail(FU_IS_VOLUME(self)); g_return_if_fail(partition_kind != NULL); g_return_if_fail(self->partition_kind == NULL); self->partition_kind = g_strdup(partition_kind); } /** * fu_volume_set_partition_uuid: * @self: a @FuVolume * @partition_uuid: a UUID * * Sets the partition UUID of the volume mount point. * * Since: 2.0.0 **/ void fu_volume_set_partition_uuid(FuVolume *self, const gchar *partition_uuid) { g_return_if_fail(FU_IS_VOLUME(self)); g_return_if_fail(partition_uuid != NULL); g_return_if_fail(self->partition_uuid == NULL); self->partition_uuid = g_strdup(partition_uuid); } /** * fu_volume_get_partition_name: * @self: a @FuVolume * * Gets the partition name of the volume mount point. * * Returns: (transfer full): partition name, e.g 'Recovery Partition' * * Since: 1.9.10 **/ gchar * fu_volume_get_partition_name(FuVolume *self) { g_autofree gchar *name = NULL; g_autoptr(GVariant) val = NULL; gsize namesz = 0; g_return_val_if_fail(FU_IS_VOLUME(self), NULL); if (self->proxy_part == NULL) return NULL; val = g_dbus_proxy_get_cached_property(self->proxy_part, "Name"); if (val == NULL) return NULL; /* only return if non-zero length */ name = g_variant_dup_string(val, &namesz); if (namesz == 0) return NULL; return g_steal_pointer(&name); } /** * fu_volume_is_mdraid: * @self: a @FuVolume * * Determines if a volume is part of an MDRAID array. * * Returns: %TRUE if the volume is part of an MDRAID array * * Since: 1.9.17 **/ gboolean fu_volume_is_mdraid(FuVolume *self) { g_autoptr(GVariant) val = NULL; g_return_val_if_fail(FU_IS_VOLUME(self), FALSE); if (self->proxy_blk == NULL) return FALSE; val = g_dbus_proxy_get_cached_property(self->proxy_blk, "MDRaid"); if (val == NULL) return FALSE; return g_strcmp0(g_variant_get_string(val, NULL), "/") != 0; } static guint32 fu_volume_get_block_size_from_device_name(const gchar *device_name, GError **error) { #if defined(HAVE_IOCTL_H) && defined(HAVE_BLKSSZGET) gint fd; gint rc; gint sector_size = 0; fd = g_open(device_name, O_RDONLY, 0); if (fd < 0) { g_set_error_literal(error, G_IO_ERROR, /* nocheck:error */ g_io_error_from_errno(errno), g_strerror(errno)); fwupd_error_convert(error); return 0; } rc = ioctl(fd, BLKSSZGET, §or_size); /* nocheck:blocked */ if (rc < 0) { g_set_error_literal(error, G_IO_ERROR, /* nocheck:error */ g_io_error_from_errno(errno), g_strerror(errno)); fwupd_error_convert(error); } else if (sector_size == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to get non-zero logical sector size"); } g_close(fd, NULL); return sector_size; #else g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Not supported as or BLKSSZGET not found"); return 0; #endif } /** * fu_volume_get_block_label: * @self: a @FuVolume * * Gets the block name of the volume * * Returns: (transfer full): block device name, e.g 'Recovery Partition' * * Since: 1.9.24 **/ gchar * fu_volume_get_block_name(FuVolume *self) { gsize namesz = 0; g_autofree gchar *name = NULL; g_autoptr(GVariant) val = NULL; g_return_val_if_fail(FU_IS_VOLUME(self), NULL); if (self->proxy_blk == NULL) return NULL; val = g_dbus_proxy_get_cached_property(self->proxy_blk, "IdLabel"); if (val == NULL) return NULL; /* only return if non-zero length */ name = g_variant_dup_string(val, &namesz); if (namesz == 0) return NULL; return g_steal_pointer(&name); } /** * fu_volume_get_block_size: * @self: a @FuVolume * * Gets the logical block size of the volume mount point. * * Returns: block size in bytes or 0 on error * * Since: 1.9.4 **/ gsize fu_volume_get_block_size(FuVolume *self, GError **error) { g_autoptr(GVariant) val = NULL; g_return_val_if_fail(FU_IS_VOLUME(self), 0); if (self->proxy_blk == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no udisks proxy"); return 0; } val = g_dbus_proxy_get_cached_property(self->proxy_blk, "Device"); if (val == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no device property"); return 0; } return fu_volume_get_block_size_from_device_name(g_variant_get_bytestring(val), error); } /** * fu_volume_get_mount_point: * @self: a @FuVolume * * Gets the location of the volume mount point. * * Returns: UNIX path, or %NULL * * Since: 1.4.6 **/ gchar * fu_volume_get_mount_point(FuVolume *self) { g_autofree const gchar **mountpoints = NULL; g_autoptr(GVariant) val = NULL; g_return_val_if_fail(FU_IS_VOLUME(self), NULL); /* we mounted it */ if (self->mount_path != NULL) return g_strdup(self->mount_path); /* something else mounted it */ if (self->proxy_fs == NULL) return NULL; val = g_dbus_proxy_get_cached_property(self->proxy_fs, "MountPoints"); if (val == NULL) return NULL; mountpoints = g_variant_get_bytestring_array(val, NULL); return g_strdup(mountpoints[0]); } /* private: for self tests only */ void fu_volume_set_filesystem_free(FuVolume *self, guint64 fs_free) { g_return_if_fail(FU_IS_VOLUME(self)); self->fs_free = fs_free; } /** * fu_volume_check_free_space: * @self: a @FuVolume * @required: size in bytes * @error: (nullable): optional return location for an error * * Checks the volume for required space. * * Returns: %TRUE for success * * Since: 1.4.6 **/ gboolean fu_volume_check_free_space(FuVolume *self, guint64 required, GError **error) { guint64 fs_free; g_autofree gchar *path = NULL; g_return_val_if_fail(FU_IS_VOLUME(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* skip the checks for unmounted disks */ path = fu_volume_get_mount_point(self); if (path == NULL) return TRUE; if (self->fs_free > 0) { fs_free = self->fs_free; } else { g_autoptr(GFile) file = g_file_new_for_path(path); g_autoptr(GFileInfo) info = g_file_query_filesystem_info(file, G_FILE_ATTRIBUTE_FILESYSTEM_FREE, NULL, error); if (info == NULL) return FALSE; fs_free = g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_FILESYSTEM_FREE); } if (fs_free < required) { g_autofree gchar *str_free = g_format_size(required - fs_free); g_autofree gchar *str_reqd = g_format_size(required); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "%s does not have sufficient space, required %s, need additional %s", path, str_reqd, str_free); return FALSE; } return TRUE; } /** * fu_volume_is_mounted: * @self: a @FuVolume * * Checks if the VOLUME is already mounted. * * Returns: %TRUE for success * * Since: 1.4.6 **/ gboolean fu_volume_is_mounted(FuVolume *self) { g_autofree gchar *mount_point = NULL; g_return_val_if_fail(FU_IS_VOLUME(self), FALSE); mount_point = fu_volume_get_mount_point(self); return mount_point != NULL; } /** * fu_volume_is_encrypted: * @self: a @FuVolume * * Checks if the VOLUME is currently encrypted. * * Returns: %TRUE for success * * Since: 1.5.1 **/ gboolean fu_volume_is_encrypted(FuVolume *self) { g_autoptr(GVariant) val = NULL; g_return_val_if_fail(FU_IS_VOLUME(self), FALSE); if (self->proxy_blk == NULL) return FALSE; val = g_dbus_proxy_get_cached_property(self->proxy_blk, "CryptoBackingDevice"); if (val == NULL) return FALSE; if (g_strcmp0(g_variant_get_string(val, NULL), "/") == 0) return FALSE; return TRUE; } /** * fu_volume_mount: * @self: a @FuVolume * @error: (nullable): optional return location for an error * * Mounts the VOLUME ready for use. * * Returns: %TRUE for success * * Since: 1.4.6 **/ gboolean fu_volume_mount(FuVolume *self, GError **error) { GVariantBuilder builder; g_autoptr(GError) error_local = NULL; g_autoptr(GVariant) val = NULL; g_return_val_if_fail(FU_IS_VOLUME(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* device from the self tests */ if (self->proxy_fs == NULL) return TRUE; g_debug("mounting %s", fu_volume_get_id(self)); g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); val = g_dbus_proxy_call_sync(self->proxy_fs, "Mount", g_variant_new("(a{sv})", &builder), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error_local); if (val == NULL) { if (g_error_matches(error_local, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_INTERFACE) || g_error_matches(error_local, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, error_local->message); return FALSE; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } g_variant_get(val, "(s)", &self->mount_path); return TRUE; } /** * fu_volume_is_internal: * @self: a @FuVolume * * Guesses if the drive is internal to the system * * Returns: %TRUE for success * * Since: 1.5.2 **/ gboolean fu_volume_is_internal(FuVolume *self) { g_autoptr(GVariant) val_system = NULL; g_return_val_if_fail(FU_IS_VOLUME(self), FALSE); val_system = g_dbus_proxy_get_cached_property(self->proxy_blk, "HintSystem"); if (val_system == NULL) return FALSE; return g_variant_get_boolean(val_system); } /** * fu_volume_get_id_type: * @self: a @FuVolume * * Return the IdType of the volume * * Returns: string for type or NULL * * Since: 1.5.2 **/ gchar * fu_volume_get_id_type(FuVolume *self) { g_autoptr(GVariant) val = NULL; g_return_val_if_fail(FU_IS_VOLUME(self), NULL); val = g_dbus_proxy_get_cached_property(self->proxy_blk, "IdType"); if (val == NULL) return NULL; return g_strdup(g_variant_get_string(val, NULL)); } /** * fu_volume_unmount: * @self: a @FuVolume * @error: (nullable): optional return location for an error * * Unmounts the volume after use. * * Returns: %TRUE for success * * Since: 1.4.6 **/ gboolean fu_volume_unmount(FuVolume *self, GError **error) { GVariantBuilder builder; g_autoptr(GVariant) val = NULL; g_return_val_if_fail(FU_IS_VOLUME(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* device from the self tests */ if (self->proxy_fs == NULL) return TRUE; g_debug("unmounting %s", fu_volume_get_id(self)); g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); val = g_dbus_proxy_call_sync(self->proxy_fs, "Unmount", g_variant_new("(a{sv})", &builder), G_DBUS_CALL_FLAGS_NONE, -1, NULL, error); if (val == NULL) return FALSE; g_free(self->mount_path); self->mount_path = NULL; return TRUE; } /** * fu_volume_locker: * @self: a @FuVolume * @error: (nullable): optional return location for an error * * Locks the volume, mounting it and unmounting it as required. If the volume is * already mounted then it is is _not_ unmounted when the locker is closed. * * Returns: (transfer full): a device locker for success, or %NULL * * Since: 1.4.6 **/ FuDeviceLocker * fu_volume_locker(FuVolume *self, GError **error) { g_return_val_if_fail(FU_IS_VOLUME(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* already open, so NOP */ if (fu_volume_is_mounted(self)) return g_object_new(FU_TYPE_DEVICE_LOCKER, NULL); return fu_device_locker_new_full(self, (FuDeviceLockerFunc)fu_volume_mount, (FuDeviceLockerFunc)fu_volume_unmount, error); } /* private */ FuVolume * fu_volume_new_from_mount_path(const gchar *mount_path) { g_autoptr(FuVolume) self = g_object_new(FU_TYPE_VOLUME, NULL); g_return_val_if_fail(mount_path != NULL, NULL); self->mount_path = g_strdup(mount_path); return g_steal_pointer(&self); } /** * fu_volume_kind_convert_to_gpt: * @kind: UDisk reported type string, e.g. `efi` or `0xef` * * Converts a MBR type to a GPT type. * * Returns: the GPT type, usually a GUID. If not known @kind is returned. * * Since: 1.8.6 **/ const gchar * fu_volume_kind_convert_to_gpt(const gchar *kind) { struct { const gchar *gpt; const gchar *mbrs[6]; } typeguids[] = {{FU_VOLUME_KIND_ESP, { "0xef", "efi", NULL, }}, {FU_VOLUME_KIND_BDP, { "0x0b", "0x06", "vfat", "fat32", "fat32lba", NULL, }}, {NULL, {NULL}}}; for (guint i = 0; typeguids[i].gpt != NULL; i++) { for (guint j = 0; typeguids[i].mbrs[j] != NULL; j++) { if (g_strcmp0(kind, typeguids[i].mbrs[j]) == 0) return typeguids[i].gpt; } } return kind; } static gboolean fu_volume_check_block_device_symlinks(const gchar *const *symlinks, GError **error) { for (guint i = 0; symlinks[i] != NULL; i++) { if (g_str_has_prefix(symlinks[i], "/dev/zvol")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "detected zfs zvol"); return FALSE; } } /* success */ return TRUE; } static gboolean fu_volume_check_is_recovery(const gchar *name) { g_autoptr(GString) name_safe = g_string_new(name); const gchar *recovery_partitions[] = { "DELLRESTORE", "DELLUTILITY", "DIAGS", "HP_RECOVERY", "IBM_SERVICE", "INTELRST", "LENOVO_RECOVERY", "OS", "PQSERVICE", "RECOVERY", "RECOVERY_PARTITION", "SERVICEV001", "SERVICEV002", "SYSTEM_RESERVED", "WINRE_DRV", NULL, }; /* from https://github.com/storaged-project/udisks/blob/master/data/80-udisks2.rules */ g_string_replace(name_safe, " ", "_", 0); g_string_replace(name_safe, "\"", "", 0); g_string_ascii_up(name_safe); return g_strv_contains(recovery_partitions, name_safe->str); } static void fu_volume_codec_iface_init(FwupdCodecInterface *iface) { iface->add_json = fu_volume_add_json; } /** * fu_volume_new_by_kind: * @kind: a volume kind, typically a GUID * @error: (nullable): optional return location for an error * * Finds all volumes of a specific partition type. * For ESP type partitions exclude any known partitions names that * correspond to recovery partitions. * * Returns: (transfer container) (element-type FuVolume): a #GPtrArray, or %NULL if the kind was not *found * * Since: 1.8.2 **/ GPtrArray * fu_volume_new_by_kind(const gchar *kind, GError **error) { g_autoptr(GPtrArray) devices = NULL; g_autoptr(GPtrArray) volumes = NULL; g_return_val_if_fail(kind != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); devices = fu_common_get_block_devices(error); if (devices == NULL) return NULL; g_info("Looking for volumes of type %s", kind); volumes = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); for (guint i = 0; i < devices->len; i++) { GDBusProxy *proxy_blk = g_ptr_array_index(devices, i); const gchar *type_str; g_autofree gchar *id_type = NULL; g_autofree gchar *part_type = NULL; g_autoptr(FuVolume) vol = NULL; g_autoptr(GDBusProxy) proxy_part = NULL; g_autoptr(GDBusProxy) proxy_fs = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(GVariant) symlinks = NULL; /* ignore anything in a zfs zvol */ symlinks = g_dbus_proxy_get_cached_property(proxy_blk, "Symlinks"); if (symlinks != NULL) { g_autofree const gchar **symlinks_strv = g_variant_get_bytestring_array(symlinks, NULL); if (!fu_volume_check_block_device_symlinks(symlinks_strv, &error_local)) { g_debug("ignoring due to symlink: %s", error_local->message); continue; } } proxy_part = g_dbus_proxy_new_sync(g_dbus_proxy_get_connection(proxy_blk), G_DBUS_PROXY_FLAGS_NONE, NULL, UDISKS_DBUS_SERVICE, g_dbus_proxy_get_object_path(proxy_blk), UDISKS_DBUS_INTERFACE_PARTITION, NULL, error); if (proxy_part == NULL) { g_prefix_error(error, "failed to initialize d-bus proxy %s: ", g_dbus_proxy_get_object_path(proxy_blk)); return NULL; } proxy_fs = g_dbus_proxy_new_sync(g_dbus_proxy_get_connection(proxy_blk), G_DBUS_PROXY_FLAGS_NONE, NULL, UDISKS_DBUS_SERVICE, g_dbus_proxy_get_object_path(proxy_blk), UDISKS_DBUS_INTERFACE_FILESYSTEM, NULL, &error_local); if (proxy_fs == NULL) { g_debug("failed to get filesystem for %s: %s", g_dbus_proxy_get_object_path(proxy_blk), error_local->message); continue; } vol = g_object_new(FU_TYPE_VOLUME, "proxy-block", proxy_blk, "proxy-filesystem", proxy_fs, "proxy-partition", proxy_part, NULL); if (fu_volume_is_mdraid(vol)) part_type = g_strdup(kind); if (part_type == NULL) part_type = fu_volume_get_partition_kind(vol); /* convert reported type to GPT type */ if (part_type == NULL) continue; type_str = fu_volume_kind_convert_to_gpt(part_type); id_type = fu_volume_get_id_type(vol); g_info("device %s, type: %s, internal: %d, fs: %s", g_dbus_proxy_get_object_path(proxy_blk), fu_volume_is_mdraid(vol) ? "mdraid" : type_str, fu_volume_is_internal(vol), id_type); if (g_strcmp0(type_str, kind) != 0) continue; if (g_strcmp0(id_type, "linux_raid_member") == 0) { g_debug("ignoring linux_raid_member device %s", g_dbus_proxy_get_object_path(proxy_blk)); continue; } /* ignore a partition that claims to be a recovery partition */ if (g_strcmp0(kind, FU_VOLUME_KIND_BDP) == 0 || g_strcmp0(kind, FU_VOLUME_KIND_ESP) == 0) { g_autofree gchar *name = fu_volume_get_partition_name(vol); if (name == NULL) name = fu_volume_get_block_name(vol); if (name != NULL) { if (fu_volume_check_is_recovery(name)) { g_debug("skipping partition '%s'", name); continue; } g_debug("adding partition '%s'", name); } } g_ptr_array_add(volumes, g_steal_pointer(&vol)); } if (volumes->len == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no volumes of type %s", kind); return NULL; } return g_steal_pointer(&volumes); } /** * fu_volume_new_by_device: * @device: a device string, typically starting with `/dev/` * @error: (nullable): optional return location for an error * * Finds the first volume from the specified device. * * Returns: (transfer full): a volume, or %NULL if the device was not found * * Since: 1.8.2 **/ FuVolume * fu_volume_new_by_device(const gchar *device, GError **error) { g_autoptr(GPtrArray) devices = NULL; g_return_val_if_fail(device != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* find matching block device */ devices = fu_common_get_block_devices(error); if (devices == NULL) return NULL; for (guint i = 0; i < devices->len; i++) { GDBusProxy *proxy_blk = g_ptr_array_index(devices, i); g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_get_cached_property(proxy_blk, "Device"); if (val == NULL) continue; if (g_strcmp0(g_variant_get_bytestring(val), device) == 0) { g_autoptr(GDBusProxy) proxy_fs = NULL; g_autoptr(GDBusProxy) proxy_part = NULL; g_autoptr(GError) error_local = NULL; proxy_fs = g_dbus_proxy_new_sync(g_dbus_proxy_get_connection(proxy_blk), G_DBUS_PROXY_FLAGS_NONE, NULL, UDISKS_DBUS_SERVICE, g_dbus_proxy_get_object_path(proxy_blk), UDISKS_DBUS_INTERFACE_FILESYSTEM, NULL, &error_local); if (proxy_fs == NULL) g_debug("ignoring: %s", error_local->message); proxy_part = g_dbus_proxy_new_sync(g_dbus_proxy_get_connection(proxy_blk), G_DBUS_PROXY_FLAGS_NONE, NULL, UDISKS_DBUS_SERVICE, g_dbus_proxy_get_object_path(proxy_blk), UDISKS_DBUS_INTERFACE_PARTITION, NULL, &error_local); if (proxy_part == NULL) g_debug("ignoring: %s", error_local->message); return g_object_new(FU_TYPE_VOLUME, "proxy-block", proxy_blk, "proxy-filesystem", proxy_fs, "proxy-partition", proxy_part, NULL); } } /* failed */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no volumes for device %s", device); return NULL; } /** * fu_volume_new_by_devnum: * @devnum: a device number * @error: (nullable): optional return location for an error * * Finds the first volume from the specified device. * * Returns: (transfer full): a volume, or %NULL if the device was not found * * Since: 1.8.2 **/ FuVolume * fu_volume_new_by_devnum(guint32 devnum, GError **error) { g_autoptr(GPtrArray) devices = NULL; g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* find matching block device */ devices = fu_common_get_block_devices(error); if (devices == NULL) return NULL; for (guint i = 0; i < devices->len; i++) { GDBusProxy *proxy_blk = g_ptr_array_index(devices, i); g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_get_cached_property(proxy_blk, "DeviceNumber"); if (val == NULL) continue; if (devnum == g_variant_get_uint64(val)) { return g_object_new(FU_TYPE_VOLUME, "proxy-block", proxy_blk, NULL); } } /* failed */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no volumes for devnum %u", devnum); return NULL; } fwupd-2.0.10/libfwupdplugin/fu-volume.h000066400000000000000000000053261501337203100200430ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2019 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-device-locker.h" #define FU_TYPE_VOLUME (fu_volume_get_type()) G_DECLARE_FINAL_TYPE(FuVolume, fu_volume, FU, VOLUME, GObject) /** * FU_VOLUME_KIND_ESP: * * The GUID for the ESP, see: https://en.wikipedia.org/wiki/EFI_system_partition * * Since: 1.5.0 **/ #define FU_VOLUME_KIND_ESP "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" /** * FU_VOLUME_KIND_BDP: * * The GUID for the BDP, see: https://en.wikipedia.org/wiki/Microsoft_basic_data_partition * * Since: 1.5.3 **/ #define FU_VOLUME_KIND_BDP "ebd0a0a2-b9e5-4433-87c0-68b6b72699c7" const gchar * fu_volume_get_id(FuVolume *self) G_GNUC_NON_NULL(1); gboolean fu_volume_check_free_space(FuVolume *self, guint64 required, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_volume_is_mounted(FuVolume *self) G_GNUC_NON_NULL(1); gboolean fu_volume_is_encrypted(FuVolume *self) G_GNUC_NON_NULL(1); guint64 fu_volume_get_size(FuVolume *self) G_GNUC_NON_NULL(1); gchar * fu_volume_get_block_name(FuVolume *self) G_GNUC_NON_NULL(1); gsize fu_volume_get_block_size(FuVolume *self, GError **error) G_GNUC_NON_NULL(1); gchar * fu_volume_get_partition_kind(FuVolume *self) G_GNUC_NON_NULL(1); gchar * fu_volume_get_partition_name(FuVolume *self) G_GNUC_NON_NULL(1); guint64 fu_volume_get_partition_size(FuVolume *self) G_GNUC_NON_NULL(1); guint64 fu_volume_get_partition_offset(FuVolume *self) G_GNUC_NON_NULL(1); guint32 fu_volume_get_partition_number(FuVolume *self) G_GNUC_NON_NULL(1); gchar * fu_volume_get_partition_uuid(FuVolume *self) G_GNUC_NON_NULL(1); gchar * fu_volume_get_mount_point(FuVolume *self) G_GNUC_NON_NULL(1); gboolean fu_volume_mount(FuVolume *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_volume_unmount(FuVolume *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); FuDeviceLocker * fu_volume_locker(FuVolume *self, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); gboolean fu_volume_is_internal(FuVolume *self) G_GNUC_NON_NULL(1); gchar * fu_volume_get_id_type(FuVolume *self) G_GNUC_NON_NULL(1); GPtrArray * fu_volume_new_by_kind(const gchar *kind, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); FuVolume * fu_volume_new_by_device(const gchar *device, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); FuVolume * fu_volume_new_by_devnum(guint32 devnum, GError **error) G_GNUC_WARN_UNUSED_RESULT; const gchar * fu_volume_kind_convert_to_gpt(const gchar *kind) G_GNUC_NON_NULL(1); gboolean fu_volume_is_mdraid(FuVolume *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fu-windows-efivars.c000066400000000000000000000227771501337203100216670ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuWindowsEfivars" #include "config.h" #include #include "fwupd-error.h" #include "fu-byte-array.h" #include "fu-windows-efivars.h" struct _FuWindowsEfivars { FuEfivars parent_instance; }; G_DEFINE_TYPE(FuWindowsEfivars, fu_windows_efivars, FU_TYPE_EFIVARS) static gboolean fu_windows_efivars_supported(FuEfivars *efivars, GError **error) { FIRMWARE_TYPE firmware_type = {0}; DWORD rc; /* sanity check */ if (!GetFirmwareType(&firmware_type)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot get firmware type [%u]", (guint)GetLastError()); return FALSE; } if (firmware_type != FirmwareTypeUefi) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "only supported on UEFI firmware"); return FALSE; } /* check supported */ rc = GetFirmwareEnvironmentVariableA("", "{00000000-0000-0000-0000-000000000000}", NULL, 0); if (rc == 0 && GetLastError() == ERROR_INVALID_FUNCTION) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "getting EFI variables is not supported on this system"); return FALSE; } /* success */ return TRUE; } static gboolean fu_windows_efivars_is_running_under_wine(void) { HKEY hKey = NULL; LONG lResult = RegOpenKeyEx(HKEY_LOCAL_MACHINE, "Software\\Wine", 0, KEY_READ, &hKey); if (lResult == ERROR_SUCCESS) { RegCloseKey(hKey); return TRUE; } return FALSE; } static gboolean fu_windows_efivars_get_data(FuEfivars *efivars, const gchar *guid, const gchar *name, guint8 **data, gsize *data_sz, guint32 *attr, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_autofree gchar *guid_win32 = g_strdup_printf("{%s}", guid); fu_byte_array_set_size(buf, 0x1000, 0xFF); /* unimplemented function KERNEL32.dll.GetFirmwareEnvironmentVariableExA on wine */ if (fu_windows_efivars_is_running_under_wine()) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "GetFirmwareEnvironmentVariableExA is not implemented"); return FALSE; } do { DWORD dwAttribubutes = 0; DWORD rc = GetFirmwareEnvironmentVariableExA(name, guid_win32, buf->data, buf->len, &dwAttribubutes); if (rc > 0) { if (data != NULL) *data = g_byte_array_free(g_steal_pointer(&buf), FALSE); if (data_sz != NULL) *data_sz = rc; if (attr != NULL) *attr = dwAttribubutes; return TRUE; } if (rc == 0 && GetLastError() != ERROR_INSUFFICIENT_BUFFER) break; fu_byte_array_set_size(buf, buf->len * 2, 0xFF); } while (buf->len < 0x400000); /* failed */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to get get variable [%u]", (guint)GetLastError()); return FALSE; } static gboolean fu_windows_efivars_exists(FuEfivars *efivars, const gchar *guid, const gchar *name) { return fu_windows_efivars_get_data(efivars, guid, name, NULL, NULL, NULL, NULL); } /* there is no win32 kernel interface for GetNextVariable so use from UEFI spec v2.8 */ static GPtrArray * fu_windows_efivars_get_names(FuEfivars *efivars, const gchar *guid, GError **error) { g_autoptr(GPtrArray) names = g_ptr_array_new_with_free_func(g_free); struct { const gchar *guid; const gchar *name; } variable_names[] = {{FU_EFIVARS_GUID_EFI_GLOBAL, "AuditMode"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "BootCurrent"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "BootNext"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "BootOptionSupport"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "BootOrder"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "BootOrderDefault"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "BootXXXX"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "ConIn"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "ConInDev"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "ConOut"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "ConOutDev"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "CurrentPolicy"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "dbDefault"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "dbrDefault"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "dbtDefault"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "dbxDefault"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "DeployedMode"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "DriverOrder"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "DriverXXXX"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "ErrOut"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "ErrOutDev"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "HwErrRecSupprot"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "KEK"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "KEKDefault"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "KeyXXXX"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "Lang"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "LangCodes"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "OsIndications"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "OsIndicationsSupported"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "OsRecoveryOrder"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "PK"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "PKDefault"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "PlatformLang"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "PlatformLangCodes"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "PlatformRecoveryXXXX"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "RuntimeServicesSupported"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "SecureBoot"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "SetupMode"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "SignatureSupport"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "SysPrepOrder"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "SysPrepXXXX"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "Timeout"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "VendorKeys"}, {FU_EFIVARS_GUID_FWUPDATE, "FWUPDATE_DEBUG_LOG"}, {FU_EFIVARS_GUID_FWUPDATE, "FWUPDATE_VERBOSE"}, {FU_EFIVARS_GUID_FWUPDATE, "fwupd-ux-capsule"}, {FU_EFIVARS_GUID_SECURITY_DATABASE, "db"}, {FU_EFIVARS_GUID_SECURITY_DATABASE, "dbx"}, {NULL, NULL}}; /* look for each possible guid+name */ for (guint i = 0; variable_names[i].guid != NULL; i++) { if (g_strcmp0(FU_EFIVARS_GUID_EFI_GLOBAL, variable_names[i].guid) != 0) continue; if (g_str_has_suffix(variable_names[i].name, "XXXX")) { g_autoptr(GString) name_root = g_string_new(variable_names[i].name); g_string_truncate(name_root, name_root->len - 4); for (guint j = 0; j < G_MAXUINT16; j++) { g_autofree gchar *name = g_strdup_printf("%s%04X", name_root->str, j); if (fu_windows_efivars_exists(efivars, variable_names[i].guid, name)) g_ptr_array_add(names, g_steal_pointer(&name)); } } else { if (fu_windows_efivars_exists(efivars, variable_names[i].guid, variable_names[i].name)) g_ptr_array_add(names, g_strdup(variable_names[i].name)); } } /* nothing found */ if (names->len == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no names for GUID %s", guid); return NULL; } /* success */ return g_steal_pointer(&names); } static gboolean fu_windows_efivars_set_data(FuEfivars *efivars, const gchar *guid, const gchar *name, const guint8 *data, gsize sz, guint32 attr, GError **error) { g_autofree gchar *guid_win32 = g_strdup_printf("{%s}", guid); /* unimplemented function KERNEL32.dll.SetFirmwareEnvironmentVariableExA on wine */ if (fu_windows_efivars_is_running_under_wine()) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "SetFirmwareEnvironmentVariableExA is not implemented"); return FALSE; } if (!SetFirmwareEnvironmentVariableExA(name, guid_win32, (PVOID)data, sz, attr)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to get set variable [%u]", (guint)GetLastError()); return FALSE; } return TRUE; } static gboolean fu_windows_efivars_delete(FuEfivars *efivars, const gchar *guid, const gchar *name, GError **error) { /* size of 0 bytes -> delete */ return fu_windows_efivars_set_data(efivars, guid, name, NULL, 0, 0, error); } static gboolean fu_windows_efivars_delete_with_glob(FuEfivars *efivars, const gchar *guid, const gchar *name_glob, GError **error) { g_autoptr(GPtrArray) names = NULL; g_autoptr(GError) error_local = NULL; names = fu_windows_efivars_get_names(efivars, guid, &error_local); if (names == NULL) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) return TRUE; g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } for (guint i = 0; i < names->len; i++) { const gchar *name = g_ptr_array_index(names, i); if (g_pattern_match_simple(name_glob, name)) { if (!fu_windows_efivars_delete(efivars, guid, name, error)) return FALSE; } } return TRUE; } static void fu_windows_efivars_init(FuWindowsEfivars *self) { } static void fu_windows_efivars_class_init(FuWindowsEfivarsClass *klass) { FuEfivarsClass *efivars_class = FU_EFIVARS_CLASS(klass); efivars_class->supported = fu_windows_efivars_supported; efivars_class->exists = fu_windows_efivars_exists; efivars_class->get_data = fu_windows_efivars_get_data; efivars_class->set_data = fu_windows_efivars_set_data; efivars_class->delete = fu_windows_efivars_delete; efivars_class->delete_with_glob = fu_windows_efivars_delete_with_glob; efivars_class->get_names = fu_windows_efivars_get_names; } FuEfivars * fu_efivars_new(void) { return FU_EFIVARS(g_object_new(FU_TYPE_FREEBSD_EFIVARS, NULL)); } fwupd-2.0.10/libfwupdplugin/fu-windows-efivars.h000066400000000000000000000004621501337203100216570ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-efivars.h" #define FU_TYPE_FREEBSD_EFIVARS (fu_windows_efivars_get_type()) G_DECLARE_FINAL_TYPE(FuWindowsEfivars, fu_windows_efivars, FU, FREEBSD_EFIVARS, FuEfivars) fwupd-2.0.10/libfwupdplugin/fu-x509-certificate.c000066400000000000000000000131001501337203100215010ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #ifdef HAVE_GNUTLS #include #include #endif #include "fu-common.h" #include "fu-input-stream.h" #include "fu-string.h" #include "fu-x509-certificate.h" #ifdef HAVE_GNUTLS static void fu_x509_certificate_gnutls_datum_deinit(gnutls_datum_t *d) { gnutls_free(d->data); gnutls_free(d); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTOPTR_CLEANUP_FUNC(gnutls_datum_t, fu_x509_certificate_gnutls_datum_deinit) G_DEFINE_AUTO_CLEANUP_FREE_FUNC(gnutls_x509_crt_t, gnutls_x509_crt_deinit, NULL) #pragma clang diagnostic pop #endif /** * FuX509Certificate: * * An X.509 certificate. * * See also: [class@FuFirmware] */ struct _FuX509Certificate { FuFirmware parent_instance; gchar *issuer; gchar *subject; }; G_DEFINE_TYPE(FuX509Certificate, fu_x509_certificate, FU_TYPE_FIRMWARE) static void fu_x509_certificate_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuX509Certificate *self = FU_X509_CERTIFICATE(firmware); fu_xmlb_builder_insert_kv(bn, "issuer", self->issuer); fu_xmlb_builder_insert_kv(bn, "subject", self->subject); } /** * fu_x509_certificate_get_issuer: * @self: A #FuX509Certificate * * Returns the certificate issuer. * * Returns: string, or %NULL for unset * * Since: 2.0.9 **/ const gchar * fu_x509_certificate_get_issuer(FuX509Certificate *self) { g_return_val_if_fail(FU_IS_X509_CERTIFICATE(self), NULL); return self->issuer; } static void fu_x509_certificate_set_issuer(FuX509Certificate *self, const gchar *issuer) { g_return_if_fail(FU_IS_X509_CERTIFICATE(self)); if (g_strcmp0(issuer, self->issuer) == 0) return; g_free(self->issuer); self->issuer = g_strdup(issuer); } static void fu_x509_certificate_set_subject(FuX509Certificate *self, const gchar *subject) { g_return_if_fail(FU_IS_X509_CERTIFICATE(self)); if (g_strcmp0(subject, self->subject) == 0) return; g_free(self->subject); self->subject = g_strdup(subject); } /** * fu_x509_certificate_get_subject: * @self: A #FuX509Certificate * * Returns the certificate subject. * * Returns: string, or %NULL for unset * * Since: 2.0.9 **/ const gchar * fu_x509_certificate_get_subject(FuX509Certificate *self) { g_return_val_if_fail(FU_IS_X509_CERTIFICATE(self), NULL); return self->subject; } static gboolean fu_x509_certificate_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { #ifdef HAVE_GNUTLS FuX509Certificate *self = FU_X509_CERTIFICATE(firmware); gchar buf[1024] = {'\0'}; guchar key_id[32] = {'\0'}; gsize key_idsz = sizeof(key_id); gnutls_datum_t d = {0}; gnutls_x509_dn_t dn = {0x0}; gsize bufsz = sizeof(buf); int rc; g_auto(gnutls_x509_crt_t) crt = NULL; g_autoptr(gnutls_datum_t) subject = NULL; g_autoptr(GBytes) blob = NULL; g_autoptr(GString) key_idstr = g_string_new(NULL); /* parse certificate */ blob = fu_input_stream_read_bytes(stream, 0x0, G_MAXSIZE, NULL, error); if (blob == NULL) return FALSE; d.size = g_bytes_get_size(blob); d.data = (unsigned char *)g_bytes_get_data(blob, NULL); rc = gnutls_x509_crt_init(&crt); if (rc < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "crt_init: %s [%i]", gnutls_strerror(rc), rc); return FALSE; } rc = gnutls_x509_crt_import(crt, &d, GNUTLS_X509_FMT_DER); if (rc < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "crt_import: %s [%i]", gnutls_strerror(rc), rc); return FALSE; } /* issuer */ if (gnutls_x509_crt_get_issuer_dn(crt, buf, &bufsz) == GNUTLS_E_SUCCESS) { g_autofree gchar *str = fu_strsafe((const gchar *)buf, bufsz); fu_x509_certificate_set_issuer(self, str); } /* subject */ subject = (gnutls_datum_t *)gnutls_malloc(sizeof(gnutls_datum_t)); if (gnutls_x509_crt_get_subject(crt, &dn) == GNUTLS_E_SUCCESS) { g_autofree gchar *str = NULL; gnutls_x509_dn_get_str(dn, subject); str = fu_strsafe((const gchar *)subject->data, subject->size); fu_x509_certificate_set_subject(self, str); } /* key ID */ rc = gnutls_x509_crt_get_key_id(crt, 0, key_id, &key_idsz); if (rc < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to get key ID: %s [%i]", gnutls_strerror(rc), rc); return FALSE; } for (guint i = 0; i < key_idsz; i++) g_string_append_printf(key_idstr, "%02x", key_id[i]); fu_firmware_set_id(firmware, key_idstr->str); /* success */ return TRUE; #else g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no GnuTLS support"); return FALSE; #endif } static void fu_x509_certificate_init(FuX509Certificate *self) { } static void fu_x509_certificate_finalize(GObject *obj) { FuX509Certificate *self = FU_X509_CERTIFICATE(obj); g_free(self->issuer); g_free(self->subject); G_OBJECT_CLASS(fu_x509_certificate_parent_class)->finalize(obj); } static void fu_x509_certificate_class_init(FuX509CertificateClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_x509_certificate_finalize; firmware_class->export = fu_x509_certificate_export; firmware_class->parse = fu_x509_certificate_parse; } /** * fu_x509_certificate_new: * * Creates a new #FuX509Certificate. * * Returns: (transfer full): object * * Since: 2.0.9 **/ FuX509Certificate * fu_x509_certificate_new(void) { return g_object_new(FU_TYPE_X509_CERTIFICATE, NULL); } fwupd-2.0.10/libfwupdplugin/fu-x509-certificate.h000066400000000000000000000010421501337203100215100ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-firmware.h" #define FU_TYPE_X509_CERTIFICATE (fu_x509_certificate_get_type()) G_DECLARE_FINAL_TYPE(FuX509Certificate, fu_x509_certificate, FU, X509_CERTIFICATE, FuFirmware) FuX509Certificate * fu_x509_certificate_new(void); const gchar * fu_x509_certificate_get_issuer(FuX509Certificate *self) G_GNUC_NON_NULL(1); const gchar * fu_x509_certificate_get_subject(FuX509Certificate *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/libfwupdplugin/fwupdplugin.h000066400000000000000000000121641501337203100204660ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #define __FWUPDPLUGIN_H_INSIDE__ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // #include #include #include #include #include #include #include #include #undef __FWUPDPLUGIN_H_INSIDE__ fwupd-2.0.10/libfwupdplugin/meson.build000066400000000000000000000302421501337203100201100ustar00rootroot00000000000000fwupdplugin_structs = [ 'fu-progress.rs', # fuzzing 'fu-common.rs', # fuzzing 'fu-acpi-table.rs', # fuzzing 'fu-archive.rs', # fuzzing 'fu-cab.rs', # fuzzing 'fu-cfi.rs', # fuzzing 'fu-cfu-firmware.rs', # fuzzing 'fu-coswid.rs', # fuzzing 'fu-device.rs', # fuzzing 'fu-dfu-firmware.rs', # fuzzing 'fu-dpaux.rs', # fuzzing 'fu-edid.rs', # fuzzing 'fu-efi.rs', # fuzzing 'fu-elf.rs', # fuzzing 'fu-fdt.rs', # fuzzing 'fu-fmap.rs', # fuzzing 'fu-hid.rs', # fuzzing 'fu-ifd.rs', # fuzzing 'fu-ifwi.rs', # fuzzing 'fu-ihex.rs', # fuzzing 'fu-intel-thunderbolt.rs', # fuzzing 'fu-io-channel.rs', # fuzzing 'fu-heci.rs', # fuzzing 'fu-msgpack.rs', # fuzzing 'fu-oprom.rs', # fuzzing 'fu-pefile.rs', # fuzzing 'fu-pci.rs', # fuzzing 'fu-sbatlevel-section.rs', # fuzzing 'fu-smbios.rs', # fuzzing 'fu-usb-device-ds20.rs', # fuzzing 'fu-usb.rs', # fuzzing 'fu-uswid.rs', # fuzzing 'fu-v4l.rs', # fuzzing ] # do not use structgen as these are referenced in headers too... fwupdplugin_rs_targets = [] foreach fwupdplugin_struct: fwupdplugin_structs fwupdplugin_rs_targets += custom_target('lib' + fwupdplugin_struct, input: fwupdplugin_struct, output: [ fwupdplugin_struct.replace('.rs', '-struct.c'), fwupdplugin_struct.replace('.rs', '-struct.h'), ], command: [ python3, join_paths(meson.project_source_root(), 'libfwupdplugin', 'rustgen.py'), '@INPUT@', '@OUTPUT0@', '@OUTPUT1@', ], ) endforeach fwupdplugin_src = [ 'fu-acpi-table.c', # fuzzing 'fu-archive.c', 'fu-archive-firmware.c', 'fu-backend.c', # fuzzing 'fu-bios-setting.c', # fuzzing 'fu-bios-settings.c', # fuzzing 'fu-block-device.c', 'fu-block-partition.c', 'fu-bluez-device.c', 'fu-byte-array.c', # fuzzing 'fu-bytes.c', # fuzzing 'fu-cab-firmware.c', # fuzzing 'fu-cab-image.c', # fuzzing 'fu-cfi-device.c', 'fu-cfu-offer.c', # fuzzing 'fu-cfu-payload.c', # fuzzing 'fu-chunk.c', # fuzzing 'fu-chunk-array.c', # fuzzing 'fu-common.c', # fuzzing 'fu-common-guid.c', 'fu-composite-input-stream.c', # fuzzing 'fu-config.c', # fuzzing 'fu-context.c', # fuzzing 'fu-coswid-common.c', # fuzzing 'fu-coswid-firmware.c', # fuzzing 'fu-crc.c', # fuzzing 'fu-csv-entry.c', # fuzzing 'fu-csv-firmware.c', # fuzzing 'fu-device.c', # fuzzing 'fu-device-event.c', # fuzzing 'fu-device-locker.c', # fuzzing 'fu-device-progress.c', 'fu-dfu-firmware.c', # fuzzing 'fu-dfuse-firmware.c', # fuzzing 'fu-dpaux-device.c', 'fu-drm-device.c', 'fu-dummy-efivars.c', # fuzzing 'fu-dump.c', # fuzzing 'fu-edid.c', # fuzzing 'fu-efi-common.c', # fuzzing 'fu-efi-device-path.c', # fuzzing 'fu-efi-device-path-list.c', # fuzzing 'fu-efi-file-path-device-path.c', # fuzzing 'fu-efi-file.c', # fuzzing 'fu-efi-filesystem.c', # fuzzing 'fu-efi-section.c', # fuzzing 'fu-efi-volume.c', # fuzzing 'fu-efi-lz77-decompressor.c', # fuzzing 'fu-efi-hard-drive-device-path.c', # fuzzing 'fu-efi-load-option.c', # fuzzing 'fu-efi-signature.c', 'fu-efi-signature-list.c', 'fu-efi-variable-authentication2.c', 'fu-efivars.c', # fuzzing 'fu-efi-x509-device.c', 'fu-efi-x509-signature.c', 'fu-elf-firmware.c', # fuzzing 'fu-fdt-firmware.c', # fuzzing 'fu-fdt-image.c', # fuzzing 'fu-firmware.c', # fuzzing 'fu-firmware-common.c', # fuzzing 'fu-fit-firmware.c', # fuzzing 'fu-fmap-firmware.c', # fuzzing 'fu-hid-descriptor.c', # fuzzing 'fu-hid-device.c', 'fu-hid-report-item.c', # fuzzing 'fu-hid-report.c', # fuzzing 'fu-hidraw-device.c', 'fu-hwids.c', # fuzzing 'fu-hwids-config.c', # fuzzing 'fu-hwids-dmi.c', # fuzzing 'fu-hwids-fdt.c', # fuzzing 'fu-hwids-kenv.c', # fuzzing 'fu-hwids-darwin.c', # fuzzing 'fu-hwids-smbios.c', # fuzzing 'fu-i2c-device.c', 'fu-ifd-bios.c', # fuzzing 'fu-ifd-common.c', # fuzzing 'fu-ifd-firmware.c', # fuzzing 'fu-ifd-image.c', # fuzzing 'fu-ifwi-cpd-firmware.c', # fuzzing 'fu-ifwi-fpt-firmware.c', # fuzzing 'fu-ihex-firmware.c', # fuzzing 'fu-input-stream.c', # fuzzing 'fu-intel-thunderbolt-firmware.c', # fuzzing 'fu-intel-thunderbolt-nvm.c', # fuzzing 'fu-io-channel.c', # fuzzing 'fu-ioctl.c', # fuzzing 'fu-kenv.c', # fuzzing 'fu-kernel.c', # fuzzing 'fu-kernel-search-path.c', # fuzzing 'fu-linear-firmware.c', 'fu-lzma-common.c', # fuzzing 'fu-mei-device.c', 'fu-mem.c', # fuzzing 'fu-heci-device.c', 'fu-msgpack.c', 'fu-msgpack-item.c', 'fu-oprom-firmware.c', # fuzzing 'fu-partial-input-stream.c', # fuzzing 'fu-path.c', # fuzzing 'fu-pefile-firmware.c', # fuzzing 'fu-pci-device.c', 'fu-pkcs7.c', 'fu-oprom-device.c', 'fu-plugin.c', 'fu-progress.c', # fuzzing 'fu-quirks.c', # fuzzing 'fu-sbatlevel-section.c', # fuzzing 'fu-security-attr.c', # fuzzing 'fu-security-attrs.c', 'fu-serio-device.c', 'fu-smbios.c', # fuzzing 'fu-srec-firmware.c', # fuzzing 'fu-string.c', # fuzzing 'fu-sum.c', # fuzzing 'fu-udev-device.c', # fuzzing 'fu-uefi-device.c', 'fu-usb-bos-descriptor.c', 'fu-usb-config-descriptor.c', 'fu-usb-descriptor.c', 'fu-usb-device.c', 'fu-usb-device-ds20.c', 'fu-usb-device-fw-ds20.c', 'fu-usb-device-ms-ds20.c', 'fu-usb-endpoint.c', 'fu-usb-hid-descriptor.c', 'fu-usb-interface.c', 'fu-uswid-firmware.c', # fuzzing 'fu-version-common.c', # fuzzing 'fu-v4l-device.c', 'fu-volume.c', # fuzzing 'fu-x509-certificate.c', ] if host_machine.system() in ['linux', 'android'] fwupdplugin_src += 'fu-common-linux.c' # fuzzing fwupdplugin_src += 'fu-linux-efivars.c' elif host_machine.system() == 'freebsd' fwupdplugin_src += 'fu-common-freebsd.c' fwupdplugin_src += 'fu-freebsd-efivars.c' elif host_machine.system() == 'windows' fwupdplugin_src += 'fu-common-windows.c' fwupdplugin_src += 'fu-windows-efivars.c' elif host_machine.system() == 'darwin' fwupdplugin_src += 'fu-common-darwin.c' fwupdplugin_src += 'fu-darwin-efivars.c' # fuzzing endif fwupdplugin_headers = [ 'fu-acpi-table.h', 'fu-archive-firmware.h', 'fu-archive.h', 'fu-backend.h', 'fu-bios-settings.h', 'fu-bios-settings-private.h', 'fu-block-device.h', 'fu-block-partition.h', 'fu-bluez-device.h', 'fu-byte-array.h', 'fu-bytes.h', 'fu-cab-firmware.h', 'fu-cab-image.h', 'fu-cfi-device.h', 'fu-cfu-offer.h', 'fu-cfu-payload.h', 'fu-chunk.h', 'fu-chunk-array.h', 'fu-common-guid.h', 'fu-common.h', 'fu-composite-input-stream.h', 'fu-config.h', 'fu-config-private.h', 'fu-context.h', 'fu-context-private.h', 'fu-coswid-common.h', 'fu-coswid-firmware.h', 'fu-crc.h', 'fu-csv-entry.h', 'fu-csv-firmware.h', 'fu-device.h', 'fu-device-event.h', 'fu-device-locker.h', 'fu-device-metadata.h', 'fu-device-private.h', 'fu-device-progress.h', 'fu-dfu-firmware.h', 'fu-dfuse-firmware.h', 'fu-dpaux-device.h', 'fu-drm-device.h', 'fu-dump.h', 'fu-edid.h', 'fu-efi-common.h', 'fu-efi-device-path.h', 'fu-efi-device-path-list.h', 'fu-efi-file.h', 'fu-efi-filesystem.h', 'fu-efi-hard-drive-device-path.h', 'fu-efi-section.h', 'fu-efi-volume.h', 'fu-efi-load-option.h', 'fu-efi-signature.h', 'fu-efi-signature-list.h', 'fu-efi-variable-authentication2.h', 'fu-efivars.h', 'fu-efi-x509-device.h', 'fu-efi-x509-signature.h', 'fu-elf-firmware.h', 'fu-endian.h', 'fu-fdt-firmware.h', 'fu-fdt-image.h', 'fu-firmware-common.h', 'fu-firmware.h', 'fu-fit-firmware.h', 'fu-fmap-firmware.h', 'fu-heci-device.h', 'fu-hid-descriptor.h', 'fu-hid-device.h', 'fu-hid-report.h', 'fu-hidraw-device.h', 'fu-hwids.h', 'fu-i2c-device.h', 'fu-ifd-bios.h', 'fu-ifd-common.h', 'fu-ifd-firmware.h', 'fu-ifd-image.h', 'fu-ifwi-cpd-firmware.h', 'fu-ifwi-fpt-firmware.h', 'fu-ihex-firmware.h', 'fu-input-stream.h', 'fu-intel-thunderbolt-firmware.h', 'fu-intel-thunderbolt-nvm.h', 'fu-io-channel.h', 'fu-ioctl.h', 'fu-kenv.h', 'fu-kernel.h', 'fu-kernel-search-path.h', 'fu-linear-firmware.h', 'fu-mei-device.h', 'fu-mem.h', 'fu-mem-private.h', 'fu-msgpack-item.h', 'fu-oprom-device.h', 'fu-oprom-firmware.h', 'fu-partial-input-stream.h', 'fu-partial-input-stream-private.h', 'fu-path.h', 'fu-pefile-firmware.h', 'fu-pci-device.h', 'fu-pkcs7.h', 'fu-plugin.h', 'fu-plugin-private.h', 'fu-progress.h', 'fu-quirks.h', 'fu-sbatlevel-section.h', 'fu-security-attr.h', 'fu-security-attrs.h', 'fu-security-attrs-private.h', 'fu-serio-device.h', 'fu-smbios.h', 'fu-smbios-private.h', 'fu-srec-firmware.h', 'fu-string.h', 'fu-sum.h', 'fu-udev-device.h', 'fu-udev-device-private.h', 'fu-uefi-device.h', 'fu-uefi-device-private.h', 'fu-usb-device-ds20.h', 'fu-usb-device-fw-ds20.h', 'fu-usb-bos-descriptor.h', 'fu-usb-bos-descriptor-private.h', 'fu-usb-config-descriptor.h', 'fu-usb-config-descriptor-private.h', 'fu-usb-device.h', 'fu-usb-endpoint.h', 'fu-usb-endpoint-private.h', 'fu-usb-interface.h', 'fu-usb-interface-private.h', 'fu-usb-device-ms-ds20.h', 'fu-usb-device-private.h', 'fu-usb-hid-descriptor.h', 'fu-usb-hid-descriptor-private.h', 'fu-uswid-firmware.h', 'fu-v4l-device.h', 'fu-version-common.h', 'fu-volume.h', 'fu-x509-certificate.h', ] introspection_deps = [ libxmlb, giounix, libusb, ] pkgg_requires = [ 'gio-2.0', 'gmodule-2.0', 'gobject-2.0', 'fwupd', 'json-glib-1.0', 'libarchive', 'xmlb', 'libusb', ] library_deps = [ introspection_deps, gmodule, libjsonglib, zlib, valgrind, lzma, libarchive, libusb, cbor, sqlite, libblkid, libdrm, gnutls, platform_deps, ] fwupdplugin = library( 'fwupdplugin', fwupdplugin_rs_targets, sources: [ fwupdplugin_src, fwupdplugin_headers, ], include_directories: [ root_incdir, fwupd_incdir, ], dependencies: [ library_deps ], link_with: [ fwupd, ], install_dir: libdir_pkg, install: true ) # see https://mesonbuild.com/FAQ.html#how-do-i-tell-meson-that-my-sources-use-generated-headers fwupdplugin_rs_headers = [] foreach fwupdplugin_rs_target: fwupdplugin_rs_targets fwupdplugin_rs_headers += fwupdplugin_rs_target[1] endforeach fwupdplugin_rs_dep = declare_dependency(link_with: fwupdplugin, sources: fwupdplugin_rs_headers) if introspection.allowed() gir_dep = declare_dependency(sources: fwupd_gir) girtargets = [] extra_args = [] if libxmlb.type_name() == 'internal' girtargets += subproject('libxmlb').get_variable('gir')[0] else girtargets += 'Xmlb-2.0' endif fwupdplugin_gir = gnome.generate_gir(fwupd, sources: [ fwupdplugin_src, fwupdplugin_headers, fwupdplugin_rs_targets, ], nsversion: '1.0', namespace: 'FwupdPlugin', symbol_prefix: 'fu', identifier_prefix: 'Fu', export_packages: 'fwupdplugin', extra_args: extra_args, include_directories: [ fwupd_incdir, ], header: 'fwupdplugin.h', dependencies: [ gir_dep, introspection_deps ], link_with: [ fwupdplugin, ], includes: [ 'Gio-2.0', 'GObject-2.0', girtargets, fwupd_gir[0], ], install: false ) endif if get_option('tests') gcab = executable( 'gcab', sources: [ 'fu-gcab.c', ], include_directories: [ root_incdir, fwupd_incdir, ], dependencies: [ library_deps, fwupdplugin_rs_dep, ], link_with: [ fwupd, fwupdplugin ], ) subdir('tests') env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) e = executable( 'fwupdplugin-self-test', installed_firmware_zip, rustgen.process('fu-self-test.rs'), sources: [ 'fu-test-device.c', 'fu-self-test.c' ], include_directories: [ root_incdir, fwupd_incdir, ], dependencies: [ library_deps, fwupdplugin_rs_dep, ], link_with: [ fwupd, fwupdplugin ], c_args: [ '-DSRCDIR="' + meson.current_source_dir() + '"', ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, ) test('fwupdplugin-self-test', e, is_parallel: false, timeout: 180, env: env) endif fwupdplugin_incdir = include_directories('.') fwupd-2.0.10/libfwupdplugin/rustgen.py000077500000000000000000000633121501337203100200160ustar00rootroot00000000000000#!/usr/bin/env python3 # pylint: disable=invalid-name,missing-docstring # # Copyright 2023 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later import os import sys import uuid import argparse from enum import Enum from typing import Optional, List, Tuple, Dict from jinja2 import Environment, FileSystemLoader, select_autoescape class Endian(Enum): NATIVE = "native" LITTLE = "le" BIG = "be" class Type(Enum): NONE = None U8 = "u8" U16 = "u16" U24 = "u24" U32 = "u32" U64 = "u64" STRING = "char" GUID = "Guid" B32 = "b32" I8 = "i8" I16 = "i16" I32 = "i32" I64 = "i64" class Export(Enum): NONE = "none" PRIVATE = "static " PUBLIC = "" # convert a CamelCase name into snake_case def _camel_to_snake(name: str) -> str: # specified as all caps if name.upper() == name: return name.lower() name_snake: str = "" for char in name: if char.islower() or char.isnumeric(): name_snake += char continue if char == "_": name_snake += char continue if name_snake: name_snake += "_" name_snake += char.lower() return name_snake class EnumObj: def __init__(self, name: str) -> None: self.name: str = name self.repr_type: Optional[str] = None self.items: List[EnumItem] = [] self._exports: Dict[str, Export] = { "ToString": Export.NONE, "ToBitString": Export.NONE, "FromString": Export.NONE, } def c_method(self, suffix: str): return f"{_camel_to_snake(self.name)}_{_camel_to_snake(suffix)}" @property def c_type(self): return f"{self.name}" @property def c_define_last(self) -> str: return f"{_camel_to_snake(self.name).upper()}_LAST" @property def items_any_defaults(self) -> bool: for item in self.items: if item.default: return True return False def check(self): # check we're prefixed with something sane if not self.name.startswith("Fu"): raise ValueError(f"enum {self.name} does not have 'Fu' prefix") # check we'd not just done ZERO=0, ONE=1, TWO=2, etc indexed = True for i, item in enumerate(self.items): if str(i) != item.default: indexed = False break if indexed: raise ValueError(f"enum {self.name} does not need explicit defaults") def item(self, name: str) -> Optional["EnumItem"]: for item in self.items: if item.name == name: return item return None def add_private_export(self, derive: str) -> None: if self._exports[derive] == Export.PUBLIC: return self._exports[derive] = Export.PRIVATE def add_public_export(self, derive: str) -> None: self.add_private_export(derive) self._exports[derive] = Export.PUBLIC def export(self, derive: str) -> Export: return self._exports[derive] def __str__(self) -> str: return f"EnumObj({self.name})" class EnumItem: def __init__(self, obj: EnumObj) -> None: self.obj: EnumObj = obj self.name: str = "" self.default: Optional[str] = None @property def c_define(self) -> str: name_snake = _camel_to_snake(self.obj.name) return f"{name_snake.upper()}_{_camel_to_snake(self.name).replace('-', '_').upper()}" def parse_default(self, val: str) -> None: val = { "u64::MAX": "G_MAXUINT64", "u32::MAX": "G_MAXUINT32", "u16::MAX": "G_MAXUINT16", "u8::MAX": "G_MAXUINT8", }.get(val, val) if val.startswith("0x") or val.startswith("0b"): val = val.replace("_", "") if val.startswith("0b"): val = hex(int(val[2:], 2)) self.default = val @property def value(self) -> str: return _camel_to_snake(self.name).replace("_", "-") def __str__(self) -> str: return f"EnumItem({self.name}={self.default})" class StructObj: def __init__(self, name: str) -> None: self.name: str = name self.items: List[StructItem] = [] self._exports: Dict[str, Export] = { "Validate": Export.NONE, "ValidateBytes": Export.NONE, "ValidateStream": Export.NONE, "ValidateInternal": Export.NONE, "Parse": Export.NONE, "ParseBytes": Export.NONE, "ParseStream": Export.NONE, "ParseInternal": Export.NONE, "New": Export.NONE, "ToString": Export.NONE, "Default": Export.NONE, } def c_method(self, suffix: str): return f"{_camel_to_snake(self.name)}_{_camel_to_snake(suffix)}" def c_define(self, suffix: str): return f"{_camel_to_snake(self.name).upper()}_{suffix.upper()}" @property def _has_bits(self) -> bool: for item in self.items: if item.type == Type.B32: return True return False @property def size(self) -> int: size: int = 0 if self._has_bits: return 4 for item in self.items: size += item.size return size @property def has_constant(self) -> bool: for item in self.items: if item.constant: return True return False def check(self): # check we're prefixed with something sane if not self.name.startswith("Fu"): raise ValueError(f"struct {self.name} does not have 'Fu' prefix") def add_private_export(self, derive: str) -> None: if self._exports[derive] == Export.PUBLIC: return self._exports[derive] = Export.PRIVATE if derive == "Validate": self.add_private_export("ValidateInternal") elif derive == "ValidateStream": self.add_private_export("ValidateInternal") elif derive == "ValidateBytes": self.add_private_export("Validate") elif derive == "ValidateInternal": for item in self.items: if ( item.constant and item.type != Type.STRING and not (item.type == Type.U8 and item.n_elements) ): item.add_private_export("Getters") if item.struct_obj: item.struct_obj.add_private_export("ValidateInternal") elif derive == "ToString": for item in self.items: if item.struct_obj: item.struct_obj.add_private_export("ToString") if item.enum_obj and not item.constant and item.enabled: item.enum_obj.add_private_export("ToString") elif derive == "Parse": self.add_private_export("ParseInternal") elif derive == "ParseStream": self.add_private_export("ParseInternal") elif derive == "ParseBytes": self.add_private_export("Parse") elif derive == "ParseInternal": self.add_private_export("ToString") self.add_private_export("ValidateInternal") for item in self.items: if ( item.constant and item.type != Type.STRING and not (item.type == Type.U8 and item.n_elements) ): item.add_private_export("Getters") if item.struct_obj: item.struct_obj.add_private_export("ValidateInternal") elif derive == "New": for item in self.items: if item.constant and not (item.type == Type.U8 and item.n_elements): item.add_private_export("Setters") if item.struct_obj: item.struct_obj.add_private_export("New") def add_public_export(self, derive: str) -> None: # Getters and Setters are special as we do not want public exports of const if derive in ["Getters", "Setters"]: for item in self.items: if not item.constant: item.add_public_export(derive) else: self.add_private_export(derive) self._exports[derive] = Export.PUBLIC # for convenience if derive in ["Parse", "ParseBytes", "ParseStream"]: self.add_public_export("Getters") for item in self.items: if item.struct_obj: item.struct_obj.add_public_export("Getters") if derive == "New": self.add_public_export("Setters") def export(self, derive: str) -> Export: return self._exports[derive] def __str__(self) -> str: return f"StructObj({self.name})" class StructItem: def __init__(self, obj: StructObj) -> None: self.obj: StructObj = obj self.element_id: str = "" self.type: Type = Type.NONE self.is_packed: bool = False self.enum_obj: Optional[EnumObj] = None self.struct_obj: Optional[StructObj] = None self.default: Optional[str] = None self.constant: Optional[str] = None self.padding: Optional[str] = None self.endian: Endian = Endian.NATIVE self.n_elements: int = 0 self._bits_size: int = 0 self._bits_offset: int = 0 self.offset: int = 0 self._exports: Dict[str, Export] = { "Getters": Export.NONE, "Setters": Export.NONE, } def add_private_export(self, derive: str) -> None: if self._exports[derive] == Export.PUBLIC: return self._exports[derive] = Export.PRIVATE def add_public_export(self, derive: str) -> None: self.add_private_export(derive) self._exports[derive] = Export.PUBLIC def export(self, derive: str) -> Export: return self._exports[derive] @property def bits_offset(self) -> int: # from 32 bit word start return self._bits_offset @property def bits_size(self) -> int: if self.type == Type.B32: return self._bits_size return self.size * 8 @property def bits_mask(self) -> int: return (1 << self._bits_size) - 1 @property def size(self) -> int: n_elements = self.n_elements if not n_elements: n_elements = 1 if self.struct_obj: return n_elements * self.struct_obj.size if self.type in [Type.U8, Type.I8, Type.STRING]: return n_elements if self.type in [Type.GUID]: return n_elements * 16 if self.type in [Type.U16, Type.I16]: return n_elements * 2 if self.type == Type.U24: return n_elements * 3 if self.type in [Type.U32, Type.I32]: return n_elements * 4 if self.type in [Type.U64, Type.I64]: return n_elements * 8 return 0 @property def enabled(self) -> bool: if self.element_id.startswith("_"): return False if self.element_id == "reserved": return False return True @property def endian_glib(self) -> str: if self.endian == Endian.LITTLE: return "G_LITTLE_ENDIAN" if self.endian == Endian.BIG: return "G_BIG_ENDIAN" return "G_BYTE_ORDER" def c_define(self, suffix: str): return self.obj.c_define(suffix.upper() + "_" + self.element_id.upper()) @property def c_getter(self): return self.obj.c_method("get_" + self.element_id) @property def c_setter(self): return self.obj.c_method("set_" + self.element_id) @property def type_glib(self) -> str: if self.enum_obj: return self.enum_obj.c_type if self.type == Type.U8: return "guint8" if self.type == Type.U16: return "guint16" if self.type == Type.U24: return "guint32" if self.type == Type.U32: return "guint32" if self.type == Type.U64: return "guint64" if self.type == Type.STRING: return "gchar" if self.type == Type.GUID: return "fwupd_guid_t" if self.type == Type.B32: return "guint32" if self.type == Type.I8: return "gint8" if self.type == Type.I16: return "gint16" if self.type == Type.I32: return "gint32" if self.type == Type.I64: return "gint64" return "void" @property def type_mem(self) -> str: if self.type == Type.U16: return "uint16" if self.type == Type.U24: return "uint24" if self.type == Type.U32: return "uint32" if self.type == Type.B32: return "uint32" if self.type == Type.U64: return "uint64" if self.type == Type.I16: return "uint16" if self.type == Type.I32: return "uint32" if self.type == Type.I64: return "uint64" return "" def _parse_default(self, val: str) -> str: if self.enum_obj: enum_item = self.enum_obj.item(val) if not enum_item: msg: str = [item.name for item in self.enum_obj.items] raise ValueError(f"enum default unknown, got {val} expected: {msg}") return enum_item.c_define if self.type == Type.STRING: if val.startswith('"') and val.endswith('"'): return val[1:-1] raise ValueError(f"string default {val} needs double quotes") if self.type == Type.GUID: if val.startswith("0x"): guid = uuid.UUID(bytes_le=bytes.fromhex(val[2:])) raise ValueError(f"integer {val} expected, expected: {guid}") if not val.startswith('"'): raise ValueError(f"string expected, got: {val}") uuid2 = uuid.UUID(val[1:-1]) val_hex = "" for value in uuid2.bytes_le: val_hex += f"\\x{value:x}" return val_hex if self.type == Type.U8 and self.n_elements: if not val.startswith("0x"): raise ValueError(f"0x prefix for hex number expected, got: {val}") if len(val) != (self.size * 2) + 2: raise ValueError(f"data has to be {self.size} bytes exactly") val_hex = "" for idx in range(2, len(val), 2): val_hex += f"\\x{val[idx:idx+2]}" return val_hex if self.type in [ Type.U8, Type.U16, Type.U24, Type.U32, Type.U64, Type.B32, ]: if val.startswith("0x") or val.startswith("0b"): val = val.replace("_", "") return val.replace("$struct_offset", str(self.offset)) raise ValueError(f"do not know how to parse value for type: {self.type}") def parse_default(self, val: str) -> None: if ( self.type == Type.U8 and self.n_elements and val.startswith("0x") and len(val) == 4 ): self.padding = val return self.default = self._parse_default(val) def parse_constant(self, val: str) -> None: self.default = self._parse_default(val) self.constant = self.default def parse_type( self, val: str, enum_objs: Dict[str, EnumObj], struct_objs: Dict[str, StructObj] ) -> None: # is array if val.startswith("[") and val.endswith("]"): typestr, n_elements = val[1:-1].split(";", maxsplit=1) if n_elements.startswith("0x"): self.n_elements = int(n_elements[2:], 16) else: self.n_elements = int(n_elements) else: typestr = val # nested struct if typestr in struct_objs: self.struct_obj = struct_objs[typestr] return # find the type if typestr in enum_objs: self.enum_obj = enum_objs[typestr] typestr_maybe: Optional[str] = enum_objs[typestr].repr_type if not typestr_maybe: raise ValueError(f"no repr for: {typestr}") typestr = typestr_maybe # detect endian if typestr.endswith("be"): self.endian = Endian.BIG typestr = typestr[:-2] elif typestr.endswith("le"): self.endian = Endian.LITTLE typestr = typestr[:-2] # support partial bytes for bits_size in range(1, 32): if bits_size in [8, 16, 24, 32]: continue if typestr == f"u{bits_size}": self.type = Type.B32 self._bits_size = bits_size if self.endian == Endian.NATIVE: self.endian = Endian.LITTLE return # defined types try: self.type = Type(typestr) except ValueError as e: raise ValueError(f"invalid type: {typestr}") from e # sanity check if ( self.enabled and self.is_packed and self.endian == Endian.NATIVE and self.type in [Type.U16, Type.U24, Type.U32, Type.U64, Type.I16, Type.I32, Type.I64] ): raise ValueError(f"endian not specified for packed struct: {typestr}") def __str__(self) -> str: tmp = f"{self.element_id}: " if self.n_elements: tmp += str(self.n_elements) tmp += self.type.value if self.endian != Endian.NATIVE: tmp += self.endian.value if self.default: tmp += f" = {self.default}" elif self.constant: tmp += f" == {self.constant}" elif self.padding: tmp += f" = {self.padding}" return tmp class Generator: def __init__(self, basename) -> None: self.basename: str = basename self.struct_objs: Dict[str, StructObj] = {} self.enum_objs: Dict[str, EnumObj] = {} self._env = Environment( loader=FileSystemLoader(os.path.dirname(__file__)), autoescape=select_autoescape(), keep_trailing_newline=True, ) def _process_enums(self, enum_obj: EnumObj) -> Tuple[str, str]: # render subst = { "Type": Type, "Export": Export, "obj": enum_obj, } template_h = self._env.get_template(os.path.basename("fu-rustgen-enum.h.in")) template_c = self._env.get_template(os.path.basename("fu-rustgen-enum.c.in")) return template_c.render(subst), template_h.render(subst) def _process_structs(self, struct_obj: StructObj) -> Tuple[str, str]: # render subst = { "Type": Type, "Export": Export, "obj": struct_obj, } template_h = self._env.get_template(os.path.basename("fu-rustgen-struct.h.in")) template_c = self._env.get_template(os.path.basename("fu-rustgen-struct.c.in")) return template_c.render(subst), template_h.render(subst) def process_input(self, contents: str) -> Tuple[str, str]: name = None repr_type: Optional[str] = None derives: List[str] = [] offset: int = 0 struct_seen_b32: bool = False bits_offset: int = 0 struct_cur: Optional[StructObj] = None enum_cur: Optional[EnumObj] = None for line_num, line in enumerate(contents.split("\n")): # replace all tabs with spaces line = line.replace("\t", " ") # remove comments and indent line = line.split("//")[0].strip() if not line: continue # start of structure if line.startswith("struct ") and line.endswith("{"): name = line[6:-1].strip() if name in self.struct_objs: raise ValueError( f"struct {name} already defined on line {line_num}" ) struct_cur = StructObj(name) self.struct_objs[name] = struct_cur continue if line.startswith("enum ") and line.endswith("{"): name = line[4:-1].strip() if name in self.enum_objs: raise ValueError(f"enum {name} already defined on line {line_num}") enum_cur = EnumObj(name) enum_cur.repr_type = repr_type self.enum_objs[name] = enum_cur continue # the enum type if line.startswith("#[repr(") and line.endswith(")]"): repr_type = line[7:-2] continue # what should we build if line.startswith("#[derive("): for derive in line[9:-2].replace(" ", "").split(","): derives.append(derive) continue # not in object if not struct_cur and not enum_cur: continue # end of structure if line.startswith("}"): if struct_cur: struct_cur.check() for derive in derives: struct_cur.add_public_export(derive) for item in struct_cur.items: if item.default == "$struct_size": item.default = str(offset) if item.constant == "$struct_size": item.constant = str(offset) if enum_cur: enum_cur.check() for derive in derives: enum_cur.add_public_export(derive) struct_cur = None enum_cur = None repr_type = None derives.clear() offset = 0 bits_offset = 0 struct_seen_b32 = False continue # check for trailing comma if not line.endswith(","): raise ValueError( f"invalid struct line on line {line_num}: {line} -- needs trailing comma" ) line = line[:-1] # split enumeration into sections if enum_cur: enum_item = EnumItem(enum_cur) parts = line.replace(" ", "").split("=", maxsplit=2) enum_item.name = parts[0] if len(parts) > 1: enum_item.parse_default(parts[1]) enum_cur.items.append(enum_item) # split structure into sections if struct_cur: # parse "signature: u32be == 0x12345678" parts = line.replace(" ", "").split(":", maxsplit=2) if len(parts) == 1: raise ValueError(f"invalid struct line on line {line_num}: {line}") # parse one element item = StructItem(struct_cur) item._bits_offset = bits_offset item.offset = offset item.element_id = parts[0] if repr_type == "C, packed": item.is_packed = True type_parts = parts[1].split("=", maxsplit=3) try: item.parse_type( type_parts[0], enum_objs=self.enum_objs, struct_objs=self.struct_objs, ) except ValueError as e: raise ValueError(f"{str(e)} on line {line_num}: {line}") if len(type_parts) > 1: if "Default" not in derives: raise ValueError( f"struct requires #[derive(Default)] for line {line_num}: {line}" ) if len(type_parts) == 3: item.parse_constant(type_parts[2]) elif len(type_parts) == 2: item.parse_default(type_parts[1]) if item.size == 0: struct_seen_b32 = True if not struct_seen_b32: offset += item.size bits_offset += item.bits_size struct_cur.items.append(item) # process the templates here subst = { "basename": self.basename, "enum_objs": self.enum_objs, "struct_objs": self.struct_objs, } template_h = self._env.get_template(os.path.basename("fu-rustgen.h.in")) template_c = self._env.get_template(os.path.basename("fu-rustgen.c.in")) dst_h = template_h.render(subst) dst_c = template_c.render(subst) for enum_obj in self.enum_objs.values(): str_c, str_h = self._process_enums(enum_obj) dst_c += str_c dst_h += str_h for struct_obj in self.struct_objs.values(): str_c, str_h = self._process_structs(struct_obj) dst_c += str_c dst_h += str_h # success return dst_c, dst_h if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("src", action="store", type=str, help="source") parser.add_argument("dst_c", action="store", type=str, help="destination .c") parser.add_argument("dst_h", action="store", type=str, help="destination .h") args = parser.parse_args() g = Generator(basename=os.path.basename(args.dst_h)) with open(args.src, "rb") as f: try: dst_c, dst_h = g.process_input(f.read().decode()) except ValueError as e: sys.exit(f"cannot process {args.src}: {str(e)}") with open(args.dst_c, "wb") as f: # type: ignore f.write(dst_c.encode()) with open(args.dst_h, "wb") as f: # type: ignore f.write(dst_h.encode()) fwupd-2.0.10/libfwupdplugin/tests/000077500000000000000000000000001501337203100171075ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/.gitattributes000066400000000000000000000000331501337203100217760ustar00rootroot00000000000000hid-descriptor*.bin binary fwupd-2.0.10/libfwupdplugin/tests/.gitignore000066400000000000000000000000121501337203100210700ustar00rootroot00000000000000DMI-*.bin fwupd-2.0.10/libfwupdplugin/tests/America/000077500000000000000000000000001501337203100204505ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/America/New_York000066400000000000000000000000001501337203100221160ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/archive.builder.xml000066400000000000000000000004601501337203100226770ustar00rootroot00000000000000 ustar gzip one.txt aGVsbG8gd29ybGQ= two.txt aGVsbG8gd29ybGQ= fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/000077500000000000000000000000001501337203100211765ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/000077500000000000000000000000001501337203100234645ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/000077500000000000000000000000001501337203100265065ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/000077500000000000000000000000001501337203100306745ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Absolute/000077500000000000000000000000001501337203100324525ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100413432../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Absolutedefault_value000077700000000000000000000000001501337203100413052../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Absolutedell_modifier000077700000000000000000000000001501337203100423772../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Absolutedell_value_modifier000077700000000000000000000000001501337203100435732../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Absolutedisplay_name000066400000000000000000000000111501337203100347530ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AbsoluteAbsolute display_name_language_code000077700000000000000000000000001501337203100464132../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Absolutepossible_values000066400000000000000000000000461501337203100355150ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AbsoluteEnabled;Disabled;PermanentlyDisabled; type000077700000000000000000000000001501337203100355512../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AbsoluteAdminSetupLockout/000077500000000000000000000000001501337203100342275ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100450452../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdminSetupLockoutdefault_value000077700000000000000000000000001501337203100450072../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdminSetupLockoutdell_modifier000077700000000000000000000000001501337203100442332../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdminSetupLockoutdell_value_modifier000077700000000000000000000000001501337203100454272../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdminSetupLockoutdisplay_name000066400000000000000000000000331501337203100366130ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdminSetupLockoutEnable Admin Setup Lockout display_name_language_code000077700000000000000000000000001501337203100502472../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdminSetupLockoutpossible_values000077700000000000000000000000001501337203100440412../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdminSetupLockouttype000077700000000000000000000000001501337203100374052../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdminSetupLockoutAdvBatteryChargeCfg/000077500000000000000000000000001501337203100344145ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100452322../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdvBatteryChargeCfgdefault_value000077700000000000000000000000001501337203100451742../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdvBatteryChargeCfgdell_modifier000077700000000000000000000000001501337203100444202../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdvBatteryChargeCfgdell_value_modifier000077700000000000000000000000001501337203100456142../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdvBatteryChargeCfgdisplay_name000066400000000000000000000000551501337203100370040ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdvBatteryChargeCfgEnable Advanced Battery Charge Configuration display_name_language_code000077700000000000000000000000001501337203100504342../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdvBatteryChargeCfgpossible_values000077700000000000000000000000001501337203100442262../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdvBatteryChargeCfgtype000077700000000000000000000000001501337203100375722../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdvBatteryChargeCfgAdvancedMode/000077500000000000000000000000001501337203100331275ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100420772../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdvancedModedefault_value000077700000000000000000000000001501337203100420412../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdvancedModedell_modifier000077700000000000000000000000001501337203100431332../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdvancedModedell_value_modifier000077700000000000000000000000001501337203100443272../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdvancedModedisplay_name000066400000000000000000000000311501337203100355110ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdvancedModeBIOS Setup Advanced Mode display_name_language_code000077700000000000000000000000001501337203100471472../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdvancedModepossible_values000077700000000000000000000000001501337203100427412../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdvancedModetype000077700000000000000000000000001501337203100363052../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AdvancedModeAllowBiosDowngrade/000077500000000000000000000000001501337203100343435ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100433132../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AllowBiosDowngradedefault_value000077700000000000000000000000001501337203100432552../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AllowBiosDowngradedell_modifier000077700000000000000000000000001501337203100443472../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AllowBiosDowngradedell_value_modifier000077700000000000000000000000001501337203100455432../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AllowBiosDowngradedisplay_name000066400000000000000000000000251501337203100367300ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AllowBiosDowngradeAllow BIOS Downgrade display_name_language_code000077700000000000000000000000001501337203100503632../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AllowBiosDowngradepossible_values000077700000000000000000000000001501337203100441552../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AllowBiosDowngradetype000077700000000000000000000000001501337203100375212../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AllowBiosDowngradefwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Asset/000077500000000000000000000000001501337203100317535ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100417602../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Assetdefault_value000077700000000000000000000000001501337203100371572./display_nameustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Assetdell_modifier000077700000000000000000000000001501337203100417002../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Assetdisplay_name000066400000000000000000000000121501337203100342550ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AssetAsset Tag display_name_language_code000077700000000000000000000000001501337203100457142../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Assetmax_length000066400000000000000000000000031501337203100337360ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Asset64 min_length000066400000000000000000000000021501337203100337330ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Asset1 fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Asset/type000066400000000000000000000000071501337203100326540ustar00rootroot00000000000000string AutoOSRecoveryThreshold/000077500000000000000000000000001501337203100353635ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000066400000000000000000000000021501337203100401540ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOSRecoveryThreshold2 default_value000077700000000000000000000000001501337203100430572./current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOSRecoveryThresholddell_modifier000077700000000000000000000000001501337203100453672../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOSRecoveryThresholddell_value_modifier000077700000000000000000000000001501337203100465632../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOSRecoveryThresholddisplay_name000066400000000000000000000000401501337203100377450ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOSRecoveryThresholdDell Auto OS Recovery Threshold display_name_language_code000077700000000000000000000000001501337203100514032../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOSRecoveryThresholdpossible_values000066400000000000000000000000131501337203100404770ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOSRecoveryThresholdOFF;1;2;3; type000077700000000000000000000000001501337203100405412../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOSRecoveryThresholdfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOn/000077500000000000000000000000001501337203100321015ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100426402../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOndefault_value000077700000000000000000000000001501337203100426022../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOndell_modifier000077700000000000000000000000001501337203100420262../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOndell_value_modifier000077700000000000000000000000001501337203100432222../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOndisplay_name000066400000000000000000000000151501337203100344060ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnAuto On Time display_name_language_code000077700000000000000000000000001501337203100460422../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnpossible_values000066400000000000000000000000471501337203100351450ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnDisabled;Everyday;Weekdays;SelectDays; fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOn/type000077700000000000000000000000001501337203100352572../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnFri/000077500000000000000000000000001501337203100325425ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100433012../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnFridefault_value000077700000000000000000000000001501337203100432432../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnFridell_modifier000077700000000000000000000000001501337203100420162../AutoOnTue/dell_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnFridell_value_modifier000077700000000000000000000000001501337203100436632../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnFridisplay_name000066400000000000000000000000071501337203100350500ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnFriFriday display_name_language_code000077700000000000000000000000001501337203100465032../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnFripossible_values000077700000000000000000000000001501337203100422752../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnFritype000077700000000000000000000000001501337203100356412../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnFrifwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnHr/000077500000000000000000000000001501337203100323735ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100371472./min_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnHrdefault_value000077700000000000000000000000001501337203100371112./min_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnHrdell_modifier000077700000000000000000000000001501337203100423202../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnHrdisplay_name000066400000000000000000000000131501337203100346760ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnHrHours (HH) display_name_language_code000077700000000000000000000000001501337203100463342../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnHrmax_value000066400000000000000000000000031501337203100342110ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnHr23 min_value000066400000000000000000000000021501337203100342060ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnHr0 scalar_increment000077700000000000000000000000001501337203100411042../Asset/min_lengthustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnHrtype000066400000000000000000000000101501337203100332070ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnHrinteger fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnMn/000077500000000000000000000000001501337203100323745ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100407252../AutoOnHr/min_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnMndefault_value000077700000000000000000000000001501337203100406672../AutoOnHr/min_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnMndell_modifier000077700000000000000000000000001501337203100423212../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnMndisplay_name000066400000000000000000000000151501337203100347010ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnMnMinutes (MM) display_name_language_code000077700000000000000000000000001501337203100463352../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnMnmax_value000066400000000000000000000000031501337203100342120ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnMn59 min_value000077700000000000000000000000001501337203100400262../AutoOnHr/min_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnMnscalar_increment000077700000000000000000000000001501337203100411052../Asset/min_lengthustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnMntype000077700000000000000000000000001501337203100360322../AutoOnHr/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnMnfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnMon/000077500000000000000000000000001501337203100325535ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100433122../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnMondefault_value000077700000000000000000000000001501337203100432542../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnMondell_modifier000077700000000000000000000000001501337203100420272../AutoOnTue/dell_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnMondell_value_modifier000077700000000000000000000000001501337203100436742../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnMondisplay_name000066400000000000000000000000071501337203100350610ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnMonMonday display_name_language_code000077700000000000000000000000001501337203100465142../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnMonpossible_values000077700000000000000000000000001501337203100423062../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnMontype000077700000000000000000000000001501337203100356522../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnMonfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnSat/000077500000000000000000000000001501337203100325515ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100433102../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnSatdefault_value000077700000000000000000000000001501337203100432522../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnSatdell_modifier000077700000000000000000000000001501337203100420252../AutoOnTue/dell_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnSatdell_value_modifier000077700000000000000000000000001501337203100436722../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnSatdisplay_name000066400000000000000000000000111501337203100350520ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnSatSaturday display_name_language_code000077700000000000000000000000001501337203100465122../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnSatpossible_values000077700000000000000000000000001501337203100423042../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnSattype000077700000000000000000000000001501337203100356502../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnSatfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnSun/000077500000000000000000000000001501337203100325675ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100433262../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnSundefault_value000077700000000000000000000000001501337203100432702../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnSundell_modifier000077700000000000000000000000001501337203100420432../AutoOnTue/dell_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnSundell_value_modifier000077700000000000000000000000001501337203100437102../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnSundisplay_name000066400000000000000000000000071501337203100350750ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnSunSunday display_name_language_code000077700000000000000000000000001501337203100465302../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnSunpossible_values000077700000000000000000000000001501337203100423222../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnSuntype000077700000000000000000000000001501337203100356662../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnSunfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnThur/000077500000000000000000000000001501337203100327445ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100435032../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnThurdefault_value000077700000000000000000000000001501337203100434452../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnThurdell_modifier000077700000000000000000000000001501337203100422202../AutoOnTue/dell_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnThurdell_value_modifier000077700000000000000000000000001501337203100440652../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnThurdisplay_name000066400000000000000000000000111501337203100352450ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnThurThursday display_name_language_code000077700000000000000000000000001501337203100467052../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnThurpossible_values000077700000000000000000000000001501337203100424772../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnThurtype000077700000000000000000000000001501337203100360432../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnThurfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnTue/000077500000000000000000000000001501337203100325575ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100433162../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnTuedefault_value000077700000000000000000000000001501337203100432602../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnTuedell_modifier000066400000000000000000000000421501337203100352150ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnTue[SuppressIfNot:AutoOn=SelectDays] dell_value_modifier000077700000000000000000000000001501337203100437002../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnTuedisplay_name000066400000000000000000000000101501337203100350570ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnTueTuesday display_name_language_code000077700000000000000000000000001501337203100465202../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnTuepossible_values000077700000000000000000000000001501337203100423122../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnTuetype000077700000000000000000000000001501337203100356562../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnTuefwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnWed/000077500000000000000000000000001501337203100325415ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100433002../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnWeddefault_value000077700000000000000000000000001501337203100432422../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnWeddell_modifier000077700000000000000000000000001501337203100420152../AutoOnTue/dell_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnWeddell_value_modifier000077700000000000000000000000001501337203100436622../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnWeddisplay_name000066400000000000000000000000121501337203100350430ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnWedWednesday display_name_language_code000077700000000000000000000000001501337203100465022../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnWedpossible_values000077700000000000000000000000001501337203100422742../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnWedtype000077700000000000000000000000001501337203100356402../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnWedfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BIOSConnect/000077500000000000000000000000001501337203100327425ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100416332../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BIOSConnectdefault_value000077700000000000000000000000001501337203100415752../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BIOSConnectdell_modifier000077700000000000000000000000001501337203100426672../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BIOSConnectdell_value_modifier000077700000000000000000000000001501337203100440632../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BIOSConnectdisplay_name000066400000000000000000000000141501337203100352460ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BIOSConnectBIOSConnect display_name_language_code000077700000000000000000000000001501337203100467032../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BIOSConnectpossible_values000077700000000000000000000000001501337203100424752../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BIOSConnecttype000077700000000000000000000000001501337203100360412../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BIOSConnectBiosLogClear/000077500000000000000000000000001501337203100331225ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100437372../ThermalLogClear/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosLogCleardefault_value000077700000000000000000000000001501337203100437012../ThermalLogClear/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosLogCleardell_modifier000077700000000000000000000000001501337203100431262../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosLogCleardell_value_modifier000077700000000000000000000000001501337203100443222../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosLogCleardisplay_name000066400000000000000000000000251501337203100355070ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosLogClearClear Bios Event Log display_name_language_code000077700000000000000000000000001501337203100471422../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosLogClearpossible_values000077700000000000000000000000001501337203100446012../ThermalLogClear/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosLogCleartype000077700000000000000000000000001501337203100363002../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosLogClearBiosRcvrFrmHdd/000077500000000000000000000000001501337203100334335ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100424032../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosRcvrFrmHdddefault_value000077700000000000000000000000001501337203100423452../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosRcvrFrmHdddell_modifier000077700000000000000000000000001501337203100434372../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosRcvrFrmHdddell_value_modifier000077700000000000000000000000001501337203100446332../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosRcvrFrmHdddisplay_name000066400000000000000000000000361501337203100360220ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosRcvrFrmHddBIOS Recovery from Hard Drive display_name_language_code000077700000000000000000000000001501337203100474532../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosRcvrFrmHddpossible_values000077700000000000000000000000001501337203100432452../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosRcvrFrmHddtype000077700000000000000000000000001501337203100366112../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosRcvrFrmHddfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BlockSleep/000077500000000000000000000000001501337203100327175ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100434562../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BlockSleepdefault_value000077700000000000000000000000001501337203100434202../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BlockSleepdell_modifier000077700000000000000000000000001501337203100426442../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BlockSleepdell_value_modifier000077700000000000000000000000001501337203100440402../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BlockSleepdisplay_name000066400000000000000000000000141501337203100352230ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BlockSleepBlock Sleep display_name_language_code000077700000000000000000000000001501337203100466602../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BlockSleeppossible_values000077700000000000000000000000001501337203100424522../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BlockSleeptype000077700000000000000000000000001501337203100360162../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BlockSleepBluetoothDevice/000077500000000000000000000000001501337203100337025ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100426522../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BluetoothDevicedefault_value000077700000000000000000000000001501337203100426142../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BluetoothDevicedell_modifier000077700000000000000000000000001501337203100437062../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BluetoothDevicedell_value_modifier000077700000000000000000000000001501337203100451022../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BluetoothDevicedisplay_name000066400000000000000000000000121501337203100362630ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BluetoothDeviceBluetooth display_name_language_code000077700000000000000000000000001501337203100477222../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BluetoothDevicepossible_values000077700000000000000000000000001501337203100435142../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BluetoothDevicetype000077700000000000000000000000001501337203100370602../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BluetoothDevicefwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CStatesCtrl/000077500000000000000000000000001501337203100330675ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100417602../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CStatesCtrldefault_value000077700000000000000000000000001501337203100417222../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CStatesCtrldell_modifier000077700000000000000000000000001501337203100430142../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CStatesCtrldell_value_modifier000077700000000000000000000000001501337203100442102../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CStatesCtrldisplay_name000066400000000000000000000000271501337203100353770ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CStatesCtrlEnable C-State Control display_name_language_code000077700000000000000000000000001501337203100470302../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CStatesCtrlpossible_values000077700000000000000000000000001501337203100426222../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CStatesCtrltype000077700000000000000000000000001501337203100361662../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CStatesCtrlfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Camera/000077500000000000000000000000001501337203100320645ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100407552../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Cameradefault_value000077700000000000000000000000001501337203100407172../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Cameradell_modifier000077700000000000000000000000001501337203100420112../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Cameradell_value_modifier000077700000000000000000000000001501337203100432052../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Cameradisplay_name000066400000000000000000000000161501337203100343720ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CameraEnable Camera display_name_language_code000077700000000000000000000000001501337203100460252../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Camerapossible_values000077700000000000000000000000001501337203100416172../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Camerafwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Camera/type000077700000000000000000000000001501337203100352422../SdCard/typeustar00rootroot00000000000000CapsuleFirmwareUpdate/000077500000000000000000000000001501337203100350515ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100440212../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CapsuleFirmwareUpdatedefault_value000077700000000000000000000000001501337203100437632../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CapsuleFirmwareUpdatedell_modifier000077700000000000000000000000001501337203100450552../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CapsuleFirmwareUpdatedell_value_modifier000077700000000000000000000000001501337203100462512../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CapsuleFirmwareUpdatedisplay_name000066400000000000000000000000451501337203100374400ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CapsuleFirmwareUpdateEnable UEFI Capsule Firmware Updates display_name_language_code000077700000000000000000000000001501337203100510712../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CapsuleFirmwareUpdatepossible_values000077700000000000000000000000001501337203100446632../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CapsuleFirmwareUpdatetype000077700000000000000000000000001501337203100402272../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CapsuleFirmwareUpdatefwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CpuCore/000077500000000000000000000000001501337203100322345ustar00rootroot00000000000000current_value000066400000000000000000000000111501337203100347460ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CpuCoreCoresAll default_value000077700000000000000000000000001501337203100376512./current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CpuCoredell_modifier000077700000000000000000000000001501337203100421612../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CpuCoredell_value_modifier000077700000000000000000000000001501337203100433552../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CpuCoredisplay_name000066400000000000000000000000151501337203100345410ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CpuCoreActive Cores display_name_language_code000077700000000000000000000000001501337203100461752../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CpuCorepossible_values000066400000000000000000000000201501337203100352670ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CpuCoreCoresAll;1;2;3; fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CpuCore/type000077700000000000000000000000001501337203100354122../SdCard/typeustar00rootroot00000000000000CustomChargeStart/000077500000000000000000000000001501337203100342175ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100410522./min_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStartdefault_value000077700000000000000000000000001501337203100410142./min_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStartdell_modifier000077700000000000000000000000001501337203100442232../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStartdisplay_name000066400000000000000000000000241501337203100366030ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStartCustom Charge Start display_name_language_code000077700000000000000000000000001501337203100502372../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStartmax_value000066400000000000000000000000031501337203100361140ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStart95 min_value000066400000000000000000000000031501337203100361120ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStart50 scalar_increment000077700000000000000000000000001501337203100430072../Asset/min_lengthustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStarttype000077700000000000000000000000001501337203100377342../AutoOnHr/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStartCustomChargeStop/000077500000000000000000000000001501337203100340475ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000066400000000000000000000000031501337203100366410ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStop90 default_value000077700000000000000000000000001501337203100415432./current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStopdell_modifier000077700000000000000000000000001501337203100440532../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStopdisplay_name000066400000000000000000000000231501337203100364320ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStopCustom Charge Stop display_name_language_code000077700000000000000000000000001501337203100500672../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStopmax_value000077700000000000000000000000001501337203100451532../PeakShiftBatteryThreshold/max_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStopmin_value000066400000000000000000000000031501337203100357420ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStop55 scalar_increment000077700000000000000000000000001501337203100426372../Asset/min_lengthustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStoptype000077700000000000000000000000001501337203100375642../AutoOnHr/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStopDellCoreService/000077500000000000000000000000001501337203100336275ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100444452../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DellCoreServicedefault_value000077700000000000000000000000001501337203100444072../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DellCoreServicedell_modifier000077700000000000000000000000001501337203100436332../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DellCoreServicedell_value_modifier000077700000000000000000000000001501337203100450272../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DellCoreServicedisplay_name000066400000000000000000000000201501337203100362070ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DellCoreServiceDellCoreService display_name_language_code000077700000000000000000000000001501337203100476472../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DellCoreServicepossible_values000077700000000000000000000000001501337203100434412../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DellCoreServicetype000077700000000000000000000000001501337203100370052../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DellCoreServiceDockWarningsEnMsg/000077500000000000000000000000001501337203100341405ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100431102../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DockWarningsEnMsgdefault_value000077700000000000000000000000001501337203100430522../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DockWarningsEnMsgdell_modifier000077700000000000000000000000001501337203100441442../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DockWarningsEnMsgdell_value_modifier000077700000000000000000000000001501337203100453402../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DockWarningsEnMsgdisplay_name000066400000000000000000000000351501337203100365260ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DockWarningsEnMsgEnable Dock Warning Messages display_name_language_code000077700000000000000000000000001501337203100501602../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DockWarningsEnMsgpossible_values000077700000000000000000000000001501337203100437522../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DockWarningsEnMsgtype000077700000000000000000000000001501337203100373162../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DockWarningsEnMsgfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DynTunML/000077500000000000000000000000001501337203100323465ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100412372../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DynTunMLdefault_value000077700000000000000000000000001501337203100412012../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DynTunMLdell_modifier000077700000000000000000000000001501337203100422732../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DynTunMLdell_value_modifier000077700000000000000000000000001501337203100434672../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DynTunMLdisplay_name000066400000000000000000000000471501337203100346600ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DynTunMLEnable Dynamic Tuning:Machine Learning display_name_language_code000077700000000000000000000000001501337203100463072../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DynTunMLpossible_values000077700000000000000000000000001501337203100421012../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DynTunMLtype000077700000000000000000000000001501337203100354452../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/DynTunMLfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/EmbSataRaid/000077500000000000000000000000001501337203100330105ustar00rootroot00000000000000current_value000066400000000000000000000000051501337203100355250ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/EmbSataRaidRaid default_value000077700000000000000000000000001501337203100404252./current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/EmbSataRaiddell_modifier000077700000000000000000000000001501337203100427352../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/EmbSataRaiddell_value_modifier000077700000000000000000000000001501337203100441312../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/EmbSataRaiddisplay_name000066400000000000000000000000241501337203100353150ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/EmbSataRaidSATA/NVMe Opeartion display_name_language_code000077700000000000000000000000001501337203100467512../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/EmbSataRaidpossible_values000066400000000000000000000000241501337203100360470ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/EmbSataRaidDisabled;Ahci;Raid; type000077700000000000000000000000001501337203100361072../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/EmbSataRaidfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ExtPostTime/000077500000000000000000000000001501337203100331215ustar00rootroot00000000000000current_value000066400000000000000000000000031501337203100356340ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ExtPostTime0s default_value000077700000000000000000000000001501337203100405362./current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ExtPostTimedell_modifier000077700000000000000000000000001501337203100430462../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ExtPostTimedell_value_modifier000077700000000000000000000000001501337203100442422../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ExtPostTimedisplay_name000066400000000000000000000000261501337203100354300ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ExtPostTimeExtend BIOS POST Time display_name_language_code000077700000000000000000000000001501337203100470622../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ExtPostTimepossible_values000066400000000000000000000000131501337203100361560ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ExtPostTime0s;5s;10s; type000077700000000000000000000000001501337203100362202../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ExtPostTimefwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FOTA/000077500000000000000000000000001501337203100314255ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100403162../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FOTAdefault_value000077700000000000000000000000001501337203100402602../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FOTAdell_modifier000077700000000000000000000000001501337203100413522../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FOTAdell_value_modifier000077700000000000000000000000001501337203100425462../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FOTAdisplay_name000066400000000000000000000000051501337203100337310ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FOTAFOTA display_name_language_code000077700000000000000000000000001501337203100453662../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FOTApossible_values000077700000000000000000000000001501337203100411602../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FOTAfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FOTA/type000077700000000000000000000000001501337203100346032../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Fastboot/000077500000000000000000000000001501337203100324555ustar00rootroot00000000000000current_value000066400000000000000000000000101501337203100351660ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FastbootMinimal default_value000066400000000000000000000000111501337203100351310ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FastbootThorough dell_modifier000077700000000000000000000000001501337203100424022../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Fastbootdell_value_modifier000077700000000000000000000000001501337203100435762../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Fastbootdisplay_name000066400000000000000000000000111501337203100347560ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FastbootFastboot display_name_language_code000077700000000000000000000000001501337203100464162../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Fastbootpossible_values000066400000000000000000000000271501337203100355170ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FastbootMinimal;Thorough;Auto; type000077700000000000000000000000001501337203100355542../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FastbootFingerprintReader/000077500000000000000000000000001501337203100342275ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100431772../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FingerprintReaderdefault_value000077700000000000000000000000001501337203100431412../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FingerprintReaderdell_modifier000077700000000000000000000000001501337203100442332../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FingerprintReaderdell_value_modifier000077700000000000000000000000001501337203100454272../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FingerprintReaderdisplay_name000066400000000000000000000000411501337203100366120ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FingerprintReaderEnable Fingerprint Reader Device display_name_language_code000077700000000000000000000000001501337203100502472../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FingerprintReaderpossible_values000077700000000000000000000000001501337203100440412../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FingerprintReadertype000077700000000000000000000000001501337203100374052../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FingerprintReaderFingerprintReaderSingleSignOn/000077500000000000000000000000001501337203100365075ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100454572../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FingerprintReaderSingleSignOndefault_value000077700000000000000000000000001501337203100454212../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FingerprintReaderSingleSignOndell_modifier000077700000000000000000000000001501337203100465132../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FingerprintReaderSingleSignOndell_value_modifier000077700000000000000000000000001501337203100477072../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FingerprintReaderSingleSignOndisplay_name000066400000000000000000000000511501337203100410730ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FingerprintReaderSingleSignOnEnable Fingerprint Reader Single Sign On display_name_language_code000077700000000000000000000000001501337203100525272../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FingerprintReaderSingleSignOnpossible_values000077700000000000000000000000001501337203100463212../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FingerprintReaderSingleSignOntype000077700000000000000000000000001501337203100416652../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FingerprintReaderSingleSignOnfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FnLock/000077500000000000000000000000001501337203100320505ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100407412../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FnLockdefault_value000077700000000000000000000000001501337203100407032../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FnLockdell_modifier000077700000000000000000000000001501337203100417752../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FnLockdell_value_modifier000077700000000000000000000000001501337203100431712../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FnLockdisplay_name000066400000000000000000000000201501337203100343510ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FnLockFn Lock Options display_name_language_code000077700000000000000000000000001501337203100460112../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FnLockpossible_values000077700000000000000000000000001501337203100416032../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FnLockfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FnLock/type000077700000000000000000000000001501337203100352262../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FnLockMode/000077500000000000000000000000001501337203100326555ustar00rootroot00000000000000current_value000066400000000000000000000000201501337203100353670ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FnLockModeDisableStandard default_value000066400000000000000000000000201501337203100353310ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FnLockModeEnableSecondary dell_modifier000077700000000000000000000000001501337203100426022../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FnLockModedell_value_modifier000077700000000000000000000000001501337203100437762../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FnLockModedisplay_name000066400000000000000000000000121501337203100351570ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FnLockModeLock Mode display_name_language_code000077700000000000000000000000001501337203100466162../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FnLockModepossible_values000066400000000000000000000000411501337203100357130ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FnLockModeDisableStandard;EnableSecondary; type000077700000000000000000000000001501337203100357542../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FnLockModeFullScreenLogo/000077500000000000000000000000001501337203100335005ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100443162../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FullScreenLogodefault_value000077700000000000000000000000001501337203100442602../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FullScreenLogodell_modifier000077700000000000000000000000001501337203100435042../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FullScreenLogodell_value_modifier000077700000000000000000000000001501337203100447002../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FullScreenLogodisplay_name000066400000000000000000000000211501337203100360610ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FullScreenLogoFull Screen Logo display_name_language_code000077700000000000000000000000001501337203100475202../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FullScreenLogopossible_values000077700000000000000000000000001501337203100433122../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FullScreenLogotype000077700000000000000000000000001501337203100366562../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/FullScreenLogoIntegratedAudio/000077500000000000000000000000001501337203100336655ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100426352../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/IntegratedAudiodefault_value000077700000000000000000000000001501337203100425772../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/IntegratedAudiodell_modifier000077700000000000000000000000001501337203100436712../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/IntegratedAudiodell_value_modifier000077700000000000000000000000001501337203100450652../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/IntegratedAudiodisplay_name000066400000000000000000000000151501337203100362510ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/IntegratedAudioEnable Audio display_name_language_code000077700000000000000000000000001501337203100477052../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/IntegratedAudiopossible_values000077700000000000000000000000001501337203100434772../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/IntegratedAudiotype000077700000000000000000000000001501337203100370432../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/IntegratedAudiofwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/IntelGna/000077500000000000000000000000001501337203100323755ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100431342../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/IntelGnadefault_value000077700000000000000000000000001501337203100412302../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/IntelGnadell_modifier000077700000000000000000000000001501337203100423222../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/IntelGnadell_value_modifier000077700000000000000000000000001501337203100435162../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/IntelGnadisplay_name000066400000000000000000000000271501337203100347050ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/IntelGnaIntel@ GNA Accelerator display_name_language_code000077700000000000000000000000001501337203100463362../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/IntelGnapossible_values000077700000000000000000000000001501337203100421302../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/IntelGnatype000077700000000000000000000000001501337203100354742../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/IntelGnaInternalSpeaker/000077500000000000000000000000001501337203100337045ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100426542../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/InternalSpeakerdefault_value000077700000000000000000000000001501337203100426162../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/InternalSpeakerdell_modifier000077700000000000000000000000001501337203100437102../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/InternalSpeakerdell_value_modifier000077700000000000000000000000001501337203100451042../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/InternalSpeakerdisplay_name000066400000000000000000000000301501337203100362650ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/InternalSpeakerEnable Internal Speaker display_name_language_code000077700000000000000000000000001501337203100477242../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/InternalSpeakerpossible_values000077700000000000000000000000001501337203100435162../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/InternalSpeakertype000077700000000000000000000000001501337203100370622../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/InternalSpeakerKbdBacklightTimeoutAc/000077500000000000000000000000001501337203100347415ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000066400000000000000000000000041501337203100375340ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KbdBacklightTimeoutAc10s default_value000077700000000000000000000000001501337203100424352./current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KbdBacklightTimeoutAcdell_modifier000077700000000000000000000000001501337203100447452../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KbdBacklightTimeoutAcdell_value_modifier000077700000000000000000000000001501337203100461412../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KbdBacklightTimeoutAcdisplay_name000066400000000000000000000000411501337203100373240ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KbdBacklightTimeoutAcKeyboard Backlight Timeout on AC display_name_language_code000077700000000000000000000000001501337203100507612../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KbdBacklightTimeoutAcpossible_values000066400000000000000000000000401501337203100400550ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KbdBacklightTimeoutAc5s;10s;15s;30s;1m;5m;15m;Never; type000077700000000000000000000000001501337203100401172../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KbdBacklightTimeoutAcKbdBacklightTimeoutBatt/000077500000000000000000000000001501337203100353105ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100472442../KbdBacklightTimeoutAc/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KbdBacklightTimeoutBattdefault_value000077700000000000000000000000001501337203100472062../KbdBacklightTimeoutAc/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KbdBacklightTimeoutBattdell_modifier000077700000000000000000000000001501337203100453142../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KbdBacklightTimeoutBattdell_value_modifier000077700000000000000000000000001501337203100465102../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KbdBacklightTimeoutBattdisplay_name000066400000000000000000000000461501337203100377000ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KbdBacklightTimeoutBattKeyboard Backlight Timeout on Battery display_name_language_code000077700000000000000000000000001501337203100513302../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KbdBacklightTimeoutBattpossible_values000077700000000000000000000000001501337203100501062../KbdBacklightTimeoutAc/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KbdBacklightTimeoutBatttype000077700000000000000000000000001501337203100404662../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KbdBacklightTimeoutBattKeyboardIllumination/000077500000000000000000000000001501337203100347425ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100455602../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KeyboardIlluminationdefault_value000066400000000000000000000000071501337203100375020ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KeyboardIlluminationBright dell_modifier000077700000000000000000000000001501337203100447462../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KeyboardIlluminationdell_value_modifier000077700000000000000000000000001501337203100461422../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KeyboardIlluminationdisplay_name000066400000000000000000000000261501337203100373300ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KeyboardIlluminationKeyboard Illumination display_name_language_code000077700000000000000000000000001501337203100507622../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KeyboardIlluminationpossible_values000066400000000000000000000000251501337203100400610ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KeyboardIlluminationDisabled;Dim;Bright; type000077700000000000000000000000001501337203100401202../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/KeyboardIlluminationfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/LidSwitch/000077500000000000000000000000001501337203100325665ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100414572../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/LidSwitchdefault_value000077700000000000000000000000001501337203100414212../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/LidSwitchdell_modifier000077700000000000000000000000001501337203100425132../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/LidSwitchdell_value_modifier000077700000000000000000000000001501337203100437072../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/LidSwitchdisplay_name000066400000000000000000000000221501337203100350710ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/LidSwitchEnable Lid Switch display_name_language_code000077700000000000000000000000001501337203100465272../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/LidSwitchpossible_values000077700000000000000000000000001501337203100423212../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/LidSwitchtype000077700000000000000000000000001501337203100356652../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/LidSwitchfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/LogicProc/000077500000000000000000000000001501337203100325555ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100414462../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/LogicProcdefault_value000077700000000000000000000000001501337203100414102../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/LogicProcdell_modifier000077700000000000000000000000001501337203100425022../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/LogicProcdell_value_modifier000077700000000000000000000000001501337203100436762../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/LogicProcdisplay_name000066400000000000000000000000501501337203100350610ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/LogicProcEnable Intel Hyper-Threading Technology display_name_language_code000077700000000000000000000000001501337203100465162../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/LogicProcpossible_values000077700000000000000000000000001501337203100423102../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/LogicProctype000077700000000000000000000000001501337203100356542../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/LogicProcfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/M2PcieSsd0/000077500000000000000000000000001501337203100325055ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100413762../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/M2PcieSsd0default_value000077700000000000000000000000001501337203100413402../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/M2PcieSsd0dell_modifier000077700000000000000000000000001501337203100424322../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/M2PcieSsd0dell_value_modifier000077700000000000000000000000001501337203100436262../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/M2PcieSsd0display_name000066400000000000000000000000151501337203100350120ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/M2PcieSsd0M.2 PCIe SSD display_name_language_code000077700000000000000000000000001501337203100464462../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/M2PcieSsd0possible_values000077700000000000000000000000001501337203100422402../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/M2PcieSsd0type000077700000000000000000000000001501337203100356042../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/M2PcieSsd0MacAddrPassThru/000077500000000000000000000000001501337203100336025ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000066400000000000000000000000151501337203100363770ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/MacAddrPassThruSystemUnique default_value000077700000000000000000000000001501337203100412762./current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/MacAddrPassThrudell_modifier000077700000000000000000000000001501337203100436062../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/MacAddrPassThrudell_value_modifier000077700000000000000000000000001501337203100450022../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/MacAddrPassThrudisplay_name000066400000000000000000000000311501337203100361640ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/MacAddrPassThruMAC Address Pass-Through display_name_language_code000077700000000000000000000000001501337203100476222../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/MacAddrPassThrupossible_values000066400000000000000000000000271501337203100367230ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/MacAddrPassThruSystemUnique;Disabled; type000077700000000000000000000000001501337203100367602../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/MacAddrPassThruMasterPasswordLockout/000077500000000000000000000000001501337203100351345ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100457522../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/MasterPasswordLockoutdefault_value000077700000000000000000000000001501337203100457142../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/MasterPasswordLockoutdell_modifier000077700000000000000000000000001501337203100451402../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/MasterPasswordLockoutdell_value_modifier000077700000000000000000000000001501337203100463342../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/MasterPasswordLockoutdisplay_name000066400000000000000000000000301501337203100375150ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/MasterPasswordLockoutMaster Password Lockout display_name_language_code000077700000000000000000000000001501337203100511542../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/MasterPasswordLockoutpossible_values000077700000000000000000000000001501337203100447462../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/MasterPasswordLockouttype000077700000000000000000000000001501337203100403122../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/MasterPasswordLockoutfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Microphone/000077500000000000000000000000001501337203100327775ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100416702../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Microphonedefault_value000077700000000000000000000000001501337203100416322../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Microphonedell_modifier000077700000000000000000000000001501337203100427242../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Microphonedell_value_modifier000077700000000000000000000000001501337203100441202../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Microphonedisplay_name000066400000000000000000000000221501337203100353020ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/MicrophoneEnable Microphone display_name_language_code000077700000000000000000000000001501337203100467402../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Microphonepossible_values000077700000000000000000000000001501337203100425322../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Microphonetype000077700000000000000000000000001501337203100360762../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Microphonefwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/NumLock/000077500000000000000000000000001501337203100322445ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100411352../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/NumLockdefault_value000077700000000000000000000000001501337203100410772../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/NumLockdell_modifier000077700000000000000000000000001501337203100421712../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/NumLockdell_value_modifier000077700000000000000000000000001501337203100433652../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/NumLockdisplay_name000066400000000000000000000000171501337203100345530ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/NumLockEnable Numlock display_name_language_code000077700000000000000000000000001501337203100462052../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/NumLockpossible_values000077700000000000000000000000001501337203100417772../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/NumLockfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/NumLock/type000077700000000000000000000000001501337203100354222../SdCard/typeustar00rootroot00000000000000PasswordBypass/000077500000000000000000000000001501337203100336015ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100444172../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PasswordBypassdefault_value000077700000000000000000000000001501337203100443612../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PasswordBypassdell_modifier000077700000000000000000000000001501337203100436052../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PasswordBypassdell_value_modifier000077700000000000000000000000001501337203100450012../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PasswordBypassdisplay_name000066400000000000000000000000201501337203100361610ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PasswordBypassPassword Bypass display_name_language_code000077700000000000000000000000001501337203100476212../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PasswordBypasspossible_values000066400000000000000000000000271501337203100367220ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PasswordBypassDisabled;RebootBypass; type000077700000000000000000000000001501337203100367572../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PasswordBypassPasswordLock/000077500000000000000000000000001501337203100332305ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100422002../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PasswordLockdefault_value000077700000000000000000000000001501337203100421422../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PasswordLockdell_modifier000077700000000000000000000000001501337203100432342../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PasswordLockdell_value_modifier000077700000000000000000000000001501337203100444302../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PasswordLockdisplay_name000066400000000000000000000000421501337203100356140ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PasswordLockEnable Non-Admin Password Changes display_name_language_code000077700000000000000000000000001501337203100472502../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PasswordLockpossible_values000077700000000000000000000000001501337203100430422../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PasswordLocktype000077700000000000000000000000001501337203100364062../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PasswordLockPeakShiftBatteryThreshold/000077500000000000000000000000001501337203100357035ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100443132../AutoOnHr/min_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PeakShiftBatteryThresholddefault_value000077700000000000000000000000001501337203100425002./min_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PeakShiftBatteryThresholddell_modifier000066400000000000000000000000431501337203100404210ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PeakShiftBatteryThreshold[SuppressIf:PeakShiftCfg=Disabled] display_name000066400000000000000000000000401501337203100402650ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PeakShiftBatteryThresholdBattery Threshold [15% to 100%] display_name_language_code000077700000000000000000000000001501337203100517232../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PeakShiftBatteryThresholdmax_value000066400000000000000000000000041501337203100376010ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PeakShiftBatteryThreshold100 min_value000066400000000000000000000000031501337203100375760ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PeakShiftBatteryThreshold15 scalar_increment000077700000000000000000000000001501337203100444732../Asset/min_lengthustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PeakShiftBatteryThresholdtype000077700000000000000000000000001501337203100414202../AutoOnHr/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PeakShiftBatteryThresholdPeakShiftCfg/000077500000000000000000000000001501337203100331135ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100437312../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PeakShiftCfgdefault_value000077700000000000000000000000001501337203100436732../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PeakShiftCfgdell_modifier000077700000000000000000000000001501337203100431172../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PeakShiftCfgdell_value_modifier000077700000000000000000000000001501337203100443132../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PeakShiftCfgdisplay_name000066400000000000000000000000221501337203100354750ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PeakShiftCfgEnable Peak Shift display_name_language_code000077700000000000000000000000001501337203100471332../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PeakShiftCfgpossible_values000077700000000000000000000000001501337203100427252../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PeakShiftCfgtype000077700000000000000000000000001501337203100362712../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PeakShiftCfgfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PntDevice/000077500000000000000000000000001501337203100325555ustar00rootroot00000000000000current_value000066400000000000000000000000111501337203100352670ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PntDeviceTouchpad default_value000077700000000000000000000000001501337203100401722./current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PntDevicedell_modifier000077700000000000000000000000001501337203100425022../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PntDevicedell_value_modifier000077700000000000000000000000001501337203100436762../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PntDevicedisplay_name000066400000000000000000000000171501337203100350640ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PntDeviceMouse/Touchpad display_name_language_code000077700000000000000000000000001501337203100465162../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PntDevicepossible_values000066400000000000000000000000371501337203100356200ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PntDeviceSerialMouse;Ps2Mouse;Touchpad; type000077700000000000000000000000001501337203100356542../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PntDevicePowerLogClear/000077500000000000000000000000001501337203100333225ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100441372../ThermalLogClear/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerLogCleardefault_value000077700000000000000000000000001501337203100441012../ThermalLogClear/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerLogCleardell_modifier000077700000000000000000000000001501337203100433262../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerLogCleardell_value_modifier000077700000000000000000000000001501337203100445222../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerLogCleardisplay_name000066400000000000000000000000261501337203100357100ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerLogClearClear POWER Event Log display_name_language_code000077700000000000000000000000001501337203100473422../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerLogClearpossible_values000077700000000000000000000000001501337203100450012../ThermalLogClear/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerLogCleartype000077700000000000000000000000001501337203100365002../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerLogClearPowerOnLidOpen/000077500000000000000000000000001501337203100334615ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100442772../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerOnLidOpendefault_value000077700000000000000000000000001501337203100423732../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerOnLidOpendell_modifier000077700000000000000000000000001501337203100434652../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerOnLidOpendell_value_modifier000077700000000000000000000000001501337203100446612../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerOnLidOpendisplay_name000066400000000000000000000000221501337203100360430ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerOnLidOpenPower On Lid Open display_name_language_code000077700000000000000000000000001501337203100475012../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerOnLidOpenpossible_values000077700000000000000000000000001501337203100432732../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerOnLidOpentype000077700000000000000000000000001501337203100366372../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerOnLidOpenfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerWarn/000077500000000000000000000000001501337203100326205ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100415112../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerWarndefault_value000077700000000000000000000000001501337203100414532../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerWarndell_modifier000077700000000000000000000000001501337203100425452../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerWarndell_value_modifier000077700000000000000000000000001501337203100437412../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerWarndisplay_name000066400000000000000000000000301501337203100351220ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerWarnEnable Adapter Warnings display_name_language_code000077700000000000000000000000001501337203100465612../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerWarnpossible_values000077700000000000000000000000001501337203100423532../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerWarntype000077700000000000000000000000001501337203100357172../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PowerWarnPrimaryBattChargeCfg/000077500000000000000000000000001501337203100346055ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000066400000000000000000000000111501337203100373760ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PrimaryBattChargeCfgAdaptive default_value000077700000000000000000000000001501337203100423012./current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PrimaryBattChargeCfgdell_modifier000077700000000000000000000000001501337203100446112../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PrimaryBattChargeCfgdell_value_modifier000077700000000000000000000000001501337203100460052../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PrimaryBattChargeCfgdisplay_name000066400000000000000000000000261501337203100371730ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PrimaryBattChargeCfgBattery Configuration display_name_language_code000077700000000000000000000000001501337203100506252../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PrimaryBattChargeCfgpossible_values000066400000000000000000000000541501337203100377260ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PrimaryBattChargeCfgAdaptive;Standard;Express;PrimAcUse;Custom; type000077700000000000000000000000001501337203100377632../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PrimaryBattChargeCfgfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdDigitRqd/000077500000000000000000000000001501337203100330565ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100436152../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdDigitRqddefault_value000077700000000000000000000000001501337203100435572../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdDigitRqddell_modifier000077700000000000000000000000001501337203100430032../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdDigitRqddell_value_modifier000077700000000000000000000000001501337203100441772../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdDigitRqddisplay_name000066400000000000000000000000061501337203100353630ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdDigitRqdDigit display_name_language_code000077700000000000000000000000001501337203100470172../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdDigitRqdpossible_values000077700000000000000000000000001501337203100426112../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdDigitRqdtype000077700000000000000000000000001501337203100361552../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdDigitRqdPwdLowerCaseRqd/000077500000000000000000000000001501337203100336235ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000066400000000000000000000000111501337203100364140ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdLowerCaseRqdDisabled default_value000077700000000000000000000000001501337203100413172./current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdLowerCaseRqddell_modifier000077700000000000000000000000001501337203100436272../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdLowerCaseRqddell_value_modifier000077700000000000000000000000001501337203100450232../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdLowerCaseRqddisplay_name000066400000000000000000000000221501337203100362050ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdLowerCaseRqdLower Case Letter display_name_language_code000077700000000000000000000000001501337203100476432../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdLowerCaseRqdpossible_values000077700000000000000000000000001501337203100434352../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdLowerCaseRqdtype000077700000000000000000000000001501337203100370012../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdLowerCaseRqdfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdMinLen/000077500000000000000000000000001501337203100325315ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100373052./min_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdMinLendefault_value000077700000000000000000000000001501337203100372472./min_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdMinLendell_modifier000077700000000000000000000000001501337203100424562../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdMinLendisplay_name000066400000000000000000000000231501337203100350350ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdMinLenMinimum Characters display_name_language_code000077700000000000000000000000001501337203100464722../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdMinLenmax_value000066400000000000000000000000031501337203100343470ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdMinLen32 min_value000066400000000000000000000000021501337203100343440ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdMinLen4 scalar_increment000077700000000000000000000000001501337203100412422../Asset/min_lengthustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdMinLentype000077700000000000000000000000001501337203100361672../AutoOnHr/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdMinLenPwdSpecialCharRqd/000077500000000000000000000000001501337203100341155ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100447332../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdSpecialCharRqddefault_value000077700000000000000000000000001501337203100446752../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdSpecialCharRqddell_modifier000077700000000000000000000000001501337203100441212../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdSpecialCharRqddell_value_modifier000077700000000000000000000000001501337203100453152../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdSpecialCharRqddisplay_name000066400000000000000000000000221501337203100364770ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdSpecialCharRqdSpecial Character display_name_language_code000077700000000000000000000000001501337203100501352../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdSpecialCharRqdpossible_values000077700000000000000000000000001501337203100437272../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdSpecialCharRqdtype000077700000000000000000000000001501337203100372732../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdSpecialCharRqdPwdUpperCaseRqd/000077500000000000000000000000001501337203100336265ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100444442../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdUpperCaseRqddefault_value000077700000000000000000000000001501337203100444062../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdUpperCaseRqddell_modifier000077700000000000000000000000001501337203100436322../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdUpperCaseRqddell_value_modifier000077700000000000000000000000001501337203100450262../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdUpperCaseRqddisplay_name000066400000000000000000000000221501337203100362100ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdUpperCaseRqdUpper Case Letter display_name_language_code000077700000000000000000000000001501337203100476462../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdUpperCaseRqdpossible_values000077700000000000000000000000001501337203100434402../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdUpperCaseRqdtype000077700000000000000000000000001501337203100370042../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/PwdUpperCaseRqdRemoteWipeInternalDrives/000077500000000000000000000000001501337203100355475ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000066400000000000000000000000101501337203100403370ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/RemoteWipeInternalDrivesUnarmed default_value000077700000000000000000000000001501337203100432432./current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/RemoteWipeInternalDrivesdell_modifier000066400000000000000000000000251501337203100402650ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/RemoteWipeInternalDrives[ProgHideLocal:TRUE] dell_value_modifier000077700000000000000000000000001501337203100467472../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/RemoteWipeInternalDrivesdisplay_name000077700000000000000000000000001501337203100454222../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/RemoteWipeInternalDrivesdisplay_name_language_code000077700000000000000000000000001501337203100515672../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/RemoteWipeInternalDrivespossible_values000066400000000000000000000000361501337203100406700ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/RemoteWipeInternalDrivesUnarmed;Armed;Complete;Error; type000077700000000000000000000000001501337203100407252../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/RemoteWipeInternalDrivesfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SHA256/000077500000000000000000000000001501337203100315445ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100404352../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SHA256default_value000077700000000000000000000000001501337203100403772../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SHA256dell_modifier000077700000000000000000000000001501337203100414712../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SHA256dell_value_modifier000077700000000000000000000000001501337203100426652../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SHA256display_name000066400000000000000000000000101501337203100340440ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SHA256SHA-256 display_name_language_code000077700000000000000000000000001501337203100455052../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SHA256possible_values000077700000000000000000000000001501337203100412772../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SHA256fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SHA256/type000077700000000000000000000000001501337203100347222../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCard/000077500000000000000000000000001501337203100320345ustar00rootroot00000000000000current_value000066400000000000000000000000101501337203100345450ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCardEnabled default_value000077700000000000000000000000001501337203100374512./current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCarddell_modifier000077700000000000000000000000001501337203100405432./dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCarddell_value_modifier000066400000000000000000000000011501337203100356610ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCard display_name000066400000000000000000000000311501337203100343370ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCardSecure Digital (SD) Card display_name_language_code000066400000000000000000000000061501337203100371760ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCarden-US possible_values000066400000000000000000000000221501337203100350710ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCardDisabled;Enabled; fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCard/type000066400000000000000000000000141501337203100327330ustar00rootroot00000000000000enumeration fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCardBoot/000077500000000000000000000000001501337203100326605ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100434172../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCardBootdefault_value000077700000000000000000000000001501337203100433612../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCardBootdell_modifier000077700000000000000000000000001501337203100426052../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCardBootdell_value_modifier000077700000000000000000000000001501337203100440012../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCardBootdisplay_name000066400000000000000000000000451501337203100351700ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCardBootEnable Secure Digital (SD) Card Boot display_name_language_code000077700000000000000000000000001501337203100466212../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCardBootpossible_values000077700000000000000000000000001501337203100424132../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCardBoottype000077700000000000000000000000001501337203100357572../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCardBootSdCardReadOnly/000077500000000000000000000000001501337203100334135ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100442312../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCardReadOnlydefault_value000077700000000000000000000000001501337203100441732../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCardReadOnlydell_modifier000077700000000000000000000000001501337203100434172../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCardReadOnlydell_value_modifier000077700000000000000000000000001501337203100446132../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCardReadOnlydisplay_name000066400000000000000000000000501501337203100357760ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCardReadOnlySecure Digital (SD) Card Read-Only Mode display_name_language_code000077700000000000000000000000001501337203100474332../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCardReadOnlypossible_values000077700000000000000000000000001501337203100432252../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCardReadOnlytype000077700000000000000000000000001501337203100365712../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SdCardReadOnlyfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBoot/000077500000000000000000000000001501337203100327465ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100416372../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBootdefault_value000077700000000000000000000000001501337203100434472../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBootdell_modifier000077700000000000000000000000001501337203100426732../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBootdell_value_modifier000077700000000000000000000000001501337203100440672../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBootdisplay_name000066400000000000000000000000231501337203100352520ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBootEnable Secure Boot display_name_language_code000077700000000000000000000000001501337203100467072../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBootpossible_values000077700000000000000000000000001501337203100425012../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBoottype000077700000000000000000000000001501337203100360452../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBootSecureBootMode/000077500000000000000000000000001501337203100334745ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000066400000000000000000000000151501337203100362710ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBootModeDeployedMode default_value000077700000000000000000000000001501337203100411702./current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBootModedell_modifier000077700000000000000000000000001501337203100435002../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBootModedell_value_modifier000077700000000000000000000000001501337203100446742../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBootModedisplay_name000066400000000000000000000000211501337203100360550ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBootModeSecure Boot Mode display_name_language_code000077700000000000000000000000001501337203100475142../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBootModepossible_values000066400000000000000000000000301501337203100366070ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBootModeDeployedMode;AuditMode; type000077700000000000000000000000001501337203100366522../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBootModeSignOfLifeByDisplay/000077500000000000000000000000001501337203100344235ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100433732../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SignOfLifeByDisplaydefault_value000077700000000000000000000000001501337203100433352../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SignOfLifeByDisplaydell_modifier000077700000000000000000000000001501337203100444272../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SignOfLifeByDisplaydell_value_modifier000077700000000000000000000000001501337203100456232../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SignOfLifeByDisplaydisplay_name000066400000000000000000000000231501337203100370060ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SignOfLifeByDisplayEarly Logo Display display_name_language_code000077700000000000000000000000001501337203100504432../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SignOfLifeByDisplaypossible_values000077700000000000000000000000001501337203100442352../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SignOfLifeByDisplaytype000077700000000000000000000000001501337203100376012../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SignOfLifeByDisplaySignOfLifeByKbdBacklight/000077500000000000000000000000001501337203100353275ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100442772../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SignOfLifeByKbdBacklightdefault_value000077700000000000000000000000001501337203100442412../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SignOfLifeByKbdBacklightdell_modifier000077700000000000000000000000001501337203100453332../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SignOfLifeByKbdBacklightdell_value_modifier000077700000000000000000000000001501337203100465272../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SignOfLifeByKbdBacklightdisplay_name000066400000000000000000000000311501337203100377110ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SignOfLifeByKbdBacklightEarly Keyboard Backlight display_name_language_code000077700000000000000000000000001501337203100513472../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SignOfLifeByKbdBacklightpossible_values000077700000000000000000000000001501337203100451412../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SignOfLifeByKbdBacklighttype000077700000000000000000000000001501337203100405052../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SignOfLifeByKbdBacklightfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SmartErrors/000077500000000000000000000000001501337203100331575ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100437162../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SmartErrorsdefault_value000077700000000000000000000000001501337203100436602../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SmartErrorsdell_modifier000077700000000000000000000000001501337203100431042../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SmartErrorsdell_value_modifier000077700000000000000000000000001501337203100443002../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SmartErrorsdisplay_name000066400000000000000000000000271501337203100354670ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SmartErrorsEnable SMART Reporting display_name_language_code000077700000000000000000000000001501337203100471202../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SmartErrorspossible_values000077700000000000000000000000001501337203100427122../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SmartErrorstype000077700000000000000000000000001501337203100362562../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SmartErrorsSmmSecurityMitigation/000077500000000000000000000000001501337203100351265ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100440762../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SmmSecurityMitigationdefault_value000077700000000000000000000000001501337203100457062../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SmmSecurityMitigationdell_modifier000077700000000000000000000000001501337203100451322../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SmmSecurityMitigationdell_value_modifier000077700000000000000000000000001501337203100463262../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SmmSecurityMitigationdisplay_name000066400000000000000000000000301501337203100375070ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SmmSecurityMitigationSMM Security Mitigation display_name_language_code000077700000000000000000000000001501337203100511462../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SmmSecurityMitigationpossible_values000077700000000000000000000000001501337203100447402../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SmmSecurityMitigationtype000077700000000000000000000000001501337203100403042../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SmmSecurityMitigationfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SpeedShift/000077500000000000000000000000001501337203100327325ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100416232../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SpeedShiftdefault_value000077700000000000000000000000001501337203100415652../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SpeedShiftdell_modifier000077700000000000000000000000001501337203100426572../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SpeedShiftdell_value_modifier000077700000000000000000000000001501337203100440532../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SpeedShiftdisplay_name000066400000000000000000000000351501337203100352410ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SpeedShiftIntel Speed Shift Technology display_name_language_code000077700000000000000000000000001501337203100466732../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SpeedShiftpossible_values000077700000000000000000000000001501337203100424652../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SpeedShifttype000077700000000000000000000000001501337203100360312../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SpeedShiftfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Speedstep/000077500000000000000000000000001501337203100326305ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100415212../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Speedstepdefault_value000077700000000000000000000000001501337203100414632../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Speedstepdell_modifier000077700000000000000000000000001501337203100425552../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Speedstepdell_value_modifier000077700000000000000000000000001501337203100437512../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Speedstepdisplay_name000066400000000000000000000000421501337203100351350ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SpeedstepEnable Intel SpeedStep Technology display_name_language_code000077700000000000000000000000001501337203100465712../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Speedsteppossible_values000077700000000000000000000000001501337203100423632../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Speedsteptype000077700000000000000000000000001501337203100357272../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SpeedstepStrongPassword/000077500000000000000000000000001501337203100336145ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100444322../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/StrongPassworddefault_value000077700000000000000000000000001501337203100443742../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/StrongPassworddell_modifier000077700000000000000000000000001501337203100436202../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/StrongPassworddell_value_modifier000077700000000000000000000000001501337203100450142../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/StrongPassworddisplay_name000066400000000000000000000000301501337203100361750ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/StrongPasswordEnable Strong Passwords display_name_language_code000077700000000000000000000000001501337203100476342../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/StrongPasswordpossible_values000077700000000000000000000000001501337203100434262../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/StrongPasswordtype000077700000000000000000000000001501337203100367722../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/StrongPasswordSupportAssistOSRecovery/000077500000000000000000000000001501337203100354415ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100444112../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SupportAssistOSRecoverydefault_value000077700000000000000000000000001501337203100443532../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SupportAssistOSRecoverydell_modifier000077700000000000000000000000001501337203100454452../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SupportAssistOSRecoverydell_value_modifier000077700000000000000000000000001501337203100466412../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SupportAssistOSRecoverydisplay_name000066400000000000000000000000321501337203100400240ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SupportAssistOSRecoverySupportAssist OS Recovery display_name_language_code000077700000000000000000000000001501337203100514612../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SupportAssistOSRecoverypossible_values000077700000000000000000000000001501337203100452532../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SupportAssistOSRecoverytype000077700000000000000000000000001501337203100406172../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SupportAssistOSRecoveryfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SvcTag/000077500000000000000000000000001501337203100320635ustar00rootroot00000000000000current_value000066400000000000000000000000101501337203100345740ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SvcTag8RQ19C3 default_value000077700000000000000000000000001501337203100372672./display_nameustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SvcTagdell_modifier000077700000000000000000000000001501337203100420102../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SvcTagdisplay_name000066400000000000000000000000141501337203100343670ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SvcTagService Tag display_name_language_code000077700000000000000000000000001501337203100460242../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SvcTagmax_length000077700000000000000000000000001501337203100362542./min_lengthustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SvcTagmin_length000066400000000000000000000000021501337203100340430ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SvcTag7 fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SvcTag/type000077700000000000000000000000001501337203100351602../Asset/typeustar00rootroot00000000000000TelemetryAccessLvl/000077500000000000000000000000001501337203100343675ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000066400000000000000000000000051501337203100371630ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TelemetryAccessLvlFull default_value000077700000000000000000000000001501337203100420632./current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TelemetryAccessLvldell_modifier000077700000000000000000000000001501337203100443732../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TelemetryAccessLvldell_value_modifier000077700000000000000000000000001501337203100455672../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TelemetryAccessLvldisplay_name000066400000000000000000000000271501337203100367560ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TelemetryAccessLvlTelemetry Access Level display_name_language_code000077700000000000000000000000001501337203100504072../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TelemetryAccessLvlpossible_values000066400000000000000000000000361501337203100375100ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TelemetryAccessLvlDisabled;Basic;Enhanced;Full; type000077700000000000000000000000001501337203100375452../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TelemetryAccessLvlThermalLogClear/000077500000000000000000000000001501337203100336225ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000066400000000000000000000000051501337203100364160ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThermalLogClearKeep default_value000077700000000000000000000000001501337203100413162./current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThermalLogCleardell_modifier000077700000000000000000000000001501337203100436262../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThermalLogCleardell_value_modifier000077700000000000000000000000001501337203100450222../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThermalLogCleardisplay_name000066400000000000000000000000301501337203100362030ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThermalLogClearClear Thermal Event Log display_name_language_code000077700000000000000000000000001501337203100476422../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThermalLogClearpossible_values000066400000000000000000000000141501337203100367370ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThermalLogClearKeep;Clear; type000077700000000000000000000000001501337203100370002../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThermalLogClearThermalManagement/000077500000000000000000000000001501337203100342065ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000066400000000000000000000000121501337203100370000ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThermalManagementOptimized default_value000077700000000000000000000000001501337203100417022./current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThermalManagementdell_modifier000077700000000000000000000000001501337203100442122../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThermalManagementdell_value_modifier000077700000000000000000000000001501337203100454062../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThermalManagementdisplay_name000066400000000000000000000000231501337203100365710ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThermalManagementThermal Management display_name_language_code000077700000000000000000000000001501337203100502262../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThermalManagementpossible_values000066400000000000000000000000471501337203100373310ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThermalManagementOptimized;Cool;Quiet;UltraPerformance; type000077700000000000000000000000001501337203100373642../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThermalManagementThunderboltBoot/000077500000000000000000000000001501337203100337335ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100445512../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThunderboltBootdefault_value000077700000000000000000000000001501337203100445132../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThunderboltBootdell_modifier000077700000000000000000000000001501337203100437372../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThunderboltBootdell_value_modifier000077700000000000000000000000001501337203100451332../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThunderboltBootdisplay_name000066400000000000000000000000411501337203100363160ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThunderboltBootEnable Thunderbolt" Boot Support display_name_language_code000077700000000000000000000000001501337203100477532../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThunderboltBootpossible_values000077700000000000000000000000001501337203100435452../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThunderboltBoottype000077700000000000000000000000001501337203100371112../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThunderboltBootThunderboltPreboot/000077500000000000000000000000001501337203100344425ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100452602../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThunderboltPrebootdefault_value000077700000000000000000000000001501337203100452222../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThunderboltPrebootdell_modifier000077700000000000000000000000001501337203100444462../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThunderboltPrebootdell_value_modifier000077700000000000000000000000001501337203100456422../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThunderboltPrebootdisplay_name000066400000000000000000000000731501337203100370320ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThunderboltPrebootEnable Thunderbolt" (and PCIe behind TBT) pre-boot modules display_name_language_code000077700000000000000000000000001501337203100504622../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThunderboltPrebootpossible_values000077700000000000000000000000001501337203100442542../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThunderboltPreboottype000077700000000000000000000000001501337203100376202../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/ThunderboltPrebootfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Touchscreen/000077500000000000000000000000001501337203100331565ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100420472../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Touchscreendefault_value000077700000000000000000000000001501337203100420112../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Touchscreendell_modifier000077700000000000000000000000001501337203100431032../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Touchscreendell_value_modifier000077700000000000000000000000001501337203100442772../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Touchscreendisplay_name000066400000000000000000000000141501337203100354620ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TouchscreenTouchscreen display_name_language_code000077700000000000000000000000001501337203100471172../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Touchscreenpossible_values000077700000000000000000000000001501337203100427112../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Touchscreentype000077700000000000000000000000001501337203100362552../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TouchscreenTpmActivation/000077500000000000000000000000001501337203100333775ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100423472../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmActivationdefault_value000077700000000000000000000000001501337203100423112../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmActivationdell_modifier000066400000000000000000000000421501337203100361140ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmActivation[SuppressIf:TpmSecurity=Disabled] dell_value_modifier000077700000000000000000000000001501337203100445772../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmActivationdisplay_name000066400000000000000000000000121501337203100357600ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmActivationTPM State display_name_language_code000077700000000000000000000000001501337203100474172../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmActivationpossible_values000077700000000000000000000000001501337203100432112../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmActivationtype000077700000000000000000000000001501337203100365552../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmActivationTpmPpiClearOverride/000077500000000000000000000000001501337203100344755ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100453132../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiClearOverridedefault_value000077700000000000000000000000001501337203100452552../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiClearOverridedell_modifier000077700000000000000000000000001501337203100447272../TpmActivation/dell_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiClearOverridedell_value_modifier000077700000000000000000000000001501337203100456752../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiClearOverridedisplay_name000066400000000000000000000000361501337203100370640ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiClearOverridePPI Bypass for Clear Commands display_name_language_code000077700000000000000000000000001501337203100505152../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiClearOverridepossible_values000077700000000000000000000000001501337203100443072../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiClearOverridetype000077700000000000000000000000001501337203100376532../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiClearOverridefwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiDpo/000077500000000000000000000000001501337203100325505ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100433072../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiDpodefault_value000077700000000000000000000000001501337203100432512../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiDpodell_modifier000077700000000000000000000000001501337203100427232../TpmActivation/dell_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiDpodell_value_modifier000077700000000000000000000000001501337203100436712../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiDpodisplay_name000066400000000000000000000000401501337203100350530ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiDpoPPI Bypass for Disable Commands display_name_language_code000077700000000000000000000000001501337203100465112../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiDpopossible_values000077700000000000000000000000001501337203100423032../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiDpotype000077700000000000000000000000001501337203100356472../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiDpofwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiPo/000077500000000000000000000000001501337203100324045ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100412752../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiPodefault_value000077700000000000000000000000001501337203100431052../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiPodell_modifier000077700000000000000000000000001501337203100425572../TpmActivation/dell_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiPodell_value_modifier000077700000000000000000000000001501337203100435252../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiPodisplay_name000066400000000000000000000000371501337203100347150ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiPoPPI Bypass for Enable Commands display_name_language_code000077700000000000000000000000001501337203100463452../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiPopossible_values000077700000000000000000000000001501337203100421372../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiPotype000077700000000000000000000000001501337203100355032../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmPpiPofwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmSecurity/000077500000000000000000000000001501337203100331645ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100420552../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmSecuritydefault_value000077700000000000000000000000001501337203100420172../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmSecuritydell_modifier000077700000000000000000000000001501337203100431112../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmSecuritydell_value_modifier000077700000000000000000000000001501337203100443052../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmSecuritydisplay_name000066400000000000000000000000241501337203100354710ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmSecurityTPM 2.0 Security On display_name_language_code000077700000000000000000000000001501337203100471252../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmSecuritypossible_values000077700000000000000000000000001501337203100427172../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmSecuritytype000077700000000000000000000000001501337203100362632../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TpmSecurityTrustExecution/000077500000000000000000000000001501337203100336225ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100444402../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TrustExecutiondefault_value000077700000000000000000000000001501337203100444022../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TrustExecutiondell_modifier000077700000000000000000000000001501337203100436262../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TrustExecutiondell_value_modifier000066400000000000000000000002061501337203100375350ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TrustExecutionDisabled[ForceIf:TpmSecurity=Disabled][ForceIf:Virtualization=Disabled][ForceIf:VtForDirectIo=Disabled][ForceIfNot:CpuCore=CoresAll]; display_name000066400000000000000000000000601501337203100362060ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TrustExecutionEnable Intel Trusted Execution Technology (TXT) display_name_language_code000077700000000000000000000000001501337203100476422../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TrustExecutionpossible_values000077700000000000000000000000001501337203100434342../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TrustExecutiontype000077700000000000000000000000001501337203100370002../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TrustExecutionfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TurboMode/000077500000000000000000000000001501337203100325745ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100414652../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TurboModedefault_value000077700000000000000000000000001501337203100414272../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TurboModedell_modifier000077700000000000000000000000001501337203100425212../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TurboModedell_value_modifier000077700000000000000000000000001501337203100437152../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TurboModedisplay_name000066400000000000000000000000441501337203100351030ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TurboModeEnable Intel Turbo Boost Technology display_name_language_code000077700000000000000000000000001501337203100465352../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TurboModepossible_values000077700000000000000000000000001501337203100423272../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TurboModetype000077700000000000000000000000001501337203100356732../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/TurboModeUefiBootPathSecurity/000077500000000000000000000000001501337203100346765ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000066400000000000000000000000301501337203100374700ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UefiBootPathSecurityAlwaysExceptInternalHdd default_value000077700000000000000000000000001501337203100423722./current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UefiBootPathSecuritydell_modifier000077700000000000000000000000001501337203100447022../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UefiBootPathSecuritydell_value_modifier000077700000000000000000000000001501337203100460762../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UefiBootPathSecuritydisplay_name000066400000000000000000000000301501337203100372570ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UefiBootPathSecurityUEFI Boot Path Security display_name_language_code000077700000000000000000000000001501337203100507162../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UefiBootPathSecuritypossible_values000066400000000000000000000001011501337203100400100ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UefiBootPathSecurityNever;Always;AlwaysExceptInternalHdd;AlwaysExceptInternalHddPxe; type000077700000000000000000000000001501337203100400542../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UefiBootPathSecurityfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UefiNwStack/000077500000000000000000000000001501337203100330575ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100417502../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UefiNwStackdefault_value000077700000000000000000000000001501337203100417122../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UefiNwStackdell_modifier000077700000000000000000000000001501337203100430042../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UefiNwStackdell_value_modifier000077700000000000000000000000001501337203100442002../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UefiNwStackdisplay_name000066400000000000000000000000321501337203100353630ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UefiNwStackEnable UEFI Network Stack display_name_language_code000077700000000000000000000000001501337203100470202../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UefiNwStackpossible_values000077700000000000000000000000001501337203100426122../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UefiNwStacktype000077700000000000000000000000001501337203100361562../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UefiNwStackfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UsbEmu/000077500000000000000000000000001501337203100320745ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100407652../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UsbEmudefault_value000077700000000000000000000000001501337203100407272../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UsbEmudell_modifier000077700000000000000000000000001501337203100420212../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UsbEmudell_value_modifier000077700000000000000000000000001501337203100432152../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UsbEmudisplay_name000066400000000000000000000000301501337203100343760ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UsbEmuEnable USB Boot Support display_name_language_code000077700000000000000000000000001501337203100460352../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UsbEmupossible_values000077700000000000000000000000001501337203100416272../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UsbEmufwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UsbEmu/type000077700000000000000000000000001501337203100352522../SdCard/typeustar00rootroot00000000000000UsbPortsExternal/000077500000000000000000000000001501337203100341015ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100430512../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UsbPortsExternaldefault_value000077700000000000000000000000001501337203100430132../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UsbPortsExternaldell_modifier000077700000000000000000000000001501337203100441052../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UsbPortsExternaldell_value_modifier000077700000000000000000000000001501337203100453012../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UsbPortsExternaldisplay_name000066400000000000000000000000321501337203100364640ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UsbPortsExternalEnable External USB Ports display_name_language_code000077700000000000000000000000001501337203100501212../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UsbPortsExternalpossible_values000077700000000000000000000000001501337203100437132../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UsbPortsExternaltype000077700000000000000000000000001501337203100372572../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/UsbPortsExternalVirtualization/000077500000000000000000000000001501337203100336415ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100426112../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Virtualizationdefault_value000077700000000000000000000000001501337203100425532../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Virtualizationdell_modifier000077700000000000000000000000001501337203100436452../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Virtualizationdell_value_modifier000077700000000000000000000000001501337203100450412../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Virtualizationdisplay_name000066400000000000000000000000541501337203100362300ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/VirtualizationEnable Intel Virtualization Technology (VT) display_name_language_code000077700000000000000000000000001501337203100476612../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Virtualizationpossible_values000077700000000000000000000000001501337203100434532../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Virtualizationtype000077700000000000000000000000001501337203100370172../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/VirtualizationVtForDirectIo/000077500000000000000000000000001501337203100333005ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100422502../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/VtForDirectIodefault_value000077700000000000000000000000001501337203100422122../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/VtForDirectIodell_modifier000077700000000000000000000000001501337203100433042../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/VtForDirectIodell_value_modifier000077700000000000000000000000001501337203100445002../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/VtForDirectIodisplay_name000066400000000000000000000000371501337203100356700ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/VtForDirectIoEnable Intel VT for Direct I/O display_name_language_code000077700000000000000000000000001501337203100473202../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/VtForDirectIopossible_values000077700000000000000000000000001501337203100431122../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/VtForDirectIotype000077700000000000000000000000001501337203100364562../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/VtForDirectIofwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnAc/000077500000000000000000000000001501337203100323245ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100430632../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnAcdefault_value000077700000000000000000000000001501337203100430252../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnAcdell_modifier000077700000000000000000000000001501337203100422512../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnAcdell_value_modifier000077700000000000000000000000001501337203100434452../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnAcdisplay_name000066400000000000000000000000131501337203100346270ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnAcWake on AC display_name_language_code000077700000000000000000000000001501337203100462652../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnAcpossible_values000077700000000000000000000000001501337203100420572../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnActype000077700000000000000000000000001501337203100354232../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnAcfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnDock/000077500000000000000000000000001501337203100326615ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100434202../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnDockdefault_value000077700000000000000000000000001501337203100415142../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnDockdell_modifier000077700000000000000000000000001501337203100426062../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnDockdell_value_modifier000077700000000000000000000000001501337203100440022../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnDockdisplay_name000066400000000000000000000000301501337203100351630ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnDockWake on Dell USB-C Dock display_name_language_code000077700000000000000000000000001501337203100466222../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnDockpossible_values000077700000000000000000000000001501337203100424142../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnDocktype000077700000000000000000000000001501337203100357602../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnDockfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnLan/000077500000000000000000000000001501337203100325135ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100432522../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnLandefault_value000077700000000000000000000000001501337203100432142../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnLandell_modifier000077700000000000000000000000001501337203100424402../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnLandell_value_modifier000077700000000000000000000000001501337203100436342../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnLandisplay_name000066400000000000000000000000141501337203100350170ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnLanWake on LAN display_name_language_code000077700000000000000000000000001501337203100464542../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnLanpossible_values000066400000000000000000000000411501337203100355510ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnLanDisabled;LanOnly;LanWithPxeBoot; type000077700000000000000000000000001501337203100356122../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WakeOnLanWarningsAndErr/000077500000000000000000000000001501337203100335015ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000066400000000000000000000000151501337203100362760ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WarningsAndErrPromptWrnErr default_value000077700000000000000000000000001501337203100411752./current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WarningsAndErrdell_modifier000077700000000000000000000000001501337203100435052../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WarningsAndErrdell_value_modifier000077700000000000000000000000001501337203100447012../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WarningsAndErrdisplay_name000066400000000000000000000000241501337203100360650ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WarningsAndErrWarnings and Errors display_name_language_code000077700000000000000000000000001501337203100475212../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WarningsAndErrpossible_values000066400000000000000000000000411501337203100366160ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WarningsAndErrPromptWrnErr;ContWrn;ContWrnErr; type000077700000000000000000000000001501337203100366572../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WarningsAndErrfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WirelessLan/000077500000000000000000000000001501337203100331245ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100420152../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WirelessLandefault_value000077700000000000000000000000001501337203100417572../SdCard/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WirelessLandell_modifier000077700000000000000000000000001501337203100430512../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WirelessLandell_value_modifier000077700000000000000000000000001501337203100442452../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WirelessLandisplay_name000066400000000000000000000000051501337203100354300ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WirelessLanWLAN display_name_language_code000077700000000000000000000000001501337203100470652../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WirelessLanpossible_values000077700000000000000000000000001501337203100426572../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WirelessLantype000077700000000000000000000000001501337203100362232../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WirelessLanWlanAutoSense/000077500000000000000000000000001501337203100333455ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributescurrent_value000077700000000000000000000000001501337203100441632../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WlanAutoSensedefault_value000077700000000000000000000000001501337203100441252../PwdLowerCaseRqd/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WlanAutoSensedell_modifier000077700000000000000000000000001501337203100433512../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WlanAutoSensedell_value_modifier000077700000000000000000000000001501337203100445452../SdCard/dell_value_modifierustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WlanAutoSensedisplay_name000066400000000000000000000000231501337203100357300ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WlanAutoSenseControl WLAN radio display_name_language_code000077700000000000000000000000001501337203100473652../SdCard/display_name_language_codeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WlanAutoSensepossible_values000077700000000000000000000000001501337203100431572../SdCard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WlanAutoSensetype000077700000000000000000000000001501337203100365232../SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WlanAutoSensepending_reboot000077700000000000000000000000001501337203100371322AutoOnHr/min_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributesfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/reset_bios000066400000000000000000000000521501337203100327520ustar00rootroot00000000000000builtinsafe lastknowngood factory custom fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/dell-xps13-9310/strings.txt000066400000000000000000000154111501337203100257200ustar00rootroot00000000000000#TRANSLATORS: Description of BIOS setting Service Tag #TRANSLATORS: Description of BIOS setting Keyboard Illumination #TRANSLATORS: Description of BIOS setting Intel Speed Shift Technology #TRANSLATORS: Description of BIOS setting BIOS Recovery from Hard Drive #TRANSLATORS: Description of BIOS setting Enable Thunderbolt (and PCIe behind TBT) pre-boot modules #TRANSLATORS: Description of BIOS setting BIOSConnect #TRANSLATORS: Description of BIOS setting Control WLAN radio #TRANSLATORS: Description of BIOS setting FOTA #TRANSLATORS: Description of BIOS setting TPM State #TRANSLATORS: Description of BIOS setting Battery Threshold [15% to 100%] #TRANSLATORS: Description of BIOS setting Special Character #TRANSLATORS: Description of BIOS setting Clear Thermal Event Log #TRANSLATORS: Description of BIOS setting Enable Numlock #TRANSLATORS: Description of BIOS setting Sunday #TRANSLATORS: Description of BIOS setting Enable Advanced Battery Charge Configuration #TRANSLATORS: Description of BIOS setting Secure Boot Mode #TRANSLATORS: Description of BIOS setting Enable External USB Ports #TRANSLATORS: Description of BIOS setting Master Password Lockout #TRANSLATORS: Description of BIOS setting Enable Non-Admin Password Changes #TRANSLATORS: Description of BIOS setting M.2 PCIe SSD #TRANSLATORS: Description of BIOS setting Battery Configuration #TRANSLATORS: Description of BIOS setting Enable Intel Hyper-Threading Technology #TRANSLATORS: Description of BIOS setting Lower Case Letter #TRANSLATORS: Description of BIOS setting Enable Intel Virtualization Technology (VT) #TRANSLATORS: Description of BIOS setting Thermal Management #TRANSLATORS: Description of BIOS setting DellCoreService #TRANSLATORS: Description of BIOS setting Keyboard Backlight Timeout on AC #TRANSLATORS: Description of BIOS setting Mouse/Touchpad #TRANSLATORS: Description of BIOS setting Enable Microphone #TRANSLATORS: Description of BIOS setting Enable UEFI Network Stack #TRANSLATORS: Description of BIOS setting SMM Security Mitigation #TRANSLATORS: Description of BIOS setting Enable USB Boot Support #TRANSLATORS: Description of BIOS setting Allow BIOS Downgrade #TRANSLATORS: Description of BIOS setting Lock Mode #TRANSLATORS: Description of BIOS setting Clear Bios Event Log #TRANSLATORS: Description of BIOS setting Wake on Dell USB-C Dock #TRANSLATORS: Description of BIOS setting Enable Secure Digital (SD) Card Boot #TRANSLATORS: Description of BIOS setting Enable Adapter Warnings #TRANSLATORS: Description of BIOS setting Saturday #TRANSLATORS: Description of BIOS setting Custom Charge Start #TRANSLATORS: Description of BIOS setting Enable Admin Setup Lockout #TRANSLATORS: Description of BIOS setting Wake on LAN #TRANSLATORS: Description of BIOS setting Fastboot #TRANSLATORS: Description of BIOS setting Enable SMART Reporting #TRANSLATORS: Description of BIOS setting Early Keyboard Backlight #TRANSLATORS: Description of BIOS setting Intel@ GNA Accelerator #TRANSLATORS: Description of BIOS setting Power On Lid Open #TRANSLATORS: Description of BIOS setting Touchscreen #TRANSLATORS: Description of BIOS setting Auto On Time #TRANSLATORS: Description of BIOS setting Asset Tag #TRANSLATORS: Description of BIOS setting Block Sleep #TRANSLATORS: Description of BIOS setting Thursday #TRANSLATORS: Description of BIOS setting MAC Address Pass-Through #TRANSLATORS: Description of BIOS setting Enable Intel SpeedStep Technology #TRANSLATORS: Description of BIOS setting Enable Intel Turbo Boost Technology #TRANSLATORS: Description of BIOS setting Enable Peak Shift #TRANSLATORS: Description of BIOS setting Wake on AC #TRANSLATORS: Description of BIOS setting Bluetooth #TRANSLATORS: Description of BIOS setting PPI Bypass for Clear Commands #TRANSLATORS: Description of BIOS setting Absolute #TRANSLATORS: Description of BIOS setting Clear POWER Event Log #TRANSLATORS: Description of BIOS setting SATA/NVMe Opeartion #TRANSLATORS: Description of BIOS setting Active Cores #TRANSLATORS: Description of BIOS setting UEFI Boot Path Security #TRANSLATORS: Description of BIOS setting Dell Auto OS Recovery Threshold #TRANSLATORS: Description of BIOS setting SHA-256 #TRANSLATORS: Description of BIOS setting PPI Bypass for Enable Commands #TRANSLATORS: Description of BIOS setting Friday #TRANSLATORS: Description of BIOS setting Enable Internal Speaker #TRANSLATORS: Description of BIOS setting Minimum Characters #TRANSLATORS: Description of BIOS setting Enable Intel VT for Direct I/O #TRANSLATORS: Description of BIOS setting Enable C-State Control #TRANSLATORS: Description of BIOS setting TPM 2.0 Security On #TRANSLATORS: Description of BIOS setting Enable Lid Switch #TRANSLATORS: Description of BIOS setting Enable Audio #TRANSLATORS: Description of BIOS setting Enable Thunderbolt Boot Support #TRANSLATORS: Description of BIOS setting Enable Fingerprint Reader Single Sign On #TRANSLATORS: Description of BIOS setting PPI Bypass for Disable Commands #TRANSLATORS: Description of BIOS setting Keyboard Backlight Timeout on Battery #TRANSLATORS: Description of BIOS setting Telemetry Access Level #TRANSLATORS: Description of BIOS setting Tuesday #TRANSLATORS: Description of BIOS setting Warnings and Errors #TRANSLATORS: Description of BIOS setting BIOS Setup Advanced Mode #TRANSLATORS: Description of BIOS setting Digit #TRANSLATORS: Description of BIOS setting Full Screen Logo #TRANSLATORS: Description of BIOS setting Upper Case Letter #TRANSLATORS: Description of BIOS setting Fn Lock Options #TRANSLATORS: Description of BIOS setting Enable Fingerprint Reader Device #TRANSLATORS: Description of BIOS setting SupportAssist OS Recovery #TRANSLATORS: Description of BIOS setting Enable UEFI Capsule Firmware Updates #TRANSLATORS: Description of BIOS setting Minutes (MM) #TRANSLATORS: Description of BIOS setting Hours (HH) #TRANSLATORS: Description of BIOS setting Enable Dock Warning Messages #TRANSLATORS: Description of BIOS setting Enable Strong Passwords #TRANSLATORS: Description of BIOS setting Enable Secure Boot #TRANSLATORS: Description of BIOS setting Extend BIOS POST Time #TRANSLATORS: Description of BIOS setting Password Bypass #TRANSLATORS: Description of BIOS setting Wednesday #TRANSLATORS: Description of BIOS setting WLAN #TRANSLATORS: Description of BIOS setting Secure Digital (SD) Card Read-Only Mode #TRANSLATORS: Description of BIOS setting Monday #TRANSLATORS: Description of BIOS setting Enable Camera #TRANSLATORS: Description of BIOS setting Secure Digital (SD) Card #TRANSLATORS: Description of BIOS setting Enable Dynamic Tuning:Machine Learning #TRANSLATORS: Description of BIOS setting Early Logo Display #TRANSLATORS: Description of BIOS setting Enable Intel Trusted Execution Technology (TXT) #TRANSLATORS: Description of BIOS setting Custom Charge Stop fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p14s-gen1/000077500000000000000000000000001501337203100241155ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p14s-gen1/thinklmi/000077500000000000000000000000001501337203100257345ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p14s-gen1/thinklmi/attributes/000077500000000000000000000000001501337203100301225ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p14s-gen1/thinklmi/attributes/SecureBoot/000077500000000000000000000000001501337203100321745ustar00rootroot00000000000000current_value000066400000000000000000000000071501337203100347130ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p14s-gen1/thinklmi/attributes/SecureBootEnable display_name000066400000000000000000000000131501337203100344770ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p14s-gen1/thinklmi/attributes/SecureBootSecureBoot possible_values000066400000000000000000000000171501337203100352350ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p14s-gen1/thinklmi/attributes/SecureBootDisable,Enable fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p14s-gen1/thinklmi/attributes/SleepState/000077500000000000000000000000001501337203100321735ustar00rootroot00000000000000current_value000066400000000000000000000000131501337203100347070ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p14s-gen1/thinklmi/attributes/SleepStateWindows 10 display_name000066400000000000000000000000131501337203100344760ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p14s-gen1/thinklmi/attributes/SleepStateSleepState possible_values000066400000000000000000000000211501337203100352270ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p14s-gen1/thinklmi/attributes/SleepStateLinux,Windows 10 fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p14s-gen1/thinklmi/attributes/pending_reboot000077700000000000000000000000001501337203100465232../../../dell-xps13-9310/dell-wmi-sysman/attributes/Asset/min_lengthustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620-6.3/000077500000000000000000000000001501337203100234715ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620-6.3/thinklmi/000077500000000000000000000000001501337203100253105ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620-6.3/thinklmi/attributes/000077500000000000000000000000001501337203100274765ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620-6.3/thinklmi/attributes/AMDMemoryGuard/000077500000000000000000000000001501337203100322535ustar00rootroot00000000000000current_value000066400000000000000000000000101501337203100347640ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620-6.3/thinklmi/attributes/AMDMemoryGuardDisable display_name000077700000000000000000000000001501337203100507742../../../../lenovo-p620/thinklmi/attributes/AMDMemoryGuard/display_nameustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620-6.3/thinklmi/attributes/AMDMemoryGuardpossible_values000066400000000000000000000000171501337203100353140ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620-6.3/thinklmi/attributes/AMDMemoryGuardDisable;Enable fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620-6.3/thinklmi/attributes/AMDMemoryGuard/type000077700000000000000000000000001501337203100507552../../../../dell-xps13-9310/dell-wmi-sysman/attributes/KeyboardIllumination/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620-6.3/thinklmi/attributes/AlarmDate/000077500000000000000000000000001501337203100313305ustar00rootroot00000000000000current_value000077700000000000000000000000001501337203100513672../../../../lenovo-p620/thinklmi/attributes/AlarmDate(MM\DD\YYYY)/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620-6.3/thinklmi/attributes/AlarmDatedisplay_name000066400000000000000000000000121501337203100336320ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620-6.3/thinklmi/attributes/AlarmDateAlarmDate fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620-6.3/thinklmi/attributes/AlarmDate/type000077700000000000000000000000001501337203100447642../../../../dell-xps13-9310/dell-wmi-sysman/attributes/Asset/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620-6.3/thinklmi/attributes/StartupSequence/000077500000000000000000000000001501337203100326315ustar00rootroot00000000000000current_value000066400000000000000000000000101501337203100353420ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620-6.3/thinklmi/attributes/StartupSequencePrimary display_name000077700000000000000000000000001501337203100517302../../../../lenovo-p620/thinklmi/attributes/StartupSequence/display_nameustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620-6.3/thinklmi/attributes/StartupSequencepossible_values000066400000000000000000000000221501337203100356660ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620-6.3/thinklmi/attributes/StartupSequencePrimary;Automatic type000077700000000000000000000000001501337203100512542../../../../dell-xps13-9310/dell-wmi-sysman/attributes/KeyboardIllumination/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620-6.3/thinklmi/attributes/StartupSequenceWindowsUEFIFirmwareUpdate/000077500000000000000000000000001501337203100343625ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620-6.3/thinklmi/attributescurrent_value000077700000000000000000000000001501337203100540312../../../../lenovo-p14s-gen1/thinklmi/attributes/SecureBoot/current_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620-6.3/thinklmi/attributes/WindowsUEFIFirmwareUpdatedisplay_name000077700000000000000000000000001501337203100553502../../../../lenovo-p620/thinklmi/attributes/WindowsUEFIFirmwareUpdate/display_nameustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620-6.3/thinklmi/attributes/WindowsUEFIFirmwareUpdatepossible_values000077700000000000000000000000001501337203100456112../AMDMemoryGuard/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620-6.3/thinklmi/attributes/WindowsUEFIFirmwareUpdatetype000077700000000000000000000000001501337203100500772../../../../dell-xps13-9310/dell-wmi-sysman/attributes/SdCard/typeustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620-6.3/thinklmi/attributes/WindowsUEFIFirmwareUpdatefwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620-6.3/thinklmi/attributes/pending_reboot000077700000000000000000000000001501337203100441702../../../lenovo-p620/thinklmi/attributes/pending_rebootustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620/000077500000000000000000000000001501337203100231655ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620/thinklmi/000077500000000000000000000000001501337203100250045ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620/thinklmi/attributes/000077500000000000000000000000001501337203100271725ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620/thinklmi/attributes/AMDMemoryGuard/000077500000000000000000000000001501337203100317475ustar00rootroot00000000000000current_value000066400000000000000000000000421501337203100344650ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620/thinklmi/attributes/AMDMemoryGuardDisable;[Optional:Disable,Enable] display_name000066400000000000000000000000171501337203100342560ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620/thinklmi/attributes/AMDMemoryGuardAMDMemoryGuard possible_values000077700000000000000000000000001501337203100440132../AlarmDate(MM\DD\YYYY)/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620/thinklmi/attributes/AMDMemoryGuardfwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620/thinklmi/attributes/AlarmDate(MM\DD\YYYY)/000077500000000000000000000000001501337203100326435ustar00rootroot00000000000000current_value000066400000000000000000000000361501337203100353640ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620/thinklmi/attributes/AlarmDate(MM\DD\YYYY)[01/01/2019][Status:ShowOnly] display_name000066400000000000000000000000261501337203100351520ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620/thinklmi/attributes/AlarmDate(MM\DD\YYYY)AlarmDate(MM\DD\YYYY) possible_values000066400000000000000000000000001501337203100356740ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620/thinklmi/attributes/AlarmDate(MM\DD\YYYY)fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620/thinklmi/attributes/StartupSequence/000077500000000000000000000000001501337203100323255ustar00rootroot00000000000000current_value000066400000000000000000000000661501337203100350510ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620/thinklmi/attributes/StartupSequencePrimary;[Optional:Primary,Automatic][Status:ShowOnly] display_name000066400000000000000000000000201501337203100346260ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620/thinklmi/attributes/StartupSequenceStartupSequence possible_values000077700000000000000000000000001501337203100443712../AlarmDate(MM\DD\YYYY)/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620/thinklmi/attributes/StartupSequenceWindowsUEFIFirmwareUpdate/000077500000000000000000000000001501337203100340565ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620/thinklmi/attributescurrent_value000066400000000000000000000000411501337203100366520ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620/thinklmi/attributes/WindowsUEFIFirmwareUpdateEnable;[Optional:Disable,Enable] display_name000066400000000000000000000000321501337203100364410ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620/thinklmi/attributes/WindowsUEFIFirmwareUpdateWindowsUEFIFirmwareUpdate possible_values000077700000000000000000000000001501337203100462012../AlarmDate(MM\DD\YYYY)/possible_valuesustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620/thinklmi/attributes/WindowsUEFIFirmwareUpdatefwupd-2.0.10/libfwupdplugin/tests/bios-attrs/lenovo-p620/thinklmi/attributes/pending_reboot000077700000000000000000000000001501337203100460462../../../dell-xps13-9310/dell-wmi-sysman/attributes/AutoOnHr/min_valueustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/cab-compressed.builder.xml000066400000000000000000000004351501337203100241470ustar00rootroot00000000000000 true hello.txt aGVsbG8gd29ybGQ= goodbye.txt aGVsbG8gd29ybGQ= fwupd-2.0.10/libfwupdplugin/tests/cab.builder.xml000066400000000000000000000004361501337203100220060ustar00rootroot00000000000000 false hello.txt aGVsbG8gd29ybGQ= goodbye.txt aGVsbG8gd29ybGQ= fwupd-2.0.10/libfwupdplugin/tests/cfu-offer.builder.xml000066400000000000000000000006731501337203100231400ustar00rootroot00000000000000 0x42 true true 0xAB 0xCD 0x4567 0xFACE 0xF 0x1 0x2 0xDEAD fwupd-2.0.10/libfwupdplugin/tests/cfu-payload.builder.xml000066400000000000000000000003671501337203100234700ustar00rootroot00000000000000 aGVsbG8gd29ybGQ= 0x8001234 aGVsbG8gd29ybGQ= 0x8005678 fwupd-2.0.10/libfwupdplugin/tests/chassis_type000066400000000000000000000000031501337203100215210ustar00rootroot0000000000000016 fwupd-2.0.10/libfwupdplugin/tests/colorhug/000077500000000000000000000000001501337203100207315ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/colorhug/firmware.metainfo.xml000066400000000000000000000024311501337203100250700ustar00rootroot00000000000000 com.hughski.ColorHugALS.firmware ColorHugALS Firmware Firmware for the ColorHugALS Ambient Light Sensor

    Updating the firmware on your ColorHugALS device improves performance and adds new features.

    84f40464-9272-4ef7-9399-cd95f12da696 12345678-1234-1234-1234-123456789012 http://www.hughski.com/ CC0-1.0 GPL-2.0+ richard_at_hughsie.com Hughski Limited

    This stable release fixes the following bugs:

    • Fix the return code from GetHardwareVersion
    • Scale the output of TakeReadingRaw by the datasheet values
    fwupd-2.0.10/libfwupdplugin/tests/colorhug/firmware.txt000066400000000000000000000000141501337203100233010ustar00rootroot00000000000000hello world fwupd-2.0.10/libfwupdplugin/tests/colorhug/firmware.txt.asc000066400000000000000000000000301501337203100240440ustar00rootroot00000000000000hello world, but signed fwupd-2.0.10/libfwupdplugin/tests/colorhug/meson.build000066400000000000000000000005111501337203100230700ustar00rootroot00000000000000colorhug_test_firmware = custom_target('colorhug-test-firmware', input: [ 'firmware.txt', 'firmware.metainfo.xml', ], output: 'colorhug-als-3.0.2.cab', command: [ gcab, '--create', '--nopath', '@OUTPUT@', '@INPUT@', ], install: true, install_dir: join_paths(installed_test_datadir, 'tests/colorhug'), ) fwupd-2.0.10/libfwupdplugin/tests/coswid.builder.xml000066400000000000000000000013511501337203100225460ustar00rootroot00000000000000 fwupd-efi:fwupdx64 1.4 semver fwupdx64 EFI helpers to install system firmware 1.3-3-g1d0c69f license https://spdx.org/licenses/LGPL-2.0.html foo.bin 123 sha256 dc626520dcd53a22f727af3ee42c770e56c97a64fe3adb063799d8ab032fe551 Richard Hughes hughsie.com maintainer tag-creator fwupd-2.0.10/libfwupdplugin/tests/cpuinfo000066400000000000000000000001351501337203100204740ustar00rootroot00000000000000processor : 0 vendor_id : AuthenticAMD fpu_exception : yes processor : 1 fpu_exception : no fwupd-2.0.10/libfwupdplugin/tests/csv.builder.xml000066400000000000000000000006011501337203100220460ustar00rootroot00000000000000 sbat 1 grub 1 fwupd-2.0.10/libfwupdplugin/tests/dfu.builder.xml000066400000000000000000000003301501337203100220300ustar00rootroot00000000000000 WFhYWFhYWFhYWFhY 0x1234 0x4321 0xDEAD 0x100 fwupd-2.0.10/libfwupdplugin/tests/dfuse.builder.xml000066400000000000000000000012151501337203100223630ustar00rootroot00000000000000 0x1234 0x5678 0x8642 0x1 one aGVsbG8gd29ybGQ= 0x8001234 aGVsbG8gd29ybGQ= 0x8005678 two 0x7 aGVsbG8gd29ybGQ= 0x8000000 fwupd-2.0.10/libfwupdplugin/tests/edid.builder.xml000066400000000000000000000002531501337203100221630ustar00rootroot00000000000000 0x1234 0x87654321 HUG Hughes fwupd-2.0.10/libfwupdplugin/tests/efi-file.builder.xml000066400000000000000000000002771501337203100227440ustar00rootroot00000000000000 ced4eac6-49f3-4c12-a597-fc8c33447691 0x0B aGVsbG8gd29ybGQ= fwupd-2.0.10/libfwupdplugin/tests/efi-filesystem.builder.xml000066400000000000000000000007421501337203100242060ustar00rootroot00000000000000 8c8ce578-8a3d-4f1c-9935-896185c32dd3 0x3 ced4eac6-49f3-4c12-a597-fc8c33447691 aGVsbG8gd29ybGQ= 0x3 ced4eac6-49f3-4c12-a597-fc8c33447691 aGVsbG8gd29ybGQ= fwupd-2.0.10/libfwupdplugin/tests/efi-load-option-data.builder.xml000066400000000000000000000013571501337203100251610ustar00rootroot00000000000000 Fedora 0x1 data AABCTwo= 0x4 0x1 0x800 0x12c000 9ed1b2b2-5c29-4b62-a329-84d462b47ce7 guid-partition-table guid 0x4 /EFI/fedora/shimx64.efi fwupd-2.0.10/libfwupdplugin/tests/efi-load-option-hive.builder.xml000066400000000000000000000006531501337203100252010ustar00rootroot00000000000000 Linux-Firmware-Updater 0x1 hive /foo/bar/baz acpi=off 0x04 0x04 Hello World fwupd-2.0.10/libfwupdplugin/tests/efi-load-option.builder.xml000066400000000000000000000014451501337203100242500ustar00rootroot00000000000000 Linux-Firmware-Updater 0x1 aGVsbG8gd29ybGQ= 0x04 0x01 0x1 0x800 0x12c000 c7e198db-ec30-4a23-b0fd-8e2dbc092a01 guid-partition-table guid 0x04 0x04 Hello World fwupd-2.0.10/libfwupdplugin/tests/efi-section.builder.xml000066400000000000000000000002201501337203100234550ustar00rootroot00000000000000 0x02 ced4eac6-49f3-4c12-a597-fc8c33447691 aGVsbG8gd29ybGQ= fwupd-2.0.10/libfwupdplugin/tests/efi-signature-list.builder.xml000066400000000000000000000007521501337203100247750ustar00rootroot00000000000000 sha256 77fa9abd-0359-4d32-bd60-28f4e78f784b 418ad44c79e3fddd6a0574b24fcf0fb8fee4b3ff2be635d21a5c0852bdea635c sha256 77fa9abd-0359-4d32-bd60-28f4e78f784b 819ebd0aeb8f0b73d237a02d9344ad1fd6fae6ad763cacf1694a6d13c1986cde fwupd-2.0.10/libfwupdplugin/tests/efi-signature.builder.xml000066400000000000000000000003221501337203100240150ustar00rootroot00000000000000 sha256 77fa9abd-0359-4d32-bd60-28f4e78f784b 418ad44c79e3fddd6a0574b24fcf0fb8fee4b3ff2be635d21a5c0852bdea635c fwupd-2.0.10/libfwupdplugin/tests/efi-variable-authentication2.builder.xml000066400000000000000000000007641501337203100267120ustar00rootroot00000000000000 sha256 77fa9abd-0359-4d32-bd60-28f4e78f784b 418ad44c79e3fddd6a0574b24fcf0fb8fee4b3ff2be635d21a5c0852bdea635c sha256 77fa9abd-0359-4d32-bd60-28f4e78f784b 819ebd0aeb8f0b73d237a02d9344ad1fd6fae6ad763cacf1694a6d13c1986cde fwupd-2.0.10/libfwupdplugin/tests/efi-volume.builder.xml000066400000000000000000000002301501337203100233210ustar00rootroot00000000000000 0x3 fff12b8d-7696-4c8b-a985-2747075b4f50 aGVsbG8gd29ybGQ= fwupd-2.0.10/libfwupdplugin/tests/efi/000077500000000000000000000000001501337203100176525ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/efi/efivars/000077500000000000000000000000001501337203100213115ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/efi/efivars/Boot0000-8be4df61-93ca-11d2-aa0d-00e098032b8c000066400000000000000000000002661501337203100273360ustar00rootroot00000000000000bLinux-Firmware-Updater*ÀÛ˜áÇ0ì#J°ýŽ-¼ *4\EFI\fedora\shimx64.efiÿ\fwupdx64.efifwupd-2.0.10/libfwupdplugin/tests/efi/efivars/Boot0001-8be4df61-93ca-11d2-aa0d-00e098032b8c000066400000000000000000000001721501337203100273330ustar00rootroot00000000000000bFedora*ÀÛ˜áÇ0ì#J°ýŽ-¼ *4\EFI\fedora\shimx64.efiÿfwupd-2.0.10/libfwupdplugin/tests/efi/efivars/Boot0002-8be4df61-93ca-11d2-aa0d-00e098032b8c000066400000000000000000000004601501337203100273340ustar00rootroot00000000000000tWindows Boot Manager* Êó"ȼÏaJ´`/=[á…F\EFI\Microsoft\Boot\bootmgfw.efiÿWINDOWSˆxBCDOBJECT={9dea862c-5cdd-4e70-acc1-f32b344d4795}oÿfwupd-2.0.10/libfwupdplugin/tests/efi/efivars/BootNext-8be4df61-93ca-11d2-aa0d-00e098032b8c000066400000000000000000000000001501337203100276570ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/efi/efivars/BootOrder-8be4df61-93ca-11d2-aa0d-00e098032b8c000066400000000000000000000000121501337203100300170ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c000066400000000000000000000000011501337203100301700ustar00rootroot000000000000001fwupd-ddc0ee61-e7f0-4e7d-acc5-c070a398838e-0-0abba7dc-e516-4167-bbf5-4d9d1c739416000066400000000000000000000003461501337203100344460ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/efi/efivars Ù{iÏ©Mƒ…™i ¼eY*@Zª—ZÕ~I™ Ê5MÈm†\EFI\fedora\fw\fwupd-697bd920-12cf-4da9-8385-996909bc6559.capÿfwupd-2.0.10/libfwupdplugin/tests/efi/efivars/meson.build000066400000000000000000000010441501337203100234520ustar00rootroot00000000000000fs.copyfile('BootNext-8be4df61-93ca-11d2-aa0d-00e098032b8c', install: true, install_dir: join_paths(installed_test_datadir, 'tests/efi/efivars')) fs.copyfile('fwupd-ddc0ee61-e7f0-4e7d-acc5-c070a398838e-0-0abba7dc-e516-4167-bbf5-4d9d1c739416', install: true, install_dir: join_paths(installed_test_datadir, 'tests/efi/efivars')) fs.copyfile('SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c', install: true, install_dir: join_paths(installed_test_datadir, 'tests/efi/efivars')) fwupd-2.0.10/libfwupdplugin/tests/efi/meson.build000066400000000000000000000000221501337203100220060ustar00rootroot00000000000000subdir('efivars') fwupd-2.0.10/libfwupdplugin/tests/elf.builder.xml000066400000000000000000000003171501337203100220250ustar00rootroot00000000000000 .text aGVsbG8gd29ybGQ= .rodata aGVsbG8gd29ybGQ= fwupd-2.0.10/libfwupdplugin/tests/fdt.builder.xml000066400000000000000000000015401501337203100220330ustar00rootroot00000000000000 0x11 0x5 hello world images firmware-1 0x123 hash-1 aGVsbG8gd29ybGQ configurations conf-1 conf-1 aGVsbG8gd29ybGQ fwupd-2.0.10/libfwupdplugin/tests/fit.builder.xml000066400000000000000000000030321501337203100220360ustar00rootroot00000000000000 0x11 FIT test FIT description 0x629d4abd images firmware-1 u-boot YWJj AAABAA== arm64 firmware none v1.2.4 hash-1 crc32 0x352441C2 configurations conf-1 conf-1 alice:bob:clara firmware-1 fwupd-2.0.10/libfwupdplugin/tests/fmap-offset.builder.xml000066400000000000000000000003341501337203100234650ustar00rootroot00000000000000 0x10 FMAP aGVsbG8gd29ybGQ= TEST V29ybGQh fwupd-2.0.10/libfwupdplugin/tests/fmap.builder.xml000066400000000000000000000003041501337203100221760ustar00rootroot00000000000000 FMAP aGVsbG8gd29ybGQ= TEST V29ybGQh fwupd-2.0.10/libfwupdplugin/tests/hid-descriptor.builder.xml000066400000000000000000000163521501337203100242050ustar00rootroot00000000000000 usage-page 0xff0b usage 0x101 collection 0x1 usage-page 0xff0b logical-minimum logical-maximum 0xff report-id 0xf4 report-size 0x8 report-count < 0x3c usage 0xc7 output 0x2 usage-page 0xff0b logical-minimum logical-maximum 0xff report-id 0xf5 report-size 0x8 report-count 0x10 usage 0xcc input 0x2 usage-page 0xff0b logical-minimum logical-maximum 0xff report-id 0xf2 report-size 0x8 report-count 0x10 usage 0xdc output 0x2 usage-page 0xff0b logical-minimum logical-maximum 0xff report-id 0xf3 report-size 0x8 report-count 0x10 usage 0xd8 input 0x2 usage-page 0xff0b logical-minimum logical-maximum 0xff report-id 0xf1 report-size 0x8 report-count < 0x3c usage 0xc8 feature 0x2 usage-page 0xff0b logical-minimum logical-maximum 0xff report-id 0xf9 report-size 0x8 report-count 0x18 usage 0xdd output 0x2 usage-page 0xff0b logical-minimum logical-maximum 0xff report-id 0xf8 report-size 0x8 report-count 0x14 usage 0xde input 0x2 usage-page 0xff0b logical-minimum logical-maximum 0xff report-id 0xf8 report-size 0x8 report-count 0x14 end-collection fwupd-2.0.10/libfwupdplugin/tests/hid-descriptor2.builder.xml000066400000000000000000000032471501337203100242660ustar00rootroot00000000000000 usage-page 0xff02 usage local 0x1 collection 0x1 usage-page 0xff02 report-size 0x8 report-count 0x14 report-id 0x9 usage local 0x1 feature 0x2 usage-page 0xff02 report-size 0x8 report-count 0x14 report-id 0x9 end-collection fwupd-2.0.10/libfwupdplugin/tests/hid-report-item.builder.xml000066400000000000000000000001261501337203100242660ustar00rootroot00000000000000 usage 0xDE fwupd-2.0.10/libfwupdplugin/tests/ifd-bios.builder.xml000066400000000000000000000006441501337203100227560ustar00rootroot00000000000000 bios 0x1 0x1000 fff12b8d-7696-4c8b-a985-2747075b4f50 0xB aGVsbG8gd29ybGQ= fff12b8d-7696-4c8b-a985-2747075b4f50 0xB aGVsbG8gd29ybGQ= fwupd-2.0.10/libfwupdplugin/tests/ifd-no-bios.builder.xml000066400000000000000000000010231501337203100233600ustar00rootroot00000000000000 0x40003 0x58100208 0x310330 0x325c00f5 0x42 gbe 0x3 0x3000 V29ybGQh me 0x2 0x2000 V29ybGQh fwupd-2.0.10/libfwupdplugin/tests/ifd.builder.xml000066400000000000000000000027351501337203100220270ustar00rootroot00000000000000 0x40003 0x58100208 0x310330 0x325c00f5 0x42 bios 0x1 0x1000 8c8ce578-8a3d-4f1c-9935-896185c32dd3 0xB 0x3 ced4eac6-49f3-4c12-a597-fc8c33447691 0x0B 0x02 ced4eac6-49f3-4c12-a597-fc8c33447691 aGVsbG8gd29ybGQ= aGVsbG8gd29ybGQ= ced4eac6-49f3-4c12-a597-fc8c33447691 aGVsbG8gd29ybGQ= fff12b8d-7696-4c8b-a985-2747075b4f50 0xB aGVsbG8gd29ybGQ= me 0x2 0x2000 V29ybGQh fwupd-2.0.10/libfwupdplugin/tests/ifwi-cpd.builder.xml000066400000000000000000000005231501337203100227600ustar00rootroot00000000000000 0x1234 0x1 0x2 one aGVsbG8gd29ybGQ= two aGVsbG8gd29ybGQ= fwupd-2.0.10/libfwupdplugin/tests/ifwi-fpt.builder.xml000066400000000000000000000004051501337203100230020ustar00rootroot00000000000000 0x4f464e49 aGVsbG8gd29ybGQ= 0x4d495746 aGVsbG8gd29ybGQ= fwupd-2.0.10/libfwupdplugin/tests/ihex-signed.builder.xml000066400000000000000000000002361501337203100234630ustar00rootroot00000000000000 aGVsbG8gd29ybGQ= signature ZGVhZGJlZWY= fwupd-2.0.10/libfwupdplugin/tests/ihex.builder.xml000066400000000000000000000004061501337203100222130ustar00rootroot00000000000000 TmVxdWUgcG9ycm8gcXVpc3F1YW0gZXN0IHF1aSBkb2xvcmVtIGlwc3VtIHF1aWEgZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyLCBhZGlwaXNjaSB2ZWxpdAo= signature ZGF2ZQ== fwupd-2.0.10/libfwupdplugin/tests/intel-thunderbolt.builder.xml000066400000000000000000000005021501337203100247160ustar00rootroot00000000000000 0x10 0xd4 0x15ef 0xb070 titan-ridge 0x3 false false 0x6000 fwupd-2.0.10/libfwupdplugin/tests/linear.builder.xml000066400000000000000000000004011501337203100225230ustar00rootroot00000000000000 FuDfuFirmware aGVsbG8gd29ybGQ= aGVsbG8gd29ybGQ= fwupd-2.0.10/libfwupdplugin/tests/localtime000077700000000000000000000000001501337203100240172America/New_Yorkustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/lockdown/000077500000000000000000000000001501337203100207275ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/lockdown/locked/000077500000000000000000000000001501337203100221705ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/lockdown/locked/lockdown000066400000000000000000000000411501337203100237260ustar00rootroot00000000000000none integrity [confidentiality] fwupd-2.0.10/libfwupdplugin/tests/lockdown/none/000077500000000000000000000000001501337203100216665ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/lockdown/none/lockdown000066400000000000000000000000411501337203100234240ustar00rootroot00000000000000[none] integrity confidentiality fwupd-2.0.10/libfwupdplugin/tests/meson.build000066400000000000000000000160001501337203100212460ustar00rootroot00000000000000subdir('colorhug') subdir('efi') installed_firmware_zip = custom_target('installed-firmware-zip', input: [ 'colorhug/firmware.txt', 'colorhug/firmware.txt.asc', ], output: 'firmware.zip', command: [ python3, '-m', 'zipfile', '-c', '@OUTPUT@', '@INPUT@', ], install: true, install_dir: join_paths(installed_test_datadir, 'tests'), ) install_data([ 'America/New_York', ], install_dir: join_paths(installed_test_datadir, 'tests/America'), ) install_data([ 'cpuinfo', 'cab.builder.xml', 'cab-compressed.builder.xml', 'cfu-offer.builder.xml', 'cfu-payload.builder.xml', 'chassis_type', 'csv.builder.xml', 'dfu.builder.xml', 'dfuse.builder.xml', 'edid.builder.xml', 'efi-filesystem.builder.xml', 'efi-file.builder.xml', 'efi-load-option.builder.xml', 'efi-load-option-data.builder.xml', 'efi-load-option-hive.builder.xml', 'efi-section.builder.xml', 'efi-signature.builder.xml', 'efi-signature-list.builder.xml', 'efi-variable-authentication2.builder.xml', 'efi-volume.builder.xml', 'elf.builder.xml', 'fdt.builder.xml', 'fit.builder.xml', 'fmap.builder.xml', 'fmap-offset.builder.xml', 'hid-descriptor.builder.xml', 'hid-descriptor2.builder.xml', 'hid-report-item.builder.xml', 'ifd.builder.xml', 'ifd-no-bios.builder.xml', 'ifd.builder.xml', 'ifwi-cpd.builder.xml', 'ifwi-fpt.builder.xml', 'ihex.builder.xml', 'ihex-signed.builder.xml', 'intel-thunderbolt.builder.xml', 'linear.builder.xml', 'metadata.xml', 'oprom.builder.xml', 'pefile.builder.xml', 'srec-addr32.builder.xml', 'sbatlevel.builder.xml', 'srec.builder.xml', 'sys_vendor', 'usb-bos-descriptor.builder.xml', 'uswid.builder.xml', 'uswid-compressed.builder.xml', ], install_dir: join_paths(installed_test_datadir, 'tests') ) install_data([ 'oui.txt', 'pci.ids', 'pnp.ids', 'usb.ids', 'uevent', ], install_dir: join_paths(installed_test_datadir, 'tests') ) install_data([ 'lockdown/locked/lockdown', ], install_dir: join_paths(installed_test_datadir, 'tests/lockdown/locked'), ) install_data([ 'lockdown/none/lockdown', ], install_dir: join_paths(installed_test_datadir, 'tests/lockdown/none'), ) install_data([ 'quirks.d/tests.quirk', ], install_dir: join_paths(installed_test_datadir, 'tests/quirks.d'), ) install_symlink('localtime', install_dir: join_paths(installed_test_datadir, 'tests'), pointing_to: join_paths('America', 'New_York'), ) install_data([ 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/pending_reboot', ], install_dir: join_paths(installed_test_datadir, 'tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes'), ) install_data([ 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WlanAutoSense/current_value', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WlanAutoSense/default_value', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WlanAutoSense/dell_modifier', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WlanAutoSense/dell_value_modifier', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WlanAutoSense/display_name', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WlanAutoSense/display_name_language_code', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WlanAutoSense/possible_values', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WlanAutoSense/type', ], install_dir: join_paths(installed_test_datadir, 'tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/WlanAutoSense'), ) install_data([ 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStop/current_value', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStop/default_value', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStop/dell_modifier', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStop/display_name', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStop/display_name_language_code', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStop/min_value', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStop/max_value', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStop/scalar_increment', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStop/type', ], install_dir: join_paths(installed_test_datadir, 'tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/CustomChargeStop'), ) install_data([ 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Asset/current_value', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Asset/default_value', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Asset/dell_modifier', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Asset/display_name', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Asset/display_name_language_code', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Asset/type', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Asset/min_length', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Asset/max_length', ], install_dir: join_paths(installed_test_datadir, 'tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/Asset'), ) install_data([ 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosRcvrFrmHdd/current_value', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosRcvrFrmHdd/default_value', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosRcvrFrmHdd/dell_modifier', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosRcvrFrmHdd/dell_value_modifier', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosRcvrFrmHdd/display_name', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosRcvrFrmHdd/display_name_language_code', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosRcvrFrmHdd/possible_values', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosRcvrFrmHdd/type', ], install_dir: join_paths(installed_test_datadir, 'tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/BiosRcvrFrmHdd'), ) install_data([ 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBoot/current_value', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBoot/default_value', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBoot/dell_modifier', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBoot/dell_value_modifier', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBoot/display_name', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBoot/display_name_language_code', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBoot/possible_values', 'bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBoot/type', ], install_dir: join_paths(installed_test_datadir, 'tests/bios-attrs/dell-xps13-9310/dell-wmi-sysman/attributes/SecureBoot'), ) fwupd-2.0.10/libfwupdplugin/tests/metadata.xml000077700000000000000000000000001501337203100262652../../src/tests/metadata.xmlustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/oprom.builder.xml000066400000000000000000000004571501337203100224200ustar00rootroot00000000000000 0x1 0x0 0x8086 0x1 aGVsbG8gd29ybGQ= cpd aGVsbG8gd29ybGQ= fwupd-2.0.10/libfwupdplugin/tests/oui.txt000066400000000000000000000012211501337203100204400ustar00rootroot00000000000000OUI/MA-L Organization company_id Organization Address 10-E9-92 (hex) INGRAM MICRO SERVICES 10E992 (base 16) INGRAM MICRO SERVICES 100 CHEMIN DE BAILLOT MONTAUBAN 82000 FR 80-86-F2 (hex) Intel Corporate 8086F2 (base 16) Intel Corporate Lot 8, Jalan Hi-Tech 2/3 Kulim Kedah 09000 MY B0-F2-F6 (hex) Samsung Electronics Co.,Ltd B0F2F6 (base 16) Samsung Electronics Co.,Ltd 129, Samsung-ro, Youngtongl-Gu Suwon Gyeonggi-Do 16677 KR fwupd-2.0.10/libfwupdplugin/tests/pci.ids000066400000000000000000000003241501337203100203620ustar00rootroot00000000000000# List of PCI ID's 0001 SafeNet (wrong ID) 8086 Intel Corporation 0007 82379AB # List of known device classes, subclasses and programming interfaces C 00 Unclassified device 00 Non-VGA unclassified device fwupd-2.0.10/libfwupdplugin/tests/pefile.builder.xml000066400000000000000000000023611501337203100225240ustar00rootroot00000000000000 .text aGVsbG8gd29ybGQ= .textXXXXXX aGVsbG8gd29ybGQ= .sbat false sbat 1 .sbatlevel previous false grub 1 latest false sbat 1 fwupd-2.0.10/libfwupdplugin/tests/pnp.ids000066400000000000000000000000661501337203100204070ustar00rootroot00000000000000AAA Avolites Ltd ICO Intel Corp ZZZ Boca Research Inc fwupd-2.0.10/libfwupdplugin/tests/quirks.d/000077500000000000000000000000001501337203100206475ustar00rootroot00000000000000fwupd-2.0.10/libfwupdplugin/tests/quirks.d/tests.quirk000066400000000000000000000010351501337203100230650ustar00rootroot00000000000000[USB\VID_0A5C&PID_6412] Flags = ignore-runtime [ACME Inc.=True] Name = awesome [CORP*] Name = town [USB\VID_0BDA&PID_1100] Flags = clever Name = Hub Children = FuDevice|USB\VID_0763&PID_2806&I2C_01 Flags = no-generic-guids [USB\VID_0763&PID_2806&I2C_01] Name = HDMI Flags = updatable,internal [CFI\FLASHID_3730] Name = A25Lxxx CfiDeviceCmdChipErase = 0xc7 CfiDeviceCmdSectorErase = 0x20 CfiDevicePageSize = 0x200 CfiDeviceSectorSize = 0x2000 CfiDeviceBlockSize = 0x8000 FirmwareSizeMax = 0x10000 [MEI] Plugin = one [MEI] Plugin = two fwupd-2.0.10/libfwupdplugin/tests/sbatlevel.builder.xml000066400000000000000000000012101501337203100232310ustar00rootroot00000000000000 previous false grub 1 latest false sbat 1 fwupd-2.0.10/libfwupdplugin/tests/srec-addr32.builder.xml000066400000000000000000000001661501337203100232720ustar00rootroot00000000000000 0x1000000 HDR aGVsbG8gd29ybGQ= fwupd-2.0.10/libfwupdplugin/tests/srec.builder.xml000066400000000000000000000001351501337203100222110ustar00rootroot00000000000000 HDR aGVsbG8gd29ybGQ= fwupd-2.0.10/libfwupdplugin/tests/sys_vendor000066400000000000000000000000121501337203100212160ustar00rootroot00000000000000FwupdTest fwupd-2.0.10/libfwupdplugin/tests/uevent000066400000000000000000000001051501337203100203340ustar00rootroot00000000000000DRIVER=snd_hda_codec_realtek MODALIAS=hdaudio:v10EC0298r00100103a01 fwupd-2.0.10/libfwupdplugin/tests/usb-bos-descriptor.builder.xml000066400000000000000000000003101501337203100247760ustar00rootroot00000000000000 bos payload WFhYWFhYWFhYWFhY fwupd-2.0.10/libfwupdplugin/tests/usb.ids000066400000000000000000000002751501337203100204050ustar00rootroot00000000000000# List of USB ID's 8086 Intel Corp. 0001 AnyPoint (TM) Home Network 1.6 Mbps Wireless Adapter # List of known device classes, subclasses and protocols C 00 (Defined at Interface level) fwupd-2.0.10/libfwupdplugin/tests/uswid-compressed.builder.xml000066400000000000000000000016211501337203100245530ustar00rootroot00000000000000 foo 1.2.3 0x3 zlib fwupd-efi:fwupdx64 1.4 semver fwupdx64 EFI helpers to install system firmware 1.3-3-g1d0c69f license https://spdx.org/licenses/LGPL-2.0.html foo.bin 123 sha256 dc626520dcd53a22f727af3ee42c770e56c97a64fe3adb063799d8ab032fe551 Richard Hughes hughsie.com tag-creator fwupd-2.0.10/libfwupdplugin/tests/uswid.builder.xml000066400000000000000000000016761501337203100224230ustar00rootroot00000000000000 foo 1.2.3 0x1 fwupd-efi:fwupdx64 1.4 semver fwupdx64 EFI helpers to install system firmware 1.3-3-g1d0c69f license https://spdx.org/licenses/LGPL-2.0.html foo.bin 123 sha256 dc626520dcd53a22f727af3ee42c770e56c97a64fe3adb063799d8ab032fe551 Richard Hughes hughsie.com tag-creator 456 fwupd-2.0.10/meson.build000066400000000000000000000552651501337203100150710ustar00rootroot00000000000000project('fwupd', 'c', version: '2.0.10', license: 'LGPL-2.1-or-later', meson_version: '>=1.3.0', default_options: ['warning_level=2', 'c_std=c17'], ) fwupd_version = meson.project_version() varr = fwupd_version.split('.') fwupd_major_version = varr[0] fwupd_minor_version = varr[1] fwupd_micro_version = varr[2] conf = configuration_data() conf.set('MAJOR_VERSION', fwupd_major_version) conf.set('MINOR_VERSION', fwupd_minor_version) conf.set('MICRO_VERSION', fwupd_micro_version) conf.set_quoted('PACKAGE_VERSION', fwupd_version) # get source version, falling back to package version source_version = fwupd_version git = find_program('git', required: false) tag = false if git.found() source_version = run_command([git, 'describe'], check: false).stdout().strip() if source_version == '' source_version = fwupd_version endif tag = run_command([git, 'describe', '--exact-match'], check: false).returncode() == 0 endif conf.set_quoted('SOURCE_VERSION', source_version) # libtool versioning - this applies to libfwupd # # See http://sources.redhat.com/autobook/autobook/autobook_91.html#SEC91 for details # # - If interfaces have been changed or added, but binary compatibility # has been preserved, change: # CURRENT += 1 # REVISION = 0 # AGE += 1 # - If binary compatibility has been broken (eg removed or changed # interfaces), change: # CURRENT += 1 # REVISION = 0 # AGE = 0 # - If the interface is the same as the previous version, but bugs are # fixed, change: # REVISION += 1 libfwupd_lt_current = '3' libfwupd_lt_revision = '0' libfwupd_lt_age = '0' libfwupd_lt_version = '@0@.@1@.@2@'.format(libfwupd_lt_current, libfwupd_lt_age, libfwupd_lt_revision) # get supported warning flags warning_flags = [ '-Wfatal-errors', '-Waggregate-return', '-Wunused', '-Warray-bounds', '-Wcast-align', '-Wclobbered', '-Wdeclaration-after-statement', '-Wdiscarded-qualifiers', '-Wduplicated-branches', '-Wduplicated-cond', '-Wempty-body', '-Wfloat-equal', '-Wformat=2', '-Wformat-nonliteral', '-Wformat-security', '-Wformat-signedness', '-Wignored-qualifiers', '-Wimplicit-function-declaration', '-Wimplicit-int', '-Wincompatible-pointer-types', '-Winit-self', '-Wint-conversion', '-Wlogical-op', '-Wmaybe-uninitialized', '-Wmissing-declarations', '-Wmissing-format-attribute', '-Wmissing-include-dirs', '-Wmissing-noreturn', '-Wmissing-parameter-type', '-Wmissing-prototypes', '-Wnested-externs', '-Wno-cast-function-type', '-Wno-address-of-packed-member', # incompatible with g_autoptr() '-Wno-unknown-pragmas', '-Wno-missing-field-initializers', '-Wno-strict-aliasing', '-Wno-suggest-attribute=format', '-Wno-typedef-redefinition', '-Wno-unknown-warning-option', '-Wno-unused-parameter', '-Wno-nonnull-compare', '-Wno-analyzer-use-of-uninitialized-value', # incompatible with g_autoptr() '-Wno-analyzer-fd-double-close', '-Wold-style-definition', '-Woverride-init', '-Wpointer-arith', '-Wredundant-decls', '-Wreturn-type', '-Wshadow', '-Wsign-compare', '-Wstrict-aliasing', '-Wstrict-prototypes', '-Wswitch-default', '-Wtype-limits', '-Wundef', '-Wuninitialized', '-Wunused-but-set-variable', '-Wunused-variable', '-Wvla', '-Wwrite-strings' ] static_analysis = get_option('static_analysis') and host_machine.system() != 'windows' if static_analysis warning_flags += ['-fanalyzer', '-Wno-analyzer-null-dereference'] endif cc = meson.get_compiler('c') add_project_arguments(cc.get_supported_arguments(warning_flags), language: 'c') if not meson.is_cross_build() add_project_arguments('-fstack-protector-strong', language: 'c') endif if cc.get_id() == 'msvc' error('MSVC is not supported as it does not support __attribute__((cleanup))') endif # ensure tests do not fail because of locale specific decimal separators (e.g. when comparing # outputs with `diff`) add_test_setup( 'default', env: { 'LANG': 'C.UTF-8', 'LC_ALL': 'C.UTF-8', }, is_default: true, ) # enable full RELRO where possible # FIXME: until https://github.com/mesonbuild/meson/issues/1140 is fixed global_link_args = [] test_link_args = [ '-Wl,-z,relro', '-Wl,-z,defs', '-Wl,-z,now', '-Wl,-z,ibt,-z,shstk', ] foreach arg: test_link_args if cc.has_link_argument(arg) global_link_args += arg endif endforeach add_project_link_arguments( global_link_args, language: 'c' ) add_project_arguments('-DFWUPD_COMPILATION', language: 'c') # Needed for realpath(), syscall(), cfmakeraw(), etc. add_project_arguments('-D_DEFAULT_SOURCE', language: 'c') # needed for symlink() and BYTE_ORDER add_project_arguments('-D_BSD_SOURCE', language: 'c') add_project_arguments('-D__BSD_VISIBLE', language: 'c') # needed for memfd_create() add_project_arguments('-D_GNU_SOURCE', language: 'c') # needed for memmem() add_project_arguments('-D_DARWIN_C_SOURCE=900000', language: 'c') # sanity check if get_option('build') == 'all' build_standalone = true build_daemon = true elif get_option('build') == 'standalone' build_standalone = true build_daemon = false elif get_option('build') == 'library' build_standalone = false build_daemon = false endif prefix = get_option('prefix') bindir = join_paths(prefix, get_option('bindir')) libdir = join_paths(prefix, get_option('libdir')) libexecdir = join_paths(prefix, get_option('libexecdir')) #this ends up in compiled code, ignore prefix if host_machine.system() == 'windows' sysconfdir = get_option('sysconfdir') localstatedir = get_option('localstatedir') datadir = get_option('datadir') installed_test_bindir = get_option('libexecdir') installed_test_datadir = get_option('datadir') daemon_dir = get_option('libexecdir') else datadir = join_paths(prefix, get_option('datadir')) sysconfdir = join_paths(prefix, get_option('sysconfdir')) localstatedir = join_paths(prefix, get_option('localstatedir')) installed_test_bindir = join_paths(libexecdir, 'installed-tests', meson.project_name()) installed_test_datadir = join_paths(datadir, 'installed-tests', meson.project_name()) daemon_dir = join_paths(libexecdir, 'fwupd') endif mandir = join_paths(prefix, get_option('mandir')) localedir = join_paths(prefix, get_option('localedir')) diffcmd = find_program('diff') gio = dependency('gio-2.0', version: '>= 2.72.0') giounix = dependency('gio-unix-2.0', version: '>= 2.72.0', required: false) if giounix.found() conf.set('HAVE_GIO_UNIX', '1') endif gmodule = dependency('gmodule-2.0') if host_machine.system() == 'linux' conf.set('HAVE_UDEV', '1') endif if build_standalone bluez = get_option('bluez').disable_auto_if(host_machine.system() != 'linux') if bluez.allowed() conf.set('HAVE_BLUEZ', '1') endif host_cpu = host_machine.cpu_family() hsi = get_option('hsi').disable_auto_if(host_machine.system() != 'linux').disable_auto_if(host_cpu != 'x86' and host_cpu != 'x86_64').allowed() if hsi conf.set('HAVE_HSI', '1') endif libxmlb = dependency('xmlb', version: '>= 0.3.19', fallback: ['libxmlb', 'libxmlb_dep']) if libxmlb.get_variable('zstd') == 'true' lvfs_metadata_format = 'zst' elif libxmlb.get_variable('lzma') == 'true' lvfs_metadata_format = 'xz' else lvfs_metadata_format = 'gz' endif conf.set_quoted('FU_LVFS_METADATA_FORMAT', lvfs_metadata_format) # FreeBSD is missing some libusb symbols libusb = dependency('libusb-1.0', version : '>= 0.1.27') conf.set_quoted('LIBUSB_VERSION', libusb.version()) if cc.has_header_symbol('libusb.h', 'libusb_set_option', dependencies: libusb) conf.set('HAVE_LIBUSB_SET_OPTION', '1') endif if cc.has_header_symbol('libusb.h', 'libusb_init_context', dependencies: libusb) conf.set('HAVE_LIBUSB_INIT_CONTEXT', '1') endif if cc.has_header_symbol('libusb.h', 'libusb_get_parent', dependencies: libusb) conf.set('HAVE_LIBUSB_GET_PARENT', '1') endif readline = dependency('readline', required: get_option('readline')) if readline.found() and get_option('readline').allowed() conf.set('HAVE_READLINE', '1') endif sqlite = dependency('sqlite3') if sqlite.found() conf.set('HAVE_SQLITE', '1') endif passim = dependency('passim', version: '>= 0.1.6', required: get_option('passim'), fallback: ['passim', 'passim_dep']) if passim.found() conf.set('HAVE_PASSIM', '1') endif libarchive = dependency('libarchive', required: get_option('libarchive')) if libarchive.found() conf.set('HAVE_LIBARCHIVE', '1') if cc.has_header_symbol('archive.h', 'archive_write_add_filter_zstd') conf.set('HAVE_LIBARCHIVE_WRITE_ADD_FILTER_ZSTD', '1') endif endif endif libjcat = dependency('jcat', version: '>= 0.2.0', fallback: ['libjcat', 'libjcat_dep']) libjsonglib = dependency('json-glib-1.0', version: '>= 1.6.0', fallback: ['libjsonglib', 'libjsonglib_dep']) libblkid = dependency('blkid', required: get_option('blkid')) if libblkid.found() conf.set('HAVE_BLKID', '1') endif valgrind = dependency('valgrind', required: get_option('valgrind')) libcurl = dependency('libcurl', version: '>= 7.62.0') libdrm = dependency('libdrm', required: get_option('libdrm')) if libdrm.found() conf.set('HAVE_LIBDRM' , '1') endif polkit = dependency('polkit-gobject-1', version: '>= 0.114', required: get_option('polkit').disable_auto_if(host_machine.system() != 'linux')) if polkit.found() conf.set('HAVE_POLKIT', '1') conf.set_quoted ('POLKIT_ACTIONDIR', polkit.get_variable(pkgconfig: 'actiondir')) endif if build_daemon if not polkit.found() warning('Polkit is disabled, the daemon will allow ALL client actions') endif endif libm = cc.find_library('m', required: false) zlib = dependency('zlib') fs = import('fs') # look for usb.ids in both of the Debian and Fedora locations, # and fall back to the system datadir in case we're building in a venv or prefix vendor_ids_dir = get_option('vendor_ids_dir') if vendor_ids_dir == '' vendor_ids_dir = join_paths(datadir, 'misc') if not fs.is_file(join_paths(vendor_ids_dir, 'usb.ids')) vendor_ids_dir = join_paths(datadir, 'hwdata') endif if not fs.is_file(join_paths(vendor_ids_dir, 'usb.ids')) vendor_ids_dir = '/usr/share/hwdata' endif if not fs.is_file(join_paths(vendor_ids_dir, 'usb.ids')) vendor_ids_dir = '/usr/share/misc' endif if not fs.is_file(join_paths(vendor_ids_dir, 'usb.ids')) vendor_ids_dir = '/usr/local/var/homebrew/linked/usb.ids/share/misc' endif if not fs.is_file(join_paths(vendor_ids_dir, 'usb.ids')) error('could not auto-detect -Dvendor_ids_dir=') endif endif conf.set_quoted ('FWUPD_DATADIR_VENDOR_IDS', vendor_ids_dir) bashcomp = dependency('bash-completion', required: false) python3path = get_option('python') if python3path == '' python3 = import('python').find_installation('python3') else python3 = find_program(python3path) endif gnutls = dependency('gnutls', version: '>= 3.6.0', required: get_option('gnutls')) if gnutls.found() conf.set('HAVE_GNUTLS', '1') endif lzma = dependency('liblzma') cbor = dependency('libcbor', version: '>= 0.7.0', required: get_option('cbor')) if cbor.found() conf.set('HAVE_CBOR', '1') if cc.has_header_symbol('cbor.h', 'cbor_set_allocs') conf.set('HAVE_CBOR_SET_ALLOCS', '1') endif endif platform_deps = [] if get_option('default_library') != 'static' if host_machine.system() == 'windows' platform_deps += cc.find_library('shlwapi') endif if host_machine.system() == 'freebsd' platform_deps += dependency('efivar') endif endif if valgrind.found() conf.set('HAVE_VALGRIND', '1') endif libsystemd = dependency('libsystemd', required: get_option('systemd').disable_auto_if(host_machine.system() != 'linux')) if cc.has_header('sys/auxv.h') conf.set('HAVE_AUXV_H', '1') endif if cc.has_header('sys/utsname.h') conf.set('HAVE_UTSNAME_H', '1') endif if cc.has_header('sys/inotify.h') conf.set('HAVE_INOTIFY_H', '1') endif if cc.has_header('sys/ioctl.h') conf.set('HAVE_IOCTL_H', '1') endif if cc.has_header('termios.h') conf.set('HAVE_TERMIOS_H', '1') endif if cc.has_header('errno.h') conf.set('HAVE_ERRNO_H', '1') endif if cc.has_header('sys/socket.h') conf.set('HAVE_SOCKET_H', '1') endif if cc.has_header('scsi/sg.h') conf.set('HAVE_SCSI_SG_H', '1') endif if cc.has_header('sys/select.h') conf.set('HAVE_SELECT_H', '1') endif if cc.has_header('sys/io.h') and cc.has_function('outb', prefix: '#include ') conf.set('HAVE_IO_H', '1') endif if cc.has_header('linux/ethtool.h') conf.set('HAVE_ETHTOOL_H', '1') endif if cc.has_header('linux/i2c-dev.h') conf.set('HAVE_I2C_DEV_H', '1') endif if cc.has_header('linux/mei.h') conf.set('HAVE_MEI_H', '1') endif if cc.has_header('linux/videodev2.h') conf.set('HAVE_VIDEODEV2_H', '1') endif if cc.has_header('mtd/mtd-user.h') conf.set('HAVE_MTD_USER_H', '1') endif if cc.has_header('linux/hidraw.h') conf.set('HAVE_HIDRAW_H', '1') endif if cc.has_header('sys/mman.h') conf.set('HAVE_MMAN_H', '1') endif if cc.has_header('poll.h') conf.set('HAVE_POLL_H', '1') endif if cc.has_header('kenv.h') conf.set('HAVE_KENV_H', '1') endif if cc.has_header('malloc.h') conf.set('HAVE_MALLOC_H', '1') if cc.has_function('malloc_trim', prefix: '#include ') conf.set('HAVE_MALLOC_TRIM', '1') endif endif has_cpuid = cc.has_header_symbol('cpuid.h', '__get_cpuid_count', required: false) if has_cpuid conf.set('HAVE_CPUID_H', '1') endif if cc.has_function('getuid') conf.set('HAVE_GETUID', '1') endif if cc.has_function('realpath') conf.set('HAVE_REALPATH', '1') endif if cc.has_function('memmem') conf.set('HAVE_MEMMEM', '1') endif if cc.has_function('sigaction') conf.set('HAVE_SIGACTION', '1') endif if cc.has_function('memfd_create') conf.set('HAVE_MEMFD_CREATE', '1') endif if cc.has_header_symbol('locale.h', 'LC_MESSAGES') conf.set('HAVE_LC_MESSAGES', '1') endif if cc.has_header('linux/ipmi.h') conf.set('HAVE_LINUX_IPMI_H', '1') endif if cc.has_header_symbol('fcntl.h', 'F_WRLCK') conf.set('HAVE_WRLCK', '1') endif if cc.has_header_symbol('fcntl.h', 'F_OFD_SETLK') conf.set('HAVE_OFD', '1') endif if cc.has_function('pwrite', args: '-D_XOPEN_SOURCE') conf.set('HAVE_PWRITE', '1') endif if cc.has_header_symbol('sys/mount.h', 'BLKSSZGET') conf.set('HAVE_BLKSSZGET', '1') endif if host_machine.system() == 'freebsd' if cc.has_type('struct efi_esrt_entry_v1', prefix: '#include \n#include ') conf.set('HAVE_FREEBSD_ESRT', '1') endif endif launchctl = find_program('launchctl', required: host_machine.system() == 'darwin') # this is way less hassle than including TargetConditionals.h and looking for TARGET_OS_MAC=1 if host_machine.system() == 'darwin' conf.set('HOST_MACHINE_SYSTEM_DARWIN', '1') summary({ 'launchctl': launchctl, 'launchd_agent_dir': get_option('launchd_agent_dir'), }, section: 'Darwin options') endif # EFI if build_standalone efi_app_location = join_paths(libexecdir, 'fwupd', 'efi') conf.set_quoted('EFI_APP_LOCATION', efi_app_location) endif flashrom = get_option('plugin_flashrom').disable_auto_if(host_machine.system() != 'linux') allow_flashrom = flashrom.allowed() if build_standalone libflashrom = dependency('flashrom', fallback: ['flashrom', 'flashrom_dep'], required: flashrom) if libflashrom.type_name() == 'pkgconfig' and cc.has_function('flashrom_set_progress_callback_v2', dependencies: libflashrom) conf.set('HAVE_FLASHROM_SET_PROGRESS_CALLBACK_V2' , '1') endif endif if libsystemd.found() systemd = dependency('systemd', version: '>= 249', required: get_option('systemd')) conf.set('HAVE_SYSTEMD' , '1') conf.set('HAVE_LOGIND' , '1') systemd_root_prefix = get_option('systemd_root_prefix') if systemd_root_prefix == '' systemdunitdir = systemd.get_variable(pkgconfig: 'systemdsystemunitdir') systemd_shutdown_dir = systemd.get_variable(pkgconfig: 'systemdshutdowndir') systemd_modules_load_dir = systemd.get_variable(pkgconfig: 'modulesloaddir') systemd_sysusers_dir = systemd.get_variable(pkgconfig: 'sysusersdir') else systemdunitdir = systemd.get_variable(pkgconfig: 'systemdsystemunitdir', pkgconfig_define: ['rootprefix', systemd_root_prefix]) systemd_shutdown_dir = systemd.get_variable(pkgconfig: 'systemdshutdowndir', pkgconfig_define: ['root_prefix', systemd_root_prefix]) systemd_modules_load_dir = systemd.get_variable(pkgconfig: 'modulesloaddir', pkgconfig_define: ['root_prefix', systemd_root_prefix]) systemd_sysusers_dir = systemd.get_variable(pkgconfig: 'sysusersdir', pkgconfig_define: ['root_prefix', systemd_root_prefix]) endif endif supported_build = get_option('supported_build').disable_auto_if(not tag).allowed() if supported_build conf.set('SUPPORTED_BUILD', '1') endif gnome = import('gnome') i18n = import('i18n') conf.set_quoted('FWUPD_PREFIX', prefix) conf.set_quoted('FWUPD_BINDIR', bindir) conf.set_quoted('FWUPD_LIBDIR', libdir) conf.set_quoted('FWUPD_LIBEXECDIR', libexecdir) conf.set_quoted('FWUPD_DATADIR', datadir) conf.set_quoted('FWUPD_LOCALSTATEDIR', localstatedir) conf.set_quoted('FWUPD_SYSCONFDIR', sysconfdir) conf.set_quoted('FWUPD_LOCALEDIR', localedir) if build_standalone if host_machine.system() == 'windows' libdir_pkg = bindir else libdir_pkg = join_paths(libdir, 'fwupd-@0@'.format(fwupd_version)) endif conf.set_quoted('FWUPD_LIBDIR_PKG', libdir_pkg) endif conf.set_quoted('GETTEXT_PACKAGE', meson.project_name()) conf.set_quoted('PACKAGE_NAME', meson.project_name()) conf.set_quoted('VERSION', meson.project_version()) if get_option('dbus_socket_address') != '' conf.set_quoted('FWUPD_DBUS_SOCKET_ADDRESS', get_option('dbus_socket_address')) endif motd_file = '85-fwupd' motd_dir = 'motd.d' conf.set_quoted('MOTD_FILE', motd_file) conf.set_quoted('MOTD_DIR', motd_dir) conf.set_quoted('FU_DEFAULT_P2P_POLICY', get_option('p2p_policy')) configure_file( output: 'config.h', configuration: conf ) libdrm_amdgpu = dependency('libdrm_amdgpu', version: '>= 2.4.113', required: get_option('libdrm')) protobufc = dependency('libprotobuf-c', required: get_option('protobuf')) protoc = find_program('protoc', 'protoc-c', required: get_option('protobuf')) root_incdir = include_directories('.') fwupd_gir = [] gir_dep = dependency('gobject-introspection-1.0', required: get_option('introspection')) introspection = get_option('introspection').disable_auto_if(host_machine.system() != 'linux').disable_auto_if(not gir_dep.found()) gidocgen_dep = dependency('gi-docgen', version: '>= 2021.1', native: true, fallback: ['gi-docgen', 'dummy_dep'], required: get_option('docs'), ) gidocgen_app = find_program('gi-docgen', required: gidocgen_dep.found()) build_docs = gidocgen_dep.found() and gidocgen_app.found() and introspection.allowed() if build_docs and gidocgen_dep.version().version_compare('< 2022.2') markdown_version = run_command( [python3, '-c', 'import markdown; print(markdown.__version__)'], check: true, ).stdout().strip() build_docs = get_option('docs').require( markdown_version.version_compare('>=3.2'), error_message: 'docs=enabled requires at least markdown >= 3.2' ).allowed() endif jinja2 = run_command( [python3, '-c', 'import jinja2; print(jinja2.__version__)'], check: true, ) if jinja2.stderr().strip() != '' error('Python module jinja2 not found') endif # using "meson configure -Db_sanitize=address,undefined" is super useful in finding corruption, # but it does not work with our GMainContext-abuse tests... if get_option('b_sanitize') in ['address,undefined', 'address', 'undefined', 'leak'] run_sanitize_unsafe_tests = false else run_sanitize_unsafe_tests = true endif # take foo.rs and generate foo-struct.c and foo-struct.h files like protobuf_c rustgen = generator(python3, output : ['@BASENAME@-struct.c', '@BASENAME@-struct.h'], arguments : [ join_paths(meson.project_source_root(), 'libfwupdplugin', 'rustgen.py'), '@INPUT@', '@OUTPUT0@', '@OUTPUT1@', ], ) dbusmock = run_command( [python3, '-c', 'import dbusmock; print(dbusmock.__version__)'], check: false, ) umockdev_integration_tests = get_option('umockdev_tests') \ .disable_auto_if(not get_option('tests')) \ .disable_auto_if(not introspection.allowed()) \ .disable_auto_if(not run_sanitize_unsafe_tests) \ .disable_auto_if(dbusmock.returncode() != 0) umockdev = dependency('umockdev-1.0', required: get_option('umockdev_tests')) if dbusmock.returncode() != 0 and get_option('umockdev_tests').allowed() warning('python dbusmock not found, umockdev tests will be disabled') endif allow_uefi_capsule = host_machine.system() in ['linux', 'freebsd'] and \ host_machine.cpu_family() in ['x86', 'x86_64', 'aarch64', 'riscv64', 'loongarch64'] subdir('generate-build') subdir('libfwupd') if polkit.found() subdir('policy') endif if build_standalone man_md = [] md_targets = [] plugin_quirks = [] subdir('libfwupdplugin') subdir('po') subdir('contrib') # common to all plugins plugin_builtins = [] plugin_incdirs = [ root_incdir, fwupd_incdir, fwupdplugin_incdir, ] plugin_libs = [ fwupd, fwupdplugin, ] subdir('plugins') subdir('src') subdir('docs') subdir('data') # append all the quirks into one big file and gzip it custom_target('builtin-quirk-gz', input: plugin_quirks, output: 'builtin.quirk.gz', command: [ generate_quirk_builtin, '@OUTPUT@', '@INPUT@', ], install: true, install_dir: join_paths(datadir, 'fwupd', 'quirks.d'), ) endif if libsystemd.found() summary({ 'systemd_unit_user': get_option('systemd_unit_user'), 'systemd unit dir': systemdunitdir, 'systemd shutdown dir': systemd_shutdown_dir, 'systemd modules dir': systemd_modules_load_dir, 'systemd sysusers dir': systemd_sysusers_dir, }, section: 'systemd options') endif summary({ 'fwupdtool': build_standalone, 'fwupd (daemon)': build_daemon }, section: 'build targets') summary({ 'cbor': cbor, 'dbus_socket_address': get_option('dbus_socket_address'), 'vendor_ids_dir': vendor_ids_dir, 'docs': build_docs, 'gnutls': gnutls, 'introspection': introspection.allowed(), 'libblkid': libblkid, 'libdrm': libdrm, 'valgrind': valgrind, 'polkit': polkit, 'python3': python3, 'supported_build': supported_build, 'static_analysis': static_analysis, 'tests': get_option('tests'), 'umockdev_tests': umockdev_integration_tests.allowed(), }, section: 'project features') if build_daemon summary({ 'bluez': bluez.allowed(), 'libusb': libusb, 'hsi': hsi, 'lvfs_metadata_format': lvfs_metadata_format, 'libarchive': libarchive.found(), 'passim': passim, 'GPG support': supported_gpg, 'PKCS7 support': supported_pkcs7, }, section: 'daemon features') en=[] dis=[] foreach plugin: plugins.keys() if plugins[plugin] en += plugin else dis += plugin endif endforeach summary({ 'enabled': ', '.join(en), 'disabled': ', '.join(dis), }, section: 'plugins') endif fwupd-2.0.10/meson_options.txt000066400000000000000000000077401501337203100163570ustar00rootroot00000000000000option('bash_completion', type: 'boolean', value: true, description: 'enable bash completion', ) option('bluez', type: 'feature', description: 'BlueZ support', ) option('build', type: 'combo', choices: [ 'all', 'standalone', 'library', ], value: 'all', description: 'build type', ) option('cbor', type: 'feature', description: 'CBOR support for coSWID and uSWID', ) option('dbus_socket_address', type: 'string', value: '', description: 'D-Bus socket address to use for p2p mode', ) option('docs', type: 'feature', description: 'Build developer documentation', ) option('efi_binary', type: 'boolean', value: false, description: 'generate uefi binary if missing', ) option('efi_os_dir', type: 'string', description: 'the hardcoded name of OS directory in ESP, e.g. fedora', ) option('firmware-packager', type: 'boolean', value: true, description: 'enable firmware-packager installation', ) option('fish_completion', type: 'boolean', value: true, description: 'enable fish completion', ) option('gnutls', type: 'feature', description: 'GnuTLS support', ) option('hsi', type: 'feature', description: ' Host Security Information', ) option('introspection', type: 'feature', description: 'generate GObject Introspection data', ) option('launchd_agent_dir', type: 'string', value: '/Library/LaunchAgents', description: 'Directory to put the launchd agent', ) option('libarchive', type: 'feature', description: 'libarchive support', ) option('readline', type: 'feature', description: 'readline support', ) option('libdrm', type: 'feature', description: 'libdrm support', ) option('valgrind', type: 'feature', description: 'valgrind support', ) option('blkid', type: 'feature', description: 'libblkid support', ) option('lvfs', type: 'combo', choices: [ 'true', 'false', 'disabled', ], value: 'true', description: 'install LVFS remotes', ) option('man', type: 'boolean', value: true, description: 'enable man pages', ) option('metainfo', type: 'boolean', value: true, description: 'install the project metainfo.xml information', ) option('passim', type: 'feature', description: 'Passim support', ) option('p2p_policy', type: 'combo', choices: [ 'none', 'metadata', 'firmware', 'metadata,firmware', ], value: 'metadata', description: 'Default P2P sharing policy', ) option('plugin_flashrom', type: 'feature', description: 'flashrom support', ) option('protobuf', type: 'feature', description: 'protobuf support', ) option('plugin_modem_manager', type: 'feature', description: 'ModemManager support', ) option('plugin_uefi_capsule_splash', type: 'boolean', value: true, description: 'enable UEFI capsule splash support', ) option('polkit', type: 'feature', description: 'PolKit support in daemon', ) option('python', type: 'string', description: 'the absolute path of the python3 binary', ) option('qubes', type: 'boolean', value: false, description: 'build packages for Qubes OS', ) option('static_analysis', type: 'boolean', value: false, description: 'enable GCC static analysis support', ) option('supported_build', type: 'feature', description: 'distribution package with upstream support', ) option('systemd', type: 'feature', description: 'systemd support', ) option('systemd_root_prefix', type: 'string', value: '', description: 'Directory to base systemd’s installation directories on', ) option('systemd_unit_user', type: 'string', value: 'fwupd-refresh', description: 'User account to use for fwupd-refresh.service (empty for DynamicUser)', ) option('tests', type: 'boolean', value: true, description: 'enable tests', ) option('umockdev_tests', type: 'feature', description: 'umockdev tests', ) option('vendor_ids_dir', type: 'string', value: '', description: 'Directory for usb.ids, pci.ids etc.', ) option('vendor_metadata', type: 'boolean', value: false, description: 'install OS vendor provided metadata', ) fwupd-2.0.10/plugins/000077500000000000000000000000001501337203100143735ustar00rootroot00000000000000fwupd-2.0.10/plugins/README.md000066400000000000000000000021611501337203100156520ustar00rootroot00000000000000# Adding a new plugin An extensible architecture allows for providing new plugin types (for reading and writing different firmware) as well as ways quirk their behavior. You can find more information about the architecture in the developers section of the [fwupd website](https://fwupd.org). You can use the [fwupd developer documentation](https://fwupd.github.io) to assist with APIs available to write the plugin. If you have a firmware specification and would like to see support in this project, please file an issue and share the spec. Patches are also welcome. We will not accept plugins that upgrade hardware using a proprietary Linux executable, proprietary UEFI executable, proprietary library, or DBus interface. ## Plugin interaction Some plugins may be able to influence the behavior of other plugins. This includes things like one plugin turning on a device, or providing missing metadata to another plugin. The ABI for these interactions is defined in: All interactions between plugins should have the interface defined in that file. fwupd-2.0.10/plugins/acpi-dmar/000077500000000000000000000000001501337203100162305ustar00rootroot00000000000000fwupd-2.0.10/plugins/acpi-dmar/README.md000066400000000000000000000006041501337203100175070ustar00rootroot00000000000000--- title: Plugin: ACPI DMAR — DMA Protection --- ## Introduction This plugin checks if DMA remapping for Thunderbolt devices is available. The result will be stored in an security attribute for HSI. ## External Interface Access This plugin requires read access to `/sys/firmware/acpi/tables`. ## Version Considerations This plugin has been available since fwupd version `1.5.0`. fwupd-2.0.10/plugins/acpi-dmar/fu-acpi-dmar-plugin.c000066400000000000000000000043501501337203100221370ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-acpi-dmar-plugin.h" #include "fu-acpi-dmar.h" struct _FuAcpiDmarPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuAcpiDmarPlugin, fu_acpi_dmar_plugin, FU_TYPE_PLUGIN) static void fu_acpi_dmar_plugin_add_security_attrs(FuPlugin *plugin, FuSecurityAttrs *attrs) { g_autofree gchar *fn = NULL; g_autofree gchar *path = NULL; g_autoptr(FuAcpiDmar) dmar = fu_acpi_dmar_new(); g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(GError) error_local = NULL; /* only Intel */ if (fu_cpu_get_vendor() != FU_CPU_VENDOR_INTEL) return; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_PREBOOT_DMA_PROTECTION); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_ENABLED); fu_security_attrs_append(attrs, attr); /* load DMAR table */ path = fu_path_from_kind(FU_PATH_KIND_ACPI_TABLES); fn = g_build_filename(path, "DMAR", NULL); stream = fu_input_stream_from_path(fn, &error_local); if (stream == NULL) { g_debug("failed to load %s: %s", fn, error_local->message); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); return; } if (!fu_firmware_parse_stream(FU_FIRMWARE(dmar), stream, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error_local)) { g_warning("failed to parse %s: %s", fn, error_local->message); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); return; } if (!fu_acpi_dmar_get_opt_in(dmar)) { fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_acpi_dmar_plugin_init(FuAcpiDmarPlugin *self) { } static void fu_acpi_dmar_plugin_class_init(FuAcpiDmarPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->add_security_attrs = fu_acpi_dmar_plugin_add_security_attrs; } fwupd-2.0.10/plugins/acpi-dmar/fu-acpi-dmar-plugin.h000066400000000000000000000003641501337203100221450ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuAcpiDmarPlugin, fu_acpi_dmar_plugin, FU, ACPI_DMAR_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/acpi-dmar/fu-acpi-dmar.c000066400000000000000000000031341501337203100206420ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-acpi-dmar.h" struct _FuAcpiDmar { FuAcpiTable parent_instance; gboolean opt_in; }; G_DEFINE_TYPE(FuAcpiDmar, fu_acpi_dmar, FU_TYPE_ACPI_TABLE) #define DMAR_DMA_CTRL_PLATFORM_OPT_IN_FLAG 0x4 static gboolean fu_acpi_dmar_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuAcpiDmar *self = FU_ACPI_DMAR(firmware); guint8 dma_flags = 0; /* FuAcpiTable->parse */ if (!FU_FIRMWARE_CLASS(fu_acpi_dmar_parent_class) ->parse(FU_FIRMWARE(self), stream, flags, error)) return FALSE; /* check signature and read flags */ if (g_strcmp0(fu_firmware_get_id(FU_FIRMWARE(self)), "DMAR") != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Not a DMAR table, got %s", fu_firmware_get_id(FU_FIRMWARE(self))); return FALSE; } if (!fu_input_stream_read_u8(stream, 0x25, &dma_flags, error)) return FALSE; g_debug("Flags: 0x%02x", dma_flags); self->opt_in = (dma_flags & DMAR_DMA_CTRL_PLATFORM_OPT_IN_FLAG) > 0; return TRUE; } FuAcpiDmar * fu_acpi_dmar_new(void) { return g_object_new(FU_TYPE_ACPI_DMAR, NULL); } gboolean fu_acpi_dmar_get_opt_in(FuAcpiDmar *self) { g_return_val_if_fail(FU_IS_ACPI_DMAR(self), FALSE); return self->opt_in; } static void fu_acpi_dmar_class_init(FuAcpiDmarClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_acpi_dmar_parse; } static void fu_acpi_dmar_init(FuAcpiDmar *self) { } fwupd-2.0.10/plugins/acpi-dmar/fu-acpi-dmar.h000066400000000000000000000005611501337203100206500ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ACPI_DMAR (fu_acpi_dmar_get_type()) G_DECLARE_FINAL_TYPE(FuAcpiDmar, fu_acpi_dmar, FU, ACPI_DMAR, FuAcpiTable) FuAcpiDmar * fu_acpi_dmar_new(void); gboolean fu_acpi_dmar_get_opt_in(FuAcpiDmar *self); fwupd-2.0.10/plugins/acpi-dmar/fu-self-test.c000066400000000000000000000040211501337203100207070ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-acpi-dmar.h" static void fu_acpi_dmar_opt_in_func(void) { gboolean ret; g_autoptr(FuAcpiDmar) dmar = fu_acpi_dmar_new(); g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = NULL; g_autofree gchar *fn = NULL; fn = g_test_build_filename(G_TEST_DIST, "tests", "DMAR", NULL); if (!g_file_test(fn, G_FILE_TEST_EXISTS)) { g_test_skip("Missing DMAR"); return; } stream = fu_input_stream_from_path(fn, &error); g_assert_no_error(error); g_assert_nonnull(stream); ret = fu_firmware_parse_stream(FU_FIRMWARE(dmar), stream, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_true(fu_acpi_dmar_get_opt_in(dmar)); } static void fu_acpi_dmar_opt_out_func(void) { gboolean ret; g_autoptr(FuAcpiDmar) dmar = fu_acpi_dmar_new(); g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = NULL; g_autofree gchar *fn = NULL; fn = g_test_build_filename(G_TEST_DIST, "tests", "DMAR-OPTOUT", NULL); if (!g_file_test(fn, G_FILE_TEST_EXISTS)) { g_test_skip("Missing DMAR-OPTOUT"); return; } stream = fu_input_stream_from_path(fn, &error); g_assert_no_error(error); g_assert_nonnull(stream); ret = fu_firmware_parse_stream(FU_FIRMWARE(dmar), stream, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_false(fu_acpi_dmar_get_opt_in(dmar)); } int main(int argc, char **argv) { (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); (void)g_setenv("G_MESSAGES_DEBUG", "all", TRUE); /* tests go here */ g_test_add_func("/acpi-dmar/opt-in", fu_acpi_dmar_opt_in_func); g_test_add_func("/acpi-dmar/opt-out", fu_acpi_dmar_opt_out_func); return g_test_run(); } fwupd-2.0.10/plugins/acpi-dmar/meson.build000066400000000000000000000021161501337203100203720ustar00rootroot00000000000000if hsi and (host_cpu == 'x86' or host_cpu == 'x86_64') plugins += {meson.current_source_dir().split('/')[-1]: true} cargs = ['-DG_LOG_DOMAIN="FuPluginAcpiDmar"'] plugin_builtin_acpi_dmar = static_library('fu_plugin_acpi_dmar', sources: [ 'fu-acpi-dmar-plugin.c', 'fu-acpi-dmar.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) plugin_builtins += plugin_builtin_acpi_dmar if get_option('tests') env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) e = executable( 'acpi-dmar-self-test', sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: plugin_deps, link_with: [ plugin_libs, plugin_builtin_acpi_dmar, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('acpi-dmar-self-test', e, env: env) # added to installed-tests endif endif fwupd-2.0.10/plugins/acpi-facp/000077500000000000000000000000001501337203100162165ustar00rootroot00000000000000fwupd-2.0.10/plugins/acpi-facp/README.md000066400000000000000000000005661501337203100175040ustar00rootroot00000000000000--- title: Plugin: ACPI FACP — Fixed ACPI Description Table --- ## Introduction This plugin checks if S2I sleep is available. The result will be stored in an security attribute for HSI. ## External Interface Access This plugin requires read access to `/sys/firmware/acpi/tables`. ## Version Considerations This plugin has been available since fwupd version `1.5.0`. fwupd-2.0.10/plugins/acpi-facp/fu-acpi-facp-plugin.c000066400000000000000000000042631501337203100221160ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-acpi-facp-plugin.h" #include "fu-acpi-facp.h" struct _FuAcpiFacpPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuAcpiFacpPlugin, fu_acpi_facp_plugin, FU_TYPE_PLUGIN) static void fu_acpi_facp_plugin_add_security_attrs(FuPlugin *plugin, FuSecurityAttrs *attrs) { g_autofree gchar *fn = NULL; g_autofree gchar *path = NULL; g_autoptr(FuAcpiFacp) facp = NULL; g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(GBytes) blob = NULL; g_autoptr(GError) error_local = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_SUSPEND_TO_IDLE); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_ENABLED); fu_security_attrs_append(attrs, attr); /* load FACP table */ path = fu_path_from_kind(FU_PATH_KIND_ACPI_TABLES); fn = g_build_filename(path, "FACP", NULL); blob = fu_bytes_get_contents(fn, &error_local); if (blob == NULL) { g_debug("failed to load %s: %s", fn, error_local->message); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); return; } facp = fu_acpi_facp_new(blob, &error_local); if (facp == NULL) { g_warning("failed to parse %s: %s", fn, error_local->message); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); return; } /* options are usually "Linux" (S3) or "Windows" (s2idle) */ fu_security_attr_add_bios_target_value(attr, "com.thinklmi.SleepState", "windows"); if (!fu_acpi_facp_get_s2i(facp)) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_OS); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_acpi_facp_plugin_init(FuAcpiFacpPlugin *self) { } static void fu_acpi_facp_plugin_class_init(FuAcpiFacpPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->add_security_attrs = fu_acpi_facp_plugin_add_security_attrs; } fwupd-2.0.10/plugins/acpi-facp/fu-acpi-facp-plugin.h000066400000000000000000000003641501337203100221210ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuAcpiFacpPlugin, fu_acpi_facp_plugin, FU, ACPI_FACP_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/acpi-facp/fu-acpi-facp.c000066400000000000000000000020441501337203100206150ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fu-acpi-facp.h" struct _FuAcpiFacp { GObject parent_instance; gboolean get_s2i; }; G_DEFINE_TYPE(FuAcpiFacp, fu_acpi_facp, G_TYPE_OBJECT) #define LOW_POWER_S0_IDLE_CAPABLE (1 << 21) FuAcpiFacp * fu_acpi_facp_new(GBytes *blob, GError **error) { FuAcpiFacp *self = g_object_new(FU_TYPE_ACPI_FACP, NULL); gsize bufsz = 0; guint32 flags = 0; const guint8 *buf = g_bytes_get_data(blob, &bufsz); /* parse table */ if (!fu_memread_uint32_safe(buf, bufsz, 0x70, &flags, G_LITTLE_ENDIAN, error)) return NULL; g_debug("Flags: 0x%04x", flags); self->get_s2i = (flags & LOW_POWER_S0_IDLE_CAPABLE) > 0; return self; } gboolean fu_acpi_facp_get_s2i(FuAcpiFacp *self) { g_return_val_if_fail(FU_IS_ACPI_FACP(self), FALSE); return self->get_s2i; } static void fu_acpi_facp_class_init(FuAcpiFacpClass *klass) { } static void fu_acpi_facp_init(FuAcpiFacp *self) { } fwupd-2.0.10/plugins/acpi-facp/fu-acpi-facp.h000066400000000000000000000005761501337203100206320ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ACPI_FACP (fu_acpi_facp_get_type()) G_DECLARE_FINAL_TYPE(FuAcpiFacp, fu_acpi_facp, FU, ACPI_FACP, GObject) FuAcpiFacp * fu_acpi_facp_new(GBytes *blob, GError **error); gboolean fu_acpi_facp_get_s2i(FuAcpiFacp *self); fwupd-2.0.10/plugins/acpi-facp/fu-self-test.c000066400000000000000000000034301501337203100207000ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-acpi-facp.h" static void fu_acpi_facp_s2i_disabled_func(void) { g_autofree gchar *fn = NULL; g_autoptr(FuAcpiFacp) facp = NULL; g_autoptr(GBytes) blob = NULL; g_autoptr(GError) error = NULL; fn = g_test_build_filename(G_TEST_DIST, "tests", "FACP", NULL); if (!g_file_test(fn, G_FILE_TEST_EXISTS)) { g_test_skip("Missing FACP"); return; } blob = fu_bytes_get_contents(fn, &error); g_assert_no_error(error); g_assert_nonnull(blob); facp = fu_acpi_facp_new(blob, &error); g_assert_no_error(error); g_assert_nonnull(facp); g_assert_false(fu_acpi_facp_get_s2i(facp)); } static void fu_acpi_facp_s2i_enabled_func(void) { g_autofree gchar *fn = NULL; g_autoptr(FuAcpiFacp) facp = NULL; g_autoptr(GBytes) blob = NULL; g_autoptr(GError) error = NULL; fn = g_test_build_filename(G_TEST_DIST, "tests", "FACP-S2I", NULL); if (!g_file_test(fn, G_FILE_TEST_EXISTS)) { g_test_skip("Missing FACP-S2I"); return; } blob = fu_bytes_get_contents(fn, &error); g_assert_no_error(error); g_assert_nonnull(blob); facp = fu_acpi_facp_new(blob, &error); g_assert_no_error(error); g_assert_nonnull(facp); g_assert_true(fu_acpi_facp_get_s2i(facp)); } int main(int argc, char **argv) { (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); (void)g_setenv("G_MESSAGES_DEBUG", "all", TRUE); /* tests go here */ g_test_add_func("/acpi-facp/s2i{disabled}", fu_acpi_facp_s2i_disabled_func); g_test_add_func("/acpi-facp/s2i{enabled}", fu_acpi_facp_s2i_enabled_func); return g_test_run(); } fwupd-2.0.10/plugins/acpi-facp/meson.build000066400000000000000000000021161501337203100203600ustar00rootroot00000000000000if hsi and (host_cpu == 'x86' or host_cpu == 'x86_64') cargs = ['-DG_LOG_DOMAIN="FuPluginAcpiFacp"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_builtin_acpi_facp = static_library('fu_plugin_acpi_facp', sources: [ 'fu-acpi-facp-plugin.c', 'fu-acpi-facp.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) plugin_builtins += plugin_builtin_acpi_facp if get_option('tests') env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) e = executable( 'acpi-facp-self-test', sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: plugin_deps, link_with: [ plugin_libs, plugin_builtin_acpi_facp, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('acpi-facp-self-test', e, env: env) # added to installed-tests endif endif fwupd-2.0.10/plugins/acpi-ivrs/000077500000000000000000000000001501337203100162705ustar00rootroot00000000000000fwupd-2.0.10/plugins/acpi-ivrs/.gitignore000066400000000000000000000000061501337203100202540ustar00rootroot00000000000000tests fwupd-2.0.10/plugins/acpi-ivrs/README.md000066400000000000000000000007151501337203100175520ustar00rootroot00000000000000--- title: Plugin: ACPI IVRS — DMA Protection --- ## Introduction This plugin checks if Pre-boot DMA remapping is available and enabled from the [ACPI IVRS](http://support.amd.com/TechDocs/48882_IOMMU.pdf) table. The result will be stored in an security attribute for HSI. ## External Interface Access This plugin requires read access to `/sys/firmware/acpi/tables`. ## Version Considerations This plugin has been available since fwupd version `1.8.0`. fwupd-2.0.10/plugins/acpi-ivrs/fu-acpi-ivrs-plugin.c000066400000000000000000000044471501337203100222460ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * Copyright 2022 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-acpi-ivrs-plugin.h" #include "fu-acpi-ivrs.h" struct _FuAcpiIvrsPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuAcpiIvrsPlugin, fu_acpi_ivrs_plugin, FU_TYPE_PLUGIN) static void fu_acpi_ivrs_plugin_add_security_attrs(FuPlugin *plugin, FuSecurityAttrs *attrs) { g_autofree gchar *fn = NULL; g_autofree gchar *path = NULL; g_autoptr(FuAcpiIvrs) ivrs = fu_acpi_ivrs_new(); g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(GError) error_local = NULL; /* only AMD */ if (fu_cpu_get_vendor() != FU_CPU_VENDOR_AMD) return; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_PREBOOT_DMA_PROTECTION); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_ENABLED); fu_security_attrs_append(attrs, attr); /* load IVRS table */ path = fu_path_from_kind(FU_PATH_KIND_ACPI_TABLES); fn = g_build_filename(path, "IVRS", NULL); stream = fu_input_stream_from_path(fn, &error_local); if (stream == NULL) { g_debug("failed to load %s: %s", fn, error_local->message); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); return; } if (!fu_firmware_parse_stream(FU_FIRMWARE(ivrs), stream, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error_local)) { g_warning("failed to parse %s: %s", fn, error_local->message); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); return; } if (!fu_acpi_ivrs_get_dma_remap(ivrs)) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_acpi_ivrs_plugin_init(FuAcpiIvrsPlugin *self) { } static void fu_acpi_ivrs_plugin_class_init(FuAcpiIvrsPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->add_security_attrs = fu_acpi_ivrs_plugin_add_security_attrs; } fwupd-2.0.10/plugins/acpi-ivrs/fu-acpi-ivrs-plugin.h000066400000000000000000000003641501337203100222450ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuAcpiIvrsPlugin, fu_acpi_ivrs_plugin, FU, ACPI_IVRS_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/acpi-ivrs/fu-acpi-ivrs.c000066400000000000000000000032471501337203100207470ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * Copyright 2022 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-acpi-ivrs.h" struct _FuAcpiIvrs { FuAcpiTable parent_instance; gboolean remap_support; }; G_DEFINE_TYPE(FuAcpiIvrs, fu_acpi_ivrs, FU_TYPE_ACPI_TABLE) /* IVINfo field */ #define IVRS_DMA_REMAP_SUPPORT_FLAG 0x2 static gboolean fu_acpi_ivrs_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuAcpiIvrs *self = FU_ACPI_IVRS(firmware); guint8 ivinfo = 0; /* FuAcpiTable->parse */ if (!FU_FIRMWARE_CLASS(fu_acpi_ivrs_parent_class) ->parse(FU_FIRMWARE(self), stream, flags, error)) return FALSE; /* check signature and read flags */ if (g_strcmp0(fu_firmware_get_id(FU_FIRMWARE(self)), "IVRS") != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Not a IVRS table, got %s", fu_firmware_get_id(FU_FIRMWARE(self))); return FALSE; } if (!fu_input_stream_read_u8(stream, 0x24, &ivinfo, error)) return FALSE; g_debug("Flags: 0x%02x", ivinfo); self->remap_support = ivinfo & IVRS_DMA_REMAP_SUPPORT_FLAG; return TRUE; } FuAcpiIvrs * fu_acpi_ivrs_new(void) { return g_object_new(FU_TYPE_ACPI_IVRS, NULL); } gboolean fu_acpi_ivrs_get_dma_remap(FuAcpiIvrs *self) { g_return_val_if_fail(FU_IS_ACPI_IVRS(self), FALSE); return self->remap_support; } static void fu_acpi_ivrs_class_init(FuAcpiIvrsClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_acpi_ivrs_parse; } static void fu_acpi_ivrs_init(FuAcpiIvrs *self) { } fwupd-2.0.10/plugins/acpi-ivrs/fu-acpi-ivrs.h000066400000000000000000000006641501337203100207540ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * Copyright 2022 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ACPI_IVRS (fu_acpi_ivrs_get_type()) G_DECLARE_FINAL_TYPE(FuAcpiIvrs, fu_acpi_ivrs, FU, ACPI_IVRS, FuAcpiTable) FuAcpiIvrs * fu_acpi_ivrs_new(void); gboolean fu_acpi_ivrs_get_dma_remap(FuAcpiIvrs *self); fwupd-2.0.10/plugins/acpi-ivrs/fu-self-test.c000066400000000000000000000057531501337203100207640ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * Copyright 2022 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-acpi-ivrs.h" static void fu_acpi_ivrs_dma_remap_func(void) { gboolean ret; guint32 rev; const gchar *oem_id; g_autoptr(FuAcpiIvrs) ivrs = fu_acpi_ivrs_new(); g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = NULL; g_autofree gchar *fn = NULL; fn = g_test_build_filename(G_TEST_DIST, "tests", "IVRS-REMAP", NULL); if (!g_file_test(fn, G_FILE_TEST_EXISTS)) { g_test_skip("Missing IVRS-REMAP"); return; } stream = fu_input_stream_from_path(fn, &error); g_assert_no_error(error); g_assert_nonnull(stream); ret = fu_firmware_parse_stream(FU_FIRMWARE(ivrs), stream, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_true(fu_acpi_ivrs_get_dma_remap(ivrs)); rev = fu_acpi_table_get_revision(FU_ACPI_TABLE(ivrs)); g_assert_cmpuint(rev, ==, 0x2); oem_id = fu_acpi_table_get_oem_id(FU_ACPI_TABLE(ivrs)); g_assert_cmpstr(oem_id, ==, "LENOVO"); oem_id = fu_acpi_table_get_oem_table_id(FU_ACPI_TABLE(ivrs)); g_assert_cmpstr(oem_id, ==, "TP-R1K "); rev = fu_acpi_table_get_oem_revision(FU_ACPI_TABLE(ivrs)); g_assert_cmpuint(rev, ==, 2417033216); } static void fu_acpi_ivrs_no_dma_remap_func(void) { gboolean ret; guint32 rev; const gchar *oem_id; g_autoptr(FuAcpiIvrs) ivrs = fu_acpi_ivrs_new(); g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = NULL; g_autofree gchar *fn = NULL; fn = g_test_build_filename(G_TEST_DIST, "tests", "IVRS-NOREMAP", NULL); if (!g_file_test(fn, G_FILE_TEST_EXISTS)) { g_test_skip("Missing IVRS-NOREMAP"); return; } stream = fu_input_stream_from_path(fn, &error); g_assert_no_error(error); g_assert_nonnull(stream); ret = fu_firmware_parse_stream(FU_FIRMWARE(ivrs), stream, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_false(fu_acpi_ivrs_get_dma_remap(ivrs)); rev = fu_acpi_table_get_revision(FU_ACPI_TABLE(ivrs)); g_assert_cmpuint(rev, ==, 0x2); oem_id = fu_acpi_table_get_oem_id(FU_ACPI_TABLE(ivrs)); g_assert_cmpstr(oem_id, ==, "LENOVO"); oem_id = fu_acpi_table_get_oem_table_id(FU_ACPI_TABLE(ivrs)); g_assert_cmpstr(oem_id, ==, "TC-S07 "); rev = fu_acpi_table_get_oem_revision(FU_ACPI_TABLE(ivrs)); g_assert_cmpuint(rev, ==, 1074921472); } int main(int argc, char **argv) { (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); (void)g_setenv("G_MESSAGES_DEBUG", "all", TRUE); /* tests go here */ g_test_add_func("/acpi-ivrs/dma-remap-support", fu_acpi_ivrs_dma_remap_func); g_test_add_func("/acpi-ivrs/no-dma-remap-support", fu_acpi_ivrs_no_dma_remap_func); return g_test_run(); } fwupd-2.0.10/plugins/acpi-ivrs/meson.build000066400000000000000000000021161501337203100204320ustar00rootroot00000000000000if hsi and (host_cpu == 'x86' or host_cpu == 'x86_64') cargs = ['-DG_LOG_DOMAIN="FuPluginAcpiIvrs"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_builtin_acpi_ivrs = static_library('fu_plugin_acpi_ivrs', sources: [ 'fu-acpi-ivrs-plugin.c', 'fu-acpi-ivrs.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) plugin_builtins += plugin_builtin_acpi_ivrs if get_option('tests') env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) e = executable( 'acpi-ivrs-self-test', sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: plugin_deps, link_with: [ plugin_libs, plugin_builtin_acpi_ivrs, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('acpi-ivrs-self-test', e, env: env) # added to installed-tests endif endif fwupd-2.0.10/plugins/acpi-phat/000077500000000000000000000000001501337203100162415ustar00rootroot00000000000000fwupd-2.0.10/plugins/acpi-phat/README.md000066400000000000000000000017731501337203100175300ustar00rootroot00000000000000--- title: Plugin: ACPI PHAT — Platform Health Assessment Table --- ## Introduction The PHAT is an ACPI table where a platform can expose health related telemetry that may be useful for software running within the constraints of an OS. These elements are typically going to encompass things that are likely otherwise not enumerable during the OS runtime phase of operations, such as version of pre-OS components. The daemon includes some of the PHAT data in the report data sent to the LVFS so that we can debug failures with the help of the IHV. This allows us to find the root cause of the problem, and so we know what other OEMs may be affected. See for more information. ## External Interface Access This plugin requires read access to `/sys/firmware/acpi/tables`. ## Version Considerations This plugin has been available since fwupd version `1.6.1`. fwupd-2.0.10/plugins/acpi-phat/fu-acpi-phat-health-record.c000066400000000000000000000136151501337203100234100ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-acpi-phat-health-record.h" #include "fu-acpi-phat-struct.h" #include "fu-acpi-phat.h" struct _FuAcpiPhatHealthRecord { FuFirmware parent_instance; guint8 am_healthy; gchar *guid; gchar *device_path; }; G_DEFINE_TYPE(FuAcpiPhatHealthRecord, fu_acpi_phat_health_record, FU_TYPE_FIRMWARE) static void fu_acpi_phat_health_record_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuAcpiPhatHealthRecord *self = FU_ACPI_PHAT_HEALTH_RECORD(firmware); if (self->guid != NULL) fu_xmlb_builder_insert_kv(bn, "guid", self->guid); if (self->device_path != NULL) fu_xmlb_builder_insert_kv(bn, "device_path", self->device_path); if (self->am_healthy != 0) fu_xmlb_builder_insert_kx(bn, "am_healthy", self->am_healthy); } static gboolean fu_acpi_phat_health_record_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuAcpiPhatHealthRecord *self = FU_ACPI_PHAT_HEALTH_RECORD(firmware); gsize streamsz = 0; guint16 rcdlen; guint32 dataoff; g_autoptr(GByteArray) st = NULL; /* sanity check record length */ st = fu_struct_acpi_phat_health_record_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; rcdlen = fu_struct_acpi_phat_health_record_get_rcdlen(st); if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (rcdlen != streamsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "record length not valid: %" G_GUINT16_FORMAT, rcdlen); return FALSE; } self->am_healthy = fu_struct_acpi_phat_health_record_get_flags(st); self->guid = fwupd_guid_to_string(fu_struct_acpi_phat_health_record_get_device_signature(st), FWUPD_GUID_FLAG_MIXED_ENDIAN); /* device path */ dataoff = fu_struct_acpi_phat_health_record_get_device_specific_data(st); if (streamsz > 28) { gsize ubufsz; /* bytes */ g_autoptr(GBytes) ubuf = NULL; /* header -> devicepath -> data */ if (dataoff == 0x0) { ubufsz = streamsz - 28; } else { ubufsz = dataoff - 28; } if (ubufsz == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "device path not valid: %" G_GSIZE_FORMAT, ubufsz); return FALSE; } /* align and convert */ ubuf = fu_input_stream_read_bytes(stream, 28, ubufsz, NULL, error); if (ubuf == NULL) return FALSE; self->device_path = fu_utf16_to_utf8_bytes(ubuf, G_LITTLE_ENDIAN, error); if (self->device_path == NULL) return FALSE; } /* success */ return TRUE; } static GByteArray * fu_acpi_phat_health_record_write(FuFirmware *firmware, GError **error) { FuAcpiPhatHealthRecord *self = FU_ACPI_PHAT_HEALTH_RECORD(firmware); g_autoptr(GByteArray) st = fu_struct_acpi_phat_health_record_new(); /* convert device path ahead of time */ if (self->device_path != NULL) { g_autoptr(GByteArray) buf = fu_utf8_to_utf16_byte_array(self->device_path, G_LITTLE_ENDIAN, FU_UTF_CONVERT_FLAG_NONE, error); if (buf == NULL) return NULL; g_byte_array_append(st, buf->data, buf->len); } /* data record */ if (self->guid != NULL) { fwupd_guid_t guid = {0x0}; if (!fwupd_guid_from_string(self->guid, &guid, FWUPD_GUID_FLAG_MIXED_ENDIAN, error)) return NULL; fu_struct_acpi_phat_health_record_set_device_signature(st, &guid); } fu_struct_acpi_phat_health_record_set_rcdlen(st, st->len); fu_struct_acpi_phat_health_record_set_version(st, fu_firmware_get_version_raw(firmware)); fu_struct_acpi_phat_health_record_set_flags(st, self->am_healthy); /* success */ return g_steal_pointer(&st); } static void fu_acpi_phat_health_record_set_guid(FuAcpiPhatHealthRecord *self, const gchar *guid) { g_free(self->guid); self->guid = g_strdup(guid); } static void fu_acpi_phat_health_record_set_device_path(FuAcpiPhatHealthRecord *self, const gchar *device_path) { g_free(self->device_path); self->device_path = g_strdup(device_path); } static gboolean fu_acpi_phat_health_record_build(FuFirmware *firmware, XbNode *n, GError **error) { FuAcpiPhatHealthRecord *self = FU_ACPI_PHAT_HEALTH_RECORD(firmware); const gchar *tmp; guint64 tmp64; /* optional properties */ tmp = xb_node_query_text(n, "device_path", NULL); if (tmp != NULL) fu_acpi_phat_health_record_set_device_path(self, tmp); tmp = xb_node_query_text(n, "guid", NULL); if (tmp != NULL) fu_acpi_phat_health_record_set_guid(self, tmp); tmp64 = xb_node_query_text_as_uint(n, "am_healthy", NULL); if (tmp64 != G_MAXUINT64) { if (tmp64 > G_MAXUINT8) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "am_healthy value invalid, got 0x%x", (guint)tmp64); return FALSE; } self->am_healthy = (guint8)tmp64; } /* success */ return TRUE; } static void fu_acpi_phat_health_record_init(FuAcpiPhatHealthRecord *self) { fu_firmware_set_images_max(FU_FIRMWARE(self), 2000); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_NO_AUTO_DETECTION); } static void fu_acpi_phat_health_record_finalize(GObject *object) { FuAcpiPhatHealthRecord *self = FU_ACPI_PHAT_HEALTH_RECORD(object); g_free(self->guid); g_free(self->device_path); G_OBJECT_CLASS(fu_acpi_phat_health_record_parent_class)->finalize(object); } static void fu_acpi_phat_health_record_class_init(FuAcpiPhatHealthRecordClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_acpi_phat_health_record_finalize; firmware_class->parse = fu_acpi_phat_health_record_parse; firmware_class->write = fu_acpi_phat_health_record_write; firmware_class->export = fu_acpi_phat_health_record_export; firmware_class->build = fu_acpi_phat_health_record_build; } FuFirmware * fu_acpi_phat_health_record_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_ACPI_PHAT_HEALTH_RECORD, NULL)); } fwupd-2.0.10/plugins/acpi-phat/fu-acpi-phat-health-record.h000066400000000000000000000006521501337203100234120ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ACPI_PHAT_HEALTH_RECORD (fu_acpi_phat_health_record_get_type()) G_DECLARE_FINAL_TYPE(FuAcpiPhatHealthRecord, fu_acpi_phat_health_record, FU, ACPI_PHAT_HEALTH_RECORD, FuFirmware) FuFirmware * fu_acpi_phat_health_record_new(void); fwupd-2.0.10/plugins/acpi-phat/fu-acpi-phat-plugin.c000066400000000000000000000041761501337203100221670ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-acpi-phat-health-record.h" #include "fu-acpi-phat-plugin.h" #include "fu-acpi-phat-version-element.h" #include "fu-acpi-phat-version-record.h" #include "fu-acpi-phat.h" struct _FuAcpiPhatPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuAcpiPhatPlugin, fu_acpi_phat_plugin, FU_TYPE_PLUGIN) static gboolean fu_acpi_phat_plugin_coldplug(FuPlugin *plugin, FuProgress *progress, GError **error) { g_autofree gchar *path = NULL; g_autofree gchar *fn = NULL; g_autofree gchar *str = NULL; g_autoptr(FuFirmware) phat = fu_acpi_phat_new(); g_autoptr(GBytes) blob = NULL; g_autoptr(GError) error_local = NULL; path = fu_path_from_kind(FU_PATH_KIND_ACPI_TABLES); fn = g_build_filename(path, "PHAT", NULL); blob = fu_bytes_get_contents(fn, &error_local); if (blob == NULL) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, error_local->message); return FALSE; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } if (!fu_firmware_parse_bytes(phat, blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error)) return FALSE; str = fu_acpi_phat_to_report_string(FU_ACPI_PHAT(phat)); fu_plugin_add_report_metadata(plugin, "PHAT", str); return TRUE; } static void fu_acpi_phat_plugin_init(FuAcpiPhatPlugin *self) { } static void fu_acpi_phat_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_ACPI_PHAT); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_ACPI_PHAT_HEALTH_RECORD); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_ACPI_PHAT_VERSION_ELEMENT); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_ACPI_PHAT_VERSION_RECORD); } static void fu_acpi_phat_plugin_class_init(FuAcpiPhatPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_acpi_phat_plugin_constructed; plugin_class->coldplug = fu_acpi_phat_plugin_coldplug; } fwupd-2.0.10/plugins/acpi-phat/fu-acpi-phat-plugin.h000066400000000000000000000003641501337203100221670ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuAcpiPhatPlugin, fu_acpi_phat_plugin, FU, ACPI_PHAT_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/acpi-phat/fu-acpi-phat-version-element.c000066400000000000000000000103141501337203100237740ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-acpi-phat-struct.h" #include "fu-acpi-phat-version-element.h" struct _FuAcpiPhatVersionElement { FuFirmware parent_instance; gchar *guid; gchar *producer_id; }; G_DEFINE_TYPE(FuAcpiPhatVersionElement, fu_acpi_phat_version_element, FU_TYPE_FIRMWARE) static void fu_acpi_phat_version_element_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuAcpiPhatVersionElement *self = FU_ACPI_PHAT_VERSION_ELEMENT(firmware); if (self->guid != NULL) fu_xmlb_builder_insert_kv(bn, "guid", self->guid); if (self->producer_id != NULL) fu_xmlb_builder_insert_kv(bn, "producer_id", self->producer_id); } static gboolean fu_acpi_phat_version_element_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuAcpiPhatVersionElement *self = FU_ACPI_PHAT_VERSION_ELEMENT(firmware); g_autoptr(GByteArray) st = NULL; /* unpack */ st = fu_struct_acpi_phat_version_element_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; fu_firmware_set_size(firmware, st->len); self->guid = fwupd_guid_to_string(fu_struct_acpi_phat_version_element_get_component_id(st), FWUPD_GUID_FLAG_MIXED_ENDIAN); self->producer_id = fu_struct_acpi_phat_version_element_get_producer_id(st); fu_firmware_set_version_raw(firmware, fu_struct_acpi_phat_version_element_get_version_value(st)); return TRUE; } static GByteArray * fu_acpi_phat_version_element_write(FuFirmware *firmware, GError **error) { FuAcpiPhatVersionElement *self = FU_ACPI_PHAT_VERSION_ELEMENT(firmware); g_autoptr(GByteArray) st = fu_struct_acpi_phat_version_element_new(); /* pack */ if (self->guid != NULL) { fwupd_guid_t guid = {0x0}; if (!fwupd_guid_from_string(self->guid, &guid, FWUPD_GUID_FLAG_MIXED_ENDIAN, error)) return NULL; fu_struct_acpi_phat_version_element_set_component_id(st, &guid); } fu_struct_acpi_phat_version_element_set_version_value( st, fu_firmware_get_version_raw(firmware)); if (!fu_struct_acpi_phat_version_element_set_producer_id(st, self->producer_id, error)) return NULL; /* success */ return g_steal_pointer(&st); } static void fu_acpi_phat_version_element_set_guid(FuAcpiPhatVersionElement *self, const gchar *guid) { g_free(self->guid); self->guid = g_strdup(guid); } static void fu_acpi_phat_version_element_set_producer_id(FuAcpiPhatVersionElement *self, const gchar *producer_id) { g_free(self->producer_id); self->producer_id = g_strdup(producer_id); } static gboolean fu_acpi_phat_version_element_build(FuFirmware *firmware, XbNode *n, GError **error) { FuAcpiPhatVersionElement *self = FU_ACPI_PHAT_VERSION_ELEMENT(firmware); const gchar *tmp; /* optional properties */ tmp = xb_node_query_text(n, "producer_id", NULL); if (tmp != NULL) fu_acpi_phat_version_element_set_producer_id(self, tmp); tmp = xb_node_query_text(n, "guid", NULL); if (tmp != NULL) fu_acpi_phat_version_element_set_guid(self, tmp); /* success */ return TRUE; } static void fu_acpi_phat_version_element_init(FuAcpiPhatVersionElement *self) { fu_firmware_set_images_max(FU_FIRMWARE(self), 2000); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_NO_AUTO_DETECTION); } static void fu_acpi_phat_version_element_finalize(GObject *object) { FuAcpiPhatVersionElement *self = FU_ACPI_PHAT_VERSION_ELEMENT(object); g_free(self->guid); g_free(self->producer_id); G_OBJECT_CLASS(fu_acpi_phat_version_element_parent_class)->finalize(object); } static void fu_acpi_phat_version_element_class_init(FuAcpiPhatVersionElementClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_acpi_phat_version_element_finalize; firmware_class->parse = fu_acpi_phat_version_element_parse; firmware_class->write = fu_acpi_phat_version_element_write; firmware_class->export = fu_acpi_phat_version_element_export; firmware_class->build = fu_acpi_phat_version_element_build; } FuFirmware * fu_acpi_phat_version_element_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_ACPI_PHAT_VERSION_ELEMENT, NULL)); } fwupd-2.0.10/plugins/acpi-phat/fu-acpi-phat-version-element.h000066400000000000000000000006661501337203100240120ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ACPI_PHAT_VERSION_ELEMENT (fu_acpi_phat_version_element_get_type()) G_DECLARE_FINAL_TYPE(FuAcpiPhatVersionElement, fu_acpi_phat_version_element, FU, ACPI_PHAT_VERSION_ELEMENT, FuFirmware) FuFirmware * fu_acpi_phat_version_element_new(void); fwupd-2.0.10/plugins/acpi-phat/fu-acpi-phat-version-record.c000066400000000000000000000061521501337203100236260ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-acpi-phat-struct.h" #include "fu-acpi-phat-version-element.h" #include "fu-acpi-phat-version-record.h" #include "fu-acpi-phat.h" struct _FuAcpiPhatVersionRecord { FuFirmware parent_instance; }; G_DEFINE_TYPE(FuAcpiPhatVersionRecord, fu_acpi_phat_version_record, FU_TYPE_FIRMWARE) static gboolean fu_acpi_phat_version_record_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { gsize offset = 0; guint32 record_count = 0; g_autoptr(GByteArray) st = NULL; st = fu_struct_acpi_phat_version_record_parse_stream(stream, offset, error); if (st == NULL) return FALSE; record_count = fu_struct_acpi_phat_version_record_get_record_count(st); for (guint32 i = 0; i < record_count; i++) { g_autoptr(FuFirmware) firmware_tmp = fu_acpi_phat_version_element_new(); g_autoptr(GInputStream) stream_tmp = NULL; stream_tmp = fu_partial_input_stream_new(stream, offset + st->len, FU_STRUCT_ACPI_PHAT_VERSION_ELEMENT_SIZE, error); if (stream_tmp == NULL) return FALSE; fu_firmware_set_offset(firmware_tmp, offset + st->len); if (!fu_firmware_parse_stream(firmware_tmp, stream_tmp, 0x0, flags | FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error)) return FALSE; if (!fu_firmware_add_image_full(firmware, firmware_tmp, error)) return FALSE; offset += fu_firmware_get_size(firmware_tmp); } return TRUE; } static GByteArray * fu_acpi_phat_version_record_write(FuFirmware *firmware, GError **error) { g_autoptr(GByteArray) buf2 = g_byte_array_new(); g_autoptr(GByteArray) st = fu_struct_acpi_phat_version_record_new(); g_autoptr(GPtrArray) images = fu_firmware_get_images(firmware); /* write each element so we get the image size */ for (guint i = 0; i < images->len; i++) { FuFirmware *img = g_ptr_array_index(images, i); g_autoptr(GBytes) blob = fu_firmware_write(img, error); if (blob == NULL) return NULL; fu_byte_array_append_bytes(buf2, blob); } /* data record */ fu_struct_acpi_phat_version_record_set_rcdlen(st, st->len + buf2->len); fu_struct_acpi_phat_version_record_set_version(st, fu_firmware_get_version_raw(firmware)); fu_struct_acpi_phat_version_record_set_record_count(st, images->len); /* element data */ g_byte_array_append(st, buf2->data, buf2->len); return g_steal_pointer(&st); } static void fu_acpi_phat_version_record_init(FuAcpiPhatVersionRecord *self) { fu_firmware_set_images_max(FU_FIRMWARE(self), 2000); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_NO_AUTO_DETECTION); g_type_ensure(FU_TYPE_ACPI_PHAT_VERSION_ELEMENT); } static void fu_acpi_phat_version_record_class_init(FuAcpiPhatVersionRecordClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_acpi_phat_version_record_parse; firmware_class->write = fu_acpi_phat_version_record_write; } FuFirmware * fu_acpi_phat_version_record_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_ACPI_PHAT_VERSION_RECORD, NULL)); } fwupd-2.0.10/plugins/acpi-phat/fu-acpi-phat-version-record.h000066400000000000000000000006601501337203100236310ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ACPI_PHAT_VERSION_RECORD (fu_acpi_phat_version_record_get_type()) G_DECLARE_FINAL_TYPE(FuAcpiPhatVersionRecord, fu_acpi_phat_version_record, FU, ACPI_PHAT_VERSION_RECORD, FuFirmware) FuFirmware * fu_acpi_phat_version_record_new(void); fwupd-2.0.10/plugins/acpi-phat/fu-acpi-phat.c000066400000000000000000000233031501337203100206640ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-acpi-phat-health-record.h" #include "fu-acpi-phat-struct.h" #include "fu-acpi-phat-version-record.h" #include "fu-acpi-phat.h" struct _FuAcpiPhat { FuFirmware parent_instance; gchar *oem_id; }; G_DEFINE_TYPE(FuAcpiPhat, fu_acpi_phat, FU_TYPE_FIRMWARE) static void fu_acpi_phat_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuAcpiPhat *self = FU_ACPI_PHAT(firmware); if (self->oem_id != NULL) fu_xmlb_builder_insert_kv(bn, "oem_id", self->oem_id); } static gboolean fu_acpi_phat_record_parse(FuFirmware *firmware, GInputStream *stream, gsize *offset, FuFirmwareParseFlags flags, GError **error) { guint16 record_length = 0; guint16 record_type = 0; guint8 revision; g_autoptr(FuFirmware) firmware_rcd = NULL; /* common header */ if (!fu_input_stream_read_u16(stream, *offset, &record_type, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_input_stream_read_u16(stream, *offset + 2, &record_length, G_LITTLE_ENDIAN, error)) return FALSE; if (record_length < 5) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "PHAT record length invalid, got 0x%x", record_length); return FALSE; } if (!fu_input_stream_read_u8(stream, *offset + 4, &revision, error)) return FALSE; /* firmware version data record */ if (record_type == FU_ACPI_PHAT_RECORD_TYPE_VERSION) { firmware_rcd = fu_acpi_phat_version_record_new(); } else if (record_type == FU_ACPI_PHAT_RECORD_TYPE_HEALTH) { firmware_rcd = fu_acpi_phat_health_record_new(); } /* supported record type */ if (firmware_rcd != NULL) { g_autoptr(GInputStream) partial_stream = NULL; partial_stream = fu_partial_input_stream_new(stream, *offset, record_length, error); if (partial_stream == NULL) return FALSE; fu_firmware_set_size(firmware_rcd, record_length); fu_firmware_set_offset(firmware_rcd, *offset); fu_firmware_set_version_raw(firmware_rcd, revision); if (!fu_firmware_parse_stream(firmware_rcd, partial_stream, 0x0, flags, error)) return FALSE; if (!fu_firmware_add_image_full(firmware, firmware_rcd, error)) return FALSE; } *offset += record_length; return TRUE; } static void fu_acpi_phat_set_oem_id(FuAcpiPhat *self, const gchar *oem_id) { g_free(self->oem_id); self->oem_id = g_strdup(oem_id); } static gboolean fu_acpi_phat_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_acpi_phat_hdr_validate_stream(stream, offset, error); } static gboolean fu_acpi_phat_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuAcpiPhat *self = FU_ACPI_PHAT(firmware); gchar oem_id[6] = {'\0'}; gchar oem_table_id[8] = {'\0'}; gsize streamsz = 0; guint32 length = 0; guint32 oem_revision = 0; g_autofree gchar *oem_id_safe = NULL; g_autofree gchar *oem_table_id_safe = NULL; /* parse table */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (!fu_input_stream_read_u32(stream, 4, &length, G_LITTLE_ENDIAN, error)) return FALSE; if (streamsz < length) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "PHAT table invalid size, got 0x%x, expected 0x%x", (guint)streamsz, length); return FALSE; } /* spec revision */ if ((flags & FWUPD_INSTALL_FLAG_FORCE) == 0) { guint8 revision = 0; if (!fu_input_stream_read_u8(stream, 8, &revision, error)) return FALSE; if (revision != FU_ACPI_PHAT_REVISION) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "PHAT table revision invalid, got 0x%x, expected 0x%x", revision, (guint)FU_ACPI_PHAT_REVISION); return FALSE; } } /* verify checksum */ if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { guint8 checksum = 0; g_autoptr(GInputStream) stream_tmp = fu_partial_input_stream_new(stream, 0, length, error); if (stream_tmp == NULL) return FALSE; if (!fu_input_stream_compute_sum8(stream_tmp, &checksum, error)) return FALSE; if (checksum != 0x00) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "PHAT table checksum invalid, got 0x%x", checksum); return FALSE; } } /* OEMID */ if (!fu_input_stream_read_safe(stream, (guint8 *)oem_id, sizeof(oem_id), 0x0, /* dst */ 10, /* src */ sizeof(oem_id), error)) return FALSE; oem_id_safe = fu_strsafe((const gchar *)oem_id, sizeof(oem_id)); fu_acpi_phat_set_oem_id(self, oem_id_safe); /* OEM Table ID */ if (!fu_input_stream_read_safe(stream, (guint8 *)oem_table_id, sizeof(oem_table_id), 0x0, /* dst */ 16, /* src */ sizeof(oem_table_id), error)) return FALSE; oem_table_id_safe = fu_strsafe((const gchar *)oem_table_id, sizeof(oem_table_id)); fu_firmware_set_id(firmware, oem_table_id_safe); if (!fu_input_stream_read_u32(stream, 24, &oem_revision, G_LITTLE_ENDIAN, error)) return FALSE; fu_firmware_set_version_raw(firmware, oem_revision); /* platform telemetry records */ for (gsize offset_tmp = 36; offset_tmp < length;) { if (!fu_acpi_phat_record_parse(firmware, stream, &offset_tmp, flags, error)) return FALSE; } /* success */ return TRUE; } static GByteArray * fu_acpi_phat_write(FuFirmware *firmware, GError **error) { FuAcpiPhat *self = FU_ACPI_PHAT(firmware); const gchar *oem_table_id_str = fu_firmware_get_id(firmware); guint8 creator_id[] = {'F', 'W', 'U', 'P'}; guint8 creator_rev[] = {'0', '0', '0', '0'}; guint8 oem_id[6] = {'\0'}; guint8 oem_table_id[8] = {'\0'}; guint8 signature[] = {'P', 'H', 'A', 'T'}; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GByteArray) buf2 = g_byte_array_new(); g_autoptr(GPtrArray) images = fu_firmware_get_images(firmware); /* write each image so we get the total size */ for (guint i = 0; i < images->len; i++) { FuFirmware *img = g_ptr_array_index(images, i); g_autoptr(GBytes) blob = fu_firmware_write(img, error); if (blob == NULL) return NULL; fu_byte_array_append_bytes(buf2, blob); } /* header */ g_byte_array_append(buf, signature, sizeof(signature)); fu_byte_array_append_uint32(buf, buf2->len + 36, G_LITTLE_ENDIAN); fu_byte_array_append_uint8(buf, fu_firmware_get_version_raw(firmware)); fu_byte_array_append_uint8(buf, 0xFF); /* will fixup */ if (self->oem_id != NULL) { gsize oem_id_strlen = strlen(self->oem_id); if (!fu_memcpy_safe(oem_id, sizeof(oem_id), 0x0, /* dst */ (const guint8 *)self->oem_id, oem_id_strlen, 0x0, /* src */ oem_id_strlen, error)) return NULL; } g_byte_array_append(buf, oem_id, sizeof(oem_id)); if (oem_table_id_str != NULL) { gsize oem_table_id_strlen = strlen(oem_table_id_str); if (!fu_memcpy_safe(oem_table_id, sizeof(oem_table_id), 0x0, /* dst */ (const guint8 *)oem_table_id_str, oem_table_id_strlen, 0x0, /* src */ oem_table_id_strlen, error)) return NULL; } g_byte_array_append(buf, oem_table_id, sizeof(oem_table_id)); fu_byte_array_append_uint32(buf, fu_firmware_get_version_raw(firmware), G_LITTLE_ENDIAN); g_byte_array_append(buf, creator_id, sizeof(creator_id)); g_byte_array_append(buf, creator_rev, sizeof(creator_rev)); g_byte_array_append(buf, buf2->data, buf2->len); /* fixup checksum */ buf->data[9] = 0xFF - fu_sum8(buf->data, buf->len); /* success */ return g_steal_pointer(&buf); } static gboolean fu_acpi_phat_build(FuFirmware *firmware, XbNode *n, GError **error) { FuAcpiPhat *self = FU_ACPI_PHAT(firmware); const gchar *tmp; /* optional properties */ tmp = xb_node_query_text(n, "oem_id", NULL); if (tmp != NULL) fu_acpi_phat_set_oem_id(self, tmp); /* success */ return TRUE; } static gboolean fu_acpi_phat_to_report_string_cb(XbBuilderNode *bn, gpointer user_data) { if (g_strcmp0(xb_builder_node_get_element(bn), "offset") == 0 || g_strcmp0(xb_builder_node_get_element(bn), "flags") == 0 || g_strcmp0(xb_builder_node_get_element(bn), "size") == 0) xb_builder_node_add_flag(bn, XB_BUILDER_NODE_FLAG_IGNORE); return FALSE; } gchar * fu_acpi_phat_to_report_string(FuAcpiPhat *self) { g_autoptr(XbBuilderNode) bn = xb_builder_node_new("firmware"); fu_firmware_export(FU_FIRMWARE(self), FU_FIRMWARE_EXPORT_FLAG_NONE, bn); xb_builder_node_traverse(bn, G_PRE_ORDER, G_TRAVERSE_ALL, 3, fu_acpi_phat_to_report_string_cb, NULL); return xb_builder_node_export(bn, XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE | XB_NODE_EXPORT_FLAG_FORMAT_INDENT, NULL); } static void fu_acpi_phat_init(FuAcpiPhat *self) { fu_firmware_set_images_max(FU_FIRMWARE(self), 2000); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); g_type_ensure(FU_TYPE_ACPI_PHAT_HEALTH_RECORD); g_type_ensure(FU_TYPE_ACPI_PHAT_VERSION_RECORD); } static void fu_acpi_phat_finalize(GObject *object) { FuAcpiPhat *self = FU_ACPI_PHAT(object); g_free(self->oem_id); G_OBJECT_CLASS(fu_acpi_phat_parent_class)->finalize(object); } static void fu_acpi_phat_class_init(FuAcpiPhatClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_acpi_phat_finalize; firmware_class->validate = fu_acpi_phat_validate; firmware_class->parse = fu_acpi_phat_parse; firmware_class->write = fu_acpi_phat_write; firmware_class->export = fu_acpi_phat_export; firmware_class->build = fu_acpi_phat_build; } FuFirmware * fu_acpi_phat_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_ACPI_PHAT, NULL)); } fwupd-2.0.10/plugins/acpi-phat/fu-acpi-phat.h000066400000000000000000000007731501337203100206770ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ACPI_PHAT (fu_acpi_phat_get_type()) G_DECLARE_FINAL_TYPE(FuAcpiPhat, fu_acpi_phat, FU, ACPI_PHAT, FuFirmware) #define FU_ACPI_PHAT_RECORD_TYPE_VERSION 0x0000 #define FU_ACPI_PHAT_RECORD_TYPE_HEALTH 0x0001 #define FU_ACPI_PHAT_REVISION 0x01 FuFirmware * fu_acpi_phat_new(void); gchar * fu_acpi_phat_to_report_string(FuAcpiPhat *self); fwupd-2.0.10/plugins/acpi-phat/fu-acpi-phat.rs000066400000000000000000000015311501337203100210650ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ValidateStream, Default)] #[repr(C, packed)] struct FuStructAcpiPhatHdr { magic: [char; 4] == "PHAT", } #[derive(New, ParseStream, Default)] #[repr(C, packed)] struct FuStructAcpiPhatHealthRecord { signature: u16le = 0x1, rcdlen: u16le, version: u8, reserved: [u8; 2], flags: u8, device_signature: Guid, device_specific_data: u32le, } #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructAcpiPhatVersionElement { component_id: Guid, version_value: u64le, producer_id: [char; 4], } #[derive(New, ParseStream, Default)] #[repr(C, packed)] struct FuStructAcpiPhatVersionRecord { signature: u16le = 0x0, rcdlen: u16le, version: u8, reserved: [u8; 3], record_count: u32le, } fwupd-2.0.10/plugins/acpi-phat/fu-self-test.c000066400000000000000000000024551501337203100207310ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-acpi-phat.h" static void fu_acpi_phat_parse_func(void) { gboolean ret; g_autoptr(FuFirmware) phat = fu_acpi_phat_new(); g_autoptr(GError) error = NULL; g_autoptr(GBytes) blob = NULL; g_autofree gchar *str = NULL; g_autofree gchar *fn = NULL; fn = g_test_build_filename(G_TEST_DIST, "tests", "PHAT", NULL); if (!g_file_test(fn, G_FILE_TEST_EXISTS)) { g_test_skip("missing PHAT"); return; } blob = fu_bytes_get_contents(fn, &error); g_assert_no_error(error); g_assert_nonnull(blob); ret = fu_firmware_parse_bytes(phat, blob, 0x0, FWUPD_INSTALL_FLAG_FORCE | FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, &error); g_assert_no_error(error); g_assert_true(ret); str = fu_acpi_phat_to_report_string(FU_ACPI_PHAT(phat)); g_print("%s\n", str); } int main(int argc, char **argv) { (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); (void)g_setenv("G_MESSAGES_DEBUG", "all", TRUE); /* tests go here */ g_test_add_func("/acpi-phat/parse", fu_acpi_phat_parse_func); return g_test_run(); } fwupd-2.0.10/plugins/acpi-phat/meson.build000066400000000000000000000024511501337203100204050ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginAcpiPhat"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_builtin_acpi_phat = static_library('fu_plugin_acpi_phat', rustgen.process( 'fu-acpi-phat.rs', # fuzzing ), sources: [ 'fu-acpi-phat-plugin.c', 'fu-acpi-phat.c', # fuzzing 'fu-acpi-phat-health-record.c', # fuzzing 'fu-acpi-phat-version-element.c', # fuzzing 'fu-acpi-phat-version-record.c', # fuzzing ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) plugin_builtins += plugin_builtin_acpi_phat if get_option('tests') env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) e = executable( 'acpi-phat-self-test', sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: plugin_deps, link_with: [ plugin_libs, plugin_builtin_acpi_phat, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('acpi-phat-self-test', e, env: env) # added to installed-tests endif endif fwupd-2.0.10/plugins/acpi-phat/tests/000077500000000000000000000000001501337203100174035ustar00rootroot00000000000000fwupd-2.0.10/plugins/acpi-phat/tests/.gitignore000066400000000000000000000000051501337203100213660ustar00rootroot00000000000000PHAT fwupd-2.0.10/plugins/acpi-phat/tests/acpi-phat.builder.xml000066400000000000000000000022761501337203100234270ustar00rootroot00000000000000 SUDODAVE 0x1 HUGHES 0x1 0x123 40338ceb-b966-4eae-adae-9c32edfcc484 HUGH 0x124 2082b5e0-7a64-478a-b1b2-e3404fab6dad LENO 0x1 0x125 dfbaaded-754b-5214-a5f2-46aa3331e8ce DELL 0x1 0x1 0x1 dfbaaded-754b-5214-a5f2-46aa3331e8ce /dev/foo fwupd-2.0.10/plugins/algoltek-usb/000077500000000000000000000000001501337203100167645ustar00rootroot00000000000000fwupd-2.0.10/plugins/algoltek-usb/README.md000066400000000000000000000030061501337203100202420ustar00rootroot00000000000000--- title: Plugin: Algoltek USB --- ## Introduction This plugin supports the firmware upgrade of DisplayPort over USB-C to HDMI converter provided by Algoltek, Inc. These DisplayPort over USB-C to HDMI converters can be updated through multiple interfaces, but this plugin is only designed for the USB interface. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a packed binary file format. This plugin supports the following protocol ID: * `tw.com.algoltek.usb` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_25A4&PID_9311` * `USB\VID_25A4&PID_9312` * `USB\VID_25A4&PID_9313` * `USB\VID_25A4&PID_9411` * `USB\VID_25A4&PID_9421` ## Update Behavior The firmware is deployed when the device is in normal runtime mode, and the device will reset when the new firmware has been programmed. ## Quirk Use This plugin uses the following plugin-specific quirks: ### `Flags=ers-skip-first-sector` Skip erasing the first sector, needed for AG9411 and AG9421 products. Since: 2.0.2 ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x25A4`. ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.9.11`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: Mason Lyu: @MasonLyu fwupd-2.0.10/plugins/algoltek-usb/algoltek-usb.quirk000066400000000000000000000006321501337203100224330ustar00rootroot00000000000000# AG9311 [USB\VID_25A4&PID_9311] Plugin = algoltek_usb # AG9312 [USB\VID_25A4&PID_9312] Plugin = algoltek_usb # AG9313 [USB\VID_25A4&PID_9313] Plugin = algoltek_usb # AG9411 [USB\VID_25A4&PID_9411] Plugin = algoltek_usb Flags = ers-skip-first-sector # AG9421 [USB\VID_25A4&PID_9421] Plugin = algoltek_usb Flags = ers-skip-first-sector # j5create USB-C JCD373 [USB\VID_2DE5&PID_373E] Plugin = algoltek_usb fwupd-2.0.10/plugins/algoltek-usb/fu-algoltek-usb-common.h000066400000000000000000000006131501337203100234240ustar00rootroot00000000000000/* * Copyright 2024 Algoltek, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define ALGOLTEK_DEVICE_USB_TIMEOUT 3000 /* ms */ #define AG_ISP_ADDR 0x2000 #define AG_ISP_SIZE 0x1000 #define AG_FIRMWARE_SIZE 0x20000 #define AG_UPDATE_STATUS 0x860C #define AG_UPDATE_PASS 1 #define AG_UPDATE_FAIL 2 #define AG_IDENTIFICATION_128K_ADDR 31 fwupd-2.0.10/plugins/algoltek-usb/fu-algoltek-usb-device.c000066400000000000000000000414211501337203100233700ustar00rootroot00000000000000/* * Copyright 2024 Algoltek, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-algoltek-usb-common.h" #include "fu-algoltek-usb-device.h" #include "fu-algoltek-usb-firmware.h" #include "fu-algoltek-usb-struct.h" struct _FuAlgoltekUsbDevice { FuUsbDevice parent_instance; }; G_DEFINE_TYPE(FuAlgoltekUsbDevice, fu_algoltek_usb_device, FU_TYPE_USB_DEVICE) #define FU_ALGOLTEK_USB_DEVICE_FLAG_ERS_SKIP_FIRST_SECTOR "ers-skip-first-sector" static gboolean fu_algoltek_usb_device_ctrl_transfer(FuAlgoltekUsbDevice *self, FuUsbDirection direction, FuAlgoltekCmd algoltek_cmd, guint16 value, guint16 index, GByteArray *buf, guint8 len, GError **error) { return fu_usb_device_control_transfer(FU_USB_DEVICE(self), direction, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_INTERFACE, algoltek_cmd, value, index, buf->data, len, NULL, ALGOLTEK_DEVICE_USB_TIMEOUT, NULL, error); } static GByteArray * fu_algoltek_usb_device_rdr(FuAlgoltekUsbDevice *self, int address, GError **error) { g_autoptr(GByteArray) st = fu_struct_algoltek_cmd_address_pkt_new(); fu_struct_algoltek_cmd_address_pkt_set_len(st, 5); fu_struct_algoltek_cmd_address_pkt_set_cmd(st, FU_ALGOLTEK_CMD_RDR); fu_struct_algoltek_cmd_address_pkt_set_address(st, address); fu_struct_algoltek_cmd_address_pkt_set_checksum(st, ~fu_sum8(st->data, st->len) + 1); if (!fu_algoltek_usb_device_ctrl_transfer(self, FU_USB_DIRECTION_DEVICE_TO_HOST, FU_ALGOLTEK_CMD_RDR, address, 0xFFFF, st, st->len, error)) return NULL; /* success */ return g_steal_pointer(&st); } static GByteArray * fu_algoltek_usb_device_rdv(FuAlgoltekUsbDevice *self, GError **error) { guint16 version_prefix; g_autoptr(GByteArray) st = fu_struct_algoltek_cmd_transfer_pkt_new(); g_autoptr(GByteArray) version_data = g_byte_array_new(); fu_struct_algoltek_cmd_transfer_pkt_set_len(st, 3); fu_struct_algoltek_cmd_transfer_pkt_set_cmd(st, FU_ALGOLTEK_CMD_RDV); fu_struct_algoltek_cmd_transfer_pkt_set_checksum(st, ~fu_sum8(st->data, st->len) + 1); if (!fu_algoltek_usb_device_ctrl_transfer(self, FU_USB_DIRECTION_DEVICE_TO_HOST, FU_ALGOLTEK_CMD_RDV, 0xFFFF, 0xFFFF, st, st->len, error)) return NULL; if (!fu_memread_uint16_safe(st->data, st->len, 2, &version_prefix, G_BIG_ENDIAN, error)) return NULL; if (version_prefix == 0x4147) { guint8 underscore_count = 0; /* remove len, cmd bytes and "AG" prefixes */ for (guint32 i = 4; i < st->len; i++) { if (st->data[i] == '_') { underscore_count++; if (underscore_count == 1) continue; } if (underscore_count > 2) break; if (underscore_count > 0) fu_byte_array_append_uint8(version_data, st->data[i]); } } else { /* remove len and cmd bytes */ for (guint32 i = 2; i < st->len; i++) { if (st->data[i] < 128) fu_byte_array_append_uint8(version_data, st->data[i]); } } /* success */ return g_steal_pointer(&version_data); } static gboolean fu_algoltek_usb_device_en(FuAlgoltekUsbDevice *self, GError **error) { g_autoptr(GByteArray) st = fu_struct_algoltek_cmd_address_pkt_new(); fu_struct_algoltek_cmd_address_pkt_set_len(st, 3); fu_struct_algoltek_cmd_address_pkt_set_cmd(st, FU_ALGOLTEK_CMD_EN); fu_struct_algoltek_cmd_address_pkt_set_checksum(st, ~fu_sum8(st->data, st->len) + 1); if (!fu_algoltek_usb_device_ctrl_transfer(self, FU_USB_DIRECTION_HOST_TO_DEVICE, FU_ALGOLTEK_CMD_EN, 0, 0, st, st->data[0], error)) { g_prefix_error(error, "system activation failure: "); return FALSE; } return TRUE; } static gboolean fu_algoltek_usb_device_rst(FuAlgoltekUsbDevice *self, guint16 address, GError **error) { g_autoptr(GByteArray) st = fu_struct_algoltek_cmd_address_pkt_new(); fu_struct_algoltek_cmd_address_pkt_set_len(st, 4); fu_struct_algoltek_cmd_address_pkt_set_cmd(st, FU_ALGOLTEK_CMD_RST); fu_struct_algoltek_cmd_address_pkt_set_address(st, address); fu_struct_algoltek_cmd_address_pkt_set_checksum(st, ~fu_sum8(st->data, st->len) + 1); if (st->data[0] > st->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "rst length invalid, 0x%x > 0x%x", st->data[0], st->len); return FALSE; } if (!fu_algoltek_usb_device_ctrl_transfer(self, FU_USB_DIRECTION_HOST_TO_DEVICE, FU_ALGOLTEK_CMD_RST, 0, 0, st, st->data[0], error)) { g_prefix_error(error, "system reboot failure: "); return FALSE; } return TRUE; } static gboolean fu_algoltek_usb_device_wrr(FuAlgoltekUsbDevice *self, int address, int value, GError **error) { g_autoptr(GByteArray) st = fu_struct_algoltek_cmd_address_pkt_new(); fu_struct_algoltek_cmd_address_pkt_set_len(st, 7); fu_struct_algoltek_cmd_address_pkt_set_cmd(st, FU_ALGOLTEK_CMD_WRR); fu_struct_algoltek_cmd_address_pkt_set_address(st, address); fu_struct_algoltek_cmd_address_pkt_set_value(st, value); fu_struct_algoltek_cmd_address_pkt_set_checksum(st, ~fu_sum8(st->data, st->len) + 1); if (st->data[0] > st->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "wrr length invalid, 0x%x > 0x%x", st->data[0], st->len); return FALSE; } if (!fu_algoltek_usb_device_ctrl_transfer(self, FU_USB_DIRECTION_HOST_TO_DEVICE, FU_ALGOLTEK_CMD_WRR, 0, 0, st, st->data[0], error)) { g_prefix_error(error, "data write failure: "); return FALSE; } return TRUE; } static gboolean fu_algoltek_usb_device_isp(FuAlgoltekUsbDevice *self, GInputStream *stream, guint address, FuProgress *progress, GError **error) { guint8 basic_data_size = 5; g_autoptr(FuChunkArray) chunks = NULL; chunks = fu_chunk_array_new_from_stream(stream, address, FU_CHUNK_PAGESZ_NONE, 64 - basic_data_size, error); if (chunks == NULL) return FALSE; fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GByteArray) st = fu_struct_algoltek_cmd_transfer_pkt_new(); chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; fu_struct_algoltek_cmd_transfer_pkt_set_len(st, basic_data_size + fu_chunk_get_data_sz(chk)); fu_struct_algoltek_cmd_transfer_pkt_set_cmd(st, FU_ALGOLTEK_CMD_ISP); fu_struct_algoltek_cmd_transfer_pkt_set_address(st, fu_chunk_get_address(chk)); if (!fu_struct_algoltek_cmd_transfer_pkt_set_data(st, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "assign isp data failure: "); return FALSE; } fu_struct_algoltek_cmd_transfer_pkt_set_checksum(st, ~fu_sum8(st->data, st->len) + 1); if (st->data[0] > st->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "isp length invalid, 0x%x > 0x%x", st->data[0], st->len); return FALSE; } if (!fu_algoltek_usb_device_ctrl_transfer(self, FU_USB_DIRECTION_HOST_TO_DEVICE, FU_ALGOLTEK_CMD_ISP, 0, 0, st, st->data[0], error)) { g_prefix_error(error, "isp failure: "); return FALSE; } fu_progress_step_done(progress); } return TRUE; } static gboolean fu_algoltek_usb_device_bot(FuAlgoltekUsbDevice *self, int address, GError **error) { g_autoptr(GByteArray) st = fu_struct_algoltek_cmd_address_pkt_new(); fu_struct_algoltek_cmd_address_pkt_set_len(st, 5); fu_struct_algoltek_cmd_address_pkt_set_cmd(st, FU_ALGOLTEK_CMD_BOT); fu_struct_algoltek_cmd_address_pkt_set_address(st, address); fu_struct_algoltek_cmd_address_pkt_set_checksum(st, ~fu_sum8(st->data, st->len) + 1); if (st->data[0] > st->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "bot length invalid, 0x%x > 0x%x", st->data[0], st->len); return FALSE; } if (!fu_algoltek_usb_device_ctrl_transfer(self, FU_USB_DIRECTION_HOST_TO_DEVICE, FU_ALGOLTEK_CMD_BOT, 0, 0, st, st->data[0], error)) { g_prefix_error(error, "system boot failure: "); return FALSE; } return TRUE; } static gboolean fu_algoltek_usb_device_ers(FuAlgoltekUsbDevice *self, guint erase_type, guint8 sector, GError **error) { guint16 value; g_autoptr(GByteArray) st = fu_struct_algoltek_cmd_address_pkt_new(); fu_struct_algoltek_cmd_address_pkt_set_len(st, 3); fu_struct_algoltek_cmd_address_pkt_set_cmd(st, FU_ALGOLTEK_CMD_ERS); fu_struct_algoltek_cmd_address_pkt_set_checksum(st, ~fu_sum8(st->data, st->len) + 1); value = (erase_type << 8) | sector; if (!fu_algoltek_usb_device_ctrl_transfer(self, FU_USB_DIRECTION_HOST_TO_DEVICE, FU_ALGOLTEK_CMD_ERS, value, 0, st, st->len, error)) { g_prefix_error(error, "data clear failure: "); return FALSE; } return TRUE; } static gboolean fu_algoltek_usb_device_status_check_cb(FuDevice *self, gpointer user_data, GError **error) { guint8 update_status; g_autoptr(GByteArray) update_status_array = NULL; update_status_array = fu_algoltek_usb_device_rdr(FU_ALGOLTEK_USB_DEVICE(self), AG_UPDATE_STATUS, error); if (update_status_array == NULL) return FALSE; update_status = update_status_array->data[0]; switch (update_status) { case AG_UPDATE_PASS: break; case AG_UPDATE_FAIL: default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "update procedure is failed."); return FALSE; } return TRUE; } static gboolean fu_algoltek_usb_device_wrf(FuAlgoltekUsbDevice *self, GInputStream *stream, FuProgress *progress, GError **error) { g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(GByteArray) buf_parameter = g_byte_array_new(); chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 64, error); if (chunks == NULL) return FALSE; fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { guint16 value; guint16 index; g_autoptr(FuChunk) chk = NULL; g_autoptr(GByteArray) buf = g_byte_array_new(); chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; g_byte_array_append(buf, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); fu_byte_array_set_size(buf_parameter, 4, 0); if ((i + 1) % 4 == 0) buf_parameter->data[0] = 1; else buf_parameter->data[0] = 0; fu_memwrite_uint24(buf_parameter->data + 1, fu_chunk_get_address(chk), G_BIG_ENDIAN); value = fu_memread_uint16(buf_parameter->data, G_BIG_ENDIAN); index = fu_memread_uint16(buf_parameter->data + 2, G_BIG_ENDIAN); if (!fu_algoltek_usb_device_ctrl_transfer(self, FU_USB_DIRECTION_HOST_TO_DEVICE, FU_ALGOLTEK_CMD_WRF, value, index, buf, buf->len, error)) { g_prefix_error(error, "data write failure: "); return FALSE; } if ((i + 1) % 4 == 0 || (i + 1) == fu_chunk_array_length(chunks)) { if (!fu_device_retry_full(FU_DEVICE(self), fu_algoltek_usb_device_status_check_cb, 10, 0, NULL, error)) return FALSE; } fu_progress_step_done(progress); } return TRUE; } static gboolean fu_algoltek_usb_device_setup(FuDevice *device, GError **error) { FuAlgoltekUsbDevice *self = FU_ALGOLTEK_USB_DEVICE(device); g_autofree gchar *version_str = NULL; g_autoptr(GByteArray) version_data = NULL; /* UsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_algoltek_usb_device_parent_class)->setup(device, error)) return FALSE; version_data = fu_algoltek_usb_device_rdv(self, error); if (version_data == NULL) return FALSE; version_str = fu_strsafe((const gchar *)version_data->data, version_data->len); fu_device_set_version(device, version_str); /* success */ return TRUE; } static gboolean fu_algoltek_usb_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuAlgoltekUsbDevice *self = FU_ALGOLTEK_USB_DEVICE(device); g_autoptr(GInputStream) stream_isp = NULL; g_autoptr(GInputStream) stream_payload = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 18, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 2, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 80, NULL); if (!fu_algoltek_usb_device_en(self, error)) return FALSE; if (!fu_algoltek_usb_device_rst(self, 0x200, error)) return FALSE; fu_device_sleep(FU_DEVICE(self), 900); if (!fu_algoltek_usb_device_wrr(self, 0x80AD, 0, error)) return FALSE; if (!fu_algoltek_usb_device_wrr(self, 0x80C0, 0, error)) return FALSE; if (!fu_algoltek_usb_device_wrr(self, 0x80C9, 0, error)) return FALSE; if (!fu_algoltek_usb_device_wrr(self, 0x80D1, 0, error)) return FALSE; if (!fu_algoltek_usb_device_wrr(self, 0x80D9, 0, error)) return FALSE; if (!fu_algoltek_usb_device_wrr(self, 0x80E1, 0, error)) return FALSE; if (!fu_algoltek_usb_device_wrr(self, 0x80E9, 0, error)) return FALSE; if (!fu_algoltek_usb_device_rst(self, 0, error)) return FALSE; fu_device_sleep(FU_DEVICE(self), 500); /* get ISP image */ stream_isp = fu_firmware_get_image_by_id_stream(firmware, "isp", error); if (stream_isp == NULL) return FALSE; if (!fu_algoltek_usb_device_isp(self, stream_isp, AG_ISP_ADDR, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); if (!fu_algoltek_usb_device_bot(self, AG_ISP_ADDR, error)) return FALSE; fu_device_sleep(FU_DEVICE(self), 1000); if (!fu_algoltek_usb_device_ers(self, 0x20, AG_IDENTIFICATION_128K_ADDR, error)) return FALSE; /* preserves compatibility with existing emulation data */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { if (!fu_algoltek_usb_device_ers(self, 0x20, 63, error)) return FALSE; for (guint i = 0; i < 64; i++) { if (!fu_algoltek_usb_device_ers(self, 0x20, i, error)) return FALSE; } } else if (fu_device_has_private_flag(device, FU_ALGOLTEK_USB_DEVICE_FLAG_ERS_SKIP_FIRST_SECTOR)) { /* 1 sector = 4 kb, 128kb = 32 sector */ for (int i = 1; i < 31; i++) { if (!fu_algoltek_usb_device_ers(self, 0x20, i, error)) return FALSE; } } else { if (!fu_algoltek_usb_device_ers(self, 0x60, 0, error)) return FALSE; } fu_progress_step_done(progress); fu_device_sleep(FU_DEVICE(self), 500); /* get payload image */ stream_payload = fu_firmware_get_image_by_id_stream(firmware, FU_FIRMWARE_ID_PAYLOAD, error); if (stream_payload == NULL) return FALSE; if (!fu_algoltek_usb_device_wrf(self, stream_payload, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); if (!fu_algoltek_usb_device_rst(self, 0x100, error)) return FALSE; /* the device automatically reboots */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* success! */ return TRUE; } static void fu_algoltek_usb_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_algoltek_usb_device_init(FuAlgoltekUsbDevice *self) { fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); fu_device_add_protocol(FU_DEVICE(self), "tw.com.algoltek.usb"); fu_device_register_private_flag(FU_DEVICE(self), FU_ALGOLTEK_USB_DEVICE_FLAG_ERS_SKIP_FIRST_SECTOR); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_ALGOLTEK_USB_FIRMWARE); fu_device_set_remove_delay(FU_DEVICE(self), 10000); } static void fu_algoltek_usb_device_class_init(FuAlgoltekUsbDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_algoltek_usb_device_setup; device_class->write_firmware = fu_algoltek_usb_device_write_firmware; device_class->set_progress = fu_algoltek_usb_device_set_progress; } fwupd-2.0.10/plugins/algoltek-usb/fu-algoltek-usb-device.h000066400000000000000000000005161501337203100233750ustar00rootroot00000000000000/* * Copyright 2024 Algoltek, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ALGOLTEK_USB_DEVICE (fu_algoltek_usb_device_get_type()) G_DECLARE_FINAL_TYPE(FuAlgoltekUsbDevice, fu_algoltek_usb_device, FU, ALGOLTEK_USB_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/algoltek-usb/fu-algoltek-usb-firmware.c000066400000000000000000000057771501337203100237630ustar00rootroot00000000000000/* * Copyright 2024 Algoltek, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-algoltek-usb-common.h" #include "fu-algoltek-usb-firmware.h" #include "fu-algoltek-usb-struct.h" struct _FuAlgoltekUsbFirmware { FuFirmware parent_instance; }; G_DEFINE_TYPE(FuAlgoltekUsbFirmware, fu_algoltek_usb_firmware, FU_TYPE_FIRMWARE) static gboolean fu_algoltek_usb_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_algoltek_product_identity_validate_stream(stream, offset, error); } static gboolean fu_algoltek_usb_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { g_autofree gchar *version = NULL; gsize offset = 0; g_autoptr(FuFirmware) img_isp = fu_firmware_new(); g_autoptr(FuFirmware) img_payload = fu_firmware_new(); g_autoptr(GByteArray) st = NULL; g_autoptr(GInputStream) stream_isp = NULL; g_autoptr(GInputStream) stream_payload = NULL; /* identity */ st = fu_struct_algoltek_product_identity_parse_stream(stream, offset, error); if (st == NULL) return FALSE; version = fu_struct_algoltek_product_identity_get_version(st); offset += FU_STRUCT_ALGOLTEK_PRODUCT_IDENTITY_SIZE; /* ISP */ stream_isp = fu_partial_input_stream_new(stream, offset, AG_ISP_SIZE, error); if (stream_isp == NULL) return FALSE; if (!fu_firmware_parse_stream(img_isp, stream_isp, 0x0, flags, error)) return FALSE; fu_firmware_set_id(img_isp, "isp"); fu_firmware_add_image(firmware, img_isp); offset += AG_ISP_SIZE; /* payload */ stream_payload = fu_partial_input_stream_new(stream, offset, AG_FIRMWARE_SIZE, error); if (stream_payload == NULL) return FALSE; if (!fu_firmware_parse_stream(img_payload, stream_payload, 0x0, flags, error)) return FALSE; fu_firmware_set_version(img_payload, version); fu_firmware_set_id(img_payload, FU_FIRMWARE_ID_PAYLOAD); fu_firmware_add_image(firmware, img_payload); /* success */ return TRUE; } static GByteArray * fu_algoltek_usb_firmware_write(FuFirmware *firmware, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GBytes) blob_isp = NULL; g_autoptr(GBytes) blob_payload = NULL; blob_isp = fu_firmware_get_image_by_id_bytes(firmware, "isp", error); if (blob_isp == NULL) return NULL; fu_byte_array_append_bytes(buf, blob_isp); blob_payload = fu_firmware_get_image_by_id_bytes(firmware, FU_FIRMWARE_ID_PAYLOAD, error); if (blob_payload == NULL) return NULL; fu_byte_array_append_bytes(buf, blob_payload); return g_steal_pointer(&buf); } static void fu_algoltek_usb_firmware_init(FuAlgoltekUsbFirmware *self) { } static void fu_algoltek_usb_firmware_class_init(FuAlgoltekUsbFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_algoltek_usb_firmware_validate; firmware_class->parse = fu_algoltek_usb_firmware_parse; firmware_class->write = fu_algoltek_usb_firmware_write; } fwupd-2.0.10/plugins/algoltek-usb/fu-algoltek-usb-firmware.h000066400000000000000000000005271501337203100237540ustar00rootroot00000000000000/* * Copyright 2024 Algoltek, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ALGOLTEK_USB_FIRMWARE (fu_algoltek_usb_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuAlgoltekUsbFirmware, fu_algoltek_usb_firmware, FU, ALGOLTEK_USB_FIRMWARE, FuFirmware) fwupd-2.0.10/plugins/algoltek-usb/fu-algoltek-usb-plugin.c000066400000000000000000000015561501337203100234340ustar00rootroot00000000000000/* * Copyright 2024 Algoltek, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-algoltek-usb-device.h" #include "fu-algoltek-usb-firmware.h" #include "fu-algoltek-usb-plugin.h" struct _FuAlgoltekUsbPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuAlgoltekUsbPlugin, fu_algoltek_usb_plugin, FU_TYPE_PLUGIN) static void fu_algoltek_usb_plugin_init(FuAlgoltekUsbPlugin *self) { } static void fu_algoltek_usb_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_ALGOLTEK_USB_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_ALGOLTEK_USB_FIRMWARE); } static void fu_algoltek_usb_plugin_class_init(FuAlgoltekUsbPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_algoltek_usb_plugin_constructed; } fwupd-2.0.10/plugins/algoltek-usb/fu-algoltek-usb-plugin.h000066400000000000000000000003471501337203100234360ustar00rootroot00000000000000/* * Copyright 2024 Algoltek, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuAlgoltekUsbPlugin, fu_algoltek_usb_plugin, FU, ALGOLTEK_USB_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/algoltek-usb/fu-algoltek-usb.rs000066400000000000000000000015541501337203100223400ustar00rootroot00000000000000// Copyright 2024 Algoltek, Inc. // SPDX-License-Identifier: LGPL-2.1-or-later enum FuAlgoltekCmd { Rdr = 0x06, Wrr, Rdv, En, Wrf = 0x10, Isp = 0x13, Ers = 0x19, Bot = 0x1D, Rst = 0x20, } #[derive(ParseStream, ValidateStream, Default)] #[repr(C, packed)] struct FuStructAlgoltekProductIdentity { header_len: u8, header: u64le == 0x4B45544C4F474C41, // 'A' 'L' 'G' 'O' 'L' 'T' 'E' 'K' product_name_len: u8, product_name: [char; 16], version_len: u8, version: [char; 48], } #[derive(New)] #[repr(C, packed)] struct FuStructAlgoltekCmdAddressPkt { len: u8, cmd: u8, address: u16be, value: u16be, reserved: [u8; 4], checksum: u8, } #[derive(New)] #[repr(C, packed)] struct FuStructAlgoltekCmdTransferPkt { len: u8, cmd: u8, address: u16be, data: [u8; 61], checksum: u8, } fwupd-2.0.10/plugins/algoltek-usb/meson.build000066400000000000000000000010721501337203100211260ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginAlgoltekUsb"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('algoltek-usb.quirk') plugin_builtins += static_library('fu_plugin_algoltek_usb', rustgen.process('fu-algoltek-usb.rs'), sources: [ 'fu-algoltek-usb-device.c', 'fu-algoltek-usb-firmware.c', 'fu-algoltek-usb-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files( 'tests/algoltek-ag9421.json', ) fwupd-2.0.10/plugins/algoltek-usb/tests/000077500000000000000000000000001501337203100201265ustar00rootroot00000000000000fwupd-2.0.10/plugins/algoltek-usb/tests/algoltek-ag9421.json000066400000000000000000000016751501337203100235410ustar00rootroot00000000000000{ "name": "Algoltek AG9421", "interactive": false, "steps": [ { "url": "60518fa0f9a78a38afda311e8a08f21995dc21020f16cd3b959c159f1ee360ca-algoltek-ag9421-00.09.07_02.18.05.cab", "emulation-url": "6d8137ff38bed4c60f0ed31cba15fca023827b181be57cfd753a5751ce9c2d0d-algoltek-usb_00.09.07_02.18.05_gitMain.zip", "components": [ { "version": "00.09.07_02.18.05", "guids": [ "b5edd38c-b9f3-5917-819a-0947612e4d7e" ] } ] }, { "url": "6984d9c2e03ddc9f12879565587c2f42ded40209d0434f2f269208213f052dfd-algoltek-ag9421-00.09.14_03.00.00.cab", "emulation-url": "da2c904be60988cc200f1ba0920295913981bb58053a33deba5a9260caf00f46-algoltek-usb_00.09.14_03.00.00_gitMain.zip", "components": [ { "version": "00.09.14_03.00.00", "guids": [ "b5edd38c-b9f3-5917-819a-0947612e4d7e" ] } ] } ] } fwupd-2.0.10/plugins/algoltek-usbcr/000077500000000000000000000000001501337203100173115ustar00rootroot00000000000000fwupd-2.0.10/plugins/algoltek-usbcr/README.md000066400000000000000000000015571501337203100206000ustar00rootroot00000000000000--- title: Plugin: Algoltek Usbcr --- ## Introduction This plugin supports firmware upgrade for USB card reader products provided by Algoltek, Inc. ## Firmware Format This plugin supports the following protocol ID: * `com.algoltek.usbcr` ## GUID Generation These devices use the standard UDEV DeviceInstanceId values, e.g. * `[BLOCK\VEN_058F&DEV_8461]` ## Update Behavior The firmware is deployed when the device is in normal runtime mode, and the device will reset when the new firmware has been programmed. ## Vendor ID Security The vendor ID is set from the udev vendor, in this instance set to `BLOCK:0x058F` ## External Interface Access This plugin requires read/write access to `/dev/sd*` block devices and requires using a `sg_io ioctl` for interaction with the device. ## Version Considerations This plugin has been available since fwupd version `2.0.0`. fwupd-2.0.10/plugins/algoltek-usbcr/algoltek-usbcr.quirk000066400000000000000000000002661501337203100233100ustar00rootroot00000000000000# AU84610 [BLOCK\VEN_058F&DEV_8461] Plugin = algoltek_usbcr # AU84612 [BLOCK\VEN_058F&DEV_8468] Plugin = algoltek_usbcr # AU84616 [BLOCK\VEN_058F&DEV_8466] Plugin = algoltek_usbcr fwupd-2.0.10/plugins/algoltek-usbcr/fu-algoltek-usbcr-common.h000066400000000000000000000011461501337203100243000ustar00rootroot00000000000000/* * Copyright 2024 Algoltek, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_AG_USBCR_MAX_CDB_LEN 16 #define FU_AG_USBCR_MAX_BUFFER_SIZE 512 #define FU_AG_USBCR_SENSE_BUFFER_SIZE 18 #define FU_AG_USBCR_IOCTL_TIMEOUT_MS 20000 #define FU_AG_USBCR_RD_WR_RAM 0x84 #define FU_AG_USBCR_RD_WR_XDATA 0x03 #define FU_AG_SPIFLASH_VALID 0x40 #define FU_AG_SPECIFY_SPI_CMD_SIG_1 0x05 #define FU_AG_SPECIFY_SPI_CMD_SIG_2 0x8F #define FU_AG_SPECIFY_EEPROM_TYPE_TAG 0xA5 #define FU_AG_BLOCK_MODE_DISEN 0x00 #define FU_AG_BLOCK_MODE_EN 0x01 fwupd-2.0.10/plugins/algoltek-usbcr/fu-algoltek-usbcr-device.c000066400000000000000000000453001501337203100242420ustar00rootroot00000000000000/* * Copyright 2024 Algoltek, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-algoltek-usbcr-common.h" #include "fu-algoltek-usbcr-device.h" #include "fu-algoltek-usbcr-firmware.h" #include "fu-algoltek-usbcr-struct.h" struct _FuAlgoltekUsbcrDevice { FuBlockDevice parent_instance; }; G_DEFINE_TYPE(FuAlgoltekUsbcrDevice, fu_algoltek_usbcr_device, FU_TYPE_BLOCK_DEVICE) typedef struct { guint16 reg; guint8 val; } FuAgUsbcrRegSetup; static GByteArray * fu_algoltek_usbcr_device_cmd_get_ver(FuAlgoltekUsbcrDevice *self, GError **error) { guint8 cdb[FU_AG_USBCR_MAX_CDB_LEN] = {0}; g_autoptr(GByteArray) buf = g_byte_array_new(); cdb[0] = FU_AG_USBCR_SCSIOP_VENDOR_FIRMWARE_REVISION; fu_byte_array_set_size(buf, FU_AG_USBCR_MAX_BUFFER_SIZE, 0x0); if (!fu_block_device_sg_io_cmd_read(FU_BLOCK_DEVICE(self), cdb, sizeof(cdb), buf->data, buf->len, error)) return NULL; return g_steal_pointer(&buf); } static gboolean fu_algoltek_usbcr_device_write_reg(FuAlgoltekUsbcrDevice *self, guint16 addr, guint8 value, guint8 ram_dest, GError **error) { g_autoptr(GByteArray) st = fu_struct_ag_usbcr_reg_cdb_new(); fu_struct_ag_usbcr_reg_cdb_set_cmd(st, FU_AG_USBCR_SCSIOP_VENDOR_GENERIC_CMD); fu_struct_ag_usbcr_reg_cdb_set_subcmd(st, FU_AG_USBCR_RD_WR_RAM); fu_struct_ag_usbcr_reg_cdb_set_ramdest(st, ram_dest); fu_struct_ag_usbcr_reg_cdb_set_addr(st, addr); fu_struct_ag_usbcr_reg_cdb_set_val(st, value); return fu_block_device_sg_io_cmd_none(FU_BLOCK_DEVICE(self), st->data, st->len, error); } static gboolean fu_algoltek_usbcr_device_read_reg(FuAlgoltekUsbcrDevice *self, guint16 addr, guint8 *buf, guint16 bufsz, guint8 ram_dest, GError **error) { g_autoptr(GByteArray) st = fu_struct_ag_usbcr_reg_cdb_new(); fu_struct_ag_usbcr_reg_cdb_set_cmd(st, FU_AG_USBCR_SCSIOP_VENDOR_GENERIC_CMD); fu_struct_ag_usbcr_reg_cdb_set_subcmd(st, FU_AG_USBCR_RD_WR_RAM); fu_struct_ag_usbcr_reg_cdb_set_ramdest(st, ram_dest); fu_struct_ag_usbcr_reg_cdb_set_addr(st, addr); return fu_block_device_sg_io_cmd_read(FU_BLOCK_DEVICE(self), st->data, st->len, buf, bufsz, error); } static gboolean fu_algoltek_usbcr_device_send_spi_cmd(FuAlgoltekUsbcrDevice *self, guint8 cmd, GError **error) { guint8 buf[8] = {0}; g_autoptr(GByteArray) st = fu_struct_ag_usbcr_spi_cdb_new(); fu_struct_ag_usbcr_spi_cdb_set_cmd(st, FU_AG_USBCR_SCSIOP_VENDOR_EEPROM_WR); fu_struct_ag_usbcr_spi_cdb_set_addr(st, 0xFFFF); fu_struct_ag_usbcr_spi_cdb_set_bufsz(st, sizeof(buf) & 0xFF); fu_struct_ag_usbcr_spi_cdb_set_tag(st, FU_AG_SPECIFY_EEPROM_TYPE_TAG); fu_struct_ag_usbcr_spi_cdb_set_valid(st, FU_AG_SPIFLASH_VALID); fu_struct_ag_usbcr_spi_cdb_set_spisig1(st, FU_AG_SPECIFY_SPI_CMD_SIG_1); fu_struct_ag_usbcr_spi_cdb_set_spisig2(st, FU_AG_SPECIFY_SPI_CMD_SIG_2); fu_struct_ag_usbcr_spi_cdb_set_spicmd(st, cmd); return fu_block_device_sg_io_cmd_write(FU_BLOCK_DEVICE(self), st->data, st->len, buf, sizeof(buf), error); } static gboolean fu_algoltek_usbcr_device_do_write_spi(FuAlgoltekUsbcrDevice *self, guint16 addr, const guint8 *buf, guint8 bufsz, guint8 access_sz, GError **error) { g_autoptr(GByteArray) st = fu_struct_ag_usbcr_spi_cdb_new(); if (!fu_algoltek_usbcr_device_send_spi_cmd(self, FU_AG_USBCR_WREN, error)) return FALSE; fu_struct_ag_usbcr_spi_cdb_set_cmd(st, FU_AG_USBCR_SCSIOP_VENDOR_EEPROM_WR); fu_struct_ag_usbcr_spi_cdb_set_addr(st, addr); fu_struct_ag_usbcr_spi_cdb_set_bufsz(st, access_sz); fu_struct_ag_usbcr_spi_cdb_set_tag(st, FU_AG_SPECIFY_EEPROM_TYPE_TAG); fu_struct_ag_usbcr_spi_cdb_set_valid(st, FU_AG_SPIFLASH_VALID); return fu_block_device_sg_io_cmd_write(FU_BLOCK_DEVICE(self), st->data, st->len, buf, bufsz, error); } static gboolean fu_algoltek_usbcr_device_do_read_spi(FuAlgoltekUsbcrDevice *self, guint16 addr, guint8 *buf, guint8 bufsz, GError **error) { g_autoptr(GByteArray) st = fu_struct_ag_usbcr_spi_cdb_new(); fu_struct_ag_usbcr_spi_cdb_set_cmd(st, FU_AG_USBCR_SCSIOP_VENDOR_EEPROM_RD); fu_struct_ag_usbcr_spi_cdb_set_addr(st, addr); fu_struct_ag_usbcr_spi_cdb_set_bufsz(st, bufsz); fu_struct_ag_usbcr_spi_cdb_set_tag(st, FU_AG_SPECIFY_EEPROM_TYPE_TAG); fu_struct_ag_usbcr_spi_cdb_set_valid(st, FU_AG_SPIFLASH_VALID); return fu_block_device_sg_io_cmd_read(FU_BLOCK_DEVICE(self), st->data, st->len, buf, bufsz, error); } static gboolean fu_algoltek_usbcr_device_verify_reg_cb(FuDevice *device, gpointer user_data, GError **error) { FuAlgoltekUsbcrDevice *self = FU_ALGOLTEK_USBCR_DEVICE(device); guint8 *buf = (guint8 *)user_data; if (!fu_algoltek_usbcr_device_read_reg(self, 0xC8, buf, 1, FU_AG_USBCR_RD_WR_XDATA, error)) return FALSE; if ((buf[0] & 0x01) != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not ready"); return FALSE; } /* success */ return TRUE; } static gboolean fu_algoltek_usbcr_device_check_status_cb(FuDevice *device, gpointer user_data, GError **error) { FuAlgoltekUsbcrDevice *self = FU_ALGOLTEK_USBCR_DEVICE(device); guint8 *buf = (guint8 *)user_data; FuAgUsbcrRegSetup regs[5] = { {0x400, FU_AG_USBCR_RDSR}, {0xC9, 0x01}, {0xC4, 0x01}, {0xC7, 0x00}, {0xC8, 0x07}, }; for (guint j = 0; j < G_N_ELEMENTS(regs); j++) { if (!fu_algoltek_usbcr_device_write_reg(self, regs[j].reg, regs[j].val, FU_AG_USBCR_RD_WR_XDATA, error)) return FALSE; } buf[0] = 0; if (!fu_device_retry_full(FU_DEVICE(self), fu_algoltek_usbcr_device_verify_reg_cb, 5, 0, buf, error)) return FALSE; buf[0] = 0; if (!fu_algoltek_usbcr_device_read_reg(self, 0x400, buf, 2, FU_AG_USBCR_RD_WR_XDATA, error)) return FALSE; if ((buf[0] & 0x01) != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not ready"); return FALSE; } /* success */ return TRUE; } static gboolean fu_algoltek_usbcr_device_command_wren(FuAlgoltekUsbcrDevice *self, GError **error) { guint8 buf[1] = {0}; FuAgUsbcrRegSetup regs[] = { {0xC8, 0x04}, {0xCA, 0x01}, {0x400, FU_AG_USBCR_WREN}, {0xC9, 0x01}, {0xC8, 0x05}, }; for (guint i = 0; i < G_N_ELEMENTS(regs); i++) { if (!fu_algoltek_usbcr_device_write_reg(self, regs[i].reg, regs[i].val, FU_AG_USBCR_RD_WR_XDATA, error)) return FALSE; } if (!fu_device_retry_full(FU_DEVICE(self), fu_algoltek_usbcr_device_verify_reg_cb, 5, 0, buf, error)) return FALSE; /* success*/ return TRUE; } static gboolean fu_algoltek_usbcr_device_command_wrsr(FuAlgoltekUsbcrDevice *self, gboolean en, GError **error) { guint8 buf[1] = {0}; FuAgUsbcrRegSetup regs[] = { {0xC8, 0x04}, {0xCA, 0x01}, {0x400, FU_AG_USBCR_WRSR}, {0x401, 0x00}, {0xC9, 0x02}, {0xC8, 0x05}, }; if (en == FU_AG_BLOCK_MODE_EN) regs[3].val = 0x0C; for (guint i = 0; i < G_N_ELEMENTS(regs); i++) { if (!fu_algoltek_usbcr_device_write_reg(self, regs[i].reg, regs[i].val, FU_AG_USBCR_RD_WR_XDATA, error)) return FALSE; } if (!fu_device_retry_full(FU_DEVICE(self), fu_algoltek_usbcr_device_verify_reg_cb, 5, 0, buf, error)) return FALSE; /* success*/ return TRUE; } static gboolean fu_algoltek_usbcr_device_command_rdsr(FuAlgoltekUsbcrDevice *self, GError **error) { guint8 buf[1] = {0}; FuAgUsbcrRegSetup regs[] = { {0xC8, 0x04}, {0xCA, 0x01}, {0x400, FU_AG_USBCR_RDSR}, {0xC9, 0x01}, {0xC4, 0x01}, {0xC7, 0x00}, {0xC8, 0x07}, }; for (guint i = 0; i < G_N_ELEMENTS(regs); i++) { if (!fu_algoltek_usbcr_device_write_reg(self, regs[i].reg, regs[i].val, FU_AG_USBCR_RD_WR_XDATA, error)) return FALSE; } if (!fu_device_retry_full(FU_DEVICE(self), fu_algoltek_usbcr_device_verify_reg_cb, 5, 0, buf, error)) return FALSE; /* success*/ return TRUE; } static gboolean fu_algoltek_usbcr_device_spi_flash_block_mode_cb(FuDevice *device, gpointer user_data, GError **error) { FuAlgoltekUsbcrDevice *self = FU_ALGOLTEK_USBCR_DEVICE(device); guint8 buf[2] = {0}; guint8 en = GPOINTER_TO_INT(user_data); /* set command wren */ if (!fu_algoltek_usbcr_device_command_wren(self, error)) return FALSE; /* set command wrsr */ if (!fu_algoltek_usbcr_device_command_wrsr(self, en, error)) return FALSE; /* set command rdsr */ if (!fu_algoltek_usbcr_device_command_rdsr(self, error)) return FALSE; /* read data */ if (!fu_algoltek_usbcr_device_read_reg(self, 0x400, buf, sizeof(buf), FU_AG_USBCR_RD_WR_XDATA, error)) return FALSE; if (en == FU_AG_BLOCK_MODE_DISEN) { if ((buf[0] & 0xC) != 0x0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "return value is 0x%x while expecting value is 0x0", (guint)(buf[0] & 0xC)); return FALSE; } } else { if ((buf[0] & 0xC) != 0xC) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "return value is 0x%x while expecting value is 0xC", (guint)(buf[0] & 0xC)); return FALSE; } } return TRUE; } static gboolean fu_algoltek_usbcr_device_set_clear_soft_reset_flag(FuAlgoltekUsbcrDevice *self, guint8 val, GError **error) { g_autoptr(GByteArray) st = fu_struct_ag_usbcr_reset_cdb_new(); fu_struct_ag_usbcr_reset_cdb_set_cmd(st, FU_AG_USBCR_SCSIOP_VENDOR_GENERIC_CMD); fu_struct_ag_usbcr_reset_cdb_set_subcmd(st, 0x96); fu_struct_ag_usbcr_reset_cdb_set_val(st, 0x78); fu_struct_ag_usbcr_reset_cdb_set_val2(st, val); return fu_block_device_sg_io_cmd_none(FU_BLOCK_DEVICE(self), st->data, st->len, error); } static gboolean fu_algoltek_usbcr_device_reset_chip(FuAlgoltekUsbcrDevice *self, GError **error) { g_autoptr(GByteArray) st = fu_struct_ag_usbcr_reset_cdb_new(); fu_struct_ag_usbcr_reset_cdb_set_cmd(st, FU_AG_USBCR_SCSIOP_VENDOR_GENERIC_CMD); fu_struct_ag_usbcr_reset_cdb_set_subcmd(st, 0x95); fu_struct_ag_usbcr_reset_cdb_set_val(st, 0x23); return fu_block_device_sg_io_cmd_none(FU_BLOCK_DEVICE(self), st->data, st->len, error); } static gboolean fu_algoltek_usbcr_device_ensure_version(FuAlgoltekUsbcrDevice *self, GError **error) { guint16 app_ver = 0; guint16 boot_ver = 0; g_autoptr(GByteArray) ver_array = NULL; ver_array = fu_algoltek_usbcr_device_cmd_get_ver(self, error); if (ver_array == NULL) { g_prefix_error(error, "failed to read version: "); return FALSE; } if (!fu_memread_uint16_safe(ver_array->data, ver_array->len, 130, &app_ver, G_LITTLE_ENDIAN, error)) return FALSE; fu_device_set_version_raw(FU_DEVICE(self), app_ver); if (!fu_memread_uint16_safe(ver_array->data, ver_array->len, 132, &boot_ver, G_LITTLE_ENDIAN, error)) return FALSE; fu_device_set_version_bootloader_raw(FU_DEVICE(self), boot_ver); return TRUE; } static gboolean fu_algoltek_usbcr_device_probe(FuDevice *device, GError **error) { /* FuUdevDevice->probe */ if (!FU_DEVICE_CLASS(fu_algoltek_usbcr_device_parent_class)->probe(device, error)) return FALSE; /* set the physical ID */ return fu_udev_device_set_physical_id(FU_UDEV_DEVICE(device), "usb", error); } static gboolean fu_algoltek_usbcr_device_setup(FuDevice *device, GError **error) { FuAlgoltekUsbcrDevice *self = FU_ALGOLTEK_USBCR_DEVICE(device); if (!fu_algoltek_usbcr_device_ensure_version(self, error)) return FALSE; fu_device_build_vendor_id_u16(device, "BLOCK", fu_device_get_vid(device)); /* success */ return TRUE; } static FuFirmware * fu_algoltek_usbcr_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { g_autoptr(FuFirmware) firmware = fu_algoltek_usbcr_firmware_new(); /* validate compatibility */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; if (fu_algoltek_usbcr_firmware_get_boot_ver(FU_ALGOLTEK_USBCR_FIRMWARE(firmware)) != fu_device_get_version_bootloader_raw(device)) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware boot version is 0x%x while expecting value is 0x%x", fu_algoltek_usbcr_firmware_get_boot_ver(FU_ALGOLTEK_USBCR_FIRMWARE(firmware)), (guint)fu_device_get_version_bootloader_raw(device)); return NULL; } /* success */ return g_steal_pointer(&firmware); } static gboolean fu_algoltek_usbcr_device_write_chunk(FuAlgoltekUsbcrDevice *self, FuChunk *chk, GError **error) { guint8 back_data[8] = {0}; if (!fu_algoltek_usbcr_device_do_write_spi(self, fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; if (!fu_device_retry_full(FU_DEVICE(self), fu_algoltek_usbcr_device_check_status_cb, 5, 0, back_data, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_algoltek_usbcr_device_write_chunks(FuAlgoltekUsbcrDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (gint i = fu_chunk_array_length(chunks) - 1; i >= 0; i--) { g_autoptr(FuChunk) chk = NULL; chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_algoltek_usbcr_device_write_chunk(self, chk, error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_algoltek_usbcr_device_verify_chunks(FuAlgoltekUsbcrDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autofree guint8 *buf = NULL; g_autoptr(FuChunk) chk = NULL; chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; buf = g_malloc0(fu_chunk_get_data_sz(chk)); if (!fu_algoltek_usbcr_device_do_read_spi(self, fu_chunk_get_address(chk), buf, fu_chunk_get_data_sz(chk), error)) return FALSE; if (!fu_memcmp_safe(fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0, buf, fu_chunk_get_data_sz(chk), 0x0, fu_chunk_get_data_sz(chk), error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_algoltek_usbcr_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuAlgoltekUsbcrDevice *self = FU_ALGOLTEK_USBCR_DEVICE(device); g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(GInputStream) stream = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 4, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 48, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 48, NULL); if (!fu_device_retry_full(FU_DEVICE(self), fu_algoltek_usbcr_device_spi_flash_block_mode_cb, 5, 0, GINT_TO_POINTER(FU_AG_BLOCK_MODE_DISEN), error)) return FALSE; if (!fu_algoltek_usbcr_device_send_spi_cmd(self, FU_AG_USBCR_WREN, error)) return FALSE; if (!fu_algoltek_usbcr_device_send_spi_cmd(self, FU_AG_USBCR_ERASE, error)) return FALSE; fu_progress_step_done(progress); /* get default image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 32, error); if (chunks == NULL) return FALSE; /* write */ if (!fu_algoltek_usbcr_device_write_chunks(self, chunks, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* verify */ if (!fu_algoltek_usbcr_device_verify_chunks(self, chunks, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); if (!fu_device_retry_full(FU_DEVICE(self), fu_algoltek_usbcr_device_spi_flash_block_mode_cb, 5, 0, GINT_TO_POINTER(FU_AG_BLOCK_MODE_EN), error)) return FALSE; /* reset */ if (!fu_algoltek_usbcr_device_set_clear_soft_reset_flag(self, 0xAF, error)) return FALSE; if (!fu_algoltek_usbcr_device_reset_chip(self, error)) return FALSE; /* success! */ return TRUE; } static void fu_algoltek_usbcr_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static gchar * fu_algoltek_usbcr_device_convert_version(FuDevice *device, guint64 version_raw) { return g_strdup_printf("%x", (guint)fu_device_get_version_raw(device)); } static void fu_algoltek_usbcr_device_init(FuAlgoltekUsbcrDevice *self) { fu_device_set_vendor(FU_DEVICE(self), "Algoltek"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_HEX); fu_device_add_protocol(FU_DEVICE(self), "com.algoltek.usbcr"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_set_name(FU_DEVICE(self), "USB Card Reader"); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_SYNC); } static void fu_algoltek_usbcr_device_class_init(FuAlgoltekUsbcrDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_algoltek_usbcr_device_probe; device_class->setup = fu_algoltek_usbcr_device_setup; device_class->prepare_firmware = fu_algoltek_usbcr_device_prepare_firmware; device_class->write_firmware = fu_algoltek_usbcr_device_write_firmware; device_class->set_progress = fu_algoltek_usbcr_device_set_progress; device_class->convert_version = fu_algoltek_usbcr_device_convert_version; } fwupd-2.0.10/plugins/algoltek-usbcr/fu-algoltek-usbcr-device.h000066400000000000000000000005321501337203100242450ustar00rootroot00000000000000/* * Copyright 2024 Algoltek, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ALGOLTEK_USBCR_DEVICE (fu_algoltek_usbcr_device_get_type()) G_DECLARE_FINAL_TYPE(FuAlgoltekUsbcrDevice, fu_algoltek_usbcr_device, FU, ALGOLTEK_USBCR_DEVICE, FuBlockDevice) fwupd-2.0.10/plugins/algoltek-usbcr/fu-algoltek-usbcr-firmware.c000066400000000000000000000065151501337203100246240ustar00rootroot00000000000000/* * Copyright 2024 Algoltek, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-algoltek-usbcr-firmware.h" #include "fu-algoltek-usbcr-struct.h" struct _FuAlgoltekUsbcrFirmware { FuFirmware parent_instance; guint16 boot_ver; }; G_DEFINE_TYPE(FuAlgoltekUsbcrFirmware, fu_algoltek_usbcr_firmware, FU_TYPE_FIRMWARE) static void fu_algoltek_usbcr_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuAlgoltekUsbcrFirmware *self = FU_ALGOLTEK_USBCR_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "boot_ver", self->boot_ver); } static gboolean fu_algoltek_usbcr_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuAlgoltekUsbcrFirmware *self = FU_ALGOLTEK_USBCR_FIRMWARE(firmware); gsize offset = 0; guint16 app_ver = 0; guint16 emmc_support_ver = 0; guint16 emmc_ver = 0; guint16 fw_addr = 0; guint16 fw_len = 0; /* emmc version */ if (!fu_input_stream_read_u16(stream, FU_AG_USBCR_OFFSET_EMMC_VER, &emmc_ver, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_input_stream_read_u16(stream, FU_AG_USBCR_OFFSET_FIRMWARE_START_ADDR, &fw_addr, G_BIG_ENDIAN, error)) return FALSE; if (!fu_input_stream_read_u16(stream, FU_AG_USBCR_OFFSET_FIRMWARE_LEN, &fw_len, G_BIG_ENDIAN, error)) return FALSE; /* calculate the offset of the app_ver */ offset += fw_addr + fw_len - FU_AG_USBCR_OFFSET_APP_VER_FROM_END; /* app version */ if (!fu_input_stream_read_u16(stream, offset, &app_ver, G_BIG_ENDIAN, error)) return FALSE; fu_firmware_set_version_raw(firmware, app_ver); /* boot version */ offset += 2; if (!fu_input_stream_read_u16(stream, offset, &self->boot_ver, G_BIG_ENDIAN, error)) return FALSE; /* emmc support version */ offset += FU_AG_USBCR_OFFSET_EMMC_SUPPORT_VER_FROM_BOOT_VER; if (!fu_input_stream_read_u16(stream, offset, &emmc_support_ver, G_BIG_ENDIAN, error)) return FALSE; if (emmc_ver != emmc_support_ver) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "EMMC support version is 0x%x while expecting value is 0x%x", emmc_support_ver, emmc_ver); return FALSE; } return TRUE; } guint16 fu_algoltek_usbcr_firmware_get_boot_ver(FuAlgoltekUsbcrFirmware *self) { g_return_val_if_fail(FU_IS_ALGOLTEK_USBCR_FIRMWARE(self), G_MAXUINT16); return self->boot_ver; } static gchar * fu_algoltek_usbcr_firmware_convert_version(FuFirmware *firmware, guint64 version_raw) { return fu_version_from_uint16_hex(version_raw, fu_firmware_get_version_format(firmware)); } static void fu_algoltek_usbcr_firmware_init(FuAlgoltekUsbcrFirmware *self) { fu_firmware_set_version_format(FU_FIRMWARE(self), FWUPD_VERSION_FORMAT_HEX); } static void fu_algoltek_usbcr_firmware_class_init(FuAlgoltekUsbcrFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->convert_version = fu_algoltek_usbcr_firmware_convert_version; firmware_class->parse = fu_algoltek_usbcr_firmware_parse; firmware_class->export = fu_algoltek_usbcr_firmware_export; } FuFirmware * fu_algoltek_usbcr_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_ALGOLTEK_USBCR_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/algoltek-usbcr/fu-algoltek-usbcr-firmware.h000066400000000000000000000007451501337203100246300ustar00rootroot00000000000000/* * Copyright 2024 Algoltek, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ALGOLTEK_USBCR_FIRMWARE (fu_algoltek_usbcr_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuAlgoltekUsbcrFirmware, fu_algoltek_usbcr_firmware, FU, ALGOLTEK_USBCR_FIRMWARE, FuFirmware) FuFirmware * fu_algoltek_usbcr_firmware_new(void); guint16 fu_algoltek_usbcr_firmware_get_boot_ver(FuAlgoltekUsbcrFirmware *self); fwupd-2.0.10/plugins/algoltek-usbcr/fu-algoltek-usbcr-plugin.c000066400000000000000000000017061501337203100243030ustar00rootroot00000000000000/* * Copyright 2024 Algoltek, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-algoltek-usbcr-device.h" #include "fu-algoltek-usbcr-firmware.h" #include "fu-algoltek-usbcr-plugin.h" struct _FuAlgoltekUsbcrPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuAlgoltekUsbcrPlugin, fu_algoltek_usbcr_plugin, FU_TYPE_PLUGIN) static void fu_algoltek_usbcr_plugin_init(FuAlgoltekUsbcrPlugin *self) { } static void fu_algoltek_usbcr_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_udev_subsystem(plugin, "block:disk"); fu_plugin_add_device_gtype(plugin, FU_TYPE_ALGOLTEK_USBCR_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_ALGOLTEK_USBCR_FIRMWARE); } static void fu_algoltek_usbcr_plugin_class_init(FuAlgoltekUsbcrPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_algoltek_usbcr_plugin_constructed; } fwupd-2.0.10/plugins/algoltek-usbcr/fu-algoltek-usbcr-plugin.h000066400000000000000000000004111501337203100243000ustar00rootroot00000000000000/* * Copyright 2024 Algoltek, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuAlgoltekUsbcrPlugin, fu_algoltek_usbcr_plugin, FU, ALGOLTEK_USBCR_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/algoltek-usbcr/fu-algoltek-usbcr.rs000066400000000000000000000024111501337203100232030ustar00rootroot00000000000000// Copyright 2024 Algoltek, Inc. // SPDX-License-Identifier: LGPL-2.1-or-later enum FuAgUsbcrOffset { FirmwareStartAddr = 0x0B, FirmwareLen = 0x0D, EmmcVer = 0x1FE, AppVerFromEnd = 0x33, EmmcSupportVerFromBootVer = 0x2A, } enum FuAgUsbcrScsiopVendor { EepromRd = 0xC0, EepromWr, FirmwareRevision = 0xC3, GenericCmd = 0xC7, } enum FuAgUsbcr { Wrsr = 0x01, Rdsr = 0x05, Wren, Erase = 0xC7, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructAgUsbcrRegCdb { opcode: u8 == 0xC7, subopcode: u8 == 0x1F, sig: u16be == 0x058F, cmd: u8, subcmd: u8, sig2: u32be == 0x30353846, ramdest: u8, addr:u16be, val:u8, reserved:[u8; 2], } #[derive(New, Default)] #[repr(C, packed)] struct FuStructAgUsbcrResetCdb { opcode: u8 == 0xC7, subopcode: u8 == 0x1F, sig: u16be == 0x058F, cmd: u8, subcmd: u8, sig2: u32be == 0x30353846, val: u8, val2:u8, reserved:[u8; 4], } #[derive(New, Default)] #[repr(C, packed)] struct FuStructAgUsbcrSpiCdb { opcode: u8 == 0xC7, subopcode: u8 == 0x1F, sig: u16be == 0x058F, cmd: u8, addr: u16be, bufsz: u8, tag: u8, valid: u8, spisig1: u8, spisig2: u8, spicmd: u8, reserved:[u8; 3], } fwupd-2.0.10/plugins/algoltek-usbcr/meson.build000066400000000000000000000007641501337203100214620ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginAlgoltekUsbcr"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('algoltek-usbcr.quirk') plugin_builtins += static_library('fu_plugin_algoltek_usbcr', rustgen.process('fu-algoltek-usbcr.rs'), sources: [ 'fu-algoltek-usbcr-device.c', 'fu-algoltek-usbcr-firmware.c', 'fu-algoltek-usbcr-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) fwupd-2.0.10/plugins/amd-gpu/000077500000000000000000000000001501337203100157255ustar00rootroot00000000000000fwupd-2.0.10/plugins/amd-gpu/README.md000066400000000000000000000037321501337203100172110ustar00rootroot00000000000000--- title: Plugin: AMDGPU --- ## Introduction This plugin reports the vbios version of APU devices supported by amdgpu and supports flashing the IFWI of some dGPU devices. ## External Interface Access This plugin requires R/W access to sysfs files located within `/sys/bus/pci/drivers/*/amdgpu`. This plugin requires ioctl access to `DRM_IOCTL_AMDGPU_INFO`. ## Firmware Format This plugin supports the following protocol ID: * `com.amd.pspvbflash` ## GUID Generation The plugin will use standard PCI GUIDs, but also generate an AMD GPU specific GUID with the part number: `AMD\$PART_NUMBER` ## Update behavior The dGPU will boot into the new firmware when the system is rebooted. The dGPU contains two partitions, and the update will be applied to the inactive partition. If the active partition becomes corrupted for any reason the dGPU may revert back to an older firmware present on the inactive partition. ## Version Considerations This plugin has been available since fwupd version `1.8.11`. Update functionality has been available since fwupd version `1.9.6`. ## Threat Model The plugin runs within the privileged fwupd process. The plugin doesn't directly interface with the hardware, but rather interfaces with the kernel driver which interfaces with the hardware. ```mermaid flowchart LR subgraph dGPU PSP SPI[(SPI)] end subgraph fwupd Process fwupdengine(FuEngine) plugin(AmdGpu\nPlugin) end subgraph Linux Kernel kernel(amdgpu driver) end PSP <-.->SPI kernel<--"mailbox"-->PSP plugin--"psp_vbflash\nsysfs"-->kernel plugin<--"psp_vbflash_status\nsysfs"-->kernel plugin<--"DRM_IOCTL_AMDGPU_INFO()"-->kernel fwupdengine -.- plugin PSP ~~~ kernel kernel ~~~ fwupdengine ``` ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Mario Limonciello: @superm1 fwupd-2.0.10/plugins/amd-gpu/amd-gpu.quirk000066400000000000000000000000451501337203100203330ustar00rootroot00000000000000[PCI\DRIVER_amdgpu] Plugin = amd_gpu fwupd-2.0.10/plugins/amd-gpu/fu-amd-gpu-atom-firmware.c000066400000000000000000000226121501337203100226060ustar00rootroot00000000000000/* * Copyright 2023 Advanced Micro Devices Inc. * All rights reserved. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-amd-gpu-atom-firmware.h" #include "fu-amd-gpu-atom-struct.h" #define BIOS_VERSION_PREFIX "ATOMBIOSBK-AMD VER" #define BIOS_STRING_LENGTH 43 #define STRLEN_NORMAL 32 #define STRLEN_LONG 64 struct _FuAmdGpuAtomFirmware { FuOpromFirmware parent_instance; gchar *part_number; gchar *asic; gchar *pci_type; gchar *memory_type; gchar *bios_date; gchar *model_name; gchar *config_filename; }; /** * FuAmdGpuAtomFirmware: * * Firmware for AMD dGPUs. * * This parser collects information from the "CSM" image also known as * the ATOM image. * * This image contains strings that describe the version and the hardware * the ATOM is intended to be used for. */ G_DEFINE_TYPE(FuAmdGpuAtomFirmware, fu_amd_gpu_atom_firmware, FU_TYPE_OPROM_FIRMWARE) static void fu_amd_gpu_atom_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuAmdGpuAtomFirmware *self = FU_AMD_GPU_ATOM_FIRMWARE(firmware); FU_FIRMWARE_CLASS(fu_amd_gpu_atom_firmware_parent_class)->export(firmware, flags, bn); fu_xmlb_builder_insert_kv(bn, "part_number", self->part_number); fu_xmlb_builder_insert_kv(bn, "asic", self->asic); fu_xmlb_builder_insert_kv(bn, "pci_type", self->pci_type); fu_xmlb_builder_insert_kv(bn, "memory_type", self->memory_type); fu_xmlb_builder_insert_kv(bn, "bios_date", self->bios_date); fu_xmlb_builder_insert_kv(bn, "model_name", self->model_name); fu_xmlb_builder_insert_kv(bn, "config_filename", self->config_filename); } static gboolean fu_amd_gpu_atom_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { g_autoptr(GByteArray) atom = NULL; atom = fu_struct_atom_image_parse_stream(stream, offset, error); if (atom == NULL) return FALSE; return fu_struct_atom_rom21_header_validate_stream( stream, offset + fu_struct_atom_image_get_rom_loc(atom), error); } const gchar * fu_amd_gpu_atom_firmware_get_vbios_pn(FuFirmware *firmware) { FuAmdGpuAtomFirmware *self = FU_AMD_GPU_ATOM_FIRMWARE(firmware); return self->part_number; } static gboolean fu_amd_gpu_atom_firmware_parse_vbios_version(FuAmdGpuAtomFirmware *self, GBytes *blob, GError **error) { gsize bufsz = 0; gsize offset = 0; const guint8 *buf = g_bytes_get_data(blob, &bufsz); g_autofree gchar *version = NULL; if (!fu_memmem_safe(buf, bufsz, (const guint8 *)BIOS_VERSION_PREFIX, sizeof(BIOS_VERSION_PREFIX) - 1, &offset, error)) { g_prefix_error(error, "failed to find anchor: "); return FALSE; } /* skip anchor */ offset += sizeof(BIOS_VERSION_PREFIX) - 1; version = fu_memstrsafe(buf, bufsz, offset, BIOS_STRING_LENGTH, error); if (version == NULL) return FALSE; fu_firmware_set_version(FU_FIRMWARE(self), version); return TRUE; } static gboolean fu_amd_gpu_atom_firmware_parse_vbios_date(FuAmdGpuAtomFirmware *self, FuStructAtomImage *atom_image, GError **error) { g_autoptr(GByteArray) st = fu_struct_atom_image_get_vbios_date(atom_image); g_autofree gchar *year = NULL; g_autofree gchar *month = NULL; g_autofree gchar *day = NULL; g_autofree gchar *hours = NULL; g_autofree gchar *minutes = NULL; if (st == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "ATOMBIOS date is invalid"); return FALSE; } year = fu_struct_vbios_date_get_year(st); month = fu_struct_vbios_date_get_month(st); day = fu_struct_vbios_date_get_day(st); hours = fu_struct_vbios_date_get_hours(st); minutes = fu_struct_vbios_date_get_minutes(st); /* same date format as atom_get_vbios_date() */ self->bios_date = g_strdup_printf("20%s/%s/%s %s:%s", year, month, day, hours, minutes); return TRUE; } static gboolean fu_amd_gpu_atom_firmware_parse_vbios_pn(FuAmdGpuAtomFirmware *self, GBytes *blob, FuStructAtomImage *atom_image, GError **error) { gsize bufsz = 0; guint16 atombios_size; gint num_str, i; guint16 idx; const guint8 *buf = g_bytes_get_data(blob, &bufsz); g_autofree gchar *model = NULL; num_str = fu_struct_atom_image_get_num_strings(atom_image); if (num_str == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "ATOMBIOS number of strings is 0"); return FALSE; } idx = fu_struct_atom_image_get_str_loc(atom_image); if (idx == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "ATOMBIOS string location is invalid"); return FALSE; } /* make sure there is enough space for all the strings */ atombios_size = fu_firmware_get_size(FU_FIRMWARE(self)); if ((gsize)(idx + (num_str * (STRLEN_NORMAL - 1))) > atombios_size) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "bufsz is too small for all strings"); return FALSE; } /* parse atombios strings */ for (i = 0; i < num_str; i++) { g_autofree char *str = NULL; str = fu_memstrsafe(buf, bufsz, idx, STRLEN_NORMAL - 1, error); if (str == NULL) return FALSE; idx += strlen(str) + 1; switch (i) { case FU_ATOM_STRING_INDEX_PART_NUMBER: self->part_number = g_steal_pointer(&str); break; case FU_ATOM_STRING_INDEX_ASIC: self->asic = g_steal_pointer(&str); break; case FU_ATOM_STRING_INDEX_PCI_TYPE: self->pci_type = g_steal_pointer(&str); break; case FU_ATOM_STRING_INDEX_MEMORY_TYPE: self->memory_type = g_steal_pointer(&str); break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "unknown string index: %d", i); return FALSE; } } /* skip the following 2 chars: 0x0D 0x0A */ idx += 2; /* make sure there is enough space for name string */ if ((gsize)(idx + STRLEN_LONG - 1) > atombios_size) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "bufsz is too small for name string"); return FALSE; } model = fu_memstrsafe(buf, bufsz, idx, STRLEN_LONG - 1, error); if (model == NULL) return FALSE; self->model_name = fu_strstrip(model); return TRUE; } static gboolean fu_amd_gpu_atom_firmware_parse_config_filename(FuAmdGpuAtomFirmware *self, GBytes *blob, FuStructAtomRom21Header *atom_header, GError **error) { gsize bufsz = 0; const guint8 *buf = g_bytes_get_data(blob, &bufsz); g_autofree gchar *config_filename = NULL; config_filename = fu_memstrsafe(buf, bufsz, fu_struct_atom_rom21_header_get_config_filename_offset(atom_header), STRLEN_LONG - 1, error); if (config_filename == NULL) return FALSE; /* this function is called twice, but value only stored once */ if (self->config_filename == NULL) self->config_filename = fu_strstrip(config_filename); return TRUE; } static gboolean fu_amd_gpu_atom_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuAmdGpuAtomFirmware *self = FU_AMD_GPU_ATOM_FIRMWARE(firmware); guint32 loc; g_autoptr(FuStructAtomImage) atom_image = NULL; g_autoptr(FuStructAtomRom21Header) atom_rom = NULL; g_autoptr(GBytes) blob = NULL; if (!FU_FIRMWARE_CLASS(fu_amd_gpu_atom_firmware_parent_class) ->parse(firmware, stream, flags, error)) return FALSE; /* atom rom image */ atom_image = fu_struct_atom_image_parse_stream(stream, 0x0, error); if (atom_image == NULL) return FALSE; /* unit is 512 bytes */ fu_firmware_set_size(firmware, fu_struct_atom_image_get_size(atom_image) * 512); /* atom rom header */ loc = fu_struct_atom_image_get_rom_loc(atom_image); atom_rom = fu_struct_atom_rom21_header_parse_stream(stream, loc, error); if (atom_rom == NULL) return FALSE; blob = fu_input_stream_read_bytes(stream, 0x0, G_MAXSIZE, NULL, error); if (blob == NULL) return FALSE; if (!fu_amd_gpu_atom_firmware_parse_config_filename(self, blob, atom_rom, error)) return FALSE; if (!fu_amd_gpu_atom_firmware_parse_vbios_date(self, atom_image, error)) return FALSE; if (!fu_amd_gpu_atom_firmware_parse_vbios_pn(self, blob, atom_image, error)) return FALSE; if (!fu_amd_gpu_atom_firmware_parse_vbios_version(self, blob, error)) return FALSE; return TRUE; } static void fu_amd_gpu_atom_firmware_init(FuAmdGpuAtomFirmware *self) { } static void fu_amd_gpu_atom_firmware_finalize(GObject *object) { FuAmdGpuAtomFirmware *self = FU_AMD_GPU_ATOM_FIRMWARE(object); g_free(self->part_number); g_free(self->asic); g_free(self->pci_type); g_free(self->memory_type); g_free(self->bios_date); g_free(self->model_name); g_free(self->config_filename); G_OBJECT_CLASS(fu_amd_gpu_atom_firmware_parent_class)->finalize(object); } static void fu_amd_gpu_atom_firmware_class_init(FuAmdGpuAtomFirmwareClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_amd_gpu_atom_firmware_finalize; firmware_class->parse = fu_amd_gpu_atom_firmware_parse; firmware_class->export = fu_amd_gpu_atom_firmware_export; firmware_class->validate = fu_amd_gpu_atom_firmware_validate; } FuFirmware * fu_amd_gpu_atom_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_AMD_GPU_ATOM_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/amd-gpu/fu-amd-gpu-atom-firmware.h000066400000000000000000000013341501337203100226110ustar00rootroot00000000000000/* * Copyright 2023 Advanced Micro Devices Inc. * All rights reserved. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_AMD_GPU_ATOM_FIRMWARE (fu_amd_gpu_atom_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuAmdGpuAtomFirmware, fu_amd_gpu_atom_firmware, FU, AMD_GPU_ATOM_FIRMWARE, FuOpromFirmware) FuFirmware * fu_amd_gpu_atom_firmware_new(void); const gchar * fu_amd_gpu_atom_firmware_get_vbios_pn(FuFirmware *firmware); fwupd-2.0.10/plugins/amd-gpu/fu-amd-gpu-atom.rs000066400000000000000000000035101501337203100211720ustar00rootroot00000000000000// Copyright 2023 Advanced Micro Devices Inc. // SPDX-License-Identifier: LGPL-2.1-or-later OR MIT #[derive(Getters)] #[repr(C, packed)] struct FuStructVbiosDate { month: [char; 2], _separator: u8, day: [char; 2], _separator: u8, year: [char; 2], _separator: u8, hours: [char; 2], _separator: u8, minutes: [char; 2], _separator: u8, seconds: [char; 2], _nullchar: u8, } #[derive(ParseStream, Default)] #[repr(C, packed)] struct FuStructAtomImage { signature: u16be = 0x55aa, size: u16le, reserved: [u64be; 2], reserved: u32le, pcir_loc: u16le, reserved: u32le, compat_sig: [char; 3] == "IBM", checksum: u8, reserved: [u32le; 3], reserved: u8, num_strings: u8, reserved: [u64le; 3], rom_loc: u16le, reserved: [u16le; 3], vbios_date: FuStructVbiosDate, oem: u16le, reserved: [u16le; 5], str_loc: u32le, } #[derive(Getters)] #[repr(C, packed)] struct FuStructAtomHeaderCommon { size: u16le, format_rev: u8, content_rev: u8, } #[derive(ParseStream, ValidateStream, Default)] #[repr(C, packed)] struct FuStructAtomRom21Header { header: FuStructAtomHeaderCommon, signature: [char; 4] == "ATOM" , bios_runtime_seg_addr: u16le, protected_mode_info_offset: u16le, config_filename_offset: u16le, crc_block_offset: u16le, bios_bootup_message_offset: u16le, int10_offset: u16le, pci_bus_dev_init_code: u16le, io_base_addr: u16le, subsystem_vendor_id: u16le, subsystem_id: u16le, pci_info_offset: u16le, master_command_table_offset: u16le, master_data_table_offset: u16le, extended_function_code: u8, reserved: u8, psp_dir_table_offset: u32le, } #[repr(u8)] enum FuAtomStringIndex { PartNumber = 0x00, ASIC = 0x01, PciType = 0x02, MemoryType = 0x03, } fwupd-2.0.10/plugins/amd-gpu/fu-amd-gpu-device.c000066400000000000000000000326161501337203100213000ustar00rootroot00000000000000/* * Copyright 2023 Advanced Micro Devices Inc. * All rights reserved. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include #include #include #include #include "fu-amd-gpu-atom-firmware.h" #include "fu-amd-gpu-device.h" #include "fu-amd-gpu-psp-firmware.h" struct _FuAmdGpuDevice { FuOpromDevice parent_instance; gchar *vbios_pn; guint32 drm_major; guint32 drm_minor; }; #define PSPVBFLASH_MAX_POLL 1500 #define PSPVBFLASH_NOT_STARTED 0x0 #define PSPVBFLASH_IN_PROGRESS 0x1 #define PSPVBFLASH_SUCCESS 0x80000000 #define PART_NUM_STR_SIZE 10 G_DEFINE_TYPE(FuAmdGpuDevice, fu_amd_gpu_device, FU_TYPE_OPROM_DEVICE) static void fu_amd_gpu_device_to_string(FuDevice *device, guint idt, GString *str) { FuAmdGpuDevice *self = FU_AMDGPU_DEVICE(device); fwupd_codec_string_append_int(str, idt, "DrmMajor", self->drm_major); fwupd_codec_string_append_int(str, idt, "DrmMinor", self->drm_minor); } static gboolean fu_amd_gpu_device_set_device_file(FuAmdGpuDevice *self, const gchar *base, GError **error) { FuDeviceEvent *event = NULL; const gchar *f; g_autofree gchar *ddir = NULL; g_autofree gchar *device_file = NULL; g_autofree gchar *event_id = NULL; g_autoptr(GDir) dir = NULL; /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED) || fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event_id = g_strdup_printf("DrmAmdgpuSetDeviceFile:Base=%s", base); } /* emulated */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { event = fu_device_load_event(FU_DEVICE(self), event_id, error); if (event == NULL) return FALSE; f = fu_device_event_get_str(event, "Filename", error); if (f == NULL) return FALSE; fu_udev_device_set_device_file(FU_UDEV_DEVICE(self), f); return TRUE; } /* save */ if (fu_context_has_flag(fu_device_get_context(FU_DEVICE(self)), FU_CONTEXT_FLAG_SAVE_EVENTS)) { event = fu_device_save_event(FU_DEVICE(self), event_id); } /* find card path */ ddir = g_build_filename(base, "drm", NULL); dir = g_dir_open(ddir, 0, error); if (dir == NULL) return FALSE; while ((f = g_dir_read_name(dir))) { if (g_str_has_prefix(f, "card")) { g_autofree gchar *devbase = fu_path_from_kind(FU_PATH_KIND_DEVFS); device_file = g_build_filename(devbase, "dri", f, NULL); break; } } /* nothing found */ if (device_file == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no DRM device file found"); return FALSE; } if (event != NULL) fu_device_event_set_str(event, "Filename", device_file); /* success */ fu_udev_device_set_device_file(FU_UDEV_DEVICE(self), device_file); return TRUE; } static gboolean fu_amd_gpu_device_probe(FuDevice *device, GError **error) { FuAmdGpuDevice *self = FU_AMDGPU_DEVICE(device); const gchar *base; gboolean exists_rom = FALSE; gboolean exists_vbflash = FALSE; gboolean exists_vbflash_status = FALSE; g_autofree gchar *rom = NULL; g_autofree gchar *psp_vbflash = NULL; g_autofree gchar *psp_vbflash_status = NULL; base = fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device)); if (!fu_amd_gpu_device_set_device_file(self, base, error)) return FALSE; /* APUs don't have 'rom' sysfs file */ rom = g_build_filename(base, "rom", NULL); if (!fu_device_query_file_exists(FU_DEVICE(device), rom, &exists_rom, error)) return FALSE; if (!exists_rom) { fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_HOST_CPU_CHILD); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(device), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_INTERNAL); } else { fu_device_set_logical_id(device, "rom"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(device), FU_IO_CHANNEL_OPEN_FLAG_READ); } /* firmware upgrade support */ psp_vbflash = g_build_filename(base, "psp_vbflash", NULL); if (!fu_device_query_file_exists(device, psp_vbflash, &exists_vbflash, error)) return FALSE; psp_vbflash_status = g_build_filename(base, "psp_vbflash_status", NULL); if (!fu_device_query_file_exists(device, psp_vbflash_status, &exists_vbflash_status, error)) return FALSE; if (exists_vbflash && exists_vbflash_status) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_SELF_RECOVERY); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT); fu_device_set_install_duration(device, 70); fu_device_add_protocol(device, "com.amd.pspvbflash"); } return TRUE; } static void fu_amd_gpu_device_set_marketing_name(FuAmdGpuDevice *self) { FuIOChannel *io_channel = fu_udev_device_get_io_channel(FU_UDEV_DEVICE(self)); amdgpu_device_handle device_handle = {0}; gint r; /* ignore */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) return; r = amdgpu_device_initialize(fu_io_channel_unix_get_fd(io_channel), &self->drm_major, &self->drm_minor, &device_handle); if (r == 0) { const gchar *marketing_name = amdgpu_get_marketing_name(device_handle); if (marketing_name != NULL) fu_device_set_name(FU_DEVICE(self), marketing_name); amdgpu_device_deinitialize(device_handle); } else g_warning("unable to set marketing name: %s", g_strerror(r)); } static gboolean fu_amd_gpu_device_ioctl_buffer_cb(FuIoctl *self, gpointer ptr, guint8 *buf, gsize bufsz, GError **error) { struct drm_amdgpu_info *request = (struct drm_amdgpu_info *)ptr; request->return_pointer = GPOINTER_TO_SIZE(buf); request->return_size = bufsz; return TRUE; } static gboolean fu_amd_gpu_device_ioctl_drm_info(FuAmdGpuDevice *self, guint8 *buf, gsize bufsz, GError **error) { g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); struct drm_amdgpu_info request = { .query = AMDGPU_INFO_VBIOS, .vbios_info.type = AMDGPU_INFO_VBIOS_INFO, }; /* include these when generating the emulation event */ fu_ioctl_add_key_as_u16(ioctl, "Request", DRM_IOCTL_AMDGPU_INFO); fu_ioctl_add_key_as_u8(ioctl, "Query", request.query); fu_ioctl_add_mutable_buffer(ioctl, NULL, buf, bufsz, fu_amd_gpu_device_ioctl_buffer_cb); if (!fu_ioctl_execute(ioctl, DRM_IOCTL_AMDGPU_INFO, &request, sizeof(request), NULL, 1000, /* ms */ FU_IOCTL_FLAG_NONE, error)) { g_prefix_error(error, "failed to DRM_IOCTL_AMDGPU_INFO: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_amd_gpu_device_setup(FuDevice *device, GError **error) { FuAmdGpuDevice *self = FU_AMDGPU_DEVICE(device); struct drm_amdgpu_info_vbios vbios_info = {0}; g_autofree gchar *part = NULL; g_autofree gchar *model = NULL; g_auto(GStrv) tokens = NULL; fu_amd_gpu_device_set_marketing_name(self); if (!fu_amd_gpu_device_ioctl_drm_info(self, (guint8 *)&vbios_info, sizeof(vbios_info), error)) return FALSE; self->vbios_pn = fu_strsafe((const gchar *)vbios_info.vbios_pn, PART_NUM_STR_SIZE); part = g_strdup_printf("AMD\\%s", self->vbios_pn); fu_device_add_instance_id(device, part); tokens = fu_strsplit((const gchar *)vbios_info.vbios_pn, sizeof(vbios_info.vbios_pn), "-", -1); if (g_strv_length(tokens) >= 3) { guint64 ver; if (!fu_strtoull(tokens[2], &ver, 0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_device_set_version_raw(device, ver); } model = fu_strsafe((const gchar *)vbios_info.name, sizeof(vbios_info.name)); fu_device_set_summary(device, model); return TRUE; } static gchar * fu_amd_gpu_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_device_get_version_format(device)); } static FuFirmware * fu_amd_gpu_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuAmdGpuDevice *self = FU_AMDGPU_DEVICE(device); g_autoptr(FuFirmware) firmware = fu_amd_gpu_psp_firmware_new(); g_autoptr(FuFirmware) ish_a = NULL; g_autoptr(FuFirmware) partition_a = NULL; g_autoptr(FuFirmware) csm = NULL; g_autofree gchar *fw_pn = NULL; if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; /* we will always flash the contents of partition A */ ish_a = fu_firmware_get_image_by_id(firmware, "ISH_A", error); if (ish_a == NULL) return NULL; partition_a = fu_firmware_get_image_by_id(ish_a, "PARTITION_A", error); if (partition_a == NULL) return NULL; csm = fu_firmware_get_image_by_id(partition_a, "ATOM_CSM_A", error); if (csm == NULL) return NULL; fw_pn = fu_strsafe(fu_amd_gpu_atom_firmware_get_vbios_pn(csm), PART_NUM_STR_SIZE); if (g_strcmp0(fw_pn, self->vbios_pn) != 0) { if ((flags & FWUPD_INSTALL_FLAG_FORCE) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "firmware for %s does not match %s", fw_pn, self->vbios_pn); return NULL; } g_warning("firmware for %s does not match %s but is being force installed anyway", fw_pn, self->vbios_pn); } return g_steal_pointer(&firmware); } static gboolean fu_amd_gpu_device_wait_for_completion_cb(FuDevice *device, gpointer user_data, GError **error) { const gchar *base = fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device)); gsize sz = 0; guint64 status = 0; g_autofree gchar *buf = NULL; g_autofree gchar *psp_vbflash_status = NULL; psp_vbflash_status = g_build_filename(base, "psp_vbflash_status", NULL); if (!g_file_get_contents(psp_vbflash_status, &buf, &sz, error)) return FALSE; if (!fu_strtoull(buf, &status, 0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error)) return FALSE; if (status != PSPVBFLASH_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "status was %" G_GUINT64_FORMAT, status); return FALSE; } /* success */ return TRUE; } static gboolean fu_amd_gpu_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { g_autofree gchar *psp_vbflash = NULL; g_autoptr(FuIOChannel) image_io = NULL; g_autoptr(GBytes) fw = NULL; g_autoptr(GError) error_read = NULL; const gchar *base; /* emulation doesn't currently cover IO channel use */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) return TRUE; base = fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device)); psp_vbflash = g_build_filename(base, "psp_vbflash", NULL); image_io = fu_io_channel_new_file(psp_vbflash, FU_IO_CHANNEL_OPEN_FLAG_READ | FU_IO_CHANNEL_OPEN_FLAG_WRITE, error); if (image_io == NULL) return FALSE; fu_progress_set_id(progress, G_STRLOC); /* stage the image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; if (!fu_io_channel_write_bytes(image_io, fw, 100, FU_IO_CHANNEL_FLAG_NONE, error)) return FALSE; /* trigger the update (this looks funny but amdgpu returns 0 bytes) */ if (!fu_io_channel_read_raw(image_io, NULL, 1, NULL, 100, FU_IO_CHANNEL_FLAG_NONE, &error_read)) g_debug("triggered update: %s", error_read->message); /* poll for completion */ return fu_device_retry_full(device, fu_amd_gpu_device_wait_for_completion_cb, PSPVBFLASH_MAX_POLL, 100, /* ms */ NULL, error); } static void fu_amd_gpu_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, NULL); /* detach */ fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, NULL); /* write */ fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, NULL); /* attach */ fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, NULL); /* reload */ } static void fu_amd_gpu_device_init(FuAmdGpuDevice *self) { fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_AUTO_PARENT_CHILDREN); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_NO_GENERIC_GUIDS); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_NUMBER); } static void fu_amd_gpu_device_finalize(GObject *object) { FuAmdGpuDevice *self = FU_AMDGPU_DEVICE(object); g_free(self->vbios_pn); G_OBJECT_CLASS(fu_amd_gpu_device_parent_class)->finalize(object); } static void fu_amd_gpu_device_class_init(FuAmdGpuDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->finalize = fu_amd_gpu_device_finalize; device_class->probe = fu_amd_gpu_device_probe; device_class->setup = fu_amd_gpu_device_setup; device_class->set_progress = fu_amd_gpu_device_set_progress; device_class->write_firmware = fu_amd_gpu_device_write_firmware; device_class->prepare_firmware = fu_amd_gpu_device_prepare_firmware; device_class->to_string = fu_amd_gpu_device_to_string; device_class->convert_version = fu_amd_gpu_device_convert_version; } fwupd-2.0.10/plugins/amd-gpu/fu-amd-gpu-device.h000066400000000000000000000010341501337203100212730ustar00rootroot00000000000000/* * Copyright 2023 Advanced Micro Devices Inc. * All rights reserved. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_AMDGPU_DEVICE (fu_amd_gpu_device_get_type()) G_DECLARE_FINAL_TYPE(FuAmdGpuDevice, fu_amd_gpu_device, FU, AMDGPU_DEVICE, FuOpromDevice) fwupd-2.0.10/plugins/amd-gpu/fu-amd-gpu-plugin.c000066400000000000000000000025071501337203100213330ustar00rootroot00000000000000/* * Copyright 2023 Advanced Micro Devices Inc. * All rights reserved. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-amd-gpu-atom-firmware.h" #include "fu-amd-gpu-device.h" #include "fu-amd-gpu-plugin.h" #include "fu-amd-gpu-psp-firmware.h" struct _FuAmdGpuPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuAmdGpuPlugin, fu_amd_gpu_plugin, FU_TYPE_PLUGIN) static void fu_amd_gpu_plugin_init(FuAmdGpuPlugin *self) { } static void fu_amd_gpu_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_udev_subsystem(plugin, "pci"); fu_plugin_add_device_gtype(plugin, FU_TYPE_AMDGPU_DEVICE); /* Navi3x and later use PSP firmware container */ fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_AMD_GPU_PSP_FIRMWARE); /* Navi 2x and older have the ATOM firmware at start of image */ fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_AMD_GPU_ATOM_FIRMWARE); } static void fu_amd_gpu_plugin_class_init(FuAmdGpuPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_amd_gpu_plugin_constructed; } fwupd-2.0.10/plugins/amd-gpu/fu-amd-gpu-plugin.h000066400000000000000000000007321501337203100213360ustar00rootroot00000000000000/* * Copyright 2023 Advanced Micro Devices Inc. * All rights reserved. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include G_DECLARE_FINAL_TYPE(FuAmdGpuPlugin, fu_amd_gpu_plugin, FU, AMDGPU_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/amd-gpu/fu-amd-gpu-psp-firmware.c000066400000000000000000000146321501337203100224530ustar00rootroot00000000000000/* * Copyright 2023 Advanced Micro Devices Inc. * All rights reserved. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-amd-gpu-atom-firmware.h" #include "fu-amd-gpu-psp-firmware.h" #include "fu-amd-gpu-psp-struct.h" struct _FuAmdGpuPspFirmware { FuFirmware parent_instance; guint32 dir_location; }; /** * FuAmdGpuPspFirmware: * * An AMD PSP firmware image * * The firmware is structured in an Embedded Firmware Structure (EFS). * Within the EFS is a point to an "L1 PSP directory table". * * The L1 PSP directory table contains entries which point to * "Image Slot Headers" (ISH). * * The ISH headers contain entries that point to a given partition (A or B). * The partition contains an "L2 PSP directory table". * * The L2 directory table specifies a variety of IDs. Supported IDs will * be parsed by other firmware parsers. */ G_DEFINE_TYPE(FuAmdGpuPspFirmware, fu_amd_gpu_psp_firmware, FU_TYPE_FIRMWARE) static void fu_amd_gpu_psp_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuAmdGpuPspFirmware *self = FU_AMD_GPU_PSP_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "dir_location", self->dir_location); } static gboolean fu_amd_gpu_psp_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { g_autoptr(GByteArray) efs = NULL; efs = fu_struct_efs_parse_stream(stream, 0, error); if (efs == NULL) return FALSE; return fu_struct_psp_dir_validate_stream(stream, fu_struct_efs_get_psp_dir_loc(efs), error); } static gboolean fu_amd_gpu_psp_firmware_parse_l2(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { g_autoptr(FuStructPspDirTable) l2 = NULL; /* parse the L2 entries */ l2 = fu_struct_psp_dir_table_parse_stream(stream, offset, error); if (l2 == NULL) return FALSE; offset += l2->len; for (guint i = 0; i < fu_struct_psp_dir_get_total_entries(l2); i++) { g_autoptr(FuStructPspDirTable) l2_entry = NULL; l2_entry = fu_struct_psp_dir_table_parse_stream(stream, offset, error); if (l2_entry == NULL) return FALSE; offset += l2_entry->len; } /* success */ return TRUE; } static gboolean fu_amd_gpu_psp_firmware_parse_l1(FuFirmware *firmware, GInputStream *stream, gsize offset, FuFirmwareParseFlags flags, GError **error) { g_autoptr(FuStructPspDir) l1 = NULL; /* parse the L1 entries */ l1 = fu_struct_psp_dir_parse_stream(stream, offset, error); if (l1 == NULL) return FALSE; offset += l1->len; for (guint i = 0; i < fu_struct_psp_dir_get_total_entries(l1); i++) { guint loc; guint sz; g_autoptr(FuStructPspDirTable) l1_entry = NULL; g_autoptr(FuStructImageSlotHeader) ish = NULL; g_autoptr(FuFirmware) ish_img = fu_firmware_new(); g_autoptr(FuFirmware) csm_img = fu_amd_gpu_atom_firmware_new(); g_autoptr(FuFirmware) l2_img = fu_firmware_new(); g_autoptr(GInputStream) l2_stream = NULL; l1_entry = fu_struct_psp_dir_table_parse_stream(stream, offset, error); if (l1_entry == NULL) return FALSE; switch (fu_struct_psp_dir_table_get_fw_id(l1_entry)) { case FU_FWID_ISH_A: fu_firmware_set_id(ish_img, "ISH_A"); break; case FU_FWID_ISH_B: fu_firmware_set_id(ish_img, "ISH_B"); break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "Unknown ISH FWID: %x", fu_struct_psp_dir_table_get_fw_id(l1_entry)); return FALSE; } /* parse the image slot header */ loc = fu_struct_psp_dir_table_get_loc(l1_entry); ish = fu_struct_image_slot_header_parse_stream(stream, loc, error); if (ish == NULL) return FALSE; if (!fu_firmware_parse_stream(ish_img, stream, loc, flags, error)) return FALSE; fu_firmware_set_addr(ish_img, loc); fu_firmware_add_image(firmware, ish_img); /* parse the csm image */ loc = fu_struct_image_slot_header_get_loc_csm(ish); fu_firmware_set_addr(csm_img, loc); if (!fu_firmware_parse_stream(csm_img, stream, loc, flags, error)) return FALSE; loc = fu_struct_image_slot_header_get_loc(ish); sz = fu_struct_image_slot_header_get_slot_max_size(ish); switch (fu_struct_image_slot_header_get_fw_id(ish)) { case FU_FWID_PARTITION_A_L2: fu_firmware_set_id(l2_img, "PARTITION_A"); fu_firmware_set_id(csm_img, "ATOM_CSM_A"); break; case FU_FWID_PARTITION_B_L2: fu_firmware_set_id(l2_img, "PARTITION_B"); fu_firmware_set_id(csm_img, "ATOM_CSM_B"); break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "unknown Partition FWID: %x", fu_struct_image_slot_header_get_fw_id(ish)); return FALSE; } fu_firmware_add_image(l2_img, csm_img); l2_stream = fu_partial_input_stream_new(stream, loc, sz, error); if (l2_stream == NULL) return FALSE; fu_firmware_set_addr(l2_img, loc); if (!fu_firmware_parse_stream(l2_img, l2_stream, 0x0, flags, error)) return FALSE; fu_firmware_add_image(ish_img, l2_img); /* parse the partition */ if (!fu_amd_gpu_psp_firmware_parse_l2(l2_img, stream, loc, error)) return FALSE; /* next entry */ offset += l1_entry->len; } return TRUE; } static gboolean fu_amd_gpu_psp_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuAmdGpuPspFirmware *self = FU_AMD_GPU_PSP_FIRMWARE(firmware); g_autoptr(GByteArray) efs = NULL; efs = fu_struct_efs_parse_stream(stream, 0, error); if (efs == NULL) return FALSE; self->dir_location = fu_struct_efs_get_psp_dir_loc(efs); return fu_amd_gpu_psp_firmware_parse_l1(firmware, stream, self->dir_location, flags, error); } static void fu_amd_gpu_psp_firmware_init(FuAmdGpuPspFirmware *self) { } static void fu_amd_gpu_psp_firmware_class_init(FuAmdGpuPspFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_amd_gpu_psp_firmware_validate; firmware_class->parse = fu_amd_gpu_psp_firmware_parse; firmware_class->export = fu_amd_gpu_psp_firmware_export; } /** * fu_amd_gpu_psp_firmware_new * * Creates a new #FuFirmware of sub type amd-gpu-psp * * Since: 1.9.6 **/ FuFirmware * fu_amd_gpu_psp_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_AMD_GPU_PSP_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/amd-gpu/fu-amd-gpu-psp-firmware.h000066400000000000000000000012051501337203100224500ustar00rootroot00000000000000/* * Copyright 2023 Advanced Micro Devices Inc. * All rights reserved. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_AMD_GPU_PSP_FIRMWARE (fu_amd_gpu_psp_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuAmdGpuPspFirmware, fu_amd_gpu_psp_firmware, FU, AMD_GPU_PSP_FIRMWARE, FuFirmware) FuFirmware * fu_amd_gpu_psp_firmware_new(void); fwupd-2.0.10/plugins/amd-gpu/fu-amd-gpu-psp.rs000066400000000000000000000022461501337203100210410ustar00rootroot00000000000000// Copyright 2023 Advanced Micro Devices Inc. // SPDX-License-Identifier: LGPL-2.1-or-later OR MIT #[derive(ParseStream, Default)] #[repr(C, packed)] struct FuStructEfs { signature: u32le = 0x55aa55aa, reserved: [u32le; 4], psp_dir_loc: u32le, reserved: [u32le; 5], _psp_dir_loc_back: u32le, reserved: [u32le; 6], _psp_dir_ind_loc: u32le, _rom_strap_a_loc: u32le, _rom_strap_b_loc: u32le, } #[derive(ParseStream, ValidateStream, Getters, Default)] #[repr(C, packed)] struct FuStructPspDir { cookie: [char; 4] == "$PSP", checksum: u32le, total_entries: u32le, reserved: u32le, } #[derive(ParseStream)] #[repr(C, packed)] struct FuStructPspDirTable { fw_id: u32le, size: u32le, loc: u64le, } #[derive(ParseStream)] #[repr(C, packed)] struct FuStructImageSlotHeader { checksum: u32le, boot_priority: u32le, update_retries: u32le, glitch_retries: u8, fw_id: u16le, reserved: u8, loc: u32le, psp_id: u32le, slot_max_size: u32le, loc_csm: u32le, } #[repr(u8)] enum FuFwid { AtomCsm = 0x1, PartitionAL2 = 0x014D, PartitionBL2 = 0x014E, IshA = 0x013C, IshB = 0x013D, } fwupd-2.0.10/plugins/amd-gpu/meson.build000066400000000000000000000015351501337203100200730ustar00rootroot00000000000000if host_machine.system() == 'linux' and libdrm_amdgpu.found() cargs = ['-DG_LOG_DOMAIN="FuPluginAmdGpu"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('amd-gpu.quirk') plugin_builtins += static_library('fu_plugin_amd_gpu', rustgen.process( 'fu-amd-gpu-atom.rs', # fuzzing 'fu-amd-gpu-psp.rs', # fuzzing ), sources: [ 'fu-amd-gpu-plugin.c', 'fu-amd-gpu-device.c', 'fu-amd-gpu-psp-firmware.c', 'fu-amd-gpu-atom-firmware.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) enumeration_data += files('tests/amd-apu-setup.json', 'tests/amd-dgpu-setup.json') device_tests += files('tests/amd-apu.json', 'tests/amd-dgpu.json', 'tests/amd-dgpu-navi3x.json', ) endif fwupd-2.0.10/plugins/amd-gpu/tests/000077500000000000000000000000001501337203100170675ustar00rootroot00000000000000fwupd-2.0.10/plugins/amd-gpu/tests/amd-apu-setup.json000066400000000000000000000055071501337203100224530ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUdevDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:08.1/0000:c4:00.0", "DeviceFile": "/dev/dri/card0", "Subsystem": "pci", "Driver": "amdgpu", "BindId": "0000:c4:00.0", "Vendor": 4098, "Model": 5390, "Created": "2024-11-01T17:27:45.279095Z", "Events": [ { "Id": "#d5a801ad", "Data": "pci" }, { "Id": "#ad8c58d3", "Data": "amdgpu" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "DRIVER=amdgpu\nPCI_CLASS=38000\nPCI_ID=1002:150E\nPCI_SUBSYS_ID=1043:1DF3\nPCI_SLOT_NAME=0000:c4:00.0\nMODALIAS=pci:v00001002d0000150Esv00001043sd00001DF3bc03sc80i00" }, { "Id": "#d432c663" }, { "Id": "#9b895db2", "Data": "0x1002" }, { "Id": "#66f3e150", "Data": "0x150e" }, { "Id": "#d410b6c7", "Data": "0x038000" }, { "Id": "#1075ed5c" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "DRIVER=amdgpu\nPCI_CLASS=38000\nPCI_ID=1002:150E\nPCI_SUBSYS_ID=1043:1DF3\nPCI_SLOT_NAME=0000:c4:00.0\nMODALIAS=pci:v00001002d0000150Esv00001043sd00001DF3bc03sc80i00" }, { "Id": "#d432c663" }, { "Id": "#9b895db2", "Data": "0x1002" }, { "Id": "#66f3e150", "Data": "0x150e" }, { "Id": "#d410b6c7", "Data": "0x038000" }, { "Id": "#1075ed5c" }, { "Id": "#d410b6c7", "Data": "0x038000" }, { "Id": "#41bc32d8", "Data": "113-STRIXEMU-001" }, { "Id": "#bf29d2f6", "Data": "0xc1" }, { "Id": "#269abd81", "Data": "0x1043" }, { "Id": "#360cec38", "Data": "0x1df3" }, { "Id": "#d2629d83", "Data": "0000:c4:00.0" }, { "Id": "#7305decd", "Filename": "/dev/dri/card0" }, { "Id": "#c0ff063f", "Exists": 0 }, { "Id": "#1b923827", "Exists": 0 }, { "Id": "#cdb78a8a", "Exists": 0 }, { "Id": "#9747b377", "DataOut": "QU1EIFNUUklYX0VNVQAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgADExMy1TVFJJWEVNVS0wMDEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARAQoXAAAAADAyMy4wMTAuMDAxLjAxNy4wMDAwMDEAU1RSSVguYmluMjAyNC8wNi8xNCAxNzo0MQAAAAAAAAAAAAAAAAAAAAA=" } ] } ] } fwupd-2.0.10/plugins/amd-gpu/tests/amd-apu.json000066400000000000000000000004641501337203100213120ustar00rootroot00000000000000{ "name": "AMD Strix", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/amd-apu-setup.json", "components": [ { "version": "1", "guids": [ "cc72cddc-68ef-5809-afce-bb1e5bcf571f" ] } ] } ] } fwupd-2.0.10/plugins/amd-gpu/tests/amd-dgpu-navi3x.json000066400000000000000000000007401501337203100226670ustar00rootroot00000000000000{ "name": "AMD Navi31", "interactive": false, "steps": [ { "url": "5885a99e908d37ae8cad445cb2b06f1c93d468d36fa53a2355604ba0116fbba5-D704_XT_A0_20GB_MBA_NAVI31_Baseline_PRD004B_69818.cab", "emulation-url": "808f6a085f8a59a8db62d08921e786e15d252eb3cf5069ee4c0b991e4a250cf1-navi31.zip", "components": [ { "version": "114", "guids": [ "8ed32270-e736-52e2-87d8-acef1dd2a946" ] } ] } ] } fwupd-2.0.10/plugins/amd-gpu/tests/amd-dgpu-setup.json000066400000000000000000000055101501337203100226170ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUdevDevice", "BackendId": "/sys/devices/pci0000:60/0000:60:03.1/0000:61:00.0", "DeviceFile": "/dev/dri/card0", "Subsystem": "pci", "Driver": "amdgpu", "BindId": "0000:61:00.0", "Vendor": 4098, "Model": 27009, "Created": "2024-11-01T16:46:04.782170Z", "Events": [ { "Id": "#d5a801ad", "Data": "pci" }, { "Id": "#ad8c58d3", "Data": "amdgpu" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "DRIVER=amdgpu\nPCI_CLASS=30000\nPCI_ID=1002:6981\nPCI_SUBSYS_ID=17AA:104C\nPCI_SLOT_NAME=0000:61:00.0\nMODALIAS=pci:v00001002d00006981sv000017AAsd0000104Cbc03sc00i00" }, { "Id": "#d432c663" }, { "Id": "#9b895db2", "Data": "0x1002" }, { "Id": "#66f3e150", "Data": "0x6981" }, { "Id": "#d410b6c7", "Data": "0x030000" }, { "Id": "#1075ed5c" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "DRIVER=amdgpu\nPCI_CLASS=30000\nPCI_ID=1002:6981\nPCI_SUBSYS_ID=17AA:104C\nPCI_SLOT_NAME=0000:61:00.0\nMODALIAS=pci:v00001002d00006981sv000017AAsd0000104Cbc03sc00i00" }, { "Id": "#d432c663" }, { "Id": "#9b895db2", "Data": "0x1002" }, { "Id": "#66f3e150", "Data": "0x6981" }, { "Id": "#d410b6c7", "Data": "0x030000" }, { "Id": "#1075ed5c" }, { "Id": "#d410b6c7", "Data": "0x030000" }, { "Id": "#41bc32d8", "Data": "113-D0155500-100" }, { "Id": "#bf29d2f6", "Data": "0x10" }, { "Id": "#269abd81", "Data": "0x17aa" }, { "Id": "#360cec38", "Data": "0x104c" }, { "Id": "#d2629d83", "Data": "0000:61:00.0" }, { "Id": "#7c908431", "Filename": "/dev/dri/card0" }, { "Id": "#849bc01d", "Exists": 1 }, { "Id": "#e6156e67", "Exists": 0 }, { "Id": "#f317d8be", "Exists": 0 }, { "Id": "#9747b377", "DataOut": "RDAxNTU1IFBvbGFyaXMyMyBHTFhUIERUIEEwIEdERFI1IDI1Nk14MzIgNEdCIDM1VwAgICAgICAgICAgICAgADExMy1EMDE1NTUwMC0xMDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzIPAAAAADAxNS4wNTAuMDAzLjAwMC4wMTI2MzYARDAxNTU1MDAuMjAxOS8wNy8wMiAxNzowNAAAAAAAAAAAAAAAAAAAAAA=" } ] } ] } fwupd-2.0.10/plugins/amd-gpu/tests/amd-dgpu.json000066400000000000000000000004711501337203100214620ustar00rootroot00000000000000{ "name": "AMD Polaris", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/amd-dgpu-setup.json", "components": [ { "version": "100", "guids": [ "fec45400-2aa5-51b9-9bba-091bfb972433" ] } ] } ] } fwupd-2.0.10/plugins/amd-kria/000077500000000000000000000000001501337203100160605ustar00rootroot00000000000000fwupd-2.0.10/plugins/amd-kria/README.md000066400000000000000000000024461501337203100173450ustar00rootroot00000000000000--- title: Plugin: AMD Kria --- ## Introduction The AMD Kria plugin is used to represent the system firmware stored on QSPI for the AMD Kria system on module device specifically when not booted using UEFI support in U-Boot. **When UEFI support is used the plugin will be disabled.** It uses the devices created by the mtd plugin to discover the firmware version and uses the known behavior of U-Boot ESP handling to distribute updates to the device. U-Boot will automatically pick up the firmware (so no efivars are needed) and will also clean up the firmware after upgrade is completed. ## GUID Generation These devices use a GUID generation scheme that reflects data stored in the EEPROM on the board the Kria SoM is inserted into. * `UEFI\VENDOR_XILINX` * `UEFI\VENDOR_XILINX&PRODUCT_SMK-K26-XCL2G` ## Firmware Format The firmware is distributed in UEFI capsule format and it's format is described in . * `org.uefi.capsule` ## Quirks * `AmdKriaEepromAddr` represents the I2C address for the SoM EEPROM ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Mario Limonciello: @superm1 * Michal Simek @michalsimek fwupd-2.0.10/plugins/amd-kria/eeprom.txt000066400000000000000000000037231501337203100201150ustar00rootroot00000000000000$ sudo ipmi-fru --fru-file=/sys/bus/i2c/devices/1-0051/eeprom --interpret-oem-data FRU Inventory From File: /sys/bus/i2c/devices/1-0051/eeprom FRU Board Manufacturing Date/Time: 05/13/24 - 18:26:00 FRU Board Manufacturer: XILINX FRU Board Product Name: SCK-KV-G FRU Board Serial Number: XFL1XEYN4SLK FRU Board Part Number: 5066-01 FRU FRU File ID: 00h FRU Board Custom Info: 1 FRU Board Custom Info: 10h EEh 00h 00h 00h 00h 00h 00h FRU Board Custom Info: C5h 4Eh 65h 57h B5h 9Ah 4Bh 48h A0h C4h 53h 7Ah 9Fh 53h B0h CBh FRU DC Load Output Number: 1 FRU DC Load Nominal Voltage: 12000 mV FRU DC Load Spec'd Minimum Voltage: 11500 mV FRU DC Load Spec'd Maximum Voltage: 12500 mV FRU DC Load Spec'd Ripple and Noise pk-pk: 100 mV FRU DC Load Minimum Current Load: 0 mA FRU DC Load Maximum Current Load: 3000 mA FRU Error: multirecord area checksum invalid ipmi_fru_next: multirecord area checksum invalid $ sudo ipmi-fru --fru-file=/sys/bus/i2c/devices/1-0050/eeprom --interpret-oem-data FRU Inventory From File: /sys/bus/i2c/devices/1-0050/eeprom FRU Board Manufacturing Date/Time: 05/13/24 - 18:28:00 FRU Board Manufacturer: XILINX FRU Board Product Name: SMK-K26-XCL2G FRU Board Serial Number: XFL1T4UVG44P FRU Board Part Number: 5057-01 FRU FRU File ID: 00h FRU Board Custom Info: 1 FRU Board Custom Info: 10h EEh 00h 00h 00h 00h 00h 00h FRU Board Custom Info: 92h 2Ah B2h C5h F9h F1h 43h BFh 81h 38h DDh 73h 55h ABh 62h 71h FRU DC Load Output Number: 1 FRU DC Load Nominal Voltage: 5000 mV FRU DC Load Spec'd Minimum Voltage: 4500 mV FRU DC Load Spec'd Maximum Voltage: 5500 mV FRU DC Load Spec'd Ripple and Noise pk-pk: 100 mV FRU DC Load Minimum Current Load: 0 mA FRU DC Load Maximum Current Load: 4000 mA FRU OEM Manufacturer ID: Xilinx, Inc. (10DAh) FRU OEM MAC Version: DUT - MAC (31h) FRU OEM MAC ID 0: 00:0a:35:17:98:8f FRU Error: multirecord area checksum invalid ipmi_fru_next: multirecord area checksum invalidfwupd-2.0.10/plugins/amd-kria/fu-amd-kria-device.c000066400000000000000000000173401501337203100215630ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2024 Advanced Micro Devices Inc. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include #include "fu-amd-kria-device.h" #include "fu-amd-kria-som-eeprom.h" typedef struct { FuVolume *esp; FuDeviceLocker *esp_locker; gchar *eeprom_address; } FuAmdKriaDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuAmdKriaDevice, fu_amd_kria_device, FU_TYPE_I2C_DEVICE) #define GET_PRIVATE(o) (fu_amd_kria_device_get_instance_private(o)) static void fu_amd_kria_device_to_string(FuDevice *device, guint idt, GString *str) { FuAmdKriaDevice *self = FU_AMD_KRIA_DEVICE(device); FuAmdKriaDevicePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append(str, idt, "AmdKriaEepromAddr", priv->eeprom_address); } static gboolean fu_amd_kria_device_prepare(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuAmdKriaDevice *self = FU_AMD_KRIA_DEVICE(device); FuAmdKriaDevicePrivate *priv = GET_PRIVATE(self); priv->esp_locker = fu_volume_locker(priv->esp, error); if (priv->esp_locker == NULL) return FALSE; return TRUE; } static gboolean fu_amd_kria_device_cleanup(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuAmdKriaDevice *self = FU_AMD_KRIA_DEVICE(device); FuAmdKriaDevicePrivate *priv = GET_PRIVATE(self); if (!fu_device_locker_close(priv->esp_locker, error)) return FALSE; g_clear_object(&priv->esp_locker); return TRUE; } static gboolean fu_amd_kria_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuAmdKriaDevice *self = FU_AMD_KRIA_DEVICE(device); FuAmdKriaDevicePrivate *priv = GET_PRIVATE(self); g_autofree gchar *cod_path = NULL; g_autoptr(GBytes) fw = NULL; fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; cod_path = g_build_filename(fu_volume_get_mount_point(priv->esp), "EFI", "UpdateCapsule", "fwupd.cap", NULL); g_debug("using %s for capsule", cod_path); if (!fu_path_mkdir_parent(cod_path, error)) return FALSE; if (!fu_bytes_set_contents(cod_path, fw, error)) return FALSE; return TRUE; } static gboolean fu_amd_kria_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuAmdKriaDevice *self = FU_AMD_KRIA_DEVICE(device); FuAmdKriaDevicePrivate *priv = GET_PRIVATE(self); if (g_strcmp0(key, "AmdKriaEepromAddr") == 0) { priv->eeprom_address = g_strdup(value); return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static gboolean fu_amd_kria_device_probe(FuDevice *device, GError **error) { FuAmdKriaDevice *self = FU_AMD_KRIA_DEVICE(device); FuAmdKriaDevicePrivate *priv = GET_PRIVATE(self); const gchar *tmp; g_autofree gchar *prop_of_fullname = NULL; g_auto(GStrv) of_name = NULL; /* FuI2cDevice->probe */ if (!FU_DEVICE_CLASS(fu_amd_kria_device_parent_class)->probe(device, error)) return FALSE; /* * Fetch the OF_FULLNAME udev property and look for the I2C address in it * sample format: OF_FULLNAME=/axi/i2c@ff030000/eeprom@50 */ prop_of_fullname = fu_udev_device_read_property(FU_UDEV_DEVICE(device), "OF_FULLNAME", error); if (prop_of_fullname == NULL) return FALSE; of_name = fu_strsplit(prop_of_fullname, strlen(prop_of_fullname), "@", -1); if (of_name == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no '@' found in %s", prop_of_fullname); return FALSE; } tmp = of_name[g_strv_length(of_name) - 1]; if (g_strcmp0(priv->eeprom_address, tmp) != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid device"); return FALSE; } return TRUE; } static gboolean fu_amd_kria_device_setup(FuDevice *device, GError **error) { const gchar *devpath = fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device)); gsize bufsz = 0; g_autofree gchar *buf = NULL; g_autofree gchar *path = g_build_path("/", devpath, "eeprom", NULL); g_autoptr(FuFirmware) firmware = NULL; g_autoptr(GError) error_esp = NULL; g_autoptr(GBytes) bytes = NULL; if (!g_file_get_contents(path, &buf, &bufsz, error)) return FALSE; /* parse the eeprom */ bytes = g_bytes_new(buf, bufsz); firmware = fu_amd_kria_som_eeprom_new(); if (!fu_firmware_parse_bytes(firmware, bytes, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return FALSE; /* build instance IDs from EEPROM data */ fu_device_set_vendor( device, fu_amd_kria_som_eeprom_get_manufacturer(FU_AMD_KRIA_SOM_EEPROM(firmware))); fu_device_build_vendor_id(device, "DMI", fu_device_get_vendor(device)); fu_device_add_instance_str(device, "VENDOR", fu_device_get_vendor(device)); fu_device_add_instance_str( device, "PRODUCT", fu_amd_kria_som_eeprom_get_product_name(FU_AMD_KRIA_SOM_EEPROM(firmware))); fu_device_set_serial( device, fu_amd_kria_som_eeprom_get_serial_number(FU_AMD_KRIA_SOM_EEPROM(firmware))); if (!fu_device_build_instance_id(device, error, "UEFI", "VENDOR", NULL)) return FALSE; if (!fu_device_build_instance_id(device, error, "UEFI", "VENDOR", "PRODUCT", NULL)) return FALSE; return TRUE; } static void fu_amd_kria_device_constructed(GObject *obj) { FuAmdKriaDevice *self = FU_AMD_KRIA_DEVICE(obj); FuAmdKriaDevicePrivate *priv = GET_PRIVATE(self); FuContext *ctx; g_autoptr(GError) error_esp = NULL; /* setup the default ESP */ ctx = fu_device_get_context(FU_DEVICE(obj)); priv->esp = fu_context_get_default_esp(ctx, &error_esp); if (priv->esp == NULL) fu_device_inhibit(FU_DEVICE(obj), "no-esp", error_esp->message); } static void fu_amd_kria_device_init(FuAmdKriaDevice *self) { fu_device_set_name(FU_DEVICE(self), "System Firmware"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_logical_id(FU_DEVICE(self), "U-Boot"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_REBOOT); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_COMPUTER); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_set_summary(FU_DEVICE(self), "AMD Kria device (Updated via capsule-on-disk)"); fu_device_add_protocol(FU_DEVICE(self), "org.uefi.capsule"); } static void fu_amd_kria_device_finalize(GObject *object) { FuAmdKriaDevice *self = FU_AMD_KRIA_DEVICE(object); FuAmdKriaDevicePrivate *priv = GET_PRIVATE(self); if (priv->esp != NULL) g_object_unref(priv->esp); if (priv->esp_locker != NULL) g_object_unref(priv->esp_locker); g_free(priv->eeprom_address); G_OBJECT_CLASS(fu_amd_kria_device_parent_class)->finalize(object); } static void fu_amd_kria_device_class_init(FuAmdKriaDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->finalize = fu_amd_kria_device_finalize; object_class->constructed = fu_amd_kria_device_constructed; device_class->set_quirk_kv = fu_amd_kria_device_set_quirk_kv; device_class->setup = fu_amd_kria_device_setup; device_class->prepare = fu_amd_kria_device_prepare; device_class->cleanup = fu_amd_kria_device_cleanup; device_class->probe = fu_amd_kria_device_probe; device_class->write_firmware = fu_amd_kria_device_write_firmware; device_class->to_string = fu_amd_kria_device_to_string; } fwupd-2.0.10/plugins/amd-kria/fu-amd-kria-device.h000066400000000000000000000006511501337203100215650ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * Copyright 2024 Advanced Micro Devices Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_AMD_KRIA_DEVICE (fu_amd_kria_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuAmdKriaDevice, fu_amd_kria_device, FU, AMD_KRIA_DEVICE, FuI2cDevice) struct _FuAmdKriaDeviceClass { FuI2cDeviceClass parent_class; }; fwupd-2.0.10/plugins/amd-kria/fu-amd-kria-image-firmware.c000066400000000000000000000034171501337203100232200ustar00rootroot00000000000000/* * Copyright 2024 Advanced Micro Devices Inc. * All rights reserved. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-amd-kria-image-firmware.h" struct _FuAmdKriaImageFirmware { FuFirmwareClass parent_instance; }; #define VERSION_OFFSET 0x70 #define VERSION_SIZE 0x24 G_DEFINE_TYPE(FuAmdKriaImageFirmware, fu_amd_kria_image_firmware, FU_TYPE_FIRMWARE) static gboolean fu_amd_kria_image_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { const gchar *buf; gsize bufsz = 0; g_autoptr(GBytes) fw = NULL; g_autofree gchar *version = NULL; fw = fu_input_stream_read_bytes(stream, VERSION_OFFSET, VERSION_SIZE, NULL, error); if (fw == NULL) return FALSE; buf = g_bytes_get_data(fw, &bufsz); version = fu_strsafe(buf, bufsz); if (version == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_SIGNATURE_INVALID, "no valid version"); return FALSE; } fu_firmware_set_version(FU_FIRMWARE(firmware), version); return TRUE; } static void fu_amd_kria_image_firmware_init(FuAmdKriaImageFirmware *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_NO_AUTO_DETECTION); } static void fu_amd_kria_image_firmware_class_init(FuAmdKriaImageFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_amd_kria_image_firmware_parse; } FuFirmware * fu_amd_kria_image_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_AMD_KRIA_IMAGE_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/amd-kria/fu-amd-kria-image-firmware.h000066400000000000000000000012271501337203100232220ustar00rootroot00000000000000/* * Copyright 2024 Advanced Micro Devices Inc. * All rights reserved. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_AMD_KRIA_IMAGE_FIRMWARE (fu_amd_kria_image_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuAmdKriaImageFirmware, fu_amd_kria_image_firmware, FU, AMD_KRIA_IMAGE_FIRMWARE, FuFirmware) FuFirmware * fu_amd_kria_image_firmware_new(void); fwupd-2.0.10/plugins/amd-kria/fu-amd-kria-persistent-firmware.c000066400000000000000000000042431501337203100243340ustar00rootroot00000000000000/* * Copyright 2024 Advanced Micro Devices Inc. * All rights reserved. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-amd-kria-persistent-firmware.h" #include "fu-amd-kria-persistent-struct.h" struct _FuAmdKriaPersistentFirmware { FuFirmwareClass parent_instance; FuAmdKriaBootImageId last_booted; }; G_DEFINE_TYPE(FuAmdKriaPersistentFirmware, fu_amd_kria_persistent_firmware, FU_TYPE_FIRMWARE) static gboolean fu_amd_kria_persistent_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuAmdKriaPersistentFirmware *self = FU_AMD_KRIA_PERSISTENT_FIRMWARE(firmware); g_autoptr(FuStructAmdKriaPersistReg) content = NULL; content = fu_struct_amd_kria_persist_reg_parse_stream(stream, 0x0, error); if (content == NULL) return FALSE; self->last_booted = fu_struct_amd_kria_persist_reg_get_last_booted_img(content); return TRUE; } gboolean fu_amd_kria_persistent_firmware_booted_image_a(FuAmdKriaPersistentFirmware *self) { return self->last_booted == FU_AMD_KRIA_BOOT_IMAGE_ID_A; } static void fu_amd_kria_persistent_firmware_init(FuAmdKriaPersistentFirmware *self) { } static void fu_amd_kria_persistent_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuAmdKriaPersistentFirmware *self = FU_AMD_KRIA_PERSISTENT_FIRMWARE(firmware); fu_xmlb_builder_insert_kv(bn, "last_booted", self->last_booted == FU_AMD_KRIA_BOOT_IMAGE_ID_A ? "A" : "B"); } static void fu_amd_kria_persistent_firmware_class_init(FuAmdKriaPersistentFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_amd_kria_persistent_firmware_parse; firmware_class->export = fu_amd_kria_persistent_firmware_export; } FuFirmware * fu_amd_kria_persistent_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_AMD_KRIA_PERSISTENT_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/amd-kria/fu-amd-kria-persistent-firmware.h000066400000000000000000000014221501337203100243350ustar00rootroot00000000000000/* * Copyright 2024 Advanced Micro Devices Inc. * All rights reserved. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_AMD_KRIA_PERSISTENT_FIRMWARE (fu_amd_kria_persistent_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuAmdKriaPersistentFirmware, fu_amd_kria_persistent_firmware, FU, AMD_KRIA_PERSISTENT_FIRMWARE, FuFirmware) FuFirmware * fu_amd_kria_persistent_firmware_new(void); gboolean fu_amd_kria_persistent_firmware_booted_image_a(FuAmdKriaPersistentFirmware *self); fwupd-2.0.10/plugins/amd-kria/fu-amd-kria-persistent.rs000066400000000000000000000010011501337203100227110ustar00rootroot00000000000000// Copyright 2024 Advanced Micro Devices Inc. // SPDX-License-Identifier: LGPL-2.1-or-later OR MIT #[derive(ParseStream, Default)] #[repr(C, packed)] struct FuStructAmdKriaPersistReg { id_sig: [char; 4] == "ABUM", ver: u32le, len: u32le, checksum: u32le, last_booted_img: u8, requested_booted_img: u8, img_b_bootable: u8, img_a_bootable: u8, img_a_offset: u32le, img_b_offset: u32le, recovery_offset: u32le, } #[repr(u8)] enum FuAmdKriaBootImageId { A, B, } fwupd-2.0.10/plugins/amd-kria/fu-amd-kria-plugin.c000066400000000000000000000131611501337203100216170ustar00rootroot00000000000000/* * Copyright 2024 Advanced Micro Devices Inc. * All rights reserved. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-amd-kria-device.h" #include "fu-amd-kria-image-firmware.h" #include "fu-amd-kria-persistent-firmware.h" #include "fu-amd-kria-plugin.h" #include "fu-amd-kria-som-eeprom.h" struct _FuAmdKriaPlugin { FuPlugin parent_instance; gchar *version_a; gchar *version_b; const gchar *active; }; G_DEFINE_TYPE(FuAmdKriaPlugin, fu_amd_kria_plugin, FU_TYPE_PLUGIN) static void fu_amd_kria_plugin_init(FuAmdKriaPlugin *self) { } static gboolean fu_amd_kria_plugin_process_image(FuPlugin *plugin, FuDevice *dev, GError **error) { g_autoptr(GBytes) bytes = NULL; g_autoptr(FuFirmware) firmware = NULL; g_autoptr(FuProgress) progress = fu_progress_new(NULL); bytes = fu_device_dump_firmware(dev, progress, error); if (bytes == NULL) return FALSE; firmware = fu_amd_kria_image_firmware_new(); if (!fu_firmware_parse_bytes(firmware, bytes, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return FALSE; fu_device_set_version(dev, fu_firmware_get_version(firmware)); return TRUE; } static gboolean fu_amd_kria_plugin_process_persistent(FuPlugin *plugin, FuDevice *dev, GError **error) { FuAmdKriaPlugin *self = FU_AMD_KRIA_PLUGIN(plugin); g_autoptr(GBytes) bytes = NULL; g_autoptr(FuFirmware) firmware = NULL; g_autoptr(FuProgress) progress = fu_progress_new(NULL); bytes = fu_device_dump_firmware(dev, progress, error); if (bytes == NULL) return FALSE; firmware = fu_amd_kria_persistent_firmware_new(); if (!fu_firmware_parse_bytes(firmware, bytes, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return FALSE; if (fu_amd_kria_persistent_firmware_booted_image_a( FU_AMD_KRIA_PERSISTENT_FIRMWARE(firmware))) self->active = "A"; else self->active = "B"; return TRUE; } static void fu_amd_kria_plugin_device_registered(FuPlugin *plugin, FuDevice *dev) { FuAmdKriaPlugin *self; const gchar *name; g_autoptr(GError) error_local = NULL; if (g_strcmp0(fu_device_get_plugin(dev), "mtd") != 0) return; self = FU_AMD_KRIA_PLUGIN(plugin); name = fu_device_get_name(dev); if (g_strcmp0(name, "Image A") == 0) { if (!fu_amd_kria_plugin_process_image(plugin, dev, &error_local)) g_warning("%s", error_local->message); self->version_a = g_strdup(fu_device_get_version(dev)); } else if (g_strcmp0(name, "Image B") == 0) { if (!fu_amd_kria_plugin_process_image(plugin, dev, &error_local)) g_warning("%s", error_local->message); self->version_b = g_strdup(fu_device_get_version(dev)); } else if (g_strcmp0(name, "Persistent Register") == 0) { if (!fu_amd_kria_plugin_process_persistent(plugin, dev, &error_local)) g_warning("%s", error_local->message); } /* mark the active partition version on the created KRIA device */ if (fu_device_get_parent(dev) != NULL && fu_device_get_version(dev) != NULL) { if (g_strcmp0(self->active, "A") == 0 && self->version_a != NULL) fu_device_set_version(fu_device_get_parent(dev), self->version_a); else if (g_strcmp0(self->active, "B") == 0 && self->version_b != NULL) fu_device_set_version(fu_device_get_parent(dev), self->version_b); fu_device_add_flag(fu_device_get_parent(dev), FWUPD_DEVICE_FLAG_UPDATABLE); } fu_device_remove_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE); } static void fu_amd_kria_plugin_to_string(FuPlugin *plugin, guint idt, GString *str) { FuAmdKriaPlugin *self = FU_AMD_KRIA_PLUGIN(plugin); fwupd_codec_string_append(str, idt, "VersionA", self->version_a); fwupd_codec_string_append(str, idt, "VersionB", self->version_b); fwupd_codec_string_append(str, idt, "Activeimage", self->active); } static gboolean fu_amd_kria_plugin_startup(FuPlugin *plugin, FuProgress *progress, GError **error) { #ifdef __aarch64__ g_autofree gchar *sysfsfwdir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR_FW); g_autofree gchar *esrt_path = g_build_filename(sysfsfwdir, "efi", "esrt", NULL); /* if there is an ESRT use that instead and disable the plugin */ if (g_file_test(esrt_path, G_FILE_TEST_IS_DIR)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "system uses UEFI ESRT"); return FALSE; } return TRUE; #else g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "only for aarch64"); return FALSE; #endif } static void fu_amd_kria_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); /* for parsing QSPI in registered callback */ fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_AMD_KRIA_IMAGE_FIRMWARE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_AMD_KRIA_PERSISTENT_FIRMWARE); /* for reading FRU inventory */ fu_plugin_add_device_gtype(plugin, FU_TYPE_AMD_KRIA_DEVICE); fu_plugin_add_udev_subsystem(plugin, "i2c"); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_AMD_KRIA_SOM_EEPROM); } static void fu_amd_kria_plugin_finalize(GObject *obj) { FuAmdKriaPlugin *self = FU_AMD_KRIA_PLUGIN(obj); g_free(self->version_a); g_free(self->version_b); G_OBJECT_CLASS(fu_amd_kria_plugin_parent_class)->finalize(obj); } static void fu_amd_kria_plugin_class_init(FuAmdKriaPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_amd_kria_plugin_finalize; plugin_class->startup = fu_amd_kria_plugin_startup; plugin_class->device_registered = fu_amd_kria_plugin_device_registered; plugin_class->constructed = fu_amd_kria_plugin_constructed; plugin_class->to_string = fu_amd_kria_plugin_to_string; } fwupd-2.0.10/plugins/amd-kria/fu-amd-kria-plugin.h000066400000000000000000000007361501337203100216300ustar00rootroot00000000000000/* * Copyright 2024 Advanced Micro Devices Inc. * All rights reserved. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include G_DECLARE_FINAL_TYPE(FuAmdKriaPlugin, fu_amd_kria_plugin, FU, AMD_KRIA_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/amd-kria/fu-amd-kria-som-eeprom.c000066400000000000000000000077271501337203100224170ustar00rootroot00000000000000/* * Copyright 2024 Advanced Micro Devices Inc. * All rights reserved. * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-amd-kria-som-eeprom-struct.h" #include "fu-amd-kria-som-eeprom.h" struct _FuAmdKriaSomEeprom { FuFirmwareClass parent_instance; gchar *manufacturer; gchar *product_name; gchar *serial_number; }; G_DEFINE_TYPE(FuAmdKriaSomEeprom, fu_amd_kria_som_eeprom, FU_TYPE_FIRMWARE) /* IPMI spec encodes 0:5 as length and 6:7 as "type" code */ #define LENGTH(data) data & 0x3f #define TYPE_CODE(data) data >> 6 static gboolean fu_amd_kria_som_eeprom_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuAmdKriaSomEeprom *self = FU_AMD_KRIA_SOM_EEPROM(firmware); guint8 str_len = 0; guint8 str_offset = FU_STRUCT_BOARD_INFO_OFFSET_MANUFACTURER_LEN; guint8 board_offset; const guint8 *buf; gsize bufsz = 0; g_autoptr(FuStructIpmiCommon) common = NULL; g_autoptr(FuStructBoardInfo) board = NULL; g_autoptr(GBytes) fw = NULL; /* parse IPMI common header */ common = fu_struct_ipmi_common_parse_stream(stream, 0x0, error); if (common == NULL) return FALSE; board_offset = fu_struct_ipmi_common_get_board_offset(common) * 8; /* parse board info area */ board = fu_struct_board_info_parse_stream(stream, board_offset, error); if (board == NULL) return FALSE; fw = fu_input_stream_read_bytes(stream, board_offset, fu_struct_board_info_get_length(board) * 8, NULL, error); if (fw == NULL) return FALSE; buf = fu_bytes_get_data_safe(fw, &bufsz, error); if (buf == NULL) return FALSE; /* manufacturer string in board area */ str_offset = str_offset + str_len; str_len = LENGTH(buf[str_offset]); str_offset++; self->manufacturer = fu_strsafe((gchar *)buf + str_offset, str_len); str_offset = str_offset + str_len; str_len = LENGTH(buf[str_offset]); str_offset++; self->product_name = fu_strsafe((gchar *)buf + str_offset, str_len); str_offset = str_offset + str_len; str_len = LENGTH(buf[str_offset]); str_offset++; self->serial_number = fu_strsafe((gchar *)buf + str_offset, str_len); return TRUE; } const gchar * fu_amd_kria_som_eeprom_get_manufacturer(FuAmdKriaSomEeprom *self) { return self->manufacturer; } const gchar * fu_amd_kria_som_eeprom_get_product_name(FuAmdKriaSomEeprom *self) { return self->product_name; } const gchar * fu_amd_kria_som_eeprom_get_serial_number(FuAmdKriaSomEeprom *self) { return self->serial_number; } static void fu_amd_kria_som_eeprom_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuAmdKriaSomEeprom *self = FU_AMD_KRIA_SOM_EEPROM(firmware); fu_xmlb_builder_insert_kv(bn, "manufacturer", self->manufacturer); fu_xmlb_builder_insert_kv(bn, "product_name", self->product_name); fu_xmlb_builder_insert_kv(bn, "serial_number", self->serial_number); } static void fu_amd_kria_som_eeprom_init(FuAmdKriaSomEeprom *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_NO_AUTO_DETECTION); } static void fu_amd_kria_som_eeprom_finalize(GObject *object) { FuAmdKriaSomEeprom *self = FU_AMD_KRIA_SOM_EEPROM(object); g_free(self->manufacturer); g_free(self->product_name); g_free(self->serial_number); G_OBJECT_CLASS(fu_amd_kria_som_eeprom_parent_class)->finalize(object); } static void fu_amd_kria_som_eeprom_class_init(FuAmdKriaSomEepromClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_amd_kria_som_eeprom_finalize; firmware_class->parse = fu_amd_kria_som_eeprom_parse; firmware_class->export = fu_amd_kria_som_eeprom_export; } FuFirmware * fu_amd_kria_som_eeprom_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_AMD_KRIA_SOM_EEPROM, NULL)); } fwupd-2.0.10/plugins/amd-kria/fu-amd-kria-som-eeprom.h000066400000000000000000000015641501337203100224150ustar00rootroot00000000000000/* * Copyright 2024 Advanced Micro Devices Inc. * All rights reserved. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_AMD_KRIA_SOM_EEPROM (fu_amd_kria_som_eeprom_get_type()) G_DECLARE_FINAL_TYPE(FuAmdKriaSomEeprom, fu_amd_kria_som_eeprom, FU, AMD_KRIA_SOM_EEPROM, FuFirmware) FuFirmware * fu_amd_kria_som_eeprom_new(void); const gchar * fu_amd_kria_som_eeprom_get_manufacturer(FuAmdKriaSomEeprom *self); const gchar * fu_amd_kria_som_eeprom_get_product_name(FuAmdKriaSomEeprom *self); const gchar * fu_amd_kria_som_eeprom_get_serial_number(FuAmdKriaSomEeprom *self); fwupd-2.0.10/plugins/amd-kria/fu-amd-kria-som-eeprom.rs000066400000000000000000000014371501337203100226110ustar00rootroot00000000000000// Copyright 2024 Advanced Micro Devices Inc. // SPDX-License-Identifier: LGPL-2.1-or-later OR MIT // https://www.intel.com/content/dam/www/public/us/en/documents/specification-updates/ipmi-platform-mgt-fru-info-storage-def-v1-0-rev-1-3-spec-update.pdf #[derive(ParseStream, Default)] #[repr(C, packed)] struct FuStructIpmiCommon { version: u8 = 0x1, internal_offest: u8, chassis_offeset: u8, board_offset: u8, product_offset: u8, multirecord_offset: u8, reserved: u8, checksum: u8, } #[derive(ParseStream, Default)] #[repr(C, packed)] struct FuStructBoardInfo { version: u8 = 0x1, length: u8, lang_code: u8, mfg_date: u24le, manufacturer_len: u8, } #[repr(u8)] enum FuAmdKriaTypeCode { Binary, BcdPlus, Acsii6, LangCodeDep, } fwupd-2.0.10/plugins/amd-kria/kria.quirk000066400000000000000000000014571501337203100200720ustar00rootroot00000000000000[I2C\NAME_24c64] Plugin = amd_kria AmdKriaEepromAddr = 50 # KV260 [MTD\VENDOR_xlnx&NAME_Image-A-FSBL-PMU-ATF-U-Boot] Name = Image A ParentGuid = UEFI\VENDOR_XILINX [MTD\VENDOR_xlnx&NAME_Image-B-FSBL-PMU-ATF-U-Boot] Name = Image B ParentGuid = UEFI\VENDOR_XILINX [MTD\VENDOR_xlnx&NAME_Persistent-Register] Name = Persistent Register ParentGuid = UEFI\VENDOR_XILINX [MTD\VENDOR_xlnx&NAME_Persistent-Register-Backup] Name = Persistent Register Backup ParentGuid = UEFI\VENDOR_XILINX [MTD\VENDOR_xlnx&NAME_Open-1] Flags = no-probe [MTD\VENDOR_xlnx&NAME_Open-2] Flags = no-probe [MTD\VENDOR_xlnx&NAME_Secure-OS-Storage] Flags = no-probe [MTD\VENDOR_xlnx&NAME_U-Boot-storage-variables] Flags = no-probe [MTD\VENDOR_xlnx&NAME_U-Boot-storage-variables-backup] Flags = no-probe [MTD\VENDOR_xlnx&NAME_User] Flags = no-probe fwupd-2.0.10/plugins/amd-kria/meson.build000066400000000000000000000013051501337203100202210ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginAmdKria"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('kria.quirk') plugin_builtin_kria = static_library('fu_plugin_amd_kria', rustgen.process( 'fu-amd-kria-persistent.rs', # fuzzing 'fu-amd-kria-som-eeprom.rs', # fuzzing ), sources: [ 'fu-amd-kria-plugin.c', 'fu-amd-kria-device.c', 'fu-amd-kria-image-firmware.c', 'fu-amd-kria-persistent-firmware.c', 'fu-amd-kria-som-eeprom.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) plugin_builtins += plugin_builtin_kria endif fwupd-2.0.10/plugins/amd-pmc/000077500000000000000000000000001501337203100157115ustar00rootroot00000000000000fwupd-2.0.10/plugins/amd-pmc/README.md000066400000000000000000000011751501337203100171740ustar00rootroot00000000000000--- title: Plugin: AMD PMC --- ## Introduction This plugin reports the firmware version of a microcontroller contained in AMD Zen CPU/APUs called the System Management Unit on kernels that support exporting this information. ## External Interface Access This plugin requires read only access to attributes located within `/sys/bus/platform/drivers/amd_pmc`. ## Version Considerations This plugin has been available since fwupd version `1.8.5`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Mario Limonciello: @superm1 fwupd-2.0.10/plugins/amd-pmc/amd-pmc.quirk000066400000000000000000000000531501337203100203020ustar00rootroot00000000000000[PLATFORM\DRIVER_amd_pmc] Plugin = amd_pmc fwupd-2.0.10/plugins/amd-pmc/fu-amd-pmc-device.c000066400000000000000000000050541501337203100212440ustar00rootroot00000000000000/* * Copyright 2022 Advanced Micro Devices Inc. * All rights reserved. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-amd-pmc-device.h" struct _FuAmdPmcDevice { FuUdevDevice parent_instance; }; G_DEFINE_TYPE(FuAmdPmcDevice, fu_amd_pmc_device, FU_TYPE_UDEV_DEVICE) static gboolean fu_amd_pmc_device_probe(FuDevice *device, GError **error) { guint64 program = 0; g_autofree gchar *attr_smu_program = NULL; g_autofree gchar *version = NULL; g_autoptr(GError) error_local = NULL; g_autofree gchar *summary = NULL; version = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(device), "smu_fw_version", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, &error_local); if (version == NULL) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported kernel version"); return FALSE; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } attr_smu_program = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(device), "smu_program", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, error); if (attr_smu_program == NULL) return FALSE; if (!fu_strtoull(attr_smu_program, &program, 0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_device_set_version(device, version); summary = g_strdup_printf("Microcontroller used within CPU/APU program %" G_GUINT64_FORMAT, program); fu_device_set_summary(device, summary); fu_device_add_instance_id(device, fu_device_get_backend_id(device)); return TRUE; } static void fu_amd_pmc_device_init(FuAmdPmcDevice *self) { fu_device_set_name(FU_DEVICE(self), "System Management Unit (SMU)"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_COMPUTER); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_HOST_CPU_CHILD); fu_device_set_vendor(FU_DEVICE(self), "Advanced Micro Devices, Inc."); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_physical_id(FU_DEVICE(self), "amd-pmc"); } static void fu_amd_pmc_device_class_init(FuAmdPmcDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_amd_pmc_device_probe; } fwupd-2.0.10/plugins/amd-pmc/fu-amd-pmc-device.h000066400000000000000000000010351501337203100212440ustar00rootroot00000000000000/* * Copyright 2022 Advanced Micro Devices Inc. * All rights reserved. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_AMD_PMC_DEVICE (fu_amd_pmc_device_get_type()) G_DECLARE_FINAL_TYPE(FuAmdPmcDevice, fu_amd_pmc_device, FU, AMD_PMC_DEVICE, FuUdevDevice) fwupd-2.0.10/plugins/amd-pmc/fu-amd-pmc-plugin.c000066400000000000000000000017661501337203100213110ustar00rootroot00000000000000/* * Copyright 2022 Advanced Micro Devices Inc. * All rights reserved. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-amd-pmc-device.h" #include "fu-amd-pmc-plugin.h" struct _FuAmdPmcPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuAmdPmcPlugin, fu_amd_pmc_plugin, FU_TYPE_PLUGIN) static void fu_amd_pmc_plugin_init(FuAmdPmcPlugin *self) { } static void fu_amd_pmc_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_udev_subsystem(plugin, "platform"); fu_plugin_add_device_gtype(plugin, FU_TYPE_AMD_PMC_DEVICE); } static void fu_amd_pmc_plugin_class_init(FuAmdPmcPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_amd_pmc_plugin_constructed; } fwupd-2.0.10/plugins/amd-pmc/fu-amd-pmc-plugin.h000066400000000000000000000010221501337203100212770ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * Copyright 2022 Advanced Micro Devices Inc. * All rights reserved. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include G_DECLARE_FINAL_TYPE(FuAmdPmcPlugin, fu_amd_pmc_plugin, FU, AMD_PMC_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/amd-pmc/meson.build000066400000000000000000000010761501337203100200570ustar00rootroot00000000000000if host_machine.system() == 'linux' and (host_cpu == 'x86' or host_cpu == 'x86_64') cargs = ['-DG_LOG_DOMAIN="FuPluginAmdPmc"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('amd-pmc.quirk') plugin_builtins += static_library('fu_plugin_amd_pmc', sources: [ 'fu-amd-pmc-plugin.c', 'fu-amd-pmc-device.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) enumeration_data += files('tests/amd-pmc-setup.json') device_tests += files('tests/amd-pmc.json') endif fwupd-2.0.10/plugins/amd-pmc/tests/000077500000000000000000000000001501337203100170535ustar00rootroot00000000000000fwupd-2.0.10/plugins/amd-pmc/tests/amd-pmc-setup.json000066400000000000000000000020541501337203100224230ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUdevDevice", "BackendId": "/sys/devices/platform/AMDI0009:00", "Subsystem": "platform", "Driver": "amd_pmc", "Created": "2024-11-02T01:12:31.404404Z", "Events": [ { "Id": "#d5a801ad", "Data": "platform" }, { "Id": "#ad8c58d3", "Data": "amd_pmc" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "DRIVER=amd_pmc\nMODALIAS=acpi:AMDI0009:PNP0D80:" }, { "Id": "#0d176430", "Data": "acpi:AMDI0009:PNP0D80:" }, { "Id": "#d432c663" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c" }, { "Id": "#1e802949", "Data": "76.87.0" }, { "Id": "#a7d0bb1a", "Data": "0" } ] } ] } fwupd-2.0.10/plugins/amd-pmc/tests/amd-pmc.json000066400000000000000000000004701501337203100212650ustar00rootroot00000000000000{ "name": "AMD PMC", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/amd-pmc-setup.json", "components": [ { "version": "76.87.0", "guids": [ "3ac3159e-6eef-5f6b-bc8d-67686b238747" ] } ] } ] } fwupd-2.0.10/plugins/analogix/000077500000000000000000000000001501337203100161755ustar00rootroot00000000000000fwupd-2.0.10/plugins/analogix/README.md000066400000000000000000000024461501337203100174620ustar00rootroot00000000000000--- title: Plugin: Analogix --- ## Introduction This plugin can flash the firmware of some Analogix billboard devices. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a Intel Hex file format. The resulting binary image is either: * `OCM` section only * `CUSTOM` section only * Multiple sections excluded `CUSTOM` -- at fixed offsets and sizes This plugin supports the following protocol ID: * `com.analogix.bb` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_1F29&PID_7518` * `USB\VID_050D&PID_008B` * `USB\VID_047D&PID_80C8` * `USB\VID_0502&PID_04C4` * `USB\VID_14B0&PID_01D0` * `USB\VID_14B0&PID_01D1` ## Update Behavior The device is updated at runtime using USB control transfers. ## Vendor ID Security The vendor ID is set from the USB vendor. The list of USB VIDs used is: * `USB:0x1F29` * `USB:0x050D` * `USB:0x047D` * `USB:0x0502` * `USB:0x14B0` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.6.0`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Analogix: @xtcui fwupd-2.0.10/plugins/analogix/analogix.quirk000066400000000000000000000004701501337203100210550ustar00rootroot00000000000000# Phoenix-Lite [USB\VID_1F29&PID_7518] Plugin = analogix # Belkin [USB\VID_050D&PID_008B] Plugin = analogix # Kensington [USB\VID_047D&PID_80C8] Plugin = analogix # ACER [USB\VID_0502&PID_04C4] Plugin = analogix # Startech [USB\VID_14B0&PID_01D0] Plugin = analogix [USB\VID_14B0&PID_01D1] Plugin = analogix fwupd-2.0.10/plugins/analogix/fu-analogix-common.h000066400000000000000000000022411501337203100220450ustar00rootroot00000000000000/* * Copyright 2021 Xiaotian Cui * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define ANX_BB_TRANSACTION_TIMEOUT 5000 /* ms */ #define BILLBOARD_CLASS 0x11 #define BILLBOARD_SUBCLASS 0x00 #define BILLBOARD_PROTOCOL 0x00 #define BILLBOARD_MAX_PACKET_SIZE 64 #define OCM_FLASH_SIZE 0x18000 #define SECURE_OCM_TX_SIZE 0x3000 #define SECURE_OCM_RX_SIZE 0x3000 #define CUSTOM_FLASH_SIZE 0x1000 #define FLASH_OCM_ADDR 0x1000 #define FLASH_TXFW_ADDR 0x31000 #define FLASH_RXFW_ADDR 0x34000 #define FLASH_CUSTOM_ADDR 0x38000 #define OCM_FW_VERSION_ADDR 0x14FF0 /* bRequest for Phoenix-Lite Billboard */ typedef enum { ANX_BB_RQT_SEND_UPDATE_DATA = 0x01, ANX_BB_RQT_READ_UPDATE_DATA = 0x02, ANX_BB_RQT_GET_UPDATE_STATUS = 0x10, ANX_BB_RQT_READ_FW_VER = 0x12, ANX_BB_RQT_READ_CUS_VER = 0x13, ANX_BB_RQT_READ_FW_RVER = 0x19, ANX_BB_RQT_READ_CUS_RVER = 0x1c, } AnxBbRqtCode; /* wValue low byte */ typedef enum { ANX_BB_WVAL_UPDATE_OCM = 0x06, ANX_BB_WVAL_UPDATE_CUSTOM_DEF = 0x07, ANX_BB_WVAL_UPDATE_SECURE_TX = 0x08, ANX_BB_WVAL_UPDATE_SECURE_RX = 0x09, } AnxwValCode; fwupd-2.0.10/plugins/analogix/fu-analogix-device.c000066400000000000000000000335771501337203100220270ustar00rootroot00000000000000/* * Copyright 2021 Xiaotian Cui * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-analogix-common.h" #include "fu-analogix-device.h" #include "fu-analogix-firmware.h" #include "fu-analogix-struct.h" struct _FuAnalogixDevice { FuUsbDevice parent_instance; guint16 ocm_version; guint16 custom_version; }; G_DEFINE_TYPE(FuAnalogixDevice, fu_analogix_device, FU_TYPE_USB_DEVICE) static void fu_analogix_device_to_string(FuDevice *device, guint idt, GString *str) { FuAnalogixDevice *self = FU_ANALOGIX_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "OcmVersion", self->ocm_version); fwupd_codec_string_append_hex(str, idt, "CustomVersion", self->custom_version); } static gboolean fu_analogix_device_send(FuAnalogixDevice *self, AnxBbRqtCode reqcode, guint16 val0code, guint16 index, const guint8 *buf, gsize bufsz, GError **error) { gsize actual_len = 0; g_autofree guint8 *buf_tmp = NULL; g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(bufsz <= 64, FALSE); /* make mutable */ buf_tmp = fu_memdup_safe(buf, bufsz, error); if (buf_tmp == NULL) return FALSE; /* send data to device */ if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, reqcode, /* request */ val0code, /* value */ index, /* index */ buf_tmp, /* data */ bufsz, /* length */ &actual_len, /* actual length */ (guint)ANX_BB_TRANSACTION_TIMEOUT, NULL, error)) { g_prefix_error(error, "send data error: "); return FALSE; } if (actual_len != bufsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "send data length is incorrect"); return FALSE; } /* success */ return TRUE; } static gboolean fu_analogix_device_receive(FuAnalogixDevice *self, AnxBbRqtCode reqcode, guint16 val0code, guint16 index, guint8 *buf, gsize bufsz, GError **error) { gsize actual_len = 0; g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(bufsz <= 64, FALSE); /* get data from device */ if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, reqcode, /* request */ val0code, /* value */ index, buf, /* data */ bufsz, /* length */ &actual_len, /* actual length */ (guint)ANX_BB_TRANSACTION_TIMEOUT, NULL, error)) { g_prefix_error(error, "receive data error: "); return FALSE; } if (actual_len != bufsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "receive data length is incorrect"); return FALSE; } /* success */ return TRUE; } static gboolean fu_analogix_device_get_update_status(FuAnalogixDevice *self, FuAnalogixUpdateStatus *status, GError **error) { for (guint i = 0; i < 3000; i++) { guint8 status_tmp = FU_ANALOGIX_UPDATE_STATUS_INVALID; if (!fu_analogix_device_receive(self, ANX_BB_RQT_GET_UPDATE_STATUS, 0, 0, &status_tmp, sizeof(status_tmp), error)) return FALSE; g_debug("status now: %s [0x%x]", fu_analogix_update_status_to_string(status_tmp), status_tmp); if ((status_tmp != FU_ANALOGIX_UPDATE_STATUS_ERROR) && (status_tmp != FU_ANALOGIX_UPDATE_STATUS_INVALID)) { if (status != NULL) *status = status_tmp; return TRUE; } fu_device_sleep(FU_DEVICE(self), 1); /* ms */ } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "timed out: status was invalid"); return FALSE; } static gboolean fu_analogix_device_setup(FuDevice *device, GError **error) { FuAnalogixDevice *self = FU_ANALOGIX_DEVICE(device); guint8 buf_fw[2] = {0x0}; guint8 buf_custom[2] = {0x0}; g_autofree gchar *version = NULL; /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_analogix_device_parent_class)->setup(device, error)) return FALSE; /* get OCM version */ if (!fu_analogix_device_receive(self, ANX_BB_RQT_READ_FW_VER, 0, 0, &buf_fw[1], 1, error)) return FALSE; if (!fu_analogix_device_receive(self, ANX_BB_RQT_READ_FW_RVER, 0, 0, &buf_fw[0], 1, error)) return FALSE; self->ocm_version = fu_memread_uint16(buf_fw, G_LITTLE_ENDIAN); /* get custom version */ if (!fu_analogix_device_receive(self, ANX_BB_RQT_READ_CUS_VER, 0, 0, &buf_custom[1], 1, error)) return FALSE; if (!fu_analogix_device_receive(self, ANX_BB_RQT_READ_CUS_RVER, 0, 0, &buf_custom[0], 1, error)) return FALSE; self->custom_version = fu_memread_uint16(buf_custom, G_LITTLE_ENDIAN); /* device version is both versions as a pair */ version = g_strdup_printf("%04x.%04x", self->custom_version, self->ocm_version); fu_device_set_version(FU_DEVICE(device), version); return TRUE; } static gboolean fu_analogix_device_find_interface(FuUsbDevice *device, GError **error) { FuAnalogixDevice *self = FU_ANALOGIX_DEVICE(device); g_autoptr(GPtrArray) intfs = NULL; intfs = fu_usb_device_get_interfaces(FU_USB_DEVICE(self), error); if (intfs == NULL) return FALSE; for (guint i = 0; i < intfs->len; i++) { FuUsbInterface *intf = g_ptr_array_index(intfs, i); if (fu_usb_interface_get_class(intf) == BILLBOARD_CLASS && fu_usb_interface_get_subclass(intf) == BILLBOARD_SUBCLASS && fu_usb_interface_get_protocol(intf) == BILLBOARD_PROTOCOL) { fu_usb_device_add_interface(FU_USB_DEVICE(self), fu_usb_interface_get_number(intf)); return TRUE; } } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no update interface found"); return FALSE; } static gboolean fu_analogix_device_probe(FuDevice *device, GError **error) { if (!fu_analogix_device_find_interface(FU_USB_DEVICE(device), error)) { g_prefix_error(error, "failed to find update interface: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_analogix_device_write_chunks(FuAnalogixDevice *self, FuChunkArray *chunks, guint16 req_val, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { FuAnalogixUpdateStatus status = FU_ANALOGIX_UPDATE_STATUS_INVALID; g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_analogix_device_send(self, ANX_BB_RQT_SEND_UPDATE_DATA, req_val, i + 1, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "failed send on chk %u: ", i); return FALSE; } if (!fu_analogix_device_get_update_status(self, &status, error)) { g_prefix_error(error, "failed status on chk %u: ", i); return FALSE; } fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_analogix_device_write_image(FuAnalogixDevice *self, FuFirmware *image, guint16 req_val, FuProgress *progress, GError **error) { FuAnalogixUpdateStatus status = FU_ANALOGIX_UPDATE_STATUS_INVALID; gsize streamsz = 0; guint8 buf_init[4] = {0x0}; g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "initialization"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 98, NULL); /* offset into firmware */ stream = fu_firmware_get_stream(image, error); if (stream == NULL) return FALSE; /* initialization */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; fu_memwrite_uint32(buf_init, streamsz, G_LITTLE_ENDIAN); if (!fu_analogix_device_send(self, ANX_BB_RQT_SEND_UPDATE_DATA, req_val, 0, buf_init, 3, error)) { g_prefix_error(error, "program initialized failed: "); return FALSE; } if (!fu_analogix_device_get_update_status(self, &status, error)) return FALSE; fu_progress_step_done(progress); /* write data */ chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, BILLBOARD_MAX_PACKET_SIZE, error); if (chunks == NULL) return FALSE; if (!fu_analogix_device_write_chunks(self, chunks, req_val, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_analogix_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuAnalogixDevice *self = FU_ANALOGIX_DEVICE(device); gsize totalsz = 0; g_autoptr(FuFirmware) fw_cus = NULL; g_autoptr(FuFirmware) fw_ocm = NULL; g_autoptr(FuFirmware) fw_srx = NULL; g_autoptr(FuFirmware) fw_stx = NULL; /* these are all optional */ fw_cus = fu_firmware_get_image_by_id(firmware, "custom", NULL); fw_stx = fu_firmware_get_image_by_id(firmware, "stx", NULL); fw_srx = fu_firmware_get_image_by_id(firmware, "srx", NULL); fw_ocm = fu_firmware_get_image_by_id(firmware, "ocm", NULL); /* progress */ fu_progress_set_id(progress, G_STRLOC); if (fw_cus != NULL) totalsz += fu_firmware_get_size(fw_cus); if (fw_stx != NULL) totalsz += fu_firmware_get_size(fw_stx); if (fw_srx != NULL) totalsz += fu_firmware_get_size(fw_srx); if (fw_ocm != NULL) totalsz += fu_firmware_get_size(fw_ocm); if (totalsz == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no firmware sections to update"); return FALSE; } if (fw_cus != NULL) { fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, (100 * fu_firmware_get_size(fw_cus) / totalsz), "cus"); } if (fw_stx != NULL) { fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, (100 * fu_firmware_get_size(fw_stx) / totalsz), "stx"); } if (fw_srx != NULL) { fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, (100 * fu_firmware_get_size(fw_srx) / totalsz), "srx"); } if (fw_ocm != NULL) { fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, (100 * fu_firmware_get_size(fw_ocm) / totalsz), "ocm"); } /* CUSTOM_DEF */ if (fw_cus != NULL) { if (!fu_analogix_device_write_image(self, fw_cus, ANX_BB_WVAL_UPDATE_CUSTOM_DEF, fu_progress_get_child(progress), error)) { g_prefix_error(error, "program custom define failed: "); return FALSE; } fu_progress_step_done(progress); } /* SECURE_TX */ if (fw_stx != NULL) { if (!fu_analogix_device_write_image(self, fw_stx, ANX_BB_WVAL_UPDATE_SECURE_TX, fu_progress_get_child(progress), error)) { g_prefix_error(error, "program secure TX failed: "); return FALSE; } fu_progress_step_done(progress); } /* SECURE_RX */ if (fw_srx != NULL) { if (!fu_analogix_device_write_image(self, fw_srx, ANX_BB_WVAL_UPDATE_SECURE_RX, fu_progress_get_child(progress), error)) { g_prefix_error(error, "program secure RX failed: "); return FALSE; } fu_progress_step_done(progress); } /* OCM */ if (fw_ocm != NULL) { if (!fu_analogix_device_write_image(self, fw_ocm, ANX_BB_WVAL_UPDATE_OCM, fu_progress_get_child(progress), error)) { g_prefix_error(error, "program OCM failed: "); return FALSE; } fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_analogix_device_attach(FuDevice *device, FuProgress *progress, GError **error) { g_autoptr(FwupdRequest) request = fwupd_request_new(); /* the user has to do something */ fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_IMMEDIATE); fwupd_request_set_id(request, FWUPD_REQUEST_ID_REMOVE_REPLUG); fwupd_request_add_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); if (!fu_device_emit_request(device, request, progress, error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* success */ return TRUE; } static void fu_analogix_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 99, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reload"); } static void fu_analogix_device_init(FuAnalogixDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "com.analogix.bb"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_request_flag(FU_DEVICE(self), FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PAIR); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_ANALOGIX_FIRMWARE); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_USER_REPLUG); /* 40 s */ } static void fu_analogix_device_class_init(FuAnalogixDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_analogix_device_to_string; device_class->write_firmware = fu_analogix_device_write_firmware; device_class->attach = fu_analogix_device_attach; device_class->setup = fu_analogix_device_setup; device_class->probe = fu_analogix_device_probe; device_class->set_progress = fu_analogix_device_set_progress; } fwupd-2.0.10/plugins/analogix/fu-analogix-device.h000066400000000000000000000004651501337203100220220ustar00rootroot00000000000000/* * Copyright 2021 Xiaotian Cui * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ANALOGIX_DEVICE (fu_analogix_device_get_type()) G_DECLARE_FINAL_TYPE(FuAnalogixDevice, fu_analogix_device, FU, ANALOGIX_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/analogix/fu-analogix-firmware.c000066400000000000000000000075701501337203100223760ustar00rootroot00000000000000/* * Copyright 2021 Xiaotian Cui * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-analogix-common.h" #include "fu-analogix-firmware.h" struct _FuAnalogixFirmware { FuIhexFirmwareClass parent_instance; }; G_DEFINE_TYPE(FuAnalogixFirmware, fu_analogix_firmware, FU_TYPE_IHEX_FIRMWARE) static gboolean fu_analogix_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuFirmwareClass *klass = FU_FIRMWARE_CLASS(fu_analogix_firmware_parent_class); const guint8 *buf = NULL; gsize bufsz = 0; guint16 ocm_version; guint8 version_hi = 0; guint8 version_lo = 0; g_autoptr(FuFirmware) fw_ocm = NULL; g_autoptr(GBytes) blob_cus = NULL; g_autoptr(GBytes) blob = NULL; g_autoptr(GBytes) blob_ocm = NULL; g_autoptr(GBytes) blob_srx = NULL; g_autoptr(GBytes) blob_stx = NULL; /* convert to binary with FuIhexFirmware->parse */ if (!klass->parse(firmware, stream, flags, error)) return FALSE; blob = fu_firmware_get_bytes_with_patches(firmware, error); if (blob == NULL) return FALSE; /* OCM section only, CUSTOM section only, or multiple sections excluded CUSTOM */ if (g_bytes_get_size(blob) == OCM_FLASH_SIZE) { blob_ocm = g_bytes_ref(blob); } else if (g_bytes_get_size(blob) == CUSTOM_FLASH_SIZE) { /* custom */ blob_cus = fu_bytes_new_offset(blob, 0, CUSTOM_FLASH_SIZE, error); } else { blob_ocm = fu_bytes_new_offset(blob, 0, OCM_FLASH_SIZE, error); if (blob_ocm == NULL) return FALSE; } if (blob_ocm != NULL) { fw_ocm = fu_firmware_new_from_bytes(blob_ocm); fu_firmware_set_id(fw_ocm, "ocm"); fu_firmware_set_addr(fw_ocm, FLASH_OCM_ADDR); fu_firmware_add_image(firmware, fw_ocm); /* get OCM version */ buf = g_bytes_get_data(blob_ocm, &bufsz); if (!fu_memread_uint8_safe(buf, bufsz, OCM_FW_VERSION_ADDR - FLASH_OCM_ADDR + 8, &version_hi, error)) return FALSE; if (!fu_memread_uint8_safe(buf, bufsz, OCM_FW_VERSION_ADDR - FLASH_OCM_ADDR + 12, &version_lo, error)) return FALSE; ocm_version = ((guint16)version_hi) << 8 | version_lo; fu_firmware_set_version_raw(fw_ocm, ocm_version); } /* TXFW is optional */ blob_stx = fu_bytes_new_offset(blob, FLASH_TXFW_ADDR - FLASH_OCM_ADDR, SECURE_OCM_TX_SIZE, NULL); if (blob_stx != NULL && !fu_bytes_is_empty(blob_stx)) { g_autoptr(FuFirmware) fw2 = fu_firmware_new_from_bytes(blob_stx); fu_firmware_set_id(fw2, "stx"); fu_firmware_set_addr(fw2, FLASH_TXFW_ADDR); fu_firmware_add_image(firmware, fw2); } /* RXFW is optional */ blob_srx = fu_bytes_new_offset(blob, FLASH_RXFW_ADDR - FLASH_OCM_ADDR, SECURE_OCM_RX_SIZE, NULL); if (blob_srx != NULL && !fu_bytes_is_empty(blob_srx)) { g_autoptr(FuFirmware) fw2 = fu_firmware_new_from_bytes(blob_srx); fu_firmware_set_id(fw2, "srx"); fu_firmware_set_addr(fw2, FLASH_RXFW_ADDR); fu_firmware_add_image(firmware, fw2); } if (blob_cus != NULL && !fu_bytes_is_empty(blob_cus)) { g_autoptr(FuFirmware) fw2 = fu_firmware_new_from_bytes(blob_cus); fu_firmware_set_id(fw2, "custom"); fu_firmware_set_addr(fw2, FLASH_CUSTOM_ADDR); fu_firmware_add_image(firmware, fw2); } /* success */ return TRUE; } static gchar * fu_analogix_firmware_convert_version(FuFirmware *firmware, guint64 version_raw) { return fu_version_from_uint16_hex(version_raw, fu_firmware_get_version_format(firmware)); } static void fu_analogix_firmware_init(FuAnalogixFirmware *self) { fu_ihex_firmware_set_padding_value(FU_IHEX_FIRMWARE(self), 0xFF); fu_firmware_set_version_format(FU_FIRMWARE(self), FWUPD_VERSION_FORMAT_PAIR); } static void fu_analogix_firmware_class_init(FuAnalogixFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->convert_version = fu_analogix_firmware_convert_version; firmware_class->parse = fu_analogix_firmware_parse; } fwupd-2.0.10/plugins/analogix/fu-analogix-firmware.h000066400000000000000000000005361501337203100223760ustar00rootroot00000000000000/* * Copyright 2021 Xiaotian Cui * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ANALOGIX_FIRMWARE (fu_analogix_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuAnalogixFirmware, fu_analogix_firmware, FU, ANALOGIX_FIRMWARE, FuIhexFirmware) fwupd-2.0.10/plugins/analogix/fu-analogix-plugin.c000066400000000000000000000015211501337203100220460ustar00rootroot00000000000000/* * Copyright 2021 Xiaotian Cui * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-analogix-device.h" #include "fu-analogix-firmware.h" #include "fu-analogix-plugin.h" struct _FuAnalogixPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuAnalogixPlugin, fu_analogix_plugin, FU_TYPE_PLUGIN) static void fu_analogix_plugin_init(FuAnalogixPlugin *self) { } static void fu_analogix_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_ANALOGIX_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_ANALOGIX_FIRMWARE); } static void fu_analogix_plugin_class_init(FuAnalogixPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_analogix_plugin_constructed; } fwupd-2.0.10/plugins/analogix/fu-analogix-plugin.h000066400000000000000000000003621501337203100220550ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuAnalogixPlugin, fu_analogix_plugin, FU, ANALOGIX_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/analogix/fu-analogix.rs000066400000000000000000000003201501337203100207500ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] enum FuAnalogixUpdateStatus { Invalid, Start, Finish, Error = 0xFF, } fwupd-2.0.10/plugins/analogix/meson.build000066400000000000000000000010061501337203100203340ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginAnalogix"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('analogix.quirk') plugin_builtins += static_library('fu_plugin_analogix', rustgen.process('fu-analogix.rs'), sources: [ 'fu-analogix-plugin.c', 'fu-analogix-device.c', 'fu-analogix-firmware.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files( 'tests/analogix-anx7518.json', ) fwupd-2.0.10/plugins/analogix/tests/000077500000000000000000000000001501337203100173375ustar00rootroot00000000000000fwupd-2.0.10/plugins/analogix/tests/analogix-anx7518.json000066400000000000000000000016271501337203100231530ustar00rootroot00000000000000{ "name": "Analogix ANX7518", "interactive": true, "steps": [ { "url": "5d860747c1378ef8921f95e41bbb7055dc9b470043655a65b00157ab74dd12e8-Analogix-ANX7518-fw-1.5.01-rx-1206.cab", "emulation-url": "63698f1d36491d795f36335a28e301e7484b1735c63aa763c007c149cc74569c-Analogix-ANX7518-fw-1.5.01-rx-1206.zip", "components": [ { "version": "0001.1501", "guids": [ "cfc5f783-2f3c-5db0-9d09-d5a3044eabd9" ] } ] }, { "url": "2e4b5747ea2659ddae893a7b8a238d6d9c3166b426c0d0e5feb308a443662c38-Analogix-ANX7518-fw-1.5.08.cab", "emulation-url": "ff75670536d3de6def5cb4e6e1377dc1ef132882288dd6ceee37c0aecc3fbda3-Analogix-ANX7518-fw-1.5.08.zip", "components": [ { "version": "0001.1508", "guids": [ "cfc5f783-2f3c-5db0-9d09-d5a3044eabd9" ] } ] } ] } fwupd-2.0.10/plugins/android-boot/000077500000000000000000000000001501337203100167545ustar00rootroot00000000000000fwupd-2.0.10/plugins/android-boot/README.md000066400000000000000000000034121501337203100202330ustar00rootroot00000000000000--- title: Plugin: Android Boot --- ## Introduction This plugin is used to update hardware that use partitions to store their firmware on. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob as a Raw Disk File in the IMG format. The firmware blob will be flashed to the partition. Fastboot devices are similar but are flashed in fastboot mode using an external device. This plugin is similar but can be used to flash from the device itself rather than external device. This plugin supports the following protocol ID: * `com.google.android_boot` ## GUID Generation The GUID is generated by combining the partition UUID of the block device, its label and optionally boot slot when using an Android A/B partitioning scheme, e.g. * `DRIVE\UUID_c49183ed-aaec-9bf5-760a-66330fbcffc1&LABEL_label&SLOT_a` * `DRIVE\UUID_c49183ed-aaec-9bf5-760a-66330fbcffc1&LABEL_label` * `DRIVE\UUID_c49183ed-aaec-9bf5-760a-66330fbcffc1` ## Update Behavior The block device is erased in chunks, written and then read back to verify. ## Quirk Use This plugin uses the following plugin-specific quirk: ### AndroidBootVersionProperty Property to parse from `/proc/cmdline` to retrieve the bootloader version. Since: 1.8.5 ### AndroidBootPartitionMaxSize Maximum size the firmware may use of a partition. Since: 1.8.5 ## Vendor ID Security The vendor ID is set through the `android-boot.quirk` file. ## External Interface Access This plugin requires read/write access to `/dev/block`. ## Version Considerations This plugin has been available since fwupd version `1.8.5`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Dylan Van Assche: @DylanVanAssche fwupd-2.0.10/plugins/android-boot/android-boot.quirk000066400000000000000000000015171501337203100224160ustar00rootroot00000000000000# SHIFT6mq ABL A [DRIVE\UUID_c49183ed-aaec-9bf5-760a-66330fbcffc1&LABEL_abl-a] Flags = updatable,signed-payload Vendor = SHIFT GmbH VendorId = DRIVE:SHIFT AndroidBootVersionProperty = androidboot.abl.revision # SHIFT6mq ABL B [DRIVE\UUID_3d7b21e8-048b-db0b-0c18-d07a9bb32f2d&LABEL_abl-b] Flags = updatable,signed-payload Vendor = SHIFT GmbH VendorId = DRIVE:SHIFT AndroidBootVersionProperty = androidboot.abl.revision # SHIFTphone 8 ABL A [DRIVE\UUID_0072779b-8343-75c6-525f-44ff0e1d04db&LABEL_abl-a] Flags = updatable,signed-payload Vendor = SHIFT GmbH VendorId = DRIVE:SHIFT AndroidBootVersionProperty = androidboot.abl.revision # SHIFTphone 8 ABL B [DRIVE\UUID_584e4f5f-6023-f6d6-eb3f-48bb7da59feb&LABEL_abl-b] Flags = updatable,signed-payload Vendor = SHIFT GmbH VendorId = DRIVE:SHIFT AndroidBootVersionProperty = androidboot.abl.revision fwupd-2.0.10/plugins/android-boot/fu-android-boot-device.c000066400000000000000000000277361501337203100233650ustar00rootroot00000000000000/* * Copyright 2022 Dylan Van Assche * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fu-android-boot-device.h" #define ANDROID_BOOT_UNKNOWN_VERSION "0.0.0" #define ANDROID_BOOT_SECTOR_SIZE 512 struct _FuAndroidBootDevice { FuBlockPartition parent_instance; gchar *boot_slot; guint64 max_size; }; G_DEFINE_TYPE(FuAndroidBootDevice, fu_android_boot_device, FU_TYPE_BLOCK_PARTITION) static void fu_android_boot_device_to_string(FuDevice *device, guint idt, GString *str) { FuAndroidBootDevice *self = FU_ANDROID_BOOT_DEVICE(device); fwupd_codec_string_append(str, idt, "BootSlot", self->boot_slot); fwupd_codec_string_append_hex(str, idt, "MaxSize", self->max_size); } static gboolean fu_android_boot_device_probe(FuDevice *device, GError **error) { FuAndroidBootDevice *self = FU_ANDROID_BOOT_DEVICE(device); guint64 sectors = 0; guint64 size = 0; g_autofree gchar *prop_size = NULL; g_autoptr(GHashTable) cmdline = NULL; /* FuUdevDevice->probe */ if (!FU_DEVICE_CLASS(fu_android_boot_device_parent_class)->probe(device, error)) return FALSE; /* get kernel cmdline */ cmdline = fu_kernel_get_cmdline(error); if (cmdline == NULL) return FALSE; /* set the physical ID */ if (!fu_udev_device_set_physical_id(FU_UDEV_DEVICE(device), "block", error)) return FALSE; /* extract boot slot if available */ self->boot_slot = g_strdup(g_hash_table_lookup(cmdline, "androidboot.slot_suffix")); /* set max firmware size, required to avoid writing firmware bigger than partition */ prop_size = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(device), "size", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (prop_size == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device does not expose its size"); return FALSE; } if (!fu_strtoull(prop_size, §ors, 0x0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error)) return FALSE; size = sectors * ANDROID_BOOT_SECTOR_SIZE; self->max_size = size; /* extract serial number and set it */ fu_device_set_serial(device, g_hash_table_lookup(cmdline, "androidboot.serialno")); /* set the firmware maximum size based on partition size or from quirk */ fu_device_set_firmware_size_max(device, self->max_size); return TRUE; } static gboolean fu_android_boot_device_setup(FuDevice *device, GError **error) { FuAndroidBootDevice *self = FU_ANDROID_BOOT_DEVICE(device); const gchar *fs_label; /* FuBlockPartition->setup() */ if (!FU_DEVICE_CLASS(fu_android_boot_device_parent_class)->setup(device, error)) return FALSE; /* extract label and check if it matches boot slot */ fs_label = fu_block_partition_get_fs_label(FU_BLOCK_PARTITION(self)); if (fs_label != NULL) { fu_device_set_name(device, fs_label); /* If the device has A/B partitioning, compare boot slot to only expose partitions * in-use */ if (self->boot_slot != NULL && !g_str_has_suffix(fs_label, self->boot_slot)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device is on a different bootslot"); return FALSE; } } /* * Some devices don't have unique TYPE UUIDs, add the partition label to make them truly * unique Devices have a fixed partition scheme anyway because they originally have Android * which has such requirements. */ if (fu_block_partition_get_fs_uuid(FU_BLOCK_PARTITION(self)) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no partition UUID"); return FALSE; } fu_device_add_instance_strsafe(device, "UUID", fu_block_partition_get_fs_uuid(FU_BLOCK_PARTITION(self))); fu_device_add_instance_strsafe(device, "LABEL", fs_label); fu_device_add_instance_strsafe(device, "SLOT", self->boot_slot); /* GUID based on UUID / UUID, label / UUID, label, slot */ if (!fu_device_build_instance_id(device, error, "DRIVE", "UUID", NULL)) return FALSE; fu_device_build_instance_id(device, NULL, "DRIVE", "UUID", "LABEL", NULL); fu_device_build_instance_id(device, NULL, "DRIVE", "UUID", "LABEL", "SLOT", NULL); /* quirks will have matched now */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device is not updatable"); return FALSE; } /* success */ return TRUE; } static gboolean fu_android_boot_device_open(FuDevice *device, GError **error) { g_autoptr(GError) error_local = NULL; /* FuUdevDevice->open */ if (!FU_DEVICE_CLASS(fu_android_boot_device_parent_class)->open(device, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_PERMISSION_DENIED)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, error_local->message); return FALSE; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } return TRUE; } static gboolean fu_android_boot_device_write(FuAndroidBootDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); /* rewind */ if (!fu_udev_device_seek(FU_UDEV_DEVICE(self), 0x0, error)) { g_prefix_error(error, "failed to rewind: "); return FALSE; } /* write each chunk */ for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_udev_device_pwrite(FU_UDEV_DEVICE(self), fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "failed to write @0x%x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } fu_progress_step_done(progress); } return TRUE; } static gboolean fu_android_boot_device_erase(FuAndroidBootDevice *self, FuProgress *progress, GError **error) { g_autoptr(FuChunkArray) chunks = NULL; gsize bufsz = fu_device_get_firmware_size_max(FU_DEVICE(self)); g_autofree guint8 *buf = g_malloc0(bufsz); g_autoptr(GBytes) fw = g_bytes_new_take(g_steal_pointer(&buf), bufsz); chunks = fu_chunk_array_new_from_bytes(fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 10 * 1024); return fu_android_boot_device_write(self, chunks, progress, error); } static gboolean fu_android_boot_device_verify(FuAndroidBootDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); /* verify each chunk */ for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autofree guint8 *buf = NULL; g_autoptr(FuChunk) chk = NULL; g_autoptr(GBytes) blob1 = NULL; g_autoptr(GBytes) blob2 = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; buf = g_malloc0(fu_chunk_get_data_sz(chk)); if (!fu_udev_device_pread(FU_UDEV_DEVICE(self), fu_chunk_get_address(chk), buf, fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "failed to read @0x%x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } blob1 = fu_chunk_get_bytes(chk); blob2 = g_bytes_new_static(buf, fu_chunk_get_data_sz(chk)); if (!fu_bytes_compare(blob1, blob2, error)) { g_prefix_error(error, "failed to verify @0x%x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } fu_progress_step_done(progress); } return TRUE; } static gboolean fu_android_boot_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuAndroidBootDevice *self = FU_ANDROID_BOOT_DEVICE(device); g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* get data to write */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 10 * 1024, error); if (chunks == NULL) return FALSE; fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 72, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 20, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 7, NULL); /* erase, write, verify */ if (!fu_android_boot_device_erase(self, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); if (!fu_android_boot_device_write(self, chunks, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); if (!fu_android_boot_device_verify(self, chunks, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); return TRUE; } static gboolean fu_android_boot_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuAndroidBootDevice *self = FU_ANDROID_BOOT_DEVICE(device); /* load from quirks */ if (g_strcmp0(key, "AndroidBootVersionProperty") == 0) { g_autoptr(GHashTable) cmdline = NULL; const gchar *version = NULL; cmdline = fu_kernel_get_cmdline(error); if (cmdline == NULL) return FALSE; version = g_hash_table_lookup(cmdline, value); if (version != NULL) fu_device_set_version(device, version); return TRUE; } if (g_strcmp0(key, "AndroidBootPartitionMaxSize") == 0) { guint64 size = 0; if (!fu_strtoull(value, &size, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->max_size = size; return TRUE; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_android_boot_device_finalize(GObject *obj) { FuAndroidBootDevice *self = FU_ANDROID_BOOT_DEVICE(obj); G_OBJECT_CLASS(fu_android_boot_device_parent_class)->finalize(obj); g_free(self->boot_slot); } static void fu_android_boot_device_init(FuAndroidBootDevice *self) { fu_device_set_summary(FU_DEVICE(self), "Android Bootloader"); fu_device_add_protocol(FU_DEVICE(self), "com.google.android_boot"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_REBOOT); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_SYNC); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_COMPUTER); /* * Fallback for ABL without version reporting, fwupd will always provide an upgrade in this * case. Once upgraded, the version reporting will be available and the update notification * will disappear. If version reporting is available, the reported version is set. */ fu_device_set_version(FU_DEVICE(self), ANDROID_BOOT_UNKNOWN_VERSION); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); } static void fu_android_boot_device_class_init(FuAndroidBootDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_android_boot_device_finalize; device_class->probe = fu_android_boot_device_probe; device_class->setup = fu_android_boot_device_setup; device_class->open = fu_android_boot_device_open; device_class->write_firmware = fu_android_boot_device_write_firmware; device_class->to_string = fu_android_boot_device_to_string; device_class->set_quirk_kv = fu_android_boot_device_set_quirk_kv; } fwupd-2.0.10/plugins/android-boot/fu-android-boot-device.h000066400000000000000000000005551501337203100233600ustar00rootroot00000000000000/* * Copyright 2022 Dylan Van Assche * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ANDROID_BOOT_DEVICE (fu_android_boot_device_get_type()) G_DECLARE_FINAL_TYPE(FuAndroidBootDevice, fu_android_boot_device, FU, ANDROID_BOOT_DEVICE, FuBlockPartition) fwupd-2.0.10/plugins/android-boot/fu-android-boot-plugin.c000066400000000000000000000015261501337203100234110ustar00rootroot00000000000000/* * Copyright 2022 Dylan Van Assche * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-android-boot-device.h" #include "fu-android-boot-plugin.h" struct _FuAndroidBootPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuAndroidBootPlugin, fu_android_boot_plugin, FU_TYPE_PLUGIN) static void fu_android_boot_plugin_init(FuAndroidBootPlugin *self) { } static void fu_android_boot_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_ANDROID_BOOT_DEVICE); fu_plugin_add_device_udev_subsystem(plugin, "block:partition"); } static void fu_android_boot_plugin_class_init(FuAndroidBootPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_android_boot_plugin_constructed; } fwupd-2.0.10/plugins/android-boot/fu-android-boot-plugin.h000066400000000000000000000003751501337203100234170ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuAndroidBootPlugin, fu_android_boot_plugin, FU, ANDROID_BOOT_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/android-boot/meson.build000066400000000000000000000007051501337203100211200ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginAndroidBoot"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('android-boot.quirk') plugin_builtins += static_library('fu_plugin_android_boot', sources: [ 'fu-android-boot-plugin.c', 'fu-android-boot-device.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/asus-hid/000077500000000000000000000000001501337203100161105ustar00rootroot00000000000000fwupd-2.0.10/plugins/asus-hid/README.md000066400000000000000000000021551501337203100173720ustar00rootroot00000000000000--- title: Plugin: Asus HID --- ## Introduction The ASUS HID plugin is used for interacting with the ITE MCUs on Asus devices. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a packed binary file format. This plugin supports the following protocol ID: * `com.asus.hid` ## GUID Generation These devices use the a DeviceInstanceId value that also reflects the microcontroller ID. * `USB\VID_0B05&PID_1ABE&PART_RC72LA` ## Update Behavior The device will restart after update. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x0B05` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `2.0.0`. ## Quirk Use This plugin uses the following plugin-specific quirks: ### AsusHidNumMcu The number of MCUs connected to the USB endpoint. Since: 2.0.0 ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: @superm1 fwupd-2.0.10/plugins/asus-hid/asus-hid.quirk000066400000000000000000000011711501337203100207020ustar00rootroot00000000000000# N-Key for Rog Ally [USB\VID_0B05&PID_1ABE] Plugin = asus_hid AsusHidNumMcu = 2 CounterpartGuid = USB\VID_048D&PID_89DB FirmwareSizeMax = 0x40000 Flags = use-runtime-version # Rog Ally-X [USB\VID_0B05&PID_1B4C] Plugin = asus_hid AsusHidNumMcu = 1 CounterpartGuid = USB\VID_048D&PID_89DC FirmwareSizeMax = 0x40000 Flags = use-runtime-version # Ally Bootloader [USB\VID_048D&PID_89DB] AsusHidNumMcu = 2 Plugin = asus_hid Flags = is-bootloader,can-verify-image FirmwareSizeMax = 0x40000 # Ally X Bootloader [USB\VID_048D&PID_89DC] AsusHidNumMcu = 1 Plugin = asus_hid Flags = is-bootloader,can-verify-image FirmwareSizeMax = 0x40000 fwupd-2.0.10/plugins/asus-hid/fu-asus-hid-child-device.c000066400000000000000000000160411501337203100227210ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * Copyright 2024 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-asus-hid-child-device.h" #include "fu-asus-hid-device.h" #include "fu-asus-hid-firmware.h" struct _FuAsusHidChildDevice { FuDevice parent_instance; guint8 idx; }; G_DEFINE_TYPE(FuAsusHidChildDevice, fu_asus_hid_child_device, FU_TYPE_DEVICE) #define FU_ASUS_HID_CHILD_DEVICE_TIMEOUT 200 /* ms */ static void fu_asus_hid_child_device_to_string(FuDevice *device, guint idt, GString *str) { FuAsusHidChildDevice *self = FU_ASUS_HID_CHILD_DEVICE(device); fwupd_codec_string_append_int(str, idt, "ChipIdx", self->idx); } static gboolean fu_asus_hid_child_device_transfer_feature(FuAsusHidChildDevice *self, GByteArray *req, GByteArray *res, guint8 report, GError **error) { FuHidDevice *hid_dev = FU_HID_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); if (req != NULL) { if (!fu_hid_device_set_report(hid_dev, report, req->data, req->len, FU_ASUS_HID_CHILD_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_IS_FEATURE, error)) { g_prefix_error(error, "failed to send packet: "); return FALSE; } } if (res != NULL) { if (!fu_hid_device_get_report(hid_dev, report, res->data, res->len, FU_ASUS_HID_CHILD_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_IS_FEATURE, error)) { g_prefix_error(error, "failed to receive packet: "); return FALSE; } } return TRUE; } static gboolean fu_asus_hid_child_device_ensure_manufacturer(FuAsusHidChildDevice *self, GError **error) { g_autofree gchar *man = NULL; g_autoptr(FuStructAsusManCommand) cmd = fu_struct_asus_man_command_new(); g_autoptr(FuStructAsusManResult) result = fu_struct_asus_man_result_new(); if (!fu_asus_hid_child_device_transfer_feature(self, cmd, result, FU_ASUS_HID_REPORT_ID_INFO, error)) return FALSE; man = fu_struct_asus_man_result_get_data(result); if (g_strcmp0(man, "ASUSTech.Inc.") != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "manufacturer %s not supported", man); return FALSE; } return TRUE; } static gboolean fu_asus_hid_child_device_ensure_version(FuAsusHidChildDevice *self, GError **error) { g_autoptr(FuStructAsusHidCommand) cmd = fu_struct_asus_hid_command_new(); g_autoptr(FuStructAsusHidFwInfo) result = fu_struct_asus_hid_fw_info_new(); g_autoptr(FuStructAsusHidFwInfo) fw_info = NULL; g_autofree gchar *version = NULL; if (self->idx == FU_ASUS_HID_CONTROLLER_PRIMARY) fu_struct_asus_hid_command_set_cmd(cmd, FU_ASUS_HID_COMMAND_FW_VERSION); else if (self->idx == FU_ASUS_HID_CONTROLLER_MAIN) fu_struct_asus_hid_command_set_cmd(cmd, FU_ASUS_HID_COMMAND_MAIN_FW_VERSION); else { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "mcu not supported"); return FALSE; } fu_struct_asus_hid_command_set_length(cmd, FU_STRUCT_ASUS_HID_RESULT_SIZE); if (!fu_asus_hid_child_device_transfer_feature(self, cmd, result, FU_ASUS_HID_REPORT_ID_INFO, error)) return FALSE; fw_info = fu_struct_asus_hid_fw_info_get_description(result); version = fu_struct_asus_hid_desc_get_version(fw_info); fu_device_set_version(FU_DEVICE(self), version); if (fu_device_get_logical_id(FU_DEVICE(self)) == NULL) { g_autofree gchar *product = fu_struct_asus_hid_desc_get_product(fw_info); fu_device_add_instance_strsafe(FU_DEVICE(self), "PART", product); fu_device_build_instance_id(FU_DEVICE(self), NULL, "USB", "VID", "PID", "PART", NULL); fu_device_set_logical_id(FU_DEVICE(self), product); } return TRUE; } static gboolean fu_asus_hid_child_device_setup(FuDevice *device, GError **error) { FuAsusHidChildDevice *self = FU_ASUS_HID_CHILD_DEVICE(device); g_autofree gchar *name = NULL; if (fu_device_get_proxy(FU_DEVICE(self)) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return FALSE; } name = g_strdup_printf("Microcontroller %u", self->idx); fu_device_set_name(FU_DEVICE(self), name); if (fu_device_has_flag(fu_device_get_proxy(FU_DEVICE(self)), FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_autofree gchar *recovery_str = g_strdup_printf("%d", self->idx); // RC71LS = 0 // RC71LM = 1 fu_device_add_instance_strsafe(FU_DEVICE(self), "RECOVERY", recovery_str); fu_device_build_instance_id(FU_DEVICE(self), NULL, "USB", "VID", "PID", "RECOVERY", NULL); fu_device_set_logical_id(FU_DEVICE(self), recovery_str); fu_device_set_version(FU_DEVICE(self), "0"); return TRUE; } if (!fu_asus_hid_child_device_ensure_manufacturer(self, error)) { g_prefix_error(error, "failed to ensure manufacturer: "); return FALSE; } if (!fu_asus_hid_child_device_ensure_version(self, error)) { g_prefix_error(error, "failed to ensure version: "); return FALSE; } return TRUE; } static gboolean fu_asus_hid_child_device_reload(FuDevice *device, GError **error) { FuAsusHidChildDevice *self = FU_ASUS_HID_CHILD_DEVICE(device); return fu_asus_hid_child_device_ensure_version(self, error); } static gboolean fu_asus_hid_child_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuDevice *proxy = fu_device_get_proxy(device); if (proxy == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return FALSE; } return fu_device_attach(proxy, error); } static gboolean fu_asus_hid_child_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuDevice *proxy = fu_device_get_proxy(device); if (proxy == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return FALSE; } return fu_device_detach(proxy, error); } static void fu_asus_hid_child_device_init(FuAsusHidChildDevice *self) { fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PROXY_FALLBACK); fu_device_add_protocol(FU_DEVICE(self), "com.asus.hid"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); } static void fu_asus_hid_child_device_class_init(FuAsusHidChildDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_asus_hid_child_device_to_string; device_class->detach = fu_asus_hid_child_device_detach; device_class->attach = fu_asus_hid_child_device_attach; device_class->setup = fu_asus_hid_child_device_setup; device_class->reload = fu_asus_hid_child_device_reload; } FuDevice * fu_asus_hid_child_device_new(FuDevice *proxy, guint8 idx) { FuDevice *dev = g_object_new(FU_TYPE_ASUS_HID_CHILD_DEVICE, "proxy", proxy, NULL); FuAsusHidChildDevice *self = FU_ASUS_HID_CHILD_DEVICE(dev); self->idx = idx; return dev; } fwupd-2.0.10/plugins/asus-hid/fu-asus-hid-child-device.h000066400000000000000000000010351501337203100227230ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * Copyright 2024 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-asus-hid-struct.h" #define FU_TYPE_ASUS_HID_CHILD_DEVICE (fu_asus_hid_child_device_get_type()) G_DECLARE_FINAL_TYPE(FuAsusHidChildDevice, fu_asus_hid_child_device, FU, ASUS_HID_CHILD_DEVICE, FuDevice) FuDevice * fu_asus_hid_child_device_new(FuDevice *proxy, guint8 idx) G_GNUC_NON_NULL(1); fwupd-2.0.10/plugins/asus-hid/fu-asus-hid-device.c000066400000000000000000000242011501337203100216350ustar00rootroot00000000000000/* * Copyright 2024 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-asus-hid-child-device.h" #include "fu-asus-hid-device.h" #include "fu-asus-hid-struct.h" struct _FuAsusHidDevice { FuHidDevice parent_instance; guint8 num_mcu; gulong child_added_id; }; G_DEFINE_TYPE(FuAsusHidDevice, fu_asus_hid_device, FU_TYPE_HID_DEVICE) #define FU_ASUS_HID_DEVICE_TIMEOUT 200 /* ms */ static gboolean fu_asus_hid_device_transfer_feature(FuAsusHidDevice *self, GByteArray *req, GByteArray *res, guint8 report, GError **error) { FuHidDevice *hid_dev = FU_HID_DEVICE(self); if (req != NULL) { if (!fu_hid_device_set_report(hid_dev, report, req->data, req->len, FU_ASUS_HID_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_IS_FEATURE, error)) { g_prefix_error(error, "failed to send packet: "); return FALSE; } } if (res != NULL) { if (!fu_hid_device_get_report(hid_dev, report, res->data, res->len, FU_ASUS_HID_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_IS_FEATURE, error)) { g_prefix_error(error, "failed to receive packet: "); return FALSE; } } return TRUE; } static gboolean fu_asus_hid_device_init_seq(FuAsusHidDevice *self, GError **error) { g_autoptr(FuStructAsusHidCommand) cmd = fu_struct_asus_hid_command_new(); fu_struct_asus_hid_command_set_cmd(cmd, FU_ASUS_HID_COMMAND_INIT_SEQUENCE); if (!fu_asus_hid_device_transfer_feature(self, cmd, NULL, FU_ASUS_HID_REPORT_ID_INFO, error)) { g_prefix_error(error, "failed to initialize device: "); return FALSE; } return TRUE; } static void fu_asus_hid_device_child_added_cb(FuDevice *device, FuDevice *child, gpointer user_data) { g_debug("child %s added to parent %s updating proxy", fu_device_get_id(child), fu_device_get_id(device)); fu_device_set_proxy(child, device); } static gboolean fu_asus_hid_device_probe(FuDevice *device, GError **error) { FuAsusHidDevice *self = FU_ASUS_HID_DEVICE(device); fu_hid_device_set_interface(FU_HID_DEVICE(device), 0); for (guint i = 0; i < self->num_mcu; i++) { g_autoptr(FuDevice) dev_tmp = fu_asus_hid_child_device_new(device, i); fu_device_add_child(device, dev_tmp); } /* FuHidDevice->probe */ return FU_DEVICE_CLASS(fu_asus_hid_device_parent_class)->probe(device, error); } static gboolean fu_asus_hid_device_setup(FuDevice *device, GError **error) { FuAsusHidDevice *self = FU_ASUS_HID_DEVICE(device); /* HidDevice->setup */ if (!FU_DEVICE_CLASS(fu_asus_hid_device_parent_class)->setup(device, error)) return FALSE; /* bootloader mode won't know about children */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) return TRUE; if (!fu_asus_hid_device_init_seq(self, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_asus_hid_device_attach(FuDevice *device, FuProgress *progress, GError **error) { g_autoptr(FuStructAsusFlashReset) cmd = fu_struct_asus_flash_reset_new(); if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) return TRUE; if (!fu_asus_hid_device_transfer_feature(FU_ASUS_HID_DEVICE(device), cmd, NULL, FU_ASUS_HID_REPORT_ID_FLASHING, error)) { g_prefix_error(error, "failed to reset device: "); return FALSE; } fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_asus_hid_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuAsusHidDevice *self = FU_ASUS_HID_DEVICE(device); g_autoptr(FuStructAsusPreUpdateCommand) cmd = fu_struct_asus_pre_update_command_new(); g_autoptr(FuStructAsusHidResult) result = fu_struct_asus_hid_result_new(); guint32 previous_result; if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) return TRUE; fu_struct_asus_hid_command_set_cmd(cmd, FU_ASUS_HID_COMMAND_PRE_UPDATE); fu_struct_asus_hid_command_set_length(cmd, FU_STRUCT_ASUS_HID_RESULT_SIZE); if (!fu_asus_hid_device_transfer_feature(self, cmd, result, FU_ASUS_HID_REPORT_ID_INFO, error)) return FALSE; // TODO save some bits from result here for data for next command fu_struct_asus_hid_command_set_cmd(cmd, FU_ASUS_HID_COMMAND_PRE_UPDATE2); fu_struct_asus_hid_command_set_length(cmd, 1); if (!fu_asus_hid_device_transfer_feature(self, cmd, result, FU_ASUS_HID_REPORT_ID_INFO, error)) return FALSE; // TODO save some bits from result here for data for next command previous_result = 0x1; fu_struct_asus_hid_command_set_cmd(cmd, FU_ASUS_HID_COMMAND_PRE_UPDATE3); fu_struct_asus_hid_command_set_length(cmd, 1); if (!fu_struct_asus_pre_update_command_set_data(cmd, (guint8 *)&previous_result, sizeof(previous_result), error)) return FALSE; if (!fu_asus_hid_device_transfer_feature(self, cmd, NULL, FU_ASUS_HID_REPORT_ID_INFO, error)) return FALSE; previous_result = 0x0; fu_struct_asus_hid_command_set_cmd(cmd, FU_ASUS_HID_COMMAND_PRE_UPDATE4); fu_struct_asus_hid_command_set_length(cmd, FU_STRUCT_ASUS_HID_RESULT_SIZE); if (!fu_struct_asus_pre_update_command_set_data(cmd, (guint8 *)&previous_result, sizeof(previous_result), error)) return FALSE; if (!fu_asus_hid_device_transfer_feature(self, cmd, result, FU_ASUS_HID_REPORT_ID_INFO, error)) return FALSE; // TODO save some bits from result here for data for next command previous_result = 0x2; fu_struct_asus_hid_command_set_cmd(cmd, FU_ASUS_HID_COMMAND_PRE_UPDATE5); fu_struct_asus_hid_command_set_length(cmd, 0x01); if (!fu_struct_asus_pre_update_command_set_data(cmd, (guint8 *)&previous_result, sizeof(previous_result), error)) return FALSE; if (!fu_asus_hid_device_transfer_feature(self, cmd, NULL, FU_ASUS_HID_REPORT_ID_INFO, error)) return FALSE; /* Maybe this command unlocks for flashing mode? */ previous_result = 0x0; fu_struct_asus_hid_command_set_cmd(cmd, FU_ASUS_HID_COMMAND_PRE_UPDATE6); fu_struct_asus_hid_command_set_length(cmd, 0x0); if (!fu_struct_asus_pre_update_command_set_data(cmd, (guint8 *)&previous_result, sizeof(previous_result), error)) return FALSE; if (!fu_asus_hid_device_transfer_feature(self, cmd, NULL, FU_ASUS_HID_REPORT_ID_INFO, error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static GBytes * fu_asus_hid_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuAsusHidDevice *self = FU_ASUS_HID_DEVICE(device); g_autoptr(GByteArray) fw = g_byte_array_new(); g_autoptr(GPtrArray) blocks = NULL; if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device is not in bootloader mode"); return NULL; } fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ); fu_byte_array_set_size(fw, fu_device_get_firmware_size_max(device), 0x00); blocks = fu_chunk_array_mutable_new(fw->data, fw->len, 0x0, 0x1000, FU_STRUCT_ASUS_READ_FLASH_COMMAND_SIZE_DATA); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, blocks->len); for (guint i = 0, offset = 0; i < blocks->len; i++) { FuChunk *chk = g_ptr_array_index(blocks, i); const guint8 *buf; gsize bufsz = 0; g_autoptr(FuStructAsusReadFlashCommand) cmd = fu_struct_asus_read_flash_command_new(); g_autoptr(FuStructAsusReadFlashCommand) result = fu_struct_asus_read_flash_command_new(); fu_struct_asus_read_flash_command_set_offset(cmd, offset); fu_struct_asus_read_flash_command_set_datasz(cmd, fu_chunk_get_data_sz(chk)); if (!fu_asus_hid_device_transfer_feature(self, cmd, result, FU_ASUS_HID_REPORT_ID_FLASHING, error)) return NULL; buf = fu_struct_asus_read_flash_command_get_data(result, &bufsz); if (!fu_memcpy_safe(fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), 0x0, buf, bufsz, 0x0, fu_struct_asus_read_flash_command_get_datasz(result), error)) return NULL; offset += fu_chunk_get_data_sz(chk); fu_progress_step_done(progress); } return g_bytes_new(fw->data, fw->len); } static gboolean fu_asus_hid_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuAsusHidDevice *self = FU_ASUS_HID_DEVICE(device); if (g_strcmp0(key, "AsusHidNumMcu") == 0) { guint64 tmp; if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->num_mcu = tmp; return TRUE; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_asus_hid_device_dispose(GObject *object) { FuAsusHidDevice *self = FU_ASUS_HID_DEVICE(object); if (self->child_added_id != 0) { g_signal_handler_disconnect(FU_DEVICE(self), self->child_added_id); self->child_added_id = 0; } G_OBJECT_CLASS(fu_asus_hid_device_parent_class)->dispose(object); } static void fu_asus_hid_device_init(FuAsusHidDevice *self) { /* TODO: automatic backup */ // fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_BACKUP_BEFORE_INSTALL); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_set_remove_delay(FU_DEVICE(self), 10000); self->child_added_id = g_signal_connect(FU_DEVICE(self), "child-added", G_CALLBACK(fu_asus_hid_device_child_added_cb), self); } static void fu_asus_hid_device_class_init(FuAsusHidDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->dispose = fu_asus_hid_device_dispose; device_class->setup = fu_asus_hid_device_setup; device_class->probe = fu_asus_hid_device_probe; device_class->set_quirk_kv = fu_asus_hid_device_set_quirk_kv; device_class->detach = fu_asus_hid_device_detach; device_class->attach = fu_asus_hid_device_attach; device_class->dump_firmware = fu_asus_hid_device_dump_firmware; } fwupd-2.0.10/plugins/asus-hid/fu-asus-hid-device.h000066400000000000000000000007411501337203100216450ustar00rootroot00000000000000/* * Copyright 2024 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ASUS_HID_DEVICE (fu_asus_hid_device_get_type()) G_DECLARE_FINAL_TYPE(FuAsusHidDevice, fu_asus_hid_device, FU, ASUS_HID_DEVICE, FuHidDevice) gboolean fu_asus_hid_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error); fwupd-2.0.10/plugins/asus-hid/fu-asus-hid-firmware.c000066400000000000000000000045551501337203100222240ustar00rootroot00000000000000/* * Copyright 2024 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-asus-hid-firmware.h" #include "fu-asus-hid-struct.h" #define FGA_OFFSET 0x2010 struct _FuAsusHidFirmware { FuFirmware parent_instance; gchar *fga; gchar *product; gchar *version; }; G_DEFINE_TYPE(FuAsusHidFirmware, fu_asus_hid_firmware, FU_TYPE_FIRMWARE) static void fu_asus_hid_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuAsusHidFirmware *self = FU_ASUS_HID_FIRMWARE(firmware); fu_xmlb_builder_insert_kv(bn, "fga", self->fga); fu_xmlb_builder_insert_kv(bn, "product", self->product); fu_xmlb_builder_insert_kv(bn, "version", self->version); } static gboolean fu_asus_hid_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuAsusHidFirmware *self = FU_ASUS_HID_FIRMWARE(firmware); g_autoptr(GByteArray) desc = NULL; g_autoptr(FuFirmware) img_payload = fu_firmware_new(); g_autoptr(GInputStream) stream_payload = NULL; desc = fu_struct_asus_hid_desc_parse_stream(stream, FGA_OFFSET, error); if (desc == NULL) return FALSE; self->fga = fu_struct_asus_hid_desc_get_fga(desc); self->product = fu_struct_asus_hid_desc_get_product(desc); self->version = fu_struct_asus_hid_desc_get_version(desc); stream_payload = fu_partial_input_stream_new(stream, 0x2000, G_MAXSIZE, error); if (stream_payload == NULL) return FALSE; if (!fu_firmware_parse_stream(img_payload, stream_payload, 0x0, flags, error)) return FALSE; fu_firmware_set_id(img_payload, FU_FIRMWARE_ID_PAYLOAD); fu_firmware_add_image(firmware, img_payload); return TRUE; } static void fu_asus_hid_firmware_init(FuAsusHidFirmware *self) { } static void fu_asus_hid_firmware_finalize(GObject *object) { FuAsusHidFirmware *self = FU_ASUS_HID_FIRMWARE(object); g_free(self->fga); g_free(self->product); g_free(self->version); G_OBJECT_CLASS(fu_asus_hid_firmware_parent_class)->finalize(object); } static void fu_asus_hid_firmware_class_init(FuAsusHidFirmwareClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_asus_hid_firmware_finalize; firmware_class->parse = fu_asus_hid_firmware_parse; firmware_class->export = fu_asus_hid_firmware_export; } fwupd-2.0.10/plugins/asus-hid/fu-asus-hid-firmware.h000066400000000000000000000004761501337203100222270ustar00rootroot00000000000000/* * Copyright 2024 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ASUS_HID_FIRMWARE (fu_asus_hid_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuAsusHidFirmware, fu_asus_hid_firmware, FU, ASUS_HID_FIRMWARE, FuFirmware) fwupd-2.0.10/plugins/asus-hid/fu-asus-hid-plugin.c000066400000000000000000000020551501337203100216770ustar00rootroot00000000000000/* * Copyright 2024 Mario Limonciello * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-asus-hid-child-device.h" #include "fu-asus-hid-device.h" #include "fu-asus-hid-firmware.h" #include "fu-asus-hid-plugin.h" struct _FuAsusHidPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuAsusHidPlugin, fu_asus_hid_plugin, FU_TYPE_PLUGIN) static void fu_asus_hid_plugin_init(FuAsusHidPlugin *self) { } static void fu_asus_hid_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_plugin_add_device_gtype(plugin, FU_TYPE_ASUS_HID_CHILD_DEVICE); /* coverage */ fu_plugin_set_device_gtype_default(plugin, FU_TYPE_ASUS_HID_DEVICE); fu_context_add_quirk_key(ctx, "AsusHidNumMcu"); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_ASUS_HID_FIRMWARE); } static void fu_asus_hid_plugin_class_init(FuAsusHidPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_asus_hid_plugin_constructed; } fwupd-2.0.10/plugins/asus-hid/fu-asus-hid-plugin.h000066400000000000000000000003621501337203100217030ustar00rootroot00000000000000/* * Copyright 2024 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuAsusHidPlugin, fu_asus_hid_plugin, FU, ASUS_HID_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/asus-hid/fu-asus-hid.rs000066400000000000000000000050011501337203100205770ustar00rootroot00000000000000// Copyright 2024 Mario Limonciello // SPDX-License-Identifier: LGPL-2.1-or-later #[repr(u8)] enum FuAsusHidController { Primary, Main, } // these are called with report ID 0x5A #[repr(u32)] enum FuAsusHidCommand { InitSequence = 0x010101d1, // No idea PreUpdate = 0x200005, PreUpdate2 = 0x200907, //packet 265 in ally-x (Get report response in 268) PreUpdate3 = 0x200908, //packet 269 in ally-x PreUpdate4 = 0x260b07, //packet 271 in ally-x (Get report response in 274) PreUpdate5 = 0x260b08, //packet 275 in ally-x PreUpdate6 = 0x9332e0, GetFwConfig = 0x312005, // OEM_GetFWConfig FwVersion = 0x00310305, // OEM_GetFWVersion MainFwVersion = 0x00310405, // OEM_GetMainFWVersion FlashTaskSomething = 0xc000, FlashTaskSomething2 = 0xd000, SwitchToRom = 0xd2, // OEM_MainSwitchToRom, doesn't appear in packet cap } //Info stuff happens on report ID 0x5A //Flashing seems to happen on report ID 0x0 #[repr(u8)] enum FuAsusHidReportId { Flashing = 0x00, Info = 0x5A, } #[derive(Default, New)] #[repr(C, packed)] struct FuStructAsusManCommand { report_id: FuAsusHidReportId == Info, data: [char; 14] == "ASUS Tech.Inc.", terminator: u8 == 0, } #[derive(Default, New, Getters)] #[repr(C, packed)] struct FuStructAsusManResult { report_id: FuAsusHidReportId == Info, data: [char; 31], } #[derive(Default, New)] #[repr(C, packed)] struct FuStructAsusHidCommand { report_id: FuAsusHidReportId == Info, cmd: u32le, length: u8, } #[derive(Default, New)] #[repr(C, packed)] struct FuStructAsusHidResult { report_id: FuAsusHidReportId == Info, data: [u8; 31], } #[derive(Getters, ParseStream)] #[repr(C, packed)] struct FuStructAsusHidDesc { fga: [char; 8], reserved: u8, product: [char; 6], reserved: u8, version: [char; 8], reserved: u8, } #[derive(New, Getters)] #[repr(C, packed)] struct FuStructAsusHidFwInfo { header: FuStructAsusHidCommand, reserved: u8, description: FuStructAsusHidDesc, } #[derive(Default, New)] #[repr(C, packed)] struct FuStructAsusPreUpdateCommand { report_id: FuAsusHidReportId == Info, cmd: u32le, length: u8, data: [u8; 58], } // Flashing sequence #[derive(Default, New)] #[repr(C, packed)] struct FuStructAsusFlashReset { command: u8 == 0xc4, reserved: [u8; 62], } #[derive(Default, New, Getters)] #[repr(C, packed)] struct FuStructAsusReadFlashCommand { command: u8 == 0xd1, offset: u24le, datasz: u8, data: [u8; 58], } fwupd-2.0.10/plugins/asus-hid/meson.build000066400000000000000000000011211501337203100202450ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginAsusHid"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('asus-hid.quirk') plugin_builtins += static_library('fu_plugin_asus_hid', rustgen.process('fu-asus-hid.rs'), sources: [ 'fu-asus-hid-device.c', 'fu-asus-hid-child-device.c', 'fu-asus-hid-plugin.c', 'fu-asus-hid-firmware.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) enumeration_data += files('tests/asus-hid-setup.json') device_tests += files('tests/asus-hid.json') fwupd-2.0.10/plugins/asus-hid/tests/000077500000000000000000000000001501337203100172525ustar00rootroot00000000000000fwupd-2.0.10/plugins/asus-hid/tests/asus-hid-setup.json000066400000000000000000000077761501337203100230410ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUsbDevice", "PlatformId": "1-3", "Created": "2024-10-07T02:18:32.385431Z", "IdVendor": 2821, "IdProduct": 6846, "Device": 2, "USB": 512, "Manufacturer": 1, "Product": 2, "UsbConfigDescriptors": [ { "ConfigurationValue": 1 } ], "UsbInterfaces": [ { "Length": 9, "DescriptorType": 4, "InterfaceClass": 3, "InterfaceSubClass": 1, "InterfaceProtocol": 1, "Interface": 3, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 129, "Interval": 1, "MaxPacketSize": 64 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 1, "InterfaceClass": 3, "InterfaceSubClass": 1, "InterfaceProtocol": 1, "Interface": 1, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 130, "Interval": 4, "MaxPacketSize": 64 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 2, "InterfaceClass": 3, "InterfaceSubClass": 1, "InterfaceProtocol": 1, "Interface": 1, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 131, "Interval": 1, "MaxPacketSize": 64 }, { "DescriptorType": 5, "EndpointAddress": 4, "Interval": 1, "MaxPacketSize": 64 } ] } ], "UsbEvents": [ { "Id": "#d5a801ad", "Data": "usb" }, { "Id": "#ad8c58d3", "Data": "usb" }, { "Id": "#1075ed5c", "Data": "usb_device" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=2\nDEVNAME=bus/usb/001/003\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=b05/1abe/2\nTYPE=0/0/0\nBUSNUM=001\nDEVNUM=003" }, { "Id": "#d432c663", "Data": "bus/usb/001/003" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c", "Data": "usb_device" }, { "Id": "#4693935e", "Data": "001" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=2\nDEVNAME=bus/usb/001/003\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=b05/1abe/2\nTYPE=0/0/0\nBUSNUM=001\nDEVNUM=003" }, { "Id": "#1ab3ae0a", "Data": "003" }, { "Id": "#1fcf122d", "Data": "Ti1LRVkgRGV2aWNlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" }, { "Id": "#16f4e7da", "Data": "WtEBAQEA" }, { "Id": "#a3bf98cf", "Data": "WkFTVVNUZWNoLkluYy4AAA==" }, { "Id": "#ff7d0ded", "Data": "WkFTVVNUZWNoLkluYy4AAGUAdgBpAGMAZQAAAAAAAAA=" }, { "Id": "#66f65422", "Data": "WgUDMQAg" }, { "Id": "#ff7d0ded", "Data": "WgUDMQAaE0ZHQTgwMTAwLlJDNzFMUy4zMTEAAAAAAAA=" }, { "Id": "#a3bf98cf", "Data": "WkFTVVNUZWNoLkluYy4AAA==" }, { "Id": "#ff7d0ded", "Data": "WkFTVVNUZWNoLkluYy4AAFJDNzFMUy4zMTEAAAAAAAA=" }, { "Id": "#2fa92714", "Data": "WgUEMQAg" }, { "Id": "#ff7d0ded", "Data": "WgUEMQAaE0ZHQTgwMTAwLlJDNzFMTS4zMTEAAAAAAAA=" } ] } ] } fwupd-2.0.10/plugins/asus-hid/tests/asus-hid.json000066400000000000000000000011001501337203100216520ustar00rootroot00000000000000{ "name": "ROG Ally", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/asus-hid-setup.json", "components": [ { "version": "0.2", "guids": [ "40e5a299-fe27-5e3b-a4a3-6649aeef4875" ] }, { "version": "311", "guids": [ "f7765461-3ff4-52a0-a4d8-d782b1f885ab" ] }, { "version": "311", "guids": [ "545ddbd6-f041-5d86-bfd7-4764193e0a17" ] } ] } ] } fwupd-2.0.10/plugins/ata/000077500000000000000000000000001501337203100151405ustar00rootroot00000000000000fwupd-2.0.10/plugins/ata/README.md000066400000000000000000000031741501337203100164240ustar00rootroot00000000000000--- title: Plugin: ATA --- ## Introduction This plugin allows updating ATA/ATAPI storage hardware. Devices are enumerated from the block devices and if ID_ATA_DOWNLOAD_MICROCODE is supported they can be updated with appropriate firmware file. Updating ATA devices is more dangerous than other hardware such as DFU or NVMe and should be tested carefully with the help of the drive vendor. The device GUID is read from the trimmed model string. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in an unspecified binary file format. This plugin supports the following protocol ID: * `org.t13.ata` ## GUID Generation These device use the Microsoft DeviceInstanceId values, e.g. * `IDE\VENDOR[40]REVISION[8]` * `IDE\0VENDOR[40]` See for more details. ## Update Behavior The firmware is deployed when the device is in normal runtime mode, but it is only activated when the system is in the final shutdown stages. This is done to minimize the chance of data loss if the switch to the new firmware is not done correctly. ## Vendor ID Security No vendor ID is set as there is no vendor field in the IDENTIFY response. ## Quirk Use This plugin uses the following plugin-specific quirks: ### AtaTransferBlocks Blocks to transfer, or `0xffff` for max Since: 1.2.4 ### AtaTransferMode The transfer mode, `0x3`, `0x7` or `0xe` Since: 1.2.4 ## External Interface Access This plugin requires the `SG_IO` ioctl interface. ## Version Considerations This plugin has been available since fwupd version `1.2.4`. fwupd-2.0.10/plugins/ata/ata.quirk000066400000000000000000000025671501337203100167740ustar00rootroot00000000000000[ThinkSystem M.2 VD] Flags = ~updatable [OUI\VID_000039] Vendor = Toshiba VendorId = ATA:0x1179 [OUI\VID_0000F0] Vendor = Samsung VendorId = ATA:0x144D [OUI\VID_000120] Vendor = Corsair VendorId = ATA:0x1987 [OUI\VID_0004CF] Vendor = Seagate VendorId = ATA:0x1BB1 [OUI\VID_00080D] Vendor = Toshiba VendorId = ATA:0x1179 [OUI\VID_000C50] Vendor = Seagate VendorId = ATA:0x1BB1 [OUI\VID_000CCA] Vendor = Western Digital VendorId = ATA:0x101C [OUI\VID_0014EE] Vendor = Western Digital VendorId = ATA:0x101C [OUI\VID_001517] Vendor = Intel VendorId = ATA:0x8086 [OUI\VID_001B44] Vendor = Western Digital VendorId = ATA:0x101C [OUI\VID_002303] Vendor = LITE-ON VendorId = ATA:0x14A4 [OUI\VID_0024E9] Vendor = Samsung VendorId = ATA:0x144D [OUI\VID_002538] Vendor = Samsung VendorId = ATA:0x144D [OUI\VID_0026B7] Vendor = Kingston VendorId = ATA:0x2646 Flags = needs-shutdown [OUI\VID_00A075] Vendor = Micron VendorId = ATA:0x1344 [OUI\VID_030302] Vendor = SK hynix VendorId = ATA:0x1C5C [OUI\VID_5CD2E4] Vendor = Intel VendorId = ATA:0x8086 [OUI\VID_707C18] Vendor = ADATA VendorId = ATA:0x1CC1 [OUI\VID_7C3548] Vendor = Transcend VendorId = ATA:0x8564 [OUI\VID_001B44] Vendor = SanDisk VendorId = ATA:0x15B7 [OUI\VID_96A060] Vendor = Western Digital VendorId = ATA:0x101C [OUI\VID_F8DB4C] Vendor = PNY VendorId = ATA:0x196E [OUI\VID_E83A97] Vendor = Toshiba VendorId = ATA:0x1179 fwupd-2.0.10/plugins/ata/fu-ata-device.c000066400000000000000000000653361501337203100177330ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fu-ata-device.h" #define FU_ATA_IDENTIFY_SIZE 512 /* bytes */ #define FU_ATA_BLOCK_SIZE 512 /* bytes */ struct ata_tf { guint8 dev; guint8 command; guint8 error; guint8 status; guint8 feat; guint8 nsect; guint8 lbal; guint8 lbam; guint8 lbah; }; #define ATA_USING_LBA (1 << 6) #define ATA_STAT_DRQ (1 << 3) #define ATA_STAT_ERR (1 << 0) #define ATA_OP_IDENTIFY 0xec #define ATA_OP_FLUSH_CACHE 0xe7 #define ATA_OP_DOWNLOAD_MICROCODE 0x92 #define ATA_OP_STANDBY_IMMEDIATE 0xe0 #define ATA_SUBCMD_MICROCODE_OBSOLETE 0x01 #define ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS_ACTIVATE 0x03 #define ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNK 0x07 #define ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS 0x0e #define ATA_SUBCMD_MICROCODE_ACTIVATE 0x0f #define SG_CHECK_CONDITION 0x02 #define SG_DRIVER_SENSE 0x08 #define SG_ATA_12 0xa1 #define SG_ATA_12_LEN 12 #define SG_ATA_PROTO_NON_DATA (3 << 1) #define SG_ATA_PROTO_PIO_IN (4 << 1) #define SG_ATA_PROTO_PIO_OUT (5 << 1) #define FU_ATA_DEVICE_IOCTL_TIMEOUT 5000 /* ms */ enum { SG_CDB2_TLEN_NODATA = 0 << 0, SG_CDB2_TLEN_FEAT = 1 << 0, SG_CDB2_TLEN_NSECT = 2 << 0, SG_CDB2_TLEN_BYTES = 0 << 2, SG_CDB2_TLEN_SECTORS = 1 << 2, SG_CDB2_TDIR_TO_DEV = 0 << 3, SG_CDB2_TDIR_FROM_DEV = 1 << 3, SG_CDB2_CHECK_COND = 1 << 5, }; struct _FuAtaDevice { FuUdevDevice parent_instance; guint pci_depth; guint usb_depth; guint16 transfer_blocks; guint8 transfer_mode; guint32 oui; }; G_DEFINE_TYPE(FuAtaDevice, fu_ata_device, FU_TYPE_UDEV_DEVICE) guint8 fu_ata_device_get_transfer_mode(FuAtaDevice *self) { return self->transfer_mode; } guint16 fu_ata_device_get_transfer_blocks(FuAtaDevice *self) { return self->transfer_blocks; } static gchar * fu_ata_device_get_string(const guint16 *buf, guint start, guint end) { g_autoptr(GString) str = g_string_new(NULL); for (guint i = start; i <= end; i++) { g_string_append_c(str, (gchar)(buf[i] >> 8)); g_string_append_c(str, (gchar)(buf[i] & 0xff)); } /* remove whitespace before returning */ if (str->len > 0) { g_strstrip(str->str); if (str->str[0] == '\0') return NULL; } return g_string_free(g_steal_pointer(&str), FALSE); } static void fu_ata_device_to_string(FuDevice *device, guint idt, GString *str) { FuAtaDevice *self = FU_ATA_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "TransferMode", self->transfer_mode); fwupd_codec_string_append_hex(str, idt, "TransferBlocks", self->transfer_blocks); fwupd_codec_string_append_hex(str, idt, "OUI", self->oui); fwupd_codec_string_append_int(str, idt, "PciDepth", self->pci_depth); fwupd_codec_string_append_int(str, idt, "UsbDepth", self->usb_depth); } /* https://docs.microsoft.com/en-us/windows-hardware/drivers/install/identifiers-for-ide-devices */ static gchar * fu_ata_device_pad_string_for_id(const gchar *name) { GString *str = g_string_new(name); g_string_replace(str, " ", "_", 0); for (guint i = str->len; i < 40; i++) g_string_append_c(str, '_'); return g_string_free(str, FALSE); } static gchar * fu_ata_device_get_guid_safe(const guint16 *buf, guint16 addr_start) { if (!fu_common_guid_is_plausible((((guint8 *)buf) + addr_start))) return NULL; return fwupd_guid_to_string((const fwupd_guid_t *)(((guint8 *)buf) + addr_start), FWUPD_GUID_FLAG_MIXED_ENDIAN); } static void fu_ata_device_parse_id_maybe_dell(FuAtaDevice *self, const guint16 *buf) { g_autofree gchar *component_id = NULL; g_autofree gchar *guid_efi = NULL; g_autofree gchar *guid_id = NULL; /* add extra component ID if set */ component_id = fu_ata_device_get_string(buf, 137, 140); if (component_id == NULL || !g_str_is_ascii(component_id) || strlen(component_id) < 6) { g_debug("invalid component ID, skipping"); return; } /* do not add the FuUdevDevice instance IDs as generic firmware * should not be used on these OEM-specific devices */ fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_NO_AUTO_INSTANCE_IDS); /* add instance ID *and* GUID as using no-auto-instance-ids */ guid_id = g_strdup_printf("STORAGE-DELL-%s", component_id); fu_device_add_instance_id(FU_DEVICE(self), guid_id); /* also add the EFI GUID */ guid_efi = fu_ata_device_get_guid_safe(buf, 129); if (guid_efi != NULL) fu_device_add_instance_id(FU_DEVICE(self), guid_efi); /* owned by Dell */ fu_device_set_vendor(FU_DEVICE(self), "Dell"); fu_device_build_vendor_id_u16(FU_DEVICE(self), "ATA", 0x1028); } static void fu_ata_device_parse_vendor_name(FuAtaDevice *self, const gchar *name) { struct { const gchar *prefix; /* in CAPS */ guint16 vid; const gchar *name; } map_name[] = {/* vendor matches */ {"ADATA*", 0x1cc1, "ADATA"}, {"APACER*", 0x0000, "Apacer"}, /* not in pci.ids */ {"APPLE*", 0x106b, "Apple"}, {"CORSAIR*", 0x1987, "Corsair"}, /* identifies as Phison */ {"CRUCIAL*", 0xc0a9, "Crucial"}, {"FUJITSU*", 0x10cf, "Fujitsu"}, {"GIGABYTE*", 0x1458, "Gigabyte"}, {"HGST*", 0x101c, "Western Digital"}, {"HITACHI*", 0x101c, "Western Digital"}, /* was acquired by WD */ {"HITACHI*", 0x1054, "Hitachi"}, {"HP SSD*", 0x103c, "HP"}, {"INTEL*", 0x8086, "Intel"}, {"KINGSPEC*", 0x0000, "KingSpec"}, /* not in pci.ids */ {"KINGSTON*", 0x2646, "Kingston"}, {"LITEON*", 0x14a4, "LITE-ON"}, {"MAXTOR*", 0x115f, "Maxtor"}, {"MICRON*", 0x1344, "Micron"}, {"OCZ*", 0x1179, "Toshiba"}, {"PNY*", 0x196e, "PNY"}, {"QEMU*", 0x1b36, "QEMU"}, /* identifies as Red Hat! */ {"SAMSUNG*", 0x144d, "Samsung"}, {"SANDISK*", 0x15b7, "SanDisk"}, {"SEAGATE*", 0x1bb1, "Seagate"}, { "SK HYNIX*", 0x1c5c, "SK hynix", }, {"SUPERMICRO*", 0x15d9, "SuperMicro"}, {"TOSHIBA*", 0x1179, "Toshiba"}, {"WDC*", 0x101c, "Western Digital"}, {NULL, 0x0000, NULL}}; struct { const gchar *prefix; /* in CAPS */ guint16 vid; const gchar *name; } map_fuzzy[] = {/* fuzzy name matches -- also see legacy list at: * https://github.com/linuxhw/hw-probe/blob/master/hw-probe.pl#L647 */ {"001-*", 0x1bb1, "Seagate"}, {"726060*", 0x101c, "Western Digital"}, {"CT*", 0xc0a9, "Crucial"}, {"DT0*", 0x1179, "Toshiba"}, {"EK0*", 0x1590, "HPE"}, {"EZEX*", 0x101c, "Western Digital"}, {"GB0*", 0x1590, "HPE"}, {"GOODRAM*", 0x1987, "Phison"}, {"H??54*", 0x101c, "Western Digital"}, {"H??72?0*", 0x101c, "Western Digital"}, {"HDWG*", 0x1179, "Toshiba"}, {"M?0??CA*", 0x1179, "Toshiba"}, /* enterprise */ {"M4-CT*", 0xc0a9, "Crucial"}, { "MA*", 0x10cf, "Fujitsu", }, { "MB*", 0x10cf, "Fujitsu", }, {"MK0*", 0x1590, "HPE"}, {"MTFDDAK*", 0x1344, "Micron"}, { "NIM*", 0x0000, "Nimbus", }, /* no PCI ID */ { "SATADOM*", 0x0000, "Innodisk", }, /* no PCI ID */ {"SSD 860*", 0x144d, "Samsung"}, {"SSDPR*", 0x1987, "Phison"}, {"SSDSC?K*", 0x8086, "Intel"}, { "ST*", 0x1bb1, "Seagate", }, {"TEAM*", 0x0000, "Team Group"}, /* not in pci.ids */ {"TS*", 0x8564, "Transcend"}, {"VK0*", 0x1590, "HPE"}, {"WD*", 0x101c, "Western Digital"}, {NULL, 0x0000, NULL}}; struct { const gchar *prefix; /* in CAPS */ guint16 vid; const gchar *name; } map_version[] = {/* fuzzy version matches */ {"CS2111*", 0x196e, "PNY"}, {"S?FM*", 0x1987, "Phison"}, {NULL, 0x0000, NULL}}; guint16 vid = 0; g_autofree gchar *name_up = g_ascii_strup(name, -1); /* find match */ for (guint i = 0; map_name[i].prefix != NULL; i++) { if (g_pattern_match_simple(map_name[i].prefix, name_up)) { name += strlen(map_name[i].prefix) - 1; fu_device_set_vendor(FU_DEVICE(self), map_name[i].name); vid = map_name[i].vid; break; } } /* fall back to fuzzy match */ if (vid == 0x0) { for (guint i = 0; map_fuzzy[i].prefix != NULL; i++) { if (g_pattern_match_simple(map_fuzzy[i].prefix, name_up)) { fu_device_set_vendor(FU_DEVICE(self), map_fuzzy[i].name); vid = map_fuzzy[i].vid; break; } } } /* fall back to version */ if (vid == 0x0) { g_autofree gchar *version_up = g_ascii_strup(fu_device_get_version(FU_DEVICE(self)), -1); for (guint i = 0; map_version[i].prefix != NULL; i++) { if (g_pattern_match_simple(map_version[i].prefix, version_up)) { fu_device_set_vendor(FU_DEVICE(self), map_version[i].name); vid = map_version[i].vid; break; } } } /* devices without a vendor ID will not be UPGRADABLE */ fu_device_build_vendor_id_u16(FU_DEVICE(self), "ATA", vid); /* remove leading junk */ while (name[0] == ' ' || name[0] == '_' || name[0] == '-') name += 1; /* if changed */ if (g_strcmp0(fu_device_get_name(FU_DEVICE(self)), name) != 0) fu_device_set_name(FU_DEVICE(self), name); } static gboolean fu_ata_device_parse_id(FuAtaDevice *self, const guint8 *buf, gsize sz, GError **error) { FuDevice *device = FU_DEVICE(self); gboolean has_oui_quirk = FALSE; guint16 xfer_min = 1; guint16 xfer_max = 0xffff; guint16 id[FU_ATA_IDENTIFY_SIZE / 2]; g_autofree gchar *name = NULL; g_autofree gchar *sku = NULL; /* check size */ if (sz != FU_ATA_IDENTIFY_SIZE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "ID incorrect size, got 0x%02x", (guint)sz); return FALSE; } /* read LE buffer */ for (guint i = 0; i < sz / 2; i++) id[i] = fu_memread_uint16(buf + (i * 2), G_LITTLE_ENDIAN); /* verify drive correctly supports DOWNLOAD_MICROCODE */ if (!(id[83] & 1 && id[86] & 1)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "DOWNLOAD_MICROCODE not supported by device"); return FALSE; } fu_ata_device_parse_id_maybe_dell(self, id); /* firmware will be applied when the device restarts */ if (self->transfer_mode == ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS) fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_REBOOT); /* the newer, segmented transfer mode */ if (self->transfer_mode == ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS_ACTIVATE || self->transfer_mode == ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS) { xfer_min = id[234]; if (xfer_min == 0x0 || xfer_min == 0xffff) xfer_min = 1; xfer_max = id[235]; if (xfer_max == 0x0 || xfer_max == 0xffff) xfer_max = xfer_min; } /* fall back to a sane block size */ if (self->transfer_blocks == 0x0) self->transfer_blocks = xfer_min; else if (self->transfer_blocks == 0xffff) self->transfer_blocks = xfer_max; /* get values in case the kernel didn't */ if (fu_device_get_serial(device) == NULL) { g_autofree gchar *tmp = NULL; tmp = fu_ata_device_get_string(id, 10, 19); if (tmp != NULL) fu_device_set_serial(device, tmp); } if (fu_device_get_version(device) == NULL) { g_autofree gchar *tmp = NULL; tmp = fu_ata_device_get_string(id, 23, 26); if (tmp != NULL) fu_device_set_version(device, tmp); } /* get OUI if set */ self->oui = ((guint32)(id[108] & 0x0fff)) << 12 | ((guint32)(id[109] & 0xfff0)) >> 4; if (self->oui > 0x0) { g_autofree gchar *tmp = NULL; tmp = g_strdup_printf("OUI\\VID_%06x", self->oui); fu_device_add_instance_id_full(device, tmp, FU_DEVICE_INSTANCE_FLAG_QUIRKS); has_oui_quirk = fu_device_get_vendor(FU_DEVICE(self)) != NULL; } if (self->oui > 0x0) { g_autofree gchar *vendor_id = g_strdup_printf("%06x", self->oui); fu_device_build_vendor_id(device, "OUI", vendor_id); } /* if not already set using the vendor block or a OUI quirk */ name = fu_ata_device_get_string(id, 27, 46); if (name != NULL) { /* use the name as-is */ if (has_oui_quirk) { fu_device_set_name(FU_DEVICE(self), name); } else { fu_ata_device_parse_vendor_name(self, name); } } /* 8 byte additional product identifier == SKU? */ sku = fu_ata_device_get_string(id, 170, 173); if (sku != NULL) g_debug("SKU=%s", sku); /* add extra GUIDs if none detected from identify block */ if (name != NULL && fu_device_get_guids(device)->len == 0) { g_autofree gchar *name_pad = fu_ata_device_pad_string_for_id(name); if (name_pad != NULL && fu_device_get_version(device) != NULL) { g_autofree gchar *tmp = NULL; tmp = g_strdup_printf("IDE\\%s%s", name_pad, fu_device_get_version(device)); fu_device_add_instance_id(device, tmp); } if (name_pad != NULL) { g_autofree gchar *tmp = NULL; tmp = g_strdup_printf("IDE\\0%s", name_pad); fu_device_add_instance_id(device, tmp); } /* add the name fallback */ fu_device_add_instance_id(device, name); } /* for Phison this is per-chipset -- which is specified in the version prefix */ if (g_strcmp0(fu_device_get_vendor(device), "Phison") == 0 && fu_device_get_version(device) != NULL) { if (g_str_has_prefix(fu_device_get_version(device), "SB")) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); } else if (g_str_has_prefix(fu_device_get_version(device), "SC") || g_str_has_prefix(fu_device_get_version(device), "SH")) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); } } return TRUE; } static gboolean fu_ata_device_probe(FuDevice *device, GError **error) { FuAtaDevice *self = FU_ATA_DEVICE(device); g_autoptr(FuDevice) scsi_parent = NULL; /* set the SCSI physical ID for compat */ scsi_parent = fu_device_get_backend_parent_with_subsystem(device, "scsi", error); if (scsi_parent == NULL) return FALSE; fu_device_set_physical_id(device, fu_device_get_backend_id(scsi_parent)); /* look at the PCI and USB depth to work out if in an external enclosure */ self->pci_depth = fu_udev_device_get_subsystem_depth(FU_UDEV_DEVICE(device), "pci"); self->usb_depth = fu_udev_device_get_subsystem_depth(FU_UDEV_DEVICE(device), "usb"); if (self->pci_depth <= 2 && self->usb_depth <= 2) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE); } return TRUE; } static guint64 fu_ata_device_tf_to_pack_id(struct ata_tf *tf) { guint32 lba24 = (tf->lbah << 16) | (tf->lbam << 8) | (tf->lbal); guint32 lbah = tf->dev & 0x0f; return (((guint64)lbah) << 24) | (guint64)lba24; } static gboolean fu_ata_device_ioctl_buf_cb(FuIoctl *self, gpointer ptr, guint8 *buf, gsize bufsz, GError **error) { struct sg_io_hdr *io_hdr = (struct sg_io_hdr *)ptr; io_hdr->dxferp = buf; io_hdr->dxfer_len = bufsz; return TRUE; } static gboolean fu_ata_device_ioctl_cdb_cb(FuIoctl *self, gpointer ptr, guint8 *buf, gsize bufsz, GError **error) { struct sg_io_hdr *io_hdr = (struct sg_io_hdr *)ptr; io_hdr->cmdp = buf; io_hdr->cmd_len = bufsz; return TRUE; } static gboolean fu_ata_device_ioctl_sense_cb(FuIoctl *self, gpointer ptr, guint8 *buf, gsize bufsz, GError **error) { struct sg_io_hdr *io_hdr = (struct sg_io_hdr *)ptr; io_hdr->sbp = buf; io_hdr->mx_sb_len = bufsz; return TRUE; } static gboolean fu_ata_device_command(FuAtaDevice *self, struct ata_tf *tf, gint dxfer_direction, guint timeout_ms, guint8 *dxferp, gsize dxfer_len, GError **error) { guint8 cdb[SG_ATA_12_LEN] = {0x0}; guint8 sb[32] = {0x0}; sg_io_hdr_t io_hdr = {0x0}; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); /* map _TO_DEV to PIO mode */ if (dxfer_direction == SG_DXFER_TO_DEV) cdb[1] = SG_ATA_PROTO_PIO_OUT; else if (dxfer_direction == SG_DXFER_FROM_DEV) cdb[1] = SG_ATA_PROTO_PIO_IN; else cdb[1] = SG_ATA_PROTO_NON_DATA; /* libata workaround: don't demand sense data for IDENTIFY */ if (dxfer_len > 0) { cdb[2] |= SG_CDB2_TLEN_NSECT | SG_CDB2_TLEN_SECTORS; cdb[2] |= dxfer_direction == SG_DXFER_TO_DEV ? SG_CDB2_TDIR_TO_DEV : SG_CDB2_TDIR_FROM_DEV; } else { cdb[2] = SG_CDB2_CHECK_COND; } /* populate non-LBA48 CDB */ cdb[0] = SG_ATA_12; cdb[3] = tf->feat; cdb[4] = tf->nsect; cdb[5] = tf->lbal; cdb[6] = tf->lbam; cdb[7] = tf->lbah; cdb[8] = tf->dev; cdb[9] = tf->command; /* hit hardware */ io_hdr.interface_id = 'S'; io_hdr.dxfer_direction = dxfer_direction; io_hdr.pack_id = fu_ata_device_tf_to_pack_id(tf); io_hdr.timeout = timeout_ms; /* include these when generating the emulation event */ fu_ioctl_add_key_as_u16(ioctl, "Request", SG_IO); fu_ioctl_add_key_as_u8(ioctl, "DxferDirection", io_hdr.dxfer_direction); fu_ioctl_add_key_as_u8(ioctl, "PackId", io_hdr.pack_id); fu_ioctl_add_mutable_buffer(ioctl, NULL, dxferp, dxfer_len, fu_ata_device_ioctl_buf_cb); fu_ioctl_add_const_buffer(ioctl, "Cdb", cdb, sizeof(cdb), fu_ata_device_ioctl_cdb_cb); fu_ioctl_add_mutable_buffer(ioctl, "Sense", sb, sizeof(sb), fu_ata_device_ioctl_sense_cb); if (!fu_ioctl_execute(ioctl, SG_IO, (guint8 *)&io_hdr, sizeof(io_hdr), NULL, FU_ATA_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_NONE, error)) return FALSE; g_debug("ATA_%u status=0x%x, host_status=0x%x, driver_status=0x%x", io_hdr.cmd_len, io_hdr.status, io_hdr.host_status, io_hdr.driver_status); fu_dump_raw(G_LOG_DOMAIN, "SB", sb, sizeof(sb)); /* error check */ if (io_hdr.status && io_hdr.status != SG_CHECK_CONDITION) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "bad status: 0x%x", io_hdr.status); return FALSE; } if (io_hdr.host_status) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "bad host status: 0x%x", io_hdr.host_status); return FALSE; } if (io_hdr.driver_status && (io_hdr.driver_status != SG_DRIVER_SENSE)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "bad driver status: 0x%x", io_hdr.driver_status); return FALSE; } /* repopulate ata_tf */ tf->error = sb[8 + 3]; tf->nsect = sb[8 + 5]; tf->lbal = sb[8 + 7]; tf->lbam = sb[8 + 9]; tf->lbah = sb[8 + 11]; tf->dev = sb[8 + 12]; tf->status = sb[8 + 13]; g_debug("ATA_%u stat=%02x err=%02x nsect=%02x lbal=%02x " "lbam=%02x lbah=%02x dev=%02x", io_hdr.cmd_len, tf->status, tf->error, tf->nsect, tf->lbal, tf->lbam, tf->lbah, tf->dev); /* io error */ if (tf->status & (ATA_STAT_ERR | ATA_STAT_DRQ)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "I/O error, ata_op=0x%02x ata_status=0x%02x ata_error=0x%02x", tf->command, tf->status, tf->error); return FALSE; } /* success */ return TRUE; } static gboolean fu_ata_device_setup(FuDevice *device, GError **error) { FuAtaDevice *self = FU_ATA_DEVICE(device); struct ata_tf tf = {0x0}; guint8 id[FU_ATA_IDENTIFY_SIZE] = {0x0}; /* get ID block */ tf.dev = ATA_USING_LBA; tf.command = ATA_OP_IDENTIFY; tf.nsect = 1; /* 512 bytes */ if (!fu_ata_device_command(self, &tf, SG_DXFER_FROM_DEV, 1000, id, sizeof(id), error)) { g_prefix_error(error, "failed to IDENTIFY: "); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "IDENTIFY", id, sizeof(id)); if (!fu_ata_device_parse_id(self, id, sizeof(id), error)) return FALSE; /* success */ return TRUE; } static gboolean fu_ata_device_activate(FuDevice *device, FuProgress *progress, GError **error) { FuAtaDevice *self = FU_ATA_DEVICE(device); struct ata_tf tf = {0x0}; /* flush cache and put drive in standby to prepare to activate */ tf.dev = ATA_USING_LBA; tf.command = ATA_OP_FLUSH_CACHE; if (!fu_ata_device_command(self, &tf, SG_DXFER_NONE, 120 * 1000, /* a long time! */ NULL, 0, error)) { g_prefix_error(error, "failed to flush cache immediate: "); return FALSE; } tf.command = ATA_OP_STANDBY_IMMEDIATE; if (!fu_ata_device_command(self, &tf, SG_DXFER_NONE, 120 * 1000, /* a long time! */ NULL, 0, error)) { g_prefix_error(error, "failed to standby immediate: "); return FALSE; } /* load the new firmware */ tf.dev = 0xa0 | ATA_USING_LBA; tf.command = ATA_OP_DOWNLOAD_MICROCODE; tf.feat = ATA_SUBCMD_MICROCODE_ACTIVATE; if (!fu_ata_device_command(self, &tf, SG_DXFER_NONE, 120 * 1000, /* a long time! */ NULL, 0, error)) { g_prefix_error(error, "failed to activate firmware: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_ata_device_fw_download(FuAtaDevice *self, guint32 idx, guint32 addr, const guint8 *data, guint32 data_sz, GError **error) { struct ata_tf tf = {0x0}; guint32 block_count = data_sz / FU_ATA_BLOCK_SIZE; guint32 buffer_offset = addr / FU_ATA_BLOCK_SIZE; /* write block */ tf.dev = 0xa0 | ATA_USING_LBA; tf.command = ATA_OP_DOWNLOAD_MICROCODE; tf.feat = self->transfer_mode; tf.nsect = block_count & 0xff; tf.lbal = block_count >> 8; tf.lbam = buffer_offset & 0xff; tf.lbah = buffer_offset >> 8; if (!fu_ata_device_command(self, &tf, SG_DXFER_TO_DEV, 120 * 1000, /* a long time! */ (guint8 *)data, data_sz, error)) { g_prefix_error(error, "failed to write firmware @0x%0x: ", (guint)addr); return FALSE; } /* check drive status */ if (tf.nsect == 0x0) return TRUE; /* drive wants more data, or thinks it is all done */ if (tf.nsect == 0x1 || tf.nsect == 0x2) return TRUE; /* the offset was set up incorrectly */ if (tf.nsect == 0x4) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "alignment error"); return FALSE; } /* other error */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "unknown return code 0x%02x", tf.nsect); return FALSE; } static gboolean fu_ata_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuAtaDevice *self = FU_ATA_DEVICE(device); gsize streamsz = 0; guint32 chunksz = (guint32)self->transfer_blocks * FU_ATA_BLOCK_SIZE; guint max_size = 0xffff * FU_ATA_BLOCK_SIZE; g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* get default image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* only one block allowed */ if (self->transfer_mode == ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNK) max_size = 0xffff; /* check is valid */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz > max_size) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "firmware is too large, maximum size is %u", max_size); return FALSE; } if (streamsz % FU_ATA_BLOCK_SIZE != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "firmware is not multiple of block size %i", FU_ATA_BLOCK_SIZE); return FALSE; } /* write each block */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, chunksz, error); if (chunks == NULL) return FALSE; fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_ata_device_fw_download(self, fu_chunk_get_idx(chk), fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "failed to write chunk %u: ", i); return FALSE; } fu_progress_step_done(progress); } /* success! */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION); return TRUE; } static gboolean fu_ata_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuAtaDevice *self = FU_ATA_DEVICE(device); guint64 tmp = 0; if (g_strcmp0(key, "AtaTransferMode") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; if (tmp != ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS_ACTIVATE && tmp != ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS && tmp != ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNK) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "AtaTransferMode only supports " "values 0x3, 0x7 or 0xe"); return FALSE; } self->transfer_mode = (guint8)tmp; return TRUE; } if (g_strcmp0(key, "AtaTransferBlocks") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->transfer_blocks = (guint16)tmp; return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_ata_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 98, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static void fu_ata_device_init(FuAtaDevice *self) { /* we chose this default as _DOWNLOAD_CHUNKS_ACTIVATE applies the * firmware straight away and the kernel might not like the unexpected * ATA restart and panic */ self->transfer_mode = ATA_SUBCMD_MICROCODE_DOWNLOAD_CHUNKS; fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_INHERIT_ACTIVATION); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_SIGNED); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_FLAGS); fu_device_set_summary(FU_DEVICE(self), "ATA drive"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_DRIVE_HARDDISK); fu_device_add_protocol(FU_DEVICE(self), "org.t13.ata"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); } static void fu_ata_device_class_init(FuAtaDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_ata_device_to_string; device_class->set_quirk_kv = fu_ata_device_set_quirk_kv; device_class->setup = fu_ata_device_setup; device_class->activate = fu_ata_device_activate; device_class->write_firmware = fu_ata_device_write_firmware; device_class->probe = fu_ata_device_probe; device_class->set_progress = fu_ata_device_set_progress; } FuAtaDevice * fu_ata_device_new_from_blob(FuContext *ctx, const guint8 *buf, gsize sz, GError **error) { g_autoptr(FuAtaDevice) self = NULL; self = g_object_new(FU_TYPE_ATA_DEVICE, "context", ctx, NULL); if (!fu_ata_device_parse_id(self, buf, sz, error)) return NULL; return g_steal_pointer(&self); } fwupd-2.0.10/plugins/ata/fu-ata-device.h000066400000000000000000000010251501337203100177210ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ATA_DEVICE (fu_ata_device_get_type()) G_DECLARE_FINAL_TYPE(FuAtaDevice, fu_ata_device, FU, ATA_DEVICE, FuUdevDevice) FuAtaDevice * fu_ata_device_new_from_blob(FuContext *ctx, const guint8 *buf, gsize sz, GError **error); /* for self tests */ guint8 fu_ata_device_get_transfer_mode(FuAtaDevice *self); guint16 fu_ata_device_get_transfer_blocks(FuAtaDevice *self); fwupd-2.0.10/plugins/ata/fu-ata-plugin.c000066400000000000000000000013461501337203100177610ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-ata-device.h" #include "fu-ata-plugin.h" struct _FuAtaPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuAtaPlugin, fu_ata_plugin, FU_TYPE_PLUGIN) static void fu_ata_plugin_init(FuAtaPlugin *self) { } static void fu_ata_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_udev_subsystem(plugin, "block:disk"); fu_plugin_add_device_gtype(plugin, FU_TYPE_ATA_DEVICE); } static void fu_ata_plugin_class_init(FuAtaPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_ata_plugin_constructed; } fwupd-2.0.10/plugins/ata/fu-ata-plugin.h000066400000000000000000000003431501337203100177620ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuAtaPlugin, fu_ata_plugin, FU, ATA_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/ata/fu-self-test.c000066400000000000000000000063061501337203100176270ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-ata-device.h" #include "fu-context-private.h" #include "fu-device-private.h" static void fu_ata_id_func(void) { gboolean ret; gsize sz; g_autofree gchar *data = NULL; g_autofree gchar *path = NULL; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuAtaDevice) dev = NULL; g_autoptr(GError) error = NULL; ret = fu_context_load_quirks(ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE, &error); g_assert_no_error(error); g_assert_true(ret); path = g_test_build_filename(G_TEST_DIST, "tests", "StarDrive-SBFM61.2.bin", NULL); if (!g_file_test(path, G_FILE_TEST_EXISTS)) { g_test_skip("Missing StarDrive-SBFM61.2.bin"); return; } ret = g_file_get_contents(path, &data, &sz, &error); g_assert_no_error(error); g_assert_true(ret); dev = fu_ata_device_new_from_blob(ctx, (guint8 *)data, sz, &error); g_assert_no_error(error); g_assert_nonnull(dev); g_assert_cmpint(fu_ata_device_get_transfer_mode(dev), ==, 0xe); g_assert_cmpint(fu_ata_device_get_transfer_blocks(dev), ==, 0x1); g_assert_cmpstr(fu_device_get_serial(FU_DEVICE(dev)), ==, "A45A078A198600476509"); g_assert_cmpstr(fu_device_get_name(FU_DEVICE(dev)), ==, "SATA SSD"); g_assert_cmpstr(fu_device_get_version(FU_DEVICE(dev)), ==, "SBFM61.2"); } static void fu_ata_oui_func(void) { gboolean ret; gsize sz; g_autofree gchar *data = NULL; g_autofree gchar *path = NULL; g_autofree gchar *str = NULL; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuAtaDevice) dev = NULL; g_autoptr(GError) error = NULL; ret = fu_context_load_quirks(ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE, &error); g_assert_no_error(error); g_assert_true(ret); path = g_test_build_filename(G_TEST_DIST, "tests", "Samsung SSD 860 EVO 500GB.bin", NULL); if (!g_file_test(path, G_FILE_TEST_EXISTS)) { g_test_skip("Missing Samsung SSD 860 EVO 500GB.bin"); return; } ret = g_file_get_contents(path, &data, &sz, &error); g_assert_no_error(error); g_assert_true(ret); dev = fu_ata_device_new_from_blob(ctx, (guint8 *)data, sz, &error); g_assert_no_error(error); g_assert_nonnull(dev); fu_device_convert_instance_ids(FU_DEVICE(dev)); str = fu_device_to_string(FU_DEVICE(dev)); g_debug("%s", str); g_assert_cmpint(fu_ata_device_get_transfer_mode(dev), ==, 0xe); g_assert_cmpint(fu_ata_device_get_transfer_blocks(dev), ==, 0x1); g_assert_cmpstr(fu_device_get_serial(FU_DEVICE(dev)), ==, "S3Z1NB0K862928X"); g_assert_cmpstr(fu_device_get_name(FU_DEVICE(dev)), ==, "SSD 860 EVO 500GB"); g_assert_cmpstr(fu_device_get_version(FU_DEVICE(dev)), ==, "RVT01B6Q"); } int main(int argc, char **argv) { g_autofree gchar *testdatadir = NULL; (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); (void)g_setenv("G_MESSAGES_DEBUG", "all", TRUE); testdatadir = g_test_build_filename(G_TEST_DIST, "tests", NULL); (void)g_setenv("FWUPD_SYSFSFWATTRIBDIR", testdatadir, TRUE); /* tests go here */ g_test_add_func("/fwupd/ata/id", fu_ata_id_func); g_test_add_func("/fwupd/ata/oui", fu_ata_oui_func); return g_test_run(); } fwupd-2.0.10/plugins/ata/meson.build000066400000000000000000000024441501337203100173060ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginAta"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('ata.quirk') plugin_builtin_ata = static_library('fu_plugin_ata', sources: [ 'fu-ata-plugin.c', 'fu-ata-device.c', ], include_directories: plugin_incdirs, c_args: [ cargs, '-DLOCALSTATEDIR="' + localstatedir + '"', ], link_with: plugin_libs, dependencies: plugin_deps, ) plugin_builtins += plugin_builtin_ata enumeration_data += files('tests/samsung-ssd-870-evo-setup.json') device_tests += files('tests/samsung-ssd-870-evo.json') if get_option('tests') env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) env.set('FWUPD_DATADIR_QUIRKS', meson.current_source_dir()) e = executable( 'ata-self-test', sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: plugin_deps, link_with: [ plugin_libs, plugin_builtin_ata, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('ata-self-test', e, env: env) # added to installed-tests endif endif fwupd-2.0.10/plugins/ata/tests/000077500000000000000000000000001501337203100163025ustar00rootroot00000000000000fwupd-2.0.10/plugins/ata/tests/samsung-ssd-870-evo-setup.json000066400000000000000000000115371501337203100237110ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUdevDevice", "BackendId": "/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:2/end_device-17:2/target17:0:2/17:0:2:0/block/sdd", "DeviceFile": "/dev/sdd", "Subsystem": "block", "Created": "2024-11-06T16:43:40.107335Z", "Events": [ { "Id": "#d5a801ad", "Data": "block" }, { "Id": "#ad8c58d3" }, { "Id": "#1075ed5c", "Data": "disk" }, { "Id": "#bddbca22", "Data": "MAJOR=8\nMINOR=48\nDEVNAME=sdd\nDEVTYPE=disk\nDISKSEQ=4" }, { "Id": "#d432c663", "Data": "sdd" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c", "Data": "disk" }, { "Id": "#19c26604" }, { "Id": "#862c0ee3", "GType": "FuUdevDevice", "BackendId": "/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:2/end_device-17:2/target17:0:2/17:0:2:0/block/sdd" }, { "Id": "#d5a801ad", "Data": "scsi" }, { "Id": "#ad8c58d3", "Data": "sd" }, { "Id": "#1075ed5c", "Data": "scsi_device" }, { "Id": "#bddbca22", "Data": "DEVTYPE=scsi_device\nDRIVER=sd\nMODALIAS=scsi:t-0x00" }, { "Id": "#d432c663" }, { "Id": "#9b895db2", "Data": "ATA " }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c", "Data": "scsi_device" }, { "Id": "#285d9a88", "GType": "FuPciDevice", "BackendId": "/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:2/end_device-17:2/target17:0:2/17:0:2:0/block/sdd", "PhysicalId": "/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:2/end_device-17:2/target17:0:2/17:0:2:0" }, { "Id": "#d5a801ad", "Data": "pci" }, { "Id": "#ad8c58d3", "Data": "mpt3sas" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "DRIVER=mpt3sas\nPCI_CLASS=10700\nPCI_ID=1000:00AF\nPCI_SUBSYS_ID=1D49:0200\nPCI_SLOT_NAME=0000:51:00.0\nMODALIAS=pci:v00001000d000000AFsv00001D49sd00000200bc01sc07i00" }, { "Id": "#d432c663" }, { "Id": "#9b895db2", "Data": "0x1000" }, { "Id": "#66f3e150", "Data": "0x00af" }, { "Id": "#d410b6c7", "Data": "0x010700" }, { "Id": "#1075ed5c" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "DRIVER=mpt3sas\nPCI_CLASS=10700\nPCI_ID=1000:00AF\nPCI_SUBSYS_ID=1D49:0200\nPCI_SLOT_NAME=0000:51:00.0\nMODALIAS=pci:v00001000d000000AFsv00001D49sd00000200bc01sc07i00" }, { "Id": "#d432c663" }, { "Id": "#9b895db2", "Data": "0x1000" }, { "Id": "#66f3e150", "Data": "0x00af" }, { "Id": "#d410b6c7", "Data": "0x010700" }, { "Id": "#1075ed5c" }, { "Id": "#fbe018fe" }, { "Id": "#d410b6c7", "Data": "0x010700" }, { "Id": "#bf29d2f6", "Data": "0x01" }, { "Id": "#269abd81", "Data": "0x1d49" }, { "Id": "#360cec38", "Data": "0x0200" }, { "Id": "#d2629d83", "Data": "0000:51:00.0" }, { "Id": "#3df6ae5e" }, { "Id": "#fb0427e1", "SenseDataOut": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", "DataOut": "QAD/PzfIEAAAAAAAPwAAAAAAAAA2U1BQWE5XMDAyNzM1MSBQICAgIAAAAAAAAFZTMFRCMlE2YVNzbW51IGdTUyBENzggMFZFIE9UMiBCICAgICAgICAgICAgICAgIAGAAUAALwBAAAIAAgcA/z8QAD8AEPz7AAEB////DwAABwADAHgAeAB4AHgAME8AAAAAAAAAAAAAHwAOhcYAbAVkAPwJXgBrdAF9Y0FpdAG8Y0F/QAIABAAAAP7/AAAAAAAAAAAAAAAAAACwiODoAAAAAAAACAAAQAAAAlCPUyJDsSIAAAAAAAAAAAAAAAAAAB5AHEAAAAAAAAAAAAAAAAAAACEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwABACAgICAgICAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD0AAAAAAABAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA/xEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApck=" } ] } ] } fwupd-2.0.10/plugins/ata/tests/samsung-ssd-870-evo.json000066400000000000000000000005251501337203100225460ustar00rootroot00000000000000{ "name": "Samsung SSD 870 EVO 2TB", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/samsung-ssd-870-evo-setup.json", "components": [ { "version": "SVT02B6Q", "guids": [ "e0518d76-10bc-5c2d-9693-ee18a7e91881" ] } ] } ] } fwupd-2.0.10/plugins/aver-hid/000077500000000000000000000000001501337203100160725ustar00rootroot00000000000000fwupd-2.0.10/plugins/aver-hid/README.md000066400000000000000000000034671501337203100173630ustar00rootroot00000000000000--- title: Plugin: Aver HID-ISP --- ## Introduction The AVer HID In-System-Programming plugin is used for various products that can be updated using a proprietary HID protocol. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a packed binary file format. This plugin supports the following protocol ID: * `com.aver.hid` * `com.aver.safeisp` ## GUID Generation These devices use the standard DeviceInstanceId values, e.g. * `USB\VID_2574&PID_09F0` ## Update Behavior The device is updated using a HID request/response with a fixed size payload. Once ready, the plugin can start the update with the `UVCX_UCAM_ISP_FILE_START` header. After the device sends back the `UVCX_UCAM_ISP_FILE_START` packet, the PC process can send the firmware file in chunks using `UVCX_UCAM_ISP_FILE_DNLOAD`. After the last chunk, the plugin sends `UVCX_UCAM_ISP_FILE_END` packet and the device will check whether the firmware is valid. If the firmware file is correct, the device will send `UVCX_UCAM_ISP_START` to PC, and the plugin can continuously send `UVCX_UCAM_ISP_STATUS` to get the ISP progress percentage. If the firmware file is incorrect, the device sends `UVCX_UCAM_ISP_STOP` back to the plugin, and the ISP progress should be terminated. The PC process should go back to `UVCX_UCAM_ISP_STATUS` and restart the process if needed. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x2574` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.9.2`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Pierce Wang: @AVer-V001598 fwupd-2.0.10/plugins/aver-hid/aver-hid.quirk000066400000000000000000000005431501337203100206500ustar00rootroot00000000000000# AVer Fone540 [USB\VID_2574&PID_09F0] Plugin = aver_hid GType = FuAverHidDevice # AVer CAM520Pro3 [USB\VID_2574&PID_0B20] Plugin = aver_hid GType = FuAverHidDevice # AVer VB342Pro [USB\VID_2574&PID_0AB1] Plugin = aver_hid Flags = dual-isp GType = FuAverHidDevice # AVer CAM340Plus [USB\VID_2574&PID_0980] Plugin = aver_hid GType = FuAverSafeispDevice fwupd-2.0.10/plugins/aver-hid/fu-aver-hid-device.c000066400000000000000000000423701501337203100216100ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-aver-hid-device.h" #include "fu-aver-hid-firmware.h" #include "fu-aver-hid-struct.h" struct _FuAverHidDevice { FuHidDevice parent_instance; }; G_DEFINE_TYPE(FuAverHidDevice, fu_aver_hid_device, FU_TYPE_HID_DEVICE) #define FU_AVER_HID_DEVICE_TIMEOUT 200 /* ms */ #define FU_AVER_HID_DEVICE_GET_STATUS_POLL_INTERVAL 1000 /* ms */ #define FU_AVER_HID_DEVICE_POLL_INTERVAL 5000 /* ms */ #define FU_AVER_HID_DEVICE_ISP_RETRY_COUNT 300 #define FU_AVER_HID_DEVICE_ISP_UNTAR_WAIT_COUNT 600 #define FU_AVER_HID_FLAG_DUAL_ISP "dual-isp" static gboolean fu_aver_hid_device_transfer(FuAverHidDevice *self, GByteArray *req, GByteArray *res, GError **error) { if (req != NULL) { if (!fu_hid_device_set_report(FU_HID_DEVICE(self), req->data[0], req->data, req->len, FU_AVER_HID_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error)) { g_prefix_error(error, "failed to send packet: "); return FALSE; } } if (res != NULL) { if (!fu_hid_device_get_report(FU_HID_DEVICE(self), res->data[0], res->data, res->len, FU_AVER_HID_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error)) { g_prefix_error(error, "failed to receive packet: "); return FALSE; } g_debug("custom-isp-cmd: %s [0x%x]", fu_aver_hid_custom_isp_cmd_to_string( fu_struct_aver_hid_res_isp_get_custom_isp_cmd(res)), fu_struct_aver_hid_res_isp_get_custom_isp_cmd(res)); } return TRUE; } static gboolean fu_aver_hid_device_ensure_status(FuAverHidDevice *self, GError **error) { g_autoptr(GByteArray) req = fu_struct_aver_hid_req_isp_new(); g_autoptr(GByteArray) res = fu_struct_aver_hid_res_isp_status_new(); fu_struct_aver_hid_req_isp_set_custom_isp_cmd(req, FU_AVER_HID_CUSTOM_ISP_CMD_STATUS); if (!fu_aver_hid_device_transfer(self, req, res, error)) return FALSE; if (!fu_struct_aver_hid_res_isp_status_validate(res->data, res->len, 0x0, error)) return FALSE; if (fu_struct_aver_hid_res_isp_status_get_status(res) == FU_AVER_HID_STATUS_BUSY) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "device has status %s", fu_aver_hid_status_to_string( fu_struct_aver_hid_res_isp_status_get_status(res))); return FALSE; } return TRUE; } static gboolean fu_aver_hid_device_ensure_version(FuAverHidDevice *self, GError **error) { g_autofree gchar *ver = NULL; g_autoptr(GByteArray) req = fu_struct_aver_hid_req_device_version_new(); g_autoptr(GByteArray) res = fu_struct_aver_hid_res_device_version_new(); g_autoptr(GError) error_local = NULL; if (!fu_aver_hid_device_transfer(self, req, res, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT)) { g_debug("ignoring %s", error_local->message); fu_device_set_version(FU_DEVICE(self), "0.0.0000.00"); return TRUE; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } if (!fu_struct_aver_hid_res_device_version_validate(res->data, res->len, 0x0, error)) return FALSE; ver = fu_strsafe((const gchar *)fu_struct_aver_hid_res_device_version_get_ver(res, NULL), FU_STRUCT_AVER_HID_RES_DEVICE_VERSION_SIZE_VER); fu_device_set_version(FU_DEVICE(self), ver); return TRUE; } static gboolean fu_aver_hid_device_setup(FuDevice *device, GError **error) { FuAverHidDevice *self = FU_AVER_HID_DEVICE(device); /* HidDevice->setup */ if (!FU_DEVICE_CLASS(fu_aver_hid_device_parent_class)->setup(device, error)) return FALSE; /* ensure that the device status is updateable */ if (!fu_aver_hid_device_ensure_status(self, error)) return FALSE; /* get the version from the hardware while open */ if (!fu_aver_hid_device_ensure_version(self, error)) return FALSE; /* success */ return TRUE; } static FuFirmware * fu_aver_hid_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { g_autoptr(FuFirmware) firmware = fu_aver_hid_firmware_new(); if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; return g_steal_pointer(&firmware); } static gboolean fu_aver_hid_device_isp_file_dnload(FuAverHidDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GByteArray) req = fu_struct_aver_hid_req_isp_file_dnload_new(); g_autoptr(GByteArray) res = fu_struct_aver_hid_res_isp_status_new(); /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; /* copy in payload */ if (fu_device_has_private_flag(FU_DEVICE(self), FU_AVER_HID_FLAG_DUAL_ISP)) { fu_struct_aver_hid_req_isp_file_dnload_set_custom_isp_cmd( req, FU_AVER_HID_CUSTOM_ISP_CMD_ALL_FILE_DNLOAD); } else { fu_struct_aver_hid_req_isp_file_dnload_set_custom_isp_cmd( req, FU_AVER_HID_CUSTOM_ISP_CMD_FILE_DNLOAD); } if (!fu_memcpy_safe(req->data, req->len, FU_STRUCT_AVER_HID_REQ_ISP_FILE_DNLOAD_OFFSET_DATA, /* dst */ fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0x0, /* src */ fu_chunk_get_data_sz(chk), error)) return FALSE; /* resize the last packet */ if ((i == (fu_chunk_array_length(chunks) - 1)) && (fu_chunk_get_data_sz(chk) < FU_STRUCT_AVER_HID_REQ_ISP_FILE_DNLOAD_SIZE_DATA)) fu_byte_array_set_size(req, 3 + fu_chunk_get_data_sz(chk), 0x0); if (!fu_aver_hid_device_transfer(self, req, res, error)) return FALSE; if (!fu_struct_aver_hid_res_isp_status_validate(res->data, res->len, 0x0, error)) return FALSE; /* invalid chunk */ if (fu_struct_aver_hid_res_isp_status_get_status(res) == FU_AVER_HID_STATUS_FILEERR) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "device has status %s", fu_aver_hid_status_to_string( fu_struct_aver_hid_res_isp_status_get_status(res))); return FALSE; } /* update progress */ fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_aver_hid_device_wait_for_ready_cb(FuDevice *device, gpointer user_data, GError **error) { FuAverHidDevice *self = FU_AVER_HID_DEVICE(device); g_autoptr(GByteArray) req = fu_struct_aver_hid_req_isp_new(); g_autoptr(GByteArray) res = fu_struct_aver_hid_res_isp_status_new(); fu_struct_aver_hid_req_isp_set_custom_isp_cmd(req, FU_AVER_HID_CUSTOM_ISP_CMD_STATUS); if (!fu_aver_hid_device_transfer(self, req, res, error)) return FALSE; if (!fu_struct_aver_hid_res_isp_status_validate(res->data, res->len, 0x0, error)) return FALSE; if (fu_struct_aver_hid_res_isp_status_get_status(res) != FU_AVER_HID_STATUS_READY) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "device has status %s", fu_aver_hid_status_to_string( fu_struct_aver_hid_res_isp_status_get_status(res))); return FALSE; } return TRUE; } static gboolean fu_aver_hid_device_isp_file_start(FuAverHidDevice *self, gsize sz, const gchar *name, GError **error) { g_autoptr(GByteArray) req = fu_struct_aver_hid_req_isp_file_start_new(); g_autoptr(GByteArray) res = fu_struct_aver_hid_res_isp_status_new(); if (fu_device_has_private_flag(FU_DEVICE(self), FU_AVER_HID_FLAG_DUAL_ISP)) { fu_struct_aver_hid_req_isp_file_start_set_custom_isp_cmd( req, FU_AVER_HID_CUSTOM_ISP_CMD_ALL_FILE_START); } else { fu_struct_aver_hid_req_isp_file_start_set_custom_isp_cmd( req, FU_AVER_HID_CUSTOM_ISP_CMD_FILE_START); } if (!fu_struct_aver_hid_req_isp_file_start_set_file_name(req, name, error)) return FALSE; fu_struct_aver_hid_req_isp_file_start_set_file_size(req, sz); if (!fu_aver_hid_device_transfer(self, req, res, error)) return FALSE; if (!fu_struct_aver_hid_res_isp_status_validate(res->data, res->len, 0x0, error)) return FALSE; return TRUE; } static gboolean fu_aver_hid_device_isp_file_end(FuAverHidDevice *self, gsize sz, const gchar *name, GError **error) { g_autoptr(GByteArray) req = fu_struct_aver_hid_req_isp_file_end_new(); g_autoptr(GByteArray) res = fu_struct_aver_hid_res_isp_status_new(); if (fu_device_has_private_flag(FU_DEVICE(self), FU_AVER_HID_FLAG_DUAL_ISP)) { fu_struct_aver_hid_req_isp_file_end_set_custom_isp_cmd( req, FU_AVER_HID_CUSTOM_ISP_CMD_ALL_FILE_END); } else { fu_struct_aver_hid_req_isp_file_end_set_custom_isp_cmd( req, FU_AVER_HID_CUSTOM_ISP_CMD_FILE_END); } if (!fu_struct_aver_hid_req_isp_file_end_set_file_name(req, name, error)) return FALSE; fu_struct_aver_hid_req_isp_file_end_set_end_flag(req, 1); fu_struct_aver_hid_req_isp_file_end_set_file_size(req, sz); if (!fu_aver_hid_device_transfer(self, req, res, error)) return FALSE; if (!fu_struct_aver_hid_res_isp_status_validate(res->data, res->len, 0x0, error)) return FALSE; return TRUE; } static gboolean fu_aver_hid_device_wait_for_untar_cb(FuDevice *device, gpointer user_data, GError **error) { FuAverHidDevice *self = FU_AVER_HID_DEVICE(device); g_autoptr(GByteArray) req = fu_struct_aver_hid_req_isp_file_end_new(); g_autoptr(GByteArray) res = fu_struct_aver_hid_res_isp_status_new(); fu_struct_aver_hid_req_isp_set_custom_isp_cmd(req, FU_AVER_HID_CUSTOM_ISP_CMD_STATUS); if (!fu_aver_hid_device_transfer(self, req, res, error)) return FALSE; g_info("isp status: %s", fu_aver_hid_status_to_string(fu_struct_aver_hid_res_isp_status_get_status(res))); if (fu_struct_aver_hid_res_isp_status_get_status(res) != FU_AVER_HID_STATUS_WAITUSR) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "device has status %s", fu_aver_hid_status_to_string( fu_struct_aver_hid_res_isp_status_get_status(res))); return FALSE; } return TRUE; } static gboolean fu_aver_hid_device_isp_start(FuAverHidDevice *self, GError **error) { g_autoptr(GByteArray) req = fu_struct_aver_hid_req_isp_new(); g_autoptr(GByteArray) res = fu_struct_aver_hid_res_isp_status_new(); if (fu_device_has_private_flag(FU_DEVICE(self), FU_AVER_HID_FLAG_DUAL_ISP)) { fu_struct_aver_hid_req_isp_start_set_custom_isp_cmd( req, FU_AVER_HID_CUSTOM_ISP_CMD_ALL_START); } else { fu_struct_aver_hid_req_isp_start_set_custom_isp_cmd( req, FU_AVER_HID_CUSTOM_ISP_CMD_START); } if (!fu_aver_hid_device_transfer(self, req, res, error)) return FALSE; if (!fu_struct_aver_hid_res_isp_status_validate(res->data, res->len, 0x0, error)) return FALSE; return TRUE; } static gboolean fu_aver_hid_device_isp_reboot(FuAverHidDevice *self, GError **error) { g_autoptr(GByteArray) req = fu_struct_aver_hid_req_isp_new(); fu_struct_aver_hid_req_isp_set_custom_isp_cmd(req, FU_AVER_HID_CUSTOM_ISP_CMD_ISP_REBOOT); return fu_aver_hid_device_transfer(self, req, NULL, error); } static gboolean fu_aver_hid_device_wait_for_reboot_cb(FuDevice *device, gpointer user_data, GError **error) { FuAverHidDevice *self = FU_AVER_HID_DEVICE(device); FuProgress *progress = FU_PROGRESS(user_data); g_autoptr(GByteArray) req = fu_struct_aver_hid_req_isp_new(); g_autoptr(GByteArray) res = fu_struct_aver_hid_res_isp_status_new(); fu_struct_aver_hid_req_isp_set_custom_isp_cmd(req, FU_AVER_HID_CUSTOM_ISP_CMD_STATUS); if (!fu_aver_hid_device_transfer(self, req, res, error)) return FALSE; if (fu_struct_aver_hid_res_isp_status_get_status(res) == FU_AVER_HID_STATUS_ISPING) { guint8 percentage = fu_struct_aver_hid_res_isp_status_get_progress(res); if (percentage < 100) fu_progress_set_percentage(progress, percentage); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "device has status %s", fu_aver_hid_status_to_string( fu_struct_aver_hid_res_isp_status_get_status(res))); return FALSE; } if (fu_struct_aver_hid_res_isp_status_get_status(res) != FU_AVER_HID_STATUS_REBOOT) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "device has status %s", fu_aver_hid_status_to_string( fu_struct_aver_hid_res_isp_status_get_status(res))); return FALSE; } return TRUE; } static gboolean fu_aver_hid_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuAverHidDevice *self = FU_AVER_HID_DEVICE(device); const gchar *aver_fw_name = NULL; gsize fw_size; g_autoptr(FuArchive) archive = NULL; g_autoptr(GBytes) aver_fw = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 80, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 15, NULL); /* get default image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* decompress */ archive = fu_archive_new_stream(stream, FU_ARCHIVE_FLAG_NONE, error); if (archive == NULL) return FALSE; aver_fw_name = fu_firmware_get_filename(firmware); aver_fw = fu_archive_lookup_by_fn(archive, aver_fw_name, error); if (aver_fw == NULL) return FALSE; fw_size = g_bytes_get_size(aver_fw); /* wait for ST_READY */ if (!fu_device_retry_full(device, fu_aver_hid_device_wait_for_ready_cb, 5, FU_AVER_HID_DEVICE_GET_STATUS_POLL_INTERVAL, NULL, error)) return FALSE; fu_progress_step_done(progress); /* ISP_FILE_START */ if (!fu_aver_hid_device_isp_file_start(self, fw_size, aver_fw_name, error)) return FALSE; fu_progress_step_done(progress); /* ISP_FILE_DNLOAD */ chunks = fu_chunk_array_new_from_bytes(aver_fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_STRUCT_AVER_HID_REQ_ISP_FILE_DNLOAD_SIZE_DATA); if (!fu_aver_hid_device_isp_file_dnload(self, chunks, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* ISP_FILE_END */ if (!fu_aver_hid_device_isp_file_end(self, fw_size, aver_fw_name, error)) return FALSE; /* poll for the file untar progress */ if (!fu_device_retry_full(device, fu_aver_hid_device_wait_for_untar_cb, FU_AVER_HID_DEVICE_ISP_UNTAR_WAIT_COUNT, FU_AVER_HID_DEVICE_GET_STATUS_POLL_INTERVAL, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* ISP_START */ if (!fu_aver_hid_device_isp_start(self, error)) return FALSE; fu_progress_step_done(progress); /* poll for the actual write progress */ if (!fu_device_retry_full(device, fu_aver_hid_device_wait_for_reboot_cb, FU_AVER_HID_DEVICE_ISP_RETRY_COUNT, FU_AVER_HID_DEVICE_GET_STATUS_POLL_INTERVAL, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* send ISP_REBOOT, no response expected */ if (!fu_aver_hid_device_isp_reboot(self, error)) return FALSE; fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* success! */ return TRUE; } static void fu_aver_hid_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 74, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 25, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reload"); } static void fu_aver_hid_device_init(FuAverHidDevice *self) { fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); fu_device_add_protocol(FU_DEVICE(self), "com.aver.hid"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SELF_RECOVERY); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_AUTO_PAUSE_POLLING); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_RETRY_OPEN); fu_device_set_poll_interval(FU_DEVICE(self), FU_AVER_HID_DEVICE_POLL_INTERVAL); fu_device_set_remove_delay(FU_DEVICE(self), 150000); fu_usb_device_set_claim_retry_count(FU_USB_DEVICE(self), 5); fu_hid_device_add_flag(FU_HID_DEVICE(self), FU_HID_DEVICE_FLAG_RETRY_FAILURE); fu_hid_device_add_flag(FU_HID_DEVICE(self), FU_HID_DEVICE_FLAG_AUTODETECT_EPS); fu_device_register_private_flag(FU_DEVICE(self), FU_AVER_HID_FLAG_DUAL_ISP); } static void fu_aver_hid_device_class_init(FuAverHidDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_aver_hid_device_setup; device_class->prepare_firmware = fu_aver_hid_device_prepare_firmware; device_class->write_firmware = fu_aver_hid_device_write_firmware; device_class->set_progress = fu_aver_hid_device_set_progress; } fwupd-2.0.10/plugins/aver-hid/fu-aver-hid-device.h000066400000000000000000000004641501337203100216130ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_AVER_HID_DEVICE (fu_aver_hid_device_get_type()) G_DECLARE_FINAL_TYPE(FuAverHidDevice, fu_aver_hid_device, FU, AVER_HID_DEVICE, FuHidDevice) fwupd-2.0.10/plugins/aver-hid/fu-aver-hid-firmware.c000066400000000000000000000030401501337203100221540ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-aver-hid-firmware.h" struct _FuAverHidFirmware { FuFirmware parent_instance; }; G_DEFINE_TYPE(FuAverHidFirmware, fu_aver_hid_firmware, FU_TYPE_FIRMWARE) static gboolean fu_aver_hid_firmware_parse_archive_cb(FuArchive *self, const gchar *filename, GBytes *bytes, gpointer user_data, GError **error) { FuFirmware *firmware = FU_FIRMWARE(user_data); if (g_str_has_suffix(filename, ".dat")) { g_autofree gchar *version = g_strndup(filename, strlen(filename) - 4); fu_firmware_set_version(firmware, version); fu_firmware_set_filename(firmware, filename); } return TRUE; } static gboolean fu_aver_hid_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { g_autoptr(FuArchive) archive = NULL; archive = fu_archive_new_stream(stream, FU_ARCHIVE_FLAG_NONE, error); if (archive == NULL) return FALSE; if (!fu_archive_iterate(archive, fu_aver_hid_firmware_parse_archive_cb, firmware, error)) return FALSE; return TRUE; } static void fu_aver_hid_firmware_init(FuAverHidFirmware *self) { } static void fu_aver_hid_firmware_class_init(FuAverHidFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_aver_hid_firmware_parse; } FuFirmware * fu_aver_hid_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_AVER_HID_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/aver-hid/fu-aver-hid-firmware.h000066400000000000000000000005531501337203100221670ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_AVER_HID_FIRMWARE (fu_aver_hid_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuAverHidFirmware, fu_aver_hid_firmware, FU, AVER_HID_FIRMWARE, FuFirmware) FuFirmware * fu_aver_hid_firmware_new(void); fwupd-2.0.10/plugins/aver-hid/fu-aver-hid-plugin.c000066400000000000000000000016621501337203100216460ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-aver-hid-device.h" #include "fu-aver-hid-firmware.h" #include "fu-aver-hid-plugin.h" #include "fu-aver-safeisp-device.h" struct _FuAverHidPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuAverHidPlugin, fu_aver_hid_plugin, FU_TYPE_PLUGIN) static void fu_aver_hid_plugin_init(FuAverHidPlugin *self) { } static void fu_aver_hid_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_AVER_HID_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_AVER_SAFEISP_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_AVER_HID_FIRMWARE); } static void fu_aver_hid_plugin_class_init(FuAverHidPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_aver_hid_plugin_constructed; } fwupd-2.0.10/plugins/aver-hid/fu-aver-hid-plugin.h000066400000000000000000000003611501337203100216460ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuAverHidPlugin, fu_aver_hid_plugin, FU, AVER_HID_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/aver-hid/fu-aver-hid.rs000066400000000000000000000103241501337203100205470ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] enum FuAverHidStatus { Ready, Busy, Dnload, Waitisp, Isping, Reboot, Fileerr, Powerisp, Version, Waitusr, Stop, } #[derive(ToString)] enum FuAverHidCustomIspCmd { Status = 0x01, FileStart, FileDnload, FileEnd, Start, Stop, Reserve, LogStart, LogUpload, IspReboot, LogEnd, AllFileStart = 0x11, AllFileDnload, AllFileEnd, AllStart, } #[derive(ToString)] enum FuAverSafeispCustomCmd { GetVersion = 0x14, Support = 0x29, EraseTemp, UploadPrepare, UploadCompareChecksum, UploadToCx3, UploadToM12mo, UploadToM051, UploadToTmpm342, UploadToTmpm342Boot, UpdateStart, } enum FuAverSafeispAckStatus { Idle = 0x00, Success, Checksum, ParamErr, PrepareFail, UploadFail, DataRead, DataReadFail, DataWrite, DataWriteFail, VerifyFail, CompareSame, CompareDiff, Support, } #[derive(Getters, New, Default)] #[repr(C, packed)] struct FuStructAverHidReqIsp { report_id_custom_command: u8 == 0x08, custom_cmd_isp: u8 == 0x10, custom_isp_cmd: u8, data: [u8; 508] = 0xFF, end: u8 == 0x00, } #[derive(Setters, Getters, New, Default)] #[repr(C, packed)] struct FuStructAverHidReqIspFileStart { report_id_custom_command: u8 == 0x08, custom_cmd_isp: u8 == 0x10, custom_isp_cmd: u8, file_name: [char; 52], file_size: u32le, free_space: u32le, _reserved: [u8; 448] = 0xFF, end: u8 == 0x00, } #[derive(Setters, Getters, New, Default)] #[repr(C, packed)] struct FuStructAverHidReqIspFileDnload { report_id_custom_command: u8 == 0x08, custom_cmd_isp: u8 == 0x10, custom_isp_cmd: u8, data: [u8; 508] = 0xFF, } #[derive(Setters, Getters, New, Default)] #[repr(C, packed)] struct FuStructAverHidReqIspFileEnd { report_id_custom_command: u8 == 0x08, custom_cmd_isp: u8 == 0x10, custom_isp_cmd: u8, file_name: [char; 51], end_flag: u8, file_size: u32le, free_space: u32le, _reserved: [u8; 448] = 0xFF, end: u8 == 0x00, } #[derive(Getters, Setters, Default)] #[repr(C, packed)] struct FuStructAverHidReqIspStart { report_id_custom_command: u8 == 0x08, custom_cmd_isp: u8 == 0x10, custom_isp_cmd: u8, isp_cmd: [u8; 60], _reserved: [u8; 448] = 0xFF, end: u8 == 0x00, } #[derive(Getters, New, Default)] #[repr(C, packed)] struct FuStructAverHidReqDeviceVersion { report_id_custom_command: u8 == 0x08, custom_cmd_isp: u8 == 0x25, ver: [u8; 11], _reserved: [u8; 498] = 0xFF, end: u8 == 0x00, } #[derive(New, Getters, Validate, Default)] #[repr(C, packed)] struct FuStructAverHidResIspStatus { report_id_custom_command: u8 == 0x09, custom_cmd_isp: u8 == 0x10, custom_isp_cmd: u8, status: u8, status_string: [char; 58], progress: u8, _reserved: [u8; 448] = 0xFF, end: u8 == 0x00, } #[derive(Getters, Setters, Default)] #[repr(C, packed)] struct FuStructAverHidResIsp { report_id_custom_command: u8 == 0x09, custom_cmd_isp: u8 == 0x10, custom_isp_cmd: u8, _reserved: [u8; 508] = 0xFF, end: u8 == 0x00, } #[derive(Getters, New, Validate, Default)] #[repr(C, packed)] struct FuStructAverHidResDeviceVersion { report_id_custom_command: u8 == 0x09, custom_cmd_isp: u8 == 0x25, ver: [u8; 11], _reserved: [u8; 498] = 0xFF, end: u8 == 0x00, } #[derive(Setters, Getters, New, Default)] #[repr(C, packed)] struct FuStructAverSafeispReq { report_id_custom_command: u8 == 0x08, custom_cmd: u8, custom_res: u16le, custom_parm0: u32le = 0x00, custom_parm1: u32le = 0x00, data: [u8; 1012] = 0x00, }; #[derive(New, Getters, Validate, Default)] #[repr(C, packed)] struct FuStructAverSafeispRes { report_id_custom_command: u8 == 0x09, custom_cmd: u8, custom_res: u16le, custom_parm0: u32le, custom_parm1: u32le, data: [u8; 4] = 0x00, }; #[derive(Getters, Validate, Default)] #[repr(C, packed)] struct FuStructAverSafeispResDeviceVersion { report_id_custom_command: u8 == 0x09, custom_cmd: u8 == 0x14, ver: [u8; 11], _reserved: [u8; 3] = 0x00, } fwupd-2.0.10/plugins/aver-hid/fu-aver-safeisp-device.c000066400000000000000000000333711501337203100224770ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-aver-hid-firmware.h" #include "fu-aver-hid-struct.h" #include "fu-aver-safeisp-device.h" struct _FuAverSafeispDevice { FuHidDevice parent_instance; }; G_DEFINE_TYPE(FuAverSafeispDevice, fu_aver_safeisp_device, FU_TYPE_HID_DEVICE) #define FU_AVER_SAFEISP_DEVICE_TIMEOUT 100000 /* ms */ #define FU_AVER_SAFEISP_DEVICE_POLL_INTERVAL 5000 /* ms */ typedef enum { ISP_CX3, ISP_M12 } FuAverSafeIspPartition; static gboolean fu_aver_safeisp_device_transfer(FuAverSafeispDevice *self, GByteArray *req, GByteArray *res, GError **error) { if (req != NULL) { if (!fu_hid_device_set_report(FU_HID_DEVICE(self), req->data[0], req->data, req->len, FU_AVER_SAFEISP_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error)) { g_prefix_error(error, "failed to send packet: "); return FALSE; } } if (res != NULL) { if (!fu_hid_device_get_report(FU_HID_DEVICE(self), res->data[0], res->data, res->len, FU_AVER_SAFEISP_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error)) { g_prefix_error(error, "failed to receive packet: "); return FALSE; } g_debug("custom-isp-cmd: %s [0x%x]", fu_aver_safeisp_custom_cmd_to_string( fu_struct_aver_safeisp_res_get_custom_cmd(res)), fu_struct_aver_safeisp_res_get_custom_cmd(res)); } return TRUE; } static gboolean fu_aver_safeisp_device_ensure_version(FuAverSafeispDevice *self, GError **error) { g_autofree gchar *ver = NULL; g_autoptr(GByteArray) req = fu_struct_aver_safeisp_req_new(); g_autoptr(GByteArray) res = fu_struct_aver_safeisp_res_new(); fu_struct_aver_safeisp_req_set_custom_cmd(req, FU_AVER_SAFEISP_CUSTOM_CMD_GET_VERSION); if (!fu_aver_safeisp_device_transfer(self, req, res, error)) return FALSE; if (!fu_struct_aver_safeisp_res_device_version_validate(res->data, res->len, 0x0, error)) return FALSE; ver = fu_strsafe((const gchar *)fu_struct_aver_safeisp_res_device_version_get_ver(res, NULL), FU_STRUCT_AVER_SAFEISP_RES_DEVICE_VERSION_SIZE_VER); fu_device_set_version(FU_DEVICE(self), ver); return TRUE; } static gboolean fu_aver_safeisp_device_setup(FuDevice *device, GError **error) { FuAverSafeispDevice *self = FU_AVER_SAFEISP_DEVICE(device); /* HidDevice->setup */ if (!FU_DEVICE_CLASS(fu_aver_safeisp_device_parent_class)->setup(device, error)) return FALSE; /* get the version from the hardware while open */ if (!fu_aver_safeisp_device_ensure_version(self, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_aver_safeisp_device_support(FuAverSafeispDevice *self, GError **error) { g_autoptr(GByteArray) req = fu_struct_aver_safeisp_req_new(); g_autoptr(GByteArray) res = fu_struct_aver_safeisp_res_new(); fu_struct_aver_safeisp_req_set_custom_cmd(req, FU_AVER_SAFEISP_CUSTOM_CMD_SUPPORT); if (!fu_aver_safeisp_device_transfer(self, req, res, error)) return FALSE; if (!fu_struct_aver_safeisp_res_validate(res->data, res->len, 0x0, error)) return FALSE; if (fu_struct_aver_safeisp_res_get_custom_cmd(res) != FU_AVER_SAFEISP_ACK_STATUS_SUPPORT) return FALSE; return TRUE; } static gboolean fu_aver_safeisp_device_upload_prepare(FuAverSafeispDevice *self, FuAverSafeIspPartition partition, gsize size, GError **error) { g_autoptr(GByteArray) req = fu_struct_aver_safeisp_req_new(); g_autoptr(GByteArray) res = fu_struct_aver_safeisp_res_new(); fu_struct_aver_safeisp_req_set_custom_cmd(req, FU_AVER_SAFEISP_CUSTOM_CMD_UPLOAD_PREPARE); fu_struct_aver_safeisp_req_set_custom_parm0(req, partition); fu_struct_aver_safeisp_req_set_custom_parm1(req, size); if (!fu_aver_safeisp_device_transfer(self, req, res, error)) return FALSE; if (!fu_struct_aver_safeisp_res_validate(res->data, res->len, 0x0, error)) return FALSE; return TRUE; } static gboolean fu_aver_safeisp_device_erase_flash(FuAverSafeispDevice *self, gsize param0, gsize param1, GError **error) { g_autoptr(GByteArray) req = fu_struct_aver_safeisp_req_new(); g_autoptr(GByteArray) res = fu_struct_aver_safeisp_res_new(); fu_struct_aver_safeisp_req_set_custom_cmd(req, FU_AVER_SAFEISP_CUSTOM_CMD_ERASE_TEMP); fu_struct_aver_safeisp_req_set_custom_parm0(req, param0); fu_struct_aver_safeisp_req_set_custom_parm1(req, param1); if (!fu_aver_safeisp_device_transfer(self, req, res, error)) return FALSE; if (!fu_struct_aver_safeisp_res_validate(res->data, res->len, 0x0, error)) return FALSE; return TRUE; } static gboolean fu_aver_safeisp_device_upload(FuAverSafeispDevice *self, FuChunkArray *chunks, FuProgress *progress, FuAverSafeIspPartition partition, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GByteArray) req = fu_struct_aver_safeisp_req_new(); g_autoptr(GByteArray) res = fu_struct_aver_safeisp_res_new(); /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; /* copy in payload */ if (partition == ISP_CX3) { fu_struct_aver_safeisp_req_set_custom_cmd( req, FU_AVER_SAFEISP_CUSTOM_CMD_UPLOAD_TO_CX3); } else if (partition == ISP_M12) { fu_struct_aver_safeisp_req_set_custom_cmd( req, FU_AVER_SAFEISP_CUSTOM_CMD_UPLOAD_TO_M12MO); } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid argument %u", partition); return FALSE; } fu_struct_aver_safeisp_req_set_custom_parm0(req, fu_chunk_get_address(chk)); fu_struct_aver_safeisp_req_set_custom_parm1(req, fu_chunk_get_data_sz(chk)); if (!fu_memcpy_safe(req->data, req->len, FU_STRUCT_AVER_SAFEISP_REQ_OFFSET_DATA, /* dst */ fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0x0, /* src */ fu_chunk_get_data_sz(chk), error)) return FALSE; /* resize the last packet */ if ((i == (fu_chunk_array_length(chunks) - 1)) && (fu_chunk_get_data_sz(chk) < 512)) { fu_byte_array_set_size(req, 12 + fu_chunk_get_data_sz(chk), 0x0); fu_struct_aver_safeisp_req_set_custom_parm1(req, fu_chunk_get_data_sz(chk)); } if (!fu_aver_safeisp_device_transfer(self, req, res, error)) return FALSE; if (!fu_struct_aver_safeisp_res_validate(res->data, res->len, 0x0, error)) return FALSE; /* update progress */ fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_aver_safeisp_device_upload_checksum(FuAverSafeispDevice *self, gsize param0, gsize param1, GError **error) { g_autoptr(GByteArray) req = fu_struct_aver_safeisp_req_new(); g_autoptr(GByteArray) res = fu_struct_aver_safeisp_res_new(); fu_struct_aver_safeisp_req_set_custom_cmd( req, FU_AVER_SAFEISP_CUSTOM_CMD_UPLOAD_COMPARE_CHECKSUM); fu_struct_aver_safeisp_req_set_custom_parm0(req, param0); fu_struct_aver_safeisp_req_set_custom_parm1(req, param1); if (!fu_aver_safeisp_device_transfer(self, req, res, error)) return FALSE; if (!fu_struct_aver_safeisp_res_validate(res->data, res->len, 0x0, error)) return FALSE; if (fu_struct_aver_safeisp_req_get_custom_cmd(res) != FU_AVER_SAFEISP_ACK_STATUS_SUCCESS) return FALSE; return TRUE; } static gboolean fu_aver_safeisp_device_update(FuAverSafeispDevice *self, gsize param0, gsize param1, GError **error) { g_autoptr(GByteArray) req = fu_struct_aver_safeisp_req_new(); fu_struct_aver_safeisp_req_set_custom_cmd(req, FU_AVER_SAFEISP_CUSTOM_CMD_UPDATE_START); fu_struct_aver_safeisp_req_set_custom_parm0(req, param0); fu_struct_aver_safeisp_req_set_custom_parm1(req, param1); if (!fu_aver_safeisp_device_transfer(self, req, NULL, error)) return FALSE; return TRUE; } static gboolean fu_aver_safeisp_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuAverSafeispDevice *self = FU_AVER_SAFEISP_DEVICE(device); gsize cx3_fw_size; gsize m12_fw_size; const guint8 *cx3_fw_buf; const guint8 *m12_fw_buf; guint32 cx3_checksum = 0; guint32 m12_checksum = 0; g_autoptr(FuArchive) archive = NULL; g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(GBytes) cx3_fw = NULL; g_autoptr(GBytes) m12_fw = NULL; g_autoptr(GInputStream) stream = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 5, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 58, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 34, NULL); /* get default image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* decompress */ archive = fu_archive_new_stream(stream, FU_ARCHIVE_FLAG_NONE, error); if (archive == NULL) return FALSE; cx3_fw = fu_archive_lookup_by_fn(archive, "update/cx3uvc.img", error); if (cx3_fw == NULL) return FALSE; m12_fw = fu_archive_lookup_by_fn(archive, "update/RS_M12MO.bin", error); if (m12_fw == NULL) return FALSE; /* CX3 fw file size should be less than 256KB */ cx3_fw_buf = g_bytes_get_data(cx3_fw, &cx3_fw_size); if (cx3_fw_size > 256 * 1024) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "cx3 file size is invalid: 0x%x", (guint)cx3_fw_size); return FALSE; } /* calculate CX3 firmware checksum */ cx3_checksum = fu_sum32(cx3_fw_buf, cx3_fw_size); /* M12 fw file size should be less than 3MB */ m12_fw_buf = g_bytes_get_data(m12_fw, &m12_fw_size); if (m12_fw_size > 3 * 1024 * 1024) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "m12 file size is invalid: 0x%x", (guint)m12_fw_size); return FALSE; } /* calculate M12 firmware checksum */ m12_checksum = fu_sum32(m12_fw_buf, m12_fw_size); /* check if the device supports safeisp */ if (!fu_aver_safeisp_device_support(self, error)) return FALSE; /* CX3 safeisp prepare */ if (!fu_aver_safeisp_device_upload_prepare(self, ISP_CX3, cx3_fw_size, error)) return FALSE; fu_progress_step_done(progress); /* CX3 safeisp erase flash */ if (!fu_aver_safeisp_device_erase_flash(self, ISP_CX3, 0x0, error)) return FALSE; /* CX3 safeisp firmware upload */ chunks = fu_chunk_array_new_from_bytes(cx3_fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 512); if (!fu_aver_safeisp_device_upload(self, chunks, fu_progress_get_child(progress), ISP_CX3, error)) return FALSE; fu_progress_step_done(progress); /* CX3 safeisp checksum */ if (!fu_aver_safeisp_device_upload_checksum(self, ISP_CX3, cx3_checksum, error)) return FALSE; fu_progress_step_done(progress); /* M12 safeisp prepare */ if (!fu_aver_safeisp_device_upload_prepare(self, ISP_M12, m12_fw_size, error)) return FALSE; fu_progress_step_done(progress); /* M12 safeisp erase flash */ if (!fu_aver_safeisp_device_erase_flash(self, ISP_M12, 0x0, error)) return FALSE; /* M12 safeisp firmware upload */ chunks = fu_chunk_array_new_from_bytes(m12_fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 512); if (!fu_aver_safeisp_device_upload(self, chunks, fu_progress_get_child(progress), ISP_M12, error)) return FALSE; fu_progress_step_done(progress); /* M12 safeisp checksum */ if (!fu_aver_safeisp_device_upload_checksum(self, ISP_M12, m12_checksum, error)) return FALSE; fu_progress_step_done(progress); /* update device */ if (!fu_aver_safeisp_device_update(self, ((1 << ISP_CX3) | (1 << ISP_M12)), 0x0, error)) return FALSE; fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* success! */ return TRUE; } static void fu_aver_safeisp_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 68, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 31, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reload"); } static void fu_aver_safeisp_device_init(FuAverSafeispDevice *self) { fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); fu_device_add_protocol(FU_DEVICE(self), "com.aver.safeisp"); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_AVER_HID_FIRMWARE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SELF_RECOVERY); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_AUTO_PAUSE_POLLING); fu_device_set_remove_delay(FU_DEVICE(self), 150000); fu_hid_device_add_flag(FU_HID_DEVICE(self), FU_HID_DEVICE_FLAG_RETRY_FAILURE); fu_hid_device_add_flag(FU_HID_DEVICE(self), FU_HID_DEVICE_FLAG_AUTODETECT_EPS); } static void fu_aver_safeisp_device_class_init(FuAverSafeispDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_aver_safeisp_device_setup; device_class->write_firmware = fu_aver_safeisp_device_write_firmware; device_class->set_progress = fu_aver_safeisp_device_set_progress; } fwupd-2.0.10/plugins/aver-hid/fu-aver-safeisp-device.h000066400000000000000000000005421501337203100224760ustar00rootroot00000000000000/* * Copyright 2024 Pierce Wang * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_AVER_SAFEISP_DEVICE (fu_aver_safeisp_device_get_type()) G_DECLARE_FINAL_TYPE(FuAverSafeispDevice, fu_aver_safeisp_device, FU, AVER_SAFEISP_DEVICE, FuHidDevice) fwupd-2.0.10/plugins/aver-hid/meson.build000066400000000000000000000010411501337203100202300ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginAverHid"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('aver-hid.quirk') plugin_builtins += static_library('fu_plugin_aver_hid', rustgen.process('fu-aver-hid.rs'), sources: [ 'fu-aver-hid-device.c', 'fu-aver-safeisp-device.c', 'fu-aver-hid-firmware.c', 'fu-aver-hid-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files( 'tests/aver-fone540.json', ) fwupd-2.0.10/plugins/aver-hid/test/000077500000000000000000000000001501337203100170515ustar00rootroot00000000000000fwupd-2.0.10/plugins/aver-hid/test/firmware.metainfo.xml000066400000000000000000000024331501337203100232120ustar00rootroot00000000000000 com.aver.FONE540.firmware FONE540 System firmware for AVer FONE540

    AVer Fone540 can be updated using fwupd with plugin aver-hid.

    audio-card a5dcbf10-6530-11d2-901f-00c04fb951ed https://www.aver.com/ CC0-1.0 LicenseRef-proprietary AVer Information Inc. X-Device

    This release updates firmware to 0.0.7004.02

    org.freedesktop.fwupd plain com.aver.hid
    fwupd-2.0.10/plugins/aver-hid/tests/000077500000000000000000000000001501337203100172345ustar00rootroot00000000000000fwupd-2.0.10/plugins/aver-hid/tests/aver-fone540.json000066400000000000000000000015141501337203100222430ustar00rootroot00000000000000{ "name": "AVER FONE540", "interactive": false, "steps": [ { "url": "3dca7723feba4f86d6cbd1d3cb39e0d2524d59bf03e3c904465edc88ca8254c6-0.0.7004.09.cab", "emulation-url": "bbe19a7f6ad1025fc9da37c71c2a155884403876c5bc0c4ef64174319f91f398-0.0.7004.09.zip", "components": [ { "version": "0.0.7004.09", "guids": [ "2a1b9a80-0319-504d-841c-c7dca4f7cbf1" ] } ] }, { "url": "e16c8a7a9dd19921ace461334dcc3148c52edf95145a3047db7cc872113f82c8-0.0.7106.12.cab", "emulation-url": "d93a8cdaa112ad2fa43da573aa92938bc261220b97c80b99b2f08aedff8ca959-0.0.7106.12.zip", "components": [ { "version": "0.0.7106.12", "guids": [ "2a1b9a80-0319-504d-841c-c7dca4f7cbf1" ] } ] } ] } fwupd-2.0.10/plugins/bcm57xx/000077500000000000000000000000001501337203100156705ustar00rootroot00000000000000fwupd-2.0.10/plugins/bcm57xx/README.md000066400000000000000000000026701501337203100171540ustar00rootroot00000000000000--- title: Plugin: BCM57xx --- ## Introduction This plugin updates BCM57xx wired network adaptors from Broadcom using a reverse-engineered flashing protocol. It is designed to be used with the clean-room reimplementation of the BCM5719 firmware found here: ## Protocol BCM57xx devices support a custom `com.broadcom.bcm57xx` protocol which is implemented as ioctls like ethtool does. ## GUID Generation These devices use the standard PCI instance IDs, for example: * `PCI\VEN_14E4&DEV_1657` * `PCI\VEN_14E4&DEV_1657&SUBSYS_17AA222E` ## Update Behavior The device usually presents in runtime mode, and the firmware is written to the device without disconnecting the working kernel driver. Once complete the APE is reset which may cause a brief link reconnection. On flash failure the device is nonfunctional, but is recoverable using direct BAR writes, which is typically much slower than updating the device using the kernel driver and the ethtool API. ## Vendor ID Security The vendor ID is set from the PCI vendor, in this instance set to `PCI:0x14E4` ## External Interface Access This plugin requires the `SIOCETHTOOL` ioctl interface. ## Version Considerations This plugin has been available since fwupd version `1.5.0`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Evan Lojewski: @meklort fwupd-2.0.10/plugins/bcm57xx/bcm57xx.quirk000066400000000000000000000004121501337203100202370ustar00rootroot00000000000000# Broadcom BCM5719 [PCI\VEN_14E4&DEV_1657] Plugin = bcm57xx # OEM PCI cards [PCI\VEN_14E4&DEV_1657&SUBSYS_14E41904] FirmwareSize = 0x80000 [PCI\VEN_14E4&DEV_1657&SUBSYS_94E41904] FirmwareSize = 0x80000 [PCI\VEN_14E4&DEV_1657&SUBSYS_17AA402D] FirmwareSize = 0x80000 fwupd-2.0.10/plugins/bcm57xx/fu-bcm57xx-common.c000066400000000000000000000056211501337203100212330ustar00rootroot00000000000000/* * Copyright 2018 Evan Lojewski * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-bcm57xx-common.h" gboolean fu_bcm57xx_verify_crc(GInputStream *stream, GError **error) { guint32 crc_actual = 0xFFFFFFFF; guint32 crc_file = 0; gsize streamsz = 0; g_autoptr(GInputStream) stream_tmp = NULL; /* expected */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz < sizeof(guint32)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "image is too small for CRC"); return FALSE; } if (!fu_input_stream_read_u32(stream, streamsz - sizeof(guint32), &crc_file, G_LITTLE_ENDIAN, error)) return FALSE; /* reality */ stream_tmp = fu_partial_input_stream_new(stream, 0, streamsz - sizeof(guint32), error); if (stream_tmp == NULL) return FALSE; if (!fu_input_stream_compute_crc32(stream_tmp, FU_CRC_KIND_B32_STANDARD, &crc_actual, error)) return FALSE; if (crc_actual != crc_file) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid CRC, expected 0x%08x got: 0x%08x", (guint)crc_file, (guint)crc_actual); return FALSE; } /* success */ return TRUE; } gboolean fu_bcm57xx_verify_magic(GInputStream *stream, gsize offset, GError **error) { guint32 magic = 0; /* hardcoded value */ if (!fu_input_stream_read_u32(stream, offset, &magic, G_BIG_ENDIAN, error)) return FALSE; if (magic != BCM_NVRAM_MAGIC) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid magic, got: 0x%x", (guint)magic); return FALSE; } /* success */ return TRUE; } void fu_bcm57xx_veritem_free(Bcm57xxVeritem *veritem) { g_free(veritem->branch); g_free(veritem->version); g_free(veritem); } Bcm57xxVeritem * fu_bcm57xx_veritem_new(const guint8 *buf, gsize bufsz) { g_autofree gchar *tmp = NULL; g_autoptr(Bcm57xxVeritem) veritem = g_new0(Bcm57xxVeritem, 1); struct { const gchar *prefix; const gchar *branch; FwupdVersionFormat verfmt; } data[] = {{"5719-v", BCM_FW_BRANCH_UNKNOWN, FWUPD_VERSION_FORMAT_PAIR}, {"stage1-", BCM_FW_BRANCH_OSS_FIRMWARE, FWUPD_VERSION_FORMAT_TRIPLET}, {NULL, NULL, 0}}; /* do not assume this is NUL terminated */ tmp = g_strndup((const gchar *)buf, bufsz); if (tmp == NULL || tmp[0] == '\0') return NULL; /* use prefix to define object */ for (guint i = 0; data[i].prefix != NULL; i++) { if (g_str_has_prefix(tmp, data[i].prefix)) { veritem->version = g_strdup(tmp + strlen(data[i].prefix)); veritem->branch = g_strdup(data[i].branch); veritem->verfmt = data[i].verfmt; return g_steal_pointer(&veritem); } } veritem->verfmt = FWUPD_VERSION_FORMAT_UNKNOWN; veritem->version = g_strdup(tmp); return g_steal_pointer(&veritem); } fwupd-2.0.10/plugins/bcm57xx/fu-bcm57xx-common.h000066400000000000000000000034401501337203100212350ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define BCM_VENDOR_BROADCOM 0x14E4 #define BCM_FW_BRANCH_UNKNOWN NULL #define BCM_FW_BRANCH_OSS_FIRMWARE "oss-firmware" #define BCM_FIRMWARE_SIZE 0x40000 /* x2 for Dell */ #define BCM_PHYS_ADDR_DEFAULT 0x08003800 #define BCM_NVRAM_MAGIC 0x669955AA /* offsets into NVMRAM */ #define BCM_NVRAM_HEADER_BASE 0x00 #define BCM_NVRAM_DIRECTORY_BASE 0x14 #define BCM_NVRAM_INFO_BASE 0x74 #define BCM_NVRAM_VPD_BASE 0x100 #define BCM_NVRAM_INFO2_BASE 0x200 #define BCM_NVRAM_STAGE1_BASE 0x28c #define BCM_NVRAM_HEADER_MAGIC 0x00 #define BCM_NVRAM_HEADER_PHYS_ADDR 0x04 #define BCM_NVRAM_HEADER_SIZE_WRDS 0x08 #define BCM_NVRAM_HEADER_OFFSET 0x0C #define BCM_NVRAM_HEADER_CRC 0x10 #define BCM_NVRAM_HEADER_SZ 0x14 #define BCM_NVRAM_INFO_MAC_ADDR0 0x00 #define BCM_NVRAM_INFO_VENDOR 0x2E #define BCM_NVRAM_INFO_DEVICE 0x2C #define BCM_NVRAM_INFO_SZ 0x8C #define BCM_NVRAM_DIRECTORY_ADDR 0x00 #define BCM_NVRAM_DIRECTORY_SIZE_WRDS 0x04 #define BCM_NVRAM_DIRECTORY_OFFSET 0x08 #define BCM_NVRAM_DIRECTORY_SZ 0x0c #define BCM_NVRAM_VPD_SZ 0x100 #define BCM_NVRAM_INFO2_SZ 0x8c #define BCM_NVRAM_STAGE1_VERADDR 0x08 #define BCM_NVRAM_STAGE1_VERSION 0x0C typedef struct { gchar *branch; gchar *version; FwupdVersionFormat verfmt; } Bcm57xxVeritem; gboolean fu_bcm57xx_verify_crc(GInputStream *stream, GError **error); gboolean fu_bcm57xx_verify_magic(GInputStream *stream, gsize offset, GError **error); /* parses stage1 version */ void fu_bcm57xx_veritem_free(Bcm57xxVeritem *veritem); Bcm57xxVeritem * fu_bcm57xx_veritem_new(const guint8 *buf, gsize bufsz); G_DEFINE_AUTOPTR_CLEANUP_FUNC(Bcm57xxVeritem, fu_bcm57xx_veritem_free) fwupd-2.0.10/plugins/bcm57xx/fu-bcm57xx-device.c000066400000000000000000000504451501337203100212060ustar00rootroot00000000000000/* * Copyright 2018 Evan Lojewski * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #ifdef HAVE_ERRNO_H #include #endif #include #include #ifdef HAVE_ETHTOOL_H #include #include #include #endif #ifdef HAVE_SOCKET_H #include #endif #include "fu-bcm57xx-common.h" #include "fu-bcm57xx-device.h" #include "fu-bcm57xx-dict-image.h" #include "fu-bcm57xx-firmware.h" #include "fu-bcm57xx-recovery-device.h" #define FU_BCM57XX_BLOCK_SZ 0x4000 /* 16kb */ struct _FuBcm57xxDevice { FuPciDevice parent_instance; gchar *ethtool_iface; }; G_DEFINE_TYPE(FuBcm57xxDevice, fu_bcm57xx_device, FU_TYPE_PCI_DEVICE) enum { PROP_0, PROP_IFACE, PROP_LAST }; static void fu_bcm57xx_device_to_string(FuDevice *device, guint idt, GString *str) { FuBcm57xxDevice *self = FU_BCM57XX_DEVICE(device); fwupd_codec_string_append(str, idt, "EthtoolIface", self->ethtool_iface); } static gboolean fu_bcm57xx_device_probe(FuDevice *device, GError **error) { return fu_udev_device_set_physical_id(FU_UDEV_DEVICE(device), "pci", error); } #ifdef HAVE_ETHTOOL_H static gboolean fu_bcm57xx_device_ioctl_buffer_cb(FuIoctl *self, gpointer ptr, guint8 *buf, gsize bufsz, GError **error) { struct ifreq *ifr = (struct ifreq *)ptr; ifr->ifr_data = (char *)buf; return TRUE; } static gboolean fu_bcm57xx_device_submit_ifreq(FuBcm57xxDevice *self, guint8 *buf, gsize bufsz, GError **error) { struct ifreq ifr = {0}; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); g_return_val_if_fail(buf != NULL, FALSE); /* include these when generating the emulation event */ strncpy(ifr.ifr_name, self->ethtool_iface, IFNAMSIZ - 1); fu_ioctl_add_key_as_u16(ioctl, "Request", SIOCETHTOOL); fu_ioctl_add_mutable_buffer(ioctl, NULL, buf, bufsz, fu_bcm57xx_device_ioctl_buffer_cb); if (!fu_ioctl_execute(ioctl, SIOCETHTOOL, (guint8 *)&ifr, sizeof(ifr), NULL, 500, /* ms */ FU_IOCTL_FLAG_NONE, error)) { g_prefix_error(error, "failed to SIOCETHTOOL: "); return FALSE; } /* success */ return TRUE; } #endif static gboolean fu_bcm57xx_device_nvram_write(FuBcm57xxDevice *self, guint32 address, const guint8 *buf, gsize bufsz, GError **error) { #ifdef HAVE_ETHTOOL_H gsize eepromsz; g_autofree struct ethtool_eeprom *eeprom = NULL; /* failed to load tg3 */ if (self->ethtool_iface == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Not supported as ethtool interface disabled"); return FALSE; } /* sanity check */ if (address + bufsz > fu_device_get_firmware_size_max(FU_DEVICE(self))) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "tried to read outside of EEPROM size [0x%x]", (guint)fu_device_get_firmware_size_max(FU_DEVICE(self))); return FALSE; } /* write EEPROM (NVRAM) data */ eepromsz = sizeof(struct ethtool_eeprom) + bufsz; eeprom = (struct ethtool_eeprom *)g_malloc0(eepromsz); eeprom->cmd = ETHTOOL_SEEPROM; eeprom->magic = BCM_NVRAM_MAGIC; eeprom->len = bufsz; eeprom->offset = address; memcpy(eeprom->data, buf, eeprom->len); /* nocheck:blocked */ if (!fu_bcm57xx_device_submit_ifreq(FU_BCM57XX_DEVICE(self), (guint8 *)eeprom, eepromsz, error)) { g_prefix_error(error, "cannot write eeprom: "); return FALSE; } /* success */ return TRUE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Not supported as not found"); return FALSE; #endif } static gboolean fu_bcm57xx_device_nvram_read(FuBcm57xxDevice *self, guint32 address, guint8 *buf, gsize bufsz, GError **error) { #ifdef HAVE_ETHTOOL_H gsize eepromsz; g_autofree struct ethtool_eeprom *eeprom = NULL; /* failed to load tg3 */ if (self->ethtool_iface == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Not supported as ethtool interface disabled"); return FALSE; } /* sanity check */ if (address + bufsz > fu_device_get_firmware_size_max(FU_DEVICE(self))) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "tried to read outside of EEPROM size [0x%x]", (guint)fu_device_get_firmware_size_max(FU_DEVICE(self))); return FALSE; } /* read EEPROM (NVRAM) data */ eepromsz = sizeof(struct ethtool_eeprom) + bufsz; eeprom = (struct ethtool_eeprom *)g_malloc0(eepromsz); eeprom->cmd = ETHTOOL_GEEPROM; eeprom->len = bufsz; eeprom->offset = address; if (!fu_bcm57xx_device_submit_ifreq(FU_BCM57XX_DEVICE(self), (guint8 *)eeprom, eepromsz, error)) { g_prefix_error(error, "cannot read eeprom: "); return FALSE; } /* copy back data */ if (!fu_memcpy_safe(buf, bufsz, 0x0, /* dst */ (guint8 *)eeprom, eepromsz, /* src */ G_STRUCT_OFFSET(struct ethtool_eeprom, data), bufsz, error)) return FALSE; /* success */ return TRUE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Not supported as not found"); return FALSE; #endif } static gboolean fu_bcm57xx_device_nvram_check(FuBcm57xxDevice *self, GError **error) { #ifdef HAVE_ETHTOOL_H struct ethtool_drvinfo drvinfo = {.cmd = ETHTOOL_GDRVINFO}; /* failed to load tg3 */ if (self->ethtool_iface == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Not supported as ethtool interface disabled"); return FALSE; } /* get driver info */ if (!fu_bcm57xx_device_submit_ifreq(FU_BCM57XX_DEVICE(self), (guint8 *)&drvinfo, sizeof(drvinfo), error)) { g_prefix_error(error, "cannot get driver information: "); return FALSE; } g_debug("FW version %s", drvinfo.fw_version); /* detect more OEM cards */ if (drvinfo.eedump_len == fu_device_get_firmware_size_max(FU_DEVICE(self)) * 2) { g_autofree gchar *subsys = g_strdup_printf("%04X%04X", fu_pci_device_get_subsystem_vid(FU_PCI_DEVICE(self)), fu_pci_device_get_subsystem_pid(FU_PCI_DEVICE(self))); g_debug("auto-sizing expected EEPROM size for OEM SUBSYS %s", subsys); fu_device_set_firmware_size(FU_DEVICE(self), drvinfo.eedump_len); } else if (drvinfo.eedump_len != fu_device_get_firmware_size_max(FU_DEVICE(self))) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "EEPROM size invalid, got 0x%x, expected 0x%x", drvinfo.eedump_len, (guint)fu_device_get_firmware_size_max(FU_DEVICE(self))); return FALSE; } /* success */ return TRUE; #else g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Not supported as not found"); return FALSE; #endif } static GBytes * fu_bcm57xx_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuBcm57xxDevice *self = FU_BCM57XX_DEVICE(device); const gsize bufsz = fu_device_get_firmware_size_max(FU_DEVICE(self)); g_autofree guint8 *buf = g_malloc0(bufsz); g_autoptr(GPtrArray) chunks = NULL; chunks = fu_chunk_array_mutable_new(buf, bufsz, 0x0, 0x0, FU_BCM57XX_BLOCK_SZ); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_bcm57xx_device_nvram_read(self, fu_chunk_get_address(chk), fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), error)) return NULL; fu_progress_step_done(progress); } /* read from hardware */ return g_bytes_new_take(g_steal_pointer(&buf), bufsz); } static FuFirmware * fu_bcm57xx_device_read_firmware(FuDevice *device, FuProgress *progress, GError **error) { g_autoptr(FuFirmware) firmware = fu_bcm57xx_firmware_new(); g_autoptr(GBytes) fw = NULL; /* read from hardware */ fw = fu_bcm57xx_device_dump_firmware(device, progress, error); if (fw == NULL) return NULL; if (!fu_firmware_parse_bytes(firmware, fw, 0x0, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error)) return NULL; /* remove images that will contain user-data */ if (!fu_firmware_remove_image_by_id(firmware, "info", error)) return NULL; if (!fu_firmware_remove_image_by_id(firmware, "info2", error)) return NULL; if (!fu_firmware_remove_image_by_id(firmware, "vpd", error)) return NULL; return g_steal_pointer(&firmware); } static FuFirmware * fu_bcm57xx_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { guint dict_cnt = 0; g_autofree gchar *str_existing = NULL; g_autofree gchar *str_proposed = NULL; g_autoptr(GBytes) fw_old = NULL; g_autoptr(FuFirmware) firmware = fu_bcm57xx_firmware_new(); g_autoptr(FuFirmware) firmware_tmp = fu_bcm57xx_firmware_new(); g_autoptr(FuFirmware) img_ape = NULL; g_autoptr(FuFirmware) img_stage1 = NULL; g_autoptr(FuFirmware) img_stage2 = NULL; g_autoptr(GPtrArray) images = NULL; /* try to parse NVRAM, stage1 or APE */ if (!fu_firmware_parse_stream(firmware_tmp, stream, 0x0, flags, error)) { g_prefix_error(error, "failed to parse new firmware: "); return NULL; } /* for full NVRAM image, verify if correct device */ if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_VID_PID) == 0) { guint16 vid = fu_bcm57xx_firmware_get_vendor(FU_BCM57XX_FIRMWARE(firmware_tmp)); guint16 did = fu_bcm57xx_firmware_get_model(FU_BCM57XX_FIRMWARE(firmware_tmp)); if (vid != 0x0 && did != 0x0 && (fu_device_get_vid(device) != vid || fu_device_get_pid(device) != did)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "PCI vendor or model incorrect, " "got: %04X:%04X expected %04X:%04X", vid, did, fu_device_get_vid(device), fu_device_get_pid(device)); return NULL; } } /* get the existing firmware from the device */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ); fw_old = fu_bcm57xx_device_dump_firmware(device, progress, error); if (fw_old == NULL) return NULL; if (!fu_firmware_parse_bytes(firmware, fw_old, 0x0, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error)) { g_prefix_error(error, "failed to parse existing firmware: "); return NULL; } str_existing = fu_firmware_to_string(firmware); g_info("existing device firmware: %s", str_existing); /* merge in all the provided images into the existing firmware */ img_stage1 = fu_firmware_get_image_by_id(firmware_tmp, "stage1", NULL); if (img_stage1 != NULL) fu_firmware_add_image(firmware, img_stage1); img_stage2 = fu_firmware_get_image_by_id(firmware_tmp, "stage2", NULL); if (img_stage2 != NULL) fu_firmware_add_image(firmware, img_stage2); img_ape = fu_firmware_get_image_by_id(firmware_tmp, "ape", NULL); if (img_ape != NULL) fu_firmware_add_image(firmware, img_ape); /* the src and dst dictionaries may be in different order */ images = fu_firmware_get_images(firmware); for (guint i = 0; i < images->len; i++) { FuFirmware *img = g_ptr_array_index(images, i); if (FU_IS_BCM57XX_DICT_IMAGE(img)) { fu_firmware_set_idx(img, 0x80 + dict_cnt); dict_cnt++; } } str_proposed = fu_firmware_to_string(firmware); g_info("proposed device firmware: %s", str_proposed); /* success */ return g_steal_pointer(&firmware); } static gboolean fu_bcm57xx_device_write_chunks(FuBcm57xxDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_bcm57xx_device_nvram_write(self, fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; fu_progress_step_done(progress); } return TRUE; } static gboolean fu_bcm57xx_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuBcm57xxDevice *self = FU_BCM57XX_DEVICE(device); g_autoptr(GBytes) blob = NULL; g_autoptr(GBytes) blob_verify = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "build-img"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 80, "write-chunks"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 19, NULL); /* build the images into one linear blob of the correct size */ blob = fu_firmware_write(firmware, error); if (blob == NULL) return FALSE; fu_progress_step_done(progress); /* hit hardware */ chunks = fu_chunk_array_new_from_bytes(blob, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_BCM57XX_BLOCK_SZ); if (!fu_bcm57xx_device_write_chunks(self, chunks, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* verify */ blob_verify = fu_bcm57xx_device_dump_firmware(device, fu_progress_get_child(progress), error); if (blob_verify == NULL) return FALSE; if (!fu_bytes_compare(blob, blob_verify, error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_bcm57xx_device_attach(FuDevice *device, FuProgress *progress, GError **error) { g_autoptr(FwupdRequest) request = fwupd_request_new(); /* APE reset cannot be done at runtime */ fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_POST); fwupd_request_add_flag(request, FWUPD_REQUEST_FLAG_NON_GENERIC_MESSAGE); fwupd_request_set_message(request, "After shutting down, disconnect the computer from all " "power sources for 30 seconds to complete the update."); if (!fu_device_emit_request(device, request, progress, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_bcm57xx_device_setup(FuDevice *device, GError **error) { FuBcm57xxDevice *self = FU_BCM57XX_DEVICE(device); guint32 fwversion = 0; /* check the EEPROM size */ if (!fu_bcm57xx_device_nvram_check(self, error)) return FALSE; /* get NVRAM version */ if (!fu_bcm57xx_device_nvram_read(self, BCM_NVRAM_STAGE1_BASE + BCM_NVRAM_STAGE1_VERSION, (guint8 *)&fwversion, sizeof(guint32), error)) return FALSE; if (fwversion != 0x0) { /* this is only set on the OSS firmware */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version_raw(device, GUINT32_FROM_BE(fwversion)); /* nocheck:blocked */ fu_device_set_branch(device, BCM_FW_BRANCH_OSS_FIRMWARE); } else { guint8 bufver[16] = {0x0}; guint32 veraddr = 0; g_autoptr(Bcm57xxVeritem) veritem = NULL; /* fall back to the string, e.g. '5719-v1.43' */ if (!fu_bcm57xx_device_nvram_read(self, BCM_NVRAM_STAGE1_BASE + BCM_NVRAM_STAGE1_VERADDR, (guint8 *)&veraddr, sizeof(guint32), error)) return FALSE; veraddr = GUINT32_FROM_BE(veraddr); /* nocheck:blocked */ if (veraddr > BCM_PHYS_ADDR_DEFAULT) veraddr -= BCM_PHYS_ADDR_DEFAULT; if (!fu_bcm57xx_device_nvram_read(self, BCM_NVRAM_STAGE1_BASE + veraddr, bufver, sizeof(bufver), error)) return FALSE; veritem = fu_bcm57xx_veritem_new(bufver, sizeof(bufver)); if (veritem != NULL) { fu_device_set_version_format(device, veritem->verfmt); fu_device_set_version(device, veritem->version); /* nocheck:set-version */ fu_device_set_branch(device, veritem->branch); } } /* success */ fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_REBOOT); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_BACKUP_BEFORE_INSTALL); return TRUE; } static gboolean fu_bcm57xx_device_open(FuDevice *device, GError **error) { #ifdef HAVE_SOCKET_H FuBcm57xxDevice *self = FU_BCM57XX_DEVICE(device); gint fd; g_autoptr(FuIOChannel) io_channel = NULL; fd = socket(AF_INET, SOCK_DGRAM, 0); if (fd < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to open socket: %s", #ifdef HAVE_ERRNO_H g_strerror(errno)); #else "unspecified error"); #endif return FALSE; } io_channel = fu_io_channel_unix_new(fd); fu_udev_device_set_io_channel(FU_UDEV_DEVICE(self), io_channel); return TRUE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "socket() not supported as sys/socket.h not available"); return FALSE; #endif } static void fu_bcm57xx_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 98, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static gchar * fu_bcm57xx_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_device_get_version_format(device)); } static void fu_bcm57xx_device_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuBcm57xxDevice *self = FU_BCM57XX_DEVICE(object); switch (prop_id) { case PROP_IFACE: g_value_set_string(value, self->ethtool_iface); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_bcm57xx_device_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuBcm57xxDevice *self = FU_BCM57XX_DEVICE(object); switch (prop_id) { case PROP_IFACE: g_free(self->ethtool_iface); self->ethtool_iface = g_value_dup_string(value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_bcm57xx_device_init(FuBcm57xxDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN); fu_device_add_request_flag(FU_DEVICE(self), FWUPD_REQUEST_FLAG_NON_GENERIC_MESSAGE); fu_device_add_protocol(FU_DEVICE(self), "com.broadcom.bcm57xx"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_NETWORK_WIRED); /* other values are set from a quirk */ fu_device_set_firmware_size(FU_DEVICE(self), BCM_FIRMWARE_SIZE); } static void fu_bcm57xx_device_finalize(GObject *object) { FuBcm57xxDevice *self = FU_BCM57XX_DEVICE(object); g_free(self->ethtool_iface); G_OBJECT_CLASS(fu_bcm57xx_device_parent_class)->finalize(object); } static void fu_bcm57xx_device_class_init(FuBcm57xxDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); GParamSpec *pspec; FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->get_property = fu_bcm57xx_device_get_property; object_class->set_property = fu_bcm57xx_device_set_property; object_class->finalize = fu_bcm57xx_device_finalize; device_class->prepare_firmware = fu_bcm57xx_device_prepare_firmware; device_class->setup = fu_bcm57xx_device_setup; device_class->reload = fu_bcm57xx_device_setup; device_class->open = fu_bcm57xx_device_open; device_class->write_firmware = fu_bcm57xx_device_write_firmware; device_class->attach = fu_bcm57xx_device_attach; device_class->read_firmware = fu_bcm57xx_device_read_firmware; device_class->dump_firmware = fu_bcm57xx_device_dump_firmware; device_class->probe = fu_bcm57xx_device_probe; device_class->to_string = fu_bcm57xx_device_to_string; device_class->set_progress = fu_bcm57xx_device_set_progress; device_class->convert_version = fu_bcm57xx_device_convert_version; pspec = g_param_spec_string("iface", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_IFACE, pspec); } fwupd-2.0.10/plugins/bcm57xx/fu-bcm57xx-device.h000066400000000000000000000004601501337203100212030ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_BCM57XX_DEVICE (fu_bcm57xx_device_get_type()) G_DECLARE_FINAL_TYPE(FuBcm57xxDevice, fu_bcm57xx_device, FU, BCM57XX_DEVICE, FuPciDevice) fwupd-2.0.10/plugins/bcm57xx/fu-bcm57xx-dict-image.c000066400000000000000000000110251501337203100217410ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-bcm57xx-common.h" #include "fu-bcm57xx-dict-image.h" struct _FuBcm57xxDictImage { FuFirmware parent_instance; guint8 target; guint8 kind; }; G_DEFINE_TYPE(FuBcm57xxDictImage, fu_bcm57xx_dict_image, FU_TYPE_FIRMWARE) static void fu_bcm57xx_dict_image_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuBcm57xxDictImage *self = FU_BCM57XX_DICT_IMAGE(firmware); if (self->target != 0xff) fu_xmlb_builder_insert_kx(bn, "target", self->target); if (self->kind != 0xff) fu_xmlb_builder_insert_kx(bn, "kind", self->kind); } static gboolean fu_bcm57xx_dict_image_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { g_autoptr(GInputStream) stream_nocrc = NULL; gsize streamsz = 0; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz < sizeof(guint32)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "dict image is too small"); return FALSE; } if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { if (!fu_bcm57xx_verify_crc(stream, error)) return FALSE; } stream_nocrc = fu_partial_input_stream_new(stream, 0x0, streamsz - sizeof(guint32), error); if (stream_nocrc == NULL) return FALSE; return fu_firmware_set_stream(firmware, stream_nocrc, error); } static GByteArray * fu_bcm57xx_dict_image_write(FuFirmware *firmware, GError **error) { guint32 crc; g_autoptr(GByteArray) blob = NULL; g_autoptr(GBytes) fw_nocrc = NULL; /* get the CRC-less data */ fw_nocrc = fu_firmware_get_bytes(firmware, error); if (fw_nocrc == NULL) return NULL; /* add to a mutable buffer */ blob = g_byte_array_sized_new(g_bytes_get_size(fw_nocrc) + sizeof(guint32)); fu_byte_array_append_bytes(blob, fw_nocrc); /* add CRC */ crc = fu_crc32_bytes(FU_CRC_KIND_B32_STANDARD, fw_nocrc); fu_byte_array_append_uint32(blob, crc, G_LITTLE_ENDIAN); return g_steal_pointer(&blob); } static gboolean fu_bcm57xx_dict_image_build(FuFirmware *firmware, XbNode *n, GError **error) { FuBcm57xxDictImage *self = FU_BCM57XX_DICT_IMAGE(firmware); guint64 tmp; /* two simple properties */ tmp = xb_node_query_text_as_uint(n, "kind", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT8) fu_bcm57xx_dict_image_set_kind(self, tmp); tmp = xb_node_query_text_as_uint(n, "target", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT8) fu_bcm57xx_dict_image_set_target(self, tmp); /* success */ return TRUE; } static void fu_bcm57xx_dict_image_ensure_id(FuBcm57xxDictImage *self) { g_autofree gchar *id = NULL; struct { guint8 target; guint8 kind; const gchar *id; } ids[] = {{0x00, 0x00, "pxe"}, {0x0D, 0x00, "ape"}, {0x09, 0x00, "iscsi1"}, {0x05, 0x00, "iscsi2"}, {0x0b, 0x00, "iscsi3"}, {0x00, 0x01, "cfg1000"}, {0x04, 0x01, "vpd2"}, {0xff, 0xff, NULL}}; if (self->target == 0xff || self->kind == 0xff) return; for (guint i = 0; ids[i].id != NULL; i++) { if (self->target == ids[i].target && self->kind == ids[i].kind) { g_debug("using %s for %02x:%02x", ids[i].id, self->target, self->kind); fu_firmware_set_id(FU_FIRMWARE(self), ids[i].id); return; } } id = g_strdup_printf("dict-%02x-%02x", self->target, self->kind); if (g_getenv("FWUPD_FUZZER_RUNNING") == NULL) g_warning("falling back to %s, please report", id); fu_firmware_set_id(FU_FIRMWARE(self), id); } void fu_bcm57xx_dict_image_set_target(FuBcm57xxDictImage *self, guint8 target) { self->target = target; fu_bcm57xx_dict_image_ensure_id(self); } guint8 fu_bcm57xx_dict_image_get_target(FuBcm57xxDictImage *self) { return self->target; } void fu_bcm57xx_dict_image_set_kind(FuBcm57xxDictImage *self, guint8 kind) { self->kind = kind; fu_bcm57xx_dict_image_ensure_id(self); } guint8 fu_bcm57xx_dict_image_get_kind(FuBcm57xxDictImage *self) { return self->kind; } static void fu_bcm57xx_dict_image_init(FuBcm57xxDictImage *self) { self->target = 0xff; self->kind = 0xff; } static void fu_bcm57xx_dict_image_class_init(FuBcm57xxDictImageClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_bcm57xx_dict_image_parse; firmware_class->write = fu_bcm57xx_dict_image_write; firmware_class->build = fu_bcm57xx_dict_image_build; firmware_class->export = fu_bcm57xx_dict_image_export; } FuFirmware * fu_bcm57xx_dict_image_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_BCM57XX_DICT_IMAGE, NULL)); } fwupd-2.0.10/plugins/bcm57xx/fu-bcm57xx-dict-image.h000066400000000000000000000012211501337203100217430ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_BCM57XX_DICT_IMAGE (fu_bcm57xx_dict_image_get_type()) G_DECLARE_FINAL_TYPE(FuBcm57xxDictImage, fu_bcm57xx_dict_image, FU, BCM57XX_DICT_IMAGE, FuFirmware) FuFirmware * fu_bcm57xx_dict_image_new(void); void fu_bcm57xx_dict_image_set_kind(FuBcm57xxDictImage *self, guint8 kind); guint8 fu_bcm57xx_dict_image_get_kind(FuBcm57xxDictImage *self); void fu_bcm57xx_dict_image_set_target(FuBcm57xxDictImage *self, guint8 target); guint8 fu_bcm57xx_dict_image_get_target(FuBcm57xxDictImage *self); fwupd-2.0.10/plugins/bcm57xx/fu-bcm57xx-firmware.c000066400000000000000000000472041501337203100215620ustar00rootroot00000000000000/* * Copyright 2018 Evan Lojewski * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-bcm57xx-common.h" #include "fu-bcm57xx-dict-image.h" #include "fu-bcm57xx-firmware.h" #include "fu-bcm57xx-stage1-image.h" #include "fu-bcm57xx-stage2-image.h" struct _FuBcm57xxFirmware { FuFirmware parent_instance; guint16 vendor; guint16 model; gboolean is_backup; guint32 phys_addr; gsize source_size; guint8 source_padchar; }; G_DEFINE_TYPE(FuBcm57xxFirmware, fu_bcm57xx_firmware, FU_TYPE_FIRMWARE) #define BCM_STAGE1_HEADER_MAGIC_BROADCOM 0x0E000E03 #define BCM_STAGE1_HEADER_MAGIC_MEKLORT 0x3C1D0800 #define BCM_APE_HEADER_MAGIC 0x1A4D4342 #define BCM_CODE_DIRECTORY_ADDR_APE 0x07 static void fu_bcm57xx_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuBcm57xxFirmware *self = FU_BCM57XX_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "vendor", self->vendor); fu_xmlb_builder_insert_kx(bn, "model", self->model); if (flags & FU_FIRMWARE_EXPORT_FLAG_INCLUDE_DEBUG) { fu_xmlb_builder_insert_kb(bn, "is_backup", self->is_backup); fu_xmlb_builder_insert_kx(bn, "phys_addr", self->phys_addr); } } static gboolean fu_bcm57xx_firmware_parse_header(FuBcm57xxFirmware *self, GInputStream *stream, GError **error) { /* verify magic and CRC */ if (!fu_bcm57xx_verify_magic(stream, 0x0, error)) return FALSE; if (!fu_bcm57xx_verify_crc(stream, error)) return FALSE; /* get address */ return fu_input_stream_read_u32(stream, BCM_NVRAM_HEADER_PHYS_ADDR, &self->phys_addr, G_BIG_ENDIAN, error); } static FuFirmware * fu_bcm57xx_firmware_parse_info(FuBcm57xxFirmware *self, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { guint32 mac_addr0 = 0; g_autoptr(FuFirmware) img = fu_firmware_new(); /* if the MAC is set non-zero this is an actual backup rather than a container */ if (!fu_input_stream_read_u32(stream, BCM_NVRAM_INFO_MAC_ADDR0, &mac_addr0, G_BIG_ENDIAN, error)) return NULL; self->is_backup = mac_addr0 != 0x0 && mac_addr0 != 0xffffffff; /* read vendor + model */ if (!fu_input_stream_read_u16(stream, BCM_NVRAM_INFO_VENDOR, &self->vendor, G_BIG_ENDIAN, error)) return NULL; if (!fu_input_stream_read_u16(stream, BCM_NVRAM_INFO_DEVICE, &self->model, G_BIG_ENDIAN, error)) return NULL; /* success */ if (!fu_firmware_parse_stream(img, stream, 0x0, flags, error)) return NULL; fu_firmware_set_id(img, "info"); return g_steal_pointer(&img); } static FuFirmware * fu_bcm57xx_firmware_parse_stage1(FuBcm57xxFirmware *self, GInputStream *stream, guint32 *out_stage1_sz, FuFirmwareParseFlags flags, GError **error) { gsize streamsz = 0; guint32 stage1_wrds = 0; guint32 stage1_sz; guint32 stage1_off = 0; g_autoptr(FuFirmware) img = fu_bcm57xx_stage1_image_new(); g_autoptr(GInputStream) stream_tmp = NULL; if (!fu_input_stream_size(stream, &streamsz, error)) return NULL; if (!fu_input_stream_read_u32(stream, BCM_NVRAM_HEADER_BASE + BCM_NVRAM_HEADER_SIZE_WRDS, &stage1_wrds, G_BIG_ENDIAN, error)) return NULL; if (!fu_input_stream_read_u32(stream, BCM_NVRAM_HEADER_BASE + BCM_NVRAM_HEADER_OFFSET, &stage1_off, G_BIG_ENDIAN, error)) return NULL; stage1_sz = (stage1_wrds * sizeof(guint32)); if (stage1_off != BCM_NVRAM_STAGE1_BASE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "stage1 offset invalid, got: 0x%x, expected 0x%x", (guint)stage1_sz, (guint)BCM_NVRAM_STAGE1_BASE); return NULL; } if (stage1_off + stage1_sz > streamsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "bigger than firmware, got: 0x%x @ 0x%x", (guint)stage1_sz, (guint)stage1_off); return NULL; } /* verify CRC */ stream_tmp = fu_partial_input_stream_new(stream, stage1_off, stage1_sz, error); if (stream_tmp == NULL) return NULL; if (!fu_firmware_parse_stream(img, stream_tmp, 0x0, flags | FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error)) return NULL; /* needed for stage2 */ if (out_stage1_sz != NULL) *out_stage1_sz = stage1_sz; /* success */ fu_firmware_set_id(img, "stage1"); fu_firmware_set_offset(img, stage1_off); return g_steal_pointer(&img); } static FuFirmware * fu_bcm57xx_firmware_parse_stage2(FuBcm57xxFirmware *self, GInputStream *stream, guint32 stage1_sz, FuFirmwareParseFlags flags, GError **error) { gsize streamsz = 0; guint32 stage2_off = 0; guint32 stage2_sz = 0; g_autoptr(FuFirmware) img = fu_bcm57xx_stage2_image_new(); g_autoptr(GInputStream) stream_tmp = NULL; stage2_off = BCM_NVRAM_STAGE1_BASE + stage1_sz; if (!fu_bcm57xx_verify_magic(stream, stage2_off, error)) return NULL; if (!fu_input_stream_read_u32(stream, stage2_off + sizeof(guint32), &stage2_sz, G_BIG_ENDIAN, error)) return NULL; if (!fu_input_stream_size(stream, &streamsz, error)) return NULL; if (stage2_off + stage2_sz > streamsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "bigger than firmware, got: 0x%x @ 0x%x", (guint)stage2_sz, (guint)stage2_off); return NULL; } /* verify CRC */ stream_tmp = fu_partial_input_stream_new(stream, stage2_off + 0x8, stage2_sz, error); if (stream_tmp == NULL) return NULL; if (!fu_firmware_parse_stream(img, stream_tmp, 0x0, flags | FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error)) return NULL; /* success */ fu_firmware_set_id(img, "stage2"); fu_firmware_set_offset(img, stage2_off); return g_steal_pointer(&img); } static gboolean fu_bcm57xx_firmware_parse_dict(FuBcm57xxFirmware *self, GInputStream *stream, guint idx, FuFirmwareParseFlags flags, GError **error) { gsize streamsz = 0; guint32 dict_addr = 0x0; guint32 dict_info = 0x0; guint32 dict_off = 0x0; guint32 dict_sz; guint32 base = BCM_NVRAM_DIRECTORY_BASE + (idx * BCM_NVRAM_DIRECTORY_SZ); g_autoptr(FuFirmware) img = fu_bcm57xx_dict_image_new(); g_autoptr(GInputStream) stream_tmp = NULL; /* header */ if (!fu_input_stream_read_u32(stream, base + BCM_NVRAM_DIRECTORY_ADDR, &dict_addr, G_BIG_ENDIAN, error)) return FALSE; if (!fu_input_stream_read_u32(stream, base + BCM_NVRAM_DIRECTORY_SIZE_WRDS, &dict_info, G_BIG_ENDIAN, error)) return FALSE; if (!fu_input_stream_read_u32(stream, base + BCM_NVRAM_DIRECTORY_OFFSET, &dict_off, G_BIG_ENDIAN, error)) return FALSE; /* no dict stored */ if (dict_addr == 0 && dict_info == 0 && dict_off == 0) return TRUE; dict_sz = (dict_info & 0x00FFFFFF) * sizeof(guint32); /* implies that maximum size is 16 MB */ fu_bcm57xx_dict_image_set_target(FU_BCM57XX_DICT_IMAGE(img), (dict_info & 0x0F000000) >> 24); fu_bcm57xx_dict_image_set_kind(FU_BCM57XX_DICT_IMAGE(img), (dict_info & 0xF0000000) >> 28); fu_firmware_set_addr(img, dict_addr); fu_firmware_set_offset(img, dict_off); fu_firmware_set_idx(img, 0x80 + idx); /* empty */ if (dict_sz == 0) { g_autoptr(GBytes) blob = g_bytes_new(NULL, 0); fu_firmware_set_bytes(img, blob); fu_firmware_add_image(FU_FIRMWARE(self), img); return TRUE; } /* check against image size */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (dict_off + dict_sz > streamsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "bigger than firmware, got: 0x%x @ 0x%x", (guint)dict_sz, (guint)dict_off); return FALSE; } stream_tmp = fu_partial_input_stream_new(stream, dict_off, dict_sz, error); if (stream_tmp == NULL) return FALSE; if (!fu_firmware_parse_stream(img, stream_tmp, 0x0, flags | FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error)) return FALSE; /* success */ fu_firmware_add_image(FU_FIRMWARE(self), img); return TRUE; } static gboolean fu_bcm57xx_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { guint32 magic = 0; if (!fu_input_stream_read_u32(stream, 0x0, &magic, G_BIG_ENDIAN, error)) { g_prefix_error(error, "failed to read magic: "); return FALSE; } if (magic != BCM_APE_HEADER_MAGIC && magic != BCM_STAGE1_HEADER_MAGIC_BROADCOM && magic != BCM_STAGE1_HEADER_MAGIC_MEKLORT && magic != BCM_NVRAM_MAGIC) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "file not supported, got: 0x%08X", magic); return FALSE; } /* success */ return TRUE; } static gboolean fu_bcm57xx_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuBcm57xxFirmware *self = FU_BCM57XX_FIRMWARE(firmware); gsize streamsz = 0; guint32 magic = 0; guint32 stage1_sz = 0; g_autoptr(FuFirmware) img_info2 = fu_firmware_new(); g_autoptr(FuFirmware) img_info = NULL; g_autoptr(FuFirmware) img_stage1 = NULL; g_autoptr(FuFirmware) img_stage2 = NULL; g_autoptr(FuFirmware) img_vpd = fu_firmware_new(); g_autoptr(GInputStream) stream_header = NULL; g_autoptr(GInputStream) stream_info2 = NULL; g_autoptr(GInputStream) stream_info = NULL; g_autoptr(GInputStream) stream_vpd = NULL; /* try to autodetect the file type */ if (!fu_input_stream_read_u32(stream, 0x0, &magic, G_BIG_ENDIAN, error)) return FALSE; /* standalone APE */ if (magic == BCM_APE_HEADER_MAGIC) { g_autoptr(FuFirmware) img = fu_bcm57xx_dict_image_new(); fu_bcm57xx_dict_image_set_target(FU_BCM57XX_DICT_IMAGE(img), 0xD); fu_bcm57xx_dict_image_set_kind(FU_BCM57XX_DICT_IMAGE(img), 0x0); fu_firmware_set_addr(img, BCM_CODE_DIRECTORY_ADDR_APE); fu_firmware_set_id(img, "ape"); fu_firmware_add_image(firmware, img); return TRUE; } /* standalone stage1 */ if (magic == BCM_STAGE1_HEADER_MAGIC_BROADCOM || magic == BCM_STAGE1_HEADER_MAGIC_MEKLORT) { g_autoptr(FuFirmware) img_stage1_standalone = fu_firmware_new(); if (!fu_firmware_set_stream(img_stage1_standalone, stream, error)) return FALSE; fu_firmware_set_id(img_stage1_standalone, "stage1"); fu_firmware_add_image(firmware, img_stage1_standalone); return TRUE; } /* not full NVRAM image */ if (magic != BCM_NVRAM_MAGIC) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "file not supported, got: 0x%08X", magic); return FALSE; } /* save the size so we can export the padding for a perfect roundtrip */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; self->source_size = streamsz; if (!fu_input_stream_read_u8(stream, streamsz - 1, &self->source_padchar, error)) return FALSE; /* NVRAM header */ stream_header = fu_partial_input_stream_new(stream, BCM_NVRAM_HEADER_BASE, BCM_NVRAM_HEADER_SZ, error); if (stream_header == NULL) return FALSE; if (!fu_bcm57xx_firmware_parse_header(self, stream_header, error)) { g_prefix_error(error, "failed to parse header: "); return FALSE; } /* info */ stream_info = fu_partial_input_stream_new(stream, BCM_NVRAM_INFO_BASE, BCM_NVRAM_INFO_SZ, error); if (stream_info == NULL) return FALSE; img_info = fu_bcm57xx_firmware_parse_info(self, stream_info, flags, error); if (img_info == NULL) { g_prefix_error(error, "failed to parse info: "); return FALSE; } fu_firmware_set_offset(img_info, BCM_NVRAM_INFO_BASE); fu_firmware_add_image(firmware, img_info); /* VPD */ stream_vpd = fu_partial_input_stream_new(stream, BCM_NVRAM_VPD_BASE, BCM_NVRAM_VPD_SZ, error); if (stream_vpd == NULL) return FALSE; if (!fu_firmware_parse_stream(img_vpd, stream_vpd, 0x0, flags, error)) { g_prefix_error(error, "failed to parse VPD: "); return FALSE; } fu_firmware_set_id(img_vpd, "vpd"); fu_firmware_set_offset(img_vpd, BCM_NVRAM_VPD_BASE); fu_firmware_add_image(firmware, img_vpd); /* info2 */ stream_info2 = fu_partial_input_stream_new(stream, BCM_NVRAM_INFO2_BASE, BCM_NVRAM_INFO2_SZ, error); if (stream_info2 == NULL) return FALSE; if (!fu_firmware_parse_stream(img_info2, stream_info2, 0x0, flags, error)) { g_prefix_error(error, "failed to parse info2: "); return FALSE; } fu_firmware_set_id(img_info2, "info2"); fu_firmware_set_offset(img_info2, BCM_NVRAM_INFO2_BASE); fu_firmware_add_image(firmware, img_info2); /* stage1 */ img_stage1 = fu_bcm57xx_firmware_parse_stage1(self, stream, &stage1_sz, flags, error); if (img_stage1 == NULL) { g_prefix_error(error, "failed to parse stage1: "); return FALSE; } fu_firmware_add_image(firmware, img_stage1); /* stage2 */ img_stage2 = fu_bcm57xx_firmware_parse_stage2(self, stream, stage1_sz, flags, error); if (img_stage2 == NULL) { g_prefix_error(error, "failed to parse stage2: "); return FALSE; } fu_firmware_add_image(firmware, img_stage2); /* dictionaries, e.g. APE */ for (guint i = 0; i < 8; i++) { if (!fu_bcm57xx_firmware_parse_dict(self, stream, i, flags, error)) { g_prefix_error(error, "failed to parse dict 0x%x: ", i); return FALSE; } } /* success */ return TRUE; } static GBytes * _g_bytes_new_sized(gsize sz) { g_autoptr(GByteArray) tmp = g_byte_array_sized_new(sz); for (gsize i = 0; i < sz; i++) fu_byte_array_append_uint8(tmp, 0x0); return g_bytes_new(tmp->data, tmp->len); } static gboolean fu_bcm57xx_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { FuBcm57xxFirmware *self = FU_BCM57XX_FIRMWARE(firmware); guint64 tmp; /* two simple properties */ tmp = xb_node_query_text_as_uint(n, "vendor", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT16) self->vendor = tmp; tmp = xb_node_query_text_as_uint(n, "model", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT16) self->model = tmp; /* success */ return TRUE; } static GByteArray * fu_bcm57xx_firmware_write(FuFirmware *firmware, GError **error) { gsize off = BCM_NVRAM_STAGE1_BASE; FuBcm57xxFirmware *self = FU_BCM57XX_FIRMWARE(firmware); g_autoptr(GByteArray) buf = g_byte_array_sized_new(self->source_size); g_autoptr(FuFirmware) img_info2 = NULL; g_autoptr(FuFirmware) img_info = NULL; g_autoptr(FuFirmware) img_stage1 = NULL; g_autoptr(FuFirmware) img_stage2 = NULL; g_autoptr(FuFirmware) img_vpd = NULL; g_autoptr(GBytes) blob_info2 = NULL; g_autoptr(GBytes) blob_info = NULL; g_autoptr(GBytes) blob_stage1 = NULL; g_autoptr(GBytes) blob_stage2 = NULL; g_autoptr(GBytes) blob_vpd = NULL; g_autoptr(GPtrArray) blob_dicts = NULL; /* write out the things we need to pre-compute */ img_stage1 = fu_firmware_get_image_by_id(firmware, "stage1", error); if (img_stage1 == NULL) return NULL; blob_stage1 = fu_firmware_write(img_stage1, error); if (blob_stage1 == NULL) return NULL; off += g_bytes_get_size(blob_stage1); img_stage2 = fu_firmware_get_image_by_id(firmware, "stage2", error); if (img_stage2 == NULL) return NULL; blob_stage2 = fu_firmware_write(img_stage2, error); if (blob_stage2 == NULL) return NULL; off += g_bytes_get_size(blob_stage2); /* add header */ fu_byte_array_append_uint32(buf, BCM_NVRAM_MAGIC, G_BIG_ENDIAN); fu_byte_array_append_uint32(buf, self->phys_addr, G_BIG_ENDIAN); fu_byte_array_append_uint32(buf, g_bytes_get_size(blob_stage1) / sizeof(guint32), G_BIG_ENDIAN); fu_byte_array_append_uint32(buf, BCM_NVRAM_STAGE1_BASE, G_BIG_ENDIAN); fu_byte_array_append_uint32(buf, fu_crc32(FU_CRC_KIND_B32_STANDARD, buf->data, buf->len), G_LITTLE_ENDIAN); /* add directory entries */ blob_dicts = g_ptr_array_new_with_free_func((GDestroyNotify)g_bytes_unref); for (guint i = 0; i < 8; i++) { g_autoptr(FuFirmware) img = NULL; g_autoptr(GBytes) blob = NULL; img = fu_firmware_get_image_by_idx(firmware, 0x80 + i, NULL); if (img != NULL) { blob = fu_firmware_write(img, error); if (blob == NULL) return NULL; } if (blob != NULL) { fu_byte_array_append_uint32(buf, fu_firmware_get_addr(img), G_BIG_ENDIAN); fu_byte_array_append_uint32( buf, (g_bytes_get_size(blob) / sizeof(guint32)) | (guint32)fu_bcm57xx_dict_image_get_target( FU_BCM57XX_DICT_IMAGE(img)) << 24 | (guint32)fu_bcm57xx_dict_image_get_kind(FU_BCM57XX_DICT_IMAGE(img)) << 28, G_BIG_ENDIAN); if (g_bytes_get_size(blob) > 0) { fu_byte_array_append_uint32(buf, off, G_BIG_ENDIAN); off += g_bytes_get_size(blob); } else { fu_byte_array_append_uint32(buf, 0x0, G_BIG_ENDIAN); } } else { blob = g_bytes_new(NULL, 0); for (guint32 j = 0; j < sizeof(guint32) * 3; j++) fu_byte_array_append_uint8(buf, 0x0); } g_ptr_array_add(blob_dicts, g_steal_pointer(&blob)); } /* add info */ img_info = fu_firmware_get_image_by_id(firmware, "info", NULL); if (img_info != NULL) { blob_info = fu_firmware_write(img_info, error); if (blob_info == NULL) return NULL; } else { g_autoptr(GByteArray) tmp = g_byte_array_sized_new(BCM_NVRAM_INFO_SZ); for (gsize i = 0; i < BCM_NVRAM_INFO_SZ; i++) fu_byte_array_append_uint8(tmp, 0x0); fu_memwrite_uint16(tmp->data + BCM_NVRAM_INFO_VENDOR, self->vendor, G_BIG_ENDIAN); fu_memwrite_uint16(tmp->data + BCM_NVRAM_INFO_DEVICE, self->model, G_BIG_ENDIAN); blob_info = g_bytes_new(tmp->data, tmp->len); } fu_byte_array_append_bytes(buf, blob_info); /* add vpd */ img_vpd = fu_firmware_get_image_by_id(firmware, "vpd", NULL); if (img_vpd != NULL) { blob_vpd = fu_firmware_write(img_vpd, error); if (blob_vpd == NULL) return NULL; } else { blob_vpd = _g_bytes_new_sized(BCM_NVRAM_VPD_SZ); } fu_byte_array_append_bytes(buf, blob_vpd); /* add info2 */ img_info2 = fu_firmware_get_image_by_id(firmware, "info2", NULL); if (img_info2 != NULL) { blob_info2 = fu_firmware_write(img_info2, error); if (blob_info2 == NULL) return NULL; } else { blob_info2 = _g_bytes_new_sized(BCM_NVRAM_INFO2_SZ); } fu_byte_array_append_bytes(buf, blob_info2); /* add stage1+2 */ fu_byte_array_append_bytes(buf, blob_stage1); fu_byte_array_append_bytes(buf, blob_stage2); /* add dictionaries, e.g. APE */ for (guint i = 0; i < blob_dicts->len; i++) { GBytes *blob = g_ptr_array_index(blob_dicts, i); fu_byte_array_append_bytes(buf, blob); } /* pad until full */ for (guint32 i = buf->len; i < self->source_size; i++) fu_byte_array_append_uint8(buf, self->source_padchar); /* add EOF */ return g_steal_pointer(&buf); } guint16 fu_bcm57xx_firmware_get_vendor(FuBcm57xxFirmware *self) { return self->vendor; } guint16 fu_bcm57xx_firmware_get_model(FuBcm57xxFirmware *self) { return self->model; } gboolean fu_bcm57xx_firmware_is_backup(FuBcm57xxFirmware *self) { return self->is_backup; } static void fu_bcm57xx_firmware_init(FuBcm57xxFirmware *self) { self->phys_addr = BCM_PHYS_ADDR_DEFAULT; self->source_size = BCM_FIRMWARE_SIZE; self->source_padchar = 0xff; fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_DEDUPE_ID); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_VID_PID); g_type_ensure(FU_TYPE_BCM57XX_STAGE1_IMAGE); g_type_ensure(FU_TYPE_BCM57XX_STAGE2_IMAGE); g_type_ensure(FU_TYPE_BCM57XX_DICT_IMAGE); } static void fu_bcm57xx_firmware_class_init(FuBcm57xxFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_bcm57xx_firmware_validate; firmware_class->parse = fu_bcm57xx_firmware_parse; firmware_class->export = fu_bcm57xx_firmware_export; firmware_class->write = fu_bcm57xx_firmware_write; firmware_class->build = fu_bcm57xx_firmware_build; } FuFirmware * fu_bcm57xx_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_BCM57XX_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/bcm57xx/fu-bcm57xx-firmware.h000066400000000000000000000010501501337203100215540ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_BCM57XX_FIRMWARE (fu_bcm57xx_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuBcm57xxFirmware, fu_bcm57xx_firmware, FU, BCM57XX_FIRMWARE, FuFirmware) FuFirmware * fu_bcm57xx_firmware_new(void); guint16 fu_bcm57xx_firmware_get_vendor(FuBcm57xxFirmware *self); guint16 fu_bcm57xx_firmware_get_model(FuBcm57xxFirmware *self); gboolean fu_bcm57xx_firmware_is_backup(FuBcm57xxFirmware *self); fwupd-2.0.10/plugins/bcm57xx/fu-bcm57xx-plugin.c000066400000000000000000000064601501337203100212430ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-bcm57xx-device.h" #include "fu-bcm57xx-dict-image.h" #include "fu-bcm57xx-firmware.h" #include "fu-bcm57xx-plugin.h" #include "fu-bcm57xx-recovery-device.h" #include "fu-bcm57xx-stage1-image.h" #include "fu-bcm57xx-stage2-image.h" struct _FuBcm57XxPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuBcm57XxPlugin, fu_bcm57xx_plugin, FU_TYPE_PLUGIN) static gboolean fu_bcm57xx_plugin_backend_device_added(FuPlugin *plugin, FuDevice *device, FuProgress *progress, GError **error) { gboolean exists_net = FALSE; g_autofree gchar *fn = NULL; g_autoptr(GPtrArray) ifaces = NULL; g_autoptr(FuDevice) dev = NULL; g_autoptr(FuDeviceLocker) locker = NULL; /* not us */ if (!FU_IS_UDEV_DEVICE(device)) return TRUE; /* only enumerate number 0 */ if (fu_udev_device_get_number(FU_UDEV_DEVICE(device)) != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "only device 0 supported on multi-device card"); return FALSE; } /* is in recovery mode if has no ethtool interface */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { dev = g_object_new(FU_TYPE_BCM57XX_DEVICE, "iface", "enp81s0f0", NULL); } else { fn = g_build_filename(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device)), "net", NULL); if (!fu_device_query_file_exists(device, fn, &exists_net, error)) return FALSE; if (!exists_net) { g_debug("waiting for net devices to appear"); fu_device_sleep(device, 50); /* ms */ } ifaces = fu_path_glob(fn, "en*", NULL); if (ifaces == NULL || ifaces->len == 0) { dev = g_object_new(FU_TYPE_BCM57XX_RECOVERY_DEVICE, NULL); } else { g_autofree gchar *ethtool_iface = g_path_get_basename(g_ptr_array_index(ifaces, 0)); dev = g_object_new(FU_TYPE_BCM57XX_DEVICE, "iface", ethtool_iface, NULL); } } fu_device_incorporate(dev, device, FU_DEVICE_INCORPORATE_FLAG_ALL); locker = fu_device_locker_new(dev, error); if (locker == NULL) return FALSE; fu_plugin_device_add(plugin, dev); return TRUE; } static void fu_bcm57xx_plugin_init(FuBcm57XxPlugin *self) { } static void fu_bcm57xx_plugin_object_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_set_name(plugin, "bcm57xx"); } static void fu_bcm57xx_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_udev_subsystem(plugin, "pci"); fu_plugin_add_device_gtype(plugin, FU_TYPE_BCM57XX_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_BCM57XX_RECOVERY_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_BCM57XX_FIRMWARE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_BCM57XX_DICT_IMAGE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_BCM57XX_STAGE1_IMAGE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_BCM57XX_STAGE2_IMAGE); } static void fu_bcm57xx_plugin_class_init(FuBcm57XxPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->constructed = fu_bcm57xx_plugin_object_constructed; plugin_class->constructed = fu_bcm57xx_plugin_constructed; plugin_class->backend_device_added = fu_bcm57xx_plugin_backend_device_added; } fwupd-2.0.10/plugins/bcm57xx/fu-bcm57xx-plugin.h000066400000000000000000000003571501337203100212470ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuBcm57XxPlugin, fu_bcm57xx_plugin, FU, BCM57XX_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/bcm57xx/fu-bcm57xx-recovery-device.c000066400000000000000000000611471501337203100230430ustar00rootroot00000000000000/* * Copyright 2018 Evan Lojewski * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include #include #include #include #ifdef HAVE_MMAN_H #include #endif #ifdef HAVE_VALGRIND #include #endif /* HAVE_VALGRIND */ #include "fu-bcm57xx-common.h" #include "fu-bcm57xx-firmware.h" #include "fu-bcm57xx-recovery-device.h" /* offsets into BAR[0] */ #define REG_DEVICE_PCI_VENDOR_DEVICE_ID 0x6434 #define REG_NVM_SOFTWARE_ARBITRATION 0x7020 #define REG_NVM_ACCESS 0x7024 #define REG_NVM_COMMAND 0x7000 #define REG_NVM_ADDR 0x700c #define REG_NVM_READ 0x7010 #define REG_NVM_WRITE 0x7008 /* offsets into BAR[2] */ #define REG_APE_MODE 0x0 typedef struct { guint8 *buf; gsize bufsz; } FuBcm57xxMmap; #define FU_BCM57XX_BAR_DEVICE 0 #define FU_BCM57XX_BAR_APE 1 #define FU_BCM57XX_BAR_MAX 3 struct _FuBcm57xxRecoveryDevice { FuUdevDevice parent_instance; FuBcm57xxMmap bar[FU_BCM57XX_BAR_MAX]; }; typedef union { guint32 r32; struct { guint32 reserved_0_0 : 1; guint32 Reset : 1; guint32 reserved_2_2 : 1; guint32 Done : 1; guint32 Doit : 1; guint32 Wr : 1; guint32 Erase : 1; guint32 First : 1; guint32 Last : 1; guint32 reserved_15_9 : 7; guint32 WriteEnableCommand : 1; guint32 WriteDisableCommand : 1; guint32 reserved_31_18 : 14; } __attribute__((packed)) bits; /* nocheck:blocked */ } BcmRegNVMCommand; typedef union { guint32 r32; struct { guint32 ReqSet0 : 1; guint32 ReqSet1 : 1; guint32 ReqSet2 : 1; guint32 ReqSet3 : 1; guint32 ReqClr0 : 1; guint32 ReqClr1 : 1; guint32 ReqClr2 : 1; guint32 ReqClr3 : 1; guint32 ArbWon0 : 1; guint32 ArbWon1 : 1; guint32 ArbWon2 : 1; guint32 ArbWon3 : 1; guint32 Req0 : 1; guint32 Req1 : 1; guint32 Req2 : 1; guint32 Req3 : 1; guint32 reserved_31_16 : 16; } __attribute__((packed)) bits; /* nocheck:blocked */ } BcmRegNVMSoftwareArbitration; typedef union { guint32 r32; struct { guint32 Enable : 1; guint32 WriteEnable : 1; guint32 reserved_31_2 : 30; } __attribute__((packed)) bits; /* nocheck:blocked */ } BcmRegNVMAccess; typedef union { guint32 r32; struct { guint32 Reset : 1; guint32 Halt : 1; guint32 FastBoot : 1; guint32 HostDiag : 1; guint32 reserved_4_4 : 1; guint32 Event1 : 1; guint32 Event2 : 1; guint32 GRCint : 1; guint32 reserved_8_8 : 1; guint32 SwapATBdword : 1; guint32 reserved_10_10 : 1; guint32 SwapARBdword : 1; guint32 reserved_13_12 : 2; guint32 Channel0Enable : 1; guint32 Channel2Enable : 1; guint32 reserved_17_16 : 2; guint32 MemoryECC : 1; guint32 ICodePIPRdDisable : 1; guint32 reserved_29_20 : 10; guint32 Channel1Enable : 1; guint32 Channel3Enable : 1; } __attribute__((packed)) bits; /* nocheck:blocked */ } BcmRegAPEMode; G_DEFINE_TYPE(FuBcm57xxRecoveryDevice, fu_bcm57xx_recovery_device, FU_TYPE_UDEV_DEVICE) #ifdef __ppc64__ #define BARRIER() __asm__ volatile("sync 0\neieio\n" : : : "memory") #else #define BARRIER() __asm__ volatile("" : : : "memory"); #endif static gboolean fu_bcm57xx_recovery_device_bar_read(FuBcm57xxRecoveryDevice *self, guint bar, gsize offset, guint32 *val, GError **error) { /* this should never happen */ if (self->bar[bar].buf == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "BAR[%u] is not mapped!", bar); return FALSE; } BARRIER(); return fu_memcpy_safe((guint8 *)val, sizeof(*val), 0x0, /* dst */ self->bar[bar].buf, self->bar[bar].bufsz, offset, sizeof(*val), error); } static gboolean fu_bcm57xx_recovery_device_bar_write(FuBcm57xxRecoveryDevice *self, guint bar, gsize offset, guint32 val, GError **error) { /* this should never happen */ if (self->bar[bar].buf == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "BAR[%u] is not mapped!", bar); return FALSE; } BARRIER(); if (!fu_memcpy_safe(self->bar[bar].buf, self->bar[bar].bufsz, offset, /* dst */ (const guint8 *)&val, sizeof(val), 0x0, /* src */ sizeof(val), error)) return FALSE; BARRIER(); return TRUE; } static gboolean fu_bcm57xx_recovery_device_nvram_disable(FuBcm57xxRecoveryDevice *self, GError **error) { BcmRegNVMAccess tmp; if (!fu_bcm57xx_recovery_device_bar_read(self, FU_BCM57XX_BAR_DEVICE, REG_NVM_ACCESS, &tmp.r32, error)) return FALSE; tmp.bits.Enable = FALSE; tmp.bits.WriteEnable = FALSE; return fu_bcm57xx_recovery_device_bar_write(self, FU_BCM57XX_BAR_DEVICE, REG_NVM_ACCESS, tmp.r32, error); } static gboolean fu_bcm57xx_recovery_device_nvram_enable(FuBcm57xxRecoveryDevice *self, GError **error) { BcmRegNVMAccess tmp; if (!fu_bcm57xx_recovery_device_bar_read(self, FU_BCM57XX_BAR_DEVICE, REG_NVM_ACCESS, &tmp.r32, error)) return FALSE; tmp.bits.Enable = TRUE; tmp.bits.WriteEnable = FALSE; return fu_bcm57xx_recovery_device_bar_write(self, FU_BCM57XX_BAR_DEVICE, REG_NVM_ACCESS, tmp.r32, error); } static gboolean fu_bcm57xx_recovery_device_nvram_enable_write(FuBcm57xxRecoveryDevice *self, GError **error) { BcmRegNVMAccess tmp; if (!fu_bcm57xx_recovery_device_bar_read(self, FU_BCM57XX_BAR_DEVICE, REG_NVM_ACCESS, &tmp.r32, error)) return FALSE; tmp.bits.Enable = TRUE; tmp.bits.WriteEnable = TRUE; return fu_bcm57xx_recovery_device_bar_write(self, FU_BCM57XX_BAR_DEVICE, REG_NVM_ACCESS, tmp.r32, error); } static gboolean fu_bcm57xx_recovery_device_nvram_acquire_lock(FuBcm57xxRecoveryDevice *self, GError **error) { BcmRegNVMSoftwareArbitration tmp = {0}; g_autoptr(GTimer) timer = g_timer_new(); tmp.bits.ReqSet1 = 1; if (!fu_bcm57xx_recovery_device_bar_write(self, FU_BCM57XX_BAR_DEVICE, REG_NVM_SOFTWARE_ARBITRATION, tmp.r32, error)) return FALSE; do { if (!fu_bcm57xx_recovery_device_bar_read(self, FU_BCM57XX_BAR_DEVICE, REG_NVM_SOFTWARE_ARBITRATION, &tmp.r32, error)) return FALSE; if (tmp.bits.ArbWon1) return TRUE; if (g_timer_elapsed(timer, NULL) > 0.2) break; } while (TRUE); /* timed out */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "timed out trying to acquire lock #1"); return FALSE; } static gboolean fu_bcm57xx_recovery_device_nvram_release_lock(FuBcm57xxRecoveryDevice *self, GError **error) { BcmRegNVMSoftwareArbitration tmp = {0}; tmp.r32 = 0; tmp.bits.ReqClr1 = 1; return fu_bcm57xx_recovery_device_bar_write(self, FU_BCM57XX_BAR_DEVICE, REG_NVM_SOFTWARE_ARBITRATION, tmp.r32, error); } static gboolean fu_bcm57xx_recovery_device_nvram_wait_done(FuBcm57xxRecoveryDevice *self, GError **error) { BcmRegNVMCommand tmp = {0}; g_autoptr(GTimer) timer = g_timer_new(); do { if (!fu_bcm57xx_recovery_device_bar_read(self, FU_BCM57XX_BAR_DEVICE, REG_NVM_COMMAND, &tmp.r32, error)) return FALSE; if (tmp.bits.Done) return TRUE; if (g_timer_elapsed(timer, NULL) > 0.2) break; } while (TRUE); /* timed out */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "timed out"); return FALSE; } static gboolean fu_bcm57xx_recovery_device_nvram_clear_done(FuBcm57xxRecoveryDevice *self, GError **error) { BcmRegNVMCommand tmp = {0}; tmp.bits.Done = 1; return fu_bcm57xx_recovery_device_bar_write(self, FU_BCM57XX_BAR_DEVICE, REG_NVM_COMMAND, tmp.r32, error); } static gboolean fu_bcm57xx_recovery_device_nvram_read(FuBcm57xxRecoveryDevice *self, guint32 address, guint32 *buf, gsize bufsz, FuProgress *progress, GError **error) { fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, bufsz); for (guint i = 0; i < bufsz; i++) { BcmRegNVMCommand tmp = {0}; guint32 val32 = 0; if (!fu_bcm57xx_recovery_device_nvram_clear_done(self, error)) return FALSE; if (!fu_bcm57xx_recovery_device_bar_write(self, FU_BCM57XX_BAR_DEVICE, REG_NVM_ADDR, address, error)) return FALSE; tmp.bits.Doit = 1; tmp.bits.First = i == 0; tmp.bits.Last = i == bufsz - 1; if (!fu_bcm57xx_recovery_device_bar_write(self, FU_BCM57XX_BAR_DEVICE, REG_NVM_COMMAND, tmp.r32, error)) return FALSE; if (!fu_bcm57xx_recovery_device_nvram_wait_done(self, error)) { g_prefix_error(error, "failed to read @0x%x: ", address); return FALSE; } if (!fu_bcm57xx_recovery_device_bar_read(self, FU_BCM57XX_BAR_DEVICE, REG_NVM_READ, &val32, error)) return FALSE; buf[i] = GUINT32_FROM_BE(val32); /* nocheck:blocked */ address += sizeof(guint32); fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_bcm57xx_recovery_device_nvram_write(FuBcm57xxRecoveryDevice *self, guint32 address, const guint32 *buf, gsize bufsz_dwrds, FuProgress *progress, GError **error) { const guint32 page_size_dwrds = 64; /* can only write in pages of 64 dwords */ if (bufsz_dwrds % page_size_dwrds != 0 || (address * sizeof(guint32)) % page_size_dwrds != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "can only write aligned with page size 0x%x", page_size_dwrds); return FALSE; } fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, bufsz_dwrds); for (guint i = 0; i < bufsz_dwrds; i++) { BcmRegNVMCommand tmp = {0}; if (!fu_bcm57xx_recovery_device_nvram_clear_done(self, error)) return FALSE; if (!fu_bcm57xx_recovery_device_bar_write( self, FU_BCM57XX_BAR_DEVICE, REG_NVM_WRITE, GUINT32_TO_BE(buf[i]), /* nocheck:blocked */ error)) return FALSE; if (!fu_bcm57xx_recovery_device_bar_write(self, FU_BCM57XX_BAR_DEVICE, REG_NVM_ADDR, address, error)) return FALSE; tmp.bits.Wr = TRUE; tmp.bits.Doit = TRUE; tmp.bits.First = i % page_size_dwrds == 0; tmp.bits.Last = (i + 1) % page_size_dwrds == 0; if (!fu_bcm57xx_recovery_device_bar_write(self, FU_BCM57XX_BAR_DEVICE, REG_NVM_COMMAND, tmp.r32, error)) return FALSE; if (!fu_bcm57xx_recovery_device_nvram_wait_done(self, error)) { g_prefix_error(error, "failed to write @0x%x: ", address); return FALSE; } address += sizeof(guint32); fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_bcm57xx_recovery_device_detach(FuDevice *device, FuProgress *progress, GError **error) { /* unbind tg3 */ return fu_device_unbind_driver(device, error); } static gboolean fu_bcm57xx_recovery_device_attach(FuDevice *device, FuProgress *progress, GError **error) { g_autoptr(GError) error_local = NULL; /* bind tg3, which might fail if the module is not compiled */ if (!fu_device_bind_driver(device, "pci", "tg3", &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_warning("failed to bind tg3: %s", error_local->message); } else { g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to bind tg3: "); return FALSE; } } /* success */ return TRUE; } static gboolean fu_bcm57xx_recovery_device_activate(FuDevice *device, FuProgress *progress, GError **error) { BcmRegAPEMode mode = {0}; FuBcm57xxRecoveryDevice *self = FU_BCM57XX_RECOVERY_DEVICE(device); /* halt */ mode.bits.Halt = 1; mode.bits.FastBoot = 0; if (!fu_bcm57xx_recovery_device_bar_write(self, FU_BCM57XX_BAR_APE, REG_APE_MODE, mode.r32, error)) return FALSE; /* boot */ mode.bits.Halt = 0; mode.bits.FastBoot = 0; mode.bits.Reset = 1; return fu_bcm57xx_recovery_device_bar_write(self, FU_BCM57XX_BAR_APE, REG_APE_MODE, mode.r32, error); } static GBytes * fu_bcm57xx_recovery_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuBcm57xxRecoveryDevice *self = FU_BCM57XX_RECOVERY_DEVICE(device); gsize bufsz_dwrds = fu_device_get_firmware_size_max(FU_DEVICE(self)) / sizeof(guint32); g_autofree guint32 *buf_dwrds = g_new0(guint32, bufsz_dwrds); g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(FuDeviceLocker) locker2 = NULL; /* read from hardware */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ); locker = fu_device_locker_new_full( self, (FuDeviceLockerFunc)fu_bcm57xx_recovery_device_nvram_acquire_lock, (FuDeviceLockerFunc)fu_bcm57xx_recovery_device_nvram_release_lock, error); if (locker == NULL) return NULL; locker2 = fu_device_locker_new_full(self, (FuDeviceLockerFunc)fu_bcm57xx_recovery_device_nvram_enable, (FuDeviceLockerFunc)fu_bcm57xx_recovery_device_nvram_disable, error); if (locker2 == NULL) return NULL; if (!fu_bcm57xx_recovery_device_nvram_read(self, 0x0, buf_dwrds, bufsz_dwrds, progress, error)) return NULL; if (!fu_device_locker_close(locker2, error)) return NULL; return g_bytes_new(buf_dwrds, bufsz_dwrds * sizeof(guint32)); } static FuFirmware * fu_bcm57xx_recovery_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { g_autoptr(FuFirmware) firmware_bin = fu_firmware_new(); g_autoptr(FuFirmware) firmware_tmp = fu_bcm57xx_firmware_new(); /* check is a NVRAM backup */ if (!fu_firmware_parse_stream(firmware_tmp, stream, 0x0, flags, error)) { g_prefix_error(error, "failed to parse new firmware: "); return NULL; } if (!fu_bcm57xx_firmware_is_backup(FU_BCM57XX_FIRMWARE(firmware_tmp))) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "can only recover with backup firmware"); return NULL; } if (!fu_firmware_parse_stream(firmware_bin, stream, 0x0, flags, error)) return NULL; return g_steal_pointer(&firmware_bin); } static gboolean fu_bcm57xx_recovery_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuBcm57xxRecoveryDevice *self = FU_BCM57XX_RECOVERY_DEVICE(device); const guint8 *buf; gsize bufsz = 0; gsize bufsz_dwrds; g_autofree guint32 *buf_dwrds = NULL; g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(FuDeviceLocker) locker2 = NULL; g_autoptr(GBytes) blob = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 95, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 5, NULL); /* build the images into one linear blob of the correct size */ blob = fu_firmware_write(firmware, error); if (blob == NULL) return FALSE; fu_progress_step_done(progress); /* align into uint32_t buffer */ buf = g_bytes_get_data(blob, &bufsz); bufsz_dwrds = bufsz / sizeof(guint32); buf_dwrds = g_new0(guint32, bufsz_dwrds); if (!fu_memcpy_safe((guint8 *)buf_dwrds, bufsz_dwrds * sizeof(guint32), 0x0, /* dst */ buf, bufsz, 0x0, /* src */ bufsz, error)) return FALSE; /* hit hardware */ locker = fu_device_locker_new_full( self, (FuDeviceLockerFunc)fu_bcm57xx_recovery_device_nvram_acquire_lock, (FuDeviceLockerFunc)fu_bcm57xx_recovery_device_nvram_release_lock, error); if (locker == NULL) return FALSE; locker2 = fu_device_locker_new_full( self, (FuDeviceLockerFunc)fu_bcm57xx_recovery_device_nvram_enable_write, (FuDeviceLockerFunc)fu_bcm57xx_recovery_device_nvram_disable, error); if (locker2 == NULL) return FALSE; if (!fu_bcm57xx_recovery_device_nvram_write(self, 0x0, buf_dwrds, bufsz_dwrds, fu_progress_get_child(progress), error)) return FALSE; if (!fu_device_locker_close(locker2, error)) return FALSE; if (!fu_device_locker_close(locker, error)) return FALSE; fu_progress_step_done(progress); /* reset APE */ if (!fu_device_activate(device, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_bcm57xx_recovery_device_setup(FuDevice *device, GError **error) { FuBcm57xxRecoveryDevice *self = FU_BCM57XX_RECOVERY_DEVICE(device); guint32 fwversion = 0; g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(FuDeviceLocker) locker2 = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 10, "enable"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 80, "nvram"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 10, "veraddr"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 10, "version"); locker = fu_device_locker_new_full( self, (FuDeviceLockerFunc)fu_bcm57xx_recovery_device_nvram_acquire_lock, (FuDeviceLockerFunc)fu_bcm57xx_recovery_device_nvram_release_lock, error); if (locker == NULL) return FALSE; locker2 = fu_device_locker_new_full(self, (FuDeviceLockerFunc)fu_bcm57xx_recovery_device_nvram_enable, (FuDeviceLockerFunc)fu_bcm57xx_recovery_device_nvram_disable, error); if (locker2 == NULL) return FALSE; fu_progress_step_done(progress); /* get NVRAM version */ if (!fu_bcm57xx_recovery_device_nvram_read(self, BCM_NVRAM_STAGE1_BASE + BCM_NVRAM_STAGE1_VERSION, &fwversion, 1, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); if (fwversion != 0x0) { /* this is only set on the OSS firmware */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version_raw(device, GUINT32_FROM_BE(fwversion)); /* nocheck:blocked */ fu_device_set_branch(device, BCM_FW_BRANCH_OSS_FIRMWARE); fu_progress_step_done(progress); fu_progress_step_done(progress); } else { guint32 bufver[4] = {0x0}; guint32 veraddr = 0; g_autoptr(Bcm57xxVeritem) veritem = NULL; /* fall back to the string, e.g. '5719-v1.43' */ if (!fu_bcm57xx_recovery_device_nvram_read(self, BCM_NVRAM_STAGE1_BASE + BCM_NVRAM_STAGE1_VERADDR, &veraddr, 1, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); veraddr = GUINT32_FROM_BE(veraddr); /* nocheck:blocked */ if (veraddr > BCM_PHYS_ADDR_DEFAULT) veraddr -= BCM_PHYS_ADDR_DEFAULT; if (!fu_bcm57xx_recovery_device_nvram_read(self, BCM_NVRAM_STAGE1_BASE + veraddr, bufver, 4, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); veritem = fu_bcm57xx_veritem_new((guint8 *)bufver, sizeof(bufver)); if (veritem != NULL) { fu_device_set_version(device, veritem->version); /* nocheck:set-version */ fu_device_set_branch(device, veritem->branch); fu_device_set_version_format(device, veritem->verfmt); } } return TRUE; } static gboolean fu_bcm57xx_recovery_device_open(FuDevice *device, GError **error) { #ifdef HAVE_MMAN_H FuBcm57xxRecoveryDevice *self = FU_BCM57XX_RECOVERY_DEVICE(device); FuUdevDevice *udev_device = FU_UDEV_DEVICE(device); const gchar *sysfs_path = fu_udev_device_get_sysfs_path(udev_device); #endif #ifdef RUNNING_ON_VALGRIND /* this can't work */ if (RUNNING_ON_VALGRIND) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot mmap'ing BARs when using valgrind"); return FALSE; } #endif #ifdef HAVE_MMAN_H /* map BARs */ for (guint i = 0; i < FU_BCM57XX_BAR_MAX; i++) { int memfd; struct stat st; g_autofree gchar *fn = NULL; g_autofree gchar *resfn = NULL; /* open 64 bit resource */ resfn = g_strdup_printf("resource%u", i * 2); fn = g_build_filename(sysfs_path, resfn, NULL); memfd = open(fn, O_RDWR | O_SYNC); if (memfd < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "error opening %s", fn); return FALSE; } if (fstat(memfd, &st) < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "could not stat %s", fn); close(memfd); return FALSE; } /* mmap */ g_debug("mapping BAR[%u] %s for 0x%x bytes", i, fn, (guint)st.st_size); self->bar[i].buf = (guint8 *)mmap(0, st.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, memfd, 0); self->bar[i].bufsz = st.st_size; close(memfd); if (self->bar[i].buf == MAP_FAILED) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "could not mmap %s: %s", fn, g_strerror(errno)); return FALSE; } } /* success */ return TRUE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "mmap() not supported as sys/mman.h not available"); return FALSE; #endif } static gboolean fu_bcm57xx_recovery_device_close(FuDevice *device, GError **error) { #ifdef HAVE_MMAN_H FuBcm57xxRecoveryDevice *self = FU_BCM57XX_RECOVERY_DEVICE(device); /* unmap BARs */ for (guint i = 0; i < FU_BCM57XX_BAR_MAX; i++) { if (self->bar[i].buf == NULL) continue; g_debug("unmapping BAR[%u]", i); munmap(self->bar[i].buf, self->bar[i].bufsz); self->bar[i].buf = NULL; self->bar[i].bufsz = 0; } /* success */ return TRUE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "munmap() not supported as sys/mman.h not available"); return FALSE; #endif } static void fu_bcm57xx_recovery_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static gchar * fu_bcm57xx_recovery_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_device_get_version_format(device)); } static void fu_bcm57xx_recovery_device_init(FuBcm57xxRecoveryDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_REBOOT); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_BACKUP_BEFORE_INSTALL); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_protocol(FU_DEVICE(self), "com.broadcom.bcm57xx"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_NETWORK_WIRED); fu_device_set_logical_id(FU_DEVICE(self), "recovery"); /* other values are set from a quirk */ fu_device_set_firmware_size(FU_DEVICE(self), BCM_FIRMWARE_SIZE); /* no BARs mapped */ for (guint i = 0; i < FU_BCM57XX_BAR_MAX; i++) { self->bar[i].buf = NULL; self->bar[i].bufsz = 0; } } static gboolean fu_bcm57xx_recovery_device_probe(FuDevice *device, GError **error) { return fu_udev_device_set_physical_id(FU_UDEV_DEVICE(device), "pci", error); } static void fu_bcm57xx_recovery_device_class_init(FuBcm57xxRecoveryDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->activate = fu_bcm57xx_recovery_device_activate; device_class->prepare_firmware = fu_bcm57xx_recovery_device_prepare_firmware; device_class->setup = fu_bcm57xx_recovery_device_setup; device_class->reload = fu_bcm57xx_recovery_device_setup; device_class->open = fu_bcm57xx_recovery_device_open; device_class->close = fu_bcm57xx_recovery_device_close; device_class->write_firmware = fu_bcm57xx_recovery_device_write_firmware; device_class->dump_firmware = fu_bcm57xx_recovery_device_dump_firmware; device_class->attach = fu_bcm57xx_recovery_device_attach; device_class->detach = fu_bcm57xx_recovery_device_detach; device_class->probe = fu_bcm57xx_recovery_device_probe; device_class->set_progress = fu_bcm57xx_recovery_device_set_progress; device_class->convert_version = fu_bcm57xx_recovery_device_convert_version; } fwupd-2.0.10/plugins/bcm57xx/fu-bcm57xx-recovery-device.h000066400000000000000000000005711501337203100230420ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_BCM57XX_RECOVERY_DEVICE (fu_bcm57xx_recovery_device_get_type()) G_DECLARE_FINAL_TYPE(FuBcm57xxRecoveryDevice, fu_bcm57xx_recovery_device, FU, BCM57XX_RECOVERY_DEVICE, FuUdevDevice) fwupd-2.0.10/plugins/bcm57xx/fu-bcm57xx-stage1-image.c000066400000000000000000000112531501337203100222050ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-bcm57xx-common.h" #include "fu-bcm57xx-stage1-image.h" struct _FuBcm57xxStage1Image { FuFirmware parent_instance; }; G_DEFINE_TYPE(FuBcm57xxStage1Image, fu_bcm57xx_stage1_image, FU_TYPE_FIRMWARE) static gboolean fu_bcm57xx_stage1_image_parse(FuFirmware *image, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { gsize streamsz = 0; guint32 fwversion = 0; g_autoptr(GInputStream) stream_nocrc = NULL; /* verify CRC */ if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { if (!fu_bcm57xx_verify_crc(stream, error)) return FALSE; } /* get version number */ if (!fu_input_stream_read_u32(stream, BCM_NVRAM_STAGE1_VERSION, &fwversion, G_BIG_ENDIAN, error)) return FALSE; if (fwversion != 0x0) { fu_firmware_set_version_raw(image, fwversion); } else { guint32 veraddr = 0x0; /* fall back to the optional string, e.g. '5719-v1.43' */ if (!fu_input_stream_read_u32(stream, BCM_NVRAM_STAGE1_VERADDR, &veraddr, G_BIG_ENDIAN, error)) return FALSE; if (veraddr != 0x0) { guint32 bufver[4] = {'\0'}; g_autoptr(Bcm57xxVeritem) veritem = NULL; if (veraddr < BCM_PHYS_ADDR_DEFAULT + sizeof(bufver)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "version address 0x%x less than physical 0x%x", veraddr, (guint)BCM_PHYS_ADDR_DEFAULT); return FALSE; } if (!fu_input_stream_read_safe(stream, (guint8 *)bufver, sizeof(bufver), 0x0, /* dst */ veraddr - BCM_PHYS_ADDR_DEFAULT, /* src */ sizeof(bufver), error)) return FALSE; veritem = fu_bcm57xx_veritem_new((guint8 *)bufver, sizeof(bufver)); if (veritem != NULL) { fu_firmware_set_version(image, /* nocheck:set-version */ veritem->version); } } } if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz < sizeof(guint32)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "stage1 image is too small"); return FALSE; } stream_nocrc = fu_partial_input_stream_new(stream, 0x0, streamsz - sizeof(guint32), error); if (stream_nocrc == NULL) return FALSE; return fu_firmware_set_stream(image, stream_nocrc, error); } static GByteArray * fu_bcm57xx_stage1_image_write(FuFirmware *firmware, GError **error) { guint32 crc; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GBytes) fw_nocrc = NULL; /* sanity check */ if (fu_firmware_get_alignment(firmware) > FU_FIRMWARE_ALIGNMENT_1M) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "alignment invalid, got 0x%02x", fu_firmware_get_alignment(firmware)); return NULL; } /* the CRC-less payload */ fw_nocrc = fu_firmware_get_bytes(firmware, error); if (fw_nocrc == NULL) return NULL; /* fuzzing, so write a header with the version */ if (g_bytes_get_size(fw_nocrc) < BCM_NVRAM_STAGE1_VERSION) fu_byte_array_set_size(buf, BCM_NVRAM_STAGE1_VERSION + sizeof(guint32), 0x00); /* payload */ fu_byte_array_append_bytes(buf, fw_nocrc); /* update version */ if (!fu_memwrite_uint32_safe(buf->data, buf->len, BCM_NVRAM_STAGE1_VERSION, fu_firmware_get_version_raw(firmware), G_BIG_ENDIAN, error)) return NULL; /* align */ fu_byte_array_set_size( buf, fu_common_align_up(g_bytes_get_size(fw_nocrc), fu_firmware_get_alignment(firmware)), 0x00); /* add CRC */ crc = fu_crc32(FU_CRC_KIND_B32_STANDARD, buf->data, buf->len); fu_byte_array_append_uint32(buf, crc, G_LITTLE_ENDIAN); return g_steal_pointer(&buf); } static gchar * fu_bcm57xx_stage1_image_convert_version(FuFirmware *firmware, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_firmware_get_version_format(firmware)); } static void fu_bcm57xx_stage1_image_init(FuBcm57xxStage1Image *self) { fu_firmware_set_alignment(FU_FIRMWARE(self), FU_FIRMWARE_ALIGNMENT_4); fu_firmware_set_version_format(FU_FIRMWARE(self), FWUPD_VERSION_FORMAT_TRIPLET); } static void fu_bcm57xx_stage1_image_class_init(FuBcm57xxStage1ImageClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->convert_version = fu_bcm57xx_stage1_image_convert_version; firmware_class->parse = fu_bcm57xx_stage1_image_parse; firmware_class->write = fu_bcm57xx_stage1_image_write; } FuFirmware * fu_bcm57xx_stage1_image_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_BCM57XX_STAGE1_IMAGE, NULL)); } fwupd-2.0.10/plugins/bcm57xx/fu-bcm57xx-stage1-image.h000066400000000000000000000006311501337203100222100ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_BCM57XX_STAGE1_IMAGE (fu_bcm57xx_stage1_image_get_type()) G_DECLARE_FINAL_TYPE(FuBcm57xxStage1Image, fu_bcm57xx_stage1_image, FU, BCM57XX_STAGE1_IMAGE, FuFirmware) FuFirmware * fu_bcm57xx_stage1_image_new(void); fwupd-2.0.10/plugins/bcm57xx/fu-bcm57xx-stage2-image.c000066400000000000000000000045241501337203100222110ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-bcm57xx-common.h" #include "fu-bcm57xx-stage2-image.h" struct _FuBcm57xxStage2Image { FuFirmware parent_instance; }; G_DEFINE_TYPE(FuBcm57xxStage2Image, fu_bcm57xx_stage2_image, FU_TYPE_FIRMWARE) static gboolean fu_bcm57xx_stage2_image_parse(FuFirmware *image, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { gsize streamsz = 0; g_autoptr(GInputStream) stream_nocrc = NULL; if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { if (!fu_bcm57xx_verify_crc(stream, error)) return FALSE; } if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz < sizeof(guint32)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "stage2 image is too small"); return FALSE; } stream_nocrc = fu_partial_input_stream_new(stream, 0x0, streamsz - sizeof(guint32), error); if (stream_nocrc == NULL) return FALSE; return fu_firmware_set_stream(image, stream_nocrc, error); } static GByteArray * fu_bcm57xx_stage2_image_write(FuFirmware *image, GError **error) { g_autoptr(GByteArray) blob = NULL; g_autoptr(GBytes) fw_nocrc = NULL; /* get the CRC-less data */ fw_nocrc = fu_firmware_get_bytes(image, error); if (fw_nocrc == NULL) return NULL; /* add to a mutable buffer */ blob = g_byte_array_sized_new(g_bytes_get_size(fw_nocrc) + (sizeof(guint32) * 3)); fu_byte_array_append_uint32(blob, BCM_NVRAM_MAGIC, G_BIG_ENDIAN); fu_byte_array_append_uint32(blob, g_bytes_get_size(fw_nocrc) + sizeof(guint32), G_BIG_ENDIAN); fu_byte_array_append_bytes(blob, fw_nocrc); /* add CRC */ fu_byte_array_append_uint32(blob, fu_crc32_bytes(FU_CRC_KIND_B32_STANDARD, fw_nocrc), G_LITTLE_ENDIAN); return g_steal_pointer(&blob); } static void fu_bcm57xx_stage2_image_init(FuBcm57xxStage2Image *self) { } static void fu_bcm57xx_stage2_image_class_init(FuBcm57xxStage2ImageClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_bcm57xx_stage2_image_parse; firmware_class->write = fu_bcm57xx_stage2_image_write; } FuFirmware * fu_bcm57xx_stage2_image_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_BCM57XX_STAGE2_IMAGE, NULL)); } fwupd-2.0.10/plugins/bcm57xx/fu-bcm57xx-stage2-image.h000066400000000000000000000006311501337203100222110ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_BCM57XX_STAGE2_IMAGE (fu_bcm57xx_stage2_image_get_type()) G_DECLARE_FINAL_TYPE(FuBcm57xxStage2Image, fu_bcm57xx_stage2_image, FU, BCM57XX_STAGE2_IMAGE, FuFirmware) FuFirmware * fu_bcm57xx_stage2_image_new(void); fwupd-2.0.10/plugins/bcm57xx/fu-self-test.c000066400000000000000000000121551501337203100203560ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-bcm57xx-common.h" #include "fu-bcm57xx-dict-image.h" #include "fu-bcm57xx-firmware.h" #include "fu-bcm57xx-stage1-image.h" #include "fu-bcm57xx-stage2-image.h" static void fu_bcm57xx_create_verbuf(guint8 *bufver, gsize bufsz, const gchar *version) { gboolean ret; gsize versionsz = strlen(version) + 1; g_autoptr(GError) error = NULL; ret = fu_memcpy_safe(bufver, bufsz, 0x0, (const guint8 *)version, versionsz, 0x0, versionsz, &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_bcm57xx_common_veritem_func(void) { g_autoptr(Bcm57xxVeritem) veritem1 = NULL; g_autoptr(Bcm57xxVeritem) veritem2 = NULL; g_autoptr(Bcm57xxVeritem) veritem3 = NULL; guint8 bufver[16] = {0x0}; fu_bcm57xx_create_verbuf(bufver, sizeof(bufver), "5719-v1.43"); veritem1 = fu_bcm57xx_veritem_new(bufver, sizeof(bufver)); g_assert_nonnull(veritem1); g_assert_cmpstr(veritem1->version, ==, "1.43"); g_assert_cmpstr(veritem1->branch, ==, BCM_FW_BRANCH_UNKNOWN); g_assert_cmpint(veritem1->verfmt, ==, FWUPD_VERSION_FORMAT_PAIR); fu_bcm57xx_create_verbuf(bufver, sizeof(bufver), "stage1-0.4.391"); veritem2 = fu_bcm57xx_veritem_new(bufver, sizeof(bufver)); g_assert_nonnull(veritem2); g_assert_cmpstr(veritem2->version, ==, "0.4.391"); g_assert_cmpstr(veritem2->branch, ==, BCM_FW_BRANCH_OSS_FIRMWARE); g_assert_cmpint(veritem2->verfmt, ==, FWUPD_VERSION_FORMAT_TRIPLET); fu_bcm57xx_create_verbuf(bufver, sizeof(bufver), "RANDOM-7"); veritem3 = fu_bcm57xx_veritem_new(bufver, sizeof(bufver)); g_assert_nonnull(veritem3); g_assert_cmpstr(veritem3->version, ==, "RANDOM-7"); g_assert_cmpstr(veritem3->branch, ==, BCM_FW_BRANCH_UNKNOWN); g_assert_cmpint(veritem3->verfmt, ==, FWUPD_VERSION_FORMAT_UNKNOWN); } static void fu_bcm57xx_firmware_talos_func(void) { gboolean ret; g_autofree gchar *fn = NULL; g_autofree gchar *fn_out = NULL; g_autoptr(GBytes) blob = NULL; g_autoptr(GBytes) blob_out = NULL; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) images = NULL; g_autoptr(FuFirmware) firmware = fu_bcm57xx_firmware_new(); /* load file */ fn = g_test_build_filename(G_TEST_DIST, "tests", "Bcm5719_talos.bin", NULL); if (!g_file_test(fn, G_FILE_TEST_EXISTS)) { g_test_skip("missing file"); return; } blob = fu_bytes_get_contents(fn, &error); g_assert_no_error(error); g_assert_nonnull(blob); ret = fu_firmware_parse_bytes(firmware, blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH | FU_FIRMWARE_PARSE_FLAG_CACHE_STREAM, &error); g_assert_no_error(error); g_assert_true(ret); images = fu_firmware_get_images(firmware); g_assert_cmpint(images->len, ==, 6); blob_out = fu_firmware_write(firmware, &error); g_assert_no_error(error); g_assert_nonnull(blob_out); fn_out = g_test_build_filename(G_TEST_BUILT, "tests", "Bcm5719_talos.bin", NULL); ret = fu_bytes_set_contents(fn_out, blob_out, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_bytes_compare(blob, blob_out, &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_bcm57xx_firmware_xml_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autofree gchar *csum1 = NULL; g_autofree gchar *csum2 = NULL; g_autofree gchar *xml_out = NULL; g_autofree gchar *xml_src = NULL; g_autoptr(FuFirmware) firmware1 = fu_bcm57xx_firmware_new(); g_autoptr(FuFirmware) firmware2 = fu_bcm57xx_firmware_new(); g_autoptr(GError) error = NULL; /* build and write */ filename = g_test_build_filename(G_TEST_DIST, "tests", "bcm57xx.builder.xml", NULL); ret = g_file_get_contents(filename, &xml_src, NULL, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_firmware_build_from_xml(firmware1, xml_src, &error); g_assert_no_error(error); g_assert_true(ret); csum1 = fu_firmware_get_checksum(firmware1, G_CHECKSUM_SHA1, &error); g_assert_no_error(error); g_assert_cmpstr(csum1, ==, "a3ac108905c37857cf48612b707c1c72c582f914"); /* ensure we can round-trip */ xml_out = fu_firmware_export_to_xml(firmware1, FU_FIRMWARE_EXPORT_FLAG_NONE, &error); g_assert_no_error(error); ret = fu_firmware_build_from_xml(firmware2, xml_out, &error); g_assert_no_error(error); g_assert_true(ret); csum2 = fu_firmware_get_checksum(firmware2, G_CHECKSUM_SHA1, &error); g_assert_cmpstr(csum1, ==, csum2); } int main(int argc, char **argv) { (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); (void)g_setenv("G_MESSAGES_DEBUG", "all", TRUE); g_test_init(&argc, &argv, NULL); g_type_ensure(FU_TYPE_BCM57XX_STAGE1_IMAGE); g_type_ensure(FU_TYPE_BCM57XX_STAGE2_IMAGE); g_type_ensure(FU_TYPE_BCM57XX_DICT_IMAGE); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); /* tests go here */ g_test_add_func("/fwupd/bcm57xx/firmware{xml}", fu_bcm57xx_firmware_xml_func); g_test_add_func("/fwupd/bcm57xx/firmware{talos}", fu_bcm57xx_firmware_talos_func); g_test_add_func("/fwupd/bcm57xx/common{veritem}", fu_bcm57xx_common_veritem_func); return g_test_run(); } fwupd-2.0.10/plugins/bcm57xx/meson.build000066400000000000000000000031171501337203100200340ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginBcm57xx"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('bcm57xx.quirk') plugin_builtin_bcm57xx = static_library('fu_plugin_bcm57xx', sources: [ 'fu-bcm57xx-plugin.c', 'fu-bcm57xx-common.c', # fuzzing 'fu-bcm57xx-device.c', 'fu-bcm57xx-dict-image.c', # fuzzing 'fu-bcm57xx-firmware.c', # fuzzing 'fu-bcm57xx-recovery-device.c', 'fu-bcm57xx-stage1-image.c', # fuzzing 'fu-bcm57xx-stage2-image.c', # fuzzing ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: [ plugin_deps, valgrind, ], ) plugin_builtins += plugin_builtin_bcm57xx enumeration_data += files('tests/bcm5719-setup.json') device_tests += files( 'tests/bcm5719.json', 'tests/dell-kh08p.json', ) if get_option('tests') install_data(['tests/bcm57xx.builder.xml'], install_dir: join_paths(installed_test_datadir, 'tests')) env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) e = executable( 'bcm57xx-self-test', sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: plugin_deps, link_with: [ plugin_libs, plugin_builtin_bcm57xx, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('bcm57xx-self-test', e, env: env) endif endif fwupd-2.0.10/plugins/bcm57xx/tests/000077500000000000000000000000001501337203100170325ustar00rootroot00000000000000fwupd-2.0.10/plugins/bcm57xx/tests/.gitignore000066400000000000000000000000221501337203100210140ustar00rootroot00000000000000Bcm5719_talos.bin fwupd-2.0.10/plugins/bcm57xx/tests/bcm5719-setup.json000066400000000000000000000055371501337203100221640ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUdevDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:1c.0/0000:04:00.0/0000:05:04.0/0000:2d:00.0/0000:2e:04.0/0000:51:00.0", "Subsystem": "pci", "Driver": "tg3", "BindId": "0000:51:00.0", "Vendor": 5348, "Model": 5719, "Created": "2024-10-25T11:38:39.920300Z", "Events": [ { "Id": "#d5a801ad", "Data": "pci" }, { "Id": "#ad8c58d3", "Data": "tg3" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "DRIVER=tg3\nPCI_CLASS=20000\nPCI_ID=14E4:1657\nPCI_SUBSYS_ID=14E4:1904\nPCI_SLOT_NAME=0000:51:00.0\nMODALIAS=pci:v000014E4d00001657sv000014E4sd00001904bc02sc00i00" }, { "Id": "#d432c663" }, { "Id": "#9b895db2", "Data": "0x14e4" }, { "Id": "#66f3e150", "Data": "0x1657" }, { "Id": "#d410b6c7", "Data": "0x020000" }, { "Id": "#1075ed5c" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "DRIVER=tg3\nPCI_CLASS=20000\nPCI_ID=14E4:1657\nPCI_SUBSYS_ID=14E4:1904\nPCI_SLOT_NAME=0000:51:00.0\nMODALIAS=pci:v000014E4d00001657sv000014E4sd00001904bc02sc00i00" }, { "Id": "#d432c663" }, { "Id": "#9b895db2", "Data": "0x14e4" }, { "Id": "#66f3e150", "Data": "0x1657" }, { "Id": "#d410b6c7", "Data": "0x020000" }, { "Id": "#1075ed5c" }, { "Id": "#d410b6c7", "Data": "0x020000" }, { "Id": "#bf29d2f6", "Data": "0x00" }, { "Id": "#269abd81", "Data": "0x14e4" }, { "Id": "#360cec38", "Data": "0x1904" }, { "Id": "#d2629d83", "Data": "0000:51:00.0" }, { "Id": "#d2629d83", "Data": "0000:51:00.0" }, { "Id": "#bddbca22", "Data": "DRIVER=tg3\nPCI_CLASS=20000\nPCI_ID=14E4:1657\nPCI_SUBSYS_ID=14E4:1904\nPCI_SLOT_NAME=0000:51:00.0\nMODALIAS=pci:v000014E4d00001657sv000014E4sd00001904bc02sc00i00" }, { "Id": "#16ecb14e", "DataOut": "AwAAAHRnMwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANi4xMS4zLTIwMC5mYzQwLng4Nl82NAAAAAAAAAAAAABGRlY3LjYuMTQgYmMgc3RhZ2UxLTAuNC40NAAAAAAAADAwMDA6NTE6MDAuMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASAAAAAgAAAAAAAgAAIAAAA==" }, { "Id": "#6bb0ef32", "DataOut": "CwAAAKpVmWaYAgAABAAAAAAEACw=" } ] } ] } fwupd-2.0.10/plugins/bcm57xx/tests/bcm5719.json000066400000000000000000000005441501337203100210170ustar00rootroot00000000000000{ "name": "Broadcom BCM5719", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/bcm5719-setup.json", "components": [ { "version": "0.4.44", "branch": "oss-firmware", "guids": [ "30fe13b6-aa73-5d8c-a19f-c7b600f0117a" ] } ] } ] } fwupd-2.0.10/plugins/bcm57xx/tests/bcm57xx.builder.xml000066400000000000000000000007541501337203100225040ustar00rootroot00000000000000 1.2.3 0x123456 stage1 0x01 aGVsbG8gd29ybGQ= stage2 ape 0x7 aGVsbG8gd29ybGQ= fwupd-2.0.10/plugins/bcm57xx/tests/dell-kh08p.json000066400000000000000000000012061501337203100215740ustar00rootroot00000000000000{ "name": "Dell KH08P [BCM5719]", "interactive": false, "steps": [ { "url": "6cf165037a381eb29c183319e031def6b87e3ce955781ecf73f28751a1365db2-kh08p-bcm5719-0.4.62.cab", "components": [ { "version": "0.4.62", "guids": [ "ec5b8a9e-973b-58cc-935b-8322fabaebe9" ] } ] }, { "url": "c786be1c525ad062c5af8983474a9412f83f5251efb767fe9cb414a3a124b8ce-kh08p-bcm5719-0.4.64.cab", "components": [ { "version": "0.4.64", "guids": [ "ec5b8a9e-973b-58cc-935b-8322fabaebe9" ] } ] } ] } fwupd-2.0.10/plugins/bios/000077500000000000000000000000001501337203100153275ustar00rootroot00000000000000fwupd-2.0.10/plugins/bios/README.md000066400000000000000000000005341501337203100166100ustar00rootroot00000000000000--- title: Plugin: BIOS --- ## Introduction This plugin checks UEFI capsules are available, and if missing a HSI failure is reported. ## External Interface Access This plugin requires read only access to attributes located within `/sys/firmware/efi/esrt`. ## Version Considerations This plugin has been available since fwupd version `1.5.0`. fwupd-2.0.10/plugins/bios/fu-bios-plugin.c000066400000000000000000000053211501337203100203340ustar00rootroot00000000000000/* * Copyright 2020 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-bios-plugin.h" struct _FuBiosPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuBiosPlugin, fu_bios_plugin, FU_TYPE_PLUGIN) static gboolean fu_bios_plugin_startup(FuPlugin *plugin, FuProgress *progress, GError **error) { FuContext *ctx = fu_plugin_get_context(plugin); const gchar *vendor; vendor = fu_context_get_hwid_value(ctx, FU_HWIDS_KEY_BIOS_VENDOR); if (g_strcmp0(vendor, "coreboot") == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "system uses coreboot"); return FALSE; } return TRUE; } static gboolean fu_bios_plugin_coldplug(FuPlugin *plugin, FuProgress *progress, GError **error) { FuContext *ctx = fu_plugin_get_context(plugin); FuEfivars *efivars = fu_context_get_efivars(ctx); g_autofree gchar *sysfsfwdir = NULL; g_autofree gchar *esrt_path = NULL; g_autoptr(GError) error_local = NULL; /* are the EFI dirs set up so we can update each device */ if (!fu_efivars_supported(efivars, &error_local)) { fu_plugin_add_flag(plugin, FWUPD_PLUGIN_FLAG_LEGACY_BIOS); fu_plugin_add_flag(plugin, FWUPD_PLUGIN_FLAG_USER_WARNING); return TRUE; } /* get the directory of ESRT entries */ sysfsfwdir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR_FW); esrt_path = g_build_filename(sysfsfwdir, "efi", "esrt", NULL); if (!g_file_test(esrt_path, G_FILE_TEST_IS_DIR)) { fu_plugin_add_flag(plugin, FWUPD_PLUGIN_FLAG_CAPSULES_UNSUPPORTED); fu_plugin_add_flag(plugin, FWUPD_PLUGIN_FLAG_USER_WARNING); return TRUE; } /* we appear to have UEFI capsule updates */ fu_plugin_add_flag(plugin, FWUPD_PLUGIN_FLAG_DISABLED); return TRUE; } static void fu_bios_plugin_add_security_attrs(FuPlugin *plugin, FuSecurityAttrs *attrs) { g_autoptr(FwupdSecurityAttr) attr = NULL; if (!fu_plugin_has_flag(plugin, FWUPD_PLUGIN_FLAG_LEGACY_BIOS)) return; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_UEFI_SECUREBOOT); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_ENABLED); fu_security_attrs_append(attrs, attr); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); } static void fu_bios_plugin_init(FuBiosPlugin *self) { } static void fu_bios_plugin_class_init(FuBiosPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->startup = fu_bios_plugin_startup; plugin_class->coldplug = fu_bios_plugin_coldplug; plugin_class->add_security_attrs = fu_bios_plugin_add_security_attrs; } fwupd-2.0.10/plugins/bios/fu-bios-plugin.h000066400000000000000000000003461501337203100203430ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuBiosPlugin, fu_bios_plugin, FU, BIOS_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/bios/meson.build000066400000000000000000000006031501337203100174700ustar00rootroot00000000000000if allow_uefi_capsule and (host_cpu == 'x86' or host_cpu == 'x86_64') cargs = ['-DG_LOG_DOMAIN="FuPluginBios"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_builtins += static_library('fu_plugin_bios', sources: [ 'fu-bios-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/bnr-dp/000077500000000000000000000000001501337203100155555ustar00rootroot00000000000000fwupd-2.0.10/plugins/bnr-dp/README.md000066400000000000000000000036021501337203100170350ustar00rootroot00000000000000--- title: Plugin: B&R DisplayPort receiver --- ## Introduction This plugin updates the firmware of DisplayPort receivers by B&R Industrial Automation GmbH over the DisplayPort aux channel. ## Firmware Format The daemon will decompress the cabinet archive and extract a single firmware blob in a binary file format. The firmware file includes a proprietary XML header with general metadata for update tools. The plugin makes use of this data as needed. The XML header is separated by a null byte from the actual firmware payload that needs to be written to the device in chunks. This plugin supports the following protocol ID: - `com.br-automation.dpaux` ## GUID Generation These devices build their GUIDs by adding the `OUI` from the DpAux DPCD with additional B&R-specific device, variant and revision metadata, e.g. - `DPAUX\OUI_00006065&DEV_00002F1A&VARIANT_00000000&HW_REV_A0` ## Update Behavior During normal runtime, the firmware is written to the low/high (A/B) section that is currently not in use. The boot counter needs to be adjusted appropriately to ensure that this newly written firmware is the one that is preferred when the device boots. To activate the new firmware a reset of the device is necessary, this will cause a brief screen flicker. Aside from the brief restart at the end of the update, the device stays usable during most of the update procedure. ## Vendor ID Security The vendor ID is set from the OUI, i.e. `OUI:006065*`. ## Quirk Use This plugin currently does not use any plugin-specific quirks. ## External Interface Access This plugin requires read/write access to `/dev/drm_dp_aux*`. ## Version Considerations This plugin has been available since fwupd version `2.0.7`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: - Thomas Mühlbacher: @tmuehlbacher-bnr fwupd-2.0.10/plugins/bnr-dp/bnr-dp.quirk000066400000000000000000000000431501337203100200110ustar00rootroot00000000000000[DPAUX\OUI_006065] Plugin = bnr_dp fwupd-2.0.10/plugins/bnr-dp/fu-bnr-dp-common.c000066400000000000000000000026061501337203100210050ustar00rootroot00000000000000/* * Copyright 2024 B&R Industrial Automation GmbH * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fwupdplugin.h" #include "fu-bnr-dp-common.h" #include "fu-bnr-dp-struct.h" gchar * fu_bnr_dp_version_to_string(guint64 version) { guint64 major = version / 100; guint64 minor = version % 100; return g_strdup_printf("%" G_GUINT64_FORMAT ".%02" G_GUINT64_FORMAT, major, minor); } /* read, convert and validate the version from `header` to an integer */ gboolean fu_bnr_dp_version_from_header(const FuStructBnrDpPayloadHeader *st_header, guint64 *version, GError **error) { g_autofree gchar *tmp = fu_struct_bnr_dp_payload_header_get_version(st_header); return fu_strtoull(tmp, version, 0, 9999, FU_INTEGER_BASE_10, error); } guint32 fu_bnr_dp_effective_product_num(const FuStructBnrDpFactoryData *st_factory_data) { guint32 parent = fu_struct_bnr_dp_factory_data_get_parent_product_num(st_factory_data); return (parent != 0 && parent != G_MAXUINT32) ? parent : fu_struct_bnr_dp_factory_data_get_product_num(st_factory_data); } guint16 fu_bnr_dp_effective_compat_id(const FuStructBnrDpFactoryData *st_factory_data) { guint16 parent = fu_struct_bnr_dp_factory_data_get_parent_compat_id(st_factory_data); return (parent != 0 && parent != G_MAXUINT16) ? parent : fu_struct_bnr_dp_factory_data_get_compat_id(st_factory_data); } fwupd-2.0.10/plugins/bnr-dp/fu-bnr-dp-common.h000066400000000000000000000011071501337203100210050ustar00rootroot00000000000000/* * Copyright 2024 B&R Industrial Automation GmbH * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-bnr-dp-struct.h" gchar * fu_bnr_dp_version_to_string(guint64 version); gboolean fu_bnr_dp_version_from_header(const FuStructBnrDpPayloadHeader *st_header, guint64 *version, GError **error) G_GNUC_NON_NULL(1, 2); guint32 fu_bnr_dp_effective_product_num(const FuStructBnrDpFactoryData *st_factory_data) G_GNUC_NON_NULL(1); guint16 fu_bnr_dp_effective_compat_id(const FuStructBnrDpFactoryData *st_factory_data) G_GNUC_NON_NULL(1); fwupd-2.0.10/plugins/bnr-dp/fu-bnr-dp-device.c000066400000000000000000000642501501337203100207570ustar00rootroot00000000000000/* * Copyright 2024 B&R Industrial Automation GmbH * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-bnr-dp-common.h" #include "fu-bnr-dp-device.h" #include "fu-bnr-dp-firmware.h" #include "fu-bnr-dp-struct.h" #define FU_BNR_DP_IEEE_OUI 0x006065 #define FU_BNR_DP_DEVICE_HEADER_OFFSET 0x00A00 #define FU_BNR_DP_DEVICE_DATA_OFFSET 0x00900 #define FU_BNR_DP_DEVICE_DATA_CHUNK_SIZE 256 #define FU_BNR_DP_DEVICE_FLASH_PAGE_SIZE 65536 /* timeout in ms for aux reads/writes */ #define FU_BNR_DP_DEVICE_DPAUX_TIMEOUT_MSEC 3000 /* maximum number of polls to attempt without delay and in total. some commands will finish pretty * quickly, but more elaborate commands can take some time and a delay becomes appropriate when * polling */ #define FU_BNR_DP_DEVICE_POLL_MAX_FAST 10 #define FU_BNR_DP_DEVICE_POLL_MAX_TOTAL 100 #define FU_BNR_DP_DEVICE_POLL_INTERVAL_MSEC 5 struct _FuBnrDpDevice { FuDpauxDevice parent_instance; }; G_DEFINE_TYPE(FuBnrDpDevice, fu_bnr_dp_device, FU_TYPE_DPAUX_DEVICE) static guint8 fu_bnr_dp_device_xor_checksum(guint8 init, const guint8 *buf, gsize bufsz) { for (gsize i = 0; i < bufsz; i++) init ^= buf[i]; return init; } static FuStructBnrDpAuxRequest * fu_bnr_dp_device_build_request(FuBnrDpOpcodes opcode, FuBnrDpModuleNumber module_number, guint16 offset, guint16 data_len, GError **error) { g_autoptr(FuStructBnrDpAuxRequest) st_request = fu_struct_bnr_dp_aux_request_new(); g_autoptr(FuStructBnrDpAuxCommand) st_command = fu_struct_bnr_dp_aux_command_new(); fu_struct_bnr_dp_aux_command_set_module_number(st_command, module_number); fu_struct_bnr_dp_aux_command_set_opcode(st_command, opcode); if (!fu_struct_bnr_dp_aux_request_set_command(st_request, st_command, error)) return NULL; fu_struct_bnr_dp_aux_request_set_data_len(st_request, data_len); fu_struct_bnr_dp_aux_request_set_offset(st_request, offset); return g_steal_pointer(&st_request); } /* evaluate the status from a response from the controller into an appropriate bool/GError */ static gboolean fu_bnr_dp_device_eval_result(const FuStructBnrDpAuxStatus *st_status, GError **error) { guint8 error_byte = fu_struct_bnr_dp_aux_status_get_error(st_status); guint8 error_code = error_byte & 0x0F; if (error_byte & FU_BNR_DP_AUX_STATUS_FLAGS_ERROR) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "device command failed with error '%s'", fu_bnr_dp_aux_error_to_string(error_code) ?: "(invalid error code)"); return FALSE; } return TRUE; } static gboolean fu_bnr_dp_device_is_done(const FuStructBnrDpAuxStatus *st_status, GError **error) { guint8 error_byte = fu_struct_bnr_dp_aux_status_get_error(st_status); if (error_byte & FU_BNR_DP_AUX_STATUS_FLAGS_BUSY) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "device is busy"); return FALSE; } return TRUE; } /* write a single `request` and some optional data to the device */ static gboolean fu_bnr_dp_device_write_request(FuBnrDpDevice *self, const FuStructBnrDpAuxRequest *st_request, const guint8 *buf, gsize bufsz, GError **error) { guint8 checksum; g_autoptr(FuStructBnrDpAuxTxHeader) st_header = fu_struct_bnr_dp_aux_tx_header_new(); if (!fu_struct_bnr_dp_aux_tx_header_set_request(st_header, st_request, error)) { g_prefix_error(error, "failed to set request: "); return FALSE; } /* write optional data */ checksum = fu_bnr_dp_device_xor_checksum(FU_BNR_DP_CHECKSUM_INIT_TX, st_request->data, st_request->len); if (buf != NULL && bufsz > 0) { if (!fu_dpaux_device_write(FU_DPAUX_DEVICE(self), FU_BNR_DP_DEVICE_DATA_OFFSET, buf, bufsz, FU_BNR_DP_DEVICE_DPAUX_TIMEOUT_MSEC, error)) { g_prefix_error(error, "failed to write request: "); return FALSE; } checksum = fu_bnr_dp_device_xor_checksum(checksum, buf, bufsz); } fu_struct_bnr_dp_aux_tx_header_set_checksum(st_header, checksum); /* write header to kick off processing by the device */ return fu_dpaux_device_write(FU_DPAUX_DEVICE(self), FU_BNR_DP_DEVICE_HEADER_OFFSET, st_header->data, st_header->len, FU_BNR_DP_DEVICE_DPAUX_TIMEOUT_MSEC, error); } /* read a single `response` and some optional data from the device after a finished command. reading * the full 7 byte header from the header offset returns a different structure than when reading * only 2 bytes */ static gboolean fu_bnr_dp_device_read_response(FuBnrDpDevice *self, GByteArray *data, GError **error) { guint8 actual_checksum; guint8 tmp[FU_STRUCT_BNR_DP_AUX_RX_HEADER_SIZE] = {0}; g_autoptr(FuStructBnrDpAuxRxHeader) st_header = NULL; g_autoptr(FuStructBnrDpAuxResponse) st_response = NULL; /* read full header once command has finished */ if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), FU_BNR_DP_DEVICE_HEADER_OFFSET, tmp, sizeof(tmp), FU_BNR_DP_DEVICE_DPAUX_TIMEOUT_MSEC, error)) return FALSE; st_header = fu_struct_bnr_dp_aux_rx_header_parse(tmp, sizeof(tmp), 0, error); if (st_header == NULL) return FALSE; st_response = fu_struct_bnr_dp_aux_rx_header_get_response(st_header); if (st_response == NULL) return FALSE; actual_checksum = fu_bnr_dp_device_xor_checksum(FU_BNR_DP_CHECKSUM_INIT_RX, st_response->data, st_response->len); /* read command output data */ g_byte_array_set_size(data, fu_struct_bnr_dp_aux_response_get_data_len(st_response)); if (data->len > 0) { if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), FU_BNR_DP_DEVICE_DATA_OFFSET, data->data, data->len, FU_BNR_DP_DEVICE_DPAUX_TIMEOUT_MSEC, error)) return FALSE; actual_checksum = fu_bnr_dp_device_xor_checksum(actual_checksum, data->data, data->len); } if (actual_checksum != fu_struct_bnr_dp_aux_rx_header_get_checksum(st_header)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "checksum mismatch in device response header (header specified: 0x%X, " "actual: 0x%X)", fu_struct_bnr_dp_aux_rx_header_get_checksum(st_header), actual_checksum); return FALSE; } return TRUE; } /* read only 2 bytes from the header offset to receive the status */ static FuStructBnrDpAuxStatus * fu_bnr_dp_device_read_status(FuBnrDpDevice *self, GError **error) { guint8 buf[FU_STRUCT_BNR_DP_AUX_STATUS_SIZE] = {0}; g_autoptr(FuStructBnrDpAuxStatus) st_status = NULL; /* only read the first 2 bytes of the header to check status bits */ if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), FU_BNR_DP_DEVICE_HEADER_OFFSET, buf, sizeof(buf), FU_BNR_DP_DEVICE_DPAUX_TIMEOUT_MSEC, error)) return NULL; st_status = fu_struct_bnr_dp_aux_status_parse(buf, sizeof(buf), 0, error); if (st_status == NULL) return NULL; return g_steal_pointer(&st_status); } static gboolean fu_bnr_dp_device_poll_status_cb(FuDevice *device, gpointer user_data, GError **error) { g_autoptr(FuStructBnrDpAuxStatus) st_status = NULL; st_status = fu_bnr_dp_device_read_status(FU_BNR_DP_DEVICE(device), error); if (st_status == NULL) return FALSE; if (!fu_bnr_dp_device_eval_result(st_status, error)) return FALSE; return fu_bnr_dp_device_is_done(st_status, error); } static gboolean fu_bnr_dp_device_poll_status(FuBnrDpDevice *self, GError **error) { if (fu_device_retry_full(FU_DEVICE(self), fu_bnr_dp_device_poll_status_cb, FU_BNR_DP_DEVICE_POLL_MAX_FAST, 0, NULL, NULL)) return TRUE; return fu_device_retry_full(FU_DEVICE(self), fu_bnr_dp_device_poll_status_cb, FU_BNR_DP_DEVICE_POLL_MAX_TOTAL - FU_BNR_DP_DEVICE_POLL_MAX_FAST, FU_BNR_DP_DEVICE_POLL_INTERVAL_MSEC, NULL, error); } static GByteArray * fu_bnr_dp_device_exec_cmd(FuBnrDpDevice *self, FuBnrDpOpcodes opcode, FuBnrDpModuleNumber module_number, guint16 offset, GError **error) { g_autoptr(FuStructBnrDpAuxRequest) st_request = NULL; g_autoptr(GByteArray) buf = g_byte_array_new(); st_request = fu_bnr_dp_device_build_request(opcode, module_number, offset, 0, error); if (st_request == NULL) return NULL; if (!fu_bnr_dp_device_write_request(self, st_request, NULL, 0, error)) return NULL; if (!fu_bnr_dp_device_poll_status(self, error)) { g_prefix_error(error, "command %s to module %s at offset 0x%X: ", fu_bnr_dp_opcodes_to_string(opcode), fu_bnr_dp_module_number_to_string(module_number), offset); return NULL; } if (!fu_bnr_dp_device_read_response(self, buf, error)) return NULL; /* success */ return g_steal_pointer(&buf); } static GByteArray * fu_bnr_dp_device_read_data(FuBnrDpDevice *self, FuBnrDpOpcodes opcode, FuBnrDpModuleNumber module_number, gsize offset, gsize size, FuProgress *progress, GError **error) { const guint16 start = offset / FU_BNR_DP_DEVICE_DATA_CHUNK_SIZE; const guint16 end = (offset + size) / FU_BNR_DP_DEVICE_DATA_CHUNK_SIZE; g_autoptr(GByteArray) buf = g_byte_array_sized_new(size); g_return_val_if_fail(offset % FU_BNR_DP_DEVICE_DATA_CHUNK_SIZE == 0, NULL); g_return_val_if_fail(size % FU_BNR_DP_DEVICE_DATA_CHUNK_SIZE == 0, NULL); g_return_val_if_fail(start < end, NULL); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, end - start); for (guint16 idx = start; idx < end; idx++) { g_autoptr(GByteArray) chunk = NULL; chunk = fu_bnr_dp_device_exec_cmd(self, opcode, module_number, idx, error); if (chunk == NULL) return NULL; g_byte_array_append(buf, chunk->data, chunk->len); fu_progress_step_done(progress); } return g_steal_pointer(&buf); } /* check if the current chunk can be skipped. this is a flash optimization. writing to start of page * erases full block and allows us to skip further writes to that page if the chunk is entirely * 0xff */ static gboolean fu_bnr_dp_device_can_skip_chunk(const guint8 *buf, gsize bufsz, gsize cur_offset) { g_return_val_if_fail(cur_offset + FU_BNR_DP_DEVICE_DATA_CHUNK_SIZE <= bufsz, FALSE); /* can't skip the first chunk in a flash page */ if ((cur_offset % FU_BNR_DP_DEVICE_FLASH_PAGE_SIZE) == 0) return FALSE; /* can't skip if any byte in the chunk is not 0xff */ for (gsize i = cur_offset; i < cur_offset + FU_BNR_DP_DEVICE_DATA_CHUNK_SIZE; i++) { if (buf[i] != 0xff) return FALSE; } /* can skip */ return TRUE; } static gboolean fu_bnr_dp_device_write_data(FuBnrDpDevice *self, FuBnrDpOpcodes opcode, FuBnrDpModuleNumber module_number, gsize offset, const guint8 *buf, gsize bufsz, FuProgress *progress, GError **error) { const guint16 start = offset / FU_BNR_DP_DEVICE_DATA_CHUNK_SIZE; const guint16 end = (offset + bufsz) / FU_BNR_DP_DEVICE_DATA_CHUNK_SIZE; g_autoptr(FuStructBnrDpAuxRequest) st_request = NULL; g_return_val_if_fail(offset % FU_BNR_DP_DEVICE_DATA_CHUNK_SIZE == 0, FALSE); g_return_val_if_fail(bufsz % FU_BNR_DP_DEVICE_DATA_CHUNK_SIZE == 0, FALSE); g_return_val_if_fail(start < end, FALSE); st_request = fu_bnr_dp_device_build_request(opcode, module_number, 0, FU_BNR_DP_DEVICE_DATA_CHUNK_SIZE, error); if (st_request == NULL) { g_prefix_error(error, "failed to build request: "); return FALSE; } fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, end - start); for (guint16 idx = start; idx < end; idx++) { gsize cur_offset = idx * FU_BNR_DP_DEVICE_DATA_CHUNK_SIZE; if (fu_bnr_dp_device_can_skip_chunk(buf, bufsz, cur_offset)) { fu_progress_step_done(progress); continue; } fu_struct_bnr_dp_aux_request_set_offset(st_request, idx); if (!fu_bnr_dp_device_write_request(self, st_request, &buf[cur_offset], FU_BNR_DP_DEVICE_DATA_CHUNK_SIZE, error)) { g_prefix_error(error, "failed @0x%x (idx: 0x%x): ", (guint)cur_offset, idx); return FALSE; } if (!fu_bnr_dp_device_poll_status(self, error)) { g_prefix_error(error, "command %s to module %s at offset 0x%X: ", fu_bnr_dp_opcodes_to_string(opcode), fu_bnr_dp_module_number_to_string(module_number), idx); return FALSE; } fu_progress_step_done(progress); } return TRUE; } static FuStructBnrDpPayloadHeader * fu_bnr_dp_device_factory_data(FuBnrDpDevice *self, FuBnrDpModuleNumber module_number, GError **error) { g_autoptr(GByteArray) buf = NULL; buf = fu_bnr_dp_device_exec_cmd(self, FU_BNR_DP_OPCODES_FACTORY_DATA, module_number, 0x0, error); if (buf == NULL) { g_prefix_error_literal(error, "failed to read device factory data: "); return NULL; } return fu_struct_bnr_dp_factory_data_parse(buf->data, buf->len, 0, error); } /* read the fw header for the currently active firmware */ static FuStructBnrDpPayloadHeader * fu_bnr_dp_device_fw_header(FuBnrDpDevice *self, FuBnrDpModuleNumber module_number, GError **error) { g_autoptr(GByteArray) buf = NULL; buf = fu_bnr_dp_device_exec_cmd(self, FU_BNR_DP_OPCODES_FLASH_SAVE_HEADER_INFO, module_number, 0x0, error); if (buf == NULL) { g_prefix_error_literal(error, "failed to read device firmware header: "); return NULL; } return fu_struct_bnr_dp_payload_header_parse(buf->data, buf->len, 0, error); } /* read the fw header for the currently active firmware */ static FuStructBnrDpInfoFlags * fu_bnr_dp_device_info_flags(FuBnrDpDevice *self, FuBnrDpModuleNumber module_number, GError **error) { g_autoptr(GByteArray) buf = NULL; buf = fu_bnr_dp_device_exec_cmd(self, FU_BNR_DP_OPCODES_INFO_FLAGS, module_number, 0x0, error); if (buf == NULL) { g_prefix_error_literal(error, "failed to read device info flags: "); return NULL; } return fu_struct_bnr_dp_info_flags_parse(buf->data, buf->len, 0, error); } static gboolean fu_bnr_dp_device_reset(FuBnrDpDevice *self, FuBnrDpModuleNumber module_number, GError **error) { g_autoptr(FuStructBnrDpAuxRequest) st_request = NULL; st_request = fu_bnr_dp_device_build_request(FU_BNR_DP_OPCODES_RESET, module_number, 0xDEAD, 0, error); if (st_request == NULL) return FALSE; return fu_bnr_dp_device_write_request(self, st_request, NULL, 0, error); } static gboolean fu_bnr_dp_device_setup(FuDevice *device, GError **error) { FuBnrDpDevice *self = FU_BNR_DP_DEVICE(device); guint64 version = 0; g_autofree gchar *version_str = NULL; g_autofree gchar *id_str = NULL; g_autofree gchar *serial = NULL; g_autofree gchar *hw_rev = NULL; g_autofree gchar *oui = NULL; g_autoptr(FuStructBnrDpPayloadHeader) st_header = NULL; g_autoptr(FuStructBnrDpFactoryData) st_factory_data = NULL; /* FuDpauxDevice->setup */ if (!FU_DEVICE_CLASS(fu_bnr_dp_device_parent_class)->setup(device, error)) return FALSE; /* check if device dpcd matches before writing anything else to the device */ if (fu_dpaux_device_get_dpcd_ieee_oui(FU_DPAUX_DEVICE(device)) != FU_BNR_DP_IEEE_OUI) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "device id doesn't match"); return FALSE; } st_header = fu_bnr_dp_device_fw_header(self, FU_BNR_DP_MODULE_NUMBER_RECEIVER, error); if (st_header == NULL) return FALSE; st_factory_data = fu_bnr_dp_device_factory_data(self, FU_BNR_DP_MODULE_NUMBER_RECEIVER, error); if (st_factory_data == NULL) return FALSE; /* convert from string encoded version to integer and back to a nicer string format */ if (!fu_bnr_dp_version_from_header(st_header, &version, error)) return FALSE; version_str = fu_bnr_dp_version_to_string(version); fu_device_set_version(device, version_str); id_str = fu_struct_bnr_dp_factory_data_get_identification(st_factory_data); if (id_str == NULL) return FALSE; fu_device_set_name(FU_DEVICE(self), id_str); serial = fu_struct_bnr_dp_factory_data_get_serial(st_factory_data); if (serial == NULL) return FALSE; fu_device_set_serial(device, serial); fu_device_add_instance_u32(device, "DEV", fu_bnr_dp_effective_product_num(st_factory_data)); fu_device_add_instance_u32(device, "VARIANT", fu_bnr_dp_effective_compat_id(st_factory_data)); hw_rev = fu_struct_bnr_dp_factory_data_get_hw_rev(st_factory_data); if (hw_rev == NULL) return FALSE; fu_device_add_instance_str(device, "HW_REV", hw_rev); oui = g_strdup_printf("%06X", fu_dpaux_device_get_dpcd_ieee_oui(FU_DPAUX_DEVICE(device))); fu_device_build_vendor_id(FU_DEVICE(self), "OUI", oui); return fu_device_build_instance_id(device, error, "DPAUX", "OUI", "DEV", "VARIANT", "HW_REV", NULL); } static FuFirmware * fu_bnr_dp_device_read_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuBnrDpDevice *self = FU_BNR_DP_DEVICE(device); FuBnrDpPayloadFlags flags; gsize offset = FU_BNR_DP_FIRMWARE_SIZE; guint16 crc; g_autoptr(FuFirmware) firmware = fu_bnr_dp_firmware_new(); g_autoptr(GByteArray) image = NULL; g_autoptr(GBytes) bytes = NULL; g_autoptr(FuStructBnrDpFactoryData) st_factory_data = NULL; g_autoptr(FuStructBnrDpPayloadHeader) st_header = NULL; st_factory_data = fu_bnr_dp_device_factory_data(self, FU_BNR_DP_MODULE_NUMBER_RECEIVER, error); if (st_factory_data == NULL) return NULL; st_header = fu_bnr_dp_device_fw_header(self, FU_BNR_DP_MODULE_NUMBER_RECEIVER, error); if (st_header == NULL) return NULL; flags = fu_struct_bnr_dp_payload_header_get_flags(st_header); /* the flash is 3 * `FU_BNR_DP_FW_SIZE`; first third is boot loader, then low and high * images */ if ((flags & FU_BNR_DP_PAYLOAD_FLAGS_BOOT_AREA) == (guint)FU_BNR_DP_BOOT_AREA_HIGH) offset *= 2; image = fu_bnr_dp_device_read_data(self, FU_BNR_DP_OPCODES_FLASH_SERVICE, FU_BNR_DP_MODULE_NUMBER_RECEIVER, offset, FU_BNR_DP_FIRMWARE_SIZE, progress, error); if (image == NULL) return NULL; crc = fu_crc16(FU_CRC_KIND_B16_BNR, image->data, image->len); if (crc != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "CRC mismatch in read firmware image: 0x%Xu", crc); return NULL; } bytes = g_bytes_new(image->data, image->len); if (bytes == NULL) return NULL; fu_firmware_set_bytes(firmware, bytes); /* populate private data to be able to build an XML header if `firmware->write()` is used */ if (!fu_bnr_dp_firmware_parse_from_device(FU_BNR_DP_FIRMWARE(firmware), st_factory_data, st_header, error)) return NULL; /* success */ return g_steal_pointer(&firmware); } static GBytes * fu_bnr_dp_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuBnrDpDevice *self = FU_BNR_DP_DEVICE(device); g_autoptr(GByteArray) buf = NULL; buf = fu_bnr_dp_device_read_data(self, FU_BNR_DP_OPCODES_FLASH_SERVICE, FU_BNR_DP_MODULE_NUMBER_RECEIVER, 0, FU_BNR_DP_FIRMWARE_SIZE * 3, progress, error); if (buf == NULL) return NULL; return g_bytes_new(buf->data, buf->len); } static FuFirmware * fu_bnr_dp_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuBnrDpDevice *self = FU_BNR_DP_DEVICE(device); g_autoptr(FuFirmware) firmware = fu_bnr_dp_firmware_new(); g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(FuStructBnrDpFactoryData) st_factory_data = NULL; g_autoptr(FuStructBnrDpPayloadHeader) st_active_header = NULL; g_autoptr(FuStructBnrDpPayloadHeader) st_fw_header = NULL; g_autoptr(GBytes) bytes = NULL; /* parse to bnr-dp firmware */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; /* use bytes instead of stream to make patching work */ bytes = fu_firmware_get_bytes(firmware, error); if (bytes == NULL) return NULL; fu_firmware_set_bytes(firmware, bytes); locker = fu_device_locker_new(device, error); if (locker == NULL) { g_prefix_error_literal(error, "failed to get device locker to prepare firmware: "); return NULL; } /* patch firmware boot counter to be higher than active image */ st_active_header = fu_bnr_dp_device_fw_header(self, FU_BNR_DP_MODULE_NUMBER_RECEIVER, error); if (st_active_header == NULL) return NULL; if (!fu_bnr_dp_firmware_patch_boot_counter( FU_BNR_DP_FIRMWARE(firmware), fu_struct_bnr_dp_payload_header_get_counter(st_active_header), error)) { g_prefix_error_literal(error, "failed to patch firmware boot counter: "); return NULL; } /* check fw image */ st_factory_data = fu_bnr_dp_device_factory_data(self, FU_BNR_DP_MODULE_NUMBER_RECEIVER, error); if (st_factory_data == NULL) return NULL; st_fw_header = fu_struct_bnr_dp_payload_header_parse(g_bytes_get_data(bytes, NULL), g_bytes_get_size(bytes), FU_BNR_DP_FIRMWARE_HEADER_OFFSET, error); if (st_fw_header == NULL) return NULL; if (!fu_bnr_dp_firmware_check(FU_BNR_DP_FIRMWARE(firmware), st_factory_data, st_active_header, st_fw_header, flags, error)) return NULL; return g_steal_pointer(&firmware); } static gboolean fu_bnr_dp_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuBnrDpDevice *self = FU_BNR_DP_DEVICE(device); FuBnrDpBootArea boot_area_pre; FuBnrDpBootArea boot_area_post; g_autoptr(GBytes) bytes = NULL; g_autoptr(GByteArray) read_back = NULL; g_autoptr(FuStructBnrDpInfoFlags) st_info_flags_pre = NULL; g_autoptr(FuStructBnrDpInfoFlags) st_info_flags_post = NULL; /* progress, values based on dev tests with -vv */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 32, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 67, "verify"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "activate"); /* get payload bytes including patched boot counter */ bytes = fu_firmware_get_bytes_with_patches(firmware, error); if (bytes == NULL) return FALSE; st_info_flags_pre = fu_bnr_dp_device_info_flags(self, FU_BNR_DP_MODULE_NUMBER_RECEIVER, error); if (st_info_flags_pre == NULL) { g_prefix_error_literal( error, "failed to read device info flags before writing firmware: "); return FALSE; } /* write new firmware to inactive area */ if (!fu_bnr_dp_device_write_data(self, FU_BNR_DP_OPCODES_FLASH_USER, FU_BNR_DP_MODULE_NUMBER_RECEIVER, 0, g_bytes_get_data(bytes, NULL), g_bytes_get_size(bytes), fu_progress_get_child(progress), error)) { g_prefix_error_literal(error, "failed to write: "); return FALSE; } fu_progress_step_done(progress); /* verify written data */ read_back = fu_bnr_dp_device_read_data(self, FU_BNR_DP_OPCODES_FLASH_USER, FU_BNR_DP_MODULE_NUMBER_RECEIVER, 0, FU_BNR_DP_FIRMWARE_SIZE, fu_progress_get_child(progress), error); if (read_back == NULL) { g_prefix_error(error, "failed to read data: "); return FALSE; } if (!fu_memcmp_safe(g_bytes_get_data(bytes, NULL), g_bytes_get_size(bytes), 0, read_back->data, read_back->len, 0, FU_BNR_DP_FIRMWARE_SIZE, error)) { g_prefix_error_literal(error, "verification of written firmware failed: "); return FALSE; } fu_progress_step_done(progress); /* apply new firmware by resetting the device */ if (!fu_bnr_dp_device_reset(self, FU_BNR_DP_MODULE_NUMBER_RECEIVER, error)) { g_prefix_error(error, "failed to reset: "); return FALSE; } /* give controller some time before ->reload() tries to read info again */ fu_device_sleep(device, 3000); st_info_flags_post = fu_bnr_dp_device_info_flags(self, FU_BNR_DP_MODULE_NUMBER_RECEIVER, error); if (st_info_flags_post == NULL) { g_prefix_error_literal(error, "failed to read device info flags after writing firmware: "); return FALSE; } boot_area_pre = (fu_struct_bnr_dp_info_flags_get_inner(st_info_flags_pre) & FU_BNR_DP_INFO_FLAGS_BOOT_AREA); boot_area_post = (fu_struct_bnr_dp_info_flags_get_inner(st_info_flags_post) & FU_BNR_DP_INFO_FLAGS_BOOT_AREA); if (boot_area_pre == boot_area_post) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "boot area did not switch after device reset; still booted from %s area", fu_bnr_dp_boot_area_to_string(boot_area_post)); return FALSE; } fu_progress_step_done(progress); return TRUE; } static gchar * fu_bnr_dp_device_convert_version(FuDevice *self, guint64 version_raw) { return fu_bnr_dp_version_to_string(version_raw); } static void fu_bnr_dp_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_bnr_dp_device_init(FuBnrDpDevice *self) { fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PAIR); fu_device_set_vendor(FU_DEVICE(self), "B&R Industrial Automation GmbH"); fu_device_add_protocol(FU_DEVICE(self), "com.br-automation.dpaux"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_VIDEO_DISPLAY); fu_device_set_firmware_size_max(FU_DEVICE(self), FU_BNR_DP_FIRMWARE_SIZE_MAX); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_retry_add_recovery(FU_DEVICE(self), FWUPD_ERROR, FWUPD_ERROR_READ, NULL); } static void fu_bnr_dp_device_class_init(FuBnrDpDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->convert_version = fu_bnr_dp_device_convert_version; device_class->dump_firmware = fu_bnr_dp_device_dump_firmware; device_class->prepare_firmware = fu_bnr_dp_device_prepare_firmware; device_class->read_firmware = fu_bnr_dp_device_read_firmware; device_class->reload = fu_bnr_dp_device_setup; device_class->set_progress = fu_bnr_dp_device_set_progress; device_class->setup = fu_bnr_dp_device_setup; device_class->write_firmware = fu_bnr_dp_device_write_firmware; } fwupd-2.0.10/plugins/bnr-dp/fu-bnr-dp-device.h000066400000000000000000000004461501337203100207610ustar00rootroot00000000000000/* * Copyright 2024 B&R Industrial Automation GmbH * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_BNR_DP_DEVICE (fu_bnr_dp_device_get_type()) G_DECLARE_FINAL_TYPE(FuBnrDpDevice, fu_bnr_dp_device, FU, BNR_DP_DEVICE, FuDpauxDevice) fwupd-2.0.10/plugins/bnr-dp/fu-bnr-dp-firmware.c000066400000000000000000000426021501337203100213310ustar00rootroot00000000000000/* * Copyright 2024 B&R Industrial Automation GmbH * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-bnr-dp-common.h" #include "fu-bnr-dp-firmware.h" #include "fu-bnr-dp-struct.h" struct _FuBnrDpFirmware { FuFirmwareClass parent_instance; /* mandatory XML header attributes, not part of payload. additionally, "Ver" (version) is * also mandatory */ guint64 device_id; /* Dev */ gchar *usage; /* Use */ gchar function; /* Fct */ guint64 variant; /* Var */ guint64 payload_length; /* Len */ guint16 payload_checksum; /* Chk */ gchar *material; /* Mat */ gchar *creation_date; /* Date (nullable) */ gchar *comment; /* Rem (nullable) */ }; G_DEFINE_TYPE(FuBnrDpFirmware, fu_bnr_dp_firmware, FU_TYPE_FIRMWARE) static void fu_bnr_dp_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuBnrDpFirmware *self = FU_BNR_DP_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "device_id", self->device_id); fu_xmlb_builder_insert_kv(bn, "usage", self->usage); fu_xmlb_builder_insert_kx(bn, "function", self->function); fu_xmlb_builder_insert_kx(bn, "variant", self->variant); fu_xmlb_builder_insert_kx(bn, "payload_length", self->payload_length); fu_xmlb_builder_insert_kx(bn, "payload_checksum", self->payload_checksum); fu_xmlb_builder_insert_kv(bn, "material", self->material); fu_xmlb_builder_insert_kv(bn, "creation_date", self->creation_date); fu_xmlb_builder_insert_kv(bn, "comment", self->comment); } static gchar * fu_bnr_dp_firmware_convert_version(FuFirmware *self, guint64 version_raw) { return fu_bnr_dp_version_to_string(version_raw); } static gboolean fu_bnr_dp_firmware_attribute_parse_u64(XbNode *root, const gchar *attribute, guint64 *value, GError **error) { *value = xb_node_get_attr_as_uint(root, attribute); if (*value == G_MAXUINT64) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "missing or invalid header attribute: '%s'", attribute); return FALSE; } return TRUE; } static gchar * fu_bnr_dp_firmware_attribute_parse_string(XbNode *root, const gchar *attribute, GError **error) { const gchar *value; value = xb_node_get_attr(root, attribute); if (value == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "missing or invalid header attribute: '%s'", attribute); return NULL; } return g_strdup(value); } static gboolean fu_bnr_dp_firmware_header_parse(FuBnrDpFirmware *self, XbSilo *silo, GError **error) { guint64 tmp_u64 = 0; g_autofree gchar *chk_str = NULL; g_autofree gchar *fct_str = NULL; g_autoptr(XbNode) root = NULL; root = xb_silo_get_root(silo); if (root == NULL || g_strcmp0(xb_node_get_element(root), "Firmware") != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid or missing firmware header element"); return FALSE; } if (!fu_bnr_dp_firmware_attribute_parse_u64(root, "Dev", &self->device_id, error)) return FALSE; if (!fu_bnr_dp_firmware_attribute_parse_u64(root, "Ver", &tmp_u64, error)) return FALSE; fu_firmware_set_version_raw(FU_FIRMWARE(self), tmp_u64); self->usage = fu_bnr_dp_firmware_attribute_parse_string(root, "Use", error); if (self->usage == NULL) return FALSE; if (g_strcmp0(self->usage, "fw") != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported usage string in XML header: '%s'", self->usage); return FALSE; } fct_str = fu_bnr_dp_firmware_attribute_parse_string(root, "Fct", error); if (fct_str == NULL) return FALSE; if (strlen(fct_str) != 1) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported Fct: '%s'", fct_str); return FALSE; } /* function compatibility check */ self->function = fct_str[0]; if (self->function != '_') { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unexpected function (Fct) value in XML header: '%c' (0x%hX)", self->function, self->function); return FALSE; } if (!fu_bnr_dp_firmware_attribute_parse_u64(root, "Var", &self->variant, error)) return FALSE; if (!fu_bnr_dp_firmware_attribute_parse_u64(root, "Len", &self->payload_length, error)) return FALSE; chk_str = fu_bnr_dp_firmware_attribute_parse_string(root, "Chk", error); if (chk_str == NULL) return FALSE; if (!fu_strtoull(chk_str, &tmp_u64, 0, G_MAXUINT16, FU_INTEGER_BASE_16, error)) return FALSE; self->payload_checksum = (guint16)tmp_u64; self->material = fu_bnr_dp_firmware_attribute_parse_string(root, "Mat", error); if (self->material == NULL) return FALSE; /* these are optional and may be NULL */ self->creation_date = fu_bnr_dp_firmware_attribute_parse_string(root, "Date", NULL); self->comment = fu_bnr_dp_firmware_attribute_parse_string(root, "Rem", NULL); /* success */ return TRUE; } static guint16 fu_bnr_dp_firmware_checksum_finish(guint16 csum) { return ~csum + 1; } static gboolean fu_bnr_dp_firmware_stream_checksum(GInputStream *stream, guint16 *csum, GError **error) { if (!fu_input_stream_compute_sum16(stream, csum, error)) return FALSE; *csum = fu_bnr_dp_firmware_checksum_finish(*csum); return TRUE; } static guint16 fu_bnr_dp_firmware_buf_checksum(const guint8 *buf, gsize bufsz) { return fu_bnr_dp_firmware_checksum_finish(fu_sum16(buf, bufsz)); } static gboolean fu_bnr_dp_firmware_payload_parse(FuBnrDpFirmware *self, GInputStream *stream, gsize payload_offset, GError **error) { gsize streamsz = 0; guint16 xml_checksum = 0; guint16 crc = G_MAXUINT16; g_autoptr(GInputStream) payload_stream = NULL; payload_stream = fu_partial_input_stream_new(stream, payload_offset, G_MAXSIZE, error); if (payload_stream == NULL) return FALSE; if (!fu_input_stream_size(payload_stream, &streamsz, error)) return FALSE; if (self->payload_length != streamsz) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "unexpected firmware payload length (header specified: %" G_GUINT64_FORMAT ", actual: %" G_GSIZE_FORMAT ")", self->payload_length, streamsz); return FALSE; } if (streamsz != FU_BNR_DP_FIRMWARE_SIZE) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "unexpected firmware payload length (must be: %d, actual: %" G_GSIZE_FORMAT ")", FU_BNR_DP_FIRMWARE_SIZE, streamsz); return FALSE; } /* the XML header has a simple sum checksum for the payload */ if (!fu_bnr_dp_firmware_stream_checksum(payload_stream, &xml_checksum, error)) return FALSE; if (self->payload_checksum != xml_checksum) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "checksum mismatch in firmware payload (XML header specified: 0x%X, " "actual: 0x%X)", self->payload_checksum, xml_checksum); return FALSE; } /* we can do a CRC16 check on this type of payload as well */ if (!fu_input_stream_compute_crc16(payload_stream, FU_CRC_KIND_B16_BNR, &crc, error)) return FALSE; if (crc != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "CRC mismatch in firmware payload: 0x%X", crc); return FALSE; } /* discard the XML header and keep only the payload */ if (!fu_firmware_set_stream(FU_FIRMWARE(self), payload_stream, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_bnr_dp_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuBnrDpFirmware *self = FU_BNR_DP_FIRMWARE(firmware); guint8 byte = 0; guint8 header_separator[] = {0x0}; gsize separator_idx = 0; g_autoptr(GBytes) header = NULL; g_autoptr(XbBuilderSource) builder_source = xb_builder_source_new(); g_autoptr(XbBuilder) builder = xb_builder_new(); g_autoptr(XbSilo) silo = NULL; if (!fu_input_stream_read_u8(stream, 0, &byte, error)) return FALSE; /* find the index of the first null byte, indicating the end of the XML header */ if (!fu_input_stream_find(stream, header_separator, sizeof(header_separator), &separator_idx, error)) return FALSE; /* read XML header */ header = fu_input_stream_read_bytes(stream, 0, separator_idx, NULL, error); if (header == NULL) return FALSE; if (!xb_builder_source_load_bytes(builder_source, header, XB_BUILDER_SOURCE_FLAG_NONE, error)) { fwupd_error_convert(error); return FALSE; } xb_builder_import_source(builder, builder_source); silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_SINGLE_ROOT, NULL, error); if (silo == NULL) { fwupd_error_convert(error); return FALSE; } if (!fu_bnr_dp_firmware_header_parse(self, silo, error)) return FALSE; if (!fu_bnr_dp_firmware_payload_parse(self, stream, separator_idx + 1, error)) return FALSE; /* success */ return TRUE; } /* set FuBnrDpFirmware private data to information from device */ gboolean fu_bnr_dp_firmware_parse_from_device(FuBnrDpFirmware *self, const FuStructBnrDpFactoryData *st_factory_data, const FuStructBnrDpPayloadHeader *st_fw_header, GError **error) { guint64 version = 0; g_autoptr(GBytes) bytes = NULL; g_autoptr(GDateTime) now = g_date_time_new_now_local(); bytes = fu_firmware_get_bytes_with_patches(FU_FIRMWARE(self), error); if (bytes == NULL) return FALSE; self->device_id = fu_bnr_dp_effective_product_num(st_factory_data); self->usage = g_strdup("fw"); self->function = '_'; self->variant = fu_bnr_dp_effective_compat_id(st_factory_data); self->payload_length = g_bytes_get_size(bytes); self->payload_checksum = fu_bnr_dp_firmware_buf_checksum(g_bytes_get_data(bytes, NULL), g_bytes_get_size(bytes)); self->material = fu_struct_bnr_dp_factory_data_get_identification(st_factory_data); self->creation_date = g_date_time_format(now, "%d.%m.%Y"); self->comment = g_strdup("created by " PACKAGE_NAME " " PACKAGE_VERSION); if (!fu_bnr_dp_version_from_header(st_fw_header, &version, error)) return FALSE; fu_firmware_set_version_raw(FU_FIRMWARE(self), version); return TRUE; } static GByteArray * fu_bnr_dp_firmware_write(FuFirmware *firmware, GError **error) { FuBnrDpFirmware *self = FU_BNR_DP_FIRMWARE(firmware); g_autofree gchar *xml = NULL; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GBytes) payload = NULL; g_autoptr(XbBuilderNode) bn = NULL; g_autofree gchar *device_id = g_strdup_printf("%" G_GUINT64_FORMAT, self->device_id); g_autofree gchar *version = g_strdup_printf("%" G_GUINT64_FORMAT, fu_firmware_get_version_raw(firmware)); g_autofree gchar *function = g_strdup_printf("%c", self->function); g_autofree gchar *variant = g_strdup_printf("%" G_GUINT64_FORMAT, self->variant); g_autofree gchar *payload_length = g_strdup_printf("%" G_GUINT64_FORMAT, self->payload_length); g_autofree gchar *payload_checksum = g_strdup_printf("0x%X", self->payload_checksum); bn = xb_builder_node_insert(NULL, "Firmware", "Dev", device_id, "Ver", version, "Use", self->usage, "Fct", function, "Var", variant, "Len", payload_length, "Chk", payload_checksum, "Mat", self->material, "Date", self->creation_date, "Rem", self->comment, NULL); if (bn == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to build firmware XML header"); return NULL; } xml = xb_builder_node_export(bn, XB_NODE_EXPORT_FLAG_NONE, error); if (xml == NULL) return NULL; /* start with xml header including null byte */ g_byte_array_append(buf, (guint8 *)xml, strlen(xml) + 1); /* append payload after null byte */ payload = fu_firmware_get_bytes_with_patches(firmware, error); if (payload == NULL) return NULL; fu_byte_array_append_bytes(buf, payload); /* success */ return g_steal_pointer(&buf); } /* add firmware patch that increments the boot counter embedded in the firmware */ gboolean fu_bnr_dp_firmware_patch_boot_counter(FuBnrDpFirmware *self, guint32 active_boot_counter, GError **error) { guint16 crc; g_autoptr(FuStructBnrDpPayloadHeader) st_header = NULL; g_autoptr(GBytes) image = NULL; g_autoptr(GBytes) patch = NULL; /* practically impossible under normal conditions, would indicate some form of corruption. * could technically be worked around by resetting the active boot counter */ if (active_boot_counter == G_MAXUINT32) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "update count exhausted"); return FALSE; } image = fu_firmware_get_bytes(FU_FIRMWARE(self), error); st_header = fu_struct_bnr_dp_payload_header_parse(g_bytes_get_data(image, NULL), g_bytes_get_size(image), FU_BNR_DP_FIRMWARE_HEADER_OFFSET, error); if (st_header == NULL) return FALSE; /* check that the current CRC was correct */ crc = fu_crc16(FU_CRC_KIND_B16_BNR, st_header->data, FU_STRUCT_BNR_DP_PAYLOAD_HEADER_SIZE - sizeof(crc)); if (fu_struct_bnr_dp_payload_header_get_crc(st_header) != crc) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "CRC mismatch in firmware binary header (header specified: 0x%X, actual: 0x%X)", fu_struct_bnr_dp_payload_header_get_crc(st_header), crc); return FALSE; } /* set new counter */ g_info("incrementing boot counter: %u => %u", active_boot_counter, active_boot_counter + 1); fu_struct_bnr_dp_payload_header_set_counter(st_header, active_boot_counter + 1); /* clear CRC error flag if set for some reason */ fu_struct_bnr_dp_payload_header_set_flags( st_header, fu_struct_bnr_dp_payload_header_get_flags(st_header) & ~FU_BNR_DP_PAYLOAD_FLAGS_CRC_ERROR); /* update checksum */ crc = fu_crc16(FU_CRC_KIND_B16_BNR, st_header->data, FU_STRUCT_BNR_DP_PAYLOAD_HEADER_SIZE - sizeof(crc)); fu_struct_bnr_dp_payload_header_set_crc(st_header, crc); patch = g_bytes_new(st_header->data, st_header->len); fu_firmware_add_patch(FU_FIRMWARE(self), FU_BNR_DP_FIRMWARE_HEADER_OFFSET, patch); return TRUE; } /* do checks that can only be done with data from an opened device */ gboolean fu_bnr_dp_firmware_check(FuBnrDpFirmware *self, const FuStructBnrDpFactoryData *st_factory_data, const FuStructBnrDpPayloadHeader *st_active_header, const FuStructBnrDpPayloadHeader *st_fw_header, FuFirmwareParseFlags flags, GError **error) { guint64 active_version = 0; guint64 fw_version = 0; guint32 product_num; guint16 compat_id; g_autofree gchar *fw_version_str = NULL; /* compare versions */ if (!fu_bnr_dp_version_from_header(st_active_header, &active_version, error)) return FALSE; if (!fu_bnr_dp_version_from_header(st_fw_header, &fw_version, error)) return FALSE; fw_version_str = fu_bnr_dp_version_to_string(fw_version); if (fu_firmware_get_version_raw(FU_FIRMWARE(self)) != fw_version) { if ((flags & FWUPD_INSTALL_FLAG_FORCE) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "versions in firmware XML header (%s) and binary payload (%s) " "are inconsistent", fu_firmware_get_version(FU_FIRMWARE(self)), fw_version_str); return FALSE; } g_warning("forcing installation of firmware with inconsistent XML header (%s) and " "binary payload (%s) versions", fu_firmware_get_version(FU_FIRMWARE(self)), fw_version_str); } /* check for compatibility of device/firmware combination. customized products use separate * product numbers but set the parent product number to the original stock product. since * these customizations are generally mechanical, they shall not make the firmware * incompatible */ product_num = fu_bnr_dp_effective_product_num(st_factory_data); if (product_num != G_MAXUINT32 && product_num != self->device_id) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "firmware file is not for a compatible device (expected id: 0x%X, " "received id: 0x%" G_GUINT64_FORMAT "X)", product_num, self->device_id); return FALSE; } /* variant compatibility check, similar to device id check */ compat_id = fu_bnr_dp_effective_compat_id(st_factory_data); if (compat_id != G_MAXUINT16 && compat_id != self->variant) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "firmware file is not for a compatible variant (expected: 0x%X, " "received: 0x%" G_GUINT64_FORMAT "X)", compat_id, self->variant); return FALSE; } return TRUE; } static void fu_bnr_dp_firmware_init(FuBnrDpFirmware *self) { } static void fu_bnr_dp_firmware_finalize(GObject *object) { FuBnrDpFirmware *self = FU_BNR_DP_FIRMWARE(object); g_free(self->usage); g_free(self->material); g_free(self->creation_date); g_free(self->comment); G_OBJECT_CLASS(fu_bnr_dp_firmware_parent_class)->finalize(object); } static void fu_bnr_dp_firmware_class_init(FuBnrDpFirmwareClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_bnr_dp_firmware_finalize; firmware_class->convert_version = fu_bnr_dp_firmware_convert_version; firmware_class->export = fu_bnr_dp_firmware_export; firmware_class->parse = fu_bnr_dp_firmware_parse; firmware_class->write = fu_bnr_dp_firmware_write; } FuFirmware * fu_bnr_dp_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_BNR_DP_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/bnr-dp/fu-bnr-dp-firmware.h000066400000000000000000000025531501337203100213370ustar00rootroot00000000000000/* * Copyright 2024 B&R Industrial Automation GmbH * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-bnr-dp-struct.h" #define FU_TYPE_BNR_DP_FIRMWARE (fu_bnr_dp_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuBnrDpFirmware, fu_bnr_dp_firmware, FU, BNR_DP_FIRMWARE, FuFirmware) FuFirmware * fu_bnr_dp_firmware_new(void); /* payload is 3MiB, XML header is variable size but really shouldn't be very large */ #define FU_BNR_DP_FIRMWARE_SIZE (3 * 1024 * 1024) #define FU_BNR_DP_FIRMWARE_SIZE_MAX (FU_BNR_DP_FIRMWARE_SIZE + (4 * 1024)) /* location of the payload header in firmware images */ #define FU_BNR_DP_FIRMWARE_HEADER_OFFSET 0x10 gboolean fu_bnr_dp_firmware_parse_from_device(FuBnrDpFirmware *self, const FuStructBnrDpFactoryData *st_factory_data, const FuStructBnrDpPayloadHeader *st_fw_header, GError **error) G_GNUC_NON_NULL(1, 2, 3); gboolean fu_bnr_dp_firmware_patch_boot_counter(FuBnrDpFirmware *self, guint32 active_boot_counter, GError **error) G_GNUC_NON_NULL(1); gboolean fu_bnr_dp_firmware_check(FuBnrDpFirmware *self, const FuStructBnrDpFactoryData *st_factory_data, const FuStructBnrDpPayloadHeader *st_active_header, const FuStructBnrDpPayloadHeader *st_fw_header, FuFirmwareParseFlags flags, GError **error) G_GNUC_NON_NULL(1, 2, 3, 4); fwupd-2.0.10/plugins/bnr-dp/fu-bnr-dp-plugin.c000066400000000000000000000016301501337203100210070ustar00rootroot00000000000000/* * Copyright 2024 B&R Industrial Automation GmbH * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-bnr-dp-device.h" #include "fu-bnr-dp-firmware.h" #include "fu-bnr-dp-plugin.h" struct _FuBnrDpPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuBnrDpPlugin, fu_bnr_dp_plugin, FU_TYPE_PLUGIN) static void fu_bnr_dp_plugin_init(FuBnrDpPlugin *self) { } static void fu_bnr_dp_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_udev_subsystem(plugin, "drm"); fu_plugin_add_device_udev_subsystem(plugin, "drm_dp_aux_dev"); fu_plugin_add_device_gtype(plugin, FU_TYPE_BNR_DP_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_BNR_DP_FIRMWARE); } static void fu_bnr_dp_plugin_class_init(FuBnrDpPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_bnr_dp_plugin_constructed; } fwupd-2.0.10/plugins/bnr-dp/fu-bnr-dp-plugin.h000066400000000000000000000003451501337203100210160ustar00rootroot00000000000000/* * Copyright 2024 B&R Industrial Automation GmbH * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuBnrDpPlugin, fu_bnr_dp_plugin, FU, BNR_DP_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/bnr-dp/fu-bnr-dp.rs000066400000000000000000000053651501337203100177260ustar00rootroot00000000000000// Copyright 2024 B&R Industrial Automation GmbH // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] #[repr(u1)] enum FuBnrDpBootArea { Low, High, } #[repr(u16le)] enum FuBnrDpPayloadFlags { BootArea = 1 << 0, CrcError = 1 << 1, } #[derive(Default, Setters, Parse)] #[repr(C, packed)] struct FuStructBnrDpPayloadHeader { id: [char; 4] == "DP0R", version: [char; 4], counter: u32le, flags: FuBnrDpPayloadFlags, crc: u16be, } // FIXME: should be repr(u4) and AuxCommand struct should have `_reserve: u4` at front #[derive(ToString)] #[repr(u8)] enum FuBnrDpModuleNumber { Receiver = 0x00, Display = 0x10, KeyExpansion = 0x20, } #[derive(ToString)] #[repr(u8)] enum FuBnrDpOpcodes { Reset = 0x08, FwVersion = 0x10, InfoFlags = 0x11, FlashSaveHeaderInfo = 0x6A, FactoryData = 0x80, FlashUser = 0xB0, FlashService = 0xF0, } #[derive(New)] #[repr(C, packed)] struct FuStructBnrDpAuxCommand { module_number: FuBnrDpModuleNumber, opcode: FuBnrDpOpcodes, } #[derive(New)] #[repr(C, packed)] struct FuStructBnrDpAuxRequest { data_len: u16le, offset: u16le, command: FuStructBnrDpAuxCommand, } #[derive(New)] #[repr(C, packed)] struct FuStructBnrDpAuxTxHeader { request: FuStructBnrDpAuxRequest, checksum: u8, } #[derive(ToString)] #[repr(u4)] enum FuBnrDpAuxError { IrqCollision, UnknownCommand, Timeout, BadParameter, DeviceBusy, DeviceFailure, DataFailure, } // FIXME: remove this enum, AuxStatus.error should be `error_number: u4`, u2 reserve followed by // error and busy bits #[repr(u8)] enum FuBnrDpAuxStatusFlags { Error = 1 << 6, Busy = 1 << 7, } #[derive(Parse)] #[repr(C, packed)] struct FuStructBnrDpAuxStatus { _reserve: u8, error: u8, } #[repr(C, packed)] struct FuStructBnrDpAuxResponse { data_len: u16le, _reserve: [u8; 4], } #[derive(Parse)] #[repr(C, packed)] struct FuStructBnrDpAuxRxHeader { response: FuStructBnrDpAuxResponse, checksum: u8, } #[repr(u8)] enum FuBnrDpChecksumInit { Rx = 0xAB, Tx = 0xBA, } #[repr(u32le)] enum FuBnrDpInfoFlags { BootArea = 1 << 0, CrcOk = 1 << 1, PmeEnable = 1 << 4, IctEnable = 1 << 5, RecEnable = 1 << 6, } #[derive(Parse)] #[repr(C, packed)] struct FuStructBnrDpInfoFlags { inner: FuBnrDpInfoFlags, } #[derive(Default, Parse)] #[repr(C, packed)] struct FuStructBnrDpFactoryData { id: [char; 4] == "FACT", version_struct: u8, version_data: u8, data_len: u16le, header_type: u16le, product_num: u32le, compat_id: u16le, vendor_id: u32le, hw_rev: [char; 5], serial: [char; 12], identification: [char; 41], hw_num: [char; 3], parent_product_num: u32le, parent_compat_id: u16le, } fwupd-2.0.10/plugins/bnr-dp/fu-self-test.c000066400000000000000000000033471501337203100202460ustar00rootroot00000000000000/* * Copyright 2025 B&R Industrial Automation GmbH * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-bnr-dp-firmware.h" static void fu_bnr_dp_firmware_xml_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autofree gchar *csum1 = NULL; g_autofree gchar *csum2 = NULL; g_autofree gchar *xml_out = NULL; g_autofree gchar *xml_src = NULL; g_autoptr(FuFirmware) firmware1 = fu_bnr_dp_firmware_new(); g_autoptr(FuFirmware) firmware2 = fu_bnr_dp_firmware_new(); g_autoptr(GError) error = NULL; /* build and write */ filename = g_test_build_filename(G_TEST_DIST, "tests", "bnr-dp.builder.xml", NULL); ret = g_file_get_contents(filename, &xml_src, NULL, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_firmware_build_from_xml(firmware1, xml_src, &error); g_assert_no_error(error); g_assert_true(ret); csum1 = fu_firmware_get_checksum(firmware1, G_CHECKSUM_SHA1, &error); g_assert_no_error(error); g_assert_cmpstr(csum1, ==, "e5d645902551f55258827223905fb097cf3af58c"); /* ensure we can round-trip */ xml_out = fu_firmware_export_to_xml(firmware1, FU_FIRMWARE_EXPORT_FLAG_NONE, &error); g_assert_no_error(error); ret = fu_firmware_build_from_xml(firmware2, xml_out, &error); g_assert_no_error(error); g_assert_true(ret); csum2 = fu_firmware_get_checksum(firmware2, G_CHECKSUM_SHA1, &error); g_assert_cmpstr(csum1, ==, csum2); } int main(int argc, char **argv) { (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); /* tests go here */ g_test_add_func("/bnr-dp/firmware{xml}", fu_bnr_dp_firmware_xml_func); return g_test_run(); } fwupd-2.0.10/plugins/bnr-dp/meson.build000066400000000000000000000026031501337203100177200ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginBnrDp"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('bnr-dp.quirk') plugin_rustgen_output = rustgen.process('fu-bnr-dp.rs') plugin_builtin_bnr_dp = static_library( 'fu_plugin_bnr_dp', plugin_rustgen_output, sources: [ 'fu-bnr-dp-common.c', 'fu-bnr-dp-device.c', 'fu-bnr-dp-firmware.c', 'fu-bnr-dp-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) plugin_builtins += plugin_builtin_bnr_dp device_tests += files('tests/bnr-dp.json') if get_option('tests') install_data( 'tests' / 'bnr-dp.builder.xml', install_dir: installed_test_datadir / 'tests', ) test( 'bnr-dp-self-test', executable( 'bnr-dp-self-test', plugin_rustgen_output, sources: ['fu-self-test.c'], include_directories: plugin_incdirs, dependencies: plugin_deps, link_with: [ plugin_builtin_bnr_dp, plugin_libs, ], install: true, install_dir: installed_test_bindir, install_rpath: libdir_pkg, c_args: ['-DSRCDIR="@0@"'.format(meson.current_source_dir())], ), env: environment( { 'G_TEST_BUILDDIR': meson.current_build_dir(), 'G_TEST_SRCDIR': meson.current_source_dir(), }, ), ) endif endif fwupd-2.0.10/plugins/bnr-dp/tests/000077500000000000000000000000001501337203100167175ustar00rootroot00000000000000fwupd-2.0.10/plugins/bnr-dp/tests/bnr-dp.builder.xml000066400000000000000000000006551501337203100222560ustar00rootroot00000000000000 0.13 0xd [GInputStream] 0x2f1a fw 0x5f 0x300000 0x7262 5DLDPO.1001-00 04.09.2024 fwupd-2.0.10/plugins/bnr-dp/tests/bnr-dp.json000066400000000000000000000007641501337203100210030ustar00rootroot00000000000000{ "name": "B&R Automation Panel Link Module", "interactive": false, "steps": [ { "url": "5a909ed2e4997518a8be94003042a53ed2d3bb7676f23745d6da0b21875cf03f-bnr-dp-0.13-test.cab", "emulation-url": "2d65498250eab4800c250b9499c3595be165762154f41dbfa1118797f5004636-bnr-dp-0.13-dpaux-dpcd-check-minimal.zip", "components": [ { "version": "0.13", "guids": [ "9099977e-4a86-5c57-9a4d-40db2d31b52b" ] } ] } ] } fwupd-2.0.10/plugins/ccgx-dmc/000077500000000000000000000000001501337203100160605ustar00rootroot00000000000000fwupd-2.0.10/plugins/ccgx-dmc/README.md000066400000000000000000000064461501337203100173510ustar00rootroot00000000000000--- title: Plugin: CCGX DMC --- ## Introduction This plugin can flash firmware on Infineon (previously Cypress) CCGx DMC devices used in docks. ## Supported Protocols This plugin supports the following protocol IDs: * `com.cypress.ccgx.dmc` (deprecated) * `com.infineon.ccgx.dmc` ### DMC Factory Mode Dock Management Controller devices have a *composite version* that is used to describe the dock hardware as a whole, rather than enumerating and updating each sub-component separately. When OEMs have not followed the IHV-approved factory assembly process, the composite number is unset and fwupd would display `0.0.0.0` in the GUI and on the command line. In fwupd >= 1.8.11 we detect if the device is in *factory mode* and set the version number to `0.0.0.1`. When the device is in factory mode any valid upgrade will be allowed, which means the user might be prompted to “update to†the same current version installed on the dock. For millions of devices this is both a waste of time, resources, and also would inconvenience the user with an additional process for no reason. For devices that have been shipped in factory mode, but would like to avoid the update from `0.0.0.1` to the original version on the LVFS, can add a quirk entry which matches the `devx` subcomponent *base version*. In this example we match the parent VID, PID, the ComponentID and the `devx` base firmware version, setting the parent composite version to `0.0.0.15`. [USB\VID_2188&PID_0035&CID_05&VER_3.3.1.69] CcgxDmcCompositeVersion = 15 All the `devx` subcomponent versions can be shown on the console using: sudo fwupdtool –plugins ccgx get-devices –show-all –verbose ## Firmware Format In composite firmware topology, a single firmware image contains metadata and firmware images of multiple devices including DMC itself in a dock system. The daemon will decompress the cabinet archive and extract several firmware blobs in a combined image file format. See 4.4.1 Single Composite (Combined) Dock Image at for more details. ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_1234&PID_5678` ## Update Behavior The device usually presents in runtime HID mode, but on detach re-enumerates with with a DMC or HPI interface. On attach the device again re-enumerates back to the runtime HID mode. ## Vendor ID Security The vendor ID is set from the USB vendor, for example set to `USB:0x04B4` ## Quirk Use This plugin uses the following plugin-specific quirks: ### CcgxDmcTriggerCode DMC devices need a specified trigger code to request the device to update the firmware and the trigger code depends on the devices. 0x0: Do not update 0x1: Update immediately 0x2: Update after port disconnected Since: 1.8.0 ### CcgxDmcCompositeVersion Set the parent composite version, as a 32 bit integer. Since: 1.8.11 ### Flags=has-manual-replug Needs a manual replug from the end-user. ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.5.0`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Brent Wu: @IfxBrent fwupd-2.0.10/plugins/ccgx-dmc/ccgx-dmc-noinst.quirk000066400000000000000000000003071501337203100221320ustar00rootroot00000000000000# Any EVB board that uses CY7C65219 [USB\VID_04B4&PID_5220] Plugin = ccgx_dmc RemoveDelay = 732000 # Any EVB board that uses CYUSB4357 [USB\VID_04B4&PID_521B] Plugin = ccgx_dmc RemoveDelay = 732000 fwupd-2.0.10/plugins/ccgx-dmc/ccgx-dmc.quirk000066400000000000000000000050341501337203100206240ustar00rootroot00000000000000# Lenovo ThinkPad Universal USB-C Dock [USB\VID_17EF&PID_30A9] Plugin = ccgx_dmc Summary = Dock Management Controller Device ParentGuid = USB\VID_17EF&PID_30AF Name = ThinkPad Universal USB-C Dock Flags = has-manual-replug CcgxDmcTriggerCode = 0x02 InstallDuration = 60 [USB\VID_17EF&PID_3105] Plugin = ccgx_dmc Summary = Dock Management Controller Device ParentGuid = USB\VID_17EF&PID_30AF Name = ThinkPad Universal USB-C Dock Flags = has-manual-replug CcgxDmcTriggerCode = 0x02 InstallDuration = 60 # HP USB-C Dock G5 [USB\VID_03F0&PID_046B] Plugin = ccgx_dmc Summary = Dock Management Controller Device ParentGuid = USB\VID_03F0&PID_0363 Vendor = HP Name = USB-C Dock G5 InstallDuration = 233 RemoveDelay = 203000 # HP USB-C/A Universal Dock G2 [USB\VID_03F0&PID_0A6B] Plugin = ccgx_dmc Summary = Dock Management Controller Device ParentGuid = USB\VID_03F0&PID_096B Vendor = HP Name = USB-C/A Universal Dock G2 InstallDuration = 180 RemoveDelay = 162000 # HP Thunderbolt Dock G4 Root Hub [USB\VID_1D5C&PID_5801] Summary = USB Hub Name = Thunderbolt Dock G4 top most USB Hub # HP Thunderbolt Dock G4 Hub [USB\VID_03F0&PID_2488] Summary = USB Hub ParentGuid = USB\VID_1D5C&PID_5801 Name = Thunderbolt Dock G4 USB Hub # HP Thunderbolt Dock G4 [USB\VID_03F0&PID_0488] Plugin = ccgx_dmc Summary = Dock Management Controller Device ParentGuid = USB\VID_03F0&PID_2488 Vendor = HP Name = Thunderbolt Dock G4 InstallDuration = 898 RemoveDelay = 732000 # Quanta Storage Inc. QSI Thunderbolt4 Godzilla Hub [USB\VID_2BEF&PID_9065] Plugin = ccgx_dmc Summary = Dock Management Controller Device RemoveDelay = 732000 # Anker Thunderbolt4 Mini Dock [USB\VID_291A&PID_8398] Plugin = ccgx_dmc Summary = Dock Management Controller Device RemoveDelay = 732000 # Caldigit ElementHub [USB\VID_2188&PID_0035] Plugin = ccgx_dmc [USB\VID_2188&PID_0035&CID_05&VER_3.3.1.69] CcgxDmcCompositeVersion = 15 # Belkin Thunderbolt 4 Core Hub dock [USB\VID_050D&PID_006E] Plugin = ccgx_dmc # CE-LINK TB4-Dock01 [USB\VID_2095&PID_4D01] Plugin = ccgx_dmc RemoveDelay = 732000 # HP Engage_One_Pro_Aio_System [USB\VID_03F0&PID_0480] Plugin = ccgx_dmc Summary = Dock Management Controller Device ParentGuid = USB\VID_03F0&PID_0281 Vendor = HP Name = HP Engage One Pro Aio System CcgxImageKind = dmc-composite InstallDuration = 180 RemoveDelay = 162000 # Lenovo ThinkPad Thunderbolt 4 Smart Dock Gen2 7500 [USB\VID_17EF&PID_A5D9] Plugin = ccgx_dmc Summary = Dock Management Controller Device Name = ThinkPad Thunderbolt 4 Smart Dock Gen2 7500 CcgxDmcTriggerCode = 0x01 InstallDuration = 300 RemoveDelay = 732000 fwupd-2.0.10/plugins/ccgx-dmc/fu-ccgx-dmc-device.c000066400000000000000000000653561501337203100215750ustar00rootroot00000000000000/* * Copyright 2020 Cypress Semiconductor Corporation. * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-ccgx-dmc-device.h" #include "fu-ccgx-dmc-devx-device.h" #include "fu-ccgx-dmc-firmware.h" #include "fu-ccgx-dmc-struct.h" #define DMC_FW_WRITE_STATUS_RETRY_COUNT 3 #define DMC_FW_WRITE_STATUS_RETRY_DELAY_MS 30 #define DMC_CONTROL_TRANSFER_DEFAULT_TIMEOUT 5000 /* control in/out pipe policy in ms */ #define DMC_BULK_OUT_PIPE_TIMEOUT 2000 /* bulk out pipe policy in ms */ #define DMC_GET_REQUEST_TIMEOUT 20000 /* bulk out pipe policy in ms */ #define DMC_INTERRUPT_PIPE_ID 0x82 /* interrupt ep for DMC Dock */ #define DMC_BULK_PIPE_ID 1 /* USB bulk end point for DMC Dock */ /* maximum number of programmable devices expected to be connected in dock */ #define DMC_DOCK_MAX_DEV_COUNT 16 struct _FuCcgxDmcDevice { FuUsbDevice parent_instance; FuCcgxDmcDeviceStatus device_status; guint8 ep_intr_in; guint8 ep_bulk_out; FuCcgxDmcUpdateModel update_model; guint16 trigger_code; /* trigger code for update */ guint8 custom_meta_flag; }; #define FU_CCGX_DMC_DEVICE_FLAG_HAS_MANUAL_REPLUG "has-manual-replug" G_DEFINE_TYPE(FuCcgxDmcDevice, fu_ccgx_dmc_device, FU_TYPE_USB_DEVICE) static gboolean fu_ccgx_dmc_device_ensure_dock_id(FuCcgxDmcDevice *self, GError **error) { g_autoptr(GByteArray) st_id = fu_struct_ccgx_dmc_dock_identity_new(); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, FU_CCGX_DMC_RQT_CODE_DOCK_IDENTITY, /* request */ 0, /* value */ 0, /* index */ st_id->data, st_id->len, NULL, /* actual length */ DMC_CONTROL_TRANSFER_DEFAULT_TIMEOUT, NULL, error)) { g_prefix_error(error, "get_dock_id error: "); return FALSE; } self->custom_meta_flag = fu_struct_ccgx_dmc_dock_identity_get_custom_meta_data_flag(st_id); return TRUE; } static gboolean fu_ccgx_dmc_device_ensure_status(FuCcgxDmcDevice *self, GError **error) { guint remove_delay = 20 * 1000; /* guard band */ gsize bufsz; gsize offset = FU_STRUCT_CCGX_DMC_DOCK_STATUS_SIZE; g_autofree guint8 *buf = NULL; g_autoptr(GByteArray) st = fu_struct_ccgx_dmc_dock_status_new(); /* read minimum status length */ fu_byte_array_set_size(st, 32, 0x0); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, FU_CCGX_DMC_RQT_CODE_DOCK_STATUS, /* request */ 0, /* value */ 0, /* index */ st->data, st->len, NULL, /* actual length */ DMC_CONTROL_TRANSFER_DEFAULT_TIMEOUT, NULL, error)) { g_prefix_error(error, "get_dock_status min size error: "); return FALSE; } /* read full status length */ bufsz = FU_STRUCT_CCGX_DMC_DOCK_STATUS_SIZE + (DMC_DOCK_MAX_DEV_COUNT * FU_STRUCT_CCGX_DMC_DEVX_STATUS_SIZE); buf = g_malloc0(bufsz); if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_EMULATED)) { /* copying the old buffer preserves compatibility with old emulation files */ if (!fu_memcpy_safe(buf, bufsz, 0x0, st->data, st->len, 0x0, st->len, error)) return FALSE; } if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, FU_CCGX_DMC_RQT_CODE_DOCK_STATUS, /* request */ 0, /* value */ 0, /* index */ buf, bufsz, NULL, /* actual length */ DMC_CONTROL_TRANSFER_DEFAULT_TIMEOUT, NULL, error)) { g_prefix_error(error, "get_dock_status actual size error: "); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "DmcDockStatus", buf, bufsz); /* add devx children */ for (guint i = 0; i < fu_struct_ccgx_dmc_dock_status_get_device_count(st); i++) { g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(FuCcgxDmcDevxDevice) devx = fu_ccgx_dmc_devx_device_new(FU_DEVICE(self), buf, bufsz, offset, error); if (devx == NULL) return FALSE; locker = fu_device_locker_new(devx, error); if (locker == NULL) return FALSE; remove_delay += fu_ccgx_dmc_devx_device_get_remove_delay(devx); fu_device_add_child(FU_DEVICE(self), FU_DEVICE(devx)); offset += FU_STRUCT_CCGX_DMC_DEVX_STATUS_SIZE; } /* ensure the remove delay is set */ if (fu_device_get_remove_delay(FU_DEVICE(self)) == 0) { g_debug("autosetting remove delay to %ums using DMC devx components", remove_delay); fu_device_set_remove_delay(FU_DEVICE(self), remove_delay); } /* success */ self->device_status = fu_struct_ccgx_dmc_dock_status_get_device_status(st); fu_device_set_version_raw(FU_DEVICE(self), fu_struct_ccgx_dmc_dock_status_get_composite_version(st)); return TRUE; } static gboolean fu_ccgx_dmc_device_send_reset_state_machine(FuCcgxDmcDevice *self, GError **error) { if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, FU_CCGX_DMC_RQT_CODE_RESET_STATE_MACHINE, /* request */ 0, /* value */ 0, /* index */ 0, /* data */ 0, /* length */ NULL, /* actual length */ DMC_CONTROL_TRANSFER_DEFAULT_TIMEOUT, NULL, error)) { g_prefix_error(error, "send reset state machine error: "); return FALSE; } return TRUE; } static gboolean fu_ccgx_dmc_device_send_soft_reset(FuCcgxDmcDevice *self, gboolean reset_later, GError **error) { if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, FU_CCGX_DMC_RQT_CODE_SOFT_RESET, /* request */ reset_later, /* value */ 0, /* index */ 0, /* data */ 0, /* length */ NULL, /* actual length */ DMC_CONTROL_TRANSFER_DEFAULT_TIMEOUT, NULL, error)) { g_prefix_error(error, "send reset error: "); return FALSE; } return TRUE; } static gboolean fu_ccgx_dmc_device_send_start_upgrade(FuCcgxDmcDevice *self, GBytes *fw, GError **error) { gsize bufsz = 0; const guint8 *buf = NULL; g_autofree guint8 *buf_mut = NULL; if (fw != NULL) buf = g_bytes_get_data(fw, &bufsz); if (bufsz > 0) { buf_mut = fu_memdup_safe(buf, bufsz, error); if (buf_mut == NULL) return FALSE; } if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, FU_CCGX_DMC_RQT_CODE_UPGRADE_START, /* request */ bufsz > 0 ? 1 : 0, /* value */ 1, /* index, forced update */ buf_mut, /* data */ bufsz, /* length */ NULL, /* actual length */ DMC_CONTROL_TRANSFER_DEFAULT_TIMEOUT, NULL, error)) { g_prefix_error(error, "send reset error: "); return FALSE; } return TRUE; } static gboolean fu_ccgx_dmc_device_send_download_trigger(FuCcgxDmcDevice *self, guint16 trigger, GError **error) { if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, FU_CCGX_DMC_RQT_CODE_TRIGGER, /* request */ trigger, /* value */ 0, /* index */ 0, /* data */ 0, /* length */ NULL, /* actual length */ DMC_CONTROL_TRANSFER_DEFAULT_TIMEOUT, NULL, error)) { g_prefix_error(error, "send download trigger error: "); return FALSE; } return TRUE; } static gboolean fu_ccgx_dmc_device_send_fwct(FuCcgxDmcDevice *self, const guint8 *buf, guint16 bufsz, GError **error) { g_autofree guint8 *buf_mut = NULL; g_return_val_if_fail(buf != NULL, FALSE); buf_mut = fu_memdup_safe(buf, bufsz, error); if (buf_mut == NULL) return FALSE; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, FU_CCGX_DMC_RQT_CODE_FWCT_WRITE, /* request */ 0, /* value */ 0, /* index */ buf_mut, /* data */ bufsz, /* length */ NULL, /* actual length */ DMC_CONTROL_TRANSFER_DEFAULT_TIMEOUT, NULL, error)) { g_prefix_error(error, "send fwct error: "); return FALSE; } return TRUE; } static gboolean fu_ccgx_dmc_device_read_intr_req(FuCcgxDmcDevice *self, GByteArray *intr_rqt, GError **error) { guint8 rqt_opcode; g_autofree gchar *title = NULL; g_return_val_if_fail(intr_rqt != NULL, FALSE); if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), self->ep_intr_in, intr_rqt->data, intr_rqt->len, NULL, DMC_GET_REQUEST_TIMEOUT, NULL, error)) { g_prefix_error(error, "read intr rqt error: "); return FALSE; } /* success */ rqt_opcode = fu_struct_ccgx_dmc_int_rqt_get_opcode(intr_rqt); title = g_strdup_printf("DmcIntRqt-opcode=0x%02x[%s]", rqt_opcode, fu_ccgx_dmc_int_opcode_to_string(rqt_opcode)); fu_dump_raw(G_LOG_DOMAIN, title, fu_struct_ccgx_dmc_int_rqt_get_data(intr_rqt, NULL), MIN(fu_struct_ccgx_dmc_int_rqt_get_length(intr_rqt), FU_STRUCT_CCGX_DMC_INT_RQT_SIZE_DATA)); return TRUE; } static gboolean fu_ccgx_dmc_device_send_write_command(FuCcgxDmcDevice *self, guint16 start_row, guint16 num_of_row, GError **error) { if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, FU_CCGX_DMC_RQT_CODE_IMG_WRITE, /* request */ start_row, /* value */ num_of_row, /* index */ 0, /* data */ 0, /* length */ NULL, /* actual length */ DMC_CONTROL_TRANSFER_DEFAULT_TIMEOUT, NULL, error)) { g_prefix_error(error, "send fwct error: "); return FALSE; } return TRUE; } static gboolean fu_ccgx_dmc_device_send_row_data(FuCcgxDmcDevice *self, const guint8 *row_buffer, guint16 row_size, GError **error) { g_return_val_if_fail(row_buffer != NULL, FALSE); if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), self->ep_bulk_out, (guint8 *)row_buffer, row_size, NULL, DMC_BULK_OUT_PIPE_TIMEOUT, NULL, error)) { g_prefix_error(error, "write row data error: "); return FALSE; } return TRUE; } static void fu_ccgx_dmc_device_to_string(FuDevice *device, guint idt, GString *str) { FuCcgxDmcDevice *self = FU_CCGX_DMC_DEVICE(device); fwupd_codec_string_append(str, idt, "UpdateModel", fu_ccgx_dmc_update_model_to_string(self->update_model)); fwupd_codec_string_append_hex(str, idt, "EpBulkOut", self->ep_bulk_out); fwupd_codec_string_append_hex(str, idt, "EpIntrIn", self->ep_intr_in); fwupd_codec_string_append_hex(str, idt, "TriggerCode", self->trigger_code); fwupd_codec_string_append(str, idt, "DeviceStatus", fu_ccgx_dmc_device_status_to_string(self->device_status)); } static gboolean fu_ccgx_dmc_device_get_image_write_status_cb(FuDevice *device, gpointer user_data, GError **error) { FuCcgxDmcDevice *self = FU_CCGX_DMC_DEVICE(device); const guint8 *req_data; guint8 req_opcode; g_autoptr(GByteArray) dmc_int_req = fu_struct_ccgx_dmc_int_rqt_new(); /* get interrupt request */ if (!fu_ccgx_dmc_device_read_intr_req(self, dmc_int_req, error)) { g_prefix_error(error, "failed to read intr req in image write status: "); return FALSE; } /* check opcode for fw write */ req_opcode = fu_struct_ccgx_dmc_int_rqt_get_opcode(dmc_int_req); if (req_opcode != FU_CCGX_DMC_INT_OPCODE_IMG_WRITE_STATUS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid intr req opcode in image write status: %u [%s]", req_opcode, fu_ccgx_dmc_int_opcode_to_string(req_opcode)); return FALSE; } /* retry if data[0] is 1 otherwise error */ req_data = fu_struct_ccgx_dmc_int_rqt_get_data(dmc_int_req, NULL); if (req_data[0] != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid intr req data in image write status = %u", req_data[0]); fu_device_sleep(device, DMC_FW_WRITE_STATUS_RETRY_DELAY_MS); return FALSE; } return TRUE; } static gboolean fu_ccgx_dmc_device_write_firmware_record(FuCcgxDmcDevice *self, FuCcgxDmcFirmwareSegmentRecord *seg_rcd, gsize *fw_data_written, FuProgress *progress, GError **error) { GPtrArray *data_records = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 99, NULL); /* write start row and number of rows to a device */ if (!fu_ccgx_dmc_device_send_write_command(self, seg_rcd->start_row, seg_rcd->num_rows, error)) return FALSE; fu_progress_step_done(progress); /* send data records */ data_records = seg_rcd->data_records; for (guint32 data_index = 0; data_index < data_records->len; data_index++) { GBytes *data_rcd = g_ptr_array_index(data_records, data_index); const guint8 *row_buffer = NULL; gsize row_size = 0; /* write row data */ row_buffer = g_bytes_get_data(data_rcd, &row_size); if (!fu_ccgx_dmc_device_send_row_data(self, row_buffer, (guint16)row_size, error)) return FALSE; /* increase fw written size */ *fw_data_written += row_size; /* get status */ if (!fu_device_retry(FU_DEVICE(self), fu_ccgx_dmc_device_get_image_write_status_cb, DMC_FW_WRITE_STATUS_RETRY_COUNT, NULL, error)) return FALSE; /* done */ fu_progress_set_percentage_full(fu_progress_get_child(progress), data_index + 1, data_records->len); } fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_ccgx_dmc_device_write_firmware_image(FuDevice *device, FuCcgxDmcFirmwareRecord *img_rcd, gsize *fw_data_written, const gsize fw_data_size, FuProgress *progress, GError **error) { FuCcgxDmcDevice *self = FU_CCGX_DMC_DEVICE(device); GPtrArray *seg_records; g_return_val_if_fail(img_rcd != NULL, FALSE); g_return_val_if_fail(fw_data_written != NULL, FALSE); /* get segment records */ seg_records = img_rcd->seg_records; fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, seg_records->len); for (guint32 seg_index = 0; seg_index < seg_records->len; seg_index++) { FuCcgxDmcFirmwareSegmentRecord *seg_rcd = g_ptr_array_index(seg_records, seg_index); if (!fu_ccgx_dmc_device_write_firmware_record(self, seg_rcd, fw_data_written, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); } return TRUE; } static gboolean fu_ccgx_dmc_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuCcgxDmcDevice *self = FU_CCGX_DMC_DEVICE(device); FuCcgxDmcFirmwareRecord *img_rcd = NULL; GBytes *custom_meta_blob; GBytes *fwct_blob; GPtrArray *image_records; const guint8 *fwct_buf = NULL; const guint8 *rqt_data = NULL; gsize fwct_sz = 0; gsize fw_data_size = 0; gsize fw_data_written = 0; guint8 img_index = 0; guint8 rqt_opcode; g_autoptr(GByteArray) dmc_int_rqt = fu_struct_ccgx_dmc_int_rqt_new(); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "fwct"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 98, "img"); /* this is used in FuDevice->attach */ self->update_model = FU_CCGX_DMC_UPDATE_MODEL_NONE; /* get fwct record */ fwct_blob = fu_ccgx_dmc_firmware_get_fwct_record(FU_CCGX_DMC_FIRMWARE(firmware)); fwct_buf = g_bytes_get_data(fwct_blob, &fwct_sz); if (fwct_buf == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid fwct data"); return FALSE; } /* reset */ if (!fu_ccgx_dmc_device_send_reset_state_machine(self, error)) return FALSE; fu_progress_step_done(progress); /* start fw upgrade with custom metadata */ custom_meta_blob = fu_ccgx_dmc_firmware_get_custom_meta_record(FU_CCGX_DMC_FIRMWARE(firmware)); if (!fu_ccgx_dmc_device_send_start_upgrade(self, custom_meta_blob, error)) return FALSE; /* send fwct data */ if (!fu_ccgx_dmc_device_send_fwct(self, fwct_buf, fwct_sz, error)) return FALSE; fu_progress_step_done(progress); /* get total fw size */ image_records = fu_ccgx_dmc_firmware_get_image_records(FU_CCGX_DMC_FIRMWARE(firmware)); fw_data_size = fu_ccgx_dmc_firmware_get_fw_data_size(FU_CCGX_DMC_FIRMWARE(firmware)); while (1) { /* get interrupt request */ if (!fu_ccgx_dmc_device_read_intr_req(self, dmc_int_rqt, error)) return FALSE; rqt_data = fu_struct_ccgx_dmc_int_rqt_get_data(dmc_int_rqt, NULL); /* fw upgrade request */ rqt_opcode = fu_struct_ccgx_dmc_int_rqt_get_opcode(dmc_int_rqt); if (rqt_opcode != FU_CCGX_DMC_INT_OPCODE_FW_UPGRADE_RQT) break; img_index = rqt_data[0]; if (img_index >= image_records->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid image index %d, expected less than %u", img_index, image_records->len); return FALSE; } /* write image */ g_debug("writing image index %u/%u", img_index, image_records->len - 1); img_rcd = g_ptr_array_index(image_records, img_index); if (!fu_ccgx_dmc_device_write_firmware_image(device, img_rcd, &fw_data_written, fw_data_size, fu_progress_get_child(progress), error)) return FALSE; } if (rqt_opcode != FU_CCGX_DMC_INT_OPCODE_FW_UPGRADE_STATUS) { if (rqt_opcode == FU_CCGX_DMC_FWCT_ANALYSIS_STATUS_INVALID_FENCE) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot downgrade to this firmware version"); return FALSE; } if (rqt_opcode == FU_CCGX_DMC_INT_OPCODE_FWCT_ANALYSIS_STATUS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid fwct analysis failed with status 0x%02x[%s]", rqt_data[0], fu_ccgx_dmc_fwct_analysis_status_to_string(rqt_data[0])); return FALSE; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid dmc intr req opcode 0x%02x[%s] with status 0x%02x", rqt_opcode, fu_ccgx_dmc_int_opcode_to_string(rqt_opcode), rqt_data[0]); return FALSE; } if (rqt_data[0] == FU_CCGX_DMC_DEVICE_STATUS_UPDATE_PHASE1_COMPLETE_FULL_PHASE2_NOT_DONE) { self->update_model = FU_CCGX_DMC_UPDATE_MODEL_DOWNLOAD_TRIGGER; } else if (rqt_data[0] == FU_CCGX_DMC_DEVICE_STATUS_FW_DOWNLOADED_UPDATE_PEND) { self->update_model = FU_CCGX_DMC_UPDATE_MODEL_PENDING_RESET; } else if (rqt_data[0] >= FU_CCGX_DMC_DEVICE_STATUS_PHASE2_UPDATE_FAIL_INVALID_FWCT) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid status code = %u", rqt_data[0]); return FALSE; } fu_progress_step_done(progress); /* success */ return TRUE; } static FuFirmware * fu_ccgx_dmc_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { g_autoptr(FuFirmware) firmware = fu_ccgx_dmc_firmware_new(); FuCcgxDmcDevice *self = FU_CCGX_DMC_DEVICE(device); GBytes *custom_meta_blob = NULL; gboolean custom_meta_exist = FALSE; /* parse all images */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; /* get custom meta record */ custom_meta_blob = fu_ccgx_dmc_firmware_get_custom_meta_record(FU_CCGX_DMC_FIRMWARE(firmware)); if (custom_meta_blob) if (g_bytes_get_size(custom_meta_blob) > 0) custom_meta_exist = TRUE; /* check custom meta flag */ if (self->custom_meta_flag != custom_meta_exist) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "custom metadata mismatch"); return NULL; } return g_steal_pointer(&firmware); } static gboolean fu_ccgx_dmc_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuCcgxDmcDevice *self = FU_CCGX_DMC_DEVICE(device); gboolean manual_replug; manual_replug = fu_device_has_private_flag(device, FU_CCGX_DMC_DEVICE_FLAG_HAS_MANUAL_REPLUG); /* device action required */ if (self->update_model == FU_CCGX_DMC_UPDATE_MODEL_DOWNLOAD_TRIGGER) { if (self->trigger_code > 0) { if (!fu_ccgx_dmc_device_send_download_trigger(self, self->trigger_code, error)) { g_prefix_error(error, "download trigger error: "); return FALSE; } } } else if (self->update_model == FU_CCGX_DMC_UPDATE_MODEL_PENDING_RESET) { if (!fu_ccgx_dmc_device_send_soft_reset(self, manual_replug, error)) { g_prefix_error(error, "soft reset error: "); return FALSE; } } /* the user has to do something */ if (manual_replug) { g_autoptr(FwupdRequest) request = fwupd_request_new(); fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_IMMEDIATE); fwupd_request_set_id(request, FWUPD_REQUEST_ID_REMOVE_REPLUG); fwupd_request_add_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); if (!fu_device_emit_request(device, request, progress, error)) return FALSE; } /* success */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static void fu_ccgx_dmc_device_ensure_factory_version(FuCcgxDmcDevice *self) { GPtrArray *children = fu_device_get_children(FU_DEVICE(self)); for (guint i = 0; i < children->len; i++) { FuCcgxDmcDevxDevice *child = g_ptr_array_index(children, i); const guint8 *fw_version = fu_ccgx_dmc_devx_device_get_fw_version(child); FuCcgxDmcDevxDeviceType device_type = fu_ccgx_dmc_devx_device_get_device_type(child); guint64 fwver_img1 = fu_memread_uint64(fw_version + 0x08, G_LITTLE_ENDIAN); guint64 fwver_img2 = fu_memread_uint64(fw_version + 0x10, G_LITTLE_ENDIAN); if (device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_DMC && fwver_img1 == fwver_img2 && fwver_img1 != 0) { g_info("overriding version as device is in factory mode"); fu_device_set_version_raw(FU_DEVICE(self), 0x1); return; } } } static gboolean fu_ccgx_dmc_device_probe(FuDevice *device, GError **error) { FuCcgxDmcDevice *self = FU_CCGX_DMC_DEVICE(device); g_autoptr(FuUsbInterface) intf = NULL; /* find the correct vendor-specific interface */ intf = fu_usb_device_get_interface(FU_USB_DEVICE(self), 0xFF, 0x03, 0x00, error); if (intf == NULL) return FALSE; fu_usb_device_add_interface(FU_USB_DEVICE(self), fu_usb_interface_get_number(intf)); return TRUE; } static gboolean fu_ccgx_dmc_device_setup(FuDevice *device, GError **error) { FuCcgxDmcDevice *self = FU_CCGX_DMC_DEVICE(device); /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_ccgx_dmc_device_parent_class)->setup(device, error)) return FALSE; /* get dock identity */ if (!fu_ccgx_dmc_device_ensure_dock_id(self, error)) return FALSE; if (!fu_ccgx_dmc_device_ensure_status(self, error)) return FALSE; /* use composite version, but also try to detect "factory mode" where the SPI has been * imaged but has not been updated manually to the initial version */ if (fu_device_get_version_raw(device) == 0) fu_ccgx_dmc_device_ensure_factory_version(self); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); if (self->custom_meta_flag > 0) fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); else fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); if (fu_device_has_private_flag(device, FU_CCGX_DMC_DEVICE_FLAG_HAS_MANUAL_REPLUG)) { fu_device_add_request_flag(FU_DEVICE(self), FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); } return TRUE; } static gboolean fu_ccgx_dmc_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuCcgxDmcDevice *self = FU_CCGX_DMC_DEVICE(device); if (g_strcmp0(key, "CcgxDmcTriggerCode") == 0) { guint64 tmp = 0; if (!fu_strtoull(value, &tmp, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->trigger_code = tmp; return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no supported"); return FALSE; } static void fu_ccgx_dmc_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_NO_PROFILE); /* actually 0, 20, 0, 80! */ fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 75, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 25, "reload"); } static gchar * fu_ccgx_dmc_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_device_get_version_format(device)); } static void fu_ccgx_dmc_device_init(FuCcgxDmcDevice *self) { self->ep_intr_in = DMC_INTERRUPT_PIPE_ID; self->ep_bulk_out = DMC_BULK_PIPE_ID; self->trigger_code = 0x1; fu_device_add_protocol(FU_DEVICE(self), "com.cypress.ccgx.dmc"); fu_device_add_protocol(FU_DEVICE(self), "com.infineon.ccgx.dmc"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SELF_RECOVERY); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_register_private_flag(FU_DEVICE(self), FU_CCGX_DMC_DEVICE_FLAG_HAS_MANUAL_REPLUG); } static void fu_ccgx_dmc_device_class_init(FuCcgxDmcDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_ccgx_dmc_device_to_string; device_class->write_firmware = fu_ccgx_dmc_device_write_firmware; device_class->prepare_firmware = fu_ccgx_dmc_device_prepare_firmware; device_class->attach = fu_ccgx_dmc_device_attach; device_class->probe = fu_ccgx_dmc_device_probe; device_class->setup = fu_ccgx_dmc_device_setup; device_class->set_quirk_kv = fu_ccgx_dmc_device_set_quirk_kv; device_class->set_progress = fu_ccgx_dmc_device_set_progress; device_class->convert_version = fu_ccgx_dmc_device_convert_version; } fwupd-2.0.10/plugins/ccgx-dmc/fu-ccgx-dmc-device.h000066400000000000000000000005511501337203100215640ustar00rootroot00000000000000/* * Copyright 2020 Cypress Semiconductor Corporation. * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_CCGX_DMC_DEVICE (fu_ccgx_dmc_device_get_type()) G_DECLARE_FINAL_TYPE(FuCcgxDmcDevice, fu_ccgx_dmc_device, FU, CCGX_DMC_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/ccgx-dmc/fu-ccgx-dmc-devx-device.c000066400000000000000000000320471501337203100225300ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * Copyright 2020 Cypress Semiconductor Corporation. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-ccgx-dmc-devx-device.h" #define DMC_FW_WRITE_STATUS_RETRY_COUNT 3 #define DMC_FW_WRITE_STATUS_RETRY_DELAY_MS 30 struct _FuCcgxDmcDevxDevice { FuDevice parent_instance; GByteArray *status; /* DmcDevxStatus */ }; G_DEFINE_TYPE(FuCcgxDmcDevxDevice, fu_ccgx_dmc_devx_device, FU_TYPE_DEVICE) const guint8 * fu_ccgx_dmc_devx_device_get_fw_version(FuCcgxDmcDevxDevice *self) { return fu_struct_ccgx_dmc_devx_status_get_fw_version(self->status, NULL); } FuCcgxDmcDevxDeviceType fu_ccgx_dmc_devx_device_get_device_type(FuCcgxDmcDevxDevice *self) { return fu_struct_ccgx_dmc_devx_status_get_device_type(self->status); } static gchar * fu_ccgx_dmc_devx_device_version_dmc_bfw(FuCcgxDmcDevxDevice *self, gsize offset) { const guint8 *fw_version = fu_ccgx_dmc_devx_device_get_fw_version(self); return g_strdup_printf("%u.%u.%u.%u", (guint)(fw_version[offset + 3] >> 4), fw_version[offset + 3] & 0xFu, fw_version[offset + 2], fu_memread_uint16(fw_version + offset, G_LITTLE_ENDIAN)); } static gchar * fu_ccgx_dmc_devx_device_version_dmc_app(FuCcgxDmcDevxDevice *self, gsize offset) { const guint8 *fw_version = fu_ccgx_dmc_devx_device_get_fw_version(self); return g_strdup_printf("%u.%u.%u", (guint)(fw_version[offset + 4 + 3] >> 4), fw_version[offset + 4 + 3] & 0xFu, fw_version[offset + 4 + 2]); } static gchar * fu_ccgx_dmc_devx_device_version_hx3(FuCcgxDmcDevxDevice *self, gsize offset) { const guint8 *fw_version = fu_ccgx_dmc_devx_device_get_fw_version(self); return g_strdup_printf("%u.%u.%u", fw_version[offset + 4 + 3], fw_version[offset + 4 + 2], fw_version[offset + 4 + 1]); } static void fu_ccgx_dmc_devx_device_hexver_to_string(FuCcgxDmcDevxDevice *self, const gchar *kind, gsize offset, guint idt, GString *str) { const guint8 *fw_version = fu_ccgx_dmc_devx_device_get_fw_version(self); g_autofree gchar *key = g_strdup_printf("FwVersion[%s]", kind); g_autofree gchar *val = fu_version_from_uint64(fu_memread_uint64(fw_version + offset, G_LITTLE_ENDIAN), FWUPD_VERSION_FORMAT_HEX); fwupd_codec_string_append(str, idt, key, val); } static void fu_ccgx_dmc_devx_device_hx3ver_to_string(FuCcgxDmcDevxDevice *self, const gchar *kind, gsize offset, guint idt, GString *str) { g_autofree gchar *key = g_strdup_printf("FwVersion[%s]", kind); g_autofree gchar *val = fu_ccgx_dmc_devx_device_version_hx3(self, offset); fwupd_codec_string_append(str, idt, key, val); } static void fu_ccgx_dmc_devx_device_dmcver_to_string(FuCcgxDmcDevxDevice *self, const gchar *kind, gsize offset, guint idt, GString *str) { g_autofree gchar *key = g_strdup_printf("FwVersion[%s]", kind); g_autofree gchar *bfw_val = fu_ccgx_dmc_devx_device_version_dmc_bfw(self, offset); g_autofree gchar *app_val = fu_ccgx_dmc_devx_device_version_dmc_app(self, offset); g_autofree gchar *tmp = g_strdup_printf("base:%s\tapp:%s", bfw_val, app_val); fwupd_codec_string_append(str, idt, key, tmp); } static FuCcgxDmcDevxDeviceType fu_ccgx_dmc_devx_device_version_type(FuCcgxDmcDevxDevice *self) { guint8 device_type; if (self->status == NULL) return FU_CCGX_DMC_DEVX_DEVICE_TYPE_INVALID; device_type = fu_struct_ccgx_dmc_devx_status_get_device_type(self->status); if (device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_DMC || device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_CCG3 || device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_CCG4 || device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_CCG5 || device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_CCG6 || device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_CCG8 || device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_CCG6SF || device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_CCG7SC || device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_PMG1S3 || device_type == 0x0B) return FU_CCGX_DMC_DEVX_DEVICE_TYPE_DMC; if (device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_HX3) return FU_CCGX_DMC_DEVX_DEVICE_TYPE_HX3; return FU_CCGX_DMC_DEVX_DEVICE_TYPE_INVALID; } static void fu_ccgx_dmc_devx_device_to_string(FuDevice *device, guint idt, GString *str) { FuCcgxDmcDevxDevice *self = FU_CCGX_DMC_DEVX_DEVICE(device); FuCcgxDmcDevxDeviceType device_version_type = fu_ccgx_dmc_devx_device_version_type(self); guint8 device_type; guint8 image_mode; guint8 img_status; const gchar *device_type_str; if (self->status == NULL) return; device_type = fu_struct_ccgx_dmc_devx_status_get_device_type(self->status); device_type_str = fu_ccgx_dmc_devx_device_type_to_string(device_type); if (device_type_str != NULL) { g_autofree gchar *tmp = g_strdup_printf("0x%x [%s]", device_type, device_type_str); fwupd_codec_string_append(str, idt, "DeviceType", tmp); } else { fwupd_codec_string_append_hex(str, idt, "DeviceType", device_type); } image_mode = fu_struct_ccgx_dmc_devx_status_get_image_mode(self->status); if (image_mode < FU_CCGX_DMC_IMG_MODE_LAST) { g_autofree gchar *tmp = g_strdup_printf("0x%x [%s]", image_mode, fu_ccgx_dmc_img_mode_to_string(image_mode)); fwupd_codec_string_append(str, idt, "ImageMode", tmp); } else { fwupd_codec_string_append_hex(str, idt, "ImageMode", image_mode); } fwupd_codec_string_append_hex( str, idt, "CurrentImage", fu_struct_ccgx_dmc_devx_status_get_current_image(self->status)); img_status = fu_struct_ccgx_dmc_devx_status_get_img_status(self->status); fwupd_codec_string_append(str, idt, "ImgStatus1", fu_ccgx_dmc_img_status_to_string(img_status & 0x0F)); fwupd_codec_string_append(str, idt, "ImgStatus2", fu_ccgx_dmc_img_status_to_string((img_status >> 4) & 0x0F)); /* versions */ if (device_version_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_DMC) { fu_ccgx_dmc_devx_device_dmcver_to_string(self, "boot", 0x00, idt, str); fu_ccgx_dmc_devx_device_dmcver_to_string(self, "img1", 0x08, idt, str); if (image_mode != FU_CCGX_DMC_IMG_MODE_SINGLE_IMG) fu_ccgx_dmc_devx_device_dmcver_to_string(self, "img2", 0x10, idt, str); } else if (device_version_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_HX3) { fu_ccgx_dmc_devx_device_hx3ver_to_string(self, "boot", 0x00, idt, str); fu_ccgx_dmc_devx_device_hx3ver_to_string(self, "img1", 0x08, idt, str); if (image_mode != FU_CCGX_DMC_IMG_MODE_SINGLE_IMG) fu_ccgx_dmc_devx_device_hx3ver_to_string(self, "img2", 0x10, idt, str); } else { fu_ccgx_dmc_devx_device_hexver_to_string(self, "boot", 0x00, idt, str); fu_ccgx_dmc_devx_device_hexver_to_string(self, "img1", 0x08, idt, str); if (image_mode != FU_CCGX_DMC_IMG_MODE_SINGLE_IMG) fu_ccgx_dmc_devx_device_hexver_to_string(self, "img2", 0x10, idt, str); } } static gboolean fu_ccgx_dmc_devx_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { if (g_strcmp0(key, "CcgxDmcCompositeVersion") == 0) { guint64 tmp = 0; FuDevice *proxy = fu_device_get_proxy(device); if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; if (fu_device_get_version_raw(proxy) != tmp) { g_debug("overriding composite version from %u to %u from %s", (guint)fu_device_get_version_raw(proxy), (guint)tmp, fu_device_get_id(device)); fu_device_set_version_raw(proxy, tmp); } return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no supported"); return FALSE; } static const gchar * fu_ccgx_dmc_devx_device_type_to_name(FuCcgxDmcDevxDeviceType device_type) { if (device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_CCG3) return "CCG3"; if (device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_DMC) return "DMC"; if (device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_CCG4) return "CCG4"; if (device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_CCG5) return "CCG5"; if (device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_HX3) return "HX3"; if (device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_HX3_PD) return "HX3 PD"; if (device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_DMC_PD) return "DMC PD"; if (device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_CCG6) return "CCG6"; if (device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_CCG6SF) return "CCG6SF"; if (device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_CCG7SC) return "CCG7SC"; if (device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_CCG8) return "CCG8"; if (device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_PMG1S3) return "PMG1S3"; if (device_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_SPI) return "SPI"; return NULL; } guint fu_ccgx_dmc_devx_device_get_remove_delay(FuCcgxDmcDevxDevice *self) { guint remove_delay = 0; g_return_val_if_fail(FU_IS_CCGX_DMC_DEVX_DEVICE(self), G_MAXUINT); switch (fu_struct_ccgx_dmc_devx_status_get_device_type(self->status)) { case FU_CCGX_DMC_DEVX_DEVICE_TYPE_DMC: remove_delay = 40 * 1000; break; default: remove_delay = 30 * 1000; break; } return remove_delay; } static gboolean fu_ccgx_dmc_devx_device_probe(FuDevice *device, GError **error) { FuCcgxDmcDevxDevice *self = FU_CCGX_DMC_DEVX_DEVICE(device); FuDevice *proxy = fu_device_get_proxy(device); FuCcgxDmcDevxDeviceType device_version_type = fu_ccgx_dmc_devx_device_version_type(self); gsize offset = 0; guint8 device_type; g_autofree gchar *logical_id = NULL; g_autofree gchar *name = NULL; g_autofree gchar *version = NULL; /* sanity check */ if (self->status == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no status"); return FALSE; } device_type = fu_struct_ccgx_dmc_devx_status_get_device_type(self->status); name = g_strdup(fu_ccgx_dmc_devx_device_type_to_name(device_type)); if (name == NULL) name = g_strdup_printf("Custom Component %03u", device_type); fu_device_set_name(device, name); logical_id = g_strdup_printf("0x%02x", fu_struct_ccgx_dmc_devx_status_get_component_id(self->status)); fu_device_set_logical_id(device, logical_id); /* for the version number */ if (fu_struct_ccgx_dmc_devx_status_get_current_image(self->status) == 0x01) offset = 4; else if (fu_struct_ccgx_dmc_devx_status_get_current_image(self->status) == 0x02) offset = 8; /* version, if possible */ if (device_version_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_DMC) { version = fu_ccgx_dmc_devx_device_version_dmc_bfw(self, offset); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); } else if (device_version_type == FU_CCGX_DMC_DEVX_DEVICE_TYPE_HX3) { version = fu_ccgx_dmc_devx_device_version_hx3(self, offset); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); } if (version != NULL) { fu_device_set_version(device, version); /* nocheck:set-version */ fu_device_add_instance_strsafe(device, "VER", version); } /* add GUIDs */ fu_device_add_instance_strup(device, "TYPE", fu_ccgx_dmc_devx_device_type_to_string(device_type)); fu_device_add_instance_u8(device, "CID", fu_struct_ccgx_dmc_devx_status_get_component_id(self->status)); fu_device_add_instance_u16(device, "VID", fu_device_get_vid(proxy)); fu_device_add_instance_u16(device, "PID", fu_device_get_pid(proxy)); fu_device_build_instance_id(device, NULL, "USB", "VID", "PID", "CID", NULL); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "USB", "VID", "PID", "CID", "TYPE", NULL); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "USB", "VID", "PID", "CID", "VER", NULL); /* success */ return TRUE; } static gchar * fu_ccgx_dmc_devx_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_device_get_version_format(device)); } static void fu_ccgx_dmc_devx_device_init(FuCcgxDmcDevxDevice *self) { } static void fu_ccgx_dmc_devx_device_finalize(GObject *object) { FuCcgxDmcDevxDevice *self = FU_CCGX_DMC_DEVX_DEVICE(object); if (self->status != NULL) fu_struct_ccgx_dmc_devx_status_unref(self->status); G_OBJECT_CLASS(fu_ccgx_dmc_devx_device_parent_class)->finalize(object); } static void fu_ccgx_dmc_devx_device_class_init(FuCcgxDmcDevxDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_ccgx_dmc_devx_device_finalize; device_class->probe = fu_ccgx_dmc_devx_device_probe; device_class->to_string = fu_ccgx_dmc_devx_device_to_string; device_class->set_quirk_kv = fu_ccgx_dmc_devx_device_set_quirk_kv; device_class->convert_version = fu_ccgx_dmc_devx_device_convert_version; } FuCcgxDmcDevxDevice * fu_ccgx_dmc_devx_device_new(FuDevice *proxy, const guint8 *buf, gsize bufsz, gsize offset, GError **error) { g_autoptr(FuCcgxDmcDevxDevice) self = g_object_new(FU_TYPE_CCGX_DMC_DEVX_DEVICE, "context", fu_device_get_context(proxy), "proxy", proxy, NULL); self->status = fu_struct_ccgx_dmc_devx_status_parse(buf, bufsz, offset, error); if (self->status == NULL) return NULL; return g_steal_pointer(&self); } fwupd-2.0.10/plugins/ccgx-dmc/fu-ccgx-dmc-devx-device.h000066400000000000000000000015231501337203100225300ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * Copyright 2020 Cypress Semiconductor Corporation. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-ccgx-dmc-struct.h" #define FU_TYPE_CCGX_DMC_DEVX_DEVICE (fu_ccgx_dmc_devx_device_get_type()) G_DECLARE_FINAL_TYPE(FuCcgxDmcDevxDevice, fu_ccgx_dmc_devx_device, FU, CCGX_DMC_DEVX_DEVICE, FuDevice) FuCcgxDmcDevxDevice * fu_ccgx_dmc_devx_device_new(FuDevice *proxy, const guint8 *buf, gsize bufsz, gsize offset, GError **error); guint fu_ccgx_dmc_devx_device_get_remove_delay(FuCcgxDmcDevxDevice *self); const guint8 * fu_ccgx_dmc_devx_device_get_fw_version(FuCcgxDmcDevxDevice *self); FuCcgxDmcDevxDeviceType fu_ccgx_dmc_devx_device_get_device_type(FuCcgxDmcDevxDevice *self); fwupd-2.0.10/plugins/ccgx-dmc/fu-ccgx-dmc-firmware.c000066400000000000000000000345161501337203100221440ustar00rootroot00000000000000/* * Copyright 2020 Cypress Semiconductor Corporation. * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-ccgx-dmc-firmware.h" #include "fu-ccgx-dmc-struct.h" struct _FuCcgxDmcFirmware { FuFirmwareClass parent_instance; GPtrArray *image_records; GBytes *fwct_blob; GBytes *custom_meta_blob; guint32 row_data_offset_start; guint32 fw_data_size; }; G_DEFINE_TYPE(FuCcgxDmcFirmware, fu_ccgx_dmc_firmware, FU_TYPE_FIRMWARE) #define DMC_FWCT_MAX_SIZE 2048 #define DMC_HASH_SIZE 32 #define DMC_CUSTOM_META_LENGTH_FIELD_SIZE 2 static void fu_ccgx_dmc_firmware_record_free(FuCcgxDmcFirmwareRecord *rcd) { if (rcd->seg_records != NULL) g_ptr_array_unref(rcd->seg_records); g_free(rcd); } static void fu_ccgx_dmc_firmware_segment_record_free(FuCcgxDmcFirmwareSegmentRecord *rcd) { if (rcd->data_records != NULL) g_ptr_array_unref(rcd->data_records); g_free(rcd); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuCcgxDmcFirmwareRecord, fu_ccgx_dmc_firmware_record_free) G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuCcgxDmcFirmwareSegmentRecord, fu_ccgx_dmc_firmware_segment_record_free) GPtrArray * fu_ccgx_dmc_firmware_get_image_records(FuCcgxDmcFirmware *self) { g_return_val_if_fail(FU_IS_CCGX_DMC_FIRMWARE(self), NULL); return self->image_records; } GBytes * fu_ccgx_dmc_firmware_get_fwct_record(FuCcgxDmcFirmware *self) { g_return_val_if_fail(FU_IS_CCGX_DMC_FIRMWARE(self), NULL); return self->fwct_blob; } GBytes * fu_ccgx_dmc_firmware_get_custom_meta_record(FuCcgxDmcFirmware *self) { g_return_val_if_fail(FU_IS_CCGX_DMC_FIRMWARE(self), NULL); return self->custom_meta_blob; } guint32 fu_ccgx_dmc_firmware_get_fw_data_size(FuCcgxDmcFirmware *self) { g_return_val_if_fail(FU_IS_CCGX_DMC_FIRMWARE(self), 0); return self->fw_data_size; } static void fu_ccgx_dmc_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuCcgxDmcFirmware *self = FU_CCGX_DMC_FIRMWARE(firmware); if (flags & FU_FIRMWARE_EXPORT_FLAG_INCLUDE_DEBUG) { fu_xmlb_builder_insert_kx(bn, "fw_data_size", self->fw_data_size); fu_xmlb_builder_insert_kx(bn, "image_records", self->image_records->len); } } static gboolean fu_ccgx_dmc_firmware_parse_segment(FuFirmware *firmware, GInputStream *stream, FuCcgxDmcFirmwareRecord *img_rcd, gsize *seg_off, FuFirmwareParseFlags flags, GError **error) { FuCcgxDmcFirmware *self = FU_CCGX_DMC_FIRMWARE(firmware); gsize row_off; g_autoptr(GChecksum) csum = g_checksum_new(G_CHECKSUM_SHA256); /* set row data offset in current image */ row_off = self->row_data_offset_start + img_rcd->img_offset; /* parse segment in image */ img_rcd->seg_records = g_ptr_array_new_with_free_func((GFreeFunc)fu_ccgx_dmc_firmware_segment_record_free); for (guint32 i = 0; i < img_rcd->num_img_segments; i++) { guint16 row_size_bytes = 0; g_autoptr(FuCcgxDmcFirmwareSegmentRecord) seg_rcd = NULL; g_autoptr(GByteArray) st_info = NULL; /* read segment info */ seg_rcd = g_new0(FuCcgxDmcFirmwareSegmentRecord, 1); st_info = fu_struct_ccgx_dmc_fwct_segmentation_info_parse_stream(stream, *seg_off, error); if (st_info == NULL) return FALSE; seg_rcd->start_row = fu_struct_ccgx_dmc_fwct_segmentation_info_get_start_row(st_info); seg_rcd->num_rows = fu_struct_ccgx_dmc_fwct_segmentation_info_get_num_rows(st_info); /* calculate actual row size */ row_size_bytes = img_rcd->row_size * 64; /* create data record array in segment record */ seg_rcd->data_records = g_ptr_array_new_with_free_func((GDestroyNotify)g_bytes_unref); /* read row data in segment */ for (int row = 0; row < seg_rcd->num_rows; row++) { g_autoptr(GBytes) data_rcd = NULL; data_rcd = fu_input_stream_read_bytes(stream, row_off, row_size_bytes, NULL, error); if (data_rcd == NULL) return FALSE; /* update hash */ g_checksum_update(csum, (guchar *)g_bytes_get_data(data_rcd, NULL), g_bytes_get_size(data_rcd)); /* add row data to data record */ g_ptr_array_add(seg_rcd->data_records, g_steal_pointer(&data_rcd)); /* increment row data offset */ row_off += row_size_bytes; } /* add segment record to segment array */ g_ptr_array_add(img_rcd->seg_records, g_steal_pointer(&seg_rcd)); /* increment segment info offset */ *seg_off += st_info->len; } /* check checksum */ if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { guint8 csumbuf[DMC_HASH_SIZE] = {0x0}; gsize csumbufsz = sizeof(csumbuf); g_checksum_get_digest(csum, csumbuf, &csumbufsz); if (memcmp(csumbuf, img_rcd->img_digest, DMC_HASH_SIZE) != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid hash"); return FALSE; } } /* success */ return TRUE; } static gboolean fu_ccgx_dmc_firmware_parse_image(FuFirmware *firmware, guint8 image_count, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuCcgxDmcFirmware *self = FU_CCGX_DMC_FIRMWARE(firmware); gsize img_off = FU_STRUCT_CCGX_DMC_FWCT_INFO_SIZE; gsize seg_off = FU_STRUCT_CCGX_DMC_FWCT_INFO_SIZE + image_count * FU_STRUCT_CCGX_DMC_FWCT_IMAGE_INFO_SIZE; /* set initial segment info offset */ for (guint32 i = 0; i < image_count; i++) { g_autoptr(FuCcgxDmcFirmwareRecord) img_rcd = NULL; g_autoptr(GByteArray) st_img = NULL; /* read image info */ img_rcd = g_new0(FuCcgxDmcFirmwareRecord, 1); st_img = fu_struct_ccgx_dmc_fwct_image_info_parse_stream(stream, img_off, error); if (st_img == NULL) return FALSE; img_rcd->row_size = fu_struct_ccgx_dmc_fwct_image_info_get_row_size(st_img); if (img_rcd->row_size == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid row size 0x%x", img_rcd->row_size); return FALSE; } img_rcd->img_offset = fu_struct_ccgx_dmc_fwct_image_info_get_img_offset(st_img); img_rcd->num_img_segments = fu_struct_ccgx_dmc_fwct_image_info_get_num_img_segments(st_img); /* segments are optional */ if (img_rcd->num_img_segments > 0) { gsize img_digestsz = 0; const guint8 *img_digest; img_digest = fu_struct_ccgx_dmc_fwct_image_info_get_img_digest(st_img, &img_digestsz); if (!fu_memcpy_safe((guint8 *)&img_rcd->img_digest, sizeof(img_rcd->img_digest), 0x0, /* dst */ img_digest, img_digestsz, 0, /* src */ img_digestsz, error)) return FALSE; /* parse segment */ if (!fu_ccgx_dmc_firmware_parse_segment(firmware, stream, img_rcd, &seg_off, flags, error)) return FALSE; /* add image record to image record array */ g_ptr_array_add(self->image_records, g_steal_pointer(&img_rcd)); } /* increment image offset */ img_off += FU_STRUCT_CCGX_DMC_FWCT_IMAGE_INFO_SIZE; } return TRUE; } static gboolean fu_ccgx_dmc_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_ccgx_dmc_fwct_info_validate_stream(stream, offset, error); } static gboolean fu_ccgx_dmc_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuCcgxDmcFirmware *self = FU_CCGX_DMC_FIRMWARE(firmware); gsize streamsz = 0; guint16 hdr_size = 0; guint16 mdbufsz = 0; guint32 hdr_composite_version = 0; guint8 hdr_image_count = 0; g_autoptr(GByteArray) st_hdr = NULL; /* parse */ st_hdr = fu_struct_ccgx_dmc_fwct_info_parse_stream(stream, 0x0, error); if (st_hdr == NULL) return FALSE; /* check fwct size */ hdr_size = fu_struct_ccgx_dmc_fwct_info_get_size(st_hdr); if (hdr_size > DMC_FWCT_MAX_SIZE || hdr_size == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid dmc fwct size, expected <= 0x%x, got 0x%x", (guint)DMC_FWCT_MAX_SIZE, (guint)hdr_size); return FALSE; } /* set version */ hdr_composite_version = fu_struct_ccgx_dmc_fwct_info_get_composite_version(st_hdr); if (hdr_composite_version != 0) fu_firmware_set_version_raw(firmware, hdr_composite_version); /* read fwct data */ self->fwct_blob = fu_input_stream_read_bytes(stream, 0x0, hdr_size, NULL, error); if (self->fwct_blob == NULL) return FALSE; /* create custom meta binary */ if (!fu_input_stream_read_u16(stream, hdr_size, &mdbufsz, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to read metadata size: "); return FALSE; } if (mdbufsz > 0) { self->custom_meta_blob = fu_input_stream_read_bytes(stream, hdr_size + 2, mdbufsz, NULL, error); if (self->custom_meta_blob == NULL) return FALSE; } /* set row data start offset */ self->row_data_offset_start = hdr_size + DMC_CUSTOM_META_LENGTH_FIELD_SIZE + mdbufsz; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; self->fw_data_size = streamsz - self->row_data_offset_start; /* parse image */ hdr_image_count = fu_struct_ccgx_dmc_fwct_info_get_image_count(st_hdr); if (!fu_ccgx_dmc_firmware_parse_image(firmware, hdr_image_count, stream, flags, error)) return FALSE; /* success */ return TRUE; } static GByteArray * fu_ccgx_dmc_firmware_write(FuFirmware *firmware, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GByteArray) st_hdr = fu_struct_ccgx_dmc_fwct_info_new(); g_autoptr(GPtrArray) images = fu_firmware_get_images(firmware); /* add header */ fu_struct_ccgx_dmc_fwct_info_set_size( st_hdr, FU_STRUCT_CCGX_DMC_FWCT_INFO_SIZE + (images->len * (FU_STRUCT_CCGX_DMC_FWCT_IMAGE_INFO_SIZE + FU_STRUCT_CCGX_DMC_FWCT_SEGMENTATION_INFO_SIZE))); fu_struct_ccgx_dmc_fwct_info_set_version(st_hdr, 0x2); fu_struct_ccgx_dmc_fwct_info_set_custom_meta_type(st_hdr, 0x3); fu_struct_ccgx_dmc_fwct_info_set_cdtt_version(st_hdr, 0x1); fu_struct_ccgx_dmc_fwct_info_set_device_id(st_hdr, 0x1); fu_struct_ccgx_dmc_fwct_info_set_composite_version(st_hdr, fu_firmware_get_version_raw(firmware)); fu_struct_ccgx_dmc_fwct_info_set_image_count(st_hdr, images->len); g_byte_array_append(buf, st_hdr->data, st_hdr->len); /* add image headers */ for (guint i = 0; i < images->len; i++) { g_autoptr(GByteArray) st_img = fu_struct_ccgx_dmc_fwct_image_info_new(); fu_struct_ccgx_dmc_fwct_image_info_set_device_type(st_img, 0x2); fu_struct_ccgx_dmc_fwct_image_info_set_img_type(st_img, 0x1); fu_struct_ccgx_dmc_fwct_image_info_set_row_size(st_img, 0x1); fu_struct_ccgx_dmc_fwct_image_info_set_fw_version(st_img, 0x330006d2); fu_struct_ccgx_dmc_fwct_image_info_set_app_version(st_img, 0x14136161); fu_struct_ccgx_dmc_fwct_image_info_set_num_img_segments(st_img, 0x1); g_byte_array_append(buf, st_img->data, st_img->len); } /* add segments */ for (guint i = 0; i < images->len; i++) { FuFirmware *img = g_ptr_array_index(images, i); g_autoptr(GByteArray) st_info = fu_struct_ccgx_dmc_fwct_segmentation_info_new(); g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(GBytes) img_bytes = fu_firmware_get_bytes(img, error); if (img_bytes == NULL) return NULL; chunks = fu_chunk_array_new_from_bytes(img_bytes, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 64); fu_struct_ccgx_dmc_fwct_segmentation_info_set_num_rows( st_info, MAX(fu_chunk_array_length(chunks), 1)); g_byte_array_append(buf, st_info->data, st_info->len); } /* metadata */ fu_byte_array_append_uint16(buf, 0x1, G_LITTLE_ENDIAN); fu_byte_array_append_uint8(buf, 0xff); /* add image headers */ for (guint i = 0; i < images->len; i++) { FuFirmware *img = g_ptr_array_index(images, i); gsize csumbufsz = DMC_HASH_SIZE; gsize img_offset = FU_STRUCT_CCGX_DMC_FWCT_INFO_SIZE + (i * FU_STRUCT_CCGX_DMC_FWCT_IMAGE_INFO_SIZE); guint8 csumbuf[DMC_HASH_SIZE] = {0x0}; g_autoptr(GChecksum) csum = g_checksum_new(G_CHECKSUM_SHA256); g_autoptr(GBytes) img_bytes = NULL; g_autoptr(GBytes) img_padded = NULL; g_autoptr(FuChunkArray) chunks = NULL; img_bytes = fu_firmware_get_bytes(img, error); if (img_bytes == NULL) return NULL; chunks = fu_chunk_array_new_from_bytes(img_bytes, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 64); img_padded = fu_bytes_pad(img_bytes, MAX(fu_chunk_array_length(chunks), 1) * 64, 0xFF); fu_byte_array_append_bytes(buf, img_padded); g_checksum_update(csum, (const guchar *)g_bytes_get_data(img_padded, NULL), g_bytes_get_size(img_padded)); g_checksum_get_digest(csum, csumbuf, &csumbufsz); /* update checksum */ if (!fu_memcpy_safe(buf->data, buf->len, /* dst */ img_offset + FU_STRUCT_CCGX_DMC_FWCT_IMAGE_INFO_OFFSET_IMG_DIGEST, csumbuf, sizeof(csumbuf), 0x0, /* src */ sizeof(csumbuf), error)) return NULL; } return g_steal_pointer(&buf); } static gchar * fu_ccgx_dmc_firmware_convert_version(FuFirmware *firmware, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_firmware_get_version_format(firmware)); } static void fu_ccgx_dmc_firmware_init(FuCcgxDmcFirmware *self) { self->image_records = g_ptr_array_new_with_free_func((GFreeFunc)fu_ccgx_dmc_firmware_record_free); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); fu_firmware_set_version_format(FU_FIRMWARE(self), FWUPD_VERSION_FORMAT_QUAD); } static void fu_ccgx_dmc_firmware_finalize(GObject *object) { FuCcgxDmcFirmware *self = FU_CCGX_DMC_FIRMWARE(object); if (self->fwct_blob != NULL) g_bytes_unref(self->fwct_blob); if (self->custom_meta_blob != NULL) g_bytes_unref(self->custom_meta_blob); if (self->image_records != NULL) g_ptr_array_unref(self->image_records); G_OBJECT_CLASS(fu_ccgx_dmc_firmware_parent_class)->finalize(object); } static void fu_ccgx_dmc_firmware_class_init(FuCcgxDmcFirmwareClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->convert_version = fu_ccgx_dmc_firmware_convert_version; object_class->finalize = fu_ccgx_dmc_firmware_finalize; firmware_class->validate = fu_ccgx_dmc_firmware_validate; firmware_class->parse = fu_ccgx_dmc_firmware_parse; firmware_class->write = fu_ccgx_dmc_firmware_write; firmware_class->export = fu_ccgx_dmc_firmware_export; } FuFirmware * fu_ccgx_dmc_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_CCGX_DMC_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/ccgx-dmc/fu-ccgx-dmc-firmware.h000066400000000000000000000017401501337203100221420ustar00rootroot00000000000000/* * Copyright 2020 Cypress Semiconductor Corporation. * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_CCGX_DMC_FIRMWARE (fu_ccgx_dmc_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuCcgxDmcFirmware, fu_ccgx_dmc_firmware, FU, CCGX_DMC_FIRMWARE, FuFirmware) typedef struct { guint16 start_row; guint16 num_rows; GPtrArray *data_records; } FuCcgxDmcFirmwareSegmentRecord; typedef struct { guint8 row_size; guint32 img_offset; guint8 img_digest[32]; guint8 num_img_segments; GPtrArray *seg_records; } FuCcgxDmcFirmwareRecord; FuFirmware * fu_ccgx_dmc_firmware_new(void); GPtrArray * fu_ccgx_dmc_firmware_get_image_records(FuCcgxDmcFirmware *self); GBytes * fu_ccgx_dmc_firmware_get_fwct_record(FuCcgxDmcFirmware *self); GBytes * fu_ccgx_dmc_firmware_get_custom_meta_record(FuCcgxDmcFirmware *self); guint32 fu_ccgx_dmc_firmware_get_fw_data_size(FuCcgxDmcFirmware *self); fwupd-2.0.10/plugins/ccgx-dmc/fu-ccgx-dmc-plugin.c000066400000000000000000000021471501337203100216210ustar00rootroot00000000000000/* * Copyright 2020 Cypress Semiconductor Corporation. * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-ccgx-dmc-device.h" #include "fu-ccgx-dmc-devx-device.h" #include "fu-ccgx-dmc-firmware.h" #include "fu-ccgx-dmc-plugin.h" struct _FuCcgxDmcPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuCcgxDmcPlugin, fu_ccgx_dmc_plugin, FU_TYPE_PLUGIN) static void fu_ccgx_dmc_plugin_init(FuCcgxDmcPlugin *self) { } static void fu_ccgx_dmc_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "CcgxDmcTriggerCode"); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_CCGX_DMC_FIRMWARE); fu_plugin_set_device_gtype_default(plugin, FU_TYPE_CCGX_DMC_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_CCGX_DMC_DEVX_DEVICE); /* coverage */ } static void fu_ccgx_dmc_plugin_class_init(FuCcgxDmcPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_ccgx_dmc_plugin_constructed; } fwupd-2.0.10/plugins/ccgx-dmc/fu-ccgx-dmc-plugin.h000066400000000000000000000003611501337203100216220ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuCcgxDmcPlugin, fu_ccgx_dmc_plugin, FU, CCGX_DMC_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/ccgx-dmc/fu-ccgx-dmc.rs000066400000000000000000000151101501337203100205210ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later enum FuCcgxDmcImgType { Invalid = 0, Image0, Image1, } #[derive(ToString)] enum FuCcgxDmcImgStatus { Unknown = 0, Valid, Invalid, Recovery, RecoveredFromSecondary, NotSupported = 0x0F, } // flash architecture #[derive(ToString)] #[repr(u8)] enum FuCcgxDmcImgMode { // indicates that the device has a single image SingleImg, // the device supports symmetric boot. In symmetric mode the bootloader // boots the image with higher version, when they are valid DualImgSym, // the device supports Asymmetric boot. Image-1 & 2 can be different or // same. in this method Bootloader is hard coded to boot the primary // image. Secondary acts as recovery DualImgAsym, SingleImgWithRamImg, } // dock status #[derive(ToString)] #[repr(u8)] enum FuCcgxDmcDeviceStatus { // status code indicating DOCK IDLE state. SUCCESS: no malfunctioning // no outstanding request or event Idle = 0, // status code indicating dock FW update in progress UpdatePhase1InProgress, // status code indicating dock FW update is partially complete UpdatePhase1Partial, // status code indicating dock FW update SUCCESS - all m_images of all // devices are valid UpdateCompleteFull, // status code indicating dock FW update SUCCESS - not all m_images of all // devices are valid UpdatePhase1CompletePartial, // fw download status UpdatePhase1CompleteFullPhase2NotDone, FwDownloadedUpdatePend, FwDownloadedPartialUpdatePend, Phase2UpdateInProgress = 0x81, Phase2UpdatePartial, Phase2UpdateFactoryBackup, Phase2UpdateCompletePartial, Phase2UpdateCompleteFull, Phase2UpdateFailInvalidFwct, Phase2UpdateFailInvalidDockIdentity, Phase2UpdateFailInvalidCompositeVer, Phase2UpdateFailAuthenticationFailed, Phase2UpdateFailInvalidAlgorithm, Phase2UpdateFailSpiReadFailed, Phase2UpdateFailNoValidKey, Phase2UpdateFailNoValidSpiPackage, Phase2UpdateFailRamInitFailed, Phase2UpdateFailFactoryBackupFailed, Phase2UpdateFailNoValidFactoryPackage, // status code indicating dock FW update FAILED UpdateFail = 0xff, } #[derive(ToString)] #[repr(u8)] enum FuCcgxDmcDevxDeviceType { Invalid = 0x00, Ccg3 = 0x01, Dmc = 0x02, Ccg4 = 0x03, Ccg5 = 0x04, Hx3 = 0x05, Hx3Pd = 0x0A, DmcPd = 0x0B, Ccg6 = 0x13, Pmg1s3 = 0xF0, Ccg7sc = 0xF1, Ccg6sf = 0xF2, Ccg8 = 0xF3, Spi = 0xFF, } // request codes for vendor interface enum FuCcgxDmcRqtCode { UpgradeStart = 0xD0, Reserv0, FwctWrite, ImgWrite, Reserv1, Reserv2, DockStatus, DockIdentity, ResetStateMachine, // command to reset dmc state machine of DMC SoftReset = 0xDC, // command to reset for online enhanced mode (no reset during update) Trigger = 0xDA, // Update Trigger command for offline mode } // opcode of interrupt read #[derive(ToString)] #[repr(u8)] enum FuCcgxDmcIntOpcode { FwUpgradeRqt = 1, FwUpgradeStatus = 0x80, ImgWriteStatus, Reenum, FwctAnalysisStatus, } // fwct analysis status #[derive(ToString)] enum FuCcgxDmcFwctAnalysisStatus { InvalidFwct = 0, InvalidDockIdentity, InvalidCompositeVersion, AuthenticationFailed, InvalidAlgorithm, InvalidFence, } #[derive(ToString)] enum FuCcgxDmcUpdateModel { None = 0, DownloadTrigger, // need to trigger after updating FW PendingReset, // need to set soft reset after updating FW } // fields of data returned when reading dock_identity for new firmware #[derive(New, Getters)] #[repr(C, packed)] struct FuStructCcgxDmcDockIdentity { // this field indicates both validity and structure version // 0 : invalid // 1 : old structure // 2 : new structure structure_version: u8, cdtt_version: u8, vid: u16le, pid: u16le, device_id: u16le, vendor_string: [char; 32], product_string: [char; 32], custom_meta_data_flag: u8, // model field indicates the type of the firmware upgrade status // 0 - online/offline // 1 - Online model // 2 - ADICORA/Offline model // 3 - No reset // 4 - 0xFF - Reserved model: u8, } // fields of status of a specific device #[derive(Parse)] #[repr(C, packed)] struct FuStructCcgxDmcDevxStatus { // device ID of the device device_type: FuCcgxDmcDevxDeviceType, // component ID of the device component_id: u8, // image mode of the device - single image/ dual symmetric/ dual // asymmetric image > image_mode: FuCcgxDmcImgMode, // current running image current_image: u8, // image status // b7:b4 => Image 2 status // b3:b0 => Image 1 status // 0 = Unknown // 1 = Valid // 2 = Invalid // 3-0xF = Reserved img_status: u8, // padding _reserved0: [u8; 3], // complete fw version 8 bytes for bootload, image1 and image2. 8 byte // for fw version and application version fw_version: [u8; 24], } // fields of data returned when reading dock_status #[derive(New, Getters)] #[repr(C, packed)] struct FuStructCcgxDmcDockStatus { device_status: FuCcgxDmcDeviceStatus, device_count: u8, status_length: u16le, // including dock_status, devx_status for each device composite_version: u32le, // dock composite version m_fwct_info // CcgxDmcDevxStatus devx_status[DMC_DOCK_MAX_DEV_COUNT], } // fields of data returned when reading an interrupt from DMC #[derive(New, Getters)] #[repr(C, packed)] struct FuStructCcgxDmcIntRqt { opcode: FuCcgxDmcIntOpcode, length: u8, data: [u8; 8], } // header structure of FWCT #[derive(New, ParseStream, ValidateStream, Default)] #[repr(C, packed)] struct FuStructCcgxDmcFwctInfo { signature: u32le == 0x54435746, // 'F' 'W' 'C' 'T' size: u16le, checksum: u8, version: u8, custom_meta_type: u8, cdtt_version: u8, vid: u16le, pid: u16le, device_id: u16le, _reserv0: [u8; 16], composite_version: u32le, image_count: u8, _reserv1: [u8; 3], } #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructCcgxDmcFwctImageInfo { device_type: u8, img_type: u8, comp_id: u8, row_size: u8, _reserv0: [u8; 4], fw_version: u32le, app_version: u32le, img_offset: u32le, img_size: u32le, img_digest: [u8; 32], num_img_segments: u8, _reserv1: [u8; 3], } #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructCcgxDmcFwctSegmentationInfo { img_id: u8, type: u8, start_row: u16le, num_rows: u16le, // size _reserv0: [u8; 2], } fwupd-2.0.10/plugins/ccgx-dmc/fu-self-test.c000066400000000000000000000033731501337203100205500ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-ccgx-dmc-firmware.h" static void fu_ccgx_dmc_firmware_xml_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autofree gchar *csum1 = NULL; g_autofree gchar *csum2 = NULL; g_autofree gchar *xml_out = NULL; g_autofree gchar *xml_src = NULL; g_autoptr(FuFirmware) firmware1 = fu_ccgx_dmc_firmware_new(); g_autoptr(FuFirmware) firmware2 = fu_ccgx_dmc_firmware_new(); g_autoptr(GError) error = NULL; /* build and write */ filename = g_test_build_filename(G_TEST_DIST, "tests", "ccgx-dmc.builder.xml", NULL); ret = g_file_get_contents(filename, &xml_src, NULL, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_firmware_build_from_xml(firmware1, xml_src, &error); g_assert_no_error(error); g_assert_true(ret); csum1 = fu_firmware_get_checksum(firmware1, G_CHECKSUM_SHA1, &error); g_assert_no_error(error); g_assert_cmpstr(csum1, ==, "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"); /* ensure we can round-trip */ xml_out = fu_firmware_export_to_xml(firmware1, FU_FIRMWARE_EXPORT_FLAG_NONE, &error); g_assert_no_error(error); ret = fu_firmware_build_from_xml(firmware2, xml_out, &error); g_assert_no_error(error); g_assert_true(ret); csum2 = fu_firmware_get_checksum(firmware2, G_CHECKSUM_SHA1, &error); g_assert_cmpstr(csum1, ==, csum2); } int main(int argc, char **argv) { (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); /* tests go here */ g_test_add_func("/ccgx-dmc/firmware{xml}", fu_ccgx_dmc_firmware_xml_func); return g_test_run(); } fwupd-2.0.10/plugins/ccgx-dmc/meson.build000066400000000000000000000025601501337203100202250ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginCcgxDmc"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files(['ccgx-dmc.quirk']) plugin_builtin_ccgx_dmc = static_library('fu_plugin_ccgx_dmc', rustgen.process( 'fu-ccgx-dmc.rs', # fuzzing ), sources: [ 'fu-ccgx-dmc-plugin.c', 'fu-ccgx-dmc-device.c', 'fu-ccgx-dmc-devx-device.c', 'fu-ccgx-dmc-firmware.c', # fuzzing ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) plugin_builtins += plugin_builtin_ccgx_dmc device_tests += files( 'tests/hp-dock-g5.json', ) if get_option('tests') install_data(['tests/ccgx-dmc.builder.xml'], install_dir: join_paths(installed_test_datadir, 'tests')) env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) e = executable( 'ccgx-dmc-self-test', rustgen.process('fu-ccgx-dmc.rs'), sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: plugin_deps, link_with: [ plugin_libs, plugin_builtin_ccgx_dmc, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('ccgx-dmc-self-test', e, env: env) endif fwupd-2.0.10/plugins/ccgx-dmc/tests/000077500000000000000000000000001501337203100172225ustar00rootroot00000000000000fwupd-2.0.10/plugins/ccgx-dmc/tests/ccgx-dmc.builder.xml000066400000000000000000000001701501337203100230540ustar00rootroot00000000000000 0x1000800 aGVsbG8gd29ybGQ= fwupd-2.0.10/plugins/ccgx-dmc/tests/hp-dock-g5.json000066400000000000000000000016021501337203100217520ustar00rootroot00000000000000{ "name": "HP USB-C Dock G5", "interactive": false, "steps": [ { "url": "eb866447bb755c00e748cce14918dcbfaec0ec123237daefce5876c769c2bf92-HP-USBC_DOCK_G5-V1.0.11.0.cab", "emulation-url": "1cd36afb2da2ec2c43c346076d494180ee8f8f236671985cccd3b82601349bc7-HP-USBC_DOCK_G5-V1.0.11.0.zip", "components": [ { "version": "1.0.11.0", "guids": [ "9434f89a-3351-536d-a281-f70203326833" ] } ] }, { "url": "c15a0df7386812781d1f376fe54729e64f69b2a8a6c4b580914d4f6740e4fcc3-HP-USBC_DOCK_G5-V1.0.13.0.cab", "emulation-url": "5d35b4edc89fd01b7e6f7e7101cbf076fe4f712bdd817540351573c9fcafe01a-HP-USBC_DOCK_G5-V1.0.13.0.zip", "components": [ { "version": "1.0.13.0", "guids": [ "9434f89a-3351-536d-a281-f70203326833" ] } ] } ] } fwupd-2.0.10/plugins/ccgx/000077500000000000000000000000001501337203100153175ustar00rootroot00000000000000fwupd-2.0.10/plugins/ccgx/README.md000066400000000000000000000066061501337203100166060ustar00rootroot00000000000000--- title: Plugin: CCGX --- ## Introduction This plugin can flash firmware on Infineon (previously Cypress) CCGx USB-C controller family of devices used in docks. ## Supported Protocols This plugin supports the following protocol IDs: * `com.cypress.ccgx` (deprecated) * `com.infineon.ccgx` ## Device Flash There are four kinds of flash layout. Single image firmware is not currently supported in this plugin. ### Symmetric Firmware In symmetric firmware topology, FW1 and FW2 are both primary (main) firmware with identical sizes and functionality. We can only update FW1 from FW2 or FW2 from FW1. This does mean we need to update just one time as booting from either firmware slot gives a fully functional device. After updating the "other" firmware we can just use `CY_PD_DEVICE_RESET_CMD_SIG` to reboot into the new firmware, and no further action is required. ### Asymmetric Firmware In asymmetric firmware topology, FW1 is backup and FW2 is primary (main) firmware with different firmware sizes. The backup firmware may not support all dock functionality. To update primary, we thus need to update twice: Case 1: FW2 is running * Update FW1 -> Jump to backup FW `CY_PD_JUMP_TO_ALT_FW_CMD_SIG` -> reboot * Update FW2 -> Reset device `CY_PD_DEVICE_RESET_CMD_SIG` -> reboot -> FW2 Case 2: FW1 is running (recovery case) * Update FW2 -> Reset device `CY_PD_DEVICE_RESET_CMD_SIG` -> reboot -> FW2 The `CY_PD_JUMP_TO_ALT_FW_CMD_SIG` command is allowed only in asymmetric FW, but `CY_PD_DEVICE_RESET_CMD_SIG` is allowed in both asymmetric FW and symmetric FW. ## Firmware Format ### Cyacd firmware format The daemon will decompress the cabinet archive and extract several firmware blobs in cyacd file format. See for more details. ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_1234&PID_5678` Devices also have additional instance IDs which corresponds to the silicon ID, application ID and device mode, e.g. * `USB\VID_1234&PID_5678&SID_9ABC` * `USB\VID_1234&PID_5678&SID_9ABC&APP_DEF1` * `USB\VID_1234&PID_5678&SID_9ABC&APP_DEF1&MODE_FW2` ## Update Behavior The device usually presents in runtime HID mode, but on detach re-enumerates with with the HPI interface. On attach the device again re-enumerates back to the runtime HID mode. For this reason the `REPLUG_MATCH_GUID` internal device flag is used so that the HPI and runtime modes are treated as the same device. ## Vendor ID Security The vendor ID is set from the USB vendor, for example set to `USB:0x04B4` ## Quirk Use This plugin uses the following plugin-specific quirks: ### CcgxFlashRowSize Set the size of the flash row in bytes, as a 32 bit integer. Since: 1.4.0 ### CcgxFlashSize Set the maximum flash size, as a 32 bit integer. Since: 1.4.0 ### CcgxImageKind Set the image kind from one of: * `unknown` * `single` * `dual-symmetric` * `dual-asymmetric` * `dual-asymmetric-variable` Since: 1.4.0 ### Flags=device-is-in-restart Device is in restart and should not be closed manually. Since: 1.7.0 ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.4.0`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Brent Wu: @IfxBrent fwupd-2.0.10/plugins/ccgx/ccgx-ids.quirk000066400000000000000000000132651501337203100201040ustar00rootroot00000000000000# CCG2 - CYPD2103-20FNXI [CCGX\SID_1400] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x8000 # CCG2 - CYPD2104-20FNXI [CCGX\SID_1401] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x8000 # CCG2 - CYPD2105-20FNXI [CCGX\SID_1402] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x8000 # CCG2 - CYPD2103-14LHXI [CCGX\SID_1403] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x8000 # CCG2 - CYPD2122-24LQXI [CCGX\SID_1404] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x8000 # CCG2 - CYPD2134-24LQXI [CCGX\SID_1405] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x8000 # CCG2 - CYPD2122-20FNXI [CCGX\SID_1406] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x8000 # CCG2 - CYPD2123-24LQXI [CCGX\SID_1407] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x8000 # CCG2 - CYPD2124-24LQXI [CCGX\SID_1408] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x8000 # CCG2 - CYPD2119-24LQXI [CCGX\SID_1409] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x8000 # CCG2 - CYPD2121-24LQXI [CCGX\SID_1410] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x8000 # CCG2 - CYPD2125-24LQXI [CCGX\SID_1411] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x8000 # CCG2 - CYPD2120-24LQXI [CCGX\SID_1412] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x8000 # CCG3 - CYPD3120-40LQXI [CCGX\SID_1D00] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x20000 # CCG3 - CYPD3105-42FNXI [CCGX\SID_1D01] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x20000 # CCG3 - CYPD3121-40LQXI [CCGX\SID_1D02] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x20000 # CCG3 - CYPD3122-40LQXI [CCGX\SID_1D03] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x20000 # CCG3 - CYPD3125-40LQXI [CCGX\SID_1D04] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x20000 # CCG3 - CYPD3135-40LQXI [CCGX\SID_1D05] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x20000 # CCG3 - CYPD3135-16SXQ' [CCGX\SID_1D06] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x20000 # CCG3 - CYPD3126-42FNXI [CCGX\SID_1D07] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x20000 # CCG3 - CYPD3123-40LQXI [CCGX\SID_1D09] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x20000 # CCG4 - CYPD4225-40LQXI [CCGX\SID_1800] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # CCG4 - CYPD4125-40LQXI [CCGX\SID_1801] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # CCG4 - CYPD4235-40LQXI [CCGX\SID_1802] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # CCG4 - CYPD4135-40LQXI [CCGX\SID_1803] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # CCG4 - CYPD4225A0-33FNXIT [CCGX\SID_1810] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # CCG4 - CYPD4226-40LQXI [CCGX\SID_1F00] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # CCG4 - CYPD4126-40LQXI [CCGX\SID_1F01] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # CCG4 - CYPD4126-24LQXI [CCGX\SID_1F04] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # CCG4 - CYPD4236-40LQXI [CCGX\SID_1F02] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # CCG4 - CYPD4136-40LQXI [CCGX\SID_1F03] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # CCG4 - CYPD4136-24LQXI [CCGX\SID_1F05] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # CCG3PA - CYPD3174-24LQXQ [CCGX\SID_2000] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x10000 # CCG3PA - CYPD3174-16SXQ [CCGX\SID_2001] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x10000 # CCG3PA - CYPD3175-24LQXQ [CCGX\SID_2002] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x10000 # CCG3PA - CYPD3171-24LQXQ [CCGX\SID_2003] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x10000 # CCG3PA - CYPD3195-24LDXS [CCGX\SID_2005] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x10000 # CCG3PA - CYPD3196-24LDXS [CCGX\SID_2006] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x10000 # CCG3PA - CYPD3197-24LDXS [CCGX\SID_2007] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x10000 # CCG3PA2 - CYPDC1185-32LQXQ [CCGX\SID_2400] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x20000 # CCG3PA2 - CYPDC1186-30FNXI [CCGX\SID_2401] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x20000 # CCG3PA2 - CYPDC1186B2-30FNXI [CCGX\SID_2402] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x20000 # CCG5 - CYPD5225-96BZXI [CCGX\SID_2100] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # CCG5 - CYPD5125-40LQXI [CCGX\SID_2101] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # CCG5 - CYPD5235-96BZXI [CCGX\SID_2102] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # CCG5 - CYPD5236-96BZXI [CCGX\SID_2103] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # CCG5 - CYPD5237-96BZXI [CCGX\SID_2104] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # CCG5 - CYPD5227-96BZXI [CCGX\SID_2105] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # CCG5 - CYPD5135-40LQXI [CCGX\SID_2106] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # CCG6 - CYPD6125-40LQXI [CCGX\SID_2A00] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # CCG6 - CYPD6126-96BZXI [CCGX\SID_2A10] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # CCG6 - CYPD5126-40LQXI [CCGX\SID_2A01] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # CCG6 - CYPD5137-40LQXI [CCGX\SID_2A02] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # CCG6 - CYPD6137-40LQXI [CCGX\SID_2A03] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # PAG1S - CYPAS111-24LQXQ [CCGX\SID_2B01] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x10000 # PAG1S - CYPD3184-24LQXQ [CCGX\SID_2B00] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x10000 # HX3PD - CYUSB4347-BZXC_PD [CCGX\SID_1F82] CcgxFlashRowSize = 0x100 CcgxFlashSize = 0x20000 # ACG1F - CYAC1126-24LQXI [CCGX\SID_2F00] CcgxFlashRowSize = 0x40 CcgxFlashSize = 0x4000 # ACG1F - CYAC1126-40LQXI [CCGX\SID_2F01] CcgxFlashRowSize = 0x40 CcgxFlashSize = 0x4000 # CCG6DF - CYPD6227-96BZXI [CCGX\SID_3000] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x10000 # CCG6DF - CYPD6127-96BZXI [CCGX\SID_3001] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x10000 # CCG6SF - CYPD6128-96BZXI [CCGX\SID_3300] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x10000 # CCG6SF - CYPD6127-48LQXI [CCGX\SID_3301] CcgxFlashRowSize = 0x80 CcgxFlashSize = 0x10000 fwupd-2.0.10/plugins/ccgx/ccgx.quirk000066400000000000000000000030551501337203100173230ustar00rootroot00000000000000# Lenovo ThinkPad USB-C Dock Gen2 [USB\VID_17EF&PID_A38F] Plugin = ccgx GType = FuCcgxHidDevice ParentGuid = USB\VID_17EF&PID_A391 [USB\VID_04B4&PID_521A] Plugin = ccgx GType = FuCcgxHpiDevice [USB\VID_04B4&PID_521A&SID_1F00&APP_6D64] CcgxImageKind = dual-asymmetric Name = ThinkPad USB-C Dock Gen2 PD Controller ParentGuid = USB\VID_17EF&PID_A391 InstallDuration = 120 RemoveDelay = 60000 [USB\VID_04B4&PID_521A&SID_1F00&APP_6D64&MODE_FW1] Summary = CCGx Power Delivery Device (Bootloader) Flags = is-bootloader [USB\VID_04B4&PID_521A&SID_1F00&APP_6D64&MODE_FW2] Summary = CCGx Power Delivery Device CounterpartGuid = USB\VID_04B4&PID_521A&SID_1F00&APP_6D64&MODE_FW1 # Lenovo ThinkPad USB-C Dock Hybrid [USB\VID_17EF&PID_A354] Plugin = ccgx GType = FuCcgxHidDevice ParentGuid = USB\VID_17EF&PID_1028 [USB\VID_17EF&PID_A35F] Plugin = ccgx GType = FuCcgxHidDevice ParentGuid = USB\VID_17EF&PID_1028 [USB\VID_04B4&PID_5218] Plugin = ccgx GType = FuCcgxHpiDevice [USB\VID_04B4&PID_5218&SID_1F00&APP_6432] CcgxImageKind = dual-symmetric Name = ThinkPad USB-C Dock Hybrid PD Controller ParentGuid = USB\VID_17EF&PID_1028 InstallDuration = 120 RemoveDelay = 60000 [USB\VID_04B4&PID_5218&SID_1F00&APP_6432&MODE_FW1] Summary = CCGx Power Delivery Device (Symmetric FW1) [USB\VID_04B4&PID_5218&SID_1F00&APP_6432&MODE_FW2] Summary = CCGx Power Delivery Device (Symmetric FW2) # Framework HDMI Expansion Card [USB\VID_32AC&PID_0002] Plugin = ccgx GType = FuCcgxPureHidDevice # Framework DisplayPort Expansion Card [USB\VID_32AC&PID_0003] Plugin = ccgx GType = FuCcgxPureHidDevice fwupd-2.0.10/plugins/ccgx/fu-ccgx-common.c000066400000000000000000000012571501337203100203120ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-ccgx-common.h" gchar * fu_ccgx_version_to_string(guint32 val) { /* 16 bits: application type [LSB] * 8 bits: build number * 4 bits: minor version * 4 bits: major version [MSB] */ return g_strdup_printf("%u.%u.%u", (val >> 28) & 0x0f, (val >> 24) & 0x0f, (val >> 16) & 0xff); } FuCcgxFwMode fu_ccgx_fw_mode_get_alternate(FuCcgxFwMode val) { if (val == FU_CCGX_FW_MODE_FW1) return FU_CCGX_FW_MODE_FW2; if (val == FU_CCGX_FW_MODE_FW2) return FU_CCGX_FW_MODE_FW1; return FU_CCGX_FW_MODE_BOOT; } fwupd-2.0.10/plugins/ccgx/fu-ccgx-common.h000066400000000000000000000004411501337203100203110ustar00rootroot00000000000000/* * Copyright 2020 Cypress Semiconductor Corporation. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-ccgx-struct.h" gchar * fu_ccgx_version_to_string(guint32 val); FuCcgxFwMode fu_ccgx_fw_mode_get_alternate(FuCcgxFwMode val); fwupd-2.0.10/plugins/ccgx/fu-ccgx-firmware.c000066400000000000000000000351621501337203100206400ustar00rootroot00000000000000/* * Copyright 2020 Cypress Semiconductor Corporation. * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-ccgx-common.h" #include "fu-ccgx-firmware.h" struct _FuCcgxFirmware { FuFirmwareClass parent_instance; GPtrArray *records; guint16 app_type; guint16 silicon_id; FuCcgxFwMode fw_mode; }; G_DEFINE_TYPE(FuCcgxFirmware, fu_ccgx_firmware, FU_TYPE_FIRMWARE) /* offset stored application version for CCGx */ #define CCGX_APP_VERSION_OFFSET 228 /* 128+64+32+4 */ #define FU_CCGX_FIRMWARE_TOKENS_MAX 100000 /* lines */ GPtrArray * fu_ccgx_firmware_get_records(FuCcgxFirmware *self) { g_return_val_if_fail(FU_IS_CCGX_FIRMWARE(self), NULL); return self->records; } guint16 fu_ccgx_firmware_get_app_type(FuCcgxFirmware *self) { g_return_val_if_fail(FU_IS_CCGX_FIRMWARE(self), 0); return self->app_type; } guint16 fu_ccgx_firmware_get_silicon_id(FuCcgxFirmware *self) { g_return_val_if_fail(FU_IS_CCGX_FIRMWARE(self), 0); return self->silicon_id; } FuCcgxFwMode fu_ccgx_firmware_get_fw_mode(FuCcgxFirmware *self) { g_return_val_if_fail(FU_IS_CCGX_FIRMWARE(self), 0); return self->fw_mode; } static void fu_ccgx_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuCcgxFirmware *self = FU_CCGX_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "silicon_id", self->silicon_id); if (flags & FU_FIRMWARE_EXPORT_FLAG_INCLUDE_DEBUG) { fu_xmlb_builder_insert_kx(bn, "app_type", self->app_type); fu_xmlb_builder_insert_kx(bn, "records", self->records->len); fu_xmlb_builder_insert_kv(bn, "fw_mode", fu_ccgx_fw_mode_to_string(self->fw_mode)); } } static void fu_ccgx_firmware_record_free(FuCcgxFirmwareRecord *rcd) { if (rcd->data != NULL) g_bytes_unref(rcd->data); g_free(rcd); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuCcgxFirmwareRecord, fu_ccgx_firmware_record_free) static gboolean fu_ccgx_firmware_add_record(FuCcgxFirmware *self, GString *token, FuFirmwareParseFlags flags, GError **error) { guint16 buflen; guint8 checksum_calc = 0; g_autoptr(FuCcgxFirmwareRecord) rcd = NULL; g_autoptr(GByteArray) data = g_byte_array_new(); /* this is not in the specification, but exists in reality */ if (token->str[0] == ':') g_string_erase(token, 0, 1); /* parse according to https://community.cypress.com/docs/DOC-10562 */ rcd = g_new0(FuCcgxFirmwareRecord, 1); if (!fu_firmware_strparse_uint8_safe(token->str, token->len, 0, &rcd->array_id, error)) return FALSE; if (!fu_firmware_strparse_uint16_safe(token->str, token->len, 2, &rcd->row_number, error)) return FALSE; if (!fu_firmware_strparse_uint16_safe(token->str, token->len, 6, &buflen, error)) return FALSE; if (token->len != ((gsize)buflen * 2) + 12) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid record, expected %u chars, got %u", (guint)(buflen * 2) + 12, (guint)token->len); return FALSE; } /* parse payload, adding checksum */ for (guint i = 0; i < buflen; i++) { guint8 tmp = 0; if (!fu_firmware_strparse_uint8_safe(token->str, token->len, 10 + (i * 2), &tmp, error)) return FALSE; fu_byte_array_append_uint8(data, tmp); checksum_calc += tmp; } rcd->data = g_bytes_new(data->data, data->len); /* verify 2s complement checksum */ if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { guint8 checksum_file; if (!fu_firmware_strparse_uint8_safe(token->str, token->len, (buflen * 2) + 10, &checksum_file, error)) return FALSE; for (guint i = 0; i < 5; i++) { guint8 tmp = 0; if (!fu_firmware_strparse_uint8_safe(token->str, token->len, i * 2, &tmp, error)) return FALSE; checksum_calc += tmp; } checksum_calc = 1 + ~checksum_calc; if (checksum_file != checksum_calc) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "checksum invalid, got %02x, expected %02x", checksum_calc, checksum_file); return FALSE; } } /* success */ g_ptr_array_add(self->records, g_steal_pointer(&rcd)); return TRUE; } static gboolean fu_ccgx_firmware_parse_md_block(FuCcgxFirmware *self, FuFirmwareParseFlags flags, GError **error) { FuCcgxFirmwareRecord *rcd; gsize bufsz = 0; gsize md_offset = 0; guint32 fw_size = 0; guint32 rcd_version_idx = 0; guint32 version = 0; guint8 checksum_calc = 0; g_autoptr(GByteArray) st_metadata = NULL; /* sanity check */ if (self->records->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no records added to image"); return FALSE; } /* read metadata from correct offset */ rcd = g_ptr_array_index(self->records, self->records->len - 1); bufsz = g_bytes_get_size(rcd->data); if (bufsz == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid buffer size"); return FALSE; } switch (bufsz) { case 0x80: md_offset = 0x40; break; case 0x100: md_offset = 0xC0; break; default: break; } /* parse */ st_metadata = fu_struct_ccgx_metadata_hdr_parse_bytes(rcd->data, md_offset, error); if (st_metadata == NULL) return FALSE; if (fu_struct_ccgx_metadata_hdr_get_metadata_valid(st_metadata) != FU_STRUCT_CCGX_METADATA_HDR_DEFAULT_METADATA_VALID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid metadata 0x@%x, expected 0x%04x, got 0x%04x", (guint)md_offset, (guint)FU_STRUCT_CCGX_METADATA_HDR_DEFAULT_METADATA_VALID, (guint)fu_struct_ccgx_metadata_hdr_get_metadata_valid(st_metadata)); return FALSE; } for (guint i = 0; i < self->records->len - 1; i++) { rcd = g_ptr_array_index(self->records, i); checksum_calc += fu_sum8_bytes(rcd->data); fw_size += g_bytes_get_size(rcd->data); } if (fw_size != fu_struct_ccgx_metadata_hdr_get_fw_size(st_metadata)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware size invalid, got %02x, expected %02x", fw_size, fu_struct_ccgx_metadata_hdr_get_fw_size(st_metadata)); return FALSE; } checksum_calc = 1 + ~checksum_calc; if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { if (fu_struct_ccgx_metadata_hdr_get_fw_checksum(st_metadata) != checksum_calc) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "checksum invalid, got %02x, expected %02x", checksum_calc, fu_struct_ccgx_metadata_hdr_get_fw_checksum(st_metadata)); return FALSE; } } /* get version if enough data */ rcd_version_idx = CCGX_APP_VERSION_OFFSET / bufsz; if (rcd_version_idx < self->records->len) { const guint8 *buf; rcd = g_ptr_array_index(self->records, rcd_version_idx); buf = g_bytes_get_data(rcd->data, &bufsz); if (bufsz == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "metadata record had zero size"); return FALSE; } if (!fu_memread_uint32_safe(buf, bufsz, CCGX_APP_VERSION_OFFSET % bufsz, &version, G_LITTLE_ENDIAN, error)) return FALSE; self->app_type = version & 0xffff; fu_firmware_set_version_raw(FU_FIRMWARE(self), version); } /* work out the FuCcgxFwMode */ if (self->records->len > 0) { rcd = g_ptr_array_index(self->records, self->records->len - 1); if ((rcd->row_number & 0xFF) == 0xFF) /* last row */ self->fw_mode = FU_CCGX_FW_MODE_FW1; if ((rcd->row_number & 0xFF) == 0xFE) /* penultimate row */ self->fw_mode = FU_CCGX_FW_MODE_FW2; } return TRUE; } typedef struct { FuCcgxFirmware *self; FuFirmwareParseFlags flags; } FuCcgxFirmwareTokenHelper; static gboolean fu_ccgx_firmware_tokenize_cb(GString *token, guint token_idx, gpointer user_data, GError **error) { FuCcgxFirmwareTokenHelper *helper = (FuCcgxFirmwareTokenHelper *)user_data; FuCcgxFirmware *self = FU_CCGX_FIRMWARE(helper->self); /* sanity check */ if (token_idx > FU_CCGX_FIRMWARE_TOKENS_MAX) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "file has too many lines"); return FALSE; } /* remove WIN32 line endings */ g_strdelimit(token->str, "\r\x1a", '\0'); token->len = strlen(token->str); /* header */ if (token_idx == 0) { guint32 device_id = 0; if (token->len != 12) { g_autofree gchar *strsafe = fu_strsafe(token->str, 12); if (strsafe != NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid header, expected == 12 chars -- got %s", strsafe); return FALSE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid header, expected == 12 chars"); return FALSE; } if (!fu_firmware_strparse_uint32_safe(token->str, token->len, 0, &device_id, error)) return FALSE; self->silicon_id = device_id >> 16; return TRUE; } /* ignore blank lines */ if (token->len == 0) return TRUE; /* parse record */ if (!fu_ccgx_firmware_add_record(self, token, helper->flags, error)) { g_prefix_error(error, "error on line %u: ", token_idx + 1); return FALSE; } /* success */ return TRUE; } static gboolean fu_ccgx_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuCcgxFirmware *self = FU_CCGX_FIRMWARE(firmware); FuCcgxFirmwareTokenHelper helper = {.self = self, .flags = flags}; /* tokenize */ if (!fu_strsplit_stream(stream, 0x0, "\n", fu_ccgx_firmware_tokenize_cb, &helper, error)) return FALSE; /* address is first data entry */ if (self->records->len > 0) { FuCcgxFirmwareRecord *rcd = g_ptr_array_index(self->records, 0); fu_firmware_set_addr(firmware, rcd->row_number); } /* parse metadata block */ if (!fu_ccgx_firmware_parse_md_block(self, flags, error)) { g_prefix_error(error, "failed to parse metadata: "); return FALSE; } /* success */ return TRUE; } static void fu_ccgx_firmware_write_record(GString *str, guint8 array_id, guint8 row_number, const guint8 *buf, guint16 bufsz) { guint8 checksum_calc = 0xff; g_autoptr(GString) datastr = g_string_new(NULL); /* offset for bootloader perhaps? */ row_number += 0xE; checksum_calc += array_id; checksum_calc += row_number; checksum_calc += bufsz & 0xff; checksum_calc += (bufsz >> 8) & 0xff; for (guint j = 0; j < bufsz; j++) { g_string_append_printf(datastr, "%02X", buf[j]); checksum_calc += buf[j]; } g_string_append_printf(str, ":%02X%04X%04X%s%02X\n", array_id, row_number, bufsz, datastr->str, (guint)((guint8)~checksum_calc)); } static GByteArray * fu_ccgx_firmware_write(FuFirmware *firmware, GError **error) { FuCcgxFirmware *self = FU_CCGX_FIRMWARE(firmware); gsize fwbufsz = 0; guint8 checksum_img = 0xff; const guint8 *fwbuf; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GByteArray) mdbuf = g_byte_array_new(); g_autoptr(GByteArray) st_metadata = fu_struct_ccgx_metadata_hdr_new(); g_autoptr(GBytes) fw = NULL; g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(GString) str = g_string_new(NULL); /* header record */ g_string_append_printf(str, "%04X%04X%02X%02X\n", self->silicon_id, (guint)0x11AF, /* SiliconID */ (guint)0x0, /* SiliconRev */ (guint)0x0); /* Checksum, or 0x0 */ /* add image in chunks */ fw = fu_firmware_get_bytes_with_patches(firmware, error); if (fw == NULL) return NULL; chunks = fu_chunk_array_new_from_bytes(fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 0x100); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return NULL; fu_ccgx_firmware_write_record(str, 0x0, i, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); } /* add metadata */ fwbuf = g_bytes_get_data(fw, &fwbufsz); for (guint j = 0; j < fwbufsz; j++) checksum_img += fwbuf[j]; /* copy into place */ fu_byte_array_set_size(mdbuf, 0x80, 0x00); fu_struct_ccgx_metadata_hdr_set_fw_checksum(st_metadata, ~checksum_img); fu_struct_ccgx_metadata_hdr_set_fw_entry(st_metadata, 0x0); /* unknown */ fu_struct_ccgx_metadata_hdr_set_last_boot_row(st_metadata, 0x13); fu_struct_ccgx_metadata_hdr_set_fw_size(st_metadata, fwbufsz); fu_struct_ccgx_metadata_hdr_set_boot_seq(st_metadata, 0x0); /* unknown */ if (!fu_memcpy_safe(mdbuf->data, mdbuf->len, 0x40, /* dst */ st_metadata->data, st_metadata->len, 0x0, /* src */ st_metadata->len, error)) return NULL; fu_ccgx_firmware_write_record(str, 0x0, 0xFE, /* FW2: penultimate row */ mdbuf->data, mdbuf->len); /* success */ g_byte_array_append(buf, (const guint8 *)str->str, str->len); return g_steal_pointer(&buf); } static gboolean fu_ccgx_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { FuCcgxFirmware *self = FU_CCGX_FIRMWARE(firmware); guint64 tmp; /* optional properties */ tmp = xb_node_query_text_as_uint(n, "silicon_id", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT16) self->silicon_id = tmp; /* success */ return TRUE; } static gchar * fu_ccgx_firmware_convert_version(FuFirmware *firmware, guint64 version_raw) { return fu_ccgx_version_to_string(version_raw); } static void fu_ccgx_firmware_init(FuCcgxFirmware *self) { self->records = g_ptr_array_new_with_free_func((GFreeFunc)fu_ccgx_firmware_record_free); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_VID_PID); fu_firmware_set_version_format(FU_FIRMWARE(self), FWUPD_VERSION_FORMAT_TRIPLET); } static void fu_ccgx_firmware_finalize(GObject *object) { FuCcgxFirmware *self = FU_CCGX_FIRMWARE(object); g_ptr_array_unref(self->records); G_OBJECT_CLASS(fu_ccgx_firmware_parent_class)->finalize(object); } static void fu_ccgx_firmware_class_init(FuCcgxFirmwareClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->convert_version = fu_ccgx_firmware_convert_version; object_class->finalize = fu_ccgx_firmware_finalize; firmware_class->parse = fu_ccgx_firmware_parse; firmware_class->write = fu_ccgx_firmware_write; firmware_class->build = fu_ccgx_firmware_build; firmware_class->export = fu_ccgx_firmware_export; } FuFirmware * fu_ccgx_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_CCGX_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/ccgx/fu-ccgx-firmware.h000066400000000000000000000014031501337203100206340ustar00rootroot00000000000000/* * Copyright 2020 Cypress Semiconductor Corporation. * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-ccgx-common.h" #define FU_TYPE_CCGX_FIRMWARE (fu_ccgx_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuCcgxFirmware, fu_ccgx_firmware, FU, CCGX_FIRMWARE, FuFirmware) typedef struct { guint8 array_id; guint16 row_number; GBytes *data; } FuCcgxFirmwareRecord; FuFirmware * fu_ccgx_firmware_new(void); GPtrArray * fu_ccgx_firmware_get_records(FuCcgxFirmware *self); guint16 fu_ccgx_firmware_get_app_type(FuCcgxFirmware *self); guint16 fu_ccgx_firmware_get_silicon_id(FuCcgxFirmware *self); FuCcgxFwMode fu_ccgx_firmware_get_fw_mode(FuCcgxFirmware *self); fwupd-2.0.10/plugins/ccgx/fu-ccgx-hid-device.c000066400000000000000000000067031501337203100210240ustar00rootroot00000000000000/* * Copyright 2020 Cypress Semiconductor Corporation. * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-ccgx-hid-device.h" struct _FuCcgxHidDevice { FuHidDevice parent_instance; }; G_DEFINE_TYPE(FuCcgxHidDevice, fu_ccgx_hid_device, FU_TYPE_HID_DEVICE) #define FU_CCGX_HID_DEVICE_TIMEOUT 5000 /* ms */ #define FU_CCGX_HID_DEVICE_RETRY_DELAY 30 /* ms */ #define FU_CCGX_HID_DEVICE_RETRY_CNT 5 static gboolean fu_ccgx_hid_device_enable_hpi_mode_cb(FuDevice *device, gpointer user_data, GError **error) { guint8 buf[5] = {0xEE, 0xBC, 0xA6, 0xB9, 0xA8}; if (!fu_hid_device_set_report(FU_HID_DEVICE(device), buf[0], buf, sizeof(buf), FU_CCGX_HID_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "switch to HPI mode error: "); return FALSE; } return TRUE; } static gboolean fu_ccgx_hid_device_detach(FuDevice *device, FuProgress *progress, GError **error) { if (!fu_device_retry(device, fu_ccgx_hid_device_enable_hpi_mode_cb, FU_CCGX_HID_DEVICE_RETRY_CNT, NULL, error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_ccgx_hid_device_setup(FuDevice *device, GError **error) { /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_ccgx_hid_device_parent_class)->setup(device, error)) return FALSE; /* This seems insane... but we need to switch the device from HID * mode to HPI mode at startup. The device continues to function * exactly as before and no user-visible effects are noted */ if (!fu_device_retry(device, fu_ccgx_hid_device_enable_hpi_mode_cb, FU_CCGX_HID_DEVICE_RETRY_CNT, NULL, error)) return FALSE; /* never add this device, the daemon does not expect the device to * disconnect before it is added */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device is replugging into HPI mode"); return FALSE; } static void fu_ccgx_hid_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 98, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static void fu_ccgx_hid_device_init(FuCcgxHidDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "com.cypress.ccgx"); fu_device_add_protocol(FU_DEVICE(self), "com.infineon.ccgx"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_WILL_DISAPPEAR); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_retry_set_delay(FU_DEVICE(self), FU_CCGX_HID_DEVICE_RETRY_DELAY); } static void fu_ccgx_hid_device_class_init(FuCcgxHidDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->detach = fu_ccgx_hid_device_detach; device_class->setup = fu_ccgx_hid_device_setup; device_class->set_progress = fu_ccgx_hid_device_set_progress; } fwupd-2.0.10/plugins/ccgx/fu-ccgx-hid-device.h000066400000000000000000000005511501337203100210240ustar00rootroot00000000000000/* * Copyright 2020 Cypress Semiconductor Corporation. * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_CCGX_HID_DEVICE (fu_ccgx_hid_device_get_type()) G_DECLARE_FINAL_TYPE(FuCcgxHidDevice, fu_ccgx_hid_device, FU, CCGX_HID_DEVICE, FuHidDevice) fwupd-2.0.10/plugins/ccgx/fu-ccgx-hpi-common.h000066400000000000000000000304311501337203100210710ustar00rootroot00000000000000/* * Copyright 2020 Cypress Semiconductor Corporation. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define I2C_READ_WRITE_DELAY_MS 10 /* ms */ #define CY_SCB_INDEX_POS 15 #define CY_I2C_WRITE_COMMAND_POS 3 #define CY_I2C_WRITE_COMMAND_LEN_POS 4 #define CY_I2C_GET_STATUS_LEN 3 #define CY_I2C_MODE_WRITE 1 #define CY_I2C_MODE_READ 0 #define CY_I2C_ERROR_BIT 1 #define CY_I2C_ARBITRATION_ERROR_BIT (1 << 1) #define CY_I2C_NAK_ERROR_BIT (1 << 2) #define CY_I2C_BUS_ERROR_BIT (1 << 3) #define CY_I2C_STOP_BIT_ERROR (1 << 4) #define CY_I2C_BUS_BUSY_ERROR (1 << 5) #define CY_I2C_ENABLE_PRECISE_TIMING 1 #define CY_I2C_EVENT_NOTIFICATION_LEN 3 #define PD_I2C_TARGET_ADDRESS 0x08 /* timeout (ms) for USB I2C communication */ #define FU_CCGX_HPI_WAIT_TIMEOUT 5000 /* max i2c frequency */ #define FU_CCGX_HPI_FREQ 400000 typedef enum { CY_GET_VERSION_CMD = 0xB0, /* get the version of the boot-loader * value = 0, index = 0, length = 4; * data_in = 32 bit version */ CY_GET_SIGNATURE_CMD = 0xBD, /* get the signature of the firmware * It is suppose to be 'CYUS' for normal firmware * and 'CYBL' for Bootloader */ CY_UART_GET_CONFIG_CMD = 0xC0, /* retrieve the 16 byte UART configuration information * MS bit of value indicates the SCB index * length = 16, data_in = 16 byte configuration */ CY_UART_SET_CONFIG_CMD, /* update the 16 byte UART configuration information * MS bit of value indicates the SCB index. * length = 16, data_out = 16 byte configuration information */ CY_SPI_GET_CONFIG_CMD, /* retrieve the 16 byte SPI configuration information * MS bit of value indicates the SCB index * length = 16, data_in = 16 byte configuration */ CY_SPI_SET_CONFIG_CMD, /* update the 16 byte SPI configuration information * MS bit of value indicates the SCB index * length = 16, data_out = 16 byte configuration information */ CY_I2C_GET_CONFIG_CMD, /* retrieve the 16 byte I2C configuration information * MS bit of value indicates the SCB index * length = 16, data_in = 16 byte configuration */ CY_I2C_SET_CONFIG_CMD = 0xC5, /* update the 16 byte I2C configuration information * MS bit of value indicates the SCB index * length = 16, data_out = 16 byte configuration information */ CY_I2C_WRITE_CMD, /* perform I2C write operation * value = bit0 - start, bit1 - stop, bit3 - start on idle, * bits[14:8] - target address, bit15 - scbIndex. length = 0 the * data is provided over the bulk endpoints */ CY_I2C_READ_CMD, /* perform I2C read operation. * value = bit0 - start, bit1 - stop, bit2 - Nak last byte, * bit3 - start on idle, bits[14:8] - target address, bit15 - scbIndex, * length = 0. The data is provided over the bulk endpoints */ CY_I2C_GET_STATUS_CMD, /* retrieve the I2C bus status. * value = bit0 - 0: TX 1: RX, bit15 - scbIndex, length = 3, * data_in = byte0: bit0 - flag, bit1 - bus_state, bit2 - SDA state, * bit3 - TX underflow, bit4 - arbitration error, bit5 - NAK * bit6 - bus error, * byte[2:1] Data count remaining */ CY_I2C_RESET_CMD, /* the command cleans up the I2C state machine and frees the bus * value = bit0 - 0: TX path, 1: RX path; bit15 - scbIndex, * length = 0 */ CY_SPI_READ_WRITE_CMD = 0xCA, /* the command starts a read / write operation at SPI * value = bit 0 - RX enable, bit 1 - TX enable, bit 15 - * scbIndex; index = length of transfer */ CY_SPI_RESET_CMD, /* the command resets the SPI pipes and allows it to receive new * request * value = bit 15 - scbIndex */ CY_SPI_GET_STATUS_CMD, /* the command returns the current transfer status * the count will match the TX pipe status at SPI end * for completion of read, read all data * at the USB end signifies the end of transfer * value = bit 15 - scbIndex */ CY_JTAG_ENABLE_CMD = 0xD0, /* enable JTAG module */ CY_JTAG_DISABLE_CMD, /* disable JTAG module */ CY_JTAG_READ_CMD, /* jtag read vendor command */ CY_JTAG_WRITE_CMD, /* jtag write vendor command */ CY_GPIO_GET_CONFIG_CMD = 0xD8, /* get the GPIO configuration */ CY_GPIO_SET_CONFIG_CMD, /* set the GPIO configuration */ CY_GPIO_GET_VALUE_CMD, /* get GPIO value */ CY_GPIO_SET_VALUE_CMD, /* set the GPIO value */ CY_PROG_USER_FLASH_CMD = 0xE0, /* program user flash area. The total space available is 512 * bytes this can be accessed by the user from USB. The flash * area address offset is from 0x0000 to 0x00200 and can be * written to page wise (128 byte) */ CY_READ_USER_FLASH_CMD, /* read user flash area. The total space available is 512 bytes * this can be accessed by the user from USB. The flash area * address offset is from 0x0000 to 0x00200 and can be written to * page wise (128 byte) */ CY_DEVICE_RESET_CMD = 0xE3, /* performs a device reset from firmware */ } CyVendorCommand; typedef struct __attribute__((packed)) { /* nocheck:blocked */ guint32 frequency; /* frequency of operation. Only valid values are 100KHz and 400KHz */ guint8 target_address; /* target address to be used when in target mode */ guint8 is_msb_first; /* whether to transmit most significant bit first */ guint8 is_initiator; /* whether to block is to be configured as a initiator */ guint8 s_ignore; /* ignore general call in target mode */ guint8 is_clock_stretch; /* whether to stretch clock in case of no FIFO availability */ guint8 is_loop_back; /* whether to loop back TX data to RX. Valid only for debug purposes */ guint8 reserved[6]; } CyI2CConfig; typedef enum { CY_I2C_DATA_CONFIG_NONE = 0, CY_I2C_DATA_CONFIG_STOP = 1 << 0, CY_I2C_DATA_CONFIG_NAK = 1 << 1, /* only for read */ } CyI2CDataConfigBits; typedef enum { HPI_DEV_REG_DEVICE_MODE = 0, HPI_DEV_REG_BOOT_MODE_REASON, HPI_DEV_REG_SI_ID, HPI_DEV_REG_SI_ID_LSB, HPI_DEV_REG_BL_LAST_ROW, HPI_DEV_REG_BL_LAST_ROW_LSB, HPI_DEV_REG_INTR_ADDR, HPI_DEV_REG_JUMP_TO_BOOT, HPI_DEV_REG_RESET_ADDR, HPI_DEV_REG_RESET_CMD, HPI_DEV_REG_ENTER_FLASH_MODE, HPI_DEV_REG_VALIDATE_FW_ADDR, HPI_DEV_REG_FLASH_READ_WRITE, HPI_DEV_REG_FLASH_READ_WRITE_CMD, HPI_DEV_REG_FLASH_ROW, HPI_DEV_REG_FLASH_ROW_LSB, HPI_DEV_REG_ALL_VERSION, HPI_DEV_REG_ALL_VERSION_BYTE_1, HPI_DEV_REG_ALL_VERSION_BYTE_2, HPI_DEV_REG_ALL_VERSION_BYTE_3, HPI_DEV_REG_ALL_VERSION_BYTE_4, HPI_DEV_REG_ALL_VERSION_BYTE_5, HPI_DEV_REG_ALL_VERSION_BYTE_6, HPI_DEV_REG_ALL_VERSION_BYTE_7, HPI_DEV_REG_ALL_VERSION_BYTE_8, HPI_DEV_REG_ALL_VERSION_BYTE_9, HPI_DEV_REG_ALL_VERSION_BYTE_10, HPI_DEV_REG_ALL_VERSION_BYTE_11, HPI_DEV_REG_ALL_VERSION_BYTE_12, HPI_DEV_REG_ALL_VERSION_BYTE_13, HPI_DEV_REG_ALL_VERSION_BYTE_14, HPI_DEV_REG_ALL_VERSION_BYTE_15, HPI_DEV_REG_FW_2_VERSION, HPI_DEV_REG_FW_2_VERSION_BYTE_1, HPI_DEV_REG_FW_2_VERSION_BYTE_2, HPI_DEV_REG_FW_2_VERSION_BYTE_3, HPI_DEV_REG_FW_2_VERSION_BYTE_4, HPI_DEV_REG_FW_2_VERSION_BYTE_5, HPI_DEV_REG_FW_2_VERSION_BYTE_6, HPI_DEV_REG_FW_2_VERSION_BYTE_7, HPI_DEV_REG_FW_BIN_LOC, HPI_DEV_REG_FW_1_BIN_LOC_LSB, HPI_DEV_REG_FW_2_BIN_LOC_MSB, HPI_DEV_REG_FW_2_BIN_LOC_LSB, HPI_DEV_REG_PORT_ENABLE, HPI_DEV_SPACE_REG_LEN, HPI_DEV_REG_RESPONSE = 0x007E, HPI_DEV_REG_FLASH_MEM = 0x0200 } HPIDevReg; typedef enum { HPI_REG_SECTION_DEV = 0, /* device information */ HPI_REG_SECTION_PORT_0, /* USB-PD Port 0 related */ HPI_REG_SECTION_PORT_1, /* USB-PD Port 1 related */ HPI_REG_SECTION_ALL /* select all registers */ } HPIRegSection; typedef struct __attribute__((packed)) { /* nocheck:blocked */ guint16 event_code; guint16 event_length; guint8 event_data[128]; } HPIEvent; typedef enum { HPI_REG_PART_REG = 0, /* register region */ HPI_REG_PART_DATA = 1, /* data memory */ HPI_REG_PART_FLASH = 2, /* flash memory */ HPI_REG_PART_PDDATA_READ = 4, /* read data memory */ HPI_REG_PART_PDDATA_WRITE = 8, /* write data memory */ } HPIRegPart; typedef enum { FU_CCGX_PD_RESP_REG_DEVICE_MODE_ADDR, FU_CCGX_PD_RESP_BOOT_MODE_REASON, FU_CCGX_PD_RESP_SILICON_ID, FU_CCGX_PD_RESP_BL_LAST_ROW = 0x04, FU_CCGX_PD_RESP_REG_INTR_REG_ADDR = 0x06, FU_CCGX_PD_RESP_JUMP_TO_BOOT_REG_ADDR, FU_CCGX_PD_RESP_REG_RESET_ADDR, FU_CCGX_PD_RESP_REG_ENTER_FLASH_MODE_ADDR = 0x0A, FU_CCGX_PD_RESP_REG_VALIDATE_FW_ADDR, FU_CCGX_PD_RESP_REG_FLASH_READ_WRITE_ADDR, FU_CCGX_PD_RESP_GET_VERSION = 0x10, FU_CCGX_PD_RESP_REG_DBG_PD_INIT = 0x12, FU_CCGX_PD_RESP_REG_U_VDM_CTRL_ADDR = 0x20, FU_CCGX_PD_RESP_REG_READ_PD_PROFILE = 0x22, FU_CCGX_PD_RESP_REG_EFFECTIVE_SOURCE_PDO_MASK = 0x24, FU_CCGX_PD_RESP_REG_EFFECTIVE_SINK_PDO_MASK, FU_CCGX_PD_RESP_REG_SELECT_SOURCE_PDO, FU_CCGX_PD_RESP_REG_SELECT_SINK_PDO, FU_CCGX_PD_RESP_REG_PD_CONTROL, FU_CCGX_PD_RESP_REG_PD_STATUS = 0x2C, FU_CCGX_PD_RESP_REG_TYPE_C_STATUS = 0x30, FU_CCGX_PD_RESP_REG_CURRENT_PDO = 0x34, FU_CCGX_PD_RESP_REG_CURRENT_RDO = 0x38, FU_CCGX_PD_RESP_REG_CURRENT_CABLE_VDO = 0x3C, FU_CCGX_PD_RESP_REG_DISPLAY_PORT_STATUS = 0x40, FU_CCGX_PD_RESP_REG_DISPLAY_PORT_CONFIG = 0x44, FU_CCGX_PD_RESP_REG_ALTERNATE_MODE_MUX_SELECTION = 0X45, FU_CCGX_PD_RESP_REG_EVENT_MASK = 0x48, FU_CCGX_PD_RESP_REG_RESPONSE_ADDR = 0x7E, FU_CCGX_PD_RESP_REG_BOOTDATA_MEMORY_ADDR = 0x80, FU_CCGX_PD_RESP_REG_FWDATA_MEMORY_ADDR = 0xC0, } CyPDReg; #define FU_CCGX_PD_RESP_BRIDGE_MODE_CMD_SIG 0x42 #define FU_CCGX_PD_RESP_GET_SILICON_ID_CMD_SIG 0x53 #define FU_CCGX_PD_RESP_REG_INTR_REG_CLEAR_RQT 0x01 #define FU_CCGX_PD_RESP_JUMP_TO_BOOT_CMD_SIG 0x4A #define FU_CCGX_PD_RESP_JUMP_TO_ALT_FW_CMD_SIG 0x41 #define FU_CCGX_PD_RESP_DEVICE_RESET_CMD_SIG 0x52 #define FU_CCGX_PD_RESP_REG_RESET_DEVICE_CMD 0x01 #define FU_CCGX_PD_RESP_ENTER_FLASHING_MODE_CMD_SIG 0x50 #define FU_CCGX_PD_RESP_FLASH_READ_WRITE_CMD_SIG 0x46 #define FU_CCGX_PD_RESP_REG_FLASH_ROW_READ_CMD 0x00 #define FU_CCGX_PD_RESP_REG_FLASH_ROW_WRITE_CMD 0x01 #define FU_CCGX_PD_RESP_REG_FLASH_READ_WRITE_ROW_LSB 0x02 #define FU_CCGX_PD_RESP_REG_FLASH_READ_WRITE_ROW_MSB 0x03 #define FU_CCGX_PD_RESP_U_VDM_TYPE 0x00 #define HPI_GET_SILICON_ID_CMD_SIG 0x53 #define HPI_REG_INTR_REG_CLEAR_RQT 0x01 #define HPI_JUMP_TO_BOOT_CMD_SIG 0x4A #define HPI_DEVICE_RESET_CMD_SIG 0x52 #define HPI_REG_RESET_DEVICE_CMD 0x01 #define HPI_ENTER_FLASHING_MODE_CMD_SIG 0x50 #define HPI_FLASH_READ_WRITE_CMD_SIG 0x46 #define HPI_REG_FLASH_ROW_READ_CMD 0x00 #define HPI_REG_FLASH_ROW_WRITE_CMD 0x01 #define HPI_REG_FLASH_READ_WRITE_ROW_LSB 0x02 #define HPI_REG_FLASH_READ_WRITE_ROW_MSB 0x03 #define HPI_PORT_DISABLE_CMD 0x11 #define HPI_DEVICE_VERSION_SIZE_HPIV1 16 #define HPI_DEVICE_VERSION_SIZE_HPIV2 24 #define HPI_META_DATA_OFFSET_ROW_128 64 #define HPI_META_DATA_OFFSET_ROW_256 (64 + 128) #define PD_I2C_USB_EP_BULK_OUT 0x01 #define PD_I2C_USB_EP_BULK_IN 0x82 #define PD_I2C_USB_EP_INTR_IN 0x83 #define PD_I2CM_USB_EP_BULK_OUT 0x02 #define PD_I2CM_USB_EP_BULK_IN 0x83 #define PD_I2CM_USB_EP_INTR_IN 0x84 typedef enum { HPI_RESPONSE_NO_RESPONSE, HPI_RESPONSE_SUCCESS = 0x02, HPI_RESPONSE_FLASH_DATA_AVAILABLE, HPI_RESPONSE_INVALID_COMMAND = 0x05, HPI_RESPONSE_FLASH_UPDATE_FAILED = 0x07, HPI_RESPONSE_INVALID_FW, HPI_RESPONSE_INVALID_ARGUMENT, HPI_RESPONSE_NOT_SUPPORTED, HPI_RESPONSE_PD_TRANSACTION_FAILED = 0x0C, HPI_RESPONSE_PD_COMMAND_FAILED, HPI_RESPONSE_UNDEFINED_ERROR = 0x0F, HPI_EVENT_RESET_COMPLETE = 0x80, HPI_EVENT_MSG_OVERFLOW, HPI_EVENT_OC_DETECT, HPI_EVENT_OV_DETECT, HPI_EVENT_CONNECT_DETECT, HPI_EVENT_DISCONNECT_DETECT, HPI_EVENT_NEGOTIATION_COMPLETE, HPI_EVENT_SWAP_COMPLETE, HPI_EVENT_PS_RDY_RECEIVED = 0x8A, HPI_EVENT_GOTO_MIN_RECEIVED, HPI_EVENT_ACCEPT_RECEIVED, HPI_EVENT_REJECT_RECEIVED, HPI_EVENT_WAIT_RECEIVED, HPI_EVENT_HARD_RESET_RECEIVED, HPI_EVENT_VDM_RECEIVED = 0x90, HPI_EVENT_SOURCE_CAP_RECEIVED, HPI_EVENT_SINK_CAP_RECEIVED, HPI_EVENT_DP_MODE_ENTERED, HPI_EVENT_DP_STATUS_UPDATE, HPI_EVENT_DP_SID_NOT_FOUND = 0x96, HPI_EVENT_DP_MANY_SID_FOUND, HPI_EVENT_DP_NO_CABLE_SUPPORT, HPI_EVENT_DP_NO_UFP_SUPPORT, HPI_EVENT_HARD_RESET_SENT, HPI_EVENT_SOFT_RESET_SENT, HPI_EVENT_CABLE_RESET_SENT, HPI_EVENT_SOURCE_DISABLED, HPI_EVENT_SENDER_TIMEOUT, HPI_EVENT_VDM_NO_RESPONSE, HPI_EVENT_UNEXPECTED_VOLTAGE, HPI_EVENT_ERROR_RECOVERY, HPI_EVENT_EMCA_DETECT = 0xA6, HPI_EVENT_RP_CHANGE_DETECT = 0xAA, HPI_EVENT_TB_ENTERED = 0xB0, HPI_EVENT_TB_EXITED } HPIResp; fwupd-2.0.10/plugins/ccgx/fu-ccgx-hpi-device.c000066400000000000000000001337041501337203100210420ustar00rootroot00000000000000/* * Copyright 2020 Cypress Semiconductor Corporation. * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-ccgx-common.h" #include "fu-ccgx-firmware.h" #include "fu-ccgx-hpi-common.h" #include "fu-ccgx-hpi-device.h" #include "fu-ccgx-struct.h" struct _FuCcgxHpiDevice { FuUsbDevice parent_instance; guint8 inf_num; /* USB interface number */ guint8 scb_index; guint16 silicon_id; guint16 fw_app_type; guint8 hpi_addrsz; /* hpiv1: 1 byte, hpiv2: 2 byte */ guint8 num_ports; /* max number of ports */ FuCcgxFwMode fw_mode; FuCcgxImageType fw_image_type; guint8 target_address; guint8 ep_bulk_in; guint8 ep_bulk_out; guint8 ep_intr_in; guint32 flash_row_size; guint32 flash_size; }; #define FU_CCGX_HPI_DEVICE_FLAG_IS_IN_RESTART "device-is-in-restart" G_DEFINE_TYPE(FuCcgxHpiDevice, fu_ccgx_hpi_device, FU_TYPE_USB_DEVICE) #define HPI_CMD_REG_READ_WRITE_DELAY_MS 10 #define HPI_CMD_ENTER_FLASH_MODE_DELAY_MS 20 #define HPI_CMD_SETUP_EVENT_WAIT_TIME_MS 200 #define HPI_CMD_SETUP_EVENT_CLEAR_TIME_MS 150 #define HPI_CMD_COMMAND_RESPONSE_TIME_MS 500 #define HPI_CMD_COMMAND_CLEAR_EVENT_TIME_MS 30 #define HPI_CMD_RESET_COMPLETE_DELAY_MS 150 #define HPI_CMD_RETRY_DELAY 30 /* ms */ #define HPI_CMD_RESET_RETRY_CNT 3 #define HPI_CMD_ENTER_LEAVE_FLASH_MODE_RETRY_CNT 3 #define HPI_CMD_FLASH_WRITE_RETRY_CNT 3 #define HPI_CMD_FLASH_READ_RETRY_CNT 3 #define HPI_CMD_VALIDATE_FW_RETRY_CNT 3 static void fu_ccgx_hpi_device_to_string(FuDevice *device, guint idt, GString *str) { FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "ScbIndex", self->scb_index); fwupd_codec_string_append_hex(str, idt, "SiliconId", self->silicon_id); fwupd_codec_string_append_hex(str, idt, "FwAppType", self->fw_app_type); fwupd_codec_string_append_hex(str, idt, "HpiAddrsz", self->hpi_addrsz); fwupd_codec_string_append_hex(str, idt, "NumPorts", self->num_ports); fwupd_codec_string_append(str, idt, "FuCcgxFwMode", fu_ccgx_fw_mode_to_string(self->fw_mode)); fwupd_codec_string_append(str, idt, "FwImageType", fu_ccgx_image_type_to_string(self->fw_image_type)); fwupd_codec_string_append_hex(str, idt, "EpBulkIn", self->ep_bulk_in); fwupd_codec_string_append_hex(str, idt, "EpBulkOut", self->ep_bulk_out); fwupd_codec_string_append_hex(str, idt, "EpIntrIn", self->ep_intr_in); fwupd_codec_string_append_hex(str, idt, "CcgxFlashRowSize", self->flash_row_size); fwupd_codec_string_append_hex(str, idt, "CcgxFlashSize", self->flash_size); } typedef struct { guint8 mode; guint16 addr; guint8 *buf; gsize bufsz; } FuCcgxHpiDeviceRetryHelper; typedef struct { guint16 addr; const guint8 *buf; gsize bufsz; } FuCcgxHpiFlashWriteRetryHelper; typedef struct { guint16 addr; guint8 *buf; gsize bufsz; } FuCcgxHpiFlashReadRetryHelper; static gboolean fu_ccgx_hpi_device_i2c_reset_cb(FuDevice *device, gpointer user_data, GError **error) { FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE(device); FuCcgxHpiDeviceRetryHelper *helper = (FuCcgxHpiDeviceRetryHelper *)user_data; g_autoptr(GError) error_local = NULL; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, CY_I2C_RESET_CMD, (self->scb_index << CY_SCB_INDEX_POS) | helper->mode, 0x0, NULL, 0x0, NULL, FU_CCGX_HPI_WAIT_TIMEOUT, NULL, &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to reset i2c: %s", error_local->message); return FALSE; } return TRUE; } static gboolean fu_ccgx_hpi_device_check_i2c_status(FuCcgxHpiDevice *self, guint8 mode, GError **error) { guint8 buf[CY_I2C_GET_STATUS_LEN] = {0x0}; g_autoptr(GError) error_local = NULL; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, CY_I2C_GET_STATUS_CMD, (((guint16)self->scb_index) << CY_SCB_INDEX_POS) | mode, 0x0, buf, sizeof(buf), NULL, FU_CCGX_HPI_WAIT_TIMEOUT, NULL, &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to get i2c status: %s", error_local->message); return FALSE; } if (buf[0] & CY_I2C_ERROR_BIT) { if (buf[0] & 0x80) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "i2c status write error: 0x%x", buf[0]); return FALSE; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "i2c status read error: 0x%x", buf[0]); return FALSE; } return TRUE; } static gboolean fu_ccgx_hpi_device_get_i2c_config(FuCcgxHpiDevice *self, CyI2CConfig *i2c_config, GError **error) { g_autoptr(GError) error_local = NULL; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, CY_I2C_GET_CONFIG_CMD, ((guint16)self->scb_index) << CY_SCB_INDEX_POS, 0x0, (guint8 *)i2c_config, sizeof(*i2c_config), NULL, FU_CCGX_HPI_WAIT_TIMEOUT, NULL, &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "i2c get config error: control xfer: %s", error_local->message); return FALSE; } return TRUE; } static gboolean fu_ccgx_hpi_device_set_i2c_config(FuCcgxHpiDevice *self, CyI2CConfig *i2c_config, GError **error) { g_autoptr(GError) error_local = NULL; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, CY_I2C_SET_CONFIG_CMD, ((guint16)self->scb_index) << CY_SCB_INDEX_POS, 0x0, (guint8 *)i2c_config, sizeof(*i2c_config), NULL, FU_CCGX_HPI_WAIT_TIMEOUT, NULL, &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "i2c set config error: control xfer: %s", error_local->message); return FALSE; } return TRUE; } static gboolean fu_ccgx_hpi_device_wait_for_notify(FuCcgxHpiDevice *self, guint16 *bytes_pending, GError **error) { guint8 buf[CY_I2C_EVENT_NOTIFICATION_LEN] = {0x0}; g_autoptr(GError) error_local = NULL; if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), self->ep_intr_in, buf, sizeof(buf), NULL, FU_CCGX_HPI_WAIT_TIMEOUT, NULL, &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to get i2c event: %s", error_local->message); return FALSE; } /* @bytes_pending available on failure */ if (buf[0] & CY_I2C_ERROR_BIT) { if (bytes_pending != NULL) { if (!fu_memread_uint16_safe(buf, sizeof(buf), 0x01, bytes_pending, G_LITTLE_ENDIAN, error)) return FALSE; } if (buf[0] & 0x80) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "i2c status write error: 0x%x", buf[0]); return FALSE; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "i2c status read error: 0x%x", buf[0]); return FALSE; } return TRUE; } static gboolean fu_ccgx_hpi_device_i2c_read(FuCcgxHpiDevice *self, guint8 *buf, gsize bufsz, CyI2CDataConfigBits cfg_bits, GError **error) { guint8 target_address = 0; if (!fu_ccgx_hpi_device_check_i2c_status(self, CY_I2C_MODE_READ, error)) { g_prefix_error(error, "i2c read error: "); return FALSE; } target_address = (self->target_address & 0x7F) | (self->scb_index << 7); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, CY_I2C_READ_CMD, (((guint16)target_address) << 8) | cfg_bits, bufsz, NULL, 0x0, NULL, FU_CCGX_HPI_WAIT_TIMEOUT, NULL, error)) { g_prefix_error(error, "i2c read error: control xfer: "); return FALSE; } if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), self->ep_bulk_in, buf, bufsz, NULL, FU_CCGX_HPI_WAIT_TIMEOUT, NULL, error)) { g_prefix_error(error, "i2c read error: bulk xfer: "); return FALSE; } /* 10 msec delay */ fu_device_sleep(FU_DEVICE(self), I2C_READ_WRITE_DELAY_MS); if (!fu_ccgx_hpi_device_wait_for_notify(self, NULL, error)) { g_prefix_error(error, "i2c read error: "); return FALSE; } return TRUE; } static gboolean fu_ccgx_hpi_device_i2c_write(FuCcgxHpiDevice *self, guint8 *buf, gsize bufsz, CyI2CDataConfigBits cfg_bits, GError **error) { guint8 target_address; if (!fu_ccgx_hpi_device_check_i2c_status(self, CY_I2C_MODE_WRITE, error)) { g_prefix_error(error, "i2c get status error: "); return FALSE; } target_address = (self->target_address & 0x7F) | (self->scb_index << 7); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, CY_I2C_WRITE_CMD, ((guint16)target_address << 8) | (cfg_bits & CY_I2C_DATA_CONFIG_STOP), bufsz, /* idx */ NULL, 0x0, NULL, FU_CCGX_HPI_WAIT_TIMEOUT, NULL, error)) { g_prefix_error(error, "i2c write error: control xfer: "); return FALSE; } if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), self->ep_bulk_out, buf, bufsz, NULL, FU_CCGX_HPI_WAIT_TIMEOUT, NULL, error)) { g_prefix_error(error, "i2c write error: bulk xfer: "); return FALSE; } /* 10 msec delay */ fu_device_sleep(FU_DEVICE(self), I2C_READ_WRITE_DELAY_MS); if (!fu_ccgx_hpi_device_wait_for_notify(self, NULL, error)) { g_prefix_error(error, "i2c wait for notification error: "); return FALSE; } return TRUE; } static gboolean fu_ccgx_hpi_device_i2c_write_no_resp(FuCcgxHpiDevice *self, guint8 *buf, gsize bufsz, CyI2CDataConfigBits cfg_bits, GError **error) { guint8 target_address = 0; g_autoptr(GError) error_local = NULL; if (!fu_ccgx_hpi_device_check_i2c_status(self, CY_I2C_MODE_WRITE, error)) { g_prefix_error(error, "i2c write error: "); return FALSE; } target_address = (self->target_address & 0x7F) | (self->scb_index << 7); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, CY_I2C_WRITE_CMD, ((guint16)target_address << 8) | (cfg_bits & CY_I2C_DATA_CONFIG_STOP), bufsz, NULL, 0x0, NULL, FU_CCGX_HPI_WAIT_TIMEOUT, NULL, error)) { g_prefix_error(error, "i2c write error: control xfer: "); return FALSE; } /* device will reboot after this, so txfer will fail */ if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), self->ep_bulk_out, buf, bufsz, NULL, FU_CCGX_HPI_WAIT_TIMEOUT, NULL, &error_local)) { g_debug("ignoring i2c write error: bulk xfer: %s", error_local->message); } return TRUE; } static gboolean fu_ccgx_hpi_device_reg_read_cb(FuDevice *device, gpointer user_data, GError **error) { FuCcgxHpiDeviceRetryHelper *helper = (FuCcgxHpiDeviceRetryHelper *)user_data; FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE(device); g_autofree guint8 *bufhw = g_malloc0(self->hpi_addrsz); for (guint32 i = 0; i < self->hpi_addrsz; i++) bufhw[i] = (guint8)(helper->addr >> (8 * i)); if (!fu_ccgx_hpi_device_i2c_write(self, bufhw, self->hpi_addrsz, CY_I2C_DATA_CONFIG_NAK, error)) { g_prefix_error(error, "write error: "); return FALSE; } if (!fu_ccgx_hpi_device_i2c_read(self, helper->buf, helper->bufsz, CY_I2C_DATA_CONFIG_STOP | CY_I2C_DATA_CONFIG_NAK, error)) { g_prefix_error(error, "read error: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), HPI_CMD_REG_READ_WRITE_DELAY_MS); return TRUE; } static gboolean fu_ccgx_hpi_device_reg_read(FuCcgxHpiDevice *self, guint16 addr, guint8 *buf, gsize bufsz, GError **error) { FuCcgxHpiDeviceRetryHelper helper = { .addr = addr, .mode = CY_I2C_MODE_READ, .buf = buf, .bufsz = bufsz, }; return fu_device_retry(FU_DEVICE(self), fu_ccgx_hpi_device_reg_read_cb, HPI_CMD_RESET_RETRY_CNT, &helper, error); } static gboolean fu_ccgx_hpi_device_reg_write_cb(FuDevice *device, gpointer user_data, GError **error) { FuCcgxHpiDeviceRetryHelper *helper = (FuCcgxHpiDeviceRetryHelper *)user_data; FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE(device); g_autofree guint8 *bufhw = g_malloc0(helper->bufsz + self->hpi_addrsz); for (guint32 i = 0; i < self->hpi_addrsz; i++) bufhw[i] = (guint8)(helper->addr >> (8 * i)); memcpy(&bufhw[self->hpi_addrsz], helper->buf, helper->bufsz); /* nocheck:blocked */ if (!fu_ccgx_hpi_device_i2c_write(self, bufhw, helper->bufsz + self->hpi_addrsz, CY_I2C_DATA_CONFIG_STOP | CY_I2C_DATA_CONFIG_NAK, error)) { g_prefix_error(error, "reg write error: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), HPI_CMD_REG_READ_WRITE_DELAY_MS); return TRUE; } static gboolean fu_ccgx_hpi_device_reg_write(FuCcgxHpiDevice *self, guint16 addr, const guint8 *buf, gsize bufsz, GError **error) { FuCcgxHpiDeviceRetryHelper helper = { .addr = addr, .mode = CY_I2C_MODE_WRITE, .buf = (guint8 *)buf, .bufsz = bufsz, }; return fu_device_retry(FU_DEVICE(self), fu_ccgx_hpi_device_reg_write_cb, HPI_CMD_RESET_RETRY_CNT, &helper, error); } static gboolean fu_ccgx_hpi_device_reg_write_no_resp(FuCcgxHpiDevice *self, guint16 addr, guint8 *buf, guint16 bufsz, GError **error) { g_autofree guint8 *bufhw = g_malloc0(bufsz + self->hpi_addrsz); for (guint32 i = 0; i < self->hpi_addrsz; i++) bufhw[i] = (guint8)(addr >> (8 * i)); memcpy(&bufhw[self->hpi_addrsz], buf, bufsz); /* nocheck:blocked */ if (!fu_ccgx_hpi_device_i2c_write_no_resp(self, bufhw, bufsz + self->hpi_addrsz, CY_I2C_DATA_CONFIG_STOP | CY_I2C_DATA_CONFIG_NAK, error)) { g_prefix_error(error, "reg write no-resp error: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), HPI_CMD_REG_READ_WRITE_DELAY_MS); return TRUE; } static gboolean fu_ccgx_hpi_device_clear_intr(FuCcgxHpiDevice *self, HPIRegSection section, GError **error) { guint8 intr = 0; for (guint8 i = 0; i <= self->num_ports; i++) { if (i == section || section == HPI_REG_SECTION_ALL) FU_BIT_SET(intr, i); } if (!fu_ccgx_hpi_device_reg_write(self, HPI_DEV_REG_INTR_ADDR, &intr, sizeof(intr), error)) { g_prefix_error(error, "failed to clear interrupt: "); return FALSE; } return TRUE; } static guint16 fu_ccgx_hpi_device_reg_addr_gen(guint8 section, guint8 part, guint8 addr) { return (((guint16)section) << 12) | (((guint16)part) << 8) | addr; } static gboolean fu_ccgx_hpi_device_read_event_reg(FuCcgxHpiDevice *self, HPIRegSection section, HPIEvent *event, GError **error) { if (section != HPI_REG_SECTION_DEV) { guint16 reg_addr; guint8 buf[4] = {0x0}; /* first read the response register */ reg_addr = fu_ccgx_hpi_device_reg_addr_gen(section, HPI_REG_PART_PDDATA_READ, 0); if (!fu_ccgx_hpi_device_reg_read(self, reg_addr, buf, sizeof(buf), error)) { g_prefix_error(error, "read response reg error: "); return FALSE; } /* byte 1 is reserved and should read as zero */ buf[1] = 0; memcpy((guint8 *)event, buf, sizeof(buf)); /* nocheck:blocked */ if (event->event_length != 0) { reg_addr = fu_ccgx_hpi_device_reg_addr_gen(section, HPI_REG_PART_PDDATA_READ, sizeof(buf)); if (!fu_ccgx_hpi_device_reg_read(self, reg_addr, event->event_data, event->event_length, error)) { g_prefix_error(error, "read event data error: "); return FALSE; } } } else { guint8 buf[2] = {0x0}; if (!fu_ccgx_hpi_device_reg_read(self, FU_CCGX_PD_RESP_REG_RESPONSE_ADDR, buf, sizeof(buf), error)) { g_prefix_error(error, "read response reg error: "); return FALSE; } event->event_code = buf[0]; event->event_length = buf[1]; if (event->event_length != 0) { /* read the data memory */ if (!fu_ccgx_hpi_device_reg_read(self, FU_CCGX_PD_RESP_REG_BOOTDATA_MEMORY_ADDR, event->event_data, event->event_length, error)) { g_prefix_error(error, "read event data error: "); return FALSE; } } } /* success */ return fu_ccgx_hpi_device_clear_intr(self, section, error); } static gboolean fu_ccgx_hpi_device_app_read_intr_reg(FuCcgxHpiDevice *self, HPIRegSection section, HPIEvent *event_array, guint8 *event_count, GError **error) { guint16 reg_addr; guint8 event_count_tmp = 0; guint8 intr_reg = 0; reg_addr = fu_ccgx_hpi_device_reg_addr_gen(HPI_REG_SECTION_DEV, HPI_REG_PART_REG, HPI_DEV_REG_INTR_ADDR); if (!fu_ccgx_hpi_device_reg_read(self, reg_addr, &intr_reg, sizeof(intr_reg), error)) { g_prefix_error(error, "read intr reg error: "); return FALSE; } /* device section will not come here */ for (guint8 i = 0; i <= self->num_ports; i++) { /* check if this section is needed */ if (section == i || section == HPI_REG_SECTION_ALL) { /* check whether this section has any event/response */ if ((1 << i) & intr_reg) { if (!fu_ccgx_hpi_device_read_event_reg(self, section, &event_array[i], error)) { g_prefix_error(error, "read event error: "); return FALSE; } event_count_tmp++; } } } if (event_count != NULL) *event_count = event_count_tmp; return TRUE; } static gboolean fu_ccgx_hpi_device_wait_for_event(FuCcgxHpiDevice *self, HPIRegSection section, HPIEvent *event_array, guint32 timeout_ms, GError **error) { guint8 event_count = 0; g_autoptr(GTimer) start_time = g_timer_new(); do { if (!fu_ccgx_hpi_device_app_read_intr_reg(self, section, event_array, &event_count, error)) return FALSE; if (event_count > 0) return TRUE; } while (g_timer_elapsed(start_time, NULL) * 1000.f <= timeout_ms); /* timed out */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "failed to wait for event in %ums", timeout_ms); return FALSE; } static gboolean fu_ccgx_hpi_device_get_event(FuCcgxHpiDevice *self, HPIRegSection reg_section, FuCcgxPdResp *event, guint32 io_timeout, GError **error) { HPIEvent event_array[HPI_REG_SECTION_ALL + 1] = {0x0}; if (!fu_ccgx_hpi_device_wait_for_event(self, reg_section, event_array, io_timeout, error)) { g_prefix_error(error, "failed to get event: "); return FALSE; } *event = event_array[reg_section].event_code; return TRUE; } static gboolean fu_ccgx_hpi_device_clear_all_events(FuCcgxHpiDevice *self, guint32 io_timeout, GError **error) { HPIEvent event_array[HPI_REG_SECTION_ALL + 1] = {0x0}; if (io_timeout == 0) { return fu_ccgx_hpi_device_app_read_intr_reg(self, HPI_REG_SECTION_ALL, event_array, NULL, error); } for (guint8 i = 0; i < self->num_ports; i++) { g_autoptr(GError) error_local = NULL; if (!fu_ccgx_hpi_device_wait_for_event(self, i, event_array, io_timeout, &error_local)) { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT)) { g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to clear events: "); return FALSE; } } } return TRUE; } static gboolean fu_ccgx_hpi_device_validate_fw_cb(FuDevice *device, gpointer user_data, GError **error) { FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE(device); guint8 *fw_index = (guint8 *)user_data; FuCcgxPdResp hpi_event = 0; g_return_val_if_fail(fw_index != NULL, FALSE); if (!fu_ccgx_hpi_device_clear_all_events(self, HPI_CMD_COMMAND_CLEAR_EVENT_TIME_MS, error)) return FALSE; if (!fu_ccgx_hpi_device_reg_write(self, FU_CCGX_PD_RESP_REG_VALIDATE_FW_ADDR, fw_index, 1, error)) { g_prefix_error(error, "validate fw error: "); return FALSE; } if (!fu_ccgx_hpi_device_get_event(self, HPI_REG_SECTION_DEV, &hpi_event, HPI_CMD_COMMAND_RESPONSE_TIME_MS, error)) { g_prefix_error(error, "validate fw resp error: "); return FALSE; } if (hpi_event != FU_CCGX_PD_RESP_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "validate failed: %s [0x%x]", fu_ccgx_pd_resp_to_string(hpi_event), hpi_event); return FALSE; } return TRUE; } static gboolean fu_ccgx_hpi_device_validate_fw(FuCcgxHpiDevice *self, guint8 fw_index, GError **error) { return fu_device_retry(FU_DEVICE(self), fu_ccgx_hpi_device_validate_fw_cb, HPI_CMD_VALIDATE_FW_RETRY_CNT, &fw_index, error); } static gboolean fu_ccgx_hpi_device_enter_flash_mode_cb(FuDevice *device, gpointer user_data, GError **error) { FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE(device); FuCcgxPdResp hpi_event = 0; guint8 buf[] = {FU_CCGX_PD_RESP_ENTER_FLASHING_MODE_CMD_SIG}; if (!fu_ccgx_hpi_device_clear_all_events(self, HPI_CMD_COMMAND_CLEAR_EVENT_TIME_MS, error)) return FALSE; if (!fu_ccgx_hpi_device_reg_write(self, FU_CCGX_PD_RESP_REG_ENTER_FLASH_MODE_ADDR, buf, sizeof(buf), error)) { g_prefix_error(error, "enter flash mode error: "); return FALSE; } if (!fu_ccgx_hpi_device_get_event(self, HPI_REG_SECTION_DEV, &hpi_event, HPI_CMD_COMMAND_RESPONSE_TIME_MS, error)) { g_prefix_error(error, "enter flash mode resp error: "); return FALSE; } if (hpi_event != FU_CCGX_PD_RESP_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "enter flash failed: %s [0x%x]", fu_ccgx_pd_resp_to_string(hpi_event), hpi_event); return FALSE; } /* wait 10 msec */ fu_device_sleep(FU_DEVICE(self), HPI_CMD_ENTER_FLASH_MODE_DELAY_MS); return TRUE; } static gboolean fu_ccgx_hpi_device_enter_flash_mode(FuCcgxHpiDevice *self, GError **error) { return fu_device_retry(FU_DEVICE(self), fu_ccgx_hpi_device_enter_flash_mode_cb, HPI_CMD_ENTER_LEAVE_FLASH_MODE_RETRY_CNT, NULL, error); } static gboolean fu_ccgx_hpi_device_leave_flash_mode_cb(FuDevice *device, gpointer user_data, GError **error) { FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE(device); FuCcgxPdResp hpi_event = 0; guint8 buf = {0x0}; if (!fu_ccgx_hpi_device_clear_all_events(self, HPI_CMD_COMMAND_CLEAR_EVENT_TIME_MS, error)) return FALSE; if (!fu_ccgx_hpi_device_reg_write(self, FU_CCGX_PD_RESP_REG_ENTER_FLASH_MODE_ADDR, &buf, sizeof(buf), error)) { g_prefix_error(error, "leave flash mode error: "); return FALSE; } if (!fu_ccgx_hpi_device_get_event(self, HPI_REG_SECTION_DEV, &hpi_event, HPI_CMD_COMMAND_RESPONSE_TIME_MS, error)) { g_prefix_error(error, "leave flash mode resp error: "); return FALSE; } if (hpi_event != FU_CCGX_PD_RESP_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "leave flash mode failed: %s [0x%x]", fu_ccgx_pd_resp_to_string(hpi_event), hpi_event); return FALSE; } /* wait 10 msec */ fu_device_sleep(FU_DEVICE(self), HPI_CMD_ENTER_FLASH_MODE_DELAY_MS); return TRUE; } static gboolean fu_ccgx_hpi_device_leave_flash_mode(FuCcgxHpiDevice *self, GError **error) { return fu_device_retry(FU_DEVICE(self), fu_ccgx_hpi_device_leave_flash_mode_cb, HPI_CMD_ENTER_LEAVE_FLASH_MODE_RETRY_CNT, NULL, error); } static gboolean fu_ccgx_hpi_device_write_flash_cb(FuDevice *device, gpointer user_data, GError **error) { FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE(device); FuCcgxHpiFlashWriteRetryHelper *helper = (FuCcgxHpiFlashWriteRetryHelper *)user_data; FuCcgxPdResp hpi_event = 0; guint16 addr_tmp = 0; guint8 bufhw[] = { FU_CCGX_PD_RESP_FLASH_READ_WRITE_CMD_SIG, FU_CCGX_PD_RESP_REG_FLASH_ROW_WRITE_CMD, helper->addr & 0xFF, helper->addr >> 8, }; if (!fu_ccgx_hpi_device_clear_all_events(self, HPI_CMD_COMMAND_CLEAR_EVENT_TIME_MS, error)) return FALSE; /* write data to memory */ addr_tmp = self->hpi_addrsz > 1 ? HPI_DEV_REG_FLASH_MEM : FU_CCGX_PD_RESP_REG_BOOTDATA_MEMORY_ADDR; if (!fu_ccgx_hpi_device_reg_write(self, addr_tmp, helper->buf, helper->bufsz, error)) { g_prefix_error(error, "write buf to memory error: "); return FALSE; } if (!fu_ccgx_hpi_device_reg_write(self, FU_CCGX_PD_RESP_REG_FLASH_READ_WRITE_ADDR, bufhw, sizeof(bufhw), error)) { g_prefix_error(error, "write flash error: "); return FALSE; } /* wait until flash is written */ if (!fu_ccgx_hpi_device_get_event(self, HPI_REG_SECTION_DEV, &hpi_event, HPI_CMD_COMMAND_RESPONSE_TIME_MS, error)) { g_prefix_error(error, "write flash resp error: "); return FALSE; } if (hpi_event != FU_CCGX_PD_RESP_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "write flash failed: %s [0x%x]", fu_ccgx_pd_resp_to_string(hpi_event), hpi_event); return FALSE; } return TRUE; } static gboolean fu_ccgx_hpi_device_write_flash(FuCcgxHpiDevice *self, guint16 addr, const guint8 *buf, guint16 bufsz, GError **error) { FuCcgxHpiFlashWriteRetryHelper helper = { .addr = addr, .buf = buf, .bufsz = bufsz, }; return fu_device_retry(FU_DEVICE(self), fu_ccgx_hpi_device_write_flash_cb, HPI_CMD_FLASH_WRITE_RETRY_CNT, &helper, error); } static gboolean fu_ccgx_hpi_device_read_flash_cb(FuDevice *device, gpointer user_data, GError **error) { FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE(device); FuCcgxHpiFlashReadRetryHelper *helper = (FuCcgxHpiFlashReadRetryHelper *)user_data; FuCcgxPdResp hpi_event = 0; guint16 addr_tmp; guint8 bufhw[] = { FU_CCGX_PD_RESP_FLASH_READ_WRITE_CMD_SIG, FU_CCGX_PD_RESP_REG_FLASH_ROW_READ_CMD, helper->addr & 0xFF, helper->addr >> 8, }; /* set address */ if (!fu_ccgx_hpi_device_clear_all_events(self, HPI_CMD_COMMAND_CLEAR_EVENT_TIME_MS, error)) return FALSE; if (!fu_ccgx_hpi_device_reg_write(self, FU_CCGX_PD_RESP_REG_FLASH_READ_WRITE_ADDR, bufhw, sizeof(bufhw), error)) { g_prefix_error(error, "read flash error: "); return FALSE; } /* wait until flash is read */ if (!fu_ccgx_hpi_device_get_event(self, HPI_REG_SECTION_DEV, &hpi_event, HPI_CMD_COMMAND_RESPONSE_TIME_MS, error)) { g_prefix_error(error, "read flash resp error: "); return FALSE; } if (hpi_event != FU_CCGX_PD_RESP_FLASH_DATA_AVAILABLE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "read flash failed: %s [0x%x]", fu_ccgx_pd_resp_to_string(hpi_event), hpi_event); return FALSE; } addr_tmp = self->hpi_addrsz > 1 ? HPI_DEV_REG_FLASH_MEM : FU_CCGX_PD_RESP_REG_BOOTDATA_MEMORY_ADDR; if (!fu_ccgx_hpi_device_reg_read(self, addr_tmp, helper->buf, helper->bufsz, error)) { g_prefix_error(error, "read data from memory error: "); return FALSE; } return TRUE; } static gboolean fu_ccgx_hpi_device_read_flash(FuCcgxHpiDevice *self, guint16 addr, guint8 *buf, guint16 bufsz, GError **error) { FuCcgxHpiFlashReadRetryHelper helper = { .addr = addr, .buf = buf, .bufsz = bufsz, }; return fu_device_retry(FU_DEVICE(self), fu_ccgx_hpi_device_read_flash_cb, HPI_CMD_FLASH_READ_RETRY_CNT, &helper, error); } static gboolean fu_ccgx_hpi_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE(device); guint8 buf[] = { FU_CCGX_PD_RESP_JUMP_TO_ALT_FW_CMD_SIG, }; /* not required */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER) || self->fw_image_type == FU_CCGX_IMAGE_TYPE_DUAL_SYMMETRIC) return TRUE; /* jump to Alt FW */ if (!fu_ccgx_hpi_device_clear_all_events(self, HPI_CMD_COMMAND_CLEAR_EVENT_TIME_MS, error)) return FALSE; if (!fu_ccgx_hpi_device_reg_write(self, FU_CCGX_PD_RESP_JUMP_TO_BOOT_REG_ADDR, buf, sizeof(buf), error)) { g_prefix_error(error, "jump to alt mode error: "); return FALSE; } /* sym not required */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); fu_device_add_private_flag(device, FU_CCGX_HPI_DEVICE_FLAG_IS_IN_RESTART); /* success */ return TRUE; } static gboolean fu_ccgx_hpi_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE(device); guint8 buf[] = { FU_CCGX_PD_RESP_DEVICE_RESET_CMD_SIG, FU_CCGX_PD_RESP_REG_RESET_DEVICE_CMD, }; if (!fu_ccgx_hpi_device_clear_all_events(self, HPI_CMD_COMMAND_CLEAR_EVENT_TIME_MS, error)) return FALSE; if (!fu_ccgx_hpi_device_reg_write_no_resp(self, FU_CCGX_PD_RESP_REG_RESET_ADDR, buf, sizeof(buf), error)) { g_prefix_error(error, "reset device error: "); return FALSE; } fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); fu_device_add_private_flag(device, FU_CCGX_HPI_DEVICE_FLAG_IS_IN_RESTART); return TRUE; } static FuFirmware * fu_ccgx_hpi_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE(device); FuCcgxFwMode fw_mode; guint16 fw_app_type; guint16 fw_silicon_id; g_autoptr(FuFirmware) firmware = fu_ccgx_firmware_new(); /* parse all images */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; /* check the silicon ID */ fw_silicon_id = fu_ccgx_firmware_get_silicon_id(FU_CCGX_FIRMWARE(firmware)); if (fw_silicon_id != self->silicon_id) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "silicon id mismatch, expected 0x%x, got 0x%x", self->silicon_id, fw_silicon_id); return NULL; } if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_VID_PID) == 0) { fw_app_type = fu_ccgx_firmware_get_app_type(FU_CCGX_FIRMWARE(firmware)); if (fw_app_type != self->fw_app_type) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "app type mismatch, expected 0x%x, got 0x%x", self->fw_app_type, fw_app_type); return NULL; } } fw_mode = fu_ccgx_firmware_get_fw_mode(FU_CCGX_FIRMWARE(firmware)); if (fw_mode != fu_ccgx_fw_mode_get_alternate(self->fw_mode)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "FuCcgxFwMode mismatch, expected %s, got %s", fu_ccgx_fw_mode_to_string(fu_ccgx_fw_mode_get_alternate(self->fw_mode)), fu_ccgx_fw_mode_to_string(fw_mode)); return NULL; } return g_steal_pointer(&firmware); } static gboolean fu_ccgx_hpi_device_get_metadata_offset(FuCcgxHpiDevice *self, FuCcgxFwMode fw_mode, guint32 *addr, guint32 *offset, GError **error) { guint32 addr_max; /* sanity check */ if (self->flash_row_size == 0x0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unset support row size"); return FALSE; } /* get the row offset for the flash size */ addr_max = self->flash_size / self->flash_row_size; if (self->flash_row_size == 128) { *offset = HPI_META_DATA_OFFSET_ROW_128; } else if (self->flash_row_size == 256) { *offset = HPI_META_DATA_OFFSET_ROW_256; } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported support row size: 0x%x", self->flash_row_size); return FALSE; } /* get the row offset in the flash */ switch (fw_mode) { case FU_CCGX_FW_MODE_FW1: *addr = addr_max - 1; break; case FU_CCGX_FW_MODE_FW2: *addr = addr_max - 2; break; default: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "boot recovery not supported"); return FALSE; } return TRUE; } /* this will only work after fu_ccgx_hpi_device_enter_flash_mode() has been used */ static gboolean fu_ccgx_hpi_device_load_metadata(FuCcgxHpiDevice *self, FuCcgxFwMode fw_mode, GByteArray *st_metadata, GError **error) { guint32 addr = 0x0; guint32 md_offset = 0x0; g_autofree guint8 *buf = NULL; /* read flash at correct address */ if (!fu_ccgx_hpi_device_get_metadata_offset(self, fw_mode, &addr, &md_offset, error)) return FALSE; buf = g_malloc0(self->flash_row_size); if (!fu_ccgx_hpi_device_read_flash(self, addr, buf, self->flash_row_size, error)) { g_prefix_error(error, "fw metadata read error: "); return FALSE; } return fu_memcpy_safe(st_metadata->data, st_metadata->len, 0x0, buf, self->flash_row_size, md_offset, st_metadata->len, error); } /* this will only work after fu_ccgx_hpi_device_enter_flash_mode() has been used */ static gboolean fu_ccgx_hpi_device_save_metadata(FuCcgxHpiDevice *self, FuCcgxFwMode fw_mode, GByteArray *st_metadata, GError **error) { guint32 addr = 0x0; guint32 md_offset = 0x0; g_autofree guint8 *buf = NULL; /* read entire row of flash at correct address */ if (!fu_ccgx_hpi_device_get_metadata_offset(self, fw_mode, &addr, &md_offset, error)) return FALSE; buf = g_malloc0(self->flash_row_size); if (!fu_ccgx_hpi_device_read_flash(self, addr, buf, self->flash_row_size, error)) { g_prefix_error(error, "fw metadata read existing error: "); return FALSE; } if (!fu_memcpy_safe(buf, self->flash_row_size, md_offset, st_metadata->data, st_metadata->len, 0x0, st_metadata->len, error)) return FALSE; if (!fu_ccgx_hpi_device_write_flash(self, addr, buf, self->flash_row_size, error)) { g_prefix_error(error, "fw metadata write error: "); return FALSE; } return TRUE; } static gboolean fu_ccgx_hpi_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE(device); GPtrArray *records = fu_ccgx_firmware_get_records(FU_CCGX_FIRMWARE(firmware)); FuCcgxFwMode fw_mode_alt = fu_ccgx_fw_mode_get_alternate(self->fw_mode); g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GByteArray) st_metadata = fu_struct_ccgx_metadata_hdr_new(); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 5, "invalidate-metadata"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 80, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 10, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 5, "leave-flash"); /* enter flash mode */ locker = fu_device_locker_new_full(self, (FuDeviceLockerFunc)fu_ccgx_hpi_device_enter_flash_mode, (FuDeviceLockerFunc)fu_ccgx_hpi_device_leave_flash_mode, error); if (locker == NULL) return FALSE; /* invalidate metadata for alternate image */ if (!fu_ccgx_hpi_device_load_metadata(self, fw_mode_alt, st_metadata, error)) return FALSE; fu_struct_ccgx_metadata_hdr_set_metadata_valid(st_metadata, 0x0); if (!fu_ccgx_hpi_device_save_metadata(self, fw_mode_alt, st_metadata, error)) return FALSE; fu_progress_step_done(progress); /* write new image */ for (guint i = 0; i < records->len; i++) { FuCcgxFirmwareRecord *rcd = g_ptr_array_index(records, i); /* write chunk */ if (!fu_ccgx_hpi_device_write_flash(self, rcd->row_number, g_bytes_get_data(rcd->data, NULL), g_bytes_get_size(rcd->data), error)) { g_prefix_error(error, "fw write error @0x%x: ", rcd->row_number); return FALSE; } /* update progress */ fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)i + 1, (gsize)records->len); } fu_progress_step_done(progress); /* validate fw */ if (!fu_ccgx_hpi_device_validate_fw(self, fw_mode_alt, error)) { g_prefix_error(error, "fw validate error: "); return FALSE; } fu_progress_step_done(progress); /* this is a good time to leave the flash mode *before* rebooting */ if (!fu_device_locker_close(locker, error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_ccgx_hpi_device_ensure_silicon_id(FuCcgxHpiDevice *self, GError **error) { guint8 buf[2] = {0x0}; if (!fu_ccgx_hpi_device_reg_read(self, FU_CCGX_PD_RESP_SILICON_ID, buf, sizeof(buf), error)) { g_prefix_error(error, "get silicon id error: "); return FALSE; } if (!fu_memread_uint16_safe(buf, sizeof(buf), 0x0, &self->silicon_id, G_LITTLE_ENDIAN, error)) return FALSE; /* add quirks */ if (self->silicon_id != 0x0) fu_device_add_instance_u16(FU_DEVICE(self), "SID", self->silicon_id); fu_device_build_instance_id_full(FU_DEVICE(self), FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "CCGX", "SID", NULL); g_debug("got silicon ID: 0x%04x", self->silicon_id); /* sanity check */ if (self->flash_row_size == 0x0 || self->flash_size == 0x0 || self->flash_size % self->flash_row_size != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid row size for: 0x%x/0x%x", self->flash_row_size, self->flash_size); return FALSE; } /* success */ return TRUE; } static gchar * fu_ccgx_hpi_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_ccgx_version_to_string(version_raw); } static gboolean fu_ccgx_hpi_device_setup(FuDevice *device, GError **error) { FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE(device); CyI2CConfig i2c_config = {0x0}; guint32 hpi_event = 0; guint8 mode = 0; g_autoptr(GError) error_local = NULL; /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_ccgx_hpi_device_parent_class)->setup(device, error)) return FALSE; /* set the new config */ if (!fu_ccgx_hpi_device_get_i2c_config(self, &i2c_config, error)) { g_prefix_error(error, "get config error: "); return FALSE; } i2c_config.frequency = FU_CCGX_HPI_FREQ; i2c_config.is_initiator = TRUE; i2c_config.is_msb_first = TRUE; if (!fu_ccgx_hpi_device_set_i2c_config(self, &i2c_config, error)) { g_prefix_error(error, "set config error: "); return FALSE; } if (!fu_ccgx_hpi_device_reg_read(self, FU_CCGX_PD_RESP_REG_DEVICE_MODE_ADDR, &mode, 1, error)) { g_prefix_error(error, "get device mode error: "); return FALSE; } self->hpi_addrsz = mode & 0x80 ? 2 : 1; self->num_ports = (mode >> 2) & 0x03 ? 2 : 1; self->fw_mode = (FuCcgxFwMode)(mode & 0x03); fu_device_set_logical_id(device, fu_ccgx_fw_mode_to_string(self->fw_mode)); fu_device_add_instance_str(device, "MODE", fu_device_get_logical_id(device)); /* get silicon ID */ if (!fu_ccgx_hpi_device_ensure_silicon_id(self, error)) return FALSE; /* get correct version if not in boot mode */ if (self->fw_mode != FU_CCGX_FW_MODE_BOOT) { guint16 bufsz; guint32 versions[FU_CCGX_FW_MODE_LAST] = {0x0}; guint8 bufver[HPI_DEVICE_VERSION_SIZE_HPIV2] = {0x0}; bufsz = self->hpi_addrsz == 1 ? HPI_DEVICE_VERSION_SIZE_HPIV1 : HPI_DEVICE_VERSION_SIZE_HPIV2; if (!fu_ccgx_hpi_device_reg_read(self, FU_CCGX_PD_RESP_GET_VERSION, bufver, bufsz, error)) { g_prefix_error(error, "get version error: "); return FALSE; } /* fw1 */ if (!fu_memread_uint32_safe(bufver, sizeof(bufver), 0x0c, &versions[FU_CCGX_FW_MODE_FW1], G_LITTLE_ENDIAN, error)) return FALSE; /* fw2 */ if (!fu_memread_uint32_safe(bufver, sizeof(bufver), 0x14, &versions[FU_CCGX_FW_MODE_FW2], G_LITTLE_ENDIAN, error)) return FALSE; /* add GUIDs that are specific to the firmware app type */ self->fw_app_type = versions[self->fw_mode] & 0xffff; if (self->fw_app_type != 0x0) fu_device_add_instance_u16(device, "APP", self->fw_app_type); /* if running in bootloader force an upgrade to any version */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { fu_device_set_version_raw(device, 0x0); } else { fu_device_set_version_raw(device, versions[self->fw_mode]); } } /* not supported in boot mode */ if (self->fw_mode == FU_CCGX_FW_MODE_BOOT) { fu_device_inhibit(device, "device-in-boot-mode", "Not supported in BOOT mode"); } else { fu_device_uninhibit(device, "device-in-boot-mode"); } /* add extra instance IDs */ fu_device_build_instance_id(device, NULL, "USB", "VID", "PID", "SID", "APP", NULL); fu_device_build_instance_id(device, NULL, "USB", "VID", "PID", "SID", "APP", "MODE", NULL); /* if we are coming back from reset, wait for hardware to settle */ if (!fu_ccgx_hpi_device_get_event(self, HPI_REG_SECTION_DEV, &hpi_event, HPI_CMD_SETUP_EVENT_WAIT_TIME_MS, &error_local)) { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT)) { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } else { if (hpi_event == FU_CCGX_PD_RESP_RESET_COMPLETE) fu_device_sleep(FU_DEVICE(self), HPI_CMD_RESET_COMPLETE_DELAY_MS); } /* start with no events in the queue */ return fu_ccgx_hpi_device_clear_all_events(self, HPI_CMD_SETUP_EVENT_CLEAR_TIME_MS, error); } static gboolean fu_ccgx_hpi_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuCcgxHpiDevice *self = FU_CCGX_HPI_DEVICE(device); guint64 tmp = 0; if (g_strcmp0(key, "SiliconId") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->silicon_id = tmp; return TRUE; } if (g_strcmp0(key, "CcgxFlashRowSize") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->flash_row_size = tmp; return TRUE; } if (g_strcmp0(key, "CcgxFlashSize") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->flash_size = tmp; return TRUE; } if (g_strcmp0(key, "CcgxImageKind") == 0) { self->fw_image_type = fu_ccgx_image_type_from_string(value); if (self->fw_image_type != FU_CCGX_IMAGE_TYPE_UNKNOWN) return TRUE; g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid CcgxImageKind"); return FALSE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no supported"); return FALSE; } static gboolean fu_ccgx_hpi_device_close(FuDevice *device, GError **error) { /* do not close handle when device restarts */ if (fu_device_has_private_flag(device, FU_CCGX_HPI_DEVICE_FLAG_IS_IN_RESTART)) return TRUE; /* FuUsbDevice->close */ return FU_DEVICE_CLASS(fu_ccgx_hpi_device_parent_class)->close(device, error); } static void fu_ccgx_hpi_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static void fu_ccgx_hpi_device_init(FuCcgxHpiDevice *self) { self->inf_num = 0x0; self->hpi_addrsz = 1; self->num_ports = 1; self->target_address = PD_I2C_TARGET_ADDRESS; self->ep_bulk_out = PD_I2C_USB_EP_BULK_OUT; self->ep_bulk_in = PD_I2C_USB_EP_BULK_IN; self->ep_intr_in = PD_I2C_USB_EP_INTR_IN; fu_device_add_protocol(FU_DEVICE(self), "com.cypress.ccgx"); fu_device_add_protocol(FU_DEVICE(self), "com.infineon.ccgx"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SELF_RECOVERY); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_retry_set_delay(FU_DEVICE(self), HPI_CMD_RETRY_DELAY); fu_device_register_private_flag(FU_DEVICE(self), FU_CCGX_HPI_DEVICE_FLAG_IS_IN_RESTART); /* we can recover the I²C link using reset */ fu_device_retry_add_recovery(FU_DEVICE(self), FWUPD_ERROR, FWUPD_ERROR_READ, fu_ccgx_hpi_device_i2c_reset_cb); fu_device_retry_add_recovery(FU_DEVICE(self), FWUPD_ERROR, FWUPD_ERROR_WRITE, fu_ccgx_hpi_device_i2c_reset_cb); /* this might not be true for future hardware */ if (self->inf_num > 0) self->scb_index = 1; fu_usb_device_add_interface(FU_USB_DEVICE(self), self->inf_num); } static void fu_ccgx_hpi_device_class_init(FuCcgxHpiDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_ccgx_hpi_device_to_string; device_class->write_firmware = fu_ccgx_hpi_device_write_firmware; device_class->prepare_firmware = fu_ccgx_hpi_device_prepare_firmware; device_class->detach = fu_ccgx_hpi_device_detach; device_class->attach = fu_ccgx_hpi_device_attach; device_class->setup = fu_ccgx_hpi_device_setup; device_class->set_quirk_kv = fu_ccgx_hpi_device_set_quirk_kv; device_class->close = fu_ccgx_hpi_device_close; device_class->set_progress = fu_ccgx_hpi_device_set_progress; device_class->convert_version = fu_ccgx_hpi_device_convert_version; } fwupd-2.0.10/plugins/ccgx/fu-ccgx-hpi-device.h000066400000000000000000000005511501337203100210400ustar00rootroot00000000000000/* * Copyright 2020 Cypress Semiconductor Corporation. * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_CCGX_HPI_DEVICE (fu_ccgx_hpi_device_get_type()) G_DECLARE_FINAL_TYPE(FuCcgxHpiDevice, fu_ccgx_hpi_device, FU, CCGX_HPI_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/ccgx/fu-ccgx-plugin.c000066400000000000000000000022531501337203100203150ustar00rootroot00000000000000/* * Copyright 2020 Cypress Semiconductor Corporation. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-ccgx-firmware.h" #include "fu-ccgx-hid-device.h" #include "fu-ccgx-hpi-device.h" #include "fu-ccgx-plugin.h" #include "fu-ccgx-pure-hid-device.h" struct _FuCcgxPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuCcgxPlugin, fu_ccgx_plugin, FU_TYPE_PLUGIN) static void fu_ccgx_plugin_init(FuCcgxPlugin *self) { } static void fu_ccgx_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "CcgxFlashRowSize"); fu_context_add_quirk_key(ctx, "CcgxFlashSize"); fu_context_add_quirk_key(ctx, "CcgxImageKind"); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_CCGX_FIRMWARE); fu_plugin_add_device_gtype(plugin, FU_TYPE_CCGX_HID_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_CCGX_PURE_HID_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_CCGX_HPI_DEVICE); } static void fu_ccgx_plugin_class_init(FuCcgxPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_ccgx_plugin_constructed; } fwupd-2.0.10/plugins/ccgx/fu-ccgx-plugin.h000066400000000000000000000003461501337203100203230ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuCcgxPlugin, fu_ccgx_plugin, FU, CCGX_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/ccgx/fu-ccgx-pure-hid-device.c000066400000000000000000000340751501337203100220000ustar00rootroot00000000000000/* * Copyright 2023 Framework Computer Inc * Copyright 2024 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-ccgx-firmware.h" #include "fu-ccgx-hid-device.h" #include "fu-ccgx-hpi-common.h" #include "fu-ccgx-pure-hid-device.h" #include "fu-ccgx-pure-hid-struct.h" #define DEFAULT_ROW_SIZE 0x80 #define VIDPID_BLOCK_ID 6 struct _FuCcgxPureHidDevice { FuHidDevice parent_instance; FuCcgxPureHidFwMode operating_mode; guint32 silicon_id; gsize flash_row_size; }; G_DEFINE_TYPE(FuCcgxPureHidDevice, fu_ccgx_pure_hid_device, FU_TYPE_HID_DEVICE) #define FU_CCGX_PURE_HID_DEVICE_TIMEOUT 5000 /* ms */ static gboolean fu_ccgx_pure_hid_device_command(FuCcgxPureHidDevice *self, guint8 param1, guint8 param2, GError **error) { g_autoptr(GByteArray) cmd = fu_struct_ccgx_pure_hid_command_new(); fu_struct_ccgx_pure_hid_command_set_cmd(cmd, param1); fu_struct_ccgx_pure_hid_command_set_opt(cmd, param2); if (!fu_hid_device_set_report(FU_HID_DEVICE(self), FU_CCGX_PURE_HID_REPORT_ID_COMMAND, cmd->data, cmd->len, FU_CCGX_PURE_HID_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_NONE, error)) { return FALSE; } return TRUE; } static gboolean fu_ccgx_pure_hid_device_enter_flashing_mode(FuCcgxPureHidDevice *self, GError **error) { if (!fu_ccgx_pure_hid_device_command(self, FU_CCGX_PURE_HID_COMMAND_FLASH, FU_CCGX_PD_RESP_ENTER_FLASHING_MODE_CMD_SIG, error)) { g_prefix_error(error, "flashing enable command error: "); return FALSE; } return TRUE; } static gboolean fu_ccgx_pure_hid_device_magic_unlock(FuCcgxPureHidDevice *self, GError **error) { guint8 buf[8] = {FU_CCGX_PURE_HID_REPORT_ID_CUSTOM, FU_CCGX_PD_RESP_BRIDGE_MODE_CMD_SIG, 0x43, 0x59, 0x00, 0x00, 0x00, 0x0B}; g_autoptr(GError) error_local = NULL; if (!fu_hid_device_set_report(FU_HID_DEVICE(self), buf[0], buf, sizeof(buf), FU_CCGX_PURE_HID_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_IS_FEATURE, error)) { g_prefix_error(error, "magic enable command error: "); return FALSE; } /* ignore error: this always fails but has the correct behavior */ if (!fu_ccgx_pure_hid_device_command(self, FU_CCGX_PURE_HID_COMMAND_MODE, FU_CCGX_PD_RESP_BRIDGE_MODE_CMD_SIG, &error_local)) { g_debug("expected HID report bridge mode failure: %s", error_local->message); } return TRUE; } static gboolean fu_ccgx_pure_hid_device_ensure_fw_info(FuCcgxPureHidDevice *self, GError **error) { guint8 buf[0x40] = {FU_CCGX_PURE_HID_REPORT_ID_INFO, 0}; guint version = 0; g_autofree gchar *bl_ver = NULL; g_autoptr(GByteArray) st_info = NULL; if (!fu_hid_device_get_report(FU_HID_DEVICE(self), buf[0], buf, sizeof(buf), FU_CCGX_PURE_HID_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_IS_FEATURE, error)) return FALSE; st_info = fu_struct_ccgx_pure_hid_fw_info_parse(buf, sizeof(buf), 0x0, error); if (st_info == NULL) return FALSE; self->silicon_id = fu_struct_ccgx_pure_hid_fw_info_get_silicon_id(st_info); self->operating_mode = fu_struct_ccgx_pure_hid_fw_info_get_operating_mode(st_info); fu_device_remove_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); /* set current version */ switch (self->operating_mode) { case FU_CCGX_PURE_HID_FW_MODE_FW1: version = fu_struct_ccgx_pure_hid_fw_info_get_image1_version(st_info); break; case FU_CCGX_PURE_HID_FW_MODE_FW2: version = fu_struct_ccgx_pure_hid_fw_info_get_image2_version(st_info); break; case FU_CCGX_PURE_HID_FW_MODE_BOOT: /* force an upgrade to any version */ version = 0x0; fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); break; default: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported mode"); return FALSE; } fu_device_set_version_raw(FU_DEVICE(self), version); /* set bootloader version */ fu_device_set_version_bootloader_raw( FU_DEVICE(self), fu_struct_ccgx_pure_hid_fw_info_get_bl_version(st_info)); bl_ver = fu_version_from_uint32(fu_struct_ccgx_pure_hid_fw_info_get_bl_version(st_info), fu_device_get_version_format(self)); fu_device_set_version_bootloader(FU_DEVICE(self), bl_ver); /* TODO: d4s: from wireshark for ReadVersion.exe: * > e1 04 01 00 cc cc cc cc * > e4 42 41 41 00 00 00 18 */ return TRUE; } static gboolean fu_ccgx_pure_hid_device_setup(FuDevice *device, GError **error) { FuCcgxPureHidDevice *self = FU_CCGX_PURE_HID_DEVICE(device); /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_ccgx_pure_hid_device_parent_class)->setup(device, error)) return FALSE; if (!fu_ccgx_pure_hid_device_magic_unlock(self, error)) return FALSE; if (!fu_ccgx_pure_hid_device_ensure_fw_info(self, error)) return FALSE; fu_device_add_instance_strup(device, "MODE", fu_ccgx_pure_hid_fw_mode_to_string(self->operating_mode)); if (!fu_device_build_instance_id(FU_DEVICE(self), error, "USB", "VID", "PID", "MODE", NULL)) return FALSE; fu_device_add_instance_u16(FU_DEVICE(self), "SID", self->silicon_id); if (!fu_device_build_instance_id_full(FU_DEVICE(self), FU_DEVICE_INSTANCE_FLAG_QUIRKS, error, "CCGX", "SID", NULL)) return FALSE; /* ensure the remove delay is set, even if no quirk matched */ if (fu_device_get_remove_delay(FU_DEVICE(self)) == 0) fu_device_set_remove_delay(FU_DEVICE(self), 5000); /* success */ return TRUE; } static FuFirmware * fu_ccgx_pure_hid_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuCcgxPureHidDevice *self = FU_CCGX_PURE_HID_DEVICE(device); FuCcgxFwMode fw_mode; GPtrArray *records = NULL; FuCcgxFirmwareRecord *vidpid_rcd = NULL; guint16 fw_silicon_id; gsize fw_size = 0; guint16 vid; guint16 pid; g_autoptr(FuFirmware) firmware = fu_ccgx_firmware_new(); if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; /* check the silicon ID */ fw_silicon_id = fu_ccgx_firmware_get_silicon_id(FU_CCGX_FIRMWARE(firmware)); if (fw_silicon_id != self->silicon_id) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "silicon id mismatch, expected 0x%x, got 0x%x", self->silicon_id, fw_silicon_id); return NULL; } fw_mode = fu_ccgx_firmware_get_fw_mode(FU_CCGX_FIRMWARE(firmware)); if (fw_mode != fu_ccgx_fw_mode_get_alternate((FuCcgxFwMode)self->operating_mode)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "FuCcgxFwMode mismatch, expected %s, got %s", fu_ccgx_fw_mode_to_string( fu_ccgx_fw_mode_get_alternate((FuCcgxFwMode)self->operating_mode)), fu_ccgx_fw_mode_to_string(fw_mode)); return NULL; } /* validate all records has proper size */ records = fu_ccgx_firmware_get_records(FU_CCGX_FIRMWARE(firmware)); g_debug("records found: %u", records->len); for (guint i = 0; i < records->len; i++) { FuCcgxFirmwareRecord *record = g_ptr_array_index(records, i); gsize record_size = g_bytes_get_size(record->data); if (record_size != self->flash_row_size) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "expected block length %u, got %u: array id=0x%02x, " "row=0x%04x (:%02x%04x%04x)", (guint)self->flash_row_size, (guint)record_size, record->array_id, record->row_number, record->array_id, record->row_number, (guint)record_size); return NULL; } fw_size += record_size; } g_debug("firmware size: %u", (guint)fw_size); /* Check the target VID and PID */ /* FIXME: address is guessed: 0036 and 0206 for fw1 and fw2 */ if (records->len <= VIDPID_BLOCK_ID) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "unable to read VID and PID from the image"); return NULL; } vidpid_rcd = g_ptr_array_index(records, VIDPID_BLOCK_ID); if (!fu_memread_uint16_safe(g_bytes_get_data(vidpid_rcd->data, NULL), g_bytes_get_size(vidpid_rcd->data), 0, &vid, G_LITTLE_ENDIAN, error)) return NULL; if (!fu_memread_uint16_safe(g_bytes_get_data(vidpid_rcd->data, NULL), g_bytes_get_size(vidpid_rcd->data), 2, &pid, G_LITTLE_ENDIAN, error)) return NULL; if (vid != fu_device_get_vid(device) || pid != fu_device_get_pid(device)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "image VID:PID mismatch, expected %04X:%04X, got %04X:%04X", fu_device_get_vid(device), fu_device_get_pid(device), vid, pid); return NULL; } return g_steal_pointer(&firmware); } static gboolean fu_ccgx_pure_hid_device_write_row(FuCcgxPureHidDevice *self, guint16 address, const guint8 *row, gsize row_len, GError **error) { g_autoptr(GByteArray) st_hdr = fu_struct_ccgx_pure_hid_write_hdr_new(); fu_struct_ccgx_pure_hid_write_hdr_set_pd_resp(st_hdr, FU_CCGX_PD_RESP_FLASH_READ_WRITE_CMD_SIG); fu_struct_ccgx_pure_hid_write_hdr_set_addr(st_hdr, address); if (!fu_memcpy_safe(st_hdr->data, st_hdr->len, FU_STRUCT_CCGX_PURE_HID_WRITE_HDR_OFFSET_DATA, row, row_len, 0, self->flash_row_size, error)) return FALSE; if (!fu_hid_device_set_report(FU_HID_DEVICE(self), st_hdr->data[0], st_hdr->data, st_hdr->len, FU_CCGX_PURE_HID_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "write row command error: "); return FALSE; } return TRUE; } static gboolean fu_ccgx_pure_hid_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuCcgxPureHidDevice *self = FU_CCGX_PURE_HID_DEVICE(device); guint8 fw_mode = 1; GPtrArray *records = fu_ccgx_firmware_get_records(FU_CCGX_FIRMWARE(firmware)); if (!fu_ccgx_pure_hid_device_enter_flashing_mode(self, error)) return FALSE; if (self->operating_mode != FU_CCGX_PURE_HID_FW_MODE_FW2) fw_mode = 2; fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, records->len); for (guint i = 0; i < records->len; i++) { FuCcgxFirmwareRecord *record = g_ptr_array_index(records, i); if (!fu_ccgx_pure_hid_device_write_row(self, record->row_number, g_bytes_get_data(record->data, NULL), g_bytes_get_size(record->data), error)) return FALSE; fu_progress_step_done(progress); } if (!fu_ccgx_pure_hid_device_command(self, FU_CCGX_PURE_HID_COMMAND_SET_BOOT, fw_mode, error)) { g_prefix_error(error, "bootswitch command error: "); return FALSE; } if (!fu_ccgx_pure_hid_device_command(self, FU_CCGX_PURE_HID_COMMAND_JUMP, FU_CCGX_PD_RESP_DEVICE_RESET_CMD_SIG, error)) { g_prefix_error(error, "reset command error: "); return FALSE; } fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static void fu_ccgx_pure_hid_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_ccgx_pure_hid_device_init(FuCcgxPureHidDevice *self) { self->flash_row_size = DEFAULT_ROW_SIZE; fu_device_add_protocol(FU_DEVICE(self), "com.infineon.ccgx"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_INTEL_ME2); } static void fu_ccgx_pure_hid_device_to_string(FuDevice *device, guint idt, GString *str) { FuCcgxPureHidDevice *self = FU_CCGX_PURE_HID_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "SiliconId", self->silicon_id); fwupd_codec_string_append(str, idt, "FwMode", fu_ccgx_pure_hid_fw_mode_to_string(self->operating_mode)); fwupd_codec_string_append_hex(str, idt, "CcgxFlashRowSize", self->flash_row_size); } static gboolean fu_ccgx_pure_hid_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuCcgxPureHidDevice *self = FU_CCGX_PURE_HID_DEVICE(device); guint64 tmp = 0; if (g_strcmp0(key, "SiliconId") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->silicon_id = tmp; return TRUE; } if (g_strcmp0(key, "CcgxFlashRowSize") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->flash_row_size = tmp; return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported"); return FALSE; } static gchar * fu_ccgx_pure_hid_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32((guint32)version_raw, fu_device_get_version_format(device)); } static void fu_ccgx_pure_hid_device_class_init(FuCcgxPureHidDeviceClass *klass) { FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass); klass_device->to_string = fu_ccgx_pure_hid_device_to_string; klass_device->setup = fu_ccgx_pure_hid_device_setup; klass_device->write_firmware = fu_ccgx_pure_hid_device_write_firmware; klass_device->set_progress = fu_ccgx_pure_hid_device_set_progress; klass_device->set_quirk_kv = fu_ccgx_pure_hid_device_set_quirk_kv; klass_device->convert_version = fu_ccgx_pure_hid_device_convert_version; klass_device->prepare_firmware = fu_ccgx_pure_hid_device_prepare_firmware; } fwupd-2.0.10/plugins/ccgx/fu-ccgx-pure-hid-device.h000066400000000000000000000005321501337203100217740ustar00rootroot00000000000000/* * Copyright 2023 Framework Computer Inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_CCGX_PURE_HID_DEVICE (fu_ccgx_pure_hid_device_get_type()) G_DECLARE_FINAL_TYPE(FuCcgxPureHidDevice, fu_ccgx_pure_hid_device, FU, CCGX_PURE_HID_DEVICE, FuHidDevice) fwupd-2.0.10/plugins/ccgx/fu-ccgx-pure-hid.rs000066400000000000000000000026441501337203100207420ustar00rootroot00000000000000// Copyright 2024 Denis Pynkin // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] #[repr(u8)] enum FuCcgxPureHidFwMode { Boot, Fw1, Fw2, } #[repr(u8)] enum FuCcgxPureHidReportId { Info = 0xE0, Command = 0xE1, Write = 0xE2, Read = 0xE3, Custom = 0xE4, } #[repr(u8)] enum FuCcgxPureHidCommand { Jump = 0x01, Flash = 0x02, SetBoot = 0x04, Mode = 0x06, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructCcgxPureHidFwInfo { report_id: FuCcgxPureHidReportId == Info, _reserved_1: u8, signature: u16le == 0x5943, operating_mode: FuCcgxPureHidFwMode, bootloader_info: u8, bootmode_reason: u8, _reserved_2: u8, silicon_id: u32le, bl_version: u32le, _bl_version_reserved: [u8; 4], image1_version: u32le, _image1_version_reserved: [u8; 4], image2_version: u32le, _image2_version_reserved: [u8; 4], image1_row: u32le, image2_row: u32le, device_uid: [u8; 6], _reserved_3: [u8; 10], } #[derive(New, Default)] #[repr(C, packed)] struct FuStructCcgxPureHidCommand { report_id: FuCcgxPureHidReportId == Command, cmd: u8, opt: u8, pad1: u8 = 0x00, pad2: u32le = 0xCCCCCCCC, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructCcgxPureHidWriteHdr { report_id: FuCcgxPureHidReportId == Write, pd_resp: u8, addr: u16le, data: [u8; 128], } fwupd-2.0.10/plugins/ccgx/fu-ccgx.rs000066400000000000000000000036561501337203100172330ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(New, ParseBytes, Default)] #[repr(C, packed)] struct FuStructCcgxMetadataHdr { fw_checksum: u8, fw_entry: u32le, last_boot_row: u16le, // last flash row of bootloader or previous firmware _reserved1: [u8; 2], fw_size: u32le, _reserved2: [u8; 9], metadata_valid: u16le = 0x4359, // "CY" _reserved3: [u8; 4], boot_seq: u32le, } #[derive(ToString, FromString)] enum FuCcgxImageType { Unknown, Single, DualSymmetric, // A/B runtime DualAsymmetric, // A=bootloader (fixed) B=runtime DualAsymmetricVariable, // A=bootloader (variable) B=runtime } #[derive(ToString)] enum FuCcgxFwMode { Boot, Fw1, Fw2, } #[derive(ToString)] enum FuCcgxPdResp { // responses NoResponse, Success = 0x02, FlashDataAvailable, InvalidCommand = 0x05, CollisionDetected, FlashUpdateFailed, InvalidFw, InvalidArguments, NotSupported, TransactionFailed = 0x0C, PdCommandFailed, Undefined, RaDetect = 0x10, RaRemoved, // device specific events ResetComplete = 0x80, MessageQueueOverflow, // type-c specific events OverCurrentDetected, OverVoltageDetected, TypeCConnected, TypeCDisconnected, // pd specific events and asynchronous messages PdContractEstablished, DrSwap, PrSwap, VconSwap, PsRdy, Gotomin, AcceptMessage, RejectMessage, WaitMessage, HardReset, VdmReceived, SrcCapRcvd, SinkCapRcvd, DpAlternateMode, DpDeviceNonnected, DpDeviceNotConnected, DpSidNotFound, MultipleSvidDiscovered, DpFunctionNotSupported, DpPortConfigNotSupported, // not a response? HardResetSent, SoftResetSent, CableResetSent, SourceDisabledStateEntered, SenderResponseTimerTimeout, NoVdmResponseReceived, } fwupd-2.0.10/plugins/ccgx/fu-self-test.c000066400000000000000000000033371501337203100200070ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-ccgx-firmware.h" static void fu_ccgx_firmware_xml_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autofree gchar *csum1 = NULL; g_autofree gchar *csum2 = NULL; g_autofree gchar *xml_out = NULL; g_autofree gchar *xml_src = NULL; g_autoptr(FuFirmware) firmware1 = fu_ccgx_firmware_new(); g_autoptr(FuFirmware) firmware2 = fu_ccgx_firmware_new(); g_autoptr(GError) error = NULL; /* build and write */ filename = g_test_build_filename(G_TEST_DIST, "tests", "ccgx.builder.xml", NULL); ret = g_file_get_contents(filename, &xml_src, NULL, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_firmware_build_from_xml(firmware1, xml_src, &error); g_assert_no_error(error); g_assert_true(ret); csum1 = fu_firmware_get_checksum(firmware1, G_CHECKSUM_SHA1, &error); g_assert_no_error(error); g_assert_cmpstr(csum1, ==, "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"); /* ensure we can round-trip */ xml_out = fu_firmware_export_to_xml(firmware1, FU_FIRMWARE_EXPORT_FLAG_NONE, &error); g_assert_no_error(error); ret = fu_firmware_build_from_xml(firmware2, xml_out, &error); g_assert_no_error(error); g_assert_true(ret); csum2 = fu_firmware_get_checksum(firmware2, G_CHECKSUM_SHA1, &error); g_assert_cmpstr(csum1, ==, csum2); } int main(int argc, char **argv) { (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); /* tests go here */ g_test_add_func("/ccgx/firmware{xml}", fu_ccgx_firmware_xml_func); return g_test_run(); } fwupd-2.0.10/plugins/ccgx/meson.build000066400000000000000000000030101501337203100174530ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginCcgx"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files([ 'ccgx-ids.quirk', 'ccgx.quirk', ]) plugin_builtin_ccgx = static_library('fu_plugin_ccgx', rustgen.process( 'fu-ccgx.rs', # fuzzing 'fu-ccgx-pure-hid.rs', # fuzzing ), sources: [ 'fu-ccgx-plugin.c', 'fu-ccgx-common.c', # fuzzing 'fu-ccgx-firmware.c', # fuzzing 'fu-ccgx-hid-device.c', 'fu-ccgx-pure-hid-device.c', 'fu-ccgx-hpi-device.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) plugin_builtins += plugin_builtin_ccgx enumeration_data += files('tests/framework-hdmi-setup.json') device_tests += files('tests/framework-hdmi.json') if get_option('tests') install_data(['tests/ccgx.builder.xml'], install_dir: join_paths(installed_test_datadir, 'tests')) env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) e = executable( 'ccgx-self-test', rustgen.process('fu-ccgx.rs'), sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: plugin_deps, link_with: [ plugin_libs, plugin_builtin_ccgx, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('ccgx-self-test', e, env: env) endif fwupd-2.0.10/plugins/ccgx/tests/000077500000000000000000000000001501337203100164615ustar00rootroot00000000000000fwupd-2.0.10/plugins/ccgx/tests/ccgx.builder.xml000066400000000000000000000001601501337203100215510ustar00rootroot00000000000000 0x1F00 aGVsbG8gd29ybGQ= fwupd-2.0.10/plugins/ccgx/tests/framework-hdmi-setup.json000066400000000000000000000066661501337203100234440ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUsbDevice", "PlatformId": "1-5", "Created": "2024-10-24T17:00:14.631829Z", "IdVendor": 12972, "IdProduct": 2, "USB": 513, "Manufacturer": 1, "Product": 2, "SerialNumber": 3, "UsbBosDescriptors": [ { "DevCapabilityType": 80, "ExtraData": "AAM=" }, { "DevCapabilityType": 2, "ExtraData": "AAAAAA==" }, { "DevCapabilityType": 4, "ExtraData": "AAAAAAAAAAAAAAAAAAAAAAA=" }, { "DevCapabilityType": 13, "ExtraData": "BwEAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABABAAAB/wAI" } ], "UsbConfigDescriptors": [ { "Configuration": 4, "ConfigurationValue": 1 } ], "UsbHidDescriptors": [ "Bu7/CQGhAYXgCQIVACb/AHUIlT+xAoXhCQIVACb/AHUIlQeRAoXiCQIVACb/AHUIlYORAoXjCQIVACb/AHUIlYOxAoXkCQIVACb/AHUIlQexAsA=" ], "UsbInterfaces": [ { "Length": 9, "DescriptorType": 4, "InterfaceClass": 17, "Interface": 5 }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 1, "InterfaceClass": 3, "Interface": 6, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 129, "Interval": 255, "MaxPacketSize": 64 } ] } ], "UsbEvents": [ { "Id": "#4693935e", "Data": "001" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=64\nDEVNAME=bus/usb/001/065\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=32ac/2/0\nTYPE=0/0/0\nBUSNUM=001\nDEVNUM=065" }, { "Id": "#1ab3ae0a", "Data": "065" }, { "Id": "#4693935e", "Data": "001" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=64\nDEVNAME=bus/usb/001/065\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=32ac/2/0\nTYPE=0/0/0\nBUSNUM=001\nDEVNUM=065" }, { "Id": "#1ab3ae0a", "Data": "065" }, { "Id": "#ded9760f", "Data": "RnJhbWV3b3JrAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" }, { "Id": "#1fcf122d", "Data": "SERNSSBFeHBhbnNpb24gQ2FyZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" }, { "Id": "#028c3a0e", "Data": "MTFBRDFEMDAwMURCM0UxOTQyMTkwQjAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" }, { "Id": "#b4508d4c", "Data": "5EJDWQAAAAs=" }, { "Id": "#ef24907d", "Error": -9 }, { "Id": "#ec40eb22", "Data": "4ABDWQEAAAAAHQAAhAMCMGFhAgBpABAwYWECAGkAEDBhYQIAABgAAAAAAQAB2z4ZQhkLAAAAAAAAAAAAAAAAAA==" }, { "Id": "#c89fe8bd", "Data": "Bu7/CQGhAYXgCQIVACb/AHUIlT+xAoXhCQIVACb/AHUIlQeRAoXiCQIVACb/AHUIlYORAoXjCQIVACb/AHUIlYOxAoXkCQIVACb/AHUIlQexAsA=" } ] } ] } fwupd-2.0.10/plugins/ccgx/tests/framework-hdmi.json000066400000000000000000000005301501337203100222660ustar00rootroot00000000000000{ "name": "Framework HDMI Expansion Card", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/framework-hdmi-setup.json", "components": [ { "version": "3.0.16.105", "guids": [ "64e95475-00c5-583a-a4f4-bc212aa51504" ] } ] } ] } fwupd-2.0.10/plugins/cfu/000077500000000000000000000000001501337203100151505ustar00rootroot00000000000000fwupd-2.0.10/plugins/cfu/README.md000066400000000000000000000130501501337203100164260ustar00rootroot00000000000000--- title: Plugin: CFU - Component Firmware Update --- ## Introduction CFU is a protocol from Microsoft to make it easy to install firmware on HID devices. See for more details. This plugin supports the following protocol ID: * `com.microsoft.cfu` ## Implementation Notes CFU has a pre-download phase that is used to send the firmware *offer* to the microcontroller, so the device can check if the firmware is required and compatible. CFU also requires devices to be able to transfer the entire new transfer mode in runtime mode. The pre-download “offer†allows the device to check any sub-components attached (e.g. other devices attached to the SoC) and forces it to do dependency resolution in case sub-components have to be updated in a specific order. Pushing the dependency resolution down to the device means the low-power device has to do all the version comparisons and also know all the logic with regard to protocol incompatibilities. The end-user could be in a position where the device firmware needs to be updated so that it “knows†about the new protocol restrictions, which are needed to update the device and the things attached in the right order in a subsequent update. If the user always updates the device to the latest version, the factory-default running version *might yet know* about the new restrictions. It is therefore imperative that all previous versions are tested being updated *from*. Something that we support in fwupd is being able to restrict the peripheral device firmware to a specific SMBIOS CHID or a system firmware vendor, which lets vendors solve the *same hardware in different chassis, with custom firmware* problem. Using CFU in Microsoft Windows also means that the peripheral is unaware of the other devices in the system, so for instance couldn’t only install a new firmware version for only new builds of Windows for example. A few other consideration for vendors is the doubling of flash storage required to do an runtime transfer, the extra power budget of being woken up to process the *offer* and providing enough bulk power to stay alive if *unplugged* during a A/B swap. On most existing hardware the easiest way to implement CFU is an additional ARM micro-controller to act as a CFU “bridge†for legacy silicon. The CFU “bridge†could also do signing and encryption. CFU does not define any standard way to encrypt and sign firmware, or to detect if devices have any firmware verification capabilities and so this too will need to be set per-device either in the metadata or in the quirk file. CFU also downloads in the runtime mode in the background, at a maximum of 52 bytes per HID request and response. This means even small updates will take a long time to complete due to the huge number of USB control transfers required. The specification also doesn't specify the HID reports to use, so it all needs to be hardcoded per-device unless the exact same defaults are used as in `CFU/Tools/ComponentFirmwareUpdateStandAloneToolSample/protocolCfgExample.cfg`. In fwupd these can be set as quirks in `cfu.quirk`. The included `https://github.com/fwupd/fwupd/blob/main/contrib/cfu-inf-to-quirk.py` script may be useful to convert an existing `.inf` file to fwupd `.quirk` format. ## Firmware Format Due to the one-shot way fwupd deploys firmware, the daemon only deals with one “payload†per update. The offer and payload currently have to be combined in an archive where they are transferred to the device one after the other. The files in the firmware archive therefore should have the extensions `.offer.bin` and `.payload.bin`. ## GUID Generation These devices use standard USB DeviceInstanceId values, as well as two extra for the component ID and the bank, e.g. * `HIDRAW\VEN_17EF&DEV_7226&CID_01&BANK_1` * `HIDRAW\VEN_17EF&DEV_7226&CID_01` * `HIDRAW\VEN_17EF&DEV_7226` ## Quirk Use This plugin uses the following plugin-specific quirks: ### CfuVersionGetReport The HID report usage to use when parsing the response of `GET_FIRMWARE_VERSION`. This usually corresponds to the `VersionsFeatureValueCapabilityUsageRangeMinimum` value set in the `.inf` file. Since: 1.9.1 ### CfuOfferSetReport The HID report usage to use when sending the request for `FIRMWARE_UPDATE_OFFER`. This usually corresponds to the `OfferOutputValueCapabilityUsageRangeMinimum` value set in the `.inf` file. Since: 1.9.1 ### CfuOfferGetReport The HID report usage to use when parsing the response of `FIRMWARE_UPDATE_OFFER`. This usually corresponds to the `OfferInputValueCapabilityUsageRangeMinimum` value set in the `.inf` file. Since: 1.9.1 ### CfuContentSetReport The HID report usage to use when sending the request for `FIRMWARE_UPDATE_CONTENT`. This usually corresponds to the `PayloadOutputValueCapabilityUsageRangeMinimum` value set in the `.inf` file. Since: 1.9.1 ### CfuContentGetReport The HID report usage to use when parsing the response of `FIRMWARE_UPDATE_CONTENT`. This usually corresponds to the `PayloadInputValueCapabilityUsageRangeMinimum` value set in the `.inf` file. Since: 1.9.1 ## Update Behavior The device has to support runtime updates and does not have a detach-into-bootloader mode -- but after the install has completed the device still has to reboot into the new firmware. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `HIDRAW:0x17EF` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.7.1`. fwupd-2.0.10/plugins/cfu/cfu.quirk000066400000000000000000000003761501337203100170100ustar00rootroot00000000000000[USB\VID_273F&PID_100A] Plugin = cfu # Microsoft USB-C Travel Hub [USB\VID_045E&PID_09BC] Plugin = cfu Vendor = Microsoft CfuVersionGetReport = 0xC8 CfuOfferSetReport = 0xDC CfuOfferGetReport = 0xD8 CfuContentSetReport = 0xC7 CfuContentGetReport = 0xCC fwupd-2.0.10/plugins/cfu/fu-cfu-device.c000066400000000000000000000452471501337203100177520ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-cfu-device.h" #include "fu-cfu-module.h" #include "fu-cfu-struct.h" typedef struct { guint8 op; guint8 id; guint8 ct; } FuCfuDeviceMap; struct _FuCfuDevice { FuHidDevice parent_instance; guint8 protocol_version; FuCfuDeviceMap version_get_report; FuCfuDeviceMap offer_set_report; FuCfuDeviceMap offer_get_report; FuCfuDeviceMap content_set_report; FuCfuDeviceMap content_get_report; }; G_DEFINE_TYPE(FuCfuDevice, fu_cfu_device, FU_TYPE_HID_DEVICE) #define FU_CFU_DEVICE_TIMEOUT 5000 /* ms */ #define FU_CFU_DEVICE_FLAG_SEND_OFFER_INFO "send-offer-info" static void fu_cfu_device_map_to_string(GString *str, guint idt, FuCfuDeviceMap *map, const gchar *title) { g_autofree gchar *title_op = g_strdup_printf("%sOp", title); g_autofree gchar *title_id = g_strdup_printf("%sId", title); g_autofree gchar *title_ct = g_strdup_printf("%sCt", title); fwupd_codec_string_append_hex(str, idt, title_op, map->op); fwupd_codec_string_append_hex(str, idt, title_id, map->id); fwupd_codec_string_append_hex(str, idt, title_ct, map->ct); } static void fu_cfu_device_to_string(FuDevice *device, guint idt, GString *str) { FuCfuDevice *self = FU_CFU_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "ProtocolVersion", self->protocol_version); fu_cfu_device_map_to_string(str, idt, &self->version_get_report, "VersionGetReport"); fu_cfu_device_map_to_string(str, idt, &self->offer_set_report, "OfferSetReport"); fu_cfu_device_map_to_string(str, idt, &self->offer_get_report, "OfferGetReport"); fu_cfu_device_map_to_string(str, idt, &self->content_set_report, "ContentSetReport"); fu_cfu_device_map_to_string(str, idt, &self->content_get_report, "ContentGetReport"); } static gboolean fu_cfu_device_send_offer_info(FuCfuDevice *self, FuCfuOfferInfoCode info_code, GError **error) { g_autoptr(GByteArray) buf_in = g_byte_array_new(); g_autoptr(GByteArray) buf_out = g_byte_array_new(); g_autoptr(GByteArray) st_req = fu_struct_cfu_offer_info_req_new(); g_autoptr(GByteArray) st_res = NULL; /* not all devices handle this */ if (!fu_device_has_private_flag(FU_DEVICE(self), FU_CFU_DEVICE_FLAG_SEND_OFFER_INFO)) return TRUE; /* SetReport */ fu_struct_cfu_offer_info_req_set_code(st_req, info_code); fu_byte_array_append_uint8(buf_out, self->offer_set_report.id); g_byte_array_append(buf_out, st_req->data, st_req->len); fu_byte_array_set_size(buf_out, self->offer_set_report.ct, 0x0); if (!fu_hid_device_set_report(FU_HID_DEVICE(self), self->offer_set_report.id, buf_out->data, buf_out->len, FU_CFU_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to send offer info: "); return FALSE; } /* GetReport */ fu_byte_array_append_uint8(buf_in, self->offer_get_report.id); fu_byte_array_set_size(buf_in, self->offer_get_report.ct + 0x1, 0x0); if (!fu_hid_device_get_report(FU_HID_DEVICE(self), self->offer_get_report.id, buf_in->data, buf_in->len, FU_CFU_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error)) { g_prefix_error(error, "failed to send offer info: "); return FALSE; } st_res = fu_struct_cfu_offer_rsp_parse(buf_in->data, buf_in->len, 0x1, error); if (st_res == NULL) return FALSE; if (fu_struct_cfu_offer_rsp_get_token(st_res) != FU_STRUCT_CFU_OFFER_INFO_REQ_DEFAULT_TOKEN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "token invalid: got 0x%x and expected 0x%x", fu_struct_cfu_offer_rsp_get_token(st_res), (guint)FU_STRUCT_CFU_OFFER_INFO_REQ_DEFAULT_TOKEN); return FALSE; } if (fu_struct_cfu_offer_rsp_get_status(st_res) != FU_CFU_OFFER_STATUS_ACCEPT) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "offer info %s not supported: %s", fu_cfu_offer_info_code_to_string(info_code), fu_cfu_offer_status_to_string(fu_struct_cfu_offer_rsp_get_status(st_res))); return FALSE; } /* success */ return TRUE; } static gboolean fu_cfu_device_send_offer(FuCfuDevice *self, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { g_autoptr(GByteArray) buf_in = g_byte_array_new(); g_autoptr(GByteArray) buf_out = g_byte_array_new(); g_autoptr(GByteArray) st = NULL; g_autoptr(GBytes) blob = NULL; /* generate a offer blob */ if (flags & FWUPD_INSTALL_FLAG_FORCE) fu_cfu_offer_set_force_ignore_version(FU_CFU_OFFER(firmware), TRUE); blob = fu_firmware_write(firmware, error); if (blob == NULL) return FALSE; /* SetReport */ fu_byte_array_append_uint8(buf_out, self->offer_set_report.id); fu_byte_array_append_bytes(buf_out, blob); fu_byte_array_set_size(buf_out, self->offer_set_report.ct, 0x0); if (!fu_hid_device_set_report(FU_HID_DEVICE(self), self->offer_set_report.id, buf_out->data, buf_out->len, FU_CFU_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to send offer: "); return FALSE; } /* GetReport */ fu_byte_array_append_uint8(buf_in, self->offer_get_report.id); fu_byte_array_set_size(buf_in, self->offer_get_report.ct + 0x1, 0x0); if (!fu_hid_device_get_report(FU_HID_DEVICE(self), self->offer_get_report.id, buf_in->data, buf_in->len, FU_CFU_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error)) { g_prefix_error(error, "failed to get offer response: "); return FALSE; } st = fu_struct_cfu_offer_rsp_parse(buf_in->data, buf_in->len, 0x1, error); if (st == NULL) return FALSE; if (fu_struct_cfu_offer_rsp_get_token(st) != fu_cfu_offer_get_token(FU_CFU_OFFER(firmware))) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "offer token invalid: got %02x but expected %02x", fu_struct_cfu_offer_rsp_get_token(st), fu_cfu_offer_get_token(FU_CFU_OFFER(firmware))); return FALSE; } if (fu_struct_cfu_offer_rsp_get_status(st) != FU_CFU_OFFER_STATUS_ACCEPT) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "offer not supported: %s: %s", fu_cfu_offer_status_to_string(fu_struct_cfu_offer_rsp_get_status(st)), fu_cfu_rr_code_to_string(fu_struct_cfu_offer_rsp_get_rr_code(st))); return FALSE; } /* success */ return TRUE; } static gboolean fu_cfu_device_send_payload(FuCfuDevice *self, FuFirmware *firmware, FuProgress *progress, GError **error) { g_autoptr(GPtrArray) chunks = NULL; /* write each chunk */ chunks = fu_firmware_get_chunks(firmware, error); if (chunks == NULL) return FALSE; fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); g_autoptr(GByteArray) buf_in = g_byte_array_new(); g_autoptr(GByteArray) buf_out = g_byte_array_new(); g_autoptr(GByteArray) st_req = fu_struct_cfu_content_req_new(); g_autoptr(GByteArray) st_rsp = NULL; /* build */ if (i == 0) { fu_struct_cfu_content_req_set_flags(st_req, FU_CFU_CONTENT_FLAG_FIRST_BLOCK); } else if (i == chunks->len - 1) { fu_struct_cfu_content_req_set_flags(st_req, FU_CFU_CONTENT_FLAG_LAST_BLOCK); } fu_struct_cfu_content_req_set_data_length(st_req, fu_chunk_get_data_sz(chk)); fu_struct_cfu_content_req_set_seq_number(st_req, i); fu_struct_cfu_content_req_set_address(st_req, fu_chunk_get_address(chk)); fu_byte_array_append_uint8(buf_out, self->content_set_report.id); g_byte_array_append(buf_out, st_req->data, st_req->len); g_byte_array_append(buf_out, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); fu_byte_array_set_size(buf_out, self->content_set_report.ct + 1, 0x0); /* SetReport */ if (!fu_hid_device_set_report(FU_HID_DEVICE(self), self->content_set_report.id, buf_out->data, buf_out->len, FU_CFU_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to send payload: "); return FALSE; } /* GetReport */ fu_byte_array_append_uint8(buf_in, self->content_get_report.id); fu_byte_array_set_size(buf_in, self->content_get_report.ct + 1, 0x0); if (!fu_hid_device_get_report(FU_HID_DEVICE(self), self->content_get_report.id, buf_in->data, buf_in->len, FU_CFU_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error)) { g_prefix_error(error, "failed to get payload response: "); return FALSE; } st_rsp = fu_struct_cfu_content_rsp_parse(buf_in->data, buf_in->len, 0x1, error); if (st_rsp == NULL) return FALSE; /* verify */ if (fu_struct_cfu_content_rsp_get_seq_number(st_rsp) != fu_struct_cfu_content_req_get_seq_number(st_req)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "sequence number invalid 0x%x: expected 0x%x", fu_struct_cfu_content_rsp_get_seq_number(st_rsp), fu_struct_cfu_content_req_get_seq_number(st_req)); return FALSE; } if (fu_struct_cfu_content_rsp_get_status(st_rsp) != FU_CFU_CONTENT_STATUS_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to send chunk %u: %s", i + 1, fu_cfu_content_status_to_string( fu_struct_cfu_content_rsp_get_status(st_rsp))); return FALSE; } fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_cfu_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuCfuDevice *self = FU_CFU_DEVICE(device); g_autoptr(FuFirmware) fw_offer = NULL; g_autoptr(FuFirmware) fw_payload = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "start-entire"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "start-offer"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "offer"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 98, "payload"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "end-offer"); /* get both images */ fw_offer = fu_firmware_get_image_by_id(firmware, FU_FIRMWARE_ID_HEADER, error); if (fw_offer == NULL) return FALSE; fw_payload = fu_firmware_get_image_by_id(firmware, FU_FIRMWARE_ID_PAYLOAD, error); if (fw_payload == NULL) return FALSE; /* host is now initialized */ if (!fu_cfu_device_send_offer_info(self, FU_CFU_OFFER_INFO_CODE_START_ENTIRE_TRANSACTION, error)) return FALSE; fu_progress_step_done(progress); /* send offer */ if (!fu_cfu_device_send_offer_info(self, FU_CFU_OFFER_INFO_CODE_START_OFFER_LIST, error)) return FALSE; fu_progress_step_done(progress); if (!fu_cfu_device_send_offer(self, fw_offer, fu_progress_get_child(progress), flags, error)) return FALSE; fu_progress_step_done(progress); /* send payload */ if (!fu_cfu_device_send_payload(self, fw_payload, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* all done */ if (!fu_cfu_device_send_offer_info(self, FU_CFU_OFFER_INFO_CODE_END_OFFER_LIST, error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } /* find report properties from usage */ static gboolean fu_cfu_device_ensure_map_item(FuHidDescriptor *descriptor, FuCfuDeviceMap *map, GError **error) { g_autoptr(FuFirmware) item_ct = NULL; g_autoptr(FuFirmware) item_id = NULL; g_autoptr(FuHidReport) report = NULL; report = fu_hid_descriptor_find_report(FU_HID_DESCRIPTOR(descriptor), error, "usage", map->op, NULL); if (report == NULL) return FALSE; item_id = fu_firmware_get_image_by_id(FU_FIRMWARE(report), "report-id", error); if (item_id == NULL) return FALSE; map->id = fu_hid_report_item_get_value(FU_HID_REPORT_ITEM(item_id)); item_ct = fu_firmware_get_image_by_id(FU_FIRMWARE(report), "report-count", error); if (item_ct == NULL) return FALSE; map->ct = fu_hid_report_item_get_value(FU_HID_REPORT_ITEM(item_ct)); return TRUE; } static gboolean fu_cfu_device_verify_descriptor(FuCfuDevice *self, FuHidDescriptor *descriptor, GError **error) { if (!fu_cfu_device_ensure_map_item(descriptor, &self->version_get_report, error)) { g_prefix_error(error, "invalid version-get-report: "); return FALSE; } if (!fu_cfu_device_ensure_map_item(descriptor, &self->offer_set_report, error)) { g_prefix_error(error, "invalid offer-set-report: "); return FALSE; } if (!fu_cfu_device_ensure_map_item(descriptor, &self->offer_get_report, error)) { g_prefix_error(error, "invalid offer-get-report: "); return FALSE; } if (!fu_cfu_device_ensure_map_item(descriptor, &self->content_set_report, error)) { g_prefix_error(error, "invalid content-set-report: "); return FALSE; } if (!fu_cfu_device_ensure_map_item(descriptor, &self->content_get_report, error)) { g_prefix_error(error, "invalid content-get-report: "); return FALSE; } return TRUE; } static gboolean fu_cfu_device_setup(FuDevice *device, GError **error) { FuCfuDevice *self = FU_CFU_DEVICE(device); guint8 component_cnt = 0; gsize offset = 0; g_autoptr(GHashTable) modules_by_cid = NULL; g_autoptr(GByteArray) st = NULL; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GPtrArray) descriptors = NULL; g_autoptr(GString) descriptors_error = g_string_new(NULL); /* FuHidDevice->setup */ if (!FU_DEVICE_CLASS(fu_cfu_device_parent_class)->setup(device, error)) return FALSE; /* weirdly, use the in EP if out is missing */ if (fu_hid_device_get_ep_addr_out(FU_HID_DEVICE(device)) == 0x0) { fu_hid_device_set_ep_addr_out(FU_HID_DEVICE(device), fu_hid_device_get_ep_addr_in(FU_HID_DEVICE(device))); } /* try to parse each HID descriptor -- trying to find a CFU section */ descriptors = fu_hid_device_parse_descriptors(FU_HID_DEVICE(device), error); if (descriptors == NULL) return FALSE; for (guint i = 0; i < descriptors->len; i++) { FuHidDescriptor *descriptor = g_ptr_array_index(descriptors, i); g_autoptr(GError) error_local = NULL; if (!fu_cfu_device_verify_descriptor(self, descriptor, &error_local)) { if (descriptors_error->len > 0) { g_string_append(descriptors_error, ", "); } g_string_append_printf(descriptors_error, "descriptor 0x%x: %s", i, error_local->message); continue; } } if (self->content_get_report.ct == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no CFU descriptor found: %s", descriptors_error->str); return FALSE; } /* get version */ fu_byte_array_append_uint8(buf, self->version_get_report.id); fu_byte_array_set_size(buf, self->version_get_report.ct + 0x1, 0x0); if (!fu_hid_device_get_report(FU_HID_DEVICE(device), self->version_get_report.id, buf->data, buf->len, FU_CFU_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_IS_FEATURE, error)) return FALSE; st = fu_struct_cfu_get_version_rsp_parse(buf->data, buf->len, 0x1, error); if (st == NULL) return FALSE; self->protocol_version = fu_struct_cfu_get_version_rsp_get_flags(st) & 0b1111; /* keep track of all modules so we can work out which are dual bank */ modules_by_cid = g_hash_table_new(g_direct_hash, g_direct_equal); /* read each component module version */ offset += 0x1 + st->len; component_cnt = fu_struct_cfu_get_version_rsp_get_component_cnt(st); for (guint i = 0; i < component_cnt; i++) { g_autoptr(FuCfuModule) module = fu_cfu_module_new(device); FuCfuModule *module_tmp; if (!fu_cfu_module_setup(module, buf->data, buf->len, offset, error)) return FALSE; fu_device_add_child(device, FU_DEVICE(module)); /* same module already exists, so mark both as being dual bank */ module_tmp = g_hash_table_lookup(modules_by_cid, GINT_TO_POINTER(fu_cfu_module_get_component_id(module))); if (module_tmp != NULL) { fu_device_add_flag(FU_DEVICE(module), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_flag(FU_DEVICE(module_tmp), FWUPD_DEVICE_FLAG_DUAL_IMAGE); } else { g_hash_table_insert(modules_by_cid, GINT_TO_POINTER(fu_cfu_module_get_component_id(module)), module); } /* done */ offset += FU_STRUCT_CFU_GET_VERSION_RSP_COMPONENT_SIZE; } /* success */ return TRUE; } static gboolean fu_cfu_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuCfuDevice *self = FU_CFU_DEVICE(device); /* load from quirks */ if (g_strcmp0(key, "CfuVersionGetReport") == 0) { guint64 tmp = 0; if (!fu_strtoull(value, &tmp, 0x0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->version_get_report.op = tmp; return TRUE; } if (g_strcmp0(key, "CfuOfferSetReport") == 0) { guint64 tmp = 0; if (!fu_strtoull(value, &tmp, 0x0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->offer_set_report.op = tmp; return TRUE; } if (g_strcmp0(key, "CfuOfferGetReport") == 0) { guint64 tmp = 0; if (!fu_strtoull(value, &tmp, 0x0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->offer_get_report.op = tmp; return TRUE; } if (g_strcmp0(key, "CfuContentSetReport") == 0) { guint64 tmp = 0; if (!fu_strtoull(value, &tmp, 0x0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->content_set_report.op = tmp; return TRUE; } if (g_strcmp0(key, "CfuContentGetReport") == 0) { guint64 tmp = 0; if (!fu_strtoull(value, &tmp, 0x0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->content_get_report.op = tmp; return TRUE; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_cfu_device_init(FuCfuDevice *self) { /* values taken from CFU/Tools/ComponentFirmwareUpdateStandAloneToolSample/README.md */ self->version_get_report.op = 0x62; self->offer_set_report.op = 0x8A; self->offer_get_report.op = 0x8E; self->content_set_report.op = 0x61; self->content_get_report.op = 0x66; fu_hid_device_add_flag(FU_HID_DEVICE(self), FU_HID_DEVICE_FLAG_AUTODETECT_EPS); fu_device_register_private_flag(FU_DEVICE(self), FU_CFU_DEVICE_FLAG_SEND_OFFER_INFO); } static void fu_cfu_device_class_init(FuCfuDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_cfu_device_setup; device_class->to_string = fu_cfu_device_to_string; device_class->write_firmware = fu_cfu_device_write_firmware; device_class->set_quirk_kv = fu_cfu_device_set_quirk_kv; } fwupd-2.0.10/plugins/cfu/fu-cfu-device.h000066400000000000000000000004341501337203100177440ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_CFU_DEVICE (fu_cfu_device_get_type()) G_DECLARE_FINAL_TYPE(FuCfuDevice, fu_cfu_device, FU, CFU_DEVICE, FuHidDevice) fwupd-2.0.10/plugins/cfu/fu-cfu-module.c000066400000000000000000000146141501337203100177720ustar00rootroot00000000000000/*# * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-cfu-module.h" #include "fu-cfu-struct.h" struct _FuCfuModule { FuDevice parent_instance; guint8 component_id; guint8 bank; }; G_DEFINE_TYPE(FuCfuModule, fu_cfu_module, FU_TYPE_DEVICE) static void fu_cfu_module_to_string(FuDevice *device, guint idt, GString *str) { FuCfuModule *self = FU_CFU_MODULE(device); fwupd_codec_string_append_hex(str, idt, "ComponentId", self->component_id); fwupd_codec_string_append_hex(str, idt, "Bank", self->bank); } guint8 fu_cfu_module_get_component_id(FuCfuModule *self) { return self->component_id; } gboolean fu_cfu_module_setup(FuCfuModule *self, const guint8 *buf, gsize bufsz, gsize offset, GError **error) { FuDevice *device = FU_DEVICE(self); FuDevice *parent = fu_device_get_proxy(device); g_autofree gchar *logical_id = NULL; g_autoptr(GByteArray) st = NULL; /* parse */ st = fu_struct_cfu_get_version_rsp_component_parse(buf, bufsz, offset, error); if (st == NULL) return FALSE; /* these GUIDs may cause the name or version-format to be overwritten */ self->component_id = fu_struct_cfu_get_version_rsp_component_get_component_id(st); fu_device_add_instance_u8(device, "CID", self->component_id); if (!fu_device_build_instance_id(device, error, "USB", "VID", "PID", NULL)) return FALSE; if (!fu_device_build_instance_id(device, error, "USB", "VID", "PID", "CID", NULL)) return FALSE; /* bank */ self->bank = fu_struct_cfu_get_version_rsp_component_get_flags(st) & 0b11; fu_device_add_instance_u4(device, "BANK", self->bank); if (!fu_device_build_instance_id(device, error, "USB", "VID", "PID", "CID", "BANK", NULL)) return FALSE; /* set name, if not already set using a quirk */ if (fu_device_get_name(device) == NULL) { g_autofree gchar *name = NULL; name = g_strdup_printf("%s (0x%02X:0x%02x)", fu_device_get_name(parent), self->component_id, self->bank); fu_device_set_name(device, name); } /* version */ fu_device_set_version_raw(device, fu_struct_cfu_get_version_rsp_component_get_fw_version(st)); /* logical ID */ logical_id = g_strdup_printf("CID:0x%02x,BANK:0x%02x", self->component_id, self->bank); fu_device_set_logical_id(device, logical_id); /* success */ return TRUE; } static FuFirmware * fu_cfu_module_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { g_autoptr(FuFirmware) firmware = fu_firmware_new(); g_autoptr(FuFirmware) firmware_archive = fu_archive_firmware_new(); g_autoptr(FuFirmware) fw_offer = NULL; g_autoptr(FuFirmware) fw_payload = NULL; g_autoptr(FuFirmware) offer = fu_cfu_offer_new(); g_autoptr(FuFirmware) payload = fu_cfu_payload_new(); g_autoptr(GBytes) blob_offer = NULL; g_autoptr(GBytes) blob_payload = NULL; /* parse archive */ if (!fu_firmware_parse_stream(firmware_archive, stream, 0x0, flags, error)) return NULL; /* offer */ fw_offer = fu_archive_firmware_get_image_fnmatch(FU_ARCHIVE_FIRMWARE(firmware_archive), "*.offer.bin", error); if (fw_offer == NULL) return NULL; blob_offer = fu_firmware_get_bytes(fw_offer, NULL); if (blob_offer == NULL) return NULL; if (!fu_firmware_parse_bytes(offer, blob_offer, 0x0, flags, error)) return NULL; fu_firmware_set_id(offer, FU_FIRMWARE_ID_HEADER); fu_firmware_add_image(firmware, offer); /* payload */ fw_payload = fu_archive_firmware_get_image_fnmatch(FU_ARCHIVE_FIRMWARE(firmware_archive), "*.payload.bin", error); if (fw_payload == NULL) return NULL; blob_payload = fu_firmware_get_bytes(fw_payload, NULL); if (blob_payload == NULL) return NULL; if (!fu_firmware_parse_bytes(payload, blob_payload, 0x0, flags, error)) return NULL; fu_firmware_set_id(payload, FU_FIRMWARE_ID_PAYLOAD); fu_firmware_add_image(firmware, payload); /* success */ return g_steal_pointer(&firmware); } static gboolean fu_cfu_module_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDevice *proxy; FuDeviceClass *device_class; /* process by the parent */ proxy = fu_device_get_proxy(device); if (proxy == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy device assigned"); return FALSE; } device_class = FU_DEVICE_GET_CLASS(proxy); return device_class->write_firmware(proxy, firmware, progress, flags, error); } static void fu_cfu_module_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 96, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static gchar * fu_cfu_module_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_device_get_version_format(device)); } static void fu_cfu_module_init(FuCfuModule *self) { fu_device_add_protocol(FU_DEVICE(self), "com.microsoft.cfu"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_SURFACE); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_ARCHIVE_FIRMWARE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_SIGNED); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_OPEN); } static void fu_cfu_module_class_init(FuCfuModuleClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_cfu_module_to_string; device_class->prepare_firmware = fu_cfu_module_prepare_firmware; device_class->write_firmware = fu_cfu_module_write_firmware; device_class->set_progress = fu_cfu_module_set_progress; device_class->convert_version = fu_cfu_module_convert_version; } FuCfuModule * fu_cfu_module_new(FuDevice *parent) { FuCfuModule *self; self = g_object_new(FU_TYPE_CFU_MODULE, "proxy", parent, "parent", parent, NULL); return self; } fwupd-2.0.10/plugins/cfu/fu-cfu-module.h000066400000000000000000000010161501337203100177670ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_CFU_MODULE (fu_cfu_module_get_type()) G_DECLARE_FINAL_TYPE(FuCfuModule, fu_cfu_module, FU, CFU_MODULE, FuDevice) guint8 fu_cfu_module_get_component_id(FuCfuModule *self); gboolean fu_cfu_module_setup(FuCfuModule *self, const guint8 *buf, gsize bufsz, gsize offset, GError **error); FuCfuModule * fu_cfu_module_new(FuDevice *parent); fwupd-2.0.10/plugins/cfu/fu-cfu-plugin.c000066400000000000000000000017521501337203100200020ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-cfu-device.h" #include "fu-cfu-plugin.h" struct _FuCfuPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuCfuPlugin, fu_cfu_plugin, FU_TYPE_PLUGIN) static void fu_cfu_plugin_init(FuCfuPlugin *self) { } static void fu_cfu_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "CfuVersionGetReport"); fu_context_add_quirk_key(ctx, "CfuOfferSetReport"); fu_context_add_quirk_key(ctx, "CfuOfferGetReport"); fu_context_add_quirk_key(ctx, "CfuContentSetReport"); fu_context_add_quirk_key(ctx, "CfuContentGetReport"); fu_plugin_add_device_gtype(plugin, FU_TYPE_CFU_DEVICE); } static void fu_cfu_plugin_class_init(FuCfuPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_cfu_plugin_constructed; } fwupd-2.0.10/plugins/cfu/fu-cfu-plugin.h000066400000000000000000000003431501337203100200020ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuCfuPlugin, fu_cfu_plugin, FU, CFU_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/cfu/fu-cfu.rs000066400000000000000000000046011501337203100167040ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // Copyright 2021 Michael Cheng // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(New, Parse)] #[repr(C, packed)] struct FuStructCfuGetVersionRsp { component_cnt: u8, _reserved: u16le, flags: u8, } #[derive(New, Parse)] #[repr(C, packed)] struct FuStructCfuGetVersionRspComponent { fw_version: u32le, flags: u8, component_id: u8, _vendor_specific: u16le, } #[derive(ToString)] #[repr(u8)] enum FuCfuOfferInfoCode { StartEntireTransaction = 0x00, StartOfferList = 0x01, EndOfferList = 0x02, } #[derive(ToString)] #[repr(u8)] enum FuCfuRrCode { OfferRejectOldFirmware = 0x00, OfferRejectInvComponent = 0x01, UpdateOfferSwapPending = 0x02, WrongBank = 0x04, SignRule = 0xE0, VerReleaseDebug = 0xE1, DebugSameVersion = 0xE2, None = 0xFF, } #[derive(ToString)] #[repr(u8)] enum FuCfuOfferStatus { Skip = 0x00, Accept = 0x01, Reject = 0x02, Busy = 0x03, Command = 0x04, CmdNotSupported = 0xFF, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructCfuOfferInfoReq { code: FuCfuOfferInfoCode, _reserved1: u8, component_id: u8 == 0xFF, token: u8 == 0xDE, // chosen by dice roll _reserved2: [u8; 12], } #[derive(Parse)] #[repr(C, packed)] struct FuStructCfuOfferRsp { _reserved1: [u8; 3], token: u8, _reserved2: [u8; 4], rr_code: FuCfuRrCode, _reserved3: [u8; 3], status: FuCfuOfferStatus, _reserved3: [u8; 3], } #[repr(u8)] enum FuCfuContentFlag { Verify = 0x08, TestReplaceFilesystem = 0x20, LastBlock = 0x40, FirstBlock = 0x80, } #[derive(ToString)] #[repr(u8)] enum FuCfuContentStatus { Success = 0x00, ErrorPrepare = 0x01, ErrorWrite = 0x02, ErrorComplete = 0x03, ErrorVerify = 0x04, ErrorCrc = 0x05, ErrorSignature = 0x06, ErrorVersion = 0x07, SwapPending = 0x08, ErrorInvalidAddr 0x09, ErrorNoOffer = 0x0A, ErrorInvalid = 0x0B, } #[derive(New, Getters)] #[repr(C, packed)] struct FuStructCfuContentReq { flags: FuCfuContentFlag, data_length: u8, seq_number: u16le, address: u32le, } #[derive(Parse)] #[repr(C, packed)] struct FuStructCfuContentRsp { seq_number: u16le, _reserved1: u16le, status: FuCfuContentStatus, _reserved2: [u8; 3], _reserved3: [u8; 4], _reserved4: [u8; 4], } fwupd-2.0.10/plugins/cfu/meson.build000066400000000000000000000016721501337203100173200ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginCfu"'] plugins += {meson.current_source_dir().split('/')[-1]: true} # do not use structgen as these files are used in the elanfp plugin too... cfu_rs = custom_target('fu-cfu-rs', input: 'fu-cfu.rs', output: ['fu-cfu-struct.c', 'fu-cfu-struct.h'], command: [ python3, join_paths(meson.project_source_root(), 'libfwupdplugin', 'rustgen.py'), '@INPUT@', '@OUTPUT0@', '@OUTPUT1@', ], ) plugin_quirks += files('cfu.quirk') plugin_builtin_cfu = static_library('fu_plugin_cfu', cfu_rs, sources: [ 'fu-cfu-device.c', 'fu-cfu-module.c', 'fu-cfu-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) plugin_builtins += plugin_builtin_cfu plugincfu_incdir = include_directories('.') enumeration_data += files('tests/microsoft-usbc-travel-hub-setup.json') device_tests += files('tests/microsoft-usbc-travel-hub.json') fwupd-2.0.10/plugins/cfu/tests/000077500000000000000000000000001501337203100163125ustar00rootroot00000000000000fwupd-2.0.10/plugins/cfu/tests/microsoft-usbc-travel-hub-setup.json000066400000000000000000000062671501337203100253640ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUsbDevice", "PlatformId": "1-5.4", "Created": "2024-10-24T15:20:05.758885Z", "IdVendor": 1118, "IdProduct": 2492, "Device": 257, "USB": 513, "Manufacturer": 1, "Product": 2, "UsbBosDescriptors": [ { "DevCapabilityType": 32, "ExtraData": "AAI=" }, { "DevCapabilityType": 2, "ExtraData": "HvQAAA==" }, { "DevCapabilityType": 4, "ExtraData": "AK9PrA0Ah0Dut1gmoQfXbGs=" } ], "UsbConfigDescriptors": [ { "ConfigurationValue": 1 } ], "UsbHidDescriptors": [ "Bgv/CgEBoQEVACX/hfR1CJU8CceRAoX1dQiVEAnMgQKF8nUIlRAJ3JEChfN1CJUQCdiBAoXxdQiVPAnIsQKF+XUIlRgJ3ZEChfh1CJUUCd6BAsA=" ], "UsbInterfaces": [ { "Length": 9, "DescriptorType": 4, "InterfaceClass": 3, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 129, "Interval": 16, "MaxPacketSize": 64 } ] } ], "UsbEvents": [ { "Id": "#d5a801ad", "Data": "usb" }, { "Id": "#ad8c58d3", "Data": "usb" }, { "Id": "#1075ed5c", "Data": "usb_device" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=40\nDEVNAME=bus/usb/001/041\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=45e/9bc/101\nTYPE=0/0/0\nBUSNUM=001\nDEVNUM=041" }, { "Id": "#d432c663", "Data": "bus/usb/001/041" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c", "Data": "usb_device" }, { "Id": "#4693935e", "Data": "001" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=40\nDEVNAME=bus/usb/001/041\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=45e/9bc/101\nTYPE=0/0/0\nBUSNUM=001\nDEVNUM=041" }, { "Id": "#1ab3ae0a", "Data": "041" }, { "Id": "#4693935e", "Data": "001" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=40\nDEVNAME=bus/usb/001/041\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=45e/9bc/101\nTYPE=0/0/0\nBUSNUM=001\nDEVNUM=041" }, { "Id": "#1ab3ae0a", "Data": "041" }, { "Id": "#1fcf122d", "Data": "VVNCMi4wIEhJRAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" }, { "Id": "#dee13002", "Data": "Bgv/CgEBoQEVACX/hfR1CJU8CceRAoX1dQiVEAnMgQKF8nUIlRAJ3JEChfN1CJUQCdiBAoXxdQiVPAnIsQKF+XUIlRgJ3ZEChfh1CJUUCd6BAsA=" }, { "Id": "#07310562", "Data": "8QEAAAIDDwkCAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsQ==" } ] } ] } fwupd-2.0.10/plugins/cfu/tests/microsoft-usbc-travel-hub.json000066400000000000000000000005361501337203100242170ustar00rootroot00000000000000{ "name": "Microsoft USB-C Travel Hub", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/microsoft-usbc-travel-hub-setup.json", "components": [ { "version": "2.2319.3", "guids": [ "0ea2a144-c192-5d42-8b4d-8036392acfac" ] } ] } ] } fwupd-2.0.10/plugins/ch341a/000077500000000000000000000000001501337203100153565ustar00rootroot00000000000000fwupd-2.0.10/plugins/ch341a/README.md000066400000000000000000000033601501337203100166370ustar00rootroot00000000000000--- title: Plugin: CH341A --- ## Introduction The CH341A is an affordable SPI programmer. The assumed map between UIO command bits, pins on CH341A chip and pins on SPI chip: UIO CH341A SPI CH341A 0 D0/15 CS/1 CS0 1 D1/16 unused CS1 2 D2/17 unused CS2 3 D3/18 SCK/6 DCK 4 D4/19 unused DOUT2 5 D5/20 SI/5 DOUBT 6 D6/21 unused DIN2 7 D7/22 SO/2 DIN IMPORTANT NOTE: You must perform the 3.3V signal output modification if you are using the CH341A with 3.3V SPI chips. The CH341A has a design flaw that outputs 5V on the MISO and MOSI pins even when VCC is 3V which will almost certainly be out-of-specification for the device you are trying to program. ![CH341A Signal Output Modification](ch341a-vmod.png) See [this guide](https://www.chucknemeth.com/usb-devices/ch341a/3v-ch341a-mod) for more details. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob of unspecified format. This plugin supports the following protocol ID: - `org.jedec.cfi` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. - `USB\VID_1A86&PID_5512` ## Update Behavior The device programs devices in raw mode, and can best be used with `fwupdtool`. To write an image, use `sudo fwupdtool --plugins ch341a install-blob firmware.bin` and to backup the contents of a SPI device use `sudo fwupdtool --plugins ch341a firmware-dump backup.bin` ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x1A86` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.8.0`. fwupd-2.0.10/plugins/ch341a/ch341a-vmod.png000066400000000000000000012003041501337203100200120ustar00rootroot00000000000000‰PNG  IHDRõjŒTÐ pHYs.#.#x¥?v IDATxÚL¼Y³dÙu¶†½÷2óæjìêê¡Ýh HÐ'I”…_ᇟýhG(l?È!‡íPØ’"ø˜#"Îç³{÷Þ8>ºùÙg=yüÚk÷SJqr.c” PÕ˜)ø€@}ßÅqÌšÕHU»Ù¬‘”½<µ”sN¯½öú[o= ¢õfóèÉã¡ï°ª«”;ªªä<ö]ãÝ;·ÚfvyqõâÅYL ¼÷mÛŠˆª²óEsÎ9Q3$R3QEDUED3BVf_U5"#圊ä\Ê´¶Û¶03hšúέ۷nÝ^¯®®®/Ùy00""3G. 9¡ZÝÔäùÛßùÎ'Ÿ~öéÇ-ƒƒK‰R¢)Eúœb6æ³ÅñâðnÞ¸õøÑ‘ì*oŽœoÚÙRcVU¹yzòö׿uxãîß|øáfsM’´ë%ݶwäNN×ëUã†q_JA€­[7úÝnsq¹œ-V×ו§®ß^_¾xõöÓ“£óóóqº®#"*&†Æ¡9¹qëï~ãý÷Š`ÝÐíº]Iqs}™âè³xçæ³y7 Ý88ßü—ÿÕóWïýìÅóó7ßzɽúê+ï¿÷“íê’Q†~—òPÏÌ"šs&""B @CDç¼ã‚¯ëÆ $—œ¦”—ËÃùlþâ좉HÉ™™˜ˆ€M‘ˆ‘yèD bÇѧXÚvV×þùÙ3düÖw¾½¾^Ìáßÿ>>}öäý¿ý›R¤mÛ;wï¿öÚÛUh}ùÙÃÿúéã‡cןþƒøûÙÌWí+÷ÔíAŽeìöϾ|ôó÷ß»<2æQLÕ€‰HUµ@a0ÇÔTíÛ÷ã·ÿÞñÍÛBŒ¾ LL7WÏ.Ï>!ȯ½úæóç/þÍüÁÍãcïy»ÙÄ"¾j—'¿ù½ßþæ¯}ÿÑ‹§ÿëÿö?•~åÊhh"ÂLf6U™˜1’€‰ ‹è8ŽÖÔ-!Ã`ÞùºªCMÓón¿Ÿ€eßíÎΞìvçÉ Œi«k%D#-Zyï˜Õ(f¦"€‹vNL"ˆ!ï|UUóùl¿ßŘJΩdD&çI ùøôÖ«¯=xq~öÁ‡J.}×!Â;_{ëá/6]7Ï®igŽ}JùòüܪvξòUE¾öÕ¬]ž|ý[ßE‡¯¼8{¡¥0ÀbyØÔó"À ‰Ð9I}·Ú\=={z´<lŸ>üè¾ýõoýä'?ýÿ—º¾|* %OþŸÿ󖛊ɌÐÏçóÓ[wg‹â`àˆ½«ƒ!õc_´ s¨êj4•±‰7îÜš-Ú<öޏ–¬E¤d!"ç8¥ãH€uªªÇÑ T¥”¼ÛwŽyºìÑ{„ j`¦‹”Rr.X²¨X×uggçË““[·îÎçs0ñ|´Ù®rŒÞ{@Bª®‹MZŠU  !À´€˜ˆK)f¶X,‹åÍÛwº~ÿâì¨ÕU jD™‰Í”™RŒqì ¡©ÍÚÙ›·²”¾ïÏ//ET$zïóÅb¾ˆ1ívû¶mÛ¶evÞûÕjåœ{öôéùùùô|UUì úP5íìh¹œ5Õ¼mSÊ1%vŽ$#bǘEŠ:ïDtw"Q5³ézTJÊ1v)¥*DÀ ˧kBÃÕryûý¾r~¹8p)&@4ST5D4¤vqHUu伊Žã0Äñ‹Gg‹ÃÛßÿ;ß~ðF@xüäóógOž={z¹Ú\]­»ùÁiuz[½º.^\»ùš†Ú“÷\µ¯Ü{Ñwýnu}‰.³'g/è{ßýÆ£Åúb»ßnþÁÄyªíd»åá­:T¹”n쟞¯OÃP3›Qû­«mqT¶CÞ=~N ¾iOÚY‘\…¦ïú˜"±÷L„p|t †cŒ`®³Qñ`y šlÖ4õ¬í†®ëöW€™üÅ_þ‡õjã±üâg?l›Ù§Ào>xðêw?úèÛí5"4MÃÄÃ0‚"µRÀ1øJM‡a,%#pŽ©®ë¶/|=››‡JMœ÷Æì˜™LÀ Œ€PŬHð‘ýwßyûÝþðG¯¼òàwüñŸþÁ‹³9Å"ö£Ÿ|ðèéÕõÕUóÑéÍùü°[§mÿìÉÓëÝfõìéç#hTÉÝÐýä¯Þ«šæèäæv×µóVDc×¥>Þ¾}zãÆ¡™Å"˜s."Þ{BCGF ¨Ö¶›ÕÕºÛ²¯êÙ‘s‘S·¹~òè—Ïž~ÖTÍf½‡»½ã †š>ýp1ûðÃË«ÍnèâØ“ 2!¨ÙÔÒ̦ÆèÈMd…‘§êfQDnêÙÓWa¿]OåÜ÷ `pÉÏf‹Ùìàòê…;/ÎVW‚›ÍMÝ A)"Eœs€ÆfuÓ´M `¥denÚ¦ÏÛÅr>›€ÉzØïS))—ªªNNOëf^Uóã“ÃÃÃ!Dˆ`µwóƒù¢i bL¢!´ÍÜ9—RbšÙ·‹“Ó;Ë£õv¿XžœŸ_ܺuóko¼iZµ®‡\íû” †ºqĵwTÒ®[?~þ¤är|8sh¼^]/ƒwwûFßïÓ82"ªHO±ß?zôYÛ6óù’\ &¥äR4¥bÞU ÊαcßÔ9Š‚¦”QAÙqê¦iC¨s))G "ï<‰Èß)瘒H)%{ç½sŽØÀÀ€˜¼÷ÌœsžÈ»÷,"ªÀŒ€¢%§hn½Yu]7ÆQ¤ì÷Û*T³væƒSS5 320BâʇRÊKN„ªªÃ0ˆ¨s'¬œà˜ÝùâàÐy®«JU«ª²ªnšv쇫ËK5!$3“"„„ˆÎÑÁÁòøpyt0_¯ÖªÐÌÚÅÁ2T•©îv{¬m`³Y?{öl>ŸÇ«¦{-Rȹº®ç³ÙÉòðæÉIåy³]?qÞ£š1³™!""b)BÄ``fäÈÔÄŒÀÌ™¼÷R˜É«N½“˜”’ñ«€ª"€sNETÕ1ߺyûxyüo¼Ûõ},©”„Ädˆj˜ܸ}÷­·Þªêªïû8Ž9e®¼#hgÍéÑòµ¯½!q¼<¿Øìúÿ佟òåáWg‡7¶ëÍí“;9Ŷi£$ ç|3{ów^ý6£^>~rÀZ9Üý_{í[ïÜkjÞln\\w÷ïµáh‰æž?ëÖ«uíðøè ªÂõjsuuy0kѬzG! »GŸÿr·i‘ýfÅ”©ï(Áb61ö»áð öŽ>øåßzïJÉLN¥4¡mªºÛcì ™Øë÷__­¯?|ø±ÎëÑùâù ˜ÍÚàÜØo pØ÷íÙïýîïÿ¿ÿß¿ŠiDZ­ÛºªbNfÊŒ``¦*póæía///R,•¯ªPHùæ7¾9¤üñýwì–"/ñ]ÌÙ¹`úò¾c o¼ñÖÉÉÍùüàòêòÅ¿¸¼:€›Ç÷¾ýÍ×KâëËÇŸ½õàÁ·¿ó=d÷þûõðáû›õ“à$ç˜bl›ÊõÝêù‹|uyþ”½+’U‘‡qŒPUUNyâÑEÄ9>˜ÏrŠãÐ;ÂY;C:ÛþòoÄÌùÐ÷£€IÎ{³r 3û\¢Š½õö7ON~þÅ›wëùÁ׿ñîóçOßßD²28D#œæN¨|€ÓÀŠÌf¢Y ÄÁ ̹ðÝï|ïÓÏ>é÷{‘,"ˆBÅÝÉñ-˜Í1»_ûî·þýŸýùjµóªæÆ;òžK3Eš&,‡€„NDŒ™C]…ºòM]5­ósCN9ÆXT™Ýb±8XÌæ‡U}°ÛîþäOÿ(K®ªà‚—œPâjs%ZnÝ|e½Ýív»¢Zrq®Br伯ۓ›w¿öö·Þ|ókû¾kfsª¾ïMðdÅ`ˆiÕ÷Þ7 ¬T¹j^ q(stÁÍÚæòââúb÷úýWJ.¹èý{w@ïŒÃ½¡ïдä1ÇáúúÒ;ðžŽNêºâP‰²sÄèœÃI“bvŽ&@E´ë÷¥Qq„̬fí|1[,ª¢S !@.¹ÐWœQU'²ì½oª:ƘR$"ï3;Çf–sÊ9ƒ™iiêÆ˜˜HŠ"F•RÒpu‘DRI)EfSDd0DSõ!°YPÍ9‹™ˆÄs¥”RŠª9ç_jnÌ6 c×õ›Íîêò2Æ1§t°<(¥L‹6—œR4B(eÀm»»ÊßÍfm}zt°,¥¸ªrU5‘åãC $B ÁC¿^_#âÁb6QæA¤ÍÜ|¶XÌöýÞ{ž·mÝl7fÀγw®ªêª>çœcvÞÇ”ÀLAĈÀ¹‰þ¨Šªš˜LS*"Õu­j"ÅÔ‰0%5S&Š1Nw„ºnÿâù 0pÌ9ÃDÞÙU~¶Èä]{ÀÕ¼ºº™W3\û¸îGP1´Ë…Ÿµï|ã=¸›¯½ÜÚ]´~¿Û䜔 Iñ¡öm3dùü‹³Àü_å›÷Ú–Œ”¬=ÇGáðþÑÙ»Ìøìùª­hÖV º8¾sû•$7ž–3FŠÝþݯ½ùþÏþbÖÎ?þðÃ'>{ú‚!«èz³ï‡~½ß.—÷_»÷ãŸü¸ë7!4¯Üº‡À?øÞo|þù§ãîšQ¤üò ˆì+#Ï ÖÔM7ìÚº=9<÷û;ÐIp1:h—‡§×ë‹(û"ƒsT²Øf³U5"*IFéÀ#éöþ{¾®T³™•,ÓêbDAœ„1sÎ…P©ê8ŽˆúáG?ÿùÏ–‹Üºuc³Ù³#U}òøó8yØxÒ­“åÝ‹1&×§Y­9 dR(ys5×m‰XÔrN†S±àBÊE¤´m㈦&Ré`±Ø¬×HX…êæéM&^mvE2‚) yçTÅÈ™™=3sðd»Ýêähyöb÷'t¶º¾ÔÔ1#ï'Q«”—J¸ª‰"~Õ7¤€¹Y³(§©õâò|¿ß«;OÎ!RUUÎS~ëí¯¯ÖÛÇO;׸ïýÞžŸŸÿÍ/>Ç*“"£S;""B3PU Uð•Dv>ø¶©g•s›\†¾—RÈ9ï}Âl6;=>“~öŧCŽ¡jêzª¦ˆív›ÇgèÜz½Mb¹h.CÎã¬=ä0gtM={õî½û÷^ !0Œq”<:¢»l‚€ZÍâØ§"Ý…k¤àCÒ¨¹ß\ŸïWë~·úúƒ×ßyûkCLcLž!zöª2Æq³YñÅg_~ñÙÓl± Á‡¦ÙÈ9tˆˆìiR›À˜HKÉ9dfjÛ¶iª2 8uZU󃯑0KTpÌÓšäEG<ᵪL˜ŒqRŠª ˆ Rú¡›@ Dˆ‘˜ÁŒ™™YMBÛÎ|ŠfH("àÛºQUèaïT&¼ÃIùj©¨åR@Š É~»µª®š¦õÎŘº®Ûn¶"rt¼“ëëqÕD"ŠóÅœ™ëª6Ñ,…bloÞ@Â’r)fˆ¸]¯Æ¡Oãh`Î;DôŽT`ìû,e½ÝŸœÄœ6ÛµUã¼;9:éú.Æ8‘8Àé8Uº”™©jÉâ21¨*ñËÊP…‰§ª¦Jä½wˆpzzRUUß÷°ï÷cìW›«1¥,RLÞFC€ÉY2tÌ„9•ªªÀ´JJCîbÜíwû1¿õŸýúÅzòxëÖÍÃyS¿~—½§à}Å¢Ô%óìÚt{›ê99P$C,€×]ùÅ»/ÏÉ2kf*þKÝ€€H>4¡QÍû~=çŽ E¼“o¼ûµÏ>ùâÿ£øüï/ÎÎŽ"F–‡g—?üóÿð—?ùñ˜ãf}~óônêÙlþ“ŸþÅåÙ³ºvUfED *rÕ,N©j‹ZÉãéÍ[cÿ¼~ÿõ8t’(gÏ>û2g‘‘ˆÀÕ¿ûõ_ïý¿ˆÛŽˆÌÔ¹`V6›Íl6¿sóÖ“§_ @)iL£s>çñ;>ûìsD4³œ³ªVÞ{ïً͌ÍÁ×›õÚÀœó!fN±{þ|'jD*zøÙ{>”£B°gÏ?ùÃ?üÈà³Ã:„&x45”RÉÙ²š‘vÖ!‘Ë1YVBŠ)•RæóƒùbN›õUÁìäôÔÀ̇ÀÈóYSTÓi70SU@&fç™È9ç½ó¢ôÙ§Ÿ¨Y‡GŸü‚ ´D_GˆL“B@DDœFä‰LÓ5¨ˆŠœÞ><¼q¾~Ñ»}·þè£_LZ£Pð8°¯ùá§ïöÝv·•’܃o½ý[ç¿ýå£GÃjJݾ#$öŽˆyv„Ä¥"¹¦i|pFØ4óùlÞÖÕØwÃЉÊˆ¦Bˆ)¥ÝvóôÅÙj}yx||pxäÜ™»axöâ²ORÆTûpóîktñ¢Û­TETÑŒ(ÌëÙñl¾ß¬J‰ìˆLÕTLŠѦ®nª)¦®÷ý¨ÀÎWZÕÛÝf7TÆ~¿Þn×q»Ù5ëålqpLÁ”±ßo‡˜“lw»Í~·Ýoʼnf³1 yv¡BB3,Óõ%BP)ÙÌ@Eµn›ívÛ÷ƒ‘"EJL‘ãèªvÂM›´pÄ’,—<ÑÇìœ3S4Ѓƒyß÷9—IwË¥ˆ˜éÔMaÂ#›YD1„Z­.—³ÙìÑ“GÎyfŸrÊ9;ï™LÍbJ¹È¤ç;çØsÆLŽ™'ÕB¦±CÍ U…‰3( Z]×§§§Ëƒƒõj]‡j}uKVÕà˜‰¡ª^­¯.VWUG‡ÇL¬*Vt¿Y‰JŠ9Æd`8ÆXD,猙™™ A5Çý¦Œ1uìfu…D9¥ªj¼"eµZØn·C€”’ˆL€cDbREbfF"üꡪ1 ¢9çÔ,g!ÂI!Ĥ*1"‚s¼Ûí÷ûŽ]pCÛºA±¢Ù9ö\îªÈ1Ϫàƒ)Ó§—œ&JåUηí´Ä>i²àk¢¸\VM]ÓQxòülì.6ÉŸ¼ñúl>/è„ˆÔælL4Žýv¿~íþé`v±.7—®"0€mÒžl?øüj3&«¼‡’Dµ °‰]ÜRË{Ñ^À²šÕxóÖâ§?ýs„ìÈ4 ï~íÛß¾º¼xúôqN=’5u]ŸR¤„²é·«Mº{û6‚‡:B{|xÿÁ›ï0»Ÿ½÷ç›ëš¦qüë÷êüÖ¾ÿ³÷~f1)'DòΡ•Õæñþãd‰Ez3U5B"vm3!„ªB¤åÁAÛ¶gçç¦&}¡%©1šÙ0 ˆh6LºÙ8b?ôÓ­gæRBÛ4rvö,=::šÍÚ®ïs—¼g<3  ˆŠe3USDrlhHè<{ž11ÇÕ¦²"D$"«k0$âR$猈uUyçÚPƒ©Š€óÄH^X”Á\Ó4U]™¡¨)¡cçˆPDhrƒˆ}Î),Z-%ù:0 ³/ESJŽÙ1OÕ/M20@QïÜ[¾vï•·~ø´ÖjfPØQbF@3è†ÁRìS)CÜÇ1: úkßûÖg?ú“?þQÑ óˆˆÀR²ˆ’÷žE £>´MM}¿ïúŽ˜Øx‚ç¸Ûï®.¯×Û]S×MÕTu[µ‹Íf[ÖËÅááâ°P^´³ÃÃãÅ|þøóOÆ~«¥˜+óY{óôtÑVèp»¹vžgmÃ`RJÊ1¥4ŸÏˆˆ+ÄÍÐÅýN ÔL bcêIìvÃæSÔa?ì.Ç~ëª6§q·ßì÷ã8Ä1C‡~ ,LJJÉŒ«j±tUì=;f „„€Ž ªhÌ9rΕ1NRýØɹˆ(:ÇÄ̦¥¨BPÕ”u‚]v)•”rHÄΡ™""šª™"‚™B!„`fÄ#BŒc¨‚sÞrÎ’…‰Ú¦©ëf‡Õfm`ž™¥œ™9T!oEJ)Uå1—"ªÌ €%K]UU¨J.f(ªëõz†üÁbDEÊÕÕU˜™ˆ‡1¥ ˜™}&Ýí;fÒRÆ®žô³Yë8t]ÇìœóÀ/ñ·ª‚ª‚ª("ì¶3Í¥Ôu›ÒhªÎ;瀥”6ÃzRá'‘GDSÊ“Ò5›UHS6àÀ  ˆHÞñË!Æ9R‹1M~¬üªX)e³Ù”R²dY,—Áû¦j[âv6#Äý¾ŸÜ­¶ ƨdj¦ìÜWÙ'S!ç\@䪮8D8Zγ8öµƒ»Ç³¶uÛ¸‰n, šwCÿâìÉv»º|‚wŽ—¡úoÜíö²ú£÷>ûür·M¤p ¶s Ä•M ‘I= C!-Î…8ŠpÌïÿõOvëËÚ‘©€iÉñOþèß}øð“IftÛ¶._<\ÕÍ[o½í¼»X]˶ €P5ÍáòÞƒßþ­ïýîb±ðÿøOþ%YFÓýîºmê_~ðË”"Nø¨*äˆMãjýdjݪ€HH¸:>½µÛn¾xôÀDeL‘ð¥P jH„¹BQ!t¤V˜YÕ @r .k‘’cŠÈÇ"Bt\Š¢óј‰Ð‚wj3 ˜™©P™æ9"@TsÄ /ö G™yÂwfï}@40…º©ÍLJQU‘lj`h“aÆì¼H¤ hÆä'eIU‰ˆUÍ9¾sçîj}å<™éõõ•! €÷Ad,eRÞƒ™M*Í䚦”É9Dèúîêú<ÝíÖˆ`bŸívÝPFrP4øۮƒwm=`B 2—òptcþÝßøæg¿üô“ÇO2 Kp<}ƒéLe¡"È舧t‚–RTJ)Î9v‰Dd‡¶®ONÑ×f4 )„úèè¨e99<<Û]öûîé—_œÞ¼ÕÌf%  Æ޹©«ÝfÍq ïJ´¤{&È9É€ ªã˜Òn·[#Ûìw`h¦åj} ¹h[Bˆc·Z™¢Ñ!•®b7¦<öC7 cLÙú®suÓ,ç‹åÉòð[“¦­E¥¨:vl¦ÎyˆÙ$0C‚“½¢ªYÄDIJ"VKu·àù«ØŒ©•RÖëõ$™‰HÎy ü•¢¿Rrˆ˜È™Ó"BDÞ{DüªyP¹¸¸H97MC„“Ž¿<8\,®¯®º~ðÁïã8NKMż«\E9ËäËOT×{_UU]×Ó¯¥”$yL#" ê|6ctªbª„„ÌHH_%/µªêºnÈ&ñAL±ª*®ª ‰HÑ9´éÓí%:hß÷U˜ §áÓÏ>.Y‰œ™É¥$"˜†6"4…R²ªyç§êrÞ)‰¨8çóù|6ë‡aµ^{çë¦.EbL)¥ipy™2)øÕÚvÎU¡2Õ£† €º}?Ž#!–œ-P žÛ¥˜HF@3çÈÌADœc&R3$p> ú\TÊàIlSW·8szy½‹0Yw‡ ú VÒõóçãXJêÇQìÙu—KéÖçû–JÝÀ¡“o¬ÂÀÞؤÁTbÏ„u€MXŽoˆ†o<øàoׄbÄTyWîÜ}e;aĺۮ6ëÝÝWî{漢¦Q+91à½Û7^{õÞ8tÉÕ‹'X ³Œã€fŠŠ“´Xrö(T J¼SБĀÐÅlèj@6ÃýnÏΉˆ©©ª€Hˆlf¥df4ðè\Bλqb_TôÍw¾VJúòÑ—¥”< »¥Tu­ª)Ú"€šäRÀБͦ©ÔLM €¦¥/>©LbéKEÔ`Ê”Â4Q#z0#fà—½‚̤€‘!˜¾ädS¥«ª™¥×ë뺮îÝ»ÛwÝn»)’ §0×déMê+M¢_%}»v¾,RT£óá˧Ÿ»² ƘjWýîoþîOöÞ³ëgcî׫ë]·†þp¹Ì)×uë]ðnî8¸!ö¯¼ùÊoþ½ß¼øVÏ®W€Œ¤äÙ;¢—jÃ0 HUÓ6!Kqf0óÎ ªS¹N§'ªp°<¼ÿÍåÑñºOŸ|þ$†±öçÏ®×g»Íõ½›7Ƹýìá% z4tnŠ{KNŽfûõ†=#‡ÀUåꪧÿã IDATõžšÊTEs¿Ûu]÷£+YYS¿îû½Šìwûó³óE;«œ›zâa¿—’Äd”¼O9'“!—œÓûaXm6’³ˆ„,§wïÜ<~ãõ7K_<}¬ẽ’‰:&GH€V‚ÃPW¾ÄJqB4É)M¢ !š#“'D.%‹¨÷~Âe3n6!e-ÞyU%âI‘Ÿä—É}ýÕÊx™ôgV³œó¤üä\à«®;Æ8ÅÛçóù´À‘Ûo÷»ýÞ;çÙUU|˜ÞiÅL3áôxÙo˜C]9vûÝnbÉY@‘A–‹9¶u³ŸµÛ­«ª`)e"j›¶njª¡Ç”­òÁûÙ|.¥‚¯^£jªZL÷ö2T¡Óù†RJ¥ Á›™d!r)¥”’s „“”HDÆqŒ11ó|>Ÿ &SB\ÌçoÜ-ç¼ÛîL™â~rÎÓÉNžÇ”/žÆÐ©óMA…vÞÖ•Ï)«3›Z~ºb)I¨yÞÌæuÓ±ˆzf-fXU ™Å"ìÐ@‹II%K)U–Ëã'OŸ>y£@á@H)—qè¤Äš™ Ç8lûÍzµúôóg€©ÔPf&cîëÖk BUÏÑŒTƒCtÎ HIŒæÇn4¤Ùl‡®»yrüKC5fv¡9žÏ)¬·+)‰=©f¤ÀÇ'ÇL„`Ž9ç¬ÔôâüÙ~מÜ*ãæOÿí_>üèofµYÌ9’\b—ËCÌ9wCŸ±ÌÚ¹sÍ”È"¦œc,y©÷]¤ØñXH Ä¡Ç)Hg¦ª4D3«« A¡€ÅRØIÓ2“›/Ž®®6’óÁb~ëî+þòz·!CÏ.•”Š,Be f6É DHìÙ1gr$ž´ñIÐ6Uû#{ÙòÿîW3ûOÆ¿KÔ’šàWú¸âËÃNGÁidø;N 8 ®]·Ûlò‹çOÚYK„@s.""l/•÷¯–âÄEDd¹<úÖ·¾÷ɧuýÙZ– “6Æáèô@¡ìûý‹óg—/ÎSQdRÁóùáòȹÊi)ûý.CùÎo}o½íÿÕ¿ú¿sJÀ̉¼“ÿ$|‰f Ž BÛ¶)E‘ÌSåx_D†q„í¦A`ïCÓÎBå±Û¯¯ÏgÇ¢j€OŸ>£aØmÖ¢"*ˆ@ÁfrTrvžÛY3Æq·]c¼;::nÚqÈ)Šä¶©Ò°Ïò¬¶ëÍþjè{-¹83-ÅUuU7GÇÇ9§ë«aú]Ü_ï÷¹ $C50á’E¤0²”$©ñèa¿ºP)ý~‡€‡ÇG0_zµYu„¨Ã¾C“4öûÕêÙ—Ÿ3‚€ÂK0áÈl¶¤‹‹‹” "†IjÈ)8g¦1%ï~•BŸÄ÷—V§ˆ™!Ò¤Žã¸Ûí*œwXŠ:GÌŒH¦S€¶m§#˜YÉeßu¥d-¢¦9纩€c.¥ }Ÿrf&b"C3›r5ܰ*ˆ*{çØ‘cjªj¹\ªØäOKßKÉ"eZedDÈuÝê0ª†F-}ß01‹ ;r¾nê:x"03ïƒc×ýùÅ…”>Q)‚`@LŽ©”Ò÷ƒ÷Þ¿ŽÑ,©”YÛžœœì÷»O?ýlßuˆ¤¥ÄacRUœ8©2³s./Rú®ó!p!r8ošåá‘$Ým÷ZÕUUU¥΢¹È4¿:FE›<¡JQPAsއµäœ†Ýîj½º8<¬ªªîvýåõuVS¡œKÓ„:8±2VTÞ tÄÅʰ—¡«0%)Q9næF$%#`(»ÝÆò>0§à\œ‹ÈÑñIåÔŠ  å"–Ê?þÿàòjû£¿ú«ÐÖhesõċݽ{ttx:m´cæ"%—BÓ›2þËñÏD $Þ¹}³ M¼^ž¾s¥ðo¼ñøñã{÷î}üñÇPÙk¯Þ›Ï†>¡sEõìüyܯ ÐŒ|5¨™¦ívg‚–7šEµÚ>;;a*LJ§n·^ )v]÷ôþ~çï¿÷³GW/7‹LŒË€Í,x_…zò©˜iÚ©(E&…ALÇ”bNý8–¬mÝnVWŸ|øA»œ‘–±©šƒƒƒ£Å»ß|çïýèO;‡Î{d$rbK&À}·Þì®NOöývß­Et·Oc.GGÇŽ9Æ8ûœ‡û¡ëDLĆaŒC‡|¸qëvLe²MÌl»Þä”Ö›Õv»Þ±I MØyvtt|p}yQrï!œ=þtwñ¼ïú˜’æ†`z°<4ôÈó°]_‚© ÃPùÀD¢RDJÔy_Õgóƒ{¯¾j€çg/bLª…UU¤äœSS×0 ãKlåi‰€ª…àQ ˆœYS_™«J„fªb¥ˆ÷¾išªªÔl»Û‹¨2¡Ø$æO- §9BBP0S“ò’5´³–™†±««êÆÉI[×qH——ÃÐ{ï^Z¾b-rÎÕÍì`yDìÕÀ7)Cp<Ž}ß)ŽÄ~^T¡BæÓÓS °;7o˜I)Ù9'¥Ô'Ó1&f'¦cŒSD™êªǸ—Žˆ¼9ÇŽˆ*x¹Œ)‘®ëJçœwì½/¥|ùèË1F5UtÎ×U-f¢&jh*"å¥4Ï)%0ÞWu(’¼ ‹ÅÁááñ£/oÖëRJUUL4Œ1Ô@$E‡vÐÖæ8—ÌDž±­½gŠe4ËLȈMÓŒûnè6RbIcÓ„ýþ*e€Ÿï¶ÝÓ~·q¯×;br~뇔Rf¼÷¦eØ­ËárãÈ_½¯n«8v9íºÝ‘Vu= ÖÉ@äf''·" }õv%@4ÉS½¨É”œÀœ „ðr‹ åœ{G¤Z¦iûù‹gç—ç»ýŽ˜³9¥”¦¥šöqœÍfM¢{ì~íëßÿ½¿÷_üø/<Œãv{Ùwû£å‰Ë¹MšãÅáüx¶øî¯¿ûüßýY³¤¦ŸMý‡‰i‚$+%ÚÇqL1•‚D%æ©¨Šˆ”Òu¨íÖ±BÞsuˆˆiŒ•ƒ£““*ø—.¢« ¦”4Å”Ò'Á”˜¯¯W]×ùÚ¶-ZbJÛͺۭSLÓvÏ¢ÈèNOo®V«ëõæðä¸ÛíSJ¥äÓjuÙí7Ý®Q‹B’4Œ±˜°óUå¥Û¯»íZU̦p10!íWgã˜öû-˜,‹j6wuë™Ç±/%Xµ¢N­Þ,Æxyu…H'§§ÝnChf×Á7M=ã§ñ>”"1Ɖƒ#RU)e’/D•»yëfÎyß퀫‰22j–R,—RDr.Äì|U8>9Ùl6ûý^Jú¡iꪪڶ-¥ Ã0yŒ“eâ½Ç©[hñÞ˜ü'§7³Åjµ’bS®WD§('Ì‹Ã屯šºiÙy0Çqu‘ŒÙ³u¨êÙbQ5m;›˜ˆŠ´³9”Ü ]ÎÉ¢i)âCˆ)Åq,ªãÈ Í mfc¾¸¸œv½‡P5M=ý30Ì9ï¶²jWªÊäšÆ!2!ÕupÎ¥\ÀlJŽNÕÉD)¥IG®«ŠJ‘4¹ÎýÝwOþã~œÆ¡ÛïK.¢Z5m·ß}üá•Çã7Ìó`@ávu!9{¢’cNc]ÕËóÇO«*ì6Ûý°1Ë—— PÀP%Æ}욺6MãÐ&C±RâChú’*ëÍåõvX­×wnÞzòüœ W—×7NNbÌìÃÑÑqFÍQÐ ïÆqÌ’×›ÕñÉÁ‡GÇG›Ý*ÆÑ‡“ô] 5þÙÿíþþ?ùýø;w_¹ÿå§Ÿ?¸ÿö÷ãŸ~ö‹‹óëÕUÉ£XAv@ˆˆ ¦f)å‹ósD˜ò¦ù•4|qq9‘Qƒóó‹õzsrrc³Ý\¯v1M;«ê㪹ý;?ø'ÇÇÛþúÆÍ¹ÄýóÏ~y||²ßwí|é\4F·#¸ª]TUíUt/sÀ²NKÄÁ9ï}II$(Oæ›) ‚¢4#¤º®TrÊQ'a¨™X6 üj=¿Ì!¾Ì«˜MÛÈ¿¢èÓˆþÕ÷0›hú?Ã) œc40EöL&Ò6‰®ˆ¨¦ã8Náæù|>‰0"æ\¨*†az­˜r)Ûý™ÑyšµmÉe(ƒ‚•bf¦CN³à˃ëu›3¿ùΛ¯=¸ÿðˇ竚V$D솴žOOO˜¡aüηÞþèÃ?úò‚s¦2mS°I\šô\Çœcìº.Æ856hš¦mg]×i™¶æ`ɹöì|½h´HÎ%vzçUDLIpdæåò€=}öüÅsº{÷&]^\Îf‹W^¹ˆ>0£ˆïh6›y««UJ ‘¦mÖT)6Á‡×^»ÿ‹þvuyNÚë«K|Õ´}îqJ:ª(XûnŸâ¸Ëý®ßmTÔŠiUøÿ‰zӟɲä¼/–sÎ]òfæ»UU¯3ÍÙ9Ü$‘AH‚%Jþßü0`À´C– ›”DÒä EQÒôÌt÷ÌtWWW×þn¹Þ{Ïþp²F…B}(¼@nÞ<'"žçù…óŒÕ ã tj¹01¡yßvÝ šµF‡Dˆ 7Ϲ„\a¿?d‘®ï<f’æÉ¬°ÃÅbÁÌ%§RJ˧”èíD¸”LÄÕù×´mhUmº¶í;S)Ž©>çú~±cQ)š^^\„Ðìv{$ZŸŸK‘œó<ªºX,Öëõ<Ï5ËS±6µäPbcGfc<ì÷ì\×ufv8ÛЕ"}?l6{fíú3ÍóÀçççWŽ›Ê®0³’Ò~¿G•~Ñ·m?MGbšvX.üÏjSŸ?{FfEr*Ù9ׄà\hbBPà UÉ9«DU`öD„XT-§ì½W2öÞ;w9çTl»ÝMÓšÎ9Ï„}ß‹ª¥\«°Ú/Ÿ„,"ï[3E°išŠˆóaç/?þdœÒa¼UµT2ÌðìÉ“Ûû?ùýü‡>Z~ãÝÅǘþÝ¿ýÓûÝë\¢j.95MÓ…–Á!¢X@ÛvDTNXËSÓÅÐ¥ÄóSbW3¦`–¥Ä4‡ÐÌóК°~ôðÑjõ蓟þèòÑ;—WO¿øêãÿú·¿øä'â1òM·^®ÏWËåógÏRèÜjy¶Z-ï7¯ŒÅ:0pDE¥ˆ “c·Z.Sœ£‚‘WüàÝßûí<~òJ©»·q•”“!ï{$Œ)ÓŒHŽÙTÁl¹\žŸ_ä”"çÀašçãáˆý"”"αašw_üê1§æ8;v`¦EÑàúÍõ_ýÅ_^]=˜SÊQîï6>è‡NUJ)LÎ9oÄÄN49F"ËÓøâÙÎ9/qúêñç÷ׯn®_¿yùõ¯¾øTDûnPÃåÙùb±ô.´}g &²ÛNÛ»ÛÂbZÌöªbØLSŽ¢´¡¢-E 0;˜çè½+"®Ê.ªêœ©æœ¸ëº9ΦŠqŒÇc’,âGr€‰°m[35ÓRJ.YT¨ˆp)!1çäƒo».Î3ZJ  y–>„išbŒD,E yóæ 3Wj"–’sNÎñbèï7w¹äz7;vµ"À"ºÙmއcÎ¥§ž›§ññ“'›í~½^Ÿ_žÓ´ÙlÛ®1³’chZç‚s>Åùù×Oû}J1¦âØK)ì\×w9ç}Ó¶Õ—9ÛÍf:Œ›Û[3ñÞgUrŽ‘‰ð|µ¸º8Ãêïì K)‡©NA‹¨Gn»Þ1ï÷;@SÓ³ÔBìÔç!âxœ§qb&öžÙ‰$"òìcÎ*꽯" ‚¨)ÁûwÞ}ï7¾õãq|õâÅã_}^J™ŽÇàý) ¦6Ï“zÇ«õêx<ýb<ì_¿y3N{G@m£ìnÞ¼~öòYÛ÷—ç÷»ë¦ «³¥±Þoîß½z؆EÉùË'_mîï†Ï>ý´õÎ3.†aµZž¯×ÃòìùÍÓÃí>Ž%MùŸÿÓÿá믟ý‡¿þmCVJèœÛlmßãƒË˦ %Çíý].…}8Nñá»W¯®·ì®o7ã!_]^–œ™ßÞ¼äíÍŸÝ<ùö'Ž)Nãaœ÷]×4M›,úá|}Á„ó<¡"lîïÀ¤äbÀ9oj€puyé}øâ‹ÇI³œ”r« ýî°Û…@j6Oã›×_½~õô˯¿>»¼¸½½c‚¾ ÃТ¡?¿|÷£oÿჇ»ñoƤ"vvyùÝï}çÇ?þÿ‚'ÔRd`—WÎÎÏ_¾z5æ8Å죸˜]7\‰ANÌ31Ž)¦TBÓhÉ*©oƒˆ±J‰óhRP‹•䬘*€‘c“šÅ“‚eªÆÌ„l‹¾Ÿ§¹HQj#2³Óߌ°‚?XE4¸¦]\öÝÅÐ/ö‡ &-Â)µSC­¦ZR.%@Óôgg—ççg_|ñ¹ªÖšåäM`bç‰(—FÇÀÎ5jÕXŒhVRCj\zŸ,ýý'?>_­µÄn8TÄu!(ŒûHÓî°MI>øÆûï¾óðþ0KÎ@ì0qJñxDpäÛ;NR²jQkŸ¥”œÙ¹‹ós$œc4Õ¬)¥ ˆ 丈˜áƒ«w~ðýßù,‡í+FXŸ-ï7TØïж­¡Õ|phCÌù‹/?ýúÅ04†èˆ½s¡"SJ.ž±”)§Y$MÓ(EÐÀT;æûÍ™|ðé¸rû{e0¿¾@D |tyñþ£‹û7ÏÒ °q‰ˆ ‘ ‹ˆˆ¨«“‘@Á œó5‚T-Œˆ”Rö1; Á•RÒ<°ä(ZJ)9•g¬é§”2„rÉã8V•ÞZt  H1„,»®‘å°8??;›íVU—ëõj}¶ßïïïï  ¬]tªj*ªb¢u˜¶ÛíJÎmߥ”Ԍۮcv¢Òvónœ¦Ãþâ\¨äçx±\ô‹ÅçàûãñB ¶àCP»yóf¿ßwÞ»P™›ÍöpÛ®½¸¼Z ƒŠþì§?+) ‹ÅÉVUñ:¥PÕ,¯næÝa? ÝùÅÙÙÙÙ\òqœa>´¡ÞûàóõòïþîoU d̹€Auå<²÷M…ü˜Š!TQ«6•L<ªª©yçkXBUsLÌB³Uëºn–sœ¤†´;}4€Ë¦ Á«ÙþpøÖ‡]\]=þÕWO¾`D4‡4Ž@ i:N‡í˯Q4÷{ß9¿¼|ñüÕÂ-´¸¶[Üßî?ÿâ«Ë³«åbX_ž½ÿÎÃa1L)–¤«a¹»ÛýâñU¸XÏóq<î}UE_TŽÇ9œõ«õ:Kº½½7Âýn³ì®nnï^>ùìë¯ÇÝŸÿ_(\]]üñ¿ø—ýWys·ÙÌ#ˆX<äìcÏÌ)Ï9—þæoþöO>þo¡iš¦A°y xŸs2³¾ïÏÏÏÆqŒsVUM9«™¨œrqoãóø6´Q#Ä"y¿»I¥¬'é8ô¼è{"Ì)Ω`ð~ø­‡?˜SþÎÿñ1[ß8LûÃýõ¿ú§ÔxŸSQç cüæG?¸ÝîŽS\tii<;Ç9x¦ó´¿Ùî¹]õý¢LÇGçÝ¢Ãóruì¾úêiœŽZæO¿ÌãÖJÌ’‰NÕ;b!çœRvÞUë-xï;ß ‚ó^U*É#Î ìÈ,RúEçÙ²™9çÛÅrµzwÑå”z\S5©€jR´ Aíbê{n¦Þ{ç8çt:ßWôˆJ ®ëû“GÆýaSJ&çxžg-öíoý&úåãgϦ<_^uï¼óîÍõ뾡aȵŽs‰éÕ9DЇï½suy~8¾˜E M×u}ð 3©) ´}ﻦès1ÓR3û¦¹¾¾¾¹¹Q3TˆØ{O§@>«3¹‡ï¼ÿú壡ûÝM̉¹úY¤BÛõìy·Ûrä¦í†vZîïîîï7¦æÛnXžy± öÇ8ïKÇþ:êÈ{ò ˆ}S·w3a‰;œ?@êßy¶HŒ®œÄÌ ¤h¡Žä"xb¸Ð\t´mWgºj ErJhFfh`’£ª¤UJe)3ó<ÏUH1³å0t}×A7ÍsXN£F" !ØÉ‡ÃñÁƒ‡Ì´>»M7Å >4äx¹^Í)ÆÙ‘oÃÐv&2“‰@ý‡JÉûÃAÌúŢЗ«ÕZUU´í»³‹³ãt|òøKdôèTµ”Ô/VgçgLîúæÍn³1E3›çYUÛÎ5mB˜¦i¿ß«è0¬¼s"ê½3…¦ísÉgççgçç>øý~/"!0(%Wf,€yïD‰´2o‡¹ÌbZ…–¾ï]6p¾–«Ðöw÷›ãáèŒéóÇOúaÙ4]ß/Te¿ßÕS¦úú‰øüâsÙ›R.9ÆòâÅëÅb €»ÃáTüÖßfPŠ3X >ùs9TS@S@úÆGþñ¿þ—ï¾ûîÍͶŸÄ†Ö¦û7óqû­o~¤¥˜âË—¯ËEß÷¢ˆÁíŽùxŒ]Û9†à hY.‡ûû]ãÙ@§ãawœ‡õÕŃËÍÍö¬·eÇíAÌÍYÖËU.鸽çb]æJlšz‚›ˆä’J)ë³uÅt«*3UTã}ÎYTÁ ˆ03{Çì¹m;ïý<Ï!dnBˆÁ·¾Y,‡ï¼ûaŠó§ÿÓÏ%g¨@jR-* ( DľíépØ¿RJýªÖó½ÎlUÍ19vD¼è—ï½÷a.é‹Ç“ª”"¹dÍ9åX~ûàòý_>~ܯV~ø $ÙÞ½¾{õEžw®l¶Ôø”cœÔsëÓñáƒËÇŸ>ÅhìÀ¤ïÛ¶k¼ŠZÕåjx8Žc:Žu¶;ÎsmØš¦!â±"0;«B¶cMÓ~BÚî¯Sš./.}øúÙc3cçBpªäر#cb)–S2ïÝùÅQs{w[ñ‡`°^¯¯®:GÓq#‰Èi:+"9KQä½Ë"%•â !€é¼½Ç½bã|nÛT²¦¹qÁ jeYEr)c9Dh@¨'´.úš¶)%WËwˆY‹ÌÓXbŠbbæØ¥’ÔLUhD\rFçNo¿*13ž¼1r’ꑬê6f>„”²©­W+Ô\>ÿüsbgHª2NS‘BÓ6 "0hÉ9åisÓ4L¡ó»®[,úÅ0,Ã~¿¯Š™Áy_m$mÓXNZ³ÎŽŒÐ@Ä-ú¶d˜§yÇ”’oNþ­yžS΀´Z®Ú®¿½¹“ˆ†¶9?¿¸¼º\,YJÉ%8·è{)rˆQUL„Ø9DB B¼™ÅyÞl6õøV•:÷Ëq|ùâëýnç›A ˜ëÛ_D™0ÇŒªEMŠ$<ŽY¤4M[[0U«„&afÉEO@(³cÛ¶}ßÇ«?™“s´Xtª’µÜ¾~ä8ð~ÞûÙ¯‡~5ô»Ý]*Ñ¡y¶<O IDATçR–yž*ê™U-ŽcÙãŸ6þô“Ÿ"з¿ýý‡ß[t‹Ë³óW¯^¼ùòë'Ÿ:6ÆØ 1EQSœÍŒ?|ÿ›ßýÎLóõíõÃç7w·»ÝæWŸý|·ß„6H–Ÿ|úIžÒ;Þÿá«”¬–L__ß`7,ÚeŸâ|Øîç9Í1©Âj}îÛv¿Û¯WgLÔvM×/–ËóO?ýyÛ6‹¾+fšR§ñ'ìØùz¡lvÛÝaß¶-³® Û·lˆ:·¤“\‡JHHfRÄ©ëü°h=§Ï~þŸoo®s)1%4ë»F¤¼xöåZQf¢Såc5M$ªÇ¨ªTÔ˜ÐEÅÀ1—ŒŒì2#*Xå[¥”ˆwdªçÑ÷ 4È*D5ƉÈÄ¡iˆª¸JˆÌc”R¼÷•©¢ÊŽ+…CÔѳk]ðÎ!8صÇñþn®vÚàÁÇ8Î1ª#;©þDìÚ–™­’u™K)‹~áƒ?ŽÇûû[Sá*˜KA„’‹ˆïI]ç̘k#¢%Ë4ÍëÕúüü,ç¼ßCÓŸ_ÜÝÝMs¬[Kúa ïˆYRDÂa¹ŒÓ´¹Û¤47m`®äÍY\Þ»œsNeÑ·ÌŽ¨f•S*YhžŽr؉Šc 3+EªÂUëišNQÉ¿†ÖZ¾:‘Kut•à¡ð«¬æOœw5€-¢1¦É¤íšà=1Zιë;1œçh)OûÍîîúÕ4‘9‘”s)‚„Ž™œCÔ˜1²ðMó䜟þÉÇq>D´ýÍ_ýí\$+ä¾ïûýåååÝí›& ¼ÿ)¥®o^<{ÒÿêÕóšìÈÔTM¤¦òÕûþòüáÝíCeO)Å\æÐ8@ÐRDÔBXÿàøòÕ×ûÝ“”F5ó!(;Wƒšuuö][±qMÓüº@ykŸ…Ð4ÌœR};7@B5r¡[¬/b.«åbX´7×PÕTói£;Þn7Çñoýn%–Ò¬Î$4󮞹ˆõ«”ã â àx8Œóä˜/®.ÚÆ/‡ÅË—¯Ñ ­€“¢¥(šU§ÿÛ\w4ü5•àÔ}0;3ªä.ÔÓÏPËžyxºœ &çš®Ýî·P£"` &Ö¶Ýáp$¦¾ù­ßÿ'ô§ÿæ˹„²U5€œKÞ¹P£o/.®}§)„@Lf†ÄÞÑBH9ŽwãxøË¿üw¡i»&hNZJIsviS×¶÷ä÷ÓqމQÏ׫õj};M@•‘¥„œK™çùâò¢F°!x?5!0•çÜœÓ~KÝŽQŠ1v {gD>ø÷?øàûßý΋'?çÁ3ŠI9Á‡µÔûBh†ahÚ6å,b„8¶¿üô¿¡•®iãœ+FbžŽDà}Ó¶ë®ïÛΩæyÜ9&Ç”svb&‡„fó<–R@Àà­¯«®óÎPÓ„¦i™yšg-™È1{74+10"f"©àô¬c"Â*ÉÖk ¼ãŠKT@ˆ“´mBPµ_sJ)󇛛ë,„® ¤¦ª.øÅbሣ®ï™)Ætv~ɾÛm÷ÄTŠV4¦o›8ÏY¤~2vB–’”b`†Dž94ÍéúDôoy µ®¯g=!*3sÉšRÚïL¼\.œóÇãKI9‹ª¥˜EŇ`Ä H€çBcX˜Éy.’»¾Í9ÅÍÄ€ê1°X.Î.¯ÚaµÛïÛ»ªvÜ+"V¬ÔŽÅ{ïƒoV«aðhš–¯QáRÈÀéz50ÀÃË®Þûþ÷~﫯ž6 ݽyC MëErJ¹_a[!4€˜rDÀ?ü'ôïÿüÿ¾»U¤ haõLïº>üîïÿ£?þÕç?ý›}5O94Œˆ"ZÊéRøÖÓ§_(4ΙHÛ,RŒÓ4ªˆTS£jÌEœójšK%îÑqÕLÊ鎯;LEK°dªªi¿OEÚ®ÄãñøúõõjµL1ZÉ* DL TMµNŽLMÀ´V økö"*1* ›1!#(  ‘#b&5¬‹—êGZvu‡甚®ŸçISʎѳ[CÓ5„O?ùÙ~wHó4,Z·ww"R bºEëM ʺßïlÀa¹È%™ýÚЈÈÂä¼·zŒóÈP8 `)¥LÌM׺iÜwCg¶H%Ç4¦TBpëå²m‚#"S‰s”"@à*愱ääkéúv|UÕ<)ÂDxǺŒq9,ÏÏϸ s×= 9/—˾ïKN€Ø÷=3«á´ÝÇi¹ZõýB¤Ä ›àBŠ©ˆè)¶­M˜q·ß#Ò4Mª#Uƒ¹ccº¹¹Aµ1ÎÛÝv¿ß‹Ùr9Òfw0ÔÊbè×çg¾iÆqbòMÛxçæ9n6»”"LÓ¸è;ß´ªzyyÙ÷}].S!ŸsA¦Å0 #™iJñpïî‰ŠGDr\TH¤ž4€P"Ò4óþm™ oÙ5®>áJ€¨¿º®›ç¹äŒD·=Os~{¦‡¦ï«¯Ÿ>I)WO´saµèˆ)å9¥Ôµí·¾ûݳUÿŸþúÏ]ËDfâ=÷}—³C4@020y>¾9¾¹~cÈ5B„"Ä3™*"{ODMÓvý°è—Þ7ëõç©ïúÅåÙj½Þíﯯ_±k½cPË)™3÷÷?úëÏÎIIªižÀrŽ"YòDVãv΂ˆý×ÿúŸê"0iš‚_ ‹ãáXŠ¢A‘øøW?yxùÎ×_?–| ²÷3«¡k Á¯V« Îý5*µ”ò¶þµ®ë«"u8ìPĨ¹¿1PNùW¿üÅçÕªÙn6ìØ©×¢ÎySÎr ÷15H*jZçÌfP•­”u* ¦ ¦„`õËHÎÅ”æ8ƒ1Î77 ˆVTr­Ðé„;Uëƒ2„ÓŸÚ‹ˆhlêÍUú€ š:`S3ðÌD L@äHÅ4%‰ˆy1 ý0¤TTt5 }Û=ýúéñ8ž­ÏÐÔöû=#8ç08wvvYJÞîîÅD%âArbB¨*·©f*ŒÔw-3š©-{ÍIªªæ½«Ã3Fª8L5Õ,Å$"­‘T@ 2 >ˆ±õÌÌyç˜L €ðôÄCŒ”E™ÙÀéÞ2“Rì­8þðŒªJޤ!,ú¦yª \œ*Š˜ ˆÞÝÝÕ’Û¦ékDž§1§Ti'ŽÙÉÃ…SJÌ´ZMÓŽÇq»Ý©©Sʉ›¦=qR Ø»*‰‡ÐšÙ£äSªôÔ”â<Ï9å9Î]Ó¥$v˜œ}¿¨Çårµ ¡9ŽA‹°sDB0µã¿~úTÍš¦éÎVÌÜ4!¥œEÆã(Ešà/..êݰÝíSÊ!„š”R …Ÿ· ˆõíNà”{_óµZ¬ÀN"ªØÀ¢‚*Ž]¡ä !x$.Y*ýŒ‰ú®}pu HÌ®.ô>4Mà·Ó$æ`†¥Èjµ|ôî»h…œG„®kIEˆ€špF„Î{":Æ8ͦ0¿M´A•”M”ˆ‚w.4ì|ÛtðêÚh´ív×øþ·¾ÿ;sœ?ûÅ'9€ê;@S#"ç@IqÇ…Ù±”˜sÉ)3˜ ©‚šbVe$n›v³»>öö6Ç qNà™U”A-|öñ_\ß½-’£R²!UTFŒ±ºfÍ „@T™¾ìÜ\f":??›ç‘ˆbLs¼fòª<Ž39&§9Õo¾÷ ÀJ.Ù‡0Nq)¥˜eu9'SP *ÅŒÂÉy@¥$Wõ1AÃl¦$UõD`63)9¸ÀΨ¤¨†Uz1@05+ FuÅnÆÔ­–@¬JV·è¨’š7d¬8a d¢œŠ:‡€êP­iš¦ s̈$¥äœš¦S±³³³çËíÝÍá°¿¿»Ýl7)'ÏðÞ‡?úèýëÛgoÞܘ•RJ-3×]ÍõÛHToPšã¸Ým–Ë ª "UfRE 4€`"ºè—}ß•’ Ð{c"ÇÕT%皦ë5vlGK.ªfYꮣk#K*ÓþXDnÞ¼Iqfö•‰ŒVÉ Rªp…rG½¾é×—Þ»†ÕÓ×O~üã¿Yyë¸Â²ˆ™p5,cLRôüì P¡kšGŒÓ4O“‰ÄR2E38;{øáßlÛæG?þ«ñ¸sLÞŠ˜Ø”æ&aªÕD•¼QÑD †Æ¸ÄÇТ2¥’D‹š €÷ŽØ©B,¥29Né2D<ïÌTu¹Z~V)YÕŠ{_ãNuÛg*¥ñ©ä:œÄZ/“w¾ëzUÙ1Fkš Æzy¸Z¥Ú€Åªm¡ˆH±Êa9{Ä\ŠCòu©-1ÖìxÂk$±äœbjÚ¶nÛ¡u‚êÚF•¼?ÎÎÎSN!8fªÀýZŸ¬V«í~̹ÄyžÆ# }ï©ï9ÉDäœkÚ¶ß<ûÒh8¥˜söÞ9¹ËÌ̈2PÛ ‹nÅœ«=NK†\Òg¿øùaÞ›0àRœ©Hfb¦‚¥8¢÷ ˆ ÝЄ0Ž1©{݈YDÍ€ ¦qÜ·ÛÍöì쬲K‘”„ƒ÷"¹kÚð[¿óÍ÷>ú³¿øvóV´àÛ]cU†eÎéx<†ÐPJ¹ˆ®«ï~çûÿ_þsŒñɓǵbQCð..Å÷Çý·¾óý³ó‹¿ÿ/ÿEE_½¹ [M.e·»WÓ$YÈ ¢t;öb$ÊÆ¨jd`R„Á5¾1€Y²Jî.‚Ûív¥èªtšöJf` û«³3«-p.9çœ3ŽóœKI%ªž°¯u fhØ„¶izŒYAN­Kf²Ó„Â4äìÑ\pÉрЌµ"#"ˆäœ¾xùòëÇ¿†æÅ‹g¥( [#¾¾yþôù—9'3EÂê8ªÎÀz‡Õ eÄ|xï½÷¯¯o¶Ûí0 ÕùFf:ÇÉ_^žØ~¿/93‚æƒç8`nÇf¹è êoZ‘‘8«‹HRLméCbLo®o¥HŒy±q¿;T.9¢¥ëÐÐ{ßö=;·Ùîçyäf±h»Õbøæßø>|óò+aD3T%Òû»»ÛÛ»:×~óꙩ-ºÎ£+yž§Ãv»ÇiŽÑDxœÓÍÍõn·ëú@Æc2•à=©”ÃîÓl1gç;R‘ìàí$MO0h¬!e¬öÁ7¡a€œ«›†LêÔ ÐÌÌ1Ï¥Ô”rA4B)DS‰q¯f]×¢>çRŠ2£ ¡†šÁ¿Ý-WÿFÄLóv·K1Ö¡ ÍÓtZ e@„9ͦ†*mìœäx{=úÐvmϧ%aæ<7]˜æ,*_÷Y7Õ™‡ÅBT QEQU¥HƬ¦9•¦i™Irbº ;OGÍ%'P& ÞOÓ¤ª}ß«JH Mólª]Û.‡…öâo¯¯¯oî‹Áùcœç輟§øæúzžg)r¸Ûï÷„u™Šœ®=ï½÷È\;éç¦m½wV9õ)ædB˜ç¹†Æ½÷©¢Ø©&˜N§»*fǾ_ô¥~¢9K]+Zv›í4ŽWçËåbš(¥.Å4ŽÕ¢.øš$$¢ûÛ»çÏ_,ÚàCSí¢É‚oJÉ*2Ïc½œ˜‘T@›¦],zj/*m”ˆ˜sÑyžs3*¢jÙ;¯ÿçÿõ‰eºxpÖôgiÎCߤiwóúe¥ ÍÓ¼=ЬmšE¿þƒôG/^>¿¹ÿYÛºiŠÄèÀ<±ˆš]ß¼º¾m[Õâ½#ö¥èùÙù«—/Ь 0çøéŸÝïï…™‹"Žó8ÏsÂ?øÝß}ùò対xœRžçè]`böáý¿ñ“Ÿ}óTŠ02¹®œ[þáý‹ÍöþG?ú«ãqjû8ÎùÛßù±»½¿Ùãfw$3j»•RÓ·«BÁ ×C|ëB{w¿ùÆçëePSç]ãiѸEp¦¶ycš@”?úìÅ›ûóõ9ªÚÍ‹¦b*¹@Ó6ì‚sãñXöÇa±nšö’h{Ø“qŠÞµ«a šã6ç¹ñm×ö—çWEyÛ§yD3ÇoécšY¢äÉ,;20É% ˆ³rØÒ=z§i[5Åœüð·~øÙOÿóÅÅYMŠtýðÍo~´ÝÞîvwF!tj¢lªc*E²Q±·~Sf~ñâåxœ±*sU§<‰IìÎÎÎöû]J鄸Q%: ‹ªÖÝ^E…˜¼÷†HäS40kBcbì"ªhN)Î1åœrj»6„f±\9æ"¢Í”â”sVïC7,>zçÁ£w>ùäEjû!pðè°q #€O¾éŠHœæ”Sã9ïˆßïÝ7oÞüô㟕\ŠUà1‚Ýoo*®3£cçXŠv- }SDÆ©€8Ä)EÈPJ!À¼#VÍ*Š„%Í•ëößiÔÄÌNcŠ9´½÷-9ïo©ôXÑÞûR¤i»¦fäê’¿RÊn·#¢õrÕwmJ©…ˆ¸dÄc-=ΈÙí6û'ãt8ŒEËa?²sUrpÞÇ»®¦”Òñ8æT˜­ŠL.¦BCĵl7µ”“™ Šš‘s®ñMÓŒñÓb5£ËÌT·hVjÝàqò,ŸŠ4ÓSR1¦ªb33#sí´rL_>~\ „¿ŽešZÎz§¦íÚ¶•4ßÞÝç¡#öV »€¢ Ê;ï½ÏŽ~ö³cŽSœ´Hp Hò “ÛíµÛsÎ!R%v@$jXDÁÔT´íÙL§\ð½÷?øÖ÷~ožË«g_ã1Ú4ÑÞ8çǚ惇Üßßó1çx¿¹é†f·ß"ÒrÖÃ*óÝæþ8O]ð«åŠ›¦é—ËÚÃaüæG½~õÊLR)¯ï®‘i–ç©m|…vMó”RB³ŸÿôgûÃ1øà]³^‚:öûãÿû瞊º°b´xßùÐÝÝïŸ~ýlÜÍ%ßÞÝÞÜß„àÿá?üŸþÿ¦[4c²E?4‹رoßùð£»CÜíÝ0t]¯Ûö£ß8_†„s2ËÚ8j¶ìÂŃ¥P€w¾½þûŸ¾yýìÆT 4¯oï½oËáWÏ_OÇ£'Z-—‹ÅrŒ’öÛœc×ùWÖ—Íåå;ËE¿ßÝ}õå/€°V†Ô­ˆš³ðì›Ö¶ä/º^DÌÀ;>¶ã´+HÐ&3'™6F…r¿¹#t¸\ŸÕ Ô~ô#ÛÞ{`j¥mšw½ssý ½ó'¾‘²©¦Ũ>:‡dfž÷>¥IU½Þ5ˆ)¥TóD`"oÞ¼N)ÕB3ÆH0kÆÌmÛf× Cè:tŒDª†HLqŽ%eÇN‹Š 1åœ%jÓµÃbyqqˆã4Ó‘ˆ›À9—’ÙùxÚ9;ÇøâåónÑ­Î.×ëGCßæ4ç”W«Õxv–âÁ1:瑨kûõÙƒÝý橤Y?ÿâ—·÷·j¸jê’Û¦a¢" *Î1#:v~<Ž)&ÄÀ.ø¬•(©HE»4Þ‰Ô4§TÝp@ˆªE¤:DRÜ-«ŠÂT$ÏYÚ†›Ð{ÍyžbމOæ"°º†UÄšJ 0¨r+3')α-¥¤˜rɪVñ/ÿÝ“`ÈtÚîT÷ãüÿD½I³^Ùu¦·×Z»9Í×Ý@"‘™@vlDŠTJU’¬Š ;ÂöÌG8ü£<µ‡”‡XªJ%ÒE‘Å"%2[f‹ö¶_šÝ­åÁ¾Ha€@s¿sö^ë}Ÿ§ €ËÝÊ KIˆJ8ƒR>ø0Ž9%c ’.·%WYD°ÖMç mÜz³¹¾ºZ¯Ö1†ÍfKZï»N˜Û¦a–qbÂ/{³"p{ÒwÃàcªqH0ú!Œ!x/¨”ÑZ‰È¤’¦“1V”òãh´aQ«ínß÷)¦“R`´c…¤ÐZc¯àK IDATò>Ä”˜5q΄4™LÈhMš4/»Z«TÞd•›L_}åiüð“O}È®r¼6 ~½ÚùÔ­Ï ÑSW.Æ$ÅnÁ’8iMPZjR´Z¹ifDm E8Æ[”lŒ™X!Rñx[k°ëúâè# m$eï½45šš™E(³÷!|ùø«vÒìº>æ @P“¶Z²RŠP!ŠÎBF‹Q¨4)¤¬@iKäH[òãP·FkÂ`#å®ë//_hמÝ2ï»!]ÊÂÌÖ´m{øúÃo¹_þÕ_ÿån¿ÒÐÈn¿G?ŽCÛ´¢¤²®­ê¶®ëRL¬à¾ùíÇŸ/W/ÞÿíÎ59Çél6?\,Ÿ~úI§efPRh·Ì¼ÞlŒ±€ÚUÕááÑr¹"mh>?Ò¦uÕ|1?9?ÒuKQzßSUËÍF„¿ýíoþ»¿üÛÉ|>kWë- ± ˆ™ÌNÈ:tz²˜Û¶M»°ÜìÖÛíd:K!8ãn–7DÊ8#Á‡º¶ÎвnšºF99žÞ¬6ûÝx|vúúƒcCI±†´Þå«ÕöüüÜY»˜-"üüéÅ~»ì»ÕÅÍuLùƒ?â”Óάú+Ðú³çϘD¡±ŽY,‘±&gV®n´1Ô4,™ &• `ÓΆ É^qØef !$ά4[§} ÈüÅ_:çÀ¾_ýä§k4…àSND(Ôh”Í9‹d%r|xôèGŸ|ò± N¦SM”^,ÖšínCˆÌÉ~ô…ÒŒ„„ZD¥@ëÛ²K^»º®'­uˆP2Ç$™5Bßw9g-¨A[m­ñ!LÛÉÁâ m'™yðPk£gÔXµ ‹dÉZkuiâv:w®¶³iÛøà¿züx»Û¤¡ˆŠrdfëšWï¿®îÜÿè·¿ÜwÛ˜³¨ngÓÙq3™‡˜b ˆPПYP#êBˆuº¢œ•RÂQ+VÌ’’‚, ¦Ü¨(IÌÙgD5™L¦ B •5jºåò2s0ÎTõ ° uRˆ9p ÊÖv2MÙ1(4,é²êÁ2¢ÍÌꥰ-'A‚ÊU®®`À]êB ZkU¹Ê9Ç,]×½é*æÉð™QfÌ1•D.xrŽJ)g­&ò)c4‘•R¶"ÁûƘ¢(õæ;“ÃãS2´Ý®´¦œy·ïÀÞhSWõ0 M]CÉ)yŸ‹¾ Î%€ÕÚƒÖÚ¶i—W×Fë¶i ò¥¤Ú³pCL™4±RÚš(¹ëº,Éä”9²5¶<+cŠ9å‚„’§k­d•rR^| ew”RÊÌ1F ¼»8]Ì2'k\ßo_<{Ñíú¾¿v˜å\ -à¬ÕF3—õÏËŒ›0!é[¡ (€âÂeá—>$]•Ëël6›N§›ÍƇˆHåËœY±µ1 œœžô»íúêJòè} 1 ƒßm÷•«+h5QYÍX×´ÓƒqˆÎTEÏ%ŸG”Ñh&‹3õâü9‚ÖŽ“#ˆ÷‰¶»Þ‘öfG¯¦q')(á㳩1ö?ýç_ÍZ;Ÿ.4NëÅåõ%s– ÷_yõæúJ˜K›Ik¼ÿÑjµiªÊjëlÝ÷;™Ïç777šŒX" M(½1¦äÍY)ÎY£µÖG¯E“v aðéôƒïï‡ùWÿÏ¿?=:îºa¶XœÞ9ûö·¾ýþÿt~qyxrzÿþƒÏ¾øì‹¯žšª&D•Cûê¢}÷Ý» ³<œ..ç²Z­NOgc?ä”E™”Sç;@jjç,!í»5:Ãã°¿üäÅòfSƒ^èÃñÅó6ìÓ¥»VO&C¿3ãUw¾"K?ÔÊ+•PØ7KD1%SÝF#!KeˆšªŽ9uýØ8ÛÔõ~ðÝÐè4ŸØvbëÚ¶­k[g%p|hŸÆäC œQ €Ä”&SãœE@%JÆì•¨ºž¸j6 }åª9ä …ç# EAßprr¶ë»årå}H1ïv{kuNi2™n·ë~ DFÛ¦ïw·cgçªòDÍ9+€œbÊYÛÊYc*[¡B.¸DNDB”­5x{º"§©¶Æݶ«ê1„œY)ÐÚØºbóÈgt̉ˆXÄUn¾XÔÕ¬næ³ÙÜÛTîìììêò"ù}í0%Q„jèö/ž:¿¸pÎ&ªª*yYC:>Æ´º^Šˆµ6s>ÄX¿J È «ÊM¦“œSÎY#zïˤþë?á8ŽÛý®ë:mM3™¡ä¨”ÔuF/€MS+>FçiR(uãê¶òãè*Gýг0‘Î"JrUUš0¥¬¸Pú „>e~ñüb³\_ß,s9ëôýP$ãůMSt}ñd) 5Rº‰ˆ²Ö¤˜‡aøÚ¾ÆÌí¤98<4Ælw[íŒk*Þ®Y˜“”Ûn‰2„ŒTÓcP*WÍüÛßùþÇïÿæòÙ ºªLåÌdÚô}âí$ôVåƒ&cÅMæG½©?ûò+ BR"’hÐÚÜ»ÿÖè»BRl&mÛTýn‰*Š$Ÿå`qvvvo³óˆFÈCVë÷¾ûÝGo¼ñüÛÿ U:<¾ûʽÝ~‹ ÞüÆ»ç—/ºaïƒß­wVWZk MMÝjmú±«\}xx\Wuöñã/ÖëÕOò"Bd@Ê1eV µqª@3ã°Û픈­û°ëF±ºgOŸüî³Çà&£è·¾õýþð½ÿõùŸ÷»M×uOž~µZß¼ýλ7«åùõÒVŽŒÕ¦Ú\\ÿëï¼zgQ穼}vå~­% J¹¼œÿE³*óQæ ¥k€(¥Ê+Ì P!"Þ—,@Øû´Ú Óy]šÌºþ"aOZGp˺GD,XƒHcÌÛýh­48CDåËJÝþa³sˆ!¬‘¤œJrSq‹(£黾©[P€@sgó³óËó.ìA¡pFÔûnøàƒïÞ»×¶Ó‹Ë«sù´nöƒÕ”r 1¬¾õÎï]]_ŸGΘcÓ4½¹^¯÷»Mßu„À QÛªZÌ®ªCJD²D?ú±†=’ ’ˆÒ­ÕÖ 2Z+ÐÚ  £¢ÀÀ¶2ÆZHTnÓùìää.‘#¤Ù¬5hDd±˜Ÿžž={² 1+Éb´†q¿^[­s €èêV» ØzLÙ5?8›ÞQ ÇСÁ¤tÕLH¸Ò¨RN,bôc§$ïÖ[±[E@$‡0ìü¾ãœkçÚ¶fN»ívbC°¼Yî÷›Ýn¹Þ¬BLu»˜Ü;={íäè•£“ã“;'“ƒÓ6Y`GfåÀ ¦ð»ßþúG÷71Çb·&BT8ÇĤ)\´…„Zë2ñÍ)¥ë««}·/EÓ¢]½Í´Ü¶o”BB ²–ÜâõðÖKµ¾í©çûªº¤àýúfÕCÓÔD¨µÖUÕä”séÓ†PÎéÁû”"‘ƒ™™9+š´«œ±ZkÊ9u}†qßušXkµã8æ$ƘÙ|DÚYWW“éĽßîö»]ð·/-R€H™“F·8œO'S%JkÒcŠ]ßyï“ðáÁâøèøù³ççW913!"Jˆ7ËU_W¶ªTˆåŠSÚÅr_x/gg'qô/ÎÏ—7[­mù ˜P•˜üË¿rf}kÄD1v]Wâfݾßïú’Ü/ü©#!’Ö`b¶¶j*g²²o<¸÷ä«/OÏÞ|ýþòêâñã/«j: )甄t1v¢itµ vÆÎNïÚº¹"3”bÔn‚“ÅlvМ˜Ùéñüòòz:Ÿ_]]ØJÌúnœÎ_¹ïþ«wo®¯vûNÁiç½Ñæz?¼Ft÷GÝf­’ëW»Ýêñ—_’q§'w¼zÿ¿üùz³r¶zóá›,œRšÎ¦!&Y]*Œ¯½öÍÙlþ“¿ÿQeœÛn÷dtÛ¶Ä9fÅ¢ðv¾)G­5Œã˜sn§“ÈyìǦöøƒgç/Nîø'óù£wýô~Ôí®/¯j£ëï~ç÷þþ§?þò«/ÚéáñÑA?x•ã°_^\/õ{zï‡ÆÜ–I€^ª”n‰ìJ@¿ü®ÈíDSX” ¡Q¨nØ·ª<«Ë E©Û¬R•ÅÉV€ò/&>xÙSµ_«=¾þ!P ŠDpšª¬ž·V)E_ûZ¿ö~€€Hô£µmÝlý>sdaRcô~dΚ4}‘CT"c?€B»ñí7ß½{÷îõÕµ5Ž™CˆÓù¼­šèËó«˜SN ™e»ß¦œ6ÛmÎépÒ¾vïµíjw÷î+@òüùãcñ®hm‰FD ~Îúèø¸i%*‡cä$œÒz³Üí7œ+]pò%â @¥œB ¢‘´†+Π‘4ˆâ¶q»>VµqU=ú!†-"*É‹Åã“Ö‡€Æh”J¬39åÇùtn­ÝmiƒDºjíôh?*;9zó¿|râÈŠ³B$ÝX=­*Òû ƒ1ŒÁ’óÕÕÕ0zcõõÕy¿[ïv[å¬ÄœâôÐG?HŽºž>yñxèÖVãÑñÝ£ãûusP×sí*E˜•D–±ëµ±u;1Æ0OU ˜Ò¸Û$fc´©ŒbÊÌê–¤6ø11`ÝL sñeÔ …¹ï»qE1œ³ÖÚn¿ÇPæÌœ b´Ë6ƒ"@æ ¹„Ù9K†Š~ˆ•krV¤5dž1‡ˆ„1¥ÓÁâ('Ž!!’SLÞ#b;™¤|›¨%$tUUU.Ät}s£@i­±®\]å–ýdJÆàÉScmÊ9æ¬m' s 㸘ÏbLÅy6¨±ïzWYã b •«H ˆ’‚ˆMFÓÐï…sÓTC?ŽcÈ¡}ED\UMÚ©RÊCŽ)ç\ÖGŠ¥œAIU×t;Q9gFÒœ‚’[¡GŒÑ^)(è×[DŸïý0z@<88@,@|É„HŒL€ÖT¶ž¡™€©„4KÞã—OŸžÜ{õ{ßÿîŸÿð½Íõů~ýŸŸ>{^·í‹ç»>&¥IWâš¶šùÑ¿ÿé§wï>8:}uº8˜M³E}µ¼þõ¯ß?=9n*ì÷Rrvçäj³QŽŽæÇ‡M»xeñüz|â˜Þz󭜕©Ý¶ï´6¾ë_.ï¼öðòé­òÕv·¹Y>xøÖåùùó'_m·»”Äh×uÃë¯?¼¸ºbÉí´yúé‡ÛÝÎè°ÎY]“JZ«˜˜Œyý·á“ßþsêGcô×¹½²2Æ"Òf³-,\ŠFÆ¡ïúõ~Œß|÷{÷ï½öñçýô'suþäíGo¼úʃ/¿zrvrý8ië{÷îmwý8 Ôn·^îVû³_mC<:l\eû1’s¶j8§âÙÉÂàM’æóë뛋«õdÚTÖk!y?朔s•ˆª+76mkWËÍd2=\LSð!ÆS©‰!¦”ŒÖ¥Q¨”¨l¾%P©²­)(!MÄ")F£µ1•Òš #AXq’ëåö—¿z¯Ú("@9@N)—@‡±‰”(a•c¥èeþ"¥ÜNšín;ŽY‰1öøèl>ùÅ 8ÄE(ï}Ê·"æ DÝ8üíÿ÷EÁât¦†qpÎÕM]Ð)*çDDÎUº®kÉ’cL!ÄSäÝn·Z¯µÕœ$s¶ˆ ˜3ǘE| ­­‘KÚÑZíÃ(ÂM3±ÖDNÆ:ÃùåEŠ@_\,‚2úüüŶÛÖ•Kc*´å„ IW)²‘Œb“bt^ðàÑ7ˆÒN[@D2PÂ(ˆ¢¡Ó¤ëêÊ¡ïîÝíúê<ø¾Ò’N纪C‰³bv)ŒÛí øÑ—Ÿ.ž?ù ß©ê™¹=y£z…}¥ªÖµ5 (Å9ÇÒÖõ>Œqô¡·Ž|LxKûA$­Ä”X¤i]]Wãè_vüs.Ê*æü²}#ÌœS*,¡‚.ƒš”Òè½ j£ëºÆÛ 'gŽ)%… ²0g‚1†AáÁáál6c,[x¥Ô8ãà™•LX£#´PWAÂãããa6»ÝKÝ»P)Å”bVÚYS#DMÛviÇ̃Á9·8X,X2õÃØu} žˆêº!Äò@¢ªª¦³IÓVÕv½nëv:™lw»œÓv»Ùï;Dš-Ún¿ÆX«ç³–Bˆ9©?'"k\‰eæ2úÀBö`6V ç‹ósïCŒ©´¥ ]¢¬…SV¨¥PÒ¢|ËŠQD cfÈ9‹ø¯E¸)gVHºjçÓùÉÝûÞxëM´&¥ý€*!(àáâúºqú‡þ'à»ðäñ³_þúý÷?y¢Í”´1µ£ªÊ¨ÝÌYW‰Ñít~ïþë³i]µÆ4³ýÖÿ÷ÿí{wæ. ãr»¿\vë0™Wo½~x:«&NøÙþ¯ü[a9=;|øð®qºë¢&÷ìéMÞ¶ðýÇŸô«_þl¿ßU¯½ñú£7×››!Çv>‡÷ûþ£÷áÇjëÍòf{IÚ¶Õüîé[ÓÉ„4ø!Y­'Ó #|ï½÷>þèžNÚ¾ïËš@ – Æd ”ÿˆa€Ð9‹ ¯oÖǧ³ƒÅÁ8vëååjõ\ÉÈÜÆêÆ|ðáo}ŠëÕºë?ñ!.ÆqL’¤ëÝòãÇOÒ9²rQ™ ¤I/Õ“‘•BM!F­Xå§12£–ËXUÕ8xQ¢1Æ4m³Z®f3"„aÜJÎ~rVµs9ú1¥<޾·… 8籈TSŒ1'2ZD•ÇKƒ"¢¦i”R›ÍJ /&ŽcŽ# ·•!Q¸ÚÇ`›j>W(DÀ±Ð_•èû‘³j&3Ä\÷™“(´Ö•¬ `þõo~]×uÊ…ÀZˆ»}²4uݘ iÇý®ëˆYYm­«XXX² !çÏ¿¼N) d¥ÔÐûý.Ï9–Æ­5V£’0úÆ”bˆq³ë—›-’ö!+EåC•R>ç hB" ,÷Bª¬á®ïK«(ådœS1&Pj»Û#šé´ASUu»ÛoW뛣IÍH"*¦\@1gMÈTÍtz¨L½8:}øÆÃfRïw«ª©8Q €sf@Q*¤ÔX×Ôu×+•9G%iVëïüñ|õäéz··®¶Îi­½´RFӳϮ.Î'óãɶk*·8<…Ql[×Ó‰v¶è¢Êƒ˜UiZd[†ÌKè3k­Ë¿Øn·ó>”'T™Z[ë4™CO9>ÜBk8ç|>ŪªŒ±y“x_)VFka!ÒDh´eN1P|>[kgÓºªje¬É|{{U 8+%0Ÿ>»¤”ʹÕZ›sÊœú¾/`ƒB¬,~HpÎh­sÎ]ßÒÆZSU¥¹ªt˜´³iˆ>g&mÆÑï»>Æ(œµm[€Õæàð@PMíNŽ—WË~ð×7×ÏŸ=‰ Heu¼6Û¦ª«ºjšúèèP‰Zm¶, Û†ˆˆ0¥ä½O)©\>= bŠ1‘1º\+]‘Th­SJ¥d÷RCÈZk4ôu`é¶¡†ÚÃ>”½µV))åƒÛyÎHÚÕ h]M]W>F…h¬«\+ÀÁÃÀã8 i½üçþåÏQ[:=;[®VÏž=ÓÆ4US*§Ú# JŠ!$"ƒÎ¶o¿þÍ?þ“ó³üÙ§ŸܶÕÉÑÉgŸ~ðtد×ׂ¸ßíOkWï÷×Ô»íM;µµ½I·µIL‘)C É5÷îÞk\5zïSÔZï–Ëf67†Þû˜ÛvÂÌUídb+Wªü‡g÷”p]׳ã³ýn[Åè}˜M&Ι}×y?4JPAN)„xp°€œ%Æ D…´&çjÂ~¿«‹1Vu5™Ì¬qe[c»cõ°½~üéÇÃ*ˆX[e«ÄFgM:kôž‰3rF/¢>ü† ]·»,Í#f&$«R F‘Î9•ã¶ÏŸ?#21ædéþ«oˆðã'Ÿdµq„)ëÝw>úøÃ~§ÓYÊ „ÞwJÒÐw/Fß4Í8ŽRŒÊ)±»>ŒcJ!æ°ÝïnÖû>¦e?A)¤ÒÅȉÇ1Z‡Ö€1äHà˜…Ó™p aT,)¥n¿'"W¹ª®Ûét>™äÄý‘êª~÷oþìødsù\¢3– §Uˆ>„²ÏbYé“û¯ß8_ÌÑB¶D’O±“$sN9å”%‡Φ$ (g¿ÛÞ ëË Ê¿uØ6“æ0‹2ÆicÆ-é~è§mÓ7Õ¤mOŽNŠSÉǘEU•#£Šz ˜%Ũ¤<, KŽ)Àm¸YrN)ÄXS%À8Ž5’s®œ‰¨dT5骪ÊÛ» joo¾ÖÙ”Êd3¥ä}@$M:ÇCLZkÂÌY :k$ Ž1ÆØ)m;ÕÆ,Wë2[H)!ât:%¢”Øçê*qæÛðåËD& }o¬-^ K:çìcTJUµµÖŠb?Œaô1DABmmÕ4“ùÌÕΣaùâr½ÝjãjW§œ¶›m·ßâb¶àÙt>Ÿ*&MÛÝî“O>GÏ™ ЭQ>x‰p0¾m+æd-MgM?ôýKÑ¿xEJÿ–YJE Ë=ƒ¨¢[„giŸc*¹#–²Û`æòbuë‰Q9sJ©ªÜ|>‡Ý¾ë{cÌÑÑ‘sîòòb»Ý"Ѝ[ìm]×Í´ÌݰKD)C))EF¡R(¡bŒÝªëŒk¿óýïx4ÛQ½z÷p1oÚf~8O)…˜|¥]ÓÀÉlš IYD1€‡ÕjX.wÝ×[cãÖ_¯BNA+ÖÀÙ÷mÓˆÐ8 ½ù³?ûó›õþ£O>hLFÅ)tÖÚÓó‹Ç7Ë«ËëKPêdúèðèìóÏ?Þí×_=+çꦆÑUî nûëõÝ£ÓaÜo¶Û®ëŒ±®róùBD…àwûm)µ!(šLe룃£WîÞ/GÇǯßõÝ·Þì6›g/¾bÙ5u«v¶zïßûñ²Ý¬«¦ÖD~ôJ©ÆYœrÒdzå•WêºQ,íd‚C?zŸ£ßï·}?N&“ã“S Á§>'N”I„‰4uýÈ–YÛªª&Ö†Ò[†À M;£ “Åœªªï:CšA‘ÑM=)ÔoƒnBÕv³É'G®m…­Š)&¥›Ù‘ÓˆDYéUfVeZ% 1ÎÓu"ƒÅ,Ö¹WîÞ}þåï2seP)f"¼%W~ ž!2uS¥”ÇBˆJ 9-š¦¾ºy>z1Æ C—¼?¿¾ÚtÝbqø'üg¿z|³¾~rþ¸Ûõ“¦ùÁþhµ\}öÙ§d¨®ë˜SŒƒû>§¤€cˆû}?ôc7¤§7Aˆ€±r5jS$mR&Ùl6"8™Î4i2†ˆ¼!$±:Oš†c+F£1Õáâ€óR"¥M…ñC¤ëfj]5úbÈÌDJeQÖUwNïNê (Å4€âTÒþýv#¢¬6¨” ã~·öÑmMŒ²ß¯×ëóquñêÁbu<Ÿž)ërY0Ç~bÊ9Œû4îIekH)ð~}THÉh¥r1¹ÜÒ³p’"ÑKB¥ø®¡˜31¤ËµfæàÃ|Þ¼<çÌ™€¨èý²{.—5lëZ2ïû.„(¢ˆtŽ©ð ¨Dé5ŒãBŠ1(@k]U7Úè1„ SƆdŒ)D-mLI=N&m!Â眬³Ö¹œ“ǾïÊ= ‹—\e'íô]÷ä«Ç)•™†÷cJ)¥´ÙïD*fUzR!µZ®´Æébþüù‹?øðúòFXµm Â’9¢Ó΃$)ñv³%Tí¤­œëºÑû €¥„šŒ.¾M$dÖ…8Xše)¥ÒÓZ—DpÔ䯩dY˜“fΙ·»½^),iˆªrwîÜǾë:•E¡… TŒ^TžNZktLA†œbÌue ÆR›†Œ‰ã>džÌç³Å¡­f?øá.nv9›MP<ŸÕÕÙ©›,ì“§[!ÞìÒ?ýbùèµijì6Žö¨=¬-s2¿ÿùæ²ç§/^ì÷û¦n”j"Á2¨ÄqHa8‡n ®[·†œ3'''ôÃ>~þâF¥cªœf~õOÿi»Ý‚R“úè¿þ¯þ»?øþŸýå_þßówÿ×â8†0Ü÷þ“/ϧÓ¼÷Þ‹çÏg³ö£O>}ò>"¢°¬V«·ß~wú/¦“é|6å<Ýn· dL•2ÿâ?×®Ùwë>¹$ƒ IDAT“ããf2NãÛÉÌZÝÔ“åúæâúêgÿø Q#sc.ˆ§ ´­­­´¶1zf}C<<9Ýw½©qvx„Ö ­#B%ÊX×—0%ê˜UâdLc5sv¹\z„º "MÝ £6u½Y­š¶mÚV„ 1%!dôf¿W ’¨«Õ²ªLÃr¹tÎè[ØAf‘”Xœ&†B–”¢sN¥cÎÓtf­3~õü©3 H[ëD”BJ‚„šŒE@ QWM¥u•b ã~·c€v2]mÖïôÛõn}vçx¿Ý²jª/ÏŸƒ1öÿúoÿž°úâ«Oc³äÃÇ}ÄÂJ•ã€Ò¹ØˆRìúÁQeH]¸ºXÆÛ(…%M,Ì9eaÈL$/—È!†aü”u¹(I9ú°^­b”b!(Ìœqºn+¬³#UÕbqtJ?ûì#á8*¥Æº©gó©RÉ{4 (e 8G8ìü0F2Ú‡aè6ý8 ³d5¡ï6~Ø™ƒFßí×&2`ŒÙýf}E‘âòòÅææf<)@¤ŒÖFu»®'´†8瘙H›b&lÛºðø­³1'$[YGˆœX‰€‚œRJ±0RÊ?•6Z ã D‰z‰Weæ_LŸ=ûj½¾º™T_>ýâw¿ûd·ß·íôäø,ßu{?z6üé§Ÿt}oœC£»¡÷ƒÐÓéÁ£Gßüèã/<|àjûé?~´Ù­Þ~çíß}ò›nܹÊýéÿ‹¦™ýû¿þwœw«Õº°É´ˆJ,å?¥LEiEä£7†äÑ~Œ®®€ÐÖÕáé)çL€"ʧV«&˜Ïç!Dk¶V‡Pø?ŠÙj“AåÈÑÇ£©µ5ÅÿœH£®ªÊžž¯™(¤´Tôçóöàh–2]ßwkB€aô|ô#šŠÈÖÎX¥JŒÇE µ«ÙÉ÷BH9ˆD¢šúad!k¦,’&•`ô>³9iç¹[…> ‚kšLêüæ:"ÌNÏ"a2N7Sk äÌ)þî‹/—ËÝï>þ`ݭDZoÚÖ¢Þn·¨1-zÇ  ï]Î>1sV¢—Ýj¸¹ÞfÀ  PKA”³0h"â‚Çn· ÞÒJ“R,Jåýf–‰&Óùt2KbsfAõþïŸ_\ÌšªØ­i¿ñîï &¥ÒÅù“Õr‰ˆÚ$"ÂÊU 93B·[õy<:8¨ MeöÛí~Û ¡†Á‡!Œ#‚¶Z;‰! 9ŔڛõrT(¨ÑºÎû«Ë¨²°ì¶»¡ëúý&ôû#‘5¦Q¦Ö„TY2š¨"™Æ)TQ! ¨Ä¹  11J cðãdZ%¢µV Løå°X›R¨V9¥œ™…Û¶ÕÚ|-33Æ+¥–ëµ±&çLF7M]×µR@Áû̹ï{=BE¢TJ©FSé¾–qDY ”ò|±XÞ¬ŽC}¢iL9ARDÿº/éÌ25P…Ð-)[¢Æ¹ùtvõâ Œš´c^"ý„ýÐm7Ýf½aNY„5BÖ©®kµXÌ'“ Àl6YÝ,‡¾Ï);çÊ 5eΜcñ‹âÙÑüäø¨œ®cŠÅ˜P^c%E JiÒ…Ä„Då#-"!„‚#‚B€àœË~U*ùÈò“A óoŸïJ­ëº"Mý~ÿÕ_äœååà©  å”ô­Òè®D‚Dk sÊ„¤0v]ŒŒ9Êvuþ”‡®ïÇcŠILΠTèbŸ½ÿüÅþoèçßúö7ž_®o®–Û¡kŒN9§ØA`+˜µ6J"hmIÁíßžŒÑHAŽÙjuïîÙÓÏ?4®òcB°ˆm3ûÖ7¾÷ÁïŸ_\üòŸú³_üýâpºV)ÅG¯?T9Õþâò¹­h³]‡ÄóÃÃããã/¾üôðð°†B? fæ®ÛK–£ÃcgmS»?~ï{ßüÖw®6ÛG^ÿÙ?>¿º|~ÿ•×ÎNïø0¾xþüµ®­§WÚVŹU'9KJ*‘ ©DÓ¦i›6d¥8+ÒÚ)$<>8$["Úï¶Î:Ò@Åe:š‘5‰ŒÕÚXWm·[æ¨ ˆYªª Ņ̃Ómw;D,_~ÊÜJx4 °ôF몲xp0_LŒFgµÄ|ÿÕWŒ¡ëÕæüüü`:­êz~xªU&1à T#³2HÓé|¾Íc¤ôéçŸN48±*° Ô´¯4í!S´ ả˜5äó›çì·ãÐUÍôíoýá“« •sÎ6.øpl§ÓÚZ¿ïc·ýà³gµ¹0*(PÆØwÞ|«ïú«Ë«2產&SÐìGˆœ$„¼ëâÅrÛVZ@3Îó”8'N9#)H¤EX1çœâ8”®|NbŒF£µ&ÉœBˆ1P¡pdöaìú^¡~óÍoüj~â‡)(~Ü?ùêQI’ŸM&ëÕŠsV,)øa¿óuk+CŠ£ÙÐÕfßmwMUçcô)FI%È*fÒ¬rAj——ëÍn«ªM;?ìvk•ݰ^®ü° Ã^8kkE@+£+ ÎH³÷`5¶™Nb(JL(›n¼ rбˆ±IFÉŠAS¿¥EYTÎ¥›*ŠE1€ÒVsfNRŽ“1Æ«««£36ÆÄŠ5 €jšf2™öý~¿Ûõ}BPªìlKЋ֛µÖ9W‚"ÅU@+Ú˜Ãã£}ßï‡^8—€kŒ tqE QN9¥DtK-®«j6™œžõý`´6šNOŽŸ=~–R‡!HØ÷ýÙéñÁ|îÇA£YLcCßKf‘œgóùñÉñt6ÉœBð!ÅÙl–côã˜s¹àHÊY3‘"dNÂ9sTJ&u]ä«9gmoo?ºT(™‹RYÊÙüë§¶ÖfÆœ“1¶d`²|-W¹õµÞžñ•îX!M¦C).$¤ÂÂE¸µ(hÒôò9úR«„³­Q5uã4¦Ñç ‚Xkˆ%úý8FMD»n4šÑ „^D­r­³–”¢2 ÄÕz÷ÉçO@cènx''Ç»ý âÛzž4ˆÕ¤„ÀZºu†éÄÌJ•¨é8vP9x4Ÿ9W¥”BˆÆXfFÔÛݰ\m·ûͮ߆˜ÖC; ÝÙéÑ믾òâù‹Wîݽº9/Ò Rùæfù³ÕÏ­¦ïþþïÿú׿*=/T‚$ …"ÇœÃõÕóõjù?þÿÓßÿøo"jFW™£Åáïç{WWW×ËË÷?øÀióú§»¿ÚMó¼÷£ÖšŒEºÎX‰£v¾¸ûêëÓyÛLª¾Ë,\U¦ï²«µŒDÖ"À”PgæªÂ;wζ۽ֺi›³RŠt®*“s‘˜"iKš\ÝÔ̶nsý0’5P @Î9g%JΦWg)%D5™ÚiÓ²ävf«é4¦|z|\Yë\µYÞô»U½¥R ÊJ!t~ßh †a@4³ÙI­™@I¤íâäþÿÕytöÚùj5=ši€aðçç/N?ÿôýOVç>e´õÁñ½{¯}#W»·¿ñûŒ’ÞÏ&M†®/.¾üìw¬ë~·ä¸f@¥ ÛíETÓ¶ÁûÆ’*;]àJc}?tûáj¹y|qíÚI£m]O&“Ùv»sàë Ú¦1%%/,lŒÖu]÷}ˆcŸ,rÈ™AJJ‘UÊ–9£2‡‡gLJw//bI__=]¯žµÓºrUÊ e¸%œâÐwK?uÑK ”d•8ú±V—ç¹|Žˆ4Šêw»˜<¶M a³Û$ŸÛvZÕ“ùt²ï¶»Ðe‘½ûa > Fã´í}Ý^ ÇÑ뙺™I±¿¹Š„uÓ@ ¢6$îô8æä‡>Œcö½ÿÛÏ>ý,ÆQ8'É9%Î’RâìꪜÍ3gQ* —‘7ˆ"ÂrjÎ9Ç‹{Þûé6ÜZúÀ‰³dÉÂ}ßO&€‰pöÁûq Bk´±Ö9—™GRf%k2Hä}Š1“pÊq¿ßµM“3]‹’±DrÓLªºîû¢0vbG,ëYf6FÏ&“‡o¼ñäñSïý½;gÃàç³IÊ<ŽÃ°SÎÈüÚk¯žŸ6õÐõ¾Ši>³–n0 cŒaz0Âû÷ïŸVÎ-×@ê‡QÆQÝÔ(QÌM到mêÁ~ûý¨”*ÖËÒ,Tw²˜[YcLa}ŒIkS^oåH^ªÂ9ç(Šs6Τ”Ë’V±"b9s VUëœa–TÞ‹Æð­ÙîÖmÓ¤9§òfÕF‹°ÖD’²b&D¥‘sÔ*KÚÇâ致A­Q£»Üçnsg«½ärR1hbmØÇq³Û ˆï×ýn÷l¿âœ´1„Vç“F­Pr]µ*£áÅóÇ–ý+gÓIÛÚjjۛÅbΙ+B Îr8?úƒïýá_ÿí¿W(D²Y¯Ü»ûÞ÷¿ëÃpssÞ(ǘ“«Lm›aÜÿèG?*_x0?¾÷Õ˜Ò®[?}þdcŒ“ÿßÿÏÛN¦¯?|ôúGË«çóYóúë÷·»m·ü0Ÿ|û[ßýëÿðwÖ¸“ã;ç—ÏSÌÊ^)m\kª™sÕƒ7^¿sï(©Œ$ˆ8 c 1ûà §ª\)Ü…”˜ÊU2QΚœB +%ÓiSØr!&P*Ĉ¤ON‘Ô09%ƒd >± ˆ¨œ…IÈ•ÖÞ‡œR £6ÆXÍ, øäx~ïÞ"GÕÖoùá•åÍÕÅåÍf³ ~``É@Œ8§´éöUåÚɼ©šƒƒ³ä;ŸR@ÂvRWµ>>ZÜyíþ.Â|åák$Ån}ñ)‘B­.·a?¤õçÞ43)ŠdaAcìlvØLþé7&Û*¬EBã°Y­ëÉ´$°GŸË©¤dªµ&Í) ã°Ünn–«õzׇ ‰´©êb ½Oœ7=9^´³i·ßN¥ Pkµ)€rgmJyµ\ÇÿŸ´7ý‘4ËÎûÎ9wy·Xr­½ººz™NÏPÃ69¤IQ!J†a°aû¯²à,0 Á ²Y´Åæ"Q‡œ½×é®®®5³rÏŒåÝîvŽ?ܬšR7‡_…²2£"ož{ï9ÏóübL¥åÃtjÈÔUaµ" Á‡èá¹Z!qb«„A™¶уsý|Zk,†¡ËÆ3­Tð¡÷½€¤FçÚvåüpr|J„1†<¢$}³ª«ÕòüììÄÅ(ˆ)±wчD¤ãèº6ïß/¹Š’‘¼0%£ ˜*©"qÒF)‚³ósç<§àÆèÚR$ðž4sJ’бÙ9¡›¦fáaµ2HØu].|Ì9û“„%ëÚCˆ¢!«º±1¦a$+œú¾wã8 ƒ5fÚÔˆ …-«JÓ}ŒàœÆ‘bp€ šÌ›«Û!ÄÓÓóf:­µRЧӡoE„Cˆ…-laËŠ¼s1¦œJZÅjµ†ïÝûäÙþaßww_{íÙ³ý®ï@ k»³Óóélæ}LÌ)%"•iÎ0º±ëÛa¼ó?6…¾qíÚÕ²,›Úkmçó™0 p±°f:™(­•V„øøÉÞ༰ æ´wfa‰!«µÊ¹¼œ-è}È©ÂYr0Ëóà~@A¢Ë<÷q›ÙÔZÓ45§tøì`:*ç}nA¾Ï]F‹2‡Ñ+kSΪ2–AÄcS¦nCSš€&SÁ4ºQ)E ÉЮUY˜ªÔ¦ô}'ÂìýâbQ•åæt¢4 Ѫí2)y½^3a::|æü¢2óÙ–11FY,£óM3K Ã0äΛ~h*•áÓ“ÃÚ”W¶®.ÎN‰ˆÐ°2ŠTJUUÞ¾óÊr}q|z¬q“ºÔ¨Ÿ<{ÜöÝjµÜÚÚ2ÖînlžVfúÿá?þèÃ÷?øä§ ²§ZWEýꫯ=;|vr~äC¨›iYÑjÕnlmu]gµúèÃ÷ÏÎŽº¾ûþÙÿ_ý7ÿíÝWo¯—'ï¼óGï½ûÞÙÅéö•Ý+W®>Ù{„ Œ2JiE¦*¦×¶v§ýʆծ[w}× h£*ë¦ÐŠ!ÖˆÀ0ôZ+¥•÷1§4µ±·Ú«)È>O­‘ïýЇ=R0·¯l,—KÄ´1¯ÎÎ{¼Ö&¥°9ŸÅ1ÄôìàÀi•˜SL¤Ž"`̘R׎MUöûýÐyØÜj^{ëÖøå[ž}öð`±jÇ>…è]J€•V1Å¡o§Óùþºfb$ ßÛ½~}ûÚ5I]à­-.–‹nqöñG?‡3Îæ;wßüÚWÞúe¼AM)®»UQkËÄÌ€Ílr÷Õ×o]¿òð»ýàüV¬Cô1zmó¯³FïÝéÉÉg!4Îaa‘º*777`¹\Î3HQØÍ© 9?j¥Š¢˜L&$ µÖŠ¢R@8º±zʈBf"á$ x2™O›jtý“'ŸŸЏÌR” J+SVP¥‹eŠ>¹ñüìÄ–ÅÎÖÆæÎfÛ ç‹3c´Qjh;vaF@ ÎuëUô#絑†¾C9;9$ÝjÕ·Ý0ºËYfN~¹:÷ýÜ|pã˜aF«ÅEJ1qB«ŒV@á0 ,@¤H©éζ6¶e¢Òˆž(£¬Õ¥µõd’8Eæ¶mÛ¶¥<µæÁN”›*bŒN)!’VF+C¤_ KE¼&‚©1 ˜bDĪª6æ³¼€1sâd”™LST@fµjŸ<|Է릩”¥²¶¶°“é´nê¡Ã଱…µš°ïêÅâÂy/œxGR”+#÷½sÁ{·X-}AÐ{¿\.Gç†qüàƒrôMÆôŸ–U¥q>heªºZ®ÖÃÐ'N‹Å¹ó¾,Ëat‹åŠ#[m¼³§kÖLËÒV)æDJ‘ÒÂâ¼ßÛv±XmmmŠ@ÛuÙ –ÓE²„˜, RšYbLÙ’š¥Ù9± óµ»¶Ë’P‰Ák&u¥”Z^¿q]‚sEY*­ú¾†¡( ¥9òe¿^ÅǦ™¶gçO|9ÄÄYû»»5ô½ÚÍ Ì7&‹ÅY× óùVQ–À­—Ô^‚%)¬aÄÀ¹’³7æó¾ïë¢(Êêô|ͬÆÞ)J1ññéùzÕ ã›o«e{zz¤ÕU=©¬fÚž5õdzõêÕýý½+ׯî?~ôt{k~õæÖjyöèÑ}I‰±‚BeYìï?>8Üû½ôûwîÞ}ölÿßûëÓ£ƒý½ƒ¶º®CTU5QÃà0Å4m&cß§Ä€ÀŒuQ3ãüÎ_ôC»XŸ¥”~õí_¿qýÆøó¿øõ·ýÏÿü?üÍ÷¿§i…Âñ«_ýêЭšI±{uçðøèdqd-<Û½vE+ ŒÑF•‚ÜÙ~ëë7 C0•—’f ÏG àsöô2,Ÿß÷’·€(R"Îb!D­PnT‚éj-rɨ4ú¹"ÄÝi´Qé9D3?cŠL„‰9ŸRâ’Ò E›Wwn½õÊÖ‡÷OþêûŸ¢¢¬RJ©”¢0?yòÈ”•5[Á›ÖÖ*·wðqÀâÕ»_6E1Æøðé½Þý›Ã½O &´ÍoýîïýßL÷Þã4ÌꪶöääÈf2&ÐÇ‹­ík_ÿêW”`{q¶ÿô¡"›Òü œ)Õ._‰Þ/‹ƒýÃ0`D<_ŒÎ§¢¬·6·Ë²v£ªªšI'§§—aSDH“I½^UÞG@`áLøÄË^sj»µPÜØ¾výêõ¦®SŠ!!Ô¨”b|L ÄZ©ÒÚBpc7Œ÷žÀ«ýµëW_™ÝnÛÕââ´ÓêÜ»®]cƒÏ=ÈVõ)Hè»Ew|@ÃúTXœ‚è¨M¡`I^’Ëœù&ˆÞÇbˆˆhµ¥Y«ª¬ ‰O1%€r6+,)­ŒR¢ŒB0 $ÐJiMÖha ½bŒIiÕL&Š‘ÊP¡|…(E"1·”ÒÆ¨lk!7ƒ0*SÕt6ö>¸±ïú¢(XØ{ûèC°uÕ̶šùælëÚz=†àvv6”BE8ôìlm®Í°Zµ"ž9%žÎfÚ9S”‹‹EˆÁyŸ‘Ù!Äâ0 Î5Æ¢ªšš´ò‡'ÀpyÈ~ˆÖõ"²^­stŒqyqRò! —ECФ„¾÷9±ÑjÒèÑy¥H‘Z›Òâø|ÇûŸ=Áù­[7û~pÎ'—óÂr†”ä\­µ÷“O‰.GY¯©µžN§yžÁ9Û’9%†¡ï{kmŒüôñ“œ, “¦‰1ú² ¥9KN°äûåÅa¿ºx÷‡g}×!ÑtcÒTåýOïçnIœL*2TÏ[˜+»WæóÍíÍ­Ó£ƒ¡](pÉ—…Õ˜âéÅùrµ(­=Üß‘¢(oíî,Î/z‡ Øx<:99>>kêš´º8=†n¹ØÚœOJ{úlŸˆÊ¢ê–Íë¯ÜmL9±··?úÙ»ëåù¬,f“æg?û°Ò›o¾ Çg+$:=9©ŠõÃ'n¿rÛ{?ãælzÿèÉzuÁ@“éLëÒ‡4™—@mïÛ?øWÿrðƒ®, Ä(›Û¿÷ÿ»ßûÎýGgI¢|öðÓíÍ¢ÐúgïÌ&“éôêªm™Åè¢.˧~ç¯ÿº¬Šé|V•Ǿ[½ÿÞ»‘*ˆ!€‚3SY”Ë¢./×søyÙ~ùør Ìî•“™A@éçÀìç£yPñœ¥§bhk ôÒO !ˆ1öå'd°{k¢Âø7ßó’$‡=gZY´Ö¥-ªÒØI9™¬.عáý÷~òxooûÚko¼ùKeY¬NÛŸüðoÚ㇕[57ïn]¿¦LÉ¢–ëöÉ£Ÿ­N÷-ÄÅùiaŒ nì^3ÿíßþ=‘0&2ueª29•BF)ÒÄ‘ôt6wãØu­F ƒgÇû{§^ÍÎÖËuç¨,+cŠàƒ´ÒÓé´(ËgÏž¡Ò­É,1ÆÒX•û §³‰¶jt£óãeô5QˆU:;=mÛöÕ×_»yóÎ|sg}q‚”Ñdu â}‡±`4ÆÎ§ó¶ëq2Öóí!¤û>;:yf ‚ç•p;¬|èF׆àB@„„I10û~è÷žôÀ„¶,µ-Á¤KDŠÑ‡¡w]‹£ œ‚Ät cB˜UÖy©r£))¥T¯û¶LgZ)Ðö##"±ð0^Däyð/Ž£«”Ò¤ó`3?E¶dΩ“ slÖ É¶ L§“«W¯”eÕ÷ÝÂ0ŽëõÚ ã8x´1hÔÙÅz¾½}ëök¿öö·ÜßHa=Ëåâüäìȃ<Cdï}aMQ³éls{»š E}°ÿÔŸÖQXb €PX[Våd6gk¬ǾëXØZ«´æ˜²}?„HÅ»게ѥ˜8¥Ì\‘”XX2ÕdÇ#¶mk”ژϭ1m×õ}/"!¥¾•- føìÁÃcJ’O<ÙB „˜RCã” 6Öšq…cŒY'ªé²#ŸÉ 9‘ûuW–¦ ÏB[•0vóÍMk¬÷8uëµÇ$%/}a ؆Ç~½ÂÔ;F¢Ð½Zšº®ö÷­±\Uµ³³­•?î^Ùýèã{ö÷¬)b CÛUådROTˆw_yõÎÝ[÷<ýî÷ˆÄo¼~gc2­›Jµ··¿^¬&uSh»ÿd/ÄH(_þÊWÿÅÿú/^»s×¹öððñéâøð™gïö÷î}úYQMî¾úÊ›_þʽÏþlÇ¡ïf³YQ– òñ'+kìÕÝÝ®_?ÝÚÔUÝÌ·¶væ³Õj©¶7·v¶wËÕr½Œ)‹üééé;òGG{ ‚Ö:óþò»ßéû>Œ~w{£®š®D’÷î½÷Þs!ÍfÓÜ"ó£c†­­mS ¨²SPúžž~õÝUÍ’{n H=ï‘Éçê÷‹KîŸeØsšñeeÇ—CÀ²€íçe™àó7¼ŒÎŽÅç™c—ô¾ÿd‘Ÿ¯#ÈݘÄ##q Dʇ>†aΗç·nßmŠ©”›ÓÍëë~yqvñ[ÿà—&ͬ†?ú ]¯ ­ EÏÜú±ìÍé IDATÑs<9?ŸÌ¶77w~ú£E~ÆnÑ1‘6ëÕòï½ý;óé@Ȩª©róQD’µf2mÎ/Îëz²½½svr @úÉ£ƒ{Ÿî-Ö©‡þ|Ýû0Ø¢.Ê*&áè3\•J)úfó¹µ:+•‰YÆÁõCoŒ™Ô“¢(û¡ ™(„$”„"ptr~wMÑLD|;²•µ\/–Üù-´MUhÅ]ßµëe5Ynl]q.<|øh½>DLJ!!­†P!€V€¢’AX|×/F7†˜bL:³a¢6% ˆ¤ÚZ!t„ëÕÊÇ™Sp, ”2J)„€)&7:@Œ1¥” [ÅtÉgfç|H²‰†´ŒQ¡(J¥5A&¶_Îý¬µJ麪ëzâ.ÀÔs›+HJ)d]7'i&ˆ }SJ̆01Gm,³° ëÕ*JÚݾ2»~k:™œŸ­ 1òzÕ»>pJEQh£…€ƒó{R¦(¦[ P”RVe”ëF2 êa>ßšnÌÚu+IRz{Gœ’(R…UÖ@P)%æ„„PÖM¥Ô8Œ!úL¸EJ)ÝzÁ9NLHÃà!„˜’(MM3™ÍªajNà¼ÏxÛLåȳV­Mií‹€ßÌÏoi!‹}ˆù3BDÔÚƒøBjÍ ´²Æf5 ) ˆ›;Ûcð€jsg·)ËÓã#?Ž% Z›i31Úž/–‘X4~½:?\¬Hš™cLƒoEØFã‡5'‘¢0F«ª,©W¯?y¼÷àÁÃnfEm­¬F[ݼùJYF#´XÞÿôÃó³$ÙþÖ×oߺytrtv~1›NžhPç'罇º©Iü³g'''g¨í•+;|°ÿàñ:Ž~ïé!¡úõßøûòÉ÷ô“üøÝÌê†aµ^–e©t–{ÚóÓ³‹‹óSSNË¢Þ˜o•E¹\.öžîݸq#¥4¸ÁZ[Q¹3ß&T]·>½8õ~-)¥¦nHÐu}ÇcÛ¶írÝu½. ¥tLÁØ* ’„nݺíBìÏÏ«¢°…ÐÆ¬Æ¡®K”ËÈË’ÍÏÉ×­žkï ¨Öóˆ|‘TñR‰¿üÑs”¼¬~qçóò^üßk"¾XI^Ôõ—_ "&‘½ûq?eQdÒÌ4¤qè‡qBÇnX¬—Ç_¾s÷ë¿ý»³föƒïÇŸ|0ε)?ùôáÔ†JJ£÷Ãr­tQV¥"¬ËjR5c0Dd‰Fç0ÅÕòb®+4d« µTˆ @Ä9ÏIº®;>:¾ÌýòôèøÜ­F^û®G$,«ÊØ2 µé9(Áõ)Jœ"d(hŽ aݶƒóZ›ÁyÖJkm$F} 4N§úÊÕ+‚ôÓwßëÇM3UF¥i=ßÑ“­ùbñècM±nêåra•J¾¿8z¢•yã+_¿vý7~øƒ¿8—T+¥mY RÊè¢(BH1¦Ì`+ŠBiÅ-Â`´ÍBø$ìSD7’RV—Â("”¢žˆ¢$¥©)j¥Ô0Œ,RVUL<Ž@`D"ÒÊQJIAVqðêâ|Ö;;[{{Ú•ë»^“U•íºu®)F?Žº0jp®‡­«»¯}éõ·ÞúÊÞÓ'C·zÚî!QH!v]?z¯´Àªªy訰–ERLÏKX˜QkÓ4Íî•+=–‘3HƒSð1EÎssA˜11 éÊQÊ §±{kÖš„/ù…éòÆ'1F+99,ˆ>„ŒdÊe½,Š˜aÆ>OØWea2i6ÀË,Þ‡ý½ýÜÆ!¤®ëNDX=¿¼‹Âîìl¶È@"¥¨n&CkÜ8¼H¬dN",! ¢0‹p•ÑØ-°]öGÏNFç’6tçöíàürµ¾Xwžìßyí•é¼þøÁ'í§ô}óÆO?ýôÉ£?úñO²1‘®ï\Ù¹zãþò'wêYj×güìö­ïðÞOôýÍ¢2•.— §´½~õúþ_ü—Íd²X.únhû–9ŸŸå¡×õ~ðóí²nêjZØÂÃÖÆÖïþƒßùó?ÿscLÓ4YŠZUÕÙÙ™!ƒsüÆ/ÿ=ïÃÏîýìðÔ.ÛE×u}×Mªæío½}xxðɽ{ÖØÝ«;ëOïEõÚë¯>y²?™]½~ëõg‡‡Æêóó3]ÔûGO Tmêííã³ fŽ j2­)sÄMž¦¼\…_ÔÓŸo¹Ÿÿôs_àóøE±~±Ç±dõðËeýÅÿ}ñh/~þŒ_|€D7nÝ¢Ÿæ„÷œ:@Jª¨ªB)”-ÒÛßüÆ[_ùÚ¿ÿÃOïß„aSâ7^ÿÊ“§Çþdº_}ók³­²ªË¢¬¬IÞ 'BÌìhŽQ„×Ý"¦qZ´±»«ËÉ@å°„”œsÖZèûVD”VúñÞéÚË¢{Y¸©§ÆÖÌ ™%‰c‚ *PQ—Æ("Ê~ñËÔ½”º®ÏÔ±‹å2¥Ø¶]ð>$VZ)ªÊâÊîîî»{ûüƒ¿9~v>-Aé¤Êw_ûÚ7 ÐÏÜ/‰c, £uãC:zúéòâøÎ¯onLÇõ S”eÙÅ©ÕjA€DTM&uUc×»¥DƒÀ¥>G@B >ôÌ@*x÷ü÷¶0Ö*’9˜y¦—ÿÍ sjS„ѳ\â2HbæH:aJ!%˘ceE@S•Ubafc­‚K”6) ”R×÷—J>ÌÛÀS¸œ¾ZcH©Ór¹NœÖëµóAf'*"²"j¥ 0B¼uûæ½{>ül¿oÇi5¹ôådöiâdËb2›ŽÎ¯ÛÕ_ûòµk7àru±<;ˆ,cH19çÏÎγ&Ë C`N,Š(Èf(¥ÈX“6æ³ÙLPµËÕ¤i&M=ŽãzÝ%fÀ\ó"ˆh¥Pj¢lî)ŠÈ8Št¶ëdÖÂå§ÀlŒ¹¼ç,š¢,P)£4^2=BbŽ™· HŠL¤šº£Ã7)ù²°/ÊJLqôCâdŒRÖ“{ŸÃ‡•1VS’CΞ4ZqŒ%¥Ôõnez¥K&”Äûí0¶Þyî?x4›5) FÆÈ,UYuíðѣǓz¾»»óÚ+wßúú×ßúú×w¯_ýGÿôñ'?»÷Þþêøèàí_Û–æb¹HÌ7n\ÛÕOϺ1ŸCûoþÍ¿J‘¿ý¿¹^¯òÓŸ$I¦°è½CcʲžÍ·Öëþ[oûÕ;wÞùwØwíáá6zkkëÆOŸîÕ“É8¸óîÜ(|¼7«ÊfÕ®æ³y”pvvê¼÷1$N‹EYUçýº7ÆH ñêÅzyq )T¶²J?yò„‰ÈºowÕ®-1Ì€ÌÌ|é5{¹Ô¾¨¿/WØ—KÿË5÷Eeÿ[k÷çJöçç‹w{ùp­/¾ÿòB‚€HrëæVa”U8­ÖçF‰RÈJQ QÍ66ÿìßÿéþÛýàÁ#*4Ň|¸±u‹t9tnìPÊX¥×CÛÁÅG\,Ï$E•5SoÔÞ³ý_A…ZëóãEô¨…‘(·£ž¿!„ˆ`¬Õ'íÐÇ4Ƙ8’2¤  @RŠ9^¾#Ì È‰µ5ù Dæ”g¬Ã0¬×ít61­Ûõ8v ’é±ÓÉ´(*A¹ÿžcõÖ×¾zunÿùÿøKÒdÅ;×oÖ¼ÿÃïŸìSw~~~£ß˜ÍŠ¢**4ãpz¶wv¾)FBZ¯;ÛÛÓI]5“qtˆP•eY”Óé–\—Sa|pÎqŠ1qÖ¸‚ŽA$H IxÝEðJ+£ $¢(ÌÌó<…ŠPÅ”¢ 12i›[sùúˆé’L­Œµ@) ’µE "BÎV¢ì¢$"¥•R*ÆB !r"!"c,)tnˆÁ)bYM'³MT*Œ£wŽlaó,‘ˆø2Ç”r‹Ó¹áüü´iê×^»{rø¬]u£«²¬«Êム1uuåúµ»wî>Ûöìô°kÛ÷ß{ïÁ§Ÿ®ÏO+«v¶vŒ±íºmÛ.IÞåÈw%Ì„˜uU‰Èj½¶EQÅ›o¼9 }ßwÓiUV@äC½'…‹,“M¤•ˆpŠ ¡i&Vk­È˜ÂÚ"¤8 Ãè\&¯f=¢dž¤BÁ#h¥P 1åÎ~rÎDzµë{£òÎC˜“1ZŒ1)Rˆ.øe?ÆØ²¨É{/D BÃ0j…ŠÐZ]—…1yZëF?¸þÑ“ÇUÕ$†È¬ày8%)T)!)Ò‰Sþµ&€œÃUVõd¾)Ì*¥f[[ÒC/ŒëõðÖ[¿|r|øÉ½ûßúÆ7ïÞyýÑ£'Ïž¶gÝjÕÿñ;výÆîÕ[¯|rïþéñáãGŸýË?øßˆšéäÑ£^Ì&Ó¦ª!FOˆ< REQGöeU#ªakHÍdVUõÆÆÖÉéqS•…6 þýÞSÚÔUµ¸¸8>:ÞÜÜ´Æ*¥´V¶ÔËöâ‡?ù‘s¾iêÈ@‘!ùñ»?%Ä7ßüÒ'ÒTMU”Ã8.Î/ªj"q<Ú¿¯Mñôɽø{¿ÿÁG2*çý8 Ož=4M¡ÈjŒ  ùE9ýÜús5÷ïøæ‹ýr¹ÿE›ý—ïö¹/~ѳ¿èö|¾ú"ˆ.ŽÎIê4D@ÄÉt¦¥„óéR±|¸X•+Hˆ&“© ÙÜØÐÆ(.!ÄÂhÖŠ¢GN@—»B’02_»ñJY͘É(}ëÚí[×n?>ߥr¯âù+á”bdݲô!²€Ö¦™Ì«ÉI! 30¦”lQc3o7r”Ñ„¢…ÆæCðÞwí ë¦cMÓL”RÃØ.;¿{+ƒ¡( 7^87Κ¹âñÁûßðîw¿ñÆ«ãRÝûä#­”R9…­šêŠÞ 1Eæ‚@V1b3j¥2i»ïúx6ÝœM7c ÇLJí*fŒÒ&5ž7‚ˆh£!D¯µaÉr~BP’õ""¤Ta ¯4mlL›¦T„BD)C4”6ƔƔ™Õ]”¥)ŠÒsõžÎ§NcŒs.%£µIÉ‹ˆ5)•=™)%"¬ª² ®m×…-f󭢪Ñ{ŸRÔZikbˆÙ{™[Ò¹ÛàS:=;ofÍ|>wÎ…˲ÐÊ ‹ŒÎõCC¬&õd2]¯W{O%HG1†X¥Ñ8Í$Áéxf¬VŠ?*2uU[­úv) ˜Ç)¢6¶ÐÚ*ÚÞÞÞÜœiT1D£ÖzbLBÞ-¯úÆaR,RZÕT夞JÛB©$²n[D™L›¢°Þ§³“ó¾ëSba®êš™…yt#牀ÒJiˆÌÉ{ŸSʲ,ŒÉ­’”bNR[¯×",)ã¥(ŠÅ|ˆ1æK ‚1E̼C4äKE£ó‰Yi%(ιœ{@„bÔÊŠ°V$¹•'"H”çJ¡±¥µÓ­í›¨èéÓÇÞu1yžomwëö•W^Y·«>CPÆ–EUom_}úôàÁÃ=búæ×~í·~ó·ÿäÿxwóÕßý¯ýáÿóþÕ÷þš àöÖNQee¾üÕ/}å—¾Ê ÿÝ¿ýË:eY˜‘@‘Ê/ýÒWŸì§¦³­õº]wëâÚ/¯î\1Æ„~ýí_ý•”Â{½¯R‡¾m[Eúí_ýµ;¯Þýƒÿã玎÷ö÷D@kݽ°(ÒJiDRD)¦gû…-&Óél6òto]Œâ} EÌQÍæÓÍÍ«7n~üÉ=7:Ž1XÂÚ’0Æ€ d[1‹^ÊßÚùâ¾þ‹M•ÏÝ^dý±]ó¹ÊþŵáÅý¿xh¸¼¡ @aËè¼OASÒZ)F›ºœqR ê¿þ§ÿ‘ú‹ïü±&ª«J”ö£¼þÆ›Õdrv±N€7nß]¥õ¹[¯û;ó-[Nšéœ”±ÅÔ–Sߟ^æD± 3mm_#Ó˜¢q>%ŽÞÆè@*¿h­%Â(F§‡$CŒ¤µQ…­mlU½œGÃËï5d÷vnb4e]×M»ê½ …1V+?F`ÖÆj"A±ZçæißõŒ°^­Ï/.‹EHa‡²°ýųï¾ó­.Îâß?Þ²RÂcG;™LlY¸œwÖ}ß»±×Z+•á]˜Ý‰TUV$œP¸*JDÈ NÑ9§V§dò°ž…‘”1–µR…-˜¹ï;ï< ‘Öª°EQTÖlf˜€ÕJ$²$¹ÔïkƒÊ˜¢œNçʘ˜ ”U½N]¾blU”e‘“ñÇqì.# µQˆQ´VEaÛnÀÖš²(˪jfs¥L×v‹ çÖĉói1‡k¥@D•Ec†~¹¸xòäÑ0 ?‡X‚$9fkÆÏ>½?ô}оª‹è¥’)8Åý½ƒbŠacs>M7··ÆÑÝØu­Q1xß¶\VUÕ4ÓÙ|2i†aŒÞÏgÓ²°Çë“®rjº±¡šmn£Z·]_Uõµ-C(•çSHãàF–Gœ’5jc{j¬Z-[PZKŠÌŒMÝXksÃ0ÈÁ´9Ÿód²î:­uY–š(oºS óùüìì¼ë:£Maµ¾ÌØâqtÁ‡cJÜ4Ít6E­”Õ³ét¹X‹w1aÊ:j£u]W>‘ãz½ >‚Rbà!Ek £P¡dX¶U˜bbI q"ÎtQ”Å0,Ž–ËsDfŽ}·Æ½§¶¬^¹}ûÑ“‡Zç¢AÚÜÜ:?[\¹ríúîÕRé_ûÖ7›ÒÖ“úlqztrPVŵ+W“$¥4 BBfÿøñ“}óW¿}íæ­§?ËR‘ÖÝ ÉjmQØhýú«wÏ/.žî?€Ã8¬ý TVÅîæ0ŒÂQ)=†àb4 ~ø£~ç/¿s±Z"p P´6„J‘¦Ö0ö €€sγ^¯E²ÏÎ)Òº(ªº,¦3ýÎýQUMúÁwm?­ç7¯ìrêIxVY Úå=ŠÐeÕ~éÒýÛ÷ì/¶á¿hwÿòfüE)ÿÜ"ñÅÎÌ•¿õò×ÏÉ—†s­µ„—S`ˆIRôU©)I^!h¥l5iãPM&}ß•¶¸uë•Õùñ³½÷±4õz¹ ~LuQت¥•v! ‹ÑUŠ˜e 9öfdæÑ9IÉ!3+EDÄ)ÅbŒÉh£mAŠ´V!Ä…ËÒˆ@ŒÑ*S×u3iò©ÁB¼8_ŒƒCÀü8ö œ¨ )¥ä† ˲tQ•eYUõ‡Ïž_,­báäÖ§Cß@„”8*"ÔRL9«ëú¾QQ¶ÿs.”œbÎ&ÀËWO1FD ”ü&Í„ˆr<]!Eo­Î¹´—âŠ,ÉA†…5Zå k½ÖYF-Zé–D— 2D"á(QX2T^H[éœchŠR›B)—R²Öh­bŒùèÝ7"³Ê0Æü2š¦ÙÚÚzòÄwݵV,D´ÑͤY­ÎI¡ÖÚXƒ°¤¢´È)ïBôFë®oONN™™P!")²…)Ц‰Ð‡H››]ßM¦Õt6O‰‡~Œ)¥„!x‘d 3:罿råÊÙÙ…s”BfqΕ֢ºœ¸nnmqJntggŸÝ¿‰=Š)¦¤´ÞÇk·nMg¶¬ÕrYØ¢,kîÜúbµ X ¦”ÏLœ’R´¹µÙ466¥Ôõƒ >1sŒÂP·“»Š˜·í¯Þ¹³^¯Û¾×Z‡zçRJEQ ÃðÞ»ï")RDy)¿DúAnàpNQ#… Ð9wÜ÷ãÌbŠ)&ÖJªºÄë¶A…&ŸB¬7g[›Û³SƒTU…µFkç}J1&2V›ÂÎç{O÷/.î%bž4“ ‘ïÛõŸþÉ;ŒˆHÍdº±µÝ¶=]»r]"w¾ûßýç†1%ìÜ©sNÛ²T†H#’Œ®½¸8EÂ÷ß}8 Æ~qæäƒgN´\žmªzòÑGï`ðþÖ­ÛÑgÇ!øu7þåwEŽ7oß"RÃx*ÂíП{b Hd¬ÑÊŠ¡šL¦DúW¾ñ­O?ýtoÿ³dOŒqÇ¢Özp!ÄPÙÂTõlcÓû°^­Öë çÇÉl moíŒã*&& •"eƒ"!à‹N÷ÿŸÛ/jÎüÝM˜¿£ÜÿâÖÐÅÙ². ·î‚¢lc„¦©šº qøŸþçve÷Êlc9\œ/n¼²…è?y¼±uc:mvwvº~í“C£¶v· «×«³élcˆ~N¿ñkoŸ>¾÷é'ˆjÒL†~öå·Þž4eÙ2È0úɤ8=ïŠRçï+¥9±nÌ7Rê@%ïi»1ÙþÒ—¿|ñÞGûûÏÞü%¯Š±s¾ TÖ6ºõñäÉÓoý¦®êJ«Ò*Sj³8¿X¯[@Ušr{¾ûìäbZÏê²Ô(9ï„Ífž×É1ò¥9Äû •.lHZ+%—„:oӳіùrrË™6› \Œ‰´!¥½K"BЧ¡*¬Í,iïRÊÍæIcšY=kêYS_ÝݾzíÚÞg•"P™À)¥ÄÖè,KS¤€ g ²k’º(Š¢Jœ†aè‡A“Š“iS×ëõ"ø‘Š¢NÞùK¿‚¤KC‚“Ú*;³RPyä àœaVJm²Në¹Ñ™ù¹ .O”"œjBBñÞOç@8:b̹ƒÌ ‘H¡pÆ,oÏÝsd‰Þ'DéºÖ¹!ŸûŒ)¦ó™¤~—«¥ ±™Î.ÎŽ×ë5³ äí¿hÃ8rJ¦°…ª’O®*[zïƒ÷1¤ÄÉX­”ÆÑ‡W%",‹‘ºn(¬‰s@z¾p™Y‘æÄ"€Yô’G+Âbl´ÙØÜÜÚØØë»Ó“cÒ9ËY©º®CHC?BlTõÊí»ßþµ_><è&å”`üÑ÷ÿfïéCBp‰SÐÚXÃJkMŠS\-ˆ©nêé¤>ÄD 7¤"_µ”V¹ãwéPÍž&bŠó¢(†¡7ÆlÎ7æóy×µmÛå%“ IDAT¡®ËÆ'ùëüPlùêÝWµÖΰ±¹|'+MD·Gçf“êÚîîælö³O~ZšdµV„ã8*Â×µö1Ž«• cJ~ì×½HR€9DÏ-"ìœ×H$òJcH,D:Gf)Q_Ñ1†qm]5MCˆ1FÑ…i&µs­£€(MZÌ‹¶æûà‡¾_,—m×.ÏÏÿìwüê¨@.lII"§#p²Z `‰¦”óIâàðµ6U­ ~—ËEŒIkåÆqûKož¹ÃÃ#I)“¬óPE„ó9…èòZ°…ABN)ƘbBDŠ1ä$´”„­±¤´²ÖÖõ¤®+¥u†\¨PºàIi…V8µmWMlLI"ï‘(§Ú!3g-‡zDÌ¢3lŒ•)¬OXHåjæœÛߺìVʪjRì´íÚ;ŸÍ~¤(H Þ#ଜ[Sî†8 ³¤Äé²û!GGÇ}ßç¯Rª¬ŠÉd¢”ò£_œ_¬VÝ ˆ`¾€‰(¦½ÏQVg(WuYEY–„Ôu]Ûö!c´Š0Zk«Jï}H¥©NŽNò£ï?z’xç†öôãO>^®× ªëGclQئ™ft8 ÄèÇaØÝÙºråʤnb‚źeæB–H"a?d"93k¶w¶Ë²†~*¥§³yQ”mÛéŸÛ²¦í¹ù%ËRJG‡Gççgù̧ˆòø=·q‚¹Ùuq±øÞ÷þúàèYQ`Û÷ ¤„À!ø,eD@µZ¯£Ji>;F—3=AŸ]œ÷CgŠªjš®ï"G‰~Õ®»aô “éÆî•]ˆô¯ÿíÿm*}¶\hEVë7^»ýÑ{?!ˆ¹Ç”b"Òwî¾ñ+oýÊ¿ÿÓw¦ã÷Á#ÛÜ|û‘±¤µÝÞÜZ/WiE¨•ú”RQ6Ø }]—ÓºZ¯×œXrýêÁ£¶(#K³µu³®ìãG*£ªÂînmݼ}çƒïOçó‹å©‹òv¡*SâÁùƒÃ£¥i6]7Þþò+ç§núŽ’p n”˜'Mqv~¡ "èOöÛÓãÃõj½µµùêíÝi]¸qdÀª*ú~(‹‚𯶭+ª ˜½‡ãlÖ`JIbºÔG‚"ò$ËO˜!÷䥰”X?—Ê%„=Ó3¯#II0«€P€3%†ôjð ˜DÔr韎Ãè´Î 7QJ!@Œ1Ä0ŒƒaksÛRSÕ[ʨ³Åñ£B,4)…ôÛÿÙÿÇÔ›ôj–eçy«ÙÍ9çko]FFfdEµÌjXEŠASÒ@†`˜€€á?å‰'ÙËl †,@$H±YÉê“U•}FdF{Û¯=ÝnÖò`‘Ôbp{q¿sö^ë}Ÿç¿!Ð_ýôÇOŸ|HÇaèû®öæ˜b,ç˜G¬9§l­³)§ H)gfíºUÚ­ïÈL'ÓÓÓSÍùêò"„Ñ((I–1…”±5Æ”ª7x_cIA5T!&ŠÔM ‚)Ží¾Ê))[ç‰ÙhËd)¥´ï÷//Ïg/—¿ýíGà­û¯^>1I–’et–SÎÄFëªÎ)µûÝ8"h¬ó1æ¾ß”ãNš™s&¥´YïU¨ªD`ÄB[,( DRPDö•wÖ“¨h…¤*ã8Ã%;6" p̈ ý8bú!uýHÌÆ˜z:qumœË!©Š³v ‰Œa‘ÜC5Ã1&ѪÌ*""Þ;f* «B„g&*uy"B¤I3›Ì×ûÝ®8OBHÆXD꺽­ìñäØóLUlíQµÝíÓDRß÷9eB a‰×7«˜’÷Þ9‡@uõOá÷ÉdRUUÓ4LdEÂÒÐÛͽuã8"q©é9ï MÁ0(8kªÚ×UElˆ)¦DUU•wF™áÕÕeŒIEf‹£['G«›íßýõ_5uµX,®nV ‘P‰\iŠ•„{’€bÁ&«(1‡Wë5# ‡;9 ÞÙ”½”Õzy‡žœ¤Ö«›’ìû¾k»¢98}^•HŽ!ì¶Û¶mOOŽ—ËåR/..ƾ?ðß­™ûù8BŠ)™”rœ/O®Ÿ~¡YRV@„1Ä~ û®ïºÞ°ÉXzßrh«óË~›)!±±UaŒG1Ö×ÍlßöwïL*_?½|öÁ‡¿]Ô“?ù£õ‹÷ùÅÓ—ýŸ{_Y[†FÜuýnÛî÷»£5¦]ŒˆÉØRu¤×(sjÛœµYÒ"YcCUP­|¡rGDðÖæ5ef¶Dš£·X²Œ iÖøœ­Húê^(Y˶\TA‘™•™w»ÝõõÍj³k÷£dUSÖžÌ š%¢r-#²¾ë>úðÃ"buÞa©•+/— ɲÛï¾Êf”—Jaÿ&`ÃÖXPº!¥<CLñ«FU]Õ•¯&Íô÷~ï÷?~ü7û·9Ábz„ìc5åœrܵÛ~Œè|EHÞ””¯É’ÈoælR"UôÕ”L ´u½È Î»”äêb…¼ÝîVóåÉÔÖš5'Ønö¾ž’&K¼Xš¶Ý5uóÎ;ïâ«—1¥C)CÌ)¹Ê³1“ÅëZ•$C»ï$+’µ“æôôöÝ/Ÿ> b3bœ4Ó7ïßCs}¥‡_‚—/ž-—‹}ÆawyþlºcÒçOž ãðýw¿õ_þê/þðÿyè[ÃÎ:ûâÕ‹¾ï‰Ít6oªéÑñr±˜}øéo­³I‚H–{@œÌ&mÛ•"˜Jº}v‹5 Ã`”LŠ)dIÂd«˜”ˆ™ífß]·‘˜¼ó}×]·aU FP=:Zn7ÃåvÇq:¦´ÇÑ0³5ªhŒA^cI³Ü\]Nš}]ûÍnU8Þˆtѧü5Ô…ô ïÚîôìv*Ý ;ï]Ûµ}7 ¢õv6  ) ÝDU%cˬ•÷X–DfÒL³À¼ÙíX3à?þãO„èöÝ;ÛÍêæòcr¬Ió'Ÿ|¼ÛwÏ¿üàóÇ-‘Ì!‹jˆ±2†ÀçØƒ 9>»ÿÁ'ï¼óõÙT-£3FB&@d2Ö@‚a ?yòä û.ŒÁ€BÊ ‘J×P'õÌ1«hމœG@çñ>¥”^ÓT˜ÙçUçœSòÂÄc&"ë]UUÄF‡<­&woßš>¸»>ÿƒ?ûÿþo’– ‰]©üæÚu›¡Í|îÝ|:½­„«íõG2LĈÄĆm³¦,*¬€"I³j23O"aŒ²H/H1Ž" :_O›£ùÉmb¿]¯Íö"ë¡Ûõ]ݼžL‰9+jÊ®ëVÛm€AŽìDц¼ÏŠ1e&cؔiju¶ðR¬3€•‹)Žã¨*Î[Bˆ)•gg9V33¨`·Ûl6+cs–ˆ§Ó)"¶m—sR‘q»Û‡f³©?>bg}ÓgC !Ä¢m"B"VÈ TS¡5¨f™µNbˆRù¦®›e½YKNª“0¬ ˜Rî‡ñt^/æIeš™ cJÖºédZO&a€4Ÿ/CcŒ*9qòuˆ1å}»ÂÆcpÞ•h?³Q0eˆ¯"øO 4†‰ ®í½7î-û®Ûí‡[Q f.ýÉŠÀƤ”Ê2®¨ûÊÓ£$PKbµ®ëCáÀ˜étzssS."‚HªRÎõåOÎY4Îæœ¯¯oBˆDè««jú”"¢M9JÊrÿþoÞóý÷½˜ÌÞýö»çO¿|ñÅã$ƒHBò‹ÅÌZSB8D,¹XЬc75nb&¨ š € ëgItÞ4³ºÙµ[pìüC?üâ£O©®íЄ*“¦~ðæÝ§_<Ùíw×›mV™Ÿžå0û>¦(šÃ8c6ûýåÕÕñÑ‘a3­ "€÷þèG?üƒ‡¾þ¿ýõß{/„ÑNšæøøøƒßþ:Æè™µ„øàÁ›JfÚÛg·ªjóñêÚJ¦”To®oöû¶iš?üðüü|1[üËõ/Î/_u]:9;úÑïýè§?ûÙÿò¿þÏ)‹ó9tÞ ÃBhêšïܽûìÅ 2$ )ç»·nJJ#"2Ûƒd¥”’W…1:KC?樤™PÂØåsÎÆpLñb蚪BÀÚÙ0 mÛèí7ïºu‰sÎä« SŒãØ‹$C¶©&WWW„DÌ*Z×5¨f6¶®'›õÆç¬e¢0¦vߎ!2„) jÈ‘‰ÇnkD]åû}OÈ\£@ÞWÞWHª†±r0Œëë]×4v1©ôîwLÇíM3;i,þô7ÿ°ºü< «“ã·Son®æÓ“e3øj}ùÜC`vŠÆ»é|~ Ö-fÇþ}ôŒ2jÊ’Â(ÐRI sØìVûÝÞ„û¾·ÖN&S2BDdöòµe.›ÏRÖb6*Ò÷[×¾ ÅQÿ©ÀÖHÊl ³%Îdle (ìöí´9:[UÖhÎÏ_„84΀÷ÞY[ºGH„ ÌRJ*윭n~Û/îœê«/Î/ž³5Îø¦™2c½ˆæûa1hBTÍÓ^$EФd|-EØ¡_Ÿ=øî~4tíÏÞû«võâêüézµ èüd¦~Æue­;=9;:>!c»~èºýfuEl«<1!¦˜‚q¶lXÊYØ:CL9Y=² S¥ ŽcŒ¯›Îcl‡¡i&Þ{ÙívIEJgPVÙï»z·³Þ³á¯(çg1¡‚( c§ªÆk¬ˆ” dß·9GT˜Njƒ°ººyþòUÎÉZ£ªüºýqÈ¥@ïÝt:íÛv¿k‹{}Ǻ®góÅÛÞ¾ûÆgŸ?¶Ì*¹ïzÈ9f‘CM"1'c΢ˆ1íµ<‹U!ä¤"e'_béD`-Ošj>Ÿæœv»öüâêüü’¯*OD„T8€9çx0Í2„‹¤¦ÔÄ‘‰}U C(LÊœóÍêf6›cÚ¶-Óöòc"¢µÀ ˆ`ÔrU;ï+ÃÄ„ˆR,ïžb!çÍ¿ý?þ¯ÙlAd3p5™þàGðôÙyJF³Cj¼Ãé´f>‹’¢v)4d*kæ'§·wݶœŠö»Ä’qo?xç÷¾÷îÏßÿÉÓ›s@kš¢Ìyhs{Æžžn/·m'H„À’ïë8þà»ß~öüy5­ï?|ûßþïÿÛùù¥w¶¹wïäøxu³­æ³³wßýÝjÚü³öÇo=xøþýÿÙn¯ˆpÆ«ëP(˜)cŒqöþƒ·ñ‹Ÿøá¯?þè·ïþÎ÷—óÓgÏŸ\^¾$¢a7» T¾žL&ò×ó×»ÝØÄœ>ýì³ÕvU5^rVc¸ªªC § axÿ׿\UõÒyŸn6›Åíc@cÒÜCԬʶ΂ª¤9×ÎÆ aÎyì{Ô *³cf”úŒ8HTç*h*#’W×—)—î6cŒ1tÆc „ži³Þâ\ÔöDÃЖlŒ¨$È9'1.Œ¦0_™Ñ1!ˆA!М²aÍY% 1ÚÊö"ƒBÞï¶Ã0JÊóéd: ˆ1ÔTöw¿ÿݱ _|òÛܵÛ~œ__ž.Üp3H¿ß]œ?»Þ´·OïŸ?±Û\‚’’wŽTe2¼xñy}ïcÓñÙóO^=ûìúò1äQbŽš3ÎZ[ã¾m/®Î·Û3Þ䜫ª*àŽ(ˆ¨‚¦” rÉ)æ˜{íÁ7 !§”r2LDH}×í¶[f&ÃcÈ€lìbÙ”í<³Qàרû=B^,¤ @E5f!"PB$ç<{ÏÎ[[ûznröÆ[}kÒLŽïžÞ:c’©ª‰¯&HlÍ1¶û6¤¨9µÛõn¿1•ŸNgãÐûýËW¯ú¾cÒ(š1»¨?yb¼ÛÝã'Ÿmo^öý~¾<;={pzû¯=zÈÖì†ñääÖ÷î/Žwû}ßîӸ߬W b@ûíêçïýøå³ç¢¢Å׬¨YrÂA,wI™„Zk1Xî‡fbIõGÎÙ[rŸÆp!+¨ªsÞZ›5ýØíö’rFÇšæ¿Jæ–Û!¶‡6ôùíûö%%ĺ>ÄHRÎô:òÅL)S`öÞýPŽ"2™4¿ó;ïnwûýnW7«W/ŸWÖZk›Y=™Ý—œ7›Õv»“1Ý[ ÃRÊUUwR )')þ_cÌòhYyg [ ‚ÄmÛ_Þl6û”• " ·ncÊ@Ça€ªªˆ0¥¤9)PePçœsV‡!„TNèÖÚºnNNN·›íjµff9D+]ö‚ªŠ‚¯+okDI–1ç *)KdfÓ¥dÜl†›Uì‚ÀŸ|¦*<]¤q¼}o~¼˜^œ¿úVbN„QeÈš«I=’sn.\Õ“³ºššº:^ÌšÙqÓôLEôÉŸ_^×_~I³Å©·éÕ˱ë]S-–G}’6 ©®½ÆÊ»ÍÍÕéÉÑÛ·¯Vë;;;9µÖ=þü1„ñ³§7ûíðï?š6ó¦Yb éäø¬®§/û—ÆÉm·Gâ7îß»uëôÓ›}ß=yüx>©fAÆþfµŠ9ß¹÷àÑ;_ÿýŸk–ýf6ëkCPtÈRT_y&TõÅű%vx@4åaèKï7Äh¤nVWµwÆšã/~ùþo¿åk›bД Å4"e+±B‡‰P²Šæ†žq¦Š¡÷¢9¤‡¶­ªš˜±¹mr„"4ÆAU4"Rè!"Êü‡NÎ1ç`ˆB YÄZg™¸Tôc@‰9óaK/€˜6ݱ„*±xÙdè;E%Iy¿Ý²óŒúñoÿñ­7ßþÙOþîñg6‹£‹Õnß¶';úùï¿øò1ºz±¼ÕmvízsûÖ=c`èwG'G iÜ^ÍÓ†v1mÞ¿øòóO%ý Co‘Øg‰C/nÎ×»íññÉÓ;FE3†YR–”AµVBŒ†¬f-½ˆ!šº¶‰­óÖš]ŠªZ×µq™BHˆ†YE“dcøh¹¸YwÝ0¤”>ÿì“Ï?ûXUØÚB¶A뜅R@QSs=­ë“‡¾q||ÒöƒÄ°Yí+Ã?úþ¬›øz®„YI§“úìd±^µ]ßמ5ÅÍvw³ÙwY2kzï§ïmv‹:m×µ·În¡ê_<ùËŸüؤº¶ ‰ëéÙý‡ßøæ|Õ¬¶{%­gK×L3B?öUã½G”æäd)9kŽ7ç¦Ô` QDPrFR€œ3‚ZkDÅPÈ©ôŸAû’æ.Rco ¬6¨ä!X²Š¦”˜ÉC»›U»ßÏš%• @MÈ¡"Ua € DT]9§,‚€] ±mŒ%F£ª J%9P&7è}E´«ªJ‘йj·oÏ_½ºººRUb3ŸMgóùt6¯+¤ 9¦ƒ«OK>¬ŽÃØ÷CŒ‰‰‰ØZ“%!¢µ†˜\åæÓ nÖë0Ž»}¿Ûw1‰óU…ï»V!nåFrECT¤NESdfB c,Q}É ÕJN¯^½lÛž˜­u…$[WHb¸Œ^q#šÑÕÎ8köã̾®ÏÎîy7Y¯·“i-¦²Ë“c"ó{Lo½qûí{§ÇóÉÓ/ž|ô›ß¼|öòúf½bSO¸ª“‚!;[Üj&‹¡aܸ9ÖÇïùôÓ›««åRwû«¿ÿûÿ|ûþ½”óËçO¿xü±1Æa·YÿéŸþ÷¿zÿ—›Ý&ådŒ‡øä³ÇÞ|Ó#˜\E«õ«œC!“PÙ¬i–B 1¹fª@ÖùÙ¤¹¼¸ô¾ùÚ;_üø3ïXrîáÀ ª¨2lc!ŽÝÐ)ÒfÛŽc˜ C/5Ç1çä¼!¤ ?JU23IL)ÄÊ;bêºMЉ!åÌÌβs4ÃÁó¥ZRÉÞsJX†Æ$Œ¡7ÌXE@ÖI%Çlˆ  Ü²¢äàˆrˆ’²sŒ&“椮ª3@‰ñ\¯ÌB¸u|üÎ[÷V7çaØ[b#ðÅOž~ù%0Þ¬/vã¾™œœ.ïm7¯Æ¬áEÍ1ìC·¯Ç™´ùÖ7¾³Ù­>ùäz»Û§q0×/¾üÙßýe=}þÛ_ÆíµÉ#jöÆ3[¶°o·/Ï_^®.Ž—'wnß;=¹}0ò@y¤Ä„ÖYQDfc•œ£qAb tÆÎçóI3Q…œ"VƳ³!F±@„LTÚ=šR,.&î»ARÊqôþuÁDÁû MUÛÉ‘›.îß}øÃüîÛß:?¿ØîvͤN)[ã}5 1‡I€£ÂÕºOQ«çˉ'lª)Áõó«‹é¬‘4>zçk•÷9…Åt‚D»n•{o¼õêéýn=ìÖíîH OÎîn¶ûýù¹óf¹œRÈ9ç$’‰”­s4[[O&Æš³6ÈÄEhUö¨Ö°aAcŒŠ¤˜Ê!ù`9¨ø¡\ÃCŒ˜¥ }±lˆ™ 3[k°ïÚ†ÃéÑ  ÂP¦É¢òU¯ˆ•ñu]ÖPÊ@‘تjŒ!ÅT¦ÿ•5ðÀNcfï]Šñ;È)¯»õÍͺï{‘ì¬uŽú®ßn÷“éæîÝ;Uå˜Ít:+ÎÄ”²v[Ê1¥`sì¢:-ú~H9Žcìº~‚sN4E¨ëJsBC¿gæ¶m­s¥ë@L)ôED!–T!!y‘’ 2)ÉJF…@@‹L  |”˜1ôm =¡NšÆ Ä8ˆjå½õÍb~4¬3‚¢!ªòÎÙÛ·n/OÆ¡‡A¬ˆàœcÕ?þý?ìûñ½Ÿ>#fb2 ’Õ²¾|ö™¨µÖýòýŸl·×oÞ½ûÍGÞÿͯõ“¿dgSf‹ ÄLþÖÙÝm·Ú¾z~u}=ÎNNϬs›ÍÖ°1定B´l k­¨A@AÈ Y2“s\9)9‹¾kwû}NÉZ[ªLh,W•ËYÛ¶C I£YsMÓœœœö«c,oX=4˜±„ÉavÍâÎ݇¾ñ-ëüó/$«1® ºú1îûMÊCrNŠÈ®ª‚È„7»mÍ’úþúò¢m׎ço¾yß(>ï‘ícC<[,ïÌæ?ùðe?ÂäØU“CT&5† 13f1ÏÚZš IDATB Á"‚¨82LÇQbÖ à˜1¥€#3 b‹âPw•¢•Òa_ˆe8ÀÌLŒHYTT™…˜¼q…v[Ö§Þ{kí8 ã8¤8ªÈ—_>-sérÌWÕ2&rÎzïsLãbUÅSŠËH¥†£)æά¼7ÆÈkídQtíøòÅ«®ï+ïËDcFD"Dc]UUE‘‘Eö»Ý“aX,ÎZǘ³Šdæ @€Š¨Ör1£–:๚¥Ý·çç{¡òq"DQE”¯ %³€ªJ)DC¨¢)Æ×¼eD¤œ’`yßcŒ=xá‰Øy¿ÛnCeúÎ9×ÎÀ"1Ed.5…q÷û-3«”UmÕÌç ®šNýtDú¾/P ïj_M‰›®ï1a¤bÙu9LŽŽ¾ýɦ‹wî¾¹<¾åªÚ0œ_\¤Û’©žÂÒûŒ:[çH‘r J×û|q3"Ú¡ß¿ZÑÛã8ŒcDbæˆ )ÇÁš|ëhròÖÿö_þÁ?üøþŸ÷ÿ^Ýlû£äŸþôúng­ÙlnR&MÝ4Íñr²o»˜Š­w//΋³ž-2›éÌÅǾ¯œ=9=yñòeI…ÿSHDbFÏpß÷·ïÝ?::1Æ?þ4 éÓgOú~øìq{}}=„!¤4e.¿|"#‚Y1‰ѾëÏ£@ÊEö’D$Žã8ŽC1´ˆÐt]ŸRžÎ¦7×7mÛžžžÃ}×FD, Š”Ç!K×u®òÞ×Ö¸”S; )Ël6Gc*Ë!Œý¾¢µžøwPdcA@r*ÐÆÂ·ÆXÃC¿Úa2Æ ªeN!8_1ç*kªì›zƒìêÆX“œw¯.®ÆýþßûObtµÝ^H³„ôwýçý0$¤Ôf‹öN!;;»}÷A;t)ÇÊ6ûmØlV~ô›«›WÎ*bÈΰ$b«Öé÷ýÍjÕÔÍý7ÞzøðëëUÌÑ¢¨Œã˜S²I’)DdÊ(!†TÖÊû Ç8æœJVA²¬×ëÒ’ïc,bâÒ¶ê»~·Ý((ÊÉN‡wí^bŒÝ¾3Æk cŒ’I9“몓“[_ÿú·nݾUÈS!Eï몮B†)Ä”S’8†nŸ‘ÙU‰IÙ'1²Z¯vׯ6×çûÕŸ;~ãÎñƒ{·†q!*’óÖX&‰¾2C妓éñÑqÌID —¼ª¼5–ˆJ–À[?¦DŒ„Ju±XÚWVe㪪™L&d  –L‘”´j  ”†k9q”dä~·ÇÀÌM3)êíªòU][cBû®c°Æ"•ì<Šä0¯—~PB¥$Tòãå9XÞ‘*2™4óé¬ëºqb,q4†Ë› ,˜1Ä$ŠåCŒ18±ã0¾>×CyŠZg™LJ TU$†@DMÝÔu-"ݾïqP®ëË6>%AT묱¶H>¬µ]•àfû¶ÆÊû¼I1Æ×€Ã»ê ¢âl#ÀmÛµÝÞ²=LxšÉ@CH1¦¯úÅ,VüÂ)¥««ëõzã?”›@Ç0†€@U)¤³{÷†~ñÅ—_R–Ôu}5Wuä2ú(¨€e©€!Åj:c¨+–ØïÃе}X÷Ãéí[wÞšwQ¯˜Ý¡ÍãxÿÎ"4k/c²›ÞÔì† ×Ýxtæ³hTèzy¹nÏ7r¹ÍW»WëBôUÑ-€±"•¶ŽjNqdæ›õŠ)?|ûíï~ç[¿üÕ¯cèC¿ÍaoÏfóífE„ƸÕf+ˆÞ{d*ÇI@Í€˜$§ñ*öγ±mHéj••)ÊAaGŠªÈdóeȲÛw“él±X¤oVWUíQÙ±Ë Î¡ŠvCL9Y“ÊMk’Zç3`Ì"@ÀNP3HÌIrc(6®ÂtVçÌÞ5 c×÷ z÷ÎÙ|Q^á]ß;g ác»[[ë|5­'s~ß¶1ŒLX×ucŒ±©ª›ëë¡ÛÂl>7Œ1Ç0ª³™S h­Ãר’2³Ì ’Trr¾2†@ußöÝФ\× ¸=¶OŸq;^ì»+Î5§†—‹©M?ÿǧëNy³Ùì÷ݤnH{€4ì6ÈÄÆ–Ê’11Ž)…Røìó«Æº7ïßûüÓO6q4¨Jd«)!«&cøf³ÞíZëœa¶Ö”BFÑÃG‘0åØv­q„ÕLÀ_vc5]a ãÉt2nnrŠ¢j¼¿ûÆýnV›Ý'Ÿ}rquc0–4æ„É’ñóÅRrÞ·[dGÖg`PåœÔ[¼¾¹Ùt! ÆUõ<ç~¶8íû+Wíw»~è«Ú1™,Ô46¦”R°·¯|× Y²q~fÝíÛ·÷ûÝzµò~rçÞ=k³ɾšŒC‡ ®r9ç®ëcÖ”åøäŒRˆa«zÂ̆­D*±BF Ô¬e°!9!"²aBÉÖW Pš±cˆ&CBªÇóÓÛ¬ùôøx}³Ijº¾G•®ßÿ§ÿô¯¯/s9Î’jŠ¡žLšéâz½q“™÷>…!†þàBs>_]õ?û‡]·Ï:ž¿|J€š³!&Fì93¢·&‹ìúöýÿ±©üùêòí‡ïÌf³a¿üâ Q5M9ô[+¢|P9PV0–\]•¾ž5óÅl>©5§6 EL)è¾ÝqÖØ+çœRŒ)YkÙ0#±ÁÊVÅe<İoûYã 1®¤MØ9QvÕìôÖƒ³³7ºÝf»¾Hã°¾ Ö2ëªÊGÄ"qûVS4 ëÍf¼¾¨&«£ã3"òŒýØî×íöÆóbuõrŒÉÕ 3ô}ßö)KŒ±ÛnÒØõmÛ¶»1!8ƒ®$œ€¾ZEV$“%§,•´ô“1VAPSÚ¬¯‡4ÔócG  ,Eˆ9kÁU”Ɇ¾f!bÝL§“Ùj½™L¦Ä¼ZoÚ}{s}•rf¢¦®Ú¾l ¬²aB`B')ȹ´í‘SJã8:c  °b©žÔ*Z¢;mÛv}ĈF$—üx¡) g.KZ€r¦c¢ò:Éš5¥ÎU“Iƒ Ã0ä”E¤m»ÂÂ$B@Í)(u9ç ÖZïm ©lØJÇ@÷íà¼÷β«êqË…@D´°÷sŽ1bÊüzµ`8Òk)ZéC±1fF‘œE³ VÞ "ˆˆHDÀÜ- ºŽaÐ,ùUD躾ëzfvÞj¯©j.t9TÐ\V%ŒD€H’bß©³d¬MÁ¨4Ö¤! $éhV1Ûåtq÷ÖQS›vLŸ~ôåJ»ŒŸnvëÝv5tÛ/>n¾ù·¼ó«Y»¾Ö4l×P¡Ê;à¡GÃ1Ž9Æ1'iꆉ° ¹bJW›­·þ­oÿôâg“f–a2-ÚõuN¡ïÚÝ®Bèû«ª"fQ"TAµl ["ÐÌç÷¾÷íßMYýtJ ϾüòöÙé/ßûûØm¢!¥//×ï~÷{ïò«Î:ÃÕrQìbz²kCÛŽLfÈ"I*¶^9¸ºž,ujÝ›÷Þyøè]çf! ôA'ó³BRàz>ñSÃ4†1¡¦D”€\UïÛþÙóÓÙt2™ÁÍn?Œw|z É*òf»ŸL'l\=aIÑZSUÆÚšˆçÓù|>Õœðüæ:#Ú"Ìù VS^uÈÀ„Dxl+!–ÿ``ÃÆ¹J£hS7hüååêÕ‹gøÑ­£å×Þyç½÷~úâÙ‹?ý8+ï3<>¿0 'ÇǪ’ST%_ÏNªé¢QÿèÑ£/¿xÜõ!‹v.+‘k~øGò‹_üæ¦T™Ï6 Y%ƒ‚(’HÞíöÏÖ+ïíéÙ­år±X,¯®®Æq@RD(¦$UM’ˆˆØˆª¢’!W;`¶Ö4“‰³VSýc2u“sÚívmßíÛÎyY²Qsp›!”ìÇ8Žh}]×MU-æË¯ã[ï-Ç×w]¬ªÊTõ0Н&óåINi¿^7N‡œÛÝŽÁ{G˜ÇëÕ%³É’ri%ªa»ÍúêâÅæòú¶ß¯»íŠ4å±ß‘1)Y$q†Óbè»Ǿ]÷í&ç,³ÙÒXËÆ¹ºëˆ‰ªºBdBcØÅqðÖXv]N Ûœråý3¤>šÙE?Ž1EgØZÓÔ•wV%‰ˆfDZìŽJ—x ƒH®|5ô½e®›*¥Øö½¨²µH\Øä¬©k­÷žsŒ!Äqý0dA&:HºTËn ïûœ“ª’µ¶ÜJŽ@cJŠŠ޵„DÆsÆ{ª)åK©ªÊ:s³ÚíZx°.´äÝn›Rí}µ\,TõÃ>r¯¿˜y½^!bÓ4ÖùR(ÓßòïRú}-eÍÌ„H†M ÆÄ”Ê<§T••rJè½·Æ~Tå\’¬ bŒa¦é´1H€P×53§œµ-Û&~½ñ¦×Ø*EÕœc&VÍ ú4å¨qìb Ö:c Ý_¾½žO+öÕÅ‹—1f¥ÂfBŸû÷çϯ+g‚šÑÄ4°$ë,CbË©hŠ*GÃBœ¬'S4 hÞ·í¤ž ¼ù&#ýÍ_ýU³ý`ï|ÍŒ!EF%¸Y¯'ÍdqtÄl‰Q€&áÙâHÍíìäøÿá¿ûÓAܪ¯¯.¿û?Ü·ÝjgÚýÍf·ÉÑòñM|û{dØNëÉ|6Ý·ûÍzÇñîí[ó£ÓÕnXï¶³¦99:2ˆl¸†åÑÑÝ7ßÈ’'¾îÚq:Ÿ-OŽ'GqüðãOÛ®óôγgϨžÏv»ý®mCÌuS%Ä}¿c€ÙâX(.On=|ø ={öEßö}ßЭ³Ó¬ €jÜÍf?ãb>5¦ê‡€AÔ° ñ‹—ß|ôõ[§g7»_ucŸD*ï 3² 0 W°¼ï £í@!××ÿM¦žy?ckÃØ+ñl~ôêÙó£åé®ëöþG¦9~ãÑq®æ7W/—ó IN]ï+{³¾Þ÷¡©+d÷jxè‘üùªkƒé¥Iꦓ) ¦¬®^D^Ìn½µ ÃNã S Õјb$EÛn·Ùì·»¦nnä·»Í0öˆPˆæ!Œ‡ç»dñÞ#yÖbñ 6ÎgÐÚª®ËÓåÑn½*£Ÿåbi­»Y­ÖÛ-SLIƦƒ& ³äa˜fGž½ó9åq¨DÈZĤÖĘBîÙNæ“ùn}uaÖXÇŒuí­u¨À€óù @7Ûõn»‰ý0ö‘Ñ c?]ßëW/Û¶Ç®rÖªÇÖQŽiWaìÒ8hN9©(@Œývµ]]ŒÝž­™Bçüly\ͦAÕ‡¢i u=9ZÕÞ¶»æÄ±ªa:;=9}õâ…ädˆ8ÇŒæ ÏCÍåÔ-_ÅJ¨£ìW‰MÛ¶Ìv>[ä¬ÛÝNœóª2†Øö yyO0SÓT†@ ‹g˜ˆeïlå]ULkíÍf½k[$¨ª  Œbæeæ`Øëú¾¿¾¼ê¸S€aJÍ• ‹f:„BɆÉ”€÷à PH§³¦i*bŒ1¤º®­1”R.I!¨/"\–s¥bèº÷í®ë{dÊ*ÁzÇHMÓ #–Tîkªå- ªÖZ¦rML‡é¼¡ªª§Óé8Ž›ÍŽ™_Ó‘'MM@åB™Ó!qÄÆ4Þ!ÓcL)„ñêæÆ¶Î–ÖBÎI¢É”}V¬„‡®r\ûF¢8k¬­Æqû ªšÆvsƒˆÍdÆq>™îsÛvC»ë (1³ÉãÊh˜W^b¸9¿P¤<Ž)„qèn®7‹ù´öÕ|:évëÝ~c˜›¦™x?ä¡íâhØèØímÝ4MuqñŠH‰ôúæÕ®ß̖NjŢ߯j?¼Z•ÅÃ[o=´Ö ýƃ±JÖd¤^9AM<ùֻ߀_ðÑ“ç¯ñ;ßø¦¨yðÎ7•4¥$šËùz½ÉÖ¹Êû0ŽØwÓÛ` ÏNNBL´ QIQ»&Õ´®—×ûýæÓ/¥åb^¹zõòUúvX€PE_^­3ZP19ã ­§|´˜UU”ÑTݘ`2;ÐùRTa6]Lfóªi ˓ǟ^^¾2¾F¤²!C @X7MX÷#nÛ.©q“ƒ°Çƒä@²@±SX%”T™ª#çǘ“F‘œúa÷ÎGo¿yëìÖn»ÛµCÅ[Ïž>zøæl2ùÿ™z¯'˲ëÌo­µÍ1×¥)_Õ Ó0$B3”H‘1ŒyVL(Bœ¡@#8ÒýÀ€pݪMuW—K{ÝqÛ¬µô°oæµ"+ãfæ9{/ó}¿¯Ûíò㟼º¼¼ÿoŸ¬Žv›õõÕú¾óݶ­/Ï9§ëÝо÷Î;ç—ç×WëÙlYµ'ÿîùã®ï‡ýîåÓo,w»îéóg?þù˜Áû:&‰Ó¶jŽŽnMà ‘3„",¹ÌðTÕñ†*±Þ;dÖ”SU[cÔ¶MíŒqÖŠhUWH†3o®·Ì\ÕU¡¦±HéQ™8ÇÈtÎÍfmŒq½^¾.«Ä+2ÖÚêäøäέãëógV—Gõƒ³—W¬ØÃ‚¬V‹£ã•«<¨¦) ]ú(¬hГo+Iã`Q“p’XWUÞ Eæ¡Û¦‡~Æ=‰ãq‡õåùv}9{ç©¡0vËãSç¬5¤œ§n;¤i‚åñªšovë'ŸþVRPI•7aØm6×e#­È¥FF$2äœ1H*cdUgmažÀ!_¢$óò0 †œ÷ ‘Ã23uýPy_NöÂÆ§Q™Cœ¬3ÎUD”³Ä0CM]‘!D¨¼#‚i»ýÞ—u¥0g¬q€ØÔµöêj]/êÅr¹ÛïsmÛZ€µ¶ª|Îfe–ý~ÇœV+Ün7 ÐÔÍn¿!4MÝ4Î5¾ªýêhŽÌÉpÊ)F”bÊ-‹_ª*r|Ü4³¡‹'7ÆXz‚Â+½8?Ï™ ÉW ¤fÖ*hΉSÖR³«!0öu6›±Ö:K‘Jàø!süP_›"ÀdQaµd­µ„‰„%§$¢ŠÀš”àÎéëͦïGCÔõ[-Ëån·!”®µÆ[k@!GGUDF¢Å|a—m ƒsNQSÊj¬e0ìwÛë+km‡º®U³‚‰qJaŸ³4mƒÆŽSæÐ$W·„úòÙgd `ŠÃ4ìwë—·oÝ>{6.æÍ†²ug¬ÍiÜvÝûï¿ÿÆýû¯^½âœœ‹§‚(keŽ8Ž:›ÏgCSêÇQDWG«õÕz×ó£¹¥VMŸÒÉê¶µ~yjÜ¿ÿâüòé_üÇ?Q5í­;·~ùáϽŸÜZ¬Ž«ºuÞ!é|q"ªdH@u³Ù<„c4q•Ø !jéS™Ù’APÉ¢ÙWÏæJ¸Ùh  ­˪ÆWª’3#_aaGÆyŸ÷äHdýõvÇÂuÝ2¤ª›]w½Þ)eÉËãQTfvd,(sÚ AŒ¿Üvëýdë9jš:X+   K¼Tl™Œ•ÌdŒu†A8KwÆzCbê‡~·ççÃÐTõldéW>®œ¯›æèÁ›Í‡ˆFRRçî¾yÿá;TÏÚ)¥jy‹uvÚõÃ.#ÔG‹;í¼™‰à¯>x||r’c¾u÷÷¿ú®€}ôѳ‹ëLu]Õ4ñbþ§Ã ,“#S|‘Kt|6ÖXD"B[Y@Ì\²=UE•™SŽ•Ÿ7Me­ qªšúèø˜™qš‚di|UU8¥#($WWMÝ45ïTIX™u>Ÿ=¸ï‡ÿx¶Ýïž™¶º^_KN3뎎nß¹õÁ/~œÝùn]7IlHŒ£\^]¦ýþr³nËeÛ¶(œC,sð~œ˜IC@Ys‚2VäšR ˆS¿ßõÃ0ÓÐ)gg½1S†]'ÌÂ1ÇÈ9‹µv¿»Þîwa𬱵kgÍÀuc~}öêüùñbî,Mã0t;öÎçœ@´jgU]—5&.xœ) ï}‰…šbðÎ9?›bJi°Ö´m«ªÃ003'u]ƒpIâ>;»T‘¶©›z¦i»Ý§˜Â4JÛÖÎÙœcSW''ÇËÙ|†—çç‰9«x_uݘR,ÉJDÆZYïrþ4ÅèŒå”cu][k„™s¢¹ŸÏçªìœãœ›¶Ž)OaÊ95µ_.šºñDˆD•¯ÆqØn÷’Å##Äý¾ÇÉ9×4MÓÔ¾ª¬±³Ù,Ä8…IÇqHSVU¤*Î8WբдÍÉÉÉbµè?݃¾UaVQãµ–3‡µuU31¢~2–½¥gµÖ는™…ѤÆYcma‘"‚sd ±È8E­´h‡DUA… §£Wi\+ècÊ,ì,UÎ!çcžúÆ[#À í¬‘”‘¨ï{•윛Íf†pÀ.Æà rd‘xçÿäÏ?ùøÃïÿ3”HÀÈ)‡)†‰ gØ÷=@¶rLÀP9{4_ÕW»îúú:äœbTÉÖ„âM1ƒ—uÂ8 Ý~? ̹H€,úœy𢠉÷“6õ‚*‚ìö»qŒ¡ÜŽä‹÷–LÙWçÌ*jœ-rŽ‚²z ·Æyï–ËÛ÷¼ñÛÇÍ/ç¸=³îw©®üÑjqrr™_½‹‹ MRûZT%ñ8 œ3!lÖ[%1Öóù®lˆéòòz ÈZëXôuÍKDÎy‘m9±7!…cÉ)%@òUstrûý¯½¿Ù\¯¯Î8gç̾ۧ˜¥¦õÜA‚ý~?Mi}¹Þn·ÛÝ뺮ª<ŽSˆÁXRÜu{hÛÖ9ï}½X.fóÙ4EÚ™RQ¼ÙÓÓ[m3;»¼,S¬ÝnWæ0ú:iÖÚðY­V1¤ëëë¾P§vBˆÁ‹– RyÉ— bœ4€@xykçË…±&¦tuy•r¬*ç¬qΈ¤iІŒ÷~ !ÅØ ;0äûÝã_ãý‡ÈÏ çÔ4^)nׂ(3H– ¯«eÝH–K1SŒ³Ê¼ßžƒHå-3 1%öœS¨1Dï¼õÎÓW¯vûM{Ü\^Æ…ååËç÷îÞ?}ðΪ9~öâó~?š{vêÆ”CeÍgO>½:?·@¿ýíãÕѲiÛ‡wïHL¿þå‡Þûâ(îûYk½ë¼CæËEˆjÝruçݯ¦¦` IDAT~ã»ßù×´89šBhëÖ<¦1þÓ?þp¬½CÒ:WmëÙ®öØm/Œ«bL»í@g³™(컫Ê;çËÂÃ[9‹ ªœ{_ëëªîº]I$GS.[‹\¤ñEÆ €ŠH9±s˜ù ¨7ùÖYf†tÖ¶e©áJ¨µÚ´-ÊÌH¦ÈˆYB,¦\äœ) 9窙‘s9eï\Q‹7)‚"aŒ±mÛS]ùvÖ®ŽVfF@A㈌3Æ! "†8kŠ]Ö9豪šÕê¸0hçKßÎjœB¡8§ÙÛ§1<üÕ¯?øÇü—¦žýÁ·~ÿ›ßø=kIQ™ay|ÚÌ?ùA\Ç+È=ªBbI1ª0‘”‚‘9€ÂËx”™+_•jÔ)«ªÑ0Ó423¢†aÇC¡¤`­µ³³ä”`èú˜2³ØªµUSŠ!šùbÙÌÛÝõ•ã,*D4ôÝ'ÿv áþý;Ó8>ýüó!ä÷¾úÌâêæOþôÏw›Í§Ÿü¢2Øo¶û͆³¤„ˆuU5_ÿú;ý°ÿÅ?†±²®r6Naì†W)ºÌ–pÝíúXIUsæªä‹ÄrN!L";clŒ1‹Ôu]U•º(9ǰg΄ ÎCjѺnٙɢ0ZKªÅüyã]:¸ü!1+" ìv»årQ7õ0 å:¤ºi}ee1k·»Í4N1dažÍÚº®˜ór±˜Í›«ËËn·K!9c8å³gHh™5³”"QYú€*ûÊZc> 2m=ÏY„ÕYW0^ šoôïekTîïÂÃ1Æ”ÙxÝTÍb6_.OïÜÉÏÏžÅrއÌ?U99=Ê™ÏÎ.ú~ SÇ€`Y¤Ë¡]ŒqÞyŽ1r–ý®w>ø*Tµ_,d=XÃ"%¸|žý~7öC=«sά|h“éä^plÃ0Īª*CùÂßog-«ŒÓ˜…!EKÆ9wHX-ö]…~rfT¨ëº®k²$Âa‡qVDã]uçö݇ý~¿ÝÄ4¥œ*€˜¦iÔùðƒÿòòêjc­GÄYc÷Û+PVå‹Ò6µ÷U»8š-V_ýÊ×*_}üøã«ë1æøh™C<õr6¯U¡ÛMH–sFàÖÙYSo×—qŸ~öY˜çÌ1·U=NÓ¸ÎÓù7¿òo¾ñ¦#ýå¯~>î¯û®3†^Ž]ÝÌŽOû¾ÖÓ«OοòåwOO¾<³öòâ<¥´XÎŒu)³j¹D ‚©ë°K"@.s»:yðÎWÞ·Þ§9ƪi1ŒÇ•ùË¿ý{•‡‚{ãí·ºaºÞ í¯^–ØÔsÊŒã–1‹Kq(O¢2 (ZCÎç³´Ã0Y닆9•à³,Ê1Eã,aeBT2”3Óä¬ç”EØYWpJÁ.ªÌB "‚„•÷Æk,‘ѲP,cO¢›Å»5ÎÆ±®j"ìúNX¼s…TGÖ¨z猵EÊõðþý/¾xnËƒÍ  ÖšYÛ¦œr–§¬šYÊ™Eoß¾B¸^ïŒ!4´Ýnç‹ù­[·¶Û-%è«j×u8k‰Ñ ¢ŽÃ”ržÍç)çÙ|ö¥·Þ~xÿÁÕâSMš‡óŸß?žø)FV­ìbµ\­V«õ 9£jåPÆBJɬ•‡ä)UQDTQ㟵m3›@LÑ ã8 Ìœ9)ç,ͬ€½HNSJ)Å,¢Ó‘訯Žç§÷À:2¶ª«7ÞzóñÕ…¨Ô¾„¾ïLŒ§wîV•_¯×C7t»Íù«ç/ÎÎÉAeíúâù¶­ÿü³'›Í&&1¶qÞ°*«øÊ-–­d ™ãUešºó‹/Ÿfƒ¾jªfÆäÌ9eÎ)§dP%çÆ†œcL±iZBp„•AÒ‡‰³$ÈÂ’ƒI&笠7ÄL° Xì®ÖZNRý]KU]!!™‚ðdæaR>õò…!$$aÉdÌl6›ÍÛ#\œw»}Š‘Dde)Cg- ”)a}vÝQ÷ûÞ:g¨Š1•œ£ÃlšLÁ£ `)IJɬ7ò”’0÷ýö¿üÉúú % :k‰Lá\]^OÓ´Ûî†>d¬<)B‰¢U’×HHÖZ•l­µÎÀz½¶ÖÍW«Ù|Cس„iRÑõzÓ÷}Ìy¶hµ¢BÅŠ¡šsº1"AIÝ"CmÛXc‹- † Ìè­-õãÓqB(®tˆ1e…‰ C"R{ï«FXoݺûßÿÑŸ¼õæ[?ýé»ý0Ä^TTÑš Tó§ýfƒ7ê Ž–«å¬úàÙãЩHUWMå6×$[­õWŸ¿x•S´$cåLŽ;k1œ&# CJ¼ÝnOOŽ`ÖžU•'Âë«ë~?ଭ'™àdµì‡þ×þìâìùÓÏŸT.Ïžyç|Ý/oÝ}ðèòj½<:}ÿ›ßzñâS±ï¯¿ÿ£ÿúä³êº6Ö•U0”žPÔ{GÖUU;[“;ùö·¿sïþݬ9¦9owëÝ«Ÿ}ôXÇà$M¹[ß_.çÃ8Â0×ç É Ù\ÙJÌ…îî½5DI’JÉ×sÂfL5ó£z5ëöû©‹h@SŒ‰ ’r”"œòÖ¬æ³Ú;–¼Û)ôÊlˆ3³rfÕÂ@‡ª UE¢”…E&fR$ÑÌcO$ "h¬µ6Kæœ X Q;D´e—#¢E@K(ÐX3÷U}†±´%° ò$›n“™™UŒ3«£% M! û3NÈ8g§qT•ÝxÉÃEÊ< £±TŒq°·¾®«ƒ³Ö:»ß÷US_Ÿ]fN׸>ÿìüÞÃåjÙb˜†îìÙðÅÜ¿ùÖÛT25Õ•=Y?%¢œàôäö[o¾ûãûœÙk_'–„Ùbù&@ƒÆYçŒáqè´(g†iÜl׈h ëÐ9ï«Z˜µ³Ù<†8…3 €E`Q2nšÂ«çÏ>~ü9KΈŠTΑñEJoq•Ï9?{ú]5Œû8iìOOŽš¦éûa³Ýúª®jNië½ûìɯÒÔfcIS÷}Êq‡&°V™É&ÆšºF$(6Îiúý&JÎErY¿”iÑšÈ,¨ T Œ% ÈœbL \µ³ªšY_£)‘{œböÍÌ;›sñ¿©¯«Üe­»‰óv ˜StÖzï„§­1FT†®ëö;B–ÒRqñ,—ÜWBÈ)–œ@U”“u„,‚Æx"›Y,ÂM ïk:1‘iš¦ªªœó8Ž1FaÈ‚ªÖ ppAL6ˆ9%6†Œ±ÃTµnêiL$"ÆZ*„€Pd l|r朴ÐE‰Unß¹sëÖé8 ÃÀ†LIJ"g1‡ÔPUálŒ-ÒL(ÁåÅ6彇C –Ë©mšò•%ɼt¢Î$3…X`Å)L*š3³råkeX.æËåb³¹~üø7g—¯|e3gTB4)ÅÌÙZ×6Í8D@&>ñôòò™·†ˆö»®°ˆÆ¸u·3mãªjGCÒ8{ûô$£OQ´äBBª,D<¾uëù˳OŸ½èǘU9'é9 ƒ·ö;ßùö‹—Ï?ûü“Ë«W9…~ØZkEëª9>½å}ýÿî@ƒ?y¼½xþâäôÉo?®Û–œaPPaä̉ÙBâÌÖ¸åÑ­ÈußícŒIy·ÛhÎÓøñ/ö‹~/籓Ãtqöüå«ÇÇ·gËcpÖ 6„¨Ê1)’1•…5¶UE¥HVBDLÓN²jN™A•KAi¡l8P‹¨T9è;_~¯ª«/_\O¬ái""ë(—êì@µBÅâI *ÙÆŒ™……ÐZ±Fß~tïë_ÿV7Æïýós˜ˆ„K«)Šw"ƒD¥r**,&e8ô a°Âª@ Xè‰r°.2€ZW–9VÇŽˆ¼pS Ž®Ý|±hTa·ÛÉ0QcA³0§X§YÂ~Ú)k$4†â4N;6†š¦™-æëÍÅ“~{ûô4týz»»óàͳ« ×Ö•¯j€Ôí¶O_~ªPY™S ƈ°µÆ{o%ÇD¿“8c (çièsжrŠ8Žõ4[˜~v»]Ó4óvfJâsY$Çä}]ùjF$J¹c4c^¯· ›“<=>ºsz§ñ-ÔÞ{Ký~Ç9Ê8€¤¢Ökˆ ‡Î¢lú]ãÃGo×Ö-žœõûíåÕùõÕ³<îkȘb Aµ²®(+2sFT2λÂóÞzôgQBŒ¦D¶¢2Çæ!•‘YÊVÝzxpÖ°æ¬ì­]-½õÞÕzŸX• 9çæ³¹±vúœ“µf1k7ëM“µÞ9ÇÅï¼_­VmÓìw½–42oMÎ)Ơµ¯¬!6EèÊÃíàl* ºª«Ê-—Ëåb ºé†!'ž¦I‹5âpÄ3úÊ#âˆ#@IÆ5’’¨Ìæ³”RA¹ªj™q:C@È)•¦¸ÔûDX`¼·oߎ!ºqŒH ª()E,zÿ¶mµ*J„VEúnG„MÓÌ‹ûîUÞoÖ›'€bDP…B¥G€Ê[Uc,£C8à2 ¤³4¯-µ†{‘º®ïÞ¹}ûÎi?L›ÍîâòJñå6¥Xô9çýþ*§< Ã0öHxµ½Ìˆ0Ç Ý!g)yé»M?n´n–ó%"t]¢Î!ÍÜAju5 ý¶cgÀW^™Cóa+@µ¯„ùÅ‹ç1„>øluûþ›U» »ý{ ä7¿ú•púÏßÿ^LÓéé©­£t×qš6S±•õ—ë«Ó“Ó¿ø‹ÿsˆ1¤}¿¹úÑ÷~4««GÔMbäÌ¥:‰)ŠheMœFfE¢CD@Ç09ïgíììå³ß~ð‹'ü! }Ÿsæ,j|å,m×e'GÖÞºuë·ßùìɧçŽLÖrýÃô;.¨ Í´1ÂA"emyt).)pS=XÔ„3ÿŸÿ³±rD"$fuγ0+ÿÎ^/Rо¬EÕhÛÅr¹˜ýý¯Þ»û Ƹ\.ŽNæñK_úê/.þùŸþI9€€ ÊM‚vY1³ªÎw-`ïDñZ,oRôß7Eƒ*f‰}z ­+ÎMŽSUÕ ”bR Jd«úð”õF àŒ³DH ÊÀ1MC°š’QLã8íÏÃÞ;¯ü;oÜûôé1ô½;Ÿû£Õjè÷×»«)ô.ÂñɧŸ~ö”QŒ©|k1©òáù”œûû®SÀz1s¾š¦§$Ã4zïÛÙ¬®kß4~šD!„Cô‡sw¯íæÓ435YŒÁœÓ½;÷Þ{÷k_|ñÄz»˜7)É8t‡|wãÈYo­u΀* Àb¹"¤ÛwîqÃ0Îæ³Å|vëôøîÝÛ¨iÜ]ç‹íݧ á £UVåd0Â4R<\åìsΉ@æLˆ@` Bysf-&,cŠVµ« Ñ \___oö³Åqqˆ Zclå+DÍ9-)ÇíFTÉ:ë]­@Ó4åœÔIå}UU[ÙåœÊ”ëªòÃh ¨P6XξR̾Î?ÉÆc¨òõñêx¿D:U ¢rW•2E,D\-—ÖºaE5Ä9;ï›¶­U©ïbLa c?8ÛZ"k-"–¬sŠw Qi±œ_­÷Æ¡ˆ„0©ªw–9# "Š`è}SH™e,žRúîêâ}UŸ³ÈõåUŒÑ|a@xœAä̈¨7ªG@–Èáf ¤ÖZȹ€}Ê&¶¬*ï  ôÖ­ÓzEäàží¡‰1¤¢)å¾ï/¯/—«Åé­“®ƒ”r¹ bJÌÙ¹*¥4 CŒÉ Ùl¶ëÍÖ—3!JQŠHrJã.WÞV­w6%&È1L!Eëîë+f&ŽiÛ÷³¹Û®wcÆ?úã?[-W?ø‡¿x×$±§·NÞ|ãaÛ6uSߺuw»»zþâ‹¶i¬±Ì|v~Ù Ó”ò­ãecÛ¡^ØÚ©q%®Ã×õÁ1Àrt´:=9:{uÎ d(¦˜~ö‹Ÿ½÷Íß[B‹ø×ÿñ?-&ˆ[éR:dÔKNûý6³øº%*§žn®¯; }ßOcÏ9ˆB!µ‘)£€t} g@…¬˜Ôu€”n†Árƒ/Õ¬YI²ÁÄ™…QÅ¡5†P8ŠȀ(ç²-™¢À1¿ûðûç6Œ{Q}ãþƒ;÷N¯Ö— üô‹ÿïÿç/×»ó¶nA5§\nŽbQCÀò˜PT©h ñP©pÁ˜ aa™1̆qœSé±sŽå·HDÖ»J™C×9ïXaš‚.œ5Ö¢4䌫QUJÞ¨ ‘ñ¦ÄC׺if³f³Ùì./Öç÷¼„_þËO¬m¾ûÝ÷æÍìhqÔ¸jHlÁ¢± $[cYáäøö·ÿà»V@€J*æAt  ùæϜǮ—Fi^TYĶ÷Î:‡D˜a4†šªŽ1†P€ŒacMI—&D Zov ôÕ/ rœVÍ|³¾î‡n»½öÖîͬ©‹…‘­Lã4ìŸ|ú8„¼<:¹¸<Û¬¯Ë£f-ùjá¨x@fŸT„S.¢ D ”qèBœò Å¥ª|]ÕHVBˆ)¥²Ñ³–le­u9'ç,"—\V%㜯€PE/ÎÏ_]5óUÝÌUUeÇiTÖ›õ8Ž9³1˜R2䬥nbîûSâívÇ9;kºýΉd_ùâ&5ÖçJÐÇaì¯t€»Ii%ÄÉ{¢œÙ;gÈXo¨ïºÌ¬ ŠPªýÓnßU‡ÜTÕ’ýc*ìšC&WKJ¶Î–ª–I=OSã½_9ç³(2E´ifæœKøQJLdŠêÿõ)¿ßmõèä´mšÕÑ ú¾1Š0K&¤–vº+û´C”‡ÈëH“¢üADï}ÙÄŠ”fCC ÛÝn £(ôý4 £uvµZ½ÿþûOž|úé“O(¦$"† "Zg|åJýˆGË£¦n¯¯®Â4¦”áÞ†­A•,œË:z{9¼ù",†D5Å"×ócïk¬(Ômã¼m›6Å´^oŒµÉ¢‘œ¦aCŽì·×—?ýÑ÷ÿú7ï¾ýnNº˜/»~wu±ÿÁãïWµI9³$ca·Y«hN™ªº©œÇœ€áÛßüÃWç/ÎÎ/ÛÖÏ›¶më¦6!L!LGËÕ7¿ñÍõõ?'ŽÎÚÑþá·¿»š/îÞ»ÿgÿæßþË?ÿ½uP•ŒqÎ:_eᜣ—âÈEüÏ)ç˜RŒ1p.ck(3Q$%ï!cPU"{bcJ¦ *ü; Byº¡d“1°A:øƒU2§Ò–¦"÷*DBTP…œÒÇ¿ýÍíU³ZÍ×›íÖ×Sþ§ÿùßݹs÷éÓ/^<û!§4º ,;ä§Áa¢Šrx…‘UÔ@©xQ•U‘à"+B®²dV¾n«:XcrŠœ#‘©ê†ÀŽÓdˆš¦i+‡1ŽC™õOóM;+'(Tʶä¦X¤.)å®ö]?±l¶=Ë™ ‘ŒÆ)gqè,yT ‚eüŠÎUœõÉ“Ï,ˆ8ëS'8üðåµ·Ö*‹Žã„ílnÈZWv+¶à´Bˆã™sÛ6)¥¾ïºnï\e­Ä zÔ¨°µöêzýÑ'ɽ{§³¦mêª©š¶™Ÿ½zùëßüò·ÿ:¥)CLaš¬5¾],ÚÙl±Ù\cG¤Ç'«ªª‡¡[-Ê ÁûÖÙº©ÚÊÙ˜Æ 1‡nwqÞm¯ÆÝF²”av}7£‚8çêº.A9%a¨½­ŒqÎ甑¦)ˆä×D_ïý¬®W'·–«eÊ%é9)ÇPµ3‚B;)ýMa,éïD(Ì*b™3 ™Ëã=Ž¡°ÛONNv›MóC"³æœ½÷d­Üôq¯ xB*P 5`µ@µ«åâÙ‹Æ8PÎYmÖé )—YŒA|]+@fžµ­ˆ¨(9*Ùf>+;¯aJ R¨‘qXDRŒ³åÜy¿\.RJ¨àýq‘!3K? }?RNœÒd­-@(öbŸ†ž@½sËÕìèd™sŠ1å<‰`Œ)'ö®¼€<ýk¡dyî_ÿ{©ÜU5çÄå`­œ»}zúðÑýO>þ¬ãžA”s†!¦xûöiÁ0]\$ƒh,•8ÃrþÔM}ïÞ½0]gû¾¯*_ùŠSö®"1)”9/¨°A¬êÊX3v]¡ˆ’uÀ-NNNE$oÓõõå4MÓX²*ªã4efŒ«ëz9î®†ÝæKï>:>>ònùGôÇÿæKï<šÆ®ë¯ÞyûÝÌùÅË/RfƒhŒ9Z­¦)pUAÑ?ûÿìoþwgW›ë”Ó|¾:>9™ÕÕ8õ1Ä¿úO5NÁ5MÎIÅ â4M9çqŒ®®fMóæ{_ûÛÿ÷?ÁØçÄȪ(Þ¹£ããÝ0(ˆpŽª„Ur’ArÛ¶MSÃ@hÁcœ«Œqˆ Z”èRÊ”×™6¢ E\öúÚ¶Ž2³pñ`™“”î?¦”S.ì)öcôÖ‚K.h?Ýí·ù×*"süæ7¿ "»Ý6„0öSS·%~ ã{DÐ×um±…—§«LfKë|HI@Å›ä†B$¢ò?µ¬@ËK…"Šäq쬫ª“£EJi·¹Ìa*¼D-:_7ÍOðZOV>±í(ª”اÄY²$Yã½'CSH]×O!˜RÛHÞìÖ1ÆÓ”RS7d\ÛÔÖZ¨Fhé³§O>ü«ß|ðÃÆûåòøäøî—¿üõãã£õ¯ÿøÞƒ{?ûÙ®¯.vÛ]J²:õ9§—/Ÿj]·œcÎ)¦M¡Pšz¶¼}롯V_ýÒ—ܽ½¾|Õ§aJÃÓOöɽ·’¢]9èb>oê*礪Îh“dÀz^7uk­÷Þǘvû]ˆQŒã­sÎ;@4Æù¦©Û¹u.¡*cÊÑz-3•Ä(‚Z ÀeÊ’­qu] CùsN!ÑÝÛwÞ}ûí‹‹óëë+å\VÖA‚1Ö›4Ų<,=o â¸ýÖÖ"ä,Û]7 }ß÷9³µÎ[×5B˜RùúœsŒ‰9—»¼Íl–SÜl7šÔ‰UBCd³£Y½¼¾Úæ<0gD0Ƅכ]?N³Y3_ÌÛºFTCä­˜rÎ)çŠS6Æ8ïSN!ÅÃA,RTê]·çÝvµ\Î kM[{ëÚqb☸› 쩈ªªù@DS 9ÌRÈò‡s ¬©qÞùÝf·Ý¬«ªiê:¦„ë«ËO8ß»g;묫–ó¨‚D”´xÊ6ë+ͲXÎC:I™™³&Dïª/½ýåýöúÉ“ÇÎhe±ä½#k$Ga5ÆL“ !áz}½Ùl»n›Ò¨")egmå½%Ch-Q‰žâxÿÞÉññì­·¤…0={öä;øûû7urzçähùíßÿÎÙ« Md  +/—«íf—×µ‡ýßþÝ_+ä¯}åk¿øðç××—Þø{wî-—«¦®¶ÛÝ4NÎ:TPæ2¶ëÖÝ4^m{kíêˆ'†z±ÚíÎäà»¶)Æífœ÷‡jìÑï¬=99I)½Š@Q%ûª9=½5ãf³æZ€\âº\1(G‚”¼$2Dñu«Š°7€rN…[gæ‹öµ}ä&òE±xsòÑ4ôCOƸzn8繡ûîvÛ~èÿýÿñï7ë½sâMÈQ™É€Îy(TÅ2a·Î•{ ±ã ßL†è @8àKY[Ÿ×Á#¥¼@Ή@rÈáé';C„ ¨Y%‡‰‘&"ƒ£3Ö¹ªF,Žca*j@%"oÝìtö•÷¾tqyEÊÇÚYï*ïŒJVÕ”YE igUUeÎ1¤{wîÛr5%fë’)¼rïjN™ma@*¨¨³®mfYEÆ1¦Ì»® !,s2¨*1%5†¦)L*‡`¬³MÛܽ{ç;øíîì—W/·Ã~?ô›gÏ?ÿéÏ~8Ÿ-Û¶™Íên¿á,Ù8õÊˇ÷n}òñÇã0k4g®›ÀLÓHdæusÿÞƒ·Þ~oØoþæoþÃúâ|q²r3îÞ»?ÍÛ¡Û¥išúT!õÎÙ\f@ƒ0ŸÍox&giÛ&¦  ÆYë¬äœÉXQМsÈ €°Ä$±ˆµ¦¤½8g)±0LÆ9Dä  E8af³¹oš¾E¤ªkf!ïmNq– @†¨±‰4ë!ôˆ¨å)‹Xyg æ³Èååån¿U¸.:çJnNÙŽ–÷-çLtÈE¨«Šˆöûý0 ‰3*8k o-6u}rrûɧŸvÝÞÚå8¥T²ÓbL*°\γ*CÎ*!%ÑC˜T[”øÙr»”1zy˜e·ÛÇÄ{×¶m]WUU‘qMÝæÄ7ËËœ~W\´4tÃ{ЂP´ÖÔmÓ4Þzfîú^UDQDT8 ;×c¦qêúþòríœ+‹¢×/g¹Â$•5MÝô}÷èÑo¿óîÏÿå'Ê™H§0¼:{•U‹EeÇ” IDATÔ{ dˆæó™sU]5Ã8Œa†ééÓ/ÐZUHi¬›ÊZ+93«ÎW·BaèËå7¿õëÍååÕÕgŸ|„h!†¬Â9íßÿ껯žå¿þð¢Ã÷ðOþî{=úè£ÖëõUº`GõŸþ鿹¾>ÿÿþòÿ2bÏÞ»iê¯ÖßûÁ?¼ñ௼÷•Ìb¬$)(Ëòêå+V}øè!³î‡þG?ýñÕöÚ¨ Cˆ©ˆS+Ï H )'RE"@ØívÅ[T$‘9qž8GáÈœÊWù»#è!ì·4Çe¥Yê˺®Ënþ(%àåÆ$hn¼:H%êóuÍDPôŠdbà«Ëݳ/¾xñür±8–2äVE4Dr²—Û¡~7†ERæÑ#À²s(±hlˆA„4u8 Láæ/ AQo]LÉ9ˆ1„œ3€˜òÆk˜…c½·*Ç „P EcŠ!+ªŠ€¦˜vÃ0Ę5Ÿ_¾¸º>{øè½ßûÖ7®žôÉÕ§K;‹"œÃ4¥”?üðW¶EÊï´Ü®Hd¬5ÀLÆ™¢_6ÖùÊk*Èï˜Y«’ ¢‚P×UÎYä L9 Â,1¶"RÒ.//DERéÒvû³)TÃX g8`ÒÓn}õËŸþxš&DEB0Ö@NÌŒ,"iºóWŸðËŸž}á­hJWkƒÖ ªs¦rÖWf.PCBU9ÐÒES,ü´ÒòJIÝlýLQ@9%ASø023KIx@<ÈDKß™Yû÷r®ÝÜðhÏYˆŒ±.³BLYÛù»Íf§}·Ûî¶"Œˆ êÐZg‘¨„°Šf=¬“ʦÑ•”PEp•³“«*_0E?D…+]6®¯…SÌPö<ö]? ˆX¹ªŒ¿…./Îv»½¯jkËsDÆ9Œ1¥˜ÂRÌû®¯œ#$ç,ZB˜¦ÀY 3g惺èÓE”ŒŽ!ÅR ÆØ¦ꦮª:‹îw}JLÌIDErs—ø””3¨ZëþÛ%Ä4Q)ç”ç³yf1Æ‹÷ÞX“s§1ų3€”8§ìœæ0M`›Qc SˆM=SÀ®ž½øE?ŽËÙŒYÆ)8›7û}âl -ÂÚ45g­Y,–®®ë†ÇÆRLδóù\”#„ d—«eW?ª¥ëû““Û¾jw»]£µ¤Ê*û¦ÿýû_///†iË2õƒ:òãÔŸ]¼dæ˜Ã°ïœwÕÜúéãª2M[Mq¨*[W•ÌæÊy½½У“cCNoÆYª$I€tìGk ­ý8ecŒ'Ò”T²L’„E çU¥¢ÌÊ¢uU!˜œ" ŽÓØõˆ¶umŒ‘Ueúç_|f­+x¢ª*Å>YFj,Z¸ÖÆZ‡HΙ‚×Á^"DXˆ…4>Ž#Öuâ‚xÈ f˜b4Ba"ôÎ1Ë÷¿ÿ£øû$cÛùòÑ£ÌòùÓ§Ì‚ ÎÕ%Ó(¥ ‡´µdÈW-g‘ÃY_"GÈ ¡t9t1–"ÙkA²Öæ”ÑàÝ»w‡¡öìùl>7Ö @ 9KLHTUM9hc’iꌩ¼Ç"ã5†Yö]§¨uS·³z‚ yüä³Åò¸jg»~?†1sÚl7×ÛMf6ªh¬rÇ‘sjšZEú¡·P©Î ÒaŒD€ÎPk+?‹°¨÷U±Î*ª«k—¦××)* ‹’±Ì,Zq ÌÂzvþr¶hDZ¯ªªï²Q4ÖxïË“þ#%DUÍ1LŽTb Â¬ÆØh "UU=…ñ¢Ûo.ÏXÄa»A1šK „°˜Í,™ó8öpW–Ãz‡jç¬5‡E¼Ö7¡±Ö9„ ¢$¨Å¶cËŠóõäŽÉU”›³"ç8ˆpBQï=¢A4SLÉ73$Ì"ÖØœóz}â x³îÉ9•q_Ê)rR‘ò!ËPÒã½/`§œ3¢6u}zë–ó®‡ÌÉšÃäŒ ! *"ÜX@2çra˜BhɬV™ó‚šÍ+ú>Æ 9DzÉ4ÖVM=MSb‘) c&BC†TÅd”d ƒ1½ö°(TU•R.<"*ˆDTÅÝv9Ë8ŽDv±˜jˆS¡ïf}ɃUÕ”rù%ÆñhÉ;ß÷³¨jAUc)ÝÊ*-ådŒ-5Ž÷öøø¸ªª««Ëqœ¼sâ½÷>÷ãÏ_®7›¨¦Äª˜2×UsëöaاnƒtØÐk Æ”Bˆ"ETIU¼3MåÊÄ6‡h ‰ ]o6o=úòlÖþÿL½×“eIræç¸"eéjQ­€à’¶#±”Kñ’„íš­i|!Íh$øî,°f03Ý;=ÝÓ=]Z¤¼úˆˆpw>ÄÍÂ>VY¥eÖÍs"\|ßï{ñêÅ›‹ëÉt×6ÍrÕã`- 'Di|ñúYUU¬ºë;a1Ö,Ae>›Šæ~·=´Ç‡“ï¿ÿJÜnWä,)W¡òÖYK°Þm¾ùî›ùäP¥ü­w­õ“í.LÚIì{ÎzÐÖòþ“ó¯~õâÛsðÆeäPW³ùŒÉ™Nf¾÷üùóåraˆrbѬ\Êœ"â¾ !0ûNCf¿AUPQµ„ˆ´Y«ïmæ\B!xcHD$¥$¢!kA«µ)PáÈ’'ÓY%áüìMÎÙ[«¤H&Ô-kˆŒµÛÍÉøŒeD0† ·üøøèî{ßþîq +^¥¢(}s×õ)Eë,s6@,ÂK”˜u®iÚË‹ Î9G$,åÇ*·þ>ÙÓÔµµ¶ª›ò”8ë e­ªÊº°ëvE†»Ùn ½@7Læ4¦ˆÖYïݘáÞ½ûî¿'Žnݾ}zg³Ù\-XdY†Š•½5¨Æ’Í9Yë¬s7‰ä¼÷>Xç»n({á˜bÓ´Öz-³ BµÁ5mÓí69g2h+ qc"iJeéâCðu]U³ŒyüâßýrÛ¯Kl­3dŒµU]‹È.wXÂmU¼±µ³µ÷"Òo×1ŘдíÔ9OH’ójq„ÓÉ$;v»r·—–r,‹wŽSÊ9sÎ¥yÉ)ݨ²xË’Æ>sF"C‰Œ% 4dꦱޓ#k ‘œ ˆ5T>‚ò_Õ"À(í’ÛYˆ]å VW¥”RJÅÐb­õ¡öUpÞ«ÊzqÕw[嬨{ýY:ˆŠè0 ªbÉ °(3•åÃ̆H„û¾ÁkCðÆÚ~Jè("¦TQÿqMTlM1F) žÈà½oêš§1!‚µT7¡ÌyVëÕr¹ÈÌUU1ç”Î糓ÓÓË«ëåfh8å}ŠwRoŒ%3Ž= ˆÄ,9³!9éÍvÔ9ï| $ï=s.g±"*K!‹ˆ*ï«?4P`œå6*wÛÛ­"ÊjÎXeï}‰=¹Y}—¹–8ç‚÷™­sUS2¥c%k¦³öÁý‡Ûíî׿ùzŒÑïœE…;·îl·›ÍnW7õ­[·Î^¿@J/Ç,)ŽýcÊã82K©ŠDŇPUU? ªDˆVÖ›õ—_ÑÑØ@dw}2fÙ½µ¤# 'DX®;}ß­v+묪ƒÀi³¹Í ºZ.W‹«««Ë*أæ¶®cJGG‡m;yñüEìÇ-lêj¢"óÙ‘1íü?ý›™ÏϯœwdLSͼƇwïðær³D‚ÔtÛ-8gCí½O9}÷í·ëÍÚY£¢v¿/e)ê@gmæœbD@P(²S¬nÖ2«HV„›v™sFCÖcHÁ”ùXyn0¥ÄªUÓVU3Ä¡ªªª“*#QJ YQáp2=<˜ÏBQùÂf³Ym¶Ìê"ïýõj­ŠÖlÎ9&Þ®×)åÙì€UCëõÆ…PfE0æƒ4¢9FF²d‘ÄÙ›™ÇÍv½Ú¨HŒÉy?;8œÎæg¯Þì¶C`È\__ï#3Uððèðƒ>ª|ˆ1ã9“µ9ól>o›6Å´X,×W¿ÿ~³ÙÜ»w— ¢ú¦­­µÆXãòzuë(îÖÛùÁ©d©ëæüìu×÷@¦ø¯œsDãØ ªú¬‚¦µhJ)\ª`íÑɱAÓ÷ýõb© ¡ªT5匆˜¹ªë{÷1 c2.¨‚‚UkmÝ´í|~r|4iKôàÁ½‹ï²8n;B*ª¾¡ëE…svûÜ5Ê1n6«ëëË£µ–È’q¡jO˜y¹ÝL;'×L²D)±DeØŒ¶8ñ d¬u¡žÌT8gfáœ93ƒ‚5¤9õÝv¶€¹¸®PbÎy³C3øÚ‡ÊŠ¢¯= r’Â(‡‹)f$"QÍœ‰LÌiŒ±®šÙt>i&›ÍöòêÂyODλ¦Tìõ9 ÝNrÜ—1 åiP„2?aI„‚'¢qã0Šh¹6˜¹¤DYkÛ¶%‚m†^@o^›’<•‹B²@"11¥Ív›8[çJ/¼o«&Ì«ízãØN›I9¦4Š‚1eY a‘a†¡çœÈ¨!$T*#"Et!ÉKNŠ/º´7)1ˆhÊozclÑUX瑰䆋ˆ!´Æªò^P¦ùeÛVØœ7£ÅÒá@‘ЪwÊóÆ™{ùŽ*¬V믾ú:xS¬ª:H¨«æp~ Ž0Æì}dÉbŒ%cAuºÅÕEî{Í +¹Wˆ4ôãvÛõãÈ"™sꦪ°bN2`R QÐ Þ{窘9fÆPùÐÀf»6Û¦Miˆã•#Ƕiþøÿ£Ûwný‹ñ—Œ¯Ÿ®2Öyï}L‰U Å$*¢PÕU]o¬'üðÑ£/þ–ºUÏ)‹jR5Æ eu†h­EŽL7 «õÒBå| \51Öïv£¢úÉAÓLÑÉUò0žœVÊ9å˜yŸâœŸ´-3ÆÑWž9å~GÂÎSmƒ1žSv†n‘®–ËÕ…³¶L€Xö3A ÍIYÙT¶­g)q?ö¸ƒ* òÙŠBiîRŠ1&k¬! ¸÷¼Bas2CŒ ’â°ÙlrŠÂ©0óQ³ˆjÊY¬q*j ì ,Qð^Œ1ª`ö¼²ÁçbeÇÑ:w0?èûÁ{ï½K)9O¥¶rÁõ}ŸRcL™µÎÚ"aëºÎ‘)'¤s–…/®.»¡'c©®ëaEYPV›Íf·+®E"ªCeËêI1Tuuå”ÜTcLræ”sʹÜÙÌœ3³¤\"û‰–}xì ¾õ=š·Bø·‹»·âÑrÄ—<¨:k8S á)©µˆH(ϪֹqÇqlšÆy·Ým_¼‡¾GĶmê €Ø÷yÆ—/_NçS"ˆ±ýöÎñQwõAY4&É9öã3{ç šªžN§m7&Eƒd¤D ©ã«jR°‚õäLxxÿa·Ûž½y£€ï¿ûîz}}vþ º¾³Ö¯·ýaß}ôÉû}òáÐmñóŸÅa;tk!ÕàµÄ9YëFDXõÆ\¯%¨Ö†RÌò»ß~5Ÿxb[O¶ ³ù»wû÷ÝW_þæ‹»Góùdš5³dPDᡈ̤mŒ¡”r‰.jD`攳µÆ ¤œTá÷ß»}çÞÝÛw6«Õ/¾øb»ín㮫EÅ8wzû cßrÖhð÷µÄ1Ò~üOž>ÍYrfØÛÙðö­ÛuÖ›Íßÿò—Ûí&TÁ+,ïßÿá~4›ÏñÅ/˜E”5H$ wïß¿}ëöj¹|uv^¼lÎûS©xꦎ16m{ÿþƒ÷=ºº¾f$#D‡aØívDÆ qU¸}÷ôч¡8ä”3‘¹¹Ý€ˆ¼R6¥^!C„)ecãÞyøÎ‹/ž?{êŒL&¡ò׋ëÅrAÖkYX¶bœkš¶­'ÞºI[ûÝoß}ôñéí{Hd]%¢ÎY ˜òH9Hc¶ÎÛº®Ë –sväµ”9%ލ©+KÇÝnGņ Ê,F± ­5&Gñ¾òn´Î9çK᪠l °eŽÌœ‡TšUÇaGUá\"kA„‰ ¸pãÑRÙ½5§”ü&¸ö¿Ø•±Uá½”û ä‰›}(.ÃÀ,äÜ>™àðððã?zýúÕby½ÙnbLžÜ[wº®†rNW——"œ2vÛáÎñ)`Ã’ú±ïú¡Lg“º©ƒ!TBõΉ`ÛÎ|=›Í»¡OY²2¹`Ý$±d…¦=Uµg‹Á’ŸßCº=Œ1Í,™ƒ¹DN¯Ï.ªþÙúçꌼxqvzúÎá|öýï¾–4 ()£ ªÜív*‚DD(¬9Z ZëDÅ:;iPÚ®Wšwß}÷³?ø“z:_]-È»<ŽÇ'·×gU¨~ï‡?Z]]üîÛo ȳ$§s”¶zç4 «°0ÜdÓ”˜?)I°!Lçs4øÕ7¿y÷½÷šÉd½érŠÁWÖª¦‡ql¦ÓªÓããù´ùæÛߊ(4ûñÖééyÇ_ýêW)qÎlˆP‰Ú¶><:Ún×ÛÝVª¦1†ÆqL1}ûøÉz×ݺuz||zyuÅ9jp®Mî?x3ùå—|ð¡1v³Y–gÀ‡ pô¡rƾóNU×ÿæ§3™N}ðÖ9Î{’]Ò(ÌÆøÈñƒ½óþ;OŸ>}þüÅÇZ”~¿Õ»™µŽM§H׋³ÆS×õÝ»w7ÛÍbµzðàÈ>¢ è˜E¤™´dÌb¹æ,Öç,–«>üì÷€ì³—¯ê¶L¦ŽÌ¤m9öD0fÈq>:|çƒ÷?µ\BÔ)ï*ËICd }Ÿb ÖB‘\ÆÔÂJdªª¹8¿Œcô>”Ø"f0ÆR!‘ª,×}ÌóùQßwÏž>ÉãhÐ8ç ½¶´Þe2žbL1zォ¼oÁU¶™™ªVl«ú¨™¶c”Ð6Ó;Þk¦¯¿›õµT„$ç“ÛÝ‹—¬«¬§×»ØÍ®ÚÆ$ÖÀAc«ª™ÿÙŸýÅrqý«øé³oŸ^]]g_µ]öÄádz:™L­óGÇd1öüâ"¦Æ¿SÜñÆP±Î+-fzØÇo–ý§µÎZ;¦ñòò" wC§ ï\ÙĨ™cÎ.8‹Tl“eu©ªÞ;U Î…`…³ìšsHSNªÈœBðþF¿¥ ¥¸Î™µ³YU`),âƒó¾šÌçÆø!%i§“õz½Ù¬”T:ÞBÆY_NLç]Y§§U‰()»kãŒE¬B8=9ª‚÷Æ:ç¬ ¬%ÀK$ÎY2@•K¶D¶ šbrÞ[ð™EDbªw»Ýv»efïC©Ëw%"-Á:ûœçɵ}¿ÓΙ‹Œ¹à.RJoýzªZ”Ó7ÿXø­W Èo¼ó·NïXÂׯ^׳ڑ)ʹP7ÞU̹*Ô5ó;ïÞ»|õìYA«µq6ø)"–,ÂÛ]¦jšj:¿ûðËËËË«•$õY´Hd’Xð PÏί,²«' ’¸PrÔ´Õårùêü¢ëâï‡ïß??¿ºµ Æ2¯y—™#¨–ä ÙóvAE4%E0çó9¨íûq½ë¿üâ‹×‹á?û¯ÿ_9Mê]8¹u·Û,(ƒÝÅìš6§qèFêÆ€œÁ¹ÈÌ…åYŽ‹”è^¿‹°÷ßY릳ƒÌzq½xùæM;?8½}çìüšcBcXEU­wÁÚn7Îà'Ÿ|bûꫯ˜PYNoÝþø“Ï?~â¼ú>å\°š™•Cdír¹º¼¼Ue ¥,‚Šèùõõb½U$‚圊òíújñìùó¶iÖ›M™I()ÝÐ÷šº…§ßy~1ŸÍTU½u#Kð¾®j2´Ûõ1ÆóóóÍnóúü‘À^ÀY^(Z{`Î*JxÓ€ªzëNŽOšºýîñïêºvÎ¥”8¦£5¦ Á9ÿÁ}ã¬{ùúâò UcìÓ0nÖËn·cÎWËõÙo¾¾xóšsÞ[Ô À1Ž1‚áÑŸøè3+ª‰eO W݋ꥄ}'‰±Ûn%g_W ‚dš¦1Æ[kû¾¿º¾kѲ†ŠÐ­µ†B¬ª "œSG¾bÉ)§7¯^÷Ûn‚%§E+(j ¡’œ‡qDb4Æ7¦TLO«f>W“8K¦ÓIUWÛÝòó?2=ŠYAa½Zõ»^l=dð¡mëf×Dz}ùüûa³†Œ)d!+¼|ùì_ýËÿ !¿~õývyé­»uzçèøþüÞ{·ïT!´mÕ¶íñéÝãÓÓa×ëõÓ'O†Ýê³ßûd·^={òý·_ÿ:½1–¥`Çö+Ñ µ-Ô`"Š)Žãh Yë‰lfáœÇ”À!²ÖqLˆ4›M¦ÓiU‡ß|óM]U˜sÎ)Ð~¹/JD)Åqˆå…Ê™@‹§ËíRUÆìû4މ™‰òz¹:??w6 ¶¡ÆV€sßíPCä½sÎ ƒ÷>„ànÊ[æPË ¤„YçÔ™I¨,"°°æ<ÆÄ)÷}¿Û®€ÐªBQdYcePã­³Æ qtÁO§3ïÜ3€Æ:,̆›Ä”EÊñáœ-G‰5ÆV2 ³ŒãXf¦Ófn€áFŸ,{1~øHJ Uêº.ße½^ÿý÷uUY21Æ¡¯ób"97™ë*ßt]ß ‰™ÙTvzð£ú§×õWyqYë”d¬LFå~ÈLndD2Õlv4?¹[Õ5îøôn3=©gó!馢9ötr|üɧ·ÉØŸñú꺶“'2hEq¾šÎµ3øb±nB;;ÐùmºþàøøŸÿ÷ÿÝÿ÷·?]/Ö—oÞXDr­d”<Æœ ?úð£/õ«câÇøøûÇwï½7æ<ŒQ±'Ô"]lVY!TSβ¼¾8>>QÀvv w>¼u|tú»Ç¿Ý 瀞ùx.Ã7U%CDfï> f!oÛé̇j·ZÅ>®Wë»wÔMó61o4†)3‹¬–+_ùÉtªÐ÷=›S.‚ýõf› vÑÊoí)ç¾ï×› Ž1 ™bÒÆ,"1 !zçpZ…õrsy¹È‰ˆYC¨œó)¥ÂÿÉ©¨³ž<}:ƒ‚äœÒ8 ý@š±Ó¶½uzkŒq»íq¹^/6«]7Î M*$êºmY""¢ì»I kAߎ÷^¼ƒƒ¹0KæífãÈ•ŠÆ1zïG¾ûæ·ïðþ¤mWËeN㽓9¨yùü±¤áøðà–ŸëM“0ä sŒ9ÅÄÎâ‹—ß¼ymñ­Ã‰BðÖ:CV%wëU©©­w–P3uÝ’ œy½\vÝÎZôÎ#•üQÄ„Å@_zsF kCSµu]öÉ'¿ýù!¤%[c8‹d1µ-/­ë6Ôò ú ùɽî>xçôîÓÛ÷B3‹ j´ Þ;ÛT.U•œÁƒ¢ÛÍîz±^nû‹ËkK°¸~£CUëmüÙë7çgo†ÔAN­óÝv‹Æ†vv0™?xøþñí‡&Æ7®®]SÏOnQÕô¬è¼o§>þ˜S’!ާ§'ýv}qö*ŜǔSŠãèê쨈±ÎZË¢ "eïäœ#r""D¬RT(ˆ$ª0Îîúr– IDATÝ»sr|üÕo~M*Ê )%P˜Lp»Ý^]_¿w¾„]ÀM´‡ì±g(†ŠÌÜŽCN)Ç8ŠŠªøšªrγpJqµZ "Ün7»Í¦”ÅwÍ™Ë@„…lá`äïn皺–·ÝVbŒ×׋Ìê½çœúaçƒíÏײ[UUCè½W²Fš¦Ii‘¼s†p±X•5²/ZEÙ7Zh ø–\¯¬ÊJå8—²a‡œ9ë v¿È<Þ£BÈ{W<%øèð@qµ\‹ ’sHÖû ›º~ÿwˆÉ\¯–·îÞþgÿÍ9¯ÝÄc×y솮ûÛû³g—‡ÓÛêœ(Ô¡õõ|:?Pô‘£m§`0v=+Úzrr<û‹?ÿÑ_üÓŒÂûç›õõZUŒ¥Iã½3›.ž¯crUFÀàÜ´Ëeo¬»ÝvµºõÞ;ôCÎÙ£*Dæ­#G˜Ç8þú×_ß:>Ùm®ÿðÇŸ~ùï¾S÷äÙï}öãGÜýìãóëÝ›ÇÖzPCàŽfwv»ÍÁÁAUù7ço,‹,Á^–{÷íØwœò¿·JdF¢R1 Zk•3‹ Ñ‚EPömJ<c*ßÎOæGÞ„àB&‚†s(³Qˆ1fN¢b·.€ñäß¶ó[~úùÝûwCS¡U ¤1#Ú4HŒ´ëÈY²–¬u ±€ Ítº‹±­m[»ÊtÝîx>kÚ)¡}ùâÅz±ˆ©O)RJ‹«óÅU=tÛI;1õtݱ˭0€øàY)æœv;0Æøà)ر۩°1{Éé¨ ª’rL9™Ò¡dï|Ç“µ¶®ëªªSâ®ëÇqôÞ:ç@¡¦*§„åk³Œ½÷œsß÷ΖŒ 9gÉŒ%‹È3­›¶®²ðåj¹\.ɘ̒²Q ÒBÂ#[pæTªûr¹¥®.//U™³óæà`~x8‡ùlãÐG2Æ: (d¬-R¼âžÝ?±æÆ¶^W÷vélrŒ¤Àc÷ûmpãcX$¦Tb°TEEÆa89:™4³WÏ^¤<3YtÞß¹s÷ÓO?>9>S^w ¦<Ʊ³fï´UpÆüàÿ諯¿ýâë§¿üæûùÁñ­Ûwïܾ3Îëf‚HCæwÞ}¸X\-®1L'³F«IÜÅ{3ü`Z‚ÂK‘Vúä,®úè<1§. ˜²H7¤ëM ¾žßò||üÑïÿÞ—¿ø‡ý¯ÿzbÕ#iŽ 9½xòØû€ÆÔM‹ ýnÇ!øÐéúúêç?û‡?ý/þyÝÎB%s”û''Ë—ß÷›ŸŸŸ¿yøð½O>þäßüô¯ÿÕ¿úŸA!s®&ÍÉÉñ­[·^¾|YlÛo?mΜ'ÓÙ~ò¯^¾zýêÌk¬ßî†Íf—–ëÍ0 wîÞ}úýã’4`)ÉJu…ˆczuqvxpèŒ782‹È^Kv~~–RΩﻱï÷&ÒÝv›büñ~ÇáâêzL¹\ÕÆâœÐš}ìèÞ ,ZìúÖÃXhNZ¥ˆ{ä"VU ˆd¬²x4Çqû~\­7R­17v§ˆ€!„”³raIB±ç•÷„ÁZ{0?8::Z,———W1d¨¨Bö[$2Ô4ÍÝ{÷pâ|pyîRê_Ÿ½Š<¦ÌÃ/¯—9Gc Aë4ã¼9øü“|ýí¿ µÍ,ǧ§Ö9«€ªÀ¬e•4Ž©QUÄ:goP‘™™RÉÞ_UÎs\.WûpÂýg„*\îJ•bØkMS‡ÊÛqØ®××uÓ4ó¡ïV«+CH …E„Š!T€ÄªUUÝ}ðî‡ߺ÷Ð×.å!‰=¡•¬¢c@c­õ ‹tÙJÝz·¾ŒýjÞ=zï¡J²Ö.V»>É'Ÿ:mZZ/g/ž>~lØù:TúŠE ¼3ç”…3ªÇ›¾íí´!s^¯×›ÍTŠeƒŠ[ÃX*4MKDËåzèGQ& @ƒHÞXͬ¬ÎùÚUÊ–¬rÃcQ0à¬+Ìw1ÆN&Må}ß÷¯Î^_-,ê ¦'xNûŸ­è_ÊR–E Ñ~UUD{õ r CVÍC”ë"Öuª*³æ¬Ý0 ì³–Š…õmƒµå¥úG„Ž1†o o—®Ì\“{j ¨"rR#¢Ö˜ÛTUÕ÷ý8Œ¤hNnrUù¸éYØ 6mëBÅH»Ì¼^)œ5„ YäŒùÉ‹×FãÑÑ¡Ø:|ðéç?ùñ®Ï?úh:™vC õ„ENn·.„Ó÷îüxb®¹©õÖQµí†Üz# ŠjX`—áùb8_áõ:ïÆèÈIÆAœ5iìxì8#ØÑ«†M\;;øóÿü¿b ŸþÕÿ9óˆäDYy—r=™ýÅ?û‹¿û»¿ûËß~ý[EŒq¸¸¸8¸=9=9¡ºmŸsìúm!è¦1¢³$_]½ñÎOæS~üäéË—g€o†uåÜ´ÆhVP¸ÿ¾%y±ôUÕ4íõrÑCŒ1çüúõËÏ>ÿüôöÉâêÊ”ß=¢¨uΕÃQ„ã0¼÷Þ{¿úÕ)&kµN„»n ÊU]i§ã0Æ4º!sŽcúæÛßÞ¹}ûþƒ?þö»o?}2Ž ½÷…&‡PÊ¢ˆ˜™-Q–’î.„ÂngÞúf‡aTÕétêý~R…Nì½K•D$cŒ·&ï“‚ÄZSâ,uU«Â0 *©<ýÅ-ZÔZÎWÓÙ,§Ì,!„ª©ŽSÎUU‚!,«Ê˜b7ôÌ\U•u¾jêÕjµÝmñ‹Hc|ô­1¾²Õø“;wî/»~µ¸Š))Àj»ÑœÞûà÷?ûÁ“çß_­/v»¾i[Èæ"ªÀÅWRdj¥Ü2ÖTu ˆhÈ:ïœË™«†Ú¶‚¦*xy‹DÄ{o­1"j…[g0¥4ŽCñ,~óÛoœ3·ïÞ~ñüÙߨ0†Œq  €³ùá{wŽN’ä‰k&óv½YŒCd”³¡)Ý‹ á~·Y\½yÉãvÚ¸Ùl’RjÛª5Þ8gѬV[ÕdŒÔ¯ÛÆ:ç­«ë*'&WÎLE$y‹þ)û$kÉX³Z-IÅ1$BSUUUÕÆyN9qDZt‚Þê]mSÿ úàœëúxköŽ»w'[kf6•‘fÑ„”ñeΙnЋ,¤¤cDTÕlŒ­*7Œc7ŒuÛN燻~R2%¶@ˆ±Ì¹„œ¢ÅÂ0ÆÂÙlVUUßwû?#0'$ôu¨«`­éûnÌI”€¸€Ï¨<3ÊeBDV•CUyoû®+íK]fVà˜K 1ñv»Ke‘+šERbDðè,3Ülï©©ï\ %gáKR°e"’ö@ã}iïœÛ»i åì¬+eÚáÁáÁü¦Íäêê2ÆñùóçœR×íÞ~ypÞ[ÆŽ¢(À` Ъ°*²êÈ¢äiHºÙnG–£ããÙÑËÅ*æ±ëÉö»•q~Úº ™õöÜÔd\Ыëx<óIp3ê›ÕøâlÁ^múź[¯VDâ½³Ö’q9#H’<æÈ×W];­&“Ö:›ÆÈ»þ'?ù£WßÿîÙw¿&Ŭ ÌurJ ùßüô¯¯.¯+[ÿ'ö§¯ÏÎ~öË_¦4Ô•”¡ß¦”†±Û®»nÛT>„ÙÐoŸ=ùΠTB¶†¬qÅÅT¬â*Ø#†TqµZýoÿËÿZUÍj½}xðîl6Ù¬ÀÉ Å«ëˇùát½^æ1RUUMÓ°ªó¶m[U¹¸8ôèƒédÚuÖ¹º®BðÃÐçœ ñqáöw:C¨ƒ1ög¿øÅƒû÷ßôèäö­ífóøÉ³Õfã¬),õb"%$.:ªÒçs|||ˆ)‘}žOŒÑ‡P|çcAU'“‰²¼zö4£s¶ªšRIX‹¢Š${R<ÒÁáa{Ö^‚¢u.§XØZ3m'ï=|ôÎûïÿüW¿Øl6Óél>?êú1%>:<‰)ŠäÌŒD»ÝŽŒGÄq×ÛÝîû'*YD†nê†Ã02Ãõå’gío¾ü‡ë«+U™¶íéÉÑÓ—ßÿßý¼ºx>¤þóO~OíÞR˜ù­mäm1nƒ7Î[çÈ cqHÁºªTe»Ùn·»bJ, ”’!ÊÀDè¬EC)I?ľóùüƒ>û¿¾ú‡7¯žk"–Mœ!C^2‹‚zï]ðH¢Ò§Þ,•ÇÁû`ÅÊØq‡1¥Ì‘y(Φ4³\_]»ÅúòuŽ]Šý|2E€¦iéf·î†>ÅÜwƒæa±<ßl B°ÆZ²¾öJHÎY$Ü‹‘YEØÇYTeèû¶m†ÝÖì rÎŒÙX†‘#Bq`ë>™TE›¦ñ™w»NU­1I˜“J9F”3ó8E%RºT¸IÇ‚b–·‚–R}ý¢ªªŒq0Ö²òÓëóó«å*Ũʦ` @5³pÊ)¦¶iEe“!•¦j&m[W@°“žse-6`U¨ëf>›£±Ý˜Râ2 )·»s~6¦¯—kfmÛv>› CÿêÕ«ífsãH2Å ä«à]àœC³ÙÜy¿ë‡ÝJ¼vÊ c[äX`5Dû!{J܈ʇ ú$nÚç„îõïÆZc]? ªê¬5†v»íbqMdf³Ù®ïsJÃ8’1„*2Žãn·«ê)"y_[çU¡‘,*HDmS;áØÄaË̪n2Wm ̓ڮ–ྲîäøðãÏn+ÊtîräYò÷϶¿}z)ÔüÇ?x÷îÉÁëËøËß_n"åw‹k”ÞÍÉSh‡¢ Ù[uÖZäÔïv›•õ•j«ðÙ'Ÿ¼~ü}]O»îªòprtüì链[ÃN3GÖï¾ûz½ë›º“lÖË‹³³ã[ˆ2ŽýÅÕ›—¯Ÿ±äÍzÑõÛ±ßeÉekˆÞ‡P5ˆ†%k¯7{BRZƒÃÐcô¡™L§9ÆÇ¿ûv>›VÍÁáÑÑt6{þôÉl6õÖÆnà1‚s¾ÄÐ;osÊgoÎîܾ3ÍÞœŽœó*BÆ4ucŒ«ë:¦¡ c„™È1‰óïžÁyç¬)а1VrÎ]ßQ ™‘DáÅ‹——××)傺zv¢Óvrz|Òm¶ËÅ‚Yêªúþåë×9æÙtfÄ BL윽{ÿA×u1%?öåí6Ö唌¡»·žœÞ]®VÌÜ´í0÷oþÙŸüɸY}}þ$„zµ\ž]¿>_ykàÕ›Þy[Þ’’XÿÖ_¤sŒœ3X7Äaè{C¤ÂqŒïðÁ«óóPU`Ìv·#ce½Ù„PÙS™q9gcµÄ]?ôÃÙÙùåáÉññÉé{>zÿýÖ›ÕÓ'OPØbÍÌœ‘YίÏž¿ÌQˆ Y$¢”r‰SUglÎP3Kß÷Ûœ«º¸ôšb­mš1Æ0K”H1óvÛ‹¢ Aó˜¹Ð﹤3_¯×+rv»]ŸœÜžC×mwÃ0ŒC_U•óˆRfDªf—dŒÅû œwýîù‹ç1ekÝ—_ýº9¾?0L›ÖZk½Ûl—qܾûþ{/_½âh‰ÎÎßÍlÉ¥ðÞ›·3ôqUEÀ”3³´ÍÄWUuä’³Á€g Åd/1Î "œãÈÊÌEÐOVj@¨ª–Œ³ÎƒèìÔÕõ¼iÛq®×ä÷C.OÆdGLÂÀ‚DFµßm—×—ãf•†ÁyÏqc–ÜkŒNÇݰEƒ(Ž0õýâò<Ž»Õ;–¸Ý­ÒÓ(ÝnX]]v»m昢ÔU{tpâ|åM ëÀX BBkTsÎ¥4&ƒ œ9#Yf•¬’3§TvñÝ0bR} ;½­8–œ™n.ŒÌQYn²÷J¬9ZEÙåC²ÏKÒ2GBclÙ"½`Y%Åo´ P躟8«r*²1OÊ"Ê š2ûÊ7m‹ªÛÍ&¥d !sÉ0cÚn¶ÎÙºnÈ ˆLÙî¶ÞV‡‡''§'›nw æà€ûqƱ뺜“s¾‡õvƒ¦L;Ä”4%(Ô'ÄPU `¼G„¦­§“‰·ÎyǸZ¯6ÛMŒÉ:g ‘Sb-Ï'!ZcU¤ø¼Ê1{}”„™‰Ê h¬µ˜HYtßd9çsŠÖða6ŸOfó”³5ÔT~¹Xõý¨*Ôû’¦ëˆÈ9ìbÕz[U6e5ÈdñÇÞ¿Þ,)öã°íyLÝfw)ºÛnž¼|Ö±Œ¼±ß©€E&3ì¶ã7ß==½ÕíúXyÐÄ»±³À’bf@UBtAE¢÷d“‡qtÎWUSÜ,WÝf}4­ÝòË/~VÕM¨ìz-i­E*‹3Ö÷ù~ð寓Fùàƒ÷§Ó6)$æàÝý»÷¾L–gÏA¢wÐß:¾uçâì%‘Q€aYPÑ D([a*¡•I¤½–‹EQúq5ž_Æq8=:<99}õò¥u6öùââ²j'óƒˆÌ×ëu̹Ä4ú‰sÖ†Pí¡ {R…/¼~•ý˜w¹X^^-½ô! Žã€*ιý|F}ðÅú߶ͦÛ&fRM)yg!眰ÑíÞùá}`7çbé­¹ è‘TÄ ¾jÛI×íxìÆ<–µ³–9j–CÊ~p""PDTˆ€b­ã°\^?þ8ÆÑ* ÇkßžÞyï½>œN›Ÿÿüož=þ\N<Œc_ÓÄ{›Ç~·ºvÞ¦9Kß ÎÛPMw]ìV›®ëUĤ˜bŸ†­p"_CÀ©ß,ºÕuW7Yç‡gÞm»~½É}·;phP¤©ªéÁy'@¨þ¦ÞëKÓ#¹Ó “™¯ùLÙn´AÃÎ 8àÐ ‡ËWÒéf¥ ý›º[éì!¥=ÒjW<«uœá8˜ 4Ú–¯Ï½.]„.ò«&ûwuºQõUfdÄ/ž'†M[×M]jao=aJY¨]Îó&“a6FD}HJHªHðvîw‡\V1†Ê’Á݃ ï´^åͤÅ•³ ªêß“oË`ÃZÛ4MÑŒãXβò='BWUDXf›¥1JDθÆ5¥¶ªªJrÇ!í±‹öøäøã?úÅý/»ÝNµ<á0uœuÖ¹CŒ©Ðå&ïËçØs³¾É×× Ñl­Cc«ªB¦¬Ú÷}™=°1M;1UM}|r²Z­º¡C¤ºmß’¾Ù0*0‘µ, ¢ÐÓj½Ç÷—.%k ÞYÝˆÈ äšæ­M‚ªÊ¼Ý¹ef6•"ù0•ÄQ¡­Õu¼ D\~"mÛ~ðþó1¿xõúúú:¥T7Íýûïl¶CYäa4%õªUešºj*Æ€•Ÿ‡®÷1”åE¿Û„ŒuÃ0ž]½j§ƒª™!šÝn"Î 1gªrÓ4e×7×~êòäýÔm%'–„É["kœs.Š ÎiÖbôÓŽÉÔIU1P8»úöÁƒw’1(j ã^z”32ÿøÓ?|÷ɓϿüª®¨øiœ’†,ðtyp´XnÎd1_<~üøìò†Œ{üäýÕêÆ¸êOî½óÝwßû0éÝ£HÍÀT”ì9§$`œífcŒ%”bÙÂë½ÿÛ_üüƒ?BÆò®7DLP1ÏÛ¶ˆ>V›Íîw¿B89½‡Hã0¦”êªÊ)Ý;>™ýôg¿üͯBðÖ™cJ¹øn…-ŸÞ4 Ï_|ÿãO?Y,VW×EŠÄ¦´š1ÅTG!ź®]]Ê^–siÔ—]­¦in™˜9Æ$yŸ>(]ý²~JâÄ9W7õn½1#¨dEPFst|œ¶ÛÞ:k¬•”×›5²©ª&åi§ºr RÆÎDĨ)¥CÓÔ?øÑÇ'GÎÎnˆhÙ¶qÛñù«ë•Yn¬­1c˜&$zòä½¾ëbH¦ìJyuO©k*ǽˆ@–œRR&,9¥iè­«ºíf³^21PޱøícŒ1&6¦P®ªv>[´ z~yöÙ矅il3”¬9“«ï}øñŸþìg_ÿþ··79v·¹‡M]ÕM»d¦iÆ¡GU?ù0M»^ÖÛ¤8 c½ªÄí8«n{›•ˆ%åÚ9ë¸ ãØmWã4HŒƒŠÆ$iòaØùaôÃÀÖð,;ƒvöè1¥ë›[%Ö±8!™ˆ xÆÎûT ­U±%‰ŒuÆZ {÷4”´‰1,"!x@Ô,1FkLé!`Imßå= «j–¾ïU³µ¦8áÐ:gŒ©›š˜‚°¯O ‰$FfbÃ%á[êkäL¸ßó–œT”[jÛyÝÔYŒ“¿¹¾î·»¬j]%’J€‰*kfmspp°ÛlrÎ1Ä’d¹Û’•ǫ۸ë7ÎVUÕ뚦qÖÔUU.¤²ú@ÌYTû® 1VUõvIOr&6ª’sF€ívˆ)ÌgË”Rßõ1%D²Ší¬©›†±ôXRJÕ¹Œnî$"ûkY™qŽ˜m‰H2s]7Öºf6÷aºº¾†,IÇà½a³Ýî&‘ UÆáôèà`1Ÿ“ïúÞ7ôƒäl­Óœ¹rwèfhÐdH²¾½šÏ*hëÚ9ST‘!ôÓ0Ü^_;ç”p~°tœ x@›Ñ )¨uÕz½AÅì{3·›«íí¶ŸÍÛÝnÃ(qêýÐÍêöøø3Ç>Tµ!Ê–ÛvVuýØo"oúáûï_,—ÄùòæÊ^Ý^ÏÚ*'»­.D"AýæÙ7_|óÕj·kçGëõºë{vMY¦“q˜¦NÓÙÙY?†Íf{px$YCŠ1¦õjcTE-ËIŒ…‡Ë†$  qîÝ'ïÝ®V«Õê~øƒ?úÐÇ€Äd8¦´^¯Ø¹'|ðÍÓo$GE1Íg3eâ˜#‘hÌ :oš¶®’÷ΘYÛk_¾zùýóï»aP‘”ØîOP””‹z)åT°ðÆÇ †±øèa˜¼÷¡TdT‘™Ð²ÊÓ IDAT pVÈ¢¥]QZ4eh_ ªP×MˆÑY›EI±<ëŠè]õæ¬í°ע¥RÙ 31¡*cC>$fqÖÕuÛÎgëÍà½/ ñÙlöá‡|÷ýs )6 J•µÆpÞχÑ0~zñòÅ4äãÃÃqèýÔ}øîãÊTŸen^ï¦Ë±[½º){`~õúUiùšÒÃ-¢Á·œ¦’çL)a1ã¨ïãäƒ1lȇhŒg²ÓèÇÉ×m›rÊ1Åà­µ â½DÑl«º=8>><9mæóÊ™?úaö]]UUSš¶^ ›˜Âï¾üò—¿üÅÍÍM ±nòØw7gG‡'Ó46MËl A¿Û Ýn×uý4‰BNYB®ŒEÈy¾ß¥ä‘8VQ@ðS?îۥAÊõ«:tÝ4 aò¨‚¢qÚvÛóË3}ýê÷!øŒ;8¾WÏfÆpíìÐmŸ~ýMð“«mˆñÍëWš¦šQR”œ]ÍD„{ﶨ(@¡Hï£/åÜ !”ÿ–åc B>•<¡(@lÛ¶øT“(ä¼hçGÇÇmÛÁ4MM;«ëfì§”s `ˆëY›E&?¥”$5ޤâ((cɲ‹¿\.Ù˜aê½÷1§ízu›ÄZk­)Ò®”"6Ö2!—6RêÅ!`É ((SH"0]¿Äv>Câ2“$IÞÇ”Œ±óŲræúúÂ4¥K¢¿0ÖŽúÿ›»ë®®¯@À‡Sd&$CHI #’ÂÝìz¿Û%ò}žûuDUÈÙ{/€:Ó4YcÊÄÇ1!@‰‡gÐÊšÓ7_?µÖôÃRnfóàÃn×Íf'G‡‡ó– *bƒˆ‡³Sxú»ßúnõчïeÈõlF„D&gUÑõízØn¢cª«¶­1Å(’d уè¼m4njѲ¼sÿaÝõåE×u³ùrÚlÎ_=›ÏêÇî뼉!Çà­1uÝTÆF?v».…àl‡ñäô^˜üÉññÑéÉj}³Z¯çm»˜7Yb¿Û:kT¡ät±nšÙb1­¶Ä¤š/Ï/†a|xr?Eñ“ß)\_"ä¼½½ÍH»ÍmÉKÖÛÕª ¹igÓäSŠlLU×c×wš÷Í1ž/2¨¨2Óëׯ$EAekœu‚¸Þn«º­Ûv/|'F¢«ë«Ý®+ó¦i³*2VµS•º®rŽ*¢*>ú”’±%-V>–ÅÕg‰ iÎÆf«"Ûͦ®ëªnØT9ç¾ïA3£€±$’Ùš’ÏÜ+¸¼ÏämºÁ9§¢åׇŒ-ò¿”EU°à•œ#6Ìf¶8$&DÌ)…à‰¨©ë2•$"Ë6¥ –ß{ïÝm×···UeçM[Ðrï>~ïi=Ïq 0Í—‡Æ2ù©ëOŽO—ÿøÙÓÏû~Ó _¿9<<†¶»Mðþæò|·* çbÌC ’"¢ÆÐÅÐ3aã*$MS¿í7«0ùi}ôHV²¤4„8^BJÁ‹$È~Hø½~ýâ÷1úcÁ%¤Œ@œrJÓD"ÎÚ¦mˆI¥ß¥<)"23K± B‘Å,±Â\º(…iþ–”‚û!6í÷8@i¯Kóùœ˜§Æq`˃åb9“¬»Ínœ¦àÃv·K1QeíƒwßUÐßõ[cŒâ$ª$ųx—G„Ýn[U®®ë˜BNɱ©ëzƒäËÐs;k–‹ƒÂ8ŒóÙ¬4‘D¥É2HΨ…[ ˆœó¾lI)µ‹E]7¢ÊÄÖUu]¥œ‡q`k÷¢Û›œrß÷åí9ã4Mo±_ÛÕæ—¿øE·ÛµmcŒbX­×†ÙYkmU÷’rŒÑ:.Ø÷Ò-)7hÉ\Ùo…}¢Eµï;¾$”rJûj©þ›¦><9¾wïtìúõÍ­(Ôm;ŸÍ¦Ñ,¯¯W éôh~|ÿD*gRŒßýÅo~ñ·qܾûÎiwØú¶Ã˜ªÙì“ý'¶ë UÀ8û˜ÐðÕíÊ0½szzþâ™5ÀΚº9»ˆÏ¾ûÖ[9·º¼X­Ö)Ç£ï.Ž6ë5¤¸<œíÖ›íÍÊZ×gWDŒDoÞ¼™Í?yò䃋›«®ë·Ûm·ÛÖ Yc>|°\,¾ü⋜óÁÁÁ§ø“‹«•[æ®\Õ>º$‡7«[Êùá½û«Ë=3€È³¯¿ðîjZd›RÖ¶v ›¢·ÖŽ H¬ wd˜æáÃGÔu;cííz}}sL1Ç21ÖJŒŸ|òãƒÅ²‡"¥¼íú~ؘ¶m³~ë¦îú†™ç‹eÑÚ,—‹ÛÛv·ëªªbæiò¥4ú¾igˆT¨ñPz¿ Z¹ŠÙƘc,¬!R@Ã.‹¨ªs•3þ-º®„/AT0qVb"Ò\¨õ¬‚ÎÖ`AEbô"9¥dØä,EÅ<›Í¶Û "{]Ý´!„œRU7MMªÚÎÖÖý®?:8üïþò/ÏÞ¼ 1N><ñ²,5¼ˆÌg­J!°aÀ²÷FÌæÉ“ÇßöÝn½#·¬\{v¹¶Æž¿§-ƒèÐíêʽ~y¶ëÖG‹cS o=DlXE NEÈ4Œ9‹qU ¸$Õ‘Cð!E6¨9KeOØb"QQbD¢ÅÁ!‘EДR–;‹d?öÝvkªšžÞ¿ïôð`i1ü—Ÿÿ‰Ÿ÷Uׯ‡†utÜCŸb¨ëÁk•Isš¡¶„X‡ *i‡ËW·ãn… ŒÆ8×¶‚\TÛ~ )ƦGI^$"¼jéBpS·`sE&…$)` ¢’ÀæŒu³L*l Å,abRPD"6@,*Œ¬wõRQ¾Å?”ƒž™ñ.]óVöTˆqÖš¿"„Êš“£ÃÃ…ªÜÜÜ^¼9!ª‚µ¶TMS¿sïôêúºª#ª®ª ™ZS@ç,(„àSŒ”i½ZUu]Ædm]ÕÎT1…,Ùû TsNÞEš*ÆXÖúÛ¦9==U•}1¦sV"L)§èEµìˆJLb%¦8Iλ ‰Êv»Ïçe/ZT‘‘¬1ÈÜõ}-¨hJéùógÆX@<^Ÿœl¶ÛW¯Ï€ ³;)"Õ—ìW $÷©´ÈE §’ˆ!“(Š#1rÑgÙ·™¾óΟþÉ__|6~>Œ“­Ý|¾è»¡Ûm‡Ý.E©þõ¿úWëÝ.«c Ó°[…©#‰¿'ý¯‹ÃåáqÕÖìÌáñ1Eˆc¼<¿X­¶²þìõy!‰ôÝPUfÎxùæ5¼ÿèáòèäððþíéêË/¿@ÍÎU"`/Ï/ÆMÿîƒÇ_÷õÍÕõáÁÉf½™ü@„uÓVu Û®g[­ÖÛÕîw?ü‹¿øÇ7o^~¦Ù¬N)1Òv»õ~"棨®×««Ë 88<!5Î9æÁûar㸉֙¼³„Yrö}ß=zï£ÉçͶ¦©jZdÙ:$2®®š”RbC<_Ìg9Ëjµ‰!Tµ±d‰yˆã8FÏÏÎò‡ô£ýèw_}åCFD¶ÆÇ˜DŒa$²ÎýøÓO]UýîË/ƒ„hp9ŸWÖnS& ¢m[ÿÅ_üì«/?ÿý×ß.Žö£/‘,9‹æùbQ“9Iɼ…X0RZ1·«Ûq µ%e)¼Ç¢ÿE¥”$¦sŽ9Ûª:9:©\µÙÜNSŸbŒ!ª2—ÔoAÞ«ïÃf³5Æ8çJýQÂqëõ:C$‚˜RÑT5€NÞßä©;WÎñ^ìŒ)¥œòf³íº^D¯ooþÅÿø—ï¿ÿƒËõøúòüõÕ› ùxq@·›uðÓÙëÃ80rJaŸä+M™Î•ô¾ÿ.ª$’ÅOÞV¥S$fãŒÜ¥D¼|—a(qM)‰€au–›ºvÖú)L¾ÿòË/w»Mí(‹1ÅÐ}÷í—›Õåõò¨iš®[·³Ö§Òx³:¿^]TUóàácÄý ”SÚ®¶(ê ažRA³1†­Í»m7v›i¦n ŠÖØÙì€p½0®ÖÂÒ"Jc?Ä1ø0KCß1‘$I«TbŠÞˆ’Vꪂ óXZt P{¬M¢9zÕDP:ò\8Ve¹÷íä™îr5wÙT€˜SQJ!HƒÐ.ÚÓ“ðÝö—ç!„’\ß»,@×ëõ—¿ÿêàpyzï4e§‰­êŠšR>Ç‚ïû.%vŒ"Ò´u]×Dì§©`¾÷£`6ªB  ·ŽÓbhi›z¹XÄTf±¦R…R¿Ä%¶Ö(Â8)  ( ‘1DÙp T]S!¨ä˜B(! ÉÙXÃl‰öÀ5&šÆ¡ëº"Ë)*åݩ"Ÿ¸gV€‚¸*A¦Ò˜gc üf/Æd2ÖÔu­¢ã0¦ €¬´M›¦yôè‘%þ»ŸÿÝ®ÛB‡Ýn›Bì¶[½zzq>nÖ·>xc,‚lo7·×ç9…Š0X“#m;ßõÛ$É:óøß“Ð|¾Ì€@öÑ£SRy}ö¦ ±¢÷g˃v6ëûØñ‡ßïõÿé?ýǦ²óù|Øn}ðÃ`RÃà§ÉÒá!?xüøââõÅùX¯ŽNï=|üðƒß»<¿ºº¼¾^íÖëušvÿ׿ù?ž®×«àýjµb檪|7··ý8TU£"¨‚€)Ʊ"j›Íõ4ŽGÇÓDDÓ¨)õcÿø½÷>þÁ§/Ÿ?ÿÕ¯SÍf)擪²1‘¨ã4…¢ǤÓÔu}/ijêªX<²Îg‹\gZ¯×I%Æ„¸¿¤µ€ÄE~Ðív–eƒA÷™k)‹vÖÚ»•%º>çøÏþ‡æÓxq±Ê ó墴JsŒÇÇGšõüì"¦\œe j¬ô}9æ®ëöQË"ÕQ@,UüÝ€ˆM;K1ÍÚEŠiš|·ëf÷š£ƒ£[Éáðôh†‘ú¾ÏYˆÙWR %ö¦YsÎκY[;çÆ¡ëºm‘öeÎÓ8”S×gQÕ”A‡‚÷uSQS70NÓãÍÕµ»—¯ÞxóæM×íDËçYD„î¶µÖcm[7÷nëªï¶!?Žï>~Œ›õ*„´Zm²è|6 c!MæÊUåHÉ)—¹fÛ¶÷ïßO)o61ÖbÝl7Þ‡’lVMYÔ)øŒµumsÎUU‹ŒÖ˜ºª¼÷È”A3¦û'÷ÿÛrÔÎõ;ôÅÄ™R.3BEDS7Î{¿÷HîkU™ ²B¼L–Q [k )xŸcDQ >îñ=eë¢D.N±ßl–÷g•k ÉXgê R`†iR MS£åàæ|x´X.f!Åå|á}`C<þ0„8kæ‡Ë“~;"ñåÕkI)K`VBM9jÞú¡ÛÆ0ÝN³WPÍ)L9ø8M9§8îˆ9'ATÍ©dcø.`§ªÆ0!‚BU9&"@ɹx;ÊÑ‹w´uU0lÊï€aÎ)båœaœ&‘”LUY˘l,äR¢=¯CDö^ÕS–ÌûÂ$«¦rz+䲪ªÓ8bY+?,êW€¾ï·Ûu-j‘‡@D7>¨–½v[,8¥H)ñð´Y3SÓ4!øiËßXÊ"çl–œrŽ9O!6MržBPU¨ªjoâ‘RÃfÖºªªê¦QÉB¢:ùiÏtµ¦<0'¿7Ÿ0sÁ@Ê©ðúTE÷¨K»XÎsN³¦­]Éκ¶ã8NÃ)Œ¬uÖjñ¤”´Dˆß/E÷°I(õþÛü.³1lˆ9kV$yóúÍÅù6\ÕŒýß;¾÷Îã®ëÿåç)'fÌ" cºNs" 1F€œRPH1:ªÃòà„Ù†ào·›¬ Ř,)9c­C&Ù®Wóü¹uõlÖœœžœ_\´OS–ûï¼³Z¯v»3¦±TŒ*“ ƒŸ¾{úí®Û¦<|8Ÿ|ö›Ÿ757uS(Ò+ãlÛ¶>º¸8B¶†T2#êf½JQûݶžÍæ³yóþ‡‡Móóÿ|sysé}_ÕFPØ+:vÛÿó¯þjý—ÛÇï¿ÿòìjGk ’QÀa}ˆÆº2Æ|üøññáñêæª®cKÅ’ u]—T%1÷}wúÎ}c ©dIyÖÎ,³Ï1WΩB cir®VkÃ|{{ë½/u¢æœ6ëݯon¿øâ«Oþà½û2#¢÷áÕ›³«ë«ëëÕÑÁ œ¤ŠX眵oÞ¼î»îË/>ßu]I|í÷EDß K"0z{y1õÝíímN‘‘¦qüͯ•rìû¡ˆ ¼4Œã÷ß|S,¸uöîA™PE®®nn®oËΊsÎ{¿^¯‘˜˜R@²Î!Rˆ™ +B鵊ÈÕåíä}UUǧ'÷NOà•¾|öüéÇŸþôøäþ¢^>8}ðèáû¿ÿõÓ{DI!«1ÖOaoÆq$"Õ¬ªÄP ´2ô+T) Ђw)$‘ò¤ ~êû¡Ø;sŒ©ÄÝö§`Ž)À°[¯n®ÎÀ˜‡>r9ù!L³ª}øî£8 ¯_?+sjïý8W——ÛÝn¶˜[gŽ’ÂË—/@áƒ÷>zÿ½'ÑçW/_¤iL!£(:kŽš¶~öÝ7Ûî6§4ôj•b†Ó,ªY%M=Æ¡sÎ1Y!9„ì^¥%*IAPîĪˆ"ɰ-x-¼ó4•%½Â]AE¬!&c¹-ö¥»<¿] VUa$ ã´Ç`¥”÷›dY4 €ºŠ¤ë¶Ûm?SÎ"¹€yèퟻB^¬1"âC0ÆBJ±€ Šºn¿$UÄÜ9×MMÆ$•)zA%k¦8N~Ê9ï ’U‚ˆîÔ …/”ö¬Ôâ(6<›Íj[7um­Câ½ô‹ÊN£dCˆ±ëº˜cÎyô“1&Iffc©°òRŠ{o6êÛLØðQNùêúª®›¦iÎÏ!k™ˆÙŒ“/7DÁ˜ øÝí O¦iÚ#Žá.ÀàlÛ¶ÛíÖ{/RÆ@¢Jl`fÌðñ0t›Ý¦\À¢¢ ÆVH¶ŸbBrT¢nì}ð"3î×”¹¤§r c·u¶9¹÷àøð°:EI)ÃTUÕ½Ó{(|ˆ!@Ãf7Ž"z{}HM]¿ÿîÇ)哣ÿ½8'PI‰˜23~ôñ..ÏnnnsL’|ñÙ¯‡~¬-ÔÖÛCu]ÅBœ|Šý8B¼Û@‘ÓõõPuïôý÷Þ{¶îÇ.úQsNiòÓ.ibÅRÞ9ŽÐœ_\ÌOü“Ÿ|õÕWCßŠÄ [kœu•ˆ¼~õêìիׯ_"Šâ^K©Šˆ…`q}}ÝC;›1sJñûïŸq×õZºÄŒˆŽˆcŒ!$c@‡ëávµ2T®ð2"RB"Kƾ9»Èù Ü=‘§qÆiÞÌÚvVTP9x­]ëC°ÖãÎ/.Uµª]iMÑ?”e)ɈôæìspOT²¦ñÁQÑÍ{eRЦvÞl7hˆËªŒ*T®.\ÕrXd6ÌÆ¤Cˆ{e©6²©sN$3'¹!ÄÙ|Ö´mAÛ¦~÷ñ㯿ùúGŸ|úá{ïwýØ áÛo¾úþù7!Mu3kwssur²üáÇñÙç××WûTQó ÒÛ öÑ$r*{+LHDÆÙªª6ë +FØpÉßÉP$3£JÞÜ^…œoÖëY½ ˜=ûö)ß¿ÿáÿÑŸ-î¯þõÿºÛ\Uum]¯nƒ÷1F‘4_.fóYL"yœüôý‹§/ž;îºâñÉñÑÉ¢ßuŠ ’%ú€G‡G†yèÚÇõf= cS×PÖ„rêˆyÄ“E¼©¢E?]êó#:<Ÿ±Ùnv»‚~—,@X¤‰ ""Hh˜­akL™ ÐWTæ*`˜Ÿ¼û8¿HÛÝNUg³íúÚöôþƒ~¯¯/ý°Óï?~xþòéÐGHˆËU¿ë©¨+oTkfuU[óúÅ÷oÎß(ß8›ïž>"rìjWëvã0ŽÃ0 >|¾~ñêül9ŸonoE¤i*·æÏ>ÿ<tt”mlifØ Be­5<›ÏØš”ÃÃ.ÎÏÏ/vMÝüÉŸüÉÏþs@¬ëz1›“*ªN>LA+ç$'ã$NÃææêÅwß^\¼ )…˜BTÃÓgÏꃣù?ÿ/!¥ó³×mÕt}t|R.ãàcŒaÐ]N‰sÀ=_E0¥=*–§÷Ÿƒf&Þm·)«ˆXgcJ)gÃ{-WùÀK.à_ÃÆª¢äT|ÖYPFÄÒ*®­±*Zö¸÷¾J~@rJ¢UU•Ý‹{'§ 8 CL ïæó¥ÆË}‹c‚œˆÑV6_¨¿Eâ^U®ª\Þ(û›’²µÄ–ú¾¯\¥")'0ÖªjV±Îæe9Ð5%UmšÆX›bJx%¶ÖÔÕ̺]ŒqÖÎŽOú¾É1¥‹ËKïC]Õ 0oZ£îó¯¿~uñj=ܤ<ÍÍÒVµ±v½^ﺠC†‰RÊ ÂL¥ùæ›ß®·[R1¤€P›¶‘¾ë«ª™µówî?€ë«Ën³9<˜òáÁaJÑ1eRðSòp|tì*«"mS¥\ÕlVkkLJ!øQE‰ÙSìÇ)øX:Ýo-ÏÖ– :g‘8„²0’©*²ÆÔUÝ´õ¬eÃEĈ„\È-¥¥“b4lùîÏYö”}6©Ð4‹‚+~—,!¦²X‘”¥†"1Zc™*ºª²Özˆ k^Ö(¨BŒ¹Ø0DT4ß!û÷rýÎZ)N§q²ªí¼=8X2»7oÎò>îfœµ'GÇOÞ}̈>„‚dÉtw\–µÙbÞÎjëŒa£‚l†qˆ)æœJÕöv‰Üìh®nj"Þn6ã8ŽãP^åYE@µ®kç\U9f¾¼Üœ_\é¶a«)æÏBLÞoaµRÀ²8ÖõNU‡i´Î0a¬uªYÈp™£æ”Æa( \P@— +ïGÿìÙ÷œNÃXƳd0¤$:þà{÷¼›!ÿÁÿàËßþÝ·O¿qÎÍÚ™ª²a©œ5–RL˜EJ¸ ˆ€ ˆ¬oÏÏÞ¼X­n³jÙ cŠíÙHF`7…ú‘ˆ*ëþè§ŸüËù?ý?ÿ6qe} IDATîßù?øð£W¯_3™Ål~{{=M½‚š=aꦎ)?yòî_üÖ½uæèøøþ;÷¾ýî©1ä*³\.bðÿû¿úßØØºª¬11Ct|xÛîvuüx}}~pt:õH^o®/o®€€ÉÄcN€h™U€æóENòë_þjÛm óñé12‡Éï½÷EçY9×Ôõn»éº]qÞÆ”$‹5Ö°Í%Æ‚k^ßÞv»] ?•0]1#¢!.hfv% £Uç*çQ1©`vUËb’‚JV%°¶Le’16ÅXÚ€ÆkMŠ9§< £"0kŒ)ÅX@ÇÆòþIŠ`+»·‚ƘÙYWW ˜ý·B+éûœR )R&UuäRŒã0”ŽkÎépyðøÝGÏžŸ’ÔuKLu;_,––ùÙ·ßãØÎçu]‡,!F‰¹|Œa@€ÊU9e?ùI§òbÈ9¯·ÛÇ_^^ŽÓ8Œýé£úéOLŒ·/¿ê oV·»Ž$û0uŸ}ökglUU&ø`­Q¥œ[óø‘w;#ªYRÎ)xφ‘)ÅØ÷}ŠÑX«9‚>ƒªY!ç”UQPA„Ø!¥(ËÃ?ÿGñâù—Ûîö«¯ýó¿ýÿŒå<é4y…Â)Òº®Ù˜ÛÛÛäc,p"CÔT•¦ *S¿%¢Æ’Â~öB$DÍe•f±X¶ˆ(’b˜ºÝ®H!²ˆª¸Æ6Ö°1\ þeÄç'_•­æ§óª²ÈeU‡‘q¦ŒëJã T™X² }¯uUïÛ&å@%x;عã dcË0#3AeËÑ/1DæÂð*¢£\°¨š3¢Þ-ßïIˆECƒwÖ- *À,w¡ÂÅ--è;Ç4(@’Œ9eç*gÜ^™¹ªëª® dËZ,áÞ¦ ÌÔ´õò`±“µ¥æT(ˆ´ÖªhйÌW³ä”‚j6ŒZrYˆ{DZi¼¨1\;ç¬+›öÁ{c­ä´Ýn˜|ð!†º®K£‰™ëÊAåD¤ÛuÓ§Ÿü°®éûç¯ÊÉCÄmÛvÝnq°¬šª´U‘6ýŽˆ‹…s.¦€ˆ®iˆ0ŸZ­‹”ÍÕÕl1¦ÉXûãOüüÕKµËÆ6Ž*ª 988ú[‰ª¦ÛvfJ¶ ÔÅbR,YcªjWN%R²Æ€*h'æ,¬\•ý”%‘µ¶PŸ‰KƒUsHy&râá4öÿïßüMž ;k$–ì ¦œ»¡—k(-ˆlRY¨ëFUQ!kFE(ΦiÛÙÌ{¿Û®÷ÀnUŸ¼H6ƶsJ)!¸º®‹eN)„P"w jJ£ ÊËÝæ;ê}I^¨Š1l ‰JÊš² *ÃÆ˜·›%­U&¥C]˜æÄ†ö{åxÅ¿‡>€–娬1XÕ¦í~šzR><@*õY‘ï4ðÖu‡ò61‰å&Ä‚†/µ³‚Š .w6hœ5Ž!£¦ãƒåÐõ>…¬’²ôÈ€uU‰dfªëöàààüü̹*¥T`¿óY£¢~7›Í8ŒåöH)kPD…‚D)5ÔÁrÞ¶»ù²=9>Š)ÑõÍäCðÞ¹Šˆle›ªRfÇa·Û镵®4šÊU4M]øÁ“úOÿû§Ïžÿû¿ù¶rL”S6%U¢E9fkLù‚H6f¯ò½“*ƒ8kç‹Åâpîý˜RÎûí34ÆÀÁáòvµÙlׯ߼¹¾]iÈ“Þw¯Ïž9ΆÈ0’Y¶méºQV )Žã| 1ÙâÛÒ\˜8¢* ÎÕYED‹åÑá1^_¯.¯þsU;[¹]· ãôìÙ7e'ëôøt§q˜êº€‹‹séú®m뜳÷‰ÿêuðaèFmÛöÞéiLyµZK–jæ–óy˜¦—/_ŠÀ½{÷†),—Ëåò€‘–‹Çÿð‡?øÅ/o|ðYs Áp#Í—ÇÇÇ)„çßgŒ©,Ór©Ì&ç\œòaöãDÉ ÷j­*÷äñã]×­VëÒ|÷ÞKVk\¹GE2®*çCôÁ[k˼JUÊ4–¶ž*¦sŠ1ÃTÕ5Â’ú+Kø1„‘aO.B ‰ T­!dKÌu“µØbÄ9'’ˆLIîJfÃŒ¨ÌbðÓcfæòÄôaÚlÖH°\.Eò[uLNÉUu-9ǘ¬1~œ^~ÿÍ?úóŸ^__­×#»§É™ºµóyJ)Œ¾Ä–K‘ãÆÓ4²1•1óY[*žisN!„õzÃÖTM=ãâøÞ8 RÙ*&­ëÙ?ÿ§ÿ|>Ÿÿ»û×S·Ëåb1_ò»þ¡Š¢Ò(ÖÀ²´köÑÉ¢»Áºnªº*;ÌŒ„¢*Åc$RHP¥sŸs–ÒÕÂrÐ ŸÞÔ¶Õ³§_~ûÍgµ%Í^“DŸ›¶iÛTS1ncmeØäœÃ†~Ì)#87…cˆˆ`Œ‰1ô}OHOÞ{rïþ½«Ë‹ÌP„»Ã—ê¦a6’ÕUUÝÔÖ FPQÉûÕ¤’¬0†µ]$ : 2#qŒQ“hÞÏV1ÍlÑ.–®™1!•R…Ê×Ü5ÄÙp]U€á¢tו/Åû>ØgÊa/D()gI³ÅÌUn‡÷:vëígžz×1Ìoƒ=P^Qˆ¢RjöýÝû1Æ9ëœ9>ZÎÛ÷<úäã]]_¯»b»¡G„¦ª‹H>§b¸½]ÀÙlÖÎg3kíz½º8¿\­Våé“RUÐ}´§Ôkw/ .ïTèf’³è8N±Pqöÿ/¼€ªª4Kö!„E$¦èÃcD€ºªš¦aƺrÌô›ß~Vp")%¦išv»T­µe`ËÌHHÍ>ÊO§|¼%gkÍñé‘uþè£×¯Îú®'¤÷ž|p{³:¿¸Œ1†iªœ Ó8Ý4}ß–ö‘ Cw||½!UU]Uuy‹øi ¶mgm“EÆqÌ’­5 PU•÷~§‚«fþyptc†îòüõ«—Ïnn¯cNY‘·ÇŸ|ò“>þÑíjbfkÐ!•T)§!?½æd ©jð^r*%6|||”b‡±üÒ{u9S6œrœ¦)ç¤Yœ±ÎºI½3÷îoè¾ïCð)Å”=~ünÓ´]7Œãc|[ÂÃ]½4KÝóö}—À·+)%Ys—@+£±BÇrŠ>„”s*ïÍ”´d“ìëRb¤ ‚ˆÅÌY.9?M9¥ïŸ=ý£Ÿ|úÉ'Ÿž_Ü ÓXì€bÜl7!„œrð!Ç\¢ý>L%ê2M“¾®ª¶i J¶äîf‹ÅñÉÉíz•¢üéÿ£zvð݋׿ÿê÷››³yÓØm7«ÛÛ‹è"xòä½?û“?3š¥ªëºr¢8eï½g6åï+w ˆ)!JN)FVF檪À‡`²µ®Š! ª¦  ®²€:Ž˜‰¼÷3?›íæ7¿ýmŽS;_°6!û A€MU5ÖÕ‹ÅýœñâêÌ¨Š‚&™Ífˆx}}1Íf3Üõ}¿ÛÂzgg˜“ä,MÝ¡HÜu5¼Þn}œrJ 0›Í †c2Æ ß†€šB ¡°/lU!r96 Å×ZËÄÄX€¿’5D?S 1ÆŒ±Èl ·m3?8¦2~ËB):gÊŸ,IÕÜ Y÷uwÜ7PU¼OÖ—MÇÌøÎ;'ËeûúÕùf=ä¬Ä\˜6ÀLHpGšüÿ™z³žÛ®+=oŒ1»µÖn¿þô<$±U’J²]pl¸à2ÄÑÉEr“ù!I€ä&1À lØUNI*—º’J¤DŠ<=Oûu»]íœsÌ‘‹¹?ÊwÄÁáÆnÖškŒ÷}žc$²N¢µ&•‘¹;ÍÓ®ÿ©¨(œ5º(Üx2ެ׫劙•2»¡†a2ˆÕvèû¬ÎÉÚ3­5!º®ç˜ÐQŒœ_ˆdà …sni¢$²Ýný0øÀ󽽃ƒ½‹Å²ÝJb ŒÆè”ØZeŒIYÑ~•¾HŠL¾oÅ<|ôÕW÷MQd”¤ˆ8kq1ÎO´KË•DåôW„ÝÙ•RÆ(R9FáÜ­M‘£‚µ. jc´"I‰c9Æ$Ài÷œ DI0ø¨”框ap®PJo¶¥Œ±Niûêõë¶iª²B މGf`ÉgÙ“Ãëû“õjyçÙäwŸ}†Ú®·Í¦n9BèÚVRÒ¤º¡=:¾vëöí§ÏžF£óÓ×M]·um¬©ÊêÃ>|øøáÐu¤4{ï_ŸžŠH L1Ä0øà½u.%FÓýDh»¦éº¡Û®×‹‹‹sDÔHoÞ}71Þ¼ùÖñµÏ_>»¼¼Ø;8ë‰"Êâö Ñhýàû>q ÁRßw"lÙ"áÐuŸ~ú©µÖ; =Æè™“U*çîÊQµX^†Œ1™~CH»Ê II˜w…È>Cr§p¾ëzÉqÉ+ñ/ïs="1sÌCE•9ÑÞŸ„…¥ë$…ŠHk °»„¬ùõòn4 )‚Lsç¨QQÖ„™ìñqÆ©¦®cŒ…+9r1F+ýø¯~ü?üÿ³¢Ñ¿þÿn]o‡FU•šmÛ¦È1ò|¶gµD[˜ÂYNUŽó*­sÔ¸ïû”€â””Ò“étÛ6ýÐOÇG·oÝ:}ñìÌèÁ÷_|þÔh4 H0û~xöì™ÎO”9¤É»Q ÄC;|6¢R”» Ã0Tz¤Hƒ ±f4žä1"…¡Ïž!¥•%'‘Ai-"!öéʉùî»ï¼~ú÷ËÅJ"*ÄD¤™õ¦ÕZÞÿðÛG—Ïûëw­Wˆ]7€HÝ´Zk­ÂjµôC'Avœ"Ùl¶§gg)…¢pD°Ú¬ºÐIð˜`2›[­RY–J©Á]×1§Ä©kjILˆ¨Imäjýæ\á\£‘­ [T®)#]7DîCŒ¨rMzw–Q:?läâîb2)wÒv£¡arJLÁî€Í)çëwå0­uÀ#)RCÔšnÜ8 !-׫mA„EroŸRb€$ÂùyX)LD)ÊÈS¾êC¡ì6¥ôßvÃÓ¯þî·¿?;[–Õ¸AR®R~=ΙÙlzqq–ï Ã00‡ù|®”2J[m§ü%É'bE*Ï?9%kMJI@ÔUëšc ! >$‘éhDs}ß×õ¶ï£ˆ ¾IaÌè›V—WU•Gœ"€&" ÒJûL.Ë[}Nˆ$Ö¨ÑÖ)Ó@´Ò"ä‡!?}c³1Š8„†·Þ{gðñÁW÷SÈ`8@$ïùììâr±üýçŸbâ8ô¢”*œ#¥¤˜"°BÒCdâ$"›õ:Ä û^Û¾A¥t±m~ýœ™ö2É €)&TƜܸõêbù?ý/ÿ+Jz÷Þ›€üõ‹×g«e⨵™Î÷îÞ½sqvvïí·ÿÓüËÕjÓt—ëMÓtC×Éf³0Zß¼uk>™pðCß+£º¡'ëœ&%I´¢ÃýCgŒ±”$=}úTk--ôñìË/ÿðÉw¾o”k6›zµÙ"(`œç{³£þðýÍÏÿËgŸ}¶r¦”R1 aès{0¡JZ7uÍ1ÄïQ aˆ!%Î[%¥52'ï1ÿ,@`÷Äu]÷>€pV…x’’ RbÀF+Ȱ]¥ñââ‚#k­s>„”úaèÛÎ*£ˆÃÙ¢9;›@´Ö"©ï»¾o­+œq Â1h­$ÿ!“âvÙìJJcL‰™œuZ©Øj£• !Äœ%­MÈ d¢(FEIÀ"tÿË/¿õÁ·ž¾xö_~õ‹!zèh<Ò¨ªÆãQü‹ç¯Õt¾'ÀÃÐ;g«ªÊVœ‹‹‹|ã±Æ¢$ >Ng{—Ïž>~o|l>9>x1*·kE(Q‘!mÂëº]{ ärædÉÓF¿|õÊg6ÖLg3W/_¼p6ŒÇMÓ5}ˆ1B:Ìxbëœâr±VJÅ$Ê!”Lj‚9ÁZ)$„Ùd:Å)¢pdc-X¥«ñXœ xbðCßuM“˜S’<(¥8¦yêÝ4Cé2‰)&Õxª´Á¡í3ž7¦D1Öu­–ËŽ HW[DÄ„»?©¾bd¥)„¤òÌn^’8c‘™•¢|ÏÀ"²^×/žŸV£5€Lg£$a»iò؇c̵£¼º"p®be›tJZ[ˆÇ8UYo½Z¯/–«¶í¼ZkAÈÊ(ë €ÌH@ïCÓ4çç‹aˆÖ¹´sîV®(ö¦³M½m‡þ ka²×”9€6TVckí0ø~ð9 ÃÐûZÕIx2£D8Fß=rJ‰µ¦¾gïæ ãÞÕ%„>'ŽKN½JR»Ú3;§I©ª*´qHjµZu]çœõÈNÓQBJ¬UQˆHÓ¶`Œ+K·tXU£‹ó³år5ÄSH‘æ»uf!'ND“ý¹V:¿\,-ikmFôsˆ ˆq¤vOÜœbâ|‚c´Öš92‡œ¾ÛÛ›MnݪÊêåË—MÓj­•¦c×uEÄ™½÷ÎÙÙlêý` 3‘ iŽ‘<‡b2¾{ímbuýàÖÉÑÉ_ÿä?¯š«+E˜$!wâë'×îܹ5Oˆè7¿þõår Ú˜¢,'eÙµõÿö¿ÿöOþi?x"c”²º ¤îܼ;›NE½ýÖ»¿ù»_ŸŸŸ“‚̳,ŠBk@†àE’Ñy³•´1‘S»]_,VÅxLZQ¢ÕåE×lg³ ÅN¢G£]ØÉÑÁ CÙ/ž½ „;ûyºíýHÆ8­‚'Ñ„Æ(b0ºÔF×mÛ÷^HkÒÚ–Ìœd‘²(´u8Ä”O…‹Vë•+ MÊ:‹"ÉûÁ‡êfùêô뢬nÞy§«—M½å”ŒVÖh… ‰’€2f§n!¸ŠÇ I@8…>q”«D Bäȇ“"çªj4%c81$fß5Ûír±Ë¢ E!ÄB¦& Rd©›®šæ4yŒœò˜M#²Žƒ…$kŠÈPÄj@`ŒÑÚH’$œ)(™7”)£˜Ápµçð>*µÛ C_o›ùÞìàp®4Í÷&£ªìßw>ÆØ÷»)¤”éñ)§]%²‡˜C“IR²FWUY¸rzïC×úè )¤£í $•tN´˜ÂEsÁÞ÷ˆ˜„W«µ³n2š(@[ººmú¾Áw]”q®@ÜÕ²Ö£ªjê6 X´¶Ù´¾^­#‡ª*Q㨪ÁZµÞlbÂ)ÆØÖMb!¥ kÓ97Î0 xïкB@P(o’¯ªÅ”X83.÷÷Þ~ûívýË_þL„ËQ%IBï‰Èƒ„Y >Ä‹óËÅb5xOZEéŒÑD¢SLqÓl»¾?<<‚0Ô›µÉŸ©ÂÙþÞå)²3wƒoº@lvðæŸ²FÛlŲΉR@.%D +“ Œvc ßw{û‡{Ó½¿øó?¿8?ýéÏ~2—£É€6-¯þðRˆCw|rëúõ“ÅåúÕéÅÁñ‘sªpv³\tÛÍj¹º¼XGã¸×§×ޝkm}rƒó÷¿¾qrìýpzþ @´¦”À{ýúµ¢*ÚÖŸ_ž¯6–týÖÍj4ZžŸ-NOÏ_)eÆ“yŠÁK—/1ÁG1Æàî¹²?2‰Te!ÓùüÝ÷ÞϦ‘Óòäñ“Oû›è;Iœ$u]cŒµ®@eÜÍ7î\¿s'%€¦©ŸÜ¿ï»¡kZ¥Uá ðÞKâÀÙ#1Äp…;†«Lv–§_»~íäød<O¦Ó[wnÿæï~}±¸œîÍŽ¯]ûü÷Ÿ·]g$±,FúÝï{¹X­ÊÂ|ûãÞ¸ûFÿà1z2™¬V«aðDª(Ê\áÐJÅ€°kÚùÞÑÍ[GQôÑõë!ÆízÝtÛ²t³qIÒÕ›¦­ONntm{|x\UåŽ/cÚõ‹Ó–qõ²#¨’0G”LNNITŒ©Ñ²EY–ãÑ€´-¶ÛºOŠ¢¸8_$°Õd¢­}ñòeŠ2U†)ôÑ}C¨ÊÂäVçf½²ÅˆŠñX;åF>MŠ@(e )­ŒÝÙD2_-†Oa§3eÎì/@ ƒ,Ðv½á„ÊR’q9.LɱšËöâ¼®kÑsǦ,Šé¬b‰9Äè# ¢*»>nÛõv³ê9ò®’£¬-”µùÛ–·…1Æ|u‘˜½a I@´Q;&;j2štv~fó\‡~È=*ŽÂœêm[”¶(U£ÊEá†l†Cìú>·µ¥¨*µ±|`ï{£”Ñ&˼÷MÓú!(DÐyÌ ( 9Æ~œ+RJ}?h¥ ¡(œ"ìº:ï DЇ¸^oÝ¡s…cçl!ÆØõ-ǘýS2 Ãâr±^­Û¾çZ›ÈQÄï2ï %M§ckÆÎéQU? `¡uU”Óçß¼dmˆ•"€%Wyw8qâœþP@bôÁ_^^¼zõB)…¨Š¢ð!¤ŒÖÖ9E4 CŒ!ÆcTZ‹ÎZÔÚe‰÷œ8 =n· °´v³YG iAEwß¼÷ôá—ö!D@0ΙÂY£É9gŒÕÚJ‚”0E²J•ZU¤œ‰$NQˆiç£ "ZYúé/~µÙ,Ýh!­V2“ùÁþG}ðËŸý4t !EvÏž_;¶‚ñxTãºXômýøúÛo¼i*óþßÿ×Ûw7Ûõf³ ±ëÛº^>|ô¹¶DŠ´Ö’„#7ÛZRÚn×§‹ÅÅåé«Ë³¤éý>|òàÁÃû÷Ù{gõ¨*§ÓIÛûŒëà­1Iä*˜K"’)ç"2 ½«ÊÙþÁh2ûÝg¿»¸\ܹsçúÍ[Ç×n\œ>÷mL>ú\™¥Þ¸yãèäøáý¯^¾|¹7ßûèÛ¿ûþ{øÝï%12GæI2ì:£ú´6¨„û!ˆ€$qÎimæÓñl¾w¹\>xô°*«o½÷þ½wÞm>ýû(RŽÆßúà£ûøCf¯:gn߸v8ŸMÂÑ;cœµH|TŠ&ó™pzððÁÙËó_Þùê) o6«ÑhôÆÝ[óù¼©ë$‰H!>::–$‹Åj±ZŽ.~øÑûßúÖ¾üŠ”MF°®·Ý0Ø"ŽÕ„×o\ÿÑþeÛÇßþå¯~ñ+Yl¶/_½te1›ÏbŒ €B9@LDG‡GÇH„Ëåêôå‹ñdV••÷¡ëúÉdÔvÍv»A€ÓW¯b·æ0Xk‚¶õöÙ³¯µýM´.?zï®F‰3 2íf²*ÆèDòÊN$景161(‘éDKB­k UçÓÙlµZ áY×·œb”h‹qï£q£Ñäк‘5h”¤”4¢ç$“$ñQ‚†ÐWóÉ˧_ǧó½I5åÔ6€¤\¡]É)9K|Y8? ËÕZ•e½2›Kkµ$Ié ýö’ZšÐ4BÔaMä7ßxs»iž=}vyvîûPª•ŽTQŒöÇÅÄè"Æ„ bJqB#\'=€²¥©&{“ñRœP@ IDATê†!44’RÞš:“ò\8Oçóþ'¿Õp…KÔÞï ð_‰˜sm´÷áâ|Q”N, —£θq9å’™9×h¯˜qi)¥Õf“ •e©µÀzÛøaèû¥RJœ>ùV9ô;e;€´ÍÆûA81ïB÷ZkÒBØl7ÎÙédT–e’$)…Ëj‡¦Y¯6]ïIkb|dEJPi ’êºö¾¾Ô„ãqe‘5“ª••uEdT‹mŒÃ)²‡žrˆ6&æd­Éœjk- C))c­÷ë—Ï_¿|Á‰¤(‘J<ä(ü0DH‰“ì¸C)¥HdòSìî›ØZëŒíCŒ‘¯µËÍN ¤O®Ý¾uûíg_}É^`H7+k (`b`ÀzVíOç ¨B ‘‰õâ R@؇ˆ4& "\8§´›L÷ö®½õÎûVÓŸY÷0Q‘™=[æˆfZVÕ»÷>¸~|ôoþý¿¦ççœ8)Dk5„€d:›¤$Á‡ªo6[Nq½^›¢j<>¹~óΛoŸŸ¾øÉ\*¥@¤ëZ +G…³¶p ˜ÉÀ;¸ÛU:e—ƒBM¦£éôéÓ§§§gŠèÙ×_'“ýÃíú2t &‘];O®¬V—/Ÿ}Í!.ÏÏž>xP”UŒ1[_ÒU±†¬µÞûÀ,ŠD•…Í £Ã£ã““ã¶oCŒ}× Ãfµùì³ß¿ýö½{ï¼óìÅóÉxt°¿_”v»ZoÖËÙt<ÛÛ«ëmŒ¬Cð>„#*¥´BÀ—_¿¨75"(MñÞ½7}hOÏÏOÏÎò¨”ŽM×¹Âq’Årˆ“éD8­×«í¶AktQ”>†‹ËÅb¹b­Ýd4¹yãöþþÑmG¿ÿìómÛqJmß—£ µuÇœ@‘3&Æxv~Âói€€óÉcÜ›Lç³.ú€@o¿uûæ¯þí}®×±«XkH) óx\…0è¼ÈïýŽÖ¤v ‚L˜ä<ÝÈ›?$„H€ˆÊ(*…!r"mæóÙññ œŸ¾úÐõƒRf2žVÕd2ÙûÁÿñ¯ü8¬•˜›Dš„¨´Ö¹Ñx4쯗eÔüðð“¾C /Ï/T5ží“¹1vTY…BN›×g§›¶ !<ö‹Â)…]×m—Ëíòl–©tÊÎPÄ÷]dè×Ý«¿ýÙvq–8X7Ý¿1»ûÆÝƒ£CAœLçÇG7öY”‚qîÅéÙëóSH¡^ž?~ðE_og{ó㣓óׯ¹ë²o!_óµ5ƨµ¼¢À féLÆ-彫¦ldM!ì.²ùÚœ«MÓ¤$Z©$©®»®ë­Uµªs^8ZŽI©וušÁA* qÊ!z€]/I¾É%ü#͘H)휛Íf>zc B–hGI‰Œµ  ÒE5jÛn|×u…sÖcLJBŒÑ'QI +?2›Æš'·®ß솨ˆÆeyñêR@+§˜õjÕœ\{óõ‹ó²ò“ª¨·ÛbJ$€‘@d ë&׎¯¿ùÖ=…v» Ç'7¯•“nð¤uQŽ÷¦Ó¡é^=}µ¾X={ü8±G5b 1¥ßÿÀ{ÏÀû‡ûúY5™ÍÞÿdlM1®Fç—ç—«ó““ã½yÕl×<|ýôéOþËIi×MãœÍ­.R9롲ÚT·¿жÍEÛöC›Ç؇à˪œ ¼{ï^Ûµ?ÿùßþä¯þ³ÄHçc£ª4ÎåMR*˾¿áwæoˆR*2k§ÓÙÁÞÁãå*ÒÖåØkæ·&HP••Öf:Ÿ®W+•˜÷CßÕÛ²(ЛgXN*åA\&p„!(2UQ à¦ÙžŸ/¦Ó™uŶ^´mÏ1ÐâÙù™Öj6™¤àkß[g/WË= .WÛfÛù¶'Ánèû•ÖÓù¬*ËõjKÎZM*îºîàð¨îºWgguÛÞ¸~£Úv¡”>8<˜Ïg_ž¡QªiÚñx¢•Z\^c"GÓ|ï`2ž ‘úÎ'bŒë<Ÿž§$MÛ••íûA9Óv-)u÷Í»mÓ5Í6#‰LˆÊY1FðÞ»×oÝü÷ùãñüúx|˜ŸŸžEß§0D"„}×Ì7Û sȡԤ•ÑJB@¨Éäe*ò12KQ¨üÇè‘©$"NüPU•RZi]…-ŠÂYEx°·w÷î[‹Å¢n»à¹(GÎY©mÚr4 ŽP„c`f@•nÖ”£j½tMüð£ï‚ƒÃCÒF’Ü{ÿýë7o… œ*ƨ•±„qJkOî¾»\\F?¼õÎ/_Ÿ€9 ý¨Í÷æ>vœ¢>D‘Ûº>Ø›9‚'¿8;}¡‹é‡}rýä„SL£ñl~p2?8öœX’°8g“×¢÷ÉwÂüìÑa9}ýrq~‘ fclYŽvƒ‚ÌãœÁ”œ]ýÆþÌÌ(12¡Ùa÷9ÆÝЍ( kMß÷1F0Ú1‡® Îá8ø!n¤Í[$UÁä¤ ¤œã !„À›õV@ò%_&¥Õ!Ggt¦í~„Q‘Bë®àõ».Ä*¥8¥~¶õÖhm©ªªp"QJâ½Ïñ«ÙhÙeÔH)€$Rk#M;´ƒ·F)¥ÔzÝhcš¦ß«AQn«‡˜sùSi¥­±9]ž/1ÖZç\ 1‰0ÇÌûÑF¹¢RÆp’ºnµ±o¿ý¦Ñú«/¿á¼¶`˜ŒÇÅh„Ú\Lƒ&5 m„éìür)«b¼—°ˆ’ü ¾þúl2ý‹ÿþG½ÿÆÍãýG÷ï?|øèù×/^¼<]¬{e§åþád6QŠjäŠÑvÛG¥«½£ÉQi,tç«j6;9:Ú³þèÚ7ß>­©èëÓfo6-ŒYœ­©ù£ÿΕ@$H]×ù¡]¯.9ø;o¼õÅoÿîÙÓÇ¡©‹QiXÅàEBþˆ@’`JéÑ£G À‰CHªœÏ棪b€~h¾øìׯž|å0QY ¢(­$)ÐZû,üQ¨4Y­ó™7Ó:¿QGeÚ¶ýÃï~W£Ãý£ºÝ삆Š@@k<“ÑEQ2§×¯^•Ÿî½~Äaè}ßc¼yóÆb±hwÞ]æ3Å>3Êc eQ|ï»ß{üäÉj³^­7?zãîMÛ´]O¤ tmã`4õ}OF÷CyyQXËIX„œÔ!0(ÅI¼÷EQìC’E\&fE(Ûzóð±'­|Ûº®ªRYà œ¢Ñz4®rˆ%ªnè‹Å|¶_–åz½<;;Íj¼Ívc”rÖœ½þÉÿR[Ï^¿~9Ýß»£•ÑêÆõ“Ó³—«åj2?$ŽÃ00G"4¦dÎZ¥uðCÓu———ÕäàöÍ;ôIJ²Ú.›®‰U†6aMJq ¹F®ÉäκÂqŸ8qˆ1è„“d\ÞäßžÑzeUy NÖÚ"Æ ]kÛFæóÙx2Yoë‹‹"ß4õúñ£/µ†¾ÌÎ Á ’u)CH„F”*ªÑ‡ção¬"B\,–ƒ‹Õ*Šb•¬v=+…„!휵Ú& ÝjuêÛº2t0-Ç£j:Ÿrðg§—O>šLJ Þ÷^ pÕu¹XnÄj2‰ ªnòÚÓ9KJY¥D€‰Ðjcmbîú¶ï[^eæ§# ìÐÁ*‡#¯–ŸYD,ù-U¨"sƹäUŽI;9FÞ…ì*|*ËMˆCÒÊðÀ„”F$èºa S­£ÑÈ~è=sìº.µ”RŠHiMD»A„ˆV†cF#V*Äsì!„ü?Î%#\AÛíÓD$¦˜DÚ®ó> >¤¼4DÌJ"€9Y{‹Oˆ’sŠÉCŒLZ3#@× ÌcÊØ•-Έ1FB"­RJ’P‘ʩϦir§w‡½È»ë]Ý@g™¥b¥”Öª(˾m3.?t=sËÃýýºm[QBT¤!y~Öt1°ìíüðþãÉtvzq–’ß›OFUUŽ­÷«M×Ý~ëÍ{ï½›†!=}vöïþÓÏ:råx²çŠk'‡éh~XNözÖ_<[•£r[×wF£/_Þû䯇·ŠühƒDÀ`v]°k¸Þƶmª)'ÞÔmd?ô³ÏÉÝñln‰®]{ÿÞ[ÿ›_ýä¯ÿ¿M·yóîûç¯_µõ%ð •áq5Iƒï(‘¤PA¾ºW£Ñx<øáüò¼m7M»}ù<ËùPi(o·ëJ”a/Ϋ5\ owt+wa³\ýÃ?ýÿäŸþ³õ¯þŸ/î¯`ú$B€”òøP‘‘П}ú)‘ve‰ &ãIYU1rß³é‘DÀ9gbŒÃ0ô}¯uRZÂÞ‹ÅårµòÁÇÕÞþžÖÔ6MÛµ„ÚKGû›Íªï»ãÐuãÉx6øa(ËŠS2ÖzÖ›z¾7Lj˜8jBܹ>v®» éAŒÌ`Œ­FÕÅÅEJ¾ï»¯¿~2*‹²ªBŒ 䛤ƒVÚ9'’©wÞz»kê÷Þ}ïlqùìùÓ¿üë¿úéßþÍÁÑu4åÑÑÉÍ›7šíöÅ‹W¥³aè?z¨µ 1ã|±°`jûæg¿øùñ;Î9Z}}y¾Ú¬3ÄsÖO]\œcDƒR¤5«•ÑPªrÄ)Åè}ßsJ„”•€ìCÔÁ8¥Íް‘)(JA6+ `Ûwƒ÷eYr uÝp’r4žL'Ãà»v•Æzõüi³YmÝ×uUŒ„@m\el!ÌdÇó·Þ{ÿÎ;Š(E^o7½ïÇãQe\Ó…¶çåê²k-dCž±¤Hߣ„c½ZõÛó½Iqtp”8¥«q¹Ýn6ºªF7˱-ïõ»Ó—ÏÚm3ª¦Åh "œ€Œ"4­‹£ªPD‰S²°Lطè*X )L‰@ëœï# B„LŸß=ºïsVW!%望O@ßÄò6{gwéý'9yjOD¨”%ŠÑ>dP„À䓆g¿Ûj¤”ˆàààðøè¸ïºí¶ ‰­sJ©CÜnÖ5v¨ÊÑto¾·7Ý›“1‡7®#±¡DÛ®“"©o7„RX«Íxv°ÿíïýÉœ¶¦xóîjRµ!¸jƒ·5±Ä½7îsŒÇ{šb°1ªŽÇ&ß„@BÙÁÓÇQU€0A2šPÔnØ© "ʼnÀìàï»#¼p"CJbΞ^íZ“Ö:+25ÏÁó™ÈZ›·Ü»Ž"¢Ê&?­5þWlÈ?þ Åå…÷ƒ"«[ùö¯wù /»Ò~Ã6ØÙ#¯÷)%mh2J©]'[gùbÞâ’sN+=ôǘ7»Î­5åãÏ7½ðœ^ÿÆI’ÿµ²,­uJ)#­ À§„’™!¿$Ž1ß™ò’oTUUíïïû°ëŽg6ÙÕjZ®l-)Ÿ)˜cÛ´>ì0Ÿî‰È:Ë×ËuÛ¶(¢­…$Ñ{0 2[Z!a Ü÷±ÕªHZ¡d&(2`SŸ˜‡¶Úe¾jœsóéþj½í‡ºn¶);rûU|ûvQ‚T–ˆÆŠ ‹æl/ÎÖ÷®Ï'N{P¯jøüñöô¢#bBÓ*”¡­/_÷V;E&aD!IÑ÷½©Êåj5*¬¶vÿ («/?ÿ|½®›Mã …à%1€hmR”cQ”ÖP )†,¿>;}çÝ÷ ¨õâtâF³Ñ´÷]ˆQP($A@…`ŒB B@N ’Ž ”1J£VŠú¾÷!‚ÖúrµŒn½q÷ƒ»çfÜ£G´V*ªüåþò«/3£*˘îÍ÷÷ö?~²^o“È|úÖ‡gg§Z«Û·o9WB–ÿÙoŸnD )L) *u÷½ïÚ¢¤äªñh2VJ¥­Ö1Ä ("$ÔJ#aŒAkв UJâŠÊ:8.Kï=‚ĘS®ýkmœu/_¼<{}qÿóÏ~óËŸ©Ô[m¬µÎšÌˆˆ1f0vzpòö›÷Ž@À®MÞw÷žI+Cßm0yŽÃÐ7Ûår}yÛÚo±[7«Óv}®“;íŒÑÆ&®ëÖ«e³Ý..Ï«‹ÕúrµºŒÞ[M…6»ñVö Yg+«ÑÞ|VX£3+!4!ñÅùë§¾DaNœD”)ªÑ¬(ÇD„„`É#kí•ÊXk³1Ÿ[Cyص‹ÃKÊúÃ|´õ>DŽJ)R”’HÖƒ€äÆÿ(åÙ=€aæÄú~0Æ£‘v¢¨üb¾YüÊ-Ÿå#Ç+tÁŽÑ*"ù™wõ:!ƒ·‘R¦( c¬<2'PDÆXBÔZ'Ù)“2 -cùò\ˆ9©²Yãò3$‡©¯ÈO)éœzfÎç(ÊU¥"s×÷Y·7«ùý„«¼M!rÈøaè%%Bø†‘¯àJ)ïGVÚTÕH“aå;ƒ+Rþï“éx<›^»yA"æ(,’sÃ" €‡ÈAÛÒGð‘Gãj2)Sô¾¯GŽÞº}|8Õ•IUAÒøô²}ð¢}qá_]4OŸ½ÞtòùÓõ«M¼Üt1ú¡kyè4r¡¡4 S8ŒŠâÚñQ½­‡¾ÏH5?ø‹‹sk•HÊ´ù|R–_üö·“±ÔGfÄLõA@2¶,ËÑÉÉIb”³ý£ï~ÿ‡“ÉDB´D{ãI×6Ï^¼HÀ9¶ã g]©´²Ö ~ç×+ÛH¶•†|;çÄCðMÓ¼|ùòáÃûD°Ù¬…W‚ÉüTªG^­WŸ~öégŸ}–@ö÷b ÿí¿ø‹?øÖârùñG×MÝw"²Æ"dq½ô}‚¿}óÆ~ðƒÕbQeYVëõ:ŽÞ¿xñüå«—û‡‡ãé´ï‡¢¬N_Ÿ²÷׎O´Rï¾ý¶p'MÓ6M»¿¿_8ÛÔµ÷CY‚¸Ùn™#§˜K¶9SQ9%ãÜÁÁáz¹ôm§H1§¦í@Ò›o¾yïÞ½ƒÃæiLáÎÎÎêÍ&?P¶}·m¶ëõzo>‡DÛMcœ»výº÷~¹¸h¶›à‡ª,¶ÛõrµTDeQÆ|`cm–@%械oÜ}ëO¾ÿÃmÝ4uCÖ&Ôßúà»Ú–"òòÅÓóWS»†àï½õÖÙéiˆcŒ9€'ˆœ’ï:d™ííŒËêÅÓgË¡‘ÈL( € (q"B… €$ j@D£uaMצíjEJ·œ£Y´¾8{½]¯+·7ËÍÁT—Îâ²»Àºb2O§UŒC?t™~ã{ß÷]ßÕeY‚1ÊXî[Å=)·0ô¾í©uC[_._‡X£ðátÏývs!¡Ò¾ÎÎOÃàCﻡoÛ6ôC ž9FðÀBc‹ÒVÕÈYg”Øé¡cŒ‘%Óæ‡¦­›¦V µß]w¼u"…€’çKóEKÉû’W ™³È)åU*"dœ0!¢¢¬ÉM€„>"²Ú¦ÌhŽ!òß‹1ä&Q~ÐDQ„0Ñ.i)ßDµBÎ ‘a°"¢#ê¼_áH´—ïü\;Ûݮ꙽€’’V:㎕¢¸ï†|·My êw# Œ«$ºã\)ÀÔàÃf½ÉÂxCjðƒa?13+­F®Ê³š$œòc8§X·uà ù}!\í‡ piŒÑÆèüææoÚ=f)ŽÑǨ•6Zçû.ÆìùI1&‰•fÀìNu]›ª‚<çDB«4¢`FDé²*uã¥ë›fYºRûað^ž½~ñ¤_­×Þ·mÓøjXᨹk·Ë£ë7F{ûª(•¤1±` 1Ä¡%I³ñäñýûþðЖ%К€T×¶1”!™]ñVß¹õ–V&FÏ!d¾’’Ä®,®5ÛmÛ¶Z«élž-]×sâ®m»¶¥M¼©RJäÁØ÷]Óµ»/ *DÌAv"Ê¥ %æÈiµ\FaD|ðð«élV–•o¶#3k­Qi². „ØÇz˜µµ_Ýÿò£olœúþäèÐýéë×aaîèågÑ3Ǿþú)sh»µÞ;8 È©í‡”¸ ñìâI¢&]Ùê›7SðÌ|qqÙ¶Ýd:'$ CÀ±â”bL{{ÇGMß7›­Ñ6 #Qð!†Ø¶]Jìƒ7JcBð=:={íœë»~TÞ~÷Ii:N§ÓÅò¢í{@ Ì›M}°øüù T4—Ëåж)F!@„ã“ãm]Ÿ¾ŽYÐ)BZÍ÷öÇõb‘÷]ûé§ŸEÆwÞyÿåÙùd2½yã†+§Ÿñ»M½œÎÇçË—Ïøýï~ûƒþîÓ¿[Õ]¶*ëÈ"ƒwE9žLÆ£Qá*m¬6†“E™„‡¾cMŠblNÃgF—0GII+P†zÝÕ2N$ÅÄÞDpeÙ´ÛÕr9Í>ùøU¡ñ;äó¢°Þ{!(Gc!èý@JC „)qª›¦í:«ÉÓ4 ]³¹|²ZŽgsí" aˆ±ëº¶n$¦8„ÚÇ®m×›³˜ÚQYHYM}‘,HRòÑÇÀ}Û÷¯·Û¡Ýú¡ Þ Ð•ªFΙѨrÖRäÔCa5a>/ïˆB¼DÖ„*ïò½ÊMÈ«²)˜Ñ Èœ•åIDªª²V·mË̲;¨¦”ð›¸dQÊ^9Í1)Ê{0Êþ|ÉÍWáî= ³\-"’RNë ïŠ]¼ÓzôAò.—(ÄòÏ,T•ʧZ‚GDmô7“ú\GÚÉ‚s­‘9æ³RJ²r^þ8Ù‘‹”ÊWsH,mÛ !!)"¼‘ç³…ˆP’X«SJÞ§\¦SJe)‘…y@@й; ”Ê›·<ÌÉ„Ë$i†«Û­ÎïR:w;vHþ+cœÑZi•D8ưñù†œv—3 ˜Ė• ÑFfýG.Ž(Ú½E@ˆ ”+SBL IÝÆwµï#s:š¶Y“¦ëŒÖ I¬òÈ=‚'îÛvyñbÐZêÓn2…fö½V¤@ H×7O/Îg“ý[7ßè†þùó'€ ´š&] ¾÷ç¯^-Ï_½qçNUím—ëÈa:Ìû6‰ˆh]¡Œyñ곇ä"NÊ‹©ÛÖ¸ªÝlÛõv±X¼~ýÊhåƒd•›vÆ•ÒRBqµ2®D@!BÙµ”BT1Áí[·oß¾µZ-îõeè["ªëúÆ[{{‡ëÅyÛ!cÜÄæ‡×ö÷öžýärq>šîýÙŸý7=ðÞ[ãþßóo“÷)ÉÙù™ÒJk•ïñþ“ IDATßÖ:‘Š(‘J)½xñ2²ˆ2Æ•E]o™UF@çO†”|ïû?úÑ_üõöüW¿zøõó‹³ÓÑhšÒ®ß.~ˆuÝ,ËÑtš8"‰VhTÁ’BŒS®JJUY¥½÷hŒµ.¥¢7VWÕ‘ŠQÙstÎýÿT½Ù¬Ùuå·÷>Ã7Ä‘Ój.ŽÅ*JIKm«[–Ú6аå~1 ûO³alôK6ºÑj¶lY¤Dq*ɪ[Ó½u§œ3Æo8ÃÞÛ'ò’Χû‘7â k¯õ[ïðÁW_=þòÉcÅ!„ƒÃCëŠXC™1ŽÃÓ¯7“ZQY4ŒcfL§MÝä-!¨ä0ì67¾™ž¼þλ_ßüÃLJ8NfîŸ~ùù¯oo—JÒ¥îï~ôÿN«V˜€Œeõ–Œ)9k ‘cˆ1Å”€œóR«HÁc:ãJˆq_CNÄ)13áÝf 1EEDBFÀœ2aPÖƒ£ƒãÓ{¯=<}ÿ¾û³ýkgK¶ØŒ}¯ èLNAb–Æ®3d¬©C ËÕUŒ½G­ëêèè ‡ínïòrCßmV+È1Å1¦•GÔˆ‰¬jí ăЋ tÛ~Øc· ã.§ "B6ÅU›ªšÖ5FT­AB1Vµ÷UÅÂ)&T Ú·‚XkÒTAµàâœ÷)FfÁ}Y]©'“Ye~¿Ãª”ý^=Ë*cE.:µRñ#’î{¥½³ÆPÎÌ™ÑZ'¥Ãc/mÿ~ ø~ _|;* *Æ’±¶¸wtÿLJ¸ X¥Ð‘î4¸ËÜñ6÷Oµ<,ÞU%ˆŠï_ýöbûQÅ‚3"BQ)^i"rŽÌ:ÍdΪttxˆ}ß5uÍ"¥¼ˆ09ó|~€hXôøøx»ÝöãÇ‘sa…ï-=Î9ïÜdÒ¦;Sö«Æ«jxc,³äÊ“QS`ËT€PÅëY¤ÿ#䔳¨‚Au¾"cQ¸hJ¯ÚE•Œiëƒa·Ç‘9 gaN1Çзu]»qì%t,†c6Æ8M@5¥4Ì'nÝwÛ«‹›ÛåÊ»ª®%gáX93m'G‹C`™¶ÍëOæÕxµ®*Ê9Œý&Ûj:ãØO›*tÝzÕ½øê±@Ï:ÖŽ)„aÇ$2µÎ93—ë³D€ÛåòÞý×­qUU§” É{‡Ø²dl›öÁk¯¿õÖ»Ÿ|úé®ë'ÓY=i-+ÜÝ„ #¯¼Ã]Šùôôô¾÷½ífõøóÏŒ1u]!@]Wd>øà/®®?ýⱟøª~ë­7‡±¿^w¯¿ùÎ0 /^¼xóí·¬uÞW«®k*¯šT„Д£Þ3ÊAUŠi¶ë:Iép6;{öXš¶ΈTHæ¬úøÙ³ÿî¿ÿŸõ›ßÚ¦j›êz2†\ÞÛ1fç|Û¶»ívÚTÇ‹ÙgW/°®€‚}çÝo¬V›Õí-îK7EÚªöÞ9ï5}ßÍfÓb%ŸÎæÀ"q™y±XL§Óõjs6–@A­·“¶™M&×ç„{Dªª"cŒu¢“iÛÔ8×§Çœsâ¼Z­ÞýúW7×! C/¾øÄ9ÿúƒ‡ðþ÷οøuUå/ž=NC2•™N&dÚ$Ö6º1†-©ŽÃnµ\c_:Ö¬uªÊœ¡|f‘ ïDÀ›K ²)Øò¡"ëªÚEa€nì7ãîøþÉ8öÖjí=¡ãÌŠè¨"@BJÝnÜíæ³#è†xqqæñæä`al%†cJŽ,"Æ1raÑ%÷–¤´ôØÂÎsÊ’·Ýz·[æ,ˆ&ÎI”c6)Í@usPYwºX,æ}ßeÑÖ’Õ,Œ Þ8‹hlã*c0Œyw“À€YDXRÊÞ•Š±=ÑaO…&%P5Ö‚g† s.{CÑÜ_­‰{û À8㪦)ùÌòþÃßÙÑÐZ[­…•JD@™U´¤à޽¯”¼ëõvÎ!zÀ²êafF4%â „È"d¬µz÷€”""îIRNƒr‡2Þ`°Hí¸† ¢ , PvGDÑÊû½Œc-"¥˜êª²Öö}?iÛ‡0ózí*﻾O9›ý-„rÎÖúÅ¢Ýv;cmÕ¶õtúâÙWÌÜ4Íl6»¹¹Aï}U×ÖZÞ“È))§ÂIvÎ…1”ûŒ!âœKõ–F·¦žO§ÓIÛn¶ë˜! Yg÷ÞåÌj „‘ ë<¢‚ h&C è]•R²Æ ’ÇЇ~´Ælâ®­E¨Œr€3¢õXyŒ½÷ä|ýòò:ç\{¿Û,Ÿ?¾%Åù¬žM›¼ÛtË›“Ó{ó£ÅåÅó'O>-‡'ﵡ9›Í¦‡G ]s?bca¯¯–Ëq·›]ÕL›¦uÞ‰P‘¼ !e&ïgͤï{ƒ8´mÛ¬»]LœY®¯¯no®½¯¾ÿÃ?>¹÷ðù—ÿòêꪔÏM'3cªó³«Ý¶?9¹çmÅœë¦^èAD`Žã(tçû*žr+¢Þyç|?ŽÊúâùóo½÷Þ“/w]oŒÍ*ˆ8cŒ }úåç»íö½÷Þ{ëí7?þ|:iÎÏÏcN!†œsùz_Å»ÍúðýËOŸ<}"¢ßýàûo½õv ŸΡ±³ély{3 £±–3§ÔûÚ/f¿ü¼ªÛƒÅ‘sA¶«53_^\,ÛÍn»ÛžŸÜ»OXV«ÕÇ_<{¶\.«ºn&W××7Ã0,‡Î»Ýn{{»¬½©,}÷ÛïmÖëO¿øÌÔÓºm>ÿâË]×_/×c :¦q½Yƒ‚!K(jTr&¢ã£“˜hµM¶®k$Ó´íb1WŽ«ëËõz³^Þæœ%q¾B¢F)¸.Ð"Äsæ}‹¹0 Qæ,BbÙÙª8ðêº:X5í|µ·}φÇÏŸJŒHÊɈ KëzcŠ»uºc`f£nX‡ÇqHUåbŠ1²0d#mÛT\CŸSTI"QRŠq´Ö  w®­ëíÍzÕ-×›ë0n••À2‘M¡w;T%"Ž©[/ãñvsy¶:?‹)‹JuÕ¢3(H‹Íf³\Þ6D2 ýöæÙW_ }Ç,!¤ý€P4Œc!È –(+í¯ò"q "b­1ƦTM°oÁVÉ™ â­(Ý´âï¯ì`HDP±hˆh+Õ6TúÎ GT÷íNðêg÷ç2Æ;oŒQ-±©½ªS(r™¹|˜eGk‹hÌ¡<ßbz,!R-´©’N,š¡÷^  5 ˆà½+„nÆ‘ )`N¹®ë““SP¸m§“~èbŒ¢ŠÆÌæ3²ææúz³L§ÓÒ´Z.on¯ŽŽü>K 9ç¾ïû¾+eXd¨Ô1«–ä##"’Zcš¶Íf·7·ÌÒÔ5”X€³®ú¾ï5†ê  1DÌ©t¼Cuã}å¼·³¦jÚ*&ëTÁY_9‡9)0CLH¤¬ 0¦¾únwËÃh ¡õRZ_ÇŒµãØ•ôЪZ¹&¥œÒâp¼X`xØÅx'cíÞzã-ºu¨m?ô¥>'GƒXcsN‡‡‡dˆ UmýèóÏôw³^µ5BÊœ³xï§0ƾþóŸzWÏçó˜Ó¾ÿ›®ŸOŽ ”—ËeLÁ{ï¼›Rò®Š)ö×5¦n¦ªCd!%#‚d­jéOU$báÆ×ÿÁ?ýÓÍzýWýו··«eí+²oŬzusû·ÿ÷ßþ‹ÿô/¿ùÎ×H@_žßŸœLëÇú½?ü”úôÓO¦ÓÚšó~‡ÊEÆ$"ïsþèðxÆÄl M§³¶iÓÅ%³ì¶»®ë¾þoüú׿îÇá›ï}ëðè𫯾@B<=>²†b¯/Ï»íÖ<9™ow»Ï¿\.3‘2ÔArzü凳Ó{B̾qx|»ÞÜ\]ªä8tÛÉd⬊÷7gï'?øÁ÷¾ñí¯?~6Ÿ6uýè7¿IÀÆ^œŸŸœÞûÃï}/ Ýb±¸ÿþ¯>ü0Œ£8÷èѧ‡ÿþÑŸýó¿8{y††ÚÉtzŽqÚ¶¥^Î’B\çx~þ·ª¬ §“Å8 @Ö˜æ½o½õÁû?TpW77»~# @ÄÖPUWÌúìå kjrs[z‹š¦mÛIß÷—ݶ+î…œÁ;[Õ5E£ûžN"2ź÷e#2‹3¥­‚@¥ˆH(fŸÌA"cõMŒÝÓçÏrÊ ’ÙPi€ ¡ßÒ %ųç_2@RZÞÿÿÑ’†ÕßüÝß qÝÖ8tªˆ•§³ÙÑbqyv¾º¾Î±`csã°é6W7—Ïûq+$ŒªÀ™3ǾK}ï¬%ë*€°ºùä—?yDÄ¢e§CŒ¯]U/—Ës;iÛÊÐÐmâØYkÐûº™Ìl5)µ@¨,ÖRqÂI:çœròεm-"!—F’¢×8ç ò¿ˆÈÅè’R2ˆ’Ù£¸¯ž1¦´Šî-.ò;ÃA_ :K£ê«&¿½i½´à¤àœ-î”r¸Cˆün½.讜3QŒá.€º/ã½CÕ;f)nEUí‡AUsfï}]Uã8úÊ–U8Ƹ/2¶.ÄäŒ}øðÁwÞÿnÓ´¿ýí¯7ÛÍ®ëSŠÃ0t}7NË­ï{U88X°Èv»aD„Û›ëÌ\æ}ßà "u]YëöžŽý«6eà¼ïM0dæóƒªªvÛ-3{ï§ÓiUUÅó7Œ= öýrï,Š Â"9{§ÚÝÞÖxTY³o£®ÔÜÞ\¿|ùìäô¨vöæjys»ŒÂuåÅÄõ8*GeÒØ[ˆ1†„dBL7··q†ƒùl4;åP% }‡ŽDŒ•ª&×ÃÓ/WU5™¶>ýd2mµmÚ¡‹,ù·¿ù¹÷ÕÁÁüæéò£O>Œ¹Ÿ¶-jÜÛ^™ç¾>/–·kâ ’p³Ëô£ÿs:;|ÿƒo }¿ÛíúqÆq×íRŒ¥¥àÕÂZk¬UМ“¢!‹Hº·À  SŠ¿úð—ã8¤qü³?ÿ³Ó÷?y¶8>žÏf7Wç»Ý6f~~võé§_¾ùÖ×w»ðèÓG)ÊÛo¿ûŸÿg©˜ïžŠÈWOžŒýðÚk¯/ooPQXˆ\©EáÜ÷Ûa !DDŠa¼Ebfcl·Û>úÍoÞÿàƒ¿üË¿ìÆÞÕõ“ÇO–×Kq„³Ùôúêúêêâù³Çœ£µ¸Y_Yë±3zzzäœÃØmWŸ}öùÓ'_Ìæ³?û³¿h&‹G>ùðg?k*ç+'œo¯û¯}íݓÖ·,™ ÆÐô«_|烾õío/oVŸ|ôÑêúÖ[CÞö}÷ÉÇ¿yçÝwNÞá_üü§ÏŸ¿$"H)þò?;>9>>>BÔóçO¯¯®bUÝ:gYÕZ;›ÍbŒÅ°0ýw¾ýÍüÉŸþâWÖëåÍõ•s€zç^>»¸].çóƒ—cSN*jÈë ÙÈÑýЫín\­;ï}LRFY1³ ¬j+¯DÂì½'21e—rY›^MáîÖV‘T.ì†D8ÆB®êöèèp2}øÍ¯ûö«_±DB*üvNr,)†~·>{öEýñ½ÑVÇ©Ÿž “aØÍÚv>i8³1T9Ë,'Ç'13³°1f6ŸŸi¼äÐ43ˆÎÔÍT…Û¶ùèãßœ]ž-¯/ç‡íÅÙsÂ}¶ŽEêz2;À«›kckcMâ$Â1õMC·9=:4Æ:kw»ÝÕÕ•o]éðf1Då4VE8ò*YÉzDH9£±¯½ö@Óöúòò³GÞýÆ7Þ{ÿýÄòâÙWŸüö7Ýf5o§m=ýñþþ[ßùÖõÕÕ0tñç¿øÇNOŽ–×g¿þõoÎÏ®ÚfºñÞ½ÓÓã“]·ÇQrÆ»n¦§,*¢u]çœsNª ’@aÇ?þèþƒ€t}ss{»rdc ?ùû¿C0ÆÖÞՕ߯žÈ_^œ«JíìÍÕÙ'9«š<•…§‹§_~~õòYèºÓûoŒ1M[W×q’—Ÿ="²Ö2ˆðØÛÍíÙË—U3•ÌÝz]yï]ƒ¨)…Ë«‹««KkŒ‚Ä”ˆö.aïý0 O?þôÑ£ƒù3&…˜%ã¸ëzW×Öšœ³Š”Ò´”ӣϾ¬§Ço¾ýNd~až‚¾ßß?ý‹þÏÜ;ùëõªªÙî¢!7içÆà¬·9ç”"Ž‘C°•oâTEt_YÆ8"ÇœÛÉDT2sÉ4–ªëºªªrJ šR†¾´.Xï«ÉÌXëÁ²§t0¿÷OþÉ?ýÍ?þ»q»&cœ÷ ŽÊ’RTÕ~³‡Ý˧_{ëêüêÓ]?|û»ïÏÛÙó¯Î¦m]UNr1ÞŒ}UùÖÁÓ/uۛʒ#30§bΛ՚9‰pUÕM=![ †7ë¤dÈxg8EÄ”#çÈ9åÚ”€ ª²°’gÁ¡gàœ#gã +Nl]HLˆ wð€@…=PŽÉe«SQ°Pd Îæ÷-1¥Ë)sN11³³Ž9§1ë&Ó‰1&§$œ¼÷˜YJ Ë]œçAK?xq‹€ µɨXk¼÷¢¢Š„¶Ì{K|ϰHŒÑ{ïîçÀ²ŸXbsJ2(¦œËÉ=¦Ôu}J‘b´¸C!ç¼÷þáÇ·Ëårµ$”òÍÍͯ?úÕógÏ}þiÙ„Ê›¾¸õ Z ŒFÇq¸¸¸CxeKwÖÍgkÍååuJ5CPŠtKH¸Ð:SJ)¥ÒßB¼¾ºF2Öù$šbÆAA†q0Ö¨ˆ˜ª2‹‰œ¹wïþ⦆˜cH»1†±«kwpppsv~þâÒ(¢JåÜövÍ’ëÆ,f1l®ÏÚ†ÍÍ¡ÖuýÃöO¿ÿ‡ð·ÿ×ß|ùøi?„Éü ªÛ¦vÓik ´W—××—W«í¶L¾öõ¯ÒNŠq7 ówÿ ÁÅùùv·d_9­ëºì¶»í.Åázy“sŠC¸X>GNu]ºƒ1Õ8¦‹Ë«7ßùÚÕír{ÓÖ ¢JÎg­äcšÎfùôäÄÕW@ 爷{™» QÊIU]U!€qV±îææšßyëÍùlþÕã'—çç³ùÌXwvöbÜm¼ußþæ·÷ñËŸ?úí/¥ÊqÎáùóÇ×—/ú¡Ë)‰pÛL1„T9?›LsŽ¥l9ó¾Ê&çŒÅ¸«¬**‰Š¡Ó[w{sqs{eÈŽ!)‹!K†bʉy±h³1ô’yFk¼µ)¥›«3ïꔣHBŽ'Gã`¯Ïž¦q8¾w6i½¯ˆP*‡ˆ y~pPy¿Ù¬»ÝFT0¦ûmå|eMÊ#F@¤ XÆV!i¹J–¾øùlSŠ1XrÎY)ƒ“ÉÔWµUQTXÓ8ŽÖçl[ùõvûùç_ž¾vxpàœ{ñüìÉ“'“ÓÕê¶´én»JtÞ7d¼A;»„–9ûÊ"ZS®ç‚HÆ"á®ß5ͤ´ÓÙTTË/6†b”ÊŒÇ0’A,ýª1¥ ‰ê¦!Sƒ¬7›Ùj²ÞlJúQUÐU•÷Uí} qè{• ˆ1„˜ûO>­Ú©­š>üÉf³C×x“bTU– M]ýx³!k p»]§”DXT¬±„˜ccG¾rª<ôCe­pâc)HJ¡Œ& ¢r&Õœ’ä­)}J”3‡~ë½­½Kœ÷mÂ!%Ú5ÎZDbD²Ö‘-ý|¬ZÚ[)„XbDÃ8䜛º¶ÎUuµ_US,"Œs®ª*,]ØÙmw[–,œe¶0%ôCÁçî¡ó¯z_‹þ^𽋓EµT;q† ©øRÊó¼[ߙ˭ÙK>€åeSàN‘Êå=‹HÌšvSʪ`­Qc©$¹0§$"/^¾`VUÈ9æ”ǾùòLUç³ LUyùªcR@c,€æ¼gãLÚ6åÜu]SÕG‡‡óÅlµZ¿2a4ÖXk뺩ëz·Û…q,CæhK9‹ÈÑÑѽÓÓ®ÛÞ\]§”Ä;ç|©µ"Ä}> 3ï‡oÖ˜é|cºº¹Ž)Þ»·xãõû?}ùt³ºDÎ¥,LoKÓbB4´8:š¶Í®ÛöC°8üõG¿}ôñ£/?e…v:¹¾¾²ÎVÞÇÔk溮ûqØv»,ùé³§g/Ÿ«h]ya ã¨Ï´ß´\wËàdÚ¶“¦)ïaØ’3¾ªÖëÕ|2i«öËÒ~ý[ï=xøàáë¯}ÿ‡ò¿ÿ¯ÿêææ¢r¦}V«[Ba,†Û¦i^{ðà«OÆŒ1Þ»¶nT$Äì|É:$ÐøÇ*HÖÎgsQÙv}ŠéÃoJs@·Ûu»¦½³ÖºåêvÃdêe ¢©ª=Œyú4ŽÆ¬ë¶ªjBóÃï_¼|v}s[²tÎ)à¾ç2¥X¼³9Ç"‹úª*ï÷,œ9çÂòzóûGG³Óû÷ú!¾ñæo½ùöó§—ÿðã¿!¢Dâ””Y8‘p0* ˆ³ùÁÉñQJÀÔµ­*K Òu»«««ÅÑqß»Ìíò¦©+U´ÆØº€Ú9ª%l†õ¸Z[c³d T-=TR$ÌëõJDœ5Æø»,^4ÎO&326‹EG®ªlŠcc³˜Ô•Ï—/_QìâŸÿùü½ü°O<„þÅ‹çWËÛíjE¤Â¹w£3;DôýDížÂ…¥Cªè|Í9”ìhÎc¬êÚWUÓ6ªš•µ9sß÷ã†a̹[­ÖÂE*@Ðý|¯ì¥ˆ£4€2 }ÝÔ#GB²ÆdaRI" jœ#ô€H6ë8æaÜ ã–—R9?q¤I““èl6%€ 4Ç!Ž‘ ´¨CYYMe³’dèvLyf`@­5äo©m|މ…ó0 ‘"Ž}¯¥±Úz"bN1FçœÝ·Œ’1&£¦Œ¥b'CÖºbÛ4EYöežœ£ˆÙ·Þ :Å•H„UU圻¾+]ÓÅLYÕÞidžLÑ.ôn^*¥–ÿnÈWË««Š*Ú³\˜i¯†—£ý+¬cÉÓ/MÑ›J­ î‰ôøû[‡¥S»ÔU#P1«ªµTv "J9‡X³°1®üáX©ª*c óª\¿´‡‘w¾®ü|>߯û eR:?¿üùϾ|òå¿ý·AUس%ÓNçò§ÿ¡’æ¡r¶öUUyK` $Sæ”D8Å1HÐC7ìB03sŒ®už8åœÇ. ÛÍrÕÖ³ÊתÒ÷ƒ!šMZ´û_íjg+o¼[k8Å]·Šã œ»]g­=€£ª>É!t}_bÿÈixyõ5$5vbŽ»Nv›ÍÉñk³Ù,…¬7Ëõj9?~ðþ·ÿàêüòòù— ~½îXM;mrJµÁ÷ßÿ¡›ZDBR !ré"áÒJ£"Æ«<" ³1–È„†!ˆH·ëSŠdLíMÇX†1€šBìw[5uUÍJeŽ÷¶©+TCÖørPk)g.lAQnêÉÄ6¾ªÌv×w}—S"ekRL›m°8˜ÎDÕŠNȨrNArŠã˜BÕq…ÐT.Ñ Å8ÆœÊhTr®š¦nZgmˆ!ÄB°Ö6MݶP\­V,RUµó•(1“1Ö(ëºÄ!Œ…¥E ЍˆPž¡(¢ (YãªÊ¨Dc,s..ÉÝa2dŠÑZGˆ!„ò Tf©Åœ¸oª® ä$ísˆT¶”’œ*’²—Q{ig-=Þyl^Ùð_™ñ÷™D€W²þ+~YIº2KÁ4¦”˜D3(Ù´(VDôU¥*Qµ®ë’ÞW3 pD0„\ =PÐ=ˆÖY?%.Ãç²K)è8Œ{°Z±lYG„)çâ)º{-˜3‹è~ž\SÊÌAc 1„àf‰#¡6u•¦BdÅL–ÈÂè+×6“˜RÎryöòðè˜ÊãçU%Ƹ^¯¬µ†PTR¸Kß4¾&Cm忇*¦ëû”c±•c¯1–E¼¯ QÇ1Äã ò›~%’&í0ìXÀUµ1& —鎵Vb¼ZÞKº¼¹êúÞY{zt:m§ P»z2ië¦qηmóóŸþüÑ'?ûŸþ‡³Ë«ó'OŸÍÛ†$LtuyIh8ãÇ¿þh ãÌ8ö̹öͤÞ.—AGPÇ¡LGPYKŽp dœ±^3%*Þë pcN¢Hû@÷ïõ !’CD! ¶®cˆý8kãÐþéÇ "9£(.NNÜ¿w}}}yyÑ6-§9jÎÑbLéW~8„xx|â\…výð¯ÿêß…±¯›VØíÝ7sŠg/Ÿ!Ìfg/¿ÊqðÎ#!¡2çÛ«[\W¦rP‡0î6Ûõzc4–RŠãØÇDÔXë½o*¿¼½Ømw»Ý6'°óÙÁtÖV¾ZÝÞô}÷ú[o¤´Ã‡Ã““P&ÓÆõ]ŠaLa´„± ½"÷a—™}匱Æ!!™¶J­o–Þùf2d¢mš&åt½¼ngíBEÀ{'>øà‡·ÏŸãölZ$½ÿþzµì»í7¾þíXµ6ç\š‚!+¢7–°X"TUÝÔ5"Æ>ÆÄ•‡Ò[H !s.ào(8ÉqЬÕåª]¬»n2¯¼³¨”ceÓ÷>„‘E-U ãÃhÈ‹ÓTMíêÅÁIík?öýíå9§!ôƒ3Ù×~>?èû¢zïœw¢Üw»0ö¡ïÇm7 Æ”b –À …8*¢qt]Qæ4ô]'šrRPkÉ9cŒI)cêÆWUÕN¦ÖÖ,ૺ4ç•\Ç^X'@QRùÊy¿Ö‹lí+Æ>ç$œUy?q°Î…Šºúª–¡\Ü+o9k™™íÉ3%CoJ†"ø<‹4²dÌ’UöP,^šýÓsöàœý÷IÚý÷ãmVqåý·Þ1)÷­OåØ^¶…âÍ/_¾²è]–¬!Î\ö€W;J‰}+IŒ¹˜©‹…¿àb¨_r*/.eä"’³–糿s¼Š“‚îwú yc½÷±Š¦ï…]e‰ "WÎÛºòUUz­¼÷,YTÇv»Ý0Œ]×ÇÇqP‘¶n󦩽f‹¨M]9kËáw cÊT˜3åú]bêºsvÖ BÁ'P@’”X4fÂÅâÀ9« ï÷ƒ8ëÕ²$àÌÞ÷y˜Ç1çä¼CUPàœ)¥lȤ”V«%mÖÖ¹íf[Õîúúü£_<±†&0ƒË™¤­§÷ïT•'kr›¶9<<ÜvkW؉·Ý(ºw—¦”D#RRÀ!ï&ÓÙÁ|ru};™½ýî;ËåíõÕEŽBZºŸ@E€„•ÈœÞ8;8Ú®ow›ULQP9«ˆHÂ8îvÁ#fÞÞŽuU«È†Ãù›ß¿9{vïxn½¿¾~â ’îX~6kŽŽæ»®?¹÷š«ÚGŸ|Z×Õf³F#Š >xðм¼8G¤Ü¯¯f“v~0C\.WC·KÃöæê<¦®v®n›”Óz½*ÎãbSfæ‚@ÔóõÕÕj¹Ü»ÀNgUå†a×ï¶Êi·¾}ùŒçGÇm]§~ΫåÕК’2«ä†¦®KÍÒb>ËÂ×/ON¼krÎ9±1„•+f:C iÜ1sÝ>|ýµ®Ö:T¶;P KæëoóÉ÷uBï}ëkD²^oÞ~óíçÏÿèÇ?²G§V8s–Ê D™UU¼÷dŒµvFë,C,Æ’‚)õs™ÁYGÆäÌåpgÕ…³ˆÃ©ŒI$0‡±ï•³CUqLc7ª0‰Æ˜²H]×ÌÜw;á˜ï/^{ãÍã{÷7«[Và~'M‡îµ7^)-—7ÆYDIZæB’œÏfó¹r‡¾ïºÒóFSŽY8çˆHídæuÎ9ëJœgá»L& ë\ã]™UÑ+,¬…]¥ÏSU2gÈlïd!À=p¦ URˆTUxwX~õZ´…Bo|…Xye,ºF9«ñ«,©¯¡Ä|°$§ôw‚rhWëHµÜÀ÷zwå+¾+âAD¦»Þ†½süNiyõ0Æ ûï>N%|W*kLùñòSÞ{¾k+D@Õý~SUÕ 4稊€(9“¡½jŽøj¨PvCN$3gWù²»ìG¾ÆØòCûqÁÖQ™ý µ¶m[BÇ!3[K)ëÈAD@%ApuƒYDU¬±)f1ª !„¶™4m}örUïlSW ¹m*CTyWšëºv¾tˆ+å˜ú¾ïv½1Ö¢A²Ö`.òí¹¡ÆÑâøèí·Þ2„è}*¾ªiÿªª êºVÕSÓ6Öμñ2ý8„}e˜2¢ôÃ.†q»Û"Òï0ë»¶©EA-¹ªiONNW«õn»uÎ̦ÓibAƒ¬yX¯×ëÕZ÷r—¨j×uÝЋª±–¬Àãƒ×Þ‡.Ç-ëÝòbus&*°¿c‚”£ kæ\‘;˜Oó¤íÖ··—Ïw]‡9ç0åžHÖ"Œa ›Ûå¤n¬5Öýx}ùòË/.¯ŽïŸf•³Ë‹Ìb1îzˆmÛrff !(Âü^Ÿ_±ÀöÓœò<ösÞý¬;¦TÖhAÚo*pGÞ“· )ìWf. ¬ˆzïìÝ8· .5Æèœ•Š*>œRö½'à©–›Êl:K{w–Œ1˜™ IE¥¬ÇÌšbR«Þ›R?[^#¡*0€"眯*jt}ÿ (V‚²œ£÷¾ ›Ë HDl ô³ª’ɘÅÁD"çÉd2›¶}¿Ûm·€h+?™L|uzÿÞÁlÎIÃ8¤0n^>¿&,³ACXü$jÈyg*o­wBôÞ kaç–{í>ˆ¨ dõZgëªNSŽYTW«¥lû¡TÛ-®®/–Ëmˆ!Çl¬=Zœ,7k”D¨€P6¼}Óo ˆìcxúôqßwÖ³x_Õu#‚÷ï½vzzò—ÿ˜bÜn·,âœ[÷·Ý°;;v}s‘sŒ19ëTöv©œÓ]5°²Ä1¥Ô÷»ÏŸØrþø·"bˆQÐeQëœ%‘Ì ,çWWJÖà”#ˆ°ŠXk½÷Ö×ë˳Ízå¬ÝŒ›ªª½÷’9ŒñòâyÎ|³<÷•ï†AU‡Í-‹¨0‘!c¦³™1æöúê`qúµ·¿~¸Xô«óÕúF³8çyÜä¾Rá.š~1ŒqT9¤´í†³§_8gbSØî |ôöG«˜Ç!”;¥õUyç$Uaa@ÀŠ$ô[2”øëº~·Q›ë—Ç'ÇïýÁ÷¾øüÓå‹§eÉn-x‹:Œã£³$o6kúþöjÜØq ŒÞ í¶Ûn»UÄʹºjÛé$ÝõËç‹éÁt6¯*oCÊÊ9slÚfµÎ/Ïοs|züòì¨êz~°°ª €¨,%*„b Õu¥w÷ë”ÒSHi~0oÚºÛíBïÝ;išúêê&evÖXkÞ-U¥hÒNf‹{Ó¶ñÖŒƒäcˆµ·  jÞ|ã¯?|ó«'Ÿ-oÏûaÍ_ÅЄ±Ocw}qÖmWŸ~üÑíå%iç,;ã‰LÉã§Èdìv·ÉÅ÷Œ€„ ’‹’¢,šÙ3Ï­³Ì<޽f)þ¹ƒ¨¨C Ñ^Ð@4Du†Ñ8WY¢n·#s×cg-nqn•ÊÕ’"sÎ)HŽ9Í9å$Ƴ‹QP×U‘ª*Ð ´xö‚±†ŒC0wfž}wÇÝq±\Ü•ŠcH„cé}G,U¥ÅÃ Š¾ò€SD@EK¦˜Ç­µŠª dJl‹$z×+R¤dc¨²­3\ÌKº7çJxÝxCÔ÷½ gUA21¦Ò­Š†ˆYDr.ñ-Ux…ß)szÍBdÈbÙ9ȹ‚לYD*ï Ù†ÅÄ™ù®50‡02çÊ{"šÎgƘó¤i A€_ýAÎièw1ƺm‹…3.¥ÆPT//Ï@@ǰµ  b ™»qÞ§0sì"(¤”­µµw1%)ÿ}* Š d­s¾ªÛv~`\µÙn»Ýn:ƒ€ª·Ëe±n圄٠„±_¯—ý°Ë9$g«÷¿ýÝ—éìùÓ3$!ÄbŸ5Ö"€*ûÊ#Ω ¤]ÕcVÊl{ùòùçŸ*ÂHŠ·Ëe;Ÿ]_^ž=ýâ‹ÏRio–r_DE0Æ’³"ê¬C€œ¢!lfÓªªbJ­€L&Ó¦iŠ7!†0ö}iÝB"$LiLahl¥ÂEsrÎ{çË8Ÿ´ÖÚBÎ)¦@H%—PØ3CŸ !ª5(dÀ’"3ËRz’Ç/rÚnV8m'o½þÚírùìɧãÐ+‚÷¾­šù|žc\¯VcSŠDèªJ²HÊAUrÊ ŠhŒ±Æ5û¡猘,ª¬€Æªê“n·›ÍÚ `Cæ ÝzsùâéÏ~ü·ÏŸ=ÍÝ6K¦ÊsN]1„˜rÚ\Ëê‚EØ3”X(€³.!þT½Ù¯eɕ޷†ˆØÃî”CefUV«ŠT“M6›ìVF0l äÑ6 Ãò£`Àz°ÝÚjÛÜÉÉfkʬœîxƽw k-?Ä9·éD>d"7ož³Oľï÷-©ä\‰Ltdd/iŠÃ°Cœâ$›Õw¯¾;{ø$t­ø†™é½§ï½»¼Í]ßå<¦q¦äâ43F¦® Dä‰f]à qQqÎU…5"xïJ)9åR$¥øðáƒgÏžNñ?ßÞ® øà}‰9Å”êùNDLnš¦˜“òøƒüù×WßY­ aÖö]Ó={òþûOÿçßüÇÕæ¶ò¹ ©†–®o®þêýWFxuuõàìüéûOß¾þbµÛ1¨rh{ç$żۭ ‚¤”rÉhÀ„µx6QǼ/Ä¡mSÇÝVJÓiØç”CÓÔº8´Ø""çÈ9UÝnöûaœÍOæóEå©!(bµà""äœrIÁ7Ì<Å Ú\ðÊŽ­¸Æq¦ˆ˜*~]ê´ŽŒÕjÔ-b[aÈ5«:„å|^´Tÿ’heŽ“ª˜”{p%سiÕr˜ã@ 5e&çÈ7Ëåé'ßûÔ;þí翹¹¼‚ºwÖb fX½²¢ê«@""ô3Ucs,Þ+>ð€? K¨™¢Ú÷½¨æ’£nÖ圙œ‰"‰˜X=…!a†:è;ÞUõeÁ?<&J)ã8Ö~ETرa"ÍR\p Z¤8ç™]Ó„ùba¦52EÍÍç‹÷Þ{Ï7Íï¿ürŠq¿ÛùWwm.y·ß«êl6ë»~·ÙÜÞÞ9ç‡ýXu“ì˜<¨ˆ":ïª}¡£¦s‘jõňՂCG ñÁ\3$uÜÉ‹jʹë›ó³³a»­TF®-Îë—ïr‰Î“ßb&ýÝç¿{÷öÝn¿A4DöLÞ“Gi—’s‚C4ŠcUišFr.YRœ’MT·*"Ì0¥XŠä"høøâÑøá§»õjšFvɃÀ5@ØÐ€Š¶éÎ/.ú~Öu=;Æ!Ƭj¸Xžœž–,Ãn;î6ã8bYœCÆõêúöúT•œsJqœD%x¼—Rˆˆ¬bIåPâ0£ÕF©ÖÅ?ïjÚ_}ìr%îÞ¾Úä`¹X6MØ®W»íÚT˜ÑÀL¤®©RÉìh溘¸”òàâÑû|œE†aŸbdâóósçBÉå¨:UÉ·w7ã¸?„¨ ŽÃ.I!ÄšaY-Dšó«_í·ÛYß羽fÆ¡mÀø ÉZ5¾ÀTûƒ¦ij„Ž™å{.b»ÝnŠÙ~þgþÕïÞÃúå¿O¹¶@ ÌUœçÙrv~~&7WW–Írõ³5¡ï;5‰ib&¼ù&t]O´©‰h÷Ud-¾Ì,ú>¥(Rp·ÛNÓ¾o}Ûtmëon¾{ýÝWmÛ:5 ·3)9§œÆRJJ¹v³Óù|sZí¶9G‡ªÉùè£Oº¶{÷öÕ›·/ã4Tf€f3r>4ˆ ’R¦i*)Ó.µlCIÙDÇq\m6¸C" M Hňû"&ª]W_îÒ¶MŒcU§8GÞ»¦ ˆ Zr3è»yÛÎL%§$GÊ.õ}_«¼j®9^u>Ó4Á¹¡§"¥ª`jSÓ„Ùl潕˜Ruó×l£ªX¯'f>¼N**ªÿðFlP"MgçôG?üþgÿhµÙ€£¿·_•qP‚ÆyBõŽI)hŒ‘‰…Ýqs¼¯Û¿8M€Ø¯ªEMBeÒ{S$lÚ¶iš|J¹î TTDªoö=޹éBíIîíN‡€Mľï«@¾SÇiªó¨œ3Ó!n"åÉTÙqkBÓ¶Mß÷mÓÜÜÞæœ÷¾iæ§Ë©ä!å“““&ø»»ÛÝ0XÛ6ÆÎûa¾€8M“ÆWåuÉBÛų%f8L# t]WéZ"¥ˆ”¢5ˆÌÁ‡ÐÏ–Ã8æ’;5ÈbZ’‰iʲÝíâ8‰˜Í&ŽéôÁRµìö»”ÍÌDã_e¦%G1Ïì1˜,Z±\ÞÜ­ÇqÜn·Ì,¹8v`5;EU“š u]??Y’ã®ë\>|þ½ÕÝÍ¿ÿ¼”´æ>âqÍcˆ`*q,EÅqÈYK±aˆErÎ ‘TM ﮯ÷ë@¹ª©Ðûi†8ŒûMŠaµ«‰ÞïuRJã8Þo’ìX¯¢Éa Uõ¿¦*ªÈJÒÊÁá†*FÏ\½}ËÍÍå•jQ!¢¶kç ûý4MÌ!8UÙm··7W)çív“svìvë;"®ñÌ\Çkˆ¸ÛoÇqH9J)MÓïK.¹ä*Ê ãâM¤ÜÝÝ8bÇÜwíý`“À…P}3÷ƒ %—±T‹¥¡ŸÏwã ªfP¡"Èãövw÷6x§¹|öÙgŸ|ÿÈÜ8Üy7;ÿî›/þæoþÏ’fÍE ¹@íÜ¢ÁIãúηͬXÙ®7¸èmÛ:ç+{«”b D4Åxuu%Rª_±h•zÕág4S# 6NÛ˜â›×/r‰1 !Ž ºÞ¬·³~ÞöB-EË0ì7ë¨Vfº¨pèAåêÝ›ÝveX|hƒ óÙ¼”2ìÇ7¯Þô³þnu'j¾éBðÎ1»P5@jH ¤¦5©ÚsëCrd£àÈ!±áf¿[¯×)§,¥ˆ ˆª–‘]ÍÙéEß-êšÓ‡P¯+ÄC®)!( 9G¹3˜õ½ŠXeyë»êöBDï<3Ç«f¦že!x(eœ¦‰ˆLMJiÛ :dïu³ î…ÞÞ40õ’8¬" «†é x?ü«#âœ[Ì;ïèÕwߎ±Ì‹ «›““E¾f+æ"¢Ö4ç Ë%«jʉ‰»¦0b@®–]vn÷©t Šªâ½WQð‡Ü%Åš–3T8R©ÁJ*ZJ99¿³œbšRN¹~þU…‘º¾›Íûú¿6=¸‚«,¥‚H,Fˆ.k·[?ì÷û½Šd)'§'¸à‹ùo~ý›ízs~q>6mÓvÁûÉ“bŒCJå€I`gˆUê‹H1–¦ñÜôæ|TKžR†ƒÛŽÂ$惵ʱó úžÚå¼=1²s|àŠiÒ" ¹Øryê‡QEK‘»í ÝlY—̈f"j Lš™¥h) ãÆ^œ¿÷äý\ìvuWquªšD@9Í’'ÂÐ4MצÛâ[ßÍû‡]__½~3H‘Jx?ʺÐ;Uep°Ûmwû]=֩Ϋ­L\åT¦šsª*^2åArb@«®%pt„Ÿ×#ì¸úCCݹD¯–art£¥œK.}ß1"EbΣª5fv.Ôé4f5Ë^  Uóíí»ë«wHU/D€Gy×ÿï{8þˆ(O"‰ vÒrÈä®É3R+pQ&êÚ¶Ž1ª.ã7”VšgRÓÂÜö}§Ã8圧jâCbv2l†ÕÕbq6&æÀ7 `fqJ¾9ùÑŸüÅ׿þ7 ëaØ@?[žœ?ó‹s‡u¥ FhMpäAÓ4uŽOg³–]µ}Ø~˜êm©ª9—¦mÔàúfµÝíêR¢©Mº”ÃÔÁvþPðÝÜ †ýôêõ›aœÐBe¨Áv·SéKÇ!¥L.‚kE·Í|hëÞÔª¬ñ¦àœs®ñ¾1]Η}Û­.Bα󩨩6í 8äÃ~|tñ8RI{ÒœÇq7Œã0¨Y×ϡɥ 㨢ʊÌmÛ-—ç}šâTJ–’UŰä,RrÉìªÃˆ ¹ ¨ÕÔ7¡:j‹)Ç”ÁOÌrìuTašbJ鈇¬ËIjÛYe‰  šò¯¼0ÀÌÓ4•R|У⛜cï;ôÎiÑ”S•šQ ]3„TÊíjÝÏ–ÌžÌÀ¬zö˜‘]H¹¤,G~´mç}†94Þùº;Aï|ƒÔìÀZŠ chšVU%gS#æ>x$Ò#”؇¦ ä\JÉ1%öÎ{_Ù;©Ž‘Ð;¥ä”¤Ý IDATÝnw´G‘OíaUTECÛûv>Ÿ-ç¯^¾ô¹HšvÃ>8ç½O1½}ûÆ9~‡¶Û¬TÊÝí 9nBã<ÓÉÙ 3O)¦)VƲ÷5H ²˜¥rr²<¹x2 # Õ‰0@L‰›Ð4¾©s3švvÂa9dlº®oΪü€Œjn»wÞ3ÓN´0ØÃÓ‹‹óó/^€â~¢äœ~¿Ùˆäà]íÔÔTDK.%P×o†¼¹»îX_¼zÓ÷ýf»eb5ÕÐtRrι”,5]]AЍéÅÅb·gg¥_Ì©þìôôу77Wû<˜êá-@€#X©‚ ‰Ð´dUDV5&΄¦Äõé%0APBË’²D‚B|8šfU&<ì½èh¨»\=˜3ª˜ k®A=tˆ¡Hº—öAάeWí¾è€« LÌމNbæ¼Ï¹ä’E‹s.@`ö»í® à˜ŽYC¡ñç[¡¤õþ®áP HŽQE¥ú‹ª©ªÔ"Q)û}×4³¾g®PB4«÷¡®¸ÙC¥b€`¡ O›õv³ZW‘‚˜¦ín»ŒòÞóOô³ÜΗ’˰‹]7;??ÿÍ/?7Ó.4’£¨õíìƒg¼¹˜šwƒL<ï;ß4óY†5Š!ÅI}ˆ94¡ëgûýðîòjØí½óõª1i©B`DÅ%°bæšðð½OþëúÏÿÕ¿ø¶myÜíRI1¥a˜\²©h‘B"ÖvÝò|F®‹±ø6L1§”\ÞRëZn³¢4Œ{0cgXsŠiT£¢ZLÔ 2PC„èfž}òôýïÝ]­.–~ôÇßÿüó_¼üú»»q?Åë×ïÆý^M94æš¾_¶>ð>¦˜LÅ Ú¶›-Î]hÆqŸK®2ÉZG×{]D ¡nMÓÆƒopŠQrͲà\èp¬—RIÿ þ#U±„à™Áoµ–Æ1Æ©k;fªì—RHÌTµä\½|mÛaÞG`"5sÌÁùÚEå,÷í”Hiû½ kZTÉW×Wd¹iü|Þ‰ÊjµÆ„ˆ`hEkјc…1KŠùÞ£A¼ÏUŸ“JªQÄÄ\Ëp&J1›;'R|hŠjÎK¬ÚDÑRDÅ*½§BIË! +;Sͦ˜  *b¬ÚdU],Ož¿ÿAÚë››,Ú™¿8=q!<<9ýâ7wõö÷á°HÏ@›&tM3ä"ŒKJY‹ù“'O<|舿üü‹à}íÕŠäº1&"°¦íž}øñW_ü}ë*÷‘Ðæ]pŽ E¥) æ¡ïg¡éÌ7©2gI­õ,3³’€ø HÐäj•oÖï4‘G ô 3™yò3Ôô 4”˜<ûqÅ7Æ.ž|ðÁGŸXÚýû÷¯ÓxýúÍ›Ývµ˜/Q‹´m#…cŒf@ÄÄ®Š$¦7¯__]^~ôéÇ]¦ýj'âC¸8xz~=N¯ Tjæ¯hʹžJMÓ# ¦,L=þc1—qŠ›ý®éûÐ/gœÎ7ë"î÷ƒo'CWûtƾåÐbè(œ>{ÿ‰™¾zýºé¬o:œ·¡ñägó‚žs‰‘svR†Í­Q3¥ÈóvÖuhfmÛ…¦áÐ,=ûàû?þìG‹›·WJÍ~?}÷í‹/·ëM'Mp\kMgíÉüôQ³P"j‚ŸÆ!çÂ}w·¹M*†˜ë¶途¬ŸU`$tìë”æð ÖArü0»"E’Ô;BÕê³R±½µ>½—ŸÓõ,„P9Ìu÷(Gí·sÁö»óÁi\=>Õ…ÀD¥HŒ©öUÕ’ÍØÆ"CÌË“Óo¿üâêí›Ó“Y^ÌÕpµY¿yû:%iš–È¡Ún·© ×°z›ëÎMkQS·Tù‚9×oŸsΕ"ÈÌ5´Š­M.yïŽb€Y×=|ø`ß¶)ÄÑuzÛ©Ò¦Z¸5MCˆ›ÍÆ»±â”¯®/wS^žŸ<}òÞzu»[oØ€)}¢†Ž]±‚€TCXÔL$ŽÓÝímÓ´)Æàƒ#FÄbÐ4­÷ízµ‘\LÁß÷ÝÏ?xóõ—7oÞ&$«\!‘2ÅIÔŒ:ïºÓÇÏ?[Gè—Ÿ=ÿ˜€ÄJ; Îj"›ëí·Ÿ—¦”r$Ц%'ƒÈ¤.žÄÇXÐûî=~´ºÛ€â{ç¾ýúÅ +â˜KtìÑóçïß¼{ëCçha:.q¡ju0McÉèû‡¨B!´¡yxÒíV×\œ†”ͧï=yß9Wâxww3 CQQÉÕ(€)M³ùœÑ4Qñ>Ôçë«ä Ì¢¦*`6Å©Öu¨.¢}ŽU.Öó'çljXzªZ¡ªÌÈDE˜ëÞ÷~PJ)EÍ,ÆØo¦h`f%ÙÑcçj°ZÊ¥i[³ÌD9ÅÝnSä8z#jšF¡ðÕ†ÂÌuÉ™RT­4DQs΃aQ50B63d"`Q$0`b§¡ žBë›60V‡U@ I)Ró#‹¦\î®^ݬï?zϱ Þ§)Spì^¼÷OþÉNŸmJb†`Ïß\¾¹¾üÒáCRÕÛ›+0l|{uýŽè0&fföÞum‹¦%NíÙ©­×ëiJD.ƼÝîÇq$v1¥¶ïÍl'BnBËäjAÊL!x$’šêÆÜõ}ÓõHŽÉ ã¸âpu}»˜÷]?kºžîÖû!æ¢jâÚ¦_,\˜uM{öágüÓŸüèööæ«—/Рm‚¦>;=c>ÐÉrùæ»—ß|ýõíên··¯¿‡D³ªˆTÂúÁuIĹ$=ÂXr.õ£UŠT¿RmÏcT¨³¾ºÆ©ô{1û¡L@vÎûqw»=;çÁ @™Ø!V-Z¥ÊÔHkçœcOˆ9—”Š) 1ƒÕ ™Øúævu·15)©ñDcÊYl³Ʊ”¢%ˆ„ŠÕT«fÅ ˆ ¥€Uš˜U*2#i.u솘b.E‰©"ËÓ “©äQBªƒËœ“.–ÁÑêî®–nªðºO;RnªÁ§”öûww7·Áß4Š ä>º˜Ï篾ýææÍ›ÀÄÞÕÁiÉÙ̈)Å›uªRQ1©ïš’ ` ­°œ!2c”)朥bP‘×ÿàßÖã"Š1Ö­/cÑÌêÚ²R4C«–ºZá‰:fb$O,c™L!¥‰¹@¢XTsLf–jÌ«¡Ä¬Dܶ]p«ßع ’ú~Ö¶ÍÅùéË—_ÑíP‚¿Ûoûy‡Ý·Ÿÿò*O9b‚¢bŠäi±<+®uMÓ9ȹ¶ëû¾ó!¨IÉÅÌNËë«ëqUa¿KWØÌì܃f³—qJìˆ[Q¬‘Œ†æ¼C&pÜÏú~y2ëO_<ØOÃç¿ûív·qV††aPCçüÃóÞûÓDÚ¾/ÀülqòÁ=yÿýý4>xøð{Ÿ~2_.(¦ ‚];w®á|ëˆ,Ť9¥ÝöruûÍw¯^¿½¾n¡]4}˱oç Uè»Þ@‘à³ïoÿ¨Oãj6h¶˜/ÏÏö‹Ófv&ä?ý`¾8»8;ïûÓ$%?ùð£ÙÙ…ªæýíÕ¿û«ÿíë/?GÓrH™@5hœÏ©¨•œs©ÄgÇJTD”’*ëû¾5Ìûÿþõ6R»<¹ÙÜ]œ®n¯¦Ý”‘À ô{}eIqpžšÐh‰##9ôÁƒ‘’R EÔЃ„LÌ&uþ€MfÆÎ圦˜ˆ¹† £õŽ!«fÆÞAÝÇR]Ï8ÇA Ua2ÖÈjP;l{”Y‹TÌcÕõ›ò¡ßDwE¬“ &®ã%ç*>^KIb ù¸½„£ž—»®Í¹¤”«T¿6þ÷ØaUE¤£ž «5)IG®&£„4ìªÇ5x$$$ª'»ƒ‚³išÔ­é!ºSõДÛýZé  b"ßµÞ@t¼CLÃîÛ»››Ë+ç\uêVÉšªyç(Æ(E|8N? «žÍ>öû/¿ø½±"2Íú®›_œ\<øáÿÙr¹0°ËËKÓ2ŸµLåáù|Þñ“Þû“?ûÙ4M×·«·—«¿ú·¿¸ÝÚ÷Ÿ>ýøÓÏBïCßO†1Ocj|ww·[ýâýýMÒÿéžýäÇÏz°S†OÌGEîçÁ,‹MŶclºÆ7Ýn·ßÞÞN㘅bQ]ÎBkÒõóOðÙÙÅéø›ûÛ_]Õgñõ›·qÛæ ¶ª îÐ4ÞûišD¤ä `)§ÕÕåls]íÖŽ™7×7w›µ!Ô‡Ù97›ÏCÛV³Jê?ëZ) jÐvMÛºz8wxÁ‘È»®ÇAE«Ã¹˜™v³~±\2Ó£ÇRLÃnWrÉ©T9 "†à¦R<;æƒ š¦q®ŽØ«Ú'D Dšµí{HΩÞiU« VUÂTÏkDòìïî®oÐ[úƒPQs®ÕXŒSRpÎ…àU­ˆT<^×yp)um[åÎÓ4©”ÅâôÃ÷ßoCóîíÛý0¤49$x|qöáÇX@‘ì×8ûZÖ9v"ê}«ª¹H×¼ywý·¿ø»"ÖöÅö]?ëf̘â>^­àÓ@ZÊ««ÏÙ1‚iI¦â}óãþÈ€>ýä uË÷ž]Ý®Bò2‚V )9°ãÍíÍz»Y­ÖMÓžœœ„Æ—"•ëíÇÀäg³ù‡ÏŸ_]]UÍ –â½÷Þ•š,A¦Ð8`9¦Ð´Ÿ|úý/~ùkÜrÝž¨cÙî¶›ýxþàüâÑÓŸüôg?ýÓŸþö7ŸóòíÙùÅòô JJfÄ®Ýï§qœÂ,ô½1š)Ù†-Iœ¶ÃnµY/ûö“?,%«BÉB–]ߺÖ/—½¿yûêݛׯß^æBóù p3%Í왈EAs¥{›IÎqÜm׫Õ8ŽÎW‘\² 1„¦c¤* JDI)MS¨²‘¼V¸Ç2’BÂGv\G• rÐÞï*Àɪã‡M” ‘ï#úî-”s6¶mÛ¶eæaчP¼¦ ]×ívÛ”&bôÎUвàž^[WDpî`Yr®^ ¥–<ÀBĵ®-§Çæ@b©{*"ªÚ|D !„ÊA¨Gz¤·mÛ„bª¢”RU´ ixðŽ‹(‚3úcþ—ª–)®óÍúæv»ÛU­È_œ‹gç[_‹wFôUµõè3õ¹@ý¬Š(£1‘cç|³8Y>zïÁ“ŸMcœb¼xü¨kƒi–4M9eKŠ„ºXÌO.N÷bÿäŸÿÓÝÂüìäô¬ëB?ëeLÊšf­Û·g§‹}žÎ_¤d7w8šž=`HF»IÞîå·ß _½¼&bBÓÝn5ì·9î É©1;·qgr:Ÿõm?ïf?ûÙŸ½}ùÅ›7$vU¶XŸ7çÜååÕ0 µx‘˜’÷‡×Ç_R0vÌH§Ëå~¿Ÿ¤(hIÂÌ}ß#“HÓ®kçËžÈ1;ïÕÌú¦2€áç«3xæç'w«S*‰/>|ïÉ{uqêˆp»ÙNã¤bëÍzu·š¦IA듟÷©F8çL•ˆ(„`̆pÈ:ζÛíRJMãÁ  MÛ4>°*ÃÑœsýðô}[Õ«jdjŠF 3y*xµèiPµ¯T +`dªÞûv±¨˜sÎ9^.ËÙ|Ö/†a_Šš¡C§–òÓç?ÿÉû“9˜å’àˆÔÊ™"ª<Ñ”¹ Í‹ÿø«_#¨å˜Tй°˜Íû®ÉH@Tóã°YÓTD±©alšù?þOÏž=bººYeò½ÿNEÑÕY8G9Én;ìw»4Å®ïC航 žBÓ—œó8ŽmÛ×÷lÞßÞq4çC(Ä0k€ˆRòn· ~½Ùlo¯ÞNÃÆò@Pm¿Á¤¼{{éBãÚn?¤ÅüìÃ>Ú®‡ùüô“O:$'qÇqrÜX‰yŠ9ަ 9©E%Œ1IÉ% y¿ŽÓn½º[ÿ Ñ³ó‡ŠŽ±HŠYQRšLÂt{û&¦!„ƹFÕƒ÷Ù:BOäçRëS°ã¹œR¬§‰š¨ "!±©‰BJ98Gä©Nj>­™Öb¢HÊ9×A9þ”ˆT­ªÈ+‚¦ªGJ98ON„^g&Ž]Ö\Š˜%8ºÉðØ Üÿ–™Ác¾zÈÕ©Ã߬ždb”RØUˆ¨gÇ„ ˆjÓ0ÚQ(vú‘µü©Ãv5K%›YÛ4Þ‡{ÌÚ¦‘œÊÀ¢5gÊ1Wð ¨J­•¤Ê\ÌšâE,4 œ«_I˜ØádVŘ"#£ZQ8F ZŠ‘1#ÕÜõ!Åsçœó®”T$3‘ª1v~·ÛOôÝlªaÂ*Ç’e7ìr¶TdÖ5ÌLžÔ‘ —‚ ª:n¶kâv¿ß:wÚ·x³ºº]Ýö­{tq*HM;ûáÏÎØø§&°Ø%{w‡»©¼½¼œömçÜn›¿»ºÛ$Xí3’%³l%jÉÁ@$RvÄdÁèñBÈÈAœµþ§?ûù«ÿå+Bv.N¦vß0sÎøáGÃ0Ä)ž><½»»â `õ9Ǩ ޱ w.jñÞ•"f‚d³yhÃÏÎONOODÔ‚¡÷>ç¬uÔH÷:P)¥kÙ{TÍR`ˆ=þüÑãGûýþÝÛww«»œ3#9â¶mg³¤‹–Ý kìb•EÖrñ^¿ÇCuó¾³®õÏž=n™‘[.D4Œ£0CGu®ìbNCÜ;_Ÿ¬Ê±*÷xmõꊖ«8¿  )Q­Üé8CwΧWëõ8 )%ÏUkÛðbÑ?}ü›PM]ˆõaÆ”SJű#¦¢bˆbn6›-–‹‹”bÌÞ3š@E¦"…©¤œono‡)þìç?{õêõ«W¯=;BÜn7—ãøúÍ[cšÏ–Êã7¿û®H¡BÎûã°ß›Ú°ªiDµ_¡z[Ž}¿^¯SŒªŠš%§Sê»î e«;eâ’¥ú‰@u»ÙÌgçD´ßoß½}1îWhÅWÐ`A!bçÙ»ù|q~vÑps{y“C?›-–1gIy6›‡Ð ûI5wPp)4àÆ”Ê0 ûq}wÝ`–¸—ý:g?mîRׇnf ’¦XJÊeöûÝêæòuI»Ö T3õ¡!‚Æsðì¡# ÇœM ˆƒgR Þ{ïDĤîëø(E ÕÓ½{3xO)GõþTT×G÷áV""EÚ¶IE|ð]×VÛ3TUm­ñ"¸×ÕTå_J9K©ô`øâ3·M—sQÕqš*âªÞ%"Ð\L±HfF¨éš®k—§'ww[5m›Žbšbœ|õ„uŽë窺eŽSšRbB¦ûõ]Ýüa¤…&¨ZÎêµx”óßùª7µ.–Ù9ï=ó!°øõ¥P•Ê«bº¢¥k[&Ôj¡Raf@tÎ9ïáx)GZeÂÔï¿ .E-ʼn½s¦jDj&ªã8ÄiÊ9°ªN1ZIÈhˆÔö'漌R­¡}Ó³y(*ÓåÍŠ­èpöôƒOúî›o®ôYßò AôÅ7ï¾øâ»_ /æóàÛd9gAUæB¦¦ÑrîÚ –]pÜV;³CrEÄLÆq»qÄùâü̑ג˜Èû*¹f'¢"i³Ù˜Z)²\ž¤”Šd"v>”¢L8cb€}?ÛÅ)¦LDSLß¾x¹X.Û6h‰>ÿ kÚíz†P ¢Õ: #£fâàé“GO?xóæÝ0æzR¿yýæêÝÕn·=(‹krÀn¿»»»[,u.W³}j©kuk `ÕÉP @@mè×x~øðÌ{žbÊ9]^_‹d6TEl p€‰.ÎNOoWcŽyJé>UÿÄ#³Ï0þàøÁqá_ÛÀcV;REÏH*rvqæ<§)ÃHÌÌ”K©T¬\ ;×4­¨7žçxfB¾1•¬Z)朆!MÓ°ÛâÃ7ëíÉÉiã}J)4™BÓÎK ͧ2\:âºå³c<ž> 6ŽcÉeØ’é}ðÞw}oÛí6NcнiY­Ö)çår %Ç¾Ö YÄL=!ÚdçvÛͯ~ù‹8Ŷqìò.ë.ATKI›ìw{qDŽÅ‘•ŒZr,ÓµHhZï\À.ÛÍÝÝ»wßÜnVED%“åqØMqêλ}?ß±8M%NCÜïÆ]§Ýz»¾½¶\Ì,昳̫;—½óìß04PÑ£ÿ“й¶YÎçÓ4ÖqA–"Vu"u;Zñ¦`–%ãž™#$(¹äƒiÈîïGh¢Ôz›&ÌçsUͩ䜫Àø ÀÉ)• cQ3@dç©ÔH%%"3=fˆVã¾?µÇdeR‚) .O–!ø×o_cè;ß6.4Îü8Œ6èªî…îÁ¿÷÷)Q5¦ªÉAíHT'u)¥”Q™5੺+©jÓqSŠ5ÇÇÔ¦8:vmÛ¡Š:ï*‘»ŠÄUE¨/Ý IDAT+®ó ö  ! ¦I`‰h%„àxæ“” ­m;cdœs¹ "f:ì÷1ÅÐxçüÅ{JN&R3OJ)ðwÎu]÷õ7_Õ;RÕº¾mÚ0û”¢w¬ày6ëÛÝnÈ©ˆˆ®7[Óæù³Çú“?I1­Wë)E}ðàá|>'BïÃlF:PåöËåIðîÏþÓWß½Û¿xœwΗ"õè>„àp§º½€Ý~wº\Ìó˜Òn·«Ù„L\Ý!„ÓÝf­¥,`JѾÿé÷?üðC6Å8NÃÝjÕwMßϪ³„L43°ó><|ðèÓO?ùúÛ—»1éAŒÕIÅLÕxXÛ…*l7°ªÉ‘ƒªVáh’HUdkdz^L§¸/û~¾ßïSN»ýþúêZ¤”\œs)&fWŠ3+5××5z¦Ÿð_ýå_þ_ÿæ_£0PQ"4´œsѽ™Í‚›†í°_+SÉã°ojÖâ0ÆiŠ*Ú–V7w›ÕݰßãvœÆ˜sIÉʘKšR¡±ùI™Æa½™#kŽ»ÍÍíê2Å¡$ÙoÇi·¦IUQ±˜hÓW½÷ÞUsùñl0$@S!d‘òÝËûí¾õXJ°Ž)ª†„HEøþð=„VRÓ?Èûjî¶1ó1Ù9`¢TÊnL5«S2tì¬Öþj&V* ¾”,2ÎfDLõÜ:4Ç LF9GÁÃ?ª‡1Ó·Sœ¶û]ª†#pE¸d©í¼«°—cþ~–s>àÏè j¬‚ÇúUë´7ÆX¯±£1¯Ü³‚k ž“ÔŠŽxµÃ´ÝÌØ˜A}5i3ÆHuôŸê%Q½Ôwm¡FñÀ~j²Ua'3›R¬FTUA9BUªÈôФŠÄx¿Û¨%Þ{$Ç.ø¦E®4…ê/` ö.´½4#0Õ+Ñ’'yÞ`ŽñÍëW«»»ùÉÙP4 )7MÇP¬Œ2îÕ¦}ÓTTJ‘qLÓmÜßùàÉ7ë[|¨³&jÚ†ˆiyr*R<ƒ'|wõN´,Þ‡½VŽÝ‹J$NWõ)¥LÎ9ïCιëºù|n*V2˜Õ«š˜BãÇé`æ!f*Yš^¼x Dxv~áÙu]ç\m’p1?QÕÝ~ŸIÌòjµ^Ìåt¹”\¬fl8ïœ'P›Ïú¦iœwûýR!˜i =G°“żªsUEî½gvÛÝn³]wMÓuíf»O•CgÚ¶a»]§8 ã0Œã0 ———Ïž>].—ˆT]&•>]J1ö•¶ UâPßææóYA½wDx@É`Ý»㈕óLЄ ª5£ñõÛKAB2°¢bjCÖüà\Ì9åšFD‹D4«ñ¬šE´jÒñìdùƒŸ__]f@v佟uýjÚH)¾ñ"y»]•,ËÅÉéÉ2–ògúSQùÝï¾hšÖ³ŸÏiJ%¦4ŽMh'E‡¹dÇäЕSJ¦¦N3@1P0»€PºY?ë«»»Ý´¿½»›¦œŸö£ª€LYD²"&çÀ3RÖ5NÓhj¦Bã½ÏS¬ »+¡%±Šæ¼ßn£–\2±ôèÑ8ÆÐ´³¦¹¹¹•2ì§ýõ»Ë2Œe˜Ê4YŠ2Řã÷Y…p6ŸõCIÃîn6b²Z]ï¶w’RŠ2ìÇõj=ì·Ì¾í熬RјÞ9®›ËZªbð HÎdÈ€c,%‰$!0FòärÉ„{š‰È!~]ðH„v¸àêÅz†Vßs­\*yî@ÎGï=ëðšà „Tr­8”"Õáq{¿·f‚!x¬æc÷!‚±ÒÐ6ë‚5M{rvþèñ{~ða§ÕÍJäp¾—’Uµ”|¹WçB5ä0$9ºmë׿W $@f5ŽõñZå š‰LsI¥hš&=Œ›ü½•é~ë`ÇlÕƒ+$ÄÃtÜ´æªBeŒ¨Êf³I¥ä,Hš®ífoM˰ۊ"¼OüPµáÔq=ùPM‰‰Ù“k»Åò„¼ŸR)†÷ÝŒ«ÕZ½%L„˜"g±išÐrÖq¸+¹¬Ëà錭#Q›7®k‚i± yŠ)o×·¦/j@—§|½¾]ß]žž,OÏ/RžRžT¬”’sZ,fýlžrq§'g„xuýf»ÛŸŸ ãD³ª¯5)1þL½ÙdW’Þifg»÷ú¹“,’µ5«JÝÑhŒ$H³bFz˜ÿth@ƒ´Hên©ªºÈÚÈâ’™Ì=p÷»œÅÌæá\’oLDDºû=Ç–ïû}X_4©!¨¢ÕFOÄUÜí½«Öùù°C ƒ¾õ¶«Õ:ç<Œ£ˆ¦’¯no_½»8ÛlÐ!³Æý€MÛÊ…‹Š4>óååu°n³ã-*J’¥¾×Ã4„ÆC]×„àœ 9'¢Jçfi›P¬Õ#3‹ç¡zC~ð^Û¶/_½~{y ˆ: ûo¾ù*ÆÞzB³Z¯»ÅÂX› [‹•$LG1•²ìw{æRI_ÞÐ\5¨eo¨],µ LH„Xï·zþº:ñ3d«€Ð0Ë8×7·]œuý02œó@d8›¡Ÿ| œË ¦5R΢êƒ÷Þ«Â0 ‡›utïÞY³<]Ÿ=ì‡H`þò/ž½»ºøOûoonº¶9Y­–]ç ‡Ã½÷×땈X²ŽL°æúò ´ÁCè¬!L©ô¥p.*R9À ­U2Ö0Îøå‚œK%™dOOýý÷§W9¥¬\J¿ï+oËΊ?àRtNpV!žú}šFÕ$ Îù®k)N©Y,VëUÌ iUÍ…ß¼}—…E84ípõ«õ:¦iöÆÂ®¿Mã¾ ƒLYKá4¦8ìúH´ ÎͱŸF˜Ò4ôûœ¦iL1ò¡ß†}?ˆ(rë}·´Îœ¬‰¨pá<¥iV]ëƒwÎ*‹”\J! e»²`Œ2’IH–ŒÁz£×M£* «V*"*a™‹Y¨çÕݱŽ8ƒòi>ô¹BTïö0KÊÆØj 2ƈ0$ï}Õ¶Ôµç|néLzì:çŸu4ÙÚ ¿R5ˆ `i‹÷Þûà/þü¿99Ù¼yõ²i^šÅ¡ Æ–”J)h¬Q–³ŒRjz)ªš;R±uy4wµ ªyþ" j)¥n½s%[iS\I ˆP8×øgfQcçè>.e'¢š5»ÉUµ°Ä)ÎFÓÐ=zôÁééùíöêÍ«ç»Û-Q a'ÀïÀQT'ýÖÖƒ‚ªEò>4í¢ŽÝ,oÝ$Rw~@Ö8kê^:³! cAï)×ýþ*.1‹vÕ¹ûwc.1é¤]¼U‹¹ñäíâöæÒŒ7M xñöòp} RbŸ“›JÓ‡¶mºŽY÷»Gè÷ƒØï÷¸º¹¹yÿƒ'?ùñŸ•?ÿòó\&˜1áRð8”«ƒ¬ÂŒ„sì—µ”bìûi™)xD gÚ®„{÷ÎYøÍ»w··û`œ?ÙœùІ¡ñ+¢Âl¬ˆLµµSUfõÞwm›¦Üµí‡šŸüðGÝj DλËë«¿ùå¯ö‡Ãqê(¥ÄZú¨ÌÎçÚæk© ‡Ã!—TmÅÁYTƱfÍ#B×uiÌ·Ûƒ±Z·\­+V¸f&DÁYÖˆ©”8 ¥Ä³ÍÉ?úøáý{ z~rfŒýô÷üüó/óVE„«ŽËX‹<‡aä’T•˜ÔYB5Ö2ê­=Y,ßnwHt~~ïã˜c§éÛ/Þ^^T)s˜ÆI+5ÁY.Ì%Ïü ˜‰ÛªB¬÷®[,V§c­u •…m{ÿôäöªÛ¾yE*5€3¢ ¡ª58(*ÓÎío®/.¯†qÿË_vrú0„°ßÝ$û1d0„Οк“õæõ»‹œ!K!ÃMà˜3 ä4‡mŠ£¦ÂS$B²d‹:[J.,SI"àCc¼ÞÞ‘)2£-€ªªGD '‰)gãšnÇýþµ””SjBsóv½>Y-qÜ_^\TîÞÍõöù—_öÛëÅÂ#¡ª²UãœË)!Q9æ*!Ô4NB”ã(¿¶Šu¹ZuP­ã;}a¶Ìú-DBŠ1©ÈÜ-Τº£w¿²—ëaWŸYÆó|ÿNéhè®-`DtÆúÚÅbèß¾½PÍ¥jMòΩÚj¢£½Vªì…ÌRCòйßQÿ¥Õ9~§Þ‰Ót4_Q5DÖ?Á‡jóQ§?Õºâœõ>¤˜…µ¦WW‹¯ aŽCQåRbJÖÚÅbÁ¥LãdŒDÎ1ýAU¼óÞ{"à2ÃOjãXE2ÆÎ¨–çÆ[ëÍYö»ƒk–«Å¦ m‚–¢ µ4·Pã¥È¦ÂÎy"`æ¦iJŠ’£· žúÔ÷ûá0dBÛ†‡¥·×$eáo¨5zïtýÁÃûŸýñógß|]b¶d@5Å,Ì%N À…n½:Y.7Sœö»= é}=àþôÅ—oÖ«eëÿÙ§ã°?Z0YT™ïÖC¥•Bdçd˜šú‹(Õv^„½÷@è¼sÞqdfnÛîìôäl³Ü¬ÖmÓM1* dÖT$%Žå°ègm]YÅXÒ”Þ]\•X~ô£‡CÿÁ“‡÷>]g­MœEåoÿë/ï>5åÕ²‚0Wb°H!Bïýzµ,¥Ôºˆ Ótyu}˜" ,»ðäá}‡Ø5 ´èZch³Ù´M÷å—_öûÍfHPiò€ ˜RÌ%v]xôðìñ£Ç<Ø^_½ÿÞÃû=þàýý8Ýîöž<"LÓ¤ª ‘A¬ŽêŽ5ÆÌ“ú˜ÅÔ:).Ü÷C.Å9sºY{§»›ý7_}»Ûß{øÃßo](ʦzŒêR!„‡¾¯¡7Ÿ<‰ê~õÙ§÷Þÿpµ>a1»~xùîâÙË×äÝÚo6›MÕþsö ‰ ™Ü4¢c¬`kÛÅfe¥e±–dÖ?8U c½oŒ±]Û:kEØ­V+kœ÷V”ƒ7§«î©r)óÐYU¡ã<±JIEUÉšœ5o (‘ëV«³{§ëÕj™bñ6üö·¿Bà¶[®×Dn÷ûíÍÍɽ‹nÑ-ÏÁ˜7oŸ¦äÌMÁ4À8R-ÅZ+ „¦qÁº†,ˆò»Ë‹8Žœóùéù;noú¡G¶ëÚåbF=.)‹d€)ïU¿¸¹Î9–”ˆ¨k‚3(hÈ6œb>ìvãÖ«uIywsE ‡ƒ7Î Zk,¡*‰àÌ+š!0K_d±X @J±Ž¡ïö“wå¶ùnwœõ~hÍpZr&.Efå–Þ¥WÏ9vwƒï»ëãA¯GoÔwòUEPœ¦éúæZD6«õ“Ç~ÿ™–’¬³û<ϣ瀻;\êwÿÝÑﺇ;îkýyÎÚ”RŒ±rw ¥ïî!ï}ÓuõèI)ÝÙ¦4„år±º)·ãëCõ=Àu75š%“ˆUªÑï÷ÃahÛ` Y㜳Ó8r5p™ùÄŠP`I €º6¬«üãçϾ}úí·ëÌ*ÆǸX,WMgJIÞYåÔ÷}&ÐÜvn˜¢u`ŒíÚÅõåv¢uÎ(—ûÛ›Û¯Þì±ùí§¿.\‚£‡g'F´1®k”¼}wõîÕ%áÄÅ \ß)%¦­?»÷ðÑ“ —œK·XYã†áà¿ÿàÑÕv[8þö·Ÿ¾}ýjê9ŽMÛxïÆqàÚ©É97MÓuÝv»-¥(@kš³³óƒóÌÅ9DŽ޺¥ïûÌ¥iÚÐïl»^üàƒ,h‰y»›¸XçœÉ9¦<ŽsIE›¦APMIrQšxµXþâgŸ<úüövk,¹Cc-µ‹îþÙÆªHN œu¨ xôâYk™g§ñ]%Ī‹åòv·;ŒQ Âùf½Y-w»Û›ý¾É¬Á0 7Ûý~×7MËEY‹wèÈ!PL1M»¦ù¿øùv»}ýæÕ°ß-º®[¯lã·Æ¹—2ÁÄÂUp\?«Î¹¹ø¼ûø‰ˆ±v8ìý¨†`¥ÆÛ4C>­‘L¤ZŒAçæTN|m­Ò s~üèÿæŸþ¯÷«_¦xzþøöfs³µÎôÿÿƒ¾þÓçÃЧtQZP9ûª}J½1DXJ)¹(˜ÇïŸÝðÀÖC`VGheZUp¾ñ¡5Î:kœµÖ{TÅŠe¨BQ%uhê …™fŽ*²ªÐu‹õÉêg¿øä¯ÿïÿ«ëÚõj‚±Ö‡Á;ßµÍO~ü“Ïžö÷s³ûéÏ~±9Ùü£öo·WÿÏ¿û÷Þ/1>ÿú3eAE ä\°Á2fm³ÀåÍõáÐiÇ[Éö×ÿˇ÷/—‹««w»¡7ƬKžâtss3iêIÙ ©,qک䡱mšd-( Í)¢ —¨ hP¸”’™ &©HŽSM3­•Q%„Õ#<¥”smžu–íjô³µv^~ox=[ŸDµÚÿ­wˆÐ4Mœ¦ië4Ì9'Rê„€¤B™¿gßÿÎñ4ôðÎTuGÛ0†5—è}M؃6ÁOS™è»i**T+ÓQ;w]ÂÝÁwµ|Ç£G¼‹qøþÝs{¢ªÕTY»úcf÷ã4¦iŠõ³¤Ç8ﺕÿ™Æ†Œ³ XNaš.e½ZWàç|­š*Ù¬ÖÕ‰F†\Û¶g§g'«5sƃHÙÝ\©à˧ý~ó©õ ‚g I­ó•0¦‘%áz½~òäÉr¹~ýêõÛoßX‚ÖØ<ùÉO>þ?ÿê¯öWW±è~ß“¡àCIyœòùrýÁ{ï½~õꋯŸ‰|;¾wï‚ìn·¨šãtÛ÷ûýAƉ‡ý˜J"¢ŸþÙϺvÉ1_¾»xõæ%:|ñüëWO¿&‘aï*ʼXïÂzŦ” !‘±Þ=yòÞ³gφ¡÷!×xü“>üüóß÷§ëíöv¸ßvm»L)‚ª54£1†f¬cõóCD‡ÃaǦiBHØ4}{õm…¢ †p»Û]lwMãÇ”N øíö–T Ƣʪ¬5¦ 1ôêåëôñÙéÙßýÍ>;;í‡áÐŒo¥f¬”‚ªMÓL)Îâ…ú¼õÇÆµ.¥4N£µîìüœU_¾~+Z£1uٵˮÝïöË®í–+ðs™b"Ðâ}S ƒ1&‘ @”ŒMBayÞnîÿ³ÿñ{öÍ7WÕÍÀ cŠ¿þåß-‚/)}|_²2cœw ŠT}ˆšÕ4‹õÕÕÕ#鉙«‰jú³64m­Éš¦Y¯N|1ç©ï»ÅÒ)Vå*2!4h뢔Ry5ÎZÖT?¤1ê-—9™¡!„{gç¼ZŘúýîÓßü D…áùó¯¶Û«üèç¯/^<}úôþ£÷ž¼÷¤uîç?yïÿýwuÆT÷\‚÷¡ÅXâîp=L‡ÛÝu÷Á…ÝÍÕÅ»7 5 ÌZï÷=Ýìow»ÍEs‰ÓÈ%£Š” ¨ÌYR M§%)E”@%ç(ÊÖX¨[·*õ­q6*€*R ‘Tá:H!DRfcMð.:¥Têç¸. ê|`N¦Ve©Ê¨«Ú;‰PqžÇ%>¢³f¹X”œM€xçªu­^ǤdÕàCµ™çãRÉøH@…‹qÖÓ6 "Š”q<¼xñ ‚sUäŽ€Š ˆ³ÕŠ9眘CwªcÒ÷wR÷»ÃýîЗ9ÈU5s¹Sàð\è¬OÃ9[a¹\~ðÁûß<}zy±e–¦iDAeŽë»Óà—RŽÒ@$¬WQÌqö[§"…3¨aelÈ]‡Qÿ¾³64a±\XK—W—OwO÷û]Nñöæjšõûáš<Ùýö:Æ ¶­·Þ±7«“õÍÅÅÓ/¾È…AÁã­}x¶yòè=‹öÍó—7×[t~ݵ¥$oq¹:kCçÛ΄|óàüÑ›·ï¾}öåòöæÞùÙþ榲SœbÌ̺ßí^¿x¡*MÛNÃ0åæöðøÉ£"Ó/?ý‚¼jÛíõÎXÛ´]ÝÓTÔ]ã÷v<ÎY"Ù¿üê«§ß|óðÁƒR’H±ÆKh §˜bŒHB¦ñújë‚éºnȳX‡¨€dš®D"£²APƒ~‰ÄÆ·)æÏ~óYFù³ŸýlFR48;ç õ¾ëººPaç\·è¸p. s)DÔv"’1¶ÆË««;Yu,ˆîv‡¶[,O69¥a(ˆ}Š2޽ŠH)ªÚøæÁý¨XJ ¡Y-Ö¾ñ»ýáÛç/~øÑ‡?þñSʇaØí÷Þón»µÆÖ`ËúѵÎÚyð(Ç5àÍíî°ßçW+Û4¡i"R‹Ø¶Ýƒ‡Òaß/N»a*YöÃd¼uJ­ñj•BXéù“øÅêíÕµ‚kÛõóg/žŸo6Fsu}•J!v*UnT #Zcɪ² ‹(‚Bÿðþ“_ÿæS[…_õÎÇð@ß4Öº)å§È3úYkZ àw†ûa‡i@%k Tåƒ" CyqµX.rž €ÝœÜ3†.Þ¼ÍS\.—diãn{q}õn»¿¥f™8¿¹|ùÍÓ?~ôäýë·ßþá÷Ÿ¶]¨ú Ê%C_òè-ê4ÜÆaб‘wh¬ ]ç‚Öø¦Û¬–Ó¡Ÿ}¯8ŽCIQ˜™³¢Ö ‚™¡´Þ«`A–ÈÑz;ËTfc»ã­uèŒ3¶b¬È«ŠÕej¬­O"}GuB"KsÍ€¨")gƒ”s&B…y|*\¤¦0Ãõõõ0 ÇÉ©ªÌ2«JÝ«sðRrŒœ÷G¹ ˆ ( ª©„²ÖCÔ¸  ÂåÙ×úæë?IŠÕ؉w\½ÛR3и¹E'$Ç =º3"Ý}ñ»»DäèžeU%@¨(f4dЂb *ª²9Ëóg/r.Ý¢Cœ×ÎGúæüíù¸˜µÆVfÎË¡ŠíëÚ ˜KÅ:ˆrEØÜ —“©Òûaœ.//rÊSŒ…%剻®SJdS·\9r¯_lµL rØg!ãˆË4ý42gç\Û„“åæw¿½ùúO¿·„ï.¯#3Z“b”Ýíõ›×/q¹<ùÃç|òäýv±vÞÒ/>ÿýóÆ‹”œ3¨:ëêEYÉ<õ¿ož>íºÓŸþôÿóÿô¿|ûíW¯_=Œ7—\Š·gÃýœw¤GD9kµG²nFƶmk¬HƒŠsãU_ߣÚL)_ßÞî†á÷?|øèqfu6XD >qPÉK*ÐÓBåëÃø÷¿ü¯Ï^½|øäáýó{ç?½ÿÛßý.Ç4Æ¡¢`ª“³ª9‰(ÅÈÌιšø‘siÛnµ^©b)<Å´ÝÞS¬š‚:6ÜÓåø“O>9Y¬.¦Ë>N¢¼ßíRŽÂ%§HO}ðàþƒ¾ïw·;em¼ge‰yÚíw§§§oÞ^ ™íÍíÕåSSQô*¥”aJ‡¡?=;sÎ)(‹˜ã’v˜¦«««64ë“C¸ßï/.¯rá*©†iGIL>üüÏÿüWŸ~6íú¢ØÇB  Bã$* Ø„`­/DÒT¸xLÅPrÖ<~üä0öëÎ/»eŸËööv³ÙKÊÅ"V¦[Ý+¢*X›âÄ\ÐTÏ­¾}ý´Ø»VqŽr*Ì`¬©¾ˆ ¢¥”iœœ“3gŽ<Å’jŒu¶Êþk±–K)¥Ômƒ{ç¬ >V:ú¯¿~–SöÎZçpGC¦†{Õ&]UYŠ0ßn_›Ð‘ØÛ¸X(tµ^Nq¦C݇~üäý¼{ûòóßÿ&cÐÌUèd›`Ä*–~ß§˜ûƒŠ¢$ƒØ/–²3)MÌE ×¶ÑY‹hPµiãðö6׈‡êf6†rÑáKì ªîíKdQ ë'f–ZÒVB"‰*Õ1qe"’©ö1ž^¦Rµ ™”S&"â8kªXÈslÓŠh 2C†(xï«mAk$pIS™Ÿsg«£T¹$ïÜÓÏ^¿ø–@¨|µT®[Ó£ÇoæœëÒõø7¿›ÃÜ­Xïf8õëMÓÜ Ì1 ϨzAWû€¢*ÇXÇœö‡H†ºnA„Ì¢5p½fÛk«còY£Ï_7DÞ‡ÚJVã˜=æÔö?ΖÅÚ°Uæ~·»¾¾Þï÷µ+ )gkƒ±èƒ9 i·¿2@77o2ÖB„@¶)% ¢*ç"œAu)Œx½¤HEéðÄ9d9 ¸ººÈ,1Mß|ó•5î£>D¨©×ÅY眅c €ˆVâÉé½ó¯ÿÞûÛÿü_þý_ÿõöúåÍõÅþ°m‚«…5u¡ª)ÍŸºâÂBò(¾bæTs,åá)”á IDATùÙ£ûgo./_]^ãõHP )¥‹Ë«,ùÉ'Mè^¿y»½¹ÝT) … ªrIÉ›‹oŸ¿pÖ½yýÏïÛ`s)ý8nÎïýàý.®w&+C‹E·Ûf13䔊÷p7¥A¬ ÕÍfS³¨¬³)—zÚªjÎe_Ê‹—oI¥[®.oö¡]ÛIÍ0åR2!zg U-ÆHΜrb jÎ6§OQo•Ì£'Oä’Ó‹϶ۛß{<ö»˜R cþÙ÷OŸ={öîí©ý@-÷Ye½ß<ýí0MönæKTiãȬŠäX¸”â‚UÐišno¶Áûœ«yâ8ŒÓX/|=÷ëR«Ë,¥|–Œs!t‹“Õ½òOþû¿Þ~í-*€–ÂM–ËUL•j"Xº ,R¤0ˆÑt¸Ù¾u„EJäÁø§·¯^]¼{“bF2…K ªÌ© kÈYÓøÀÌ%Å)§”òþЃù.¶ât!çÌ\´Ò±¼wH˜J6l s.©§”EÔ9cLEOˆª.áA×XSGŠÀsŽ—RLÚÔ{röW×ø0™å„m×iÊœ''’S²Ö¢:S¯VÙЄ»M6h1dHŽdùBzäÄlR3U‘jæä"›—sÊÑÄI€Œµ«eÇqܬ=@Ÿ÷ÂJÆâñ£ì½ßï÷)åÅbA†b*µ²û^D§ÁïeqT.£s®*Àj H%(ÜþˆZ_eäXy³-  (!²p.",ËÅr½^___ßAÊT5¥¨ÊÓ4{¨ÿËZ`ª¾»Î÷é¸Ñ¾cæÔW,çLˆ)'ÝnÇqôΩÂ4ųwöþýÍæôôÍ›(ÊhÔ •’&D!!²d­…\¦±?€°CER’¬Lˆ¬1 —,,„Š– 3óÛW/ED èv»Ý÷=‚CšS®¡óÞûêÝóæa–ׯß\\¼AKÿèÁƒÐ¸à<ŠÀ8¥š@Zõµ(ÃÖEDPçIšš\ Y×-öú©&ÒÌ…¼.‹û÷îoooY!F< ÜuóëÌÖ"YšÄÑLÚ_ﯯß¾º`ô©ŒSÎ ²Ú,Bg¯.®§<²Šu®:E¹¨yë*‹ÅâÎlq¤$ͽ³ÑÜyE+RâäääçŸüb}r¶\­Ïnož>{Ú6MíO6˵!èw»×¯^ ûñ½÷> ²€YÞ¾}óÍ·¯²ðåõ°hÚårqïìÔ+\ˆÐãckcæ¡~Òæi*bí-X„ Õϧ‚ Þ®WKDš0ô‡œK?Fв†?|ñUË\Ó¬á©h)gÁ9—Š”qL¹bV¹ïÔøEBΑ\ðÍÒȘ¦i¿ß[çêy’c,).Ë4Mõ)‹œ«§Á!k¬A…rqù2q±u£u·"›ñ„UŠTJŒ‘ ’±E çTBc3Æ1Ë8N···€†”@Ak(()”Â)F.¢¦LqBÛYã‚ó…e·¿õ&GH¨V­5Îûº—tƬ– ÑÒD¹dÍE˜‹(¤qßOc.IPCãÅׯž•†èl³9;;í÷»7¯ž“MXTXDZ s„qe)9Öƒ±…³ªÌ… cmÛ¶9ó8Å”beK¤˜RÊ!D­ÂUA½  Z›kl5+Yë‰ÌQ(9Çk«ê €zç°.ë+é­©fû’3„Ðûœ³õ3“y®wjÔ–* ;gªø$¦dÑúЂ’!!cIDs, ÁDšât8ì­sÌÓxºYß??õ›õËô¢ßAMØ™ã.‰œsmÓTIåÝîn&Ñ3ÏúKc¬µHuJ£ßÏ*™‘YªU¤Æf‘÷AUªï ×"U€ëc È…s) Z3u@µmÛúü“1<ö¢P¡ómÛV‚M}YªcÇÚ6U[­èg2ðw‹_¬ïrKÆY[JAÝmÖ'gçgH¸Ý^_\\–TÔi‘ÔµALE†@ 6꺦?¦iô†œµDv¹hÛ`ýmß÷5­wöA""¡ˆ¤Rƒ5Á"baЯ¾~öÞûïlN÷·7"Å[c7¶pa©Ñ Ô„0ã«WOsÖÉ{<F½¯± ˆÖ@2Â¥ƒÎ?šŽ[cË®›Í¦mÛa8ì§éÓ/¾ðΑµõÀã:%çLƦœ…Ù’3¶=½ÿÞ׊j)YëØÖ’ã'¦ m{ËéÁ£÷_¿~1ÞÜ (êmæØ.Bd¯ „ˆÖ6Mª%çœÒ‡¶mCð9—’“¨C˜’uÞYgŒµÆ3€’1ιRJh:ïB.Ü­–ÿê_ý+Pýæë¯¤ïãa·???}ôà1ox÷ÏÎûqØÞÞLq|óîbHÅØðüÕ;Ot¶^•qüÑ>>Ýœ^]mûà 43þ²Špa2µŒ›q4UEcœñÞU]/ÚÍɉþÝ;s»ÛÅ$4íí¡7“­ÙvÁSq–)Ïõœ•Y4ª.—ëд\ÄÜîmp,x³â” âöúÒ[c)ÂÓ°ÿ/óŸš®5Æ* ocÈ8ç›Ö“K·;['›D8Ž#‘ ¡©ç¼ª–ABNi"C%Æ’sÛ.Bc©ò,%åm{{ÛWBMÊ9çØ¸K¹Ø^ûEg.ü£÷ÿóñ/ò4½½º0–,°2º6<:?Uë\‹R"Z¯Nh‚h€Dh{{xöüÅÙ½sclJÉz‡h¬5Õ:gù~î*3+ªµµD³„söÞû¦Ýï..UÕ´mºÎÏÈ:BÌ) ëía_ç9¢•XÒ¨bŒ“ïNŠr*Ü4Á7‹,äC×6”AJBŠãD–DØZ Çà™“ Øn³a¦“³3{Ô62UØ¢¨$# H4ÆlëBçe‘Ãaoí”ó8‡R ª¶ˆ¶m] ºÉ9U–«x"šöˆr¬ª!<‚SfÑAÎùÎd!´Fr.qué¼'kcŒcœŠpÓ¶sÙ‹ä¬e–­0SîÆ,wŠÆzAVÒAÍȶÖzë Ï%ð|˜¾+ÿ¿·ºT"Dƒ„¨Êó[7ï"AU}S³ƒÁ©™s†SJ€XD­Ì–…;Ñ‹V*&!–2N"¢ˆ,uÈC)¥ºäëlͰ/%«ª1d­Ñ»XË äQæÂ%¥mÛná×ëÅôñÙÉéå»wOŸ>55·á¨­ÿä±qÎ6!B.ût³ÞüƒŸÿ·y8¤©wÎùÐ8ÖÚËËËygÈÊ9§Rî"®îhûó   ¤ºÅÒû°X,Bh·»½”-+_n¡R°„ šÒdæØ E,E.®±0¯WëO>ùäwŸ}vµ½•Rœ«‰êyŠÉØš= † Πšç®µn¨¼#g©i*’zrnæž'‹P ß4¡s†©Ü;=qÖc~ü³ORѯž¿Œ‡½*Œ).ëõòÑ£ûøüwDúg?ýáóÏßûÎêî0¼Ûn çt}³qÔcªFmñÓ8 »}¿Ûÿà£êN"ç¼X,š¶ÝíwÓ8!bÛ„åb„%—qšÅÊ%9 ¡i6§›¡ïÿðÇ?f•ûOÒ8ªU!»Ÿâí4]\½­r²TJb!c‚¨sRL¥¤}?Æ,×·û" Ф*"Þ[k *U g),)%²Æ[K \ŠqއiJYnnv€Ð´Þ{»S\ι]XTæ’Yê=zQ*TcŠqß÷mðS;qRuÆX”†³¨H‡7¯_Ía6u"C–*¿>/}øñ¢[no§ËS#úí×_¾|þÔCÃ8ê¼"£»[Å9ë¬#N¹¸,ª¾é€ Ê±ÞPB4Î 9ò>¨È œË,Ìkºîá£G?ûéðäña·ýäÏþìÍןI)2좙ù·`IEÉ¢E"‚8¤œ‹‡„,:+_ª˜‡Áìh·\¬NÖD¸¼ºéÛœGÊ\â4¢µd°fÄHI)ÕòDTÅqúþP;D…cÊs ²¨Ï¡™ùÆ„dÍu¨žÀ"Ñì Õš½WE•We­]/W1¦R²1¦:Ja¨a1ÆRJ- T´æÄ³" Çܶ&Å4ßss\¨1Æ‚©Q%CÈs ªßW§˜j,UÝË"€#h± ¾ ¶âýXDYE¸ŠXĘY?3ï +>ÁXš1UXç¨&å$Rêm‰hŽÜ›ùwÆR˜…aŠ‘,ä´xóòÛg_}u}}Å¥ï¬5ÖÙ”â4M¥$kmJ¹–*ã0T߀w¶iÚšÒéœ{üøI.%N©‚ÇEYæD”y·§éNÇYQÕõžÃ ReU4uîl­],DìÇÁ¹æÑ£GûÛëiì7|°^¯û¾ßlNû[¬—AM¬úU¬ó0UEC ³D¤® ÈYç‘Ìa˜ãˆÖXâ ’Qï]BLSͧÃá ,ó·`)5»ýÍË׊`k D,,1Fa°n^–u¢µdf­Ö¶kã”X Ö¢Œ 4ØvÝbqâÉ[ë½sñ‹¿xôá6g›.|ñÕ·r²Y,WKUD€Õj1L‡?|b­ªN_~ù;¤’r2Î6]W˜‰Úº*ÞÇ3Ë¢ë¬1Ô„qvûýŸþô…¡^½]×ÕúéÁÃû¬Ú5íééé0Ž——Ẅ4ǘBSh¥äËë«¡ïÝp±½Õqxw}½'ºÍ½÷ÇUe:Ü–<(ç’K×4† ðäSaãÀ»P?NÎÙiš —2ô*P¦€(ÆP)9r隆Ȩè0ŽÛ›]a-EêPCpmëkrÝñä\AòŒ•­xaž¦ép軕½¼xšàAÉ4„âXR‘\mGsÞ )ƒÉŒŒ]·¸wÿ^ɹiì—Ÿÿú·Ÿýד¨ÚR23#ÑŒ©š¨Â…3fT.ˆæäüÌKä¬sÖ{CÝù½³«ëÛ¦["a•ÏBð´Ù³ä”ëŠ}úÒ¸pyuñÞûONO7Ópûâù7¿üÕßz#ˆ´Xt¥”ý~ÿóŸýt·»U¹Bãg(#˜®›•3ïÞ½ ¡¹a­Ä‚ŸI¼yš˜âÔ÷ýòdQŸÖ~c.ãÇœO6móp½ÞîvÁ7ú¶(^^ß,×n½¾gŠã`Šd  Â)ŽyçȘ¾ÐyGDÓ42»cLãæä´éZÖ£ÎØ¶ ã8æs€sž¦\~›u[¸¬W‹¶ –Éyœwd qI9s1Öª–xŒSEŽ1ç:À!ÃÌeо!×µ!x CMŒ!åÂÊ€P™Wª:ÅÉZK†bŠúò‹q‹Ôq©TÕ©cl= D¤^ uî–R&DKsBŸqd­AƒÖSÓ¶Þ¹åbã}‹ˆÞ¹Ã~¨;ê˜^t]vETSŒ/ž3¦ñÞý{PøüÝ8îƒÆÚ¦i§˜ÇÃP§¢µ½·‚Àó>‘K@ë'Ä…Yx,Ãa¿ÎX}s *Œ/¬ÛÛýn?VëUßïKNÁ{c­rΉ33z·´ë’3IæœSæï2E @UÊÐGcÖ«eh T´}ý-ɘ$Œa.%gªg À¤_]]ÖºJ#€*×9‚ñSÎ Š•¸¨3M¬6Îu ZW©Î™®kç:Ñ;·9Y‡JáišŽ wá’f}¡(YgêÜ©V’¡i°03+*hU¾Š²¡ ¤ ¼!BBï}iЍ©ïˆÁy[Pï §Õ8S q@5RIAA| ¥šÃZëLð.›¥ˆŠ7Æ)NS´†šÐ µ:o!Æ,\È8ü^è`í«p¾,%¥©rlˆèêê€îP6†ŒG©rý*Êǽ)"’­a+¡ µ¢^¶5¦¨ òŒ@¨øûÍÙæÐ^¾zUÛªJ!¥äœÏ¹G2®ªÏfÝ*HEÛ2Ö‘qhuŒå?W‡D\DŠ(aÎ…E½\ŠŠ `N‰…§4Tmmm)µp’XK`,êíö @±•IÛ< ˶‹Ã„h>üð#~óæ¥ˆú}ιÞ÷%f$4d,À˜'fN)ÎÞÔTEU´H„1¦8IŠª‚ÐI»†¯¿þúêòr{Ó-Úäôøí÷œsÛ>#£'K3ow»œ¹\u¢š™5‹sæÖ¹lˆÐØPn[@FrEb‘jC&Å(eˆÀ›ëœ u³¨Wg‰ XÛ.†}·•*TˆÇ´ÙnQ³%$BKÔwý”¢ªÆ˜K–ßcE2 §Ì1e4$"EçÁ"tË£rüVÍ)k@ ,ÖJ÷¡ÌÙsfæ^­s¶TsfŽô5»—…¥X \Œ¨pÈ‘ ú xÚYB¸S)¥)¥Ñ8N†ÈÅ”Ñ{«Ê,Œdˆð0) *,Aõ\T&|Ø»ˆ÷ÖXÓ4µÍ–eË·#c=dfË=Œ·35TVôZž[%ĉ„œóü~x-c-?û¼ÎLÍ¡2qП•°MŒyœRÍ” !rΕp÷]‚>¥´Û훦-G¡”SΙ3‡ÜÁ×úuÄóë½"š»•FJÌ]pÖ™*ø1æ#+³À¶ÛÇ—ËÕ¬]­onBUíwZnú)%R|üèí››«m·ATaa9àƒ RÛ¶ÖzEª–ÈDƒÆÐ )¢°YŽ)¦t ªß ç•ãÈŠ * î`¶JŠxHOM1m-Qp>©NÓøòùóÇ‚˜ÒǧixýúEÊ9æQ˜qƘR³$C1ƾyU$¸cý—T5žŸ“·Ö»\6Of›1öÒ‘³ª~ùåo¯o®Èú!÷ó—ÿÁ†ªmf„2n._:‰¹˜s|X­æ¡ªnÖ›¶wÍT‡ÊYw€£¤8 C×w)±*К /ï½TÑRu+cPE•qœúý'Žqìºíå›óÞš2_ï:Ëöáêø­·gÛ!~ùì9±ÎªJòè0¾~þ»—Ï?uV¤jÂ4N›Íf·Û)€ó^ArNÃÐ!šY;+§‡r^)-ȪªÊt¶,~Œ1ÞyKÆ30oö]F½?_.Ke…ý~S >|È0 ãÔZçBðÞ‰p7 dí8 ÎûPÕãØOiš¦ÑZÇe¾&‡¢ÁƒÂž9¦ªÊ‡€e‹ËmI #€*XcȘªrÊ´Ö ’ d- /2® «ã£¶mâ0Xc¬%àtHê]-‚UE‘€÷u3?>yxrïø‡ü_ü"í󸛆ý¦ëÀ8o+ë 0³qT®v Öñue]À©6Õ IDATÆ*Áš¢¬c$4Hh]C¢œsN…H¶š­ŽËåÍÍub¥ë:2³å|^Ï%,†aêÆ~›Ç®4ÐÛ¹A¢qšº®/#kLfDVL™ËK%sèb)s6œ‘lŠBˆÌZææÌLX<^åÞH"BE¡œ™  µÀY“³*_–@µ®BÛ6ÖY2ª:¦è­Ä”²óÎ9K¦âœ%¯êƒ(’5¦ÉWÞ:ãÈUÍÌ„ä™K¥5)Tue­)âiI9“µaÖ”ñ}JÉ€†ÊÍÑ’QfP<üK”l»óÎSF+>DÇqš¦Òž5†œ3ª 9e`!kRJ… ƒ*ÅAè‚ïû¢Ã"¾;ƒ—ãye©Rб—±8tÊät;Ï$gI·qÒ»ÜÑmÃ_TÕûà«pvÿl¾˜Ÿ¿yœG€cJ¹¼S‘qÍlÙÔ˯>Þu TÁ_^\dHI•êv¹évð'gmUÕH–P½÷d˜:´+@/j Q!8 ¡Tuë|µÝl¬á”yŠ©j*ÌÓÞ˜d £³¢Ii4=Œ}D9³ T³’æ¦iF„i§qøü³Ï¬5ílþŸþãäœÛÚ£11cΉÞuÐlñS¹xËú/å Hw"—૜#GrùüSA Ü)%HýÅ›7ûq¨š¶ª—Þ…Íæ’b›õùÍå³Ê+¡ÄàÜ|>¯›vÊùz³»¼YWu=›µ…ô9 Ãv·‹™½wg§§''GÎÚ'áò~y('#¨sž§r9€(À!>Ÿ’±¦nÂåù«¦iß~ÿé|ßÍV7¿ýøƒÞÉ6Ül÷«¦9™·—ë›—/¾üí§¿ûø7ýìËÏ›•˜õ¶B¯·Ʊ®ë‚R(‡•”"[º„ ®ª†Yºý :ŽSRÍæ1%È)["àJé€3+—cJÜwûªiê¦qζm㜷γü8öóùlÚïg³EŠ£%@ጤ’Ë­Ò[Þ¬s‡Ê1㬤”%YÎ| _.BU‚ÈZ@Æ’uÎU%)lœ)‡BKƲÆÂ!¯ƒ   Bd”؉wઓÓ³Åq¨ëê·ßzúôé;?ùñß\œŸ—×PæœI’ó‰XUTSFo*ïÃìääôÞ‹áÕ³/bÜ»êºî‡!¥X\(""sž8F@CIäb³}}}ÓÌZ´FíQnÞÜ{çûßÿþñ¬>ýêåËÏ?ÿô—›ë«Q¹CȘº©Ûv6Å4›§˜bŠ™…«Ùâäþã)NÛáMÌÂ"!80€Šš…¢€±VDE’äTB–†ÈÙR UAgÑ)äiT´æð ãDDÌ9%dcйYUÕ¡™sŒqßu„XJŒÝ~(ƒ¬„`VCÆ;Ÿ"§˜|0dRba)s €œYXSU!Ð8M•óˆ%„ã €—ˆŒ±\´D€ÖZÉ\Œ†Â…© )åBû;Ô‘ø A„BADkÌ4N·À„H–\¡äqŠ9Ýšþ‘|U=zôðÍùÅõÕ&ÅÎÞÒ{nµ'$"ÎyÎ9ãX.-2VDK¬S˜Aá0«M’ÔÉÂñ¹Í¼gN"JhR-Õ¡ëÖ7k½}b!sVÁvqtrvfõú"&¨ÛÙ|>³†f‹Ews½ï»ÓÓ³‡õ {GmÓú¦²)¥Ó” €òX }m&šÐ#‘ KR¬\PjêÙêj6¸¾ó¾5ô›Ï?ûõ°'Qc‚…2õ3hA–,"æääáÙÙñ_üz{sžãÎZD%c¬±’s¨ÚoÿÞÿìãŸNœ"‹ ˆ¹í3–I öc?ô½1}®±¶|¬1Æç H êzJÝ0îv73ïw"è»>¥d HVQ«¡¾²‚<ÅÉU~Ómž½|ñðÉÛv{ÓÞ»g½sUU¿üü³þhA¤`Ñ÷_ýó?ÿÁ÷¾û?ü÷ÿÝv؇ºª²o¸*B̹mš»ü—h2Ù²±sÎÍgmYø3ó~¿?Z-ClÍÂq2Dµs„Ù;“}ŸbDÅÌ, 9gæ´ÛmôÞǪB4…U„T׳¦šuv猪f"ÌqR-™^-ôòÍdol¨jcméÎï½w.¥¬Hdlbv.¸C4™Å8GÆ 5ÆÙr‰Š5TÞ;@K‚( †ÀYg­ea°–}åªJÕ¨PÛ.žÇø«_}|³Ýºººï~;k_<ÞíwD䫯…ZµX@]SµÆYšÍ—ŸþùŸý7ç/ŸýÛû¿AÎy>;+cúÇÌišzÌ9ò­­ç‹꺞‡ºÚu_[æTÏW?xò~¿>yçOÿä?ÿÉÏ~÷é/ÏÏßtûÎ3›ÏÈû¬2ˆvÕìx±°¬À Ûͦ^.W÷ž\_¾ÊzE€ !AÊ œ kàpl¡’ÃÌ1å”ÊÖˆ9¨!ªêêøìô½ÇO.ß¼ùõ'¿íûT¿~-(ÇyA4!tzvª ]×í÷dv·³æŠr¥ää?èÄTºœö;fÍ)§Òœ""CÆXË™cL%NSò"â]ð>°HL±8¬5Å%ª`­åÌ}׉HSÕ„¸ïö9s9=•ÇŒ±ÖšM"LDUU9g™å0ÒU%¤â¼•µF„SNÞ…â¾|¨S¾J9«RyNú±‡®l.[ŠÙeNÆÐ!=$r‡×*é#Q¸Æ–lX™SÅ)¾~õºL«˜ÓÁåŒXöáÖ˜åb™91…zf0÷Cêdh˔ąæäøÁÉòøËÏ@‚]7 ©'д@!„Ùlq¼Zž®VG§gg³åQ»ª3»}G¹:ßýæwŸ]¼¸2nÞ/Î<~üøË‹kk8õûÄ”¬ŸÊnU-ùEæ4Ú”X7Û~ê»~ íÌUÞXãŒÆAY2O||zïþøŸ¿yU„ÅSÔrrëÕbæ,œsšR¤ ‹°tß@°Øb¡AÈš6ÛKˆièÆ?:º÷ö;¿~ýšsæ) çå¼™Õ8Ÿ77뛩ôHÚ¶­B(«Rˆ³Æ +3;ë–‹6umœ-ßâAIª*ÁYçœ1fJyèz-h$\,æóÅâèøøá£‡¡ ͬ-WC×÷}·oÚöújóÕ«/WG„ÖÛÝö‹/»]Ï·››àõ½wìºëË«Wu¶q_*Ö)gD´Ötª’Š MŒÙ9ÇœUÉŽãD”¼Þ9ï}ùì!¢‹s¡müjÖæ±cªºŠqÌ)§SÎÌY +R•”“€jLÓn¿™/VH®¼øŠhðÕ`F: B9§)ÆQ$+sS×gggÃ0ìö{½Ü[ë¨04ÆXNIj*‡jÀXŒ‰]@ëÝ!c³1Î9¯9g<ÌXMJ‰•­µ6ø²¯ êsΙÙZg­ÇD1rÞµ³ùõÍåÏþÓ«ç_Jܵ³Y;›³À‹ç¯8KUÕYM˜/O½oŒñÌ&„*JJ¾ ³Ùg¯ž]_\|øþ´i«”³±ÆûP×uæ1Æaèö›õæz½4¾jÚùòèø´†Ý~óòŧ닗™â¾K§Ç÷ñw¿þwÿæ_ñå§Û›­Ã“û÷«f‹e]U® óÕ‘ ‹¦>^,—.84f>›m7›Ï>ù-ÿàÁ£›+Úí7e Ê*¤ ¥“(ÌÅTnl]7Á×Ó8v]' c‰‹×œ“³¶©«a苳pÉKªIÑ*3úCH9§¼s®¼‡ŽÃ Ì>„²,õÚ”sF´Æ²D(‹ä2ÒŽ#)WƒuQ•,a‘¢oPMSâÛoDrä²U€ý¾+f(a±dŒAû!s>DVÊ Ü$´ej­‰C_–2SY>š‰SLÉ:Gh`Š’RÇq¾X¶<Ÿ¦ÄªIø0R,$2¸ÓaiŒñv4lîùXBeeBå) Â\°ÈÁz(å¨BmŒ-ôÊ4ź uSÌ]©³`Æ‹ó×Ý~LÓ`_,ŽšùчïýàžþÅéòèí‡ùñ/:ˆcìÔíÑÛï}`üâürÛ¶³ªnV«ãßÿÎïýèÏþq½$¿p1a7è¾Ë¿ùõ³Þ7›ÍÓXK²iM=ý“Ÿ|ð­ßzºKüübÚ)LSR‘ÕrA ÈÜ­o†¾ŸbÚwÇñòÍ‹/¾úü³Ï> aÖêÐw»qôúúúþ_þeßwB J} qQ)³ "úQÑš·-BGââeÕ‚²á«ýfŸÆ8¥ëÍån}¨ÉWÔ¬–«£ãÕ‹ç/ƾ‹Ó0ô]]Ttζm}ïÞ=çܽ{÷¬7¥~e­­ÛšˆŠÑ¥±¶4&YE˜ÍA/eJ—nÌ95uƒ€Âz°‡¡úªj›Ö¹úÁƒ'm»Èœú¡óu0&Xy8å£ãÙ4õ1í2+ðD”¾øâ“å¬}øø~[Û—Ï__Þ\2ªEf(­{"ã€NÓ(¢úqäýÞãœͪÚuý4M§''åM]•oÖ›ívcšµµsh8›5ç¯_¾™-Иi䂯’1Ì<ŒcN‰D¤ijDéº]ɉ©Ê8ŽÖÙ¾T¤ïöC¿GHœ§œã!ð€§ûô£Ÿýt½Ý[ö†DhÉX ‘C  CÓ0hÎYDQ$8',±Î_¾zöégW×W®ªb{UC@¹ì5§”9²)EQ;c¾ªÂl>Û‰”»!ˆŠµ$Z(áTˆ Æœ˜‹‡Ë; ªÎ…²ÿ¢¤…3I·|5¦¢J)kvë¬M9byXCÎ*s&fvÞCºƒb6_ƒ‚«V¦("dÈZ.‰¤œSŠQEèvöRUÕl>÷>„P«b·ïöÛmÁÕsÎU]ßʸÞäV‚ˆ€ÎÚqšîàIÞûòfSäS€9—W) Në @I;•G¡/€*‚ ‹A£ }? åó/uÖPLiu|º:»ÿ÷¿ûkÛøÃÓEk$Ø|øöwÞiš¦ï¦~õZÝ{´4?nšPÅ8 s›¾ÿÛÿ¢Z4=£ñþݧ§ï¿·::}ôà[‹Ÿÿìâé™ÿ“§ó÷¬êÚ)Ý&xÝÉÑ.íú1%QEæìÉNC‡ª¶Iõ÷+çmå-j¾Y¯ÿú¯ÿúoÿê¯Lê„T8•Xªsf?l‘¤ª¼¨Ž2A0«p.{u%CÌìŒ==>]Ìæ7ë+QfÍ™TÄYú©Ä®†·ãSzuñ†Eê*)Y-Ï‚»íîõë×/Ÿ?gž~ï[ïYãUQXµ¢<ôûín‹DSŠ¡®bŠëõ ¡qÖ9çŒ5UYÊ>–l¹i ‘(ìö»ÚWÍaɪZFUÂBÆÎç«”òõÕMÛØ¦®‹v¿ S·Ar•µÎ˜4íT9CîÌ¿ó­§ûíz;›/î=|tïÁ[×ûÓ*ÆD£%@£µ®Ö ±jÚår1S9Ô/fóaʨ˜‡a8?¿(d·”¦Õ²ñn5 ]ÝÔ'÷Ï6›m×O¼ÕΖ̒8e-†H^=V®ªªÚ9?›µ}ßÍæ¾È­‚mÛŽû>ç8޽wŠ¨Î¹Âø§HÔMS䜈PX·»ÍÕÍ5‹,æ *ëœ ¾.É«²vƦ“eòV&ìΕq„eè2æèä˜EÆ4!’#‹S$B JUhŒ±ûaê'åÌ’Óƒ{§o½õøùÇ hDçéÕËgÓ°)&K@]CÖÚ–G«§OßP|òÖã¾¦ÈÆW˜•€5æLJƒëÛà `Ìšå)9@§ÑX·ÞlÞ;ýî{Or½±ûÝ–€S¿ëwÛÅ" »›ÝvSWáþÃÇÎWç—Ƙª™¡©­3hQ±xQÑWáÉ»ï¡ðvw=öãÉêÅ8å¨Rêå5€ Í"Å3g¯ªj†"§ac ^_]1sÝ´, OÓ4QÛÎvΜ dBTÇ)„X˜@X7•fN)§œKxID°ÔG™¥¸ʧSny;RVSJ,lÙhaÀm^ K:nݪˆJ¤OXX˜5Î9D cbJt(R©~­',àa$Ãå”Pcã8ÜÑš˜… •u¿*c­±)¦n·ÇÑ©½¯ëºTÀ¼ræ2b–»lLɲ¶Ä7qÁ·C)¯ ¹–œŽNÓ”9wÝž%“1RÀgˆ" ªåle¯ªfµZ¹ÐÔíê[|{¾\^ÝÜôœN—<}×£Î+[·•µœñ‡ÿäO>úè×?ýÕ§‹“'ï½ÿîÙñÑ|Q“¯¶û®Sº¸šnúi~4{öãß ã;½óÎj¾X=héé1Ínqþ×#þì³õg¯w]·ŸÕ!83M½ª $œ÷Ûë`«ÄqÕÂüÁ:Äÿãßüo2ô©®ë¶m?üð~õë_]^]–ã|y·*ÿ‹²e&Dóø­'«Åñ÷¾ûa]Õ_}õÅ‹—ÏÎÏ_ 'c @E’H^["k è3Ç”a|3›©ê4 )ŽqŠ¿÷ío­–ÌéÞé*8»ÞÜäKÏ’]-—W×kB¬ëª®+g°®[kˆpJ QCÖÚ®ë™ïtŒ“R@ k©nš¦™Ç˜3Kè½[-ç 9¥$yomíÜŒœÉ9æ…ãja¿÷ïÍ›ùúújÓmªPå¶}øè­—7ëÍ:N= IfÉ9#Qås¾Õ‘›iŠÌ",ŽLUU!ø)Æ2žÝlwÃpÖÔuí¬Ñ'xÿ½o=|üäÅ‹ç?ýù/Æ¡ï‡aÈ’Ð9Ówû÷ÏbJ)庪sÞ†~¶˜‹Ê0ŒÂ2NÓ”¢*šàƒ“‹X‘TašFæd-¹`ßûÖÓ]×§1"{ZÜÚ9ñ £ ZhŸ2Ž£÷CÎâ¼EkÏtÐCX´–T«¦±Ö…P«`бè-D%C&2ª’Rž¦~6k‘iê¼ ›Íå~¿C…'å”Ðú”3_-êY½^_ù2’’õÖYã£jŠ9å¸É{PdPt·`É g6(±ß÷ûëÕj9««Ö¶ÞXÕä,*ÀÉÑ*Çþ·ß±Æ^^¾Xo»ùòd¹\´³å4M…º‘9)AÒL‡Tzé:«jMhçójòÔ9kî`³¥>c­-ñ¡œ“xï½·™§œ"9Ç‚ª Þ{뜨¦o= €(¥ÔŠHÎyDTbK)—»0¨xïLU ýPzühJQK5I‘ÕÙÂ,*þøÒÑœbÌ9ªfADAD2x l#€Š 0ß)÷¾64€²h ‚ˆ–û>Àâ]GônUœåË ×{ï½BV½ ÆkËcLSŒ™ÙZÛ4Íñññz½.¨ÈÊ´ªÊ×/¶¦Âr9ã@³…½c­a†2ÌAƒwq‘rºGÄ”c ¨ämò“@‘¬u‹ÅʘžYȘÅêèí£Ó§|k×íÁÙãû÷‚7ÊiLƵϜ1Þ…ª²÷ÜûÃzÑMÏÞ;>9rõ¬›ØZ:=Á®M°§÷Éï§¼Ù«Dóf=³¹â™qÓ§ŸrõÅ›~SŽS¿ß:ˆÁ•3”<G´”Æ!—QðdHß{ïïøýÿß§˜oÿþ·¿éú®ÜÊã|˜ ADY1|ë[ÿà?ûÏH@Ý~ÿλßnÛeLñüâ¹5æí·Ÿ4³Ù4î½ÁÊÙ/¿øbè»P×Î[ï­%r¤ˆ°:;Z,VM]‘ïˆs¬Ûzßç]ÛÖ˶YÌço^_â[̗ϽýÕ‹WÛn`Î㘅 2Œ1!„̬ ¬JÆòAí…¥{XÖÈ ÚÔíâøÞõÍUNº¾Z?}ïy]í¥Ó1£"°ØœbN1öSÌÉrcê‡ûº~ªçË'ï¾ _>»¾UåÍvg­«ªÀÌÓsΠ¬w;¡”R¡ª*-ð¥BÁO4mS‹ŽW«Å¬µÖ"À;ï½G¾ú寳Ùìëy»\-º}×u;Côôéû7×WÏŸ½H‡6;؂ՄÃo.¦KD˜Ï毞ç:ØœYouö9gï‚÷¡É9ŠÈâxõO~ô£ç/_þöן^_ï¬ÔÌd­(‘Ójrë.øcJÒR#,…‰èëZDò4¹B¨ëª9>:ä×××%ù²æÈC?5H4ŒýçŸ=ÿÝo>vÁNqRµ9eC®ÈE!±8gCå·Ìi;BAÑUe‘4ñ㔘%g&c'¬ Æ:Øïn¶_Ö6Ÿ¶náZF0Ä¢2æœTrÂ8 iÇn7l·Á¸ÐΛªµÖ–&E¡®‚3TNÄ€ ¨ª1%æ8ŒãÍf½ï:åCVè.rWæÎwõ¨’MiagÝéññéÉéÍÍúúâÊÐÁ[MˆãT2*åv§ªUU9ëäƒu`ÛZ³ðsîû~œ&PõÞǨ‡!µBÁ¥ ¨¨’†œÊÀLJ[ÕS¹Š™ "ñ›FÖ¤€nQ×€øMµqæƒ1㛞Õò9» ÿÿl|wÇ…OVãq¶ª«cLIÒMgÓc,acLÍÕò‘­ü¼ß€Èkæ§—s¶Á#B·í\f݉mïøwî·2²Ö•[–\À¨Ã0åÌœXÐXÍÂIØU¬aD¬ÛÚ™¬i„4(BŒi²Þ;;؇a{u“Óåe;?Zž®îÍñÁ[skaøìuVåíuº¸Èq»ùªÖcöÛíõz;Lƒ1¼óFHÙ ‹’Š0ʤiŒ*DÆZ—sûI5ç8ê|øáG?þ±æHdã4<{öÕ¡¬GeIq;‘³Æ‹äïß{üá‡?tT¥˜DOŽ?~k³»Š±Gä{÷ïÿÃþÑË7¯?ùäj5[ŸýðèýwÞýäóÏîœ4m‹H•÷M]O1Þ\_™ÆUÌYUÛ¶éûºn‚1ÔÖÌ)Â#ª~ðôéùåõë««Â\*×*؃½™‰È€"â8M»Ø/›¦öÞ×um ‘1d¨ ÕÉêèOÿôGñÿ®Û¼9Y|G§É°PÌ„Äq˜¦©jêf{³}óúê­‡On¶;‚ v}o|E!,ŽO–Ûn}}Uì6‹Å2¯ªC×—)b‚–C=Ò-ÃU¼÷ý0ìv»ùlœÞ $‘Ò¼Š›aH‘ˆæóY¨šº®›¶©ê:Æ‘¯..8çù¬Mqêû.¦)x_Õ•1&T!TUJSÂÓ§Oo®^NÃ.Å<¥é°&AòÞ/—«¡ß]^ž³¤›ë«ÿóßÿ‡¾ó½?üÁ?ê&µ·æL,¡`"ï}f.T‹–­qd\ÍfdÖU!lö{WÕÖâñGG«ýú„QXEA o}[Ï.vo^¿~-œQ˜å”ÕKÆ!!Zš-æGÇ«y[ç4Š£4Bš&ßÌŒ3 J¨ž ç<Œý¸ÙõYŒ©›6øD§¡™¦ýVâiì=KöÎÑ8öÝâêÕÅùõùË~¿©œ%  DF…´1-´„‚*ÂÅeRäqÈÆz ,½ôÒú!"ïŠ(–#3«ªê|Þ>|ôÖw~ÿû9Ë¿ÿ‹ÿ]Ó$ÊEˆêœM99gJº¶Œ3Šy¡(ÛáÀ&3TUã4ÓD@!T!TÅ,SRc3S:×èÀtd‘Ò92Ö–2”;èÿAÃ]tv¥æðMÓ7 +( ¡æÒ‚ ½…·„ðg½ë‘–9iιàʨ/WËãÚWÞ8"lêжuá>m÷›ý–¬·²ï:UÐùb¦Ïž?Ûn×÷ïÝ7Ï.?çÇq(I :.@ï„_·‡e"co6ëÄzß;´(‚ˆu]/óÍz½ÛmsžZ§Ëy\‹ªäѸbdEkÍl±<:9ýì“O7›õn?œÞ{´ëö1oã8¼óäíP5Ÿ¿~ö|š&edú¾¿º¾\-W)¥Ýv‡Kjêf¦išªªöÞ{ïKr†n¿ÞûàçóùØ÷œ¤©¿¼¼úðæÆ_ýýß¿¾8÷¾::>=^5Ã8IæÅ|Ž=¼÷ðùó¯æóÙ¬]L‘«pšF@SN-¯_úmî@ IDATŸÃ0ýì'Ë#,I¡ãT•SsN…=âÈn..|ó·?ø‡ÿô¿ü¯ÿ[+"€’™™¥L»°ü*ÊÄ$c¼s6Ôd,øÜ‡W™L‘1ÅBPÆHLœEXœµµ‰ÈX|õÞ{ßúÓö£¿ü×ÿ"s– È8BRÆÉZš­V§§'ï¿÷^Õåù¹L³Óã3†üúåKj_7* ™ûÝn·Ývcê»qœFëÌú"‹h´ÆÔfÈýõËÚNÓˆHºq‡ÉðõåKà¡ ŽRÌÓ8‚1>@Ã""Z⎀‡– ‚ŠXŽOκ³ëߢ¹Ó4‘-°h8g-2jDkŠ6¡ng§g÷ÞúþßôRávY$*Õ›r>M)± fæÂv/g"L1•sk¨*‰1ÝÞIQõQ4Æ’™R>¸µË‰Õ¹ƒo,s™´”ç˜Èaút«-(Yãxo7˜d°—œsÆ’µ&Dz®Œ=8¥;uuÉ]”)yŒ ‰ ¯„E)¦èâõåUâ’–ÌM]‡ª*l·›irNÖØ’(%Bk¼ÞR- Ù "”㡪’"³*Ê)ç”Éûp÷zQ<%ß„8Þ¾sB²ÆÕu=NCÀ© :Ø\@ §œR¶Î‘1šR,O §Š€È’cIL„Uݸ>Oi€,È©v`“ÆíùËÝM7Ž},Ó8‰–G&šØ§qä}ç,ê°V©ˆ3sJ@Ì8"8kI ‚çDs“!ÎÎÚÕb)9ÐÉñ©‚d)ÿ…+YC-µ„; BᾩH[7œ¹ëŒñZUMÎùüÍùã·kv»ÝùùùÐ÷λn~ñ«_瘆aj›¹%§Y\œ%„²éÉÃØçœ¬CfÆT…UÔÀ'Ÿ}šsþ½ï|ÀSüâÓO?ÿâ‹Ï>û„Bëƒ*Õ^">`BªÂ‹Ž)š˜òf·;=]‘r޲ÎZBº¼¾üÿåÿTsÜLýöÊ{Uc­s¢²Û퟿z#?xøèÉ[o§˜NON‡a|ÿèx·ßOã´ß¬×W7ûnœWUšúq¼EjêÊlꪺ®IqPùœã81ƪ G«•2Ÿ{ïéìäth†iۦ鄻n¸Yo櫳fî΄ñøø¸mg—W‰3)6u3ŸÏ¿ûÝï^]^|þÙüøá£j¶Pc-)å(MÝ.WËÊšiêÇah›P€‚Þ»rÛí77ë+*·*Uq†ÐÀOò·¿ûôs‹¨D¥#ª H€xMZ‹Æ_#!Y‹Æ@2«èm¸Û²1Å~ßõ]O Ã˜²Bæ¬* <ûínÝÔúòå+Q cIà`B7¶¼‚£s†ScǾ¯½ïºnˆ©ë»˜³ˆ’uª”ºéj½žb„”E†]Ú§œ†>Ǭ*Œ:Þ¿w,r»”sÎŲ‘Rð^˜yÚC4å(Ê))YP°Î;gœ3>xkKùQ‘ȨYoª&43téQC†Ð`‘ö©ª!+"å ¯"Ó0)ËÕÅyå-‚Ä4…Ê!QæÃ°[UsNú¼bf!”âAŽ9«D§q¹ZÖu[U5 w1FºÓi8ç «çp/1CEì—sú‘õÿ:`/ñl 4‡¿_‡!ç¸19gæ¬|Û*@(B+ºÓU—wʹdr½¯¾¸‹Þܬõùgç¬Í­u"2MCßïJOU¤Léî†?†\A÷ØíCˆàŠÐRä AÇ»±;È-öù@ÛWÅ‚š//X™Ó]³„/Ë›™5®õÑÑ‘)•ãœSc @Q 2eÝwXr8k«à÷ûŽ™½•Õ<0‹‘A3#ZC4o½Äi$ÍJiH;Q•‚ô1AxJ¢)¥n·Ëc¿¨ªºªC]ÏË·!ìCÈÂcã4’ÂÙééëW/µ•¯â0D.‰-uæðÙˆ1}-p-#E(H9~ùì«wßû6‘†˜'ïàôôÔ[Ÿ£ kí|ÞšsP…~__\x[µUå­kšfutÜ÷}JåÓ«u]õ½w޼7¢âœ™âTÕ-gmí“'O®/¯Ænç­{òäíí¦ŸFîÊE‡ ÖúrÙ‘!4sZo¶Ÿù%‹0kU¡yôàNéj·-ºÇ¾ÇoÕ`†î…×××Ç÷NÐ:a†“““ž,«}×·íìæfS׋õæêúú|>«Ö7n}³~õêuJYÒ”ÆÎö}GØxg›ªáÌÆ˜º€Ê9{o lÛ¶õ> Ã0ŽãññqôZÄYUmvÛã“ãå|~su€÷ÎÎ6}¿ëöÖÙý~¯ªUl¦ræèû¾$Í8K]7†Œwn¹:r¾q¦ZºàùñÏ5¥ª®§ao¾«û¹¯íÙpHÁ`¹Hˆ,š4¬ícPÒÓ‡F¸½+ KŪ¢"¬€Æ± È;¬Š ýfßuÝЪ’XKyU•ó¸ßûgaáд•#d–Ì*̉•µßë›+¶ªñüÍËõ͵^T §´›FVMCâ˜tès¿OyHyH9NC§H„MÛzc–‹…ói˜²£LãÐïTSœºýTlô€Æ†Bošª< €!råE9ggH@v›í'ã’ñÆNœL©ÑXkY ç¤ hâiæÄ1¥izñÕ———?ÿ±ì¶kc‹Ó*sÎ1ŽtÝ•<˜ )À”!–mGiê’sgMãL˜ê8 #äø¿Ü¼J$îð$.󶢯†T¤ŒÔ§ÚÛõé7÷w~(é1‚+e'šR,‚oÔÃ6µXÖôöPlŒ)ÐÇ”R*!¤¦iCã81«Ÿ›¶ 4ŸÍONN}ð]·Ç¡0;ËN^ŠN«H·só²’â›-=u¨JDd˜¥€-ËÅå¼LT†Ú¦Ó´ßªÃ­X2(zŽWÇMÓŽãh*wØáÃ!Z.ÿ…£*©¯az‚‚ – )æ„ǘŒ"‘!ŽÑ(˸'™ &Γ³™$‹ç˜„Ǿ[¿~^Y“tnQ‰d}g³EÛ¶ˆ4½uF-}õåW›õõ¬©Ç!m®Ö’óÅåEâìɰˆ9È̆;ÍËÁÄ¢¢÷»_Ý»ïñãwcÌÞ»õõÅù›¯þîïþŸaͶ[ÚEÎéúúêææz†&„÷Ö¾æaâ8ö]÷æü|œ:cl•1V…sŠ duÖ2ël¶gÏž?|x¿ þ{ßý"ÌšåG¿øíÐÇlòr¹Pë MΊ ÖÚ”„ó$ª›í¾bÎ  Á‡yÝþèÿé²ýøoþî'ÖZ"Ó4³þgÞÌæÿê_ý‹£“úáãÇmÛªqÓÄ"êgU¨[@šÏÛßýî“?þÕ½³ÍlñàÁÉåÍ5‚VUSùºß]JŒp0ØMqRL†dœ¦PW‹å"„P5US]ã„€,üòÕ«õf³X.h»1$æ”Sú>8'ÊÛÝŽ3Ç)®×›®šª©ZÇ)×uMˆ*9&Ó§BÕÃ'Ÿ|òàñÛGÇAQT†±ßì¶)ñ8F%kË‘ºÔÆq !”ëŽn3ÃEþ£D ÂVDrJÆERÑ”²sÞZWv­" ÂÞ¸¢:Pç]UÕÎú~7û®G뜆ǩ뺳ó‘™3ì1™”ãØ¿|ýæ'?ÿEÃ=¨G¥”YP5iƒ&¹¾ë»®ð1õ}e»wÖ7U¨ëÊ*1NƒpÒ”dìxܪeÎqÂØ{ÉÖÚÚ¶ÁšúývG¨œ’N“æ4ÅAU¦ûn—S´äÀX" ª!T''§)Åœ“!Ìi˜À™¦©ˆˆsN1uÃÔ»<(lÉ *"‚@äœE¤”2V ŠE?ôúÕ+2¦ª|Aœ—ê’±&`Uæ3…Þ \h* ¡u–ÆqMS² U5_,ö»]¡R•!ÌAód­%§©@Ó”¾)Š‚Û,Å7¤w·õ[¹ß—¿¡ÂC,_¢èôÎúd¾V­JŒ1Ƭˆà½-¶–²ËôÞùàéÀÁOÞ‡ãÕªmgœóf»Þw›2{PçœsüQÎÓ8b!=Wwñ”$³šâ®Ã’‘t±˜[rÛÝ^UÈ”þyјš·79ÌýDX’ïmYkˆH´¬V@ ÀaÓk¡ç”5GIMðó»´!Œ¨#‚Ö¡6,pŽ]b2Ö çŒÊyì,ˆsÈ‘ 2§a·ÙU¡šûêz½Á)"8N'Ó縆³³û–°ú¯¾úÂZ‡!ÇDÂo¿õ°®fy’Ÿýäÿ}þò™¢–Dp‡ômáwJ9É¥”rÎÞ{2€†þw?~õêõßþnšàg?ûO›õB­CcÖ¬}7üæ7us½ßn—÷ß:>:¾xs~qyr:žVëÝîââRA|ÖôÎÙqŒSš¬1}×ûºš/Á‡PÕ¯_¿Úîwï¾õèÕ«×Þ¹ùË¿ü«¿MÓRDb‘«‹ëa˜ ?•Ȉj? Î9U@Pk¬7Þ¡KSž’¬ŽŽÚùl·»Éih›jÖÎêª!À<% pºX¼ýàá|¹Üì¶_~ùÅéÙÙåÍuÉntÝpuq9—›ó~ŠÉf³ÞlTÔZWg‰bÊ1Ŧo=z¨¢1%$#Ã4ù~0ÆLq<¼ÅŠXãˆÌr±L1cñˆ4=RF2¬¼Ûw]ßè ¤Ur‰À¡Xôňw5k­!³‘’e%T‘Ív‹ˆm;;h"UËÍ+±øâ´s¾ijCv}“nvûœ“³¤I××ëœS]ÕZ „„‰ó0MS⦞7Uýýïÿà£þÁ'ÿ_ç—¯X@¬1UÌn»ÞívýdcœH}ÚiTuÊ“ ƒ1cJ3,–GªÌyšÍêÛqØ1gR¶Ê»ë«ëëu·¹v-ï‘’£”xL£Ä^R¦šªºa‚Œ’€Ó~sc’)I8v¾j ­¬·ý~/‘û®{ñù§—/Ÿ÷››©ïT²ª ZPPEQ¬|pÖ[›¼w™yßõ%§H„E•E–¤”ˆÐ‡ã$ÅŒy¸ý"!qþÿ¨z³.K³ã^.ïŒc€ç_}YÖ«›uŒArKÖJNåýVBè pXÉ’Lìfw… 1t977o-‘5Ö;ÇÌ•­Âû¡O)MšÉ½“û?®ÚÉôÞÃ,‡¡ïûjº¨\i–ŒÒèÉã§3ΗWd'DÕååõÛ³óÇï¿xõAóÅç_Å(í´u•5ÖbdÙ÷ýjµ±ÆÞQKK’ªª@˜»]wuqõîôlÒNƾ·•W‘˜â—ßþüøä¾¯(†t}}}ÿñ½Ì *d÷UÛNOb”˜d¸n&Ëãe]û˜Bßu à¼u†‘¾ûþù?ýçÿû¿øÅ/þ·ÿý5€(Úív™eu³2ˆÓé¤ò~Ò¶D¦zeæý¾«ª*³ j=›:kû¾o*7™L­÷!EëìÑÑr³]i4LWç—óÅâÀ}l?ŽÃz³®êéÉòþûOÞSN)%W·9¥Íúfûºm'ÓùæúL瘺®J€¯0V cé‘Q@ëlV>|+‹³S çó™5¨’‰È;? Y„(eö€¥Ò,ÑlU,’û,SÜmºÝ~‹ªŠpk¥À±ïw»=’…ÈèÝ8朸ñ•s΄œc Þg‘|¡`ÍâäQ[Oï?øà‡ßÿµ®_=ÿöËmw9`µ hÝâ¨mëZRJi@Ôû­‘1 œ9n×ï¾ý"Ç *=Z0«,¦“ÚŽç‘TŒ!´Í‚¨èˆºý•'mM*Þâ^²ä”SfN‰P(¸I3_Ì/Nßì6ͺ1 cã›Ú9ŽÁÄ~Ì1 §g =çq8žOOsDê»}ÎjŒ€7§¯Vën~ôpØ®s[ÔÀÆXkT%çT¶áøøh2™\\\„0 3‚†¸;¿èTr]yU‘"~úÞØ kf–ÂZo·U3y²|°Ù»n˜Ü·œ9§€ E¦2¹§«õêèÞ#c©žœ,—,aµÞ/O¦íô;¼ÿìÉ£vC×wýñññòd鬋c4D•sTæÛe-– 0 Ç÷N>úÞwß×þæë/\íK]Óè¼1X¯6òüÕÅßûØV¶ª+$|üäɽ{æ‹ã嫯ßþìóoÿåïÿ«ýÎgOß{xr²øòƒ* 瘒)ÀjµþÃÿ𯶻f2™ŸÜ{ùò¹‚öÛa»ëæÓé{ÏžYk†aˆ1æ,Þ{QñÞãxØüPm|=­Ûõfuyu½Ý÷Ÿý¨~ÿƒ®×›KþÊ Ã`Ç‹ývÿðÁã飩!³Ý¯«¦}øèé>ýá“'O/nnÞ_ 1ŸÔÖÖ…ïEäEå¶PkšÚ#|HSSA¼–+fUWÖ[ ¦D¡ª¼÷vÒNSˆ’EHkc}ÓXç3çÝ~7ÛϪª%k¬ˆ !öý8ôC±%6Žsæ¬]ˆ à5¾êÆ@!<ûð½ó— ¤Þ‹„H´O±Œø$GºÐm›‡Ï>þî÷jòôɇOŸýazýšCH„C0Fí¤©'ÕÌ€ÖJ ¤ yÃð®¯ëÚbR0mSÊ0ÆÍfòØÃ¾ëÆqÇ>çœ9#24#‹XcLå&ÆÖ€°Ý­·}ç½­ëÆ8âú~ $K^”‰úUBc½?ÈæÌÅÙ„Êz[Y•zA;Ý®üöéï2Ú¿K/Ñ{sèÀ0vݾiü|±@ÑÕÍ2$f.A&$0†)O–M4Æð¡âV!4ªôËØ>b©å ‹ì¨ü©(a¡µTMS¿ÿÞ³¶ª_¿yC–~ù+by—•ÇÉ åœ‘ŠÕ5H};iONN¦Ó©ªŽÃ°Ù¬ ËÌa"-¸Ú;ì¡”C,÷!C˜B,#ͪ™,OîÿÆßúÍßúß|þÍ;ý7$,)ecL /•'Я:lË›™3§¦©‰\>¢)ÍD ,4Ù÷;4¦mš‹wfØÆûËëZL'Žt¿½¾¾<çœË˜ÝÕ±Uí+Îi¿»Y_^Xe^L—‹ÉlR¿øæÛÕ¶ïcîºnÇÊâlRß{ðè“?A€?þÿžSr®®|­*ÓùLD*­­3)Nã0€s„d-ŠÈf»Í™¯W7ÌØN™ñ¿ÿÿ‡ùòÁ¿ú?~ÿ_~ûŤñcdÍ…Äp¸cgi:iwuƱ­«Ù|Ñí÷ã0 “zøD5ôûÕ[ÛÔn:™Õ¾~ûæWÖ«0 ŽaÈijbˆ’¹®*–²‚ r‹sFDP€ÕvÿW?ûEÆ~Ìg"ì,©hŠÙ€ÉÚÜl8&ÓÎ[b煮Ǻn§óúïü7çÞÉý‹‹·šÃÅÅé£ÇO*ï‚ Â<›Ì6)^¯ÖÖÖÇìW/Þ}÷ã?üîß¼»åe:ÞðÀ{/ÂåGÞ¨Mb(’€¾ï™¹òÞÍ&Ó”ÂÅÕ^ÉýüóÏ«¶=}w¾éwCʹvõt2KIW«u·ëàââj³Ý.ŽOž¼÷þt64“ÙòXÜÕj½éÆm¬mÐW³¦õÛ ‚l qµò> ‡‚¬ŒÄræËœTÁ– s‘ÒöÐR¿@åk–$Ù8ë|¥ˆÌ<ƸÝí& UÝ<~òôôÝÛ˜ d昢Û”`V×õ¢¥í@óùr¶8Úî·ïÞ½ ¡o Âá(кrÎB[Y‡†â¸?{÷r»Ù}üÏÎïŸüäÇÏÏN¿úí“'O¼óo_¾Èã6)—­fEõÞôÃ~µYo¶«ÐÎ(ÖXUSOŽŽOÏVß¾8Ý ûúnèH8ýAXØYÓÔMЉEÄÚ¦nÂƘ3KL‰A¢XÂܚƉª¦TT€D²*î‡Ñe°Î‚2V½ãó!’uo ÅÁ™YÐAÁmôåγ­ÊE–]6)Ìm¶£lDøfµ2dÚ¦åÙLYú®/Çß9Ö;WJ6æ= ëÔ)Hƒ»¼à­sK>Æ”r¾““"âf½Ùîö€ œAÖöÏDD´äJËgxx“YcáV3SHxP7õÑòh2›cÃ0\__‡18ç°(÷D¨<켫oí(‘œõMÓ”ˆqñÉIÊm;›M'1…˜G$%"tÆ*YkA•…™õ—ß[=L‚µÖZÊ婦‡œ>6¸U%¦*ãEÀûúÍÙ«WçYU›¦1(ëõÍv¿›LK„@YóüxòðÉUM!Ì»Ç'ËÝfeé½n¿ú«¿þq×gAc¬k›¦òµ³¤Â×—W—çïÎÎ8„môru ‡l J¿ÛCUí«ªRÕ$l+^°Ìy½ÞM§÷þ§ÿùñ¾B•òþáúÕ·?û›ÇÔ§œÊúI!îfÎÉZ+¬7××ã¼÷Ïž=›Ïç?ÿÙÏŠ<6Æ`1Æ–x,‹Ä”w•÷„øôéÓ§ï¿ß,&sä&ÇàÌüè(±Â˜ÁT1 ‰W÷OÞŸ-&“Iûáw¿3^_^¾øÂ’;9>‘<ËÕõõØíŸ=}²XÌ*oï<'J½U<®ZôŠ€¦¨X5qR~syýôéUÜn÷®®½sž=«Ú¶ïºÅr¹]_]^¬?:ZžŸž¿yý¶²ÖZWù&Z;iƒ´Ùnnnnf“i‰P>®h9sT$eVP+Dh¬C2Æzë«élê½'ç\íLU °¯=qÊ`кâŠÎꜫªšE fPÑÌ)ÆèKvMÄ”R©iêï|øÑ¦»ùÉ­¯²sVYPÑVD-¡óN Æ0^_ž­nn®Îß=ÿú§)Ûͪ\ž¾‡ñêòœT‘lŒ¡®›‡.ç‚ùìò|³¾»>§DH€’ÈT~â­±Ö4³™ŸN«<ëûîHųº¾JyÌ)"³ÄKâ[8ï÷[E2c‹óÈVH%)‰ 1†„`ˆ]i¢T©ÈAÙ(°C¸ïáÝò("‰0éaýг©ÀuI%«âaÍŠo—ƒn;0¢9sŒá]î?˜/ŽpÇý~ïœ)¥ŠŠ—Ç.‚‚¢Š(6Bncìú«Kƒ¥¿û-3÷}¿ÝnrÎ%R^(7åÂÁ,!Efª«ª@á°G^· ¢¬±dàˆèh¾8šÏ¬51Ä«««ÝjSijŠjle­å”ï\w%ÝÏÌH@·™®q bžÈ€ðÍåkF曫MúÊ£³NXР±å ·Ÿ àmÌ8ã|c,™µ¦=øÒ²²æ¬*1†›››nCPL¥v¨Œ”…»Õj…ª•sªÚwröö+!ÀÚ»¶ö–c·YwûÝn·]­¶}Ì‚ÆC;gŒÒ»ÓŸÿâ‹Ý~;ô îö»aÉÐÍz†Œµ%Ç0Hf»7’HÞÃl¾ðÞ :‹ˆ„úýþÿùÓ¿þ«Þÿ³O¾¿¾¹á”ÔzW‚Cw É,@9徆qtµÏ1]\\Ä”˜Å ?Š29KL Yïa6›ýú¯úàÑûÍìɪ«<ýNÓš§<‹`ª.n61Ž÷Ž—ÞYú°]¯_|óM¸|3lÎÆa=›ºYs"* ¸{Jþ IDATÂr»©nÀg­­ëj¿ßÝMûK»¸-·}Na%EKÞG¶•µPÝ¬×«Íæé“'ð‹!MÛé¾ïâ˜/.¯­o†ðüÿøÏû®¿rl­|ðÁÓãåñ›7§ÖxïòÙÛÓÕúæúæ&r®luzönßw›M÷ÏþÉ÷OÿÉ?»¹¼þ³Ÿþ'“ÐŦ©Ê-µnê¦ii:­×ë²QºÝn‡q¸ÿøÁã÷žÌæ³£ÅqÛLBJÆÚv2‰cb没fADĬ1òËçßžŸ¿«ªöøzõƒßúÝ?ü`¿ë—ËeS·Ï¿ýv½Ùl·;K„ ¢j­]̵÷…Òáœ+óV[ /Bc²@ÆY2ÎUUÓN§Ó©qƘ¬wËù²Œ?»~BgH„S 9Ën·‹1‚¨%CXÔ­¨¬aU¥”v êœWaIááÉñoþÆßz÷åŸW­wÖÂÝ € [_DE•¦”8®VWÁZJcG€Wçïr̤ê½øèÉG}ÔõýùÅÅõÍÕêæ|³º"=¸ š¦Ñ”8Ű߭b×»ñ>sBë,3Š&ïæÂ®) Y"`f)írTL)zkU8Ǩ! 1eïüaƒŠ…œsÆZfÉ%shz„Ÿt7ë¾KgK9q¹ ¾„™U R9†@ K@U˜ùÖs@!¦Õjã«Ú:»\5u=†q³]ªuѲÀY®·%–ŽHDÝ-NÈYËÌÃ0Xk­¡_ÍX2I“1Æ;'ÂØKzV~…»‹DB_¶ÂîúKa @™'áétÒNÚÙlæ¬ ãxuyuqvîÉX¤1E™Î޾ûÉ'ã0¾}ùªDƒ‹…Y-B4o·›82T‘'€±ß¿{ýüôÝëçŸÞí{+ìÈ•µ…»wÖ/O_é}•çe±´u]O§mUU×××%YY8g÷]7¦˜UM9…a/yß6±ªÜúú: 0j`@ÚYsqzj-½»¸(ó-`Ì1¥1YKN#(:ëAµø‘ M]ÕÆ1ÖW•ó5 j޵?YÌæÍßÿÛ¿;›Ôñ“ŸþÖoÿÎë×/ÿëŸþéÙõuÃ뛯ü±ÛëÓ?ÿÓ+µÞVÞ¡gŽ9'sÐ캒÷ï‡cªê ^­n†®1úÊ˲0 ë|2AÄ««›1$¾™T'ÇöãÞºíõlúÑ{ï?{z/ª¼ÛôëaÌÌMÛ²ò¾ëß¾yÍ,©Û#Æ«Ëw¹»ùøã÷ïO­ÑífSzqû}¿Ûu•oöCï«SÊ·®ÃEª\þÊ0/’D$}wöêëo¾•ÞÿÈWÖy—RD"A­'m=i–G9gŽ<ìûøôôŒõfÒ.Þ½½\ßlfÓúÓ~`Bd|óölµÞ!{ï§Ó Ÿ½yýòÛ”å_ü‹Wøoÿ ª=+?zô`2iË{r»Û†™Ù9Ï,»ÝŽ™'³™(ä˜X¸7Ÿ~úI·ÛKNµqÓ¦UDUb†q©Ûwã8zçŽK"3ŒaÛí­¡¶mßžî»îwþîßóêÝòèÞ´´uí¬sÞY¨RSJ1޵¯ÚIË,Ã0ò¡ÄâF%"c<™P¤Q„®rMÛLg“ÅbÞ´MŒÑtûº®/RŒ]ŒCÄ”s¹”3çÌ9ÆP’!Öë+ˆ!ªˆ&Nc`ÕÆaÝnϼ¿8{ ’i ÷ƒ¡P@…¡fR“UdV !„a4€ÌœXsÊÅÆÙuÝÙÙé¾Û_œí¶×C·u¶xosΫ¨ÖVÌ™@Ò8vûþ°gPU»¡Ï9h±kUõðjÝÂO8ËÝ¡9—Ƹ!Sy¯ ¨pG39¬”A" 3å…«¼ˆÈÁ†¡ˆ1•íSz×™3dÈ¥‘’EtÎÞÍ– ˜…à(\ò)„ †èèhq¼8’…nwÛǘk¡ÖRy½‹B¯ü‹PÁmÙ^ª*)•»ì{IRKûåaØ[Hw`½Kæ8ç@åN Â tèõdεmŽš¶PÙowçç›ÕZ2»ºĶm­³óùüèhùbýR€ÕqEŽ \þâív›snëÚ9‹ªbS§¨– €!CX@‰À9 3ßæôo¡:€éàr#cM¹·YcËŠ™K)$mj_UõÍÍn³Ùö!2îLf³››‹€HÅ€sÆ$΢ZP6Å$b¬¯ÚiU9$Œ!Äœ¯$(pŠ¢¢ D(Â!Æòi`·ïëÊY“‡ýŽEXA$yÂç9§¿ùòg(RþôG]ß…¬«|åšÊÎÚº2¾©›ÊWHØwÝ|>ùê›}·ÉUUy_’r2«°µv6Ÿ§wÛ]J±®ë;r–ˆÔ•ôèÑ8ëêÕfs¬Ø;çjoÓ8†õÍå×_}zúÆ5u@ºsTÍ)4ß¾þz·^ÿèG? ¬]OÏ_½ÿàèäxæ½Ùïv»}ßV•ˆaå=‘©*×wcJ)§2,ˆ7oŒÔœ3çl,VÎ"ZD"Kc ݾûü›/ß{ïé‘_²äª<ûàÙìhÑukcí~×ÍÚYïÆ/Ï_}õíétqïèøÀæœU©®Ú«« Q¥aHÓÙуsv1Ÿ6u}qqùöÍËœ‘^¿{áWÈÆ}$*œóÕÕÕz³²¦ªkï}ñæfus³rÞç” ‰ åÚÀ“'osÚmûqè+šôà »}7Œq q»Û[2‚8Ž#‘ÙívÅ6á<)Ä¿þÉŸtCXbŒ¹ë»ªi>||úæ NAtG@<¹òâÅ+!Cæhq´X,Ue"³X$$k¼÷UUWMÝLêåñl¾XÁv»Å´­ãÕuŽ1¦¬¢Î:"ÜwûòCžb²d‹ß¹4Μ³)æ"Úƒ˜r?†Í~?rîû½1ÈÂF)¥äJ“PA™ÁXrÞzˆØTõ`Ç0Æ¢VAL9ILy»Ým·›qìšÆŸ,C[ §œra nw;klÛN­µ*ã˜Y!¦˜8‹JL)¤¨çï|eÉda²DÎzç QCGÀB …¢Q-lFFC„Æ6bD@4åÄ™Ä • òÓ¶HñÂ@å*PàQ ùÆ6 Ê’óFsVec †ŸŒå㆘Ê:ÆÑìŒA3iÛùtÊ÷ï…s–õj•b„²à –ÌŽ+bÐ3ǔԻª ŠKÝTÎgD0„*Zà×¥ÃtÈØÈÝ>”QÛ¶HX”eu¶$`bNd€ G‹Å|>Ÿ–ÿf½Þ®wÝv Æ$˜/ç=nšúÕ›ÓŸþä'C7pJ E^`+×ó"¢ÌŒE­‚€¢A&u–J¦tuîdwõ;³”~3+Ó,2Ƥ›m–|·hm±šâMžMkçêÙb9ŽCèC¯×`–ƒV!Va0„Èc )3Z棊zÞ«‚!„˜^'“ét:­½_.æ1„Íf•%#ªfïÑWW`ŒiAgó5Y2š¹pœ³ 2ŽCŒ¡|ÉÆµŽEC …Úc¬9>9éºaF€l®¬3¨wv6™4uãÛÉqw|usµïvëõÍf»:99yóæÕùͻӳW³Å2‹PUƒoÑ5›íúòìUÛ§¾ýü§œÃËo~öýž/Z^­¶ãŒYT±ªjÄ Ñ”-P±Îjb=\µ ÕÕÍý{÷&“FU¬5÷<|ðÞ¾;ÒøÙô×RìñòÞGßýäþÉÒû\Õ²~ñÕ×_|õ šÉf½ýËŸüõr±8>šùªB%ã3GçëÙ|rvñVA&mµ§TוóéÑ£GÎB ay4¿ÿàñÏñÅn;Äíþìúrƒ%KdÇa”Á{ï €T¾zpÿdºX.–KCrا«!tï÷~ïÃ÷¿kÈü§?ú£ÕÍåÙÛvž¤rÎX ªç›íF¤Dà,¨®7›³‹+f2d„Lù¯ÆzgQ¡¼8Y¶óºßíg³©Q<»¼â1¨ª²ÄrÎã0-fŽÌz³ï|4céX9²,¨R¬ ˜¾ýâëÿúgvd8¦PŽ¿ÚW†(sVV–ܽ¯¼¯*çlÓ´MÕú¹‡9! ‹¢1ÆxcŒ1Bßí·³Ùd1Ÿîvëíf==Ĉ0Å8ˆˆ@Ñô ¡‚)N) è¡,-äqãlA‡#:ks*R¸Ïí¤išÆ9[jÉCT™™-+/ ÀÌ™À"ÂAžË|€ª´ë¶äI€!@d4è|Ý΋Å|Úûýé›×ýnSr8‚ ŠXÔÌÞ»Br!*hUU™»~ 2„tïÞqÕTY¥ë†1PŒ!ÄÛ,E@<3DráÞ!*UË‚Ü~)¿l€–Ôò (ìßÛç‹wù+DD²€ª14i›é´mÛº‡÷»~»Þ¦1!’ª¢u¦n*_ùÍv»Y¯àÛ4DeI ‘¥l}e$òÖ–$ÌRä@jJANTš^¨ã½¿ƒ¿3ËíÅêºöÞë¬÷„¸Ùï]åɸBçE` R€Ùl6™-]yY4Ð̦3*)ôèœ%2ÞXg¬(‡0†±Ïq8\ž µ©ìà²âa‡³*:g_Íg“ÇŸ6ͤÀû£ MÝT•3ʬÊYE­1Eo^4FªBe"Rº²Ã0Œc?Ž8„~:Dª}e­ñΨhNéÃ÷ž~öÙgù—¹ßïž=y:Žq·YœrQqbÊ9ç$)5“Õ‹WÛµ€nv;_U'÷—7/Q4ìÇ,<…Åöæõv”m×­¯Î½Ñ‹þ¦oª”çþøÃû~ðìòâòâòº®«¦n¦ó¹užÈ²ãLR庭&¦Ý1¨*D` ¤”ÚúèÁý{mÛtÝ~ÆãåòÑ£ÇoÞž…†a8¿¸xøþG¾n/¯®.ß¼ø[Ÿ}lœ;»¼øüëoàãï~èÜäüü,ýòçÓé$¤4]®¯^½;}ñòëªö_}óEÝø¶²Ïž>É‹Åüᣇó£% >ÈùúzõòÅ‹a2kë¼–TôÛo¾UÙlêŒuÖ;_DPÍ"š5 aøÞûOgGo^¿{óö ®.$gÛø¶nÒ‚H»éd2›M§Óél¾øÎ÷>mÚéÅÅéÍfóì½Ù÷~ð™1þ/þâÇóI[Wn1Ÿ¼}ÈsÖZKÆpN :ޱ˜WU¥­μÙõÖy2'ÝAûÊP-åÐãCuÿÞ=r„ r嬿¤¢i:•a`±XÎfscíf½AhsJÃ8BLâ°ØÎŒ1†Z;Í=y6[¶ù_>~ûùKŽ¥IÄÆ Ø¢+Œ””nš†Èä,@€=jÛI· !¢)KÞw{cm;–Ï¿išIÛrNûý>†aGC™3"6MÕ4•ˆDg̤m½÷’åô)joDD¬k?›OrN…ÖP–6KÒK÷HD+k€H­5h ˆôV ÈÂñR„GKèœc¦óåd±|øø©18îöºG]’nV+@hêÖÊ9… ÐZÓ4Ín·3Ʊà ÂɘQDd6ŸÖUÝ÷c]7H¸j@d HclUÛr~ß•º‡Qä]´P_Œ))ä[Ãä!kXžÀ‘(P!9CÖ”¼÷GG‹÷ï/—ûýîòújµZív{ÎRùª­êÂDÄý¶ëºç›íŽ³Þ¡ÁÈœËóõŽÄ€@Ƹ [¤Y„c(}[cn¯-ªå”Ï93Ç_E/ˆr®«é³÷ž½xñb a sU (Å,Ld'Óy3™>þªßwÓ¶qØ®®ž>}ôþÓ§Ýöfss  DàœiêŠêºÚï÷¹ô¾H:S²DTxKÆX˜Ïf÷–Ëåbv4_N&³Óóó¢äu‡p-#cÀ eN’UÑdX’r`NF„¯¯/†¡G4†, ‚÷~rtÔ6þúú²Ûï­uÿíïýýßøÏ^¿xq}uù—ñgÌr@ï|QõÞî£AÎyÌrÆÈ\7M;™^^^"ГG÷ÀÔ›Í~Q7ÂÆÙï*“ ŒÝî×?ùøŸ|ïåËëíšsúò‹¯@ÅYëŒsÎ9ësæã““'Ož~ûí+$Ê1¹Ê>yú^¼x=Ž!«—1 `¦³‰sVDú®1öûþË/¾Ò‚k%óý_ûuÓLË£º²œãv·4õëwgÎUÿøýÃ?úäþŸÇ)T¾žOg=Ù÷áz;¼~{öüÕ‹·ï^pŽÛíêþÉ¢ml·ß¨ä‡ÙªJœKÑs|²œ´ÍååU;!™ŽwÛ]ÇÛU,¸EøåóË+ga¾°1Ư¾þòÓþúã'O?¸¼úÎw?¬+Oh¾üâËÅtú£ßýíf2Ùuãlq,¢Îúäíé)ª,Ç3p'Ëûm;ÙU›íöf»ßª÷ΈVÞXk˜³¨ €ÁÇ–IÛ.ŽÜx}³:½8w-N 2@T9¦¢©¡6u:©¹¶û]·ÛqJ¢*œ…p¤œ“s®ª\Û6ØN¼óÝ0Œã ¾n§*]'‰A@SN1ƶÌfóRáÅ(ÆXÈDÌ ¢"BÖ¨¡Žý0Žc)/5J¨ê:³ˆ¨1†9çœ"¡'@œ“5T¸È¹+¾ö̹´GˆL À @ù9óÞWÖLK©a“e•˜0eUåœSJÙ@ÆVÞYk@ÑL¬5¬93s–G&)jÌé,ô|±¼÷èÉ÷¾ÿén»Û\_]Ÿ÷ÝîñÃûó¿óÅç_ôýpˆ-ZkE9ÆÔí/ÀW5†cL*E~và| ãà+¯óÅœ'LH»íÖ{_òŽì¬!#"1Eï*k,ªÆ ™‚á;Ü®à0ÜÆX1{ bH‰8ëñ·´h¬1ÖÒòh¹\žÍç̼ZoÞž‡sÊÎZç,4„ªcwû¢yšÎUUãÐwµ¦òUAæÔucS梨C†¨"@.cÝrÉÈ€p+nE44ÆcT’7-/tAȘÏŸ!hÎ ÓéÔ”ãÌacÊŠwþÓþðÅ×ßö»UNãØw×—WËÅ|»Ù„1Bˆ€)Fï1¦m›˜Ræ ,€ *`ŒA²@%}ˆÆXc ‘Ùu}æl]ãªöââjŒƒsž5§œ5† ¥Tn“ªBD¥$)§”ã~·÷n¶˜Á„¤€ÑZqÖY2MÝ–…³¶i«>ÿü‹¿ú«¿vÞ¥²83‘iÚª”#„ª•¯¬µÓù¢jÛÿõ_­×kwè•׎h¿²ÀõåYßXͦí£ûG÷!Diw}y–S ã¸]ok_-óébŠˆÆY$Ë"™¹n[­›fŒã´®Žb ˆâhTC®iÛI;/ŠØã»‹sTlÛQˆáòòj‡G÷Ü\_™åÒ;ŸÍ×ÛíâèøÙž]\~þÕ‹?ûóûºzòøñâxé›fˆÜuýÅåéÙé«››‹û÷Nój:GüÛ7ý~ßå]Ç*Æç|S7ÎkM Á9W¤”bŒUUU®b攢Üv€œG£ `¬)þëý¯ž<{ß9?iÚaè_<¹Zoœ±gçÇ÷ï5“ùã0Œ"jLU×M…‰¿úòÇ÷C§™ $ÇѲdÈZ4Æ–~b‰ð!¢"^]ߌcšLZµÆ U{-ZKF˜ã0"+pæ0ÑÔÛÕjussDc´©*C&å,9sNÌÙ:ÛÖ­·®\Û‡aDD4$·@ç”"ˆc=zðª®9‡»ô‚° ’5ænn®Š9qÊ|]êéÙÛBwP$rÖ‘ÏfCßÃhKÜD¥øÀTTäàGb"ã¬3D–0s9ŒØ6轤ÕjSDD ʜ˽±žùAo…* ª l›ŠUj‰â(ƒ a™4朳ƚÌùnO‡ˆ”°Y,O=ùþ§Ÿv]÷ú'ýf[¨ž›Ú}úé'•wó7?óÎ[¢ÕxÃ9crÊ™¥¬«å,1D­*oÈb!ÅÐõ䪪Læ‹9UØxß6‡rx·ïBJ j+Ö"glFNÂ(Tà[‡|ýÁ~w+o*9N4Æðáú„*ZÙY¤ÈyKß–ëÊ·­o§Óùò˜ûq\­×gçÌ⬣»æ•1*šsVº5.—Ë{'÷†¡ÿ⋟‹”â ˆ0Æ¡LßJŸQTŒµÖ»‹j½³– ©†”rN ‡ÕGJ)§!²6'¾ã •¥àív»Ým¼¯|ÕXcSJ9&tN„UEK¶ Ó8ÆÐ#dD©ÛÚzûæõåé•,€ªE9çË…D•P pŠrä6Wz oòØ«®Ö|vySùªˆô OWU,"€Hf"ePá¬*ˆ@ ³Ùt2™-þÁ?øÇÿþßÿÁë·=Ù‚³f‘Ín³vœ1þ›û)†ºi³ÞûaT‘³ôýP–X8s¢Àüîì22¿zõÖ9/te¥ù|8·ÓI?ŒD­óÞ8Į̈Ûíή®êfòñÇßï»n:™ŽãÐÔ53 @UU1„«ë믾ýf»ß !圇qØl¶1†qÀ8ë½kšæèøh:›[k³êC? aŒËããùüh¹<:;7ŒÃ«çÏû1}øÉËÛ“ÈÞ»·DS]\­ÿßÿô_vûýòäÞû|çôíµØN'ëívµÞmÖ«~¿N1¬VW“Ö¼ÿÁ“‡Ç'–š¯¿y³Yon6»ÌBí¤c°ÞC@ÀédÚN'Î9BŒ)ÂYHDc2Æúª¶ÆX#Ö*ªÈòäd·ßæÌ€T¹úɳg¿õÛ¿½ÝlCÊ1ó¸ÙdÁr6•Ë*¢bE£¢ùüâMÓ´“fRµmééY$^Œ†ªbKÙ R ›Íæf³aCF$Ûòã *†È©2çú>ì}h7×ëë«U×÷¬Ù’QŽÎšº™¤,ˆBR€Ê»²Ñn Qia4°[çœ|e«ªƒûnûöõËC4¨ÌôDÄ(@ÀCÌC scfNE¼«eÅp<4Ž­ æ”RˆL@! 1  lŒ1ˆœ™9±° Q‰…–â]U mkÌòþñùÕÕ‹7o¡3ˆ1’µÆXU1Î8WYDƒ )%9TÍœär&Bð¶¾íÉ賎ª ˆPa€PM&ÍbþáG¾yõ'ÿù1kŽ‘ÈØ¶J1í¶ÛßùÝßmÛÉ~·ŸÏç?ýÉO®.. ©äÀ $aac¬s6„XŽ-VDðÎÍfÓv6™N* c KÝ0„gUæ˜9ÆT¨ˆ…5Šw‹T‡T¨ªJ„‹fý°tèËcñg2Ó6µµäœmÚºmëv>òÞ‡×7o¿øüüâbèû¦i¤Œ\˜sfg¥(£D…ÐÒ­ÿwƳÓÓƒ³ÉÉ9ÆÈ\NvS&o€è½/ßfaUç\QbÝõͱª«Bªªëš™…‘P@9¥[:+€©Lå<* û^÷¨¢Ýn·Ûl÷û-cU9eVëìÙ鉣5˜3“BbUæ,r`BpÙ#²®1Ö’Ð*³b Šä”%}? ‡us*û0”U„3X„3sQÎ@ˆÐ[*3ÉÕæòþð_ßÜ\³æ³‚G 1K샵Ö"¥û¾¯üÞu½ª"’±¬uDÔ}ß÷ëݶªÝí»ý>¦d+o8çœ :ƒhjÎ2›Íœ÷åûct†6«•*ä”XÔWÕñ½{÷îÝïºnßuYôþýÛÝNåÀ2Š1Vu-YUdÃóoŸ“!gítÚBéx_Yë be2™|ôáGÏŸ¿ˆ)±j]WmÛ³>&‡¸[­*MÇÇ'mÛœ_¬^|ûò“ï}Ò ÃóWoŸ¿x÷ƒ|òÛûwšÙô¯~þ‹/Oß¾9»¹¹2ÎÀg¿þï?{æ Zªšéüfµ$].—îß‹!¦”Ó4öý*•µ@Ø÷ƒŠ°°1¶Lªª¾ººnšv6›ª¸ŸNZë\±ïD4Œµ]×ÕUCÆ! àCëëºÎ1& Jœ‰HH¬µ¾²"™9ƒ! 1Æ»±kšIÓNËÏÖ²¢°ßw!DëlU7ÌzvvýÄ/¦ÍäíÕÅtÙÆ±Ãîüô¢™¶×«msÓÎç³ßûø'G÷ÀÕÕ»·o¯ÎßUÖ*Ê´m–Ëû‹ùýwï^‡q³Úì²hÕØyÓ>{öÌ[âªâ,Ã0…ßýÑoúé¯ýéO~üÍó—ûÝÖ’É™‰¨m'm;t]'"ˆ²˜UÓÉÚvJ)‹€1î£Rˆ9’óÞU5[´BÆZVa)úåJ”T­!»Ýîöû™ !µh-¨ –€Þ.BªªK È;9ìC¥âØ;c½÷Þ9«&îÂ~ÕŸŸ_^œŸï‡œ?˜*UD„Sˆ]·S£ÅX÷]_ìŽMãRÊÃЇ!_9_ù”’f¶'ÇÇ/ЈòaKò !5伯Æq‘?V¬|Í"HªE%\ú$9—úï —S²¶rõ C䫪njBJqÎÞYDH)VMƒÆ âl6sÎíÖëÙtÞÔÊ’c€XT’"Xgµ€DÒÿGÕ›<[ve÷y«ÙÍ9çv¯É>d¢T£¢X$K–X”D…<eMä¡?ËC=v¸‰RX¦$‹%«G5hÙg¾öv§Ù{¯µ<Ø÷txÀ½÷ܽ×úý¾¯äÃЖÁf"¤ŠC2©,Üqë*)ç$ju݇DPje©”ö3“ÂÞW쮩È~»•œÖW×¢ÒÍgý8lw;1«ë2:´Ì…Ù7®ƒ’å Æ;ì<‰ 0S¿ÛŠHÓ6Ó49Ç!"bÇ@¸ÛïПx2xóúLĘýß™¼Ó ®ØþÖ aʌλà} >zO€Dì‚1²stØA?ŒbýzÓÿö7Ÿž-`f±‰±’cÓ4123Õ•lž«šHiÛV¤ Vä¯Õsew±0¢Ví[e[Rýn³zl¯™;”ÂLÍ §""!"9ܨ Z¹cœóŒDžÙ-Çqê‡J…sMÓyêÍòêdu´8ÚïתB„šUÁˆ0x¯¥˜Â|± à¦))€!5.ÄŠÀ=غ˜ëEˆ}ã9„ïÝ»÷âéÓ2öhj E ¡!X2 ‹!‚ùÃu°þü΋ j5©áQ31! rœ3²óï¿ÿÍï}ï¼|þù‡¿øÉ8î겄™É»Šwnšºä`5€à›££ÓRwVR\ðwMÓÄ&ŠQÜ“óÑÀHÕTbŒÎûY×Ñz½AÄÙ|^TRÊE$g‰Áœs)%ïýn¿kÛ¶ßgDŒ±!ªË?Q"ç|¨».&vµ\½Û÷‹a8üÌDÈšfýê5’;==i›YÊr}½ûƒ?ºsëøèé“|ëö­RRîwmîܹûäéÛ7g—ï½û^)Ó›WÏÿè¿y±½Úíúë‹·U¤HѦíž=}Ö5íf½Ýí÷Œ´˜u¹ˆ)‚AßWWW„è˜ïÝ¿ýøÑ{~øáÓÏŸ.Úåã‡_™ÒøöìíÕúêêú:™˜¦iZÌÇÝùùEÊRjÚY?ŒW×WD\D¹ ”BUÔœ³ ˆp¨kBb&ö»ÝÖc7›ß_>ö ™›¦Éy$ϵ["E¦”œóÞ‡/ƒŽYU‹d$»™Ki"bDÜîv&ÖÆ¦á#E}uövÛ÷W›õzw% ¡!FfNiÚn®RžR–a™½–NЦ4yÇj&R›¤ ÄE²IÊã§õçŸ~’ÒrfjEa9›-—G ×—Þ9&ïC¬¨î"Z­.@J\ïÙT¤‚O”bôÎÈ9!Õ"-9ï]pE2xg6e)ð©4º¦[ÌfóÙbÛ\o–Ë#B.Rv»ˆæ4™üA䈈wì\a±˜#Ru:ûèCŒSÞÙÖ‘wìÌLRnblÚ&—)å\®Õs\Êë‹óç_|¡)SlÉûÊ´R•¾OÃÐoûýËW¯KÎ!î"¢á¡åd(ª5§©>6`R¿•¥~ÓŒÃXDq½Þ:Gv´ZŦ½}«=½uÒ÷ƒˆ¥)-Ž* ¥Ô£1²s”sª¥gf¬£D !„à›&4!¢!"Q™J.¥ŒÛýf»ëûQ+#б+R¦”*˾mcÛ¶ØïûúT‘"r€ÞÀ4N>¸ù|€*V¤àÍŸÈ!þN1ÆØ4uYªÉí¡0Ì7¿ˆ(çú}¬v“ºIIÖü{U5aŒ!ƛ轑êxÇ ÀºY|stt¼Ùm×ëË4 1ƒÃ$ÂR‡KõæP/dHa±œ‡®Ebö3#rÌ3ÇÕ²Ö<˜1#…Ùû4¥³7W.„Î!ÕÒ²™‚)A"@0•L* „¢R|F„’“’ãõæª{Uqž‚ HÔýñÿÓÇ=ùôw»ý¾kÕJ¨wÐ}0™;ˆN4¨±©)x€T‡™i÷M3[,WÃ0¤4¦qÀ¶m‹èÙÙYÓ4€èC3p©6$˜ýñÉér¹Úl¶f@ä|33`çÂ4%0¤"êÉ1‡"ˆ„޽™ ¹)•·gLWDè¼'ÄÝv×ãÉ ‹/3f¿ßí>ûäϼ˜uËÅ|«I†a½ÙÇv&põöüÌÀþà~[í’|ñìùîúJŸÓþÎí{ï>|çäøèÉOTJíô›éb±xsvVò§L°Z­0—²Ùî_½~»Ùnכݽ۾ÿ½úöüM?Þõ½*v³hÚµÝjuÔµ~6›sˆEuÇq€œó¾”âØ©)+µßA ¦FØ´q¾˜71:òÍz·3À…õ}zrò4DÉ£‰(s)EoÌtÎ…¶ëþF{5+e7ôndòÞ›©•\¦(y2ÃëÍ>ÉVD ‰«ŒÚtš†”§"6%mZ”úáS],¢Ö÷£jjÛV Æ<õi›†±?¿<ÛoŸýu ƒ×¶¨:vH„&EKÖœKJ9Q=-Ö¬E G.ÔzU5‚c§` ˜a×.JÖÍÙë&8BC$îÈ3(ì×ëË©÷“ëbç¡[eÑ4ŽW—›ÔïC1ÆùláÐqö»Ý~ìÉùnq²<>1“R © "VrSMpŒ¤-Xêó%xx‘jíõ‹4MÓ6‘$M«Åbì{&Ft5…bšÒ„š»9æÛ·NWbssfÉ¥¨¢ÊA˜KžÒ8ŽAD‚÷mÓQÛuSšØqŒu,AD h„Ø4öýÙ–G BF ±Ÿ¦ilb8x-Š”’=;ï}lbÍMúà³:‹ˆ±¹Z_—\‹‘ç‹Eå³#fÓ”sŽwM Áù4å£Õ<„0N©z"Gžƒì€w»!M%ÄàØ j'Õo5­"VçÜl6˹l·Ûi™9ÆX'oI¡ˆ™å\DÔ9*¹É7êí\/T¥ÎÅ›ƒs''Ǧ:öCß÷ÃÐ×G¶÷Ž™ÆaŸÒ”JîÓ6ŠogLsð¡aöû&¸º"ƂȾa×:òss4’à\ÉeR+jb@FS"J>ðŒ¬4¡#P0%cÄ pl[‡eÐÔûHYŠÞ¾s÷ÿäÿë_üùùÛ—ÎÊí“Ûïõ+?üÑ_~ñôYgÞ»YÛ©è˜òÇý6zúÙÏÿf‡6ú’ zWã7¥È! TOõêëH­!19çM­˜x½o›v¶ÆJölBlÛY.ùpCrìÎ..i5_2“crªÖ4]Õ;;b ûý~ßO TsHÁÀÔÀû0ŽÕ}é<¨ê0f0CÓ#3ýàœÛnwHÄM$BÜnö«£)/’Ô\xöâ æ÷øgÏ_ß»sÇGwtz<–²éÇÍn %wNoíñã{÷îØìÞý¶i éÕó·Ó8qåöŸœ¬VË”RJyÌåìj­ÈäógOŽ~ý‹ù|>ôãŸþÉÿéçŸ}þäSѬ’·Ûí‹WÚxÎY|¶—W©”“[wq½^à|>_,—oßžUÉâc3›ÍÕpµZß>½ý“ŸýBÇ¢"Ã8Ý:…j0>?¿Çä‘Íä 8.…‰ ~Âë;VJ4ï}ªàÌt*IU¯wD2$1#¢&Fæctìˆ<“7µiûa4t]7Gæ¡ß£còÌHE5‹RÓ6qˆ°§~¯v»—WgÏ>ýÅ«7Ï—^‰!!1ÑX†ý›>¹éºåñ­Ó“{m3[o·æh±Z©ñ0IÉÙLT‡Ø ‚9¿‡1 ÙýGþÞ?ø'Ÿ~üëa¿ëº³>VïN×ÄȰ½:{ýúIh(6±ŸÆõÅYöS.E-Ó¨ºš/¢÷DÔ¸ˆ®~PríÑÝå­;ëë·£\$Ih@. QûàéÎ×q0†rN—×»)'"B³ÃÛ)ºØeËÞ9q‡åÕ¢?3U  ×.æ³[ÇÇÇÇË&ϾsM:¤R,åT¤ \¤.¯J)ä\ÓµÌDŒ!8&̹ì÷½ªVK_Óž‰Œ˜‘Xó™w\JU‚=;N´"DT«Aw½ÛíúÁ9ï‚_07]>ä¹>>9ç\JF€Ps6D˜S΢CÊã4ªBÓDÇHÄRpJe»Ý‰YŒ‡zUMdy ¹ä4Nµ/6Žcß÷ûý¾Îåë `š¦RJÝÅUKQ}iª–KUE23;çbl¤Tu˜™˜¨)Ó82Òv»Ýl69—¢÷>åéÙËçS‘ÅÑjuzrt|ëäöþœϙ;+â †[ïߺuzuuÝ÷CÑT¸YìúäCׯã£Û÷CôÛnÞŠÇ,éd¶Ä.^ïÖg;Í{‡Y4‹˜ãލ)¢ä\†Í =h ÜrÛüýßÿýÅjõãÿøã§¯„ÃüäT‡ÍÕõÅâíìh~|Õ­7ûáÖý÷NOO>ýäcÀôá‡?ýü³ß¤±÷žUå UGäƒ` Dõ°ÌD@„nÖºäTÍ{„E¤¤\){„Ö63"§ŽãÑ“&FföAEŠIɪr÷öí›û(­n/¯¯‰ƒšû¢ª) ¥ÄY‡ä;€õ‰½#1LyR•ʂƈÐêhÕF}Øl÷ã8Ò|>ßn·Þµûý¾ö¼ûîÑÉÉÕvß÷ÓÏõÑÃÇß<»¸ ÆÇmv—]×möÕZŒ€°X,Üp÷Öí.Æý~¿Gƒy7[-G‹yAx÷Ý1Ö÷@$ç½<{õ’ˆ–«%˜þüw-Ši”?ýçÿb?”§/žMû^5³R > cîèwÞÛîvã0`h"9§‘{ ›í.xcL)k)g¯ßä$M·\oö§Ç·úa m; ¡Q3ÇdU@S¬.ó»®{ïÑ£aLÛíÖÔÉ`uèŠfGDf’ÒB4D•TDj¬‰›Øxï½>„ÊŠÕ"Ó4 ÃsahŸç¹þDuPÆf|ôÞïÆ$9/Úù÷½?yû»_^¼üoô@ ’ÐÄ´õ¾iV+·<~ðþôÝðÛO>~}}õÞûﻈïœwì¼Ë¥ŒÓf)§ív=ã0ʃ÷¿þþãÇwß}ÿââ|>_ˆÈÛ7g×ë £“…ù*ÎOÆáºÖoÞ¼rŽ ¹À>4]×ÍgM mÓ4íÜù€È¥èÕzºv7íÏ®6ÞÏ)ë4í¸64ì;vDÌäèÀ´²”¦Ú„ªÇÉfPEJš˜)MSš†ú6S1=ìÁÍêù´iš|ÅtU+Ël1Ûl6ã8ÕóNUSÎØo泤œ­’н—RÁ1à0¤\ €M©¥ZÖD—¥8v>xï—hPJ©S¼¶išØ¢šöãX£“jPÌRNÆ,jiJä<²Dò¨ºX«¼‰™¹i¢©¦q Žƒw±‰DÜÃ0Mq?FÄ\Jð^Šö»)ưÛHÉëòËqJ•×íhßïs.³ÙŒ‰J.’ ˜•œõ&ÖùåZXDÔ”Ù»z qÎ5M FRdœÆ”rA‹T V%p…êk—KI)#¹¦[Þøxy´Èÿðýó_5ó³ÿxòÄ ¶ž·Îu³ããý8"ÑüäÎ×ï>`r×»½sîÖ½Û«åÑj¶œŽÞ<8zðN¤é;ø{Ç÷Žî¹Õ­ÓÅùzýü‹ÏµL¶8ZaðW›íf¿kÛv÷‹ùòòzýìå `œÏgËå²¢ŠjÞ¯¶gDdJYDˆ@$§” Ü_üåŸß»û¡cϤbƒÄEí‹g/c×.³7I÷}ÏŽ—‹¹n÷»ó‹ C¸{÷~»èB†~¯bgowûýOõaìNœ‹1¸Ù¼ÍSÿÙ«7Ÿ}þôúòŒ4™‰©@åí÷ûçÏž©¡šÕìb¥ dÙ¹RŠhU3S ¡žÎ Ð>ï}x‘‰¥a”RRÎ¥:gˆ•4é½'&ÇÔ5-£#dQ)²óÁÅÂ0v±‰æ³Y7;¾WÁŠp(³k)YˆÙ‡èb(@pŸÊ§O_î ÝôÁéƒm׬Žn/æGMŒèˆœ¯Íõ2 ‚ªûݰ˘‹ùÖ·‹—oÏû~ð>¼ÿÕ¯#ÈŸý.OYв*£¹åüTÔNóy󵯼Ÿ¥ø¶içíÑÑqÓÎr1ÈE§~z{þú‹Ï~÷Íoûx±|õôógO?Uö÷û½šq‡tXøUbÆaÄB(R9€"²Ýì†~SÉÂXÁÜ*"䨞°¨2tÎm¶»Ýn¿zÉ9Oý0¨šc1aJ ‰ ª9J‘T²AÕI³A3ç LSfvõÙmGï+ŽæPb>¬Œ+€ ‚ójÐ÷SM4N9§<±cT5Xà ўÙ˜©DÒ¦œcŒÝ¬sÞ£™–â˜HVÇ«ÙbyqyîƒwÎ!Q!çr}µNSJibÆ誄3H9©jˆþîý;Ó8žŸŸW5Ê­Ó[Ltqy>ôcýZ¢× ÷nÖµ)%QçªTïÊ8Ž¥HÛb%Ç9ï©”R7Ý7åRÌEj†g±8YoöggÛõvšÏ—÷}ë÷vCÿÕGwæóÅêødè§Ë‹Ë§Ï_k—'Fã;Þiçó7¯ÞÌ–·îÞ½ÅÞ­Çér{æ7ýåX¯ž7ÿã?ûÎûß}gÄú-T°8B.º4eZ?r[`ØïeÜ›B¿ÛÇM?Îb÷Î㯼÷ÞWž~þlÜOwïžÒâøöWÿÑû_××ÿÛÿú¿\¿’ãŠ"fÎ%›;ÀSÎj)÷Z»cÀLrVÑR 5M7Mé?þÇÿçÎÝ;MÛd•œ )¹dïù;ï[ï?zD.æ\²âfÓÿù~hB €ú~dçæ]sr|ôÛß~ôÃý¨éæš;›ùã÷ß¿º¾~þâ…(†Øò8ô"kÀ@úqÊÅbbÓ†‹Òbyk¾Zf™º£Ùòh) ©`ÛvÍLßyôh7•¯ðµÇßùü³ß,Wž`}q¶½º$‘—¯^~ñùG ¦E\e-zcÌ9o·Ûõ~kŽ«ª7ÆØ÷}u²«*1ašÄÁcJy½>ÿÓ?ù§„ú_~øŸ¦qW­îCÊÿîßÿ‡¶kë6e¿ÛÞ:þþŸ|ÿþÃwØG×t>„ý˜¼së×ç];‹!ýðé§Ÿþ—ýd3*DÏMtMÛìûI5ír¿÷Xcä¼WS5Ýï÷DlX .TÑÝH(%˜›¦‰™½su£ÃÈ‚šsþ²¤NL•¿WJ3͵èßø¶ó(]‘œ±›Í™§R²(:„Є0k"T0c×-OŒ¢Ö½Ž!r®LZ\}l-4ðl»V~ûÕ¯}ýÎ;ï²÷ìh¦a¼@F 8ºBü²AIí"(öûᢛϵqv½¾þæ7¿õÞ»?ÿüó¡ßÞ½u:î÷wïÜÞ­ßÞ½}|ööüÙÓ''Ëwß¹ûèû——‚ÀmÚE»\ÍÌå’DéøtõàżŒÉ²^ž_%é‹YI©ñPÑù"f`ÈÔ´mΩfZèà7!ºQ׳Š2apNU%f&w}èßùU3Ô9åœ2"˜¨ªTŠ:G€0¥‰³üšZEÒfpƒU0CŒq'­Í¬šv7U»Q¨×.åáþ``*jàØÑÍÇXU‹Fª™<€ªg2³Æ‡àØG/¦"…™§´>€ÙrÖµ10bNiì{DÌ)Õì£i9{{¶Ûí7ëýõz³ßí‰8p$$"WÇQDbG ÀÆÁÏÞ{øµÐÅ>§)¥§/žß:]ýëÿé_ãkï‰÷€‰|Êøòõú¯~ü‹—ç{tÑÇö[ßù½wßyo¾èöýæî½#+úêífB7_ºôÝ£#çÒhÚTq9(€€&£—ùåEî ¹õv·^_ç>ã8å}@ .¬«Õñ‰•¤¥ïSȽ÷õ¯¯7›ŸüäçO^¼ºÿàahÛ'¿ùÕ›·¯(UÖ§Ÿ}6Œ"»¶í,ÀÑÉéwÿðúaúÕ/~uñFATJ×FïýŒ]#ʪZ¯MÓžžÜ.¥¼=[«])1ÕZx n}óëfKZvžˆw/^½½¸ÞôzïOŽNK‘Ýîj³Þ4Þ;GfØ´³*03¥OŸ<Û÷}QtŠjL¡óFX’i±z`B§Z.®×ëͶvýš¦}ôþ×Û£ùÛË׋[w¹6Φ mÇ@tÿ‡ó³ŸÊnsu}yqÔÐæúêêüíûË¿øÕÏ~HHÍ—ËUˆ‘™RʛݎBÛ¥Ëõn·›Ïæ]¼÷C?ª;‡„Ì®²ƒþVsJW—gŽI ¦R´ù|~rr«mZ5ìÇ1—|tr²]¿E™N—K×-î=z_ûa8Z唼 Räâìâj=~ï{üøk߸wû47$½uûþOž}ôÛß½|öñG¿ü±•dÕŽˆ@M Þ»qšªb§fÌ fƒ€ÐÕv&3«šiÚND$—\2»˜Ô«TaïæónqzÒ.–È®!Æp½s–ívÛ÷»õfMã4䔑(¶M%¶\^\={ua® mGóx…ö·Þ5b§ä»åññí‡;ßvï½ÿî½÷bÛš”4åÜX&ŠìßDïùê#™•´½:¿¾:²ï|ëƒÇïþÁÇ¿ûH Öç/$ßøÆÞy,åÖñâ“ßöççã°?9YÎfín»ùÍo®<»Ðu ¹”ŠOÙ;ô!¶Î#`7ë–Ëy@·½Ú,–·fó#Ù‹Z1àú¬<9=õ.¼9K‡A°š™ŠÖx3¹Ô¿Ã…?À¹ÜZЛ4FÅðæœ/Î/éd¿‰yßä@¦dH ª 0ÚTC«}0U00)EµˆdÑ\´ˆæRJB 1ònœsÞ©®[D§œë¤†CÐl$@„C>7MÉDˆkÇJTÕyÈu,[@]€êÆcY-—³¦½}zBi{uy9ŽSßWp9NÓ4¥©”«ëõnß{ÑÄêªNÒëTÝ{o›ÍFJQ3"*%]\ì§”¦q¨»"”›|ýf“‡¡O9×wW.…™C¥dÕª·ý~ßu3"T"rÞ#’ª!U´?{çÍß.¾òèƒo|ã[óU Ã4 û­w,ezþêutÅ;œi»E7?Þl¯<¼sÝ??½}ûÁ{ŒšÆµðøöêNëß=aúv÷É“ó_üŒûåë)]ö‡ãUh;¿Ûî.®¦í^Ÿ¼xKÞçi4ZQÄèxæ!zΉ¤¡##š¤œ ë(e¾:ú³ù?üåt½ßýßÿ×ÿþ鯵p“êViøV¯8fe†@®±´“ÓãÍú"ç$RØqç#šYdj¬jÔÈ%&V$5‹MÓtí0ŽX å4^owC1eMbG''×ûçẔøÎ»Î‡þà‡×ëu×6¹v5Ú&ƶÉI÷Û~u¾1×$@ß:+ ÈÔDi³ïMÍqhôøñ;G·Ng¯eßš&=;¿Ì%·mû•ÇS¿ß­îÜ»»:Z]]\¼|úÄòäšcÈ9«¨Ê!JËìêÿ¢ããcB¸}ç"™Á{wÇ15Ñw]W 69çRЉ#"_\ýŸÿöÿ@¢aÜw]›™X¤ »|QÍ©¤T¦¾?],7b"6›¯ÌØ Oï¤C¾Që IDAT,L^° ³Õƒ‡ïžÞM¹œÞ]ýô?|þçÿ¹kŽ@dš’÷¡”RJA&¬ ruHìÕJÝÍ‘#ï})ÈBhœ©‚YI™©bcmÖÍÓf³“’sÝ‚#š)¢âØ£ /ºùüˆ ÍäÕ˳4éžÜååÕ0Þó”Ò8Nv€Äº£ãã“£ùW?úxxûþW¿µ[Ÿ?ùäm$&> â‰9vmÛv÷ï?èŽo©È­“c" ö!—Rʨ%ç\ú’“Ÿ-MÛ2âhúíæìåõÅ‹£eãóýý%Κ²äœV³ÆÀ³‚¨ä1M{‘ž™ÀP…]¢12žRŒ@jâ\@drÈ“‹HNEIL ,6íý{÷SIEÄLJ)¨†HEÄ{ïü ý†ë‚X1‡²~=× õú’KVQ R°ˆ”’1Æ`ªFŒ¢b@•rsèM Ü]‹h'vŽˆ+ÜÞûà½ïºA Ȫ·(•\OôHTyE$gSï8xf Éõ¿Ñ JDnÛHÄŽh1oOoŸÌ3$$æØÄà<ŒÓ Ô¦œûýpu½Ùm·»Ý^Õœ÷>F‘Šl,äœ÷Þ¹Ê_vÞ{ÕÑ9W «5Z\J© ®ú¨åP¡š¤ £ÕêÃЧ”«SÛq˜¦©HvŽcÓ8W©mE Ð@E ¡íZï[¾ \m®-Dêæ-«6m!]çi«fW——/^žïöº8:}üè®:Þo×M׆‡·gïÞ¶wçnA†F¿sï;O±óŸ¼.?ßn÷;& އ¾7³œ%ú†UË8Pî=AçCë=€…Ôo6ÛiïSò>´m×Ë> YEÿâ¿þàêêBUÐÑT…؈Ø;‚š+Sëw»7òb}}̵"[‰.EdÊ™Ð{gHÎÌRÉ›·¯ëûST\x…f6Œc aÓüÿâ?úé8JÛÆ¶i¯¯®ÇœÄ²£°Æõóã“?û—ÿêégŸ>þÌ9Õ¦ ¨HEш\l"â`ì*ìŸ ÈE”,½@)€Dì«?–‘‘^¾|¡ÌgíêèGïÍ‹$™Š¬æËå :篮/Þ\¼~ððôþý;Ÿ}òôóO¾¸xõ6 =–’SFæÅbîC8¿¼‡‘ïÝ»wÿÞ±šæ’÷}†j&*!Ι(ãiš¾\­ªˆÑi˜‚GGÇÎñn·Ûï‡4åëõfʉïQÓ¿û÷ÿé¯ÿúÃÉPˆONo­·Û”ó”D½ b¶ÙlvëmˆÏ’]ëƒÃ®'££…g+r B&’\RÎyêz¼öÎo>,ŠBDÞvÁy-¹( !-æÝb±(EÒ4Iɪ-B)åRÄ{öžÙuí L..Î.ÏÎÒ˜·Û~*âœß«Œi2D`DŽ[:9Zž-­ a±:Ú®/r’غºÁ7´zë—\Î_¿^ŒåÛßþ6L㸈‚I)¥ä)Ó¸SQL{ëæHœ§¼Û^¿}ö1¤5ìÝÛ§ ÖbLÈ1ú&†~6›uNÓù›çVF´ÂÄÞ³gVSçBð¾‰1ú¦‹³ØÌ½j&¢T‡ÒRÌTò´Ý¬s*jÕ]‡D f—g¹dä¼^oK)ޏ¦ ¾<ž×cûÀåøFƒÞßøo •χÁÈ¡‰£¦ÆŽêŽ“ˆ¡f®¡:ÿ@ŠXUAši.Å ÍÌy‡D7þQCD3¦±Vö¥Seæœsaçš¶«’Õ”“GvÕ,0{ïTJeB Á1O>‡—ËEðÑ·ZÎOn5m< †qÌ)ýs)¹Ò0N›]¿Ë>IQMŒÙ/—+"Ô÷‰–CÞKJÛ6Þ¹úÑBD®ß‘D•À¯Þf&&61ó¾âhê˜]-Ž~ •¬ADªüôŠÜ±›‚‰GJ(:+©dDŠ|hä¹Ð•4zÏD¾˜LWWùò¬mæ‚¶Y_ÈПDwÚÌçŒÛMRmpVdx½ÏŸ}øììé«åj¶Y_ÌnŸtÌpôÄmÚn0#¡ó¡ñèX‰É<ÏZW·K)k™J»_ýàëŸ~öôÛ¿÷1§óËëÿüþí{·š©¿zsö‚Ð¥dQU@r7™¢œ'bvÞ¹àIÑRÊE]pÎ7¾1"U #dpì¸ÂòV«.†ÆV„ˆÃ0NÓ¤¦y6[º4Wlôûý/ö&þÊ{Ä »nª¥äbEs§Œ8(°ªš :vÎG“¤– EDvý—ÿö³“W÷¿ÿ{¿Vpæg' J°©!¶»Óå‚-ÿú—õÁ×ïLSÿÛß}üá/?4QÛ÷½Aæù|ŽD±i>øàÎysìš6šÉ4MóÙb·Ûo¶›¦iÌ F0˜¦DäšØ¥iT@ ‡mÛÌh\8:>qÞÉ8Žë«íj‰":å"¹Œ“*¨\njÇâån¯fcš™’*ÎÙuóhVŠ {ôλ‹ÕüW¿üh ım›¦eGìI‹n×ë)M Z ˆPgòV²`ÎâbˆMÓ â0 "ŠÃÐÇcðZŠHÉS.Ù!QšRM7ëÚ&€G¨"*M_<}þêåËq˜¤X­œ§\JѦm5:æHد·»õ:ú(’Sžœcb ÕLŽªtBs¶’–‹à Í% j‰­H÷yØ”~›†Þ̆iÁÓªˆcÙ¬/rÖoü_<ýŒÉÚù±‹]‘q¿¯®¯EóØo¯/^K” ’ªŠ, ``¢u¾¡ j *äØT¥Õ<‘ @ý6Ì103I)i¯ôòèøÑêê¢oÖî¨*Þ¹qšRÊ_æù L¤TR9€VU7äÂU­‡‡'{-b"€sŽ >’êVü…šBÀ\*€ ‰\­Õ”H}ÀÕƒ[eáÛÐ;ÎBÉY  ¨æœ“ZA‡mîݽsçö½7oÞ†à—Ë¥Þ{&B@§1gIcšÆi§~wÛ]…c„Õ ‰çóE=Vf!^.M ççã4•"ÄND§i0ïÀrNRJUfM)q¥O3×ñ4©sÎûàç,4t¨á*˜x!DDªÑID4Õzƒ:ÌÍä†yPOýMÓŸ¬ˆ¬”¤f¦³£”RW*d@HY&FßÄBØõ}@Šo½zšRþ³Ÿöc? »ÁR^µMÝÎM9_îz3éËõ°¹ºL×>,)Aʈ\7Nû4l½#f'Ê#¢½÷ RÚh™ ßœ½‘’‡qà¾ù­\Ê~ø£åjõàÁiÞÉ¿3Ç!( 0RÒ DB@¸iÿ:’RòLŽ "7$ǪšS2«TQßÄù¬»àrNZ.´˜1qÛvËÕÑb¹ Ïý0>zôøÙ³gÿê_üÙÇ¿ûø×¿ýˆÐÅØ¨¨Œ9í†]RHRÀ˜nÜŽ¨ˆÞç¹$P©Ì@t Èêf“òìÁéìîÃoü½ï}ý›ß .$ Ò|¾Š˜>ûèwÏž?‘2~í«_½}ûáÏöÛ¿ø¯?xþôi튇¼iÛv¾Xãjµ\32“«j„"©–K‚Ì<£ªÃXrúq¹\ð*†„hwïÝ?=½m†*FD*‚Hc?9çK‘²Ùf€f6ïÝwwÛõ~¿?ØKk¹Ï±!šw¾‰Aƒ"LY6—o†Ý…³²XÍON|Œ.x$ðžÇ1M}ŸœS‘ øC³,2•5sÐIVó÷}Ÿs®Bä’' žŠÉ U÷ ¥¢hæµ@žÞ¼9ÿä·]]\©(Ħa š%ƒ8˜„¤èëWo^¿üâɳ/NçÞ{fGMÛ"è¡Ô‰@ΫHÊc)©¢Í9C*R 2;ÐAò´ñÃÚØJ;o8ÐæÍ‹7O>·ç‹@âçWoß”©Üºs¿íæ)å¾ßO©vÛÍ´ÛX)¨ÅJN–ChcÉ;E¨÷RŠŠ°"8ÌJ¤”4ôã°-Î1Þ0[êÑŒ&fB Õª]ãªUvÁ„xs‚6qÞ„Õ€D¤‡dáᩇ‡¿L‰ØÔª#«>ÔþvNug(ªÀލÓ‘ØRf$vÄî&;h‡´™cf$Ddç19NiÊ9!9öÁOÓTJî‡MdG`åäxut´üàƒ˜½c¨U çJ‘õz=MÓ4ŽC?J)UY™R1ƒÃ4M€ÈÈæüÖœ¯`¹aÆiºÒpÝ)×Ý`ÝŽ¨Å"‡È)Z-«ÞèGŠsιPJQPÕƒWº›jÄûïúWëÞÛ1Se$ ™•/“ÌÜ´m!6ÁGŸ†éKa0‚‚’9ò!4Ä<뺋Ë+Q9^­¼Ñ8ì†]’iÜyœ’§0” Çq¼ì!ï‡!uÁD™.À<ð8æ·O_ìÖCŠÑ·]ÉÖc.Úfq$„&‹ÅrÊI%71ïž?F>4]›¦iÞvwNoÿêÃ_¯–íññíj¼)’Ur‘¢’×r•s!D-%:ïØEr€Ñ:UFtn¹<ÊjûýĈÆ> ):Z'Æq4€Åb©"9e5ô±Uà’T>úìsOü‹_ür}½- L0L©îxúqJÄ"9SvÄõ§¾Öì‡Y™C14Utauzûñé½ßù“´ÏåûßÿÇŽÃf½V°Â˳×çO>œ6çRÆ”¦ã“/žýâg>ùøsÄ¢Z¹›Íoß¾S#ÔãØ§”¶›]®H ´ÜRJ9g­>K5Ûíw¦Šˆ%ç"Å烦áâì"g›Íæ„TJ0Ê4N›ƒ)L´Ý\çœÆqB@çü—qªy"5°úI5awµÉÙ9ïˆÁ dCL¹”Rg$&E18€¾Ç~ïîÝ»wvv6Ž£J=ÆUŒ‰¤œÆZ‡qÎ×p¢–œûrt2M2ö¯7û_þâW›ëKOæ¢75çÑPµdµ]7J>»>5…fî·]+R 4PœÍfäX †IÆ~»ßϧqÈ9±¤$“MiQM2õ½L=å¾Éƒš9t0^÷ãV×/ÊùgŠB#ɧ~H®ß]ží®ÎLKJÉ4«%é÷eتYÎ9pÁ‡Õr›hj\ ˜T‹WHìDL¥˜;çšNFq>ø÷Ãd¥,y‚Mh‚Ûï¶ ê꿱ڳ†a¨>1…ê•â½W9¬Xë‹m€D|³}­•|@@$GµÙD`¢Z]E)ç:áù²_?6ukäœ3«5E+i¬^)T£w¦P³guvá¼ÄRJN Ñf³ÖyÊPÙêèh1kïœumKˆÌ¤SždòÌS.¹äõõ”³ìwûÍv—s®¶7pŽAÀ±wÁW·ŠH™’HñÞ»è Ó8ô¥äRJŒ±¢*冣[5ª]×åœE0Ç1Ô¹Öá|PG.9×hÿA ~ˆKVžjp1 #_µSàýØc½Í ÐÍä‡ë2m³ÛÌOX×5ㄊõ1€>4)àêheÏž¥a—†™¡M4—qK ã(>4ÞÅ\µ¢eÓ.§œ&̹A,Îñv»ßnvë«k0eDf¼ÿáéí{²ëAUwëëÅbq}q¾¾¼†þâüB‹h)¡iClÎÏÞûwîÒwþþ_]£i¿ZÍBC14·nŸþèG?üë¿üQê÷ZÄÔ©ï‡ËËËúI˹ŒãØ¿=ŸÆÌ56Êtrz„›Í¦ž¾äâ1“oB‘RJ©LºR (ŽýóëÝºÍæÃ0†üA@FÎQ±ÑWWW9eÃÃð¶Ù¾ÌMˆˆÁù ¯fç1¨ˆ‰ZÙm×ûÝÚ{B`rUS@Õ$ÇPõ; މ PÁ¥”vÛ­gÓÚ>S–’Ò8˜™÷ê Uû~WDB0퇋‹ë˳7–&ò~Ö6D¤ESÑJƵRÒ0¶Ë[ïÜ{øþ{NVÝó'¿™¶Wã4•”¤bª¦cJ%aqŽ uè·Ó´÷Ü¥<å<™e-ã¨ãP†]Câ@‘lHW/´ì—nxüà¨Oi×'ïÝñj¥Zö›KfEPP-%MiØ »ë¢ÉÌT¸“’‘¨i[B¶bHVaYV5:FH¦ý,¶ûØ­^¼~õúuE뉂F>dD¤ ¶É3˜3ÌŽ jzA*]÷fÈ€5È7~ÐZÖûå_$f5ÃÊ+aº!øÂr¹lÚæåË×-IÌÄLbŠ€Hˆ„€Qôouá¦õ··ÃfR*ÂL‰MAÅŠcô³6¯Žš&¢–ãåÂyN¹ôã¸_oMm7ô›Ý~¦ý~(Y̽sõšW¯z.ú"¥®E ¢kšŽ;ž¦±ï÷RJ¥WLZ­ÔÖ«Oõ–WÙw¬˜˜¦ɈX…0¢ª¥X}X€Y9¤D¡.]½÷_Z¿SΦò%ù0˜P³\J.…s©ŠZª8œ:ÿen›¶¶…µ‚{ ‘‰Ð²÷ ˜¥iÇ€c*ûõ•Hr•R*¢yL¹ ÆiœÔ;Ç ’ËÔ›dǰÙì†~"ãœÓÙùÙùùEG0A„®köÃn3loÆéÿcêÍž-;®3¿5dæÞûLwªUˆ"ÁQ¢$’’»ê!l¹ö‹GGøÅa‡ÿ7ûÅᶺ-w[¶š¢(’àR SªnUÝéL{Ê̵–rß à‡Â­sî9;‡o}ßïË)壃ƒÊ»q蟿|IÌžCÓÔD¬Y¿|z~uµY_}úág:tdÝ·Õ5w_}ÝYúð÷¿¬*JY\U¿þÊCC÷òåùv»N)Ö‡G÷ú(ï|û¾þöÁÁÁbÞ4Mƒ†Ÿöäw¿ÿ}5ôßýÎwRJݰ)}øþGý~”ÌÎyÉ9+xCYˆ\P•aŒˆ¤S÷"‹‰‰D5B"dSdtÌUà™ã¦ –$‹XIÎÌ5cÎ â¬nßþ—ÿê¿>¼{×±;9>Îq€u6í¤—Ø,fÞÉñí£;wïþúWïþìïþa·ÞÈ ˆ¯¼CD3†±©kf·Ùl¯6[•2ú²åb‘’\(V¢ù¦S¨œbU•3ËõÍÒ9¶¬âHÝf½AäYÝ PñV|èû¢¥±Á~xaÕM ‚QU5ã8ªZ1+à~¿Û÷­Š€gªPê‚÷à‰¾lN§¦e}'p}ßç” A%™f0œ†¾WÉŒH–“a@Tɨڷ]ž¥³ý¶UçÁÐ,8l€y{Ó,°ôõáò`9_\]œî7kÐÜDZë:šnÁÄL¥.È™8t‡Ë—_ìoÍüýŠ™Ì‚ˆ óXy˜˜¦þϬcFmœ»uûáŶ}òìl6 ì¡ÛìÍd6󇋅/À,H>»¦ñ)IŠ±í»¬#‡&È‚œ+W›;ªü˜°fšÏgŒ»¢¬è0öm¿Ývë¬BLìœ'æ˜ ¤”º¶-3ÔR]Wu]5…`N„YRN ®W)eH6YhÊà;ç\ À¥®³˜Z½÷¾ðÍ¿b©ìú®íÚÂŽ@œ,öˆ%ÆfSõž©áTúÃ×ÊLrššH–,è€=U¡:9YÍfÕ|ÞTUpά¬Ûm íâò|ßõÃ$Cß›Ý6« cÊYj˜Ù{§“™²d2*­ÿî<ï‹åÉñ×^¼|þÅÓ'Ã0¨ 3}Õ3ZŒ1„Œå—À|rr²XÌ^¼|QâÝij9p±ÔXc‰’ÝÜfŠþÅÌY§sý͈»l¨Î;fWŠg¹\–U€Ø9d®ê†û¼gïÙ9‡€Ž‰@œ:€@dDصë³Ï MU‡¡Ñ|& U5 –Æ1fdISNyI‹¦išæÙéÙ|~ØEH$'¢G«2.öÛíl ¸ßmw›unKN‡o|ç;1ffÿàÁÃgÏÏï¿þöfßýõ¿û+þÎñ+C»óÍ?À0;\Ý»zþ8çìçïÜ¿óÃý¹wó>øôý?<¿<ïyðÊk_ÿæ·]Csçþò¼Ýî^yëppðâù…Yåcµ\ QOî/žžɼ‘_féMFPC3&$æÂm(ñšÒµ2æT¤CD¤º-]О›Šˆ*B7"»”÷)«Á ¹¿}göOþâÇïüÑ'·†±Ï^<”À )·ë‹§?~qúÉzIßøÆÇ·nôè“ÿø·?}yz÷Cñ¼Æ=†º®Ëg$9€ˆ6uS>ý¦iVËÕÍ«°lˉJrF Ç!Å>ŽÉyCb ¡*àØzÖ8çœks.µê<Ĕʽ³ø ô–âtã4ï÷ADUl¿o ©$­Æ˜œã*„6§ì¼3µ1§ ê€Gvì˜ÁŠä@`@“B€HÄ®ÛïÆn‡ "IR.èDIÉÔÔ¬0`Á9o¹T\ÆqŒãص49‰‘H%O BUuT12*tí~·9MµwUU!a‰Ò0s(ÆgïÀDr”±Ï}xöÙ'‹º¹uû¶cúMÀ|p'BSS"Ts½X’Øî‡ý¾WÍγÔ {ÓüB5ƒ²s¼›7aVq¸¼¼\Çu×÷Y ®ªíÕ…Šå¤UaÞdbßÌ‹&N1víTÇ¡ïÛí'½¿^_‘C+ÕŒˆ1朇,R7M±¾ºäg0+„ûëfêi®§ZR H%M;±YAl–µ/¦T³EŽ©ø©hÊÎMƲÂä*î¯NkõÚŠ3ÝÌÀ q1*嬪«£ã#œ¨ŒãHD)g•ÔvÝÓ«‹Ë««”$gUîï Q+8|  ÚœK6úÚ£29AsLl†¦*9š Ýâøè¤mÛ”ã4âþr¨PÞ˜sœrêûVJšözöP~ryVáKm‹B¨‹FcDB5Ë"…ÁÄLÎq‘¡bJ윙1q*̹ì`fiGß‹F`ä&4³zÆ„ÌThÀŽH《}‡$}ßž¾ˆ1£óÌŽ»6Ã\U7Õúò´ïz„¶®gŠpvy ž¬:>¾wr@€ëõzˆÑ@«ºª›úEŒ¢ÀlÁUVŠ!Íš:ÄÊõ]ÇNrf´ÍåË'OO£Èï?üÝbuòƒ¼²k»ÿñúŸsLÛó«ÿýû__œ^„f?¯—çÏ÷Ý–Ý¢j‚š>~üŬ>øâé‹«íàëå?ý§öíï}ïÅË‹};ƒƒö¼X,VÇîÝ}u?ùà“Ï=ö´èw1©Ý>y£©o9_!óåvÝhìû“dFÍ1¦~`ƒS#`IÀŽƒóà ¡®]ÊéÞý‡èOOϱV[x\yv>GóÞÚî2åÅájvxXÝy¥ùÁßùî¾Ý ýÙÅ™<¶m•¶GË&§ñó'Ÿ|úéÙ铺Á·ÿà¾õÝw>zôñ¯~õ»ÿñ“Øf³rc.AnÏœK}MŒÑL½÷³ùÔ‚÷óù\U/¯.›Ù¬À  s´Z “n6›yïë¦qÞ•òÇ!I.M@Œ) ãÄÉ@¢Õábê° àÔ2”S€Ç”sù†;纮O){ïŽ ç8¦±NJøŸ‰‘°ÌÅU ©pÆ\éZBtû<ì4Å2D÷û8NÄ`0ôÎa‡Òw7Æ€1e4"$6PDdv>'vÎ!²ˆŒã0Š`åa±Ž—»¶Ò©>}þü´ô¯×îr>oªÀެp‹†¡_¯×]ߎ]ûqèZf@)g3s.pU0¦$%ÁLªyìÛýîÊ9F¦0›9ïuZq´d—JôÆË´6ƈH׉TÉ9YÀ®SNªên4>š¸{p¸³PUbZ:ê†~|~úr}yÙµ{ÍI²¤œ þ׊á¾\‰'?|É)½QÀô½P°”ân·Ùï·E5*ap­§ß´`߸ڳ$IúüeÌ)¥ É9_î³S÷ôÛÀë ±À ‰K Œ33)ŠI, 2D»Vê%gUqì§Ù)€eДƱk糃íù†ÁÍf3•,)–ú„v»[_ž÷C·ï÷m×Í D! ‘\n¶qèǶ¿{ë5´úôôr¿ßH9åÜч°ZÎﮎ@Ð>òé|±Z-V!x%d®›å¢ñŽ$ç<(‘ØÒÁ¢ñ ¥h%xßî·—Ûíf³;<¼õÞ»¿‘¬o¾õæÃ‡¯}ó¾¹\º_þüý>î^>ÿìåËÓ¡_ÃrÙìûüôE›!Îï¿þ¿üwÿHÜn/}üHÔ)TÞǾ¿úùb^9ûÖÛ·~üÍÃ4~§vîù³«÷W?ïâ*Ý}}=Àè«æòêüh±ýâS`fWÏæÇ·ï=Èý~ÆÍf㈚¦™Íš1Ž—WWmkU5{öìñÅúòö­[GóEå| ±Ó4B°œs"DœnvÃóË«vè»1Æ/SLP×u3«öížËò«*9¥Ýv ¦¦Ðwƒ àvCÄ¢&ªž›"2;0çüb9CSʱf¥ó3¦4ÐKžP«¥°ØSbJXöc€R’¥Ì\ŒüåÚS„ã©—Õ¦öÁ©«B•¾âtI¸R±B×!¨k©ºô{€c&¦£¤BUzˆT±ÀÎf³Ùv·¹¾”yvMSÝ{pçõ×_M9n÷ûÝ~×q¿mÛ¶K)ŸŸ_HÎfâ™àšWH BesÂkõ€˜r4ð~j[½¹1”QR)ß(­U]‡$Ë8 Ä~6+5O:±ìÕŠÇ1Ƥ*XU嫌ףM4tÞá”3˜̲¤Cqýy‡yª)5•ÅÃZŽöŒXXóÀ“nVfì0çtqörèÓ?=¿¼ªªàÓ0jJ&ê½Ûïw¸Þ¾}âœï†VÌŽNîÜ»ûàÖÑñ“;——ëƒÃ£Ë˳¾k½#GT{•oêfLé‹§O^è3]6uñ56u ³ívCŒ«åjÖÔW›íåz«…ñ]΀™³Ù\0dP,˜–<)‡€¢š%¥$Ù±ë 1D•“ˆ•0:¬$‚¦rSPICO€R-&æ8ál¾`Çf6É[ >°’³¹_#Aa”O:8^³ÀLËÛ96㜥ÌK5Âä÷˜Ï×™©R/‚wäL4qÇ¢ ès\NfPÂSXt:3çØWn¾šgÉÃ0žŸ]½<»lÛ>¥2EBBçR’1¥›êíb^œìF“ÓßD¬ôu“ÈÔ#1‰à“²Åš‰Á‰Ôüu3…àE´l¥ƒTD0;*ß×›)‚©@ŽéfRŠ„€†DìÕRéà)£äRG=Õ€X©E3Ç\×w¦OD™‰˜mìbÉgö1£lwû,ÒøYîS·Þ¦8ˆ%@04çH4Ÿ>~Dì\ut¼˜/—ózUßiž|þÅoõ»íîªë[dËšò>Ž1åf6ß÷ëç/‹ä‹Ë—Y„MÕ!£âl>ÓcÇŽÙyF°´ÏÎ9âÀÎÙ¹JŒ‹Å4Ç~·'UOžWË:nωÄÔ3«1·ƒ»uðàõo~ëÁk_C€'§Oó‹Ÿõ» Ï8 ì™G]×…P-ËÓ[!ÔÿðîÍ_¿sÈà ¾¶zøÚ€‰ÂŸ¼s¸Ý¥ÿïoûþ%UõуÕÅ‹3ÆLk¢(š¾ý7<|øäó'qèVK_Qóöë_{¹Þüþ‹ÁUa±ò÷`N¯lþËõýïÿñë«ã • ̺AÿïŸ=þøé:dSIé~}þl¿9ñìÙz³Y.Vý8¶íîÕ¯½öÎ;ï|ç{ßÏ:œ¯Ïÿý_ÿõOþŸÿÐ^^¨ävã0ï§F\Õ‚èp>Ä8ö1¦RÃÌèR–œUTc½wÞûÒLˆU]ƒA*ÅX†ö)§ÜHy*›¦)ýnÅâ«"àw ZÌ›¶m0ç´ÝnrUM)zöˆ Ršg4ç´ßïg‹…EK95Í,„sSv Ž./.;É9‹XÊBjÌlm×}ôáGã0–d ¨¹ó‹މ¯ý‘“P•Jc$¢–êG3p.°§ë'Š=™ÉUUBݱP+UMÌrðdì#ø.¦‡n¿VM³¦9<œëQsÙŠL´ðBUåœ  `oÇ%[×^©·˜cÜlŸ©ŒFåÎe&’ÔTÕ˜ÃÐÌg«?|üoDZÐlœ§f^“‘hš%'5û­í@EU3X?ލ–«ýÉV«ƒ}pqöŽoÿð‡Ö¶­ˆ¾òàþÇ>üøãG›ÍUÇ^dè:ÇÎ{§€ PpÞýЫI1þ¯¯†(ÕwÿðG·¾}µm_wN$=ûâ³v·­˜ëà†ý¾e®›†«yvÃìþÇ"ô韽ûÞÙ?ÿ³¬ “€ÈîÝYÞ»cîÿàO]ôÆôòIþàwçþà ›çÙjøÓýá=ùú›¯ýÑ}ëpA÷ŽO”ÌþñÃÓm;|ýଦPàÔU OžôÿágÏŸï÷m·m÷ëÇ~ؽ|þɰ¿h¯Î=óRú㣓ÿö_ÿë£Û'/®®ž¼xþÅŸýâg÷ó¿ÿiÜ­Y•À2X?t9ç§±ª¡…* ðoÛ~ úò´Îf³˜EsfÇe¿¶ª•°z9'9(¨QçÂômÏ XÐfPVò>µµ”ê„ÐrßÌ´\®bcLmÛ­K$Š)!ß½{˜ž>?%ñÍl9Äè`tΉªš6U(tqUÇ‘l:á0çÍôòê"[ö>”—ïrNªT’|pmµ6@TÍb€ì½¨xJQ‹™1"{ï}pÎqð•÷Aö]'"@”Ç$@ªG·N–GGÝöìòâ…ÈÈûõåo~ùÓG¿ûe€@»q¹Y9Ïçó¦iع”ó8¤·LÝêè¨ï»®íúq˜Ì?¥iL³‰jΚ3[™*XDòÐO.j‘Œ¬¬#ÈD¢ Ëj¢ÊìØ»,³¨ˆg@" ]ðI2f)Ë ï«*\{W¦KÓ—’Eià DbÌY˜§¯¸h®£ª€îi`˜rïôšÅ@Y(»Lº^r‰ˆJ¾±<á¦ff9  #™ÅPEM'[b‡ì¬8vBcâHÀ¥Ü–½s@òTÛ†D† ¦¦ ÙDJÄdÕÂiû15ÍÉ;§ªã8¦bUû®ªªâõW•1Zy³Þ{ï=zÊž¥dëŠãI53€"˜c.ãèRu{-ú£÷>çlj@Eô@ä2¸‘¬ ˆ˜S¦9±cç}ðS1HL1g!&OAÁÌ€Mƒ+§»ý˜.œC瘘! ˜‚z"®ëår Œëó««31!‚P{*í_&Vêt5{öH¤H¡™ÍšÅïßÿ@E^žžæÜá“ǧŸ~ü9‘K)gMYÒÐ·Ž±©«¡ï–Ë•f! mšyUW—Û-‘f$žÏç"šEÌùW¾óÏþóÿêÕ7ßÌ"1Ž/Nw—ç3Ï‹¦ö•æÕ¬Î1f 扇qÜï‡6Žžö¯=»]q<;½¼ºÜ×õìûß{x°rå[Lh³9ïûw¬,‡Ξo¿8ÝÝ9¼ó`.fO?{òÛ?¿8ÛÎkþïÿ›?«æUEøýoÞÅ‚MÁbPAS Ý.ýü§§ÿ×ÿûq®—_[<;öÑï®7ëlJ–› Ì4k®›p°:xð𕺮ž>yòáãÏ?ùìã'Ÿ~òñ‡¿§<`ÊeyUpP5uêž. QÍbD®ôâ¦({륪ȴ"šÍj3-|‘2ì‰)ƒÐumbJ¯¬ÝP²ÉQࢱ"b‰§^ŸKÀL«ºbv}ß…`ˆ`…/?ÕH8¯ mÛú*˜ÑýWÞzç[ßþí{???F4 !ˆä1%Ïttr’³œŸ_äØ1•¬Œ‰ŠÂuÛŒ§Êl‘ÅVrghb‡‘ðz= SûqÞÌæõìÁý‡³ù¢ëûÝ~wûîÝrõqL)@];çk†ªÄÖVËù¢‰sJã8 ýpµY/KçÜåÅ%ï• †‘¹ÊjÏŸ?{ðÚ½ºj®..Ïž=Û\\ ŒJ{ ^’˜"3ïvûßýæ·jˆ@‚ú“w?Ûí»}78_ŸœÌõóñh6ìÎ×o½uûÞ­ŠJ<ÀÃW—w^]•âÜ:ø¯¿yÿoþý¯‡¡’eÕîãb^£!@9€–ð²€<útûoþêÃ>÷Wòéïß{ÿw¿ÜmöÙ½&ÈðäÖáÁáŠÙ!ºó‹³ÿãÿü7ÛnÿéãÇ2ôíÕåÊ1¹‚7Dò>8¼÷¦V®Î)§ÂÎ11¡÷bšEfU˜ØPר¢’y¬f& å,_(’7f®ªÚ9g€¢VX¸Ž‰ ½we,/ ‹™•©à¨¹” šˆHNiz©e'®}½k÷í¾5F,A”›“ˆ¤Ô#@Iº…˜Ù ²( æ®ví¥h޳æb2ƒr¬Ë Ë Ð{nBXÍæ÷ïß¾ÎYRNõ¬ž/æÅ3ЉbVmšÊ¬IRžT6çŽ_¹wÞxøŸþé;g?øã7ŽOæ0u €˜ƒ¶“ç/·¯¼vØ÷úèÉn—7UÓß9Ä[«´{q°Ù©iU…û÷ﮎáj} ªöìÙéÕz-YšYqÀqB¨oúH)Æ”r†©ÓÈÍš9c,WÃ2bæ¦ ©È˜LÌγ7…˜Rñ1«Yñ7íT›LˆD¡ªÌ&üT9ôˆH bL9›©wÁùª8 0¦”bô!dÕÊQp^Á†¡ÿÍ{??¹u[uœÏf¦Ó¹Ç9Ê)%dšÏç…"UT¤öi²Å©Yy. Ù•áÓT>7´b…b­”’ó,N8"6Q‘TÕ®ïǘDA‹^Ê`‹9T@^r¾¼¼<¹}›Íù:xGÛÍå‹§›§Ÿ?’¡«ƒ#"5EDb§bYD†dWï|¾ª*FÄ2T#Ç\‚‰ÐTÁ;Gˆ¨¦`«²Åc†¾<ÿcìID ”àjµ ¢àCðÕ„†qŒ“û TJ%‘Ž)'5ôäÑñ¤¾ã”&P™ü|„®<ü׊ÜtÞ°u2n[<#qÖk\¢d@`v„S>;;×i¦ŠÅ>ó¥ËpÚ-&yùØJR¬œ,nþÆ¼Šæ‰Ñ9VU,WÛ‰x)%ï|!·\\\UMÝÌê˜â‡ºñ³Y•³f1Uq…û­’r6EÑ @Å!@¥> ¾JãÁŸ~™Q“”Q¯ªÎ‡Ã£ƒý~sµ¾tΗâ&^ôú2à)?‰ A²ia3\Ÿ› †¡Ð}±,£jYSJ¹ìC×ÊOu:`JA]TÕÙÊE¶¯Åª–$ow;0àòH[®ëzu°:{IæçâËTÅâRBD0buÞvûݮݱC@*·rËœúCÀÌy>9]ß:\p¿ûíûŸþhÞXEéÁ+sï_yc¼—†×›«³OM²J¼¸8¿¸Xo·ûÙ|~xpèÖû X&t¦â|Hš5 9hš*ŽirЂyóÅBÎÈËùB$C¯*ˆXy‡å ‘(¥ìÙ9ç™]kL\?*Ìêr{š\ÁÄSelñ:—!VÁœmêZc&4¨Š„YÕê¼8ç)gu‰¹òqìÚóù|µ8TÍëõÕ“gnš&%0…ªª¾r£E"$*ž€é²í®]kפ›’¿¾˜0#LZ€”Ì;GΫjß¶j²æBäRœª,$ç!Fnæa± æÕrÙ4 PÊ6îv/×ÏÛ« [ï]áãÄ”²hUMGœœs¯ ‡ž® ]é+L(fž.׊€håª\:g¥ÜBf³™Hc¯&ÄDÎÇëºr욪*:jª&Ô•÷ÝEº²â"GD3L)F‘0_­šù¢x/'¥ŠJ¶ÀбgöˆXŽKE)Kí 8g)!L& UUÒóY4GQU"Cb$˜5Yõ†o&Ó÷ê+#ÍIšA4S`vUU•kãÍ‘8F‘¼Ÿæ‰¦_õ¤‹HN™9BJ¹íúår*Ô£ÃÃã#3†(šóúäÖQ»ßǘrÎC?ÄœLmc̉™ƒ÷S¸IÕ®¹1±¸@UÁ¼÷Dä|X,>x-åñÝwßÇh¡ ðPþT\NTÜi¢Ñ9f"$2Ó€,›JÛ¶BBUÞ}΄0ÆAE›¦AšÎh×nT‘,ÙlrÎÞd¦ÊÇSJÎ9`²â+&tÁçœAÔT}¨NNNºý¾ÝïË×OT x2§˜òfhZf6«Ê›`Âཌ)C¹]w©fÉ’ mʱÏ9«ØÝ»÷}3Û¶íÅÕ€9¤££Ã8 ÛíðêÃWûÓ§_…Š_öZ"ò¥ãÜÑ8Œ̺ýn¿Ý<úùÅÅËYÓÜZÝogõØnOf«¾ëªºáùÁe±¨<¸ÿþílÚ¥>4^EBàÊc»9k7WÝv“¢˜»j¿I]»2VÏÞ}~°Z¸@tPñwßX}íÞr1£fQI†ËÍ(@gçí?~ôâ£ÏÚ]ŸÂþâÑóKÕžm˜¼8û¢™ó­[ÇM圫¿û§ß“¬Ïþúßþõ{¿þ…¢.ç³[ÇÇ1Ç}pyuµX­Žo×Õl·¾ìú68$²!DA1¢0_ÌD4ç€qŒ)å3!¡jÎ9ßpUKŸ¥!:vª)çBÊdC+WÒâ 3+L("(Ìí8„!!˜~5¦ÅÄ pí@&‡Õ„»Ħžmwû««uS×M]ûʃJ»‡nxõáÃã㋳—)Ž",ç 솎œç2TÓ”ML‘ˆÙT§åÑÝ,哆+‚×yîrž@SqŽÊצ“Y—s5+’’ €µûý~³w³YÓ4äxµZ­VÞ¹8lßûÍ/>~ô» ƒS žPM‘È{§ÓÅÌ$Žc¨Æ!©d ºÏ ` -x/’¥¼Z0‘œ%ƒæ\(<Úívûa_t¶ÿðG.Ô§g¢†›Í•©nÖkïÂk¯¿þñ/Ç.ýšùøÉûãІP½òÊÃn·yú'—‰!(2|þù'}¿?<>dÄývÝí÷ñÁo~ýë_ÿÉ»¿=k[?ÆY3[,æëõåó—Íâð ª\–ëË3“±ß^©Š™Î|ÊÀ,çQs¯V_¼x*F&±ëcCsvnÛãßýjû „àíÎíÃó³u×Ë×ßùú¯ßûøj·EôgW/H]INCðt)TýWzÏmÛ›áÙË›ËÝþr{þì9HB”Y}p|¼Ú¶û1Õ¼>¹uk>Ÿ]»Ý^0©sÞ9¾hOmm»/ëÔUÍ\Â.6õ.°c,p\UP4MRUj Y‹ÿ @ JhœŠ¶m`ªhÅÛ_ù‰éÆÿ`&ÌRF̾ªcŒ1eva>_˜ 1æuT"µëúýÁj•å±:9:yë­·ÿãßýmßö&J€È”T-‰”WÊdÞ3¹Bé§/'÷#•%^²”#©IÎQ‘*&C°œ5%)¡’aËÙ9§Ç8›Í¸iDuL±hÞιf9û×ß;X¶ç3Aibc%º)tׂ_ÉYÑ4gɦ ʤ>ǘÑêJUcNj ˆjê`Ì€í IDAT;X®ÓåÅÅæò"K’8”w™¢Ž›õz¿ß ¨ •ʇ Y ¯G§qBï}p¡š¯–G'wî¿:{ô‘"°#ÇM1‘´²”Oµã J‰ˆ ͼ÷%Œc`©´9L133…)9©üÎ9”,E‚1P.(Q ‰tÈX(¦y£ªâ$˜BVeñ2ûr 0¹æKÿ¢"“÷·ªBÀ4«u]Ð9O„gçÇG‡UåꦨêPÅcWH4ôããÕÕA™Hr65bÈ¢`¥¯Ÿ"UH9øÛm¦R1DBÊ9© b!¬V+Ç~»Ýöž‰^+ÂÄX„”òŽBÎ9ç8篟F""4fçü—*™^_Œˆ‹ì÷å¹É¸ºª®›Eo4M)v½ö}ŸRDSÝn6›õÚĦ-"PK¦)¦„LåÓ%pTk0,ù)Ñ‚LàÉÆÌˆ|xtüƒ?úÁ£G=~üyL#üæwï-Ëý0„ºzë­7Þ{oÓ¶=!TÜ€ÙÉÉÉì}U·Ý`’B5»{÷ßÅ~¿oû^@㘯|½è2¼òÚ+‹ƒù­;‡*²ÛlâØ/æÍ·¾õÎÿÉEýñ½ÍÅçÕ4¥qðöÉQÊq½ÙìwëÔ]Vð,ˆÑrĬ6ŒÂ.9ƒÔ¶»`y† ’ÂcBPÚeì»ñ—¿ý¤Ýï ÒÓË3œr爘ö˜…28Ó ÞË[o¼z|´ì†1‰>~üüýßY›ùjÞ¼òÊý¤ñøxe Ÿ}ú‰ŠÌf30[_^tÝÎ;r¡ñΕØ6]C‹ÅÑ9.œ9vŒÅ `ÀĦņ–4T2 ;†ª’S¼nV`.D_$3c"çM×pb‘‚ZA $",Å—˜]ÝÌR@þþ~ÿÅóç_<ù¬”»Q]WbîêjcªšºïûBxòô‹Ý~_ꦆx­øgƒñÏþ‚þþïšÓ€€®ŒÆÊÀ­Ø*ÔLMó0S‚c_8–YÔiqzf©4çe‘¾ï üVrŠcô¾Z.V~6ߨil»ÝùåÙêx5zr|çöí»ýùK4µÀ…€Œ×‡/•’Û$Ì1‚)'_Œˆ@ìo$*OΤ­’ª¢‘ã€Äcq–Jf"ð>ïg˃’(K1–Y?!–òtÌŽÙ{?Ž#會¯L¡†¶kSN·ïÜùü³1ÔˆH6B`â)ËpÝ&ÁDØÃõô $ŠÏ^ÍDSŒ±L2ˆ¹tp—©”€ÊÄ%‚rÅi¥VQ¥²…@ñ¼}iêÙÁ/‡Ÿ*25Z\û8‘Ø3WÕ¬O΋qŒqlÇјÈû`bfÚí†ùþäó_ýú×çgUðì9¥T5õ¾o×›Ë$ñÙ3êúÞÏÎ…ºj‡ñï~ò“ãã[›õÙ~³%´œÖ>ú°ö~·ÛM}ŒU=›t÷^¿wxçDQ«àMÐäùlêo¾ó­íh//ׇw_…'/Ça_yßõû§§;|þ,„hï ­Á9³¦jâ0v—›«õ6ÖóÅlÆž@DXÚ*Ð<$ƒ¤JÙÀ‚›;²™§}ÎcêOÍgÎB·¿þàù§g/NSêòÿìû¯Ü?vöûýöj¿ÛÆ:ÌÅ:Dt!,®¶—ïøÁååÅåf꙼|q bUðŽÉ4aTõ7Ä:v%ÇoÅ å0©a\ŒÈ9«²‚óŒÌ8õܳD`BQFšf˜ˆàC¸‰årk€âR)Orɉ±«ëWå Ý:„0ŒéóÏ?“$`Öõs®ª*˜=y½^—?ï½WQb¾Ü\æ˜$§rØ+‡3% çøÍ7Þ|ÿ÷¿½Z'r@ÈÀÄS´',tãrÔreÙssT‚<{SÊbì¼óADã8Œ±Ë1•å¤äÇ1šsh˜SÚï7í~ÃKC4ý2°~Ý5%9^äª ¢`*SA²\#Ÿ˜™] 2Ù5²2Ò€1)åÍf; CV¨f+BpLLT`Ó„L0Éë)Ž1¦IrŽqÔ)—7ÝÎ9|]W Ðï6Ûó³ËÕá­ã[óƒíöBEˆÉ¼+ìÈ"v=›¼J€ èÉõ %T•J–Œ˜Œ1ybÂR–[¢.rô´É:r}¯ºFh¡dC ÉiUU…Êà^r.y×¢$–â="pÁO$3u\vj888U-"ãsÎ)G‘,&„\–ݨQEMd­¹Ý‘™ÇĶX4³¦ !¤¤Ì®vÕÝÛ·ê°55Ï>Žñj³ë† ÊIy»Ý¦~H‰ é+©(ð>dÑh‘êfÞw]©#Ã8Ž»”’ä|ðLìhèGhH)‹Lí}åDe ëœóÞRRUavÅÆBèûAU½ŸJW ]‰Ê“ãPW7½ºZ—DAN‚’Ð4uÕ4©ƪFÔœ%§Òõ%(ÂW!„ªj6kDÑ9dïˆ\Û”MªœlDÕ°´XZßÅg§/Åp}u¥]™©®g«Ã£ç/ž#2k¶:4ŽÉQ°,cßýÃ϶Ù\Ä¡µœ»Qò§Ÿ<òÞ#`0óü~›nÿå¿øÏfL|¸\Ž]—bêÚvhû§OOýr?ƒÿŸ«7ýµ3»îôÖ°÷~‡3Þ—¼œk”ª$K–,YØ=È6ÚnH$È_ÙHÝj¤Ó۱ݲlK¶†ªbU±Š¬âpç{†wÜÃZù°Ï¥œ|& ,òœû¾{¯õû=¢(L¦Ó[·ö.Ï_¶m'1j<'Ò86~¯¡cè:úòäôõY;Æå£½ƒý£ÃýÃ=DEñ•:Lªˆ `³]Å Õdᩯí¨&’6[;4›±Y§Ô ôAšï}ï[ï¼ó ,iƧŸ~yrÖ1ñd2±óù¤ªT㫯Î?yò›¦Ý„0JODÑû‚¦ÐÑûÁ9W×µsåŽ"7áVEPå$â£1epE‰ÔeéŒ >ú裊D0»¢&¥]—NÉ• }Ÿ‡°ù¾“ìm‚i·*cB#1zï=gÎGJɹº²ïz?úü%ôÞçÿœ ½¹tæß<ŽcÖpúq(¬É;ÕL›Á$ò—ÿõ/þñþ.ïDÁf 6ù!¢1ag .ó"qç1+òÞ !… ’Ë;:$Bê†~ôÞ s.)Œ!ˆ  ׯ.ç³;GG³é, ›¯¿z±Ý4ùÐJ ãè³SMòñVAE%Ea&"'’Œaë!) ±C¶€f($l¼©DÄX›Ù˜YTÇÀ I”-[Ô$)¦´B ÎX¹Á¼cDc IÕÛõùÉÉt29Ü?Øn®’(åwÓ(RÚ-î0³`¬1D”$åä{~£¡‚£Gb6†o6òIw‚½·Úy´)ËYòø‚ß„#³ªB³pˆ‰Ê¢¨êz½^{ïá¶Å7'ˆüGߨM‘ó ó`S’Ã8!Œ‰v9BPB6†¼ÄvÛ  H×uÓYÝ Ù¢(\Q•Å­Ã}¥žT>æ¿ Ãƒ1¤¤ ‚„…qÆfв°Æ`U”“ÉäZ%„]j¸(‹›¯oRŲ,³©c'ˆ)Å›4ªˆøÑ#¡¤cRÀLuP6Vw¹Xy?gmñÞë›àí͈&„`­e$‚¼ò€|³"¤²š^)@‰’ûiIAr*ÑZ‡HUUå͈±%‚ãÈZص‰É° Þ E P5@ÀÙ¬v®–wa>Àͺwv¶¿œhŒª‚2A5BRÕV¶.ª®k·íVAÙ0³±`Xu“ï|ïû|ûC¾:=Càözã c Kç ÆÐ4ë®m¿ñî[ÆàÐon___¼xöeeM ©*'MO^¾îÛáÓ'_&ïóå´^¢’a›b‡¾®-9oüÙ¹òêruuþ*„à6E’TXÐäȱ‹iì‡vôñ`oÿÖáã²6o¿ûX!uû04£#¼¼º"k>øÎwN^¾þ«¿ü/Ï¿ú2†ËÒ"@Lѳ7I "†a•l(Âc¸‘Þä#xJ¢@h&L”’TUÝ´™Ò2`ׇÚÖÕr6®®ú”Æ”‰,’…Ý4¤œ SŠ>¨j ,ÈM~ŒHòTEUCDÕ$a Á[k ³A„œ)CDI1E¶¶Ñg1'î`y”Sz1‰@ÌßUgìj’Ÿ-HÖØÜÞjš&¦$€Ìdœu>„ŒNŒ1…˜vc)LŒdÉrm`6@’$Æ$¢L, cˆ}? ãˆdPP$ GQâ˜yZW³é´,êñjµ¾X5⽈A$Í 21…c:Ç”RL†Ù9§¬Î9´Ù 9äÐ09b&@…iUIÔ¥ñ›p%bJ)"2(Ú²p®È[ëè»±Û¦ÈUµ›K ~h …˜ˆÆ°q®*ë¾ïÇуIŒf:?vÁ ¨³#k(€HJoÒHùs5Ì»hTˆ1Äl¹ÄL…ŒƒUÐÚ›w±²(q}Œ!ió^ä&¤*ˆ$ª²«+Á8Žª²˜ÍF﻾Ó‰•œK>È[g™9Ť‚ºëîÄ4C9R¾i7">¯¦$SÎ3ñFR4&Ï‚$Š팒´mD²…3†ëªX.ÎZC´¿¿WVµÏŸ¬1)í—¥C 1fþ ‘I)¤‰,+i†Óåc`J)yODÆp¾8+€±6‰¨JÎÇ›Ó#ˆHÓ6ª@ì¬uH¼K7'ñ>äíwæðä iÞÄføg¡ú7 Š7Ç%5HB;í•NêùÃûï\ží&]* €Æ²±Ù¬k]ÁÄÎÖ€•ÚJ™sŠV•q•F$Bƒf÷õá(’‘+“dô>³e#TÙ³$£3&Ũâ%E/¡U9Ø?~üð­ß|ô˶k( äyµw|tÿÿøOð£?H^_œýæó§®¬ëÚmºæÖ½»ËùôÖÑÑt±ÿütÕœ]Å0<¼ÿøÞñ¼ïâདྷößûþóÏ?ûùÏñɧ_@òûÓòòìZcúñý˪¬7íd¼÷ð®5vèFŽA¯VÍõõ DŠ>¦¦ÓʘckÈDŸB˲lúFŠÒX‡Ã°¾}øàÎñø1\_]÷íˆ"Íê\“•üÇ_|öÉ“/^Ð.Xœ Ï *bÛ4¤p÷îñÐ÷mÛ¦”Øì(%€  KRÝyÿÑ;ï}ô«_ß»s÷üü -6C—T÷ï~øo/‡?ùÉì¶§*ªl “ëúˆlcÃ6ÇO2k:wSÇ‘Tóc€lQf‚ ²%FJ1YcÂØ•&8pˆŠ;ñ§ÜÔ×ER$Bë¬u6ƒh߀7nØMˆùž¿o`«9?͆Íx ±3VUÍåÕµO1FÙ•(a—fCDg,Æû1CŠ oø«€lLÞA§$ll.X&c\L!13*‚HôAE’JQO~ïGpñê铟_¤$ZU5íð‡šóbPPCsÅ®"* 55ÚšMaÐQg,—‡î¿7[î}üù¯?ýêI’ ‰M]M¦Ó™+ÜnbÏÀ0¦aÓ7›ƒåµnuuuyúÊ÷]ëÇ8z"SuYLbJJ "&Ç”±cÆ"Ä0æ²M~>æaÔ®@p“qÖ"QJiìûÂl ªîD6€dL…uê‰(àn—ó0*’¿šy¬cd¦]ò@vÏ{‘…!g1sØßcmcÊ9ËL³IIóV'Þƒ‰³Ž5Æqòüš²Ã:›'³"0„¨ªSLaW`"dQ•¡²)5ä¹á¦Ù®cT·M»XîÙVM)ZcööȰ±ëG¢÷A4¦ Þ{I’0ÃCˆ)1C©Þ¬Ç)yßuÝ.™^Vy¶ž§yyææ‡ÁûÀl‹ÊM¦ó|ZÏ ØbÛŽP8W”EJIdŒ1Æ4¾´†T»Â¹L €:ëbŒ’DE³Š«œÔw<üüãß  *’5¬Àl¬›¸É´œoš- ‘™¨™Ì÷ï 9ÍZÒ¤†Šª®÷–>šs¿>új!‘DABb„’„Áb†¢š3ª&C㨩=ú.w¿‹MÓ=}þ¬‡¢®pGD.æÕ­ý[ï¼õŠïšµzÿÁûï/ö‘´é[ç\QÚ!Åîòb»yMN®¯.°Y]¬6M»¯«R ~ñüåÉÙyßlÀº2Wñ™°(­as~~Ñ·­a¾¼¼Ô†ß´(TÄXÇlE!`èF8¸÷îÿÝÿX/öÄÎY¹:8Ã0úÙþrÿèÎÝ;—‹Ãƒùêó0ôˆÚöýþÞž+jQXošétæc¨§³óóó‡Cc¼¼¼|û·ƒˆùùó¯BŒ¶(Û~˜Ï†`o±wòúd±\ž¼>ù§Ÿÿ<ö×%@L#$ !°a‡6Š 0t’’ìxadÑ#"òÉ/‡1pWxƒ”3û”-}!À1Û¼:=¿¡VÁÿ/Üã™)æãüo9çj™‘‰ë¢°Ööí’ qaªab&I/..¾zþ•°™O\×¶×WW!DS8f UEwCU[õÄ–e1YÌg‡¢RLj;YV‹ƒÅb>›”uQbÓ´9é7ŒRìM÷çÍÕ2tÓùâððN&UiÛVbDB…໵HŠÚ¼:yµ˜-o>züࣲϟ ÄhØÚ¢Zõý`3;¬‹¢èûv6™,÷ö\Y6Íæz³Šš(3j5?rÓÎZ³ûG !òVpGEäL‰Ës°<}Îڷݺ&ÓDs‹Å"o­sh[˜ˆ €HŠ¢–ˆ¸@FÆtÓÝ!Äò皥¤ÆpJ±ëÚìáCÑ("ÉGk]bÓhŒ%ä#ˆ‚€"ké%ÅìÂ#M dØ„ó81} ZÇUb†70S÷|(NÎæ³¹±»`amYÖ9¾Âõ0øi=ImÛ´M» §RB& T6Lù&ˆ;În¨ 9ùÙyÒí\IÆ8W¾iTUÅÌ«ÕZœ5u]—eÙ¶­akþØ'£ v$a$f–˜Þì`STc¹&®W›fÿè¸zñ²Û®˜)ÄâèªYU/¬1d\Uï·&‹ÛA-óƒGH¦®Å‹«·îÝ÷ƒÿ»ïÿðí ºðÙ“/_ž\l{!‹‹…ãªpeu÷pQ¦õ²éÇjR™Ð@–^_ ²¬M»Z…qÜ\]<ùõ¯þæ¯ÿêruME9)ŠD‘Òv»cóüó®/fƲ-ÊaÛ‹®üBHåþü›?ü!0mß7›¦mš¶QÂ>Œ€Z•¶¨¦$IÂpuÒ\]žO'Õè/ºwûøñÃûeQUÕôàðvQUBtëøvY¸‹ÓÓ×_¿Øl·›f$“‚ ŽÞgÖA.90SJQo²*ãèERQL$)_³æ[Rç&Õ7[å]0Twp$Ê㪺®¦“‰3fgÔK2Cž˜5²D’DÌ{^!¦˜-3ÖZ2–|SJ1zI±(ŒCŸR,˪(ŠÑUÊûÚù|ªReä$¨ÎßUI¼Ë´d#<çk2 ù¸«RŠIUÈ ˆ†è‡qTÝM‡œµ„½†g³ÅÁÞb|ž–Î1™{öl³Z;Wܾ}\–ó¾í“Ÿ’—jQÍ&óÍÕŠ‰ïÞ»ßwcÓlïM´'yЄu1ýú«“‹³Ëä8㬵(¢"EáJgTµiš˜bö,fÊ4Zg3 Dw±$QÈ@PžÜ)Ꙏ͢ÖI¡*Ñ:ÇÆ¡jJ)Äž’E/ÑÛº‘>zfk¶]WU5sæG‘DÌ®0ÞûaèÊ¢¨ª*Ä8úÁ8úq ¡Àä\YMÝ7¿ý3ôWÿɯϞ‹Ä A“$Db*P‘ДEBC“ ý£¤¤™@§šã¹A•Óy “HRTDTÀ íˆ)™Ýð^wx–ü&0†³47¼±„4Îö–ËÍv«ª~v¥DDÇ!ƤJùƒDl4%±ˆÖ¹åb1­êóÕù?ýýO¿úò3V!Ä\M* G„pÑ+ 0–¬Íu1¿^­oß9¼{t§¬k"RddÛŒië» ÄµÌUQUµq!RY iÆÑV¬C³"MuZÙ²,½lܼªöæÇ‡û—§''§§m×Í&Õ‡ßxïáý»ÆXWNظ²žÖ{{¦,$-Šêâòâ.>\Ì*‡Ÿý·¿9yuÒt›˜îªïj@)3 wq g]Nà1sN§ä8TŽñÁŽÃιý´ëèÞà#FïcJ‹ùô`o6«s>>†‚ßÅ|óZsa 1…ó­ª Wؘ¢ªè޳¨}ßwmk§“‰µf}þôSLÙs÷;šfŠD˜#­Óõ½–®(‹BT»®)²1yb¨™Á«ªÃè-r¯u`òÞc_Uu½7Ï"°”d±˜¥˜úqh»Ö¬yèGC.TçŸRg­©lÞ‹äÙTþÁ DIª¢Ì5…ox™’ÿÐÌ(Š’ˆsÏl†|¯Êb[ !rQæûÔMxNoðhƵqËjêeD¨§“;÷îþà_|¯kVïFÛ®;=[_\µÁÃG}¼eÿΣï¾ó­r~Ķü ªæ‹év}UÈðàöZ“¨†çsç3ÓGA/ÚøËg›——ÛÊ` ñjµöI³ŸR ‘D}»&HµsÖkl=©nß½÷'ö§ÛÍùÏ~öß\.P©JŒç—§ûËiUÖHͶiÖMÛ'Ùvã0D*ì«ÏcsÃf³í1$G$‰ŠÄ°[Ú´½H r°œ”fvöúùÉÅÕùùjˆúÙÓÏâ£Âb:ÓùWëõÓO?ÝnI‹øå_œ…e%T‘|O Cßo׫õf•b” 1‚3År¾_—UQÖ"úÉÇŸ>}öÕ«×—›mª³Éœ@¢1†Ù¨ÆuÓÄà‡q !ä¾EQ:c€1ŤdXU!f¢’|ûÛß;º}¨ú~L² ÇtC‹H“IY8c °a$xU›>j+}ß§”êzh†Á7mko8£˜Õ}7ˆ‰} ÖZë\.1ÅècÕ¡ëºI]—åäñÛïÐÿÛÿüüûæê:% Ä lR !¾÷èqôéúêºÛ !ÌcòIÀ8Á2*{$ñ²¿X"àõõª¶%Êh±ñŠ* Y%@ÍD‹ì Ï=ïå 6‡ c”¶íú~ƒw…Ë>.Jô>„²æ# …$ h• Û!¥„É8kö–îÝýâWÅØõÀ¤I€!¦ÆÆØçÜb>{ëÑý÷Þ¯Yu(°œOˆHA-W IDATÄÇ.õ€!Af[*;2`€ÚØ“Š#ô]{µºˆ…ʼ4îlÛ²ïºÙ|:›/šm+J†°½>y¹ºAdRUwŽŽ&e±º¼¨§S%,­)ªBr¾S¥œÎØXSUÄä ¯Î^—“ùü paWëkÜLd@zƒ Øk™Ù9—ñFää}Èd•7ù­QrÇÌòÙíbŒ±Ö8K!5ŽmU¿¡º«B.I6*šBUÍ;ï1ÅŒeWÔH¨’„2Ñ8oA­ÝùcóhïÍÕ-Ëù²\Q ‚}ç YBM 0›Î åFVÌè kÌnöÇÆ³a« MÓ(ª)l1H”ÖšÉlR”%!ªÊõjÝu&>N1²‹ðï¶ù½ ÑÀ $AcŠª¶¢Æ˜aڶ͹¯.¶ÖÅUSÞd%?Ž"⊒뺊鷞ØÜ- Ä!½¬]=ÿæw~ððñ£Ù¤º®:ãh1¯NO4uéj·8<|W˶.§A`ˆpûøþfdp:-ö<š~ãÞñÝýJú¤}Äg/7/Ï"ÞªY¥°ˆëðÕ*~qºiÛH ×—Ñ9²ÖZfRuL ” Ä!y‰ LE»NGTÖôíï|ïÉgOÚÍŠU’álµ>~L³ù" QÐ ,”4øØF624›<î*ë†!Dï‡!xφ,séì´²0nº¾o.^Ígóé´.V[Méêüüúüœ §Ø_­Ècé6ñAT½ÈùÉ©3\Z*JSÎûž@..äìì,yq9Ži>ßÛn[&7]ì_®ÖÁûMߟ]®7íà#(ÐþrT/.N™MŒºÞ¬†¡Móéä·_^^^__çK•BL1%"bÑÌ9¿é%Ùaô! S3Ge ~W 1Æ6†‹ªI’R¬J³ÃCŘbpÓiUÕl̨ªÖpC"qUUù™ˆ$¥$RZWTen³±Ö¹~‰Mö…Ÿ}õâó'Oz‘ʾOïþÎw›1L—ÓÉüÿÅ¿ªÜdèº/>ùø?ý?\¬˜ƒ’3åüÞÃo(´(÷oÅ42¤·<˜Öóg_<ûâ“/¾|ò‘!&lU=ªdû°¢M;^03м9“ï^QÆX"£‘\e38·ð!²5åb^Ìç"¸º¼’ ëˆ]ˆàl1WkQ´‰ìéåùl9Ÿ–tëö±qÎ÷¹ ¨cUbtäØ¸Ò•·–u½¾<_ÌÓ©u.*ÄM„b õ]3@kë™"k+ âˆ¶Ñ¯¯/¶›K±6<¿{çh99>\¨0Zcì¼ß6M—‚ú€)ÌJ[MêIabð™èTeUW ”v“,£ª®¨‘lR It÷}a»Ž;Š鎂‰(7'HÄŒ¨Ìº„†>_¼—œâÈè©7TÂ7)ì%‡\L+¶¶(\Ñ÷ã0øà+ò"Wÿ9øñF …Î9&2Ìu=!Â~èv”×<ßDk‹z¶˜ÔËÂLhãÕäêÊ05ŸL÷˜â¶éÙ@ˆ²º>Ym“qvoÿèâzu}}‰Ä‡û³oÜŸ~ð¨<œ Ë¯D€LËù/>oú›géÝ·nïïO^¾\={qqyµ ãàƒIea1Q"$BÔ$‚`3”;úár³Q6Ž@JÇGwŽŽn}ryQ;CH>¥«v;J|øðÑ‹ÓÓ$°wëÞýÉ¿›/—_<ýôÙgO,Æõêüz胤âBUM kÜÞr¿ïÚ¡o¸(ŠÂFI¾o·›vƒ+ʽƒÅÅÅé¶mUbî¸ÄQÔ2瘦((!yBjG°†$Fª’ªÖuµÝ~jœ-‹Éøÿñ?üá½»÷ï?xË3ãªÿô“ÿëìäR“^]œ#Á8¶ÎMbôqóÀy6›Ý=>Þß[|ôÑÇ]?ªh”ècB$K<Æ”O€¤ †ñ«çÏþàÇ?. UÛ¡+]Q×uÁ:Ë„Ãàs‘•Ø2ª³EᜈÖU•I±!ª‹â¬s…õÞç„…a+"Ä4Y,†aȯ6,9sA.©j —Uyç޽˓Wð¤ìøÝïýÙÿô¿Ì×í&9Xì`5üþá¡aûó¿ý›¡cðmoýè_þétqYsÎQ £%cÈÜ:~øÖ;ßþß›þÕWŸ•„ôkÓ$1ï<~àCBS¸|½uÖZgÇÑ#@UÕ†, e*fó9g”¢©Jªê!iTžOæ†8„4D)êzŒ'Ð;Ç÷Œ­D¹¬Š¦ïÆQ/·kr,*È”(r6<·|$ŒW§·y!ƒïB ¶˜";Œ!?]qÅ”<ûC š¤ûíæ:tëé´X]{‚ÛË2U– °o‡óÍzýÐÃfëciœa”è©t³år2ì-—ÓÅÞ d€¼ác 1ß×µŠbd2JJÆŠ*î á9Ë;'ºˆqž–äÝi&Ï0±1†oꦒ"1½édæšU aµZ£O‘DRÎóÞtý3ìERIiô##WUeˆ(’HÕ¦]°DTE¬³)EÐ, ÊsÉì`Ë&‹: 9«]òê…ò;ÅÍÀ2Q !t]¯ ιÂYºªg´(„$H™†qH)¡ªµ.Ÿ­UA˲¨ª2¦Øwsyyµi·Ì¦( B‰!ĘT4ÆÀ† †LªÉx5 Á·Í†íZÞy<2˹¬ª£Ä€>x%c²yUTSJ}×Zëò ù†’]fÄÖº¢*ŠQ¼FïQQ‰U (*Cuß^‹O&%PI¾wÄUÉUÉÞÝlÚ±B³b?+ º˜ÕyŒEƨ×ýùUr¹…Ð;Âû«¦oÚ†%CúÎ0ø€XXóE®P$>}c ÅàÙØz2ŸÌ—ÛÍ5jôƒŸÖSQñ~¬Œ1ÎŽ!:ÐëÍõùùY\Þ=8º³xÔt½¤¡%‚Õú B‡Š‡ûË;Ç·«²šL&''¯Ÿ>ýœ,qìú~Û4ý8 10)‘.—Bs‚ÚfŠ'a.…ºÂ¡¨ Ƙ|†v&M;Wb £e<\N'Žþþ§?=úóÿ¡ru]Um×/fW—gë¶Û2ª&¿¾êÐ2PˆñììÜ¢GbÀ²œL&îðÖ-=99%И’r}üøÉdFÌi çˆ-¼÷[ßš|~BSÖ@h»>%EıCðÖYV…f»¶ƒE¤â8øªªTFéºʪD¤f³M È”Á“UY cHˆ¤Ißÿà[O~ùˋׯ½õþl¶( ³ ¹OІÀ°E0{÷Žÿ3/ˆê¾û»?˜ÝzØE<œ/¥;BËvâƒ÷)‰¤z1ÿÎ ¿øâ aBé™LUÕ!&óÞ»T lQd0af£çlÜd:q®BtÌÅd¶Ñ¢,ÙYtEìCäb:Ÿ`ÂEå&ÓE;&¶<™N¸,§"¡ï›«‹Ó«Õ¹÷R>±&"Œ1äOZriÃÙÓ³—c—ûKÇa¬q"Ç £O!ľ‹)ÊØB6؆ ¾ÛöWçÓ’*˜\´Ñ9ï[æ*cØn×›Õµ„^%ÄqÄJUacŒ ÀºÂCÄ(’ú®“ ÓÐÌêGdI9ë.ªH&ŠdzW¾ê¤!%FÚ•qxwlLªš£~1ÆœýfÆÝ’JªêZňȘ‹õùa Q Iú˜v9oâ’D )kKwwÕ¢ª^™s'HE¢Š”…eÐÜÞ•¤Trq,+2¢Hð!û.T’¤tëy˜#¤œELù žÁx)‡8SJ†©(!¥|-Ad69"•×Ⱥsi£ª†8ËezÛo™“H×uÙ’çK’©{)ãvÉ"UI>J“9d”W cß)¢"ݔѹ¬ŠÙtQ”åååU‚¸ÓñÎì Bcm!n¸•*ªd˜1¿¡c !øcÁIbB‹d ""ëKL½5voû^´ñãl:+.ý ¡ö勯ž¿êûQCœX »ÁoúÐ$K›íFÆ!®^w›Ke¶,C”Âòv»e-Õ!§³eJš î/c×8cêjÒ÷ã—ŸŽLï¿õRšÎÑûÉ´º}ûvýùÉ©¤x½ZEŠÜöÛ³³—B ûûË¡ÓùrÆŒcðÕdª*wî-—óãÕõùz{-*!¥®ï3NB }D¡(˺*µÄ, ß6‰¤(‹ƒéÞ8 MÓ„”@1ÿÛ3ÐÖ&8ºµ_—ÎÞ»uðöÛ^œ\>ûâ³Ù|T“a(l5/áLâØÛÂjˆÂ9äKä¬mÚö‹/Ÿ9g‰ˆÉDÑ·Þ~g>ŸO§Sï}]UÍv{½Z'ACŘÐuý`ê € Wá…™_ž¼:{ýueع:3Ú®k»vooo:©Sˆ'§'·o¢¾ë./.nß¾cbW½zõúð`?ÄPUÕåÅÅdRûènýÕ×®*VÛë·Þy÷öûÖ–V)„CÎ!úüÄ+J÷·û×÷ŽÜ6®2E…Œ( È)ééåXT¦¿ù½?ûïÿçéþ­&¤í°UfÂ0Ž•³ 0ú u9ùý?üƒ÷¿ùáOþ·ÿìãCXV{Ò¦ª¬³…a›¡µ¹ºIH€LÄ®(‰,Ûª,kR`g-ZkK*uQ-Aƒ!Hb€¢(\ÉÀÀªIcK€•¥‚áÙ“W—ç%¢P1khʲRÕ¤4™,<|‹¬ËëGïǼ!ÕØ‰&H1 } 11XgûŽ„“9²¡a{A±Ÿîí—E•‚÷}ÃúŽb'¡“0¢ipŒw?ÈÎZf Q6ͶR(§ö5õ†  ;˜²á¬¿z4J"$BšTˆ‰Pé"õ·jÄ#«jFaŽ€(xïó¸9ƈ;DÌnµ—TcˆY "¢»u÷ޤ6Lóv2î.ï%­1’@ ¢ˆÆ¤’KÚy^AI²CÙ åÕŽ‡€D¨º›}ãÎÞ(ù唳ù!û#P ' w±"Õ$ È,JI³üµ*KÆñæbÇ P'Â]Š"™Ä†DY#©SÚµU‰™ŒAÃü–Ý¿y9”[¹Ø%ìˆcÆM'Óù|Þ6íÐ÷!xcŒ+ bd2¹øš?Ü”’Uûfi†ÃdW16Θ"d¦¡ˆø”€È$5ìýÂúFê2h“¬?zöEµË&µ1 1 qÛãà!¤ën•Æf¾œ3Ûè“o×}»^  úÖ0“µÛ²o»²šjJÛ•„±(lšÆ¦m_½|upëðìü¬ÙlCˆUY¦˜¶›M»Ù($LI5M7¼zýòç¿øûïýà‡ÝvÛn.|·&H“ºjΛ¦k–‹™hlÚ­÷ãõõõõõU?´1Œ³ÙŒˆˆ!¥ˆÖ92lÍÚ–("’jô1Ä@˜ÊšÙ],&Ì [ìí-D¤ïZ()Æ0 èÁ|v0Ÿѯþñ‘íÅùéùêêð¯~÷÷~÷juy}úÕl>i®| ^4 ÇS €Yˆ–!Ž"á믿* ·ÛVăŒÌD¬tb|¤˜‡î¨þj½úþë_¼zö%…8Ï]Yl6 ‡˜ŠÂ…êºj›Îý8«Ëqèç‹e³mªÉ´é@L)Ü¿ÿüìbô>Æpxpˆd|CôÏŸ½øÃ?úãÞ"Ö P‘éz}½Ú¬È’¤py~òWùŸ?üþ÷?øöwKcRJ’¢*^_\¿þÒ×@ºÚ¶ŠìØj.·Ã‹‹×eQ„~C‹¢:<¼]ëœK*ÓåòÑ»ß8}ñ<õÚ›p}M†µd "KHÄ;ÍBR`f B&ÑSdÀäGÏZ&EÀ0tJÈ$4D…Æ‘Ê.@fÅ"!B# ãýÛÇ—Ï'¡ëRˆˆÊÎ0£µT–NCTgíÞþþà#å@2hß5FF`Òyåæcå #€D?”.¨&$ŸFðÛyUÞ»ÿ`¹\ØÂ(C~0’4zˆ^R´l„A42‚±®,Jk¬u™"Š&•KDwˆQÕ$)2 ņqÈØŠ8Ù™P…Éä {Ñdï…ÓΛª ˆY!½›È©&rÎeèÏ?C‹ ªh–Kä–PÆ¢áîwGñtI™¼bIšð†U @ÙPE…ÜlJ*…HaG³'’¸jïTÚš%ݨ D»+—ùíù’&E% €V"‰†˜ÁZVUL ’¹¸Bˆ*»`”BŒ1*‚uYàµC´çëÑ› ¿1ÌLY£¡)fëê@Šˆ s›i·[•”ú¾Æ1çšD%ÛÓUs€r»Ýæ²üÿ(Ù@%rpóy11h‚ü^I!ôipÎX&R!$0UE%tëas}áJ£ h´ì"Ęb”Hˆ„Œ˜[ !&‰ªc#gíµ569¿¸‡ncŒs®´Ö‡¶šÌârºwµÙ®.W«ÕELÞYóèñ[l±iVOÛkUlÚ-­//0†IU% %)‚4‹½}Âß¾Ì1†ñêâìz³‚´T’BJq‡®ëEÒãÞÞr±Xt]ßwc?VdÃÆš$ ˜æÓE7ŒÄd­³Ì“º>>:û!†PWõþþÁ|>Ïßü®Ù¶mÈ …3}»MŽAŠÒ¬V+ ñg¯¿~üî{ôoþpuy:´Ýf³MC kL’ !*¨ÄÄÆÊè/// wóªÎ_$²ÌF%È«W_÷c[.“>zÆd ³^_ŸàØ[áú´‘‰‡Fý˜ZËÌ*¦2 q8^Ÿàvì¢Jñ~|º½R ¤‚ ¯šëùlĬÍé˫ˋwÞzŒõÞ#çʦ9ÿ‡¿ûùÉëÓÙl¶?›|ñÙ§Å|þö7>¨Tý8Ž£A>úÕ¯^ýeê×qýRÐv³ÈÝêêã_þB’MãC·°÷ýßûý;wî"ZR¤;w«étÝŸ ˆTueº~(ZeƒxƒS 1±1³ÍÍŒ‘” #“÷>DeãOêÇ„¶ãÐ0 Y® ù1 Àf}ùë_ü]¿m‹¼¦\°JA‘@4¶ÝúÉ'ÿ¤l¾=ÿž³F’“°¨ÊÒ@]غtLª%¥1!åÁqL1¨W÷öŽîܹLm³±”"J"B2Ƨ LÖÃÅšDX‰Ë¢šL榨:?ëòÕ_rãT3ΫGU”¤ÎºIU ÛétVO£„l¢Ü¨D‰øÆ°”å-ÄÌ!†Ý³ù&6BÌ ÔøF÷¢7=~PæljŬɶE‘tã~$Vtea‡¤”Hs'(ç ~ëÚƒt#¥QFÚZvSŸ9 STÉaL‘ «{ó¿š“'Ä7b)DÉfÁ¼€ÊÞ» C7þñ jF»0e>ûnÍ€Öº›7xhg@´ÆdR?Àó›GGÙ’cÜl¶]7ˆ(3eõJ¾ª³D?x˜LªüçæAÓ.J”ïEH„d˜uWÃNÙìã£÷CskYV.3($çÌdZžž©&â”ï~`YÕx‘’JW%ñMå†4%²0«hFŸ¢0é0´Áû0ŽÂd¾˜•rU—uŠ‘T—ó™3¸Ù®} 'gÛvÒ°°4Ö½>yi‰¾ùþ{óéôzµòƒi6Ûõz[”Sç¦ßüÆwÜ{±Ï!´ ߆f{ú­ï›×/¶§'¯'‹ù½ûw‘*¼íïïݾB¬'ó¾÷]ÛeqëøŽ2u5x¯*ý—Ë=ËôàîƒÛG‡¿þåÏ/Ï/êÉ/./ÆÑÏO_ŸžH ˆ2ŸMïݽu°7†m5=~tïz1ýúå«Ç÷'•={ýõÑÝãþè‡EYo6ëëW¯4…|øafÈž7(ȶ2Ã%æs^eü²€Æˆ ¬jkœ1„’Ç€××C³)d$kˆÙú½÷ª@(¦­)œ-™0FAP!O„-‰&…h(/³`ì.HMY‡Ð÷}[–³¢j¬ŠÉ·ÞÿÖÂNþ׫ËËW_¸ÒÖµ[­VMÓNæÑºbðé“'Ož|úéÐ4)JúðÃo¡@¿m6íæüåó³çÇ~0IC ×§/.O¾þ“ûçÇ÷[S5Ýøüë/òOHBÐ$MÓšß|ôéþÁa]Õ³éÔ°iÛ~½^7í6“ ‰ÙUDPѺª¯VëMßRJBÈYe¢Š£÷Ьùij çJ‰ Y“}³¹ J&fL’2{W@4¶Pâ¡@µžÏÎÎ^Ý=¾{°Ë}ëx:­ãЧ82#hÔà% (ƒƒ HL!ú1Æ‚þåçý0N§5 ‘RYØÊLI¬aÜlÏ·Íù’s3[ˆX–ŒQ™À0 Æ\ÒEt¬dÈt›ÆXWÛYAÕz¸Š£G…½åòÖá­ÅÞáõj½Ý´;ŸÀÎY·Ó%y7lò‰Rw @Ò?ëï¸4 YäF³ÿü;…„¨»ÁtáÜ|6›/—e]õÕs 1?uA5¥È†UóXf§ Å62º1”ä=¨‚î\f `­ñ1†$Ùw0 €Ä3“2Æx³¼ï‡ Á!±1!„(æf„L|Cë¡üÃLÌ”ŸãùáþF±ý†¶#p@L;ñá¹0/BóÊTDÔûAµßÉwþõ U!¦$1åéÁ›nZ^™äÊ !ä³Uá¼=êC c´lK¶ .Q"´aô=¤¡ï׫ë²,¬áÀÒ犘¢Ä‚„4ö!€¤44­„4)ÊýåâjµÚl·ÛíдMJioïòâ¼ošÑYZîÍ÷–c ëë Bdˆ2›-È•CƒrJF˺øâ‹O»¡BÜ®¯›í* µßüàÛ>Hß…ºª-Pû¡ÝôÛzÒTUEY¸v³‚/ª-*¤år†0w…±ÆTådoÙVÕ$Æ ®(¹t«-æ>P,‚HÛ6Ó²zõâÙ“O>qÆì…ËÒQ;3Ãv}.!ú~ûõêüääù£;ûË’nïO7›Í'ÿr²Ü;¸{ßTæÑ[is}þ·çgÄàã` ‹Šª(ìv-ªj,!ál6«ËªÙ6CŒ±šŸû’Iàÿ/WoÖcÙ‘¤‰™™/眻DĈŒÈ=™LnEÖÖÕ¨®éêê–ªÕÒ˜'=ègéQГ híÁSè©.umd‘Å5¹äž{Üý,înfzðsƒD'&ÀÈŒ¸¼‹»Ù·’½sçÞx¼mÉ jR9}ùêƒßý˜2X',A„Y(Ê 6L!¤¶[:_õºœo ‚”;ºTåJl¯ìma–‹ùW¿øÞ»?ÞÞ.Š¢L))óz¹\¯Ö"ІՓggâýÓgÏOŽoÞzÍ9W¸òɣǧ§§»Îˆ 'ûågŸ(éñ³G—Ë3^^jlEÕ¨ ÁËÇg—'¯&“C»UZk¦Ó˲ª¬÷m@椶‹¼X5ŠmP—'ÇÇu];ç@8õ×£+ik{§ OÎÏ^eGÔ&… q¹öI‘$»Æd¶VD9ET†\bdˆ8¦„à¬a4l §lÖM“‘‰³ÓÓgÏž¾óöØ{ï­©œµÕKlëu¨;î 5¤F¤KÜ&ÖUÓΗM'Xn¡jÚÞ "ÇBÃumqà\Âj6?[w˹  Q]/Lá×ëÕ`0.‡[¦¢5í-ï‹ùtÙ®;î"wÝãG_½zö|]/Bl­µÞe5ÈbºÄ)g1ªæqÛä¹õ_/!„ì¥ìO(Ý1ˆ¢›ôè\Ï*,¯€¶kE8ksÔ¸æÙÑZã½TÕþd·K‘cÖÅrã(ˆ³}ÉÆUßÅ•à‡ íìì ƒ£W/ \uÉ©µÖX«)ÂftV‘,ºÉƒ>srΙ\%<Œ*hÛŽsçª5‘“3ÖIŽÉD2Ö²öªÙ¶éƒ4›Ä%ýޝBÞ à&šF™9ç»æ; kQ1b-Ë"³½³Ìä¦Gc­Uˆ -™üþ !\•ö]a5êœóEQ¾( #‘IIFãáÖNUÊ¢,UXE’>;99~uœB0U¢Â³Y’%ƒ ¡®ÏO/;–SâbG[ãárÝœœŸ×]ÇJdÜjÝ´m½^¯Ûõj4FæÕz¼/*Tà›º9¼uëîý7ïÜ=99þìá§·îÜ C;ÃÑÇ¿û­*ÕMcÈŽãwßûÉp¸—bèš®‹éå³G¡ÙQn,i›‘zç½÷ýšÕ5fMëõ|w2EÙ»b{krûöÝ¢½z9›^8çD ­n|1|ç­·‹¢øèãO?öäÉc -¥xqzìœ3’¶·'(¬çgç«Å|²³S‡" HÖÑóGŸÅŠ';e ‹{÷ož_ξzô+pP¼î¬)*sïþ­‹ï¿ûõç/U0׿d?s¶€0KV”‰Èt6UÎS‚"mªnÈ(ó¥!$, ¯HgçsŽ(¬ Èœ8EÉ -2– YKκ¢(‹ÉöÉù”Kë-`Š©Ï¬#¤ ¢Q:Q¶÷ v IDATHdÀëܰ³=A€.„˜’J²ÈÊÎYª7vwïÞ~Í[WŨÿä‡?|ùÍ×m=w)¢†O¾œž±²’Fi%µ¶Ï™Q4Æ(Teµ»»;Üš Ú·ßzûŸ?Ž)åñO$Ùݽ=²Žœ ,DÔ¥x9_˜”ã# ˆ·ÎYŠ‘ë&¾:®×µ 0"p€Š`RŒd-$d,’«|¡,̺\ï‰QDc„sb&§²4èm‘ãŸT„^<¡Œ¯½vßMvbˆ]Swí:´kM-†B‹Sl#¯ç«útºº\uª&I¹>e]¯¦çÒ4Äa<¨ªÒ—¾°gË󓳋Eݰb Í|¾¼<;¥Ä D‹bHn ¶o]áÛ¶iÖ )å'k½\„¶éÚ&¯( cR 1¦˜8qrÞkŘ+{jJñjÍ'UnžÑœ}…Ñ÷aâ @hɨ&¾º ãw(‚ æ&Ekmá]×6Ÿ<•˜¯´<ðÝVnÝÔã]U ^ir’¨1–€SŠÙ'‚*ÈÌ"›¼þ<5K¶‡€ªE2€y¨ÊÁ (‹Ò[W\^^6m›ï­Lf¢!A(ËÒ£˜û§zeQY–¹Cæ ùÙØG銩Î9KþPT”úÒUE„üÎ) _ƒœˆ™‹Oi“ž¡ùœûjíKvr×5ä¼?ÜtÎeqx°ïÊJ“8°[[#$ªë:Æ.´ñ¢NE$5êÔµóéìb:C€ýÝ}U]ÎU‚Ç£ñ`x9½xq|òìèÎ;wÞ¸­(õz…ˆ{û{íºùèƒN/¦ãÉÖÛoÿüô<±œž5ÍʸˆºÈuÓ6m(ŠqáË®ë‹+t1´¡±Þܾ{÷Ã?|õêøÆõÛ¹Ù«³g>' 2æðàÖo¾óáGŸ¼xòìïÿ‹_>|ø°õâ`hQg´*QÓ:tdÈ{ߥ¨ u³\,¦ËÕ6p$;”˳£¶©G[ÛëåRRTçÐZ‡e3_¾˜~õâÑg‹éüù‹«®Ž#·±­›ÓñÓ§€CLQ®í_¿qýÚîöŽõ2n8Þ©ªb~9­—Ï„üt¾(NŒá;·o€õË٬ي1îõ·¼þÖ›‹ËE×v›¹AˆÈy+¨ ’€œsÚGò)ænfP1„,‘ކ[ï½ûãnÑþoÿóÿd4ÄÔÄÔ³¥u§Þ¸ÂVÖ–7ïÞºuûvùâåÅléÈÅuƒ–A$v­µb`ˆj,„Ä,ÈÑšâúá-h𮋠Ñó§Ï¿úìÓ.´ˆ@iX~à#€ÂÚ7¼¹U ×ëK£ ‘I!\Zƒ¢¢)¡BŽÏdÎç3kŒ2Ðîî>%‘ÌA)mm¹ÂYfmŒ!3Œ)YëÐ J_¯ õrÙ¶µ2Â\B˜Ók¨O£µn8Æ[ç—)´€# “AD*¬7dTÑôy,,œÇèK?Ž‹yÛ4šå|ú8Äåryëú­QEšTÚ®Y€‡jD@Rh]·œ.×—«°lcŒrq¹¸8;oŸ>y2½¸,}€ÞšÒYCÐÆvÙ-ë. L)ª7—*Ùie1*Ša2¬²^­Û¦uÖlMv¶¶¶G£jÁq½ŽV1”BH‘Ûºi‘Œ%0ÖäX£«Séª)e£5TC€Dy*ýî)œƒPD•SJÎ9À«ò¹B4ú`P @ÈX6@á}ÓÔ]Ûn<âlßsýuu]‡2û”Rå‚rk­"ÔëUÛÔš»Œ8êu>¹@5ë>Azؽϗ߸Lm¬Á°ˆˆrê‡úÄŒDmL11cŒqÖ*YƒH‘Sîßp.7yõ6)¥ŒtçÌ»+—/3{ë@úy¿OOEØ4y«ˆ†?*_®æ¹Ý{¯ºYrX?sŸú~Ž)¦˜ŒËòmÖ«Ùtúêå‹Ç_|C``¹\®ëUŒ¡k»®];GÖ@aqwg\”N8„‹óó®mwÆ£7ÜÿËŸþôôââý?üéÅ«ãûo}ï­áöØ82„Dpz|º¸œiÒ.…—'_?}úJ’¨äæó˜BX¯V'GǃAÅIÆ£Édo‘˜_¾z"ßY,½|õé_à}ÜÔËÛwï\ßôè‹Åôt4ðw4Ÿ~úùÿð·ÿð‹Ÿÿ⛯¿° Õ”Þz; ¡kÛÖkÍ&gcÉfµZ4ËùÍÃkVíÑÑÑåt ˆƒ¢<Ø?( 9%æÅr1_̼wm‘•tzq"…uMb’ÄmÛ1sU«²,laUMe¬‰Ÿ<{Nivs^gK_]lOv î¤^¯Žïï¼¾½=¥[¯Ýýô£OMS FPfÍùºLhŒqm—T‘,€"˜ µ¡±ªˆH&¦øÍãG?Y.ýxܶͺi‰\i}汬§hÑe9 G7nÝÚÛŸìMöÊj´C®b¤¿|ýÝéjÍÌ)Ī(Û¶^­–•5ÜÖ—çgǯ^œŸŠ 2CLŒ%$fF“ f4¿ñÆ'/Ÿ­›FEPD4 «bˆ€ÚÆËrÐ’·êX ,h"€ÌÁåN8MÌ"XU£® –Pêz ˆÆØÄ ,H`QÄ©rhšÙt±˜ÎŒ5*š«iPäÞ2Q€$€j`Ó€¹PÐûr8M&{eYž7M“µÌÊê­7† í:omæ UU8±°Š¸²®Êñd²k[ÌçÍre˪ŽÎ.çOž½ÚÛ–F‹Õ–@½1 œHpIZ6&U]ÍçSæ”b0¦ (˲bá&Šu¦SLâÑP꺶i@µ,Jæ¶ír ~Óv,JdCŒMÝBv.u¤JclDbæ'uëu݆$ª¾¬û¸o›&GÁõ|£Š19îæêˆ„³tçÛ¹Þ9 @‘SèZk-*DQÌYVù}hÈÆÔ¡‚!J¹ ¾·0A_+š=TŠ]€èœEÁ¬”bVnö9 Ê‰%#év#J)2s¾®z¬¾›išs3Z¿‰LÚ4oQÎøgÐ>>3sÉ9ð'ÿRFN©tgkÍÖÖØµÖ‰HŠ]  "]ºœ.– ÒÔíjÑ*T´©Wçç§*b9OÎõƼõú½ƒƒ½ËÙôåó1Dï·­'VÃÇÏž=ùú©unz9ýÓûïÇ£rà‹Â¯×«ét~z|ÆIšϦ—Ìl 9cœ71„”¤°n\bë¶XûAwxóÆÅâRšn6_ýøÓgËõÃ/>ÕÄŸ|ò§¯ßÙ߇ŠISJœõ=\‹·&û“f±†®ñ·@‚Ã8, »f>›&" ¡‹)¬VË:çœ#›$v®(«˜h:]—E)¶m› ²c"ëóÎäœ3dÛVÎϧ×&¢²³³ƒäe¶GãÑ(Ômy`gnݽ9ÜŸz»á·T"³ªñh÷nÜY¯ÚýýýÓÓ3ç *Ê®kC³ö˜Û.ÅP¡J÷îÞÙÙÞv—1ªBÛ¶—õôäâÜ••6kCE鋽k×÷¯Ý¸qëöí{w®]Û»qóVˆFѵ†“É`2\Öõº•áÀLl#)3~úñ—G/_‡ãõrÝt­ˆŠ¢jBG¯žÞ~ýuçJŸÀƒÑø‡?þÙgþlºž!¢€œžŸŸm^cÑÅbñû÷ÿ°X×ÖV:f–„œK{@1wþ%Ö”°^.­uÕ¨â_ýõzµfÍ «‚Ngƒ.„¶Y¯×ËUÛ†€Š,쬃 L›¥Úy\7Æ(±Þ;ç˜9„R2d fžN/bL"‰T9%Éa¹~ÔÛ¾ì8£À_f–¦®»QØÚÛ»60 èh0¼}ï-$szôr~ö"1+ ‹¬êšc4†€*ô5ÔùàâE’÷ŽÈpŽ"DKspÎ;,œq¤ºbäså,µ]‹ÐYç…4Yˆ(,êÅ%ç#8‚1cÙ8w…h_ÖyvÎé‰&‡ñÓ³½²¾ÇdôŠåÈroÝHS ™l«!DÍÃ'3" ÔÄ"ªMÛ­ë&ÅhÈ\m,* Þ$âÔmúW{è#ŸòÖç\ÓFgÉ&×6^¡ó=."ÀÞ{AÎèf¾2¬s™ŸU€ï¶T3sß.¯Ì"Þç ¾¯ÔéÑ^PDÌ"÷+Ž7Ïï¹Ûë;þøC– Pþæ­Û€(1iLëu·Z71±H²~C|ñüÅââòp{ÚÕ`< Ñä\ÁƒAJi½^5Íš9ŠD$ARcÑxDcŠÁ`0[…˜ºV‚¤$ÀdIëùÎöغ‰óEn¡räöww¿9vyq~~¾µ»»=ŸÏ}9ÈŠvïǤ)9Õ ¸vãðé£'º1g³*ºêíw~ôæ›ß¿ýàíÁhk<Ï—«ÅªùòëoláŸ<üâøé³Ø¶;[¯ßýOŸ|9޳Ñ’õÃj4Þš-ëß}øÉödw°U¼xúh°½sxïõ7¶'“·î\¿~c4Þ=:_¦:ñÁd»ØŽ©NÊnìØctQq{Çýê?¬/ÆãÙЙE…9´/ž?þ+ø»Ì”%–ºkr¢îd{wöLAÅz³ëóùÙkœ ôÖ¹Ýí˜:m[ês0Ð** h~ƒ¨5䈌JZ.æ·îÞ½3==~õtµœÎså§]/Ûv¦³Ë¶í)$Énc@Én¬ÕË5DJÒʘ9ôî>­ë ÎÒ@4WAUÊ9BUû6Uø¶Ñž™ÉQJi>Ÿ’¥ÁhtóúõÛ·o?yüäÅ‹§¡ioÝ!ˆee994¾iê$b½/œ«×«Õ²Fg÷.¦¤ÊWáÆ€R )¡Š&aÑ((‡!’H¿+ät‡L䆿ó™uÎÞXë‹BB _°$$à,¡E€RW¯ÏcœDEðEUw© õru]·]0htݵu ƒÊ o„HT$°ærÂÌ(pbT@Õ¶iÛ¶[YãðD_<{Ú´­¨6]Ç ¶ôów¿zôäÅ«gÄùj=›-_¾8ÚÚÞyíõƒªxqrA~´µ·S·m9”ÕàÖ­;Ÿ–Ÿh×IäTU£wÿâ§¿ü峿{ÀD)ò`XŽv¶¯#½öÖ›¢þúoçÓ_ÿêW/¾|ØtŒÆ¯š¶-F[¥1ÆÝ½sÿ·¾ÿñ?ÿã[7öþöÿv±^^Îguè¶,ÍW«]¡g'Óý:ÄeÊ)/_µdÍhËvêE7ÛIn W+FÿÅÞ›].öö÷/gçÞƒ(uÅÏ~ö·ƒbû é§Ð,gs_cQ; Æ·|OXï\aÜ{oï“ßýáh9Í'„*õb94¢LhPcOQ¿÷Μ+¸ëâôòr0ͦÓ+s(ØÅbu®ôFÆ 1´O›Ê»wî~Îòa2FUº¦NÉ繯( "jÛ–Eœµ)%IJH9üd<«,,YÄ } ·¨ø>/W9v§ÇËyéïšgϞ̦ç¤qyytyöªm‘P· KRNÂÉY7T–‰BÛ"úÔJ䨅/ˆˆ'ÑØã ¢ªSä”2InŒ#„#1"a9p·®_ßÞÙ%…““cg [¥Â Tr> °¤ZÃÉZWš>cY¯¤G#’ˆfÞ)§£ÊÕ1ÝC¿Wó©þë_BhŒˆ€µf#lQÉ"?í Œ”%K\ï€p%ÐÀMêæ/±äÄ.ƒ„Ù—˜ÏQFK$ª˜":c9„𓍔e‘ CòdŸKr.l2mŒ±Æô¡\fݸ{b)™ þS”V„Û¶5†°m;"S4/:úÝ »´·¡] PD«ª, ßua]×)±19R-çŠà•üqµZu]“½iªÚvun<Ùq€Þ÷ÝŠ!ÆœTUDêÚÄClc[ÏWÓºYYkÈWå`°å\5*Çõjc T_ú²YWt]cˆ±K"1Pc ŠpJ¹Gi<ž¾bB×5ˆ}=vm¼up3q8½ùð‰ÝÃ{©[=ýæa»Z—EˆDêÛomoç3‡ ¯^½\̧EQäûf±X´mS°Ö 8¥íñp<Y#,L(Þy¦’ŠzöE2B¦¬ÐX!2Û;»«î2¦TU¢Œ2±*wíz:;·Œ/ÊÁpvyþÉ'.VSA¨£\.êÝðäÉÓkׯe!|~yyE±ª[òå Ü<¼>mMë•sÎïËíÉä¼ïR’¬ëv:[ƒ¡4]·39<¸y{÷Úᣧ/~ûÛ÷g³OÍ zöòi۵í£º€‚àÇׯßúc‡Ïž"lÖAãŒ'cY°ìíîü›¿ùëÿãÅKî’A°* 1IÄ*¡(ÒÖî¤më'O¾yë½ïoí] 1u!0§Ðµ97 ír¹ìÏûLE^IôòÙC”!MÌÉMª¨ H˜aÍ|”Ä11Æ\’ ¢*BÎdïDeI’RÊotU$UQ4–”cÀùô訪JoqkàPD;[&ë(¦¤*–°ôåÖh«,‹õjCm umÛõ%Ίý )Z%±$aI1÷Q¨‚’r QE s$Ò¦©+„YY•@Es¿3Ä8@å>H P@§ƒ·…÷>WQ#(mLI䋢뺦iR ÎB“5¹ø­’æÛÃ=;w®âp¯@gÚD;2ç0‚M©ˆ²ät‡É(§Éw㪠ä½ÏENý3¬ 1eߪôä¹û†ò·Í[Eƒ-­ªªëBÓ®s‡ÒF™‘EŒ÷¾çî¯t/y̪‹!„8e)öR¢LDÓæ¸&DCDŠŒ}Ê>ä¥4“ÕWÐÖæ«dŒ-‹ SLÒ':ô|†s.ßsùˆoÛ6Æ”ÏÙ¼;$a‹±ë÷˜\zƒ€¢ ,ÂÊQAbŠ!F$rÖîMvÇÃÕçŸ~â,Æ ªh (Ì1%ëìîîîjµX­×œòž¤Šh¨*«×^»H1´1DʶYÇ šÂUÃrë×ßš/¦}üGrT†¿ù¦ÜÞù‹¿ú¹"5Ýz:;?8ÜßÝÝþóï÷üqP”b0:ÜݔՋÏ$¶Ü4³‡Ÿ}°Z/bWÇÐM»¦W€n¨å¼Ðt]×u)ã‡D¦¬Êë·®¯š•õ®(Ê¢,EÕù2†SBÀlŒËÚUFãzëî\\\&IHÄ¢Ó)Ò3WTÛ“É«óTƒº•6¤e«Öûår¹ÚÚ;8¼~¡RÕõº GUYìlm]¿~°<=.ýàÖÝùÓŸöJ?pÖu!<}ü¼m›:Wøu—~ð—?³¾r®Ø=<|û½÷ž|þIê *uªÈ¬uÓ6MÇŠ@Žb>xÿŸµû×ö÷vÕøE“æu³UD–¦*‹¢„Õb:_¯Ëj0 fóu½ uhv¶ç§G¾øôúÎxÿÚÞîd U8ã $ˆ*9¦P„Y½s¯?xkPŒÿïÿuõòâ#¿::úéßÿ;gœ w±3oß¿¿{xøâë¥Gk·vvÇ£í¯N”J–R€¢ØF9õê¯î¼Q”£ª®›t~~®*²it";z$¨ô™0º1ço2ƒ²Ù&³Òh{RîÃÌ"_DSUƒÄlUìQÑM£(ˆ°€ § ¡ ±qΰB–Ê+ ä[„åüìlk4Ú›Œ‹Â9c (lªˆH&«¢ªª13Çá°02u1´m³Z­cRc]êl–zéuÐý‰cpÆæ?‘wŒ¡õºîZ]ô¾kñJšMd4GøŠ§*)%  Ì,IYR¦Uµ4eé½#“ºB`@cr3¯µ6FîÄMŒm/Kïº.×/‰*‘ÉÊ,dÜÒìœM‰³?*¥Yü]UûRÕ‰rC¬ªˆ·Ž|!*Ò¯}È$aϾ^å3äoè}±³³sxí„?ýü“åri¬Õ>6²Êl&ecŒ*"¢äôUQ éºîj´Ö5)¥ÙÍBFaQÉ †Më•Ä3ŸÚyëÏ Š(¡¶m—˜›®SÚÜ”)¥‚sn8¦”Öëun\!"ë,(t]—ÔeQ„®Nº|i‰bN•P²nXU5ÛŸìnïì ’>öM-3çá4µÍ2†Î‹@„è šü™ì»ÊÙG¤A5ÜÞžä¾­í­aèÚË‹ Ž­¦`U妮Ó_~¸\-T#Š5"§§'õîº׫5"ßíîÑËçþþ·Ÿ~øÁ­Û×¼õÖÍÿ{óþkŸ|øÇåâr9 Ê]ÖÂc@g³7‘Yxseªj׿P•hÈC¢ry9õe¹möÉXk]ê“5Ù1E1v][‹¦,×Í›éÞþÞµkûËåòø8Šj†³B DæÚµÃýýƒ®kB¸ÏO$p”2'ÙÔiµ¬¯Ýð;;þr8†ÐôEQÛ“mkŠîlíìT“½¦ëËÙÑéŸÞÿãÉ«—[Dóæ{?šMë6ݾ{ï{?ø¾5åÿrr>»8¿}ûÁx¼‹äš®&[ ªrY‡G¾LÒ†Ôˆ!骶ÿñ?ýãïþˆWËy²¶(MY)H§tÀ ,.ÏçÏBsaÒüôÕ™ó’  ª¨ÈX^4íïÞÿãÎÛÛ;{ ¤*«¦qE‘Õ(`Œ·góE»^yÔ.Ä’( Ç;ÿî¿ýï~óŸþßÏ?þèÍ·ïÿ—ÿÕ¿ÝÛ¿öòèh¶X|ø‡ß?úd@^»}wÿðÁ;?tå°(Þ#‡v½Zös*(Ûœ|"1&æì !"¢ARf&¤+ƒ5²À[r§¶sÞ—ƒÁ0;¼Wë¥ó.…xÅöB_UãÑVJºœ/C!F°Æ!ƒ%´Æ9ß5uÚÄI”·ÇæÄÌ®ðd B¢$ID ¨,‡!Æ® Æ`J9ÜJ §)"fSCÿ!)1sCvs…óƘÂù²ªFã3sÒD´X,D“óÆž5÷‹ôÌÊÌ@@cŒÖär³i9ÊìÙFùÞ—:!å“Öß%0¯†ú«R‹+D¾?£!;3%¾=šO}ÎbÆ(Ë.¯Ôî ùÏ+K¶8åîJ“Ò™æŸñ=™Œ´ùfÎì§(Y4†AÉYcMJQU3*BHÆÚ¬GÌÿ+W‰ê›ÜãÞ˜Š˜ê¼÷^@CYcŒ1dÐbö.åT3Pa‰ŠýʘùÕ|Ä«æ!T5¤˜³ƒrÑpV×ç—)ưZ±s.¥C’o59½-?˜+UáEÄ{okºPTÕzAdlU•EY¹Ò[çÎÏÏC[·í"†&¿+‰T“Õ$ìŒñ9è@DNN’¤¾˜ÐZSÞŸ'‰££WƸƒƒý¶mÏ/.UHT‰ˆ*‰Sd$2Æ«ªµn¹j’KBxðàõ[7>ÿô“þðþÑ“'»£‚bhfÓñëŸüù£ßÿ¿Ù ”X$Åùp@ˆ)ƒ4¬ÊºnºuˆHhLßÈÌëºD°ž|A¶p )$LŠä¼s¥ƒ!tje ™ˆÌçóõz-Y”Ž˜^†LÎX®×Ç'Ç«u- Æ:dFN0Ÿ-9IÛhÓ[aê¸k¢+ _£í- š-¦ŸñÉÎd² q0Χ—_|ùõùù¹5`AQéé—_-k];ܹv£êÍ{÷ïÝ??_45/æ5ØN•œóEQ‚õ’B ±$«˜4¤öéÓoÀù×¼3ÕÐÞk—‹UŠak40O_¬³v½|úX IDATêV³“'ƒµá³z>^ø¢ØÛ¿vqrjT¥pîõ×^ÛÝÙU21 XƒÌ$еFEEt8¨¼5$É"¨ó nßùÙ/þþÕ³W¯=xw÷ðÖîµ½Éí{çóåѼ~üê¬^&7¤·ÞûáÏñK5U`îºÎ{çÖÍñh¸ž-„ /¶^­)©lÄ„•Üݧ"I•¬³Hd¬q!±1”ÛÁˆìîdo8ÎæÓÕjupp ª'ÇÇW: @ƒåhxxýÖo¼5mµëÆÿÍ×_=yò¸i¡3…¿¶{íÍ·Þñ>üäùóãESŽÊ‰…%sŒ½q‹Û®+‹êäì,kfT$ÄbðÎeP?óÙER$DC(ˆœI­1†ŒˆXccJØvÌ11§Ä€@d ClòçU„ŒÉÛ," æˆðÞ%d ëœCPÑMÿhÓ4mÛf +å¼aQíwèkoó#A2htP ²ž,ÛÕ¾`5dIJ‚ÊF–žÁø>(¸/Ü"DŒ) Ùžù|Q»±)eølóûwĈ½(<M„“0‹8|¶^-ê¦!DéK.ó““9è}³ˆ&„„eYö5Øc&ÎÙì¹E„"e·QNšDDç !"N1ëaB H½ðæêŠÍùjÚ»Éró;I~*úpgÈZÊB1?3–löJIϦ æ]ŒDœ1ÆZf1Š0Y³=™Ü¸~ûKNõôÜi›vvy~áíj±p†¼'唌u¢jÈpŽDî[@UU!AJ CÊæ !Ÿ®»„€Ëåbµ^‡ÐBî pJQ˜Ë²R“IÅ­Û‡ƒ­ÃÊáõýÉÙéË÷ÿå×üÃïÏ_=¿±;èB7]-–³éôôXR®†Ú9Gˆ èœçì¾O#ƒEY\»vmµ^¿|yÔ{D ¡Ç”DÅ8 Æ€ªõ®(d¬(Y§)$›×M"ëìÈ–)¥ÄÑèp¸^¯×«õ!pd½sÚ¯Î1„Îx÷òø8„VT”¨‹“™¶i».Nv/Ÿv«v8¡ ®ðÖÛáx,†árvþëßüªo­Û:Å€HŽö £I¡>=>ÿÁOÿf2ÙMÂííw~ôŸÿùËÅ¢îBªÊB»ÄѲ(¿ÿý}þ‡ß@"c½u€ªƒ4«n9ÛÞ;ØÛܹ¹ß„¦r,!xçÓX½´íªË’oÞ¼6ªÌ‡ÇGˆA÷ö®ýì¯ÿöÿ¯ÿ³©WJ¾º~pÓ+äBŠd” ={öäÓ>˜Í.hbEC˜-¢ “Ú.Ƴ³³¯¿øòâè¸ ÍÇ}tóÞ=*©ªHzxÿÞ÷ß·õ ݺy @Ž2_ÌHôäøˆ%"AJI•5%›DòºŠ„9){ü2»&¢„ä‹Â:º@D4¨œ¯¹«*[[;ËÕâÕ«#æ8 š¦éÚ6D›âh¶ïßðæoΧóÃýkDtïÞí¾ZÎ^=¶^Ï\é¼óîí»º°¸~£|üä8F}ñì•ĺn¾\4±#s•ÂmèÃc „›&D#ÎY`‘”5'3PÈDVã=(#‘¦”TTXlÈPˆ!©¾CÕP?äZc$ «(f ¿ªª³9Ë2'ì…‰""¹M"ËωH %3„™j“,Ú†lšïÍP,Ùsd !€¡lA@D ,„˜E«ˆ9Y'ä¢Ñ,né¥à*’Õ颪ÊÈb­ÉH¸Ùdª\Eä$Í!ÀÄ\D%'KFicŠóùŒ›¦f–œþ HÆb_Èœ4 ô;‡lrYXtãå²Öf“ªª8çUm–3I^Ñ ’3Öƒ D®²MKÎioÆX€Ô;›‰HÁ9½@€@%#ÃŒÎÙBˆÉy—¥>YÏ#}^ÞeJZU­õκhBŠ¥/Ü{c÷ÚÁã‡_X²†Œ³¶anë„«aY•~‘B‚lÁu]D"(!’ö”*¢qf“r£ˆ Ì¡SU1F‘(„”£˜¢Éê!áÕ’³ä•e½ª÷&ÅõýÉt>_ž=›O†¿úÿضëÅésé–Ó˹ÕÀ׋s®gEá©1ÖY ¢‚ÖZMÉë$‚ÞËr½Š!:o¯Ö7q…cQSøb0,ª ’Õ|Az?Øïj–òžÄ€‰£±/|×¶©Kdr˜ ""«¶m#1 ÈÞîÄZÓvJdK_97žÏ›qéWõª®×;v²¿=­C «õªWƒÑpïÆ–ÚB”/çSßԢ’œµÀ*Ö”„”„“wNb0U¹\­Î§ zùòÈy?Û.¶«õr¹*¼Ä.9W£¡^9Æ)ZÂAawGû»ÃnqÞÔ³Éxà =?{ë³Û{F&´ºìnï\¿³ýáÃGó“£[¾7½<Û9¸ó³¿þù7?ÿø£3N€mÛ…¬þTã;·îž>?úhõG±¨CòªÒv  <šÊÙÇϾzòÕ%´‹Ë°šOÏO¼Æ{{?úá÷IA8vÓb[·‹Õ²iVíjùøÙ—«zšÚUooM,+   Ìy†%omv»xï·¶¶ò”ó99!’ª¬Ö+–´XÌ›ví;?;KWUkH½s…E«ÝôøñåÅÅ¥¡ª¬¶··‡ÃjoÛß½þª¡ÝÃëë¦UIwï^ßÙ”åø³O¿øóŸ>:?ŸFk(E‘sê³5q> ³¸NM'‘¥×Êë\é§H„ι¢ªF‰›z ˆ”¡Š”R×v2ÄlcŒ¢‚Šd¬af@D†È(Ä(0ØšlMI]õ~€›±4ÆÈ,yDÊf®Œ:\µ:™¬IDbf¼šF‰HAòé”m¢ÂBHxÌW1`1±Ä€Ôcý™*ÿØéeNÙ·Î[gMÛ2%ްÉíºR‘oü>ßfä§”$Ÿ³ÌRïý†pÅ:å;ˆÿwÑ&"c³KŽ8q~!T9„›? y(TŬæôÌBçœ"VQÙÄ»#š+"ú;q:Œ„d²Ÿ@…Q­µ“É$¥4›/$W“s®ö÷÷_{íÞ³gOçó\qÝ€…õ·nÞ~óÍ·ÎÎ/ó¹„h E )" BN‰À’$mb›U1Š(‚hò~cGêûrMHLˆÆ:W µQVÖU‡7nUUõÕǤÁÚ \Õ5sÅ& /—KaáħÇG¿¾œ–£í¯}ý/¿ùÕålJÚf ÂX°¨)„E“AQ26ÅC1 BJ¹…\…•È„./æ€9'C"‹Š’!ç‡ãmã 4Þ!ÉU@¹â1‡æW;Ó?ƒªDÔØ4ªj¬s^SA” ª€¨FMMLMäŽ% €Žã7^óÇ?úÉÉÉËõâX@Ûv®_Ûë‡[:§0šLF»fi×+Ó#  ÂJƾØnïïì?yqœSlëz5(»µ½MDï|ïÍíííì¶wΦ×—ÓË‹™Ag ±&2¾\;8¼uóFh–Âve8ƳÓçéR¶Þ…åõ[Þšö*hºo¾üâÑ7/¦³¿û¯ïÏ—ÿôÛß `9ðˆ’ c>üüàÎkÞ—$ãR‹rtûÎ[Û×ên±6àéWæóÚ J2ÄB]^.m\·m§'Ï/æç4¨ÞûÉO\5I«5ÆiŒËÕêäôøìüøÅ“¯Ÿ¾x*š1 8o†ã‘€Œ²bÃ;ç¬+œÖØl_Š1ö9±’b›XM‹ˆœøô¬M1Ee­áÄ9’ ë,‰PbèºåÅüè+˜Ù¦]w‘GãqZŒ/Aʪ, ’¯ÎO_‰/ ³[ |É¿øÙÞ|ýÆùÅåÓ'Ïÿüég«ù*E’$YAˆ,Je9pÎÅÀm×Zï­õ"R¾,}5Zçr jŠ ©Ž *ÎQI1umÛÔµˆöD$ÊÒKCˆ¦( CÐPQ ¶vï?øÁ¿ùË¿þòˇ_>}¢Ì"Ò‰H×…˜XU«ªº’ág·$ª 4&±0s \EƦ³k‘Ìf5Q%fÎð–h¯£7vSõ—‘Ü+8@YòÁM´¤¯Ø×Ü<՟ŨÆÚhaûÏG× n6 B¤Þ(¨=€flYÛÛ[——)„c2Ö´¹-E‰˜ó ªY¹‘™ @v0aÖž÷=–ªÎ{"ìm_@Øu™´ŒmŒ¹çL¾ŽŸï¤ìŒË'uŒœ ûkF!"íº6»¯3Cžáõ|¹2sY‹Å¢ë:–žñN)©BJ d–ëõ׿yøå×mLÜͧ`JĘ̂õzåŒ%c ,cL}JYc ‘ o½5$€Æ{0ä @2H$IÑúÁöÖnU–AÎF»‡¯ïÇθçϧ“¡ßßÛyöü@²ÆIŠÃ@TE!FVDëíéŹ!ÚìîÚÓGßt]à ¬)pJJPº 립¸8÷…+Ë2¥Ô4­1TUƒáp(ÂMÝ  ¨±TT%ëË!YŸw* ƒYSy¢+G†E$1§ÐIê 1)Ff6*H.ÛÕ ­V«—/^ž\žæ}A®ì½ùÆý½Ýñû\(@YøÒ;ƒ¥³¶K\¥³6X5Þ Ç[Ë錈 v±ª*뜳~4ÜêÚh¬­SóáGxý½7в(Ê $qJ BPá3ä×Ëö³Ï¾¨HÎXklbeN…7ÃÒÖó îºf~±µÝ—QRç $n›öÑÃÇ‹Y÷äøBŠr:Ÿ¢B×­ž>ýº / Ç$ ç—çŠB1AH±éÚ5uÁ A@‘À³ÙÙïÿ›b0š\ÛÇ'§ÓxyùjQL Ò}øÛòãÑöµk[[ÛwÈVQ´Ž]cÌp0ˆ!,¦Óõôr5îïî¾÷7?Åüþ÷ËË“$ÙÁÂZøbgk›Úº•„¾ªÍb¹¨× ‹ a7¤€ÁXËÂ)uξ°@ÆBï>ï¹;aVI-tëáxâRVu—ºöDÔYSUƒ.F è¦U‚¾(‹rP &bÃÉä'?ùÞOþâÝŸþô‹y}zrùñŸ>_ÌWmÛ…’èÎÎî·«b€àOÎÏsÜ‘5Ö8gcLàÈF†Œª E(®­¬ ‰Š³÷¹á(¥ÐÂF-$¢Š¢ˆÀ FEä´X2Ö•åÑùéñÅiähsßP–‘ˆ¨ª÷EŸ†Ž˜¢" ª†òPŽÔ÷ʿӋÔz4 õ(‘!ÜŒÙt”E ÔëÒ6Óv/TÏuKÆX ‰¹ËºŽ¾Jò•…ˆºéz•ž·ˆ¤š“&A¨°öÀJJ±ëº¶i6–Q“O꾌šEÑ‘éº WT°ˆ8c¸çrsIZgB×óÕ1çíZÛgm²Jˆ!7Š«0cœCÄ\™wÇœ– ]@TD4Ër®–<@h»ïD´ÆæÄ£üHÊÂC]?~ì½+‹"fD _VÕx:Ý»{çÎõk¿ûõ¯µ¶Þ–!EÑ,@@ï< «_xç¼![úÒÛÒ«D$ ùE D#h¸£iÝúÁý6“ƒÃÃwÞ]¾|ôÕÉÿOÖ{=Ûz]W~3¬µ¾°ãÉ7á"  b)µÊjËn»[å²Ë®rõƒË.ÿ?~öàG—«lµ«ìVwÉ–Üê¦D‘@ñ^Ü|òN_ZaN?¬½nÛxB᜻Ï>û[aŽ1~ãÅ•ZS4TAŒÉH~Éܺz<.œm6›WîÜHç§§m“;e%©FÄf½Ž1”¥%º®êñÈKH ཷ®!’uÆfo½Bß÷þz±>>Q[UÔ”+ÌòÕDÓ𓽬*iðrkY”âSäÂ`Ë=×DÐõíã¾x¸j?ôÆpY¸é¸&ˆEa­3ÆÚ¶–«5ɬǼƒ5¶.ª7ßxóñ§Ÿgì¦L‘h½ÞlÖ½*$2‘ŠW^yeïà0ú€h§“ B [ô^6m›£ã“÷ÞûÆß½øtðÁãLYWU5ª‚NŽnêRâà¿zïîÙÓ/cÓl®cg]×´ëeãŠÉÅââj½)&“'OÙzÌŒ‹ÅÕz½ˆ)dbõ×^+ú¸Ta¹^œ½xªÑÇ€5v¡?÷ÝE¿yÊLC1%Æd´YgÅêÐ?ûåßüß÷¾öîÑû­Œ5—çó½½nÚfs÷Ö­a³xöäÑÏ~öÓÙtòîw¿súìI³hŒ³ÖYˆ£ÄàSð>ÆàŒ#¢ÁîºüfÓÄÉ0 ¤lFÑLCv®`æì¿vΩ@> ©@^œ1Ö`Yиvýš‚¿Y¯A©(œº(©°PÙn2)‚Àf¹\^„ÑôêYŠK.±“jòµ×ÞLb¾ýÍw»^ýáïž>zv~1\¯WÝ ãù¡©¦«Ö E¯ÖTÎ8‡UYÖ•µ.ί14Ø]¢¬ûnÝ6­^’€«ÊlÿH)ÄB̆ QaËïE?4OŸ><;}Ñõ}RdcŒµ» «@¦óæXM^"ÍNK¢ 1©@6egb¢¼DÊEDç\J)¿¹7SU‰1å-!¦È°=ø¥TºjJÛ¯ËÇjW”Œ9i [NYŽ˜"""c ‘ >æÿ‘˜8B$!Dˆ1®V«˜’5™yç‚#!±±ªê}Øi¿”_dÞ´¶©Ù­H Û7áæ4BðÞç3~^¯rJ3-Œˆ$i¦?2©³d¬õÁ‹ä–l fÎw–1 ŔԓeÃT•å¶ô܇ÌÎü²v\Vóƒ“?øÁœ1Ÿ}öÉïz½\ÖUÁ†%&WØí­ Æ”¤ÏÓkŒ- .œ«ÆÌC!jY‚¢°I‚FP’2VÕ¹ª:zåšúñÃgÍr}qqÝ ªEX k…£1hËÂÞûX9'aX5mˆ~2?¸Z®7«eQŽˆ)t o ƒÊÐûårÉÆš¢¼5ZËùÚ§@!Ä%&)ËÚ•òÀ€0F£ÑY³i›~ÿp/å8"æÐ d‡n&ùƒJhQ> (*1(•åd<š÷A°®@$(ÊQ?„eÛ¨÷>I²È''ÇhyÕlVë56Cˆ½›õÕþþѪÙ\^œTUQ0‡¶M>ªJLÙtåÃú.$ðÆDSUッ;]ª‘óiú^?yÒ6ÍÄíYc%FL’fóyîû*l‰ÊPu:žHL]»P±åxu~¶Y\­×+E1XSDÅ'gg^cwuñßþ/ÞùÖwÈýåÅåúìÑCT`º‘Cn‹Ð0„O?ýdqu‘3 Š0¤b a€\,c Qv®(FމSPtsýì‹ÓÇ_Ä߇að¡ïg9Å¡mV"zñLâðÊk¯¿ÙNÛmÆ/IÊ|"j›ŽÇ¦ª+íúÎ{¯dM.«Cl g­,©¨&€­ØcQC," )Ó,ÉXPõ}Û®U5$IÙ¨ŽHIGMçECUHQ͸—õ¦éºŽ‰=…u© *q§ÃÅ…°§ÕÁíã7¿õÖj½~ÿ—?öôEÛàõ"E=ŸMŽæ‡ÇÕ¨VÕÕz½p}X/.ëI9ÿîýïïüþ£ÿéü ¤¾ïVMˆ&—ekËÊX|ßµMÒ~c@Ì–AH)†Íêªí{cêéì€Ý8Æ„H)ÊK^}°ŽˆÙ<ê½'ÊfÁÝ—!fÎ;¼Ì®É#òw~·o¾[þ-dé9×V d«|L)×\€sÎC 7<Å<×cXS”¤/ä·ãé8I€Â˜d6_RJà½ë,fT}æ±"k“ˆh¾;)q®ðøJnÜZãw*æTTÓ4ÙËŸ_Lþ©s ,Æ·/Þdj匪ö]—$SÖºSJI4Ãñ‹¢È)§ªªyµZ!¨aF„Ü>™’ˆøºr/Q °†-' ÏOŸû~øÙßÿìÙ“'\Öó£ƒÛï¼ý³¿m|ך|{ÉA0cعÂcÊÒ³Ází¸ÍÆ£±«ªÉþ^rvPUmV˱5Ób¼ºî6M7ïw]³_ËÅê½·¿þ¨vC7÷±õÐ5±M11õìöÞ>!@êÚùúÖ¯¯–¶ëʺ`l¿Y¤èAq2Î÷ö¦ÓÙrµ†> CÛô!ø,šÏæ‡o½õÆñ­[m×=~üH51kQÚ¤¨Ê¯¾öÆÿñ««$ÉÇRÊó!ÌD@d•H€Qs;. Ä ÆM«zß¹v:f*ËUʪ~ã­÷Ìâúº,‹/|vqqîC|ôäñ0 ÏOÏÊñØÇEíf󽢬„ÕXj6KW¸8øñ^ÉêïÜ>¸{ëøéãÇÈl€8e6³Ù™‰A…H,ªëRÑ…0¨ÂÁþ¡+Ê|a !F…”h>›ª*¶*hlVÍÈz ±r…ÿüùó~¥µøêºY\-¸àe³èƒ6QQUOŽ&£j>©þäÇßÿèãýêW”?ØB ª’|°Æˆ°Ù›ì}ÿ»?8}ðyçaëJ–8l›¯Õ¢1D À)ù® !x‘dÍÐÚ¢B¿ímN¢¢%hìƒ:ÒDªúèãÏž<þ³ÿðŸ™-™Ï `¶±ì¸FH)v]¯ªA’1,¢ˆ\8—57DpÎE!*MÛðs7god7sÌgÅ*’ù–M×u¾U$QŸ> v];ªéÍ7öš¶º¼êßQì P«R¡Úe¢A;›Š IUðð'?¨Šr½Ö¶7¿ÿâɧϛ׿ñÝw¿ù½½}$^­×’dµ¸Z-®­Á”†ƒ½ùÅõù¯~õs‰‚Q˜]ÍLEáœ-«z„Ùco𔊠¨)‚ø]i(¥®(Ù°+‹“)3[Ú¬Äæꛥ-Ï|)‡þsVr ÍWÇóüÅYúHÛ.Qx Ö“NI „@»ìkvÍì€×”'ÚYnÝZÅ }¥…ŠèV%Åìä‘BŽ›æðU¤v7YÝ–Œob”Ñ9¿–SÂÙÌbË 3ëg·Ãe6äW1QÞ¿ŒaB’´ÝÕr°¹(оï·ÁÝ—Òq¸+e­ªŠÐô~Ø6RÊ3ú£ht숨(Šñhäcêú>³wòvHD…+­µ;¬BJ)µ]B,\yã"¢$¼O"!ÆObŠ^®®®ÙU㽓»o~ôÁU9–Ш$dÃlªzT–%cÊÑ­“{ó[¯%µ†i:›¼ýî7Oî¿ »zd,/®ºóg<¬ï̸_\]^¬ Â+'‡‡ûeÛÇÑdtpôÃÉ|L71=½b²~@ªz *½±”T5ÁõéùãÏ>ý«ý//Î_pLlºað1 óùüö­;ç›õÒÃÐ1Ô$GGGßûþ¾ñwŒs§ççÖ˜u³^¯¯ˆ1ÄX§ó½Ãå²9=;#{×)¤Ĉ¨+49£v¿®b !Ĉ)…¡I¡¯‹Q«PŒ'{Åþ|Û4–Âd6®Çõo}Óš’ßy÷»ÿöoÿÍ_|Ô5ý©\ ©þì{ß›Îkõk"# Èøù§Ÿ„”æ{¯Ðf½0F^¹4™1>‰…åÏræú0³¨¦‰eˆíƒGŸÜûõqçª13&T=>>®Š‰²·0öúzóåÇRìaÆÒbµØ[M“ï ¾Ý4mp €5èŒ7]»jºÒØéñüÖÁÁ‡~ðñgßýÍ7_{µªK*,!±Ðï>üðÛßùþ|ÿM¢Î¸0¤«‹‹$ 4æŠÌü ;n&rÖcëj„H!úMÓô¾ÏÍšŽA†õ61¦Ò¹ÒÙõº•˜Ic`³MA* ·›Õÿù—ÿûö ÷Þ³a²&ú²½!—<äCPN“ç~¸¾ï ªJ&>Ø;ѳÍfCDÌ™õ Ib !@‘Ù:"ʶ6  ”EI˜¬û÷¾ûÝ×–Ëöïÿáãu—¾öúÝÊ‘¤HÆØ„¼ À9FešÍ&ÎÂùÙ騘Ü}ãkßêm§ÖY\m®T¨bÓ6]·ÁÒÍçó“£ùŋǿ}ÿg¾_Þ}åÎrÃP–}, šMÆ·oÝ™Í÷¼1% Ó{o]¹\·M»9{úeÚ{Tcl5UU…ŒeYZ¾ïs ™sèô¦O¼²{IaH!“¹nŒ.ÖUH)hîgf$¶ÆTeS†.äKµˆµ&gv ±ì\4[¥wÉýÕ±Ö¼T©¤9È ð‘b[k™ nâ¯@¤( ”ýû š‰Ä)D$Ô¤1—|VI¶´™˜ˆó¶§¢/Cò†Eˆ™**1Ř’µ† g9ni € @w°å²Øú)·³†E3•v4™Œo0/HxcÊw õz½a“•Œ¼¾a~“óÔx{µõ}/ˆ…b=Ö¯½ùÆ›¯¿vúü´ëÛ»÷îïÏf{Ç›>œ½x¢Ò{ß2£éL•ÈÖõl^ŒgÇwßì£Y­úÛ·Oªñüùùúóçï7]7žÌLQ1¯í}ï›w¿óæžC¨6iU ‚p Q©Ä72˦M†ª¡oú¶ñÝ•*Ä1©ÆÇEýÖ7¿=™Îú7õë_þ´ÏCß´€ôäé“óÓ’"©$1lGÓñÿñŸ¼÷Þ{ÄÔ÷ÃÉáÁþ|ï³Ï?¿ºz‘‡Á«Êõõ¢Y7ãýñdzØû$`æ³ÃÛ·î½òÚ›Óã[ƒµ0®-›™u…Ñv®Ï›Ó§OŸ?{¨©ß××/ÎEì†q IDAT?þàˆ¢D**q±\ÎŽ»².«r‚¯Vsücw}yüB€rUMÇãÑÕé²ï½&?Mn­Û6Ä4)$Ÿ†Ö¯N¦ø±Û?|ãzÓø¾g„$Áä<ÊÖŽ†Ð½8}Ú·Ý“Ç÷ŽŽ}·ä׫EÓ4M@€ ÛºªÏ_¼h›5HèúX8kM¿øt:æëëç«Íj³iÚ¦sEA ËËKHq\ÔeYúàÛfCÖ©Êâê|óþFÿãÿäŸÝyE…Ù”…ø`ˆöF£ÔûëóËéÁ>Y&4lÐU ‰Tc"Fg«~è£÷1‰8D»µ,†Åêzµ^ŠŠ‚´ì8eÒ½*9WZÛ3 } CH! !e‹(86ƒoLvà•®Øª²Åg›]屈¡A̧¶\¸ÃÄ!¦MÓVë !4M“—$a0¨ ÃàeëPɰ+BœŒ&eQË–H¥I‚54®ËQ…{“" ý³g/BhNöÇÓ ª)Ø:p¬ÆÎiîê£éþ­»÷_™NÆ ýñ¾>yz}úâ#1Ç<:™¸ F/H1‚_6Ì®.ÜÕby懧>?{úy^¤m|«d&³Ã{÷æûóñlRß»}w2›ú”lQš¢ Iû‡Iáó?` ëõåÙ'Ÿ}¢LÆæ9¤”Ø8"JyÍ 9ÑjL>,Ãùx½…Üå3±îVÕÌÆÊÎè¼¾Ü8³•ÞZ#o#K!(d÷'Ý,Á/•Øeö'çtþ rÛQ—÷@E$IsBo¢›¢„†9…˜bÌ\xÔ„°©äîr|H¹<ÄZ»ýO˜ÅdÍç)D’¼}Õ²„ºÍÙæSB6•Ä\Æ€1Jˆ v¹­¬î„½¾ô1¤$²£aU•ÖÌ&G™bŠ9*l­õÞèÁ¶Ý¯²ýµuU:몪Ú2År—Ê6z †Ìt:;>999:9æ.ÃfTÁÿ¿\ø¥+Ô^Ò“:W£€hÆ1±÷þòêâz¹èûnð¡,«èc2Ž,;Cì½'„½ù,„SJ,DRBfÍkâÙäÔhŒùó7)Á<ÝvC“¥0BÌô‚gÏŸ¥]é²ó,¦ŠU92ÖåºzcÑY;™ßzÍ}¸¾`cªñl6Ÿ—e…ý°Ù¬.+×Í&°Y^´8{øäQ1)±do˜¦“23u3(æ¦>ÜÛÅEò‹fõ|oæ,^²^ ›E‚ öçÕäØº‰W wQl}ͨ4³¡ë_œ-L !¬ ŽÆó½ÙèέÃQaGeQ8bJÓqåªZÉE±B&²Óþ vmß-ÙвYo6mð‘¬/b4¶ "!bÊ=ªú²k¬1Ö˜˜RnrÎÆ˜|Jÿd<¼„€WQM$äöjÝ °" ˜Y_"È߬˜Ûz?Õ<šÝsbòÞ3å «fªÖ¶o„y—!ÈÈaUàf/¨Ä”²*ûòüƪ¸›Ý£ê® – 9öÞƒ|ådÌNÌ,1g‹K¶)åûKŒQR2Æ @ȃ#Ycó»9 ÃÖPox:®–ËÜ •_RþX†³®›$)¤¬=äq|ÞärÖnð c¬uu=ÚF¶˜6È$Ö$0týÓ'ÏR[Tl`¹öïß¿3ªJˆ+ WÎ ;OW‹æÑ“ËŸþïœ.§'·ï¼:êéx6›kê^¿WÖ`E¾|¶X›Ý`Ý ³ÊM €V|ö´ûìtñììE¿i+6†A£gf·ig’è%† ©m[L1ô‹B»wÞ~ó÷¿}?yUÕÅÅ¥÷žD4%%2IÄîðø8¦”…ç@5©Òx4šŒÆ]×êJ©ëÊ:wxrrxtXËaèºa¥ÔÇ´î½"+¤D%‡Œ3¡Ãn}Ù®WL´Ú¬QÀD(LëMá|6;D$4¼î6—‹ë!¤(ñ‹_¼sñÞëoÜ“(¾o … ˆË¢bæ¾ï“&drÕhÿäîý7Þ|ûàð>œ?úùg¿ÿwÿÏ_-®.ü0ÈÝÛwøGúî·¾oìhfþö¯ÿ·ÇŸÂgOöÛ_û¹,mQvë¥A¹¾x6*ST'AˆJ ªX–æÕWî1UY¶1MÐ6Íz³N1 ‚CÓu1%7ª‹ºªëñt2›Î÷>üýÍfMj ´ÍÕßýÛÿëëï~w¾?3±:½8ÿàƒ_>~ð K›z… ªýЪ‚HBãlY•‚°\-»~ØÑ¨Êï|ëGM›>øè·&ôó©Ac|ôM³ÙßߟŒêåzSX‡š—u‚$9ËNÈæÓJDðÿ[ßsÔ[v€SkœˆI$·爃TÕh†£èVe'&B2ŽŠ‚çóÙÝ»wâæ9 #jJqµZv]—Rìº5é0*À:Ûõ¡ÌfªRee9&0åìðþèðÕzv"ž?þ¼o×Åæ‚ã‹g§MKí"²¹’á4¬GE5°*:.¦(„:¯ËBŽq~U´/ž¯bðlÕYšŒ\eca)D釈˜ kd2¶`Ä•ë4ÄTäçD‰x4ªªñ´®jÃ&"›¼ˆ³ÅjæíPU|FîÄ¢(¼ ê®§Iò›IœgÍ–XQ3[kW'»ó8ߌì_ªlÝ-¸¸ÃÊ‚"bŽS‰HBî!SI™ø—£·ÛâºÆSJ)’±œ#¦€Ûr¤› Ó,ßlKÌœ`5SH’…UA½m<;;ܰ‰$#îŒÕ]8 €™‹¢PÙº0Uµ,KçtÛ07›MVt_.àÞm9Ûm¾ø!Gó¶µM¢’¢ &Qñ™|oŒKIŒµˆ”DBJ˜BÚ¬›¢jºyœìMØØ[Õˆ-šÒ´~í7Ëü&6ÆQˆÝõUóàá—óý½oŸ¼êÆGUiA•S÷µ{î­»n Àwæ'˜«pÑÇ_œïMªÛãÅ:,z¸¸îV«nXv”¢)¢PVa¢|_ ) *“Fï}Á\Ö®Cç÷çï¾÷îOÿú¯VË…ÄdŒ‘˜«ÈÈ0Zg‹² âŸPq<Í&Ó§OŸ]õWE]Öãñ»¯¾ñÆkEI)5’´ë¢"€½ ‚)ư‹Yøšaª¹`á,1ëŒDVõãq=›Ïʲ²EQV•±Ì¶(Ýøèö­§¿ Ôñdrtt||ôå£Q!aðÞ'c˲žõQk¡ÑdFžæÅ×ßBçLô!ÆtûΫ'·Nnññ‡gçï}çÛo½ý¶s£¶í_ /JWŒ9}òS:}ðÑûJAˆLß4ã‘q&1S81ê}S‰"TÅÑÁ~U˜LfWőԖV“0Â0tÅzc,Æ£r<šN&ãѸžŒQÃjq‰Z²q¨½áB’¬×¿ÿýoúÞ?züåÅÅcí{D@b”ïË+ªm×ömcIlù_ÿæýÏÿûv÷ÿËû¿ü›“±ªªmÛìíWe¹^o(S¢T¶·q1Æš$1±ÖÖU=øARÊäìkII‰òY)©du%I†Ä¨) ãñx6«×Í&†”§ö"‰QÑBß^=úòC#±£.–W]ß‹¨J"‚QE“ºT!¶î`6L"É2dø÷©,œ©÷ƳxòåË«O(] ‹E×4]+ÇV(–uAlDdf6l*‰Xhg`Ò:+ã EA– "[”c[Ž•qIÆeZJfcKk<± 1ÅÙ˜ÑdŒÄÁ{/>ƒkÁûcÊÁŒÅÏÕ†)ÅÜ­…ˆ8"´Åæè7dy2YA¶øøŒ @ü*x¹+LÙMØ·–ç<‚W•,nëK<¸v•û d’o™07½"(’!" I’Üó ./ñ;u˜ %O¬Rž Üg߈@dDrJ@)›ìTvdvå]«W–$¢„(™œlÚÊ0¥Ô¶m¦$Ùæ¨·À‡]-æhef%‘Á‡Ü‡HH@SÒÚõC?fç, Y猵„‡AD‰ ²aSÖ0¶¾œ³<™iÏ)õ!Ó"Ý¿{ÇTó¦ëüдK¸½WÏGãÍzÇ–07î¨àll=¦åùò7<Þ;8¶†âÐ/ÎeuÙw U–+[V%*kPD§È)¦Œ©NÉK쥢0Æ ÔÅ´ž@ GÇǪ¨"Û6P@Hš‘™\6G2ÓöÌJÄÎÙìoŽ1ÕH1 õ}')Hè4âz±`WV$Š˜ÈRä¶lY–l¨(Œaò>6mÛJ Üz®®w_}ëν{e5Š)Š+ìÉ­Û!ª5Teé (‹¢,4)\u÷ÞÉìóÓÓý=žÌö¨Å««³º__ýöý_þÔŒ÷oÝgÓz:úÑÿÑþÑORˆ* RŒ1­®OóÑoGUõåƒO‡Ö)Å.ª(¨2Blƒ!P$¥Ü–L¹é)ø!¨brΰ…ýÐkL€v µªº²tU9®‹º´“ºº¶mZg9 ©Kª€G·oí­×/‹Ÿÿâ}tC±#Mˆ9Ä’/¹1yN„lرé|S—³ý‹ñ¿–ã½¢´U]ùÐXk ñàC?ôÛ49h >‰¶ÄœˆŠ˜~èp†ºD´Æ0¡l[1Š$kÊ—+ˆ‰H2¹PËñ¤¬JkL Þ°Í·ù­ßC5¥4„áú*9ÙÜ¿»7™ÖJpqíd:Î…0XV6†í¨*‹ªrªƒŠ¨&`|5u¯~Ý/ž-—qõ‡«¡¿’n³Ùtý –5ú5bb‡ÐÕ„¬ ªÐ|Îþ °aކ}뤙¢¥afBña`2E1**g‹¢,KATB¦”¢¤ ÌÆ…-+ÝRjSÛ5€ÉpÊ=èrs ¾Á²ßÜ„D¶n™®ïYR”CTDº]¸áFäØº‰sî¾'oc±Û°eþu˜›ÖÃm%Òð,)¥LÌÏ_ ­s Z–.¿òÕ(äV¶ØãnTbÊî™ì»áôÞÔéeG{þ—÷g‚x7=‘­Éaë¾!«ˆfçdvèfútÞòV•+JRJÌy°£[g‘HFá²1EDBÂBaŠ¢pÌÆ9ç‡bŠql¶jl·LG³ÙÔû^·s'â·¸7CTZÇŒl) A†Çˆ)\Q[±8PýR‹j4™ÍüucXë’bÀЋ„þôÙ³«ÓÓfÕJŒi<*ëªèZïcXlº'/®×m×/—óý©1¾ ›¥ß¬»®©Ì*ŽÁ/—+U¬Æ#çʼZ‹jôm×m|ŠÓñ¸,¢:*Û¦ã(Ö¹aH’|Š’¡ÐÌ$"Ö †¦mC’€ÑZÓ6M|0l!IÛv1fÀBVËvèbQ½oÛÎ×£ÚpÁh ‡ÈÆÌ÷¼«õȲ¡Åúi’è¬UH’$úŒ:7*ë‰+ËÜ‚BÐBˆTb臡íºuþ;}üf±ºèÖýîܹwtth»vøì³‡ŸÿþÁ‡¿{è¾8ýæw~rïÞ} ah;I±p!©¤õra­Ý›ÏúÍúïÿÍ_¯¯N­¶ Q²I?WÙ(Hºn (¬AÈûO¬q̆D%&Ù^‹AÙ‚²Z$dÃHd 笭ªŠbTÕóÐÃt<¾sçÖf³IìF“é7¾õíO?ùüù“'z!Â$ªš2§@%?­DT”ÎYg o6ˆ}·^?n’Yf”m’EEU^œ23Mg³ü¦i…9Ï”BÛôÆYcŒñ!Äà‰Y†¶Í½ØDä¬Kô‹ ±ŠbL*Uá&óùüàh2.¯.Ÿ|ù«ªÜ^r)¢ú˜–ëuJUU;%. ÖUuxx2 }›ÑŕEÅ„~èE„MÂqˆI%úu\=]¬OÛv‡‹~uÞv ‹¤6¥ ¢›®kA’³n2™«0ÊT«ö±_DµA`¡<;ÞÛgžÙKF„àSß7]a+Ä|WÈ¡v“‘ÀdëY0*Ñh [6֊סm™•+“Ñl¤˜T`—Œ'$ůzònœ-¢:„` d‘GTIoœ6»®(‘<-!"BÚ–ll'¨Û¦lÝUÉZ—’„à·-®ôæ&·™Äu›¼‚­ÛvšgΔf$dÆrm'þIòOAˆš$Oÿoª¿‚Ø3sžÓß°.bV?óz йÕ&O–H23vwHšë­^¹ÜèÙkwC6Ž16))@ð;ª{Þ:œ+Ê¢d6) :çööö˜ùüâœÙ”eeŒ);˜ÏNÏÎú~Èû“$QÔíûÈ„†¨,¬1ìC ¾.N—r¼ëx’av^Y™i2®ŸŸžž½xt‹’Z”ÚÕz ­¼‡¦íCJ¸Ñ_hJ :„ï+)­»hÄ1­×Ëõ:J‚šÕu±k"ªëÑx2®G6š¼ZëlUš†ÁwÏ6‹°Ü,—×ìLê" gjt>ahΟǔDÀZÃL"Ñ83Ÿï[óÄQQ9ØÛ#‰uaãÐõ›Mß…¤"ÊËMÛ÷s½\µmOÄE5úòÉSc¸pn:ªŽ²KBŒJ€dö÷O~ðƒŸܺ-*A&“iLrv~îœC•ý£Ÿ;ºC$£—×{÷âþð}þð©¤Ô¶f¶8ß?^o>êú~×üö}.FÀæàè$†À&Sûûä»Ùtêûx}þtuýã` bÙ>ˆIAqësH#£”¥S!"ŠIÁ“×"Ø©’„ºÓº PëìöšhL.!ð^î¾òÖ÷ßþÁÝ»o`Hþñã/y4™MfßýþŸ|ù¸OiT$€”òùGs©¡’Ê6¼·lʪšÎf×W—çÏÙ¸¢ h’Òι"´]k˜s£qýî{ú7ó·}ß+LÀãÉž999Î†Š¦iÛ¶g2Î)ÅliØUOh!c+T$ÅC ¦¢(sëõòôìtÓl kòô9/OªÂ† ƒ Y•|Y ˜NëuŒ1÷î¼zvy¾Zy¶Å´S@9 ª5[Fé1¾k¯Î¾DJÈ)ú„Ž5bði³ñ¹ïCŠ^Ê4c6A1ómŒ<$Fò“™»}t°ptv¾yððÑrµ–’Œ5ÆÛÌPfd€¾é:ÃvDÆ)†”’*0±aò*]³aŽ„Œ‰ $Úb8‘@)·iæ3<ÜœúQESŠ[· ’%þªÃúßkÙæy)±á­pJ˜¡c7•×9¥}Ó6§*„_yØwÍš3>DŒ*ITCÈQ5z¹ ¶Ïün£²í£€mxD·ï \!o;ù@xÓ•ð»íJq‹"Ö²tŒ7Pý÷M“°ë§efÎ…<ãêû!EÝ%_³P-y„E„)†¼ç[E1‰*Q7ô÷EY Æ°1¦ªª´íŠ’Ò™õfÕv¹DÛ‚nÙ¸m8ABF¤ ”WÉ]t ͦÿ²z°?Wu®Ë²B@Ð2`b»6—Dd1!¢åB!`Lˆj2ä\Á"ÄÔ q(Œ:ìóž/dÙôÿòó'Ö˜²¬4ú@*KŠ5‰¦B¿îªº¬Êâúìz³¼öM{±ºê}%YËŒVD5øÒ¶ÍŠM;–2;t½Hb6ÇÇ·«â³¾k3]z³X=yðàÎÁ¬°®d·šåbnZ¿jšÞ‡åz=ŸÏ뺩+Šýù K·ºDõ+LÃòê"Æ%•ãšÉØ(Eˆ9ÆÔ4mŒP•ãëë…aÓ5¿zÿýë˶€ã“½:Êr½ùòáƒÕr#¢M»Þ›OG“ÑÞlï•»w÷«W‹gëe3ò|²OHãq¢W ) £ªj7Íùé3›š”ZD9¡{3ßAÃÄR²Äˆ9~—o¢Èf‹_ÊüÔ$Icam:çnØ|òŠ>™Âþø'ÿ¸Oî§?ý»ß}øÁfµØlÖ®(OîÜQô£ŸÍf_üî׿úÕÏ’clLA’&Ç\”¥$I14›$ôìKIóã“ tñìÌÙ„DIÕÙL¿°¹4YD’H”ôÙÎ..­3ƒG)ÿéŸþ¹a6ŠBhÛ6%±Ö2“sU†î6M›—’¢,s» Q ŠUYæ‘YŠáñÃËåÂ{;˜\QÙ¢ÇlŒC‚Æãêö­ãW^9¾¾^<}¼(œ›Í¦!ö××çl¨é IUŒoauœŒ­FsuL&rZ4ç×—Æ\Û‚E ÃО—0øÕªÝ4-HTQ¨JÁØ4Ëà˜LR=ù^ wÉ£É|:ŸŽ'ûû'‡ëuÓ7]eÝt6«ÆÓ¤i@?¤¸Ù¬/Û>`uk4-]EœD|ß2¨6ëMŠ!ŠDU‰!²µy5ȼpñ¶tT£¦í‰;Çô·Ý/«£[³|V !¥ÈìòND¶ìâŒbÜý“w{€t³%d¬¼¤¬‡KþdgfŒ)%Á­fžÏú$¹P²½Goêôrõ¨Â Tô¦dû³hÞyRL›IfÄFAv2AÞò4O˜ —3±™A¦¢ùoÏVcMŒ©i7!ƺÕ82ÖrŒàCÞð Q‰ÙˆëîÇÏ»ELi½Þ > }ŸD‹Â@×uÌlŒAÑ”bÞn³,’ZÇĔӼ€’ï_[²­jß ¶¬HQ‹ª¼}çvÓ¬N_œwÙñP;G ÎòñáaïchlÉ0!‘ ½¥ "ƒO‘Š¢R•ºU6 Ã¨¬ÆeácÚtë¦kCJ(´¸¼F¥Ùt† ]×ILEWçëfÝõ݆«Åµeº:{á}g _]_¶m›3 lÈcÒŒk‘²,U!„(’Œ!d&cPDQBôID}‘ÅÕ¹³éÑÃGeQ/—›Þ‡¶ó«M·\·«¦í‡ÞZ³†~°Î•U9¬F£Ò®ÏýÑÿHˆû¶ED辔Ȼrëö]ñ¾¯ k5ÞÚÒM÷¦_ÿú›_|üaR‰¡?¿<iïÜ=¼{ç`¶OI>ùõûWËM=›ï¥F>{qú¡´d’o.‘s›sËnsíȘ”Bòþ*c@†®0:$Hªj4† ±hRѲ¬¬syqô!$© hŒ1ŸŽs¦Ow¥l:½m2PÉÅj¸+Ãc‹Î‡_üæÃù¯þ²[-4ùCˆéÅóçãÑèÅó'«Ë‹ÿèŸü§ßÿÑ÷ÿÅ_üÏçϾ4ÄC ƹª¬­5÷îÞýͯ~µÞ¬‡~X¯×åüàëïýp2y¾^\k(¥Ž1ïóÍlË1!†°\­¬±yÀÙ´Ã;ßüÑáÉ}Óu=0„´E f‘([/X”9äL s®çí.gÉ‘1¶ÕƘM ÎFL)cÆ£QîmÈÄ8I1Jt«²¸}ëøÍ7^ûâÁ#ÄϪÂCóÛßý’«QµiÀ§é(ìM¾uÿwƳÃ!ú8¬ÉŸ“{Íõ§ÃæYÓ>t©)õ.–Cßµ„0*«Ò›õ2%_UÎYbL)ö}3f¨Çh†›˜TÐLÔ$žíMÞ|ãÕ¾õçW£ÑÌÓ,/‡aÓô›Ø_8«3J4Fí­™(”Em1J„ì\QØØÃÖ¶5îòÙ‡2, #ÈseJn°Ý¶a€ÂÍùú%—!ÁK‘¥¼ÐÇ2ìk‡M/upߤ[wÿ-¨ [α¢¯¸=gÖon6ßbäsÜM2P7ÝôHåv ÊQdùjh¸Õ0òíuûG’b‰˜rç(d‡ULq«@äAPF°ÅUÑU}ŠªšK±Sй7œ˜bйäP‘”¬c2„Àƺìì2ÆÞ–aW™ê}ða[™DDÆð0ø\€Hƨz¬Ålá'b•<ÎÚÎØrµSëegNàV³ÑÞ¬î›eÓ,¦£ÒašLÇáj!IE@SŒCƒÊê̶\o‡¾ ƒOÖ:¯9LZc€ÙÆãr4 ›Ö{éû~½i6«5\×%€æÚ?MŒ€}ÛS]–]ÓLªñ­““‡` Äwý¦o‹Åt6KÁû¾g6Öº{P%fÞ±m×"ˆsv<¹¢èúþ³Ÿ.Ö«QY1¨XF&>=»¹R$DöªJ6„4qÎåZ¶f2I ~hM9¿<=ýø“ÏÛÁ7Mg].–IãàcLúã?ùG'Ç>ÄQMÆZbOF16Ê1‹exX1 BŒCSÈzn' IDAT1¿vû(’ݬWaolL ;[12GDÑØ+BŒ$ѕ՘‰2*„›´“+QÐj€S†àÆrIQ×µcòÓ¡”[é™·•kŒkf3Kd[+ž;dò§+SíaðýlZ[aŠ"! ¨Ò7á³~sxrëW¿üùÃO~{ïþÝ<ÄΆ²Á{7ø$ Ù¬7®,¹íRLëåJí‹éÞý?ûóÿêÇ?üþ£GÿÕ¿þ?öß´Ãàw+Àv8šÒî¶$J_ûÛÿùùÏû»Mß \P=1›¾í‚> @À9nETE]ת E‘Dïs5åx<ë8?£ª¦ÕÚ¶mbŒ¨`­‘$q( Ç—‹Åï?úèñÓÇ''{1™˜°¬Š§WWÃx|èÆu½w<¿õ*Žc±7žW¾o-ÝwÊÉ=íŸm.Þß\¼ß ›Þk?H?$KZX‡†Íd\ϦÐÔõmY•1ª"DÑ(MõÍõ鿹8¿¬fw…ÝÞÑýƒ[ŽC쮆¾ëVOB9t›éØN‹1X3ÐõêèÇåÐÇ¡“Õõ¦m6ÌLˆÄÖXk¬µE%e›G¦Ál ãŒÛ¦ZÊvÔ3™7+ÝKc™ VÌ.—­àé½G¸™WC~íX•ø²}žÚ¨knHî™ t£ÄÞT«"¢fcfŽ’f[Tž{#“¨fu4„˜±Iò!9Êe‡Ÿg¦´ë³Þ>’Ï’¢l&µÆ0§!DPQf£ zŒBò>zU‘™ W"Pö€÷![4ÖXkª†‹¢È°™c, F☄Iò„…«ª*œ¢BÖ¡}?¨vóùœmb†( E¶j¶u®ªkbcØÅÈ:AËÀ((É@Ù®¯&ÅlT WIZnÚ>IœŒ‹qéØ2 ¾7Æ?,¢$¤QÛnC@ЙÉl✙ÍçJFÁå´*½÷~°|»šõòª*ʪ(†¡üø!!dÂe6Úª@LC×3Hß´×WËÅ2û‹J犲FÕì ÉÇŠ\Ô^8×5Mˆ¡ª*ç &›Dú¡WHYíóÞ¯×Mm6]]TÈœÇèC¦ëÉ9ë}ŸEïKQµÚ÷ý~Û…ïýè¿ùíïýÝßÿ»çLjØ}ï{ß;::ÊQ;¼ æ<)(²!ÀÂÐvë¡o¦Ó‰)¸÷½ÓrdPÇuR2Öbˆ‘p»ãæÞW4ëˆ # )Å3Å·FcNCˆ«Õšhc¬qÎv&EBÞMU!DI(¹ó¾R}nÚ‰·ÃÏ]k‚¦‹óg¿øÙ/L¶ ã¶’Ò™r6ACüâÙÓåÕ9$¢#"¼rûVvåù>øð·dŒ ^â¯OÏßo~¾ZûMÓ¬V×—Wƒïƒ÷™%cÈ—ÄHLÌÄÌ>êõ~ø“³ÓŸ~ü¡†¡²ÔuÍdú®†AT!&. W.Wü¤|ˆ]ßeX h66Hß²ñBÞKžY& H‰µÈ(‚ëMï»õÐgʶg[!™¦é*h†ë³³$îðv9DéÚÎM·¹îW ªšPùP÷Þ¯šnÕJ×&CÐù`STOË>ÆÔG6f<™¬Ö=“-\Idʱ)غñòjÝž?Ö !«íY{:g-Jôí%èà4 ´õÿåêÍ~,Ë®3¿5ì½Ï9wˆ9TeU2«XU¤$J¤(jBKnYè'ÁOvÛ/†û/óƒ?øÉ0à¡%[²-´$R-‘EV‘ª™5äãΰ‡µ–ö½Á’_ d""ãæ9{¯áû~å¡ÜÞ¤ëÜìŒVŸ}òéúÑ,©¯*ðâÔ‡ÆbŠEuu`ÇÄU©x—a…H̘rõÁÿoè|çÚ?fFÃADF€"¥ªTrI„$*„täSVJpõ"ðÛ¢*bµ)«÷Á±v63%Bf–£jGÀ‘+" ZùÈ"bˆ³nfÙŠ­w‘šÔª¡nAJMµAUCpÞû#V³Rà C-YTŒ°îLMgó9—œ«\Ô¼ õªH)#B>FóŽÛ¦ñÞW/39çÉ96À”È©ÏìÙêÉéâµ×ÑÉjõÅç_|ðá'YKJÐïöÄ䛦i[v\ët2MÓ„w}šiJR¬iÛ"Ñš¡Ä‹ë«eéw·×»——WEÊlÞž­–VâÅŋŢ[tíOž|ñìj»ÙìöSLÅjEhÀ̸ìÚöôôôtJé‹Ï¾ŒIg]7›ÏÁ¤õTrÚïûív ¤šI ä’Çi§Á`¯//ŸWÝÅ~·ÉiHiâØ“w|~zÖv]ÇRrýðÕ¤mülÞ.æ34퇡ëf ¥ ÀÙéªkÁÁâcÌR¦qðÎyßÎæ†.K‘qD@«[µf£H&T•¯ž5eýíßù½ÿò¿þ·ïýì'?úáßÛ×–äDÔu³ùr™SúñOþI´ÔÀÈ"bZÈ9S‹Ã¨97a¶+1î÷ÍÉI*•èšó9™BP2¬¢ÑRr))¶Ýܵs)ضÍãǯ‡>ûô³”¦0k™]ÊIĈƒ–RЦ¥sι1Ç’Sì9d䦀,µs;Ô^†wù½"ŒjƒZNþ⽟üèG?œQ뀑ç14~±Ýì¯×Ï‹Ç>—I%‰–\Ò|6ûýßûƒÏ>ût³Ù ã®ëjUÇ䂳,¥ì®>üÙŸ}þ÷2ê4‚¨¤ŒdŽ€R)%¸Æ93´òñ/â77Ï÷ s)j€„M×è0Œ*z¶:ÍE‚÷Ì5hm¿Ûïî@4)§Ýn§j*æC¨â }źu³CêfÓú¶£¶›vØT‚¦gž~ð»Ð-¼ûîO Ž»_|Ǹ½¹\¿x–³”¢e}óTòíêÔ{ÙMëÒ÷e³ÉÃ`1zðŒÎ‘µSFæ³¼Ù¯¯¯vÞ5Ì™÷N|°à­i—÷>ÚÜ®·ÛK¡¸^‹Sª°&ÂR²ù¢nKtçt²yðÚw¼³Ï>[,Ú¦™™ˆ ©Z*“¨Á‘ŒXߪÜà jYZ‰(uþpkZM»Æ 6¨‡{ݘVÿd-ì+^ÀŽmb½Bˆ+KŠ”ªö²ãz°Úä87¡cΘ" ê¡Á¼ òÖÙ_r)H@X£­€bÊ%—êðD¨\¹C@ˆVä…iM»EC3ƒ‹üˆ©JG©l^&bUË9©27Msvv³ÞìvÛ¶íê‡)ñÁ‡&8"&6)cŽfb`HuØẨ<³óŽ‹(‰r÷èìüôñ7m6»ýnHf"ÈwV>D&vD¹H¥^«é4Y¤¦§Ï.øÝŸW:gëqµl[ïwëëíúÅGïµhˆ® ¡ >øÍíÍíõ¥äøíwÞúïýúr±ú÷O~ô±#:bbªQ¼cûʃ3¹¼¸þò˧—‹E×8WâDhιSßÓ$ÁÁ‡RòØïÇqŒ) ª$“lZÚÔ¬†œËÉbѵ]Ûµj–rúñŒ˜œ#ÇØuĉ ’Ê»jhhÜòd‘s.EÔ4x¯j‹å,Ì[U†1¥TC™R΄h–³L?øÁoÞÜnßÿç>ùåç7·7ò¯ÿóßþï› !ú¶I¹¦úäÀr¹xûío~þɇDQE è@¤“â™Ì0e †„.§dEÆq<>lu!uÔ˜A¥Å½ý'³ùüÝŸ¿ïÞ½ijsàÛv½™Ð/ºYƒû}ïɳÉ~?ŠZ©<Ñàœg¯¢"‚Df™¨Œ&H¦‚ÊL&BHÞ±*„à¿û½ïÿÝþ ±“q»„$Ö̺û¯>ÎpíC\œ~ñÙ/f —ú胮ë˜ÉbÓæbÈ‚»Á‡8(¬ãºßŠ2Yܬe³žRÊ’0MÄHLìXÍ×2LSp~FnßCjSO)'ƒH4욦kÛ¡k;-¥ß ²ïÓn×÷ÃHΗ¢¢VDE´ ;vÞÀ©‹<„AZçgóù­ ,Ì04N¦iDª©V£Ejì2V]îq„bfbZOyÏÎ{_&õp7U0EEÈ%ûÐ0;0pHãd`Ç +²÷õ2)¥XÉ©šë×,’‰Vµ¯¥ñ!;>°‚™ªlªÑ”2«®}—‹Ôf⸽¬ãöCUƒz¸Ò¸O¨‰±sÌŽÍ·t Êä*ïPï3–\Å 1¥Ýn'Rî:ß_ £ @ò~·Þïw)M¢B„ÝlB¨¹Q.´«³Ëåù”5xÕ’…™ƒçœâ0ìno¯¿øüy™Íg«åâüôìù‹77·Î9ÇÎ;‡ÁŠM)•"è8™ »Ýtsuë˜s±ßoMr/2¶¡5µm¿}øú+~˜yÆièƒsÛÛýúz·ÙìþágïMXcoS-BjÇ¡ùÉØ÷ý`®onÖ›kGØøÆ³Ï9›1½÷àQÓtýn/bÌŒ¾9 1š ™Œã–Lúýöå‹g›õf1Ÿ=8¿WWÊÃ~ß÷=6M“s.ª%—a?¤”kR‡ª9®›mUÑÉ£ñÑ£{çççûaº]ïÅ`>_¸àCÛ8v‚ºç\¦\èœKJ©rlœ˜ãlPDTÄß;?7Àœ32ovû¶ë±æYW‘Y­ÌTµÖsÌ<ŸÏ#cD .%g‘C°=¹~Š—7·’¨õ¦H”ÆiüÙû¿8Ä;¨¤w‹“ùù’ž-€²¹Ùìáâd±Ùo÷ûqg —‚>›©¤(Ž3îF½È3„@«€v»‘owÔ ct†àUu+åA¥dBTƒiŒ¥¨Y"òˆXDÕÀ±³ŒR¢Ø­ŠHŠ<~íñ7¾üüC/¢@†®¡“(Fˆê‘Tám‡ñ·™æ Duµj\íšt*TzK±äl"!07®YÌOVg§ËóË—— ÒvATwûÝAW®FìÚ¶uì˜T§aÈ);OÛí¦2¶ Œj[s —²z̬ÿ6¸ƒ 4é*%ªA©!ŽˆÁ;$&Äʱ¨,xЯA„»ú§Tó9½óD~(½{ôìDÄû b%‹AŠÓMÉ…É9wTÖ“µÁwó6DZ_¯‡ív¿ÛÜÞ^KIÞû¦k ¢"o^.fsw²bÄ}ûqˆ™†±_¯·û}¿ßÄÚàØ3²c7ŸÏg]Båƒg®í‘‚в ìpRSæf·Ýß\\¤bVT´ëº"òÙ'ŸRpL¬ nssÛ¿÷þ§IU˜B×A:vÁ9Ç,Å6›}ŠÓvwÕO£–ìºæÑ+¯ž¯î)SžbœÞùÖ¯ß{ðÊ0ÅàÃP_ùë«—*iê7~ð|Øoûݶä¼:Y>¸w¾œ/l¿Ûívëqèg]‹„H¦YrŠŸ}úÙ7ß|ûüþ=Q)9ïOOΈl6_®NO·›­w0ë–î±ã®mÊŠR–ÅɲmÛ˜e臮kïß{à\ôBr¯Þ{ð`šÒn»Ûn6q±ŒS¿ÞÀ8Œo¼õÎk¯?žÏ9ç~?朇q2$3µ\k‰ãDŒg§'Âdš h³]Ï7-;OD@l_ Z¸“*ܱ¤·»ÍŸÿ執m»ûœ"!tm˜Rî–§ï|ï÷ï=zœ%ªê#§çgOŸùìãÄUQÅQ]´Æ˜MŒÀ©š"# š Z}PEÄTaØŸ~ôÙo}÷ûÿî/þŸq<2ÒâoÿàÉ[ß=¹÷`yÿÑv³/)E§ŒÁudŠ DHìj€!³ˆTÃK%92“©VÁ 3«¢zœç"%§tGXºs)5Þ×Å•K¹´eÞ¹’rŠ“#ç U‹¥”"Ó4Ö⽦ !Ê!Ú‚ˆbŒµ<8qˆ™Ð ³÷×›ÛyXÌ›¹¤¤!ÌÞyç7öû-€îv{É9[ÁŽ1èò¼[®&? 㸟D‰Ýìþ«÷x½Èèõæ‹ÍôUœ¦”(ö)EHŽFÝŒÐú0Le³×”5xŠ`0ÄfR2 :K©Ä©¢DhÞqý AE¡ƒ µ\Ì,43QsÞ/VVç¯6¾íûqŒQT¡é:Çt°É¨!‘C8òÆ¡:†¨š9ùŸ*)§’3,óÕêþêdu¾ðËðàµ×¾ý­ïΛÕþî‡üó/Bë§”J©{H´|×vuzψxzzuyÕ´ÁL¯onê迆ØÙ‘9f•<£P•³‡ù8€ 5ÕÅfö/ó²Íª¬Ó!i<Õ¥1 !“«á U5|•‰kPÓ6!„®›9Ç/§i¬Q?1Kµç’E%¦¡iZf_šê€™Bqiè¯^<Û\_–HºeÐÍ´Ê-%Å}¿é§[ïœg/Z”tˆ…Ù·MS¤Lã jíl¾89 M»ßíd r.–Éùз\ÌU$O±OÃ0 jxïþ¹”<öýõõÕÐ"u;»>øÅ{W·•ºœK30-9J§a»Û­ƒãÕÉòÁý{³¶].*Òï÷ÛÍmÉ™™ŽKopž›Y÷æ;o?|€Ì ’‰ª5]w~~±<Ç‘IãÉ¢Ë9G\JðÞ‡^ýqÓ´1åiŠ9M1§ÓÕ"4øôÙ‹OŸOÓ´:9Ùܬk»ƒDfz~vöï|§k»¡Ÿî0…»}ÿɧŸ({˜A¹à6»µs1 †qšÆi"v)gBvÞ0_‹FÀÃã*IÒó˯šà]]‚11̓ç‘Âûyê Ò|¹|ôÆ[WÏ?‘ÍÆ1ÔM†) …q·/Y²#ר±£PJA0ÓrÈkCö®uÎgPÄ ôýþñ{øŸ¥'òͽßøæ¯!5»¾G¶nÙÍÛÓïÿÎ÷†Íåö*’@M~fvÕÛÇÔ»0†º,ó>TáDJ¥m[$Ž©¨š÷Î1ç”j´Ž«ÁÎ*®bT ¬”Ä€%—œ úƒ§°”R;÷"bjz×éK΀hZD 03;W5Îy;\ªTŠÄÛ&œ®Î¾õÎÛNWmhnÖ7û~fOŸ>Ûï·`”sBd EJAÒÐbÛxbéã´½ÙmÎçßýý?öß{÷ÿß§_}%Y½)bHÔ¶!‡ f¥Lâ²vsÖXJN9Sï ˜Xp~ѵŽy¯C„@ä¨)÷Ž=ˆ Òá0•˜Óf7ßlBDLÞ¿öä›Ož|û¯¿ñÙgŸ¼÷þÏÄ,„¦›u)ÅCÆ#ì–p\¸×9¯¨’X?IÃ&øàýùùù;o½ñ;¿õ§ç‹1t]àf¿ÞHŠ%ç/_¤á`®&LŽÖ××µ™sìÚ6l·Ûr›cŠErŸ«ÊäuÀ“AÅlÝìÕ°zpÆš ‚¹SD5wåP§×Ÿø0Û©alU£ÉÌU(Paȹ€øœóD\šØ P U\1æ<ªn—:_’"h*fÛëáòåÓõÍEN‚ïEÄL¥äºô§šþ*6ìû”bΉ]8¿ÿzÍwª]DhºÐðM(¥Œã`Þ¹:¼:Ðù‘*—¿äÕ$å qÜooó´wŒI`†œ&-(âj)£(VŠi:àºó8íöÃzö¥4¨úW*ÎO=Ý^ƒB¡H9;;ŸÍæã0NSŒ9%Gä£é§`"Î95C1o&¹äÆ9n›“æüÑý3ïÃlÖqU¬Šl×ëÛõí8 èœ70#bCó­_œ®0x&4ôÇi³­²H?ôCõo¨ˆ#ðmH)·Å,Å ‘æóåêôt6›‡ÆqJ)9¦qL)M»~øû^iÎ9¦àŽÔBƒ’2q5Mãf·õÍŒ¼çÓÓÓÍv?ô£é•wXÔÞàv¿oÌ‘£E;ŸŸžÝ?¿¿\.µ:ˆ³¤C¸¢¦»!jLÓ(eÖµ×t÷Ÿ¼ñë>¬ÁÊ™ÈÏ–³Ço¼ùñOOÒ4C˜áÉÉüþù¢(£[¿òNè–¿Ñtˇ^Ÿu‹aš¦8IÎYM—óλöövøÅ?ÿ|»»mc ×ë»~|ó­w®ž=_®Î<~{Œ’ÄÄTš“ð¯þèO®ž¾øñúʈ$øµÔxD¨‚/=ÒM~¹£ôÙáÙ1Ãà›¶U`ŀة¨ªåR¤ˆ¹Ú§;Ïf‡ ÎNÏ»®K%שk?Mj hÞ9|­ÿK)5¨̨- `&Ð\.–f6MQUU4øðÎ[o¯–ËÍÍõÚd»Û¨–/§­wvv¶º½ÙVu‘¬¢fÀÚÖ·Á Ðõ$×·}œRhš/ö›í6§œ#BPïýl6sŽÕ„ù'P£ÞÛ¶-™brILĈ¦’3L}ðUõTªÚú8Ë Ã0¯N¥­” PÄÎNï·Í’‰w›õíúÚÌÆ8fdïB˜Y¥?¢QdÇú~Ñr¹¬¯#§œâ4y¢®á&¸ûÎßxóÁýW–‹y×OVrüàŸ|þËOŸ]\\\\]_yǵyª¥7ÖØnÀ&x×Ñz}Û÷}Ío¬¼«È0<¦©˜¡óα ÁÑn¿?ª_ˆÁ±°\´RððLñ&ÊT…1¥5«|ÐcüÓAIT]DNUí^WÕq „Æû &"E¥8v€d¦6›Í¦Ù’µÜuš–ã¸[_ß\½PÉÄtÀ6”\}JÀÎZ)… O1»qØÞœ¬ÎØÍˆÈ sVßÒ²›§’Õ”˜œ:çSŒ‡\Õ"…ª¼I¤X)Y¥ä4©#çjÒ¨#R9R%ªx €€´ßÞÓx¸]+© ‘LÉ$؄ƱcjrÛ´áÜ1G"DP-¦…‰TÍDAÓ䃟Å8Þ6´v`0MãÕÅÅn·-9 ²;ú} ‰SÕTJpdVÓÉ=€MÓx{{3NcЦ˜RŒm×–œ4x.*º˜µ³¶íw;麶m^ýñjµúì—Ÿ^^>{úÅ—C¿¾vÚørɈà¸b–ý4 _>ýü>૯.êç‹'ËÕo~绾û³šÙfjb Ds¦ÞÏW]ךo—󹩯qŠ)Õät:lĄ̊îþj>‚€”ºÊš¦ ÌÁíüÁkËÅiAÉYD!¥ì˜J&¯êÀ 8¼wþàü|µã¿þ“?ýõßü®P“÷S)è wݼëØq­ù–]@àbíkOÞúõÕo?8?3Qôó©ïß|üäËW^ÝOùÙÅËGOârÖ¹†#À0L_Åg3ß )›¢ZÅV‡<ÙJÿ"²MĪBÏ9×¶íAaBãý|šF3Pƒ\¢Ç V§ ªÐú@mû™Û¶u걨8¦”¥k[5ÀŠt®.Â+ЈEÕ¤˜¥Î™XbrˆàÐY6‰ÉÊt}õrŠCÓµqRr!X.É{/%ǘr.Pù-ªP./_î¦ôâòÙW_~r{ùñÉâ¤õn @ÎI *¦T˜1t- î¶›aœÀ¤›µmÎV')ŽZ"˜MSJYSI¢RJ6Ó:Æ`Çγcò„VL™P5!r¹egà^yõñƒóÏMÇ~sû‚>ûå矤¬©(3Û0¤”¬ò—%³dUpÞW¥îl>_.—Ž=sÎ/f'î MÒ¢ãàí•ǧßx¼š·èÀ,Åýí~òã÷·}É·Û}GuÞ‘CB0(ZBãgM‡ˆ¢Tâ4‰‡n¢Uê/RÎ¥ÎÕë¦ T„°ñΠ&·¨žªµº'ªžDZÑ#˜²¶51Ö¡0\E‡V€Ù¹;ιZsÇq¬9«5ÙjtÌ1£õkÕÉáA¯*O)iìwÛíj©Ú4 AàÎF`VJaOhª (©Lý®ßÝ.Ϻ&´)g3A $2µq‡aï]ãCý޹H?Ž9g©A¼ˆ#µÔ˜¸>„HÉqØos.qloÒ4ýò³¯®.wÛu¿Ù”4Â|ZjBï ’cØìvïþôÀ@v% IDAT'úê7‹%¡Lãps³îºïC…5éÁà,޹iôCöq¿ß+¹í½›ÛëÝnwˆ{¬Œ-UDtTã¸ÁÔ Vqwwö#7-° Þ›%çÜÛo¿­øG÷—þG´)å1£×ÏçíÅåK¡®»Ý ×›÷mJ9¥8 ƒ©:C8993$Ë·BŠmãW«Uã½oûßýoÿëíænþøÉ7º.ŠodÚí6/ž]nljƒ€ˆpxœs5ÖÆØw컸Ê;F·H¥;v® ­)¤T$rÎFBr®ä2ëºÆ‡Ù¬+m·s.¥2Ž1K‰1!RÛ¶m H¼ÙíRJà|ÅËÔ&rì°IÔBÛ!±­ãR¬Ó€ê¤1•œ§RÒlÖ=~üÚãÕõÍ8MˆÎs L%$Ça7ôýͺ§‹ËËõæ¶á=íÖWÝÙò¾¦’y ÞOÎ1!ˆªÃ$V6ÛmI± NUg]Û…¥¼þW½ç§Ï^Þn'3e$ïc7N£™†Æ{f-‚ä*XªZ– kžIÉ1—v±xùô²küõ4änyÖ6 ³¤’‹U$ƒó¡’™©‚hž-æÝ|–sî÷½™9rD˜Ù4wîþýÅ,ð,„4ŒS~ñÞÏÿú¯þf?Y˜Ír”R²¯O-BvŽSJ–„s1N)M’sµ VÆ–Ýõ«wláz)1Ž)M)MˆT—:_›¶W<³«Gù€¦rÇüª×XÊÙj\õ3VhU1ïÃ,¾mRJÎU¢J…-¡}ØA-zI)|¨ÑuWŽo Õ+Ä,VÙ¡ˆ 3;7õÃÍÅÐlyö˜PHÕ¤~jûrŽKÑÊ{b2xß±ó®JAÔ„)üÁþøÍ×ßPÉ Šw„F8 { @É»®;]ßlnœ sßlÇ>Å@óùb>›Õ1© šU3Aucß©‰ ”|®ä,Ù†>üù›¿ö­Õùi‘jé£qQ­i[ï$B”qw}ÿíóoå41áÐ-Ù™l¯.üf½MÓØÍ»š ˆ›Í5‡&´"üòË»¶ÛÛ¾ïKLyˆXðþÙÃZöNDÁt¹ZŒy|÷Ÿþôù󱔓nédºµc¼/’Rªs'ÐCR¦©sÆ;ÜžHQU",*\”É!r×ÎSšÐð0‰0SD§©'j›¦.›¦EÊf6Ž£HöÁsðj0 ÃÐĘr’"¥dìºÎëF»m›6ø¦éfg§g!ø~¿“RÔI‡8^on¼1—~µZ˜•iê¡gßïÇ)IÓt³y«f¢²ïûMßõd}÷èµ×Íu‹Ó³)FMCœ\ÛÒwM3™(„@ 9ǘ’ŠJÑœ’Õ˜² ìuŸcϬˆ¶Ûjà'"‡Ô„ж¡jøÐT  $çI•зjÇÁí¶ððÕïÿÁïýõ_¼ÌS$‘vÞκ®(øÌ0›!3! ‘CCU!&0˜†iúœ“™1"³s„ ìnovë“m"ƒûùû\^nšå©*±Ã¦ 9E ¬õ® !šjÉ9æ<å’âfG|ÌXÎñ‡í“©¨!Bu ™bµ9†ÚÖJ\¤ ¨y`gª‘¾ý*"ª¢ƒù€µ¬Ë.3ú—{ç™ë÷ª/^Îuè×äi¼Sß¿ˆ¢rô‰ «ûgS%Dï}ûÑ7ª#•Ã>T*˜o"¦”Eµï÷‹Í*bJ)gË)!³+YF}p‹®­6±ºŠlºVTj³\Îgm;ŸÍ‡]ß´íɼyñ%UòŒ¨G™?"WÞŽ(©(ˆ*9R5b,Eî2ʬƒñ@•cœKÉ>o÷;1³ûg¯>|øù_JG¾ñôÉçüÏÿËÿøÙ/?ºZ¿,:ª$@ÕÓ³3½xùŒÀˆÐ9¾cz›š‘ŠiçBðž‘rJ•UgˆÌÁÞ…Øy$2-I>úèãÅrñúk‰C,™}@dD‰9«óNEûaA“Y×-—ó‡,æ')~ HÀD1&׆Ùbv*Yr‘¬ªí¬uÎ}øá‡*jt8ˆwE¤ï‡qO—«Æ;UÈûÝ0 i¾1("1N„ P´>Û‡I"BeˆBUM¢¥ôü«/s½XJ25Ɖž¿|Ùc (˜ÑÁ¸¾ºÙôýT ÞÞlwû©k»®k¼©åÔÎZ¦^¦¾Ís&Êi˜ÏϧTd½Y‹é¼],»%‘Ï`Ì>Æ~·ß=zôÚŸýÙŸýõÿýï^~U Ð倜+´îçÍ´:âër¨”Rf¦¥¨ØÄBë¼s~¡noUp¥ÈÐ÷mÓNã伟ÍÚ®kEÔMNµäœ¬`-V' ɹT˜ yS(&Þ9sÎyç;P‡ñ ÁD$¢˜Çç4ÚÂKнjFrM»h;·âz»íºE7ï1—2LcLÓ¬Q#æ¶}åÕÕùÃ'Báæv½ÙÇÛ›çûísvΣƒ)W}Ÿ)xrîd±\,bÜ|þÙ y>›ÍÛ&Åa—ö/ÁùV4#f0uŽó¨LcOtðß{Ç)¡˜Ö®¼”<Åþêêâ[¿öí?þ×ÿæ‡óïÇaÛÌ›@ˆŠ¿Ê­´’Ãôš ÄœsŽ9NÓ0ŒR²ˆvMƒìYDm·Ÿ>ýä‹g_~%"‹å©°_®V‚LˆbFŽPPU“™©"dvfš$yg"H XWŒfJK­©UØÝåðÝÑZŽËÕ_ísà¥jZJµ7}¹TwýLî뀄º=k‹"‡„¢Jø«§Jt€œ+Óª¾âWM1£Úi !™”ŒÌmÓÔ/’s®ú˃¹—¨Î>ª• j~¡ã³³Ó'Ož¼¼¼šÒº$A")å@T3Õ(‘ÃN¬f6ŸÏOV'ý8¤”‰hèGÉRÊãÄLE²©P‘_¥àŠŠ³“R ™ÔÄÕŒœ3Àê)ãY˜])RJJ5ª°6/¦PÌ`SÎEeŠ“wôòòêÅó—)fB2²˜û¿x÷“Ï–Ò0[„<(!™H-SÒ8Ö¥:Å­„ÈDZ>€ÊÂ'$߶ĨÕz(ŠFÔÍfFTL¹|ùôÅ”þq¹:SÐÙ|ÆìŠh.r1©¦”êe§i 4›µý~0£üñOÀD€ Ùyd˜âžQŲH0eæ7ß|óÇÿwH‚v0¦œ®¯oOO»oómQBB£e#Ø]ÐxïÉ\ÎyF&pÁÀ¹P[HŒ&`Plö蕇‹Å²ú8”¨íf¥À{?ýé§Ÿ}šeòÇ1·!CÑ8û}ÖøÕó QxíÑc pÙQÓ´j²ÛïœóyŠc¿sžOW‹¶ån>»Ç³úÉO 1Åñ«/¿¼½½Z=| hjŠVà•‡¯¾ñäŸÿ§vŠƒÂxPÕª•"Á‡ ³UûˆUtCM"T80c¨YRL‚ïÚv¶:­ö+M19ÏÁRÌÞ93жm]¥Àºä¾*̤m»¶m§)9WNWÝÉÉYÓtW—W›ÝÖÌ™<"—,ëÛu–Œ¨u8ƒdŽxÇÒ`;k=úV²$çƒwÞMӘ₼³,5031P –ÀÝüÁù·N^ýåÇï}þÑÇ[RôªVÌ9íü¤mÛ&fä2vݼöÊùýÕ2¥ñfÙ8BüõíîÙåµYɪUÃ<×—¡©ç\Û4MbJ)DGH%Mi}YrÊ­w‹åb·[ç\r.¾V”G¤"¬?‡¼$üòdùàþƒgOŸMãàW—¬HѦ™ÏÚÅɉ>øøsÓ‘˜}è|èºÅr“TÔŒŽÈŠ ïf*ÇÀ&!r‰~GÄÐÚÀ2X¦uz')«ûÀ»ø§cÄ«ÞE«‹XQ¹ÛÞ×û0<9–üÒ4Mmˆˆ‘¡öÝDÌPEuÑZvuoëV|>¾BµPvJ¥bUÔÈÀ›jÍA¼£ø"’ªñ!jËLTÅ ™rÎëõ:<.pLòmãÇcŒRîfš)¥Úû—"%•JTÛïû­lëç?MÓPQÃiù/ÎU^W!TRë1Dç ‰TÆC¦"’K)WFŽKÉõ£C ü|6G‚iš N1Æ©¨("HB “U÷æ7ß|úìÙõÅ…ªÆiúüóÏÑÔ$;ªzÆ£¡ùŽhêØÕˆà :¯¢ÝÌ{su­ª¤F1—‹«õÅíFÔ€200Sï]UÖ€B653eÏuTÅuÇŽ„SìßûÅ›åì›oÿÆjµª/¾™-—KïœY4¤:6²išTuÖ¶`‡aØî|771^.Nf³e«/WÚÅüìá;÷?ýäÃ{¸i‚ÙAþ{°n¨€)9@Ô8Nã~Ç׆3‚²ßÞ&QÅ@ŽÀÎVKgÆh‹®ƒprqÓ;×®NÏ@ˆ\ÊÓWϾûýééÉ0Œ9%fçáÿÊ+«““Íþòé³§ì€\àÙl† E‹¢9rûýND^}ôøÞƒG7Ï7E­ã–Ð¥§¬¡ ¡±œ2K)¹m"§AákšÉÊÑ«¶Ói0ï±÷Ž­äfnÖÎDÄÌ`¦êiïºÅÉÉJUƱú™¼÷ˆì}h›…sþõ×žÌæ \o·XÓ2Éêç„d€EAÍ1;ß äyÞ?cv1—õ¾_oÖ·"à¼?»wöÆ“7²èÕz‹DŠ4fíãÔ—óùé©ëV§]ãpä +×À¤°^]]Móå’Ù×£­þ Û¶EÄÓ$RîâPÆqœ¦)ÆdjHV}>øª³ªšà®ësMA:|VÇ´BfF"É¥ªÐ9U@çk’’¢þ |wpÿ8öêY)²ÙnrÎ!&03GÌ ×13Ö\.S½¼¼¼½¹­ƒ©Ä 6”brŽ‘‰í€ÀÈ’ QÅÛªY jdUú> cK¨fEÕÐPµh5¸1‚¥,1fïáwDtì*yXDXÁ¹•´¶zĨ¦1„prrÚ4!¥4ÅHγÙÑêôôö¦g¤C“ žc&4÷Ã4ì|JIž½g/¥jÁPE„Jðúꫯ\_>þbíŒL4•r°`c}BL KÑØÃêüj-à|ÿ{ßûòçÿølý K&’’¬dDèfíî«—ÜÁƒ'Ÿ|üY¿½,%ŠäZ5Üí7wÏw7Ó8朜óÞâÛ_½¼Þüíßüí~}Á¨JîÉoÞ¿>k×ΧԦ)§”§iº]¯É5@€QŒ UrÐ̧œZ¿49÷ÎOåtñ¬ªoåt¬@¢¢`.x*jy,©LS¼E0B­rYgzT! ô}/*vгÅÉl¾\,ÏŠjÉ©ï&ïC;_v„®ßµSó>XIÞyB1 ï–Ë%;¾¼x©*]×®N9÷¥WïɬôC¼¾Ùl‡Q‰H6›m*ÂäŠÀ8I1ÙŽ°KРd/'u,:æ°<[œ¿¶ÚM)½”ÍLÈPI²€)‚€&•b"}?í·I$瑬k¶`¸ÛõuTƒ°‰¨! ÇÐx/EJÊ)ƨE™ï›ZrÒ” @ãRº¦SÑSU¬t$SD®»¿š5*"!xcüð£^^\!"#͈ÀLÓ4Åý¾ßìÇ=ƒKÞ³YIt˜JððÎb»ô 63´šMA†Ç#Õ£ë%pôXÔ/ˆˆ©²›°n†Ь‘è‘=‰Xêÿ¨"k˜é×2³ëœóÄìˆ"bÉšõ­‹;²wÔLÔÔ4 J)"ÊÁ¢cçÉùêØ¬9‡e×ÎàH:£j¿bÇ)eÍæ|Xžœ¾ñæ›Yt§jÊË’Ç85Mˆ@¨v´ºCrNÓPÏÕέŽ]Å)1"8ï]c4#Â?{Ç›)21‚w\3¼cï| $4¡ErȤ` ‹¦XÌ{&.¢ÉP@Å$9vóÙ¼X ø°€®¨ZÐ\dûý°ÏEð˜¾âØG*b*9ç˜@ÈW@˜˜ˆV½,{ß°ãiK­Íä7¾ññ{?ÉÃNT j–:ª4=4µ‚T‡pdE™³:·*»TµÔ.!Ì'¿ù[?8?5ç2ŽcQ ÎíwÛO>þd·ÛªjÕ1I&žÏfónärœã4ì|ã—«UÜØïãÅby}±Ïf°/ùŸþñï§aIq­YÕvÜ= ¢€å8}úùç¿;ô³ÅlØïsN9¨"hZU€%Ë8L¨åÙ³§''óù0mwëñæv~r²œÙÅ˯ÆahšvþðÕ««õÍb¦a»!‚Óy7í×%ïñþ?¼ÿÓå±4¦\ˆ A%¸ÆûEt)¥’‹n÷ûË›3Ó¢)&s®i–S)Ódm×>~ëÛ¯¿ö¹ðþ—_ çH$€ AÅp˜¯Æ)VÓ5¦„û$hV™u•_ D@DÎ{€(ÆB.…&œß{šöv}½YoÓýsvÎŒ7»­mlJ‘‚DŒÈt˜ô¡ ~>ŸõûÝ~¿Ë"»}Ÿãnæ´ Î9— ˆ&jÛVŒJš´ØÐ9bïg'à»ÞÄÌε¾YÌç«næ^<¿shOŸðu:ã³ÝæfÚï¬&6uŽƒ'^œå½¥©ŸÆ4¥œŠ¨ÁG)¥ *䤈VŠ‘‘‘(%¡’AQÉ04'óö|yþúÅfºí×Y ªêÉb¹˜Ï>ô>£9v"’K®äÒÊ|7ƒ Ê4‰Y–²ÞnÇñj»^ƒ €­wf9FÕ\WÙ‡l$U0S×xç êAjfR}7uwÌÈE:ì쪱Žej¨ªcfb)å0cƱžhÞùœKJ¹R¢‰ˆ<»Cnö×È–pð(!"cÊñN©öu }½pD Œ£–"JAý;fSŒœsÛÍDTD+ìÒÔLAAÕŒ‰+À;çÛ®/R‰Rrz×mÇñÖ9l9˜êü †F’²Ãд>4‹Å2å"EûÔ÷i82¶\åÿJdff!„œKífj‹¥d“‚LŒßEÝUílpGaçðç ¾›qhŒ}Qè‚ h€È±˜˜û0Ÿµ'Ì«ÓÓ’ÓógÏBÉQËà‚ˆ¤œÄ{o¥HP 1fÍ¥d3òìEŒKÎ’ê|$CP br)ÇêE¡3FõN‰ÿ?ªÞëÙ²+¹ÓK³ÖÚæ¸kË¡€Þ4§ É&‡ÃéæPiB£ÅÄè/Ôƒ4¡J/Òœ6Éa7Û Ñh˜†)Ê]Ü6Ëeêa{ÝGDETáž³×ÊùËïC $À5^]m­H%çÒ2e©"K&,¼\1Ö‰jÊ)…h Aa³ÅPW#êÑ»³ý!&dbǘëªq¶‘¡ïìÎóž5iYìHI;ŸÈ8@ÎYsVDªœ›NÛÅ|Vôuˆ*9¥ýå÷L×4¬]¦©T„ˆ@o¿öÚK/¾ˆ LASΨüè\5™Í6ë+ÇP9×6õ'ºízãš*¦L§¼X,j CÑËɳÇݶ·ÖY¤”“uöððàøèh»Þ<~röõÃç8=`f;»¸8½\ž²£«õ²™LÙ8uUÕ¶“”³Qûî»?|áÅn6_Þº¼¸ôšV«Õÿì/nß[÷ÃËï½UXŸ>7ËÿöÓŸ^œ<HTÿð~$Ü>xë½/?ÿòÍ7^É¢ðõ“çu³X/—ïÿòŸº‹'@ÙÜ: "ÛºF6UUm×+ëë*D@49+¡K)#r×õ)+³)ï@mÛL¾*–÷tñ£¯«ªmš¼Šl6ƒ¡™TmÛìû˜¼ïƒ@Î8†ÌÓIV ÙV€&Fð|2•ZÃqL)fÑìêöÖÝYŒtµìRªöo}‡/ÏŸ­ÎŸÆn#É3W¨b9:ö¤ÐÎs{茙‡ýübÕ§ŒÀ–±€‚4ŒÁ0[ªŒ!à ý0FA3·u[UÓª>˜ÏŸž?ŠAlÕÌ&“ù;·»íÆ0 ¤œRNHSf) " ‚1ì껡Ϛ°LGJÄ{· žA3 –n2›ÒQÇkÃìxTÞ¼éº"ßY‘vñ‹R r)×Ë1(";^3!2vÑn”qbÌ Øšªª ÀÈZ …FXf^¥µr<£oå#¡,£•ö˵ÞO®O|Ý)UoÒÊ Dd®;BeÖʼãö\óG‹4 ‘™Úfo{جóõ@øÚ-¥zý›Ù±*1沊ÎY$ôÞŸŸ;W¥”T ¥l±ÖæœoþÍ׉O½9åU5„PJxf“C*¤IfcÉðdo{µL9#Ûº™¤”ÐS¯ ãŒk€Õ3u6í|z8i°uÎVÕþ­£ÙþÂZ·^n?ûýç]×íß¹Ý4ÍÞ½^yååÍòüƒßürè:LþÖí[ï½óuö$¯×kÊiâÌíÛÇ/Ü¿÷üù³ÓÓç x|x¼^.ÏOž¡æ~»M! *ìÜè˜T4*"d¬ªf6[°uI¥jZ‘¬*R5“w¾û1Ä~ûˆl7Ýb±ˆ!ž_îï<~òäèÖáÕÕÕd:¹Z.ïܹ[ò3L4›MCð*Øm»ã£ÛM;Ǒ٧dëú¥û/Îg‹£ãÛÆZU%Ã’ÚºýΫ¯þ£µ »ÏÂëªFÖÛ>d•œ»1d 4•s•aF¤Ë««ò +9Þè…‘ˆwÔŒ1-Õ„¹ºfh¬­Ãè}rNlŒs6ôý'Ÿ|üôé“”}Ùß ¢¶m‘õáÃG!¥¦°±’3CÉ‘A¤EEkŒ³Öڮ뽺^w_|uzÿþ›ùoÿÓÿûÿüõòüüëÓ‹¯Ÿ=Ù¬—GÇ·ü8lúÞÕmÝ´um«ÊUÆþÉŸþù_þ÷?Ù;:¢¶áª€ËÍê‹Ï?o÷FȦu/|çRyõµ7Ó«é½ÿëÿøßúm ‹?úÉ_þøßüOU{ɾû½?qÖLš~˜%eê»þwßý/õ^>ýʤ,È\·S[5hÜÁÑ­ªn¾úâ3DÃ\¢µppÀ{û‡ãè·ÛÍz³1ƹ ¬±ÎU%ºP$gT6ôƒ÷›õ:zOX–Ñ•™5•³•sÛaT™Lš½ý[Ûû|µŠI]Tc×uäV>‚O¨¡½œLáO>ù<ôÁ™Ib™Õîîoé+÷–'ÏOžŸ¬×[QE ‰¶Ž+ëðh¿Ý›9Mio6{v±½Z§5ú0¥ò|K×m(‘A¨U²W‹nЏ7™Þí£‰ŠõlÆu;Ý?>¾ÿ`:ßûàƒ_gIÆ:•Ýw €ÙrÒDDJXÕM5©o߽묋1?^]9þæ|”²Ëe½ ”ÆŽaˆ»jþ:Õ›U¯ƒ.»/ïMè·T)7[Bzݵ)ÌosÄJ‰]¯Ý}¬»-S&RfcL-ªè7æR4]Ûw?)¥k«Ý$pе\Iª¥¨0ÅÖ¸CÂt: Áï*Ä_]ʹº¬à qµq@ÙIÆ6ÎNSu­á.z T„$¢l¸içê®÷Š˜Uùi­|ëWA\˜RŠŒH|ói²ŠÆ˜bˆ**¢ÓùÞíßñãøäñ—&ænÓ«ÈM,R‘¬ÚöøÅïÜåÍv6¢(V·nß}ù¥kk‰­éú±ë¶à£›O³¡¨‘¢,æÕlBï½ñæ;ï=8_L‘ˆóù+o¾ñÚ«¯x?^œžæt6›L&õåÕESFج֟üîwÿôòü1­$%?"[ÃH×' e‘ò¿³\­EÔTULEÊ †¶³Ã·Þûþ+¯¾CxþüÙÝ;w›ºn'ÓϾør±Ø)í¬Ö+U†ñèè¸ëºœåèàÐYsqyilCćG ¸ÞlF‹fR5¶jÁ¸j2oÚvïð8o·ªèCÌ1ZgœqÎÕ£»®o[TUq‚Lc > µ¶X'Tdè{ɹ¬5# ¨÷ñjµ‡±m[@¬ÛÊUˆõÙÉØû@¶BÃ’S·¾T .æšr})W>|ùÙ§%åûîß=ž6zËt-GÈòôëËÕ:ö>?züäñÉCáP7¶m|ë†qý,ù•©n¿pÿî«{Óél½\-/®DäÅ/·í4Æ$ Ãè©’àjÝ‘q 8ì9ׯ1}öéG*;Ÿˆmš£;ßyûÝç¿zúä1 cw~¦ë¨ _OÿØX+¥DhÚÉÞÞÞógOÚ¶É)•|‰$$!P%¶ªR9§âµ)uwŽiWn§´ãÅkÏS¯õÖå4/Mù’…mC™8å]çáfeéZÚǪ@È`¸¸§KªTv‰EEævÌøm!I™ŠÝЃ èf·+I|=°Õ™HÙáCðD(r³­Šp}·íÖbBζjÛÉb}J¹²fç*wØõ)"Ys|tàªêìì´ µªªÊY6›m >§Ìü ò¾ÜI…ñMTԒ䜩ëJ €awyH٨ȂŒÎÞ½÷vÊ'ÏOÂ8Œ26UÈHX^Ô6‹»/6·_Í¢iöç3Ûa5Ä“õÈFƒ/Ê$ûúw_ùÉ×)'sk f¬ Í'53œðÙ3]o| #€ZÌÛ˜³P{pÜo·†T˜.Ö[%G) ÈùûøÞ›o½òóŸÿâoþË߸±‰1¦,&«qÞ¹KL1Q À¢s‘²¨dTÉIfóÅb¾ð~<ºuÛX.Ä»ßý.k-×M•å^JŒ±1&Éb\Õ÷ýôàh2™‹ ³¥é£±†øðÖœsT°“6öTLΤŠ@llaqc£ª¨" )(*€ÇÉbÆlDrß÷1Ec 'Q,ԅȯUõ×F´Ö bðc ««õr½<¾wWEŇÅíÍçmã☌ä¼ÞlUåèè`:Ÿd…¡ÙÈšDP2؂Ա‹)cnWÖÙ¦¹Ñõ·û7ë~tÕäêôÜ :vŒ”¬«ööªœ³†™é¯ÿúo/.žô½ïîßÝÿákÓ T¾ûÞ £O}|øóŸÓz»­¦V&ʹ¾Ö§”|͵©ÉÁMímƒ÷Çwî7mk]%m;]ìv5±«§{û{GLôËþM—Óüþý½££ý㻓éÁù“G¿ûðC@-lØœ…wÆêòm+†bL¢ÆY&îÇ‹Ål6OaL1(±Q̼mcJB‘¬qÖÚÂ)Qzk­”âZäF$½K(–Nô®_„Ýz’á2bbf‘̆¡@nžvˆ1V•Û%礪*e¹±´M ¿‰©Ø&éf…µ4LŒ57uýõI ×°LÎ’T€ˆwRïò'­µ)%Uºþ+4¥\VTvÁ;D2¬l39¼mcŠ9å"•œÓMLìzjL儘ÎÎ/–ÀÔN÷i½ZÅTz;…ñnŠ4e‡û½¯jk€@• ™‰Ùö”Io ™l•„€švq€vr¾ÚR59ºsï¥Ço¼þÊòjõåûmgë¦ÏxÞû‘Ü~»¿to1Y̧S0lBÒíØM'õƒu3ñ“ºYT\+ZÀEaà÷úÓMÇ8½æQü`4UÖŒ¾sÞl6”‡4Hð5¤‘ÍŸþÉŸ2Úÿû¯þ*g8PùU p¾aÀÕrU«L糪i4eII+b0UÚ¶ÉgIˆTøþÖJ–œØ˜”RðCΉˆK@¡L9Ä$¢˜…mÍÖ >(¢5Vˆ{ïa ,@ EU"²•ýô³Ï6½g„bvÑÅ|o6[XWͦíjJFàúýLD„‘RŒ„,ˆP~mÈåK€× ®ª |ÛBž£©ªj— ×L ! ¾$ÅÁûŒüƒýË˳ó§_‘Ï/±Dä*Û!‘±Ž%r!‰¾¥¶m›¦©÷{·Ÿýèý_†~3ô+SW÷^zðg?þ7wÈTûÇ“vÏ S–œ4YÂì}Øf ƒ÷#jÎ9o·KlJ­®›û÷s.§´í7’³1lë]ü@‹ °p‹Š@%i6¸Ý¤+ð«õ°é¶Æñáñl>oaˆÚõL›J† „ÀU1úÞA+¡z’Ðt¡oûtÆÄų¬Sç¦38>~õûß{ñÉ£³'Ï/>;ÛŽU›5¶1´ÆPŽ¡ßžœ|EJ±ÆßþÙ|oAÈ>ÄãÛwV›5°qíĺ \.¿~|ÒõããÇ_ß~ðòý—^vusptëÑç~õÅCËØÔUˆAU@集8îàG)'9eÃ80¡Àý^šLgÏŸ­Q3*ˆDc)eQdf(Ç}ÄíÚ1„œ21WÌz^¼‰±‹Öÿ]@U˼2‘H2lÊ´k ¥”E@TH•©èŸô[?7>î2ZÂÎ߆n眉ø†‚p]¿±¿¬±•ù’ \«BvÄêë†q9™9Ę¢³ÎšƒÃÃÉlþðÓßõ«sK C å&D"$$ÎYÖÛ®óOg{dzÙ³HL‘¬µeáöf²].-br•­œ³Æ Q‰½¸àã"(rUïíO^{ó;wî1^\\ÌZw´7ýÉ_þë/ß«+‡nè¶–ÍàgïüÏ~bG¯½þÖƒ_ƒ óùÌV¶jMÍi¼ËÚÇÏór™¶½:A!Kc"ê³¾ÿùå§{Q€ì%–Qü%ÃÌhÙjv¢ƒßZ¥Åljˆ$¥ä3½÷Ýw>üðÃ/>û¼ßÉYËŒ‚ ‚dé½ÿÅ/~9&øá¿üÑÁÑIŽD¤8ßU "x/»%QJÅà MktçÓëoŸ€RÙSÈYÈg_,]d dP€¢f.a°]×1‡¾ïš¶yáåÿw?mÙ FƒTÞ[,ꪪ­›¶µ1Í´­A€€*WW®>Œã˜R¶;$ðÎ$ŒDªÊÖ”·@‘]ª¬x:,ß»{÷ÖíÛ®ªcÆ"Ž›íÙéiàfvxxïøÿëïÞÿÕó'‡®ë{ëøh>m¶›m]YfËlªº©êƘJÕdj&M;™¼÷îwW«á׿þ0iL¾»Ld,[p“‡Ož]œ¼u÷ÞàSßìýt± „ÓÓç~è,fÐ~½<6 dF„Ü4ð×HEù“O>eÜ÷ÏŸ>»%Jøü“&ÝnVˆ¼\­OOOóæjuö\%Žž>Ÿ¶ÍþYÛÎx6›™íf“$gžNZgA@¢GñƒïCãØ…Œ%ÃÆÛÔõ¤ÁSJ)ä\.CfWUÎ[PÀ1ŽÎ9c-‹Š‚€JL¹÷°ÞFͪ[Q°Ý¦”ÓfŒ·î¼hªÅj½ IˆÈZnCb )p’ÔOš¹A¢ ^bæ,hèzÀX®o‚é¼~ó½ûo¼wÿí.žf¬W:.÷ýÙ“Ï?8ù犇ió¦žÌ&Ì<ú°:{V£ŒËˤȮžî’iRÆÍ¦Õz¶wú쩱õÉãç¿{ÿƒg¾Ø¬/T("á®?^N%ÉšSÊ"ÆIÁ÷ë$­uˆt|ëöÕÕ…æ I@$Ǭ;[Ò7zœ¥º&Â6Kº.¥áºÍ}Më^6R {\W¦7-D$4°ó£BA#4=«¢d!”£-K¾éà[Û]þ£a#š¿íŒ½ùÇÄ®1Âehj@¥è¶½³d{]c6¥•ÛMVK Ÿ™0—šÓ "ks–tïè…7«É‡¿ø‡üõÅÉ#çl 1Æ0 ½C¿ÍÁ³!ýçø;×ÎßûÁŸ€D0ˆ9G? Î:ÉBÈCûqèº>¦à¶f>›ŸsLÎV¥Ãžs2Ä‚ˆ€L\DÌ9åœrΙ˜³…=©cŒåxÖÌæ{³YÛ÷]?ô$bØV{mífÇ®™Ûª>9=ÌUCÓ™EcC°Ì­p奮]MhÂ(ÛíøðÑÕÛ/ܺµ@vyÁ²— ¢BËU]{ÕùÍ:Œ«V?û¤‚0wz|4·¼hÛFÖËíù“ÇϾz8ô> ÜyñEMR7³SèGë1¼ÞŽ«­Qºõe×]z¿- "2„Ö0\ÃX‹©^T$Åcα®ÛËååbÿààðöéÉSÉÉ–œ!1ìÊñ\$–mÎB†n²†7-㛺øæÌ-ßUQI»èÎΛˆ D¬;° !AyJ1Kkíd:;8¸Ý¶Ó‡vÛ¥{fe&PàºoÑû•å%È»%þYÙ*2¦X\L¹rrÞuº‰¨iê³É’w-,‘’P,ç{¹—‘¨”ÛÄŒ»\}™Ì£Q8º}÷û?ú“ßÿîÃóÓç)eWUDšr’TfŸØ´Ó{/¼tëî=999Y¯7)Iݶ‡‡bìÆ‡ ²›`—¨e–8SœÖU šC’˜"ø¡ï%edt®ª› sPW6 èÑVqŒIJĘ֫UyÒŠ3BÚ]?„¡1‡Ç Å$7«qRQÈJ^ór?¾Ü¬º”Ï.×›£ƒý®ÛŽCOqÔœ%‡ !ú^|rö@ÄÊd-W•#ƒ’e»í—Ë•$%ÐT×æÎ­Ãé¬Úl¯léT”` (($?›G¾à_5“Åžkš·ßy»²UÎD$CNälå¬ST4ÆâjµÊQK§¥r&K™…³*2±Adæzb*Ãà  )eQµ†ËSÄm;¹<=ÿ¯û·ýî#‹ ŠÔ:cdHeÚ®º~Óyȶžxê¦mÛI ¡¤ ZCw‹j ¼™î »%®]ÒúE  êC8yúü‹O~÷äùSjÝáÞôñÃ/>ûø×O¿ú¢†ØÖpt0{ëµïÔU=Î&)zQ(>„º®R¨ÂÐ70^çí÷¾wpp{µÙþî?kZ·þòËaöŽs:L^1JGß_H² ³³Ö2;k€]Ì%.œw D¨È*ÅÑ/—˽}:¾uçòrbÌ…[–òK»Š¿c·\ƒÌ7àÜÒà¾ñ«ìÚ°ãs^·kô&U"*圅]1ƒ²[ÝÎDœE#è­û÷ïÞ;}öäË/> ¡7Lˆ’B’oÚ/ˆ¢e&IÌ$1ÜtoÊlÓZ³“ñ^{å ù ˜m…LÌ ò.#S’f×÷VÀ”’q‰R Ì\n±,JÖÕ¨èÇQ^|ùÕï|çµ³Ó“gÏž~ùÅç)&Î@§Óù½{/¼üêëÓùÞ0tÏž<¹¼¸èûÁ8§ 1çISÏçÔ÷Ã8 ‡–UXk­ªv]çˆQ!%¹ºZQÙ@Ɖ9•µI@H¢Œ¨@d*£­Å$€zBEPK Fо%v2ŒËñ£Ë«>Q BòcŒ1ÅÓ0Ä~‚Ó*EÅØSôÑû0n%Œ$¾"`ÐS? â¬N[‰>kžLZkÜd¶¶ýfµ1LÓi£°Þ?ØßlÖÇGwÏÏÏãèsNÖØïÄœ¶Ýå§Ÿ}øøì©k÷ü“¿Œƒ"Fˆ18k6ë5ƒ2¢åÕ bÎ]ßm¶Ã6Æ0ŒãÞÞ"ñè»”rUÕ¥Ç3ÆÔÎöb’Õr‹ù¼€†q˜L&9§”b·íŽöö¾úüË/>þ˜Bß2ƒHÎÑ2ª&c)çàœ#ĦmŒ«|"éúíóçÏ‚÷9xÆÙ$–wÊò*i,;t„;—\9–ÙØG_?}òõ×ûGG¥€xôõ£~ÿûw¿ûîdZm/Ÿýîg?í·Ë4 vZ‘æÿÙŸ½t÷…³“Óš#)cÚ¶­ëIåæ³ùþf;üêWï?=yÆL€ÑLf{Ö¸>gCf—¹@šÏŠ´íûsÓL’»¾ËV¶]7Žq½<7œ²À[ßý?øÓ¿8¸{;y~y>N!ûÿöÓÜ\k¿ãš! 3)"³Æ Œ#D/1YÃL˜@Tøƒ?œN¦Ýv‚𢭠~¬•ÂE Áëf“y]Wç*ÝvüS²Öu}N)Mgs[Ùqó¨ z}ô`|Uue+WUUVaáFÉ!¥>„3»ªT“¢ @,j¿¦©D"AÌmS7MûØ'7fmHkȪ!\^t›Õðøéú·Ç“ƒƒ¶iܤµ³‰é·Ão~ùûÇÎÅ0ƒÅä0WyÙTa1¥¶âœ£a2–Ø1{óÙ¤mkãÜþÑáÑññ´­6›íÕÅIJ±m*RTp(ú˱;Ë©Ë)€ÒŽò ê½·µ+)”„F…¸d&me¤»õÚ{ttûÇÿê_þÙG§'ƒ†mÛÔ)FF¹oîr‡…¤h,s)uwj‹ò ·+Èå,¥eô™: QŒ»‰¥qV@SLŒ„ý¸ýêÑ—‹ÙÞÑí[G{C·õCzòäââÜCLº IL9¥\¹2‰…}Ù.1Í2,(¯Àå–rýƒÅ‡·3*)¨JJp×Ì*"š¼7l™Xv4Ô,ã0¤”‚sŒ³ÙäèÞ o~ï{?ÎùììÌ÷=Šž_\ª¢uõr¹|þüéÕåe–Ü´µ±Îù¡gÔ”ré#«–•EÃ\UUU×%æs½ru=cPEDS&Û9‹¢îœ[9&ƒ;Y3 *)…àxd…“q`1hèúmB–¬e!H$IΠâ`Sdc)lÔ1[M)yˆ>Ûn}Áš°r®™Lš6uµP|?t"2_ÌÛéTcÊm;QÕ³«Õ¶ë”p2›Se& )Oë Ž ¿ÉËTyý곇«ó Ia1ô-¯®æ³iôT—W—“¶µÎ"a7tH8™LRŠ)eëgÑ«å2ÆÜN¦ÄfG`j§³Íj»Zmªªnšº¨F?Tu Ì¼^­X¡_odì1ŒÆš( *hx:­f³J2îïTm»‰¾™ïù>5“š ¬×Ëì}¿Ù0jÖ¤åUUAT‹+¢ïûÊU"ù›­<É!%M‹ysûÖ±ä„ÓIý“Ÿüù¤2g_?zxöÕÇ9Œ‚mëê½wß=Ø;ºº¼Z-W~ôÖ¸”UbÒœ%ÅÌA$tÝf³^£JåÚÙüà{ßûc2Õ?üãß ›«ò8 ¢¢Œ¾;9}f›Æ[xéèC(Žæ*¦¡žÍw_xù­wgû{Ëíæ³/¿˜LrŒë³³qs!ÕæA¬±ÆX²(E? !âb”ÁâçFf ¸¿hó!}øÁoÌÅÙY¿Ù B¬*YRÊ%ñ葵ëx»ÙÂ0n MÓ´DÆûQAÛÉ ‘¨ï{D%ªr;P½³ÖÚ2€FE2 SÎÛa˜5•±v26m=ŽcÊ1„¨)“‰Ä}J¶±…åŒaÎ׃Eµ$s N˜£ÄèñârèBÿ|ݸªjjÇ Ëóó³“S }kpnÝÄ1GdÛ‡õÙó>,f{« r¦ª+f+ŽŒ«sÖéâh2Y$• 9çh•,ãÐID5v+ƒ%Ìl²¨‚¤ çëá*z¿ãÝõ@JR–ýÐo6—·¿óê÷øý¯íþùq Œš Û¤ *Ì š…v¬s,ÅëSNùÂNÖ X€?LxSVßDì¿a©#²1“¶„íf3Œù ×WgÝziÙÖµI›åÕryUŠëÝèªèûPˆÐSU•µ-£Ñ2­-ƒ8Ä]>f—‡É Ä*YBHI2 ›œR”cdkyØÍª ÖU¥µSwJiæ¡\U©‚ l}ru>mZï‡è£eRî¶ýzÓ?n6+h'ÅE àG_@7}ß#RáR bñv¡5Ž€U¥0ذ1£” µŠX|éq bÙ/ÎdÄ1Y¶ÈÆÍ)gòƒ•ˆ"щÃRÆ\Š ]¯4 *ƒ¨HsàØ'66„¡ß~ôÝújØ.g‚ÊÈ<«šZ®J/šzSÜl†M7¶Óiíœ!Z®Ö›±ß¬7>EX­V)%¶lÅ&KFE”@PCŒ²ùÍ/þ~>Ÿ Þƒª5 Ò)‚fψ„>Œ)¥¦iÊådŒQÅà=³Kd¬‚“1V“h.Â-ίF…£÷µµ’r#¦T¶ˆªZG9…ɤ™ÎfËõæô²ó’Eó|2©¬kšI_ ãØ d%M91Riùªˆ$‰1DbºÃ3‰¦²)rûö££ÃÊÙCQDŽoÝúÙ?þÃÉ£O'Ð;d[·ÆÀ­Û·_zðÓ‹õéɳaÛ cR 1eêü ɰq‡ÇwRÆaØ:cš¶mÛ)±½¸8~4LZÆÊªzzö¬ß®SŠ’%eQ¤²@ºØÛ¯Û¦ëé_ýä'ÞxËîßÞF_§XYóüÉ×ï?}²>y,ý’ã†tÔœ "dBDcÈZæžrŠª¢"  jQ΂köŽOÏÎŒï‡ä#1‚æ”RùÓˆèÃè/¼1ÖW ¸Ùnؘªž´³Å~^¯—@s„,™%+@Ž”²”Ø‚†Òvj­jUκ†¾ï” gÙ©­ªz²šUR$ro©2EÀ iÌ•GH 4š¬µwί ˆ¦~ͲDÓ圗W²Ž)õÛõÉTSX®¯îÞ?^,Ã8™Ïf‹ýnãoÝ9ºÍ HTQ}ècˆUÝUŒ1ú1ˆää¬+3k €†q4õ|‡S'ÉY®£/c.»¦R…$§¡Û‹ü5¶nÚã[·/O/†®+dÁÊU")¥Ä†•Xu)¹‰±©"RQj|³R€×˜°o@¯;9¤€uÓØºVUSGõ¡ëzÍ)c…Õ*§sŠYÄpé†ç²±…DÎÝ Ã,€I9ÆKô›QÀn' RNY2 ©H+H”ìf<9p‘ !ÿ—ˆ³¶@:ˆ9‰8bb‘qô1å=4+¾ Œ!Š$>=9´Ók—4—®ܰØcŒã82sÑJhsªJðqÀ\]óÅχÈ|½/–sÂ/Äkt÷a&B2 åWW˜Ê! 9–ûfbS!D 9‰¦œ¢d•¨a€RNÆX£­Çn»Z>yü$†,)E?ÆÚÑl.)‹j.7=3ó «¡°ºa\o7WëÕéÉÉz»í·d4HuÓ(b!¦SMɹJTQ ýÙéye*ÜËmw£{L9+ˆu†ErŽ1Œëpê®äÛow IDATµ\(ŠŠ%œŠ"ꌳlJÏp7VÉ%8»ÃmªÈÇ]6všìÒ "æ⤭œµYàr¹ÞtÝÑÑ­ÊY‰ÒçþÙóçËe†"Â)™w„@IÝn¿ºðÒ•ÙV“©µUßõdŒHVÈ—óÅì½÷ÞÙ<}H¡7l5UÍ«åòçÿüKCßwâýb*¹¯ÂËT$€]ÝtÝà}\oûÇzúÉï¿@•˜b &"Éšs o¼ýÞ«¯¾º˜ÏSã¦ï-óÞÞÜÐ+G·Ž–Ëg‹[·¿ÿGj'{OO/>ûìÓÃýż¶¿|øñpuRkÄ4 DA œrœ‰°® XçdT%§*¤ ’Ó—ŸöÊïe…×ßzψä໲è*uSO§³*Å©ˆ÷ãjµDPçv4Uä”cðCY&®šºi›¦­Wëå0tY4–wrPmêfÌlBÖ#­:cž.d¬±n³î5)Å¡0¤¨c¯")&ÞY“Rc]Ê9ä´Ó‹ëSA¯Xk«ªŠ1íV2E‰PTr ÛÕºlS»ªf몺Þnº<3ðÎg "JÈ×Ù•›ýþÝ›AL)„r¶âM~÷Ñ|3tÝ­_–ィfI ‚ >„Ò0M1ú“ˆs¶ì£–£T^r!€‰iûr² )ª7m)Ôø¬S,žD´Æ”Š&§”`7·)Ly.À$@5Öìø0Öt]‡BÆ:ç sÎ1ÆÐ~ìœåò²’‚/¥;[ÃÆòf³ýÀåÕCÄ{_´g̈(>„,BLÖ5Y`½#Ã7G÷.> Ι½˜Šö˜méZ‰€"’ÉšE‘‘ ! a*—´j¶ÎŠP`cêŠDRM9©æ2É¡—$馮š¦v"Ña׫ Ê)åU±qaÒÄr˜;k‘1$/*Æ2³©¬µÖY[ÙÊmº.EñCzoØWá7ûJ„)kÌ)YcʃOÑÛB¯,ß"U&CÆfI)…¤Ì×3ÌY!•´«R±(£*dJ¢šáš”KD¬@U‘P‘v{×(éZMÇd™ !í^’ؤ¬€äœSQC%Ÿ)BJ©”vi÷›óý¡¡;á0 aüýgŸ¾ûøë7öÞ%Ã)„ÚÙù|ñýüð«>xòÉEÛš¦6ȺY­žŸjé‘–M6Æ´Šâ8P0D𓀆ÞcK†€QÀš»wîLÚ¶ª,ƒl©ªrJËÕÕ8†\¹Å½[[•e×Ö‹ýÅþ/ñ‹_ýìg—§OSß™$nŠÂ91ÛœR™‰xÍÎYR 1$T5D©[sëèVÛ´8í2®2šÚ¦]ìïÏêºEÄ/¿øâäì 32—KˆyúåêJ”ÊÕ8'CsF„Tv*dFe6„jl„hÝdï°ë¶Cˆ|½…Ÿ²(1Ygˆ0EŸ}/¡•à™M[×ÄÖFdɃX3F×CuF{1‹‚6ŽêŠ÷÷›êvŽñ£>:9ß\¬KyoVOsÝmÆ0<þò‹~º·¿Ø?œÍö,[EÂaˆcBˆ1DIyÜvÉø’Û³•‰!~ˆiDÍuå6Û®À-s&“rcØ€ªrÌ€Š-OGNˆ†T6GŸLíq:›ªTÑÞ ¢ª¤ªL7›÷7©vDŠ!»û)‘äoCTnäs7-r ˆäÓU8†–Ø00¥¨%ÜYîF2†™QE@w‹ˆHl´€U‹¤” | —E$bPL9•‘ «¶I)¨,w"©H r*^ìRõ  h†1F‘Ä̈ê,GªIs’äA„@T4‡ „ ‚"šHÚ¦E,¢& qŒ!6ÅQDe1å$ %ŸˆEh\[ ýÌ ˆ@Zº8ÖÙ2Š ¦ª…¹¸·” `\²3Uí\ ~”¬š“jf2dÔ`˦¶>E YQBa$A"rÎY{çÞ‹·îÜûõ¯~µZ.—WWcJ: ’ÅYk +2ÕM¥ƒÄd ±E2ª0ŒC×wÓé¼v ¡AdÆ™SNY… 8ƒØÊMg3TÍ©Ø3XP£ˆ¦Tä6 À–¾ÿýï].¯>ýäã †z‡¼ßá¯‘Šš‰JJ E„U´ñ è‹4—˜ËN^¯+ ÝqѰrí¬[Ãd*AN¢*`Œ+c¹aµ¼ZžwÛµuF3æ˜Tt÷Pþ®Ì®âE@•e¢ÅÞl2™¨äÕ8. "®ëzÿ¢4­"äõfcd6¢€ŒÂ¼ûäQ S% Ę+×0›’è%bBd6‚mÕD!¯°í»õjI® JcLÆ8DìÇ­ûÉ÷³©]ÌÚ®ë¯íïWJŸþî£qsæ´£4"2vŽ™†a‰’µ[©´-B$®LåÈ€€5ÂÊóÅ"«æàƒtæþƒ—Ÿ¢\^ž!'À¤…Gȳvñúkoo»m߯˜ˆH™-!ã¬ÅóÐË(¹ž4ƹÉ|á/Ïb†™É¤”D.„ËŠŒÍtzppøÎ;ï~üñDZßHqx沑®PËÙÉXokfcªÆV­3ÖV "¦œEP²fLÑwën”󳋘£­lå¬a(=ÉåÕò¿ùíɦ²óÉÞ¼žL'Uå.Oo×Ï/ŸÛf:¯ê¶²µu¦ª«qŒ›uï}â´ÝÚœŒB2!ÅËõêbuåÃh¬ Á§Ù4¿Sƒ®sÆ»ä ":WdÝZ ÝHH¢³‚2Û¦©æ³YSÕ–Á2ÿúW¿ ãH¥ñ­XÄ%ùyÝÔ@Þñ ‡òà¿–ÌI±YhL;L Q‹°Q VÐZç‹ QË™NŠb ß`Y.Dæ8t¥¼šBͪ9ݼ.¨ªæXò;Ll-9çæóYUUg§çÛmW¶Õ™ŒaÞ=ß……’ðš£5Ä”r4%>‘“æè‘)ÌZ)ÞÚâ-ig3ç\ßÞûqðåÅH@*k4a’ÌlЍÕ9[UÕ8ŽÙ'½†ù¨¤@`wxbPÃVUˆi¾Ø3ì¶Û@ˆuU•æoJI$ÇÄ ’£ø0Æbˆ)!¦”‚O]ץ䭣ºq(„Ãv»îúív¼¸ÜŽ!HÊš@‡¡«¬™6íÓ'O™ëæèÖñ½»wÿÇÿï×—W¿}ÿƒÇ¥”¬±dH’a u#d*‰ƒ*©BŒi³í../ÆÐpßw)ec¬uŽ˜òÎfµƒYW·MóÊ+¯¬W«/>D7i¿÷/¾¿Ùl>üàý”1ß¹uçÿèëvòåÇ;E«î÷v \|» K†‰TE-LÓoa»ä2jQß¡Bao¨B)+™ITf³=W7íı«Î—›ç§Ã(µsÛÍÚÝvØ^\œl»uek¸nN–°mb.o–¥mÈÿUoúkÛqæç½CU­µöt¦;r&%K$»eI–zr»mÀ § Æ ^ò9@$°N€înKVjµJ¢8]òÎçì³ÏÖPõùPû\9I.Ï­Uõ¾¿ßóT³Ìñú39ðëÞžµ­•²¹ÙŒCß¶gf**1¥¦éÆþp}³¹É1%pŠ·A5 ,*`îàƒH7[€º™d÷à$Ƴåy?–¡R$¦¦™ŸÞ½¥ ñùõî7½šº"Ê,!„¦m™Ã×ßûú¬cTy™¯~ð_þjg—OãphtÙ£ª3)™93Çn¾XÓ Í£ Y³ ÛajšØÄHˆ‰‘ø øÕógú¡]DÓCƃ3BŠ‘ˆöÃá0ö16wï„»gïݹß}ôG !T#Ú‘öÀAÕúaKYØÉÉéIÓvËåÉvs]WH``¤ªîêhm›6º•OóëõåsS!FB0ó†µ]"æªÄò€æˆÈQ¬BÖÛYB$sÀ9'Êe¼¹Ùõ‡É$ÖRÌ]v›ýz}•,f'Ëexýõ×V³Ù‹ÇŸì_»WlØÝ\¤4‘OOOÀp¼¾ÙõSVgÄ&%Â`‚ƒÐf?¾¸Þ(XÛ¶Ó ¹ëÒ$ç^¬õQHÈ·Ô/¯#šÀ„Õjìj®"¡ÍféÁû'ËÕ—ŸqØïJ.àFÄê^éZp„Gºèñ´%¯ã‘›UGr· —<ŠäÕBˆ•‰n¦‘IÍS¨YF07&Rw„j´Ó#0ëý·rŠ›[M[Ö&ªWé«Ô !¦”(Ò4M¥d$õ’§6õù€î¶°êrsä* `DèZLŠ9ˆ1sÛ8 h+äÊÌÓ4!QL‰Ä” ~W¬¨ 6§q,eª‰Ow½E}9š*#4)QQµÕrénãØ×ƒ1މ1€©Š`dQ©|fD+ã¸yy 1¥Ãá0‡®¡&b×6ešò~—§!Eb5!@$œÆ²¾¾Z.šÅ¼9;y¸Xýýßÿãååæêj³¹Þ†^ÍÓDÅÛöp诮.÷ûÞ€îܹ¸~çÍ÷ßÿ†”,9ç)‹ }Ÿ¥Œyìfíw¾÷½ŸýãÏ>ÿâQj›”âr¹\¬¶Ûín=W/_¨hÕŒø‘ècxì‰êååån·+"!ðêôüݯ¿ÿøË/‰?2-©i¿÷{¿ÿ­ï|ï/þâ/.×NMÎÅ¡Mñ8€¯'ûm|ö¸ê!°ªaúmŠ×ÝjÁÁë‚?ÞÇáž9€£ŸžŸ¯îÜWnÒ|¹ïÇçW×Å€ˆÇqœ·mŒüôųëõ†)”R@kݺ²Ý u'ËÕw¾ýíþð‡Ï_¾Œ1 ”H¦îm çgRòf}µ¹ÙÂ~Ÿbh‹Å·¿ûÝ'Ÿü|»~1ûfÑÚdƒNŠõÍdUt~²úÝÿéÅÝÜ-=~6•r~r ¡ûòÙåï~ãz;rLm×îv‡qšf‹åûüÎryRÀAe:f·Ì}’€—/žw OO–wÎïûßùOÿûÿùäÓÏ)oÉF“2•ÌÙìÁëoôCñÐ>~±!HFÛ†ÂìôâÞó«¥ô’G° 4ºÙòŒºYV¢1RøòËÏm¹Z¶]§ªÛív‡ù|1Žãá°Ÿu󳳋‡¯½q½¹2Sfƒ‘¨I85óýáÐtÍ|>?=9‹wÛ›Nˆ)¸ ‘#AÓ¤¶mMõñã¯TÔÝ+í-ƘBh›äjê*%g‡˜S“ÃØ‹‚¨…Ð$„–0e•I¼dN'«–¸¨Ú0ô7CÓ~{¸Ú\Ncncºs¶"›6—/t•ÜßX.æMd/mwsÓo79ÍÛäîÐÑz¦òâ‚y„~¢CŸK™BZ sŒ±¡&51— ¼ 1A gôºüD31w3E5CõÃ~óô‰?U¿|y9)‚`.b‚ Ç6*ª™hACD £pœ¹Of@P¯SLE RL11É BMOz@ªÅ²Ô\¥A}3¢«–¢Ê5Œ R5r!4UÝk*¥”£4*ï\FE$Wq3$U3 ©7)YŠD74'"—ì€L`*âGT0F"fc .±»ïÇChÕ« 1«HFB0È9›qu¢Ö v)¹¶YÜClLKÝä’×ë«qìMЉHÉD#°3ƒVs ¤ZBÞm÷fPŠä¬7×ëGŸüf·?¸ùj5_ÌZ0ÉÓp³^O‡=1:nšt~~æ€WWW»›M×EÕq>ŸK'_>»Þì®ÖkÉ™«Ú¡~FòäÏž<ÇQ² ýøâñ“_üô§?ø«¿†©ßõ‘803a? ŽßûڻϞ½üõG¿yúòJÊbÞ4)öûýîæzìÃ~ψÕFTÜLå„­&‡Ãþp¨`¢Àq±\˜kÎ4mÓÎÚ——ù×ÿù“Ï?rîfí-6Î ¹òª_÷ØT®¯[щ™×‚ÏCø„Úrw°J…•’뽄ÝÔM؈/yô%8®××o¾ñÖç_|™É·¾ýÝ·ÞþZZ,?Ü ûa¸8¿S ýú‹¯8-><m¶X!ÐÓgOûa¸÷àÁéÉÉr±b6å©HÉÓT% fv³Ù~ôÓŸº ï¼óæ÷ÿ÷?üK¿ÿßþ—ÿu{#šN†ßüÆß|÷Ãï~¿´ÍüoðƒüÕ_ß;¿óçÿîß=xãmé§?ûÉõÕËõó¯>ùù ${^Ü{ð¯þü?|ý›ÿŒ9ìw)†iqvvzrV¹š˜ðÐï.×/Æ<"ÀÙÙ¹j¹º|Q²ÆÐ`ˆÝCŒÈaš²ûy1›]œÇn6SK± ý~š¦DqÊ¥/ùååÚLSJÎHgsšÍºy×MEöC)cF¢¢€”ãè±Ñ¶kÛÙTͰdÝïdÕÁ5·›%OÓhaÑ匄ªˆÀ䦕Nc’ÕÔÍTФØ0¸J±¢2MnC@"sPpS¯£ë2Õ2j ÌŒã4@`• ’§q¯´ m7Sm9&¦ÞL벎…ív»aC—Wk [C(R6ã– A EÐÅÕMT ´ î""2"™ú Sí—õr(¹ŒÃ8ŽÓ¬ëHú~y¯ž?Ó2å2©‰ºYtìåJ))…çÏÿê—¿ˆ7›ƒºOy:*˜ˆêª<ÄÈLDÉ91;Q<\ˆÔ¥˜–i$7³æý?œ-æOŸ>ûë¿ü/7×7‹ùô‡ëõMúq4F uX‡`*5;S¢+ú¿2Ö­ðhw³þ›ýåöz‹h‘ã8N?ýÙOÜ=©£Ç[ {µ\)¥²à¿=Çð–]Z™f@Ç.9瀦ŠPß‹Ç'm%98Õå|~þàA3ïÒ¬ ©!nÎÎçƒD¢+-ÑÝÊ‹gûýFË(¥ ‡ÀG»aê>{þìúz]ÊÕdÀÕu5_Åõ¶Y.O¿÷‡Ü6ñå‹—óÕ™"©Ù¬KØ Óï|-5 ˆóÛÔ´íòd·ÛQ@Œé E̼¢LQФ³YC©¿Þ¾xúä;þŽM¥¤ñ›|ðïÿçÿð“¿ùÁßý×q—þä_~ïOÿìß6ó“!P@àïÿÑüÉW_{ïþÛï|³]Î=Ñüñ¿Ì“}þÕç—›ýöù#úþ·ÿàµ7ßOqÖÆödÑÓÛÝ^Š1ñr¹<99I)íµ²Ù^õÓ>…„îÊŒšÍTM5«Æ›®E G(¦"ÅAÕ”c“ê8 ™b3 Ó4í^^]51Åíã4‰è²kkˆm»;Ül·9k§ÐÌit·1äÕ Ýoý”³;…~9¦À 1:¨hÙ÷‡1CWMÛV_×%Qvë¾?rŒÝêülµßînn.)%5¨+wóùBTtI©L2uËÚ£«iD¨wÂcGÃKɪ\RŠõ2•@ED$‡D¤„¤ç‘xš¦±?€{¨Î³Bœc$fU !rˆfæjb*ªÅ¤¸«–bªuÚc38Š*ÇÏ/î8À~¿‘"HÇ!3«zÕIº©{ÉÁ9hÎ’'܉‘k”³žVÆaÌ™aå s´qØOS¿ÉešÆ‰ˆcŸšÔ¶RM""¢« 8  iu6Ÿ/‘¾Þš«Ve€\©m·¹˜À¡ ,kX[¤¤”ºv)xÛˆfT7G»-î&pŸl›iÆìcœ‡Ã~ì9GèJ­´™šYjRˆ‹øv§›íÁ«kø!†È!ÜBù¡j?Á§iMµ­æÀ„®ûÝ–æÓüï´ž/}?䜫H]TŠL"“‰€#º9…ºÕÄê6gvw&®µ"6WT7&B+z}µY_®q`675­” H¯º¡®@ŽDŠº–wĪ.ðWé©‚ÿìèO:Þè½$1sÌ̈¿þþç÷ïbðn–...À`{½ãH¯½ñàòÙó”XE¶›Í°Á1ÆH·èŽ£çÝÔ†i¨6ypèÇ2ÅaÔœÞ~ûÍÓ³³’s‘âĘDô×J@ÿæOÿ»wß~swyÙddîRûλïLS™u³¶i/îÜÆ©nM³›Ñl9_œ0§lÀ™Ù™#3Ñj PU‡añ°½‘aôœ§ýáé—šÙìíwßûý?þWï|ퟜÞ{ýÁƒ‡o¿ûîÉÅa,ûÃÍnwÃÄëËgfy·½ùì³ã¬¹÷Æ›‹ÕE»êBšýë?û·÷.”аMÍ*Rªkl™rhš¦M-:šŠJ™Ï:æ93S qè·yc ¬~㈦5ûˆx ‰ xŒÁÝúþP€ÇàˆÛ7Þ~ól5û곟—aÓÆ‘‹)EÈj×»ý®÷‡A‹z:6 !çbsóS)åpØ—¢ŽLܶí*5Tò¨SCÀ@„Fèyêµ”ÒrÖÝ=Ÿ¿ñðüÍ×ï~ãkï–qûɯ~*eš¦|rr6Y¼ÚÞp(yZ?}q#jØ÷}Å xŠaœÍrßZ£}Ìñüüìüüâ0Œ)"‘»ífý|8ìšn6[Ý]ÌfßýÎwûÃþ/ÿóÿ³ï·^&sÖCcæRò4ŽErŒ‘Wp10"È4 Ù4Ü”ib)cUÄ\cˆCÉð÷Ùbbz•à´ªºv XDÇa¿Y—a`ŸÐj¾p@à@ЋŠhÍÞsI9‘AlR¤&WÉÅ­"T92SˆDdª#Ç}¿Þ\_J)UCÊGž×D)!ÖR"Bý¥nBJÕRc“R“ó´?ìͪšs.Ãt(9Û-ø³‰\ FPïwÝl^’¨ª™h-âr`2 pˆ±Á m®®;w>ö¼~0j tp1tCwD¦úW‰˜)rD,*PæTéÐŽè^G¯žúq;º× ¡zÞÍ+±Î‰R²’ÝÕᕞë¿ÛPôOÿì¿ÿþýÑXö]cç'̰Æ\rJádÕÝ\ÚéÉìòÙáòùzJ×Í‚Lz˺0ðê* ‘)W }Œ¡¾ûáœÃ0nÖ×›ËõUlÛ³ó úø£_^½\ÿñ·>xï­·»ÄdIµ~[8¨i)cašÏæ©i9„ÊØs1WGÈ‚6pc€a2›r™´Ö¶#ƒ›ª˜©ª¼ :ý¨SY_^Ý\o·ë«ÿ÷ÿþï~ãë_ÿæ‡w/îYÓœžÜùóÿ?™š˜N¹ŒÙEùýr{}y¸z1ÉõÇŸøðüîÃ~¿oZóák‹o~óáƒ;î0NåfwØÞl×77þÉÇ¿ ËÅòîÅÃn?ô7%„Ô6îž³ˆ¨Õ<œäú³ˆõÖ™MJ¿Ý…¦ )†&BÉÅnoDÔÍ»¦icèV« Õi¦®iµLªÆÄļjW "×»1pýžã1™Ç€„L˜SJ"¢2<}ú|œ QŒÍìôTXÊȨ)&1˜¥0ïÚí>Lžc¤”bŒ¡íº·ß{÷÷ÞúOÿ×ÿALf>ôC?N4äböCˆ‘Ü`ÑÌÛØURˆ±‰óù25ÝI±!ËãgÏwû¾uݼC¦¦m³øÕ/wÛËÀĤŒоøô×}?”< Ûó;_ïýùl~µ~ùé—!»O‡íz:ìûÍK™F°ÊbEU­+C7T!Cw'fs *š,Y%«V IHŽˆ)³VhýoND*ÊuõjG“ŸTßžÚqE†$RԴ뺶m$ã8T> ˜!BbŒìîê®h> e¶µ©KDæÎµÍ) DÎänùùË®MjE'É’G‚äÊÇ( 2¢‹¸Y‘B„Ù]M‹ˆƒÑÑÕ`)Ä¥`Å_©•<Šd&®d2Щß^Nãˆâ½­‘dŠ\5U5, `È\¹æ.¡¢Ã—«e“¢‚ƒ{L„E‹¨Ž*êjõ>œRDt³Œ®|LË#ƒKÆ1< IDATMšˆRg¦º•@fs¯.õ‚º—¢î£H.2I*u¹ˆŒS?£™3Q!2«”*©vÄbuQR08æ\Ëo± )Ä”’!ÀßR£Žé£IËÕä8ác§ÁˆkEÀÁœÐí¶0@„Ìf.jèHC:†RÌÕôV#ÄÌÌ]Üê§µ²2˜¢™Ç¦9Y­ö»Í°ßššeÇðö7¾ù¯ÿ‡ÿ16qØ_̓6©óZÀqì{XÆßùàë'óÅ?þí?ܬ·m×9A€ZìðŠgâW@W•zR!C<̘štùòòéWîÞðäË/¨åp²:ÿ཯ïŸ~õ'ÿâOîݹ«6My×ïw§çËÕj9 y²¶Mb<ŠØˆCl;2ê3†^|T@÷HCê 96픇aìk_/ÄÀ9kм˜Í÷bÛë›çOžºLìÃÕ‹í0îßyóÝ/>þ¸h^]œÜ9+Ež?}Jã‹—ë§_~öüɧXz¶:LöãÍÃÃÕËçããÅéùé;/ŸmŸ<úÌÁcŒÒv·ÿòÑçeؽxò8̺®m[‡¾iš#1/– UßlvõmÏç3Óî&•Îîn2Ž® !FtˆD‹º¤Ù¬iLjž×/¿üì7¿*Óæá½ó2ŽnR !ƘTô°ß©æy×5)q"М§aH1„‘«îwÛ)ëåzbƒ(¦º#dŽV´?ô››+æàel"žœäœ™‘ U5uÍÙÅùl¹¸¸{w¶˜§¶su0G 8ÖÂ}‘BD³ÅòdµjSªÐ˜Õj•Únš2mwôòÊ̦iØínJÉ〗/ž]]>e2&@W·¾L7W/òv»†mˆ¡›5!áîp}µ~Z¦m„¢ûõ'?û[G9lɈ@ÑÜk/|6k™YJÉS–29(UwZÉDŠ  3sÍáµnà<öÇÿ/„„nîÂÀè@5f \¯Û¥€ ÝŒ½€¸M‡àÊÕT4sJ x154s Q­8JÇX];€î®ÅÀêÃô(À“©×2ŠjŒ€©d…ÛŠ¿9„èfNŒ õ7¬!Vß¡ˆjñ’ ƒ©ˆNp«Sr¹U~7œL@ÍT V£7Â@E¬däÀàÀ ^XZ5?ǽl®š „8£„0_¬fMÛÌLa·Ûæ’ÍEŠ”ìjLC½Õ8šÕz®›•œEU»nfVu„úrÎæ¦êµjdª¹L¢Cðiˆ€˜s@‡Àl„H¼ZL0nÚ–BHÛí0e@ˆ1p)‚AuxÑÚjÍ%Óm¾ö·ZÇã;ã•ýBÑ!¥ˆõÇ 阸¥œÝŒù8§r3@¬Ø±£» ÑÃ8r:ŽIywÂZcÖSÛͦ)ë œ]MÜ"ó½×ßøîü±ÆÔ6i ó9i ðr}ýù§û)#Érøýññ¯>þäÓß‘ùr‘UÚù_Í0«Ò7—\7ÇP}%Î!8Å&6 ~ñôÑ“ÏÏÖO¿º^op̾ÿmšDúþÙãGo¾v*2šÉÕúr±œ¡c“Ú/¾øÅƒûÚ¶cærìá³7!ÅÔÅ&ûX4+ˆÅ¦Í+Dª¿ƒ$0×øXÛ¶¹L®:‹)Søû¿ýÑåã¯òá*’˜õý?üð¿ÎON!àòô¬˜Îg³ë—WÌa?NWëõõúL;Í{°‘IÑÆÏý‹r)tíÉih[NQJ‘RŠ”ØuÅì«ÇdØ–Ã!8€©U+`m<ª"·m»Zán¿/¥„»®3¦zØÜìöÎÅ€jã¡·[uQ•y†Õê¬Iiö×/?wÝÎÚ„Hk÷U:᱉$BHmŠAE%ç Ýf)&ì÷[â¸:¹x÷½¯Ý\okÇA‰x½]?{ü¼¦À'Ljfm2Dˆ)Rˆ†ÔOåµ7ßzôù#q*RÔBjOO/€CÌèÃ0ˆ…¦ÅX[,ÉCÑ2¦2L‘x8ìË4ˆp—" 2kšj©“iØLU}ÊÙÜŒÒæú¥È8 ýØï´Œ ê¹w@ËBšÝÑ•í–{ 5S Sn."RŠ–BÐ+ÌE%R¨uy8Ƹ3ºbàpÛBDDÂ[W™»b Sóû€È1¦Sl##x:9!€~¦œ)%3³[D»ƒÄÃmh¡æýé6™QáïTïÈLŒ€ªÔcŒàX¬R¼ü•Ø^Íê;ÿHú# €úæ°Zu4a0+ž3¸ƒ[ 8²UÊá°#¤:Žº½÷¿ ô1„€EŠ›º;#08X½÷Iî-ãÍK±iŸRS§Ïzr¶ºs¹Z…‰P¤”’»¶1Õ)ç:†ª®§JÝaÆtü€ReçÅØTšX‰8˜ZÅÔÛ3Äbds??;cÏŸ= 1uM“B´¢&…Í «D™S“Ú¶éæÝïÿÁüø~òégŸ™Z²#@uÄWüë¾ÏñïVÓ,ªj*\Á\µ0¡Ú¶35)bÕ{rÌÌhÙÔÌ1$S”õ|'V3«xg"Àúµ;nVk¸ED™NÏ’YqP"(¹¨ãâüì{ø‡o¼õö”‡‡wOg'6»íöW?ûèéÓK áµ7î´]óôù³_üò—/ÖW§wÎ!GF„À©ÎµÐü?8ºuÝ¼Ò \ÀUÉ=™ãaýìW?™¦1?ûòó¼Ûì^>ôÅ£§ý$ç«Øu ²ï7{x/_¬Õàf³ûõÆÅdš*q¥äRT 5)šcZœ®Vb„Ü1U3³"Ã8Ž]ÛˆW6ªy‰ —¼Y_¥Øµä7WOxÚ²ó󳱿úì£ÔÍ8…©ˆ#4m«¹ ИKÑ ¬Ž.#XaÕ27_}þËØ,ãÕb*YÜD@ÆRÄÑÁUòÁKë«Ë2åcL©†¾ïÍcZÌW)5³ Ãtèûº©ZŠQ¤T·E)¢R ‰ÇCïmÛqLY¤5O1 úÐ÷*ºX,Rà¦i›e,\‡ÄãŠ0ç݈𘙓bî²,èЄL1ò|1?;¿'bŒ¼\.wÛ›Ô6_<:\ínÆIK1CöÐób1ðÀÀ„*þâźñõ7Þlf-8Ý9{ïìÝ›ÝN_\*:ºeîØ¤Ð$º¦ ‡ÃË—WÃá°¹Z‡-—œ³H©„ã«-–kÉR޳³2”ƒ”a»)Dà¦2唸mKÜ5 )6ælª`Šher•BÈDôðÁÝ‹ó³Èa¿Û_­×—WkD0Ð"x¤ÈL¦jPg¦È„‰‘*Ö‘üx?ƒZ4­è3C´8Å®fçQDlBŒgm˜^\­EêȞݎ‚ícÖ¾Î>‰È ^‰t –TýxŠ3qŒau²Ì¹ ã8&G' ŽPG¼ã8J)Žu.XSöLÈT厱î DÍÜè8¾«ÚWB5w"6G3ªñ#s+ îª˜Q H,ꉑÉ\­¸”^nÆ=80‡âÐoúqo`Ýü„c“m¹\Ö!u ±aЏ†ÙÁ™B­ùv³ÙjµdŽ•ûVDˆ™ˆU츮¨µ`p¦mÛ5MÛ¦†î“¹E‰c™røí·ß†/?Þí¶žšcBòÕbqØmóØ/æ‹z¦Õ_ªÊu³ú‹Y¡ø^o¹GB‹ÀBŠAM­‚ðUi>›A?LY´Z©+‘±wÔÔ´zÕãB@ðˆXïy•îÇÏ?9wZCø™†¾sïÍ·ÞºsÿÞý×6m{2ïfm¯¯o}úÉÕ³çeœšYóÞ;¯?|øðó/ýø'?Ù÷ÇDÕ~z«1§ÀäÎH€H·²_÷jè½Ýþ›£æœ7—O®ž?ÎE®7×7ë§_}ùK-b"Oë_üÇ¿èæÍÉùª”éóÏ]¯¯Ÿ<{Þ¤fóðµýæ:ÆPÌr‘õÍNœÄ½¦ý!ŸžÞ?¿ûÃ, ŒÓ䈎4ŒãØf³¶kG__o‹yŠÑž=Ùp³__¶$“슗ÝM‘’EÝ[ +Ä»¯Ä½ ¯RB 7pQ-R»:šöÙö˜c*r-”Õ3ºˆ«„š~e¦”Rú›ë͘s×-ðnhÚÏæóýNn¶Ûck†¡k› _ÇqÇ:·ñpP‘ÐÌ9}ñârš†ýv㨋YÃÌLHµÏB16)…¦iÀRëŽÓæúz¹X¬–ó¦kÛ6µ]7[.€a³Þt©íšv·ßçqÊE¦QúqŠm ŽèµëèˆÖu±M±á¸»Þ ÛƒŒ¹™u÷ܹsïþOÞ¾ÞcÖ±)-›@AU37µ<åÝM¿Ým‡~œúáf·ò”ë^SŒ•¿fîê.¯BÄ Çç¡™ù8ö¯$¥•%ŒK‘SÛtdGµ afDZmÛù|nnÃ0öÛíöf·ÇÑÌRJ„À÷ŠÇ£0sžˆªOˆü¨Õ7<öƪ¡ œëxFD,g(Á›t{fA%€V=Û«Fî+Ãê1,QRH@·YG‡ù|~z¶Z,fê>æ2õ“Š„G¨ë8™Æ±ÎNêÛZyÅ!¸yˆ\EÊà ÇâL­‰ ³JÐd®ûÃ@ ·¹#3)bu"øh,·ã ‚FuÊì0Ý:mÆé°1¸¸ûs3›5…m1Ÿq ˜RJLÕ c¬©,‚Hjvèð±þ+ªs`®Œ„,’sN15ÃØ¤hf5,Ž®FH¯Ýp÷ák±M¿þô³«›­¹u]ËLã8|õôé?þü£Rc8 ¹ì(™vS÷"å\TŒÅôUç¨k›Ælúœ'0U3ßîö` îE €‘R.‰¤”+>Hýlpäà>šC}¯W¸7Ua&dÄ@H‹“³‡¯¿ÕO0,OæwNÏÏVw/NSËEJ dœD¦ÝþñŸ|ñD²´1¾ùúý;§«›«ë¿û›ÿäÇ¿¸|¹K©3w©ª ¨ Ÿ@\U×·®‚:÷V333âÐ Æq*¥Õúc–ݘ‰À/Ÿ÷ÛËgjbhÛæÓ_ÿzš¦Ýn8~õé'ˆÖÍš¢.àâ$†Ã˜E!gIͬéNBšpᑈKÉÈÝRŒæ¾\.·7[Ç Õ$ï5ÀÔ]†ÃÎÜÑuR3@ 0ww«YbC1ÆÛ®mÚfws³Ûn¸40ÌÝô謯-â^ ps õ™Ö÷Ã~·+SVÕ"¨m7›náMÛžžÌæÝõõõîæfM !p ÒÛ£šQà˜ŠHliyršBÜm‡~kR8T0"•2å‘»UÜeÑ"Vµ‰õ§‡\ìf»Ÿ_o–ËyL™ÆQ´Ü»OrùìóOr.ÛÝþË'/¦¢)¥Ô¶€\ÔCŠŸ,gç§«¶ fr~zïÁÉÉúŸý%~ý­7è|ëùu›õdÖ]ìý:Û0ö›i3 ¥GÏ…C37ä¹{]2Ó8†èZ¿Å³.Õa jŽGÀ ª"™Y`4õÆqÈ¢¹TJ˜¸9"Ö"Å )†éª<¿¼¬ç“±:s'ðœ'sWWU­—²£MØP¬2äÕ-›SL5ù஦µ~„Ħ£8J%O"ŒÓÈü0@`äz²¿êÊÖP•R^Q-qý#„+ƒˆ”RJÎfMl’ºöš«fk·ßºÕóÛéLU­¯£F®Æ®ÑÑ©ˆ˜™š£uužŒ fb†DªbyÀÇJ»q„£Ž¹VmK™nÅ5•Lì^ÃF ˆP&fîªÃnûòI$:¹¸Çhõ[°ÝlË8Ê4å[¹ÒbBDu'W˜|˜F« %­Qb÷ì€Ä„LÄœR{½Ùú§’}gDøJ0íæ„Ôãõ~ïäÏž>8ô1NÓTŠff&æ©~)¬P@SC¤ÂîfDGGæ­A7—Éw£¨¨(3%ȸ©ª2!€5mT…€!c.èj¬ªG`#c ŽC©íRÓµœ’qˆmÓͺå¬[®NïÌ'ÅW'Ë6Ñj-2hÓF"8l‡ÍåzýüÙÕËK‘R¦é½wßûÝ÷¿y¶˜ÿü¿ü»üøò鮆žw/Ejž‡ŽOe Ìxëm@¿…¨:¡˜…¦›™˜f9*K\ Ù½R‹9âHyŒ×W—Õ“Çá&DŠ)Ì—«Ôͤ”íö0NæÎH˜eÌã)!@ sà —LD5’{XB2).þöo‹¥/ï>Ÿ€ÌÜ]µŽ˜Ì]Šš2W­I±q¹œ/W+-S¢½ÂFVHCeßÚŽ>h‘Á’<‰S!"‚äÒö6[\,—Ën¾¸žÍ®^>?ì¶›ÍMCtG@P@bn»åÙŽù|¾½ÙôýnšF)Ð%'K-“h6h€H‹nwû)O@,EKÉ5‰™S÷ýpuµ‰)MYNN!Ä›”ÚÔ´'‹ùæå‹—ÏŸ?»ÜìÆŒDͼ99]!ÇR”-V‹®mÏÏOóN‹‡ç——³>øÖ?ífTˆÝâþ6_h|pïáLÀ&ÃæÅ¯?þGó~ÖÀé邻ݣ¯>[_¿¼¹|ñ„?¾^?W·iœ²¨9r ¦À€õ(‘êe @T$Wèµ™Q`sWE"œ²!a)6NSƒ¨‚XÅ朑HEÀ!r ëmåˆÛz½²¤ºy½¼”Rê÷ß Ô眉¨I‰ùÈN|E"s÷œs=¬µH)ZêùnæŽÕZê9˜å¿¡8B÷[¦Å8ŽC?qEe©™Š:¶M˜Ï»4›/Ítµ¨›ŽÃ fuãõŽe`&¢ªèÎGI×8s "uôÏLîJñöD¾}X¼EÅÖz¯©—RýÿOÖTrzEg33dªGZ½ªªŒ‡Ý楚ÔKYßïnÖ—eÌŒ°>öÜŠÑŠ å)2"×d$"!òqu(Þ¹swur2¦®kc ý့„Jsss/O/¯D :9 ƒ‰ÇLG¸¹ç"èˆLT·ä £Y §E$—b¨ù™ÐD!…ÀÜ¥‹!†Ó;÷ºùévw¬ëD)FB@´,"¦Bj’xÓÌêT`¶X6ó…S år±\®ÚfÑvs¢à@ã42b"CíÝ”Ù,Æùêñîú*½–ñþý‹o|ã½7_{}»]ÿê翺z~Ù漢HÄXDÐéÈe‰äzë5;¹Ë f¤£´ŒŽˆV¨ „z=S«ÕFRó©ŒRÔÁC ̨ܼ9›,—+ Únw%îPy±¢P‹dw‡#{OH`ˆ…À]¥$lîžœpÓ¼xþt,Šä¡JŠ”Ê“ ¨1W©ŸÜ€\e®Òï¶ZòØ÷õënU=¿ [Ô— ÚqïVûVÆaTÕW/œ8¤8•¢û}ŒÁÌSÛ^ܽCxAtØÝ˜މ†œ•c$"J펇¾þø«/ÖWÏs‰É] L! &àFf6æ¬5°El€EE™QÌlRU¸Þìs‘ëõæþ½aØMe?®NVàp6ëòù©ÌÇÑ»vvqrÊM“Ec¤åjÑvÝl6# ®zØllgmºsç0p7›T@\[lT5`€išÌ¬išÙlÞ4ÉD«A¬2RŒ9sÔ˜9RÝUº™@G¬=VšI=îKQ3kç3A¤ŒÃ~yz NÛíõõú B‰c&ÑÍÕÚ4#žprĺp¬ÐGBä TgÄýn˜y–æ.ГÇ”ŽŸ(@ˆÀÅ<¤±A·¦i§<ÕvTu8¹7!¤bu­SÛµˆ†”Æ©P3k´hSJÁͪ$Ø]ë.´íZBRõcÝi´)álÖ6³E7;³Ê™éš†‰Æ¡?;=a„¶‹±IÀ|²êšÄÞÍgÌ5?BLÆ,æÌè€Rp¿÷õº\_ݘ¨i6Ÿ"Š˜«›õóëë—7×›ÍÕµNšÜ½wöÆÎÎgÛÝæ‡?üÑø£—WWE q¬¼FÆPYe^‘–Èìõ#O„`tÛ d8Þfòx0óâÓttTªR…€KMÈ ÆÜÝ¡i1›¥ÆaÜî÷×Û­¹8±0³C}Rê¬Mf *Gíñ¡È*jn·4fÍj?ÿÅß:Ñ4Ü À%«š¸˜€Þ½sqvzúåW_å2ŸÇyKžTÔÝ*·Ú?ŠÇÍòñd¯…êÉ©·'$ˆ#‡€”RCÌÐMu¿Û¡#€Ç¦m»Ùb¹2+Ó4: Æb;_^??[,æê0ëâk¯ÝUéH)bœÏÀD‹ù‘ÉÈ\ `Øíóþ›äZNÏÏ–'§í2¶!ž…ë§û8¤”òË¿ÿA™öMÃE¦õõ•©m®o®^®MJuÞÍONN¹þ9r‘ÜÔÇœ¡!3swU%ÄÅ|^gÕàˆ31B S“ê½»²}sΈØ4 3à4M9w¯#ßPƒšÛ8ÊW«Së?ŸÏëݼRSŒuN2I§iÆR„˜–³E“šn>o[+¥L%B۶ݬ#¤¦mêõôô4†8cÎsÎ&z –’ã‘[ 8ëæ)$$ 1Nã8匈Õ0 ã~˜rSBFdJ1˜YΙ™B uTCŸ•mrlpªê4M®ÊcJ•sÐumµÄ‰él6 !¤”æóyÛv‡ÃaØÜ=¥”š&0眷»!ߺÂCö®ëà(íSÃZ!p 1¥.4ËÙòÔ™ÕÛ…k›×ÀÛy{rzJ!…¶ ¯–óÅ|!"¸\ÎÍMÕ(ÄaѼumSÆÁ´ …cRµ¢%‹æì@„H]—\ ¹ÅÈàÒµ­åRsΓ©žß9§ÐâlÞHÑ®‹¸›µÈLÁ¡|uùÿqõf½–dYšÐšö6³s'wˆÌˆÈ¡è.Ô-^J*!ƒÔÐâ Ä_ã ‰ÐjD£‚¦º@]•¢ÝÙ••c îáw<ÇÌö^kÛ¡T>d¸§¿Çlïµ¾q]÷þå—·µ\¶Ø»Ý]Ieìè¡®k¿yܺﻯ»öfm·õÒÀÀš·U£==¯/O¾þïÛþòüøxy~*Œïnnü£Oîî®.ëË/þâÿùŸÿé?ûúýÇt4fyˆƒæÏ ó²üá?Yç2š=2d>ýÅÂĈB`ê1¼û‘ŸŒâHc `¦ZËÝÝÍõéîïaBФÖÌ<ðöÝmÛûÓósV°eƳ«{"éánú|Ùh„ÉF:ŽO0Ú=nonÞ½{÷þýûÞ›{zv¨»y„Ñм¾î$?lø €÷æx ëQw õQ„‚¦ÝÕ<üê–yyûö­jï]uPm8Õ:-§7Ÿ}ñîÇ_nÛå·÷ËóË}©‚áĬª< S,S™¦„èfÛ¶ ±¼¸€Ä뺉Hî›jNŽçóVé¹¶óãËÃ7ww·\¦à¹ÌWBÅy)\k©–Jdª¡ a-´…¹öÆóãããýË'_è'Qx¡¾~úxA„ËY¿úåßÆÓ‡Ï¿¼µöüñÃʄۺßxèëåéãwûÓC[_ ӻǶo//uÌ%Ñxº\¤Ö2õp[·m׎­µî6M Wâå´Ü\Ÿj©q5‡#‘•RK­Ë²ˆˆ¹Yxa±°ý»®æ{ïîþ|¹¹½-RšYeN„'Óf–:J)ÞM¡L•¦Z1 ›†²ØÄš1Ñ<Ïé§ë½åž^kM’hÝ·mߡ֚ÁIß.ËRkÍ”’}Û;Ø4‹y)¥H=-WoËtC\ËéZæ“ÕæçÓÛ¹Þ쌤­b„ȧRnnß”²Ü¾ùäöÍgñ|~V߸ÈÖ\¤Î…™¥Öùf9Ý\®ï®ÊÕL,¦þ§?ÿ·ˆ ÍÔ´³H™–9/3™dš*³0QªÓÔZ ìªUJ¸-³÷Så«Ó­1³i*)¬”Z³#k–qr žwÄó¹/OW¼í¦\Ù=˜@-¶¶Û®zÑÞ ^‚Ø~¦mÛ¶§ç3x\MÎ]÷î¦n­õí¼Ÿ×Þ; ãÌ‹”Í›µKß^¢¯/ï?Þ¿ïýâ}õô IDATºê¶›Óòã½{÷æíËËå/ÿÏ¿þù/¿úêÃÖ}×1ŒÔl€q@û¡tˆ!É<²å3¬{è&Á#{Ñó_…i?òŽ1¹Ø”Šáh¬‡*üöîöÍÝ"¾œÏjF€)pHðƒ ÝàåéÞ=Â: ¸YÊîn®~öóŸÿõ/~aÖ%;iÐ9•¼Yz`žœO¢{ +žÙ·¾=_Îy…tBLÊJ_C#0{[¿oœý¾Ê €ˆÄ=²X.Â=|ïÖ-˜K­çû`¶¯[ª»ßÜz`–i>íÛyo­H™j™¦åtuE®_ÿá÷ß|ý;B(RÕ,TÍ:MÕT·í¼^ˆ0(L‹:ó«0#A¨±)›ž‡ðˆ‡óª¦w×óÍRêeD-ÎÛÓcP¡i©WwS9Uf¦PÛ6ÝZß;ºE˜YwWßçåþãã¾½¿ùÝ·—¦¿ù÷wo¤——§‡óã7_ŸÄ/Ï—ßÿÝCöïh÷¶éúôôrÿÝËãÇçó{í!æAj3‡û2M™È‘ó<á5›úÑ0àG[iòœæ^˜[ÛÎÏpÙ·ç——moY>ƒˆæºí[ŽÞ(LUä³wo<î~û»ß­ëº®kžMGG0sè?øýrY]]݇ïiØž «~¸ÿx¾œbÛ›y‚0›Ö¹°ðÕÕiÛš°¸û2ϽëÃÇû¤%{ëÏOÏ[oµ”Ó²xøããc‚$øüòÜ{€¬p:ô÷Ñ,PíÒ¾«"°í;#Q¾1f:DÖÙ*¾í­™¦©÷ÙL;zh‹lû¾LÓÖöpg–ì]¶³ Òy½œ×•¯¯N ÄäÞ¼·vTS¥Þ™³òôæÍ>ùä‹ÓÕ;ž¯—·oÖp¬§ )¬w KÅu_Ï¥D;- LËõõÛOn?ù|ººš*|òöTky>¯oo绫©Ö*R@ÝR ˆy.ËT‰¨u-…‘ HiÝLmž ”Â¡ã  8_ú2°´ð¨]/ÂÀáÑÍÕÓ‚@pyÞ?Þ?!EΛÉÈ!@×8ŸWí}ßwFb¡mwUÍ®T5m»®—¶oMÓ7`II R| S㼓­«;@†C(‚33ïD—ËY0óBµuÝ_u¿`˜ëzyþ¸ï+3û?Ïà ~Èÿ¡Å|\lßÌ-uš›À,¸­fúùO¿ˆðßüæ·)N_-fj+††›13ªYº¬ë岆;#2AþÕbäC¤á#™¥üÑx CóÜÏ… 0km³–!mH¡”²,3A@o{Âû¶3žAøêúf^æ‡û—ó‹jïê3@ß.Ïm{~ü"¦yA$ wpß¶‹™˜í¦Sš×ˆð肟/KXhT*çÖnš…ë¶?=?>^¦½¹­\Ow'l½õ ‹L;E·ýY¬÷§ûû¶m£?¢·dB‘ÀŸpºáSÿêÛûï¯nïât½«^îï/Ïïµ âäˆmß´iÛûùé©]ž÷õ¹éE„™˜fš¯O'÷è–„ØúžÉŽ”û]d˜Yíà1ŒBîÆDŒ´¯Û~Ùº+¸Wá,PÍÌ&˜J¹¹:Mµ0ÑRK˜‘ZËÕõi×¾¹Mµà¡Á!²qBT³}ß Á’[b@¨Z8ì]=bo;D aav3€–fvYwx~º|àÓ\3æáüò²­½0" YÑíÍÍßÔ·wpUµÛue¡ ¸(|8·åT9«’i#f$Ĭ¾B"õ\ábÝÚº7ô px~¹P€šuu7hm/U1…êmo»ZF »êóÓº BWS÷ðÀˆÃÜ„ªÜÃò•§|\‘²׬{8DxÇÐ<@ÜÁ<¡ C„Þ5²M7£~<œ !˜[Ï!ofíülm­ÂÞW³}"(ÈUêétusºÙ·þ¯¿ý7ÿú—¿úÛ_ý×µ­m³üósVÍ’?t÷ü6™Yͼ÷ˆ©0мGþÐdÍÙ(KÀÖõååå³w?þéÏþèëo¾~xzWw©ñ‹Ô7··ïÞÞÕRïîî_Ö­”Z¥˜ùa­K#f!3Bb&12qÀñüü|¹œáöæzÛÖ*d­wH !œ {uZ^žžÃƒˆ{k®kM1XÖÚŒs¤Ú:"øqÞçœÕˆ$¥HÎýÈDPpªÓ4•L"Bž'·ÀZK™Ô|7-ý³Ÿÿì³ÏÞÝÜ^Ñ4¸#¹`hßzï•Ú'wóôÙ'Ëõ)‹k[_wÂÊ¥Ò¶7¸9ÕóãåýûǽéÝ͉™?<]ž.mž§€@„0këFˆLÔÝÌ݃—iÚ[몑·¾)˜r*û="ÐTcTþZ¸»‡9¤zÆÃµw7ÍnBÊ`$ w &È„‘”ÿ#eÑʘ«Ô{ïfÝuWípä$¨Y.CÛ¶…;x`¦Hé±pÕnÁDÛvaŒðÞ, Æé´H%af Øüýó‡ß~õÛ¯¿ýêÃýã¾[2üÙQžÃë(¿6¹@Œþáf2sÓÑÅÊHáGqT€‡{ûDŸêôÿGÿÙçŸÿÑ?ÿ‹?|þ+dÓÖ^^^¦ZP­¥X·§õéþᱫŠ+ ¼>¡C”*×ãlü8š!F Àøýo~=ÏS%¼XDó0Ô@zí¤ˆ|ƒNóB—ËÅÍÁƒÃ9†·$~И6Ò„"²ÿà¤0¿öð„¿aŒD™ÄÈLaª=Ü-˜x^fd4‡åúÍ?ÿ‡?þôÇÿêoþ Í™¹õ¦ªÏOO©l[¦9g4ˆ+ A\*__M絸›f „ S™gÜ·-׎Ûëë7oÞ"¼ww?_¶ß~ýíËóeY®–ÓõU¹‹ëéTÍíéùãýw÷ë¶"ÎóòæîݧŸþè»{³i9©÷m¿l­ÕZjA]]µÍlx’@‡ØrHdŒIP–2É‚WU˜<íÚ”Ãi"81º©Y>L¨=Ãy1,cÊñ‡ÿ°g6!ˆH†A!%ÑŠû¾ï{KÈ u½¹¹¹9]½úzÜàæêF#–:|_ËgG˜Ž[Ý-•1£Ndè(K©Dt}ºQUuµ‚0Â:Ü3*$DdâÂæ¶­›½ TS,R"‡Aˆ© ‘œ–)©ªÖÛ¶íRd™gaÄ1›¾- áyåU%—S-¨ ¸ïëõÕiªÓºwcFê=U:™ "Ö#€X$ʉoÉÂI„ç"}{yhZëVê//ëe¾½)ÎSOËTkÁ©€ OL§ ätSë•lûK?¿ôï>ìÆèaû¾G#¬OÛemˆ¼Ô 4Ð2¸&Óä•z,w –®`©" pY€™bX¥ñ}j×Ì…L°žÂÝ,Ý{sSÌÐHm-ÂÐÌ<ñÁƒÌ€0báuÝ#€˜#!<‘¨ë{ÛWÓž². /’=ïÉõa¸››uwÓÞ1‚‰¸„’a‘"ËÄBàÚКn¾™mkÿöýýw/ç—çó 'TWD€F>Ú87ሶˆHË®jÏDœÓrRÕ}Ý’T%öQ:–Ž ÷<÷}/¥¨ú§Ÿ}V§úñþ»ÇLJ*L8Po=OêÔ ¼X÷í²n™{˜ÜkavL–ç´Öò¦‰ƒN³'ÂÕM7vU ËP~w„´^3©ªåôÑ»¶VJ©Eš5wwE¤¼ÀÒi˜D#LÐ=‚K&9ýˆ™åý!ÌájÖZÏåN„à #2‘—i ÷Þvw–ºœn?»zûžxÜŸ?^^÷Ö´u‡¨¥ÔZRt¦ª„îNÎèZ ^ªš›{ïÍÊó««+„èmOeŠˆ”R\•ˆTÛåraæO>ùtÛ7ÝÛ×?Äõ›Ûw“Ȫݑ!âããùá¼"ceÉ¢`õ8_.ë¶íÝYÔØ‘`Ó½#ZÁ×À¥ü{„4€‚LT §éŠ‘Ìl”NÓ‘´4ÞÊ|Ì4Õ\ÞºÚ1{#»;†—Ròá‹p®u.%™f5›ç…Ó…hªŒôöî­goÓÀCô-#ÞD|8Y‘,g€t‹jWDr¨•™^5HL„™’à>~?7í½,[Ø´3s©…UÕT™0¹Êßö}k½«âõé´Ôi*-<µC"LLMõ›o>¬{[N‹°¨j),<ìr½73MÉŒÊà,=¼Æ>L® Ë<á\VÍ))%þzTu‘ˆê€ªˆÄ,êÖzGâyZæiZêU-‹ƒ·öÝ7~¾§Rh^xYÚTH©”&àø‚Ž^öæ=dÛqWË÷ÂA733d¸´0T&AqP” AïJ"­+’ lªÖ;D }îæîÂRj1Ë!*Rdw{«íòr~æ¬7gawß·µ†ðÖv3C&€Ö;D2!¨½{ûŽŸŸ^ö}Ï(Ytmì184óJ‹¢ºo ™#ȲRµ«ƒ;dIÎižÓüæÞÍm_×v9·¾ž×—óåòò²Þ¿\¶ÖУ«2CðQå͇¡!%#ƒQëDèæ„c9!BBêÚ]è8ß! uW Ä>uÄ1–1 4^ošS³´5MæQ=Pæn™)‘Döï °ˆC¸{&Bd rO‹2"t˜š’Y¤ ¹a@ú‰Ì²c6Ké| èî)fBfrÂÔÕE€i­­um]X¶sdžYV›ïzÁÑÚ‘Qþí›_?µm×Þ0U ™HCÔÜF˜&Dæ»GÛÍÐ9ÌTõr¹PoçZçl¼ë½cGxwPËsBJc™ª»{xR°µÝ<ºªöž!g±®gUD5s³ÞD0 p)bÇ5u®™X‡ˆ1’›Ž÷u©3sˆ~ÃZ·œÂY°ÖîY;œk´ˆ´ÖKQ)E´G2–ìWâŒÏ5©µžE2ibŠp˾{¤‘€À9øRW"÷È™.k‡‚FðÑ÷æê8ò<˜yäüÐÁ F@á,…@a:Â}€& 9È'*%*ˆhÖ  "SR4"%oÇ`ƒÈyj¬íÖ×Ëã·/OßΧóóý¶=cꯗÓU^®D½onÆ2ô[žfî$ä 2Û ÷ÌÓ(­ik j­¶µ©îûà§ÓÂÖ¬µáÌ$HÓD¸÷Þº=>ÝïÚžŸˆ@Ϋ»wAè¯è öm/܆ÄÀÌǸÁœq.ÖÚf¥H4FFœŸ–éöæÚ¾úæÃyS™+3@8åꇗÈÜó¦¡!§ÅÜÊX˜…8õH"üêT"D–©VaÑ®S„9ó+ÒÓÁ"Ó4—ÂÓ{sSjýæ›oZÛsÔEL°…‘€à "")Ù®¡‘Oäµ”÷Mï 劙óÑcDÄ!wÉÒ$òêïF6µiBV½…¨VÈ4ÝRf7CDæ‘7€?(Ž bLÊ‘€ddI‚gþ¤iåU@8::|ì"d˜ðZ*1ÏþIAäŒ,,éhŸJ™P]»š ‡‘ÂD»¸¡Ð(Ü f ɱÜÛ"Ždìî”Ú¡<‘dL0^(@A‚üšF¼'bpI¿X8ÂÎ3 kF*;q”±€Ð*…7Ý7ïMóHåw„a¨nàã,"„¨ƒ÷æëëë}[_.ç­÷Ëee‘d s×LysÎ M·óöân™`…œÁ÷`»in ÝŒˆ@ÍÚÞ²Þ+±ï1b¸e!7˜E€”ìƒ×ü 35S D³ôà33:þ€i=XV€ÌÚö¦ff"²œNû¶©í|ÐTˆx>Ÿ÷}O"Ö:Qk­í;ºf zo¯QîÁ¥0—}ÕÔšµÌ¿;‚fç–$¹“êÌp`t©¨Ãy‘‡#«)fæQÊÃM‹ÊÕÕ•›÷–—œÕiÌ^‘µ§!€Ü^»Ã@Ý)s¨)CÔL’bgJAÅUEdQ¯¡%aûöUÕ?öáÝ•Àˆ)@¸D8ff„)¢·5߬޽w÷@³hMµy`W4fÉÖG–L(yYמ2•C®áˆDãú&LwÌÓËóÞF&Ééa#6b#ÔÌÌ© cñhª;Ê937#eÀÚ Ë³8çZkëšÈ}ïæµ”¬Ó$B·°±AEª<„³® ‡«:ÂÌ#€…áw$C3¹÷®×Wó<×µ¤?Q˜™ét:½¹¾ùÉ_>½<ÿê×›•DîQk!¢¬e)œ±GP¤ 1sšEò»â#yn‡G´ #"S©H˲ ‘C¸Y®!~Iråå^¹¯lØøðGŒmò„„„`šMs.¥pÖÙí5ÚÛ¥µ½¶9G¦Ò4qObÎ_öº‰§xNDb”y>X™Zkª "D8ÀÑ|„Ž„,ÅÌó3gs4Ákv q7K¿X8Z„”)ï3?×!(ADþþ“Ãx•=Ñ€?~WO&o4—Æø`žBO€€h½'‘ž™ÃKíª¡– T€„yX˜çOÏm´beŠfwØ»wïZÛžž×¦f†GéuÆádPD©ΗUÕeÓ(øHÇ1õ\+"Í:ˆh‰¡çF{$Â@#C¤;M7žHùë“CD’!HDDÀ@L,‚l©Â„#›2T¦å¦ÙÖ‹¹³š=>=e_`~"ß{éÌ¥”«ÓõÞö|vkù…XDï%ÒOLR‰KÈPèD`À¡XA$& xóæ=Ÿ_Fü_ø0î!刚À„bÔ:‡g?„™%Fî àû~bLl=“í‰(Ì3ÉÈÕÌœ> #PF¡Ô( cnCS­ªÙ&­«HÁQ°k /¼ÊròËÈV9–šù«Ó´2!ycøÑz³¾cìïîæ/>»™giÝMaÛmÛ5 ªúÚØçñ}´Ç²Ì -%bl­  ûxusé ð€nzYW¬µ¶]uÏ; œ™ÔŽÔjD!¡áó” ±`F¨CŒ…ƒG¢Zïqw{ýé'Ÿt³¯¾yÿ²š¶fhqÜLÃ/÷ºfa-¥µÝÝr^IÞa@x­Ð7/8ñèIØ{ƒˆZ8ÇOf$$7gáe™+Ëõrz>??=½Ô¹xþ+ 0LÝ}N©ÉOZ8@”ZADªáB’We3\)ù_™_1MS-5ƒ×ó$å!Fþob.8à¢|“‘) r­ÀL6hD&u ¨‘Ò|t`2¹]4³ðH¤¨ˆð¬2U<"@µáxiepRX$ ,"w¬Ô ‚i7wÌjwD 3ËŽ•qc^™fÞÄÄžÖpó ÑÍÃ<†Ê> ›!d"Jk3ç‘ÅL#ÚA~pBd`zÞ„a®™ :ô0#apšg xzŒÈòÂÄpÁ<@5/¾,0Q¤â‘M¨,C ˜÷«…"÷€æ19Ôãœ=ÄÀT rå6“>§Ô(cF†ïâ0²¸# s!ˆ°k>íãø0S³{ 3;’¨;½¾vm.E2èôæö¶ˆ<>>4퉆#@¶àª*" ³šZÊŠòÄgF„©N±m—ÌONt;©Y°Ó=¼Ôrü¾¼á˜(×D`.^EþìÏþ‹_þ›¿ýÅÿý‹Öw"œ¦Ér~¤\D Ì!ôñˆ2ꡱˆÞ•¹$ë–ÏtƒdŠxW §ŒvÍÞ×ü<"J½¢»'C-ˆ™,Dküð Àëµà¦@ an!BÂdîÀ \¹–’Á=~¤Ä¥Ìß<Ô<Ýù"ŒHŠ`¦©2u7@è=˜QH»Õ"€Ä‚ˆEÆ“¦&¥3!g7$ð‡ç.µºÛf  ÌïpHw>Å÷«Ékæ°ÊD)‡¿„ðà’'­cD„ZuWeCSæ@ä}>šaˆˆñ Ú9§lÍçs?G₈s™|8*‡ä ¦(0Ì=Å|ް·m’:• Ô\Ïç •Z™(£÷è¦?hÍs*ãz²¦:•3ÈÂ6(Ÿ— ¼'ptŽ9(‡j€‰iŠ: fÎ-²Rñ˜®\ó aðqÿ¥bÍTÃrúJÌ4µ™Ž¡½+£sv!iÊ}³/8añqÇ 9ˆY‚ –¹[îÃî<œÇ#`ÃÝ[ïI¾cä‚Yíé×8 `r ÇÊY2¬2Xfíbd²ObĽ©bÁ4‰›oûþ¼žÐ<ÑÿÈš$w8ªœÀ=0LÒðHDL<ô7`¶‚ wf˜’€™l™¦JˆúÀ@H‘-º«HE€ÂS="ô ³RÌSkIªG A@Û-ŽB„\àF0º÷Æ®-…Ñ´!b8€)äúnr‚›ÛÛ»ëëû. æƒ3ž„DÞ(Û¢} o™o¢`„”$0wËOÈŒEäpàg £“gýôH d‚> ‘ìùÖÇp í†ó2­ëÆ€H îá‚Ê”ÄU008æg9ÂfC†FÉaŽ ÃÙC™¥aDb™Í‡,2h¹ pþËd›@ÛH^B¢0 ÷¼“ KŒ Ëô4Pª#w €c¸»z„T©Q3+,L ”-š¶ í„ÁáÆ¥‚Ck]˜—RLw0@í7ótµ”ß}—X愈"%14½„©2—R+ @Ú/P"u m{zº÷ó_|ñã·óûö¾Žäþwèî>MK„wÝ­÷:Í"X°»¹”î%2µˆ¶ðÝÑh\ê¦-ˆn§Ù#Àƒ8˜Èö®]no+b‰pÕŽÙusAðÓ†/,R/[»º¹úÇÿõóøøð¿ýùÿz~¹˜Ù¿ÿþÿý÷ß><ÿüÏÿÉÿô?þÚ^ÂTd™—kÕnÖÛ~.¥Lлº[‘›Sx¨ZïJä,²,§t·öֈȖ#rkÓÀÌ)ó`¯K9]ŸÍãëhz ÛéÚKú7͘E¤äДsbÔ ÖZäþã‡Ìý‘™­›°°È–‰hÌDìn€þ}Àˆ¶¡D L5C0]¢¯EîÆ„6Uº¾šÜö‰Ì)´+qFy`LäAŠC˜#'jÄÂ1®KQ„pIÛT‚¶ä¬{oûFPèæêJ¶¶aT ÖÖ¶u‘!@ÀBL`šBDZDÄ‹MÕÜ‘ ‰`Yj-tÞ¶Çûç¦Q' äÒ¬udÌ>b4ŠøPöÊu)DBá}ÈõÜɃ©2¡›m³`B BT âìJfÓÆDPȈÄÌÛ®(…¤”)ˆ°m­Bbw'‚Rfv3@b$Ríªn~}sb‚Þµmp‡£pIɉp©¥dûkm”¬Šã1èn…èÍÍÝ?øã¿?—ò›ßã»G¸\Š™1“©f:’™9`fÊ[RåÌl¦ˆ ,£>t„ör‘yª1H…ÈzbªEÌÆ–1 µLyq˜)½›Y’8™ Y˜¯®Nï¿ûNÍÜÍB‡";©Bûˆ“Q Bd©¡‰ˆe¤®2s¿Hèó¼d|`î‹90«ÙðF¦„‰1Ѫ”¾ÔZ™ÅÝ£×Z1Ü÷¶§º4&‘ˆópøÌ ˆ0 Zï­í0ÏS©õ5<í}p¦­+em€ª•RK‘ÆÂt{s{ssóí‡o7Ýu‰³DŠy”×4¤LL\JñèëúŒÓTÛÞšúg?úÑŸüÉù»ßþá׿ú÷ˆ>MÕ][k‘T;!³”D Ká$«lxsáúúzYwßÚÞZC$„ ë›;)"ömÏÙÖÜR؆Ìdf½7b!dVs„ŒÞöZJ²}€ÞöÖÝ?ûü'òïþ½Ëùü“·üÍ×ßnÛþÓŸ½ýå¿üçTèGWû?þÏÿÔû,W?ùé—2}ó‡ß¼<~Óû:ŸNËr ÄÀS÷;bÜàîîv]/© ÍyCMz(jÌ55Ðî>OWoÞ¾»¾¾¹|l­ ´Ë´Í¬™—ÅTïîçZ—yöCöÞU]H¢µ­éîøï|9…Ç<Ï󼼓8©'Ÿ&ÂæÞ3fÐÝ )1úäT;ñAÚáÐßBM3;vôvª°ˆ…ömSíÁŒÈÈäîÁÂ,åyíO—Q .K©µdÕŽ™©ZÄÁç[àI´ÖÌŒÉç"7Ë‚]{rA@˽‰p&ßmlàé!=Dªä.Í<õXÌR¸Öi)ÓtYwƒ¨U˜¢Š¤‚‚ˆ™9S2âÇ* fÊDEJ†E ¥Þ«ÈhY1'ÊÁùõ eº0ò¾6fžç‰‰5“=j­,¬:ôÎR ¨F˜©»ÕZÜMDã- ÈFâ´5i.fáaÌÄ$8,ižãd2Ú«Ë," ¤ˆ»ímwb)R«”ÓàYïǃµ£äâx•&CB@ȹÌEJšñˆ8Â2#!/$À${(åxp‘Ç&™(¦|ÓÜ=³Z®N'7‹°t?©Ù‘s£â$'·WF:žÛä¨) —R³‘EºöÜ"‡½‡Rð<æîƒbMÝÈ‘Hž©È˜Ýk¥I¶ÏåŒéYK;2‰GJªÈ³¬3f`)ü½ÀÙæ/õ´¹y"³k à<Ï"¥éž†©ªö"\ë4Ð<ó8¼Çwë½ÊP·nFåê§ô?ÿñ—õýÖï— [oa‰ºa)EDΗKŠ4À™È#zï„\J™—9E2{où¢°H¹½{W¤ôÞŸŸžMÕµ·Þ#,Þz‹p–ÑAx¯Þµgìy¸§‘Ð<¨ÔŸ|ù³"åÃû÷?~ì½GPÛ°žJ)êÞZ.7úïý'ïïÿñ¿øßÿÙ¯þÕ_ïû#ÞÜÜNó1xµœ6ò[KŒWU{o#Åõ@'54ßHf.’û´å¢|}s}™f"VÕDØsY¯µ2ÑÈ"ÜV&ÌXÖRJ™¦ØÖµµ=[Q#\þ«ô§"\K-R érYÓvÅDLnˆ8ÕÉÜp>-¥uUíµ– ÌœyÉÍlPvÌ„hº…m¨/~ù0Ã:—BLE˜ejM×KÓî"y„?œû‡û‹¦«ëyYjADOÿœÅ0p „Ä Bw›‹Ÿ¦™å`fÝ#…Í@¥±ÃGê^zŠËÐËÝ õÚÜ©ÌËÂu^ºž¦¹^]Ÿ <[Ã,,çÈù]ó;fÂp×ÞB¤¸ƒ‡%ÜÄ2üŸfA„ù*ƒA‚fit¼HâL€%ºöZ§ìSE„š{2ƘàF »E pºó)ÖÁÂ¥”p×ÞÕÔ—,áRËf×ÁRòˆý# kF¾ÀænŸÜJ8b)»öÌ+MÌ0ýÒ@¯ÙÔƒÈ&Lâê‡&!?ü0Ï&ˆf–#ôpƒ$@H$,î‘“8€ªÞdæ|ͦb§š’äygSMÓ,\ºvÙ[ÎćÚrë"bžjÉ XU[ç¢y 0D‡$å˜ÛLdKd åkàYd– @ôÿqõ¦O¶eç™×;­½Ï99Ü©êÖ K*Ë2-í0A퉀† Íð÷õ7þ"ˆ‚ èãîÆwÛlYRÍ÷Ö2óœ³÷ZïÀ‡w­)들¨º7óœ½×z‡çù=.DjÍÈX zÂQxKz”{öyÚ‡»Û;ï;:‚&Nݸº?¾Ø‡›L……?ÈB—‡ý6ªµÖ¶2!«j._ L‹Ÿ}ùF¹®.ËõõÅÅÅÎÎg¼»³Z•0"HÍ<ðñE|xíA(“L“ ÄÚ¥“AÁ~ŸBåé9:À؉ Ÿzù93 "‹¨ºjM倛 дx‚0‡£˜ã鼬UOkc™çñL0í›ñn.û N¤”9S€ ˆÔË413K¤9ådË<Ì׈ÁàΩA&FðTeuÞg×yçÚ0Ovp¯€Ô™ÓªSÄ!vSÖMd0¨½ª¦êÌyØ#‡1°x„¶Ê»iž‹F“ ˆ° ›£$=o:‹¸ZV7j:0@1òSîaÖM➣äèá€ÅwbLŽbóï²Ë'h@:¤;‡ 0÷Ùåp‡mbS‹ Ì›ÑÔÌ£IA¢þº2Ó|˜:@Ùɺú#™‡Œäœ±ù’0í§=f(‰9 f è;Éüû‘(åq}cŸ¿GxDÈÜLÝQ‡ÐZ%H»PPŠ„$19G[!cÁ`¹ÌŽ#ÓÝÒG–6ªîLp"êz÷nPìŸÞöIŠðräØÌÔTŠdg’¶ÞVÛ¦n%¢¯ݤ’-^_f@¸nQº™,êáL‚è‘–˜’a³–ÒO WM~DÉŒEÒ? ÍÒËP>Þ– £âœgùæ»6|ÔDm]ÚºNÓ¼ßï¿yõGìv»yÚÏóŒÈD¸®­é®D½øí/WXX¤m b ìE=0š¶«Ë'Eä|¾ðÂlf˲ôô¨ðÈîðI«SF`JF'|b¿ß•2µ¦¥H–HH¥ %òÚÜÔ 0É©á!$=_ÐÃ"’²1LÁD PkE soªì=Ü£«´="@9ëq–Ôf²ˆyCÊ—ÉQº/ z Ru².XƒôRl餀!"ØTÍ=¯¢œÑ熿õ­_Y–õíÛ7iüž¦’ë˜!DY×UU"ùÔž·ÙÈù‹ÚSñôx4õ0Õ,8Õ¢Ex8C8³lf™Z׈%«fžk[\O;Ž‹ƒ€[[c­POE@Ø # ¸ «Ešˆ À<ÝÒ@‚¡<·L„SDÆÑšRèdMt’i÷ý÷Ì\F$É=­´oþ™ɆÈÃÇ!Ìd¦©"G"áôŒGUí(hîw!!oNÌ22ÿXè5—Òin¦NªˆHš^wåIÕká +º*EÌ,Ìób–2•ýe<„›*³d3Û)h ìffñ©Ü}Œý}è·ZƦ)ÛšnjOD"â¢Ünÿe{£† ÔˆQ(}áзdÃOK@szb;¯½ŸËÝ–ê–¼9«¥‚{ Oáiú˽Þ½qp/3f1ˆ@ ¥Š9q¡™F—ÄoœÊ,Ej«f®jÛ’"É)¦aÔL/hJTÝçy8å–¤Ñì$ÑLe"ÕB§R Ó Šap7ÓÓñ6Gˆ8M­VÕ– `Ø= Ü4dšæ‰..?úø{‡ýáoúW¦§ÖZb*pøÛDÊnÞG€ª¥©Þz„&™•+Ñ–åÜÃÓ ÊTÔ”H01jDdï?ÿh]×ãím¸!²©¹:!÷ÍCÇÈF§ñ»«ÖDzD–bƒÊÒ­pi*AâÔ7æ·7MÅìyú=·¨ë"F¤ý¥ŽFdMŒ·l]ÏS™§iFB0.~÷f¤Õ‹/Fsàè‘HW[×%¹î‘& ïÈ&/%T7ðDÂóÝ)ÜÔ,Ë ÂðL±hÐW%Þ±ËÙ+#;ƒGDsp)it³"¥L(L{Ä8Þ-ëjÈ®H½ wCft o@}yÖÛ0ÈQ”ÒáLA˜aZÞû#ܰáŽÝÒzb¯ÜDÄM×ê­E³a™Š9©[󪊻‹ë‹‹‹"B桦®‰˜¿òøÚ†Ì®{”,QÕ´VíÝBú¤V²;Uº¥“÷S™Cã8KO–·ºRFb«kkνM—?fOdä Q Æã€žÖUm,‚Ý0Ù‡Ú„äpÜp >^ȵÛ?Ç–@[Í4 ¢¤»1‚çSëžbÿçSo—¾žV‹– ¢A¿äxÄÁo',å  i©1€È¤\1È…÷ÑòdŠ™ú‚5ÔÝ Y€÷„ ›ÔÞ  PVÖÊ,ù{xâAr*h™95.”Dteï5¢Di«õrÕz¹me‚vÎBäg¥È~AĵV д›%†z$ýnÝ}îîh$SQϼG¢"]•ÔZó°Ôn-Ü/uƒðV—;÷"s@˜·pWwbˆÌ²nûT/)zÂHᾞOŽªF„"’N¥  ýî€Hu]Í|ÚM"s@ñ0á)ëß ‡ãè”ëŒ^6` ÿ)ôíï|÷Í›7Ç»»®gËðN§@(gܵ®½a‘"Ørd ›E°/xI ºÁª·Cºs¤PUU1\8{S'&tÑÝonÞ‰”ý~_k]j¢<úí|:2ñÓgÏ?ùÕïùùçïÞ½$‚ZwñH"9Wòþ˜é=‰³#“:º!+Í FÉeæ6xBêB` `jºzŠi–U"ÊD¥p­zw·–Ijó¥š9ºc¦ë¤ú$L ;|†:¼ zˆ`úe\£ãV»8 Ë~ûáž^D´Œ—ïÝz¿9±ÑÈMí|öp BR(‰ÚZUgB&j­ÖZá©t´~@g ,ç IDAT½áÚAéYˆŽ»)…“ ™é=“e”™0~N3oxÏ¿t`!Fkàë²Fx~fp8ëÊ HÑÌZZs Ÿ¥ü<Ïæ…È’zJ¹¹;öêµ+óú±N÷–í{B7`Â4ZCbÊóz]—3¢Tö¾ûô{°I·}u‰¤G¶üy™§eŒï;ÊÌG û t- öyˆox¯{•TÀÃ!mÈÙèÁ­DlfÉãØÅ«%Ñik¯Ø)[é>nêaP?€ÀÌÇ£LežvqOYˆnnŸ[ßÙŒ*ÂäîxŠóÏÊ]})êº¦ŠÆÇJ#‡<[Õ9Œ‘ž·o·F$ ·/#ßqôjù%1”‘Ü|õÖrZU¦ä}ºåì;‚ˆöû}Våyj»{·Ä'×Âôxûútf-[."j­•BÈî Z3˜¬5+ðÁ‡ÄôöÍëu]³›¹ªÃ;ãÁýa?×7K]5äHôÿúËÞRH1ÓΔÈcj¸‚YŠ»/˹÷y,äÞDÄ·ú9q ÔˆñÑ!$k‚y2¤ûê±»¸úãòß|ýå×ÿ÷ÿñ¿C«8"oó±”ô{»[DÓvuýþã'Ï_¾üÂÚ™™ÃÜMa3ÃQ<3]\æy×Z›ç‰™T[)‚X\[k*2=yüìÝ›·ç»w€&c×ûˆQÖæƒë ºa±:Èx;t6lYÎÆë„ä¦ÄXJq×оg¿Ø—«ë«Wß¼=ŸÃŒÎgû曕x-B€Ù„*$xï°/ˆÞbŒe_íãµ|)J™pJûz,O•ÖÂ¥û7™æ©PvkˆûÃ~Y–ó²t_?Óõõuaùð£o}øÁGo^½Hž`¿Q2ô€¤„/ø+ßùä{¿úƒ¿þ›ÃÏþ#k« B)ˆÒ5òÓ„Dù;î÷;(…ËkÚ)c´8Û·›‹å„XÕVc,s’4c3ž5Zæm ~ÇÅäïåq?„íß#õ̉¾î›½]Î9'a–Dâ1SŸee¶LW]l~À önY¼ð¶Á „Äïå=÷fûý~švé¦é£°ñjõêdƒþ!2³”²ñµïåÍc§§@ßõš4ÿ®q|÷/w\¢ÛÄqÜ(@Fù ä°Ûíò{i­™iWa'ŽÄæÞ³w\>uKú]7‹wÇŒ4ä<µ¥ÎmIGÀáØ½b°ŒÇíä¢\-øÈ!Ð6Â>¦x¾?]–¿ì~¿ßïw9SΣv;ÂòÔÈáMeÊ\˜œT¤ƒ¿çd0QxЈ ØB†¹?-±ÕïµÖe9/ËšT–Aø÷îä~B¥*i]—ÖôââJÊÜšfªu~ ,E¤´¦Çó)Û¤þ5ÕZóËù«è )t037c*ãìCsÉöW„~Mešöûý~¿Ÿ§ÙÌü’Ä Æ7hf!,ã}a"â`® !sÐt:][w9:,Çó/þög_}þéùî6Ó4·g/q°éKwäÝÝéx:ÏGS­¦šqù ‹HžïùÅåÌGŠ”"™3Þ±`„ªmYNËùn’üÕAð ÐØ åétDx­#@v¹ö¼·-QdÒ¶F8eÈ#ˆO:!ð餦UoqóÎY€ ™ƒY‰¬jß0b¯$KaSõˆ³PĈíåÍ·¥HÀ¦-Kç~ñ@1Fn(çÝÉjõuÅã­¹ƒD dÌ Q¦4FŒ ¦Ýž¯‘3§ðð2qŠ˜9=cÉ–ó =š±…0x!€à±‹[j­+C“|²ÝNLˆyÔv÷Äö[ˆˆ™f"ù3ŒÀ¶sÈØÅH­n]íýa¾Ý¥YN÷Ò,µ9€ˆh®yÅn%íÀlåÃKýre¦®¸¿¢j8¤»Íb•<´Aú&òy8Qéû"IS˜ç„Ç͵9`¨fÑš)®ŒcŸ±%Ú©ƒg ¸¯ãrRd£Æì¡]É¢ÊW=Ïwm5ï¼!·Ê¤!g–¡m$²pHeF”< Àá°W³,B¶ 1) HÉTêmx$æeÊFŸ‰Fy¢kò¨RÕÖ ™` ŸÊS8¯ÕZWÕ¾YEîCŒ<>.¼@¤û O_Ç<7d•v7Ãý×·­ïnokˈMŠ€Út|n®±›çü²NO »×umuMK`×/Q÷£•Rj][kˆe ‚Ó_ŠD$­JŒ¢¦­µTˆ&ªHú’6«òl¯="㚈Ë\\£©Íó×àæžžÍÑSöÄlwÛ Ž¦p÷øù·æ‹G——WŸýí_¿}ùå®dG¯¶œn´µ§ow%j¸»™Øêé‹Ïúèñ#F,,-ÎçSç9u)…ˆ’s¯¿»sé`Cww3bÊ”$aæ"ØÕ“,Ì‘¶ª.§ãéêTmêý™Å¬öï;—­êI|ÖéÌ™!€­úM­íØÚË¢77@s!&†²#DZ'èÁ@"È"ÄÂ@Œ¡-<‚ Ì"˜QJ†ŸäŒÈÍÚýÆlE««ƒ>\¯!¢E¬+ÜݹVà‚WF4Ê Um <  ûeŠp‡Š;H)ó<÷À€Ëø\öùd­”Z\µ­bÎÜ4ôûŸdld0v=_~È[³1ÏsvúÑÉØ¥c*”õFN~{g€ØT“Þ £vØ ¥ônŒ^¶Ax– 4ô[,ìæƒÐÓ]²v(Ä)ª÷{Epßå5¸{kU[V4ôç÷K…û 82™Rlƒ#¥ÿŽM5ÜÜ :Ñ×Ë4]ìwS™Ô}ë'äӴÛí÷Mk]–{5~ï¢úבñ­UêÿaD0íª—ñ¹1&¹ѳ³‰\ºb^è,âͬŸM'ADgnBþ¼Ú|#j ,ºº6eÚö{”ËÚ>Η.3{à¦+Þæ!"’ÏÉæ*évˆ[é“€MäœÁoC×1ržæŒ òɧ¦9ï›­UÊäy9C×౪׵öºG›0 å•S2cd»MÏçãØå  Gßá5Õy¿+¥œN§Ž )`îÄåâp]ëâ¦Yz«ªªQ¨j«§‰“ÒKxQ.@¬æ¾$¹°HV}ãˆÞõX¤<(È ‡J\_?ýwþÝ?œ¯9Òw>ù{Ÿýâg¯¾þâ嗟ݼýz7a1S)=°j·Ã3TË .§"“¤Ä µº0óaX×…r©‹ÈÌM5{—-Ps]€o—oë$ Hïÿ‘ø¿ÿ+¿ÎÖZžìY¹÷’ÁŠÈˆ›Î 6}O^J k®?\¦0Bw=ÕÓëõø²-~^¡.@óD¥x)8M´Û7X…‘r€8¦~DÄŒIÝ Or0õ*à ÜA›«æ7’A愦Ð*0!1Ô_ö¢»ºÓTî^kÕfiYx¸ã, ½n#>B‘yšîW‰y³u*oº†YîGd¸¡÷­I¾HjÝš.ËêÞ½Ó<#q>Χ.ºv05™ä‰µH)b)Ó4ï²ØÏc}{s™åêòúùû¸Çé|JŠÑZ×u]²âéÙáÛõ™4Žü3ÌayýäɳlkDxžç~­8›ç9“dR1Ÿ}°ªm;Òfj_óæË/¿zñåצñÁG÷{ßûõüàéûÞÜ­oßÞ¾zõJMM­µº®ç€Âjª¦ËzN´†ºDÕ–1OóTõ«Ž+ú¢ê£L0uz¶Ï7¹è½õÿ~òŽH,ÚÄmÈDˆá$ÊuSæ/ÆHïÌ/3Wpžs Ö3Ýõ麴R ³äO1’0ïëî<Àé¸Pvf‚Ç_MDö^Ýüóà_ɧQ„kÓìHÖV-BÕ8ŸM½j÷Ê$y/@$™¡ 3uYj­JÄ®f˜Y"¹ïBn&Ú¬U£‘ŽÜfvŒ}O4 ÷è9Ëf^ˆæ¹äçÜ'­€ª—²åÔÞŒÓ G(1IÌ # ií3ìÑQ=Ð}’¹5Umžüçå¼"ҌŪ"‚µ51^yU2sÕÖóÚˆ<"Ö‡ý\ÊTo—‡K)4"o· ¬±m²—ãþüñZ‡¤0aAb*BîêŽk­LtØÍ£ìÉPÈÅ϶Òë“%5 Äe9..̬”²)Ðó8»¼8<úô[}|¼yîÉàÍÕnÞù=Þ{ÇòomQ¦IU-ð[ßþî‡|øîí›Ï>ý™»vmHDÊØL­L“™%ý4ÿcfMµ5I < ª½zõŠH `ùh>WOž=ýøÃoÿÿè_½xùÿü‹?ùêó/§#BS­õâò"£¥È<Ï̤)…PUm&(‰ŒÏ`ã®mÙpÙev1vH¿}ͽ÷Lº®+3˺´­Zü;§PMk‘r?­Û‚“z°Ví{'„ „¶¦ê îd²#‘%0/<ŽÇUµÇ fãî™Éd€¥`kš+)´›d·›=¼ÕJTl*<"–”Fa8Ô–ìð| ÀÜAˆƒÀa.8ï'$Ì_¶¶Õ£¢’!åûqØ5g ÑÖª'b”ŽTìKž!#a¤Ì¥kÐ9îÔ#=ÇñÇ™cnÖ…héC| ¹ÙjЈ꫻—©§”R˜('ïBà ùyMëˆAîZ…Ì•7<ÓÆyŽ­Ùtg¢‘L;äÒ)(Žð FÉ€@˜(d‚>"ÂY€>&êò•iš²­¾GŒ‘úÍóvžb&JÔÕÖjÕmÇ«MÕ)‘3”›–e5³yš¤çŒ÷v¡6mËùt>뺤 ¦ce ñø g< ‡|‹zD}×ZädÙÜ:‚¥Ï:<jU&@Äiš3m1í €Yºö¤&™¦>®I=¤y˜k¯«Üzu_dU×z'|ȳ_2 Qsˆ0µÊãSx0å‹§>‰D<ŽÓ¤CcŽ›ª2g³éDÅÑ·mûVLô¡V¸”jáâNëÚ׉k­áÆ]E„… ;²"c>óÎ0µ¶¤\hÓ ŠXÞ¼y3Ï»ž æŸUŸ;I&-Žì¦Ùš5W Éd &Ü’ŸøÁ(ù~÷…f:´‰}Œ¢ Epšfñiƙ㱠Æ<åj³ÚL Fò2YËzÌÀ4æÔjÚNy2L‹°˜™Goº"R@ónšT-9­Y8 !'TKMJ—P—‚»ybé8æuUGçGJ‰MRÅœršu]=êCk.b¤|-çKDéb±›SmÖcH­y«fž¯Ì…D0Â[mUº-T3ÆO:@&bY—íóß–.0²L³”ßP¿Öòš ïv{?×»óúò›7µj¾à.ï÷³š®uí8ΪSÆè’Æ…jD>ïö‡Ëi­5…F[¬±Hß§ÿëG?úÑç_|ñöö¹q÷>v쌟Oíõù&Ü…™(ŠP™vîÂÄEн~ùÏG­-ìê-Â’AßÛGﱫ©Û"bFäRF›šAð™cžç ËÝr¼y÷ú›Ÿüø/çéÑ\à˯Úz<ìw—dT­T$c”ˆÄÂ-e)ªš›áLòìñåõå^5¢ªéé”gÓn7›ëfzØÔ ›=£™6½UëD©›ê–=‘’¡ YJ«Z¨»®ËòêÕ›WoÞEPLÂï=¹~tu‚")êãÐpÊ4Ó6¦Ý¦$tß„Q·J¹Ù¦¦ï‹ÊÜõÿÌïîNwÇõå7¯¿úúÕÍÝ÷SyöäêÙþñ~Þ Á.p6êKe÷äG1ï~û·ó¼¶·ïÞÌóôöí;â.úÜž‡<»bU}{sÓ’vß“L‚˜gfmííÛÛ/_¼~÷æÖU¯..w—W»©”L•wS«vZOæ^ëºßíÕ<;C7AfID̶EÕÓ¼_‘)a¥”ˆäW‡µè!9Q±¥µ¶Vûó?ûW¯ßÜ6[ãÑåî×¾ûq¸CôGKµNw¥LØSÀlGºË$Ä’°k ¬­áƒïbšæRf"J]xJò;øOÎ˺ÖvZêr\ãù{Oû¹”}Þ­CJf:½«àùþ§Éé>dfž"P(¢!úÚÀõpY˜™NK½=®jÔ²zpµX•oW`!²§ö‡ymëêÖ9±î}²K„­_j)Éd!ïá¯ܳFQ„Ì‚š€Î3‰@„kÓZcEw€ÖrÝÄîA­ÉqáÕÀ̽x¼“OóžÊ­­È$L'Ò÷Ú=Ya|]}Ï´)Jó¾$bKG"aÎU˜˜¹¨Zy»p{·žnÛº¬aöøÉõº¾‘çÿìå©.HtØÏon—ßøõOÞ¿Ú7k8Me]צ•‰)³/R¶CdÙ¶â)Gí9feæiwèœíîÙ! 0‹fÈœ2vp°¨ÍÎ5>ýâ›ÓÒ˜èòb§fS)‡‹«i’Ü"!GÁÞ\Ñ67Ç\\ge¹ €±`èCö§ÞUY$y°­«¶Mé˯޼»;!Ê^ʺأÇwóNM™‘8ã°iãÿä1ZëúÞûÏ>øà£_|öź®Ñ¹§®jˆ=gÊÆq&RÌuž÷Ýè)µ/L­U™æ—/_óê­0ßÞÞœ¯öß.Ïå铹H†C…§*<‚ebÆä1äSš¬5+#ïÞTä“Oþ­ÇOŸýù_üér¾‹Fʱ`NʱÕÜA#¢”ùG?þÑÏ?ýâ°šß¼»½¾:üÚ÷¿SÛªn þ‚°Œ`Üï/j«Ypô]b–2M­5sUa~àóÈz2•ß1)yœzkMŽçz:/ïnŽß¼xÍËi}ÿ½ÇÏ?xúøúúîtlu '˜Ê&ôûÊ ‘¤ ³$1ÊG QªH ÑÖå||‡ëRÀp5}{󮶨 ØÃÌòë”ÓoŽñòÔ–E"Ûù£gÓÅ„ÑÌc*½€ˆõLfVÊ’† éÀ1Ïe´{«em‹0Î;LàmS3µð@fdÆSÅc+_ßÚ«[ňÇ×öt·«×Ïh¿?hxÓºéÝA¤0s­Ë0ÇÖÉn‹Ía¤… &ˆ äH™4qN^ÔL]SÂÂa¸VýöøÕç_[Ó««Ãr~ÿÛßyÎS®ÝѨ֚cY&ÚÜðà4€¾B÷ÈO0ÆÏæ$ˆ9)6s t¨­!0¢L»ÃjðÍë; Ÿ‹¸3|ðá{‡ùæömJ‰6êCÆ ø92a6µZ×D(¦¬ÖÉJŠ>’K‡ˆ«ç€›[­ K‘2ͤ~ø×Ÿ~óz 2‘¿¹}úè€××u]Ý zb;‚%* …^nÚLÉR"%"饊q»`޶õ@È`ÊܘeY¥ìö ²¿ÚŸÏðúÍÝy5.åtŽÚàý÷n>úà)uº$akͱ‹R†þ8ç§¾i4sêÒZ›¦i7ïº/oc:„«y߸Ä8Œ"ÂwŸþÅß¾ºY΋Äii¦íêpøàÃgD Öò)<¥ÍX‡@s#øßÏñ²bè‰XFS{¸s~P( [žE*<‰È§ŸþõËwŽÀ ¶jØÍ7/^ýú¯}»™Ï§Öª$&¬ãH!ãz ðööæ'?ùÑÍÝݺžk]i8 cl|Ô±/%\תª­j†, ìö—K=~ýõ«ãy-"wVoÞ¾>ìæïýê'»Ýî\WGÉ•³—iGe·ßïooßeVs­=ã0g•ËrΟ´µ8®“€æ$ˆëy¡rš!“©š¹bàÅÅã›ãúÙßœŽuW€àÚ꺾}÷úòêòxõ|ÔºÎÓ. j[jŬ¹‡…W…ÓJ̘ïfzMC0'u]ffçó9û³۰µÚÔLäx²¯_¼}õê&'˜§óI&zúä½»5a¹'ùõÑ&æòVDò2PÄÖŒ™‚x3÷õXï^ãÚfv^ ÂÎ T)¤¦jÄÜŒnûêñ:Ö`.p×ÜmýäÃù0K$8Ÿˆ0ÕÄaªaæÒ áC, ¤-ÜcÞz˜Aaœ‹¨{„ O…=,=±HP g íÛ³öz}{­/Ò IDATs… ¡û¡6¥»ýõ“iškm,#Z¢û‘ÁpðÓkCý–0˜€H­! ô’94û3ˆf–+‘Rd÷‹ÏÿæÍ«#¬ ,//®vßþîG,|{â„»Ä8·ýÀ(¹ Î-¨G¬™¹¹‘ðóýn÷ôýçÿêÏÿôö9a3ŒÓi¹yûv¿ÛkðÝí;B,»ÝU¥;`šf"6õyžží.ÏKu3…Æ,@„6q±y~{{³´&Tb&óÖ*˜Fu»~þýÿðþØpÿó¿þËOôÿE»­u¢ßøÁo}ýõׯ߼LöU–,9¯Û¤Åjîæ€8M“ –_|þÍ7¯n׿L€ìt{~ýæ-â'‡ór"ò6-efàû¾ID:Ò$7ZDYŸš‚!Ä4)ö%Ô2±E -@ ÌúÉhK¥¯Þê—ï 3Õˆ°x<òÄÅŽ˜¼5ÐR†äL; {º1šÆ O…"Ô`F„@AO»áR#ÎE=¢i¸3Ka¦r»ÒÝq=•bÂÅøæì-¢Éé#”i>Ðy @"Éüí,ÊDеpïb8ó›¦~?K÷­£LuF7ÕÚÔ=ñFL,@Äáð“¿ùüå«“Ãlj~vx{|òÍÛ_ùÎGOŸ>[kU3@JÅHŒ|lºìM·s_%dž BN•‹™©µðá¡ÂD1&ø„B8ýô§Ÿ~ùÙ¤™¡V=ŸÖãi™¦siØ2¶4Á¿Yó%•¬º…y*IÌ,t¹êLÕv®‘|Œ¶zÊJn†U5yv<­ëj]~F¬7Çúöö´•ªÚ×…Z[- "šVK­SS-èúѳýþÂ,j«vZŽ—×./."lžæóé´®_‘A‡#¥pÙmÙívWOž=yúøöÕ[%;Ÿ|Yõúño ü%£”2#$”¢;ê™ÀVDÙ?{þñÅõßúðƒÿíöíëo~XkáZë<Ïáh™gB4t·÷š4wTÕIÊÕå¥ÜÞÞ}öÙ-˜™‘yQ/‹žÏ5pEJÓœÞÛâ{$ bêC˜KöFÂC:oSU Ð ò´?\í&_ßÙº˜Áqµh=ëÀ „“­êkƒãª#¨Q”YÌÁ¤°EE ȬƒDÊ%['ò(±Ø¼* –ñ4­7ÅóâÓÓ ¡i!Öû=Sm® …°U3ÂUq©VpC?ŸÏ¥b¬^‰äââò|>«*EW¼ôæzlÆdšº¥X­¥Æ.é |ÏTñ-mMÓå€ /+RD¦»ãòù‹W§,àA®.‹–ö»ÝIª3ûú7Á4LÞo·!ïFÉIô–ßEôÌÓA.퉹X8¡€k’R¼ÓZ‡˜îU^¹\íËIS¦°~šG.@fHv^*ô¬ryuEÓ¾š¦ ÔÌCƒ ®­î{Õš1µ¹q'b„ûU!3…€2Òæ[N/"Oן5m§Ó ¤ò’˜Yˆri›JDæyÞ.Þ{úŒ ±x2x›_\^Ïs‘sAä$jPÌDtD.% €–%p)‰öÑÃMÖuíßLCAkàÑ…dˆ0Ms 0ï>ùä;ÿÞ¿ÿoÿ/ÿÓÿ¹ß±™7"xüäñóçï¯Ke.—×¥ä/Bfmð„EÊĄ̈æûÞYªÚ—_~ÙZäÿä?ýÏð_üÉ?;Þ½Þ7˜¦)Ax[ASJˆÝawõèŠdÀª ZEšö3"¶Õ(ut¹÷¾½y§ænÝšP¸¸ºàÃá:Wb;°óù´¬KUãi€ãéÔªvŒ$£°H¡Haï?öÿËüOÿéÿpº9­ºS¹,øýÑÕ£G··ëãGO€d≈Ê$av:Ýz™vìøå/~ü/ÿùÿúÛ¿ó{‡y~ÿÙû·7?K)ÝÏ~öÓ”«ò(»ó,Õ< ã|^èÀ»y/ÄŒÄÐÙØÍ1"j­»yFb³%iÅîIÁ2MDdY!¡ßÇYdf…#‚ˆ02¨p€,ë1Ö•Ã#؃<<;b&pk•ƒ§‚L Pv;ÚïgÀÕ!ÊÄf@ y÷]—02>-Kj¡É^'DÒ­9101;¸¥¡ÀÁˆé°Ÿ‰íöX«ê³H”ÔBÐ ‘Ý‚IÒ\–¡‹i–#â†n",ÓtyqÅÂw··K]°«?#Ë¥NˆpôîV÷ /He‚;²ÌS)êpZÛ@!ÈÉ\è™>ÛHÔ7,ê½¶¿1g¬£¨j¤<)r‹»y¯Z³·è’g sÁi.Røƒ÷¯æ)](Iœ†¼¼¸Ìÿ±¡h{ÝuÑQ Ì¿‚ÑãSÐ@’)QѵçÞí>FHcs™ùûßÿøùÇÏþ³¥šÎÂôøÑ£ÖÔÌPÝr^Ô™<ƒ÷]f @—Ii˜!@y oS¦cæ –2Akgµºßï——ßûð÷þð÷ÿçÿñŸYÁB9ð‡Gï=ôìñ×_½0G‡@ìÀw‡ `DàdÚ”iÎ m-'§É´¹~ôø£å‹/¾ ÆáÎÊîŸdfd.‡ýáwÿþþÍïþôÿý‹ŸœZ%4>üæïü !Ò<íÔŒÇlÇa*%"¦©´ÖZk°ßíò+–Ô¸œŽGsËìrïˆ~çN—ïrt"vÀ4U üïþÛÿì|ZÿäŸÿY(!Îô{ô{Ï?|þêÍm3™dsiY‚ÎišæîÊC´ÐL ¸¾¾zïÙ_|ñ¥ùÝ»7o./®D¦ü6U•…0(Pƒ 7ïnŠ}›~ÿ~ÿOÿå_¼zùz*l-jó_ùäãßþß2w5ÿä“ï½|ùõ²®^ks5£a+é¡1"ónǼ?®ßݼ;žî>øàƒe9 ‘¬keæÛÛ£µšO®)ˆ¹SˆnïnonÞý“ÿú¿øþßÿÝ?û×?~ùõ ‰øíßüÿ»z“fËÎëLo5_³÷9ç6ÙI$ˆŽ-‰"%ª¨’T¥()JQUjàÔÄá¡Ã?É?ÃGx`—–ËeÚ’%J¤HŠ2‘ímN·÷׬µtÜ s0×ÛßµÑ ìd; !æ(ÀÜËö[©½•.Ê]ÍãUW1D†Ç£ìÐ Ø1n¸?zÈïÞTª¨Ò2†>¹’ˆ(p¸«Ã«Bï>"„ÓÁÍlµ ÃȰ‹¨,&fŒ‘bdQëâ/@Až+LU‰1gWy5®bL÷>üì³O™xØÃ²Š[r èàõú€tʢ؂ðKîÉK‚"KÈ?aΦ¿SvqJ™9~õõëg϶̄ Ì’Þ¿ÌŸ}úBÚîvn’\ò¹ 2jÙ¾¼§y›º³Ðƒ—cýÔ#äeCWk'g1‡sL9çÈOž<9ìÄko)Ù·¾õèò#DÞÞ^‹t¾/Ïn»{|Ÿ$ØËCó?ûæ>áçИbKÜÛùóKwô « !pJi¸wyñþãG/_¿¾½ÝÚÃwÎþäŸÿø»ßþpš‡iç‘âé”÷fɼpCIì³?kw‘…àƒ¼lóDk-¥O\p*J! ØW«Ëwßýþg߃/¯¯¤ÃíGÿäÿÕ¿û/ôæúZNúïØÔGè€n0Ây.]„N»p'IäaefÛÝM­EL‰ÙÅ¥ž”ð‰1/…¥”ßyç|þévžöe“o|ãÝùçòÇø{‡ý~{»35&&GÝyôK5¥äëQQFRQëŸ"P[m­:\¤·ÞcJÓ¼+Ür¤jH–rþþgŸþ‹?ûçŸ|ïÃïÿö·>þÎ7ÿí¿ýóßýᧇÃq·ÝIïDœÓà«KOd¤4¤œ™£Ú’äRSCòêìì¼Ì“J¿¹¾º¹¹nµ Z¡ÖŠ'rõÒs¦“XŒ)æáwÿ>ýáï÷°¦|ñÎ{÷ÇŸÿ7ÿõ¿ûæã÷¾þêé<—þð‡jv{³uÊ£ª‘ß!BX¯WD4M“š‰ZÊãÅååî°‘œ‡ã4«ñòâÂñ¨"­ÎÅ÷ö¸(¨‚œâÑ­W3¸8¿÷ýÏ~ëOþì_þéŸýéO~ü»ßx÷ÞëÏ^¾x~<”<ŒëÕúæú¦–ê@Mé1ÅÈÌ„XçÃóçOn·¯L;˜¥˜BzÇt8±öÄ_Û§`´”RºÈáxÀ¿ùÿû›ãü?ÿ¯ñÿ÷ÿãéÓ/.7é[ïÝÿ£úûï½÷ÎÍÍÍþ°Õ³ÿqƸTƒîÄ~ìÒ¼ôUë$"„Gæ!Ç¤í¾œ®~Nò:ˆx jµÖÝÆ©–’Aù˯ëþeyz…dÄ,÷Ïà·>Ä~þ`ˆõ°ßyóåôEEß²!Q(sQdîftGq2ƒVÑÌÎ.0& âV¥Ve†LCNÜTkÕ® ŠÄ„<Üà—OŽ»£!Ѱޤ}ú Œ>úð½Ë‹|œ7·×ÓtôUŒén´}Ê],À,U5u›OôŽâR”•ND>K¹ûþ8Â'¶Òˆrãz=^Ü{ÿão_PL¯¿~±¿~¹Y§›ë×O¾üu› ªåäˆÖå[· 0qIÊáÍÍ­™nÆŸÚ]‰¸¶b @HÈNݸM›GƒbTSUX¯×î?xçáÃó‡÷o羿-ëU69¾xþìÙÓ§‡ÝnÌŸŠB"â ºBë]µ-z34íªµ5fò Z«­3cŽ^½†!¥lê;gw~ñiïÃ8¼ÿÑÇç;ØSŒÏ¿z2íw×/_¼~ýº–î)˜¥×FЖÃotÒÑv·Ïypl’³7SÊß|üþ'}ü·÷ó«›oã3óÕÕæ”ñ”ø:mqÏ/ï}çóß}ï“Ï6÷!dÔÃþõ¯¶¯ž=ýòWÿ·¿0³qµAÄÝn磈ÚêøÎÃo~BŽ»Û×Ï~óêëß´Zæir†á°Û—yròA™è‰tT[#fæT{ss·™^X@ ɰ÷®&oÌdš&?„ëí͇ßýü¿ýÁþÕŸýÉÿýÓÿ«—ÛG—ùüìl{Ø hG·mˆˆ“üDÔ̈) ¸5 ‘b\ ã ‚ŠÐ ·¹·‚¤ (b*`jLÆjUA%4F0"<ÛÐï|vöÙç¿|Ù¯¯æÃáæþ&~ò8­Øíö¾þ2X Úhbˆ@€ªhÌ, ­ €,!p²¼k… 1ˆ@-j@H¦f½›BSCd‘e[—"ÜÛÀûùö 8nÒæüüÑû½÷èÁþ¸¯­°W`—-œ!‡¥µOä¯@§5øpÓ–4ÛÝCvyºEÎØÅ‰>àÂ?ýö¿ÿÎWO¯ö»y½¾ý­÷Þ÷Án·¿ºÙ¶®11œèäÿˆïz¢ ¼mÓ¾Ó--SøS}p¡#,QŸe ï ØZ sO1«ð¸‚o}üÑ'}Ôšô^ÏÏ×cJ¯^¼ž¦ƒ˜ÜQËqqÛáI²ˆd '“1œ&^é¯Å9éáN…AŽáv¶Ü…> EZ)†H­kJöÁ÷À°½z½-uî½§”iá’£ªÞ1Ñ–&΢¹7U½Ä»q{¹PÂÛÌ'ó±ˆ01ø®×@Íê<7ym€ÞÿðâÁ»«Í™”éù‹'W/žM‡b½1Yh­«jÎq!ß¾O)‡øàÁ‘Žf½7%Ffîî#÷ƒ€±¨ƒÈNÄ:U¯eL±Ø»\]]KïBÀy¢Þ¼~±Ûn·7×µ´;(æÛ@Pï0ÇiWD ˆ µy E‚ RŒ ¤kˆÔ¦Ö»ˆï¯Ì¤5f‹1øÿp8~ùë_ï·Û<ŽHÔæi>îë4MÇ Œ˜ã¼ALÙÿ[D´vu‚˜ª†ÀŽZ굂Zo­Ö"ÒÉÔŠWÌTÄ ®`V›k͹µ¶½Ùþâoþz?ËãOæ³ÍeŸ§WO®~ùó¿Ù^=ë"j‚u}Û¢¦Çi2³.Ú¥ûC‘ÍzoõxÜ`×eÕqqyùüùóéx¼¸¸Ün·m.jRÊ æÖ\8ÙKMCŠÒçøÅ_õåÀLh(Ú«H·”s—¾ßï»t FNS)9zàzï]dšæû ÃPkéMº,O’FÕÌ\†e`‹‘¦5UÇñüü|šJxùì7­ÔÕêlšvïÜßôf&óõöºu·ú†~Ÿ Úý=é$q#X$µÕZŠˆpZŸ_<øæão~Wk½zö÷ÛW¿ªÓÑLò¸)µOžú6ë È–v›M²mÎì|Äö£÷Ji¯®¨mÞÞ^ûûƒN’O?ÕÚÌi*jL$ J´ NS ‹ c$BkETÌûVîÿE5솪PŠJ ¢&PœáÅ:)r™†p»»Ë¤bŠj9%ó#Þ\ í©âw¹xUNùZ€JªF>1Ïʼ¶@>o=!Îm&‹÷Î.îv¯ÌSJ!¥xs»½¾¾žËäcœ»îÒòH²ÿ_BÞvù~RÔKªv²“‚§ªïLþ7û]xšJÈ q>ªILÁTz—í¶mE[DˆKïöÖÊÒ‡ïŽùÅEDnokd|–ëݼ;MÑN„w/?ÁÂíöb޵&½ïC)wÝáV[ë•É+¸hæF]Þµ†w”M3j{¦Ø­&Ô=AR™)¨.p7 å"ÜÝd+,Øj½}öõñvkˆjÒËtl¥" ˆY×ÉŠÜ ¢»Mì”BñËÄzµB„ù8Ißéštá@1¦ÚêÂbS"45Ïᘙ¨ù €¦RêU;Ô†LÒEšÖÒ¤÷!¥ÀØj}ÓPf2`t ѽ{bL­uõ}x€œÆq\¥”ü‘Ç!ãÒJv½ #±€0!`ˆ NG í}>_•FLœâ8 ÒHÒÝÉGpÞu¢Úz×®"o•Ìä¬'O¿úúù³R+X)s­-Æ8ÜIÐîJÌA¦~P…«íúùßýlµZØ´¿må†Ñ Q ŒORÌüÉn¿½¾z5¦”" `o] ˜çÍÙ½aÀÞ¥Ö¢**¶Ûn‡ƒ÷oTE¥³ßΈݻx–02™·jjàæcf5­¥ÖZUáÛßýìñûïÿÅÿö¦²_ ¹–ÒZ{ðî£ßúpàßüæ $@£îa—VžÆŒ¾Y[:À*nê]‡<¬†‘‘C=_M_¨Z“Vú‘QNq<¥nîì~DQ_dik¥÷b7ï?þÖ£¿eš/rzùõ/w·7(s"Nù¬o˜-D® {©Å`L”s”.]¤U;´¦Úµ×éxàçý¼ÛÖ“ Â4ÆlÚ™´TC±Ž¾\]Íbo>’8 šhQ[Ð ±uk¢"8O bD°!`#†D8D Ä˜D#tlýŒ]NÁH$µÞ¥¿CŒwâ<©–мã~ZR¸ýL´«! …˜8GxªŠª«·¥›zï9Æ *óÜÇcëMÁ- †@žbr{²ƒÌN¼Xò§LÏ?‰úÛ¼@ueÏ5ãÿ‘Ãë  Ô2•9öÓ47&tü¼`-}]ÇCšgBï¹oÓ.ï„oéÐÀÖ"6ñ§õå]ÀÜZk½s4•7PAé€)‘¹å±;œÇÖnç¾ÃÝøÅñè!DìHUm*fšŸ(—ß¡)0³ «‘Á˜¹µVö7b¶ 1Ãr ¡÷.½û]Æ(0!µRª2Í“Ë]CÎþ "¢<ÔZ‰XLG_ãûLÖG¯KEÔŒÔZ—ãñèÊ& j10STSˆä4Ü4fÐoÎ6!F!.÷‹)ã8æ<Ìó¼ÛmCˆ1$QªOp.}»>z‡^Y~ÝLjTKÓÒ¼CÏH—$‰Û&K-z¢—ˆˆ¨±-™k<í³}ÜÁ'0Ú²Ðã;¹±úVúd1µ\}ðQ"";¾Jd‘T,èƒc-®ÊJ1‘‰. P"Ó÷”ZS¯¢xnèíáû]˜Úw±Ë?ÿÔQôâØÓŸ†ÐD«ˆ˜edPS54”.­wѺzÈýtLDnrB£¢Èb8;é/ü郈4Ž«u½·Þýù» oÉÔ0 åS(¾KãÈ1UÅÞCpÿ¨XÌÈ­ÕJEzëj$Ô€, ãò¥©­:“ÎÇq)&FênN3[^üÞN×Ö™ƒðM–yú FM­«Šái&"`Èb Ó4©ªs±½ºAˆ]¬9‘jÉ‘㡽]«f ¾f&Ž1ðBä^+ùëˆ=:é;?Óe~ëôsî¦3 Zk¸Ð*¢·.ý6à9)f>LÇÚšlb™%æ&C†‘C0ÀÖÛWOžôÖZ0bæ“¡K4P â. ‘1plÖ Ñ3{)åÍú¼·~*Ú‡ÞDM,¥¬ì·„©µ&¦lIU÷ûmà4Œ«Ö ™öR– /-Ò.²åu×_óìF«]DcJ)ZàÏ3ÕÞ»tQ 1™ÑÓ'_ÑÒ5áa}ñÇúç—÷Îÿîï~î4bS£“„Ù‘ôw¹;ܯS{Õ´õÊa€Öe®-¦¼dàý f `&tU³ðñCKÕÅn½7rH1€J¯óñë/~q8n÷ׯ2Yf‰4$Ž0pO1ìzS@¸wBdDSéÚ¥ª1HQÀ„jƈ–3 .3wO7€)„€ªFvëöî¿]×G~^ö  äL>ĬÕj³.RÆa`B%/"…PŠçîP50ãÀ+Š«ƒH×J|æ}"Ÿ8s?QUðS³Ú?Ö àÛKÎ… J¢IDAT²ë'±RŠÜñÚ‚k+–e# ªJ­âÒåÌãÙy1ÕNo¸ we&<ñ àÎjêz7wìÕZì¯ïF9'8Œa HÜz9é'°÷îaíÖÔ-*;ë>^Œ<§g½5‹ ²Åg€ ;áŽïcJf6ϳ<)xòL ÎeînU5©‰zk½5F)…À@(¢¸Ø9ˆ T•€M (ÆL0ÅcôÔŽqš'é½»€ ¥*{‰Áw­ "Ž€ÐZqY§‹4³Ö»r U 0bF¢”ŸÀ¸ḦSXUÝd.Å?½w ìS;À»ŸçÂë÷KÛjµ:ýÜ7HŒqž /i*§‰Ø/<ä¡;ÑÂqñD„ÔUHÙ¥õ%e‹€ŒÄj†Lþñ]²tõ<%3ß9O °–*­ÕÖ Œ”1¸÷ÇCDSšæÒ{3Å”#"ÔÚî=x8¬6Ïž}ÝZg¦Z›ÛlÜèÔ{¿¾¹ÄË{÷.//Ÿ>y²Ûn/ ÆÅTA˜‰0åR¦ãñHK|ˆ©¶f'Jóш ¬LÅ@™9çÑIËenÓTD”9¦!©¸£PZëÌñáÃëqsØïSk­÷†!D"v_¨’Zõ‹¹-‘_ñ5*€™­V›û÷ß¹¹¹yñâö·ðÛ?>ÎÓóç¯_½ºê­¬†q™é…{ownú“âCÔ€¼œrýóÃ̇ã„Ç9ø°%ÄðÚä)ìÏŽãðÆ(š¢ a1…°•Û¿™Ž7&Mˆ)0JÑÙ`[ŽÓtTUxpŸ6«à' ![­Úæàæ "84D:½&…ô1@ÑÖl™¨º¬'%ñÃTèJé&˜"[L˜R齉® ÔÇPd„bqTƒ^…ÐB Æ@A8&1zÆ_T•N¯º»õ)iˆ¤­6ç7ÜÅ®ý%é§B=Mj˜HØIJøfMs— TD´%ôJËÁÙ~;óÖ+½é©žP]è™<ÍL¼Ãb „t‡Sõ’7‚ž\ƒÞ؃ÀÑGD§»›ø¿•Á@MÕ7ÁP3ñ¾ õGáégµX´ˆ]©()ñzµV3"©л„Åf§€Ø[k§yt U·Cx#,Df K±Í€CDRo9Úâ^÷±{ÈŽ…Z­ÒÅÅ=<öìì]\ã(OZ칸à ™I–)gpî•™åÔ–1} \f©â›È1çÌ)p ö|‡`^¼cBßߊáB6U2õ¡ÿpŠÉÌ1DéÚjAâq½žîÔåcŠ™1˜õV‰(Åcdâ˜bí½”Bœ¾Bk Nÿ³æN)ƒHÄãjìM¦iŽrJÄ 8ˆx; OÞGT暈ª1ªh)ETbL‰1ˆBVj¤ªÈ‘Ã0šé8Žî‰$ 1åLÙgªÐZÓ××W¢†ÈÌ3c`F5ad ”ó0®V¥Ôy>šš¦œ˜C ¥Û\*qŒìœjXvß6#!¥>þøë§O ©«‚¨PWˆ‚)=*ZW­Ò5…Á@R,þC65U ™(FˆÑÍ,ÐUÒOy• LºˆÙív ÀüÏþÅ0äß|ñ¤µ²Ý]÷Vù´ 5{ððþ<Íûý81Åœn®¯O¶®uFÄœ“ª1âPk ‡Ãä”g"âÀ1&BéLê¤,Z¢†ùtU×,ªHÁe §åXÏ90ºÍ»6ÕÙ´s€‹1]]Ê™ÀdHj`BuÈËAÀ˜5'ÖÛ 2îïTåìW$EÃa <$æ(‰‚XR‚qµ¹Ÿ0\__ZŒa[ᢠÌb`„`ŒA€ÙQ @dníu$Ëi ª€ÀSjµqˆ!„¸€OV" ¸kœˆãr„ô*ÓÉLoÖ›€œt9N'ú°p™¢›ò(SæHˆ¢ªuÏÒRÀ Jü¥f´€'“—1ø\Üu^¹Pσ)˜…À‰¢Zѹ”‹Gó …ˆ"ö"Úâ“:÷ˆʾ×Õ"„îß¾KÖƒ¨öæõ\o_v&ÊipÅ9ǼŠy¥bG$Ï’Û<÷»Â(‘óROy|"Œq@"wꪺK'­³Ÿ±é¬ÏÄDã0ˆôÚš°š61R ]–ƒb|r çÁ˜Ž Ìn ÀÀCRµR›™ãÚAӢغ2G“ZˆI€ÔüÚD1fSXfÞ1¹÷Ö[÷—ô®hBŒ"¢ Œ1§˜8¤ZŠ_ýÐ×¥—Ö°Y©U«ô”h5Œ¥µ¹4U‡q.ó\´ùm—K%b ôìÅkU­TÅ“ÁÓ<ÏeŽ91…ÞÕcï‡ýá«/ž–"Ç©óãÃû———ç!¥e Â0Ž¢JZo@!VéHciè]>|xïÞå¯ÿáD5ç”ü€Ëzï÷?úö~óå‹!"õR  °”n@ǹM1$¼¾Ýj˜öI1Ž :7™§¶^% 1$v›€³»+)P̪}žÊ×ÏŸ|üÉ·ÿðŸþÑívÏixðîûÿ‹¿UEG\Œ›sé-D`¦Íù½³ÍåË<.‚Õj5Íe·Û…ïÝ¿?ÏÓ<ÏøòÙnÖçá7_…aÌ M‚Äè}d@5¦S÷[‚ÏæM ˜‰CªÖzwo‘Ÿvq ì2;yŸñ„ÕZwºïmžú]ÍÌ€”¨õfó(ú(DUËI@ˆK1x™é¸Ë› ÄØ ØêÜ{Ó£:ˆšeA™èÒ­`¥¹€!ª ÝÌû«}7…Ö X_† ˆD(Ò[m 2dW©šª¸mZå›sJLkÍo'*K³Ô¤(¤œZ«*Ɖ“Š™sÒ<¶ ?ÑGR¢f&Ä„„`æ{<&dptuµã4ùmÝ¿]ÄïgþÚív"9GÅ"Zïµµ6Ï“ˆÅœ‰hV!p«•˜bŒ¾d6µÀ<ŒCë­µžòPk›K#⫃ø´µê51fVme>–Ò §ÒºÇœ"RJ=¿¸¼ ~úäª÷æçÌãlA˜Èë$·0M‡ÝþZT6›³õzc]±õv{»¿ºÚÌ8$¾wÿüáÃ{ççLD½ür»½‰!Ä”™‚ˆNó$ª9 µ¶Z«Œ_=¹ÿàþóçÏzëþç8®V!­ÒÅ{/®öO_ÞädMj-Uú²Ã(µ·n,ª‘,‹iøÎ÷ˆ”þß¿üÏ1à‡>þõÏÿîÑ7}ýêzì½o~s{˜no_qqžçRªïቹ¶z}}SŠüöÿÉ_ýì¿øå¯ÿõ¿ùןýèwŽšÿú¯þŸãa—‹\m§ZZÎùÞåýëí³››×ˆW7wÊÆÖ[kÍnÓñxÜn·ëÕzµ qœBÃׯ¶‘ƒ•ÖšÈý/ï]ÞÞ^«HÎiL)2E¦!E‘q7g›;·si P°ÖVJ™Ë¼ÛÝÖy ˆï<|ÀrÆh5äD÷[M*s×f’kŒ ¼Ú<²N¦ÑèyÈDР#‡€àY:ó7EJDµ+˜ñ. ›ß>€¨×:ÕV»ÄŠ8ŠÒ\SÊÄçe:NµH™UlfŽÃ¸F¦.½ÎÅÏ !$%D˜†’åáìxœŠ /&RP':uYÔÊw3 ÿºžÜ· õgu}Do=3µÖ›‹&<¾²¼;[„#!§RK#j1åÅ3{Jµ‡&€IL§îRíA[0–¡¥7ªuTƒ;+ÞÂkäEö¤"½u¦àWI¡¤”\4èÏeYÞt⸴Å>JoR¢ºð®ø€ïr{ë•x" Î7oµ©Ñ©bŠhþóE÷jv?ÉkC¤Zjom†”³ùïHmÁQª¬nv'EµÚ¥Ô~¶9C„Ãa›Ò’ñ0UEb&Œ€  ˆ&êpöØzk‚!j½¶Rˆ „àÈ;$d0¢À"‡òØËq®b°Z¯r ¦ây0ˆ)ŠúnÌ40cé­Î ®A»'ZÕ`³Ù˜Ân¿7g¥9¬N·\Õ¥ÿ/HþÎS5ïÝù¾™ÒÄqˆ£ß™º˜" ziŽ˜HÔˆÇÐD|ÕÙZ+u–ÖrŠ8Ìa5ŒrºU„ˆnÊ=ÛkäÛívk&1ú| ú²!TD œDTº2ܤÕZÍšH!„Ã| «ÙùBDL8ð޵4ŒÔ^oç}}MÏoˆca:k-`9ø,N º¨C±RNª °ýêù•ž¶Ù€ˆt3®Îogü?úW­Î¥º´»b8N)m.î™òÃóôƒþàõÍáñ'?zïýf;ûèƒ÷¥—¿üÛ/¾÷ùO¾óéõË_~ãƒOþÓOúäëb•>ûΖݥ•ZZ«½÷úWǹow‡Ûéøð[¿0à¯^ì^<{^¦[&t¡òf³‘Þëúr ÁD„#*.ÙëRO9ìŽÍQ Ó)3cNúôTuïêy#_¸ù#CUÑ–6'#2¡[‰(Ä(fª‚@zê^z~¦”zvv†'ð›;º8îQMÅ÷äHàN™,8xDë®DSoù‚ïT 9§$­Jo®ŠF».xœjoªbv̤¿RNHàYxÿ#C1£˜ó¾Q—Þ$íêâl Ä1DÌiàj-½73c@BS¾ó®h¿¹¾"-`Ö]‹ ¨ŽÃkˆ3 †.j!Ȇ½U´¹ôbRFêb¥÷®=æ ‹ "ì ™’Y@ÃÖ»ªO""4 HÆ£;&³©zËcZ]N¥˜Y ó\|8£¢j« !ÕÍj= Ãn{ÄÌÜç™RN]ºõ¶Z­bˆ‡ãT…ªPÆ4äñžt )®WgD¤‡ý.¨eµÚf¿1«‚І˜ a¸ˆÞ•uRr"PíÒ‡a¬s-sé”b‚gí½¬WĘCD´K) y¬¥Í³šr`´ ˜B¨µXZ=4?ܲxón­òF)…ˆDE@0Äcb 9ÃÊb 2O½5çÑ1:kÒ{VcdÊÜ®nW7å7_¼¼>ذ¹,/^¼Ú\»º¾Þí9S­µÑÅå…¡v[•(?|ð^­ÿýŸÿÄ ZNéìü¼öþìÅó®Í·`Fˆ«!'&Ds0‚ªõæ3@DSóN¼Ï%ÉTï¯F’Ã:Ô³8ŸŸYd5)Û›Wótà€!€ªõj¨ ††ª võ²tU¢|¶9ÿ0†~ýú‹é°ï @$c¿º ì·ÐÆ¼ÌæS‚6ƒ0/gÒZ¡+!v5Uˆ .Ρî÷¶ÛG E-0§”TÄTE%ä”BT̶z´ºÿa6Ûýþë—×­vDe&ïI–2;@Ì»uˆˆ)†œ"§£w‹Á,§¤ |ašÃIJøFÛ¦fàl±V1Ÿ,€½urESqv¤§Ë ÕL9‡àñSSqp¬Hï3³T˜Ðµ…¥ÌªæJb3TSbÌžõ7U•N`9%$b¢‚§ø¿½õ?5Að`8iž˜]„ä,Âå!¥Žª€ålî#B&&Q©½"ŘG!ålª]„‘Rˆ­–Zg3q.‹ª†”jk­Éfs&"e.|Ê´¨Ja»µÖÁ +∄RˆÉi·ÄÜ{@º¬ÆÕ4MµVçöÄUõx<¦”ÜP[ÙÝn»ô!g·Ë"PL)¹‹¸-: ë˜"!†®`!p 4ñ g-e·Ý†À)„Þ«Šã¸AEt:îÓ BŒ9¥âz½2³*MEÔãkÒÕúìòâòòò>µRºcëÍÎÏ.Žó|œŽh¿ßÝîn Ô6›õƒ÷o®¯Êá¶µzœ&S‹‘C ¥”aȈðâÙs$ÊØÒè!t'iÃójµZŸ]\†˜ºH-ó¼?Ôij­R ê.KB4ÄÕzƒLµ·RæÖú’P[¯×CÊ··7»íÍnwÃÄãúìÝo<¾zù´!‚åa•óÇ!åaž¦Ãí¾Õê™ 3#' 9ƒˆö‡¼ÎÏ1hËsÚzk*²ÞlV«•ŠçYÁ;3Žq×VJ9ìwÓñØ[÷ïÒÂ7cä2‰…¥ –Ö¾uU §`¶„˜SJ©·ædBB»û>öÞ‡Ã<wqœvž^ɱ“¼|Bô¿æû t¿p— Ç#ŽãÈcŒ›Í§i23•~<îË<©öÚjo †qL½wQÿêr(ãzÍ̵ÌLo©ÐÅtv~É1/…ɘ‡qÍ!"-ÍÊiš Ê,)ÅÅi£dÚ{·i>Üî§bJ1åb"¦Rªv!­ÖR†a\«M™8²‹¢!EF°ã41Ç!Gím6jJ€ˆ[RÌ)õÞvûëts%]ÇÕHÄ1¤q\ –ÉT‰±ÕB¬iXÅ< Ū”(qhŸ­Ç#…¸ZoŽÓT›^P$0c¦y©ÎÌCmµ÷&‚È© ‚òàèŽ9«!¾*G Â(BÓTDÓÐbK€!gß ¸‘Cň8Å,&LœbŽ!:Ò?OâJŒ9 諜‡³óëÍù<—ù¸‹Næ1#”?§3F°Ò0®Õ ‹ b;6eˆ~$q÷€!œj‰¥–éjc²*PˆÇñw=LS«ÝÔ˜0¥ÔkѹÆæV·û½j7€ÞŠJí¥+(€ X¯“XãÞEz0íå¨;@4€£ÝncˆÍZï*ýˆÀ„µN]ä«§Osçv»})S—êPÖ¯§£ˆ¤¼Fš¦YçRE©Ví­•Z»ÜÞᛈƒ7ÚrÎÇã™Ê<—Ò[½ÝnoÌ ¬ï=ò ș"v/Æœ’ªôÞÌ4ƨj!ð8¬‰˜( Ê´Â0ä.ýx<Šª*pH99'­óîêE;^±*’Ô#AËC¢áv.{±Æ)Žãš€jë9pk¥´} <ŽëÕú<®îo6__í·7·ŠšFZEFÀ^Ì´»9>„˜„B/³ ­Wì£cÕ^k E!%Ì™[3[žÁÞä4P8BÌÌL)Eâ€U(¤4X©]rÈ£Õ&Œ˜†õ&¤á 2|aò¢O'¼ ­«ZÌ™9ÄqtˆW"ź¨vŸZ›YˆEjí¥E²ÀóÈ!ÖZK?¦˜sb ‡v¤˜±x‚…EÕ»wGVãf7~l+s40Jq )ˆÚ)6¤Ð›ˆbT 4l< ÙUÔÄT…ÃbV@ ‰QŒKÕÀI}vÜ”ÂB¿£2 †úübÑaÂRʃ3½q†H!`wA"“ 2 º´¤”RJÙívã8z~_¤ué¢Ý«Úê89µ˜rp-(dzó!¥ä +‹x¤ÖÆ‘SJ¬]Í[U5C#Ä@A¥·Z!ƘƒKÝ"¢t5p¤ º»Õ¬‡£ùC)DÀM¥31Sªe&&@Üv‡Xo­4° Æáf³‘Z;&Ìq\•:ùÈuµ:óù@Ÿûí5srŽià0ÄaÃ!M‡ín{#j›õE <€Žˆx‡5.eÞn·„Ùl\æ‘Ò@1w&íiˆ! q‘‚ØbLÝf}c†yXëMkŽsíF6*'/©ôÞûq¢#çÖšš:.|‰úš¶ÖÁ™1,©["FäRí8O`jn…±@¾ a9sâ b"JHBJ ÍP¸yÿk«^jðOï­”90…ÕlšJC$cdß1*¬Ò 5óñ‰¥Ejc”.Hb ZK!&âS÷ÕÙ}‹“Ä>A´ßA¯©÷¾ñ† C«½õæÚ q Cà.7]èúH8øL1„ÇZkëÁ¨•^[eboÙ8ì—h™> ‡…Ÿrö)ÏÊÝ£“s=±¨ I‰ºkêý¯ö*Ú»š©(ˆš´¶ÔhÍyÎ}ÌľŠ)ºÄMÒÕ[]vÚß2ag³e‡ˆ8xV–ÐJ«K&sq¯K«þOi½#ÁÒž504E5é³õÞD¼“âÏ ßSw•Ú{Q5£™a„Å`,`|i†Œž’»µt!öJ³€jBT4†ÆÒš‘  <¦yžve¯¢„Øû\ü$Kk yYÃù]z¯…SΑ0ÄvÛ¡I¯­Ô„ØÐµ‘†(½M1×Å$à¥Ñ·œåºZïݤK|8–bB¤ãq24Ä&ŠŠF™1aÌ`I¥;†b5æ!Ç.ÊÄN0l§o± ¨@N,÷\\Œ€„]<öå$ŸuÓ3 0Å”²÷RZ«Ò— ¾grE)÷ÿ0³¢ ¨ª.-7B0ëL”sì]ZSA°”RNŒFÀþ?`BÒ8XÀ‘IEND®B`‚fwupd-2.0.10/plugins/ch341a/ch341a.quirk000066400000000000000000000000501501337203100174110ustar00rootroot00000000000000[USB\VID_1A86&PID_5512] Plugin = ch341a fwupd-2.0.10/plugins/ch341a/fu-ch341a-cfi-device.c000066400000000000000000000313001501337203100211060ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-ch341a-cfi-device.h" #include "fu-ch341a-device.h" struct _FuCh341aCfiDevice { FuCfiDevice parent_instance; }; G_DEFINE_TYPE(FuCh341aCfiDevice, fu_ch341a_cfi_device, FU_TYPE_CFI_DEVICE) #define CH341A_PAYLOAD_SIZE 0x1A static gboolean fu_ch341a_cfi_device_chip_select(FuCfiDevice *self, gboolean value, GError **error) { FuCh341aDevice *proxy = FU_CH341A_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); return fu_ch341a_device_chip_select(proxy, value, error); } typedef struct { guint8 mask; guint8 value; } FuCh341aCfiDeviceHelper; static gboolean fu_ch341a_cfi_device_wait_for_status_cb(FuDevice *device, gpointer user_data, GError **error) { FuCh341aCfiDeviceHelper *helper = (FuCh341aCfiDeviceHelper *)user_data; FuCh341aCfiDevice *self = FU_CH341A_CFI_DEVICE(device); FuCh341aDevice *proxy = FU_CH341A_DEVICE(fu_device_get_proxy(device)); guint8 buf[2] = {0x0}; g_autoptr(FuDeviceLocker) cslocker = NULL; /* enable chip */ cslocker = fu_cfi_device_chip_select_locker_new(FU_CFI_DEVICE(self), error); if (cslocker == NULL) return FALSE; if (!fu_cfi_device_get_cmd(FU_CFI_DEVICE(self), FU_CFI_DEVICE_CMD_READ_STATUS, &buf[0], error)) return FALSE; if (!fu_ch341a_device_spi_transfer(proxy, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to want to status: "); return FALSE; } if ((buf[0x1] & helper->mask) != helper->value) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "wanted 0x%x, got 0x%x", helper->value, (guint)(buf[0x1] & helper->mask)); return FALSE; } /* success */ return TRUE; } static gboolean fu_ch341a_cfi_device_wait_for_status(FuCh341aCfiDevice *self, guint8 mask, guint8 value, guint count, guint delay, GError **error) { FuCh341aCfiDeviceHelper helper = {.mask = mask, .value = value}; return fu_device_retry_full(FU_DEVICE(self), fu_ch341a_cfi_device_wait_for_status_cb, count, delay, &helper, error); } static gboolean fu_ch341a_cfi_device_read_jedec(FuCfiDevice *self, GError **error) { FuCh341aDevice *proxy = FU_CH341A_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); guint8 buf[CH341A_PAYLOAD_SIZE] = {0x9F}; g_autoptr(FuDeviceLocker) cslocker = NULL; g_autoptr(GString) flash_id = g_string_new(NULL); /* enable chip */ cslocker = fu_cfi_device_chip_select_locker_new(FU_CFI_DEVICE(self), error); if (cslocker == NULL) return FALSE; /* read JEDEC ID */ if (!fu_ch341a_device_spi_transfer(proxy, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to request JEDEC ID: "); return FALSE; } if (buf[1] == 0x0 && buf[2] == 0x0 && buf[3] == 0x0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "flash ID non-valid, got 0x000000"); return FALSE; } if (buf[1] == 0xFF && buf[2] == 0xFF && buf[3] == 0xFF) { fu_cfi_device_set_flash_id(FU_CFI_DEVICE(self), NULL); return TRUE; } g_string_append_printf(flash_id, "%02X", buf[1]); g_string_append_printf(flash_id, "%02X", buf[2]); g_string_append_printf(flash_id, "%02X", buf[3]); fu_cfi_device_set_flash_id(FU_CFI_DEVICE(self), flash_id->str); /* success */ return TRUE; } static gboolean fu_ch341a_cfi_device_write_enable(FuCh341aCfiDevice *self, GError **error) { FuCh341aDevice *proxy = FU_CH341A_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); guint8 buf[1] = {0x0}; g_autoptr(FuDeviceLocker) cslocker = NULL; /* write enable */ if (!fu_cfi_device_get_cmd(FU_CFI_DEVICE(self), FU_CFI_DEVICE_CMD_WRITE_EN, &buf[0], error)) return FALSE; cslocker = fu_cfi_device_chip_select_locker_new(FU_CFI_DEVICE(self), error); if (cslocker == NULL) return FALSE; if (!fu_ch341a_device_spi_transfer(proxy, buf, sizeof(buf), error)) return FALSE; if (!fu_device_locker_close(cslocker, error)) return FALSE; /* check that WEL is now set */ return fu_ch341a_cfi_device_wait_for_status(self, 0b10, 0b10, 10, 5, error); } static gboolean fu_ch341a_cfi_device_chip_erase(FuCh341aCfiDevice *self, GError **error) { FuCh341aDevice *proxy = FU_CH341A_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); guint8 buf[] = {0x0}; g_autoptr(FuDeviceLocker) cslocker = NULL; /* enable chip */ cslocker = fu_cfi_device_chip_select_locker_new(FU_CFI_DEVICE(self), error); if (cslocker == NULL) return FALSE; /* erase */ if (!fu_cfi_device_get_cmd(FU_CFI_DEVICE(self), FU_CFI_DEVICE_CMD_CHIP_ERASE, &buf[0], error)) return FALSE; if (!fu_ch341a_device_spi_transfer(proxy, buf, sizeof(buf), error)) return FALSE; if (!fu_device_locker_close(cslocker, error)) return FALSE; /* poll Read Status register BUSY */ return fu_ch341a_cfi_device_wait_for_status(self, 0b1, 0b0, 100, 500, error); } static gboolean fu_ch341a_cfi_device_write_page(FuCh341aCfiDevice *self, FuChunk *page, GError **error) { FuCh341aDevice *proxy = FU_CH341A_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); guint8 buf[4] = {0x0}; g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(FuDeviceLocker) cslocker = NULL; g_autoptr(GBytes) page_blob = fu_chunk_get_bytes(page); if (!fu_ch341a_cfi_device_write_enable(self, error)) return FALSE; cslocker = fu_cfi_device_chip_select_locker_new(FU_CFI_DEVICE(self), error); if (cslocker == NULL) return FALSE; /* cmd, then 24 bit starting address */ fu_memwrite_uint32(buf, fu_chunk_get_address(page), G_BIG_ENDIAN); if (!fu_cfi_device_get_cmd(FU_CFI_DEVICE(self), FU_CFI_DEVICE_CMD_PAGE_PROG, &buf[0], error)) return FALSE; if (!fu_ch341a_device_spi_transfer(proxy, buf, sizeof(buf), error)) return FALSE; /* send data */ chunks = fu_chunk_array_new_from_bytes(page_blob, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, CH341A_PAYLOAD_SIZE); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; guint8 buf2[CH341A_PAYLOAD_SIZE] = {0x0}; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_memcpy_safe(buf2, sizeof(buf2), 0x0, /* dst */ fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0x0, /* src */ fu_chunk_get_data_sz(chk), error)) return FALSE; if (!fu_ch341a_device_spi_transfer(proxy, buf2, fu_chunk_get_data_sz(chk), error)) return FALSE; } if (!fu_device_locker_close(cslocker, error)) return FALSE; /* poll Read Status register BUSY */ return fu_ch341a_cfi_device_wait_for_status(self, 0b1, 0b0, 100, 50, error); } static gboolean fu_ch341a_cfi_device_write_pages(FuCh341aCfiDevice *self, FuChunkArray *pages, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(pages)); for (guint i = 0; i < fu_chunk_array_length(pages); i++) { g_autoptr(FuChunk) page = fu_chunk_array_index(pages, i, error); if (page == NULL) return FALSE; if (!fu_ch341a_cfi_device_write_page(self, page, error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static GBytes * fu_ch341a_cfi_device_read_firmware(FuCh341aCfiDevice *self, gsize bufsz, FuProgress *progress, GError **error) { FuCh341aDevice *proxy = FU_CH341A_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); guint8 buf[CH341A_PAYLOAD_SIZE] = {0x0}; g_autoptr(FuDeviceLocker) cslocker = NULL; g_autoptr(GByteArray) blob = g_byte_array_new(); g_autoptr(GPtrArray) chunks = NULL; /* enable chip */ cslocker = fu_cfi_device_chip_select_locker_new(FU_CFI_DEVICE(self), error); if (cslocker == NULL) return NULL; /* read each block */ chunks = fu_chunk_array_new(NULL, bufsz + 0x4, 0x0, 0x0, CH341A_PAYLOAD_SIZE); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ); /* cmd, then 24 bit starting address */ fu_memwrite_uint32(buf, 0x0, G_BIG_ENDIAN); if (!fu_cfi_device_get_cmd(FU_CFI_DEVICE(self), FU_CFI_DEVICE_CMD_READ_DATA, &buf[0], error)) return NULL; for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); /* the first package has cmd and address info */ if (!fu_ch341a_device_spi_transfer(proxy, buf, sizeof(buf), error)) return NULL; if (i == 0) { g_byte_array_append(blob, buf + 0x4, fu_chunk_get_data_sz(chk) - 0x4); } else { g_byte_array_append(blob, buf + 0x0, fu_chunk_get_data_sz(chk)); } /* done */ fu_progress_step_done(progress); } /* success */ return g_bytes_new(blob->data, blob->len); } static gboolean fu_ch341a_cfi_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuCh341aCfiDevice *self = FU_CH341A_CFI_DEVICE(device); FuCh341aDevice *proxy = FU_CH341A_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); g_autoptr(GBytes) fw = NULL; g_autoptr(GBytes) fw_verify = NULL; g_autoptr(FuChunkArray) pages = NULL; g_autoptr(FuDeviceLocker) locker = NULL; /* open programmer */ locker = fu_device_locker_new(proxy, error); if (locker == NULL) return FALSE; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 33, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 44, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 35, NULL); /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* erase */ if (!fu_ch341a_cfi_device_write_enable(self, error)) { g_prefix_error(error, "failed to enable writes: "); return FALSE; } if (!fu_ch341a_cfi_device_chip_erase(self, error)) { g_prefix_error(error, "failed to erase: "); return FALSE; } fu_progress_step_done(progress); /* write each block */ pages = fu_chunk_array_new_from_bytes(fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, fu_cfi_device_get_page_size(FU_CFI_DEVICE(self))); if (!fu_ch341a_cfi_device_write_pages(self, pages, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to write pages: "); return FALSE; } fu_progress_step_done(progress); /* verify each block */ fw_verify = fu_ch341a_cfi_device_read_firmware(self, g_bytes_get_size(fw), fu_progress_get_child(progress), error); if (fw_verify == NULL) { g_prefix_error(error, "failed to verify blocks: "); return FALSE; } if (!fu_bytes_compare(fw, fw_verify, error)) return FALSE; fu_progress_step_done(progress); /* success! */ return TRUE; } static GBytes * fu_ch341a_cfi_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuCh341aCfiDevice *self = FU_CH341A_CFI_DEVICE(device); FuCh341aDevice *proxy = FU_CH341A_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); gsize bufsz = fu_device_get_firmware_size_max(device); g_autoptr(FuDeviceLocker) locker = NULL; /* open programmer */ locker = fu_device_locker_new(proxy, error); if (locker == NULL) return NULL; /* sanity check */ if (bufsz == 0x0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_READ, "device firmware size not set"); return NULL; } return fu_ch341a_cfi_device_read_firmware(self, bufsz, progress, error); } static void fu_ch341a_cfi_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_ch341a_cfi_device_init(FuCh341aCfiDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "org.jedec.cfi"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); } static void fu_ch341a_cfi_device_class_init(FuCh341aCfiDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); FuCfiDeviceClass *cfi_class = FU_CFI_DEVICE_CLASS(klass); cfi_class->chip_select = fu_ch341a_cfi_device_chip_select; cfi_class->read_jedec = fu_ch341a_cfi_device_read_jedec; device_class->write_firmware = fu_ch341a_cfi_device_write_firmware; device_class->dump_firmware = fu_ch341a_cfi_device_dump_firmware; device_class->set_progress = fu_ch341a_cfi_device_set_progress; } fwupd-2.0.10/plugins/ch341a/fu-ch341a-cfi-device.h000066400000000000000000000004761501337203100211250ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_CH341A_CFI_DEVICE (fu_ch341a_cfi_device_get_type()) G_DECLARE_FINAL_TYPE(FuCh341aCfiDevice, fu_ch341a_cfi_device, FU, CH341A_CFI_DEVICE, FuCfiDevice) fwupd-2.0.10/plugins/ch341a/fu-ch341a-device.c000066400000000000000000000161521501337203100203570ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-ch341a-cfi-device.h" #include "fu-ch341a-device.h" struct _FuCh341aDevice { FuUsbDevice parent_instance; guint8 speed; }; G_DEFINE_TYPE(FuCh341aDevice, fu_ch341a_device, FU_TYPE_USB_DEVICE) #define CH341A_USB_TIMEOUT 1000 #define CH341A_EP_OUT 0x02 /* host to device (write) */ #define CH341A_EP_IN 0x82 /* device to host (read) */ #define CH341A_EP_SIZE 0x20 #define CH341A_CMD_SET_OUTPUT 0xA1 #define CH341A_CMD_IO_ADDR 0xA2 #define CH341A_CMD_PRINT_OUT 0xA3 #define CH341A_CMD_SPI_STREAM 0xA8 #define CH341A_CMD_SIO_STREAM 0xA9 #define CH341A_CMD_I2C_STREAM 0xAA #define CH341A_CMD_UIO_STREAM 0xAB #define CH341A_CMD_I2C_STM_START 0x74 #define CH341A_CMD_I2C_STM_STOP 0x75 #define CH341A_CMD_I2C_STM_OUT 0x80 #define CH341A_CMD_I2C_STM_IN 0xC0 #define CH341A_CMD_I2C_STM_SET 0x60 #define CH341A_CMD_I2C_STM_US 0x40 #define CH341A_CMD_I2C_STM_MS 0x50 #define CH341A_CMD_I2C_STM_DLY 0x0F #define CH341A_CMD_I2C_STM_END 0x00 #define CH341A_CMD_UIO_STM_IN 0x00 #define CH341A_CMD_UIO_STM_DIR 0x40 #define CH341A_CMD_UIO_STM_OUT 0x80 #define CH341A_CMD_UIO_STM_US 0xC0 #define CH341A_CMD_UIO_STM_END 0x20 #define CH341A_STM_I2C_SPEED_LOW 0x00 #define CH341A_STM_I2C_SPEED_STANDARD 0x01 #define CH341A_STM_I2C_SPEED_FAST 0x02 #define CH341A_STM_I2C_SPEED_HIGH 0x03 #define CH341A_STM_SPI_MODUS_STANDARD 0x00 #define CH341A_STM_SPI_MODUS_DOUBLE 0x04 #define CH341A_STM_SPI_ENDIAN_BIG 0x0 #define CH341A_STM_SPI_ENDIAN_LITTLE 0x80 static const gchar * fu_ch341a_device_speed_to_string(guint8 speed) { if (speed == CH341A_STM_I2C_SPEED_LOW) return "20kHz"; if (speed == CH341A_STM_I2C_SPEED_STANDARD) return "100kHz"; if (speed == CH341A_STM_I2C_SPEED_FAST) return "400kHz"; if (speed == CH341A_STM_I2C_SPEED_HIGH) return "750kHz"; if (speed == (CH341A_STM_I2C_SPEED_LOW | CH341A_STM_SPI_MODUS_DOUBLE)) return "2*20kHz"; if (speed == (CH341A_STM_I2C_SPEED_STANDARD | CH341A_STM_SPI_MODUS_DOUBLE)) return "2*100kHz"; if (speed == (CH341A_STM_I2C_SPEED_FAST | CH341A_STM_SPI_MODUS_DOUBLE)) return "2*400kHz"; if (speed == (CH341A_STM_I2C_SPEED_HIGH | CH341A_STM_SPI_MODUS_DOUBLE)) return "2*750kHz"; return NULL; } static void fu_ch341a_device_to_string(FuDevice *device, guint idt, GString *str) { FuCh341aDevice *self = FU_CH341A_DEVICE(device); fwupd_codec_string_append(str, idt, "Speed", fu_ch341a_device_speed_to_string(self->speed)); } static gboolean fu_ch341a_device_write(FuCh341aDevice *self, guint8 *buf, gsize bufsz, GError **error) { gsize actual_length = 0; /* debug */ fu_dump_raw(G_LOG_DOMAIN, "write", buf, bufsz); if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), CH341A_EP_OUT, buf, bufsz, &actual_length, CH341A_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to write 0x%x bytes: ", (guint)bufsz); return FALSE; } if (bufsz != actual_length) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "only wrote 0x%x of 0x%x", (guint)actual_length, (guint)bufsz); return FALSE; } /* success */ return TRUE; } static gboolean fu_ch341a_device_read(FuCh341aDevice *self, guint8 *buf, gsize bufsz, GError **error) { gsize actual_length = 0; if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), CH341A_EP_IN, buf, bufsz, &actual_length, CH341A_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to read 0x%x bytes: ", (guint)bufsz); return FALSE; } if (bufsz != actual_length) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "only read 0x%x of 0x%x", (guint)actual_length, (guint)bufsz); return FALSE; } /* debug */ fu_dump_raw(G_LOG_DOMAIN, "read", buf, bufsz); /* success */ return TRUE; } /** * fu_ch341a_device_reverse_uint8: * @value: integer * * Calculates the reverse bit order for a single byte. * * Returns: the @value, reversed **/ static guint8 fu_ch341a_device_reverse_uint8(guint8 value) { guint8 tmp = 0; if (value & 0x01) tmp = 0x80; if (value & 0x02) tmp |= 0x40; if (value & 0x04) tmp |= 0x20; if (value & 0x08) tmp |= 0x10; if (value & 0x10) tmp |= 0x08; if (value & 0x20) tmp |= 0x04; if (value & 0x40) tmp |= 0x02; if (value & 0x80) tmp |= 0x01; return tmp; } gboolean fu_ch341a_device_spi_transfer(FuCh341aDevice *self, guint8 *buf, gsize bufsz, GError **error) { gsize buf2sz = bufsz + 1; g_autofree guint8 *buf2 = g_malloc0(buf2sz); /* requires LSB first */ buf2[0] = CH341A_CMD_SPI_STREAM; for (gsize i = 0; i < bufsz; i++) buf2[i + 1] = fu_ch341a_device_reverse_uint8(buf[i]); /* debug */ fu_dump_raw(G_LOG_DOMAIN, "SPIwrite", buf, bufsz); if (!fu_ch341a_device_write(self, buf2, buf2sz, error)) return FALSE; if (!fu_ch341a_device_read(self, buf, bufsz, error)) return FALSE; /* requires LSB first */ for (gsize i = 0; i < bufsz; i++) buf[i] = fu_ch341a_device_reverse_uint8(buf[i]); /* debug */ fu_dump_raw(G_LOG_DOMAIN, "SPIread", buf, bufsz); /* success */ return TRUE; } static gboolean fu_ch341a_device_configure_stream(FuCh341aDevice *self, GError **error) { guint8 buf[] = {CH341A_CMD_I2C_STREAM, CH341A_CMD_I2C_STM_SET | self->speed, CH341A_CMD_I2C_STM_END}; if (!fu_ch341a_device_write(self, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to configure stream: "); return FALSE; } /* success */ return TRUE; } gboolean fu_ch341a_device_chip_select(FuCh341aDevice *self, gboolean val, GError **error) { guint8 buf[] = { CH341A_CMD_UIO_STREAM, CH341A_CMD_UIO_STM_OUT | (val ? 0x36 : 0x37), /* CS* high, SCK=0, DOUBT*=1 */ CH341A_CMD_UIO_STM_DIR | (val ? 0x3F : 0x00), /* pin direction */ CH341A_CMD_UIO_STM_END, }; return fu_ch341a_device_write(self, buf, sizeof(buf), error); } static gboolean fu_ch341a_device_probe(FuDevice *device, GError **error) { g_autoptr(FuCh341aCfiDevice) cfi_device = NULL; cfi_device = g_object_new(FU_TYPE_CH341A_CFI_DEVICE, "context", fu_device_get_context(device), "proxy", device, "logical-id", "SPI", NULL); fu_device_add_child(device, FU_DEVICE(cfi_device)); return TRUE; } static gboolean fu_ch341a_device_setup(FuDevice *device, GError **error) { FuCh341aDevice *self = FU_CH341A_DEVICE(device); /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_ch341a_device_parent_class)->setup(device, error)) return FALSE; /* set speed */ if (!fu_ch341a_device_configure_stream(self, error)) return FALSE; /* success */ return TRUE; } static void fu_ch341a_device_init(FuCh341aDevice *self) { self->speed = CH341A_STM_I2C_SPEED_STANDARD; fu_usb_device_add_interface(FU_USB_DEVICE(self), 0x0); fu_device_set_name(FU_DEVICE(self), "CH341A"); fu_device_set_vendor(FU_DEVICE(self), "WinChipHead"); } static void fu_ch341a_device_class_init(FuCh341aDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_ch341a_device_probe; device_class->setup = fu_ch341a_device_setup; device_class->to_string = fu_ch341a_device_to_string; } fwupd-2.0.10/plugins/ch341a/fu-ch341a-device.h000066400000000000000000000007571501337203100203700ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_CH341A_DEVICE (fu_ch341a_device_get_type()) G_DECLARE_FINAL_TYPE(FuCh341aDevice, fu_ch341a_device, FU, CH341A_DEVICE, FuUsbDevice) gboolean fu_ch341a_device_chip_select(FuCh341aDevice *self, gboolean val, GError **error); gboolean fu_ch341a_device_spi_transfer(FuCh341aDevice *self, guint8 *buf, gsize bufsz, GError **error); fwupd-2.0.10/plugins/ch341a/fu-ch341a-plugin.c000066400000000000000000000017231501337203100204140ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-ch341a-device.h" #include "fu-ch341a-plugin.h" struct _FuCh341APlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuCh341APlugin, fu_ch341a_plugin, FU_TYPE_PLUGIN) static void fu_ch341a_plugin_init(FuCh341APlugin *self) { } static void fu_ch341a_plugin_object_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_set_name(plugin, "ch341a"); } static void fu_ch341a_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_CH341A_DEVICE); } static void fu_ch341a_plugin_class_init(FuCh341APluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->constructed = fu_ch341a_plugin_object_constructed; plugin_class->constructed = fu_ch341a_plugin_constructed; } fwupd-2.0.10/plugins/ch341a/fu-ch341a-plugin.h000066400000000000000000000003541501337203100204200ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuCh341APlugin, fu_ch341a_plugin, FU, CH341A_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/ch341a/lsusb.txt000066400000000000000000000045311501337203100172520ustar00rootroot00000000000000Bus 001 Device 124: ID 1a86:5512 QinHeng Electronics CH341 in EPP/MEM/I2C mode, EPP/I2C adapter Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 1.10 bDeviceClass 255 Vendor Specific Class bDeviceSubClass 0 bDeviceProtocol 2 bMaxPacketSize0 8 idVendor 0x1a86 QinHeng Electronics idProduct 0x5512 CH341 in EPP/MEM/I2C mode, EPP/I2C adapter bcdDevice 3.04 iManufacturer 0 iProduct 0 iSerial 0 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 0x0027 bNumInterfaces 1 bConfigurationValue 1 iConfiguration 0 bmAttributes 0x80 (Bus Powered) MaxPower 96mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 3 bInterfaceClass 255 Vendor Specific Class bInterfaceSubClass 1 bInterfaceProtocol 2 iInterface 0 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x82 EP 2 IN bmAttributes 2 Transfer Type Bulk Synch Type None Usage Type Data wMaxPacketSize 0x0020 1x 32 bytes bInterval 0 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x02 EP 2 OUT bmAttributes 2 Transfer Type Bulk Synch Type None Usage Type Data wMaxPacketSize 0x0020 1x 32 bytes bInterval 0 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0008 1x 8 bytes bInterval 1 Device Status: 0x0000 (Bus Powered) fwupd-2.0.10/plugins/ch341a/meson.build000066400000000000000000000007751501337203100175310ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginCh341a"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('ch341a.quirk') plugin_builtins += static_library('fu_plugin_ch341a', sources: [ 'fu-ch341a-cfi-device.c', 'fu-ch341a-device.c', 'fu-ch341a-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) enumeration_data += files('tests/ch341a-setup.json') device_tests += files('tests/ch341a.json') fwupd-2.0.10/plugins/ch341a/tests/000077500000000000000000000000001501337203100165205ustar00rootroot00000000000000fwupd-2.0.10/plugins/ch341a/tests/ch341a-setup.json000066400000000000000000000035451501337203100215430ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUsbDevice", "PlatformId": "1-6", "Created": "2024-10-24T17:46:38.660081Z", "IdVendor": 6790, "IdProduct": 21778, "Device": 772, "USB": 272, "DeviceClass": 255, "DeviceProtocol": 2, "UsbConfigDescriptors": [ { "ConfigurationValue": 1 } ], "UsbInterfaces": [ { "Length": 9, "DescriptorType": 4, "InterfaceClass": 255, "InterfaceSubClass": 1, "InterfaceProtocol": 2, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 130, "MaxPacketSize": 32 }, { "DescriptorType": 5, "EndpointAddress": 2, "MaxPacketSize": 32 }, { "DescriptorType": 5, "EndpointAddress": 129, "Interval": 1, "MaxPacketSize": 8 } ] } ], "UsbEvents": [ { "Id": "#4693935e", "Data": "001" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=76\nDEVNAME=bus/usb/001/077\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=1a86/5512/304\nTYPE=255/0/2\nBUSNUM=001\nDEVNUM=077" }, { "Id": "#1ab3ae0a", "Data": "077" }, { "Id": "#cb0c7049", "Data": "qmEA" }, { "Id": "#8fcc316a", "Data": "q7Z/IA==" }, { "Id": "#21f01298", "Data": "qPkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" }, { "Id": "#469f419f", "Data": "//////////////////////////////////8=" }, { "Id": "#9952189d", "Data": "q7dAIA==" } ] } ] } fwupd-2.0.10/plugins/ch341a/tests/ch341a.json000066400000000000000000000004761501337203100204050ustar00rootroot00000000000000{ "name": "WinChipHead CH341a", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/ch341a-setup.json", "components": [ { "version": "3.4", "guids": [ "e06cabdb-2908-5d74-a63f-0daa8a1be58a" ] } ] } ] } fwupd-2.0.10/plugins/ch347/000077500000000000000000000000001501337203100152235ustar00rootroot00000000000000fwupd-2.0.10/plugins/ch347/README.md000066400000000000000000000017001501337203100165000ustar00rootroot00000000000000--- title: Plugin: CH347 --- ## Introduction The CH347 is an affordable SPI programmer. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob of unspecified format. This plugin supports the following protocol ID: - `org.jedec.cfi` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. - `USB\VID_1A86&PID_55DB` ## Update Behavior The device programs devices in raw mode, and can best be used with `fwupdtool`. To write an image, use `sudo fwupdtool --plugins ch347 install-blob firmware.bin` and to backup the contents of a SPI device use `sudo fwupdtool --plugins ch347 firmware-dump backup.bin` ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x1A86` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.8.14`. fwupd-2.0.10/plugins/ch347/ch347.quirk000066400000000000000000000000471501337203100171310ustar00rootroot00000000000000[USB\VID_1A86&PID_55DB] Plugin = ch347 fwupd-2.0.10/plugins/ch347/fu-ch347-cfi-device.c000066400000000000000000000023621501337203100206260ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-ch347-cfi-device.h" #include "fu-ch347-device.h" struct _FuCh347CfiDevice { FuCfiDevice parent_instance; }; G_DEFINE_TYPE(FuCh347CfiDevice, fu_ch347_cfi_device, FU_TYPE_CFI_DEVICE) static gboolean fu_ch347_cfi_device_chip_select(FuCfiDevice *self, gboolean value, GError **error) { FuCh347Device *proxy = FU_CH347_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); return fu_ch347_device_chip_select(proxy, value, error); } static gboolean fu_ch347_cfi_device_send_command(FuCfiDevice *self, const guint8 *wbuf, gsize wbufsz, guint8 *rbuf, gsize rbufsz, FuProgress *progress, GError **error) { FuCh347Device *proxy = FU_CH347_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); return fu_ch347_device_send_command(proxy, wbuf, wbufsz, rbuf, rbufsz, progress, error); } static void fu_ch347_cfi_device_init(FuCh347CfiDevice *self) { } static void fu_ch347_cfi_device_class_init(FuCh347CfiDeviceClass *klass) { FuCfiDeviceClass *cfi_class = FU_CFI_DEVICE_CLASS(klass); cfi_class->chip_select = fu_ch347_cfi_device_chip_select; cfi_class->send_command = fu_ch347_cfi_device_send_command; } fwupd-2.0.10/plugins/ch347/fu-ch347-cfi-device.h000066400000000000000000000004711501337203100206320ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_CH347_CFI_DEVICE (fu_ch347_cfi_device_get_type()) G_DECLARE_FINAL_TYPE(FuCh347CfiDevice, fu_ch347_cfi_device, FU, CH347_CFI_DEVICE, FuCfiDevice) fwupd-2.0.10/plugins/ch347/fu-ch347-device.c000066400000000000000000000175541501337203100201000ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-ch347-cfi-device.h" #include "fu-ch347-device.h" #include "fu-ch347-struct.h" struct _FuCh347Device { FuUsbDevice parent_instance; guint8 divisor; }; G_DEFINE_TYPE(FuCh347Device, fu_ch347_device, FU_TYPE_USB_DEVICE) #define FU_CH347_USB_TIMEOUT 1000 #define FU_CH347_CS_ASSERT 0x00 #define FU_CH347_CS_DEASSERT 0x40 #define FU_CH347_CS_CHANGE 0x80 #define FU_CH347_CS_IGNORE 0x00 #define FU_CH347_EP_OUT 0x06 #define FU_CH347_EP_IN 0x86 #define FU_CH347_MODE1_IFACE 0x2 #define FU_CH347_MODE2_IFACE 0x1 #define FU_CH347_PACKET_SIZE 510 #define FU_CH347_PAYLOAD_SIZE (FU_CH347_PACKET_SIZE - 3) static void fu_ch347_device_to_string(FuDevice *device, guint idt, GString *str) { FuCh347Device *self = FU_CH347_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "Divisor", self->divisor); } static gboolean fu_ch347_device_write(FuCh347Device *self, FuCh347CmdSpi cmd, const guint8 *buf, gsize bufsz, GError **error) { gsize actual_length = 0; g_autoptr(FuStructCh347Req) st = fu_struct_ch347_req_new(); /* pack */ fu_struct_ch347_req_set_cmd(st, cmd); fu_struct_ch347_req_set_payloadsz(st, bufsz); if (bufsz > 0) g_byte_array_append(st, buf, bufsz); /* debug */ fu_dump_raw(G_LOG_DOMAIN, "write", st->data, st->len); if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), FU_CH347_EP_OUT, st->data, st->len, &actual_length, FU_CH347_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to write 0x%x bytes: ", (guint)bufsz); return FALSE; } if (st->len != actual_length) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "only wrote 0x%x of 0x%x", (guint)actual_length, (guint)st->len); return FALSE; } /* success */ return TRUE; } static gboolean fu_ch347_device_read(FuCh347Device *self, FuCh347CmdSpi cmd, guint8 *buf, gsize bufsz, GError **error) { gsize actual_length = 0; guint16 size_rsp; g_autoptr(GByteArray) st = fu_struct_ch347_req_new(); /* pack */ fu_struct_ch347_req_set_cmd(st, cmd); fu_struct_ch347_req_set_payloadsz(st, sizeof(guint32)); fu_byte_array_append_uint32(st, bufsz, G_LITTLE_ENDIAN); fu_byte_array_set_size(st, FU_CH347_PACKET_SIZE, 0x0); if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), FU_CH347_EP_IN, st->data, st->len, &actual_length, FU_CH347_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to read 0x%x bytes: ", (guint)bufsz); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "read", st->data, actual_length); if (actual_length == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "returned 0 bytes"); return FALSE; } /* debug */ if (fu_struct_ch347_req_get_cmd(st) != cmd) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid cmd, got 0x%02x, expected 0x%02x", fu_struct_ch347_req_get_cmd(st), cmd); return FALSE; } size_rsp = fu_struct_ch347_req_get_payloadsz(st); if (size_rsp != bufsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "size invalid, got 0x%04x, expected 0x04%x", size_rsp, (guint)bufsz); return FALSE; } /* success */ return fu_memcpy_safe(buf, bufsz, 0x0, st->data, st->len, FU_STRUCT_CH347_REQ_SIZE, size_rsp, error); } gboolean fu_ch347_device_send_command(FuCh347Device *self, const guint8 *wbuf, gsize wbufsz, guint8 *rbuf, gsize rbufsz, FuProgress *progress, GError **error) { /* write */ if (wbufsz > 0) { g_autoptr(GBytes) wblob = g_bytes_new_static(wbuf, wbufsz); g_autoptr(FuChunkArray) chunks = fu_chunk_array_new_from_bytes(wblob, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_CH347_PAYLOAD_SIZE); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { guint8 buf[1] = {0x0}; g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_ch347_device_write(self, FU_CH347_CMD_SPI_OUT, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; if (!fu_ch347_device_read(self, FU_CH347_CMD_SPI_OUT, buf, sizeof(buf), error)) return FALSE; } } /* read */ if (rbufsz > 0) { g_autoptr(GPtrArray) chunks = fu_chunk_array_mutable_new(rbuf, rbufsz, 0x0, 0x0, FU_CH347_PAYLOAD_SIZE); g_autoptr(GByteArray) cmdbuf = g_byte_array_new(); fu_byte_array_append_uint32(cmdbuf, rbufsz, G_LITTLE_ENDIAN); if (!fu_ch347_device_write(self, FU_CH347_CMD_SPI_IN, cmdbuf->data, cmdbuf->len, error)) return FALSE; fu_progress_set_id(progress, G_STRLOC); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_ch347_device_read(self, FU_CH347_CMD_SPI_IN, fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; fu_progress_step_done(progress); } } /* success */ return TRUE; } static gboolean fu_ch347_device_configure_stream(FuCh347Device *self, GError **error) { guint8 data[26] = {[2] = 4, /* ?? */ [3] = 1, /* ?? */ [6] = 0, /* clock polarity: bit 1 */ [8] = 0, /* clock phase: bit 0 */ [11] = 2, /* ?? */ [12] = (self->divisor & 0x7) << 3, /* clock divisor: bits 5:3 */ [14] = 0, /* bit order: bit 7, 0=MSB */ [16] = 7, /* ?? */ [21] = 0}; /* CS polarity: bit 7 CS2, bit 6 CS1. 0 = active low */ if (!fu_ch347_device_write(self, FU_CH347_CMD_SPI_SET_CFG, data, sizeof(data), error)) { g_prefix_error(error, "failed to configure stream: "); return FALSE; } if (!fu_ch347_device_read(self, FU_CH347_CMD_SPI_SET_CFG, data, 1, error)) { g_prefix_error(error, "failed to confirm configure stream: "); return FALSE; } /* success */ return TRUE; } gboolean fu_ch347_device_chip_select(FuCh347Device *self, gboolean val, GError **error) { guint8 buf[10] = { [0] = val ? FU_CH347_CS_ASSERT | FU_CH347_CS_CHANGE : FU_CH347_CS_DEASSERT | FU_CH347_CS_CHANGE, [5] = FU_CH347_CS_IGNORE /* CS2 */ }; return fu_ch347_device_write(self, FU_CH347_CMD_SPI_CS_CTRL, buf, sizeof(buf), error); } static gboolean fu_ch347_device_probe(FuDevice *device, GError **error) { g_autoptr(FuCh347CfiDevice) cfi_device = NULL; cfi_device = g_object_new(FU_TYPE_CH347_CFI_DEVICE, "context", fu_device_get_context(device), "proxy", device, "parent", device, "logical-id", "SPI", NULL); fu_device_add_child(device, FU_DEVICE(cfi_device)); return TRUE; } static gboolean fu_ch347_device_setup(FuDevice *device, GError **error) { FuCh347Device *self = FU_CH347_DEVICE(device); /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_ch347_device_parent_class)->setup(device, error)) return FALSE; /* set divisor */ if (!fu_ch347_device_configure_stream(self, error)) return FALSE; /* success */ return TRUE; } static void fu_ch347_device_init(FuCh347Device *self) { self->divisor = 0b10; fu_usb_device_add_interface(FU_USB_DEVICE(self), FU_CH347_MODE1_IFACE); fu_device_set_name(FU_DEVICE(self), "CH347"); fu_device_set_vendor(FU_DEVICE(self), "WinChipHead"); } static void fu_ch347_device_class_init(FuCh347DeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_ch347_device_probe; device_class->setup = fu_ch347_device_setup; device_class->to_string = fu_ch347_device_to_string; } fwupd-2.0.10/plugins/ch347/fu-ch347-device.h000066400000000000000000000011201501337203100200630ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_CH347_DEVICE (fu_ch347_device_get_type()) G_DECLARE_FINAL_TYPE(FuCh347Device, fu_ch347_device, FU, CH347_DEVICE, FuUsbDevice) gboolean fu_ch347_device_chip_select(FuCh347Device *self, gboolean val, GError **error); gboolean fu_ch347_device_send_command(FuCh347Device *self, const guint8 *wbuf, gsize wbufsz, guint8 *rbuf, gsize rbufsz, FuProgress *progress, GError **error); fwupd-2.0.10/plugins/ch347/fu-ch347-plugin.c000066400000000000000000000013021501337203100201170ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-ch347-device.h" #include "fu-ch347-plugin.h" struct _FuCh347Plugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuCh347Plugin, fu_ch347_plugin, FU_TYPE_PLUGIN) static void fu_ch347_plugin_init(FuCh347Plugin *self) { } static void fu_ch347_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_CH347_DEVICE); } static void fu_ch347_plugin_class_init(FuCh347PluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_ch347_plugin_constructed; } fwupd-2.0.10/plugins/ch347/fu-ch347-plugin.h000066400000000000000000000003511501337203100201270ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuCh347Plugin, fu_ch347_plugin, FU, CH347_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/ch347/fu-ch347.rs000066400000000000000000000005611501337203100170330ustar00rootroot00000000000000// Copyright 2024 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[repr(u8)] enum FuCh347CmdSpi { SetCfg = 0xC0, CsCtrl = 0xC1, OutIn = 0xC2, In = 0xC3, Out = 0xC4, GetCfg = 0xCA, } #[derive(New, Getters)] #[repr(C, packed)] struct FuStructCh347Req { cmd: FuCh347CmdSpi, payloadsz: u16le, } fwupd-2.0.10/plugins/ch347/meson.build000066400000000000000000000010271501337203100173650ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginCh347"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('ch347.quirk') plugin_builtins += static_library('fu_plugin_ch347', rustgen.process('fu-ch347.rs'), sources: [ 'fu-ch347-cfi-device.c', 'fu-ch347-device.c', 'fu-ch347-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) enumeration_data += files('tests/ch347-setup.json') device_tests += files('tests/ch347.json') fwupd-2.0.10/plugins/ch347/tests/000077500000000000000000000000001501337203100163655ustar00rootroot00000000000000fwupd-2.0.10/plugins/ch347/tests/ch347-setup.json000066400000000000000000000057221501337203100212540ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUsbDevice", "PlatformId": "1-1", "Created": "2024-10-24T17:45:42.490939Z", "IdVendor": 6790, "IdProduct": 21979, "Device": 320, "USB": 512, "Manufacturer": 1, "Product": 2, "SerialNumber": 3, "UsbConfigDescriptors": [ { "ConfigurationValue": 1 } ], "UsbInterfaces": [ { "Length": 9, "DescriptorType": 4, "InterfaceClass": 2, "InterfaceSubClass": 2, "InterfaceProtocol": 1, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 131, "Interval": 1, "MaxPacketSize": 64 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 1, "InterfaceClass": 10, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 4, "MaxPacketSize": 512 }, { "DescriptorType": 5, "EndpointAddress": 132, "MaxPacketSize": 512 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 2, "InterfaceClass": 255, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 6, "MaxPacketSize": 512 }, { "DescriptorType": 5, "EndpointAddress": 134, "MaxPacketSize": 512 } ] } ], "UsbEvents": [ { "Id": "#4693935e", "Data": "001" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=75\nDEVNAME=bus/usb/001/076\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=1a86/55db/140\nTYPE=0/0/0\nBUSNUM=001\nDEVNUM=076" }, { "Id": "#1ab3ae0a", "Data": "076" }, { "Id": "#028c3a0e", "Data": "MDEyMzQ1Njc4OQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" }, { "Id": "#4f7f767b", "Data": "wBoAAAAEAQAAAAAAAAACEAAAAAcAAAAAAAAAAAA=" }, { "Id": "#e8e979a2", "Data": "wAEAAA==" }, { "Id": "#e62e60a8", "Data": "wQoAgAAAAAAAAAAAAA==" }, { "Id": "#e8f9929d", "Data": "xAEAnw==" }, { "Id": "#9074dd4e", "Data": "xAEAAA==" }, { "Id": "#c6abc2c8", "Data": "wwQAAwAAAA==" }, { "Id": "#43321f40", "Data": "wwMAAAAA" }, { "Id": "#279f56ea", "Data": "wQoAwAAAAAAAAAAAAA==" } ] } ] } fwupd-2.0.10/plugins/ch347/tests/ch347.json000066400000000000000000000004751501337203100201160ustar00rootroot00000000000000{ "name": "WinChipHead CH347", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/ch347-setup.json", "components": [ { "version": "1.40", "guids": [ "114d0d9f-3a6a-55f1-9f67-4ba7ec653071" ] } ] } ] } fwupd-2.0.10/plugins/corsair/000077500000000000000000000000001501337203100160355ustar00rootroot00000000000000fwupd-2.0.10/plugins/corsair/README.md000066400000000000000000000040751501337203100173220ustar00rootroot00000000000000--- title: Plugin: Corsair --- ## Introduction This plugin allows to update firmware on Corsair mice and receivers: * SABRE RGB PRO WIRELESS * SLIPSTREAM WIRELESS USB Receiver * KATAR PRO WIRELESS * KATAR PRO XT Gaming Mouse * SABRE PRO Gaming Mouse ## Code structure All devices handled by one object (FuCorsairDevice). Receivers with wireless-only devices will be shown as two entities: parent device as a receiver and wireless device as a child. Difference in behavior is handled by private flags. FuCorsairBp contains low-level protocol related routines. Device objects should call correct versions of these routines in order to update firmware. Correct routines chosen by device quirks and private flags. ## Wired mice update behavior Mice and/or it's wireless adapter must be connected to host via USB cable to apply an update. The device is switched to bootloader mode to flash updates, and is reset automatically to new firmware after flashing. ## Wireless mice update behavior The receiver should be connected to host and the mouse should be turned on and not sleeping. ## Quirk Use This plugin uses the following plugin-specific quirks: ### CorsairVendorInterfaceId Some devices have non-standard USB interface for protocol communication. This quirk should be set if protocol interface is not 1. Since: 1.8.0 ### CorsairSubdeviceId Specifies ID of any wireless child device which can be updated. Polling will be turned on if a subdevice is not connected when parent is being probed. ### Flags:legacy-attach This flag is used if legacy attach command should be used ### Flags:no-version-in-bl This flag handles cases if device reports incorrect firmware version in bootloader mode. ### Flags:is-subdevice This flag tells device that it is a child device. All subdevice behavior tweaks will be applied. ## Version Considerations This plugin has been available since fwupd version `1.8.0`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Andrii Dushko: @dushko-devx fwupd-2.0.10/plugins/corsair/corsair.quirk000066400000000000000000000007511501337203100205570ustar00rootroot00000000000000[USB\VID_1B1C&PID_1BAC] Plugin = corsair GType = FuCorsairDevice Name = KATAR PRO XT Gaming Mouse CorsairDeviceKind = mouse Flags = legacy-attach,no-version-in-bl [USB\VID_1B1C&PID_1B7A] Plugin = corsair GType = FuCorsairDevice Name = SABRE PRO Gaming Mouse CorsairDeviceKind = mouse Flags = legacy-attach,no-version-in-bl [USB\VID_1B1C&PID_1B79] Plugin = corsair GType = FuCorsairDevice Name = SABRE RGB PRO Gaming Mouse CorsairDeviceKind = mouse Flags = legacy-attach,no-version-in-bl fwupd-2.0.10/plugins/corsair/fu-corsair-bp.c000066400000000000000000000311611501337203100206540ustar00rootroot00000000000000/* * Copyright 2022 Andrii Dushko * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-corsair-bp.h" #include "fu-corsair-common.h" #define CORSAIR_DEFAULT_VENDOR_INTERFACE_ID 1 #define CORSAIR_ACTIVATION_TIMEOUT 30000 #define CORSAIR_MODE_BOOTLOADER 3 #define CORSAIR_FIRST_CHUNK_HEADER_SIZE 7 #define CORSAIR_NEXT_CHUNKS_HEADER_SIZE 3 #define CORSAIR_TRANSACTION_TIMEOUT 10000 #define CORSAIR_DEFAULT_CMD_SIZE 64 #define CORSAIR_OFFSET_CMD_PROPERTY_ID 0x02 #define CORSAIR_OFFSET_CMD_PROPERTY_VALUE 0x03 #define CORSAIR_OFFSET_CMD_VERSION 0x03 #define CORSAIR_OFFSET_CMD_CRC 0x08 #define CORSAIR_OFFSET_CMD_MODE 0x03 #define CORSAIR_OFFSET_CMD_STATUS 0x02 #define CORSAIR_OFFSET_CMD_FIRMWARE_SIZE 0x03 #define CORSAIR_OFFSET_CMD_SET_MODE 0x04 #define CORSAIR_OFFSET_CMD_DESTINATION 0x00 #define CORSAIR_INPUT_FLUSH_TIMEOUT 10 #define CORSAIR_INPUT_FLUSH_ITERATIONS 3 typedef enum { FU_CORSAIR_BP_DESTINATION_SELF = 0x08, FU_CORSAIR_BP_DESTINATION_SUBDEVICE = 0x09 } FuCorsairBpDestination; struct _FuCorsairBp { FuUsbDevice parent_instance; guint8 destination; guint8 epin; guint8 epout; guint16 cmd_write_size; guint16 cmd_read_size; gboolean is_legacy_attach; }; G_DEFINE_TYPE(FuCorsairBp, fu_corsair_bp, FU_TYPE_USB_DEVICE) static gboolean fu_corsair_bp_command(FuCorsairBp *self, guint8 *data, guint timeout, gboolean need_reply, GError **error) { gsize actual_len = 0; gboolean ret; data[CORSAIR_OFFSET_CMD_DESTINATION] = self->destination; fu_dump_raw("FuPluginCorsair", "command", data, self->cmd_write_size); ret = fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), self->epout, data, self->cmd_write_size, &actual_len, timeout, NULL, error); if (!ret) { g_prefix_error(error, "failed to write command: "); return FALSE; } if (actual_len != self->cmd_write_size) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "wrong size written: %" G_GSIZE_FORMAT, actual_len); return FALSE; } if (!need_reply) return TRUE; memset(data, 0, FU_CORSAIR_MAX_CMD_SIZE); ret = fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), self->epin, data, self->cmd_read_size, &actual_len, timeout, NULL, error); if (!ret) { g_prefix_error(error, "failed to get command response: "); return FALSE; } if (actual_len != self->cmd_read_size) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "wrong size read: %" G_GSIZE_FORMAT, actual_len); return FALSE; } fu_dump_raw("FuPluginCorsair", "response", data, self->cmd_write_size); if (data[CORSAIR_OFFSET_CMD_STATUS] != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "device replied with error: 0x%02x", data[CORSAIR_OFFSET_CMD_STATUS]); return FALSE; } return TRUE; } /** * @brief Flush all input reports if there are any. * @self: a #FuCorsairBp * * This function clears any dangling IN reports that * the device may have sent after the enumeration. */ void fu_corsair_bp_flush_input_reports(FuCorsairBp *self) { gsize actual_len; g_autofree guint8 *buf = g_malloc0(self->cmd_read_size); for (guint i = 0; i < CORSAIR_INPUT_FLUSH_ITERATIONS; i++) { g_autoptr(GError) error_local = NULL; if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), self->epin, buf, self->cmd_read_size, &actual_len, CORSAIR_INPUT_FLUSH_TIMEOUT, NULL, &error_local)) g_debug("flushing status: %s", error_local->message); } } static gboolean fu_corsair_bp_write_first_chunk(FuCorsairBp *self, FuChunk *chunk, guint32 firmware_size, GError **error) { guint8 init_cmd[FU_CORSAIR_MAX_CMD_SIZE] = {0x08, 0x0d, 0x00, 0x03}; guint8 write_cmd[FU_CORSAIR_MAX_CMD_SIZE] = {0x08, 0x06, 0x00}; if (!fu_corsair_bp_command(self, init_cmd, CORSAIR_TRANSACTION_TIMEOUT, TRUE, error)) { g_prefix_error(error, "firmware init fail: "); return FALSE; } if (!fu_memwrite_uint32_safe(write_cmd, sizeof(write_cmd), CORSAIR_OFFSET_CMD_FIRMWARE_SIZE, firmware_size, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "cannot serialize firmware size: "); return FALSE; } if (!fu_memcpy_safe(write_cmd, sizeof(write_cmd), CORSAIR_FIRST_CHUNK_HEADER_SIZE, fu_chunk_get_data(chunk), fu_chunk_get_data_sz(chunk), 0, fu_chunk_get_data_sz(chunk), error)) { g_prefix_error(error, "cannot set data: "); return FALSE; } if (!fu_corsair_bp_command(self, write_cmd, CORSAIR_TRANSACTION_TIMEOUT, TRUE, error)) { g_prefix_error(error, "write command fail: "); return FALSE; } return TRUE; } static gboolean fu_corsair_bp_write_chunk(FuCorsairBp *self, FuChunk *chunk, GError **error) { guint8 cmd[FU_CORSAIR_MAX_CMD_SIZE] = {0x08, 0x07}; if (!fu_memcpy_safe(cmd, sizeof(cmd), CORSAIR_NEXT_CHUNKS_HEADER_SIZE, fu_chunk_get_data(chunk), fu_chunk_get_data_sz(chunk), 0, fu_chunk_get_data_sz(chunk), error)) { g_prefix_error(error, "cannot set data: "); return FALSE; } if (!fu_corsair_bp_command(self, cmd, CORSAIR_TRANSACTION_TIMEOUT, TRUE, error)) { g_prefix_error(error, "write command fail: "); return FALSE; } return TRUE; } static void fu_corsair_bp_incorporate(FuDevice *self, FuDevice *donor) { FuCorsairBp *bp_self = FU_CORSAIR_BP(self); FuCorsairBp *bp_donor = FU_CORSAIR_BP(donor); g_return_if_fail(FU_IS_CORSAIR_BP(self)); g_return_if_fail(FU_IS_CORSAIR_BP(donor)); bp_self->epin = bp_donor->epin; bp_self->epout = bp_donor->epout; bp_self->cmd_write_size = bp_donor->cmd_write_size; bp_self->cmd_read_size = bp_donor->cmd_read_size; } static void fu_corsair_bp_init(FuCorsairBp *self) { self->cmd_read_size = CORSAIR_DEFAULT_CMD_SIZE; self->cmd_write_size = CORSAIR_DEFAULT_CMD_SIZE; self->destination = FU_CORSAIR_BP_DESTINATION_SELF; } gboolean fu_corsair_bp_get_property(FuCorsairBp *self, FuCorsairBpProperty property, guint32 *value, GError **error) { guint8 data[FU_CORSAIR_MAX_CMD_SIZE] = {0x08, 0x02}; fu_memwrite_uint16(&data[CORSAIR_OFFSET_CMD_PROPERTY_ID], (guint16)property, G_LITTLE_ENDIAN); if (!fu_corsair_bp_command(self, data, CORSAIR_TRANSACTION_TIMEOUT, TRUE, error)) return FALSE; *value = fu_memread_uint32(&data[CORSAIR_OFFSET_CMD_PROPERTY_VALUE], G_LITTLE_ENDIAN); return TRUE; } static gboolean fu_corsair_bp_set_mode(FuCorsairBp *self, FuCorsairDeviceMode mode, GError **error) { guint8 cmd[FU_CORSAIR_MAX_CMD_SIZE] = {0x08, 0x01, 0x03}; cmd[CORSAIR_OFFSET_CMD_SET_MODE] = mode; if (!fu_corsair_bp_command(self, cmd, CORSAIR_TRANSACTION_TIMEOUT, TRUE, error)) { g_prefix_error(error, "set mode command fail: "); return FALSE; } return TRUE; } static gboolean fu_corsair_bp_write_firmware_chunks(FuCorsairBp *self, FuChunk *first_chunk, FuChunkArray *chunks, FuProgress *progress, guint32 firmware_size, GError **error) { fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks) + 1); if (!fu_corsair_bp_write_first_chunk(self, first_chunk, firmware_size, error)) { g_prefix_error(error, "cannot write first chunk: "); return FALSE; } fu_progress_step_done(progress); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_corsair_bp_write_chunk(self, chk, error)) { g_prefix_error(error, "cannot write chunk %u: ", i); return FALSE; } fu_progress_step_done(progress); } return TRUE; } static gboolean fu_corsair_bp_commit_firmware(FuCorsairBp *self, GError **error) { guint8 commit_cmd[FU_CORSAIR_MAX_CMD_SIZE] = {0x08, 0x05, 0x01, 0x00}; if (!fu_corsair_bp_command(self, commit_cmd, CORSAIR_TRANSACTION_TIMEOUT, TRUE, error)) { g_prefix_error(error, "firmware commit fail: "); return FALSE; } return TRUE; } static gboolean fu_corsair_bp_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { const guint8 *firmware_raw; gsize firmware_size; g_autoptr(GBytes) blob = NULL; g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(FuChunk) firstChunk = NULL; g_autoptr(GBytes) rest_of_firmware = NULL; FuCorsairBp *self = FU_CORSAIR_BP(device); guint32 first_chunk_size = self->cmd_write_size - CORSAIR_FIRST_CHUNK_HEADER_SIZE; blob = fu_firmware_get_bytes(firmware, error); if (blob == NULL) { g_prefix_error(error, "cannot get firmware data: "); return FALSE; } firmware_raw = fu_bytes_get_data_safe(blob, &firmware_size, error); if (firmware_raw == NULL) { g_prefix_error(error, "cannot get firmware data: "); return FALSE; } /* the firmware size should be greater than 1 chunk */ if (firmware_size <= first_chunk_size) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "update file should be bigger"); return FALSE; } firstChunk = fu_chunk_new(0, 0, 0, g_bytes_get_data(blob, NULL), first_chunk_size); rest_of_firmware = fu_bytes_new_offset(blob, first_chunk_size, firmware_size - first_chunk_size, error); if (rest_of_firmware == NULL) { g_prefix_error(error, "cannot get firmware past first chunk: "); return FALSE; } chunks = fu_chunk_array_new_from_bytes(rest_of_firmware, first_chunk_size, FU_CHUNK_PAGESZ_NONE, self->cmd_write_size - CORSAIR_NEXT_CHUNKS_HEADER_SIZE); if (!fu_corsair_bp_write_firmware_chunks(self, firstChunk, chunks, progress, g_bytes_get_size(blob), error)) return FALSE; if (!fu_corsair_bp_commit_firmware(self, error)) return FALSE; return TRUE; } gboolean fu_corsair_bp_activate_firmware(FuCorsairBp *self, FuFirmware *firmware, GError **error) { g_autoptr(GBytes) blob = NULL; guint8 cmd[FU_CORSAIR_MAX_CMD_SIZE] = {0x08, 0x16, 0x00, 0x01, 0x03, 0x00, 0x01, 0x01}; blob = fu_firmware_get_bytes(firmware, error); if (blob == NULL) { g_prefix_error(error, "cannot get firmware bytes: "); return FALSE; } fu_memwrite_uint32(&cmd[CORSAIR_OFFSET_CMD_CRC], fu_crc32_bytes(FU_CRC_KIND_B32_MPEG2, blob), G_LITTLE_ENDIAN); return fu_corsair_bp_command(self, cmd, CORSAIR_ACTIVATION_TIMEOUT, TRUE, error); } static gboolean fu_corsair_bp_attach(FuDevice *device, FuProgress *progress, GError **error) { FuCorsairBp *self = FU_CORSAIR_BP(device); if (self->is_legacy_attach) { guint8 cmd[FU_CORSAIR_MAX_CMD_SIZE] = {0x08, 0x10, 0x01, 0x00, 0x03, 0x00, 0x01}; return fu_corsair_bp_command(self, cmd, CORSAIR_TRANSACTION_TIMEOUT, FALSE, error); } return fu_corsair_bp_set_mode(self, FU_CORSAIR_DEVICE_MODE_APPLICATION, error); } static gboolean fu_corsair_bp_detach(FuDevice *device, FuProgress *progress, GError **error) { FuCorsairBp *self = FU_CORSAIR_BP(device); return fu_corsair_bp_set_mode(self, FU_CORSAIR_DEVICE_MODE_BOOTLOADER, error); } static void fu_corsair_bp_to_string(FuDevice *device, guint idt, GString *str) { FuCorsairBp *self = FU_CORSAIR_BP(device); fwupd_codec_string_append_hex(str, idt, "InEndpoint", self->epin); fwupd_codec_string_append_hex(str, idt, "OutEndpoint", self->epout); } static void fu_corsair_bp_class_init(FuCorsairBpClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->incorporate = fu_corsair_bp_incorporate; device_class->write_firmware = fu_corsair_bp_write_firmware; device_class->attach = fu_corsair_bp_attach; device_class->detach = fu_corsair_bp_detach; device_class->to_string = fu_corsair_bp_to_string; } FuCorsairBp * fu_corsair_bp_new(FuUsbDevice *usb_device, gboolean is_subdevice) { FuCorsairBp *self = g_object_new(FU_TYPE_CORSAIR_BP, NULL); fu_device_incorporate(FU_DEVICE(self), FU_DEVICE(usb_device), FU_DEVICE_INCORPORATE_FLAG_ALL); if (is_subdevice) { self->destination = FU_CORSAIR_BP_DESTINATION_SUBDEVICE; } else { self->destination = FU_CORSAIR_BP_DESTINATION_SELF; } return self; } void fu_corsair_bp_set_cmd_size(FuCorsairBp *self, guint16 write_size, guint16 read_size) { self->cmd_write_size = write_size; self->cmd_read_size = read_size; } void fu_corsair_bp_set_endpoints(FuCorsairBp *self, guint8 epin, guint8 epout) { self->epin = epin; self->epout = epout; } void fu_corsair_bp_set_legacy_attach(FuCorsairBp *self, gboolean is_legacy_attach) { self->is_legacy_attach = is_legacy_attach; } fwupd-2.0.10/plugins/corsair/fu-corsair-bp.h000066400000000000000000000017751501337203100206710ustar00rootroot00000000000000/* * Copyright 2021 Andrii Dushko * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-corsair-common.h" #define FU_TYPE_CORSAIR_BP (fu_corsair_bp_get_type()) G_DECLARE_FINAL_TYPE(FuCorsairBp, fu_corsair_bp, FU, CORSAIR_BP, FuUsbDevice) struct _FuCorsairBpClass { FuUsbDeviceClass parent_class; }; void fu_corsair_bp_flush_input_reports(FuCorsairBp *self); gboolean fu_corsair_bp_get_property(FuCorsairBp *self, FuCorsairBpProperty property, guint32 *value, GError **error); gboolean fu_corsair_bp_activate_firmware(FuCorsairBp *self, FuFirmware *firmware, GError **error); void fu_corsair_bp_set_cmd_size(FuCorsairBp *self, guint16 write_size, guint16 read_size); void fu_corsair_bp_set_endpoints(FuCorsairBp *self, guint8 epin, guint8 epout); void fu_corsair_bp_set_legacy_attach(FuCorsairBp *self, gboolean is_legacy_attach); FuCorsairBp * fu_corsair_bp_new(FuUsbDevice *usb_device, gboolean is_subdevice); fwupd-2.0.10/plugins/corsair/fu-corsair-common.c000066400000000000000000000012041501337203100215360ustar00rootroot00000000000000/* * Copyright 2022 Andrii Dushko * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-corsair-common.h" /** * fu_corsair_version_from_uint32: * @val: version in corsair device format * * fu_version_from_uint32(... %FWUPD_VERSION_FORMAT_TRIPLET) * cannot be used because bytes in the version are in non-standard * order: 0xCCDD.BB.AA. * * Returns: a version number, e.g. `1.0.3`. **/ gchar * fu_corsair_version_from_uint32(guint32 value) { return g_strdup_printf("%u.%u.%u", value & 0xff, (value >> 8) & 0xff, (value >> 16) & 0xffff); } fwupd-2.0.10/plugins/corsair/fu-corsair-common.h000066400000000000000000000011671501337203100215530ustar00rootroot00000000000000/* * Copyright 2022 Andrii Dushko * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_CORSAIR_MAX_CMD_SIZE 1024 typedef enum { FU_CORSAIR_BP_PROPERTY_MODE = 0x03, FU_CORSAIR_BP_PROPERTY_BATTERY_LEVEL = 0x0F, FU_CORSAIR_BP_PROPERTY_VERSION = 0x13, FU_CORSAIR_BP_PROPERTY_BOOTLOADER_VERSION = 0x14, FU_CORSAIR_BP_PROPERTY_SUBDEVICES = 0x36, } FuCorsairBpProperty; typedef enum { FU_CORSAIR_DEVICE_MODE_APPLICATION = 0x01, FU_CORSAIR_DEVICE_MODE_BOOTLOADER = 0x03 } FuCorsairDeviceMode; gchar * fu_corsair_version_from_uint32(guint32 val); fwupd-2.0.10/plugins/corsair/fu-corsair-device.c000066400000000000000000000420561501337203100215170ustar00rootroot00000000000000/* * Copyright 2022 Andrii Dushko * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-corsair-bp.h" #include "fu-corsair-common.h" #include "fu-corsair-device.h" #include "fu-corsair-struct.h" #define CORSAIR_DEFAULT_VENDOR_INTERFACE_ID 1 #define CORSAIR_TRANSACTION_TIMEOUT 4000 #define CORSAIR_SUBDEVICE_POLL_PERIOD 30000 #define CORSAIR_SUBDEVICE_REBOOT_DELAY 4000 /* ms */ #define CORSAIR_SUBDEVICE_RECONNECT_RETRIES 30 #define CORSAIR_SUBDEVICE_RECONNECT_PERIOD 1000 #define CORSAIR_SUBDEVICE_FIRST_POLL_DELAY 2000 /* ms */ #define FU_CORSAIR_DEVICE_FLAG_LEGACY_ATTACH "legacy-attach" #define FU_CORSAIR_DEVICE_FLAG_IS_SUBDEVICE "is-subdevice" #define FU_CORSAIR_DEVICE_FLAG_NO_VERSION_IN_BOOTLOADER "no-version-in-bl" struct _FuCorsairDevice { FuUsbDevice parent_instance; FuCorsairDeviceKind device_kind; guint8 vendor_interface; gchar *subdevice_id; FuCorsairBp *bp; }; G_DEFINE_TYPE(FuCorsairDevice, fu_corsair_device, FU_TYPE_USB_DEVICE) static FuCorsairDevice * fu_corsair_device_new(FuCorsairDevice *parent, FuCorsairBp *bp); static gboolean fu_corsair_device_probe(FuDevice *device, GError **error) { FuCorsairDevice *self = FU_CORSAIR_DEVICE(device); FuUsbInterface *iface = NULL; FuUsbEndpoint *ep1 = NULL; FuUsbEndpoint *ep2 = NULL; g_autoptr(GPtrArray) ifaces = NULL; g_autoptr(GPtrArray) endpoints = NULL; guint16 cmd_write_size; guint16 cmd_read_size; guint8 epin; guint8 epout; /* probing are skipped for subdevices */ if (fu_device_has_private_flag(device, FU_CORSAIR_DEVICE_FLAG_IS_SUBDEVICE)) return TRUE; if (!FU_DEVICE_CLASS(fu_corsair_device_parent_class)->probe(device, error)) return FALSE; ifaces = fu_usb_device_get_interfaces(FU_USB_DEVICE(self), error); if (ifaces == NULL || (ifaces->len < (self->vendor_interface + 1u))) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "update interface not found"); return FALSE; } iface = g_ptr_array_index(ifaces, self->vendor_interface); endpoints = fu_usb_interface_get_endpoints(iface); /* expecting to have two endpoints for communication */ if (endpoints == NULL || endpoints->len != 2) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "update interface endpoints not found"); return FALSE; } ep1 = g_ptr_array_index(endpoints, 0); ep2 = g_ptr_array_index(endpoints, 1); if (fu_usb_endpoint_get_direction(ep1) == FU_USB_DIRECTION_DEVICE_TO_HOST) { epin = fu_usb_endpoint_get_address(ep1); epout = fu_usb_endpoint_get_address(ep2); cmd_read_size = fu_usb_endpoint_get_maximum_packet_size(ep1); cmd_write_size = fu_usb_endpoint_get_maximum_packet_size(ep2); } else { epin = fu_usb_endpoint_get_address(ep2); epout = fu_usb_endpoint_get_address(ep1); cmd_read_size = fu_usb_endpoint_get_maximum_packet_size(ep2); cmd_write_size = fu_usb_endpoint_get_maximum_packet_size(ep1); } if (cmd_write_size > FU_CORSAIR_MAX_CMD_SIZE || cmd_read_size > FU_CORSAIR_MAX_CMD_SIZE) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "endpoint size is bigger than allowed command size"); return FALSE; } fu_usb_device_add_interface(FU_USB_DEVICE(self), self->vendor_interface); self->bp = fu_corsair_bp_new(FU_USB_DEVICE(device), FALSE); fu_corsair_bp_set_cmd_size(self->bp, cmd_write_size, cmd_read_size); fu_corsair_bp_set_endpoints(self->bp, epin, epout); return TRUE; } static gboolean fu_corsair_device_poll_subdevice(FuDevice *device, gboolean *subdevice_added, GError **error) { FuCorsairDevice *self = FU_CORSAIR_DEVICE(device); guint32 subdevices; g_autoptr(FuCorsairDevice) child = NULL; g_autoptr(FuCorsairBp) child_bp = NULL; if (!fu_corsair_bp_get_property(self->bp, FU_CORSAIR_BP_PROPERTY_SUBDEVICES, &subdevices, error)) { g_prefix_error(error, "cannot get subdevices: "); return FALSE; } if (subdevices == 0) { *subdevice_added = FALSE; return TRUE; } child_bp = fu_corsair_bp_new(FU_USB_DEVICE(device), TRUE); fu_device_incorporate(FU_DEVICE(child_bp), FU_DEVICE(self->bp), FU_DEVICE_INCORPORATE_FLAG_ALL); child = fu_corsair_device_new(self, child_bp); fu_device_add_instance_id(FU_DEVICE(child), self->subdevice_id); fu_device_set_logical_id(FU_DEVICE(child), "subdevice"); fu_device_add_private_flag(FU_DEVICE(child), FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_OPEN); if (!fu_device_probe(FU_DEVICE(child), error)) return FALSE; if (!fu_device_setup(FU_DEVICE(child), error)) return FALSE; fu_device_add_child(device, FU_DEVICE(child)); *subdevice_added = TRUE; return TRUE; } static gchar * fu_corsair_device_get_version(FuDevice *device, GError **error) { FuCorsairDevice *self = FU_CORSAIR_DEVICE(device); guint32 version_raw; if (!fu_corsair_bp_get_property(self->bp, FU_CORSAIR_BP_PROPERTY_VERSION, &version_raw, error)) return NULL; if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { gboolean broken_by_flag = fu_device_has_private_flag(device, FU_CORSAIR_DEVICE_FLAG_NO_VERSION_IN_BOOTLOADER); /* Version 0xffffffff means that previous update was interrupted. Set version to 0.0.0 in both broken and interrupted cases to make sure that new firmware will not be rejected because of older version. It is safe to always pass firmware because setup in bootloader mode can only happen during emergency update */ if (broken_by_flag || version_raw == G_MAXUINT32) { version_raw = 0; } } return fu_corsair_version_from_uint32(version_raw); } static gchar * fu_corsair_device_get_bootloader_version(FuCorsairBp *self, GError **error) { guint32 version_raw; if (!fu_corsair_bp_get_property(self, FU_CORSAIR_BP_PROPERTY_BOOTLOADER_VERSION, &version_raw, error)) return NULL; return fu_corsair_version_from_uint32(version_raw); } static gboolean fu_corsair_device_setup(FuDevice *device, GError **error) { guint32 mode; guint32 battery_level; g_autofree gchar *bootloader_version = NULL; g_autofree gchar *version = NULL; FuCorsairDevice *self = FU_CORSAIR_DEVICE(device); fu_corsair_bp_flush_input_reports(self->bp); if (!fu_corsair_bp_get_property(self->bp, FU_CORSAIR_BP_PROPERTY_MODE, &mode, error)) return FALSE; if (mode == FU_CORSAIR_DEVICE_MODE_BOOTLOADER) fu_device_add_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); version = fu_corsair_device_get_version(device, error); if (version == NULL) { g_prefix_error(error, "cannot get version: "); return FALSE; } fu_device_set_version(device, version); bootloader_version = fu_corsair_device_get_bootloader_version(self->bp, error); if (bootloader_version == NULL) { g_prefix_error(error, "cannot get bootloader version: "); return FALSE; } fu_device_set_version_bootloader(device, bootloader_version); if (fu_device_has_private_flag(device, FU_CORSAIR_DEVICE_FLAG_IS_SUBDEVICE) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { if (!fu_corsair_bp_get_property(self->bp, FU_CORSAIR_BP_PROPERTY_BATTERY_LEVEL, &battery_level, error)) { g_prefix_error(error, "cannot get battery level: "); return FALSE; } fu_device_set_battery_level(device, battery_level / 10); } fu_corsair_bp_set_legacy_attach( self->bp, fu_device_has_private_flag(device, FU_CORSAIR_DEVICE_FLAG_LEGACY_ATTACH)); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); /* check for a subdevice */ if (self->subdevice_id != NULL && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { gboolean subdevice_added = FALSE; g_autoptr(GError) local_error = NULL; /* Give some time to a subdevice to get connected to the receiver. * Without this delay a subdevice may be not present even if it is * turned on. */ fu_device_sleep(device, CORSAIR_SUBDEVICE_FIRST_POLL_DELAY); if (!fu_corsair_device_poll_subdevice(device, &subdevice_added, &local_error)) { g_warning("error polling subdevice: %s", local_error->message); } else { /* start polling if a subdevice was not added */ if (!subdevice_added) fu_device_set_poll_interval(device, CORSAIR_SUBDEVICE_POLL_PERIOD); } } return TRUE; } static gboolean fu_corsair_device_reload(FuDevice *device, GError **error) { if (fu_device_has_private_flag(device, FU_CORSAIR_DEVICE_FLAG_IS_SUBDEVICE)) { return fu_corsair_device_setup(device, error); } /* USB devices will be reloaded by FWUPD after reenumeration */ return TRUE; } static gboolean fu_corsair_device_is_subdevice_connected_cb(FuDevice *device, gpointer user_data, GError **error) { guint32 subdevices = 0; FuCorsairDevice *self = FU_CORSAIR_DEVICE(device); if (!fu_corsair_bp_get_property(self->bp, FU_CORSAIR_BP_PROPERTY_SUBDEVICES, &subdevices, error)) { g_prefix_error(error, "cannot get subdevices: "); return FALSE; } if (subdevices == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "subdevice is not connected"); return FALSE; } return TRUE; } static gboolean fu_corsair_device_reconnect_subdevice(FuDevice *device, GError **error) { FuDevice *parent = fu_device_get_parent(device); if (parent == NULL) { g_prefix_error(error, "cannot get parent: "); return FALSE; } /* Wait some time to make sure that a subdevice was disconnected. */ fu_device_sleep(device, CORSAIR_SUBDEVICE_REBOOT_DELAY); if (!fu_device_retry_full(parent, fu_corsair_device_is_subdevice_connected_cb, CORSAIR_SUBDEVICE_RECONNECT_RETRIES, CORSAIR_SUBDEVICE_RECONNECT_PERIOD, NULL, error)) { g_prefix_error(error, "a subdevice did not reconnect after attach: "); return FALSE; } return TRUE; } static gboolean fu_corsair_device_ensure_mode(FuDevice *device, FuCorsairDeviceMode mode, GError **error) { FuCorsairDevice *self = FU_CORSAIR_DEVICE(device); FuCorsairDeviceMode current_mode; if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { current_mode = FU_CORSAIR_DEVICE_MODE_BOOTLOADER; } else { current_mode = FU_CORSAIR_DEVICE_MODE_APPLICATION; } if (mode == current_mode) return TRUE; if (mode == FU_CORSAIR_DEVICE_MODE_APPLICATION) { if (!fu_device_attach(FU_DEVICE(self->bp), error)) { g_prefix_error(error, "attach failed: "); return FALSE; } } else { if (!fu_device_detach(FU_DEVICE(self->bp), error)) { g_prefix_error(error, "detach failed: "); return FALSE; } } if (fu_device_has_private_flag(device, FU_CORSAIR_DEVICE_FLAG_IS_SUBDEVICE)) { if (!fu_corsair_device_reconnect_subdevice(device, error)) { g_prefix_error(error, "subdevice did not reconnect: "); return FALSE; } if (mode == FU_CORSAIR_DEVICE_MODE_BOOTLOADER) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); } else { fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); } } else { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); } return TRUE; } static gboolean fu_corsair_device_attach(FuDevice *device, FuProgress *progress, GError **error) { return fu_corsair_device_ensure_mode(device, FU_CORSAIR_DEVICE_MODE_APPLICATION, error); } static gboolean fu_corsair_device_detach(FuDevice *device, FuProgress *progress, GError **error) { return fu_corsair_device_ensure_mode(device, FU_CORSAIR_DEVICE_MODE_BOOTLOADER, error); } static gboolean fu_corsair_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuCorsairDevice *self = FU_CORSAIR_DEVICE(device); fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 95, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 5, NULL); if (!fu_device_write_firmware(FU_DEVICE(self->bp), firmware, fu_progress_get_child(progress), flags, error)) { g_prefix_error(error, "cannot write firmware: "); return FALSE; } fu_progress_step_done(progress); if (!fu_device_has_private_flag(device, FU_CORSAIR_DEVICE_FLAG_LEGACY_ATTACH)) { if (!fu_corsair_bp_activate_firmware(self->bp, firmware, error)) { g_prefix_error(error, "firmware activation fail: "); return FALSE; } fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); } fu_progress_step_done(progress); return TRUE; } static void fu_corsair_device_to_string(FuDevice *device, guint idt, GString *str) { FuCorsairDevice *self = FU_CORSAIR_DEVICE(device); fwupd_codec_string_append(str, idt, "DeviceKind", fu_corsair_device_kind_to_string(self->device_kind)); if (self->bp != NULL) fu_device_add_string(FU_DEVICE(self->bp), idt, str); } static void fu_corsair_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 4, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 92, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 4, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reload"); } static gboolean fu_corsair_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuCorsairDevice *self = FU_CORSAIR_DEVICE(device); guint64 vendor_interface; if (g_strcmp0(key, "CorsairDeviceKind") == 0) { self->device_kind = fu_corsair_device_kind_from_string(value); if (self->device_kind != FU_CORSAIR_DEVICE_KIND_UNKNOWN) return TRUE; g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "unsupported device in quirk"); return FALSE; } if (g_strcmp0(key, "CorsairVendorInterfaceId") == 0) { /* clapped to uint8 because bNumInterfaces is 8 bits long */ if (!fu_strtoull(value, &vendor_interface, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "cannot parse CorsairVendorInterface: "); return FALSE; } self->vendor_interface = vendor_interface; return TRUE; } if (g_strcmp0(key, "CorsairSubdeviceId") == 0) { self->subdevice_id = g_strdup(value); return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static gboolean fu_corsair_device_poll(FuDevice *device, GError **error) { gboolean subdevice_added = FALSE; g_autoptr(FuDeviceLocker) locker = NULL; locker = fu_device_locker_new(device, error); if (locker == NULL) { g_prefix_error(error, "cannot open device: "); return FALSE; } if (!fu_corsair_device_poll_subdevice(device, &subdevice_added, error)) { return FALSE; } /* stop polling if a subdevice was added */ if (subdevice_added) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "subdevice added successfully"); return FALSE; } return TRUE; } static void fu_corsair_device_finalize(GObject *object) { FuCorsairDevice *self = FU_CORSAIR_DEVICE(object); g_free(self->subdevice_id); if (self->bp != NULL) g_object_unref(self->bp); G_OBJECT_CLASS(fu_corsair_device_parent_class)->finalize(object); } static void fu_corsair_device_class_init(FuCorsairDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); device_class->poll = fu_corsair_device_poll; device_class->probe = fu_corsair_device_probe; device_class->set_quirk_kv = fu_corsair_device_set_quirk_kv; device_class->setup = fu_corsair_device_setup; device_class->reload = fu_corsair_device_reload; device_class->attach = fu_corsair_device_attach; device_class->detach = fu_corsair_device_detach; device_class->write_firmware = fu_corsair_device_write_firmware; device_class->to_string = fu_corsair_device_to_string; device_class->set_progress = fu_corsair_device_set_progress; object_class->finalize = fu_corsair_device_finalize; } static void fu_corsair_device_init(FuCorsairDevice *device) { FuCorsairDevice *self = FU_CORSAIR_DEVICE(device); self->device_kind = FU_CORSAIR_DEVICE_KIND_MOUSE; self->vendor_interface = CORSAIR_DEFAULT_VENDOR_INTERFACE_ID; fu_device_register_private_flag(FU_DEVICE(device), FU_CORSAIR_DEVICE_FLAG_IS_SUBDEVICE); fu_device_register_private_flag(FU_DEVICE(device), FU_CORSAIR_DEVICE_FLAG_LEGACY_ATTACH); fu_device_register_private_flag(FU_DEVICE(device), FU_CORSAIR_DEVICE_FLAG_NO_VERSION_IN_BOOTLOADER); fu_device_set_remove_delay(FU_DEVICE(device), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_set_version_format(FU_DEVICE(device), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_add_flag(FU_DEVICE(device), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(device), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_add_protocol(FU_DEVICE(device), "com.corsair.bp"); } static FuCorsairDevice * fu_corsair_device_new(FuCorsairDevice *parent, FuCorsairBp *bp) { FuCorsairDevice *self = NULL; self = g_object_new(FU_TYPE_CORSAIR_DEVICE, "context", fu_device_get_context(FU_DEVICE(parent)), NULL); fu_device_incorporate(FU_DEVICE(self), FU_DEVICE(parent), FU_DEVICE_INCORPORATE_FLAG_ALL); self->bp = g_object_ref(bp); return self; } fwupd-2.0.10/plugins/corsair/fu-corsair-device.h000066400000000000000000000006651501337203100215240ustar00rootroot00000000000000/* * Copyright 2021 Andrii Dushko * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-corsair-bp.h" #include "fu-corsair-common.h" #define FU_TYPE_CORSAIR_DEVICE (fu_corsair_device_get_type()) G_DECLARE_FINAL_TYPE(FuCorsairDevice, fu_corsair_device, FU, CORSAIR_DEVICE, FuUsbDevice) struct _FuCorsairDeviceClass { FuUsbDeviceClass parent_class; }; fwupd-2.0.10/plugins/corsair/fu-corsair-plugin.c000066400000000000000000000021331501337203100215460ustar00rootroot00000000000000/* * Copyright 2022 Andrii Dushko * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-corsair-bp.h" #include "fu-corsair-device.h" #include "fu-corsair-plugin.h" struct _FuCorsairPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuCorsairPlugin, fu_corsair_plugin, FU_TYPE_PLUGIN) static void fu_corsair_plugin_init(FuCorsairPlugin *self) { } static void fu_corsair_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "CorsairDeviceKind"); fu_context_add_quirk_key(ctx, "CorsairVendorInterfaceId"); fu_context_add_quirk_key(ctx, "CorsairSubdeviceId"); fu_plugin_set_device_gtype_default(plugin, FU_TYPE_CORSAIR_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_CORSAIR_BP); /* coverage */ } static void fu_corsair_plugin_class_init(FuCorsairPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_corsair_plugin_constructed; } fwupd-2.0.10/plugins/corsair/fu-corsair-plugin.h000066400000000000000000000003571501337203100215610ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuCorsairPlugin, fu_corsair_plugin, FU, CORSAIR_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/corsair/fu-corsair.rs000066400000000000000000000003111501337203100204500ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString, FromString)] enum FuCorsairDeviceKind { Unknown, Mouse, Receiver, } fwupd-2.0.10/plugins/corsair/meson.build000066400000000000000000000012011501337203100201710ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginCorsair"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += join_paths(meson.current_source_dir(), 'corsair.quirk') plugin_builtins += static_library('fu_plugin_corsair', rustgen.process('fu-corsair.rs'), sources: [ 'fu-corsair-plugin.c', 'fu-corsair-common.c', 'fu-corsair-device.c', 'fu-corsair-bp.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files( 'tests/corsair-katar-pro-xt.json', 'tests/corsair-sabre-pro.json', 'tests/corsair-sabre-rgb-pro.json', ) fwupd-2.0.10/plugins/corsair/tests/000077500000000000000000000000001501337203100171775ustar00rootroot00000000000000fwupd-2.0.10/plugins/corsair/tests/corsair-katar-pro-xt.json000066400000000000000000000006361501337203100240700ustar00rootroot00000000000000{ "name": "Corsair KATAR PRO XT Gaming Mouse", "interactive": false, "steps": [ { "url": "7598356dbb3c40f16bc4cd314497ad0b983303e52311f95afc1d9aa8661a154d-corsair-bora-1.6.26.cab", "components": [ { "version": "1.6.26", "protocol": "com.corsair.bp", "guids": [ "b7b0728e-94e9-5ad5-999f-c6a359a9ddd4" ] } ] } ] } fwupd-2.0.10/plugins/corsair/tests/corsair-sabre-pro.json000066400000000000000000000006361501337203100234310ustar00rootroot00000000000000{ "name": "Corsair SABRE PRO Gaming Mouse", "interactive": false, "steps": [ { "url": "5adc54e39596b5b941339d247ac01f6a40377bc58e6b1b87c49d60c8a7509f4e-corsair-tongs-1.15.25.cab", "components": [ { "version": "1.15.25", "protocol": "com.corsair.bp", "guids": [ "9896f0c3-6682-5392-80f6-0ba462918645" ] } ] } ] } fwupd-2.0.10/plugins/corsair/tests/corsair-sabre-rgb-pro.json000066400000000000000000000006411501337203100241750ustar00rootroot00000000000000{ "name": "Corsair SABRE RGB PRO Gaming Mouse", "interactive": false, "steps": [ { "url": "a7540311d6b8da599cc24cd6225c34d4c27a4afb9bbbe73397fa23ccf71723f8-corsair-vise-1.15.30.cab", "components": [ { "version": "1.15.30", "protocol": "com.corsair.bp", "guids": [ "7eb05392-a5e0-529a-991b-1910a4bce8cf" ] } ] } ] } fwupd-2.0.10/plugins/cpu/000077500000000000000000000000001501337203100151625ustar00rootroot00000000000000fwupd-2.0.10/plugins/cpu/README.md000066400000000000000000000016171501337203100164460ustar00rootroot00000000000000--- title: Plugin: CPU Microcode --- ## Introduction This plugin reads the sysfs attributes associated with CPU microcode. It displays a read-only value of the CPU microcode version loaded onto the physical CPU at fwupd startup. ## GUID Generation These devices add extra instance IDs from the CPUID values, e.g. * `CPUID\PRO_0&FAM_06` (only-quirk) * `CPUID\PRO_0&FAM_06&MOD_0E` * `CPUID\PRO_0&FAM_06&MOD_0E&STP_3` ## Quirk Use This plugin uses the following plugin-specific quirks: ### CpuMitigationsRequired Mitigations required for this specific CPU. Valid values are: * `gds` Since: 1.9.4 * `sinkclose` Since: 2.0.2 ### CpuSinkcloseMicrocodeVersion Minimum version of microcode to mitigate the `sinkclose` vulnerability. Since: 2.0.2 ## External Interface Access This plugin requires no extra access. ## Version Considerations This plugin has been available since fwupd version `1.4.0`. fwupd-2.0.10/plugins/cpu/cpu.quirk000066400000000000000000000110641501337203100170300ustar00rootroot00000000000000# Intel Atom Bay Trail [Silvermont] [CPUID\PRO_0&FAM_06&MOD_37] PciBcrAddr = 0x0 # affected by Gather Data Sampling bug [CPUID\PRO_0&FAM_06&MOD_55&STP_3] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_55&STP_4] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_55&STP_6] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_55&STP_7] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_55&STP_A] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_55&STP_B] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_6A&STP_4] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_6A&STP_5] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_6A&STP_6] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_6C&STP_1] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_7E&STP_5] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_8C&STP_0] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_8C&STP_1] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_8C&STP_2] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_8D&STP_0] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_8D&STP_1] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_8E&STP_9] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_8E&STP_A] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_8E&STP_B] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_8E&STP_C] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_9E&STP_9] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_9E&STP_A] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_9E&STP_B] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_9E&STP_C] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_9E&STP_D] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_A5&STP_0] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_A5&STP_1] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_A5&STP_2] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_A5&STP_3] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_A5&STP_5] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_A6&STP_0] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_A6&STP_1] CpuMitigationsRequired = gds [CPUID\PRO_0&FAM_06&MOD_A7] CpuMitigationsRequired = gds #Affected by Sinkclose [CPUID\PRO_0&FAM_17&MOD_01&STP_2] CpuMitigationsRequired = sinkclose CpuSinkcloseMicrocodeVersion = 0x0800126f [CPUID\PRO_0&FAM_17&MOD_31&STP_0] CpuMitigationsRequired = sinkclose CpuSinkcloseMicrocodeVersion = 0x0830107c [CPUID\PRO_0&FAM_17&MOD_60&STP_1] CpuMitigationsRequired = sinkclose CpuSinkcloseMicrocodeVersion = 0x0860010d [CPUID\PRO_0&FAM_17&MOD_68&STP_1] CpuMitigationsRequired = sinkclose CpuSinkcloseMicrocodeVersion = 0x08608108 [CPUID\PRO_0&FAM_17&MOD_71&STP_0] CpuMitigationsRequired = sinkclose CpuSinkcloseMicrocodeVersion = 0x08701034 [CPUID\PRO_0&FAM_17&MOD_A0&STP_0] CpuMitigationsRequired = sinkclose CpuSinkcloseMicrocodeVersion = 0x08a0000a [CPUID\PRO_0&FAM_19&MOD_01&STP_1] CpuMitigationsRequired = sinkclose CpuSinkcloseMicrocodeVersion = 0x0a0011d5 [CPUID\PRO_0&FAM_19&MOD_01&STP_2] CpuMitigationsRequired = sinkclose CpuSinkcloseMicrocodeVersion = 0x0a001238 [CPUID\PRO_0&FAM_19&MOD_08&STP_2] CpuMitigationsRequired = sinkclose CpuSinkcloseMicrocodeVersion = 0x0a00820c [CPUID\PRO_0&FAM_19&MOD_11&STP_1] CpuMitigationsRequired = sinkclose CpuSinkcloseMicrocodeVersion = 0x0a101148 [CPUID\PRO_0&FAM_19&MOD_11&STP_2] CpuMitigationsRequired = sinkclose CpuSinkcloseMicrocodeVersion = 0x0a101248 [CPUID\PRO_0&FAM_19&MOD_18&STP_1] CpuMitigationsRequired = sinkclose CpuSinkcloseMicrocodeVersion = 0x0a108108 [CPUID\PRO_0&FAM_19&MOD_21&STP_0] CpuMitigationsRequired = sinkclose CpuSinkcloseMicrocodeVersion = 0x0a20102d [CPUID\PRO_0&FAM_19&MOD_21&STP_2] CpuMitigationsRequired = sinkclose CpuSinkcloseMicrocodeVersion = 0x0a201210 [CPUID\PRO_0&FAM_19&MOD_44&STP_1] CpuMitigationsRequired = sinkclose CpuSinkcloseMicrocodeVersion = 0x0a404107 [CPUID\PRO_0&FAM_19&MOD_50&STP_0] CpuMitigationsRequired = sinkclose CpuSinkcloseMicrocodeVersion = 0x0a500011 [CPUID\PRO_0&FAM_19&MOD_61&STP_2] CpuMitigationsRequired = sinkclose CpuSinkcloseMicrocodeVersion = 0x0a601209 [CPUID\PRO_0&FAM_19&MOD_74&STP_1] CpuMitigationsRequired = sinkclose CpuSinkcloseMicrocodeVersion = 0x0a704107 [CPUID\PRO_0&FAM_19&MOD_77&STP_2] CpuMitigationsRequired = sinkclose CpuSinkcloseMicrocodeVersion = 0x0a705206 [CPUID\PRO_0&FAM_19&MOD_78&STP_0] CpuMitigationsRequired = sinkclose CpuSinkcloseMicrocodeVersion = 0x0a708007 [CPUID\PRO_0&FAM_19&MOD_7C&STP_0] CpuMitigationsRequired = sinkclose CpuSinkcloseMicrocodeVersion = 0x0a70c005 [CPUID\PRO_0&FAM_19&MOD_A0&STP_2] CpuMitigationsRequired = sinkclose CpuSinkcloseMicrocodeVersion = 0x0aa00215 fwupd-2.0.10/plugins/cpu/fu-cpu-device.c000066400000000000000000000337611501337203100177740ustar00rootroot00000000000000/* * Copyright 2019 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #ifdef HAVE_UTSNAME_H #include #endif #include "fu-cpu-device.h" typedef enum { FU_CPU_DEVICE_FLAG_NONE = 0, FU_CPU_DEVICE_FLAG_SHSTK = 1 << 0, FU_CPU_DEVICE_FLAG_IBT = 1 << 1, FU_CPU_DEVICE_FLAG_TME = 1 << 2, FU_CPU_DEVICE_FLAG_SMAP = 1 << 3, } FuCpuDeviceFlag; struct _FuCpuDevice { FuDevice parent_instance; FuCpuDeviceFlag flags; }; G_DEFINE_TYPE(FuCpuDevice, fu_cpu_device, FU_TYPE_DEVICE) static gboolean fu_cpu_device_has_flag(FuCpuDevice *self, FuCpuDeviceFlag flag) { return (self->flags & flag) > 0; } static void fu_cpu_device_to_string(FuDevice *device, guint idt, GString *str) { FuCpuDevice *self = FU_CPU_DEVICE(device); fwupd_codec_string_append_bool(str, idt, "HasSHSTK", fu_cpu_device_has_flag(self, FU_CPU_DEVICE_FLAG_SHSTK)); fwupd_codec_string_append_bool(str, idt, "HasIBT", fu_cpu_device_has_flag(self, FU_CPU_DEVICE_FLAG_IBT)); fwupd_codec_string_append_bool(str, idt, "HasTME", fu_cpu_device_has_flag(self, FU_CPU_DEVICE_FLAG_TME)); fwupd_codec_string_append_bool(str, idt, "HasSMAP", fu_cpu_device_has_flag(self, FU_CPU_DEVICE_FLAG_SMAP)); } static const gchar * fu_cpu_device_convert_vendor(const gchar *vendor) { if (g_strcmp0(vendor, "GenuineIntel") == 0) return "Intel"; if (g_strcmp0(vendor, "AuthenticAMD") == 0 || g_strcmp0(vendor, "AMDisbetter!") == 0) return "Advanced Micro Devices, Inc."; if (g_strcmp0(vendor, "CentaurHauls") == 0) return "IDT"; if (g_strcmp0(vendor, "CyrixInstead") == 0) return "Cyrix"; if (g_strcmp0(vendor, "TransmetaCPU") == 0 || g_strcmp0(vendor, "GenuineTMx86") == 0) return "Transmeta"; if (g_strcmp0(vendor, "Geode by NSC") == 0) return "National Semiconductor"; if (g_strcmp0(vendor, "NexGenDriven") == 0) return "NexGen"; if (g_strcmp0(vendor, "RiseRiseRise") == 0) return "Rise"; if (g_strcmp0(vendor, "SiS SiS SiS ") == 0) return "SiS"; if (g_strcmp0(vendor, "UMC UMC UMC ") == 0) return "UMC"; if (g_strcmp0(vendor, "VIA VIA VIA ") == 0) return "VIA"; if (g_strcmp0(vendor, "Vortex86 SoC") == 0) return "Vortex"; if (g_strcmp0(vendor, " Shanghai ") == 0) return "Zhaoxin"; if (g_strcmp0(vendor, "HygonGenuine") == 0) return "Hygon"; if (g_strcmp0(vendor, "E2K MACHINE") == 0) return "MCST"; if (g_strcmp0(vendor, "bhyve bhyve ") == 0) return "bhyve"; if (g_strcmp0(vendor, " KVMKVMKVM ") == 0) return "KVM"; if (g_strcmp0(vendor, "TCGTCGTCGTCG") == 0) return "QEMU"; if (g_strcmp0(vendor, "Microsoft Hv") == 0) return "Microsoft"; if (g_strcmp0(vendor, " lrpepyh vr") == 0) return "Parallels"; if (g_strcmp0(vendor, "VMwareVMware") == 0) return "VMware"; if (g_strcmp0(vendor, "XenVMMXenVMM") == 0) return "Xen"; if (g_strcmp0(vendor, "ACRNACRNACRN") == 0) return "ACRN"; if (g_strcmp0(vendor, " QNXQVMBSQG ") == 0) return "QNX"; if (g_strcmp0(vendor, "VirtualApple") == 0) return "Apple"; return vendor; } static void fu_cpu_device_init(FuCpuDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_HOST_CPU); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_COMPUTER); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_HEX); fu_device_set_physical_id(FU_DEVICE(self), "cpu:0"); } static gboolean fu_cpu_device_add_instance_ids(FuDevice *device, GError **error) { guint32 eax = 0; guint32 family_id; guint32 family_id_ext; guint32 model_id; guint32 model_id_ext; guint32 processor_id; guint32 stepping_id; /* decode according to https://en.wikipedia.org/wiki/CPUID */ if (!fu_cpuid(0x1, &eax, NULL, NULL, NULL, error)) return FALSE; processor_id = (eax >> 12) & 0x3; model_id = (eax >> 4) & 0xf; family_id = (eax >> 8) & 0xf; model_id_ext = (eax >> 16) & 0xf; family_id_ext = (eax >> 20) & 0xff; stepping_id = eax & 0xf; /* use extended IDs where required */ if (family_id == 6 || family_id == 15) model_id |= model_id_ext << 4; if (family_id == 15) family_id += family_id_ext; /* add GUIDs */ fu_device_add_instance_u4(device, "PRO", processor_id); fu_device_add_instance_u8(device, "FAM", family_id); fu_device_add_instance_u8(device, "MOD", model_id); fu_device_add_instance_u4(device, "STP", stepping_id); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "CPUID", "PRO", "FAM", NULL); fu_device_build_instance_id(device, NULL, "CPUID", "PRO", "FAM", "MOD", NULL); fu_device_build_instance_id(device, NULL, "CPUID", "PRO", "FAM", "MOD", "STP", NULL); /* success */ return TRUE; } static gboolean fu_cpu_device_probe_manufacturer_id(FuDevice *device, GError **error) { guint32 ebx = 0; guint32 ecx = 0; guint32 edx = 0; gchar str[13] = {'\0'}; if (!fu_cpuid(0x0, NULL, &ebx, &ecx, &edx, error)) return FALSE; if (!fu_memcpy_safe((guint8 *)str, sizeof(str), 0x0, /* dst */ (const guint8 *)&ebx, sizeof(ebx), 0x0, /* src */ sizeof(guint32), error)) return FALSE; if (!fu_memcpy_safe((guint8 *)str, sizeof(str), 0x4, /* dst */ (const guint8 *)&edx, sizeof(edx), 0x0, /* src */ sizeof(guint32), error)) return FALSE; if (!fu_memcpy_safe((guint8 *)str, sizeof(str), 0x8, /* dst */ (const guint8 *)&ecx, sizeof(ecx), 0x0, /* src */ sizeof(guint32), error)) return FALSE; fu_device_set_vendor(device, fu_cpu_device_convert_vendor(str)); return TRUE; } static gboolean fu_cpu_device_probe_model(FuDevice *device, GError **error) { guint32 eax = 0; guint32 ebx = 0; guint32 ecx = 0; guint32 edx = 0; gchar str[49] = {'\0'}; for (guint32 i = 0; i < 3; i++) { if (!fu_cpuid(0x80000002 + i, &eax, &ebx, &ecx, &edx, error)) return FALSE; if (!fu_memcpy_safe((guint8 *)str, sizeof(str), (16 * i) + 0x0, /* dst */ (const guint8 *)&eax, sizeof(eax), 0x0, /* src */ sizeof(guint32), error)) return FALSE; if (!fu_memcpy_safe((guint8 *)str, sizeof(str), (16 * i) + 0x4, /* dst */ (const guint8 *)&ebx, sizeof(ebx), 0x0, /* src */ sizeof(guint32), error)) return FALSE; if (!fu_memcpy_safe((guint8 *)str, sizeof(str), (16 * i) + 0x8, /* dst */ (const guint8 *)&ecx, sizeof(ecx), 0x0, /* src */ sizeof(guint32), error)) return FALSE; if (!fu_memcpy_safe((guint8 *)str, sizeof(str), (16 * i) + 0xc, /* dst */ (const guint8 *)&edx, sizeof(edx), 0x0, /* src */ sizeof(guint32), error)) return FALSE; } fu_device_set_name(device, str); return TRUE; } static gboolean fu_cpu_device_probe_extended_features(FuDevice *device, GError **error) { FuCpuDevice *self = FU_CPU_DEVICE(device); guint32 ebx = 0; guint32 ecx = 0; guint32 edx = 0; if (!fu_cpuid(0x7, NULL, &ebx, &ecx, &edx, error)) return FALSE; if ((ebx >> 20) & 0x1) self->flags |= FU_CPU_DEVICE_FLAG_SMAP; if ((ecx >> 7) & 0x1) self->flags |= FU_CPU_DEVICE_FLAG_SHSTK; if (fu_cpu_get_vendor() == FU_CPU_VENDOR_INTEL) { if ((ecx >> 13) & 0x1) self->flags |= FU_CPU_DEVICE_FLAG_TME; if ((edx >> 20) & 0x1) self->flags |= FU_CPU_DEVICE_FLAG_IBT; } return TRUE; } static gboolean fu_cpu_device_probe(FuDevice *device, GError **error) { if (!fu_cpu_device_probe_manufacturer_id(device, error)) return FALSE; if (!fu_cpu_device_probe_model(device, error)) return FALSE; if (!fu_cpu_device_probe_extended_features(device, error)) return FALSE; if (!fu_cpu_device_add_instance_ids(device, error)) return FALSE; return TRUE; } static gboolean fu_cpu_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { if (g_strcmp0(key, "PciBcrAddr") == 0) { guint64 tmp = 0; if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_device_set_metadata_integer(device, "PciBcrAddr", tmp); return TRUE; } if (g_strcmp0(key, "CpuMitigationsRequired") == 0) { fu_device_set_metadata(device, "CpuMitigationsRequired", value); return TRUE; } if (g_strcmp0(key, "CpuSinkcloseMicrocodeVersion") == 0) { guint64 tmp = 0; if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_16, error)) return FALSE; fu_device_set_metadata_integer(device, FU_DEVICE_METADATA_CPU_SINKCLOSE_MICROCODE_VER, tmp); return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no supported"); return FALSE; } static void fu_cpu_device_add_security_attrs_cet_enabled(FuCpuDevice *self, FuSecurityAttrs *attrs) { g_autoptr(FwupdSecurityAttr) attr = NULL; /* create attr */ attr = fu_device_security_attr_new(FU_DEVICE(self), FWUPD_SECURITY_ATTR_ID_CET_ENABLED); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_SUPPORTED); fu_security_attrs_append(attrs, attr); switch (fu_cpu_get_vendor()) { case FU_CPU_VENDOR_INTEL: if (fu_cpu_device_has_flag(self, FU_CPU_DEVICE_FLAG_SHSTK) && fu_cpu_device_has_flag(self, FU_CPU_DEVICE_FLAG_IBT)) { fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); return; } break; case FU_CPU_VENDOR_AMD: if (fu_cpu_device_has_flag(self, FU_CPU_DEVICE_FLAG_SHSTK)) { fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); return; } break; default: break; } fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_SUPPORTED); } static void fu_cpu_device_add_security_attrs_cet_active(FuCpuDevice *self, FuSecurityAttrs *attrs) { gint exit_status = 0xff; g_autofree gchar *toolfn = NULL; g_autofree gchar *dir = NULL; g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(FwupdSecurityAttr) cet_plat_attr = NULL; g_autoptr(GError) error_local = NULL; /* check for CET */ cet_plat_attr = fu_security_attrs_get_by_appstream_id(attrs, FWUPD_SECURITY_ATTR_ID_CET_ENABLED, NULL); if (cet_plat_attr == NULL) return; if (!fwupd_security_attr_has_flag(cet_plat_attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)) return; /* create attr */ attr = fu_device_security_attr_new(FU_DEVICE(self), FWUPD_SECURITY_ATTR_ID_CET_ACTIVE); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_SUPPORTED); fu_security_attrs_append(attrs, attr); /* check that userspace has been compiled for CET support */ dir = fu_path_from_kind(FU_PATH_KIND_LIBEXECDIR_PKG); toolfn = g_build_filename(dir, "fwupd-detect-cet", NULL); if (!g_spawn_command_line_sync(toolfn, NULL, NULL, &exit_status, &error_local)) { g_warning("failed to test CET: %s", error_local->message); return; } if (!g_spawn_check_wait_status(exit_status, &error_local)) { g_debug("CET does not function, not supported: %s", error_local->message); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_SUPPORTED); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_cpu_device_add_security_attrs_intel_tme(FuCpuDevice *self, FuSecurityAttrs *attrs) { g_autoptr(FwupdSecurityAttr) attr = NULL; /* create attr */ attr = fu_device_security_attr_new(FU_DEVICE(self), FWUPD_SECURITY_ATTR_ID_ENCRYPTED_RAM); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_ENABLED); fu_security_attrs_append(attrs, attr); /* check for TME */ if (!fu_cpu_device_has_flag(self, FU_CPU_DEVICE_FLAG_TME)) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_SUPPORTED); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_cpu_device_add_security_attrs_smap(FuCpuDevice *self, FuSecurityAttrs *attrs) { g_autoptr(FwupdSecurityAttr) attr = NULL; /* create attr */ attr = fu_device_security_attr_new(FU_DEVICE(self), FWUPD_SECURITY_ATTR_ID_SMAP); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_ENABLED); fu_security_attrs_append(attrs, attr); /* check for SMEP and SMAP */ if (!fu_cpu_device_has_flag(self, FU_CPU_DEVICE_FLAG_SMAP)) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_SUPPORTED); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } #ifdef HAVE_UTSNAME_H static void fu_cpu_device_add_x86_64_security_attrs(FuDevice *device, FuSecurityAttrs *attrs) { FuCpuDevice *self = FU_CPU_DEVICE(device); /* only Intel */ if (fu_cpu_get_vendor() == FU_CPU_VENDOR_INTEL) fu_cpu_device_add_security_attrs_intel_tme(self, attrs); fu_cpu_device_add_security_attrs_cet_enabled(self, attrs); fu_cpu_device_add_security_attrs_cet_active(self, attrs); fu_cpu_device_add_security_attrs_smap(self, attrs); } #endif static void fu_cpu_device_add_security_attrs(FuDevice *device, FuSecurityAttrs *attrs) { #ifdef HAVE_UTSNAME_H struct utsname name_tmp; memset(&name_tmp, 0, sizeof(struct utsname)); if (uname(&name_tmp) < 0) { g_warning("failed to read CPU architecture"); return; } if (g_strcmp0(name_tmp.machine, "x86_64") == 0) fu_cpu_device_add_x86_64_security_attrs(device, attrs); #endif } static gchar * fu_cpu_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_device_get_version_format(device)); } static void fu_cpu_device_class_init(FuCpuDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_cpu_device_to_string; device_class->probe = fu_cpu_device_probe; device_class->set_quirk_kv = fu_cpu_device_set_quirk_kv; device_class->add_security_attrs = fu_cpu_device_add_security_attrs; device_class->convert_version = fu_cpu_device_convert_version; } FuCpuDevice * fu_cpu_device_new(FuContext *ctx) { FuCpuDevice *device = NULL; device = g_object_new(FU_TYPE_CPU_DEVICE, "context", ctx, NULL); return device; } fwupd-2.0.10/plugins/cpu/fu-cpu-device.h000066400000000000000000000005251501337203100177710ustar00rootroot00000000000000/* * Copyright 2020 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_CPU_DEVICE (fu_cpu_device_get_type()) G_DECLARE_FINAL_TYPE(FuCpuDevice, fu_cpu_device, FU, CPU_DEVICE, FuDevice) FuCpuDevice * fu_cpu_device_new(FuContext *ctx); fwupd-2.0.10/plugins/cpu/fu-cpu-helper-cet-common.S000066400000000000000000000007001501337203100220160ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * Copyright 2024 H.J. Lu * * SPDX-License-Identifier: LGPL-2.1-or-later */ .text .globl fu_cpu_helper_cet_testfn1 .type fu_cpu_helper_cet_testfn1, %function fu_cpu_helper_cet_testfn1: .cfi_startproc movq (%rsp), %rax addq $8, %rsp jmp *%rax .cfi_endproc .size fu_cpu_helper_cet_testfn1, .-fu_cpu_helper_cet_testfn1 .section .note.GNU-stack,"",%progbits fwupd-2.0.10/plugins/cpu/fu-cpu-helper-cet-common.h000066400000000000000000000003241501337203100220450ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * Copyright 2020 H.J. Lu * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once void fu_cpu_helper_cet_testfn1(void); fwupd-2.0.10/plugins/cpu/fu-cpu-helper-cet.c000066400000000000000000000014301501337203100205510ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * Copyright 2020 H.J. Lu * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fu-cpu-helper-cet-common.h" #ifdef HAVE_SIGACTION static __attribute__((noreturn)) void fu_cpu_helper_cet_segfault_sigaction(int signal, siginfo_t *si, void *arg) { /* CET did exactly as it should to protect the system */ exit(0); } #endif int main(int argc, char *argv[]) { #ifdef HAVE_SIGACTION struct sigaction sa = {0}; sigemptyset(&sa.sa_mask); sa.sa_sigaction = fu_cpu_helper_cet_segfault_sigaction; sa.sa_flags = SA_SIGINFO; sigaction(SIGSEGV, &sa, NULL); #endif fu_cpu_helper_cet_testfn1(); /* this means CET did not work */ return 1; } fwupd-2.0.10/plugins/cpu/fu-cpu-plugin.c000066400000000000000000000031151501337203100200210ustar00rootroot00000000000000/* * Copyright 2020 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-cpu-device.h" #include "fu-cpu-plugin.h" struct _FuCpuPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuCpuPlugin, fu_cpu_plugin, FU_TYPE_PLUGIN) static gboolean fu_cpu_plugin_coldplug(FuPlugin *plugin, FuProgress *progress, GError **error) { FuContext *ctx = fu_plugin_get_context(plugin); g_autoptr(FuCpuDevice) dev = fu_cpu_device_new(ctx); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 99, "probe"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "setup"); if (!fu_device_probe(FU_DEVICE(dev), error)) return FALSE; fu_progress_step_done(progress); if (!fu_device_setup(FU_DEVICE(dev), error)) return FALSE; fu_progress_step_done(progress); fu_plugin_cache_add(plugin, "cpu", dev); fu_plugin_device_add(plugin, FU_DEVICE(dev)); return TRUE; } static void fu_cpu_plugin_init(FuCpuPlugin *self) { } static void fu_cpu_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "CpuMitigationsRequired"); fu_context_add_quirk_key(ctx, "CpuSinkcloseMicrocodeVersion"); fu_plugin_add_rule(plugin, FU_PLUGIN_RULE_RUN_BEFORE, "msr"); } static void fu_cpu_plugin_class_init(FuCpuPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_cpu_plugin_constructed; plugin_class->coldplug = fu_cpu_plugin_coldplug; } fwupd-2.0.10/plugins/cpu/fu-cpu-plugin.h000066400000000000000000000003431501337203100200260ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuCpuPlugin, fu_cpu_plugin, FU, CPU_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/cpu/meson.build000066400000000000000000000024651501337203100173330ustar00rootroot00000000000000if hsi cargs = ['-DG_LOG_DOMAIN="FuPluginCpu"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('cpu.quirk') plugin_builtins += static_library('fu_plugin_cpu', sources: [ 'fu-cpu-plugin.c', 'fu-cpu-device.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) code = ''' #if !__has_attribute (__noclone__) #error symver attribute not supported #endif static void __attribute__((noinline,noclone)) f(void) {} ''' # verify the compiler knows what to do if host_cpu == 'x86_64' and cc.has_argument('-fcf-protection') build_fwupdcethelper = cc.compiles(code, name: '__attribute__((noinline,noclone))', ) else build_fwupdcethelper = false endif if build_fwupdcethelper libfwupdcethelper = static_library('fwupdcethelper', sources: [ 'fu-cpu-helper-cet-common.S', ], include_directories: [ root_incdir, ], c_args: ['-fcf-protection=none'], install: false, ) executable( 'fwupd-detect-cet', sources: [ 'fu-cpu-helper-cet.c', ], include_directories: [ root_incdir, ], link_with: [ libfwupdcethelper, ], c_args: ['-fcf-protection=full'], install: true, install_dir: join_paths(libexecdir, 'fwupd') ) endif endif fwupd-2.0.10/plugins/cros-ec/000077500000000000000000000000001501337203100157265ustar00rootroot00000000000000fwupd-2.0.10/plugins/cros-ec/README.md000066400000000000000000000040431501337203100172060ustar00rootroot00000000000000--- title: Plugin: Chrome OS EC --- ## Introduction This plugin provides support for the firmware updates for Chrome OS EC project based devices. Initially, it supports the USB endpoint updater, but lays the groundwork for future updaters which use other update methods other than the USB endpoint. This is based on the chromeos ec project's [usb_updater2 application](https://chromium.googlesource.com/chromiumos/platform/ec/+/master/extra/usb_updater/usb_updater2.c). Information about the USB update protocol is [available here](https://chromium.googlesource.com/chromiumos/platform/ec/+/master/docs/usb_updater.md). ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in the Google [fmap file format](https://www.chromium.org/chromium-os/firmware-porting-guide/fmap). This plugin supports the following protocol ID: * `com.google.usb.crosec` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_18D1&PID_501A` It also adds one more instance ID that includes the board name, parsed from the device version string, e.g. * `USB\VID_18D1&PID_501A&BOARDNAME_gingerbread` ## Update Behavior The device usually presents in runtime mode, but on detach re-enumerates with the same USB PID in an unlocked mode. On attach the device again re-enumerates back to the runtime locked mode. For this reason the `REPLUG_MATCH_GUID` internal device flag is used so that the bootloader and runtime modes are treated as the same device. ## Vendor ID Security The vendor ID is set from the USB vendor, which is set to various different values depending on the model and device mode. The list of USB VIDs used is: * `USB:0x18D1` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.5.0`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Benson Leung: @bleungatchromium fwupd-2.0.10/plugins/cros-ec/cros-ec.quirk000066400000000000000000000010421501337203100203330ustar00rootroot00000000000000# Servo Micro [USB\VID_18D1&PID_501A] Plugin = cros_ec Summary = Servo Micro (aka "uServo") Debug Board # Quiche [USB\VID_18D1&PID_5048] Plugin = cros_ec Summary = Quiche Reference Board # Baklava (D501) [USB\VID_0502&PID_1195] Plugin = cros_ec Summary = D501 Device (Google Quiche derivative) # Gingerbread [USB\VID_18D1&PID_5049] Plugin = cros_ec Summary = Gingerbread Reference Board # Belkin [USB\VID_050D&PID_003F] Plugin = cros_ec Summary = Belkin Reference Board # Prism [USB\VID_18D1&PID_5022] Plugin = cros_ec Summary = Prism Board fwupd-2.0.10/plugins/cros-ec/data/000077500000000000000000000000001501337203100166375ustar00rootroot00000000000000fwupd-2.0.10/plugins/cros-ec/data/lsusb-servo-micro.txt000066400000000000000000000205531501337203100230000ustar00rootroot00000000000000 Bus 003 Device 006: ID 18d1:501a Google Inc. Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 0 (Defined at Interface level) bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 64 idVendor 0x18d1 Google Inc. idProduct 0x501a bcdDevice 1.00 iManufacturer 1 Google Inc. iProduct 2 Servo Micro iSerial 3 CMO653-00166-040491U00771 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 170 bNumInterfaces 7 bConfigurationValue 1 iConfiguration 4 servo_micro_v2.4.17-df61092c3 bmAttributes 0x80 (Bus Powered) MaxPower 100mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 2 bInterfaceClass 255 Vendor Specific Class bInterfaceSubClass 80 bInterfaceProtocol 1 iInterface 6 UART3 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 2 Transfer Type Bulk Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 10 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x01 EP 1 OUT bmAttributes 2 Transfer Type Bulk Synch Type None Usage Type Data wMaxPacketSize 0x0020 1x 32 bytes bInterval 0 Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 1 bAlternateSetting 0 bNumEndpoints 2 bInterfaceClass 255 Vendor Specific Class bInterfaceSubClass 83 bInterfaceProtocol 255 iInterface 10 Firmware update Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x82 EP 2 IN bmAttributes 2 Transfer Type Bulk Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 10 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x02 EP 2 OUT bmAttributes 2 Transfer Type Bulk Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 0 Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 2 bAlternateSetting 0 bNumEndpoints 2 bInterfaceClass 255 Vendor Specific Class bInterfaceSubClass 81 bInterfaceProtocol 1 iInterface 0 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x83 EP 3 IN bmAttributes 2 Transfer Type Bulk Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 10 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x03 EP 3 OUT bmAttributes 2 Transfer Type Bulk Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 0 Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 3 bAlternateSetting 0 bNumEndpoints 2 bInterfaceClass 255 Vendor Specific Class bInterfaceSubClass 80 bInterfaceProtocol 1 iInterface 7 Servo Shell Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x84 EP 4 IN bmAttributes 2 Transfer Type Bulk Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 10 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x04 EP 4 OUT bmAttributes 2 Transfer Type Bulk Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 0 Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 4 bAlternateSetting 0 bNumEndpoints 2 bInterfaceClass 255 Vendor Specific Class bInterfaceSubClass 82 bInterfaceProtocol 1 iInterface 5 I2C Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x85 EP 5 IN bmAttributes 2 Transfer Type Bulk Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 10 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x05 EP 5 OUT bmAttributes 2 Transfer Type Bulk Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 0 Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 5 bAlternateSetting 0 bNumEndpoints 2 bInterfaceClass 255 Vendor Specific Class bInterfaceSubClass 80 bInterfaceProtocol 1 iInterface 8 CPU Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x86 EP 6 IN bmAttributes 2 Transfer Type Bulk Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 10 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x06 EP 6 OUT bmAttributes 2 Transfer Type Bulk Synch Type None Usage Type Data wMaxPacketSize 0x0020 1x 32 bytes bInterval 0 Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 6 bAlternateSetting 0 bNumEndpoints 2 bInterfaceClass 255 Vendor Specific Class bInterfaceSubClass 80 bInterfaceProtocol 1 iInterface 9 EC Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x87 EP 7 IN bmAttributes 2 Transfer Type Bulk Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 10 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x07 EP 7 OUT bmAttributes 2 Transfer Type Bulk Synch Type None Usage Type Data wMaxPacketSize 0x0020 1x 32 bytes bInterval 0 Device Status: 0x0000 (Bus Powered) fwupd-2.0.10/plugins/cros-ec/fu-cros-ec-common.c000066400000000000000000000042711501337203100213270ustar00rootroot00000000000000/* * Copyright 2020 Benson Leung * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-cros-ec-common.h" #include "fu-cros-ec-struct.h" void fu_cros_ec_version_free(FuCrosEcVersion *version) { g_free(version->boardname); g_free(version->triplet); g_free(version->sha1); g_free(version); } FuCrosEcVersion * fu_cros_ec_version_parse(const gchar *version_raw, GError **error) { gchar *ver = NULL; g_autofree gchar *board = g_strdup(version_raw); g_auto(GStrv) marker_split = NULL; g_auto(GStrv) triplet_split = NULL; g_autoptr(FuCrosEcVersion) version = g_new0(FuCrosEcVersion, 1); if (version_raw == NULL || strlen(version_raw) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "no version string to parse"); return NULL; } /* sample version string: cheese_v1.1.1755-4da9520 */ ver = g_strrstr(board, "_v"); if (ver == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "version marker not found"); return NULL; } *ver = '\0'; ver += 2; marker_split = g_strsplit_set(ver, "-+", 2); if (g_strv_length(marker_split) < 2) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "hash marker not found: %s", ver); return NULL; } triplet_split = g_strsplit_set(marker_split[0], ".", 3); if (g_strv_length(triplet_split) < 3) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "improper version triplet: %s", marker_split[0]); return NULL; } version->triplet = fu_strsafe(marker_split[0], FU_STRUCT_CROS_EC_FIRST_RESPONSE_PDU_SIZE_VERSION); version->boardname = fu_strsafe(board, FU_STRUCT_CROS_EC_FIRST_RESPONSE_PDU_SIZE_VERSION); if (version->boardname == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "empty board name"); return NULL; } version->sha1 = fu_strsafe(marker_split[1], FU_STRUCT_CROS_EC_FIRST_RESPONSE_PDU_SIZE_VERSION); if (version->sha1 == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "empty SHA"); return NULL; } version->dirty = (g_strrstr(ver, "+") != NULL); return g_steal_pointer(&version); } fwupd-2.0.10/plugins/cros-ec/fu-cros-ec-common.h000066400000000000000000000007251501337203100213340ustar00rootroot00000000000000/* * Copyright 2020 Benson Leung * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include typedef struct { gchar *boardname; gchar *triplet; gchar *sha1; gboolean dirty; } FuCrosEcVersion; FuCrosEcVersion * fu_cros_ec_version_parse(const gchar *version_raw, GError **error); void fu_cros_ec_version_free(FuCrosEcVersion *version); G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuCrosEcVersion, fu_cros_ec_version_free) fwupd-2.0.10/plugins/cros-ec/fu-cros-ec-firmware.c000066400000000000000000000122371501337203100216540ustar00rootroot00000000000000/* * Copyright 2020 Benson Leung * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-cros-ec-common.h" #include "fu-cros-ec-firmware.h" #define MAXSECTIONS 2 struct _FuCrosEcFirmware { FuFmapFirmware parent_instance; GPtrArray *sections; }; G_DEFINE_TYPE(FuCrosEcFirmware, fu_cros_ec_firmware, FU_TYPE_FMAP_FIRMWARE) gboolean fu_cros_ec_firmware_pick_sections(FuCrosEcFirmware *self, guint32 writeable_offset, GError **error) { gboolean found = FALSE; for (gsize i = 0; i < self->sections->len; i++) { FuCrosEcFirmwareSection *section = g_ptr_array_index(self->sections, i); guint32 offset = section->offset; if (offset != writeable_offset) continue; section->ustatus = FU_CROS_EC_FIRMWARE_UPGRADE_STATUS_NEEDED; found = TRUE; } if (!found) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no writeable section found with offset: 0x%x", writeable_offset); return FALSE; } /* success */ return TRUE; } GPtrArray * fu_cros_ec_firmware_get_needed_sections(FuCrosEcFirmware *self, GError **error) { g_autoptr(GPtrArray) needed_sections = g_ptr_array_new(); for (guint i = 0; i < self->sections->len; i++) { FuCrosEcFirmwareSection *section = g_ptr_array_index(self->sections, i); if (section->ustatus != FU_CROS_EC_FIRMWARE_UPGRADE_STATUS_NEEDED) continue; g_ptr_array_add(needed_sections, section); } if (needed_sections->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no needed sections"); return NULL; } /* success */ return g_steal_pointer(&needed_sections); } gboolean fu_cros_ec_firmware_ensure_version(FuCrosEcFirmware *self, GError **error) { for (gsize i = 0; i < self->sections->len; i++) { gboolean rw = FALSE; FuCrosEcFirmwareSection *section = g_ptr_array_index(self->sections, i); const gchar *fmap_name; const gchar *fmap_fwid_name; g_autoptr(FuCrosEcVersion) version = NULL; g_autoptr(FuFirmware) img = NULL; g_autoptr(FuFirmware) fwid_img = NULL; g_autoptr(GBytes) payload_bytes = NULL; g_autoptr(GBytes) fwid_bytes = NULL; if (g_strcmp0(section->name, "RO") == 0) { fmap_name = "EC_RO"; fmap_fwid_name = "RO_FRID"; } else if (g_strcmp0(section->name, "RW") == 0) { rw = TRUE; fmap_name = "EC_RW"; fmap_fwid_name = "RW_FWID"; } else { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "incorrect section name"); return FALSE; } img = fu_firmware_get_image_by_id(FU_FIRMWARE(self), fmap_name, error); if (img == NULL) { g_prefix_error(error, "%s image not found: ", fmap_name); return FALSE; } fwid_img = fu_firmware_get_image_by_id(FU_FIRMWARE(self), fmap_fwid_name, error); if (fwid_img == NULL) { g_prefix_error(error, "%s image not found: ", fmap_fwid_name); return FALSE; } fwid_bytes = fu_firmware_write(fwid_img, error); if (fwid_bytes == NULL) { g_prefix_error(error, "unable to get bytes from %s: ", fmap_fwid_name); return FALSE; } if (!fu_memcpy_safe((guint8 *)section->raw_version, FU_FMAP_FIRMWARE_STRLEN, 0x0, g_bytes_get_data(fwid_bytes, NULL), g_bytes_get_size(fwid_bytes), 0x0, g_bytes_get_size(fwid_bytes), error)) return FALSE; payload_bytes = fu_firmware_write(img, error); if (payload_bytes == NULL) { g_prefix_error(error, "unable to get bytes from %s: ", fmap_name); return FALSE; } section->offset = fu_firmware_get_addr(img); section->size = g_bytes_get_size(payload_bytes); fu_firmware_set_version(img, section->raw_version); section->image_idx = fu_firmware_get_idx(img); version = fu_cros_ec_version_parse(section->raw_version, error); if (version == NULL) { g_prefix_error(error, "failed parsing firmware's version: %32s: ", section->raw_version); return FALSE; } if (rw) { g_autoptr(FuCrosEcVersion) version_rw = NULL; version_rw = fu_cros_ec_version_parse(section->raw_version, error); if (version_rw == NULL) { g_prefix_error(error, "failed parsing firmware's version: %32s: ", section->raw_version); return FALSE; } fu_firmware_set_version(FU_FIRMWARE(self), version_rw->triplet); } } /* success */ return TRUE; } static void fu_cros_ec_firmware_init(FuCrosEcFirmware *self) { FuCrosEcFirmwareSection *section; self->sections = g_ptr_array_new_with_free_func(g_free); section = g_new0(FuCrosEcFirmwareSection, 1); section->name = "RO"; g_ptr_array_add(self->sections, section); section = g_new0(FuCrosEcFirmwareSection, 1); section->name = "RW"; g_ptr_array_add(self->sections, section); } static void fu_cros_ec_firmware_finalize(GObject *object) { FuCrosEcFirmware *self = FU_CROS_EC_FIRMWARE(object); g_ptr_array_free(self->sections, TRUE); G_OBJECT_CLASS(fu_cros_ec_firmware_parent_class)->finalize(object); } static void fu_cros_ec_firmware_class_init(FuCrosEcFirmwareClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_cros_ec_firmware_finalize; } FuFirmware * fu_cros_ec_firmware_new(void) { return g_object_new(FU_TYPE_CROS_EC_FIRMWARE, NULL); } fwupd-2.0.10/plugins/cros-ec/fu-cros-ec-firmware.h000066400000000000000000000017201501337203100216540ustar00rootroot00000000000000/* * Copyright 2020 Benson Leung * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-cros-ec-common.h" #include "fu-cros-ec-struct.h" #define FU_TYPE_CROS_EC_FIRMWARE (fu_cros_ec_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuCrosEcFirmware, fu_cros_ec_firmware, FU, CROS_EC_FIRMWARE, FuFmapFirmware) typedef struct { const gchar *name; guint32 offset; gsize size; FuCrosEcFirmwareUpgradeStatus ustatus; gchar raw_version[FU_FMAP_FIRMWARE_STRLEN]; FuCrosEcVersion version; gint32 rollback; guint32 key_version; guint64 image_idx; } FuCrosEcFirmwareSection; gboolean fu_cros_ec_firmware_ensure_version(FuCrosEcFirmware *self, GError **error); gboolean fu_cros_ec_firmware_pick_sections(FuCrosEcFirmware *self, guint32 writeable_offset, GError **error); GPtrArray * fu_cros_ec_firmware_get_needed_sections(FuCrosEcFirmware *self, GError **error); FuFirmware * fu_cros_ec_firmware_new(void); fwupd-2.0.10/plugins/cros-ec/fu-cros-ec-plugin.c000066400000000000000000000015041501337203100213310ustar00rootroot00000000000000/* * Copyright 2020 Benson Leung * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-cros-ec-firmware.h" #include "fu-cros-ec-plugin.h" #include "fu-cros-ec-usb-device.h" struct _FuCrosEcPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuCrosEcPlugin, fu_cros_ec_plugin, FU_TYPE_PLUGIN) static void fu_cros_ec_plugin_init(FuCrosEcPlugin *self) { } static void fu_cros_ec_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_CROS_EC_USB_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_CROS_EC_FIRMWARE); } static void fu_cros_ec_plugin_class_init(FuCrosEcPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_cros_ec_plugin_constructed; } fwupd-2.0.10/plugins/cros-ec/fu-cros-ec-plugin.h000066400000000000000000000003561501337203100213420ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuCrosEcPlugin, fu_cros_ec_plugin, FU, CROS_EC_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/cros-ec/fu-cros-ec-usb-device.c000066400000000000000000001015621501337203100220660ustar00rootroot00000000000000/* * Copyright 2020 Benson Leung * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-cros-ec-common.h" #include "fu-cros-ec-firmware.h" #include "fu-cros-ec-struct.h" #include "fu-cros-ec-usb-device.h" #define FU_CROS_EC_USB_SUBCLASS_GOOGLE_UPDATE 0x53 #define FU_CROS_EC_USB_PROTOCOL_GOOGLE_UPDATE 0xff #define FU_CROS_EC_SETUP_RETRY_CNT 5 #define FU_CROS_EC_MAX_BLOCK_XFER_RETRIES 10 #define FU_CROS_EC_FLUSH_TIMEOUT_MS 10 #define FU_CROS_EC_BULK_SEND_TIMEOUT 2000 /* ms */ #define FU_CROS_EC_BULK_RECV_TIMEOUT 5000 /* ms */ #define FU_CROS_EC_USB_DEVICE_REMOVE_DELAY 20000 #define FU_CROS_EC_REQUEST_UPDATE_DONE 0xB007AB1E #define FU_CROS_EC_REQUEST_UPDATE_EXTRA_CMD 0xB007AB1F struct _FuCrosEcUsbDevice { FuUsbDevice parent_instance; guint8 iface_idx; /* bInterfaceNumber */ guint8 ep_num; /* bEndpointAddress */ guint16 chunk_len; /* wMaxPacketSize */ gchar *raw_version; guint32 maximum_pdu_size; guint32 flash_protection; guint32 writeable_offset; guint16 protocol_version; gchar configuration[FU_STRUCT_CROS_EC_FIRST_RESPONSE_PDU_SIZE_VERSION]; gboolean in_bootloader; }; G_DEFINE_TYPE(FuCrosEcUsbDevice, fu_cros_ec_usb_device, FU_TYPE_USB_DEVICE) typedef struct { FuChunk *block; FuProgress *progress; } FuCrosEcUsbBlockHelper; #define FU_CROS_EC_USB_DEVICE_FLAG_RO_WRITTEN "ro-written" #define FU_CROS_EC_USB_DEVICE_FLAG_RW_WRITTEN "rw-written" #define FU_CROS_EC_USB_DEVICE_FLAG_REBOOTING_TO_RO "rebooting-to-ro" #define FU_CROS_EC_USB_DEVICE_FLAG_SPECIAL "special" static gboolean fu_cros_ec_usb_device_get_configuration(FuCrosEcUsbDevice *self, GError **error) { guint8 index; g_autofree gchar *configuration = NULL; index = fu_usb_device_get_configuration_index(FU_USB_DEVICE(self), error); if (index == 0x0) return FALSE; configuration = fu_usb_device_get_string_descriptor(FU_USB_DEVICE(self), index, error); if (configuration == NULL) return FALSE; g_debug("%s(%s): raw configuration read: %s", fu_device_get_id(FU_DEVICE(self)), fu_device_get_name(FU_DEVICE(self)), configuration); if (g_strlcpy(self->configuration, configuration, FU_STRUCT_CROS_EC_FIRST_RESPONSE_PDU_SIZE_VERSION) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "empty iConfiguration"); return FALSE; } /* success */ return TRUE; } static gboolean fu_cros_ec_usb_device_find_interface(FuUsbDevice *device, GError **error) { FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device); g_autoptr(GPtrArray) intfs = NULL; /* based on usb_updater2's find_interfacei() and find_endpoint() */ intfs = fu_usb_device_get_interfaces(device, error); if (intfs == NULL) return FALSE; for (guint i = 0; i < intfs->len; i++) { FuUsbInterface *intf = g_ptr_array_index(intfs, i); if (fu_usb_interface_get_class(intf) == 255 && fu_usb_interface_get_subclass(intf) == FU_CROS_EC_USB_SUBCLASS_GOOGLE_UPDATE && fu_usb_interface_get_protocol(intf) == FU_CROS_EC_USB_PROTOCOL_GOOGLE_UPDATE) { FuUsbEndpoint *ep; g_autoptr(GPtrArray) endpoints = fu_usb_interface_get_endpoints(intf); if (NULL == endpoints || endpoints->len == 0) continue; ep = g_ptr_array_index(endpoints, 0); self->iface_idx = fu_usb_interface_get_number(intf); self->ep_num = fu_usb_endpoint_get_address(ep) & 0x7f; self->chunk_len = fu_usb_endpoint_get_maximum_packet_size(ep); return TRUE; } } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no update interface found"); return FALSE; } static gboolean fu_cros_ec_usb_device_probe(FuDevice *device, GError **error) { FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device); /* very much like usb_updater2's usb_findit() */ if (!fu_cros_ec_usb_device_find_interface(FU_USB_DEVICE(device), error)) { g_prefix_error(error, "failed to find update interface: "); return FALSE; } fu_usb_device_add_interface(FU_USB_DEVICE(self), self->iface_idx); if (self->chunk_len == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "wMaxPacketSize isn't valid: %" G_GUINT16_FORMAT, self->chunk_len); return FALSE; } /* success */ return TRUE; } static gboolean fu_cros_ec_usb_device_do_xfer(FuCrosEcUsbDevice *self, const guint8 *outbuf, gsize outlen, guint8 *inbuf, gsize inlen, gboolean allow_less, gsize *rxed_count, GError **error) { gsize actual = 0; /* send data out */ if (outbuf != NULL && outlen > 0) { g_autofree guint8 *outbuf_tmp = NULL; /* make mutable */ outbuf_tmp = fu_memdup_safe(outbuf, outlen, error); if (outbuf_tmp == NULL) return FALSE; if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), self->ep_num, outbuf_tmp, outlen, &actual, FU_CROS_EC_BULK_SEND_TIMEOUT, NULL, error)) return FALSE; if (actual != outlen) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "only sent %" G_GSIZE_FORMAT "/%" G_GSIZE_FORMAT " bytes", actual, outlen); return FALSE; } } /* read reply back */ if (inbuf != NULL && inlen > 0) { actual = 0; if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), self->ep_num | 0x80, inbuf, inlen, &actual, FU_CROS_EC_BULK_RECV_TIMEOUT, NULL, error)) { fu_error_convert(error); return FALSE; } if (actual != inlen && !allow_less) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "only received %" G_GSIZE_FORMAT "/%" G_GSIZE_FORMAT " bytes", actual, inlen); return FALSE; } } if (rxed_count != NULL) *rxed_count = actual; return TRUE; } static gboolean fu_cros_ec_usb_device_flush(FuDevice *device, gpointer user_data, GError **error) { FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device); gsize actual = 0; g_autofree guint8 *inbuf = g_malloc0(self->chunk_len); /* bulk transfer expected to fail normally (ie, no stale data) * but if bulk transfer succeeds, indicates stale bytes on the device * so this will retry until they're emptied */ if (fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), self->ep_num | 0x80, inbuf, self->chunk_len, &actual, FU_CROS_EC_FLUSH_TIMEOUT_MS, NULL, NULL)) { g_debug("flushing %" G_GSIZE_FORMAT " bytes", actual); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "flushing %" G_GSIZE_FORMAT " bytes", actual); return FALSE; } /* success */ return TRUE; } static gboolean fu_cros_ec_usb_device_recovery(FuCrosEcUsbDevice *self, GError **error) { /* flush all data from endpoint to recover in case of error */ if (!fu_device_retry(FU_DEVICE(self), fu_cros_ec_usb_device_flush, FU_CROS_EC_SETUP_RETRY_CNT, NULL, error)) { g_prefix_error(error, "failed to flush device to idle state: "); return FALSE; } /* success */ return TRUE; } /* * Channel TPM extension/vendor command over USB. The payload of the USB frame * in this case consists of the 2 byte subcommand code concatenated with the * command body. The caller needs to indicate if a response is expected, and * if it is - of what maximum size. */ static gboolean fu_cros_ec_usb_device_ext_cmd(FuCrosEcUsbDevice *self, guint16 subcommand, guint8 *cmd_body, gsize body_size, guint8 *resp, gsize *resp_size, gboolean allow_less, GError **error) { gsize usb_msg_size = FU_STRUCT_CROS_EC_UPDATE_FRAME_HEADER_SIZE + sizeof(subcommand) + body_size; g_autoptr(FuStructCrosEcUpdateFrameHeader) ufh = fu_struct_cros_ec_update_frame_header_new(); fu_struct_cros_ec_update_frame_header_set_block_size(ufh, usb_msg_size); fu_struct_cros_ec_update_frame_header_set_cmd_block_base( ufh, FU_CROS_EC_REQUEST_UPDATE_EXTRA_CMD); fu_byte_array_append_uint16(ufh, subcommand, G_BIG_ENDIAN); if (body_size > 0) g_byte_array_append(ufh, cmd_body, body_size); return fu_cros_ec_usb_device_do_xfer(self, ufh->data, ufh->len, (guint8 *)resp, resp_size != NULL ? *resp_size : 0, TRUE, NULL, error); } static gboolean fu_cros_ec_usb_device_start_request_cb(FuDevice *device, gpointer user_data, GError **error) { FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device); FuStructCrosEcFirstResponsePdu *st_rpdu = (FuStructCrosEcFirstResponsePdu *)user_data; gsize rxed_size = 0; g_autoptr(FuStructCrosEcUpdateFrameHeader) ufh = fu_struct_cros_ec_update_frame_header_new(); fu_struct_cros_ec_update_frame_header_set_block_size(ufh, ufh->len); if (!fu_cros_ec_usb_device_do_xfer(self, ufh->data, ufh->len, st_rpdu->data, st_rpdu->len, TRUE, &rxed_size, error)) return FALSE; /* we got something, so check for errors in response */ if (rxed_size < 8) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "unexpected response size %" G_GSIZE_FORMAT, rxed_size); return FALSE; } /* success */ return TRUE; } static gboolean fu_cros_ec_usb_device_setup(FuDevice *device, GError **error) { FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device); guint32 error_code; g_auto(GStrv) config_split = NULL; g_autoptr(FuStructCrosEcFirstResponsePdu) st_rpdu = fu_struct_cros_ec_first_response_pdu_new(); g_autoptr(FuCrosEcVersion) active_version = NULL; g_autoptr(FuCrosEcVersion) version = NULL; g_autoptr(GError) error_local = NULL; /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_cros_ec_usb_device_parent_class)->setup(device, error)) return FALSE; if (!fu_cros_ec_usb_device_recovery(self, error)) return FALSE; /* send start request */ if (!fu_device_retry(device, fu_cros_ec_usb_device_start_request_cb, FU_CROS_EC_SETUP_RETRY_CNT, st_rpdu, error)) { g_prefix_error(error, "failed to send start request: "); return FALSE; } self->protocol_version = fu_struct_cros_ec_first_response_pdu_get_protocol_version(st_rpdu); if (self->protocol_version < 5 || self->protocol_version > 6) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported protocol version %d", self->protocol_version); return FALSE; } error_code = fu_struct_cros_ec_first_response_pdu_get_return_value(st_rpdu); if (error_code != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "target reporting error %u", error_code); return FALSE; } self->writeable_offset = fu_struct_cros_ec_first_response_pdu_get_offset(st_rpdu); g_free(self->raw_version); self->raw_version = fu_struct_cros_ec_first_response_pdu_get_version(st_rpdu); self->maximum_pdu_size = fu_struct_cros_ec_first_response_pdu_get_maximum_pdu_size(st_rpdu); self->flash_protection = fu_struct_cros_ec_first_response_pdu_get_flash_protection(st_rpdu); /* get active version string and running region from iConfiguration */ if (!fu_cros_ec_usb_device_get_configuration(self, error)) return FALSE; config_split = g_strsplit(self->configuration, ":", 2); if (g_strv_length(config_split) < 2) { /* no prefix found so fall back to offset */ self->in_bootloader = self->writeable_offset != 0x0; active_version = fu_cros_ec_version_parse(self->configuration, error); if (active_version == NULL) { g_prefix_error(error, "failed parsing device's version: %32s: ", self->configuration); return FALSE; } } else { self->in_bootloader = g_strcmp0("RO", config_split[0]) == 0; active_version = fu_cros_ec_version_parse(config_split[1], error); if (active_version == NULL) { g_prefix_error(error, "failed parsing device's version: %32s: ", config_split[1]); return FALSE; } } /* get the other region's version string from targ */ version = fu_cros_ec_version_parse(self->raw_version, &error_local); if (version == NULL) { if (!self->in_bootloader) { g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed parsing device's version: %32s: ", self->raw_version); return FALSE; } /* if unable to parse version, copy from the active_version. * This allows to restore devices failed on write due different reasons. */ version = g_new0(FuCrosEcVersion, 1); version->boardname = g_strndup(active_version->boardname, FU_STRUCT_CROS_EC_FIRST_RESPONSE_PDU_SIZE_VERSION); version->triplet = g_strndup(active_version->triplet, FU_STRUCT_CROS_EC_FIRST_RESPONSE_PDU_SIZE_VERSION); version->sha1 = g_strndup(active_version->sha1, FU_STRUCT_CROS_EC_FIRST_RESPONSE_PDU_SIZE_VERSION); version->dirty = active_version->dirty; } if (self->in_bootloader) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); fu_device_set_version(FU_DEVICE(device), version->triplet); fu_device_set_version_bootloader(FU_DEVICE(device), active_version->triplet); } else { fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); fu_device_set_version(FU_DEVICE(device), active_version->triplet); fu_device_set_version_bootloader(FU_DEVICE(device), version->triplet); } /* one extra instance ID */ fu_device_add_instance_str(FU_DEVICE(device), "BOARDNAME", version->boardname); if (!fu_device_build_instance_id(FU_DEVICE(device), error, "USB", "VID", "PID", "BOARDNAME", NULL)) return FALSE; /* success */ return TRUE; } static gboolean fu_cros_ec_usb_device_reload(FuDevice *device, GError **error) { if (fu_device_has_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RO_WRITTEN) && fu_device_has_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_REBOOTING_TO_RO)) return TRUE; fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED); return TRUE; } static gboolean fu_cros_ec_usb_device_transfer_block_cb(FuDevice *device, gpointer user_data, GError **error) { FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device); FuCrosEcUsbBlockHelper *helper = (FuCrosEcUsbBlockHelper *)user_data; gsize transfer_size = 0; guint32 reply = 0; g_autoptr(FuStructCrosEcUpdateFrameHeader) ufh = fu_struct_cros_ec_update_frame_header_new(); g_autoptr(GPtrArray) chunks = NULL; /* first send the header */ fu_struct_cros_ec_update_frame_header_set_block_size( ufh, ufh->len + fu_chunk_get_data_sz(helper->block)); fu_struct_cros_ec_update_frame_header_set_cmd_block_base( ufh, fu_chunk_get_address(helper->block)); if (!fu_cros_ec_usb_device_do_xfer(self, ufh->data, ufh->len, NULL, 0, FALSE, NULL, error)) { g_autoptr(GError) error_flush = NULL; /* flush all data from endpoint to recover in case of error */ if (!fu_cros_ec_usb_device_recovery(self, &error_flush)) g_debug("failed to flush to idle: %s", error_flush->message); g_prefix_error(error, "failed at sending header: "); return FALSE; } /* we're in a retry handler */ fu_progress_reset(helper->progress); /* send the block, chunk by chunk */ chunks = fu_chunk_array_new(fu_chunk_get_data(helper->block), fu_chunk_get_data_sz(helper->block), 0x00, 0x00, self->chunk_len); fu_progress_set_id(helper->progress, G_STRLOC); fu_progress_set_steps(helper->progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_cros_ec_usb_device_do_xfer(self, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), NULL, 0, FALSE, NULL, error)) { g_autoptr(GError) error_flush = NULL; g_prefix_error(error, "failed sending chunk 0x%x: ", i); /* flush all data from endpoint to recover in case of error */ if (!fu_cros_ec_usb_device_recovery(self, &error_flush)) g_debug("failed to flush to idle: %s", error_flush->message); return FALSE; } fu_progress_step_done(helper->progress); } /* get the reply */ if (!fu_cros_ec_usb_device_do_xfer(self, NULL, 0, (guint8 *)&reply, sizeof(reply), TRUE, &transfer_size, error)) { g_autoptr(GError) error_flush = NULL; g_prefix_error(error, "failed at reply: "); /* flush all data from endpoint to recover in case of error */ if (!fu_cros_ec_usb_device_recovery(self, &error_flush)) g_debug("failed to flush to idle: %s", error_flush->message); return FALSE; } if (transfer_size == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_READ, "zero bytes received for block reply"); return FALSE; } if (reply != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "error: status 0x%#x", reply); return FALSE; } /* success */ return TRUE; } static gboolean fu_cros_ec_usb_device_transfer_section(FuCrosEcUsbDevice *self, FuFirmware *firmware, FuCrosEcFirmwareSection *section, FuProgress *progress, GError **error) { const guint8 *data_ptr = NULL; gsize data_len = 0; g_autoptr(GBytes) img_bytes = NULL; g_autoptr(GPtrArray) blocks = NULL; g_return_val_if_fail(section != NULL, FALSE); img_bytes = fu_firmware_get_image_by_idx_bytes(firmware, section->image_idx, error); if (img_bytes == NULL) { g_prefix_error(error, "failed to find section image: "); return FALSE; } data_ptr = (const guint8 *)g_bytes_get_data(img_bytes, &data_len); if (data_ptr == NULL || data_len != section->size) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "image and section sizes do not match: image = %" G_GSIZE_FORMAT " bytes vs section size = %" G_GSIZE_FORMAT " bytes", data_len, section->size); return FALSE; } /* smart update: trim trailing bytes */ while (data_len > 1 && (data_ptr[data_len - 1] == 0xff)) data_len--; g_debug("trimmed %" G_GSIZE_FORMAT " trailing bytes", section->size - data_len); g_debug("sending 0x%x bytes to 0x%x", (guint)data_len, section->offset); /* send in chunks of PDU size */ blocks = fu_chunk_array_new(data_ptr, data_len, section->offset, 0x0, self->maximum_pdu_size); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, blocks->len); for (guint i = 0; i < blocks->len; i++) { FuCrosEcUsbBlockHelper helper = { .block = g_ptr_array_index(blocks, i), .progress = fu_progress_get_child(progress), }; if (!fu_device_retry(FU_DEVICE(self), fu_cros_ec_usb_device_transfer_block_cb, FU_CROS_EC_MAX_BLOCK_XFER_RETRIES, &helper, error)) { g_prefix_error(error, "failed to transfer block 0x%x: ", i); return FALSE; } fu_progress_step_done(progress); } /* success */ return TRUE; } static void fu_cros_ec_usb_device_send_done(FuCrosEcUsbDevice *self) { guint8 buf[1] = {0x0}; g_autoptr(FuStructCrosEcUpdateDone) st = fu_struct_cros_ec_update_done_new(); g_autoptr(GError) error_local = NULL; /* send stop request, ignoring reply */ if (!fu_cros_ec_usb_device_do_xfer(self, st->data, st->len, buf, sizeof(buf), FALSE, NULL, &error_local)) { g_debug("error on transfer of done: %s", error_local->message); } } static gboolean fu_cros_ec_usb_device_send_subcommand(FuCrosEcUsbDevice *self, guint16 subcommand, guint8 *cmd_body, gsize body_size, guint8 *resp, gsize *resp_size, gboolean allow_less, GError **error) { fu_cros_ec_usb_device_send_done(self); if (!fu_cros_ec_usb_device_ext_cmd(self, subcommand, cmd_body, body_size, resp, resp_size, FALSE, error)) { g_prefix_error(error, "failed to send subcommand %" G_GUINT16_FORMAT ": ", subcommand); return FALSE; } /* success */ return TRUE; } static void fu_cros_ec_usb_device_reset_to_ro(FuCrosEcUsbDevice *self) { guint8 response = 0x0; guint16 subcommand = FU_CROS_EC_UPDATE_EXTRA_CMD_IMMEDIATE_RESET; guint8 command_body[2] = {0x0}; /* max command body size */ gsize command_body_size = 0; gsize response_size = 1; g_autoptr(GError) error_local = NULL; if (!fu_cros_ec_usb_device_send_subcommand(self, subcommand, command_body, command_body_size, &response, &response_size, FALSE, &error_local)) { /* failure here is ok */ g_debug("ignoring failure: reset: %s", error_local->message); } } static gboolean fu_cros_ec_usb_device_jump_to_rw(FuCrosEcUsbDevice *self) { guint8 response = 0x0; guint16 subcommand = FU_CROS_EC_UPDATE_EXTRA_CMD_JUMP_TO_RW; guint8 command_body[2] = {0x0}; /* max command body size */ gsize command_body_size = 0; gsize response_size = 1; g_autoptr(GError) error_local = NULL; if (!fu_cros_ec_usb_device_send_subcommand(self, subcommand, command_body, command_body_size, &response, &response_size, FALSE, &error_local)) { /* bail out early here if subcommand failed, which is normal */ g_debug("ignoring failure: jump to rw: %s", error_local->message); return TRUE; } /* Jump to rw may not work, so if we've reached here, initiate a * full reset using immediate reset */ fu_cros_ec_usb_device_reset_to_ro(self); /* success */ return TRUE; } static gboolean fu_cros_ec_usb_device_stay_in_ro(FuCrosEcUsbDevice *self, GError **error) { gsize response_size = 1; guint8 response = 0x0; guint16 subcommand = FU_CROS_EC_UPDATE_EXTRA_CMD_STAY_IN_RO; guint8 command_body[2] = {0x0}; /* max command body size */ gsize command_body_size = 0; if (!fu_cros_ec_usb_device_send_subcommand(self, subcommand, command_body, command_body_size, &response, &response_size, FALSE, error)) { return FALSE; } return TRUE; } static gboolean fu_cros_ec_usb_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device); g_autoptr(GPtrArray) sections = NULL; FuCrosEcFirmware *cros_ec_firmware = FU_CROS_EC_FIRMWARE(firmware); fu_device_remove_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_SPECIAL); if (fu_device_has_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_REBOOTING_TO_RO)) { g_autoptr(FuStructCrosEcFirstResponsePdu) st_rpdu = fu_struct_cros_ec_first_response_pdu_new(); fu_device_remove_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_REBOOTING_TO_RO); if (!fu_cros_ec_usb_device_stay_in_ro(self, error)) { g_prefix_error(error, "failed to send stay-in-ro subcommand: "); return FALSE; } /* flush all data from endpoint to recover in case of error */ if (!fu_cros_ec_usb_device_recovery(self, error)) { g_prefix_error(error, "failed to flush device to idle state: "); return FALSE; } /* send start request */ if (!fu_device_retry(device, fu_cros_ec_usb_device_start_request_cb, FU_CROS_EC_SETUP_RETRY_CNT, st_rpdu, error)) { g_prefix_error(error, "failed to send start request: "); return FALSE; } } if (fu_device_has_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RW_WRITTEN) && self->in_bootloader) { /* * We had previously written to the rw region (while we were * booted from ro region), but somehow landed in ro again after * a reboot. Since we wrote rw already, we wanted to jump * to the new rw so we could evaluate ro. * * This is a transitory state due to the fact that we have to * boot through RO to get to RW. Set another write required to * allow the RO region to auto-jump to RW. * * Special flow: write phase skips actual write -> attach skips * send of reset command, just sets wait for replug, and * device restart status. */ fu_device_add_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_SPECIAL); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED); return TRUE; } sections = fu_cros_ec_firmware_get_needed_sections(cros_ec_firmware, error); if (sections == NULL) return FALSE; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, sections->len); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); for (guint i = 0; i < sections->len; i++) { FuCrosEcFirmwareSection *section = g_ptr_array_index(sections, i); g_autoptr(GError) error_local = NULL; if (!fu_cros_ec_usb_device_transfer_section(self, firmware, section, fu_progress_get_child(progress), &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_READ)) { g_debug("failed to transfer section, trying another write, " "ignoring error: %s", error_local->message); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED); fu_progress_finished(progress); return TRUE; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } if (self->in_bootloader) { fu_device_set_version(device, section->version.triplet); } else { fu_device_set_version_bootloader(device, section->version.triplet); } fu_progress_step_done(progress); } /* send done */ fu_cros_ec_usb_device_send_done(self); if (self->in_bootloader) fu_device_add_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RW_WRITTEN); else fu_device_add_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RO_WRITTEN); /* logical XOR */ if (fu_device_has_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RW_WRITTEN) != fu_device_has_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RO_WRITTEN)) fu_device_add_flag(device, FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED); /* success */ return TRUE; } static FuFirmware * fu_cros_ec_usb_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device); g_autoptr(FuFirmware) firmware = fu_cros_ec_firmware_new(); if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; if (!fu_cros_ec_firmware_ensure_version(FU_CROS_EC_FIRMWARE(firmware), error)) return NULL; /* pick sections */ if (!fu_cros_ec_firmware_pick_sections(FU_CROS_EC_FIRMWARE(firmware), self->writeable_offset, error)) { g_prefix_error(error, "failed to pick sections: "); return NULL; } return g_steal_pointer(&firmware); } static gboolean fu_cros_ec_usb_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device); if (self->in_bootloader && fu_device_has_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_SPECIAL)) { /* * attach after the SPECIAL flag was set. The EC will auto-jump * from ro -> rw, so we do not need to send an explicit * reset_to_ro. We just need to set for another wait for replug * as a detach + reenumeration is expected as we jump from * ro -> rw. */ fu_device_remove_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_SPECIAL); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } if (fu_device_has_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RO_WRITTEN) && !fu_device_has_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RW_WRITTEN)) { fu_device_add_private_flag(FU_DEVICE(self), FU_CROS_EC_USB_DEVICE_FLAG_REBOOTING_TO_RO); fu_cros_ec_usb_device_reset_to_ro(self); } else { fu_cros_ec_usb_device_jump_to_rw(self); } fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* success */ return TRUE; } static gboolean fu_cros_ec_usb_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device); if (fu_device_has_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RW_WRITTEN) && !fu_device_has_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RO_WRITTEN)) return TRUE; if (self->in_bootloader) { /* If EC just rebooted - prevent jumping to RW during the update */ fu_device_add_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_REBOOTING_TO_RO); g_debug("skipping immediate reboot in case of already in bootloader"); /* in RO so skip reboot */ return TRUE; } if (self->flash_protection != 0x0) { /* in RW, and RO region is write protected, so jump to RO */ fu_device_add_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RO_WRITTEN); fu_device_add_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_REBOOTING_TO_RO); fu_cros_ec_usb_device_reset_to_ro(self); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); } /* success */ return TRUE; } static void fu_cros_ec_usb_device_replace(FuDevice *device, FuDevice *donor) { if (fu_device_has_private_flag(donor, FU_CROS_EC_USB_DEVICE_FLAG_RO_WRITTEN)) fu_device_add_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RO_WRITTEN); if (fu_device_has_private_flag(donor, FU_CROS_EC_USB_DEVICE_FLAG_RW_WRITTEN)) fu_device_add_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RW_WRITTEN); if (fu_device_has_private_flag(donor, FU_CROS_EC_USB_DEVICE_FLAG_REBOOTING_TO_RO)) fu_device_add_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_REBOOTING_TO_RO); if (fu_device_has_private_flag(donor, FU_CROS_EC_USB_DEVICE_FLAG_SPECIAL)) fu_device_add_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_SPECIAL); } static gboolean fu_cros_ec_usb_device_cleanup(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { fu_device_remove_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RO_WRITTEN); fu_device_remove_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_RW_WRITTEN); fu_device_remove_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_REBOOTING_TO_RO); fu_device_remove_private_flag(device, FU_CROS_EC_USB_DEVICE_FLAG_SPECIAL); /* success */ return TRUE; } static void fu_cros_ec_usb_device_init(FuCrosEcUsbDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "com.google.usb.crosec"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_DETACH_PREPARE_FIRMWARE); fu_device_set_acquiesce_delay(FU_DEVICE(self), 7500); /* ms */ fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_remove_delay(FU_DEVICE(self), FU_CROS_EC_USB_DEVICE_REMOVE_DELAY); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_CROS_EC_FIRMWARE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_register_private_flag(FU_DEVICE(self), FU_CROS_EC_USB_DEVICE_FLAG_RO_WRITTEN); fu_device_register_private_flag(FU_DEVICE(self), FU_CROS_EC_USB_DEVICE_FLAG_RW_WRITTEN); fu_device_register_private_flag(FU_DEVICE(self), FU_CROS_EC_USB_DEVICE_FLAG_REBOOTING_TO_RO); fu_device_register_private_flag(FU_DEVICE(self), FU_CROS_EC_USB_DEVICE_FLAG_SPECIAL); } static void fu_cros_ec_usb_device_to_string(FuDevice *device, guint idt, GString *str) { FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(device); fwupd_codec_string_append_int(str, idt, "ProtocolVersion", self->protocol_version); fwupd_codec_string_append_int(str, idt, "MaxPduSize", self->maximum_pdu_size); fwupd_codec_string_append_hex(str, idt, "FlashProtection", self->flash_protection); fwupd_codec_string_append(str, idt, "RawVersion", self->raw_version); fwupd_codec_string_append_hex(str, idt, "WriteableOffset", self->writeable_offset); } static void fu_cros_ec_usb_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 76, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 22, "reload"); } static void fu_cros_ec_usb_device_finalize(GObject *object) { FuCrosEcUsbDevice *self = FU_CROS_EC_USB_DEVICE(object); g_free(self->raw_version); G_OBJECT_CLASS(fu_cros_ec_usb_device_parent_class)->finalize(object); } static void fu_cros_ec_usb_device_class_init(FuCrosEcUsbDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_cros_ec_usb_device_finalize; device_class->attach = fu_cros_ec_usb_device_attach; device_class->detach = fu_cros_ec_usb_device_detach; device_class->prepare_firmware = fu_cros_ec_usb_device_prepare_firmware; device_class->setup = fu_cros_ec_usb_device_setup; device_class->to_string = fu_cros_ec_usb_device_to_string; device_class->write_firmware = fu_cros_ec_usb_device_write_firmware; device_class->probe = fu_cros_ec_usb_device_probe; device_class->set_progress = fu_cros_ec_usb_device_set_progress; device_class->reload = fu_cros_ec_usb_device_reload; device_class->replace = fu_cros_ec_usb_device_replace; device_class->cleanup = fu_cros_ec_usb_device_cleanup; } fwupd-2.0.10/plugins/cros-ec/fu-cros-ec-usb-device.h000066400000000000000000000005001501337203100220610ustar00rootroot00000000000000/* * Copyright 2020 Benson Leung * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_CROS_EC_USB_DEVICE (fu_cros_ec_usb_device_get_type()) G_DECLARE_FINAL_TYPE(FuCrosEcUsbDevice, fu_cros_ec_usb_device, FU, CROS_EC_USB_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/cros-ec/fu-cros-ec.rs000066400000000000000000000047611501337203100202470ustar00rootroot00000000000000// Copyright 2024 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[repr(u16be)] enum FuStructCrosEcFirstResponsePduHeaderType { Cr50, // must be 0 for backwards compatibility Common, } enum FuCrosEcUpdateExtraCmd { ImmediateReset, JumpToRw, StayInRo, UnlockRw, UnlockRollback, InjectEntropy, PairChallenge, TouchpadInfo, TouchpadDebug, ConsoleReadInit, ConsoleReadNext, } // Each RO or RW section of the new image can be in one of the following states enum FuCrosEcFirmwareUpgradeStatus { NotNeeded, // below or equal that on the target NotPossible, // RO is newer, but can't be transferred due to target RW shortcomings Needed, // section needs to be transferred to the target } // This is the frame format the host uses when sending update PDUs over USB. // The PDUs are up to 1K bytes in size, they are fragmented into USB chunks of 64 bytes each and // reassembled on the receive side before being passed to update function. // The flash update function receives the unframed PDU body (starting at the cmd field below), // and puts its reply into the same buffer the PDU was in. #[derive(New)] #[repr(C, packed)] struct FuStructCrosEcUpdateFrameHeader { block_size: u32be, // total frame size, including this field _cmd_block_digest: u32be, // four bytes of the structure sha1 digest (or 0 where ignored) cmd_block_base: u32be, // offset of this PDU into the flash SPI // payload goes here } #[derive(New, Default)] #[repr(C, packed)] struct FuStructCrosEcUpdateDone { value: u32be == 0xB007AB1E, } // various revision fields of the header created by the signer (cr50-specific). // these fields are compared when deciding if versions of two images are the same or when deciding // which one of the available images to run. #[repr(C, packed)] struct FuStructCrosEcSignedHeaderVersion { minor: u32be, major: u32be, epoch: u32be, } #[derive(New, Getters)] #[repr(C, packed)] struct FuStructCrosEcFirstResponsePdu { return_value: u32be, header_type: FuStructCrosEcFirstResponsePduHeaderType, protocol_version: u16be, maximum_pdu_size: u32be, flash_protection: u32be, // status offset: u32be, // offset of the other region version: [char; 32], // version string of the other region _min_rollback: u32be, // minimum rollback version that RO will accept _key_version: u32be, // RO public key version } fwupd-2.0.10/plugins/cros-ec/meson.build000066400000000000000000000011631501337203100200710ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginCrosEc"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('cros-ec.quirk') plugin_builtins += static_library('fu_plugin_cros_ec', rustgen.process( 'fu-cros-ec.rs', # fuzzing ), sources: [ 'fu-cros-ec-plugin.c', 'fu-cros-ec-usb-device.c', 'fu-cros-ec-common.c', # fuzzing 'fu-cros-ec-firmware.c', # fuzzing ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files( 'tests/acer-d501.json', 'tests/google-servo-micro.json', ) fwupd-2.0.10/plugins/cros-ec/tests/000077500000000000000000000000001501337203100170705ustar00rootroot00000000000000fwupd-2.0.10/plugins/cros-ec/tests/acer-d501.json000066400000000000000000000005431501337203100213460ustar00rootroot00000000000000{ "name": "Acer D501", "interactive": false, "steps": [ { "url": "1aca09b306293eab77af46f02c4ef913538ac99d33e53ff8cc70f71850ef405a-Baklava_11022021_sign.cab", "components": [ { "version": "2.0.10832", "guids": [ "d58a9b10-fb95-58ec-91a2-13764eca99f9" ] } ] } ] } fwupd-2.0.10/plugins/cros-ec/tests/cros-ec.builder.xml000066400000000000000000000007721501337203100226000ustar00rootroot00000000000000 0x3000 RO_FRID Y2hlZXNlX3YxLjEuMTc1NS00ZGE5NTIwICAgICAgICA= RW_FWID Y2hlZXNlX3YxLjEuMTc1NS00ZGE5NTIwICAgICAgICA= EC_RO Y2hlZXNlX3YxLjEuMTc1NS00ZGE5NTIwICAgICAgICA= EC_RW Y2hlZXNlX3YxLjEuMTc1NS00ZGE5NTIwICAgICAgICA= fwupd-2.0.10/plugins/cros-ec/tests/google-servo-micro.json000066400000000000000000000015451501337203100235070ustar00rootroot00000000000000{ "name": "Google Servo Micro", "interactive": false, "steps": [ { "url": "3c3123b6eaa89d5469553b301210a9e4cb06efa98a1cb7c6847a1d10bb0a0c4b-servo_micro_v2.4.0.cab", "emulation-url": "86fd0f1ae48eec0aad0ec97eaebc5bb8aa67fe03485f7b5824e6e1e9dfab42a6-servo_micro_v2.4.0.zip", "components": [ { "version": "2.4.0", "guids": [ "13564257-c649-586d-b4e4-4f048d480f36" ] } ] }, { "url": "1dc362734f138e71fa838ac5503491d297715d7e9bccc0424a62c4a5c68526cc-servo_micro_v2.4.17.cab", "emulation-url": "4e85ebbd498b25b1528151745f86b167363629665f4a2580a472773ea176dc14-servo_micro_v2.4.17.zip", "components": [ { "version": "2.4.17", "guids": [ "13564257-c649-586d-b4e4-4f048d480f36" ] } ] } ] } fwupd-2.0.10/plugins/dell-dock/000077500000000000000000000000001501337203100162315ustar00rootroot00000000000000fwupd-2.0.10/plugins/dell-dock/README.md000066400000000000000000000125321501337203100175130ustar00rootroot00000000000000--- title: Plugin: Dell USB-C Dock --- ## Dell System Unlike previous Dell USB-C devices, a Dell system is not needed for updating. ## Components The device contains components the following directly updatable components: * USB hubs * MST controller * Thunderbolt controller * Embedded controller This plugin is used to perform the update on the USB hubs as well as the Dell Embedded controller. The USB hubs are updated directly over a USB HID endpoint while the embedded controller is updated using an I2C over HID interface. The fwupd thunderbolt plugin is used for updating the Titan Ridge controller. The MST controller is updated through either the DP Aux interface (SynapticsMST plugin) or I2C over HID interface provided by this plugin. ## Device topology When this plugin is used, devices present in other plugins may be shown in the topology of this dock. This is intentional as this plugin works together with those plugins to manage the flashing of all components. ## Firmware Format The daemon will decompress the cabinet archive and extract several firmware blobs with an unspecified binary file format. This plugin supports the following protocol ID: * `com.dell.dock` * `com.synaptics.mst` ## GUID Generation These devices use several different generation schemes, e.g. * USB Hub1: `USB\VID_413C&PID_B06F&hub` * USB Hub2: `USB\VID_413C&PID_B06E&hub` * Embedded Controller: `USB\VID_413C&PID_B06E&hub&embedded` * Update Level: `USB\VID_413C&PID_B06E&hub&status` * MST Hub: `MST-panamera-vmm5331-259` * Thunderbolt Controller: `TBT-00d4b070` * USB4 Controller: `TBT-00d4b071` ## Update Behavior All devices will be updated the next time the usb-c plug from the dock is unplugged from the host. ### USB4 Controller This device will be probed by `dell-dock` plugin over the USB interface, additionally will be probed by `thunderbolt` plugin if thunderbolt hardware is enabled at the host. The primary plugin has been chosen to `dell-dock` for broader supoprt, the device introduced by `thunderbolt` plugin will be default inhibited, before fwupd version 1.8.2 (include), `Update Error` will be seen which is expected. ```shell USB4 controller in Dell dock: Device ID: ce501f4b2e03e819c525bb9354aa88c03db4f11e Summary: USB4 controller Current version: 36.00 Vendor: Dell Inc. (THUNDERBOLT:0x00D4, TBT:0x00D4) Install Duration: 46 seconds Update Error: firmware update inhibited by [dell_dock] plugin GUIDs: 4fb9d92e-2b96-51a7-9ed5-3db156dfcf12 ↠THUNDERBOLT\VEN_00D4&DEV_B071 bd79ce60-525b-5f39-a3f6-c98c495039ff ↠TBT-00d4b071 03f008d5-d06a-5d2e-89ca-61f12a8dbf73 ↠TBT-00d4b071-controller0-3 Device Flags: • System requires external power source • Device stages updates • Updatable • Signed Payload ``` After version 1.8.2, the device introduced by `thunderbolt` plugin will be default hidden, so only the primary plugin will be seen on the device view. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x413C` ## Quirk Use This plugin uses the following plugin-specific quirks: ### DellDockUnlockTarget The EC argument needed for unlocking certain device usage. Since: 1.1.3 ### DellDockBlobMajorOffset The offset of the major version number in a payload. Since: 1.1.3 ### DellDockBlobMinorOffset The offset of the minor version number in a payload Since: 1.1.3 ### DellDockBlobBuildOffset The offset of the build version number in a payload Since: 1.1.3 ### DellDockBlobVersionOffset The offset of the ASCII representation of a version string in a payload. Since: 1.1.3 ### DellDockBoardMin The minimum board revision required to safely operate the plugin. Since: 1.1.3 ### DellDockVersionLowest The minimum component version required to safely operate the plugin. Since: 1.1.3 ### DellDockBoard* The board description of a board revision. Since: 1.1.3 ### DellDockInstallDurationI2C The duration of time required to install a payload via I2C. Since: 1.1.3 ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.2.0`. ## Data flow ```mermaid flowchart TD subgraph Dell Dock I2C([I2C Bus]) EC EC_SPI[(EC\nStorage)] USB1 USB1_SPI[(SPI)] USB2 MST_SPI[(SPI)] USB2_SPI[(SPI)] MST TBT(TBT/USB4) TBT_SPI[(SPI)] end subgraph fwupd Process fwupdengine(FuEngine) dell_plugin(DellDock\nPlugin) tbt_plugin(Thunderbolt\nPlugin) usb4_plugin(USB4\nPlugin) end dell_plugin<--hid proxy-->I2C tbt_plugin<--thunderbolt-->TBT usb4_plugin<--hid-->TBT fwupdengine --- dell_plugin fwupdengine --- tbt_plugin USB1<-->I2C USB2<-.->I2C EC<-.->I2C TBT<-.->I2C MST<-.->I2C EC<-.->EC_SPI USB1---USB1_SPI USB2---USB2_SPI MST---MST_SPI TBT---TBT_SPI I2C~~~EC I2C~~~MST I2C~~~USB1 I2C~~~USB2 ``` ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Crag Wang: @CragW * Mario Limonciello: @superm1 fwupd-2.0.10/plugins/dell-dock/dell-dock.quirk000066400000000000000000000135261501337203100211530ustar00rootroot00000000000000# # Copyright 2018 Dell Inc. # All rights reserved. # # This software and associated documentation (if any) is furnished # under a license and may only be used or copied in accordance # with the terms of the license. # # This file is provided under a dual MIT/LGPLv2 license. When using or # redistributing this file, you may do so under either license. # Dell Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. # # SPDX-License-Identifier: LGPL-2.1-or-later OR MIT # # Used to make plugin probe the devices [USB\VID_413C&PID_B06F] Name = Unprobed Dell accessory endpoint Plugin = dell_dock [USB\VID_413C&PID_B06E] Name = Unprobed Dell accessory endpoint Plugin = dell_dock Flags = has-bridge # USB hub1 [USB\VID_413C&PID_B06F&hub] Name = RTS5413 in Dell dock Summary = USB 3.1 Generation 1 Hub ParentGuid = USB\VID_413C&PID_B06E&hub&embedded Plugin = dell_dock Vendor = Dell Inc. Icon = dock-usb FirmwareSize = 0x10000 Flags = updatable,dual-image,usable-during-update DellDockUnlockTarget = 8 DellDockBlobMajorOffset = 0x7F6E DellDockBlobMinorOffset = 0x7F6F InstallDuration = 14 # USB hub2 [USB\VID_413C&PID_B06E&hub] Name = RTS5487 in Dell dock Summary = USB 3.1 Generation 2 Hub ParentGuid = USB\VID_413C&PID_B06E&hub&embedded Vendor = Dell Inc. Plugin = dell_dock Icon = dock-usb FirmwareSize = 0x10000 Flags = updatable,has-bridge,dual-image,usable-during-update DellDockUnlockTarget = 7 DellDockBlobMajorOffset = 0x7F52 DellDockBlobMinorOffset = 0x7F53 InstallDuration = 3 # Atomic USB hub1 [USB\VID_413C&PID_B06F&atomic_hub] Name = RTS5413 in Dell dock Summary = USB 3.1 Generation 1 Hub ParentGuid = USB\VID_413C&PID_B06E&hub&atomic_embedded Plugin = dell_dock Vendor = Dell Inc. Icon = dock-usb FirmwareSize = 0x10000 Flags = updatable,dual-image,usable-during-update DellDockUnlockTarget = 8 DellDockBlobMajorOffset = 0x7F6E DellDockBlobMinorOffset = 0x7F6F InstallDuration = 14 # Atomic USB hub2 [USB\VID_413C&PID_B06E&atomic_hub] Name = RTS5487 in Dell dock Summary = USB 3.1 Generation 2 Hub ParentGuid = USB\VID_413C&PID_B06E&hub&atomic_embedded Vendor = Dell Inc. Plugin = dell_dock Icon = dock-usb FirmwareSize = 0x10000 Flags = updatable,has-bridge,dual-image,usable-during-update DellDockUnlockTarget = 7 DellDockBlobMajorOffset = 0x7F52 DellDockBlobMinorOffset = 0x7F53 InstallDuration = 3 # Embedded Controller # Name is intentionally not set (it's queried by dock) [USB\VID_413C&PID_B06E&hub&embedded] Name = Dell dock Summary = High performance dock Plugin = dell_dock Vendor = Dell Inc. VendorId = USB:0x413C Icon = dock-usb FirmwareSizeMin = 0x1FFC0 FirmwareSizeMax = 0x20000 Flags = dual-image,self-recovery,usable-during-update DellDockUnlockTarget = 1 DellDockBoardMin = 6 DellDockVersionLowest = 01.01.00.01 DellDockBlobVersionOffset = 0x1AFC0 InstallDuration = 60 #Atomic Embedded Controller # Name is intentionally not set (it's queried by dock) [USB\VID_413C&PID_B06E&hub&atomic_embedded] Name = Dell dock Summary = High performance dock Plugin = dell_dock Vendor = Dell Inc. VendorId = USB:0x413C Icon = dock-usb FirmwareSizeMin = 0x1FFC0 FirmwareSizeMax = 0x20000 Flags = dual-image,self-recovery,usable-during-update DellDockUnlockTarget = 1 DellDockBlobVersionOffset = 0x1AFC0 InstallDuration = 60 # Representation of overall dock update [USB\VID_413C&PID_B06E&hub&status] Name = Package level of Dell dock Summary = A representation of dock update status Plugin = dell_dock Vendor = Dell Inc. Flags = self-recovery,usable-during-update FirmwareSize = 24 InstallDuration = 5 DellDockBlobVersionOffset = 0x14 # Representation of overall dock update [USB\VID_413C&PID_B06E&hub&salomon_mlk_status] Name = Package level of Dell dock Summary = A representation of dock update status Plugin = dell_dock Vendor = Dell Inc. Flags = self-recovery,usable-during-update FirmwareSize = 24 InstallDuration = 5 DellDockBlobVersionOffset = 0x14 # Representation of overall dock update [USB\VID_413C&PID_B06E&hub&atomic_status] Name = Package level of Dell dock Summary = A representation of dock update status Plugin = dell_dock Vendor = Dell Inc. Flags = self-recovery,usable-during-update FirmwareSize = 24 InstallDuration = 5 DellDockBlobVersionOffset = 0x14 # MST Hub [MST-panamera-vmm5331-259] Name = VMM5331 in Dell dock Summary = Multi Stream Transport controller Vendor = Dell Inc. Plugin = dell_dock ParentGuid = USB\VID_413C&PID_B06E&hub&embedded Flags = skips-restart,dual-image,usable-during-update FirmwareSize = 524288 DellDockUnlockTarget = 9 InstallDuration = 95 DellDockInstallDurationI2C = 360 DellDockBlobMajorOffset = 0x18400 DellDockBlobMinorOffset = 0x18401 DellDockBlobBuildOffset = 0x18402 Icon = video-display #Atomic MST Hub [MST-cayenne-vmm6210-257] Name = VMM6210 in Dell dock Summary = Multi Stream Transport controller Vendor = Dell Inc. Plugin = dell_dock ParentGuid = USB\VID_413C&PID_B06E&hub&atomic_embedded Flags = skips-restart,dual-image,usable-during-update FirmwareSize = 1048576 DellDockUnlockTarget = 9 InstallDuration = 95 DellDockInstallDurationI2C = 360 DellDockBlobMajorOffset = 0x4000 DellDockBlobMinorOffset = 0x4001 DellDockBlobBuildOffset = 0x4002 Icon = video-display # Thunderbolt controller [TBT-00d4b070] Name = Thunderbolt controller in Dell dock Summary = Thunderbolt controller Vendor = Dell Inc. VendorId = TBT:0x00D4 ParentGuid = USB\VID_413C&PID_B06E&hub&embedded FirmwareSizeMin = 0x40000 FirmwareSizeMax = 0x80000 Flags = dual-image Icon = thunderbolt InstallDuration = 22 DellDockInstallDurationI2C = 181 DellDockUnlockTarget = 10 DellDockHubVersionLowest = 1.31 DellDockBlobMajorOffset = 0x400a DellDockBlobMinorOffset = 0x4009 # USB4 controller [TBT-00d4b071] Name = USB4 controller in Dell dock Summary = USB4 controller Vendor = Dell Inc. VendorId = TBT:0x00D4 ParentGuid = USB\VID_413C&PID_B06E&hub&embedded FirmwareSizeMin = 0x40000 FirmwareSizeMax = 0x80000 Flags = skips-restart,dual-image,usable-during-update Icon = thunderbolt InstallDuration = 46 fwupd-2.0.10/plugins/dell-dock/fu-dell-dock-common.c000066400000000000000000000023021501337203100221260ustar00rootroot00000000000000/* * Copyright 2018 Dell Inc. * All rights reserved. * * This software and associated documentation (if any) is furnished * under a license and may only be used or copied in accordance * with the terms of the license. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * Dell Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-dell-dock-common.h" #include "fu-dell-dock-ec.h" gboolean fu_dell_dock_set_power(FuDevice *device, guint8 target, gboolean enabled, GError **error) { FuDevice *parent; g_autoptr(FuDeviceLocker) locker = NULL; g_return_val_if_fail(device != NULL, FALSE); parent = FU_IS_DELL_DOCK_EC(device) ? device : fu_device_get_parent(device); if (parent == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "Couldn't find parent for %s", fu_device_get_name(device)); return FALSE; } locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; return fu_dell_dock_ec_modify_lock(parent, target, enabled, error); } fwupd-2.0.10/plugins/dell-dock/fu-dell-dock-common.h000066400000000000000000000033371501337203100221440ustar00rootroot00000000000000/* * Copyright 2018 Dell Inc. * All rights reserved. * * This software and associated documentation (if any) is furnished * under a license and may only be used or copied in accordance * with the terms of the license. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * Dell Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #include "fu-dell-dock-ec.h" #include "fu-dell-dock-hid.h" #include "fu-dell-dock-hub.h" #include "fu-dell-dock-mst.h" #include "fu-dell-dock-status.h" #include "fu-dell-dock-tbt.h" #define DELL_DOCK_DOCK1_INSTANCE_ID "USB\\VID_413C&PID_B06E&hub&status" #define DELL_DOCK_DOCK2_INSTANCE_ID "USB\\VID_413C&PID_B06E&hub&salomon_mlk_status" #define DELL_DOCK_EC_INSTANCE_ID "USB\\VID_413C&PID_B06E&hub&embedded" #define DELL_DOCK_TBT_INSTANCE_ID "TBT-00d4b070" #define DELL_DOCK_USB4_INSTANCE_ID "TBT-00d4b071" #define DELL_DOCK_VM5331_INSTANCE_ID "MST-panamera-vmm5331-259" #define GR_USB_VID 0x8087 #define GR_USB_PID 0x0B40 #define DELL_DOCK_ATOMIC_STATUS_INSTANCE_ID "USB\\VID_413C&PID_B06E&hub&atomic_status" #define DELL_DOCK_ATOMIC_EC_INSTANCE_ID "USB\\VID_413C&PID_B06E&hub&atomic_embedded" #define DELL_DOCK_VMM6210_INSTANCE_ID "MST-cayenne-vmm6210-257" #define ATOMIC_HUB2_PID 0x548A #define ATOMIC_HUB1_PID 0x541A #define DELL_VID 0x413C #define DOCK_BASE_TYPE_UNKNOWN 0x0 #define DOCK_BASE_TYPE_SALOMON 0x04 #define DOCK_BASE_TYPE_ATOMIC 0x05 gboolean fu_dell_dock_set_power(FuDevice *device, guint8 target, gboolean enabled, GError **error); fwupd-2.0.10/plugins/dell-dock/fu-dell-dock-ec.c000066400000000000000000000742061501337203100212410ustar00rootroot00000000000000/* * Copyright 2024 Dell Inc. * All rights reserved. * * This software and associated documentation (if any) is furnished * under a license and may only be used or copied in accordance * with the terms of the license. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * Dell Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include #include "fu-dell-dock-common.h" #define I2C_EC_ADDRESS 0xec #define EC_CMD_SET_DOCK_PKG 0x01 #define EC_CMD_GET_DOCK_INFO 0x02 #define EC_CMD_GET_DOCK_DATA 0x03 #define EC_CMD_GET_DOCK_TYPE 0x05 #define EC_CMD_MODIFY_LOCK 0x0a #define EC_CMD_RESET 0x0b #define EC_CMD_PASSIVE 0x0d #define EC_GET_FW_UPDATE_STATUS 0x0f #define EXPECTED_DOCK_INFO_SIZE 0xb7 #define TBT_MODE_MASK 0x01 #define PASSIVE_RESET_MASK 0x01 #define PASSIVE_REBOOT_MASK 0x02 #define PASSIVE_TBT_MASK 0x04 typedef enum { FW_UPDATE_IN_PROGRESS, FW_UPDATE_COMPLETE, FW_UPDATE_AUTHENTICATION_FAILED, } FuDellDockECFWUpdateStatus; const FuHIDI2CParameters ec_base_settings = { .i2ctargetaddr = I2C_EC_ADDRESS, .regaddrlen = 1, .i2cspeed = I2C_SPEED_250K, }; typedef enum { LOCATION_BASE, LOCATION_MODULE, } FuDellDockLocationEnum; typedef enum { FU_DELL_DOCK_DEVICETYPE_MAIN_EC = 0, FU_DELL_DOCK_DEVICETYPE_PD = 1, FU_DELL_DOCK_DEVICETYPE_HUB = 3, FU_DELL_DOCK_DEVICETYPE_MST = 4, FU_DELL_DOCK_DEVICETYPE_TBT = 5, } FuDellDockDeviceTypeEnum; typedef enum { SUBTYPE_GEN2, SUBTYPE_GEN1, } FuDellDockHubSubTypeEnum; typedef struct __attribute__((packed)) { /* nocheck:blocked */ guint8 total_devices; guint8 first_index; guint8 last_index; } FuDellDockDockInfoHeader; typedef struct __attribute__((packed)) { /* nocheck:blocked */ guint8 location; guint8 device_type; guint8 sub_type; guint8 arg; guint8 instance; } FuDellDockEcAddrMap; typedef struct __attribute__((packed)) { /* nocheck:blocked */ FuDellDockEcAddrMap ec_addr_map; union { guint32 version_32; guint8 version_8[4]; } version; } FuDellDockEcQueryEntry; typedef enum { MODULE_TYPE_45_TBT = 1, MODULE_TYPE_45, MODULE_TYPE_130_TBT, MODULE_TYPE_130_DP, MODULE_TYPE_130_UNIVERSAL, MODULE_TYPE_240_TRIN, MODULE_TYPE_210_DUAL, MODULE_TYPE_130_USB4, } FuDellDockDockModule; typedef struct __attribute__((packed)) { /* nocheck:blocked */ guint8 dock_configuration; guint8 dock_type; guint16 power_supply_wattage; guint16 module_type; guint16 board_id; guint16 port0_dock_status; guint16 port1_dock_status; guint32 dock_firmware_pkg_ver; guint64 module_serial; guint64 original_module_serial; gchar service_tag[7]; gchar marketing_name[64]; } FuDellDockDockDataStructure; typedef struct __attribute__((packed)) { /* nocheck:blocked */ guint32 ec_version; guint32 mst_version; guint32 hub1_version; guint32 hub2_version; guint32 tbt_version; guint32 pkg_version; } FuDellDockDockPackageFWVersion; struct _FuDellDockEc { FuDevice parent_instance; FuDellDockDockDataStructure *data; FuDellDockDockPackageFWVersion *raw_versions; guint8 base_type; gchar *ec_version; gchar *mst_version; gchar *tbt_version; guint8 unlock_target; guint8 board_min; gchar *ec_minimum_version; guint64 blob_version_offset; guint8 passive_flow; guint32 dock_unlock_status; }; static gboolean fu_dell_dock_ec_get_status(FuDevice *device, FuDellDockECFWUpdateStatus *status_out, GError **error); G_DEFINE_TYPE(FuDellDockEc, fu_dell_dock_ec, FU_TYPE_DEVICE) /* Used to root out I2C communication problems */ static gboolean fu_dell_dock_ec_test_valid_byte(const guint8 *str, gint index) { if (str[index] == 0x00 || str[index] == 0xff) return FALSE; return TRUE; } static void fu_dell_dock_ec_set_board(FuDevice *device) { FuDellDockEc *self = FU_DELL_DOCK_EC(device); const gchar *summary = NULL; g_autofree gchar *board_type_str = NULL; board_type_str = g_strdup_printf("DellDockBoard%u", self->data->board_id); summary = fu_device_get_metadata(device, board_type_str); if (summary != NULL) fu_device_set_summary(device, summary); } gboolean fu_dell_dock_ec_module_is_usb4(FuDevice *device) { FuDellDockEc *self = FU_DELL_DOCK_EC(device); return self->data->module_type == MODULE_TYPE_130_USB4; } guint8 fu_dell_dock_ec_get_dock_type(FuDevice *device) { FuDellDockEc *self = FU_DELL_DOCK_EC(device); return self->base_type; } const gchar * fu_dell_dock_ec_get_module_type(FuDevice *device) { FuDellDockEc *self = FU_DELL_DOCK_EC(device); switch (self->data->module_type) { case MODULE_TYPE_45_TBT: return "45 (TBT)"; case MODULE_TYPE_45: return "45"; case MODULE_TYPE_130_TBT: return "130 (TBT)"; case MODULE_TYPE_130_DP: return "130 (DP)"; case MODULE_TYPE_130_UNIVERSAL: return "130 (Universal)"; case MODULE_TYPE_240_TRIN: return "240 (Trinity)"; case MODULE_TYPE_210_DUAL: return "210 (Dual)"; case MODULE_TYPE_130_USB4: return "130 (TBT4)"; default: return "unknown"; } } gboolean fu_dell_dock_ec_needs_tbt(FuDevice *device) { FuDellDockEc *self = FU_DELL_DOCK_EC(device); gboolean port0_tbt_mode = self->data->port0_dock_status & TBT_MODE_MASK; /* check for TBT module type */ if (self->data->module_type != MODULE_TYPE_130_TBT && self->data->module_type != MODULE_TYPE_45_TBT) return FALSE; g_info("found thunderbolt dock, port mode: %d", port0_tbt_mode); return !port0_tbt_mode; } gboolean fu_dell_dock_ec_tbt_passive(FuDevice *device) { FuDellDockEc *self = FU_DELL_DOCK_EC(device); if (self->passive_flow > 0) { self->passive_flow |= PASSIVE_TBT_MASK; return TRUE; } return FALSE; } static const gchar * fu_dell_dock_ec_devicetype_to_str(guint device_type, guint sub_type) { switch (device_type) { case FU_DELL_DOCK_DEVICETYPE_MAIN_EC: return "EC"; case FU_DELL_DOCK_DEVICETYPE_MST: return "MST"; case FU_DELL_DOCK_DEVICETYPE_TBT: return "Thunderbolt"; case FU_DELL_DOCK_DEVICETYPE_HUB: if (sub_type == SUBTYPE_GEN2) return "USB 3.1 Gen2"; else if (sub_type == SUBTYPE_GEN1) return "USB 3.1 Gen1"; return NULL; case FU_DELL_DOCK_DEVICETYPE_PD: return "PD"; default: return NULL; } } static gboolean fu_dell_dock_ec_read(FuDevice *device, guint32 cmd, gsize length, GBytes **bytes, GError **error) { /* The first byte of result data will be the size of the return, hide this from callers */ guint8 result_length = length + 1; g_autoptr(GBytes) bytes_local = NULL; const guint8 *result; g_return_val_if_fail(device != NULL, FALSE); g_return_val_if_fail(fu_device_get_proxy(device) != NULL, FALSE); g_return_val_if_fail(bytes != NULL, FALSE); if (!fu_dell_dock_hid_i2c_read(fu_device_get_proxy(device), cmd, result_length, &bytes_local, &ec_base_settings, error)) { g_prefix_error(error, "read over HID-I2C failed: "); return FALSE; } result = g_bytes_get_data(bytes_local, NULL); /* first byte of result should be size of our data */ if (result[0] != length) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Invalid result data: %d expected %" G_GSIZE_FORMAT, result[0], length); return FALSE; } *bytes = g_bytes_new(result + 1, length); return TRUE; } static gboolean fu_dell_dock_ec_write(FuDevice *device, gsize length, guint8 *data, GError **error) { g_return_val_if_fail(device != NULL, FALSE); g_return_val_if_fail(fu_device_get_proxy(device) != NULL, FALSE); g_return_val_if_fail(length > 1, FALSE); if (!fu_dell_dock_hid_i2c_write(fu_device_get_proxy(device), data, length, &ec_base_settings, error)) { g_prefix_error(error, "write over HID-I2C failed: "); return FALSE; } return TRUE; } static gboolean fu_dell_dock_ec_is_valid_dock(FuDevice *device, GError **error) { FuDellDockEc *self = FU_DELL_DOCK_EC(device); const guint8 *result = NULL; gsize sz = 0; g_autoptr(GBytes) data = NULL; g_return_val_if_fail(device != NULL, FALSE); if (!fu_dell_dock_ec_read(device, EC_CMD_GET_DOCK_TYPE, 1, &data, error)) { g_prefix_error(error, "Failed to query dock type: "); return FALSE; } result = g_bytes_get_data(data, &sz); if (sz != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "No valid dock was found"); return FALSE; } self->base_type = result[0]; /* this will trigger setting up all the quirks */ if (self->base_type == DOCK_BASE_TYPE_SALOMON) { fu_device_add_instance_id(device, DELL_DOCK_EC_INSTANCE_ID); return TRUE; } if (self->base_type == DOCK_BASE_TYPE_ATOMIC) { fu_device_add_instance_id(device, DELL_DOCK_ATOMIC_EC_INSTANCE_ID); return TRUE; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Invalid dock type: %x", self->base_type); return FALSE; } static gboolean fu_dell_dock_ec_get_dock_info(FuDevice *device, GError **error) { FuDellDockEc *self = FU_DELL_DOCK_EC(device); const FuDellDockDockInfoHeader *header = NULL; const FuDellDockEcQueryEntry *device_entry = NULL; const FuDellDockEcAddrMap *map = NULL; guint32 oldest_base_pd = 0; g_autoptr(GBytes) data = NULL; g_return_val_if_fail(device != NULL, FALSE); if (!fu_dell_dock_ec_read(device, EC_CMD_GET_DOCK_INFO, EXPECTED_DOCK_INFO_SIZE, &data, error)) { g_prefix_error(error, "Failed to query dock info: "); return FALSE; } if (!g_bytes_get_data(data, NULL)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "Failed to read dock info"); return FALSE; } header = (FuDellDockDockInfoHeader *)g_bytes_get_data(data, NULL); if (!header) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "Failed to parse dock info"); return FALSE; } /* guard against EC not yet ready and fail init */ if (header->total_devices == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_SIGNATURE_INVALID, "No bridge devices detected, dock may be booting up"); return FALSE; } g_info("%u devices [%u->%u]", header->total_devices, header->first_index, header->last_index); device_entry = (FuDellDockEcQueryEntry *)((guint8 *)header + sizeof(FuDellDockDockInfoHeader)); for (guint i = 0; i < header->total_devices; i++) { const gchar *type_str; map = &(device_entry[i].ec_addr_map); type_str = fu_dell_dock_ec_devicetype_to_str(map->device_type, map->sub_type); if (type_str == NULL) continue; g_debug("#%u: %s in %s (A: %u I: %u)", i, type_str, (map->location == LOCATION_BASE) ? "Base" : "Module", map->arg, map->instance); g_debug("\tVersion32: %08x\tVersion8: %x %x %x %x", device_entry[i].version.version_32, device_entry[i].version.version_8[0], device_entry[i].version.version_8[1], device_entry[i].version.version_8[2], device_entry[i].version.version_8[3]); /* BCD but guint32 */ if (map->device_type == FU_DELL_DOCK_DEVICETYPE_MAIN_EC) { self->raw_versions->ec_version = device_entry[i].version.version_32; self->ec_version = g_strdup_printf("%02x.%02x.%02x.%02x", device_entry[i].version.version_8[0], device_entry[i].version.version_8[1], device_entry[i].version.version_8[2], device_entry[i].version.version_8[3]); g_debug("\tParsed version %s", self->ec_version); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); fu_device_set_version(FU_DEVICE(self), self->ec_version); } else if (map->device_type == FU_DELL_DOCK_DEVICETYPE_MST) { self->raw_versions->mst_version = device_entry[i].version.version_32; /* guard against invalid MST version read from EC */ if (!fu_dell_dock_ec_test_valid_byte(device_entry[i].version.version_8, 1)) { g_warning("[EC Bug] EC read invalid MST version %08x", device_entry[i].version.version_32); continue; } self->mst_version = g_strdup_printf("%02x.%02x.%02x", device_entry[i].version.version_8[1], device_entry[i].version.version_8[2], device_entry[i].version.version_8[3]); g_debug("\tParsed version %s", self->mst_version); } else if (map->device_type == FU_DELL_DOCK_DEVICETYPE_TBT && (self->data->module_type == MODULE_TYPE_130_TBT || self->data->module_type == MODULE_TYPE_45_TBT || self->data->module_type == MODULE_TYPE_130_USB4)) { /* guard against invalid Thunderbolt version read from EC */ if (!fu_dell_dock_ec_test_valid_byte(device_entry[i].version.version_8, 2)) { g_warning("[EC bug] EC read invalid Thunderbolt version %08x", device_entry[i].version.version_32); continue; } self->raw_versions->tbt_version = device_entry[i].version.version_32; self->tbt_version = g_strdup_printf("%02x.%02x", device_entry[i].version.version_8[2], device_entry[i].version.version_8[3]); g_debug("\tParsed version %s", self->tbt_version); } else if (map->device_type == FU_DELL_DOCK_DEVICETYPE_HUB) { g_debug("\thub subtype: %u", map->sub_type); if (map->sub_type == SUBTYPE_GEN2) self->raw_versions->hub2_version = device_entry[i].version.version_32; else if (map->sub_type == SUBTYPE_GEN1) self->raw_versions->hub1_version = device_entry[i].version.version_32; } else if (map->device_type == FU_DELL_DOCK_DEVICETYPE_PD && map->location == LOCATION_BASE && map->sub_type == 0) { if (oldest_base_pd == 0 || device_entry[i].version.version_32 < oldest_base_pd) oldest_base_pd = GUINT32_TO_BE(/* nocheck:blocked */ device_entry[i].version.version_32); g_debug("\tParsed version: %02x.%02x.%02x.%02x", device_entry[i].version.version_8[0], device_entry[i].version.version_8[1], device_entry[i].version.version_8[2], device_entry[i].version.version_8[3]); } } /* Thunderbolt SKU takes a little longer */ if (self->data->module_type == MODULE_TYPE_130_TBT || self->data->module_type == MODULE_TYPE_45_TBT || self->data->module_type == MODULE_TYPE_130_USB4) { guint64 tmp = fu_device_get_install_duration(device); fu_device_set_install_duration(device, tmp + 20); } /* passive flow is default enabled for production docks */ self->passive_flow = PASSIVE_REBOOT_MASK; fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_SKIPS_RESTART); return TRUE; } static gboolean fu_dell_dock_ec_get_dock_data(FuDevice *device, GError **error) { FuDellDockEc *self = FU_DELL_DOCK_EC(device); g_autoptr(GBytes) data = NULL; g_autoptr(GString) name = NULL; gchar service_tag[8] = {0x00}; const guint8 *result; gsize length = sizeof(FuDellDockDockDataStructure); g_autofree gchar *bundled_serial = NULL; FuDellDockECFWUpdateStatus status; g_return_val_if_fail(device != NULL, FALSE); if (!fu_dell_dock_ec_read(device, EC_CMD_GET_DOCK_DATA, length, &data, error)) { g_prefix_error(error, "Failed to query dock info: "); return FALSE; } result = g_bytes_get_data(data, NULL); if (result == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "Failed to read dock data"); return FALSE; } if (g_bytes_get_size(data) != length) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "Unexpected dock data size %" G_GSIZE_FORMAT, g_bytes_get_size(data)); return FALSE; } memcpy(self->data, result, length); /* nocheck:blocked */ /* guard against EC not yet ready and fail init */ name = g_string_new(self->data->marketing_name); if (name->len > 0) fu_device_set_name(device, name->str); else g_warning("[EC bug] Invalid dock name detected"); if (self->data->module_type >= 0xfe) g_warning("[EC bug] Invalid module type 0x%02x", self->data->module_type); /* set serial number */ memcpy(service_tag, self->data->service_tag, 7); /* nocheck:blocked */ bundled_serial = g_strdup_printf("%s/%08" G_GUINT64_FORMAT, service_tag, self->data->module_serial); fu_device_set_serial(device, bundled_serial); /* copy this for being able to send in next commit transaction */ self->raw_versions->pkg_version = self->data->dock_firmware_pkg_ver; /* read if passive update pending */ if (!fu_dell_dock_ec_get_status(device, &status, error)) return FALSE; /* make sure this hardware spin matches our expectations */ if (self->data->board_id >= self->board_min) { if (status != FW_UPDATE_IN_PROGRESS) { fu_dell_dock_ec_set_board(device); fu_device_uninhibit(device, "update-pending"); } else { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION); fu_device_add_problem(device, FWUPD_DEVICE_PROBLEM_UPDATE_PENDING); } } else { fu_device_inhibit(device, "not-supported", "Utility does not support this board"); } return TRUE; } static void fu_dell_dock_ec_to_string(FuDevice *device, guint idt, GString *str) { FuDellDockEc *self = FU_DELL_DOCK_EC(device); gchar service_tag[8] = {0x00}; fwupd_codec_string_append_int(str, idt, "BaseType", self->base_type); fwupd_codec_string_append_int(str, idt, "BoardId", self->data->board_id); fwupd_codec_string_append_int(str, idt, "PowerSupply", self->data->power_supply_wattage); fwupd_codec_string_append_hex(str, idt, "StatusPort0", self->data->port0_dock_status); fwupd_codec_string_append_hex(str, idt, "StatusPort1", self->data->port1_dock_status); memcpy(service_tag, self->data->service_tag, 7); /* nocheck:blocked */ fwupd_codec_string_append(str, idt, "ServiceTag", service_tag); fwupd_codec_string_append_int(str, idt, "Configuration", self->data->dock_configuration); fwupd_codec_string_append_hex(str, idt, "PackageFirmwareVersion", self->data->dock_firmware_pkg_ver); fwupd_codec_string_append_int(str, idt, "ModuleSerial", self->data->module_serial); fwupd_codec_string_append_int(str, idt, "OriginalModuleSerial", self->data->original_module_serial); fwupd_codec_string_append_int(str, idt, "Type", self->data->dock_type); fwupd_codec_string_append_hex(str, idt, "ModuleType", self->data->module_type); fwupd_codec_string_append(str, idt, "MinimumEc", self->ec_minimum_version); fwupd_codec_string_append_int(str, idt, "PassiveFlow", self->passive_flow); } gboolean fu_dell_dock_ec_modify_lock(FuDevice *device, guint8 target, gboolean unlocked, GError **error) { FuDellDockEc *self = FU_DELL_DOCK_EC(device); guint32 cmd; g_return_val_if_fail(device != NULL, FALSE); g_return_val_if_fail(target != 0, FALSE); cmd = EC_CMD_MODIFY_LOCK | /* cmd */ 2 << 8 | /* length of data arguments */ target << 16 | /* device to operate on */ unlocked << 24; /* unlock/lock */ if (!fu_dell_dock_ec_write(device, 4, (guint8 *)&cmd, error)) { g_prefix_error(error, "Failed to unlock device %d: ", target); return FALSE; } g_debug("Modified lock for %d to %d through %s (%s)", target, unlocked, fu_device_get_name(device), fu_device_get_id(device)); if (unlocked) FU_BIT_SET(self->dock_unlock_status, target); else FU_BIT_CLEAR(self->dock_unlock_status, target); g_debug("current overall unlock status: 0x%08x", self->dock_unlock_status); return TRUE; } static gboolean fu_dell_dock_ec_reset(FuDevice *device, GError **error) { guint16 cmd = EC_CMD_RESET; g_return_val_if_fail(device != NULL, FALSE); return fu_dell_dock_ec_write(device, 2, (guint8 *)&cmd, error); } static gboolean fu_dell_dock_ec_activate(FuDevice *device, FuProgress *progress, GError **error) { FuDellDockECFWUpdateStatus status; /* read if passive update pending */ if (!fu_dell_dock_ec_get_status(device, &status, error)) return FALSE; if (status != FW_UPDATE_IN_PROGRESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "No firmware update pending for %s", fu_device_get_name(device)); return FALSE; } return fu_dell_dock_ec_reset(device, error); } gboolean fu_dell_dock_ec_reboot_dock(FuDevice *device, GError **error) { FuDellDockEc *self = FU_DELL_DOCK_EC(device); guint32 cmd = EC_CMD_PASSIVE | /* cmd */ 1 << 8 | /* length of data arguments */ self->passive_flow << 16; g_return_val_if_fail(device != NULL, FALSE); g_info("activating passive flow (%x) for %s", self->passive_flow, fu_device_get_name(device)); return fu_dell_dock_ec_write(device, 3, (guint8 *)&cmd, error); } static gboolean fu_dell_dock_ec_get_status(FuDevice *device, FuDellDockECFWUpdateStatus *status_out, GError **error) { g_autoptr(GBytes) data = NULL; const guint8 *result = NULL; g_return_val_if_fail(device != NULL, FALSE); g_return_val_if_fail(status_out != NULL, FALSE); if (!fu_dell_dock_ec_read(device, EC_GET_FW_UPDATE_STATUS, 1, &data, error)) { g_prefix_error(error, "Failed to read FW update status: "); return FALSE; } result = g_bytes_get_data(data, NULL); if (!result) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "Failed to read FW update status"); return FALSE; } *status_out = *result; return TRUE; } const gchar * fu_dell_dock_ec_get_tbt_version(FuDevice *device) { FuDellDockEc *self = FU_DELL_DOCK_EC(device); return self->tbt_version; } const gchar * fu_dell_dock_ec_get_mst_version(FuDevice *device) { FuDellDockEc *self = FU_DELL_DOCK_EC(device); return self->mst_version; } guint32 fu_dell_dock_ec_get_status_version(FuDevice *device) { FuDellDockEc *self = FU_DELL_DOCK_EC(device); return self->raw_versions->pkg_version; } gboolean fu_dell_dock_ec_commit_package(FuDevice *device, GBytes *blob_fw, GError **error) { FuDellDockEc *self = FU_DELL_DOCK_EC(device); gsize length = 0; const guint8 *data = g_bytes_get_data(blob_fw, &length); g_autofree guint8 *payload = g_malloc0(length + 2); g_return_val_if_fail(device != NULL, FALSE); g_return_val_if_fail(blob_fw != NULL, FALSE); if (length != sizeof(FuDellDockDockPackageFWVersion)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "Invalid package size %" G_GSIZE_FORMAT, length); return FALSE; } memcpy(self->raw_versions, data, length); /* nocheck:blocked */ g_debug("Committing (%zu) bytes ", sizeof(FuDellDockDockPackageFWVersion)); g_debug("\tec_version: %x", self->raw_versions->ec_version); g_debug("\tmst_version: %x", self->raw_versions->mst_version); g_debug("\thub1_version: %x", self->raw_versions->hub1_version); g_debug("\thub2_version: %x", self->raw_versions->hub2_version); g_debug("\ttbt_version: %x", self->raw_versions->tbt_version); g_debug("\tpkg_version: %x", self->raw_versions->pkg_version); payload[0] = EC_CMD_SET_DOCK_PKG; payload[1] = length; memcpy(payload + 2, data, length); /* nocheck:blocked */ if (!fu_dell_dock_ec_write(device, length + 2, payload, error)) { g_prefix_error(error, "Failed to query dock info: "); return FALSE; } return TRUE; } static gboolean fu_dell_dock_ec_write_fw(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDellDockEc *self = FU_DELL_DOCK_EC(device); gsize fw_size = 0; const guint8 *data; gsize write_size = 0; gsize nwritten = 0; guint32 address = 0 | 0xff << 24; g_autofree gchar *dynamic_version = NULL; g_autoptr(GBytes) fw = NULL; g_return_val_if_fail(device != NULL, FALSE); g_return_val_if_fail(FU_IS_FIRMWARE(firmware), FALSE); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 15, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 85, NULL); /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; data = g_bytes_get_data(fw, &fw_size); write_size = (fw_size / HIDI2C_MAX_WRITE) >= 1 ? HIDI2C_MAX_WRITE : fw_size; dynamic_version = g_strndup((gchar *)data + self->blob_version_offset, 11); g_info("writing EC firmware version %s", dynamic_version); /* meet the minimum EC version */ if ((flags & FWUPD_INSTALL_FLAG_IGNORE_REQUIREMENTS) == 0 && (fu_version_compare(dynamic_version, self->ec_minimum_version, FWUPD_VERSION_FORMAT_QUAD) < 0)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "New EC version %s is less than minimum required %s", dynamic_version, self->ec_minimum_version); return FALSE; } g_info("writing EC firmware version %s", dynamic_version); if (!fu_dell_dock_ec_modify_lock(device, self->unlock_target, TRUE, error)) return FALSE; if (!fu_dell_dock_hid_raise_mcu_clock(fu_device_get_proxy(device), TRUE, error)) return FALSE; /* erase */ if (!fu_dell_dock_hid_erase_bank(fu_device_get_proxy(device), 0xff, error)) return FALSE; fu_progress_step_done(progress); /* write */ do { /* last packet */ if (fw_size - nwritten < write_size) write_size = fw_size - nwritten; if (!fu_dell_dock_hid_write_flash(fu_device_get_proxy(device), address, data, write_size, error)) { g_prefix_error(error, "write over HID failed: "); return FALSE; } fu_progress_set_percentage_full(fu_progress_get_child(progress), nwritten, fw_size); nwritten += write_size; data += write_size; address += write_size; } while (nwritten < fw_size); fu_progress_step_done(progress); if (!fu_dell_dock_hid_raise_mcu_clock(fu_device_get_proxy(device), FALSE, error)) return FALSE; /* dock will reboot to re-read; this is to appease the daemon */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_QUAD); fu_device_set_version(device, dynamic_version); /* activate passive behavior */ self->passive_flow |= PASSIVE_RESET_MASK; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION); return TRUE; } static gboolean fu_dell_dock_ec_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuDellDockEc *self = FU_DELL_DOCK_EC(device); guint64 tmp = 0; if (g_strcmp0(key, "DellDockUnlockTarget") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->unlock_target = tmp; return TRUE; } if (g_strcmp0(key, "DellDockBoardMin") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->board_min = tmp; return TRUE; } if (g_strcmp0(key, "DellDockVersionLowest") == 0) { self->ec_minimum_version = g_strdup(value); return TRUE; } if (g_str_has_prefix(key, "DellDockBoard")) { fu_device_set_metadata(device, key, value); return TRUE; } if (g_strcmp0(key, "DellDockBlobVersionOffset") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->blob_version_offset = tmp; return TRUE; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static gboolean fu_dell_dock_ec_query(FuDevice *device, GError **error) { if (!fu_dell_dock_ec_get_dock_data(device, error)) return FALSE; return fu_dell_dock_ec_get_dock_info(device, error); } static gboolean fu_dell_dock_ec_setup(FuDevice *device, GError **error) { g_autoptr(GError) error_local = NULL; /* if query looks bad, wait a few seconds and retry */ if (!fu_dell_dock_ec_query(device, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_SIGNATURE_INVALID)) { g_warning("%s", error_local->message); fu_device_sleep(device, 2000); /* ms */ if (!fu_dell_dock_ec_query(device, error)) return FALSE; } else { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } return TRUE; } static gboolean fu_dell_dock_ec_open(FuDevice *device, GError **error) { FuDellDockEc *self = FU_DELL_DOCK_EC(device); /* sanity check */ if (fu_device_get_proxy(device) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return FALSE; } if (!fu_device_open(fu_device_get_proxy(device), error)) return FALSE; if (!self->data->dock_type) return fu_dell_dock_ec_is_valid_dock(device, error); return TRUE; } static gboolean fu_dell_dock_ec_close(FuDevice *device, GError **error) { /* sanity check */ if (fu_device_get_proxy(device) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return FALSE; } return fu_device_close(fu_device_get_proxy(device), error); } static void fu_dell_dock_ec_finalize(GObject *object) { FuDellDockEc *self = FU_DELL_DOCK_EC(object); g_free(self->ec_version); g_free(self->mst_version); g_free(self->tbt_version); g_free(self->data); g_free(self->raw_versions); g_free(self->ec_minimum_version); G_OBJECT_CLASS(fu_dell_dock_ec_parent_class)->finalize(object); } static void fu_dell_dock_ec_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_dell_dock_ec_init(FuDellDockEc *self) { self->data = g_new0(FuDellDockDockDataStructure, 1); self->raw_versions = g_new0(FuDellDockDockPackageFWVersion, 1); fu_device_add_protocol(FU_DEVICE(self), "com.dell.dock"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); } static void fu_dell_dock_ec_class_init(FuDellDockEcClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->finalize = fu_dell_dock_ec_finalize; device_class->activate = fu_dell_dock_ec_activate; device_class->to_string = fu_dell_dock_ec_to_string; device_class->setup = fu_dell_dock_ec_setup; device_class->open = fu_dell_dock_ec_open; device_class->close = fu_dell_dock_ec_close; device_class->write_firmware = fu_dell_dock_ec_write_fw; device_class->set_quirk_kv = fu_dell_dock_ec_set_quirk_kv; device_class->set_progress = fu_dell_dock_ec_set_progress; } FuDellDockEc * fu_dell_dock_ec_new(FuDevice *proxy) { FuDellDockEc *self = NULL; FuContext *ctx = fu_device_get_context(proxy); self = g_object_new(FU_TYPE_DELL_DOCK_EC, "context", ctx, NULL); fu_device_set_proxy(FU_DEVICE(self), proxy); fu_device_incorporate(FU_DEVICE(self), proxy, FU_DEVICE_INCORPORATE_FLAG_PHYSICAL_ID); fu_device_set_logical_id(FU_DEVICE(self), "ec"); return self; } fwupd-2.0.10/plugins/dell-dock/fu-dell-dock-ec.h000066400000000000000000000027201501337203100212360ustar00rootroot00000000000000/* * Copyright 2018 Dell Inc. * All rights reserved. * * This software and associated documentation (if any) is furnished * under a license and may only be used or copied in accordance * with the terms of the license. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * Dell Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_DELL_DOCK_EC (fu_dell_dock_ec_get_type()) G_DECLARE_FINAL_TYPE(FuDellDockEc, fu_dell_dock_ec, FU, DELL_DOCK_EC, FuDevice) FuDellDockEc * fu_dell_dock_ec_new(FuDevice *proxy); const gchar * fu_dell_dock_ec_get_module_type(FuDevice *device); gboolean fu_dell_dock_ec_needs_tbt(FuDevice *device); gboolean fu_dell_dock_ec_tbt_passive(FuDevice *device); gboolean fu_dell_dock_ec_modify_lock(FuDevice *device, guint8 target, gboolean unlocked, GError **error); gboolean fu_dell_dock_ec_reboot_dock(FuDevice *device, GError **error); const gchar * fu_dell_dock_ec_get_mst_version(FuDevice *device); const gchar * fu_dell_dock_ec_get_tbt_version(FuDevice *device); guint32 fu_dell_dock_ec_get_status_version(FuDevice *device); gboolean fu_dell_dock_ec_commit_package(FuDevice *device, GBytes *blob_fw, GError **error); gboolean fu_dell_dock_ec_module_is_usb4(FuDevice *device); guint8 fu_dell_dock_ec_get_dock_type(FuDevice *device); fwupd-2.0.10/plugins/dell-dock/fu-dell-dock-hid.c000066400000000000000000000311331501337203100214060ustar00rootroot00000000000000/* * Copyright 2018 Realtek Semiconductor Corporation * Copyright 2018 Dell Inc. * All rights reserved. * * This software and associated documentation (if any) is furnished * under a license and may only be used or copied in accordance * with the terms of the license. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * Dell Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include #include #include "fu-dell-dock-hid.h" #define HIDI2C_MAX_REGISTER 4 #define HID_MAX_RETRIES 5 #define TBT_MAX_RETRIES 2 #define HIDI2C_TRANSACTION_TIMEOUT 300 #define HUB_CMD_READ_DATA 0xC0 #define HUB_CMD_WRITE_DATA 0x40 #define HUB_EXT_READ_STATUS 0x09 #define HUB_EXT_MCUMODIFYCLOCK 0x06 #define HUB_EXT_I2C_WRITE 0xC6 #define HUB_EXT_WRITEFLASH 0xC8 #define HUB_EXT_I2C_READ 0xD6 #define HUB_EXT_VERIFYUPDATE 0xD9 #define HUB_EXT_ERASEBANK 0xE8 #define HUB_EXT_WRITE_TBT_FLASH 0xFF #define TBT_COMMAND_WAKEUP 0x00000000 #define TBT_COMMAND_AUTHENTICATE 0xFFFFFFFF #define TBT_COMMAND_AUTHENTICATE_STATUS 0xFFFFFFFE typedef struct __attribute__((packed)) { /* nocheck:blocked */ guint8 cmd; guint8 ext; union { guint32 dwregaddr; struct { guint8 cmd_data0; guint8 cmd_data1; guint8 cmd_data2; guint8 cmd_data3; }; }; guint16 bufferlen; FuHIDI2CParameters parameters; guint8 extended_cmdarea[53]; guint8 data[192]; } FuHIDCmdBuffer; typedef struct __attribute__((packed)) { /* nocheck:blocked */ guint8 cmd; guint8 ext; guint8 i2ctargetaddr; guint8 i2cspeed; union { guint32 startaddress; guint32 tbt_command; }; guint8 bufferlen; guint8 extended_cmdarea[55]; guint8 data[192]; } FuTbtCmdBuffer; static gboolean fu_dell_dock_hid_set_report_cb(FuDevice *self, gpointer user_data, GError **error) { guint8 *outbuffer = (guint8 *)user_data; return fu_hid_device_set_report(FU_HID_DEVICE(self), 0x0, outbuffer, 192, HIDI2C_TRANSACTION_TIMEOUT, FU_HID_DEVICE_FLAG_NONE, error); } static gboolean fu_dell_dock_hid_set_report(FuDevice *self, guint8 *outbuffer, GError **error) { return fu_device_retry(self, fu_dell_dock_hid_set_report_cb, HID_MAX_RETRIES, outbuffer, error); } static gboolean fu_dell_dock_hid_get_report_cb(FuDevice *self, gpointer user_data, GError **error) { guint8 *inbuffer = (guint8 *)user_data; return fu_hid_device_get_report(FU_HID_DEVICE(self), 0x0, inbuffer, 192, HIDI2C_TRANSACTION_TIMEOUT, FU_HID_DEVICE_FLAG_NONE, error); } static gboolean fu_dell_dock_hid_get_report(FuDevice *self, guint8 *inbuffer, GError **error) { return fu_device_retry(self, fu_dell_dock_hid_get_report_cb, HID_MAX_RETRIES, inbuffer, error); } gboolean fu_dell_dock_hid_get_hub_version(FuDevice *self, GError **error) { g_autofree gchar *version = NULL; FuHIDCmdBuffer cmd_buffer = { .cmd = HUB_CMD_READ_DATA, .ext = HUB_EXT_READ_STATUS, .cmd_data0 = 0, .cmd_data1 = 0, .cmd_data2 = 0, .cmd_data3 = 0, .bufferlen = GUINT16_TO_LE(12), /* nocheck:blocked */ .parameters = {.i2ctargetaddr = 0, .regaddrlen = 0, .i2cspeed = 0}, .extended_cmdarea[0 ... 52] = 0, }; if (!fu_dell_dock_hid_set_report(self, (guint8 *)&cmd_buffer, error)) { g_prefix_error(error, "failed to query hub version: "); return FALSE; } if (!fu_dell_dock_hid_get_report(self, cmd_buffer.data, error)) { g_prefix_error(error, "failed to query hub version: "); return FALSE; } version = g_strdup_printf("%02x.%02x", cmd_buffer.data[10], cmd_buffer.data[11]); fu_device_set_version_format(self, FWUPD_VERSION_FORMAT_PAIR); fu_device_set_version(self, version); return TRUE; } gboolean fu_dell_dock_hid_raise_mcu_clock(FuDevice *self, gboolean enable, GError **error) { FuHIDCmdBuffer cmd_buffer = { .cmd = HUB_CMD_WRITE_DATA, .ext = HUB_EXT_MCUMODIFYCLOCK, .cmd_data0 = (guint8)enable, .cmd_data1 = 0, .cmd_data2 = 0, .cmd_data3 = 0, .bufferlen = 0, .parameters = {.i2ctargetaddr = 0, .regaddrlen = 0, .i2cspeed = 0}, .extended_cmdarea[0 ... 52] = 0, }; if (!fu_dell_dock_hid_set_report(self, (guint8 *)&cmd_buffer, error)) { g_prefix_error(error, "failed to set mcu clock to %d: ", enable); return FALSE; } return TRUE; } gboolean fu_dell_dock_hid_erase_bank(FuDevice *self, guint8 idx, GError **error) { FuHIDCmdBuffer cmd_buffer = { .cmd = HUB_CMD_WRITE_DATA, .ext = HUB_EXT_ERASEBANK, .cmd_data0 = 0, .cmd_data1 = idx, .cmd_data2 = 0, .cmd_data3 = 0, .bufferlen = 0, .parameters = {.i2ctargetaddr = 0, .regaddrlen = 0, .i2cspeed = 0}, .extended_cmdarea[0 ... 52] = 0, }; if (!fu_dell_dock_hid_set_report(self, (guint8 *)&cmd_buffer, error)) { g_prefix_error(error, "failed to erase bank: "); return FALSE; } return TRUE; } gboolean fu_dell_dock_hid_write_flash(FuDevice *self, guint32 dwAddr, const guint8 *input, gsize write_size, GError **error) { FuHIDCmdBuffer cmd_buffer = { .cmd = HUB_CMD_WRITE_DATA, .ext = HUB_EXT_WRITEFLASH, .dwregaddr = GUINT32_TO_LE(dwAddr), /* nocheck:blocked */ .bufferlen = GUINT16_TO_LE(write_size), /* nocheck:blocked */ .parameters = {.i2ctargetaddr = 0, .regaddrlen = 0, .i2cspeed = 0}, .extended_cmdarea[0 ... 52] = 0, }; g_return_val_if_fail(write_size <= HIDI2C_MAX_WRITE, FALSE); memcpy(cmd_buffer.data, input, write_size); /* nocheck:blocked */ if (!fu_dell_dock_hid_set_report(self, (guint8 *)&cmd_buffer, error)) { g_prefix_error(error, "failed to write %" G_GSIZE_FORMAT " flash to %x: ", write_size, dwAddr); return FALSE; } return TRUE; } gboolean fu_dell_dock_hid_verify_update(FuDevice *self, gboolean *result, GError **error) { FuHIDCmdBuffer cmd_buffer = { .cmd = HUB_CMD_WRITE_DATA, .ext = HUB_EXT_VERIFYUPDATE, .cmd_data0 = 1, .cmd_data1 = 0, .cmd_data2 = 0, .cmd_data3 = 0, .bufferlen = GUINT16_TO_LE(1), /* nocheck:blocked */ .parameters = {.i2ctargetaddr = 0, .regaddrlen = 0, .i2cspeed = 0}, .extended_cmdarea[0 ... 52] = 0, }; if (!fu_dell_dock_hid_set_report(self, (guint8 *)&cmd_buffer, error)) { g_prefix_error(error, "failed to verify update: "); return FALSE; } if (!fu_dell_dock_hid_get_report(self, cmd_buffer.data, error)) { g_prefix_error(error, "failed to verify update: "); return FALSE; } *result = cmd_buffer.data[0]; return TRUE; } gboolean fu_dell_dock_hid_i2c_write(FuDevice *self, const guint8 *input, gsize write_size, const FuHIDI2CParameters *parameters, GError **error) { FuHIDCmdBuffer cmd_buffer = { .cmd = HUB_CMD_WRITE_DATA, .ext = HUB_EXT_I2C_WRITE, .dwregaddr = 0, .bufferlen = GUINT16_TO_LE(write_size), /* nocheck:blocked */ .parameters = {.i2ctargetaddr = parameters->i2ctargetaddr, .regaddrlen = 0, .i2cspeed = parameters->i2cspeed | 0x80}, .extended_cmdarea[0 ... 52] = 0, }; g_return_val_if_fail(write_size <= HIDI2C_MAX_WRITE, FALSE); memcpy(cmd_buffer.data, input, write_size); /* nocheck:blocked */ return fu_dell_dock_hid_set_report(self, (guint8 *)&cmd_buffer, error); } gboolean fu_dell_dock_hid_i2c_read(FuDevice *self, guint32 cmd, gsize read_size, GBytes **bytes, const FuHIDI2CParameters *parameters, GError **error) { FuHIDCmdBuffer cmd_buffer = { .cmd = HUB_CMD_WRITE_DATA, .ext = HUB_EXT_I2C_READ, .dwregaddr = GUINT32_TO_LE(cmd), /* nocheck:blocked */ .bufferlen = GUINT16_TO_LE(read_size), /* nocheck:blocked */ .parameters = {.i2ctargetaddr = parameters->i2ctargetaddr, .regaddrlen = parameters->regaddrlen, .i2cspeed = parameters->i2cspeed | 0x80}, .extended_cmdarea[0 ... 52] = 0, .data[0 ... 191] = 0, }; g_return_val_if_fail(read_size <= HIDI2C_MAX_READ, FALSE); g_return_val_if_fail(bytes != NULL, FALSE); g_return_val_if_fail(parameters->regaddrlen < HIDI2C_MAX_REGISTER, FALSE); if (!fu_dell_dock_hid_set_report(self, (guint8 *)&cmd_buffer, error)) return FALSE; if (!fu_dell_dock_hid_get_report(self, cmd_buffer.data, error)) return FALSE; *bytes = g_bytes_new(cmd_buffer.data, read_size); return TRUE; } gboolean fu_dell_dock_hid_tbt_wake(FuDevice *self, const FuHIDI2CParameters *parameters, GError **error) { FuTbtCmdBuffer cmd_buffer = { .cmd = HUB_CMD_READ_DATA, /* special write command that reads status result */ .ext = HUB_EXT_WRITE_TBT_FLASH, .i2ctargetaddr = parameters->i2ctargetaddr, .i2cspeed = parameters->i2cspeed, /* unlike other commands doesn't need | 0x80 */ .tbt_command = TBT_COMMAND_WAKEUP, .bufferlen = 0, .extended_cmdarea[0 ... 53] = 0, .data[0 ... 191] = 0, }; if (!fu_dell_dock_hid_set_report(self, (guint8 *)&cmd_buffer, error)) { g_prefix_error(error, "failed to set wake thunderbolt: "); return FALSE; } if (!fu_dell_dock_hid_get_report(self, cmd_buffer.data, error)) { g_prefix_error(error, "failed to get wake thunderbolt status: "); return FALSE; } g_debug("thunderbolt wake result: 0x%x", cmd_buffer.data[1]); return TRUE; } static const gchar * fu_dell_dock_hid_tbt_map_error(guint32 code) { if (code == 1) return g_strerror(EINVAL); if (code == 2) return g_strerror(EPERM); return g_strerror(EIO); } gboolean fu_dell_dock_hid_tbt_write(FuDevice *self, guint32 start_addr, const guint8 *input, gsize write_size, const FuHIDI2CParameters *parameters, GError **error) { FuTbtCmdBuffer cmd_buffer = { .cmd = HUB_CMD_READ_DATA, /* It's a special write command that reads status result */ .ext = HUB_EXT_WRITE_TBT_FLASH, .i2ctargetaddr = parameters->i2ctargetaddr, .i2cspeed = parameters->i2cspeed, /* unlike other commands doesn't need | 0x80 */ .startaddress = GUINT32_TO_LE(start_addr), /* nocheck:blocked */ .bufferlen = write_size, .extended_cmdarea[0 ... 53] = 0, }; guint8 result; g_return_val_if_fail(input != NULL, FALSE); g_return_val_if_fail(write_size <= HIDI2C_MAX_WRITE, FALSE); memcpy(cmd_buffer.data, input, write_size); /* nocheck:blocked */ for (gint i = 1; i <= TBT_MAX_RETRIES; i++) { if (!fu_dell_dock_hid_set_report(self, (guint8 *)&cmd_buffer, error)) { g_prefix_error(error, "failed to run TBT update: "); return FALSE; } if (!fu_dell_dock_hid_get_report(self, cmd_buffer.data, error)) { g_prefix_error(error, "failed to get TBT flash status: "); return FALSE; } result = cmd_buffer.data[1] & 0xf; if (result == 0) break; g_debug("attempt %d/%d: Thunderbolt write failed: %x", i, TBT_MAX_RETRIES, result); } if (result != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Writing address 0x%04x failed: %s", start_addr, fu_dell_dock_hid_tbt_map_error(result)); return FALSE; } return TRUE; } gboolean fu_dell_dock_hid_tbt_authenticate(FuDevice *self, const FuHIDI2CParameters *parameters, GError **error) { FuTbtCmdBuffer cmd_buffer = { .cmd = HUB_CMD_READ_DATA, /* It's a special write command that reads status result */ .ext = HUB_EXT_WRITE_TBT_FLASH, .i2ctargetaddr = parameters->i2ctargetaddr, .i2cspeed = parameters->i2cspeed, /* unlike other commands doesn't need | 0x80 */ .tbt_command = GUINT32_TO_LE(TBT_COMMAND_AUTHENTICATE), /* nocheck:blocked */ .bufferlen = 0, .extended_cmdarea[0 ... 53] = 0, }; guint8 result; if (!fu_dell_dock_hid_set_report(self, (guint8 *)&cmd_buffer, error)) { g_prefix_error(error, "failed to send authentication: "); return FALSE; } cmd_buffer.tbt_command = GUINT32_TO_LE(TBT_COMMAND_AUTHENTICATE_STATUS); /* nocheck:blocked */ /* needs at least 2 seconds */ fu_device_sleep(self, 2000); for (gint i = 1; i <= TBT_MAX_RETRIES; i++) { if (!fu_dell_dock_hid_set_report(self, (guint8 *)&cmd_buffer, error)) { g_prefix_error(error, "failed to set check authentication: "); return FALSE; } if (!fu_dell_dock_hid_get_report(self, cmd_buffer.data, error)) { g_prefix_error(error, "failed to get check authentication: "); return FALSE; } result = cmd_buffer.data[1] & 0xf; if (result == 0) break; g_debug("attempt %d/%d: Thunderbolt authenticate failed: %x", i, TBT_MAX_RETRIES, result); fu_device_sleep(self, 500); /* ms */ } if (result != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_AUTH_FAILED, "thunderbolt authentication failed: %s", fu_dell_dock_hid_tbt_map_error(result)); return FALSE; } return TRUE; } fwupd-2.0.10/plugins/dell-dock/fu-dell-dock-hid.h000066400000000000000000000042261501337203100214160ustar00rootroot00000000000000/* * Copyright 2018 Realtek Semiconductor Corporation * Copyright 2018 Dell Inc. * All rights reserved. * * This software and associated documentation (if any) is furnished * under a license and may only be used or copied in accordance * with the terms of the license. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * Dell Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include typedef struct __attribute__((packed)) { /* nocheck:blocked */ guint8 i2ctargetaddr; guint8 regaddrlen; guint8 i2cspeed; } FuHIDI2CParameters; typedef enum { I2C_SPEED_250K, I2C_SPEED_400K, I2C_SPEED_800K, /* */ I2C_SPEED_LAST, } BridgedI2CSpeed; #define HIDI2C_MAX_READ 192 #define HIDI2C_MAX_WRITE 128 gboolean fu_dell_dock_hid_i2c_write(FuDevice *self, const guint8 *input, gsize write_size, const FuHIDI2CParameters *parameters, GError **error); gboolean fu_dell_dock_hid_i2c_read(FuDevice *self, guint32 cmd, gsize read_size, GBytes **bytes, const FuHIDI2CParameters *parameters, GError **error); gboolean fu_dell_dock_hid_get_hub_version(FuDevice *self, GError **error); gboolean fu_dell_dock_hid_raise_mcu_clock(FuDevice *self, gboolean enable, GError **error); gboolean fu_dell_dock_hid_erase_bank(FuDevice *self, guint8 idx, GError **error); gboolean fu_dell_dock_hid_write_flash(FuDevice *self, guint32 addr, const guint8 *input, gsize write_size, GError **error); gboolean fu_dell_dock_hid_verify_update(FuDevice *self, gboolean *result, GError **error); gboolean fu_dell_dock_hid_tbt_wake(FuDevice *self, const FuHIDI2CParameters *parameters, GError **error); gboolean fu_dell_dock_hid_tbt_write(FuDevice *self, guint32 start_addr, const guint8 *input, gsize write_size, const FuHIDI2CParameters *parameters, GError **error); gboolean fu_dell_dock_hid_tbt_authenticate(FuDevice *self, const FuHIDI2CParameters *parameters, GError **error); fwupd-2.0.10/plugins/dell-dock/fu-dell-dock-hub.c000066400000000000000000000150271501337203100214240ustar00rootroot00000000000000/* * Copyright 2018 Dell Inc. * All rights reserved. * * This software and associated documentation (if any) is furnished * under a license and may only be used or copied in accordance * with the terms of the license. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * Dell Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-dell-dock-common.h" struct _FuDellDockHub { FuHidDevice parent_instance; guint8 unlock_target; guint64 blob_major_offset; guint64 blob_minor_offset; }; G_DEFINE_TYPE(FuDellDockHub, fu_dell_dock_hub, FU_TYPE_HID_DEVICE) void fu_dell_dock_hub_add_instance(FuDevice *device, guint8 dock_type) { g_autofree gchar *devid = NULL; if (dock_type == DOCK_BASE_TYPE_ATOMIC) { devid = g_strdup_printf("USB\\VID_%04X&PID_%04X&atomic_hub", (guint)fu_device_get_vid(device), (guint)fu_device_get_pid(device)); } else { devid = g_strdup_printf("USB\\VID_%04X&PID_%04X&hub", (guint)fu_device_get_vid(device), (guint)fu_device_get_pid(device)); } fu_device_add_instance_id(device, devid); } static gboolean fu_dell_dock_hub_probe(FuDevice *device, GError **error) { fu_device_set_logical_id(device, "hub"); fu_device_add_protocol(device, "com.dell.dock"); return TRUE; } static gboolean fu_dell_dock_hub_write_fw(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDellDockHub *self = FU_DELL_DOCK_HUB(device); gsize fw_size = 0; const guint8 *data; gsize write_size; gsize nwritten = 0; guint32 address = 0; gboolean result = FALSE; g_autofree gchar *dynamic_version = NULL; g_autoptr(GBytes) fw = NULL; g_return_val_if_fail(device != NULL, FALSE); g_return_val_if_fail(FU_IS_FIRMWARE(firmware), FALSE); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 49, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 50, NULL); /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; data = g_bytes_get_data(fw, &fw_size); write_size = (fw_size / HIDI2C_MAX_WRITE) >= 1 ? HIDI2C_MAX_WRITE : fw_size; dynamic_version = g_strdup_printf("%02x.%02x", data[self->blob_major_offset], data[self->blob_minor_offset]); g_info("writing hub firmware version %s", dynamic_version); if (!fu_dell_dock_set_power(device, self->unlock_target, TRUE, error)) return FALSE; if (!fu_dell_dock_hid_raise_mcu_clock(device, TRUE, error)) return FALSE; /* erase */ if (!fu_dell_dock_hid_erase_bank(device, 1, error)) return FALSE; fu_progress_step_done(progress); /* write */ do { /* last packet */ if (fw_size - nwritten < write_size) write_size = fw_size - nwritten; if (!fu_dell_dock_hid_write_flash(device, address, data, write_size, error)) return FALSE; nwritten += write_size; data += write_size; address += write_size; fu_progress_set_percentage_full(fu_progress_get_child(progress), nwritten, fw_size); } while (nwritten < fw_size); fu_progress_step_done(progress); /* verify */ if (!fu_dell_dock_hid_verify_update(device, &result, error)) return FALSE; if (!result) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to verify the update"); return FALSE; } fu_progress_step_done(progress); /* dock will reboot to re-read; this is to appease the daemon */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_PAIR); fu_device_set_version(device, dynamic_version); return TRUE; } static gboolean fu_dell_dock_hub_setup(FuDevice *device, GError **error) { /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_dell_dock_hub_parent_class)->setup(device, error)) return FALSE; /* skip version setup here as we don't know HID header format yet */ if (fu_device_has_private_flag(device, FU_DELL_DOCK_HUB_FLAG_HAS_BRIDGE)) return TRUE; return fu_dell_dock_hid_get_hub_version(device, error); } static gboolean fu_dell_dock_hub_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuDellDockHub *self = FU_DELL_DOCK_HUB(device); guint64 tmp = 0; if (g_strcmp0(key, "DellDockUnlockTarget") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->unlock_target = tmp; return TRUE; } if (g_strcmp0(key, "DellDockBlobMajorOffset") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->blob_major_offset = tmp; return TRUE; } if (g_strcmp0(key, "DellDockBlobMinorOffset") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->blob_minor_offset = tmp; return TRUE; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_dell_dock_hub_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_dell_dock_hub_init(FuDellDockHub *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_retry_set_delay(FU_DEVICE(self), 1000); fu_device_register_private_flag(FU_DEVICE(self), FU_DELL_DOCK_HUB_FLAG_HAS_BRIDGE); } static void fu_dell_dock_hub_class_init(FuDellDockHubClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_dell_dock_hub_setup; device_class->probe = fu_dell_dock_hub_probe; device_class->write_firmware = fu_dell_dock_hub_write_fw; device_class->set_quirk_kv = fu_dell_dock_hub_set_quirk_kv; device_class->set_progress = fu_dell_dock_hub_set_progress; } FuDellDockHub * fu_dell_dock_hub_new(FuUsbDevice *device) { FuDellDockHub *self = g_object_new(FU_TYPE_DELL_DOCK_HUB, NULL); fu_device_incorporate(FU_DEVICE(self), FU_DEVICE(device), FU_DEVICE_INCORPORATE_FLAG_ALL); return self; } fwupd-2.0.10/plugins/dell-dock/fu-dell-dock-hub.h000066400000000000000000000017221501337203100214260ustar00rootroot00000000000000/* * Copyright 2018 Dell Inc. * All rights reserved. * * This software and associated documentation (if any) is furnished * under a license and may only be used or copied in accordance * with the terms of the license. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * Dell Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_DELL_DOCK_HUB (fu_dell_dock_hub_get_type()) G_DECLARE_FINAL_TYPE(FuDellDockHub, fu_dell_dock_hub, FU, DELL_DOCK_HUB, FuHidDevice) /** * FU_DELL_DOCK_HUB_FLAG_HAS_BRIDGE: * * A bridge is present, possibly with extended devices. */ #define FU_DELL_DOCK_HUB_FLAG_HAS_BRIDGE "has-bridge" FuDellDockHub * fu_dell_dock_hub_new(FuUsbDevice *device); void fu_dell_dock_hub_add_instance(FuDevice *device, guint8 dock_type); fwupd-2.0.10/plugins/dell-dock/fu-dell-dock-mst.c000066400000000000000000001105721501337203100214520ustar00rootroot00000000000000/* * Copyright 2018 Synaptics * Copyright 2018 Dell Inc. * All rights reserved. * * This software and associated documentation (if any) is furnished * under a license and may only be used or copied in accordance * with the terms of the license. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * Dell Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include #include "fu-dell-dock-common.h" #define I2C_MST_ADDRESS 0x72 /* Panamera MST registers */ #define PANAMERA_MST_RC_TRIGGER_ADDR 0x2000fc #define PANAMERA_MST_CORE_MCU_BOOTLOADER_STS 0x20010c #define PANAMERA_MST_RC_COMMAND_ADDR 0x200110 #define PANAMERA_MST_RC_OFFSET_ADDR 0x200114 #define PANAMERA_MST_RC_LENGTH_ADDR 0x200118 #define PANAMERA_MST_RC_DATA_ADDR 0x200120 #define PANAMERA_MST_CORE_MCU_FW_VERSION 0x200160 #define PANAMERA_MST_REG_QUAD_DISABLE 0x200fc0 #define PANAMERA_MST_REG_HDCP22_DISABLE 0x200f90 /* Cayenne MST registers */ #define CAYENNE_MST_RC_TRIGGER_ADDR 0x2020021C #define CAYENNE_MST_CORE_MCU_BOOTLOADER_STS 0x2020022C #define CAYENNE_MST_RC_COMMAND_ADDR 0x20200280 #define CAYENNE_MST_RC_OFFSET_ADDR 0x20200284 #define CAYENNE_MST_RC_LENGTH_ADDR 0x20200288 #define CAYENNE_MST_RC_DATA_ADDR 0x20200290 /* MST remote control commands */ #define MST_CMD_ENABLE_REMOTE_CONTROL 0x1 #define MST_CMD_DISABLE_REMOTE_CONTROL 0x2 #define MST_CMD_CHECKSUM 0x11 #define MST_CMD_ERASE_FLASH 0x14 #define MST_CMD_WRITE_FLASH 0x20 #define MST_CMD_READ_FLASH 0x30 #define MST_CMD_WRITE_MEMORY 0x21 #define MST_CMD_READ_MEMORY 0x31 /* Cayenne specific remote control commands */ #define MST_CMD_CRC16_CHECKSUM 0x17 #define MST_CMD_ACTIVATE_FW 0x18 /* Arguments related to flashing */ #define FLASH_SECTOR_ERASE_4K 0x1000 #define FLASH_SECTOR_ERASE_32K 0x2000 #define FLASH_SECTOR_ERASE_64K 0x3000 #define EEPROM_TAG_OFFSET 0x1fff0 #define EEPROM_BANK_OFFSET 0x20000 #define EEPROM_ESM_OFFSET 0x40000 /* Flash offsets */ #define MST_BOARDID_OFFSET 0x10e /* Remote control offsets */ #define MST_CHIPID_OFFSET 0x1500 /* magic triggers */ #define MST_TRIGGER_WRITE 0xf2 #define MST_TRIGGER_REBOOT 0xf5 /* IDs used in DELL_DOCK */ #define EXPECTED_CHIPID 0x5331 /* firmware file offsets */ #define MST_BLOB_VERSION_OFFSET 0x06F0 typedef enum { Panamera_mst, Cayenne_mst, Unknown, } MSTType; typedef enum { Bank0, Bank1, ESM, Cayenne, } MSTBank; typedef struct { guint start; guint length; guint checksum_cmd; } MSTBankAttributes; const MSTBankAttributes bank0_attributes = { .start = 0, .length = EEPROM_BANK_OFFSET, .checksum_cmd = MST_CMD_CHECKSUM, }; const MSTBankAttributes bank1_attributes = { .start = EEPROM_BANK_OFFSET, .length = EEPROM_BANK_OFFSET, .checksum_cmd = MST_CMD_CHECKSUM, }; const MSTBankAttributes esm_attributes = { .start = EEPROM_ESM_OFFSET, .length = 0x3ffff, .checksum_cmd = MST_CMD_CHECKSUM, }; const MSTBankAttributes cayenne_attributes = { .start = 0, .length = 0x50000, .checksum_cmd = MST_CMD_CRC16_CHECKSUM, }; FuHIDI2CParameters mst_base_settings = { .i2ctargetaddr = I2C_MST_ADDRESS, .regaddrlen = 0, .i2cspeed = I2C_SPEED_400K, }; struct _FuDellDockMst { FuDevice parent_instance; guint8 unlock_target; guint64 blob_major_offset; guint64 blob_minor_offset; guint64 blob_build_offset; guint32 mst_rc_trigger_addr; guint32 mst_rc_command_addr; guint32 mst_rc_data_addr; guint32 mst_core_mcu_bootloader_addr; guint8 dock_type; MSTType mst_type; }; G_DEFINE_TYPE(FuDellDockMst, fu_dell_dock_mst, FU_TYPE_DEVICE) /** * fu_dell_dock_mst_get_bank_attribs: * @bank: the MSTBank * @out (out): the MSTBankAttributes attribute that matches * @error: (nullable): optional return location for an error * * Returns a structure that corresponds to the attributes for a bank * * Returns: %TRUE for success **/ static gboolean fu_dell_dock_mst_get_bank_attribs(MSTBank bank, const MSTBankAttributes **out, GError **error) { switch (bank) { case Bank0: *out = &bank0_attributes; break; case Bank1: *out = &bank1_attributes; break; case ESM: *out = &esm_attributes; break; case Cayenne: *out = &cayenne_attributes; break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Invalid bank specified %u", bank); return FALSE; } return TRUE; } static gboolean fu_dell_dock_mst_rc_command(FuDevice *device, guint8 cmd, guint32 length, guint32 offset, const guint8 *data, GError **error); static gboolean fu_dell_dock_mst_read_register(FuDevice *proxy, guint32 address, gsize length, GBytes **bytes, GError **error) { g_return_val_if_fail(proxy != NULL, FALSE); g_return_val_if_fail(bytes != NULL, FALSE); g_return_val_if_fail(length <= 32, FALSE); /* write the offset we're querying */ if (!fu_dell_dock_hid_i2c_write(proxy, (guint8 *)&address, 4, &mst_base_settings, error)) return FALSE; /* read data for the result */ if (!fu_dell_dock_hid_i2c_read(proxy, 0, length, bytes, &mst_base_settings, error)) return FALSE; return TRUE; } static gboolean fu_dell_dock_mst_write_register(FuDevice *proxy, guint32 address, guint8 *data, gsize length, GError **error) { g_autofree guint8 *buffer = g_malloc0(length + 4); g_return_val_if_fail(proxy != NULL, FALSE); g_return_val_if_fail(data != NULL, FALSE); memcpy(buffer, &address, 4); /* nocheck:blocked */ memcpy(buffer + 4, data, length); /* nocheck:blocked */ /* write the offset we're querying */ return fu_dell_dock_hid_i2c_write(proxy, buffer, length + 4, &mst_base_settings, error); } static gboolean fu_dell_dock_mst_query_active_bank(FuDevice *proxy, MSTBank *active, GError **error) { g_autoptr(GBytes) bytes = NULL; const guint32 *data = NULL; gsize length = 4; if (!fu_dell_dock_mst_read_register(proxy, PANAMERA_MST_CORE_MCU_BOOTLOADER_STS, length, &bytes, error)) { g_prefix_error(error, "Failed to query active bank: "); return FALSE; } data = g_bytes_get_data(bytes, &length); if ((data[0] & (1 << 7)) || (data[0] & (1 << 30))) *active = Bank1; else *active = Bank0; g_debug("MST: active bank is: %u", *active); return TRUE; } static gboolean fu_dell_dock_mst_disable_remote_control(FuDevice *device, GError **error) { g_debug("MST: Disabling remote control"); return fu_dell_dock_mst_rc_command(device, MST_CMD_DISABLE_REMOTE_CONTROL, 0, 0, NULL, error); } static gboolean fu_dell_dock_mst_enable_remote_control(FuDevice *device, GError **error) { g_autoptr(GError) error_local = NULL; const gchar *data = "PRIUS"; g_debug("MST: Enabling remote control"); if (!fu_dell_dock_mst_rc_command(device, MST_CMD_ENABLE_REMOTE_CONTROL, 5, 0, (guint8 *)data, &error_local)) { g_debug("Failed to enable remote control: %s", error_local->message); /* try to disable / re-enable */ if (!fu_dell_dock_mst_disable_remote_control(device, error)) return FALSE; return fu_dell_dock_mst_enable_remote_control(device, error); } return TRUE; } static gboolean fu_dell_dock_mst_trigger_rc_command(FuDevice *device, GError **error) { const guint8 *result = NULL; FuDevice *proxy = fu_device_get_proxy(device); FuDellDockMst *self = FU_DELL_DOCK_MST(device); guint32 tmp; /* Trigger the write */ tmp = MST_TRIGGER_WRITE; if (!fu_dell_dock_mst_write_register(proxy, self->mst_rc_trigger_addr, (guint8 *)&tmp, sizeof(guint32), error)) { g_prefix_error(error, "Failed to write MST_RC_TRIGGER_ADDR: "); return FALSE; } /* poll for completion */ tmp = 0xffff; for (guint i = 0; i < 1000; i++) { g_autoptr(GBytes) bytes = NULL; if (!fu_dell_dock_mst_read_register(proxy, self->mst_rc_command_addr, sizeof(guint32), &bytes, error)) { g_prefix_error(error, "Failed to poll MST_RC_COMMAND_ADDR: "); return FALSE; } result = g_bytes_get_data(bytes, NULL); /* complete */ if ((result[2] & 0x80) == 0) { tmp = result[3]; break; } fu_device_sleep(FU_DEVICE(self), 2); /* ms */ } switch (tmp) { /* need to enable remote control */ case 4: return fu_dell_dock_mst_enable_remote_control(device, error); /* error scenarios */ case 3: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Unknown error"); return FALSE; case 2: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Unsupported command"); return FALSE; case 1: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "Invalid argument"); return FALSE; /* success scenario */ case 0: return TRUE; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "command timed out or unknown failure: 0x%x", tmp); return FALSE; } } static gboolean fu_dell_dock_mst_rc_command(FuDevice *device, guint8 cmd, guint32 length, guint32 offset, const guint8 *data, GError **error) { /* 4 for cmd, 4 for offset, 4 for length, 4 for garbage */ FuDellDockMst *self = FU_DELL_DOCK_MST(device); FuDevice *proxy = fu_device_get_proxy(device); gint buffer_len = (data == NULL) ? 12 : length + 16; g_autofree guint8 *buffer = g_malloc0(buffer_len); guint32 tmp; g_return_val_if_fail(proxy != NULL, FALSE); /* command */ tmp = (cmd | 0x80) << 16; memcpy(buffer, &tmp, 4); /* nocheck:blocked */ /* offset */ memcpy(buffer + 4, &offset, 4); /* nocheck:blocked */ /* length */ memcpy(buffer + 8, &length, 4); /* nocheck:blocked */ /* data */ if (data != NULL) memcpy(buffer + 16, data, length); /* nocheck:blocked */ /* write the combined register stream */ if (!fu_dell_dock_mst_write_register(proxy, self->mst_rc_command_addr, buffer, buffer_len, error)) return FALSE; return fu_dell_dock_mst_trigger_rc_command(device, error); } static MSTType fu_dell_dock_mst_get_module_type(FuDevice *device) { FuDellDockMst *self = FU_DELL_DOCK_MST(device); return self->mst_type; } static gboolean fu_dell_dock_mst_check_offset(guint8 byte, guint8 offset) { if ((byte & offset) != 0) return TRUE; return FALSE; } static gboolean fu_dell_dock_mst_d19_check_fw(FuDevice *device, GError **error) { FuDellDockMst *self = FU_DELL_DOCK_MST(device); g_autoptr(GBytes) bytes = NULL; const guint8 *data; gsize length = 4; if (!fu_dell_dock_mst_read_register(fu_device_get_proxy(device), self->mst_core_mcu_bootloader_addr, length, &bytes, error)) return FALSE; data = g_bytes_get_data(bytes, &length); g_debug("MST: firmware check: %d", fu_dell_dock_mst_check_offset(data[0], 0x01)); g_debug("MST: HDCP key check: %d", fu_dell_dock_mst_check_offset(data[0], 0x02)); g_debug("MST: Config0 check: %d", fu_dell_dock_mst_check_offset(data[0], 0x04)); g_debug("MST: Config1 check: %d", fu_dell_dock_mst_check_offset(data[0], 0x08)); if (fu_dell_dock_mst_check_offset(data[0], 0xF0)) g_debug("MST: running in bootloader"); else g_debug("MST: running in firmware"); g_debug("MST: Error code: %x", data[1]); g_debug("MST: GPIO boot strap record: %d", data[2]); g_debug("MST: Bootloader version number %x", data[3]); return TRUE; } static guint16 fu_dell_dock_mst_get_crc(guint8 type, guint32 length, const guint8 *payload_data) { static const guint16 CRC16_table[] = { 0x0000, 0x8005, 0x800f, 0x000a, 0x801b, 0x001e, 0x0014, 0x8011, 0x8033, 0x0036, 0x003c, 0x8039, 0x0028, 0x802d, 0x8027, 0x0022, 0x8063, 0x0066, 0x006c, 0x8069, 0x0078, 0x807d, 0x8077, 0x0072, 0x0050, 0x8055, 0x805f, 0x005a, 0x804b, 0x004e, 0x0044, 0x8041, 0x80c3, 0x00c6, 0x00cc, 0x80c9, 0x00d8, 0x80dd, 0x80d7, 0x00d2, 0x00f0, 0x80f5, 0x80ff, 0x00fa, 0x80eb, 0x00ee, 0x00e4, 0x80e1, 0x00a0, 0x80a5, 0x80af, 0x00aa, 0x80bb, 0x00be, 0x00b4, 0x80b1, 0x8093, 0x0096, 0x009c, 0x8099, 0x0088, 0x808d, 0x8087, 0x0082, 0x8183, 0x0186, 0x018c, 0x8189, 0x0198, 0x819d, 0x8197, 0x0192, 0x01b0, 0x81b5, 0x81bf, 0x01ba, 0x81ab, 0x01ae, 0x01a4, 0x81a1, 0x01e0, 0x81e5, 0x81ef, 0x01ea, 0x81fb, 0x01fe, 0x01f4, 0x81f1, 0x81d3, 0x01d6, 0x01dc, 0x81d9, 0x01c8, 0x81cd, 0x81c7, 0x01c2, 0x0140, 0x8145, 0x814f, 0x014a, 0x815b, 0x015e, 0x0154, 0x8151, 0x8173, 0x0176, 0x017c, 0x8179, 0x0168, 0x816d, 0x8167, 0x0162, 0x8123, 0x0126, 0x012c, 0x8129, 0x0138, 0x813d, 0x8137, 0x0132, 0x0110, 0x8115, 0x811f, 0x011a, 0x810b, 0x010e, 0x0104, 0x8101, 0x8303, 0x0306, 0x030c, 0x8309, 0x0318, 0x831d, 0x8317, 0x0312, 0x0330, 0x8335, 0x833f, 0x033a, 0x832b, 0x032e, 0x0324, 0x8321, 0x0360, 0x8365, 0x836f, 0x036a, 0x837b, 0x037e, 0x0374, 0x8371, 0x8353, 0x0356, 0x035c, 0x8359, 0x0348, 0x834d, 0x8347, 0x0342, 0x03c0, 0x83c5, 0x83cf, 0x03ca, 0x83db, 0x03de, 0x03d4, 0x83d1, 0x83f3, 0x03f6, 0x03fc, 0x83f9, 0x03e8, 0x83ed, 0x83e7, 0x03e2, 0x83a3, 0x03a6, 0x03ac, 0x83a9, 0x03b8, 0x83bd, 0x83b7, 0x03b2, 0x0390, 0x8395, 0x839f, 0x039a, 0x838b, 0x038e, 0x0384, 0x8381, 0x0280, 0x8285, 0x828f, 0x028a, 0x829b, 0x029e, 0x0294, 0x8291, 0x82b3, 0x02b6, 0x02bc, 0x82b9, 0x02a8, 0x82ad, 0x82a7, 0x02a2, 0x82e3, 0x02e6, 0x02ec, 0x82e9, 0x02f8, 0x82fd, 0x82f7, 0x02f2, 0x02d0, 0x82d5, 0x82df, 0x02da, 0x82cb, 0x02ce, 0x02c4, 0x82c1, 0x8243, 0x0246, 0x024c, 0x8249, 0x0258, 0x825d, 0x8257, 0x0252, 0x0270, 0x8275, 0x827f, 0x027a, 0x826b, 0x026e, 0x0264, 0x8261, 0x0220, 0x8225, 0x822f, 0x022a, 0x823b, 0x023e, 0x0234, 0x8231, 0x8213, 0x0216, 0x021c, 0x8219, 0x0208, 0x820d, 0x8207, 0x0202}; static const guint16 CRC8_table[] = { 0x00, 0xd5, 0x7f, 0xaa, 0xfe, 0x2b, 0x81, 0x54, 0x29, 0xfc, 0x56, 0x83, 0xd7, 0x02, 0xa8, 0x7d, 0x52, 0x87, 0x2d, 0xf8, 0xac, 0x79, 0xd3, 0x06, 0x7b, 0xae, 0x04, 0xd1, 0x85, 0x50, 0xfa, 0x2f, 0xa4, 0x71, 0xdb, 0x0e, 0x5a, 0x8f, 0x25, 0xf0, 0x8d, 0x58, 0xf2, 0x27, 0x73, 0xa6, 0x0c, 0xd9, 0xf6, 0x23, 0x89, 0x5c, 0x08, 0xdd, 0x77, 0xa2, 0xdf, 0x0a, 0xa0, 0x75, 0x21, 0xf4, 0x5e, 0x8b, 0x9d, 0x48, 0xe2, 0x37, 0x63, 0xb6, 0x1c, 0xc9, 0xb4, 0x61, 0xcb, 0x1e, 0x4a, 0x9f, 0x35, 0xe0, 0xcf, 0x1a, 0xb0, 0x65, 0x31, 0xe4, 0x4e, 0x9b, 0xe6, 0x33, 0x99, 0x4c, 0x18, 0xcd, 0x67, 0xb2, 0x39, 0xec, 0x46, 0x93, 0xc7, 0x12, 0xb8, 0x6d, 0x10, 0xc5, 0x6f, 0xba, 0xee, 0x3b, 0x91, 0x44, 0x6b, 0xbe, 0x14, 0xc1, 0x95, 0x40, 0xea, 0x3f, 0x42, 0x97, 0x3d, 0xe8, 0xbc, 0x69, 0xc3, 0x16, 0xef, 0x3a, 0x90, 0x45, 0x11, 0xc4, 0x6e, 0xbb, 0xc6, 0x13, 0xb9, 0x6c, 0x38, 0xed, 0x47, 0x92, 0xbd, 0x68, 0xc2, 0x17, 0x43, 0x96, 0x3c, 0xe9, 0x94, 0x41, 0xeb, 0x3e, 0x6a, 0xbf, 0x15, 0xc0, 0x4b, 0x9e, 0x34, 0xe1, 0xb5, 0x60, 0xca, 0x1f, 0x62, 0xb7, 0x1d, 0xc8, 0x9c, 0x49, 0xe3, 0x36, 0x19, 0xcc, 0x66, 0xb3, 0xe7, 0x32, 0x98, 0x4d, 0x30, 0xe5, 0x4f, 0x9a, 0xce, 0x1b, 0xb1, 0x64, 0x72, 0xa7, 0x0d, 0xd8, 0x8c, 0x59, 0xf3, 0x26, 0x5b, 0x8e, 0x24, 0xf1, 0xa5, 0x70, 0xda, 0x0f, 0x20, 0xf5, 0x5f, 0x8a, 0xde, 0x0b, 0xa1, 0x74, 0x09, 0xdc, 0x76, 0xa3, 0xf7, 0x22, 0x88, 0x5d, 0xd6, 0x03, 0xa9, 0x7c, 0x28, 0xfd, 0x57, 0x82, 0xff, 0x2a, 0x80, 0x55, 0x01, 0xd4, 0x7e, 0xab, 0x84, 0x51, 0xfb, 0x2e, 0x7a, 0xaf, 0x05, 0xd0, 0xad, 0x78, 0xd2, 0x07, 0x53, 0x86, 0x2c, 0xf9}; guint8 val; guint16 crc = 0; const guint8 *message = payload_data; if (type == 8) { for (guint32 byte = 0; byte < length; ++byte) { val = (guint8)(message[byte] ^ crc); crc = CRC8_table[val]; } } else { for (guint32 byte = 0; byte < length; ++byte) { val = (guint8)(message[byte] ^ (crc >> 8)); crc = CRC16_table[val] ^ (crc << 8); } } return crc; } static gboolean fu_dell_dock_mst_checksum_bank(FuDevice *device, GBytes *blob_fw, MSTBank bank, gboolean *checksum, GError **error) { FuDellDockMst *self = FU_DELL_DOCK_MST(device); g_autoptr(GBytes) csum_bytes = NULL; const MSTBankAttributes *attribs = NULL; gsize length = 0; const guint8 *data = g_bytes_get_data(blob_fw, &length); guint32 payload_sum = 0; guint32 bank_sum = 0; g_return_val_if_fail(blob_fw != NULL, FALSE); g_return_val_if_fail(checksum != NULL, FALSE); if (!fu_dell_dock_mst_get_bank_attribs(bank, &attribs, error)) return FALSE; /* bank is specified outside of payload */ if (attribs->start + attribs->length > length) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "payload %u is bigger than bank %u", attribs->start + attribs->length, bank); return FALSE; } /* checksum the file */ if (attribs->checksum_cmd == MST_CMD_CRC16_CHECKSUM) payload_sum = fu_dell_dock_mst_get_crc(16, (attribs->length + attribs->start), data); else { for (guint i = attribs->start; i < attribs->length + attribs->start; i++) { payload_sum += data[i]; } } g_debug("MST: Payload checksum: 0x%x", payload_sum); /* checksum the bank */ if (!fu_dell_dock_mst_rc_command(device, attribs->checksum_cmd, attribs->length, attribs->start, NULL, error)) { g_prefix_error(error, "Failed to checksum bank %u: ", bank); return FALSE; } /* read result from data register */ if (!fu_dell_dock_mst_read_register(fu_device_get_proxy(device), self->mst_rc_data_addr, 4, &csum_bytes, error)) return FALSE; data = g_bytes_get_data(csum_bytes, NULL); bank_sum = GUINT32_FROM_LE(data[0] | data[1] << 8 | data[2] << 16 | /* nocheck:blocked */ data[3] << 24); g_debug("MST: Bank %u checksum: 0x%x", bank, bank_sum); *checksum = (bank_sum == payload_sum); return TRUE; } static gboolean fu_dell_dock_mst_erase_panamera_bank(FuDevice *device, MSTBank bank, GError **error) { const MSTBankAttributes *attribs = NULL; guint32 sector; if (!fu_dell_dock_mst_get_bank_attribs(bank, &attribs, error)) return FALSE; for (guint32 i = attribs->start; i < attribs->start + attribs->length; i += 0x10000) { sector = FLASH_SECTOR_ERASE_64K | (i / 0x10000); g_debug("MST: Erasing sector 0x%x", sector); if (!fu_dell_dock_mst_rc_command(device, MST_CMD_ERASE_FLASH, 4, 0, (guint8 *)§or, error)) { g_prefix_error(error, "Failed to erase sector 0x%x: ", sector); return FALSE; } } g_debug("MST: Waiting for flash clear to settle"); fu_device_sleep(device, 5000); /* ms */ return TRUE; } static gboolean fu_dell_dock_mst_erase_cayenne(FuDevice *device, GError **error) { guint8 data[4] = {0, 0x30, 0, 0}; for (guint8 i = 0; i < 5; i++) { data[0] = i; if (!fu_dell_dock_mst_rc_command(device, MST_CMD_ERASE_FLASH, 4, 0, (guint8 *)&data, error)) { g_prefix_error(error, "Failed to erase sector: %d", i); return FALSE; } } g_debug("MST: Waiting for flash clear to settle"); fu_device_sleep(device, 5000); return TRUE; } static gboolean fu_dell_dock_mst_write_flash_bank(FuDevice *device, GBytes *blob_fw, MSTBank bank, FuProgress *progress, GError **error) { const MSTBankAttributes *attribs = NULL; gsize write_size = 32; guint end; const guint8 *data = g_bytes_get_data(blob_fw, NULL); g_return_val_if_fail(blob_fw != NULL, FALSE); if (!fu_dell_dock_mst_get_bank_attribs(bank, &attribs, error)) return FALSE; end = attribs->start + attribs->length; g_debug("MST: Writing payload to bank %u", bank); for (guint i = attribs->start; i < end; i += write_size) { if (!fu_dell_dock_mst_rc_command(device, MST_CMD_WRITE_FLASH, write_size, i, data + i, error)) { g_prefix_error(error, "Failed to write bank %u payload offset 0x%x: ", bank, i); return FALSE; } fu_progress_set_percentage_full(progress, i - attribs->start, end - attribs->start); } return TRUE; } static gboolean fu_dell_dock_mst_stop_esm(FuDevice *device, GError **error) { g_autoptr(GBytes) quad_bytes = NULL; g_autoptr(GBytes) hdcp_bytes = NULL; guint32 payload = 0x21; gsize length = sizeof(guint32); const guint8 *data; guint8 data_out[sizeof(guint32)]; /* disable ESM first */ if (!fu_dell_dock_mst_rc_command(device, MST_CMD_WRITE_MEMORY, length, PANAMERA_MST_RC_TRIGGER_ADDR, (guint8 *)&payload, error)) return FALSE; /* waiting for ESM exit */ fu_device_sleep(device, 1); /* disable QUAD mode */ if (!fu_dell_dock_mst_rc_command(device, MST_CMD_READ_MEMORY, length, PANAMERA_MST_REG_QUAD_DISABLE, NULL, error)) return FALSE; if (!fu_dell_dock_mst_read_register(fu_device_get_proxy(device), PANAMERA_MST_RC_DATA_ADDR, length, &quad_bytes, error)) return FALSE; data = g_bytes_get_data(quad_bytes, &length); memcpy(data_out, data, length); /* nocheck:blocked */ data_out[0] = 0x00; if (!fu_dell_dock_mst_rc_command(device, MST_CMD_WRITE_MEMORY, length, PANAMERA_MST_REG_QUAD_DISABLE, data_out, error)) return FALSE; /* disable HDCP2.2 */ if (!fu_dell_dock_mst_rc_command(device, MST_CMD_READ_MEMORY, length, PANAMERA_MST_REG_HDCP22_DISABLE, NULL, error)) return FALSE; if (!fu_dell_dock_mst_read_register(fu_device_get_proxy(device), PANAMERA_MST_RC_DATA_ADDR, length, &hdcp_bytes, error)) return FALSE; data = g_bytes_get_data(hdcp_bytes, &length); memcpy(data_out, data, length); /* nocheck:blocked */ data_out[0] = data[0] & (1 << 2); if (!fu_dell_dock_mst_rc_command(device, MST_CMD_WRITE_MEMORY, length, PANAMERA_MST_REG_HDCP22_DISABLE, data_out, error)) return FALSE; return TRUE; } static gboolean fu_dell_dock_mst_invalidate_bank(FuDevice *device, MSTBank bank_in_use, GError **error) { const MSTBankAttributes *attribs; g_autoptr(GBytes) bytes = NULL; const guint8 *crc_tag; const guint8 *new_tag; guint32 crc_offset; guint retries = 2; if (!fu_dell_dock_mst_get_bank_attribs(bank_in_use, &attribs, error)) { g_prefix_error(error, "unable to invalidate bank: "); return FALSE; } /* we need to write 4 byte increments over I2C so this differs from DP aux */ crc_offset = attribs->start + EEPROM_TAG_OFFSET + 12; /* Read CRC byte to flip */ if (!fu_dell_dock_mst_rc_command(device, MST_CMD_READ_FLASH, 4, crc_offset, NULL, error)) { g_prefix_error(error, "failed to read tag from flash: "); return FALSE; } if (!fu_dell_dock_mst_read_register(fu_device_get_proxy(device), PANAMERA_MST_RC_DATA_ADDR, 1, &bytes, error)) { return FALSE; } crc_tag = g_bytes_get_data(bytes, NULL); g_debug("CRC byte is currently 0x%x", crc_tag[3]); for (guint32 retries_cnt = 0;; retries_cnt++) { g_autoptr(GBytes) bytes_new = NULL; /* CRC8 is not 0xff, erase last 4k of bank# */ if (crc_tag[3] != 0xff) { guint32 sector = FLASH_SECTOR_ERASE_4K + (attribs->start + attribs->length - 0x1000) / 0x1000; g_debug("Erasing 4k from sector 0x%x invalidate bank %u", sector, bank_in_use); /* offset for last 4k of bank# */ if (!fu_dell_dock_mst_rc_command(device, MST_CMD_ERASE_FLASH, 4, 0, (guint8 *)§or, error)) { g_prefix_error(error, "failed to erase sector 0x%x: ", sector); return FALSE; } /* CRC8 is 0xff, set it to 0x00 */ } else { guint32 write = 0x00; g_debug("Writing 0x00 byte to 0x%x to invalidate bank %u", crc_offset, bank_in_use); if (!fu_dell_dock_mst_rc_command(device, MST_CMD_WRITE_FLASH, 4, crc_offset, (guint8 *)&write, error)) { g_prefix_error(error, "failed to clear CRC byte: "); return FALSE; } } /* re-read for comparison */ if (!fu_dell_dock_mst_rc_command(device, MST_CMD_READ_FLASH, 4, crc_offset, NULL, error)) { g_prefix_error(error, "failed to read tag from flash: "); return FALSE; } if (!fu_dell_dock_mst_read_register(fu_device_get_proxy(device), PANAMERA_MST_RC_DATA_ADDR, 4, &bytes_new, error)) { return FALSE; } new_tag = g_bytes_get_data(bytes_new, NULL); g_debug("CRC byte is currently 0x%x", new_tag[3]); /* tag successfully cleared */ if ((new_tag[3] == 0xff && crc_tag[3] != 0xff) || (new_tag[3] == 0x00 && crc_tag[3] == 0xff)) { break; } if (retries_cnt > retries) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "set tag invalid fail (new 0x%x; old 0x%x)", new_tag[3], crc_tag[3]); return FALSE; } } return TRUE; } static gboolean fu_dell_dock_mst_write_bank(FuDevice *device, GBytes *fw, guint8 bank, FuProgress *progress, GError **error) { const guint retries = 2; for (guint i = 0; i < retries; i++) { gboolean checksum = FALSE; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 15, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 84, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 1, NULL); if (!fu_dell_dock_mst_erase_panamera_bank(device, bank, error)) return FALSE; fu_progress_step_done(progress); if (!fu_dell_dock_mst_write_flash_bank(device, fw, bank, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); if (!fu_dell_dock_mst_checksum_bank(device, fw, bank, &checksum, error)) return FALSE; if (!checksum) { g_debug("MST: Failed to verify checksum on bank %u", bank); fu_progress_reset(progress); continue; } fu_progress_step_done(progress); g_debug("MST: Bank %u successfully flashed", bank); return TRUE; } /* failed after all our retries */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to write to bank %u", bank); return FALSE; } static FuProgress * fu_dell_dock_mst_set_local_progress(FuProgress *progress, guint steps) { FuProgress *progress_local; progress_local = fu_progress_get_child(progress); fu_progress_set_id(progress_local, G_STRLOC); fu_progress_set_steps(progress_local, steps); return progress_local; } static gboolean fu_dell_dock_mst_write_panamera(FuDevice *device, GBytes *fw, FwupdInstallFlags flags, FuProgress *progress, GError **error) { gboolean checksum = FALSE; MSTBank bank_in_use = 0; guint8 order[2] = {ESM, Bank0}; FuProgress *progress_local; fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "stop-esm"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 99, NULL); /* determine the flash order */ if (!fu_dell_dock_mst_query_active_bank(fu_device_get_proxy(device), &bank_in_use, error)) return FALSE; if (bank_in_use == Bank0) order[1] = Bank1; /* ESM needs special handling during flash process*/ if (!fu_dell_dock_mst_stop_esm(device, error)) return FALSE; fu_progress_step_done(progress); progress_local = fu_dell_dock_mst_set_local_progress(progress, 2); /* Write each bank in order */ for (guint phase = 0; phase < 2; phase++) { g_debug("MST: Checking bank %u", order[phase]); if (!fu_dell_dock_mst_checksum_bank(device, fw, order[phase], &checksum, error)) return FALSE; if (checksum) { g_debug("MST: bank %u is already up to date", order[phase]); fu_progress_step_done(progress_local); continue; } g_debug("MST: bank %u needs to be updated", order[phase]); if (!fu_dell_dock_mst_write_bank(device, fw, order[phase], fu_progress_get_child(progress_local), error)) return FALSE; fu_progress_step_done(progress_local); } /* invalidate the previous bank */ if (!fu_dell_dock_mst_invalidate_bank(device, bank_in_use, error)) { g_prefix_error(error, "failed to invalidate bank %u: ", bank_in_use); return FALSE; } fu_progress_step_done(progress); return TRUE; } static gboolean fu_dell_dock_mst_write_cayenne(FuDevice *device, GBytes *fw, FwupdInstallFlags flags, FuProgress *progress, GError **error) { gboolean checksum = FALSE; guint retries = 2; fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 3, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 97, NULL); for (guint i = 0; i < retries; i++) { if (!fu_dell_dock_mst_erase_cayenne(device, error)) return FALSE; fu_progress_step_done(progress); if (!fu_dell_dock_mst_write_flash_bank(device, fw, Cayenne, fu_progress_get_child(progress), error)) return FALSE; if (!fu_dell_dock_mst_checksum_bank(device, fw, Cayenne, &checksum, error)) return FALSE; fu_progress_step_done(progress); if (!checksum) { g_debug("MST: Failed to verify checksum"); fu_progress_reset(progress); continue; } break; } /* failed after all our retries */ if (!checksum) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to write to bank"); return FALSE; } /* activate the FW */ if (!fu_dell_dock_mst_rc_command(device, MST_CMD_ACTIVATE_FW, g_bytes_get_size(fw), 0x0, NULL, error)) { g_prefix_error(error, "Failed to activate FW: "); return FALSE; } return TRUE; } static gboolean fu_dell_dock_mst_write_fw(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDellDockMst *self = FU_DELL_DOCK_MST(device); const guint8 *data; g_autofree gchar *dynamic_version = NULL; g_autoptr(GBytes) fw = NULL; MSTType type; g_return_val_if_fail(device != NULL, FALSE); g_return_val_if_fail(FU_IS_FIRMWARE(firmware), FALSE); g_return_val_if_fail(fu_device_get_proxy(device) != NULL, FALSE); /* open the hub*/ if (!fu_device_open(fu_device_get_proxy(device), error)) return FALSE; /* open up access to controller bus */ if (!fu_dell_dock_set_power(device, self->unlock_target, TRUE, error)) return FALSE; /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; data = g_bytes_get_data(fw, NULL); dynamic_version = g_strdup_printf("%02x.%02x.%02x", data[self->blob_major_offset], data[self->blob_minor_offset], data[self->blob_build_offset]); g_info("writing MST firmware version %s", dynamic_version); /* enable remote control */ if (!fu_dell_dock_mst_enable_remote_control(device, error)) return FALSE; type = fu_dell_dock_mst_get_module_type(device); if (type == Panamera_mst) { if (!fu_dell_dock_mst_write_panamera(device, fw, flags, progress, error)) return FALSE; } else if (type == Cayenne_mst) { if (!fu_dell_dock_mst_write_cayenne(device, fw, flags, progress, error)) return FALSE; } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Unknown mst found"); return FALSE; } /* dock will reboot to re-read; this is to appease the daemon */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, dynamic_version); /* disable remote control now */ return fu_dell_dock_mst_disable_remote_control(device, error); } static gboolean fu_dell_dock_mst_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuDellDockMst *self = FU_DELL_DOCK_MST(device); guint64 tmp = 0; if (g_strcmp0(key, "DellDockUnlockTarget") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->unlock_target = tmp; return TRUE; } if (g_strcmp0(key, "DellDockBlobMajorOffset") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->blob_major_offset = tmp; return TRUE; } if (g_strcmp0(key, "DellDockBlobMinorOffset") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->blob_minor_offset = tmp; return TRUE; } if (g_strcmp0(key, "DellDockBlobBuildOffset") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->blob_build_offset = tmp; return TRUE; } if (g_strcmp0(key, "DellDockInstallDurationI2C") == 0) { if (!fu_strtoull(value, &tmp, 0, 60 * 60 * 24, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_device_set_install_duration(device, tmp); return TRUE; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static gboolean fu_dell_dock_mst_setup(FuDevice *device, GError **error) { FuDevice *parent; const gchar *version; /* sanity check that we can talk to MST */ if (!fu_dell_dock_mst_d19_check_fw(device, error)) return FALSE; /* set version from EC if we know it */ parent = fu_device_get_parent(device); version = fu_dell_dock_ec_get_mst_version(parent); if (version != NULL) { fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, version); } return TRUE; } static gboolean fu_dell_dock_mst_probe(FuDevice *device, GError **error) { FuDellDockMst *self = FU_DELL_DOCK_MST(device); /* logical id */ fu_device_set_logical_id(FU_DEVICE(device), "mst"); /* instance id */ if (self->dock_type == DOCK_BASE_TYPE_ATOMIC) { self->mst_type = Cayenne_mst; self->mst_rc_trigger_addr = CAYENNE_MST_RC_TRIGGER_ADDR; self->mst_rc_command_addr = CAYENNE_MST_RC_COMMAND_ADDR; self->mst_rc_data_addr = CAYENNE_MST_RC_DATA_ADDR; self->mst_core_mcu_bootloader_addr = CAYENNE_MST_CORE_MCU_BOOTLOADER_STS; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_instance_id(device, DELL_DOCK_VMM6210_INSTANCE_ID); } else if (self->dock_type == DOCK_BASE_TYPE_SALOMON) { self->mst_type = Panamera_mst; self->mst_rc_trigger_addr = PANAMERA_MST_RC_TRIGGER_ADDR; self->mst_rc_command_addr = PANAMERA_MST_RC_COMMAND_ADDR; self->mst_rc_data_addr = PANAMERA_MST_RC_DATA_ADDR; self->mst_core_mcu_bootloader_addr = PANAMERA_MST_CORE_MCU_BOOTLOADER_STS; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_instance_id(device, DELL_DOCK_VM5331_INSTANCE_ID); } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unknown dock type 0x%x", self->dock_type); return FALSE; } return TRUE; } static gboolean fu_dell_dock_mst_open(FuDevice *device, GError **error) { FuDellDockMst *self = FU_DELL_DOCK_MST(device); FuDevice *parent = fu_device_get_parent(device); g_return_val_if_fail(self->unlock_target != 0, FALSE); g_return_val_if_fail(parent != NULL, FALSE); if (fu_device_get_proxy(device) == NULL) fu_device_set_proxy(device, fu_device_get_proxy(parent)); if (!fu_device_open(fu_device_get_proxy(device), error)) return FALSE; /* open up access to controller bus */ if (!fu_dell_dock_set_power(device, self->unlock_target, TRUE, error)) return FALSE; return TRUE; } static gboolean fu_dell_dock_mst_close(FuDevice *device, GError **error) { FuDellDockMst *self = FU_DELL_DOCK_MST(device); /* close access to controller bus */ if (!fu_dell_dock_set_power(device, self->unlock_target, FALSE, error)) return FALSE; return fu_device_close(fu_device_get_proxy(device), error); } static void fu_dell_dock_mst_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_dell_dock_mst_init(FuDellDockMst *self) { fu_device_add_protocol(FU_DEVICE(self), "com.synaptics.mst"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); } static void fu_dell_dock_mst_class_init(FuDellDockMstClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_dell_dock_mst_probe; device_class->open = fu_dell_dock_mst_open; device_class->close = fu_dell_dock_mst_close; device_class->setup = fu_dell_dock_mst_setup; device_class->write_firmware = fu_dell_dock_mst_write_fw; device_class->set_quirk_kv = fu_dell_dock_mst_set_quirk_kv; device_class->set_progress = fu_dell_dock_mst_set_progress; } FuDellDockMst * fu_dell_dock_mst_new(FuContext *ctx, guint8 dock_type) { FuDellDockMst *self = NULL; self = g_object_new(FU_TYPE_DELL_DOCK_MST, "context", ctx, NULL); self->dock_type = dock_type; return self; } fwupd-2.0.10/plugins/dell-dock/fu-dell-dock-mst.h000066400000000000000000000013651501337203100214560ustar00rootroot00000000000000/* * Copyright 2018 Dell Inc. * All rights reserved. * * This software and associated documentation (if any) is furnished * under a license and may only be used or copied in accordance * with the terms of the license. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * Dell Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_DELL_DOCK_MST (fu_dell_dock_mst_get_type()) G_DECLARE_FINAL_TYPE(FuDellDockMst, fu_dell_dock_mst, FU, DELL_DOCK_MST, FuDevice) FuDellDockMst * fu_dell_dock_mst_new(FuContext *ctx, guint8 dock_type); fwupd-2.0.10/plugins/dell-dock/fu-dell-dock-plugin.c000066400000000000000000000263071501337203100221470ustar00rootroot00000000000000/* * Copyright 2018 Dell Inc. * All rights reserved. * * This software and associated documentation (if any) is furnished * under a license and may only be used or copied in accordance * with the terms of the license. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * Dell Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-dell-dock-common.h" #include "fu-dell-dock-ec.h" #include "fu-dell-dock-mst.h" #include "fu-dell-dock-plugin.h" #include "fu-dell-dock-status.h" #include "fu-dell-dock-tbt.h" struct _FuDellDockPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuDellDockPlugin, fu_dell_dock_plugin, FU_TYPE_PLUGIN) static gboolean fu_dell_dock_plugin_create_node(FuPlugin *plugin, FuDevice *device, GError **error) { g_autoptr(FuDeviceLocker) locker = NULL; locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; fu_plugin_device_add(plugin, device); return TRUE; } static gboolean fu_dell_dock_plugin_probe(FuPlugin *plugin, FuDevice *proxy, GError **error) { g_autoptr(FuDellDockEc) ec_device = NULL; g_autoptr(FuDellDockMst) mst_device = NULL; g_autoptr(FuDellDockStatus) status_device = NULL; guint8 dock_type = DOCK_BASE_TYPE_UNKNOWN; gboolean dock_usb4_present; FuContext *ctx = fu_plugin_get_context(plugin); /* create ec endpoint */ ec_device = fu_dell_dock_ec_new(proxy); if (!fu_dell_dock_plugin_create_node(plugin, FU_DEVICE(ec_device), error)) return FALSE; /* setup hub version after knowing the dock type is supported */ if (!fu_dell_dock_hid_get_hub_version(proxy, error)) return FALSE; /* determine the dock type */ dock_type = fu_dell_dock_ec_get_dock_type(FU_DEVICE(ec_device)); /* create mst endpoint */ mst_device = fu_dell_dock_mst_new(ctx, dock_type); if (!fu_device_probe(FU_DEVICE(mst_device), error)) return FALSE; fu_device_add_child(FU_DEVICE(ec_device), FU_DEVICE(mst_device)); if (!fu_dell_dock_plugin_create_node(plugin, FU_DEVICE(mst_device), error)) return FALSE; /* create package version endpoint */ dock_usb4_present = fu_dell_dock_ec_module_is_usb4(FU_DEVICE(ec_device)); status_device = fu_dell_dock_status_new(ctx, dock_type, dock_usb4_present); if (!fu_device_probe(FU_DEVICE(status_device), error)) return FALSE; fu_device_add_child(FU_DEVICE(ec_device), FU_DEVICE(status_device)); if (!fu_dell_dock_plugin_create_node(plugin, FU_DEVICE(status_device), error)) return FALSE; /* create TBT endpoint if Thunderbolt SKU and Thunderbolt link inactive */ if (fu_dell_dock_ec_needs_tbt(FU_DEVICE(ec_device))) { g_autoptr(FuDellDockTbt) tbt_device = fu_dell_dock_tbt_new(proxy); fu_device_add_instance_id(FU_DEVICE(tbt_device), DELL_DOCK_TBT_INSTANCE_ID); fu_device_add_child(FU_DEVICE(ec_device), FU_DEVICE(tbt_device)); if (!fu_dell_dock_plugin_create_node(plugin, FU_DEVICE(tbt_device), error)) return FALSE; } return TRUE; } /* prefer to use EC if in the transaction and parent if it is not */ static FuDevice * fu_dell_dock_plugin_get_ec(GPtrArray *devices) { FuDevice *ec_parent = NULL; for (gint i = devices->len - 1; i >= 0; i--) { FuDevice *dev = g_ptr_array_index(devices, i); FuDevice *parent; if (FU_IS_DELL_DOCK_EC(dev)) return dev; parent = fu_device_get_parent(dev); if (parent != NULL && FU_IS_DELL_DOCK_EC(parent)) ec_parent = parent; } return ec_parent; } static gboolean fu_dell_dock_plugin_backend_device_added(FuPlugin *plugin, FuDevice *device, FuProgress *progress, GError **error) { g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(FuDellDockHub) hub = NULL; const gchar *hub_cache_key = "hub-usb3-gen1"; GPtrArray *devices; FuDevice *ec_device; FuDevice *hub_dev; guint8 dock_type; /* not interesting */ if (!FU_IS_USB_DEVICE(device)) return TRUE; hub = fu_dell_dock_hub_new(FU_USB_DEVICE(device)); locker = fu_device_locker_new(FU_DEVICE(hub), error); if (locker == NULL) return FALSE; /* probe extend devices under Usb3.1 Gen 2 Hub */ if (fu_device_has_private_flag(FU_DEVICE(hub), FU_DELL_DOCK_HUB_FLAG_HAS_BRIDGE)) { if (!fu_dell_dock_plugin_probe(plugin, FU_DEVICE(hub), error)) return FALSE; } /* process hub devices if ec device is added */ devices = fu_plugin_get_devices(plugin); ec_device = fu_dell_dock_plugin_get_ec(devices); if (ec_device == NULL) { fu_plugin_cache_add(plugin, hub_cache_key, FU_DEVICE(hub)); return TRUE; } /* determine dock type by ec */ dock_type = fu_dell_dock_ec_get_dock_type(ec_device); if (dock_type == DOCK_BASE_TYPE_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "can't read base dock type from EC"); return FALSE; } fu_dell_dock_hub_add_instance(FU_DEVICE(hub), dock_type); fu_plugin_device_add(plugin, FU_DEVICE(hub)); /* add hub instance id for the cached device */ hub_dev = fu_plugin_cache_lookup(plugin, hub_cache_key); if (hub_dev != NULL) { fu_dell_dock_hub_add_instance(FU_DEVICE(hub_dev), dock_type); fu_plugin_device_add(plugin, FU_DEVICE(hub_dev)); fu_plugin_cache_remove(plugin, hub_cache_key); } return TRUE; } static void fu_dell_dock_plugin_separate_activation(FuPlugin *plugin) { FuDevice *device_ec = fu_plugin_cache_lookup(plugin, "ec"); FuDevice *device_usb4 = fu_plugin_cache_lookup(plugin, "usb4"); /* both usb4 and ec device are found */ if (device_usb4 && device_ec) { if (fu_device_has_flag(device_usb4, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION) && fu_device_has_flag(device_ec, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION)) { fu_device_remove_flag(FU_DEVICE(device_ec), FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION); g_info("activate for %s is inhibited by %s", fu_device_get_name(device_ec), fu_device_get_name(device_usb4)); } } } static void fu_dell_dock_plugin_device_registered(FuPlugin *plugin, FuDevice *device) { /* dell dock delays the activation so skips device restart */ if (fu_device_has_guid(device, DELL_DOCK_TBT_INSTANCE_ID)) { fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_SKIPS_RESTART); fu_plugin_cache_add(plugin, "tbt", device); } if (fu_device_has_guid(device, DELL_DOCK_USB4_INSTANCE_ID)) { fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_SKIPS_RESTART); fu_plugin_cache_add(plugin, "usb4", device); } if (FU_IS_DELL_DOCK_EC(device)) fu_plugin_cache_add(plugin, "ec", device); /* usb4 device from thunderbolt plugin */ if (g_strcmp0(fu_device_get_plugin(device), "thunderbolt") == 0 && fu_device_has_guid(device, DELL_DOCK_USB4_INSTANCE_ID)) { g_autofree gchar *msg = NULL; msg = g_strdup_printf("firmware update inhibited by [%s] plugin", fu_plugin_get_name(plugin)); fu_device_inhibit(device, "hidden", msg); return; } /* online activation is mutually exclusive between usb4 and ec */ fu_dell_dock_plugin_separate_activation(plugin); } static gboolean fu_dell_dock_plugin_backend_device_removed(FuPlugin *plugin, FuDevice *device, GError **error) { FuDevice *parent; if (!FU_IS_USB_DEVICE(device)) return TRUE; parent = fu_device_get_parent(device); if (parent != NULL && FU_IS_DELL_DOCK_EC(parent)) { g_debug("Removing %s (%s)", fu_device_get_name(parent), fu_device_get_id(parent)); fu_plugin_device_remove(plugin, parent); } return TRUE; } static gboolean fu_dell_dock_plugin_composite_prepare(FuPlugin *plugin, GPtrArray *devices, GError **error) { FuDevice *parent = fu_dell_dock_plugin_get_ec(devices); const gchar *sku; if (parent == NULL) return TRUE; sku = fu_dell_dock_ec_get_module_type(parent); if (sku != NULL) fu_plugin_add_report_metadata(plugin, "DellDockSKU", sku); return TRUE; } static gboolean fu_dell_dock_plugin_composite_cleanup(FuPlugin *plugin, GPtrArray *devices, GError **error) { FuDevice *parent = fu_dell_dock_plugin_get_ec(devices); FuDevice *dev = NULL; g_autoptr(FuDeviceLocker) locker = NULL; gboolean needs_activation = FALSE; if (parent == NULL) return TRUE; /* if thunderbolt is in the transaction it needs to be activated separately */ for (guint i = 0; i < devices->len; i++) { dev = g_ptr_array_index(devices, i); if ((g_strcmp0(fu_device_get_plugin(dev), "thunderbolt") == 0 || g_strcmp0(fu_device_get_plugin(dev), "intel_usb4") == 0 || g_strcmp0(fu_device_get_plugin(dev), "dell_dock") == 0) && fu_device_has_flag(dev, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION)) { /* the kernel and/or thunderbolt plugin have been configured to let HW * finish the update */ if (fu_device_has_flag(dev, FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE)) { fu_dell_dock_ec_tbt_passive(parent); /* run the update immediately - no kernel support */ } else { needs_activation = TRUE; break; } } } /* separate activation flag between usb4 and ec device */ fu_dell_dock_plugin_separate_activation(plugin); locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; if (!fu_dell_dock_ec_reboot_dock(parent, error)) return FALSE; /* close this first so we don't have an error from the thunderbolt activation */ if (!fu_device_locker_close(locker, error)) return FALSE; if (needs_activation && dev != NULL) { g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); if (!fu_device_activate(dev, progress, error)) return FALSE; } return TRUE; } static void fu_dell_dock_plugin_init(FuDellDockPlugin *self) { } static void fu_dell_dock_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "DellDockBlobBuildOffset"); fu_context_add_quirk_key(ctx, "DellDockBlobMajorOffset"); fu_context_add_quirk_key(ctx, "DellDockBlobMinorOffset"); fu_context_add_quirk_key(ctx, "DellDockBlobVersionOffset"); fu_context_add_quirk_key(ctx, "DellDockBoardMin"); fu_context_add_quirk_key(ctx, "DellDockHubVersionLowest"); fu_context_add_quirk_key(ctx, "DellDockInstallDurationI2C"); fu_context_add_quirk_key(ctx, "DellDockUnlockTarget"); fu_context_add_quirk_key(ctx, "DellDockVersionLowest"); /* allow these to be built by quirks */ fu_plugin_add_device_gtype(plugin, FU_TYPE_DELL_DOCK_STATUS); fu_plugin_add_device_gtype(plugin, FU_TYPE_DELL_DOCK_MST); fu_plugin_add_device_gtype(plugin, FU_TYPE_DELL_DOCK_EC); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_DELL_DOCK_MST); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_DELL_DOCK_STATUS); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_DELL_DOCK_TBT); /* coverage */ #ifndef _WIN32 /* currently slower performance, but more reliable in corner cases */ fu_plugin_add_rule(plugin, FU_PLUGIN_RULE_BETTER_THAN, "synaptics_mst"); #endif } static void fu_dell_dock_plugin_class_init(FuDellDockPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_dell_dock_plugin_constructed; plugin_class->device_registered = fu_dell_dock_plugin_device_registered; plugin_class->backend_device_added = fu_dell_dock_plugin_backend_device_added; plugin_class->backend_device_removed = fu_dell_dock_plugin_backend_device_removed; plugin_class->composite_cleanup = fu_dell_dock_plugin_composite_cleanup; plugin_class->composite_prepare = fu_dell_dock_plugin_composite_prepare; } fwupd-2.0.10/plugins/dell-dock/fu-dell-dock-plugin.h000066400000000000000000000003641501337203100221470ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuDellDockPlugin, fu_dell_dock_plugin, FU, DELL_DOCK_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/dell-dock/fu-dell-dock-status.c000066400000000000000000000143561501337203100221750ustar00rootroot00000000000000/* * Copyright 2018 Dell Inc. * All rights reserved. * * This software and associated documentation (if any) is furnished * under a license and may only be used or copied in accordance * with the terms of the license. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * Dell Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include #include "fu-dell-dock-common.h" struct _FuDellDockStatus { FuDevice parent_instance; guint64 blob_version_offset; guint8 dock_type; gboolean dock_usb4_present; }; G_DEFINE_TYPE(FuDellDockStatus, fu_dell_dock_status, FU_TYPE_DEVICE) static gchar * fu_dell_dock_status_ver_string(guint32 status_version) { /* guint32 BCD */ return g_strdup_printf("%02x.%02x.%02x.%02x", status_version & 0xff, (status_version >> 8) & 0xff, (status_version >> 16) & 0xff, (status_version >> 24) & 0xff); } static gboolean fu_dell_dock_status_setup(FuDevice *device, GError **error) { FuDevice *parent; guint32 status_version; g_autofree gchar *dynamic_version = NULL; parent = fu_device_get_parent(device); status_version = fu_dell_dock_ec_get_status_version(parent); dynamic_version = fu_dell_dock_status_ver_string(status_version); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_QUAD); fu_device_set_version(device, dynamic_version); fu_device_set_logical_id(FU_DEVICE(device), "status"); return TRUE; } static gboolean fu_dell_dock_status_write(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDellDockStatus *self = FU_DELL_DOCK_STATUS(device); gsize length = 0; guint32 status_version = 0; const guint8 *data; g_autofree gchar *dynamic_version = NULL; g_autoptr(GBytes) fw = NULL; g_return_val_if_fail(device != NULL, FALSE); g_return_val_if_fail(FU_IS_FIRMWARE(firmware), FALSE); /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; data = g_bytes_get_data(fw, &length); if (!fu_memcpy_safe((guint8 *)&status_version, sizeof(status_version), 0x0, /* dst */ data, length, self->blob_version_offset, /* src */ sizeof(status_version), error)) return FALSE; dynamic_version = fu_dell_dock_status_ver_string(status_version); g_info("writing status firmware version %s", dynamic_version); if (!fu_dell_dock_ec_commit_package(fu_device_get_proxy(device), fw, error)) return FALSE; /* dock will reboot to re-read; this is to appease the daemon */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_QUAD); fu_device_set_version(device, dynamic_version); return TRUE; } static gboolean fu_dell_dock_status_probe(FuDevice *device, GError **error) { FuDellDockStatus *self = FU_DELL_DOCK_STATUS(device); if (self->dock_type == DOCK_BASE_TYPE_ATOMIC) { fu_device_add_instance_id(device, DELL_DOCK_ATOMIC_STATUS_INSTANCE_ID); } else if (self->dock_type == DOCK_BASE_TYPE_SALOMON) { if (self->dock_usb4_present) fu_device_add_instance_id(device, DELL_DOCK_DOCK2_INSTANCE_ID); else fu_device_add_instance_id(device, DELL_DOCK_DOCK1_INSTANCE_ID); } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unknown supported dock type 0x%x", self->dock_type); return FALSE; } return TRUE; } static gboolean fu_dell_dock_status_open(FuDevice *device, GError **error) { if (fu_device_get_proxy(device) == NULL) { if (fu_device_get_parent(device) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "no parent"); return FALSE; } fu_device_set_proxy(device, fu_device_get_parent(device)); } return fu_device_open(fu_device_get_proxy(device), error); } static gboolean fu_dell_dock_status_close(FuDevice *device, GError **error) { if (fu_device_get_proxy(device) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "no proxy"); return FALSE; } return fu_device_close(fu_device_get_proxy(device), error); } static gboolean fu_dell_dock_status_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuDellDockStatus *self = FU_DELL_DOCK_STATUS(device); if (g_strcmp0(key, "DellDockBlobVersionOffset") == 0) { guint64 tmp = 0; if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->blob_version_offset = tmp; return TRUE; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_dell_dock_status_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 13, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 72, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 9, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 7, "reload"); } static void fu_dell_dock_status_init(FuDellDockStatus *self) { fu_device_add_protocol(FU_DEVICE(self), "com.dell.dock"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); } static void fu_dell_dock_status_class_init(FuDellDockStatusClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_dell_dock_status_write; device_class->probe = fu_dell_dock_status_probe; device_class->setup = fu_dell_dock_status_setup; device_class->open = fu_dell_dock_status_open; device_class->close = fu_dell_dock_status_close; device_class->set_quirk_kv = fu_dell_dock_status_set_quirk_kv; device_class->set_progress = fu_dell_dock_status_set_progress; } FuDellDockStatus * fu_dell_dock_status_new(FuContext *ctx, guint8 dock_type, gboolean dock_usb4_present) { FuDellDockStatus *self = NULL; self = g_object_new(FU_TYPE_DELL_DOCK_STATUS, "context", ctx, NULL); self->dock_type = dock_type; self->dock_usb4_present = dock_usb4_present; return self; } fwupd-2.0.10/plugins/dell-dock/fu-dell-dock-status.h000066400000000000000000000014461501337203100221760ustar00rootroot00000000000000/* * Copyright 2018 Dell Inc. * All rights reserved. * * This software and associated documentation (if any) is furnished * under a license and may only be used or copied in accordance * with the terms of the license. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * Dell Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_DELL_DOCK_STATUS (fu_dell_dock_status_get_type()) G_DECLARE_FINAL_TYPE(FuDellDockStatus, fu_dell_dock_status, FU, DELL_DOCK_STATUS, FuDevice) FuDellDockStatus * fu_dell_dock_status_new(FuContext *ctx, guint8 dock_type, gboolean dock_usb4_present); fwupd-2.0.10/plugins/dell-dock/fu-dell-dock-tbt.c000066400000000000000000000216351501337203100214410ustar00rootroot00000000000000/* * Copyright 2019 Intel Corporation. * Copyright 2019 Dell Inc. * All rights reserved. * * This software and associated documentation (if any) is furnished * under a license and may only be used or copied in accordance * with the terms of the license. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * Dell Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include #include "fu-dell-dock-common.h" #define I2C_TBT_ADDRESS 0xa2 const FuHIDI2CParameters tbt_base_settings = { .i2ctargetaddr = I2C_TBT_ADDRESS, .regaddrlen = 1, .i2cspeed = I2C_SPEED_400K, }; /* TR Device ID */ #define PID_OFFSET 0x05 #define INTEL_PID 0x15ef /* earlier versions have bugs */ #define MIN_NVM "36.01" struct _FuDellDockTbt { FuDevice parent_instance; guint8 unlock_target; guint64 blob_major_offset; guint64 blob_minor_offset; gchar *hub_minimum_version; }; G_DEFINE_TYPE(FuDellDockTbt, fu_dell_dock_tbt, FU_TYPE_DEVICE) static gboolean fu_dell_dock_tbt_write_fw(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDellDockTbt *self = FU_DELL_DOCK_TBT(device); guint32 start_offset = 0; gsize image_size = 0; const guint8 *buffer; guint16 target_system = 0; g_autoptr(GTimer) timer = g_timer_new(); g_autofree gchar *dynamic_version = NULL; g_autoptr(GBytes) fw = NULL; g_return_val_if_fail(device != NULL, FALSE); g_return_val_if_fail(FU_IS_FIRMWARE(firmware), FALSE); /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; buffer = g_bytes_get_data(fw, &image_size); dynamic_version = g_strdup_printf("%02x.%02x", buffer[self->blob_major_offset], buffer[self->blob_minor_offset]); g_info("writing Thunderbolt firmware version %s", dynamic_version); g_debug("Total Image size: %" G_GSIZE_FORMAT, image_size); memcpy(&start_offset, buffer, sizeof(guint32)); /* nocheck:blocked */ g_debug("Header size 0x%x", start_offset); if (start_offset > image_size) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "Image header is too big (0x%x)", start_offset); return FALSE; } memcpy(&target_system, /* nocheck:blocked */ buffer + start_offset + PID_OFFSET, sizeof(guint16)); if (target_system != INTEL_PID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "Image is not intended for this system (0x%x)", target_system); return FALSE; } buffer += start_offset; image_size -= start_offset; g_debug("waking Thunderbolt controller"); if (!fu_dell_dock_hid_tbt_wake(fu_device_get_proxy(device), &tbt_base_settings, error)) return FALSE; fu_device_sleep(device, 2000); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); for (guint i = 0; i < image_size; i += HIDI2C_MAX_WRITE, buffer += HIDI2C_MAX_WRITE) { guint8 write_size = (image_size - i) > HIDI2C_MAX_WRITE ? HIDI2C_MAX_WRITE : (image_size - i); if (!fu_dell_dock_hid_tbt_write(fu_device_get_proxy(device), i, buffer, write_size, &tbt_base_settings, error)) return FALSE; fu_progress_set_percentage_full(progress, i, image_size); } g_debug("writing took %f seconds", g_timer_elapsed(timer, NULL)); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_BUSY); if (fu_dell_dock_ec_tbt_passive(fu_device_get_parent(device))) { g_info("using passive flow for Thunderbolt"); } else if (!fu_dell_dock_hid_tbt_authenticate(fu_device_get_proxy(device), &tbt_base_settings, error)) { g_prefix_error(error, "failed to authenticate: "); return FALSE; } /* dock will reboot to re-read; this is to appease the daemon */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_PAIR); fu_device_set_version(device, dynamic_version); return TRUE; } static gboolean fu_dell_dock_tbt_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuDellDockTbt *self = FU_DELL_DOCK_TBT(device); guint64 tmp = 0; if (g_strcmp0(key, "DellDockUnlockTarget") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->unlock_target = tmp; return TRUE; } if (g_strcmp0(key, "DellDockInstallDurationI2C") == 0) { if (!fu_strtoull(value, &tmp, 0, 60 * 60 * 24, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_device_set_install_duration(device, tmp); return TRUE; } if (g_strcmp0(key, "DellDockHubVersionLowest") == 0) { self->hub_minimum_version = g_strdup(value); return TRUE; } if (g_strcmp0(key, "DellDockBlobMajorOffset") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->blob_major_offset = tmp; return TRUE; } if (g_strcmp0(key, "DellDockBlobMinorOffset") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->blob_minor_offset = tmp; return TRUE; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static gboolean fu_dell_dock_tbt_setup(FuDevice *device, GError **error) { FuDellDockTbt *self = FU_DELL_DOCK_TBT(device); FuDevice *parent; const gchar *version; const gchar *hub_version; /* set version from EC if we know it */ parent = fu_device_get_parent(device); version = fu_dell_dock_ec_get_tbt_version(parent); if (version != NULL) { fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_PAIR); fu_device_set_version(device, version); } /* minimum version of NVM that supports this feature */ if (version == NULL || fu_version_compare(version, MIN_NVM, FWUPD_VERSION_FORMAT_PAIR) < 0) { fu_device_set_update_error( device, "Updates over I2C are disabled due to insufficient NVM version"); return TRUE; } /* minimum Hub2 version that supports this feature */ hub_version = fu_device_get_version(fu_device_get_proxy(device)); if (fu_version_compare(hub_version, self->hub_minimum_version, FWUPD_VERSION_FORMAT_PAIR) < 0) { fu_device_set_update_error( device, "Updates over I2C are disabled due to insufficient USB 3.1 G2 hub version"); return TRUE; } return TRUE; } static gboolean fu_dell_dock_tbt_probe(FuDevice *device, GError **error) { FuDevice *parent = fu_device_get_parent(device); /* sanity check */ if (parent == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no parent"); return FALSE; } fu_device_incorporate(device, parent, FU_DEVICE_INCORPORATE_FLAG_PHYSICAL_ID); fu_device_set_logical_id(FU_DEVICE(device), "tbt"); fu_device_add_instance_id(device, DELL_DOCK_TBT_INSTANCE_ID); /* this is true only when connected to non-thunderbolt port */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE); return TRUE; } static gboolean fu_dell_dock_tbt_open(FuDevice *device, GError **error) { FuDellDockTbt *self = FU_DELL_DOCK_TBT(device); g_return_val_if_fail(self->unlock_target != 0, FALSE); if (!fu_device_open(fu_device_get_proxy(device), error)) return FALSE; /* adjust to access controller */ if (!fu_dell_dock_set_power(device, self->unlock_target, TRUE, error)) return FALSE; return TRUE; } static gboolean fu_dell_dock_tbt_close(FuDevice *device, GError **error) { FuDellDockTbt *self = FU_DELL_DOCK_TBT(device); /* adjust to access controller */ if (!fu_dell_dock_set_power(device, self->unlock_target, FALSE, error)) return FALSE; return fu_device_close(fu_device_get_proxy(device), error); } static void fu_dell_dock_tbt_finalize(GObject *object) { FuDellDockTbt *self = FU_DELL_DOCK_TBT(object); g_free(self->hub_minimum_version); G_OBJECT_CLASS(fu_dell_dock_tbt_parent_class)->finalize(object); } static void fu_dell_dock_tbt_init(FuDellDockTbt *self) { fu_device_add_protocol(FU_DEVICE(self), "com.intel.thunderbolt"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); } static void fu_dell_dock_tbt_class_init(FuDellDockTbtClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->finalize = fu_dell_dock_tbt_finalize; device_class->probe = fu_dell_dock_tbt_probe; device_class->setup = fu_dell_dock_tbt_setup; device_class->open = fu_dell_dock_tbt_open; device_class->close = fu_dell_dock_tbt_close; device_class->write_firmware = fu_dell_dock_tbt_write_fw; device_class->set_quirk_kv = fu_dell_dock_tbt_set_quirk_kv; } FuDellDockTbt * fu_dell_dock_tbt_new(FuDevice *proxy) { FuContext *ctx = fu_device_get_context(proxy); FuDellDockTbt *self = g_object_new(FU_TYPE_DELL_DOCK_TBT, "context", ctx, NULL); fu_device_set_proxy(FU_DEVICE(self), proxy); return self; } fwupd-2.0.10/plugins/dell-dock/fu-dell-dock-tbt.h000066400000000000000000000014111501337203100214340ustar00rootroot00000000000000/* * Copyright 2019 Intel Corporation. * Copyright 2019 Dell Inc. * All rights reserved. * * This software and associated documentation (if any) is furnished * under a license and may only be used or copied in accordance * with the terms of the license. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * Dell Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_DELL_DOCK_TBT (fu_dell_dock_tbt_get_type()) G_DECLARE_FINAL_TYPE(FuDellDockTbt, fu_dell_dock_tbt, FU, DELL_DOCK_TBT, FuDevice) FuDellDockTbt * fu_dell_dock_tbt_new(FuDevice *proxy); fwupd-2.0.10/plugins/dell-dock/meson.build000066400000000000000000000010521501337203100203710ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginDellDock"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('dell-dock.quirk') plugin_builtins += static_library('fu_plugin_dell_dock', sources: [ 'fu-dell-dock-plugin.c', 'fu-dell-dock-common.c', 'fu-dell-dock-hid.c', 'fu-dell-dock-status.c', 'fu-dell-dock-ec.c', 'fu-dell-dock-hub.c', 'fu-dell-dock-tbt.c', 'fu-dell-dock-mst.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) fwupd-2.0.10/plugins/dell-kestrel/000077500000000000000000000000001501337203100167625ustar00rootroot00000000000000fwupd-2.0.10/plugins/dell-kestrel/README.md000066400000000000000000000055161501337203100202500ustar00rootroot00000000000000--- title: Plugin: Dell USB-C Dock --- ## Dell System This plugin supports Dell dock with no dependencies on Dell system. ## Components Supported hardware devices: * USB Hubs * MST * TBT * EC * LAN * Retimer/dpmux * PD * RMM ## Device topology When this plugin is used, devices present in other plugins may be shown in the topology of this dock. This is intentional as this plugin works together with those plugins to manage the flashing of all components. * synaptics_vmm9 * intel_usb4 ## Firmware Format The daemon will decompress the cabinet archive and extract several firmware blobs with an unspecified binary file format. This plugin supports the following protocol ID: * `com.dell.kestrel` ## GUID Generation These devices use several different generation schemes, e.g. ### Dock config independent devices * Embedded Controller: `USB\VID_413C&PID_B06E&DOCKTYPE_07&DEVTYPE_00` * RTS0 USB 3 G1 Hub: `USB\VID_413C&PID_B0A1&DOCKTYPE_07` * RTS0 USB 3 G2 hub: `USB\VID_413C&PID_B0A2&DOCKTYPE_07` * RTS5 USB 3 G2 hub: `USB\VID_413C&PID_B0A3&DOCKTYPE_07` * RMM: `USB\VID_413C&PID_B0A4` * MST: `USB\VID_413C&PID_B0A5` * LAN: `EC\DOCKTYPE_07&DEVTYPE_07` ### DP ALT config * Package: `EC\DOCKTYPE_07&DOCKSKU_01&DEVTYPE_PACKAGE` * PD UP 5: `EC\DOCKTYPE_07&DOCKSKU_01&DEVTYPE_01&INST_00` * PD UP 15: `EC\DOCKTYPE_07&DOCKSKU_01&DEVTYPE_01&INST_01` ### T4 config * Package: `EC\DOCKTYPE_07&DOCKSKU_02&DEVTYPE_PACKAGE` * PD UP 5: `EC\DOCKTYPE_07&DOCKSKU_02&DEVTYPE_01&INST_00` * PD UP 15: `EC\DOCKTYPE_07&DOCKSKU_02&DEVTYPE_01&INST_01` * PD UP 17: `EC\DOCKTYPE_07&DOCKSKU_02&DEVTYPE_01&INST_02` * TBT Controller: `TBT-00d4b0a1` ### T5 config * Package: `EC\DOCKTYPE_07&DOCKSKU_03&DEVTYPE_PACKAGE` * PD UP 5: `EC\DOCKTYPE_07&DOCKSKU_03&DEVTYPE_01&INST_00` * PD UP 15: `EC\DOCKTYPE_07&DOCKSKU_03&DEVTYPE_01&INST_01` * PD UP 17: `EC\DOCKTYPE_07&DOCKSKU_03&DEVTYPE_01&INST_02` * Retimer: `EC\DOCKTYPE_07&DOCKSKU_03&DEVTYPE_06` * WT PD: `EC\DOCKTYPE_07&DOCKSKU_03&DEVTYPE_10` * TBT Controller: `TBT-00d4b0a2` ## Update Behavior This dock is a composite device with inclusion of various components, the update takes particular order to completion. All updates will be staged on the device until the user manually disconnects the dock's Type-C cable from the host, at which point they will take effect. ## Plugin Configuration This plugin supports the following parameters by modifying `dell_kestrel` section of `/etc/fwupd/fwupd.conf`. ### UpdateOnDisconnect The firmware updates are staged to the devices in the dell dock and activated when the user manually unplugs the dock cable. Default: true. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x413C` ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Crag Wang: @CragW fwupd-2.0.10/plugins/dell-kestrel/dell-kestrel.quirk000066400000000000000000000022561501337203100224330ustar00rootroot00000000000000# # Copyright 2024 Dell Technologies # # SPDX-License-Identifier: LGPL-2.1-or-later OR MIT # # Used to make plugin probe the devices [USB\VID_413C&PID_B06E] Name = Unprobed Dell accessory endpoint Summary = Dell Dock EC Icon = dock Plugin = dell_kestrel [USB\VID_413C&PID_B0A1] Name = Unprobed Dell accessory endpoint Summary = Dell Dock USB Hub Icon = usb-hub Plugin = dell_kestrel [USB\VID_413C&PID_B0A2] Name = Unprobed Dell accessory endpoint Summary = Dell Dock USB Hub Icon = usb-hub Plugin = dell_kestrel [USB\VID_413C&PID_B0A3] Name = Unprobed Dell accessory endpoint Summary = Dell Dock USB Hub Icon = usb-hub Plugin = dell_kestrel [USB\VID_413C&PID_B0A4] Name = Remote Management Summary = Dell Dock Remote Management Icon = usb-hub Plugin = dell_kestrel [USB\VID_413C&PID_B0A5] Name = Unprobed Dell accessory endpoint Summary = Dell Dock MST Icon = video-display Plugin = synaptics_vmm9 [TBT-00d4b0a2] Name = USB4v2 controller Summary = Dell Dock USB4v2 Controller Icon = thunderbolt Vendor = Dell Computer Corp. VendorId = TBT:0x00D4 [TBT-00d4b0a1] Name = USB4 controller Summary = Dell Dock USB4 Controller Icon = thunderbolt Vendor = Dell Computer Corp. VendorId = TBT:0x00D4 fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-common.h000066400000000000000000000013661501337203100234260ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #include "fu-dell-kestrel-dpmux.h" #include "fu-dell-kestrel-ec-struct.h" #include "fu-dell-kestrel-ec.h" #include "fu-dell-kestrel-ilan.h" #include "fu-dell-kestrel-package.h" #include "fu-dell-kestrel-pd.h" #include "fu-dell-kestrel-rmm.h" #include "fu-dell-kestrel-rtshub.h" #include "fu-dell-kestrel-wtpd.h" /* device IDs: Main HID on USB Hub */ #define DELL_VID 0x413C #define DELL_KESTREL_HID_PID 0xB06E /* device IDs: mst */ #define MST_VMM89_USB_VID 0x413C #define MST_VMM89_USB_PID 0xB0A5 /* device IDs: tbt */ #define DELL_KESTREL_T5_DEVID "TBT-00d4b0a2" #define DELL_KESTREL_T4_DEVID "TBT-00d4b0a1" fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-dpmux.c000066400000000000000000000075441501337203100232720ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-dell-kestrel-common.h" struct _FuDellKestrelDpmux { FuDevice parent_instance; }; G_DEFINE_TYPE(FuDellKestrelDpmux, fu_dell_kestrel_dpmux, FU_TYPE_DEVICE) static gchar * fu_dell_kestrel_dpmux_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32_hex(version_raw, fu_device_get_version_format(device)); } static gboolean fu_dell_kestrel_dpmux_setup(FuDevice *device, GError **error) { FuDevice *proxy = fu_device_get_proxy(device); FuDellDockBaseType dock_type = fu_dell_kestrel_ec_get_dock_type(FU_DELL_KESTREL_EC(proxy)); FuDellKestrelDockSku dock_sku = fu_dell_kestrel_ec_get_dock_sku(FU_DELL_KESTREL_EC(proxy)); FuDellKestrelEcDevType dev_type = FU_DELL_KESTREL_EC_DEV_TYPE_DP_MUX; guint32 dpmux_version; g_autofree gchar *devname = NULL; /* name */ devname = g_strdup_printf("%s", fu_dell_kestrel_ec_devicetype_to_str(dev_type, 0, 0)); fu_device_set_name(device, devname); fu_device_set_logical_id(device, devname); /* instance ID */ fu_device_add_instance_u8(device, "DOCKTYPE", dock_type); fu_device_add_instance_u8(device, "DOCKSKU", dock_sku); fu_device_add_instance_u8(device, "DEVTYPE", dev_type); fu_device_build_instance_id(device, error, "EC", "DOCKTYPE", "DOCKSKU", "DEVTYPE", NULL); /* version */ dpmux_version = fu_dell_kestrel_ec_get_dpmux_version(FU_DELL_KESTREL_EC(proxy)); fu_device_set_version_raw(device, dpmux_version); return TRUE; } static gboolean fu_dell_kestrel_dpmux_write(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDevice *proxy = fu_device_get_proxy(device); return fu_dell_kestrel_hid_device_write_firmware(FU_DELL_KESTREL_HID_DEVICE(proxy), firmware, progress, FU_DELL_KESTREL_EC_DEV_TYPE_DP_MUX, 0, error); } static void fu_dell_kestrel_dpmux_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_dell_kestrel_dpmux_init(FuDellKestrelDpmux *self) { fu_device_add_protocol(FU_DEVICE(self), "com.dell.kestrel"); fu_device_add_vendor_id(FU_DEVICE(self), "USB:0x413C"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_THUNDERBOLT); fu_device_set_summary(FU_DEVICE(self), "Dell Dock Retimer"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INSTALL_SKIP_VERSION_CHECK); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_EXPLICIT_ORDER); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PROXY_FOR_OPEN); } static void fu_dell_kestrel_dpmux_class_init(FuDellKestrelDpmuxClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_dell_kestrel_dpmux_write; device_class->setup = fu_dell_kestrel_dpmux_setup; device_class->set_progress = fu_dell_kestrel_dpmux_set_progress; device_class->convert_version = fu_dell_kestrel_dpmux_convert_version; } FuDellKestrelDpmux * fu_dell_kestrel_dpmux_new(FuDevice *proxy) { FuContext *ctx = fu_device_get_context(proxy); FuDellKestrelDpmux *self = NULL; self = g_object_new(FU_TYPE_DELL_KESTREL_DPMUX, "context", ctx, NULL); fu_device_set_proxy(FU_DEVICE(self), proxy); return self; } fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-dpmux.h000066400000000000000000000005661501337203100232740ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_DELL_KESTREL_DPMUX (fu_dell_kestrel_dpmux_get_type()) G_DECLARE_FINAL_TYPE(FuDellKestrelDpmux, fu_dell_kestrel_dpmux, FU, DELL_KESTREL_DPMUX, FuDevice) FuDellKestrelDpmux * fu_dell_kestrel_dpmux_new(FuDevice *proxy); fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-ec.c000066400000000000000000000600151501337203100225140ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-dell-kestrel-common.h" /* Private structure */ struct _FuDellKestrelEc { FuDellKestrelHidDevice parent_instance; FuStructDellKestrelDockData *dock_data; FuStructDellKestrelDockInfo *dock_info; FuDellDockBaseType base_type; FuDellKestrelDockSku base_sku; }; G_DEFINE_TYPE(FuDellKestrelEc, fu_dell_kestrel_ec, FU_TYPE_DELL_KESTREL_HID_DEVICE) static FuStructDellKestrelDockInfoEcQueryEntry * fu_dell_kestrel_ec_dev_entry(FuDellKestrelEc *self, FuDellKestrelEcDevType dev_type, FuDellKestrelEcDevSubtype subtype, FuDellKestrelEcDevInstance instance) { g_autoptr(FuStructDellKestrelDockInfoHeader) hdr = NULL; guint num = 0; hdr = fu_struct_dell_kestrel_dock_info_get_header(self->dock_info); num = fu_struct_dell_kestrel_dock_info_header_get_total_devices(hdr); if (num < 1) { g_debug("no device found in dock info hdr"); return NULL; } for (guint i = 0; i < num; i++) { g_autoptr(FuStructDellKestrelDockInfoEcQueryEntry) comp_dev = NULL; g_autoptr(FuStructDellKestrelDockInfoEcAddrMap) comp_info = NULL; comp_dev = fu_struct_dell_kestrel_dock_info_get_devices(self->dock_info, i); comp_info = fu_struct_dell_kestrel_dock_info_ec_query_entry_get_ec_addr_map(comp_dev); if (dev_type != fu_struct_dell_kestrel_dock_info_ec_addr_map_get_device_type(comp_info)) continue; if (subtype != 0 && subtype != fu_struct_dell_kestrel_dock_info_ec_addr_map_get_subtype(comp_info)) continue; /* vary by instance index */ if (dev_type == FU_DELL_KESTREL_EC_DEV_TYPE_PD && instance != fu_struct_dell_kestrel_dock_info_ec_addr_map_get_instance(comp_info)) continue; return g_steal_pointer(&comp_dev); } return NULL; } gboolean fu_dell_kestrel_ec_is_dev_present(FuDellKestrelEc *self, FuDellKestrelEcDevType dev_type, FuDellKestrelEcDevSubtype subtype, FuDellKestrelEcDevInstance instance) { g_autoptr(FuStructDellKestrelDockInfoEcQueryEntry) dev_entry = NULL; dev_entry = fu_dell_kestrel_ec_dev_entry(self, dev_type, subtype, instance); return dev_entry != NULL; } const gchar * fu_dell_kestrel_ec_devicetype_to_str(FuDellKestrelEcDevType dev_type, FuDellKestrelEcDevSubtype subtype, FuDellKestrelEcDevInstance instance) { switch (dev_type) { case FU_DELL_KESTREL_EC_DEV_TYPE_MAIN_EC: return "EC"; case FU_DELL_KESTREL_EC_DEV_TYPE_PD: if (subtype == FU_DELL_KESTREL_EC_DEV_SUBTYPE_TI) { if (instance == FU_DELL_KESTREL_EC_DEV_INSTANCE_TI_UP5) return "PD"; if (instance == FU_DELL_KESTREL_EC_DEV_INSTANCE_TI_UP15) return "PD UP15"; if (instance == FU_DELL_KESTREL_EC_DEV_INSTANCE_TI_UP17) return "PD UP17"; } return NULL; case FU_DELL_KESTREL_EC_DEV_TYPE_USBHUB: if (subtype == FU_DELL_KESTREL_EC_DEV_SUBTYPE_RTS0) return "USB Hub RTS0"; if (subtype == FU_DELL_KESTREL_EC_DEV_SUBTYPE_RTS5) return "USB Hub RTS5"; return NULL; case FU_DELL_KESTREL_EC_DEV_TYPE_MST: if (subtype == FU_DELL_KESTREL_EC_DEV_SUBTYPE_VMM8) return "MST VMM8"; if (subtype == FU_DELL_KESTREL_EC_DEV_SUBTYPE_VMM9) return "MST VMM9"; return NULL; case FU_DELL_KESTREL_EC_DEV_TYPE_TBT: if (subtype == FU_DELL_KESTREL_EC_DEV_SUBTYPE_TR) return "TR"; if (subtype == FU_DELL_KESTREL_EC_DEV_SUBTYPE_GR) return "GR"; if (subtype == FU_DELL_KESTREL_EC_DEV_SUBTYPE_BR) return "BR"; return NULL; case FU_DELL_KESTREL_EC_DEV_TYPE_QI: return "QI"; case FU_DELL_KESTREL_EC_DEV_TYPE_DP_MUX: return "Retimer"; case FU_DELL_KESTREL_EC_DEV_TYPE_LAN: return "LAN"; case FU_DELL_KESTREL_EC_DEV_TYPE_FAN: return "Fan"; case FU_DELL_KESTREL_EC_DEV_TYPE_RMM: return "RMM"; case FU_DELL_KESTREL_EC_DEV_TYPE_WTPD: return "WT PD"; default: return NULL; } } FuDellDockBaseType fu_dell_kestrel_ec_get_dock_type(FuDellKestrelEc *self) { return self->base_type; } FuDellKestrelDockSku fu_dell_kestrel_ec_get_dock_sku(FuDellKestrelEc *self) { return self->base_sku; } static gboolean fu_dell_kestrel_ec_read(FuDellKestrelEc *self, FuDellKestrelEcCmd cmd, GByteArray *res, GError **error) { if (!fu_dell_kestrel_hid_device_i2c_read(FU_DELL_KESTREL_HID_DEVICE(self), cmd, res, 800, error)) { g_prefix_error(error, "read over HID-I2C failed: "); return FALSE; } return TRUE; } static gboolean fu_dell_kestrel_ec_write(FuDellKestrelEc *self, GByteArray *buf, GError **error) { g_return_val_if_fail(buf->len > 1, FALSE); if (!fu_dell_kestrel_hid_device_i2c_write(FU_DELL_KESTREL_HID_DEVICE(self), buf, error)) { g_prefix_error(error, "write over HID-I2C failed: "); return FALSE; } return TRUE; } static gboolean fu_dell_kestrel_ec_create_node(FuDellKestrelEc *self, FuDevice *new_device, GError **error) { g_autoptr(FuDeviceLocker) locker = NULL; locker = fu_device_locker_new(new_device, error); if (locker == NULL) return FALSE; /* setup relationship */ fu_device_add_child(FU_DEVICE(self), FU_DEVICE(new_device)); return TRUE; } static gboolean fu_dell_kestrel_ec_probe_package(FuDellKestrelEc *self, GError **error) { g_autoptr(FuDellKestrelPackage) pkg_dev = NULL; pkg_dev = fu_dell_kestrel_package_new(FU_DEVICE(self)); return fu_dell_kestrel_ec_create_node(self, FU_DEVICE(pkg_dev), error); } static gboolean fu_dell_kestrel_ec_probe_pd(FuDellKestrelEc *self, FuDellKestrelEcDevType dev_type, FuDellKestrelEcDevSubtype subtype, FuDellKestrelEcDevInstance instance, GError **error) { g_autoptr(FuDellKestrelPd) pd_dev = NULL; g_autoptr(FuStructDellKestrelDockInfoEcQueryEntry) dev_entry = NULL; dev_entry = fu_dell_kestrel_ec_dev_entry(self, dev_type, subtype, instance); if (dev_entry == NULL) return TRUE; pd_dev = fu_dell_kestrel_pd_new(FU_DEVICE(self), subtype, instance); return fu_dell_kestrel_ec_create_node(self, FU_DEVICE(pd_dev), error); } static gboolean fu_dell_kestrel_ec_probe_subcomponents(FuDellKestrelEc *self, GError **error) { g_autoptr(FuStructDellKestrelDockInfoEcQueryEntry) dev_entry_ilan = NULL; g_autoptr(FuStructDellKestrelDockInfoEcQueryEntry) dev_entry_dpmux = NULL; g_autoptr(FuStructDellKestrelDockInfoEcQueryEntry) dev_entry_wt = NULL; /* Package */ if (!fu_dell_kestrel_ec_probe_package(self, error)) return FALSE; /* PD UP5 */ if (!fu_dell_kestrel_ec_probe_pd(self, FU_DELL_KESTREL_EC_DEV_TYPE_PD, FU_DELL_KESTREL_EC_DEV_SUBTYPE_TI, FU_DELL_KESTREL_EC_DEV_INSTANCE_TI_UP5, error)) return FALSE; /* PD UP15 */ if (!fu_dell_kestrel_ec_probe_pd(self, FU_DELL_KESTREL_EC_DEV_TYPE_PD, FU_DELL_KESTREL_EC_DEV_SUBTYPE_TI, FU_DELL_KESTREL_EC_DEV_INSTANCE_TI_UP15, error)) return FALSE; /* PD UP17 */ if (!fu_dell_kestrel_ec_probe_pd(self, FU_DELL_KESTREL_EC_DEV_TYPE_PD, FU_DELL_KESTREL_EC_DEV_SUBTYPE_TI, FU_DELL_KESTREL_EC_DEV_INSTANCE_TI_UP17, error)) return FALSE; /* DP MUX | Retimer */ dev_entry_dpmux = fu_dell_kestrel_ec_dev_entry(self, FU_DELL_KESTREL_EC_DEV_TYPE_DP_MUX, 0, 0); if (dev_entry_dpmux != NULL) { g_autoptr(FuDellKestrelDpmux) dpmux_device = NULL; dpmux_device = fu_dell_kestrel_dpmux_new(FU_DEVICE(self)); if (!fu_dell_kestrel_ec_create_node(self, FU_DEVICE(dpmux_device), error)) return FALSE; } /* WT PD */ dev_entry_wt = fu_dell_kestrel_ec_dev_entry(self, FU_DELL_KESTREL_EC_DEV_TYPE_WTPD, 0, 0); if (dev_entry_wt != NULL) { g_autoptr(FuDellKestrelWtpd) wt_dev = NULL; wt_dev = fu_dell_kestrel_wtpd_new(FU_DEVICE(self)); if (!fu_dell_kestrel_ec_create_node(self, FU_DEVICE(wt_dev), error)) return FALSE; } /* LAN */ dev_entry_ilan = fu_dell_kestrel_ec_dev_entry(self, FU_DELL_KESTREL_EC_DEV_TYPE_LAN, 0, 0); if (dev_entry_ilan != NULL) { g_autoptr(FuDellKestrelIlan) ilan_device = NULL; ilan_device = fu_dell_kestrel_ilan_new(FU_DEVICE(self)); if (!fu_dell_kestrel_ec_create_node(self, FU_DEVICE(ilan_device), error)) return FALSE; /* max firmware size */ if (fu_struct_dell_kestrel_dock_data_get_board_id(self->dock_data) < 0x4) fu_device_set_firmware_size(FU_DEVICE(ilan_device), 2 * 1024 * 1024); else fu_device_set_firmware_size(FU_DEVICE(ilan_device), 1 * 1024 * 1024); } return TRUE; } static gboolean fu_dell_kestrel_ec_dock_type_extract(FuDellKestrelEc *self, GError **error) { FuDellDockBaseType dock_type = fu_dell_kestrel_ec_get_dock_type(self); FuDellKestrelEcDevType dev_type = FU_DELL_KESTREL_EC_DEV_TYPE_MAIN_EC; /* don't change error type, the plugin ignores it */ if (dock_type != FU_DELL_DOCK_BASE_TYPE_KESTREL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "No valid dock was found"); return FALSE; } /* this will trigger setting up all the quirks */ fu_device_add_instance_u8(FU_DEVICE(self), "DOCKTYPE", dock_type); fu_device_add_instance_u8(FU_DEVICE(self), "DEVTYPE", dev_type); fu_device_build_instance_id(FU_DEVICE(self), error, "USB", "VID", "PID", "DOCKTYPE", "DEVTYPE", NULL); return TRUE; } static gboolean fu_dell_kestrel_ec_dock_type_cmd(FuDellKestrelEc *self, GError **error) { FuDellKestrelEcCmd cmd = FU_DELL_KESTREL_EC_CMD_GET_DOCK_TYPE; gsize length = 1; g_autoptr(GByteArray) res = g_byte_array_new_take(g_malloc0(length), length); /* expect response 1 byte */ if (!fu_dell_kestrel_ec_read(self, cmd, res, error)) { g_prefix_error(error, "Failed to query dock type: "); return FALSE; } self->base_type = res->data[0]; /* check dock type to proceed with this plugin or exit as unsupported */ return fu_dell_kestrel_ec_dock_type_extract(self, error); } static gboolean fu_dell_kestrel_ec_dock_info_cmd(FuDellKestrelEc *self, GError **error) { FuDellKestrelEcCmd cmd = FU_DELL_KESTREL_EC_CMD_GET_DOCK_INFO; g_autoptr(GByteArray) res = fu_struct_dell_kestrel_dock_info_new(); /* get dock info over HID */ if (!fu_dell_kestrel_ec_read(self, cmd, res, error)) { g_prefix_error(error, "Failed to query dock info: "); return FALSE; } self->dock_info = fu_struct_dell_kestrel_dock_info_parse(res->data, res->len, 0, error); return TRUE; } static gboolean fu_dell_kestrel_ec_dock_data_extract(FuDellKestrelEc *self, GError **error) { g_autofree gchar *mkt_name = NULL; g_autofree gchar *serial = NULL; g_autofree gchar *service_tag = NULL; /* set FuDevice name */ mkt_name = fu_struct_dell_kestrel_dock_data_get_marketing_name(self->dock_data); fu_device_set_name(FU_DEVICE(self), mkt_name); /* set FuDevice serial */ service_tag = fu_struct_dell_kestrel_dock_data_get_service_tag(self->dock_data); serial = g_strdup_printf("%.7s/%016" G_GUINT64_FORMAT, service_tag, fu_struct_dell_kestrel_dock_data_get_module_serial(self->dock_data)); fu_device_set_serial(FU_DEVICE(self), serial); return TRUE; } static gboolean fu_dell_kestrel_ec_dock_data_cmd(FuDellKestrelEc *self, GError **error) { FuDellKestrelEcCmd cmd = FU_DELL_KESTREL_EC_CMD_GET_DOCK_DATA; g_autoptr(GByteArray) res = fu_struct_dell_kestrel_dock_data_new(); /* get dock data over HID */ if (!fu_dell_kestrel_ec_read(self, cmd, res, error)) { g_prefix_error(error, "Failed to query dock data: "); return FALSE; } if (self->dock_data != NULL) fu_struct_dell_kestrel_dock_data_unref(self->dock_data); self->dock_data = fu_struct_dell_kestrel_dock_data_parse(res->data, res->len, 0, error); if (self->dock_data == NULL) return FALSE; if (!fu_dell_kestrel_ec_dock_data_extract(self, error)) return FALSE; return TRUE; } gboolean fu_dell_kestrel_ec_is_dock_ready4update(FuDevice *device, GError **error) { FuDellKestrelEc *self = FU_DELL_KESTREL_EC(device); guint16 bitmask_fw_update_pending = 1 << 8; guint32 dock_status = 0; if (!fu_dell_kestrel_ec_dock_data_cmd(self, error)) return FALSE; dock_status = fu_struct_dell_kestrel_dock_data_get_dock_status(self->dock_data); if ((dock_status & bitmask_fw_update_pending) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "dock status (%x) has pending updates, unavailable for now.", dock_status); return FALSE; } return TRUE; } gboolean fu_dell_kestrel_ec_own_dock(FuDellKestrelEc *self, gboolean lock, GError **error) { g_autoptr(GByteArray) st_req = fu_struct_dell_kestrel_ec_databytes_new(); g_autoptr(GError) error_local = NULL; g_autofree gchar *msg = NULL; guint16 bitmask = 0x0; fu_struct_dell_kestrel_ec_databytes_set_cmd(st_req, FU_DELL_KESTREL_EC_CMD_SET_MODIFY_LOCK); fu_struct_dell_kestrel_ec_databytes_set_data_sz(st_req, 2); if (lock) { msg = g_strdup("own the dock"); bitmask = 0xFFFF; } else { msg = g_strdup("relesae the dock"); bitmask = 0x0000; } if (!fu_struct_dell_kestrel_ec_databytes_set_data(st_req, (const guint8 *)&bitmask, sizeof(bitmask), error)) return FALSE; fu_device_sleep(FU_DEVICE(self), 1000); if (!fu_dell_kestrel_ec_write(self, st_req, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) g_debug("ignoring: %s", error_local->message); else { g_propagate_error(error, g_steal_pointer(&error_local)); g_prefix_error(error, "failed to %s", msg); return FALSE; } } g_debug("%s successfully", msg); return TRUE; } gboolean fu_dell_kestrel_ec_run_passive_update(FuDellKestrelEc *self, GError **error) { guint max_tries = 2; g_autoptr(GByteArray) st_req = fu_struct_dell_kestrel_ec_databytes_new(); const guint8 bitmap = 0x07; /* ec included in cmd, set bit2 in data for tbt */ fu_struct_dell_kestrel_ec_databytes_set_cmd(st_req, FU_DELL_KESTREL_EC_CMD_SET_PASSIVE); fu_struct_dell_kestrel_ec_databytes_set_data_sz(st_req, 1); if (!fu_struct_dell_kestrel_ec_databytes_set_data(st_req, (const guint8 *)&bitmap, sizeof(bitmap), error)) return FALSE; for (guint i = 1; i <= max_tries; i++) { g_debug("register passive update (uod) flow (%u/%u)", i, max_tries); if (!fu_dell_kestrel_ec_write(self, st_req, error)) { g_prefix_error(error, "failed to register uod flow: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), 100); } return TRUE; } static gboolean fu_dell_kestrel_ec_set_dock_sku(FuDellKestrelEc *self, GError **error) { if (self->base_type == FU_DELL_DOCK_BASE_TYPE_KESTREL) { g_autoptr(FuStructDellKestrelDockInfoEcQueryEntry) dev_entry = NULL; /* TBT type yet available, do workaround */ dev_entry = fu_dell_kestrel_ec_dev_entry(self, FU_DELL_KESTREL_EC_DEV_TYPE_TBT, FU_DELL_KESTREL_EC_DEV_SUBTYPE_BR, 0); if (dev_entry != NULL) { self->base_sku = FU_DELL_KESTREL_DOCK_SKU_T5; return TRUE; } dev_entry = fu_dell_kestrel_ec_dev_entry(self, FU_DELL_KESTREL_EC_DEV_TYPE_TBT, FU_DELL_KESTREL_EC_DEV_SUBTYPE_GR, 0); if (dev_entry != NULL) { self->base_sku = FU_DELL_KESTREL_DOCK_SKU_T4; return TRUE; } self->base_sku = FU_DELL_KESTREL_DOCK_SKU_DPALT; return TRUE; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "unsupported dock type: %x", self->base_type); return FALSE; } guint32 fu_dell_kestrel_ec_get_pd_version(FuDellKestrelEc *self, FuDellKestrelEcDevSubtype subtype, FuDellKestrelEcDevInstance instance) { FuDellKestrelEcDevType dev_type = FU_DELL_KESTREL_EC_DEV_TYPE_PD; g_autoptr(FuStructDellKestrelDockInfoEcQueryEntry) dev_entry = NULL; dev_entry = fu_dell_kestrel_ec_dev_entry(self, dev_type, subtype, instance); return (dev_entry == NULL) ? 0 : fu_struct_dell_kestrel_dock_info_ec_query_entry_get_version_32(dev_entry); } guint32 fu_dell_kestrel_ec_get_ilan_version(FuDellKestrelEc *self) { FuDellKestrelEcDevType dev_type = FU_DELL_KESTREL_EC_DEV_TYPE_LAN; g_autoptr(FuStructDellKestrelDockInfoEcQueryEntry) dev_entry = NULL; dev_entry = fu_dell_kestrel_ec_dev_entry(self, dev_type, 0, 0); return (dev_entry == NULL) ? 0 : fu_struct_dell_kestrel_dock_info_ec_query_entry_get_version_32(dev_entry); } guint32 fu_dell_kestrel_ec_get_wtpd_version(FuDellKestrelEc *self) { FuDellKestrelEcDevType dev_type = FU_DELL_KESTREL_EC_DEV_TYPE_WTPD; g_autoptr(FuStructDellKestrelDockInfoEcQueryEntry) dev_entry = NULL; dev_entry = fu_dell_kestrel_ec_dev_entry(self, dev_type, 0, 0); return (dev_entry == NULL) ? 0 : fu_struct_dell_kestrel_dock_info_ec_query_entry_get_version_32(dev_entry); } guint32 fu_dell_kestrel_ec_get_dpmux_version(FuDellKestrelEc *self) { FuDellKestrelEcDevType dev_type = FU_DELL_KESTREL_EC_DEV_TYPE_DP_MUX; g_autoptr(FuStructDellKestrelDockInfoEcQueryEntry) dev_entry = NULL; dev_entry = fu_dell_kestrel_ec_dev_entry(self, dev_type, 0, 0); return (dev_entry == NULL) ? 0 : fu_struct_dell_kestrel_dock_info_ec_query_entry_get_version_32(dev_entry); } guint32 fu_dell_kestrel_ec_get_rmm_version(FuDellKestrelEc *self) { FuDellKestrelEcDevType dev_type = FU_DELL_KESTREL_EC_DEV_TYPE_RMM; g_autoptr(FuStructDellKestrelDockInfoEcQueryEntry) dev_entry = NULL; dev_entry = fu_dell_kestrel_ec_dev_entry(self, dev_type, 0, 0); return (dev_entry == NULL) ? 0 : fu_struct_dell_kestrel_dock_info_ec_query_entry_get_version_32(dev_entry); } static guint32 fu_dell_kestrel_ec_get_ec_version(FuDellKestrelEc *self) { FuDellKestrelEcDevType dev_type = FU_DELL_KESTREL_EC_DEV_TYPE_MAIN_EC; g_autoptr(FuStructDellKestrelDockInfoEcQueryEntry) dev_entry = NULL; dev_entry = fu_dell_kestrel_ec_dev_entry(self, dev_type, 0, 0); return (dev_entry == NULL) ? 0 : fu_struct_dell_kestrel_dock_info_ec_query_entry_get_version_32(dev_entry); } guint32 fu_dell_kestrel_ec_get_package_version(FuDellKestrelEc *self) { return fu_struct_dell_kestrel_dock_data_get_dock_firmware_pkg_ver(self->dock_data); } gboolean fu_dell_kestrel_ec_commit_package(FuDellKestrelEc *self, GInputStream *stream, GError **error) { g_autoptr(GByteArray) st_req = fu_struct_dell_kestrel_ec_databytes_new(); g_autoptr(GByteArray) buf = NULL; gsize streamsz = 0; /* verify package length */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz != FU_STRUCT_DELL_KESTREL_PACKAGE_FW_VERSIONS_SIZE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "Invalid package size %" G_GSIZE_FORMAT, streamsz); return FALSE; } /* get the data bytes */ buf = fu_input_stream_read_byte_array(stream, 0, FU_STRUCT_DELL_KESTREL_PACKAGE_FW_VERSIONS_SIZE, NULL, error); fu_struct_dell_kestrel_ec_databytes_set_cmd(st_req, FU_DELL_KESTREL_EC_CMD_SET_DOCK_PKG); fu_struct_dell_kestrel_ec_databytes_set_data_sz(st_req, streamsz); if (!fu_struct_dell_kestrel_ec_databytes_set_data(st_req, buf->data, buf->len, error)) return FALSE; fu_dump_raw(G_LOG_DOMAIN, "->PACKAGE", st_req->data, st_req->len); if (!fu_dell_kestrel_ec_write(self, st_req, error)) { g_prefix_error(error, "Failed to commit package: "); return FALSE; } return TRUE; } static gboolean fu_dell_kestrel_ec_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDellKestrelEc *self = FU_DELL_KESTREL_EC(device); return fu_dell_kestrel_hid_device_write_firmware(FU_DELL_KESTREL_HID_DEVICE(self), firmware, progress, FU_DELL_KESTREL_EC_DEV_TYPE_MAIN_EC, 0, error); } static gboolean fu_dell_kestrel_ec_query_cb(FuDevice *device, gpointer user_data, GError **error) { FuDellKestrelEc *self = FU_DELL_KESTREL_EC(device); /* dock data */ if (!fu_dell_kestrel_ec_dock_data_cmd(self, error)) return FALSE; /* dock info */ if (!fu_dell_kestrel_ec_dock_info_cmd(self, error)) return FALSE; /* set internal dock sku, must after dock info */ if (!fu_dell_kestrel_ec_set_dock_sku(self, error)) return FALSE; return TRUE; } static gboolean fu_dell_kestrel_ec_reload(FuDevice *device, GError **error) { FuDellKestrelEc *self = FU_DELL_KESTREL_EC(device); /* if query looks bad, wait a few seconds and retry */ if (!fu_device_retry_full(FU_DEVICE(self), fu_dell_kestrel_ec_query_cb, 10, 2000, NULL, error)) { g_prefix_error(error, "failed to query dock ec: "); return FALSE; } return TRUE; } static gboolean fu_dell_kestrel_ec_setup(FuDevice *device, GError **error) { FuDellKestrelEc *self = FU_DELL_KESTREL_EC(device); guint32 ec_version = 0; /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_dell_kestrel_ec_parent_class)->setup(device, error)) return FALSE; /* get dock type */ if (!fu_dell_kestrel_ec_dock_type_cmd(self, error)) return FALSE; /* if query looks bad, wait a few seconds and retry */ if (!fu_device_retry_full(device, fu_dell_kestrel_ec_query_cb, 10, 2000, NULL, error)) { g_prefix_error(error, "failed to query dock ec: "); return FALSE; } /* setup version */ ec_version = fu_dell_kestrel_ec_get_ec_version(self); fu_device_set_version_raw(device, ec_version); /* create the subcomponents */ if (!fu_dell_kestrel_ec_probe_subcomponents(self, error)) return FALSE; g_debug("dell-kestrel-ec->setup done successfully"); return TRUE; } static gchar * fu_dell_kestrel_ec_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32_hex(version_raw, fu_device_get_version_format(device)); } static gboolean fu_dell_kestrel_ec_open(FuDevice *device, GError **error) { /* FuUdevDevice->open */ return FU_DEVICE_CLASS(fu_dell_kestrel_ec_parent_class)->open(device, error); } static void fu_dell_kestrel_ec_finalize(GObject *object) { FuDellKestrelEc *self = FU_DELL_KESTREL_EC(object); if (self->dock_data != NULL) fu_struct_dell_kestrel_dock_data_unref(self->dock_data); if (self->dock_info != NULL) fu_struct_dell_kestrel_dock_info_unref(self->dock_info); G_OBJECT_CLASS(fu_dell_kestrel_ec_parent_class)->finalize(object); } static void fu_dell_kestrel_ec_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_dell_kestrel_ec_init(FuDellKestrelEc *self) { fu_device_add_protocol(FU_DEVICE(self), "com.dell.kestrel"); fu_device_add_vendor_id(FU_DEVICE(self), "USB:0x413C"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_DOCK_USB); fu_device_set_summary(FU_DEVICE(self), "Dell Dock EC"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SELF_RECOVERY); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INSTALL_SKIP_VERSION_CHECK); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_SKIPS_RESTART); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_EXPLICIT_ORDER); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_RETRY_OPEN); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_FLAGS); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); } static void fu_dell_kestrel_ec_class_init(FuDellKestrelEcClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->finalize = fu_dell_kestrel_ec_finalize; device_class->open = fu_dell_kestrel_ec_open; device_class->setup = fu_dell_kestrel_ec_setup; device_class->write_firmware = fu_dell_kestrel_ec_write_firmware; device_class->reload = fu_dell_kestrel_ec_reload; device_class->set_progress = fu_dell_kestrel_ec_set_progress; device_class->convert_version = fu_dell_kestrel_ec_convert_version; } FuDellKestrelEc * fu_dell_kestrel_ec_new(FuDevice *device, gboolean uod) { FuDellKestrelEc *self = NULL; FuContext *ctx = fu_device_get_context(device); self = g_object_new(FU_TYPE_DELL_KESTREL_EC, "context", ctx, NULL); fu_device_incorporate(FU_DEVICE(self), device, FU_DEVICE_INCORPORATE_FLAG_ALL); fu_device_set_logical_id(FU_DEVICE(self), "ec"); if (uod) fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE); return self; } fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-ec.h000066400000000000000000000036321501337203100225230ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include "fu-dell-kestrel-common.h" #include "fu-dell-kestrel-ec-struct.h" #include "fu-dell-kestrel-hid-device.h" #define FU_TYPE_DELL_KESTREL_EC (fu_dell_kestrel_ec_get_type()) G_DECLARE_FINAL_TYPE(FuDellKestrelEc, fu_dell_kestrel_ec, FU, DELL_KESTREL_EC, FuDellKestrelHidDevice) FuDellKestrelEc * fu_dell_kestrel_ec_new(FuDevice *device, gboolean uod); void fu_dell_kestrel_ec_enable_tbt_passive(FuDellKestrelEc *self); gboolean fu_dell_kestrel_ec_own_dock(FuDellKestrelEc *self, gboolean lock, GError **error); gboolean fu_dell_kestrel_ec_run_passive_update(FuDellKestrelEc *self, GError **error); guint32 fu_dell_kestrel_ec_get_pd_version(FuDellKestrelEc *self, FuDellKestrelEcDevSubtype subtype, FuDellKestrelEcDevInstance instance); guint32 fu_dell_kestrel_ec_get_ilan_version(FuDellKestrelEc *self); guint32 fu_dell_kestrel_ec_get_wtpd_version(FuDellKestrelEc *self); guint32 fu_dell_kestrel_ec_get_rmm_version(FuDellKestrelEc *self); guint32 fu_dell_kestrel_ec_get_dpmux_version(FuDellKestrelEc *self); guint32 fu_dell_kestrel_ec_get_package_version(FuDellKestrelEc *self); gboolean fu_dell_kestrel_ec_commit_package(FuDellKestrelEc *self, GInputStream *stream, GError **error); FuDellDockBaseType fu_dell_kestrel_ec_get_dock_type(FuDellKestrelEc *self); FuDellKestrelDockSku fu_dell_kestrel_ec_get_dock_sku(FuDellKestrelEc *self); const gchar * fu_dell_kestrel_ec_devicetype_to_str(FuDellKestrelEcDevType dev_type, FuDellKestrelEcDevSubtype subtype, FuDellKestrelEcDevInstance instance); gboolean fu_dell_kestrel_ec_is_dock_ready4update(FuDevice *device, GError **error); gboolean fu_dell_kestrel_ec_is_dev_present(FuDellKestrelEc *self, FuDellKestrelEcDevType dev_type, FuDellKestrelEcDevSubtype subtype, FuDellKestrelEcDevInstance instance); fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-ec.rs000066400000000000000000000054711501337203100227230ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #[repr(u8)] enum FuDellDockBaseType { Unknown = 0x0, Kestrel = 0x07, } #[repr(u8)] enum FuDellKestrelEcCmd { SetDockPkg = 0x01, GetDockInfo = 0x02, GetDockData = 0x03, GetDockType = 0x05, SetModifyLock = 0x0a, SetFwupMode = 0x0b, SetPassive = 0x0d, } #[repr(C, packed)] #[derive(New)] struct FuStructDellKestrelEcDatabytes { cmd: FuDellKestrelEcCmd, data_sz: u8, data: [u8; 126], } #[repr(u8)] enum FuDellKestrelEcLocation { Base = 0x00, Module, } #[repr(u8)] enum FuDellKestrelEcDevType{ MainEc = 0x00, Pd, Usbhub, Mst, Tbt, Qi, DpMux, Lan, Fan, Rmm, Wtpd, } #[repr(u8)] enum FuDellKestrelEcDevSubtype { Rts0 = 0x00, Rts5 = 0x01, Tr = 0x00, Gr = 0x01, Br = 0x02, Vmm8 = 0x00, Vmm9 = 0x01, Ti = 0x00, } #[repr(u8)] enum FuDellKestrelEcDevInstance { TiUp5 = 0, TiUp15, TiUp17, } #[repr(u8)] enum FuDellKestrelDockSku { Dpalt = 0x01, T4, T5, } #[repr(C, packed)] #[derive(New, Getters, Parse)] struct FuStructDellKestrelDockData { dock_configuration: u8, dock_type: u8, reserved: u16le, module_type: u16le, board_id: u16le, reserved: u16le, reserved: u16le, dock_firmware_pkg_ver: u32be, module_serial: u64le, reserved: u64le, service_tag: [char; 7], marketing_name: [char; 32], reserved: u32le, reserved: u32le, reserved: u32le, reserved: u8, dock_status: u32le, reserved: u16le, reserved: u16le, dock_mac_addr: [u8; 6], reserved: u32le, reserved: u32le, reserved: u32le, reserved: u32le, reserved: u16le, eppid: u8, reserved: [u8; 74], } #[repr(C, packed)] struct FuStructDellKestrelPackageFwVersions { pkg_ver: u32le, ec_ver: u32le, mst_ver: u32le, rts0_g2_ver: u32le, rts5_g2_ver: u32le, rts0_g1_ver: u32le, tbt_ver: u32le, pd_up5_ver: u32le, pd_up15_ver: u32le, pd_up17_ver: u32le, dpmux_ver: u32le, rmm_ver: u32le, lan_ver: u32le, reserved: [u32le; 3], } /* dock info */ #[repr(C, packed)] #[derive(Getters)] struct FuStructDellKestrelDockInfoEcAddrMap { location: u8, device_type: u8, subtype: u8, arg: u8, instance: u8, } #[repr(C, packed)] #[derive(Getters)] struct FuStructDellKestrelDockInfoEcQueryEntry { ec_addr_map: FuStructDellKestrelDockInfoEcAddrMap, version_32: u32be, } #[repr(C, packed)] struct FuStructDellKestrelDockInfoHeader { total_devices: u8, first_index: u8, last_index: u8, } #[repr(C, packed)] #[derive(New, Getters, Parse)] struct FuStructDellKestrelDockInfo { header: FuStructDellKestrelDockInfoHeader, devices: [FuStructDellKestrelDockInfoEcQueryEntry; 20], } fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-hid-device.c000066400000000000000000000306001501337203100241230ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-dell-kestrel-hid-device.h" #include "fu-dell-kestrel-hid-struct.h" G_DEFINE_TYPE(FuDellKestrelHidDevice, fu_dell_kestrel_hid_device, FU_TYPE_HID_DEVICE) /* Used for EC HID communication */ #define FU_DELL_KESTREL_HID_TIMEOUT 300 #define FU_DELL_KESTREL_HID_CMD_FWUPDATE 0xAB #define FU_DELL_KESTREL_HID_EXT_FWUPDATE 0x80 #define FU_DELL_KESTREL_HID_SUBCMD_FWUPDATE 0x00 #define FU_DELL_KESTREL_HID_DEV_EC_CHUNK_SZ 160000 #define FU_DELL_KESTREL_HID_DEV_PD_CHUNK_SZ 190000 #define FU_DELL_KESTREL_HID_DEV_ANY_CHUNK_SZ 180000 #define FU_DELL_KESTREL_HID_DEV_NO_CHUNK_SZ G_MAXSIZE #define FU_DELL_KESTREL_HID_DATA_PAGE_SZ 192 #define FU_DELL_KESTREL_HID_RESPONSE_LENGTH 0x03 #define FU_DELL_KESTREL_HID_I2C_ADDRESS 0xec #define FU_DELL_KESTREL_HID_MAX_RETRIES 8 #define FU_DELL_KESTREL_HID_I2C_MAX_READ 192 #define FU_DELL_KESTREL_HID_I2C_MAX_WRITE 128 static gboolean fu_dell_kestrel_hid_device_write(FuDellKestrelHidDevice *self, GByteArray *buf, guint delay_ms, GError **error) { return fu_hid_device_set_report(FU_HID_DEVICE(self), 0x0, buf->data, buf->len, delay_ms, FU_HID_DEVICE_FLAG_RETRY_FAILURE, error); } static GBytes * fu_dell_kestrel_hid_device_fwup_pkg_new(FuChunk *chk, gsize fw_size, FuDellKestrelEcDevType dev_type, guint8 dev_identifier) { g_autoptr(FuStructDellKestrelHidFwUpdatePkg) fwbuf = fu_struct_dell_kestrel_hid_fw_update_pkg_new(); gsize chk_datasz = fu_chunk_get_data_sz(chk); /* header */ fu_struct_dell_kestrel_hid_fw_update_pkg_set_cmd(fwbuf, FU_DELL_KESTREL_HID_CMD_FWUPDATE); fu_struct_dell_kestrel_hid_fw_update_pkg_set_ext(fwbuf, FU_DELL_KESTREL_HID_EXT_FWUPDATE); fu_struct_dell_kestrel_hid_fw_update_pkg_set_chunk_sz( fwbuf, 7 + chk_datasz); // 7 = sizeof(command) /* command */ fu_struct_dell_kestrel_hid_fw_update_pkg_set_sub_cmd(fwbuf, FU_DELL_KESTREL_HID_SUBCMD_FWUPDATE); fu_struct_dell_kestrel_hid_fw_update_pkg_set_dev_type(fwbuf, dev_type); fu_struct_dell_kestrel_hid_fw_update_pkg_set_dev_identifier(fwbuf, dev_identifier); fu_struct_dell_kestrel_hid_fw_update_pkg_set_fw_sz(fwbuf, fw_size); /* data */ fu_byte_array_append_bytes(fwbuf, fu_chunk_get_bytes(chk)); return g_bytes_new(fwbuf->data, fwbuf->len); } static gboolean fu_dell_kestrel_hid_device_hid_set_report_cb(FuDevice *self, gpointer user_data, GError **error) { guint8 *outbuffer = (guint8 *)user_data; return fu_hid_device_set_report(FU_HID_DEVICE(self), 0x0, outbuffer, 192, FU_DELL_KESTREL_HID_TIMEOUT * 3, FU_HID_DEVICE_FLAG_NONE, error); } static gboolean fu_dell_kestrel_hid_device_hid_set_report(FuDellKestrelHidDevice *self, guint8 *outbuffer, GError **error) { return fu_device_retry(FU_DEVICE(self), fu_dell_kestrel_hid_device_hid_set_report_cb, FU_DELL_KESTREL_HID_MAX_RETRIES, outbuffer, error); } static gboolean fu_dell_kestrel_hid_device_get_report_cb(FuDevice *self, gpointer user_data, GError **error) { guint8 *inbuffer = (guint8 *)user_data; return fu_hid_device_get_report(FU_HID_DEVICE(self), 0x0, inbuffer, 192, FU_DELL_KESTREL_HID_TIMEOUT, FU_HID_DEVICE_FLAG_NONE, error); } static gboolean fu_dell_kestrel_hid_device_get_report(FuDellKestrelHidDevice *self, guint8 *inbuffer, GError **error) { return fu_device_retry_full(FU_DEVICE(self), fu_dell_kestrel_hid_device_get_report_cb, FU_DELL_KESTREL_HID_MAX_RETRIES, 2000, inbuffer, error); } gboolean fu_dell_kestrel_hid_device_i2c_write(FuDellKestrelHidDevice *self, GByteArray *cmd_buf, GError **error) { g_autoptr(FuStructDellKestrelHidCmdBuffer) buf = fu_struct_dell_kestrel_hid_cmd_buffer_new(); g_return_val_if_fail(cmd_buf->len <= FU_DELL_KESTREL_HID_I2C_MAX_WRITE, FALSE); fu_struct_dell_kestrel_hid_cmd_buffer_set_cmd(buf, FU_DELL_KESTREL_HID_CMD_WRITE_DATA); fu_struct_dell_kestrel_hid_cmd_buffer_set_ext(buf, FU_DELL_KESTREL_HID_CMD_EXT_I2C_WRITE); fu_struct_dell_kestrel_hid_cmd_buffer_set_dwregaddr(buf, 0x00); fu_struct_dell_kestrel_hid_cmd_buffer_set_bufferlen(buf, cmd_buf->len); if (!fu_struct_dell_kestrel_hid_cmd_buffer_set_databytes(buf, cmd_buf->data, cmd_buf->len, error)) return FALSE; return fu_dell_kestrel_hid_device_hid_set_report(self, buf->data, error); } gboolean fu_dell_kestrel_hid_device_i2c_read(FuDellKestrelHidDevice *self, FuDellKestrelEcCmd cmd, GByteArray *res, guint delayms, GError **error) { g_autoptr(FuStructDellKestrelHidCmdBuffer) buf = fu_struct_dell_kestrel_hid_cmd_buffer_new(); guint8 inbuf[FU_DELL_KESTREL_HID_I2C_MAX_READ] = {0xff}; fu_struct_dell_kestrel_hid_cmd_buffer_set_cmd(buf, FU_DELL_KESTREL_HID_CMD_WRITE_DATA); fu_struct_dell_kestrel_hid_cmd_buffer_set_ext(buf, FU_DELL_KESTREL_HID_CMD_EXT_I2C_READ); fu_struct_dell_kestrel_hid_cmd_buffer_set_dwregaddr(buf, cmd); fu_struct_dell_kestrel_hid_cmd_buffer_set_bufferlen(buf, res->len + 1); if (!fu_dell_kestrel_hid_device_hid_set_report(self, buf->data, error)) return FALSE; if (delayms > 0) fu_device_sleep(FU_DEVICE(self), delayms); if (!fu_dell_kestrel_hid_device_get_report(self, inbuf, error)) return FALSE; return fu_memcpy_safe(res->data, res->len, 0, inbuf, sizeof(inbuf), 1, res->len, error); } static guint fu_dell_kestrel_hid_device_get_chunk_delaytime(FuDellKestrelEcDevType dev_type) { switch (dev_type) { case FU_DELL_KESTREL_EC_DEV_TYPE_MAIN_EC: return 3 * 1000; case FU_DELL_KESTREL_EC_DEV_TYPE_RMM: return 60 * 1000; case FU_DELL_KESTREL_EC_DEV_TYPE_PD: return 15 * 1000; case FU_DELL_KESTREL_EC_DEV_TYPE_LAN: return 70 * 1000; default: return 30 * 1000; } } static gsize fu_dell_kestrel_hid_device_get_chunk_size(FuDellKestrelEcDevType dev_type) { /* return the max chunk size in bytes */ switch (dev_type) { case FU_DELL_KESTREL_EC_DEV_TYPE_MAIN_EC: return FU_DELL_KESTREL_HID_DEV_EC_CHUNK_SZ; case FU_DELL_KESTREL_EC_DEV_TYPE_PD: return FU_DELL_KESTREL_HID_DEV_PD_CHUNK_SZ; case FU_DELL_KESTREL_EC_DEV_TYPE_RMM: return FU_DELL_KESTREL_HID_DEV_NO_CHUNK_SZ; default: return FU_DELL_KESTREL_HID_DEV_ANY_CHUNK_SZ; } } static gboolean fu_dell_kestrel_hid_device_write_firmware_pages(FuDellKestrelHidDevice *self, FuChunkArray *pages, FuProgress *progress, FuDellKestrelEcDevType dev_type, guint chunk_idx, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(pages)); for (guint j = 0; j < fu_chunk_array_length(pages); j++) { g_autoptr(GByteArray) page_aligned = g_byte_array_new(); g_autoptr(FuChunk) page = NULL; g_autoptr(GError) error_local = NULL; guint page_ack_time = FU_DELL_KESTREL_HID_TIMEOUT; page = fu_chunk_array_index(pages, j, error); if (page == NULL) return FALSE; g_debug("sending chunk: %u, page: %u/%u.", chunk_idx, j, fu_chunk_array_length(pages) - 1); /* strictly align the page size with 0x00 as packet */ g_byte_array_append(page_aligned, fu_chunk_get_data(page), fu_chunk_get_data_sz(page)); fu_byte_array_set_size(page_aligned, FU_DELL_KESTREL_HID_DATA_PAGE_SZ, 0xFF); /* rmm needs extra time to ack the first page */ if (j == 0 && dev_type == FU_DELL_KESTREL_EC_DEV_TYPE_RMM) page_ack_time = 75 * 1000; /* send to ec */ if (!fu_dell_kestrel_hid_device_write(self, page_aligned, page_ack_time, &error_local)) { /* A buggy device may fail to send an acknowledgment receipt after the last page write, resulting in a timeout error. This is a known issue so waive it for now. */ if (dev_type == FU_DELL_KESTREL_EC_DEV_TYPE_LAN && j == fu_chunk_array_length(pages) - 1 && g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT)) { g_debug("ignored error: %s", error_local->message); fu_progress_step_done(progress); continue; } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "%s failed to write page: ", fu_device_get_name(FU_DEVICE(self))); return FALSE; } /* rmm needs extra time to accept incoming pages */ if (j == 0 && dev_type == FU_DELL_KESTREL_EC_DEV_TYPE_RMM) { FuDevice *dev = FU_DEVICE(self); if (fu_version_compare(fu_device_get_version(dev), "1.8.6.0", fu_device_get_version_format(dev)) < 0) { guint delay_ms = 75 * 1000; g_debug("wait %u ms before the next page", delay_ms); fu_device_sleep(FU_DEVICE(self), delay_ms); } } fu_progress_step_done(progress); } return TRUE; } static gboolean fu_dell_kestrel_hid_device_verify_chunk_result(FuDellKestrelHidDevice *self, FuDellKestrelHidEcChunkResponse *resp, GError **error) { guint8 buf[FU_DELL_KESTREL_HID_DATA_PAGE_SZ] = {0xff}; if (!fu_hid_device_get_report(FU_HID_DEVICE(self), 0x0, buf, sizeof(buf), FU_DELL_KESTREL_HID_TIMEOUT, FU_HID_DEVICE_FLAG_NONE, error)) return FALSE; return fu_memread_uint8_safe(buf, sizeof(buf), 1, (guint8 *)resp, error); } gboolean fu_dell_kestrel_hid_device_write_firmware(FuDellKestrelHidDevice *self, FuFirmware *firmware, FuProgress *progress, FuDellKestrelEcDevType dev_type, guint8 dev_identifier, GError **error) { gsize fw_sz = 0; gsize chunk_sz = fu_dell_kestrel_hid_device_get_chunk_size(dev_type); guint chunk_delay = fu_dell_kestrel_hid_device_get_chunk_delaytime(dev_type); g_autoptr(GBytes) fw = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* payload size */ fw_sz = g_bytes_get_size(fw); if (fu_firmware_get_version(firmware) != 0x0) { g_debug("writing %s firmware %s -> %s", fu_device_get_name(FU_DEVICE(self)), fu_device_get_version(FU_DEVICE(self)), fu_firmware_get_version(firmware)); } /* maximum buffer size */ chunks = fu_chunk_array_new_from_bytes(fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, chunk_sz); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); /* iterate the chunks */ for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { FuDellKestrelHidEcChunkResponse resp = FU_DELL_KESTREL_HID_EC_CHUNK_RESPONSE_UNKNOWN; g_autoptr(FuChunk) chk = NULL; g_autoptr(FuChunkArray) pages = NULL; g_autoptr(GBytes) buf = NULL; chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; /* prepend header and command to the chunk data */ buf = fu_dell_kestrel_hid_device_fwup_pkg_new(chk, fw_sz, dev_type, dev_identifier); /* slice the chunk into pages */ pages = fu_chunk_array_new_from_bytes(buf, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_DELL_KESTREL_HID_DATA_PAGE_SZ); /* write pages */ if (!fu_dell_kestrel_hid_device_write_firmware_pages( self, pages, fu_progress_get_child(progress), dev_type, i, error)) return FALSE; /* delay time */ g_debug("wait %u ms for dock to finish the chunk", chunk_delay); fu_device_sleep(FU_DEVICE(self), chunk_delay); /* get device response for the chunk in transaction */ if (!fu_dell_kestrel_hid_device_verify_chunk_result(self, &resp, error)) return FALSE; /* verify the device response */ g_debug("dock response %u to chunk[%u]: %s", resp, i, fu_dell_kestrel_hid_ec_chunk_response_to_string(resp)); switch (resp) { case FU_DELL_KESTREL_HID_EC_CHUNK_RESPONSE_UPDATE_COMPLETE: fu_progress_finished(progress); return TRUE; case FU_DELL_KESTREL_HID_EC_CHUNK_RESPONSE_SEND_NEXT_CHUNK: fu_progress_step_done(progress); break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "%s failed to write chunk[%u]: %s", fu_device_get_name(FU_DEVICE(self)), i, fu_dell_kestrel_hid_ec_chunk_response_to_string(resp)); return FALSE; } } /* success */ return TRUE; } static void fu_dell_kestrel_hid_device_init(FuDellKestrelHidDevice *self) { } static void fu_dell_kestrel_hid_device_class_init(FuDellKestrelHidDeviceClass *klass) { } fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-hid-device.h000066400000000000000000000020561501337203100241340ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #include "fu-dell-kestrel-ec-struct.h" #define FU_TYPE_DELL_KESTREL_HID_DEVICE (fu_dell_kestrel_hid_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuDellKestrelHidDevice, fu_dell_kestrel_hid_device, FU, DELL_KESTREL_HID_DEVICE, FuHidDevice) struct _FuDellKestrelHidDeviceClass { FuHidDeviceClass parent_class; }; gboolean fu_dell_kestrel_hid_device_i2c_write(FuDellKestrelHidDevice *self, GByteArray *cmd_buf, GError **error); gboolean fu_dell_kestrel_hid_device_i2c_read(FuDellKestrelHidDevice *self, FuDellKestrelEcCmd cmd, GByteArray *res, guint delayms, GError **error); gboolean fu_dell_kestrel_hid_device_write_firmware(FuDellKestrelHidDevice *self, FuFirmware *firmware, FuProgress *progress, FuDellKestrelEcDevType dev_type, guint8 dev_identifier, GError **error); fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-hid.rs000066400000000000000000000015321501337203100230720ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #[repr(u8)] enum FuDellKestrelHidCmd { WriteData = 0x40, ExtI2cWrite = 0xc6, ExtI2cRead = 0xd6, } #[repr(C, packed)] #[derive(New, Setters, Getters, Default)] struct FuStructDellKestrelHidCmdBuffer { cmd: u8, ext: u8, dwregaddr: u32le, bufferlen: u16le, parameters: [u8; 3] = 0xEC0180, // addr, length, speed extended_cmdarea: [u8; 53] = 0x00, databytes: [u8; 192] = 0x00, } #[derive(New, Setters)] struct FuStructDellKestrelHidFwUpdatePkg { cmd: u8, ext: u8, chunk_sz: u32be, sub_cmd: u8, dev_type: u8, dev_identifier: u8, fw_sz: u32be, } #[repr(u8)] #[derive(ToString)] enum FuDellKestrelHidEcChunkResponse { Unknown = 0, UpdateComplete, SendNextChunk, UpdateFailed, } fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-ilan.c000066400000000000000000000072321501337203100230520ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-dell-kestrel-common.h" struct _FuDellKestrelIlan { FuDevice parent_instance; }; G_DEFINE_TYPE(FuDellKestrelIlan, fu_dell_kestrel_ilan, FU_TYPE_DEVICE) static gchar * fu_dell_kestrel_ilan_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint16_hex(version_raw, fu_device_get_version_format(device)); } static gboolean fu_dell_kestrel_ilan_setup(FuDevice *device, GError **error) { FuDevice *proxy = fu_device_get_proxy(device); FuDellDockBaseType dock_type = fu_dell_kestrel_ec_get_dock_type(FU_DELL_KESTREL_EC(proxy)); FuDellKestrelEcDevType dev_type = FU_DELL_KESTREL_EC_DEV_TYPE_LAN; guint32 version_raw; g_autofree gchar *devname = NULL; /* name */ devname = g_strdup_printf("%s", fu_dell_kestrel_ec_devicetype_to_str(dev_type, 0, 0)); fu_device_set_name(device, devname); fu_device_set_logical_id(device, devname); /* instance ID */ fu_device_add_instance_u8(device, "DOCKTYPE", dock_type); fu_device_add_instance_u8(device, "DEVTYPE", dev_type); fu_device_build_instance_id(device, error, "EC", "DOCKTYPE", "DEVTYPE", NULL); /* version */ version_raw = fu_dell_kestrel_ec_get_ilan_version(FU_DELL_KESTREL_EC(proxy)); fu_device_set_version_raw(device, version_raw); return TRUE; } static gboolean fu_dell_kestrel_ilan_write(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDevice *proxy = fu_device_get_proxy(device); return fu_dell_kestrel_hid_device_write_firmware(FU_DELL_KESTREL_HID_DEVICE(proxy), firmware, progress, FU_DELL_KESTREL_EC_DEV_TYPE_LAN, 0, error); } static void fu_dell_kestrel_ilan_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 13, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 72, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 9, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 7, "reload"); } static void fu_dell_kestrel_ilan_init(FuDellKestrelIlan *self) { fu_device_add_protocol(FU_DEVICE(self), "com.dell.kestrel"); fu_device_add_vendor_id(FU_DEVICE(self), "USB:0x413C"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_NETWORK_WIRED); fu_device_set_summary(FU_DEVICE(self), "Dell Dock LAN"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INSTALL_SKIP_VERSION_CHECK); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PROXY_FOR_OPEN); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_EXPLICIT_ORDER); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PAIR); } static void fu_dell_kestrel_ilan_class_init(FuDellKestrelIlanClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_dell_kestrel_ilan_write; device_class->setup = fu_dell_kestrel_ilan_setup; device_class->set_progress = fu_dell_kestrel_ilan_set_progress; device_class->convert_version = fu_dell_kestrel_ilan_convert_version; } FuDellKestrelIlan * fu_dell_kestrel_ilan_new(FuDevice *proxy) { FuContext *ctx = fu_device_get_context(proxy); FuDellKestrelIlan *self = NULL; self = g_object_new(FU_TYPE_DELL_KESTREL_ILAN, "context", ctx, NULL); fu_device_set_proxy(FU_DEVICE(self), proxy); return self; } fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-ilan.h000066400000000000000000000005571501337203100230620ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_DELL_KESTREL_ILAN (fu_dell_kestrel_ilan_get_type()) G_DECLARE_FINAL_TYPE(FuDellKestrelIlan, fu_dell_kestrel_ilan, FU, DELL_KESTREL_ILAN, FuDevice) FuDellKestrelIlan * fu_dell_kestrel_ilan_new(FuDevice *proxy); fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-package.c000066400000000000000000000123141501337203100235170ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-dell-kestrel-common.h" #include "fu-dell-kestrel-ec-struct.h" #include "fu-dell-kestrel-package.h" struct _FuDellKestrelPackage { FuDevice parent_instance; }; G_DEFINE_TYPE(FuDellKestrelPackage, fu_dell_kestrel_package, FU_TYPE_DEVICE) static gchar * fu_dell_kestrel_package_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32_hex(version_raw, fu_device_get_version_format(device)); } static gboolean fu_dell_kestrel_package_setup(FuDevice *device, GError **error) { FuDevice *proxy = fu_device_get_proxy(device); FuDellKestrelDockSku dock_sku = fu_dell_kestrel_ec_get_dock_sku(FU_DELL_KESTREL_EC(proxy)); FuDellDockBaseType dock_type = fu_dell_kestrel_ec_get_dock_type(FU_DELL_KESTREL_EC(proxy)); guint32 pkg_version_raw; /* instance ID */ fu_device_add_instance_u8(device, "DOCKTYPE", dock_type); fu_device_add_instance_u8(device, "DOCKSKU", dock_sku); fu_device_add_instance_strup(device, "DEVTYPE", "PACKAGE"); fu_device_build_instance_id(device, error, "EC", "DOCKTYPE", "DOCKSKU", "DEVTYPE", NULL); /* setup version */ pkg_version_raw = fu_dell_kestrel_ec_get_package_version(FU_DELL_KESTREL_EC(proxy)); fu_device_set_version_raw(device, pkg_version_raw); return TRUE; } static gboolean fu_dell_kestrel_package_write(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDevice *proxy = fu_device_get_proxy(device); guint32 pkg_version = 0; g_autoptr(GInputStream) stream = NULL; g_autofree gchar *dynamic_version = NULL; g_return_val_if_fail(device != NULL, FALSE); g_return_val_if_fail(FU_IS_FIRMWARE(firmware), FALSE); /* get default image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* get the new package version */ if (!fu_input_stream_read_u32(stream, 0, &pkg_version, G_BIG_ENDIAN, error)) return FALSE; /* print the package version */ dynamic_version = fu_version_from_uint32_hex(pkg_version, fu_device_get_version_format(device)); g_debug("writing firmware: %s, %s -> %s", fu_device_get_name(device), fu_device_get_version(device), dynamic_version); /* write to device */ if (!fu_dell_kestrel_ec_commit_package(FU_DELL_KESTREL_EC(proxy), stream, error)) return FALSE; /* dock will reboot to re-read; this is to appease the daemon */ fu_device_set_version(device, dynamic_version); /* nocheck:set-version */ return TRUE; } static gboolean fu_dell_kestrel_package_attach(FuDevice *device, FuProgress *progress, GError **error) { FuDevice *proxy = fu_device_get_proxy(device); /* register post message */ if (fu_device_has_flag(proxy, FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE)) { g_autoptr(FwupdRequest) request = fwupd_request_new(); fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_POST); fwupd_request_set_id(request, FWUPD_REQUEST_ID_REMOVE_USB_CABLE); fwupd_request_add_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); return fu_device_emit_request(device, request, progress, error); } return TRUE; } static void fu_dell_kestrel_package_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 45, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 55, "reload"); } static void fu_dell_kestrel_package_init(FuDellKestrelPackage *self) { fu_device_add_protocol(FU_DEVICE(self), "com.dell.kestrel"); fu_device_add_vendor_id(FU_DEVICE(self), "USB:0x413C"); fu_device_set_name(FU_DEVICE(self), "Package Version of Dell dock"); fu_device_set_summary(FU_DEVICE(self), "Dell Dock Package"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_request_flag(FU_DEVICE(self), FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_EXPLICIT_ORDER); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PROXY_FOR_OPEN); } static void fu_dell_kestrel_package_class_init(FuDellKestrelPackageClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_dell_kestrel_package_write; device_class->setup = fu_dell_kestrel_package_setup; device_class->set_progress = fu_dell_kestrel_package_set_progress; device_class->convert_version = fu_dell_kestrel_package_convert_version; device_class->attach = fu_dell_kestrel_package_attach; } FuDellKestrelPackage * fu_dell_kestrel_package_new(FuDevice *proxy) { FuContext *ctx = fu_device_get_context(proxy); FuDellKestrelPackage *self = NULL; self = g_object_new(FU_TYPE_DELL_KESTREL_PACKAGE, "context", ctx, NULL); fu_device_set_proxy(FU_DEVICE(self), proxy); fu_device_set_logical_id(FU_DEVICE(self), "package"); return self; } fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-package.h000066400000000000000000000006401501337203100235230ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_DELL_KESTREL_PACKAGE (fu_dell_kestrel_package_get_type()) G_DECLARE_FINAL_TYPE(FuDellKestrelPackage, fu_dell_kestrel_package, FU, DELL_KESTREL_PACKAGE, FuDevice) FuDellKestrelPackage * fu_dell_kestrel_package_new(FuDevice *proxy); fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-pd.c000066400000000000000000000106631501337203100225340ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-dell-kestrel-common.h" #include "fu-dell-kestrel-ec-struct.h" struct _FuDellKestrelPd { FuDevice parent_instance; FuDellKestrelEcDevSubtype pd_subtype; FuDellKestrelEcDevInstance pd_instance; guint8 pd_identifier; }; G_DEFINE_TYPE(FuDellKestrelPd, fu_dell_kestrel_pd, FU_TYPE_DEVICE) static gchar * fu_dell_kestrel_pd_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32_hex(version_raw, fu_device_get_version_format(device)); } static gboolean fu_dell_kestrel_pd_setup(FuDevice *device, GError **error) { FuDellKestrelPd *self = FU_DELL_KESTREL_PD(device); FuDevice *proxy = fu_device_get_proxy(device); FuDellDockBaseType dock_type = fu_dell_kestrel_ec_get_dock_type(FU_DELL_KESTREL_EC(proxy)); FuDellKestrelDockSku dock_sku = fu_dell_kestrel_ec_get_dock_sku(FU_DELL_KESTREL_EC(proxy)); FuDellKestrelEcDevType dev_type = FU_DELL_KESTREL_EC_DEV_TYPE_PD; guint32 raw_version; g_autofree gchar *devname = NULL; /* name */ devname = g_strdup( fu_dell_kestrel_ec_devicetype_to_str(dev_type, self->pd_subtype, self->pd_instance)); fu_device_set_name(device, devname); fu_device_set_logical_id(device, devname); /* instance ID */ fu_device_add_instance_u8(device, "DOCKTYPE", dock_type); fu_device_add_instance_u8(device, "DOCKSKU", dock_sku); fu_device_add_instance_u8(device, "DEVTYPE", dev_type); fu_device_add_instance_u8(device, "INST", self->pd_instance); fu_device_build_instance_id(device, error, "EC", "DOCKTYPE", "DOCKSKU", "DEVTYPE", "INST", NULL); /* version */ raw_version = fu_dell_kestrel_ec_get_pd_version(FU_DELL_KESTREL_EC(proxy), self->pd_subtype, self->pd_instance); fu_device_set_version_raw(device, raw_version); return TRUE; } static gboolean fu_dell_kestrel_pd_write(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDellKestrelPd *self = FU_DELL_KESTREL_PD(device); FuDevice *proxy = fu_device_get_proxy(device); return fu_dell_kestrel_hid_device_write_firmware(FU_DELL_KESTREL_HID_DEVICE(proxy), firmware, progress, FU_DELL_KESTREL_EC_DEV_TYPE_PD, self->pd_identifier, error); } static void fu_dell_kestrel_pd_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_dell_kestrel_pd_init(FuDellKestrelPd *self) { fu_device_add_protocol(FU_DEVICE(self), "com.dell.kestrel"); fu_device_add_vendor_id(FU_DEVICE(self), "USB:0x413C"); fu_device_set_summary(FU_DEVICE(self), "Dell Dock PowerDelivery"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INSTALL_SKIP_VERSION_CHECK); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_SKIPS_RESTART); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_EXPLICIT_ORDER); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PROXY_FOR_OPEN); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); } static void fu_dell_kestrel_pd_class_init(FuDellKestrelPdClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_dell_kestrel_pd_write; device_class->setup = fu_dell_kestrel_pd_setup; device_class->set_progress = fu_dell_kestrel_pd_set_progress; device_class->convert_version = fu_dell_kestrel_pd_convert_version; } FuDellKestrelPd * fu_dell_kestrel_pd_new(FuDevice *proxy, FuDellKestrelEcDevSubtype subtype, FuDellKestrelEcDevInstance instance) { FuContext *ctx = fu_device_get_context(proxy); FuDellKestrelPd *self = NULL; self = g_object_new(FU_TYPE_DELL_KESTREL_PD, "context", ctx, NULL); self->pd_subtype = subtype; self->pd_instance = instance; self->pd_identifier = instance + 1; fu_device_set_proxy(FU_DEVICE(self), proxy); return self; } fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-pd.h000066400000000000000000000007021501337203100225320ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_DELL_KESTREL_PD (fu_dell_kestrel_pd_get_type()) G_DECLARE_FINAL_TYPE(FuDellKestrelPd, fu_dell_kestrel_pd, FU, DELL_KESTREL_PD, FuDevice) FuDellKestrelPd * fu_dell_kestrel_pd_new(FuDevice *parent, FuDellKestrelEcDevSubtype pd_subtype, FuDellKestrelEcDevInstance pd_instance); fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-plugin.c000066400000000000000000000341271501337203100234300ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-dell-kestrel-common.h" #include "fu-dell-kestrel-plugin.h" /* register the firmware types */ #include "fu-dell-kestrel-rtshub-firmware.h" /* plugin config */ #define FWUPD_DELL_KESTREL_PLUGIN_CONFIG_UOD "UpdateOnDisconnect" struct _FuDellKestrelPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuDellKestrelPlugin, fu_dell_kestrel_plugin, FU_TYPE_PLUGIN) static gboolean fu_dell_kestrel_plugin_create_node(FuPlugin *plugin, FuDevice *device, GError **error) { g_autoptr(FuDeviceLocker) locker = NULL; locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; fu_plugin_device_add(plugin, device); return TRUE; } static gboolean fu_dell_kestrel_plugin_device_add(FuPlugin *plugin, FuDevice *device, GError **error) { FuDellDockBaseType dock_type; FuDevice *ec_device = fu_plugin_cache_lookup(plugin, "ec"); guint16 vid = fu_device_get_vid(device); guint16 pid = fu_device_get_pid(device); /* cache this device until dock type is seen */ if (ec_device == NULL) { g_autofree gchar *key = g_strdup_printf("USB\\VID_%04X&PID_%04X", vid, pid); fu_plugin_cache_add(plugin, key, device); return TRUE; } /* dock type according to ec */ dock_type = fu_dell_kestrel_ec_get_dock_type(FU_DELL_KESTREL_EC(ec_device)); if (dock_type == FU_DELL_DOCK_BASE_TYPE_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "can't read base dock type from EC"); return FALSE; } /* dell devices */ if (vid != DELL_VID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device vid not dell, got: 0x%04x", vid); return FALSE; } /* Remote Management */ if (pid == DELL_KESTREL_USB_RMM_PID) { g_autoptr(FuDellKestrelRmm) rmm_device = NULL; g_autoptr(FuDeviceLocker) locker = NULL; rmm_device = fu_dell_kestrel_rmm_new(FU_USB_DEVICE(device)); if (rmm_device == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to create rmm device"); return FALSE; } locker = fu_device_locker_new(FU_DEVICE(rmm_device), error); if (locker == NULL) return FALSE; fu_device_add_child(ec_device, FU_DEVICE(rmm_device)); fu_dell_kestrel_rmm_fix_version(rmm_device); return TRUE; } /* RTS usb hub devices */ if (pid == DELL_KESTREL_USB_RTS0_G1_PID || pid == DELL_KESTREL_USB_RTS0_G2_PID || pid == DELL_KESTREL_USB_RTS5_G2_PID) { g_autoptr(FuDellKestrelRtsHub) hub_device = NULL; g_autoptr(FuDeviceLocker) locker = NULL; hub_device = fu_dell_kestrel_rtshub_new(FU_USB_DEVICE(device), dock_type); if (hub_device == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to create rtshub device, pid: 0x%04x", pid); return FALSE; } locker = fu_device_locker_new(FU_DEVICE(hub_device), error); if (locker == NULL) return FALSE; fu_device_add_child(ec_device, FU_DEVICE(hub_device)); return TRUE; } /* devices added from quirk only the RTSHUB */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "ignoring unsupported device, vid: 0x%04x, pid: 0x%04x", vid, pid); return FALSE; } static gboolean fu_dell_kestrel_plugin_ec_add_cached_devices(FuPlugin *plugin, FuDevice *ec_device, GError **error) { struct { guint16 vid; guint16 pid; } hw_dev_ids[] = { {DELL_VID, DELL_KESTREL_USB_RTS0_G1_PID}, {DELL_VID, DELL_KESTREL_USB_RTS0_G2_PID}, {DELL_VID, DELL_KESTREL_USB_RTS5_G2_PID}, {DELL_VID, DELL_KESTREL_USB_RMM_PID}, {0}, }; for (guint i = 0; hw_dev_ids[i].pid != 0; i++) { FuDevice *device; g_autofree gchar *key = NULL; key = g_strdup_printf("USB\\VID_%04X&PID_%04X", hw_dev_ids[i].vid, hw_dev_ids[i].pid); device = fu_plugin_cache_lookup(plugin, key); if (device != NULL) { if (!(fu_dell_kestrel_plugin_device_add(plugin, device, error))) return FALSE; fu_plugin_cache_remove(plugin, key); } } return TRUE; } static gboolean fu_dell_kestrel_plugin_backend_device_added(FuPlugin *plugin, FuDevice *device, FuProgress *progress, GError **error) { guint16 vid, pid; /* not interesting */ if (!FU_IS_USB_DEVICE(device)) return TRUE; /* VID and PID */ vid = fu_device_get_vid(device); pid = fu_device_get_pid(device); /* USB HUB HID bridge device */ if ((vid == DELL_VID && pid == DELL_KESTREL_HID_PID)) { gboolean uod; g_autoptr(FuDellKestrelEc) ec_dev = NULL; g_autoptr(GError) error_local = NULL; uod = fu_plugin_get_config_value_boolean(plugin, FWUPD_DELL_KESTREL_PLUGIN_CONFIG_UOD); ec_dev = fu_dell_kestrel_ec_new(device, uod); if (ec_dev == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "can't create EC V2 device"); return FALSE; } if (fu_dell_kestrel_plugin_create_node(plugin, FU_DEVICE(ec_dev), &error_local)) { /* flush the cached devices to plugin */ if (!fu_dell_kestrel_plugin_ec_add_cached_devices(plugin, FU_DEVICE(ec_dev), error)) return FALSE; } else { /* api version 2 doesn't support legacy docks */ if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_debug("ignoring: %s", error_local->message); } else { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } return TRUE; } if (!fu_dell_kestrel_plugin_device_add(plugin, device, error)) return FALSE; return TRUE; } static void fu_dell_kestrel_plugin_config_mst_dev(FuPlugin *plugin) { FuDevice *device_ec = fu_plugin_cache_lookup(plugin, "ec"); FuDevice *device_mst = fu_plugin_cache_lookup(plugin, "mst"); FuDellKestrelEcDevType mst_devtype = FU_DELL_KESTREL_EC_DEV_TYPE_MST; FuDellKestrelEcDevSubtype mst_subtype; const gchar *devname = NULL; if (device_ec == NULL || device_mst == NULL) return; /* run only once */ if (fu_device_has_private_flag(device_mst, FU_DEVICE_PRIVATE_FLAG_EXPLICIT_ORDER)) return; /* vmm8 */ mst_subtype = FU_DELL_KESTREL_EC_DEV_SUBTYPE_VMM8; if (fu_dell_kestrel_ec_is_dev_present(FU_DELL_KESTREL_EC(device_ec), mst_devtype, mst_subtype, 0)) devname = fu_dell_kestrel_ec_devicetype_to_str(mst_devtype, mst_subtype, 0); /* vmm9 */ mst_subtype = FU_DELL_KESTREL_EC_DEV_SUBTYPE_VMM9; if (fu_dell_kestrel_ec_is_dev_present(FU_DELL_KESTREL_EC(device_ec), mst_devtype, mst_subtype, 0)) devname = fu_dell_kestrel_ec_devicetype_to_str(mst_devtype, mst_subtype, 0); /* device name */ if (devname == NULL) { g_warning("no mst device found in ec, device name is undetermined"); return; } fu_device_set_name(device_mst, devname); /* flags */ fu_device_add_private_flag(device_mst, FU_DEVICE_PRIVATE_FLAG_EXPLICIT_ORDER); fu_device_add_private_flag(device_mst, FU_DEVICE_PRIVATE_FLAG_SKIPS_RESTART); return; } static void fu_dell_kestrel_plugin_config_parentship(FuPlugin *plugin) { FuDevice *device_ec = fu_plugin_cache_lookup(plugin, "ec"); FuDevice *device_usb4 = fu_plugin_cache_lookup(plugin, "usb4"); FuDevice *device_mst = fu_plugin_cache_lookup(plugin, "mst"); if (device_ec && device_usb4 && !fu_device_get_parent(device_usb4)) { fu_device_add_child(device_ec, device_usb4); fu_plugin_cache_remove(plugin, "usb4"); } if (device_ec && device_mst && !fu_device_get_parent(device_mst)) { fu_device_add_child(device_ec, device_mst); fu_plugin_cache_remove(plugin, "mst"); } } static void fu_dell_kestrel_plugin_device_registered(FuPlugin *plugin, FuDevice *device) { /* leverage intel_usb4 for usb4 devices */ if (fu_device_has_guid(device, DELL_KESTREL_T4_DEVID) || fu_device_has_guid(device, DELL_KESTREL_T5_DEVID)) { /* default go through usb protocol instead thunderbolt */ if (g_strcmp0(fu_device_get_plugin(device), "thunderbolt") == 0) { g_autofree gchar *msg = g_strdup_printf("firmware update inhibited by [%s] plugin", fu_plugin_get_name(plugin)); fu_device_inhibit(device, "hidden", msg); return; } /* activation should already done when device is added */ fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_EXPLICIT_ORDER); fu_plugin_cache_add(plugin, "usb4", device); } /* usb device of interset */ if (!FU_IS_USB_DEVICE(device)) return; /* leverage synaptics_vmm9 plugin for the mst device */ if (fu_device_get_vid(device) == MST_VMM89_USB_VID && fu_device_get_pid(device) == MST_VMM89_USB_PID) fu_plugin_cache_add(plugin, "mst", device); /* add ec to cache */ if (FU_IS_DELL_KESTREL_EC(device)) fu_plugin_cache_add(plugin, "ec", device); /* config mst device */ fu_dell_kestrel_plugin_config_mst_dev(plugin); /* setup parent device */ fu_dell_kestrel_plugin_config_parentship(plugin); } static FuDevice * fu_dell_kestrel_plugin_get_ec_from_devices(GPtrArray *devices) { for (guint i = 0; i < devices->len; i++) { FuDevice *dev = g_ptr_array_index(devices, i); FuDevice *parent = fu_device_get_parent(dev); if (parent == NULL) parent = dev; if (FU_IS_DELL_KESTREL_EC(parent)) return parent; } return NULL; } static gboolean fu_dell_kestrel_plugin_composite_cleanup(FuPlugin *plugin, GPtrArray *devices, GError **error) { FuDevice *ec_dev = NULL; g_autoptr(FuDeviceLocker) locker = NULL; /* locate the ec device */ ec_dev = fu_dell_kestrel_plugin_get_ec_from_devices(devices); if (ec_dev == NULL) return TRUE; /* open ec device */ locker = fu_device_locker_new(ec_dev, error); if (locker == NULL) return FALSE; /* release the dock */ if (!fu_dell_kestrel_ec_own_dock(FU_DELL_KESTREL_EC(ec_dev), FALSE, error)) return FALSE; return TRUE; } static gboolean fu_dell_kestrel_plugin_composite_prepare(FuPlugin *plugin, GPtrArray *devices, GError **error) { FuDevice *ec_dev = NULL; g_autoptr(FuDeviceLocker) locker = NULL; /* locate the ec device */ ec_dev = fu_dell_kestrel_plugin_get_ec_from_devices(devices); if (ec_dev == NULL) return TRUE; /* open ec device */ locker = fu_device_locker_new(ec_dev, error); if (locker == NULL) return FALSE; /* check if dock is ready to process updates */ if (!fu_dell_kestrel_ec_is_dock_ready4update(ec_dev, error)) return FALSE; /* own the dock */ if (!fu_dell_kestrel_ec_own_dock(FU_DELL_KESTREL_EC(ec_dev), TRUE, error)) return FALSE; /* conditionally enable passive flow */ if (fu_plugin_get_config_value_boolean(plugin, FWUPD_DELL_KESTREL_PLUGIN_CONFIG_UOD)) { if (fu_device_has_flag(ec_dev, FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE)) { if (!fu_dell_kestrel_ec_run_passive_update(FU_DELL_KESTREL_EC(ec_dev), error)) return FALSE; } } return TRUE; } static gboolean fu_dell_kestrel_plugin_modify_config(FuPlugin *plugin, const gchar *key, const gchar *value, GError **error) { const gchar *keys[] = {FWUPD_DELL_KESTREL_PLUGIN_CONFIG_UOD, NULL}; if (!g_strv_contains(keys, key)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "config key %s not supported", key); return FALSE; } return fu_plugin_set_config_value(plugin, key, value, error); } static gboolean fu_dell_kestrel_plugin_backend_device_removed(FuPlugin *plugin, FuDevice *device, GError **error) { const gchar *cache_keys[] = {"ec", "mst", "usb4"}; FuDevice *parent = fu_device_get_parent(device); if (parent == NULL) return TRUE; if (!FU_IS_DELL_KESTREL_EC(parent)) return TRUE; if (FU_IS_USB_DEVICE(device)) { g_autofree gchar *key = g_strdup_printf("USB\\VID_%04X&PID_%04X", fu_device_get_vid(device), fu_device_get_pid(device)); fu_plugin_cache_remove(plugin, key); } for (gsize i = 0; i < G_N_ELEMENTS(cache_keys); i++) fu_plugin_cache_remove(plugin, cache_keys[i]); return TRUE; } static gboolean fu_dell_kestrel_plugin_prepare(FuPlugin *plugin, FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { /* usb4 device reboot is suppressed, let ec handle it in passive update */ if (fu_device_has_guid(device, DELL_KESTREL_T4_DEVID) || fu_device_has_guid(device, DELL_KESTREL_T5_DEVID)) { /* uod requires needs-activate from intel-usb4 plugin */ if (fu_plugin_get_config_value_boolean(plugin, FWUPD_DELL_KESTREL_PLUGIN_CONFIG_UOD)) fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_SKIPS_RESTART); } return TRUE; } static void fu_dell_kestrel_plugin_init(FuDellKestrelPlugin *self) { } static void fu_dell_kestrel_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); /* allow these to be built by quirks */ fu_plugin_add_device_gtype(plugin, FU_TYPE_DELL_KESTREL_PACKAGE); fu_plugin_add_device_gtype(plugin, FU_TYPE_DELL_KESTREL_PD); fu_plugin_add_device_gtype(plugin, FU_TYPE_DELL_KESTREL_DPMUX); fu_plugin_add_device_gtype(plugin, FU_TYPE_DELL_KESTREL_WTPD); fu_plugin_add_device_gtype(plugin, FU_TYPE_DELL_KESTREL_ILAN); fu_plugin_add_device_gtype(plugin, FU_TYPE_DELL_KESTREL_RMM); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_DELL_KESTREL_EC); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_DELL_KESTREL_RTSHUB); /* coverage */ /* register firmware parser */ fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_DELL_KESTREL_RTSHUB_FIRMWARE); /* defaults changed here will also be reflected in the fwupd.conf man page */ fu_plugin_set_config_default(plugin, FWUPD_DELL_KESTREL_PLUGIN_CONFIG_UOD, "true"); } static void fu_dell_kestrel_plugin_class_init(FuDellKestrelPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_dell_kestrel_plugin_constructed; plugin_class->device_registered = fu_dell_kestrel_plugin_device_registered; plugin_class->backend_device_added = fu_dell_kestrel_plugin_backend_device_added; plugin_class->backend_device_removed = fu_dell_kestrel_plugin_backend_device_removed; plugin_class->composite_prepare = fu_dell_kestrel_plugin_composite_prepare; plugin_class->composite_cleanup = fu_dell_kestrel_plugin_composite_cleanup; plugin_class->modify_config = fu_dell_kestrel_plugin_modify_config; plugin_class->prepare = fu_dell_kestrel_plugin_prepare; } fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-plugin.h000066400000000000000000000003611501337203100234260ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include G_DECLARE_FINAL_TYPE(FuDellKestrelPlugin, fu_dell_kestrel_plugin, FU, DELL_KESTREL_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-rmm.c000066400000000000000000000062371501337203100227260ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-dell-kestrel-common.h" struct _FuDellKestrelRmm { FuDellKestrelHidDevice parent_instance; }; G_DEFINE_TYPE(FuDellKestrelRmm, fu_dell_kestrel_rmm, FU_TYPE_DELL_KESTREL_HID_DEVICE) static gchar * fu_dell_kestrel_rmm_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32_hex(version_raw, fu_device_get_version_format(device)); } void fu_dell_kestrel_rmm_fix_version(FuDellKestrelRmm *self) { FuDevice *parent = NULL; /* use version given by parent */ parent = fu_device_get_parent(FU_DEVICE(self)); if (parent != NULL) { guint32 rmm_version; rmm_version = fu_dell_kestrel_ec_get_rmm_version(FU_DELL_KESTREL_EC(parent)); fu_device_set_version_raw(FU_DEVICE(self), rmm_version); } } static gboolean fu_dell_kestrel_rmm_setup(FuDevice *device, GError **error) { FuDellKestrelRmm *self = FU_DELL_KESTREL_RMM(device); /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_dell_kestrel_rmm_parent_class)->setup(device, error)) return FALSE; fu_dell_kestrel_rmm_fix_version(self); return TRUE; } static gboolean fu_dell_kestrel_rmm_write(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { return fu_dell_kestrel_hid_device_write_firmware(FU_DELL_KESTREL_HID_DEVICE(device), firmware, progress, FU_DELL_KESTREL_EC_DEV_TYPE_RMM, 0, error); } static void fu_dell_kestrel_rmm_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 13, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 72, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 9, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 7, "reload"); } static void fu_dell_kestrel_rmm_init(FuDellKestrelRmm *self) { fu_device_add_protocol(FU_DEVICE(self), "com.dell.kestrel"); fu_device_add_vendor_id(FU_DEVICE(self), "USB:0x413C"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INSTALL_SKIP_VERSION_CHECK); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_EXPLICIT_ORDER); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); } static void fu_dell_kestrel_rmm_class_init(FuDellKestrelRmmClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_dell_kestrel_rmm_write; device_class->setup = fu_dell_kestrel_rmm_setup; device_class->set_progress = fu_dell_kestrel_rmm_set_progress; device_class->convert_version = fu_dell_kestrel_rmm_convert_version; } FuDellKestrelRmm * fu_dell_kestrel_rmm_new(FuUsbDevice *device) { FuDellKestrelRmm *self = g_object_new(FU_TYPE_DELL_KESTREL_RMM, NULL); fu_device_incorporate(FU_DEVICE(self), FU_DEVICE(device), FU_DEVICE_INCORPORATE_FLAG_ALL); return self; } fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-rmm.h000066400000000000000000000010461501337203100227240ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include "fu-dell-kestrel-hid-device.h" /* Device IDs: USB RMM */ #define DELL_KESTREL_USB_RMM_PID 0xB0A4 #define FU_TYPE_DELL_KESTREL_RMM (fu_dell_kestrel_rmm_get_type()) G_DECLARE_FINAL_TYPE(FuDellKestrelRmm, fu_dell_kestrel_rmm, FU, DELL_KESTREL_RMM, FuDellKestrelHidDevice) FuDellKestrelRmm * fu_dell_kestrel_rmm_new(FuUsbDevice *device); void fu_dell_kestrel_rmm_fix_version(FuDellKestrelRmm *self); fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-rtshub-firmware.c000066400000000000000000000057611501337203100252550ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-dell-kestrel-common.h" #include "fu-dell-kestrel-rtshub-firmware.h" #define DOCK_RTSHUB_GEN2_VERSION_OFFSET 0x7F52 #define DOCK_RTSHUB_GEN1_VERSION_OFFSET 0x7FA6 #define DOCK_RTSHUB_GEN1_VID_OFFSET 0x7FA8 #define DOCK_RTSHUB_GEN1_PID_OFFSET 0x7FAA struct _FuDellKestrelRtshubFirmware { FuFirmwareClass parent_instance; guint16 pid; }; G_DEFINE_TYPE(FuDellKestrelRtshubFirmware, fu_dell_kestrel_rtshub_firmware, FU_TYPE_FIRMWARE) static gchar * fu_dell_kestrel_rtshub_firmware_convert_version(FuFirmware *firmware, guint64 version_raw) { return fu_version_from_uint32_hex(version_raw, fu_firmware_get_version_format(firmware)); } static void fu_dell_kestrel_rtshub_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuDellKestrelRtshubFirmware *self = FU_DELL_KESTREL_RTSHUB_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "product_id", self->pid); } static gboolean fu_dell_kestrel_rtshub_firmware_set_offset(GInputStream *stream, guint16 *version_offset, guint16 *pid_offset, GError **error) { guint16 vid_raw = 0; if (!fu_input_stream_read_u16(stream, DOCK_RTSHUB_GEN1_VID_OFFSET, &vid_raw, G_BIG_ENDIAN, error)) return FALSE; if (vid_raw == DELL_VID) { *version_offset = (guint16)DOCK_RTSHUB_GEN1_VERSION_OFFSET; *pid_offset = (guint16)DOCK_RTSHUB_GEN1_PID_OFFSET; return TRUE; } *version_offset = (guint16)DOCK_RTSHUB_GEN2_VERSION_OFFSET; return TRUE; } static gboolean fu_dell_kestrel_rtshub_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuDellKestrelRtshubFirmware *self = FU_DELL_KESTREL_RTSHUB_FIRMWARE(firmware); guint16 version_raw = 0; guint16 version_offset = 0; guint16 pid_raw = 0; guint16 pid_offset = 0; /* match vid first */ if (!fu_dell_kestrel_rtshub_firmware_set_offset(stream, &version_offset, &pid_offset, error)) return FALSE; /* version */ if (!fu_input_stream_read_u16(stream, version_offset, &version_raw, G_BIG_ENDIAN, error)) return FALSE; fu_firmware_set_version_raw(firmware, version_raw); /* pid */ if (pid_offset != 0) { if (!fu_input_stream_read_u16(stream, pid_offset, &pid_raw, G_BIG_ENDIAN, error)) return FALSE; self->pid = pid_raw; } return TRUE; } static void fu_dell_kestrel_rtshub_firmware_init(FuDellKestrelRtshubFirmware *self) { fu_firmware_set_version_format(FU_FIRMWARE(self), FWUPD_VERSION_FORMAT_PAIR); } static void fu_dell_kestrel_rtshub_firmware_class_init(FuDellKestrelRtshubFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_dell_kestrel_rtshub_firmware_parse; firmware_class->export = fu_dell_kestrel_rtshub_firmware_export; firmware_class->convert_version = fu_dell_kestrel_rtshub_firmware_convert_version; } fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-rtshub-firmware.h000066400000000000000000000006031501337203100252500ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_DELL_KESTREL_RTSHUB_FIRMWARE (fu_dell_kestrel_rtshub_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuDellKestrelRtshubFirmware, fu_dell_kestrel_rtshub_firmware, FU, DELL_KESTREL_RTSHUB_FIRMWARE, FuFirmware) fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-rtshub.c000066400000000000000000000304741501337203100234420ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-dell-kestrel-common.h" #include "fu-dell-kestrel-rtshub-firmware.h" #include "fu-dell-kestrel-rtshub-struct.h" #include "fu-dell-kestrel-rtshub.h" struct _FuDellKestrelRtsHub { FuHidDevice parent_instance; FuDellDockBaseType dock_type; gboolean fw_auth; gboolean dual_bank; }; G_DEFINE_TYPE(FuDellKestrelRtsHub, fu_dell_kestrel_rtshub, FU_TYPE_HID_DEVICE) static void fu_dell_kestrel_rtshub_to_string(FuDevice *device, guint idt, GString *str) { FuDellKestrelRtsHub *self = FU_DELL_KESTREL_RTSHUB(device); fwupd_codec_string_append_bool(str, idt, "FwAuth", self->fw_auth); fwupd_codec_string_append_bool(str, idt, "DualBank", self->dual_bank); fwupd_codec_string_append_hex(str, idt, "DockType", self->dock_type); } static gboolean fu_dell_kestrel_rtshub_set_clock_mode(FuDellKestrelRtsHub *self, gboolean enable, GError **error) { g_autoptr(GByteArray) cmd_buf = fu_struct_rtshub_hid_cmd_buf_new(); fu_struct_rtshub_hid_cmd_buf_set_cmd(cmd_buf, RTSHUB_CMD_WRITE_DATA); fu_struct_rtshub_hid_cmd_buf_set_ext(cmd_buf, RTSHUB_EXT_MCUMODIFYCLOCK); fu_struct_rtshub_hid_cmd_buf_set_regaddr(cmd_buf, (guint8)enable); fu_struct_rtshub_hid_cmd_buf_set_bufferlen(cmd_buf, 0); if (!fu_hid_device_set_report(FU_HID_DEVICE(self), 0x0, cmd_buf->data, cmd_buf->len, DELL_KESTREL_RTSHUB_TIMEOUT, FU_HID_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to set clock-mode=%i: ", enable); return FALSE; } return TRUE; } static gboolean fu_dell_kestrel_rtshub_erase_spare_bank(FuDellKestrelRtsHub *self, GError **error) { g_autoptr(GByteArray) cmd_buf = fu_struct_rtshub_hid_cmd_buf_new(); fu_struct_rtshub_hid_cmd_buf_set_cmd(cmd_buf, RTSHUB_CMD_WRITE_DATA); fu_struct_rtshub_hid_cmd_buf_set_ext(cmd_buf, RTSHUB_EXT_ERASEBANK); fu_struct_rtshub_hid_cmd_buf_set_regaddr(cmd_buf, 0x0100); fu_struct_rtshub_hid_cmd_buf_set_bufferlen(cmd_buf, 0); if (!fu_hid_device_set_report(FU_HID_DEVICE(self), 0x0, cmd_buf->data, cmd_buf->len, DELL_KESTREL_RTSHUB_TIMEOUT * 3, FU_HID_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to erase spare bank: "); return FALSE; } return TRUE; } static gboolean fu_dell_kestrel_rtshub_verify_update_fw(FuDellKestrelRtsHub *self, FuProgress *progress, GError **error) { g_autoptr(GByteArray) cmd_buf = fu_struct_rtshub_hid_cmd_buf_new(); fu_struct_rtshub_hid_cmd_buf_set_cmd(cmd_buf, RTSHUB_CMD_WRITE_DATA); fu_struct_rtshub_hid_cmd_buf_set_ext(cmd_buf, RTSHUB_EXT_VERIFYUPDATE); fu_struct_rtshub_hid_cmd_buf_set_regaddr(cmd_buf, 0x01); fu_struct_rtshub_hid_cmd_buf_set_bufferlen(cmd_buf, 0); if (!fu_hid_device_set_report(FU_HID_DEVICE(self), 0x0, cmd_buf->data, cmd_buf->len, DELL_KESTREL_RTSHUB_TIMEOUT, FU_HID_DEVICE_FLAG_NONE, error)) return FALSE; fu_device_sleep_full(FU_DEVICE(self), 4000, progress); /* ms */ if (!fu_hid_device_get_report(FU_HID_DEVICE(self), 0x0, cmd_buf->data, cmd_buf->len, DELL_KESTREL_RTSHUB_TIMEOUT, FU_HID_DEVICE_FLAG_NONE, error)) return FALSE; /* check device status, 1 for success otherwise fail */ if (cmd_buf->data[0] != 0x01) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "firmware flash failed"); return FALSE; } /* success */ return TRUE; } static gboolean fu_dell_kestrel_rtshub_write_flash(FuDellKestrelRtsHub *self, guint32 addr, const guint8 *data, guint16 data_sz, GError **error) { g_autoptr(GByteArray) cmd_buf = fu_struct_rtshub_hid_cmd_buf_new(); g_return_val_if_fail(data_sz <= 128, FALSE); g_return_val_if_fail(data != NULL, FALSE); g_return_val_if_fail(data_sz != 0, FALSE); fu_struct_rtshub_hid_cmd_buf_set_cmd(cmd_buf, RTSHUB_CMD_WRITE_DATA); fu_struct_rtshub_hid_cmd_buf_set_ext(cmd_buf, RTSHUB_EXT_WRITEFLASH); fu_struct_rtshub_hid_cmd_buf_set_regaddr(cmd_buf, addr); fu_struct_rtshub_hid_cmd_buf_set_bufferlen(cmd_buf, data_sz); if (!fu_struct_rtshub_hid_cmd_buf_set_data(cmd_buf, data, data_sz, error)) return FALSE; if (!fu_hid_device_set_report(FU_HID_DEVICE(self), 0x0, cmd_buf->data, cmd_buf->len, DELL_KESTREL_RTSHUB_TIMEOUT, FU_HID_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write flash @%08x: ", (guint)addr); return FALSE; } return TRUE; } static gboolean fu_dell_kestrel_rtshub_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDellKestrelRtsHub *self = FU_DELL_KESTREL_RTSHUB(device); g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 2, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 28, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 70, NULL); /* set MCU to high clock rate for better ISP performance */ if (!fu_dell_kestrel_rtshub_set_clock_mode(self, TRUE, error)) return FALSE; /* get default image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; g_debug("writing firmware: %s, %s -> %s", fu_device_get_name(device), fu_device_get_version(device), fu_firmware_get_version(firmware)); chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, DELL_KESTREL_RTSHUB_TRANSFER_BLOCK_SIZE, error); if (chunks == NULL) return FALSE; /* erase spare flash bank only if it is not empty */ if (!fu_dell_kestrel_rtshub_erase_spare_bank(self, error)) return FALSE; fu_progress_step_done(progress); /* write each block */ for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; /* write chunk */ if (!fu_dell_kestrel_rtshub_write_flash(self, fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; /* update progress */ fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)i + 1, (gsize)fu_chunk_array_length(chunks)); } fu_progress_step_done(progress); /* get device to authenticate the firmware */ if (!fu_dell_kestrel_rtshub_verify_update_fw(self, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* success! */ return TRUE; } static gboolean fu_dell_kestrel_rtshub_get_status(FuDevice *device, GError **error) { FuDellKestrelRtsHub *self = FU_DELL_KESTREL_RTSHUB(device); g_autofree gchar *version = NULL; g_autoptr(GByteArray) cmd_buf = fu_struct_rtshub_hid_cmd_buf_new(); fu_struct_rtshub_hid_cmd_buf_set_cmd(cmd_buf, RTSHUB_CMD_READ_DATA); fu_struct_rtshub_hid_cmd_buf_set_ext(cmd_buf, RTSHUB_EXT_READ_STATUS); fu_struct_rtshub_hid_cmd_buf_set_regaddr(cmd_buf, 0x00); fu_struct_rtshub_hid_cmd_buf_set_bufferlen(cmd_buf, 12); if (!fu_hid_device_set_report(FU_HID_DEVICE(self), 0x0, cmd_buf->data, cmd_buf->len, DELL_KESTREL_RTSHUB_TIMEOUT, FU_HID_DEVICE_FLAG_RETRY_FAILURE, error)) return FALSE; if (!fu_hid_device_get_report(FU_HID_DEVICE(self), 0x0, cmd_buf->data, cmd_buf->len, DELL_KESTREL_RTSHUB_TIMEOUT, FU_HID_DEVICE_FLAG_RETRY_FAILURE, error)) return FALSE; /* version: index 10, subversion: index 11 */ version = g_strdup_printf("%x.%x", cmd_buf->data[10], cmd_buf->data[11]); fu_device_set_version(device, version); /* dual bank capability */ self->dual_bank = (cmd_buf->data[13] & 0xf0) == 0x80; /* authentication capability */ self->fw_auth = (cmd_buf->data[13] & 0x02) > 0; return TRUE; } static gboolean fu_dell_kestrel_rtshub_setup(FuDevice *device, GError **error) { FuDellKestrelRtsHub *self = FU_DELL_KESTREL_RTSHUB(device); /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_dell_kestrel_rtshub_parent_class)->setup(device, error)) return FALSE; if (!fu_dell_kestrel_rtshub_get_status(device, error)) return FALSE; if (self->dual_bank) fu_device_add_flag(device, FWUPD_DEVICE_FLAG_DUAL_IMAGE); if (!self->fw_auth) fu_device_set_update_error(device, "device does not support authentication"); return TRUE; } static gboolean fu_dell_kestrel_rtshub_probe(FuDevice *device, GError **error) { g_autofree const gchar *logical_id = NULL; FuDellKestrelRtsHub *self = FU_DELL_KESTREL_RTSHUB(device); /* not interesting */ if (fu_device_get_vid(device) != DELL_VID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device vid not dell, expected: 0x%04x, got: 0x%04x", (guint)DELL_VID, fu_device_get_vid(device)); return FALSE; } /* caring for my family back home after fw reset */ switch (fu_device_get_pid(device)) { case DELL_KESTREL_USB_RTS0_G1_PID: fu_device_set_name(device, "RTS0 Gen 1 USB Hub"); break; case DELL_KESTREL_USB_RTS0_G2_PID: fu_device_set_name(device, "RTS0 Gen 2 USB Hub"); break; case DELL_KESTREL_USB_RTS5_G2_PID: fu_device_set_name(device, "RTS5 Gen 2 USB Hub"); break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device pid '%04x' is not supported", fu_device_get_pid(device)); return FALSE; } /* build logical id */ logical_id = g_strdup_printf("RTSHUB_%04X", fu_device_get_pid(device)); fu_device_set_logical_id(device, logical_id); /* build instance id */ fu_device_add_instance_u8(device, "DOCKTYPE", self->dock_type); fu_device_build_instance_id(device, error, "USB", "VID", "PID", "DOCKTYPE", NULL); return TRUE; } static gboolean fu_dell_kestrel_rtshub_open(FuDevice *device, GError **error) { FuDevice *parent = fu_device_get_parent(device); if (!FU_DEVICE_CLASS(fu_dell_kestrel_rtshub_parent_class)->open(device, error)) return FALSE; if (parent != NULL) return fu_device_open(parent, error); return TRUE; } static void fu_dell_kestrel_rtshub_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_dell_kestrel_rtshub_init(FuDellKestrelRtsHub *self) { fu_device_add_protocol(FU_DEVICE(self), "com.dell.kestrel"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INSTALL_SKIP_VERSION_CHECK); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_SKIPS_RESTART); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_EXPLICIT_ORDER); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_RETRY_OPEN); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PAIR); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_DELL_KESTREL_RTSHUB_FIRMWARE); fu_device_retry_set_delay(FU_DEVICE(self), 1000); } static void fu_dell_kestrel_rtshub_class_init(FuDellKestrelRtsHubClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_dell_kestrel_rtshub_to_string; device_class->setup = fu_dell_kestrel_rtshub_setup; device_class->probe = fu_dell_kestrel_rtshub_probe; device_class->write_firmware = fu_dell_kestrel_rtshub_write_firmware; device_class->set_progress = fu_dell_kestrel_rtshub_set_progress; device_class->open = fu_dell_kestrel_rtshub_open; } FuDellKestrelRtsHub * fu_dell_kestrel_rtshub_new(FuUsbDevice *device, FuDellDockBaseType dock_type) { FuDellKestrelRtsHub *self = g_object_new(FU_TYPE_DELL_KESTREL_RTSHUB, NULL); fu_device_incorporate(FU_DEVICE(self), FU_DEVICE(device), FU_DEVICE_INCORPORATE_FLAG_ALL); self->dock_type = dock_type; return self; } fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-rtshub.h000066400000000000000000000021201501337203100234320ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_DELL_KESTREL_RTSHUB (fu_dell_kestrel_rtshub_get_type()) G_DECLARE_FINAL_TYPE(FuDellKestrelRtsHub, fu_dell_kestrel_rtshub, FU, DELL_KESTREL_RTSHUB, FuHidDevice) /* Device IDs: USB Hub */ #define DELL_KESTREL_USB_RTS0_G1_PID 0xB0A1 #define DELL_KESTREL_USB_RTS0_G2_PID 0xB0A2 #define DELL_KESTREL_USB_RTS5_G2_PID 0xB0A3 /* USB RTSHUB HID COMMAND */ #define RTSHUB_CMD_READ_DATA 0xC0 #define RTSHUB_CMD_WRITE_DATA 0x40 #define RTSHUB_EXT_READ_STATUS 0x09 #define RTSHUB_EXT_MCUMODIFYCLOCK 0x06 #define RTSHUB_EXT_WRITEFLASH 0xC8 #define RTSHUB_EXT_VERIFYUPDATE 0xD9 #define RTSHUB_EXT_ERASEBANK 0xE8 #define RTSHUB_EXT_RESET_TO_FLASH 0xE9 /* USB RTSHUB HID COMMON */ #define DELL_KESTREL_RTSHUB_TIMEOUT 2000 #define DELL_KESTREL_RTSHUB_BUFFER_SIZE 192 #define DELL_KESTREL_RTSHUB_TRANSFER_BLOCK_SIZE 128 FuDellKestrelRtsHub * fu_dell_kestrel_rtshub_new(FuUsbDevice *device, FuDellDockBaseType dock_type); fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-rtshub.rs000066400000000000000000000004541501337203100236370ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #[repr(C, packed)] #[derive(New, Setters, Getters)] struct FuStructRtshubHidCmdBuf { cmd: u8, ext: u8, regaddr: u32le, bufferlen: u16le, reserved: [u8; 56], data: [u8; 128], } fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-wtpd.c000066400000000000000000000074011501337203100231030ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-dell-kestrel-common.h" struct _FuDellKestrelWtpd { FuDevice parent_instance; }; G_DEFINE_TYPE(FuDellKestrelWtpd, fu_dell_kestrel_wtpd, FU_TYPE_DEVICE) static gchar * fu_dell_kestrel_wtpd_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32_hex(version_raw, fu_device_get_version_format(device)); } static gboolean fu_dell_kestrel_wtpd_setup(FuDevice *device, GError **error) { FuDevice *proxy = fu_device_get_proxy(device); FuDellDockBaseType dock_type = fu_dell_kestrel_ec_get_dock_type(FU_DELL_KESTREL_EC(proxy)); FuDellKestrelDockSku dock_sku = fu_dell_kestrel_ec_get_dock_sku(FU_DELL_KESTREL_EC(proxy)); FuDellKestrelEcDevType dev_type = FU_DELL_KESTREL_EC_DEV_TYPE_WTPD; guint32 wtpd_version; g_autofree gchar *devname = NULL; /* name */ devname = g_strdup_printf("%s", fu_dell_kestrel_ec_devicetype_to_str(dev_type, 0, 0)); fu_device_set_name(device, devname); fu_device_set_logical_id(device, devname); /* instance ID */ fu_device_add_instance_u8(device, "DOCKTYPE", dock_type); fu_device_add_instance_u8(device, "DOCKSKU", dock_sku); fu_device_add_instance_u8(device, "DEVTYPE", dev_type); fu_device_build_instance_id(device, error, "EC", "DOCKTYPE", "DOCKSKU", "DEVTYPE", NULL); /* version */ wtpd_version = fu_dell_kestrel_ec_get_wtpd_version(FU_DELL_KESTREL_EC(proxy)); fu_device_set_version_raw(device, wtpd_version); return TRUE; } static gboolean fu_dell_kestrel_wtpd_write(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDevice *proxy = fu_device_get_proxy(device); return fu_dell_kestrel_hid_device_write_firmware(FU_DELL_KESTREL_HID_DEVICE(proxy), firmware, progress, FU_DELL_KESTREL_EC_DEV_TYPE_WTPD, 0, error); } static void fu_dell_kestrel_wtpd_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 13, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 72, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 9, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 7, "reload"); } static void fu_dell_kestrel_wtpd_init(FuDellKestrelWtpd *self) { fu_device_add_protocol(FU_DEVICE(self), "com.dell.kestrel"); fu_device_add_vendor_id(FU_DEVICE(self), "USB:0x413C"); fu_device_set_summary(FU_DEVICE(self), "Dell Dock WT PD"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INSTALL_SKIP_VERSION_CHECK); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_EXPLICIT_ORDER); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PROXY_FOR_OPEN); } static void fu_dell_kestrel_wtpd_class_init(FuDellKestrelWtpdClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_dell_kestrel_wtpd_write; device_class->setup = fu_dell_kestrel_wtpd_setup; device_class->set_progress = fu_dell_kestrel_wtpd_set_progress; device_class->convert_version = fu_dell_kestrel_wtpd_convert_version; } FuDellKestrelWtpd * fu_dell_kestrel_wtpd_new(FuDevice *proxy) { FuContext *ctx = fu_device_get_context(proxy); FuDellKestrelWtpd *self = NULL; self = g_object_new(FU_TYPE_DELL_KESTREL_WTPD, "context", ctx, NULL); fu_device_set_proxy(FU_DEVICE(self), proxy); return self; } fwupd-2.0.10/plugins/dell-kestrel/fu-dell-kestrel-wtpd.h000066400000000000000000000005571501337203100231150ustar00rootroot00000000000000/* * Copyright 2024 Dell Technologies * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_DELL_KESTREL_WTPD (fu_dell_kestrel_wtpd_get_type()) G_DECLARE_FINAL_TYPE(FuDellKestrelWtpd, fu_dell_kestrel_wtpd, FU, DELL_KESTREL_WTPD, FuDevice) FuDellKestrelWtpd * fu_dell_kestrel_wtpd_new(FuDevice *proxy); fwupd-2.0.10/plugins/dell-kestrel/meson.build000066400000000000000000000016641501337203100211330ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginDellKestrel"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('dell-kestrel.quirk') plugin_builtins += static_library('fu_plugin_dell_kestrel', rustgen.process( 'fu-dell-kestrel-hid.rs', 'fu-dell-kestrel-ec.rs', 'fu-dell-kestrel-rtshub.rs', ), sources: [ 'fu-dell-kestrel-dpmux.c', 'fu-dell-kestrel-hid-device.c', 'fu-dell-kestrel-ec.c', 'fu-dell-kestrel-ilan.c', 'fu-dell-kestrel-package.c', 'fu-dell-kestrel-pd.c', 'fu-dell-kestrel-plugin.c', 'fu-dell-kestrel-rmm.c', 'fu-dell-kestrel-rtshub.c', 'fu-dell-kestrel-rtshub-firmware.c', 'fu-dell-kestrel-wtpd.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) enumeration_data += files( 'tests/dell-kestrel-enumerate-ec.json', ) device_tests += files( 'tests/dell-kestrel-enumerate.json', ) fwupd-2.0.10/plugins/dell-kestrel/tests/000077500000000000000000000000001501337203100201245ustar00rootroot00000000000000fwupd-2.0.10/plugins/dell-kestrel/tests/dell-kestrel-enumerate-ec.json000066400000000000000000000143071501337203100257630ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUsbDevice", "PlatformId": "3-1.3.4.3", "Created": "2024-11-13T10:50:18.821903Z", "IdVendor": 16700, "IdProduct": 45166, "Device": 775, "USB": 512, "Manufacturer": 1, "Product": 2, "SerialNumber": 3, "UsbConfigDescriptors": [ { "ConfigurationValue": 1 } ], "UsbHidDescriptors": [ "BQEJAKEBFQAm/wB1CJXACQCRAnUIlcAJAIECwA==" ], "UsbInterfaces": [ { "Length": 9, "DescriptorType": 4, "InterfaceClass": 3, "InterfaceSubClass": 1, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 129, "Interval": 9, "MaxPacketSize": 16 } ] } ], "UsbEvents": [ { "Id": "ReadProp:Key=BUSNUM", "Data": "003" }, { "Id": "ReadAttr:Attr=uevent", "Data": "MAJOR=189\nMINOR=319\nDEVNAME=bus/usb/003/064\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=413c/b06e/307\nTYPE=0/0/0\nBUSNUM=003\nDEVNUM=064" }, { "Id": "ReadProp:Key=DEVNUM", "Data": "064" }, { "Id": "ReadProp:Key=BUSNUM", "Data": "003" }, { "Id": "ReadAttr:Attr=uevent", "Data": "MAJOR=189\nMINOR=319\nDEVNAME=bus/usb/003/064\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=413c/b06e/307\nTYPE=0/0/0\nBUSNUM=003\nDEVNUM=064" }, { "Id": "ReadProp:Key=DEVNUM", "Data": "064" }, { "Id": "GetStringDescriptor:DescIndex=0x03", "Data": "RjQzNkYyOTBGMTU0OTRGQgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" }, { "Id": "ControlTransfer:Direction=0x01,RequestType=0x01,Recipient=0x01,Request=0x09,Value=0x0200,Idx=0x0000,Data=QNYFAAAAAgDsAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA,Length=0xc0", "Data": "QNYFAAAAAgDsAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" }, { "Id": "ControlTransfer:Direction=0x00,RequestType=0x01,Recipient=0x01,Request=0x01,Value=0x0100,Idx=0x0000,Data=/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA,Length=0xc0", "Data": "AQcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" }, { "Id": "ControlTransfer:Direction=0x01,RequestType=0x01,Recipient=0x01,Request=0x09,Value=0x0200,Idx=0x0000,Data=QNYDAAAAwADsAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA,Length=0xc0", "Data": "QNYDAAAAwADsAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" }, { "Id": "ControlTransfer:Direction=0x00,RequestType=0x01,Recipient=0x01,Request=0x01,Value=0x0100,Idx=0x0000,Data=/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA,Length=0xc0", "Data": "vgIHtAChAAMAeAYAAAAAEAAAAAAAAAAAAAAAAABFyI0ZNURITlA1NFNEMjVUQjUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdMAAAAAAAAAAAAAAAAAAAAAAAAAAAfIR4AAAAAAIIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" }, { "Id": "ControlTransfer:Direction=0x01,RequestType=0x01,Recipient=0x01,Request=0x09,Value=0x0200,Idx=0x0000,Data=QNYCAAAAuADsAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA,Length=0xc0", "Data": "QNYCAAAAuADsAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" }, { "Id": "ControlTransfer:Direction=0x00,RequestType=0x01,Recipient=0x01,Request=0x01,Value=0x0100,Idx=0x0000,Data=/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA,Length=0xc0", "Data": "vw0ADAAAAAEAAAAUAAABAAAAAAUAFQABAAABAAUCFQABAAACAAUBFQACAAAAAAABEgACAQAAAAABGQACAAABAAABEgADAQAAAAkDAgAEAgAAAABVVQAGAAAAgwAAAgAHAAAAAAACJQAJAAAAAAAAAAAKAAAAAAABAv//////////////////////////////////////////////////////////////////////////////////////////////" }, { "Id": "ControlTransfer:Direction=0x00,RequestType=0x00,Recipient=0x01,Request=0x06,Value=0x2200,Idx=0x0000,Data=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==,Length=0x1c", "Data": "BQEJAKEBFQAm/wB1CJXACQCRAnUIlcAJAIECwA==" } ] } ] } fwupd-2.0.10/plugins/dell-kestrel/tests/dell-kestrel-enumerate.json000066400000000000000000000024371501337203100253770ustar00rootroot00000000000000{ "name": "dell kestrel (enumerate only) ec", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/dell-kestrel-enumerate-ec.json", "components": [ { "version": "0.0.14.0", "guids": [ "68486488-92bb-5509-9a4c-801c93331416" ] }, { "version": "83.0.0.2", "guids": [ "e9d7e527-79bc-5752-9fe3-7347df1dea31" ] }, { "version": "2.25", "guids": [ "ded18075-2dc8-5de0-aea5-575300b1b661" ] }, { "version": "0.5.2.15", "guids": [ "64f56a0a-57ab-596d-ad19-b6ee0e8e8339" ] }, { "version": "0.5.1.15", "guids": [ "7058008e-18d9-55c6-9ec1-e326df92bd94" ] }, { "version": "0.5.0.15", "guids": [ "fe3f6be7-825b-5f56-a59e-d91f79eb02d0" ] }, { "version": "0.0.10.0", "guids": [ "f8511274-2f3c-567c-8fb1-2ff95da4ae14" ] }, { "version": "0.0.1.2", "guids": [ "5ed55572-7312-538c-b062-3bb82b21a4ef" ] } ] } ] } fwupd-2.0.10/plugins/dell/000077500000000000000000000000001501337203100153135ustar00rootroot00000000000000fwupd-2.0.10/plugins/dell/README.md000066400000000000000000000041761501337203100166020ustar00rootroot00000000000000--- title: Plugin: Dell --- ## Introduction This allows installing Dell capsules that are not part of the ESRT table. ## GUID Generation These devices uses custom GUIDs for Dell-specific hardware. * Thunderbolt devices: `TBT-0x00d4u$(system-id)` * TPM devices `$(system-id)-$(mode)`, where `mode` is either `2.0` or `1.2` In both cases the `system-id` is derived from the SMBIOS Product SKU property. TPM GUIDs are also built using the TSS properties `TPM2_PT_FAMILY_INDICATOR`, `TPM2_PT_MANUFACTURER`, and `TPM2_PT_VENDOR_STRING_*` These are built hierarchically with more parts for each GUID: * `DELL-TPM-$FAMILY-$MANUFACTURER-$VENDOR_STRING_1` * `DELL-TPM-$FAMILY-$MANUFACTURER-$VENDOR_STRING_1$VENDOR_STRING_2` * `DELL-TPM-$FAMILY-$MANUFACTURER-$VENDOR_STRING_1$VENDOR_STRING_2$VENDOR_STRING_3` * `DELL-TPM-$FAMILY-$MANUFACTURER-$VENDOR_STRING_1$VENDOR_STRING_2$VENDOR_STRING_3$VENDOR_STRING_4` If there are non-ASCII values in any vendor string or any vendor is missing that octet will be skipped. Example resultant GUIDs from a real system containing a TPM from Nuvoton: ```text Guid: 7d65b10b-bb24-552d-ade5-590b3b278188 <- DELL-TPM-2.0-NTC-NPCT Guid: 6f5ddd3a-8339-5b2a-b9a6-cf3b92f6c86d <- DELL-TPM-2.0-NTC-NPCT75x Guid: fe462d4a-e48f-5069-9172-47330fc5e838 <- DELL-TPM-2.0-NTC-NPCT75xrls ``` ## Devices powered by the Dell Plugin The Dell plugin creates device nodes for PC's with upgradable TPMs. These device nodes can be flashed using UEFI capsule but don't use the ESRT table to communicate device status or version information. This is intentional behavior because more complicated decisions need to be made on the OS side to determine if the devices should be offered to flash. ## External Interface Access This plugin requires read/write access to `/dev/wmi/dell-smbios` and `/sys/bus/platform/devices/dcdbas`. ## Version Considerations This plugin has been available since fwupd version `0.8.0`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Mario Limonciello: @superm1 fwupd-2.0.10/plugins/dell/dell.quirk000066400000000000000000000013571501337203100173160ustar00rootroot00000000000000# Dell TB16/TB18 cable [TBT-00d4b051] Plugin = thunderbolt ParentGuid = e7ca1f36-bf73-4574-afe6-a4ccacabf479 # Dell TB16/TB18 dock [TBT-00d4b054] Plugin = thunderbolt ParentGuid = e7ca1f36-bf73-4574-afe6-a4ccacabf479 # Dell WD15 dock [MST-wd15-vmm3332-274] Plugin = synaptics_mst ParentGuid = e7ca1f36-bf73-4574-afe6-a4ccacabf479 # Dell TB16 dock [MST-tb16-vmm3320-274] Plugin = synaptics_mst ParentGuid = e7ca1f36-bf73-4574-afe6-a4ccacabf479 [MST-tb16-vmm3330-274] Plugin = synaptics_mst ParentGuid = e7ca1f36-bf73-4574-afe6-a4ccacabf479 #Dell TB18 dock [MST-tb18-vmm3320-274] Plugin = synaptics_mst ParentGuid = e7ca1f36-bf73-4574-afe6-a4ccacabf479 [MST-tb18-vmm3330-274] Plugin = synaptics_mst ParentGuid = e7ca1f36-bf73-4574-afe6-a4ccacabf479 fwupd-2.0.10/plugins/dell/fu-dell-plugin.c000066400000000000000000000170541501337203100203120ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * Copyright 2016 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include #include "fu-dell-plugin.h" #define DACI_FLASH_INTERFACE_CLASS 7 #define DACI_FLASH_INTERFACE_SELECT 3 #define BIOS_SETTING_BIOS_DOWNGRADE "com.dell-wmi-sysman.AllowBiosDowngrade" struct _FuDellPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuDellPlugin, fu_dell_plugin, FU_TYPE_PLUGIN) struct da_structure { guint8 type; guint8 length; guint16 handle; guint16 cmd_address; guint8 cmd_code; guint32 supported_cmds; guint8 *tokens; } __attribute__((packed)); /* nocheck:blocked */ /** * Dell device types to run */ static guint8 enclosure_allowlist[] = {0x03, /* desktop */ 0x04, /* low profile desktop */ 0x06, /* mini tower */ 0x07, /* tower */ 0x08, /* portable */ 0x09, /* laptop */ 0x0A, /* notebook */ 0x0D, /* AIO */ 0x1E, /* tablet */ 0x1F, /* convertible */ 0x21, /* IoT gateway */ 0x22, /* embedded PC */}; static guint16 fu_dell_plugin_get_system_id(FuPlugin *plugin) { FuContext *ctx = fu_plugin_get_context(plugin); const gchar *system_id_str = NULL; guint64 system_id_val = 0; system_id_str = fu_context_get_hwid_value(ctx, FU_HWIDS_KEY_PRODUCT_SKU); if (system_id_str != NULL) { g_autoptr(GError) error_local = NULL; if (!fu_strtoull(system_id_str, &system_id_val, 0, G_MAXUINT16, FU_INTEGER_BASE_16, &error_local)) { g_warning("failed to parse system ID: %s", error_local->message); } } return (guint16)system_id_val; } static gboolean fu_dell_plugin_supported(FuPlugin *plugin, GError **error) { FuContext *ctx = fu_plugin_get_context(plugin); FuSmbiosChassisKind chassis_kind = fu_context_get_chassis_kind(ctx); GBytes *de_blob = NULL; GBytes *da_blob = NULL; g_autoptr(GPtrArray) de_tables = NULL; g_autoptr(GPtrArray) da_tables = NULL; guint8 value = 0; struct da_structure da_values = {0x0}; /* make sure that Dell SMBIOS methods are available */ de_tables = fu_context_get_smbios_data(ctx, 0xDE, FU_SMBIOS_STRUCTURE_LENGTH_ANY, error); if (de_tables == NULL) return FALSE; de_blob = g_ptr_array_index(de_tables, 0); if (!fu_memread_uint8_safe(g_bytes_get_data(de_blob, NULL), g_bytes_get_size(de_blob), 0x0, &value, error)) { g_prefix_error(error, "invalid DE data: "); return FALSE; } if (value != 0xDE) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid DE data"); return FALSE; } da_tables = fu_context_get_smbios_data(ctx, 0xDA, FU_SMBIOS_STRUCTURE_LENGTH_ANY, error); if (da_tables == NULL) return FALSE; da_blob = g_ptr_array_index(da_tables, 0); if (!fu_memcpy_safe((guint8 *)&da_values, sizeof(da_values), 0x0, /* dst */ g_bytes_get_data(da_blob, NULL), g_bytes_get_size(da_blob), 0x0, /* src */ sizeof(da_values), error)) { g_prefix_error(error, "unable to access flash interface: "); return FALSE; } if (!(da_values.supported_cmds & (1 << DACI_FLASH_INTERFACE_CLASS))) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "unable to access flash interface. supported commands: 0x%x", da_values.supported_cmds); return FALSE; } /* only run on intended Dell hw types */ for (guint i = 0; i < G_N_ELEMENTS(enclosure_allowlist); i++) { if (enclosure_allowlist[i] == chassis_kind) return TRUE; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "chassis invalid"); return FALSE; } static void fu_dell_plugin_device_registered(FuPlugin *plugin, FuDevice *device) { /* thunderbolt plugin */ if (g_strcmp0(fu_device_get_plugin(device), "thunderbolt") == 0 && fu_device_has_flag(device, FWUPD_DEVICE_FLAG_INTERNAL)) { /* fix VID/DID of safe mode devices */ if (fu_device_get_metadata_boolean(device, FU_DEVICE_METADATA_TBT_IS_SAFE_MODE)) { g_autofree gchar *device_id = NULL; guint16 system_id = 0; system_id = fu_dell_plugin_get_system_id(plugin); if (system_id == 0) return; /* the kernel returns lowercase in sysfs, need to match it */ device_id = g_strdup_printf("TBT-%04x%04x", 0x00d4u, (unsigned)system_id); fu_device_build_vendor_id_u16(device, "TBT", 0x00D4); fu_device_add_instance_id(device, device_id); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); } } /* tpm plugin */ if (g_strcmp0(fu_device_get_plugin(device), "tpm") == 0) { guint16 system_id = fu_dell_plugin_get_system_id(plugin); g_autofree gchar *tpm_guid_raw = NULL; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT); fu_device_set_metadata(device, FU_DEVICE_METADATA_UEFI_DEVICE_KIND, "dell-tpm-firmware"); tpm_guid_raw = g_strdup_printf("%04x-2.0", system_id); fu_device_add_instance_id(device, tpm_guid_raw); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_QUAD); } } static gboolean fu_dell_plugin_startup(FuPlugin *plugin, FuProgress *progress, GError **error) { g_autofree gchar *sysfsfwdir = NULL; g_autofree gchar *esrtdir = NULL; if (!fu_dell_plugin_supported(plugin, error)) { g_prefix_error(error, "firmware updating not supported: "); return FALSE; } /* If ESRT is not turned on, fwupd will have already created an * unlock device. * * Once unlocked, that will enable flashing capsules here too. */ sysfsfwdir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR_FW); esrtdir = g_build_filename(sysfsfwdir, "efi", "esrt", NULL); if (!g_file_test(esrtdir, G_FILE_TEST_EXISTS)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "capsule support disabled in BIOS"); return FALSE; } return TRUE; } static void fu_dell_plugin_add_security_attrs(FuPlugin *plugin, FuSecurityAttrs *attrs) { FwupdBiosSetting *bios_attr; FuContext *ctx = fu_plugin_get_context(plugin); g_autoptr(FwupdSecurityAttr) attr = NULL; bios_attr = fu_context_get_bios_setting(ctx, BIOS_SETTING_BIOS_DOWNGRADE); if (bios_attr == NULL) { g_debug("failed to find %s in cache", BIOS_SETTING_BIOS_DOWNGRADE); return; } attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_BIOS_ROLLBACK_PROTECTION); fu_security_attr_add_bios_target_value(attr, BIOS_SETTING_BIOS_DOWNGRADE, "Disabled"); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_ENABLED); fu_security_attrs_append(attrs, attr); if (g_strcmp0(fwupd_bios_setting_get_current_value(bios_attr), "Enabled") == 0) { fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); return; } fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_dell_plugin_init(FuDellPlugin *self) { } static void fu_dell_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); /* make sure that UEFI plugin is ready to receive devices */ fu_plugin_add_rule(plugin, FU_PLUGIN_RULE_RUN_AFTER, "uefi_capsule"); } static void fu_dell_plugin_class_init(FuDellPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_dell_plugin_constructed; plugin_class->startup = fu_dell_plugin_startup; plugin_class->device_registered = fu_dell_plugin_device_registered; plugin_class->add_security_attrs = fu_dell_plugin_add_security_attrs; } fwupd-2.0.10/plugins/dell/fu-dell-plugin.h000066400000000000000000000003461501337203100203130ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuDellPlugin, fu_dell_plugin, FU, DELL_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/dell/meson.build000066400000000000000000000006721501337203100174620ustar00rootroot00000000000000if allow_uefi_capsule cargs = ['-DG_LOG_DOMAIN="FuPluginDell"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('dell.quirk') plugin_builtin_dell = static_library('fu_plugin_dell', sources: [ 'fu-dell-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: [ cargs, ], dependencies: [ plugin_deps, ], ) plugin_builtins += plugin_builtin_dell endif fwupd-2.0.10/plugins/dell/tests000077700000000000000000000000001501337203100223342../uefi-capsule/tests/ustar00rootroot00000000000000fwupd-2.0.10/plugins/dfu/000077500000000000000000000000001501337203100151515ustar00rootroot00000000000000fwupd-2.0.10/plugins/dfu/README.md000066400000000000000000000130331501337203100164300ustar00rootroot00000000000000--- title: Plugin: DFU --- ## Introduction Device Firmware Update is a standard that allows USB devices to be easily and safely updated by any operating system. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in DFU or DfuSe file format. This plugin supports the following protocol IDs: * `org.usb.dfu` * `com.st.dfuse` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_273F&PID_1003&REV_0001` * `USB\VID_273F&PID_1003` ## Update Behavior A DFU device usually presents in runtime mode (with optional DFU runtime), but on detach re-enumerates with an additional required DFU descriptor. On attach the device again re-enumerates back to the runtime mode. For this reason the `REPLUG_MATCH_GUID` internal device flag is used so that the bootloader and runtime modes are treated as the same device. ## Implementation Notes The runtime mode is just as important as the DFU mode from the point of view of fwupd and should be included if firmware updates are to "just work". Without a DFU runtime interface we can match the device with `Flags = no-dfu-runtime` but will need a suitably new fwupd version before the device is recognized. The USB interface revision (`REV`) is used as the BCD version number, as DFU has no way of representing a firmware version number. A new firmware version should always increment the USB REV of the *runtime* interface as fwupd will **not** switch the device into *DFU mode* during enumeration to read the version number. The version number of the DFU mode should represent the *bootloader version* and this should not change as the firmware is updated. The runtime USB interface should have a unique vendor ID and product ID for the specific firmware stream. A different version of software should have a unique VID/PID USB descriptor pair. The microcontroller example VID/PID should **never** be used in the runtime mode otherwise fwupd would not know what firmware to match. Ideally, the bootloader should also have a unique USB vendor ID and product ID. This allows fwupd to more easily recognize the runtime interface *going away* and the DFU interface *coming back*. If the VID/PID is the same in runtime and DFU modes then the quirk `Flags = no-pid-change` is required. If the bootloader VID/PID is not customized (as might be the default for the supplied MCU) then fwupd can match the runtime VID/PID to the bootloader VID/PID. Where this fails is when the device is *stuck* in the DFU mode, perhaps because the user removed the USB cable before the device had completed updating. With a unique VID/PID fwupd can *recover* the device stuck in DFU mode, reflashing the device with the latest matching firmware and then attaching it back into runtime mode. Using a *generic* VID/PID for the bootloader means fwupd does not know how to recover the device back into runtime mode as the client does not know what firmware to choose and the user is forced to either RMA the device, or to download the correct file manually from the vendor vebsite and use low-level commands like `sudo fwupdtool install-blob`. ## Vendor ID Security The vendor ID is set from the USB vendor, for example `USB:0x0A12` ## Quirk Use This plugin uses the following plugin-specific quirks: ### DfuFlags Optional quirks for a DFU device which doesn't follow the DFU 1.0 or 1.1 specification. Since: 1.0.1 ### DfuForceVersion Forces a specific DFU version for the hardware device. This is required if the device does not set, or sets incorrectly, items in the DFU functional descriptor. If set to 0000 then the DFU functionality is disabled. Since: 1.0.1 ### DfuForceTimeout Forces a specific device timeout, in ms. Since: 1.4.0 ### DfuForceTransferSize Forces a target transfer size, in bytes. Since: 1.5.6 ### `Flags=can-download` Can download from host->device. ### `Flags=can-upload` Can upload from device->host. ### `Flags=manifest-tol` Can answer GetStatus in manifest ### `Flags=will-detach` Will self-detach. ### `Flags=can-accelerate` Use a larger transfer size for speed. ### `Flags=attach-upload-download` An upload or download is required for attach. ### `Flags=force-dfu-mode` Force DFU mode. ### `Flags=ignore-polltimeout` Ignore the device download timeout. ### `Flags=ignore-runtime` Device has broken DFU runtime support. ### `Flags=ignore-upload` Uploading from the device is broken. ### `Flags=no-dfu-runtime` No DFU runtime interface is provided. ### `Flags=no-get-status-upload` Do not do GetStatus when uploading. ### `Flags=no-pid-change` Accept the same VID:PID when changing modes. ### `Flags=use-any-interface` Use any interface for DFU. ### `Flags=use-atmel-avr` Device uses the ATMEL bootloader. ### `Flags=use-protocol-zero` Fix up the protocol number. ### `Flags=legacy-protocol` Use a legacy protocol version. ### `Flags=detach-for-attach` Requires a FU_DFU_REQUEST_DETACH to attach. ### `Flags=absent-sector-size` In absence of sector size, assume byte. ### `Flags=manifest-poll` Requires polling via GetStatus in dfuManifest state. ### `Flags=no-bus-reset-attach` Do not require a bus reset to attach to normal. ### `Flags=gd32` Uses the slightly weird GD32 variant of DFU. ### `Flags=allow-zero-polltimeout` Allows the zero bwPollTimeout from GetStatus in dfuDNLOAD-SYNC state. ### `Flags=index-force-detach` Requires Force Detach in wIndex to bypass status checking. ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `0.8.0`. fwupd-2.0.10/plugins/dfu/contrib/000077500000000000000000000000001501337203100166115ustar00rootroot00000000000000fwupd-2.0.10/plugins/dfu/contrib/parse-avrdude-conf.py000077500000000000000000000114641501337203100226610ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2017 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later """ This parses avrdude.conf and generates quirks for fwupd """ # pylint: disable=wrong-import-position,pointless-string-statement import sys from difflib import SequenceMatcher # finds a part using the ID def _find_part_by_id(parts, part_id): for part in parts: if "id" not in part: continue if part["id"] == part_id: return part return None # finds a memory layout for a part, climbing up the tree to the parent if reqd. def _find_mem_layout(parts, part): if "memory-application" in part: memory_flash = part["memory-application"] if memory_flash: return memory_flash # look at the parent if "parent" in part: parent = _find_part_by_id(parts, part["parent"]) if parent: return _find_mem_layout(parts, parent) print("no parent ", part["parent"], "found for", part["id"]) return None # parses the weird syntax of avrdude.conf and makes lots of nested dictionaries def _parse_parts(fn_source): print("reading", fn_source) part = None memory_id = None parts = [] for line in open(fn_source).readlines(): # try to clean up crazy syntax line = line.replace("\n", "") if line.endswith(";"): line = line[:-1] # ignore blank lines line = line.rstrip() if not line: continue # count how many spaces deep this is lvl = 0 for char in line: if char != " ": break lvl = lvl + 1 # ignore comments line = line.strip() if line[0] == "#": continue # level 0 of hell if lvl == 0: if line.startswith("part"): memory_id = None part = {} parts.append(part) if line.startswith("part parent "): part["parent"] = line[13:].replace('"', "") continue # level 4 of hell if lvl == 4: if line.startswith("memory"): memory_id = "memory-" + line[7:].replace('"', "") part[memory_id] = {} continue split = line.split("=") if len(split) != 2: print("ignoring", line) continue part[split[0].strip()] = split[1].strip().replace('"', "") continue # level 8 of hell if lvl == 8: if memory_id: split = line.split("=") if len(split) != 2: continue memory = part[memory_id] memory[split[0].strip()] = split[1].strip() continue return parts def _get_longest_substring(s1, s2): match = SequenceMatcher(None, s1, s2).find_longest_match(0, len(s1), 0, len(s2)) return s2[match.b : match.b + match.size] # writes important data to the quirks file def _write_quirks(parts, fn_destination): outp = [] results = {} for part in parts: # ignore meta parts with deprecated names if "desc" not in part: continue if "signature" not in part: continue # find the layout mem_part = _find_mem_layout(parts, part) if not mem_part: print("no memory layout for", part["desc"]) continue if "size" not in mem_part: print("no memory size for", part["desc"]) continue if mem_part["size"].startswith("0x"): size = int(mem_part["size"], 16) else: size = int(mem_part["size"], 10) # output the line for the quirk chip_id = "0x" + part["signature"].replace("0x", "").replace(" ", "") mem_layout = "@Flash/0x0/1*%.0iKg" % int(size / 1024) # merge duplicate quirks if chip_id in results: result = results[chip_id] result["desc"] = _get_longest_substring(result["desc"], part["desc"]) else: result = {} result["desc"] = part["desc"] result["size"] = size result["mem_layout"] = mem_layout results[chip_id] = result for chip_id in results: result = results[chip_id] outp.append( "# " + result["desc"] + f"\t[USER]\t\tUSER=0x{result['size']:x}" + "\n" ) outp.append(chip_id + "=" + result["mem_layout"] + "\n\n") # write file print("writing", fn_destination) open(fn_destination, "w").writelines(outp) if __name__ == "__main__": if len(sys.argv) != 3: print(f"USAGE: {sys.argv[0]} avrdude.conf tmp.quirk") sys.exit(1) all_parts = _parse_parts(sys.argv[1]) _write_quirks(all_parts, sys.argv[2]) fwupd-2.0.10/plugins/dfu/dfu.quirk000066400000000000000000000324131501337203100170070ustar00rootroot00000000000000# All DFU devices [USB\CLASS_FE&SUBCLASS_01] Plugin = dfu # GD32VF103 Rev1 [USB\VID_28E9&PID_0189] Flags = gd32,force-dfu-mode,will-disappear Name = GD32VF103 Vendor = GDMicroelectronics # Realtek USB camera [USB\VID_0BDA&PID_5850] Flags = enforce-requires CounterpartGuid = USB\VID_0BDA&PID_5800 [USB\VID_0BDA&PID_5855] Flags = enforce-requires CounterpartGuid = USB\VID_0BDA&PID_5800 [USB\VID_0BDA&PID_58FE] Flags = enforce-requires CounterpartGuid = USB\VID_0BDA&PID_5800 [USB\VID_0BDA&PID_5800] Flags = detach-for-attach,enforce-requires # Openmoko Freerunner / GTA02 [USB\VID_1D50&PID_5119] Plugin = dfu Flags = ignore-polltimeout,no-pid-change,no-dfu-runtime,needs-bootloader,no-get-status-upload # OpenPCD Reader [USB\VID_16C0&PID_076B] Plugin = dfu Flags = ignore-polltimeout # SIMtrace [USB\VID_16C0&PID_0762] Plugin = dfu Flags = ignore-polltimeout # OpenPICC [USB\VID_16C0&PID_076C] Plugin = dfu Flags = ignore-polltimeout # Siemens AG, PXM 40 & PXM 50 [USB\VID_0908&PID_02C4] Plugin = dfu [USB\VID_0908&PID_02C5] Plugin = dfu [USB\VID_0908&PID_02C4&REV_0000] Flags = ignore-polltimeout [USB\VID_0908&PID_02C5&REV_0000] Flags = ignore-polltimeout # Midiman M-Audio Transit [USB\VID_0763&PID_2806] Plugin = dfu Flags = ignore-polltimeout # LPC DFU bootloader [USB\VID_1FC9&PID_000C] Plugin = dfu Flags = force-dfu-mode # m-stack DFU [USB\VID_273F&PID_1003] Flags = attach-upload-download [USB\VID_273F&PID_100A] Flags = attach-upload-download [USB\VID_273F&PID_1008] Flags = attach-upload-download # HydraBus [USB\VID_1D50&PID_60A7] Plugin = dfu Flags = no-dfu-runtime,needs-bootloader # Hughski AT90USBKEY Mouse+DFU Demo [USB\VID_273F&PID_2000] Flags = unsigned-payload # Jabra 410 [appIDLE & dfuIDLE] [USB\VID_0B0E&PID_0411] Plugin = dfu Flags = no-pid-change,ignore-upload,attach-extra-reset # Jabra 510 [appIDLE & dfuIDLE] [USB\VID_0B0E&PID_0421] Plugin = dfu Flags = no-pid-change,ignore-upload,attach-extra-reset # Jabra 710 [appIDLE & dfuIDLE] [USB\VID_0B0E&PID_0982] Plugin = dfu Flags = no-pid-change,ignore-upload,attach-extra-reset # Jabra 810 [appIDLE & dfuIDLE] [USB\VID_0B0E&PID_0971] Plugin = dfu Flags = no-pid-change,ignore-upload,attach-extra-reset # Atmel AT90USB Bootloader [USB\VID_03EB&PID_2FF7] Plugin = dfu Flags = use-any-interface,legacy-protocol,force-dfu-mode DfuForceVersion = 0xff01 [USB\VID_03EB&PID_2FF9] Plugin = dfu Flags = use-any-interface,legacy-protocol,force-dfu-mode DfuForceVersion = 0xff01 [USB\VID_03EB&PID_2FFA] Plugin = dfu Flags = use-any-interface,legacy-protocol,force-dfu-mode DfuForceVersion = 0xff01 [USB\VID_03EB&PID_2FFB] Plugin = dfu Flags = use-any-interface,legacy-protocol,force-dfu-mode DfuForceVersion = 0xff01 # Atmel ATMEGA Bootloader [USB\VID_03EB&PID_2FEE] Plugin = dfu Flags = use-any-interface,legacy-protocol,force-dfu-mode [USB\VID_03EB&PID_2FEF] Plugin = dfu Flags = use-any-interface,legacy-protocol,force-dfu-mode [USB\VID_03EB&PID_2FF0] Plugin = dfu Flags = use-any-interface,legacy-protocol,force-dfu-mode [USB\VID_03EB&PID_2FF2] Plugin = dfu Flags = use-any-interface,legacy-protocol,force-dfu-mode [USB\VID_03EB&PID_2FF3] Plugin = dfu Flags = use-any-interface,legacy-protocol,force-dfu-mode [USB\VID_03EB&PID_2FF4] Plugin = dfu Flags = use-any-interface,legacy-protocol,force-dfu-mode # Atmel XMEGA Bootloader [USB\VID_03EB&PID_2FE2] Plugin = dfu Flags = use-any-interface,force-dfu-mode DfuForceVersion = 0xff01 # Leaflabs Maple3 [USB\VID_1EAF&PID_0003&REV_0200] Plugin = dfu DfuForceVersion = 0x0110 # AT32UC3B1256 [BLDR][USER] USER@0x2000, BLDR+USER=0x40000 [DFU_AVR\CID_0x58200203] DfuAltName = @Flash/0x2000/1*248Kg # AT32UC3A3256 [BLDR][USER] USER@0x2000, BLDR+USER=0x40000 [DFU_AVR\CID_0x58200204] DfuAltName = @Flash/0x2000/1*248Kg # AT90USB1287 [USER][BLDR] BLDR@0x1e000, BLDR+USER=0x20000 [DFU_AVR\CID_0x581e9782] DfuAltName = @Flash/0x0/1*120Kg # AT90USB647 [USER][BLDR] BLDR@0x0e000, BLDR+USER=0x10000 # AT90USB646 [USER][BLDR] BLDR@0x0e000, BLDR+USER=0x10000 [DFU_AVR\CID_0x581e9682] DfuAltName = @Flash/0x0/1*56Kg # ATmega32U4 [USER][BLDR] BLDR@0x07000, BLDR+USER=0x08000 [DFU_AVR\CID_0x581e9587] DfuAltName = @Flash/0x0/1*28Kg # ATmega16U4 [USER][BLDR] BLDR@0x03000, BLDR+USER=0x04000 [DFU_AVR\CID_0x581e9488] DfuAltName = @Flash/0x0/1*12Kg # ATmega32U2 [USER][BLDR] BLDR@0x07000, BLDR+USER=0x08000 [DFU_AVR\CID_0x581e958a] DfuAltName = @Flash/0x0/1*28Kg # ATmega16U2 [USER][BLDR] BLDR@0x03000, BLDR+USER=0x04000 [DFU_AVR\CID_0x581e9489] DfuAltName = @Flash/0x0/1*12Kg # AT90USB162 [USER][BLDR] BLDR@0x03000, BLDR+USER=0x04000 [DFU_AVR\CID_0x581e9482] DfuAltName = @Flash/0x0/1*12Kg # ATmega8U2 [USER][BLDR] BLDR@0x01000, BLDR+USER=0x02000 [DFU_AVR\CID_0x581e9389] DfuAltName = @Flash/0x0/1*4Kg # AT90USB82 [USER][BLDR] BLDR@0x01000, BLDR+USER=0x02000 [DFU_AVR\CID_0x581e9382] DfuAltName = @Flash/0x0/1*4Kg # ATxmega16A4 [USER] USER=0x4000 [DFU_AVR\CID_0x1e9441] DfuAltName = @Flash/0x0/1*16Kg # ATxmega16C4 [USER] USER=0x4000 [DFU_AVR\CID_0x1e9544] DfuAltName = @Flash/0x0/1*16Kg # ATxmega16D4 [USER] USER=0x4000 [DFU_AVR\CID_0x1e9442] DfuAltName = @Flash/0x0/1*16Kg # ATxmega32A4 [USER] USER=0x8000 [DFU_AVR\CID_0x1e9541] DfuAltName = @Flash/0x0/1*32Kg # ATxmega32C4 [USER] USER=0x8000 [DFU_AVR\CID_0x1e9443] DfuAltName = @Flash/0x0/1*32Kg # ATxmega32D4 [USER] USER=0x8000 [DFU_AVR\CID_0x1e9542] DfuAltName = @Flash/0x0/1*32Kg # ATxmega64A4 [USER] USER=0x10000 [DFU_AVR\CID_0x1e9646] DfuAltName = @Flash/0x0/1*64Kg # ATxmega64C3 [USER] USER=0x10000 [DFU_AVR\CID_0x1e9649] DfuAltName = @Flash/0x0/1*64Kg # ATxmega64D3 [USER] USER=0x10000 [DFU_AVR\CID_0x1e964a] DfuAltName = @Flash/0x0/1*64Kg # ATxmega64D4 [USER] USER=0x10000 [DFU_AVR\CID_0x1e9647] DfuAltName = @Flash/0x0/1*64Kg # ATxmega64A1 [USER] USER=0x10000 [DFU_AVR\CID_0x1e964e] DfuAltName = @Flash/0x0/1*64Kg # ATxmega64A3 [USER] USER=0x10000 [DFU_AVR\CID_0x1e9642] DfuAltName = @Flash/0x0/1*64Kg # ATxmega64B1 [USER] USER=0x10000 [DFU_AVR\CID_0x1e9652] DfuAltName = @Flash/0x0/1*64Kg # ATxmega64B3 [USER] USER=0x10000 [DFU_AVR\CID_0x1e9651] DfuAltName = @Flash/0x0/1*64Kg # ATxmega128C3 [USER] USER=0x20000 [DFU_AVR\CID_0x1e9752] DfuAltName = @Flash/0x0/1*128Kg # ATxmega128D3 [USER] USER=0x20000 [DFU_AVR\CID_0x1e9748] DfuAltName = @Flash/0x0/1*128Kg # ATxmega128D4 [USER] USER=0x20000 [DFU_AVR\CID_0x1e9747] DfuAltName = @Flash/0x0/1*128Kg # ATxmega128A1 [USER] USER=0x20000 [DFU_AVR\CID_0x1e974c] DfuAltName = @Flash/0x0/1*128Kg # ATxmega128A1D [USER] USER=0x20000 [DFU_AVR\CID_0x1e9741] DfuAltName = @Flash/0x0/1*128Kg # ATxmega128A3 [USER] USER=0x20000 [DFU_AVR\CID_0x1e9742] DfuAltName = @Flash/0x0/1*128Kg # ATxmega128A4 [USER] USER=0x20000 [DFU_AVR\CID_0x1e9746] DfuAltName = @Flash/0x0/1*128Kg # ATxmega128B1 [USER] USER=0x20000 [DFU_AVR\CID_0x1e974d] DfuAltName = @Flash/0x0/1*128Kg # ATxmega128B3 [USER] USER=0x20000 [DFU_AVR\CID_0x1e974b] DfuAltName = @Flash/0x0/1*128Kg # ATxmega192C3 [USER] USER=0x30000 [DFU_AVR\CID_0x1e9751] DfuAltName = @Flash/0x0/1*192Kg # ATxmega192D3 [USER] USER=0x30000 [DFU_AVR\CID_0x1e9749] DfuAltName = @Flash/0x0/1*192Kg # ATxmega192A1 [USER] USER=0x30000 [DFU_AVR\CID_0x1e974e] DfuAltName = @Flash/0x0/1*192Kg # ATxmega192A3 [USER] USER=0x30000 [DFU_AVR\CID_0x1e9744] DfuAltName = @Flash/0x0/1*192Kg # ATxmega256 [USER] USER=0x40000 [DFU_AVR\CID_0x1e9846] DfuAltName = @Flash/0x0/1*256Kg # ATxmega256D3 [USER] USER=0x40000 [DFU_AVR\CID_0x1e9844] DfuAltName = @Flash/0x0/1*256Kg # ATxmega256A3 [USER] USER=0x40000 [DFU_AVR\CID_0x1e9842] DfuAltName = @Flash/0x0/1*256Kg # ATxmega256A3B [USER] USER=0x40000 [DFU_AVR\CID_0x1e9843] DfuAltName = @Flash/0x0/1*256Kg # ATxmega384C3 [USER] USER=0x60000 [DFU_AVR\CID_0x1e9845] DfuAltName = @Flash/0x0/1*384Kg # ATxmega384D3 [USER] USER=0x60000 [DFU_AVR\CID_0x1e9847] DfuAltName = @Flash/0x0/1*384Kg # ATxmega8E5 [USER] USER=0x2000 [DFU_AVR\CID_0x1e9341] DfuAltName = @Flash/0x0/1*8Kg # ATxmega16E5 [USER] USER=0x4000 [DFU_AVR\CID_0x1e9445] DfuAltName = @Flash/0x0/1*16Kg # ATxmega32E5 [USER] USER=0x8000 [DFU_AVR\CID_0x1e954c] DfuAltName = @Flash/0x0/1*32Kg # STM32F745 dfuse bootloader [USB\VID_0483&PID_DF11] Flags = absent-sector-size,will-disappear Plugin = dfu DfuForceVersion = 0x011a DfuForceTimeout = 5000 # Poly Studio USB [USB\VID_095D&PID_9217] Plugin = dfu Flags = manifest-poll,no-bus-reset-attach,allow-zero-polltimeout,signed-payload,index-force-detach RemoveDelay = 60000 [USB\VID_095D&PID_9218] Plugin = dfu Flags = manifest-poll,no-bus-reset-attach,allow-zero-polltimeout,signed-payload RemoveDelay = 60000 # Poly Eagle Eye Cube [USB\VID_095D&PID_9212] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,unsigned-payload RemoveDelay = 30000 [USB\VID_095D&PID_9213] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,unsigned-payload RemoveDelay = 30000 # Poly Studio P15 [USB\VID_095D&PID_9290] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,signed-payload,index-force-detach RemoveDelay = 60000 [USB\VID_095D&PID_9291] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,signed-payload RemoveDelay = 60000 # Poly ULCC [USB\VID_095D&PID_9160] Plugin = dfu Flags = manifest-poll,no-bus-reset-attach,allow-zero-polltimeout,signed-payload,index-force-detach RemoveDelay = 60000 [USB\VID_095D&PID_927B] Plugin = dfu Flags = manifest-poll,no-bus-reset-attach,allow-zero-polltimeout,signed-payload RemoveDelay = 60000 # Poly Eagle Eye Mini [USB\VID_095D&PID_3001] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,unsigned-payload RemoveDelay = 9000 [USB\VID_095D&PID_3002] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,unsigned-payload RemoveDelay = 9000 # Poly Studio R30 [USB\VID_095D&PID_92B2] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,signed-payload,index-force-detach RemoveDelay = 60000 [USB\VID_095D&PID_92B3] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,signed-payload RemoveDelay = 60000 [USB\VID_095D&PID_92B4] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,signed-payload,index-force-detach RemoveDelay = 60000 # Poly Studio P5 [USB\VID_095D&PID_9296] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,unsigned-payload RemoveDelay = 9000 [USB\VID_095D&PID_9297] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,unsigned-payload RemoveDelay = 9000 # Poly Studio E70 [USB\VID_095D&PID_92A1] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,signed-payload,index-force-detach RemoveDelay = 90000 [USB\VID_095D&PID_92A2] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,signed-payload RemoveDelay = 90000 # Poly Studio P21 [USB\VID_095D&PID_9298] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,unsigned-payload RemoveDelay = 9000 [USB\VID_095D&PID_9299] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,unsigned-payload RemoveDelay = 9000 # Poly Studio V52 [USB\VID_095D&PID_92C6] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,signed-payload,index-force-detach RemoveDelay = 180000 [USB\VID_095D&PID_92C8] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,signed-payload,index-force-detach RemoveDelay = 180000 [USB\VID_095D&PID_92C7] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,signed-payload RemoveDelay = 180000 # Poly Studio V72 [USB\VID_095D&PID_92D8] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,signed-payload,index-force-detach RemoveDelay = 180000 [USB\VID_095D&PID_92DA] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,signed-payload,index-force-detach RemoveDelay = 180000 [USB\VID_095D&PID_92D9] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,signed-payload RemoveDelay = 180000 # Poly Studio V12 [USB\VID_095D&PID_92D2] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,signed-payload,index-force-detach RemoveDelay = 180000 [USB\VID_095D&PID_92D4] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,signed-payload,index-force-detach RemoveDelay = 180000 [USB\VID_095D&PID_92D3] Plugin = dfu Flags = manifest-poll,allow-zero-polltimeout,signed-payload RemoveDelay = 180000 # AVer ATLAS CAM [USB\VID_34AD&PID_0006] Plugin = dfu Flags = detach-for-attach RemoveDelay = 60000 # AVer CAM520 Pro2 [USB\VID_2574&PID_0A30] Plugin = dfu Flags = detach-for-attach RemoveDelay = 180000 # FlatFrog DFU [USB\VID_25B5&PID_0004] Plugin = dfu Flags = manifest-poll,detach-for-attach,ignore-upload # SunplusIT USB cameras [USB\VID_1BCF&PID_0B1D] Plugin = dfu Flags = detach-for-attach,ignore-upload,ignore-polltimeout [USB\VID_1BCF&PID_0B1E] Plugin = dfu Flags = detach-for-attach,ignore-upload [USB\VID_1BCF&PID_0B22] Plugin = dfu Flags = detach-for-attach,ignore-upload # Sonix USB cameras [USB\VID_0C45&PID_636D] Plugin = dfu Flags = detach-for-attach,ignore-upload,enforce-requires [USB\VID_0C45&PID_636C] Plugin = dfu Flags = detach-for-attach,ignore-upload,enforce-requires [USB\VID_0C45&PID_636E] Plugin = dfu Flags = detach-for-attach,ignore-upload,enforce-requires [USB\VID_0C45&PID_636F] Plugin = dfu Flags = detach-for-attach,ignore-upload,enforce-requires [USB\VID_0C45&PID_6373] Plugin = dfu Flags = detach-for-attach,ignore-upload,enforce-requires [USB\VID_0C45&PID_6374] Plugin = dfu Flags = detach-for-attach,ignore-upload,enforce-requires # FPC Fingerprint Reader [USB\VID_10A5&PID_FFE0] DfuForceVersion = 0x0 Flags = enforce-requires [USB\VID_10A5&PID_FFE1] DfuForceVersion = 0x0 Flags = enforce-requires [USB\VID_10A5&PID_9800] DfuForceVersion = 0x0 Flags = enforce-requires [USB\VID_10A5&PID_D805] DfuForceVersion = 0x0 Flags = enforce-requires fwupd-2.0.10/plugins/dfu/fu-dfu-common.c000066400000000000000000000021601501337203100177700ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-dfu-common.h" /** * fu_dfu_utils_bytes_join_array: * @chunks: (element-type GBytes): bytes * * Creates a monolithic block of memory from an array of #GBytes. * * Returns: (transfer full): a new GBytes **/ GBytes * fu_dfu_utils_bytes_join_array(GPtrArray *chunks) { gsize total_size = 0; guint32 offset = 0; guint8 *buffer; /* get the size of all the chunks */ for (guint i = 0; i < chunks->len; i++) { GBytes *chunk_tmp = g_ptr_array_index(chunks, i); total_size += g_bytes_get_size(chunk_tmp); } /* copy them into a buffer */ buffer = g_malloc0(total_size); for (guint i = 0; i < chunks->len; i++) { const guint8 *chunk_data; gsize chunk_size = 0; GBytes *chunk_tmp = g_ptr_array_index(chunks, i); chunk_data = g_bytes_get_data(chunk_tmp, &chunk_size); if (chunk_size == 0) continue; memcpy(buffer + offset, chunk_data, chunk_size); /* nocheck:blocked */ offset += chunk_size; } return g_bytes_new_take(buffer, total_size); } fwupd-2.0.10/plugins/dfu/fu-dfu-common.h000066400000000000000000000032671501337203100200060ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_DFU_DEVICE_FLAG_CAN_DOWNLOAD "can-download" #define FU_DFU_DEVICE_FLAG_CAN_UPLOAD "can-upload" #define FU_DFU_DEVICE_FLAG_MANIFEST_TOL "manifest-tol" #define FU_DFU_DEVICE_FLAG_WILL_DETACH "will-detach" #define FU_DFU_DEVICE_FLAG_CAN_ACCELERATE "can-accelerate" #define FU_DFU_DEVICE_FLAG_ATTACH_UPLOAD_DOWNLOAD "attach-upload-download" #define FU_DFU_DEVICE_FLAG_FORCE_DFU_MODE "force-dfu-mode" #define FU_DFU_DEVICE_FLAG_IGNORE_POLLTIMEOUT "ignore-polltimeout" #define FU_DFU_DEVICE_FLAG_IGNORE_RUNTIME "ignore-runtime" #define FU_DFU_DEVICE_FLAG_IGNORE_UPLOAD "ignore-upload" #define FU_DFU_DEVICE_FLAG_NO_DFU_RUNTIME "no-dfu-runtime" #define FU_DFU_DEVICE_FLAG_NO_GET_STATUS_UPLOAD "no-get-status-upload" #define FU_DFU_DEVICE_FLAG_NO_PID_CHANGE "no-pid-change" #define FU_DFU_DEVICE_FLAG_USE_ANY_INTERFACE "use-any-interface" #define FU_DFU_DEVICE_FLAG_USE_ATMEL_AVR "use-atmel-avr" #define FU_DFU_DEVICE_FLAG_USE_PROTOCOL_ZERO "use-protocol-zero" #define FU_DFU_DEVICE_FLAG_LEGACY_PROTOCOL "legacy-protocol" #define FU_DFU_DEVICE_FLAG_DETACH_FOR_ATTACH "detach-for-attach" #define FU_DFU_DEVICE_FLAG_ABSENT_SECTOR_SIZE "absent-sector-size" #define FU_DFU_DEVICE_FLAG_MANIFEST_POLL "manifest-poll" #define FU_DFU_DEVICE_FLAG_NO_BUS_RESET_ATTACH "no-bus-reset-attach" #define FU_DFU_DEVICE_FLAG_GD32 "gd32" #define FU_DFU_DEVICE_FLAG_ALLOW_ZERO_POLLTIMEOUT "allow-zero-polltimeout" #define FU_DFU_DEVICE_FLAG_INDEX_FORCE_DETACH "index-force-detach" GBytes * fu_dfu_utils_bytes_join_array(GPtrArray *chunks); fwupd-2.0.10/plugins/dfu/fu-dfu-device.c000066400000000000000000001371301501337203100177450ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ /** * FuDfuDevice: * * This object allows two things: * * - Downloading from the host to the device, optionally with * verification using a DFU or DfuSe firmware file. * * - Uploading from the device to the host to a DFU or DfuSe firmware * file. The file format is chosen automatically, with DfuSe being * chosen if the device contains more than one target. * * See also: [class@FuDfuTarget], [class@FuDfuseFirmware] */ /** * FU_QUIRKS_DFU_FORCE_VERSION: * @key: the USB device ID, e.g. `USB\VID_0763&PID_2806` * @value: the uint16_t DFU version, encoded in base 16, e.g. `0110` * * Forces a specific DFU version for the hardware device. This is required * if the device does not set, or sets incorrectly, items in the DFU functional * descriptor. If zero, then DFU functionality is disabled. * * Since: 1.0.1 */ #define FU_QUIRKS_DFU_FORCE_VERSION "DfuForceVersion" #define DFU_DEVICE_DNLOAD_TIMEOUT_DEFAULT 5 /* ms */ #include "config.h" #include "fu-dfu-common.h" #include "fu-dfu-device.h" #include "fu-dfu-target-avr.h" #include "fu-dfu-target-private.h" /* waive-pre-commit */ #include "fu-dfu-target-stm.h" static gboolean fu_dfu_device_clear_status(FuDfuDevice *self, GError **error); static void fu_dfu_device_finalize(GObject *object); typedef struct { FuDfuState state; FuDfuStatus status; GPtrArray *targets; gboolean done_upload_or_download; gboolean claimed_interface; gchar *chip_id; guint16 version; guint16 force_version; guint16 force_transfer_size; guint16 runtime_pid; guint16 runtime_vid; guint16 runtime_release; guint16 transfer_size; guint8 iface_number; guint dnload_timeout; guint timeout_ms; } FuDfuDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuDfuDevice, fu_dfu_device, FU_TYPE_USB_DEVICE) #define GET_PRIVATE(o) (fu_dfu_device_get_instance_private(o)) static void fu_dfu_device_set_state(FuDfuDevice *self, FuDfuState state); static void fu_dfu_device_to_string(FuDevice *device, guint idt, GString *str) { FuDfuDevice *self = FU_DFU_DEVICE(device); FuDfuDevicePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append(str, idt, "State", fu_dfu_state_to_string(priv->state)); fwupd_codec_string_append(str, idt, "Status", fu_dfu_status_to_string(priv->status)); fwupd_codec_string_append_bool(str, idt, "DoneUploadOrDownload", priv->done_upload_or_download); fwupd_codec_string_append_bool(str, idt, "ClaimedInterface", priv->claimed_interface); fwupd_codec_string_append(str, idt, "ChipId", priv->chip_id); fwupd_codec_string_append_hex(str, idt, "Version", priv->version); if (priv->force_version != G_MAXUINT16) fwupd_codec_string_append_hex(str, idt, "ForceVersion", priv->force_version); fwupd_codec_string_append_hex(str, idt, "ForceTransferSize", priv->force_transfer_size); fwupd_codec_string_append_hex(str, idt, "RuntimePid", priv->runtime_pid); fwupd_codec_string_append_hex(str, idt, "RuntimeVid", priv->runtime_vid); fwupd_codec_string_append_hex(str, idt, "RuntimeRelease", priv->runtime_release); fwupd_codec_string_append_hex(str, idt, "TransferSize", priv->transfer_size); fwupd_codec_string_append_hex(str, idt, "IfaceNumber", priv->iface_number); fwupd_codec_string_append_hex(str, idt, "DnloadTimeout", priv->dnload_timeout); fwupd_codec_string_append_hex(str, idt, "TimeoutMs", priv->timeout_ms); for (guint i = 0; i < priv->targets->len; i++) { FuDfuTarget *target = g_ptr_array_index(priv->targets, i); fu_device_add_string(FU_DEVICE(target), idt + 1, str); } } /** * fu_dfu_device_get_transfer_size: * @device: a USB device * * Gets the transfer size in bytes. * * Returns: packet size, or 0 for unknown **/ guint16 fu_dfu_device_get_transfer_size(FuDfuDevice *self) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_DEVICE(self), 0xffff); return priv->transfer_size; } /** * fu_dfu_device_get_version: * @self: a #FuDfuDevice * * Gets the DFU specification version supported by the device. * * Returns: integer, or 0 for unknown, e.g. %FU_DFU_FIRMARE_VERSION_DFU_1_1 **/ guint16 fu_dfu_device_get_version(FuDfuDevice *self) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_DEVICE(self), 0xffff); return priv->version; } /** * fu_dfu_device_get_download_timeout: * @self: a #FuDfuDevice * * Gets the download timeout in ms. * * Returns: delay, or 0 for unknown **/ guint fu_dfu_device_get_download_timeout(FuDfuDevice *self) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_DEVICE(self), 0); return priv->dnload_timeout; } static void fu_dfu_device_set_download_timeout(FuDfuDevice *self, guint dnload_timeout) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DFU_DEVICE(self)); /* quirked */ if (fu_device_has_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_IGNORE_POLLTIMEOUT)) { g_debug("ignoring dnload-timeout, using default of %ums", priv->dnload_timeout); return; } if (dnload_timeout == 0 && !fu_device_has_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_ALLOW_ZERO_POLLTIMEOUT)) { g_debug("no dnload-timeout, using default of %ums", priv->dnload_timeout); return; } /* use what the device says */ priv->dnload_timeout = dnload_timeout; } static gboolean fu_dfu_device_parse_iface_data(FuDfuDevice *self, GBytes *iface_data, GError **error) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); guint8 attributes; g_autoptr(FuUsbDfuDescriptorHdr) st = NULL; g_autoptr(GBytes) bytes = NULL; /* weirdly, quite common */ if (g_bytes_get_size(iface_data) == FU_USB_DFU_DESCRIPTOR_HDR_SIZE - 2) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_warning("truncated DFU interface data, no bcdDFUVersion"); fu_byte_array_append_bytes(buf, iface_data); fu_byte_array_append_uint8(buf, 0x1); fu_byte_array_append_uint8(buf, 0x1); bytes = g_byte_array_free_to_bytes(g_steal_pointer(&buf)); /* nocheck:blocked */ } else { bytes = g_bytes_ref(iface_data); } /* parse the functional descriptor */ st = fu_usb_dfu_descriptor_hdr_parse_bytes(bytes, 0x0, error); if (st == NULL) return FALSE; priv->transfer_size = fu_usb_dfu_descriptor_hdr_get_transfer_size(st); priv->version = fu_usb_dfu_descriptor_hdr_get_dfu_version(st); attributes = fu_usb_dfu_descriptor_hdr_get_attributes(st); /* ST-specific */ if (priv->version == FU_DFU_FIRMARE_VERSION_DFUSE && attributes & FU_DFU_DEVICE_ATTR_CAN_ACCELERATE) priv->transfer_size = 0x1000; /* get attributes about the DFU operation */ if (attributes & FU_DFU_DEVICE_ATTR_CAN_DOWNLOAD) fu_device_add_private_flag(FU_DEVICE(self), "can-download"); if (attributes & FU_DFU_DEVICE_ATTR_CAN_UPLOAD) fu_device_add_private_flag(FU_DEVICE(self), "can-upload"); if (attributes & FU_DFU_DEVICE_ATTR_MANIFEST_TOL) fu_device_add_private_flag(FU_DEVICE(self), "manifest-tol"); if (attributes & FU_DFU_DEVICE_ATTR_WILL_DETACH) fu_device_add_private_flag(FU_DEVICE(self), "will-detach"); if (attributes & FU_DFU_DEVICE_ATTR_CAN_ACCELERATE) fu_device_add_private_flag(FU_DEVICE(self), "can-accelerate"); return TRUE; } static void fu_dfu_device_guess_state_from_iface(FuDfuDevice *self, FuUsbInterface *iface) { /* some devices use the wrong interface */ if (fu_device_has_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_FORCE_DFU_MODE)) { g_debug("quirking device into DFU mode"); fu_dfu_device_set_state(self, FU_DFU_STATE_DFU_IDLE); return; } /* runtime */ if (fu_usb_interface_get_protocol(iface) == 0x01) { fu_dfu_device_set_state(self, FU_DFU_STATE_APP_IDLE); return; } /* DFU */ if (fu_usb_interface_get_protocol(iface) == 0x02) { fu_dfu_device_set_state(self, FU_DFU_STATE_DFU_IDLE); return; } g_warning("unable to guess initial device state from interface %u", fu_usb_interface_get_protocol(iface)); } static gboolean fu_dfu_device_add_targets(FuDfuDevice *self, GError **error) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(GPtrArray) ifaces = NULL; /* disabled using quirk */ if (priv->force_version == 0x0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "ignoring device as DFU version set to 0x0"); return FALSE; } /* add all DFU-capable targets */ ifaces = fu_usb_device_get_interfaces(FU_USB_DEVICE(self), error); if (ifaces == NULL) return FALSE; g_ptr_array_set_size(priv->targets, 0); for (guint i = 0; i < ifaces->len; i++) { FuDfuTarget *target; g_autoptr(GBytes) iface_data = NULL; g_autoptr(GError) error_local = NULL; FuUsbInterface *iface = g_ptr_array_index(ifaces, i); /* some devices don't use the right class and subclass */ if (!fu_device_has_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_USE_ANY_INTERFACE)) { if (fu_usb_interface_get_class(iface) != FU_USB_CLASS_APPLICATION_SPECIFIC) continue; if (fu_usb_interface_get_subclass(iface) != 0x01) continue; } /* re-parse as a FuUsbDfuDescriptorHdr -- yes DFU FUNCTIONAL is 0x21 like HID... */ iface_data = fu_firmware_get_image_by_idx_bytes(FU_FIRMWARE(iface), FU_USB_DESCRIPTOR_KIND_HID, &error_local); if (iface_data == NULL) { g_warning("failed to parse interface data: %s", error_local->message); fu_device_add_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_CAN_UPLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_CAN_DOWNLOAD); } else { if (!fu_dfu_device_parse_iface_data(self, iface_data, &error_local)) { g_warning("failed to parse interface data for %04x:%04x: %s", fu_device_get_vid(FU_DEVICE(self)), fu_device_get_pid(FU_DEVICE(self)), error_local->message); continue; } } /* fix up the version */ if (priv->force_version != G_MAXUINT16) priv->version = priv->force_version; if (priv->version == FU_DFU_FIRMARE_VERSION_DFU_1_0 || priv->version == FU_DFU_FIRMARE_VERSION_DFU_1_1) { g_info("DFU v1.1"); } else if (priv->version == FU_DFU_FIRMARE_VERSION_ATMEL_AVR) { g_info("AVR-DFU support"); priv->version = FU_DFU_FIRMARE_VERSION_ATMEL_AVR; } else if (priv->version == FU_DFU_FIRMARE_VERSION_DFUSE) { g_info("STM-DFU support"); } else if (priv->version == 0x0101) { g_info("DFU v1.1 assumed"); priv->version = FU_DFU_FIRMARE_VERSION_DFU_1_1; } else { g_warning("DFU version 0x%04x invalid, v1.1 assumed", priv->version); priv->version = FU_DFU_FIRMARE_VERSION_DFU_1_1; } /* set expected protocol */ if (priv->version == FU_DFU_FIRMARE_VERSION_DFUSE) { fu_device_add_protocol(FU_DEVICE(self), "com.st.dfuse"); } else { fu_device_add_protocol(FU_DEVICE(self), "org.usb.dfu"); } /* fix up the transfer size */ if (priv->force_transfer_size != 0x0) { priv->transfer_size = priv->force_transfer_size; g_debug("forcing DFU transfer size 0x%04x bytes", priv->transfer_size); } else if (priv->transfer_size == 0xffff) { priv->transfer_size = 0x0400; g_debug("DFU transfer size unspecified, guessing"); } else if (priv->transfer_size == 0x0) { g_warning("DFU transfer size invalid, using default"); priv->transfer_size = 64; } else { g_debug("using DFU transfer size 0x%04x bytes", priv->transfer_size); } /* create a target of the required type */ switch (priv->version) { case FU_DFU_FIRMARE_VERSION_DFUSE: target = fu_dfu_target_stm_new(); break; case FU_DFU_FIRMARE_VERSION_ATMEL_AVR: target = fu_dfu_target_avr_new(); break; default: target = fu_dfu_target_new(); break; } fu_device_set_proxy(FU_DEVICE(target), FU_DEVICE(self)); fu_dfu_target_set_alt_idx(target, fu_usb_interface_get_index(iface)); fu_dfu_target_set_alt_setting(target, fu_usb_interface_get_alternate(iface)); /* add target */ priv->iface_number = fu_usb_interface_get_number(iface); g_ptr_array_add(priv->targets, target); fu_dfu_device_guess_state_from_iface(self, iface); } /* save for reset */ if (priv->state == FU_DFU_STATE_APP_IDLE || fu_device_has_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_NO_PID_CHANGE)) { priv->runtime_vid = fu_device_get_vid(FU_DEVICE(self)); priv->runtime_pid = fu_device_get_pid(FU_DEVICE(self)); priv->runtime_release = fu_usb_device_get_release(FU_USB_DEVICE(self)); } /* the device has no DFU runtime, so cheat */ if (priv->targets->len == 0 && fu_device_has_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_NO_DFU_RUNTIME)) { g_debug("no DFU runtime, so faking device"); fu_dfu_device_set_state(self, FU_DFU_STATE_APP_IDLE); priv->iface_number = 0xff; priv->runtime_vid = fu_device_get_vid(FU_DEVICE(self)); priv->runtime_pid = fu_device_get_pid(FU_DEVICE(self)); priv->runtime_release = fu_usb_device_get_release(FU_USB_DEVICE(self)); fu_device_add_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_CAN_DOWNLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_CAN_UPLOAD); return TRUE; } /* no targets */ if (priv->targets->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no DFU interfaces"); return FALSE; } /* the device upload is broken */ if (fu_device_has_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_IGNORE_UPLOAD)) fu_device_remove_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_CAN_UPLOAD); return TRUE; } /** * fu_dfu_device_get_timeout: * @device: a #FuDfuDevice * * Gets the device timeout. * * Returns: enumerated timeout in ms **/ guint fu_dfu_device_get_timeout(FuDfuDevice *self) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_DEVICE(self), 0); return priv->timeout_ms; } /** * fu_dfu_device_get_state: * @device: a #FuDfuDevice * * Gets the device state. * * Returns: enumerated state, e.g. %FU_DFU_STATE_DFU_UPLOAD_IDLE **/ FuDfuState fu_dfu_device_get_state(FuDfuDevice *self) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_DEVICE(self), 0); return priv->state; } /** * fu_dfu_device_get_status: * @device: a USB device * * Gets the device status. * * Returns: enumerated status, e.g. %FU_DFU_STATUS_ERR_ADDRESS **/ FuDfuStatus fu_dfu_device_get_status(FuDfuDevice *self) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_DEVICE(self), 0); return priv->status; } static FuDfuTarget * fu_dfu_device_get_target_by_alt_setting(FuDfuDevice *self, guint8 alt_setting, GError **error) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_DEVICE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* find by ID */ for (guint i = 0; i < priv->targets->len; i++) { FuDfuTarget *target = g_ptr_array_index(priv->targets, i); if (fu_dfu_target_get_alt_setting(target) == alt_setting) return g_object_ref(target); } /* failed */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "No target with alt-setting %i", alt_setting); return NULL; } const gchar * fu_dfu_device_get_chip_id(FuDfuDevice *self) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_DEVICE(self), NULL); return priv->chip_id; } static void fu_dfu_device_set_chip_id(FuDfuDevice *self, const gchar *chip_id) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DFU_DEVICE(self)); g_debug("chip ID set to: %s", chip_id); priv->chip_id = g_strdup(chip_id); } static void fu_dfu_device_set_state(FuDfuDevice *self, FuDfuState state) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); if (priv->state == state) return; priv->state = state; /* set bootloader status */ if (state == FU_DFU_STATE_APP_IDLE || state == FU_DFU_STATE_APP_DETACH) { fu_device_remove_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); } else { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); } } static void fu_dfu_device_set_status(FuDfuDevice *self, FuDfuStatus status) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); if (priv->status == status) return; priv->status = status; } gboolean fu_dfu_device_ensure_interface(FuDfuDevice *self, GError **error) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(GError) error_local = NULL; /* already done */ if (priv->claimed_interface) return TRUE; /* nothing set */ if (priv->iface_number == 0xff) return TRUE; /* claim, without detaching kernel driver */ if (!fu_usb_device_claim_interface(FU_USB_DEVICE(self), (gint)priv->iface_number, FU_USB_DEVICE_CLAIM_FLAG_KERNEL_DRIVER, &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot claim interface %i: %s", priv->iface_number, error_local->message); return FALSE; } /* success */ priv->claimed_interface = TRUE; return TRUE; } static gboolean fu_dfu_device_refresh_and_clear(FuDfuDevice *self, GError **error) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); if (!fu_dfu_device_refresh(self, 0, error)) return FALSE; switch (priv->state) { case FU_DFU_STATE_DFU_UPLOAD_IDLE: case FU_DFU_STATE_DFU_DNLOAD_IDLE: case FU_DFU_STATE_DFU_DNLOAD_SYNC: g_debug("aborting transfer %s", fu_dfu_status_to_string(priv->status)); if (!fu_dfu_device_abort(self, error)) return FALSE; break; case FU_DFU_STATE_DFU_ERROR: g_debug("clearing error %s", fu_dfu_status_to_string(priv->status)); if (!fu_dfu_device_clear_status(self, error)) return FALSE; break; default: break; } return TRUE; } /** * fu_dfu_device_refresh: * @self: a #FuDfuDevice * @timeout_ms: a timeout, or 0 to use the default device timeout * @error: (nullable): optional return location for an error * * Refreshes the cached properties on the DFU device. * * Returns: %TRUE for success **/ gboolean fu_dfu_device_refresh(FuDfuDevice *self, guint timeout_ms, GError **error) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); gsize actual_length = 0; guint8 buf[6] = {0x0}; g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_DFU_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* fall back to default */ if (timeout_ms == 0) timeout_ms = priv->timeout_ms; /* the device has no DFU runtime, so cheat */ if (priv->state == FU_DFU_STATE_APP_IDLE && fu_device_has_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_NO_DFU_RUNTIME)) return TRUE; /* ensure interface is claimed */ if (!fu_dfu_device_ensure_interface(self, error)) return FALSE; /* Device that cannot communicate via the USB after the * Manifestation phase indicated this limitation to the * host by clearing bmAttributes bit bitManifestationTolerant. * so we assume the operation was successful */ if (priv->state == FU_DFU_STATE_DFU_MANIFEST && !fu_device_has_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_MANIFEST_TOL)) return TRUE; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_CLASS, FU_USB_RECIPIENT_INTERFACE, FU_DFU_REQUEST_GETSTATUS, 0, priv->iface_number, buf, sizeof(buf), &actual_length, timeout_ms, NULL, /* cancellable */ &error_local)) { /* got STALL */ if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_info("GetStatus not implemented, assuming appIDLE"); fu_dfu_device_set_status(self, FU_DFU_STATUS_OK); fu_dfu_device_set_state(self, FU_DFU_STATE_APP_IDLE); return TRUE; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot get device state: %s", error_local->message); return FALSE; } if (actual_length != 6) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "cannot get device status, invalid size: %04x", (guint)actual_length); return FALSE; } /* some devices use the wrong state value */ if (fu_device_has_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_FORCE_DFU_MODE) && fu_dfu_device_get_state(self) != FU_DFU_STATE_DFU_IDLE) { g_info("quirking device into DFU mode"); fu_dfu_device_set_state(self, FU_DFU_STATE_DFU_IDLE); } else { fu_dfu_device_set_state(self, buf[4]); } /* status or state changed */ fu_dfu_device_set_status(self, buf[0]); fu_dfu_device_set_download_timeout(self, fu_memread_uint24(&buf[1], G_LITTLE_ENDIAN)); g_debug("refreshed status=%s and state=%s (dnload=%u)", fu_dfu_status_to_string(priv->status), fu_dfu_state_to_string(priv->state), priv->dnload_timeout); return TRUE; } static gboolean fu_dfu_device_request_detach(FuDfuDevice *self, FuProgress *progress, GError **error) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); const guint16 timeout_reset_ms = 1000; guint16 ctrl_setup_index = priv->iface_number; g_autoptr(GError) error_local = NULL; if (fu_device_has_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_INDEX_FORCE_DETACH)) FU_BIT_SET(ctrl_setup_index, 8); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_CLASS, FU_USB_RECIPIENT_INTERFACE, FU_DFU_REQUEST_DETACH, timeout_reset_ms, ctrl_setup_index, NULL, 0, NULL, priv->timeout_ms, NULL, /* cancellable */ &error_local)) { /* some devices just reboot and stall the endpoint :/ */ if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED) || // g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_READ) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL)) { g_debug("ignoring while detaching: %s", error_local->message); } else { /* refresh the error code */ fu_dfu_device_error_fixup(self, &error_local); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot detach device: %s", error_local->message); return FALSE; } } return TRUE; } static gboolean fu_dfu_device_reload(FuDevice *device, GError **error) { FuDfuDevice *self = FU_DFU_DEVICE(device); return fu_dfu_device_refresh_and_clear(self, error); } static gboolean fu_dfu_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuDfuDevice *self = FU_DFU_DEVICE(device); FuDfuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* already in DFU mode */ if (!fu_dfu_device_refresh_and_clear(self, error)) return FALSE; if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) return TRUE; /* the device has no DFU runtime, so cheat */ if (priv->state == FU_DFU_STATE_APP_IDLE && fu_device_has_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_NO_DFU_RUNTIME)) return TRUE; /* ensure interface is claimed */ if (!fu_dfu_device_ensure_interface(self, error)) return FALSE; /* inform UI there's going to be a detach:attach */ if (!fu_dfu_device_request_detach(self, progress, error)) return FALSE; /* do a host reset */ if (!fu_device_has_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_WILL_DETACH)) { g_info("doing device reset as host will not self-reset"); if (!fu_dfu_device_reset(self, progress, error)) return FALSE; } /* success */ priv->force_version = G_MAXUINT16; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } /** * fu_dfu_device_abort: * @self: a #FuDfuDevice * @error: (nullable): optional return location for an error * * Aborts any upload or download in progress. * * Returns: %TRUE for success **/ gboolean fu_dfu_device_abort(FuDfuDevice *self, GError **error) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_DFU_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* the device has no DFU runtime, so cheat */ if (priv->state == FU_DFU_STATE_APP_IDLE && fu_device_has_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_NO_DFU_RUNTIME)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported as no DFU runtime"); return FALSE; } /* ensure interface is claimed */ if (!fu_dfu_device_ensure_interface(self, error)) return FALSE; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_CLASS, FU_USB_RECIPIENT_INTERFACE, FU_DFU_REQUEST_ABORT, 0, priv->iface_number, NULL, 0, NULL, priv->timeout_ms, NULL, /* cancellable */ &error_local)) { /* refresh the error code */ fu_dfu_device_error_fixup(self, &error_local); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot abort device: %s", error_local->message); return FALSE; } return TRUE; } static gboolean fu_dfu_device_clear_status(FuDfuDevice *self, GError **error) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(GError) error_local = NULL; /* the device has no DFU runtime, so cheat */ if (priv->state == FU_DFU_STATE_APP_IDLE && fu_device_has_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_NO_DFU_RUNTIME)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported as no DFU runtime"); return FALSE; } /* ensure interface is claimed */ if (!fu_dfu_device_ensure_interface(self, error)) return FALSE; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_CLASS, FU_USB_RECIPIENT_INTERFACE, FU_DFU_REQUEST_CLRSTATUS, 0, priv->iface_number, NULL, 0, NULL, priv->timeout_ms, NULL, /* cancellable */ &error_local)) { /* refresh the error code */ fu_dfu_device_error_fixup(self, &error_local); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot clear status on the device: %s", error_local->message); return FALSE; } return TRUE; } /** * fu_dfu_device_get_interface: * @self: a #FuDfuDevice * * Gets the interface number. **/ guint8 fu_dfu_device_get_interface(FuDfuDevice *self) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_DEVICE(self), 0xff); return priv->iface_number; } /** * fu_dfu_device_open: * @self: a #FuDfuDevice * @error: (nullable): optional return location for an error * * Opens a DFU-capable device. * * Returns: %TRUE for success **/ static gboolean fu_dfu_device_open(FuDevice *device, GError **error) { FuDfuDevice *self = FU_DFU_DEVICE(device); FuDfuDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_DEVICE(device), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* FuUsbDevice->open */ if (!FU_DEVICE_CLASS(fu_dfu_device_parent_class)->open(device, error)) return FALSE; /* the device has no DFU runtime, so cheat */ if (priv->state == FU_DFU_STATE_APP_IDLE && fu_device_has_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_NO_DFU_RUNTIME)) { fu_dfu_device_set_state(self, FU_DFU_STATE_APP_IDLE); priv->status = FU_DFU_STATUS_OK; } /* GD32VF103 encodes the serial number in UTF-8 (rather than UTF-16) * and also uses the first two bytes as the model identifier */ if (fu_device_has_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_GD32)) { const guint8 *buf; gsize bufsz = 0; guint16 langid = FU_USB_LANGID_ENGLISH_UNITED_STATES; guint8 idx = fu_usb_device_get_serial_number_index(FU_USB_DEVICE(device)); g_autofree gchar *chip_id = NULL; g_autofree gchar *serial_str = NULL; g_autoptr(GBytes) serial_blob = NULL; serial_blob = fu_usb_device_get_string_descriptor_bytes(FU_USB_DEVICE(device), idx, langid, error); if (serial_blob == NULL) return FALSE; fu_dump_bytes(G_LOG_DOMAIN, "GD32 serial", serial_blob); buf = g_bytes_get_data(serial_blob, &bufsz); if (bufsz < 2) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "GD32 serial number invalid"); return FALSE; } /* ID is first two bytes */ chip_id = g_strdup_printf("%02x%02x", buf[0], buf[1]); fu_dfu_device_set_chip_id(self, chip_id); /* serial number follows */ serial_str = g_strndup((const gchar *)buf + 2, bufsz - 2); fu_device_set_serial(FU_DEVICE(device), serial_str); } /* set up target ready for use */ for (guint j = 0; j < priv->targets->len; j++) { FuDfuTarget *target = g_ptr_array_index(priv->targets, j); if (!fu_dfu_target_setup(target, error)) return FALSE; } /* success */ return TRUE; } /** * fu_dfu_device_close: * @self: a #FuDfuDevice * @error: (nullable): optional return location for an error * * Closes a DFU device. * * Returns: %TRUE for success **/ static gboolean fu_dfu_device_close(FuDevice *device, GError **error) { FuDfuDevice *self = FU_DFU_DEVICE(device); FuDfuDevicePrivate *priv = GET_PRIVATE(self); /* release interface */ if (priv->claimed_interface) { g_autoptr(GError) error_local = NULL; if (!fu_usb_device_release_interface(FU_USB_DEVICE(device), (gint)priv->iface_number, 0, &error_local)) { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_warning("failed to release interface: %s", error_local->message); } } priv->claimed_interface = FALSE; } /* FuUsbDevice->close */ return FU_DEVICE_CLASS(fu_dfu_device_parent_class)->close(device, error); } static gboolean fu_dfu_device_probe(FuDevice *device, GError **error) { FuDfuDevice *self = FU_DFU_DEVICE(device); /* add all the targets */ if (!fu_dfu_device_add_targets(self, error)) { g_prefix_error(error, "%04x:%04x is not supported: ", fu_device_get_vid(FU_DEVICE(self)), fu_device_get_pid(FU_DEVICE(self))); return FALSE; } /* check capabilities */ if (!fu_device_has_private_flag(device, FU_DFU_DEVICE_FLAG_CAN_DOWNLOAD)) { g_info("%04x:%04x is missing download capability", fu_device_get_vid(FU_DEVICE(self)), fu_device_get_pid(FU_DEVICE(self))); } /* hardware from Jabra literally reboots if you try to retry a failed * write -- there's no way to avoid blocking the daemon like this... */ if (fu_device_has_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ATTACH_EXTRA_RESET)) { g_debug("blocking wait to work around Jabra hardware..."); fu_device_sleep(device, 10000); } /* success */ return TRUE; } gboolean fu_dfu_device_reset(FuDfuDevice *self, FuProgress *progress, GError **error) { g_autoptr(GError) error_local = NULL; g_autoptr(GTimer) timer = g_timer_new(); g_return_val_if_fail(FU_IS_DFU_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_usb_device_reset(FU_USB_DEVICE(self), &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot reset USB device: %s [%i]", error_local->message, error_local->code); return FALSE; } g_debug("reset took %.2lfms", g_timer_elapsed(timer, NULL) * 1000); return TRUE; } static gboolean fu_dfu_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuDfuDevice *self = FU_DFU_DEVICE(device); FuDfuDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(FuDfuTarget) target = NULL; g_return_val_if_fail(FU_IS_DFU_DEVICE(device), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* already in runtime mode */ if (!fu_dfu_device_refresh_and_clear(self, error)) return FALSE; if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) return TRUE; /* handle weirdness */ if (fu_device_has_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_DETACH_FOR_ATTACH)) { if (!fu_dfu_device_request_detach(self, progress, error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } /* handle m-stack DFU bootloaders */ if (!priv->done_upload_or_download && fu_device_has_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_ATTACH_UPLOAD_DOWNLOAD)) { g_autoptr(GBytes) chunk = NULL; g_autoptr(FuDfuTarget) target_zero = NULL; g_debug("doing dummy upload to work around m-stack quirk"); target_zero = fu_dfu_device_get_target_by_alt_setting(self, 0, error); if (target_zero == NULL) return FALSE; chunk = fu_dfu_target_upload_chunk(target_zero, 0, 0, progress, error); if (chunk == NULL) return FALSE; } /* get default target */ target = fu_dfu_device_get_target_by_alt_setting(self, 0, error); if (target == NULL) return FALSE; /* normal DFU mode just needs a bus reset */ if (fu_device_has_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_NO_BUS_RESET_ATTACH) && fu_device_has_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_WILL_DETACH)) { g_info("bus reset is not required; device will reboot to normal"); } else if (!fu_dfu_target_attach(target, progress, error)) { g_prefix_error(error, "failed to attach target: "); return FALSE; } /* there is no USB runtime whatsoever */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_WILL_DISAPPEAR)) return TRUE; /* success */ priv->force_version = 0x0; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static FuFirmware * fu_dfu_device_upload(FuDfuDevice *self, FuProgress *progress, FuDfuTargetTransferFlags flags, GError **error) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); gboolean use_dfuse = FALSE; g_autoptr(FuFirmware) firmware = NULL; /* ensure interface is claimed */ if (!fu_dfu_device_ensure_interface(self, error)) return NULL; /* choose the most appropriate type */ for (guint i = 0; i < priv->targets->len; i++) { FuDfuTarget *target = g_ptr_array_index(priv->targets, i); if (fu_device_get_logical_id(FU_DEVICE(target)) != NULL || i > 0) { use_dfuse = TRUE; break; } } if (use_dfuse) { firmware = fu_dfuse_firmware_new(); g_debug("switching to DefuSe automatically"); } else { firmware = fu_dfu_firmware_new(); } fu_dfu_firmware_set_vid(FU_DFU_FIRMWARE(firmware), priv->runtime_vid); fu_dfu_firmware_set_pid(FU_DFU_FIRMWARE(firmware), priv->runtime_pid); fu_dfu_firmware_set_release(FU_DFU_FIRMWARE(firmware), 0xffff); /* upload from each target */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, priv->targets->len); for (guint i = 0; i < priv->targets->len; i++) { FuDfuTarget *target; /* upload to target and proxy signals */ target = g_ptr_array_index(priv->targets, i); /* ignore some target types */ if (g_strcmp0(fu_device_get_name(FU_DEVICE(target)), "Option Bytes") == 0) { g_debug("ignoring target %s", fu_device_get_name(FU_DEVICE(target))); continue; } if (!fu_dfu_target_upload(target, firmware, fu_progress_get_child(progress), DFU_TARGET_TRANSFER_FLAG_NONE, error)) return NULL; fu_progress_step_done(progress); } /* do not do the dummy upload for quirked devices */ priv->done_upload_or_download = TRUE; /* success */ return g_object_ref(firmware); } static gboolean fu_dfu_device_id_compatible(guint16 id_file, guint16 id_runtime, guint16 id_dev) { /* file doesn't specify */ if (id_file == 0xffff) return TRUE; /* runtime matches */ if (id_runtime != 0xffff && id_file == id_runtime) return TRUE; /* bootloader matches */ if (id_dev != 0xffff && id_file == id_dev) return TRUE; /* nothing */ return FALSE; } static gsize fu_dfu_device_calculate_chunks_size(GPtrArray *chunks) { gsize total = 0; for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); total += fu_chunk_get_data_sz(chk); } return total; } static gboolean fu_dfu_device_download(FuDfuDevice *self, FuFirmware *firmware, FuProgress *progress, FuDfuTargetTransferFlags flags, GError **error) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); gboolean ret; g_autoptr(GPtrArray) images = NULL; guint16 firmware_pid = 0xffff; guint16 firmware_vid = 0xffff; /* ensure interface is claimed */ if (!fu_dfu_device_ensure_interface(self, error)) return FALSE; /* firmware supports footer? */ if (FU_IS_DFU_FIRMWARE(firmware)) { firmware_vid = fu_dfu_firmware_get_vid(FU_DFU_FIRMWARE(firmware)); firmware_pid = fu_dfu_firmware_get_pid(FU_DFU_FIRMWARE(firmware)); } else { flags |= DFU_TARGET_TRANSFER_FLAG_WILDCARD_VID; flags |= DFU_TARGET_TRANSFER_FLAG_WILDCARD_PID; } /* do we allow wildcard VID:PID matches */ if ((flags & DFU_TARGET_TRANSFER_FLAG_WILDCARD_VID) == 0) { if (firmware_vid == 0xffff) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "firmware vendor ID not specified"); return FALSE; } } if ((flags & DFU_TARGET_TRANSFER_FLAG_WILDCARD_PID) == 0) { if (firmware_pid == 0xffff) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "firmware product ID not specified"); return FALSE; } } /* check vendor matches */ if (priv->runtime_vid != 0xffff) { if (!fu_dfu_device_id_compatible(firmware_vid, priv->runtime_vid, fu_device_get_vid(FU_DEVICE(self)))) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "vendor ID incorrect, expected 0x%04x " "got 0x%04x and 0x%04x\n", firmware_vid, priv->runtime_vid, fu_device_get_vid(FU_DEVICE(self))); return FALSE; } } /* check product matches */ if (priv->runtime_pid != 0xffff) { if (!fu_dfu_device_id_compatible(firmware_pid, priv->runtime_pid, fu_device_get_pid(FU_DEVICE(self)))) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "product ID incorrect, expected 0x%04x " "got 0x%04x and 0x%04x", firmware_pid, priv->runtime_pid, fu_device_get_pid(FU_DEVICE(self))); return FALSE; } } /* download each target */ images = fu_firmware_get_images(firmware); if (images->len == 0) g_ptr_array_add(images, g_object_ref(firmware)); fu_progress_set_id(progress, G_STRLOC); for (guint i = 0; i < images->len; i++) { FuFirmware *image = g_ptr_array_index(images, i); g_autoptr(GPtrArray) chunks = fu_firmware_get_chunks(image, error); if (chunks == NULL) return FALSE; fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, fu_dfu_device_calculate_chunks_size(chunks), NULL); } for (guint i = 0; i < images->len; i++) { FuFirmware *image = g_ptr_array_index(images, i); FuDfuTargetTransferFlags flags_local = DFU_TARGET_TRANSFER_FLAG_NONE; guint8 alt; g_autoptr(FuDfuTarget) target_tmp = NULL; alt = fu_firmware_get_idx(image); target_tmp = fu_dfu_device_get_target_by_alt_setting(self, alt, error); if (target_tmp == NULL) return FALSE; if (!fu_dfu_target_setup(target_tmp, error)) return FALSE; g_debug("downloading to target: %s", fu_device_get_logical_id(FU_DEVICE(target_tmp))); /* download onto target */ if (flags & DFU_TARGET_TRANSFER_FLAG_VERIFY) flags_local = DFU_TARGET_TRANSFER_FLAG_VERIFY; if (!FU_IS_DFU_FIRMWARE(firmware) || fu_dfu_firmware_get_version(FU_DFU_FIRMWARE(firmware)) == 0x0) flags_local |= DFU_TARGET_TRANSFER_FLAG_ADDR_HEURISTIC; ret = fu_dfu_target_download(target_tmp, image, fu_progress_get_child(progress), flags_local, error); if (!ret) return FALSE; fu_progress_step_done(progress); } /* do not do the dummy upload for quirked devices */ priv->done_upload_or_download = TRUE; /* success */ return TRUE; } void fu_dfu_device_error_fixup(FuDfuDevice *self, GError **error) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); /* sad panda */ if (error == NULL) return; /* not the right error to query */ if (!g_error_matches(*error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) return; /* get the status */ if (!fu_dfu_device_refresh(self, 0, NULL)) return; /* not in an error state */ if (priv->state != FU_DFU_STATE_DFU_ERROR) return; /* prefix the error */ switch (priv->status) { case FU_DFU_STATUS_OK: /* ignore */ break; case FU_DFU_STATUS_ERR_VENDOR: g_prefix_error(error, "read protection is active: "); break; default: g_prefix_error(error, "[%s,%s]: ", fu_dfu_state_to_string(priv->state), fu_dfu_status_to_string(priv->status)); break; } } static GBytes * fu_dfu_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuDfuDevice *self = FU_DFU_DEVICE(device); g_autoptr(FuFirmware) firmware = NULL; /* get data from hardware */ g_debug("uploading from device->host"); if (!fu_dfu_device_refresh_and_clear(self, error)) return NULL; firmware = fu_dfu_device_upload(self, progress, DFU_TARGET_TRANSFER_FLAG_NONE, error); if (firmware == NULL) return NULL; /* get the checksum */ return fu_firmware_write(firmware, error); } static FuFirmware * fu_dfu_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { return fu_firmware_new_from_gtypes(stream, 0x0, flags, error, FU_TYPE_IHEX_FIRMWARE, FU_TYPE_DFUSE_FIRMWARE, FU_TYPE_DFU_FIRMWARE, FU_TYPE_FIRMWARE, G_TYPE_INVALID); } static gboolean fu_dfu_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDfuDevice *self = FU_DFU_DEVICE(device); FuDfuTargetTransferFlags transfer_flags = DFU_TARGET_TRANSFER_FLAG_VERIFY; /* open it */ if (!fu_dfu_device_refresh_and_clear(self, error)) return FALSE; if (flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_VID_PID) { transfer_flags |= DFU_TARGET_TRANSFER_FLAG_WILDCARD_VID; transfer_flags |= DFU_TARGET_TRANSFER_FLAG_WILDCARD_PID; } /* hit hardware */ return fu_dfu_device_download(self, firmware, progress, transfer_flags, error); } static gboolean fu_dfu_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuDfuDevice *self = FU_DFU_DEVICE(device); FuDfuDevicePrivate *priv = GET_PRIVATE(self); guint64 tmp = 0; if (g_strcmp0(key, FU_QUIRKS_DFU_FORCE_VERSION) == 0) { if (!fu_strtoull(value, &tmp, 0x0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->force_version = tmp; return TRUE; } if (g_strcmp0(key, "DfuForceTimeout") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->timeout_ms = tmp; return TRUE; } if (g_strcmp0(key, "DfuForceTransferSize") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->force_transfer_size = tmp; return TRUE; } if (g_strcmp0(key, "DfuAltName") == 0) { fu_dfu_device_set_chip_id(self, value); return TRUE; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_dfu_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 88, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 10, "reload"); } static void fu_dfu_device_dispose(GObject *object) { FuDfuDevice *self = FU_DFU_DEVICE(object); FuDfuDevicePrivate *priv = GET_PRIVATE(self); if (priv->targets != NULL) g_ptr_array_set_size(priv->targets, 0); G_OBJECT_CLASS(fu_dfu_device_parent_class)->dispose(object); } static void fu_dfu_device_finalize(GObject *object) { FuDfuDevice *self = FU_DFU_DEVICE(object); FuDfuDevicePrivate *priv = GET_PRIVATE(self); g_free(priv->chip_id); g_ptr_array_unref(priv->targets); G_OBJECT_CLASS(fu_dfu_device_parent_class)->finalize(object); } static void fu_dfu_device_class_init(FuDfuDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->set_quirk_kv = fu_dfu_device_set_quirk_kv; device_class->to_string = fu_dfu_device_to_string; device_class->dump_firmware = fu_dfu_device_dump_firmware; device_class->write_firmware = fu_dfu_device_write_firmware; device_class->prepare_firmware = fu_dfu_device_prepare_firmware; device_class->attach = fu_dfu_device_attach; device_class->detach = fu_dfu_device_detach; device_class->reload = fu_dfu_device_reload; device_class->open = fu_dfu_device_open; device_class->close = fu_dfu_device_close; device_class->probe = fu_dfu_device_probe; device_class->set_progress = fu_dfu_device_set_progress; object_class->dispose = fu_dfu_device_dispose; object_class->finalize = fu_dfu_device_finalize; } static void fu_dfu_device_init(FuDfuDevice *self) { FuDfuDevicePrivate *priv = GET_PRIVATE(self); priv->iface_number = 0xff; priv->runtime_pid = 0xffff; priv->runtime_vid = 0xffff; priv->runtime_release = 0xffff; priv->state = FU_DFU_STATE_APP_IDLE; priv->status = FU_DFU_STATUS_OK; priv->targets = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); priv->timeout_ms = 1500; priv->transfer_size = 64; priv->force_version = G_MAXUINT16; priv->dnload_timeout = DFU_DEVICE_DNLOAD_TIMEOUT_DEFAULT; fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_COUNTERPART_GUIDS); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_SIGNED); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_FLAGS); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_INSTANCE_ID_REV); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_CAN_DOWNLOAD); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_CAN_UPLOAD); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_MANIFEST_TOL); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_WILL_DETACH); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_CAN_ACCELERATE); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_ATTACH_UPLOAD_DOWNLOAD); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_FORCE_DFU_MODE); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_IGNORE_POLLTIMEOUT); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_IGNORE_RUNTIME); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_IGNORE_UPLOAD); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_NO_DFU_RUNTIME); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_NO_GET_STATUS_UPLOAD); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_NO_PID_CHANGE); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_USE_ANY_INTERFACE); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_USE_ATMEL_AVR); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_USE_PROTOCOL_ZERO); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_LEGACY_PROTOCOL); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_DETACH_FOR_ATTACH); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_ABSENT_SECTOR_SIZE); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_MANIFEST_POLL); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_NO_BUS_RESET_ATTACH); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_GD32); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_ALLOW_ZERO_POLLTIMEOUT); fu_device_register_private_flag(FU_DEVICE(self), FU_DFU_DEVICE_FLAG_INDEX_FORCE_DETACH); } fwupd-2.0.10/plugins/dfu/fu-dfu-device.h000066400000000000000000000023661501337203100177540ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-dfu-common.h" #include "fu-dfu-struct.h" #include "fu-dfu-target.h" #define FU_TYPE_DFU_DEVICE (fu_dfu_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuDfuDevice, fu_dfu_device, FU, DFU_DEVICE, FuUsbDevice) struct _FuDfuDeviceClass { FuUsbDeviceClass parent_class; }; const gchar * fu_dfu_device_get_chip_id(FuDfuDevice *self); gboolean fu_dfu_device_reset(FuDfuDevice *self, FuProgress *progress, GError **error); gboolean fu_dfu_device_refresh(FuDfuDevice *self, guint timeout_ms, GError **error); gboolean fu_dfu_device_abort(FuDfuDevice *self, GError **error); guint8 fu_dfu_device_get_interface(FuDfuDevice *self); FuDfuState fu_dfu_device_get_state(FuDfuDevice *self); FuDfuStatus fu_dfu_device_get_status(FuDfuDevice *self); guint16 fu_dfu_device_get_transfer_size(FuDfuDevice *self); guint16 fu_dfu_device_get_version(FuDfuDevice *self); guint fu_dfu_device_get_timeout(FuDfuDevice *self); void fu_dfu_device_error_fixup(FuDfuDevice *self, GError **error); guint fu_dfu_device_get_download_timeout(FuDfuDevice *self); gboolean fu_dfu_device_ensure_interface(FuDfuDevice *self, GError **error); fwupd-2.0.10/plugins/dfu/fu-dfu-plugin.c000066400000000000000000000015571501337203100200070ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-dfu-device.h" #include "fu-dfu-plugin.h" struct _FuDfuPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuDfuPlugin, fu_dfu_plugin, FU_TYPE_PLUGIN) static void fu_dfu_plugin_init(FuDfuPlugin *self) { } static void fu_dfu_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "DfuAltName"); fu_context_add_quirk_key(ctx, "DfuForceTimeout"); fu_context_add_quirk_key(ctx, "DfuForceVersion"); fu_plugin_add_device_gtype(plugin, FU_TYPE_DFU_DEVICE); } static void fu_dfu_plugin_class_init(FuDfuPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_dfu_plugin_constructed; } fwupd-2.0.10/plugins/dfu/fu-dfu-plugin.h000066400000000000000000000003431501337203100200040ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuDfuPlugin, fu_dfu_plugin, FU, DFU_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/dfu/fu-dfu-sector.c000066400000000000000000000057021501337203100200040ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ /** * This object represents an sector of memory at a specific address on the * device itself. * * This allows relocatable data segments to be stored in different * locations on the device itself. * * You can think of these objects as flash segments on devices, where a * complete block can be erased and then written to. */ #include "config.h" #include #include #include "fu-dfu-common.h" #include "fu-dfu-sector.h" typedef struct { guint32 address; guint32 size; guint32 size_left; guint16 zone; guint16 number; FuDfuSectorCap cap; } FuDfuSectorPrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuDfuSector, fu_dfu_sector, G_TYPE_OBJECT) #define GET_PRIVATE(o) (fu_dfu_sector_get_instance_private(o)) static void fu_dfu_sector_class_init(FuDfuSectorClass *klass) { } static void fu_dfu_sector_init(FuDfuSector *self) { } FuDfuSector * fu_dfu_sector_new(guint32 address, guint32 size, guint32 size_left, guint16 zone, guint16 number, FuDfuSectorCap cap) { FuDfuSectorPrivate *priv; FuDfuSector *self; self = g_object_new(FU_TYPE_DFU_SECTOR, NULL); priv = GET_PRIVATE(self); priv->address = address; priv->size = size; priv->size_left = size_left; priv->zone = zone; priv->number = number; priv->cap = cap; return self; } guint32 fu_dfu_sector_get_address(FuDfuSector *self) { FuDfuSectorPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_SECTOR(self), 0x00); return priv->address; } guint32 fu_dfu_sector_get_size(FuDfuSector *self) { FuDfuSectorPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_SECTOR(self), 0x00); return priv->size; } guint16 fu_dfu_sector_get_zone(FuDfuSector *self) { FuDfuSectorPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_SECTOR(self), 0x00); return priv->zone; } /* use this number to check if the segment is the 'same' as the last * written or read sector */ guint32 fu_dfu_sector_get_id(FuDfuSector *self) { FuDfuSectorPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_SECTOR(self), 0x00); return (((guint32)priv->zone) << 16) | priv->number; } gboolean fu_dfu_sector_has_cap(FuDfuSector *self, FuDfuSectorCap cap) { FuDfuSectorPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_SECTOR(self), FALSE); return (priv->cap & cap) > 0; } gchar * fu_dfu_sector_to_string(FuDfuSector *self) { FuDfuSectorPrivate *priv = GET_PRIVATE(self); GString *str; g_autofree gchar *caps_str = NULL; g_return_val_if_fail(FU_IS_DFU_SECTOR(self), NULL); str = g_string_new(""); caps_str = fu_dfu_sector_cap_to_string(priv->cap); g_string_append_printf(str, "Zone:%i, Sec#:%i, Addr:0x%08x, " "Size:0x%04x, Caps:0x%01x [%s]", priv->zone, priv->number, priv->address, priv->size, priv->cap, caps_str); return g_string_free(str, FALSE); } fwupd-2.0.10/plugins/dfu/fu-dfu-sector.h000066400000000000000000000016111501337203100200040ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-dfu-struct.h" #define FU_TYPE_DFU_SECTOR (fu_dfu_sector_get_type()) G_DECLARE_DERIVABLE_TYPE(FuDfuSector, fu_dfu_sector, FU, DFU_SECTOR, GObject) struct _FuDfuSectorClass { GObjectClass parent_class; }; FuDfuSector * fu_dfu_sector_new(guint32 address, guint32 size, guint32 size_left, guint16 zone, guint16 number, FuDfuSectorCap cap); guint32 fu_dfu_sector_get_id(FuDfuSector *self); guint32 fu_dfu_sector_get_address(FuDfuSector *self); guint32 fu_dfu_sector_get_size(FuDfuSector *self); guint32 fu_dfu_sector_get_size_left(FuDfuSector *self); guint16 fu_dfu_sector_get_zone(FuDfuSector *self); gboolean fu_dfu_sector_has_cap(FuDfuSector *self, FuDfuSectorCap cap); gchar * fu_dfu_sector_to_string(FuDfuSector *self); fwupd-2.0.10/plugins/dfu/fu-dfu-self-test.c000066400000000000000000000125441501337203100204150ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-context-private.h" #include "fu-dfu-device.h" #include "fu-dfu-sector.h" #include "fu-dfu-target-private.h" static gboolean fu_test_compare_lines(const gchar *txt1, const gchar *txt2, GError **error) { g_autofree gchar *output = NULL; if (g_strcmp0(txt1, txt2) == 0) return TRUE; if (g_pattern_match_simple(txt2, txt1)) return TRUE; if (!g_file_set_contents("/tmp/a", txt1, -1, error)) return FALSE; if (!g_file_set_contents("/tmp/b", txt2, -1, error)) return FALSE; if (!g_spawn_command_line_sync("diff -urNp /tmp/b /tmp/a", &output, NULL, NULL, error)) return FALSE; g_set_error_literal(error, 1, 0, output); return FALSE; } static gchar * fu_dfu_target_sectors_to_string(FuDfuTarget *target) { GPtrArray *sectors; GString *str; str = g_string_new(""); sectors = fu_dfu_target_get_sectors(target); for (guint i = 0; i < sectors->len; i++) { FuDfuSector *sector = g_ptr_array_index(sectors, i); g_autofree gchar *tmp = fu_dfu_sector_to_string(sector); g_string_append_printf(str, "%s\n", tmp); } if (str->len > 0) g_string_truncate(str, str->len - 1); return g_string_free(str, FALSE); } static void fu_dfu_target_dfuse_func(void) { gboolean ret; gchar *tmp; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuDfuDevice) device = g_object_new(FU_TYPE_DFU_DEVICE, "context", ctx, NULL); g_autoptr(FuDfuTarget) target = NULL; g_autoptr(GError) error = NULL; /* NULL */ target = g_object_new(FU_TYPE_DFU_TARGET, NULL); fu_device_set_proxy(FU_DEVICE(target), FU_DEVICE(device)); ret = fu_dfu_target_parse_sectors(target, NULL, &error); g_assert_no_error(error); g_assert_true(ret); tmp = fu_dfu_target_sectors_to_string(target); g_assert_cmpstr(tmp, ==, ""); g_free(tmp); /* no addresses */ ret = fu_dfu_target_parse_sectors(target, "@Flash3", &error); g_assert_no_error(error); g_assert_true(ret); tmp = fu_dfu_target_sectors_to_string(target); g_assert_cmpstr(tmp, ==, ""); g_free(tmp); /* one sector, no space */ ret = fu_dfu_target_parse_sectors(target, "@Internal Flash /0x08000000/2*001Ka", &error); g_assert_no_error(error); g_assert_true(ret); tmp = fu_dfu_target_sectors_to_string(target); ret = fu_test_compare_lines( tmp, "Zone:0, Sec#:0, Addr:0x08000000, Size:0x0400, Caps:0x1 [readable]\n" "Zone:0, Sec#:0, Addr:0x08000400, Size:0x0400, Caps:0x1 [readable]", &error); g_assert_no_error(error); g_assert_true(ret); g_free(tmp); /* multiple sectors */ ret = fu_dfu_target_parse_sectors(target, "@Flash1 /0x08000000/2*001Ka,4*001Kg", &error); g_assert_no_error(error); g_assert_true(ret); tmp = fu_dfu_target_sectors_to_string(target); ret = fu_test_compare_lines( tmp, "Zone:0, Sec#:0, Addr:0x08000000, Size:0x0400, Caps:0x1 [readable]\n" "Zone:0, Sec#:0, Addr:0x08000400, Size:0x0400, Caps:0x1 [readable]\n" "Zone:0, Sec#:1, Addr:0x08000800, Size:0x0400, Caps:0x7 [readable,writeable,erasable]\n" "Zone:0, Sec#:1, Addr:0x08000c00, Size:0x0400, Caps:0x7 [readable,writeable,erasable]\n" "Zone:0, Sec#:1, Addr:0x08001000, Size:0x0400, Caps:0x7 [readable,writeable,erasable]\n" "Zone:0, Sec#:1, Addr:0x08001400, Size:0x0400, Caps:0x7 [readable,writeable,erasable]", &error); g_assert_no_error(error); g_assert_true(ret); g_free(tmp); /* non-contiguous */ ret = fu_dfu_target_parse_sectors(target, "@Flash2 /0xF000/4*100Ba/0xE000/3*8Kg/0x80000/2*24Kg", &error); g_assert_no_error(error); g_assert_true(ret); tmp = fu_dfu_target_sectors_to_string(target); ret = fu_test_compare_lines( tmp, "Zone:0, Sec#:0, Addr:0x0000f000, Size:0x0064, Caps:0x1 [readable]\n" "Zone:0, Sec#:0, Addr:0x0000f064, Size:0x0064, Caps:0x1 [readable]\n" "Zone:0, Sec#:0, Addr:0x0000f0c8, Size:0x0064, Caps:0x1 [readable]\n" "Zone:0, Sec#:0, Addr:0x0000f12c, Size:0x0064, Caps:0x1 [readable]\n" "Zone:1, Sec#:0, Addr:0x0000e000, Size:0x2000, Caps:0x7 [readable,writeable,erasable]\n" "Zone:1, Sec#:0, Addr:0x00010000, Size:0x2000, Caps:0x7 [readable,writeable,erasable]\n" "Zone:1, Sec#:0, Addr:0x00012000, Size:0x2000, Caps:0x7 [readable,writeable,erasable]\n" "Zone:2, Sec#:0, Addr:0x00080000, Size:0x6000, Caps:0x7 [readable,writeable,erasable]\n" "Zone:2, Sec#:0, Addr:0x00086000, Size:0x6000, Caps:0x7 [readable,writeable,erasable]", &error); g_assert_no_error(error); g_assert_true(ret); g_free(tmp); /* invalid */ ret = fu_dfu_target_parse_sectors(target, "Flash", NULL); g_assert_true(ret); ret = fu_dfu_target_parse_sectors(target, "@Internal Flash /0x08000000", NULL); g_assert_false(ret); ret = fu_dfu_target_parse_sectors(target, "@Internal Flash /0x08000000/12*001a", NULL); g_assert_false(ret); } int main(int argc, char **argv) { g_autofree gchar *testdatadir = NULL; (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); (void)g_setenv("G_MESSAGES_DEBUG", "all", FALSE); /* environment */ testdatadir = g_test_build_filename(G_TEST_DIST, "tests", NULL); (void)g_setenv("FWUPD_SYSFSFWATTRIBDIR", testdatadir, TRUE); /* tests go here */ g_test_add_func("/dfu/target{DfuSe}", fu_dfu_target_dfuse_func); return g_test_run(); } fwupd-2.0.10/plugins/dfu/fu-dfu-target-avr.c000066400000000000000000000672361501337203100205730ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fu-dfu-common.h" #include "fu-dfu-device.h" #include "fu-dfu-sector.h" #include "fu-dfu-target-avr.h" #include "fu-dfu-target-private.h" /* waive-pre-commit */ /** * FU_QUIRKS_DFU_AVR_ALT_NAME: * @key: the AVR chip ID, e.g. `0x58200204` * @value: the UM0424 sector description, e.g. `@Flash/0x2000/1*248Kg` * * Assigns a sector description for the chip ID. This is required so fwupd can * program the user firmware avoiding the bootloader and for checking the total * chunk size. * * The chip ID can be found from a datasheet or using `sudo fwupdtool --plugins dfu get-devices` * when the hardware is connected and in bootloader mode. * * Since: 1.0.1 */ #define FU_QUIRKS_DFU_AVR_ALT_NAME "DfuAltName" typedef struct { guint32 device_id; } FuDfuTargetAvrPrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuDfuTargetAvr, fu_dfu_target_avr, FU_TYPE_DFU_TARGET) #define GET_PRIVATE(o) (fu_dfu_target_avr_get_instance_private(o)) /* ATMEL AVR version of DFU: * http://www.atmel.com/Images/doc7618.pdf */ #define DFU_AVR_CMD_PROG_START 0x01 #define DFU_AVR_CMD_DISPLAY_DATA 0x03 #define DFU_AVR_CMD_WRITE_COMMAND 0x04 #define DFU_AVR_CMD_READ_COMMAND 0x05 #define DFU_AVR_CMD_CHANGE_BASE_ADDR 0x06 /* Atmel AVR32 version of DFU: * http://www.atmel.com/images/doc32131.pdf */ #define DFU_AVR32_GROUP_SELECT 0x06 /** SELECT */ #define DFU_AVR32_CMD_SELECT_MEMORY 0x03 #define DFU_AVR32_MEMORY_UNIT 0x00 #define DFU_AVR32_MEMORY_PAGE 0x01 #define DFU_AVR32_MEMORY_UNIT_FLASH 0x00 #define DFU_AVR32_MEMORY_UNIT_EEPROM 0x01 #define DFU_AVR32_MEMORY_UNIT_SECURITY 0x02 #define DFU_AVR32_MEMORY_UNIT_CONFIGURATION 0x03 #define DFU_AVR32_MEMORY_UNIT_BOOTLOADER 0x04 #define DFU_AVR32_MEMORY_UNIT_SIGNATURE 0x05 #define DFU_AVR32_MEMORY_UNIT_USER 0x06 #define DFU_AVR32_GROUP_DOWNLOAD 0x01 /** DOWNLOAD */ #define DFU_AVR32_CMD_PROGRAM_START 0x00 #define DFU_AVR32_GROUP_UPLOAD 0x03 /** UPLOAD */ #define DFU_AVR32_CMD_READ_MEMORY 0x00 #define DFU_AVR32_CMD_BLANK_CHECK 0x01 #define DFU_AVR32_GROUP_EXEC 0x04 /** EXEC */ #define DFU_AVR32_CMD_ERASE 0x00 #define DFU_AVR32_ERASE_EVERYTHING 0xff #define DFU_AVR32_CMD_START_APPLI 0x03 #define DFU_AVR32_START_APPLI_RESET 0x00 #define DFU_AVR32_START_APPLI_NO_RESET 0x01 #define ATMEL_64KB_PAGE 0x10000 #define ATMEL_MAX_TRANSFER_SIZE 0x0400 #define ATMEL_AVR_CONTROL_BLOCK_SIZE 32 #define ATMEL_AVR32_CONTROL_BLOCK_SIZE 64 #define ATMEL_MANUFACTURER_CODE1 0x58 #define ATMEL_MANUFACTURER_CODE2 0x1e static gboolean fu_dfu_target_avr_mass_erase(FuDfuTarget *target, FuProgress *progress, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); /* format buffer */ fu_byte_array_append_uint8(buf, DFU_AVR32_GROUP_EXEC); fu_byte_array_append_uint8(buf, DFU_AVR32_CMD_ERASE); fu_byte_array_append_uint8(buf, 0xFF); if (!fu_dfu_target_download_chunk(target, 0, buf, 5000, progress, error)) { g_prefix_error(error, "cannot mass-erase: "); return FALSE; } return TRUE; } static gboolean fu_dfu_target_avr_attach(FuDfuTarget *target, FuProgress *progress, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GByteArray) buf_empty = g_byte_array_new(); g_autoptr(GError) error_local = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 50, "download-chunk"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 50, "download-zero"); /* format buffer */ fu_byte_array_append_uint8(buf, DFU_AVR32_GROUP_EXEC); fu_byte_array_append_uint8(buf, DFU_AVR32_CMD_START_APPLI); fu_byte_array_append_uint8(buf, DFU_AVR32_START_APPLI_RESET); if (!fu_dfu_target_download_chunk(target, 0, buf, 0, /* timeout default */ fu_progress_get_child(progress), &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_debug("ignoring as device rebooting: %s", error_local->message); fu_progress_finished(progress); return TRUE; } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "cannot start application reset attach: "); return FALSE; } fu_progress_step_done(progress); /* do zero-sized download to initiate the reset */ if (!fu_dfu_target_download_chunk(target, 0, buf_empty, 0, /* timeout default */ fu_progress_get_child(progress), &error_local)) { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "cannot initiate reset for attach: "); return FALSE; } g_debug("ignoring as device rebooting: %s", error_local->message); } fu_progress_step_done(progress); /* success */ return TRUE; } /** * fu_dfu_target_avr_select_memory_unit: * @target: a #FuDfuTarget * @memory_unit: a unit, e.g. %DFU_AVR32_MEMORY_UNIT_FLASH * @error: (nullable): optional return location for an error * * Selects the memory unit for the device. * * Returns: %TRUE for success **/ static gboolean fu_dfu_target_avr_select_memory_unit(FuDfuTarget *target, guint8 memory_unit, FuProgress *progress, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); /* check legacy protocol quirk */ if (fu_device_has_private_flag(fu_device_get_proxy(FU_DEVICE(target)), FU_DFU_DEVICE_FLAG_LEGACY_PROTOCOL)) { g_debug("ignoring select memory unit as legacy protocol"); return TRUE; } /* format buffer */ fu_byte_array_append_uint8(buf, DFU_AVR32_GROUP_SELECT); fu_byte_array_append_uint8(buf, DFU_AVR32_CMD_SELECT_MEMORY); fu_byte_array_append_uint8(buf, DFU_AVR32_MEMORY_UNIT); fu_byte_array_append_uint8(buf, memory_unit); g_debug("selecting memory unit 0x%02x", (guint)memory_unit); if (!fu_dfu_target_download_chunk(target, 0, buf, 0, progress, error)) { g_prefix_error(error, "cannot select memory unit: "); return FALSE; } return TRUE; } /** * fu_dfu_target_avr_select_memory_page: * @target: a #FuDfuTarget * @memory_page: an address * @error: (nullable): optional return location for an error * * Selects the memory page for the AVR device. * * Returns: %TRUE for success **/ static gboolean fu_dfu_target_avr_select_memory_page(FuDfuTarget *target, guint16 memory_page, FuProgress *progress, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); /* check page not too large for protocol */ if (memory_page > 0xff) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "cannot select memory page:0x%02x " "with FLIP protocol version 1", memory_page); return FALSE; } /* format buffer */ fu_byte_array_append_uint8(buf, DFU_AVR_CMD_CHANGE_BASE_ADDR); fu_byte_array_append_uint8(buf, 0x03); fu_byte_array_append_uint8(buf, 0x00); fu_byte_array_append_uint8(buf, memory_page & 0xFF); g_debug("selecting memory page 0x%01x", (guint)memory_page); if (!fu_dfu_target_download_chunk(target, 0, buf, 0, progress, error)) { g_prefix_error(error, "cannot select memory page: "); return FALSE; } return TRUE; } /** * fu_dfu_target_avr32_select_memory_page: * @target: a #FuDfuTarget * @memory_page: an address * @error: (nullable): optional return location for an error * * Selects the memory page for the AVR32 device. * * Returns: %TRUE for success **/ static gboolean fu_dfu_target_avr32_select_memory_page(FuDfuTarget *target, guint16 memory_page, FuProgress *progress, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); /* format buffer */ fu_byte_array_append_uint8(buf, DFU_AVR32_GROUP_SELECT); fu_byte_array_append_uint8(buf, DFU_AVR32_CMD_SELECT_MEMORY); fu_byte_array_append_uint8(buf, DFU_AVR32_MEMORY_PAGE); fu_byte_array_append_uint16(buf, memory_page, G_BIG_ENDIAN); g_debug("selecting memory page 0x%02x", (guint)memory_page); if (!fu_dfu_target_download_chunk(target, 0, buf, 0, progress, error)) { g_prefix_error(error, "cannot select memory page: "); return FALSE; } return TRUE; } /** * fu_dfu_target_avr_read_memory * @target: a #FuDfuTarget * @addr_start: an address * @addr_end: an address * @error: (nullable): optional return location for an error * * Reads flash data from the device. * * Returns: %TRUE for success **/ static gboolean fu_dfu_target_avr_read_memory(FuDfuTarget *target, guint16 addr_start, guint16 addr_end, FuProgress *progress, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); /* format buffer */ fu_byte_array_append_uint8(buf, DFU_AVR32_GROUP_UPLOAD); fu_byte_array_append_uint8(buf, DFU_AVR32_CMD_READ_MEMORY); fu_byte_array_append_uint16(buf, addr_start, G_BIG_ENDIAN); fu_byte_array_append_uint16(buf, addr_end, G_BIG_ENDIAN); g_debug("reading memory from 0x%04x to 0x%04x", (guint)addr_start, (guint)addr_end); if (!fu_dfu_target_download_chunk(target, 0, buf, 0, progress, error)) { g_prefix_error(error, "cannot read memory 0x%04x to 0x%04x: ", (guint)addr_start, (guint)addr_end); return FALSE; } return TRUE; } /** * fu_dfu_target_avr_read_command: * @target: a #FuDfuTarget * @memory_unit: a unit, e.g. %DFU_AVR32_MEMORY_UNIT_FLASH * @error: (nullable): optional return location for an error * * Performs a read operation on the device. * * Returns: %TRUE for success **/ static gboolean fu_dfu_target_avr_read_command(FuDfuTarget *target, guint8 page, guint8 addr, FuProgress *progress, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); /* format buffer */ fu_byte_array_append_uint8(buf, DFU_AVR_CMD_READ_COMMAND); fu_byte_array_append_uint8(buf, page); fu_byte_array_append_uint8(buf, addr); g_debug("read command page:0x%02x addr:0x%02x", (guint)page, (guint)addr); if (!fu_dfu_target_download_chunk(target, 0, buf, 0, progress, error)) { g_prefix_error(error, "cannot read command page: "); return FALSE; } return TRUE; } /** * fu_dfu_target_avr32_get_chip_signature: * @target: a #FuDfuTarget * @error: (nullable): optional return location for an error * * Gets the chip signature for the AVR32 device. * * Returns: a 4-byte %GBytes object for success, else %NULL **/ static GBytes * fu_dfu_target_avr32_get_chip_signature(FuDfuTarget *target, FuProgress *progress, GError **error) { g_autoptr(GBytes) buf = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 25, "mem-unit"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 25, "mem-page"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 25, "read-memory"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 25, "upload"); /* select unit, and request 4 bytes */ if (!fu_dfu_target_avr_select_memory_unit(target, DFU_AVR32_MEMORY_UNIT_SIGNATURE, fu_progress_get_child(progress), error)) return NULL; fu_progress_step_done(progress); if (!fu_dfu_target_avr32_select_memory_page(target, 0x00, fu_progress_get_child(progress), error)) return NULL; fu_progress_step_done(progress); if (!fu_dfu_target_avr_read_memory(target, 0x00, 0x03, fu_progress_get_child(progress), error)) return NULL; fu_progress_step_done(progress); /* get data back */ buf = fu_dfu_target_upload_chunk(target, 0x00, 0, fu_progress_get_child(progress), error); if (buf == NULL) return NULL; fu_progress_step_done(progress); /* success */ return g_steal_pointer(&buf); } static GBytes * fu_dfu_target_avr_get_chip_signature_for_addr(FuDfuTarget *target, guint8 page, guint addr, FuProgress *progress, GError **error) { g_autoptr(GBytes) buf = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 10, "req"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 90, "res"); /* request a single byte */ if (!fu_dfu_target_avr_read_command(target, page, addr, fu_progress_get_child(progress), error)) return NULL; fu_progress_step_done(progress); /* get data back */ buf = fu_dfu_target_upload_chunk(target, 0x00, 0x01, progress, error); if (buf == NULL) return NULL; if (g_bytes_get_size(buf) != 1) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "cannot read signature memory page:0x%02x " "addr:0x%02x, got 0x%02x bytes", (guint)page, (guint)addr, (guint)g_bytes_get_size(buf)); return NULL; } fu_progress_step_done(progress); /* success */ return g_steal_pointer(&buf); } /** * fu_dfu_target_avr_get_chip_signature: * @target: a #FuDfuTarget * @error: (nullable): optional return location for an error * * Gets the chip signature for the AVR device. * * Returns: a 4-byte %GBytes object for success, else %NULL **/ static GBytes * fu_dfu_target_avr_get_chip_signature(FuDfuTarget *target, FuProgress *progress, GError **error) { struct { guint8 page; guint addr; } signature_locations[] = {{0x01, 0x30}, {0x01, 0x31}, {0x01, 0x60}, {0x01, 0x61}, {0xff, 0xff}}; g_autoptr(GPtrArray) chunks = NULL; /* we have to request this one byte at a time */ chunks = g_ptr_array_new_with_free_func((GDestroyNotify)g_bytes_unref); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, G_N_ELEMENTS(signature_locations)); for (guint i = 0; signature_locations[i].page != 0xff; i++) { g_autoptr(GBytes) buf = NULL; buf = fu_dfu_target_avr_get_chip_signature_for_addr(target, signature_locations[i].page, signature_locations[i].addr, fu_progress_get_child(progress), error); if (buf == NULL) return NULL; g_ptr_array_add(chunks, g_steal_pointer(&buf)); fu_progress_step_done(progress); } return fu_dfu_utils_bytes_join_array(chunks); } static gboolean fu_dfu_target_avr_setup(FuDfuTarget *target, GError **error) { FuDfuDevice *device; FuDfuTargetAvr *self = FU_DFU_TARGET_AVR(target); FuDfuTargetAvrPrivate *priv = GET_PRIVATE(self); const gchar *chip_id; const guint8 *buf; gsize sz; g_autofree gchar *chip_id_guid = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GBytes) chunk_sig = NULL; /* already done */ if (priv->device_id > 0x0) return TRUE; /* different methods for AVR vs. AVR32 */ if (fu_device_has_private_flag(fu_device_get_proxy(FU_DEVICE(target)), FU_DFU_DEVICE_FLAG_LEGACY_PROTOCOL)) { chunk_sig = fu_dfu_target_avr_get_chip_signature(target, progress, error); if (chunk_sig == NULL) return FALSE; } else { chunk_sig = fu_dfu_target_avr32_get_chip_signature(target, progress, error); if (chunk_sig == NULL) { g_prefix_error(error, "failed to get chip signature: "); return FALSE; } } /* get data back */ buf = g_bytes_get_data(chunk_sig, &sz); fu_dump_bytes(G_LOG_DOMAIN, "AVR:CID", chunk_sig); if (!fu_memread_uint32_safe(buf, sz, 0x0, &priv->device_id, G_BIG_ENDIAN, error)) { g_prefix_error(error, "cannot read config memory: "); return FALSE; } if (buf[0] == ATMEL_MANUFACTURER_CODE1) { chip_id_guid = g_strdup_printf("0x%08x", (guint)priv->device_id); } else if (buf[0] == ATMEL_MANUFACTURER_CODE2) { chip_id_guid = g_strdup_printf("0x%06x", (guint)priv->device_id >> 8); } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "cannot read config vendor, got 0x%08x, " "expected 0x%02x or 0x%02x", (guint)priv->device_id, (guint)ATMEL_MANUFACTURER_CODE1, (guint)ATMEL_MANUFACTURER_CODE2); return FALSE; } /* set the alt-name using the chip ID via a quirk */ device = FU_DFU_DEVICE(fu_device_get_proxy(FU_DEVICE(target))); fu_device_add_instance_str(FU_DEVICE(device), "CID", chip_id_guid); if (!fu_device_build_instance_id(FU_DEVICE(device), error, "DFU_AVR", "CID", NULL)) return FALSE; chip_id = fu_dfu_device_get_chip_id(device); if (chip_id == NULL) { fu_device_remove_private_flag(fu_device_get_proxy(FU_DEVICE(target)), FU_DFU_DEVICE_FLAG_CAN_DOWNLOAD); fu_device_remove_private_flag(fu_device_get_proxy(FU_DEVICE(target)), FU_DFU_DEVICE_FLAG_CAN_UPLOAD); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "ChipID GUID %s is not supported", chip_id_guid); return FALSE; } fu_device_set_logical_id(FU_DEVICE(target), chip_id); return TRUE; } static gboolean fu_dfu_target_avr_download_element_chunks(FuDfuTarget *target, GPtrArray *chunks, guint16 *page_last, gsize header_sz, FuProgress *progress, GError **error) { const guint8 footer[] = {0x00, 0x00, 0x00, 0x00, /* CRC */ 16, /* len */ 'D', 'F', 'U', /* signature */ 0x01, 0x10, /* version */ 0xff, 0xff, /* vendor ID */ 0xff, 0xff, /* product ID */ 0xff, 0xff}; /* release */ /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GBytes) chunk_tmp = fu_chunk_get_bytes(chk); /* select page if required */ if (fu_chunk_get_page(chk) != *page_last) { g_autoptr(FuProgress) progress_tmp = fu_progress_new(G_STRLOC); if (fu_device_has_private_flag(fu_device_get_proxy(FU_DEVICE(target)), FU_DFU_DEVICE_FLAG_LEGACY_PROTOCOL)) { if (!fu_dfu_target_avr_select_memory_page(target, fu_chunk_get_page(chk), progress_tmp, error)) return FALSE; } else { if (!fu_dfu_target_avr32_select_memory_page(target, fu_chunk_get_page(chk), progress_tmp, error)) return FALSE; } *page_last = fu_chunk_get_page(chk); } /* create chunk with header and footer */ fu_byte_array_append_uint8(buf, DFU_AVR32_GROUP_DOWNLOAD); fu_byte_array_append_uint8(buf, DFU_AVR32_CMD_PROGRAM_START); fu_byte_array_append_uint16(buf, fu_chunk_get_address(chk), G_BIG_ENDIAN); fu_byte_array_append_uint16(buf, fu_chunk_get_address(chk) + fu_chunk_get_data_sz(chk) - 1, G_BIG_ENDIAN); fu_byte_array_set_size(buf, header_sz, 0x0); fu_byte_array_append_bytes(buf, chunk_tmp); g_byte_array_append(buf, footer, sizeof(footer)); /* download data */ g_debug("sending 0x%x bytes to the hardware", buf->len); if (!fu_dfu_target_download_chunk(target, i, buf, 0, /* timeout default */ fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to write AVR chunk %u: ", i); return FALSE; } /* update UI */ fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_dfu_target_avr_download_element(FuDfuTarget *target, FuChunk *chk, FuProgress *progress, FuDfuTargetTransferFlags flags, GError **error) { FuDfuSector *sector; const guint8 *data; gsize header_sz = ATMEL_AVR32_CONTROL_BLOCK_SIZE; guint16 page_last = G_MAXUINT16; guint32 address; guint32 address_offset = 0x0; g_autoptr(GBytes) blob = NULL; g_autoptr(GPtrArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 10, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 90, NULL); /* select a memory and erase everything */ if (!fu_dfu_target_avr_select_memory_unit(target, fu_dfu_target_get_alt_setting(target), progress, error)) return FALSE; if (!fu_dfu_target_avr_mass_erase(target, progress, error)) return FALSE; fu_progress_step_done(progress); /* verify the element isn't larger than the target size */ blob = fu_chunk_get_bytes(chk); sector = fu_dfu_target_get_sector_default(target); if (sector == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no sector defined for target"); return FALSE; } address = fu_chunk_get_address(chk) & ~0x80000000; if (address < fu_dfu_sector_get_address(sector)) { address_offset = fu_dfu_sector_get_address(sector) - address; g_warning("firmware element starts at 0x%x but sector " "starts at 0x%x, so offsetting by 0x%x (bootloader?)", (guint)address, (guint)fu_dfu_sector_get_address(sector), (guint)address_offset); } if (g_bytes_get_size(blob) + address_offset > fu_dfu_sector_get_size(sector)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "element was larger than sector size: 0x%x", (guint)fu_dfu_sector_get_size(sector)); return FALSE; } /* the original AVR protocol uses a half-size control block */ if (fu_device_has_private_flag(fu_device_get_proxy(FU_DEVICE(target)), FU_DFU_DEVICE_FLAG_LEGACY_PROTOCOL)) { header_sz = ATMEL_AVR_CONTROL_BLOCK_SIZE; } /* chunk up the memory space into pages */ data = g_bytes_get_data(blob, NULL); chunks = fu_chunk_array_new(data + address_offset, g_bytes_get_size(blob) - address_offset, fu_dfu_sector_get_address(sector), ATMEL_64KB_PAGE, ATMEL_MAX_TRANSFER_SIZE); if (!fu_dfu_target_avr_download_element_chunks(target, chunks, &page_last, header_sz, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* done */ return TRUE; } static GBytes * fu_dfu_target_avr_upload_element_chunk(FuDfuTarget *target, FuChunk *chk, FuProgress *progress, GError **error) { g_autoptr(GBytes) blob = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 70, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 30, NULL); /* prepare to read */ if (!fu_dfu_target_avr_read_memory(target, fu_chunk_get_address(chk), fu_chunk_get_address(chk) + fu_chunk_get_data_sz(chk) - 1, fu_progress_get_child(progress), error)) return NULL; fu_progress_step_done(progress); /* upload data */ g_debug("requesting %i bytes from the hardware for chunk 0x%x", ATMEL_MAX_TRANSFER_SIZE, fu_chunk_get_idx(chk)); blob = fu_dfu_target_upload_chunk(target, fu_chunk_get_idx(chk), ATMEL_MAX_TRANSFER_SIZE, fu_progress_get_child(progress), error); if (blob == NULL) return NULL; fu_progress_step_done(progress); /* success */ return g_steal_pointer(&blob); } static FuChunk * fu_dfu_target_avr_upload_element_chunks(FuDfuTarget *target, guint32 address, gsize expected_size, GPtrArray *chunks, FuProgress *progress, GError **error) { guint16 page_last = G_MAXUINT16; guint chunk_valid = G_MAXUINT; g_autoptr(FuChunk) chk2 = NULL; g_autoptr(GBytes) contents = NULL; g_autoptr(GBytes) contents_truncated = NULL; g_autoptr(GPtrArray) blobs = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); /* process each chunk */ blobs = g_ptr_array_new_with_free_func((GDestroyNotify)g_bytes_unref); for (guint i = 0; i < chunks->len; i++) { GBytes *blob_tmp = NULL; FuChunk *chk = g_ptr_array_index(chunks, i); /* select page if required */ if (fu_chunk_get_page(chk) != page_last) { g_autoptr(FuProgress) progress_tmp = fu_progress_new(G_STRLOC); if (fu_device_has_private_flag(fu_device_get_proxy(FU_DEVICE(target)), FU_DFU_DEVICE_FLAG_LEGACY_PROTOCOL)) { if (!fu_dfu_target_avr_select_memory_page(target, fu_chunk_get_page(chk), progress_tmp, error)) return NULL; } else { if (!fu_dfu_target_avr32_select_memory_page(target, fu_chunk_get_page(chk), progress_tmp, error)) return NULL; } page_last = fu_chunk_get_page(chk); } blob_tmp = fu_dfu_target_avr_upload_element_chunk(target, chk, fu_progress_get_child(progress), error); if (blob_tmp == NULL) return NULL; g_ptr_array_add(blobs, blob_tmp); /* this page has valid data */ if (!fu_bytes_is_empty(blob_tmp)) { g_debug("chunk %u has data (page %" G_GUINT32_FORMAT ")", i, fu_chunk_get_page(chk)); chunk_valid = i; } else { g_debug("chunk %u is empty", i); } /* update UI */ fu_progress_step_done(progress); } /* truncate the image if any sectors are empty, i.e. all 0xff */ if (chunk_valid == G_MAXUINT) { g_debug("all %u chunks are empty", blobs->len); g_ptr_array_set_size(chunks, 0); } else if (blobs->len != chunk_valid + 1) { g_debug("truncating chunks from %u to %u", blobs->len, chunk_valid + 1); g_ptr_array_set_size(blobs, chunk_valid + 1); } /* create element of required size */ contents = fu_dfu_utils_bytes_join_array(blobs); if (expected_size > 0 && g_bytes_get_size(contents) > expected_size) { contents_truncated = g_bytes_new_from_bytes(contents, 0x0, expected_size); } else { contents_truncated = g_bytes_ref(contents); } chk2 = fu_chunk_bytes_new(contents_truncated); fu_chunk_set_address(chk2, address | 0x80000000); /* flash */ return g_steal_pointer(&chk2); } static FuChunk * fu_dfu_target_avr_upload_element(FuDfuTarget *target, guint32 address, gsize expected_size, gsize maximum_size, FuProgress *progress, GError **error) { FuDfuSector *sector; g_autoptr(FuChunk) chk2 = NULL; g_autoptr(GPtrArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 5, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 95, NULL); /* select unit */ if (!fu_dfu_target_avr_select_memory_unit(target, fu_dfu_target_get_alt_setting(target), fu_progress_get_child(progress), error)) return NULL; fu_progress_step_done(progress); /* verify the element isn't lower than the flash area */ sector = fu_dfu_target_get_sector_default(target); if (sector == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no sector defined for target"); return NULL; } if (address < fu_dfu_sector_get_address(sector)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "cannot read from below sector start"); return NULL; } /* the flash starts at 0x80000000, but is indexed from zero */ address &= ~0x80000000; /* chunk up the memory space into pages */ chunks = fu_chunk_array_new(NULL, maximum_size, address, ATMEL_64KB_PAGE, ATMEL_MAX_TRANSFER_SIZE); chk2 = fu_dfu_target_avr_upload_element_chunks(target, address, expected_size, chunks, fu_progress_get_child(progress), error); if (chk2 == NULL) return NULL; fu_progress_step_done(progress); /* success */ return g_steal_pointer(&chk2); } static void fu_dfu_target_avr_init(FuDfuTargetAvr *self) { } static void fu_dfu_target_avr_class_init(FuDfuTargetAvrClass *klass) { FuDfuTargetClass *target_class = FU_DFU_TARGET_CLASS(klass); target_class->setup = fu_dfu_target_avr_setup; target_class->attach = fu_dfu_target_avr_attach; target_class->mass_erase = fu_dfu_target_avr_mass_erase; target_class->upload_element = fu_dfu_target_avr_upload_element; target_class->download_element = fu_dfu_target_avr_download_element; } FuDfuTarget * fu_dfu_target_avr_new(void) { FuDfuTarget *target; target = g_object_new(FU_TYPE_DFU_TARGET_AVR, NULL); return target; } fwupd-2.0.10/plugins/dfu/fu-dfu-target-avr.h000066400000000000000000000006431501337203100205650ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-dfu-target.h" #define FU_TYPE_DFU_TARGET_AVR (fu_dfu_target_avr_get_type()) G_DECLARE_DERIVABLE_TYPE(FuDfuTargetAvr, fu_dfu_target_avr, FU, DFU_TARGET_AVR, FuDfuTarget) struct _FuDfuTargetAvrClass { FuDfuTargetClass parent_class; }; FuDfuTarget * fu_dfu_target_avr_new(void); fwupd-2.0.10/plugins/dfu/fu-dfu-target-private.h000066400000000000000000000023001501337203100214370ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-dfu-device.h" #include "fu-dfu-sector.h" #include "fu-dfu-target.h" FuDfuTarget * fu_dfu_target_new(void); GBytes * fu_dfu_target_upload_chunk(FuDfuTarget *self, guint16 index, gsize buf_sz, FuProgress *progress, GError **error); gboolean fu_dfu_target_download_chunk(FuDfuTarget *self, guint16 index, GByteArray *buf, guint timeout_ms, FuProgress *progress, GError **error); gboolean fu_dfu_target_attach(FuDfuTarget *self, FuProgress *progress, GError **error); void fu_dfu_target_set_alt_idx(FuDfuTarget *self, guint8 alt_idx); void fu_dfu_target_set_alt_setting(FuDfuTarget *self, guint8 alt_setting); /* for the other implementations */ void fu_dfu_target_set_alt_name(FuDfuTarget *self, const gchar *alt_name); gboolean fu_dfu_target_check_status(FuDfuTarget *self, GError **error); FuDfuSector * fu_dfu_target_get_sector_for_addr(FuDfuTarget *self, guint32 addr); /* export this just for the self tests */ gboolean fu_dfu_target_parse_sectors(FuDfuTarget *self, const gchar *alt_name, GError **error); fwupd-2.0.10/plugins/dfu/fu-dfu-target-stm.c000066400000000000000000000344141501337203100205760ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include #include "fu-dfu-common.h" #include "fu-dfu-sector.h" #include "fu-dfu-target-private.h" /* waive-pre-commit */ #include "fu-dfu-target-stm.h" G_DEFINE_TYPE(FuDfuTargetStm, fu_dfu_target_stm, FU_TYPE_DFU_TARGET) /* STMicroelectronics STM32 version of DFU: * www.st.com/resource/en/application_note/cd00264379.pdf */ #define DFU_STM_CMD_GET_COMMAND 0x00 #define DFU_STM_CMD_SET_ADDRESS_POINTER 0x21 #define DFU_STM_CMD_ERASE 0x41 #define DFU_STM_CMD_READ_UNPROTECT 0x92 static gboolean fu_dfu_target_stm_attach(FuDfuTarget *target, FuProgress *progress, GError **error) { /* downloading empty payload will cause a dfu to leave, * the returned status will be dfuMANIFEST and expect the device to disconnect */ g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GError) error_local = NULL; if (!fu_dfu_target_download_chunk(target, 2, buf, 0, progress, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_debug("ignoring: %s", error_local->message); return TRUE; } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to attach: "); return FALSE; } return TRUE; } static gboolean fu_dfu_target_stm_mass_erase(FuDfuTarget *target, FuProgress *progress, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); /* format buffer */ fu_byte_array_append_uint8(buf, DFU_STM_CMD_ERASE); if (!fu_dfu_target_download_chunk(target, 0, buf, 35000, progress, error)) { g_prefix_error(error, "cannot mass-erase: "); return FALSE; } /* 2nd check required to get error code */ return fu_dfu_target_check_status(target, error); } /* sets the address used for the next download or upload request */ static gboolean fu_dfu_target_stm_set_address(FuDfuTarget *target, guint32 address, FuProgress *progress, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); /* format buffer */ fu_byte_array_append_uint8(buf, DFU_STM_CMD_SET_ADDRESS_POINTER); fu_byte_array_append_uint32(buf, address, G_LITTLE_ENDIAN); if (!fu_dfu_target_download_chunk(target, 0, buf, 0, progress, error)) { g_prefix_error(error, "cannot set address 0x%x: ", address); return FALSE; } /* 2nd check required to get error code */ g_debug("doing actual check status"); return fu_dfu_target_check_status(target, error); } static FuChunk * fu_dfu_target_stm_upload_element(FuDfuTarget *target, guint32 address, gsize expected_size, gsize maximum_size, FuProgress *progress, GError **error) { FuDfuDevice *device = FU_DFU_DEVICE(fu_device_get_proxy(FU_DEVICE(target))); FuDfuSector *sector; FuChunk *chk = NULL; GBytes *chunk_tmp; guint32 offset = address; guint percentage_size = expected_size > 0 ? expected_size : maximum_size; gsize total_size = 0; guint16 transfer_size = fu_dfu_device_get_transfer_size(device); g_autoptr(GBytes) contents = NULL; g_autoptr(GBytes) contents_truncated = NULL; g_autoptr(GPtrArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 40, "set-addr"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "abort"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 58, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, NULL); /* for DfuSe devices we need to handle the address manually */ sector = fu_dfu_target_get_sector_for_addr(target, offset); if (sector == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no memory sector at 0x%04x", (guint)offset); return NULL; } g_debug("using sector %u for read of %x", fu_dfu_sector_get_id(sector), offset); if (!fu_dfu_sector_has_cap(sector, FU_DFU_SECTOR_CAP_READABLE)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "memory sector at 0x%04x is not readable", (guint)offset); return NULL; } /* manually set the sector address */ g_debug("setting DfuSe address to 0x%04x", (guint)offset); if (!fu_dfu_target_stm_set_address(target, offset, fu_progress_get_child(progress), error)) return NULL; fu_progress_step_done(progress); /* abort back to IDLE */ if (!fu_dfu_device_abort(device, error)) return NULL; fu_progress_step_done(progress); /* get all the chunks from the hardware */ chunks = g_ptr_array_new_with_free_func((GDestroyNotify)g_bytes_unref); for (guint16 idx = 0; idx < G_MAXUINT16; idx++) { guint32 chunk_size; g_autoptr(FuProgress) progress_tmp = fu_progress_new(G_STRLOC); /* read chunk of data -- ST uses wBlockNum=0 for DfuSe commands * and wBlockNum=1 is reserved */ chunk_tmp = fu_dfu_target_upload_chunk(target, idx + 2, 0, /* device transfer size */ progress_tmp, /* we don't know! */ error); if (chunk_tmp == NULL) return NULL; /* add to array */ chunk_size = (guint32)g_bytes_get_size(chunk_tmp); g_debug("got #%04x chunk @0x%x of size %" G_GUINT32_FORMAT, idx, offset, chunk_size); g_ptr_array_add(chunks, chunk_tmp); total_size += chunk_size; offset += chunk_size; /* update UI */ if (chunk_size > 0) { fu_progress_set_percentage_full(fu_progress_get_child(progress), MIN(total_size, percentage_size), percentage_size); } /* detect short write as EOF */ if (chunk_size < transfer_size) break; /* more data than we needed */ if (maximum_size > 0 && total_size > maximum_size) break; } fu_progress_step_done(progress); /* abort back to IDLE */ if (!fu_dfu_device_abort(device, error)) return NULL; fu_progress_step_done(progress); /* check final size */ if (expected_size > 0) { if (total_size < expected_size) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid size, got %" G_GSIZE_FORMAT ", " "expected %" G_GSIZE_FORMAT, total_size, expected_size); return NULL; } } /* create new image */ contents = fu_dfu_utils_bytes_join_array(chunks); if (expected_size > 0) { contents_truncated = fu_bytes_new_offset(contents, 0, expected_size, error); if (contents_truncated == NULL) return NULL; } else { contents_truncated = g_bytes_ref(contents); } chk = fu_chunk_bytes_new(contents_truncated); fu_chunk_set_address(chk, address); return chk; } /** * fu_dfu_target_stm_erase_address: * @target: a #FuDfuTarget * @address: memory address * @error: (nullable): optional return location for an error * * Erases a memory sector at a given address. * * Returns: %TRUE for success **/ static gboolean fu_dfu_target_stm_erase_address(FuDfuTarget *target, guint32 address, FuProgress *progress, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); /* format buffer */ fu_byte_array_append_uint8(buf, DFU_STM_CMD_ERASE); fu_byte_array_append_uint32(buf, address, G_LITTLE_ENDIAN); if (!fu_dfu_target_download_chunk(target, 0, buf, 0, progress, error)) { g_prefix_error(error, "cannot erase address 0x%x: ", address); return FALSE; } /* 2nd check required to get error code */ g_debug("doing actual check status"); return fu_dfu_target_check_status(target, error); } static gboolean fu_dfu_target_stm_download_element1(FuDfuTarget *target, FuChunkArray *chunks, GPtrArray *sectors_array, FuProgress *progress, GError **error) { g_autoptr(GHashTable) sectors_hash = g_hash_table_new(g_direct_hash, g_direct_equal); gsize address = 0; gsize transfer_size = 0; /* start offset */ if (fu_chunk_array_length(chunks) > 0) { g_autoptr(FuChunk) chk = fu_chunk_array_index(chunks, 0, error); if (chk == NULL) return FALSE; address = fu_chunk_get_address(chk); transfer_size = fu_chunk_get_data_sz(chk); } /* no progress */ for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { guint32 offset_dev = i * transfer_size; /* for DfuSe devices we need to handle the erase and setting * the sector address manually */ while (offset_dev < (i + 1) * transfer_size) { FuDfuSector *sector = fu_dfu_target_get_sector_for_addr(target, address + offset_dev); if (sector == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no memory sector at 0x%04x", (guint)address + offset_dev); return FALSE; } if (!fu_dfu_sector_has_cap(sector, FU_DFU_SECTOR_CAP_WRITEABLE)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "memory sector at 0x%04x is not writable", (guint)address + offset_dev); return FALSE; } /* if it's erasable and not yet blanked */ if (fu_dfu_sector_has_cap(sector, FU_DFU_SECTOR_CAP_ERASABLE) && g_hash_table_lookup(sectors_hash, sector) == NULL) { g_hash_table_insert(sectors_hash, sector, GINT_TO_POINTER(1)); g_ptr_array_add(sectors_array, sector); g_debug("marking sector 0x%04x-%04x to be erased", fu_dfu_sector_get_address(sector), fu_dfu_sector_get_address(sector) + fu_dfu_sector_get_size(sector)); } offset_dev += fu_dfu_sector_get_size(sector); } } /* success */ return TRUE; } static gboolean fu_dfu_target_stm_download_element2(FuDfuTarget *target, GPtrArray *sectors_array, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, sectors_array->len); for (guint i = 0; i < sectors_array->len; i++) { FuDfuSector *sector = g_ptr_array_index(sectors_array, i); g_debug("erasing sector at 0x%04x", fu_dfu_sector_get_address(sector)); if (!fu_dfu_target_stm_erase_address(target, fu_dfu_sector_get_address(sector), fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_dfu_target_stm_download_element3(FuDfuTarget *target, FuChunkArray *chunks, GPtrArray *sectors_array, FuProgress *progress, GError **error) { guint zone_last = G_MAXUINT; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { FuDfuSector *sector; g_autoptr(FuChunk) chk_tmp = NULL; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GBytes) bytes_tmp = NULL; /* prepare chunk */ chk_tmp = fu_chunk_array_index(chunks, i, error); if (chk_tmp == NULL) return FALSE; /* for DfuSe devices we need to set the address manually */ sector = fu_dfu_target_get_sector_for_addr(target, fu_chunk_get_address(chk_tmp)); if (sector == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no valid sector for 0x%x", (guint)fu_chunk_get_address(chk_tmp)); return FALSE; } /* manually set the sector address */ if (fu_dfu_sector_get_zone(sector) != zone_last) { g_autoptr(FuProgress) progress_tmp = fu_progress_new(G_STRLOC); g_debug("setting address to 0x%04x", (guint)fu_chunk_get_address(chk_tmp)); if (!fu_dfu_target_stm_set_address(target, (guint32)fu_chunk_get_address(chk_tmp), progress_tmp, error)) return FALSE; zone_last = fu_dfu_sector_get_zone(sector); } /* we have to write one final zero-sized chunk for EOF */ bytes_tmp = fu_chunk_get_bytes(chk_tmp); g_debug("writing sector at 0x%04x (0x%" G_GSIZE_FORMAT ")", (guint)fu_chunk_get_address(chk_tmp), g_bytes_get_size(bytes_tmp)); /* ST uses wBlockNum=0 for DfuSe commands and wBlockNum=1 is reserved */ fu_byte_array_append_bytes(buf, bytes_tmp); if (!fu_dfu_target_download_chunk(target, (i + 2), buf, 0, /* timeout default */ fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to write STM chunk %u: ", i); return FALSE; } /* getting the status moves the state machine to DNLOAD-IDLE */ if (!fu_dfu_target_check_status(target, error)) return FALSE; /* update UI */ fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_dfu_target_stm_download_element(FuDfuTarget *target, FuChunk *chk, FuProgress *progress, FuDfuTargetTransferFlags flags, GError **error) { FuDfuDevice *device = FU_DFU_DEVICE(fu_device_get_proxy(FU_DEVICE(target))); g_autoptr(GBytes) bytes = NULL; g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(GPtrArray) sectors_array = g_ptr_array_new(); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 49, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 50, NULL); /* 1st pass: work out which sectors need erasing */ bytes = fu_chunk_get_bytes(chk); chunks = fu_chunk_array_new_from_bytes(bytes, fu_chunk_get_address(chk), FU_CHUNK_PAGESZ_NONE, fu_dfu_device_get_transfer_size(device)); if (!fu_dfu_target_stm_download_element1(target, chunks, sectors_array, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* 2nd pass: actually erase sectors */ if (!fu_dfu_target_stm_download_element2(target, sectors_array, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* 3rd pass: write data */ if (!fu_dfu_target_stm_download_element3(target, chunks, sectors_array, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static void fu_dfu_target_stm_init(FuDfuTargetStm *self) { } static void fu_dfu_target_stm_class_init(FuDfuTargetStmClass *klass) { FuDfuTargetClass *target_class = FU_DFU_TARGET_CLASS(klass); target_class->attach = fu_dfu_target_stm_attach; target_class->mass_erase = fu_dfu_target_stm_mass_erase; target_class->upload_element = fu_dfu_target_stm_upload_element; target_class->download_element = fu_dfu_target_stm_download_element; } FuDfuTarget * fu_dfu_target_stm_new(void) { FuDfuTarget *target; target = g_object_new(FU_TYPE_DFU_TARGET_STM, NULL); return target; } fwupd-2.0.10/plugins/dfu/fu-dfu-target-stm.h000066400000000000000000000006431501337203100206000ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-dfu-target.h" #define FU_TYPE_DFU_TARGET_STM (fu_dfu_target_stm_get_type()) G_DECLARE_DERIVABLE_TYPE(FuDfuTargetStm, fu_dfu_target_stm, FU, DFU_TARGET_STM, FuDfuTarget) struct _FuDfuTargetStmClass { FuDfuTargetClass parent_class; }; FuDfuTarget * fu_dfu_target_stm_new(void); fwupd-2.0.10/plugins/dfu/fu-dfu-target.c000066400000000000000000001047751501337203100200050ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ /** * FuDfuTarget: * * This object allows uploading and downloading an image onto a * specific DFU-capable target. * * You only need to use this in preference to #FuDfuDevice if you only * want to update one target on the device. Most users will want to * update all the targets on the device at the same time. * * See also: [class@FuDfuDevice], [class@FuFirmware] */ #include "config.h" #include #include #include "fu-dfu-common.h" #include "fu-dfu-device.h" #include "fu-dfu-sector.h" #include "fu-dfu-struct.h" #include "fu-dfu-target-private.h" /* waive-pre-commit */ #define DFU_TARGET_MANIFEST_MAX_POLLING_TRIES 200 typedef struct { gboolean done_setup; guint8 alt_setting; guint8 alt_idx; GPtrArray *sectors; /* of FuDfuSector */ } FuDfuTargetPrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuDfuTarget, fu_dfu_target, FU_TYPE_DEVICE) #define GET_PRIVATE(o) (fu_dfu_target_get_instance_private(o)) static void fu_dfu_target_init(FuDfuTarget *self) { FuDfuTargetPrivate *priv = GET_PRIVATE(self); priv->sectors = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); } static void fu_dfu_target_finalize(GObject *object) { FuDfuTarget *self = FU_DFU_TARGET(object); FuDfuTargetPrivate *priv = GET_PRIVATE(self); g_ptr_array_unref(priv->sectors); G_OBJECT_CLASS(fu_dfu_target_parent_class)->finalize(object); } static void fu_dfu_target_to_string(FuDevice *device, guint idt, GString *str) { FuDfuTarget *self = FU_DFU_TARGET(device); FuDfuTargetPrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append_hex(str, idt, "AltSetting", priv->alt_setting); fwupd_codec_string_append_hex(str, idt, "AltIdx", priv->alt_idx); for (guint i = 0; i < priv->sectors->len; i++) { FuDfuSector *sector = g_ptr_array_index(priv->sectors, i); g_autofree gchar *tmp1 = g_strdup_printf("Idx%02x", i); g_autofree gchar *tmp2 = fu_dfu_sector_to_string(sector); fwupd_codec_string_append(str, idt + 1, tmp1, tmp2); } } FuDfuSector * fu_dfu_target_get_sector_for_addr(FuDfuTarget *self, guint32 addr) { FuDfuTargetPrivate *priv = GET_PRIVATE(self); for (guint i = 0; i < priv->sectors->len; i++) { FuDfuSector *sector = g_ptr_array_index(priv->sectors, i); if (addr < fu_dfu_sector_get_address(sector)) continue; if (addr > fu_dfu_sector_get_address(sector) + fu_dfu_sector_get_size(sector)) continue; return sector; } return NULL; } static gboolean fu_dfu_target_parse_sector(FuDfuTarget *self, const gchar *dfuse_sector_id, guint32 *addr, guint16 zone, guint16 number, GError **error) { FuDfuTargetPrivate *priv = GET_PRIVATE(self); FuDfuSectorCap cap = FU_DFU_SECTOR_CAP_NONE; gchar *tmp; guint32 addr_offset = 0; guint64 nr_sectors; guint64 sector_size; /* parse # of sectors */ nr_sectors = g_ascii_strtoull(dfuse_sector_id, &tmp, 10); /* nocheck:blocked */ if (nr_sectors > 999) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Invalid number of sectors: %s", dfuse_sector_id); return FALSE; } /* check this is the delimiter */ if (tmp[0] != '*') { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Invalid sector ID: %s", dfuse_sector_id); return FALSE; } /* parse sector size */ sector_size = g_ascii_strtoull(tmp + 1, &tmp, 10); /* nocheck:blocked */ if (sector_size > 999) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Invalid sector size: %s", dfuse_sector_id); return FALSE; } /* handle weirdness */ if (fu_device_has_private_flag(fu_device_get_proxy(FU_DEVICE(self)), FU_DFU_DEVICE_FLAG_ABSENT_SECTOR_SIZE)) { if (tmp[1] == '\0') { tmp[1] = tmp[0]; tmp[0] = 'B'; } } /* get multiplier */ switch (tmp[0]) { case 'B': /* byte */ case ' ': /* byte, ST reference bootloader :/ */ break; case 'K': /* Kilo */ sector_size *= 0x400; break; case 'M': /* Mega */ sector_size *= 0x100000; break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Invalid sector multiplier: %s", tmp); return FALSE; } /* get sector type */ switch (tmp[1]) { case 'a': cap = FU_DFU_SECTOR_CAP_READABLE; break; case 'b': cap = FU_DFU_SECTOR_CAP_ERASABLE; break; case 'c': cap = FU_DFU_SECTOR_CAP_READABLE | FU_DFU_SECTOR_CAP_ERASABLE; break; case 'd': cap = FU_DFU_SECTOR_CAP_WRITEABLE; break; case 'e': cap = FU_DFU_SECTOR_CAP_READABLE | FU_DFU_SECTOR_CAP_WRITEABLE; break; case 'f': cap = FU_DFU_SECTOR_CAP_ERASABLE | FU_DFU_SECTOR_CAP_WRITEABLE; break; case 'g': cap = FU_DFU_SECTOR_CAP_READABLE | FU_DFU_SECTOR_CAP_ERASABLE | FU_DFU_SECTOR_CAP_WRITEABLE; break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Invalid sector type: %s", tmp); return FALSE; } /* add all the sectors */ for (guint i = 0; i < nr_sectors; i++) { FuDfuSector *sector; sector = fu_dfu_sector_new(*addr + addr_offset, (guint32)sector_size, (guint32)((nr_sectors * sector_size) - addr_offset), zone, number, cap); g_ptr_array_add(priv->sectors, sector); addr_offset += fu_dfu_sector_get_size(sector); } /* update for next sector */ *addr += addr_offset; return TRUE; } gboolean fu_dfu_target_parse_sectors(FuDfuTarget *self, const gchar *alt_name, GError **error) { FuDfuTargetPrivate *priv = GET_PRIVATE(self); g_auto(GStrv) zones = NULL; /* not set */ if (alt_name == NULL) return TRUE; /* From the Neo Freerunner */ if (g_str_has_prefix(alt_name, "RAM 0x")) { FuDfuSector *sector; guint64 addr_tmp = 0; if (!fu_strtoull(alt_name + 6, &addr_tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_16, error)) return FALSE; g_debug("RAM description, so parsing"); sector = fu_dfu_sector_new((guint32)addr_tmp, 0x0, /* size */ 0x0, /* size_left */ 0x0, /* zone */ 0x0, /* number */ FU_DFU_SECTOR_CAP_ERASABLE | FU_DFU_SECTOR_CAP_READABLE | FU_DFU_SECTOR_CAP_WRITEABLE); g_ptr_array_add(priv->sectors, sector); } /* not a DfuSe alternative name */ if (alt_name[0] != '@') return TRUE; /* clear any existing zones */ g_ptr_array_set_size(priv->sectors, 0); /* parse zones */ zones = g_strsplit(alt_name, "/", -1); fu_device_set_name(FU_DEVICE(self), g_strchomp(zones[0] + 1)); for (guint i = 1; zones[i] != NULL; i += 2) { guint32 addr; guint64 addr_tmp = 0; g_auto(GStrv) sectors = NULL; /* parse address */ if (!g_str_has_prefix(zones[i], "0x")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "No sector address"); return FALSE; } if (!fu_strtoull(zones[i] + 2, &addr_tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_16, error)) { g_prefix_error(error, "sector address invalid: "); return FALSE; } addr = (guint32)addr_tmp; /* no sectors?! */ if (zones[i + 1] == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "No sector section"); return FALSE; } /* parse sectors */ sectors = g_strsplit(zones[i + 1], ",", -1); for (guint16 j = 0; sectors[j] != NULL; j++) { if (!fu_dfu_target_parse_sector(self, sectors[j], &addr, (i - 1) / 2, j, error)) { g_prefix_error(error, "Failed to parse: '%s': ", sectors[j]); return FALSE; } } } /* success */ return TRUE; } /** * fu_dfu_target_new: (skip) * * Creates a new DFU target, which represents an alt-setting on a * DFU-capable device. * * Returns: a #FuDfuTarget **/ FuDfuTarget * fu_dfu_target_new(void) { FuDfuTarget *self; self = g_object_new(FU_TYPE_DFU_TARGET, NULL); return self; } /** * fu_dfu_target_get_sectors: * @self: a #FuDfuTarget * * Gets the sectors exported by the target. * * Returns: (transfer none) (element-type FuDfuSector): sectors **/ GPtrArray * fu_dfu_target_get_sectors(FuDfuTarget *self) { FuDfuTargetPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_TARGET(self), NULL); return priv->sectors; } /** * fu_dfu_target_get_sector_default: * @self: a #FuDfuTarget * * Gets the default (first) sector exported by the target. * * Returns: (transfer none): a #FuDfuSector, or %NULL **/ FuDfuSector * fu_dfu_target_get_sector_default(FuDfuTarget *self) { FuDfuTargetPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_TARGET(self), NULL); if (priv->sectors->len == 0) return NULL; return FU_DFU_SECTOR(g_ptr_array_index(priv->sectors, 0)); } /** * fu_dfu_target_status_to_error_msg: * @status: a #FuDfuStatus, e.g. %FU_DFU_STATUS_ERR_ERASE * * Converts an enumerated value to an error description. * * Returns: a string **/ static const gchar * fu_dfu_target_status_to_error_msg(FuDfuStatus status) { if (status == FU_DFU_STATUS_OK) return "No error condition is present"; if (status == FU_DFU_STATUS_ERR_TARGET) return "Firmware is not for designed this device"; if (status == FU_DFU_STATUS_ERR_FILE) return "Firmware is for this device but fails verification"; if (status == FU_DFU_STATUS_ERR_WRITE) return "Device is unable to write memory"; if (status == FU_DFU_STATUS_ERR_ERASE) return "Memory erase function failed"; if (status == FU_DFU_STATUS_ERR_CHECK_ERASED) return "Memory erase check failed"; if (status == FU_DFU_STATUS_ERR_PROG) return "Program memory function failed"; if (status == FU_DFU_STATUS_ERR_VERIFY) return "Programmed memory failed verification"; if (status == FU_DFU_STATUS_ERR_ADDRESS) return "Cannot program memory due to address out of range"; if (status == FU_DFU_STATUS_ERR_NOTDONE) return "Received zero-length download but data is incomplete"; if (status == FU_DFU_STATUS_ERR_FIRMWARE) return "Device firmware is corrupt"; if (status == FU_DFU_STATUS_ERR_VENDOR) return "Vendor-specific error"; if (status == FU_DFU_STATUS_ERR_USBR) return "Device detected unexpected USB reset signaling"; if (status == FU_DFU_STATUS_ERR_POR) return "Device detected unexpected power on reset"; if (status == FU_DFU_STATUS_ERR_UNKNOWN) return "Something unexpected went wrong"; if (status == FU_DFU_STATUS_ERR_STALLDPKT) return "Device stalled an unexpected request"; return NULL; } static gboolean fu_dfu_target_manifest_wait(FuDfuTarget *self, GError **error) { FuDfuDevice *device = FU_DFU_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); guint polling_count = 0; /* get the status */ if (!fu_dfu_device_refresh(device, 0, error)) return FALSE; /* wait for FU_DFU_STATE_DFU_MANIFEST to not be set */ while (fu_dfu_device_get_state(device) == FU_DFU_STATE_DFU_MANIFEST_SYNC || fu_dfu_device_get_state(device) == FU_DFU_STATE_DFU_MANIFEST) { g_debug("waiting for FU_DFU_STATE_DFU_MANIFEST to clear"); if (polling_count++ > DFU_TARGET_MANIFEST_MAX_POLLING_TRIES) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "reach to max polling tries"); return FALSE; } fu_device_sleep(FU_DEVICE(device), fu_dfu_device_get_download_timeout(device) + 1000); if (!fu_dfu_device_refresh(device, 0, error)) return FALSE; } /* in an error state */ if (fu_dfu_device_get_state(device) == FU_DFU_STATE_DFU_ERROR) { g_set_error_literal( error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, fu_dfu_target_status_to_error_msg(fu_dfu_device_get_status(device))); return FALSE; } return TRUE; } gboolean fu_dfu_target_check_status(FuDfuTarget *self, GError **error) { FuDfuDevice *device = FU_DFU_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); FuDfuStatus status; g_autoptr(GTimer) timer = g_timer_new(); /* get the status */ if (!fu_dfu_device_refresh(device, 0, error)) return FALSE; /* wait for dfuDNBUSY to not be set */ while (fu_dfu_device_get_state(device) == FU_DFU_STATE_DFU_DNBUSY) { g_debug("waiting for FU_DFU_STATE_DFU_DNBUSY to clear"); fu_device_sleep(FU_DEVICE(device), fu_dfu_device_get_download_timeout(device)); if (!fu_dfu_device_refresh(device, 0, error)) return FALSE; /* this is a really long time to save fwupd in case * the device has got wedged */ if (g_timer_elapsed(timer, NULL) > 120.f) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Stuck in DFU_DNBUSY"); return FALSE; } } /* not in an error state */ if (fu_dfu_device_get_state(device) != FU_DFU_STATE_DFU_ERROR) return TRUE; /* STM32-specific long errors */ status = fu_dfu_device_get_status(device); if (fu_dfu_device_get_version(device) == FU_DFU_FIRMARE_VERSION_DFUSE) { if (status == FU_DFU_STATUS_ERR_VENDOR) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Read protection is active"); return FALSE; } if (status == FU_DFU_STATUS_ERR_TARGET) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Address is wrong or unsupported"); return FALSE; } } /* use a proper error description */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, fu_dfu_target_status_to_error_msg(status)); return FALSE; } /** * fu_dfu_target_use_alt_setting: * @self: a #FuDfuTarget * @error: (nullable): optional return location for an error * * Opens a DFU-capable target. * * Returns: %TRUE for success **/ static gboolean fu_dfu_target_use_alt_setting(FuDfuTarget *self, GError **error) { FuDfuDevice *device = FU_DFU_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); FuDfuTargetPrivate *priv = GET_PRIVATE(self); g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_DFU_TARGET(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* ensure interface is claimed */ if (!fu_dfu_device_ensure_interface(device, error)) return FALSE; /* use the correct setting */ if (fu_device_has_flag(FU_DEVICE(device), FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { if (!fu_usb_device_set_interface_alt(FU_USB_DEVICE(device), fu_dfu_device_get_interface(device), priv->alt_setting, &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot set alternate setting 0x%02x on interface %i: %s", priv->alt_setting, fu_dfu_device_get_interface(device), error_local->message); return FALSE; } } return TRUE; } /** * fu_dfu_target_setup: * @self: a #FuDfuTarget * @error: (nullable): optional return location for an error * * Opens a DFU-capable target. * * Returns: %TRUE for success **/ gboolean fu_dfu_target_setup(FuDfuTarget *self, GError **error) { FuDfuTargetClass *klass = FU_DFU_TARGET_GET_CLASS(self); FuDfuTargetPrivate *priv = GET_PRIVATE(self); FuDevice *device = fu_device_get_proxy(FU_DEVICE(self)); g_return_val_if_fail(FU_IS_DFU_TARGET(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* already done */ if (priv->done_setup) return TRUE; /* superclassed */ if (klass->setup != NULL) { if (!klass->setup(self, error)) return FALSE; } /* GD32VF103 devices features and peripheral list */ if (priv->alt_setting == 0x0 && fu_device_has_private_flag(device, FU_DFU_DEVICE_FLAG_GD32)) { /* RB R8 R6 R4 VB V8 * Flash (KB) 128 64 32 16 128 64 * TB T8 T6 T4 CB C8 C6 C4 * Flash (KB) 128 64 32 16 128 64 32 16 */ const gchar *serial = fu_device_get_serial(device); if (serial == NULL || strlen(serial) < 4 || serial[3] != 'J') { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "GD32 serial number %s invalid", serial); return FALSE; } if (serial[2] == '2') { fu_device_set_logical_id(FU_DEVICE(self), "@Internal Flash /0x8000000/8*1Kg"); } else if (serial[2] == '4') { fu_device_set_logical_id(FU_DEVICE(self), "@Internal Flash /0x8000000/16*1Kg"); } else if (serial[2] == '6') { fu_device_set_logical_id(FU_DEVICE(self), "@Internal Flash /0x8000000/32*1Kg"); } else if (serial[2] == '8') { fu_device_set_logical_id(FU_DEVICE(self), "@Internal Flash /0x8000000/64*1Kg"); } else if (serial[2] == 'B') { fu_device_set_logical_id(FU_DEVICE(self), "@Internal Flash /0x8000000/128*1Kg"); } else if (serial[2] == 'D') { fu_device_set_logical_id(FU_DEVICE(self), "@Internal Flash /0x8000000/256*1Kg"); } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Unknown GD32 sector size: %c", serial[2]); return FALSE; } } /* get string */ if (priv->alt_idx != 0x00 && fu_device_get_logical_id(FU_DEVICE(self)) == NULL) { g_autofree gchar *alt_name = NULL; alt_name = fu_usb_device_get_string_descriptor(FU_USB_DEVICE(device), priv->alt_idx, NULL); fu_device_set_logical_id(FU_DEVICE(self), alt_name); } /* parse the DfuSe format according to UM0424 */ if (priv->sectors->len == 0) { if (!fu_dfu_target_parse_sectors(self, fu_device_get_logical_id(FU_DEVICE(self)), error)) return FALSE; } /* add a dummy entry */ if (priv->sectors->len == 0) { FuDfuSector *sector; sector = fu_dfu_sector_new(0x0, /* addr */ 0x0, /* size */ 0x0, /* size_left */ 0x0, /* zone */ 0x0, /* number */ FU_DFU_SECTOR_CAP_READABLE | FU_DFU_SECTOR_CAP_WRITEABLE); g_debug("no UM0424 sector description in %s", fu_device_get_logical_id(FU_DEVICE(self))); g_ptr_array_add(priv->sectors, sector); } priv->done_setup = TRUE; return TRUE; } gboolean fu_dfu_target_download_chunk(FuDfuTarget *self, guint16 index, GByteArray *buf, guint timeout_ms, FuProgress *progress, GError **error) { FuDfuDevice *device = FU_DFU_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); g_autoptr(GError) error_local = NULL; gsize actual_length; /* fall back to default */ if (timeout_ms == 0) timeout_ms = fu_dfu_device_get_timeout(device); /* low level packet debugging */ fu_dump_raw(G_LOG_DOMAIN, "Message", buf->data, buf->len); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_CLASS, FU_USB_RECIPIENT_INTERFACE, FU_DFU_REQUEST_DNLOAD, index, fu_dfu_device_get_interface(device), buf->data, buf->len, &actual_length, timeout_ms, NULL, &error_local)) { /* refresh the error code */ fu_dfu_device_error_fixup(device, &error_local); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot download data: %s", error_local->message); return FALSE; } /* for STM32 devices, the action only occurs when we do GetStatus -- * and it can take a long time to complete! */ if (fu_dfu_device_get_version(device) == FU_DFU_FIRMARE_VERSION_DFUSE) { if (!fu_dfu_device_refresh(device, 35000, error)) return FALSE; } /* wait for the device to write contents to the EEPROM */ if (buf->len == 0 && fu_dfu_device_get_download_timeout(device) > 0) fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_BUSY); if (fu_dfu_device_get_download_timeout(device) > 0) { g_debug("sleeping for %ums…", fu_dfu_device_get_download_timeout(device)); fu_device_sleep(FU_DEVICE(device), fu_dfu_device_get_download_timeout(device)); } /* find out if the write was successful, waiting for BUSY to clear */ if (!fu_dfu_target_check_status(self, error)) { g_prefix_error(error, "cannot wait for busy: "); return FALSE; } g_assert_cmpint(actual_length, ==, buf->len); return TRUE; } GBytes * fu_dfu_target_upload_chunk(FuDfuTarget *self, guint16 index, gsize buf_sz, FuProgress *progress, GError **error) { FuDfuDevice *device = FU_DFU_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); g_autoptr(GError) error_local = NULL; guint8 *buf; gsize actual_length; /* unset */ if (buf_sz == 0) buf_sz = (gsize)fu_dfu_device_get_transfer_size(device); buf = g_new0(guint8, buf_sz); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(device), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_CLASS, FU_USB_RECIPIENT_INTERFACE, FU_DFU_REQUEST_UPLOAD, index, fu_dfu_device_get_interface(device), buf, buf_sz, &actual_length, fu_dfu_device_get_timeout(device), NULL, &error_local)) { /* refresh the error code */ fu_dfu_device_error_fixup(device, &error_local); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot upload data: %s", error_local->message); return NULL; } /* low level packet debugging */ fu_dump_raw(G_LOG_DOMAIN, "Message", buf, actual_length); return g_bytes_new_take(buf, actual_length); } void fu_dfu_target_set_alt_idx(FuDfuTarget *self, guint8 alt_idx) { FuDfuTargetPrivate *priv = GET_PRIVATE(self); priv->alt_idx = alt_idx; } void fu_dfu_target_set_alt_setting(FuDfuTarget *self, guint8 alt_setting) { FuDfuTargetPrivate *priv = GET_PRIVATE(self); priv->alt_setting = alt_setting; } gboolean fu_dfu_target_attach(FuDfuTarget *self, FuProgress *progress, GError **error) { FuDfuDevice *device = FU_DFU_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); FuDfuTargetClass *klass = FU_DFU_TARGET_GET_CLASS(self); /* ensure populated */ if (!fu_dfu_target_setup(self, error)) return FALSE; /* implemented as part of a superclass */ if (klass->attach != NULL) return klass->attach(self, progress, error); /* normal DFU mode just needs a bus reset */ return fu_dfu_device_reset(device, progress, error); } static FuChunk * fu_dfu_target_upload_element_dfu(FuDfuTarget *self, guint32 address, gsize expected_size, gsize maximum_size, FuProgress *progress, GError **error) { FuDfuDevice *device = FU_DFU_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); GBytes *chunk_tmp; guint percentage_size = expected_size > 0 ? expected_size : maximum_size; gsize total_size = 0; guint16 transfer_size = fu_dfu_device_get_transfer_size(device); g_autoptr(GBytes) contents = NULL; g_autoptr(GPtrArray) chunks = NULL; /* update UI */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ); /* get all the chunks from the hardware */ chunks = g_ptr_array_new_with_free_func((GDestroyNotify)g_bytes_unref); for (guint16 idx = 0; idx < G_MAXUINT16; idx++) { guint32 chunk_size; /* read chunk of data */ chunk_tmp = fu_dfu_target_upload_chunk(self, idx, 0, /* device transfer size */ progress, error); if (chunk_tmp == NULL) return NULL; /* keep a sum of all the chunks */ chunk_size = (guint32)g_bytes_get_size(chunk_tmp); total_size += chunk_size; if (total_size > maximum_size) break; /* add to array */ g_debug("got #%04x chunk of size %" G_GUINT32_FORMAT, idx, chunk_size); g_ptr_array_add(chunks, chunk_tmp); /* update UI */ if (chunk_size > 0) fu_progress_set_percentage_full(progress, total_size, percentage_size); /* detect short write as EOF */ if (chunk_size < transfer_size) break; } /* check final size */ if (expected_size > 0) { if (total_size != expected_size) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid size, got %" G_GSIZE_FORMAT ", " "expected %" G_GSIZE_FORMAT, total_size, expected_size); return NULL; } } /* done */ fu_progress_set_percentage(progress, 100); /* create new image */ contents = fu_dfu_utils_bytes_join_array(chunks); return fu_chunk_bytes_new(contents); } static FuChunk * fu_dfu_target_upload_element(FuDfuTarget *self, guint32 address, gsize expected_size, gsize maximum_size, FuProgress *progress, GError **error) { FuDfuTargetClass *klass = FU_DFU_TARGET_GET_CLASS(self); /* implemented as part of a superclass */ if (klass->upload_element != NULL) { return klass ->upload_element(self, address, expected_size, maximum_size, progress, error); } return fu_dfu_target_upload_element_dfu(self, address, expected_size, maximum_size, progress, error); } static guint32 fu_dfu_target_get_size_of_zone(FuDfuTarget *self, guint16 zone) { FuDfuTargetPrivate *priv = GET_PRIVATE(self); guint32 len = 0; for (guint i = 0; i < priv->sectors->len; i++) { FuDfuSector *sector = g_ptr_array_index(priv->sectors, i); if (fu_dfu_sector_get_zone(sector) != zone) continue; len += fu_dfu_sector_get_size(sector); } return len; } /* private */ gboolean fu_dfu_target_upload(FuDfuTarget *self, FuFirmware *firmware, FuProgress *progress, FuDfuTargetTransferFlags flags, GError **error) { FuDfuTargetPrivate *priv = GET_PRIVATE(self); FuDfuSector *sector; guint16 zone_cur; guint32 zone_size = 0; guint32 zone_last = G_MAXUINT; g_autoptr(FuFirmware) image = NULL; g_return_val_if_fail(FU_IS_DFU_TARGET(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* ensure populated */ if (!fu_dfu_target_setup(self, error)) return FALSE; /* can the target do this? */ if (!fu_device_has_private_flag(fu_device_get_proxy(FU_DEVICE(self)), FU_DFU_DEVICE_FLAG_CAN_UPLOAD)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "target cannot do uploading"); return FALSE; } /* use correct alt */ if (!fu_dfu_target_use_alt_setting(self, error)) return FALSE; /* no open?! */ if (priv->sectors->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no sectors defined for target"); return FALSE; } /* create a new image */ image = fu_firmware_new(); fu_firmware_set_id(image, fu_device_get_logical_id(FU_DEVICE(self))); fu_firmware_set_idx(image, priv->alt_setting); /* get all the sectors for the device */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, priv->sectors->len); for (guint i = 0; i < priv->sectors->len; i++) { g_autoptr(FuChunk) chk = NULL; /* only upload to the start of any zone:sector */ sector = g_ptr_array_index(priv->sectors, i); zone_cur = fu_dfu_sector_get_zone(sector); if (zone_cur == zone_last) continue; /* get the size of the entire continuous zone */ zone_size = fu_dfu_target_get_size_of_zone(self, zone_cur); zone_last = zone_cur; /* get the first element from the hardware */ g_debug("starting upload from 0x%08x (0x%04x)", fu_dfu_sector_get_address(sector), zone_size); chk = fu_dfu_target_upload_element(self, fu_dfu_sector_get_address(sector), 0, /* expected */ zone_size, /* maximum */ fu_progress_get_child(progress), error); if (chk == NULL) return FALSE; /* this chunk was uploaded okay */ fu_firmware_add_chunk(image, chk); fu_progress_step_done(progress); } /* success */ fu_firmware_add_image(firmware, image); return TRUE; } static gchar * _g_bytes_compare_verbose(GBytes *bytes1, GBytes *bytes2) { const guint8 *data1; const guint8 *data2; gsize length1; gsize length2; data1 = g_bytes_get_data(bytes1, &length1); data2 = g_bytes_get_data(bytes2, &length2); /* not the same length */ if (length1 != length2) { return g_strdup_printf("got %" G_GSIZE_FORMAT " bytes, " "expected %" G_GSIZE_FORMAT, length1, length2); } /* return 00 01 02 03 */ for (guint i = 0; i < length1; i++) { if (data1[i] != data2[i]) { return g_strdup_printf("got 0x%02x, expected 0x%02x @ 0x%04x", data1[i], data2[i], i); } } return NULL; } static gboolean fu_dfu_target_download_element_dfu(FuDfuTarget *self, FuChunk *chk, FuProgress *progress, FuDfuTargetTransferFlags flags, GError **error) { FuDfuDevice *device = FU_DFU_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); guint32 nr_chunks; guint16 transfer_size = fu_dfu_device_get_transfer_size(device); g_autoptr(GBytes) bytes = NULL; /* round up as we have to transfer incomplete blocks */ bytes = fu_chunk_get_bytes(chk); nr_chunks = (guint)ceil((gdouble)g_bytes_get_size(bytes) / (gdouble)transfer_size); if (nr_chunks == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "zero-length firmware"); return FALSE; } fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); for (guint32 i = 0; i < nr_chunks + 1; i++) { gsize length; guint32 offset; g_autoptr(GByteArray) buf = g_byte_array_new(); /* calculate the offset into the chunk data */ offset = i * transfer_size; /* we have to write one final zero-sized chunk for EOF */ if (i < nr_chunks) { g_autoptr(GBytes) bytes_tmp = NULL; length = g_bytes_get_size(bytes) - offset; if (length > transfer_size) length = transfer_size; bytes_tmp = fu_bytes_new_offset(bytes, offset, length, error); if (bytes_tmp == NULL) return FALSE; fu_byte_array_append_bytes(buf, bytes_tmp); } g_debug("writing #%04x chunk of size 0x%x", i, buf->len); if (!fu_dfu_target_download_chunk(self, i, buf, 0, progress, error)) { g_prefix_error(error, "failed to write chunk %u: ", i); return FALSE; } /* update UI */ fu_progress_set_percentage_full(progress, i + 1, nr_chunks + 1); } /* success */ return TRUE; } static gboolean fu_dfu_target_download_element(FuDfuTarget *self, FuChunk *chk, FuProgress *progress, FuDfuTargetTransferFlags flags, GError **error) { FuDevice *device = fu_device_get_proxy(FU_DEVICE(self)); FuDfuTargetClass *klass = FU_DFU_TARGET_GET_CLASS(self); /* progress */ if (flags & DFU_TARGET_TRANSFER_FLAG_VERIFY && fu_device_has_private_flag(device, FU_DFU_DEVICE_FLAG_CAN_UPLOAD)) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 96, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 4, NULL); } else { fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, 1); } /* implemented as part of a superclass */ if (klass->download_element != NULL) { if (!klass->download_element(self, chk, fu_progress_get_child(progress), flags, error)) return FALSE; } else { if (!fu_dfu_target_download_element_dfu(self, chk, fu_progress_get_child(progress), flags, error)) return FALSE; } fu_progress_step_done(progress); /* verify */ if (flags & DFU_TARGET_TRANSFER_FLAG_VERIFY && fu_device_has_private_flag(device, FU_DFU_DEVICE_FLAG_CAN_UPLOAD)) { g_autoptr(GBytes) bytes = NULL; g_autoptr(GBytes) bytes_tmp = NULL; g_autoptr(FuChunk) chunk_tmp = NULL; bytes = fu_chunk_get_bytes(chk); chunk_tmp = fu_dfu_target_upload_element(self, fu_chunk_get_address(chk), g_bytes_get_size(bytes), g_bytes_get_size(bytes), fu_progress_get_child(progress), error); if (chunk_tmp == NULL) return FALSE; bytes_tmp = fu_chunk_get_bytes(chunk_tmp); if (g_bytes_compare(bytes_tmp, bytes) != 0) { g_autofree gchar *bytes_cmp_str = NULL; bytes_cmp_str = _g_bytes_compare_verbose(bytes_tmp, bytes); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "verify failed: %s", bytes_cmp_str); return FALSE; } fu_progress_step_done(progress); } return TRUE; } /** * fu_dfu_target_download: * @self: a #FuDfuTarget * @image: a #FuFirmware * @flags: DFU target flags, e.g. %DFU_TARGET_TRANSFER_FLAG_VERIFY * @error: (nullable): optional return location for an error * * Downloads firmware from the host to the target, optionally verifying * the transfer. * * Returns: %TRUE for success **/ gboolean fu_dfu_target_download(FuDfuTarget *self, FuFirmware *image, FuProgress *progress, FuDfuTargetTransferFlags flags, GError **error) { FuDevice *device = fu_device_get_proxy(FU_DEVICE(self)); FuDfuTargetPrivate *priv = GET_PRIVATE(self); g_autoptr(GPtrArray) chunks = NULL; g_return_val_if_fail(FU_IS_DFU_TARGET(self), FALSE); g_return_val_if_fail(FU_IS_FIRMWARE(image), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* ensure populated */ if (!fu_dfu_target_setup(self, error)) return FALSE; /* can the target do this? */ if (!fu_device_has_private_flag(device, FU_DFU_DEVICE_FLAG_CAN_DOWNLOAD)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "target cannot do downloading"); return FALSE; } /* use correct alt */ if (!fu_dfu_target_use_alt_setting(self, error)) return FALSE; /* download all chunks in the image to the device */ chunks = fu_firmware_get_chunks(image, error); if (chunks == NULL) return FALSE; if (chunks->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no image chunks"); return FALSE; } fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); g_debug("downloading chunk at 0x%04x", (guint)fu_chunk_get_address(chk)); /* auto-detect missing firmware address -- this assumes * that the first target is the main program memory and that * there is only one element in the firmware file */ if (flags & DFU_TARGET_TRANSFER_FLAG_ADDR_HEURISTIC && fu_chunk_get_address(chk) == 0x0 && chunks->len == 1 && priv->sectors->len > 0) { FuDfuSector *sector = g_ptr_array_index(priv->sectors, 0); g_debug("fixing up firmware address from 0x0 to 0x%x", fu_dfu_sector_get_address(sector)); fu_chunk_set_address(chk, fu_dfu_sector_get_address(sector)); } /* download to device */ if (!fu_dfu_target_download_element(self, chk, fu_progress_get_child(progress), flags, error)) return FALSE; fu_progress_step_done(progress); } if (fu_device_has_private_flag(device, FU_DFU_DEVICE_FLAG_MANIFEST_POLL) && fu_device_has_private_flag(device, FU_DFU_DEVICE_FLAG_MANIFEST_TOL)) if (!fu_dfu_target_manifest_wait(self, error)) return FALSE; /* success */ return TRUE; } /** * fu_dfu_target_get_alt_setting: * @self: a #FuDfuTarget * * Gets the alternate setting to use for this interface. * * Returns: the alternative setting, typically zero **/ guint8 fu_dfu_target_get_alt_setting(FuDfuTarget *self) { FuDfuTargetPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DFU_TARGET(self), 0xff); return priv->alt_setting; } static void fu_dfu_target_class_init(FuDfuTargetClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_dfu_target_to_string; object_class->finalize = fu_dfu_target_finalize; } fwupd-2.0.10/plugins/dfu/fu-dfu-target.h000066400000000000000000000046101501337203100177750ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-dfu-common.h" #include "fu-dfu-sector.h" #define FU_TYPE_DFU_TARGET (fu_dfu_target_get_type()) G_DECLARE_DERIVABLE_TYPE(FuDfuTarget, fu_dfu_target, FU, DFU_TARGET, FuDevice) /** * FuDfuTargetTransferFlags: * @DFU_TARGET_TRANSFER_FLAG_NONE: No flags set * @DFU_TARGET_TRANSFER_FLAG_VERIFY: Verify the download once complete * @DFU_TARGET_TRANSFER_FLAG_WILDCARD_VID: Allow downloading images with wildcard VIDs * @DFU_TARGET_TRANSFER_FLAG_WILDCARD_PID: Allow downloading images with wildcard PIDs * @DFU_TARGET_TRANSFER_FLAG_ADDR_HEURISTIC: Automatically detect the address to use * * The optional flags used for transferring firmware. **/ typedef enum { DFU_TARGET_TRANSFER_FLAG_NONE = 0, DFU_TARGET_TRANSFER_FLAG_VERIFY = (1 << 0), DFU_TARGET_TRANSFER_FLAG_WILDCARD_VID = (1 << 4), DFU_TARGET_TRANSFER_FLAG_WILDCARD_PID = (1 << 5), DFU_TARGET_TRANSFER_FLAG_ADDR_HEURISTIC = (1 << 7), /*< private >*/ DFU_TARGET_TRANSFER_FLAG_LAST } FuDfuTargetTransferFlags; struct _FuDfuTargetClass { FuDeviceClass parent_class; gboolean (*setup)(FuDfuTarget *self, GError **error); gboolean (*attach)(FuDfuTarget *self, FuProgress *progress, GError **error); gboolean (*detach)(FuDfuTarget *self, FuProgress *progress, GError **error); gboolean (*mass_erase)(FuDfuTarget *self, FuProgress *progress, GError **error); FuChunk *(*upload_element)(FuDfuTarget *self, guint32 address, gsize expected_size, gsize maximum_size, FuProgress *progress, GError **error); gboolean (*download_element)(FuDfuTarget *self, FuChunk *chk, FuProgress *progress, FuDfuTargetTransferFlags flags, GError **error); }; GPtrArray * fu_dfu_target_get_sectors(FuDfuTarget *self); FuDfuSector * fu_dfu_target_get_sector_default(FuDfuTarget *self); guint8 fu_dfu_target_get_alt_setting(FuDfuTarget *self); gboolean fu_dfu_target_upload(FuDfuTarget *self, FuFirmware *firmware, FuProgress *progress, FuDfuTargetTransferFlags flags, GError **error); gboolean fu_dfu_target_setup(FuDfuTarget *self, GError **error); gboolean fu_dfu_target_download(FuDfuTarget *self, FuFirmware *image, FuProgress *progress, FuDfuTargetTransferFlags flags, GError **error); fwupd-2.0.10/plugins/dfu/fu-dfu.rs000066400000000000000000000020161501337203100167040ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later enum FuDfuDeviceAttr { CanDownload = 1 << 0, CanUpload = 1 << 1, ManifestTol = 1 << 2, WillDetach = 1 << 3, CanAccelerate = 1 << 7, } enum FuDfuRequest { Detach, Dnload, Upload, Getstatus, Clrstatus, Getstate, Abort, } #[derive(ToString)] enum FuDfuStatus { Ok, ErrTarget, ErrFile, ErrWrite, ErrErase, ErrCheckErased, ErrProg, ErrVerify, ErrAddress, ErrNotdone, ErrFirmware, ErrVendor, ErrUsbr, ErrPor, ErrUnknown, ErrStalldpkt, } #[derive(ToString)] enum FuDfuState { AppIdle, AppDetach, DfuIdle, DfuDnloadSync, DfuDnbusy, DfuDnloadIdle, DfuManifestSync, DfuManifest, DfuManifestWaitReset, DfuUploadIdle, DfuError, } #[derive(ToBitString)] enum FuDfuSectorCap { None = 0, // No operations possible Readable = 1 << 0, Writeable = 1 << 1, Erasable = 1 << 2, } fwupd-2.0.10/plugins/dfu/meson.build000066400000000000000000000034471501337203100173230ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginDfu"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('dfu.quirk') # do not use structgen as these files are used in the dfu-csr plugin too... dfu_rs = custom_target('fu-dfu-rs', input: 'fu-dfu.rs', output: ['fu-dfu-struct.c', 'fu-dfu-struct.h'], command: [ python3, join_paths(meson.project_source_root(), 'libfwupdplugin', 'rustgen.py'), '@INPUT@', '@OUTPUT0@', '@OUTPUT1@', ], ) plugin_builtin_dfu = static_library('fu_plugin_dfu', dfu_rs, sources: [ 'fu-dfu-plugin.c', 'fu-dfu-common.c', 'fu-dfu-device.c', 'fu-dfu-sector.c', 'fu-dfu-target.c', 'fu-dfu-target-stm.c', 'fu-dfu-target-avr.c', ], include_directories: plugin_incdirs, c_args: cargs, dependencies: [ plugin_deps, libm, ], link_with: plugin_libs, ) plugin_builtins += plugin_builtin_dfu enumeration_data += files('tests/dfu-gd32vf103-setup.json') device_tests += files( 'tests/dfu-gd32vf103.json', 'tests/fwupd-a3bu-xplained.json', 'tests/fwupd-at90usbkey.json', 'tests/realtek-rts5855.json', ) if get_option('tests') env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) e = executable( 'fu-dfu-self-test', dfu_rs, sources: [ 'fu-dfu-self-test.c' ], include_directories: plugin_incdirs, dependencies: [ plugin_deps, libm, ], link_with: [ plugin_libs, plugin_builtin_dfu, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('fu-dfu-self-test', e, env: env) # added to installed-tests endif plugindfu_incdir = include_directories('.') fwupd-2.0.10/plugins/dfu/tests/000077500000000000000000000000001501337203100163135ustar00rootroot00000000000000fwupd-2.0.10/plugins/dfu/tests/dfu-gd32vf103-setup.json000066400000000000000000000021431501337203100224370ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUsbDevice", "PlatformId": "5-1.3", "Created": "2024-10-24T10:24:54.143375Z", "IdVendor": 10473, "IdProduct": 393, "Device": 256, "USB": 512, "Manufacturer": 1, "Product": 2, "SerialNumber": 3, "UsbConfigDescriptors": [ { "Configuration": 4, "ConfigurationValue": 1 } ], "UsbInterfaces": [ { "Length": 9, "DescriptorType": 4, "InterfaceClass": 254, "InterfaceSubClass": 1, "InterfaceProtocol": 2, "Interface": 5 } ], "UsbEvents": [ { "Id": "#4693935e", "Data": "005" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=532\nDEVNAME=bus/usb/005/021\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=28e9/189/100\nTYPE=0/0/0\nBUSNUM=005\nDEVNUM=021" }, { "Id": "#1ab3ae0a", "Data": "021" }, { "Id": "#c379adf5", "Data": "BgMzQ0JK" } ] } ] } fwupd-2.0.10/plugins/dfu/tests/dfu-gd32vf103.json000066400000000000000000000005071501337203100213030ustar00rootroot00000000000000{ "name": "GigaDevice GD32VF103", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/dfu-gd32vf103-setup.json", "components": [ { "version": "1.0", "guids": [ "f40ef7cf-e4f6-5708-b274-13edfb04646e" ] } ] } ] } fwupd-2.0.10/plugins/dfu/tests/fwupd-a3bu-xplained.json000066400000000000000000000014501501337203100227650ustar00rootroot00000000000000{ "name": "LVFS A3BU XPLAINED", "interactive": false, "steps": [ { "url": "f5bbeaba1037dce31dd12f349e8148ae35f98b61-a3bu-xplained123.cab", "emulation-url": "01b95b0206f1a42a2bf95a432d162ef1f9f1f71edb5696127c923ceffadfdf68-a3bu-xplained123.zip", "components": [ { "version": "1.23", "guids": [ "80478b9a-3643-5e47-ab0f-ed28abe1019d" ] } ] }, { "url": "24d838541efe0340bf67e1cc5a9b95526e4d3702-a3bu-xplained124.cab", "emulation-url": "357483755bbc4f3a33c0b5bc05a0ef49654be0a15c14cbde2bdf0cd9c203d632-a3bu-xplained124.zip", "components": [ { "version": "1.24", "guids": [ "80478b9a-3643-5e47-ab0f-ed28abe1019d" ] } ] } ] } fwupd-2.0.10/plugins/dfu/tests/fwupd-at90usbkey.json000066400000000000000000000014311501337203100223300ustar00rootroot00000000000000{ "name": "LVFS AT90USBKEY", "interactive": false, "steps": [ { "url": "b6bef375597e848971f230cf992c9740f7bf5b92-at90usbkey123.cab", "emulation-url": "57c8acd0de45ff01d91da5ecec1f826fbb438cc3f7b11ca732a8fbfdc2ce24e1-at90usbkey123.zip", "components": [ { "version": "1.23", "guids": [ "c1874c52-5f6a-5864-926d-ea84bcdc82ea" ] } ] }, { "url": "47807fd4a94a4d5514ac6bf7a73038e00ed63225-at90usbkey124.cab", "emulation-url": "e65432a2dd63c3ce666cc13a0f5ae13eb6d15cebbc1d022cafcff46b892cdcc4-at90usbkey124.zip", "components": [ { "version": "1.24", "guids": [ "c1874c52-5f6a-5864-926d-ea84bcdc82ea" ] } ] } ] } fwupd-2.0.10/plugins/dfu/tests/realtek-rts5855.json000066400000000000000000000012461501337203100217750ustar00rootroot00000000000000{ "name": "Realtek RTS5855 Webcam", "interactive": false, "steps": [ { "url": "ff989c4b71c92a4a217dfb2f82c1c87691b8eb31-rts5855_v0.4.cab", "emulation-url": "75d6c26e3842bbd00991a585078e872cae66c7a81e15c5736fc478ba5ec0f77d-rts5855_v0.4.zip", "components": [ { "version": "0.4", "guids": [ "9829f051-47f5-55e7-87dc-a49cf55602e2" ] } ] }, { "url": "ed5c411d6b74c363209f408f87618fa5c31b50ab-v0.3.cab", "components": [ { "version": "0.3", "guids": [ "9829f051-47f5-55e7-87dc-a49cf55602e2" ] } ] } ] } fwupd-2.0.10/plugins/ebitdo/000077500000000000000000000000001501337203100156415ustar00rootroot00000000000000fwupd-2.0.10/plugins/ebitdo/README.md000066400000000000000000000031071501337203100171210ustar00rootroot00000000000000--- title: Plugin: 8BitDo --- ## Introduction This plugin can flash the firmware on the 8BitDo game pads. Ebitdo support is supported directly by this project with the embedded libebitdo library and is possible thanks to the vendor open sourcing the flashing tool. The 8BitDo devices share legacy USB VID/PIDs with other projects and so we have to be a bit careful to not claim other devices as our own. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in an unspecified binary file format. The binary file has a vendor-specific header that is used when flashing the image. This plugin supports the following protocol ID: * `com.8bitdo` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_2DC8&PID_AB11` ## Update Behavior The device usually presents in runtime mode, but on detach re-enumerates with a different USB VID and PID in a bootloader mode. On attach the device again re-enumerates back to the runtime mode. For this reason the `REPLUG_MATCH_GUID` internal device flag is used so that the bootloader and runtime modes are treated as the same device. ## Vendor ID Security The vendor ID is set from the USB vendor, which is set to various different values depending on the model and device mode. The list of USB VIDs used is: * `USB:0x2DC8` * `USB:0x0483` * `USB:0x1002` * `USB:0x1235` * `USB:0x2002` * `USB:0x8000` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `0.8.0`. fwupd-2.0.10/plugins/ebitdo/data/000077500000000000000000000000001501337203100165525ustar00rootroot00000000000000fwupd-2.0.10/plugins/ebitdo/data/m30.txt000066400000000000000000000175211501337203100177200ustar00rootroot00000000000000Bus 002 Device 008: ID 2dc8:5006 8BitDo M30 gamepad Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 0 bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 64 idVendor 0x2dc8 8BitDo idProduct 0x5006 M30 gamepad bcdDevice 0.01 iManufacturer 1 8Bitdo iProduct 2 8BitDo M30 gamepad iSerial 0 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 0x0029 bNumInterfaces 1 bConfigurationValue 1 iConfiguration 0 bmAttributes 0xc0 Self Powered MaxPower 480mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 2 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 0 bInterfaceProtocol 0 iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.11 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 123 Report Descriptor: (length is 123) Item(Global): Usage Page, data= [ 0x01 ] 1 Generic Desktop Controls Item(Local ): Usage, data= [ 0x05 ] 5 Gamepad Item(Main ): Collection, data= [ 0x01 ] 1 Application Item(Global): Logical Minimum, data= [ 0x00 ] 0 Item(Global): Logical Maximum, data= [ 0x01 ] 1 Item(Global): Physical Minimum, data= [ 0x00 ] 0 Item(Global): Physical Maximum, data= [ 0x01 ] 1 Item(Global): Report Size, data= [ 0x01 ] 1 Item(Global): Report Count, data= [ 0x0f ] 15 Item(Global): Usage Page, data= [ 0x09 ] 9 Buttons Item(Local ): Usage Minimum, data= [ 0x01 ] 1 Button 1 (Primary) Item(Local ): Usage Maximum, data= [ 0x0f ] 15 (null) Item(Main ): Input, data= [ 0x02 ] 2 Data Variable Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Global): Report Count, data= [ 0x01 ] 1 Item(Main ): Input, data= [ 0x01 ] 1 Constant Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Global): Usage Page, data= [ 0x01 ] 1 Generic Desktop Controls Item(Global): Logical Maximum, data= [ 0x07 ] 7 Item(Global): Physical Maximum, data= [ 0x3b 0x01 ] 315 Item(Global): Report Size, data= [ 0x04 ] 4 Item(Global): Report Count, data= [ 0x01 ] 1 Item(Global): Unit, data= [ 0x14 ] 20 System: English Rotation, Unit: Degrees Item(Local ): Usage, data= [ 0x39 ] 57 Hat Switch Item(Main ): Input, data= [ 0x42 ] 66 Data Variable Absolute No_Wrap Linear Preferred_State Null_State Non_Volatile Bitfield Item(Global): Unit, data= [ 0x00 ] 0 System: None, Unit: (None) Item(Global): Report Count, data= [ 0x01 ] 1 Item(Main ): Input, data= [ 0x01 ] 1 Constant Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Global): Logical Maximum, data= [ 0xff 0x00 ] 255 Item(Global): Physical Maximum, data= [ 0xff 0x00 ] 255 Item(Local ): Usage, data= [ 0x30 ] 48 Direction-X Item(Local ): Usage, data= [ 0x31 ] 49 Direction-Y Item(Local ): Usage, data= [ 0x32 ] 50 Direction-Z Item(Local ): Usage, data= [ 0x35 ] 53 Rotate-Z Item(Global): Report Size, data= [ 0x08 ] 8 Item(Global): Report Count, data= [ 0x04 ] 4 Item(Main ): Input, data= [ 0x02 ] 2 Data Variable Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Global): Usage Page, data= [ 0x02 ] 2 Simulation Controls Item(Global): Logical Minimum, data= [ 0x00 ] 0 Item(Global): Logical Maximum, data= [ 0xff 0x00 ] 255 Item(Local ): Usage, data= [ 0xc4 ] 196 Accelerator Item(Local ): Usage, data= [ 0xc5 ] 197 Brake Item(Global): Report Count, data= [ 0x02 ] 2 Item(Global): Report Size, data= [ 0x08 ] 8 Item(Main ): Input, data= [ 0x02 ] 2 Data Variable Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Global): Usage Page, data= [ 0x08 ] 8 LEDs Item(Local ): Usage, data= [ 0x43 ] 67 Slow Blink On Time Item(Global): Logical Minimum, data= [ 0x00 ] 0 Item(Global): Logical Maximum, data= [ 0xff 0x00 ] 255 Item(Global): Physical Minimum, data= [ 0x00 ] 0 Item(Global): Physical Maximum, data= [ 0xff 0x00 ] 255 Item(Global): Report Size, data= [ 0x08 ] 8 Item(Global): Report Count, data= [ 0x02 ] 2 Item(Main ): Output, data= [ 0x82 ] 130 Data Variable Absolute No_Wrap Linear Preferred_State No_Null_Position Volatile Bitfield Item(Local ): Usage, data= [ 0x44 ] 68 Slow Blink Off Time Item(Main ): Output, data= [ 0x82 ] 130 Data Variable Absolute No_Wrap Linear Preferred_State No_Null_Position Volatile Bitfield Item(Local ): Usage, data= [ 0x45 ] 69 Fast Blink On Time Item(Main ): Output, data= [ 0x82 ] 130 Data Variable Absolute No_Wrap Linear Preferred_State No_Null_Position Volatile Bitfield Item(Local ): Usage, data= [ 0x46 ] 70 Fast Blink Off Time Item(Main ): Output, data= [ 0x82 ] 130 Data Variable Absolute No_Wrap Linear Preferred_State No_Null_Position Volatile Bitfield Item(Main ): End Collection, data=none Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 5 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x02 EP 2 OUT bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 16 Device Status: 0x0000 (Bus Powered) fwupd-2.0.10/plugins/ebitdo/data/nes30pro.txt000066400000000000000000000107671501337203100207770ustar00rootroot00000000000000Bus 003 Device 017: ID 2002:9000 DAP Technologies Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 0 bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 64 idVendor 0x2002 DAP Technologies idProduct 0x9000 bcdDevice 0.01 iManufacturer 1 8Bitdo NES30 Pro iProduct 2 8Bitdo NES30 Pro iSerial 0 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 41 bNumInterfaces 1 bConfigurationValue 1 iConfiguration 0 bmAttributes 0xc0 Self Powered MaxPower 480mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 2 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 0 bInterfaceProtocol 0 iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.11 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 123 Report Descriptors: ** UNAVAILABLE ** Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 32 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x02 EP 2 OUT bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 16 Device Status: 0x0000 (Bus Powered) Bus 003 Device 019: ID 0483:5750 STMicroelectronics Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 0 bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 64 idVendor 0x0483 STMicroelectronics idProduct 0x5750 bcdDevice 2.00 iManufacturer 1 8BitdoJoy iProduct 2 8Bitdo iSerial 3 BootMod bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 41 bNumInterfaces 1 bConfigurationValue 1 iConfiguration 0 bmAttributes 0xc0 Self Powered MaxPower 480mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 2 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 0 bInterfaceProtocol 0 iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.10 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 33 Report Descriptors: ** UNAVAILABLE ** Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x82 EP 2 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 32 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x01 EP 1 OUT bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 16 Device Status: 0x0000 (Bus Powered) fwupd-2.0.10/plugins/ebitdo/data/scf30.txt000066400000000000000000000106001501337203100202260ustar00rootroot00000000000000 Bus 002 Device 053: ID 1235:ab21 Focusrite-Novation Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 0 bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 64 idVendor 0x1235 Focusrite-Novation idProduct 0xab21 bcdDevice 0.01 iManufacturer 1 iProduct 2 iSerial 0 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 41 bNumInterfaces 1 bConfigurationValue 1 iConfiguration 0 bmAttributes 0xc0 Self Powered MaxPower 480mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 2 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 0 bInterfaceProtocol 0 iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.10 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 99 Report Descriptors: ** UNAVAILABLE ** Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0008 1x 8 bytes bInterval 32 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x02 EP 2 OUT bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0008 1x 8 bytes bInterval 16 Bus 002 Device 055: ID 0483:5750 STMicroelectronics Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 0 bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 64 idVendor 0x0483 STMicroelectronics idProduct 0x5750 bcdDevice 2.00 iManufacturer 1 iProduct 2 iSerial 3 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 41 bNumInterfaces 1 bConfigurationValue 1 iConfiguration 0 bmAttributes 0xc0 Self Powered MaxPower 480mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 2 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 0 bInterfaceProtocol 0 iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.10 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 33 Report Descriptors: ** UNAVAILABLE ** Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x82 EP 2 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 32 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x01 EP 1 OUT bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 16 fwupd-2.0.10/plugins/ebitdo/data/sf30pro.txt000066400000000000000000000303401501337203100206070ustar00rootroot00000000000000Start + Y --------- Bus 001 Device 008: ID 057e:2009 Nintendo Co., Ltd Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 0 (Defined at Interface level) bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 64 idVendor 0x057e Nintendo Co., Ltd idProduct 0x2009 bcdDevice 2.00 iManufacturer 1 Nintendo Co., Ltd. iProduct 2 Pro Controller iSerial 3 000000000001 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 41 bNumInterfaces 1 bConfigurationValue 1 iConfiguration 0 bmAttributes 0xa0 (Bus Powered) Remote Wakeup MaxPower 500mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 2 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 0 No Subclass bInterfaceProtocol 0 None iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.11 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 203 Report Descriptors: ** UNAVAILABLE ** Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 8 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x02 EP 2 OUT bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 8 Device Status: 0x0000 (Bus Powered) Start + B ------------ Bus 001 Device 009: ID 2dc8:6000 Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 0 (Defined at Interface level) bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 64 idVendor 0x2dc8 idProduct 0x6000 bcdDevice 0.01 iManufacturer 1 8Bitdo SF30 Pro iProduct 2 8Bitdo SF30 Pro iSerial 0 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 41 bNumInterfaces 1 bConfigurationValue 1 iConfiguration 0 bmAttributes 0xc0 Self Powered MaxPower 480mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 2 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 0 No Subclass bInterfaceProtocol 0 None iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.11 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 123 Report Descriptors: ** UNAVAILABLE ** Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 5 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x02 EP 2 OUT bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 16 Device Status: 0x0000 (Bus Powered) Start + A --------- Bus 001 Device 012: ID 054c:05c4 Sony Corp. DualShock 4 Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 0 (Defined at Interface level) bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 64 idVendor 0x054c Sony Corp. idProduct 0x05c4 DualShock 4 bcdDevice 1.00 iManufacturer 1 Sony Computer Entertainment iProduct 2 Wireless Controller iSerial 0 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 41 bNumInterfaces 1 bConfigurationValue 1 iConfiguration 0 bmAttributes 0xc0 Self Powered MaxPower 500mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 2 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 0 No Subclass bInterfaceProtocol 0 None iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.11 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 499 Report Descriptors: ** UNAVAILABLE ** Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x84 EP 4 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 5 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x03 EP 3 OUT bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 5 Device Status: 0x0000 (Bus Powered) Start + X --------- Bus 001 Device 013: ID 045e:028e Microsoft Corp. Xbox360 Controller Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 255 Vendor Specific Class bDeviceSubClass 255 Vendor Specific Subclass bDeviceProtocol 255 Vendor Specific Protocol bMaxPacketSize0 8 idVendor 0x045e Microsoft Corp. idProduct 0x028e Xbox360 Controller bcdDevice 1.14 iManufacturer 1 8Bitdo SF30 Pro iProduct 2 Controller iSerial 3 (error) bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 153 bNumInterfaces 4 bConfigurationValue 1 iConfiguration 0 bmAttributes 0xa0 (Bus Powered) Remote Wakeup MaxPower 500mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 2 bInterfaceClass 255 Vendor Specific Class bInterfaceSubClass 93 bInterfaceProtocol 1 iInterface 0 ** UNRECOGNIZED: 11 21 00 01 01 25 81 14 00 00 00 00 13 01 08 00 00 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0020 1x 32 bytes bInterval 4 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x01 EP 1 OUT bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0020 1x 32 bytes bInterval 8 Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 1 bAlternateSetting 0 bNumEndpoints 4 bInterfaceClass 255 Vendor Specific Class bInterfaceSubClass 93 bInterfaceProtocol 3 iInterface 0 ** UNRECOGNIZED: 1b 21 00 01 01 01 82 40 01 02 20 16 83 00 00 00 00 00 00 16 03 00 00 00 00 00 00 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x82 EP 2 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0020 1x 32 bytes bInterval 2 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x02 EP 2 OUT bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0020 1x 32 bytes bInterval 4 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x83 EP 3 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0020 1x 32 bytes bInterval 64 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x03 EP 3 OUT bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0020 1x 32 bytes bInterval 16 Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 2 bAlternateSetting 0 bNumEndpoints 1 bInterfaceClass 255 Vendor Specific Class bInterfaceSubClass 93 bInterfaceProtocol 2 iInterface 0 ** UNRECOGNIZED: 09 21 00 01 01 22 84 07 00 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x84 EP 4 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0020 1x 32 bytes bInterval 16 Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 3 bAlternateSetting 0 bNumEndpoints 0 bInterfaceClass 255 Vendor Specific Class bInterfaceSubClass 253 bInterfaceProtocol 19 iInterface 4 µ∡H釰俸샴`翰⣸贠øĀ贤Ǹɀ败˸èµÏ¸æ¡€Fã°áŸ¸è´ Ã¸è´€Ç¸èµ€Ë¸èµÏ¸æ¡€F⟰ ** UNRECOGNIZED: 06 41 00 01 01 03 Device Status: 0x0000 (Bus Powered) fwupd-2.0.10/plugins/ebitdo/ebitdo-legacy.quirk000066400000000000000000000005571501337203100214350ustar00rootroot00000000000000# This is the ID assigned to STMicroelectronics, and also seems to be used by other vendors who # did not change the default from the devkit. Install this quirk file if your 8bitdo controller # uses the legacy bootloader from 2018. # # See https://github.com/fwupd/fwupd/issues/4180 for more information [USB\VID_0483&PID_5750] Plugin = ebitdo Flags = is-bootloader fwupd-2.0.10/plugins/ebitdo/ebitdo.quirk000066400000000000000000000040721501337203100201670ustar00rootroot00000000000000# bootloader [USB\VID_2DC8&PID_5750] Plugin = ebitdo Flags = is-bootloader InstallDuration = 120 [USB\VID_0483&PID_5760] Plugin = ebitdo Flags = is-bootloader,will-disappear InstallDuration = 120 # FC30 [USB\VID_1235&PID_AB11] Plugin = ebitdo Flags = ~is-bootloader [USB\VID_2DC8&PID_AB11] Plugin = ebitdo Flags = ~is-bootloader InstallDuration = 120 # NES30 [USB\VID_1235&PID_AB12] Plugin = ebitdo Flags = ~is-bootloader [USB\VID_2DC8&PID_AB12] Plugin = ebitdo Flags = ~is-bootloader InstallDuration = 120 # SFC30 [USB\VID_1235&PID_AB21] Plugin = ebitdo Flags = ~is-bootloader [USB\VID_2DC8&PID_AB21] Plugin = ebitdo Flags = ~is-bootloader InstallDuration = 120 # SNES30 [USB\VID_1235&PID_AB20] Plugin = ebitdo Flags = ~is-bootloader [USB\VID_2DC8&PID_AB20] Plugin = ebitdo Flags = ~is-bootloader InstallDuration = 120 # FC30PRO [USB\VID_1002&PID_9000] Plugin = ebitdo Flags = ~is-bootloader [USB\VID_2DC8&PID_9000] Plugin = ebitdo Flags = ~is-bootloader InstallDuration = 120 # NES30PRO [USB\VID_2002&PID_9000] Plugin = ebitdo Flags = will-disappear [USB\VID_2DC8&PID_9001] Plugin = ebitdo Flags = will-disappear InstallDuration = 120 # FC30_ARCADE [USB\VID_8000&PID_1002] Plugin = ebitdo Flags = ~is-bootloader [USB\VID_2DC8&PID_1002] Plugin = ebitdo Flags = ~is-bootloader InstallDuration = 120 # SF30 PRO/SN30 PRO ## Dinput mode (Start + B) [USB\VID_2DC8&PID_6000] Plugin = ebitdo Flags = will-disappear [USB\VID_2DC8&PID_6001] Plugin = ebitdo Flags = will-disappear InstallDuration = 120 # SN30 PRO+ ## Dinput mode (Start + B) [USB\VID_2DC8&PID_6002] Plugin = ebitdo Flags = will-disappear InstallDuration = 120 # M30 [USB\VID_2DC8&PID_5006] Plugin = ebitdo Flags = ~is-bootloader InstallDuration = 120 # SN30v2 [USB\VID_2DC8&PID_9012] Plugin = ebitdo Flags = ~is-bootloader InstallDuration = 120 # SN30 Pro for Android [USB\VID_2DC8&PID_2100] Plugin = ebitdo Flags = ~is-bootloader InstallDuration = 120 [USB\VID_2DC8&PID_2101] Plugin = ebitdo Flags = ~is-bootloader InstallDuration = 120 # N30 Pro 2 [USB\VID_2DC8&PID_9015] Plugin = ebitdo Flags = ~is-bootloader InstallDuration = 120 fwupd-2.0.10/plugins/ebitdo/fu-ebitdo-device.c000066400000000000000000000510711501337203100211240ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-ebitdo-device.h" #include "fu-ebitdo-firmware.h" #include "fu-ebitdo-struct.h" struct _FuEbitdoDevice { FuUsbDevice parent_instance; guint32 serial[9]; }; G_DEFINE_TYPE(FuEbitdoDevice, fu_ebitdo_device, FU_TYPE_USB_DEVICE) #define FU_EBITDO_USB_TIMEOUT 5000 /* ms */ #define FU_EBITDO_USB_BOOTLOADER_EP_IN 0x82 #define FU_EBITDO_USB_BOOTLOADER_EP_OUT 0x01 #define FU_EBITDO_USB_RUNTIME_EP_IN 0x81 #define FU_EBITDO_USB_RUNTIME_EP_OUT 0x02 #define FU_EBITDO_USB_EP_SIZE 64 /* bytes */ static gboolean fu_ebitdo_device_send(FuEbitdoDevice *self, FuEbitdoPktType type, FuEbitdoPktCmd subtype, FuEbitdoPktCmd cmd, const guint8 *in, gsize in_len, GError **error) { gsize actual_length; guint8 ep_out = FU_EBITDO_USB_RUNTIME_EP_OUT; g_autoptr(GByteArray) st_hdr = fu_struct_ebitdo_pkt_new(); g_autoptr(GError) error_local = NULL; fu_byte_array_set_size(st_hdr, FU_EBITDO_USB_EP_SIZE, 0x0); /* different */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) ep_out = FU_EBITDO_USB_BOOTLOADER_EP_OUT; /* check size */ if (in_len > 64 - 8) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "input buffer too large"); return FALSE; } /* packet[0] is the total length of the packet */ fu_struct_ebitdo_pkt_set_type(st_hdr, type); fu_struct_ebitdo_pkt_set_subtype(st_hdr, subtype); /* do we have a payload */ if (in_len > 0) { fu_struct_ebitdo_pkt_set_cmd_len(st_hdr, in_len + 3); fu_struct_ebitdo_pkt_set_cmd(st_hdr, cmd); fu_struct_ebitdo_pkt_set_payload_len(st_hdr, in_len); if (!fu_memcpy_safe(st_hdr->data, st_hdr->len, FU_STRUCT_EBITDO_PKT_SIZE, /* dst */ in, in_len, 0x0, /* src */ in_len, error)) return FALSE; fu_struct_ebitdo_pkt_set_pkt_len(st_hdr, in_len + 7); } else { fu_struct_ebitdo_pkt_set_cmd_len(st_hdr, in_len + 1); fu_struct_ebitdo_pkt_set_cmd(st_hdr, cmd); fu_struct_ebitdo_pkt_set_pkt_len(st_hdr, 5); } fu_dump_raw(G_LOG_DOMAIN, "->DEVICE", st_hdr->data, st_hdr->len); /* get data from device */ if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), ep_out, st_hdr->data, st_hdr->len, &actual_length, FU_EBITDO_USB_TIMEOUT, NULL, /* cancellable */ &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to send to device on ep 0x%02x: %s", (guint)FU_EBITDO_USB_BOOTLOADER_EP_OUT, error_local->message); return FALSE; } return TRUE; } static gboolean fu_ebitdo_device_receive(FuEbitdoDevice *self, guint8 *out, gsize out_len, GError **error) { guint8 packet[FU_EBITDO_USB_EP_SIZE] = {0}; gsize actual_length; guint8 ep_in = FU_EBITDO_USB_RUNTIME_EP_IN; g_autoptr(GByteArray) st_hdr = NULL; g_autoptr(GError) error_local = NULL; /* different */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) ep_in = FU_EBITDO_USB_BOOTLOADER_EP_IN; /* get data from device */ if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), ep_in, packet, sizeof(packet), &actual_length, FU_EBITDO_USB_TIMEOUT, NULL, /* cancellable */ &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to retrieve from device on ep 0x%02x: %s", (guint)ep_in, error_local->message); return FALSE; } /* debug */ fu_dump_raw(G_LOG_DOMAIN, "<-DEVICE", packet, actual_length); st_hdr = fu_struct_ebitdo_pkt_parse(packet, sizeof(packet), 0x0, error); if (st_hdr == NULL) return FALSE; /* get-version (bootloader) */ if (fu_struct_ebitdo_pkt_get_type(st_hdr) == FU_EBITDO_PKT_TYPE_USER_CMD && fu_struct_ebitdo_pkt_get_subtype(st_hdr) == FU_EBITDO_PKT_CMD_UPDATE_FIRMWARE_DATA && fu_struct_ebitdo_pkt_get_cmd(st_hdr) == FU_EBITDO_PKT_CMD_FW_GET_VERSION) { if (out != NULL) { if (fu_struct_ebitdo_pkt_get_payload_len(st_hdr) < out_len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "payload too small, expected %" G_GSIZE_FORMAT " got %u", out_len, fu_struct_ebitdo_pkt_get_payload_len(st_hdr)); return FALSE; } if (!fu_memcpy_safe(out, out_len, 0x0, /* dst */ packet, sizeof(packet), FU_STRUCT_EBITDO_PKT_SIZE, /* src */ out_len, error)) return FALSE; } return TRUE; } /* get-version (firmware) -- not a packet, just raw data! */ if (fu_struct_ebitdo_pkt_get_pkt_len(st_hdr) == FU_EBITDO_PKT_CMD_GET_VERSION_RESPONSE) { if (out != NULL) { if (out_len != 4) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "outbuf size wrong, expected 4 got %" G_GSIZE_FORMAT, out_len); return FALSE; } if (!fu_memcpy_safe(out, out_len, 0x0, /* dst */ packet, sizeof(packet), 0x1, /* src */ 4, error)) return FALSE; } return TRUE; } /* verification-id response */ if (fu_struct_ebitdo_pkt_get_type(st_hdr) == FU_EBITDO_PKT_TYPE_USER_CMD && fu_struct_ebitdo_pkt_get_subtype(st_hdr) == FU_EBITDO_PKT_CMD_VERIFICATION_ID) { if (out != NULL) { if (fu_struct_ebitdo_pkt_get_cmd_len(st_hdr) != out_len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "outbuf size wrong, expected %" G_GSIZE_FORMAT " got %i", out_len, fu_struct_ebitdo_pkt_get_cmd_len(st_hdr)); return FALSE; } if (!fu_memcpy_safe(out, out_len, 0x0, /* dst */ packet, sizeof(packet), FU_STRUCT_EBITDO_PKT_SIZE - 3, /* src */ fu_struct_ebitdo_pkt_get_cmd_len(st_hdr), error)) return FALSE; } return TRUE; } /* update-firmware-data */ if (fu_struct_ebitdo_pkt_get_type(st_hdr) == FU_EBITDO_PKT_TYPE_USER_CMD && fu_struct_ebitdo_pkt_get_subtype(st_hdr) == FU_EBITDO_PKT_CMD_UPDATE_FIRMWARE_DATA && fu_struct_ebitdo_pkt_get_payload_len(st_hdr) == 0x00) { if (fu_struct_ebitdo_pkt_get_cmd(st_hdr) != FU_EBITDO_PKT_CMD_ACK) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "write failed, got %s", fu_ebitdo_pkt_cmd_to_string(fu_struct_ebitdo_pkt_get_cmd(st_hdr))); return FALSE; } return TRUE; } /* unhandled */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unexpected device response"); return FALSE; } static gchar * fu_ebitdo_device_convert_version(FuDevice *device, guint64 version_raw) { return g_strdup_printf("%u.%02u", (guint)version_raw / 100, (guint)version_raw % 100); } static gboolean fu_ebitdo_device_validate(FuEbitdoDevice *self, GError **error) { const gchar *ven; const gchar *allowlist[] = {"8Bitdo", "8BitDo", "SFC30", NULL}; /* this is a new, always valid, VID */ if (fu_device_get_vid(FU_DEVICE(self)) == 0x2dc8) return TRUE; /* verify the vendor prefix against a allowlist */ ven = fu_device_get_vendor(FU_DEVICE(self)); if (ven == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "could not check vendor descriptor: "); return FALSE; } for (guint i = 0; allowlist[i] != NULL; i++) { if (g_str_has_prefix(ven, allowlist[i])) return TRUE; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "vendor '%s' did not match allowlist, " "probably not a 8BitDo device…", ven); return FALSE; } static gboolean fu_ebitdo_device_open(FuDevice *device, GError **error) { FuEbitdoDevice *self = FU_EBITDO_DEVICE(device); /* FuUsbDevice->open */ if (!FU_DEVICE_CLASS(fu_ebitdo_device_parent_class)->open(device, error)) return FALSE; /* open, then ensure this is actually 8BitDo hardware */ if (!fu_ebitdo_device_validate(self, error)) return FALSE; if (!fu_usb_device_claim_interface(FU_USB_DEVICE(self), 0, /* 0 = idx? */ FU_USB_DEVICE_CLAIM_FLAG_KERNEL_DRIVER, error)) { return FALSE; } /* success */ return TRUE; } static gboolean fu_ebitdo_device_setup(FuDevice *device, GError **error) { FuEbitdoDevice *self = FU_EBITDO_DEVICE(device); guint32 version_tmp = 0; guint32 serial_tmp[9] = {0}; /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_ebitdo_device_parent_class)->setup(device, error)) return FALSE; /* in firmware mode */ if (!fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { if (!fu_ebitdo_device_send(self, FU_EBITDO_PKT_TYPE_USER_CMD, FU_EBITDO_PKT_CMD_GET_VERSION, 0, NULL, 0, /* in */ error)) { return FALSE; } if (!fu_ebitdo_device_receive(self, (guint8 *)&version_tmp, sizeof(version_tmp), error)) { return FALSE; } fu_device_set_version_raw(FU_DEVICE(self), GUINT32_FROM_LE(version_tmp)); /* nocheck:blocked */ return TRUE; } /* get version */ if (!fu_ebitdo_device_send(self, FU_EBITDO_PKT_TYPE_USER_CMD, FU_EBITDO_PKT_CMD_UPDATE_FIRMWARE_DATA, FU_EBITDO_PKT_CMD_FW_GET_VERSION, NULL, 0, /* in */ error)) { return FALSE; } if (!fu_ebitdo_device_receive(self, (guint8 *)&version_tmp, sizeof(version_tmp), error)) { return FALSE; } fu_device_set_version_raw(device, GUINT32_FROM_LE(version_tmp)); /* nocheck:blocked */ /* get verification ID */ if (!fu_ebitdo_device_send(self, FU_EBITDO_PKT_TYPE_USER_CMD, FU_EBITDO_PKT_CMD_GET_VERIFICATION_ID, 0x00, /* cmd */ NULL, 0, error)) { return FALSE; } if (!fu_ebitdo_device_receive(self, (guint8 *)&serial_tmp, sizeof(serial_tmp), error)) { return FALSE; } for (guint i = 0; i < 9; i++) self->serial[i] = GUINT32_FROM_LE(serial_tmp[i]); /* nocheck:blocked */ /* success */ return TRUE; } static gboolean fu_ebitdo_device_detach(FuDevice *device, FuProgress *progress, GError **error) { g_autoptr(FwupdRequest) request = fwupd_request_new(); /* not required */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) return TRUE; /* generate a message if not already set from the metadata */ if (fu_device_get_update_message(device) == NULL) { g_autoptr(GString) msg = g_string_new(NULL); g_string_append(msg, "Not in bootloader mode: Disconnect the controller, "); switch (fu_device_get_pid(device)) { case 0xab11: /* FC30 */ case 0xab12: /* NES30 */ case 0xab21: /* SFC30 */ case 0xab20: /* SNES30 */ case 0x9012: /* SN30v2 */ g_string_append(msg, "hold down L+R+START for 3 seconds until " "both LED lights flashing, "); break; case 0x9000: /* FC30PRO */ case 0x9001: /* NES30PRO */ g_string_append(msg, "hold down RETURN+POWER for 3 seconds until " "both LED lights flashing, "); break; case 0x1002: /* FC30-ARCADE */ g_string_append(msg, "hold down L1+R1+HOME for 3 seconds until " "both blue LED and green LED blink, "); break; case 0x6000: /* SF30 pro: Dinput mode */ case 0x6001: /* SN30 pro: Dinput mode */ case 0x6002: /* SN30 pro+: Dinput mode */ case 0x028e: /* SF30/SN30 pro: Xinput mode */ case 0x5006: /* M30 */ g_string_append(msg, "press and hold L1+R1+START for 3 seconds " "until the LED on top blinks red, "); break; case 0x2100: /* SN30 for Android */ case 0x2101: /* SN30 for Android */ g_string_append(msg, "press and hold LB+RB+Xbox buttons " "both white LED and green LED blink, "); break; case 0x9015: /* N30 Pro 2 */ g_string_append(msg, "press and hold L1+R1+START buttons " "until the yellow LED blinks, "); break; default: g_string_append(msg, "do what it says in the manual, "); break; } g_string_append(msg, "then re-connect controller"); fu_device_set_update_message(device, msg->str); } /* wait */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* emit request */ fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_IMMEDIATE); fwupd_request_set_id(request, FWUPD_REQUEST_ID_REMOVE_REPLUG); fwupd_request_set_message(request, fu_device_get_update_message(device)); fwupd_request_set_image(request, fu_device_get_update_image(device)); return fu_device_emit_request(device, request, progress, error); } static gboolean fu_ebitdo_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuEbitdoDevice *self = FU_EBITDO_DEVICE(device); const guint8 *buf; gsize bufsz = 0; guint32 serial_new[3]; g_autoptr(GBytes) fw_hdr = NULL; g_autoptr(GInputStream) stream_payload = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(FuChunkArray) chunks = NULL; const guint32 app_key_index[16] = {0x186976e5, 0xcac67acd, 0x38f27fee, 0x0a4948f1, 0xb75b7753, 0x1f8ffa5c, 0xbff8cf43, 0xc4936167, 0x92bd03f0, 0x5573c6ed, 0x57d8845b, 0x827197ac, 0xb91901c9, 0x3917edfe, 0xbcd6344f, 0xcf9e23b5}; /* not in bootloader mode */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NEEDS_USER_ACTION, "Not in bootloader mode"); return FALSE; } /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "header"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 97, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 2, NULL); /* get header and payload */ fw_hdr = fu_firmware_get_image_by_id_bytes(firmware, FU_FIRMWARE_ID_HEADER, error); if (fw_hdr == NULL) return FALSE; stream_payload = fu_firmware_get_stream(firmware, error); if (stream_payload == NULL) return FALSE; /* set up the firmware header */ buf = g_bytes_get_data(fw_hdr, &bufsz); if (!fu_ebitdo_device_send(self, FU_EBITDO_PKT_TYPE_USER_CMD, FU_EBITDO_PKT_CMD_UPDATE_FIRMWARE_DATA, FU_EBITDO_PKT_CMD_FW_UPDATE_HEADER, buf, bufsz, error)) { g_prefix_error(error, "failed to set up firmware header: "); return FALSE; } if (!fu_ebitdo_device_receive(self, NULL, 0, error)) { g_prefix_error(error, "failed to get ACK for fw update header: "); return FALSE; } fu_progress_step_done(progress); /* flash the firmware in 32 byte blocks */ chunks = fu_chunk_array_new_from_stream(stream_payload, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 32, error); if (chunks == NULL) return FALSE; for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; g_debug("writing %u bytes to 0x%04x of 0x%04x", (guint)fu_chunk_get_data_sz(chk), (guint)fu_chunk_get_address(chk), (guint)fu_chunk_get_data_sz(chk)); if (!fu_ebitdo_device_send(self, FU_EBITDO_PKT_TYPE_USER_CMD, FU_EBITDO_PKT_CMD_UPDATE_FIRMWARE_DATA, FU_EBITDO_PKT_CMD_FW_UPDATE_DATA, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "failed to write firmware @0x%04x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } if (!fu_ebitdo_device_receive(self, NULL, 0, error)) { g_prefix_error(error, "failed to get ACK for write firmware @0x%04x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } fu_progress_set_percentage_full(fu_progress_get_child(progress), i + 1, fu_chunk_array_length(chunks)); } fu_progress_step_done(progress); /* set the "encode id" which is likely a checksum, bluetooth pairing * or maybe just security-through-obscurity -- also note: * SET_ENCODE_ID enforces no read for success?! */ serial_new[0] = self->serial[0] ^ app_key_index[self->serial[0] & 0x0f]; serial_new[1] = self->serial[1] ^ app_key_index[self->serial[1] & 0x0f]; serial_new[2] = self->serial[2] ^ app_key_index[self->serial[2] & 0x0f]; if (!fu_ebitdo_device_send(self, FU_EBITDO_PKT_TYPE_USER_CMD, FU_EBITDO_PKT_CMD_UPDATE_FIRMWARE_DATA, FU_EBITDO_PKT_CMD_FW_SET_ENCODE_ID, (guint8 *)serial_new, sizeof(serial_new), error)) { g_prefix_error(error, "failed to set encoding ID: "); return FALSE; } /* mark flash as successful */ if (!fu_ebitdo_device_send(self, FU_EBITDO_PKT_TYPE_USER_CMD, FU_EBITDO_PKT_CMD_UPDATE_FIRMWARE_DATA, FU_EBITDO_PKT_CMD_FW_UPDATE_OK, NULL, 0, error)) { g_prefix_error(error, "failed to mark firmware as successful: "); return FALSE; } if (!fu_ebitdo_device_receive(self, NULL, 0, &error_local)) { g_prefix_error(&error_local, "failed to get ACK for mark firmware as successful: "); if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_WILL_DISAPPEAR)) { fu_device_set_remove_delay(device, 0); g_debug("%s", error_local->message); return TRUE; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } fu_progress_step_done(progress); /* success! */ return TRUE; } static gboolean fu_ebitdo_device_attach(FuDevice *device, FuProgress *progress, GError **error) { g_autoptr(GError) error_local = NULL; /* when doing a soft-reboot the device does not re-enumerate properly * so manually reboot the FuUsbDevice */ if (!fu_usb_device_reset(FU_USB_DEVICE(device), &error_local)) { g_prefix_error(&error_local, "failed to force-reset device: "); if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_WILL_DISAPPEAR)) { fu_device_set_remove_delay(device, 0); g_debug("%s", error_local->message); return TRUE; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } /* not all 8bito devices come back in the right mode */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_WILL_DISAPPEAR)) fu_device_set_remove_delay(device, 0); else fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* success! */ return TRUE; } static gboolean fu_ebitdo_device_probe(FuDevice *device, GError **error) { /* allowed, but requires manual bootloader step */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_set_remove_delay(device, FU_DEVICE_REMOVE_DELAY_USER_REPLUG); /* set name and vendor */ fu_device_set_summary(device, "A redesigned classic game controller"); fu_device_set_vendor(device, "8BitDo"); /* add a hardcoded icon name */ fu_device_add_icon(device, FU_DEVICE_ICON_INPUT_GAMING); /* only the bootloader can do the update */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { fu_device_add_instance_id_full(device, "USB\\VID_0483&PID_5750", FU_DEVICE_INSTANCE_FLAG_COUNTERPART); fu_device_add_instance_id_full(device, "USB\\VID_2DC8&PID_5750", FU_DEVICE_INSTANCE_FLAG_COUNTERPART); } /* success */ return TRUE; } static void fu_ebitdo_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_NO_PROFILE); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 97, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_ebitdo_device_init(FuEbitdoDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "com.8bitdo"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_COUNTERPART_GUIDS); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_add_request_flag(FU_DEVICE(self), FWUPD_REQUEST_FLAG_NON_GENERIC_MESSAGE); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_EBITDO_FIRMWARE); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PAIR); } static void fu_ebitdo_device_class_init(FuEbitdoDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_ebitdo_device_write_firmware; device_class->setup = fu_ebitdo_device_setup; device_class->detach = fu_ebitdo_device_detach; device_class->attach = fu_ebitdo_device_attach; device_class->open = fu_ebitdo_device_open; device_class->probe = fu_ebitdo_device_probe; device_class->set_progress = fu_ebitdo_device_set_progress; device_class->convert_version = fu_ebitdo_device_convert_version; } fwupd-2.0.10/plugins/ebitdo/fu-ebitdo-device.h000066400000000000000000000004531501337203100211270ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_EBITDO_DEVICE (fu_ebitdo_device_get_type()) G_DECLARE_FINAL_TYPE(FuEbitdoDevice, fu_ebitdo_device, FU, EBITDO_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/ebitdo/fu-ebitdo-firmware.c000066400000000000000000000063161501337203100215030ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-ebitdo-firmware.h" #include "fu-ebitdo-struct.h" struct _FuEbitdoFirmware { FuFirmwareClass parent_instance; }; G_DEFINE_TYPE(FuEbitdoFirmware, fu_ebitdo_firmware, FU_TYPE_FIRMWARE) static gboolean fu_ebitdo_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { guint32 payload_len; guint32 version; gsize streamsz = 0; g_autoptr(FuFirmware) img_hdr = fu_firmware_new(); g_autoptr(GByteArray) st = NULL; g_autoptr(GInputStream) stream_hdr = NULL; g_autoptr(GInputStream) stream_payload = NULL; /* check the file size */ st = fu_struct_ebitdo_hdr_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; payload_len = (guint32)(streamsz - st->len); if (payload_len != fu_struct_ebitdo_hdr_get_destination_len(st)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "file size incorrect, expected 0x%04x got 0x%04x", (guint)fu_struct_ebitdo_hdr_get_destination_len(st), (guint)payload_len); return FALSE; } /* parse version */ version = fu_struct_ebitdo_hdr_get_version(st); fu_firmware_set_version_raw(firmware, version); /* add header */ stream_hdr = fu_partial_input_stream_new(stream, 0x0, st->len, error); if (stream_hdr == NULL) return FALSE; if (!fu_firmware_parse_stream(img_hdr, stream_hdr, 0x0, flags, error)) return FALSE; fu_firmware_set_id(img_hdr, FU_FIRMWARE_ID_HEADER); fu_firmware_add_image(firmware, img_hdr); /* add payload */ stream_payload = fu_partial_input_stream_new(stream, st->len, payload_len, error); if (stream_payload == NULL) return FALSE; if (!fu_firmware_set_stream(firmware, stream_payload, error)) return FALSE; fu_firmware_set_id(firmware, FU_FIRMWARE_ID_PAYLOAD); fu_firmware_set_addr(firmware, fu_struct_ebitdo_hdr_get_destination_addr(st)); return TRUE; } static GByteArray * fu_ebitdo_firmware_write(FuFirmware *firmware, GError **error) { g_autoptr(GByteArray) st = fu_struct_ebitdo_hdr_new(); g_autoptr(GBytes) blob = NULL; /* header then payload */ blob = fu_firmware_get_bytes_with_patches(firmware, error); if (blob == NULL) return NULL; fu_struct_ebitdo_hdr_set_version(st, fu_firmware_get_version_raw(firmware)); fu_struct_ebitdo_hdr_set_destination_addr(st, fu_firmware_get_addr(firmware)); fu_struct_ebitdo_hdr_set_destination_len(st, g_bytes_get_size(blob)); fu_byte_array_append_bytes(st, blob); return g_steal_pointer(&st); } static gchar * fu_ebitdo_firmware_convert_version(FuFirmware *firmware, guint64 version_raw) { return g_strdup_printf("%.2f", version_raw / 100.f); } static void fu_ebitdo_firmware_init(FuEbitdoFirmware *self) { fu_firmware_set_version_format(FU_FIRMWARE(self), FWUPD_VERSION_FORMAT_PAIR); } static void fu_ebitdo_firmware_class_init(FuEbitdoFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->convert_version = fu_ebitdo_firmware_convert_version; firmware_class->parse = fu_ebitdo_firmware_parse; firmware_class->write = fu_ebitdo_firmware_write; } fwupd-2.0.10/plugins/ebitdo/fu-ebitdo-firmware.h000066400000000000000000000004641501337203100215060ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_EBITDO_FIRMWARE (fu_ebitdo_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuEbitdoFirmware, fu_ebitdo_firmware, FU, EBITDO_FIRMWARE, FuFirmware) fwupd-2.0.10/plugins/ebitdo/fu-ebitdo-plugin.c000066400000000000000000000014641501337203100211640ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-ebitdo-device.h" #include "fu-ebitdo-firmware.h" #include "fu-ebitdo-plugin.h" struct _FuEbitdoPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuEbitdoPlugin, fu_ebitdo_plugin, FU_TYPE_PLUGIN) static void fu_ebitdo_plugin_init(FuEbitdoPlugin *self) { } static void fu_ebitdo_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_EBITDO_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_EBITDO_FIRMWARE); } static void fu_ebitdo_plugin_class_init(FuEbitdoPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_ebitdo_plugin_constructed; } fwupd-2.0.10/plugins/ebitdo/fu-ebitdo-plugin.h000066400000000000000000000003541501337203100211660ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuEbitdoPlugin, fu_ebitdo_plugin, FU, EBITDO_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/ebitdo/fu-ebitdo.rs000066400000000000000000000032051501337203100200650ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructEbitdoHdr { version: u32le, destination_addr: u32le, destination_len: u32le, reserved: [u32le; 4], } #[repr(u8)] enum FuEbitdoPktType { UserCmd = 0x00, UserData = 0x01, MidCmd = 0x02, } #[derive(ToString)] #[repr(u8)] enum FuEbitdoPktCmd { FwUpdateData = 0x00, // update firmware data FwUpdateHeader = 0x01, // update firmware header FwUpdateOk = 0x02, // mark update as successful FwUpdateError = 0x03, // update firmware error FwGetVersion = 0x04, // get cur firmware vision FwSetVersion = 0x05, // set firmware version FwSetEncodeId = 0x06, // set app firmware encode ID Ack = 0x14, // acknowledge Nak = 0x15, // negative acknowledge UpdateFirmwareData = 0x16, // update firmware data TransferAbort = 0x18, // aborts transfer VerificationId = 0x19, // verification id (only BT?) GetVerificationId = 0x1a, // verification id (only BT) VerifyError = 0x1b, // verification error VerifyOk = 0x1c, // verification successful TransferTimeout = 0x1d, // send or receive data timeout GetVersion = 0x21, // get fw ver joystick mode GetVersionResponse = 0x22, // get fw version response } #[derive(New, Parse)] #[repr(C, packed)] struct FuStructEbitdoPkt { pkt_len: u8, type: FuEbitdoPktType, subtype: u8, cmd_len: u16le, cmd: FuEbitdoPktCmd, payload_len: u16le, // optional } fwupd-2.0.10/plugins/ebitdo/meson.build000066400000000000000000000012001501337203100177740ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginEbitdo"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('ebitdo.quirk') plugin_builtins += static_library('fu_plugin_ebitdo', rustgen.process( 'fu-ebitdo.rs', # fuzzing ), sources: [ 'fu-ebitdo-plugin.c', 'fu-ebitdo-device.c', 'fu-ebitdo-firmware.c', # fuzzing ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files('tests/8bitdo-nes30pro.json', 'tests/8bitdo-sf30pro.json', 'tests/8bitdo-sfc30.json', ) fwupd-2.0.10/plugins/ebitdo/tests/000077500000000000000000000000001501337203100170035ustar00rootroot00000000000000fwupd-2.0.10/plugins/ebitdo/tests/8bitdo-nes30pro.json000066400000000000000000000005541501337203100225420ustar00rootroot00000000000000{ "name": "8BitDo NES30Pro", "interactive": false, "steps": [ { "url": "5b7c4df860695bf000967e140682082b44cea2c033da27fabeffc91be5d2d30c-8Bitdo-SFC30PRO_NES30PRO-4.01.cab", "components": [ { "version": "4.01", "guids": [ "c6566b1b-0c6e-5d2e-9376-78c23ab57bf2" ] } ] } ] } fwupd-2.0.10/plugins/ebitdo/tests/8bitdo-sf30pro.json000066400000000000000000000005221501337203100223600ustar00rootroot00000000000000{ "name": "8BitDo SF30Pro", "interactive": true, "steps": [ { "url": "3d3a65ee2e8581647fb09d752fa7e21ee1566481-8Bitdo-SF30_Pro-SN30_Pro-1.26.cab", "components": [ { "version": "1.26", "guids": [ "269b3121-097b-50d8-b9ba-d1f64f9cd241" ] } ] } ] } fwupd-2.0.10/plugins/ebitdo/tests/8bitdo-sfc30.json000066400000000000000000000005271501337203100220070ustar00rootroot00000000000000{ "name": "8BitDo SFC30", "interactive": true, "steps": [ { "url": "fe066b57c69265f4cce8a999a5f8ab90d1c13b24-8Bitdo-SFC30_NES30_SFC30_SNES30-4.01.cab", "components": [ { "version": "4.01", "guids": [ "a7fcfbaf-e9e8-59f4-920d-7691dc6c8699" ] } ] } ] } fwupd-2.0.10/plugins/ebitdo/tests/ebitdo.builder.xml000066400000000000000000000002131501337203100224140ustar00rootroot00000000000000 0x10002 0x3000 aGVsbG8gd29ybGQ= fwupd-2.0.10/plugins/elan-kbd/000077500000000000000000000000001501337203100160505ustar00rootroot00000000000000fwupd-2.0.10/plugins/elan-kbd/README.md000066400000000000000000000024741501337203100173360ustar00rootroot00000000000000--- title: Plugin: ELAN Keyboard --- ## Introduction The ELAN keyboard interface is used by many OEMs, both in i2c mode and in USB mode -- although only updating over USB is supported by this plugin at this time. The microcontroller used is the EM85F684A which is loaded with an ELAN-specific bootloader. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a packed binary file format. This is split, with 0x2000 bytes for the bootloader, and 0x6000 for the application runtime code. This plugin supports the following protocol ID: * `com.elan.kbd` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_1018&PID_1006` ## Update Behavior The device flash is updated from a 0x2000 offset. The plugin will disconnect the device into a bootloader mode to perform the update. ## Vendor ID Security The vendor ID is set from the runtime USB vendor, in this instance set to `USB:0x1018` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `2.0.5`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Richard Hughes: @hughsie fwupd-2.0.10/plugins/elan-kbd/elan-kbd.quirk000066400000000000000000000004651501337203100206070ustar00rootroot00000000000000# StarLite Magnetic Keyboard [USB\VID_1018&PID_1006] Plugin = elan_kbd # Pine Phone Keyboard [USB\VID_04F3&PID_1910] Plugin = elan_kbd # ELAN debugger [USB\VID_04F3&PID_B001] Plugin = elan_kbd GType = FuElanKbdDebugDevice # ELAN bootloader [USB\VID_04F3&PID_0905] Plugin = elan_kbd GType = FuElanKbdDevice fwupd-2.0.10/plugins/elan-kbd/fu-elan-kbd-common.h000066400000000000000000000010461501337203100215750ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #define FU_ELAN_KBD_DEVICE_ADDR_BOOT 0x0 #define FU_ELAN_KBD_DEVICE_ADDR_APP 0x2000 #define FU_ELAN_KBD_DEVICE_ADDR_OPTION 0x8000 #define FU_ELAN_KBD_DEVICE_SIZE_ROM 0x8000 #define FU_ELAN_KBD_DEVICE_SIZE_BOOT (FU_ELAN_KBD_DEVICE_ADDR_APP - FU_ELAN_KBD_DEVICE_ADDR_BOOT) #define FU_ELAN_KBD_DEVICE_SIZE_APP (FU_ELAN_KBD_DEVICE_SIZE_ROM - FU_ELAN_KBD_DEVICE_ADDR_APP) #define FU_ELAN_KBD_DEVICE_SIZE_OPTION 0x100 fwupd-2.0.10/plugins/elan-kbd/fu-elan-kbd-debug-device.c000066400000000000000000000047011501337203100226240ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-elan-kbd-debug-device.h" #include "fu-elan-kbd-struct.h" struct _FuElanKbdDebugDevice { FuUsbDevice parent_instance; }; G_DEFINE_TYPE(FuElanKbdDebugDevice, fu_elan_kbd_debug_device, FU_TYPE_USB_DEVICE) static gboolean fu_elan_kbd_debug_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuElanKbdDebugDevice *self = FU_ELAN_KBD_DEBUG_DEVICE(device); g_autoptr(GError) error_local = NULL; guint8 buf[8] = {0x01}; if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), buf[0], buf, sizeof(buf), NULL, 1000, NULL, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_READ)) { g_debug("ignoring: %s", error_local->message); } else { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } /* success */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static void fu_elan_kbd_debug_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 57, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 43, "reload"); } static void fu_elan_kbd_debug_device_init(FuElanKbdDebugDevice *self) { fu_device_set_name(FU_DEVICE(self), "ELAN USB Keyboard (debug)"); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_add_protocol(FU_DEVICE(self), "com.elan.kbd"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_INPUT_KEYBOARD); } static void fu_elan_kbd_debug_device_class_init(FuElanKbdDebugDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->detach = fu_elan_kbd_debug_device_detach; device_class->set_progress = fu_elan_kbd_debug_device_set_progress; } fwupd-2.0.10/plugins/elan-kbd/fu-elan-kbd-debug-device.h000066400000000000000000000005551501337203100226340ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ELAN_KBD_DEBUG_DEVICE (fu_elan_kbd_debug_device_get_type()) G_DECLARE_FINAL_TYPE(FuElanKbdDebugDevice, fu_elan_kbd_debug_device, FU, ELAN_KBD_DEBUG_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/elan-kbd/fu-elan-kbd-device.c000066400000000000000000000532161501337203100215450ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-elan-kbd-common.h" #include "fu-elan-kbd-device.h" #include "fu-elan-kbd-firmware.h" #include "fu-elan-kbd-struct.h" struct _FuElanKbdDevice { FuUsbDevice parent_instance; guint16 ver_spec; FuElanKbdDevStatus status; FuElanKbdBootCond1 bootcond1; }; G_DEFINE_TYPE(FuElanKbdDevice, fu_elan_kbd_device, FU_TYPE_USB_DEVICE) #define FU_ELAN_KBD_DEVICE_EP_CMD_SIZE 4 #define FU_ELAN_KBD_DEVICE_EP_DATA_SIZE 64 static void fu_elan_kbd_device_to_string(FuDevice *device, guint idt, GString *str) { FuElanKbdDevice *self = FU_ELAN_KBD_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "VerSpec", self->ver_spec); fwupd_codec_string_append(str, idt, "Status", fu_elan_kbd_dev_status_to_string(self->status)); fwupd_codec_string_append(str, idt, "BootCond1", fu_elan_kbd_boot_cond1_to_string(self->bootcond1)); } static gboolean fu_elan_kbd_device_status_check(FuElanKbdDevice *self, GByteArray *buf, GError **error) { FuElanKbdStatus status; g_autoptr(FuStructElanKbdCmdStatusRes) st_res = NULL; st_res = fu_struct_elan_kbd_cmd_status_res_parse(buf->data, buf->len, 0x0, error); if (st_res == NULL) return FALSE; status = fu_struct_elan_kbd_cmd_status_res_get_value(st_res); if (status == FU_ELAN_KBD_STATUS_BUSY) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "busy"); return FALSE; } if (status == FU_ELAN_KBD_STATUS_FAIL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed, with unknown error"); return FALSE; } if (status == FU_ELAN_KBD_STATUS_ERROR) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed: %s", fu_elan_kbd_error_to_string( fu_struct_elan_kbd_cmd_status_res_get_error(st_res))); return FALSE; } return TRUE; } static GByteArray * fu_elan_kbd_device_cmd(FuElanKbdDevice *self, GByteArray *buf, GError **error) { g_autoptr(GByteArray) buf_out = g_byte_array_new(); fu_dump_raw(G_LOG_DOMAIN, "CmdReq", buf->data, buf->len); if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), FU_ELAN_KBD_EP_CMD, buf->data, buf->len, NULL, 1000, NULL, error)) { return NULL; } fu_byte_array_set_size(buf_out, FU_ELAN_KBD_DEVICE_EP_CMD_SIZE, 0x0); if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), FU_ELAN_KBD_EP_STATUS, buf_out->data, buf_out->len, NULL, 1000, NULL, error)) { return NULL; } fu_dump_raw(G_LOG_DOMAIN, "CmdRes", buf_out->data, buf_out->len); return g_steal_pointer(&buf_out); } static gboolean fu_elan_kbd_device_read_data(FuElanKbdDevice *self, guint8 *buf, gsize bufsz, gsize offset, GError **error) { g_autoptr(GByteArray) buf_mut = g_byte_array_new(); fu_byte_array_set_size(buf_mut, FU_ELAN_KBD_DEVICE_EP_DATA_SIZE, 0x0); if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), FU_ELAN_KBD_EP_DATA_IN, buf_mut->data, buf_mut->len, NULL, 1000, NULL, error)) { return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "DataRes", buf_mut->data, buf_mut->len); if (!fu_memcpy_safe(buf, bufsz, offset, buf_mut->data, buf_mut->len, 0x0, buf_mut->len, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_elan_kbd_device_write_data(FuElanKbdDevice *self, const guint8 *buf, gsize bufsz, gsize offset, GError **error) { g_autoptr(GByteArray) buf_mut = g_byte_array_new(); fu_byte_array_set_size(buf_mut, FU_ELAN_KBD_DEVICE_EP_DATA_SIZE, 0x0); if (!fu_memcpy_safe(buf_mut->data, buf_mut->len, 0x0, buf, bufsz, offset, buf_mut->len, error)) return FALSE; fu_dump_raw(G_LOG_DOMAIN, "DataReq", buf_mut->data, buf_mut->len); if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), FU_ELAN_KBD_EP_DATA_OUT, buf_mut->data, buf_mut->len, NULL, 1000, NULL, error)) { return FALSE; } /* success */ return TRUE; } static gboolean fu_elan_kbd_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuElanKbdDevice *self = FU_ELAN_KBD_DEVICE(device); g_autoptr(FuStructElanKbdSoftwareResetReq) st_req = fu_struct_elan_kbd_software_reset_req_new(); g_autoptr(GByteArray) buf = NULL; buf = fu_elan_kbd_device_cmd(self, st_req, error); if (buf == NULL) return FALSE; if (!fu_elan_kbd_device_status_check(self, buf, error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_elan_kbd_device_ensure_ver_spec(FuElanKbdDevice *self, GError **error) { g_autoptr(FuStructElanKbdGetVerSpecReq) st_req = fu_struct_elan_kbd_get_ver_spec_req_new(); g_autoptr(FuStructElanKbdGetVerSpecRes) st_res = NULL; g_autoptr(GByteArray) buf = NULL; buf = fu_elan_kbd_device_cmd(self, st_req, error); if (buf == NULL) return FALSE; st_res = fu_struct_elan_kbd_get_ver_spec_res_parse(buf->data, buf->len, 0x0, error); if (st_res == NULL) return FALSE; self->ver_spec = fu_struct_elan_kbd_get_ver_spec_res_get_value(st_res); return TRUE; } static gboolean fu_elan_kbd_device_ensure_ver_fw(FuElanKbdDevice *self, GError **error) { g_autoptr(FuStructElanKbdGetVerFwReq) st_req = fu_struct_elan_kbd_get_ver_fw_req_new(); g_autoptr(FuStructElanKbdGetVerFwRes) st_res = NULL; g_autofree gchar *version = NULL; g_autoptr(GByteArray) buf = NULL; buf = fu_elan_kbd_device_cmd(self, st_req, error); if (buf == NULL) return FALSE; st_res = fu_struct_elan_kbd_get_ver_fw_res_parse(buf->data, buf->len, 0x0, error); if (st_res == NULL) return FALSE; version = g_strdup_printf("%04x", fu_struct_elan_kbd_get_ver_fw_res_get_value(st_res)); fu_device_set_version_bootloader(FU_DEVICE(self), version); return TRUE; } static gboolean fu_elan_kbd_device_ensure_dev_status(FuElanKbdDevice *self, GError **error) { g_autoptr(FuStructElanKbdGetStatusReq) st_req = fu_struct_elan_kbd_get_status_req_new(); g_autoptr(FuStructElanKbdGetStatusRes) st_res = NULL; g_autoptr(GByteArray) buf = NULL; buf = fu_elan_kbd_device_cmd(self, st_req, error); if (buf == NULL) return FALSE; st_res = fu_struct_elan_kbd_get_status_res_parse(buf->data, buf->len, 0x0, error); if (st_res == NULL) return FALSE; self->status = fu_struct_elan_kbd_get_status_res_get_value(st_res); return TRUE; } static gboolean fu_elan_kbd_device_ensure_boot_cond1(FuElanKbdDevice *self, GError **error) { g_autoptr(FuStructElanKbdBootConditionReq) st_req = fu_struct_elan_kbd_boot_condition_req_new(); g_autoptr(FuStructElanKbdBootConditionRes) st_res = NULL; g_autoptr(GByteArray) buf = NULL; buf = fu_elan_kbd_device_cmd(self, st_req, error); if (buf == NULL) return FALSE; st_res = fu_struct_elan_kbd_boot_condition_res_parse(buf->data, buf->len, 0x0, error); if (st_res == NULL) return FALSE; self->bootcond1 = fu_struct_elan_kbd_boot_condition_res_get_value(st_res); return TRUE; } static gboolean fu_elan_kbd_device_abort(FuElanKbdDevice *self, GError **error) { g_autoptr(FuStructElanKbdAbortReq) st_req = fu_struct_elan_kbd_abort_req_new(); g_autoptr(GByteArray) buf = NULL; buf = fu_elan_kbd_device_cmd(self, st_req, error); if (buf == NULL) return FALSE; return fu_elan_kbd_device_status_check(self, buf, error); } static gboolean fu_elan_kbd_device_setup(FuDevice *device, GError **error) { FuElanKbdDevice *self = FU_ELAN_KBD_DEVICE(device); /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_elan_kbd_device_parent_class)->setup(device, error)) return FALSE; /* abort any transactions in-flight */ if (!fu_elan_kbd_device_abort(self, error)) return FALSE; /* get properties from the device while open */ if (!fu_elan_kbd_device_ensure_ver_spec(self, error)) return FALSE; if (!fu_elan_kbd_device_ensure_ver_fw(self, error)) return FALSE; if (!fu_elan_kbd_device_ensure_dev_status(self, error)) return FALSE; if (!fu_elan_kbd_device_ensure_boot_cond1(self, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_elan_kbd_device_cmd_read_rom_finished(FuElanKbdDevice *self, guint8 csum, GError **error) { g_autoptr(FuStructElanKbdReadRomFinishedReq) st_req = fu_struct_elan_kbd_read_rom_finished_req_new(); g_autoptr(GByteArray) buf = NULL; fu_struct_elan_kbd_read_rom_finished_req_set_csum(st_req, csum); buf = fu_elan_kbd_device_cmd(self, st_req, error); if (buf == NULL) return FALSE; return fu_elan_kbd_device_status_check(self, buf, error); } static GBytes * fu_elan_kbd_device_cmd_read_rom(FuElanKbdDevice *self, guint16 addr, guint16 len, FuProgress *progress, GError **error) { g_autoptr(FuStructElanKbdReadRomReq) st_req = fu_struct_elan_kbd_read_rom_req_new(); g_autoptr(GByteArray) buf = NULL; g_autofree guint8 *data = g_malloc0(len); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, len / FU_ELAN_KBD_DEVICE_EP_DATA_SIZE); /* set up read */ fu_struct_elan_kbd_read_rom_req_set_addr(st_req, addr); fu_struct_elan_kbd_read_rom_req_set_len(st_req, len); buf = fu_elan_kbd_device_cmd(self, st_req, error); if (buf == NULL) return NULL; if (!fu_elan_kbd_device_status_check(self, buf, error)) return NULL; for (gsize offset = 0; offset < len; offset += FU_ELAN_KBD_DEVICE_EP_DATA_SIZE) { if (!fu_elan_kbd_device_read_data(self, data, len, offset, error)) { g_prefix_error(error, "failed at 0x%x: ", (guint)offset); return NULL; } fu_progress_step_done(progress); } if (!fu_elan_kbd_device_cmd_read_rom_finished(self, fu_sum16(data, len), error)) return NULL; return g_bytes_new_take(g_steal_pointer(&data), len); } static gboolean fu_elan_kbd_device_cmd_read_option_finished(FuElanKbdDevice *self, guint8 csum, GError **error) { g_autoptr(FuStructElanKbdReadOptionFinishedReq) st_req = fu_struct_elan_kbd_read_option_finished_req_new(); g_autoptr(GByteArray) buf = NULL; fu_struct_elan_kbd_read_option_finished_req_set_csum(st_req, csum); buf = fu_elan_kbd_device_cmd(self, st_req, error); if (buf == NULL) return FALSE; return fu_elan_kbd_device_status_check(self, buf, error); } static GBytes * fu_elan_kbd_device_cmd_read_option(FuElanKbdDevice *self, FuProgress *progress, GError **error) { const gsize len = FU_STRUCT_ELAN_KBD_READ_OPTION_REQ_DEFAULT_LEN; g_autoptr(FuStructElanKbdReadOptionReq) st_req = fu_struct_elan_kbd_read_option_req_new(); g_autoptr(GByteArray) buf = NULL; g_autofree guint8 *data = g_malloc0(len); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, len / FU_ELAN_KBD_DEVICE_EP_DATA_SIZE); /* set up read */ buf = fu_elan_kbd_device_cmd(self, st_req, error); if (buf == NULL) return NULL; if (!fu_elan_kbd_device_status_check(self, buf, error)) return NULL; for (gsize offset = 0; offset < len; offset += FU_ELAN_KBD_DEVICE_EP_DATA_SIZE) { if (!fu_elan_kbd_device_read_data(self, data, len, offset, error)) { g_prefix_error(error, "failed at 0x%x: ", (guint)offset); return NULL; } fu_progress_step_done(progress); } if (!fu_elan_kbd_device_cmd_read_option_finished(self, fu_sum16(data, len), error)) return NULL; return g_bytes_new_take(g_steal_pointer(&data), len); } static GBytes * fu_elan_kbd_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuElanKbdDevice *self = FU_ELAN_KBD_DEVICE(device); return fu_elan_kbd_device_cmd_read_rom(self, 0x0, FU_ELAN_KBD_DEVICE_SIZE_ROM, progress, error); } static FuFirmware * fu_elan_kbd_device_read_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuElanKbdDevice *self = FU_ELAN_KBD_DEVICE(device); g_autoptr(FuFirmware) firmware = fu_elan_kbd_firmware_new(); g_autoptr(FuFirmware) img_app = NULL; g_autoptr(FuFirmware) img_bootloader = NULL; g_autoptr(FuFirmware) img_option = NULL; g_autoptr(GBytes) blob_app = NULL; g_autoptr(GBytes) blob_bootloader = NULL; g_autoptr(GBytes) blob_option = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 25, "bootloader"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 74, "app"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "option"); /* bootloader */ blob_bootloader = fu_elan_kbd_device_cmd_read_rom(self, FU_ELAN_KBD_DEVICE_ADDR_BOOT, FU_ELAN_KBD_DEVICE_SIZE_BOOT, fu_progress_get_child(progress), error); if (blob_bootloader == NULL) { g_prefix_error(error, "failed to read ROM: "); return NULL; } img_bootloader = fu_firmware_new_from_bytes(blob_bootloader); fu_firmware_set_id(img_bootloader, "bootloader"); fu_firmware_add_image(firmware, img_bootloader); fu_progress_step_done(progress); /* app */ blob_app = fu_elan_kbd_device_cmd_read_rom(self, FU_ELAN_KBD_DEVICE_ADDR_APP, FU_ELAN_KBD_DEVICE_SIZE_APP, fu_progress_get_child(progress), error); if (blob_app == NULL) { g_prefix_error(error, "failed to read ROM: "); return NULL; } img_app = fu_firmware_new_from_bytes(blob_app); fu_firmware_set_idx(img_app, FU_ELAN_KBD_FIRMWARE_IDX_APP); fu_firmware_add_image(firmware, img_app); fu_progress_step_done(progress); /* option */ blob_option = fu_elan_kbd_device_cmd_read_option(self, fu_progress_get_child(progress), error); if (blob_option == NULL) { g_prefix_error(error, "failed to read ROM: "); return NULL; } img_option = fu_firmware_new_from_bytes(blob_option); fu_firmware_set_idx(img_option, FU_ELAN_KBD_FIRMWARE_IDX_OPTION); fu_firmware_add_image(firmware, img_option); fu_progress_step_done(progress); /* success */ return g_steal_pointer(&firmware); } static gboolean fu_elan_kbd_device_cmd_get_auth_lock(FuElanKbdDevice *self, guint8 *key, GError **error) { g_autoptr(FuStructElanKbdGetAuthLockReq) st_req = fu_struct_elan_kbd_get_auth_lock_req_new(); g_autoptr(FuStructElanKbdGetAuthLockRes) st_res = NULL; g_autoptr(GByteArray) buf = NULL; buf = fu_elan_kbd_device_cmd(self, st_req, error); if (buf == NULL) return FALSE; st_res = fu_struct_elan_kbd_get_auth_lock_res_parse(buf->data, buf->len, 0x0, error); if (st_res == NULL) return FALSE; *key = fu_struct_elan_kbd_get_auth_lock_res_get_key(st_res) ^ 0x24; return TRUE; } static gboolean fu_elan_kbd_device_cmd_set_auth_lock(FuElanKbdDevice *self, guint8 key, GError **error) { g_autoptr(FuStructElanKbdSetAuthLockReq) st_req = fu_struct_elan_kbd_set_auth_lock_req_new(); g_autoptr(GByteArray) buf = NULL; fu_struct_elan_kbd_set_auth_lock_req_set_key(st_req, key ^ 0x58); buf = fu_elan_kbd_device_cmd(self, st_req, error); if (buf == NULL) return FALSE; return fu_elan_kbd_device_status_check(self, buf, error); } static gboolean fu_elan_kbd_device_cmd_unlock(FuElanKbdDevice *self, GError **error) { guint8 key = 0x0; if (!fu_elan_kbd_device_cmd_get_auth_lock(self, &key, error)) return FALSE; return fu_elan_kbd_device_cmd_set_auth_lock(self, key, error); } static gboolean fu_elan_kbd_device_cmd_entry_iap(FuElanKbdDevice *self, GError **error) { g_autoptr(FuStructElanKbdEntryIapReq) st_req = fu_struct_elan_kbd_entry_iap_req_new(); g_autoptr(GByteArray) buf = NULL; buf = fu_elan_kbd_device_cmd(self, st_req, error); if (buf == NULL) return FALSE; return fu_elan_kbd_device_status_check(self, buf, error); } static gboolean fu_elan_kbd_device_cmd_finished_iap(FuElanKbdDevice *self, GError **error) { g_autoptr(FuStructElanKbdFinishedIapReq) st_req = fu_struct_elan_kbd_finished_iap_req_new(); g_autoptr(GByteArray) buf = NULL; buf = fu_elan_kbd_device_cmd(self, st_req, error); if (buf == NULL) return FALSE; return fu_elan_kbd_device_status_check(self, buf, error); } static gboolean fu_elan_kbd_device_cmd_write_rom_finished(FuElanKbdDevice *self, guint8 csum, GError **error) { g_autoptr(FuStructElanKbdWriteRomFinishedReq) st_req = fu_struct_elan_kbd_write_rom_finished_req_new(); g_autoptr(GByteArray) buf = NULL; fu_struct_elan_kbd_write_rom_finished_req_set_csum(st_req, csum); buf = fu_elan_kbd_device_cmd(self, st_req, error); if (buf == NULL) return FALSE; return fu_elan_kbd_device_status_check(self, buf, error); } static gboolean fu_elan_kbd_device_cmd_write_rom(FuElanKbdDevice *self, guint16 addr, GBytes *blob, FuProgress *progress, GError **error) { gsize bufsz = 0; const guint8 *data = g_bytes_get_data(blob, &bufsz); g_autoptr(FuStructElanKbdWriteRomReq) st_req = fu_struct_elan_kbd_write_rom_req_new(); g_autoptr(GByteArray) buf = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, bufsz / FU_ELAN_KBD_DEVICE_EP_DATA_SIZE); /* set up write */ fu_struct_elan_kbd_write_rom_req_set_addr(st_req, addr); fu_struct_elan_kbd_write_rom_req_set_len(st_req, bufsz); buf = fu_elan_kbd_device_cmd(self, st_req, error); if (buf == NULL) return FALSE; if (!fu_elan_kbd_device_status_check(self, buf, error)) return FALSE; for (gsize offset = 0; offset < bufsz; offset += FU_ELAN_KBD_DEVICE_EP_DATA_SIZE) { if (!fu_elan_kbd_device_write_data(self, data, bufsz, offset, error)) { g_prefix_error(error, "failed at 0x%x: ", (guint)offset); return FALSE; } fu_progress_step_done(progress); } return fu_elan_kbd_device_cmd_write_rom_finished(self, fu_sum16_bytes(blob), error); } static gboolean fu_elan_kbd_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuElanKbdDevice *self = FU_ELAN_KBD_DEVICE(device); g_autoptr(GBytes) blob = NULL; g_autoptr(GBytes) blob_verify = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "unlock"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "entry-iap"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 96, "write-rom"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "finished"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 1, "verify"); /* unlock */ if (!fu_elan_kbd_device_cmd_unlock(self, error)) { g_prefix_error(error, "failed to unlock: "); return FALSE; } fu_progress_step_done(progress); /* enter IAP */ if (!fu_elan_kbd_device_cmd_entry_iap(self, error)) { g_prefix_error(error, "failed to entry IAP: "); return FALSE; } fu_progress_step_done(progress); /* write */ blob = fu_firmware_get_image_by_idx_bytes(firmware, FU_ELAN_KBD_FIRMWARE_IDX_APP, error); if (blob == NULL) return FALSE; if (!fu_elan_kbd_device_cmd_write_rom(self, FU_ELAN_KBD_DEVICE_ADDR_APP, blob, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to write ROM: "); return FALSE; } fu_progress_step_done(progress); /* finish IAP */ if (!fu_elan_kbd_device_cmd_finished_iap(self, error)) { g_prefix_error(error, "failed to finish IAP: "); return FALSE; } fu_progress_step_done(progress); /* verify */ blob_verify = fu_elan_kbd_device_cmd_read_rom(self, FU_ELAN_KBD_DEVICE_ADDR_APP, FU_ELAN_KBD_DEVICE_SIZE_APP, fu_progress_get_child(progress), error); if (blob_verify == NULL) { g_prefix_error(error, "failed to read ROM: "); return FALSE; } if (!fu_bytes_compare(blob, blob_verify, error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static void fu_elan_kbd_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 56, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 38, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 6, "reload"); } static void fu_elan_kbd_device_init(FuElanKbdDevice *self) { fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_NUMBER); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_set_name(FU_DEVICE(self), "ELAN USB Keyboard"); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_ELAN_KBD_FIRMWARE); fu_device_set_firmware_size_min(FU_DEVICE(self), FU_ELAN_KBD_DEVICE_EP_DATA_SIZE); fu_device_set_firmware_size_max(FU_DEVICE(self), FU_ELAN_KBD_DEVICE_SIZE_ROM + FU_ELAN_KBD_DEVICE_SIZE_OPTION); fu_device_add_protocol(FU_DEVICE(self), "com.elan.kbd"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_INPUT_KEYBOARD); fu_usb_device_add_interface(FU_USB_DEVICE(self), 0x01); fu_usb_device_add_interface(FU_USB_DEVICE(self), 0x02); fu_usb_device_add_interface(FU_USB_DEVICE(self), 0x03); } static void fu_elan_kbd_device_class_init(FuElanKbdDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_elan_kbd_device_to_string; device_class->setup = fu_elan_kbd_device_setup; device_class->attach = fu_elan_kbd_device_attach; device_class->write_firmware = fu_elan_kbd_device_write_firmware; device_class->read_firmware = fu_elan_kbd_device_read_firmware; device_class->dump_firmware = fu_elan_kbd_device_dump_firmware; device_class->set_progress = fu_elan_kbd_device_set_progress; } fwupd-2.0.10/plugins/elan-kbd/fu-elan-kbd-device.h000066400000000000000000000004641501337203100215470ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ELAN_KBD_DEVICE (fu_elan_kbd_device_get_type()) G_DECLARE_FINAL_TYPE(FuElanKbdDevice, fu_elan_kbd_device, FU, ELAN_KBD_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/elan-kbd/fu-elan-kbd-firmware.c000066400000000000000000000073211501337203100221160ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-elan-kbd-common.h" #include "fu-elan-kbd-firmware.h" #include "fu-elan-kbd-struct.h" struct _FuElanKbdFirmware { FuFirmware parent_instance; }; G_DEFINE_TYPE(FuElanKbdFirmware, fu_elan_kbd_firmware, FU_TYPE_FIRMWARE) static gboolean fu_elan_kbd_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_elan_kbd_firmware_validate_stream(stream, offset, error); } static gboolean fu_elan_kbd_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { g_autoptr(FuFirmware) firmware_app = fu_firmware_new(); g_autoptr(FuFirmware) firmware_bootloader = fu_firmware_new(); g_autoptr(FuFirmware) firmware_option = fu_firmware_new(); g_autoptr(GInputStream) stream_app = NULL; g_autoptr(GInputStream) stream_bootloader = NULL; g_autoptr(GInputStream) stream_option = NULL; /* bootloader */ stream_bootloader = fu_partial_input_stream_new(stream, FU_ELAN_KBD_DEVICE_ADDR_BOOT, FU_ELAN_KBD_DEVICE_SIZE_BOOT, error); if (stream_bootloader == NULL) return FALSE; if (!fu_firmware_set_stream(firmware_bootloader, stream_bootloader, error)) return FALSE; fu_firmware_set_idx(firmware_bootloader, FU_ELAN_KBD_FIRMWARE_IDX_BOOTLOADER); fu_firmware_add_image(firmware, firmware_bootloader); /* app */ stream_app = fu_partial_input_stream_new(stream, FU_ELAN_KBD_DEVICE_ADDR_APP, FU_ELAN_KBD_DEVICE_SIZE_APP, error); if (stream_app == NULL) return FALSE; if (!fu_firmware_set_stream(firmware_app, stream_app, error)) return FALSE; fu_firmware_set_idx(firmware_app, FU_ELAN_KBD_FIRMWARE_IDX_APP); fu_firmware_add_image(firmware, firmware_app); /* option */ stream_option = fu_partial_input_stream_new(stream, FU_ELAN_KBD_DEVICE_ADDR_OPTION, FU_ELAN_KBD_DEVICE_SIZE_OPTION, error); if (stream_option == NULL) return FALSE; if (!fu_firmware_set_stream(firmware_option, stream_option, error)) return FALSE; fu_firmware_set_idx(firmware_option, FU_ELAN_KBD_FIRMWARE_IDX_OPTION); fu_firmware_add_image(firmware, firmware_option); /* success */ return TRUE; } static GByteArray * fu_elan_kbd_firmware_write(FuFirmware *firmware, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GBytes) blob_app = NULL; g_autoptr(GBytes) blob_bootloader = NULL; g_autoptr(GBytes) blob_option = NULL; /* bootloader */ blob_bootloader = fu_firmware_get_image_by_idx_bytes(firmware, FU_ELAN_KBD_FIRMWARE_IDX_BOOTLOADER, error); if (blob_bootloader == NULL) return NULL; fu_byte_array_append_bytes(buf, blob_bootloader); /* app */ blob_app = fu_firmware_get_image_by_idx_bytes(firmware, FU_ELAN_KBD_FIRMWARE_IDX_APP, error); if (blob_app == NULL) return NULL; fu_byte_array_append_bytes(buf, blob_app); /* option */ blob_option = fu_firmware_get_image_by_idx_bytes(firmware, FU_ELAN_KBD_FIRMWARE_IDX_OPTION, error); if (blob_option == NULL) return NULL; fu_byte_array_append_bytes(buf, blob_option); /* success */ return g_steal_pointer(&buf); } static void fu_elan_kbd_firmware_init(FuElanKbdFirmware *self) { } static void fu_elan_kbd_firmware_class_init(FuElanKbdFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_elan_kbd_firmware_validate; firmware_class->parse = fu_elan_kbd_firmware_parse; firmware_class->write = fu_elan_kbd_firmware_write; } FuFirmware * fu_elan_kbd_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_ELAN_KBD_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/elan-kbd/fu-elan-kbd-firmware.h000066400000000000000000000005531501337203100221230ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ELAN_KBD_FIRMWARE (fu_elan_kbd_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuElanKbdFirmware, fu_elan_kbd_firmware, FU, ELAN_KBD_FIRMWARE, FuFirmware) FuFirmware * fu_elan_kbd_firmware_new(void); fwupd-2.0.10/plugins/elan-kbd/fu-elan-kbd-plugin.c000066400000000000000000000020361501337203100215760ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-elan-kbd-debug-device.h" #include "fu-elan-kbd-device.h" #include "fu-elan-kbd-firmware.h" #include "fu-elan-kbd-plugin.h" #include "fu-elan-kbd-runtime.h" struct _FuElanKbdPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuElanKbdPlugin, fu_elan_kbd_plugin, FU_TYPE_PLUGIN) static void fu_elan_kbd_plugin_init(FuElanKbdPlugin *self) { } static void fu_elan_kbd_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_ELAN_KBD_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_ELAN_KBD_DEBUG_DEVICE); fu_plugin_set_device_gtype_default(plugin, FU_TYPE_ELAN_KBD_RUNTIME); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_ELAN_KBD_FIRMWARE); } static void fu_elan_kbd_plugin_class_init(FuElanKbdPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_elan_kbd_plugin_constructed; } fwupd-2.0.10/plugins/elan-kbd/fu-elan-kbd-plugin.h000066400000000000000000000003611501337203100216020ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuElanKbdPlugin, fu_elan_kbd_plugin, FU, ELAN_KBD_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/elan-kbd/fu-elan-kbd-runtime.c000066400000000000000000000052311501337203100217630ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-elan-kbd-firmware.h" #include "fu-elan-kbd-runtime.h" #include "fu-elan-kbd-struct.h" struct _FuElanKbdRuntime { FuHidDevice parent_instance; }; G_DEFINE_TYPE(FuElanKbdRuntime, fu_elan_kbd_runtime, FU_TYPE_HID_DEVICE) static gboolean fu_elan_kbd_runtime_detach(FuDevice *device, FuProgress *progress, GError **error) { FuElanKbdRuntime *self = FU_ELAN_KBD_RUNTIME(device); g_autoptr(GError) error_local = NULL; guint8 buf[8] = { 0xBC, /* enter IAP */ 0x01, }; if (!fu_hid_device_set_report(FU_HID_DEVICE(self), buf[0], buf, sizeof(buf), 1000, FU_HID_DEVICE_FLAG_IS_FEATURE, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_READ)) { g_debug("ignoring: %s", error_local->message); } else { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } /* success */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static void fu_elan_kbd_runtime_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 19, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 47, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 30, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 3, "reload"); } static void fu_elan_kbd_runtime_init(FuElanKbdRuntime *self) { fu_device_set_name(FU_DEVICE(self), "ELAN USB Keyboard"); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_add_protocol(FU_DEVICE(self), "com.elan.kbd"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_INPUT_KEYBOARD); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_ELAN_KBD_FIRMWARE); fu_device_add_instance_id_full(FU_DEVICE(self), "USB\\VID_04F3&PID_0905", FU_DEVICE_INSTANCE_FLAG_COUNTERPART); } static void fu_elan_kbd_runtime_class_init(FuElanKbdRuntimeClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->detach = fu_elan_kbd_runtime_detach; device_class->set_progress = fu_elan_kbd_runtime_set_progress; } fwupd-2.0.10/plugins/elan-kbd/fu-elan-kbd-runtime.h000066400000000000000000000004711501337203100217710ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ELAN_KBD_RUNTIME (fu_elan_kbd_runtime_get_type()) G_DECLARE_FINAL_TYPE(FuElanKbdRuntime, fu_elan_kbd_runtime, FU, ELAN_KBD_RUNTIME, FuHidDevice) fwupd-2.0.10/plugins/elan-kbd/fu-elan-kbd.rs000066400000000000000000000132131501337203100205030ustar00rootroot00000000000000// Copyright 2025 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ValidateStream, Default)] #[repr(C, packed)] struct FuStructElanKbdFirmware { reset_vector: u32be == 0x21FAFF02, } // bootloader endpoints enum FuElanKbdEp { Cmd = 0x01, Status = 0x82, DataOut = 0x03, DataIn = 0x84, } enum FuElanKbdFirmwareIdx { Bootloader, App, Option, } #[repr(u8)] enum FuElanKbdCmd { EntryIap = 0x20, FinishedIap = 0x21, CancelIap = 0x23, SoftwareReset = 0x24, BootCondition = 0x25, GetVerSpec = 0x40, GetVerFw = 0x41, GetStatus = 0x42, ExitAuthMode = 0x43, GetAuthLock = 0x44, SetAuthLock = 0x45, Abort = 0x46, GetChecksum = 0x47, WriteRom = 0xA0, WriteRomFinish = 0xA1, WriteOption = 0xA2, WriteOptionFinish = 0xA3, WriteChecksum = 0xA4, WriteCustomInfo = 0xA5, WriteCustomInfoFinish = 0xA6, ReadRom = 0xE0, ReadRomFinish = 0xE1, ReadOption = 0xE2, ReadOptionFinish = 0xE3, ReadDataRequest = 0xE4, } #[derive(ToString)] #[repr(u8)] enum FuElanKbdDevStatus { Unknown, Idle, Iap, WriteRom, WriteOpt, WriteCsum, ReadRom, ReadOpt, } #[repr(u16be)] enum FuElanKbdStatus { Unknown, Ready, Busy, Success, Fail, Error, } #[derive(ToString)] #[repr(u8)] enum FuElanKbdError { Unknown, UnknownCommand, CommandStage, DataStage, RomAddressInvalid, AuthorityKeyIncorrect, WriteRomNotFinished, WriteOptionNotFinished, LengthTooBig, LengthTooSmall, ChecksumIncorrect, WriteFlashAbnormal, OverRomArea, RomPageInvalid, FlashKeyInvalid, OptionRomAddressInvalid, } #[derive(ToString)] #[repr(u8)] enum FuElanKbdBootCond1 { Unknown = 0, P80Entry = 1, NoAppEntry = 2, AppJumpEntry = 4, } enum FuElanKbdChecksumType { Boot, Main, }; #[derive(New, Default)] #[repr(C, packed)] struct FuStructElanKbdGetVerSpecReq { tag: u8 == 0xC1, cmd: FuElanKbdCmd == GetVerSpec, reserved: [u8; 6], } #[derive(Parse)] #[repr(C, packed)] struct FuStructElanKbdGetVerSpecRes { value: u16be, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructElanKbdGetVerFwReq { tag: u8 == 0xC1, cmd: FuElanKbdCmd == GetVerFw, reserved: [u8; 6], } #[derive(Parse)] #[repr(C, packed)] struct FuStructElanKbdGetVerFwRes { value: u16be, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructElanKbdGetStatusReq { tag: u8 == 0xC1, cmd: FuElanKbdCmd == GetStatus, reserved: [u8; 6], } #[derive(Parse)] #[repr(C, packed)] struct FuStructElanKbdGetStatusRes { reserved: [u8; 2], value: FuElanKbdDevStatus, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructElanKbdBootConditionReq { tag: u8 == 0xC1, cmd: FuElanKbdCmd == BootCondition, reserved: [u8; 6], } #[derive(Parse)] #[repr(C, packed)] struct FuStructElanKbdBootConditionRes { reserved: [u8; 2], value: FuElanKbdBootCond1, } #[derive(Parse)] #[repr(C, packed)] struct FuStructElanKbdCmdStatusRes { value: FuElanKbdStatus, error: FuElanKbdError, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructElanKbdAbortReq { tag: u8 == 0xC1, cmd: FuElanKbdCmd == Abort, reserved: [u8; 6], } #[derive(New, Default)] #[repr(C, packed)] struct FuStructElanKbdSoftwareResetReq { tag: u8 == 0xC1, cmd: FuElanKbdCmd == SoftwareReset, reserved: [u8; 6], } #[derive(New, Default)] #[repr(C, packed)] struct FuStructElanKbdReadRomReq { tag: u8 == 0xC1, cmd: FuElanKbdCmd == ReadRom, addr: u16le, len: u16le, reserved: [u8; 2], } #[derive(New, Default)] #[repr(C, packed)] struct FuStructElanKbdReadRomFinishedReq { tag: u8 == 0xC1, cmd: FuElanKbdCmd == ReadRomFinish, csum: u8, reserved: [u8; 5], } #[derive(New, Default)] #[repr(C, packed)] struct FuStructElanKbdReadOptionReq { tag: u8 == 0xC1, cmd: FuElanKbdCmd == ReadOption, addr: u16le == 128, len: u16le == 128, reserved: [u8; 2], } #[derive(New, Default)] #[repr(C, packed)] struct FuStructElanKbdReadOptionFinishedReq { tag: u8 == 0xC1, cmd: FuElanKbdCmd == ReadOptionFinish, csum: u8, reserved: [u8; 5], } #[derive(New, Default)] #[repr(C, packed)] struct FuStructElanKbdGetAuthLockReq { tag: u8 == 0xC1, cmd: FuElanKbdCmd == GetAuthLock, reserved: [u8; 6], } #[derive(Parse)] #[repr(C, packed)] struct FuStructElanKbdGetAuthLockRes { key: u8, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructElanKbdSetAuthLockReq { tag: u8 == 0xC1, cmd: FuElanKbdCmd == SetAuthLock, key: u8, reserved: [u8; 5], } #[derive(New, Default)] #[repr(C, packed)] struct FuStructElanKbdEntryIapReq { tag: u8 == 0xC1, cmd: FuElanKbdCmd == EntryIap, reserved: [u8; 4], key: u16le == 0x7FA9, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructElanKbdFinishedIapReq { tag: u8 == 0xC1, cmd: FuElanKbdCmd == FinishedIap, reserved: [u8; 4], key: u16le == 0x7FA9, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructElanKbdWriteRomReq { tag: u8 == 0xC1, cmd: FuElanKbdCmd == WriteRom, addr: u16le, len: u16le, key: u16le == 0x7FA9, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructElanKbdWriteRomFinishedReq { tag: u8 == 0xC1, cmd: FuElanKbdCmd == WriteRomFinish, csum: u8, reserved: [u8; 5], } fwupd-2.0.10/plugins/elan-kbd/meson.build000066400000000000000000000010671501337203100202160ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginElanKbd"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('elan-kbd.quirk') plugin_builtins += static_library('fu_plugin_elan_kbd', rustgen.process('fu-elan-kbd.rs'), sources: [ 'fu-elan-kbd-debug-device.c', 'fu-elan-kbd-device.c', 'fu-elan-kbd-firmware.c', 'fu-elan-kbd-plugin.c', 'fu-elan-kbd-runtime.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files('tests/elan-kbd.json') fwupd-2.0.10/plugins/elan-kbd/tests/000077500000000000000000000000001501337203100172125ustar00rootroot00000000000000fwupd-2.0.10/plugins/elan-kbd/tests/elan-kbd.json000066400000000000000000000006641501337203100215700ustar00rootroot00000000000000{ "name": "ELAN Keyboard", "interactive": false, "steps": [ { "url": "43cab4d273c9fff54abe4ddfb1ec3ac7d8a902f0119d575c0dfc591e305d188f-08D9.cab", "emulation-url": "1c7e5391bc51a6d0817ca7e1f22dfa966ef50b6b9a2914a335012a17c77c846b-08D9.zip", "components": [ { "version": "1.3", "guids": [ "deea77a0-33e6-565b-94a4-4b20451ab094" ] } ] } ] } fwupd-2.0.10/plugins/elanfp/000077500000000000000000000000001501337203100156405ustar00rootroot00000000000000fwupd-2.0.10/plugins/elanfp/README.md000066400000000000000000000016731501337203100171260ustar00rootroot00000000000000--- title: Plugin: Elan Fingerprint Sensor --- ## Introduction The plugin used for update firmware for fingerprint sensors from Elan. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a packed binary file format. This plugin supports the following protocol ID: * `tw.com.emc.elanfp` ## Quirk Use This plugin uses the following plugin-specific quirks: ### `Flags=usb-bulk-transfer` Use USB bulk transfer. ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_04F3&PID_0C7E` ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x04F3` ## Version Considerations This plugin has been available since fwupd version `1.7.0`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Michael Cheng: @MichaelCheng04 fwupd-2.0.10/plugins/elanfp/elanfp.quirk000066400000000000000000000005731501337203100201670ustar00rootroot00000000000000[USB\VID_04F3&PID_0C7E] Plugin = elanfp Flags = enforce-requires [USB\VID_04F3&PID_0C82] Plugin = elanfp Flags = enforce-requires [USB\VID_04F3&PID_0C88] Plugin = elanfp Flags = usb-bulk-transfer,enforce-requires [USB\VID_04F3&PID_0C9F] Plugin = elanfp Flags = usb-bulk-transfer,enforce-requires [USB\VID_04F3&PID_0CA3] Plugin = elanfp Flags = usb-bulk-transfer,enforce-requires fwupd-2.0.10/plugins/elanfp/fu-elanfp-device.c000066400000000000000000000305131501337203100211200ustar00rootroot00000000000000/* * Copyright 2021 Michael Cheng * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-cfu-struct.h" #include "fu-elanfp-device.h" #include "fu-elanfp-firmware.h" #define ELAN_EP_CMD_OUT (0x01 | 0x00) #define ELAN_EP_CMD_IN (0x02 | 0x80) #define ELAN_EP_MOC_CMD_IN (0x04 | 0x80) #define ELAN_EP_IMG_IN (0x03 | 0x80) #define ELANFP_USB_INTERFACE 0 #define CTRL_SEND_TIMEOUT_MS 3000 #define BULK_SEND_TIMEOUT_MS 3000 #define BULK_RECV_TIMEOUT_MS 3000 #define REPORT_ID_FW_VERSION_FEATURE 0x20 #define REPORT_ID_OFFER_COMMAND 0x25 #define REPORT_ID_OFFER_RESPONSE 0x25 #define REPORT_ID_PAYLOAD_COMMAND 0x20 #define REPORT_ID_PAYLOAD_RESPONSE 0x22 #define REQTYPE_GET_VERSION 0xC1 #define REQTYPE_COMMAND 0x41 #define TAG_SEND_COMMAND 0xFA #define FU_ELAN_FP_DEVICE_FLAG_USB_BULK_TRANSFER "usb-bulk-transfer" struct _FuElanfpDevice { FuUsbDevice parent_instance; }; G_DEFINE_TYPE(FuElanfpDevice, fu_elanfp_device, FU_TYPE_USB_DEVICE) static gboolean fu_elanfp_device_iap_send_command(FuElanfpDevice *self, guint8 request_type, guint8 request, const guint8 *buf, gsize bufsz, gsize rspsz, GError **error) { gsize actual = 0; gsize sendsz = bufsz + 1; guint8 start_index = 0x01; guint8 buftmp[64] = {request, 0}; if (buf != NULL) { if (fu_device_has_private_flag(FU_DEVICE(self), FU_ELAN_FP_DEVICE_FLAG_USB_BULK_TRANSFER)) { buftmp[0] = TAG_SEND_COMMAND; buftmp[1] = rspsz; buftmp[2] = bufsz + 1; buftmp[3] = request; start_index = 0x04; sendsz = bufsz + 1 + 3; } if (!fu_memcpy_safe(buftmp, sizeof(buftmp), start_index, /* dst */ buf, bufsz, 0x0, /* src */ bufsz, error)) return FALSE; } if (fu_device_has_private_flag(FU_DEVICE(self), FU_ELAN_FP_DEVICE_FLAG_USB_BULK_TRANSFER)) { if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), ELAN_EP_CMD_OUT, buftmp, sendsz, &actual, BULK_SEND_TIMEOUT_MS, NULL, error)) { g_prefix_error(error, "failed to send command (bulk): "); return FALSE; } } else { if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_INTERFACE, request, /* request */ 0x00, /* value */ 0x00, /* index */ buftmp, sendsz, &actual, CTRL_SEND_TIMEOUT_MS, NULL, error)) { g_prefix_error(error, "failed to send command (ctrl transfer): "); return FALSE; } } if (actual != sendsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "send length (%u) is not match with the request (%u)", (guint)actual, (guint)bufsz + 1); return FALSE; } return TRUE; } static gboolean fu_elanfp_device_iap_recv_status(FuElanfpDevice *self, guint8 *buf, gsize bufsz, GError **error) { guint8 endpoint = ELAN_EP_CMD_IN; gsize actual = 0; if (fu_device_has_private_flag(FU_DEVICE(self), FU_ELAN_FP_DEVICE_FLAG_USB_BULK_TRANSFER)) { endpoint = ELAN_EP_IMG_IN; } if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), endpoint, buf, bufsz, &actual, BULK_RECV_TIMEOUT_MS, NULL, error)) { g_prefix_error(error, "failed to receive status: "); return FALSE; } if (actual != bufsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "received length (%u) is not match with the request (%u)", (guint)actual, (guint)bufsz); return FALSE; } return TRUE; } static gboolean fu_elanfp_device_do_xfer(FuElanfpDevice *self, guint8 *outbuf, gsize outlen, guint8 *inbuf, gsize inlen, gboolean allow_less, gsize *rxed_count, GError **error) { gsize actual = 0; /* send data out */ if (outbuf != NULL && outlen > 0) { if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), ELAN_EP_CMD_OUT, outbuf, outlen, &actual, BULK_SEND_TIMEOUT_MS, NULL, error)) { return FALSE; } if (actual != outlen) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "only sent %" G_GSIZE_FORMAT "/%" G_GSIZE_FORMAT " bytes", actual, outlen); return FALSE; } } /* read reply back */ if (inbuf != NULL && inlen > 0) { actual = 0; if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), ELAN_EP_IMG_IN, inbuf, inlen, &actual, BULK_RECV_TIMEOUT_MS, NULL, error)) { return FALSE; } if (actual != inlen && !allow_less) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "only received %" G_GSIZE_FORMAT "/%" G_GSIZE_FORMAT " bytes", actual, outlen); return FALSE; } } if (rxed_count != NULL) *rxed_count = actual; return TRUE; } static gboolean fu_elanfp_device_setup(FuDevice *device, GError **error) { FuElanfpDevice *self = FU_ELANFP_DEVICE(device); guint16 fw_ver; guint8 usb_buf[2] = {0x40, 0x19}; g_autofree gchar *fw_ver_str = NULL; /* get version */ if (!fu_elanfp_device_do_xfer(self, (guint8 *)&usb_buf, sizeof(usb_buf), usb_buf, sizeof(usb_buf), TRUE, NULL, error)) { g_prefix_error(error, "failed to device setup: "); return FALSE; } fw_ver = fu_memread_uint16(usb_buf, G_BIG_ENDIAN); fw_ver_str = g_strdup_printf("%04x", fw_ver); fu_device_set_version(device, fw_ver_str); /* success */ return TRUE; } static gboolean fu_elanfp_device_write_payload(FuElanfpDevice *self, FuFirmware *payload, FuProgress *progress, GError **error) { g_autoptr(GPtrArray) chunks = NULL; /* write each chunk */ chunks = fu_firmware_get_chunks(payload, error); if (chunks == NULL) return FALSE; fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); guint8 databuf[60] = {0}; guint8 recvbuf[17] = {0}; /* flags */ if (i == 0) databuf[0] = FU_CFU_CONTENT_FLAG_FIRST_BLOCK; else if (i == chunks->len - 1) databuf[0] = FU_CFU_CONTENT_FLAG_LAST_BLOCK; /* length */ databuf[1] = fu_chunk_get_data_sz(chk); /* sequence number */ if (!fu_memwrite_uint16_safe(databuf, sizeof(databuf), 0x2, i + 1, G_LITTLE_ENDIAN, error)) return FALSE; /* address */ if (!fu_memwrite_uint32_safe(databuf, sizeof(databuf), 0x4, fu_chunk_get_address(chk), G_LITTLE_ENDIAN, error)) return FALSE; /* data */ if (!fu_memcpy_safe(databuf, sizeof(databuf), 0x8, /* dst */ fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0x0, /* src */ fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "memory copy for payload fail: "); return FALSE; } if (!fu_elanfp_device_iap_send_command(self, REQTYPE_COMMAND, REPORT_ID_PAYLOAD_COMMAND, databuf, sizeof(databuf), sizeof(recvbuf), error)) { g_prefix_error(error, "send payload command fail: "); return FALSE; } if (!fu_elanfp_device_iap_recv_status(self, recvbuf, sizeof(recvbuf), error)) { g_prefix_error(error, "received payload status fail: "); return FALSE; } if (recvbuf[5] != FU_CFU_CONTENT_STATUS_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to send chunk %u: %s", i + 1, fu_cfu_content_status_to_string(recvbuf[5])); return FALSE; } fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_elanfp_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuElanfpDevice *self = FU_ELANFP_DEVICE(device); guint8 usb_buf[8] = {0x40, 0x27, 0x57, 0x44, 0x54, 0x52, 0x53, 0x54}; guint i; struct { const gchar *tag; guint8 offer_idx; guint8 payload_idx; } items[] = { {"A", FU_ELANTP_FIRMWARE_IDX_CFU_OFFER_A, FU_ELANTP_FIRMWARE_IDX_CFU_PAYLOAD_A}, {"B", FU_ELANTP_FIRMWARE_IDX_CFU_OFFER_B, FU_ELANTP_FIRMWARE_IDX_CFU_PAYLOAD_B}, {NULL, FU_ELANTP_FIRMWARE_IDX_END, FU_ELANTP_FIRMWARE_IDX_END}}; g_autoptr(FuFirmware) payload = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "offer"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 99, "payload"); /* send offers */ for (i = 0; items[i].tag != NULL; i++) { g_autoptr(GBytes) offer = NULL; guint8 recvbuf[17] = {0}; offer = fu_firmware_get_image_by_idx_bytes(firmware, items[i].offer_idx, error); if (offer == NULL) return FALSE; if (!fu_elanfp_device_iap_send_command(self, REQTYPE_COMMAND, REPORT_ID_OFFER_COMMAND, g_bytes_get_data(offer, NULL), g_bytes_get_size(offer), g_bytes_get_size(offer) + 1, error)) { g_prefix_error(error, "send offer command fail: "); return FALSE; } if (!fu_elanfp_device_iap_recv_status(self, recvbuf, sizeof(recvbuf), error)) { g_prefix_error(error, "received offer status fail: "); return FALSE; } g_debug("offer-%s status:%s reject:%s", items[i].tag, fu_cfu_offer_status_to_string(recvbuf[13]), fu_cfu_rr_code_to_string(recvbuf[9])); if (recvbuf[13] == FU_CFU_OFFER_STATUS_ACCEPT) break; } if (items[i].tag == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no CFU offer was accepted"); return FALSE; } fu_progress_step_done(progress); /* send payload */ payload = fu_firmware_get_image_by_idx(firmware, items[i].payload_idx, error); if (payload == NULL) return FALSE; if (!fu_elanfp_device_write_payload(self, payload, fu_progress_get_child(progress), error)) return FALSE; /* hardware reset */ if (!fu_elanfp_device_do_xfer(self, (guint8 *)&usb_buf, sizeof(usb_buf), NULL, 0, TRUE, NULL, error)) { g_prefix_error(error, "failed to hardware reset "); return FALSE; } fu_progress_step_done(progress); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* success */ return TRUE; } static void fu_elanfp_device_init(FuElanfpDevice *device) { FuElanfpDevice *self = FU_ELANFP_DEVICE(device); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SELF_RECOVERY); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_RUNTIME_VERSION); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_remove_delay(FU_DEVICE(self), 5000); fu_device_add_protocol(FU_DEVICE(self), "tw.com.emc.elanfp"); fu_device_set_name(FU_DEVICE(self), "Fingerprint Sensor"); fu_device_set_summary(FU_DEVICE(self), "Match-On-Chip Fingerprint Sensor"); fu_device_set_vendor(FU_DEVICE(self), "Elan"); fu_device_set_install_duration(FU_DEVICE(self), 10); fu_device_set_firmware_size_min(FU_DEVICE(self), 0x20000); fu_device_set_firmware_size_max(FU_DEVICE(self), 0x90000); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_ELANFP_FIRMWARE); fu_usb_device_add_interface(FU_USB_DEVICE(self), ELANFP_USB_INTERFACE); fu_device_register_private_flag(FU_DEVICE(self), FU_ELAN_FP_DEVICE_FLAG_USB_BULK_TRANSFER); } static void fu_elanfp_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_elanfp_device_class_init(FuElanfpDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_elanfp_device_setup; device_class->write_firmware = fu_elanfp_device_write_firmware; device_class->set_progress = fu_elanfp_device_set_progress; } fwupd-2.0.10/plugins/elanfp/fu-elanfp-device.h000066400000000000000000000004571501337203100211310ustar00rootroot00000000000000/* * Copyright 2021 Michael Cheng * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ELANFP_DEVICE (fu_elanfp_device_get_type()) G_DECLARE_FINAL_TYPE(FuElanfpDevice, fu_elanfp_device, FU, ELANFP_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/elanfp/fu-elanfp-firmware.c000066400000000000000000000135421501337203100215000ustar00rootroot00000000000000/* * Copyright 2021 Michael Cheng * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-elanfp-firmware.h" #include "fu-elanfp-struct.h" struct _FuElanfpFirmware { FuFirmwareClass parent_instance; guint32 format_version; }; G_DEFINE_TYPE(FuElanfpFirmware, fu_elanfp_firmware, FU_TYPE_FIRMWARE) static void fu_elanfp_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuElanfpFirmware *self = FU_ELANFP_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "format_version", self->format_version); } static gboolean fu_elanfp_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { FuElanfpFirmware *self = FU_ELANFP_FIRMWARE(firmware); guint64 tmp; /* optional properties */ tmp = xb_node_query_text_as_uint(n, "format_version", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT32) self->format_version = tmp; /* success */ return TRUE; } static gboolean fu_elanfp_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_elanfp_firmware_hdr_validate_stream(stream, offset, error); } static gboolean fu_elanfp_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuElanfpFirmware *self = FU_ELANFP_FIRMWARE(firmware); gsize offset = 0; /* file format version */ if (!fu_input_stream_read_u32(stream, offset + 0x4, &self->format_version, G_LITTLE_ENDIAN, error)) return FALSE; /* read indexes */ offset += 0x10; while (1) { guint32 start_addr = 0; guint32 length = 0; guint32 fwtype = 0; g_autoptr(FuFirmware) img = NULL; g_autoptr(GInputStream) stream_tmp = NULL; /* type, reserved, start-addr, len */ if (!fu_input_stream_read_u32(stream, offset + 0x0, &fwtype, G_LITTLE_ENDIAN, error)) return FALSE; /* check not already added */ img = fu_firmware_get_image_by_idx(firmware, fwtype, NULL); if (img != NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "already parsed image with fwtype 0x%x", fwtype); return FALSE; } /* done */ if (fwtype == FU_ELANTP_FIRMWARE_IDX_END) break; switch (fwtype) { case FU_ELANTP_FIRMWARE_IDX_CFU_OFFER_A: case FU_ELANTP_FIRMWARE_IDX_CFU_OFFER_B: img = fu_cfu_offer_new(); break; case FU_ELANTP_FIRMWARE_IDX_CFU_PAYLOAD_A: case FU_ELANTP_FIRMWARE_IDX_CFU_PAYLOAD_B: img = fu_cfu_payload_new(); break; default: img = fu_firmware_new(); break; } fu_firmware_set_idx(img, fwtype); if (!fu_input_stream_read_u32(stream, offset + 0x8, &start_addr, G_LITTLE_ENDIAN, error)) return FALSE; fu_firmware_set_addr(img, start_addr); if (!fu_input_stream_read_u32(stream, offset + 0xC, &length, G_LITTLE_ENDIAN, error)) return FALSE; if (length == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "zero size fwtype 0x%x not supported", fwtype); return FALSE; } stream_tmp = fu_partial_input_stream_new(stream, start_addr, length, error); if (stream_tmp == NULL) return FALSE; if (!fu_firmware_parse_stream(img, stream_tmp, 0x0, flags | FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error)) return FALSE; if (!fu_firmware_add_image_full(firmware, img, error)) return FALSE; offset += 0x10; } /* success */ return TRUE; } static GByteArray * fu_elanfp_firmware_write(FuFirmware *firmware, GError **error) { FuElanfpFirmware *self = FU_ELANFP_FIRMWARE(firmware); gsize offset = 0; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GPtrArray) imgs = fu_firmware_get_images(firmware); /* S2F_HEADER */ fu_byte_array_append_uint32(buf, 0x46325354, G_LITTLE_ENDIAN); fu_byte_array_append_uint32(buf, self->format_version, G_LITTLE_ENDIAN); fu_byte_array_append_uint32(buf, 0x0, G_LITTLE_ENDIAN); /* ICID, assumed */ fu_byte_array_append_uint32(buf, 0x0, G_LITTLE_ENDIAN); /* reserved */ /* S2F_INDEX */ offset += 0x10 + ((imgs->len + 1) * 0x10); for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); g_autoptr(GBytes) blob = fu_firmware_write(img, error); if (blob == NULL) return NULL; fu_byte_array_append_uint32(buf, fu_firmware_get_idx(img), G_LITTLE_ENDIAN); fu_byte_array_append_uint32(buf, 0x0, G_LITTLE_ENDIAN); /* reserved */ fu_byte_array_append_uint32(buf, offset, G_LITTLE_ENDIAN); fu_byte_array_append_uint32(buf, g_bytes_get_size(blob), G_LITTLE_ENDIAN); offset += g_bytes_get_size(blob); } /* end of index */ fu_byte_array_append_uint32(buf, FU_ELANTP_FIRMWARE_IDX_END, G_LITTLE_ENDIAN); fu_byte_array_append_uint32(buf, 0x0, G_LITTLE_ENDIAN); /* reserved */ fu_byte_array_append_uint32(buf, 0x0, G_LITTLE_ENDIAN); /* assumed */ fu_byte_array_append_uint32(buf, 0x0, G_LITTLE_ENDIAN); /* assumed */ /* data */ for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); g_autoptr(GBytes) blob = fu_firmware_write(img, error); if (blob == NULL) return NULL; fu_byte_array_append_bytes(buf, blob); } /* success */ return g_steal_pointer(&buf); } static void fu_elanfp_firmware_init(FuElanfpFirmware *self) { fu_firmware_set_images_max(FU_FIRMWARE(self), 256); g_type_ensure(FU_TYPE_CFU_OFFER); g_type_ensure(FU_TYPE_CFU_PAYLOAD); } static void fu_elanfp_firmware_class_init(FuElanfpFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_elanfp_firmware_validate; firmware_class->parse = fu_elanfp_firmware_parse; firmware_class->write = fu_elanfp_firmware_write; firmware_class->export = fu_elanfp_firmware_export; firmware_class->build = fu_elanfp_firmware_build; } fwupd-2.0.10/plugins/elanfp/fu-elanfp-firmware.h000066400000000000000000000011541501337203100215010ustar00rootroot00000000000000/* * Copyright 2021 Michael Cheng * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ELANFP_FIRMWARE (fu_elanfp_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuElanfpFirmware, fu_elanfp_firmware, FU, ELANFP_FIRMWARE, FuFirmware) #define FU_ELANTP_FIRMWARE_IDX_FIRMWAREVERSION 0x00 #define FU_ELANTP_FIRMWARE_IDX_CFU_OFFER_A 0x72 #define FU_ELANTP_FIRMWARE_IDX_CFU_OFFER_B 0x73 #define FU_ELANTP_FIRMWARE_IDX_CFU_PAYLOAD_A 0x74 #define FU_ELANTP_FIRMWARE_IDX_CFU_PAYLOAD_B 0x75 #define FU_ELANTP_FIRMWARE_IDX_END 0xFF fwupd-2.0.10/plugins/elanfp/fu-elanfp-plugin.c000066400000000000000000000014171501337203100211600ustar00rootroot00000000000000/* * Copyright 2021 * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-elanfp-device.h" #include "fu-elanfp-firmware.h" #include "fu-elanfp-plugin.h" struct _FuElanfpPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuElanfpPlugin, fu_elanfp_plugin, FU_TYPE_PLUGIN) static void fu_elanfp_plugin_init(FuElanfpPlugin *self) { } static void fu_elanfp_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_ELANFP_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_ELANFP_FIRMWARE); } static void fu_elanfp_plugin_class_init(FuElanfpPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_elanfp_plugin_constructed; } fwupd-2.0.10/plugins/elanfp/fu-elanfp-plugin.h000066400000000000000000000003541501337203100211640ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuElanfpPlugin, fu_elanfp_plugin, FU, ELANFP_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/elanfp/fu-elanfp.rs000066400000000000000000000003411501337203100200610ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ValidateStream, Default)] #[repr(C, packed)] struct FuStructElanfpFirmwareHdr { magic: u32le == 0x46325354, } fwupd-2.0.10/plugins/elanfp/meson.build000066400000000000000000000011571501337203100200060ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginElanfp"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('elanfp.quirk') plugin_builtins += static_library('fu_plugin_elanfp', rustgen.process( 'fu-elanfp.rs', # fuzzing ), sources: [ cfu_rs[1], # header 'fu-elanfp-plugin.c', 'fu-elanfp-device.c', 'fu-elanfp-firmware.c' # fuzzing ], include_directories: [ plugin_incdirs, plugincfu_incdir, ], link_with: [ plugin_libs, plugin_builtin_cfu, ], c_args: cargs, dependencies: plugin_deps, ) device_tests += files('tests/elan-p1515e.json') fwupd-2.0.10/plugins/elanfp/tests/000077500000000000000000000000001501337203100170025ustar00rootroot00000000000000fwupd-2.0.10/plugins/elanfp/tests/elan-p1515e.json000066400000000000000000000007171501337203100215370ustar00rootroot00000000000000{ "name": "ELAN P1515E", "interactive": false, "steps": [ { "url": "ac64be4971c96c777bd503eede3b2b1999df32f425740ac22ea29c5b92a0c31d-F867_V0414_Release.cab", "emulation-url": "1dd8da35a6ce2305c58f9a87a84c5ab586a34e0eebce08c0850593490ad2a139-F867_V0414_Release.zip", "components": [ { "version": "0414", "guids": [ "d769cc37-760c-57f2-aa1f-433e72ad281f" ] } ] } ] } fwupd-2.0.10/plugins/elanfp/tests/elanfp.builder.xml000066400000000000000000000014351501337203100224210ustar00rootroot00000000000000 0x123 0x72 0x1234 0x5678 0x1 0x73 0x1234 0x5678 0x2 0x74 aGVsbG8gd29ybGQ= 0x8001234 0x75 aGVsbG8gd29ybGQ= 0x8001234 fwupd-2.0.10/plugins/elantp/000077500000000000000000000000001501337203100156565ustar00rootroot00000000000000fwupd-2.0.10/plugins/elantp/README.md000066400000000000000000000046421501337203100171430ustar00rootroot00000000000000--- title: Plugin: Elan TouchPad --- ## Introduction This plugin allows updating Touchpad devices from Elan. Devices are enumerated using HID and raw I²C nodes. The I²C mode is used for ABS devices and firmware recovery of HID devices. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in an unspecified binary file format. This plugin supports the following protocol ID: * `tw.com.emc.elantp` ## GUID Generation These device uses the standard DeviceInstanceId values, e.g. * `HIDRAW\VEN_04F3&DEV_3010` Additionally another instance ID is added which corresponds to the module ID: * `HIDRAW\VEN_04F3&DEV_3010&MOD_1234` These devices also use custom GUID values for the IC configuration, e.g. * `ELANTP\ICTYPE_09` (only-quirk) Additionally another instance ID is added which corresponds to the IC type & module ID: * `ELANTP\ICTYPE_09&MOD_1234` Additionally another instance ID is added which corresponds to the IC Type & module ID and Driver in order to distinguish HID/ABS devices: * `ELANTP\ICTYPE_09&MOD_1234&DRIVER_HID` -> HID Device * `ELANTP\ICTYPE_09&MOD_1234&DRIVER_ELAN_I2C` -> ABS Device ## Update Behavior The device usually presents in HID/ABS mode, and the firmware is written to the device by switching to a IAP mode where the touchpad is nonfunctional. Once complete the device is reset to get out of IAP mode and to load the new firmware version. For HID devices, on flash failure the device is nonfunctional, but is recoverable by writing to the i2c device. This is typically much slower than updating the device using HID and also requires a model-specific HWID quirk to match. For ABS devices, on flash failure the device is nonfunctional, but it could be recovered by the same i2c device. ## Vendor ID Security The vendor ID is set from the HID vendor, for example set to `HIDRAW:0x17EF` ## Quirk Use This plugin uses the following plugin-specific quirks: ### ElantpIcPageCount The IC page count. Since: 1.4.6 ### ElantpIapPassword The IAP password. Since: 1.4.6 ## External Interface Access This plugin requires ioctl access to `HIDIOCSFEATURE` and `HIDIOCGFEATURE`. ## Version Considerations This plugin has been available since fwupd version `1.5.0`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Jingle Wu: @jinglewu * Josh Chen: @josh-chen-elan fwupd-2.0.10/plugins/elantp/elantp.quirk000066400000000000000000000045331501337203100202230ustar00rootroot00000000000000[HIDRAW\VEN_04F3] Plugin = elantp GType = FuElantpHidDevice # ThinkPad X1 Carbon Gen 9 [6c87726f-b545-549e-840a-189422ea21d0] Flags = elantp-recovery # Lenovo X1 Nano Gen1 [4c20262a-aee0-5d6e-ba72-6d29b23fe350] Flags = elantp-recovery # Lenovo ThinkPad X13 Yoga Gen 2 [34874ca5-54f0-5a4f-9161-e03910d14b75] Flags = elantp-recovery # Lenovo ThinkPad X13 Gen 2 - 20XHFVT007 [e5319542-ca16-5d93-bd0f-2d25f547a846] Flags = elantp-recovery # Lenovo ThinkPad X13 Gen 2 - 20XHFVT003 [ea5ea62f-ec8d-5f5d-8231-0816c89d75d6] Flags = elantp-recovery # Acer Aspire V3-372T [513cde3d-d939-59bd-a634-5c1645ebb93b] Flags = elantp-recovery # absolute report device on ACPI [I2C\NAME_ELAN0670:00] Plugin = elantp GType = FuElantpI2cDevice ElantpI2cTargetAddress = 0x15 Flags = elantp-absolute # absolute report device on ACPI [I2C\NAME_ELAN0000:00] Plugin = elantp GType = FuElantpI2cDevice ElantpI2cTargetAddress = 0x15 Flags = elantp-absolute # absolute report device on Device Tree [I2C\NAME_ekth3000] Plugin = elantp GType = FuElantpI2cDevice ElantpI2cTargetAddress = 0x15 Flags = elantp-absolute # recovery device [I2C\NAME_Synopsys-DesignWare-I2C-adapter] Plugin = elantp GType = FuElantpI2cDevice ElantpI2cTargetAddress = 0x15 [ELANTP\ICTYPE_00] ElantpIcPageCount = 0x200 ElantpIapPassword = 0x1EA5 [ELANTP\ICTYPE_03] ElantpIcPageCount = 0x300 ElantpIapPassword = 0x1EA5 [ELANTP\ICTYPE_06] ElantpIcPageCount = 0x200 ElantpIapPassword = 0x1EA5 [ELANTP\ICTYPE_07] ElantpIcPageCount = 0x300 ElantpIapPassword = 0x1EA5 [ELANTP\ICTYPE_08] ElantpIcPageCount = 0x200 ElantpIapPassword = 0x1EA5 [ELANTP\ICTYPE_0A] ElantpIcPageCount = 0x300 ElantpIapPassword = 0xE15A [ELANTP\ICTYPE_0B] ElantpIcPageCount = 0x300 ElantpIapPassword = 0x1EA5 [ELANTP\ICTYPE_0C] ElantpIcPageCount = 0x300 ElantpIapPassword = 0x1EA5 [ELANTP\ICTYPE_0E] ElantpIcPageCount = 0x280 ElantpIapPassword = 0x1EA5 [ELANTP\ICTYPE_09] ElantpIcPageCount = 0x300 ElantpIapPassword = 0x1EA5 [ELANTP\ICTYPE_0D] ElantpIcPageCount = 0x380 ElantpIapPassword = 0x1EA5 [ELANTP\ICTYPE_10] ElantpIcPageCount = 0x400 ElantpIapPassword = 0x1EA5 [ELANTP\ICTYPE_11] ElantpIcPageCount = 0x500 ElantpIapPassword = 0x1EA5 [ELANTP\ICTYPE_13] ElantpIcPageCount = 0x800 ElantpIapPassword = 0x1EA5 [ELANTP\ICTYPE_14] ElantpIcPageCount = 0x400 ElantpIapPassword = 0x1EA5 [ELANTP\ICTYPE_15] ElantpIcPageCount = 0x400 ElantpIapPassword = 0x1EA5 fwupd-2.0.10/plugins/elantp/fu-elantp-common.h000066400000000000000000000045561501337203100212220ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define ETP_CMD_GET_HID_DESCRIPTOR 0x0001 #define ETP_CMD_GET_HARDWARE_ID 0x0100 #define ETP_CMD_GET_MODULE_ID 0x0101 #define ETP_CMD_I2C_FW_CHECKSUM 0x030F #define ETP_CMD_I2C_FW_VERSION 0x0102 #define ETP_CMD_I2C_IAP 0x0311 #define ETP_CMD_I2C_IAP_CHECKSUM 0x0315 #define ETP_CMD_I2C_IAP_CTRL 0x0310 #define ETP_CMD_I2C_IAP_ICBODY 0x0110 #define ETP_CMD_I2C_IAP_RESET 0x0314 #define ETP_CMD_I2C_IAP_VERSION 0x0111 #define ETP_CMD_I2C_IAP_VERSION_2 0x0110 #define ETP_CMD_I2C_OSM_VERSION 0x0103 #define ETP_CMD_I2C_GET_HID_ID 0x0100 #define ETP_CMD_I2C_IAP_TYPE 0x0304 #define ETP_CMD_I2C_FW_PW 0x030E #define ETP_CMD_FORCE_ADDR 0x03AD #define ETP_CMD_I2C_FORCE_TYPE_ENABLE 0x0104 #define ETP_CMD_I2C_SET_EEPROM_CTRL 0x0321 #define ETP_CMD_I2C_GET_EEPROM_FW_VERSION 0x0710 #define ETP_CMD_I2C_GET_EEPROM_IAP_VERSION 0x0711 #define ETP_CMD_I2C_SET_EEPROM_ENTER_IAP 0x0607 #define ETP_CMD_I2C_SET_EEPROM_LEAVE_IAP 0x0606 #define ETP_CMD_I2C_SET_EEPROM_DATATYPE 0x0702 #define ETP_CMD_I2C_CALC_EEPROM_CHECKSUM 0x060F #define ETP_CMD_I2C_READ_EEPROM_CHECKSUM 0x070A #define ETP_CMD_I2C_HAPTIC_RESTART 0x0601 #define ETP_CMD_I2C_EEPROM_SETTING 0x0322 #define ETP_CMD_I2C_EEPROM_LONG_TRANS_ENABLE 0x4607 #define ETP_CMD_I2C_EEPROM_SETTING_INITIAL 0x0000 #define ETP_CMD_I2C_EEPROM_WRITE_INFORMATION 0x4600 #define ETP_CMD_I2C_EEPROM_WRITE_CHECKSUM 0x048B #define ETP_I2C_IC13_IAPV5_PW 0x37CA #define ETP_FW_FORCE_TYPE_ENABLE_BIT 0x1 #define ETP_FW_EEPROM_ENABLE_BIT 0x2 #define ETP_I2C_IAP_TYPE_REG 0x0040 #define ETP_I2C_ENABLE_REPORT 0x0800 #define ETP_I2C_IAP_RESET 0xF0F0 #define ETP_I2C_MAIN_MODE_ON (1 << 9) #define ETP_I2C_MAIN_MODE_ON2 (1 << 12) #define ETP_I2C_IAP_REG_L 0x01 #define ETP_I2C_IAP_REG_H 0x06 #define ETP_FW_IAP_INTF_ERR (1 << 4) #define ETP_FW_IAP_PAGE_ERR (1 << 5) #define ETP_FW_IAP_CHECK_PW (1 << 7) #define ETP_FW_IAP_LAST_FIT (1 << 9) #define ELANTP_DELAY_COMPLETE 1200 /* ms */ #define ELANTP_DELAY_RESET 30 /* ms */ #define ELANTP_EEPROM_READ_DELAY 100 /* ms */ #define ELANTP_DELAY_UNLOCK 100 /* ms */ #define ELANTP_DELAY_WRITE_BLOCK 35 /* ms */ #define ELANTP_DELAY_WRITE_BLOCK_512 50 /* ms */ fwupd-2.0.10/plugins/elantp/fu-elantp-firmware.c000066400000000000000000000164401501337203100215340ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-elantp-common.h" #include "fu-elantp-firmware.h" #include "fu-elantp-struct.h" struct _FuElantpFirmware { FuFirmwareClass parent_instance; guint16 module_id; guint16 ic_type; guint16 iap_addr; guint16 iap_ver; gboolean force_table_support; guint32 force_table_addr; }; G_DEFINE_TYPE(FuElantpFirmware, fu_elantp_firmware, FU_TYPE_FIRMWARE) /* firmware block update */ #define ETP_IC_TYPE_ADDR_WRDS 0x0080 #define ETP_IAP_VER_ADDR_WRDS 0x0082 #define ETP_IAP_START_ADDR_WRDS 0x0083 #define ETP_IAP_FORCETABLE_ADDR_V5 0x0085 const guint8 elantp_signature[] = {0xAA, 0x55, 0xCC, 0x33, 0xFF, 0xFF}; guint16 fu_elantp_firmware_get_module_id(FuElantpFirmware *self) { g_return_val_if_fail(FU_IS_ELANTP_FIRMWARE(self), 0); return self->module_id; } guint16 fu_elantp_firmware_get_ic_type(FuElantpFirmware *self) { g_return_val_if_fail(FU_IS_ELANTP_FIRMWARE(self), 0); return self->ic_type; } guint16 fu_elantp_firmware_get_iap_addr(FuElantpFirmware *self) { g_return_val_if_fail(FU_IS_ELANTP_FIRMWARE(self), 0); return self->iap_addr; } gboolean fu_elantp_firmware_get_forcetable_support(FuElantpFirmware *self) { g_return_val_if_fail(FU_IS_ELANTP_FIRMWARE(self), FALSE); return self->force_table_support; } guint32 fu_elantp_firmware_get_forcetable_addr(FuElantpFirmware *self) { g_return_val_if_fail(FU_IS_ELANTP_FIRMWARE(self), 0); return self->force_table_addr; } static void fu_elantp_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuElantpFirmware *self = FU_ELANTP_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "iap_addr", self->iap_addr); fu_xmlb_builder_insert_kx(bn, "module_id", self->module_id); } static gboolean fu_elantp_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { FuElantpFirmware *self = FU_ELANTP_FIRMWARE(firmware); gsize streamsz = 0; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz < FU_STRUCT_ELANTP_FIRMWARE_HDR_SIZE) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "stream was too small"); return FALSE; } if (!fu_struct_elantp_firmware_hdr_validate_stream(stream, streamsz - FU_STRUCT_ELANTP_FIRMWARE_HDR_SIZE, error)) return FALSE; if (self->force_table_addr != 0) { if (!fu_struct_elantp_firmware_hdr_validate_stream( stream, self->force_table_addr - 1 + FU_STRUCT_ELANTP_FIRMWARE_HDR_SIZE, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_elantp_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuElantpFirmware *self = FU_ELANTP_FIRMWARE(firmware); guint16 iap_addr_wrds; guint16 force_table_addr_wrds; guint16 module_id_wrds; g_autoptr(GError) error_local = NULL; /* presumably in words */ if (!fu_input_stream_read_u16(stream, ETP_IAP_START_ADDR_WRDS * 2, &iap_addr_wrds, G_LITTLE_ENDIAN, error)) return FALSE; if (iap_addr_wrds < ETP_IAP_START_ADDR_WRDS || iap_addr_wrds > 0x7FFF) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "IAP address invalid: 0x%x", iap_addr_wrds); return FALSE; } self->iap_addr = iap_addr_wrds * 2; /* read module ID */ if (!fu_input_stream_read_u16(stream, self->iap_addr, &module_id_wrds, G_LITTLE_ENDIAN, error)) return FALSE; if (module_id_wrds > 0x7FFF) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "module ID address invalid: 0x%x", module_id_wrds); return FALSE; } if (!fu_input_stream_read_u16(stream, module_id_wrds * 2, &self->module_id, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_input_stream_read_u16(stream, ETP_IC_TYPE_ADDR_WRDS * 2, &self->ic_type, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_input_stream_read_u16(stream, ETP_IAP_VER_ADDR_WRDS * 2, &self->iap_ver, G_LITTLE_ENDIAN, error)) return FALSE; if (self->ic_type != 0x12 && self->ic_type != 0x13) return TRUE; if (self->iap_ver <= 4) { if (!fu_input_stream_read_u16(stream, self->iap_addr + 6, &force_table_addr_wrds, G_LITTLE_ENDIAN, &error_local)) { g_debug("forcetable address wrong: %s", error_local->message); return TRUE; } } else { if (!fu_input_stream_read_u16(stream, ETP_IAP_FORCETABLE_ADDR_V5 * 2, &force_table_addr_wrds, G_LITTLE_ENDIAN, &error_local)) { g_debug("forcetable address wrong: %s", error_local->message); return TRUE; } } if (force_table_addr_wrds % 32 == 0) { self->force_table_addr = force_table_addr_wrds * 2; self->force_table_support = TRUE; } /* success */ return TRUE; } static gboolean fu_elantp_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { FuElantpFirmware *self = FU_ELANTP_FIRMWARE(firmware); guint64 tmp; /* two simple properties */ tmp = xb_node_query_text_as_uint(n, "module_id", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT16) self->module_id = tmp; tmp = xb_node_query_text_as_uint(n, "iap_addr", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT16) self->iap_addr = tmp; /* success */ return TRUE; } static GByteArray * fu_elantp_firmware_write(FuFirmware *firmware, GError **error) { FuElantpFirmware *self = FU_ELANTP_FIRMWARE(firmware); g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GBytes) blob = NULL; /* only one image supported */ blob = fu_firmware_get_bytes_with_patches(firmware, error); if (blob == NULL) return NULL; /* lets build a simple firmware like this: * ------ 0x0 * HEADER (containing IAP offset and module ID) * ------ ~0x10a * DATA * ------ * SIGNATURE * ------ */ fu_byte_array_set_size(buf, self->iap_addr + 0x2 + 0x2, 0x00); if (!fu_memwrite_uint16_safe(buf->data, buf->len, ETP_IAP_START_ADDR_WRDS * 2, self->iap_addr / 2, G_LITTLE_ENDIAN, error)) return NULL; if (!fu_memwrite_uint16_safe(buf->data, buf->len, self->iap_addr, (self->iap_addr + 2) / 2, G_LITTLE_ENDIAN, error)) return NULL; if (!fu_memwrite_uint16_safe(buf->data, buf->len, self->iap_addr + 0x2, self->module_id, G_LITTLE_ENDIAN, error)) return NULL; fu_byte_array_append_bytes(buf, blob); g_byte_array_append(buf, elantp_signature, sizeof(elantp_signature)); return g_steal_pointer(&buf); } static void fu_elantp_firmware_init(FuElantpFirmware *self) { } static void fu_elantp_firmware_class_init(FuElantpFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_elantp_firmware_validate; firmware_class->parse = fu_elantp_firmware_parse; firmware_class->build = fu_elantp_firmware_build; firmware_class->write = fu_elantp_firmware_write; firmware_class->export = fu_elantp_firmware_export; } FuFirmware * fu_elantp_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_ELANTP_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/elantp/fu-elantp-firmware.h000066400000000000000000000012671501337203100215420ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ELANTP_FIRMWARE (fu_elantp_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuElantpFirmware, fu_elantp_firmware, FU, ELANTP_FIRMWARE, FuFirmware) FuFirmware * fu_elantp_firmware_new(void); guint16 fu_elantp_firmware_get_module_id(FuElantpFirmware *self); guint16 fu_elantp_firmware_get_ic_type(FuElantpFirmware *self); guint16 fu_elantp_firmware_get_iap_addr(FuElantpFirmware *self); gboolean fu_elantp_firmware_get_forcetable_support(FuElantpFirmware *self); guint32 fu_elantp_firmware_get_forcetable_addr(FuElantpFirmware *self); fwupd-2.0.10/plugins/elantp/fu-elantp-haptic-firmware.c000066400000000000000000000051221501337203100227750ustar00rootroot00000000000000/* * Copyright 2022 Jingle Wu * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-elantp-common.h" #include "fu-elantp-haptic-firmware.h" #include "fu-elantp-struct.h" struct _FuElantpHapticFirmware { FuFirmwareClass parent_instance; guint16 driver_ic; }; G_DEFINE_TYPE(FuElantpHapticFirmware, fu_elantp_haptic_firmware, FU_TYPE_FIRMWARE) guint16 fu_elantp_haptic_firmware_get_driver_ic(FuElantpHapticFirmware *self) { g_return_val_if_fail(FU_IS_ELANTP_HAPTIC_FIRMWARE(self), 0); return self->driver_ic; } static void fu_elantp_haptic_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuElantpHapticFirmware *self = FU_ELANTP_HAPTIC_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "driver_ic", self->driver_ic); } static gboolean fu_elantp_haptic_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_elantp_haptic_firmware_hdr_validate_stream(stream, offset, error); } static gboolean fu_elantp_haptic_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuElantpHapticFirmware *self = FU_ELANTP_HAPTIC_FIRMWARE(firmware); guint8 v_s = 0; guint8 v_d = 0; guint8 v_m = 0; guint8 v_y = 0; guint8 tmp = 0; g_autofree gchar *version_str = NULL; if (!fu_input_stream_read_u8(stream, 0x4, &tmp, error)) return FALSE; v_m = tmp & 0xF; v_s = (tmp & 0xF0) >> 4; if (!fu_input_stream_read_u8(stream, 0x5, &v_d, error)) return FALSE; if (!fu_input_stream_read_u8(stream, 0x6, &v_y, error)) return FALSE; if (v_y == 0xFF || v_d == 0xFF || v_m == 0xF) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "bad firmware version %02d%02d%02d%02d", v_y, v_m, v_d, v_s); return FALSE; } version_str = g_strdup_printf("%02d%02d%02d%02d", v_y, v_m, v_d, v_s); fu_firmware_set_version(FU_FIRMWARE(self), version_str); /* success */ self->driver_ic = 0x2; return TRUE; } static void fu_elantp_haptic_firmware_init(FuElantpHapticFirmware *self) { } static void fu_elantp_haptic_firmware_class_init(FuElantpHapticFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_elantp_haptic_firmware_validate; firmware_class->parse = fu_elantp_haptic_firmware_parse; firmware_class->export = fu_elantp_haptic_firmware_export; } FuFirmware * fu_elantp_haptic_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_ELANTP_HAPTIC_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/elantp/fu-elantp-haptic-firmware.h000066400000000000000000000007611501337203100230060ustar00rootroot00000000000000/* * Copyright 2022 Jingle Wu * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ELANTP_HAPTIC_FIRMWARE (fu_elantp_haptic_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuElantpHapticFirmware, fu_elantp_haptic_firmware, FU, ELANTP_HAPTIC_FIRMWARE, FuFirmware) FuFirmware * fu_elantp_haptic_firmware_new(void); guint16 fu_elantp_haptic_firmware_get_driver_ic(FuElantpHapticFirmware *self); fwupd-2.0.10/plugins/elantp/fu-elantp-hid-device.c000066400000000000000000000672471501337203100217340ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-elantp-common.h" #include "fu-elantp-firmware.h" #include "fu-elantp-hid-device.h" #include "fu-elantp-hid-haptic-device.h" struct _FuElantpHidDevice { FuHidrawDevice parent_instance; guint16 ic_page_count; guint16 ic_type; guint16 iap_type; guint16 iap_ctrl; guint16 iap_password; guint16 iap_ver; guint16 module_id; guint16 fw_page_size; gboolean force_table_support; guint32 force_table_addr; guint8 pattern; }; G_DEFINE_TYPE(FuElantpHidDevice, fu_elantp_hid_device, FU_TYPE_HIDRAW_DEVICE) static gboolean fu_elantp_hid_device_detach(FuDevice *device, FuProgress *progress, GError **error); static void fu_elantp_hid_device_to_string(FuDevice *device, guint idt, GString *str) { FuElantpHidDevice *self = FU_ELANTP_HID_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "ModuleId", self->module_id); fwupd_codec_string_append_hex(str, idt, "Pattern", self->pattern); fwupd_codec_string_append_hex(str, idt, "FwPageSize", self->fw_page_size); fwupd_codec_string_append_hex(str, idt, "IcPageCount", self->ic_page_count); fwupd_codec_string_append_hex(str, idt, "IapType", self->iap_type); fwupd_codec_string_append_hex(str, idt, "IapCtrl", self->iap_ctrl); } static gboolean fu_elantp_hid_device_probe(FuDevice *device, GError **error) { guint16 device_id = fu_device_get_pid(device); /* check is valid */ if (g_strcmp0(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device)), "hidraw") != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "is not correct subsystem=%s, expected hidraw", fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device))); return FALSE; } /* i2c-hid */ if (device_id != 0x400 && (device_id < 0x3000 || device_id >= 0x4000)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not i2c-hid touchpad"); return FALSE; } /* success */ return TRUE; } static gboolean fu_elantp_hid_device_send_cmd(FuElantpHidDevice *self, guint8 *tx, gsize txsz, guint8 *rx, gsize rxsz, GError **error) { g_autofree guint8 *buf = NULL; gsize bufsz = rxsz + 3; if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), tx, txsz, FU_IOCTL_FLAG_NONE, error)) return FALSE; if (rxsz == 0) return TRUE; /* GetFeature */ buf = g_malloc0(bufsz); buf[0] = tx[0]; /* report number */ if (!fu_hidraw_device_get_feature(FU_HIDRAW_DEVICE(self), buf, bufsz, FU_IOCTL_FLAG_NONE, error)) return FALSE; /* success */ return fu_memcpy_safe(rx, rxsz, 0x0, /* dst */ buf, bufsz, 0x3, /* src */ rxsz, error); } static gboolean fu_elantp_hid_device_read_cmd(FuElantpHidDevice *self, guint16 reg, guint8 *rx, gsize rxsz, GError **error) { guint8 buf[5] = {0x0d, 0x05, 0x03}; fu_memwrite_uint16(buf + 0x3, reg, G_LITTLE_ENDIAN); return fu_elantp_hid_device_send_cmd(self, buf, sizeof(buf), rx, rxsz, error); } static gint fu_elantp_hid_device_write_cmd(FuElantpHidDevice *self, guint16 reg, guint16 cmd, GError **error) { guint8 buf[5] = {0x0d}; fu_memwrite_uint16(buf + 0x1, reg, G_LITTLE_ENDIAN); fu_memwrite_uint16(buf + 0x3, cmd, G_LITTLE_ENDIAN); return fu_elantp_hid_device_send_cmd(self, buf, sizeof(buf), NULL, 0, error); } static gboolean fu_elantp_hid_device_ensure_iap_ctrl(FuElantpHidDevice *self, GError **error) { guint8 buf[2] = {0x0}; if (!fu_elantp_hid_device_read_cmd(self, ETP_CMD_I2C_IAP_CTRL, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read IAPControl: "); return FALSE; } self->iap_ctrl = fu_memread_uint16(buf, G_LITTLE_ENDIAN); /* in bootloader mode? */ if (self->force_table_support && self->iap_ver <= 5) { if ((self->iap_ctrl & ETP_I2C_MAIN_MODE_ON2) == 0) fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); else fu_device_remove_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); return TRUE; } if ((self->iap_ctrl & ETP_I2C_MAIN_MODE_ON) == 0) fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); else fu_device_remove_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); return TRUE; } static gboolean fu_elantp_hid_device_read_force_table_enable(FuElantpHidDevice *self, GError **error) { guint8 buf[2] = {0x0}; guint16 value; if (!fu_elantp_hid_device_read_cmd(self, ETP_CMD_I2C_FORCE_TYPE_ENABLE, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read force type cmd: "); return FALSE; } value = fu_memread_uint16(buf, G_LITTLE_ENDIAN); if (value == 0xFFFF || value == ETP_CMD_I2C_FORCE_TYPE_ENABLE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "forcetype cmd not supported"); return FALSE; } if ((buf[0] & ETP_FW_FORCE_TYPE_ENABLE_BIT) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "force type table not supported"); return FALSE; } /* success */ return TRUE; } static gboolean fu_elantp_hid_device_read_haptic_enable(FuElantpHidDevice *self, GError **error) { guint8 buf[2] = {0x0}; guint16 value; if (!fu_elantp_hid_device_read_cmd(self, ETP_CMD_I2C_FORCE_TYPE_ENABLE, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read haptic enable cmd: "); return FALSE; } value = fu_memread_uint16(buf, G_LITTLE_ENDIAN); if (value == 0xFFFF || value == ETP_CMD_I2C_FORCE_TYPE_ENABLE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not hapticpad"); return FALSE; } if ((buf[0] & ETP_FW_FORCE_TYPE_ENABLE_BIT) == 0 || (buf[0] & ETP_FW_EEPROM_ENABLE_BIT) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "the haptic eeprom not supported"); return FALSE; } /* success */ return TRUE; } static gboolean fu_elantp_hid_device_get_forcetable_address(FuElantpHidDevice *self, GError **error) { guint8 buf[2] = {0x0}; guint16 addr_wrds; if (self->iap_ver == 0x3) { self->force_table_addr = 0xFF40 * 2; return TRUE; } if (!fu_elantp_hid_device_read_cmd(self, ETP_CMD_FORCE_ADDR, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read force table address cmd: "); return FALSE; } addr_wrds = fu_memread_uint16(buf, G_LITTLE_ENDIAN); if (addr_wrds % 32 != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "illegal force table address (%x)", addr_wrds); return FALSE; } self->force_table_addr = addr_wrds * 2; /* success */ return TRUE; } static gboolean fu_elantp_hid_device_write_fw_password(FuElantpHidDevice *self, guint16 ic_type, guint16 iap_ver, GError **error) { guint8 buf[2] = {0x0}; guint16 pw = ETP_I2C_IC13_IAPV5_PW; guint16 value; if (iap_ver < 0x5 || ic_type != 0x13) return TRUE; if (!fu_elantp_hid_device_write_cmd(self, ETP_CMD_I2C_FW_PW, pw, error)) { g_prefix_error(error, "failed to write fw password cmd: "); return FALSE; } if (!fu_elantp_hid_device_read_cmd(self, ETP_CMD_I2C_FW_PW, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read fw password cmd: "); return FALSE; } value = fu_memread_uint16(buf, G_LITTLE_ENDIAN); if (value != pw) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "can't set fw password got:%x", value); return FALSE; } /* success */ return TRUE; } static gboolean fu_elantp_hid_device_setup(FuDevice *device, GError **error) { FuElantpHidDevice *self = FU_ELANTP_HID_DEVICE(device); guint16 fwver; guint16 tmp; guint8 buf[2] = {0x0}; g_autofree gchar *version_bl = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(GError) error_forcetable = NULL; /* get pattern */ if (!fu_elantp_hid_device_read_cmd(self, ETP_CMD_I2C_GET_HID_ID, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read HID ID: "); return FALSE; } tmp = fu_memread_uint16(buf, G_LITTLE_ENDIAN); self->pattern = tmp != 0xFFFF ? (tmp & 0xFF00) >> 8 : 0; /* get current firmware version */ if (!fu_elantp_hid_device_read_cmd(self, ETP_CMD_I2C_FW_VERSION, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read fw version: "); return FALSE; } fwver = fu_memread_uint16(buf, G_LITTLE_ENDIAN); if (fwver == 0xFFFF || fwver == ETP_CMD_I2C_FW_VERSION) fwver = 0; fu_device_set_version_raw(device, fwver); /* get IAP firmware version */ if (!fu_elantp_hid_device_read_cmd(self, self->pattern == 0 ? ETP_CMD_I2C_IAP_VERSION : ETP_CMD_I2C_IAP_VERSION_2, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read bootloader version: "); return FALSE; } if (self->pattern >= 1) { self->iap_ver = buf[1]; } else { self->iap_ver = fu_memread_uint16(buf, G_LITTLE_ENDIAN); } version_bl = fu_version_from_uint16(self->iap_ver, FWUPD_VERSION_FORMAT_HEX); fu_device_set_version_bootloader(device, version_bl); /* get module ID */ if (!fu_elantp_hid_device_read_cmd(self, ETP_CMD_GET_MODULE_ID, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read module ID: "); return FALSE; } self->module_id = fu_memread_uint16(buf, G_LITTLE_ENDIAN); /* define the extra instance IDs */ fu_device_add_instance_u16(device, "VEN", fu_device_get_vid(device)); fu_device_add_instance_u16(device, "DEV", fu_device_get_pid(device)); fu_device_add_instance_u16(device, "MOD", self->module_id); if (!fu_device_build_instance_id(device, error, "HIDRAW", "VEN", "DEV", "MOD", NULL)) return FALSE; /* get OSM version */ if (!fu_elantp_hid_device_read_cmd(self, ETP_CMD_I2C_OSM_VERSION, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read OSM version: "); return FALSE; } tmp = fu_memread_uint16(buf, G_LITTLE_ENDIAN); if (tmp == ETP_CMD_I2C_OSM_VERSION || tmp == 0xFFFF) { if (!fu_elantp_hid_device_read_cmd(self, ETP_CMD_I2C_IAP_ICBODY, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read IC body: "); return FALSE; } self->ic_type = fu_memread_uint16(buf, G_LITTLE_ENDIAN) & 0xFF; } else { self->ic_type = (tmp >> 8) & 0xFF; } /* define the extra instance IDs (ic_type + module_id + driver) */ fu_device_add_instance_u8(device, "ICTYPE", self->ic_type); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "ELANTP", "ICTYPE", NULL); fu_device_build_instance_id(device, NULL, "ELANTP", "ICTYPE", "MOD", NULL); fu_device_add_instance_str(device, "DRIVER", "HID"); fu_device_build_instance_id(device, NULL, "ELANTP", "ICTYPE", "MOD", "DRIVER", NULL); /* no quirk entry */ if (self->ic_page_count == 0x0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no page count for ELANTP\\ICTYPE_%02X", self->ic_type); return FALSE; } /* The ic_page_count is based on 64 bytes/page. */ fu_device_set_firmware_size(device, (guint64)self->ic_page_count * (guint64)64); /* is in bootloader mode */ if (!fu_elantp_hid_device_ensure_iap_ctrl(self, error)) return FALSE; if (self->ic_type != 0x12 && self->ic_type != 0x13) return TRUE; if (!fu_elantp_hid_device_read_force_table_enable(self, &error_forcetable)) { g_debug("no forcetable detected: %s", error_forcetable->message); } else { if (!fu_elantp_hid_device_get_forcetable_address(self, error)) { g_prefix_error(error, "get forcetable address fail: "); return FALSE; } self->force_table_support = TRUE; /* is in bootloader mode */ if (!fu_elantp_hid_device_ensure_iap_ctrl(self, error)) return FALSE; } if (!fu_elantp_hid_device_read_haptic_enable(self, &error_local)) { g_debug("no haptic device detected: %s", error_local->message); } else { g_autoptr(FuElantpHidHapticDevice) cfg = fu_elantp_hid_haptic_device_new(device); fu_device_add_child(FU_DEVICE(device), FU_DEVICE(cfg)); } /* fix an unsuitable i²c name, e.g. `VEN 04F3:00 04F3:3XXX` */ if (g_str_has_prefix(fu_device_get_name(device), "VEN 04F3:00 04F3:3")) fu_device_set_name(device, "Touchpad"); /* success */ return TRUE; } static FuFirmware * fu_elantp_hid_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuElantpHidDevice *self = FU_ELANTP_HID_DEVICE(device); guint16 module_id; guint16 ic_type; gboolean force_table_support; g_autoptr(FuFirmware) firmware = fu_elantp_firmware_new(); /* check is compatible with hardware */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; module_id = fu_elantp_firmware_get_module_id(FU_ELANTP_FIRMWARE(firmware)); if (self->module_id != module_id) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware incompatible, got 0x%04x, expected 0x%04x", module_id, self->module_id); return NULL; } ic_type = fu_elantp_firmware_get_ic_type(FU_ELANTP_FIRMWARE(firmware)); if (self->ic_type != ic_type) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware ic type incompatible, got 0x%04x, expected 0x%04x", ic_type, self->ic_type); return NULL; } force_table_support = fu_elantp_firmware_get_forcetable_support(FU_ELANTP_FIRMWARE(firmware)); if (self->force_table_support != force_table_support) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware incompatible, forcetable incorrect."); return NULL; } if (self->force_table_support) { guint32 force_table_addr; guint32 diff_size; force_table_addr = fu_elantp_firmware_get_forcetable_addr(FU_ELANTP_FIRMWARE(firmware)); if (self->force_table_addr < force_table_addr) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware forcetable address incompatible, got 0x%04x, expected 0x%04x", force_table_addr / 2, self->force_table_addr / 2); return NULL; } diff_size = self->force_table_addr - force_table_addr; if (diff_size % 64 != 0) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware forcetable address incompatible, got 0x%04x, expected 0x%04x", force_table_addr / 2, self->force_table_addr / 2); return NULL; } } /* success */ return g_steal_pointer(&firmware); } static gboolean fu_elantp_hid_device_filling_forcetable_firmware(FuDevice *device, guint8 *fw_data, gsize fw_size, guint32 force_table_addr, GError **error) { FuElantpHidDevice *self = FU_ELANTP_HID_DEVICE(device); const guint8 fillature[] = {0x77, 0x33, 0x44, 0xaa}; const guint8 signature[] = {0xAA, 0x55, 0xCC, 0x33, 0xFF, 0xFF}; guint8 buf[64] = {[0 ... 63] = 0xFF}; guint16 block_checksum; guint16 filling_value; if (self->force_table_addr == force_table_addr) return TRUE; if (self->force_table_addr < force_table_addr) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "forcetable address wrong (%x,%x): ", force_table_addr, self->force_table_addr); return FALSE; } if (!fu_memcpy_safe(buf, sizeof(buf), 0, /* dst */ fillature, sizeof(fillature), 0x0, /* src */ sizeof(fillature), error)) return FALSE; fu_memwrite_uint16(buf + 0x4, self->force_table_addr / 2, G_LITTLE_ENDIAN); if (!fu_memcpy_safe(buf, sizeof(buf), sizeof(buf) - 6, /* dst */ signature, sizeof(signature), 0x0, /* src */ sizeof(signature), error)) return FALSE; block_checksum = fu_sum16w(buf, sizeof(buf), G_LITTLE_ENDIAN) - 0xFFFF; filling_value = 0x10000 - (block_checksum & 0xFFFF); fu_memwrite_uint16(buf + 0x6, filling_value, G_LITTLE_ENDIAN); for (guint i = force_table_addr; i < self->force_table_addr; i += 64) { if (!fu_memcpy_safe(fw_data, fw_size, i, /* dst */ buf, sizeof(buf), 0x0, /* src */ sizeof(buf), error)) return FALSE; } return TRUE; } static gboolean fu_elantp_hid_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuElantpHidDevice *self = FU_ELANTP_HID_DEVICE(device); FuElantpFirmware *firmware_elantp = FU_ELANTP_FIRMWARE(firmware); gsize bufsz = 0; guint16 checksum = 0; guint16 checksum_device = 0; guint16 iap_addr; const guint8 *buf; guint8 csum_buf[2] = {0x0}; g_autoptr(GBytes) fw = NULL; g_autoptr(GPtrArray) chunks = NULL; guint total_pages; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 10, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 50, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 30, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 10, "reset"); /* simple image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* detach */ if (!fu_elantp_hid_device_detach(device, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* write each block */ buf = g_bytes_get_data(fw, &bufsz); iap_addr = fu_elantp_firmware_get_iap_addr(firmware_elantp); if (self->force_table_support && self->force_table_addr >= fu_elantp_firmware_get_forcetable_addr(FU_ELANTP_FIRMWARE(firmware))) { g_autofree guint8 *buf2 = g_malloc0(bufsz); if (!fu_memcpy_safe(buf2, bufsz, 0x0, /* dst */ buf, bufsz, 0x0, /* src */ bufsz, error)) return FALSE; if (!fu_elantp_hid_device_filling_forcetable_firmware( device, buf2, bufsz, fu_elantp_firmware_get_forcetable_addr(FU_ELANTP_FIRMWARE(firmware)), error)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "filling forcetable failed"); return FALSE; } chunks = fu_chunk_array_new(buf2 + iap_addr, bufsz - iap_addr, 0x0, 0x0, self->fw_page_size); total_pages = (self->force_table_addr - iap_addr - 1) / self->fw_page_size + 1; if (total_pages > chunks->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "total pages wrong (%u)", total_pages); return FALSE; } } else { chunks = fu_chunk_array_new(buf + iap_addr, bufsz - iap_addr, 0x0, 0x0, self->fw_page_size); total_pages = chunks->len; } for (guint i = 0; i < total_pages; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); guint16 csum_tmp = fu_sum16w(fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), G_LITTLE_ENDIAN); gsize blksz = self->fw_page_size + 3; g_autofree guint8 *blk = g_malloc0(blksz); /* write block */ blk[0] = 0x0B; /* report ID */ if (!fu_memcpy_safe(blk, blksz, 0x1, /* dst */ fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0x0, /* src */ fu_chunk_get_data_sz(chk), error)) return FALSE; fu_memwrite_uint16(blk + fu_chunk_get_data_sz(chk) + 1, csum_tmp, G_LITTLE_ENDIAN); if (!fu_elantp_hid_device_send_cmd(self, blk, blksz, NULL, 0, error)) return FALSE; fu_device_sleep(device, self->fw_page_size == 512 ? ELANTP_DELAY_WRITE_BLOCK_512 : ELANTP_DELAY_WRITE_BLOCK); if (!fu_elantp_hid_device_ensure_iap_ctrl(self, error)) return FALSE; if (self->iap_ctrl & (ETP_FW_IAP_PAGE_ERR | ETP_FW_IAP_INTF_ERR)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "bootloader reports failed write: 0x%x", self->iap_ctrl); return FALSE; } /* update progress */ checksum += csum_tmp; fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)i + 1, (gsize)chunks->len); } fu_progress_step_done(progress); /* verify the written checksum */ if (!fu_elantp_hid_device_read_cmd(self, ETP_CMD_I2C_IAP_CHECKSUM, csum_buf, sizeof(csum_buf), error)) return FALSE; if (!fu_memread_uint16_safe(csum_buf, sizeof(csum_buf), 0x0, &checksum_device, G_LITTLE_ENDIAN, error)) return FALSE; if (checksum != checksum_device) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "checksum failed 0x%04x != 0x%04x", checksum, checksum_device); return FALSE; } fu_progress_step_done(progress); /* wait for a reset */ fu_device_sleep_full(device, ELANTP_DELAY_COMPLETE, fu_progress_get_child(progress)); /* ms */ fu_progress_step_done(progress); return TRUE; } static gboolean fu_elantp_hid_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuElantpHidDevice *self = FU_ELANTP_HID_DEVICE(device); guint16 iap_ver; guint16 ic_type; guint8 buf[2] = {0x0}; guint16 tmp; /* sanity check */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_info("in bootloader mode, reset IC"); if (!fu_elantp_hid_device_write_cmd(self, ETP_CMD_I2C_IAP_RESET, ETP_I2C_IAP_RESET, error)) return FALSE; fu_device_sleep(FU_DEVICE(self), ELANTP_DELAY_RESET); } /* get OSM version */ if (!fu_elantp_hid_device_read_cmd(self, ETP_CMD_I2C_OSM_VERSION, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read OSM version: "); return FALSE; } tmp = fu_memread_uint16(buf, G_LITTLE_ENDIAN); if (tmp == ETP_CMD_I2C_OSM_VERSION || tmp == 0xFFFF) { if (!fu_elantp_hid_device_read_cmd(self, ETP_CMD_I2C_IAP_ICBODY, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read IC body: "); return FALSE; } ic_type = fu_memread_uint16(buf, G_LITTLE_ENDIAN) & 0xFF; } else { ic_type = (tmp >> 8) & 0xFF; } /* get IAP firmware version */ if (!fu_elantp_hid_device_read_cmd(self, self->pattern == 0 ? ETP_CMD_I2C_IAP_VERSION : ETP_CMD_I2C_IAP_VERSION_2, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read bootloader version: "); return FALSE; } if (self->pattern >= 1) { iap_ver = buf[1]; } else { iap_ver = fu_memread_uint16(buf, G_LITTLE_ENDIAN); } /* set the page size */ self->fw_page_size = 64; if (ic_type >= 0x10) { if (iap_ver >= 1) { /* set the IAP type, presumably some kind of ABI */ if (iap_ver >= 2 && (ic_type == 0x14 || ic_type == 0x15)) { self->fw_page_size = 512; } else { self->fw_page_size = 128; } if (!fu_elantp_hid_device_write_cmd(self, ETP_CMD_I2C_IAP_TYPE, self->fw_page_size / 2, error)) return FALSE; if (!fu_elantp_hid_device_read_cmd(self, ETP_CMD_I2C_IAP_TYPE, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read IAP type: "); return FALSE; } self->iap_type = fu_memread_uint16(buf, G_LITTLE_ENDIAN); if (self->iap_type != self->fw_page_size / 2) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to set IAP type"); return FALSE; } } } if (!fu_elantp_hid_device_write_fw_password(self, ic_type, iap_ver, error)) return FALSE; if (!fu_elantp_hid_device_write_cmd(self, ETP_CMD_I2C_IAP, self->iap_password, error)) return FALSE; fu_device_sleep(FU_DEVICE(self), ELANTP_DELAY_UNLOCK); if (!fu_elantp_hid_device_ensure_iap_ctrl(self, error)) return FALSE; if ((self->iap_ctrl & ETP_FW_IAP_CHECK_PW) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "unexpected bootloader password"); return FALSE; } /* success */ return TRUE; } static gboolean fu_elantp_hid_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuElantpHidDevice *self = FU_ELANTP_HID_DEVICE(device); /* sanity check */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in runtime mode, skipping"); return TRUE; } /* reset back to runtime */ if (!fu_elantp_hid_device_write_cmd(self, ETP_CMD_I2C_IAP_RESET, ETP_I2C_IAP_RESET, error)) return FALSE; fu_device_sleep(FU_DEVICE(self), ELANTP_DELAY_RESET); if (!fu_elantp_hid_device_write_cmd(self, ETP_CMD_I2C_IAP_RESET, ETP_I2C_ENABLE_REPORT, error)) { g_prefix_error(error, "cannot enable TP report: "); return FALSE; } if (!fu_elantp_hid_device_write_cmd(self, 0x0306, 0x003, error)) { g_prefix_error(error, "cannot switch to TP PTP mode: "); return FALSE; } if (!fu_elantp_hid_device_ensure_iap_ctrl(self, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_elantp_hid_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuElantpHidDevice *self = FU_ELANTP_HID_DEVICE(device); guint64 tmp = 0; if (g_strcmp0(key, "ElantpIcPageCount") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->ic_page_count = (guint16)tmp; return TRUE; } if (g_strcmp0(key, "ElantpIapPassword") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->iap_password = (guint16)tmp; return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_elantp_hid_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static gchar * fu_elantp_hid_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint16(version_raw, fu_device_get_version_format(device)); } static void fu_elantp_hid_device_init(FuElantpHidDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_set_summary(FU_DEVICE(self), "Touchpad"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_INPUT_TOUCHPAD); fu_device_add_protocol(FU_DEVICE(self), "tw.com.emc.elantp"); fu_device_set_vendor(FU_DEVICE(self), "ELAN Microelectronics"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_HEX); fu_device_set_priority(FU_DEVICE(self), 1); /* better than i2c */ fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_NONBLOCK); } static void fu_elantp_hid_device_class_init(FuElantpHidDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_elantp_hid_device_to_string; device_class->attach = fu_elantp_hid_device_attach; device_class->set_quirk_kv = fu_elantp_hid_device_set_quirk_kv; device_class->setup = fu_elantp_hid_device_setup; device_class->reload = fu_elantp_hid_device_setup; device_class->write_firmware = fu_elantp_hid_device_write_firmware; device_class->prepare_firmware = fu_elantp_hid_device_prepare_firmware; device_class->probe = fu_elantp_hid_device_probe; device_class->set_progress = fu_elantp_hid_device_set_progress; device_class->convert_version = fu_elantp_hid_device_convert_version; } fwupd-2.0.10/plugins/elantp/fu-elantp-hid-device.h000066400000000000000000000005011501337203100217150ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ELANTP_HID_DEVICE (fu_elantp_hid_device_get_type()) G_DECLARE_FINAL_TYPE(FuElantpHidDevice, fu_elantp_hid_device, FU, ELANTP_HID_DEVICE, FuHidrawDevice) fwupd-2.0.10/plugins/elantp/fu-elantp-hid-haptic-device.c000066400000000000000000001000751501337203100231650ustar00rootroot00000000000000/* * Copyright 2022 Jingle Wu * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-elantp-common.h" #include "fu-elantp-haptic-firmware.h" #include "fu-elantp-hid-haptic-device.h" struct _FuElantpHidHapticDevice { FuUdevDevice parent_instance; guint16 ic_page_count; guint16 iap_type; guint16 tp_iap_ctrl; guint16 tp_iap_ver; guint16 tp_ic_type; guint16 iap_ctrl; guint16 iap_password; guint16 module_id; guint16 fw_page_size; guint8 pattern; gint16 driver_ic; guint8 iap_ver; }; G_DEFINE_TYPE(FuElantpHidHapticDevice, fu_elantp_hid_haptic_device, FU_TYPE_UDEV_DEVICE) static FuElantpHidDevice * fu_elantp_hid_haptic_device_get_parent(FuDevice *self, GError **error) { FuDevice *parent = fu_device_get_parent(FU_DEVICE(self)); if (parent == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "no parent set"); return NULL; } return FU_ELANTP_HID_DEVICE(FU_UDEV_DEVICE(parent)); } static gboolean fu_elantp_hid_haptic_device_detach(FuDevice *device, FuProgress *progress, GError **error); static void fu_elantp_hid_haptic_device_to_string(FuDevice *device, guint idt, GString *str) { FuElantpHidHapticDevice *self = FU_ELANTP_HID_HAPTIC_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "ModuleId", self->module_id); fwupd_codec_string_append_hex(str, idt, "Pattern", self->pattern); fwupd_codec_string_append_hex(str, idt, "FwPageSize", self->fw_page_size); fwupd_codec_string_append_hex(str, idt, "IcPageCount", self->ic_page_count); fwupd_codec_string_append_hex(str, idt, "IapType", self->iap_type); fwupd_codec_string_append_hex(str, idt, "TpIapCtrl", self->tp_iap_ctrl); fwupd_codec_string_append_hex(str, idt, "IapCtrl", self->iap_ctrl); fwupd_codec_string_append_hex(str, idt, "DriverIC", self->driver_ic); fwupd_codec_string_append_hex(str, idt, "IAPVersion", self->iap_ver); } static gboolean fu_elantp_hid_haptic_device_send_cmd(FuDevice *self, const guint8 *tx, gsize txsz, guint8 *rx, gsize rxsz, GError **error) { g_autofree guint8 *buf = NULL; gsize bufsz = rxsz + 3; if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), tx, txsz, FU_IOCTL_FLAG_NONE, error)) return FALSE; if (rxsz == 0) return TRUE; /* GetFeature */ buf = g_malloc0(bufsz); buf[0] = tx[0]; /* report number */ if (!fu_hidraw_device_get_feature(FU_HIDRAW_DEVICE(self), buf, bufsz, FU_IOCTL_FLAG_NONE, error)) return FALSE; /* success */ return fu_memcpy_safe(rx, rxsz, 0x0, /* dst */ buf, bufsz, 0x3, /* src */ rxsz, error); } static gboolean fu_elantp_hid_haptic_device_read_cmd(FuDevice *self, guint16 reg, guint8 *buf, gsize bufz, GError **error) { guint8 tmp[5] = {0x0D, 0x05, 0x03}; fu_memwrite_uint16(tmp + 0x3, reg, G_LITTLE_ENDIAN); return fu_elantp_hid_haptic_device_send_cmd(self, tmp, sizeof(tmp), buf, bufz, error); } static gint fu_elantp_hid_haptic_device_write_cmd(FuDevice *self, guint16 reg, guint16 cmd, GError **error) { guint8 buf[5] = {0x0D}; fu_memwrite_uint16(buf + 0x1, reg, G_LITTLE_ENDIAN); fu_memwrite_uint16(buf + 0x3, cmd, G_LITTLE_ENDIAN); return fu_elantp_hid_haptic_device_send_cmd(self, buf, sizeof(buf), NULL, 0, error); } static gboolean fu_elantp_hid_haptic_device_ensure_iap_ctrl(FuDevice *parent, FuElantpHidHapticDevice *self, GError **error) { guint8 buf[2] = {0x0}; if (!fu_elantp_hid_haptic_device_read_cmd(parent, ETP_CMD_I2C_IAP_CTRL, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read IAPControl: "); return FALSE; } self->tp_iap_ctrl = fu_memread_uint16(buf, G_LITTLE_ENDIAN); /* in bootloader mode? */ if (self->tp_iap_ver <= 5) { if ((self->tp_iap_ctrl & ETP_I2C_MAIN_MODE_ON2) == 0) fu_device_add_flag(FU_DEVICE(parent), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); else fu_device_remove_flag(FU_DEVICE(parent), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); return TRUE; } if ((self->tp_iap_ctrl & ETP_I2C_MAIN_MODE_ON) == 0) fu_device_add_flag(FU_DEVICE(parent), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); else fu_device_remove_flag(FU_DEVICE(parent), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); return TRUE; } static gboolean fu_elantp_hid_haptic_device_ensure_eeprom_iap_ctrl(FuDevice *parent, FuElantpHidHapticDevice *self, GError **error) { guint8 buf[2] = {0x0}; if (!fu_elantp_hid_haptic_device_read_cmd(parent, ETP_CMD_I2C_SET_EEPROM_CTRL, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read IAPControl: "); return FALSE; } self->iap_ctrl = fu_memread_uint16(buf, G_LITTLE_ENDIAN); if ((self->iap_ctrl & 0x800) != 0x800) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "bit11 fail"); return FALSE; } if ((self->iap_ctrl & 0x1000) == 0x1000) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "bit12 fail, resend"); return FALSE; } return TRUE; } static gboolean fu_elantp_hid_haptic_device_get_haptic_driver_ic(FuDevice *parent, FuElantpHidHapticDevice *self, GError **error) { guint8 buf[2] = {0x0}; guint16 value; if (!fu_elantp_hid_haptic_device_read_cmd(parent, ETP_CMD_I2C_FORCE_TYPE_ENABLE, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read haptic enable cmd: "); return FALSE; } value = fu_memread_uint16(buf, G_LITTLE_ENDIAN); if (value == 0xFFFF || value == ETP_CMD_I2C_FORCE_TYPE_ENABLE) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to read haptic enable cmd"); return FALSE; } if ((buf[0] & ETP_FW_FORCE_TYPE_ENABLE_BIT) == 0 || (buf[0] & ETP_FW_EEPROM_ENABLE_BIT) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "eeprom enable bit unset"); return FALSE; } /* success */ self->driver_ic = (buf[0] >> 4) & 0xF; return TRUE; } static gboolean fu_elantp_hid_haptic_device_get_version(FuDevice *parent, FuElantpHidHapticDevice *self, GError **error) { guint16 v_s = 0; guint16 v_d = 0; guint16 v_m = 0; guint16 v_y = 0; guint8 buf[2] = {0x0}; if (!fu_elantp_hid_haptic_device_write_cmd(parent, ETP_CMD_I2C_SET_EEPROM_CTRL, ETP_CMD_I2C_GET_EEPROM_FW_VERSION, error)) { g_prefix_error(error, "failed to write haptic version cmd: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), ELANTP_DELAY_RESET); if (!fu_elantp_hid_haptic_device_read_cmd(parent, 0x0321, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read haptic version cmd: "); return FALSE; } v_d = buf[0]; v_m = buf[1] & 0xF; v_s = (buf[1] & 0xF0) >> 4; if (!fu_elantp_hid_haptic_device_write_cmd(parent, ETP_CMD_I2C_SET_EEPROM_CTRL, ETP_CMD_I2C_GET_EEPROM_IAP_VERSION, error)) { g_prefix_error(error, "failed to write haptic iap version cmd: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), ELANTP_DELAY_RESET); if (!fu_elantp_hid_haptic_device_read_cmd(parent, 0x0321, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read haptic iap version cmd: "); return FALSE; } v_y = buf[0]; self->iap_ver = buf[1]; if (v_y == 0xFF && v_d == 0xFF && v_m == 0xF) { fu_device_set_version(FU_DEVICE(self), "0"); } else { g_autofree gchar *str = g_strdup_printf("%02d%02d%02d%02d", v_y, v_m, v_d, v_s); fu_device_set_version(FU_DEVICE(self), str); } return TRUE; } static gboolean fu_elantp_hid_haptic_device_write_fw_password(FuDevice *parent, guint16 tp_ic_type, guint16 tp_iap_ver, GError **error) { guint8 buf[2] = {0x0}; guint16 pw = ETP_I2C_IC13_IAPV5_PW; guint16 value; if (tp_iap_ver < 0x5 || tp_ic_type != 0x13) return TRUE; if (!fu_elantp_hid_haptic_device_write_cmd(parent, ETP_CMD_I2C_FW_PW, pw, error)) { g_prefix_error(error, "failed to write fw password cmd: "); return FALSE; } if (!fu_elantp_hid_haptic_device_read_cmd(parent, ETP_CMD_I2C_FW_PW, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read fw password cmd: "); return FALSE; } value = fu_memread_uint16(buf, G_LITTLE_ENDIAN); if (value != pw) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "can't set fw password got:%x", value); return FALSE; } /* success */ return TRUE; } typedef struct { guint16 checksum; guint16 iap_password; guint16 tp_iap_ver; guint16 tp_ic_type; } FuElantpHaptictpWaitFlashEEPROMChecksumHelper; static gboolean fu_elantp_hid_haptic_device_write_checksum_cb(FuDevice *parent, gpointer user_data, GError **error) { guint8 buf[2] = {0x0}; guint16 value; FuElantpHaptictpWaitFlashEEPROMChecksumHelper *helper = user_data; if (!fu_elantp_hid_haptic_device_write_cmd(parent, ETP_CMD_I2C_EEPROM_SETTING, ETP_CMD_I2C_EEPROM_WRITE_INFORMATION, error)) { g_prefix_error(error, "failed to write haptic info: "); return FALSE; } if (!fu_elantp_hid_haptic_device_read_cmd(parent, ETP_CMD_I2C_EEPROM_SETTING, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read haptic info: "); return FALSE; } value = fu_memread_uint16(buf, G_LITTLE_ENDIAN); if ((value & 0xFFFF) != ETP_CMD_I2C_EEPROM_WRITE_INFORMATION) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to set haptic info (0x%04x): ", value); return FALSE; } if (!fu_elantp_hid_haptic_device_write_fw_password(parent, helper->tp_ic_type, helper->tp_iap_ver, error)) return FALSE; if (!fu_elantp_hid_haptic_device_write_cmd(parent, ETP_CMD_I2C_IAP, helper->iap_password, error)) { g_prefix_error(error, "failed to write iap password: "); return FALSE; } if (!fu_elantp_hid_haptic_device_write_cmd(parent, ETP_CMD_I2C_EEPROM_WRITE_CHECKSUM, helper->checksum, error)) { g_prefix_error(error, "failed to write eeprom checksum: "); return FALSE; } if (!fu_elantp_hid_haptic_device_write_cmd(parent, ETP_CMD_I2C_EEPROM_SETTING, ETP_CMD_I2C_EEPROM_SETTING_INITIAL, error)) { g_prefix_error(error, "failed to set haptic initial setting: "); return FALSE; } if (!fu_elantp_hid_haptic_device_read_cmd(parent, ETP_CMD_I2C_EEPROM_WRITE_CHECKSUM, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read haptic checksum: "); return FALSE; } value = fu_memread_uint16(buf, G_LITTLE_ENDIAN); if ((value & 0xFFFF) != helper->checksum) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "eeprom checksum failed 0x%04x != 0x%04x : ", value, helper->checksum); return FALSE; } /* success */ return TRUE; } static gboolean fu_elantp_hid_haptic_device_wait_calc_checksum_cb(FuDevice *parent, gpointer user_data, GError **error) { guint16 ctrl; guint8 buf[2] = {0x0}; if (!fu_elantp_hid_haptic_device_write_cmd(parent, ETP_CMD_I2C_SET_EEPROM_CTRL, ETP_CMD_I2C_SET_EEPROM_DATATYPE, error)) { g_prefix_error(error, "failed to write eeprom datatype: "); return FALSE; } if (!fu_elantp_hid_haptic_device_read_cmd(parent, ETP_CMD_I2C_SET_EEPROM_CTRL, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read calc haptic cmd: "); return FALSE; } ctrl = fu_memread_uint16(buf, G_LITTLE_ENDIAN); if ((ctrl & 0x20) == 0x20) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "ctrl failed 0x%04x", ctrl); return FALSE; } return TRUE; } static gboolean fu_elantp_hid_haptic_device_get_checksum(FuDevice *parent, guint16 *checksum, GError **error) { guint8 buf[2] = {0x0}; g_autoptr(GError) error_local = NULL; if (!fu_elantp_hid_haptic_device_write_cmd(parent, ETP_CMD_I2C_SET_EEPROM_CTRL, ETP_CMD_I2C_CALC_EEPROM_CHECKSUM, error)) return FALSE; if (!fu_device_retry_full(parent, fu_elantp_hid_haptic_device_wait_calc_checksum_cb, 100, ELANTP_EEPROM_READ_DELAY, NULL, &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "failed to wait calc eeprom checksum (%s)", error_local->message); return FALSE; } if (!fu_elantp_hid_haptic_device_write_cmd(parent, ETP_CMD_I2C_SET_EEPROM_CTRL, ETP_CMD_I2C_READ_EEPROM_CHECKSUM, error)) return FALSE; if (!fu_elantp_hid_haptic_device_read_cmd(parent, ETP_CMD_I2C_SET_EEPROM_CTRL, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read haptic checksum cmd: "); return FALSE; } *checksum = fu_memread_uint16(buf, G_LITTLE_ENDIAN); return TRUE; } static gboolean fu_elantp_hid_haptic_device_setup(FuDevice *device, GError **error) { FuElantpHidDevice *parent; FuElantpHidHapticDevice *self = FU_ELANTP_HID_HAPTIC_DEVICE(device); FuUdevDevice *udev_parent; guint8 ic_type; guint16 tmp; guint8 buf[2] = {0x0}; g_autofree gchar *version_bl = NULL; parent = fu_elantp_hid_haptic_device_get_parent(device, error); if (parent == NULL) return FALSE; if (!fu_elantp_hid_haptic_device_get_haptic_driver_ic(FU_DEVICE(parent), self, error)) { g_prefix_error(error, "this module is not support haptic EEPROM: "); return FALSE; } /* get pattern */ if (!fu_elantp_hid_haptic_device_read_cmd(FU_DEVICE(parent), ETP_CMD_I2C_GET_HID_ID, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read HID ID: "); return FALSE; } tmp = fu_memread_uint16(buf, G_LITTLE_ENDIAN); self->pattern = tmp != 0xFFFF ? (tmp & 0xFF00) >> 8 : 0; if (!fu_elantp_hid_haptic_device_get_version(FU_DEVICE(parent), self, error)) return FALSE; version_bl = fu_version_from_uint16(self->iap_ver, FWUPD_VERSION_FORMAT_HEX); fu_device_set_version_bootloader(device, version_bl); /* get module ID */ if (!fu_elantp_hid_haptic_device_read_cmd(FU_DEVICE(parent), ETP_CMD_GET_MODULE_ID, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read module ID: "); return FALSE; } self->module_id = fu_memread_uint16(buf, G_LITTLE_ENDIAN); /* define the extra instance IDs */ udev_parent = FU_UDEV_DEVICE(parent); fu_device_add_instance_u16(device, "VEN", fu_device_get_vid(FU_DEVICE(udev_parent))); fu_device_add_instance_u16(device, "DEV", fu_device_get_pid(FU_DEVICE(udev_parent))); fu_device_add_instance_u16(device, "DRIVERIC", self->driver_ic); fu_device_add_instance_u16(device, "MOD", self->module_id); if (!fu_device_build_instance_id(device, error, "HIDRAW", "VEN", "DEV", "DRIVERIC", "MOD", NULL)) return FALSE; /* get OSM version */ if (!fu_elantp_hid_haptic_device_read_cmd(FU_DEVICE(parent), ETP_CMD_I2C_OSM_VERSION, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read OSM version: "); return FALSE; } tmp = fu_memread_uint16(buf, G_LITTLE_ENDIAN); if (tmp == ETP_CMD_I2C_OSM_VERSION || tmp == 0xFFFF) { if (!fu_elantp_hid_haptic_device_read_cmd(FU_DEVICE(parent), ETP_CMD_I2C_IAP_ICBODY, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read IC body: "); return FALSE; } ic_type = fu_memread_uint16(buf, G_LITTLE_ENDIAN) & 0xFF; } else ic_type = (tmp >> 8) & 0xFF; /* define the extra instance IDs (ic_type + module_id + driver) */ fu_device_add_instance_u8(device, "ICTYPE", ic_type); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "ELANTP", "ICTYPE", NULL); fu_device_build_instance_id(device, NULL, "ELANTP", "ICTYPE", "DRIVERIC", "MOD", NULL); fu_device_add_instance_str(device, "DRIVER", "HID"); fu_device_build_instance_id(device, NULL, "ELANTP", "ICTYPE", "DRIVERIC", "MOD", "DRIVER", NULL); /* no quirk entry */ if (self->ic_page_count == 0x0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no page count for ELANTP\\ICTYPE_%02X", ic_type); return FALSE; } fu_device_set_firmware_size(device, 32768); /* find out if in bootloader mode */ if (!fu_elantp_hid_haptic_device_ensure_iap_ctrl(FU_DEVICE(parent), self, error)) return FALSE; /* success */ return TRUE; } static FuFirmware * fu_elantp_hid_haptic_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuElantpHidHapticDevice *self = FU_ELANTP_HID_HAPTIC_DEVICE(device); guint16 driver_ic; g_autoptr(FuFirmware) firmware = fu_elantp_haptic_firmware_new(); /* check is compatible with hardware */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; driver_ic = fu_elantp_haptic_firmware_get_driver_ic(FU_ELANTP_HAPTIC_FIRMWARE(firmware)); if (driver_ic != self->driver_ic) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "driver IC 0x%x != 0x%x", (guint)driver_ic, (guint)self->driver_ic); return NULL; } /* success */ return g_steal_pointer(&firmware); } typedef struct { guint16 checksum; guint idx_page_start; GBytes *fw; /* noref */ FuProgress *progress; /* noref */ } FuElantpHaptictpWriteHelper; static gboolean fu_elantp_hid_haptic_device_write_chunks_cb(FuDevice *device, gpointer user_data, GError **error) { FuElantpHaptictpWriteHelper *helper = (FuElantpHaptictpWriteHelper *)user_data; FuElantpHidHapticDevice *self = FU_ELANTP_HID_HAPTIC_DEVICE(device); FuElantpHidDevice *parent; const guint16 eeprom_fw_page_size = 32; g_autoptr(FuChunkArray) chunks = NULL; /* use parent */ parent = fu_elantp_hid_haptic_device_get_parent(device, error); if (parent == NULL) return FALSE; /* progress */ chunks = fu_chunk_array_new_from_bytes(helper->fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, eeprom_fw_page_size); fu_progress_set_id(helper->progress, G_STRLOC); fu_progress_set_steps(helper->progress, fu_chunk_array_length(chunks) - helper->idx_page_start + 1); for (guint i = helper->idx_page_start; i <= fu_chunk_array_length(chunks); i++) { guint16 csum_tmp; gsize blksz = self->fw_page_size + 3; g_autofree guint8 *blk = g_malloc0(blksz); g_autoptr(FuChunk) chk = NULL; g_autoptr(GError) error_iapctrl = NULL; if (i == fu_chunk_array_length(chunks)) chk = fu_chunk_array_index(chunks, 0, error); else chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; /* write block */ blk[0] = 0x0B; /* report ID */ blk[1] = eeprom_fw_page_size + 5; blk[2] = 0xA2; fu_memwrite_uint16(blk + 0x3, i * eeprom_fw_page_size, G_BIG_ENDIAN); if (i == 0) { guint8 first_page[32] = {0x0}; memset(&first_page[0], 0xFF, sizeof(first_page)); csum_tmp = fu_sum16(first_page, eeprom_fw_page_size); if (!fu_memcpy_safe(blk, blksz, 0x5, /* dst */ first_page, eeprom_fw_page_size, 0x0, /* src */ eeprom_fw_page_size, error)) return FALSE; fu_memwrite_uint16(blk + eeprom_fw_page_size + 5, csum_tmp, G_BIG_ENDIAN); csum_tmp = 0; } else { csum_tmp = fu_sum16(fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); if (!fu_memcpy_safe(blk, blksz, 0x5, /* dst */ fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0x0, /* src */ fu_chunk_get_data_sz(chk), error)) return FALSE; fu_memwrite_uint16(blk + fu_chunk_get_data_sz(chk) + 5, csum_tmp, G_BIG_ENDIAN); } if (!fu_elantp_hid_haptic_device_send_cmd(FU_DEVICE(parent), blk, blksz, NULL, 0, error)) return FALSE; fu_device_sleep(device, self->fw_page_size == 512 ? ELANTP_DELAY_WRITE_BLOCK_512 : ELANTP_DELAY_WRITE_BLOCK); if (!fu_elantp_hid_haptic_device_write_cmd(FU_DEVICE(parent), ETP_CMD_I2C_SET_EEPROM_CTRL, ETP_CMD_I2C_SET_EEPROM_DATATYPE, error)) return FALSE; if (!fu_elantp_hid_haptic_device_ensure_eeprom_iap_ctrl(FU_DEVICE(parent), self, &error_iapctrl)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "bootloader reports failed write: 0x%x (%s)", self->iap_ctrl, error_iapctrl->message); return FALSE; } /* update progress */ helper->checksum += csum_tmp; helper->idx_page_start = i + 1; fu_progress_step_done(helper->progress); } /* success */ return TRUE; } static gboolean fu_elantp_hid_haptic_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuElantpHidDevice *parent; FuElantpHidHapticDevice *self = FU_ELANTP_HID_HAPTIC_DEVICE(device); guint16 checksum_device = 0; const gchar *fw_ver; const gchar *fw_ver_device; g_autoptr(GBytes) fw = NULL; g_autoptr(GError) error_local = NULL; FuElantpHaptictpWaitFlashEEPROMChecksumHelper helper = {0x0}; FuElantpHaptictpWriteHelper helper_write = {0x0}; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 5, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 80, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 10, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 5, NULL); /* simple image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* use parent */ parent = fu_elantp_hid_haptic_device_get_parent(device, error); if (parent == NULL) return FALSE; /* detach */ if (!fu_elantp_hid_haptic_device_detach(device, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* write each block */ helper_write.fw = fw; helper_write.progress = fu_progress_get_child(progress); if (!fu_device_retry_full(device, fu_elantp_hid_haptic_device_write_chunks_cb, 3, 100, &helper_write, error)) return FALSE; fu_progress_step_done(progress); if (!fu_elantp_hid_haptic_device_write_cmd(FU_DEVICE(parent), ETP_CMD_I2C_EEPROM_SETTING, ETP_CMD_I2C_EEPROM_SETTING_INITIAL, error)) { g_prefix_error(error, "cannot disable EEPROM Long Transmission mode: "); return FALSE; } if (!fu_elantp_hid_haptic_device_write_cmd(FU_DEVICE(parent), ETP_CMD_I2C_SET_EEPROM_CTRL, ETP_CMD_I2C_SET_EEPROM_LEAVE_IAP, error)) { g_prefix_error(error, "cannot leave EEPROM IAP: "); return FALSE; } fu_device_sleep(device, ELANTP_DELAY_RESET); if (!fu_elantp_hid_haptic_device_get_checksum(FU_DEVICE(parent), &checksum_device, error)) { g_prefix_error(error, "read device checksum fail: "); return FALSE; } if (helper_write.checksum != checksum_device) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "checksum failed 0x%04x != 0x%04x", helper_write.checksum, checksum_device); return FALSE; } helper.checksum = checksum_device; helper.iap_password = self->iap_password; helper.tp_ic_type = self->tp_ic_type; helper.tp_iap_ver = self->tp_iap_ver; if (!fu_device_retry_full(FU_DEVICE(parent), fu_elantp_hid_haptic_device_write_checksum_cb, 3, ELANTP_DELAY_WRITE_BLOCK, &helper, &error_local)) { g_prefix_error(error, "write device checksum fail (%s): ", error_local->message); return FALSE; } if (!fu_elantp_hid_haptic_device_get_version(FU_DEVICE(parent), self, error)) return FALSE; fw_ver_device = fu_device_get_version(device); fw_ver = fu_firmware_get_version(firmware); if (g_strcmp0(fw_ver_device, fw_ver) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "firmware version failed %s != %s", fw_ver, fw_ver_device); return FALSE; } fu_progress_step_done(progress); if (!fu_elantp_hid_haptic_device_write_cmd(FU_DEVICE(parent), ETP_CMD_I2C_SET_EEPROM_CTRL, ETP_CMD_I2C_HAPTIC_RESTART, error)) { g_prefix_error(error, "cannot restart haptic DriverIC: "); return FALSE; } fu_progress_step_done(progress); return TRUE; } static gboolean fu_elantp_hid_haptic_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuElantpHidDevice *parent; FuElantpHidHapticDevice *self = FU_ELANTP_HID_HAPTIC_DEVICE(device); guint8 buf[2] = {0x0}; guint16 ctrl; guint16 tmp; /* haptic EEPROM IAP process runs in the TP main code */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "in touchpad bootloader mode"); return FALSE; } if (self->driver_ic != 0x2 || self->iap_ver != 0x1) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no support for EEPROM IAP 0x%x 0x%x: ", (guint)self->driver_ic, (guint)self->iap_ver); return FALSE; } parent = fu_elantp_hid_haptic_device_get_parent(device, error); if (parent == NULL) return FALSE; /* get OSM version */ if (!fu_elantp_hid_haptic_device_read_cmd(FU_DEVICE(parent), ETP_CMD_I2C_OSM_VERSION, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read OSM version: "); return FALSE; } tmp = fu_memread_uint16(buf, G_LITTLE_ENDIAN); if (tmp == ETP_CMD_I2C_OSM_VERSION || tmp == 0xFFFF) { if (!fu_elantp_hid_haptic_device_read_cmd(FU_DEVICE(parent), ETP_CMD_I2C_IAP_ICBODY, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read IC body: "); return FALSE; } self->tp_ic_type = fu_memread_uint16(buf, G_LITTLE_ENDIAN) & 0xFF; } else self->tp_ic_type = (tmp >> 8) & 0xFF; /* get IAP firmware version */ if (!fu_elantp_hid_haptic_device_read_cmd(FU_DEVICE(parent), self->pattern == 0 ? ETP_CMD_I2C_IAP_VERSION : ETP_CMD_I2C_IAP_VERSION_2, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read bootloader version: "); return FALSE; } if (self->pattern >= 1) self->tp_iap_ver = buf[1]; else self->tp_iap_ver = fu_memread_uint16(buf, G_LITTLE_ENDIAN); /* set the page size */ self->fw_page_size = 64; if (self->tp_ic_type >= 0x10) { if (self->tp_iap_ver >= 1) { /* set the IAP type, presumably some kind of ABI */ if (self->tp_iap_ver >= 2 && (self->tp_ic_type == 0x14 || self->tp_ic_type == 0x15)) { self->fw_page_size = 512; } else { self->fw_page_size = 128; } if (!fu_elantp_hid_haptic_device_write_cmd(FU_DEVICE(parent), ETP_CMD_I2C_IAP_TYPE, self->fw_page_size / 2, error)) return FALSE; if (!fu_elantp_hid_haptic_device_read_cmd(FU_DEVICE(parent), ETP_CMD_I2C_IAP_TYPE, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read IAP type: "); return FALSE; } self->iap_type = fu_memread_uint16(buf, G_LITTLE_ENDIAN); if (self->iap_type != self->fw_page_size / 2) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to set IAP type"); return FALSE; } } } if (!fu_elantp_hid_haptic_device_write_cmd(FU_DEVICE(parent), ETP_CMD_I2C_EEPROM_SETTING, ETP_CMD_I2C_EEPROM_LONG_TRANS_ENABLE, error)) { g_prefix_error(error, "cannot enable EEPROM Long Transmission mode: "); return FALSE; } if (!fu_elantp_hid_haptic_device_write_cmd(FU_DEVICE(parent), ETP_CMD_I2C_SET_EEPROM_CTRL, ETP_CMD_I2C_SET_EEPROM_ENTER_IAP, error)) { g_prefix_error(error, "cannot enter EEPROM IAP: "); return FALSE; } if (!fu_elantp_hid_haptic_device_read_cmd(FU_DEVICE(parent), ETP_CMD_I2C_SET_EEPROM_CTRL, buf, sizeof(buf), error)) return FALSE; ctrl = fu_memread_uint16(buf, G_LITTLE_ENDIAN); if ((ctrl & 0x800) == 0x800) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "unexpected EEPROM bootloader control %x", ctrl); return FALSE; } /* success */ return TRUE; } static gboolean fu_elantp_hid_haptic_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuElantpHidDevice *parent; FuElantpHidHapticDevice *self = FU_ELANTP_HID_HAPTIC_DEVICE(device); parent = fu_elantp_hid_haptic_device_get_parent(device, error); if (parent == NULL) return FALSE; /* reset back to runtime */ if (!fu_elantp_hid_haptic_device_write_cmd(FU_DEVICE(parent), ETP_CMD_I2C_IAP_RESET, ETP_I2C_IAP_RESET, error)) { g_prefix_error(error, "cannot reset TP: "); return FALSE; } fu_device_sleep(device, ELANTP_DELAY_RESET); if (!fu_elantp_hid_haptic_device_write_cmd(FU_DEVICE(parent), ETP_CMD_I2C_IAP_RESET, ETP_I2C_ENABLE_REPORT, error)) { g_prefix_error(error, "cannot enable TP report: "); return FALSE; } if (!fu_elantp_hid_haptic_device_write_cmd(FU_DEVICE(parent), 0x0306, 0x003, error)) { g_prefix_error(error, "cannot switch to TP PTP mode: "); return FALSE; } if (!fu_elantp_hid_haptic_device_ensure_iap_ctrl(FU_DEVICE(parent), self, error)) return FALSE; /* sanity check */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "in bootloader mode"); return FALSE; } /* success */ return TRUE; } static gboolean fu_elantp_hid_haptic_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuElantpHidHapticDevice *self = FU_ELANTP_HID_HAPTIC_DEVICE(device); guint64 tmp = 0; if (g_strcmp0(key, "ElantpIcPageCount") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->ic_page_count = (guint16)tmp; return TRUE; } if (g_strcmp0(key, "ElantpIapPassword") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->iap_password = (guint16)tmp; return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_elantp_hid_haptic_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 5, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 85, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 8, "reload"); } static void fu_elantp_hid_haptic_device_init(FuElantpHidHapticDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_OPEN); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_INPUT_TOUCHPAD); fu_device_add_protocol(FU_DEVICE(self), "tw.com.emc.elantp.haptic"); fu_device_set_name(FU_DEVICE(self), "HapticPad EEPROM"); fu_device_set_logical_id(FU_DEVICE(self), "eeprom"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_priority(FU_DEVICE(self), 1); /* better than i2c */ } static void fu_elantp_hid_haptic_device_class_init(FuElantpHidHapticDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_elantp_hid_haptic_device_to_string; device_class->attach = fu_elantp_hid_haptic_device_attach; device_class->set_quirk_kv = fu_elantp_hid_haptic_device_set_quirk_kv; device_class->setup = fu_elantp_hid_haptic_device_setup; device_class->reload = fu_elantp_hid_haptic_device_setup; device_class->write_firmware = fu_elantp_hid_haptic_device_write_firmware; device_class->prepare_firmware = fu_elantp_hid_haptic_device_prepare_firmware; device_class->set_progress = fu_elantp_hid_haptic_device_set_progress; } FuElantpHidHapticDevice * fu_elantp_hid_haptic_device_new(FuDevice *device) { FuElantpHidHapticDevice *self; self = g_object_new(FU_TYPE_ELANTP_HID_HAPTIC_DEVICE, NULL); return FU_ELANTP_HID_HAPTIC_DEVICE(self); } fwupd-2.0.10/plugins/elantp/fu-elantp-hid-haptic-device.h000066400000000000000000000007521501337203100231730ustar00rootroot00000000000000/* * Copyright 2022 Jingle Wu * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-elantp-hid-device.h" #define FU_TYPE_ELANTP_HID_HAPTIC_DEVICE (fu_elantp_hid_haptic_device_get_type()) G_DECLARE_FINAL_TYPE(FuElantpHidHapticDevice, fu_elantp_hid_haptic_device, FU, ELANTP_HID_HAPTIC_DEVICE, FuUdevDevice) FuElantpHidHapticDevice * fu_elantp_hid_haptic_device_new(FuDevice *device); fwupd-2.0.10/plugins/elantp/fu-elantp-i2c-device.c000066400000000000000000000576231501337203100216420ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-elantp-common.h" #include "fu-elantp-firmware.h" #include "fu-elantp-i2c-device.h" struct _FuElantpI2cDevice { FuI2cDevice parent_instance; guint16 i2c_addr; guint16 ic_page_count; guint16 iap_type; guint16 iap_ctrl; guint16 iap_password; guint16 module_id; guint16 fw_page_size; guint8 pattern; gchar *bind_path; gchar *bind_id; }; G_DEFINE_TYPE(FuElantpI2cDevice, fu_elantp_i2c_device, FU_TYPE_I2C_DEVICE) #define FU_ELANTP_DEVICE_IOCTL_TIMEOUT 5000 /* ms */ static gboolean fu_elantp_i2c_device_detach(FuDevice *device, FuProgress *progress, GError **error); static void fu_elantp_i2c_device_to_string(FuDevice *device, guint idt, GString *str) { FuElantpI2cDevice *self = FU_ELANTP_I2C_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "I2cAddr", self->i2c_addr); fwupd_codec_string_append_hex(str, idt, "ModuleId", self->module_id); fwupd_codec_string_append_hex(str, idt, "Pattern", self->pattern); fwupd_codec_string_append_hex(str, idt, "FwPageSize", self->fw_page_size); fwupd_codec_string_append_hex(str, idt, "IcPageCount", self->ic_page_count); fwupd_codec_string_append_hex(str, idt, "IapType", self->iap_type); fwupd_codec_string_append_hex(str, idt, "IapCtrl", self->iap_ctrl); fwupd_codec_string_append(str, idt, "BindPath", self->bind_path); fwupd_codec_string_append(str, idt, "BindId", self->bind_id); } static gboolean fu_elantp_i2c_device_writeln(FuElantpI2cDevice *self, const gchar *fn, const gchar *buf, GError **error) { gboolean exists_fn = FALSE; g_autoptr(FuIOChannel) io = NULL; if (!fu_device_query_file_exists(FU_DEVICE(self), fn, &exists_fn, error)) return FALSE; if (!exists_fn) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "%s does not exist", fn); return FALSE; } io = fu_io_channel_new_file(fn, FU_IO_CHANNEL_OPEN_FLAG_WRITE, error); if (io == NULL) return FALSE; return fu_io_channel_write_raw(io, (const guint8 *)buf, strlen(buf), 1000, FU_IO_CHANNEL_FLAG_NONE, error); } static gboolean fu_elantp_i2c_device_rebind_driver(FuElantpI2cDevice *self, GError **error) { g_autofree gchar *unbind_fn = g_build_filename(self->bind_path, "unbind", NULL); g_autofree gchar *bind_fn = g_build_filename(self->bind_path, "bind", NULL); if (self->bind_path == NULL || self->bind_id == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no Path or ID for rebind driver"); return FALSE; } if (!fu_elantp_i2c_device_writeln(self, unbind_fn, self->bind_id, error)) return FALSE; if (!fu_elantp_i2c_device_writeln(self, bind_fn, self->bind_id, error)) return FALSE; g_debug("rebind driver of %s", self->bind_id); return TRUE; } static gboolean fu_elantp_i2c_device_probe(FuDevice *device, GError **error) { FuElantpI2cDevice *self = FU_ELANTP_I2C_DEVICE(device); /* check is valid */ if (g_strcmp0(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device)), "i2c-dev") != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "is not correct subsystem=%s, expected i2c-dev", fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device))); return FALSE; } if (fu_udev_device_get_device_file(FU_UDEV_DEVICE(device)) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no device file"); return FALSE; } self->bind_path = g_build_filename("/sys/bus/i2c/drivers", fu_udev_device_get_driver(FU_UDEV_DEVICE(device)), NULL); self->bind_id = g_path_get_basename(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device))); /* set the physical ID */ return fu_udev_device_set_physical_id(FU_UDEV_DEVICE(device), "i2c", error); } static gboolean fu_elantp_i2c_device_send_cmd(FuElantpI2cDevice *self, guint8 *tx, gssize txsz, guint8 *rx, gssize rxsz, GError **error) { fu_dump_raw(G_LOG_DOMAIN, "Write", tx, txsz); if (!fu_udev_device_pwrite(FU_UDEV_DEVICE(self), 0, tx, txsz, error)) return FALSE; if (rxsz == 0) return TRUE; if (!fu_udev_device_pread(FU_UDEV_DEVICE(self), 0, rx, rxsz, error)) return FALSE; fu_dump_raw(G_LOG_DOMAIN, "Read", rx, rxsz); return TRUE; } static gboolean fu_elantp_i2c_device_write_cmd(FuElantpI2cDevice *self, guint16 reg, guint16 cmd, GError **error) { guint8 buf[4]; fu_memwrite_uint16(buf + 0x0, reg, G_LITTLE_ENDIAN); fu_memwrite_uint16(buf + 0x2, cmd, G_LITTLE_ENDIAN); return fu_elantp_i2c_device_send_cmd(self, buf, sizeof(buf), NULL, 0, error); } static gboolean fu_elantp_i2c_device_read_cmd(FuElantpI2cDevice *self, guint16 reg, guint8 *rx, gsize rxsz, GError **error) { guint8 buf[2]; fu_memwrite_uint16(buf + 0x0, reg, G_LITTLE_ENDIAN); return fu_elantp_i2c_device_send_cmd(self, buf, sizeof(buf), rx, rxsz, error); } static gboolean fu_elantp_i2c_device_ensure_iap_ctrl(FuElantpI2cDevice *self, GError **error) { guint8 buf[2] = {0x0}; if (!fu_elantp_i2c_device_read_cmd(self, ETP_CMD_I2C_IAP_CTRL, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read IAPControl: "); return FALSE; } if (!fu_memread_uint16_safe(buf, sizeof(buf), 0x0, &self->iap_ctrl, G_LITTLE_ENDIAN, error)) return FALSE; /* in bootloader mode? */ if ((self->iap_ctrl & ETP_I2C_MAIN_MODE_ON) == 0) fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); else fu_device_remove_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); return TRUE; } static gboolean fu_elantp_i2c_device_setup(FuDevice *device, GError **error) { FuElantpI2cDevice *self = FU_ELANTP_I2C_DEVICE(device); guint16 fwver; guint16 iap_ver; guint16 tmp; guint16 pid; guint16 vid; guint8 buf[30] = {0x0}; guint8 ic_type; g_autofree gchar *version_bl = NULL; /* read the I2C descriptor */ if (!fu_elantp_i2c_device_read_cmd(self, ETP_CMD_GET_HID_DESCRIPTOR, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to get HID descriptor: "); return FALSE; } if (!fu_memread_uint16_safe(buf, sizeof(buf), 20, &vid, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memread_uint16_safe(buf, sizeof(buf), 22, &pid, G_LITTLE_ENDIAN, error)) return FALSE; fu_device_build_vendor_id_u16(device, "HIDRAW", vid); /* add GUIDs in order of priority */ fu_device_add_instance_u16(device, "VID", vid); fu_device_add_instance_u16(device, "PID", pid); if (!fu_device_build_instance_id(device, error, "HIDRAW", "VID", "PID", NULL)) return FALSE; /* get pattern */ if (!fu_elantp_i2c_device_read_cmd(self, ETP_CMD_I2C_GET_HID_ID, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read I2C ID: "); return FALSE; } if (!fu_memread_uint16_safe(buf, sizeof(buf), 0x0, &tmp, G_LITTLE_ENDIAN, error)) return FALSE; self->pattern = tmp != 0xffff ? (tmp & 0xff00) >> 8 : 0; /* get current firmware version */ if (!fu_elantp_i2c_device_read_cmd(self, ETP_CMD_I2C_FW_VERSION, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read fw version: "); return FALSE; } if (!fu_memread_uint16_safe(buf, sizeof(buf), 0x0, &fwver, G_LITTLE_ENDIAN, error)) return FALSE; if (fwver == 0xFFFF || fwver == ETP_CMD_I2C_FW_VERSION) fwver = 0; fu_device_set_version_raw(device, fwver); /* get IAP firmware version */ if (!fu_elantp_i2c_device_read_cmd(self, self->pattern == 0 ? ETP_CMD_I2C_IAP_VERSION : ETP_CMD_I2C_IAP_VERSION_2, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read bootloader version: "); return FALSE; } if (self->pattern >= 1) { iap_ver = buf[1]; } else { if (!fu_memread_uint16_safe(buf, sizeof(buf), 0x0, &iap_ver, G_LITTLE_ENDIAN, error)) return FALSE; } version_bl = fu_version_from_uint16(iap_ver, FWUPD_VERSION_FORMAT_HEX); fu_device_set_version_bootloader(device, version_bl); /* get module ID */ if (!fu_elantp_i2c_device_read_cmd(self, ETP_CMD_GET_MODULE_ID, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read module ID: "); return FALSE; } if (!fu_memread_uint16_safe(buf, sizeof(buf), 0x0, &self->module_id, G_LITTLE_ENDIAN, error)) return FALSE; /* define the extra instance IDs */ fu_device_add_instance_u16(device, "VEN", vid); fu_device_add_instance_u16(device, "DEV", pid); fu_device_add_instance_u16(device, "MOD", self->module_id); if (!fu_device_build_instance_id(device, error, "HIDRAW", "VEN", "DEV", "MOD", NULL)) return FALSE; /* get OSM version */ if (!fu_elantp_i2c_device_read_cmd(self, ETP_CMD_I2C_OSM_VERSION, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read OSM version: "); return FALSE; } if (!fu_memread_uint16_safe(buf, sizeof(buf), 0x0, &tmp, G_LITTLE_ENDIAN, error)) return FALSE; if (tmp == ETP_CMD_I2C_OSM_VERSION || tmp == 0xFFFF) { if (!fu_elantp_i2c_device_read_cmd(self, ETP_CMD_I2C_IAP_ICBODY, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read IC body: "); return FALSE; } if (!fu_memread_uint16_safe(buf, sizeof(buf), 0x0, &tmp, G_LITTLE_ENDIAN, error)) return FALSE; ic_type = tmp & 0xFF; } else { ic_type = (tmp >> 8) & 0xFF; } /* define the extra instance IDs (ic_type + module_id + driver) */ fu_device_add_instance_u8(device, "ICTYPE", ic_type); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "ELANTP", "ICTYPE", NULL); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "ELANTP", "ICTYPE", "MOD", NULL); if (fu_device_has_private_flag(device, FU_ELANTP_I2C_DEVICE_ABSOLUTE)) { fu_device_add_instance_str(device, "DRIVER", "ELAN_I2C"); } else { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT); fu_device_add_instance_str(device, "DRIVER", "HID"); } fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "ELANTP", "ICTYPE", "MOD", "DRIVER", NULL); /* no quirk entry */ if (self->ic_page_count == 0x0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no page count for ELANTP\\ICTYPE_%02X", ic_type); return FALSE; } fu_device_set_firmware_size(device, (guint64)self->ic_page_count * (guint64)64); /* is in bootloader mode */ if (!fu_elantp_i2c_device_ensure_iap_ctrl(self, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_elantp_i2c_device_open(FuDevice *device, GError **error) { FuElantpI2cDevice *self = FU_ELANTP_I2C_DEVICE(device); guint8 tx_buf[] = {0x02, 0x01}; /* FuUdevDevice->open */ if (!FU_DEVICE_CLASS(fu_elantp_i2c_device_parent_class)->open(device, error)) return FALSE; /* set target address */ if (!fu_i2c_device_set_address(FU_I2C_DEVICE(self), self->i2c_addr, TRUE, error)) return FALSE; /* read i2c device */ return fu_udev_device_pwrite(FU_UDEV_DEVICE(device), 0x0, tx_buf, sizeof(tx_buf), error); } static FuFirmware * fu_elantp_i2c_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuElantpI2cDevice *self = FU_ELANTP_I2C_DEVICE(device); guint16 module_id; g_autoptr(FuFirmware) firmware = fu_elantp_firmware_new(); /* check is compatible with hardware */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; module_id = fu_elantp_firmware_get_module_id(FU_ELANTP_FIRMWARE(firmware)); if (self->module_id != module_id) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware incompatible, got 0x%04x, expected 0x%04x", module_id, self->module_id); return NULL; } /* success */ return g_steal_pointer(&firmware); } static gboolean fu_elantp_i2c_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuElantpI2cDevice *self = FU_ELANTP_I2C_DEVICE(device); FuElantpFirmware *firmware_elantp = FU_ELANTP_FIRMWARE(firmware); guint16 checksum = 0; guint16 checksum_device = 0; guint16 iap_addr; guint8 csum_buf[2] = {0x0}; g_autoptr(GBytes) fw = NULL; g_autoptr(GBytes) fw2 = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 90, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 10, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, NULL); /* simple image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* detach */ if (!fu_elantp_i2c_device_detach(device, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* write each block */ iap_addr = fu_elantp_firmware_get_iap_addr(firmware_elantp); fw2 = fu_bytes_new_offset(fw, iap_addr, g_bytes_get_size(fw) - iap_addr, error); if (fw2 == NULL) return FALSE; chunks = fu_chunk_array_new_from_bytes(fw2, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, self->fw_page_size); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { guint16 csum_tmp; gsize blksz = self->fw_page_size + 4; g_autofree guint8 *blk = g_malloc0(blksz); g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; csum_tmp = fu_sum16w(fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), G_LITTLE_ENDIAN); /* write block */ blk[0] = ETP_I2C_IAP_REG_L; blk[1] = ETP_I2C_IAP_REG_H; if (!fu_memcpy_safe(blk, blksz, 0x2, /* dst */ fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0x0, /* src */ fu_chunk_get_data_sz(chk), error)) return FALSE; fu_memwrite_uint16(blk + fu_chunk_get_data_sz(chk) + 2, csum_tmp, G_LITTLE_ENDIAN); if (!fu_elantp_i2c_device_send_cmd(self, blk, blksz, NULL, 0, error)) return FALSE; fu_device_sleep(device, self->fw_page_size == 512 ? ELANTP_DELAY_WRITE_BLOCK_512 : ELANTP_DELAY_WRITE_BLOCK); if (!fu_elantp_i2c_device_ensure_iap_ctrl(self, error)) return FALSE; if (self->iap_ctrl & (ETP_FW_IAP_PAGE_ERR | ETP_FW_IAP_INTF_ERR)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "bootloader reports failed write: 0x%x", self->iap_ctrl); return FALSE; } /* update progress */ checksum += csum_tmp; fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)i + 1, (gsize)fu_chunk_array_length(chunks)); } fu_progress_step_done(progress); /* verify the written checksum */ if (!fu_elantp_i2c_device_read_cmd(self, ETP_CMD_I2C_IAP_CHECKSUM, csum_buf, sizeof(csum_buf), error)) return FALSE; if (!fu_memread_uint16_safe(csum_buf, sizeof(csum_buf), 0x0, &checksum_device, G_LITTLE_ENDIAN, error)) return FALSE; if (checksum != checksum_device) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "checksum failed 0x%04x != 0x%04x", checksum, checksum_device); return FALSE; } fu_progress_step_done(progress); /* wait for a reset */ fu_device_sleep_full(device, ELANTP_DELAY_COMPLETE, fu_progress_get_child(progress)); /* ms */ fu_progress_step_done(progress); return TRUE; } static gboolean fu_elantp_i2c_device_detach(FuDevice *device, FuProgress *progress, GError **error) { guint16 iap_ver; guint16 ic_type; guint8 buf[2] = {0x0}; guint16 tmp; FuElantpI2cDevice *self = FU_ELANTP_I2C_DEVICE(device); /* sanity check */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_info("in bootloader mode, reset IC"); if (!fu_elantp_i2c_device_write_cmd(self, ETP_CMD_I2C_IAP_RESET, ETP_I2C_IAP_RESET, error)) return FALSE; fu_device_sleep(device, ELANTP_DELAY_RESET); } /* get OSM version */ if (!fu_elantp_i2c_device_read_cmd(self, ETP_CMD_I2C_OSM_VERSION, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read OSM version: "); return FALSE; } if (!fu_memread_uint16_safe(buf, sizeof(buf), 0x0, &tmp, G_LITTLE_ENDIAN, error)) return FALSE; if (tmp == ETP_CMD_I2C_OSM_VERSION || tmp == 0xFFFF) { if (!fu_elantp_i2c_device_read_cmd(self, ETP_CMD_I2C_IAP_ICBODY, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read IC body: "); return FALSE; } if (!fu_memread_uint16_safe(buf, sizeof(buf), 0x0, &ic_type, G_LITTLE_ENDIAN, error)) return FALSE; } else { ic_type = (tmp >> 8) & 0xFF; } /* get IAP firmware version */ if (!fu_elantp_i2c_device_read_cmd(self, self->pattern == 0 ? ETP_CMD_I2C_IAP_VERSION : ETP_CMD_I2C_IAP_VERSION_2, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read bootloader version: "); return FALSE; } if (self->pattern >= 1) { iap_ver = buf[1]; } else { if (!fu_memread_uint16_safe(buf, sizeof(buf), 0x0, &iap_ver, G_LITTLE_ENDIAN, error)) return FALSE; } /* set the page size */ self->fw_page_size = 64; if (ic_type >= 0x10) { if (iap_ver >= 1) { if (iap_ver >= 2 && (ic_type == 0x14 || ic_type == 0x15)) { self->fw_page_size = 512; } else { self->fw_page_size = 128; } /* set the IAP type, presumably some kind of ABI */ if (!fu_elantp_i2c_device_write_cmd(self, ETP_CMD_I2C_IAP_TYPE, self->fw_page_size / 2, error)) return FALSE; if (!fu_elantp_i2c_device_read_cmd(self, ETP_CMD_I2C_IAP_TYPE, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read IAP type: "); return FALSE; } if (!fu_memread_uint16_safe(buf, sizeof(buf), 0x0, &self->iap_type, G_LITTLE_ENDIAN, error)) return FALSE; if (self->iap_type != self->fw_page_size / 2) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to set IAP type"); return FALSE; } } } if (!fu_elantp_i2c_device_write_cmd(self, ETP_CMD_I2C_IAP, self->iap_password, error)) return FALSE; fu_device_sleep(device, ELANTP_DELAY_UNLOCK); if (!fu_elantp_i2c_device_ensure_iap_ctrl(self, error)) return FALSE; if ((self->iap_ctrl & ETP_FW_IAP_CHECK_PW) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "unexpected bootloader password"); return FALSE; } /* success */ return TRUE; } static gboolean fu_elantp_i2c_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuElantpI2cDevice *self = FU_ELANTP_I2C_DEVICE(device); /* sanity check */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in runtime mode, skipping"); return TRUE; } /* reset back to runtime */ if (!fu_elantp_i2c_device_write_cmd(self, ETP_CMD_I2C_IAP_RESET, ETP_I2C_IAP_RESET, error)) return FALSE; fu_device_sleep(device, ELANTP_DELAY_RESET); if (!fu_elantp_i2c_device_write_cmd(self, ETP_CMD_I2C_IAP_RESET, ETP_I2C_ENABLE_REPORT, error)) { g_prefix_error(error, "cannot enable TP report: "); return FALSE; } if (!fu_elantp_i2c_device_ensure_iap_ctrl(self, error)) return FALSE; if (fu_device_has_private_flag(device, FU_ELANTP_I2C_DEVICE_ABSOLUTE)) { g_autoptr(GError) error_local = NULL; if (!fu_elantp_i2c_device_write_cmd(self, 0x0300, 0x001, error)) { g_prefix_error(error, "cannot switch to TP ABS mode: "); return FALSE; } if (!fu_elantp_i2c_device_rebind_driver(self, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_PERMISSION_DENIED)) { g_debug("%s", error_local->message); } else { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } } else { if (!fu_elantp_i2c_device_write_cmd(self, 0x0306, 0x003, error)) { g_prefix_error(error, "cannot switch to TP PTP mode: "); return FALSE; } } /* success */ return TRUE; } static gboolean fu_elantp_i2c_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuElantpI2cDevice *self = FU_ELANTP_I2C_DEVICE(device); guint64 tmp = 0; if (g_strcmp0(key, "ElantpIcPageCount") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->ic_page_count = (guint16)tmp; return TRUE; } if (g_strcmp0(key, "ElantpIapPassword") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->iap_password = (guint16)tmp; return TRUE; } if (g_strcmp0(key, "ElantpI2cTargetAddress") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->i2c_addr = (guint16)tmp; return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_elantp_i2c_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static gchar * fu_elantp_i2c_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint16(version_raw, fu_device_get_version_format(device)); } static void fu_elantp_i2c_device_init(FuElantpI2cDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_set_name(FU_DEVICE(self), "Touchpad"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_INPUT_TOUCHPAD); fu_device_add_protocol(FU_DEVICE(self), "tw.com.emc.elantp"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_HEX); fu_device_set_vendor(FU_DEVICE(self), "ELAN Microelectronics"); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); fu_device_register_private_flag(FU_DEVICE(self), FU_ELANTP_I2C_DEVICE_ABSOLUTE); } static void fu_elantp_i2c_device_finalize(GObject *object) { FuElantpI2cDevice *self = FU_ELANTP_I2C_DEVICE(object); g_free(self->bind_path); g_free(self->bind_id); G_OBJECT_CLASS(fu_elantp_i2c_device_parent_class)->finalize(object); } static void fu_elantp_i2c_device_class_init(FuElantpI2cDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->finalize = fu_elantp_i2c_device_finalize; device_class->to_string = fu_elantp_i2c_device_to_string; device_class->attach = fu_elantp_i2c_device_attach; device_class->set_quirk_kv = fu_elantp_i2c_device_set_quirk_kv; device_class->setup = fu_elantp_i2c_device_setup; device_class->reload = fu_elantp_i2c_device_setup; device_class->write_firmware = fu_elantp_i2c_device_write_firmware; device_class->prepare_firmware = fu_elantp_i2c_device_prepare_firmware; device_class->probe = fu_elantp_i2c_device_probe; device_class->open = fu_elantp_i2c_device_open; device_class->set_progress = fu_elantp_i2c_device_set_progress; device_class->convert_version = fu_elantp_i2c_device_convert_version; } fwupd-2.0.10/plugins/elantp/fu-elantp-i2c-device.h000066400000000000000000000005671501337203100216420ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_ELANTP_I2C_DEVICE_ABSOLUTE "elantp-absolute" #define FU_TYPE_ELANTP_I2C_DEVICE (fu_elantp_i2c_device_get_type()) G_DECLARE_FINAL_TYPE(FuElantpI2cDevice, fu_elantp_i2c_device, FU, ELANTP_I2C_DEVICE, FuI2cDevice) fwupd-2.0.10/plugins/elantp/fu-elantp-plugin.c000066400000000000000000000034101501337203100212070ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-elantp-firmware.h" #include "fu-elantp-hid-device.h" #include "fu-elantp-i2c-device.h" #include "fu-elantp-plugin.h" struct _FuElantpPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuElantpPlugin, fu_elantp_plugin, FU_TYPE_PLUGIN) static gboolean fu_elantp_plugin_device_created(FuPlugin *plugin, FuDevice *dev, GError **error) { if (fu_device_get_specialized_gtype(dev) == FU_TYPE_ELANTP_I2C_DEVICE && !fu_context_has_hwid_flag(fu_plugin_get_context(plugin), "elantp-recovery") && !fu_device_has_private_flag(dev, FU_ELANTP_I2C_DEVICE_ABSOLUTE)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not required"); return FALSE; } return TRUE; } static void fu_elantp_plugin_init(FuElantpPlugin *self) { } static void fu_elantp_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "ElantpI2cTargetAddress"); fu_context_add_quirk_key(ctx, "ElantpIapPassword"); fu_context_add_quirk_key(ctx, "ElantpIcPageCount"); fu_plugin_add_udev_subsystem(plugin, "i2c"); fu_plugin_add_udev_subsystem(plugin, "i2c-dev"); fu_plugin_add_udev_subsystem(plugin, "hidraw"); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_ELANTP_FIRMWARE); fu_plugin_add_device_gtype(plugin, FU_TYPE_ELANTP_I2C_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_ELANTP_HID_DEVICE); } static void fu_elantp_plugin_class_init(FuElantpPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_elantp_plugin_constructed; plugin_class->device_created = fu_elantp_plugin_device_created; } fwupd-2.0.10/plugins/elantp/fu-elantp-plugin.h000066400000000000000000000003541501337203100212200ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuElantpPlugin, fu_elantp_plugin, FU, ELANTP_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/elantp/fu-elantp.rs000066400000000000000000000005531501337203100201220ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ValidateStream, Default)] #[repr(C, packed)] struct FuStructElantpFirmwareHdr { magic: [u8; 6] == 0xAA55CC33FFFF, } #[derive(ValidateStream, Default)] #[repr(C, packed)] struct FuStructElantpHapticFirmwareHdr { magic: [u8; 4] == 0xFF40A25B, } fwupd-2.0.10/plugins/elantp/fu-self-test.c000066400000000000000000000033551501337203100203460ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-elantp-firmware.h" static void fu_elantp_firmware_xml_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autofree gchar *csum1 = NULL; g_autofree gchar *csum2 = NULL; g_autofree gchar *xml_out = NULL; g_autofree gchar *xml_src = NULL; g_autoptr(FuFirmware) firmware1 = fu_elantp_firmware_new(); g_autoptr(FuFirmware) firmware2 = fu_elantp_firmware_new(); g_autoptr(GError) error = NULL; /* build and write */ filename = g_test_build_filename(G_TEST_DIST, "tests", "elantp.builder.xml", NULL); ret = g_file_get_contents(filename, &xml_src, NULL, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_firmware_build_from_xml(firmware1, xml_src, &error); g_assert_no_error(error); g_assert_true(ret); csum1 = fu_firmware_get_checksum(firmware1, G_CHECKSUM_SHA1, &error); g_assert_no_error(error); g_assert_cmpstr(csum1, ==, "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"); /* ensure we can round-trip */ xml_out = fu_firmware_export_to_xml(firmware1, FU_FIRMWARE_EXPORT_FLAG_NONE, &error); g_assert_no_error(error); ret = fu_firmware_build_from_xml(firmware2, xml_out, &error); g_assert_no_error(error); g_assert_true(ret); csum2 = fu_firmware_get_checksum(firmware2, G_CHECKSUM_SHA1, &error); g_assert_cmpstr(csum1, ==, csum2); } int main(int argc, char **argv) { (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); /* tests go here */ g_test_add_func("/elantp/firmware{xml}", fu_elantp_firmware_xml_func); return g_test_run(); } fwupd-2.0.10/plugins/elantp/meson.build000066400000000000000000000030311501337203100200150ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginElantp"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('elantp.quirk') plugin_builtin_elantp = static_library('fu_plugin_elantp', rustgen.process( 'fu-elantp.rs', # fuzzing ), sources: [ 'fu-elantp-plugin.c', 'fu-elantp-firmware.c', # fuzzing 'fu-elantp-haptic-firmware.c', 'fu-elantp-hid-device.c', 'fu-elantp-i2c-device.c', 'fu-elantp-hid-haptic-device.c', ], include_directories: plugin_incdirs, c_args: [ cargs, '-DLOCALSTATEDIR="' + localstatedir + '"', ], link_with: plugin_libs, dependencies: plugin_deps, ) plugin_builtins += plugin_builtin_elantp enumeration_data += files('tests/lenovo-nano-g1-setup.json') device_tests += files('tests/lenovo-nano-g1.json') if get_option('tests') install_data(['tests/elantp.builder.xml'], install_dir: join_paths(installed_test_datadir, 'tests')) env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) e = executable( 'elantp-self-test', sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: plugin_deps, link_with: [ plugin_libs, plugin_builtin_elantp, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('elantp-self-test', e, env: env) endif endif fwupd-2.0.10/plugins/elantp/tests/000077500000000000000000000000001501337203100170205ustar00rootroot00000000000000fwupd-2.0.10/plugins/elantp/tests/elantp.builder.xml000066400000000000000000000002321501337203100224470ustar00rootroot00000000000000 0xe00 0x2 aGVsbG8gd29ybGQ= fwupd-2.0.10/plugins/elantp/tests/lenovo-nano-g1-setup.json000066400000000000000000000056111501337203100236140ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUdevDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:15.0/i2c_designware.0/i2c-0/i2c-ELAN0670:00/0018:04F3:314F.0001/hidraw/hidraw0", "DeviceFile": "/dev/hidraw0", "Subsystem": "hidraw", "Vendor": 1267, "Model": 12623, "Created": "2024-11-05T14:38:45.042871Z", "Events": [ { "Id": "#d5a801ad", "Data": "hidraw" }, { "Id": "#ad8c58d3" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "MAJOR=241\nMINOR=0\nDEVNAME=hidraw0" }, { "Id": "#d432c663", "Data": "hidraw0" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c" }, { "Id": "#d5a801ad", "Data": "hid" }, { "Id": "#ad8c58d3", "Data": "hid-multitouch" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "DRIVER=hid-multitouch\nHID_ID=0018:000004F3:0000314F\nHID_NAME=ELAN0670:00 04F3:314F\nHID_PHYS=i2c-ELAN0670:00\nHID_UNIQ=\nMODALIAS=hid:b0018g0004v000004F3p0000314F" }, { "Id": "#d432c663" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c" }, { "Id": "#b8e66ef6", "Data": "0018:000004F3:0000314F" }, { "Id": "#89cef97f", "Data": "ELAN0670:00 04F3:314F" }, { "Id": "#3e5408a4", "Data": "" }, { "Id": "#00f97b18", "Data": "i2c-ELAN0670:00" }, { "Id": "#9eeaba55", "DataOut": "DQUDAAE=" }, { "Id": "#4fe18af3", "DataOut": "DQABAAE=" }, { "Id": "#6e38dca8", "DataOut": "DQUDAgE=" }, { "Id": "#4fe18af3", "DataOut": "DQIBAwA=" }, { "Id": "#a2791fef", "DataOut": "DQUDEAE=" }, { "Id": "#4fe18af3", "DataOut": "DRABBAE=" }, { "Id": "#4e8ff158", "DataOut": "DQUDAQE=" }, { "Id": "#4fe18af3", "DataOut": "DQEBDAA=" }, { "Id": "#1f16e790", "DataOut": "DQUDAwE=" }, { "Id": "#4fe18af3", "DataOut": "DQMBABE=" }, { "Id": "#d149ae3c", "DataOut": "DQUDEAM=" }, { "Id": "#4fe18af3", "DataOut": "DRADAAI=" } ] } ] } fwupd-2.0.10/plugins/elantp/tests/lenovo-nano-g1.json000066400000000000000000000005731501337203100224600ustar00rootroot00000000000000{ "name": "Lenovo Nano Gen1 (Touchpad)", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/lenovo-nano-g1-setup.json", "components": [ { "version": "0x0003", "botloader-version": "0x0001", "guids": [ "4103f1f4-2047-5142-be09-75ce6dff89ef" ] } ] } ] } fwupd-2.0.10/plugins/emmc/000077500000000000000000000000001501337203100153145ustar00rootroot00000000000000fwupd-2.0.10/plugins/emmc/README.md000066400000000000000000000023241501337203100165740ustar00rootroot00000000000000--- title: Plugin: eMMC --- ## Introduction This plugin reads the sysfs attributes corresponding to eMMC devices. It uses the kernel MMC API for flashing devices. ## Protocol eMMC devices support the `org.jedec.mmc` protocol. ## GUID Generation These devices use the following instance values: * `EMMC\NAME_%name%` * `EMMC\NAME_%name%&REV_%rev%` * `EMMC\MAN_%manfid%&OEM_%oemid%` (only-quirk) * `EMMC\MAN_%manfid%&OEM_%oemid%&NAME_%name%` * `EMMC\MAN_%manfid%&NAME_%name%&REV_%rev%` * `EMMC\MAN_%manfid%&OEM_%oemid%&NAME_%name%&REV_%rev%` One deprecated instance ID is also added; new firmware should not use this. * `EMMC\%manfid%&%oemid%&%name%` ## Update Behavior The firmware is deployed when the device is in normal runtime mode, but it is only activated when the device is rebooted. ## Quirk Use This plugin uses the following plugin-specific quirks: ### EmmcBlockSize The block size used for Emmc writes Since: 1.9.7 ## Vendor ID Security The vendor ID is set from the EMMC vendor, for example set to `EMMC:{$manfid}` ## External Interface Access This plugin requires ioctl `MMC_IOC_CMD` and `MMC_IOC_MULTI_CMD` access. ## Version Considerations This plugin has been available since fwupd version `1.3.3`. fwupd-2.0.10/plugins/emmc/emmc.quirk000066400000000000000000000011111501337203100173040ustar00rootroot00000000000000#Western Digital [EMMC\NAME_DI4064] EmmcBlockSize = 0x1000 [EMMC\NAME_DI4128] EmmcBlockSize = 0x1000 [EMMC\NAME_DJ4008] EmmcBlockSize = 0x1000 [EMMC\NAME_DJ4016] EmmcBlockSize = 0x1000 # vendor names [EMMC\MAN_0000] Vendor = SanDisk [EMMC\MAN_0002] Vendor = Kingston/SanDisk [EMMC\MAN_0003] Vendor = Toshiba [EMMC\MAN_0011] Vendor = Toshiba [EMMC\MAN_0013] Vendor = Micron [EMMC\MAN_0015] Vendor = Samsung/SanDisk/LG [EMMC\MAN_002c] Vendor = Kingston [EMMC\MAN_0037] Vendor = Kingmax [EMMC\MAN_0044] Vendor = SanDisk [EMMC\MAN_0045] Vendor = SanDisk [EMMC\MAN_0070] Vendor = Kingston fwupd-2.0.10/plugins/emmc/fu-emmc-device.c000066400000000000000000000473041501337203100202560ustar00rootroot00000000000000/* * Copyright 2019 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fu-emmc-device.h" /* From kernel linux/major.h */ #define MMC_BLOCK_MAJOR 179 /* From kernel linux/mmc/mmc.h */ #define MMC_SWITCH 6 /* ac [31:0] See below R1b */ #define MMC_SEND_EXT_CSD 8 /* adtc R1 */ #define MMC_SWITCH_MODE_WRITE_BYTE 0x03 /* Set target to value */ #define MMC_WRITE_BLOCK 24 /* adtc [31:0] data addr R1 */ #define MMC_SET_BLOCK_COUNT 23 /* adtc [31:0] data addr R1 */ #define MMC_WRITE_MULTIPLE_BLOCK 25 /* adtc [31:0] data addr R1 */ /* From kernel linux/mmc/core.h */ #define MMC_RSP_PRESENT (1 << 0) #define MMC_RSP_CRC (1 << 2) /* expect valid crc */ #define MMC_RSP_BUSY (1 << 3) /* card may send busy */ #define MMC_RSP_OPCODE (1 << 4) /* response contains opcode */ #define MMC_RSP_SPI_S1 (1 << 7) /* one status byte */ #define MMC_CMD_AC (0 << 5) #define MMC_CMD_ADTC (1 << 5) #define MMC_RSP_SPI_BUSY (1 << 10) /* card may send busy */ #define MMC_RSP_SPI_R1 (MMC_RSP_SPI_S1) #define MMC_RSP_SPI_R1B (MMC_RSP_SPI_S1 | MMC_RSP_SPI_BUSY) #define MMC_RSP_R1 (MMC_RSP_PRESENT | MMC_RSP_CRC | MMC_RSP_OPCODE) #define MMC_RSP_R1B (MMC_RSP_PRESENT | MMC_RSP_CRC | MMC_RSP_OPCODE | MMC_RSP_BUSY) /* EXT_CSD fields */ #define EXT_CSD_SUPPORTED_MODES 493 /* RO */ #define EXT_CSD_FFU_FEATURES 492 /* RO */ #define EXT_CSD_FFU_ARG_3 490 /* RO */ #define EXT_CSD_FFU_ARG_2 489 /* RO */ #define EXT_CSD_FFU_ARG_1 488 /* RO */ #define EXT_CSD_FFU_ARG_0 487 /* RO */ #define EXT_CSD_NUM_OF_FW_SEC_PROG_3 305 /* RO */ #define EXT_CSD_NUM_OF_FW_SEC_PROG_2 304 /* RO */ #define EXT_CSD_NUM_OF_FW_SEC_PROG_1 303 /* RO */ #define EXT_CSD_NUM_OF_FW_SEC_PROG_0 302 /* RO */ #define EXT_CSD_REV 192 #define EXT_CSD_FW_CONFIG 169 /* R/W */ #define EXT_CSD_DATA_SECTOR_SIZE 61 /* R */ #define EXT_CSD_MODE_CONFIG 30 #define EXT_CSD_MODE_OPERATION_CODES 29 /* W */ #define EXT_CSD_FFU_STATUS 26 /* R */ #define EXT_CSD_REV_V5_1 8 #define EXT_CSD_REV_V5_0 7 /* EXT_CSD field definitions */ #define EXT_CSD_NORMAL_MODE (0x00) #define EXT_CSD_FFU_MODE (0x01) #define EXT_CSD_FFU_INSTALL (0x01) #define EXT_CSD_FFU (1 << 0) #define EXT_CSD_UPDATE_DISABLE (1 << 0) #define EXT_CSD_CMD_SET_NORMAL (1 << 0) #define FU_EMMC_DEVICE_IOCTL_TIMEOUT 5000 /* ms */ struct _FuEmmcDevice { FuUdevDevice parent_instance; guint32 sect_size; guint32 write_block_size; }; G_DEFINE_TYPE(FuEmmcDevice, fu_emmc_device, FU_TYPE_UDEV_DEVICE) static void fu_emmc_device_to_string(FuDevice *device, guint idt, GString *str) { FuEmmcDevice *self = FU_EMMC_DEVICE(device); fwupd_codec_string_append_int(str, idt, "SectorSize", self->sect_size); } static gboolean fu_emmc_device_get_sysattr_guint64(FuUdevDevice *device, const gchar *name, guint64 *val_out, GError **error) { g_autofree gchar *value = NULL; value = fu_udev_device_read_sysfs(device, name, FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, error); if (value == NULL) return FALSE; return fu_strtoull(value, val_out, 0, G_MAXUINT64, FU_INTEGER_BASE_16, error); } static gboolean fu_emmc_device_probe(FuDevice *device, GError **error) { guint64 flag; guint64 oemid = 0; guint64 manfid = 0; g_autofree gchar *attr_fwrev = NULL; g_autofree gchar *attr_manfid = NULL; g_autofree gchar *attr_name = NULL; g_autofree gchar *dev_basename = NULL; g_autofree gchar *man_oem_name = NULL; g_autoptr(FuUdevDevice) udev_parent = NULL; udev_parent = FU_UDEV_DEVICE(fu_device_get_backend_parent_with_subsystem(device, "mmc:block", NULL)); if (udev_parent == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no MMC parent"); return FALSE; } /* ignore *rpmb and *boot* mmc block devices */ if (fu_udev_device_get_device_file(FU_UDEV_DEVICE(device)) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device has no device-file"); return FALSE; } dev_basename = g_path_get_basename(fu_udev_device_get_device_file(FU_UDEV_DEVICE(device))); if (!g_regex_match_simple("mmcblk\\d$", dev_basename, 0, /* G_REGEX_DEFAULT */ 0)) { /* G_REGEX_MATCH_DEFAULT */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "is not raw mmc block device, devname=%s", dev_basename); return FALSE; } /* doesn't support FFU */ if (!fu_emmc_device_get_sysattr_guint64(udev_parent, "ffu_capable", &flag, error)) return FALSE; if (flag == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "%s does not support field firmware updates", fu_device_get_name(device)); return FALSE; } /* name */ attr_name = fu_udev_device_read_sysfs(udev_parent, "name", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (attr_name == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "%s does not have 'name' sysattr", fu_device_get_name(device)); return FALSE; } fu_device_add_instance_strsafe(device, "NAME", attr_name); fu_device_build_instance_id(device, NULL, "EMMC", "NAME", NULL); fu_device_set_name(device, attr_name); /* firmware version */ attr_fwrev = fu_udev_device_read_sysfs(udev_parent, "fwrev", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (attr_fwrev != NULL) { fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_NUMBER); fu_device_set_version(device, attr_fwrev); } fu_device_add_instance_strsafe(device, "REV", attr_fwrev); if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_ADD_INSTANCE_ID_REV)) fu_device_build_instance_id(device, NULL, "EMMC", "NAME", "REV", NULL); /* manfid + oemid, manfid + oemid + name */ if (!fu_emmc_device_get_sysattr_guint64(udev_parent, "manfid", &manfid, error)) return FALSE; if (!fu_emmc_device_get_sysattr_guint64(udev_parent, "oemid", &oemid, error)) return FALSE; fu_device_add_instance_u16(device, "MAN", manfid); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "EMMC", "MAN", NULL); fu_device_add_instance_u16(device, "OEM", oemid); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "EMMC", "MAN", "OEM", NULL); fu_device_build_instance_id(device, NULL, "EMMC", "MAN", "OEM", "NAME", NULL); fu_device_build_instance_id(device, NULL, "EMMC", "MAN", "NAME", "REV", NULL); fu_device_build_instance_id(device, NULL, "EMMC", "MAN", "OEM", "NAME", "REV", NULL); /* this is a (invalid!) instance ID added for legacy compatibility */ man_oem_name = g_strdup_printf("EMMC\\%04" G_GUINT64_FORMAT "&%04" G_GUINT64_FORMAT "&%s", manfid, oemid, fu_device_get_name(device)); fu_device_add_instance_id(device, man_oem_name); /* set the vendor */ attr_manfid = fu_udev_device_read_sysfs(udev_parent, "manfid", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, error); if (attr_manfid == NULL) return FALSE; fu_device_build_vendor_id(device, "EMMC", attr_manfid); /* set the physical ID from the mmc parent */ fu_device_set_physical_id(device, fu_device_get_backend_id(FU_DEVICE(udev_parent))); /* internal */ if (!fu_emmc_device_get_sysattr_guint64(FU_UDEV_DEVICE(device), "removable", &flag, error)) return FALSE; if (flag == 0) fu_device_add_flag(device, FWUPD_DEVICE_FLAG_INTERNAL); return TRUE; } static gboolean fu_emmc_device_ioctl_buffer_cb(FuIoctl *self, gpointer ptr, guint8 *buf, gsize bufsz, GError **error) { struct mmc_ioc_cmd *idata = (struct mmc_ioc_cmd *)ptr; idata->data_ptr = (guint64)((gulong)buf); return TRUE; } static gboolean fu_emmc_device_read_extcsd(FuEmmcDevice *self, guint8 *buf, gsize bufsz, GError **error) { g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); struct mmc_ioc_cmd idata = { .write_flag = 0, .opcode = MMC_SEND_EXT_CSD, .arg = 0, .flags = MMC_RSP_SPI_R1 | MMC_RSP_R1 | MMC_CMD_ADTC, .blksz = 512, .blocks = 1, }; /* include these when generating the emulation event */ fu_ioctl_add_key_as_u16(ioctl, "Request", MMC_IOC_CMD); fu_ioctl_add_key_as_u8(ioctl, "Opcode", idata.opcode); fu_ioctl_add_mutable_buffer(ioctl, NULL, buf, bufsz, fu_emmc_device_ioctl_buffer_cb); if (!fu_ioctl_execute(ioctl, MMC_IOC_CMD, (guint8 *)&idata, sizeof(idata), NULL, FU_EMMC_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_NONE, error)) { g_prefix_error(error, "failed to MMC_IOC_CMD: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_emmc_device_validate_extcsd(FuEmmcDevice *self, GError **error) { guint8 ext_csd[512] = {0x0}; if (!fu_emmc_device_read_extcsd(self, ext_csd, sizeof(ext_csd), error)) return FALSE; if (ext_csd[EXT_CSD_REV] < EXT_CSD_REV_V5_0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "FFU is only available on devices >= " "MMC 5.0, not supported in %s", fu_device_get_name(FU_DEVICE(self))); return FALSE; } if ((ext_csd[EXT_CSD_SUPPORTED_MODES] & EXT_CSD_FFU) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "FFU is not supported in %s", fu_device_get_name(FU_DEVICE(self))); return FALSE; } if (ext_csd[EXT_CSD_FW_CONFIG] & EXT_CSD_UPDATE_DISABLE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "firmware update was disabled in %s", fu_device_get_name(FU_DEVICE(self))); return FALSE; } self->sect_size = (ext_csd[EXT_CSD_DATA_SECTOR_SIZE] == 0) ? 512 : 4096; return TRUE; } static gboolean fu_emmc_device_setup(FuDevice *device, GError **error) { FuEmmcDevice *self = FU_EMMC_DEVICE(device); g_autoptr(GError) error_validate = NULL; if (!fu_emmc_device_validate_extcsd(self, &error_validate)) g_debug("%s", error_validate->message); else fu_device_add_flag(FU_DEVICE(device), FWUPD_DEVICE_FLAG_UPDATABLE); return TRUE; } static FuFirmware * fu_emmc_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuEmmcDevice *self = FU_EMMC_DEVICE(device); g_autoptr(FuFirmware) firmware = fu_firmware_new(); /* check alignment */ if (fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; if ((fu_firmware_get_size(firmware) % self->sect_size) > 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware data size (%" G_GSIZE_FORMAT ") is not aligned", fu_firmware_get_size(firmware)); return NULL; } return g_steal_pointer(&firmware); } static gboolean fu_emmc_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuEmmcDevice *self = FU_EMMC_DEVICE(device); guint32 arg; guint32 sect_done = 0; guint32 sector_size; gboolean check_sect_done = FALSE; gsize multi_cmdsz; guint8 ext_csd[512]; guint failure_cnt = 0; g_autofree struct mmc_ioc_multi_cmd *multi_cmd = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 5, "ffu"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 50, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 45, NULL); if (!fu_emmc_device_read_extcsd(self, ext_csd, sizeof(ext_csd), error)) return FALSE; stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; sector_size = self->write_block_size ?: self->sect_size; /* mode operation codes are supported */ check_sect_done = (ext_csd[EXT_CSD_FFU_FEATURES] & 1) > 0; /* set CMD ARG */ arg = ext_csd[EXT_CSD_FFU_ARG_0] | ext_csd[EXT_CSD_FFU_ARG_1] << 8 | ext_csd[EXT_CSD_FFU_ARG_2] << 16 | ext_csd[EXT_CSD_FFU_ARG_3] << 24; /* prepare multi_cmd to be sent */ multi_cmdsz = sizeof(struct mmc_ioc_multi_cmd) + 4 * sizeof(struct mmc_ioc_cmd); multi_cmd = g_malloc0(multi_cmdsz); multi_cmd->num_of_cmds = 4; /* put device into ffu mode */ multi_cmd->cmds[0].opcode = MMC_SWITCH; multi_cmd->cmds[0].arg = (MMC_SWITCH_MODE_WRITE_BYTE << 24) | (EXT_CSD_MODE_CONFIG << 16) | (EXT_CSD_FFU_MODE << 8) | EXT_CSD_CMD_SET_NORMAL; multi_cmd->cmds[0].flags = MMC_RSP_SPI_R1B | MMC_RSP_R1B | MMC_CMD_AC; multi_cmd->cmds[0].write_flag = 1; /* send block count */ multi_cmd->cmds[1].opcode = MMC_SET_BLOCK_COUNT; multi_cmd->cmds[1].arg = sector_size / 512; multi_cmd->cmds[1].flags = MMC_RSP_SPI_R1 | MMC_RSP_R1 | MMC_CMD_ADTC; /* send image chunk */ multi_cmd->cmds[2].opcode = MMC_WRITE_MULTIPLE_BLOCK; multi_cmd->cmds[2].blksz = 512; multi_cmd->cmds[2].blocks = sector_size / 512; multi_cmd->cmds[2].arg = arg; multi_cmd->cmds[2].flags = MMC_RSP_SPI_R1 | MMC_RSP_R1 | MMC_CMD_ADTC; multi_cmd->cmds[2].write_flag = 1; /* return device into normal mode */ multi_cmd->cmds[3].opcode = MMC_SWITCH; multi_cmd->cmds[3].arg = (MMC_SWITCH_MODE_WRITE_BYTE << 24) | (EXT_CSD_MODE_CONFIG << 16) | (EXT_CSD_NORMAL_MODE << 8) | EXT_CSD_CMD_SET_NORMAL; multi_cmd->cmds[3].flags = MMC_RSP_SPI_R1B | MMC_RSP_R1B | MMC_CMD_AC; multi_cmd->cmds[3].write_flag = 1; fu_progress_step_done(progress); /* build packets */ chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, sector_size, error); if (chunks == NULL) return FALSE; while (failure_cnt < 3) { for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; mmc_ioc_cmd_set_data(multi_cmd->cmds[2], fu_chunk_get_data(chk)); if (!fu_ioctl_execute(ioctl, MMC_IOC_MULTI_CMD, (guint8 *)multi_cmd, multi_cmdsz, NULL, FU_EMMC_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_NONE, error)) { g_autoptr(GError) error_local = NULL; g_prefix_error(error, "multi-cmd failed: "); /* multi-cmd ioctl failed before exiting from ffu mode */ if (!fu_ioctl_execute(ioctl, MMC_IOC_CMD, (guint8 *)&multi_cmd->cmds[3], sizeof(struct mmc_ioc_cmd), NULL, FU_EMMC_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_NONE, &error_local)) { g_prefix_error(error, "%s: ", error_local->message); } return FALSE; } /* update progress */ fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)i + 1, (gsize)fu_chunk_array_length(chunks)); } if (!check_sect_done) break; if (!fu_emmc_device_read_extcsd(self, ext_csd, sizeof(ext_csd), error)) return FALSE; sect_done = ext_csd[EXT_CSD_NUM_OF_FW_SEC_PROG_0] | ext_csd[EXT_CSD_NUM_OF_FW_SEC_PROG_1] << 8 | ext_csd[EXT_CSD_NUM_OF_FW_SEC_PROG_2] << 16 | ext_csd[EXT_CSD_NUM_OF_FW_SEC_PROG_3] << 24; if (sect_done != 0) break; failure_cnt++; g_debug("programming failed: retrying (%u)", failure_cnt); fu_progress_step_done(progress); } fu_progress_step_done(progress); /* sanity check */ if (check_sect_done) { gsize streamsz = 0; gsize total_done = (gsize)sect_done * (gsize)self->sect_size; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (total_done != streamsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "firmware size and number of sectors written " "mismatch (%" G_GSIZE_FORMAT "/%" G_GSIZE_FORMAT "):", total_done, streamsz); return FALSE; } } /* check mode operation for ffu install*/ if (!check_sect_done) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT); } else { /* re-enter ffu mode and install the firmware */ multi_cmd->num_of_cmds = 2; multi_cmdsz = sizeof(struct mmc_ioc_multi_cmd) + multi_cmd->num_of_cmds * sizeof(struct mmc_ioc_cmd); /* set ext_csd to install mode */ multi_cmd->cmds[1].opcode = MMC_SWITCH; multi_cmd->cmds[1].blksz = 0; multi_cmd->cmds[1].blocks = 0; multi_cmd->cmds[1].arg = (MMC_SWITCH_MODE_WRITE_BYTE << 24) | (EXT_CSD_MODE_OPERATION_CODES << 16) | (EXT_CSD_FFU_INSTALL << 8) | EXT_CSD_CMD_SET_NORMAL; multi_cmd->cmds[1].flags = MMC_RSP_SPI_R1B | MMC_RSP_R1B | MMC_CMD_AC; multi_cmd->cmds[1].write_flag = 1; /* send ioctl with multi-cmd */ if (!fu_ioctl_execute(ioctl, MMC_IOC_MULTI_CMD, (guint8 *)multi_cmd, multi_cmdsz, NULL, FU_EMMC_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_NONE, error)) { g_autoptr(GError) error_local = NULL; /* In case multi-cmd ioctl failed before exiting from ffu mode */ g_prefix_error(error, "multi-cmd failed setting install mode: "); if (!fu_ioctl_execute(ioctl, MMC_IOC_CMD, (guint8 *)&multi_cmd->cmds[2], sizeof(struct mmc_ioc_cmd), NULL, FU_EMMC_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_NONE, &error_local)) { g_prefix_error(error, "%s: ", error_local->message); } return FALSE; } /* return status */ if (!fu_emmc_device_read_extcsd(self, ext_csd, sizeof(ext_csd), error)) return FALSE; if (ext_csd[EXT_CSD_FFU_STATUS] != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "FFU install failed: %d", ext_csd[EXT_CSD_FFU_STATUS]); return FALSE; } } fu_progress_step_done(progress); return TRUE; } static gboolean fu_emmc_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuEmmcDevice *self = FU_EMMC_DEVICE(device); if (g_strcmp0(key, "EmmcBlockSize") == 0) { guint64 tmp = 0; if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->write_block_size = tmp; return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_emmc_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 98, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static void fu_emmc_device_init(FuEmmcDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "org.jedec.mmc"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_MEMORY); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_SIGNED); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); } static void fu_emmc_device_class_init(FuEmmcDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->set_quirk_kv = fu_emmc_device_set_quirk_kv; device_class->setup = fu_emmc_device_setup; device_class->to_string = fu_emmc_device_to_string; device_class->prepare_firmware = fu_emmc_device_prepare_firmware; device_class->probe = fu_emmc_device_probe; device_class->write_firmware = fu_emmc_device_write_firmware; device_class->set_progress = fu_emmc_device_set_progress; } fwupd-2.0.10/plugins/emmc/fu-emmc-device.h000066400000000000000000000004541501337203100202560ustar00rootroot00000000000000/* * Copyright 2019 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_EMMC_DEVICE (fu_emmc_device_get_type()) G_DECLARE_FINAL_TYPE(FuEmmcDevice, fu_emmc_device, FU, EMMC_DEVICE, FuUdevDevice) fwupd-2.0.10/plugins/emmc/fu-emmc-plugin.c000066400000000000000000000015371501337203100203130ustar00rootroot00000000000000/* * Copyright 2019 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-emmc-device.h" #include "fu-emmc-plugin.h" struct _FuEmmcPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuEmmcPlugin, fu_emmc_plugin, FU_TYPE_PLUGIN) static void fu_emmc_plugin_init(FuEmmcPlugin *self) { } static void fu_emmc_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "EmmcBlockSize"); fu_plugin_add_device_udev_subsystem(plugin, "block:disk"); fu_plugin_add_device_gtype(plugin, FU_TYPE_EMMC_DEVICE); } static void fu_emmc_plugin_class_init(FuEmmcPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_emmc_plugin_constructed; } fwupd-2.0.10/plugins/emmc/fu-emmc-plugin.h000066400000000000000000000003461501337203100203150ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuEmmcPlugin, fu_emmc_plugin, FU, EMMC_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/emmc/meson.build000066400000000000000000000010171501337203100174550ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginEmmc"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('emmc.quirk') plugin_builtins += static_library('fu_plugin_emmc', sources: [ 'fu-emmc-plugin.c', 'fu-emmc-device.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) enumeration_data += files('tests/sandisk-da4064-setup.json') device_tests += files('tests/sandisk-da4064.json') endif fwupd-2.0.10/plugins/emmc/tests/000077500000000000000000000000001501337203100164565ustar00rootroot00000000000000fwupd-2.0.10/plugins/emmc/tests/sandisk-da4064-setup.json000066400000000000000000000076021501337203100230500ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUdevDevice", "BackendId": "/sys/devices/platform/fe330000.mmc/mmc_host/mmc2/mmc2:0001/block/mmcblk2", "DeviceFile": "/dev/mmcblk2", "Subsystem": "block", "Created": "2024-11-07T13:44:05.189919Z", "Events": [ { "Id": "#d5a801ad", "Data": "block" }, { "Id": "#ad8c58d3" }, { "Id": "#1075ed5c", "Data": "disk" }, { "Id": "#bddbca22", "Data": "MAJOR=179\nMINOR=0\nDEVNAME=mmcblk2\nDEVTYPE=disk\nDISKSEQ=1" }, { "Id": "#d432c663", "Data": "mmcblk2" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c", "Data": "disk" }, { "Id": "#19c26604" }, { "Id": "#0e9e7dc9", "GType": "FuUdevDevice", "BackendId": "/sys/devices/platform/fe330000.mmc/mmc_host/mmc2/mmc2:0001/block/mmcblk2" }, { "Id": "#d5a801ad", "Data": "mmc" }, { "Id": "#ad8c58d3", "Data": "mmcblk" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "DRIVER=mmcblk\nMMC_TYPE=MMC\nMMC_NAME=DA4064\nMODALIAS=mmc:block" }, { "Id": "#0d176430", "Data": "mmc:block" }, { "Id": "#d432c663" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c" }, { "Id": "#e9f704fa", "Data": "1" }, { "Id": "#e371a663", "Data": "DA4064" }, { "Id": "#4f7d7cad", "Data": "0x3536323330613137" }, { "Id": "#e9729218", "Data": "0x000045" }, { "Id": "#d8e0d47a", "Data": "0x0100" }, { "Id": "#e9729218", "Data": "0x000045" }, { "Id": "#1fdc635e", "GType": "FuUdevDevice", "BackendId": "/sys/devices/platform/fe330000.mmc/mmc_host/mmc2/mmc2:0001/block/mmcblk2" }, { "Id": "#d5a801ad", "Data": "mmc" }, { "Id": "#ad8c58d3", "Data": "mmcblk" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "DRIVER=mmcblk\nMMC_TYPE=MMC\nMMC_NAME=DA4064\nMODALIAS=mmc:block" }, { "Id": "#0d176430", "Data": "mmc:block" }, { "Id": "#d432c663" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c" }, { "Id": "#0b053619", "Data": "0" }, { "Id": "#09a5a089", "DataOut": "AAAAAAAAAAAAAAAAAAAAAAgDmAlnAgAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACUJAAcBAAIAABUfgAAAAAAAAAEAAAAAAAAAAAECAA0AAAAACAACAFcfCgPd3QAAAAoKCgoKCgEAwEcHFxMXBwgQAQMBCCAAB6amVQMAAAAA3d0AAf8QAAAAABkZABAAAN01NjIzMGExN1FQCAgIAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAQAAMDAAUDAwE/PwEBAQAAAAAAAAA=" } ] } ] } fwupd-2.0.10/plugins/emmc/tests/sandisk-da4064.json000066400000000000000000000005401501337203100217040ustar00rootroot00000000000000{ "name": "SanDisk DA4064 (PineBook Pro)", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/sandisk-da4064-setup.json", "components": [ { "version": "0x3536323330613137", "guids": [ "1d13840a-391c-5f1e-9c98-21814fbb0503" ] } ] } ] } fwupd-2.0.10/plugins/ep963x/000077500000000000000000000000001501337203100154315ustar00rootroot00000000000000fwupd-2.0.10/plugins/ep963x/README.md000066400000000000000000000016311501337203100167110ustar00rootroot00000000000000--- title: Plugin: EP963x --- ## Introduction The EP963x is a generic MCU used in many different products. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a packed binary file format. This plugin supports the following protocol ID: * `tw.com.exploretech.ep963x` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_17EF&PID_7226` ## Update Behavior The device usually presents in runtime mode, but on detach re-enumerates with the same USB PID in an unlocked mode. On attach the device again re-enumerates back to the runtime locked mode. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x17EF` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.4.0`. fwupd-2.0.10/plugins/ep963x/ep963x.quirk000066400000000000000000000000501501337203100175370ustar00rootroot00000000000000[USB\VID_17EF&PID_7226] Plugin = ep963x fwupd-2.0.10/plugins/ep963x/fu-ep963x-common.h000066400000000000000000000034571501337203100205470ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_EP963_FIRMWARE_SIZE 0x1f000 #define FU_EP963_TRANSFER_BLOCK_SIZE 0x200 /* 512 */ #define FU_EP963_TRANSFER_CHUNK_SIZE 0x04 #define FU_EP963_FEATURE_ID1_SIZE 0x08 #define FU_EP963_USB_CONTROL_ID 0x01 #define FU_EP963_ICP_ENTER 0x40 #define FU_EP963_ICP_EXIT 0x82 #define FU_EP963_ICP_BANK 0x83 #define FU_EP963_ICP_ADDRESS 0x84 #define FU_EP963_ICP_READBLOCK 0x85 #define FU_EP963_ICP_WRITEBLOCK 0x86 #define FU_EP963_ICP_MCUID 0x87 #define FU_EP963_ICP_DONE 0x5A #define FU_EP963_OPCODE_SMBUS_READ 0x01 #define FU_EP963_OPCODE_ERASE_SPI 0x02 #define FU_EP963_OPCODE_RESET_BLOCK_INDEX 0x03 #define FU_EP963_OPCODE_WRITE_BLOCK_DATA 0x04 #define FU_EP963_OPCODE_PROGRAM_SPI_BLOCK 0x05 #define FU_EP963_OPCODE_PROGRAM_SPI_FINISH 0x06 #define FU_EP963_OPCODE_GET_SPI_CHECKSUM 0x07 #define FU_EP963_OPCODE_PROGRAM_EP_FLASH 0x08 #define FU_EP963_OPCODE_GET_EP_CHECKSUM 0x09 #define FU_EP963_OPCODE_START_THROW_PAGE 0x0B #define FU_EP963_OPCODE_GET_EP_SITE_TYPE 0x0C #define FU_EP963_OPCODE_COMMAND_VERSION 0x10 #define FU_EP963_OPCODE_COMMAND_STATUS 0x20 #define FU_EP963_OPCODE_SUBMCU_ENTER_ICP 0x30 #define FU_EP963_OPCODE_SUBMCU_RESET_BLOCK_IDX 0x31 #define FU_EP963_OPCODE_SUBMCU_WRITE_BLOCK_DATA 0x32 #define FU_EP963_OPCODE_SUBMCU_PROGRAM_BLOCK 0x33 #define FU_EP963_OPCODE_SUBMCU_PROGRAM_FINISHED 0x34 #define FU_EP963_UF_CMD_VERSION 0x00 #define FU_EP963_UF_CMD_ENTERISP 0x01 #define FU_EP963_UF_CMD_PROGRAM 0x02 #define FU_EP963_UF_CMD_READ 0x03 #define FU_EP963_UF_CMD_MODE 0x04 /* byte 0x02 */ #define FU_EP963_USB_STATE_READY 0x00 #define FU_EP963_USB_STATE_BUSY 0x01 #define FU_EP963_USB_STATE_FAIL 0x02 #define FU_EP963_USB_STATE_UNKNOWN 0xff fwupd-2.0.10/plugins/ep963x/fu-ep963x-device.c000066400000000000000000000246561501337203100205150ustar00rootroot00000000000000/*# * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-ep963x-common.h" #include "fu-ep963x-device.h" #include "fu-ep963x-firmware.h" #include "fu-ep963x-struct.h" struct _FuEp963xDevice { FuHidDevice parent_instance; }; G_DEFINE_TYPE(FuEp963xDevice, fu_ep963x_device, FU_TYPE_HID_DEVICE) #define FU_EP963_DEVICE_TIMEOUT 5000 /* ms */ static gboolean fu_ep963x_device_write(FuEp963xDevice *self, guint8 ctrl_id, guint8 cmd, const guint8 *buf, gsize bufsz, GError **error) { guint8 bufhw[FU_EP963_FEATURE_ID1_SIZE] = { ctrl_id, cmd, 0x0, }; if (buf != NULL) { if (!fu_memcpy_safe(bufhw, sizeof(bufhw), 0x02, /* dst */ buf, bufsz, 0x0, /* src */ bufsz, error)) return FALSE; } if (!fu_hid_device_set_report(FU_HID_DEVICE(self), 0x00, bufhw, sizeof(bufhw), FU_EP963_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_IS_FEATURE, error)) return FALSE; /* wait for hardware */ fu_device_sleep(FU_DEVICE(self), 100); return TRUE; } static gboolean fu_ep963x_device_write_icp(FuEp963xDevice *self, guint8 cmd, const guint8 *buf, gsize bufsz, guint8 *bufout, gsize bufoutsz, GError **error) { /* wait for hardware */ for (guint i = 0; i < 5; i++) { guint8 bufhw[FU_EP963_FEATURE_ID1_SIZE] = { FU_EP963_USB_CONTROL_ID, cmd, }; if (!fu_ep963x_device_write(self, FU_EP963_USB_CONTROL_ID, cmd, buf, bufsz, error)) return FALSE; if (!fu_hid_device_get_report(FU_HID_DEVICE(self), 0x00, bufhw, sizeof(bufhw), FU_EP963_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_IS_FEATURE, error)) { return FALSE; } if (bufhw[2] == FU_EP963_USB_STATE_READY) { /* optional data */ if (bufout != NULL) { if (!fu_memcpy_safe(bufout, bufoutsz, 0x0, bufhw, sizeof(bufhw), 0x02, bufoutsz, error)) return FALSE; } return TRUE; } g_debug("SMBUS: %s [0x%x]", fu_ep963x_smbus_error_to_string(bufhw[7]), bufhw[7]); fu_device_sleep(FU_DEVICE(self), 100); } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to wait for icp-done"); return FALSE; } static gboolean fu_ep963x_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuEp963xDevice *self = FU_EP963X_DEVICE(device); const guint8 buf[] = {'E', 'P', '9', '6', '3'}; g_autoptr(GError) error_local = NULL; /* sanity check */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in bootloader mode, skipping"); return TRUE; } if (!fu_ep963x_device_write_icp(self, FU_EP963_ICP_ENTER, buf, sizeof(buf), /* in */ NULL, 0x0, /* out */ &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to detach: %s", error_local->message); return FALSE; } fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_ep963x_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuEp963xDevice *self = FU_EP963X_DEVICE(device); g_autoptr(GError) error_local = NULL; /* sanity check */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in runtime mode, skipping"); return TRUE; } if (!fu_ep963x_device_write(self, FU_EP963_USB_CONTROL_ID, FU_EP963_OPCODE_SUBMCU_PROGRAM_FINISHED, NULL, 0, &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to boot to runtime: %s", error_local->message); return FALSE; } fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_ep963x_device_setup(FuDevice *device, GError **error) { FuEp963xDevice *self = FU_EP963X_DEVICE(device); guint8 buf[] = {0x0}; g_autofree gchar *version = NULL; /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_ep963x_device_parent_class)->setup(device, error)) return FALSE; /* get version */ if (!fu_ep963x_device_write_icp(self, FU_EP963_UF_CMD_VERSION, NULL, 0, /* in */ buf, sizeof(buf), /* out */ error)) { return FALSE; } version = g_strdup_printf("%i", buf[0]); fu_device_set_version(device, version); /* the VID and PID are unchanged between bootloader modes */ if (buf[0] == 0x00) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); } else { fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); } /* success */ return TRUE; } static gboolean fu_ep963x_device_wait_cb(FuDevice *device, gpointer user_data, GError **error) { guint8 bufhw[FU_EP963_FEATURE_ID1_SIZE] = { FU_EP963_USB_CONTROL_ID, FU_EP963_OPCODE_SUBMCU_PROGRAM_BLOCK, 0xFF, }; if (!fu_hid_device_get_report(FU_HID_DEVICE(device), 0x00, bufhw, sizeof(bufhw), FU_EP963_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_IS_FEATURE, error)) { return FALSE; } if (bufhw[2] != FU_EP963_USB_STATE_READY) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "hardware is not ready"); return FALSE; } return TRUE; } static gboolean fu_ep963x_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuEp963xDevice *self = FU_EP963X_DEVICE(device); g_autoptr(GInputStream) stream = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(FuChunkArray) blocks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 5, "icp"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 95, NULL); /* get default image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* reset the block index */ if (!fu_ep963x_device_write(self, FU_EP963_USB_CONTROL_ID, FU_EP963_OPCODE_SUBMCU_ENTER_ICP, NULL, 0, &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to reset block index: %s", error_local->message); return FALSE; } fu_progress_step_done(progress); /* write each block */ blocks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_EP963_TRANSFER_BLOCK_SIZE, error); if (blocks == NULL) return FALSE; for (guint i = 0; i < fu_chunk_array_length(blocks); i++) { guint8 buf[] = {i}; g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(FuChunk) chk2 = NULL; g_autoptr(GBytes) chk_blob = NULL; /* set the block index */ if (!fu_ep963x_device_write(self, FU_EP963_USB_CONTROL_ID, FU_EP963_OPCODE_SUBMCU_RESET_BLOCK_IDX, buf, sizeof(buf), &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to reset block index: %s", error_local->message); return FALSE; } /* 4 byte chunks */ chk2 = fu_chunk_array_index(blocks, i, error); if (chk2 == NULL) return FALSE; chk_blob = fu_chunk_get_bytes(chk2); chunks = fu_chunk_array_new_from_bytes(chk_blob, fu_chunk_get_address(chk2), FU_CHUNK_PAGESZ_NONE, FU_EP963_TRANSFER_CHUNK_SIZE); for (guint j = 0; j < fu_chunk_array_length(chunks); j++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GError) error_loop = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, j, error); if (chk == NULL) return FALSE; /* copy data and write */ if (!fu_ep963x_device_write(self, FU_EP963_USB_CONTROL_ID, FU_EP963_OPCODE_SUBMCU_WRITE_BLOCK_DATA, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), &error_loop)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to write 0x%x: %s", (guint)fu_chunk_get_address(chk), error_loop->message); return FALSE; } } /* program block */ if (!fu_ep963x_device_write(self, FU_EP963_USB_CONTROL_ID, FU_EP963_OPCODE_SUBMCU_PROGRAM_BLOCK, buf, sizeof(buf), &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to write 0x%x: %s", (guint)fu_chunk_get_address(chk2), error_local->message); return FALSE; } /* wait for program finished */ if (!fu_device_retry(device, fu_ep963x_device_wait_cb, 5, NULL, error)) return FALSE; /* update progress */ fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)i + 1, (gsize)fu_chunk_array_length(chunks)); } fu_progress_step_done(progress); /* success! */ return TRUE; } static void fu_ep963x_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static void fu_ep963x_device_init(FuEp963xDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_protocol(FU_DEVICE(self), "tw.com.exploretech.ep963x"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_NUMBER); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_set_firmware_size(FU_DEVICE(self), FU_EP963_FIRMWARE_SIZE); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_EP963X_FIRMWARE); fu_device_retry_set_delay(FU_DEVICE(self), 100); } static void fu_ep963x_device_class_init(FuEp963xDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_ep963x_device_write_firmware; device_class->attach = fu_ep963x_device_attach; device_class->detach = fu_ep963x_device_detach; device_class->setup = fu_ep963x_device_setup; device_class->set_progress = fu_ep963x_device_set_progress; } fwupd-2.0.10/plugins/ep963x/fu-ep963x-device.h000066400000000000000000000004531501337203100205070ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_EP963X_DEVICE (fu_ep963x_device_get_type()) G_DECLARE_FINAL_TYPE(FuEp963xDevice, fu_ep963x_device, FU, EP963X_DEVICE, FuHidDevice) fwupd-2.0.10/plugins/ep963x/fu-ep963x-firmware.c000066400000000000000000000026431501337203100210620ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-ep963x-common.h" #include "fu-ep963x-firmware.h" #include "fu-ep963x-struct.h" struct _FuEp963xFirmware { FuFirmware parent_instance; }; G_DEFINE_TYPE(FuEp963xFirmware, fu_ep963x_firmware, FU_TYPE_FIRMWARE) static gboolean fu_ep963x_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_ep963x_firmware_hdr_validate_stream(stream, offset, error); } static gboolean fu_ep963x_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { gsize streamsz = 0; /* check size */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz != FU_EP963_FIRMWARE_SIZE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware size expected 0x%x, got 0x%x", (guint)FU_EP963_FIRMWARE_SIZE, (guint)streamsz); return FALSE; } /* success */ return TRUE; } static void fu_ep963x_firmware_init(FuEp963xFirmware *self) { } static void fu_ep963x_firmware_class_init(FuEp963xFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_ep963x_firmware_validate; firmware_class->parse = fu_ep963x_firmware_parse; } fwupd-2.0.10/plugins/ep963x/fu-ep963x-firmware.h000066400000000000000000000004641501337203100210660ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_EP963X_FIRMWARE (fu_ep963x_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuEp963xFirmware, fu_ep963x_firmware, FU, EP963X_FIRMWARE, FuFirmware) fwupd-2.0.10/plugins/ep963x/fu-ep963x-plugin.c000066400000000000000000000020711501337203100205370ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-ep963x-device.h" #include "fu-ep963x-firmware.h" #include "fu-ep963x-plugin.h" struct _FuEp963XPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuEp963XPlugin, fu_ep963x_plugin, FU_TYPE_PLUGIN) static void fu_ep963x_plugin_init(FuEp963XPlugin *self) { } static void fu_ep963x_plugin_object_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_set_name(plugin, "ep963x"); } static void fu_ep963x_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_EP963X_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_EP963X_FIRMWARE); } static void fu_ep963x_plugin_class_init(FuEp963XPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->constructed = fu_ep963x_plugin_object_constructed; plugin_class->constructed = fu_ep963x_plugin_constructed; } fwupd-2.0.10/plugins/ep963x/fu-ep963x-plugin.h000066400000000000000000000003541501337203100205460ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuEp963XPlugin, fu_ep963x_plugin, FU, EP963X_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/ep963x/fu-ep963x.rs000066400000000000000000000007021501337203100174440ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ValidateStream, Default)] #[repr(C, packed)] struct FuStructEp963xFirmwareHdr { reserved: [u8; 16], magic: [char; 5] == "EP963", } // byte = 0x07 #[derive(ToString)] enum FuEp963xSmbusError { None = 0x00, Address = 0x01, NoAck = 0x02, Arbitration = 0x04, Command = 0x08, Timeout = 0x10, Busy = 0x20, } fwupd-2.0.10/plugins/ep963x/meson.build000066400000000000000000000007471501337203100176030ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginEp963x"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('ep963x.quirk') plugin_builtins += static_library('fu_plugin_ep963x', rustgen.process('fu-ep963x.rs'), sources: [ 'fu-ep963x-device.c', 'fu-ep963x-firmware.c', 'fu-ep963x-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/fastboot/000077500000000000000000000000001501337203100162145ustar00rootroot00000000000000fwupd-2.0.10/plugins/fastboot/README.md000066400000000000000000000032621501337203100174760ustar00rootroot00000000000000--- title: Plugin: Fastboot --- ## Introduction This plugin is used to update hardware that uses the fastboot protocol. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in ZIP file format. Inside the zip file must be all the firmware images for each partition and a manifest file. The partition images can be in any format, but the manifest must be either an Android `flashfile.xml` format file, or a QFIL `partition_nand.xml` format file. For both types, all partitions with a defined image found in the zip file will be updated. This plugin supports the following protocol ID: * `com.google.fastboot` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_18D1&PID_4EE0` ## Update Behavior A fastboot device usually presents in runtime mode (or with no interface), but if the user puts the device into fastboot mode using a physical button it then enumerates with a USB descriptor. On attach the device reboots to runtime mode which *may* mean the device "goes away". For this reason the `REPLUG_MATCH_GUID` internal device flag is used so that the bootloader and runtime modes are treated as the same device. ## Quirk Use This plugin uses the following plugin-specific quirk: ### FastbootBlockSize Block size to use for transfers. Since: 1.2.2 ### FastbootOperationDelay Time in ms to delay after a read or write operation. Since: 1.7.4 ## Vendor ID Security The vendor ID is set from the USB vendor, for example `USB:0x18D1` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.2.2`. fwupd-2.0.10/plugins/fastboot/ci.quirk000066400000000000000000000000741501337203100176650ustar00rootroot00000000000000# Google Pixel 3a [USB\VID_18D1&PID_4EE0] Plugin = fastboot fwupd-2.0.10/plugins/fastboot/data/000077500000000000000000000000001501337203100171255ustar00rootroot00000000000000fwupd-2.0.10/plugins/fastboot/data/android/000077500000000000000000000000001501337203100205455ustar00rootroot00000000000000fwupd-2.0.10/plugins/fastboot/data/android/flashfile.xml000066400000000000000000000003701501337203100232240ustar00rootroot00000000000000 fwupd-2.0.10/plugins/fastboot/data/lsusb.txt000066400000000000000000000036661501337203100210310ustar00rootroot00000000000000Bus 001 Device 025: ID 18d1:4ee0 Google Inc. Nexus 4 (bootloader) Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 0 bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 64 idVendor 0x18d1 Google Inc. idProduct 0x4ee0 Nexus 4 (bootloader) bcdDevice 1.00 iManufacturer 1 Google iProduct 2 Android iSerial 3 034412a082919b5c bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 0x0020 bNumInterfaces 1 bConfigurationValue 1 iConfiguration 0 bmAttributes 0x80 (Bus Powered) MaxPower 500mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 2 bInterfaceClass 255 Vendor Specific Class bInterfaceSubClass 66 bInterfaceProtocol 3 iInterface 4 fastboot Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 2 Transfer Type Bulk Synch Type None Usage Type Data wMaxPacketSize 0x0200 1x 512 bytes bInterval 0 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x01 EP 1 OUT bmAttributes 2 Transfer Type Bulk Synch Type None Usage Type Data wMaxPacketSize 0x0200 1x 512 bytes bInterval 1 Device Status: 0x0000 (Bus Powered) fwupd-2.0.10/plugins/fastboot/data/qfil/000077500000000000000000000000001501337203100200605ustar00rootroot00000000000000fwupd-2.0.10/plugins/fastboot/data/qfil/partition_nand.xml000066400000000000000000000011751501337203100236170ustar00rootroot00000000000000 0xAA7D1B9A 0x1F7D48BC 0x4 0:SBL 0x8 0x2 0 0xFF 0x01 0x00 0xFE sbl1.mbn fwupd-2.0.10/plugins/fastboot/fastboot.quirk000066400000000000000000000016331501337203100211150ustar00rootroot00000000000000# Quectel EG25-G modem [USB\VID_18D1&PID_D00D] Plugin = fastboot FastbootBlockSize = 16384 FastbootOperationDelay = 250 Summary = Quectel EG25-G modem (fastboot) # Fibocom FM101 [USB\VID_2CB7&PID_D00D] Plugin = fastboot FastbootBlockSize = 16384 FastbootOperationDelay = 0 Summary = Fibocom FM101 Modem (fastboot) CounterpartGuid = USB\VID_2CB7&PID_01A2 # DW5821e [USB\VID_413C&PID_81D6] Plugin = fastboot Summary = Dell DW5821e LTE (fastboot) CounterpartGuid = USB\VID_413C&PID_81D7 # DW5821e/eSIM [USB\VID_413C&PID_81E1] Plugin = fastboot Summary = Dell DW5821e/eSIM LTE modem (fastboot) CounterpartGuid = USB\VID_413C&PID_81E0 # T77W968 [USB\VID_0489&PID_E0B7] Plugin = fastboot Summary = Foxconn T77w968 LTE modem (fastboot) CounterpartGuid = USB\VID_0489&PID_E0B4 # T77W968/eSIM [USB\VID_0489&PID_E0B8] Plugin = fastboot Summary = Foxconn T77w968/eSIM LTE modem (fastboot) CounterpartGuid = USB\VID_0489&PID_E0B5 fwupd-2.0.10/plugins/fastboot/fu-fastboot-device.c000066400000000000000000000505111501337203100220500ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-fastboot-device.h" #define FASTBOOT_REMOVE_DELAY_RE_ENUMERATE 60000 /* ms */ #define FASTBOOT_TRANSACTION_TIMEOUT 1000 /* ms */ #define FASTBOOT_TRANSACTION_RETRY_MAX 600 #define FASTBOOT_EP_IN 0x81 #define FASTBOOT_EP_OUT 0x01 #define FASTBOOT_CMD_BUFSZ 64 /* bytes */ struct _FuFastbootDevice { FuUsbDevice parent_instance; guint blocksz; guint operation_delay; }; G_DEFINE_TYPE(FuFastbootDevice, fu_fastboot_device, FU_TYPE_USB_DEVICE) static void fu_fastboot_device_to_string(FuDevice *device, guint idt, GString *str) { FuFastbootDevice *self = FU_FASTBOOT_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "BlockSize", self->blocksz); } static gboolean fu_fastboot_device_probe(FuDevice *device, GError **error) { FuFastbootDevice *self = FU_FASTBOOT_DEVICE(device); g_autoptr(FuUsbInterface) intf = NULL; /* find the correct fastboot interface */ intf = fu_usb_device_get_interface(FU_USB_DEVICE(self), 0xff, 0x42, 0x03, error); if (intf == NULL) return FALSE; fu_usb_device_add_interface(FU_USB_DEVICE(self), fu_usb_interface_get_number(intf)); return TRUE; } static gboolean fu_fastboot_device_write(FuDevice *device, const guint8 *buf, gsize buflen, GError **error) { FuFastbootDevice *self = FU_FASTBOOT_DEVICE(device); gboolean ret; gsize actual_len = 0; g_autofree guint8 *buf2 = NULL; /* make mutable */ fu_dump_raw(G_LOG_DOMAIN, "writing", buf, buflen); buf2 = fu_memdup_safe(buf, buflen, error); if (buf2 == NULL) return FALSE; ret = fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), FASTBOOT_EP_OUT, buf2, buflen, &actual_len, FASTBOOT_TRANSACTION_TIMEOUT, NULL, error); /* give device some time to handle action */ fu_device_sleep(device, self->operation_delay); if (!ret) { g_prefix_error(error, "failed to do bulk transfer: "); return FALSE; } if (actual_len != buflen) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "only wrote %" G_GSIZE_FORMAT "bytes", actual_len); return FALSE; } return TRUE; } static gboolean fu_fastboot_device_writestr(FuDevice *device, const gchar *str, GError **error) { gsize buflen = strlen(str); if (buflen > FASTBOOT_CMD_BUFSZ - 4) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "fastboot limits writes to %i bytes", FASTBOOT_CMD_BUFSZ - 4); return FALSE; } return fu_fastboot_device_write(device, (const guint8 *)str, buflen, error); } typedef enum { FU_FASTBOOT_DEVICE_READ_FLAG_NONE, FU_FASTBOOT_DEVICE_READ_FLAG_STATUS_POLL, } FuFastbootDeviceReadFlags; static gboolean fu_fastboot_device_read(FuDevice *device, gchar **str, FuProgress *progress, FuFastbootDeviceReadFlags flags, GError **error) { FuFastbootDevice *self = FU_FASTBOOT_DEVICE(device); guint retries = 1; /* these commands may return INFO or take some time to complete */ if (flags & FU_FASTBOOT_DEVICE_READ_FLAG_STATUS_POLL) retries = FASTBOOT_TRANSACTION_RETRY_MAX; for (guint i = 0; i < retries; i++) { gboolean ret; gsize actual_len = 0; guint8 buf[FASTBOOT_CMD_BUFSZ] = {0x00}; g_autofree gchar *tmp = NULL; g_autoptr(GError) error_local = NULL; ret = fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), FASTBOOT_EP_IN, buf, sizeof(buf), &actual_len, FASTBOOT_TRANSACTION_TIMEOUT, NULL, &error_local); /* give device some time to handle action */ fu_device_sleep(device, self->operation_delay); if (!ret) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT)) { g_debug("ignoring %s", error_local->message); continue; } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to do bulk transfer: "); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "read", buf, actual_len); if (actual_len < 4) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "only read %" G_GSIZE_FORMAT "bytes", actual_len); return FALSE; } /* info */ tmp = g_strndup((const gchar *)buf + 4, self->blocksz - 4); if (memcmp(buf, "INFO", 4) == 0) { if (g_strcmp0(tmp, "erasing flash") == 0) fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_ERASE); else if (g_strcmp0(tmp, "writing flash") == 0) fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); else g_debug("INFO returned unknown: %s", tmp); continue; } /* success */ if (memcmp(buf, "OKAY", 4) == 0 || memcmp(buf, "DATA", 4) == 0) { if (str != NULL) *str = g_steal_pointer(&tmp); return TRUE; } /* failure */ if (memcmp(buf, "FAIL", 4) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "failed to read response: %s", tmp); return FALSE; } /* unknown failure */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to read response"); return FALSE; } /* we timed out a *lot* */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "no response to read"); return FALSE; } static gboolean fu_fastboot_device_getvar(FuDevice *device, const gchar *key, gchar **str, GError **error) { g_autofree gchar *tmp = g_strdup_printf("getvar:%s", key); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); if (!fu_fastboot_device_writestr(device, tmp, error)) return FALSE; if (!fu_fastboot_device_read(device, str, progress, FU_FASTBOOT_DEVICE_READ_FLAG_NONE, error)) { g_prefix_error(error, "failed to getvar %s: ", key); return FALSE; } return TRUE; } static gboolean fu_fastboot_device_cmd(FuDevice *device, const gchar *cmd, FuProgress *progress, FuFastbootDeviceReadFlags flags, GError **error) { if (!fu_fastboot_device_writestr(device, cmd, error)) return FALSE; if (!fu_fastboot_device_read(device, NULL, progress, flags, error)) return FALSE; return TRUE; } static gboolean fu_fastboot_device_flash(FuDevice *device, const gchar *partition, FuProgress *progress, GError **error) { g_autofree gchar *tmp = g_strdup_printf("flash:%s", partition); return fu_fastboot_device_cmd(device, tmp, progress, FU_FASTBOOT_DEVICE_READ_FLAG_STATUS_POLL, error); } static gboolean fu_fastboot_device_download(FuDevice *device, GBytes *fw, FuProgress *progress, GError **error) { FuFastbootDevice *self = FU_FASTBOOT_DEVICE(device); gsize sz = g_bytes_get_size(fw); g_autofree gchar *tmp = g_strdup_printf("download:%08x", (guint)sz); g_autoptr(FuChunkArray) chunks = NULL; /* tell the client the size of data to expect */ if (!fu_fastboot_device_cmd(device, tmp, progress, FU_FASTBOOT_DEVICE_READ_FLAG_STATUS_POLL, error)) return FALSE; /* send the data in chunks */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); chunks = fu_chunk_array_new_from_bytes(fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, self->blocksz); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_fastboot_device_write(device, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; fu_progress_step_done(progress); } if (!fu_fastboot_device_read(device, NULL, progress, FU_FASTBOOT_DEVICE_READ_FLAG_STATUS_POLL, error)) return FALSE; return TRUE; } static gboolean fu_fastboot_device_setup(FuDevice *device, GError **error) { FuFastbootDevice *self = FU_FASTBOOT_DEVICE(device); g_autofree gchar *product = NULL; g_autofree gchar *serialno = NULL; g_autofree gchar *secure = NULL; g_autofree gchar *version_bootloader = NULL; /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_fastboot_device_parent_class)->setup(device, error)) return FALSE; /* product */ if (!fu_fastboot_device_getvar(device, "product", &product, error)) return FALSE; if (product != NULL && product[0] != '\0') { g_autofree gchar *tmp = g_strdup_printf("Fastboot %s", product); fu_device_set_name(device, tmp); } /* bootloader version */ if (!fu_fastboot_device_getvar(device, "version-bootloader", &version_bootloader, error)) return FALSE; if (version_bootloader != NULL && version_bootloader[0] != '\0') { fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_PAIR); fu_device_set_version_bootloader(device, version_bootloader); } /* serialno */ if (!fu_fastboot_device_getvar(device, "serialno", &serialno, error)) return FALSE; if (serialno != NULL && serialno[0] != '\0') fu_device_set_serial(device, serialno); /* secure */ if (!fu_fastboot_device_getvar(device, "secure", &secure, error)) return FALSE; if (secure != NULL && secure[0] != '\0') { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); } else { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); } /* success */ return TRUE; } static gboolean fu_fastboot_device_write_qfil_part(FuDevice *device, FuFirmware *firmware, XbNode *part, FuProgress *progress, GError **error) { GBytes *data; const gchar *fn; const gchar *partition; /* not all partitions have images */ fn = xb_node_query_text(part, "img_name", NULL); if (fn == NULL) return TRUE; /* find filename */ data = fu_firmware_get_image_by_id_bytes(firmware, fn, error); if (data == NULL) return FALSE; /* get the partition name */ partition = xb_node_query_text(part, "name", error); if (partition == NULL) return FALSE; if (g_str_has_prefix(partition, "0:")) partition += 2; /* flash the partition */ if (!fu_fastboot_device_download(device, data, progress, error)) return FALSE; return fu_fastboot_device_flash(device, partition, progress, error); } static gboolean fu_fastboot_device_write_motorola_part(FuDevice *device, FuFirmware *firmware, XbNode *part, FuProgress *progress, GError **error) { const gchar *op = xb_node_get_attr(part, "operation"); /* oem */ if (g_strcmp0(op, "oem") == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "OEM commands are not supported"); return FALSE; } /* getvar */ if (g_strcmp0(op, "getvar") == 0) { const gchar *var = xb_node_get_attr(part, "var"); g_autofree gchar *tmp = NULL; /* check required args */ if (var == NULL) { tmp = xb_node_export(part, XB_NODE_EXPORT_FLAG_NONE, NULL); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "required var for part: %s", tmp); return FALSE; } /* just has to be non-empty */ if (!fu_fastboot_device_getvar(device, var, &tmp, error)) return FALSE; if (tmp == NULL || tmp[0] == '\0') { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to getvar %s", var); return FALSE; } return TRUE; } /* erase */ if (g_strcmp0(op, "erase") == 0) { const gchar *partition = xb_node_get_attr(part, "partition"); g_autofree gchar *cmd = g_strdup_printf("erase:%s", partition); /* check required args */ if (partition == NULL) { g_autofree gchar *tmp = NULL; tmp = xb_node_export(part, XB_NODE_EXPORT_FLAG_NONE, NULL); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "required partition for part: %s", tmp); return FALSE; } /* erase the partition */ return fu_fastboot_device_cmd(device, cmd, progress, FU_FASTBOOT_DEVICE_READ_FLAG_NONE, error); } /* flash */ if (g_strcmp0(op, "flash") == 0) { GBytes *data; const gchar *filename = xb_node_get_attr(part, "filename"); const gchar *partition = xb_node_get_attr(part, "partition"); struct { GChecksumType kind; const gchar *str; } csum_kinds[] = {{G_CHECKSUM_MD5, "MD5"}, {G_CHECKSUM_SHA1, "SHA1"}, {G_CHECKSUM_SHA256, "SHA256"}, {0, NULL}}; /* check required args */ if (partition == NULL || filename == NULL) { g_autofree gchar *tmp = NULL; tmp = xb_node_export(part, XB_NODE_EXPORT_FLAG_NONE, NULL); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "required partition and filename: %s", tmp); return FALSE; } /* find filename */ data = fu_firmware_get_image_by_id_bytes(firmware, filename, error); if (data == NULL) return FALSE; /* checksum is optional */ for (guint i = 0; csum_kinds[i].str != NULL; i++) { const gchar *csum; g_autofree gchar *csum_actual = NULL; /* not provided */ csum = xb_node_get_attr(part, csum_kinds[i].str); if (csum == NULL) continue; /* check is valid */ csum_actual = g_compute_checksum_for_bytes(csum_kinds[i].kind, data); if (g_strcmp0(csum, csum_actual) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "%s invalid, expected %s, got %s", filename, csum, csum_actual); return FALSE; } } /* flash the partition */ if (!fu_fastboot_device_download(device, data, progress, error)) return FALSE; return fu_fastboot_device_flash(device, partition, progress, error); } /* dumb operation that doesn't expect a response */ if (g_strcmp0(op, "boot") == 0 || g_strcmp0(op, "continue") == 0 || g_strcmp0(op, "reboot") == 0 || g_strcmp0(op, "reboot-bootloader") == 0 || g_strcmp0(op, "powerdown") == 0) { return fu_fastboot_device_cmd(device, op, progress, FU_FASTBOOT_DEVICE_READ_FLAG_NONE, error); } /* unknown */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "unknown operation %s", op); return FALSE; } static gboolean fu_fastboot_device_write_motorola(FuDevice *device, FuFirmware *firmware, FuProgress *progress, GError **error) { GBytes *data; g_autoptr(GPtrArray) parts = NULL; g_autoptr(XbBuilder) builder = xb_builder_new(); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_autoptr(XbSilo) silo = NULL; /* load the manifest of operations */ data = fu_firmware_get_image_by_id_bytes(firmware, "flashfile.xml", error); if (data == NULL) return FALSE; if (!xb_builder_source_load_bytes(source, data, XB_BUILDER_SOURCE_FLAG_NONE, error)) { fwupd_error_convert(error); return FALSE; } xb_builder_import_source(builder, source); silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, error); if (silo == NULL) { fwupd_error_convert(error); return FALSE; } /* get all the operation parts */ parts = xb_silo_query(silo, "parts/part", 0, error); if (parts == NULL) { fwupd_error_convert(error); return FALSE; } fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, parts->len); for (guint i = 0; i < parts->len; i++) { XbNode *part = g_ptr_array_index(parts, i); if (!fu_fastboot_device_write_motorola_part(device, firmware, part, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_fastboot_device_write_qfil(FuDevice *device, FuFirmware *firmware, FuProgress *progress, GError **error) { GBytes *data; g_autoptr(GPtrArray) parts = NULL; g_autoptr(XbBuilder) builder = xb_builder_new(); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_autoptr(XbSilo) silo = NULL; /* load the manifest of operations */ data = fu_firmware_get_image_by_id_bytes(firmware, "partition_nand.xml", error); if (data == NULL) return FALSE; if (!xb_builder_source_load_bytes(source, data, XB_BUILDER_SOURCE_FLAG_NONE, error)) { fwupd_error_convert(error); return FALSE; } xb_builder_import_source(builder, source); silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, error); if (silo == NULL) { fwupd_error_convert(error); return FALSE; } /* get all the operation parts */ parts = xb_silo_query(silo, "nandboot/partitions/partition", 0, error); if (parts == NULL) { fwupd_error_convert(error); return FALSE; } fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, parts->len); for (guint i = 0; i < parts->len; i++) { XbNode *part = g_ptr_array_index(parts, i); if (!fu_fastboot_device_write_qfil_part(device, firmware, part, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_fastboot_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { g_autoptr(FuFirmware) manifest = NULL; /* load the manifest of operations */ manifest = fu_firmware_get_image_by_id(firmware, "partition_nand.xml", NULL); if (manifest != NULL) return fu_fastboot_device_write_qfil(device, firmware, progress, error); manifest = fu_firmware_get_image_by_id(firmware, "flashfile.xml", NULL); if (manifest != NULL) return fu_fastboot_device_write_motorola(device, firmware, progress, error); /* not supported */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "manifest not supported"); return FALSE; } static gboolean fu_fastboot_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuFastbootDevice *self = FU_FASTBOOT_DEVICE(device); guint64 tmp = 0; /* load from quirks */ if (g_strcmp0(key, "FastbootBlockSize") == 0) { if (!fu_strtoull(value, &tmp, 0x40, 0x100000, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->blocksz = tmp; return TRUE; } if (g_strcmp0(key, "FastbootOperationDelay") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXSIZE, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->operation_delay = tmp; return TRUE; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static gboolean fu_fastboot_device_attach(FuDevice *device, FuProgress *progress, GError **error) { if (!fu_fastboot_device_cmd(device, "reboot", progress, FU_FASTBOOT_DEVICE_READ_FLAG_NONE, error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static void fu_fastboot_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 97, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reload"); } static void fu_fastboot_device_init(FuFastbootDevice *self) { /* this is a safe default, even using USBv1 */ self->blocksz = 512; /* no delay is applied by default after a read or write operation */ self->operation_delay = 0; fu_device_add_protocol(FU_DEVICE(self), "com.google.fastboot"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_COUNTERPART_GUIDS); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_set_remove_delay(FU_DEVICE(self), FASTBOOT_REMOVE_DELAY_RE_ENUMERATE); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_ARCHIVE_FIRMWARE); fu_usb_device_set_claim_retry_count(FU_USB_DEVICE(self), 5); } static void fu_fastboot_device_class_init(FuFastbootDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_fastboot_device_probe; device_class->setup = fu_fastboot_device_setup; device_class->write_firmware = fu_fastboot_device_write_firmware; device_class->attach = fu_fastboot_device_attach; device_class->to_string = fu_fastboot_device_to_string; device_class->set_quirk_kv = fu_fastboot_device_set_quirk_kv; device_class->set_progress = fu_fastboot_device_set_progress; } fwupd-2.0.10/plugins/fastboot/fu-fastboot-device.h000066400000000000000000000004651501337203100220600ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_FASTBOOT_DEVICE (fu_fastboot_device_get_type()) G_DECLARE_FINAL_TYPE(FuFastbootDevice, fu_fastboot_device, FU, FASTBOOT_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/fastboot/fu-fastboot-plugin.c000066400000000000000000000016061501337203100221100ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-fastboot-device.h" #include "fu-fastboot-plugin.h" struct _FuFastbootPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuFastbootPlugin, fu_fastboot_plugin, FU_TYPE_PLUGIN) static void fu_fastboot_plugin_init(FuFastbootPlugin *self) { } static void fu_fastboot_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "FastbootBlockSize"); fu_context_add_quirk_key(ctx, "FastbootOperationDelay"); fu_plugin_add_device_gtype(plugin, FU_TYPE_FASTBOOT_DEVICE); } static void fu_fastboot_plugin_class_init(FuFastbootPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_fastboot_plugin_constructed; } fwupd-2.0.10/plugins/fastboot/fu-fastboot-plugin.h000066400000000000000000000003621501337203100221130ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuFastbootPlugin, fu_fastboot_plugin, FU, FASTBOOT_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/fastboot/meson.build000066400000000000000000000011151501337203100203540ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginFastboot"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('fastboot.quirk') plugin_builtins += static_library('fu_plugin_fastboot', sources: [ 'fu-fastboot-plugin.c', 'fu-fastboot-device.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) if not supported_build plugin_quirks += files('ci.quirk') enumeration_data += files('tests/fastboot-google-sargo-setup.json') device_tests += files('tests/fastboot-google-sargo.json') endif fwupd-2.0.10/plugins/fastboot/tests/000077500000000000000000000000001501337203100173565ustar00rootroot00000000000000fwupd-2.0.10/plugins/fastboot/tests/fastboot-google-sargo-setup.json000066400000000000000000000060171501337203100256170ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUsbDevice", "PlatformId": "5-1.3", "Created": "2024-10-25T10:44:56.796837Z", "IdVendor": 6353, "IdProduct": 20192, "Device": 256, "USB": 512, "Manufacturer": 1, "Product": 2, "SerialNumber": 3, "UsbConfigDescriptors": [ { "ConfigurationValue": 1 } ], "UsbInterfaces": [ { "Length": 9, "DescriptorType": 4, "InterfaceClass": 255, "InterfaceSubClass": 66, "InterfaceProtocol": 3, "Interface": 4, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 129, "MaxPacketSize": 512 }, { "DescriptorType": 5, "EndpointAddress": 1, "Interval": 1, "MaxPacketSize": 512 } ] } ], "UsbEvents": [ { "Id": "#d5a801ad", "Data": "usb" }, { "Id": "#ad8c58d3", "Data": "usb" }, { "Id": "#1075ed5c", "Data": "usb_device" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=534\nDEVNAME=bus/usb/005/023\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=18d1/4ee0/100\nTYPE=0/0/0\nBUSNUM=005\nDEVNUM=023" }, { "Id": "#d432c663", "Data": "bus/usb/005/023" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c", "Data": "usb_device" }, { "Id": "#4693935e", "Data": "005" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=534\nDEVNAME=bus/usb/005/023\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=18d1/4ee0/100\nTYPE=0/0/0\nBUSNUM=005\nDEVNUM=023" }, { "Id": "#1ab3ae0a", "Data": "023" }, { "Id": "#028c3a0e", "Data": "OTdYQVkxMkJHTQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" }, { "Id": "#bb126633", "Data": "Z2V0dmFyOnByb2R1Y3Q=" }, { "Id": "#deac3674", "Data": "T0tBWXNhcmdv" }, { "Id": "#5ba8514a", "Data": "Z2V0dmFyOnZlcnNpb24tYm9vdGxvYWRlcg==" }, { "Id": "#deac3674", "Data": "T0tBWWI0czQtMC40LTgwNDg2ODk=" }, { "Id": "#a5c5177c", "Data": "Z2V0dmFyOnNlcmlhbG5v" }, { "Id": "#deac3674", "Data": "T0tBWTk3WEFZMTJCR00=" }, { "Id": "#a4275b28", "Data": "Z2V0dmFyOnNlY3VyZQ==" }, { "Id": "#deac3674", "Data": "T0tBWXllcw==" } ] } ] } fwupd-2.0.10/plugins/fastboot/tests/fastboot-google-sargo.json000066400000000000000000000006041501337203100244550ustar00rootroot00000000000000{ "name": "Google Sargo Fastboot", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/fastboot-google-sargo-setup.json", "components": [ { "version": "1.0", "version-bootloader": "b4s4-0.4-8048689", "guids": [ "4980b2e3-f8f0-5434-8a41-031e35500953" ] } ] } ] } fwupd-2.0.10/plugins/flashrom/000077500000000000000000000000001501337203100162065ustar00rootroot00000000000000fwupd-2.0.10/plugins/flashrom/README.md000066400000000000000000000052711501337203100174720ustar00rootroot00000000000000--- title: Plugin: Flashrom --- ## Introduction This plugin uses `libflashrom` to update the system firmware. It can be used to update BIOS or ME regions of the flash. Device for ME region is created only if "Intel SPI" plugin indicates that such a region exists, which makes "Intel SPI" a dependency of this plugin for doing ME updates. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in an unspecified binary file format, which is typically the raw input for an EEPROM programmer. This plugin supports the following protocol ID: * `org.flashrom` ## Coreboot Version String The coreboot version string can have an optional prefix (see below). After the optional prefix the *major*, *minor* string follows and finally the *build string*, containing the exact commit and repository state, follows. For example `4.10-989-gc8a4e4b9c5-dirty` ### Exception on Lenovo devices The thinkpad_acpi kernel module requires a specific pattern in the DMI version string. To satisfy those requirements coreboot adds the CBETxxxx prefix to the DMI version string on all Lenovo devices. For example `CBET4000 4.10-989-gc8a4e4b9c5-dirty` The coreboot DMI version string always starts with `CBET`. ## GUID Generation Internal device uses hardware ID values which are derived from SMBIOS. * HardwareID-3 * HardwareID-4 * HardwareID-5 * HardwareID-6 * HardwareID-10 They should match the values provided by `fwupdtool hwids` or the `ComputerHardwareIds.exe` Windows utility. One more GUID has the following form: * `FLASHROM\VENDOR_{manufacturer}&PRODUCT_{product}®ION_{ifd_region_name}` Its purpose is to target specific regions of the flash as defined by IFD (Intel SPI Flash Descriptor), examples: * `FLASHROM\VENDOR_Notebook&PRODUCT_NS50MU®ION_BIOS` * `FLASHROM\VENDOR_Notebook&PRODUCT_NS50MU®ION_ME` ## Update Behavior The firmware is deployed to the SPI chip when the machine is in normal runtime mode, but it is only used when the device is rebooted. ## Quirk Use This plugin uses the following plugin-specific quirks: ### `Flags=reset-cmos` Flag to determine if the CMOS checksum should be reset after the flash is reprogrammed. This will force the CMOS defaults to be reloaded on the next boot. ### `Flags=fn-m-me-unlock` Flag to determine if manual ME unlocking by pressing Fn + M is supported. ## Vendor ID Security The vendor ID is set from the BIOS vendor, for example `DMI:Google` ## External Interface Access This plugin requires access to all interfaces that `libflashrom` has been compiled for. This typically is `/sys/bus/spi` but there may be other interfaces as well. ## Version Considerations This plugin has been available since fwupd version `1.1.2`. fwupd-2.0.10/plugins/flashrom/flashrom.quirk000066400000000000000000000065001501337203100210770ustar00rootroot00000000000000# raspberrypi+brcm,bcm2712 HWID [abe2c65a-0295-5818-a254-2ec5379881c5] Plugin = flashrom FlashromProgrammer = linux_spi FlashromArgs = dev=/dev/spidev10.0,spispeed=16000 # Raspberry Pi 5 Model B Rev 1.0 (HwId) [909e4cd3-bdf0-581d-955c-f7709d9fb316] Vendor = Raspberry Pi VendorId = DMI:raspberrypi VersionFormat = number # Purism [a0ce5085-2dea-5086-ae72-45810a186ad0] Plugin = flashrom # Libretrend [52b68c34-6b31-5ecc-8a5c-de37e666ccd5] Plugin = flashrom VersionFormat = quad # Star Labs generic (HwId - coreboot) [b8cf5af6-8a46-5deb-ac01-a35b1ea5fb48] Plugin = flashrom # Star Labs generic (HwID - AMI) [29230df7-d1e5-5c38-859b-229e966b22e9] Plugin = flashrom # StarLabTop Mk III (coreboot GUID) [d33219e2-b84c-53a8-a624-27af9752dba6] Branch = coreboot Flags = reset-cmos PciBcrAddr = 0x0 # StarLite Mk II (coreboot GUID) [2993474c-b4c4-5b7c-b2e1-a471d8d328a5] Branch = coreboot Flags = reset-cmos PciBcrAddr = 0x0 # StarLite Mk II (AMI GUID) [0676539d-150f-5f07-89b9-fe0afd98c44e] FirmwareSizeMax = 0x800000 # StarLite Mk III (coreboot GUID) [3d2f164a-8818-58fd-a082-6c60a67e21a6] Branch = coreboot Flags = reset-cmos PciBcrAddr = 0x0 # StarLite Mk III (AMI GUID) [ec375a72-9ed9-5a21-b1da-5e7f00dcada1] FirmwareSizeMax = 0x800000 # StarLite Mk IV (coreboot GUID) [5dc1dd5b-761e-5146-8ac2-1fdd8445f2ff] Branch = coreboot Flags = reset-cmos PciBcrAddr = 0x0 # StarLite Mk IV (AMI GUID) [32edd806-13a0-5b0f-a8e9-656a0e147369] FirmwareSizeMax = 0x800000 # StarLabTop Mk IV (coreboot GUID) [0ee5867c-93f0-5fb4-adf1-9d686ea1183a] Branch = coreboot Flags = reset-cmos PciBcrAddr = 0x0 # StarBook Mk V (coreboot GUID) [54c96fef-31e7-5011-a3ff-ea8e855d9acd] Branch = coreboot Flags = reset-cmos PciBcrAddr = 0x0 # StarBook Mk VI - Intel (AMI GUID) [1292e166-a66f-5e11-b2bb-53265a8f53d9] FirmwareSizeMax = 0x2000000 # StarBook Mk VI - Intel (coreboot GUID) [8c994a92-7ef8-5d68-80b5-99ead7cf4686] Branch = coreboot Flags = reset-cmos PciBcrAddr = 0x0 FirmwareSizeMax = 0x2000000 # StarBook Mk VIr2 - Intel (AMI GUID) [ce35649d-b89d-5188-b961-d992ce7e0f88] FirmwareSizeMax = 0x2000000 # StarBook Mk VIr2 - Intel (coreboot GUID) [595c5861-a105-509b-8dd6-f77070345286] Branch = coreboot Flags = reset-cmos PciBcrAddr = 0x0 FirmwareSizeMax = 0x2000000 # NovaCustom NV4x (HwId) [25b6ea34-8f52-598e-a27a-31e03014dbe3] Plugin = flashrom [59caeff5-6a3c-595b-aab1-56500d0fecbc] Plugin = flashrom # NovaCustom NV4x (Dasharo GUID) [9a8c30e2-1b7e-5b28-9632-fc2fbf8cd0ba] Branch = dasharo VersionFormat = triplet [41a8fb3d-213a-5a8a-b0f4-e2a7c55aaf80] Branch = dasharo VersionFormat = triplet # NovaCustom NS5x (HwId) [bab4f96c-34e4-5b92-a785-4e4489b1c395] Plugin = flashrom # NovaCustom NS5x (Dasharo GUID) [43455705-4c7d-5440-b285-8362b67a5743] Branch = dasharo VersionFormat = triplet # TUXEDO InfinityBook S 14 Gen6 (HwId) [9366407e-433e-51e9-bd79-a517e3f31536] Plugin = flashrom [FLASHROM\GUID_9366407e-433e-51e9-bd79-a517e3f31536] Flags = fn-m-me-unlock FirmwareSizeMax = 0x1000000 # TUXEDO InfinityBook S 15 Gen6 (HwId) [16cabee1-8d79-5164-9400-346dddbfe2c5] Plugin = flashrom [FLASHROM\GUID_16cabee1-8d79-5164-9400-346dddbfe2c5] Flags = fn-m-me-unlock FirmwareSizeMax = 0x1000000 # TUXEDO InfinityBook Pro 13 v3 (HwId) [caaa8a77-aca3-595b-92fa-4e6fb7bccc82] Plugin = flashrom [FLASHROM\GUID_caaa8a77-aca3-595b-92fa-4e6fb7bccc82] Flags = fn-m-me-unlock FirmwareSizeMax = 0x800000 fwupd-2.0.10/plugins/flashrom/fu-flashrom-cmos.c000066400000000000000000000026041501337203100215360ustar00rootroot00000000000000/* * Copyright 2021 Sean Rhodes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #ifdef HAVE_IO_H #include #endif #include "fu-flashrom-cmos.h" #ifdef HAVE_IO_H static gboolean fu_flashrom_cmos_write(guint8 addr, guint8 val) { guint8 tmp; /* Reject addresses in the second bank */ if (addr >= 128) return FALSE; /* Write the value to CMOS */ outb(addr, RTC_BASE_PORT); outb(val, RTC_BASE_PORT + 1); /* Read the value back from CMOS */ outb(addr, RTC_BASE_PORT); tmp = inb(RTC_BASE_PORT + 1); /* Check the read value against the written */ return (tmp == val); } #endif gboolean fu_flashrom_cmos_reset(GError **error) { #ifdef HAVE_IO_H /* Call ioperm() to grant us access to ports 0x70 and 0x71 */ if (ioperm(RTC_BASE_PORT, 2, TRUE) < 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_READ, "failed to gain access to ports 0x70 and 0x71"); return FALSE; } /* Write a default value to the CMOS checksum */ if ((!fu_flashrom_cmos_write(CMOS_CHECKSUM_OFFSET, 0xff)) || (!fu_flashrom_cmos_write(CMOS_CHECKSUM_OFFSET + 1, 0xff))) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_READ, "failed to reset CMOS"); return FALSE; } /* success */ return TRUE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no support"); return FALSE; #endif } fwupd-2.0.10/plugins/flashrom/fu-flashrom-cmos.h000066400000000000000000000007341501337203100215450ustar00rootroot00000000000000/* * Copyright 2021 Sean Rhodes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include /* From coreboot's src/include/pc80/mc146818rtc.h file */ #define RTC_BASE_PORT 0x70 /* * This is the offset of the first of the two checksum bytes * we may want to figure out how we can determine this dynamically * during execution. */ #define CMOS_CHECKSUM_OFFSET 123 gboolean fu_flashrom_cmos_reset(GError **error); fwupd-2.0.10/plugins/flashrom/fu-flashrom-device.c000066400000000000000000000325421501337203100220400ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * Copyright 2021 Daniel Campello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-flashrom-cmos.h" #include "fu-flashrom-device.h" #define FU_FLASHROM_DEVICE_FLAG_RESET_CMOS "reset-cmos" #define FU_FLASHROM_DEVICE_FLAG_FN_M_ME_UNLOCK "fn-m-me-unlock" struct _FuFlashromDevice { FuUdevDevice parent_instance; FuIfdRegion region; struct flashrom_flashctx *flashctx; struct flashrom_layout *layout; }; G_DEFINE_TYPE(FuFlashromDevice, fu_flashrom_device, FU_TYPE_UDEV_DEVICE) enum { PROP_0, PROP_FLASHCTX, PROP_REGION, PROP_LAST }; static gboolean fu_flashrom_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { if (g_strcmp0(key, "PciBcrAddr") == 0) { guint64 tmp = 0; if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_device_set_metadata_integer(device, "PciBcrAddr", tmp); return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no supported"); return FALSE; } static gboolean fu_flashrom_device_probe(FuDevice *device, GError **error) { g_autofree gchar *dev_name = NULL; const gchar *sysfs_path = NULL; sysfs_path = fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device)); if (sysfs_path != NULL) { g_autofree gchar *physical_id = NULL; physical_id = g_strdup_printf("DEVNAME=%s", sysfs_path); fu_device_set_physical_id(device, physical_id); } dev_name = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(device), "name", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (dev_name != NULL) fu_device_add_instance_id_full(device, dev_name, FU_DEVICE_INSTANCE_FLAG_QUIRKS); return TRUE; } static gboolean fu_flashrom_device_open(FuDevice *device, GError **error) { FuFlashromDevice *self = FU_FLASHROM_DEVICE(device); /* sanity check */ if (self->flashctx == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no flashctx"); return FALSE; } /* get the flash size from the device if not already been quirked */ if (fu_device_get_firmware_size_max(device) == 0) { gsize flash_size = flashrom_flash_getsize(self->flashctx); if (flash_size == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "flash size zero"); return FALSE; } fu_device_set_firmware_size_max(device, flash_size); } /* update only one specific region of the flash and do not touch others */ if (fu_cpu_get_vendor() == FU_CPU_VENDOR_INTEL) { struct flashrom_layout *layout = NULL; if (flashrom_layout_read_from_ifd(&layout, self->flashctx, NULL, 0)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_READ, "failed to read layout from Intel ICH descriptor"); return FALSE; } if (flashrom_layout_include_region(layout, fu_ifd_region_to_string(self->region))) { flashrom_layout_release(layout); g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid region name"); return FALSE; } /* does not transfer ownership, so we must manage the lifetime of layout */ self->layout = layout; flashrom_layout_set(self->flashctx, self->layout); } return TRUE; } static gboolean fu_flashrom_device_close(FuDevice *device, GError **error) { FuFlashromDevice *self = FU_FLASHROM_DEVICE(device); if (self->layout != NULL) { flashrom_layout_release(self->layout); self->layout = NULL; flashrom_layout_set(self->flashctx, NULL); } return TRUE; } #ifdef HAVE_FLASHROM_SET_PROGRESS_CALLBACK_V2 static void fu_flashrom_device_progress_cb(enum flashrom_progress_stage stage, size_t current, size_t total, void *user_data) { FuProgress *progress = FU_PROGRESS(user_data); /* status */ if (stage == FLASHROM_PROGRESS_READ) fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ); else if (stage == FLASHROM_PROGRESS_WRITE) fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); else if (stage == FLASHROM_PROGRESS_ERASE) fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_ERASE); /* progress */ fu_progress_set_percentage_full(progress, current, total); } #endif static GBytes * fu_flashrom_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuFlashromDevice *self = FU_FLASHROM_DEVICE(device); gint rc; gsize bufsz = fu_device_get_firmware_size_max(device); g_autofree guint8 *buf = g_malloc0(bufsz); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ); #ifdef HAVE_FLASHROM_SET_PROGRESS_CALLBACK_V2 flashrom_set_progress_callback_v2(self->flashctx, fu_flashrom_device_progress_cb, progress); #endif rc = flashrom_image_read(self->flashctx, buf, bufsz); #ifdef HAVE_FLASHROM_SET_PROGRESS_CALLBACK_V2 flashrom_set_progress_callback_v2(self->flashctx, NULL, NULL); #endif if (rc != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "failed to read flash [%i]", rc); return NULL; } return g_bytes_new_take(g_steal_pointer(&buf), bufsz); } static gboolean fu_flashrom_device_prepare(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { gboolean exists_orig = FALSE; g_autofree gchar *firmware_orig = NULL; g_autofree gchar *localstatedir = NULL; g_autofree gchar *basename = NULL; /* if the original firmware doesn't exist, grab it now */ basename = g_strdup_printf("flashrom-%s.bin", fu_device_get_id(device)); localstatedir = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR_PKG); firmware_orig = g_build_filename(localstatedir, "builder", basename, NULL); if (!fu_path_mkdir_parent(firmware_orig, error)) return FALSE; if (!fu_device_query_file_exists(device, firmware_orig, &exists_orig, error)) return FALSE; if (!exists_orig) { g_autoptr(GBytes) buf = NULL; buf = fu_flashrom_device_dump_firmware(device, progress, error); if (buf == NULL) { g_prefix_error(error, "failed to back up original firmware: "); return FALSE; } if (!fu_bytes_set_contents(firmware_orig, buf, error)) return FALSE; } return TRUE; } static gboolean fu_flashrom_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuFlashromDevice *self = FU_FLASHROM_DEVICE(device); gsize sz = 0; gint rc; const guint8 *buf; g_autoptr(GBytes) blob_fw = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 90, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 10, NULL); /* read early */ blob_fw = fu_firmware_get_bytes(firmware, error); if (blob_fw == NULL) return FALSE; buf = g_bytes_get_data(blob_fw, &sz); /* write region */ if (sz != fu_device_get_firmware_size_max(device)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid image size 0x%x, expected 0x%x", (guint)sz, (guint)fu_device_get_firmware_size_max(device)); return FALSE; } #ifdef HAVE_FLASHROM_SET_PROGRESS_CALLBACK_V2 flashrom_set_progress_callback_v2(self->flashctx, fu_flashrom_device_progress_cb, progress); #endif rc = flashrom_image_write(self->flashctx, (void *)buf, sz, NULL /* refbuffer */); #ifdef HAVE_FLASHROM_SET_PROGRESS_CALLBACK_V2 flashrom_set_progress_callback_v2(self->flashctx, NULL, NULL); #endif if (rc != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "image write failed, err=%i", rc); return FALSE; } fu_progress_step_done(progress); if (flashrom_image_verify(self->flashctx, (void *)buf, sz)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "image verify failed"); return FALSE; } fu_progress_step_done(progress); /* Check if CMOS needs a reset */ if (fu_device_has_private_flag(device, FU_FLASHROM_DEVICE_FLAG_RESET_CMOS)) { g_debug("attempting CMOS reset"); if (!fu_flashrom_cmos_reset(error)) { g_prefix_error(error, "failed CMOS reset: "); return FALSE; } } /* success */ return TRUE; } static void fu_flashrom_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_flashrom_device_init(FuFlashromDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_add_protocol(FU_DEVICE(self), "org.flashrom"); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ENSURE_SEMVER); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_SIGNED); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_FLAGS); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE); fu_device_set_physical_id(FU_DEVICE(self), "flashrom"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PAIR); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_COMPUTER); fu_device_register_private_flag(FU_DEVICE(self), FU_FLASHROM_DEVICE_FLAG_RESET_CMOS); fu_device_register_private_flag(FU_DEVICE(self), FU_FLASHROM_DEVICE_FLAG_FN_M_ME_UNLOCK); } static void fu_flashrom_device_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuFlashromDevice *self = FU_FLASHROM_DEVICE(object); switch (prop_id) { case PROP_FLASHCTX: g_value_set_pointer(value, self->flashctx); break; case PROP_REGION: g_value_set_uint(value, self->region); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_flashrom_device_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuFlashromDevice *self = FU_FLASHROM_DEVICE(object); switch (prop_id) { case PROP_FLASHCTX: self->flashctx = g_value_get_pointer(value); break; case PROP_REGION: self->region = g_value_get_uint(value); fu_device_set_logical_id(FU_DEVICE(self), fu_ifd_region_to_string(self->region)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_flashrom_device_finalize(GObject *object) { FuFlashromDevice *self = FU_FLASHROM_DEVICE(object); if (self->layout != NULL) flashrom_layout_release(self->layout); G_OBJECT_CLASS(fu_flashrom_device_parent_class)->finalize(object); } static void fu_flashrom_device_class_init(FuFlashromDeviceClass *klass) { GParamSpec *pspec; GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->get_property = fu_flashrom_device_get_property; object_class->set_property = fu_flashrom_device_set_property; /** * FuFlashromDevice:region: * * The IFD region that's being managed. */ pspec = g_param_spec_uint("region", NULL, NULL, 0, G_MAXUINT, 0, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_REGION, pspec); /** * FuFlashromDevice:flashctx: * * The JSON root member for the device. */ pspec = g_param_spec_pointer("flashctx", NULL, NULL, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_FLASHCTX, pspec); object_class->finalize = fu_flashrom_device_finalize; device_class->set_quirk_kv = fu_flashrom_device_set_quirk_kv; device_class->probe = fu_flashrom_device_probe; device_class->open = fu_flashrom_device_open; device_class->close = fu_flashrom_device_close; device_class->set_progress = fu_flashrom_device_set_progress; device_class->prepare = fu_flashrom_device_prepare; device_class->dump_firmware = fu_flashrom_device_dump_firmware; device_class->write_firmware = fu_flashrom_device_write_firmware; } FuDevice * fu_flashrom_device_new(FuContext *ctx, struct flashrom_flashctx *flashctx, FuIfdRegion region) { return FU_DEVICE(g_object_new(FU_TYPE_FLASHROM_DEVICE, "context", ctx, "flashctx", flashctx, "region", region, NULL)); } gboolean fu_flashrom_device_unlock(FuFlashromDevice *self, GError **error) { if (self->region == FU_IFD_REGION_ME && fu_device_has_private_flag(FU_DEVICE(self), FU_FLASHROM_DEVICE_FLAG_FN_M_ME_UNLOCK)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "\n" "ME region should be unlocked manually the following way:\n" " 1. Power off your device\n" " 2. Press and keep holding Fn + M during the next step\n" " 3. Press power on button"); return FALSE; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Unlocking of device %s is not supported", fu_device_get_name(FU_DEVICE(self))); return FALSE; } fwupd-2.0.10/plugins/flashrom/fu-flashrom-device.h000066400000000000000000000011041501337203100220330ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * Copyright 2021 Daniel Campello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_FLASHROM_DEVICE (fu_flashrom_device_get_type()) G_DECLARE_FINAL_TYPE(FuFlashromDevice, fu_flashrom_device, FU, FLASHROM_DEVICE, FuUdevDevice) struct flashrom_flashctx; FuDevice * fu_flashrom_device_new(FuContext *ctx, struct flashrom_flashctx *flashctx, FuIfdRegion region); gboolean fu_flashrom_device_unlock(FuFlashromDevice *self, GError **error); fwupd-2.0.10/plugins/flashrom/fu-flashrom-plugin.c000066400000000000000000000305611501337203100220760ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * Copyright 2019 9elements Agency GmbH * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fu-flashrom-device.h" #define SELFCHECK_TRUE 1 struct FuPluginData { struct flashrom_flashctx *flashctx; struct flashrom_programmer *flashprog; gchar *guid; /* GUID from quirks that activated this plugin */ }; typedef FuPluginData FuFlashromPlugin; #define FU_FLASHROM_PLUGIN(o) fu_plugin_get_data(FU_PLUGIN(o)) static void fu_flashrom_plugin_to_string(FuPlugin *plugin, guint idt, GString *str) { FuFlashromPlugin *self = FU_FLASHROM_PLUGIN(plugin); fwupd_codec_string_append(str, idt, "Guid", self->guid); } static int fu_flashrom_plugin_debug_cb(enum flashrom_log_level lvl, const char *fmt, va_list args) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wformat-nonliteral" g_autofree gchar *tmp = g_strdup_vprintf(fmt, args); #pragma clang diagnostic pop g_autofree gchar *str = fu_strstrip(tmp); if (g_strcmp0(str, "OK.") == 0 || g_strcmp0(str, ".") == 0) return 0; switch (lvl) { case FLASHROM_MSG_ERROR: case FLASHROM_MSG_WARN: g_warning("%s", str); break; case FLASHROM_MSG_INFO: g_info("%s", str); break; case FLASHROM_MSG_DEBUG: case FLASHROM_MSG_DEBUG2: g_debug("%s", str); break; case FLASHROM_MSG_SPEW: default: break; } return 0; } static void fu_flashrom_plugin_device_set_version(FuPlugin *plugin, FuDevice *device) { FuContext *ctx = fu_plugin_get_context(plugin); const gchar *version; const gchar *version_major; const gchar *version_minor; /* as-is */ version = fu_context_get_hwid_value(ctx, FU_HWIDS_KEY_BIOS_VERSION); if (version != NULL) { /* some Lenovo hardware requires a specific prefix for the EC, * so strip it before we use ensure-semver */ if (strlen(version) > 9 && g_str_has_prefix(version, "CBET")) version += 9; /* this may not "stick" if there are no numeric chars */ fu_device_set_version(device, version); if (fu_device_get_version(device) != NULL) return; } /* component parts only */ version_major = fu_context_get_hwid_value(ctx, FU_HWIDS_KEY_BIOS_MAJOR_RELEASE); version_minor = fu_context_get_hwid_value(ctx, FU_HWIDS_KEY_BIOS_MINOR_RELEASE); if (version_major != NULL && version_minor != NULL) { g_autofree gchar *tmp = g_strdup_printf("%s.%s.0", version_major, version_minor); fu_device_set_version(device, tmp); return; } } static gboolean fu_flashrom_plugin_device_set_bios_info(FuPlugin *plugin, FuDevice *device, GError **error) { FuContext *ctx = fu_plugin_get_context(plugin); GBytes *bios_blob; const guint8 *buf; gsize bufsz; guint32 bios_char = 0x0; g_autoptr(GPtrArray) bios_tables = NULL; /* get SMBIOS info */ bios_tables = fu_context_get_smbios_data(ctx, FU_SMBIOS_STRUCTURE_TYPE_BIOS, FU_SMBIOS_STRUCTURE_LENGTH_ANY, error); if (bios_tables == NULL) return FALSE; /* Get SMBIOS data */ bios_blob = g_ptr_array_index(bios_tables, 0); buf = g_bytes_get_data(bios_blob, &bufsz); if (bufsz == 0) return FALSE; /* BIOS characteristics */ if (fu_memread_uint32_safe(buf, bufsz, 0xa, &bios_char, G_LITTLE_ENDIAN, NULL)) { if ((bios_char & (1 << 11)) == 0) { fu_device_inhibit(device, "bios-characteristics", "Not supported from SMBIOS"); } } /* ROM size if not already been quirked */ if (fu_device_get_firmware_size_max(device) == 0) { guint8 bios_sz = 0x0; guint64 firmware_size = 0x0; if (!fu_memread_uint8_safe(buf, bufsz, 0x9, &bios_sz, NULL)) return FALSE; /* need to read extended ROM size */ if (bios_sz == 0xff) { guint16 bios_sz_ext = 0x0; /* Bits 15-14 scale, 13-0 size * 00 scale -> MiB * 01 scale -> GiB * others reserved */ if (!fu_memread_uint16_safe(buf, bufsz, 0x18, &bios_sz_ext, G_LITTLE_ENDIAN, error)) return FALSE; firmware_size = (bios_sz_ext & 0x3ff) * (1024 * 1024); if (bios_sz_ext & 0xc000) firmware_size *= 1024; } else { firmware_size = (bios_sz + 1) * 64 * 1024; } fu_device_set_firmware_size_max(device, firmware_size); } return TRUE; } static void fu_flashrom_plugin_device_set_hwids(FuPlugin *plugin, FuDevice *device) { FuContext *ctx = fu_plugin_get_context(plugin); static const gchar *chids[] = { "HardwareID-03", "HardwareID-04", "HardwareID-05", "HardwareID-06", "HardwareID-10", "fwupd-04", /* for coreboot */ "fwupd-05", /* for coreboot */ }; /* don't include FU_HWIDS_KEY_BIOS_VERSION */ for (guint i = 0; i < G_N_ELEMENTS(chids); i++) { g_autofree gchar *str = NULL; str = fu_context_get_hwid_replace_value(ctx, chids[i], NULL); if (str != NULL) fu_device_add_instance_id(device, str); } } static FuDevice * fu_flashrom_plugin_add_device(FuPlugin *plugin, const gchar *guid, FuIfdRegion region, GError **error) { FuContext *ctx = fu_plugin_get_context(plugin); FuFlashromPlugin *self = FU_FLASHROM_PLUGIN(plugin); const gchar *dmi_vendor; const gchar *product = fu_context_get_hwid_value(ctx, FU_HWIDS_KEY_PRODUCT_NAME); const gchar *vendor = fu_context_get_hwid_value(ctx, FU_HWIDS_KEY_MANUFACTURER); const gchar *region_str = fu_ifd_region_to_string(region); g_autofree gchar *name = g_strdup_printf("%s (%s)", product, region_str); g_autoptr(FuDevice) device = fu_flashrom_device_new(ctx, self->flashctx, region); g_autoptr(GError) error_local = NULL; fu_device_set_name(device, name); fu_device_set_vendor(device, vendor); fu_device_add_instance_str(device, "VENDOR", vendor); fu_device_add_instance_str(device, "PRODUCT", product); fu_device_add_instance_strup(device, "REGION", region_str); if (!fu_device_build_instance_id(device, error, "FLASHROM", "VENDOR", "PRODUCT", "REGION", NULL)) return NULL; /* add this so we can attach board-specific quirks */ fu_device_add_instance_str(FU_DEVICE(device), "GUID", guid); if (!fu_device_build_instance_id(FU_DEVICE(device), error, "FLASHROM", "GUID", NULL)) return NULL; /* use same VendorID logic as with UEFI */ dmi_vendor = fu_context_get_hwid_value(ctx, FU_HWIDS_KEY_BIOS_VENDOR); fu_device_build_vendor_id(FU_DEVICE(device), "DMI", dmi_vendor); fu_flashrom_plugin_device_set_hwids(plugin, device); fu_flashrom_plugin_device_set_version(plugin, device); if (!fu_flashrom_plugin_device_set_bios_info(plugin, device, &error_local)) g_debug("failed to set bios info: %s", error_local->message); if (!fu_device_setup(device, error)) return NULL; /* BCR is almost-never used on coreboot because SMI is evil */ if (g_strcmp0(dmi_vendor, "coreboot") == 0 && fu_device_get_metadata(device, "PciBcrAddr") == NULL) fu_device_set_metadata_integer(device, "PciBcrAddr", 0x0); /* success */ fu_plugin_device_add(plugin, device); return g_steal_pointer(&device); } static void fu_flashrom_plugin_device_registered(FuPlugin *plugin, FuDevice *device) { g_autoptr(FuDevice) me_device = NULL; FuFlashromPlugin *self = FU_FLASHROM_PLUGIN(plugin); const gchar *me_region_str = fu_ifd_region_to_string(FU_IFD_REGION_ME); /* we're only interested in a device from intel-spi plugin that corresponds to ME * region of IFD */ if (g_strcmp0(fu_device_get_plugin(device), "intel_spi") != 0) return; if (g_strcmp0(fu_device_get_logical_id(device), me_region_str) != 0) return; me_device = fu_flashrom_plugin_add_device(plugin, self->guid, FU_IFD_REGION_ME, NULL); if (me_device == NULL) return; /* unlock operation requires device to be locked */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_LOCKED)) fu_device_add_flag(me_device, FWUPD_DEVICE_FLAG_LOCKED); } static gboolean fu_flashrom_plugin_coldplug(FuPlugin *plugin, FuProgress *progress, GError **error) { FuFlashromPlugin *self = FU_FLASHROM_PLUGIN(plugin); g_autoptr(FuDevice) device = fu_flashrom_plugin_add_device(plugin, self->guid, FU_IFD_REGION_BIOS, error); return (device != NULL); } /* finds GUID that activated this plugin */ static const gchar * fu_flashrom_plugin_find_guid(FuPlugin *plugin, GError **error) { FuContext *ctx = fu_plugin_get_context(plugin); GPtrArray *hwids = fu_context_get_hwid_guids(ctx); /* any coreboot */ if (g_strcmp0(fu_context_get_hwid_value(ctx, FU_HWIDS_KEY_BIOS_VENDOR), "coreboot") == 0) return g_strdup(FWUPD_DEVICE_ID_ANY); for (guint i = 0; i < hwids->len; i++) { const gchar *guid = g_ptr_array_index(hwids, i); const gchar *plugin_name = fu_context_lookup_quirk_by_id(ctx, guid, FU_QUIRKS_PLUGIN); if (g_strcmp0(plugin_name, "flashrom") == 0) return guid; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no HwIDs found"); return NULL; } static gboolean fu_flashrom_plugin_startup(FuPlugin *plugin, FuProgress *progress, GError **error) { const gchar *flashrom_args; const gchar *flashrom_prog; gint rc; const gchar *guid; FuContext *ctx = fu_plugin_get_context(plugin); FuFlashromPlugin *self = FU_FLASHROM_PLUGIN(plugin); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 5, "find-guid"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 90, "init"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 5, "probe"); guid = fu_flashrom_plugin_find_guid(plugin, error); if (guid == NULL) return FALSE; fu_progress_step_done(progress); /* if changed */ if (g_strcmp0(self->guid, guid) != 0) { g_free(self->guid); self->guid = g_strdup(guid); } if (flashrom_init(SELFCHECK_TRUE)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "flashrom initialization error"); return FALSE; } flashrom_set_log_callback(fu_flashrom_plugin_debug_cb); fu_progress_step_done(progress); /* allow overriding from quirk file */ flashrom_prog = fu_context_lookup_quirk_by_id(ctx, guid, "FlashromProgrammer"); if (flashrom_prog == NULL) flashrom_prog = "internal"; flashrom_args = fu_context_lookup_quirk_by_id(ctx, guid, "FlashromArgs"); g_debug("using programmer %s: %s", flashrom_prog, flashrom_args); if (flashrom_programmer_init(&self->flashprog, flashrom_prog, flashrom_args)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "programmer initialization failed"); return FALSE; } rc = flashrom_flash_probe(&self->flashctx, self->flashprog, NULL); if (rc == 3) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "flash probe failed: multiple chips were found"); return FALSE; } if (rc == 2) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "flash probe failed: no chip was found"); return FALSE; } if (rc != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "flash probe failed: unknown error"); return FALSE; } fu_progress_step_done(progress); return TRUE; } static gboolean fu_flashrom_plugin_unlock(FuPlugin *self, FuDevice *device, GError **error) { return fu_flashrom_device_unlock(FU_FLASHROM_DEVICE(device), error); } static void fu_flashrom_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); (void)fu_plugin_alloc_data(plugin, sizeof(FuFlashromPlugin)); fu_plugin_add_rule(plugin, FU_PLUGIN_RULE_METADATA_SOURCE, "linux_lockdown"); fu_plugin_add_rule(plugin, FU_PLUGIN_RULE_CONFLICTS, "coreboot"); /* obsoleted */ fu_plugin_add_flag(plugin, FWUPD_PLUGIN_FLAG_REQUIRE_HWID); fu_plugin_add_flag(plugin, FWUPD_PLUGIN_FLAG_MEASURE_SYSTEM_INTEGRITY); fu_plugin_add_device_gtype(plugin, FU_TYPE_FLASHROM_DEVICE); /* coverage */ } static void fu_flashrom_plugin_finalize(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuFlashromPlugin *self = FU_FLASHROM_PLUGIN(plugin); if (self->flashctx != NULL) flashrom_flash_release(self->flashctx); if (self->flashprog != NULL) flashrom_programmer_shutdown(self->flashprog); g_free(self->guid); /* G_OBJECT_CLASS(fu_flashrom_plugin_parent_class)->finalize() not required as modular */ } void fu_plugin_init_vfuncs(FuPluginVfuncs *vfuncs) { vfuncs->constructed = fu_flashrom_plugin_constructed; vfuncs->finalize = fu_flashrom_plugin_finalize; vfuncs->to_string = fu_flashrom_plugin_to_string; vfuncs->device_registered = fu_flashrom_plugin_device_registered; vfuncs->startup = fu_flashrom_plugin_startup; vfuncs->coldplug = fu_flashrom_plugin_coldplug; vfuncs->unlock = fu_flashrom_plugin_unlock; } fwupd-2.0.10/plugins/flashrom/meson.build000066400000000000000000000011061501337203100203460ustar00rootroot00000000000000if allow_flashrom cargs = ['-DG_LOG_DOMAIN="FuPluginFlashrom"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('flashrom.quirk') shared_module('fu_plugin_flashrom', sources: [ 'fu-flashrom-device.c', 'fu-flashrom-plugin.c', 'fu-flashrom-cmos.c', ], include_directories: plugin_incdirs, install: true, install_rpath: libdir_pkg, install_dir: libdir_pkg, link_with: plugin_libs, c_args: [ cargs, '-DLOCALSTATEDIR="' + localstatedir + '"', ], dependencies: [ plugin_deps, libflashrom, ], ) endif fwupd-2.0.10/plugins/focalfp/000077500000000000000000000000001501337203100160055ustar00rootroot00000000000000fwupd-2.0.10/plugins/focalfp/README.md000066400000000000000000000030061501337203100172630ustar00rootroot00000000000000--- title: Plugin: Focal TouchPad --- ## Introduction This plugin allows updating Touchpad devices from Focal. Devices are enumerated using HID . The I²C mode is used for firmware recovery. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in an unspecified binary file format. This plugin supports the following protocol ID: * `tw.com.focalfp` ## GUID Generation These device uses the standard DeviceInstanceId values, e.g. * `HIDRAW\VEN_2808&DEV_0106` ## Update Behavior The device usually presents in HID mode, and the firmware is written to the device by switching to a IAP mode where the touchpad is nonfunctional. Once complete the device is reset to get out of IAP mode and to load the new firmware version. On flash failure the device is nonfunctional, but is recoverable by writing to the i2c device. This is typically much slower than updating the device using HID and also requires a model-specific HWID quirk to match. ## Vendor ID Security The vendor ID is set from the HID vendor, for example set to `HIDRAW:0x17EF` ## Quirk Use This plugin uses the following plugin-specific quirks: ## External Interface Access This plugin requires ioctl access to `HIDIOCSFEATURE` and `HIDIOCGFEATURE`. ## Version Considerations This plugin has been available since fwupd version `1.8.6`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Wayne Huang: @waynehuang2022 fwupd-2.0.10/plugins/focalfp/focalfp.quirk000066400000000000000000000001401501337203100204670ustar00rootroot00000000000000[HIDRAW\VEN_2808&DEV_0106] Plugin = focalfp GType = FuFocalfpHidDevice Flags = enforce-requires fwupd-2.0.10/plugins/focalfp/fu-focalfp-firmware.c000066400000000000000000000046261501337203100220150ustar00rootroot00000000000000/* * Copyright 2022 Shihwei Huang * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-focalfp-firmware.h" struct _FuFocalfpFirmware { FuFirmwareClass parent_instance; guint16 start_address; guint32 checksum; }; G_DEFINE_TYPE(FuFocalfpFirmware, fu_focalfp_firmware, FU_TYPE_FIRMWARE) /* firmware block update */ #define FOCAL_NAME_START_ADDR_WRDS 0x011E const guint8 focalfp_signature[] = {0xFF}; guint32 fu_focalfp_firmware_get_checksum(FuFocalfpFirmware *self) { g_return_val_if_fail(FU_IS_FOCALFP_FIRMWARE(self), 0); return self->checksum; } static void fu_focalfp_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuFocalfpFirmware *self = FU_FOCALFP_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "start_address", self->start_address); fu_xmlb_builder_insert_kx(bn, "checksum", self->checksum); } static gboolean fu_focalfp_firmware_compute_checksum_cb(const guint8 *buf, gsize bufsz, gpointer user_data, GError **error) { guint32 *value = (guint32 *)user_data; for (guint32 i = 0; i < bufsz; i += 4) { guint32 tmp = 0; if (!fu_memread_uint32_safe(buf, bufsz, i, &tmp, G_LITTLE_ENDIAN, error)) return FALSE; *value ^= tmp; } return TRUE; } static gboolean fu_focalfp_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuFocalfpFirmware *self = FU_FOCALFP_FIRMWARE(firmware); /* start address */ if (!fu_input_stream_read_u16(stream, FOCAL_NAME_START_ADDR_WRDS, &self->start_address, G_BIG_ENDIAN, error)) { return FALSE; } if (self->start_address != 0x582e) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "force pad address invalid: 0x%x", self->start_address); return FALSE; } /* calculate checksum */ if (!fu_input_stream_chunkify(stream, fu_focalfp_firmware_compute_checksum_cb, &self->checksum, error)) return FALSE; self->checksum += 1; /* success */ return TRUE; } static void fu_focalfp_firmware_init(FuFocalfpFirmware *self) { } static void fu_focalfp_firmware_class_init(FuFocalfpFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_focalfp_firmware_parse; firmware_class->export = fu_focalfp_firmware_export; } fwupd-2.0.10/plugins/focalfp/fu-focalfp-firmware.h000066400000000000000000000006201501337203100220100ustar00rootroot00000000000000/* * Copyright 2022 Shihwei Huang * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_FOCALFP_FIRMWARE (fu_focalfp_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuFocalfpFirmware, fu_focalfp_firmware, FU, FOCALFP_FIRMWARE, FuFirmware) guint32 fu_focalfp_firmware_get_checksum(FuFocalfpFirmware *self); fwupd-2.0.10/plugins/focalfp/fu-focalfp-hid-device.c000066400000000000000000000452561501337203100222060ustar00rootroot00000000000000/* * Copyright 2022 Shihwei Huang * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-focalfp-firmware.h" #include "fu-focalfp-hid-device.h" struct _FuFocalfpHidDevice { FuHidrawDevice parent_instance; }; G_DEFINE_TYPE(FuFocalfpHidDevice, fu_focalfp_hid_device, FU_TYPE_HIDRAW_DEVICE) #define CMD_ENTER_UPGRADE_MODE 0x40 #define CMD_CHECK_CURRENT_STATE 0x41 #define CMD_READY_FOR_UPGRADE 0x42 #define CMD_SEND_DATA 0x43 #define CMD_UPGRADE_CHECKSUM 0x44 #define CMD_EXIT_UPGRADE_MODE 0x45 #define CMD_USB_READ_UPGRADE_ID 0x46 #define CMD_USB_ERASE_FLASH 0x47 #define CMD_USB_BOOT_READ 0x48 #define CMD_USB_BOOT_BOOTLOADERVERSION 0x49 #define CMD_READ_REGISTER 0x50 #define CMD_WRITE_REGISTER 0x51 #define CMD_ACK 0xf0 #define CMD_NACK 0xff #define FIRST_PACKET 0x00 #define MID_PACKET 0x01 #define END_PACKET 0x02 #define REPORT_SIZE 64 #define MAX_USB_PACKET_SIZE 56 static gboolean fu_focalfp_hid_device_detach(FuDevice *device, FuProgress *progress, GError **error); static gboolean fu_focalfp_hid_device_probe(FuDevice *device, GError **error) { /* check is valid */ if (g_strcmp0(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device)), "hidraw") != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "is not correct subsystem=%s, expected hidraw", fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device))); return FALSE; } /* i2c-hid */ if (fu_device_get_pid(device) != 0x0106) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not i2c-hid touchpad"); return FALSE; } /* success */ return TRUE; } static guint8 fu_focalfp_hid_device_generate_checksum(const guint8 *buf, gsize bufsz) { guint8 checksum = 0; for (gsize i = 0; i < bufsz; i++) checksum ^= buf[i]; checksum++; return checksum; } static gboolean fu_focalfp_hid_device_io(FuFocalfpHidDevice *self, guint8 *wbuf, gsize wbufsz, guint8 *rbuf, gsize rbufsz, GError **error) { /* SetReport */ if (wbuf != NULL && wbufsz > 0) { guint8 buf[64] = {0x06, 0xff, 0xff}; guint8 cmdlen = 4 + wbufsz; buf[3] = cmdlen; if (!fu_memcpy_safe(buf, sizeof(buf), 0x04, wbuf, wbufsz, 0x00, wbufsz, error)) return FALSE; buf[cmdlen] = fu_focalfp_hid_device_generate_checksum(&buf[1], cmdlen - 1); if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), buf, sizeof(buf), FU_IOCTL_FLAG_NONE, error)) { return FALSE; } } /* GetReport */ if (rbuf != NULL && rbufsz > 0) { guint8 buf[64] = {0x06}; if (!fu_hidraw_device_get_feature(FU_HIDRAW_DEVICE(self), buf, sizeof(buf), FU_IOCTL_FLAG_NONE, error)) { return FALSE; } if (!fu_memcpy_safe(rbuf, rbufsz, 0x0, buf, sizeof(buf), 0x00, rbufsz, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_focalfp_hid_device_check_cmd_crc(const guint8 *buf, gsize bufsz, guint8 cmd, GError **error) { guint8 csum = 0; guint8 csum_actual; /* check was correct response */ if (buf[4] != cmd) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "got cmd 0x%02x, expected 0x%02x", buf[4], cmd); return FALSE; } /* check crc */ if (!fu_memread_uint8_safe(buf, bufsz, buf[3], &csum, error)) return FALSE; csum_actual = fu_focalfp_hid_device_generate_checksum(buf + 1, buf[3] - 1); if (csum != csum_actual) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "got checksum 0x%02x, expected 0x%02x", csum, csum_actual); return FALSE; } /* success */ return TRUE; } static gboolean fu_focalfp_hid_device_read_reg_cb(FuDevice *device, gpointer user_data, GError **error) { FuFocalfpHidDevice *self = FU_FOCALFP_HID_DEVICE(device); guint8 buf[64] = {0x0}; guint8 *val = (guint8 *)user_data; if (!fu_focalfp_hid_device_io(self, NULL, 0, buf, 8, error)) return FALSE; /* check was correct response */ if (!fu_focalfp_hid_device_check_cmd_crc(buf, sizeof(buf), CMD_READ_REGISTER, error)) return FALSE; /* success */ *val = buf[6]; return TRUE; } static gboolean fu_focalfp_hid_device_read_reg(FuFocalfpHidDevice *self, guint8 reg_address, guint8 *val, /* out */ GError **error) { guint8 buf[64] = {CMD_READ_REGISTER, reg_address}; /* write */ if (!fu_focalfp_hid_device_io(self, buf, 2, NULL, 0, error)) return FALSE; /* read */ return fu_device_retry_full(FU_DEVICE(self), fu_focalfp_hid_device_read_reg_cb, 5, 1 /* ms */, val, error); } /* enter upgrade mode */ static gboolean fu_focalfp_hid_device_enter_upgrade_mode(FuFocalfpHidDevice *self, GError **error) { guint8 wbuf[64] = {CMD_ENTER_UPGRADE_MODE}; guint8 rbuf[64] = {0x0}; if (!fu_focalfp_hid_device_io(self, wbuf, 1, rbuf, 6, error)) { g_prefix_error(error, "failed to CMD_ENTER_UPGRADE_MODE: "); return FALSE; } /* check was correct response */ return fu_focalfp_hid_device_check_cmd_crc(rbuf, sizeof(rbuf), CMD_ACK, error); } /* get bootloader current state */ static gboolean fu_focalfp_hid_device_check_current_state(FuFocalfpHidDevice *self, guint8 *val, GError **error) { guint8 wbuf[64] = {CMD_CHECK_CURRENT_STATE}; guint8 rbuf[64] = {0x0}; if (!fu_focalfp_hid_device_io(self, wbuf, 1, rbuf, 7, error)) return FALSE; /* check was correct response */ if (!fu_focalfp_hid_device_check_cmd_crc(rbuf, sizeof(rbuf), CMD_CHECK_CURRENT_STATE, error)) return FALSE; /* success */ *val = rbuf[5]; return TRUE; } static gboolean fu_focalfp_hid_device_wait_for_upgrade_ready_cb(FuDevice *device, gpointer user_data, GError **error) { FuFocalfpHidDevice *self = FU_FOCALFP_HID_DEVICE(device); guint8 wbuf[64] = {CMD_READY_FOR_UPGRADE}; guint8 rbuf[64] = {0x0}; if (!fu_focalfp_hid_device_io(self, wbuf, 1, rbuf, 7, error)) return FALSE; /* check was correct response */ return fu_focalfp_hid_device_check_cmd_crc(rbuf, sizeof(rbuf), CMD_READY_FOR_UPGRADE, error); } /* wait for ready */ static gboolean fu_focalfp_hid_device_wait_for_upgrade_ready(FuFocalfpHidDevice *self, guint retries, GError **error) { return fu_device_retry_full(FU_DEVICE(self), fu_focalfp_hid_device_wait_for_upgrade_ready_cb, retries, 500, NULL, error); } static gboolean fu_focalfp_hid_device_read_update_id_cb(FuDevice *device, gpointer user_data, GError **error) { FuFocalfpHidDevice *self = FU_FOCALFP_HID_DEVICE(device); guint16 *us_ic_id = (guint16 *)user_data; guint8 wbuf[64] = {CMD_USB_READ_UPGRADE_ID}; guint8 rbuf[64] = {0x0}; if (!fu_focalfp_hid_device_io(self, wbuf, 1, rbuf, 8, error)) return FALSE; /* check was correct response */ if (!fu_focalfp_hid_device_check_cmd_crc(rbuf, sizeof(rbuf), CMD_USB_READ_UPGRADE_ID, error)) return FALSE; /* success */ *us_ic_id = fu_memread_uint16(rbuf + 5, G_BIG_ENDIAN); return TRUE; } /* get bootload id */ static gboolean fu_focalfp_hid_device_read_update_id(FuFocalfpHidDevice *self, guint16 *us_ic_id, GError **error) { return fu_device_retry_full(FU_DEVICE(self), fu_focalfp_hid_device_read_update_id_cb, 10, 1 /* ms */, us_ic_id, error); } /* erase flash */ static gboolean fu_focalfp_hid_device_erase_flash(FuFocalfpHidDevice *self, GError **error) { guint8 wbuf[64] = {CMD_USB_ERASE_FLASH}; guint8 rbuf[64] = {0x0}; if (!fu_focalfp_hid_device_io(self, wbuf, 1, rbuf, 6, error)) return FALSE; /* check was correct response */ return fu_focalfp_hid_device_check_cmd_crc(rbuf, sizeof(rbuf), CMD_ACK, error); } static gboolean fu_focalfp_hid_device_send_data_cb(FuDevice *device, gpointer user_data, GError **error) { FuFocalfpHidDevice *self = FU_FOCALFP_HID_DEVICE(device); guint8 rbuf[64] = {0x0}; if (!fu_focalfp_hid_device_io(self, NULL, 0, rbuf, 7, error)) return FALSE; /* check was correct response */ return fu_focalfp_hid_device_check_cmd_crc(rbuf, sizeof(rbuf), CMD_ACK, error); } /* send write data */ static gboolean fu_focalfp_hid_device_send_data(FuFocalfpHidDevice *self, guint8 packet_type, const guint8 *buf, guint8 bufsz, GError **error) { guint8 wbuf[64] = {CMD_SEND_DATA, packet_type}; /* sanity check */ if (bufsz > REPORT_SIZE - 8) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "data length 0x%x invalid", bufsz); return FALSE; } if (!fu_memcpy_safe(wbuf, sizeof(wbuf), 0x02, buf, bufsz, 0x00, bufsz, error)) return FALSE; if (!fu_focalfp_hid_device_io(self, wbuf, bufsz + 2, NULL, 0, error)) return FALSE; return fu_device_retry_full(FU_DEVICE(self), fu_focalfp_hid_device_send_data_cb, 4, 1 /* ms */, NULL, error); } /* get checksum for write done */ static gboolean fu_focalfp_hid_device_checksum_upgrade(FuFocalfpHidDevice *self, guint32 *val, GError **error) { guint8 wbuf[64] = {CMD_UPGRADE_CHECKSUM}; guint8 rbuf[64] = {0x0}; if (!fu_focalfp_hid_device_io(self, wbuf, 1, rbuf, 7 + 3, error)) return FALSE; /* check was correct response */ if (!fu_focalfp_hid_device_check_cmd_crc(rbuf, sizeof(rbuf), CMD_UPGRADE_CHECKSUM, error)) return FALSE; /* success */ return fu_memread_uint32_safe(rbuf, sizeof(rbuf), 0x05, val, G_LITTLE_ENDIAN, error); } static gboolean fu_focalfp_hid_device_setup(FuDevice *device, GError **error) { FuFocalfpHidDevice *self = FU_FOCALFP_HID_DEVICE(device); guint8 buf[2] = {0x0}; /* get current firmware version */ if (!fu_focalfp_hid_device_read_reg(self, 0xA6, buf, error)) { g_prefix_error(error, "failed to read version1: "); return FALSE; } if (!fu_focalfp_hid_device_read_reg(self, 0xAD, buf + 1, error)) { g_prefix_error(error, "failed to read version2: "); return FALSE; } fu_device_set_version_raw(device, fu_memread_uint16(buf, G_BIG_ENDIAN)); /* success */ return TRUE; } static gboolean fu_focalfp_hid_device_write_chunks(FuFocalfpHidDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; guint8 uc_packet_type = MID_PACKET; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (i == 0) uc_packet_type = FIRST_PACKET; else if (i == fu_chunk_array_length(chunks) - 1) uc_packet_type = END_PACKET; if (!fu_focalfp_hid_device_send_data(self, uc_packet_type, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "failed to write chunk %u: ", i); return FALSE; } if (!fu_focalfp_hid_device_wait_for_upgrade_ready(self, 100, error)) { g_prefix_error(error, "failed to wait for chunk %u: ", i); return FALSE; } fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_focalfp_hid_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuFocalfpHidDevice *self = FU_FOCALFP_HID_DEVICE(device); const guint32 UPGRADE_ID = 0x582E; guint16 us_ic_id = 0; guint32 checksum = 0; g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 89, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 89, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 10, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 5, "reset"); /* simple image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* check chip id and erase flash */ if (!fu_focalfp_hid_device_wait_for_upgrade_ready(self, 6, error)) return FALSE; if (!fu_focalfp_hid_device_read_update_id(self, &us_ic_id, error)) return FALSE; if (us_ic_id != UPGRADE_ID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "got us_ic_id 0x%02x, expected 0x%02x", us_ic_id, (guint)UPGRADE_ID); return FALSE; } if (!fu_focalfp_hid_device_erase_flash(self, error)) return FALSE; fu_device_sleep(device, 1000); if (!fu_focalfp_hid_device_wait_for_upgrade_ready(self, 20, error)) return FALSE; fu_progress_step_done(progress); /* send packet data */ chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, MAX_USB_PACKET_SIZE, error); if (chunks == NULL) return FALSE; if (!fu_focalfp_hid_device_write_chunks(self, chunks, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* write flash end and check ready (fw calculate checksum) */ fu_device_sleep(device, 50); if (!fu_focalfp_hid_device_wait_for_upgrade_ready(self, 5, error)) return FALSE; fu_progress_step_done(progress); /* verify checksum */ if (!fu_focalfp_hid_device_checksum_upgrade(self, &checksum, error)) return FALSE; if (checksum != fu_focalfp_firmware_get_checksum(FU_FOCALFP_FIRMWARE(firmware))) { fu_device_sleep(device, 500); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "device checksum invalid, got 0x%02x, expected 0x%02x", checksum, fu_focalfp_firmware_get_checksum(FU_FOCALFP_FIRMWARE(firmware))); return FALSE; } fu_progress_step_done(progress); /* success */ return TRUE; } /* called after attach, but only when the firmware has been updated */ static gboolean fu_focalfp_hid_device_reload(FuDevice *device, GError **error) { FuFocalfpHidDevice *self = FU_FOCALFP_HID_DEVICE(device); guint8 idbuf[2] = {0x0}; fu_device_sleep(device, 500); if (!fu_focalfp_hid_device_read_reg(self, 0x9F, &idbuf[0], error)) return FALSE; if (!fu_focalfp_hid_device_read_reg(self, 0xA3, &idbuf[1], error)) return FALSE; g_debug("id1=%x, id2=%x", idbuf[1], idbuf[0]); if (idbuf[1] != 0x58 && idbuf[0] != 0x22) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "firmware id invalid, got 0x%02x:0x%02x, expected 0x%02x:0x%02x", idbuf[1], idbuf[0], (guint)0x58, (guint)0x22); return FALSE; } return fu_focalfp_hid_device_setup(device, error); } static gboolean fu_focalfp_hid_device_detach_cb(FuDevice *device, gpointer user_data, GError **error) { FuFocalfpHidDevice *self = FU_FOCALFP_HID_DEVICE(device); guint8 uc_mode = 0; if (!fu_focalfp_hid_device_enter_upgrade_mode(self, error)) { g_prefix_error(error, "failed to enter upgrade mode: "); return FALSE; } /* get current state */ if (!fu_focalfp_hid_device_check_current_state(self, &uc_mode, error)) return FALSE; /* 1: upgrade mode; 2: fw mode */ if (uc_mode != 1) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "got uc_mode 0x%02x, expected 0x%02x", uc_mode, (guint)1); return FALSE; } /* success */ return TRUE; } /* enter upgrade mode */ static gboolean fu_focalfp_hid_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuFocalfpHidDevice *self = FU_FOCALFP_HID_DEVICE(device); guint8 wbuf[64] = {CMD_ENTER_UPGRADE_MODE}; guint8 rbuf[64] = {0x0}; /* command to go from APP --> Bootloader -- but we do not check crc */ if (!fu_focalfp_hid_device_io(self, wbuf, 1, rbuf, 6, error)) { g_prefix_error(error, "failed to CMD_ENTER_UPGRADE_MODE: "); return FALSE; } fu_device_sleep(device, 200); /* second command : bootloader normal mode --> bootloader upgrade mode */ if (!fu_device_retry_full(device, fu_focalfp_hid_device_detach_cb, 3, 200 /* ms */, progress, error)) return FALSE; /* success */ fu_device_sleep(device, 200); return TRUE; } /* exit upgrade mode */ static gboolean fu_focalfp_hid_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuFocalfpHidDevice *self = FU_FOCALFP_HID_DEVICE(device); guint8 wbuf[64] = {CMD_EXIT_UPGRADE_MODE}; guint8 rbuf[64] = {0x0}; if (!fu_focalfp_hid_device_io(self, wbuf, 1, rbuf, 6, error)) return FALSE; /* check was correct response */ if (!fu_focalfp_hid_device_check_cmd_crc(rbuf, sizeof(rbuf), CMD_ACK, error)) return FALSE; /* success */ fu_device_sleep(device, 500); return TRUE; } static void fu_focalfp_hid_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 99, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static gchar * fu_focalfp_hid_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint16(version_raw, fu_device_get_version_format(device)); } static void fu_focalfp_hid_device_init(FuFocalfpHidDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_set_firmware_size(FU_DEVICE(self), 0x1E000); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_FOCALFP_FIRMWARE); fu_device_set_summary(FU_DEVICE(self), "Forcepad"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_INPUT_TOUCHPAD); fu_device_add_protocol(FU_DEVICE(self), "tw.com.focalfp"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_HEX); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_NONBLOCK); } static void fu_focalfp_hid_device_class_init(FuFocalfpHidDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->attach = fu_focalfp_hid_device_attach; device_class->detach = fu_focalfp_hid_device_detach; device_class->setup = fu_focalfp_hid_device_setup; device_class->reload = fu_focalfp_hid_device_reload; device_class->write_firmware = fu_focalfp_hid_device_write_firmware; device_class->probe = fu_focalfp_hid_device_probe; device_class->set_progress = fu_focalfp_hid_device_set_progress; device_class->convert_version = fu_focalfp_hid_device_convert_version; } fwupd-2.0.10/plugins/focalfp/fu-focalfp-hid-device.h000066400000000000000000000005651501337203100222050ustar00rootroot00000000000000/* * Copyright 2022 Shihwei Huang * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_FOCALFP_HID_DEVICE (fu_focalfp_hid_device_get_type()) G_DECLARE_FINAL_TYPE(FuFocalfpHidDevice, fu_focalfp_hid_device, FU, FOCALFP_HID_DEVICE, FuHidrawDevice) fwupd-2.0.10/plugins/focalfp/fu-focalfp-plugin.c000066400000000000000000000016161501337203100214730ustar00rootroot00000000000000/* * Copyright 2022 Shihwei Huang * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-focalfp-firmware.h" #include "fu-focalfp-hid-device.h" #include "fu-focalfp-plugin.h" struct _FuFocalfpPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuFocalfpPlugin, fu_focalfp_plugin, FU_TYPE_PLUGIN) static void fu_focalfp_plugin_init(FuFocalfpPlugin *self) { } static void fu_focalfp_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_udev_subsystem(plugin, "hidraw"); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_FOCALFP_FIRMWARE); fu_plugin_add_device_gtype(plugin, FU_TYPE_FOCALFP_HID_DEVICE); } static void fu_focalfp_plugin_class_init(FuFocalfpPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_focalfp_plugin_constructed; } fwupd-2.0.10/plugins/focalfp/fu-focalfp-plugin.h000066400000000000000000000003571501337203100215010ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuFocalfpPlugin, fu_focalfp_plugin, FU, FOCALFP_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/focalfp/meson.build000066400000000000000000000007311501337203100201500ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginFocalfp"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('focalfp.quirk') plugin_builtins += static_library('fu_plugin_focalfp', sources: [ 'fu-focalfp-plugin.c', 'fu-focalfp-firmware.c', 'fu-focalfp-hid-device.c', ], include_directories: plugin_incdirs, c_args: [ cargs, ], link_with: plugin_libs, dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/fpc/000077500000000000000000000000001501337203100151435ustar00rootroot00000000000000fwupd-2.0.10/plugins/fpc/README.md000066400000000000000000000024701501337203100164250ustar00rootroot00000000000000--- title: Plugin: FPC Fingerprint Sensor --- ## Introduction The plugin used for update firmware for fingerprint sensors from FPC. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a packed binary file format. This plugin supports the following protocol ID: * `com.fingerprints.dfupc` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_10A5&PID_FFE0` ## Update Behavior The firmware is deployed when the device is in normal runtime mode, and the device will reset when the new firmware has been written. ## Quirk Use This plugin uses the following plugin-specific quirks: ### `Flags=moh-device` Device is a MOH device ### `Flags=rts` Device is a RTS device. ### `Flags=legacy-dfu` Device supports legacy DFU mode. ### `Flags=lenfy` Device is a LENFY MOH device. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x10A5` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.8.6`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Jim Zhang: @jimzhang2 fwupd-2.0.10/plugins/fpc/fpc.quirk000066400000000000000000000004651501337203100167750ustar00rootroot00000000000000# FPC Fingerprint sensor [USB\VID_10A5&PID_FFE0] Plugin = fpc Flags = enforce-requires [USB\VID_10A5&PID_FFE1] Plugin = fpc Flags = moh-device,rts,enforce-requires [USB\VID_10A5&PID_9800] Plugin = fpc Flags = moh-device,rts,lenfy,enforce-requires [USB\VID_10A5&PID_D805] Plugin = fpc Flags = enforce-requires fwupd-2.0.10/plugins/fpc/fu-fpc-device.c000066400000000000000000000466421501337203100177400ustar00rootroot00000000000000/* * Copyright 2022 Haowei Lo * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-fpc-device.h" #include "fu-fpc-ff2-firmware.h" #include "fu-fpc-struct.h" #define FPC_USB_INTERFACE 0 #define FPC_USB_TRANSFER_TIMEOUT 1500 /* ms */ #define FPC_FLASH_BLOCK_SIZE_DEFAULT 2048 /* 2048 */ #define FPC_FLASH_BLOCK_SIZE_4096 4096 /* 4096 */ #define FPC_CMD_DFU_DETACH 0x00 #define FPC_CMD_DFU_DNLOAD 0x01 #define FPC_CMD_DFU_GETSTATUS 0x03 #define FPC_CMD_DFU_CLRSTATUS 0x04 #define FPC_CMD_DFU_GET_FW_STATUS 0x09 #define FPC_CMD_DFU_DNLOAD_FF2 0x10 #define FPC_CMD_BOOT0 0x04 #define FPC_CMD_GET_STATE 0x0B #define FPC_CMD_GET_STATE_LENFY 0x50 #define FPC_DEVICE_MOC_STATE_LEN 68 #define FPC_DEVICE_MOH_STATE_LEN 72 #define FPC_DEVICE_DFU_FW_STATUS_LEN 8 #define FPC_DFU_MAX_ATTEMPTS 50 #define FPC_DEVICE_DFU_MODE_CLASS 0xFE #define FPC_DEVICE_DFU_MODE_PORT 0x02 #define FPC_DEVICE_NORMAL_MODE_CLASS 0xFF #define FPC_DEVICE_NORMAL_MODE_PORT 0xFF #define FPC_FF2_BLK_SEC_LINK_LEN 100 #define FU_FPC_DEVICE_FLAG_MOH_DEVICE "moh-device" #define FU_FPC_DEVICE_FLAG_LEGACY_DFU "legacy-dfu" #define FU_FPC_DEVICE_FLAG_RTS_DEVICE "rts" #define FU_FPC_DEVICE_FLAG_LENFY_DEVICE "lenfy" struct _FuFpcDevice { FuUsbDevice parent_instance; guint32 max_block_size; }; G_DEFINE_TYPE(FuFpcDevice, fu_fpc_device, FU_TYPE_USB_DEVICE) static FuFirmware * fu_fpc_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { return fu_firmware_new_from_gtypes(stream, 0x0, flags, error, FU_TYPE_FPC_FF2_FIRMWARE, FU_TYPE_FIRMWARE, G_TYPE_INVALID); } static gboolean fu_fpc_device_dfu_cmd(FuFpcDevice *self, guint8 request, guint16 value, guint8 *data, gsize length, gboolean device2host, gboolean type_vendor, GError **error) { gsize actual_len = 0; if (!fu_usb_device_control_transfer( FU_USB_DEVICE(self), device2host ? FU_USB_DIRECTION_DEVICE_TO_HOST : FU_USB_DIRECTION_HOST_TO_DEVICE, type_vendor ? FU_USB_REQUEST_TYPE_VENDOR : FU_USB_REQUEST_TYPE_CLASS, FU_USB_RECIPIENT_INTERFACE, request, value, 0x0000, data, length, length ? &actual_len : NULL, FPC_USB_TRANSFER_TIMEOUT, NULL, error)) { fu_error_convert(error); return FALSE; } if (actual_len != length) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "only sent 0x%04x of 0x%04x", (guint)actual_len, (guint)length); return FALSE; } return TRUE; } static gboolean fu_fpc_device_fw_cmd(FuFpcDevice *self, guint8 request, guint8 *data, gsize length, gboolean device2host, GError **error) { gsize actual_len = 0; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), device2host ? FU_USB_DIRECTION_DEVICE_TO_HOST : FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, request, 0x0000, 0x0000, data, length, length ? &actual_len : NULL, FPC_USB_TRANSFER_TIMEOUT, NULL, error)) { fu_error_convert(error); return FALSE; } if (actual_len != length) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "only sent 0x%04x of 0x%04x", (guint)actual_len, (guint)length); return FALSE; } return TRUE; } static gboolean fu_fpc_device_setup_mode(FuFpcDevice *self, GError **error) { g_autoptr(GPtrArray) intfs = NULL; intfs = fu_usb_device_get_interfaces(FU_USB_DEVICE(self), error); if (intfs == NULL) return FALSE; for (guint i = 0; i < intfs->len; i++) { FuUsbInterface *intf = g_ptr_array_index(intfs, i); if (fu_usb_interface_get_class(intf) == FPC_DEVICE_DFU_MODE_CLASS && fu_usb_interface_get_protocol(intf) == FPC_DEVICE_DFU_MODE_PORT) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); return TRUE; } if (fu_usb_interface_get_class(intf) == FPC_DEVICE_NORMAL_MODE_CLASS && fu_usb_interface_get_protocol(intf) == FPC_DEVICE_NORMAL_MODE_PORT) { fu_device_remove_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); return TRUE; } } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no update interface found"); return FALSE; } static gboolean fu_fpc_device_setup_version(FuFpcDevice *self, GError **error) { guint32 version = 0; gsize data_len = 0; FuEndianType endian_type = G_LITTLE_ENDIAN; g_autofree guint8 *data = NULL; guint32 cmd_id = FPC_CMD_GET_STATE; if (fu_device_has_private_flag(FU_DEVICE(self), FU_FPC_DEVICE_FLAG_RTS_DEVICE)) endian_type = G_BIG_ENDIAN; if (!fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { if (fu_device_has_private_flag(FU_DEVICE(self), FU_FPC_DEVICE_FLAG_MOH_DEVICE)) { data_len = FPC_DEVICE_MOH_STATE_LEN; } else { data_len = FPC_DEVICE_MOC_STATE_LEN; } data = g_malloc0(data_len); if (fu_device_has_private_flag(FU_DEVICE(self), FU_FPC_DEVICE_FLAG_LENFY_DEVICE)) cmd_id = FPC_CMD_GET_STATE_LENFY; if (!fu_fpc_device_fw_cmd(self, cmd_id, data, data_len, TRUE, error)) return FALSE; if (!fu_memread_uint32_safe(data, data_len, 0, &version, endian_type, error)) return FALSE; } else { if (!fu_device_has_private_flag(FU_DEVICE(self), FU_FPC_DEVICE_FLAG_LEGACY_DFU)) { if (!fu_fpc_device_dfu_cmd(self, FPC_CMD_DFU_CLRSTATUS, 0x0000, NULL, 0, FALSE, FALSE, error)) { g_prefix_error(error, "fail to clear status in setup version"); return FALSE; } } data = g_malloc0(FPC_DEVICE_DFU_FW_STATUS_LEN); if (!fu_fpc_device_dfu_cmd(self, FPC_CMD_DFU_GET_FW_STATUS, 0x0000, data, FPC_DEVICE_DFU_FW_STATUS_LEN, TRUE, TRUE, error)) { g_prefix_error(error, "fail to get fw status in setup version"); return FALSE; } if (!fu_memread_uint32_safe(data, FPC_DEVICE_DFU_FW_STATUS_LEN, 4, &version, endian_type, error)) return FALSE; } /* set display version */ fu_device_set_version_raw(FU_DEVICE(self), version); return TRUE; } static gboolean fu_fpc_device_check_dfu_status_cb(FuDevice *device, gpointer user_data, GError **error) { FuFpcDevice *self = FU_FPC_DEVICE(device); g_autoptr(GByteArray) dfu_status = fu_struct_fpc_dfu_new(); if (!fu_fpc_device_dfu_cmd(self, FPC_CMD_DFU_GETSTATUS, 0x0000, dfu_status->data, dfu_status->len, TRUE, FALSE, error)) { g_prefix_error(error, "failed to get status: "); return FALSE; } if (fu_struct_fpc_dfu_get_status(dfu_status) != 0 || fu_struct_fpc_dfu_get_state(dfu_status) == FU_FPC_DFU_STATE_DNBUSY) { /* device is not in correct status/state */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "dfu status error [0x%x, 0x%x]", fu_struct_fpc_dfu_get_status(dfu_status), fu_struct_fpc_dfu_get_state(dfu_status)); return FALSE; } if (fu_struct_fpc_dfu_get_max_payload_size(dfu_status) > 0 || fu_device_has_private_flag(FU_DEVICE(self), FU_FPC_DEVICE_FLAG_RTS_DEVICE)) self->max_block_size = FPC_FLASH_BLOCK_SIZE_4096; else self->max_block_size = FPC_FLASH_BLOCK_SIZE_DEFAULT; return TRUE; } static gboolean fu_fpc_device_update_init(FuFpcDevice *self, GError **error) { if (!fu_device_has_private_flag(FU_DEVICE(self), FU_FPC_DEVICE_FLAG_LEGACY_DFU)) { if (!fu_fpc_device_dfu_cmd(self, FPC_CMD_DFU_CLRSTATUS, 0x0000, NULL, 0, FALSE, FALSE, error)) { g_prefix_error(error, "failed to clear status: "); return FALSE; } } return fu_device_retry_full(FU_DEVICE(self), fu_fpc_device_check_dfu_status_cb, FPC_DFU_MAX_ATTEMPTS, 20, NULL, error); } static void fu_fpc_device_to_string(FuDevice *device, guint idt, GString *str) { FuFpcDevice *self = FU_FPC_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "MaxBlockSize", self->max_block_size); fwupd_codec_string_append_bool( str, idt, "LegacyDfu", fu_device_has_private_flag(device, FU_FPC_DEVICE_FLAG_LEGACY_DFU)); fwupd_codec_string_append_bool( str, idt, "MocDevice", !fu_device_has_private_flag(device, FU_FPC_DEVICE_FLAG_MOH_DEVICE)); if (fu_device_has_private_flag(device, FU_FPC_DEVICE_FLAG_MOH_DEVICE)) { fwupd_codec_string_append_bool( str, idt, "RtsDevice", fu_device_has_private_flag(device, FU_FPC_DEVICE_FLAG_RTS_DEVICE)); } } static gboolean fu_fpc_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuFpcDevice *self = FU_FPC_DEVICE(device); /* sanity check */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in runtime mode, skipping"); return TRUE; } if (!fu_fpc_device_dfu_cmd(self, FPC_CMD_DFU_DETACH, 0x0000, NULL, 0, FALSE, FALSE, error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_fpc_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuFpcDevice *self = FU_FPC_DEVICE(device); /* sanity check */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in bootloader mode, skipping"); return TRUE; } if (!fu_fpc_device_fw_cmd(self, FPC_CMD_BOOT0, NULL, 0, FALSE, error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_fpc_device_setup(FuDevice *device, GError **error) { FuFpcDevice *self = FU_FPC_DEVICE(device); g_autofree gchar *name = NULL; /* setup */ if (!FU_DEVICE_CLASS(fu_fpc_device_parent_class)->setup(device, error)) return FALSE; /* remove the ' L:0001 FW:27.26.23.18' suffix */ name = g_strdup(fu_device_get_name(device)); if (name != NULL) { gchar *tmp = g_strstr_len(name, -1, " L:00"); if (tmp != NULL) *tmp = '\0'; fu_device_set_name(device, name); } if (!fu_fpc_device_setup_mode(self, error)) { g_prefix_error(error, "failed to get device mode: "); return FALSE; } /* ensure version */ if (!fu_fpc_device_setup_version(self, error)) { g_prefix_error(error, "failed to get firmware version: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_fpc_device_write_ff2_blocks(FuFpcDevice *self, GInputStream *stream, GError **error) { g_autoptr(FuChunkArray) chunks = NULL; chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FPC_FLASH_BLOCK_SIZE_4096, error); if (chunks == NULL) return FALSE; for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_fpc_device_dfu_cmd(self, FPC_CMD_DFU_DNLOAD_FF2, 0, (guint8 *)fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), FALSE, FALSE, error)) { g_prefix_error(error, "failed to write at 0x%x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } } /* success */ return TRUE; } static gboolean fu_fpc_device_write_ff2_firmware(FuFpcDevice *self, FuFpcFf2Firmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { gsize offset = 0; guint32 blocks_num; g_autoptr(GInputStream) stream = NULL; stream = fu_firmware_get_stream(FU_FIRMWARE(firmware), error); if (stream == NULL) return FALSE; blocks_num = fu_fpc_ff2_firmware_get_blocks_num(firmware); offset += FU_STRUCT_FPC_FF2_HDR_SIZE; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, blocks_num); for (guint i = 0; i < blocks_num; i++) { FuFpcFf2BlockDir direction; g_autoptr(FuStructFpcFf2BlockHdr) st_blkhdr = NULL; g_autoptr(FuStructFpcFf2BlockSec) st_blksec = NULL; guint16 payload_len; /* parse dfu_meta_content_hdr_t */ st_blkhdr = fu_struct_fpc_ff2_block_hdr_parse_stream(stream, offset, error); if (st_blkhdr == NULL) return FALSE; direction = fu_struct_fpc_ff2_block_hdr_get_dir(st_blkhdr); offset += st_blkhdr->len; /* validate dfu_sec_link_t and include the size in payload */ st_blksec = fu_struct_fpc_ff2_block_sec_parse_stream(stream, offset, error); if (st_blksec == NULL) return FALSE; payload_len = fu_struct_fpc_ff2_block_sec_get_payload_len(st_blksec); payload_len += st_blksec->len; if (direction == FU_FPC_FF2_BLOCK_DIR_OUT) { g_autoptr(GInputStream) partial_stream = NULL; g_autoptr(GByteArray) buf_sec = NULL; /* write sec-link chunk? */ buf_sec = fu_input_stream_read_byte_array(stream, offset, FPC_FF2_BLK_SEC_LINK_LEN, fu_progress_get_child(progress), error); if (buf_sec == NULL) return FALSE; if (!fu_fpc_device_dfu_cmd(self, FPC_CMD_DFU_DNLOAD_FF2, 0, buf_sec->data, buf_sec->len, FALSE, FALSE, error)) { g_prefix_error(error, "failed to write sec-link: "); return FALSE; } /* write data in 4k blocks */ partial_stream = fu_partial_input_stream_new(stream, offset + FPC_FF2_BLK_SEC_LINK_LEN, payload_len - FPC_FF2_BLK_SEC_LINK_LEN, error); if (partial_stream == NULL) return FALSE; if (!fu_fpc_device_write_ff2_blocks(self, partial_stream, error)) return FALSE; } else if (direction == FU_FPC_FF2_BLOCK_DIR_IN) { if (!fu_device_retry_full(FU_DEVICE(self), fu_fpc_device_check_dfu_status_cb, FPC_DFU_MAX_ATTEMPTS, 20, NULL, error)) return FALSE; } else { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported direction"); return FALSE; } /* there is a block terminator of 0xFF */ offset += payload_len + sizeof(guint8); fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_fpc_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuFpcDevice *self = FU_FPC_DEVICE(device); g_autoptr(GInputStream) stream = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* new format */ if (FU_IS_FPC_FF2_FIRMWARE(firmware)) { return fu_fpc_device_write_ff2_firmware(self, FU_FPC_FF2_FIRMWARE(firmware), progress, flags, error); } /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "init"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 95, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 5, "check"); /* write old fw format */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* don't auto-boot firmware */ if (!fu_fpc_device_update_init(self, &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to initial update: %s", error_local->message); return FALSE; } fu_progress_step_done(progress); /* build packets */ chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, self->max_block_size, error); if (chunks == NULL) return FALSE; /* write each block */ for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GByteArray) req = g_byte_array_new(); /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; g_byte_array_append(req, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); if (!fu_fpc_device_dfu_cmd(self, FPC_CMD_DFU_DNLOAD, (guint16)i, req->data, (gsize)req->len, FALSE, FALSE, &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to write: %s", error_local->message); return FALSE; } if (!fu_device_retry_full(device, fu_fpc_device_check_dfu_status_cb, FPC_DFU_MAX_ATTEMPTS, 20, NULL, &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to write: %s", error_local->message); return FALSE; } /* update progress */ fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)i + 1, (gsize)fu_chunk_array_length(chunks)); } if (!fu_device_has_private_flag(FU_DEVICE(self), FU_FPC_DEVICE_FLAG_LEGACY_DFU)) { /* exit fw download loop. send null package */ if (!fu_fpc_device_dfu_cmd(self, FPC_CMD_DFU_DNLOAD, 0, NULL, 0, FALSE, FALSE, error)) { g_prefix_error(error, "fail to exit dnload loop: "); return FALSE; } } fu_progress_step_done(progress); if (!fu_device_retry_full(device, fu_fpc_device_check_dfu_status_cb, FPC_DFU_MAX_ATTEMPTS, 20, NULL, error)) return FALSE; fu_progress_step_done(progress); /* success! */ return TRUE; } static void fu_fpc_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static gchar * fu_fpc_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_device_get_version_format(device)); } static void fu_fpc_device_init(FuFpcDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_RUNTIME_VERSION); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); fu_device_set_remove_delay(FU_DEVICE(self), 10000); fu_device_add_protocol(FU_DEVICE(self), "com.fingerprints.dfupc"); fu_device_set_summary(FU_DEVICE(self), "FPC fingerprint sensor"); fu_device_set_install_duration(FU_DEVICE(self), 15); fu_device_set_firmware_size_min(FU_DEVICE(self), 0x10000); fu_device_set_firmware_size_max(FU_DEVICE(self), 0x64000); fu_usb_device_add_interface(FU_USB_DEVICE(self), FPC_USB_INTERFACE); fu_device_register_private_flag(FU_DEVICE(self), FU_FPC_DEVICE_FLAG_MOH_DEVICE); fu_device_register_private_flag(FU_DEVICE(self), FU_FPC_DEVICE_FLAG_RTS_DEVICE); fu_device_register_private_flag(FU_DEVICE(self), FU_FPC_DEVICE_FLAG_LEGACY_DFU); fu_device_register_private_flag(FU_DEVICE(self), FU_FPC_DEVICE_FLAG_LENFY_DEVICE); } static void fu_fpc_device_class_init(FuFpcDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_fpc_device_to_string; device_class->prepare_firmware = fu_fpc_device_prepare_firmware; device_class->write_firmware = fu_fpc_device_write_firmware; device_class->setup = fu_fpc_device_setup; device_class->reload = fu_fpc_device_setup; device_class->attach = fu_fpc_device_attach; device_class->detach = fu_fpc_device_detach; device_class->set_progress = fu_fpc_device_set_progress; device_class->convert_version = fu_fpc_device_convert_version; } fwupd-2.0.10/plugins/fpc/fu-fpc-device.h000066400000000000000000000004361501337203100177340ustar00rootroot00000000000000/* * Copyright 2022 Haowei Lo * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_FPC_DEVICE (fu_fpc_device_get_type()) G_DECLARE_FINAL_TYPE(FuFpcDevice, fu_fpc_device, FU, FPC_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/fpc/fu-fpc-ff2-firmware.c000066400000000000000000000034221501337203100207550ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-fpc-ff2-firmware.h" #include "fu-fpc-struct.h" struct _FuFpcFf2Firmware { FuFirmware parent_instance; guint32 blocks_num; }; G_DEFINE_TYPE(FuFpcFf2Firmware, fu_fpc_ff2_firmware, FU_TYPE_FIRMWARE) static void fu_fpc_ff2_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuFpcFf2Firmware *self = FU_FPC_FF2_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "blocks_num", self->blocks_num); } static gboolean fu_fpc_ff2_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_fpc_ff2_hdr_validate_stream(stream, offset, error); } static gboolean fu_fpc_ff2_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuFpcFf2Firmware *self = FU_FPC_FF2_FIRMWARE(firmware); g_autoptr(FuStructFpcFf2Hdr) st_hdr = NULL; st_hdr = fu_struct_fpc_ff2_hdr_parse_stream(stream, 0x0, error); if (st_hdr == NULL) return FALSE; self->blocks_num = fu_struct_fpc_ff2_hdr_get_blocks_num(st_hdr); /* success */ return TRUE; } guint32 fu_fpc_ff2_firmware_get_blocks_num(FuFpcFf2Firmware *self) { g_return_val_if_fail(FU_IS_FPC_FF2_FIRMWARE(self), G_MAXUINT16); return self->blocks_num; } static void fu_fpc_ff2_firmware_init(FuFpcFf2Firmware *self) { } static void fu_fpc_ff2_firmware_class_init(FuFpcFf2FirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_fpc_ff2_firmware_validate; firmware_class->parse = fu_fpc_ff2_firmware_parse; firmware_class->export = fu_fpc_ff2_firmware_export; } fwupd-2.0.10/plugins/fpc/fu-fpc-ff2-firmware.h000066400000000000000000000005751501337203100207700ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_FPC_FF2_FIRMWARE (fu_fpc_ff2_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuFpcFf2Firmware, fu_fpc_ff2_firmware, FU, FPC_FF2_FIRMWARE, FuFirmware) guint32 fu_fpc_ff2_firmware_get_blocks_num(FuFpcFf2Firmware *self); fwupd-2.0.10/plugins/fpc/fu-fpc-plugin.c000066400000000000000000000014241501337203100177640ustar00rootroot00000000000000/* * Copyright 2022 Haowei Lo * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-fpc-device.h" #include "fu-fpc-ff2-firmware.h" #include "fu-fpc-plugin.h" struct _FuFpcPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuFpcPlugin, fu_fpc_plugin, FU_TYPE_PLUGIN) static void fu_fpc_plugin_init(FuFpcPlugin *self) { } static void fu_fpc_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_FPC_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_FPC_FF2_FIRMWARE); } static void fu_fpc_plugin_class_init(FuFpcPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_fpc_plugin_constructed; } fwupd-2.0.10/plugins/fpc/fu-fpc-plugin.h000066400000000000000000000003431501337203100177700ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuFpcPlugin, fu_fpc_plugin, FU, FPC_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/fpc/fu-fpc.rs000066400000000000000000000017021501337203100166710ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[repr(u8)] enum FuFpcDfuState { Dnbusy = 0x04, } #[derive(New, Getters)] #[repr(C, packed)] struct FuStructFpcDfu { status: u8, max_payload_size: u8, _reserved: [u8; 2], state: FuFpcDfuState, _reserved2: u8, } #[derive(ValidateStream, ParseStream, Default)] #[repr(C, packed)] struct FuStructFpcFf2Hdr { compat_sig: [char; 7] == "FPC0001", reserved: [u8; 20], blocks_num: u32le, reserved: [u8; 6], } #[repr(u8)] enum FuFpcFf2BlockDir { Out = 0x0, In = 0x1, } // dfu_meta_content_hdr_t #[derive(ParseStream, Default)] #[repr(C, packed)] struct FuStructFpcFf2BlockHdr { meta_type: u8 == 0xCD, meta_id: u8, dir: FuFpcFf2BlockDir, } // dfu_sec_link_t #[derive(ParseStream, Default)] #[repr(C, packed)] struct FuStructFpcFf2BlockSec { header: u8 == 0xEE, type: u8, payload_len: u16le, } fwupd-2.0.10/plugins/fpc/meson.build000066400000000000000000000010051501337203100173010ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginFpc"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += join_paths(meson.current_source_dir(), 'fpc.quirk') plugin_builtins += static_library('fu_plugin_fpc', rustgen.process('fu-fpc.rs'), sources: [ 'fu-fpc-device.c', 'fu-fpc-plugin.c', 'fu-fpc-ff2-firmware.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files( 'tests/fpc-lenfy-moh.json', ) fwupd-2.0.10/plugins/fpc/tests/000077500000000000000000000000001501337203100163055ustar00rootroot00000000000000fwupd-2.0.10/plugins/fpc/tests/fpc-lenfy-moh.json000066400000000000000000000007501501337203100216460ustar00rootroot00000000000000{ "name": "FPC Lenfy MoH", "interactive": false, "steps": [ { "url": "0f69958d888545c4f99e09544ed81c982470f58723ad99c8b0b45997211371be-fpc-lenfy-moh-v27_26_23_19.cab", "emulation-url": "8ef3c5f6bf4d294bec4738eb6077652171939f8c5f7eee1e13fd9ca1e5c4ba9e-fpc-lenfy-moh-v27_26_23_19.zip", "components": [ { "version": "27.26.23.19", "guids": [ "bcb666d2-d13b-5295-b6ef-32a3f4ba8737" ] } ] } ] } fwupd-2.0.10/plugins/fresco-pd/000077500000000000000000000000001501337203100162555ustar00rootroot00000000000000fwupd-2.0.10/plugins/fresco-pd/README.md000066400000000000000000000016641501337203100175430ustar00rootroot00000000000000--- title: Plugin: Fresco PD --- ## Introduction This plugin is used to update Power Delivery devices by Fresco. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in an unspecified binary format. This plugin supports the following protocol ID: * `com.frescologic.pd` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_1D5C&PID_7102` These devices also use custom GUID values, e.g. * `USB\VID_1D5C&PID_7102&CID_01` ## Update Behavior The firmware is deployed when the device is in normal runtime mode, and the device will reset when the new firmware has been written. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x1D5C` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.3.8`. fwupd-2.0.10/plugins/fresco-pd/fresco-pd.quirk000066400000000000000000000001511501337203100212110ustar00rootroot00000000000000# FL7102 [USB\VID_1D5C&PID_7102] Plugin = fresco_pd # FL7112 [USB\VID_1D5C&PID_7112] Plugin = fresco_pd fwupd-2.0.10/plugins/fresco-pd/fu-fresco-pd-common.c000066400000000000000000000006671501337203100222120ustar00rootroot00000000000000/* * Copyright 2020 Fresco Logic * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-fresco-pd-common.h" gchar * fu_fresco_pd_version_from_buf(const guint8 ver[4]) { if (ver[3] == 1 || ver[3] == 2) return g_strdup_printf("%u.%u.%u.%u", ver[0], ver[1], ver[2], ver[3]); return g_strdup_printf("%u.%u.%u.%u", ver[3], ver[1], ver[2], ver[0]); } fwupd-2.0.10/plugins/fresco-pd/fu-fresco-pd-common.h000066400000000000000000000003631501337203100222100ustar00rootroot00000000000000/* * Copyright 2020 Fresco Logic * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include gchar * fu_fresco_pd_version_from_buf(const guint8 ver[4]); fwupd-2.0.10/plugins/fresco-pd/fu-fresco-pd-device.c000066400000000000000000000321161501337203100221530ustar00rootroot00000000000000/* * Copyright 2020 Fresco Logic * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-fresco-pd-common.h" #include "fu-fresco-pd-device.h" #include "fu-fresco-pd-firmware.h" struct _FuFrescoPdDevice { FuUsbDevice parent_instance; guint8 customer_id; }; G_DEFINE_TYPE(FuFrescoPdDevice, fu_fresco_pd_device, FU_TYPE_USB_DEVICE) static void fu_fresco_pd_device_to_string(FuDevice *device, guint idt, GString *str) { FuFrescoPdDevice *self = FU_FRESCO_PD_DEVICE(device); fwupd_codec_string_append_int(str, idt, "CustomerID", self->customer_id); } static gboolean fu_fresco_pd_device_transfer_read(FuFrescoPdDevice *self, guint16 offset, guint8 *buf, guint16 bufsz, GError **error) { gsize actual_length = 0; g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(bufsz != 0, FALSE); /* to device */ fu_dump_raw(G_LOG_DOMAIN, "read", buf, bufsz); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0x40, 0x0, offset, buf, bufsz, &actual_length, 5000, NULL, error)) { g_prefix_error(error, "failed to read from offset 0x%x: ", offset); fu_error_convert(error); return FALSE; } if (bufsz != actual_length) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "read 0x%x bytes of 0x%x", (guint)actual_length, bufsz); return FALSE; } /* success */ return TRUE; } static gboolean fu_fresco_pd_device_transfer_write(FuFrescoPdDevice *self, guint16 offset, guint8 *buf, guint16 bufsz, GError **error) { gsize actual_length = 0; g_return_val_if_fail(buf != NULL, FALSE); g_return_val_if_fail(bufsz != 0, FALSE); /* to device */ fu_dump_raw(G_LOG_DOMAIN, "write", buf, bufsz); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0x41, 0x0, offset, buf, bufsz, &actual_length, 5000, NULL, error)) { g_prefix_error(error, "failed to write offset 0x%x: ", offset); return FALSE; } if (bufsz != actual_length) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "wrote 0x%x bytes of 0x%x", (guint)actual_length, bufsz); return FALSE; } /* success */ return TRUE; } static gboolean fu_fresco_pd_device_read_byte(FuFrescoPdDevice *self, guint16 offset, guint8 *buf, GError **error) { return fu_fresco_pd_device_transfer_read(self, offset, buf, 1, error); } static gboolean fu_fresco_pd_device_write_byte(FuFrescoPdDevice *self, guint16 offset, guint8 buf, GError **error) { return fu_fresco_pd_device_transfer_write(self, offset, &buf, 1, error); } static gboolean fu_fresco_pd_device_set_byte(FuFrescoPdDevice *self, guint16 offset, guint8 val, GError **error) { guint8 buf = 0x0; if (!fu_fresco_pd_device_read_byte(self, offset, &buf, error)) return FALSE; if (buf == val) return TRUE; return fu_fresco_pd_device_write_byte(self, offset, val, error); } static gboolean fu_fresco_pd_device_and_byte(FuFrescoPdDevice *self, guint16 offset, guint8 val, GError **error) { guint8 buf = 0xff; if (!fu_fresco_pd_device_read_byte(self, offset, &buf, error)) return FALSE; buf &= val; return fu_fresco_pd_device_write_byte(self, offset, buf, error); } static gboolean fu_fresco_pd_device_or_byte(FuFrescoPdDevice *self, guint16 offset, guint8 val, GError **error) { guint8 buf; if (!fu_fresco_pd_device_read_byte(self, offset, &buf, error)) return FALSE; buf |= val; return fu_fresco_pd_device_write_byte(self, offset, buf, error); } static gboolean fu_fresco_pd_device_setup(FuDevice *device, GError **error) { FuFrescoPdDevice *self = FU_FRESCO_PD_DEVICE(device); guint8 ver[4] = {0x0}; g_autofree gchar *version = NULL; /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_fresco_pd_device_parent_class)->setup(device, error)) return FALSE; /* read existing device version */ for (guint i = 0; i < 4; i++) { if (!fu_fresco_pd_device_transfer_read(self, 0x3000 + i, &ver[i], 1, error)) { g_prefix_error(error, "failed to read device version [%u]: ", i); return FALSE; } } version = fu_fresco_pd_version_from_buf(ver); fu_device_set_version(FU_DEVICE(self), version); /* get customer ID */ self->customer_id = ver[1]; /* add extra instance ID */ fu_device_add_instance_u8(device, "CID", self->customer_id); return fu_device_build_instance_id(device, error, "USB", "VID", "PID", "CID", NULL); } static FuFirmware * fu_fresco_pd_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuFrescoPdDevice *self = FU_FRESCO_PD_DEVICE(device); guint8 customer_id; g_autoptr(FuFirmware) firmware = fu_fresco_pd_firmware_new(); /* check firmware is suitable */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; customer_id = fu_fresco_pd_firmware_get_customer_id(FU_FRESCO_PD_FIRMWARE(firmware)); if (customer_id != self->customer_id) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "device is incompatible with firmware x.%u.x.x", customer_id); return NULL; } return g_steal_pointer(&firmware); } static gboolean fu_fresco_pd_device_panther_reset_device(FuFrescoPdDevice *self, GError **error) { g_autoptr(GError) error_local = NULL; g_debug("resetting target device"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* ignore when the device reset before completing the transaction */ if (!fu_fresco_pd_device_or_byte(self, 0xA003, 1 << 3, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL)) { g_debug("ignoring %s", error_local->message); return TRUE; } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to reset device: "); return FALSE; } return TRUE; } static gboolean fu_fresco_pd_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuFrescoPdDevice *self = FU_FRESCO_PD_DEVICE(device); const guint8 *buf; gsize bufsz = 0x0; guint16 begin_addr = 0x6420; guint8 config[3] = {0x0}; guint8 start_symbols[2] = {0x0}; g_autoptr(GBytes) fw = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "enable-mtp-write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 50, "copy-mmio"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 46, "customize"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "boot"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, NULL); /* get default blob, which we know is already bigger than FirmwareMin */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; buf = g_bytes_get_data(fw, &bufsz); /* get start symbols, and be slightly paranoid */ if (!fu_memcpy_safe(start_symbols, sizeof(start_symbols), 0x0, /* dst */ buf, bufsz, 0x4000, /* src */ sizeof(start_symbols), error)) return FALSE; /* 0xA001 = b'0 * 0x6C00 = b'0 * 0x6C04 = 0x08 */ g_debug("disable MCU, and enable mtp write"); if (!fu_fresco_pd_device_and_byte(self, 0xa001, ~(1 << 2), error)) { g_prefix_error(error, "failed to disable MCU bit 2: "); return FALSE; } if (!fu_fresco_pd_device_and_byte(self, 0x6c00, ~(1 << 1), error)) { g_prefix_error(error, "failed to disable MCU bit 1: "); return FALSE; } if (!fu_fresco_pd_device_write_byte(self, 0x6c04, 0x08, error)) { g_prefix_error(error, "failed to disable MCU: "); return FALSE; } /* fill safe code in the boot code */ for (guint16 i = 0; i < 0x400; i += 3) { for (guint j = 0; j < 3; j++) { if (!fu_fresco_pd_device_read_byte(self, begin_addr + i + j, &config[j], error)) { g_prefix_error(error, "failed to read config byte %u: ", j); return FALSE; } } if (config[0] == start_symbols[0] && config[1] == start_symbols[1]) { begin_addr = 0x6420 + i; break; } if (config[0] == 0 && config[1] == 0 && config[2] == 0) break; } g_debug("begin_addr: 0x%04x", begin_addr); for (guint i = begin_addr + 3; i < (guint)begin_addr + 0x400; i += 3) { for (guint j = 0; j < 3; j++) { if (!fu_fresco_pd_device_read_byte(self, i + j, &config[j], error)) { g_prefix_error(error, "failed to read config byte %u: ", j); return FALSE; } } if (config[0] == 0x74 && config[1] == 0x06 && config[2] != 0x22) { if (!fu_fresco_pd_device_write_byte(self, i + 2, 0x22, error)) return FALSE; } else if (config[0] == 0x6c && config[1] == 0x00 && config[2] != 0x01) { if (!fu_fresco_pd_device_write_byte(self, i + 2, 0x01, error)) return FALSE; } else if (config[0] == 0x00 && config[1] == 0x00 && config[2] != 0x00) break; } fu_progress_step_done(progress); /* copy buf offset [0 - 0x3FFFF] to mmio address [0x2000 - 0x5FFF] */ g_debug("fill firmware body"); for (guint16 byte_index = 0; byte_index < 0x4000; byte_index++) { if (!fu_fresco_pd_device_set_byte(self, byte_index + 0x2000, buf[byte_index], error)) return FALSE; fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)byte_index + 1, 0x4000); } fu_progress_step_done(progress); /* write file buf 0x4200 ~ 0x4205, 6 bytes to internal address 0x6600 ~ 0x6605 * write file buf 0x4210 ~ 0x4215, 6 bytes to internal address 0x6610 ~ 0x6615 * write file buf 0x4220 ~ 0x4225, 6 bytes to internal address 0x6620 ~ 0x6625 * write file buf 0x4230, 1 byte, to internal address 0x6630 */ g_debug("update customize data"); for (guint16 byte_index = 0; byte_index < 6; byte_index++) { if (!fu_fresco_pd_device_set_byte(self, 0x6600 + byte_index, buf[0x4200 + byte_index], error)) return FALSE; if (!fu_fresco_pd_device_set_byte(self, 0x6610 + byte_index, buf[0x4210 + byte_index], error)) return FALSE; if (!fu_fresco_pd_device_set_byte(self, 0x6620 + byte_index, buf[0x4220 + byte_index], error)) return FALSE; } if (!fu_fresco_pd_device_set_byte(self, 0x6630, buf[0x4230], error)) return FALSE; fu_progress_step_done(progress); /* overwrite firmware file's boot code area (0x4020 ~ 0x41ff) to the area on the device * marked by begin_addr example: if the begin_addr = 0x6420, then copy file buf [0x4020 ~ * 0x41ff] to device offset[0x6420 ~ 0x65ff] */ g_debug("write boot configuration area"); for (guint16 byte_index = 0; byte_index < 0x1e0; byte_index += 3) { if (!fu_fresco_pd_device_set_byte(self, begin_addr + byte_index + 0, buf[0x4020 + byte_index], error)) return FALSE; if (!fu_fresco_pd_device_set_byte(self, begin_addr + byte_index + 1, buf[0x4021 + byte_index], error)) return FALSE; if (!fu_fresco_pd_device_set_byte(self, begin_addr + byte_index + 2, buf[0x4022 + byte_index], error)) return FALSE; } fu_progress_step_done(progress); /* reset the device */ if (!fu_fresco_pd_device_panther_reset_device(self, error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static void fu_fresco_pd_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_fresco_pd_device_init(FuFrescoPdDevice *self) { fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_USB_HUB); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_protocol(FU_DEVICE(self), "com.frescologic.pd"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); fu_device_set_install_duration(FU_DEVICE(self), 15); fu_device_set_remove_delay(FU_DEVICE(self), 20000); fu_device_set_firmware_size(FU_DEVICE(self), 0x4400); } static void fu_fresco_pd_device_class_init(FuFrescoPdDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_fresco_pd_device_to_string; device_class->setup = fu_fresco_pd_device_setup; device_class->write_firmware = fu_fresco_pd_device_write_firmware; device_class->prepare_firmware = fu_fresco_pd_device_prepare_firmware; device_class->set_progress = fu_fresco_pd_device_set_progress; } fwupd-2.0.10/plugins/fresco-pd/fu-fresco-pd-device.h000066400000000000000000000004711501337203100221570ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_FRESCO_PD_DEVICE (fu_fresco_pd_device_get_type()) G_DECLARE_FINAL_TYPE(FuFrescoPdDevice, fu_fresco_pd_device, FU, FRESCO_PD_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/fresco-pd/fu-fresco-pd-firmware.c000066400000000000000000000036611501337203100225330ustar00rootroot00000000000000/* * Copyright 2020 Fresco Logic * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-fresco-pd-common.h" #include "fu-fresco-pd-firmware.h" struct _FuFrescoPdFirmware { FuFirmwareClass parent_instance; guint8 customer_id; }; G_DEFINE_TYPE(FuFrescoPdFirmware, fu_fresco_pd_firmware, FU_TYPE_FIRMWARE) guint8 fu_fresco_pd_firmware_get_customer_id(FuFrescoPdFirmware *self) { return self->customer_id; } static void fu_fresco_pd_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuFrescoPdFirmware *self = FU_FRESCO_PD_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "customer_id", self->customer_id); } static gboolean fu_fresco_pd_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuFrescoPdFirmware *self = FU_FRESCO_PD_FIRMWARE(firmware); guint8 ver[4] = {0x0}; g_autofree gchar *version = NULL; /* read version block */ if (!fu_input_stream_read_safe(stream, ver, sizeof(ver), 0x0, /* dst */ 0x1000, /* src */ sizeof(ver), error)) return FALSE; /* customer ID is always the 2nd byte */ self->customer_id = ver[1]; /* set version number */ version = fu_fresco_pd_version_from_buf(ver); fu_firmware_set_version(firmware, version); return TRUE; } static void fu_fresco_pd_firmware_init(FuFrescoPdFirmware *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_NO_AUTO_DETECTION); } static void fu_fresco_pd_firmware_class_init(FuFrescoPdFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_fresco_pd_firmware_parse; firmware_class->export = fu_fresco_pd_firmware_export; } FuFirmware * fu_fresco_pd_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_FRESCO_PD_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/fresco-pd/fu-fresco-pd-firmware.h000066400000000000000000000007301501337203100225320ustar00rootroot00000000000000/* * Copyright 2020 Fresco Logic * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_FRESCO_PD_FIRMWARE (fu_fresco_pd_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuFrescoPdFirmware, fu_fresco_pd_firmware, FU, FRESCO_PD_FIRMWARE, FuFirmware) FuFirmware * fu_fresco_pd_firmware_new(void); guint8 fu_fresco_pd_firmware_get_customer_id(FuFrescoPdFirmware *self); fwupd-2.0.10/plugins/fresco-pd/fu-fresco-pd-plugin.c000066400000000000000000000015321501337203100222100ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-fresco-pd-device.h" #include "fu-fresco-pd-firmware.h" #include "fu-fresco-pd-plugin.h" struct _FuFrescoPdPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuFrescoPdPlugin, fu_fresco_pd_plugin, FU_TYPE_PLUGIN) static void fu_fresco_pd_plugin_init(FuFrescoPdPlugin *self) { } static void fu_fresco_pd_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_FRESCO_PD_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_FRESCO_PD_FIRMWARE); } static void fu_fresco_pd_plugin_class_init(FuFrescoPdPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_fresco_pd_plugin_constructed; } fwupd-2.0.10/plugins/fresco-pd/fu-fresco-pd-plugin.h000066400000000000000000000003641501337203100222170ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuFrescoPdPlugin, fu_fresco_pd_plugin, FU, FRESCO_PD_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/fresco-pd/lsusb.txt000066400000000000000000000047141501337203100201540ustar00rootroot00000000000000Bus 003 Device 002: ID 1d5c:7102 Fresco Logic Generic Billboard Device Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.01 bDeviceClass 17 bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 64 idVendor 0x1d5c Fresco Logic idProduct 0x7102 bcdDevice 1.00 iManufacturer 1 Fresco Logic, Inc iProduct 2 Generic Billboard Device iSerial 0 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 0x0012 bNumInterfaces 1 bConfigurationValue 1 iConfiguration 0 bmAttributes 0xc0 Self Powered MaxPower 0mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 0 bInterfaceClass 17 bInterfaceSubClass 0 bInterfaceProtocol 0 iInterface 0 Binary Object Store Descriptor: bLength 5 bDescriptorType 15 wTotalLength 0x0050 bNumDeviceCaps 3 USB 2.0 Extension Device Capability: bLength 7 bDescriptorType 16 bDevCapabilityType 2 bmAttributes 0x00000006 BESL Link Power Management (LPM) Supported Container ID Device Capability: bLength 20 bDescriptorType 16 bDevCapabilityType 4 bReserved 0 ContainerID {00000000-0000-0000-0000-000000000000} Billboard Capability: bLength 48 bDescriptorType 16 bDevCapabilityType 13 iAdditionalInfoURL 4 www.frescologic.com bNumberOfAlternateModes 1 bPreferredAlternateMode 0 VCONN Power 0 1W bmConfigured 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 bcdVersion 1.10 bAdditionalFailureInfo 0 bReserved 0 Alternate Modes supported by Device Container: Alternate Mode 0 : Alternate Mode configuration successful wSVID[0] 0x0000 bAlternateMode[0] 0 iAlternateModeString[0] 0 Device Status: 0x0001 Self Powered fwupd-2.0.10/plugins/fresco-pd/meson.build000066400000000000000000000011211501337203100204120ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginFrescoPd"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('fresco-pd.quirk') plugin_builtins += static_library('fu_plugin_fresco_pd', sources: [ 'fu-fresco-pd-plugin.c', 'fu-fresco-pd-common.c', 'fu-fresco-pd-device.c', 'fu-fresco-pd-firmware.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) enumeration_data += files('tests/fresco-pd-setup.json') device_tests += files( 'tests/fresco-pd.json', 'tests/ugreen-cm260.json', ) fwupd-2.0.10/plugins/fresco-pd/tests/000077500000000000000000000000001501337203100174175ustar00rootroot00000000000000fwupd-2.0.10/plugins/fresco-pd/tests/fresco-pd-setup.json000066400000000000000000000043461501337203100233410ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUsbDevice", "PlatformId": "1-5.2", "Created": "2024-10-25T09:33:39.966550Z", "IdVendor": 7516, "IdProduct": 28930, "Device": 256, "USB": 513, "Manufacturer": 1, "DeviceClass": 17, "Product": 2, "UsbBosDescriptors": [ { "DevCapabilityType": 80, "ExtraData": "AAM=" }, { "DevCapabilityType": 2, "ExtraData": "BgAAAA==" }, { "DevCapabilityType": 4, "ExtraData": "AAAAAAAAAAAAAAAAAAAAAAA=" }, { "DevCapabilityType": 13, "ExtraData": "BAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABABAAAAAAAA" } ], "UsbConfigDescriptors": [ { "ConfigurationValue": 1 } ], "UsbInterfaces": [ { "Length": 9, "DescriptorType": 4, "InterfaceClass": 17 } ], "UsbEvents": [ { "Id": "#4693935e", "Data": "001" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=92\nDEVNAME=bus/usb/001/093\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=1d5c/7102/100\nTYPE=17/0/0\nBUSNUM=001\nDEVNUM=093" }, { "Id": "#1ab3ae0a", "Data": "093" }, { "Id": "#4693935e", "Data": "001" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=92\nDEVNAME=bus/usb/001/093\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=1d5c/7102/100\nTYPE=17/0/0\nBUSNUM=001\nDEVNUM=093" }, { "Id": "#1ab3ae0a", "Data": "093" }, { "Id": "#1fcf122d", "Data": "R2VuZXJpYyBCaWxsYm9hcmQgRGV2aWNlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" }, { "Id": "#933f091c", "Data": "AA==" }, { "Id": "#8e7dabc5", "Data": "Yw==" }, { "Id": "#de7b3dbb", "Data": "Eg==" }, { "Id": "#b4a87976", "Data": "Bw==" } ] } ] } fwupd-2.0.10/plugins/fresco-pd/tests/fresco-pd.json000066400000000000000000000004761501337203100222030ustar00rootroot00000000000000{ "name": "Fresco PD", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/fresco-pd-setup.json", "components": [ { "version": "7.99.18.0", "guids": [ "790a1b00-9cf9-5220-af78-ef690d303fa8" ] } ] } ] } fwupd-2.0.10/plugins/fresco-pd/tests/ugreen-cm260.json000066400000000000000000000012001501337203100224150ustar00rootroot00000000000000{ "name": "Ugreen CM260", "interactive": false, "steps": [ { "url": "de152591660080ed5de8550b1b2dfcd738d1f5a68bd30e5f4c684b39b9ebeeb2-Ugreen-CM260-7.2.1.0.cab", "components": [ { "version": "7.2.1.0", "guids": [ "7afc5bff-be55-5e95-81ca-584f13207b1d" ] } ] }, { "url": "9480d06d1a3e524609660653fadc337d14d72f362bd2e80b15c5c3c1733f629b-Ugreen-CM260-7.2.2.0.cab", "components": [ { "version": "7.2.2.0", "guids": [ "7afc5bff-be55-5e95-81ca-584f13207b1d" ] } ] } ] } fwupd-2.0.10/plugins/genesys-gl32xx/000077500000000000000000000000001501337203100171755ustar00rootroot00000000000000fwupd-2.0.10/plugins/genesys-gl32xx/README.md000066400000000000000000000022451501337203100204570ustar00rootroot00000000000000--- title: Plugin: Genesys GL322x/GL323x --- ## Introduction The GL3224/GL3232 families are USB3 card reader products. ## Firmware Format This plugin supports the following protocol ID: * `com.genesys.gl32xx` ## GUID Generation These devices use the standard UDEV DeviceInstanceId values, e.g. * `BLOCK\VEN_05E3&DEV_XXXX` (quirk-only) These devices also use custom GUID values, e.g. * `BLOCK\VEN_05E3&DEV_XXXX&VER_YY` (quirk-only) Additional GUID value based on firmware version stream and customer ID read from the device, e.g. * `BLOCK\VEN_05E3&DEV_XXXX&VER_YY&CID_ZZZZZZZZ` ## Update Behavior The device is switched to ROM mode for the update and the device must be reset the firmware update/dump to return back to normal mode. For 323x family the expected firmware size is `0x01C000`, and `0x010000` for 3224. ## Vendor ID Security The vendor ID is set from the udev vendor, in this instance set to `BLOCK:0x05E3` ## External Interface Access This plugin requires read/write access to `/dev/sd*` block devices and requires using a `sg_io ioctl` for interaction with the device. ## Version Considerations This plugin has been available since fwupd version `1.9.3`. fwupd-2.0.10/plugins/genesys-gl32xx/fu-genesys-gl32xx-device.c000066400000000000000000000573271501337203100240260ustar00rootroot00000000000000/* * Copyright 2023 Denis Pynkin * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-genesys-gl32xx-device.h" #include "fu-genesys-gl32xx-firmware.h" #define FU_GENESYS_GL32XX_FW_START_ADDR 0x0 #define FU_GENESYS_GL32XX_FW_SIZE 0x00010000 #define FU_GENESYS_GL32XX_CLEAR_WP_SLEEP_MS 800 struct _FuGenesysGl32xxDevice { FuBlockDevice parent_instance; gchar *chip_name; guint32 packetsz; guint32 customer_id; guint16 compatible_model; }; G_DEFINE_TYPE(FuGenesysGl32xxDevice, fu_genesys_gl32xx_device, FU_TYPE_BLOCK_DEVICE) static void fu_genesys_gl32xx_device_set_chip_name(FuGenesysGl32xxDevice *self, const gchar *chip_name) { g_return_if_fail(chip_name != NULL); g_free(self->chip_name); self->chip_name = g_strdup(chip_name); } static GByteArray * fu_genesys_gl32xx_device_cmd_get_version(FuGenesysGl32xxDevice *self, GError **error) { const guint8 cmd[] = {0x12, 0x00, 0x00, 0x00, 0x2e, 0x00}; g_autoptr(GByteArray) buf = g_byte_array_new(); fu_byte_array_set_size(buf, 0x2E, 0x0); if (!fu_block_device_sg_io_cmd_read(FU_BLOCK_DEVICE(self), cmd, sizeof(cmd), buf->data, buf->len, error)) return NULL; return g_steal_pointer(&buf); } static gboolean fu_genesys_gl32xx_device_cmd_switch_to_rom_mode(FuGenesysGl32xxDevice *self, GError **error) { const guint8 cmd[] = {0xF3, 0x06, 0x00, 0x00, 0x00, 0x00}; if (!fu_block_device_sg_io_cmd_none(FU_BLOCK_DEVICE(self), cmd, sizeof(cmd), error)) { g_prefix_error(error, "failed to switch into ROM mode: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_gl32xx_device_cmd_reset_usb(FuGenesysGl32xxDevice *self, GError **error) { const guint8 cmd[] = {0xE6, 0x00, 0x00, 0x00, 0x00, 0x00}; if (!fu_block_device_sg_io_cmd_none(FU_BLOCK_DEVICE(self), cmd, sizeof(cmd), error)) { g_prefix_error(error, "failed to reset USB: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_gl32xx_device_cmd_write_sr(FuGenesysGl32xxDevice *self, GError **error) { const guint8 cmd[] = {0xF3, 0x01, 0x00, 0x00, 0x01, 0x00}; return fu_block_device_sg_io_cmd_none(FU_BLOCK_DEVICE(self), cmd, sizeof(cmd), error); } static gboolean fu_genesys_gl32xx_device_cmd_write_enable(FuGenesysGl32xxDevice *self, GError **error) { const guint8 cmd[] = {0xF3, 0x00, 0x00, 0x00, 0x06, 0x00}; return fu_block_device_sg_io_cmd_none(FU_BLOCK_DEVICE(self), cmd, sizeof(cmd), error); } static gboolean fu_genesys_gl32xx_device_cmd_write_disable(FuGenesysGl32xxDevice *self, GError **error) { const guint8 cmd[] = {0xF3, 0x00, 0x00, 0x00, 0x04, 0x00}; return fu_block_device_sg_io_cmd_none(FU_BLOCK_DEVICE(self), cmd, sizeof(cmd), error); } static gboolean fu_genesys_gl32xx_device_cmd_clear_wp(FuGenesysGl32xxDevice *self, GError **error) { const guint8 cmd[] = {0xF3, 0x02, 0x00, 0x02, 0x00, 0x00}; const guint8 data[] = {0x01, 0x00}; return fu_block_device_sg_io_cmd_write(FU_BLOCK_DEVICE(self), cmd, sizeof(cmd), data, sizeof(data), error); } static gboolean fu_genesys_gl32xx_device_cmd_chip_erase(FuGenesysGl32xxDevice *self, GError **error) { const guint8 cmd[] = {0xF3, 0x00, 0x00, 0x00, 0xC7, 0x00}; return fu_block_device_sg_io_cmd_none(FU_BLOCK_DEVICE(self), cmd, sizeof(cmd), error); } static gboolean fu_genesys_gl32xx_device_cmd_wait_wip(FuGenesysGl32xxDevice *self, GError **error) { const guint8 cmd[] = {0xF3, 0x03, 0x01, 0x00, 0x05, 0x00}; return fu_block_device_sg_io_cmd_none(FU_BLOCK_DEVICE(self), cmd, sizeof(cmd), error); } static gboolean fu_genesys_gl32xx_device_cmd_read_flash(FuGenesysGl32xxDevice *self, gsize addr, guint8 *data, gsize datasz, GError **error) { guint8 cmd[] = {0xE4, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; g_return_val_if_fail(data != NULL && datasz != 0, FALSE); /* start address */ if (!fu_memwrite_uint32_safe(cmd, sizeof(cmd), 2, addr, G_BIG_ENDIAN, error)) return FALSE; /* block size */ if (!fu_memwrite_uint16_safe(cmd, sizeof(cmd), 6, (guint16)datasz, G_BIG_ENDIAN, error)) return FALSE; return fu_block_device_sg_io_cmd_read(FU_BLOCK_DEVICE(self), cmd, sizeof(cmd), data, datasz, error); } static gboolean fu_genesys_gl32xx_device_ensure_version(FuGenesysGl32xxDevice *self, GError **error) { g_autofree gchar *version = NULL; g_autofree gchar *version_prefix = NULL; g_autoptr(GByteArray) buf = NULL; buf = fu_genesys_gl32xx_device_cmd_get_version(self, error); if (buf == NULL) { g_prefix_error(error, "failed to read version: "); return FALSE; } if (buf->len < 0x24) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to read version"); return FALSE; } version = fu_memstrsafe(buf->data, buf->len, 0x20, 4, error); if (version == NULL) return FALSE; fu_device_set_version(FU_DEVICE(self), version); /* this is used to differentiate standard firmware versions */ version_prefix = fu_memstrsafe(buf->data, buf->len, 0x20, 2, error); if (version_prefix == NULL) return FALSE; fu_device_add_instance_str(FU_DEVICE(self), "VER", version_prefix); return fu_device_build_instance_id_full(FU_DEVICE(self), FU_DEVICE_INSTANCE_FLAG_QUIRKS, error, "BLOCK", "VEN", "DEV", "VER", NULL); } static gboolean fu_genesys_gl32xx_device_check_rom_mode(FuGenesysGl32xxDevice *self, guint8 *cmd, gsize cmdsz, GError **error) { const guint8 ext_rom_mode[] = {0x58, 0x52, 0x4F, 0x4D}; /* "XROM" */ const guint8 int_rom_mode[] = {0x49, 0x4E, 0x54, 0x2D}; /* "INT-" */ guint8 data[4] = {0}; if (!fu_block_device_sg_io_cmd_read(FU_BLOCK_DEVICE(self), cmd, cmdsz, data, sizeof(data), error)) return FALSE; if (fu_memcmp_safe(int_rom_mode, sizeof(int_rom_mode), 0, data, sizeof(data), 0, sizeof(data), NULL)) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); return TRUE; } if (fu_memcmp_safe(ext_rom_mode, sizeof(ext_rom_mode), 0, data, sizeof(data), 0, sizeof(data), NULL)) { fu_device_remove_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no supported devices detected"); return FALSE; } /* safe to call in any mode */ static gboolean fu_genesys_gl32xx_device_ensure_rom_mode(FuGenesysGl32xxDevice *self, GError **error) { guint8 cmd_gl323x[] = {0xE4, 0x01, 0x00, 0xDC, 0x04, 0x00}; guint8 cmd_gl3224[] = {0xE4, 0x01, 0x00, 0xFC, 0x04, 0x00}; g_autoptr(GError) error_local = NULL; /* check for 3230, 3231, 3232, 3230S, 3231S, 3232S first */ /* ignore error here */ if (!fu_genesys_gl32xx_device_check_rom_mode(self, cmd_gl323x, sizeof(cmd_gl323x), &error_local)) { g_debug("ignoring: %s", error_local->message); } else { fu_genesys_gl32xx_device_set_chip_name(self, "GL323x"); return TRUE; } /* check the 3224 */ if (!fu_genesys_gl32xx_device_check_rom_mode(self, cmd_gl3224, sizeof(cmd_gl3224), error)) return FALSE; fu_genesys_gl32xx_device_set_chip_name(self, "GL3224"); return TRUE; } static gboolean fu_genesys_gl32xx_device_verify_chip_id(FuGenesysGl32xxDevice *self, GError **error) { const guint8 cmd_req[] = {0xF3, 0x02, 0x00, 0x01, 0x00, 0x03}; const guint8 data_req[] = {0x9F}; const guint8 cmd_get[] = {0xF3, 0x04, 0x00, 0x00, 0x00, 0x03}; guint8 buf[3] = {0}; g_autofree gchar *flash_id = NULL; g_autoptr(FuCfiDevice) cfi_device = NULL; if (!fu_block_device_sg_io_cmd_write(FU_BLOCK_DEVICE(self), cmd_req, sizeof(cmd_req), data_req, sizeof(data_req), error)) return FALSE; if (!fu_block_device_sg_io_cmd_read(FU_BLOCK_DEVICE(self), cmd_get, sizeof(cmd_get), buf, sizeof(buf), error)) return FALSE; flash_id = g_strdup_printf("%02X%02X%02X", buf[0], buf[1], buf[2]); cfi_device = fu_cfi_device_new(fu_device_get_context(FU_DEVICE(self)), flash_id); if (!fu_device_setup(FU_DEVICE(cfi_device), error)) return FALSE; if (fu_device_get_name(cfi_device) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "not supported flash type"); return FALSE; } g_debug("flash type detected: %s", fu_device_get_name(cfi_device)); /* success */ return TRUE; } static void fu_genesys_gl32xx_device_ensure_enforce_requires(FuGenesysGl32xxDevice *self) { const gchar *version = fu_device_get_version(FU_DEVICE(self)); const guint16 model = fu_device_get_pid(FU_DEVICE(self)); /* GL3224 */ if (model == 0x0749 && self->customer_id == 0xFFFFFFFF && g_str_has_prefix(version, "15")) { fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ENFORCE_REQUIRES); return; } /* GL323X */ if (model == 0x0764 && self->customer_id == 0x22FFFFFF && g_str_has_prefix(version, "29")) { fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ENFORCE_REQUIRES); return; } } static gboolean fu_genesys_gl32xx_device_ensure_cid(FuGenesysGl32xxDevice *self, GError **error) { const guint8 cmd_gl3224_cid[] = {0xE4, 0x01, 0xBF, 0x80, 0x04, 0x00}; const guint8 cmd_gl323x_cid[] = {0xE4, 0x01, 0x35, 0x00, 0x04, 0x00}; const guint8 *cmd = NULL; guint16 model = fu_device_get_pid(FU_DEVICE(self)); guint8 data[4] = {0}; if (self->compatible_model != 0) model = self->compatible_model; switch (model) { case 0x0749: cmd = cmd_gl3224_cid; break; case 0x0764: cmd = cmd_gl323x_cid; break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "unsupported model [0x%04X]", model); return FALSE; } if (!fu_block_device_sg_io_cmd_read(FU_BLOCK_DEVICE(self), cmd, sizeof(cmd_gl323x_cid), data, sizeof(data), error)) return FALSE; self->customer_id = fu_memread_uint32(data, G_BIG_ENDIAN); fu_device_add_instance_u32(FU_DEVICE(self), "CID", self->customer_id); /* valid GUID with the pair of FW version stream and customer ID */ return fu_device_build_instance_id(FU_DEVICE(self), error, "BLOCK", "VEN", "DEV", "VER", "CID", NULL); } static gboolean fu_genesys_gl32xx_device_get_usb_mode(FuGenesysGl32xxDevice *self, GError **error) { guint8 mode = 0; const guint8 cmd[] = {0xF2, 0xFF, 0x00, 0x00, 0x00, 0x00}; if (!fu_block_device_sg_io_cmd_read(FU_BLOCK_DEVICE(self), cmd, sizeof(cmd), &mode, sizeof(mode), error)) { g_prefix_error(error, "failed to read USB mode: "); return FALSE; } switch (mode) { case 1: self->packetsz = 64; break; case 2: self->packetsz = 512; break; case 3: self->packetsz = 1024; break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unknown USB mode 0x%02x read from device", mode); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_gl32xx_device_erase(FuGenesysGl32xxDevice *self, GError **error) { /* write enable */ if (!fu_genesys_gl32xx_device_cmd_write_enable(self, error)) { g_prefix_error(error, "failed to write enable: "); return FALSE; } /* clear write protect */ if (!fu_genesys_gl32xx_device_cmd_clear_wp(self, error)) { g_prefix_error(error, "failed to clear WP: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), FU_GENESYS_GL32XX_CLEAR_WP_SLEEP_MS); /* write enable */ if (!fu_genesys_gl32xx_device_cmd_write_enable(self, error)) { g_prefix_error(error, "failed to write enable: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), FU_GENESYS_GL32XX_CLEAR_WP_SLEEP_MS); /* chip erase */ if (!fu_genesys_gl32xx_device_cmd_chip_erase(self, error)) { g_prefix_error(error, "failed to erase chip: "); return FALSE; } /* wait WIP to reset back to 0 */ if (!fu_genesys_gl32xx_device_cmd_wait_wip(self, error)) { g_prefix_error(error, "failed to wait WIP: "); return FALSE; } /* success */ return TRUE; } static void fu_genesys_gl32xx_device_to_string(FuDevice *device, guint idt, GString *str) { FuGenesysGl32xxDevice *self = FU_GENESYS_GL32XX_DEVICE(device); fwupd_codec_string_append(str, idt, "ChipName", self->chip_name); fwupd_codec_string_append_hex(str, idt, "BlockTransferSize", self->packetsz); fwupd_codec_string_append_hex(str, idt, "CustomerId", self->customer_id); } static gboolean fu_genesys_gl32xx_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuGenesysGl32xxDevice *self = FU_GENESYS_GL32XX_DEVICE(device); /* switch to internal, request and check chip ID */ if (!fu_genesys_gl32xx_device_cmd_switch_to_rom_mode(self, error)) return FALSE; /* get USB mode */ if (!fu_genesys_gl32xx_device_get_usb_mode(self, error)) return FALSE; if (!fu_genesys_gl32xx_device_verify_chip_id(self, error)) return FALSE; /* clear SR */ if (!fu_genesys_gl32xx_device_cmd_write_sr(self, error)) { g_prefix_error(error, "failed to clear SR: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_gl32xx_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuGenesysGl32xxDevice *self = FU_GENESYS_GL32XX_DEVICE(device); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return fu_genesys_gl32xx_device_cmd_reset_usb(self, error); } static gboolean fu_genesys_gl32xx_device_setup(FuDevice *device, GError **error) { FuGenesysGl32xxDevice *self = FU_GENESYS_GL32XX_DEVICE(device); g_autofree gchar *name = NULL; if (!fu_genesys_gl32xx_device_ensure_version(self, error)) return FALSE; if (!fu_genesys_gl32xx_device_ensure_rom_mode(self, error)) { g_prefix_error(error, "failed to check ROM mode: "); return FALSE; } /* if not detected above */ if (self->chip_name == NULL) fu_genesys_gl32xx_device_set_chip_name(self, "GL32xx"); if (fu_device_has_vendor_id(device, "BLOCK:0x05E3")) { name = g_strdup_printf("%s SD reader [0x%04X]", self->chip_name, fu_device_get_pid(device)); fu_device_set_name(device, name); } if (!fu_genesys_gl32xx_device_ensure_cid(self, error)) return FALSE; fu_genesys_gl32xx_device_ensure_enforce_requires(self); /* success */ return TRUE; } static GBytes * fu_genesys_gl32xx_device_dump_bytes(FuDevice *device, FuProgress *progress, GError **error) { FuGenesysGl32xxDevice *self = FU_GENESYS_GL32XX_DEVICE(device); const gsize fwsz = fu_device_get_firmware_size_max(device); g_autoptr(GPtrArray) chunks = NULL; g_autofree guint8 *buf = g_malloc0(fwsz); chunks = fu_chunk_array_mutable_new(buf, fwsz, 0x0, 0x0, self->packetsz); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_genesys_gl32xx_device_cmd_read_flash(self, fu_chunk_get_address(chk), fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "failed to read flash data on chunk 0x%x: ", i); return NULL; } fu_progress_step_done(progress); } return g_bytes_new_take(g_steal_pointer(&buf), fwsz); } static GBytes * fu_genesys_gl32xx_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuGenesysGl32xxDevice *self = FU_GENESYS_GL32XX_DEVICE(device); g_autoptr(GBytes) fw = NULL; g_autoptr(FuDeviceLocker) locker = fu_device_locker_new_full(FU_DEVICE(self), (FuDeviceLockerFunc)fu_genesys_gl32xx_device_detach, (FuDeviceLockerFunc)fu_genesys_gl32xx_device_attach, error); if (locker == NULL) return NULL; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); fw = fu_genesys_gl32xx_device_dump_bytes(device, progress, error); if (fw == NULL) return NULL; /* success */ return g_steal_pointer(&fw); } static FuFirmware * fu_genesys_gl32xx_device_read_firmware(FuDevice *device, FuProgress *progress, GError **error) { g_autoptr(FuFirmware) firmware = fu_genesys_gl32xx_firmware_new(); g_autoptr(GBytes) fw = NULL; fw = fu_genesys_gl32xx_device_dump_firmware(device, progress, error); if (fw == NULL) return NULL; if (!fu_firmware_parse_bytes(firmware, fw, 0x0, FU_FIRMWARE_PARSE_FLAG_CACHE_STREAM, error)) return NULL; /* success */ return g_steal_pointer(&firmware); } static FuFirmware * fu_genesys_gl32xx_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { g_autoptr(FuFirmware) firmware = fu_genesys_gl32xx_firmware_new(); if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; /* check size */ if (fu_firmware_get_size(firmware) != fu_device_get_firmware_size_max(device)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware size is [%" G_GSIZE_FORMAT "] bytes while expecting [%" G_GUINT64_FORMAT "] bytes", fu_firmware_get_size(firmware), fu_device_get_firmware_size_max(device)); return NULL; } /* TODO: validate compatibility? */ /* success */ return g_steal_pointer(&firmware); } static gboolean fu_genesys_gl32xx_device_write_block(FuGenesysGl32xxDevice *self, FuChunk *chunk, GError **error) { gsize addr = fu_chunk_get_address(chunk); gsize datasz = fu_chunk_get_data_sz(chunk); const guint8 *data = fu_chunk_get_data(chunk); guint8 cmd[] = {0xE5, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00}; /* build command */ if (!fu_memwrite_uint32_safe(cmd, sizeof(cmd), 2, addr, G_BIG_ENDIAN, error)) return FALSE; if (!fu_memwrite_uint16_safe(cmd, sizeof(cmd), 6, (guint16)datasz, G_BIG_ENDIAN, error)) return FALSE; if (!fu_block_device_sg_io_cmd_write(FU_BLOCK_DEVICE(self), cmd, sizeof(cmd), data, datasz, error)) { g_prefix_error(error, "failed to write flash data: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_gl32xx_device_write_blocks(FuGenesysGl32xxDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_genesys_gl32xx_device_write_block(self, chk, error)) { g_prefix_error(error, "failed on block 0x%x: ", i); return FALSE; } fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_genesys_gl32xx_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuGenesysGl32xxDevice *self = FU_GENESYS_GL32XX_DEVICE(device); g_autoptr(GBytes) fw = NULL; g_autoptr(GBytes) fw_read = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 80, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 10, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 9, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 1, NULL); /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* erase */ if (!fu_genesys_gl32xx_device_erase(self, error)) return FALSE; fu_progress_step_done(progress); /* write each block */ chunks = fu_chunk_array_new_from_bytes(fw, FU_GENESYS_GL32XX_FW_START_ADDR, FU_CHUNK_PAGESZ_NONE, self->packetsz); if (!fu_genesys_gl32xx_device_write_blocks(self, chunks, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* verify written data */ fw_read = fu_genesys_gl32xx_device_dump_bytes(device, progress, error); if (fw_read == NULL) return FALSE; fu_progress_step_done(progress); if (g_bytes_compare(fw, fw_read) != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "unable to verify written firmware"); return FALSE; } fu_progress_step_done(progress); /* write disable */ if (!fu_genesys_gl32xx_device_cmd_write_disable(self, error)) { g_prefix_error(error, "failed to write disable: "); return FALSE; } /* success */ return TRUE; } static void fu_genesys_gl32xx_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 55, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 45, "reload"); } static gboolean fu_genesys_gl32xx_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuGenesysGl32xxDevice *self = FU_GENESYS_GL32XX_DEVICE(device); guint64 tmp; /* load from quirks */ if (g_strcmp0(key, "GenesysGl32xxCompatibleModel") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->compatible_model = (guint16)tmp; return TRUE; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_genesys_gl32xx_device_init(FuGenesysGl32xxDevice *self) { self->packetsz = 64; fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_set_firmware_size(FU_DEVICE(self), FU_GENESYS_GL32XX_FW_SIZE); /* defaults to 64K */ fu_device_add_protocol(FU_DEVICE(self), "com.genesys.gl32xx"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_NONBLOCK); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_NO_SERIAL_NUMBER); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_NO_GENERIC_GUIDS); } static void fu_genesys_gl32xx_device_finalize(GObject *object) { FuGenesysGl32xxDevice *self = FU_GENESYS_GL32XX_DEVICE(object); g_free(self->chip_name); G_OBJECT_CLASS(fu_genesys_gl32xx_device_parent_class)->finalize(object); } static void fu_genesys_gl32xx_device_class_init(FuGenesysGl32xxDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->finalize = fu_genesys_gl32xx_device_finalize; device_class->to_string = fu_genesys_gl32xx_device_to_string; device_class->setup = fu_genesys_gl32xx_device_setup; device_class->detach = fu_genesys_gl32xx_device_detach; device_class->attach = fu_genesys_gl32xx_device_attach; device_class->dump_firmware = fu_genesys_gl32xx_device_dump_firmware; device_class->write_firmware = fu_genesys_gl32xx_device_write_firmware; device_class->read_firmware = fu_genesys_gl32xx_device_read_firmware; device_class->prepare_firmware = fu_genesys_gl32xx_device_prepare_firmware; device_class->set_progress = fu_genesys_gl32xx_device_set_progress; device_class->set_quirk_kv = fu_genesys_gl32xx_device_set_quirk_kv; } fwupd-2.0.10/plugins/genesys-gl32xx/fu-genesys-gl32xx-device.h000066400000000000000000000005661501337203100240240ustar00rootroot00000000000000/* * Copyright 2023 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_GENESYS_GL32XX_DEVICE (fu_genesys_gl32xx_device_get_type()) G_DECLARE_FINAL_TYPE(FuGenesysGl32xxDevice, fu_genesys_gl32xx_device, FU, GENESYS_GL32XX_DEVICE, FuBlockDevice) fwupd-2.0.10/plugins/genesys-gl32xx/fu-genesys-gl32xx-firmware.c000066400000000000000000000051241501337203100243670ustar00rootroot00000000000000/* * Copyright 2023 Denis Pynkin * Copyright 2023 Richard Hughes * Copyright 2023 Ben Chuang * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-genesys-gl32xx-firmware.h" #define FU_GENESYS_GL32XX_VERSION_ADDR 0x00D4 #define FU_GENESYS_GL32XX_CHECKSUM_MAGIC 0x0055 struct _FuGenesysGl32xxFirmware { FuFirmware parent_instance; }; G_DEFINE_TYPE(FuGenesysGl32xxFirmware, fu_genesys_gl32xx_firmware, FU_TYPE_FIRMWARE) static gboolean fu_genesys_gl32xx_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { guint8 ver[4] = {0}; g_autofree gchar *version = NULL; /* version */ if (!fu_input_stream_read_safe(stream, ver, sizeof(ver), 0x0, FU_GENESYS_GL32XX_VERSION_ADDR, sizeof(ver), error)) return FALSE; version = g_strdup_printf("%c%c%c%c", ver[0x0], ver[0x1], ver[0x2], ver[0x3]); fu_firmware_set_version(firmware, version); /* verify checksum */ if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { gsize streamsz = 0; guint8 chksum_actual = 0; guint8 chksum_expected = 0; g_autoptr(GInputStream) stream_tmp = NULL; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz < 2) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "image is too small"); return FALSE; } if (!fu_input_stream_read_u8(stream, streamsz - 1, &chksum_expected, error)) return FALSE; stream_tmp = fu_partial_input_stream_new(stream, 0, streamsz - 2, error); if (stream_tmp == NULL) return FALSE; if (!fu_input_stream_compute_sum8(stream_tmp, &chksum_actual, error)) return FALSE; if (FU_GENESYS_GL32XX_CHECKSUM_MAGIC - chksum_actual != chksum_expected) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "checksum mismatch, got 0x%02x, expected 0x%02x", chksum_actual, chksum_expected); return FALSE; } } /* success */ return TRUE; } static void fu_genesys_gl32xx_firmware_init(FuGenesysGl32xxFirmware *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); } static void fu_genesys_gl32xx_firmware_class_init(FuGenesysGl32xxFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_genesys_gl32xx_firmware_parse; } FuFirmware * fu_genesys_gl32xx_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_GENESYS_GL32XX_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/genesys-gl32xx/fu-genesys-gl32xx-firmware.h000066400000000000000000000006601501337203100243740ustar00rootroot00000000000000/* * Copyright 2023 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_GENESYS_GL32XX_FIRMWARE (fu_genesys_gl32xx_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuGenesysGl32xxFirmware, fu_genesys_gl32xx_firmware, FU, GENESYS_GL32XX_FIRMWARE, FuFirmware) FuFirmware * fu_genesys_gl32xx_firmware_new(void); fwupd-2.0.10/plugins/genesys-gl32xx/fu-genesys-gl32xx-plugin.c000066400000000000000000000021061501337203100240460ustar00rootroot00000000000000/* * Copyright 2023 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-genesys-gl32xx-device.h" #include "fu-genesys-gl32xx-firmware.h" #include "fu-genesys-gl32xx-plugin.h" struct _FuGenesysGl32xxPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuGenesysGl32xxPlugin, fu_genesys_gl32xx_plugin, FU_TYPE_PLUGIN) static void fu_genesys_gl32xx_plugin_init(FuGenesysGl32xxPlugin *self) { } static void fu_genesys_gl32xx_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_plugin_add_udev_subsystem(plugin, "block"); fu_plugin_add_device_gtype(plugin, FU_TYPE_GENESYS_GL32XX_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_GENESYS_GL32XX_FIRMWARE); fu_context_add_quirk_key(ctx, "GenesysGl32xxCompatibleModel"); } static void fu_genesys_gl32xx_plugin_class_init(FuGenesysGl32xxPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_genesys_gl32xx_plugin_constructed; } fwupd-2.0.10/plugins/genesys-gl32xx/fu-genesys-gl32xx-plugin.h000066400000000000000000000004441501337203100240560ustar00rootroot00000000000000/* * Copyright 2023 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuGenesysGl32xxPlugin, fu_genesys_gl32xx_plugin, FU, GENESYS_GL32XX_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/genesys-gl32xx/genesys-gl32xx.quirk000066400000000000000000000006011501337203100230510ustar00rootroot00000000000000[BLOCK\VEN_05E3] Vendor = Genesys [BLOCK\VEN_05E3&DEV_0749] Plugin = genesys_gl32xx FirmwareSize = 0x10000 [BLOCK\VEN_05E3&DEV_0764] Plugin = genesys_gl32xx FirmwareSize = 0x1C000 [BLOCK\VEN_32AC&DEV_0009] Plugin = genesys_gl32xx FirmwareSize = 0x1C000 Name = Framework SD Expansion Card Vendor = Framework # Compatible with Genesys PID 0x0764 GenesysGl32xxCompatibleModel = 0x0764 fwupd-2.0.10/plugins/genesys-gl32xx/meson.build000066400000000000000000000007631501337203100213450ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginGenesysGl32xx"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('genesys-gl32xx.quirk') plugin_builtins += static_library('fu_plugin_genesys_gl32xx', sources: [ 'fu-genesys-gl32xx-device.c', 'fu-genesys-gl32xx-firmware.c', 'fu-genesys-gl32xx-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/genesys/000077500000000000000000000000001501337203100160505ustar00rootroot00000000000000fwupd-2.0.10/plugins/genesys/README.md000066400000000000000000000071411501337203100173320ustar00rootroot00000000000000--- title: Plugin: Genesys Logic --- ## Introduction This plugin allows updating the Genesys Logic USB Hub devices. * GL3521 * GL3523 * GL3525 * GL3590 Additionally, this plugin allows updating the MStar Semiconductor Scaler connected via an I²C bus. * TSUM G ## Firmware Format The daemon will decompress the cabinet archives and extract the firmware blob in an unspecified binary file format. This plugin supports the following protocol IDs: * `com.genesys.usbhub` * `com.mstarsemi.scaler` ## GUID Generation These devices use the standard USB DeviceInstanceId values for the USB Hub, e.g. * `USB\VID_05E3&PID_0610` (quirk-only) These devices use the standard USB DeviceInstanceId values for the HID under USB hub, e.g. * `USB\VID_05E3&PID_0102` (quirk-only) Additionally, some customized instance IDs are added. e.g. * `USB\VID_03F0&PID_0610&IC_352330&BONDING_0F` * `USB\VID_03F0&PID_0610&VENDOR_GENESYSLOGIC&IC_352330&BONDING_0F&PORTNUM_23&VENDORSUP_C09B5DD3-1A23-51D2-995A-F7366AAB3CA4` * `USB\VID_05E3&PID_0630&PROJECT_1885D34D-0418-5EF8-8E69-4CEF77B6B6E8` * `USB\VID_03F0&PID_0610&PUBKEY_AB859399-95B8-5817-B521-9AD8CC7F5BD6` These devices also use custom GUID values for the Scaler, e.g. * `GENESYS_SCALER\MSTAR_TSUM_G&PUBKEY_B335BDCE-7073-5D0E-9BD3-9B69C1A6899F&PANELREV_RIM101` The Public Key is product-specific and is required to identify the product. ## Quirk Use This plugin uses the following plugin-specific quirks: ### `Flags=has-mstar-scaler` USB Hub has a MStar Semiconductor Scaler attached via I²C. Since 1.7.6. ### `Flags=has-public-key` Device has a public-key appended to firmware. Since 1.8.0 ### `Flags=pause-r2-cpu` Pause R2 CPU. Since 1.7.6 ### `Flags=use-i2c-ch0` Use I2C ch0. Since 1.7.6 ### GenesysUsbhubSwitchRequest USB Hub Switch Request value. * HP Mxfd FHD Monitors: `0xA1` Since 1.7.6. ### GenesysUsbhubReadRequest USB Hub Read Request value. * HP Mxfd FHD Monitors: `0xA2` Since 1.7.6. ### GenesysUsbhubWriteRequest USB Hub Write Request value. * HP Mxfd FHD Monitors: `0xA3` Since 1.7.6. ### use-i2c-ch0 Scalar uses I²C channel 0. Since 1.7.6. ### pause-r2-cpu Scalar pause R2 CPU. Since 1.7.6. ### GenesysScalerDeviceTransferSize Scaler Block size to use for transfers. * MStar Semiconductor TSUM G: `0x40` Since 1.7.6. ### GenesysScalerGpioOutputRegister Scaler GPIO Output Register value. * MStar Semiconductor TSUM G: `0x0426` Since 1.7.6. ### GenesysScalerGpioEnableRegister Scaler GPIO Enable Register value. * MStar Semiconductor TSUM G: `0x0428` Since 1.7.6. ### GenesysScalerGpioValue Scaler GPIO value. * MStar Semiconductor TSUM G: `0x01` Since 1.7.6. ### GenesysScalerCfiFlashId CFI Flash Id. * HP M24fd USB-C Monitor: `0xC22016` * HP M27fd USB-C Monitor: `0xC84016` Since 1.8.2. ## Update Behavior The devices are independently updated at runtime using USB control transfers. The firmware is deployed when the device is in normal runtime mode, and the device will reset when the new firmware has been written. The HP Mxfd FHD Monitors must be connected to host via the USB-C cable to apply an update. The devices remain functional during the update; the Scaler update is ~10 minute long. ## Vendor ID Security The vendor ID is set from the USB vendor, for example set to `USB:0x03F0` for HP. ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.7.6`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Adam Chen: @adamgene fwupd-2.0.10/plugins/genesys/fu-genesys-common.h000066400000000000000000000026161501337203100216010ustar00rootroot00000000000000/* * Copyright 2023 Adam.Chen * Copyright 2021 Gaël PORTAY * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include typedef struct { guint8 N[0x206]; guint8 E[0x00c]; } FuGenesysPublicKey; typedef struct { guint8 reg; guint8 expected_val; } FuGenesysWaitFlashRegisterHelper; typedef enum { ISP_MODEL_UNKNOWN, /* hub */ ISP_MODEL_HUB_GL3521, /* EOL */ ISP_MODEL_HUB_GL3523, ISP_MODEL_HUB_GL3510, ISP_MODEL_HUB_GL3590, ISP_MODEL_HUB_GL7000, ISP_MODEL_HUB_GL3525, /* pd */ ISP_MODEL_PD_GL9510, } FuGenesysModel; typedef struct { FuGenesysModel model; gint32 revision; } FuGenesysChip; #define GENESYS_USBHUB_FW_CONFIGURATION_OFFSET 0x100 #define GENESYS_USBHUB_FW_CONFIGURATION_WITHOUT_SERIAL 0x55 #define GENESYS_USBHUB_FW_CONFIGURATION_WITH_SERIAL 0xAA #define GENESYS_USBHUB_FW_CONFIGURATION_NEW_FORMAT 0xA5 #define GENESYS_USBHUB_FW_CONFIGURATION_NEW_FORMAT_V2 0xA6 #define GENESYS_USBHUB_CODE_SIZE_OFFSET 0xFB #define GENESYS_USBHUB_VERSION_OFFSET 0x10E #define GENESYS_USBHUB_STATIC_TOOL_STRING_OFFSET_GL3521 0x221 #define GENESYS_USBHUB_STATIC_TOOL_STRING_OFFSET_GL3523 0x221 #define GENESYS_USBHUB_STATIC_TOOL_STRING_OFFSET_GL3590 0x241 #define GENESYS_USBHUB_STATIC_TOOL_STRING_OFFSET_GL3525 0x251 #define GENESYS_USBHUB_STATIC_TOOL_STRING_OFFSET_GL3525_V2 0x1E1 fwupd-2.0.10/plugins/genesys/fu-genesys-hubhid-device.c000066400000000000000000000243311501337203100230020ustar00rootroot00000000000000/* * Copyright 2023 Adam.Chen * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-genesys-common.h" #include "fu-genesys-hubhid-device.h" #define GENESYS_HUBHID_REPORT_ID 0 #define GENESYS_HUBHID_REPORT_BYTE_LENGTH 0x40 #define GENESYS_HUBHID_REPORT_TIMEOUT 100 /* ms */ #define GENESYS_HUBHID_REPORT_FLAGS FU_HID_DEVICE_FLAG_ALLOW_TRUNC | FU_HID_DEVICE_FLAG_IS_FEATURE typedef union { struct { guint8 recipient : 2; /* FuUsbRecipient */ guint8 reserved : 3; guint8 type : 2; /* FuUsbRequestType */ guint8 dir : 1; /* FuUsbDirection */ }; guint8 bm; } FuGenesysUsbRequestType; typedef struct { FuGenesysUsbRequestType req_type; guint8 request; guint16 value; guint16 index; guint16 length; } FuGenesysUsbSetup; struct _FuGenesysHubhidDevice { FuHidDevice parent_instance; gboolean support_report_pack; guint16 report_length; guint16 max_report_pack_data_length; }; G_DEFINE_TYPE(FuGenesysHubhidDevice, fu_genesys_hubhid_device, FU_TYPE_HID_DEVICE) static gboolean fu_genesys_hubhid_device_command_read(FuGenesysHubhidDevice *self, FuGenesysUsbSetup *setup, guint8 *data, gsize datasz, FuProgress *progress, GError **error) { FuHidDevice *hid_device = FU_HID_DEVICE(self); g_autoptr(GPtrArray) chunks = NULL; g_autoptr(GByteArray) buf_report = g_byte_array_new(); g_return_val_if_fail(datasz == setup->length, FALSE); fu_byte_array_set_size(buf_report, self->report_length, 0); /* set request data */ if (!fu_memcpy_safe(buf_report->data, buf_report->len, 0, /* dst */ (const guint8 *)setup, sizeof(FuGenesysUsbSetup), 0x0, /* src */ sizeof(FuGenesysUsbSetup), error)) return FALSE; /* send request report */ if (!fu_hid_device_set_report(hid_device, GENESYS_HUBHID_REPORT_ID, buf_report->data, buf_report->len, GENESYS_HUBHID_REPORT_TIMEOUT, GENESYS_HUBHID_REPORT_FLAGS, error)) return FALSE; if (setup->length == 0) { g_warning("read zero-length hid report"); return TRUE; } /* receive report */ chunks = fu_chunk_array_mutable_new(data, setup->length, 0, 0x0, buf_report->len); if (progress != NULL) { fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); } for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); memset(buf_report->data, 0, buf_report->len); if (!fu_hid_device_get_report(hid_device, GENESYS_HUBHID_REPORT_ID, buf_report->data, buf_report->len, GENESYS_HUBHID_REPORT_TIMEOUT, GENESYS_HUBHID_REPORT_FLAGS | FU_HID_DEVICE_FLAG_RETRY_FAILURE, error)) { g_prefix_error(error, "error getting report at 0x%04x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } if (!fu_memcpy_safe(fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), 0, /* dst */ buf_report->data, buf_report->len, 0x0, /* src */ fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "error getting report data at 0x%04x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } if (progress != NULL) fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_genesys_hubhid_device_can_pack_report(FuGenesysHubhidDevice *self, guint16 data_length) { if (!self->support_report_pack) return FALSE; if (data_length > self->max_report_pack_data_length) return FALSE; return TRUE; } static gboolean fu_genesys_hubhid_device_command_write(FuGenesysHubhidDevice *self, FuGenesysUsbSetup *setup, const guint8 *data, gsize datasz, FuProgress *progress, GError **error) { FuHidDevice *hid_device = FU_HID_DEVICE(self); gboolean pack_report = FALSE; g_autoptr(GByteArray) buf_report = g_byte_array_new(); g_return_val_if_fail(datasz == setup->length, FALSE); fu_byte_array_set_size(buf_report, self->report_length, 0); /* set request data */ if (!fu_memcpy_safe(buf_report->data, buf_report->len, 0, /* dst */ (const guint8 *)setup, sizeof(FuGenesysUsbSetup), 0x0, /* src */ sizeof(FuGenesysUsbSetup), error)) return FALSE; /* pack report if it can */ pack_report = fu_genesys_hubhid_device_can_pack_report(self, setup->length); if (pack_report) { if (setup->length > 0 && !fu_memcpy_safe(buf_report->data, buf_report->len, sizeof(FuGenesysUsbSetup), /* dst */ data, datasz, 0x0, /* src */ setup->length, error)) { g_prefix_error(error, "error packing request data: "); return FALSE; } } /* send request report */ if (!fu_hid_device_set_report(hid_device, GENESYS_HUBHID_REPORT_ID, buf_report->data, buf_report->len, GENESYS_HUBHID_REPORT_TIMEOUT, GENESYS_HUBHID_REPORT_FLAGS, error)) return FALSE; /* command completed after packed report sent */ if (pack_report) return TRUE; /* send report */ if (setup->length > 0) { g_autoptr(GPtrArray) chunks = fu_chunk_array_new(data, setup->length, 0, 0, buf_report->len); if (progress != NULL) { fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); } for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); memset(buf_report->data, 0, buf_report->len); if (!fu_memcpy_safe(buf_report->data, buf_report->len, 0, /* dst */ fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0x0, /* src */ fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "error setting report data at 0x%04x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } if (!fu_hid_device_set_report(hid_device, GENESYS_HUBHID_REPORT_ID, buf_report->data, buf_report->len, GENESYS_HUBHID_REPORT_TIMEOUT, GENESYS_HUBHID_REPORT_FLAGS | FU_HID_DEVICE_FLAG_RETRY_FAILURE, error)) { g_prefix_error(error, "error setting report at 0x%04x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } if (progress != NULL) fu_progress_step_done(progress); } } /* finish report */ if (!fu_hid_device_get_report(hid_device, GENESYS_HUBHID_REPORT_ID, buf_report->data, buf_report->len, GENESYS_HUBHID_REPORT_TIMEOUT, GENESYS_HUBHID_REPORT_FLAGS, error)) { g_prefix_error(error, "error finishing report: "); return FALSE; } /* success */ return TRUE; } gboolean fu_genesys_hubhid_device_send_report(FuGenesysHubhidDevice *self, FuProgress *progress, FuUsbDirection direction, FuUsbRequestType request_type, FuUsbRecipient recipient, guint8 request, guint16 value, guint16 idx, guint8 *data, gsize datasz, GError **error) { g_autofree FuGenesysUsbSetup *setup = g_new0(FuGenesysUsbSetup, 1); setup->req_type.dir = (direction == FU_USB_DIRECTION_DEVICE_TO_HOST) ? 1 : 0; /* revert fu_usb in/out dir to usb spec */ setup->req_type.type = request_type; setup->req_type.recipient = recipient; setup->request = request; setup->value = value; setup->index = idx; setup->length = datasz; if (direction == FU_USB_DIRECTION_DEVICE_TO_HOST) { return fu_genesys_hubhid_device_command_read(self, setup, data, datasz, progress, error); } else { return fu_genesys_hubhid_device_command_write(self, setup, data, datasz, progress, error); } } static gboolean fu_genesys_hubhid_device_validate_token(FuGenesysHubhidDevice *self, GError **error) { g_autoptr(GByteArray) buf_hid_token = NULL; g_autoptr(GByteArray) buf_data = g_byte_array_new(); g_autofree FuGenesysUsbSetup *setup = g_new0(FuGenesysUsbSetup, 1); buf_hid_token = fu_utf8_to_utf16_byte_array("GLI HID", G_LITTLE_ENDIAN, FU_UTF_CONVERT_FLAG_NONE, error); if (buf_hid_token == NULL) return FALSE; /* get 0x80 string descriptor */ setup->req_type.bm = 0x80; setup->request = 0x06; setup->value = (0x03 << 8) | 0x80; setup->index = 0; setup->length = 0x40; fu_byte_array_set_size(buf_data, setup->length, 0); if (!fu_genesys_hubhid_device_command_read(self, setup, buf_data->data, buf_data->len, NULL, error)) return FALSE; if (!fu_memcmp_safe(buf_data->data, buf_data->len, 0x2, buf_hid_token->data, buf_hid_token->len, 0, buf_hid_token->len, error)) { g_prefix_error(error, "wrong HID token string: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_hubhid_device_probe(FuDevice *device, GError **error) { if (fu_usb_device_get_class(FU_USB_DEVICE(device)) != FU_USB_CLASS_INTERFACE_DESC) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "is not a hub hid"); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_hubhid_device_setup(FuDevice *device, GError **error) { FuGenesysHubhidDevice *self = FU_GENESYS_HUBHID_DEVICE(device); /* validate by string token */ if (!fu_genesys_hubhid_device_validate_token(self, error)) return FALSE; /* FuHidDevice->setup */ if (!FU_DEVICE_CLASS(fu_genesys_hubhid_device_parent_class)->setup(device, error)) { g_prefix_error(error, "error setting up device: "); return FALSE; } /* success */ return TRUE; } static void fu_genesys_hubhid_device_init(FuGenesysHubhidDevice *self) { self->support_report_pack = TRUE; self->report_length = GENESYS_HUBHID_REPORT_BYTE_LENGTH; self->max_report_pack_data_length = self->report_length - sizeof(FuGenesysUsbSetup); } static void fu_genesys_hubhid_device_class_init(FuGenesysHubhidDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_genesys_hubhid_device_probe; device_class->setup = fu_genesys_hubhid_device_setup; } fwupd-2.0.10/plugins/genesys/fu-genesys-hubhid-device.h000066400000000000000000000013331501337203100230040ustar00rootroot00000000000000/* * Copyright 2023 Adam.Chen * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_GENESYS_HUBHID_DEVICE (fu_genesys_hubhid_device_get_type()) G_DECLARE_FINAL_TYPE(FuGenesysHubhidDevice, fu_genesys_hubhid_device, FU, GENESYS_HUBHID_DEVICE, FuHidDevice) gboolean fu_genesys_hubhid_device_send_report(FuGenesysHubhidDevice *self, FuProgress *progress, FuUsbDirection direction, FuUsbRequestType request_type, FuUsbRecipient recipient, guint8 request, guint16 value, guint16 idx, guint8 *data, gsize datasz, GError **error); fwupd-2.0.10/plugins/genesys/fu-genesys-plugin.c000066400000000000000000000053161501337203100216020ustar00rootroot00000000000000/* * Copyright 2021 Ricardo Cañuelo * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-genesys-hubhid-device.h" #include "fu-genesys-plugin.h" #include "fu-genesys-scaler-firmware.h" #include "fu-genesys-usbhub-device.h" #include "fu-genesys-usbhub-firmware.h" struct _FuGenesysPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuGenesysPlugin, fu_genesys_plugin, FU_TYPE_PLUGIN) static void fu_genesys_plugin_init(FuGenesysPlugin *self) { } static void fu_genesys_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "GenesysScalerCfiFlashId"); fu_context_add_quirk_key(ctx, "GenesysScalerGpioOutputRegister"); fu_context_add_quirk_key(ctx, "GenesysScalerGpioEnableRegister"); fu_context_add_quirk_key(ctx, "GenesysScalerGpioValue"); fu_context_add_quirk_key(ctx, "GenesysUsbhubReadRequest"); fu_context_add_quirk_key(ctx, "GenesysUsbhubSwitchRequest"); fu_context_add_quirk_key(ctx, "GenesysUsbhubWriteRequest"); fu_plugin_add_device_gtype(plugin, FU_TYPE_GENESYS_USBHUB_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_GENESYS_HUBHID_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_GENESYS_USBHUB_FIRMWARE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_GENESYS_SCALER_FIRMWARE); } static FuDevice * fu_genesys_plugin_get_device_by_physical_id(FuPlugin *self, const gchar *physical_id) { GPtrArray *devices = fu_plugin_get_devices(self); for (guint i = 0; i < devices->len; i++) { FuDevice *dev = g_ptr_array_index(devices, i); if (!FU_IS_GENESYS_USBHUB_DEVICE(dev)) continue; if (g_strcmp0(fu_device_get_physical_id(dev), physical_id) == 0) return dev; } return NULL; } static void fu_genesys_plugin_device_added(FuPlugin *self, FuDevice *device) { FuDevice *parent = NULL; g_autoptr(FuDevice) usb_parent = NULL; /* link hid to parent hub */ if (!FU_IS_GENESYS_HUBHID_DEVICE(device)) return; usb_parent = fu_device_get_backend_parent(device, NULL); if (usb_parent == NULL) return; parent = fu_genesys_plugin_get_device_by_physical_id(self, fu_device_get_physical_id(usb_parent)); if (parent == NULL) { g_warning("hubhid cannot find parent, platform_id(%s)", fu_device_get_physical_id(usb_parent)); fu_plugin_device_remove(self, device); } else { fu_genesys_usbhub_device_set_hid_channel(parent, device); fu_device_add_child(parent, device); } } static void fu_genesys_plugin_class_init(FuGenesysPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_genesys_plugin_constructed; plugin_class->device_added = fu_genesys_plugin_device_added; } fwupd-2.0.10/plugins/genesys/fu-genesys-plugin.h000066400000000000000000000003571501337203100216070ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuGenesysPlugin, fu_genesys_plugin, FU, GENESYS_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/genesys/fu-genesys-scaler-device.c000066400000000000000000001553521501337203100230200ustar00rootroot00000000000000/* * Copyright 2021 Gaël PORTAY * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-genesys-common.h" #include "fu-genesys-scaler-device.h" #include "fu-genesys-scaler-firmware.h" #define GENESYS_SCALER_BANK_SIZE 0x200000U #define GENESYS_SCALER_MSTAR_READ 0x7a #define GENESYS_SCALER_MSTAR_WRITE 0x7b #define GENESYS_SCALER_MSTAR_DATA_OUT 0x7c #define GENESYS_SCALER_MSTAR_DATA_IN 0x7f #define GENESYS_SCALER_CMD_DDCCI_FIRMWARE_PACKET_VERSION 0x06 #define GENESYS_SCALER_CMD_DATA_WRITE 0x10 #define GENESYS_SCALER_CMD_DATA_READ 0x11 #define GENESYS_SCALER_CMD_DATA_END 0x12 #define GENESYS_SCALER_INFO 0xA4 #define GENESYS_SCALER_USB_TIMEOUT 5000 /* 5s */ #define FU_SCALER_FLAG_PAUSE_R2_CPU "pause-r2-cpu" #define FU_SCALER_FLAG_USE_I2C_CH0 "use-i2c-ch0" typedef struct { guint8 req_read; guint8 req_write; } FuGenesysVendorCommand; typedef struct { guint8 stage; guint8 model; guint8 major; guint8 minor; } FuGenesysScalerFirmwarePacketVersion; struct _FuGenesysScalerDevice { FuDevice parent_instance; guint8 level; FuGenesysPublicKey public_key; guint32 cfi_flash_id; FuCfiDevice *cfi_device; FuGenesysVendorCommand vc; guint32 sector_size; guint32 page_size; guint32 transfer_size; guint16 gpio_out_reg; guint16 gpio_en_reg; guint8 gpio_val; }; G_DEFINE_TYPE(FuGenesysScalerDevice, fu_genesys_scaler_device, FU_TYPE_DEVICE) static gboolean fu_genesys_scaler_device_enter_serial_debug_mode(FuGenesysScalerDevice *self, GError **error) { FuDevice *parent_device = fu_device_get_parent(FU_DEVICE(self)); guint8 data[] = {0x53, 0x45, 0x52, 0x44, 0x42}; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0001, /* value */ 0x0000, /* idx */ data, /* data */ sizeof(data), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error entering Serial Debug Mode: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), 1); /* ms */ /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_exit_serial_debug_mode(FuGenesysScalerDevice *self, GError **error) { FuDevice *parent_device = fu_device_get_parent(FU_DEVICE(self)); guint8 data[] = {0x45}; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0001, /* value */ 0x0000, /* idx */ data, /* data */ sizeof(data), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error exiting Serial Debug Mode: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_enter_single_step_mode(FuGenesysScalerDevice *self, GError **error) { FuDevice *parent_device = fu_device_get_parent(FU_DEVICE(self)); guint8 data1[] = {0x10, 0xc0, 0xc1, 0x53}; guint8 data2[] = {0x10, 0x1f, 0xc1, 0x53}; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0001, /* value */ 0x0000, /* idx */ data1, /* data */ sizeof(data1), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error entering Single Step Mode: "); return FALSE; } if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0001, /* value */ 0x0000, /* idx */ data2, /* data */ sizeof(data2), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error entering Single Step Mode: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_exit_single_step_mode(FuGenesysScalerDevice *self, GError **error) { FuDevice *parent_device = fu_device_get_parent(FU_DEVICE(self)); guint8 data[] = {0x10, 0xc0, 0xc1, 0xff}; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0001, /* value */ 0x0000, /* idx */ data, /* data */ sizeof(data), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error exiting Single Step Mode: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_enter_debug_mode(FuGenesysScalerDevice *self, GError **error) { FuDevice *parent_device = fu_device_get_parent(FU_DEVICE(self)); guint8 data[] = {0x10, 0x00, 0x00, 0x00}; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0001, /* value */ 0x0000, /* idx */ data, /* data */ sizeof(data), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error entering Debug Mode: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_mst_i2c_bus_ctrl(FuGenesysScalerDevice *self, GError **error) { FuDevice *parent_device = fu_device_get_parent(FU_DEVICE(self)); guint8 data[] = {0x35, 0x71}; for (guint i = 0; i < sizeof(data); i++) { if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0001, /* value */ 0x0000, /* idx */ &data[i], /* data */ sizeof(data[i]), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error sending i2c bus ctrl 0x%02x: ", data[i]); return FALSE; } } /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_mst_i2c_bus_switch_to_ch0(FuGenesysScalerDevice *self, GError **error) { FuDevice *parent_device = fu_device_get_parent(FU_DEVICE(self)); guint8 data[] = {0x80, 0x82, 0x84, 0x51, 0x7f, 0x37, 0x61}; for (guint i = 0; i < sizeof(data); i++) { if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0001, /* value */ 0x0000, /* idx */ &data[i], /* data */ sizeof(data[i]), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error sending i2c bus ch0 0x%02x: ", data[i]); return FALSE; } } /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_mst_i2c_bus_switch_to_ch4(FuGenesysScalerDevice *self, GError **error) { FuDevice *parent_device = fu_device_get_parent(FU_DEVICE(self)); guint8 data[] = {0x80, 0x82, 0x85, 0x53, 0x7f}; for (guint i = 0; i < sizeof(data); i++) { if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0001, /* value */ 0x0000, /* idx */ &data[i], /* data */ sizeof(data[i]), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error sending i2c bus ch4 0x%02x: ", data[i]); return FALSE; } } /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_disable_wp(FuGenesysScalerDevice *self, gboolean disable, GError **error) { FuDevice *parent_device = fu_device_get_parent(FU_DEVICE(self)); guint8 data_out[] = {0x10, 0x00 /* gpio_out_reg_h */, 0x00 /* gpio_out_reg_l */, 0x00 /* gpio_out_val */}; guint8 data_en[] = {0x10, 0x00 /* gpio_en_reg_h */, 0x00 /* gpio_en_reg_l */, 0x00 /* gpio_en_val */}; /* disable write protect [output] */ data_out[1] = (self->gpio_out_reg & 0xff00) >> 8; data_out[2] = self->gpio_out_reg & 0x00ff; /* read gpio-out register */ if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0003, /* value */ 0x0000, /* idx */ data_out, /* data */ sizeof(data_out) - 1, /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error reading GPIO-Out Register 0x%02x%02x: ", data_out[1], data_out[2]); return FALSE; } if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_read, 0x0003, /* value */ 0x0000, /* idx */ &data_out[3], /* data */ sizeof(data_out[3]), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error reading GPIO-Out Register 0x%02x%02x: ", data_out[1], data_out[2]); return FALSE; } if (data_out[3] == 0xff) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "error reading GPIO-Out Register 0x%02x%02x: ", data_out[1], data_out[2]); return FALSE; } if (disable) data_out[3] |= self->gpio_val; /* pull high */ else data_out[3] &= ~self->gpio_val; /* pull low */ /* write gpio-out register */ if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0001, /* value */ 0x0000, /* idx */ data_out, /* data */ sizeof(data_out), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error writing GPIO-Out Register 0x%02x%02x=0x%02x: ", data_out[1], data_out[2], data_out[3]); return FALSE; } /* disable write protect [enable] */ data_en[1] = (self->gpio_en_reg & 0xff00) >> 8; data_en[2] = self->gpio_en_reg & 0x00ff; /* read gpio-enable register */ if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0003, /* value */ 0x0000, /* idx */ data_en, /* data */ sizeof(data_en) - 1, /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error writing GPIO-Enable Register 0x%02x%02x: ", data_en[1], data_en[2]); return FALSE; } if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_read, 0x0003, /* value */ 0x0000, /* idx */ &data_en[3], /* data */ sizeof(data_en[3]), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error reading GPIO-Out Register 0x%02x%02x: ", data_en[1], data_en[2]); return FALSE; } if (data_en[3] == 0xff) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "error reading GPIO-Enable Register 0x%02x%02x: ", data_en[1], data_en[2]); return FALSE; } data_en[3] &= ~self->gpio_val; /* write gpio-enable register */ if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0001, /* value */ 0x0000, /* idx */ data_en, /* data */ sizeof(data_en), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error writing GPIO-Enable Register 0x%02x%02x=0x%02x: ", data_en[1], data_en[2], data_en[3]); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_pause_r2_cpu(FuGenesysScalerDevice *self, GError **error) { FuDevice *parent_device = fu_device_get_parent(FU_DEVICE(self)); guint8 data[] = {0x10, 0x00, 0x10, 0x0F, 0xD7, 0x00}; /* * MST9U only! * * Pause R2 CPU for preventing Scaler entering Power Saving Mode also * need for Disable SPI Flash Write Protect Mode. */ if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0003, /* value */ 0x0000, /* idx */ data, /* data */ sizeof(data) - 1, /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error reading register 0x%02x%02x%02x%02x%02x: ", data[0], data[1], data[2], data[3], data[4]); return FALSE; } if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_read, 0x0003, /* value */ 0x0000, /* idx */ &data[5], /* data */ sizeof(data[5]), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error reading register 0x%02x%02x%02x%02x%02x: ", data[0], data[1], data[2], data[3], data[4]); return FALSE; } if (data[5] == 0xff) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "error reading register 0x%02x%02x%02x%02x%02x: ", data[0], data[1], data[2], data[3], data[4]); return FALSE; } data[5] |= 0x80; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0003, /* value */ 0x0000, /* idx */ data, /* data */ sizeof(data), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error writing register 0x%02x%02x%02x%02x%02x: ", data[0], data[1], data[2], data[3], data[4]); return FALSE; } fu_device_sleep(FU_DEVICE(self), 200); /* ms */ /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_set_isp_mode(FuDevice *device, gpointer user_data, GError **error) { FuGenesysScalerDevice *self = user_data; FuDevice *parent_device = fu_device_get_parent(FU_DEVICE(self)); guint8 data[] = {0x4d, 0x53, 0x54, 0x41, 0x52}; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0000, /* value */ 0x0000, /* idx */ data, /* data */ sizeof(data), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { return FALSE; } fu_device_sleep(device, 1); /* ms */ /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_enter_isp_mode(FuGenesysScalerDevice *self, GError **error) { /* * Enter ISP mode: * * Note: MStar application note say execute twice to avoid race * condition */ if (!fu_device_retry_full(FU_DEVICE(self), fu_genesys_scaler_device_set_isp_mode, 2, 1000 /* 1ms */, self, error)) { g_prefix_error(error, "error entering ISP mode: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_exit_isp_mode(FuGenesysScalerDevice *self, GError **error) { FuDevice *parent_device = fu_device_get_parent(FU_DEVICE(self)); guint8 data[] = {0x24}; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0000, /* value */ 0x0000, /* idx */ data, /* data */ sizeof(data), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error exiting ISP mode: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuGenesysScalerDevice *self = FU_GENESYS_SCALER_DEVICE(device); /* * Important: do not change the order below; otherwise, unexpected * condition occurs. */ if (!fu_genesys_scaler_device_enter_serial_debug_mode(self, error)) return FALSE; if (!fu_genesys_scaler_device_enter_single_step_mode(self, error)) return FALSE; if (fu_device_has_private_flag(device, FU_SCALER_FLAG_USE_I2C_CH0)) if (!fu_genesys_scaler_device_mst_i2c_bus_switch_to_ch0(self, error)) return FALSE; if (!fu_genesys_scaler_device_enter_debug_mode(self, error)) return FALSE; if (!fu_genesys_scaler_device_mst_i2c_bus_ctrl(self, error)) return FALSE; if (!fu_genesys_scaler_device_disable_wp(self, TRUE, error)) return FALSE; if (fu_device_has_private_flag(device, FU_SCALER_FLAG_PAUSE_R2_CPU)) { if (!fu_genesys_scaler_device_mst_i2c_bus_switch_to_ch4(self, error)) return FALSE; if (!fu_genesys_scaler_device_mst_i2c_bus_ctrl(self, error)) return FALSE; if (!fu_genesys_scaler_device_pause_r2_cpu(self, error)) return FALSE; } if (!fu_genesys_scaler_device_enter_isp_mode(self, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuGenesysScalerDevice *self = FU_GENESYS_SCALER_DEVICE(device); if (!fu_genesys_scaler_device_exit_single_step_mode(self, error)) return FALSE; if (!fu_genesys_scaler_device_exit_serial_debug_mode(self, error)) return FALSE; if (!fu_genesys_scaler_device_exit_isp_mode(self, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_get_level(FuGenesysScalerDevice *self, guint8 *level, GError **error) { FuDevice *parent_device = fu_device_get_parent(FU_DEVICE(self)); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, GENESYS_SCALER_INFO, 0x0004, /* value */ 0x0000, /* idx */ level, /* data */ 1, /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error getting level: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), 100); /* ms */ /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_get_version(FuGenesysScalerDevice *self, guint8 *buf, guint bufsz, GError **error) { FuDevice *parent_device = fu_device_get_parent(FU_DEVICE(self)); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, GENESYS_SCALER_INFO, 0x0005, /* value */ 0x0000, /* idx */ buf, /* data */ bufsz, /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error getting version: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), 100); /* ms */ /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_get_public_key(FuGenesysScalerDevice *self, guint8 *buf, guint bufsz, GError **error) { FuDevice *parent_device = fu_device_get_parent(FU_DEVICE(self)); const gsize data_size = 0x20; g_autoptr(GPtrArray) chunks = NULL; chunks = fu_chunk_array_mutable_new(buf, bufsz, 0, 0, data_size); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, GENESYS_SCALER_INFO, 0x0006, /* value */ fu_chunk_get_address(chk), /* idx */ fu_chunk_get_data_out(chk), /* data */ fu_chunk_get_data_sz(chk), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error getting public key: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), 100); /* ms */ } /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_read_flash(FuGenesysScalerDevice *self, guint addr, guint8 *buf, guint bufsz, FuProgress *progress, GError **error) { FuDevice *parent_device = fu_device_get_parent(FU_DEVICE(self)); guint8 data1[] = { GENESYS_SCALER_CMD_DATA_WRITE, 0x00, /* Read Data command */ (addr & 0x00ff0000) >> 16, (addr & 0x0000ff00) >> 8, (addr & 0x000000ff), }; guint8 data2[] = { GENESYS_SCALER_CMD_DATA_READ, }; guint8 data3[] = { GENESYS_SCALER_CMD_DATA_END, }; g_autoptr(GPtrArray) chunks = NULL; if (!fu_cfi_device_get_cmd(self->cfi_device, FU_CFI_DEVICE_CMD_READ_DATA, &data1[1], error)) return FALSE; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0000, /* value */ 0x0000, /* idx */ data1, /* data */ sizeof(data1), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error reading flash at 0x%06x: ", addr); return FALSE; } if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0000, /* value */ 0x0000, /* idx */ data2, /* data */ sizeof(data2), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error reading flash at 0x%06x: ", addr); return FALSE; } chunks = fu_chunk_array_mutable_new(buf, bufsz, addr, 0, self->transfer_size); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_read, 0x0000, /* value */ 0x0000, /* idx */ fu_chunk_get_data_out(chk), /* data */ fu_chunk_get_data_sz(chk), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error reading flash at 0x%06x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } fu_progress_step_done(progress); } if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0000, /* value */ 0x0000, /* idx */ data3, /* data */ sizeof(data3), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error reading flash at 0x%06x: ", addr); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_wait_flash_control_register_cb(FuDevice *dev, gpointer user_data, GError **error) { FuGenesysScalerDevice *self = FU_GENESYS_SCALER_DEVICE(dev); FuDevice *parent_device = fu_device_get_parent(FU_DEVICE(self)); FuGenesysWaitFlashRegisterHelper *helper = user_data; guint8 status = 0; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_read, helper->reg << 8 | 0x04, /* value */ 0x0000, /* idx */ &status, /* data */ sizeof(status), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error reading flash control register: "); return FALSE; } if ((status & 0x81) != helper->expected_val) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "wrong value in flash control register"); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_flash_control_write_enable(FuGenesysScalerDevice *self, GError **error) { FuDevice *parent_device = fu_device_get_parent(FU_DEVICE(self)); guint8 data1[] = { GENESYS_SCALER_CMD_DATA_WRITE, 0x00, /* Write Enable command */ }; guint8 data2[] = { GENESYS_SCALER_CMD_DATA_END, }; if (!fu_cfi_device_get_cmd(self->cfi_device, FU_CFI_DEVICE_CMD_WRITE_EN, &data1[1], error)) return FALSE; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0000, /* value */ 0x0000, /* idx */ data1, /* data */ sizeof(data1), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error sending flash control write enable: "); return FALSE; } if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0000, /* value */ 0x0000, /* idx */ data2, /* data */ sizeof(data2), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error sending flash control write enable: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_flash_control_write_status(FuGenesysScalerDevice *self, guint8 status, GError **error) { FuDevice *parent_device = fu_device_get_parent(FU_DEVICE(self)); guint8 data1[] = { GENESYS_SCALER_CMD_DATA_WRITE, 0x00, /* Write Status command */ status, }; guint8 data2[] = { GENESYS_SCALER_CMD_DATA_END, }; if (!fu_cfi_device_get_cmd(self->cfi_device, FU_CFI_DEVICE_CMD_WRITE_STATUS, &data1[1], error)) return FALSE; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0000, /* value */ 0x0000, /* idx */ data1, /* data */ sizeof(data1), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error sending flash control write status 0x%02x: ", status); return FALSE; } if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0000, /* value */ 0x0000, /* idx */ data2, /* data */ sizeof(data2), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error sending flash control write status 0x%02x: ", status); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_flash_control_sector_erase(FuGenesysScalerDevice *self, guint addr, GError **error) { FuDevice *parent_device = fu_device_get_parent(FU_DEVICE(self)); FuGenesysWaitFlashRegisterHelper helper = { .reg = 0x00, /* Read Status command */ .expected_val = 0, }; guint8 data1[] = { GENESYS_SCALER_CMD_DATA_WRITE, 0x00, /* Sector Erase command */ (addr & 0x00ff0000) >> 16, (addr & 0x0000ff00) >> 8, (addr & 0x000000ff), }; guint8 data2[] = { GENESYS_SCALER_CMD_DATA_END, }; if (!fu_cfi_device_get_cmd(self->cfi_device, FU_CFI_DEVICE_CMD_READ_STATUS, &helper.reg, error)) return FALSE; if (!fu_cfi_device_get_cmd(self->cfi_device, FU_CFI_DEVICE_CMD_SECTOR_ERASE, &data1[1], error)) return FALSE; if (!fu_genesys_scaler_device_flash_control_write_enable(self, error)) return FALSE; if (!fu_genesys_scaler_device_flash_control_write_status(self, 0x00, error)) return FALSE; /* 5s */ if (!fu_device_retry_full(FU_DEVICE(self), fu_genesys_scaler_device_wait_flash_control_register_cb, 100, 50, /* 50ms */ &helper, error)) { g_prefix_error(error, "error waiting for flash control read status register: "); return FALSE; } if (!fu_genesys_scaler_device_flash_control_write_enable(self, error)) return FALSE; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0000, /* value */ 0x0000, /* idx */ data1, /* data */ sizeof(data1), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error sending flash control erase at address 0x%06x: ", addr); return FALSE; } if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, 0x0000, /* value */ 0x0000, /* idx */ data2, /* data */ sizeof(data2), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error sending flash control erase at address 0x%06x: ", addr); return FALSE; } /* 5s */ if (!fu_device_retry_full(FU_DEVICE(self), fu_genesys_scaler_device_wait_flash_control_register_cb, 100, 50, /* 50ms */ &helper, error)) { g_prefix_error(error, "error waiting for flash control read status register: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_erase_flash(FuGenesysScalerDevice *self, guint addr, guint bufsz, FuProgress *progress, GError **error) { g_autoptr(GPtrArray) chunks = NULL; chunks = fu_chunk_array_new(NULL, bufsz, addr, 0, self->sector_size); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_genesys_scaler_device_flash_control_sector_erase(self, fu_chunk_get_address(chk), error)) { g_prefix_error(error, "error erasing flash at address 0x%06x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_flash_control_page_program(FuGenesysScalerDevice *self, guint addr, const guint8 *buf, guint bufsz, FuProgress *progress, GError **error) { FuDevice *parent_device = fu_device_get_parent(FU_DEVICE(self)); FuGenesysWaitFlashRegisterHelper helper = { .reg = 0x00, /* Read Status command */ .expected_val = 0, }; guint8 data1[] = { GENESYS_SCALER_CMD_DATA_WRITE, 0x00, /* Page Program command */ (addr & 0x00ff0000) >> 16, (addr & 0x0000ff00) >> 8, (addr & 0x000000ff), }; gsize datasz = 0; g_autoptr(GPtrArray) chunks = NULL; g_autofree guint8 *data = NULL; if (!fu_cfi_device_get_cmd(self->cfi_device, FU_CFI_DEVICE_CMD_READ_STATUS, &helper.reg, error)) return FALSE; if (!fu_cfi_device_get_cmd(self->cfi_device, FU_CFI_DEVICE_CMD_PAGE_PROG, &data1[1], error)) return FALSE; datasz = sizeof(data1) + bufsz; data = g_malloc0(datasz); if (!fu_memcpy_safe(data, datasz, 0, /* dst */ data1, sizeof(data1), 0, /* src */ sizeof(data1), error)) return FALSE; if (!fu_memcpy_safe(data, datasz, sizeof(data1), /* dst */ buf, bufsz, 0, /* src */ bufsz, error)) return FALSE; chunks = fu_chunk_array_mutable_new(data, datasz, addr + sizeof(data1), 0, self->transfer_size); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); guint16 index = 0x0010 * (i + 1); /* last chunk */ if ((i + 1) == chunks->len) index |= 0x0080; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vc.req_write, index, /* value */ 0x0000, /* idx */ fu_chunk_get_data_out(chk), /* data */ fu_chunk_get_data_sz(chk), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error( error, "error sending flash control page program at address 0x%06x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } fu_progress_step_done(progress); } /* 200ms */ if (!fu_device_retry(FU_DEVICE(self), fu_genesys_scaler_device_wait_flash_control_register_cb, 20, &helper, error)) { g_prefix_error(error, "error waiting for flash control read status register: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_write_sector(FuGenesysScalerDevice *self, guint addr, const guint8 *buf, guint bufsz, FuProgress *progress, GError **error) { g_autoptr(GPtrArray) chunks = NULL; chunks = fu_chunk_array_new(buf, bufsz, addr, 0, self->page_size); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_genesys_scaler_device_flash_control_page_program( self, fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_write_flash(FuGenesysScalerDevice *self, guint addr, const guint8 *buf, guint bufsz, FuProgress *progress, GError **error) { g_autoptr(GPtrArray) chunks = NULL; chunks = fu_chunk_array_new(buf, bufsz, addr, 0, self->sector_size); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_genesys_scaler_device_write_sector(self, fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static guint8 fu_genesys_scaler_device_calculate_checksum(const guint8 *buf, gsize bufsz) { guint8 checksum = 0x00; for (gsize i = 0; i < bufsz; i++) checksum ^= buf[i]; return checksum; } static gboolean fu_genesys_scaler_device_get_ddcci_data(FuGenesysScalerDevice *self, guint8 cmd, guint8 *buf, guint bufsz, GError **error) { FuDevice *parent_device = fu_device_get_parent(FU_DEVICE(self)); guint8 data[] = {0x6e, 0x51, 0x83, 0xcd, 0x01, 0x00 /* command */, 0x00 /* checksum */}; data[5] = cmd; data[6] = fu_genesys_scaler_device_calculate_checksum(data, 6); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, GENESYS_SCALER_MSTAR_DATA_OUT, 0x0000, /* value */ 0x0000, /* idx */ data, /* data */ sizeof(data), /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error setting dddci data: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), 100); /* 1ms */ if (!fu_usb_device_control_transfer(FU_USB_DEVICE(parent_device), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, GENESYS_SCALER_MSTAR_DATA_IN, 0x0001, /* value */ 0x0000, /* idx */ buf, /* data */ bufsz, /* data length */ NULL, /* actual length */ GENESYS_SCALER_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error getting dddci data: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), 100); /* ms */ /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_get_firmware_packet_version(FuGenesysScalerDevice *self, FuGenesysScalerFirmwarePacketVersion *ver, GError **error) { guint8 buf[0x40]; guint8 offset = 4; if (!fu_genesys_scaler_device_get_ddcci_data( self, GENESYS_SCALER_CMD_DDCCI_FIRMWARE_PACKET_VERSION, buf, sizeof(buf), error)) return FALSE; if (buf[0] == 0x6f && buf[1] == 0x6e) { gsize len = buf[2] ^ 0x80; guint8 checksum; guint8 checksum_tmp = 0x0; if (len > sizeof(buf) - 3) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "error dddci length too large, got 0x%x, expected <= 0x%zx: ", (guint)len, sizeof(buf)); return FALSE; } buf[0] = 0x50; /* drifted value */ checksum = fu_genesys_scaler_device_calculate_checksum(buf, len + 3); if (!fu_memread_uint8_safe(buf, sizeof(buf), len + 3, &checksum_tmp, error)) return FALSE; if (checksum_tmp != checksum) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "error dddci checksum mismatch, got 0x%02x, expected 0x%02x", checksum_tmp, checksum); return FALSE; } offset = 7; } ver->stage = buf[offset]; ver->model = buf[offset + 1]; ver->major = buf[offset + 2]; ver->minor = buf[offset + 3]; /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_probe(FuDevice *device, GError **error) { FuGenesysScalerDevice *self = FU_GENESYS_SCALER_DEVICE(device); FuGenesysScalerFirmwarePacketVersion ver; guint8 buf[7 + 1] = {0}; g_autofree gchar *guid = NULL; g_autofree gchar *version = NULL; g_autofree gchar *panelrev = NULL; if (!fu_genesys_scaler_device_get_level(self, &self->level, error)) return FALSE; if (!fu_genesys_scaler_device_get_public_key(self, (guint8 *)&self->public_key, sizeof(self->public_key), error)) return FALSE; if (memcmp(self->public_key.N, "N = ", 4) != 0 || memcmp(self->public_key.E, "E = ", 4) != 0) { fu_dump_raw(G_LOG_DOMAIN, "PublicKey", (const guint8 *)&self->public_key, sizeof(self->public_key)); g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_SIGNATURE_INVALID, "invalid public-key"); return FALSE; } guid = fwupd_guid_hash_data((const guint8 *)&self->public_key, sizeof(self->public_key), FWUPD_GUID_FLAG_NONE); if (!fu_genesys_scaler_device_get_version(self, buf, sizeof(buf), error)) return FALSE; /* ?xIM123; where ? is 0x06 (length?) */ panelrev = fu_memstrsafe(buf, sizeof(buf), 0x1, 6, error); if (panelrev == NULL) return FALSE; if (!fu_genesys_scaler_device_get_firmware_packet_version(self, &ver, error)) return FALSE; version = g_strdup_printf("%d.%d.%d.%d", ver.stage, ver.model, ver.major, ver.minor); fu_device_set_version(device, version); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_logical_id(device, "scaler"); /* add instance ID */ fu_device_add_instance_str(device, "MSTAR", "TSUM_G"); fu_device_add_instance_strup(device, "PUBKEY", guid); fu_device_add_instance_strup(device, "PANELREV", panelrev); if (!fu_device_build_instance_id(device, error, "GENESYS_SCALER", "MSTAR", "PUBKEY", NULL)) return FALSE; if (!fu_device_build_instance_id(device, error, "GENESYS_SCALER", "MSTAR", "PUBKEY", "PANELREV", NULL)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); self->vc.req_read = GENESYS_SCALER_MSTAR_READ; self->vc.req_write = GENESYS_SCALER_MSTAR_WRITE; if (self->level != 1) { self->vc.req_read += 3; self->vc.req_write += 3; } /* success */ return TRUE; } static gboolean fu_genesys_scaler_device_setup(FuDevice *device, GError **error) { FuGenesysScalerDevice *self = FU_GENESYS_SCALER_DEVICE(device); guint64 size_min = fu_device_get_firmware_size_max(device); guint64 size; guint32 sector_size; guint32 page_size; g_autofree gchar *flash_id = NULL; flash_id = g_strdup_printf("%06X", self->cfi_flash_id); self->cfi_device = fu_cfi_device_new(fu_device_get_context(FU_DEVICE(self)), flash_id); if (!fu_device_setup(FU_DEVICE(self->cfi_device), error)) return FALSE; sector_size = fu_cfi_device_get_sector_size(self->cfi_device); if (sector_size != 0) self->sector_size = sector_size; page_size = fu_cfi_device_get_page_size(self->cfi_device); if (page_size != 0) self->page_size = page_size; if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_DUAL_IMAGE)) size_min *= 2; size = fu_device_get_firmware_size_max(FU_DEVICE(self->cfi_device)); if (size != 0 && size < size_min) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "CFI device too small, got 0x%x, expected >= 0x%x", (guint)size, (guint)size_min); return FALSE; } /* success */ return TRUE; } static GBytes * fu_genesys_scaler_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuGenesysScalerDevice *self = FU_GENESYS_SCALER_DEVICE(device); gsize size = fu_cfi_device_get_size(self->cfi_device); g_autofree guint8 *buf = NULL; g_autoptr(FuDeviceLocker) locker = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 99, NULL); /* require detach -> attach */ locker = fu_device_locker_new_full(device, (FuDeviceLockerFunc)fu_device_detach, (FuDeviceLockerFunc)fu_device_attach, error); if (locker == NULL) return NULL; fu_progress_step_done(progress); buf = g_malloc0(size); if (!fu_genesys_scaler_device_read_flash(self, 0, buf, size, fu_progress_get_child(progress), error)) return NULL; fu_progress_step_done(progress); /* success */ return g_bytes_new_take(g_steal_pointer(&buf), size); } static FuFirmware * fu_genesys_scaler_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuGenesysScalerDevice *self = FU_GENESYS_SCALER_DEVICE(device); g_autoptr(FuFirmware) firmware = fu_genesys_scaler_firmware_new(); g_autoptr(GBytes) blob_payload = NULL; g_autoptr(GBytes) blob_public_key = NULL; /* parse firmware */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; /* check public-key */ blob_public_key = fu_firmware_get_image_by_id_bytes(firmware, FU_FIRMWARE_ID_SIGNATURE, error); if (blob_public_key == NULL) return NULL; fu_dump_raw(G_LOG_DOMAIN, "PublicKey", g_bytes_get_data(blob_public_key, NULL), g_bytes_get_size(blob_public_key)); if (memcmp(g_bytes_get_data(blob_public_key, NULL), &self->public_key, sizeof(self->public_key)) != 0 && (flags & FWUPD_INSTALL_FLAG_FORCE) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_SIGNATURE_INVALID, "mismatch public-key"); return NULL; } /* check size */ blob_payload = fu_firmware_get_image_by_id_bytes(firmware, FU_FIRMWARE_ID_PAYLOAD, error); if (blob_payload == NULL) return NULL; if (g_bytes_get_size(blob_payload) > fu_device_get_firmware_size_max(device)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware too large, got 0x%x, expected <= 0x%x", (guint)g_bytes_get_size(blob_payload), (guint)fu_device_get_firmware_size_max(device)); return NULL; } /* success */ return g_steal_pointer(&firmware); } static gboolean fu_genesys_scaler_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuGenesysScalerDevice *self = FU_GENESYS_SCALER_DEVICE(device); guint addr = 0; gsize size; const guint8 *data; g_autofree guint8 *buf = NULL; g_autoptr(FuFirmware) payload = NULL; g_autoptr(GBytes) fw_payload = NULL; fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 4, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 54, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 42, NULL); if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_DUAL_IMAGE)) addr = GENESYS_SCALER_BANK_SIZE; payload = fu_firmware_get_image_by_id(firmware, FU_FIRMWARE_ID_PAYLOAD, error); if (payload == NULL) return FALSE; fw_payload = fu_firmware_get_bytes(payload, error); if (fw_payload == NULL) return FALSE; data = g_bytes_get_data(fw_payload, &size); if (data == NULL) return FALSE; if (!fu_genesys_scaler_device_erase_flash(self, addr, size, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); if (!fu_genesys_scaler_device_write_flash(self, addr, data, size, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); buf = g_malloc0(size); if (!fu_genesys_scaler_device_read_flash(self, addr, buf, size, fu_progress_get_child(progress), error)) return FALSE; if (!fu_memcmp_safe(buf, size, 0x0, data, size, 0x0, size, error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static void fu_genesys_scaler_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_genesys_scaler_device_to_string(FuDevice *device, guint idt, GString *str) { FuGenesysScalerDevice *self = FU_GENESYS_SCALER_DEVICE(device); gchar public_key_e[6 + 1] = {0}; gchar public_key_n[0x200 + 1] = {0}; g_autoptr(GError) error_local_e = NULL; g_autoptr(GError) error_local_n = NULL; fwupd_codec_string_append_hex(str, idt, "Level", self->level); if (fu_memcpy_safe((guint8 *)public_key_e, sizeof(public_key_e), 0, /* dst */ (const guint8 *)&self->public_key, sizeof(self->public_key), sizeof(self->public_key) - 2 - (sizeof(public_key_e) - 1), /* src */ sizeof(public_key_e) - 1, &error_local_e)) { fwupd_codec_string_append(str, idt, "PublicKeyE", public_key_e); } else { g_debug("ignoring public-key parameter E: %s", error_local_e->message); } if (fu_memcpy_safe((guint8 *)public_key_n, sizeof(public_key_n), 0, /* dst */ (const guint8 *)&self->public_key, sizeof(self->public_key), 4, /* src */ sizeof(public_key_n) - 1, &error_local_n)) { fwupd_codec_string_append(str, idt, "PublicKeyN", public_key_n); } else { g_debug("ignoring public-key parameter N: %s", error_local_n->message); } fwupd_codec_string_append_hex(str, idt, "ReadRequestRead", self->vc.req_read); fwupd_codec_string_append_hex(str, idt, "WriteRequest", self->vc.req_write); fwupd_codec_string_append_hex(str, idt, "SectorSize", self->sector_size); fwupd_codec_string_append_hex(str, idt, "PageSize", self->page_size); fwupd_codec_string_append_hex(str, idt, "TransferSize", self->transfer_size); fwupd_codec_string_append_hex(str, idt, "GpioOutputRegister", self->gpio_out_reg); fwupd_codec_string_append_hex(str, idt, "GpioEnableRegister", self->gpio_en_reg); fwupd_codec_string_append_hex(str, idt, "GpioValue", self->gpio_val); fwupd_codec_string_append_hex(str, idt, "CfiFlashId", self->cfi_flash_id); } static gboolean fu_genesys_scaler_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuGenesysScalerDevice *self = FU_GENESYS_SCALER_DEVICE(device); guint64 tmp; if (g_strcmp0(key, "GenesysScalerDeviceTransferSize") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->transfer_size = tmp; /* success */ return TRUE; } if (g_strcmp0(key, "GenesysScalerGpioOutputRegister") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->gpio_out_reg = tmp; /* success */ return TRUE; } if (g_strcmp0(key, "GenesysScalerGpioEnableRegister") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->gpio_en_reg = tmp; /* success */ return TRUE; } if (g_strcmp0(key, "GenesysScalerGpioValue") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->gpio_val = tmp; /* success */ return TRUE; } if (g_strcmp0(key, "GenesysScalerCfiFlashId") == 0) { if (!fu_strtoull(value, &tmp, 0, 0x00ffffffU, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->cfi_flash_id = tmp; /* success */ return TRUE; } /* failure */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_genesys_scaler_device_init(FuGenesysScalerDevice *self) { fu_device_set_vendor(FU_DEVICE(self), "MStar Semiconductor"); fu_device_set_name(FU_DEVICE(self), "TSUMG"); fu_device_add_protocol(FU_DEVICE(self), "com.mstarsemi.scaler"); fu_device_retry_set_delay(FU_DEVICE(self), 10); /* 10ms */ fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_OPEN); fu_device_register_private_flag(FU_DEVICE(self), FU_SCALER_FLAG_PAUSE_R2_CPU); fu_device_register_private_flag(FU_DEVICE(self), FU_SCALER_FLAG_USE_I2C_CH0); fu_device_set_install_duration(FU_DEVICE(self), 730); /* 12min 10s */ self->sector_size = 0x1000; /* 4KB */ self->page_size = 0x100; /* 256B */ self->transfer_size = 0x40; /* 64B */ fu_device_set_firmware_size(FU_DEVICE(self), GENESYS_SCALER_BANK_SIZE); /* 2MB */ } static void fu_genesys_scaler_device_finalize(GObject *object) { FuGenesysScalerDevice *self = FU_GENESYS_SCALER_DEVICE(object); if (self->cfi_device != NULL) g_object_unref(self->cfi_device); G_OBJECT_CLASS(fu_genesys_scaler_device_parent_class)->finalize(object); } static void fu_genesys_scaler_device_class_init(FuGenesysScalerDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->finalize = fu_genesys_scaler_device_finalize; device_class->probe = fu_genesys_scaler_device_probe; device_class->setup = fu_genesys_scaler_device_setup; device_class->dump_firmware = fu_genesys_scaler_device_dump_firmware; device_class->prepare_firmware = fu_genesys_scaler_device_prepare_firmware; device_class->write_firmware = fu_genesys_scaler_device_write_firmware; device_class->set_progress = fu_genesys_scaler_device_set_progress; device_class->detach = fu_genesys_scaler_device_detach; device_class->attach = fu_genesys_scaler_device_attach; device_class->to_string = fu_genesys_scaler_device_to_string; device_class->set_quirk_kv = fu_genesys_scaler_device_set_quirk_kv; } FuGenesysScalerDevice * fu_genesys_scaler_device_new(FuContext *ctx) { FuGenesysScalerDevice *device = NULL; device = g_object_new(FU_TYPE_GENESYS_SCALER_DEVICE, "context", ctx, NULL); return device; } fwupd-2.0.10/plugins/genesys/fu-genesys-scaler-device.h000066400000000000000000000006661501337203100230220ustar00rootroot00000000000000/* * Copyright 2021 Gaël PORTAY * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_GENESYS_SCALER_DEVICE (fu_genesys_scaler_device_get_type()) G_DECLARE_FINAL_TYPE(FuGenesysScalerDevice, fu_genesys_scaler_device, FU, GENESYS_SCALER_DEVICE, FuDevice) FuGenesysScalerDevice * fu_genesys_scaler_device_new(FuContext *ctx); fwupd-2.0.10/plugins/genesys/fu-genesys-scaler-firmware.c000066400000000000000000000114501501337203100233630ustar00rootroot00000000000000/* * Copyright 2021 Gaël PORTAY * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-genesys-common.h" #include "fu-genesys-scaler-firmware.h" struct _FuGenesysScalerFirmware { FuFirmwareClass parent_instance; FuGenesysPublicKey public_key; }; G_DEFINE_TYPE(FuGenesysScalerFirmware, fu_genesys_scaler_firmware, FU_TYPE_FIRMWARE) static gboolean fu_genesys_scaler_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuGenesysScalerFirmware *self = FU_GENESYS_SCALER_FIRMWARE(firmware); gsize streamsz = 0; g_autoptr(FuFirmware) firmware_payload = fu_firmware_new(); g_autoptr(FuFirmware) firmware_public_key = NULL; g_autoptr(GInputStream) stream_payload = NULL; g_autoptr(GBytes) blob_public_key = NULL; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz < sizeof(self->public_key)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "image is too small"); return FALSE; } if (!fu_input_stream_read_safe(stream, (guint8 *)&self->public_key, sizeof(self->public_key), 0, /* dst */ streamsz - sizeof(self->public_key), /* src */ sizeof(self->public_key), error)) return FALSE; fu_dump_raw(G_LOG_DOMAIN, "PublicKey", (const guint8 *)&self->public_key, sizeof(self->public_key)); if (memcmp(self->public_key.N, "N = ", 4) != 0 || memcmp(self->public_key.E, "E = ", 4) != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid public-key"); return FALSE; } /* set payload */ stream_payload = fu_partial_input_stream_new(stream, 0, streamsz - sizeof(self->public_key), error); if (stream_payload == NULL) return FALSE; if (!fu_firmware_parse_stream(firmware_payload, stream_payload, 0x0, flags, error)) return FALSE; fu_firmware_set_id(firmware_payload, FU_FIRMWARE_ID_PAYLOAD); fu_firmware_add_image(firmware, firmware_payload); /* set public-key */ blob_public_key = g_bytes_new(&self->public_key, sizeof(self->public_key)); firmware_public_key = fu_firmware_new_from_bytes(blob_public_key); fu_firmware_set_id(firmware_public_key, FU_FIRMWARE_ID_SIGNATURE); fu_firmware_add_image(firmware, firmware_public_key); /* success */ return TRUE; } static void fu_genesys_scaler_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuGenesysScalerFirmware *self = FU_GENESYS_SCALER_FIRMWARE(firmware); gchar N[0x200 + 1] = {'\0'}; gchar E[0x006 + 1] = {'\0'}; memcpy(N, self->public_key.N + 4, sizeof(N) - 1); /* nocheck:blocked */ fu_xmlb_builder_insert_kv(bn, "N", N); memcpy(E, self->public_key.E + 4, sizeof(E) - 1); /* nocheck:blocked */ fu_xmlb_builder_insert_kv(bn, "E", E); } static gboolean fu_genesys_scaler_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { FuGenesysScalerFirmware *self = FU_GENESYS_SCALER_FIRMWARE(firmware); const gchar *tmp; /* optional properties */ tmp = xb_node_query_text(n, "N", NULL); if (tmp != NULL) { if (!fu_memcpy_safe((guint8 *)&self->public_key.N, sizeof(self->public_key.N), 0x0, /* dst */ (const guint8 *)tmp, strlen(tmp), 0x0, /* src */ strlen(tmp), error)) return FALSE; } tmp = xb_node_query_text(n, "E", NULL); if (tmp != NULL) { if (!fu_memcpy_safe((guint8 *)&self->public_key.E, sizeof(self->public_key.E), 0x0, /* dst */ (const guint8 *)tmp, strlen(tmp), 0x0, /* src */ strlen(tmp), error)) return FALSE; } /* success */ return TRUE; } static GByteArray * fu_genesys_scaler_firmware_write(FuFirmware *firmware, GError **error) { FuGenesysScalerFirmware *self = FU_GENESYS_SCALER_FIRMWARE(firmware); g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GBytes) blob = NULL; /* payload */ blob = fu_firmware_get_bytes(firmware, error); if (blob == NULL) return NULL; fu_byte_array_append_bytes(buf, blob); /* public-key */ g_byte_array_append(buf, (const guint8 *)&self->public_key, sizeof(self->public_key)); /* success */ return g_steal_pointer(&buf); } static void fu_genesys_scaler_firmware_init(FuGenesysScalerFirmware *self) { } static void fu_genesys_scaler_firmware_class_init(FuGenesysScalerFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_genesys_scaler_firmware_parse; firmware_class->export = fu_genesys_scaler_firmware_export; firmware_class->build = fu_genesys_scaler_firmware_build; firmware_class->write = fu_genesys_scaler_firmware_write; } FuFirmware * fu_genesys_scaler_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_GENESYS_SCALER_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/genesys/fu-genesys-scaler-firmware.h000066400000000000000000000007331501337203100233720ustar00rootroot00000000000000/* * Copyright 2021 Gaël PORTAY * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_GENESYS_SCALER_FIRMWARE (fu_genesys_scaler_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuGenesysScalerFirmware, fu_genesys_scaler_firmware, FU, GENESYS_SCALER_FIRMWARE, FuFirmware) #define GENESYS_SCALER_BANK_SIZE 0x200000U FuFirmware * fu_genesys_scaler_firmware_new(void); fwupd-2.0.10/plugins/genesys/fu-genesys-usbhub-codesign-firmware.c000066400000000000000000000065021501337203100251750ustar00rootroot00000000000000/* * Copyright 2023 Adam.Chen * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-genesys-common.h" #include "fu-genesys-usbhub-codesign-firmware.h" #include "fu-genesys-usbhub-struct.h" struct _FuGenesysUsbhubCodesignFirmware { FuFirmwareClass parent_instance; FuGenesysFwCodesign codesign; }; G_DEFINE_TYPE(FuGenesysUsbhubCodesignFirmware, fu_genesys_usbhub_codesign_firmware, FU_TYPE_FIRMWARE) gint fu_genesys_usbhub_codesign_firmware_get_codesign(FuGenesysUsbhubCodesignFirmware *self) { g_return_val_if_fail(FU_IS_GENESYS_USBHUB_CODESIGN_FIRMWARE(self), 0); return self->codesign; } static gboolean fu_genesys_usbhub_codesign_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { gsize streamsz = 0; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz - offset != FU_STRUCT_GENESYS_FW_CODESIGN_INFO_RSA_SIZE && streamsz - offset != FU_STRUCT_GENESYS_FW_CODESIGN_INFO_ECDSA_SIZE) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "unknown codesign format"); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_usbhub_codesign_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuGenesysUsbhubCodesignFirmware *self = FU_GENESYS_USBHUB_CODESIGN_FIRMWARE(firmware); gsize streamsz = 0; gsize code_size; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; code_size = streamsz; if (code_size == FU_STRUCT_GENESYS_FW_CODESIGN_INFO_RSA_SIZE) { if (!fu_struct_genesys_fw_codesign_info_rsa_validate_stream(stream, 0x0, error)) { g_prefix_error(error, "not valid for codesign: "); return FALSE; } self->codesign = FU_GENESYS_FW_CODESIGN_RSA; } else if (code_size == FU_STRUCT_GENESYS_FW_CODESIGN_INFO_ECDSA_SIZE) { if (!fu_struct_genesys_fw_codesign_info_ecdsa_validate_stream(stream, 0x0, error)) { g_prefix_error(error, "not valid for codesign: "); return FALSE; } self->codesign = FU_GENESYS_FW_CODESIGN_ECDSA; } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "unknown file format of size 0x%x", (guint)streamsz); return FALSE; } fu_firmware_set_id(firmware, fu_genesys_fw_type_to_string(FU_GENESYS_FW_TYPE_CODESIGN)); fu_firmware_set_idx(firmware, FU_GENESYS_FW_TYPE_CODESIGN); fu_firmware_set_size(firmware, code_size); return TRUE; } static void fu_genesys_usbhub_codesign_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuGenesysUsbhubCodesignFirmware *self = FU_GENESYS_USBHUB_CODESIGN_FIRMWARE(firmware); fu_xmlb_builder_insert_kv(bn, "codesign", fu_genesys_fw_codesign_to_string(self->codesign)); } static void fu_genesys_usbhub_codesign_firmware_init(FuGenesysUsbhubCodesignFirmware *self) { } static void fu_genesys_usbhub_codesign_firmware_class_init(FuGenesysUsbhubCodesignFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_genesys_usbhub_codesign_firmware_validate; firmware_class->parse = fu_genesys_usbhub_codesign_firmware_parse; firmware_class->export = fu_genesys_usbhub_codesign_firmware_export; } fwupd-2.0.10/plugins/genesys/fu-genesys-usbhub-codesign-firmware.h000066400000000000000000000010071501337203100251750ustar00rootroot00000000000000/* * Copyright 2023 Adam.Chen * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_GENESYS_USBHUB_CODESIGN_FIRMWARE (fu_genesys_usbhub_codesign_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuGenesysUsbhubCodesignFirmware, fu_genesys_usbhub_codesign_firmware, FU, GENESYS_USBHUB_CODESIGN_FIRMWARE, FuFirmware) gint fu_genesys_usbhub_codesign_firmware_get_codesign(FuGenesysUsbhubCodesignFirmware *self); fwupd-2.0.10/plugins/genesys/fu-genesys-usbhub-dev-firmware.c000066400000000000000000000045511501337203100241620ustar00rootroot00000000000000/* * Copyright 2023 Adam.Chen * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-genesys-common.h" #include "fu-genesys-usbhub-dev-firmware.h" #include "fu-genesys-usbhub-firmware.h" #include "fu-genesys-usbhub-struct.h" struct _FuGenesysUsbhubDevFirmware { FuFirmwareClass parent_instance; }; G_DEFINE_TYPE(FuGenesysUsbhubDevFirmware, fu_genesys_usbhub_dev_firmware, FU_TYPE_FIRMWARE) static gboolean fu_genesys_usbhub_dev_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_genesys_dev_firmware_hdr_validate_stream(stream, offset, error); } static gboolean fu_genesys_usbhub_dev_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { gsize code_size = 0; g_autoptr(GInputStream) stream_trunc = NULL; fu_firmware_set_id(firmware, fu_genesys_fw_type_to_string(FU_GENESYS_FW_TYPE_DEV_BRIDGE)); fu_firmware_set_idx(firmware, FU_GENESYS_FW_TYPE_DEV_BRIDGE); fu_firmware_set_alignment(firmware, FU_FIRMWARE_ALIGNMENT_1K); /* truncate to correct size */ if (!fu_genesys_usbhub_firmware_calculate_size(stream, &code_size, error)) { g_prefix_error(error, "not valid for dev: "); return FALSE; } stream_trunc = fu_partial_input_stream_new(stream, 0x0, code_size, error); if (stream_trunc == NULL) return FALSE; if (!fu_firmware_set_stream(firmware, stream_trunc, error)) return FALSE; /* calculate checksum */ if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { if (!fu_genesys_usbhub_firmware_verify_checksum(stream_trunc, error)) { g_prefix_error(error, "not valid for dev: "); return FALSE; } } /* get firmware version */ if (!fu_genesys_usbhub_firmware_ensure_version(firmware, error)) { g_prefix_error(error, "not valid for dev: "); return FALSE; } /* success */ return TRUE; } static void fu_genesys_usbhub_dev_firmware_init(FuGenesysUsbhubDevFirmware *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); } static void fu_genesys_usbhub_dev_firmware_class_init(FuGenesysUsbhubDevFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_genesys_usbhub_dev_firmware_validate; firmware_class->parse = fu_genesys_usbhub_dev_firmware_parse; } fwupd-2.0.10/plugins/genesys/fu-genesys-usbhub-dev-firmware.h000066400000000000000000000006171501337203100241660ustar00rootroot00000000000000/* * Copyright 2023 Adam.Chen * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_GENESYS_USBHUB_DEV_FIRMWARE (fu_genesys_usbhub_dev_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuGenesysUsbhubDevFirmware, fu_genesys_usbhub_dev_firmware, FU, GENESYS_USBHUB_DEV_FIRMWARE, FuFirmware) fwupd-2.0.10/plugins/genesys/fu-genesys-usbhub-device.c000066400000000000000000002714501501337203100230350ustar00rootroot00000000000000/* * Copyright 2023 Adam.Chen * Copyright 2022 Gaël PORTAY * Copyright 2021 Ricardo Cañuelo * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-genesys-common.h" #include "fu-genesys-hubhid-device.h" #include "fu-genesys-scaler-device.h" #include "fu-genesys-usbhub-codesign-firmware.h" #include "fu-genesys-usbhub-device.h" #include "fu-genesys-usbhub-firmware.h" #include "fu-genesys-usbhub-struct.h" #define FU_GENESYS_USBHUB_FLAG_HAS_MSTAR_SCALER "has-mstar-scaler" #define FU_GENESYS_USBHUB_FLAG_HAS_PUBLIC_KEY "has-public-key" #define GENESYS_USBHUB_STATIC_TOOL_DESC_IDX_USB_3_0 0x84 #define GENESYS_USBHUB_DYNAMIC_TOOL_DESC_IDX_USB_3_0 0x85 #define GENESYS_USBHUB_STATIC_TOOL_DESC_IDX_USB_2_0 0x81 #define GENESYS_USBHUB_DYNAMIC_TOOL_DESC_IDX_USB_2_0 0x82 #define GENESYS_USBHUB_FW_INFO_DESC_IDX 0x83 #define GENESYS_USBHUB_VENDOR_SUPPORT_DESC_IDX 0x86 #define GENESYS_USBHUB_BRAND_PROJECT_DESC_IDX 0x8A #define GENESYS_USBHUB_GL_HUB_VERIFY 0x71 #define GENESYS_USBHUB_GL_HUB_SWITCH 0x81 #define GENESYS_USBHUB_GL_HUB_READ 0x82 #define GENESYS_USBHUB_GL_HUB_WRITE 0x83 #define GENESYS_USBHUB_GL_HUB_HW_SECURITY 0xAC #define GENESYS_USBHUB_ENCRYPT_REGION_START 0x01 #define GENESYS_USBHUB_ENCRYPT_REGION_END 0x15 #define GENESYS_USBHUB_USB_TIMEOUT 5000 /* ms */ #define GENESYS_USBHUB_FLASH_WRITE_TIMEOUT 500 /* ms */ #define GL3523_BONDING_VALID_BIT 0x0F #define GL3590_BONDING_VALID_BIT 0x7F #define GL3523_BONDING_FLASH_DUMP_LOCATION_BIT 1 << 4 #define GL3590_BONDING_FLASH_DUMP_LOCATION_BIT 1 << 7 typedef enum { ISP_EXIT, ISP_ENTER, } FuGenesysIspMode; typedef struct { guint8 req_switch; guint8 req_read; guint8 req_write; } FuGenesysVendorCommandSetting; typedef struct { guint16 cmd; guint16 len; } FuGenesysQueryRdidFormat; typedef enum { FW_BANK_1, FW_BANK_2, FW_BANK_COUNT } FuGenesysFwBank; typedef struct { FuGenesysChip chip; gboolean support_dual_bank; gboolean support_code_size; guint32 fw_bank_addr[FW_BANK_COUNT][FU_GENESYS_FW_TYPE_INSIDE_HUB_COUNT]; guint32 fw_bank_capacity[FU_GENESYS_FW_TYPE_INSIDE_HUB_COUNT]; guint32 fw_data_max_count; } FuGenesysModelSpec; struct _FuGenesysUsbhubDevice { FuUsbDevice parent_instance; GByteArray *st_static_ts; GByteArray *st_dynamic_ts; GByteArray *st_fwinfo_ts; GByteArray *st_vendor_ts; GByteArray *st_project_ts; FuGenesysVendorCommandSetting vcs; FuGenesysModelSpec spec; FuGenesysTsVersion tool_string_version; FuGenesysFwStatus running_bank; guint8 bonding; gboolean is_gl352350; /* model with unique codesign mechanism */ FuCfiDevice *cfi_device; guint32 flash_erase_delay; guint32 flash_write_delay; guint32 flash_block_size; guint32 flash_sector_size; guint32 flash_rw_size; guint16 fw_bank_code_sizes[FW_BANK_COUNT][FU_GENESYS_FW_TYPE_INSIDE_HUB_COUNT]; guint16 fw_bank_vers[FW_BANK_COUNT][FU_GENESYS_FW_TYPE_INSIDE_HUB_COUNT]; FuGenesysFwBank update_fw_banks[FU_GENESYS_FW_TYPE_INSIDE_HUB_COUNT]; /** * GL3523 hub default boot up on fw bank1 - even bank2's version is higher. * It only boots up on bank2 when bank1 is broken and bank2 is right. * * Therefore, we want bank1 always be last updated firmware. * Also, to fulfill dual bank mechanism, we shall keep at last one fw bank is right. * * For this purpose, we usually backup bank1 (right fw) to bank2, and update fw to bank1. * In rare case - bootup on bank2, we can update fw to bank1 and skip backup. */ gboolean backup_hub_fw_bank1; GBytes *hub_fw_bank1_data; /* restore hub bank1 fw for backup */ /* codesign info */ gboolean has_codesign; gboolean has_hw_codesign; FuGenesysVsCodesignCheck codesign_check; FuGenesysFwCodesign codesign; GByteArray *st_codesign; /* codesign info, may need to backup for GL352350 */ GByteArray *st_public_key; /* hid channel */ FuGenesysHubhidDevice *hid_channel; }; G_DEFINE_TYPE(FuGenesysUsbhubDevice, fu_genesys_usbhub_device, FU_TYPE_USB_DEVICE) void fu_genesys_usbhub_device_set_hid_channel(FuDevice *device, FuDevice *channel) { FuGenesysUsbhubDevice *self = FU_GENESYS_USBHUB_DEVICE(device); g_return_if_fail(self); g_return_if_fail(FU_IS_GENESYS_HUBHID_DEVICE(channel)); if (self->hid_channel != NULL) { g_warning("already set hid_channel, physical_id(%s)", fu_device_get_physical_id(FU_DEVICE(self->hid_channel))); } else { self->hid_channel = FU_GENESYS_HUBHID_DEVICE(channel); /* align usb max length(0xffff) to usb2 packet size(0x40) */ self->flash_rw_size = 0xffc0; } } static gboolean fu_genesys_usbhub_device_ctrl_transfer(FuGenesysUsbhubDevice *self, FuProgress *progress, FuUsbDirection direction, FuUsbRequestType request_type, FuUsbRecipient recipient, guint8 request, guint16 value, guint16 idx, guint8 *data, gsize length, gsize *actual_length, guint timeout, GCancellable *cancellable, GError **error) { if (self->hid_channel != NULL) { return fu_genesys_hubhid_device_send_report(self->hid_channel, progress, direction, request_type, recipient, request, value, idx, data, length, error); } else { return fu_usb_device_control_transfer(FU_USB_DEVICE(self), direction, request_type, recipient, request, value, idx, data, length, actual_length, timeout, cancellable, error); } } static gboolean fu_genesys_usbhub_device_mstar_scaler_setup(FuGenesysUsbhubDevice *self, GError **error) { FuContext *ctx = fu_device_get_context(FU_DEVICE(self)); g_autoptr(FuGenesysScalerDevice) scaler_device = fu_genesys_scaler_device_new(ctx); fu_device_add_child(FU_DEVICE(self), FU_DEVICE(scaler_device)); /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_read_flash(FuGenesysUsbhubDevice *self, guint start_addr, guint8 *buf, guint bufsz, FuProgress *progress, GError **error) { g_autoptr(GPtrArray) chunks = NULL; chunks = fu_chunk_array_mutable_new(buf, bufsz, start_addr, self->flash_block_size, self->flash_rw_size); if (progress != NULL) { fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); } for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); FuProgress *progress_child = NULL; if (progress != NULL) progress_child = fu_progress_get_child(progress); if (!fu_genesys_usbhub_device_ctrl_transfer( self, progress_child, /* progress */ FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vcs.req_read, (fu_chunk_get_page(chk) & 0x000f) << 12, /* value */ fu_chunk_get_address(chk) & 0xffff, /* idx */ fu_chunk_get_data_out(chk), /* data */ fu_chunk_get_data_sz(chk), /* data length */ NULL, /* actual length */ GENESYS_USBHUB_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error reading flash at 0x%04x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } if (progress != NULL) fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_compare_flash_blank(FuGenesysUsbhubDevice *self, guint start_addr, guint code_size, FuProgress *progress, GError **error) { guint read_addr = 0; guint read_size = 0; g_autoptr(GPtrArray) chunks = NULL; g_autoptr(GByteArray) read_buf = g_byte_array_new(); g_autoptr(GByteArray) blank_buf = g_byte_array_new(); if (code_size < 0x400) { /* for small data, compare entire size */ read_addr = start_addr; read_size = code_size; } else { /* for large data, compare last 1024 bytes */ read_addr = start_addr + code_size - 0x400; read_size = 0x400; } fu_byte_array_set_size(read_buf, self->flash_rw_size, 0xFF); fu_byte_array_set_size(blank_buf, self->flash_rw_size, 0xFF); chunks = fu_chunk_array_new(NULL, read_size, read_addr, self->flash_block_size, self->flash_rw_size); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_genesys_usbhub_device_ctrl_transfer( self, fu_progress_get_child(progress), /* progress */ FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vcs.req_read, (fu_chunk_get_page(chk) & 0x000f) << 12, /* value */ fu_chunk_get_address(chk) & 0xffff, /* idx */ read_buf->data, /* data */ fu_chunk_get_data_sz(chk), /* data length */ NULL, /* actual length */ GENESYS_USBHUB_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error reading flash at 0x%04x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } if (!fu_memcmp_safe(read_buf->data, read_buf->len, 0x0, blank_buf->data, blank_buf->len, 0x0, fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "compare flash blank at 0x%04x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_compare_flash_data(FuGenesysUsbhubDevice *self, guint start_addr, const guint8 *buf, guint bufsz, FuProgress *progress, GError **error) { g_autoptr(GPtrArray) chunks = NULL; g_autoptr(GByteArray) read_buf = g_byte_array_new(); fu_byte_array_set_size(read_buf, self->flash_rw_size, 0xFF); chunks = fu_chunk_array_new(buf, bufsz, start_addr, self->flash_block_size, self->flash_rw_size); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_genesys_usbhub_device_ctrl_transfer( self, fu_progress_get_child(progress), /* progress */ FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vcs.req_read, (fu_chunk_get_page(chk) & 0x000f) << 12, /* value */ fu_chunk_get_address(chk) & 0xffff, /* idx */ read_buf->data, /* data */ fu_chunk_get_data_sz(chk), /* data length */ NULL, /* actual length */ GENESYS_USBHUB_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error reading flash at 0x%04x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } if (!fu_memcmp_safe(read_buf->data, read_buf->len, 0x0, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0x0, fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "compare flash data failed at 0x%04x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_reset(FuGenesysUsbhubDevice *self, GError **error) { if (!fu_genesys_usbhub_device_ctrl_transfer(self, NULL, /* progress */ FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vcs.req_switch, 0x0003, /* value */ 0, /* idx */ NULL, /* data */ 0, /* data length */ NULL, /* actual length */ GENESYS_USBHUB_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error resetting device: "); return FALSE; } /* success */ return TRUE; } static FuCfiDevice * fu_genesys_usbhub_device_cfi_setup(FuGenesysUsbhubDevice *self, GError **error) { const FuGenesysQueryRdidFormat rdid[] = { {.cmd = 0x1D02, .len = 0x02}, {.cmd = 0x4B01, .len = 0x02}, {.cmd = 0x9001, .len = 0x02}, {.cmd = 0x9f02, .len = 0x03}, {.cmd = 0xAB01, .len = 0x02}, }; for (guint8 i = 0; i < G_N_ELEMENTS(rdid); i++) { guint8 buf[3] = {0}; g_autoptr(GError) error_local = NULL; g_autoptr(FuCfiDevice) cfi_device = NULL; g_autofree gchar *flash_id = NULL; if (!fu_genesys_usbhub_device_ctrl_transfer(self, NULL, /* progress */ FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vcs.req_read, rdid[i].cmd, /* value */ 0, /* idx */ buf, /* data */ rdid[i].len, /* data length */ NULL, /* actual length */ GENESYS_USBHUB_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error reading flash chip: "); return NULL; } if (rdid[i].len == 2) { flash_id = g_strdup_printf("%02X%02X", buf[0], buf[1]); } else if (rdid[i].len == 3) { flash_id = g_strdup_printf("%02X%02X%02X", buf[0], buf[1], buf[2]); } cfi_device = fu_cfi_device_new(fu_device_get_context(FU_DEVICE(self)), flash_id); if (cfi_device == NULL) continue; if (!fu_device_setup(FU_DEVICE(cfi_device), &error_local)) { g_debug("ignoring %s: %s", flash_id, error_local->message); continue; } if (fu_device_get_name(FU_DEVICE(cfi_device)) == NULL) { g_debug("read %#x: %s", rdid[i].cmd, flash_id); continue; } fu_dump_raw(G_LOG_DOMAIN, "Flash ID", buf, rdid[i].len); g_debug("CFI: %s", fu_device_get_name(FU_DEVICE(cfi_device))); return g_steal_pointer(&cfi_device); } /* failure */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no CFI device found"); return NULL; } static gboolean fu_genesys_usbhub_device_wait_flash_status_register_cb(FuDevice *device, gpointer user_data, GError **error) { FuGenesysUsbhubDevice *self = FU_GENESYS_USBHUB_DEVICE(device); guint8 status = 0; FuGenesysWaitFlashRegisterHelper *helper = user_data; if (!fu_genesys_usbhub_device_ctrl_transfer(self, NULL, /* progress */ FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vcs.req_read, helper->reg << 8 | 0x02, /* value */ 0, /* idx */ &status, /* data */ 1, /* data length */ NULL, /* actual length */ GENESYS_USBHUB_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error getting flash status register (0x%02x): ", helper->reg); return FALSE; } if (status != helper->expected_val) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "wrong value in flash status register"); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_set_isp_mode(FuGenesysUsbhubDevice *self, FuGenesysIspMode mode, GError **error) { if (!fu_genesys_usbhub_device_ctrl_transfer(self, NULL, /* progress */ FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vcs.req_switch, mode, /* value */ 0, /* idx */ NULL, /* data */ 0, /* data length */ NULL, /* actual length */ GENESYS_USBHUB_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error setting isp mode - " "control transfer error (reg 0x%02x) ", self->vcs.req_switch); return FALSE; } if (mode == ISP_ENTER) { FuGenesysWaitFlashRegisterHelper helper = {.reg = 5, .expected_val = 0}; /* 150ms */ if (!fu_device_retry(FU_DEVICE(self), fu_genesys_usbhub_device_wait_flash_status_register_cb, 5, &helper, error)) { g_prefix_error(error, "error setting isp mode: "); return FALSE; } } /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_authentication_request(FuGenesysUsbhubDevice *self, guint8 offset_start, guint8 offset_end, guint8 data_check, GError **error) { guint8 buf = 0; if (!fu_genesys_usbhub_device_ctrl_transfer(self, NULL, /* progress */ FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, GENESYS_USBHUB_GL_HUB_VERIFY, (offset_end << 8) | offset_start, /* value */ 0, /* idx */ &buf, /* data */ 1, /* data length */ NULL, /* actual length */ GENESYS_USBHUB_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "control transfer error (req: 0x%0x): ", (guint)GENESYS_USBHUB_GL_HUB_VERIFY); return FALSE; } if (!fu_genesys_usbhub_device_ctrl_transfer(self, NULL, /* progress */ FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, GENESYS_USBHUB_GL_HUB_VERIFY, (offset_end << 8) | offset_start, /* value */ 1 | (data_check << 8), /* idx */ &buf, /* data */ 1, /* data length */ NULL, /* actual length */ GENESYS_USBHUB_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "control transfer error (req: 0x%0x): ", (guint)GENESYS_USBHUB_GL_HUB_VERIFY); return FALSE; } if (buf != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_AUTH_FAILED, "device authentication failed"); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_authenticate(FuGenesysUsbhubDevice *self, GError **error) { guint16 release = fu_usb_device_get_release(FU_USB_DEVICE(self)); guint8 low_byte; guint8 high_byte; guint8 temp_byte; guint8 offset_start; guint8 offset_end; if (self->vcs.req_switch == GENESYS_USBHUB_GL_HUB_SWITCH) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device authentication not supported"); return FALSE; } low_byte = release & 0xff; high_byte = (release & 0xff00) >> 8; temp_byte = low_byte ^ high_byte; offset_start = g_random_int_range(GENESYS_USBHUB_ENCRYPT_REGION_START, /* nocheck:blocked */ GENESYS_USBHUB_ENCRYPT_REGION_END - 1); offset_end = g_random_int_range(offset_start + 1, /* nocheck:blocked */ GENESYS_USBHUB_ENCRYPT_REGION_END); for (guint8 i = offset_start; i <= offset_end; i++) { temp_byte ^= self->st_fwinfo_ts->data[i]; } if (!fu_genesys_usbhub_device_authentication_request(self, offset_start, offset_end, temp_byte, error)) { g_prefix_error(error, "error authenticating device: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_enter_isp_mode(FuGenesysUsbhubDevice *self, GError **error) { if (self->has_codesign) { if (!fu_genesys_usbhub_device_authenticate(self, error)) return FALSE; } if (!fu_genesys_usbhub_device_set_isp_mode(self, ISP_ENTER, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_get_descriptor_data(GBytes *desc_bytes, guint8 *dst, guint dst_size, GError **error) { gsize bufsz = 0; const guint8 *buf = g_bytes_get_data(desc_bytes, &bufsz); if (bufsz <= 2) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "data is too small"); return FALSE; } memset(dst, 0, dst_size); /* discard first 2 bytes (desc. length and type) */ buf += 2; bufsz -= 2; for (gsize i = 0, j = 0; i < bufsz && j < dst_size; i += 2, j++) dst[j] = buf[i]; /* legacy hub replies "USB2.0 Hub" or "USB3.0 Hub" */ if (memcmp(dst, "USB", 3) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "tool string unsupported"); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_check_fw_signature(FuGenesysUsbhubDevice *self, FuGenesysFwType fw_type, int bank_num, GError **error) { guint8 sig[FU_STRUCT_GENESYS_FIRMWARE_HDR_SIZE_MAGIC] = {0}; const gchar *sig_txt = NULL; g_return_val_if_fail(fw_type < FU_GENESYS_FW_TYPE_INSIDE_HUB_COUNT, FALSE); g_return_val_if_fail(bank_num < FW_BANK_COUNT, FALSE); /* get firmware signature from device */ if (!fu_genesys_usbhub_device_read_flash(self, self->spec.fw_bank_addr[bank_num][fw_type] + FU_STRUCT_GENESYS_FIRMWARE_HDR_OFFSET_MAGIC, sig, sizeof(sig), NULL, error)) { g_prefix_error(error, "error getting fw signature (bank %d) from device: ", bank_num); return FALSE; } /* select firmware signature text and compare */ switch (fw_type) { case FU_GENESYS_FW_TYPE_HUB: sig_txt = FU_STRUCT_GENESYS_FIRMWARE_HDR_DEFAULT_MAGIC; break; case FU_GENESYS_FW_TYPE_DEV_BRIDGE: sig_txt = FU_STRUCT_GENESYS_DEV_FIRMWARE_HDR_DEFAULT_MAGIC; break; case FU_GENESYS_FW_TYPE_PD: sig_txt = FU_STRUCT_GENESYS_PD_FIRMWARE_HDR_DEFAULT_MAGIC; break; default: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported firmware signature"); return FALSE; } if (memcmp(sig, sig_txt, sizeof(sig)) != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_SIGNATURE_INVALID, "wrong firmware signature"); return FALSE; } /* success */ return TRUE; } /* read the code size from the firmware stored in the device */ static gboolean fu_genesys_usbhub_device_get_fw_bank_code_size(FuGenesysUsbhubDevice *self, FuGenesysFwType fw_type, int bank_num, GError **error) { guint8 kbs = 0; g_return_val_if_fail(fw_type < FU_GENESYS_FW_TYPE_INSIDE_HUB_COUNT, FALSE); g_return_val_if_fail(bank_num < FW_BANK_COUNT, FALSE); /* check firmware type available */ if (self->spec.fw_bank_capacity[fw_type] == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported firmware type %s", fu_genesys_fw_type_to_string(fw_type)); return FALSE; } /* check bank 2 available */ if (!self->spec.support_dual_bank && bank_num == FW_BANK_2) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported dual bank"); return FALSE; } /* check firmware signature from device */ if (!fu_genesys_usbhub_device_check_fw_signature(self, fw_type, bank_num, error)) return FALSE; /* bank firmware use fixed code size */ if (!self->spec.support_code_size) { self->fw_bank_code_sizes[bank_num][fw_type] = self->spec.fw_bank_capacity[fw_type]; return TRUE; } /* get code size from device */ if (!fu_genesys_usbhub_device_read_flash(self, self->spec.fw_bank_addr[bank_num][fw_type] + GENESYS_USBHUB_CODE_SIZE_OFFSET, &kbs, 1, NULL, error)) { g_prefix_error(error, "error getting fw size from device: "); return FALSE; } self->fw_bank_code_sizes[bank_num][fw_type] = 1024 * kbs; /* success */ return TRUE; } /* read the version from the firmware stored in the device */ static gboolean fu_genesys_usbhub_device_get_fw_bank_version(FuGenesysUsbhubDevice *self, FuGenesysFwType fw_type, int bank_num, FuProgress *progress, GError **error) { gsize bufsz = 0; g_autoptr(GError) error_local = NULL; g_autoptr(GBytes) blob = NULL; g_autoptr(GInputStream) stream = NULL; g_autofree guint8 *buf = NULL; g_return_val_if_fail(fw_type < FU_GENESYS_FW_TYPE_INSIDE_HUB_COUNT, FALSE); g_return_val_if_fail(bank_num < FW_BANK_COUNT, FALSE); /* check firmware type available */ if (self->spec.fw_bank_capacity[fw_type] == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported firmware type %s", fu_genesys_fw_type_to_string(fw_type)); return FALSE; } /* get bank firmware code size */ if (!fu_genesys_usbhub_device_get_fw_bank_code_size(self, fw_type, bank_num, &error_local)) { g_debug("ignoring firmware %s bank%d: %s", fu_genesys_fw_type_to_string(fw_type), bank_num + 1, error_local->message); self->fw_bank_vers[bank_num][fw_type] = 0; return TRUE; } /* get bank firmware from device */ bufsz = self->fw_bank_code_sizes[bank_num][fw_type]; buf = g_malloc0(bufsz); if (!fu_genesys_usbhub_device_read_flash(self, self->spec.fw_bank_addr[bank_num][fw_type], buf, bufsz, progress, error)) return FALSE; /* verify bank firmware integrity */ blob = g_bytes_new_take(g_steal_pointer(&buf), bufsz); stream = g_memory_input_stream_new_from_bytes(blob); if (!fu_genesys_usbhub_firmware_verify_checksum(stream, &error_local)) { g_debug("ignoring firmware %s bank%d: %s", fu_genesys_fw_type_to_string(fw_type), bank_num + 1, error_local->message); self->fw_bank_vers[bank_num][fw_type] = 0; return TRUE; } /* get firmware version from bank firmware */ if (!fu_memread_uint16_safe(g_bytes_get_data(blob, NULL), g_bytes_get_size(blob), GENESYS_USBHUB_VERSION_OFFSET, &self->fw_bank_vers[bank_num][fw_type], G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to get version: "); return FALSE; } /* keep hub bank 1 fw that may needs backup to bank2 later */ if (self->spec.chip.model == ISP_MODEL_HUB_GL3523 && bank_num == FW_BANK_1) self->hub_fw_bank1_data = g_steal_pointer(&blob); /* success */ return TRUE; } /* read the public-key from the firmware stored in the device */ static gboolean fu_genesys_usbhub_device_get_public_key(FuGenesysUsbhubDevice *self, int bank_num, GError **error) { gsize bufsz = self->spec.fw_bank_capacity[FU_GENESYS_FW_TYPE_CODESIGN]; g_autofree guint8 *buf = NULL; g_autofree gchar *guid = NULL; g_return_val_if_fail(bank_num < FW_BANK_COUNT, FALSE); /* check public-key available */ if (bufsz == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported public-key"); return FALSE; } /* get public-key from device */ buf = g_malloc0(bufsz); if (!fu_genesys_usbhub_device_read_flash( self, self->spec.fw_bank_addr[bank_num][FU_GENESYS_FW_TYPE_CODESIGN], buf, bufsz, NULL, error)) { g_prefix_error(error, "error getting public-key from device: "); return FALSE; } /* validate and parse public-key */ if (self->has_hw_codesign) { /* master device has ECDSA key and signature only */ if (fu_struct_genesys_fw_ecdsa_public_key_validate(buf, bufsz, 0, NULL)) { self->codesign = FU_GENESYS_FW_CODESIGN_ECDSA; self->st_codesign = fu_struct_genesys_fw_ecdsa_public_key_parse(buf, bufsz, 0, error); self->st_public_key = g_byte_array_ref(self->st_codesign); } else { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "device does not have public-key"); return FALSE; } } else { if (fu_struct_genesys_fw_rsa_public_key_text_validate(buf, bufsz, 0, NULL)) { self->codesign = FU_GENESYS_FW_CODESIGN_RSA; self->st_codesign = fu_struct_genesys_fw_rsa_public_key_text_parse(buf, bufsz, 0, error); self->st_public_key = g_byte_array_ref(self->st_codesign); } else if (fu_struct_genesys_fw_codesign_info_ecdsa_validate(buf, bufsz, 0, NULL)) { /* slave device has completely ECDSA codesign info */ gsize keysz = 0; const guint8 *key = NULL; self->codesign = FU_GENESYS_FW_CODESIGN_ECDSA; self->st_codesign = fu_struct_genesys_fw_codesign_info_ecdsa_parse(buf, bufsz, 0, error); key = fu_struct_genesys_fw_codesign_info_ecdsa_get_key(self->st_codesign, &keysz); self->st_public_key = g_byte_array_new(); g_byte_array_append(self->st_public_key, key, keysz); } else { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "device does not have public-key"); return FALSE; } } /* add PUBKEY product instance */ guid = fwupd_guid_hash_data(self->st_public_key->data, self->st_public_key->len, FWUPD_GUID_FLAG_NONE); fu_device_add_instance_strup(FU_DEVICE(self), "PUBKEY", guid); /* success */ return TRUE; } static gint fu_genesys_usbhub_device_tsdigit_value(gchar c) { if (c >= 'A' && c <= 'Z') return c - 'A' + 10; if (c >= 'a' && c <= 'z') return c - 'a' + 10; return g_ascii_digit_value(c); } static gboolean fu_genesys_usbhub_device_validate_token(FuGenesysUsbhubDevice *self, GError **error) { const gchar *valid_tokens[] = {"GL 3.0 Hub", "GL 3.1 Hub", NULL}; g_autofree gchar *token = NULL; g_autoptr(GBytes) token_blob = NULL; g_autoptr(GBytes) token_blob_trunc = NULL; g_autoptr(GError) error_local = NULL; /* get 0x80 string descriptor */ token_blob = fu_usb_device_get_string_descriptor_bytes_full(FU_USB_DEVICE(self), 0x80, FU_USB_LANGID_ENGLISH_UNITED_STATES, 64, &error_local); if (token_blob == NULL) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_READ)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot get 0x80 string descriptor: %s", error_local->message); return FALSE; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } token_blob_trunc = fu_bytes_new_offset(token_blob, 0x2, g_bytes_get_size(token_blob) - 0x2, error); if (token_blob_trunc == NULL) return FALSE; token = fu_utf16_to_utf8_bytes(token_blob_trunc, G_LITTLE_ENDIAN, error); if (token == NULL) return FALSE; /* supported strings */ if (g_strv_contains(valid_tokens, token)) return TRUE; /* failed */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "is not a valid hub: %s", token); return FALSE; } static gboolean fu_genesys_usbhub_device_get_info_from_static_ts(FuGenesysUsbhubDevice *self, const guint8 *buf, gsize bufsz, GError **error) { g_autofree gchar *project_ic_type = NULL; self->st_static_ts = fu_struct_genesys_ts_static_parse(buf, bufsz, 0, error); if (self->st_static_ts == NULL) { g_prefix_error(error, "failed to parse static tool info: "); return FALSE; } project_ic_type = fu_struct_genesys_ts_static_get_mask_project_ic_type(self->st_static_ts); /* verify chip model and revision */ if (memcmp(project_ic_type, "3521", 4) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "ic type %s already EOL and not supported", project_ic_type); return FALSE; } else if (memcmp(project_ic_type, "3523", 4) == 0) { self->spec.chip.model = ISP_MODEL_HUB_GL3523; } else if (memcmp(project_ic_type, "3590", 4) == 0) { self->spec.chip.model = ISP_MODEL_HUB_GL3590; } else if (memcmp(project_ic_type, "3525", 4) == 0) { self->spec.chip.model = ISP_MODEL_HUB_GL3525; } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported ic type %s", project_ic_type); return FALSE; } self->spec.chip.revision = 10 * (project_ic_type[4] - '0') + (project_ic_type[5] - '0'); /* convert tool string version */ self->tool_string_version = fu_struct_genesys_ts_static_get_tool_string_version(self->st_static_ts); /* setup firmware parameters */ switch (self->spec.chip.model) { case ISP_MODEL_HUB_GL3521: self->spec.support_dual_bank = FALSE; self->spec.support_code_size = FALSE; self->spec.fw_bank_addr[FW_BANK_1][FU_GENESYS_FW_TYPE_HUB] = 0x0000; self->spec.fw_bank_capacity[FU_GENESYS_FW_TYPE_HUB] = 0x5000; self->spec.fw_data_max_count = 0x5000; break; case ISP_MODEL_HUB_GL3523: self->spec.support_dual_bank = TRUE; self->spec.fw_bank_addr[FW_BANK_1][FU_GENESYS_FW_TYPE_HUB] = 0x0000; self->spec.fw_bank_addr[FW_BANK_2][FU_GENESYS_FW_TYPE_HUB] = 0x8000; if (self->spec.chip.revision == 50) { self->is_gl352350 = TRUE; self->spec.support_code_size = TRUE; self->spec.fw_bank_addr[FW_BANK_1][FU_GENESYS_FW_TYPE_CODESIGN] = 0x7C00; self->spec.fw_bank_addr[FW_BANK_2][FU_GENESYS_FW_TYPE_CODESIGN] = 0xFC00; self->spec.fw_bank_capacity[FU_GENESYS_FW_TYPE_HUB] = 0x8000; } else { self->spec.support_code_size = FALSE; self->spec.fw_bank_addr[FW_BANK_1][FU_GENESYS_FW_TYPE_CODESIGN] = 0x6000; self->spec.fw_bank_addr[FW_BANK_2][FU_GENESYS_FW_TYPE_CODESIGN] = 0xE000; self->spec.fw_bank_capacity[FU_GENESYS_FW_TYPE_HUB] = 0x6000; } self->spec.fw_bank_capacity[FU_GENESYS_FW_TYPE_CODESIGN] = 0x400; self->spec.fw_data_max_count = 0x10000; break; case ISP_MODEL_HUB_GL3590: self->spec.support_dual_bank = TRUE; self->spec.support_code_size = TRUE; self->spec.fw_bank_addr[FW_BANK_1][FU_GENESYS_FW_TYPE_HUB] = 0x0000; self->spec.fw_bank_addr[FW_BANK_2][FU_GENESYS_FW_TYPE_HUB] = 0x10000; self->spec.fw_bank_addr[FW_BANK_1][FU_GENESYS_FW_TYPE_DEV_BRIDGE] = 0x20000; self->spec.fw_bank_addr[FW_BANK_2][FU_GENESYS_FW_TYPE_DEV_BRIDGE] = 0x30000; self->spec.fw_bank_addr[FW_BANK_1][FU_GENESYS_FW_TYPE_CODESIGN] = 0xFF00; self->spec.fw_bank_addr[FW_BANK_2][FU_GENESYS_FW_TYPE_CODESIGN] = 0x1FF00; self->spec.fw_bank_capacity[FU_GENESYS_FW_TYPE_HUB] = 0x10000; self->spec.fw_bank_capacity[FU_GENESYS_FW_TYPE_DEV_BRIDGE] = 0x10000; self->spec.fw_bank_capacity[FU_GENESYS_FW_TYPE_CODESIGN] = 0x100; self->spec.fw_data_max_count = 0x40000; break; case ISP_MODEL_HUB_GL3525: self->spec.support_dual_bank = TRUE; self->spec.support_code_size = TRUE; self->spec.fw_bank_addr[FW_BANK_1][FU_GENESYS_FW_TYPE_HUB] = 0x0000; self->spec.fw_bank_addr[FW_BANK_2][FU_GENESYS_FW_TYPE_HUB] = 0xB000; self->spec.fw_bank_addr[FW_BANK_1][FU_GENESYS_FW_TYPE_PD] = 0x16000; self->spec.fw_bank_addr[FW_BANK_2][FU_GENESYS_FW_TYPE_PD] = 0x23000; self->spec.fw_bank_addr[FW_BANK_1][FU_GENESYS_FW_TYPE_DEV_BRIDGE] = 0x30000; self->spec.fw_bank_addr[FW_BANK_2][FU_GENESYS_FW_TYPE_DEV_BRIDGE] = 0x38000; self->spec.fw_bank_addr[FW_BANK_1][FU_GENESYS_FW_TYPE_CODESIGN] = 0x16000; self->spec.fw_bank_addr[FW_BANK_2][FU_GENESYS_FW_TYPE_CODESIGN] = 0x17000; self->spec.fw_bank_capacity[FU_GENESYS_FW_TYPE_HUB] = 0xB000; self->spec.fw_bank_capacity[FU_GENESYS_FW_TYPE_PD] = 0xD000; self->spec.fw_bank_capacity[FU_GENESYS_FW_TYPE_DEV_BRIDGE] = 0x8000; self->spec.fw_bank_capacity[FU_GENESYS_FW_TYPE_CODESIGN] = 0x1000; self->spec.fw_data_max_count = 0x40000; break; default: break; } /* add IC product instance */ fu_device_add_instance_str(FU_DEVICE(self), "IC", project_ic_type); /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_get_info_from_dynamic_ts(FuGenesysUsbhubDevice *self, const guint8 *buf, gsize bufsz, GError **error) { gint ss_port_number = 0; gint hs_port_number = 0; gchar running_mode = 0; guint8 bonding = 0; guint8 portnum = 0; gboolean flash_dump_location_bit = FALSE; g_autofree gchar *rm_st = NULL; g_autofree gchar *ss_st = NULL; g_autofree gchar *hs_st = NULL; g_autofree gchar *bonding_st = NULL; /* bonding is not supported */ if (self->tool_string_version < FU_GENESYS_TS_VERSION_BONDING) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "legacy model 0x%02x not supported", self->spec.chip.model); return FALSE; } /* get running mode, portnum, bonding and flash dump location bit */ switch (self->spec.chip.model) { case ISP_MODEL_HUB_GL3523: self->st_dynamic_ts = fu_struct_genesys_ts_dynamic_gl3523_parse(buf, bufsz, 0, error); if (self->st_dynamic_ts == NULL) { g_prefix_error(error, "failed to parse dynamic tool info: "); return FALSE; } rm_st = fu_struct_genesys_ts_dynamic_gl3523_get_running_mode(self->st_dynamic_ts); ss_st = fu_struct_genesys_ts_dynamic_gl3523_get_ss_port_number(self->st_dynamic_ts); hs_st = fu_struct_genesys_ts_dynamic_gl3523_get_hs_port_number(self->st_dynamic_ts); bonding_st = fu_struct_genesys_ts_dynamic_gl3523_get_bonding(self->st_dynamic_ts); bonding = fu_genesys_usbhub_device_tsdigit_value(bonding_st[0]); if (self->tool_string_version < FU_GENESYS_TS_VERSION_BONDING_QC) bonding <<= 1; self->bonding = bonding & GL3523_BONDING_VALID_BIT; flash_dump_location_bit = (bonding & GL3523_BONDING_FLASH_DUMP_LOCATION_BIT) > 0; break; case ISP_MODEL_HUB_GL3590: if (self->spec.chip.revision == 30) { self->st_dynamic_ts = fu_struct_genesys_ts_dynamic_gl359030_parse(buf, bufsz, 0, error); if (self->st_dynamic_ts == NULL) { g_prefix_error(error, "failed to parse dynamic tool info: "); return FALSE; } rm_st = fu_struct_genesys_ts_dynamic_gl359030_get_running_mode( self->st_dynamic_ts); ss_st = fu_struct_genesys_ts_dynamic_gl359030_get_ss_port_number( self->st_dynamic_ts); hs_st = fu_struct_genesys_ts_dynamic_gl359030_get_hs_port_number( self->st_dynamic_ts); self->bonding = fu_struct_genesys_ts_dynamic_gl359030_get_bonding(self->st_dynamic_ts); flash_dump_location_bit = fu_struct_genesys_ts_dynamic_gl359030_get_hub_fw_status( self->st_dynamic_ts) == FU_GENESYS_FW_STATUS_BANK2; } else { self->st_dynamic_ts = fu_struct_genesys_ts_dynamic_gl3590_parse(buf, bufsz, 0, error); if (self->st_dynamic_ts == NULL) { g_prefix_error(error, "failed to parse dynamic tool info: "); return FALSE; } rm_st = fu_struct_genesys_ts_dynamic_gl3590_get_running_mode( self->st_dynamic_ts); ss_st = fu_struct_genesys_ts_dynamic_gl3590_get_ss_port_number( self->st_dynamic_ts); hs_st = fu_struct_genesys_ts_dynamic_gl3590_get_hs_port_number( self->st_dynamic_ts); bonding = fu_struct_genesys_ts_dynamic_gl3590_get_bonding(self->st_dynamic_ts); self->bonding = bonding & GL3590_BONDING_VALID_BIT; flash_dump_location_bit = (bonding & GL3590_BONDING_FLASH_DUMP_LOCATION_BIT) > 0; } break; case ISP_MODEL_HUB_GL3525: self->st_dynamic_ts = fu_struct_genesys_ts_dynamic_gl3525_parse(buf, bufsz, 0, error); if (self->st_dynamic_ts == NULL) { g_prefix_error(error, "failed to parse dynamic tool info: "); return FALSE; } rm_st = fu_struct_genesys_ts_dynamic_gl3525_get_running_mode(self->st_dynamic_ts); ss_st = fu_struct_genesys_ts_dynamic_gl3525_get_ss_port_number(self->st_dynamic_ts); hs_st = fu_struct_genesys_ts_dynamic_gl3525_get_hs_port_number(self->st_dynamic_ts); self->bonding = fu_struct_genesys_ts_dynamic_gl3525_get_bonding(self->st_dynamic_ts); flash_dump_location_bit = fu_struct_genesys_ts_dynamic_gl3525_get_hub_fw_status( self->st_dynamic_ts) == FU_GENESYS_FW_STATUS_BANK2; break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported model 0x%02x", self->spec.chip.model); return FALSE; } running_mode = rm_st[0]; ss_port_number = fu_genesys_usbhub_device_tsdigit_value(ss_st[0]); hs_port_number = fu_genesys_usbhub_device_tsdigit_value(hs_st[0]); if (running_mode == 'M') { self->running_bank = FU_GENESYS_FW_STATUS_MASK; } else if (flash_dump_location_bit) { self->running_bank = FU_GENESYS_FW_STATUS_BANK2; } else { self->running_bank = FU_GENESYS_FW_STATUS_BANK1; } portnum = ss_port_number << 4 | hs_port_number; /* add specific product info */ fu_device_add_instance_u8(FU_DEVICE(self), "PORTNUM", portnum); fu_device_add_instance_u8(FU_DEVICE(self), "BONDING", self->bonding); /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_get_info_from_vendor_ts(FuGenesysUsbhubDevice *self, const guint8 *buf, gsize bufsz, GError **error) { self->st_vendor_ts = fu_struct_genesys_ts_vendor_support_parse(buf, bufsz, 0, error); if (self->st_vendor_ts == NULL) { g_prefix_error(error, "failed to parse vendor support tool info: "); return FALSE; } self->codesign_check = fu_struct_genesys_ts_vendor_support_get_codesign_check(self->st_vendor_ts); if (self->codesign_check > FU_GENESYS_VS_CODESIGN_CHECK_UNSUPPORTED) { self->vcs.req_switch = 0xA1; self->vcs.req_read = 0xA2; self->vcs.req_write = 0xA3; self->has_codesign = TRUE; self->has_hw_codesign = self->codesign_check == FU_GENESYS_VS_CODESIGN_CHECK_HW; } /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_get_info_from_project_ts(FuGenesysUsbhubDevice *self, const guint8 *buf, gsize bufsz, GError **error) { g_autofree gchar *guid = NULL; self->st_project_ts = fu_struct_genesys_ts_brand_project_parse(buf, bufsz, 0, error); if (self->st_project_ts == NULL) { g_prefix_error(error, "failed to parse brand project tool info: "); return FALSE; } /* add specific product info */ guid = fwupd_guid_hash_data(self->st_project_ts->data, self->st_project_ts->len, FWUPD_GUID_FLAG_NONE); fu_device_add_instance_strup(FU_DEVICE(self), "PROJECT", guid); /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_detach(FuDevice *device, FuProgress *progress, GError **error) { return fu_genesys_usbhub_device_enter_isp_mode(FU_GENESYS_USBHUB_DEVICE(device), error); } static gboolean fu_genesys_usbhub_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuGenesysUsbhubDevice *self = FU_GENESYS_USBHUB_DEVICE(device); if (!fu_genesys_usbhub_device_reset(self, error)) return FALSE; if (self->hid_channel != NULL) { fu_device_add_flag(FU_DEVICE(self->hid_channel), FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); } /* success */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static GBytes * fu_genesys_usbhub_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuGenesysUsbhubDevice *self = FU_GENESYS_USBHUB_DEVICE(device); gsize size = fu_cfi_device_get_size(self->cfi_device); g_autoptr(FuDeviceLocker) locker = NULL; g_autofree guint8 *buf = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 99, NULL); /* require detach -> attach */ locker = fu_device_locker_new_full(device, (FuDeviceLockerFunc)fu_device_detach, (FuDeviceLockerFunc)fu_device_attach, error); if (locker == NULL) return NULL; fu_progress_step_done(progress); buf = g_malloc0(size); if (!fu_genesys_usbhub_device_read_flash(self, 0, buf, size, fu_progress_get_child(progress), error)) return NULL; fu_progress_step_done(progress); /* success */ return g_bytes_new_take(g_steal_pointer(&buf), size); } static gboolean fu_genesys_usbhub_device_probe(FuDevice *device, GError **error) { if (fu_usb_device_get_class(FU_USB_DEVICE(device)) != FU_USB_CLASS_HUB) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "is not a usb hub"); return FALSE; } if (fu_usb_device_get_spec(FU_USB_DEVICE(device)) < 0x210) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported USB2 hub"); return FALSE; } if (fu_usb_device_get_spec(FU_USB_DEVICE(device)) >= 0x300) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported USB3 hub"); return FALSE; } return TRUE; } static gboolean fu_genesys_usbhub_device_open(FuDevice *device, GError **error) { FuGenesysUsbhubDevice *self = FU_GENESYS_USBHUB_DEVICE(device); /* FuUsbDevice->open */ if (!FU_DEVICE_CLASS(fu_genesys_usbhub_device_parent_class)->open(device, error)) return FALSE; /* FuGenesysHubhidDevice->open */ if (self->hid_channel != NULL) { if (!fu_device_open(FU_DEVICE(self->hid_channel), error)) return FALSE; } return TRUE; } static gboolean fu_genesys_usbhub_device_close(FuDevice *device, GError **error) { FuGenesysUsbhubDevice *self = FU_GENESYS_USBHUB_DEVICE(device); /* FuUsbDevice->close */ if (!FU_DEVICE_CLASS(fu_genesys_usbhub_device_parent_class)->close(device, error)) return FALSE; /* FuGenesysHubhidDevice->close */ if (self->hid_channel != NULL) { if (!fu_device_close(FU_DEVICE(self->hid_channel), error)) return FALSE; } return TRUE; } static gboolean fu_genesys_usbhub_device_setup(FuDevice *device, GError **error) { FuGenesysUsbhubDevice *self = FU_GENESYS_USBHUB_DEVICE(device); guint32 block_size; guint32 sector_size; guint8 static_idx = 0; guint8 dynamic_idx = 0; const gsize bufsz = 0x20; g_autoptr(GBytes) static_buf = NULL; g_autoptr(GBytes) dynamic_buf = NULL; g_autoptr(GBytes) fw_buf = NULL; g_autofree guint8 *buf = NULL; /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_genesys_usbhub_device_parent_class)->setup(device, error)) { g_prefix_error(error, "error setting up device: "); return FALSE; } /* validate by string token */ if (!fu_genesys_usbhub_device_validate_token(self, error)) return FALSE; /* [DEBUG] - additional info from device: * release version: fu_usb_device_get_release(FU_USB_DEVICE(device)) */ /* read standard string descriptors */ if (fu_usb_device_get_spec(FU_USB_DEVICE(device)) >= 0x300) { static_idx = GENESYS_USBHUB_STATIC_TOOL_DESC_IDX_USB_3_0; dynamic_idx = GENESYS_USBHUB_DYNAMIC_TOOL_DESC_IDX_USB_3_0; } else { static_idx = GENESYS_USBHUB_STATIC_TOOL_DESC_IDX_USB_2_0; dynamic_idx = GENESYS_USBHUB_DYNAMIC_TOOL_DESC_IDX_USB_2_0; } /* * Read/parse vendor-specific string descriptors and use that * data to setup device attributes. */ buf = g_malloc0(bufsz); /* parse static tool string */ static_buf = fu_usb_device_get_string_descriptor_bytes_full(FU_USB_DEVICE(device), static_idx, FU_USB_LANGID_ENGLISH_UNITED_STATES, 64, error); if (static_buf == NULL) { g_prefix_error(error, "failed to get static tool info from device (idx=0x%02x): ", static_idx); return FALSE; } if (!fu_genesys_usbhub_device_get_descriptor_data(static_buf, buf, bufsz, error)) { g_prefix_error(error, "failed to get static tool info from device: "); return FALSE; } if (!fu_genesys_usbhub_device_get_info_from_static_ts(self, buf, bufsz, error)) return FALSE; /* parse dynamic tool string */ dynamic_buf = fu_usb_device_get_string_descriptor_bytes_full(FU_USB_DEVICE(device), dynamic_idx, FU_USB_LANGID_ENGLISH_UNITED_STATES, 64, error); if (dynamic_buf == NULL) { g_prefix_error(error, "failed to get dynamic tool info from device (idx=0x%02x): ", dynamic_idx); return FALSE; } if (!fu_genesys_usbhub_device_get_descriptor_data(dynamic_buf, buf, bufsz, error)) { g_prefix_error(error, "failed to get dynamic tool info from device: "); return FALSE; } if (!fu_genesys_usbhub_device_get_info_from_dynamic_ts(self, buf, bufsz, error)) return FALSE; /* parse firmware info tool string */ fw_buf = fu_usb_device_get_string_descriptor_bytes_full(FU_USB_DEVICE(device), GENESYS_USBHUB_FW_INFO_DESC_IDX, FU_USB_LANGID_ENGLISH_UNITED_STATES, 64, error); if (fw_buf == NULL) { g_prefix_error(error, "failed to get firmware tool info from device: "); return FALSE; } if (!fu_genesys_usbhub_device_get_descriptor_data(fw_buf, buf, bufsz, error)) { g_prefix_error(error, "failed to get firmware tool info from device: "); return FALSE; } self->st_fwinfo_ts = fu_struct_genesys_ts_firmware_info_parse(buf, bufsz, 0, error); if (self->st_fwinfo_ts == NULL) { g_prefix_error(error, "failed to parse firmware tool info: "); return FALSE; } /* parse vendor support tool string */ if (self->tool_string_version >= FU_GENESYS_TS_VERSION_VENDOR_SUPPORT) { g_autoptr(GBytes) vendor_buf = fu_usb_device_get_string_descriptor_bytes_full( FU_USB_DEVICE(device), GENESYS_USBHUB_VENDOR_SUPPORT_DESC_IDX, FU_USB_LANGID_ENGLISH_UNITED_STATES, 64, error); if (vendor_buf == NULL) { g_prefix_error(error, "failed to get vendor support tool info from device: "); return FALSE; } if (!fu_genesys_usbhub_device_get_descriptor_data(vendor_buf, buf, bufsz, error)) { g_prefix_error(error, "failed to get vendor support tool info from device: "); return FALSE; } if (!fu_genesys_usbhub_device_get_info_from_vendor_ts(self, buf, bufsz, error)) return FALSE; } else { self->st_vendor_ts = fu_struct_genesys_ts_vendor_support_new(); } /* parse brand project tool string */ if (self->tool_string_version >= FU_GENESYS_TS_VERSION_BRAND_PROJECT) { g_autoptr(GBytes) project_buf = fu_usb_device_get_string_descriptor_bytes_full( FU_USB_DEVICE(device), GENESYS_USBHUB_BRAND_PROJECT_DESC_IDX, FU_USB_LANGID_ENGLISH_UNITED_STATES, 64, error); if (project_buf == NULL) { g_prefix_error(error, "failed to get brand project tool info from device: "); return FALSE; } if (!fu_genesys_usbhub_device_get_descriptor_data(project_buf, buf, bufsz, error)) { g_prefix_error(error, "failed to get brand project tool info from device: "); return FALSE; } if (!fu_genesys_usbhub_device_get_info_from_project_ts(self, buf, bufsz, error)) return FALSE; } if (fu_device_has_private_flag(device, FU_GENESYS_USBHUB_FLAG_HAS_PUBLIC_KEY)) self->has_codesign = TRUE; /* enter isp mode */ if (!fu_genesys_usbhub_device_enter_isp_mode(self, error)) return FALSE; /* setup cfi device */ self->cfi_device = fu_genesys_usbhub_device_cfi_setup(self, error); if (self->cfi_device == NULL) return FALSE; block_size = fu_cfi_device_get_block_size(self->cfi_device); if (block_size != 0) self->flash_block_size = block_size; sector_size = fu_cfi_device_get_sector_size(self->cfi_device); if (sector_size != 0) self->flash_sector_size = sector_size; /* setup firmware parameters */ fu_device_set_firmware_size_max( device, MIN(self->spec.fw_data_max_count, fu_cfi_device_get_size(self->cfi_device))); /* has codesign */ if (self->has_codesign) { FuGenesysFwBank bank = FW_BANK_1; switch (self->running_bank) { case FU_GENESYS_FW_STATUS_BANK1: bank = FW_BANK_1; break; case FU_GENESYS_FW_STATUS_BANK2: bank = FW_BANK_2; break; default: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "wrong setting in .quirk, " "mask code does not support codesign"); return FALSE; } if (!fu_genesys_usbhub_device_get_public_key(self, bank, error)) return FALSE; } /* add specific product info */ if (self->running_bank != FU_GENESYS_FW_STATUS_MASK) { const gchar *vendor = fwupd_device_get_vendor(FWUPD_DEVICE(device)); g_autofree gchar *guid = NULL; guid = fwupd_guid_hash_data(self->st_vendor_ts->data, self->st_vendor_ts->len, FWUPD_GUID_FLAG_NONE); fu_device_add_instance_strup(device, "VENDOR", vendor); fu_device_add_instance_strup(device, "VENDORSUP", guid); } if (!fu_device_build_instance_id(device, error, "USB", "VID", "PID", "IC", NULL)) return FALSE; if (!fu_device_build_instance_id(device, error, "USB", "VID", "PID", "IC", "BONDING", NULL)) return FALSE; fu_device_build_instance_id(device, NULL, "USB", "VID", "PID", "VENDOR", "IC", "BONDING", "PORTNUM", "VENDORSUP", NULL); fu_device_build_instance_id(device, NULL, "USB", "VID", "PID", "PUBKEY", NULL); fu_device_build_instance_id(device, NULL, "USB", "VID", "PID", "PROJECT", NULL); /* have MStar scaler */ if (fu_device_has_private_flag(device, FU_GENESYS_USBHUB_FLAG_HAS_MSTAR_SCALER)) if (!fu_genesys_usbhub_device_mstar_scaler_setup(self, error)) return FALSE; /* success */ return TRUE; } static void fu_genesys_usbhub_device_codesign_to_string(FuDevice *device, guint idt, GString *str) { FuGenesysUsbhubDevice *self = FU_GENESYS_USBHUB_DEVICE(device); guint64 fw_max_size = fu_device_get_firmware_size_max(device); guint32 bank_addr1 = self->spec.fw_bank_addr[FW_BANK_1][FU_GENESYS_FW_TYPE_CODESIGN]; guint32 bank_addr2 = self->spec.fw_bank_addr[FW_BANK_2][FU_GENESYS_FW_TYPE_CODESIGN]; guint idt_detail = idt + 1; fwupd_codec_string_append(str, idt, "Codesign", fu_genesys_fw_codesign_to_string(self->codesign)); fwupd_codec_string_append(str, idt_detail, "CodesignCheck", fu_genesys_vs_codesign_check_to_string(self->codesign_check)); if (self->spec.support_dual_bank) { fwupd_codec_string_append_hex(str, idt_detail, "Bank1Addr", bank_addr1); if (fw_max_size <= bank_addr2) /* capacity too small */ return; fwupd_codec_string_append_hex(str, idt_detail, "Bank2Addr", bank_addr2); } } static void fu_genesys_usbhub_device_to_string(FuDevice *device, guint idt, GString *str) { FuGenesysUsbhubDevice *self = FU_GENESYS_USBHUB_DEVICE(device); guint64 fw_max_size = fu_device_get_firmware_size_max(device); guint idt_detail = idt + 1; guint idt_bank_detail = idt_detail + 1; if (self->cfi_device != NULL) { fwupd_codec_string_append(str, idt, "CFI", fu_device_get_name(FU_DEVICE(self->cfi_device))); } fwupd_codec_string_append_int(str, idt_detail, "FlashEraseDelay", self->flash_erase_delay); fwupd_codec_string_append_int(str, idt_detail, "FlashWriteDelay", self->flash_write_delay); fwupd_codec_string_append_hex(str, idt_detail, "FlashBlockSize", self->flash_block_size); fwupd_codec_string_append_hex(str, idt_detail, "FlashSectorSize", self->flash_sector_size); fwupd_codec_string_append_hex(str, idt_detail, "FlashRwSize", self->flash_rw_size); fwupd_codec_string_append(str, idt, "RunningBank", fu_genesys_fw_status_to_string(self->running_bank)); fwupd_codec_string_append_bool(str, idt, "SupportDualBank", self->spec.support_dual_bank); fwupd_codec_string_append_bool(str, idt, "SupportCodeSize", self->spec.support_code_size); for (gint i = 0; i < FU_GENESYS_FW_TYPE_INSIDE_HUB_COUNT; i++) { if (self->spec.fw_bank_capacity[i] == 0 || /* unsupported fw type */ fw_max_size <= self->spec.fw_bank_addr[FW_BANK_1][i]) /* capacity too small */ continue; if (i == FU_GENESYS_FW_TYPE_CODESIGN) { if (self->has_codesign) fu_genesys_usbhub_device_codesign_to_string(device, idt + 1, str); continue; } fwupd_codec_string_append(str, idt_detail, "FwBank", fu_genesys_fw_type_to_string(i)); fwupd_codec_string_append_hex(str, idt_bank_detail, "DataTotalCount", self->spec.fw_bank_capacity[i]); fwupd_codec_string_append_int(str, idt_bank_detail, "UpdateBank", self->update_fw_banks[i]); if (self->spec.chip.model == ISP_MODEL_HUB_GL3523 && i == FU_GENESYS_FW_TYPE_HUB) fwupd_codec_string_append_bool(str, idt_bank_detail, "BackupHubFwBank1", self->backup_hub_fw_bank1); if (self->spec.support_dual_bank) { fwupd_codec_string_append_hex(str, idt_bank_detail, "Bank1Addr", self->spec.fw_bank_addr[FW_BANK_1][i]); fwupd_codec_string_append_hex(str, idt_bank_detail, "Bank1Ver", self->fw_bank_vers[FW_BANK_1][i]); fwupd_codec_string_append_hex(str, idt_bank_detail, "Bank1CodeSize", self->fw_bank_code_sizes[FW_BANK_1][i]); if (fw_max_size <= self->spec.fw_bank_addr[FW_BANK_2][i]) /* capacity too small */ continue; fwupd_codec_string_append_hex(str, idt_bank_detail, "Bank2Addr", self->spec.fw_bank_addr[FW_BANK_2][i]); fwupd_codec_string_append_hex(str, idt_bank_detail, "Bank2Ver", self->fw_bank_vers[FW_BANK_2][i]); fwupd_codec_string_append_hex(str, idt_bank_detail, "Bank2CodeSize", self->fw_bank_code_sizes[FW_BANK_2][i]); } } } /** * Prepare update * * Collect dual bank's version. Then select lower version bank to update. * When device is running mask code, should not compare and select bank1. * */ static gboolean fu_genesys_usbhub_device_prepare(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuGenesysUsbhubDevice *self = FU_GENESYS_USBHUB_DEVICE(device); guint64 fw_max_size = fu_device_get_firmware_size_max(device); g_autoptr(GArray) fw_types = g_array_new(FALSE, FALSE, sizeof(FuGenesysFwType)); for (gint i = 0; i < FU_GENESYS_FW_TYPE_INSIDE_HUB_COUNT; i++) { if (self->spec.fw_bank_capacity[i] == 0 || /* unsupported fw type */ fw_max_size <= self->spec.fw_bank_addr[FW_BANK_1][i]) /* capacity is smaller */ continue; if (self->running_bank == FU_GENESYS_FW_STATUS_MASK || /* both banks are invalid */ !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_DUAL_IMAGE)) { self->update_fw_banks[i] = FW_BANK_1; continue; } /* hub & codesign info must at the same bank */ if (i == FU_GENESYS_FW_TYPE_CODESIGN) continue; g_array_append_val(fw_types, i); } if (fw_types->len == 0) return TRUE; fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 5, "detach"); for (guint i = 0; i < fw_types->len; i++) { fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 100, NULL); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 100, NULL); } /* enter isp mode */ if (!fu_genesys_usbhub_device_enter_isp_mode(self, error)) return FALSE; fu_progress_step_done(progress); /* query each fw bank version of fw type */ for (guint i = 0; i < fw_types->len; i++) { FuGenesysFwType fw_type = g_array_index(fw_types, FuGenesysFwType, i); /* hub & codesign info must at the same bank */ if (fw_type == FU_GENESYS_FW_TYPE_CODESIGN) { self->update_fw_banks[fw_type] = self->update_fw_banks[FU_GENESYS_FW_TYPE_HUB]; continue; } if (!fu_genesys_usbhub_device_get_fw_bank_version(self, fw_type, FW_BANK_1, fu_progress_get_child(progress), error)) { g_prefix_error(error, "error getting %s bank1 version: ", fu_genesys_fw_type_to_string(fw_type)); return FALSE; } fu_progress_step_done(progress); if (!fu_genesys_usbhub_device_get_fw_bank_version(self, fw_type, FW_BANK_2, fu_progress_get_child(progress), error)) { g_prefix_error(error, "error getting %s bank2 version: ", fu_genesys_fw_type_to_string(fw_type)); return FALSE; } fu_progress_step_done(progress); if (self->fw_bank_vers[FW_BANK_1][fw_type] > self->fw_bank_vers[FW_BANK_2][fw_type]) { /* bank1 is more recent than bank2: write fw on bank2 */ if (self->spec.chip.model == ISP_MODEL_HUB_GL3523) { /* GL3523 unique dual bank mechanism */ self->backup_hub_fw_bank1 = TRUE; self->update_fw_banks[fw_type] = FW_BANK_1; } else { self->update_fw_banks[fw_type] = FW_BANK_2; } } else { /* bank2 is more recent than bank1: write fw on bank1 */ self->update_fw_banks[fw_type] = FW_BANK_1; } } /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_compare_fw_public_key(FuGenesysUsbhubDevice *self, FuFirmware *firmware, GError **error) { FuGenesysFwCodesign codesign_type = FU_GENESYS_FW_CODESIGN_NONE; g_autoptr(GInputStream) stream = NULL; g_autoptr(GByteArray) st_codesign = NULL; g_return_val_if_fail(FU_IS_GENESYS_USBHUB_CODESIGN_FIRMWARE(firmware), FALSE); /* compare dev and fw codesign type */ codesign_type = fu_genesys_usbhub_codesign_firmware_get_codesign( FU_GENESYS_USBHUB_CODESIGN_FIRMWARE(firmware)); if (codesign_type != self->codesign) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_SIGNATURE_INVALID, "mismatch codesign type %s", fu_genesys_fw_codesign_to_string(codesign_type)); return FALSE; } /* get fw codesign info */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) { g_prefix_error(error, "firmware does not have %s bytes: ", fu_genesys_fw_type_to_string(FU_GENESYS_FW_TYPE_CODESIGN)); return FALSE; } /* compare dev and fw public-key */ switch (self->codesign) { case FU_GENESYS_FW_CODESIGN_RSA: { g_autofree gchar *fw_n = NULL; g_autofree gchar *fw_e = NULL; g_autofree gchar *dev_n = NULL; g_autofree gchar *dev_e = NULL; /* parse and validate */ st_codesign = fu_struct_genesys_fw_codesign_info_rsa_parse_stream(stream, 0x0, error); if (st_codesign == NULL) { g_prefix_error(error, "failed to parse %s codesgin: ", fu_genesys_fw_codesign_to_string(codesign_type)); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "PublicKey", st_codesign->data, FU_STRUCT_GENESYS_FW_CODESIGN_INFO_RSA_SIZE); fw_n = fu_struct_genesys_fw_codesign_info_rsa_get_text_n(st_codesign); dev_n = fu_struct_genesys_fw_rsa_public_key_text_get_text_n(self->st_public_key); if (!fu_memcmp_safe((const guint8 *)fw_n, FU_STRUCT_GENESYS_FW_CODESIGN_INFO_RSA_SIZE_TEXT_N, 0, (const guint8 *)dev_n, FU_STRUCT_GENESYS_FW_RSA_PUBLIC_KEY_TEXT_SIZE_TEXT_N, 0, FU_STRUCT_GENESYS_FW_RSA_PUBLIC_KEY_TEXT_SIZE_TEXT_N, error)) { g_prefix_error(error, "mismatch public-keyN: "); return FALSE; } fw_e = fu_struct_genesys_fw_codesign_info_rsa_get_text_e(st_codesign); dev_e = fu_struct_genesys_fw_rsa_public_key_text_get_text_e(self->st_public_key); if (!fu_memcmp_safe((const guint8 *)fw_e, FU_STRUCT_GENESYS_FW_CODESIGN_INFO_RSA_SIZE_TEXT_E, 0, (const guint8 *)dev_e, FU_STRUCT_GENESYS_FW_RSA_PUBLIC_KEY_TEXT_SIZE_TEXT_E, 0, FU_STRUCT_GENESYS_FW_RSA_PUBLIC_KEY_TEXT_SIZE_TEXT_E, error)) { g_prefix_error(error, "mismatch public-keyE: "); return FALSE; } break; } case FU_GENESYS_FW_CODESIGN_ECDSA: { gsize fw_keysz = 0; const guint8 *fw_key = NULL; /* parse and validate */ st_codesign = fu_struct_genesys_fw_codesign_info_ecdsa_parse_stream(stream, 0x0, error); if (st_codesign == NULL) { g_prefix_error(error, "failed to parse %s codesgin: ", fu_genesys_fw_codesign_to_string(codesign_type)); return FALSE; } fw_key = fu_struct_genesys_fw_codesign_info_ecdsa_get_key(st_codesign, &fw_keysz); if (fw_keysz != FU_STRUCT_GENESYS_FW_ECDSA_PUBLIC_KEY_SIZE) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_SIGNATURE_INVALID, "mismatch public-key size"); return FALSE; } if (!fu_memcmp_safe(fw_key, fw_keysz, 0, self->st_public_key->data, self->st_public_key->len, 0, FU_STRUCT_GENESYS_FW_ECDSA_PUBLIC_KEY_SIZE, error)) { g_prefix_error(error, "mismatch public-key: "); fu_dump_raw(G_LOG_DOMAIN, "PublicKey", fw_key, fw_keysz); return FALSE; } break; } default: break; } /* does not exist */ if (st_codesign == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "unsupported codesign type %s", fu_genesys_fw_codesign_to_string(codesign_type)); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_adjust_fw_addr(FuGenesysUsbhubDevice *self, FuFirmware *firmware, GError **error) { FuGenesysFwType fw_type = fu_firmware_get_idx(firmware); FuGenesysFwBank bank_num; guint32 code_size = 0; guint32 bank_size = 0; g_autoptr(GPtrArray) imgs = NULL; /* check supported fw type */ if (fw_type >= FU_GENESYS_FW_TYPE_INSIDE_HUB_COUNT) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unknown firmware type %s", fu_firmware_get_id(firmware)); return FALSE; } /* check bank capacity */ bank_size = self->spec.fw_bank_capacity[fw_type]; if (bank_size == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device not supported firmware type %s", fu_firmware_get_id(firmware)); return FALSE; } code_size = fu_firmware_get_size(firmware); if (code_size > bank_size) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "firmware %s too large, got %#x, expected <= %#x", fu_firmware_get_id(firmware), code_size, bank_size); return FALSE; } /* set update address */ bank_num = self->update_fw_banks[fw_type]; if (bank_num >= FW_BANK_COUNT) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unknown bank num 0x%x", bank_num); return FALSE; } fu_firmware_set_addr(firmware, self->spec.fw_bank_addr[bank_num][fw_type]); /* set child firmware */ imgs = fu_firmware_get_images(firmware); for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); if (!fu_genesys_usbhub_device_adjust_fw_addr(self, img, error)) return FALSE; } /* success */ return TRUE; } static FuFirmware * fu_genesys_usbhub_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuGenesysUsbhubDevice *self = FU_GENESYS_USBHUB_DEVICE(device); g_autoptr(FuFirmware) firmware = fu_genesys_usbhub_firmware_new(); /* parse firmware */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; /* has codesign */ if (self->has_codesign && (flags & FWUPD_INSTALL_FLAG_FORCE) == 0) { g_autoptr(FuFirmware) img = NULL; if (self->st_public_key == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "device does not have public-key"); return NULL; } img = fu_firmware_get_image_by_idx(firmware, FU_GENESYS_FW_TYPE_CODESIGN, error); if (img == NULL) { g_prefix_error(error, "firmware does not have %s: ", fu_genesys_fw_type_to_string(FU_GENESYS_FW_TYPE_CODESIGN)); return NULL; } if (!fu_genesys_usbhub_device_compare_fw_public_key(self, img, error)) return NULL; } /* set write address into each firmware address */ if (!fu_genesys_usbhub_device_adjust_fw_addr(self, firmware, error)) return NULL; /* success */ return g_steal_pointer(&firmware); } static gboolean fu_genesys_usbhub_device_erase_flash(FuGenesysUsbhubDevice *self, guint start_addr, guint len, FuProgress *progress, GError **error) { FuGenesysWaitFlashRegisterHelper helper = {.reg = 5, .expected_val = 0}; g_autoptr(GPtrArray) chunks = NULL; chunks = fu_chunk_array_new(NULL, len, start_addr, self->flash_block_size, self->flash_sector_size); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); guint16 sectornum = fu_chunk_get_address(chk) / self->flash_sector_size; guint16 blocknum = fu_chunk_get_page(chk); guint16 index = (0x01 << 8) | (sectornum << 4) | blocknum; if (!fu_genesys_usbhub_device_ctrl_transfer(self, NULL, /* progress */ FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vcs.req_write, 0x2001, /* value */ index, /* idx */ NULL, /* data */ 0, /* data length */ NULL, /* actual length */ GENESYS_USBHUB_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error erasing flash at sector 0x%02x in block 0x%02x", sectornum, blocknum); return FALSE; } /* 8s */ if (!fu_device_retry(FU_DEVICE(self), fu_genesys_usbhub_device_wait_flash_status_register_cb, self->flash_erase_delay / 30, &helper, error)) { g_prefix_error(error, "error erasing flash: "); return FALSE; } fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_write_flash(FuGenesysUsbhubDevice *self, guint start_addr, const guint8 *buf, guint bufsz, FuProgress *progress, GError **error) { FuGenesysWaitFlashRegisterHelper helper = {.reg = 5, .expected_val = 0}; g_autoptr(GPtrArray) chunks = NULL; chunks = fu_chunk_array_new(buf, bufsz, start_addr, self->flash_block_size, self->flash_rw_size); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); g_autoptr(GByteArray) buf_write = g_byte_array_new(); g_byte_array_append(buf_write, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); if (!fu_genesys_usbhub_device_ctrl_transfer( self, fu_progress_get_child(progress), /* progress */ FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, self->vcs.req_write, (fu_chunk_get_page(chk) & 0x000f) << 12, /* value */ fu_chunk_get_address(chk) & 0xffff, /* idx */ buf_write->data, /* data */ buf_write->len, /* data length */ NULL, /* actual length */ GENESYS_USBHUB_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error writing flash at 0x%02x%04x: ", fu_chunk_get_page(chk), (guint)fu_chunk_get_address(chk)); return FALSE; } /* 5s */ if (!fu_device_retry(FU_DEVICE(self), fu_genesys_usbhub_device_wait_flash_status_register_cb, self->flash_write_delay / 30, &helper, error)) { g_prefix_error(error, "error writing flash: "); return FALSE; } fu_progress_step_done(progress); } /* success */ return TRUE; } /** * GL3523 hub default boot up on fw bank1 - even bank2's version is higher. It only boots up * on bank2 when bank1 is broken and bank2 is right. Therefore, we want bank1 always be last * updated firmware. Also, to fulfill dual bank mechanism, we shall keep at last one fw bank * is right. For this purpose, we usually backup bank1 (right fw) to bank2, then update new * fw to bank1. * * GL352350's fw bank stores codesign info and code in the same sector and the sector will * be erased. Therefore, backup must include codesign info. */ static gboolean fu_genesys_usbhub_device_backup_hub_fw_bank1_to_bank2(FuGenesysUsbhubDevice *self, FuProgress *progress, GError **error) { gsize bufsz = 0; const guint start_addr = self->spec.fw_bank_addr[FW_BANK_2][FU_GENESYS_FW_TYPE_HUB]; g_autofree guint8 *buf = NULL; /* reuse fw on bank1 for GL3523 */ if (self->hub_fw_bank1_data == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "backup bank does not exist"); return FALSE; } if (self->is_gl352350 && self->has_codesign) { /* merge hub fw and codesign info for GL352350 */ gsize code_size = 0; const guint8 *code_data = g_bytes_get_data(self->hub_fw_bank1_data, &code_size); guint32 codesign_offset = self->spec.fw_bank_addr[FW_BANK_1][FU_GENESYS_FW_TYPE_CODESIGN]; if (codesign_offset < code_size) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "hub firmware too large, got 0x%x, expected <= 0x%x", (guint)code_size, codesign_offset); return FALSE; } bufsz = codesign_offset + self->st_codesign->len; buf = g_malloc0(bufsz); /* copy fw bank data */ if (!fu_memcpy_safe(buf, bufsz, 0, /* dst */ code_data, code_size, 0x0, /* src */ code_size, error)) return FALSE; /* copy codesign info */ if (!fu_memcpy_safe(buf, bufsz, codesign_offset, /* dst */ self->st_codesign->data, self->st_codesign->len, 0x0, /* src */ self->st_codesign->len, error)) return FALSE; } else { bufsz = g_bytes_get_size(self->hub_fw_bank1_data); buf = fu_memdup_safe(g_bytes_get_data(self->hub_fw_bank1_data, NULL), bufsz, error); if (buf == NULL) return FALSE; } /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 30, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 5, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 50, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 15, NULL); /* erase */ if (!fu_genesys_usbhub_device_erase_flash(self, start_addr, bufsz, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* verify blank */ if (!fu_genesys_usbhub_device_compare_flash_blank(self, start_addr, bufsz, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* write */ if (!fu_genesys_usbhub_device_write_flash(self, start_addr, buf, bufsz, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* verify */ if (!fu_genesys_usbhub_device_compare_flash_data(self, start_addr, buf, bufsz, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_update_firmware(FuGenesysUsbhubDevice *self, FuFirmware *firmware, FuProgress *progress, GError **error) { gboolean skip_erase = FALSE; gsize bufsz = 0; const guint8 *buf = NULL; const guint start_addr = fu_firmware_get_addr(firmware); g_autoptr(GBytes) blob = NULL; /* get raw data */ blob = fu_firmware_get_bytes(firmware, error); if (blob == NULL) return FALSE; buf = g_bytes_get_data(blob, &bufsz); if (fu_firmware_get_idx(firmware) == FU_GENESYS_FW_TYPE_CODESIGN) { /* already erase at FU_GENESYS_FW_TYPE_HUB before */ if (self->is_gl352350) skip_erase = TRUE; /* jump ECDSA hash on HW codesign */ if (self->has_hw_codesign && self->codesign == FU_GENESYS_FW_CODESIGN_ECDSA) { if (bufsz != FU_STRUCT_GENESYS_FW_CODESIGN_INFO_ECDSA_SIZE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_SIGNATURE_INVALID, "mismatch codesign length, got 0x%x, expected = 0x%x", (guint)bufsz, (guint)FU_STRUCT_GENESYS_FW_CODESIGN_INFO_ECDSA_SIZE); return FALSE; } buf += FU_STRUCT_GENESYS_FW_CODESIGN_INFO_ECDSA_SIZE_HASH; bufsz -= FU_STRUCT_GENESYS_FW_CODESIGN_INFO_ECDSA_SIZE_HASH; } } /* progress */ fu_progress_set_id(progress, fu_firmware_get_id(firmware)); if (skip_erase) { fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 5, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 79, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 15, NULL); } else { fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 30, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 5, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 50, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 15, NULL); } /* erase */ if (!skip_erase && !fu_genesys_usbhub_device_erase_flash(self, start_addr, bufsz, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* verify blank */ if (!fu_genesys_usbhub_device_compare_flash_blank(self, start_addr, bufsz, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* write */ if (!fu_genesys_usbhub_device_write_flash(self, start_addr, buf, bufsz, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* verify */ if (!self->has_hw_codesign) { /* skip specific codesign verification */ if (!fu_genesys_usbhub_device_compare_flash_data(self, start_addr, buf, bufsz, fu_progress_get_child(progress), error)) return FALSE; } fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_setup_hw_module(FuGenesysUsbhubDevice *self, GError **error) { if (!fu_genesys_usbhub_device_ctrl_transfer(self, NULL, /* progress */ FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, GENESYS_USBHUB_GL_HUB_HW_SECURITY, 0, /* value */ 0, /* idx */ NULL, /* data */ 0, /* data length */ NULL, /* actual length */ GENESYS_USBHUB_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error setting up HW module: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_send_hash_data_length(FuGenesysUsbhubDevice *self, gsize size_to_hash, GError **error) { guint16 length_by_4k = size_to_hash / 4096; if (!fu_genesys_usbhub_device_ctrl_transfer(self, NULL, /* progress */ FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, GENESYS_USBHUB_GL_HUB_HW_SECURITY, 0x040b, /* value */ length_by_4k, /* idx */ NULL, /* data */ 0, /* data length */ NULL, /* actual length */ GENESYS_USBHUB_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error sending hash data length by 4k(0x%x): ", length_by_4k); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_send_hash_digest(FuGenesysUsbhubDevice *self, GByteArray *st_codesign, GError **error) { gsize hash_bufsz = 0; const guint8 *hash_buf = NULL; g_autofree guint8 *buf_mut = NULL; hash_buf = fu_struct_genesys_fw_codesign_info_ecdsa_get_hash(st_codesign, &hash_bufsz); if (hash_buf == NULL) { g_prefix_error(error, "failed to get hash digest: "); return FALSE; } buf_mut = fu_memdup_safe(hash_buf, hash_bufsz, error); if (buf_mut == NULL) { g_prefix_error(error, "failed to dup hash digest: "); return FALSE; } if (!fu_genesys_usbhub_device_ctrl_transfer(self, NULL, /* progress */ FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, GENESYS_USBHUB_GL_HUB_HW_SECURITY, 0x000b, /* value */ 0, /* idx */ buf_mut, /* data */ hash_bufsz, /* data length */ NULL, /* actual length */ GENESYS_USBHUB_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error sending hash digest: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_check_hash_digest_verification(FuGenesysUsbhubDevice *self, GError **error) { guint8 verification = 0; if (!fu_genesys_usbhub_device_ctrl_transfer(self, NULL, /* progress */ FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, GENESYS_USBHUB_GL_HUB_HW_SECURITY, 0x000b, /* value */ 0, /* idx */ &verification, /* data */ 1, /* data length */ NULL, /* actual length */ GENESYS_USBHUB_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error getting hash digest verification: "); return FALSE; } if (verification != 1) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_AUTH_FAILED, "failed to verify hash digest, got 0x%x, expected 0x01", verification); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_toggle_hw_read_key(FuGenesysUsbhubDevice *self, guint16 key_addr, GError **error) { if (!fu_genesys_usbhub_device_ctrl_transfer(self, NULL, /* progress */ FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, GENESYS_USBHUB_GL_HUB_HW_SECURITY, 0x020c, /* value */ key_addr, /* idx */ NULL, /* data */ 0, /* data length */ NULL, /* actual length */ GENESYS_USBHUB_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error sending key addr 0x%x: ", key_addr); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_send_signature(FuGenesysUsbhubDevice *self, GByteArray *st_codesign, GError **error) { gsize sig_bufsz = 0; const guint8 *sig_buf = NULL; g_autofree guint8 *buf_mut = NULL; sig_buf = fu_struct_genesys_fw_codesign_info_ecdsa_get_signature(st_codesign, &sig_bufsz); if (sig_buf == NULL) { g_prefix_error(error, "failed to get signature: "); return FALSE; } buf_mut = fu_memdup_safe(sig_buf, sig_bufsz, error); if (buf_mut == NULL) { g_prefix_error(error, "failed to dup signature: "); return FALSE; } if (!fu_genesys_usbhub_device_ctrl_transfer(self, NULL, /* progress */ FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, GENESYS_USBHUB_GL_HUB_HW_SECURITY, 0x010c, /* value */ 0, /* idx */ buf_mut, /* data */ sig_bufsz, /* data length */ NULL, /* actual length */ GENESYS_USBHUB_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error sending signature: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_check_signature_verification(FuGenesysUsbhubDevice *self, GError **error) { guint8 verification = 0; if (!fu_genesys_usbhub_device_ctrl_transfer(self, NULL, /* progress */ FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, GENESYS_USBHUB_GL_HUB_HW_SECURITY, 0x000c, /* value */ 0, /* idx */ &verification, /* data */ 1, /* data length */ NULL, /* actual length */ GENESYS_USBHUB_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "error getting signature verification: "); return FALSE; } if (verification != 1) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_AUTH_FAILED, "failed to verify signature, got 0x%x, expected 0x01", verification); return FALSE; } /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_compare_firmware_flash_data(FuGenesysUsbhubDevice *self, FuFirmware *firmware, FuProgress *progress, GError **error) { gsize bufsz = 0; const guint8 *buf = NULL; const guint start_addr = fu_firmware_get_addr(firmware); g_autoptr(GBytes) blob = NULL; /* get raw data */ blob = fu_firmware_get_bytes(firmware, error); if (blob == NULL) return FALSE; buf = g_bytes_get_data(blob, &bufsz); if (!fu_genesys_usbhub_device_compare_flash_data(self, start_addr, buf, bufsz, progress, error)) return FALSE; /* success */ return TRUE; } /** * GL3590 hub has HW module to examine ECDSA codesign. */ static gboolean fu_genesys_usbhub_device_examine_fw_codesign_hw(FuGenesysUsbhubDevice *self, FuFirmware *firmware, FuProgress *progress, GError **error) { gsize codesize_to_hash = 0; g_autoptr(FuFirmware) codesign_img = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(GByteArray) st_codesign = NULL; g_autoptr(GPtrArray) imgs = fu_firmware_get_images(firmware); /* get fw codesign info */ codesign_img = fu_firmware_get_image_by_idx(firmware, FU_GENESYS_FW_TYPE_CODESIGN, error); if (codesign_img == NULL) return FALSE; stream = fu_firmware_get_stream(codesign_img, error); if (stream == NULL) return FALSE; st_codesign = fu_struct_genesys_fw_codesign_info_ecdsa_parse_stream(stream, 0x0, error); if (st_codesign == NULL) return FALSE; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 5, "setup HW codesign engine"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 100, fu_firmware_get_id(firmware)); for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); if (fu_firmware_get_idx(img) != FU_GENESYS_FW_TYPE_CODESIGN) fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 100, fu_firmware_get_id(img)); } fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 5, "verify hash"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 5, "verify signature"); /* setup HW codesign module */ if (!fu_genesys_usbhub_device_setup_hw_module(self, error)) return FALSE; fu_progress_step_done(progress); /* compare firmware data */ if (!fu_genesys_usbhub_device_compare_firmware_flash_data(self, firmware, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); codesize_to_hash += fu_firmware_get_size(firmware); for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); if (fu_firmware_get_idx(img) != FU_GENESYS_FW_TYPE_CODESIGN) { if (!fu_genesys_usbhub_device_compare_firmware_flash_data( self, img, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); codesize_to_hash += fu_firmware_get_size(img); } } /* compute and verify hash */ if (!fu_genesys_usbhub_device_send_hash_data_length(self, codesize_to_hash, error)) return FALSE; if (!fu_genesys_usbhub_device_send_hash_digest(self, st_codesign, error)) return FALSE; if (!fu_genesys_usbhub_device_check_hash_digest_verification(self, error)) return FALSE; fu_progress_step_done(progress); /* toggle hw read key */ if (!fu_genesys_usbhub_device_toggle_hw_read_key(self, fu_firmware_get_addr(codesign_img), error)) return FALSE; /* send and verify signature */ if (!fu_genesys_usbhub_device_send_signature(self, st_codesign, error)) return FALSE; if (!fu_genesys_usbhub_device_check_signature_verification(self, error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_genesys_usbhub_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuGenesysUsbhubDevice *self = FU_GENESYS_USBHUB_DEVICE(device); g_autoptr(GPtrArray) imgs = fu_firmware_get_images(firmware); /* enter isp mode */ if (!fu_genesys_usbhub_device_enter_isp_mode(self, error)) return FALSE; /* progress */ fu_progress_set_id(progress, G_STRLOC); if (self->backup_hub_fw_bank1) { fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "backup hub firmware to bank2"); } fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, fu_firmware_get_id(firmware)); for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, fu_firmware_get_id(img)); } if (self->has_hw_codesign) { fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 100, "verify firmware codesign"); } /* backup fw to bank2 first */ if (self->backup_hub_fw_bank1) { if (!fu_genesys_usbhub_device_backup_hub_fw_bank1_to_bank2( self, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); } /* write main firmware */ if (!fu_genesys_usbhub_device_update_firmware(self, firmware, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* write child firmware */ for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); if (!fu_genesys_usbhub_device_update_firmware(self, img, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); } /* examine codesign */ if (self->has_hw_codesign) { if (!fu_genesys_usbhub_device_examine_fw_codesign_hw( self, firmware, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to verify HW codesign: "); return FALSE; } fu_progress_step_done(progress); } /* success */ return TRUE; } static void fu_genesys_usbhub_device_set_progress(FuDevice *device, FuProgress *progress) { FuGenesysUsbhubDevice *self = FU_GENESYS_USBHUB_DEVICE(device); fu_progress_set_id(progress, G_STRLOC); if (self->backup_hub_fw_bank1) { fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 30, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 70, "reload"); } else { fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 15, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 85, "reload"); } } static gboolean fu_genesys_usbhub_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuGenesysUsbhubDevice *self = FU_GENESYS_USBHUB_DEVICE(device); guint64 tmp; if (g_strcmp0(key, "GenesysUsbhubDeviceTransferSize") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->flash_rw_size = tmp; /* success */ return TRUE; } if (g_strcmp0(key, "GenesysUsbhubSwitchRequest") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->vcs.req_switch = tmp; /* success */ return TRUE; } if (g_strcmp0(key, "GenesysUsbhubReadRequest") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->vcs.req_read = tmp; /* success */ return TRUE; } if (g_strcmp0(key, "GenesysUsbhubWriteRequest") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->vcs.req_write = tmp; /* success */ return TRUE; } /* failure */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_genesys_usbhub_device_init(FuGenesysUsbhubDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_NO_GENERIC_GUIDS); fu_device_add_protocol(FU_DEVICE(self), "com.genesys.usbhub"); fu_device_retry_set_delay(FU_DEVICE(self), 30); /* 30ms */ fu_device_set_remove_delay(FU_DEVICE(self), 5000); /* 5s */ fu_device_register_private_flag(FU_DEVICE(self), FU_GENESYS_USBHUB_FLAG_HAS_MSTAR_SCALER); fu_device_register_private_flag(FU_DEVICE(self), FU_GENESYS_USBHUB_FLAG_HAS_PUBLIC_KEY); fu_device_set_install_duration(FU_DEVICE(self), 9); /* 9 s */ self->vcs.req_switch = GENESYS_USBHUB_GL_HUB_SWITCH; self->vcs.req_read = GENESYS_USBHUB_GL_HUB_READ; self->vcs.req_write = GENESYS_USBHUB_GL_HUB_WRITE; self->flash_erase_delay = 8000; /* 8s */ self->flash_write_delay = 500; /* 500ms */ self->flash_block_size = 0x10000; /* 64KB */ self->flash_sector_size = 0x1000; /* 4KB */ self->flash_rw_size = 0x40; /* 64B */ self->is_gl352350 = FALSE; self->has_codesign = FALSE; self->has_hw_codesign = FALSE; self->codesign_check = FU_GENESYS_VS_CODESIGN_CHECK_UNSUPPORTED; self->codesign = FU_GENESYS_FW_CODESIGN_NONE; } static void fu_genesys_usbhub_device_finalize(GObject *object) { FuGenesysUsbhubDevice *self = FU_GENESYS_USBHUB_DEVICE(object); if (self->st_static_ts != NULL) fu_struct_genesys_ts_static_unref(self->st_static_ts); if (self->st_dynamic_ts != NULL) g_byte_array_unref(self->st_dynamic_ts); if (self->st_fwinfo_ts != NULL) g_byte_array_unref(self->st_fwinfo_ts); if (self->st_vendor_ts != NULL) g_byte_array_unref(self->st_vendor_ts); if (self->st_project_ts != NULL) g_byte_array_unref(self->st_project_ts); if (self->hub_fw_bank1_data != NULL) g_bytes_unref(self->hub_fw_bank1_data); if (self->st_codesign != NULL) g_byte_array_unref(self->st_codesign); if (self->st_public_key != NULL) g_byte_array_unref(self->st_public_key); if (self->cfi_device != NULL) g_object_unref(self->cfi_device); G_OBJECT_CLASS(fu_genesys_usbhub_device_parent_class)->finalize(object); } static void fu_genesys_usbhub_device_class_init(FuGenesysUsbhubDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->finalize = fu_genesys_usbhub_device_finalize; device_class->probe = fu_genesys_usbhub_device_probe; device_class->open = fu_genesys_usbhub_device_open; device_class->close = fu_genesys_usbhub_device_close; device_class->setup = fu_genesys_usbhub_device_setup; device_class->dump_firmware = fu_genesys_usbhub_device_dump_firmware; device_class->prepare = fu_genesys_usbhub_device_prepare; device_class->prepare_firmware = fu_genesys_usbhub_device_prepare_firmware; device_class->write_firmware = fu_genesys_usbhub_device_write_firmware; device_class->set_progress = fu_genesys_usbhub_device_set_progress; device_class->detach = fu_genesys_usbhub_device_detach; device_class->attach = fu_genesys_usbhub_device_attach; device_class->to_string = fu_genesys_usbhub_device_to_string; device_class->set_quirk_kv = fu_genesys_usbhub_device_set_quirk_kv; } fwupd-2.0.10/plugins/genesys/fu-genesys-usbhub-device.h000066400000000000000000000007171501337203100230360ustar00rootroot00000000000000/* * Copyright 2021 Ricardo Cañuelo * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_GENESYS_USBHUB_DEVICE (fu_genesys_usbhub_device_get_type()) G_DECLARE_FINAL_TYPE(FuGenesysUsbhubDevice, fu_genesys_usbhub_device, FU, GENESYS_USBHUB_DEVICE, FuUsbDevice) void fu_genesys_usbhub_device_set_hid_channel(FuDevice *device, FuDevice *channel); fwupd-2.0.10/plugins/genesys/fu-genesys-usbhub-firmware.c000066400000000000000000000375101501337203100234070ustar00rootroot00000000000000/* * Copyright 2023 Adam.Chen * Copyright 2021 Gaël PORTAY * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-genesys-common.h" #include "fu-genesys-usbhub-codesign-firmware.h" #include "fu-genesys-usbhub-dev-firmware.h" #include "fu-genesys-usbhub-firmware.h" #include "fu-genesys-usbhub-pd-firmware.h" #include "fu-genesys-usbhub-struct.h" struct _FuGenesysUsbhubFirmware { FuFirmwareClass parent_instance; GByteArray *st_static_ts; FuGenesysChip chip; }; G_DEFINE_TYPE(FuGenesysUsbhubFirmware, fu_genesys_usbhub_firmware, FU_TYPE_FIRMWARE) static gboolean fu_genesys_usbhub_firmware_get_chip(FuGenesysUsbhubFirmware *self, GInputStream *stream, GError **error) { guint8 project_ic_type[6]; /* recognize GL3523 code base product */ if (!fu_input_stream_read_safe( stream, project_ic_type, sizeof(project_ic_type), 0, /* dst */ GENESYS_USBHUB_STATIC_TOOL_STRING_OFFSET_GL3523 + FU_STRUCT_GENESYS_TS_STATIC_OFFSET_MASK_PROJECT_IC_TYPE, /* src */ sizeof(project_ic_type), error)) return FALSE; if (memcmp(project_ic_type, "3521", 4) == 0) { self->chip.model = ISP_MODEL_HUB_GL3521; self->chip.revision = 10 * (project_ic_type[4] - '0') + (project_ic_type[5] - '0'); return TRUE; } if (memcmp(project_ic_type, "3523", 4) == 0) { self->chip.model = ISP_MODEL_HUB_GL3523; self->chip.revision = 10 * (project_ic_type[4] - '0') + (project_ic_type[5] - '0'); return TRUE; } /* recognize GL3590 */ if (!fu_input_stream_read_safe( stream, project_ic_type, sizeof(project_ic_type), 0, /* dst */ GENESYS_USBHUB_STATIC_TOOL_STRING_OFFSET_GL3590 + FU_STRUCT_GENESYS_TS_STATIC_OFFSET_MASK_PROJECT_IC_TYPE, /* src */ sizeof(project_ic_type), error)) return FALSE; if (memcmp(project_ic_type, "3590", 4) == 0) { self->chip.model = ISP_MODEL_HUB_GL3590; self->chip.revision = 10 * (project_ic_type[4] - '0') + (project_ic_type[5] - '0'); return TRUE; } /* recognize GL3525 first edition */ if (!fu_input_stream_read_safe( stream, project_ic_type, sizeof(project_ic_type), 0, /* dst */ GENESYS_USBHUB_STATIC_TOOL_STRING_OFFSET_GL3525 + FU_STRUCT_GENESYS_TS_STATIC_OFFSET_MASK_PROJECT_IC_TYPE, /* src */ sizeof(project_ic_type), error)) return FALSE; if (memcmp(project_ic_type, "3525", 4) == 0) { self->chip.model = ISP_MODEL_HUB_GL3525; self->chip.revision = 10 * (project_ic_type[4] - '0') + (project_ic_type[5] - '0'); return TRUE; } /* recognize GL3525 second edition */ if (!fu_input_stream_read_safe( stream, project_ic_type, sizeof(project_ic_type), 0, /* dst */ GENESYS_USBHUB_STATIC_TOOL_STRING_OFFSET_GL3525_V2 + FU_STRUCT_GENESYS_TS_STATIC_OFFSET_MASK_PROJECT_IC_TYPE, /* src */ sizeof(project_ic_type), error)) return FALSE; if (memcmp(project_ic_type, "3525", 4) == 0) { self->chip.model = ISP_MODEL_HUB_GL3525; self->chip.revision = 10 * (project_ic_type[4] - '0') + (project_ic_type[5] - '0'); return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported IC"); return FALSE; } gboolean fu_genesys_usbhub_firmware_verify_checksum(GInputStream *stream, GError **error) { gsize streamsz = 0; guint16 fw_checksum = 0; guint16 checksum = 0; g_autoptr(GInputStream) stream_tmp = NULL; /* get checksum */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz < sizeof(checksum)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "stream was too small"); return FALSE; } if (!fu_input_stream_read_u16(stream, streamsz - sizeof(checksum), &fw_checksum, G_BIG_ENDIAN, error)) { g_prefix_error(error, "failed to get checksum: "); return FALSE; } /* calculate checksum */ stream_tmp = fu_partial_input_stream_new(stream, 0, streamsz - sizeof(checksum), error); if (stream_tmp == NULL) return FALSE; if (!fu_input_stream_compute_sum16(stream_tmp, &checksum, error)) return FALSE; if (checksum != fw_checksum) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "checksum mismatch, got 0x%04x, expected 0x%04x", checksum, fw_checksum); return FALSE; } return TRUE; } gboolean fu_genesys_usbhub_firmware_calculate_size(GInputStream *stream, gsize *size, GError **error) { guint8 kbs = 0; if (!fu_input_stream_read_u8(stream, GENESYS_USBHUB_CODE_SIZE_OFFSET, &kbs, error)) { g_prefix_error(error, "failed to get codesize: "); return FALSE; } if (kbs == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid codesize"); return FALSE; } if (size != NULL) *size = 1024 * kbs; return TRUE; } gboolean fu_genesys_usbhub_firmware_ensure_version(FuFirmware *firmware, GError **error) { guint16 version_raw = 0; g_autoptr(GBytes) fw = NULL; fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; if (!fu_memread_uint16_safe(g_bytes_get_data(fw, NULL), g_bytes_get_size(fw), GENESYS_USBHUB_VERSION_OFFSET, &version_raw, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to get version: "); return FALSE; } fu_firmware_set_version_raw(firmware, version_raw); /* success */ return TRUE; } static gboolean fu_genesys_usbhub_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_genesys_firmware_hdr_validate_stream(stream, offset, error); } static gboolean fu_genesys_usbhub_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuGenesysUsbhubFirmware *self = FU_GENESYS_USBHUB_FIRMWARE(firmware); gsize code_size = 0; gsize offset = 0; gsize streamsz = 0; guint32 static_ts_offset = 0; g_autoptr(GInputStream) stream_trunc = NULL; /* get chip */ if (!fu_genesys_usbhub_firmware_get_chip(self, stream, error)) { g_prefix_error(error, "failed to get chip: "); return FALSE; } fu_firmware_set_id(firmware, fu_genesys_fw_type_to_string(FU_GENESYS_FW_TYPE_HUB)); fu_firmware_set_idx(firmware, FU_GENESYS_FW_TYPE_HUB); fu_firmware_set_alignment(firmware, FU_FIRMWARE_ALIGNMENT_1K); /* get static tool string */ switch (self->chip.model) { case ISP_MODEL_HUB_GL3521: static_ts_offset = GENESYS_USBHUB_STATIC_TOOL_STRING_OFFSET_GL3521; break; case ISP_MODEL_HUB_GL3523: static_ts_offset = GENESYS_USBHUB_STATIC_TOOL_STRING_OFFSET_GL3523; break; case ISP_MODEL_HUB_GL3590: static_ts_offset = GENESYS_USBHUB_STATIC_TOOL_STRING_OFFSET_GL3590; break; case ISP_MODEL_HUB_GL3525: { guint8 configuration = 0; if (!fu_input_stream_read_u8(stream, GENESYS_USBHUB_FW_CONFIGURATION_OFFSET, &configuration, error)) return FALSE; if (configuration == GENESYS_USBHUB_FW_CONFIGURATION_NEW_FORMAT || configuration == GENESYS_USBHUB_FW_CONFIGURATION_NEW_FORMAT_V2) static_ts_offset = GENESYS_USBHUB_STATIC_TOOL_STRING_OFFSET_GL3525_V2; else static_ts_offset = GENESYS_USBHUB_STATIC_TOOL_STRING_OFFSET_GL3525; break; } default: break; } self->st_static_ts = fu_struct_genesys_ts_static_parse_stream(stream, static_ts_offset, error); /* deduce code size */ switch (self->chip.model) { case ISP_MODEL_HUB_GL3521: code_size = 0x5000; break; case ISP_MODEL_HUB_GL3523: { if (self->chip.revision == 50) { if (!fu_genesys_usbhub_firmware_calculate_size(stream, &code_size, error)) return FALSE; } else { code_size = 0x6000; } break; } case ISP_MODEL_HUB_GL3590: case ISP_MODEL_HUB_GL3525: { if (!fu_genesys_usbhub_firmware_calculate_size(stream, &code_size, error)) return FALSE; break; } default: break; } /* truncate to correct size */ stream_trunc = fu_partial_input_stream_new(stream, offset, code_size, error); if (stream_trunc == NULL) return FALSE; if (!fu_firmware_set_stream(firmware, stream_trunc, error)) return FALSE; /* calculate checksum */ if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { if (!fu_genesys_usbhub_firmware_verify_checksum(stream_trunc, error)) return FALSE; } /* get firmware version */ if (!fu_genesys_usbhub_firmware_ensure_version(firmware, error)) return FALSE; /* parse remaining firmware bytes */ offset += code_size; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; while (offset < streamsz) { g_autoptr(FuFirmware) firmware_sub = NULL; firmware_sub = fu_firmware_new_from_gtypes(stream, offset, flags | FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error, FU_TYPE_GENESYS_USBHUB_DEV_FIRMWARE, FU_TYPE_GENESYS_USBHUB_PD_FIRMWARE, FU_TYPE_GENESYS_USBHUB_CODESIGN_FIRMWARE, G_TYPE_INVALID); if (firmware_sub == NULL) { g_prefix_error(error, "fw bytes have dual hub firmware: "); return FALSE; } fu_firmware_set_offset(firmware_sub, offset); fu_firmware_add_image(firmware, firmware_sub); offset += fu_firmware_get_size(firmware_sub); } /* success */ return TRUE; } static GByteArray * fu_genesys_usbhub_firmware_write(FuFirmware *firmware, GError **error) { FuGenesysUsbhubFirmware *self = FU_GENESYS_USBHUB_FIRMWARE(firmware); g_autoptr(GByteArray) buf = g_byte_array_new(); guint16 code_size = 0x6000; guint16 checksum; /* fixed size */ fu_byte_array_set_size(buf, code_size, 0x00); /* signature */ if (!fu_memcpy_safe(buf->data, buf->len, FU_STRUCT_GENESYS_FIRMWARE_HDR_OFFSET_MAGIC, /* dst */ (const guint8 *)FU_STRUCT_GENESYS_FIRMWARE_HDR_DEFAULT_MAGIC, FU_STRUCT_GENESYS_FIRMWARE_HDR_SIZE_MAGIC, 0x0, /* src */ FU_STRUCT_GENESYS_FIRMWARE_HDR_SIZE_MAGIC, error)) return NULL; /* static tool string */ if (self->st_static_ts != NULL) { if (!fu_memcpy_safe(buf->data, buf->len, GENESYS_USBHUB_STATIC_TOOL_STRING_OFFSET_GL3523, /* dst */ self->st_static_ts->data, self->st_static_ts->len, 0x0, /* src */ self->st_static_ts->len, error)) return NULL; } /* version */ if (!fu_memwrite_uint16_safe(buf->data, buf->len, GENESYS_USBHUB_VERSION_OFFSET, 0x1234, // TODO: parse from firmware version string G_BIG_ENDIAN, error)) return NULL; /* checksum */ checksum = fu_sum16(buf->data, code_size - sizeof(checksum)); if (!fu_memwrite_uint16_safe(buf->data, buf->len, code_size - sizeof(guint16), checksum, G_BIG_ENDIAN, error)) return NULL; /* success */ return g_steal_pointer(&buf); } static void fu_genesys_usbhub_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuGenesysUsbhubFirmware *self = FU_GENESYS_USBHUB_FIRMWARE(firmware); if (self->st_static_ts != NULL) { FuGenesysTsVersion tool_string_version = fu_struct_genesys_ts_static_get_tool_string_version(self->st_static_ts); g_autofree gchar *mask_project_code = fu_struct_genesys_ts_static_get_mask_project_code(self->st_static_ts); g_autofree gchar *mask_project_hardware = fu_struct_genesys_ts_static_get_mask_project_hardware(self->st_static_ts); g_autofree gchar *mask_project_firmware = fu_struct_genesys_ts_static_get_mask_project_firmware(self->st_static_ts); g_autofree gchar *mask_project_ic_type = fu_struct_genesys_ts_static_get_mask_project_ic_type(self->st_static_ts); g_autofree gchar *running_project_code = fu_struct_genesys_ts_static_get_mask_project_code(self->st_static_ts); g_autofree gchar *running_project_hardware = fu_struct_genesys_ts_static_get_running_project_hardware(self->st_static_ts); g_autofree gchar *running_project_firmware = fu_struct_genesys_ts_static_get_running_project_firmware(self->st_static_ts); g_autofree gchar *running_project_ic_type = fu_struct_genesys_ts_static_get_running_project_ic_type(self->st_static_ts); fu_xmlb_builder_insert_kv(bn, "tool_string_version", fu_genesys_ts_version_to_string(tool_string_version)); fu_xmlb_builder_insert_kv(bn, "mask_project_code", mask_project_code); if (mask_project_hardware != NULL) mask_project_hardware[0] += 0x11; /* '0' -> 'A'... */ fu_xmlb_builder_insert_kv(bn, "mask_project_hardware", mask_project_hardware); fu_xmlb_builder_insert_kv(bn, "mask_project_firmware", mask_project_firmware); fu_xmlb_builder_insert_kv(bn, "mask_project_ic_type", mask_project_ic_type); fu_xmlb_builder_insert_kv(bn, "running_project_code", running_project_code); if (running_project_hardware != NULL) running_project_hardware[0] += 0x11; /* '0' -> 'A'... */ fu_xmlb_builder_insert_kv(bn, "running_project_hardware", running_project_hardware); fu_xmlb_builder_insert_kv(bn, "running_project_firmware", running_project_firmware); fu_xmlb_builder_insert_kv(bn, "running_project_ic_type", running_project_ic_type); } } static gboolean fu_genesys_usbhub_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { FuGenesysUsbhubFirmware *self = FU_GENESYS_USBHUB_FIRMWARE(firmware); const gchar *tmp; /* optional properties */ self->st_static_ts = fu_struct_genesys_ts_static_new(); tmp = xb_node_query_text(n, "tool_string_version", NULL); if (tmp == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid tool_string_version"); return FALSE; } else { fu_struct_genesys_ts_static_set_tool_string_version(self->st_static_ts, tmp[0]); } /* mask_project_code */ tmp = xb_node_query_text(n, "mask_project_code", NULL); if (tmp != NULL) { gsize len = strlen(tmp); if (len != 4) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid mask_project_code %s, got 0x%x length", tmp, (guint)len); return FALSE; } if (!fu_struct_genesys_ts_static_set_mask_project_code(self->st_static_ts, tmp, error)) return FALSE; } /* mask_project_ic_type */ tmp = xb_node_query_text(n, "mask_project_ic_type", NULL); if (tmp != NULL) { gsize len = strlen(tmp); if (len != 6) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid mask_project_ic_type %s, got 0x%x length", tmp, (guint)len); return FALSE; } if (!fu_struct_genesys_ts_static_set_mask_project_ic_type(self->st_static_ts, tmp, error)) return FALSE; } /* success */ return TRUE; } static gchar * fu_genesys_usbhub_firmware_convert_version(FuFirmware *firmware, guint64 version_raw) { return fu_version_from_uint16_hex(version_raw, fu_firmware_get_version_format(firmware)); } static void fu_genesys_usbhub_firmware_init(FuGenesysUsbhubFirmware *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); fu_firmware_set_version_format(FU_FIRMWARE(self), FWUPD_VERSION_FORMAT_PAIR); } static void fu_genesys_usbhub_firmware_finalize(GObject *object) { FuGenesysUsbhubFirmware *self = FU_GENESYS_USBHUB_FIRMWARE(object); if (self->st_static_ts != NULL) fu_struct_genesys_ts_static_unref(self->st_static_ts); G_OBJECT_CLASS(fu_genesys_usbhub_firmware_parent_class)->finalize(object); } static void fu_genesys_usbhub_firmware_class_init(FuGenesysUsbhubFirmwareClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->convert_version = fu_genesys_usbhub_firmware_convert_version; object_class->finalize = fu_genesys_usbhub_firmware_finalize; firmware_class->validate = fu_genesys_usbhub_firmware_validate; firmware_class->parse = fu_genesys_usbhub_firmware_parse; firmware_class->export = fu_genesys_usbhub_firmware_export; firmware_class->build = fu_genesys_usbhub_firmware_build; firmware_class->write = fu_genesys_usbhub_firmware_write; } FuFirmware * fu_genesys_usbhub_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_GENESYS_USBHUB_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/genesys/fu-genesys-usbhub-firmware.h000066400000000000000000000013131501337203100234040ustar00rootroot00000000000000/* * Copyright 2021 Gaël PORTAY * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_GENESYS_USBHUB_FIRMWARE (fu_genesys_usbhub_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuGenesysUsbhubFirmware, fu_genesys_usbhub_firmware, FU, GENESYS_USBHUB_FIRMWARE, FuFirmware) FuFirmware * fu_genesys_usbhub_firmware_new(void); gboolean fu_genesys_usbhub_firmware_verify_checksum(GInputStream *stream, GError **error); gboolean fu_genesys_usbhub_firmware_calculate_size(GInputStream *stream, gsize *size, GError **error); gboolean fu_genesys_usbhub_firmware_ensure_version(FuFirmware *firmware, GError **error); fwupd-2.0.10/plugins/genesys/fu-genesys-usbhub-pd-firmware.c000066400000000000000000000045301501337203100240040ustar00rootroot00000000000000/* * Copyright 2023 Adam.Chen * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-genesys-common.h" #include "fu-genesys-usbhub-firmware.h" #include "fu-genesys-usbhub-pd-firmware.h" #include "fu-genesys-usbhub-struct.h" struct _FuGenesysUsbhubPdFirmware { FuFirmwareClass parent_instance; }; G_DEFINE_TYPE(FuGenesysUsbhubPdFirmware, fu_genesys_usbhub_pd_firmware, FU_TYPE_FIRMWARE) static gboolean fu_genesys_usbhub_pd_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_genesys_pd_firmware_hdr_validate_stream(stream, offset, error); } static gboolean fu_genesys_usbhub_pd_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { gsize code_size = 0; g_autoptr(GInputStream) stream_trunc = NULL; fu_firmware_set_id(firmware, fu_genesys_fw_type_to_string(FU_GENESYS_FW_TYPE_PD)); fu_firmware_set_idx(firmware, FU_GENESYS_FW_TYPE_PD); fu_firmware_set_alignment(firmware, FU_FIRMWARE_ALIGNMENT_1K); /* truncate to correct size */ if (!fu_genesys_usbhub_firmware_calculate_size(stream, &code_size, error)) { g_prefix_error(error, "not valid for pd: "); return FALSE; } stream_trunc = fu_partial_input_stream_new(stream, 0x0, code_size, error); if (stream_trunc == NULL) return FALSE; if (!fu_firmware_set_stream(firmware, stream_trunc, error)) return FALSE; /* calculate checksum */ if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { if (!fu_genesys_usbhub_firmware_verify_checksum(stream_trunc, error)) { g_prefix_error(error, "not valid for pd: "); return FALSE; } } /* get firmware version */ if (!fu_genesys_usbhub_firmware_ensure_version(firmware, error)) { g_prefix_error(error, "not valid for pd: "); return FALSE; } /* success */ return TRUE; } static void fu_genesys_usbhub_pd_firmware_init(FuGenesysUsbhubPdFirmware *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); } static void fu_genesys_usbhub_pd_firmware_class_init(FuGenesysUsbhubPdFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_genesys_usbhub_pd_firmware_validate; firmware_class->parse = fu_genesys_usbhub_pd_firmware_parse; } fwupd-2.0.10/plugins/genesys/fu-genesys-usbhub-pd-firmware.h000066400000000000000000000006121501337203100240060ustar00rootroot00000000000000/* * Copyright 2023 Adam.Chen * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_GENESYS_USBHUB_PD_FIRMWARE (fu_genesys_usbhub_pd_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuGenesysUsbhubPdFirmware, fu_genesys_usbhub_pd_firmware, FU, GENESYS_USBHUB_PD_FIRMWARE, FuFirmware) fwupd-2.0.10/plugins/genesys/fu-genesys-usbhub.rs000066400000000000000000000153111501337203100217720ustar00rootroot00000000000000// Copyright 2023 Adam.Chen // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ValidateStream, Default)] #[repr(C, packed)] struct FuStructGenesysFirmwareHdr { reserved: [u8; 252], magic: [char; 4] == "XROM", } #[derive(ValidateStream, Default)] #[repr(C, packed)] struct FuStructGenesysDevFirmwareHdr { reserved: [u8; 252], magic: [char; 4] == "HOST", } #[derive(ValidateStream, Default)] #[repr(C, packed)] struct FuStructGenesysPdFirmwareHdr { reserved: [u8; 252], magic: [char; 4] == "PRDY", } // Tool String Descriptor #[repr(u8)] #[derive(ToString)] enum FuGenesysTsVersion { Dynamic_9Byte = 0x30, Bonding, BondingQc, VendorSupport, MultiToken, Dynamic_2nd, Reserved, Dynamic_13Byte, BrandProject, } #[derive(Parse, ParseStream, New)] #[repr(C, packed)] struct FuStructGenesysTsStatic { tool_string_version: FuGenesysTsVersion, mask_project_code: [char; 4], mask_project_hardware: char, // 0=a, 1=b... mask_project_firmware: [char; 2], // 01,02,03... mask_project_ic_type: [char; 6], // 352310=GL3523-10 (ASCII string) running_project_code: [char; 4], running_project_hardware: char, running_project_firmware: [char; 2], running_project_ic_type: [char; 6], firmware_version: [char; 4], // MMmm=MM.mm (ASCII string) } #[derive(Parse)] #[repr(C, packed)] struct FuStructGenesysTsDynamicGl3523 { running_mode: char, // 'M' for mask code, the others for bank code ss_port_number: char, // super-speed port number hs_port_number: char, // high-speed port number ss_connection_status: char, // bit field. ON = DFP is a super-speed device hs_connection_status: char, // bit field. ON = DFP is a high-speed device fs_connection_status: char, // bit field. ON = DFP is a full-speed device ls_connection_status: char, // bit field. ON = DFP is a low-speed device charging: char, // bit field. ON = DFP is a charging port non_removable_port_status: char, // bit field. ON = DFP is a non-removable port // Bonding reports Hardware register status for GL3523: (ASCII) // 2 / 4 ports : 1 means 4 ports, 0 means 2 ports // MTT / STT : 1 means Multi Token Transfer, 0 means Single TT // Type - C : 1 means disable, 0 means enable // QC : 1 means disable, 0 means enable // Flash dump location : 1 means 32KB offset bank 1, 0 means 0 offset bank 0. // Tool string Version 1: // Bit3 : Flash dump location // BIT2 : Type - C // BIT1 : MTT / STT // BIT0 : 2 / 4 ports // Tool string Version 2 or newer : // Bit4 : Flash dump location // BIT3 : Type - C // BIT2 : MTT / STT // BIT1 : 2 / 4 ports // BIT0 : QC // Default use '0'~'F', plus Bit4 may over value, should extract that. bonding: char, } #[derive(Parse)] #[repr(C, packed)] struct FuStructGenesysTsDynamicGl3590 { running_mode: char, ss_port_number: char, hs_port_number: char, ss_connection_status: char, hs_connection_status: char, fs_connection_status: char, ls_connection_status: char, charging: char, non_removable_port_status: char, // Bonding for GL3590-10/20: // Bit7 : Flash dump location, 0 means bank 0, 1 means bank 1. bonding: u8, } #[derive(ToString)] #[repr(u8)] enum FuGenesysFwStatus { Mask = 0x30, Bank1, Bank2, } #[derive(Parse)] #[repr(C, packed)] struct FuStructGenesysTsDynamicGl359030 { running_mode: char, ss_port_number: char, hs_port_number: char, ss_connection_status: char, hs_connection_status: char, fs_connection_status: char, ls_connection_status: char, charging: char, non_removable_port_status: char, bonding: u8, hub_fw_status: FuGenesysFwStatus, dev_fw_status: FuGenesysFwStatus, dev_fw_version: u16le, } #[derive(Parse)] #[repr(C, packed)] struct FuStructGenesysTsDynamicGl3525 { running_mode: char, ss_port_number: char, hs_port_number: char, ss_connection_status: char, hs_connection_status: char, fs_connection_status: char, ls_connection_status: char, charging: char, non_removable_port_status: char, bonding: u8, hub_fw_status: FuGenesysFwStatus, pd_fw_status: FuGenesysFwStatus, pd_fw_version: u16le, dev_fw_status: FuGenesysFwStatus, dev_fw_version: u16le, } #[derive(Parse)] #[repr(C, packed)] struct FuStructGenesysTsFirmwareInfo { tool_version: [u8; 6], // ISP tool defined by itself address_mode: u8, // 3 or 4: support 3 or 4-bytes address, others are no meaning. build_fw_time: [char; 12], // YYYYMMDDhhmm update_fw_time: [char; 12], // YYYYMMDDhhmm } #[derive(ToString)] #[repr(u8)] enum FuGenesysVsCodesignCheck { Unsupported = 0x30, Scaler, Fw, Master, // there is a master hub has Scaler or Hw, and this hub verified by the master. Reserved, Hw, } #[repr(u8)] enum FuGenesysVsHidIsp { Unsupported = 0x30, Support, CodesignNReset, // Support Codesign ISP Bank2 FW without reset. } #[derive(New, Parse)] #[repr(C, packed)] struct FuStructGenesysTsVendorSupport { version: [char; 2], reserved1: [char; 8], codesign_check: FuGenesysVsCodesignCheck, // offset: 0x0a reserved2: [char; 4], hid_isp: FuGenesysVsHidIsp, // offset: 0x0f reserved3: [char; 15], } #[derive(Parse)] #[repr(C, packed)] struct FuStructGenesysTsBrandProject { project: [char; 15], } // Firmware info #[derive(ToString)] enum FuGenesysFwCodesign { None, Rsa, Ecdsa, } #[derive(ParseStream, ValidateStream, Default)] #[repr(C, packed)] struct FuStructGenesysFwCodesignInfoRsa { tag_n: u32be == 0x4E203D20, // 'N = ' text_n: [char; 512], end_n: u16be == 0x0D0A, tag_e: u32be == 0x45203D20, // 'E = ' text_e: [char; 6], end_e: u16be == 0x0D0A, signature: [u8; 256], } #[derive(Parse, Validate, Default)] #[repr(C, packed)] struct FuStructGenesysFwRsaPublicKeyText { tag_n: u32be == 0x4E203D20, // 'N = ' text_n: [char; 512], end_n: u16be == 0x0D0A, tag_e: u32be == 0x45203D20, // 'E = ' text_e: [char; 6], end_e: u16be == 0x0D0A, } #[derive(Parse, ParseStream, Validate, ValidateStream)] #[repr(C, packed)] struct FuStructGenesysFwCodesignInfoEcdsa { hash: [u8; 32], key: [u8; 64], signature: [u8; 64], } #[derive(Parse, Validate)] #[repr(C, packed)] struct FuStructGenesysFwEcdsaPublicKey { key: [u8; 64], } #[derive(ToString)] enum FuGenesysFwType { Hub, // inside hub start DevBridge, Pd, Codesign, // inside hub end InsideHubCount, Scaler, // vendor support start Unknown = 0xff, } fwupd-2.0.10/plugins/genesys/genesys.quirk000066400000000000000000000043271501337203100206100ustar00rootroot00000000000000# Genesys Logic USB Hubs [USB\VID_05E3&PID_0610] Plugin = genesys GType = FuGenesysUsbhubDevice [USB\VID_05E3&PID_0630] Plugin = genesys GType = FuGenesysUsbhubDevice # Genesys Logic USB Hubs' HID [USB\VID_05E3&PID_0102] Plugin = genesys GType = FuGenesysHubhidDevice [USB\VID_05E3&PID_0155] Plugin = genesys GType = FuGenesysHubhidDevice # Brand Products # Google Servo Dock [USB\VID_05E3&PID_0610&VENDOR_GOOGLE&IC_359010&BONDING_6A&PORTNUM_25&VENDORSUP_E8A9A9E4-6C17-5F9C-B7BD-CDA49FE66D75] Name = Servo Dock # Google EVB [USB\VID_05E3&PID_0630&PROJECT_DD5E70E4-0F1A-57EB-A0F9-44555ECC56EE] Name = GL3525S USB Hub EVB Flags = dual-image # HP M2xfd # usbhub [USB\VID_03F0&PID_0610] Plugin = genesys GType = FuGenesysUsbhubDevice Name = HP USB-C Controller Flags = dual-image,has-public-key GenesysUsbhubSwitchRequest = 0xA1 GenesysUsbhubReadRequest = 0xA2 GenesysUsbhubWriteRequest = 0xA3 [USB\VID_03F0&PID_0610&PUBKEY_AB859399-95B8-5817-B521-9AD8CC7F5BD6] Name = HP M24fd USB-C Hub Flags = dual-image,has-public-key,has-mstar-scaler GenesysUsbhubSwitchRequest = 0xA1 GenesysUsbhubReadRequest = 0xA2 GenesysUsbhubWriteRequest = 0xA3 [USB\VID_03F0&PID_0610&PUBKEY_6BE97D77-C2BA-5AA2-B7DF-B9B318BEC2B5] Name = HP M27fd USB-C Hub Flags = dual-image,has-public-key,has-mstar-scaler GenesysUsbhubSwitchRequest = 0xA1 GenesysUsbhubReadRequest = 0xA2 GenesysUsbhubWriteRequest = 0xA3 # scaler [GENESYS_SCALER\MSTAR_TSUM_G&PUBKEY_B335BDCE-7073-5D0E-9BD3-9B69C1A6899F&PANELREV_RIM101] Name = HP M24fd USB-C Monitor Flags = use-i2c-ch0 GenesysScalerGpioOutputRegister = 0x0426 GenesysScalerGpioEnableRegister = 0x0428 GenesysScalerGpioValue = 0x01 GenesysScalerCfiFlashId = 0xC22016 [GENESYS_SCALER\MSTAR_TSUM_G&PUBKEY_847A3650-8648-586B-83C8-8B53714F37E3&PANELREV_AIM101] Name = HP M27fd USB-C Monitor Flags = use-i2c-ch0 GenesysScalerGpioOutputRegister = 0x0426 GenesysScalerGpioEnableRegister = 0x0428 GenesysScalerGpioValue = 0x01 GenesysScalerCfiFlashId = 0xC84016 [GENESYS_SCALER\MSTAR_TSUM_G&PUBKEY_847A3650-8648-586B-83C8-8B53714F37E3&PANELREV_RIM101] Name = HP M27fd USB-C Monitor Flags = use-i2c-ch0 GenesysScalerGpioOutputRegister = 0x0426 GenesysScalerGpioEnableRegister = 0x0428 GenesysScalerGpioValue = 0x01 GenesysScalerCfiFlashId = 0xC84016 fwupd-2.0.10/plugins/genesys/meson.build000066400000000000000000000014061501337203100202130ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginGenesys"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('genesys.quirk') plugin_builtins += static_library('fu_plugin_genesys', rustgen.process( 'fu-genesys-usbhub.rs', # fuzzing ), sources: [ 'fu-genesys-scaler-firmware.c', # fuzzing 'fu-genesys-usbhub-firmware.c', # fuzzing 'fu-genesys-usbhub-dev-firmware.c', # fuzzing 'fu-genesys-usbhub-pd-firmware.c', # fuzzing 'fu-genesys-usbhub-codesign-firmware.c', # fuzzing 'fu-genesys-scaler-device.c', 'fu-genesys-usbhub-device.c', 'fu-genesys-hubhid-device.c', 'fu-genesys-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) fwupd-2.0.10/plugins/genesys/tests/000077500000000000000000000000001501337203100172125ustar00rootroot00000000000000fwupd-2.0.10/plugins/genesys/tests/genesys-scaler.builder.xml000066400000000000000000000002041501337203100243010ustar00rootroot00000000000000 aGVsbG8gd29ybGQ= A fwupd-2.0.10/plugins/genesys/tests/genesys-usbhub.builder.xml000066400000000000000000000003141501337203100243220ustar00rootroot00000000000000 aGVsbG8gd29ybGQ= 1 352310 fwupd-2.0.10/plugins/goodix-moc/000077500000000000000000000000001501337203100164405ustar00rootroot00000000000000fwupd-2.0.10/plugins/goodix-moc/README.md000066400000000000000000000020601501337203100177150ustar00rootroot00000000000000--- title: Plugin: Goodix Fingerprint Sensor --- ## Introduction The plugin used for update firmware for fingerprint sensors from Goodix. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a packed binary file format. This plugin supports the following protocol ID: * `com.goodix.goodixmoc` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_27C6&PID_6001` ## Update Behavior The firmware is deployed when the device is in normal runtime mode, and the device will reset when the new firmware has been written. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x27C6` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.5.0`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Boger Wang: @boger047 fwupd-2.0.10/plugins/goodix-moc/fu-goodixmoc-common.h000066400000000000000000000023601501337203100225000ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * Copyright 2020 boger wang * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include /* protocol */ #define GX_CMD_ACK 0xAA #define GX_CMD_VERSION 0xD0 #define GX_CMD_RESET 0xB4 #define GX_CMD_UPGRADE 0x80 #define GX_CMD_UPGRADE_INIT 0x00 #define GX_CMD_UPGRADE_DATA 0x01 #define GX_CMD1_DEFAULT 0x00 #define GX_SIZE_CRC32 4 /* type covert */ #define MAKE_CMD_EX(cmd0, cmd1) ((guint16)(((cmd0) << 8) | (cmd1))) typedef struct { guint8 format[2]; guint8 fwtype[8]; guint8 fwversion[8]; guint8 customer[8]; guint8 mcu[8]; guint8 sensor[8]; guint8 algversion[8]; guint8 interface[8]; guint8 protocol[8]; guint8 flashVersion[8]; guint8 reserved[62]; } GxfpVersionInfo; typedef struct { guint8 cmd; gboolean configured; } GxfpAckMsg; typedef struct { guint8 result; union { GxfpAckMsg ack_msg; GxfpVersionInfo version_info; }; } GxfpCmdResp; typedef enum { GX_PKG_TYPE_NORMAL = 0x80, GX_PKG_TYPE_EOP = 0, } GxPkgType; typedef struct __attribute__((__packed__)) { /* nocheck:blocked */ guint8 cmd0; guint8 cmd1; guint8 pkg_flag; guint8 reserved; guint16 len; guint8 crc8; guint8 rev_crc8; } GxfpPkgHeader; fwupd-2.0.10/plugins/goodix-moc/fu-goodixmoc-device.c000066400000000000000000000307471501337203100224540ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * Copyright 2020 boger wang * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-goodixmoc-common.h" #include "fu-goodixmoc-device.h" struct _FuGoodixMocDevice { FuUsbDevice parent_instance; guint8 dummy_seq; }; G_DEFINE_TYPE(FuGoodixMocDevice, fu_goodixmoc_device, FU_TYPE_USB_DEVICE) #define GX_USB_BULK_EP_IN (3 | 0x80) #define GX_USB_BULK_EP_OUT (1 | 0x00) #define GX_USB_INTERFACE 0 #define GX_USB_DATAIN_TIMEOUT 2000 /* ms */ #define GX_USB_DATAOUT_TIMEOUT 200 /* ms */ #define GX_FLASH_TRANSFER_BLOCK_SIZE 1000 /* 1000 */ static gboolean fu_goodixmoc_device_cmd_send(FuGoodixMocDevice *self, guint8 cmd0, guint8 cmd1, GxPkgType type, GByteArray *req, GError **error) { guint32 crc_all = 0; guint32 crc_hdr = 0; gsize actual_len = 0; g_autoptr(GByteArray) buf = g_byte_array_new(); /* build header */ fu_byte_array_append_uint8(buf, cmd0); fu_byte_array_append_uint8(buf, cmd1); fu_byte_array_append_uint8(buf, type); /* pkg_flag */ fu_byte_array_append_uint8(buf, self->dummy_seq++); /* reserved */ fu_byte_array_append_uint16(buf, req->len + GX_SIZE_CRC32, G_LITTLE_ENDIAN); crc_hdr = ~fu_crc8(FU_CRC_KIND_B8_STANDARD, buf->data, buf->len); fu_byte_array_append_uint8(buf, crc_hdr); fu_byte_array_append_uint8(buf, ~crc_hdr); g_byte_array_append(buf, req->data, req->len); crc_all = fu_crc32(FU_CRC_KIND_B32_STANDARD, buf->data, buf->len); fu_byte_array_append_uint32(buf, crc_all, G_LITTLE_ENDIAN); /* send zero length package */ if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), GX_USB_BULK_EP_OUT, NULL, 0, NULL, GX_USB_DATAOUT_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to req: "); return FALSE; } fu_dump_full(G_LOG_DOMAIN, "REQST", buf->data, buf->len, 16, FU_DUMP_FLAGS_SHOW_ADDRESSES); /* send data */ if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), GX_USB_BULK_EP_OUT, buf->data, buf->len, &actual_len, GX_USB_DATAOUT_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to req: "); return FALSE; } if (actual_len != buf->len) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid length"); return FALSE; } /* success */ return TRUE; } static gboolean fu_goodixmoc_device_cmd_recv(FuGoodixMocDevice *self, GxfpCmdResp *presponse, gboolean data_reply, GError **error) { guint32 crc_actual = 0; guint32 crc_calculated = 0; gsize actual_len = 0; gsize offset = 0; g_return_val_if_fail(presponse != NULL, FALSE); /* * package format * | zlp | ack | zlp | data | */ while (1) { guint16 header_len = 0x0; guint8 header_cmd0 = 0x0; g_autoptr(GByteArray) reply = g_byte_array_new(); fu_byte_array_set_size(reply, GX_FLASH_TRANSFER_BLOCK_SIZE, 0x00); if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), GX_USB_BULK_EP_IN, reply->data, reply->len, &actual_len, /* allowed to return short read */ GX_USB_DATAIN_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to reply: "); return FALSE; } /* receive zero length package */ if (actual_len == 0) continue; fu_dump_full(G_LOG_DOMAIN, "REPLY", reply->data, actual_len, 16, FU_DUMP_FLAGS_SHOW_ADDRESSES); /* parse package header */ if (!fu_memread_uint8_safe(reply->data, reply->len, 0x0, &header_cmd0, error)) return FALSE; if (!fu_memread_uint16_safe(reply->data, reply->len, 0x4, &header_len, G_LITTLE_ENDIAN, error)) return FALSE; offset = sizeof(GxfpPkgHeader) + header_len - GX_SIZE_CRC32; crc_actual = fu_crc32(FU_CRC_KIND_B32_STANDARD, reply->data, offset); if (!fu_memread_uint32_safe(reply->data, reply->len, offset, &crc_calculated, G_LITTLE_ENDIAN, error)) return FALSE; if (crc_actual != crc_calculated) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid checksum, got 0x%x, expected 0x%x", crc_calculated, crc_actual); return FALSE; } /* parse package data */ if (!fu_memread_uint8_safe(reply->data, reply->len, sizeof(GxfpPkgHeader) + 0x00, &presponse->result, error)) return FALSE; if (header_cmd0 == GX_CMD_ACK) { if (header_len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid bufsz"); return FALSE; } if (!fu_memread_uint8_safe(reply->data, reply->len, sizeof(GxfpPkgHeader) + 0x01, &presponse->ack_msg.cmd, error)) return FALSE; } else if (header_cmd0 == GX_CMD_VERSION) { if (!fu_memcpy_safe((guint8 *)&presponse->version_info, sizeof(presponse->version_info), 0x0, /* dst */ reply->data, reply->len, sizeof(GxfpPkgHeader) + 0x01, /* src */ sizeof(GxfpVersionInfo), error)) return FALSE; } /* continue after ack received */ if (header_cmd0 == GX_CMD_ACK && data_reply) continue; break; } /* success */ return TRUE; } static gboolean fu_goodixmoc_device_cmd_xfer(FuGoodixMocDevice *device, guint8 cmd0, guint8 cmd1, GxPkgType type, GByteArray *req, GxfpCmdResp *presponse, gboolean data_reply, GError **error) { FuGoodixMocDevice *self = FU_GOODIXMOC_DEVICE(device); if (!fu_goodixmoc_device_cmd_send(self, cmd0, cmd1, type, req, error)) return FALSE; return fu_goodixmoc_device_cmd_recv(self, presponse, data_reply, error); } static gboolean fu_goodixmoc_device_setup_version(FuGoodixMocDevice *self, GError **error) { GxfpCmdResp rsp = {0}; g_autofree gchar *version = NULL; g_autoptr(GByteArray) req = g_byte_array_new(); fu_byte_array_append_uint8(req, 0); /* dummy */ if (!fu_goodixmoc_device_cmd_xfer(self, GX_CMD_VERSION, GX_CMD1_DEFAULT, GX_PKG_TYPE_EOP, req, &rsp, TRUE, error)) return FALSE; version = g_strndup((const gchar *)rsp.version_info.fwversion, sizeof(rsp.version_info.fwversion)); fu_device_set_version(FU_DEVICE(self), version); return TRUE; } static gboolean fu_goodixmoc_device_update_init(FuGoodixMocDevice *self, GError **error) { GxfpCmdResp rsp = {0}; g_autoptr(GByteArray) req = g_byte_array_new(); /* update initial */ if (!fu_goodixmoc_device_cmd_xfer(self, GX_CMD_UPGRADE, GX_CMD_UPGRADE_INIT, GX_PKG_TYPE_EOP, req, &rsp, TRUE, error)) { g_prefix_error(error, "failed to send initial update: "); return FALSE; } /* check result */ if (rsp.result != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "initial update failed [0x%x]", rsp.result); return FALSE; } return TRUE; } static gboolean fu_goodixmoc_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuGoodixMocDevice *self = FU_GOODIXMOC_DEVICE(device); GxfpCmdResp rsp = {0}; g_autoptr(GByteArray) req = g_byte_array_new(); /* reset device */ if (!fu_goodixmoc_device_cmd_xfer(self, GX_CMD_RESET, 0x03, GX_PKG_TYPE_EOP, req, &rsp, FALSE, error)) { g_prefix_error(error, "failed to send reset device: "); return FALSE; } /* check result */ if (rsp.result != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to reset device [0x%x]", rsp.result); return FALSE; } fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_goodixmoc_device_setup(FuDevice *device, GError **error) { FuGoodixMocDevice *self = FU_GOODIXMOC_DEVICE(device); /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_goodixmoc_device_parent_class)->setup(device, error)) return FALSE; /* ensure version */ if (!fu_goodixmoc_device_setup_version(self, error)) { g_prefix_error(error, "failed to get firmware version: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_goodixmoc_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuGoodixMocDevice *self = FU_GOODIXMOC_DEVICE(device); GxPkgType pkg_eop = GX_PKG_TYPE_NORMAL; GxfpCmdResp rsp = {0}; gboolean wait_data_reply = FALSE; g_autoptr(GBytes) fw = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "init"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 99, NULL); /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* build packets */ chunks = fu_chunk_array_new_from_bytes(fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, GX_FLASH_TRANSFER_BLOCK_SIZE); /* don't auto-boot firmware */ if (!fu_goodixmoc_device_update_init(self, &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to initial update: %s", error_local->message); return FALSE; } fu_progress_step_done(progress); /* write each block */ for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GByteArray) req = g_byte_array_new(); g_autoptr(GError) error_block = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; g_byte_array_append(req, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); /* the last chunk */ if (i == fu_chunk_array_length(chunks) - 1) { wait_data_reply = TRUE; pkg_eop = GX_PKG_TYPE_EOP; } if (!fu_goodixmoc_device_cmd_xfer(self, GX_CMD_UPGRADE, GX_CMD_UPGRADE_DATA, pkg_eop, req, &rsp, wait_data_reply, &error_block)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to write: %s", error_block->message); return FALSE; } /* check update status */ if (wait_data_reply && rsp.result != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to verify firmware [0x%x]", rsp.result); return FALSE; } /* update progress */ fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)i + 1, (gsize)fu_chunk_array_length(chunks)); } fu_progress_step_done(progress); /* success! */ return TRUE; } static void fu_goodixmoc_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static void fu_goodixmoc_device_init(FuGoodixMocDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SELF_RECOVERY); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_RUNTIME_VERSION); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_remove_delay(FU_DEVICE(self), 5000); fu_device_add_protocol(FU_DEVICE(self), "com.goodix.goodixmoc"); fu_device_set_name(FU_DEVICE(self), "Fingerprint Sensor"); fu_device_set_summary(FU_DEVICE(self), "Match-On-Chip fingerprint sensor"); fu_device_set_vendor(FU_DEVICE(self), "Goodix"); fu_device_set_install_duration(FU_DEVICE(self), 10); fu_device_set_firmware_size_min(FU_DEVICE(self), 0x20000); fu_device_set_firmware_size_max(FU_DEVICE(self), 0x30000); fu_usb_device_add_interface(FU_USB_DEVICE(self), GX_USB_INTERFACE); } static void fu_goodixmoc_device_class_init(FuGoodixMocDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_goodixmoc_device_write_firmware; device_class->setup = fu_goodixmoc_device_setup; device_class->attach = fu_goodixmoc_device_attach; device_class->set_progress = fu_goodixmoc_device_set_progress; } fwupd-2.0.10/plugins/goodix-moc/fu-goodixmoc-device.h000066400000000000000000000005521501337203100224500ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * Copyright 2020 boger wang * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_GOODIXMOC_DEVICE (fu_goodixmoc_device_get_type()) G_DECLARE_FINAL_TYPE(FuGoodixMocDevice, fu_goodixmoc_device, FU, GOODIXMOC_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/goodix-moc/fu-goodixmoc-plugin.c000066400000000000000000000020601501337203100224760ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * Copyright 2020 boger wang * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-goodixmoc-device.h" #include "fu-goodixmoc-plugin.h" struct _FuGoodixMocPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuGoodixMocPlugin, fu_goodixmoc_plugin, FU_TYPE_PLUGIN) static void fu_goodixmoc_plugin_init(FuGoodixMocPlugin *self) { } static void fu_goodixmoc_plugin_object_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_set_name(plugin, "goodixmoc"); } static void fu_goodixmoc_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_GOODIXMOC_DEVICE); } static void fu_goodixmoc_plugin_class_init(FuGoodixMocPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->constructed = fu_goodixmoc_plugin_object_constructed; plugin_class->constructed = fu_goodixmoc_plugin_constructed; } fwupd-2.0.10/plugins/goodix-moc/fu-goodixmoc-plugin.h000066400000000000000000000003651501337203100225110ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuGoodixMocPlugin, fu_goodixmoc_plugin, FU, GOODIXMOC_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/goodix-moc/goodixmoc.quirk000066400000000000000000000011331501337203100215030ustar00rootroot00000000000000# Goodix Fingerprint sensor [USB\VID_27C6&PID_60A2] Plugin = goodixmoc Flags = enforce-requires [USB\VID_27C6&PID_6384] Plugin = goodixmoc Flags = enforce-requires [USB\VID_27C6&PID_639C] Plugin = goodixmoc Flags = enforce-requires [USB\VID_27C6&PID_63AC] Plugin = goodixmoc Flags = enforce-requires [USB\VID_27C6&PID_6594] Plugin = goodixmoc Flags = enforce-requires [USB\VID_27C6&PID_6496] Plugin = goodixmoc Flags = enforce-requires [USB\VID_27C6&PID_659A] Plugin = goodixmoc Flags = enforce-requires [USB\VID_27C6&PID_609C] Plugin = goodixmoc Flags = enforce-requires,dual-image RemoveDelay = 10000 fwupd-2.0.10/plugins/goodix-moc/meson.build000066400000000000000000000006151501337203100206040ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginGoodixMoc"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('goodixmoc.quirk') plugin_builtins += static_library('fu_plugin_goodixmoc', sources: [ 'fu-goodixmoc-device.c', 'fu-goodixmoc-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) fwupd-2.0.10/plugins/goodix-tp/000077500000000000000000000000001501337203100163055ustar00rootroot00000000000000fwupd-2.0.10/plugins/goodix-tp/README.md000066400000000000000000000022671501337203100175730ustar00rootroot00000000000000--- title: Plugin: Goodix Touch Controller Sensor --- ## Introduction This plugin allows updating Touchpad devices from Goodix. Devices are enumerated using HID and raw I²C nodes. The I²C mode is used for ABS devices and firmware recovery of HID devices. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in an unspecified binary file format. This plugin supports the following protocol ID: * `com.goodix.goodixtp` ## GUID Generation These device uses the standard DeviceInstanceId values, e.g. * `HIDRAW\VEN_27C6&DEV_1234` ## Update Behavior The firmware is deployed when the device is in normal runtime mode, and the device will reset when the new firmware has been written. ## Vendor ID Security The vendor ID is set from the HID vendor, for example set to `HIDRAW:0x01E0` ## External Interface Access This plugin requires ioctl access to `HIDIOCSFEATURE` and `HIDIOCGFEATURE`. ## Version Considerations This plugin has been available since fwupd version `1.9.2`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Goodix: @xulinkun fwupd-2.0.10/plugins/goodix-tp/fu-goodixtp-brlb-device.c000066400000000000000000000345741501337203100231070ustar00rootroot00000000000000/* * Copyright 2023 Goodix.inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-goodixtp-brlb-device.h" #include "fu-goodixtp-brlb-firmware.h" #include "fu-goodixtp-common.h" struct _FuGoodixtpBrlbDevice { FuGoodixtpHidDevice parent_instance; }; G_DEFINE_TYPE(FuGoodixtpBrlbDevice, fu_goodixtp_brlb_device, FU_TYPE_GOODIXTP_HID_DEVICE) static gboolean fu_goodixtp_brlb_device_read_pkg(FuGoodixtpBrlbDevice *self, gsize addr, guint8 *buf, gsize bufsz, GError **error) { guint8 hidbuf[PACKAGE_LEN] = {0}; hidbuf[0] = REPORT_ID; hidbuf[1] = I2C_DIRECT_RW; hidbuf[2] = 0; hidbuf[3] = 0; hidbuf[4] = 7; hidbuf[5] = I2C_READ_FLAG; fu_memwrite_uint32(hidbuf + 6, addr, G_BIG_ENDIAN); fu_memwrite_uint16(hidbuf + 10, bufsz, G_BIG_ENDIAN); if (!fu_goodixtp_hid_device_set_report(FU_GOODIXTP_HID_DEVICE(self), hidbuf, 12, error)) return FALSE; if (!fu_goodixtp_hid_device_get_report(FU_GOODIXTP_HID_DEVICE(self), hidbuf, sizeof(hidbuf), error)) return FALSE; if (hidbuf[3] != 0 || hidbuf[4] != bufsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "Failed to read_pkg, hidbuf[3]:%d hidbuf[4]:%d", hidbuf[3], hidbuf[4]); return FALSE; } if (!fu_memcpy_safe(buf, bufsz, 0, hidbuf, sizeof(hidbuf), 5, hidbuf[4], error)) return FALSE; return TRUE; } static gboolean fu_goodixtp_brlb_device_hid_read(FuGoodixtpBrlbDevice *self, gsize addr, guint8 *buf, gsize bufsz, GError **error) { g_autoptr(GPtrArray) chunks = fu_chunk_array_mutable_new(buf, bufsz, addr, 0, PACKAGE_LEN - 12); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_goodixtp_brlb_device_read_pkg(self, fu_chunk_get_address(chk), fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; } return TRUE; } static gboolean fu_goodixtp_brlb_device_hid_write(FuGoodixtpBrlbDevice *self, gsize addr, guint8 *buf, gsize bufsz, GError **error) { g_autoptr(GPtrArray) chunks = fu_chunk_array_new(buf, bufsz, addr, 0, PACKAGE_LEN - 12); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); guint8 hidbuf[PACKAGE_LEN] = {0}; hidbuf[0] = REPORT_ID; hidbuf[1] = I2C_DIRECT_RW; if (i == chunks->len - 1) hidbuf[2] = 0x00; else hidbuf[2] = 0x01; hidbuf[3] = i; hidbuf[4] = fu_chunk_get_data_sz(chk) + 7; hidbuf[5] = I2C_WRITE_FLAG; fu_memwrite_uint32(hidbuf + 6, fu_chunk_get_address(chk), G_BIG_ENDIAN); fu_memwrite_uint16(hidbuf + 10, fu_chunk_get_data_sz(chk), G_BIG_ENDIAN); if (!fu_memcpy_safe(hidbuf, sizeof(hidbuf), 12, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0, fu_chunk_get_data_sz(chk), error)) return FALSE; if (!fu_goodixtp_hid_device_set_report(FU_GOODIXTP_HID_DEVICE(self), hidbuf, fu_chunk_get_data_sz(chk) + 12, error)) { g_prefix_error(error, "failed write data to addr=0x%x, len=%d: ", (guint)fu_chunk_get_address(chk), (gint)fu_chunk_get_data_sz(chk)); return FALSE; } } return TRUE; } static gboolean fu_goodixtp_brlb_device_send_cmd(FuGoodixtpBrlbDevice *self, guint8 cmd, guint8 *buf, gsize bufsz, GError **error) { guint8 hidbuf[PACKAGE_LEN] = {0}; hidbuf[0] = REPORT_ID; hidbuf[1] = cmd; hidbuf[2] = 0x00; hidbuf[3] = 0x00; hidbuf[4] = (guint8)bufsz; if (!fu_memcpy_safe(hidbuf, sizeof(hidbuf), 5, buf, bufsz, 0, bufsz, error)) return FALSE; if (!fu_goodixtp_hid_device_set_report(FU_GOODIXTP_HID_DEVICE(self), hidbuf, bufsz + 5, error)) { g_prefix_error(error, "send cmd[%x] failed: ", cmd); return FALSE; } return TRUE; } static gboolean fu_goodixtp_brlb_device_ensure_version(FuGoodixtpBrlbDevice *self, GError **error) { guint8 hidbuf[14] = {0}; guint8 vice_ver; guint8 inter_ver; guint8 cfg_ver; guint32 patch_vid_raw; g_autofree gchar *patch_pid = NULL; if (!fu_goodixtp_brlb_device_hid_read(self, 0x1001E, hidbuf, sizeof(hidbuf), error)) { g_prefix_error(error, "failed read PID/VID: "); return FALSE; } vice_ver = hidbuf[10]; inter_ver = hidbuf[11]; patch_pid = fu_memstrsafe(hidbuf, sizeof(hidbuf), 0x0, 8, NULL); if (patch_pid != NULL) fu_goodixtp_hid_device_set_patch_pid(FU_GOODIXTP_HID_DEVICE(self), patch_pid); patch_vid_raw = fu_memread_uint32(hidbuf + 8, G_BIG_ENDIAN); if (patch_vid_raw != 0) { g_autofree gchar *patch_vid = g_strdup_printf("%04X", patch_vid_raw); fu_goodixtp_hid_device_set_patch_vid(FU_GOODIXTP_HID_DEVICE(self), patch_vid); } fu_goodixtp_hid_device_set_sensor_id(FU_GOODIXTP_HID_DEVICE(self), hidbuf[13]); if (!fu_goodixtp_brlb_device_hid_read(self, 0x10076, hidbuf, 5, error)) { g_prefix_error(error, "Failed read config id/version: "); return FALSE; } cfg_ver = hidbuf[4]; fu_goodixtp_hid_device_set_config_ver(FU_GOODIXTP_HID_DEVICE(self), cfg_ver); fu_device_set_version_raw(FU_DEVICE(self), (vice_ver << 16) | (inter_ver << 8) | cfg_ver); return TRUE; } static gboolean fu_goodixtp_brlb_device_wait_mini_cb(FuDevice *device, gpointer user_data, GError **error) { FuGoodixtpBrlbDevice *self = FU_GOODIXTP_BRLB_DEVICE(device); guint8 hidbuf[1] = {0x0}; if (!fu_goodixtp_brlb_device_hid_read(self, 0x10010, hidbuf, sizeof(hidbuf), error)) return FALSE; if (hidbuf[0] != 0xDD) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "ack=0x%02x", hidbuf[0]); return FALSE; } return TRUE; } static gboolean fu_goodixtp_brlb_device_wait_erase_cb(FuDevice *device, gpointer user_data, GError **error) { FuGoodixtpBrlbDevice *self = FU_GOODIXTP_BRLB_DEVICE(device); guint8 hidbuf[5]; guint8 recvBuf[5] = {0x0}; memset(hidbuf, 0x55, sizeof(hidbuf)); if (!fu_goodixtp_brlb_device_hid_write(self, 0x14000, hidbuf, 5, error)) return FALSE; if (!fu_goodixtp_brlb_device_hid_read(self, 0x14000, recvBuf, 5, error)) return FALSE; if (memcmp(hidbuf, recvBuf, 5)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_READ, "sram not ready"); return FALSE; } return TRUE; } static gboolean fu_goodixtp_brlb_device_update_prepare(FuGoodixtpBrlbDevice *self, GError **error) { guint8 cmdbuf[1]; /* step 1. switch mini system */ cmdbuf[0] = 0x01; if (!fu_goodixtp_brlb_device_send_cmd(self, 0x10, cmdbuf, sizeof(cmdbuf), error)) { g_prefix_error(error, "failed send minisystem cmd: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), 100); if (!fu_device_retry_full(FU_DEVICE(self), fu_goodixtp_brlb_device_wait_mini_cb, 5, 30, NULL, error)) { g_prefix_error(error, "wait brlb minisystem status failed: "); return FALSE; } g_debug("switch mini system successfully"); /* step 2. erase flash */ cmdbuf[0] = 0x01; if (!fu_goodixtp_brlb_device_send_cmd(self, 0x11, cmdbuf, sizeof(cmdbuf), error)) { g_prefix_error(error, "Failed send erase flash cmd: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), 50); if (!fu_device_retry_full(FU_DEVICE(self), fu_goodixtp_brlb_device_wait_erase_cb, 5, 20, NULL, error)) { g_prefix_error(error, "wait brlb erase status failed: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_goodixtp_brlb_device_wait_flash_cb(FuDevice *device, gpointer user_data, GError **error) { FuGoodixtpBrlbDevice *self = FU_GOODIXTP_BRLB_DEVICE(device); guint8 hidbuf[1] = {0}; if (!fu_goodixtp_brlb_device_hid_read(self, 0x10011, hidbuf, 1, error)) { g_prefix_error(error, "Failed to read 0x10011"); return FALSE; } if (hidbuf[0] != 0xAA) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "ack=0x%02x", hidbuf[0]); return FALSE; } return TRUE; } static gboolean fu_goodixtp_brlb_device_load_sub_firmware_cb(FuDevice *device, gpointer user_data, GError **error) { FuGoodixtpBrlbDevice *self = FU_GOODIXTP_BRLB_DEVICE(device); FuChunk *chk = (FuChunk *)user_data; guint32 checksum = 0; guint8 buf_align4k[RAM_BUFFER_SIZE] = {0}; guint8 cmdbuf[10] = {0}; if (!fu_memcpy_safe(buf_align4k, sizeof(buf_align4k), 0, (guint8 *)fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0, fu_chunk_get_data_sz(chk), error)) return FALSE; /* send fw data to dram */ if (!fu_goodixtp_brlb_device_hid_write(self, 0x14000, buf_align4k, sizeof(buf_align4k), error)) { g_prefix_error(error, "write fw data failed: "); return FALSE; } /* send checksum */ for (guint i = 0; i < sizeof(buf_align4k); i += 2) { guint16 tmp_val; if (!fu_memread_uint16_safe(buf_align4k, sizeof(buf_align4k), i, &tmp_val, G_LITTLE_ENDIAN, error)) return FALSE; checksum += tmp_val; } fu_memwrite_uint16(cmdbuf, sizeof(buf_align4k), G_BIG_ENDIAN); fu_memwrite_uint32(cmdbuf + 2, fu_chunk_get_address(chk), G_BIG_ENDIAN); fu_memwrite_uint32(cmdbuf + 6, checksum, G_BIG_ENDIAN); if (!fu_goodixtp_brlb_device_send_cmd(self, 0x12, cmdbuf, sizeof(cmdbuf), error)) { g_prefix_error(error, "failed send start update cmd: "); return FALSE; } fu_device_sleep(device, 80); /* wait update finish */ if (!fu_device_retry_full(device, fu_goodixtp_brlb_device_wait_flash_cb, 10, 20, NULL, error)) { g_prefix_error(error, "wait flash status failed: "); return FALSE; } return TRUE; } static gboolean fu_goodixtp_brlb_device_update_process(FuGoodixtpBrlbDevice *self, FuChunk *chk, GError **error) { if (!fu_device_retry_full(FU_DEVICE(self), fu_goodixtp_brlb_device_load_sub_firmware_cb, 3, 10, chk, error)) { g_prefix_error(error, "load sub firmware failed, addr:0x%04x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } return TRUE; } static gboolean fu_goodixtp_brlb_device_update_finish(FuGoodixtpBrlbDevice *self, GError **error) { guint8 cmdbuf[1] = {0x01}; /* reset IC */ if (!fu_goodixtp_brlb_device_send_cmd(self, 0x13, cmdbuf, sizeof(cmdbuf), error)) { g_prefix_error(error, "failed reset IC: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), 100); return TRUE; } static gboolean fu_goodixtp_brlb_device_setup(FuDevice *device, GError **error) { FuGoodixtpBrlbDevice *self = FU_GOODIXTP_BRLB_DEVICE(device); if (!fu_goodixtp_brlb_device_ensure_version(self, error)) { g_prefix_error(error, "brlb read version failed: "); return FALSE; } return TRUE; } static FuFirmware * fu_goodixtp_brlb_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuGoodixtpBrlbDevice *self = FU_GOODIXTP_BRLB_DEVICE(device); g_autoptr(FuFirmware) firmware = fu_goodixtp_brlb_firmware_new(); if (!fu_goodixtp_brlb_firmware_parse( FU_GOODIXTP_FIRMWARE(firmware), stream, fu_goodixtp_hid_device_get_sensor_id(FU_GOODIXTP_HID_DEVICE(self)), error)) return NULL; return g_steal_pointer(&firmware); } static gboolean fu_goodixtp_brlb_device_write_image(FuGoodixtpBrlbDevice *self, FuFirmware *img, FuProgress *progress, GError **error) { g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(GInputStream) stream = NULL; stream = fu_firmware_get_stream(img, error); if (stream == NULL) return FALSE; chunks = fu_chunk_array_new_from_stream(stream, fu_firmware_get_addr(img), FU_CHUNK_PAGESZ_NONE, RAM_BUFFER_SIZE, error); if (chunks == NULL) return FALSE; fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_goodixtp_brlb_device_update_process(self, chk, error)) return FALSE; fu_device_sleep(FU_DEVICE(self), 20); fu_progress_step_done(progress); } return TRUE; } static gboolean fu_goodixtp_brlb_device_write_images(FuGoodixtpBrlbDevice *self, GPtrArray *imgs, FuProgress *progress, GError **error) { fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, imgs->len); for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); if (!fu_goodixtp_brlb_device_write_image(self, img, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); } return TRUE; } static gboolean fu_goodixtp_brlb_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuGoodixtpBrlbDevice *self = FU_GOODIXTP_BRLB_DEVICE(device); guint32 fw_ver = fu_goodixtp_firmware_get_version(FU_GOODIXTP_FIRMWARE(firmware)); g_autoptr(GPtrArray) imgs = fu_firmware_get_images(firmware); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 10, "prepare"); fu_progress_add_step(progress, FWUPD_STATUS_DOWNLOADING, 85, "download"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 5, "reload"); if (!fu_goodixtp_brlb_device_update_prepare(self, error)) return FALSE; fu_progress_step_done(progress); if (!fu_goodixtp_brlb_device_write_images(self, imgs, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); if (!fu_goodixtp_brlb_device_update_finish(self, error)) return FALSE; if (!fu_goodixtp_brlb_device_ensure_version(self, error)) return FALSE; fu_progress_step_done(progress); if (fu_device_get_version_raw(device) != fw_ver) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "update failed chip_ver:%x != bin_ver:%x", (guint)fu_device_get_version_raw(device), (guint)fw_ver); return FALSE; } return TRUE; } static void fu_goodixtp_brlb_device_init(FuGoodixtpBrlbDevice *self) { } static void fu_goodixtp_brlb_device_class_init(FuGoodixtpBrlbDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_goodixtp_brlb_device_setup; device_class->reload = fu_goodixtp_brlb_device_setup; device_class->prepare_firmware = fu_goodixtp_brlb_device_prepare_firmware; device_class->write_firmware = fu_goodixtp_brlb_device_write_firmware; } fwupd-2.0.10/plugins/goodix-tp/fu-goodixtp-brlb-device.h000066400000000000000000000005711501337203100231020ustar00rootroot00000000000000/* * Copyright 2023 Goodix.inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-goodixtp-hid-device.h" #define FU_TYPE_GOODIXTP_BRLB_DEVICE (fu_goodixtp_brlb_device_get_type()) G_DECLARE_FINAL_TYPE(FuGoodixtpBrlbDevice, fu_goodixtp_brlb_device, FU, GOODIXTP_BRLB_DEVICE, FuGoodixtpHidDevice) fwupd-2.0.10/plugins/goodix-tp/fu-goodixtp-brlb-firmware.c000066400000000000000000000075731501337203100234630ustar00rootroot00000000000000/* * Copyright 2023 Goodix.inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-goodixtp-brlb-firmware.h" #include "fu-goodixtp-common.h" #include "fu-goodixtp-struct.h" struct _FuGoodixtpBrlbFirmware { FuGoodixtpFirmware parent_instance; }; G_DEFINE_TYPE(FuGoodixtpBrlbFirmware, fu_goodixtp_brlb_firmware, FU_TYPE_GOODIXTP_FIRMWARE) #define FW_HEADER_SIZE 512 gboolean fu_goodixtp_brlb_firmware_parse(FuGoodixtpFirmware *self, GInputStream *stream, guint8 sensor_id, GError **error) { guint32 version; gsize offset_hdr; gsize offset_payload = FW_HEADER_SIZE; guint32 checksum = 0; guint32 firmware_size; guint8 subsys_num; guint8 cfg_ver = 0; gsize bufsz = 0; const guint8 *buf; g_autoptr(GByteArray) st = NULL; g_autoptr(GBytes) fw = NULL; st = fu_struct_goodix_brlb_hdr_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; firmware_size = fu_struct_goodix_brlb_hdr_get_firmware_size(st); firmware_size += 8; /* convert to blob */ fw = fu_input_stream_read_bytes(stream, 0, G_MAXSIZE, NULL, error); if (fw == NULL) return FALSE; buf = g_bytes_get_data(fw, &bufsz); /* [payload][64 bytes padding?][config] */ if ((gsize)firmware_size < bufsz) { g_autoptr(GBytes) fw_img = NULL; g_autoptr(FuFirmware) img = fu_firmware_new(); fu_firmware_set_idx(img, 4); fu_firmware_set_addr(img, 0x40000); fw_img = fu_bytes_new_offset(fw, firmware_size + 64, bufsz - (gsize)firmware_size - 64, error); if (fw_img == NULL) return FALSE; fu_firmware_set_bytes(img, fw_img); fu_firmware_add_image(FU_FIRMWARE(self), img); if (!fu_memread_uint8_safe(buf, bufsz, firmware_size + 64 + 34, &cfg_ver, error)) return FALSE; g_debug("config size:0x%x, config ver:0x%02x", (guint)fu_firmware_get_size(img), cfg_ver); } /* verify checksum */ for (guint i = 8; i < firmware_size; i += 2) { guint16 tmp_val; if (!fu_memread_uint16_safe(buf, bufsz, i, &tmp_val, G_LITTLE_ENDIAN, error)) return FALSE; checksum += tmp_val; } if (checksum != fu_struct_goodix_brlb_hdr_get_checksum(st)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid checksum"); return FALSE; } /* parse each image */ subsys_num = fu_struct_goodix_brlb_hdr_get_subsys_num(st); if (subsys_num == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid subsys_num"); return FALSE; } offset_hdr = st->len; for (guint i = 0; i < subsys_num; i++) { guint32 img_size; g_autoptr(GByteArray) st_img = NULL; st_img = fu_struct_goodix_brlb_img_parse_stream(stream, offset_hdr, error); if (st_img == NULL) return FALSE; img_size = fu_struct_goodix_brlb_img_get_size(st_img); if (fu_struct_goodix_brlb_img_get_kind(st_img) != 0x0B && fu_struct_goodix_brlb_img_get_kind(st_img) != 0x01) { g_autoptr(GBytes) fw_img = NULL; g_autoptr(FuFirmware) img = fu_firmware_new(); fu_firmware_set_idx(img, fu_struct_goodix_brlb_img_get_kind(st_img)); fu_firmware_set_addr(img, fu_struct_goodix_brlb_img_get_addr(st_img)); fw_img = fu_bytes_new_offset(fw, offset_payload, img_size, error); if (fw_img == NULL) return FALSE; fu_firmware_set_bytes(img, fw_img); if (!fu_firmware_add_image_full(FU_FIRMWARE(self), img, error)) return FALSE; } offset_hdr += st_img->len; offset_payload += img_size; } version = (fu_struct_goodix_brlb_hdr_get_vid(st) << 8) | cfg_ver; fu_goodixtp_firmware_set_version(self, version); return TRUE; } static void fu_goodixtp_brlb_firmware_init(FuGoodixtpBrlbFirmware *self) { fu_firmware_set_images_max(FU_FIRMWARE(self), 1024); } static void fu_goodixtp_brlb_firmware_class_init(FuGoodixtpBrlbFirmwareClass *klass) { } FuFirmware * fu_goodixtp_brlb_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_GOODIXTP_BRLB_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/goodix-tp/fu-goodixtp-brlb-firmware.h000066400000000000000000000010731501337203100234550ustar00rootroot00000000000000/* * Copyright 2023 Goodix.inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-goodixtp-firmware.h" #define FU_TYPE_GOODIXTP_BRLB_FIRMWARE (fu_goodixtp_brlb_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuGoodixtpBrlbFirmware, fu_goodixtp_brlb_firmware, FU, GOODIXTP_BRLB_FIRMWARE, FuGoodixtpFirmware) gboolean fu_goodixtp_brlb_firmware_parse(FuGoodixtpFirmware *self, GInputStream *stream, guint8 sensor_id, GError **error); FuFirmware * fu_goodixtp_brlb_firmware_new(void); fwupd-2.0.10/plugins/goodix-tp/fu-goodixtp-common.h000066400000000000000000000005201501337203100222060ustar00rootroot00000000000000/* * Copyright 2023 Goodix.inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define PACKAGE_LEN 65 #define REPORT_ID 0x0E #define I2C_DIRECT_RW 0x20 #define I2C_READ_FLAG 1 #define I2C_WRITE_FLAG 0 #define RAM_BUFFER_SIZE 4096 #define CFG_MAX_SIZE 4096 fwupd-2.0.10/plugins/goodix-tp/fu-goodixtp-firmware.c000066400000000000000000000017031501337203100225310ustar00rootroot00000000000000/* * Copyright 2023 Goodix.inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-goodixtp-firmware.h" typedef struct { FuFirmwareClass parent_instance; guint32 version; } FuGoodixtpFirmwarePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuGoodixtpFirmware, fu_goodixtp_firmware, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_goodixtp_firmware_get_instance_private(o)) guint32 fu_goodixtp_firmware_get_version(FuGoodixtpFirmware *self) { FuGoodixtpFirmwarePrivate *priv = GET_PRIVATE(self); return priv->version; } void fu_goodixtp_firmware_set_version(FuGoodixtpFirmware *self, guint32 version) { FuGoodixtpFirmwarePrivate *priv = GET_PRIVATE(self); priv->version = version; } static void fu_goodixtp_firmware_init(FuGoodixtpFirmware *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_NO_AUTO_DETECTION); } static void fu_goodixtp_firmware_class_init(FuGoodixtpFirmwareClass *klass) { } fwupd-2.0.10/plugins/goodix-tp/fu-goodixtp-firmware.h000066400000000000000000000010521501337203100225330ustar00rootroot00000000000000/* * Copyright 2023 Goodix.inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_GOODIXTP_FIRMWARE (fu_goodixtp_firmware_get_type()) G_DECLARE_DERIVABLE_TYPE(FuGoodixtpFirmware, fu_goodixtp_firmware, FU, GOODIXTP_FIRMWARE, FuFirmware) struct _FuGoodixtpFirmwareClass { FuFirmwareClass parent_class; }; guint32 fu_goodixtp_firmware_get_version(FuGoodixtpFirmware *self); void fu_goodixtp_firmware_set_version(FuGoodixtpFirmware *self, guint32 version); fwupd-2.0.10/plugins/goodix-tp/fu-goodixtp-gtx8-device.c000066400000000000000000000374611501337203100230560ustar00rootroot00000000000000/* * Copyright 2023 Goodix.inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-goodixtp-common.h" #include "fu-goodixtp-gtx8-device.h" #include "fu-goodixtp-gtx8-firmware.h" struct _FuGoodixtpGtx8Device { FuGoodixtpHidDevice parent_instance; }; G_DEFINE_TYPE(FuGoodixtpGtx8Device, fu_goodixtp_gtx8_device, FU_TYPE_GOODIXTP_HID_DEVICE) #define CMD_ADDR 0x60CC #define BL_STATE_ADDR 0x5095 #define FLASH_RESULT_ADDR 0x5096 #define FLASH_BUFFER_ADDR 0xC000 static gboolean fu_goodixtp_gtx8_device_read_pkg(FuGoodixtpGtx8Device *self, gsize addr, guint8 *buf, gsize bufsz, GError **error) { guint8 hidbuf[PACKAGE_LEN] = {0}; hidbuf[0] = REPORT_ID; hidbuf[1] = I2C_DIRECT_RW; hidbuf[2] = 0; hidbuf[3] = 0; hidbuf[4] = 5; hidbuf[5] = I2C_READ_FLAG; fu_memwrite_uint16(hidbuf + 6, addr, G_BIG_ENDIAN); fu_memwrite_uint16(hidbuf + 8, bufsz, G_BIG_ENDIAN); if (!fu_goodixtp_hid_device_set_report(FU_GOODIXTP_HID_DEVICE(self), hidbuf, 10, error)) return FALSE; if (!fu_goodixtp_hid_device_get_report(FU_GOODIXTP_HID_DEVICE(self), hidbuf, sizeof(hidbuf), error)) return FALSE; if (hidbuf[3] != 0 || hidbuf[4] != bufsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "Failed to read_pkg, hidbuf[3]:%d hidbuf[4]:%d", hidbuf[3], hidbuf[4]); return FALSE; } if (!fu_memcpy_safe(buf, bufsz, 0, hidbuf, sizeof(hidbuf), 5, hidbuf[4], error)) return FALSE; return TRUE; } static gboolean fu_goodixtp_gtx8_device_hid_read(FuGoodixtpGtx8Device *self, gsize addr, guint8 *buf, gsize bufsz, GError **error) { g_autoptr(GPtrArray) chunks = fu_chunk_array_mutable_new(buf, bufsz, addr, 0, PACKAGE_LEN - 10); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_goodixtp_gtx8_device_read_pkg(self, fu_chunk_get_address(chk), fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; } return TRUE; } static gboolean fu_goodixtp_gtx8_device_hid_write(FuGoodixtpGtx8Device *self, gsize addr, guint8 *buf, gsize bufsz, GError **error) { g_autoptr(GPtrArray) chunks = fu_chunk_array_new(buf, bufsz, addr, 0, PACKAGE_LEN - 10); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); guint8 hidbuf[PACKAGE_LEN] = {0}; hidbuf[0] = REPORT_ID; hidbuf[1] = I2C_DIRECT_RW; if (i == chunks->len - 1) hidbuf[2] = 0x00; else hidbuf[2] = 0x01; hidbuf[3] = i; hidbuf[4] = fu_chunk_get_data_sz(chk) + 5; hidbuf[5] = I2C_WRITE_FLAG; fu_memwrite_uint16(hidbuf + 6, fu_chunk_get_address(chk), G_BIG_ENDIAN); fu_memwrite_uint16(hidbuf + 8, fu_chunk_get_data_sz(chk), G_BIG_ENDIAN); if (!fu_memcpy_safe(hidbuf, sizeof(hidbuf), 10, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0, fu_chunk_get_data_sz(chk), error)) return FALSE; if (!fu_goodixtp_hid_device_set_report(FU_GOODIXTP_HID_DEVICE(self), hidbuf, fu_chunk_get_data_sz(chk) + 10, error)) { g_prefix_error(error, "failed write data to addr=0x%x, len=%d: ", (guint)fu_chunk_get_address(chk), (gint)fu_chunk_get_data_sz(chk)); return FALSE; } } return TRUE; } static gboolean fu_goodixtp_gtx8_device_send_cmd(FuGoodixtpGtx8Device *self, guint8 *buf, gsize bufsz, GError **error) { guint8 hidbuf[PACKAGE_LEN] = {0}; if (!fu_memcpy_safe(hidbuf, sizeof(hidbuf), 0, buf, bufsz, 0, bufsz, error)) return FALSE; hidbuf[0] = REPORT_ID; if (!fu_goodixtp_hid_device_set_report(FU_GOODIXTP_HID_DEVICE(self), hidbuf, bufsz, error)) { g_prefix_error(error, "failed to send cmd: "); return FALSE; } return TRUE; } static gboolean fu_goodixtp_gtx8_device_ensure_version(FuGoodixtpGtx8Device *self, GError **error) { guint8 fw_info[72] = {0}; guint8 vice_ver; guint8 inter_ver; guint8 cfg_ver = 0x0; guint8 chksum; guint32 patch_vid_raw; g_autofree gchar *patch_pid = NULL; if (!fu_goodixtp_gtx8_device_hid_read(self, 0x60DC, &cfg_ver, 1, error)) { g_prefix_error(error, "failed to read cfg version: "); return FALSE; } if (!fu_goodixtp_gtx8_device_hid_read(self, 0x452C, fw_info, sizeof(fw_info), error)) { g_prefix_error(error, "failed to read firmware version: "); return FALSE; } /*check fw version*/ chksum = fu_sum8(fw_info, sizeof(fw_info)); if (chksum != 0x0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "fw version check sum error: %d", chksum); return FALSE; } patch_pid = fu_memstrsafe(fw_info, sizeof(fw_info), 0x9, 5, NULL); if (patch_pid != NULL) fu_goodixtp_hid_device_set_patch_pid(FU_GOODIXTP_HID_DEVICE(self), patch_pid); patch_vid_raw = fu_memread_uint32(fw_info + 17, G_BIG_ENDIAN); if (patch_vid_raw != 0) { g_autofree gchar *patch_vid = g_strdup_printf("%04X", patch_vid_raw); fu_goodixtp_hid_device_set_patch_vid(FU_GOODIXTP_HID_DEVICE(self), patch_vid); } fu_goodixtp_hid_device_set_sensor_id(FU_GOODIXTP_HID_DEVICE(self), fw_info[21] & 0x0F); fu_goodixtp_hid_device_set_config_ver(FU_GOODIXTP_HID_DEVICE(self), cfg_ver); vice_ver = fw_info[19]; inter_ver = fw_info[20]; fu_device_set_version_raw(FU_DEVICE(self), (vice_ver << 16) | (inter_ver << 8) | cfg_ver); return TRUE; } static gboolean fu_goodixtp_gtx8_device_disable_report(FuGoodixtpGtx8Device *self, GError **error) { guint8 buf_disable[] = {0x33, 0x00, 0xCD}; guint8 buf_confirm[] = {0x35, 0x00, 0xCB}; guint8 buf[3] = {0}; for (gint i = 0; i < 3; i++) { if (!fu_goodixtp_gtx8_device_hid_write(self, CMD_ADDR, buf_disable, sizeof(buf_disable), error)) { g_prefix_error(error, "send close report cmd failed: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), 10); } if (!fu_goodixtp_gtx8_device_hid_write(self, CMD_ADDR, buf_confirm, sizeof(buf_confirm), error)) { g_prefix_error(error, "send confirm cmd failed: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), 30); if (!fu_goodixtp_gtx8_device_hid_read(self, CMD_ADDR, buf, sizeof(buf), error)) { g_prefix_error(error, "read confirm flag failed: "); return FALSE; } if (buf[1] != 1) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "close report failed, flag[0x%02X]", buf[1]); return FALSE; } /* success */ return TRUE; } static gboolean fu_goodixtp_gtx8_device_wait_bl_cb(FuDevice *device, gpointer user_data, GError **error) { FuGoodixtpGtx8Device *self = FU_GOODIXTP_GTX8_DEVICE(device); guint8 hidbuf[1] = {0}; if (!fu_goodixtp_gtx8_device_hid_read(self, BL_STATE_ADDR, hidbuf, 1, error)) return FALSE; if (hidbuf[0] != 0xDD) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "ack=0x%02x", hidbuf[0]); return FALSE; } return TRUE; } static gboolean fu_goodixtp_gtx8_device_update_prepare(FuGoodixtpGtx8Device *self, GError **error) { guint8 cmd_switch_to_patch[] = {0x00, 0x10, 0x00, 0x00, 0x01, 0x01}; guint8 cmd_start_update[] = {0x00, 0x11, 0x00, 0x00, 0x01, 0x01}; /* close report */ if (!fu_goodixtp_gtx8_device_disable_report(self, error)) { g_prefix_error(error, "disable report failed: "); return FALSE; } if (!fu_goodixtp_gtx8_device_send_cmd(self, cmd_switch_to_patch, sizeof(cmd_switch_to_patch), error)) { g_prefix_error(error, "failed switch to patch: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), 100); if (!fu_device_retry_full(FU_DEVICE(self), fu_goodixtp_gtx8_device_wait_bl_cb, 5, 30, NULL, error)) { g_prefix_error(error, "wait gtx8 BL status failed: "); return FALSE; } if (!fu_goodixtp_gtx8_device_disable_report(self, error)) { g_prefix_error(error, "disable report failed: "); return FALSE; } /* start update */ if (!fu_goodixtp_gtx8_device_send_cmd(self, cmd_start_update, sizeof(cmd_start_update), error)) { g_prefix_error(error, "failed to start update: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), 100); return TRUE; } static gboolean fu_goodixtp_gtx8_device_soft_reset_ic(FuGoodixtpGtx8Device *self, GError **error) { guint8 cmd_reset[] = {0x0E, 0x13, 0x00, 0x00, 0x01, 0x01}; guint8 cmd_switch_ptp_mode[] = {0x03, 0x03, 0x00, 0x00, 0x01, 0x01}; if (!fu_goodixtp_gtx8_device_send_cmd(self, cmd_reset, sizeof(cmd_reset), error)) { g_prefix_error(error, "failed write reset command: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), 100); if (!fu_goodixtp_gtx8_device_send_cmd(self, cmd_switch_ptp_mode, sizeof(cmd_switch_ptp_mode), error)) { g_prefix_error(error, "failed switch to ptp mode: "); return FALSE; } return TRUE; } static gboolean fu_goodixtp_gtx8_device_wait_flash_cb(FuDevice *device, gpointer user_data, GError **error) { FuGoodixtpGtx8Device *self = FU_GOODIXTP_GTX8_DEVICE(device); guint8 hidbuf[1] = {0}; if (!fu_goodixtp_gtx8_device_hid_read(self, FLASH_RESULT_ADDR, hidbuf, 1, error)) return FALSE; if (hidbuf[0] != 0xAA) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "ack=0x%02x", hidbuf[0]); return FALSE; } return TRUE; } static gboolean fu_goodixtp_gtx8_device_load_sub_firmware_cb(FuDevice *device, gpointer user_data, GError **error) { FuGoodixtpGtx8Device *self = FU_GOODIXTP_GTX8_DEVICE(device); guint16 check_sum; guint8 buf_align4k[RAM_BUFFER_SIZE] = {0}; guint8 buf_load_flash[15] = {0x0e, 0x12, 0x00, 0x00, 0x06}; guint8 dummy = 0; FuChunk *chk = (FuChunk *)user_data; if (!fu_memcpy_safe(buf_align4k, sizeof(buf_align4k), 0, (guint8 *)fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0, fu_chunk_get_data_sz(chk), error)) return FALSE; if (!fu_goodixtp_gtx8_device_hid_write(self, FLASH_BUFFER_ADDR, buf_align4k, sizeof(buf_align4k), error)) { g_prefix_error(error, "failed to load fw bufsz=0x%x, addr=0x%x: ", (guint)sizeof(buf_align4k), (guint)fu_chunk_get_address(chk)); return FALSE; } /* inform IC to load 4K data to flash */ check_sum = fu_sum16w(buf_align4k, sizeof(buf_align4k), G_BIG_ENDIAN); fu_memwrite_uint16(buf_load_flash + 5, sizeof(buf_align4k), G_BIG_ENDIAN); fu_memwrite_uint16(buf_load_flash + 7, fu_chunk_get_address(chk) >> 8, G_BIG_ENDIAN); fu_memwrite_uint16(buf_load_flash + 9, check_sum, G_BIG_ENDIAN); if (!fu_goodixtp_gtx8_device_send_cmd(self, buf_load_flash, 11, error)) { g_prefix_error(error, "failed write load flash command: "); return FALSE; } fu_device_sleep(device, 80); if (!fu_device_retry_full(device, fu_goodixtp_gtx8_device_wait_flash_cb, 10, 20, NULL, error)) { g_prefix_error(error, "wait flash status failed: "); return FALSE; } if (!fu_goodixtp_gtx8_device_hid_write(self, FLASH_RESULT_ADDR, &dummy, sizeof(dummy), error)) return FALSE; fu_device_sleep(device, 5); return TRUE; } static gboolean fu_goodixtp_gtx8_device_update_process(FuGoodixtpGtx8Device *self, FuChunk *chk, GError **error) { if (!fu_device_retry_full(FU_DEVICE(self), fu_goodixtp_gtx8_device_load_sub_firmware_cb, 3, 10, chk, error)) { g_prefix_error(error, "load sub firmware failed, addr=0x%04x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } return TRUE; } static gboolean fu_goodixtp_gtx8_device_setup(FuDevice *device, GError **error) { FuGoodixtpGtx8Device *self = FU_GOODIXTP_GTX8_DEVICE(device); if (!fu_goodixtp_gtx8_device_ensure_version(self, error)) { g_prefix_error(error, "gtx8 read version failed: "); return FALSE; } return TRUE; } static FuFirmware * fu_goodixtp_gtx8_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuGoodixtpGtx8Device *self = FU_GOODIXTP_GTX8_DEVICE(device); g_autoptr(FuFirmware) firmware = fu_goodixtp_gtx8_firmware_new(); if (!fu_goodixtp_gtx8_firmware_parse( FU_GOODIXTP_FIRMWARE(firmware), stream, fu_goodixtp_hid_device_get_sensor_id(FU_GOODIXTP_HID_DEVICE(self)), error)) return NULL; return g_steal_pointer(&firmware); } static gboolean fu_goodixtp_gtx8_device_write_image(FuGoodixtpGtx8Device *self, FuFirmware *img, FuProgress *progress, GError **error) { g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(GInputStream) stream = NULL; stream = fu_firmware_get_stream(img, error); if (stream == NULL) return FALSE; chunks = fu_chunk_array_new_from_stream(stream, fu_firmware_get_addr(img), FU_CHUNK_PAGESZ_NONE, RAM_BUFFER_SIZE, error); if (chunks == NULL) return FALSE; fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_goodixtp_gtx8_device_update_process(self, chk, error)) return FALSE; fu_device_sleep(FU_DEVICE(self), 20); fu_progress_step_done(progress); } return TRUE; } static gboolean fu_goodixtp_gtx8_device_write_images(FuGoodixtpGtx8Device *self, GPtrArray *imgs, FuProgress *progress, GError **error) { fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, imgs->len); for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); if (!fu_goodixtp_gtx8_device_write_image(self, img, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); } return TRUE; } static gboolean fu_goodixtp_gtx8_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuGoodixtpGtx8Device *self = FU_GOODIXTP_GTX8_DEVICE(device); FuGoodixtpFirmware *firmware_goodixtp = FU_GOODIXTP_FIRMWARE(firmware); guint32 fw_ver = fu_goodixtp_firmware_get_version(firmware_goodixtp); g_autoptr(GPtrArray) imgs = fu_firmware_get_images(firmware); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 10, "prepare"); fu_progress_add_step(progress, FWUPD_STATUS_DOWNLOADING, 85, "download"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 5, "reload"); if (!fu_goodixtp_gtx8_device_update_prepare(self, error)) return FALSE; fu_progress_step_done(progress); if (!fu_goodixtp_gtx8_device_write_images(self, imgs, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* reset IC */ if (!fu_goodixtp_gtx8_device_soft_reset_ic(self, error)) return FALSE; if (!fu_goodixtp_gtx8_device_ensure_version(self, error)) return FALSE; fu_progress_step_done(progress); if (fu_device_get_version_raw(device) != fw_ver) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "update failed chip_ver:%x != bin_ver:%x", (guint)fu_device_get_version_raw(device), (guint)fw_ver); return FALSE; } return TRUE; } static void fu_goodixtp_gtx8_device_init(FuGoodixtpGtx8Device *self) { } static void fu_goodixtp_gtx8_device_class_init(FuGoodixtpGtx8DeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_goodixtp_gtx8_device_setup; device_class->reload = fu_goodixtp_gtx8_device_setup; device_class->prepare_firmware = fu_goodixtp_gtx8_device_prepare_firmware; device_class->write_firmware = fu_goodixtp_gtx8_device_write_firmware; } fwupd-2.0.10/plugins/goodix-tp/fu-goodixtp-gtx8-device.h000066400000000000000000000005711501337203100230530ustar00rootroot00000000000000/* * Copyright 2023 Goodix.inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-goodixtp-hid-device.h" #define FU_TYPE_GOODIXTP_GTX8_DEVICE (fu_goodixtp_gtx8_device_get_type()) G_DECLARE_FINAL_TYPE(FuGoodixtpGtx8Device, fu_goodixtp_gtx8_device, FU, GOODIXTP_GTX8_DEVICE, FuGoodixtpHidDevice) fwupd-2.0.10/plugins/goodix-tp/fu-goodixtp-gtx8-firmware.c000066400000000000000000000136771501337203100234360ustar00rootroot00000000000000/* * Copyright 2023 Goodix.inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-goodixtp-common.h" #include "fu-goodixtp-gtx8-firmware.h" #include "fu-goodixtp-struct.h" struct _FuGoodixtpGtx8Firmware { FuGoodixtpFirmware parent_instance; }; G_DEFINE_TYPE(FuGoodixtpGtx8Firmware, fu_goodixtp_gtx8_firmware, FU_TYPE_GOODIXTP_FIRMWARE) #define GTX8_FW_DATA_OFFSET 256 gboolean fu_goodixtp_gtx8_firmware_parse(FuGoodixtpFirmware *self, GInputStream *stream, guint8 sensor_id, GError **error) { gboolean has_config = FALSE; gsize bufsz = 0; guint16 checksum = 0; guint32 firmware_size = 0; guint32 version; guint8 cfg_ver = 0; guint8 subsys_num; guint sub_cfg_info_pos; guint32 offset_hdr; guint32 offset_payload = GTX8_FW_DATA_OFFSET; const guint8 *buf; g_autoptr(GByteArray) st = NULL; g_autoptr(GBytes) fw = NULL; st = fu_struct_goodix_gtx8_hdr_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; firmware_size = fu_struct_goodix_gtx8_hdr_get_firmware_size(st); if (firmware_size < 6 || firmware_size > G_MAXUINT32 - GTX8_FW_DATA_OFFSET) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid firmware size"); return FALSE; } /* convert to blob */ fw = fu_input_stream_read_bytes(stream, 0, G_MAXSIZE, NULL, error); if (fw == NULL) return FALSE; buf = g_bytes_get_data(fw, &bufsz); if ((gsize)firmware_size + 6 != bufsz) { g_debug("check file len unequal 0x%x != 0x%x, this bin may contain config", (guint)firmware_size + 6, (guint)bufsz); has_config = TRUE; } /* verify checksum */ for (guint i = 6; i < (guint)firmware_size + 6; i++) { guint8 tmp_val = 0; if (!fu_memread_uint8_safe(buf, bufsz, i, &tmp_val, error)) return FALSE; checksum += tmp_val; } if (checksum != fu_struct_goodix_gtx8_hdr_get_checksum(st)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "checksum invalid"); return FALSE; } if (has_config) { guint16 cfg_packlen = 0; guint cfg_offset; guint8 sub_cfg_num; guint16 read_cksum; if (!fu_memread_uint16_safe(buf, bufsz, firmware_size + 6, &cfg_packlen, G_BIG_ENDIAN, error)) return FALSE; if ((gint)(bufsz - firmware_size - 6) != (gint)cfg_packlen + 6) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "config pack len error"); return FALSE; } checksum = 0; for (guint i = firmware_size + 12; i < (guint)bufsz; i++) { guint8 tmp_val; if (!fu_memread_uint8_safe(buf, bufsz, i, &tmp_val, error)) return FALSE; checksum += tmp_val; } if (!fu_memread_uint16_safe(buf, bufsz, firmware_size + 10, &read_cksum, G_BIG_ENDIAN, error)) return FALSE; if (checksum != read_cksum) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "config pack checksum error"); return FALSE; } if (!fu_memread_uint8_safe(buf, bufsz, firmware_size + 9, &sub_cfg_num, error)) return FALSE; if (sub_cfg_num == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "sub_cfg_num is 0"); return FALSE; } sub_cfg_info_pos = firmware_size + 12; cfg_offset = firmware_size + 6 + 64; for (guint i = 0; i < sub_cfg_num; i++) { guint8 sub_cfg_id = 0; guint16 sub_cfg_len = 0; if (!fu_memread_uint8_safe(buf, bufsz, sub_cfg_info_pos, &sub_cfg_id, error)) return FALSE; if (!fu_memread_uint16_safe(buf, bufsz, sub_cfg_info_pos + 1, &sub_cfg_len, G_BIG_ENDIAN, error)) return FALSE; if (sensor_id == sub_cfg_id) { g_autoptr(GBytes) fw_img = NULL; g_autoptr(FuFirmware) img = fu_firmware_new(); fu_firmware_set_idx(img, 3); fu_firmware_set_addr(img, 0x1E000); fw_img = fu_bytes_new_offset(fw, cfg_offset, sub_cfg_len, error); if (fw_img == NULL) return FALSE; fu_firmware_set_bytes(img, fw_img); if (!fu_firmware_add_image_full(FU_FIRMWARE(self), img, error)) return FALSE; if (!fu_memread_uint8_safe(buf, bufsz, cfg_offset, &cfg_ver, error)) return FALSE; g_debug("Find a cfg match sensorID:ID=%d, cfg version=%d", sensor_id, cfg_ver); break; } cfg_offset += sub_cfg_len; sub_cfg_info_pos += 3; } } /* parse each image */ subsys_num = fu_struct_goodix_gtx8_hdr_get_subsys_num(st); if (subsys_num == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "subsys_num is 0, exit"); return FALSE; } offset_hdr = st->len; for (guint i = 0; i < subsys_num; i++) { guint32 img_size; g_autoptr(GByteArray) st_img = NULL; st_img = fu_struct_goodix_gtx8_img_parse_stream(stream, offset_hdr, error); if (st_img == NULL) return FALSE; img_size = fu_struct_goodix_gtx8_img_get_size(st_img); if (fu_struct_goodix_gtx8_img_get_kind(st_img) != 0x01) { g_autoptr(GBytes) fw_img = NULL; g_autoptr(FuFirmware) img = fu_firmware_new(); fu_firmware_set_idx(img, fu_struct_goodix_gtx8_img_get_kind(st_img)); fu_firmware_set_addr(img, fu_struct_goodix_gtx8_img_get_addr(st_img) << 8); fw_img = fu_bytes_new_offset(fw, offset_payload, img_size, error); if (fw_img == NULL) return FALSE; fu_firmware_set_bytes(img, fw_img); if (!fu_firmware_add_image_full(FU_FIRMWARE(self), img, error)) return FALSE; } offset_hdr += st_img->len; offset_payload += img_size; } version = (fu_struct_goodix_gtx8_hdr_get_vid(st) << 8) | cfg_ver; fu_goodixtp_firmware_set_version(self, version); return TRUE; } static void fu_goodixtp_gtx8_firmware_init(FuGoodixtpGtx8Firmware *self) { fu_firmware_set_images_max(FU_FIRMWARE(self), 1024); } static void fu_goodixtp_gtx8_firmware_class_init(FuGoodixtpGtx8FirmwareClass *klass) { } FuFirmware * fu_goodixtp_gtx8_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_GOODIXTP_GTX8_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/goodix-tp/fu-goodixtp-gtx8-firmware.h000066400000000000000000000010731501337203100234260ustar00rootroot00000000000000/* * Copyright 2023 Goodix.inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-goodixtp-firmware.h" #define FU_TYPE_GOODIXTP_GTX8_FIRMWARE (fu_goodixtp_gtx8_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuGoodixtpGtx8Firmware, fu_goodixtp_gtx8_firmware, FU, GOODIXTP_GTX8_FIRMWARE, FuGoodixtpFirmware) gboolean fu_goodixtp_gtx8_firmware_parse(FuGoodixtpFirmware *self, GInputStream *stream, guint8 sensor_id, GError **error); FuFirmware * fu_goodixtp_gtx8_firmware_new(void); fwupd-2.0.10/plugins/goodix-tp/fu-goodixtp-hid-device.c000066400000000000000000000125021501337203100227150ustar00rootroot00000000000000/* * Copyright 2023 Goodix.inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-goodixtp-common.h" #include "fu-goodixtp-firmware.h" #include "fu-goodixtp-hid-device.h" typedef struct { gchar *patch_pid; gchar *patch_vid; guint8 sensor_id; guint8 cfg_ver; } FuGoodixtpHidDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuGoodixtpHidDevice, fu_goodixtp_hid_device, FU_TYPE_HIDRAW_DEVICE) #define GET_PRIVATE(o) (fu_goodixtp_hid_device_get_instance_private(o)) void fu_goodixtp_hid_device_set_patch_pid(FuGoodixtpHidDevice *self, const gchar *patch_pid) { FuGoodixtpHidDevicePrivate *priv = GET_PRIVATE(self); priv->patch_pid = g_strdup_printf("GT%s", patch_pid); } void fu_goodixtp_hid_device_set_patch_vid(FuGoodixtpHidDevice *self, const gchar *patch_vid) { FuGoodixtpHidDevicePrivate *priv = GET_PRIVATE(self); priv->patch_vid = g_strdup(patch_vid); } void fu_goodixtp_hid_device_set_sensor_id(FuGoodixtpHidDevice *self, guint8 sensor_id) { FuGoodixtpHidDevicePrivate *priv = GET_PRIVATE(self); priv->sensor_id = sensor_id; } void fu_goodixtp_hid_device_set_config_ver(FuGoodixtpHidDevice *self, guint8 ver) { FuGoodixtpHidDevicePrivate *priv = GET_PRIVATE(self); priv->cfg_ver = ver; } guint8 fu_goodixtp_hid_device_get_sensor_id(FuGoodixtpHidDevice *self) { FuGoodixtpHidDevicePrivate *priv = GET_PRIVATE(self); return priv->sensor_id; } static void fu_goodixtp_hid_device_to_string(FuDevice *device, guint idt, GString *str) { FuGoodixtpHidDevice *self = FU_GOODIXTP_HID_DEVICE(device); FuGoodixtpHidDevicePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append(str, idt, "patch_pid", priv->patch_pid); fwupd_codec_string_append(str, idt, "patch_vid", priv->patch_vid); fwupd_codec_string_append_hex(str, idt, "sensor_id", priv->sensor_id); fwupd_codec_string_append_hex(str, idt, "cfg_ver", priv->cfg_ver); } gboolean fu_goodixtp_hid_device_get_report(FuGoodixtpHidDevice *self, guint8 *buf, gsize bufsz, GError **error) { guint8 rcv_buf[PACKAGE_LEN + 1] = {0}; rcv_buf[0] = REPORT_ID; if (!fu_hidraw_device_get_feature(FU_HIDRAW_DEVICE(self), rcv_buf, sizeof(rcv_buf), FU_IOCTL_FLAG_NONE, error)) { g_prefix_error(error, "failed get report: "); return FALSE; } if (rcv_buf[0] != REPORT_ID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "rcv_buf[0]:%02x != 0x0E", rcv_buf[0]); return FALSE; } if (!fu_memcpy_safe(buf, bufsz, 0, rcv_buf, sizeof(rcv_buf), 0, PACKAGE_LEN, error)) return FALSE; return TRUE; } gboolean fu_goodixtp_hid_device_set_report(FuGoodixtpHidDevice *self, guint8 *buf, gsize bufsz, GError **error) { if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), buf, bufsz, FU_IOCTL_FLAG_NONE, error)) { g_prefix_error(error, "failed set report: "); return FALSE; } return TRUE; } static void fu_goodixtp_hid_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static gchar * fu_goodixtp_hid_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_device_get_version_format(device)); } static void fu_goodixtp_hid_device_init(FuGoodixtpHidDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_set_summary(FU_DEVICE(self), "Touchpad"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_INPUT_TOUCHPAD); fu_device_add_protocol(FU_DEVICE(self), "com.goodix.goodixtp"); fu_device_set_name(FU_DEVICE(self), "Touch Controller Sensor"); fu_device_set_vendor(FU_DEVICE(self), "Goodix inc."); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_HEX); fu_device_set_priority(FU_DEVICE(self), 1); /* better than i2c */ fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_NONBLOCK); } static void fu_goodixtp_hid_device_finalize(GObject *object) { FuGoodixtpHidDevice *self = FU_GOODIXTP_HID_DEVICE(object); FuGoodixtpHidDevicePrivate *priv = GET_PRIVATE(self); g_free(priv->patch_pid); g_free(priv->patch_vid); G_OBJECT_CLASS(fu_goodixtp_hid_device_parent_class)->finalize(object); } static void fu_goodixtp_hid_device_class_init(FuGoodixtpHidDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->finalize = fu_goodixtp_hid_device_finalize; device_class->to_string = fu_goodixtp_hid_device_to_string; device_class->set_progress = fu_goodixtp_hid_device_set_progress; device_class->convert_version = fu_goodixtp_hid_device_convert_version; } fwupd-2.0.10/plugins/goodix-tp/fu-goodixtp-hid-device.h000066400000000000000000000021741501337203100227260ustar00rootroot00000000000000/* * Copyright 2023 Goodix.inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-goodixtp-common.h" #define FU_TYPE_GOODIXTP_HID_DEVICE (fu_goodixtp_hid_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuGoodixtpHidDevice, fu_goodixtp_hid_device, FU, GOODIXTP_HID_DEVICE, FuHidrawDevice) struct _FuGoodixtpHidDeviceClass { FuHidrawDeviceClass parent_class; }; gboolean fu_goodixtp_hid_device_get_report(FuGoodixtpHidDevice *self, guint8 *buf, gsize bufsz, GError **error); gboolean fu_goodixtp_hid_device_set_report(FuGoodixtpHidDevice *self, guint8 *buf, gsize bufsz, GError **error); void fu_goodixtp_hid_device_set_patch_pid(FuGoodixtpHidDevice *self, const gchar *patch_pid); void fu_goodixtp_hid_device_set_patch_vid(FuGoodixtpHidDevice *self, const gchar *patch_vid); void fu_goodixtp_hid_device_set_sensor_id(FuGoodixtpHidDevice *self, guint8 sensor_id); void fu_goodixtp_hid_device_set_config_ver(FuGoodixtpHidDevice *self, guint8 ver); guint8 fu_goodixtp_hid_device_get_sensor_id(FuGoodixtpHidDevice *self); fwupd-2.0.10/plugins/goodix-tp/fu-goodixtp-plugin.c000066400000000000000000000067101501337203100222160ustar00rootroot00000000000000/* * Copyright 2023 Goodix.inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-goodixtp-brlb-device.h" #include "fu-goodixtp-brlb-firmware.h" #include "fu-goodixtp-common.h" #include "fu-goodixtp-firmware.h" #include "fu-goodixtp-gtx8-device.h" #include "fu-goodixtp-gtx8-firmware.h" #include "fu-goodixtp-plugin.h" #include "fu-goodixtp-struct.h" struct _FuGoodixtpPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuGoodixtpPlugin, fu_goodixtp_plugin, FU_TYPE_PLUGIN) static void fu_goodixtp_plugin_init(FuGoodixtpPlugin *self) { } static void fu_goodixtp_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_udev_subsystem(plugin, "hidraw"); fu_plugin_set_device_gtype_default(plugin, FU_TYPE_GOODIXTP_HID_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_GOODIXTP_GTX8_DEVICE); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_GOODIXTP_BRLB_DEVICE); /* coverage */ fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_GOODIXTP_FIRMWARE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_GOODIXTP_GTX8_FIRMWARE); /* coverage */ fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_GOODIXTP_BRLB_FIRMWARE); /* coverage */ } static FuGoodixtpIcType fu_goodixtp_plugin_ic_type_from_pid(guint16 pid) { if ((pid >= 0x01E0 && pid <= 0x01E7) || (pid >= 0x0D00 && pid <= 0x0D7F)) return FU_GOODIXTP_IC_TYPE_NORMANDYL; if ((pid >= 0x0EB0 && pid <= 0x0EBF) || (pid >= 0x0EC0 && pid <= 0x0ECF) || (pid >= 0x0EA5 && pid <= 0x0EAA) || (pid >= 0x0C00 && pid <= 0x0CFF)) return FU_GOODIXTP_IC_TYPE_BERLINB; return FU_GOODIXTP_IC_TYPE_NONE; } static gboolean fu_goodixtp_plugin_backend_device_added(FuPlugin *plugin, FuDevice *device, FuProgress *progress, GError **error) { FuGoodixtpIcType ic_type; g_autoptr(FuDeviceLocker) locker = NULL; if (g_strcmp0(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device)), "hidraw") != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "is not correct subsystem=%s, expected hidraw", fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device))); return FALSE; } ic_type = fu_goodixtp_plugin_ic_type_from_pid(fu_device_get_pid(device)); if (ic_type == FU_GOODIXTP_IC_TYPE_NORMANDYL) { g_autoptr(FuDevice) dev = g_object_new(FU_TYPE_GOODIXTP_GTX8_DEVICE, "context", fu_plugin_get_context(plugin), NULL); fu_device_incorporate(dev, device, FU_DEVICE_INCORPORATE_FLAG_ALL); locker = fu_device_locker_new(dev, error); if (locker == NULL) return FALSE; fu_plugin_device_add(plugin, dev); return TRUE; } if (ic_type == FU_GOODIXTP_IC_TYPE_BERLINB) { g_autoptr(FuDevice) dev = g_object_new(FU_TYPE_GOODIXTP_BRLB_DEVICE, "context", fu_plugin_get_context(plugin), NULL); fu_device_incorporate(dev, device, FU_DEVICE_INCORPORATE_FLAG_ALL); locker = fu_device_locker_new(dev, error); if (locker == NULL) return FALSE; fu_plugin_device_add(plugin, dev); return TRUE; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "can't find valid ic_type, pid is %x", fu_device_get_pid(device)); return FALSE; } static void fu_goodixtp_plugin_class_init(FuGoodixtpPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_goodixtp_plugin_constructed; plugin_class->backend_device_added = fu_goodixtp_plugin_backend_device_added; } fwupd-2.0.10/plugins/goodix-tp/fu-goodixtp-plugin.h000066400000000000000000000003561501337203100222230ustar00rootroot00000000000000/* * Copyright 2023 Goodix.inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuGoodixtpPlugin, fu_goodixtp_plugin, FU, GOODIXTP_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/goodix-tp/fu-goodixtp.rs000066400000000000000000000016421501337203100211230ustar00rootroot00000000000000// Copyright 2023 Goodix.inc // SPDX-License-Identifier: LGPL-2.1-or-later enum FuGoodixtpIcType { None, Phoenix, TypeNanjing, Mousepad, Normandyl, Berlinb, Yellowstone, } #[derive(ParseStream)] #[repr(C, packed)] struct FuStructGoodixBrlbHdr { firmware_size: u32le, checksum: u32le, _unknown: [u8; 19], vid: u16be, subsys_num: u8, _unknown: [u8; 12], } #[derive(ParseStream)] #[repr(C, packed)] struct FuStructGoodixBrlbImg { kind: u8, size: u32le, addr: u32le, _unknown: [u8; 1], } #[derive(ParseStream)] #[repr(C, packed)] struct FuStructGoodixGtx8Hdr { firmware_size: u32be, checksum: u16be, _unknown: [u8; 19], vid: u16be, subsys_num: u8, _unknown: [u8; 4], } #[derive(ParseStream)] #[repr(C, packed)] struct FuStructGoodixGtx8Img { kind: u8, size: u32be, addr: u16be, _unknown: [u8; 1], } fwupd-2.0.10/plugins/goodix-tp/goodixtp.quirk000066400000000000000000000000751501337203100212210ustar00rootroot00000000000000[HIDRAW\VEN_27C6] Plugin = goodixtp Flags = enforce-requires fwupd-2.0.10/plugins/goodix-tp/meson.build000066400000000000000000000011271501337203100204500ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginGoodixtp"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('goodixtp.quirk') plugin_builtins += static_library('fu_plugin_goodixtp', rustgen.process('fu-goodixtp.rs'), sources: [ 'fu-goodixtp-brlb-device.c', 'fu-goodixtp-brlb-firmware.c', 'fu-goodixtp-firmware.c', 'fu-goodixtp-gtx8-device.c', 'fu-goodixtp-gtx8-firmware.c', 'fu-goodixtp-hid-device.c', 'fu-goodixtp-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) fwupd-2.0.10/plugins/gpio/000077500000000000000000000000001501337203100153315ustar00rootroot00000000000000fwupd-2.0.10/plugins/gpio/README.md000066400000000000000000000016461501337203100166170ustar00rootroot00000000000000--- title: Plugin: GPIO --- ## Introduction This plugin sets GPIO outputs either high or low before and/or after an update has been deployed. ## GUID Generation These device use GPIO `gpiochip_info.label` values, e.g. * `GPIO\ID_INT3450:00` ## Quirk Use This plugin uses the following plugin-specific quirks: ### GpioForUpdate The GPIO bit to set before the update is deployed e.g. `INT3450:00,SPI_MUX,high`. After the update has finished, the bits are returned to the default state. For example, to set GPIO pin 2 low for the duration of the ColorHug device update this could be added to the quirk file: [USB\VID_273F&PID_1001] GpioForUpdate=fake-gpio-chip,2,low Since: 1.7.6 ## External Interface Access This plugin requires ioctl `GPIO_GET_CHIPINFO_IOCTL` and `GPIO_V2_GET_LINE_IOCTL` access on `/dev/gpiochip*` devices. ## Version Considerations This plugin has been available since fwupd version `1.7.6`. fwupd-2.0.10/plugins/gpio/fu-gpio-device.c000066400000000000000000000142501501337203100203020ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fu-gpio-device.h" struct _FuGpioDevice { FuUdevDevice parent_instance; guint num_lines; gint fd; /* valid when the GPIO bit is assigned */ }; G_DEFINE_TYPE(FuGpioDevice, fu_gpio_device, FU_TYPE_UDEV_DEVICE) #define FU_GPIO_DEVICE_IOCTL_TIMEOUT 5000 /* ms */ static void fu_gpio_device_to_string(FuDevice *device, guint idt, GString *str) { FuGpioDevice *self = FU_GPIO_DEVICE(device); fwupd_codec_string_append_int(str, idt, "NumLines", self->num_lines); fwupd_codec_string_append_bool(str, idt, "FdOpen", self->fd > 0); } static gboolean fu_gpio_device_probe(FuDevice *device, GError **error) { /* no device file */ if (fu_udev_device_get_device_file(FU_UDEV_DEVICE(device)) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no device file"); return FALSE; } /* set the physical ID */ return fu_udev_device_set_physical_id(FU_UDEV_DEVICE(device), "gpio", error); } static gboolean fu_gpio_device_setup(FuDevice *device, GError **error) { FuGpioDevice *self = FU_GPIO_DEVICE(device); struct gpiochip_info info = {0x0}; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); /* get info */ if (!fu_ioctl_execute(ioctl, GPIO_GET_CHIPINFO_IOCTL, (guint8 *)&info, sizeof(info), NULL, FU_GPIO_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_NONE, error)) { g_prefix_error(error, "failed to get chipinfo: "); return FALSE; } /* sanity check */ self->num_lines = info.lines; if (self->num_lines == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "0 lines is not supported"); return FALSE; } /* label is optional, but name is always set */ if (info.label[0] != '\0') { g_autofree gchar *logical_id = fu_strsafe(info.label, sizeof(info.label)); fu_device_set_logical_id(device, logical_id); /* add instance ID */ fu_device_add_instance_strsafe(device, "ID", logical_id); if (!fu_device_build_instance_id(device, error, "GPIO", "ID", NULL)) return FALSE; } /* success */ return TRUE; } gboolean fu_gpio_device_unassign(FuGpioDevice *self, GError **error) { if (self->fd < 0) return TRUE; g_info("unsetting %s", fu_device_get_logical_id(FU_DEVICE(self))); if (!g_close(self->fd, error)) return FALSE; self->fd = -1; return TRUE; } static gboolean fu_gpio_device_assign_full(FuGpioDevice *self, guint64 line, gboolean value, GError **error) { const gchar consumer[] = "fwupd"; struct gpio_v2_line_request req = { .num_lines = 1, req.offsets[0] = line, .config.flags = GPIO_V2_LINE_FLAG_OUTPUT, .config.num_attrs = 1, .config.attrs[0].attr.values = value ? 0x1 : 0x0, .config.attrs[0].mask = 0x1, }; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); /* this is useful if we have contention with other tools */ if (!fu_memcpy_safe((guint8 *)req.consumer, sizeof(req.consumer), 0x0, /* dst */ (const guint8 *)consumer, sizeof(consumer), 0x0, /* src */ sizeof(consumer), error)) return FALSE; /* slightly weird API, but roll with it */ g_info("setting %s:0x%02x → %i", fu_device_get_logical_id(FU_DEVICE(self)), (guint)line, value); if (!fu_ioctl_execute(ioctl, GPIO_V2_GET_LINE_IOCTL, (guint8 *)&req, sizeof(req), NULL, FU_GPIO_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_NONE, error)) { g_prefix_error(error, "failed to assign: "); return FALSE; } /* success */ self->fd = req.fd; return TRUE; } gboolean fu_gpio_device_assign(FuGpioDevice *self, const gchar *id, gboolean value, GError **error) { guint64 line = G_MAXUINT64; /* sanity check */ if (self->fd > 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "GPIO %s already in use", id); return FALSE; } /* specified as a number, or look for @id as named pin */ if (self->num_lines > 0 && fu_strtoull(id, &line, 0, self->num_lines - 1, FU_INTEGER_BASE_AUTO, NULL)) { struct gpio_v2_line_info info = {.offset = line}; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); if (!fu_ioctl_execute(ioctl, GPIO_V2_GET_LINEINFO_IOCTL, (guint8 *)&info, sizeof(info), NULL, FU_GPIO_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_NONE, error)) { g_prefix_error(error, "failed to get lineinfo: "); return FALSE; } } else { g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); for (guint i = 0; i < self->num_lines; i++) { struct gpio_v2_line_info info = {.offset = i}; g_autofree gchar *name = NULL; if (!fu_ioctl_execute(ioctl, GPIO_V2_GET_LINEINFO_IOCTL, (guint8 *)&info, sizeof(info), NULL, FU_GPIO_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_NONE, error)) { g_prefix_error(error, "failed to get lineinfo: "); return FALSE; } name = fu_strsafe(info.name, sizeof(info.name)); if (g_strcmp0(name, id) == 0) { line = i; break; } } } if (line == G_MAXUINT64) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "failed to find %s", id); return FALSE; } return fu_gpio_device_assign_full(self, line, value, error); } static void fu_gpio_device_init(FuGpioDevice *self) { fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); } static void fu_gpio_device_finalize(GObject *object) { FuGpioDevice *self = FU_GPIO_DEVICE(object); if (self->fd > 0) g_close(self->fd, NULL); G_OBJECT_CLASS(fu_gpio_device_parent_class)->finalize(object); } static void fu_gpio_device_class_init(FuGpioDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->finalize = fu_gpio_device_finalize; device_class->to_string = fu_gpio_device_to_string; device_class->setup = fu_gpio_device_setup; device_class->probe = fu_gpio_device_probe; } fwupd-2.0.10/plugins/gpio/fu-gpio-device.h000066400000000000000000000007161501337203100203110ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_GPIO_DEVICE (fu_gpio_device_get_type()) G_DECLARE_FINAL_TYPE(FuGpioDevice, fu_gpio_device, FU, GPIO_DEVICE, FuUdevDevice) gboolean fu_gpio_device_assign(FuGpioDevice *self, const gchar *id, gboolean value, GError **error); gboolean fu_gpio_device_unassign(FuGpioDevice *self, GError **error); fwupd-2.0.10/plugins/gpio/fu-gpio-plugin.c000066400000000000000000000123601501337203100203410ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-gpio-device.h" #include "fu-gpio-plugin.h" struct _FuGpioPlugin { FuPlugin parent_instance; GPtrArray *current_logical_ids; /* element-type: utf-8 */ }; G_DEFINE_TYPE(FuGpioPlugin, fu_gpio_plugin, FU_TYPE_PLUGIN) static void fu_gpio_plugin_to_string(FuPlugin *plugin, guint idt, GString *str) { FuGpioPlugin *self = FU_GPIO_PLUGIN(plugin); for (guint i = 0; i < self->current_logical_ids->len; i++) { const gchar *current_logical_id = g_ptr_array_index(self->current_logical_ids, i); g_autofree gchar *title = g_strdup_printf("CurrentLogicalId[0x%02x]", i); fwupd_codec_string_append(str, idt, title, current_logical_id); } } static gboolean fu_gpio_plugin_parse_level(const gchar *str, gboolean *ret, GError **error) { if (g_strcmp0(str, "high") == 0) { *ret = TRUE; return TRUE; } if (g_strcmp0(str, "low") == 0) { *ret = FALSE; return TRUE; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "cannot parse level, got %s and expected high|low", str); return FALSE; } static gboolean fu_gpio_plugin_process_quirk(FuPlugin *plugin, const gchar *str, GError **error) { FuGpioPlugin *self = FU_GPIO_PLUGIN(plugin); FuDevice *device_tmp; gboolean value = FALSE; g_auto(GStrv) split = g_strsplit(str, ",", -1); g_autoptr(FuDeviceLocker) locker = NULL; /* sanity check */ if (g_strv_length(split) != 3) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid format, CHIP_NAME,PIN_NAME,LEVEL, got '%s'", str); return FALSE; } if (!fu_gpio_plugin_parse_level(split[2], &value, error)) return FALSE; device_tmp = fu_plugin_cache_lookup(plugin, split[0]); if (device_tmp == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "GPIO device %s not found", split[0]); return FALSE; } locker = fu_device_locker_new(device_tmp, error); if (locker == NULL) return FALSE; if (!fu_gpio_device_assign(FU_GPIO_DEVICE(device_tmp), split[1], value, error)) { g_prefix_error(error, "failed to assign %s: ", split[0]); return FALSE; } /* success */ g_ptr_array_add(self->current_logical_ids, g_strdup(fu_device_get_logical_id(device_tmp))); return TRUE; } static gboolean fu_gpio_plugin_prepare(FuPlugin *self, FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { GPtrArray *guids = fu_device_get_guids(device); for (guint i = 0; i < guids->len; i++) { const gchar *guid = g_ptr_array_index(guids, i); const gchar *str; str = fu_context_lookup_quirk_by_id(fu_plugin_get_context(self), guid, "GpioForUpdate"); if (str == NULL) continue; if (!fu_gpio_plugin_process_quirk(self, str, error)) return FALSE; } return TRUE; } static gboolean fu_gpio_plugin_cleanup(FuPlugin *plugin, FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuGpioPlugin *self = FU_GPIO_PLUGIN(plugin); g_autoptr(GPtrArray) current_logical_ids = NULL; /* deep copy to local to clear transaction array */ current_logical_ids = g_ptr_array_copy(self->current_logical_ids, (GCopyFunc)g_strdup, NULL); g_ptr_array_set_size(self->current_logical_ids, 0); /* close the fds we opened during ->prepare */ for (guint i = 0; i < current_logical_ids->len; i++) { FuDevice *device_tmp; const gchar *current_logical_id = g_ptr_array_index(current_logical_ids, i); device_tmp = fu_plugin_cache_lookup(plugin, current_logical_id); if (device_tmp == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "GPIO device %s no longer found", current_logical_id); return FALSE; } if (!fu_gpio_device_unassign(FU_GPIO_DEVICE(device_tmp), error)) { g_prefix_error(error, "failed to unassign %s: ", current_logical_id); return FALSE; } } /* success */ return TRUE; } static void fu_gpio_plugin_device_added(FuPlugin *self, FuDevice *device) { fu_plugin_cache_add(self, fu_device_get_logical_id(device), device); } static void fu_gpio_plugin_init(FuGpioPlugin *self) { self->current_logical_ids = g_ptr_array_new_with_free_func(g_free); } static void fu_gpio_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "GpioForUpdate"); fu_plugin_add_device_udev_subsystem(plugin, "gpio"); fu_plugin_add_device_gtype(plugin, FU_TYPE_GPIO_DEVICE); } static void fu_gpio_plugin_finalize(GObject *obj) { FuGpioPlugin *self = FU_GPIO_PLUGIN(obj); g_ptr_array_unref(self->current_logical_ids); G_OBJECT_CLASS(fu_gpio_plugin_parent_class)->finalize(obj); } static void fu_gpio_plugin_class_init(FuGpioPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_gpio_plugin_finalize; plugin_class->constructed = fu_gpio_plugin_constructed; plugin_class->to_string = fu_gpio_plugin_to_string; plugin_class->prepare = fu_gpio_plugin_prepare; plugin_class->cleanup = fu_gpio_plugin_cleanup; plugin_class->device_added = fu_gpio_plugin_device_added; } fwupd-2.0.10/plugins/gpio/fu-gpio-plugin.h000066400000000000000000000003461501337203100203470ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuGpioPlugin, fu_gpio_plugin, FU, GPIO_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/gpio/gpio.quirk000066400000000000000000000001751501337203100173470ustar00rootroot00000000000000[GPIO\ID_AMDI0030:00] Name = GPIO controller ParentGuid = cpu [GPIO\ID_AMDI0031:00] Name = GPIO controller ParentGuid = cpu fwupd-2.0.10/plugins/gpio/meson.build000066400000000000000000000011111501337203100174650ustar00rootroot00000000000000gpio_header = cc.has_header_symbol('linux/gpio.h', 'GPIO_V2_LINE_FLAG_OUTPUT', required: false) if gpio_header and host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginGpio"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('gpio.quirk') plugin_builtins += static_library('fu_plugin_gpio', sources: [ 'fu-gpio-plugin.c', 'fu-gpio-device.c', ], include_directories: plugin_incdirs, c_args: [ cargs, '-DLOCALSTATEDIR="' + localstatedir + '"', ], link_with: plugin_libs, dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/hailuck/000077500000000000000000000000001501337203100160135ustar00rootroot00000000000000fwupd-2.0.10/plugins/hailuck/README.md000066400000000000000000000023651501337203100173000ustar00rootroot00000000000000--- title: Plugin: Hailuck --- ## Introduction Hailuck produce the firmware used on the keyboard and trackpad used in the Pinebook Pro laptops. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a packed binary file format. This plugin supports the following protocol ID: * `com.hailuck.kbd` * `com.hailuck.tp` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_0603&PID_1020` ## Update Behavior The keyboard device usually presents in runtime mode, but on detach it re-enumerates with a different USB VID and PID in bootloader mode. On attach the device again re-enumerates back to the runtime mode. For this reason the `REPLUG_MATCH_GUID` internal device flag is used so that the bootloader and runtime modes are treated as the same device. The touchpad firmware is deployed when the device is in normal runtime mode, and the device will reset when the new firmware has been written. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x0603` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.5.2`. fwupd-2.0.10/plugins/hailuck/data/000077500000000000000000000000001501337203100167245ustar00rootroot00000000000000fwupd-2.0.10/plugins/hailuck/data/lspci-bl.txt000066400000000000000000000030331501337203100211710ustar00rootroot00000000000000Bus 003 Device 003: ID 0603:1020 Novatek Microelectronics Corp. Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 1.10 bDeviceClass 0 bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 8 idVendor 0x0603 Novatek Microelectronics Corp. idProduct 0x1020 bcdDevice 3.01 iManufacturer 0 iProduct 0 iSerial 0 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 0x0022 bNumInterfaces 1 bConfigurationValue 1 iConfiguration 0 bmAttributes 0xa0 (Bus Powered) Remote Wakeup MaxPower 100mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 1 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 1 Boot Interface Subclass bInterfaceProtocol 1 Keyboard iInterface 0 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0008 1x 8 bytes bInterval 10 fwupd-2.0.10/plugins/hailuck/data/lspci.txt000066400000000000000000000057371501337203100206130ustar00rootroot00000000000000Bus 003 Device 008: ID 258a:001e HAILUCK CO.,LTD USB KEYBOARD Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 1.10 bDeviceClass 0 bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 8 idVendor 0x258a idProduct 0x001e bcdDevice 1.00 iManufacturer 1 HAILUCK CO.,LTD iProduct 2 USB KEYBOARD iSerial 0 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 0x003b bNumInterfaces 2 bConfigurationValue 1 iConfiguration 0 bmAttributes 0xa0 (Bus Powered) Remote Wakeup MaxPower 100mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 1 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 1 Boot Interface Subclass bInterfaceProtocol 1 Keyboard iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.10 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 65 Report Descriptors: ** UNAVAILABLE ** Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0008 1x 8 bytes bInterval 10 Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 1 bAlternateSetting 0 bNumEndpoints 1 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 0 bInterfaceProtocol 0 iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.10 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 487 Report Descriptors: ** UNAVAILABLE ** Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x82 EP 2 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0008 1x 8 bytes bInterval 10 Device Status: 0x0000 (Bus Powered) fwupd-2.0.10/plugins/hailuck/fu-hailuck-bl-device.c000066400000000000000000000216761501337203100220530ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-hailuck-bl-device.h" #include "fu-hailuck-common.h" #include "fu-hailuck-kbd-firmware.h" #include "fu-hailuck-struct.h" struct _FuHailuckBlDevice { FuHidDevice parent_instance; }; G_DEFINE_TYPE(FuHailuckBlDevice, fu_hailuck_bl_device, FU_TYPE_HID_DEVICE) static gboolean fu_hailuck_bl_device_attach(FuDevice *device, FuProgress *progress, GError **error) { guint8 buf[6] = { FU_HAILUCK_REPORT_ID_SHORT, FU_HAILUCK_CMD_ATTACH, }; if (!fu_hid_device_set_report(FU_HID_DEVICE(device), buf[0], buf, sizeof(buf), 1000, FU_HID_DEVICE_FLAG_IS_FEATURE, error)) return FALSE; if (!fu_usb_device_reset(FU_USB_DEVICE(device), error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_hailuck_bl_device_probe(FuDevice *device, GError **error) { /* add instance ID */ fu_device_add_instance_str(device, "MODE", "KBD"); return fu_device_build_instance_id(device, error, "USB", "VID", "PID", "MODE", NULL); } static gboolean fu_hailuck_bl_device_read_block_start(FuHailuckBlDevice *self, guint32 length, GError **error) { guint8 buf[6] = { FU_HAILUCK_REPORT_ID_SHORT, FU_HAILUCK_CMD_READ_BLOCK_START, }; fu_memwrite_uint16(buf + 4, length, G_LITTLE_ENDIAN); return fu_hid_device_set_report(FU_HID_DEVICE(self), buf[0], buf, sizeof(buf), 100, FU_HID_DEVICE_FLAG_IS_FEATURE, error); } static gboolean fu_hailuck_bl_device_read_block(FuHailuckBlDevice *self, guint8 *data, gsize data_sz, GError **error) { gsize bufsz = data_sz + 2; g_autofree guint8 *buf = g_malloc0(bufsz); buf[0] = FU_HAILUCK_REPORT_ID_LONG; buf[1] = FU_HAILUCK_CMD_READ_BLOCK; if (!fu_hid_device_get_report(FU_HID_DEVICE(self), buf[0], buf, bufsz, 2000, FU_HID_DEVICE_FLAG_IS_FEATURE, error)) return FALSE; if (!fu_memcpy_safe(data, data_sz, 0x0, /* dst */ buf, bufsz, 0x02, /* src */ data_sz, error)) return FALSE; /* success */ fu_device_sleep(FU_DEVICE(self), 10); return TRUE; } static GBytes * fu_hailuck_bl_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuHailuckBlDevice *self = FU_HAILUCK_BL_DEVICE(device); gsize fwsz = fu_device_get_firmware_size_max(device); g_autoptr(GByteArray) fwbuf = g_byte_array_new(); g_autoptr(GPtrArray) chunks = NULL; /* tell device amount of data to send */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ); if (!fu_hailuck_bl_device_read_block_start(self, fwsz, error)) return NULL; /* receive data back */ fu_byte_array_set_size(fwbuf, fwsz, 0x00); chunks = fu_chunk_array_mutable_new(fwbuf->data, fwbuf->len, 0x0, 0x0, 2048); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_hailuck_bl_device_read_block(self, fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), error)) return NULL; fu_progress_step_done(progress); } /* success */ return g_bytes_new(fwbuf->data, fwbuf->len); } static gboolean fu_hailuck_bl_device_erase(FuHailuckBlDevice *self, FuProgress *progress, GError **error) { guint8 buf[6] = { FU_HAILUCK_REPORT_ID_SHORT, FU_HAILUCK_CMD_ERASE, }; if (!fu_hid_device_set_report(FU_HID_DEVICE(self), buf[0], buf, sizeof(buf), 100, FU_HID_DEVICE_FLAG_IS_FEATURE, error)) return FALSE; fu_device_sleep_full(FU_DEVICE(self), 2000, progress); return TRUE; } static gboolean fu_hailuck_bl_device_write_block_start(FuHailuckBlDevice *self, guint32 length, GError **error) { guint8 buf[6] = { FU_HAILUCK_REPORT_ID_SHORT, FU_HAILUCK_CMD_WRITE_BLOCK_START, }; fu_memwrite_uint16(buf + 4, length, G_LITTLE_ENDIAN); return fu_hid_device_set_report(FU_HID_DEVICE(self), buf[0], buf, sizeof(buf), 100, FU_HID_DEVICE_FLAG_IS_FEATURE, error); } static gboolean fu_hailuck_bl_device_write_block(FuHailuckBlDevice *self, const guint8 *data, gsize data_sz, GError **error) { gsize bufsz = data_sz + 2; g_autofree guint8 *buf = g_malloc0(bufsz); buf[0] = FU_HAILUCK_REPORT_ID_LONG; buf[1] = FU_HAILUCK_CMD_WRITE_BLOCK; if (!fu_memcpy_safe(buf, bufsz, 0x02, /* dst */ data, data_sz, 0x0, /* src */ data_sz, error)) return FALSE; if (!fu_hid_device_set_report(FU_HID_DEVICE(self), buf[0], buf, bufsz, 2000, FU_HID_DEVICE_FLAG_IS_FEATURE, error)) return FALSE; /* success */ fu_device_sleep(FU_DEVICE(self), 10); return TRUE; } static gboolean fu_hailuck_bl_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuHailuckBlDevice *self = FU_HAILUCK_BL_DEVICE(device); g_autoptr(GBytes) fw = NULL; g_autoptr(GBytes) fw_new = NULL; g_autoptr(FuChunk) chk0 = NULL; g_autoptr(FuChunkArray) chunks = NULL; g_autofree guint8 *chk0_data = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 10, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 80, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "device-write-blk0"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 9, NULL); /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* erase all contents */ if (!fu_hailuck_bl_device_erase(self, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* tell device amount of data to expect */ if (!fu_hailuck_bl_device_write_block_start(self, g_bytes_get_size(fw), error)) return FALSE; /* build packets */ chunks = fu_chunk_array_new_from_bytes(fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 2048); /* intentionally corrupt first chunk so that CRC fails */ chk0 = fu_chunk_array_index(chunks, 0, error); if (chk0 == NULL) return FALSE; chk0_data = fu_memdup_safe(fu_chunk_get_data(chk0), fu_chunk_get_data_sz(chk0), error); if (chk0_data == NULL) return FALSE; chk0_data[0] = 0x00; if (!fu_hailuck_bl_device_write_block(self, chk0_data, fu_chunk_get_data_sz(chk0), error)) return FALSE; /* send the rest of the chunks */ for (guint i = 1; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_hailuck_bl_device_write_block(self, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; fu_progress_set_percentage_full(fu_progress_get_child(progress), i + 1, fu_chunk_array_length(chunks)); } fu_progress_step_done(progress); /* retry write of first block */ if (!fu_hailuck_bl_device_write_block_start(self, g_bytes_get_size(fw), error)) return FALSE; if (!fu_hailuck_bl_device_write_block(self, fu_chunk_get_data(chk0), fu_chunk_get_data_sz(chk0), error)) return FALSE; fu_progress_step_done(progress); /* verify */ fw_new = fu_hailuck_bl_device_dump_firmware(device, fu_progress_get_child(progress), error); fu_progress_step_done(progress); return fu_bytes_compare(fw, fw_new, error); } static void fu_hailuck_bl_device_init(FuHailuckBlDevice *self) { fu_device_set_firmware_size(FU_DEVICE(self), 0x4000); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_HAILUCK_KBD_FIRMWARE); fu_device_add_protocol(FU_DEVICE(self), "com.hailuck.kbd"); fu_device_set_name(FU_DEVICE(self), "Keyboard [bootloader]"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_INPUT_KEYBOARD); fu_hid_device_add_flag(FU_HID_DEVICE(self), FU_HID_DEVICE_FLAG_NO_KERNEL_REBIND); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); } static void fu_hailuck_bl_device_class_init(FuHailuckBlDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->dump_firmware = fu_hailuck_bl_device_dump_firmware; device_class->write_firmware = fu_hailuck_bl_device_write_firmware; device_class->attach = fu_hailuck_bl_device_attach; device_class->probe = fu_hailuck_bl_device_probe; } fwupd-2.0.10/plugins/hailuck/fu-hailuck-bl-device.h000066400000000000000000000004761501337203100220530ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_HAILUCK_BL_DEVICE (fu_hailuck_bl_device_get_type()) G_DECLARE_FINAL_TYPE(FuHailuckBlDevice, fu_hailuck_bl_device, FU, HAILUCK_BL_DEVICE, FuHidDevice) fwupd-2.0.10/plugins/hailuck/fu-hailuck-common.h000066400000000000000000000003501501337203100215000ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_HAILUCK_REPORT_ID_SHORT 0x05 #define FU_HAILUCK_REPORT_ID_LONG 0x06 fwupd-2.0.10/plugins/hailuck/fu-hailuck-kbd-device.c000066400000000000000000000061571501337203100222130ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-hailuck-common.h" #include "fu-hailuck-kbd-device.h" #include "fu-hailuck-struct.h" #include "fu-hailuck-tp-device.h" struct _FuHailuckKbdDevice { FuHidDevice parent_instance; }; G_DEFINE_TYPE(FuHailuckKbdDevice, fu_hailuck_kbd_device, FU_TYPE_HID_DEVICE) static gboolean fu_hailuck_kbd_device_detach(FuDevice *device, FuProgress *progress, GError **error) { guint8 buf[6] = {FU_HAILUCK_REPORT_ID_SHORT, FU_HAILUCK_CMD_DETACH}; if (!fu_hid_device_set_report(FU_HID_DEVICE(device), buf[0], buf, sizeof(buf), 1000, FU_HID_DEVICE_FLAG_IS_FEATURE, error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_hailuck_kbd_device_probe(FuDevice *device, GError **error) { g_autoptr(FuHailuckTpDevice) tp_device = fu_hailuck_tp_device_new(FU_DEVICE(device)); /* add extra keyboard-specific GUID */ fu_device_add_instance_str(device, "MODE", "KBD"); if (!fu_device_build_instance_id(device, error, "USB", "VID", "PID", "MODE", NULL)) return FALSE; /* add touchpad */ if (!fu_device_probe(FU_DEVICE(tp_device), error)) return FALSE; /* assume the TP has the same version as the keyboard */ fu_device_set_version(FU_DEVICE(tp_device), fu_device_get_version(device)); fu_device_set_version_format(FU_DEVICE(tp_device), fu_device_get_version_format(device)); fu_device_add_child(device, FU_DEVICE(tp_device)); /* success */ return TRUE; } static void fu_hailuck_kbd_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static void fu_hailuck_kbd_device_init(FuHailuckKbdDevice *self) { fu_device_set_firmware_size(FU_DEVICE(self), 0x4000); fu_device_add_protocol(FU_DEVICE(self), "com.hailuck.kbd"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_INPUT_KEYBOARD); fu_hid_device_set_interface(FU_HID_DEVICE(self), 0x1); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); } static void fu_hailuck_kbd_device_class_init(FuHailuckKbdDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->detach = fu_hailuck_kbd_device_detach; device_class->probe = fu_hailuck_kbd_device_probe; device_class->set_progress = fu_hailuck_kbd_device_set_progress; } fwupd-2.0.10/plugins/hailuck/fu-hailuck-kbd-device.h000066400000000000000000000005031501337203100222050ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_HAILUCK_KBD_DEVICE (fu_hailuck_kbd_device_get_type()) G_DECLARE_FINAL_TYPE(FuHailuckKbdDevice, fu_hailuck_kbd_device, FU, HAILUCK_KBD_DEVICE, FuHidDevice) fwupd-2.0.10/plugins/hailuck/fu-hailuck-kbd-firmware.c000066400000000000000000000047711501337203100225700ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-hailuck-kbd-firmware.h" struct _FuHailuckKbdFirmware { FuIhexFirmwareClass parent_instance; }; G_DEFINE_TYPE(FuHailuckKbdFirmware, fu_hailuck_kbd_firmware, FU_TYPE_IHEX_FIRMWARE) static gboolean fu_hailuck_kbd_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { GPtrArray *records = fu_ihex_firmware_get_records(FU_IHEX_FIRMWARE(firmware)); g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GBytes) fw_new = NULL; for (guint j = 0; j < records->len; j++) { FuIhexFirmwareRecord *rcd = g_ptr_array_index(records, j); if (rcd->record_type == FU_IHEX_FIRMWARE_RECORD_TYPE_EOF) break; if (rcd->record_type != FU_IHEX_FIRMWARE_RECORD_TYPE_DATA) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "only record 0x0 supported, got 0x%02x", rcd->record_type); return FALSE; } if (rcd->data->len == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "record 0x%x had zero size", j); return FALSE; } if (rcd->addr + rcd->data->len > buf->len) { if (rcd->addr + rcd->data->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "buffer would have zero size"); return FALSE; } fu_byte_array_set_size(buf, rcd->addr + rcd->data->len, 0x00); } if (!fu_memcpy_safe(buf->data, buf->len, rcd->addr, rcd->data->data, rcd->data->len, 0x0, rcd->data->len, error)) return FALSE; } /* set the main function executed on system init */ if (buf->len > 0x37FD && buf->data[1] == 0x38 && buf->data[2] == 0x00) { buf->data[0] = buf->data[0x37FB]; buf->data[1] = buf->data[0x37FC]; buf->data[2] = buf->data[0x37FD]; buf->data[0x37FB] = 0x00; buf->data[0x37FC] = 0x00; buf->data[0x37FD] = 0x00; } /* whole image */ fw_new = g_bytes_new(buf->data, buf->len); fu_firmware_set_bytes(firmware, fw_new); return TRUE; } static void fu_hailuck_kbd_firmware_init(FuHailuckKbdFirmware *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_NO_AUTO_DETECTION); } static void fu_hailuck_kbd_firmware_class_init(FuHailuckKbdFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_hailuck_kbd_firmware_parse; } fwupd-2.0.10/plugins/hailuck/fu-hailuck-kbd-firmware.h000066400000000000000000000005541501337203100225700ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_HAILUCK_KBD_FIRMWARE (fu_hailuck_kbd_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuHailuckKbdFirmware, fu_hailuck_kbd_firmware, FU, HAILUCK_KBD_FIRMWARE, FuIhexFirmware) fwupd-2.0.10/plugins/hailuck/fu-hailuck-plugin.c000066400000000000000000000016641501337203100215120ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-hailuck-bl-device.h" #include "fu-hailuck-kbd-device.h" #include "fu-hailuck-kbd-firmware.h" #include "fu-hailuck-plugin.h" struct _FuHailuckPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuHailuckPlugin, fu_hailuck_plugin, FU_TYPE_PLUGIN) static void fu_hailuck_plugin_init(FuHailuckPlugin *self) { } static void fu_hailuck_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_HAILUCK_KBD_FIRMWARE); fu_plugin_add_device_gtype(plugin, FU_TYPE_HAILUCK_BL_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_HAILUCK_KBD_DEVICE); } static void fu_hailuck_plugin_class_init(FuHailuckPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_hailuck_plugin_constructed; } fwupd-2.0.10/plugins/hailuck/fu-hailuck-plugin.h000066400000000000000000000003571501337203100215150ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuHailuckPlugin, fu_hailuck_plugin, FU, HAILUCK_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/hailuck/fu-hailuck-tp-device.c000066400000000000000000000176551501337203100221030ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-hailuck-common.h" #include "fu-hailuck-struct.h" #include "fu-hailuck-tp-device.h" struct _FuHailuckTpDevice { FuDevice parent_instance; }; G_DEFINE_TYPE(FuHailuckTpDevice, fu_hailuck_tp_device, FU_TYPE_DEVICE) static gboolean fu_hailuck_tp_device_probe(FuDevice *device, GError **error) { /* add extra touchpad-specific GUID */ fu_device_add_instance_str(device, "MODE", "TP"); return fu_device_build_instance_id(device, error, "USB", "VID", "PID", "MODE", NULL); } typedef struct { guint8 type; guint8 success; /* if 0xff, then cmd-0x10 */ } FuHailuckTpDeviceReq; static gboolean fu_hailuck_tp_device_cmd_cb(FuDevice *device, gpointer user_data, GError **error) { FuDevice *parent = fu_device_get_parent(device); FuHailuckTpDeviceReq *req = (FuHailuckTpDeviceReq *)user_data; guint8 buf[6] = { FU_HAILUCK_REPORT_ID_SHORT, FU_HAILUCK_CMD_GET_STATUS, req->type, }; guint8 success_tmp = req->success; if (!fu_hid_device_set_report(FU_HID_DEVICE(parent), buf[0], buf, sizeof(buf), 1000, FU_HID_DEVICE_FLAG_IS_FEATURE, error)) return FALSE; if (!fu_hid_device_get_report(FU_HID_DEVICE(parent), buf[0], buf, sizeof(buf), 2000, FU_HID_DEVICE_FLAG_IS_FEATURE | FU_HID_DEVICE_FLAG_ALLOW_TRUNC, error)) return FALSE; if (success_tmp == 0xff) success_tmp = req->type - 0x10; if (buf[0] != FU_HAILUCK_REPORT_ID_SHORT || buf[1] != success_tmp) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "report mismatch for type=0x%02x[%s]: " "expected=0x%02x, received=0x%02x", req->type, fu_hailuck_cmd_to_string(req->type), success_tmp, buf[1]); return FALSE; } return TRUE; } static gboolean fu_hailuck_tp_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDevice *parent = fu_device_get_parent(device); const guint block_size = 1024; g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; FuHailuckTpDeviceReq req = { .type = 0xff, .success = 0xff, }; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 10, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 85, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "end-program"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 3, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "pass"); /* get default image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* erase */ req.type = FU_HAILUCK_CMD_I2C_ERASE; if (!fu_device_retry(device, fu_hailuck_tp_device_cmd_cb, 100, &req, error)) { g_prefix_error(error, "failed to erase: "); return FALSE; } fu_device_sleep(device, 10); fu_progress_step_done(progress); /* write */ chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, block_size, error); if (chunks == NULL) return FALSE; for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GByteArray) buf = g_byte_array_new(); /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; /* write block */ fu_byte_array_append_uint8(buf, FU_HAILUCK_REPORT_ID_LONG); fu_byte_array_append_uint8(buf, FU_HAILUCK_CMD_WRITE_TP); fu_byte_array_append_uint16(buf, 0xCCCC, G_LITTLE_ENDIAN); fu_byte_array_append_uint16(buf, fu_chunk_get_address(chk), G_LITTLE_ENDIAN); fu_byte_array_append_uint16(buf, 0xCCCC, G_LITTLE_ENDIAN); g_byte_array_append(buf, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); fu_byte_array_append_uint8(buf, 0xEE); fu_byte_array_append_uint8(buf, 0xD2); fu_byte_array_append_uint16(buf, 0xCCCC, G_LITTLE_ENDIAN); fu_byte_array_append_uint16(buf, 0xCCCC, G_LITTLE_ENDIAN); fu_byte_array_append_uint16(buf, 0xCCCC, G_LITTLE_ENDIAN); if (buf->len != block_size + 16) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "packet mismatch: len=0x%04x, expected=0x%04x", buf->len, block_size + 16); return FALSE; } if (!fu_hid_device_set_report(FU_HID_DEVICE(parent), buf->data[0], buf->data, buf->len, 1000, FU_HID_DEVICE_FLAG_IS_FEATURE, error)) { g_prefix_error(error, "failed to write block 0x%x: ", i); return FALSE; } fu_device_sleep(device, 150); /* verify block */ req.type = FU_HAILUCK_CMD_I2C_VERIFY_BLOCK; if (!fu_device_retry(device, fu_hailuck_tp_device_cmd_cb, 100, &req, error)) { g_prefix_error(error, "failed to verify block 0x%x: ", i); return FALSE; } /* update progress */ fu_progress_set_percentage_full(fu_progress_get_child(progress), i + 1, fu_chunk_array_length(chunks)); } fu_device_sleep(device, 50); fu_progress_step_done(progress); /* end-program */ req.type = FU_HAILUCK_CMD_I2C_END_PROGRAM; if (!fu_device_retry(device, fu_hailuck_tp_device_cmd_cb, 100, &req, error)) { g_prefix_error(error, "failed to end program: "); return FALSE; } fu_device_sleep(device, 50); fu_progress_step_done(progress); /* verify checksum */ req.type = FU_HAILUCK_CMD_I2C_VERIFY_CHECKSUM; if (!fu_device_retry(device, fu_hailuck_tp_device_cmd_cb, 100, &req, error)) { g_prefix_error(error, "failed to verify: "); return FALSE; } fu_device_sleep(device, 50); fu_progress_step_done(progress); /* signal that programming has completed */ req.type = FU_HAILUCK_CMD_I2C_PROGRAMPASS; req.success = 0x0; if (!fu_device_retry(device, fu_hailuck_tp_device_cmd_cb, 100, &req, error)) { g_prefix_error(error, "failed to program: "); return FALSE; } fu_progress_step_done(progress); /* success! */ return TRUE; } static void fu_hailuck_tp_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static void fu_hailuck_tp_device_init(FuHailuckTpDevice *self) { fu_device_retry_set_delay(FU_DEVICE(self), 50); /* ms */ fu_device_set_firmware_size(FU_DEVICE(self), 0x6018); fu_device_add_protocol(FU_DEVICE(self), "com.hailuck.tp"); fu_device_set_logical_id(FU_DEVICE(self), "TP"); fu_device_set_name(FU_DEVICE(self), "Touchpad"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_OPEN); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_INPUT_TOUCHPAD); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); } static void fu_hailuck_tp_device_class_init(FuHailuckTpDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_hailuck_tp_device_write_firmware; device_class->probe = fu_hailuck_tp_device_probe; device_class->set_progress = fu_hailuck_tp_device_set_progress; } FuHailuckTpDevice * fu_hailuck_tp_device_new(FuDevice *parent) { FuHailuckTpDevice *self; self = g_object_new(FU_TYPE_HAILUCK_TP_DEVICE, "parent", parent, NULL); return FU_HAILUCK_TP_DEVICE(self); } fwupd-2.0.10/plugins/hailuck/fu-hailuck-tp-device.h000066400000000000000000000005741501337203100221000ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_HAILUCK_TP_DEVICE (fu_hailuck_tp_device_get_type()) G_DECLARE_FINAL_TYPE(FuHailuckTpDevice, fu_hailuck_tp_device, FU, HAILUCK_TP_DEVICE, FuDevice) FuHailuckTpDevice * fu_hailuck_tp_device_new(FuDevice *parent); fwupd-2.0.10/plugins/hailuck/fu-hailuck.rs000066400000000000000000000012571501337203100204160ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] enum FuHailuckCmd { Erase = 0x45, ReadBlockStart = 0x52, Attach = 0x55, // guessed WriteBlockStart = 0x57, ReadBlock = 0x72, Detach = 0x75, // guessed WriteBlock = 0x77, GetStatus = 0xA1, WriteTp = 0xD0, // guessed I2cCheckChecksum = 0xF0, I2cEnterBl = 0xF1, I2cErase = 0xF2, I2cProgram = 0xF3, I2cVerifyBlock = 0xF4, I2cVerifyChecksum = 0xF5, I2cProgrampass = 0xF6, I2cEndProgram = 0xF7, } fwupd-2.0.10/plugins/hailuck/hailuck.quirk000066400000000000000000000007611501337203100205140ustar00rootroot00000000000000# bootloader [USB\VID_0603&PID_1020] Plugin = hailuck GType = FuHailuckBlDevice #Flags = is-bootloader [USB\VID_258A&PID_001E] Plugin = hailuck GType = FuHailuckKbdDevice Vendor = PINE64 CounterpartGuid = USB\VID_0603&PID_1020 [USB\VID_258A&PID_001E&MODE_KBD] Name = Keyboard [USB\VID_258A&PID_001F] Plugin = hailuck GType = FuHailuckKbdDevice CounterpartGuid = USB\VID_0603&PID_1020 [USB\VID_258A&PID_000D] Plugin = hailuck GType = FuHailuckKbdDevice CounterpartGuid = USB\VID_0603&PID_1020 fwupd-2.0.10/plugins/hailuck/meson.build000066400000000000000000000011661501337203100201610ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginHailuck"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('hailuck.quirk') plugin_builtins += static_library('fu_plugin_hailuck', rustgen.process('fu-hailuck.rs'), sources: [ 'fu-hailuck-bl-device.c', 'fu-hailuck-kbd-device.c', 'fu-hailuck-kbd-firmware.c', # fuzzing 'fu-hailuck-tp-device.c', 'fu-hailuck-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) enumeration_data += files('tests/hailuck-setup.json') device_tests += files('tests/hailuck.json') fwupd-2.0.10/plugins/hailuck/tests/000077500000000000000000000000001501337203100171555ustar00rootroot00000000000000fwupd-2.0.10/plugins/hailuck/tests/hailuck-setup.json000066400000000000000000000043071501337203100226320ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUsbDevice", "PlatformId": "1-1", "Created": "2024-10-29T16:25:20.797574Z", "IdVendor": 9610, "IdProduct": 30, "Device": 256, "USB": 272, "Manufacturer": 1, "Product": 2, "UsbConfigDescriptors": [ { "ConfigurationValue": 1 } ], "UsbInterfaces": [ { "Length": 9, "DescriptorType": 4, "InterfaceClass": 3, "InterfaceSubClass": 1, "InterfaceProtocol": 1, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 129, "Interval": 10, "MaxPacketSize": 8 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 1, "InterfaceClass": 3, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 130, "Interval": 10, "MaxPacketSize": 8 } ] } ], "UsbEvents": [ { "Id": "#d5a801ad", "Data": "usb" }, { "Id": "#ad8c58d3", "Data": "usb" }, { "Id": "#1075ed5c", "Data": "usb_device" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=1\nDEVNAME=bus/usb/001/002\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=258a/1e/100\nTYPE=0/0/0\nBUSNUM=001\nDEVNUM=002" }, { "Id": "#d432c663", "Data": "bus/usb/001/002" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c", "Data": "usb_device" }, { "Id": "#4693935e", "Data": "001" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=1\nDEVNAME=bus/usb/001/002\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=258a/1e/100\nTYPE=0/0/0\nBUSNUM=001\nDEVNUM=002" }, { "Id": "#1ab3ae0a", "Data": "002" } ] } ] } fwupd-2.0.10/plugins/hailuck/tests/hailuck.json000066400000000000000000000005141501337203100214700ustar00rootroot00000000000000{ "name": "Hailuck Keyboard (PineBook Pro)", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/hailuck-setup.json", "components": [ { "version": "1.0", "guids": [ "e997e24f-850a-51ba-b2be-b7c9e07b794b" ] } ] } ] } fwupd-2.0.10/plugins/hpi-cfu/000077500000000000000000000000001501337203100157265ustar00rootroot00000000000000fwupd-2.0.10/plugins/hpi-cfu/README.md000066400000000000000000000027541501337203100172150ustar00rootroot00000000000000--- title: Plugin: HPI CFU --- ## Introduction The plugin used for updating firmware on HP USB 4 100W G6 Dock and HP Thunderbolt 4 Ultra 180W/280W G6 Dock. This plugin supports the following protocol ID: * `com.microsoft.cfu` ## Implementation Notes In fwupd these can be set as quirks in `hpi-cfu.quirk`. ## Firmware Format The daemon only deals with one “payload†per update. The offer and payload currently have to be combined in an archive where they are transferred to the device one after the other. The files in the firmware archive therefore should have the extensions `.offer.bin` and `.payload.bin` as a zip folder. ## GUID Generation These devices use the standard USB DeviceInstanceId values as well as one extra for the Revision number, e.g. * `USB\VID_03F0&PID_0BAF` * `USB\VID_03F0&PID_0BAF&REV_0001` ## Update Behavior The device has to support runtime updates and does not have a detach-into-bootloader mode -- but after the install has completed the device still has to reboot into the new firmware. ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x03F0` ## Version Considerations This plugin has been available since fwupd version `2.0.0`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Pena Christian fwupd-2.0.10/plugins/hpi-cfu/fu-hpi-cfu-device.c000066400000000000000000001337511501337203100213040ustar00rootroot00000000000000/* * Copyright 2024 HP Development Company, L.P. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fu-cfu-struct.h" #include "fu-hpi-cfu-device.h" #include "fu-hpi-cfu-struct.h" #define GET_REPORT 0x01 #define SET_REPORT 0x09 #define FIRMWARE_REPORT_ID 0x20 #define OFFER_REPORT_ID 0x25 #define END_POINT_ADDRESS 0x81 #define FU_HPI_CFU_INTERFACE 0x0000 #define IN_REPORT_TYPE 0x0100 #define OUT_REPORT_TYPE 0x0200 #define FEATURE_REPORT_TYPE 0x0300 #define FU_HPI_CFU_PAYLOAD_LENGTH 52 #define FU_HPI_CFU_DEVICE_TIMEOUT 0 /* ms */ const guint8 report_data[15] = {0x00, 0xff, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; struct _FuHpiCfuDevice { FuUsbDevice parent_instance; FuHpiCfuState state; guint8 force_version; guint8 force_reset; gint sequence_number; gint32 currentaddress; gint8 retry_attempts; gsize payload_file_size; gboolean last_packet_sent; guint8 bulk_opt; gboolean firmware_status; gboolean exit_state_machine_framework; }; typedef struct { FuFirmware *fw_offer; FuFirmware *fw_payload; } FuHpiCfuHandlerOptions; FuHpiCfuHandlerOptions handler_options; typedef gint32 (*FuHpiCfuStateHandler)(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error); typedef struct { FuHpiCfuState state_no; FuHpiCfuStateHandler handler; FuHpiCfuHandlerOptions *options; } FuHpiCfuStateMachineFramework; G_DEFINE_TYPE(FuHpiCfuDevice, fu_hpi_cfu_device, FU_TYPE_USB_DEVICE) static gboolean fu_hpi_cfu_device_start_entire_transaction(FuHpiCfuDevice *self, GError **error) { g_autoptr(GError) error_local = NULL; g_autoptr(GByteArray) st_req = fu_struct_hpi_cfu_buf_new(); fu_struct_hpi_cfu_buf_set_report_id(st_req, OFFER_REPORT_ID); fu_struct_hpi_cfu_buf_set_command(st_req, FU_CFU_OFFER_INFO_CODE_START_ENTIRE_TRANSACTION); if (!fu_struct_hpi_cfu_buf_set_report_data(st_req, report_data, sizeof(report_data), error)) return FALSE; fu_dump_raw(G_LOG_DOMAIN, "StartEntireTransaction", st_req->data, st_req->len); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, SET_REPORT, OUT_REPORT_TYPE | OFFER_REPORT_ID, FU_HPI_CFU_INTERFACE, st_req->data, st_req->len, NULL, FU_HPI_CFU_DEVICE_TIMEOUT, NULL, &error_local)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, error_local->message); return FALSE; } /* success */ return TRUE; } static gboolean fu_hpi_cfu_device_start_entire_transaction_accepted(FuHpiCfuDevice *self, GError **error) { gsize actual_length = 0; guint8 buf[128] = {0}; g_autoptr(GError) error_local = NULL; if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), END_POINT_ADDRESS, buf, sizeof(buf), &actual_length, FU_HPI_CFU_DEVICE_TIMEOUT, NULL, &error_local)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, error_local->message); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "BytesReceived", buf, actual_length); if (buf[13] == 0x01) self->state = FU_HPI_CFU_STATE_START_OFFER_LIST; else self->state = FU_HPI_CFU_STATE_ERROR; return TRUE; } static gboolean fu_hpi_cfu_device_send_start_offer_list(FuHpiCfuDevice *self, GError **error) { g_autoptr(GError) error_local = NULL; g_autoptr(GByteArray) st_req = fu_struct_hpi_cfu_buf_new(); fu_struct_hpi_cfu_buf_set_report_id(st_req, OFFER_REPORT_ID); fu_struct_hpi_cfu_buf_set_command(st_req, FU_CFU_OFFER_INFO_CODE_START_OFFER_LIST); if (!fu_struct_hpi_cfu_buf_set_report_data(st_req, report_data, sizeof(report_data), error)) return FALSE; fu_dump_raw(G_LOG_DOMAIN, "SendStartOfferList", st_req->data, st_req->len); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, SET_REPORT, OUT_REPORT_TYPE | OFFER_REPORT_ID, FU_HPI_CFU_INTERFACE, st_req->data, st_req->len, NULL, FU_HPI_CFU_DEVICE_TIMEOUT, NULL, &error_local)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, error_local->message); return FALSE; } /* success */ return TRUE; } static gboolean fu_hpi_cfu_device_send_offer_list_accepted(FuHpiCfuDevice *self, gint8 *status, GError **error) { gsize actual_length = 0; guint8 buf[128] = {0}; g_autoptr(GError) error_local = NULL; if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), END_POINT_ADDRESS, buf, sizeof(buf), &actual_length, FU_HPI_CFU_DEVICE_TIMEOUT, NULL, &error_local)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, error_local->message); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "SendOfferListAccepted", buf, actual_length); /* success */ if (buf[13] == 0x01) { g_debug("success"); } else { if (buf[13] == 0x02) { g_warning("accepted with reason: %s", fu_cfu_rr_code_to_string(buf[9])); } else { g_warning("failed with reason: %s but is not reject.", fu_cfu_rr_code_to_string(buf[9])); } } *status = buf[13]; return TRUE; } static gboolean fu_hpi_cfu_device_send_offer_update_command(FuHpiCfuDevice *self, FuFirmware *fw_offer, GError **error) { const guint8 *buf; gint8 flag_value = 0; gsize bufsz = 0; g_autoptr(GByteArray) st_req = fu_struct_hpi_cfu_offer_cmd_new(); g_autoptr(GError) error_local = NULL; g_autoptr(GBytes) blob_offer = NULL; blob_offer = fu_firmware_get_bytes(fw_offer, error); if (blob_offer == NULL) return FALSE; buf = g_bytes_get_data(blob_offer, &bufsz); fu_struct_hpi_cfu_payload_cmd_set_report_id(st_req, OFFER_REPORT_ID); if (!fu_memcpy_safe(st_req->data, st_req->len, 0x1, buf, bufsz, 0x0, 16, error)) return FALSE; FU_BIT_SET(flag_value, 7); /* (update now) */ FU_BIT_SET(flag_value, 6); /* (force update version) */ fu_struct_hpi_cfu_offer_cmd_set_flags(st_req, flag_value); fu_dump_raw(G_LOG_DOMAIN, "SendOfferUpdateCommand", st_req->data, st_req->len); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, SET_REPORT, OUT_REPORT_TYPE | FIRMWARE_REPORT_ID, FU_HPI_CFU_INTERFACE, st_req->data, st_req->len, NULL, FU_HPI_CFU_DEVICE_TIMEOUT, NULL, &error_local)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, error_local->message); return FALSE; } /* success */ return TRUE; } static gboolean fu_hpi_cfu_device_firmware_update_offer_accepted(FuHpiCfuDevice *self, gint8 *reply, gint8 *reason, GError **error) { gsize actual_length = 0; guint8 buf[128] = {0}; g_autoptr(GError) error_local = NULL; if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), END_POINT_ADDRESS, buf, sizeof(buf), &actual_length, FU_HPI_CFU_DEVICE_TIMEOUT, NULL, &error_local)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, error_local->message); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "FirmwareUpdateOfferAccepted", buf, actual_length); *reason = buf[9]; if (buf[13] == 0x01) { g_debug("success"); } else { if (buf[13] == 0x02) { g_debug("offer accepted: %s", fu_cfu_rr_code_to_string(buf[9])); } else { g_debug("offer accepted: %s is not a reject", fu_cfu_rr_code_to_string(buf[9])); } } /* success */ *reply = buf[13]; return TRUE; } static gboolean fu_hpi_cfu_device_read_content_ack(FuHpiCfuDevice *self, gboolean *lastpacket, guint8 *report_id, guint8 *reason, guint8 *status, GError **error) { gsize actual_length = 0; guint8 buf[128] = {0}; g_autoptr(GError) error_local = NULL; g_debug("sequence number: %d", self->sequence_number); if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), END_POINT_ADDRESS, buf, sizeof(buf), &actual_length, FU_HPI_CFU_DEVICE_TIMEOUT, NULL, &error_local)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, error_local->message); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "ReadContentAck", buf, actual_length); /* success */ *report_id = buf[0]; if (buf[0] == FIRMWARE_REPORT_ID) { g_debug("status:%s response:%s", fu_cfu_offer_status_to_string(buf[13]), fu_cfu_rr_code_to_string(buf[9])); if (buf[13] == 0x01) { if (self->last_packet_sent) *lastpacket = TRUE; } *status = buf[13]; } else { g_debug("read_content_ack: buffer[5]: %02x, response:%s", (guchar)buf[5], fu_cfu_content_status_to_string(buf[5])); if (buf[5] == 0x00) { g_debug("read_content_ack:1"); if (self->last_packet_sent) *lastpacket = TRUE; *status = buf[5]; } else { *status = buf[5]; } } /* success */ return TRUE; } static gboolean fu_hpi_cfu_device_firmware_update_offer_rejected(gint8 reply) { if (reply == FU_HPI_CFU_STATE_UPDATE_OFFER_REJECTED) { g_debug("OfferRejected"); return TRUE; } return FALSE; } static gboolean fu_hpi_cfu_device_send_end_offer_list(FuHpiCfuDevice *self, GError **error) { g_autoptr(GError) error_local = NULL; g_autoptr(GByteArray) st_req = fu_struct_hpi_cfu_buf_new(); fu_struct_hpi_cfu_buf_set_report_id(st_req, OFFER_REPORT_ID); fu_struct_hpi_cfu_buf_set_command(st_req, FU_CFU_OFFER_INFO_CODE_END_OFFER_LIST); if (!fu_struct_hpi_cfu_buf_set_report_data(st_req, report_data, sizeof(report_data), error)) return FALSE; fu_dump_raw(G_LOG_DOMAIN, "SendEndOfferListCommand", st_req->data, st_req->len); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, SET_REPORT, OUT_REPORT_TYPE | OFFER_REPORT_ID, FU_HPI_CFU_INTERFACE, st_req->data, st_req->len, NULL, FU_HPI_CFU_DEVICE_TIMEOUT, NULL, &error_local)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, error_local->message); return FALSE; } /* success */ return TRUE; } static gboolean fu_hpi_cfu_device_end_offer_list_accepted(FuHpiCfuDevice *self, GError **error) { gsize actual_length = 0; guint8 buf[128] = {0}; g_autoptr(GError) error_local = NULL; if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), END_POINT_ADDRESS, buf, sizeof(buf), &actual_length, FU_HPI_CFU_DEVICE_TIMEOUT, NULL, &error_local)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, error_local->message); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "EndOfferListAccepted", buf, actual_length); g_debug("identify type 0x%02x", (guchar)buf[4]); g_debug("reply status: 0x%02x (%s)", (guchar)buf[13], fu_cfu_rr_code_to_string(buf[13])); /* success */ if (buf[13] != 0x01) { if (buf[13] == 0x02) { g_warning("not acceptance with reason: %s", fu_cfu_rr_code_to_string(buf[9])); } else { g_warning("not acceptance with reason: %s but is not REJECT", fu_cfu_rr_code_to_string(buf[9])); } } /* success */ return TRUE; } static gboolean fu_hpi_cfu_device_handler_start_entire_transaction(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { if (!fu_hpi_cfu_device_start_entire_transaction(self, error)) { self->state = FU_HPI_CFU_STATE_ERROR; g_prefix_error(error, "start_entire_transaction: "); return FALSE; } self->state = FU_HPI_CFU_STATE_START_ENTIRE_TRANSACTION_ACCEPTED; return TRUE; } static gboolean fu_hpi_cfu_device_handler_start_entire_transaction_accepted(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { if (!fu_hpi_cfu_device_start_entire_transaction_accepted(self, error)) { self->state = FU_HPI_CFU_STATE_ERROR; g_prefix_error(error, "start_entire_transaction_accept: "); return FALSE; } self->state = FU_HPI_CFU_STATE_START_OFFER_LIST; fu_progress_step_done(progress); /* start-entire */ return TRUE; } static gboolean fu_hpi_cfu_device_handler_send_start_offer_list(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { if (!fu_hpi_cfu_device_send_start_offer_list(self, error)) { self->state = FU_HPI_CFU_STATE_ERROR; g_prefix_error(error, "start_offer_list: "); return FALSE; } self->state = FU_HPI_CFU_STATE_START_OFFER_LIST_ACCEPTED; /* success */ return TRUE; } static gboolean fu_hpi_cfu_device_handler_send_start_offer_list_accepted(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { gint8 status = 0; if (!fu_hpi_cfu_device_send_offer_list_accepted(self, &status, error)) { self->state = FU_HPI_CFU_STATE_UPDATE_STOP; g_prefix_error(error, "start_offer_list_accept: "); return FALSE; } if (status >= 0) self->state = FU_HPI_CFU_STATE_UPDATE_OFFER; else self->state = FU_HPI_CFU_STATE_UPDATE_STOP; fu_progress_step_done(progress); /* offer-accept */ return TRUE; } static gboolean fu_hpi_cfu_device_handler_send_offer_update_command(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { if (!fu_hpi_cfu_device_send_offer_update_command(self, options->fw_offer, error)) { self->state = FU_HPI_CFU_STATE_ERROR; g_prefix_error(error, "send_offer_update_command: "); return FALSE; } self->state = FU_HPI_CFU_STATE_UPDATE_OFFER_ACCEPTED; return TRUE; } static gboolean fu_hpi_cfu_device_handler_send_offer_accepted(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { gint8 reply = 0; gint8 reason = 0; if (!fu_hpi_cfu_device_firmware_update_offer_accepted(self, &reply, &reason, error)) { self->state = FU_HPI_CFU_STATE_ERROR; g_prefix_error(error, "send_offer_accepted: "); return FALSE; } if (reply == FU_HPI_CFU_FIRMWARE_UPDATE_OFFER_ACCEPT) { g_debug("send_offer_accepted: reason: %s", fu_hpi_cfu_firmware_update_offer_to_string(reply)); self->sequence_number = 0; self->currentaddress = 0; self->last_packet_sent = FALSE; self->state = FU_HPI_CFU_STATE_UPDATE_CONTENT; } else { if (reply == FU_HPI_CFU_FIRMWARE_UPDATE_OFFER_SKIP || reply == FU_HPI_CFU_FIRMWARE_UPDATE_OFFER_REJECT) { g_debug("send_offer_accepted: reason: %s", fu_hpi_cfu_firmware_update_offer_to_string(reason)); self->state = FU_HPI_CFU_STATE_UPDATE_MORE_OFFERS; } else if (reply == FU_HPI_CFU_FIRMWARE_UPDATE_OFFER_BUSY) { g_debug("send_offer_accepted: reason: %s", fu_hpi_cfu_firmware_update_offer_to_string(reason)); self->retry_attempts++; self->state = FU_HPI_CFU_STATE_START_ENTIRE_TRANSACTION; if (self->retry_attempts > 3) { self->state = FU_HPI_CFU_STATE_NOTIFY_ON_READY; g_warning("send_offer_accepted after 3 retry " "attempts. Restart the device(Reason: Device busy)"); } else self->state = FU_HPI_CFU_STATE_START_ENTIRE_TRANSACTION; } else { self->state = FU_HPI_CFU_STATE_UPDATE_MORE_OFFERS; } } fu_progress_step_done(progress); /* send-offer */ /* success */ return TRUE; } static gboolean fu_hpi_cfu_device_send_payload(FuHpiCfuDevice *self, GByteArray *cfu_buf, GError **error) { g_autoptr(GByteArray) st_req = fu_struct_hpi_cfu_payload_cmd_new(); g_autoptr(GError) error_local = NULL; fu_struct_hpi_cfu_payload_cmd_set_report_id(st_req, FIRMWARE_REPORT_ID); self->sequence_number++; if (self->sequence_number == 1) fu_struct_hpi_cfu_payload_cmd_set_flags(st_req, FU_CFU_CONTENT_FLAG_FIRST_BLOCK); if (self->last_packet_sent) fu_struct_hpi_cfu_payload_cmd_set_flags(st_req, FU_CFU_CONTENT_FLAG_LAST_BLOCK); fu_struct_hpi_cfu_payload_cmd_set_length(st_req, cfu_buf->len); fu_struct_hpi_cfu_payload_cmd_set_seq_number(st_req, self->sequence_number); fu_struct_hpi_cfu_payload_cmd_set_address(st_req, self->currentaddress); if (!fu_struct_hpi_cfu_payload_cmd_set_data(st_req, cfu_buf->data, cfu_buf->len, error)) return FALSE; self->currentaddress += cfu_buf->len; fu_dump_raw(G_LOG_DOMAIN, "ToDevice", st_req->data, st_req->len); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, SET_REPORT, OUT_REPORT_TYPE | FIRMWARE_REPORT_ID, FU_HPI_CFU_INTERFACE, st_req->data, st_req->len, NULL, FU_HPI_CFU_DEVICE_TIMEOUT, NULL, &error_local)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, error_local->message); return FALSE; } /* success */ return TRUE; } static gboolean fu_hpi_cfu_device_untransmitted_data(GByteArray *payload_data, GByteArray *untransmitted_data, gint8 payload_header_length, guint32 fill_from_position, GError **error) { guint32 remaining_byte_count = payload_header_length - fill_from_position; fu_byte_array_set_size(untransmitted_data, remaining_byte_count, 0x00); return fu_memcpy_safe(untransmitted_data->data, untransmitted_data->len, 0x0, payload_data->data, payload_data->len, fill_from_position, remaining_byte_count, error); } static gboolean fu_hpi_cfu_device_get_payload_header(GByteArray *payload_header, GByteArray *payload_buf, guint32 read_index, GError **error) { fu_byte_array_set_size(payload_header, 5, 0x00); return fu_memcpy_safe(payload_header->data, payload_header->len, 0x0, payload_buf->data, payload_buf->len, read_index, 5, error); } static gboolean fu_hpi_cfu_device_get_payload_data(GByteArray *payload_data, GByteArray *payload_buf, gint8 payload_header_length, gint32 read_index, GError **error) { fu_byte_array_set_size(payload_data, payload_header_length, 0x00); return fu_memcpy_safe(payload_data->data, payload_data->len, 0x0, payload_buf->data, payload_buf->len, read_index + 5, payload_header_length, error); } static gboolean fu_hpi_cfu_device_send_append_untransmitted(FuHpiCfuDevice *self, gint32 payload_header_length, GByteArray *payload_data, GByteArray *untransmitted_data, GByteArray *cfu_data, GError **error) { gsize remaining_byte_count = 0; gsize fill_from_position = 0; if (untransmitted_data->len >= FU_HPI_CFU_PAYLOAD_LENGTH) { /* cfu_data to be sent to device */ g_byte_array_append(cfu_data, untransmitted_data->data, FU_HPI_CFU_PAYLOAD_LENGTH); if (!fu_hpi_cfu_device_send_payload(self, cfu_data, error)) return FALSE; remaining_byte_count = untransmitted_data->len - FU_HPI_CFU_PAYLOAD_LENGTH; fill_from_position = untransmitted_data->len - remaining_byte_count; if (remaining_byte_count > 0) { g_autoptr(GByteArray) untransmitted_remain = g_byte_array_new(); /* store the untransmitted_data remaining data */ if (!fu_hpi_cfu_device_untransmitted_data(untransmitted_data, untransmitted_remain, untransmitted_data->len, fill_from_position, error)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "to set untransmitted_data"); return FALSE; } } } else { /* append untransmitted_data first */ g_byte_array_append(cfu_data, untransmitted_data->data, untransmitted_data->len); fill_from_position = FU_HPI_CFU_PAYLOAD_LENGTH - untransmitted_data->len; remaining_byte_count = payload_header_length - fill_from_position; /* append actual payload_data */ g_byte_array_append(cfu_data, payload_data->data, fill_from_position); if (!fu_hpi_cfu_device_send_payload(self, cfu_data, error)) return FALSE; if (remaining_byte_count >= FU_HPI_CFU_PAYLOAD_LENGTH) { g_autoptr(GByteArray) cfu_data_remain = g_byte_array_new(); /* append remaining payload_data first */ g_byte_array_append(cfu_data_remain, payload_data->data + fill_from_position, FU_HPI_CFU_PAYLOAD_LENGTH); if (!fu_hpi_cfu_device_send_payload(self, cfu_data_remain, error)) return FALSE; remaining_byte_count = remaining_byte_count - FU_HPI_CFU_PAYLOAD_LENGTH; fill_from_position = payload_header_length - remaining_byte_count; } /* store the untransmitted_data */ if (!fu_hpi_cfu_device_untransmitted_data(payload_data, untransmitted_data, payload_header_length, fill_from_position, error)) { g_prefix_error(error, "failed to set untransmitted_data: "); return FALSE; } } /* success */ return TRUE; } static void fu_hpi_cfu_device_handler_set_status_report_25(FuHpiCfuDevice *self, guint8 status, gboolean lastpacket) { switch (status) { case FU_HPI_CFU_FIRMWARE_UPDATE_OFFER_SKIP: case FU_HPI_CFU_FIRMWARE_UPDATE_OFFER_REJECT: case FU_HPI_CFU_FIRMWARE_UPDATE_OFFER_COMMAND_READY: case FU_HPI_CFU_FIRMWARE_UPDATE_OFFER_CMD_NOT_SUPPORTED: g_warning("check_update_content: reason: %s", fu_hpi_cfu_firmware_update_offer_to_string(status)); self->state = FU_HPI_CFU_STATE_UPDATE_MORE_OFFERS; break; case FU_HPI_CFU_FIRMWARE_UPDATE_OFFER_ACCEPT: g_debug("check_update_content: reason: %s", fu_hpi_cfu_firmware_update_offer_to_string(status)); if (lastpacket) { g_debug("check_update_content: reason: %s for last_packet_sent", fu_hpi_cfu_firmware_update_offer_to_string(status)); self->state = FU_HPI_CFU_STATE_UPDATE_SUCCESS; } else self->state = FU_HPI_CFU_STATE_UPDATE_CONTENT; break; case FU_HPI_CFU_FIRMWARE_UPDATE_OFFER_BUSY: g_warning("check_update_content: reason:%s", fu_hpi_cfu_firmware_update_offer_to_string(status)); self->state = FU_HPI_CFU_STATE_NOTIFY_ON_READY; break; default: g_warning("check_update_content: FU_HPI_CFU_STATE_ERROR"); self->state = FU_HPI_CFU_STATE_ERROR; break; } } static void fu_hpi_cfu_device_handler_set_status_report_22(FuHpiCfuDevice *self, guint8 status, gboolean lastpacket) { switch (status) { case FU_HPI_CFU_FIRMWARE_UPDATE_STATUS_ERROR_PREPARE: case FU_HPI_CFU_FIRMWARE_UPDATE_STATUS_ERROR_WRITE: case FU_HPI_CFU_FIRMWARE_UPDATE_STATUS_ERROR_COMPLETE: case FU_HPI_CFU_FIRMWARE_UPDATE_STATUS_ERROR_VERIFY: case FU_HPI_CFU_FIRMWARE_UPDATE_STATUS_ERROR_CRC: case FU_HPI_CFU_FIRMWARE_UPDATE_STATUS_ERROR_SIGNATURE: case FU_HPI_CFU_FIRMWARE_UPDATE_STATUS_ERROR_VERSION: case FU_HPI_CFU_FIRMWARE_UPDATE_STATUS_SWAP_PENDING: case FU_HPI_CFU_FIRMWARE_UPDATE_STATUS_ERROR_INVALID_ADDR: case FU_HPI_CFU_FIRMWARE_UPDATE_STATUS_ERROR_NO_OFFER: case FU_HPI_CFU_FIRMWARE_UPDATE_STATUS_ERROR_INVALID: self->state = FU_HPI_CFU_STATE_ERROR; g_warning("check_update_content: reason:%s", fu_cfu_content_status_to_string(status)); g_debug("check_update_content: %s", fu_hpi_cfu_firmware_update_status_to_string(status)); break; case FU_HPI_CFU_FIRMWARE_UPDATE_STATUS_SUCCESS: g_debug("check_update_content: SUCCESS"); if (lastpacket) { self->state = FU_HPI_CFU_STATE_UPDATE_SUCCESS; } else self->state = FU_HPI_CFU_STATE_UPDATE_CONTENT; break; default: g_warning("check_update_content: status none"); self->state = FU_HPI_CFU_STATE_ERROR; break; } } static gboolean fu_hpi_cfu_device_handler_check_update_content(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { gboolean lastpacket = FALSE; gboolean wait_for_burst_ack = FALSE; guint8 status = 0; guint8 report_id = 0; guint8 reason = 0; if (self->last_packet_sent) { g_debug("check_update_content: last_packet_sent"); if (!fu_hpi_cfu_device_read_content_ack(self, &lastpacket, &report_id, &reason, &status, error)) return FALSE; } else { switch (self->bulk_opt) { case 1: if (self->sequence_number % 16 == 0) { if (!fu_hpi_cfu_device_read_content_ack(self, &lastpacket, &report_id, &reason, &status, error)) return FALSE; } else { self->state = FU_HPI_CFU_STATE_UPDATE_CONTENT; wait_for_burst_ack = TRUE; } break; case 2: if (self->sequence_number % 32 == 0) { if (!fu_hpi_cfu_device_read_content_ack(self, &lastpacket, &report_id, &reason, &status, error)) return FALSE; } else { self->state = FU_HPI_CFU_STATE_UPDATE_CONTENT; wait_for_burst_ack = TRUE; } break; case 3: if ((self->sequence_number) % 64 == 0) { if (!fu_hpi_cfu_device_read_content_ack(self, &lastpacket, &report_id, &reason, &status, error)) return FALSE; } else { self->state = FU_HPI_CFU_STATE_UPDATE_CONTENT; wait_for_burst_ack = TRUE; } break; default: if (!fu_hpi_cfu_device_read_content_ack(self, &lastpacket, &report_id, &reason, &status, error)) return FALSE; } } if (wait_for_burst_ack) return TRUE; if (self->last_packet_sent) { self->state = FU_HPI_CFU_STATE_UPDATE_SUCCESS; } else self->state = FU_HPI_CFU_STATE_UPDATE_CONTENT; if (report_id == 0x25) { g_debug("check_update_content: report_id:0x%x", report_id); fu_hpi_cfu_device_handler_set_status_report_25(self, status, lastpacket); } else if (report_id == 0x22) { g_debug("check_update_content: report_id:0x%x", report_id); fu_hpi_cfu_device_handler_set_status_report_22(self, status, lastpacket); } /* success */ return TRUE; } static gboolean fu_hpi_cfu_device_handler_send_payload_chunk(FuHpiCfuDevice *self, FuChunk *chk, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { g_autoptr(GByteArray) payload_buf = g_byte_array_new(); g_autoptr(GByteArray) untransmitted_data = g_byte_array_new(); g_byte_array_append(payload_buf, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); for (guint32 read_index = 0; read_index < payload_buf->len;) { g_autoptr(GByteArray) payload_header = g_byte_array_new(); g_autoptr(GByteArray) payload_data = g_byte_array_new(); g_autoptr(GByteArray) cfu_data = g_byte_array_new(); gint8 payload_header_length = 0; /* payload header */ if (!fu_hpi_cfu_device_get_payload_header(payload_header, payload_buf, read_index, error)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "to get payload header"); return FALSE; } payload_header_length = payload_header->data[4]; /* payload data */ if (!fu_hpi_cfu_device_get_payload_data(payload_data, payload_buf, payload_header_length, read_index, error)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "to get payload data"); return FALSE; } read_index = read_index + payload_header_length + 5; if (untransmitted_data->data != NULL) { /* handle untransmitted_data */ if (!fu_hpi_cfu_device_send_append_untransmitted(self, payload_header_length, payload_data, untransmitted_data, cfu_data, error)) return FALSE; self->last_packet_sent = (read_index >= payload_buf->len) ? TRUE : FALSE; } else { guint32 remaining_byte_count = 0; guint32 fill_from_position = 0; if (payload_header_length > FU_HPI_CFU_PAYLOAD_LENGTH) { /* cfu_data to be sent to device */ g_byte_array_append(cfu_data, payload_data->data, FU_HPI_CFU_PAYLOAD_LENGTH); if (!fu_hpi_cfu_device_send_payload(self, cfu_data, error)) return FALSE; remaining_byte_count = payload_header_length - FU_HPI_CFU_PAYLOAD_LENGTH; fill_from_position = payload_header_length - remaining_byte_count; /* store the remaining bytes to untransmitted_data */ if (!fu_hpi_cfu_device_untransmitted_data(payload_data, untransmitted_data, payload_header_length, fill_from_position, error)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "to set untransmitted_data"); return FALSE; } } else { self->last_packet_sent = (read_index >= payload_buf->len) ? TRUE : FALSE; /* cfu_data to be sent to device */ g_byte_array_append(cfu_data, payload_data->data, payload_data->len); if (!fu_hpi_cfu_device_send_payload(self, cfu_data, error)) return FALSE; } } if (self->last_packet_sent) { if (untransmitted_data->data != NULL) { g_autoptr(GByteArray) cfu_last_packet = g_byte_array_new(); /* clear and assign the latest untransmitted_data */ fu_byte_array_set_size(cfu_last_packet, untransmitted_data->len, 0x00); if (!fu_memcpy_safe(cfu_last_packet->data, cfu_last_packet->len, 0x0, untransmitted_data->data, untransmitted_data->len, 0x0, untransmitted_data->len, error)) return FALSE; g_debug("sending payload last packet"); if (!fu_hpi_cfu_device_send_payload(self, cfu_last_packet, error)) return FALSE; } } if (!fu_hpi_cfu_device_handler_check_update_content(self, progress, options, error)) return FALSE; if (self->state != FU_HPI_CFU_STATE_UPDATE_CONTENT) break; } return TRUE; } static gboolean fu_hpi_cfu_device_handler_send_payload(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { g_autoptr(GPtrArray) chunks = NULL; chunks = fu_firmware_get_chunks(options->fw_payload, error); if (chunks == NULL) { g_prefix_error(error, "null chunks"); return FALSE; } for (guint32 i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_hpi_cfu_device_handler_send_payload_chunk(self, chk, progress, options, error)) { g_prefix_error(error, "send_payload: "); return FALSE; } } return TRUE; } static gboolean fu_hpi_cfu_device_handler_update_success(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { if (self->last_packet_sent) { self->firmware_status = TRUE; self->state = FU_HPI_CFU_STATE_END_OFFER_LIST; } else { self->state = FU_HPI_CFU_STATE_UPDATE_MORE_OFFERS; } return TRUE; } static gboolean fu_hpi_cfu_device_handler_update_offer_rejected(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { if (self->last_packet_sent) self->state = FU_HPI_CFU_STATE_END_OFFER_LIST; else self->state = FU_HPI_CFU_STATE_UPDATE_OFFER; return TRUE; } static gboolean fu_hpi_cfu_device_handler_update_more_offers(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { if (self->last_packet_sent) self->state = FU_HPI_CFU_STATE_END_OFFER_LIST; else self->state = FU_HPI_CFU_STATE_UPDATE_OFFER; return TRUE; } static gboolean fu_hpi_cfu_device_handler_send_end_offer_list(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { if (!fu_hpi_cfu_device_send_end_offer_list(self, error)) { self->state = FU_HPI_CFU_STATE_ERROR; g_prefix_error(error, "send_end_offer_list: "); return FALSE; } self->state = FU_HPI_CFU_STATE_END_OFFER_LIST_ACCEPTED; return TRUE; } static gboolean fu_hpi_cfu_device_handler_end_offer_list_accepted(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { if (!fu_hpi_cfu_device_end_offer_list_accepted(self, error)) { g_prefix_error(error, "end_offer_list_accept: "); return FALSE; } self->state = FU_HPI_CFU_STATE_VERIFY_CHECK_SWAP_PENDING_BY_SENDING_OFFER_LIST_AGAIN; return TRUE; } static gboolean fu_hpi_cfu_device_handler_update_stop(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { self->exit_state_machine_framework = TRUE; fu_progress_step_done(progress); /* send-payload */ return TRUE; } static gboolean fu_hpi_cfu_device_handler_error(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { self->state = FU_HPI_CFU_STATE_UPDATE_STOP; return TRUE; } static gboolean fu_hpi_cfu_device_handler_notify_on_ready(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { self->state = FU_HPI_CFU_STATE_WAIT_FOR_READY_NOTIFICATION; return TRUE; } static gboolean fu_hpi_cfu_device_handler_wait_for_ready_notification(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { self->state = FU_HPI_CFU_STATE_UPDATE_STOP; return TRUE; } static gboolean fu_hpi_cfu_device_handler_swap_pending_send_offer_list_again(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { if (!fu_hpi_cfu_device_send_start_offer_list(self, error)) { self->state = FU_HPI_CFU_STATE_UPDATE_VERIFY_ERROR; g_prefix_error(error, "swap_pending_send_offer_list_again: "); return FALSE; } self->state = FU_HPI_CFU_STATE_VERIFY_CHECK_SWAP_PENDING_OFFER_LIST_ACCEPTED; return TRUE; } static gboolean fu_hpi_cfu_device_handler_swap_pending_offer_list_accepted(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { gint8 status = 0; if (!fu_hpi_cfu_device_send_offer_list_accepted(self, &status, error)) { self->state = FU_HPI_CFU_STATE_ERROR; g_prefix_error(error, "swap_pending_offer_list_accept: "); return FALSE; } if (status >= 0) self->state = FU_HPI_CFU_STATE_VERIFY_CHECK_SWAP_PENDING_SEND_OFFER_AGAIN; else self->state = FU_HPI_CFU_STATE_UPDATE_VERIFY_ERROR; return TRUE; } static gboolean fu_hpi_cfu_device_handler_swap_pending_send_offer_again(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { if (!fu_hpi_cfu_device_send_offer_update_command(self, options->fw_offer, error)) { self->state = FU_HPI_CFU_STATE_ERROR; g_prefix_error(error, "swap_pending_send_offer_again: "); return FALSE; } self->state = FU_HPI_CFU_STATE_VERIFY_CHECK_SWAP_PENDING_OFFER_ACCEPTED; return TRUE; } static gboolean fu_hpi_cfu_device_handler_swap_pending_send_offer_list_accepted(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { gint8 reply = 0; gint8 reason = 0; /* reply status must be SWAP_PENDING */ if (!fu_hpi_cfu_device_firmware_update_offer_accepted(self, &reply, &reason, error)) { g_prefix_error(error, "swap_pending_send_offer_accept: "); return FALSE; } if (reply == FU_HPI_CFU_FIRMWARE_UPDATE_OFFER_ACCEPT) { g_debug("swap_pending_send_offer_list_accepted: " "expected a reject with SWAP PENDING"); self->state = FU_HPI_CFU_STATE_VERIFY_CHECK_SWAP_PENDING_SEND_UPDATE_END_OFFER_LIST; } else { if (fu_hpi_cfu_device_firmware_update_offer_rejected(reply)) { g_debug("swap_pending_send_offer_list_accepted: " "reply: %d,OFFER_REJECTED: Reason:'%s'", reply, fu_cfu_rr_code_to_string(reason)); switch (reason) { case FU_HPI_CFU_FIRMWARE_OFFER_REJECT_OLD_FW: case FU_HPI_CFU_FIRMWARE_OFFER_REJECT_INV_COMPONENT: case FU_HPI_CFU_FIRMWARE_OFFER_REJECT_SWAP_PENDING: case FU_HPI_CFU_FIRMWARE_OFFER_REJECT_MISMATCH: case FU_HPI_CFU_FIRMWARE_OFFER_REJECT_BANK: case FU_HPI_CFU_FIRMWARE_OFFER_REJECT_PLATFORM: case FU_HPI_CFU_FIRMWARE_OFFER_REJECT_MILESTONE: case FU_HPI_CFU_FIRMWARE_OFFER_REJECT_INV_PCOL_REV: case FU_HPI_CFU_FIRMWARE_OFFER_REJECT_VARIANT: g_debug("reason: %s", fu_hpi_cfu_firmware_offer_reject_to_string(reason)); break; default: g_debug("swap_pending_send_offer_list_accepted " "expected a reject with SWAP PENDING"); break; } } /* rejected */ self->state = FU_HPI_CFU_STATE_VERIFY_CHECK_SWAP_PENDING_SEND_UPDATE_END_OFFER_LIST; } return TRUE; } static gboolean fu_hpi_cfu_device_handler_swap_pending_send_end_offer_list(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { if (!fu_hpi_cfu_device_send_end_offer_list(self, error)) { g_prefix_error(error, "swap_pending_send_end_offer_list: "); return FALSE; } self->state = FU_HPI_CFU_STATE_VERIFY_CHECK_SWAP_PENDING_UPDATE_END_OFFER_LIST_ACCEPTED; fu_progress_step_done(progress); /* send-payload */ return TRUE; } static gboolean fu_hpi_cfu_device_handler_swap_pending_end_offer_list_accepted(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { if (!fu_hpi_cfu_device_end_offer_list_accepted(self, error)) { g_prefix_error(error, "swap_pending_end_offer_list_accept: "); return FALSE; } self->state = FU_HPI_CFU_STATE_UPDATE_STOP; return TRUE; } static gboolean fu_hpi_cfu_device_handler_verify_error(FuHpiCfuDevice *self, FuProgress *progress, FuHpiCfuHandlerOptions *options, GError **error) { self->state = FU_HPI_CFU_STATE_UPDATE_STOP; return TRUE; } FuHpiCfuStateMachineFramework hpi_cfu_states[] = { {FU_HPI_CFU_STATE_START_ENTIRE_TRANSACTION, fu_hpi_cfu_device_handler_start_entire_transaction, NULL}, {FU_HPI_CFU_STATE_START_ENTIRE_TRANSACTION_ACCEPTED, fu_hpi_cfu_device_handler_start_entire_transaction_accepted, NULL}, {FU_HPI_CFU_STATE_START_OFFER_LIST, fu_hpi_cfu_device_handler_send_start_offer_list, NULL}, {FU_HPI_CFU_STATE_START_OFFER_LIST_ACCEPTED, fu_hpi_cfu_device_handler_send_start_offer_list_accepted, NULL}, {FU_HPI_CFU_STATE_UPDATE_OFFER, fu_hpi_cfu_device_handler_send_offer_update_command, &handler_options}, {FU_HPI_CFU_STATE_UPDATE_OFFER_ACCEPTED, fu_hpi_cfu_device_handler_send_offer_accepted, NULL}, {FU_HPI_CFU_STATE_UPDATE_CONTENT, fu_hpi_cfu_device_handler_send_payload, &handler_options}, {FU_HPI_CFU_STATE_UPDATE_SUCCESS, fu_hpi_cfu_device_handler_update_success, NULL}, {FU_HPI_CFU_STATE_UPDATE_OFFER_REJECTED, fu_hpi_cfu_device_handler_update_offer_rejected, NULL}, {FU_HPI_CFU_STATE_UPDATE_MORE_OFFERS, fu_hpi_cfu_device_handler_update_more_offers, NULL}, {FU_HPI_CFU_STATE_END_OFFER_LIST, fu_hpi_cfu_device_handler_send_end_offer_list, NULL}, {FU_HPI_CFU_STATE_END_OFFER_LIST_ACCEPTED, fu_hpi_cfu_device_handler_end_offer_list_accepted, NULL}, {FU_HPI_CFU_STATE_UPDATE_STOP, fu_hpi_cfu_device_handler_update_stop, NULL}, {FU_HPI_CFU_STATE_ERROR, fu_hpi_cfu_device_handler_error, NULL}, {FU_HPI_CFU_STATE_CHECK_UPDATE_CONTENT, fu_hpi_cfu_device_handler_check_update_content, NULL}, {FU_HPI_CFU_STATE_NOTIFY_ON_READY, fu_hpi_cfu_device_handler_notify_on_ready, NULL}, {FU_HPI_CFU_STATE_WAIT_FOR_READY_NOTIFICATION, fu_hpi_cfu_device_handler_wait_for_ready_notification, NULL}, {FU_HPI_CFU_STATE_VERIFY_CHECK_SWAP_PENDING_BY_SENDING_OFFER_LIST_AGAIN, fu_hpi_cfu_device_handler_swap_pending_send_offer_list_again, NULL}, {FU_HPI_CFU_STATE_VERIFY_CHECK_SWAP_PENDING_OFFER_LIST_ACCEPTED, fu_hpi_cfu_device_handler_swap_pending_offer_list_accepted, NULL}, {FU_HPI_CFU_STATE_VERIFY_CHECK_SWAP_PENDING_SEND_OFFER_AGAIN, fu_hpi_cfu_device_handler_swap_pending_send_offer_again, &handler_options}, {FU_HPI_CFU_STATE_VERIFY_CHECK_SWAP_PENDING_OFFER_ACCEPTED, fu_hpi_cfu_device_handler_swap_pending_send_offer_list_accepted, NULL}, {FU_HPI_CFU_STATE_VERIFY_CHECK_SWAP_PENDING_SEND_UPDATE_END_OFFER_LIST, fu_hpi_cfu_device_handler_swap_pending_send_end_offer_list, NULL}, {FU_HPI_CFU_STATE_VERIFY_CHECK_SWAP_PENDING_UPDATE_END_OFFER_LIST_ACCEPTED, fu_hpi_cfu_device_handler_swap_pending_end_offer_list_accepted, NULL}, {FU_HPI_CFU_STATE_UPDATE_VERIFY_ERROR, fu_hpi_cfu_device_handler_verify_error, NULL}, }; static gboolean fu_hpi_cfu_device_setup(FuDevice *device, GError **error) { guint32 version_raw; gint8 version_table_offset = 4; gint8 component_id_offset = 5; gint8 component_data_size = 8; gint8 component_index = 0; /* multiple offers logic is in progress */ gint8 bulk_opt_index = 0; gsize actual_length = 0; guint8 buf[60] = {0}; FuHpiCfuDevice *self = FU_HPI_CFU_DEVICE(device); /* FuHidDevice->setup */ if (!FU_DEVICE_CLASS(fu_hpi_cfu_device_parent_class)->setup(device, error)) return FALSE; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, GET_REPORT, FEATURE_REPORT_TYPE | FIRMWARE_REPORT_ID, FU_HPI_CFU_INTERFACE, buf, sizeof(buf), &actual_length, FU_HPI_CFU_DEVICE_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to do device setup"); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "VersionResponse", buf, actual_length); if (!fu_memread_uint32_safe(buf, sizeof(buf), 5, &version_raw, G_LITTLE_ENDIAN, error)) return FALSE; fu_device_set_version_raw(device, version_raw); bulk_opt_index = version_table_offset + component_index * component_data_size + component_id_offset; /* Get Bulk optimization value */ if (!fu_memcpy_safe((guint8 *)&self->bulk_opt, sizeof(self->bulk_opt), 0x0, (guint8 *)buf + bulk_opt_index, 1, 0x0, 1, error)) return FALSE; g_debug("bulk_opt: %d", self->bulk_opt); /* success */ return TRUE; } static void fu_hpi_cfu_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 4, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 5, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 86, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 5, "reload"); } static gboolean fu_hpi_cfu_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuHpiCfuDevice *self = FU_HPI_CFU_DEVICE(device); gsize payload_file_size = 0; g_autoptr(FuFirmware) fw_offer = NULL; g_autoptr(FuFirmware) fw_payload = NULL; g_autoptr(GBytes) blob_payload = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "start-entire"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "start-offer"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "send-offer"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 92, "send-payload"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 8, "restart"); /* get both images */ fw_offer = fu_archive_firmware_get_image_fnmatch(FU_ARCHIVE_FIRMWARE(firmware), "*.offer.bin", error); if (fw_offer == NULL) return FALSE; fw_payload = fu_archive_firmware_get_image_fnmatch(FU_ARCHIVE_FIRMWARE(firmware), "*.payload.bin", error); if (fw_payload == NULL) return FALSE; self->state = FU_HPI_CFU_STATE_START_ENTIRE_TRANSACTION; blob_payload = fu_firmware_get_bytes(fw_payload, NULL); g_bytes_get_data(blob_payload, &payload_file_size); self->payload_file_size = payload_file_size; handler_options.fw_offer = fw_offer; handler_options.fw_payload = fw_payload; /* cfu state machine framework */ while (!self->exit_state_machine_framework) { g_debug("hpi-cfu-state: %s", fu_hpi_cfu_state_to_string(self->state)); if (!hpi_cfu_states[self->state].handler(self, progress, hpi_cfu_states[self->state].options, error)) { g_prefix_error(error, "state: "); return FALSE; } } /* the device automatically reboots */ if (self->firmware_status) fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gchar * fu_hpi_cfu_device_convert_version(FuDevice *device, guint64 version_raw) { return g_strdup_printf("%02x.%02x.%02x.%02x", (guint)(version_raw >> 24) & 0xff, (guint)(version_raw >> 16) & 0xff, (guint)(version_raw >> 8) & 0xff, (guint)version_raw & 0xff); } static void fu_hpi_cfu_device_init(FuHpiCfuDevice *self) { self->state = FU_HPI_CFU_STATE_START_ENTIRE_TRANSACTION; fu_device_add_protocol(FU_DEVICE(self), "com.microsoft.cfu"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_ARCHIVE_FIRMWARE); fu_device_set_install_duration(FU_DEVICE(self), 720); /* 12 min */ fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_INSTANCE_ID_REV); fu_usb_device_add_interface(FU_USB_DEVICE(self), FU_HPI_CFU_INTERFACE); /* reboot takes down the entire hub for ~12 minutes */ fu_device_set_remove_delay(FU_DEVICE(self), 720 * 1000); } static void fu_hpi_cfu_device_class_init(FuHpiCfuDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_hpi_cfu_device_write_firmware; device_class->setup = fu_hpi_cfu_device_setup; device_class->set_progress = fu_hpi_cfu_device_set_progress; device_class->convert_version = fu_hpi_cfu_device_convert_version; } fwupd-2.0.10/plugins/hpi-cfu/fu-hpi-cfu-device.h000066400000000000000000000004471501337203100213040ustar00rootroot00000000000000/* * Copyright 2024 HP Development Company, L.P. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_HPI_CFU_DEVICE (fu_hpi_cfu_device_get_type()) G_DECLARE_FINAL_TYPE(FuHpiCfuDevice, fu_hpi_cfu_device, FU, HPI_CFU_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/hpi-cfu/fu-hpi-cfu-plugin.c000066400000000000000000000013161501337203100213320ustar00rootroot00000000000000/* * Copyright 2024 HP Development Company, L.P. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-hpi-cfu-device.h" #include "fu-hpi-cfu-plugin.h" struct _FuHpiCfuPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuHpiCfuPlugin, fu_hpi_cfu_plugin, FU_TYPE_PLUGIN) static void fu_hpi_cfu_plugin_init(FuHpiCfuPlugin *self) { } static void fu_hpi_cfu_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_HPI_CFU_DEVICE); } static void fu_hpi_cfu_plugin_class_init(FuHpiCfuPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_hpi_cfu_plugin_constructed; } fwupd-2.0.10/plugins/hpi-cfu/fu-hpi-cfu-plugin.h000066400000000000000000000003461501337203100213410ustar00rootroot00000000000000/* * Copyright 2024 HP Development Company, L.P. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuHpiCfuPlugin, fu_hpi_cfu_plugin, FU, HPI_CFU_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/hpi-cfu/fu-hpi-cfu.rs000066400000000000000000000047151501337203100202460ustar00rootroot00000000000000/* * Copyright 2024 HP Development Company, L.P. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #[derive(ToString)] #[repr(u8)] enum FuHpiCfuState { StartEntireTransaction = 0x00, StartEntireTransactionAccepted = 0x01, StartOfferList = 0x02, StartOfferListAccepted = 0x03, UpdateOffer = 0x04, UpdateOfferAccepted = 0x05, UpdateContent = 0x06, UpdateSuccess = 0x07, UpdateOfferRejected = 0x08, UpdateMoreOffers = 0x09, EndOfferList = 0x0A, EndOfferListAccepted = 0x0B, UpdateStop = 0x0C, Error = 0x0D, CheckUpdateContent = 0x0E, NotifyOnReady = 0x0F, WaitForReadyNotification = 0x10, VerifyCheckSwapPendingBySendingOfferListAgain = 0x11, VerifyCheckSwapPendingOfferListAccepted = 0x12, VerifyCheckSwapPendingSendOfferAgain = 0x13, VerifyCheckSwapPendingOfferAccepted = 0x14, VerifyCheckSwapPendingSendUpdateEndOfferList = 0x15, VerifyCheckSwapPendingUpdateEndOfferListAccepted = 0x16, UpdateVerifyError = 0x17, } #[derive(ToString)] #[repr(u8)] enum FuHpiCfuFirmwareOfferReject { OldFw = 0x00, InvComponent = 0x01, SwapPending = 0x02, Mismatch = 0x03, Bank = 0x04, Platform = 0x05, Milestone = 0x06, InvPcolRev = 0x07, Variant = 0x08, } #[derive(ToString)] #[repr(u8)] enum FuHpiCfuFirmwareUpdateOffer { Skip = 0x00, Accept = 0x01, Reject = 0x02, Busy = 0x03, CommandReady = 0x04, CmdNotSupported = 0xFF, } #[derive(ToString)] #[repr(u8)] enum FuHpiCfuFirmwareUpdateStatus { Success = 0x00, ErrorPrepare = 0x01, ErrorWrite = 0x02, ErrorComplete = 0x03, ErrorVerify = 0x04, ErrorCrc = 0x05, ErrorSignature = 0x06, ErrorVersion = 0x07, SwapPending = 0x08, ErrorInvalidAddr = 0x09, ErrorNoOffer = 0x0A, ErrorInvalid = 0x0B, } #[derive(New, Getters)] #[repr(C, packed)] struct FuStructHpiCfuOfferCmd { report_id: u8, segment_number: u8, flags: u8, component_id: u8, token: u8, variant: u8, minor_version: u16le, major_version: u8, vendor_specific: u32le, protocol_version: u8, _reserved0: u8, product_specific: u16le, } #[derive(New, Getters)] #[repr(C, packed)] struct FuStructHpiCfuPayloadCmd { report_id: u8, flags: u8, length: u8, seq_number: u16le, address: u32le, data: [u8; 52], } #[derive(New, Getters)] #[repr(C, packed)] struct FuStructHpiCfuBuf { report_id: u8, command: u8, report_data: [u8; 15], } fwupd-2.0.10/plugins/hpi-cfu/hpi-cfu.quirk000066400000000000000000000002261501337203100203360ustar00rootroot00000000000000# HP USB 4 100W G6 Dock [USB\VID_03F0&PID_0BAF] Plugin = hpi_cfu # HP Thunderbolt 4 Ultra 180W/280W G6 Dock [USB\VID_03F0&PID_03B7] Plugin = hpi_cfu fwupd-2.0.10/plugins/hpi-cfu/meson.build000066400000000000000000000010471501337203100200720ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginHpiCfu"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('hpi-cfu.quirk') plugin_builtins += static_library('fu_plugin_hpi_cfu', rustgen.process('fu-hpi-cfu.rs'), sources: [ 'fu-hpi-cfu-device.c', 'fu-hpi-cfu-plugin.c', ], include_directories: [ plugin_incdirs, plugincfu_incdir, ], link_with: [ plugin_libs, plugin_builtin_cfu, ], c_args: cargs, dependencies: plugin_deps, ) device_tests += files( 'tests/hp-dock-g6-ultra.json', ) fwupd-2.0.10/plugins/hpi-cfu/tests/000077500000000000000000000000001501337203100170705ustar00rootroot00000000000000fwupd-2.0.10/plugins/hpi-cfu/tests/hp-dock-g6-ultra.json000066400000000000000000000016471501337203100227570ustar00rootroot00000000000000{ "name": "HP TBT4 Ultra 180W/280W G6 Dock", "interactive": false, "steps": [ { "url": "30adee4fdd507f00bce9c884718321cbe95b5fb2fca93f330d32b49d9ed10a87-HPTBT4UltraG6_HDX_00.21.22.00.cab", "emulation-url": "140f40be8950b95dd2b1132c432f0b762272e1176816f40fd6edf02a92b587e2-HPTBT4UltraG6_HDX_00.21.22.00.zip", "components": [ { "version": "00.21.22.00", "guids": [ "539fad43-dd80-56cd-b684-dfc3f9ce2546" ] } ] }, { "url": "2eaf001e5b8b44ee363dc9d1e58a3608b3adcf1fe3e6ffb5a40cfcebb90253f9-HPTBT4UltraG6_HDX_00.21.28.00.cab", "emulation-url": "bc92e78d8bc95ad69ce8d7a41bfff41ae23d0153234b7fd1dd9e8c069dd2fbd1-HPTBT4UltraG6_HDX_00.21.28.00.zip", "components": [ { "version": "00.21.28.00", "guids": [ "539fad43-dd80-56cd-b684-dfc3f9ce2546" ] } ] } ] } fwupd-2.0.10/plugins/huddly-usb/000077500000000000000000000000001501337203100164535ustar00rootroot00000000000000fwupd-2.0.10/plugins/huddly-usb/README.md000066400000000000000000000022601501337203100177320ustar00rootroot00000000000000--- title: Plugin: Huddly Usb --- ## Introduction This plugin supports performing firmware upgrades on Huddly L1 and S1 video conferencing cameras connected via the Huddly USB adapter. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a packed binary file format. This plugin supports the following protocol ID: * `com.huddly.usb` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_2BD9&PID_A032` ## Update Behavior * The firmware is deployed when the device in in normal runtime mode. * The device will reboot and re-enumerate after the firmware has been written. * The firmware file is deployed again after the reboot for verification. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x2BD9` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `2.0.0`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Huddly: @LarsStensen fwupd-2.0.10/plugins/huddly-usb/fu-huddly-usb-common.c000066400000000000000000000063601501337203100226020ustar00rootroot00000000000000/* * Copyright 2024 Huddly * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-huddly-usb-common.h" void fu_huddly_usb_hlink_msg_free(FuHuddlyUsbHLinkMsg *msg) { g_free(msg->msg_name); if (msg->header != NULL) g_byte_array_unref(msg->header); if (msg->payload != NULL) g_byte_array_unref(msg->payload); g_free(msg); } FuHuddlyUsbHLinkMsg * fu_huddly_usb_hlink_msg_new(const gchar *msg_name, GByteArray *payload) { g_autoptr(FuHuddlyUsbHLinkMsg) msg = g_new0(FuHuddlyUsbHLinkMsg, 1); g_return_val_if_fail(msg_name != NULL, NULL); msg->header = fu_struct_h_link_header_new(); msg->msg_name = g_strdup(msg_name); fu_struct_h_link_header_set_msg_name_size(msg->header, strlen(msg_name)); if (payload != NULL) { fu_struct_h_link_header_set_payload_size(msg->header, payload->len); msg->payload = g_byte_array_ref(payload); } return g_steal_pointer(&msg); } FuHuddlyUsbHLinkMsg * fu_huddly_usb_hlink_msg_new_string(const gchar *msg_name, const gchar *payload) { g_autoptr(GByteArray) payload_buf = g_byte_array_new(); g_return_val_if_fail(msg_name != NULL, NULL); g_return_val_if_fail(payload != NULL, NULL); g_byte_array_append(payload_buf, (const guint8 *)payload, strlen(payload)); return fu_huddly_usb_hlink_msg_new(msg_name, payload_buf); } GByteArray * fu_huddly_usb_hlink_msg_write(FuHuddlyUsbHLinkMsg *msg, GError **error) { g_autoptr(GByteArray) packet = g_byte_array_new(); g_return_val_if_fail(msg != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); g_byte_array_append(packet, msg->header->data, msg->header->len); g_byte_array_append(packet, (const guint8 *)msg->msg_name, strlen(msg->msg_name)); if (msg->payload != NULL) g_byte_array_append(packet, msg->payload->data, msg->payload->len); return g_steal_pointer(&packet); } FuHuddlyUsbHLinkMsg * fu_huddly_usb_hlink_msg_parse(const guint8 *buf, gsize bufsz, GError **error) { gsize offset = 0; guint16 msg_name_size; guint32 payload_size; g_autoptr(FuHuddlyUsbHLinkMsg) msg = g_new0(FuHuddlyUsbHLinkMsg, 1); g_return_val_if_fail(buf != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); msg->header = fu_struct_h_link_header_parse(buf, bufsz, 0x0, error); if (msg->header == NULL) return NULL; offset += FU_STRUCT_H_LINK_HEADER_SIZE; msg_name_size = fu_struct_h_link_header_get_msg_name_size(msg->header); if (msg_name_size == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "HLink message had no message name"); return NULL; } msg->msg_name = g_new0(gchar, msg_name_size + 1); if (!fu_memcpy_safe((guint8 *)msg->msg_name, msg_name_size, 0, /* dst */ buf, bufsz, offset, /* src */ msg_name_size, error)) { return NULL; } offset += msg_name_size; payload_size = fu_struct_h_link_header_get_payload_size(msg->header); msg->payload = g_byte_array_sized_new(payload_size); fu_byte_array_set_size(msg->payload, payload_size, 0x0); if (!fu_memcpy_safe(msg->payload->data, msg->payload->len, 0, /* dst */ buf, bufsz, offset, /* src */ payload_size, error)) { return NULL; } /* success */ return g_steal_pointer(&msg); } fwupd-2.0.10/plugins/huddly-usb/fu-huddly-usb-common.h000066400000000000000000000014211501337203100226000ustar00rootroot00000000000000/* * Copyright 2024 Huddly * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-huddly-usb-struct.h" typedef struct { FuStructHLinkHeader *header; gchar *msg_name; GByteArray *payload; /* nullable */ } FuHuddlyUsbHLinkMsg; FuHuddlyUsbHLinkMsg * fu_huddly_usb_hlink_msg_new(const gchar *msg_name, GByteArray *payload); FuHuddlyUsbHLinkMsg * fu_huddly_usb_hlink_msg_new_string(const gchar *msg_name, const gchar *payload); GByteArray * fu_huddly_usb_hlink_msg_write(FuHuddlyUsbHLinkMsg *msg, GError **error); FuHuddlyUsbHLinkMsg * fu_huddly_usb_hlink_msg_parse(const guint8 *buf, gsize bufsz, GError **error); void fu_huddly_usb_hlink_msg_free(FuHuddlyUsbHLinkMsg *msg); G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuHuddlyUsbHLinkMsg, fu_huddly_usb_hlink_msg_free) fwupd-2.0.10/plugins/huddly-usb/fu-huddly-usb-device.c000066400000000000000000000477361501337203100225650ustar00rootroot00000000000000/* * Copyright 2024 Huddly * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-huddly-usb-common.h" #include "fu-huddly-usb-device.h" #include "fu-huddly-usb-struct.h" enum { EP_OUT, EP_IN, EP_LAST }; #define HUDDLY_USB_RECEIVE_BUFFER_SIZE 1024 #if !GLIB_CHECK_VERSION(2, 74, 0) #define G_REGEX_DEFAULT 0 #define G_REGEX_MATCH_DEFAULT 0 #endif struct _FuHuddlyUsbDevice { FuUsbDevice parent_instance; guint bulk_ep[EP_LAST]; gboolean interfaces_claimed; gboolean pending_verify; GInputStream *input_stream; gchar *product_state; gboolean need_reboot; }; G_DEFINE_TYPE(FuHuddlyUsbDevice, fu_huddly_usb_device, FU_TYPE_USB_DEVICE) static void fu_huddly_usb_device_set_state(FuHuddlyUsbDevice *self, const gchar *product_state) { g_free(self->product_state); self->product_state = g_strdup(product_state); } static gboolean fu_huddly_usb_device_find_interface(FuHuddlyUsbDevice *self, GError **error) { g_autoptr(GPtrArray) intfs = NULL; intfs = fu_usb_device_get_interfaces(FU_USB_DEVICE(self), error); if (intfs == NULL) { g_prefix_error(error, "could not find interface"); return FALSE; } for (guint i = 0; i < intfs->len; i++) { FuUsbInterface *intf = g_ptr_array_index(intfs, i); if (fu_usb_interface_get_class(intf) == FU_USB_CLASS_VENDOR_SPECIFIC) { g_autoptr(GPtrArray) endpoints = fu_usb_interface_get_endpoints(intf); for (guint j = 0; j < endpoints->len; j++) { FuUsbEndpoint *ep = g_ptr_array_index(endpoints, j); if (fu_usb_endpoint_get_direction(ep) == FU_USB_DIRECTION_HOST_TO_DEVICE) { self->bulk_ep[EP_OUT] = fu_usb_endpoint_get_address(ep); } else { self->bulk_ep[EP_IN] = fu_usb_endpoint_get_address(ep); } } } } if (self->bulk_ep[EP_OUT] == 0 || self->bulk_ep[EP_IN] == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "could not find usb endpoints"); return FALSE; } return TRUE; } static gboolean fu_huddly_usb_device_bulk_write(FuHuddlyUsbDevice *self, GByteArray *src, FuProgress *progress, GError **error) { gsize offset = 0; const gsize max_chunk_size = 16 * 1024; if (progress != NULL) fu_progress_set_id(progress, G_STRLOC); do { gsize transmitted = 0; gsize remaining = src->len - offset; gsize chunk_size = (remaining > max_chunk_size) ? max_chunk_size : remaining; if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), self->bulk_ep[EP_OUT], src->data + offset, chunk_size, &transmitted, 2000, NULL, error)) { return FALSE; } offset += transmitted; if (progress != NULL) fu_progress_set_percentage_full(progress, offset, src->len); } while (offset < src->len); return TRUE; } static gboolean fu_huddly_usb_device_bulk_read(FuHuddlyUsbDevice *self, GByteArray *buf, gsize *received_length, GError **error) { return fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), self->bulk_ep[EP_IN], buf->data, buf->len, received_length, 20000, NULL, error); } static gboolean fu_huddly_usb_device_hlink_send(FuHuddlyUsbDevice *self, FuHuddlyUsbHLinkMsg *msg, GError **error) { g_autoptr(GByteArray) buf = NULL; buf = fu_huddly_usb_hlink_msg_write(msg, error); if (buf == NULL) return FALSE; return fu_huddly_usb_device_bulk_write(self, buf, NULL, error); } static FuHuddlyUsbHLinkMsg * fu_huddly_usb_device_hlink_receive(FuHuddlyUsbDevice *self, GError **error) { gsize received_length = 0; g_autoptr(GByteArray) msg_res = g_byte_array_new(); g_autoptr(FuHuddlyUsbHLinkMsg) msg = NULL; fu_byte_array_set_size(msg_res, HUDDLY_USB_RECEIVE_BUFFER_SIZE, 0u); if (!fu_huddly_usb_device_bulk_read(self, msg_res, &received_length, error)) { g_prefix_error(error, "HLink receive failed: "); return NULL; } msg = fu_huddly_usb_hlink_msg_parse(msg_res->data, received_length, error); if (msg == NULL) { g_prefix_error(error, "HLink receive failed: "); return NULL; } return g_steal_pointer(&msg); } static gboolean fu_huddly_usb_device_hlink_subscribe(FuHuddlyUsbDevice *self, const gchar *subscription, GError **error) { g_autoptr(FuHuddlyUsbHLinkMsg) msg = fu_huddly_usb_hlink_msg_new_string("hlink-mb-subscribe", subscription); g_debug("subscribe %s", subscription); return fu_huddly_usb_device_hlink_send(self, msg, error); } static gboolean fu_huddly_usb_device_hlink_unsubscribe(FuHuddlyUsbDevice *self, const gchar *subscription, GError **error) { g_autoptr(FuHuddlyUsbHLinkMsg) msg = fu_huddly_usb_hlink_msg_new_string("hlink-mb-unsubscribe", subscription); g_debug("unsubscribe %s", subscription); return fu_huddly_usb_device_hlink_send(self, msg, error); } /* send an empty packet to reset hlink communications */ static gboolean fu_huddly_usb_device_send_reset(FuHuddlyUsbDevice *self, GError **error) { g_autoptr(GByteArray) packet = g_byte_array_new(); if (!fu_huddly_usb_device_bulk_write(self, packet, NULL, error)) { g_prefix_error(error, "reset device failed: "); return FALSE; } return TRUE; } /* send a hlink salute and receive a response from the device */ static gboolean fu_huddly_usb_device_salute(FuHuddlyUsbDevice *self, GError **error) { gsize received_length = 0; g_autoptr(GByteArray) salutation = g_byte_array_new(); g_autoptr(GByteArray) response = g_byte_array_new(); g_autofree gchar *str = NULL; g_debug("send salute..."); fu_byte_array_append_uint8(salutation, 0x00); if (!fu_huddly_usb_device_bulk_write(self, salutation, NULL, error)) { g_prefix_error(error, "send salute send message failed: "); return FALSE; } fu_byte_array_set_size(response, 100, 0x0); if (!fu_huddly_usb_device_bulk_read(self, response, &received_length, error)) { g_prefix_error(error, "send salute read response failed: "); return FALSE; } str = fu_strsafe((const gchar *)response->data, received_length); g_debug("received response %s", str); return TRUE; } static gboolean fu_huddly_usb_device_ensure_product_info(FuHuddlyUsbDevice *self, GError **error) { g_auto(GStrv) version_split = NULL; g_autoptr(FuHuddlyUsbHLinkMsg) msg_req = NULL; g_autoptr(FuHuddlyUsbHLinkMsg) msg_res = NULL; g_autoptr(FuMsgpackItem) item_state = NULL; g_autoptr(FuMsgpackItem) item_version = NULL; g_autoptr(GPtrArray) items = NULL; if (!fu_huddly_usb_device_hlink_subscribe(self, "prodinfo/get_msgpack_reply", error)) { g_prefix_error(error, "failed to read product info: "); return FALSE; } msg_req = fu_huddly_usb_hlink_msg_new("prodinfo/get_msgpack", NULL); if (!fu_huddly_usb_device_hlink_send(self, msg_req, error)) { g_prefix_error(error, "failed to read product info: "); return FALSE; } msg_res = fu_huddly_usb_device_hlink_receive(self, error); if (msg_res == NULL) { g_prefix_error(error, "failed to read product info: "); return FALSE; } g_debug("receive data %s", msg_res->msg_name); items = fu_msgpack_parse(msg_res->payload, error); if (items == NULL) return FALSE; /* version */ item_version = fu_msgpack_map_lookup(items, 0, "app_version", error); if (item_version == NULL) { g_prefix_error(error, "failed to read product info: "); return FALSE; } version_split = g_regex_split_simple("[-+]", fu_msgpack_item_get_string(item_version)->str, G_REGEX_DEFAULT, G_REGEX_MATCH_DEFAULT); fu_device_set_version(FU_DEVICE(self), version_split[0]); /* state */ item_state = fu_msgpack_map_lookup(items, 0, "state", error); if (item_state == NULL) { g_prefix_error(error, "failed to read product info: "); return FALSE; } fu_huddly_usb_device_set_state(self, fu_msgpack_item_get_string(item_state)->str); return TRUE; } static gboolean fu_huddly_usb_device_reboot(FuHuddlyUsbDevice *self, GError **error) { g_autoptr(FuHuddlyUsbHLinkMsg) msg = fu_huddly_usb_hlink_msg_new("camctrl/reboot", NULL); return fu_huddly_usb_device_hlink_send(self, msg, error); } static gboolean fu_huddly_usb_device_hcp_write_file(FuHuddlyUsbDevice *self, const gchar *filename, GInputStream *stream, FuProgress *progress, GError **error) { g_autoptr(FuHuddlyUsbHLinkMsg) msg_req = NULL; g_autoptr(FuHuddlyUsbHLinkMsg) msg_res = NULL; g_autoptr(GByteArray) buf = NULL; g_autoptr(GByteArray) payload_msgpack = NULL; g_autoptr(GPtrArray) rcv_items = NULL; g_autoptr(GPtrArray) msgpack_items = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_autoptr(FuMsgpackItem) item_status = NULL; guint8 status_code; g_ptr_array_add(msgpack_items, fu_msgpack_item_new_map(2)); g_ptr_array_add(msgpack_items, fu_msgpack_item_new_string("name")); g_ptr_array_add(msgpack_items, fu_msgpack_item_new_string(filename)); g_ptr_array_add(msgpack_items, fu_msgpack_item_new_string("file_data")); g_ptr_array_add(msgpack_items, fu_msgpack_item_new_binary_stream(stream)); payload_msgpack = fu_msgpack_write(msgpack_items, error); msg_req = fu_huddly_usb_hlink_msg_new("hcp/write", payload_msgpack); buf = fu_huddly_usb_hlink_msg_write(msg_req, error); if (buf == NULL) return FALSE; if (!fu_huddly_usb_device_hlink_subscribe(self, "hcp/write_reply", error)) return FALSE; if (!fu_huddly_usb_device_bulk_write(self, buf, progress, error)) return FALSE; /* read reply and check status */ msg_res = fu_huddly_usb_device_hlink_receive(self, error); if (msg_res == NULL) return FALSE; rcv_items = fu_msgpack_parse(msg_res->payload, error); if (rcv_items == NULL) return FALSE; item_status = fu_msgpack_map_lookup(rcv_items, 0, "status", error); if (item_status == NULL) return FALSE; status_code = fu_msgpack_item_get_integer(item_status); if (status_code != 0) { g_autoptr(FuMsgpackItem) item_errstr = NULL; item_errstr = fu_msgpack_map_lookup(rcv_items, 0, "string", NULL); if (item_errstr != NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to write file to target: %s (%u)", fu_msgpack_item_get_string(item_errstr)->str, status_code); return FALSE; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to write file to target: %u", status_code); return FALSE; } return fu_huddly_usb_device_hlink_unsubscribe(self, "hcp/write_reply", error); } static gboolean fu_huddly_usb_device_hpk_done_cb(FuDevice *device, gpointer user_data, GError **error) { FuHuddlyUsbDevice *self = FU_HUDDLY_USB_DEVICE(device); GString *operation; guint8 err; g_autoptr(FuHuddlyUsbHLinkMsg) msg_res = NULL; g_autoptr(FuMsgpackItem) item_operation = NULL; g_autoptr(FuMsgpackItem) item_error = NULL; g_autoptr(FuMsgpackItem) item_reboot = NULL; g_autoptr(GPtrArray) items = NULL; msg_res = fu_huddly_usb_device_hlink_receive(self, error); if (msg_res == NULL) return FALSE; items = fu_msgpack_parse(msg_res->payload, error); if (items == NULL) return FALSE; item_operation = fu_msgpack_map_lookup(items, 0, "operation", error); if (item_operation == NULL) return FALSE; operation = fu_msgpack_item_get_string(item_operation); g_debug("operation %s", operation->str); /* get error */ item_error = fu_msgpack_map_lookup(items, 0, "error", error); if (item_error == NULL) return FALSE; err = fu_msgpack_item_get_integer(item_error); if (err != 0) { g_prefix_error(error, "received error %s", operation->str); return FALSE; } item_reboot = fu_msgpack_map_lookup(items, 0, "reboot", error); if (item_reboot == NULL) return FALSE; self->need_reboot = fu_msgpack_item_get_boolean(item_reboot); /* are we done? */ if (g_strcmp0(operation->str, "done") != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "operation was %s", operation->str); return FALSE; } /* success */ return TRUE; } static gboolean fu_huddly_usb_device_hpk_run(FuHuddlyUsbDevice *self, const gchar *filename, GError **error) { g_autoptr(GByteArray) pack_buffer = NULL; g_autoptr(GPtrArray) items = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_autoptr(FuHuddlyUsbHLinkMsg) msg = NULL; g_ptr_array_add(items, fu_msgpack_item_new_map(1)); g_ptr_array_add(items, fu_msgpack_item_new_string("filename")); g_ptr_array_add(items, fu_msgpack_item_new_string(filename)); if (!fu_huddly_usb_device_hlink_subscribe(self, "upgrader/status", error)) return FALSE; pack_buffer = fu_msgpack_write(items, error); if (pack_buffer == NULL) return FALSE; msg = fu_huddly_usb_hlink_msg_new("hpk/run", pack_buffer); if (msg == NULL) return FALSE; if (!fu_huddly_usb_device_hlink_send(self, msg, error)) return FALSE; if (!fu_device_retry_full(FU_DEVICE(self), fu_huddly_usb_device_hpk_done_cb, 100, 500, /* ms */ NULL, error)) return FALSE; return fu_huddly_usb_device_hlink_unsubscribe(self, "upgrader/status", error); } static void fu_huddly_usb_device_to_string(FuDevice *device, guint idt, GString *str) { FuHuddlyUsbDevice *self = FU_HUDDLY_USB_DEVICE(device); fwupd_codec_string_append(str, idt, "ProductState", self->product_state); fwupd_codec_string_append_bool(str, idt, "PendingVerify", self->pending_verify); fwupd_codec_string_append_bool(str, idt, "NeedReboot", self->need_reboot); } static gboolean fu_huddly_usb_device_verify(FuHuddlyUsbDevice *self, FuProgress *progress, GError **error) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 80, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 20, NULL); /* write the firmware image to the device for verification */ if (!fu_huddly_usb_device_hcp_write_file(self, "firmware.hpk", self->input_stream, fu_progress_get_child(progress), error)) { return FALSE; } fu_progress_step_done(progress); /* tell the device to execute the upgrade script in the transmitted hpk. This will verify * the written software */ if (!fu_huddly_usb_device_hpk_run(self, "firmware.hpk", error)) return FALSE; fu_progress_step_done(progress); self->pending_verify = FALSE; return TRUE; } static gboolean fu_huddly_usb_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuHuddlyUsbDevice *self = FU_HUDDLY_USB_DEVICE(device); if (!fu_huddly_usb_device_ensure_product_info(self, error)) { g_prefix_error(error, "failed to read product info: "); return FALSE; } /* check that the device is pending verification */ if (g_strcmp0(self->product_state, "Unverified") == 0) { if (!fu_huddly_usb_device_verify(self, progress, error)) return FALSE; /* ensure that the device reports state 'Verified' after the update has completed */ if (!fu_huddly_usb_device_ensure_product_info(self, error)) return FALSE; if (g_strcmp0(self->product_state, "Verified") != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "expected device state Verified. State %s", self->product_state); return FALSE; } } /* success */ return TRUE; } static gboolean fu_huddly_usb_device_probe(FuDevice *device, GError **error) { FuHuddlyUsbDevice *self = FU_HUDDLY_USB_DEVICE(device); /* FuUsbDevice->probe */ if (!FU_DEVICE_CLASS(fu_huddly_usb_device_parent_class)->probe(device, error)) return FALSE; return fu_huddly_usb_device_find_interface(self, error); } static gboolean fu_huddly_usb_device_setup(FuDevice *device, GError **error) { FuHuddlyUsbDevice *self = FU_HUDDLY_USB_DEVICE(device); /* UsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_huddly_usb_device_parent_class)->setup(device, error)) return FALSE; /* send protocol reset twice in case previous communication has not terminated correctly */ if (!fu_huddly_usb_device_send_reset(self, error)) return FALSE; if (!fu_huddly_usb_device_send_reset(self, error)) return FALSE; if (!fu_huddly_usb_device_salute(self, error)) return FALSE; if (!fu_huddly_usb_device_ensure_product_info(self, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_huddly_usb_device_cleanup(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuHuddlyUsbDevice *self = FU_HUDDLY_USB_DEVICE(device); /* no longer required */ g_clear_object(&self->input_stream); return TRUE; } static gboolean fu_huddly_usb_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuHuddlyUsbDevice *self = FU_HUDDLY_USB_DEVICE(device); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 54, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 45, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, NULL); /* get default image */ self->input_stream = fu_firmware_get_stream(firmware, error); if (self->input_stream == NULL) return FALSE; /* send the image file to the target */ if (!fu_huddly_usb_device_hcp_write_file(self, "firmware.hpk", self->input_stream, fu_progress_get_child(progress), error)) { return FALSE; } fu_progress_step_done(progress); /* tell the device to execute the upgrade script embedded in the hpk */ if (!fu_huddly_usb_device_hpk_run(self, "firmware.hpk", error)) return FALSE; fu_progress_step_done(progress); if (!self->need_reboot) { /* The device not requesting reboot could occur if the device was in an unverified state due to an aborted previous upgrade attempt, in which case this download will complete the upgrade */ g_warning("expected device to request reboot after download"); return TRUE; } /* reboot the device after the upgrade has been written */ if (!fu_huddly_usb_device_reboot(self, error)) return FALSE; fu_progress_step_done(progress); /* success */ self->pending_verify = TRUE; fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static void fu_huddly_usb_device_set_progress(FuDevice *device, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 72, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 26, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reload"); } static void fu_huddly_usb_device_init(FuHuddlyUsbDevice *self) { fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_remove_delay(FU_DEVICE(self), 60000); /* 60 second remove delay */ fu_device_add_protocol(FU_DEVICE(self), "com.huddly.usb"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SELF_RECOVERY); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_WEB_CAMERA); } static void fu_huddly_usb_device_replace(FuDevice *device, FuDevice *donor) { FuHuddlyUsbDevice *self = FU_HUDDLY_USB_DEVICE(device); FuHuddlyUsbDevice *self_donor = FU_HUDDLY_USB_DEVICE(donor); g_set_object(&self->input_stream, self_donor->input_stream); } static void fu_huddly_usb_device_finalize(GObject *object) { FuHuddlyUsbDevice *self = FU_HUDDLY_USB_DEVICE(object); if (self->input_stream != NULL) g_object_unref(self->input_stream); g_free(self->product_state); G_OBJECT_CLASS(fu_huddly_usb_device_parent_class)->finalize(object); } static void fu_huddly_usb_device_class_init(FuHuddlyUsbDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->finalize = fu_huddly_usb_device_finalize; device_class->to_string = fu_huddly_usb_device_to_string; device_class->probe = fu_huddly_usb_device_probe; device_class->setup = fu_huddly_usb_device_setup; device_class->cleanup = fu_huddly_usb_device_cleanup; device_class->attach = fu_huddly_usb_device_attach; device_class->write_firmware = fu_huddly_usb_device_write_firmware; device_class->set_progress = fu_huddly_usb_device_set_progress; device_class->replace = fu_huddly_usb_device_replace; } fwupd-2.0.10/plugins/huddly-usb/fu-huddly-usb-device.h000066400000000000000000000004401501337203100225470ustar00rootroot00000000000000/* * Copyright 2024 Huddly * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_HUDDLY_USB_DEVICE (fu_huddly_usb_device_get_type()) G_DECLARE_FINAL_TYPE(FuHuddlyUsbDevice, fu_huddly_usb_device, FU, HUDDLY_USB_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/huddly-usb/fu-huddly-usb-plugin.c000066400000000000000000000013341501337203100226040ustar00rootroot00000000000000/* * Copyright 2024 Huddly * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-huddly-usb-device.h" #include "fu-huddly-usb-plugin.h" struct _FuHuddlyUsbPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuHuddlyUsbPlugin, fu_huddly_usb_plugin, FU_TYPE_PLUGIN) static void fu_huddly_usb_plugin_init(FuHuddlyUsbPlugin *self) { } static void fu_huddly_usb_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_HUDDLY_USB_DEVICE); } static void fu_huddly_usb_plugin_class_init(FuHuddlyUsbPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_huddly_usb_plugin_constructed; } fwupd-2.0.10/plugins/huddly-usb/fu-huddly-usb-plugin.h000066400000000000000000000003311501337203100226050ustar00rootroot00000000000000/* * Copyright 2024 Huddly * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuHuddlyUsbPlugin, fu_huddly_usb_plugin, FU, HUDDLY_USB_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/huddly-usb/fu-huddly-usb.rs000066400000000000000000000004511501337203100215110ustar00rootroot00000000000000// Copyright 2024 Lars erling stensen // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(New, Parse)] #[repr(C, packed)] struct FuStructHLinkHeader { req_id: u32le, res_id: u32le, flags: u16le, msg_name_size: u16le, payload_size: u32le, } fwupd-2.0.10/plugins/huddly-usb/huddly-usb.quirk000066400000000000000000000004031501337203100216050ustar00rootroot00000000000000# Huddly L1 [USB\VID_2BD9&PID_A032] Plugin = huddly_usb # Huddly S1 [USB\VID_2BD9&PID_A031] Plugin = huddly_usb # Huddly Crew [USB\VID_2BD9&PID_A033] Plugin = huddly_usb # Huddly USB adapter, no camera connected [USB\VID_2BD9&PID_BA01] Plugin = huddly_usb fwupd-2.0.10/plugins/huddly-usb/meson.build000066400000000000000000000011501501337203100206120ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginHuddlyUsb"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('huddly-usb.quirk') plugin_builtins += static_library('fu_plugin_huddly_usb', rustgen.process('fu-huddly-usb.rs'), sources: [ 'fu-huddly-usb-common.c', 'fu-huddly-usb-device.c', 'fu-huddly-usb-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) enumeration_data += files('tests/huddly-s1-basic-setup.json') device_tests += files( 'tests/huddly-s1.json', 'tests/huddly-s1-basic.json', ) fwupd-2.0.10/plugins/huddly-usb/tests/000077500000000000000000000000001501337203100176155ustar00rootroot00000000000000fwupd-2.0.10/plugins/huddly-usb/tests/huddly-s1-basic-setup.json000066400000000000000000000110711501337203100245370ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUsbDevice", "PlatformId": "1-5", "Created": "2024-11-07T14:42:49.114706Z", "IdVendor": 11225, "IdProduct": 41009, "Device": 261, "USB": 528, "Manufacturer": 1, "DeviceClass": 239, "DeviceSubClass": 2, "DeviceProtocol": 1, "Product": 2, "SerialNumber": 3, "UsbBosDescriptors": [ { "DevCapabilityType": 22, "ExtraData": "AAI=" }, { "DevCapabilityType": 2, "ExtraData": "DgEAAA==" }, { "DevCapabilityType": 3, "ExtraData": "AA8AAQr/AQ==" } ], "UsbConfigDescriptors": [ { "Configuration": 4, "ConfigurationValue": 1 } ], "UsbInterfaces": [ { "Length": 9, "DescriptorType": 4, "InterfaceClass": 14, "InterfaceSubClass": 1, "Interface": 7 }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 1, "InterfaceClass": 14, "InterfaceSubClass": 2, "Interface": 8 }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 1, "AlternateSetting": 1, "InterfaceClass": 14, "InterfaceSubClass": 2, "Interface": 8, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 129, "Interval": 1, "MaxPacketSize": 5120 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 2, "InterfaceClass": 255, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 130, "MaxPacketSize": 512 }, { "DescriptorType": 5, "EndpointAddress": 1, "MaxPacketSize": 512 } ] } ], "UsbEvents": [ { "Id": "#4693935e", "Data": "001" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=16\nDEVNAME=bus/usb/001/017\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=2bd9/a031/105\nTYPE=239/2/1\nBUSNUM=001\nDEVNUM=017" }, { "Id": "#1ab3ae0a", "Data": "017" }, { "Id": "#4693935e", "Data": "001" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=16\nDEVNAME=bus/usb/001/017\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=2bd9/a031/105\nTYPE=239/2/1\nBUSNUM=001\nDEVNUM=017" }, { "Id": "#1ab3ae0a", "Data": "017" }, { "Id": "#ded9760f", "Data": "SHVkZGx5AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" }, { "Id": "#1fcf122d", "Data": "SHVkZGx5IFMxAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" }, { "Id": "#028c3a0e", "Data": "NTI0MjlGMDYzMS01MjQyMUMwMTMxAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" }, { "Id": "#5292e31c", "Data": "" }, { "Id": "#5292e31c", "Data": "" }, { "Id": "#9a533eb0", "Data": "AA==" }, { "Id": "#122b2970", "Data": "SExpbmsgdjA=" }, { "Id": "#2e4395f0", "Data": "AAAAAAAAAAAAABIAGgAAAGhsaW5rLW1iLXN1YnNjcmliZXByb2RpbmZvL2dldF9tc2dwYWNrX3JlcGx5" }, { "Id": "#05c6452e", "Data": "AAAAAAAAAAAAABQAAAAAAHByb2RpbmZvL2dldF9tc2dwYWNr" }, { "Id": "#67e7444f", "Data": "AAAAAAAAAAAAABoAKgEAAHByb2RpbmZvL2dldF9tc2dwYWNrX3JlcGx5i6thcHBfdmVyc2lvbrgxLjguNC0wLjc5K3NoYS5mOWY0OTk5OGWoYm9hcmRfaWShMKxib2FyZF9zZXJpYWyrSDM2MjQyOTAwMzGrY2FtZXJhLWFkZHKuMTY5LjI1NC4xMjguNTerY2FtZXJhLW1vZGWlbWpwZWeuY2FtZXJhLXZlcnNpb264MS44LjQtMC43OStzaGEuZjlmNDk5OThlpmh3X3JldqExpnNlcmlhbKo1MjQyOUYwNjMxpHNsb3ShQqxzc2JsLXZlcnNpb27ZS1UtQm9vdCAyMDIwLjA0LXNtYXJ0YmFzZS0wMTEwMC1nN2QyNDNhMGNhZDZiIChNYXkgMjcgMjAyNCAtIDA2OjUyOjIyICswMDAwKaVzdGF0ZahWZXJpZmllZA==" } ] } ] } fwupd-2.0.10/plugins/huddly-usb/tests/huddly-s1-basic.json000066400000000000000000000005211501337203100233770ustar00rootroot00000000000000{ "name": "Huddly S1 (enumerate only)", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/huddly-s1-basic-setup.json", "components": [ { "version": "1.8.4", "guids": [ "a56643db-6cf8-5fa0-95f3-c3d91cc645e3" ] } ] } ] } fwupd-2.0.10/plugins/huddly-usb/tests/huddly-s1.json000066400000000000000000000011571501337203100223260ustar00rootroot00000000000000{ "name": "Huddly S1", "interactive": false, "steps": [ { "url": "4af1947aa24b5a6cc484537b42ca16e0f79bc241fc720ea2fa72611b5274137f-huddly-s1-1.8.4.cab", "components": [ { "version": "1.8.4", "guids": [ "a56643db-6cf8-5fa0-95f3-c3d91cc645e3" ] } ] }, { "url": "57c29008fcfe98fd3ba74d0ee519321961a27c338e56faa33d74737f9bbf9f20-huddly-s1-1.8.6.cab", "components": [ { "version": "1.8.6", "guids": [ "a56643db-6cf8-5fa0-95f3-c3d91cc645e3" ] } ] } ] } fwupd-2.0.10/plugins/hughski-colorhug/000077500000000000000000000000001501337203100176555ustar00rootroot00000000000000fwupd-2.0.10/plugins/hughski-colorhug/README.md000066400000000000000000000030711501337203100211350ustar00rootroot00000000000000--- title: Plugin: ColorHug --- ## Introduction The ColorHug is an affordable open source display colorimeter built by Hughski Limited. The USB device allows you to calibrate your screen for accurate color matching. ColorHug versions 1 and 2 support a custom HID-based flashing protocol, but version 3 (ColorHug+) has now switched to DFU. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a packed binary file format. This plugin supports the following protocol ID: * `com.hughski.colorhug` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_273F&PID_1001` ## Update Behavior The device usually presents in runtime mode, but on detach re-enumerates with a different USB PID in a bootloader mode. On attach the device again re-enumerates back to the runtime mode. For this reason the `REPLUG_MATCH_GUID` internal device flag is used so that the bootloader and runtime modes are treated as the same device. ### `Flags=halfsize` Some devices have a compact memory layout and the application code starts earlier. Since: 1.0.3 ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x273F` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `0.8.0`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Richard Hughes: @hughsie fwupd-2.0.10/plugins/hughski-colorhug/fu-hughski-colorhug-device.c000066400000000000000000000423041501337203100251530ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-hughski-colorhug-device.h" #include "fu-hughski-colorhug-struct.h" #define FU_HUGHSKI_COLORHUG_DEVICE_FLAG_HALFSIZE "halfsize" struct _FuHughskiColorhugDevice { FuUsbDevice parent_instance; guint16 start_addr; }; G_DEFINE_TYPE(FuHughskiColorhugDevice, fu_hughski_colorhug_device, FU_TYPE_USB_DEVICE) #define CH_CMD_GET_FIRMWARE_VERSION 0x07 #define CH_CMD_RESET 0x24 #define CH_CMD_READ_FLASH 0x25 #define CH_CMD_WRITE_FLASH 0x26 #define CH_CMD_BOOT_FLASH 0x27 #define CH_CMD_SET_FLASH_SUCCESS 0x28 #define CH_CMD_ERASE_FLASH 0x29 #define CH_USB_HID_EP 0x0001 #define CH_USB_HID_EP_IN (CH_USB_HID_EP | 0x80) #define CH_USB_HID_EP_OUT (CH_USB_HID_EP | 0x00) #define CH_USB_HID_EP_SIZE 64 #define CH_USB_CONFIG 0x0001 #define CH_USB_INTERFACE 0x0000 #define CH_EEPROM_ADDR_RUNCODE 0x4000 #define CH_EEPROM_ADDR_RUNCODE_ALS 0x2000 #define CH_DEVICE_USB_TIMEOUT 5000 /* ms */ #define CH_FLASH_TRANSFER_BLOCK_SIZE 0x020 /* 32 */ static gboolean fu_hughski_colorhug_device_msg(FuHughskiColorhugDevice *self, guint8 cmd, guint8 *ibuf, gsize ibufsz, guint8 *obuf, gsize obufsz, GError **error) { guint8 buf[] = {[0] = cmd, [1 ... CH_USB_HID_EP_SIZE - 1] = 0x00}; gsize actual_length = 0; g_autoptr(GError) error_local = NULL; /* check size */ if (ibufsz > sizeof(buf) - 1) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "cannot process chunk of size %" G_GSIZE_FORMAT, ibufsz); return FALSE; } if (obufsz > sizeof(buf) - 2) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "cannot process chunk of size %" G_GSIZE_FORMAT, ibufsz); return FALSE; } /* optionally copy in data */ if (ibuf != NULL) { if (!fu_memcpy_safe(buf, sizeof(buf), 0x1, /* dst */ ibuf, ibufsz, 0x0, /* src */ ibufsz, error)) return FALSE; } /* request */ fu_dump_raw(G_LOG_DOMAIN, "REQ", buf, ibufsz + 1); if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), CH_USB_HID_EP_OUT, buf, sizeof(buf), &actual_length, CH_DEVICE_USB_TIMEOUT, NULL, /* cancellable */ &error_local)) { if (cmd == CH_CMD_RESET && g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_debug("ignoring '%s' on reset", error_local->message); return TRUE; } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to send request: "); return FALSE; } if (actual_length != CH_USB_HID_EP_SIZE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "request not all sent, got %" G_GSIZE_FORMAT, actual_length); return FALSE; } /* read reply */ if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), CH_USB_HID_EP_IN, buf, sizeof(buf), &actual_length, CH_DEVICE_USB_TIMEOUT, NULL, /* cancellable */ &error_local)) { if (cmd == CH_CMD_RESET && g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_debug("ignoring '%s' on reset", error_local->message); return TRUE; } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to get reply: "); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "RES", buf, actual_length); /* old bootloaders do not return the full block */ if (actual_length != CH_USB_HID_EP_SIZE && actual_length != 2 && actual_length != obufsz + 2) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "request not all received, got %" G_GSIZE_FORMAT, actual_length); return FALSE; } /* check error code */ if (buf[0] != FU_HUGHSKI_COLORHUG_ERROR_NONE) { const gchar *msg = fu_hughski_colorhug_error_to_string(buf[0]); if (msg == NULL) msg = "unknown error"; g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, msg); return FALSE; } /* check cmd matches */ if (buf[1] != cmd) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "cmd incorrect, expected %u, got %u", cmd, buf[1]); return FALSE; } /* copy back optional buf */ if (obuf != NULL) { if (!fu_memcpy_safe(obuf, obufsz, 0x0, /* dst */ buf, sizeof(buf), 0x2, /* src */ obufsz, error)) return FALSE; } return TRUE; } static gboolean fu_hughski_colorhug_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuHughskiColorhugDevice *self = FU_HUGHSKI_COLORHUG_DEVICE(device); g_autoptr(GError) error_local = NULL; /* sanity check */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in bootloader mode, skipping"); return TRUE; } if (!fu_hughski_colorhug_device_msg(self, CH_CMD_RESET, NULL, 0, /* in */ NULL, 0, /* out */ &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to reset device: %s", error_local->message); return FALSE; } fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_hughski_colorhug_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuHughskiColorhugDevice *self = FU_HUGHSKI_COLORHUG_DEVICE(device); g_autoptr(GError) error_local = NULL; /* sanity check */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in runtime mode, skipping"); return TRUE; } if (!fu_hughski_colorhug_device_msg(self, CH_CMD_BOOT_FLASH, NULL, 0, /* in */ NULL, 0, /* out */ &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to boot to runtime: %s", error_local->message); return FALSE; } fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_hughski_colorhug_device_set_flash_success(FuHughskiColorhugDevice *self, gboolean val, GError **error) { guint8 buf[] = {[0] = val ? 0x01 : 0x00}; g_autoptr(GError) error_local = NULL; g_debug("setting flash success %s", val ? "true" : "false"); if (!fu_hughski_colorhug_device_msg(self, CH_CMD_SET_FLASH_SUCCESS, buf, sizeof(buf), /* in */ NULL, 0, /* out */ &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to set flash success: %s", error_local->message); return FALSE; } return TRUE; } static gboolean fu_hughski_colorhug_device_reload(FuDevice *device, GError **error) { FuHughskiColorhugDevice *self = FU_HUGHSKI_COLORHUG_DEVICE(device); return fu_hughski_colorhug_device_set_flash_success(self, TRUE, error); } static gboolean fu_hughski_colorhug_device_erase(FuHughskiColorhugDevice *self, guint16 addr, gsize sz, GError **error) { guint8 buf[4]; g_autoptr(GError) error_local = NULL; fu_memwrite_uint16(buf + 0, addr, G_LITTLE_ENDIAN); fu_memwrite_uint16(buf + 2, sz, G_LITTLE_ENDIAN); if (!fu_hughski_colorhug_device_msg(self, CH_CMD_ERASE_FLASH, buf, sizeof(buf), /* in */ NULL, 0, /* out */ &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to erase device: %s", error_local->message); return FALSE; } return TRUE; } static gchar * fu_hughski_colorhug_device_get_version(FuHughskiColorhugDevice *self, GError **error) { guint8 buf[6]; if (!fu_hughski_colorhug_device_msg(self, CH_CMD_GET_FIRMWARE_VERSION, NULL, 0, /* in */ buf, sizeof(buf), /* out */ error)) { return NULL; } return g_strdup_printf("%i.%i.%i", fu_memread_uint16(buf + 0, G_LITTLE_ENDIAN), fu_memread_uint16(buf + 2, G_LITTLE_ENDIAN), fu_memread_uint16(buf + 4, G_LITTLE_ENDIAN)); } static gboolean fu_hughski_colorhug_device_probe(FuDevice *device, GError **error) { FuHughskiColorhugDevice *self = FU_HUGHSKI_COLORHUG_DEVICE(device); /* compact memory layout */ if (fu_device_has_private_flag(device, FU_HUGHSKI_COLORHUG_DEVICE_FLAG_HALFSIZE)) self->start_addr = CH_EEPROM_ADDR_RUNCODE_ALS; /* add hardcoded bits */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); /* success */ return TRUE; } static gboolean fu_hughski_colorhug_device_setup(FuDevice *device, GError **error) { FuHughskiColorhugDevice *self = FU_HUGHSKI_COLORHUG_DEVICE(device); guint idx; /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_hughski_colorhug_device_parent_class)->setup(device, error)) return FALSE; /* get version number, falling back to the USB device release */ idx = fu_usb_device_get_custom_index(FU_USB_DEVICE(device), FU_USB_CLASS_VENDOR_SPECIFIC, 'F', 'W', NULL); if (idx != 0x00) { g_autofree gchar *tmp = NULL; tmp = fu_usb_device_get_string_descriptor(FU_USB_DEVICE(device), idx, NULL); /* although guessing is a route to insanity, if the device has * provided the extra data it's because the BCD type was not * suitable -- and INTEL_ME is not relevant here */ if (tmp != NULL) { fu_device_set_version_format(device, fu_version_guess_format(tmp)); fu_device_set_version(device, tmp); } } /* get GUID from the descriptor if set */ idx = fu_usb_device_get_custom_index(FU_USB_DEVICE(device), FU_USB_CLASS_VENDOR_SPECIFIC, 'G', 'U', NULL); if (idx != 0x00) { g_autofree gchar *tmp = NULL; tmp = fu_usb_device_get_string_descriptor(FU_USB_DEVICE(device), idx, NULL); fu_device_add_instance_id(device, tmp); } /* using the USB descriptor and old firmware */ if (fu_device_get_version_format(device) == FWUPD_VERSION_FORMAT_BCD) { g_autofree gchar *version = NULL; g_autoptr(GError) error_local = NULL; version = fu_hughski_colorhug_device_get_version(self, &error_local); if (version != NULL) { g_debug("obtained fwver using API '%s'", version); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, version); } else { g_warning("failed to get firmware version: %s", error_local->message); } } /* success */ return TRUE; } static guint8 fu_hughski_colorhug_device_calculate_checksum(const guint8 *data, gsize len) { guint8 checksum = 0xff; for (guint32 i = 0; i < len; i++) checksum ^= data[i]; return checksum; } static gboolean fu_hughski_colorhug_device_write_blocks(FuHughskiColorhugDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { guint8 buf[CH_FLASH_TRANSFER_BLOCK_SIZE + 4]; g_autoptr(FuChunk) chk = NULL; g_autoptr(GError) error_local = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; /* set address, length, checksum, data */ fu_memwrite_uint16(buf + 0, fu_chunk_get_address(chk), G_LITTLE_ENDIAN); buf[2] = fu_chunk_get_data_sz(chk); buf[3] = fu_hughski_colorhug_device_calculate_checksum(fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); if (!fu_memcpy_safe(buf, sizeof(buf), 0x4, /* dst */ fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0x0, /* src */ fu_chunk_get_data_sz(chk), error)) return FALSE; if (!fu_hughski_colorhug_device_msg(self, CH_CMD_WRITE_FLASH, buf, sizeof(buf), /* in */ NULL, 0, /* out */ &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to write: %s", error_local->message); return FALSE; } /* update progress */ fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_hughski_colorhug_device_verify_blocks(FuHughskiColorhugDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; guint8 buf[3]; guint8 buf_out[CH_FLASH_TRANSFER_BLOCK_SIZE + 1]; g_autoptr(GError) error_local = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; /* set address */ fu_memwrite_uint16(buf + 0, fu_chunk_get_address(chk), G_LITTLE_ENDIAN); buf[2] = fu_chunk_get_data_sz(chk); if (!fu_hughski_colorhug_device_msg(self, CH_CMD_READ_FLASH, buf, sizeof(buf), /* in */ buf_out, sizeof(buf_out), /* out */ &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "failed to read: %s", error_local->message); return FALSE; } /* verify */ if (memcmp(buf_out + 1, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to verify firmware for chunk %u, " "address 0x%0x, length 0x%0x", i, (guint)fu_chunk_get_address(chk), (guint)fu_chunk_get_data_sz(chk)); return FALSE; } /* update progress */ fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_hughski_colorhug_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuHughskiColorhugDevice *self = FU_HUGHSKI_COLORHUG_DEVICE(device); g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 19, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 44, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 35, NULL); /* get default image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* don't auto-boot firmware */ if (!fu_hughski_colorhug_device_set_flash_success(self, FALSE, error)) return FALSE; fu_progress_step_done(progress); /* erase flash */ if (!fu_hughski_colorhug_device_erase(self, self->start_addr, fu_firmware_get_size(firmware), error)) return FALSE; fu_progress_step_done(progress); /* write each block */ chunks = fu_chunk_array_new_from_stream(stream, self->start_addr, FU_CHUNK_PAGESZ_NONE, CH_FLASH_TRANSFER_BLOCK_SIZE, error); if (chunks == NULL) return FALSE; if (!fu_hughski_colorhug_device_write_blocks(self, chunks, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* verify each block */ if (!fu_hughski_colorhug_device_verify_blocks(self, chunks, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* success! */ return TRUE; } static void fu_hughski_colorhug_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 57, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 43, "reload"); } static void fu_hughski_colorhug_device_init(FuHughskiColorhugDevice *self) { /* this is the application code */ self->start_addr = CH_EEPROM_ADDR_RUNCODE; fu_device_add_protocol(FU_DEVICE(self), "com.hughski.colorhug"); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_COUNTERPART_GUIDS); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_register_private_flag(FU_DEVICE(self), FU_HUGHSKI_COLORHUG_DEVICE_FLAG_HALFSIZE); fu_usb_device_set_configuration(FU_USB_DEVICE(self), CH_USB_CONFIG); fu_usb_device_add_interface(FU_USB_DEVICE(self), CH_USB_INTERFACE); } static void fu_hughski_colorhug_device_class_init(FuHughskiColorhugDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_hughski_colorhug_device_write_firmware; device_class->attach = fu_hughski_colorhug_device_attach; device_class->detach = fu_hughski_colorhug_device_detach; device_class->reload = fu_hughski_colorhug_device_reload; device_class->setup = fu_hughski_colorhug_device_setup; device_class->probe = fu_hughski_colorhug_device_probe; device_class->set_progress = fu_hughski_colorhug_device_set_progress; } fwupd-2.0.10/plugins/hughski-colorhug/fu-hughski-colorhug-device.h000066400000000000000000000005701501337203100251570ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_HUGHSKI_COLORHUG_DEVICE (fu_hughski_colorhug_device_get_type()) G_DECLARE_FINAL_TYPE(FuHughskiColorhugDevice, fu_hughski_colorhug_device, FU, HUGHSKI_COLORHUG_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/hughski-colorhug/fu-hughski-colorhug-plugin.c000066400000000000000000000015021501337203100252050ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-hughski-colorhug-device.h" #include "fu-hughski-colorhug-plugin.h" struct _FuHughskiColorhugPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuHughskiColorhugPlugin, fu_hughski_colorhug_plugin, FU_TYPE_PLUGIN) static void fu_hughski_colorhug_plugin_init(FuHughskiColorhugPlugin *self) { } static void fu_hughski_colorhug_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_HUGHSKI_COLORHUG_DEVICE); } static void fu_hughski_colorhug_plugin_class_init(FuHughskiColorhugPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_hughski_colorhug_plugin_constructed; } fwupd-2.0.10/plugins/hughski-colorhug/fu-hughski-colorhug-plugin.h000066400000000000000000000004451501337203100252170ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuHughskiColorhugPlugin, fu_hughski_colorhug_plugin, FU, HUGHSKI_COLORHUG_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/hughski-colorhug/fu-hughski-colorhug.rs000066400000000000000000000015471501337203100241240ustar00rootroot00000000000000// Copyright 2024 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] enum FuHughskiColorhugError { None, UnknownCmd, WrongUnlockCode, NotImplemented, UnderflowSensor, NoSerial, Watchdog, InvalidAddress, InvalidLength, InvalidChecksum, InvalidValue, UnknownCmdForBootloader, NoCalibration, OverflowMultiply, OverflowAddition, OverflowSensor, OverflowStack, DeviceDeactivated, IncompleteRequest, SelfTestSensor, SelfTestRed, SelfTestGreen, SelfTestBlue, SelfTestColorSelect, SelfTestMultiplier, InvalidCalibration, SramFailed, OutOfMemory, SelfTestTemperature, SelfTestI2c, SelfTestAdcVdd, SelfTestAdcVss, SelfTestAdcVref, I2cTargetAddress, I2cTargetConfig, SelfTestEeprom, } fwupd-2.0.10/plugins/hughski-colorhug/hughski-colorhug.quirk000066400000000000000000000030551501337203100242170ustar00rootroot00000000000000# ColorHug1 [USB\VID_273F&PID_1000] Plugin = hughski_colorhug Flags = is-bootloader,self-recovery Guid = 40338ceb-b966-4eae-adae-9c32edfcc484 FirmwareSizeMin = 0x2000 FirmwareSizeMax = 0x8000 CounterpartGuid = USB\VID_273F&PID_1001 InstallDuration = 8 # ColorHug1: first batch! [USB\VID_04D8&PID_F8DA] Guid = USB\VID_273F&PID_1000 [USB\VID_273F&PID_1001] Plugin = hughski_colorhug Flags = self-recovery Summary = An open source display colorimeter Icon = colorimeter-colorhug Guid = 40338ceb-b966-4eae-adae-9c32edfcc484 CounterpartGuid = USB\VID_273F&PID_1000 InstallDuration = 8 # ColorHug2 [USB\VID_273F&PID_1004] Plugin = hughski_colorhug Flags = self-recovery Summary = An open source display colorimeter Icon = colorimeter-colorhug Guid = 2082b5e0-7a64-478a-b1b2-e3404fab6dad FirmwareSizeMin = 0x2000 FirmwareSizeMax = 0x8000 CounterpartGuid = USB\VID_273F&PID_1005 InstallDuration = 8 [USB\VID_273F&PID_1005] Plugin = hughski_colorhug Flags = is-bootloader,self-recovery Guid = 2082b5e0-7a64-478a-b1b2-e3404fab6dad CounterpartGuid = USB\VID_273F&PID_1004 InstallDuration = 8 # ColorHugALS [USB\VID_273F&PID_1007] Plugin = hughski_colorhug Flags = halfsize,self-recovery Summary = An open source ambient light sensor Guid = 84f40464-9272-4ef7-9399-cd95f12da696 FirmwareSizeMin = 0x1000 FirmwareSizeMax = 0x4000 CounterpartGuid = USB\VID_273F&PID_1006 InstallDuration = 5 [USB\VID_273F&PID_1006] Plugin = hughski_colorhug Flags = halfsize,is-bootloader,self-recovery Guid = 84f40464-9272-4ef7-9399-cd95f12da696 CounterpartGuid = USB\VID_273F&PID_1007 InstallDuration = 5 fwupd-2.0.10/plugins/hughski-colorhug/meson.build000066400000000000000000000012041501337203100220140ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginHughskiColorhug"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('hughski-colorhug.quirk') plugin_builtins += static_library('fu_plugin_hughski_colorhug', rustgen.process('fu-hughski-colorhug.rs'), sources: [ 'fu-hughski-colorhug-device.c', 'fu-hughski-colorhug-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files('tests/hughski-colorhug.json', 'tests/hughski-colorhug2.json', 'tests/hughski-colorhug-plus.json', ) fwupd-2.0.10/plugins/hughski-colorhug/tests/000077500000000000000000000000001501337203100210175ustar00rootroot00000000000000fwupd-2.0.10/plugins/hughski-colorhug/tests/hughski-colorhug-plus.json000066400000000000000000000016511501337203100261600ustar00rootroot00000000000000{ "name": "Hughski ColorHug Plus", "interactive": false, "steps": [ { "url": "5cbff92158331aeb10008ca36fa918a9637dde7bfe31de3e0523d14090be8977-fakedevice01_dfu.cab", "emulation-url": "8be5c7f3fe5c399a8699768da3f4ddd2582df19c70e197b852efaa027f42688b-fakedevice01_dfu.zip", "components": [ { "version": "0.1", "guids": [ "dfbaaded-754b-5214-a5f2-46aa3331e8ce", "f5b42624-ff57-5073-ba09-b7c9c04241be" ] } ] }, { "url": "8bc3afd07a0af3baaab8b19893791dd3972e8305-fakedevice02_dfu.cab", "emulation-url": "72c580400020f22929510e9fec43d6781d56af697e72e8d560b45daa57e1817c-fakedevice02_dfu.zip", "components": [ { "version": "0.2", "guids": [ "dfbaaded-754b-5214-a5f2-46aa3331e8ce", "f5b42624-ff57-5073-ba09-b7c9c04241be" ] } ] } ] } fwupd-2.0.10/plugins/hughski-colorhug/tests/hughski-colorhug.json000066400000000000000000000015611501337203100251770ustar00rootroot00000000000000{ "name": "Hughski ColorHug", "interactive": false, "steps": [ { "url": "42a0a07a978018af235833c9b861461e923163dffd1fb177a9caa59a0c36995e-hughski-colorhug2-1.2.5.cab", "emulation-url": "08ddc01a50fb866398d01cc4829531640e651f0233306e10a54117f88474b153-hughski-colorhug-1.2.5.zip", "components": [ { "version": "1.2.5", "guids": [ "40338ceb-b966-4eae-adae-9c32edfcc484" ] } ] }, { "url": "2a066c8a1bfbd99f161c867b4dbe7e51ac36fc2b16ef37b11d18419874fbcb6c-hughski-colorhug-1.2.6.cab", "emulation-url": "b25122f17912467c1488aaadb59483efc3fe773cb7d002f64289177ca2f3285c-hughski-colorhug-1.2.6.zip", "components": [ { "version": "1.2.6", "guids": [ "40338ceb-b966-4eae-adae-9c32edfcc484" ] } ] } ] } fwupd-2.0.10/plugins/hughski-colorhug/tests/hughski-colorhug2.json000066400000000000000000000015351501337203100252620ustar00rootroot00000000000000{ "name": "Hughski ColorHug2", "interactive": false, "steps": [ { "url": "170f2c19f17b7819644d3fcc7617621cc3350a04-hughski-colorhug2-2.0.6.cab", "emulation-url": "5ddb5d8e9ac85ea05b8dafcad638befdf3e70e5e1db0ca47489fa43d93f33a57-hughski-colorhug2-2.0.6.zip", "components": [ { "version": "2.0.6", "guids": [ "2082b5e0-7a64-478a-b1b2-e3404fab6dad" ] } ] }, { "url": "e5ad222bdbd3d3d48d8613e67c7e0a0e194f8cd828e33c554d9f05d933e482c7-hughski-colorhug2-2.0.7.cab", "emulation-url": "29ee025e65b6a469556bbdd819cba665b043244c90c65b9f659933a347f8cf2e-hughski-colorhug2-2.0.7.zip", "components": [ { "version": "2.0.7", "guids": [ "2082b5e0-7a64-478a-b1b2-e3404fab6dad" ] } ] } ] } fwupd-2.0.10/plugins/intel-amt/000077500000000000000000000000001501337203100162655ustar00rootroot00000000000000fwupd-2.0.10/plugins/intel-amt/README.md000066400000000000000000000020551501337203100175460ustar00rootroot00000000000000--- title: Plugin: Intel AMT --- ## Introduction This plugin is used to version number for the Intel AMT, typically CSME. If AMT is enabled and provisioned and the AMT version is between 6.0 and 11.2, and you have not upgraded your firmware, you are vulnerable to CVE-2017-5689 and you should disable AMT in your system firmware. This code is inspired by [AMT status checker for Linux](https://github.com/mjg59/mei-amt-check) by Matthew Garrett. That tool in turn is heavily based on mei-amt-version from samples/mei in the Linux source tree and copyright Intel Corporation. ## GUID Generation These devices use the existing GUIDs provided by the ME host interfaces. ## Vendor ID Security The devices are not upgradable and thus require no vendor ID set. ## External Interface Access This plugin requires `ioctl(IOCTL_MEI_CONNECT_CLIENT)` to `/dev/mei0`. ## Version Considerations This plugin has been available since fwupd version `2.0.9`, although the functionality was previously introduced in the `intel-me` plugin released with fwupd version `1.8.7`. fwupd-2.0.10/plugins/intel-amt/fu-intel-amt-device.c000066400000000000000000000170441501337203100221760ustar00rootroot00000000000000/* * Copyright 2012 Intel Corporation. * Copyright 2017 Google, Inc. * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-intel-amt-device.h" #include "fu-intel-amt-struct.h" struct _FuIntelAmtDevice { FuMeiDevice parent_instance; }; G_DEFINE_TYPE(FuIntelAmtDevice, fu_intel_amt_device, FU_TYPE_MEI_DEVICE) #define FU_AMT_STATUS_HOST_IF_EMPTY_RESPONSE 0x4000 #define FU_INTEL_AMT_DEVICE_UUID "12f80028-b4b7-4b2d-aca8-46e0ff65814c" static gboolean fu_intel_amt_device_status_set_error(guint32 status, GError **error) { if (status == FU_AMT_STATUS_SUCCESS) return TRUE; if (status == FU_AMT_STATUS_INTERNAL_ERROR) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "internal error"); return FALSE; } if (status == FU_AMT_STATUS_NOT_READY) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "not ready"); return FALSE; } if (status == FU_AMT_STATUS_INVALID_AMT_MODE) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid AMT mode"); return FALSE; } if (status == FU_AMT_STATUS_INVALID_MESSAGE_LENGTH) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid message length"); return FALSE; } if (status == FU_AMT_STATUS_HOST_IF_EMPTY_RESPONSE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Intel AMT is disabled"); return FALSE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unknown error"); return FALSE; } static GByteArray * fu_intel_amt_device_host_if_call(FuIntelAmtDevice *self, GByteArray *inbuf, GError **error) { gsize outbufsz; g_autoptr(GByteArray) outbuf = g_byte_array_new(); fu_byte_array_set_size(outbuf, fu_mei_device_get_max_msg_length(FU_MEI_DEVICE(self)), 0x0); if (!fu_mei_device_write(FU_MEI_DEVICE(self), inbuf->data, inbuf->len, 5000, error)) return NULL; if (!fu_mei_device_read(FU_MEI_DEVICE(self), outbuf->data, outbuf->len, &outbufsz, 2000, error)) return NULL; if (outbufsz <= 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_READ, "empty response"); return NULL; } g_byte_array_set_size(outbuf, outbufsz); return g_steal_pointer(&outbuf); } static gboolean fu_intel_amt_device_get_provisioning_state(FuIntelAmtDevice *self, FuAmtProvisioningState *provisioning_state, GError **error) { g_autofree struct FuAmtHostIfRespHeader *response = NULL; g_autoptr(GByteArray) data = NULL; g_autoptr(FuAmtHostIfMsgProvisioningStateRequest) st_req = fu_amt_host_if_msg_provisioning_state_request_new(); g_autoptr(FuAmtHostIfMsgProvisioningStateResponse) st_res = NULL; data = fu_intel_amt_device_host_if_call(self, st_req, error); if (data == NULL) return FALSE; /* parse response */ st_res = fu_amt_host_if_msg_provisioning_state_response_parse(data->data, data->len, 0x0, error); if (st_res == NULL) return FALSE; if (!fu_intel_amt_device_status_set_error( fu_amt_host_if_msg_provisioning_state_response_get_status(st_res), error)) return FALSE; *provisioning_state = fu_amt_host_if_msg_provisioning_state_response_get_provisioning_state(st_res); /* success */ return TRUE; } static gboolean fu_intel_amt_device_ensure_version(FuIntelAmtDevice *self, GError **error) { guint32 version_count; g_autoptr(FuAmtHostIfMsgCodeVersionRequest) st_req = fu_amt_host_if_msg_code_version_request_new(); g_autoptr(FuAmtHostIfMsgCodeVersionResponse) st_res = NULL; g_autoptr(GByteArray) data = NULL; g_autoptr(GString) version_bl = g_string_new(NULL); g_autoptr(GString) version_fw = g_string_new(NULL); data = fu_intel_amt_device_host_if_call(self, st_req, error); if (data == NULL) return FALSE; /* parse response */ st_res = fu_amt_host_if_msg_code_version_response_parse(data->data, data->len, 0x0, error); if (st_res == NULL) return FALSE; if (!fu_intel_amt_device_status_set_error( fu_amt_host_if_msg_code_version_response_get_status(st_res), error)) return FALSE; /* parse each version */ version_count = fu_amt_host_if_msg_code_version_response_get_version_count(st_res); for (guint i = 0; i < version_count; i++) { g_autofree gchar *description = NULL; g_autofree gchar *version = NULL; g_autoptr(FuAmtUnicodeString) st_str = NULL; st_str = fu_amt_unicode_string_parse(data->data, data->len, st_res->len + (i * FU_AMT_UNICODE_STRING_SIZE), error); if (st_str == NULL) return FALSE; /* get description */ if (fu_amt_unicode_string_get_description_length(st_str) > FU_AMT_UNICODE_STRING_SIZE_DESCRIPTION_STRING) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "description string too large"); return FALSE; } description = fu_amt_unicode_string_get_description_string(st_str); /* get version */ if (fu_amt_unicode_string_get_version_length(st_str) > FU_AMT_UNICODE_STRING_SIZE_VERSION_STRING) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "version string too large"); return FALSE; } version = fu_amt_unicode_string_get_version_string(st_str); /* build something suitable for fwupd */ if (g_strcmp0(description, "AMT") == 0) { g_string_append(version_fw, version); continue; } if (g_strcmp0(description, "Recovery Version") == 0) { g_string_append(version_bl, version); continue; } if (g_strcmp0(description, "Build Number") == 0) { g_string_append_printf(version_fw, ".%s", version); continue; } if (g_strcmp0(description, "Recovery Build Num") == 0) { g_string_append_printf(version_bl, ".%s", version); continue; } } /* success */ if (version_fw->len > 0) fu_device_set_version(FU_DEVICE(self), version_fw->str); if (version_bl->len > 0) fu_device_set_version_bootloader(FU_DEVICE(self), version_bl->str); return TRUE; } static gboolean fu_intel_amt_device_setup(FuDevice *device, GError **error) { FuIntelAmtDevice *self = FU_INTEL_AMT_DEVICE(device); FuAmtProvisioningState provisioning_state = FU_AMT_PROVISIONING_STATE_UNPROVISIONED; /* get versions */ if (!fu_mei_device_connect(FU_MEI_DEVICE(device), FU_INTEL_AMT_DEVICE_UUID, 0, error)) { g_prefix_error(error, "failed to connect: "); return FALSE; } if (!fu_intel_amt_device_ensure_version(self, error)) { g_prefix_error(error, "failed to check version: "); return FALSE; } /* get provisioning state */ if (!fu_intel_amt_device_get_provisioning_state(self, &provisioning_state, error)) { g_prefix_error(error, "failed to get provisioning state: "); return FALSE; } if (provisioning_state < FU_AMT_PROVISIONING_STATE_LAST) { g_autofree gchar *name = g_strdup_printf("AMT [%s]", fu_amt_provisioning_state_to_string(provisioning_state)); fu_device_set_name(device, name); } /* success */ return TRUE; } static void fu_intel_amt_device_init(FuIntelAmtDevice *self) { fu_device_set_logical_id(FU_DEVICE(self), "AMT"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_INTEL_ME); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE_CHILD); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_COMPUTER); fu_device_set_name(FU_DEVICE(self), "AMT"); fu_device_set_summary(FU_DEVICE(self), "Hardware and firmware technology for remote " "out-of-band management"); } static void fu_intel_amt_device_class_init(FuIntelAmtDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_intel_amt_device_setup; } fwupd-2.0.10/plugins/intel-amt/fu-intel-amt-device.h000066400000000000000000000004711501337203100221770ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_INTEL_AMT_DEVICE (fu_intel_amt_device_get_type()) G_DECLARE_FINAL_TYPE(FuIntelAmtDevice, fu_intel_amt_device, FU, INTEL_AMT_DEVICE, FuMeiDevice) fwupd-2.0.10/plugins/intel-amt/fu-intel-amt-plugin.c000066400000000000000000000014341501337203100222310ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-intel-amt-device.h" #include "fu-intel-amt-plugin.h" struct _FuIntelAmtPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuIntelAmtPlugin, fu_intel_amt_plugin, FU_TYPE_PLUGIN) static void fu_intel_amt_plugin_init(FuIntelAmtPlugin *self) { } static void fu_intel_amt_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_udev_subsystem(plugin, "mei"); fu_plugin_add_device_gtype(plugin, FU_TYPE_INTEL_AMT_DEVICE); } static void fu_intel_amt_plugin_class_init(FuIntelAmtPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_intel_amt_plugin_constructed; } fwupd-2.0.10/plugins/intel-amt/fu-intel-amt-plugin.h000066400000000000000000000003641501337203100222370ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuIntelAmtPlugin, fu_intel_amt_plugin, FU, INTEL_AMT_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/intel-amt/fu-intel-amt.rs000066400000000000000000000037701501337203100211440ustar00rootroot00000000000000// Copyright 2024 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later enum FuAmtStatus { Success, InternalError, NotReady, InvalidAmtMode, InvalidMessageLength, } #[derive(ToString)] #[repr(u8)] enum FuAmtProvisioningState { Unprovisioned, BeingProvisioned, Provisioned, } #[repr(u32le)] enum FuAmtHostIfCommand { ProvisioningModeRequest = 0x04000008, ProvisioningStateRequest = 0x04000011, CodeVersionsRequest = 0x0400001A, ProvisioningModeResponse = 0x04800008, ProvisioningStateResponse = 0x04800011, CodeVersionsResponse = 0x0480001A, } #[derive(New, Default)] #[repr(C, packed)] struct FuAmtHostIfMsgCodeVersionRequest { version_major: u8 == 0x1, version_minor: u8 == 0x1, _reserved: u16le, command: FuAmtHostIfCommand == CodeVersionsRequest, length: u32le == 0x0, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuAmtHostIfMsgCodeVersionResponse { version_major: u8 == 0x1, version_minor: u8 == 0x1, _reserved: u16le, command: FuAmtHostIfCommand == CodeVersionsResponse, _length: u32le, status: u32le, _bios: [char; 65], version_count: u32le, // now variable length of FuAmtUnicodeString } #[derive(Parse)] #[repr(C, packed)] struct FuAmtUnicodeString { description_length: u16le, description_string: [char; 20], version_length: u16le, version_string: [char; 20], } #[derive(New, Default)] #[repr(C, packed)] struct FuAmtHostIfMsgProvisioningStateRequest { version_major: u8 == 0x1, version_minor: u8 == 0x1, _reserved: u16le, command: FuAmtHostIfCommand == ProvisioningStateRequest, length: u32le == 0x0, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuAmtHostIfMsgProvisioningStateResponse { version_major: u8 == 0x1, version_minor: u8 == 0x1, _reserved: u16le, command: FuAmtHostIfCommand == ProvisioningStateResponse, length: u32le == 0x8, status: u32le, provisioning_state: FuAmtProvisioningState, } fwupd-2.0.10/plugins/intel-amt/intel-amt.quirk000066400000000000000000000001361501337203100212340ustar00rootroot00000000000000# PTHI client (via the HECI device) [12f80028-b4b7-4b2d-aca8-46e0ff65814c] Plugin = intel_amt fwupd-2.0.10/plugins/intel-amt/meson.build000066400000000000000000000011141501337203100204240ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginIntelAmt"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('intel-amt.quirk') plugin_builtins += static_library('fu_plugin_intel_amt', rustgen.process( 'fu-intel-amt.rs', ), sources: [ 'fu-intel-amt-plugin.c', 'fu-intel-amt-device.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) enumeration_data += files('tests/intel-amt-setup.json') device_tests += files('tests/intel-amt.json') endif fwupd-2.0.10/plugins/intel-amt/tests/000077500000000000000000000000001501337203100174275ustar00rootroot00000000000000fwupd-2.0.10/plugins/intel-amt/tests/intel-amt-setup.json000066400000000000000000000342011501337203100233520ustar00rootroot00000000000000{ "FwupdVersion": "2.0.9", "UsbDevices": [ { "Created": "2025-04-28T11:16:00.979005Z", "GType": "FuUdevDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:16.0/mei/mei0", "DeviceFile": "/dev/mei0", "Subsystem": "mei", "Vendor": 32902, "Model": 41274, "Events": [ { "Id": "GetSymlinkTarget:Attr=subsystem", "Data": "mei" }, { "Id": "GetSymlinkTarget:Attr=driver" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "MAJOR=235\nMINOR=0\nDEVNAME=mei0" }, { "Id": "ReadProp:Key=DEVNAME", "Data": "mei0" }, { "Id": "ReadAttr:Attr=vendor" }, { "Id": "ReadAttr:Attr=device" }, { "Id": "ReadAttr:Attr=class" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "GetBackendParent:Subsystem=pci", "GType": "FuPciDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:16.0", "PhysicalId": "PCI_SLOT_NAME=0000:00:16.0" }, { "Id": "GetSymlinkTarget:Attr=subsystem", "Data": "pci" }, { "Id": "GetSymlinkTarget:Attr=driver", "Data": "mei_me" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "DRIVER=mei_me\nPCI_CLASS=78000\nPCI_ID=8086:A13A\nPCI_SUBSYS_ID=17AA:222E\nPCI_SLOT_NAME=0000:00:16.0\nMODALIAS=pci:v00008086d0000A13Asv000017AAsd0000222Ebc07sc80i00" }, { "Id": "ReadProp:Key=DEVNAME" }, { "Id": "ReadAttr:Attr=vendor", "Data": "0x8086" }, { "Id": "ReadAttr:Attr=device", "Data": "0xa13a" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "DRIVER=mei_me\nPCI_CLASS=78000\nPCI_ID=8086:A13A\nPCI_SUBSYS_ID=17AA:222E\nPCI_SLOT_NAME=0000:00:16.0\nMODALIAS=pci:v00008086d0000A13Asv000017AAsd0000222Ebc07sc80i00" }, { "Id": "ReadProp:Key=DEVNAME" }, { "Id": "ReadAttr:Attr=vendor", "Data": "0x8086" }, { "Id": "ReadAttr:Attr=device", "Data": "0xa13a" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "GetBackendParent:Subsystem=i2c", "Error": 8, "ErrorMsg": "no parent with subsystem i2c" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadAttr:Attr=revision", "Data": "0x31" }, { "Id": "ReadAttr:Attr=subsystem_vendor", "Data": "0x17aa" }, { "Id": "ReadAttr:Attr=subsystem_device", "Data": "0x222e" }, { "Id": "ReadProp:Key=PCI_SLOT_NAME", "Data": "0000:00:16.0" }, { "Id": "GetBackendParent:Subsystem=(null)", "GType": "FuPciDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:16.0", "PhysicalId": "PCI_SLOT_NAME=0000:00:16.0" }, { "Id": "GetSymlinkTarget:Attr=subsystem", "Data": "pci" }, { "Id": "GetSymlinkTarget:Attr=driver", "Data": "mei_me" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "DRIVER=mei_me\nPCI_CLASS=78000\nPCI_ID=8086:A13A\nPCI_SUBSYS_ID=17AA:222E\nPCI_SLOT_NAME=0000:00:16.0\nMODALIAS=pci:v00008086d0000A13Asv000017AAsd0000222Ebc07sc80i00" }, { "Id": "ReadProp:Key=DEVNAME" }, { "Id": "ReadAttr:Attr=vendor", "Data": "0x8086" }, { "Id": "ReadAttr:Attr=device", "Data": "0xa13a" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "DRIVER=mei_me\nPCI_CLASS=78000\nPCI_ID=8086:A13A\nPCI_SUBSYS_ID=17AA:222E\nPCI_SLOT_NAME=0000:00:16.0\nMODALIAS=pci:v00008086d0000A13Asv000017AAsd0000222Ebc07sc80i00" }, { "Id": "ReadProp:Key=DEVNAME" }, { "Id": "ReadAttr:Attr=vendor", "Data": "0x8086" }, { "Id": "ReadAttr:Attr=device", "Data": "0xa13a" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "GetBackendParent:Subsystem=i2c", "Error": 8, "ErrorMsg": "no parent with subsystem i2c" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadAttr:Attr=revision", "Data": "0x31" }, { "Id": "ReadAttr:Attr=subsystem_vendor", "Data": "0x17aa" }, { "Id": "ReadAttr:Attr=subsystem_device", "Data": "0x222e" }, { "Id": "ReadProp:Key=PCI_SLOT_NAME", "Data": "0000:00:16.0" }, { "Id": "ListAttr", "Data": "uevent\npower_state\nbroken_parity_status\nsubsystem_device\ndma_mask_bits\n0000:00:16.0-3c4852d6-d47b-4f46-b05e-b5edc1aa440e\nvendor\nlocal_cpus\npower\n0000:00:16.0-dba4d603-d7ed-4931-8823-17ad585705d5\nclass\nnuma_node\nresource\nrescan\n0000:00:16.0-05b79a6f-4628-4d7f-899d-a91514cb32ab\nmei\nmsi_bus\ndevice\n0000:00:16.0-082ee5a7-7c25-470a-9643-0c06f0466ea1\n0000:00:16.0-01e88543-8050-4380-9d6f-4f9cec704917\n0000:00:16.0-bb875e12-cb58-4d14-ae93-8566183c66c7\n0000:00:16.0-42b3ce2f-bd9f-485a-96ae-26406230b1ff\n0000:00:16.0-309dcde8-ccb1-4062-8f78-600115a34327\n0000:00:16.0-55213584-9a29-4916-badf-0fb7ed682aeb\ndriver\n0000:00:16.0-8e6a6715-9abc-4043-88ef-9e39c6f63e0f\nlocal_cpulist\n0000:00:16.0-8c2f4425-77d6-4755-aca3-891fdbc66a58\n0000:00:16.0-cea154ea-8ff5-4f94-9290-0bb7355a34db\ndriver_override\nsubsystem\nd3cold_allowed\nirq\nrevision\n0000:00:16.0-fbf6fcf1-96cf-4e2e-a6a6-1bab8cbe36b1\nconsistent_dma_mask_bits\nresource0\nconfig\nari_enabled\nmsi_irqs\nremove\n0000:00:16.0-b638ab7e-94e2-4ea2-a552-d1c54b627f04\nenable\nlink\n0000:00:16.0-f908627d-13bf-4a04-b91f-a64e9245323d\n0000:00:16.0-12f80028-b4b7-4b2d-aca8-46e0ff65814c\nmodalias\nsubsystem_vendor\n0000:00:16.0-5565a099-7fe2-45c1-a22b-d7e9dfea9a2e" }, { "Id": "GetBackendParent:Subsystem=pci", "GType": "FuPciDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:16.0", "PhysicalId": "PCI_SLOT_NAME=0000:00:16.0" }, { "Id": "GetSymlinkTarget:Attr=subsystem", "Data": "pci" }, { "Id": "GetSymlinkTarget:Attr=driver", "Data": "mei_me" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "DRIVER=mei_me\nPCI_CLASS=78000\nPCI_ID=8086:A13A\nPCI_SUBSYS_ID=17AA:222E\nPCI_SLOT_NAME=0000:00:16.0\nMODALIAS=pci:v00008086d0000A13Asv000017AAsd0000222Ebc07sc80i00" }, { "Id": "ReadProp:Key=DEVNAME" }, { "Id": "ReadAttr:Attr=vendor", "Data": "0x8086" }, { "Id": "ReadAttr:Attr=device", "Data": "0xa13a" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "DRIVER=mei_me\nPCI_CLASS=78000\nPCI_ID=8086:A13A\nPCI_SUBSYS_ID=17AA:222E\nPCI_SLOT_NAME=0000:00:16.0\nMODALIAS=pci:v00008086d0000A13Asv000017AAsd0000222Ebc07sc80i00" }, { "Id": "ReadProp:Key=DEVNAME" }, { "Id": "ReadAttr:Attr=vendor", "Data": "0x8086" }, { "Id": "ReadAttr:Attr=device", "Data": "0xa13a" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "GetBackendParent:Subsystem=i2c", "Error": 8, "ErrorMsg": "no parent with subsystem i2c" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadAttr:Attr=revision", "Data": "0x31" }, { "Id": "ReadAttr:Attr=subsystem_vendor", "Data": "0x17aa" }, { "Id": "ReadAttr:Attr=subsystem_device", "Data": "0x222e" }, { "Id": "ReadProp:Key=PCI_SLOT_NAME", "Data": "0000:00:16.0" }, { "Id": "GetBackendParent:Subsystem=(null)", "GType": "FuPciDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:16.0", "PhysicalId": "PCI_SLOT_NAME=0000:00:16.0" }, { "Id": "GetSymlinkTarget:Attr=subsystem", "Data": "pci" }, { "Id": "GetSymlinkTarget:Attr=driver", "Data": "mei_me" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "DRIVER=mei_me\nPCI_CLASS=78000\nPCI_ID=8086:A13A\nPCI_SUBSYS_ID=17AA:222E\nPCI_SLOT_NAME=0000:00:16.0\nMODALIAS=pci:v00008086d0000A13Asv000017AAsd0000222Ebc07sc80i00" }, { "Id": "ReadProp:Key=DEVNAME" }, { "Id": "ReadAttr:Attr=vendor", "Data": "0x8086" }, { "Id": "ReadAttr:Attr=device", "Data": "0xa13a" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "DRIVER=mei_me\nPCI_CLASS=78000\nPCI_ID=8086:A13A\nPCI_SUBSYS_ID=17AA:222E\nPCI_SLOT_NAME=0000:00:16.0\nMODALIAS=pci:v00008086d0000A13Asv000017AAsd0000222Ebc07sc80i00" }, { "Id": "ReadProp:Key=DEVNAME" }, { "Id": "ReadAttr:Attr=vendor", "Data": "0x8086" }, { "Id": "ReadAttr:Attr=device", "Data": "0xa13a" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "GetBackendParent:Subsystem=i2c", "Error": 8, "ErrorMsg": "no parent with subsystem i2c" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadAttr:Attr=revision", "Data": "0x31" }, { "Id": "ReadAttr:Attr=subsystem_vendor", "Data": "0x17aa" }, { "Id": "ReadAttr:Attr=subsystem_device", "Data": "0x222e" }, { "Id": "ReadProp:Key=PCI_SLOT_NAME", "Data": "0000:00:16.0" }, { "Id": "ListAttr", "Data": "uevent\npower_state\nbroken_parity_status\nsubsystem_device\ndma_mask_bits\n0000:00:16.0-3c4852d6-d47b-4f46-b05e-b5edc1aa440e\nvendor\nlocal_cpus\npower\n0000:00:16.0-dba4d603-d7ed-4931-8823-17ad585705d5\nclass\nnuma_node\nresource\nrescan\n0000:00:16.0-05b79a6f-4628-4d7f-899d-a91514cb32ab\nmei\nmsi_bus\ndevice\n0000:00:16.0-082ee5a7-7c25-470a-9643-0c06f0466ea1\n0000:00:16.0-01e88543-8050-4380-9d6f-4f9cec704917\n0000:00:16.0-bb875e12-cb58-4d14-ae93-8566183c66c7\n0000:00:16.0-42b3ce2f-bd9f-485a-96ae-26406230b1ff\n0000:00:16.0-309dcde8-ccb1-4062-8f78-600115a34327\n0000:00:16.0-55213584-9a29-4916-badf-0fb7ed682aeb\ndriver\n0000:00:16.0-8e6a6715-9abc-4043-88ef-9e39c6f63e0f\nlocal_cpulist\n0000:00:16.0-8c2f4425-77d6-4755-aca3-891fdbc66a58\n0000:00:16.0-cea154ea-8ff5-4f94-9290-0bb7355a34db\ndriver_override\nsubsystem\nd3cold_allowed\nirq\nrevision\n0000:00:16.0-fbf6fcf1-96cf-4e2e-a6a6-1bab8cbe36b1\nconsistent_dma_mask_bits\nresource0\nconfig\nari_enabled\nmsi_irqs\nremove\n0000:00:16.0-b638ab7e-94e2-4ea2-a552-d1c54b627f04\nenable\nlink\n0000:00:16.0-f908627d-13bf-4a04-b91f-a64e9245323d\n0000:00:16.0-12f80028-b4b7-4b2d-aca8-46e0ff65814c\nmodalias\nsubsystem_vendor\n0000:00:16.0-5565a099-7fe2-45c1-a22b-d7e9dfea9a2e" }, { "Id": "Ioctl:Request=0xc0104801,Data=KAD4Ere0LUusqEbg/2WBTA==,Length=0x10", "DataOut": "ABQAAAG0LUusqEbg/2WBTA==" }, { "Id": "Write:Data=AQEAABoAAAQAAAAA,Length=0xc" }, { "Id": "Read:Length=0x1400", "Data": "AQEAABoAgAQBAgAAAAAAAE4xRUVUQTJXICgxLjc1ICkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAAAAUARmxhc2gAAAAAAAAAAAAAAAAAAAAHADExLjguOTMAAAAAAAAAAAAAAAAACABOZXRzdGFjawAAAAAAAAAAAAAAAAcAMTEuOC45MwAAAAAAAAAAAAAAAAAHAEFNVEFwcHMAAAAAAAAAAAAAAAAABwAxMS44LjkzAAAAAAAAAAAAAAAAAAMAQU1UAAAAAAAAAAAAAAAAAAAAAAAHADExLjguOTMAAAAAAAAAAAAAAAAAAwBTa3UAAAAAAAAAAAAAAAAAAAAAAAEAOAAAAAAAAAAAAAAAAAAAAAAAAAAIAFZlbmRvcklEAAAAAAAAAAAAAAAABAA4MDg2AAAAAAAAAAAAAAAAAAAAAAwAQnVpbGQgTnVtYmVyAAAAAAAAAAAEADQzMjMAAAAAAAAAAAAAAAAAAAAAEABSZWNvdmVyeSBWZXJzaW9uAAAAAAcAMTEuOC45MwAAAAAAAAAAAAAAAAASAFJlY292ZXJ5IEJ1aWxkIE51bQAABAA0MzIzAAAAAAAAAAAAAAAAAAAAAAsATGVnYWN5IE1vZGUAAAAAAAAAAAAFAEZhbHNlAAAAAAAAAAAAAAAAAAAA" }, { "Id": "Write:Data=AQEAABEAAAQAAAAA,Length=0xc" }, { "Id": "Read:Length=0x1400", "Data": "AQEAABEAgAQIAAAAAAAAAAAAAAA=" } ] } ] } fwupd-2.0.10/plugins/intel-amt/tests/intel-amt.json000066400000000000000000000005011501337203100222100ustar00rootroot00000000000000{ "name": "Intel AMT", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/intel-amt-setup.json", "components": [ { "version": "11.8.93.4323", "guids": [ "d39310dc-e5d7-5eb2-945d-22011cbd3157" ] } ] } ] } fwupd-2.0.10/plugins/intel-cvs/000077500000000000000000000000001501337203100162775ustar00rootroot00000000000000fwupd-2.0.10/plugins/intel-cvs/README.md000066400000000000000000000035001501337203100175540ustar00rootroot00000000000000--- title: Plugin: Intel Computer Vision Sensing --- ## Introduction The Computer Vision Sensing camera is built by various silicon vendors and supported by Intel. The Visual Sensing Controller a companion chip that helps make PCs more smart using AI. For example, it could be used to detect the presence of a user and automatically adjust the brightness of the screen. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a packed binary file format. This plugin supports the following protocol ID: * `com.intel.cvs` ## GUID Generation These devices match from the standard I²C instance ID values, e.g. * `IC2\NAME_INTC10DE:00` The devices then create firmware matchable instance ID values which include the product PID, VID and an additional optional OEM PID, e.g. * `IC2\NAME_INTC10DE:00&VID_06CB&PID_06CB` * `IC2\NAME_INTC10DE:00&VID_06CB&PID_06CB&OPID_12345678` ## Update Behavior The device is probed by reading `cvs_ctrl_data_pre`. The firmware update is triggered by writing to `cvs_ctrl_data_pre` and status is reported by polling `cvs_ctrl_data_fwupd`. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x06CB` ## Quirk Use This plugin uses the following plugin-specific quirks: ### IntelCvsMaxDownloadTime The device maximum download time in ms, which defaults to 200s. Since: 2.0.7 ### IntelCvsMaxRetryCount The device maximum retry count, which defaults to 5. Since: 2.0.7 ## External Interface Access This plugin requires read/write access to `/dev/i2c*`. ## Version Considerations This plugin has been available since fwupd version `2.0.7`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Richard Hughes: @hughsie fwupd-2.0.10/plugins/intel-cvs/fu-intel-cvs-device.c000066400000000000000000000247621501337203100222270ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-intel-cvs-device.h" #include "fu-intel-cvs-firmware.h" #include "fu-intel-cvs-struct.h" #define FU_INTEL_CVS_DEVICE_SYSFS_TIMEOUT 500 /* ms */ struct _FuIntelCvsDevice { FuI2cDevice parent_instance; guint32 max_download_time; guint32 max_retry_count; }; G_DEFINE_TYPE(FuIntelCvsDevice, fu_intel_cvs_device, FU_TYPE_I2C_DEVICE) static void fu_intel_cvs_device_to_string(FuDevice *device, guint idt, GString *str) { FuIntelCvsDevice *self = FU_INTEL_CVS_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "MaxDownloadTime", self->max_download_time); fwupd_codec_string_append_hex(str, idt, "MaxRetryCount", self->max_retry_count); } static gboolean fu_intel_cvs_device_setup(FuDevice *device, GError **error) { FuIntelCvsDevice *self = FU_INTEL_CVS_DEVICE(device); g_autofree gchar *version = NULL; g_autoptr(FuStructIntelCvsProbe) st_probe = NULL; g_autoptr(GBytes) blob = NULL; /* read and parse the status */ blob = fu_udev_device_read_sysfs_bytes(FU_UDEV_DEVICE(self), "cvs_ctrl_data_pre", FU_STRUCT_INTEL_CVS_PROBE_SIZE, FU_INTEL_CVS_DEVICE_SYSFS_TIMEOUT, error); if (blob == NULL) return FALSE; st_probe = fu_struct_intel_cvs_probe_parse_bytes(blob, 0x0, error); if (st_probe == NULL) return FALSE; /* production, so no downgrades */ if (fu_struct_intel_cvs_probe_get_dev_capabilities(st_probe) & FU_STRUCT_INTEL_CVS_DEV_CAPABILITY_FW_ANTIROLLBACK) fu_device_add_flag(device, FWUPD_DEVICE_FLAG_ONLY_VERSION_UPGRADE); /* build the version */ version = g_strdup_printf("%x.%x.%x.%x", fu_struct_intel_cvs_probe_get_major(st_probe), fu_struct_intel_cvs_probe_get_minor(st_probe), fu_struct_intel_cvs_probe_get_hotfix(st_probe), fu_struct_intel_cvs_probe_get_build(st_probe)); fu_device_set_version(device, version); /* build the two instance IDs */ fu_device_set_vid(device, fu_struct_intel_cvs_probe_get_vid(st_probe)); fu_device_set_pid(device, fu_struct_intel_cvs_probe_get_pid(st_probe)); if (fu_struct_intel_cvs_probe_get_opid(st_probe) != 0x0) { fu_device_add_instance_u32(device, "OPID", fu_struct_intel_cvs_probe_get_opid(st_probe)); if (!fu_device_build_instance_id(device, error, "I2C", "NAME", "VID", "PID", "OPID", NULL)) return FALSE; } return fu_device_build_instance_id(device, error, "I2C", "NAME", "VID", "PID", NULL); } static void fu_intel_cvs_device_vid_notify_cb(FuDevice *device, GParamSpec *pspec, gpointer user_data) { fu_device_add_instance_u16(device, "VID", fu_device_get_vid(device)); /* although the CVS vendor ID is supposed to be allocated by Intel for each CV chip vendor * it is essentially always the USB VID -- just use that to get the vendor name */ fu_device_build_vendor_id_u16(device, "USB", fu_device_get_vid(device)); } static void fu_intel_cvs_device_pid_notify_cb(FuDevice *device, GParamSpec *pspec, gpointer user_data) { fu_device_add_instance_u16(device, "PID", fu_device_get_pid(device)); } static FuFirmware * fu_intel_cvs_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuIntelCvsDevice *self = FU_INTEL_CVS_DEVICE(device); g_autoptr(FuFirmware) firmware = fu_intel_cvs_firmware_new(); /* check is compatible */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; if (fu_device_get_vid(FU_DEVICE(self)) != fu_intel_cvs_firmware_get_vid(FU_INTEL_CVS_FIRMWARE(firmware)) || fu_device_get_pid(FU_DEVICE(self)) != fu_intel_cvs_firmware_get_pid(FU_INTEL_CVS_FIRMWARE(firmware))) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid firmware, got %04x:%04x, expected %04x:%04x", fu_intel_cvs_firmware_get_vid(FU_INTEL_CVS_FIRMWARE(firmware)), fu_intel_cvs_firmware_get_pid(FU_INTEL_CVS_FIRMWARE(firmware)), fu_device_get_vid(FU_DEVICE(self)), fu_device_get_pid(FU_DEVICE(self))); return NULL; } return g_steal_pointer(&firmware); } static gboolean fu_intel_cvs_device_check_status_cb(FuDevice *device, gpointer user_data, GError **error) { FuProgress *progress = FU_PROGRESS(user_data); g_autoptr(FuStructIntelCvsStatus) st_status = NULL; g_autoptr(GBytes) blob = NULL; /* read and parse the status */ blob = fu_udev_device_read_sysfs_bytes(FU_UDEV_DEVICE(device), "cvs_ctrl_data_fwupd", FU_STRUCT_INTEL_CVS_PROBE_SIZE, FU_INTEL_CVS_DEVICE_SYSFS_TIMEOUT, error); if (blob == NULL) return FALSE; st_status = fu_struct_intel_cvs_status_parse_bytes(blob, 0x0, error); if (st_status == NULL) return FALSE; fu_progress_set_percentage_full(progress, fu_struct_intel_cvs_status_get_num_packets_sent(st_status), fu_struct_intel_cvs_status_get_total_packets(st_status)); if (fu_struct_intel_cvs_status_get_fw_dl_finished(st_status) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "waiting for update to complete"); return FALSE; } /* this will be implemented in the future release */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_BUSY); /* success */ return TRUE; } static gboolean fu_intel_cvs_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuIntelCvsDevice *self = FU_INTEL_CVS_DEVICE(device); g_autoptr(FuIOChannel) io_payload = NULL; g_autoptr(FuStructIntelCvsWrite) st_write = fu_struct_intel_cvs_write_new(); g_autoptr(GError) error_local = NULL; g_autoptr(GInputStream) stream = NULL; /* get default image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* write firmware stream into a virtual fd */ io_payload = fu_io_channel_virtual_new("fwupd-cvs-plugin", error); if (io_payload == NULL) return FALSE; if (!fu_io_channel_write_stream(io_payload, stream, FU_INTEL_CVS_DEVICE_SYSFS_TIMEOUT, FU_IO_CHANNEL_FLAG_NONE, error)) { g_prefix_error(error, "failed to write payload to virtual stream: "); return FALSE; } if (!fu_io_channel_seek(io_payload, 0x0, error)) return FALSE; /* write */ fu_struct_intel_cvs_write_set_max_download_time(st_write, self->max_download_time); fu_struct_intel_cvs_write_set_max_flash_time(st_write, fu_device_get_remove_delay(device)); fu_struct_intel_cvs_write_set_max_fwupd_retry_count(st_write, self->max_retry_count); fu_struct_intel_cvs_write_set_fw_bin_fd(st_write, fu_io_channel_unix_get_fd(io_payload)); if (!fu_udev_device_write_sysfs_byte_array(FU_UDEV_DEVICE(self), "cvs_ctrl_data_pre", st_write, FU_INTEL_CVS_DEVICE_SYSFS_TIMEOUT, error)) return FALSE; /* poll the status */ if (!fu_device_retry_full(device, fu_intel_cvs_device_check_status_cb, self->max_download_time * self->max_retry_count / 1000, 1000, /* ms */ progress, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_debug("ignoring: %s", error_local->message); } else { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } /* success */ return TRUE; } static gboolean fu_intel_cvs_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuIntelCvsDevice *self = FU_INTEL_CVS_DEVICE(device); /* all optional */ if (g_strcmp0(key, "IntelCvsMaxDownloadTime") == 0) { guint64 tmp = 0; if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->max_download_time = tmp; return TRUE; } if (g_strcmp0(key, "IntelCvsMaxRetryCount") == 0) { guint64 tmp = 0; if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->max_retry_count = tmp; return TRUE; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_intel_cvs_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 57, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 43, "reload"); } static void fu_intel_cvs_device_init(FuIntelCvsDevice *self) { self->max_download_time = 200000; self->max_retry_count = 5; fu_device_set_remove_delay(FU_DEVICE(self), 200000); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_add_protocol(FU_DEVICE(self), "com.intel.cvs"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SELF_RECOVERY); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_REBOOT); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_VIDEO_CAMERA); fu_device_set_name(FU_DEVICE(self), "Camera"); fu_device_set_summary(FU_DEVICE(self), "Computer Vision Sensing Camera"); fu_device_retry_add_recovery(FU_DEVICE(self), FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, NULL); g_signal_connect(FU_DEVICE(self), "notify::vid", G_CALLBACK(fu_intel_cvs_device_vid_notify_cb), NULL); g_signal_connect(FU_DEVICE(self), "notify::pid", G_CALLBACK(fu_intel_cvs_device_pid_notify_cb), NULL); } static void fu_intel_cvs_device_class_init(FuIntelCvsDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_intel_cvs_device_to_string; device_class->setup = fu_intel_cvs_device_setup; device_class->prepare_firmware = fu_intel_cvs_device_prepare_firmware; device_class->write_firmware = fu_intel_cvs_device_write_firmware; device_class->set_quirk_kv = fu_intel_cvs_device_set_quirk_kv; device_class->set_progress = fu_intel_cvs_device_set_progress; } fwupd-2.0.10/plugins/intel-cvs/fu-intel-cvs-device.h000066400000000000000000000004711501337203100222230ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_INTEL_CVS_DEVICE (fu_intel_cvs_device_get_type()) G_DECLARE_FINAL_TYPE(FuIntelCvsDevice, fu_intel_cvs_device, FU, INTEL_CVS_DEVICE, FuI2cDevice) fwupd-2.0.10/plugins/intel-cvs/fu-intel-cvs-firmware.c000066400000000000000000000064411501337203100225760ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-intel-cvs-firmware.h" #include "fu-intel-cvs-struct.h" struct _FuIntelCvsFirmware { FuFirmware parent_instance; guint16 vid; guint16 pid; }; G_DEFINE_TYPE(FuIntelCvsFirmware, fu_intel_cvs_firmware, FU_TYPE_FIRMWARE) static void fu_intel_cvs_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuIntelCvsFirmware *self = FU_INTEL_CVS_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "vid", self->vid); fu_xmlb_builder_insert_kx(bn, "pid", self->pid); } static gboolean fu_intel_cvs_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_intel_cvs_firmware_hdr_validate_stream(stream, offset, error); } static gboolean fu_intel_cvs_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuIntelCvsFirmware *self = FU_INTEL_CVS_FIRMWARE(firmware); g_autofree gchar *version = NULL; guint32 checksum_new; g_autoptr(FuStructIntelCvsFirmwareHdr) st_hdr = NULL; g_autoptr(FuStructIntelCvsId) st_id = NULL; g_autoptr(FuStructIntelCvsFw) st_fw = NULL; /* parse */ st_hdr = fu_struct_intel_cvs_firmware_hdr_parse_stream(stream, 0x0, error); if (st_hdr == NULL) return FALSE; /* verify checksum of header */ checksum_new = fu_sum32w(st_hdr->data, st_hdr->len, G_LITTLE_ENDIAN); if (checksum_new != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid header checksum, got 0x%x excess", checksum_new); return FALSE; } /* get the VID/PID */ st_id = fu_struct_intel_cvs_firmware_hdr_get_vid_pid(st_hdr); self->vid = fu_struct_intel_cvs_id_get_vid(st_id); self->pid = fu_struct_intel_cvs_id_get_pid(st_id); /* quad version */ st_fw = fu_struct_intel_cvs_firmware_hdr_get_fw_version(st_hdr); version = g_strdup_printf("%u.%u.%u.%u", fu_struct_intel_cvs_fw_get_major(st_fw), fu_struct_intel_cvs_fw_get_minor(st_fw), fu_struct_intel_cvs_fw_get_hotfix(st_fw), fu_struct_intel_cvs_fw_get_build(st_fw)); fu_firmware_set_version(firmware, version); return TRUE; } guint16 fu_intel_cvs_firmware_get_vid(FuIntelCvsFirmware *self) { g_return_val_if_fail(FU_IS_INTEL_CVS_FIRMWARE(self), G_MAXUINT16); return self->vid; } guint16 fu_intel_cvs_firmware_get_pid(FuIntelCvsFirmware *self) { g_return_val_if_fail(FU_IS_INTEL_CVS_FIRMWARE(self), G_MAXUINT16); return self->pid; } static void fu_intel_cvs_firmware_init(FuIntelCvsFirmware *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_VID_PID); fu_firmware_set_version_format(FU_FIRMWARE(self), FWUPD_VERSION_FORMAT_QUAD); } static void fu_intel_cvs_firmware_class_init(FuIntelCvsFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_intel_cvs_firmware_validate; firmware_class->parse = fu_intel_cvs_firmware_parse; firmware_class->export = fu_intel_cvs_firmware_export; } FuFirmware * fu_intel_cvs_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_INTEL_CVS_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/intel-cvs/fu-intel-cvs-firmware.h000066400000000000000000000007631501337203100226040ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_INTEL_CVS_FIRMWARE (fu_intel_cvs_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuIntelCvsFirmware, fu_intel_cvs_firmware, FU, INTEL_CVS_FIRMWARE, FuFirmware) FuFirmware * fu_intel_cvs_firmware_new(void); guint16 fu_intel_cvs_firmware_get_vid(FuIntelCvsFirmware *self); guint16 fu_intel_cvs_firmware_get_pid(FuIntelCvsFirmware *self); fwupd-2.0.10/plugins/intel-cvs/fu-intel-cvs-plugin.c000066400000000000000000000021451501337203100222550ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-intel-cvs-device.h" #include "fu-intel-cvs-firmware.h" #include "fu-intel-cvs-plugin.h" struct _FuIntelCvsPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuIntelCvsPlugin, fu_intel_cvs_plugin, FU_TYPE_PLUGIN) static void fu_intel_cvs_plugin_init(FuIntelCvsPlugin *self) { } static void fu_intel_cvs_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "IntelCvsMaxDownloadTime"); fu_context_add_quirk_key(ctx, "IntelCvsMaxFlashTime"); fu_context_add_quirk_key(ctx, "IntelCvsMaxRetryCount"); fu_plugin_add_udev_subsystem(plugin, "i2c"); fu_plugin_add_device_gtype(plugin, FU_TYPE_INTEL_CVS_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_INTEL_CVS_FIRMWARE); } static void fu_intel_cvs_plugin_class_init(FuIntelCvsPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_intel_cvs_plugin_constructed; } fwupd-2.0.10/plugins/intel-cvs/fu-intel-cvs-plugin.h000066400000000000000000000003641501337203100222630ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuIntelCvsPlugin, fu_intel_cvs_plugin, FU, INTEL_CVS_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/intel-cvs/fu-intel-cvs.rs000066400000000000000000000042141501337203100211620ustar00rootroot00000000000000// Copyright 2024 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later struct FuStructIntelCvsFw { major: u32le, minor: u32le, hotfix: u32le, build: u32le, } struct FuStructIntelCvsId { vid: u16le, pid: u16le, } #[derive(ValidateStream, ParseStream, Default)] struct FuStructIntelCvsFirmwareHdr { magic_number: [char; 8] == "VISSOCFW", fw_version: FuStructIntelCvsFw, vid_pid: FuStructIntelCvsId, fw_offset: u32le, reserved: [u8; 220], header_checksum: u32le, } // CV SoC device status */ #[repr(u8)] enum FuIntelCvsDeviceState { DeviceOff = 0, PrivacyOn = 1 << 0, DeviceOn = 1 << 1, SensorOwner = 1 << 2, DeviceDwnldState = 1 << 4, DeviceDwnldError = 1 << 6, DeviceDwnldBusy = 1 << 7, }; // CVS sensor Status */ #[repr(u8)] enum FuIntelCvsSensorState { Released = 0, VisionAcquired = 1 << 0, IpuAcquired = 1 << 1, }; // CVS driver Status */ #[repr(u8)] enum FuIntelCvsState { Init = 0, FwDownloading = 1 << 1, FwFlashing = 1 << 3, Stopping = 1 << 7, } #[repr(u8)] enum FuIntelCvsCameraOwner { None, Cvs, Ipu, } #[repr(u32le)] enum FuStructIntelCvsDevCapability { CvPowerDomain = 1 << 10, NocameraDuringFwupdate = 1 << 11, FwupdateResetRequired = 1 << 12, Privacy2visiondriver = 1 << 13, FwAntirollback = 1 << 14, HostMipiConfigRequired = 1 << 15, } #[derive(ParseBytes)] struct FuStructIntelCvsProbe { major: u32le, minor: u32le, hotfix: u32le, build: u32le, vid: u16le, pid: u16le, opid: u32le, // unused by most ODMs dev_capabilities: FuStructIntelCvsDevCapability, } #[derive(New)] struct FuStructIntelCvsWrite { max_download_time: u32le, max_flash_time: u32le, max_fwupd_retry_count: u32le, fw_bin_fd: i32, } #[derive(ParseBytes)] struct FuStructIntelCvsStatus { dev_state: FuIntelCvsDeviceState, fw_upd_retries: u32le, total_packets: u32le, num_packets_sent: u32le, fw_dl_finished: u8, // 0=in-progress, 1=finished fw_dl_status_code: u32le, } fwupd-2.0.10/plugins/intel-cvs/intel-cvs.quirk000066400000000000000000000006501501337203100212610ustar00rootroot00000000000000# Intel MTL [I2C\NAME_INTC10CF:00] Plugin = intel_cvs # Intel LNL [I2C\NAME_INTC10DE:00] Plugin = intel_cvs # Intel ARL [I2C\NAME_INTC10E0:00] Plugin = intel_cvs # Intel PTL [I2C\NAME_INTC10E1:00] Plugin = intel_cvs # Intel NVL [I2C\NAME_INTC10FA:00] Plugin = intel_cvs # Synaptics Foo Bar Baz #[I2C\NAME_INTC10DE:00&VID_06CB&PID_0701] #IntelCvsMaxDownloadTime = 200000 #IntelCvsMaxRetryCount = 5 #RemoveDelay = 200000 fwupd-2.0.10/plugins/intel-cvs/meson.build000066400000000000000000000011121501337203100204340ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginIntelCvs"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('intel-cvs.quirk') plugin_builtins += static_library('fu_plugin_intel_cvs', rustgen.process('fu-intel-cvs.rs'), sources: [ 'fu-intel-cvs-device.c', 'fu-intel-cvs-firmware.c', 'fu-intel-cvs-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files( 'tests/dell-xps-9350.json', ) enumeration_data += files( 'tests/dell-xps-9350-setup.json', ) fwupd-2.0.10/plugins/intel-cvs/tests/000077500000000000000000000000001501337203100174415ustar00rootroot00000000000000fwupd-2.0.10/plugins/intel-cvs/tests/dell-xps-9350-setup.json000066400000000000000000000037471501337203100236330ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUdevDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:14.0/usb3/3-2/3-2:1.0/usbio-i2c.2.auto/i2c-2/i2c-INTC10DE:00", "DeviceFile": "/dev/i2c-2", "Subsystem": "i2c", "Driver": "Intel CVS driver", "Vendor": 10945, "Model": 8400, "Created": "2025-02-27T10:34:12.925282Z", "Events": [ { "Id": "#d5a801ad", "Data": "i2c" }, { "Id": "#ad8c58d3", "Data": "Intel CVS driver" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "DRIVER=Intel CVS driver\nMODALIAS=acpi:INTC10DE:" }, { "Id": "#d432c663" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "DRIVER=Intel CVS driver\nMODALIAS=acpi:INTC10DE:" }, { "Id": "#d432c663" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c" }, { "Id": "#e371a663", "Data": "INTC10DE:00" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "DRIVER=Intel CVS driver\nMODALIAS=acpi:INTC10DE:" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c" }, { "Id": "#e371a663", "Data": "INTC10DE:00" }, { "Id": "#d46702ba", "Data": "AQAAABMAAAAAAAAAAAAAAMEq0CAAAAAAAAAAAA==" } ] } ] } fwupd-2.0.10/plugins/intel-cvs/tests/dell-xps-9350.json000066400000000000000000000005711501337203100224650ustar00rootroot00000000000000{ "name": "Dell XPS 9350 [CVS Camera]", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/dell-xps-9350-setup.json", "components": [ { "version": "1.13.0.0", "protocol": "com.intel.cvs", "guids": [ "6344aa58-fe82-5e4d-b981-847f422e67c2" ] } ] } ] } fwupd-2.0.10/plugins/intel-gsc/000077500000000000000000000000001501337203100162605ustar00rootroot00000000000000fwupd-2.0.10/plugins/intel-gsc/README.md000066400000000000000000000026401501337203100175410ustar00rootroot00000000000000--- title: Plugin: Intel GSC — Graphics System Controller --- ## Introduction This plugin is used to update the Intel graphics system controller via the Intel Management Engine. ## Firmware Format There are two firmware formats in use: * `$FPT` with children `FuIfwiFptFirmware`, where the `FW_DATA_IMAGE` is a `FuIfwiCpdFirmware` * A linear array of `FuOpromFirmware` images, each with a `FuIfwiCpdFirmware` This plugin supports the following protocol ID: * `com.intel.gsc` ## GUID Generation These devices use the standard PCI DeviceInstanceId values, e.g. * `PCI\VID_8086&DEV_4905` They also define custom per-part PCI IDs such as: * `PCI\VID_8086&DEV_4905&PART_FWCODE` * `PCI\VID_8086&DEV_4905&PART_FWDATA` * `PCI\VID_8086&DEV_4905&PART_OPROMCODE` * `PCI\VID_8086&DEV_4905&PART_OPROMDATA` ## Quirk Use This plugin uses the following plugin-specific quirks: ### `Flags=has-aux` Has an AUX child device. ### `Flags=has-oprom` Has an option ROM child device. ## Vendor ID Security The vendor ID is set from the PCI vendor, in this instance set to `MEI:0x8086` ## External Interface Access This plugin requires read/write access to `/dev/mei*`. ## Version Considerations This plugin has been available since fwupd version `1.8.7`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Vitaly Lubart: @vlubart fwupd-2.0.10/plugins/intel-gsc/fu-igsc-aux-device.c000066400000000000000000000146431501337203100220210ustar00rootroot00000000000000/* * Copyright 2022 Intel, Inc * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later OR Apache-2.0 */ #include "config.h" #include "fu-igsc-aux-device.h" #include "fu-igsc-aux-firmware.h" #include "fu-igsc-device.h" #include "fu-igsc-heci.h" struct _FuIgscAuxDevice { FuDevice parent_instance; guint32 oem_version; guint16 major_version; guint16 major_vcn; }; G_DEFINE_TYPE(FuIgscAuxDevice, fu_igsc_aux_device, FU_TYPE_DEVICE) static void fu_igsc_aux_device_to_string(FuDevice *device, guint idt, GString *str) { FuIgscAuxDevice *self = FU_IGSC_AUX_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "OemManufDataVersion", self->oem_version); fwupd_codec_string_append_hex(str, idt, "MajorVersion", self->major_version); fwupd_codec_string_append_hex(str, idt, "MajorVcn", self->major_vcn); } static gboolean fu_igsc_aux_device_probe(FuDevice *device, GError **error) { FuDevice *parent = fu_device_get_parent(device); /* fix name */ if (parent != NULL) { g_autofree gchar *name = NULL; name = g_strdup_printf("%s Data", fu_device_get_name(parent)); fu_device_set_name(device, name); } /* add extra instance IDs */ fu_device_add_instance_str(device, "PART", "FWDATA"); if (!fu_device_build_instance_id(device, error, "MEI", "VEN", "DEV", "PART", NULL)) return FALSE; return fu_device_build_instance_id(device, error, "MEI", "VEN", "DEV", "SUBSYS", "PART", NULL); } static gboolean fu_igsc_aux_device_setup(FuDevice *device, GError **error) { FuIgscAuxDevice *self = FU_IGSC_AUX_DEVICE(device); FuIgscDevice *igsc_parent = FU_IGSC_DEVICE(fu_device_get_parent(device)); g_autofree gchar *version = NULL; /* get version */ if (!fu_igsc_device_get_aux_version(igsc_parent, &self->oem_version, &self->major_version, &self->major_vcn, error)) { g_prefix_error(error, "failed to get aux version: "); return FALSE; } version = g_strdup_printf("%u.%x", self->major_version, self->oem_version); fu_device_set_version(device, version); /* success */ return TRUE; } static FuFirmware * fu_igsc_aux_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuIgscAuxDevice *self = FU_IGSC_AUX_DEVICE(device); FuIgscDevice *igsc_parent = FU_IGSC_DEVICE(fu_device_get_parent(device)); g_autoptr(FuIgscAuxFirmware) firmware = FU_IGSC_AUX_FIRMWARE(fu_igsc_aux_firmware_new()); /* parse container */ if (!fu_firmware_parse_stream(FU_FIRMWARE(firmware), stream, 0x0, flags, error)) return NULL; /* search the device list for a match */ if (!fu_igsc_aux_firmware_match_device(firmware, self->major_version, self->major_vcn, fu_igsc_device_get_ssvid(igsc_parent), fu_igsc_device_get_ssdid(igsc_parent), error)) return NULL; /* verify is compatible */ if (fu_igsc_aux_firmware_get_major_version(firmware) != self->major_version) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "image is not for this product, got 0x%x, expected 0x%x", fu_igsc_aux_firmware_get_major_version(firmware), self->major_version); return NULL; } if (fu_igsc_aux_firmware_get_major_vcn(firmware) > self->major_vcn) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "image VCN is not compatible, got 0x%x, expected 0x%x", fu_igsc_aux_firmware_get_major_vcn(firmware), self->major_vcn); return NULL; } if (fu_igsc_aux_firmware_get_oem_version(firmware) <= self->oem_version) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid manufacturer data version, got 0x%x, expected 0x%x", fu_igsc_aux_firmware_get_oem_version(firmware), self->oem_version); return NULL; } /* success, but return container, not CPD */ return FU_FIRMWARE(g_steal_pointer(&firmware)); } static gboolean fu_igsc_aux_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuIgscDevice *igsc_parent = FU_IGSC_DEVICE(fu_device_get_parent(device)); g_autoptr(GBytes) fw_info = NULL; g_autoptr(GInputStream) stream_payload = NULL; /* get image */ fw_info = fu_firmware_get_image_by_idx_bytes(firmware, FU_IFWI_FPT_FIRMWARE_IDX_INFO, error); if (fw_info == NULL) return FALSE; stream_payload = fu_firmware_get_image_by_idx_stream(firmware, FU_IFWI_FPT_FIRMWARE_IDX_SDTA, error); if (stream_payload == NULL) return FALSE; return fu_igsc_device_write_blob(igsc_parent, FU_IGSC_FWU_HECI_PAYLOAD_TYPE_FWDATA, fw_info, stream_payload, progress, error); } static gboolean fu_igsc_aux_device_prepare(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { /* set PCI power policy */ return fu_device_prepare(fu_device_get_parent(device), progress, flags, error); } static gboolean fu_igsc_aux_device_cleanup(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { /* set PCI power policy */ return fu_device_cleanup(fu_device_get_parent(device), progress, flags, error); } static void fu_igsc_aux_device_init(FuIgscAuxDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PAIR); fu_device_add_protocol(FU_DEVICE(self), "com.intel.gsc"); fu_device_set_logical_id(FU_DEVICE(self), "fw-data"); } static void fu_igsc_aux_device_class_init(FuIgscAuxDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_igsc_aux_device_to_string; device_class->probe = fu_igsc_aux_device_probe; device_class->setup = fu_igsc_aux_device_setup; device_class->prepare_firmware = fu_igsc_aux_device_prepare_firmware; device_class->write_firmware = fu_igsc_aux_device_write_firmware; device_class->prepare = fu_igsc_aux_device_prepare; device_class->cleanup = fu_igsc_aux_device_cleanup; } FuIgscAuxDevice * fu_igsc_aux_device_new(FuContext *ctx) { return g_object_new(FU_TYPE_IGSC_AUX_DEVICE, "context", ctx, NULL); } fwupd-2.0.10/plugins/intel-gsc/fu-igsc-aux-device.h000066400000000000000000000006221501337203100220160ustar00rootroot00000000000000/* * Copyright 2022 Intel * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later OR Apache-2.0 */ #pragma once #include #define FU_TYPE_IGSC_AUX_DEVICE (fu_igsc_aux_device_get_type()) G_DECLARE_FINAL_TYPE(FuIgscAuxDevice, fu_igsc_aux_device, FU, IGSC_AUX_DEVICE, FuDevice) FuIgscAuxDevice * fu_igsc_aux_device_new(FuContext *ctx); fwupd-2.0.10/plugins/intel-gsc/fu-igsc-aux-firmware.c000066400000000000000000000176211501337203100223750ustar00rootroot00000000000000/* * Copyright 2022 Intel * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-igsc-aux-firmware.h" #include "fu-igsc-heci.h" #include "fu-igsc-struct.h" struct _FuIgscAuxFirmware { FuIfwiFptFirmware parent_instance; guint32 oem_version; guint16 major_version; guint16 major_vcn; GPtrArray *device_infos; /* of FuIgscFwdataDeviceInfo4 */ gboolean has_manifest_ext; }; G_DEFINE_TYPE(FuIgscAuxFirmware, fu_igsc_aux_firmware, FU_TYPE_IFWI_FPT_FIRMWARE) static void fu_igsc_aux_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuIgscAuxFirmware *self = FU_IGSC_AUX_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "oem_version", self->oem_version); fu_xmlb_builder_insert_kx(bn, "major_version", self->major_version); fu_xmlb_builder_insert_kx(bn, "major_vcn", self->major_vcn); fu_xmlb_builder_insert_kx(bn, "device_infos", self->device_infos->len); fu_xmlb_builder_insert_kb(bn, "has_manifest_ext", self->has_manifest_ext); } gboolean fu_igsc_aux_firmware_match_device(FuIgscAuxFirmware *self, guint16 vendor_id, guint16 device_id, guint16 subsys_vendor_id, guint16 subsys_device_id, GError **error) { g_return_val_if_fail(FU_IS_IGSC_AUX_FIRMWARE(self), FALSE); for (guint i = 0; i < self->device_infos->len; i++) { FuIgscFwdataDeviceInfo4 *info = g_ptr_array_index(self->device_infos, i); if (fu_igsc_fwdata_device_info4_get_vendor_id(info) == vendor_id && fu_igsc_fwdata_device_info4_get_device_id(info) == device_id && fu_igsc_fwdata_device_info4_get_subsys_vendor_id(info) == subsys_vendor_id && fu_igsc_fwdata_device_info4_get_subsys_device_id(info) == subsys_device_id) return TRUE; } /* not us */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "could not find 0x%04x:0x%04x 0x%04x:0x%04x in the image", vendor_id, device_id, subsys_vendor_id, subsys_device_id); return FALSE; } guint32 fu_igsc_aux_firmware_get_oem_version(FuIgscAuxFirmware *self) { g_return_val_if_fail(FU_IS_IGSC_AUX_FIRMWARE(self), G_MAXUINT32); return self->oem_version; } guint16 fu_igsc_aux_firmware_get_major_version(FuIgscAuxFirmware *self) { g_return_val_if_fail(FU_IS_IGSC_AUX_FIRMWARE(self), G_MAXUINT16); return self->major_version; } guint16 fu_igsc_aux_firmware_get_major_vcn(FuIgscAuxFirmware *self) { g_return_val_if_fail(FU_IS_IGSC_AUX_FIRMWARE(self), G_MAXUINT16); return self->major_vcn; } static gboolean fu_igsc_aux_firmware_parse_version(FuIgscAuxFirmware *self, GError **error) { g_autoptr(FuStructIgscFwdataVersion) st = NULL; g_autoptr(GInputStream) stream = NULL; stream = fu_firmware_get_image_by_idx_stream(FU_FIRMWARE(self), FU_IFWI_FPT_FIRMWARE_IDX_SDTA, error); if (stream == NULL) return FALSE; st = fu_struct_igsc_fwdata_version_parse_stream(stream, FU_STRUCT_IGSC_FWU_HECI_IMAGE_METADATA_SIZE, error); if (st == NULL) return FALSE; self->oem_version = fu_struct_igsc_fwdata_version_get_oem_manuf_data_version(st); self->major_vcn = fu_struct_igsc_fwdata_version_get_major_vcn(st); self->major_version = fu_struct_igsc_fwdata_version_get_major_version(st); return TRUE; } static gboolean fu_igsc_aux_firmware_parse_extension(FuIgscAuxFirmware *self, FuFirmware *fw, GError **error) { g_autoptr(GInputStream) stream = NULL; /* get data */ stream = fu_firmware_get_stream(fw, error); if (stream == NULL) return FALSE; if (fu_firmware_get_idx(fw) == FU_IGSC_FWU_EXT_TYPE_DEVICE_IDS) { for (gsize offset = 0; offset < fu_firmware_get_size(fw); offset += FU_IGSC_FWDATA_DEVICE_INFO4_SIZE) { g_autoptr(FuIgscFwdataDeviceInfo4) st = NULL; st = fu_igsc_fwdata_device_info4_parse_stream(stream, offset, error); if (st == NULL) return FALSE; g_ptr_array_add(self->device_infos, g_steal_pointer(&st)); } } else if (fu_firmware_get_idx(fw) == FU_IGSC_FWU_EXT_TYPE_FWDATA_UPDATE) { if (fu_firmware_get_size(fw) != FU_STRUCT_IGSC_FWDATA_UPDATE_EXT_SIZE) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "signed data update manifest ext was 0x%x bytes and expected 0x%x", (guint)fu_firmware_get_size(fw), (guint)FU_STRUCT_IGSC_FWDATA_UPDATE_EXT_SIZE); return FALSE; } self->has_manifest_ext = TRUE; } /* success */ return TRUE; } static gboolean fu_igsc_aux_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuIgscAuxFirmware *self = FU_IGSC_AUX_FIRMWARE(firmware); g_autoptr(FuFirmware) fw_cpd = fu_ifwi_cpd_firmware_new(); g_autoptr(FuFirmware) fw_manifest = NULL; g_autoptr(GInputStream) stream_dataimg = NULL; g_autoptr(GPtrArray) imgs = NULL; /* FuIfwiFptFirmware->parse */ if (!FU_FIRMWARE_CLASS(fu_igsc_aux_firmware_parent_class) ->parse(firmware, stream, flags, error)) return FALSE; /* parse data section */ stream_dataimg = fu_firmware_get_image_by_idx_stream(firmware, FU_IFWI_FPT_FIRMWARE_IDX_SDTA, error); if (stream_dataimg == NULL) return FALSE; /* parse as CPD */ if (!fu_firmware_parse_stream(fw_cpd, stream_dataimg, 0x0, flags, error)) return FALSE; /* get manifest */ fw_manifest = fu_firmware_get_image_by_idx(fw_cpd, FU_IFWI_CPD_FIRMWARE_IDX_MANIFEST, error); if (fw_manifest == NULL) return FALSE; /* parse all the manifest extensions */ imgs = fu_firmware_get_images(fw_manifest); for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); if (!fu_igsc_aux_firmware_parse_extension(self, img, error)) return FALSE; } if (!self->has_manifest_ext || self->device_infos->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "missing extensions"); return FALSE; } /* parse the info block */ if (!fu_igsc_aux_firmware_parse_version(self, error)) return FALSE; /* success */ return TRUE; } static GByteArray * fu_igsc_aux_firmware_write(FuFirmware *firmware, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); return g_steal_pointer(&buf); } static gboolean fu_igsc_aux_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { FuIgscAuxFirmware *self = FU_IGSC_AUX_FIRMWARE(firmware); const gchar *tmp; /* simple properties */ tmp = xb_node_query_text(n, "oem_version", NULL); if (tmp != NULL) { guint64 val = 0; if (!fu_strtoull(tmp, &val, 0x0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->oem_version = val; } tmp = xb_node_query_text(n, "major_version", NULL); if (tmp != NULL) { guint64 val = 0; if (!fu_strtoull(tmp, &val, 0x0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->major_version = val; } tmp = xb_node_query_text(n, "major_vcn", NULL); if (tmp != NULL) { guint64 val = 0; if (!fu_strtoull(tmp, &val, 0x0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->major_vcn = val; } /* success */ return TRUE; } static void fu_igsc_aux_firmware_init(FuIgscAuxFirmware *self) { self->device_infos = g_ptr_array_new_with_free_func((GDestroyNotify)fu_igsc_fwdata_device_info4_unref); } static void fu_igsc_aux_firmware_finalize(GObject *object) { FuIgscAuxFirmware *self = FU_IGSC_AUX_FIRMWARE(object); g_ptr_array_unref(self->device_infos); G_OBJECT_CLASS(fu_igsc_aux_firmware_parent_class)->finalize(object); } static void fu_igsc_aux_firmware_class_init(FuIgscAuxFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_igsc_aux_firmware_finalize; firmware_class->parse = fu_igsc_aux_firmware_parse; firmware_class->write = fu_igsc_aux_firmware_write; firmware_class->build = fu_igsc_aux_firmware_build; firmware_class->export = fu_igsc_aux_firmware_export; } FuFirmware * fu_igsc_aux_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_IGSC_AUX_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/intel-gsc/fu-igsc-aux-firmware.h000066400000000000000000000015121501337203100223720ustar00rootroot00000000000000/* * Copyright 2022 Intel * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_IGSC_AUX_FIRMWARE (fu_igsc_aux_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuIgscAuxFirmware, fu_igsc_aux_firmware, FU, IGSC_AUX_FIRMWARE, FuIfwiFptFirmware) FuFirmware * fu_igsc_aux_firmware_new(void); guint32 fu_igsc_aux_firmware_get_oem_version(FuIgscAuxFirmware *self); guint16 fu_igsc_aux_firmware_get_major_version(FuIgscAuxFirmware *self); guint16 fu_igsc_aux_firmware_get_major_vcn(FuIgscAuxFirmware *self); gboolean fu_igsc_aux_firmware_match_device(FuIgscAuxFirmware *self, guint16 vendor_id, guint16 device_id, guint16 subsys_vendor_id, guint16 subsys_device_id, GError **error); fwupd-2.0.10/plugins/intel-gsc/fu-igsc-code-firmware.c000066400000000000000000000102121501337203100224770ustar00rootroot00000000000000/* * Copyright 2022 Intel * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-igsc-code-firmware.h" #include "fu-igsc-struct.h" struct _FuIgscCodeFirmware { FuIfwiFptFirmware parent_instance; guint32 hw_sku; }; G_DEFINE_TYPE(FuIgscCodeFirmware, fu_igsc_code_firmware, FU_TYPE_IFWI_FPT_FIRMWARE) #define GSC_FWU_IUP_NUM 2 #define FU_IGSC_FIRMWARE_MAX_SIZE (8 * 1024 * 1024) /* 8M */ static void fu_igsc_code_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuIgscCodeFirmware *self = FU_IGSC_CODE_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "hw_sku", self->hw_sku); } guint32 fu_igsc_code_firmware_get_hw_sku(FuIgscCodeFirmware *self) { g_return_val_if_fail(FU_IS_IFWI_FPT_FIRMWARE(self), G_MAXUINT32); return self->hw_sku; } static gboolean fu_igsc_code_firmware_parse_imgi(FuIgscCodeFirmware *self, GInputStream *stream, GError **error) { g_autoptr(GByteArray) st_inf = NULL; /* the command is only supported on DG2 */ if (g_strcmp0(fu_firmware_get_id(FU_FIRMWARE(self)), "DG02") != 0) return TRUE; /* get hw_sku */ st_inf = fu_struct_igsc_fwu_gws_image_info_parse_stream(stream, 0x0, error); if (st_inf == NULL) return FALSE; self->hw_sku = fu_struct_igsc_fwu_gws_image_info_get_instance_id(st_inf); return TRUE; } static gboolean fu_igsc_code_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuIgscCodeFirmware *self = FU_IGSC_CODE_FIRMWARE(firmware); gsize streamsz = 0; g_autofree gchar *project = NULL; g_autofree gchar *version = NULL; g_autoptr(GByteArray) st_md1 = NULL; g_autoptr(GInputStream) stream_imgi = NULL; g_autoptr(GInputStream) stream_info = NULL; /* sanity check */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz > FU_IGSC_FIRMWARE_MAX_SIZE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "image size too big: 0x%x", (guint)streamsz); return FALSE; } /* FuIfwiFptFirmware->parse */ if (!FU_FIRMWARE_CLASS(fu_igsc_code_firmware_parent_class) ->parse(firmware, stream, flags, error)) { g_prefix_error(error, "failed to parse as FuIfwiFptFirmware: "); return FALSE; } stream_info = fu_firmware_get_image_by_idx_stream(FU_FIRMWARE(self), FU_IFWI_FPT_FIRMWARE_IDX_INFO, error); if (stream_info == NULL) { g_prefix_error(error, "info not found: "); return FALSE; } /* check metadata header format */ st_md1 = fu_struct_igsc_fwu_image_metadata_v1_parse_stream(stream_info, 0x0, error); if (st_md1 == NULL) return FALSE; if (fu_struct_igsc_fwu_image_metadata_v1_get_version_format(st_md1) != 0x01) { /* Note that it's still ok to use the V1 metadata struct to get the * FW version because the FW version position and structure stays * the same in all versions of the struct */ g_warning("metadata format version is %u, instead of expected V1", fu_struct_igsc_fwu_image_metadata_v1_get_version_format(st_md1)); } project = fu_struct_igsc_fwu_image_metadata_v1_get_project(st_md1); fu_firmware_set_id(FU_FIRMWARE(self), project); version = g_strdup_printf("%04d.%04d", fu_struct_igsc_fwu_image_metadata_v1_get_version_hotfix(st_md1), fu_struct_igsc_fwu_image_metadata_v1_get_version_build(st_md1)); fu_firmware_set_version(FU_FIRMWARE(self), version); /* get instance ID for image */ stream_imgi = fu_firmware_get_image_by_idx_stream(FU_FIRMWARE(self), FU_IFWI_FPT_FIRMWARE_IDX_IMGI, error); if (stream_imgi == NULL) return FALSE; if (!fu_igsc_code_firmware_parse_imgi(self, stream_imgi, error)) return FALSE; /* success */ return TRUE; } static void fu_igsc_code_firmware_init(FuIgscCodeFirmware *self) { } static void fu_igsc_code_firmware_class_init(FuIgscCodeFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_igsc_code_firmware_parse; firmware_class->export = fu_igsc_code_firmware_export; } FuFirmware * fu_igsc_code_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_IGSC_CODE_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/intel-gsc/fu-igsc-code-firmware.h000066400000000000000000000007601501337203100225130ustar00rootroot00000000000000/* * Copyright 2022 Intel * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_IGSC_CODE_FIRMWARE (fu_igsc_code_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuIgscCodeFirmware, fu_igsc_code_firmware, FU, IGSC_CODE_FIRMWARE, FuIfwiFptFirmware) FuFirmware * fu_igsc_code_firmware_new(void); guint32 fu_igsc_code_firmware_get_hw_sku(FuIgscCodeFirmware *self); fwupd-2.0.10/plugins/intel-gsc/fu-igsc-device.c000066400000000000000000000637151501337203100212320ustar00rootroot00000000000000/* * Copyright 2022 Intel, Inc * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later OR Apache-2.0 */ #include "config.h" #include "fu-igsc-aux-device.h" #include "fu-igsc-code-firmware.h" #include "fu-igsc-device.h" #include "fu-igsc-oprom-device.h" #include "fu-igsc-struct.h" struct _FuIgscDevice { FuHeciDevice parent_instance; gchar *project; guint32 hw_sku; guint16 subsystem_vendor; guint16 subsystem_model; gboolean oprom_code_devid_enforcement; guint8 svn_executing; guint8 svn_min_allowed; }; #define FU_IGSC_DEVICE_FLAG_HAS_AUX "has-aux" #define FU_IGSC_DEVICE_FLAG_HAS_OPROM "has-oprom" #define FU_IGSC_DEVICE_POWER_WRITE_TIMEOUT 1500 /* ms */ #define FU_IGSC_DEVICE_MEI_WRITE_TIMEOUT 60000 /* 60 sec */ #define FU_IGSC_DEVICE_MEI_READ_TIMEOUT 480000 /* 480 sec */ G_DEFINE_TYPE(FuIgscDevice, fu_igsc_device, FU_TYPE_HECI_DEVICE) #define GSC_FWU_STATUS_SUCCESS 0x0 #define GSC_FWU_STATUS_SIZE_ERROR 0x5 #define GSC_FWU_STATUS_UPDATE_OPROM_INVALID_STRUCTURE 0x1035 #define GSC_FWU_STATUS_UPDATE_OPROM_SECTION_NOT_EXIST 0x1032 #define GSC_FWU_STATUS_INVALID_COMMAND 0x8D #define GSC_FWU_STATUS_INVALID_PARAMS 0x85 #define GSC_FWU_STATUS_FAILURE 0x9E #define GSC_FWU_GET_CONFIG_FORMAT_VERSION 0x1 static void fu_igsc_device_to_string(FuDevice *device, guint idt, GString *str) { FuIgscDevice *self = FU_IGSC_DEVICE(device); fwupd_codec_string_append(str, idt, "Project", self->project); fwupd_codec_string_append_hex(str, idt, "HwSku", self->hw_sku); fwupd_codec_string_append_hex(str, idt, "SubsystemVendor", self->subsystem_vendor); fwupd_codec_string_append_hex(str, idt, "SubsystemModel", self->subsystem_model); fwupd_codec_string_append_bool(str, idt, "OpromCodeDevidEnforcement", self->oprom_code_devid_enforcement); fwupd_codec_string_append_hex(str, idt, "SvnExecuting", self->svn_executing); fwupd_codec_string_append_hex(str, idt, "SvnMinAllowed", self->svn_min_allowed); } gboolean fu_igsc_device_get_oprom_code_devid_enforcement(FuIgscDevice *self) { g_return_val_if_fail(FU_IS_IGSC_DEVICE(self), FALSE); return self->oprom_code_devid_enforcement; } guint16 fu_igsc_device_get_ssvid(FuIgscDevice *self) { g_return_val_if_fail(FU_IS_IGSC_DEVICE(self), G_MAXUINT16); return self->subsystem_vendor; } guint16 fu_igsc_device_get_ssdid(FuIgscDevice *self) { g_return_val_if_fail(FU_IS_IGSC_DEVICE(self), G_MAXUINT16); return self->subsystem_model; } static gboolean fu_igsc_device_heci_validate_response_header(FuIgscDevice *self, struct gsc_fwu_heci_response *resp_header, FuIgscFwuHeciCommandId command_id, GError **error) { if (resp_header->header.command_id != command_id) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid command ID (%d): ", resp_header->header.command_id); return FALSE; } if (!resp_header->header.is_response) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not a response"); return FALSE; } if (resp_header->status != GSC_FWU_STATUS_SUCCESS) { const gchar *msg; switch (resp_header->status) { case GSC_FWU_STATUS_SIZE_ERROR: msg = "num of bytes to read/write/erase is bigger than partition size"; break; case GSC_FWU_STATUS_UPDATE_OPROM_INVALID_STRUCTURE: msg = "wrong oprom signature"; break; case GSC_FWU_STATUS_UPDATE_OPROM_SECTION_NOT_EXIST: msg = "update oprom section does not exists on flash"; break; case GSC_FWU_STATUS_INVALID_COMMAND: msg = "invalid HECI message sent"; break; case GSC_FWU_STATUS_INVALID_PARAMS: msg = "invalid command parameters"; break; case GSC_FWU_STATUS_FAILURE: /* fall through */ default: msg = "general firmware error"; break; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "HECI message failed: %s [0x%x]: ", msg, resp_header->status); return FALSE; } if (resp_header->reserved != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "HECI message response is leaking data"); return FALSE; } /* success */ return TRUE; } static gboolean fu_igsc_device_command(FuIgscDevice *self, const guint8 *req_buf, gsize req_bufsz, guint8 *resp_buf, gsize resp_bufsz, GError **error) { gsize resp_readsz = 0; if (!fu_mei_device_write(FU_MEI_DEVICE(self), req_buf, req_bufsz, FU_IGSC_DEVICE_MEI_WRITE_TIMEOUT, error)) return FALSE; if (!fu_mei_device_read(FU_MEI_DEVICE(self), resp_buf, resp_bufsz, &resp_readsz, FU_IGSC_DEVICE_MEI_READ_TIMEOUT, error)) return FALSE; if (resp_readsz != resp_bufsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "read 0x%x bytes but expected 0x%x", (guint)resp_readsz, (guint)resp_bufsz); return FALSE; } return TRUE; } gboolean fu_igsc_device_get_version_raw(FuIgscDevice *self, FuIgscFwuHeciPartitionVersion partition, guint8 *buf, gsize bufsz, GError **error) { struct gsc_fwu_heci_version_req req = {.header.command_id = FU_IGSC_FWU_HECI_COMMAND_ID_GET_IP_VERSION, .partition = partition}; guint8 res_buf[100]; struct gsc_fwu_heci_version_resp *res = (struct gsc_fwu_heci_version_resp *)res_buf; if (!fu_igsc_device_command(self, (const guint8 *)&req, sizeof(req), res_buf, sizeof(struct gsc_fwu_heci_version_resp) + bufsz, error)) { g_prefix_error(error, "invalid HECI message response: "); return FALSE; } if (!fu_igsc_device_heci_validate_response_header(self, &res->response, req.header.command_id, error)) return FALSE; if (res->partition != partition) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid HECI message response payload: 0x%x: ", res->partition); return FALSE; } if (bufsz > 0 && res->version_length != bufsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid HECI message response version_length: 0x%x, expected 0x%x: ", res->version_length, (guint)bufsz); return FALSE; } if (buf != NULL) { if (!fu_memcpy_safe(buf, bufsz, 0x0, /* dst */ res->version, res->version_length, 0x0, /* src*/ res->version_length, error)) { return FALSE; } } /* success */ return TRUE; } gboolean fu_igsc_device_get_aux_version(FuIgscDevice *self, guint32 *oem_version, guint16 *major_version, guint16 *major_vcn, GError **error) { struct gsc_fw_data_heci_version_req req = { .header.command_id = FU_IGSC_FWU_HECI_COMMAND_ID_GET_GFX_DATA_UPDATE_INFO}; struct gsc_fw_data_heci_version_resp res = {0x0}; if (!fu_igsc_device_command(self, (const guint8 *)&req, sizeof(req), (guint8 *)&res, sizeof(res), error)) return FALSE; if (!fu_igsc_device_heci_validate_response_header(self, &res.response, req.header.command_id, error)) return FALSE; /* success */ *major_vcn = res.major_vcn; *major_version = res.major_version; if (res.oem_version_fitb_valid) { *oem_version = res.oem_version_fitb; } else { *oem_version = res.oem_version_nvm; } return TRUE; } static gboolean fu_igsc_device_get_subsystem_ids(FuIgscDevice *self, GError **error) { struct gsc_fwu_heci_get_subsystem_ids_message_req req = { .header.command_id = FU_IGSC_FWU_HECI_COMMAND_ID_GET_SUBSYSTEM_IDS}; struct gsc_fwu_heci_get_subsystem_ids_message_resp res = {0x0}; if (!fu_igsc_device_command(self, (const guint8 *)&req, sizeof(req), (guint8 *)&res, sizeof(res), error)) return FALSE; if (!fu_igsc_device_heci_validate_response_header(self, &res.response, req.header.command_id, error)) return FALSE; /* success */ self->subsystem_vendor = res.ssvid; self->subsystem_model = res.ssdid; return TRUE; } #define GSC_IFWI_TAG_SOC2_SKU_BIT 0x1 #define GSC_IFWI_TAG_SOC3_SKU_BIT 0x2 #define GSC_IFWI_TAG_SOC1_SKU_BIT 0x4 #define GSC_DG2_SKUID_SOC1 0 #define GSC_DG2_SKUID_SOC2 1 #define GSC_DG2_SKUID_SOC3 2 static gboolean fu_igsc_device_get_config(FuIgscDevice *self, GError **error) { struct gsc_fwu_heci_get_config_message_req req = { .header.command_id = FU_IGSC_FWU_HECI_COMMAND_ID_GET_CONFIG, }; struct gsc_fwu_heci_get_config_message_resp res = {0x0}; if (!fu_igsc_device_command(self, (const guint8 *)&req, sizeof(req), (guint8 *)&res, sizeof(res), error)) { g_prefix_error(error, "invalid HECI message response: "); return FALSE; } if (!fu_igsc_device_heci_validate_response_header(self, &res.response, req.header.command_id, error)) return FALSE; if (res.format_version != GSC_FWU_GET_CONFIG_FORMAT_VERSION) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid config version 0x%x, expected 0x%x", res.format_version, (guint)GSC_FWU_GET_CONFIG_FORMAT_VERSION); return FALSE; } /* convert to firmware bit mask for easier comparison */ if (res.hw_sku == GSC_DG2_SKUID_SOC1) { self->hw_sku = GSC_IFWI_TAG_SOC1_SKU_BIT; } else if (res.hw_sku == GSC_DG2_SKUID_SOC3) { self->hw_sku = GSC_IFWI_TAG_SOC3_SKU_BIT; } else if (res.hw_sku == GSC_DG2_SKUID_SOC2) { self->hw_sku = GSC_IFWI_TAG_SOC2_SKU_BIT; } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid hw sku 0x%x, expected 0..2", res.hw_sku); return FALSE; } self->oprom_code_devid_enforcement = res.oprom_code_devid_enforcement; /* success */ return TRUE; } static gboolean fu_igsc_device_setup(FuDevice *device, GError **error) { FuIgscDevice *self = FU_IGSC_DEVICE(device); FuContext *ctx = fu_device_get_context(FU_DEVICE(self)); g_autofree gchar *version = NULL; g_autoptr(GByteArray) fw_code_version = fu_struct_igsc_fw_version_new(); /* connect to MCA interface */ if (!fu_mei_device_connect(FU_MEI_DEVICE(self), FU_HECI_DEVICE_UUID_MCHI2, 0, error)) { g_prefix_error(error, "failed to connect: "); return FALSE; } if (!fu_heci_device_arbh_svn_get_info(FU_HECI_DEVICE(self), FU_MKHI_ARBH_SVN_INFO_ENTRY_USAGE_ID_CSE_RBE, &self->svn_executing, &self->svn_min_allowed, error)) { g_prefix_error(error, "failed to get ARBH SVN: "); return FALSE; } if (!fu_udev_device_reopen(FU_UDEV_DEVICE(self), error)) return FALSE; /* now connect to fwupdate interface */ if (!fu_mei_device_connect(FU_MEI_DEVICE(self), FU_HECI_DEVICE_UUID_FWUPDATE, 0, error)) { g_prefix_error(error, "failed to connect: "); return FALSE; } /* get current version */ if (!fu_igsc_device_get_version_raw(self, FU_IGSC_FWU_HECI_PARTITION_VERSION_GFX_FW, fw_code_version->data, fw_code_version->len, error)) { g_prefix_error(error, "cannot get fw version: "); return FALSE; } self->project = fu_struct_igsc_fw_version_get_project(fw_code_version); version = g_strdup_printf("%u.%u", fu_struct_igsc_fw_version_get_hotfix(fw_code_version), fu_struct_igsc_fw_version_get_build(fw_code_version)); fu_device_set_version(device, version); /* get hardware SKU if supported */ if (g_strcmp0(self->project, "DG02") == 0) { if (!fu_igsc_device_get_config(self, error)) { g_prefix_error(error, "cannot get SKU: "); return FALSE; } } /* allow vendors to differentiate their products */ if (!fu_igsc_device_get_subsystem_ids(self, error)) return FALSE; if (self->subsystem_vendor != 0x0 && self->subsystem_model != 0x0) { g_autofree gchar *subsys = g_strdup_printf("%04X%04X", self->subsystem_vendor, self->subsystem_model); fu_device_add_instance_str(device, "SUBSYS", subsys); } /* some devices have children */ if (fu_device_has_private_flag(device, FU_IGSC_DEVICE_FLAG_HAS_AUX)) { g_autoptr(FuIgscAuxDevice) device_child = fu_igsc_aux_device_new(ctx); fu_device_add_child(FU_DEVICE(self), FU_DEVICE(device_child)); } if (fu_device_has_private_flag(device, FU_IGSC_DEVICE_FLAG_HAS_OPROM)) { g_autoptr(FuIgscOpromDevice) device_code = NULL; g_autoptr(FuIgscOpromDevice) device_data = NULL; device_code = fu_igsc_oprom_device_new(ctx, FU_IGSC_FWU_HECI_PAYLOAD_TYPE_OPROM_CODE); device_data = fu_igsc_oprom_device_new(ctx, FU_IGSC_FWU_HECI_PAYLOAD_TYPE_OPROM_DATA); fu_device_add_child(FU_DEVICE(self), FU_DEVICE(device_code)); fu_device_add_child(FU_DEVICE(self), FU_DEVICE(device_data)); } /* success */ return TRUE; } /* @line is indexed from 1 */ static gboolean fu_igsc_device_get_fw_status(FuIgscDevice *self, guint line, guint32 *fw_status, GError **error) { guint64 tmp64 = 0; g_autofree gchar *tmp = NULL; g_autofree gchar *hex = NULL; g_return_val_if_fail(line >= 1, FALSE); /* read value and convert to hex */ tmp = fu_mei_device_get_fw_status(FU_MEI_DEVICE(self), line - 1, error); if (tmp == NULL) { g_prefix_error(error, "device is corrupted: "); return FALSE; } hex = g_strdup_printf("0x%s", tmp); if (!fu_strtoull(hex, &tmp64, 0x1, G_MAXUINT32 - 0x1, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "fw_status %s is invalid: ", tmp); return FALSE; } /* success */ if (fw_status != NULL) *fw_status = tmp64; return TRUE; } static gboolean fu_igsc_device_probe(FuDevice *device, GError **error) { FuIgscDevice *self = FU_IGSC_DEVICE(device); /* check firmware status */ if (!fu_igsc_device_get_fw_status(self, 1, NULL, error)) return FALSE; /* add extra instance IDs */ fu_device_add_instance_str(device, "PART", "FWCODE"); if (!fu_device_build_instance_id(device, error, "PCI", "VEN", "DEV", "PART", NULL)) return FALSE; return fu_device_build_instance_id(device, error, "PCI", "VEN", "DEV", "SUBSYS", "PART", NULL); } static FuFirmware * fu_igsc_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuIgscDevice *self = FU_IGSC_DEVICE(device); g_autoptr(FuFirmware) firmware = fu_igsc_code_firmware_new(); /* check project code */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; if (g_strcmp0(self->project, fu_firmware_get_id(firmware)) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "firmware is for a different project, got %s, expected %s", fu_firmware_get_id(firmware), self->project); return NULL; } if (self->hw_sku != fu_igsc_code_firmware_get_hw_sku(FU_IGSC_CODE_FIRMWARE(firmware))) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "firmware is for a different SKU, got 0x%x, expected 0x%x", fu_igsc_code_firmware_get_hw_sku(FU_IGSC_CODE_FIRMWARE(firmware)), self->hw_sku); return NULL; } /* success */ return g_steal_pointer(&firmware); } static gboolean fu_igsc_device_update_end(FuIgscDevice *self, GError **error) { struct gsc_fwu_heci_end_req req = {.header.command_id = FU_IGSC_FWU_HECI_COMMAND_ID_END}; return fu_mei_device_write(FU_MEI_DEVICE(self), (const guint8 *)&req, sizeof(req), FU_IGSC_DEVICE_MEI_WRITE_TIMEOUT, error); } static gboolean fu_igsc_device_update_data(FuIgscDevice *self, const guint8 *data, gsize length, GError **error) { struct gsc_fwu_heci_data_req req = {.header.command_id = FU_IGSC_FWU_HECI_COMMAND_ID_DATA, .data_length = length}; struct gsc_fwu_heci_data_resp res = {0x0}; g_autoptr(GByteArray) buf = g_byte_array_new(); g_byte_array_append(buf, (const guint8 *)&req, sizeof(req)); g_byte_array_append(buf, data, length); if (!fu_igsc_device_command(self, buf->data, buf->len, (guint8 *)&res, sizeof(res), error)) return FALSE; return fu_igsc_device_heci_validate_response_header(self, &res.response, req.header.command_id, error); } static gboolean fu_igsc_device_update_start(FuIgscDevice *self, guint32 payload_type, GBytes *fw_info, GInputStream *fw, GError **error) { gsize streamsz = 0; struct gsc_fwu_heci_start_req req = {.header.command_id = FU_IGSC_FWU_HECI_COMMAND_ID_START, .payload_type = payload_type, .flags = {0}}; struct gsc_fwu_heci_start_resp res = {0x0}; g_autoptr(GByteArray) buf = g_byte_array_new(); if (!fu_input_stream_size(fw, &streamsz, error)) return FALSE; req.update_img_length = streamsz; g_byte_array_append(buf, (const guint8 *)&req, sizeof(req)); if (fw_info != NULL) fu_byte_array_append_bytes(buf, fw_info); if (!fu_igsc_device_command(self, buf->data, buf->len, (guint8 *)&res, sizeof(res), error)) return FALSE; return fu_igsc_device_heci_validate_response_header(self, &res.response, req.header.command_id, error); } static gboolean fu_igsc_device_no_update(FuIgscDevice *self, GError **error) { struct gsc_fwu_heci_no_update_req req = {.header.command_id = FU_IGSC_FWU_HECI_COMMAND_ID_NO_UPDATE}; return fu_mei_device_write(FU_MEI_DEVICE(self), (const guint8 *)&req, sizeof(req), FU_IGSC_DEVICE_MEI_WRITE_TIMEOUT, error); } static gboolean fu_igsc_device_write_chunks(FuIgscDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_igsc_device_update_data(self, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "failed on chunk %u (@0x%x): ", i, (guint)fu_chunk_get_address(chk)); return FALSE; } fu_progress_step_done(progress); } return TRUE; } /* the expectation is that it will fail eventually */ static gboolean fu_igsc_device_wait_for_reset(FuIgscDevice *self, GError **error) { g_autoptr(GByteArray) fw_code_version = fu_struct_igsc_fw_version_new(); for (guint i = 0; i < 20; i++) { if (!fu_igsc_device_get_version_raw(self, FU_IGSC_FWU_HECI_PARTITION_VERSION_GFX_FW, fw_code_version->data, fw_code_version->len, NULL)) return TRUE; fu_device_sleep(FU_DEVICE(self), 100); } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "device did not reset"); return FALSE; } static gboolean fu_igsc_device_reconnect_cb(FuDevice *self, gpointer user_data, GError **error) { return fu_mei_device_connect(FU_MEI_DEVICE(self), FU_HECI_DEVICE_UUID_FWUPDATE, 0, error); } gboolean fu_igsc_device_write_blob(FuIgscDevice *self, FuIgscFwuHeciPayloadType payload_type, GBytes *fw_info, GInputStream *fw, FuProgress *progress, GError **error) { gboolean cp_mode; guint32 sts5 = 0; gsize payloadsz = fu_mei_device_get_max_msg_length(FU_MEI_DEVICE(self)) - sizeof(struct gsc_fwu_heci_data_req); g_autoptr(FuChunkArray) chunks = NULL; /* progress */ if (payload_type == FU_IGSC_FWU_HECI_PAYLOAD_TYPE_GFX_FW) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "get-status"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "update-start"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 50, "write-chunks"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "update-end"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "wait-for-reboot"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 46, "reconnect"); } else { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "get-status"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "update-start"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 96, "write-chunks"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "update-end"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "wait-for-reboot"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reconnect"); } /* need to get the new version in a loop? */ if (!fu_igsc_device_get_fw_status(self, 5, &sts5, error)) return FALSE; cp_mode = (sts5 & HECI1_CSE_FS_MODE_MASK) == HECI1_CSE_FS_CP_MODE; fu_progress_step_done(progress); /* start */ if (!fu_igsc_device_update_start(self, payload_type, fw_info, fw, error)) { g_prefix_error(error, "failed to start: "); return FALSE; } fu_progress_step_done(progress); /* data */ chunks = fu_chunk_array_new_from_stream(fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, payloadsz, error); if (chunks == NULL) return FALSE; if (!fu_igsc_device_write_chunks(self, chunks, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* stop */ if (!fu_igsc_device_update_end(self, error)) { g_prefix_error(error, "failed to end: "); return FALSE; } fu_progress_step_done(progress); /* detect a firmware reboot */ if (payload_type == FU_IGSC_FWU_HECI_PAYLOAD_TYPE_GFX_FW || payload_type == FU_IGSC_FWU_HECI_PAYLOAD_TYPE_FWDATA) { if (!fu_igsc_device_wait_for_reset(self, error)) return FALSE; } fu_progress_step_done(progress); /* after Gfx FW update there is a FW reset so driver reconnect is needed */ if (payload_type == FU_IGSC_FWU_HECI_PAYLOAD_TYPE_GFX_FW) { if (cp_mode) { if (!fu_igsc_device_wait_for_reset(self, error)) return FALSE; } if (!fu_device_retry_full(FU_DEVICE(self), fu_igsc_device_reconnect_cb, 200, 300 /* ms */, NULL, error)) return FALSE; if (!fu_igsc_device_no_update(self, error)) { g_prefix_error(error, "failed to send no-update: "); return FALSE; } fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); } fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_igsc_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuIgscDevice *self = FU_IGSC_DEVICE(device); g_autoptr(GBytes) fw_info = NULL; g_autoptr(GInputStream) stream_payload = NULL; /* get image, and install on ourself */ fw_info = fu_firmware_get_image_by_idx_bytes(firmware, FU_IFWI_FPT_FIRMWARE_IDX_INFO, error); if (fw_info == NULL) return FALSE; stream_payload = fu_firmware_get_image_by_idx_stream(firmware, FU_IFWI_FPT_FIRMWARE_IDX_FWIM, error); if (stream_payload == NULL) return FALSE; if (!fu_igsc_device_write_blob(self, FU_IGSC_FWU_HECI_PAYLOAD_TYPE_GFX_FW, fw_info, stream_payload, progress, error)) return FALSE; /* restart */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT); return TRUE; } static gboolean fu_igsc_device_set_pci_power_policy(FuIgscDevice *self, const gchar *val, GError **error) { g_autoptr(FuDevice) parent = NULL; /* get PCI parent */ parent = fu_device_get_backend_parent_with_subsystem(FU_DEVICE(self), "pci", error); if (parent == NULL) return FALSE; return fu_udev_device_write_sysfs(FU_UDEV_DEVICE(parent), "power/control", val, FU_IGSC_DEVICE_POWER_WRITE_TIMEOUT, error); } static gboolean fu_igsc_device_prepare(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuIgscDevice *self = FU_IGSC_DEVICE(device); return fu_igsc_device_set_pci_power_policy(self, "on", error); } static gboolean fu_igsc_device_cleanup(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuIgscDevice *self = FU_IGSC_DEVICE(device); return fu_igsc_device_set_pci_power_policy(self, "auto", error); } static void fu_igsc_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 96, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reload"); } static void fu_igsc_device_init(FuIgscDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_INSTALL_PARENT_FIRST); fu_device_set_summary(FU_DEVICE(self), "Discrete Graphics Card"); fu_device_add_protocol(FU_DEVICE(self), "com.intel.gsc"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_GPU); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PAIR); fu_device_set_remove_delay(FU_DEVICE(self), 60000); fu_device_register_private_flag(FU_DEVICE(self), FU_IGSC_DEVICE_FLAG_HAS_AUX); fu_device_register_private_flag(FU_DEVICE(self), FU_IGSC_DEVICE_FLAG_HAS_OPROM); } static void fu_igsc_device_finalize(GObject *object) { FuIgscDevice *self = FU_IGSC_DEVICE(object); g_free(self->project); G_OBJECT_CLASS(fu_igsc_device_parent_class)->finalize(object); } static void fu_igsc_device_class_init(FuIgscDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_igsc_device_finalize; device_class->set_progress = fu_igsc_device_set_progress; device_class->to_string = fu_igsc_device_to_string; device_class->setup = fu_igsc_device_setup; device_class->probe = fu_igsc_device_probe; device_class->prepare = fu_igsc_device_prepare; device_class->cleanup = fu_igsc_device_cleanup; device_class->prepare_firmware = fu_igsc_device_prepare_firmware; device_class->write_firmware = fu_igsc_device_write_firmware; } fwupd-2.0.10/plugins/intel-gsc/fu-igsc-device.h000066400000000000000000000021771501337203100212320ustar00rootroot00000000000000/* * Copyright 2022 Intel, Inc. * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later OR Apache-2.0 */ #pragma once #include #include "fu-igsc-heci.h" #include "fu-igsc-struct.h" #define FU_TYPE_IGSC_DEVICE (fu_igsc_device_get_type()) G_DECLARE_FINAL_TYPE(FuIgscDevice, fu_igsc_device, FU, IGSC_DEVICE, FuHeciDevice) gboolean fu_igsc_device_get_oprom_code_devid_enforcement(FuIgscDevice *self); guint16 fu_igsc_device_get_ssvid(FuIgscDevice *self); guint16 fu_igsc_device_get_ssdid(FuIgscDevice *self); gboolean fu_igsc_device_write_blob(FuIgscDevice *self, FuIgscFwuHeciPayloadType payload_type, GBytes *fw_info, GInputStream *stream_payload, FuProgress *progress, GError **error); gboolean fu_igsc_device_get_aux_version(FuIgscDevice *self, guint32 *oem_version, guint16 *major_version, guint16 *major_vcn, GError **error); gboolean fu_igsc_device_get_version_raw(FuIgscDevice *self, FuIgscFwuHeciPartitionVersion partition, guint8 *buf, gsize bufsz, GError **error); fwupd-2.0.10/plugins/intel-gsc/fu-igsc-heci.h000066400000000000000000000070661501337203100207050ustar00rootroot00000000000000/* * Copyright 2022 Intel * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define HECI1_CSE_FS_FWUPDATE_STATE_IDLE_BIT (1 << 11) #define HECI1_CSE_FS_INITSTATE_COMPLETED_BIT (1 << 9) #define HECI1_CSE_GS1_PHASE_FWUPDATE 7 #define HECI1_CSE_FS_FWUPD_PHASE_SHIFT 28 #define HECI1_CSE_FS_FWUPD_PHASE_MASK 0xF #define HECI1_CSE_FS_FWUPD_PERCENT_SHIFT 16 #define HECI1_CSE_FS_FWUPD_PERCENT_MASK 0xFF #define HECI1_CSE_FS_MODE_MASK 0x3 #define HECI1_CSE_FS_CP_MODE 0x3 struct gsc_fwu_heci_header { guint8 command_id; guint8 is_response : 1; guint8 reserved : 7; guint8 reserved2[2]; } __attribute__((packed)); /* nocheck:blocked */ struct gsc_fwu_heci_response { struct gsc_fwu_heci_header header; guint32 status; guint32 reserved; } __attribute__((packed)); /* nocheck:blocked */ struct gsc_fwu_heci_version_req { struct gsc_fwu_heci_header header; guint32 partition; } __attribute__((packed)); /* nocheck:blocked */ struct gsc_fwu_heci_version_resp { struct gsc_fwu_heci_response response; guint32 partition; guint32 version_length; guint8 version[]; } __attribute__((packed)); /* nocheck:blocked */ struct gsc_fw_data_heci_version_req { struct gsc_fwu_heci_header header; guint32 reserved[2]; } __attribute__((packed)); /* nocheck:blocked */ struct gsc_fw_data_heci_version_resp { struct gsc_fwu_heci_response response; guint32 format_version; guint32 oem_version_nvm; guint32 oem_version_fitb; guint16 major_version; guint16 major_vcn; guint32 oem_version_fitb_valid; guint32 flags; guint32 reserved[7]; } __attribute__((packed)); /* nocheck:blocked */ struct gsc_fwu_heci_get_config_message_req { struct gsc_fwu_heci_header header; guint32 reserved[2]; } __attribute__((packed)); /* nocheck:blocked */ struct gsc_fwu_heci_get_config_message_resp { struct gsc_fwu_heci_response response; guint32 format_version; guint32 hw_step; guint32 hw_sku; guint32 oprom_code_devid_enforcement : 1; guint32 flags : 31; guint32 reserved[7]; guint32 debug_config; } __attribute__((packed)); /* nocheck:blocked */ struct gsc_fwu_heci_get_subsystem_ids_message_req { struct gsc_fwu_heci_header header; guint32 reserved[2]; } __attribute__((packed)); /* nocheck:blocked */ struct gsc_fwu_heci_get_subsystem_ids_message_resp { struct gsc_fwu_heci_response response; guint16 ssvid; guint16 ssdid; guint32 reserved[2]; } __attribute__((packed)); /* nocheck:blocked */ struct gsc_fwu_heci_start_flags { guint32 force_update : 1; guint32 reserved : 31; } __attribute__((packed)); /* nocheck:blocked */ struct gsc_fwu_heci_start_req { struct gsc_fwu_heci_header header; guint32 update_img_length; guint32 payload_type; struct gsc_fwu_heci_start_flags flags; guint32 reserved[8]; } __attribute__((packed)); /* nocheck:blocked */ struct gsc_fwu_heci_start_resp { struct gsc_fwu_heci_response response; } __attribute__((packed)); /* nocheck:blocked */ struct gsc_fwu_heci_data_req { struct gsc_fwu_heci_header header; guint32 data_length; guint32 reserved; } __attribute__((packed)); /* nocheck:blocked */ struct gsc_fwu_heci_data_resp { struct gsc_fwu_heci_response response; } __attribute__((packed)); /* nocheck:blocked */ struct gsc_fwu_heci_end_req { struct gsc_fwu_heci_header header; guint32 reserved; } __attribute__((packed)); /* nocheck:blocked */ struct gsc_fwu_heci_end_resp { struct gsc_fwu_heci_response response; } __attribute__((packed)); /* nocheck:blocked */ struct gsc_fwu_heci_no_update_req { struct gsc_fwu_heci_header header; guint32 reserved; } __attribute__((packed)); /* nocheck:blocked */ fwupd-2.0.10/plugins/intel-gsc/fu-igsc-oprom-device.c000066400000000000000000000221671501337203100223600ustar00rootroot00000000000000/* * Copyright 2022 Intel, Inc * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later OR Apache-2.0 */ #include "config.h" #include "fu-igsc-device.h" #include "fu-igsc-oprom-device.h" #include "fu-igsc-oprom-firmware.h" #include "fu-igsc-struct.h" struct _FuIgscOpromDevice { FuDevice parent_instance; FuIgscFwuHeciPayloadType payload_type; FuIgscFwuHeciPartitionVersion partition_version; guint16 major_version; }; G_DEFINE_TYPE(FuIgscOpromDevice, fu_igsc_oprom_device, FU_TYPE_DEVICE) static void fu_igsc_oprom_device_to_string(FuDevice *device, guint idt, GString *str) { FuIgscOpromDevice *self = FU_IGSC_OPROM_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "PayloadType", self->payload_type); fwupd_codec_string_append_hex(str, idt, "PartitionVersion", self->partition_version); } static gboolean fu_igsc_oprom_device_probe(FuDevice *device, GError **error) { FuIgscOpromDevice *self = FU_IGSC_OPROM_DEVICE(device); FuDevice *parent = fu_device_get_parent(device); g_autofree gchar *name = NULL; /* set strings now we know the type */ if (self->payload_type == FU_IGSC_FWU_HECI_PAYLOAD_TYPE_OPROM_CODE) { self->partition_version = FU_IGSC_FWU_HECI_PARTITION_VERSION_OPROM_CODE; fu_device_add_instance_str(device, "PART", "OPROMCODE"); fu_device_set_logical_id(FU_DEVICE(self), "oprom-code"); if (parent != NULL) { name = g_strdup_printf("%s OptionROM Code", fu_device_get_name(parent)); fu_device_set_name(FU_DEVICE(self), name); } } else if (self->payload_type == FU_IGSC_FWU_HECI_PAYLOAD_TYPE_OPROM_DATA) { self->partition_version = FU_IGSC_FWU_HECI_PARTITION_VERSION_OPROM_DATA; fu_device_add_instance_str(device, "PART", "OPROMDATA"); fu_device_set_logical_id(FU_DEVICE(self), "oprom-data"); if (parent != NULL) { name = g_strdup_printf("%s OptionROM Data", fu_device_get_name(parent)); fu_device_set_name(FU_DEVICE(self), name); } } /* add extra instance IDs */ if (!fu_device_build_instance_id(device, error, "MEI", "VEN", "DEV", "PART", NULL)) return FALSE; return fu_device_build_instance_id(device, error, "MEI", "VEN", "DEV", "SUBSYS", "PART", NULL); } static gboolean fu_igsc_oprom_device_setup(FuDevice *device, GError **error) { FuIgscOpromDevice *self = FU_IGSC_OPROM_DEVICE(device); FuIgscDevice *igsc_parent = FU_IGSC_DEVICE(fu_device_get_parent(device)); guint8 buf[8] = {0x0}; g_autofree gchar *version = NULL; g_autoptr(GByteArray) st = NULL; /* get version */ if (!fu_igsc_device_get_version_raw(igsc_parent, self->partition_version, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to get oprom version: "); return FALSE; } st = fu_struct_igsc_oprom_version_parse(buf, sizeof(buf), 0x0, error); if (st == NULL) return FALSE; self->major_version = fu_struct_igsc_oprom_version_get_major(st); version = g_strdup_printf("%u.%u.%u.%u", self->major_version, fu_struct_igsc_oprom_version_get_minor(st), fu_struct_igsc_oprom_version_get_hotfix(st), fu_struct_igsc_oprom_version_get_build(st)); fu_device_set_version(device, version); /* success */ return TRUE; } static FuFirmware * fu_igsc_oprom_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuIgscOpromDevice *self = FU_IGSC_OPROM_DEVICE(device); FuIgscDevice *igsc_parent = FU_IGSC_DEVICE(fu_device_get_parent(device)); guint16 vid = fu_device_get_vid(FU_DEVICE(igsc_parent)); guint16 pid = fu_device_get_pid(FU_DEVICE(igsc_parent)); guint16 subsys_vendor_id = fu_igsc_device_get_ssvid(igsc_parent); guint16 subsys_device_id = fu_igsc_device_get_ssvid(igsc_parent); g_autoptr(FuFirmware) firmware = NULL; g_autoptr(FuFirmware) fw_linear = fu_linear_firmware_new(FU_TYPE_IGSC_OPROM_FIRMWARE); /* parse container */ if (!fu_firmware_parse_stream(fw_linear, stream, 0x0, flags, error)) return NULL; /* get correct image */ firmware = fu_firmware_get_image_by_idx(fw_linear, self->payload_type, error); if (firmware == NULL) return NULL; /* major numbers must be the same, unless the device's major is zero, * because some platforms may come originally with 0 major number */ if (fu_igsc_oprom_firmware_get_major_version(FU_IGSC_OPROM_FIRMWARE(firmware)) != self->major_version && self->major_version != 0) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "image major version is not compatible, got 0x%x, expected 0x%x", fu_igsc_oprom_firmware_get_major_version(FU_IGSC_OPROM_FIRMWARE(firmware)), self->major_version); return NULL; } /* If oprom_code_devid_enforcement is set to True: * The update is accepted only if the update file contains a Device IDs allowlist * and the card's {VID, DID, SSVID, SSDID} is in the update file's Device IDs allowlist. * If the flag doesn't exist or is False: * The update is accepted only if the update file does not contain a Device ID allowlist */ if (self->payload_type == FU_IGSC_FWU_HECI_PAYLOAD_TYPE_OPROM_CODE) { if (fu_igsc_device_get_oprom_code_devid_enforcement(igsc_parent)) { if (!fu_igsc_oprom_firmware_match_device(FU_IGSC_OPROM_FIRMWARE(firmware), vid, pid, subsys_vendor_id, subsys_device_id, error)) return NULL; } else { if (fu_igsc_oprom_firmware_has_allowlist( FU_IGSC_OPROM_FIRMWARE(firmware))) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device is not enforcing devid match, but " "firmware provided allowlist"); return NULL; } } } /* If the Device IDs allowlist (0x37) exists in the update image: * The update is accepted only if the card's {VID, DID, SSVID, SSDID} * is in the update image's Device IDs allowlist. * If the Device IDs allowlist (0x37) doesn't exist in the update image: * The update is accepted only if the card's SSVID and SSDID are zero. */ if (self->payload_type == FU_IGSC_FWU_HECI_PAYLOAD_TYPE_OPROM_DATA) { if (fu_igsc_oprom_firmware_has_allowlist(FU_IGSC_OPROM_FIRMWARE(firmware))) { if (!fu_igsc_oprom_firmware_match_device(FU_IGSC_OPROM_FIRMWARE(firmware), vid, pid, subsys_vendor_id, subsys_device_id, error)) return NULL; } else { if (subsys_vendor_id != 0x0 || subsys_device_id != 0x0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "firmware does not specify allowlist and SSVID " "and SSDID are nonzero"); return NULL; } } } /* success */ return g_steal_pointer(&firmware); } static gboolean fu_igsc_oprom_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuIgscOpromDevice *self = FU_IGSC_OPROM_DEVICE(device); FuIgscDevice *igsc_parent = FU_IGSC_DEVICE(fu_device_get_parent(device)); g_autoptr(GInputStream) stream_payload = NULL; /* get image */ stream_payload = fu_firmware_get_stream(firmware, error); if (stream_payload == NULL) return FALSE; /* OPROM image doesn't require metadata */ return fu_igsc_device_write_blob(igsc_parent, self->payload_type, NULL, stream_payload, progress, error); } static gboolean fu_igsc_oprom_device_prepare(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { /* set PCI power policy */ return fu_device_prepare(fu_device_get_parent(device), progress, flags, error); } static gboolean fu_igsc_oprom_device_cleanup(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { /* set PCI power policy */ return fu_device_cleanup(fu_device_get_parent(device), progress, flags, error); } static void fu_igsc_oprom_device_init(FuIgscOpromDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); fu_device_add_protocol(FU_DEVICE(self), "com.intel.gsc"); } static void fu_igsc_oprom_device_class_init(FuIgscOpromDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_igsc_oprom_device_to_string; device_class->probe = fu_igsc_oprom_device_probe; device_class->setup = fu_igsc_oprom_device_setup; device_class->prepare_firmware = fu_igsc_oprom_device_prepare_firmware; device_class->write_firmware = fu_igsc_oprom_device_write_firmware; device_class->prepare = fu_igsc_oprom_device_prepare; device_class->cleanup = fu_igsc_oprom_device_cleanup; } FuIgscOpromDevice * fu_igsc_oprom_device_new(FuContext *ctx, FuIgscFwuHeciPayloadType payload_type) { FuIgscOpromDevice *self = g_object_new(FU_TYPE_IGSC_OPROM_DEVICE, "context", ctx, NULL); self->payload_type = payload_type; return FU_IGSC_OPROM_DEVICE(self); } fwupd-2.0.10/plugins/intel-gsc/fu-igsc-oprom-device.h000066400000000000000000000007501501337203100223570ustar00rootroot00000000000000/* * Copyright 2022 Intel, Inc. * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later OR Apache-2.0 */ #pragma once #include #include "fu-igsc-heci.h" #define FU_TYPE_IGSC_OPROM_DEVICE (fu_igsc_oprom_device_get_type()) G_DECLARE_FINAL_TYPE(FuIgscOpromDevice, fu_igsc_oprom_device, FU, IGSC_OPROM_DEVICE, FuDevice) FuIgscOpromDevice * fu_igsc_oprom_device_new(FuContext *ctx, FuIgscFwuHeciPayloadType payload_type); fwupd-2.0.10/plugins/intel-gsc/fu-igsc-oprom-firmware.c000066400000000000000000000156441501337203100227370ustar00rootroot00000000000000/* * Copyright 2022 Intel * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-igsc-oprom-firmware.h" #include "fu-igsc-struct.h" struct _FuIgscOpromFirmware { FuOpromFirmware parent_instance; guint16 major_version; GPtrArray *device_infos; /* of FuIgscFwdataDeviceInfo4 */ }; G_DEFINE_TYPE(FuIgscOpromFirmware, fu_igsc_oprom_firmware, FU_TYPE_OPROM_FIRMWARE) static void fu_igsc_oprom_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuIgscOpromFirmware *self = FU_IGSC_OPROM_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "major_version", self->major_version); fu_xmlb_builder_insert_kx(bn, "device_infos", self->device_infos->len); } guint16 fu_igsc_oprom_firmware_get_major_version(FuIgscOpromFirmware *self) { g_return_val_if_fail(FU_IS_IGSC_OPROM_FIRMWARE(self), G_MAXUINT16); return self->major_version; } gboolean fu_igsc_oprom_firmware_has_allowlist(FuIgscOpromFirmware *self) { g_return_val_if_fail(FU_IS_IGSC_OPROM_FIRMWARE(self), FALSE); return self->device_infos->len > 0; } gboolean fu_igsc_oprom_firmware_match_device(FuIgscOpromFirmware *self, guint16 vendor_id, guint16 device_id, guint16 subsys_vendor_id, guint16 subsys_device_id, GError **error) { g_return_val_if_fail(FU_IS_IGSC_OPROM_FIRMWARE(self), FALSE); for (guint i = 0; i < self->device_infos->len; i++) { FuIgscFwdataDeviceInfo4 *info = g_ptr_array_index(self->device_infos, i); if (fu_igsc_fwdata_device_info4_get_vendor_id(info) == 0x0 && fu_igsc_fwdata_device_info4_get_device_id(info) == 0x0 && fu_igsc_fwdata_device_info4_get_subsys_vendor_id(info) == subsys_vendor_id && fu_igsc_fwdata_device_info4_get_subsys_device_id(info) == subsys_device_id) return TRUE; if (fu_igsc_fwdata_device_info4_get_vendor_id(info) == vendor_id && fu_igsc_fwdata_device_info4_get_device_id(info) == device_id && fu_igsc_fwdata_device_info4_get_subsys_vendor_id(info) == subsys_vendor_id && fu_igsc_fwdata_device_info4_get_subsys_device_id(info) == subsys_device_id) return TRUE; } /* not us */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "could not find 0x%04x:0x%04x 0x%04x:0x%04x in the image", vendor_id, device_id, subsys_vendor_id, subsys_device_id); return FALSE; } static gboolean fu_igsc_oprom_firmware_parse_extension(FuIgscOpromFirmware *self, FuFirmware *fw, GError **error) { gsize streamsz = 0; g_autoptr(GInputStream) stream = NULL; /* get data */ stream = fu_firmware_get_stream(fw, error); if (stream == NULL) return FALSE; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (fu_firmware_get_idx(fw) == FU_IGSC_FWU_EXT_TYPE_DEVICE_TYPE) { for (gsize offset = 0; offset < streamsz; offset += FU_IGSC_FWDATA_DEVICE_INFO2_SIZE) { g_autoptr(FuIgscFwdataDeviceInfo2) st = NULL; g_autoptr(FuIgscFwdataDeviceInfo4) st4 = fu_igsc_fwdata_device_info4_new(); st = fu_igsc_fwdata_device_info2_parse_stream(stream, offset, error); if (st == NULL) return FALSE; fu_igsc_fwdata_device_info4_set_vendor_id(st4, 0x0); fu_igsc_fwdata_device_info4_set_device_id(st4, 0x0); fu_igsc_fwdata_device_info4_set_subsys_vendor_id( st4, fu_igsc_fwdata_device_info2_get_subsys_vendor_id(st)); fu_igsc_fwdata_device_info4_set_subsys_device_id( st4, fu_igsc_fwdata_device_info2_get_subsys_device_id(st)); g_ptr_array_add(self->device_infos, g_steal_pointer(&st4)); } } else if (fu_firmware_get_idx(fw) == FU_IGSC_FWU_EXT_TYPE_DEVICE_ID_ARRAY) { for (gsize offset = 0; offset < streamsz; offset += FU_IGSC_FWDATA_DEVICE_INFO4_SIZE) { g_autoptr(FuIgscFwdataDeviceInfo4) st = NULL; st = fu_igsc_fwdata_device_info4_parse_stream(stream, offset, error); if (st == NULL) return FALSE; g_ptr_array_add(self->device_infos, g_steal_pointer(&st)); } } /* success */ return TRUE; } static gboolean fu_igsc_oprom_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuIgscOpromFirmware *self = FU_IGSC_OPROM_FIRMWARE(firmware); g_autoptr(FuFirmware) fw_cpd = NULL; g_autoptr(GPtrArray) cpd_imgs = NULL; /* FuOpromFirmware->parse */ if (!FU_FIRMWARE_CLASS(fu_igsc_oprom_firmware_parent_class) ->parse(firmware, stream, flags, error)) return FALSE; /* check sanity */ if (fu_oprom_firmware_get_subsystem(FU_OPROM_FIRMWARE(firmware)) != FU_OPROM_SUBSYSTEM_EFI_BOOT_SRV_DRV) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid subsystem, got 0x%x, expected 0x%x", fu_oprom_firmware_get_subsystem(FU_OPROM_FIRMWARE(firmware)), (guint)FU_OPROM_SUBSYSTEM_EFI_BOOT_SRV_DRV); return FALSE; } if (fu_oprom_firmware_get_machine_type(FU_OPROM_FIRMWARE(firmware)) != FU_OPROM_MACHINE_TYPE_X64) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid machine type, got 0x%x, expected 0x%x", fu_oprom_firmware_get_machine_type(FU_OPROM_FIRMWARE(firmware)), (guint)FU_OPROM_MACHINE_TYPE_X64); return FALSE; } if (fu_oprom_firmware_get_compression_type(FU_OPROM_FIRMWARE(firmware)) != FU_OPROM_COMPRESSION_TYPE_NONE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid compression type, got 0x%x, expected 0x%x (uncompressed)", fu_oprom_firmware_get_compression_type(FU_OPROM_FIRMWARE(firmware)), (guint)FU_OPROM_COMPRESSION_TYPE_NONE); return FALSE; } /* get CPD */ fw_cpd = fu_firmware_get_image_by_id(firmware, "cpd", error); if (fw_cpd == NULL) return FALSE; if (!FU_IS_IFWI_CPD_FIRMWARE(fw_cpd)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "CPD was not FuIfwiCpdFirmware"); return FALSE; } /* parse all the manifest extensions */ cpd_imgs = fu_firmware_get_images(fw_cpd); for (guint i = 0; i < cpd_imgs->len; i++) { FuFirmware *img = g_ptr_array_index(cpd_imgs, i); if (!fu_igsc_oprom_firmware_parse_extension(self, img, error)) return FALSE; } /* success */ return TRUE; } static void fu_igsc_oprom_firmware_init(FuIgscOpromFirmware *self) { self->device_infos = g_ptr_array_new_with_free_func((GDestroyNotify)fu_igsc_fwdata_device_info4_unref); } static void fu_igsc_oprom_firmware_finalize(GObject *object) { FuIgscOpromFirmware *self = FU_IGSC_OPROM_FIRMWARE(object); g_ptr_array_unref(self->device_infos); G_OBJECT_CLASS(fu_igsc_oprom_firmware_parent_class)->finalize(object); } static void fu_igsc_oprom_firmware_class_init(FuIgscOpromFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_igsc_oprom_firmware_finalize; firmware_class->parse = fu_igsc_oprom_firmware_parse; firmware_class->export = fu_igsc_oprom_firmware_export; } fwupd-2.0.10/plugins/intel-gsc/fu-igsc-oprom-firmware.h000066400000000000000000000013641501337203100227360ustar00rootroot00000000000000/* * Copyright 2022 Intel * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_IGSC_OPROM_FIRMWARE (fu_igsc_oprom_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuIgscOpromFirmware, fu_igsc_oprom_firmware, FU, IGSC_OPROM_FIRMWARE, FuOpromFirmware) guint16 fu_igsc_oprom_firmware_get_major_version(FuIgscOpromFirmware *self); gboolean fu_igsc_oprom_firmware_has_allowlist(FuIgscOpromFirmware *self); gboolean fu_igsc_oprom_firmware_match_device(FuIgscOpromFirmware *self, guint16 vendor_id, guint16 device_id, guint16 subsys_vendor_id, guint16 subsys_device_id, GError **error); fwupd-2.0.10/plugins/intel-gsc/fu-igsc-plugin.c000066400000000000000000000023771501337203100212660ustar00rootroot00000000000000/* * Copyright 2022 Intel * * SPDX-License-Identifier: LGPL-2.1-or-later OR Apache-2.0 */ #include "config.h" #include "fu-igsc-aux-device.h" #include "fu-igsc-aux-firmware.h" #include "fu-igsc-code-firmware.h" #include "fu-igsc-device.h" #include "fu-igsc-oprom-device.h" #include "fu-igsc-oprom-firmware.h" #include "fu-igsc-plugin.h" struct _FuIgscPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuIgscPlugin, fu_igsc_plugin, FU_TYPE_PLUGIN) static void fu_igsc_plugin_init(FuIgscPlugin *self) { } static void fu_igsc_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_udev_subsystem(plugin, "mei"); fu_plugin_set_device_gtype_default(plugin, FU_TYPE_IGSC_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_IGSC_OPROM_DEVICE); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_IGSC_AUX_DEVICE); /* coverage */ fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_IGSC_CODE_FIRMWARE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_IGSC_AUX_FIRMWARE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_IGSC_OPROM_FIRMWARE); } static void fu_igsc_plugin_class_init(FuIgscPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_igsc_plugin_constructed; } fwupd-2.0.10/plugins/intel-gsc/fu-igsc-plugin.h000066400000000000000000000003461501337203100212650ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuIgscPlugin, fu_igsc_plugin, FU, IGSC_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/intel-gsc/fu-igsc.rs000066400000000000000000000064231501337203100201700ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(Parse)] #[repr(C, packed)] struct FuStructIgscOpromVersion { major: u16le, minor: u16le, hotfix: u16le, build: u16le, } #[derive(New, Getters)] #[repr(C, packed)] struct FuStructIgscFwVersion { project: [char; 4], // project code name hotfix: u16le, build: u16le, } #[derive(ParseStream)] #[repr(C, packed)] struct FuIgscFwdataDeviceInfo2 { subsys_vendor_id: u16le, subsys_device_id: u16le, } #[derive(ParseStream, New)] #[repr(C, packed)] struct FuIgscFwdataDeviceInfo4 { vendor_id: u16le, device_id: u16le, subsys_vendor_id: u16le, subsys_device_id: u16le, } #[derive(ParseStream, Default)] #[repr(C, packed)] struct FuStructIgscFwuGwsImageInfo { format_version: u32le == 0x1, instance_id: u32le, _reserved: [u32; 14], } // represents a GSC FW sub-partition such as FTPR, RBEP #[derive(Getters)] #[repr(C, packed)] struct FuStructIgscFwuFwImageData { version_major: u16le, version_minor: u16le, version_hotfix: u16le, version_build: u16le, flags: u16le, fw_type: u8, fw_sub_type: u8, arb_svn: u32le, tcb_svn: u32le, vcn: u32le, } #[derive(Getters)] #[repr(C, packed)] struct FuStructIgscFwuIupData { iup_name: u32le, flags: u16le, _reserved: u16le, svn: u32le, vcn: u32le, } #[derive(Getters, Default)] #[repr(C, packed)] struct FuStructIgscFwuHeciImageMetadata { version_format: u32le = 0x1, } #[derive(ParseStream, Default)] #[repr(C, packed)] struct FuStructIgscFwuImageMetadataV1 { version_format: u32le = 0x1, // struct IgscFwuHeciImageMetadata project: [char; 4], version_hotfix: u16le, // version of the overall IFWI image, i.e. the combination of IPs version_build: u16le, // struct FuStructIgscFwuFwImageData // struct FuStructIgscFwuIupData } enum FuIgscFwuHeciPartitionVersion { Invalid, GfxFw, OpromData, OpromCode, } enum FuIgscFwuHeciPayloadType { Invalid, GfxFw, OpromData, OpromCode, Fwdata = 5, } enum FuIgscFwuHeciCommandId { Invalid, Start, // start firmware updated flow Data, // send firmware data to device End, // last command in update GetVersion, // retrieve version of a firmware NoUpdate, // do not wait for firmware update GetIpVersion, // retrieve version of a partition GetConfig, // get hardware config Status, // get status of most recent update GetGfxDataUpdateInfo, // get signed firmware data info GetSubsystemIds, // get subsystem ids (VID/DID) } #[derive(ParseStream)] #[repr(C, packed)] struct FuStructIgscFwdataVersion { oem_manuf_data_version: u32le, major_version: u16le, major_vcn: u16le, } #[repr(C, packed)] struct FuStructIgscFwdataUpdateExt { extension_type: u32le, extension_length: u32le, oem_manuf_data_version: u32le, major_vcn: u16le, flags: u16le, } enum FuIgscFwuExtType { DeviceIds = 0x25, FwdataUpdate = 0x1D, DeviceType = 0x07, SignedPackageInfo = 0x0F, IfwiPartMan = 0x16, DeviceIdArray = 0x37, } fwupd-2.0.10/plugins/intel-gsc/igsc.quirk000066400000000000000000000050011501337203100202560ustar00rootroot00000000000000[PCI\DRIVER_xe] Plugin = igsc [PCI\VEN_8086&DEV_4905] Name = DG1 [PCI\VEN_8086&DEV_4906] Name = DG1 [PCI\VEN_8086&DEV_4907] Name = DG1 [PCI\VEN_8086&DEV_4908] Name = DG1 [PCI\VEN_8086&DEV_0201] Name = Xe_HP SDV [PCI\VEN_8086&DEV_0202] Name = Xe_HP SDV [PCI\VEN_8086&DEV_0203] Name = Xe_HP SDV [PCI\VEN_8086&DEV_0204] Name = Xe_HP SDV [PCI\VEN_8086&DEV_0205] Name = Xe_HP SDV [PCI\VEN_8086&DEV_0206] Name = Xe_HP SDV [PCI\VEN_8086&DEV_0207] Name = Xe_HP SDV [PCI\VEN_8086&DEV_0208] Name = Xe_HP SDV [PCI\VEN_8086&DEV_0209] Name = Xe_HP SDV [PCI\VEN_8086&DEV_020A] Name = Xe_HP SDV [PCI\VEN_8086&DEV_020B] Name = Xe_HP SDV [PCI\VEN_8086&DEV_020C] Name = Xe_HP SDV [PCI\VEN_8086&DEV_020D] Name = Xe_HP SDV [PCI\VEN_8086&DEV_020E] Name = Xe_HP SDV [PCI\VEN_8086&DEV_020F] Name = Xe_HP SDV [PCI\VEN_8086&DEV_0210] Name = Xe_HP SDV [PCI\VEN_8086&DEV_FF25] Name = Xe_HP SDV [PCI\VEN_8086&DEV_4F80] Name = DG2_SOC1 Flags = has-aux,has-oprom [PCI\VEN_8086&DEV_4F81] Name = DG2_SOC1 Flags = has-aux,has-oprom [PCI\VEN_8086&DEV_4F82] Name = DG2_SOC1 Flags = has-aux,has-oprom [PCI\VEN_8086&DEV_4F83] Name = DG2_SOC1 Flags = has-aux,has-oprom [PCI\VEN_8086&DEV_4F84] Name = DG2_SOC1 Flags = has-aux,has-oprom [PCI\VEN_8086&DEV_5690] Name = Arc A770M Flags = has-aux,has-oprom [PCI\VEN_8086&DEV_5691] Name = Arc A730M Flags = has-aux,has-oprom [PCI\VEN_8086&DEV_5692] Name = Arc A550M Flags = has-aux,has-oprom [PCI\VEN_8086&DEV_56A0] Name = Arc A770 Flags = has-aux,has-oprom [PCI\VEN_8086&DEV_56A1] Name = Arc A750 Flags = has-aux,has-oprom [PCI\VEN_8086&DEV_56A2] Name = Arc A580 Flags = has-aux,has-oprom [PCI\VEN_8086&DEV_56C0] Name = DG2_SOC1 Flags = has-aux,has-oprom [PCI\VEN_8086&DEV_4F87] Name = DG2_SOC2 Flags = has-aux,has-oprom [PCI\VEN_8086&DEV_4F88] Name = DG2_SOC2 Flags = has-aux,has-oprom [PCI\VEN_8086&DEV_5693] Name = Arc A370M Flags = has-aux,has-oprom [PCI\VEN_8086&DEV_5694] Name = Arc A350M Flags = has-aux,has-oprom [PCI\VEN_8086&DEV_5695] Name = Iris(R) Xe MAX A200M Flags = has-aux,has-oprom [PCI\VEN_8086&DEV_56A5] Name = Arc A380 Flags = has-aux,has-oprom [PCI\VEN_8086&DEV_56A6] Name = Arc A310 Flags = has-aux,has-oprom [PCI\VEN_8086&DEV_56A7] Name = DG2_SOC2 Flags = has-aux,has-oprom [PCI\VEN_8086&DEV_56A8] Name = DG2_SOC2 Flags = has-aux,has-oprom [PCI\VEN_8086&DEV_56A9] Name = DG2_SOC2 Flags = has-aux,has-oprom [PCI\VEN_8086&DEV_0BD0] Name = PVC [PCI\VEN_8086&DEV_0BD5] Name = PVC [PCI\VEN_8086&DEV_56C1] Name = ATS-M3 Flags = has-aux,has-oprom [PCI\VEN_8086&DEV_E20B] Flags = has-oprom [PCI\VEN_8086&DEV_E20C] Flags = has-oprom fwupd-2.0.10/plugins/intel-gsc/meson.build000066400000000000000000000012741501337203100204260ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginIgsc"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('igsc.quirk') plugin_builtins += static_library('fu_plugin_igsc', rustgen.process('fu-igsc.rs'), sources: [ 'fu-igsc-plugin.c', 'fu-igsc-device.c', 'fu-igsc-code-firmware.c', 'fu-igsc-oprom-firmware.c', 'fu-igsc-aux-device.c', 'fu-igsc-aux-firmware.c', 'fu-igsc-oprom-device.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) enumeration_data += files('tests/intel-gsc-setup.json') device_tests += files('tests/intel-gsc.json') endif fwupd-2.0.10/plugins/intel-gsc/tests/000077500000000000000000000000001501337203100174225ustar00rootroot00000000000000fwupd-2.0.10/plugins/intel-gsc/tests/intel-gsc-setup.json000066400000000000000000000142761501337203100233520ustar00rootroot00000000000000{ "FwupdVersion": "2.0.9", "UsbDevices": [ { "Created": "2025-04-28T15:30:52.356627Z", "GType": "FuUdevDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0/0000:02:01.0/0000:03:00.0/xe.mei-gscfi.768/mei/mei0", "DeviceFile": "/dev/mei0", "Subsystem": "mei", "Vendor": 32902, "Model": 57867, "Events": [ { "Id": "GetSymlinkTarget:Attr=subsystem", "Data": "mei" }, { "Id": "GetSymlinkTarget:Attr=driver" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "MAJOR=235\nMINOR=0\nDEVNAME=mei0" }, { "Id": "ReadProp:Key=DEVNAME", "Data": "mei0" }, { "Id": "ReadAttr:Attr=vendor" }, { "Id": "ReadAttr:Attr=device" }, { "Id": "ReadAttr:Attr=class" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "GetBackendParent:Subsystem=pci", "GType": "FuPciDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0/0000:02:01.0/0000:03:00.0", "PhysicalId": "PCI_SLOT_NAME=0000:03:00.0" }, { "Id": "GetSymlinkTarget:Attr=subsystem", "Data": "pci" }, { "Id": "GetSymlinkTarget:Attr=driver", "Data": "xe" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "DRIVER=xe\nPCI_CLASS=30000\nPCI_ID=8086:E20B\nPCI_SUBSYS_ID=8086:1100\nPCI_SLOT_NAME=0000:03:00.0\nMODALIAS=pci:v00008086d0000E20Bsv00008086sd00001100bc03sc00i00" }, { "Id": "ReadProp:Key=DEVNAME" }, { "Id": "ReadAttr:Attr=vendor", "Data": "0x8086" }, { "Id": "ReadAttr:Attr=device", "Data": "0xe20b" }, { "Id": "ReadAttr:Attr=class", "Data": "0x030000" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "DRIVER=xe\nPCI_CLASS=30000\nPCI_ID=8086:E20B\nPCI_SUBSYS_ID=8086:1100\nPCI_SLOT_NAME=0000:03:00.0\nMODALIAS=pci:v00008086d0000E20Bsv00008086sd00001100bc03sc00i00" }, { "Id": "ReadProp:Key=DEVNAME" }, { "Id": "ReadAttr:Attr=vendor", "Data": "0x8086" }, { "Id": "ReadAttr:Attr=device", "Data": "0xe20b" }, { "Id": "ReadAttr:Attr=class", "Data": "0x030000" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "GetBackendParent:Subsystem=i2c", "Error": 8, "ErrorMsg": "no parent with subsystem i2c" }, { "Id": "ReadAttr:Attr=class", "Data": "0x030000" }, { "Id": "ReadAttr:Attr=vbios_version" }, { "Id": "ReadAttr:Attr=revision", "Data": "0x00" }, { "Id": "ReadAttr:Attr=subsystem_vendor", "Data": "0x8086" }, { "Id": "ReadAttr:Attr=subsystem_device", "Data": "0x1100" }, { "Id": "ReadProp:Key=PCI_SLOT_NAME", "Data": "0000:03:00.0" }, { "Id": "GetBackendParent:Subsystem=(null)", "GType": "FuUdevDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0/0000:02:01.0/0000:03:00.0/xe.mei-gscfi.768" }, { "Id": "GetSymlinkTarget:Attr=subsystem", "Data": "auxiliary" }, { "Id": "GetSymlinkTarget:Attr=driver", "Data": "mei_gsc" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "DRIVER=mei_gsc\nMODALIAS=auxiliary:xe.mei-gscfi" }, { "Id": "ReadProp:Key=DEVNAME" }, { "Id": "ReadAttr:Attr=vendor" }, { "Id": "ReadAttr:Attr=device" }, { "Id": "ReadAttr:Attr=class" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ListAttr", "Data": "uevent\nxe.mei-gscfi.768-fe2af7a6-ef22-4b45-872f-176b0bbc8b43\nxe.mei-gscfi.768-0f73db04-97ab-4125-b893-e904ad0d5464\npower\nxe.mei-gscfi.768-afd7c08e-ae0d-4dcf-9d88-b0e6941558d6\nmei\nxe.mei-gscfi.768-e2c2afa2-3817-4d19-9d95-06b16b588a5d\ndriver\nxe.mei-gscfi.768-87d90ca5-3495-4559-8105-3fbfa37b8b79\nsubsystem\nxe.mei-gscfi.768-46e0c1fb-a546-414f-9170-b7f46d57b4ad" }, { "Id": "ReadAttr:Attr=fw_status", "Data": "MDAwMDAzNTUKODAyMTA0MDgKMDAwMDAwMDAKMDAwMDAwMDAKODAwMjAwMDEKMDAwMDAwMDAK" }, { "Id": "Ioctl:Request=0xc0104801,Data=pvcq/iLvRUuHLxdrC7yLQw==,Length=0x10", "DataOut": "ABAAAALvRUuHLxdrC7yLQw==" }, { "Id": "Write:Data=ChwAAA==,Length=0x4" }, { "Id": "Read:Length=0x1000", "Data": "CpwAAAQAAAAUCAAAAwACAG1Q/wFoUP8B" }, { "Id": "Ioctl:Request=0xc0104801,Data=pQzZh5U0WUWBBT+/o3uLeQ==,Length=0x10", "DataOut": "DBAAAAE0WUWBBT+/o3uLeQ==" }, { "Id": "Write:Data=BgAAAAEAAAA=,Length=0x8" }, { "Id": "Read:Length=0x1c", "Data": "BgEAAAAAAAAAAAAAAQAAAAgAAABCTUdfFQBxBA==" }, { "Id": "Write:Data=CgAAAAAAAAAAAAAA,Length=0xc" }, { "Id": "Read:Length=0x18", "Data": "CgEAAAAAAAAAAAAAhoAAEQAAAAAAAAAA" }, { "Id": "Write:Data=BgAAAAMAAAA=,Length=0x8" }, { "Id": "Read:Length=0x1c", "Data": "BgEAAAAAAAAAAAAAAwAAAAgAAAAXABsEAAAAAA==" }, { "Id": "Write:Data=BgAAAAIAAAA=,Length=0x8" }, { "Id": "Read:Length=0x1c", "Data": "BgEAAAAAAAAAAAAAAgAAAAgAAAAXABsEAAAAAA==" } ] } ] } fwupd-2.0.10/plugins/intel-gsc/tests/intel-gsc.json000066400000000000000000000014231501337203100222020ustar00rootroot00000000000000{ "name": "Intel GSC", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/intel-gsc-setup.json", "components": [ { "protocol": "com.intel.gsc", "version": "21.1137", "guids": [ "c3808bdf-c31b-5c03-905f-6f223848cae0" ] }, { "name": "OptionROM Code", "protocol": "com.intel.gsc", "version": "23.1051.0.0", "guids": [ "055b452f-5860-53fd-bb74-5316f955de6b" ] }, { "name": "OptionROM Data", "protocol": "com.intel.gsc", "version": "23.1051.0.0", "guids": [ "41a3c2f5-bbe0-56e7-ae2f-7d4aa77e68a6" ] } ] } ] } fwupd-2.0.10/plugins/intel-mchi/000077500000000000000000000000001501337203100164245ustar00rootroot00000000000000fwupd-2.0.10/plugins/intel-mchi/README.md000066400000000000000000000031751501337203100177110ustar00rootroot00000000000000--- title: Plugin: Intel MCHI --- ## Introduction This plugin is used to talk to the Intel ME device using the Management Controller Host Interface "MCHI" interface, also sometimes called "MCA". It allows us to get the Platform Key as used for BootGuard. ## GUID Generation These devices use the existing GUIDs provided by the ME host interfaces. ## Metadata There have been several BootGuard key leaks that can be detected using the ME device. The metadata needed to match the KM checksum is found in the metadata, typically obtained from the LVFS project. This sets the `leaked-km` private flag which then causes the HSI `org.fwupd.hsi.Mei.KeyManifest` attribute to fail, and also adds a device inhibit which shows in the command line and GUI tools. The `org.linuxfoundation.bootguard.config` component is currently used to match against both the MCA and MKHI ME devices. The latest cabinet archive can also be installed into the `vendor-firmware` remote found in `/usr/share/fwupd/remotes.d/vendor/firmware/` which allows the detection to work even when offline -- although using the LVFS source is recommended for most users. New *OEM Public Key Hash* values found from `MEInfo` or calculated manually should be added to the checksums page on the LVFS. ## Vendor ID Security The devices are not upgradable and thus require no vendor ID set. ## External Interface Access This plugin requires `ioctl(IOCTL_MEI_CONNECT_CLIENT)` to `/dev/mei0`. ## Version Considerations This plugin has been available since fwupd version `2.0.9`, although the functionality was previously introduced in the `intel-me` plugin released with fwupd version `1.8.7`. fwupd-2.0.10/plugins/intel-mchi/fu-intel-mchi-device.c000066400000000000000000000116261501337203100224740ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-intel-mchi-device.h" struct _FuIntelMchiDevice { FuHeciDevice parent_instance; }; G_DEFINE_TYPE(FuIntelMchiDevice, fu_intel_mchi_device, FU_TYPE_HECI_DEVICE) #define FU_INTEL_MCHI_DEVICE_FLAG_LEAKED_KM "leaked-km" static gboolean fu_intel_mchi_device_add_checksum_for_id(FuIntelMchiDevice *self, guint32 file_id, guint32 section, GError **error) { g_autofree gchar *checksum = NULL; g_autoptr(GByteArray) buf = NULL; /* * Call READ_FILE_EX with a larger-than-required data size -- which hopefully works when * SHA512 results start being returned too. * * Icelake/Jasperlake/Cometlake: 0x20 (SHA256) * Elkhartlake/Tigerlake/Alderlake/Raptorlake: 0x30 (SHA384) */ buf = fu_heci_device_read_file_ex(FU_HECI_DEVICE(self), file_id, section, 0x40, error); if (buf == NULL) return FALSE; /* convert into checksum, but only if non-zero and set */ checksum = fu_byte_array_to_string(buf); if (g_str_has_prefix(checksum, "0000000000000000") || g_str_has_prefix(checksum, "ffffffffffffffff")) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "checksum %s was invalid", checksum); return FALSE; } fu_device_add_checksum(FU_DEVICE(self), checksum); /* success */ return TRUE; } static gboolean fu_intel_mchi_device_setup(FuDevice *device, GError **error) { FuIntelMchiDevice *self = FU_INTEL_MCHI_DEVICE(device); const guint32 file_ids[] = { 0x40002300, /* CometLake: OEM Public Key Hash */ 0x40005B00, /* TigerLake: 1st OEM Public Key Hash */ 0x40005C00, /* TigerLake: 2nd OEM Public Key Hash */ }; /* connect */ if (!fu_mei_device_connect(FU_MEI_DEVICE(device), FU_HECI_DEVICE_UUID_MCHI, 0, error)) { g_prefix_error(error, "failed to connect: "); return FALSE; } /* look for all the possible OEM Public Key hashes using the CML+ method */ for (guint i = 0; i < G_N_ELEMENTS(file_ids); i++) { g_autoptr(GError) error_local = NULL; if (!fu_intel_mchi_device_add_checksum_for_id(self, file_ids[i], 0x0, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA)) { g_debug("ignoring: %s", error_local->message); continue; } g_warning("failed to get public key using file-id 0x%x: %s", file_ids[i], error_local->message); } } /* no point even adding */ if (fu_device_get_checksums(device)->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no OEM public keys found"); return FALSE; } /* success */ return TRUE; } static void fu_intel_mchi_device_add_security_attrs(FuDevice *device, FuSecurityAttrs *attrs) { FuIntelMchiDevice *self = FU_INTEL_MCHI_DEVICE(device); g_autoptr(FwupdSecurityAttr) attr = NULL; /* create attr */ attr = fu_device_security_attr_new(FU_DEVICE(self), FWUPD_SECURITY_ATTR_ID_MEI_KEY_MANIFEST); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_VALID); fu_security_attrs_append(attrs, attr); /* verify keys */ if (fu_device_get_checksums(device)->len == 0) { fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA); return; } if (fu_device_has_private_flag(device, FU_INTEL_MCHI_DEVICE_FLAG_LEAKED_KM)) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_intel_mchi_device_version_notify_cb(FuDevice *device, GParamSpec *pspec, gpointer user_data) { if (fu_device_has_private_flag(device, FU_INTEL_MCHI_DEVICE_FLAG_LEAKED_KM)) fu_device_inhibit(device, "leaked-km", "Provisioned with a leaked private key"); } static void fu_intel_mchi_device_init(FuIntelMchiDevice *self) { fu_device_set_logical_id(FU_DEVICE(self), "MCHI"); fu_device_set_name(FU_DEVICE(self), "BootGuard Configuration"); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE_CHILD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_ONLY_CHECKSUM); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_FLAGS); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_COMPUTER); fu_device_register_private_flag(FU_DEVICE(self), FU_INTEL_MCHI_DEVICE_FLAG_LEAKED_KM); g_signal_connect(FWUPD_DEVICE(self), "notify::private-flags", G_CALLBACK(fu_intel_mchi_device_version_notify_cb), NULL); } static void fu_intel_mchi_device_class_init(FuIntelMchiDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_intel_mchi_device_setup; device_class->add_security_attrs = fu_intel_mchi_device_add_security_attrs; } fwupd-2.0.10/plugins/intel-mchi/fu-intel-mchi-device.h000066400000000000000000000004771501337203100225030ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_INTEL_MCHI_DEVICE (fu_intel_mchi_device_get_type()) G_DECLARE_FINAL_TYPE(FuIntelMchiDevice, fu_intel_mchi_device, FU, INTEL_MCHI_DEVICE, FuHeciDevice) fwupd-2.0.10/plugins/intel-mchi/fu-intel-mchi-plugin.c000066400000000000000000000014501501337203100225250ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-intel-mchi-device.h" #include "fu-intel-mchi-plugin.h" struct _FuIntelMchiPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuIntelMchiPlugin, fu_intel_mchi_plugin, FU_TYPE_PLUGIN) static void fu_intel_mchi_plugin_init(FuIntelMchiPlugin *self) { } static void fu_intel_mchi_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_udev_subsystem(plugin, "mei"); fu_plugin_add_device_gtype(plugin, FU_TYPE_INTEL_MCHI_DEVICE); } static void fu_intel_mchi_plugin_class_init(FuIntelMchiPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_intel_mchi_plugin_constructed; } fwupd-2.0.10/plugins/intel-mchi/fu-intel-mchi-plugin.h000066400000000000000000000003671501337203100225400ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuIntelMchiPlugin, fu_intel_mchi_plugin, FU, INTEL_MCHI_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/intel-mchi/intel-mchi.quirk000066400000000000000000000000731501337203100215320ustar00rootroot00000000000000[dd17041c-09ea-4b17-a271-5b989867ec65] Plugin = intel_mchi fwupd-2.0.10/plugins/intel-mchi/meson.build000066400000000000000000000010431501337203100205640ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginIntelMchi"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('intel-mchi.quirk') plugin_builtins += static_library('fu_plugin_intel_mchi', sources: [ 'fu-intel-mchi-plugin.c', 'fu-intel-mchi-device.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) enumeration_data += files('tests/intel-mchi-setup.json') device_tests += files('tests/intel-mchi.json') endif fwupd-2.0.10/plugins/intel-mchi/tests/000077500000000000000000000000001501337203100175665ustar00rootroot00000000000000fwupd-2.0.10/plugins/intel-mchi/tests/intel-mchi-setup.json000066400000000000000000000330541501337203100236550ustar00rootroot00000000000000{ "FwupdVersion": "2.0.9", "UsbDevices": [ { "Created": "2025-04-28T12:18:53.041779Z", "GType": "FuUdevDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:16.0/mei/mei0", "DeviceFile": "/dev/mei0", "Subsystem": "mei", "Vendor": 32902, "Model": 41184, "Events": [ { "Id": "GetSymlinkTarget:Attr=subsystem", "Data": "mei" }, { "Id": "GetSymlinkTarget:Attr=driver" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "MAJOR=235\nMINOR=0\nDEVNAME=mei0" }, { "Id": "ReadProp:Key=DEVNAME", "Data": "mei0" }, { "Id": "ReadAttr:Attr=vendor" }, { "Id": "ReadAttr:Attr=device" }, { "Id": "ReadAttr:Attr=class" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "GetBackendParent:Subsystem=pci", "GType": "FuPciDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:16.0", "PhysicalId": "PCI_SLOT_NAME=0000:00:16.0" }, { "Id": "GetSymlinkTarget:Attr=subsystem", "Data": "pci" }, { "Id": "GetSymlinkTarget:Attr=driver", "Data": "mei_me" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "DRIVER=mei_me\nPCI_CLASS=78000\nPCI_ID=8086:A0E0\nPCI_SUBSYS_ID=17AA:22C7\nPCI_SLOT_NAME=0000:00:16.0\nMODALIAS=pci:v00008086d0000A0E0sv000017AAsd000022C7bc07sc80i00" }, { "Id": "ReadProp:Key=DEVNAME" }, { "Id": "ReadAttr:Attr=vendor", "Data": "0x8086" }, { "Id": "ReadAttr:Attr=device", "Data": "0xa0e0" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "DRIVER=mei_me\nPCI_CLASS=78000\nPCI_ID=8086:A0E0\nPCI_SUBSYS_ID=17AA:22C7\nPCI_SLOT_NAME=0000:00:16.0\nMODALIAS=pci:v00008086d0000A0E0sv000017AAsd000022C7bc07sc80i00" }, { "Id": "ReadProp:Key=DEVNAME" }, { "Id": "ReadAttr:Attr=vendor", "Data": "0x8086" }, { "Id": "ReadAttr:Attr=device", "Data": "0xa0e0" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "GetBackendParent:Subsystem=i2c", "Error": 8, "ErrorMsg": "no parent with subsystem i2c" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadAttr:Attr=revision", "Data": "0x10" }, { "Id": "ReadAttr:Attr=subsystem_vendor", "Data": "0x17aa" }, { "Id": "ReadAttr:Attr=subsystem_device", "Data": "0x22c7" }, { "Id": "ReadProp:Key=PCI_SLOT_NAME", "Data": "0000:00:16.0" }, { "Id": "GetBackendParent:Subsystem=(null)", "GType": "FuPciDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:16.0", "PhysicalId": "PCI_SLOT_NAME=0000:00:16.0" }, { "Id": "GetSymlinkTarget:Attr=subsystem", "Data": "pci" }, { "Id": "GetSymlinkTarget:Attr=driver", "Data": "mei_me" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "DRIVER=mei_me\nPCI_CLASS=78000\nPCI_ID=8086:A0E0\nPCI_SUBSYS_ID=17AA:22C7\nPCI_SLOT_NAME=0000:00:16.0\nMODALIAS=pci:v00008086d0000A0E0sv000017AAsd000022C7bc07sc80i00" }, { "Id": "ReadProp:Key=DEVNAME" }, { "Id": "ReadAttr:Attr=vendor", "Data": "0x8086" }, { "Id": "ReadAttr:Attr=device", "Data": "0xa0e0" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "DRIVER=mei_me\nPCI_CLASS=78000\nPCI_ID=8086:A0E0\nPCI_SUBSYS_ID=17AA:22C7\nPCI_SLOT_NAME=0000:00:16.0\nMODALIAS=pci:v00008086d0000A0E0sv000017AAsd000022C7bc07sc80i00" }, { "Id": "ReadProp:Key=DEVNAME" }, { "Id": "ReadAttr:Attr=vendor", "Data": "0x8086" }, { "Id": "ReadAttr:Attr=device", "Data": "0xa0e0" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "GetBackendParent:Subsystem=i2c", "Error": 8, "ErrorMsg": "no parent with subsystem i2c" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadAttr:Attr=revision", "Data": "0x10" }, { "Id": "ReadAttr:Attr=subsystem_vendor", "Data": "0x17aa" }, { "Id": "ReadAttr:Attr=subsystem_device", "Data": "0x22c7" }, { "Id": "ReadProp:Key=PCI_SLOT_NAME", "Data": "0000:00:16.0" }, { "Id": "ListAttr", "Data": "uevent\npower_state\nbroken_parity_status\nsubsystem_device\n0000:00:16.0-6861ec7b-d07a-4673-856c-7f22b4d55769\ndma_mask_bits\n0000:00:16.0-3c4852d6-d47b-4f46-b05e-b5edc1aa440e\nvendor\nlocal_cpus\nfirmware_node\npower\n0000:00:16.0-dba4d603-d7ed-4931-8823-17ad585705d5\nclass\n0000:00:16.0-dd17041c-09ea-4b17-a271-5b989867ec65\nnuma_node\nresource\nrescan\nmei\nmsi_bus\ndevice\n0000:00:16.0-082ee5a7-7c25-470a-9643-0c06f0466ea1\n0000:00:16.0-42b3ce2f-bd9f-485a-96ae-26406230b1ff\n0000:00:16.0-309dcde8-ccb1-4062-8f78-600115a34327\n0000:00:16.0-55213584-9a29-4916-badf-0fb7ed682aeb\ndriver\n0000:00:16.0-8e6a6715-9abc-4043-88ef-9e39c6f63e0f\nlocal_cpulist\n0000:00:16.0-8c2f4425-77d6-4755-aca3-891fdbc66a58\ndriver_override\nsubsystem\nd3cold_allowed\nirq\nrevision\n0000:00:16.0-fbf6fcf1-96cf-4e2e-a6a6-1bab8cbe36b1\nconsistent_dma_mask_bits\nresource0\nconfig\nari_enabled\nmsi_irqs\nremove\n0000:00:16.0-b638ab7e-94e2-4ea2-a552-d1c54b627f04\nenable\nlink\n0000:00:16.0-f908627d-13bf-4a04-b91f-a64e9245323d\nmodalias\nsubsystem_vendor\n0000:00:16.0-5565a099-7fe2-45c1-a22b-d7e9dfea9a2e" }, { "Id": "GetBackendParent:Subsystem=pci", "GType": "FuPciDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:16.0", "PhysicalId": "PCI_SLOT_NAME=0000:00:16.0" }, { "Id": "GetSymlinkTarget:Attr=subsystem", "Data": "pci" }, { "Id": "GetSymlinkTarget:Attr=driver", "Data": "mei_me" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "DRIVER=mei_me\nPCI_CLASS=78000\nPCI_ID=8086:A0E0\nPCI_SUBSYS_ID=17AA:22C7\nPCI_SLOT_NAME=0000:00:16.0\nMODALIAS=pci:v00008086d0000A0E0sv000017AAsd000022C7bc07sc80i00" }, { "Id": "ReadProp:Key=DEVNAME" }, { "Id": "ReadAttr:Attr=vendor", "Data": "0x8086" }, { "Id": "ReadAttr:Attr=device", "Data": "0xa0e0" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "DRIVER=mei_me\nPCI_CLASS=78000\nPCI_ID=8086:A0E0\nPCI_SUBSYS_ID=17AA:22C7\nPCI_SLOT_NAME=0000:00:16.0\nMODALIAS=pci:v00008086d0000A0E0sv000017AAsd000022C7bc07sc80i00" }, { "Id": "ReadProp:Key=DEVNAME" }, { "Id": "ReadAttr:Attr=vendor", "Data": "0x8086" }, { "Id": "ReadAttr:Attr=device", "Data": "0xa0e0" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "GetBackendParent:Subsystem=i2c", "Error": 8, "ErrorMsg": "no parent with subsystem i2c" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadAttr:Attr=revision", "Data": "0x10" }, { "Id": "ReadAttr:Attr=subsystem_vendor", "Data": "0x17aa" }, { "Id": "ReadAttr:Attr=subsystem_device", "Data": "0x22c7" }, { "Id": "ReadProp:Key=PCI_SLOT_NAME", "Data": "0000:00:16.0" }, { "Id": "GetBackendParent:Subsystem=(null)", "GType": "FuPciDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:16.0", "PhysicalId": "PCI_SLOT_NAME=0000:00:16.0" }, { "Id": "GetSymlinkTarget:Attr=subsystem", "Data": "pci" }, { "Id": "GetSymlinkTarget:Attr=driver", "Data": "mei_me" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "DRIVER=mei_me\nPCI_CLASS=78000\nPCI_ID=8086:A0E0\nPCI_SUBSYS_ID=17AA:22C7\nPCI_SLOT_NAME=0000:00:16.0\nMODALIAS=pci:v00008086d0000A0E0sv000017AAsd000022C7bc07sc80i00" }, { "Id": "ReadProp:Key=DEVNAME" }, { "Id": "ReadAttr:Attr=vendor", "Data": "0x8086" }, { "Id": "ReadAttr:Attr=device", "Data": "0xa0e0" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "DRIVER=mei_me\nPCI_CLASS=78000\nPCI_ID=8086:A0E0\nPCI_SUBSYS_ID=17AA:22C7\nPCI_SLOT_NAME=0000:00:16.0\nMODALIAS=pci:v00008086d0000A0E0sv000017AAsd000022C7bc07sc80i00" }, { "Id": "ReadProp:Key=DEVNAME" }, { "Id": "ReadAttr:Attr=vendor", "Data": "0x8086" }, { "Id": "ReadAttr:Attr=device", "Data": "0xa0e0" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "GetBackendParent:Subsystem=i2c", "Error": 8, "ErrorMsg": "no parent with subsystem i2c" }, { "Id": "ReadAttr:Attr=class", "Data": "0x078000" }, { "Id": "ReadAttr:Attr=revision", "Data": "0x10" }, { "Id": "ReadAttr:Attr=subsystem_vendor", "Data": "0x17aa" }, { "Id": "ReadAttr:Attr=subsystem_device", "Data": "0x22c7" }, { "Id": "ReadProp:Key=PCI_SLOT_NAME", "Data": "0000:00:16.0" }, { "Id": "ListAttr", "Data": "uevent\npower_state\nbroken_parity_status\nsubsystem_device\n0000:00:16.0-6861ec7b-d07a-4673-856c-7f22b4d55769\ndma_mask_bits\n0000:00:16.0-3c4852d6-d47b-4f46-b05e-b5edc1aa440e\nvendor\nlocal_cpus\nfirmware_node\npower\n0000:00:16.0-dba4d603-d7ed-4931-8823-17ad585705d5\nclass\n0000:00:16.0-dd17041c-09ea-4b17-a271-5b989867ec65\nnuma_node\nresource\nrescan\nmei\nmsi_bus\ndevice\n0000:00:16.0-082ee5a7-7c25-470a-9643-0c06f0466ea1\n0000:00:16.0-42b3ce2f-bd9f-485a-96ae-26406230b1ff\n0000:00:16.0-309dcde8-ccb1-4062-8f78-600115a34327\n0000:00:16.0-55213584-9a29-4916-badf-0fb7ed682aeb\ndriver\n0000:00:16.0-8e6a6715-9abc-4043-88ef-9e39c6f63e0f\nlocal_cpulist\n0000:00:16.0-8c2f4425-77d6-4755-aca3-891fdbc66a58\ndriver_override\nsubsystem\nd3cold_allowed\nirq\nrevision\n0000:00:16.0-fbf6fcf1-96cf-4e2e-a6a6-1bab8cbe36b1\nconsistent_dma_mask_bits\nresource0\nconfig\nari_enabled\nmsi_irqs\nremove\n0000:00:16.0-b638ab7e-94e2-4ea2-a552-d1c54b627f04\nenable\nlink\n0000:00:16.0-f908627d-13bf-4a04-b91f-a64e9245323d\nmodalias\nsubsystem_vendor\n0000:00:16.0-5565a099-7fe2-45c1-a22b-d7e9dfea9a2e" }, { "Id": "Ioctl:Request=0xc0104801,Data=HAQX3eoJF0uicVuYmGfsZQ==,Length=0x10", "DataOut": "ABAAAAEJF0uicVuYmGfsZQ==" }, { "Id": "Write:Data=CgoAAAAjAEAAAAAAQAAAAAA=,Length=0x11" }, { "Id": "Read:Length=0x51", "Data": "CooAADAAAADw0VJjs1SwGy8y2VxVcus99J9RbDQ7iQ/hkplkmVpVSxLH8mmfJajLZthjPDVC8iY=" }, { "Id": "Write:Data=CgoAAABbAEAAAAAAQAAAAAA=,Length=0x11" }, { "Id": "Read:Length=0x51", "Data": "CooAADAAAADw0VJjs1SwGy8y2VxVcus99J9RbDQ7iQ/hkplkmVpVSxLH8mmfJajLZthjPDVC8iY=" }, { "Id": "Write:Data=CgoAAABcAEAAAAAAQAAAAAA=,Length=0x11" }, { "Id": "Read:Length=0x51", "Data": "CooAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" } ] } ] } fwupd-2.0.10/plugins/intel-mchi/tests/intel-mchi.json000066400000000000000000000004361501337203100225150ustar00rootroot00000000000000{ "name": "Intel MCHI", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/intel-mchi-setup.json", "components": [ { "guids": [ "f18c7464-aa01-5b3d-bd4f-6623597d0f70" ] } ] } ] } fwupd-2.0.10/plugins/intel-mkhi/000077500000000000000000000000001501337203100164345ustar00rootroot00000000000000fwupd-2.0.10/plugins/intel-mkhi/README.md000066400000000000000000000031001501337203100177050ustar00rootroot00000000000000--- title: Plugin: Intel MKHI --- ## Introduction This plugin is used to talk to the Intel ME device using the legacy MKHI interface. It allows us to get the Platform Key as used for BootGuard. ## GUID Generation These devices use the existing GUIDs provided by the ME host interfaces. ## Metadata There have been several BootGuard key leaks that can be detected using the ME device. The metadata needed to match the KM checksum is found in the metadata, typically obtained from the LVFS project. This sets the `leaked-km` private flag which then causes the HSI `org.fwupd.hsi.Mei.KeyManifest` attribute to fail, and also adds a device inhibit which shows in the command line and GUI tools. The `org.linuxfoundation.bootguard.config` component is currently used to match against both the MCA and MKHI ME devices. The latest cabinet archive can also be installed into the `vendor-firmware` remote found in `/usr/share/fwupd/remotes.d/vendor/firmware/` which allows the detection to work even when offline -- although using the LVFS source is recommended for most users. New *OEM Public Key Hash* values found from `MEInfo` or calculated manually should be added to the checksums page on the LVFS. ## Vendor ID Security The devices are not upgradable and thus require no vendor ID set. ## External Interface Access This plugin requires `ioctl(IOCTL_MEI_CONNECT_CLIENT)` to `/dev/mei0`. ## Version Considerations This plugin has been available since fwupd version `2.0.9`, although the functionality was previously introduced in the `intel-me` plugin released with fwupd version `1.8.7`. fwupd-2.0.10/plugins/intel-mkhi/fu-intel-mkhi-device.c000066400000000000000000000062101501337203100225050ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-intel-mkhi-device.h" struct _FuIntelMkhiDevice { FuHeciDevice parent_instance; }; G_DEFINE_TYPE(FuIntelMkhiDevice, fu_intel_mkhi_device, FU_TYPE_HECI_DEVICE) #define FU_INTEL_MKHI_DEVICE_FLAG_LEAKED_KM "leaked-km" static gboolean fu_intel_mkhi_device_add_checksum_for_filename(FuIntelMkhiDevice *self, const gchar *filename, GError **error) { g_autofree gchar *checksum = NULL; g_autoptr(GByteArray) buf = NULL; /* read from the MFS */ buf = fu_heci_device_read_file(FU_HECI_DEVICE(self), filename, error); if (buf == NULL) return FALSE; /* convert into checksum, but only if non-zero and set */ checksum = fu_byte_array_to_string(buf); if (g_str_has_prefix(checksum, "0000000000000000") || g_str_has_prefix(checksum, "ffffffffffffffff")) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "checksum %s was invalid", checksum); return FALSE; } fu_device_add_checksum(FU_DEVICE(self), checksum); /* success */ return TRUE; } static gboolean fu_intel_mkhi_device_setup(FuDevice *device, GError **error) { FuIntelMkhiDevice *self = FU_INTEL_MKHI_DEVICE(device); /* connect */ if (!fu_mei_device_connect(FU_MEI_DEVICE(device), FU_HECI_DEVICE_UUID_MKHI, 0, error)) { g_prefix_error(error, "failed to connect: "); return FALSE; } /* this is the legacy way to get the hash, which is removed in newer ME versions due to * possible path traversal attacks */ if (!fu_intel_mkhi_device_add_checksum_for_filename(self, "/fpf/OemCred", error)) return FALSE; /* no point even adding */ if (fu_device_get_checksums(device)->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no OEM public keys found"); return FALSE; } /* success */ return TRUE; } static void fu_intel_mkhi_device_version_notify_cb(FuDevice *device, GParamSpec *pspec, gpointer user_data) { if (fu_device_has_private_flag(device, FU_INTEL_MKHI_DEVICE_FLAG_LEAKED_KM)) fu_device_inhibit(device, "leaked-km", "Provisioned with a leaked private key"); } static void fu_intel_mkhi_device_init(FuIntelMkhiDevice *self) { fu_device_set_logical_id(FU_DEVICE(self), "MKHI"); fu_device_set_name(FU_DEVICE(self), "BootGuard Configuration"); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE_CHILD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_ONLY_CHECKSUM); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_FLAGS); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_COMPUTER); fu_device_register_private_flag(FU_DEVICE(self), FU_INTEL_MKHI_DEVICE_FLAG_LEAKED_KM); g_signal_connect(FWUPD_DEVICE(self), "notify::private-flags", G_CALLBACK(fu_intel_mkhi_device_version_notify_cb), NULL); } static void fu_intel_mkhi_device_class_init(FuIntelMkhiDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_intel_mkhi_device_setup; } fwupd-2.0.10/plugins/intel-mkhi/fu-intel-mkhi-device.h000066400000000000000000000004771501337203100225230ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_INTEL_MKHI_DEVICE (fu_intel_mkhi_device_get_type()) G_DECLARE_FINAL_TYPE(FuIntelMkhiDevice, fu_intel_mkhi_device, FU, INTEL_MKHI_DEVICE, FuHeciDevice) fwupd-2.0.10/plugins/intel-mkhi/fu-intel-mkhi-plugin.c000066400000000000000000000014501501337203100225450ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-intel-mkhi-device.h" #include "fu-intel-mkhi-plugin.h" struct _FuIntelMkhiPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuIntelMkhiPlugin, fu_intel_mkhi_plugin, FU_TYPE_PLUGIN) static void fu_intel_mkhi_plugin_init(FuIntelMkhiPlugin *self) { } static void fu_intel_mkhi_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_udev_subsystem(plugin, "mei"); fu_plugin_add_device_gtype(plugin, FU_TYPE_INTEL_MKHI_DEVICE); } static void fu_intel_mkhi_plugin_class_init(FuIntelMkhiPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_intel_mkhi_plugin_constructed; } fwupd-2.0.10/plugins/intel-mkhi/fu-intel-mkhi-plugin.h000066400000000000000000000003671501337203100225600ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuIntelMkhiPlugin, fu_intel_mkhi_plugin, FU, INTEL_MKHI_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/intel-mkhi/intel-mkhi.quirk000066400000000000000000000000731501337203100215520ustar00rootroot00000000000000[8e6a6715-9abc-4043-88ef-9e39c6f63e0f] Plugin = intel_mkhi fwupd-2.0.10/plugins/intel-mkhi/meson.build000066400000000000000000000006731501337203100206040ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginIntelMkhi"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('intel-mkhi.quirk') plugin_builtins += static_library('fu_plugin_intel_mkhi', sources: [ 'fu-intel-mkhi-plugin.c', 'fu-intel-mkhi-device.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/intel-usb4/000077500000000000000000000000001501337203100163615ustar00rootroot00000000000000fwupd-2.0.10/plugins/intel-usb4/README.md000066400000000000000000000024301501337203100176370ustar00rootroot00000000000000--- title: Plugin: Intel USB4 --- ## Introduction This plugin supports the Goshen Ridge hardware which is a USB-4 controller from Intel. These devices can updated using multiple interfaces, but this plugin only uses the XHCI interface. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in an unspecified binary file format, with vendor specific header. This plugin supports the following protocol ID: * `com.intel.thunderbolt` ## GUID Generation These devices use the standard USB DeviceInstanceId values for the USB Hub, e.g. * `USB\VID_8087&PID_0B40` (quirk-only) These devices also use a custom InstanceId, which is quite intentionally identical to thunderbolt plugin: * `TBT-{nvm_vendor_id}{nvm_product_id}` ## Update Behavior By default the USB4 controller will reboot at the end of the update. Some devices (e.g. inside some Dell docks) will instead be updated the next time the USB-C plug from the dock is unplugged from the host, or when activated manually. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x8087` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.8.5`. fwupd-2.0.10/plugins/intel-usb4/fu-intel-usb4-device.c000066400000000000000000000437561501337203100223770ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * Copyright 2021 Intel Corporation. * Copyright 2021 Dell Inc. * All rights reserved. * * This software and associated documentation (if any) is furnished * under a license and may only be used or copied in accordance * with the terms of the license. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * Dell Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-intel-usb4-device.h" #include "fu-intel-usb4-struct.h" #define GR_USB_INTERFACE_NUMBER 0x0 #define GR_USB_BLOCK_SIZE 64 /* bmRequest type */ #define USB_REQ_TYPE_GET_MMIO 0xc0 /* bm Request type */ #define USB_REQ_TYPE_SET_MMIO 0x40 /* bm Request type */ /* bRequest */ #define REQ_HUB_GET_MMIO 64 #define REQ_HUB_SET_MMIO 65 /* wValue*/ #define MBOX_ACCESS (1 << 10) /* wIndex, mailbox register offset */ /* First 16 registers are Data[0]-Data[15] registers */ #define MBOX_REG_METADATA 16 #define MBOX_REG 17 /* no name? */ /* mask for the MBOX_REG register that has no name */ #define MBOX_ERROR (1 << 6) /* of the u8 status field */ #define MBOX_OPVALID (1 << 7) /* of the u8 status field */ #define MBOX_TIMEOUT 3000 /* NVM metadata offset and length fields are in dword units */ /* note that these won't work for DROM read */ #define NVM_OFFSET_TO_METADATA(p) ((((p) / 4) & 0x3fffff) << 2) /* bits 23:2 */ #define NVM_LENGTH_TO_METADATA(p) ((((p) / 4) & 0xf) << 24) /* bits 27:24 */ /* Default length for NVM READ */ #define NVM_READ_LENGTH 0x224 #define FU_INTEL_USB4_DEVICE_REMOVE_DELAY 60000 /* ms */ struct _FuIntelUsb4Device { FuUsbDevice parent_instance; guint blocksz; guint8 intf_nr; /* from DROM */ guint16 nvm_vendor_id; guint16 nvm_model_id; /* from DIGITAL */ guint16 nvm_device_id; }; G_DEFINE_TYPE(FuIntelUsb4Device, fu_intel_usb4_device, FU_TYPE_USB_DEVICE) /* wIndex contains the hub register offset, value BIT[10] is "access to * mailbox", rest of values are vendor specific or rsvd */ static gboolean fu_intel_usb4_device_get_mmio(FuIntelUsb4Device *self, guint16 mbox_reg, guint8 *buf, gsize bufsz, GError **error) { if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, REQ_HUB_GET_MMIO, /* request */ MBOX_ACCESS, /* value */ mbox_reg, /* index */ buf, bufsz, NULL, /* actual length */ MBOX_TIMEOUT, NULL, error)) { g_prefix_error(error, "GET_MMIO failed to set control on mbox register index [0x%x]: ", mbox_reg); return FALSE; } /* verify status for specific hub mailbox register */ if (mbox_reg == MBOX_REG) { g_autoptr(GByteArray) st_regex = NULL; st_regex = fu_struct_intel_usb4_mbox_parse(buf, bufsz, 0x0, error); if (st_regex == NULL) return FALSE; /* error status bit */ if (fu_struct_intel_usb4_mbox_get_status(st_regex) & MBOX_ERROR) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "GET_MMIO opcode [0x%x] nonzero error bit in status [0x%x]", fu_struct_intel_usb4_mbox_get_opcode(st_regex), fu_struct_intel_usb4_mbox_get_status(st_regex)); return FALSE; } /* operation valid (OV) bit should be 0'b */ if (fu_struct_intel_usb4_mbox_get_status(st_regex) & MBOX_OPVALID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "GET_MMIO opcode [0x%x] nonzero OV bit in status [0x%x]", fu_struct_intel_usb4_mbox_get_opcode(st_regex), fu_struct_intel_usb4_mbox_get_status(st_regex)); return FALSE; } } return TRUE; } static gboolean fu_intel_usb4_device_set_mmio(FuIntelUsb4Device *self, guint16 mbox_reg, guint8 *buf, gsize bufsz, GError **error) { if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, REQ_HUB_SET_MMIO, /* request */ MBOX_ACCESS, /* value */ mbox_reg, /* index */ buf, bufsz, NULL, /* actual length */ MBOX_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to set mmio 0x%x: ", mbox_reg); return FALSE; } return TRUE; } /* * Read up to 64 bytes of data from the mbox data registers to a buffer. * The mailbox can hold 64 bytes of data in 16 doubleword data registers. * To get data from NVM or DROM to mbox registers issue a NVM Read or DROM * read operation before reading the mbox data registers. */ static gboolean fu_intel_usb4_device_mbox_data_read(FuIntelUsb4Device *self, guint8 *buf, guint8 bufsz, GError **error) { guint8 *ptr = buf; if (bufsz > 64 || bufsz % 4) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid firmware data read length %u", bufsz); return FALSE; } /* read 4 bytes per iteration */ for (gint i = 0; i < bufsz / 4; i++) { if (!fu_intel_usb4_device_get_mmio(self, i, ptr, 0x4, error)) { g_prefix_error(error, "failed to read mbox data registers: "); return FALSE; } ptr += 4; } return TRUE; } /* * The mailbox can hold 64 bytes in 16 doubleword data registers. * A NVM write operation writes data from these registers to NVM * at the set offset */ static gboolean fu_intel_usb4_device_mbox_data_write(FuIntelUsb4Device *self, const guint8 *buf, guint8 bufsz, GError **error) { guint8 *ptr = (guint8 *)buf; if (bufsz > 64 || bufsz % 4) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "invalid firmware data write length %u", bufsz); return FALSE; } /* writes 4 bytes per iteration */ for (gint i = 0; i < bufsz / 4; i++) { if (!fu_intel_usb4_device_set_mmio(self, i, ptr, 0x4, error)) return FALSE; ptr += 4; } return TRUE; } static gboolean fu_intel_usb4_device_operation(FuIntelUsb4Device *self, FuIntelUsb4Opcode opcode, guint8 *buf, gsize bufsz, GError **error) { gint max_tries = 100; g_autoptr(GByteArray) st_regex = fu_struct_intel_usb4_mbox_new(); /* Write metadata register for operations that use it */ switch (opcode) { case FU_INTEL_USB4_OPCODE_NVM_WRITE: case FU_INTEL_USB4_OPCODE_NVM_AUTH_WRITE: break; case FU_INTEL_USB4_OPCODE_NVM_READ: case FU_INTEL_USB4_OPCODE_NVM_SET_OFFSET: case FU_INTEL_USB4_OPCODE_DROM_READ: if (buf == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "hub opcode 0x%x requires metadata", opcode); return FALSE; } if (!fu_intel_usb4_device_set_mmio(self, MBOX_REG_METADATA, buf, bufsz, error)) { g_autofree gchar *bufstr = fu_strsafe((const gchar *)buf, bufsz); g_prefix_error(error, "failed to write metadata %s: ", bufstr); return FALSE; } break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid hub opcode: 0x%x", opcode); return FALSE; } /* write the operation and poll completion or error */ fu_struct_intel_usb4_mbox_set_opcode(st_regex, opcode); fu_struct_intel_usb4_mbox_set_status(st_regex, MBOX_OPVALID); if (!fu_intel_usb4_device_set_mmio(self, MBOX_REG, st_regex->data, st_regex->len, error)) return FALSE; /* leave early as successful USB4 AUTH resets the device immediately */ if (opcode == FU_INTEL_USB4_OPCODE_NVM_AUTH_WRITE) return TRUE; for (gint i = 0; i <= max_tries; i++) { g_autoptr(GError) error_local = NULL; if (fu_intel_usb4_device_get_mmio(self, MBOX_REG, st_regex->data, st_regex->len, &error_local)) return TRUE; if (i == max_tries) { g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "maximum tries exceeded: "); } fu_device_sleep(FU_DEVICE(self), 10); /* ms */ } return FALSE; } static gboolean fu_intel_usb4_device_nvm_read(FuIntelUsb4Device *self, guint8 *buf, gsize bufsz, gsize nvm_addr, GError **error) { g_autoptr(GPtrArray) chunks = NULL; chunks = fu_chunk_array_mutable_new(buf, bufsz, nvm_addr, 0x0, 64); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); g_autoptr(FuStructIntelUsb4MetadataNvmRead) st = fu_struct_intel_usb4_metadata_nvm_read_new(); /* ask hub to read up to 64 bytes from NVM to mbox data regs */ fu_struct_intel_usb4_metadata_nvm_read_set_address( st, NVM_OFFSET_TO_METADATA(fu_chunk_get_address(chk))); fu_struct_intel_usb4_metadata_nvm_read_set_length(st, fu_chunk_get_data_sz(chk) / 4); if (!fu_intel_usb4_device_operation(self, FU_INTEL_USB4_OPCODE_NVM_READ, st->data, st->len, error)) { g_prefix_error(error, "hub NVM read error: "); return FALSE; } /* read the data from mbox data regs */ if (!fu_intel_usb4_device_mbox_data_read(self, fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "hub firmware mbox data read error: "); return FALSE; } } /* success */ return TRUE; } static gboolean fu_intel_usb4_device_nvm_write(FuIntelUsb4Device *self, GBytes *blob, guint32 nvm_addr, FuProgress *progress, GError **error) { guint8 metadata[4]; g_autoptr(FuChunkArray) chunks = NULL; if (nvm_addr % 4 != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "Invalid NVM write offset 0x%x, must be DW aligned: ", nvm_addr); return FALSE; } if (g_bytes_get_size(blob) < 64 || g_bytes_get_size(blob) % 64) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "Invalid NVM length 0x%x, must be 64 byte aligned: ", (guint)g_bytes_get_size(blob)); return FALSE; } /* set initial offset, must be DW aligned */ fu_memwrite_uint32(metadata, NVM_OFFSET_TO_METADATA(nvm_addr), G_LITTLE_ENDIAN); if (!fu_intel_usb4_device_operation(self, FU_INTEL_USB4_OPCODE_NVM_SET_OFFSET, metadata, sizeof(metadata), error)) { g_prefix_error(error, "hub NVM set offset error: "); return FALSE; } /* write data in 64 byte blocks */ chunks = fu_chunk_array_new_from_bytes(blob, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 64); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; /* write data to mbox data regs */ if (!fu_intel_usb4_device_mbox_data_write(self, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "hub mbox data write error: "); return FALSE; } /* ask hub to write 64 bytes from data regs to NVM */ if (!fu_intel_usb4_device_operation(self, FU_INTEL_USB4_OPCODE_NVM_WRITE, NULL, 0, error)) { g_prefix_error(error, "hub NVM write operation error: "); return FALSE; } /* done */ fu_progress_step_done(progress); } /* success */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_BUSY); return TRUE; } static gboolean fu_intel_usb4_device_activate(FuDevice *device, FuProgress *progress, GError **error) { FuIntelUsb4Device *self = FU_INTEL_USB4_DEVICE(device); g_autoptr(FuDeviceLocker) locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; if (!fu_intel_usb4_device_operation(self, FU_INTEL_USB4_OPCODE_NVM_AUTH_WRITE, NULL, 0, error)) { g_prefix_error(error, "NVM authenticate failed: "); fu_device_set_update_state(device, FWUPD_UPDATE_STATE_FAILED); return FALSE; } fu_device_set_update_state(device, FWUPD_UPDATE_STATE_SUCCESS); return TRUE; } static FuFirmware * fu_intel_usb4_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuIntelUsb4Device *self = FU_INTEL_USB4_DEVICE(device); guint16 fw_vendor_id; guint16 fw_model_id; g_autoptr(FuFirmware) firmware = fu_intel_thunderbolt_firmware_new(); /* get vid:pid:rev */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; /* check is compatible */ fw_vendor_id = fu_intel_thunderbolt_nvm_get_vendor_id(FU_INTEL_THUNDERBOLT_NVM(firmware)); fw_model_id = fu_intel_thunderbolt_nvm_get_model_id(FU_INTEL_THUNDERBOLT_NVM(firmware)); if (self->nvm_vendor_id != fw_vendor_id || self->nvm_model_id != fw_model_id) { if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_VID_PID) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "firmware 0x%04x:0x%04x does not match device 0x%04x:0x%04x", fw_vendor_id, fw_model_id, self->nvm_vendor_id, self->nvm_model_id); return NULL; } g_warning("firmware 0x%04x:0x%04x does not match device 0x%04x:0x%04x", fw_vendor_id, fw_model_id, self->nvm_vendor_id, self->nvm_model_id); } /* success */ return g_steal_pointer(&firmware); } static gboolean fu_intel_usb4_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuIntelUsb4Device *self = FU_INTEL_USB4_DEVICE(device); g_autoptr(GBytes) fw_image = NULL; g_return_val_if_fail(device != NULL, FALSE); g_return_val_if_fail(FU_IS_FIRMWARE(firmware), FALSE); /* get payload */ fw_image = fu_firmware_get_image_by_id_bytes(firmware, FU_FIRMWARE_ID_PAYLOAD, error); if (fw_image == NULL) return FALSE; /* firmware install */ if (!fu_intel_usb4_device_nvm_write(self, fw_image, 0, progress, error)) return FALSE; /* success, but needs activation */ if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_SKIPS_RESTART)) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION); fu_device_set_version(device, fu_firmware_get_version(firmware)); return TRUE; } /* activate, wait for replug */ if (!fu_intel_usb4_device_operation(self, FU_INTEL_USB4_OPCODE_NVM_AUTH_WRITE, NULL, 0, error)) { g_prefix_error(error, "NVM authenticate failed: "); return FALSE; } fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* success */ return TRUE; } static gboolean fu_intel_usb4_device_setup(FuDevice *device, GError **error) { FuIntelUsb4Device *self = FU_INTEL_USB4_DEVICE(device); guint8 buf[NVM_READ_LENGTH] = {0x0}; g_autofree gchar *name = NULL; g_autoptr(FuFirmware) fw = fu_intel_thunderbolt_nvm_new(); g_autoptr(GBytes) blob = NULL; /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_intel_usb4_device_parent_class)->setup(device, error)) return FALSE; /* read from device and parse firmware */ if (!fu_intel_usb4_device_nvm_read(self, buf, sizeof(buf), 0, error)) { g_prefix_error(error, "NVM read error: "); return FALSE; } blob = g_bytes_new(buf, sizeof(buf)); if (!fu_firmware_parse_bytes(fw, blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, error)) { g_prefix_error(error, "NVM parse error: "); return FALSE; } self->nvm_vendor_id = fu_intel_thunderbolt_nvm_get_vendor_id(FU_INTEL_THUNDERBOLT_NVM(fw)); self->nvm_model_id = fu_intel_thunderbolt_nvm_get_model_id(FU_INTEL_THUNDERBOLT_NVM(fw)); self->nvm_device_id = fu_intel_thunderbolt_nvm_get_device_id(FU_INTEL_THUNDERBOLT_NVM(fw)); name = g_strdup_printf("TBT-%04x%04x", self->nvm_vendor_id, self->nvm_model_id); fu_device_add_instance_id(device, name); fu_device_set_version(device, fu_firmware_get_version(fw)); return TRUE; } static void fu_intel_usb4_device_to_string(FuDevice *device, guint idt, GString *str) { FuIntelUsb4Device *self = FU_INTEL_USB4_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "NvmVendorId", self->nvm_vendor_id); fwupd_codec_string_append_hex(str, idt, "NvmModelId", self->nvm_model_id); fwupd_codec_string_append_hex(str, idt, "NvmDeviceId", self->nvm_device_id); } static void fu_intel_usb4_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 78, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 22, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_intel_usb4_device_init(FuIntelUsb4Device *self) { self->intf_nr = GR_USB_INTERFACE_NUMBER; self->blocksz = GR_USB_BLOCK_SIZE; fu_device_add_protocol(FU_DEVICE(self), "com.intel.thunderbolt"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PAIR); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_INHERIT_ACTIVATION); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_NAME_CATEGORY); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_NO_GENERIC_GUIDS); fu_device_set_remove_delay(FU_DEVICE(self), FU_INTEL_USB4_DEVICE_REMOVE_DELAY); } static void fu_intel_usb4_device_class_init(FuIntelUsb4DeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_intel_usb4_device_to_string; device_class->setup = fu_intel_usb4_device_setup; device_class->prepare_firmware = fu_intel_usb4_device_prepare_firmware; device_class->write_firmware = fu_intel_usb4_device_write_firmware; device_class->activate = fu_intel_usb4_device_activate; device_class->set_progress = fu_intel_usb4_device_set_progress; } fwupd-2.0.10/plugins/intel-usb4/fu-intel-usb4-device.h000066400000000000000000000014371501337203100223720ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * Copyright 2021 Intel Corporation. * Copyright 2021 Dell Inc. * All rights reserved. * * This software and associated documentation (if any) is furnished * under a license and may only be used or copied in accordance * with the terms of the license. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * Dell Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_INTEL_USB4_DEVICE (fu_intel_usb4_device_get_type()) G_DECLARE_FINAL_TYPE(FuIntelUsb4Device, fu_intel_usb4_device, FU, INTEL_USB4_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/intel-usb4/fu-intel-usb4-plugin.c000066400000000000000000000040261501337203100224210ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-intel-usb4-device.h" #include "fu-intel-usb4-plugin.h" struct _FuIntelUsb4Plugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuIntelUsb4Plugin, fu_intel_usb4_plugin, FU_TYPE_PLUGIN) static void fu_intel_usb4_plugin_init(FuIntelUsb4Plugin *self) { fu_plugin_add_rule(FU_PLUGIN(self), FU_PLUGIN_RULE_RUN_BEFORE, "thunderbolt"); } static void fu_intel_usb4_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_INTEL_USB4_DEVICE); } static void fu_intel_usb4_plugin_device_registered(FuPlugin *plugin, FuDevice *device) { GPtrArray *devices = fu_plugin_get_devices(plugin); GPtrArray *instance_ids = fu_device_get_instance_ids(device); if (g_strcmp0(fu_device_get_plugin(device), "thunderbolt") != 0) return; /* prefer using this plugin over the thunderbolt one -- but the device ID is constructed * differently in each plugin as they're using very different update methods. * use the TBT-{nvm_vendor_id}{nvm_product_id} instance ID to match them up instead. */ for (guint i = 0; i < devices->len; i++) { FuDevice *device_tmp = g_ptr_array_index(devices, i); for (guint j = 0; j < instance_ids->len; j++) { const gchar *instance_id = g_ptr_array_index(instance_ids, j); if (g_str_has_prefix(instance_id, "TBT-") && fu_device_has_instance_id(device_tmp, instance_id, FU_DEVICE_INSTANCE_FLAG_VISIBLE)) { fu_device_remove_private_flag( device, FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_inhibit(device, "hidden", "updated by the intel-usb4 plugin instead"); return; } } } } static void fu_intel_usb4_plugin_class_init(FuIntelUsb4PluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_intel_usb4_plugin_constructed; plugin_class->device_registered = fu_intel_usb4_plugin_device_registered; } fwupd-2.0.10/plugins/intel-usb4/fu-intel-usb4-plugin.h000066400000000000000000000003671501337203100224320ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuIntelUsb4Plugin, fu_intel_usb4_plugin, FU, INTEL_USB4_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/intel-usb4/fu-intel-usb4.rs000066400000000000000000000010661501337203100213300ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later // hub operation #[repr(u16le)] enum FuIntelUsb4Opcode { NVM_WRITE = 0x20, NVM_AUTH_WRITE = 0x21, NVM_READ = 0x22, NVM_SET_OFFSET = 0x23, DROM_READ = 0x24, } #[derive(New, Parse)] #[repr(C, packed)] struct FuStructIntelUsb4Mbox { opcode: FuIntelUsb4Opcode, _rsvd: u8, status: u8, } #[derive(New)] #[repr(C, packed)] struct FuStructIntelUsb4MetadataNvmRead { address: u24le, length: u8, // in DWORDs } fwupd-2.0.10/plugins/intel-usb4/intel-usb4.quirk000066400000000000000000000001311501337203100214170ustar00rootroot00000000000000[USB\VID_8087&PID_0B40] Plugin = intel_usb4 [USB\VID_8087&PID_5787] Plugin = intel_usb4 fwupd-2.0.10/plugins/intel-usb4/meson.build000066400000000000000000000010451501337203100205230ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginIntelUsb4"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('intel-usb4.quirk') plugin_builtins += static_library('fu_plugin_intel_usb4', rustgen.process('fu-intel-usb4.rs'), sources: [ 'fu-intel-usb4-device.c', 'fu-intel-usb4-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) enumeration_data += files('tests/intel-gatkex-setup.json') device_tests += files('tests/intel-gatkex.json') fwupd-2.0.10/plugins/intel-usb4/tests/000077500000000000000000000000001501337203100175235ustar00rootroot00000000000000fwupd-2.0.10/plugins/intel-usb4/tests/intel-gatkex-setup.json000066400000000000000000000367771501337203100241740ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUsbDevice", "PlatformId": "8-1", "Created": "2024-11-12T15:49:43.933624Z", "IdVendor": 32903, "IdProduct": 2880, "Device": 4660, "USB": 800, "Manufacturer": 1, "DeviceClass": 9, "DeviceProtocol": 3, "Product": 2, "UsbBosDescriptors": [ { "DevCapabilityType": 70, "ExtraData": "AAQ=" }, { "DevCapabilityType": 2, "ExtraData": "BgAAAA==" }, { "DevCapabilityType": 3, "ExtraData": "AA4AAQVeAQ==" }, { "DevCapabilityType": 4, "ExtraData": "AAAAAAAAAAAAAAAAAAAAAAA=" }, { "DevCapabilityType": 10, "ExtraData": "ACMAAAAAEQAAMAAFALAABQAxQAoAsUAKAA==" } ], "UsbConfigDescriptors": [ { "ConfigurationValue": 1 } ], "UsbInterfaces": [ { "Length": 9, "DescriptorType": 4, "InterfaceClass": 9, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 129, "Interval": 8, "MaxPacketSize": 2 } ] } ], "UsbEvents": [ { "Id": "#4693935e", "Data": "008" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=897\nDEVNAME=bus/usb/008/002\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=8087/b40/1234\nTYPE=9/0/3\nBUSNUM=008\nDEVNUM=002" }, { "Id": "#1ab3ae0a", "Data": "002" }, { "Id": "#4693935e", "Data": "008" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=897\nDEVNAME=bus/usb/008/002\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=8087/b40/1234\nTYPE=9/0/3\nBUSNUM=008\nDEVNUM=002" }, { "Id": "#1ab3ae0a", "Data": "002" }, { "Id": "#1fcf122d", "Data": "VVNCMy4wIEh1YgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" }, { "Id": "#b3898fd1", "Data": "DCoECQCvAAQgAQAA" }, { "Id": "#98769e96", "Data": "AAAAEA==" }, { "Id": "#c6078ba2", "Data": "IgAAgA==" }, { "Id": "#48f37699", "Data": "IgAAAA==" }, { "Id": "#ddc999fa", "Data": "rwH3EA==" }, { "Id": "#db335c76", "Data": "KiYLoA==" }, { "Id": "#f2aa4ce8", "Data": "AAA4gA==" }, { "Id": "#37872e8b", "Data": "AAAAAA==" }, { "Id": "#6ba9f901", "Data": "DAA8BQ==" }, { "Id": "#a2a0bdcd", "Data": "NlDTyQ==" }, { "Id": "#cefb1000", "Data": "AQQtBA==" }, { "Id": "#db50a488", "Data": "gDagIw==" }, { "Id": "#a2c967b4", "Data": "JAAkAA==" }, { "Id": "#74e40049", "Data": "JAAkAA==" }, { "Id": "#dc4a8856", "Data": "LgAuAA==" }, { "Id": "#44304f43", "Data": "LsAu8A==" }, { "Id": "#cc958773", "Data": "AAAAAA==" }, { "Id": "#a763774f", "Data": "KiFh/w==" }, { "Id": "#29f84081", "Data": "7xUAAA==" }, { "Id": "#a6caa5cd", "Data": "gAAAAA==" }, { "Id": "#611ca26e", "Data": "QAAAEA==" }, { "Id": "#c6078ba2", "Data": "IgAAgA==" }, { "Id": "#48f37699", "Data": "IgAAgA==" }, { "Id": "#48f37699", "Data": "IgAAAA==" }, { "Id": "#ddc999fa", "Data": "BS0hog==" }, { "Id": "#db335c76", "Data": "GQvJNg==" }, { "Id": "#f2aa4ce8", "Data": "AKgoDA==" }, { "Id": "#37872e8b", "Data": "UAQBAA==" }, { "Id": "#6ba9f901", "Data": "A1BYIA==" }, { "Id": "#a2a0bdcd", "Data": "oDbGNg==" }, { "Id": "#cefb1000", "Data": "OTYbNg==" }, { "Id": "#db50a488", "Data": "5MmAAA==" }, { "Id": "#a2c967b4", "Data": "IIeAAA==" }, { "Id": "#74e40049", "Data": "JgsAAA==" }, { "Id": "#dc4a8856", "Data": "AQAAAA==" }, { "Id": "#44304f43", "Data": "QVBQIA==" }, { "Id": "#cc958773", "Data": "RU0gIA==" }, { "Id": "#a763774f", "Data": "xhAGAA==" }, { "Id": "#29f84081", "Data": "AL+/Dw==" }, { "Id": "#a6caa5cd", "Data": "mgAAAA==" }, { "Id": "#52e639c4", "Data": "gAAAEA==" }, { "Id": "#c6078ba2", "Data": "IgAAgA==" }, { "Id": "#48f37699", "Data": "IgAAgA==" }, { "Id": "#48f37699", "Data": "IgAAAA==" }, { "Id": "#ddc999fa", "Data": "AKBABg==" }, { "Id": "#db335c76", "Data": "AIDJNg==" }, { "Id": "#f2aa4ce8", "Data": "ADgAhA==" }, { "Id": "#37872e8b", "Data": "AADICQ==" }, { "Id": "#6ba9f901", "Data": "gmAQAA==" }, { "Id": "#a2a0bdcd", "Data": "AAB+mQ==" }, { "Id": "#cefb1000", "Data": "AHw5AA==" }, { "Id": "#db50a488", "Data": "AAAEgA==" }, { "Id": "#a2c967b4", "Data": "+gBVRA==" }, { "Id": "#74e40049", "Data": "MzPS/A==" }, { "Id": "#dc4a8856", "Data": "MwAAAA==" }, { "Id": "#44304f43", "Data": "yeTJAA==" }, { "Id": "#cc958773", "Data": "AAEPBQ==" }, { "Id": "#a763774f", "Data": "AAAaMQ==" }, { "Id": "#29f84081", "Data": "AxsAAA==" }, { "Id": "#a6caa5cd", "Data": "AP8DEA==" }, { "Id": "#cb724008", "Data": "wAAAEA==" }, { "Id": "#c6078ba2", "Data": "IgAAgA==" }, { "Id": "#48f37699", "Data": "IgAAgA==" }, { "Id": "#48f37699", "Data": "IgAAAA==" }, { "Id": "#ddc999fa", "Data": "CAB7EA==" }, { "Id": "#db335c76", "Data": "DABqAA==" }, { "Id": "#f2aa4ce8", "Data": "AAAAEA==" }, { "Id": "#37872e8b", "Data": "EAAiEA==" }, { "Id": "#6ba9f901", "Data": "EgBCAA==" }, { "Id": "#a2a0bdcd", "Data": "AAAAAA==" }, { "Id": "#cefb1000", "Data": "AAAAEA==" }, { "Id": "#db50a488", "Data": "FgBYAA==" }, { "Id": "#a2c967b4", "Data": "AAAAEA==" }, { "Id": "#74e40049", "Data": "GgBuAA==" }, { "Id": "#dc4a8856", "Data": "AAAAIA==" }, { "Id": "#44304f43", "Data": "CAECAA==" }, { "Id": "#cc958773", "Data": "AAAAAA==" }, { "Id": "#a763774f", "Data": "ABESHA==" }, { "Id": "#29f84081", "Data": "GwALDA==" }, { "Id": "#a6caa5cd", "Data": "CwwDBw==" }, { "Id": "#c568d6b9", "Data": "AAEAEA==" }, { "Id": "#c6078ba2", "Data": "IgAAgA==" }, { "Id": "#48f37699", "Data": "IgAAgA==" }, { "Id": "#48f37699", "Data": "IgAAAA==" }, { "Id": "#ddc999fa", "Data": "BiQAEQ==" }, { "Id": "#db335c76", "Data": "EBMSAA==" }, { "Id": "#f2aa4ce8", "Data": "AAASEg==" }, { "Id": "#37872e8b", "Data": "EhIQAg==" }, { "Id": "#6ba9f901", "Data": "AAAAAA==" }, { "Id": "#a2a0bdcd", "Data": "AAAAHQ==" }, { "Id": "#cefb1000", "Data": "HQAAAA==" }, { "Id": "#db50a488", "Data": "AA8AAA==" }, { "Id": "#a2c967b4", "Data": "AAAAAA==" }, { "Id": "#74e40049", "Data": "Ag8AAA==" }, { "Id": "#dc4a8856", "Data": "ABAAAA==" }, { "Id": "#44304f43", "Data": "AvAAIA==" }, { "Id": "#cc958773", "Data": "D/AAIA==" }, { "Id": "#a763774f", "Data": "D/AAAA==" }, { "Id": "#29f84081", "Data": "UAAAMw==" }, { "Id": "#a6caa5cd", "Data": "EwIFIw==" }, { "Id": "#df245998", "Data": "QAEAEA==" }, { "Id": "#c6078ba2", "Data": "IgAAgA==" }, { "Id": "#48f37699", "Data": "IgAAgA==" }, { "Id": "#48f37699", "Data": "IgAAAA==" }, { "Id": "#ddc999fa", "Data": "VVUAAA==" }, { "Id": "#db335c76", "Data": "BQAVAA==" }, { "Id": "#f2aa4ce8", "Data": "AAAQBA==" }, { "Id": "#37872e8b", "Data": "AAcAFA==" }, { "Id": "#6ba9f901", "Data": "BAIAAA==" }, { "Id": "#a2a0bdcd", "Data": "HB4gGw==" }, { "Id": "#cefb1000", "Data": "HR8TFA==" }, { "Id": "#db50a488", "Data": "FwAAAA==" }, { "Id": "#a2c967b4", "Data": "EB4AAg==" }, { "Id": "#74e40049", "Data": "ECAAAg==" }, { "Id": "#dc4a8856", "Data": "ECIAAg==" }, { "Id": "#44304f43", "Data": "ECQABQ==" }, { "Id": "#cc958773", "Data": "AAAAAA==" }, { "Id": "#a763774f", "Data": "ECYAAw==" }, { "Id": "#29f84081", "Data": "yclj/w==" }, { "Id": "#a6caa5cd", "Data": "/wAAAQ==" }, { "Id": "#5a76bd48", "Data": "gAEAEA==" }, { "Id": "#c6078ba2", "Data": "IgAAgA==" }, { "Id": "#48f37699", "Data": "IgAAgA==" }, { "Id": "#48f37699", "Data": "IgAAAA==" }, { "Id": "#ddc999fa", "Data": "AAABIQ==" }, { "Id": "#db335c76", "Data": "UAAAAA==" }, { "Id": "#f2aa4ce8", "Data": "FBOcYw==" }, { "Id": "#37872e8b", "Data": "NsmcYw==" }, { "Id": "#6ba9f901", "Data": "GhYZFQ==" }, { "Id": "#a2a0bdcd", "Data": "GBQXEw==" }, { "Id": "#cefb1000", "Data": "yTbJNg==" }, { "Id": "#db50a488", "Data": "yQEAAA==" }, { "Id": "#a2c967b4", "Data": "AAAhUA==" }, { "Id": "#74e40049", "Data": "AAAAAA==" }, { "Id": "#dc4a8856", "Data": "AAAAAA==" }, { "Id": "#44304f43", "Data": "AAAAAA==" }, { "Id": "#cc958773", "Data": "AAoAAA==" }, { "Id": "#a763774f", "Data": "AB4AKA==" }, { "Id": "#29f84081", "Data": "AAQHLg==" }, { "Id": "#a6caa5cd", "Data": "Ci2qLA==" }, { "Id": "#bdb7f1ab", "Data": "wAEAEA==" }, { "Id": "#c6078ba2", "Data": "IgAAgA==" }, { "Id": "#48f37699", "Data": "IgAAgA==" }, { "Id": "#48f37699", "Data": "IgAAAA==" }, { "Id": "#ddc999fa", "Data": "qgnQCg==" }, { "Id": "#db335c76", "Data": "wAs2/w==" }, { "Id": "#f2aa4ce8", "Data": "/////w==" }, { "Id": "#37872e8b", "Data": "/////w==" }, { "Id": "#6ba9f901", "Data": "/////w==" }, { "Id": "#a2a0bdcd", "Data": "/////w==" }, { "Id": "#cefb1000", "Data": "/////w==" }, { "Id": "#db50a488", "Data": "/////w==" }, { "Id": "#a2c967b4", "Data": "/////w==" }, { "Id": "#74e40049", "Data": "/////w==" }, { "Id": "#dc4a8856", "Data": "/////w==" }, { "Id": "#44304f43", "Data": "/////w==" }, { "Id": "#cc958773", "Data": "/////w==" }, { "Id": "#a763774f", "Data": "/////w==" }, { "Id": "#29f84081", "Data": "/////w==" }, { "Id": "#a6caa5cd", "Data": "/////w==" }, { "Id": "#628b70ed", "Data": "AAIACQ==" }, { "Id": "#c6078ba2", "Data": "IgAAgA==" }, { "Id": "#48f37699", "Data": "IgAAAA==" }, { "Id": "#ddc999fa", "Data": "RFJPTQ==" }, { "Id": "#db335c76", "Data": "ICAgIA==" }, { "Id": "#f2aa4ce8", "Data": "/////w==" }, { "Id": "#37872e8b", "Data": "/////w==" }, { "Id": "#6ba9f901", "Data": "kwC8/Q==" }, { "Id": "#a2a0bdcd", "Data": "RXYfhw==" }, { "Id": "#cefb1000", "Data": "gPxj1g==" }, { "Id": "#db50a488", "Data": "+gHaAA==" }, { "Id": "#a2c967b4", "Data": "h4A0Eg==" } ] } ] } fwupd-2.0.10/plugins/intel-usb4/tests/intel-gatkex.json000066400000000000000000000004771501337203100230220ustar00rootroot00000000000000{ "name": "Intel Gatkex", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/intel-gatkex-setup.json", "components": [ { "version": "38.0", "guids": [ "2590556b-dcca-595c-8c71-c9c2247556b3" ] } ] } ] } fwupd-2.0.10/plugins/iommu/000077500000000000000000000000001501337203100155215ustar00rootroot00000000000000fwupd-2.0.10/plugins/iommu/README.md000066400000000000000000000004071501337203100170010ustar00rootroot00000000000000--- title: Plugin: IOMMU --- ## Introduction This plugin checks if an IOMMU is available on the system. ## External Interface Access This plugin requires no extra access. ## Version Considerations This plugin has been available since fwupd version `1.5.0`. fwupd-2.0.10/plugins/iommu/fu-iommu-plugin.c000066400000000000000000000106051501337203100207210ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-iommu-plugin.h" struct _FuIommuPlugin { FuPlugin parent_instance; gboolean has_iommu; }; G_DEFINE_TYPE(FuIommuPlugin, fu_iommu_plugin, FU_TYPE_PLUGIN) static void fu_iommu_plugin_to_string(FuPlugin *plugin, guint idt, GString *str) { FuIommuPlugin *self = FU_IOMMU_PLUGIN(plugin); fwupd_codec_string_append_bool(str, idt, "HasIommu", self->has_iommu); } static gboolean fu_iommu_plugin_backend_device_added(FuPlugin *plugin, FuDevice *device, FuProgress *progress, GError **error) { FuIommuPlugin *self = FU_IOMMU_PLUGIN(plugin); /* interesting device? */ if (!FU_IS_UDEV_DEVICE(device)) return TRUE; if (g_strcmp0(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device)), "iommu") != 0) return TRUE; self->has_iommu = TRUE; return TRUE; } static void fu_iommu_plugin_add_security_attrs(FuPlugin *plugin, FuSecurityAttrs *attrs) { FuIommuPlugin *self = FU_IOMMU_PLUGIN(plugin); g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(GHashTable) cmdline = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_IOMMU); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_ENABLED); fu_security_attrs_append(attrs, attr); /* we might be able to fix this */ cmdline = fu_kernel_get_cmdline(&error_local); if (cmdline == NULL) { g_warning("failed to get kernel cmdline: %s", error_local->message); } else if (fu_kernel_check_cmdline_mutable(NULL)) { const gchar *value = g_hash_table_lookup(cmdline, "iommu"); fwupd_security_attr_set_kernel_current_value(attr, value); if (!g_hash_table_contains(cmdline, "iommu") && !g_hash_table_contains(cmdline, "intel_iommu") && !g_hash_table_contains(cmdline, "amd_iommu")) { fwupd_security_attr_set_kernel_target_value(attr, "iommu=force"); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_CAN_FIX); } if (g_strcmp0(value, "force") == 0) { fwupd_security_attr_set_kernel_target_value(attr, NULL); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_CAN_UNDO); } } fu_security_attr_add_bios_target_value(attr, "AmdVt", "enable"); fu_security_attr_add_bios_target_value(attr, "IOMMU", "enable"); fu_security_attr_add_bios_target_value(attr, "VtForDirectIo", "enable"); /** * Lenovo systems that offer a BIOS setting for ThunderboltAccess will * use this option to control whether the IOMMU is enabled by default * or not. * * It may be counter-intuitive; but as there are other more physically * difficult to attack PCIe devices it's better to have the IOMMU * enabled pre-boot even if it enables access to Thunderbolt/USB4. */ fu_security_attr_add_bios_target_value(attr, "com.thinklmi.ThunderboltAccess", "enable"); if (!self->has_iommu) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_OS); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_iommu_plugin_init(FuIommuPlugin *self) { } static void fu_iommu_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_udev_subsystem(plugin, "iommu"); } static gboolean fu_iommu_plugin_fix_host_security_attr(FuPlugin *self, FwupdSecurityAttr *attr, GError **error) { return fu_kernel_add_cmdline_arg("iommu=force", error); } static gboolean fu_iommu_plugin_undo_host_security_attr(FuPlugin *self, FwupdSecurityAttr *attr, GError **error) { return fu_kernel_remove_cmdline_arg("iommu=force", error); } static void fu_iommu_plugin_class_init(FuIommuPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_iommu_plugin_constructed; plugin_class->to_string = fu_iommu_plugin_to_string; plugin_class->backend_device_added = fu_iommu_plugin_backend_device_added; plugin_class->add_security_attrs = fu_iommu_plugin_add_security_attrs; plugin_class->fix_host_security_attr = fu_iommu_plugin_fix_host_security_attr; plugin_class->undo_host_security_attr = fu_iommu_plugin_undo_host_security_attr; } fwupd-2.0.10/plugins/iommu/fu-iommu-plugin.h000066400000000000000000000003511501337203100207230ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuIommuPlugin, fu_iommu_plugin, FU, IOMMU_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/iommu/meson.build000066400000000000000000000006151501337203100176650ustar00rootroot00000000000000if hsi and (host_cpu == 'x86' or host_cpu == 'x86_64') cargs = ['-DG_LOG_DOMAIN="FuPluginIommu"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_builtins += static_library('fu_plugin_iommu', sources: [ 'fu-iommu-plugin.c', ], include_directories: plugin_incdirs, link_with: [ fwupdplugin, fwupd, ], c_args: cargs, dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/jabra-file/000077500000000000000000000000001501337203100163675ustar00rootroot00000000000000fwupd-2.0.10/plugins/jabra-file/README.md000066400000000000000000000014461501337203100176530ustar00rootroot00000000000000--- title: Plugin: Jabra File --- ## Introduction This plugin is used to firmware update the Jabra PanaCast50 and the Lenovo ThinkSmart Bar 180. ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_0B0E&PID_3011` ## Update Behavior The device is updated at runtime using USB interrupt transfers. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x0B0E` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `2.0.0`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Gianmarco: @gdpcastro fwupd-2.0.10/plugins/jabra-file/fu-jabra-file-device.c000066400000000000000000000570261501337203100224060ustar00rootroot00000000000000/* * Copyright 2023 GN Audio * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-jabra-file-device.h" #include "fu-jabra-file-firmware.h" #include "fu-jabra-file-struct.h" #define FU_JABRA_FILE_FIRST_BLOCK 0x00 #define FU_JABRA_FILE_NEXT_BLOCK 0x01 #define FU_JABRA_FILE_CANCEL 0x02 #define FU_JABRA_FILE_MAX_RETRIES 3 #define FU_JABRA_FILE_RETRY_DELAY 100 /* ms */ #define FU_JABRA_FILE_STANDARD_SEND_TIMEOUT 3000 /* ms */ #define FU_JABRA_FILE_STANDARD_RECEIVE_TIMEOUT 1000 /* ms */ struct _FuJabraFileDevice { FuHidDevice parent_instance; guint8 sequence_number; guint8 address; guint8 epin; guint8 epout; guint dfu_pid; }; G_DEFINE_TYPE(FuJabraFileDevice, fu_jabra_file_device, FU_TYPE_HID_DEVICE) static void fu_jabra_file_device_to_string(FuDevice *device, guint idt, GString *str) { FuJabraFileDevice *self = FU_JABRA_FILE_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "SequenceNumber", self->sequence_number); fwupd_codec_string_append_hex(str, idt, "Address", self->address); fwupd_codec_string_append_hex(str, idt, "DfuPid", self->dfu_pid); } static gboolean fu_jabra_file_device_probe(FuDevice *device, GError **error) { FuJabraFileDevice *self = FU_JABRA_FILE_DEVICE(device); FuUsbInterface *iface = NULL; g_autoptr(GPtrArray) ifaces = NULL; ifaces = fu_usb_device_get_interfaces(FU_USB_DEVICE(self), error); if (ifaces == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "update interface not found"); return FALSE; } for (guint i = 0; i < ifaces->len; i++) { g_autoptr(GPtrArray) endpoints = NULL; FuUsbEndpoint *ep1 = NULL; FuUsbEndpoint *ep2 = NULL; iface = g_ptr_array_index(ifaces, i); if (fu_usb_interface_get_class(iface) == FU_USB_CLASS_HID) { endpoints = fu_usb_interface_get_endpoints(iface); if (NULL == endpoints || endpoints->len < 2) continue; ep1 = g_ptr_array_index(endpoints, 0); ep2 = g_ptr_array_index(endpoints, 1); self->epin = fu_usb_endpoint_get_address(ep1); self->epout = fu_usb_endpoint_get_address(ep2); } } if (self->epin == 0x0 || self->epout == 0x0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "update endpoints not found"); return FALSE; } return TRUE; } static gboolean fu_jabra_file_device_tx_cb(FuDevice *device, gpointer user_data, GError **error) { FuJabraFileDevice *self = FU_JABRA_FILE_DEVICE(device); FuJabraFilePacket *cmd_req = (FuJabraFilePacket *)user_data; if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), self->epout, cmd_req->data, cmd_req->len, NULL, FU_JABRA_FILE_STANDARD_SEND_TIMEOUT, NULL, /* cancellable */ error)) { g_prefix_error(error, "failed to write to device: "); return FALSE; } return TRUE; } static gboolean fu_jabra_file_device_tx(FuJabraFileDevice *self, FuJabraFilePacket *cmd_req, GError **error) { return fu_device_retry_full(FU_DEVICE(self), fu_jabra_file_device_tx_cb, FU_JABRA_FILE_MAX_RETRIES, FU_JABRA_FILE_RETRY_DELAY, cmd_req, error); } static gboolean fu_jabra_file_device_rx_cb(FuDevice *device, gpointer user_data, GError **error) { FuJabraFileDevice *self = FU_JABRA_FILE_DEVICE(device); FuJabraFilePacket *cmd_rsp = (FuJabraFilePacket *)user_data; if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), self->epin, cmd_rsp->data, cmd_rsp->len, NULL, FU_JABRA_FILE_STANDARD_RECEIVE_TIMEOUT, NULL, /* cancellable */ error)) { g_prefix_error(error, "failed to read from device: "); return FALSE; } if (cmd_rsp->data[2] == self->address && (cmd_rsp->data[5] != FU_JABRA_FILE_PACKET_CMD_IDENTITY && cmd_rsp->data[5] != FU_JABRA_FILE_PACKET_CMD_FILE && cmd_rsp->data[5] != FU_JABRA_FILE_PACKET_CMD_DFU && cmd_rsp->data[5] != FU_JABRA_FILE_PACKET_CMD_VIDEO && cmd_rsp->data[5] != FU_JABRA_FILE_PACKET_CMD_ACK && cmd_rsp->data[5] != FU_JABRA_FILE_PACKET_CMD_NACK)) { /* unrelated report, ignore and rx again */ if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), 0x82, cmd_rsp->data, cmd_rsp->len, NULL, FU_JABRA_FILE_STANDARD_RECEIVE_TIMEOUT, NULL, /* cancellable */ error)) { g_prefix_error(error, "failed to read from device: "); return FALSE; } } return TRUE; } static FuJabraFilePacket * fu_jabra_file_device_rx(FuJabraFileDevice *self, GError **error) { g_autoptr(FuJabraFilePacket) cmd_rsp = fu_jabra_file_packet_new(); if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_file_device_rx_cb, FU_JABRA_FILE_MAX_RETRIES, FU_JABRA_FILE_RETRY_DELAY, cmd_rsp, error)) return NULL; return g_steal_pointer(&cmd_rsp); } static gboolean fu_jabra_file_device_rx_with_sequence_cb(FuDevice *device, gpointer user_data, GError **error) { FuJabraFileDevice *self = FU_JABRA_FILE_DEVICE(device); FuJabraFilePacket **cmd_rsp_out = (FuJabraFilePacket **)user_data; g_autoptr(FuJabraFilePacket) cmd_rsp = NULL; cmd_rsp = fu_jabra_file_device_rx(self, error); if (cmd_rsp == NULL) return FALSE; if (self->sequence_number != cmd_rsp->data[3]) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "sequence_number error -- got 0x%x, expected 0x%x", cmd_rsp->data[3], self->sequence_number); return FALSE; } /* success */ self->sequence_number += 1; *cmd_rsp_out = g_steal_pointer(&cmd_rsp); return TRUE; } static FuJabraFilePacket * fu_jabra_file_device_rx_with_sequence(FuJabraFileDevice *self, GError **error) { g_autoptr(FuJabraFilePacket) cmd_rsp = NULL; if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_file_device_rx_with_sequence_cb, FU_JABRA_FILE_MAX_RETRIES, FU_JABRA_FILE_RETRY_DELAY, &cmd_rsp, error)) return NULL; return g_steal_pointer(&cmd_rsp); } static gboolean fu_jabra_file_device_ensure_name(FuJabraFileDevice *self, GError **error) { g_autofree gchar *name = NULL; g_autoptr(FuJabraFilePacket) cmd_req = fu_jabra_file_packet_new(); g_autoptr(FuJabraFilePacket) cmd_rsp = NULL; fu_jabra_file_packet_set_dst(cmd_req, self->address); fu_jabra_file_packet_set_src(cmd_req, 0x00); fu_jabra_file_packet_set_sequence_number(cmd_req, self->sequence_number); fu_jabra_file_packet_set_cmd_length(cmd_req, 0x46); fu_jabra_file_packet_set_cmd(cmd_req, FU_JABRA_FILE_PACKET_CMD_IDENTITY); if (!fu_jabra_file_device_tx(self, cmd_req, error)) return FALSE; cmd_rsp = fu_jabra_file_device_rx_with_sequence(self, error); if (cmd_rsp == NULL) return FALSE; name = fu_memstrsafe(cmd_rsp->data, cmd_rsp->len, FU_JABRA_FILE_PACKET_OFFSET_PAYLOAD + 1, cmd_rsp->len - (FU_JABRA_FILE_PACKET_OFFSET_PAYLOAD + 1), error); if (name == NULL) return FALSE; fu_device_set_name(FU_DEVICE(self), name); return TRUE; } static gboolean fu_jabra_file_device_ensure_dfu_pid(FuJabraFileDevice *self, GError **error) { g_autoptr(FuJabraFilePacket) cmd_req = fu_jabra_file_packet_new(); g_autoptr(FuJabraFilePacket) cmd_rsp = NULL; fu_jabra_file_packet_set_dst(cmd_req, self->address); fu_jabra_file_packet_set_src(cmd_req, 0x00); fu_jabra_file_packet_set_sequence_number(cmd_req, self->sequence_number); fu_jabra_file_packet_set_cmd_length(cmd_req, 0x46); fu_jabra_file_packet_set_cmd(cmd_req, FU_JABRA_FILE_PACKET_CMD_IDENTITY); fu_jabra_file_packet_set_sub_cmd(cmd_req, 0x13); if (!fu_jabra_file_device_tx(self, cmd_req, error)) return FALSE; cmd_rsp = fu_jabra_file_device_rx_with_sequence(self, error); if (cmd_rsp == NULL) return FALSE; self->dfu_pid = fu_memread_uint16(cmd_rsp->data + FU_JABRA_FILE_PACKET_OFFSET_PAYLOAD, G_LITTLE_ENDIAN); return TRUE; } static gboolean fu_jabra_file_device_ensure_version(FuJabraFileDevice *self, GError **error) { g_autofree gchar *version = NULL; g_autoptr(FuJabraFilePacket) cmd_req = fu_jabra_file_packet_new(); g_autoptr(FuJabraFilePacket) cmd_rsp = NULL; fu_jabra_file_packet_set_dst(cmd_req, self->address); fu_jabra_file_packet_set_src(cmd_req, 0x00); fu_jabra_file_packet_set_sequence_number(cmd_req, self->sequence_number); fu_jabra_file_packet_set_cmd_length(cmd_req, 0x46); fu_jabra_file_packet_set_cmd(cmd_req, FU_JABRA_FILE_PACKET_CMD_IDENTITY); fu_jabra_file_packet_set_sub_cmd(cmd_req, 0x03); if (!fu_jabra_file_device_tx(self, cmd_req, error)) return FALSE; cmd_rsp = fu_jabra_file_device_rx_with_sequence(self, error); if (cmd_rsp == NULL) return FALSE; version = fu_memstrsafe(cmd_rsp->data, cmd_rsp->len, FU_JABRA_FILE_PACKET_OFFSET_PAYLOAD + 1, cmd_rsp->len - (FU_JABRA_FILE_PACKET_OFFSET_PAYLOAD + 1), error); if (version == NULL) return FALSE; fu_device_set_version(FU_DEVICE(self), version); return TRUE; } static gboolean fu_jabra_file_device_file_checksum(FuJabraFileDevice *self, gchar *firmware_checksum, gboolean *match, GError **error) { guint8 device_checksum[16] = {0x00}; guint8 info[] = {0x01 << 6}; g_autoptr(FuJabraFilePacket) cmd_req1 = fu_jabra_file_packet_new(); g_autoptr(FuJabraFilePacket) cmd_req2 = fu_jabra_file_packet_new(); g_autoptr(FuJabraFilePacket) cmd_rsp1 = NULL; g_autoptr(FuJabraFilePacket) cmd_rsp2 = NULL; g_autoptr(GString) device_checksum_str = g_string_new(NULL); g_autoptr(GString) firmware_checksum_str = g_string_new(firmware_checksum); fu_jabra_file_packet_set_dst(cmd_req1, self->address); fu_jabra_file_packet_set_src(cmd_req1, 0x00); fu_jabra_file_packet_set_sequence_number(cmd_req1, self->sequence_number); fu_jabra_file_packet_set_cmd_length(cmd_req1, 0x47); fu_jabra_file_packet_set_cmd(cmd_req1, FU_JABRA_FILE_PACKET_CMD_FILE); fu_jabra_file_packet_set_sub_cmd(cmd_req1, 0x03); if (!fu_jabra_file_device_tx(self, cmd_req1, error)) return FALSE; cmd_rsp1 = fu_jabra_file_device_rx_with_sequence(self, error); if (cmd_rsp1 == NULL) return FALSE; fu_jabra_file_packet_set_dst(cmd_req2, self->address); fu_jabra_file_packet_set_src(cmd_req2, 0x00); fu_jabra_file_packet_set_sequence_number(cmd_req2, self->sequence_number); fu_jabra_file_packet_set_cmd_length(cmd_req2, 0x47); fu_jabra_file_packet_set_cmd(cmd_req2, FU_JABRA_FILE_PACKET_CMD_FILE); fu_jabra_file_packet_set_sub_cmd(cmd_req2, 0x03); if (!fu_jabra_file_packet_set_payload(cmd_req2, info, sizeof(info), error)) return FALSE; if (!fu_jabra_file_device_tx(self, cmd_req2, error)) return FALSE; cmd_rsp2 = fu_jabra_file_device_rx_with_sequence(self, error); if (cmd_rsp2 == NULL) return FALSE; if (cmd_rsp2->data[5] == FU_JABRA_FILE_PACKET_CMD_NACK) { *match = FALSE; return TRUE; } if (!fu_memcpy_safe(device_checksum, sizeof(device_checksum), 0, cmd_rsp2->data, cmd_rsp2->len, 12, sizeof(device_checksum), error)) return FALSE; for (guint i = 0; i < sizeof(device_checksum); i++) g_string_append_printf(device_checksum_str, "%02x", device_checksum[i]); *match = g_string_equal(device_checksum_str, firmware_checksum_str); return TRUE; } static gboolean fu_jabra_file_device_write_delete_file(FuJabraFileDevice *self, GError **error) { guint8 data[] = { FU_JABRA_FILE_FIRST_BLOCK << 6 | 11, 'u', 'p', 'g', 'r', 'a', 'd', 'e', '.', 'z', 'i', 'p', }; g_autoptr(FuJabraFilePacket) cmd_req = fu_jabra_file_packet_new(); g_autoptr(FuJabraFilePacket) cmd_rsp = NULL; fu_jabra_file_packet_set_dst(cmd_req, self->address); fu_jabra_file_packet_set_src(cmd_req, 0x00); fu_jabra_file_packet_set_sequence_number(cmd_req, self->sequence_number); fu_jabra_file_packet_set_cmd_length(cmd_req, 0x80 + 12 + 6); fu_jabra_file_packet_set_cmd(cmd_req, FU_JABRA_FILE_PACKET_CMD_FILE); fu_jabra_file_packet_set_sub_cmd(cmd_req, 0x04); if (!fu_jabra_file_packet_set_payload(cmd_req, data, sizeof(data), error)) return FALSE; if (!fu_jabra_file_device_tx(self, cmd_req, error)) return FALSE; cmd_rsp = fu_jabra_file_device_rx_with_sequence(self, error); if (cmd_rsp == NULL) return FALSE; if (cmd_rsp->data[5] == FU_JABRA_FILE_PACKET_CMD_NACK && cmd_rsp->data[6] == 0xF7) return TRUE; if (cmd_rsp->data[5] == FU_JABRA_FILE_PACKET_CMD_ACK) return TRUE; g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "internal error: expected 0xFF, got 0x%02x 0x%02x", cmd_rsp->data[5], cmd_rsp->data[6]); return FALSE; } static gboolean fu_jabra_file_device_write_first_block(FuJabraFileDevice *self, FuFirmware *firmware, GError **error) { guint8 data[5] = {FU_JABRA_FILE_FIRST_BLOCK << 6 | 15}; guint8 upgrade[11] = {'u', 'p', 'g', 'r', 'a', 'd', 'e', '.', 'z', 'i', 'p'}; g_autoptr(FuJabraFilePacket) cmd_req = fu_jabra_file_packet_new(); g_autoptr(FuJabraFilePacket) cmd_rsp = NULL; fu_jabra_file_packet_set_dst(cmd_req, self->address); fu_jabra_file_packet_set_src(cmd_req, 0x00); fu_jabra_file_packet_set_sequence_number(cmd_req, self->sequence_number); fu_jabra_file_packet_set_cmd_length(cmd_req, 0x80 + 16 + 6); fu_jabra_file_packet_set_cmd(cmd_req, FU_JABRA_FILE_PACKET_CMD_FILE); fu_memwrite_uint32(data + 1, fu_firmware_get_size(firmware), G_BIG_ENDIAN); if (!fu_jabra_file_packet_set_payload(cmd_req, data, sizeof(data), error)) return FALSE; if (!fu_memcpy_safe(cmd_req->data, cmd_req->len, FU_JABRA_FILE_PACKET_OFFSET_PAYLOAD + sizeof(data), upgrade, sizeof(upgrade), 0x0, sizeof(upgrade), error)) return FALSE; if (!fu_jabra_file_device_tx(self, cmd_req, error)) return FALSE; cmd_rsp = fu_jabra_file_device_rx_with_sequence(self, error); if (cmd_rsp == NULL) return FALSE; if (cmd_rsp->data[5] != FU_JABRA_FILE_PACKET_CMD_ACK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "internal error: expected 0xFF, got 0x%02x 0x%02x", cmd_rsp->data[5], cmd_rsp->data[6]); return FALSE; } return TRUE; } static gboolean fu_jabra_file_device_write_next_block(FuJabraFileDevice *self, guint32 chunk_number, const guint8 *buf, gsize bufsz, GError **error) { guint8 data[2] = {FU_JABRA_FILE_NEXT_BLOCK << 6 | (bufsz + 1), chunk_number}; g_autoptr(FuJabraFilePacket) cmd_req = fu_jabra_file_packet_new(); fu_jabra_file_packet_set_dst(cmd_req, self->address); fu_jabra_file_packet_set_src(cmd_req, 0x00); fu_jabra_file_packet_set_sequence_number(cmd_req, self->sequence_number); fu_jabra_file_packet_set_cmd_length(cmd_req, (((chunk_number + 1) % 101 == 0) ? 0x80 : 0x00) + bufsz + 8); fu_jabra_file_packet_set_cmd(cmd_req, FU_JABRA_FILE_PACKET_CMD_FILE); if (!fu_jabra_file_packet_set_payload(cmd_req, data, sizeof(data), error)) return FALSE; if (!fu_memcpy_safe(cmd_req->data, cmd_req->len, FU_JABRA_FILE_PACKET_OFFSET_PAYLOAD + sizeof(data), buf, bufsz, 0x0, bufsz, error)) return FALSE; if (!fu_jabra_file_device_tx(self, cmd_req, error)) return FALSE; if ((chunk_number + 1) % 101 == 0) { g_autoptr(FuJabraFilePacket) cmd_rsp = NULL; cmd_rsp = fu_jabra_file_device_rx(self, error); if (cmd_rsp == NULL) return FALSE; if (cmd_rsp->data[5] != FU_JABRA_FILE_PACKET_CMD_ACK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "internal error: expected 0xFF, got 0x%02x 0x%02x", cmd_rsp->data[5], cmd_rsp->data[6]); return FALSE; } } /* success */ return TRUE; } static gboolean fu_jabra_file_device_write_blocks(FuJabraFileDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (gint i = 0; (guint)i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_jabra_file_device_write_next_block(self, i, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_jabra_file_device_check_device_busy(FuJabraFileDevice *self, GError **error) { g_autoptr(FuJabraFilePacket) cmd_req = fu_jabra_file_packet_new(); g_autoptr(FuJabraFilePacket) cmd_rsp = NULL; fu_jabra_file_packet_set_dst(cmd_req, self->address); fu_jabra_file_packet_set_src(cmd_req, 0x00); fu_jabra_file_packet_set_sequence_number(cmd_req, self->sequence_number); fu_jabra_file_packet_set_cmd_length(cmd_req, 0x46); fu_jabra_file_packet_set_cmd(cmd_req, FU_JABRA_FILE_PACKET_CMD_VIDEO); fu_jabra_file_packet_set_sub_cmd(cmd_req, 0x1D); if (!fu_jabra_file_device_tx(self, cmd_req, error)) return FALSE; cmd_rsp = fu_jabra_file_device_rx_with_sequence(self, error); if (cmd_rsp == NULL) return FALSE; if (cmd_rsp->data[7] != 0x00) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_AUTH_FAILED, "is busy"); return FALSE; } /* success */ return TRUE; } static gboolean fu_jabra_file_device_start_update(FuJabraFileDevice *self, GError **error) { guint8 data[] = {0x02}; g_autoptr(FuJabraFilePacket) cmd_req = fu_jabra_file_packet_new(); g_autoptr(FuJabraFilePacket) cmd_rsp = NULL; fu_jabra_file_packet_set_dst(cmd_req, self->address); fu_jabra_file_packet_set_src(cmd_req, 0x00); fu_jabra_file_packet_set_sequence_number(cmd_req, self->sequence_number); fu_jabra_file_packet_set_cmd_length(cmd_req, 0x87); fu_jabra_file_packet_set_cmd(cmd_req, FU_JABRA_FILE_PACKET_CMD_DFU); fu_jabra_file_packet_set_sub_cmd(cmd_req, 0x03); if (!fu_jabra_file_packet_set_payload(cmd_req, data, sizeof(data), error)) return FALSE; if (!fu_jabra_file_device_tx(self, cmd_req, error)) return FALSE; cmd_rsp = fu_jabra_file_device_rx_with_sequence(self, error); if (cmd_rsp == NULL) return FALSE; if (cmd_rsp->data[5] != FU_JABRA_FILE_PACKET_CMD_ACK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "internal error: expected 0xFF, got 0x%02x 0x%02x", cmd_rsp->data[5], cmd_rsp->data[6]); return FALSE; } return TRUE; } static FuFirmware * fu_jabra_file_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuJabraFileDevice *self = FU_JABRA_FILE_DEVICE(device); g_autoptr(FuFirmware) firmware = fu_jabra_file_firmware_new(); if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; if (fu_jabra_file_firmware_get_dfu_pid(FU_JABRA_FILE_FIRMWARE(firmware)) != self->dfu_pid) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "wrong DFU PID, got 0x%x, expected 0x%x", fu_jabra_file_firmware_get_dfu_pid(FU_JABRA_FILE_FIRMWARE(firmware)), self->dfu_pid); return NULL; } return g_steal_pointer(&firmware); } static gboolean fu_jabra_file_device_setup(FuDevice *device, GError **error) { FuJabraFileDevice *self = FU_JABRA_FILE_DEVICE(device); if (!fu_jabra_file_device_ensure_name(self, error)) return FALSE; if (!fu_jabra_file_device_ensure_version(self, error)) return FALSE; if (!fu_jabra_file_device_ensure_dfu_pid(self, error)) return FALSE; return TRUE; } static gboolean fu_jabra_file_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuJabraFileDevice *self = FU_JABRA_FILE_DEVICE(device); const guint chunk_size = 55; gboolean match = FALSE; g_autofree gchar *firmware_checksum = NULL; g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(GInputStream) stream = NULL; /* check if firmware file already exists on device */ firmware_checksum = fu_firmware_get_checksum(firmware, G_CHECKSUM_MD5, error); if (!fu_jabra_file_device_file_checksum(self, firmware_checksum, &match, error)) return FALSE; if (!match) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 1, "first-block"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 89, "next-block"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 10, "update-device"); /* file not on device, transfer it */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, chunk_size, error); if (chunks == NULL) return FALSE; if (!fu_jabra_file_device_write_delete_file(self, error)) return FALSE; if (!fu_jabra_file_device_write_first_block(self, firmware, error)) return FALSE; fu_progress_step_done(progress); if (!fu_jabra_file_device_write_blocks(self, chunks, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); if (!fu_jabra_file_device_file_checksum(self, firmware_checksum, &match, error)) return FALSE; if (!match) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "error transferring file to device, checksum doesn't match"); return FALSE; } } else { /* file already on device */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "update-device"); } if (!fu_jabra_file_device_check_device_busy(self, error)) return FALSE; if (!fu_jabra_file_device_start_update(self, error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_jabra_file_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuJabraFileDevice *self = FU_JABRA_FILE_DEVICE(device); fu_device_sleep_full(FU_DEVICE(self), 900000, progress); fu_device_set_remove_delay(FU_DEVICE(self), 10000); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static void fu_jabra_file_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 85, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 7, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 7, "reload"); } static void fu_jabra_file_device_init(FuJabraFileDevice *self) { self->address = 0x01; fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SELF_RECOVERY); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_COUNTERPART_GUIDS); fu_device_set_remove_delay(FU_DEVICE(self), 120000); fu_device_add_protocol(FU_DEVICE(self), "com.jabra.file"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_JABRA_FILE_FIRMWARE); } static void fu_jabra_file_device_class_init(FuJabraFileDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_jabra_file_device_to_string; device_class->probe = fu_jabra_file_device_probe; device_class->prepare_firmware = fu_jabra_file_device_prepare_firmware; device_class->setup = fu_jabra_file_device_setup; device_class->write_firmware = fu_jabra_file_device_write_firmware; device_class->attach = fu_jabra_file_device_attach; device_class->set_progress = fu_jabra_file_device_set_progress; } fwupd-2.0.10/plugins/jabra-file/fu-jabra-file-device.h000066400000000000000000000004461501337203100224050ustar00rootroot00000000000000/* * Copyright 2023 GN Audio A/S * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_JABRA_FILE_DEVICE (fu_jabra_file_device_get_type()) G_DECLARE_FINAL_TYPE(FuJabraFileDevice, fu_jabra_file_device, FU, JABRA_FILE_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/jabra-file/fu-jabra-file-firmware.c000066400000000000000000000105611501337203100227540ustar00rootroot00000000000000/* * Copyright 2023 GN Audio A/S * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-jabra-file-firmware.h" struct _FuJabraFileFirmware { FuArchiveFirmware parent_instance; guint16 dfu_pid; }; G_DEFINE_TYPE(FuJabraFileFirmware, fu_jabra_file_firmware, FU_TYPE_FIRMWARE) static void fu_jabra_file_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuJabraFileFirmware *self = FU_JABRA_FILE_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "dfu_pid", self->dfu_pid); } static gboolean fu_jabra_file_firmware_parse_info(FuJabraFileFirmware *self, XbSilo *silo, GError **error) { const gchar *version; const gchar *dfu_pid_str; guint64 val = 0; g_autoptr(XbNode) dfu_pid = NULL; g_autoptr(XbNode) build_vector = NULL; build_vector = xb_silo_query_first(silo, "buildVector", error); if (build_vector == NULL) { fwupd_error_convert(error); return FALSE; } version = xb_node_get_attr(build_vector, "version"); if (version == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "buildVector version missing"); return FALSE; } fu_firmware_set_version(FU_FIRMWARE(self), version); dfu_pid = xb_silo_query_first(silo, "buildVector/targetUsbPids", error); if (dfu_pid == NULL) { fwupd_error_convert(error); return FALSE; } dfu_pid_str = xb_node_query_text(dfu_pid, "usbPid", error); if (dfu_pid_str == NULL) { fwupd_error_convert(error); return FALSE; } if (!fu_strtoull(dfu_pid_str, &val, 0x0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "cannot parse usbPid of %s: ", dfu_pid_str); return FALSE; } self->dfu_pid = (guint16)val; /* success */ return TRUE; } static gboolean fu_jabra_file_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuJabraFileFirmware *self = FU_JABRA_FILE_FIRMWARE(firmware); g_autoptr(FuFirmware) firmware_archive = fu_archive_firmware_new(); g_autoptr(FuFirmware) img_xml = NULL; g_autoptr(FuFirmware) upgrade_archive = NULL; g_autoptr(GBytes) img_blob = NULL; g_autoptr(GBytes) upgrade_blob = NULL; g_autoptr(XbBuilder) builder = xb_builder_new(); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_autoptr(XbSilo) silo = NULL; /* FuArchiveFirmware->parse */ fu_archive_firmware_set_format(FU_ARCHIVE_FIRMWARE(firmware_archive), FU_ARCHIVE_FORMAT_ZIP); fu_archive_firmware_set_compression(FU_ARCHIVE_FIRMWARE(firmware_archive), FU_ARCHIVE_COMPRESSION_NONE); if (!fu_firmware_parse_stream(firmware_archive, stream, 0x0, flags, error)) return FALSE; img_xml = fu_archive_firmware_get_image_fnmatch(FU_ARCHIVE_FIRMWARE(firmware_archive), "info.xml", error); img_blob = fu_firmware_get_bytes(img_xml, error); if (img_blob == NULL) return FALSE; if (!xb_builder_source_load_bytes(source, img_blob, XB_BUILDER_SOURCE_FLAG_NONE, error)) { fwupd_error_convert(error); return FALSE; } xb_builder_import_source(builder, source); silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, error); if (silo == NULL) { fwupd_error_convert(error); return FALSE; } if (!fu_jabra_file_firmware_parse_info(self, silo, error)) return FALSE; upgrade_archive = fu_archive_firmware_get_image_fnmatch(FU_ARCHIVE_FIRMWARE(firmware_archive), "upgrade.zip", error); if (upgrade_archive == NULL) return FALSE; upgrade_blob = fu_firmware_get_bytes(upgrade_archive, error); if (upgrade_blob == NULL) return FALSE; fu_firmware_set_bytes(FU_FIRMWARE(self), upgrade_blob); /* success */ return TRUE; } guint16 fu_jabra_file_firmware_get_dfu_pid(FuJabraFileFirmware *self) { g_return_val_if_fail(FU_IS_JABRA_FILE_FIRMWARE(self), G_MAXUINT16); return self->dfu_pid; } static void fu_jabra_file_firmware_init(FuJabraFileFirmware *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_VID_PID); fu_firmware_set_images_max(FU_FIRMWARE(self), 1024); } static void fu_jabra_file_firmware_class_init(FuJabraFileFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_jabra_file_firmware_parse; firmware_class->export = fu_jabra_file_firmware_export; } FuFirmware * fu_jabra_file_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_JABRA_FILE_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/jabra-file/fu-jabra-file-firmware.h000066400000000000000000000007021501337203100227550ustar00rootroot00000000000000/* * Copyright 2023 GN Audio A/S * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_JABRA_FILE_FIRMWARE (fu_jabra_file_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuJabraFileFirmware, fu_jabra_file_firmware, FU, JABRA_FILE_FIRMWARE, FuFirmware) FuFirmware * fu_jabra_file_firmware_new(void); guint16 fu_jabra_file_firmware_get_dfu_pid(FuJabraFileFirmware *self); fwupd-2.0.10/plugins/jabra-file/fu-jabra-file-plugin.c000066400000000000000000000015201501337203100224310ustar00rootroot00000000000000/* * Copyright 2023 GN Audio A/S * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-jabra-file-device.h" #include "fu-jabra-file-firmware.h" #include "fu-jabra-file-plugin.h" struct _FuJabraFilePlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuJabraFilePlugin, fu_jabra_file_plugin, FU_TYPE_PLUGIN) static void fu_jabra_file_plugin_init(FuJabraFilePlugin *self) { } static void fu_jabra_file_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_JABRA_FILE_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_JABRA_FILE_FIRMWARE); } static void fu_jabra_file_plugin_class_init(FuJabraFilePluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_jabra_file_plugin_constructed; } fwupd-2.0.10/plugins/jabra-file/fu-jabra-file-plugin.h000066400000000000000000000003371501337203100224430ustar00rootroot00000000000000/* * Copyright 2023 GN Audio A/S * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuJabraFilePlugin, fu_jabra_file_plugin, FU, JABRA_FILE_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/jabra-file/fu-jabra-file.rs000066400000000000000000000010111501337203100213320ustar00rootroot00000000000000// Copyright 2023 GN Audio // Copyright 2024 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[repr(u8)] enum FuJabraFilePacketCmd { Identity = 0x02, File = 0x03, Dfu = 0x07, Video = 0x26, Nack = 0xFE, Ack = 0xFF, } #[derive(New, Default)] #[repr(C, packed)] struct FuJabraFilePacket { iface: u8 == 0x05, dst: u8, src: u8, sequence_number: u8, cmd_length: u8 = 0x46, cmd: FuJabraFilePacketCmd, sub_cmd: u8, payload: [u8; 57], } fwupd-2.0.10/plugins/jabra-file/jabra-file.quirk000066400000000000000000000007311501337203100214410ustar00rootroot00000000000000# Jabra PanaCast 50 [runtime] [USB\VID_0B0E&PID_3009] Plugin = jabra_file # Jabra PanaCast 50 [runtime] [USB\VID_0B0E&PID_3011] Plugin = jabra_file # Jabra PanaCast 50 [runtime] [USB\VID_0B0E&PID_3012] Plugin = jabra_file # Lenovo ThinkSmart Bar 180 [runtime] [USB\VID_0B0E&PID_306F] Plugin = jabra_file # Lenovo ThinkSmart Bar 180 [runtime] [USB\VID_0B0E&PID_3071] Plugin = jabra_file # Lenovo ThinkSmart Bar 180 [runtime] [USB\VID_0B0E&PID_3072] Plugin = jabra_file fwupd-2.0.10/plugins/jabra-file/meson.build000066400000000000000000000011311501337203100205250ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginJabraFile"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('jabra-file.quirk') plugin_builtins += static_library('fu_plugin_jabra_file', rustgen.process('fu-jabra-file.rs'), sources: [ 'fu-jabra-file-device.c', 'fu-jabra-file-firmware.c', 'fu-jabra-file-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files( 'tests/jabra-panacast-50.json', ) enumeration_data += files( 'tests/jabra-panacast-50-setup.json', ) fwupd-2.0.10/plugins/jabra-file/tests/000077500000000000000000000000001501337203100175315ustar00rootroot00000000000000fwupd-2.0.10/plugins/jabra-file/tests/jabra-panacast-50-setup.json000066400000000000000000000116071501337203100246600ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUsbDevice", "PlatformId": "1-6.1", "Created": "2024-11-22T16:08:04.565942Z", "IdVendor": 2830, "IdProduct": 12306, "Device": 2048, "USB": 512, "Manufacturer": 1, "Product": 2, "SerialNumber": 3, "UsbConfigDescriptors": [ { "Configuration": 4, "ConfigurationValue": 1 } ], "UsbHidDescriptors": [ "Bpn/CQGhAQkDhZoVACb/AHUIlT+yAgEJBIWbFQAlAXUBlQGBBnUBlQ+BAQoM/4WKFQAlAnUClQGRInUBlQ6RAcAFCwkFoQGFAgULFQAlAQkgCZcJKnUBlQOBIwkvCSEJJHUBlQOBBwkHBQl1AZUBgQJ1AZUJgQEFCBUAJQEJFwkJCRgJIAkhCSp1AZUGkSIFCxUAJQEJnnUBlQGRInUBlQmRAcAGAP8JBaEBhQQGMP8VACUBCv//dQGVAbEjdQGVB7EBCSAK+/8JlwkqdQGVBIEjCS8JIQkkCv3/dQGVBIEHdQGVCIEBBkD/FQAlAQkXCvv/CQkJGAkgCSEJKnUBlQeRIgYw/xUAJQEJnnUBlQGRInUBlQiRAYUFBgD/CQEVACb/AHUIlT+SAgEJARUAJv8AdQiVP4ICAcAFDAkBoQGFARUAJQEJ6gnpdQGVAoECdQGVDoEBwA==" ], "UsbInterfaces": [ { "Length": 9, "DescriptorType": 4, "InterfaceClass": 1, "InterfaceSubClass": 1 }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 1, "InterfaceClass": 1, "InterfaceSubClass": 2 }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 1, "AlternateSetting": 1, "InterfaceClass": 1, "InterfaceSubClass": 2, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 1, "Interval": 4, "MaxPacketSize": 300 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 2, "InterfaceClass": 1, "InterfaceSubClass": 2 }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 2, "AlternateSetting": 1, "InterfaceClass": 1, "InterfaceSubClass": 2, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 129, "Interval": 4, "MaxPacketSize": 300 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 3, "InterfaceClass": 3, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 130, "Interval": 4, "MaxPacketSize": 1024 }, { "DescriptorType": 5, "EndpointAddress": 2, "Interval": 4, "MaxPacketSize": 1024 } ] } ], "UsbEvents": [ { "Id": "#4693935e", "Data": "001" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=57\nDEVNAME=bus/usb/001/058\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=b0e/3012/800\nTYPE=0/0/0\nBUSNUM=001\nDEVNUM=058" }, { "Id": "#1ab3ae0a", "Data": "058" }, { "Id": "#4693935e", "Data": "001" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=57\nDEVNAME=bus/usb/001/058\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=b0e/3012/800\nTYPE=0/0/0\nBUSNUM=001\nDEVNUM=058" }, { "Id": "#1ab3ae0a", "Data": "058" }, { "Id": "#b3f170c5", "Data": "BQEAAEYCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" }, { "Id": "#6fb93b68", "Data": "BQABANgCABFKYWJyYSBQYW5hQ2FzdCA1MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" }, { "Id": "#5807d366", "Data": "BQEAAUYCAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" }, { "Id": "#6fb93b68", "Data": "BQABAcwCAwU4LjAuNwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" }, { "Id": "#101aaaaa", "Data": "BQEAAkYCEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" }, { "Id": "#6fb93b68", "Data": "BQABAsgCExAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" }, { "Id": "#e1fb3779", "Data": "Bpn/CQGhAQkDhZoVACb/AHUIlT+yAgEJBIWbFQAlAXUBlQGBBnUBlQ+BAQoM/4WKFQAlAnUClQGRInUBlQ6RAcAFCwkFoQGFAgULFQAlAQkgCZcJKnUBlQOBIwkvCSEJJHUBlQOBBwkHBQl1AZUBgQJ1AZUJgQEFCBUAJQEJFwkJCRgJIAkhCSp1AZUGkSIFCxUAJQEJnnUBlQGRInUBlQmRAcAGAP8JBaEBhQQGMP8VACUBCv//dQGVAbEjdQGVB7EBCSAK+/8JlwkqdQGVBIEjCS8JIQkkCv3/dQGVBIEHdQGVCIEBBkD/FQAlAQkXCvv/CQkJGAkgCSEJKnUBlQeRIgYw/xUAJQEJnnUBlQGRInUBlQiRAYUFBgD/CQEVACb/AHUIlT+SAgEJARUAJv8AdQiVP4ICAcAFDAkBoQGFARUAJQEJ6gnpdQGVAoECdQGVDoEBwA==" } ] } ] } fwupd-2.0.10/plugins/jabra-file/tests/jabra-panacast-50.json000066400000000000000000000011101501337203100235060ustar00rootroot00000000000000{ "name": "Jabra PanaCast 50", "interactive": false, "steps": [ { "url": "887800a9dec01390fa1581f95c07ec5d54aa0bdcda9e7fb5f9bb782702eedfe0-jabra-panacast-50-8.0.7.cab", "emulation-url": "779eda568d6146da1c02ee36d65e3b0dcc9b0094b924ccc976ab90b01967ed51-emulation.zip", "emulation-file": "@enumeration_datadir@/jabra-panacast-50-setup.json", "components": [ { "version": "8.0.7", "protocol": "com.jabra.file", "guids": [ "e18cee37-6325-5b8d-9c30-8b2fb6ddd577" ] } ] } ] } fwupd-2.0.10/plugins/jabra-gnp/000077500000000000000000000000001501337203100162345ustar00rootroot00000000000000fwupd-2.0.10/plugins/jabra-gnp/README.md000066400000000000000000000021761501337203100175210ustar00rootroot00000000000000--- title: Plugin: Jabra GNP --- ## Introduction This plugin is used to firmware update for some Jabra devices (refer to the `jabra-gnp.quirk` file for more information). Notably this excludes devices supported by the `jabra` plugin, as well as 1st edition Jabra Evolve (non-SE) devices and the corresponding Jabra Link connectors. ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_0B0E&PID_24DB` ## Update Behavior The device is updated at runtime using USB control and interrupt transfers. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x0B0E` ## Quirk Use This plugin uses the following plugin-specific quirks: ### JabraGnpAddress Address to use, defaulting to `0x08`. Since: 1.9.20 ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.9.2`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Gianmarco: @gdpcastro fwupd-2.0.10/plugins/jabra-gnp/fu-jabra-gnp-common.c000066400000000000000000000100151501337203100221340ustar00rootroot00000000000000/* * Copyright 2023 GN Audio * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-jabra-gnp-common.h" static guint64 fu_jabra_gnp_update_crc(guint64 acc, guint64 delta) { static guint64 crcLookupTable[] = { 0x00000000, 0xDB710641, 0x6D930AC3, 0xB6E20C82, 0xDB261586, 0x005713C7, 0xB6B51F45, 0x6DC41904, 0x6D3D2D4D, 0xB64C2B0C, 0x00AE278E, 0xDBDF21CF, 0xB61B38CB, 0x6D6A3E8A, 0xDB883208, 0x00F93449, 0xDA7A5A9A, 0x010B5CDB, 0xB7E95059, 0x6C985618, 0x015C4F1C, 0xDA2D495D, 0x6CCF45DF, 0xB7BE439E, 0xB74777D7, 0x6C367196, 0xDAD47D14, 0x01A57B55, 0x6C616251, 0xB7106410, 0x01F26892, 0xDA836ED3, 0x6F85B375, 0xB4F4B534, 0x0216B9B6, 0xD967BFF7, 0xB4A3A6F3, 0x6FD2A0B2, 0xD930AC30, 0x0241AA71, 0x02B89E38, 0xD9C99879, 0x6F2B94FB, 0xB45A92BA, 0xD99E8BBE, 0x02EF8DFF, 0xB40D817D, 0x6F7C873C, 0xB5FFE9EF, 0x6E8EEFAE, 0xD86CE32C, 0x031DE56D, 0x6ED9FC69, 0xB5A8FA28, 0x034AF6AA, 0xD83BF0EB, 0xD8C2C4A2, 0x03B3C2E3, 0xB551CE61, 0x6E20C820, 0x03E4D124, 0xD895D765, 0x6E77DBE7, 0xB506DDA6, 0xDF0B66EA, 0x047A60AB, 0xB2986C29, 0x69E96A68, 0x042D736C, 0xDF5C752D, 0x69BE79AF, 0xB2CF7FEE, 0xB2364BA7, 0x69474DE6, 0xDFA54164, 0x04D44725, 0x69105E21, 0xB2615860, 0x048354E2, 0xDFF252A3, 0x05713C70, 0xDE003A31, 0x68E236B3, 0xB39330F2, 0xDE5729F6, 0x05262FB7, 0xB3C42335, 0x68B52574, 0x684C113D, 0xB33D177C, 0x05DF1BFE, 0xDEAE1DBF, 0xB36A04BB, 0x681B02FA, 0xDEF90E78, 0x05880839, 0xB08ED59F, 0x6BFFD3DE, 0xDD1DDF5C, 0x066CD91D, 0x6BA8C019, 0xB0D9C658, 0x063BCADA, 0xDD4ACC9B, 0xDDB3F8D2, 0x06C2FE93, 0xB020F211, 0x6B51F450, 0x0695ED54, 0xDDE4EB15, 0x6B06E797, 0xB077E1D6, 0x6AF48F05, 0xB1858944, 0x076785C6, 0xDC168387, 0xB1D29A83, 0x6AA39CC2, 0xDC419040, 0x07309601, 0x07C9A248, 0xDCB8A409, 0x6A5AA88B, 0xB12BAECA, 0xDCEFB7CE, 0x079EB18F, 0xB17CBD0D, 0x6A0DBB4C, 0x6567CB95, 0xBE16CDD4, 0x08F4C156, 0xD385C717, 0xBE41DE13, 0x6530D852, 0xD3D2D4D0, 0x08A3D291, 0x085AE6D8, 0xD32BE099, 0x65C9EC1B, 0xBEB8EA5A, 0xD37CF35E, 0x080DF51F, 0xBEEFF99D, 0x659EFFDC, 0xBF1D910F, 0x646C974E, 0xD28E9BCC, 0x09FF9D8D, 0x643B8489, 0xBF4A82C8, 0x09A88E4A, 0xD2D9880B, 0xD220BC42, 0x0951BA03, 0xBFB3B681, 0x64C2B0C0, 0x0906A9C4, 0xD277AF85, 0x6495A307, 0xBFE4A546, 0x0AE278E0, 0xD1937EA1, 0x67717223, 0xBC007462, 0xD1C46D66, 0x0AB56B27, 0xBC5767A5, 0x672661E4, 0x67DF55AD, 0xBCAE53EC, 0x0A4C5F6E, 0xD13D592F, 0xBCF9402B, 0x6788466A, 0xD16A4AE8, 0x0A1B4CA9, 0xD098227A, 0x0BE9243B, 0xBD0B28B9, 0x667A2EF8, 0x0BBE37FC, 0xD0CF31BD, 0x662D3D3F, 0xBD5C3B7E, 0xBDA50F37, 0x66D40976, 0xD03605F4, 0x0B4703B5, 0x66831AB1, 0xBDF21CF0, 0x0B101072, 0xD0611633, 0xBA6CAD7F, 0x611DAB3E, 0xD7FFA7BC, 0x0C8EA1FD, 0x614AB8F9, 0xBA3BBEB8, 0x0CD9B23A, 0xD7A8B47B, 0xD7518032, 0x0C208673, 0xBAC28AF1, 0x61B38CB0, 0x0C7795B4, 0xD70693F5, 0x61E49F77, 0xBA959936, 0x6016F7E5, 0xBB67F1A4, 0x0D85FD26, 0xD6F4FB67, 0xBB30E263, 0x6041E422, 0xD6A3E8A0, 0x0DD2EEE1, 0x0D2BDAA8, 0xD65ADCE9, 0x60B8D06B, 0xBBC9D62A, 0xD60DCF2E, 0x0D7CC96F, 0xBB9EC5ED, 0x60EFC3AC, 0xD5E91E0A, 0x0E98184B, 0xB87A14C9, 0x630B1288, 0x0ECF0B8C, 0xD5BE0DCD, 0x635C014F, 0xB82D070E, 0xB8D43347, 0x63A53506, 0xD5473984, 0x0E363FC5, 0x63F226C1, 0xB8832080, 0x0E612C02, 0xD5102A43, 0x0F934490, 0xD4E242D1, 0x62004E53, 0xB9714812, 0xD4B55116, 0x0FC45757, 0xB9265BD5, 0x62575D94, 0x62AE69DD, 0xB9DF6F9C, 0x0F3D631E, 0xD44C655F, 0xB9887C5B, 0x62F97A1A, 0xD41B7698, 0x0F6A70D9}; guint64 t = acc >> 24; guint32 lookup = (guint32)(t & 0xFF); acc = ((acc) << 8) ^ crcLookupTable[lookup] ^ (delta); return acc & 0x00000000FFFFFFFF; } guint64 fu_jabra_gnp_calculate_crc(GBytes *bytes) { guint64 crc = 0; guint8 tmp[] = {0xFF, 0xFF, 0xFF, 0xFF}; g_autoptr(GByteArray) buf = g_byte_array_new(); g_byte_array_append(buf, tmp, sizeof(tmp)); fu_byte_array_append_bytes(buf, bytes); for (guint i = buf->len; i > 0; i -= 2) { if (i > 1) crc = fu_jabra_gnp_update_crc(crc, buf->data[i - 2] & 0xFF); crc = fu_jabra_gnp_update_crc(crc, buf->data[i - 1] & 0xFF); } return crc; } fwupd-2.0.10/plugins/jabra-gnp/fu-jabra-gnp-common.h000066400000000000000000000002631501337203100221450ustar00rootroot00000000000000/* * Copyright 2023 GN Audio A/S * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include guint64 fu_jabra_gnp_calculate_crc(GBytes *bytes); fwupd-2.0.10/plugins/jabra-gnp/fu-jabra-gnp-device.c000066400000000000000000000755661501337203100221310ustar00rootroot00000000000000/* * Copyright 2023 GN Audio * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-jabra-gnp-device.h" #include "fu-jabra-gnp-firmware.h" #include "fu-jabra-gnp-image.h" #define FU_JABRA_GNP_BUF_SIZE 64 #define FU_JABRA_GNP_MAX_RETRIES 3 #define FU_JABRA_GNP_PRELOAD_COUNT 10 #define FU_JABRA_GNP_RETRY_DELAY 100 /* ms */ #define FU_JABRA_GNP_STANDARD_SEND_TIMEOUT 3000 /* ms */ #define FU_JABRA_GNP_STANDARD_RECEIVE_TIMEOUT 1000 /* ms */ #define FU_JABRA_GNP_LONG_RECEIVE_TIMEOUT 30000 /* ms */ #define FU_JABRA_GNP_EXTRA_LONG_RECEIVE_TIMEOUT 60000 /* ms */ #define FU_JABRA_GNP_IFACE 0x05 #define FU_JABRA_GNP_ADDRESS_PARENT 0x01 #define FU_JABRA_GNP_ADDRESS_OTA_CHILD 0x04 struct _FuJabraGnpDevice { FuUsbDevice parent_instance; guint8 iface_hid; guint8 sequence_number; guint8 address; guint dfu_pid; }; typedef struct { guint8 txbuf[FU_JABRA_GNP_BUF_SIZE]; const guint timeout; } FuJabraGnpTxData; typedef struct { guint8 rxbuf[FU_JABRA_GNP_BUF_SIZE]; const guint timeout; } FuJabraGnpRxData; G_DEFINE_TYPE(FuJabraGnpDevice, fu_jabra_gnp_device, FU_TYPE_USB_DEVICE) static void fu_jabra_gnp_device_to_string(FuDevice *device, guint idt, GString *str) { FuJabraGnpDevice *self = FU_JABRA_GNP_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "IfaceHid", self->iface_hid); fwupd_codec_string_append_hex(str, idt, "SequenceNumber", self->sequence_number); fwupd_codec_string_append_hex(str, idt, "Address", self->address); fwupd_codec_string_append_hex(str, idt, "DfuPid", self->dfu_pid); } static guint8 _fu_usb_device_get_interface_for_class(FuUsbDevice *usb_device, guint8 intf_class, GError **error) { g_autoptr(GPtrArray) intfs = NULL; intfs = fu_usb_device_get_interfaces(usb_device, error); if (intfs == NULL) return 0xFF; for (guint i = 0; i < intfs->len; i++) { FuUsbInterface *intf = g_ptr_array_index(intfs, i); if (fu_usb_interface_get_class(intf) == intf_class) return fu_usb_interface_get_number(intf); } return 0xFF; } static gboolean fu_jabra_gnp_device_tx_cb(FuDevice *device, gpointer user_data, GError **error) { FuJabraGnpDevice *self = FU_JABRA_GNP_DEVICE(device); FuJabraGnpTxData *tx_data = (FuJabraGnpTxData *)user_data; FuUsbDevice *target = self->address == FU_JABRA_GNP_ADDRESS_OTA_CHILD ? FU_USB_DEVICE(fu_device_get_parent(device)) : FU_USB_DEVICE(self); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(target), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_CLASS, FU_USB_RECIPIENT_INTERFACE, 0x09, 0x0200 | FU_JABRA_GNP_IFACE, self->iface_hid, tx_data->txbuf, FU_JABRA_GNP_BUF_SIZE, NULL, tx_data->timeout, NULL, /* cancellable */ error)) { g_prefix_error(error, "failed to write to device: "); return FALSE; } return TRUE; } static gboolean fu_jabra_gnp_device_rx_cb(FuDevice *device, gpointer user_data, GError **error) { FuJabraGnpDevice *self = FU_JABRA_GNP_DEVICE(device); const guint8 match_buf[FU_JABRA_GNP_BUF_SIZE] = {FU_JABRA_GNP_IFACE, 0x00, self->address, 0x00, 0x0A, 0x12, 0x02}; const guint8 empty_buf[FU_JABRA_GNP_BUF_SIZE] = {0x00}; FuJabraGnpRxData *rx_data = (FuJabraGnpRxData *)user_data; FuUsbDevice *target = self->address == FU_JABRA_GNP_ADDRESS_OTA_CHILD ? FU_USB_DEVICE(fu_device_get_parent(device)) : FU_USB_DEVICE(self); if (!fu_usb_device_interrupt_transfer(target, 0x81, rx_data->rxbuf, FU_JABRA_GNP_BUF_SIZE, NULL, rx_data->timeout, NULL, /* cancellable */ error)) { g_prefix_error(error, "failed to read from device: "); return FALSE; } if (rx_data->rxbuf[2] == match_buf[2] && rx_data->rxbuf[5] == match_buf[5] && rx_data->rxbuf[6] == match_buf[6]) { /* battery report, ignore and rx again */ if (!fu_usb_device_interrupt_transfer(target, 0x81, rx_data->rxbuf, FU_JABRA_GNP_BUF_SIZE, NULL, rx_data->timeout, NULL, /* cancellable */ error)) { g_prefix_error(error, "failed to read from device: "); return FALSE; } } if (fu_memcmp_safe(rx_data->rxbuf, sizeof(rx_data->rxbuf), 0, empty_buf, sizeof(rx_data->rxbuf), 0, sizeof(rx_data->rxbuf), error)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "error readuing from device"); return FALSE; } return TRUE; } static gboolean fu_jabra_gnp_device_rx_with_sequence_cb(FuDevice *device, gpointer user_data, GError **error) { FuJabraGnpDevice *self = FU_JABRA_GNP_DEVICE(device); FuJabraGnpRxData *rx_data = (FuJabraGnpRxData *)user_data; if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_rx_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, rx_data, error)) return FALSE; if (self->sequence_number != rx_data->rxbuf[3]) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "sequence_number error -- got 0x%x, expected 0x%x", rx_data->rxbuf[3], self->sequence_number); return FALSE; } self->sequence_number += 1; return TRUE; } static gboolean fu_jabra_gnp_device_read_name(FuJabraGnpDevice *self, GError **error) { FuJabraGnpTxData tx_data = { .txbuf = { FU_JABRA_GNP_IFACE, self->address, 0x00, self->sequence_number, 0x46, 0x02, 0x00, }, .timeout = FU_JABRA_GNP_STANDARD_SEND_TIMEOUT, }; FuJabraGnpRxData rx_data = { .rxbuf = {0x00}, .timeout = FU_JABRA_GNP_STANDARD_RECEIVE_TIMEOUT, }; g_autofree gchar *name = NULL; if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_tx_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&tx_data, error)) return FALSE; if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_rx_with_sequence_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&rx_data, error)) return FALSE; name = fu_memstrsafe(rx_data.rxbuf, sizeof(rx_data.rxbuf), 0x8, sizeof(rx_data.rxbuf) - 8, error); if (name == NULL) return FALSE; fu_device_set_name(FU_DEVICE(self), name); return TRUE; } static gboolean fu_jabra_gnp_device_read_child_dfu_pid(FuJabraGnpDevice *self, guint16 *child_dfu_pid, GError **error) { FuJabraGnpTxData tx_data = { .txbuf = { FU_JABRA_GNP_IFACE, FU_JABRA_GNP_ADDRESS_OTA_CHILD, 0x00, self->sequence_number, 0x46, 0x02, 0x13, }, .timeout = FU_JABRA_GNP_STANDARD_SEND_TIMEOUT, }; FuJabraGnpRxData rx_data = { .rxbuf = {0x00}, .timeout = FU_JABRA_GNP_STANDARD_RECEIVE_TIMEOUT, }; g_return_val_if_fail(child_dfu_pid != NULL, FALSE); if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_tx_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&tx_data, error)) return FALSE; if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_rx_with_sequence_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&rx_data, error)) return FALSE; /* no child device to respond properly */ if (rx_data.rxbuf[5] == 0xFE && rx_data.rxbuf[6] == 0xF4) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "internal error: no child device responded"); return FALSE; } /* success */ *child_dfu_pid = fu_memread_uint16(rx_data.rxbuf + 7, G_LITTLE_ENDIAN); return TRUE; } static gboolean fu_jabra_gnp_device_read_child_battery_level(FuJabraGnpDevice *self, GError **error) { FuJabraGnpTxData tx_data = { .txbuf = { FU_JABRA_GNP_IFACE, self->address, 0x00, self->sequence_number, 0x46, 0x12, 0x02, }, .timeout = FU_JABRA_GNP_STANDARD_SEND_TIMEOUT, }; FuJabraGnpRxData rx_data = { .rxbuf = {0x00}, .timeout = FU_JABRA_GNP_STANDARD_RECEIVE_TIMEOUT, }; guint8 battery_level = 0; if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_tx_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&tx_data, error)) return FALSE; if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_rx_with_sequence_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&rx_data, error)) return FALSE; if (!fu_memread_uint8_safe(rx_data.rxbuf, FU_JABRA_GNP_BUF_SIZE, 8, &battery_level, error)) return FALSE; if (battery_level == 0x00) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "battery level was 0"); return FALSE; } fu_device_set_battery_level(FU_DEVICE(self), battery_level); fu_device_set_battery_threshold(FU_DEVICE(self), 30); return TRUE; } static gboolean fu_jabra_gnp_device_read_dfu_pid(FuJabraGnpDevice *self, GError **error) { FuJabraGnpTxData tx_data = { .txbuf = { FU_JABRA_GNP_IFACE, self->address, 0x00, self->sequence_number, 0x46, 0x02, 0x13, }, .timeout = FU_JABRA_GNP_STANDARD_SEND_TIMEOUT, }; FuJabraGnpRxData rx_data = { .rxbuf = {0x00}, .timeout = FU_JABRA_GNP_STANDARD_RECEIVE_TIMEOUT, }; if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_tx_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&tx_data, error)) return FALSE; if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_rx_with_sequence_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&rx_data, error)) return FALSE; self->dfu_pid = fu_memread_uint16(rx_data.rxbuf + 7, G_LITTLE_ENDIAN); return TRUE; } static gboolean fu_jabra_gnp_device_read_version(FuJabraGnpDevice *self, GError **error) { FuJabraGnpTxData tx_data = { .txbuf = { FU_JABRA_GNP_IFACE, self->address, 0x00, self->sequence_number, 0x46, 0x02, 0x03, }, .timeout = FU_JABRA_GNP_STANDARD_SEND_TIMEOUT, }; FuJabraGnpRxData rx_data = { .rxbuf = {0x00}, .timeout = FU_JABRA_GNP_STANDARD_RECEIVE_TIMEOUT, }; g_autofree gchar *version = NULL; if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_tx_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&tx_data, error)) return FALSE; if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_rx_with_sequence_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&rx_data, error)) return FALSE; version = fu_memstrsafe(rx_data.rxbuf, sizeof(rx_data.rxbuf), 0x8, sizeof(rx_data.rxbuf) - 8, error); if (version == NULL) return FALSE; fu_device_set_version(FU_DEVICE(self), version); return TRUE; } static gboolean fu_jabra_gnp_device_write_partition(FuJabraGnpDevice *self, guint8 part, GError **error) { FuJabraGnpTxData tx_data = { .txbuf = { FU_JABRA_GNP_IFACE, self->address, 0x00, self->sequence_number, 0x87, 0x0F, 0x2D, part, }, .timeout = FU_JABRA_GNP_STANDARD_SEND_TIMEOUT, }; FuJabraGnpRxData rx_data = { .rxbuf = {0x00}, .timeout = FU_JABRA_GNP_STANDARD_RECEIVE_TIMEOUT, }; if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_tx_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&tx_data, error)) return FALSE; if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_rx_with_sequence_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&rx_data, error)) return FALSE; if (rx_data.rxbuf[5] != 0xFF) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "internal error: expected 0xFF, got 0x%02x 0x%02x", rx_data.rxbuf[5], rx_data.rxbuf[6]); return FALSE; } return TRUE; } static gboolean fu_jabra_gnp_device_start(FuJabraGnpDevice *self, GError **error) { FuJabraGnpTxData tx_data = { .txbuf = { FU_JABRA_GNP_IFACE, self->address, 0x00, self->sequence_number, 0x86, 0x0F, 0x17, }, .timeout = FU_JABRA_GNP_STANDARD_SEND_TIMEOUT, }; FuJabraGnpRxData rx_data = { .rxbuf = {0x00}, .timeout = FU_JABRA_GNP_STANDARD_RECEIVE_TIMEOUT, }; if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_tx_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&tx_data, error)) return FALSE; if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_rx_with_sequence_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&rx_data, error)) return FALSE; if (rx_data.rxbuf[5] != 0xFF) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "internal error: expected 0xFF, got 0x%02x 0x%02x", rx_data.rxbuf[5], rx_data.rxbuf[6]); return FALSE; } return TRUE; } static gboolean fu_jabra_gnp_device_flash_erase_done(FuJabraGnpDevice *self, GError **error) { const guint8 match_buf[FU_JABRA_GNP_BUF_SIZE] = {FU_JABRA_GNP_IFACE, 0x00, self->address, 0x00, 0x06, 0x0F, 0x18}; FuJabraGnpRxData rx_data = { .rxbuf = {0x00}, .timeout = FU_JABRA_GNP_EXTRA_LONG_RECEIVE_TIMEOUT, }; if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_rx_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&rx_data, error)) return FALSE; if (rx_data.rxbuf[5] != match_buf[5] || rx_data.rxbuf[6] != match_buf[6]) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "internal error, buf did not match"); return FALSE; } return TRUE; } static gboolean fu_jabra_gnp_device_write_crc(FuJabraGnpDevice *self, guint32 crc, guint total_chunks, guint preload_count, GError **error) { FuJabraGnpTxData tx_data = { .txbuf = { FU_JABRA_GNP_IFACE, self->address, 0x00, self->sequence_number, 0x8E, 0x0F, 0x19, }, .timeout = FU_JABRA_GNP_STANDARD_SEND_TIMEOUT, }; FuJabraGnpRxData rx_data = { .rxbuf = {0x00}, .timeout = FU_JABRA_GNP_STANDARD_RECEIVE_TIMEOUT, }; fu_memwrite_uint32(tx_data.txbuf + 7, crc, G_LITTLE_ENDIAN); fu_memwrite_uint16(tx_data.txbuf + 11, total_chunks, G_LITTLE_ENDIAN); fu_memwrite_uint16(tx_data.txbuf + 13, preload_count, G_LITTLE_ENDIAN); if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_tx_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&tx_data, error)) return FALSE; if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_rx_with_sequence_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&rx_data, error)) return FALSE; if (rx_data.rxbuf[5] != 0xFF) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "internal error: expected 0xFF, got 0x%02x 0x%02x", rx_data.rxbuf[5], rx_data.rxbuf[6]); return FALSE; } return TRUE; } static gboolean fu_jabra_gnp_device_write_chunk(FuJabraGnpDevice *self, guint32 chunk_number, const guint8 *buf, gsize bufsz, GError **error) { guint8 write_length = 0x00 + bufsz + 10; FuJabraGnpTxData tx_data = { .txbuf = { FU_JABRA_GNP_IFACE, self->address, 0x00, 0x00, write_length, 0x0F, 0x1A, }, .timeout = FU_JABRA_GNP_STANDARD_SEND_TIMEOUT, }; fu_memwrite_uint16(tx_data.txbuf + 7, chunk_number, G_LITTLE_ENDIAN); fu_memwrite_uint16(tx_data.txbuf + 9, bufsz, G_LITTLE_ENDIAN); if (!fu_memcpy_safe(tx_data.txbuf, sizeof(tx_data.txbuf), 11, buf, bufsz, 0x0, bufsz, error)) return FALSE; return fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_tx_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&tx_data, error); } static gboolean fu_jabra_gnp_device_write_chunks(FuJabraGnpDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { gboolean failed_chunk = FALSE; const guint8 match_buf[FU_JABRA_GNP_BUF_SIZE] = { FU_JABRA_GNP_IFACE, 0x00, self->address, 0x00, 0x06, 0x0F, 0x1B, }; FuJabraGnpRxData rx_data = { .rxbuf = {0x00}, .timeout = FU_JABRA_GNP_LONG_RECEIVE_TIMEOUT, }; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (gint chunk_number = 0; (guint)chunk_number < fu_chunk_array_length(chunks); chunk_number++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, chunk_number, error); if (chk == NULL) return FALSE; if (!fu_jabra_gnp_device_write_chunk(self, chunk_number, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; if (((chunk_number % FU_JABRA_GNP_PRELOAD_COUNT) == 0) || (guint)chunk_number == fu_chunk_array_length(chunks) - 1) { if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_rx_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&rx_data, error)) return FALSE; if (rx_data.rxbuf[5] != match_buf[5] || rx_data.rxbuf[6] != match_buf[6]) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "internal error, buf did not match"); return FALSE; } if (fu_memread_uint16(rx_data.rxbuf + 7, G_LITTLE_ENDIAN) != chunk_number) { chunk_number--; failed_chunk = TRUE; } else failed_chunk = FALSE; } if (!failed_chunk) fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_jabra_gnp_device_read_verify_status(FuJabraGnpDevice *self, GError **error) { const guint8 match_buf[FU_JABRA_GNP_BUF_SIZE] = {FU_JABRA_GNP_IFACE, 0x00, self->address, 0x00, 0x06, 0x0F, 0x1C}; FuJabraGnpRxData rx_data = { .rxbuf = {0x00}, .timeout = FU_JABRA_GNP_LONG_RECEIVE_TIMEOUT, }; if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_rx_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&rx_data, error)) return FALSE; if (rx_data.rxbuf[5] != match_buf[5] || rx_data.rxbuf[6] != match_buf[6]) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "internal error, buf did not match"); return FALSE; } return TRUE; } static gboolean fu_jabra_gnp_device_write_version(FuJabraGnpDevice *self, FuJabraGnpVersionData *version_data, GError **error) { FuJabraGnpTxData tx_data = { .txbuf = { FU_JABRA_GNP_IFACE, self->address, 0x00, self->sequence_number, 0x89, 0x0F, 0x1E, version_data->major, version_data->minor, version_data->micro, }, .timeout = FU_JABRA_GNP_STANDARD_SEND_TIMEOUT, }; FuJabraGnpRxData rx_data = { .rxbuf = {0x00}, .timeout = FU_JABRA_GNP_STANDARD_RECEIVE_TIMEOUT, }; if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_tx_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&tx_data, error)) return FALSE; if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_rx_with_sequence_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&rx_data, error)) return FALSE; if (rx_data.rxbuf[5] != 0xFF) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "internal error: expected 0xFF, got 0x%02x 0x%02x", rx_data.rxbuf[5], rx_data.rxbuf[6]); return FALSE; } return TRUE; } static gboolean fu_jabra_gnp_device_write_dfu_from_squif(FuJabraGnpDevice *self, GError **error) { FuJabraGnpTxData tx_data = { .txbuf = { FU_JABRA_GNP_IFACE, self->address, 0x00, self->sequence_number, 0x86, 0x0F, 0x1D, }, .timeout = FU_JABRA_GNP_STANDARD_SEND_TIMEOUT, }; FuJabraGnpRxData rx_data = { .rxbuf = {0x00}, .timeout = FU_JABRA_GNP_STANDARD_RECEIVE_TIMEOUT, }; if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_tx_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&tx_data, error)) return FALSE; if (!fu_device_retry_full(FU_DEVICE(self), fu_jabra_gnp_device_rx_with_sequence_cb, FU_JABRA_GNP_MAX_RETRIES, FU_JABRA_GNP_RETRY_DELAY, (gpointer)&rx_data, error)) return FALSE; if (rx_data.rxbuf[5] != 0xFF) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "internal error: expected 0xFF, got 0x%02x 0x%02x", rx_data.rxbuf[5], rx_data.rxbuf[6]); return FALSE; } return TRUE; } static FuFirmware * fu_jabra_gnp_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuJabraGnpDevice *self = FU_JABRA_GNP_DEVICE(device); g_autoptr(FuFirmware) firmware = fu_jabra_gnp_firmware_new(); /* unzip and get images */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; if (fu_jabra_gnp_firmware_get_dfu_pid(FU_JABRA_GNP_FIRMWARE(firmware)) != self->dfu_pid) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "wrong DFU PID, got 0x%x, expected 0x%x", fu_jabra_gnp_firmware_get_dfu_pid(FU_JABRA_GNP_FIRMWARE(firmware)), self->dfu_pid); return NULL; } return g_steal_pointer(&firmware); } static gboolean fu_jabra_gnp_device_probe(FuDevice *device, GError **error) { g_autoptr(GError) error_local = NULL; FuJabraGnpDevice *self = FU_JABRA_GNP_DEVICE(device); /* already set by parent */ if (self->address == FU_JABRA_GNP_ADDRESS_OTA_CHILD) return TRUE; self->iface_hid = _fu_usb_device_get_interface_for_class(FU_USB_DEVICE(self), FU_USB_CLASS_HID, &error_local); if (self->iface_hid == 0xFF) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot find HID interface: %s", error_local->message); return FALSE; } fu_usb_device_add_interface(FU_USB_DEVICE(self), self->iface_hid); return TRUE; } static gboolean fu_jabra_gnp_device_add_child(FuDevice *device, guint dfu_pid, GError **error) { FuJabraGnpDevice *self = FU_JABRA_GNP_DEVICE(device); g_autoptr(FuJabraGnpDevice) child = NULL; /* sanity check */ if (self->address != FU_JABRA_GNP_ADDRESS_PARENT) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "expected address 0x%x, and got 0x%x", (guint)FU_JABRA_GNP_ADDRESS_OTA_CHILD, self->address); return FALSE; } child = g_object_new(FU_TYPE_JABRA_GNP_DEVICE, "parent", FU_DEVICE(self), NULL); child->iface_hid = self->iface_hid; child->sequence_number = 0; child->address = FU_JABRA_GNP_ADDRESS_OTA_CHILD; child->dfu_pid = dfu_pid; /* prohibit to close parent's communication descriptor */ fu_device_add_private_flag(FU_DEVICE(child), FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_OPEN); fu_device_incorporate(FU_DEVICE(child), FU_DEVICE(self), FU_DEVICE_INCORPORATE_FLAG_PHYSICAL_ID); fu_device_set_logical_id(FU_DEVICE(child), "ota_device"); if (!fu_device_setup(FU_DEVICE(child), error)) { g_prefix_error(error, "failed to setup child device: "); return FALSE; } fu_device_add_instance_u16(FU_DEVICE(child), "VID", fu_device_get_vid(FU_DEVICE(self))); fu_device_add_instance_u16(FU_DEVICE(child), "PID", dfu_pid); if (!fu_device_build_instance_id_full(FU_DEVICE(child), FU_DEVICE_INSTANCE_FLAG_QUIRKS | FU_DEVICE_INSTANCE_FLAG_VISIBLE, error, "USB", "VID", "PID", NULL)) return FALSE; fu_device_add_child(FU_DEVICE(self), FU_DEVICE(child)); /* success */ return TRUE; } static gboolean fu_jabra_gnp_device_setup(FuDevice *device, GError **error) { FuJabraGnpDevice *self = FU_JABRA_GNP_DEVICE(device); g_autoptr(GError) error_local = NULL; if (!fu_jabra_gnp_device_read_name(self, error)) return FALSE; if (!fu_jabra_gnp_device_read_version(self, error)) return FALSE; if (!fu_jabra_gnp_device_read_dfu_pid(self, error)) return FALSE; if (self->address == FU_JABRA_GNP_ADDRESS_PARENT) { guint16 child_dfu_pid = 0; if (!fu_jabra_gnp_device_read_child_dfu_pid(self, &child_dfu_pid, &error_local)) { g_debug("unable to read child's PID, %s", error_local->message); return TRUE; } if (child_dfu_pid > 0x0) { if (!fu_jabra_gnp_device_add_child(FU_DEVICE(self), child_dfu_pid, error)) { g_prefix_error( error, "found child device with PID 0x%x, but failed to add as child " "of parent with PID 0x%x, unpair or turn off child device to " "update parent device: ", child_dfu_pid, self->dfu_pid); return FALSE; } } } if (self->address == FU_JABRA_GNP_ADDRESS_OTA_CHILD) { if (!fu_jabra_gnp_device_read_child_battery_level(self, error)) return FALSE; } return TRUE; } static gboolean fu_jabra_gnp_device_write_image(FuJabraGnpDevice *self, FuFirmware *firmware, FuFirmware *img, FuProgress *progress, GError **error) { const guint chunk_size = 52; g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(GInputStream) stream = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "write-partition"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 1, "start"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 5, "flash-erase-done"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 91, "write-chunks"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "read-verify-status"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "write-version"); /* write partition */ stream = fu_firmware_get_stream(img, error); if (stream == NULL) return FALSE; if (!fu_jabra_gnp_device_write_partition(self, fu_firmware_get_idx(img), error)) return FALSE; fu_progress_step_done(progress); /* start erasing */ if (!fu_jabra_gnp_device_start(self, error)) return FALSE; fu_progress_step_done(progress); /* poll for erase done */ if (!fu_jabra_gnp_device_flash_erase_done(self, error)) return FALSE; fu_progress_step_done(progress); /* write chunks */ chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, chunk_size, error); if (chunks == NULL) return FALSE; if (!fu_jabra_gnp_device_write_crc(self, fu_jabra_gnp_image_get_crc32(FU_JABRA_GNP_IMAGE(img)), fu_chunk_array_length(chunks), FU_JABRA_GNP_PRELOAD_COUNT, error)) return FALSE; if (!fu_jabra_gnp_device_write_chunks(self, chunks, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* verify */ if (!fu_jabra_gnp_device_read_verify_status(self, error)) return FALSE; fu_progress_step_done(progress); /* write version */ if (!fu_jabra_gnp_device_write_version( self, fu_jabra_gnp_firmware_get_version_data(FU_JABRA_GNP_FIRMWARE(firmware)), error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_jabra_gnp_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuJabraGnpDevice *self = FU_JABRA_GNP_DEVICE(device); g_autoptr(GPtrArray) imgs = fu_firmware_get_images(firmware); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_NO_PROFILE); for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); fu_progress_add_step(progress, FWUPD_STATUS_UNKNOWN, fu_firmware_get_size(img), fu_firmware_get_id(img)); } for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); if (!fu_jabra_gnp_device_write_image(self, firmware, img, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to write %s: ", fu_firmware_get_id(img)); return FALSE; } fu_progress_step_done(progress); } /* write squif */ if (!fu_jabra_gnp_device_write_dfu_from_squif(self, error)) return FALSE; return TRUE; } static gboolean fu_jabra_gnp_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuJabraGnpDevice *self = FU_JABRA_GNP_DEVICE(device); /* ota device needs some time to restart and reconnect */ if (self->address == FU_JABRA_GNP_ADDRESS_OTA_CHILD) { fu_device_sleep_full(FU_DEVICE(self), 45000, progress); fu_device_set_remove_delay(FU_DEVICE(self), 10000); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); } return TRUE; } static gboolean fu_jabra_gnp_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuJabraGnpDevice *self = FU_JABRA_GNP_DEVICE(device); if (g_strcmp0(key, "JabraGnpAddress") == 0) { guint64 val = 0; if (!fu_strtoull(value, &val, 0x0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->address = (guint8)val; return TRUE; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_jabra_gnp_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 5, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 75, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 5, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 15, "reload"); } static void fu_jabra_gnp_device_init(FuJabraGnpDevice *self) { self->address = 0x08; fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SELF_RECOVERY); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_COUNTERPART_GUIDS); fu_device_add_protocol(FU_DEVICE(self), "com.jabra.gnp"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_JABRA_GNP_FIRMWARE); } static void fu_jabra_gnp_device_class_init(FuJabraGnpDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_jabra_gnp_device_to_string; device_class->prepare_firmware = fu_jabra_gnp_device_prepare_firmware; device_class->probe = fu_jabra_gnp_device_probe; device_class->setup = fu_jabra_gnp_device_setup; device_class->write_firmware = fu_jabra_gnp_device_write_firmware; device_class->attach = fu_jabra_gnp_device_attach; device_class->set_quirk_kv = fu_jabra_gnp_device_set_quirk_kv; device_class->set_progress = fu_jabra_gnp_device_set_progress; } fwupd-2.0.10/plugins/jabra-gnp/fu-jabra-gnp-device.h000066400000000000000000000004411501337203100221120ustar00rootroot00000000000000/* * Copyright 2023 GN Audio A/S * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_JABRA_GNP_DEVICE (fu_jabra_gnp_device_get_type()) G_DECLARE_FINAL_TYPE(FuJabraGnpDevice, fu_jabra_gnp_device, FU, JABRA_GNP_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/jabra-gnp/fu-jabra-gnp-firmware.c000066400000000000000000000135431501337203100224710ustar00rootroot00000000000000/* * Copyright 2023 GN Audio A/S * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-jabra-gnp-firmware.h" #include "fu-jabra-gnp-image.h" struct _FuJabraGnpFirmware { FuArchiveFirmware parent_instance; guint16 dfu_pid; FuJabraGnpVersionData version_data; }; G_DEFINE_TYPE(FuJabraGnpFirmware, fu_jabra_gnp_firmware, FU_TYPE_FIRMWARE) static void fu_jabra_gnp_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuJabraGnpFirmware *self = FU_JABRA_GNP_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "dfu_pid", self->dfu_pid); } static gboolean fu_jabra_gnp_firmware_parse_version(FuJabraGnpFirmware *self, GError **error) { guint64 val = 0; g_auto(GStrv) split = NULL; split = g_strsplit(fu_firmware_get_version(FU_FIRMWARE(self)), ".", -1); if (g_strv_length(split) != 3) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "version invalid"); return FALSE; } if (!fu_strtoull(split[0], &val, 0x0, 0xFF, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->version_data.major = (guint8)val; if (!fu_strtoull(split[1], &val, 0x0, 0xFF, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->version_data.minor = (guint8)val; if (!fu_strtoull(split[2], &val, 0x0, 0xFF, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->version_data.micro = (guint8)val; /* success */ return TRUE; } static gboolean fu_jabra_gnp_firmware_parse_info(FuJabraGnpFirmware *self, XbSilo *silo, GError **error) { const gchar *version; const gchar *dfu_pid_str; guint64 val = 0; g_autoptr(XbNode) dfu_pid = NULL; g_autoptr(XbNode) build_vector = NULL; build_vector = xb_silo_query_first(silo, "buildVector", error); if (build_vector == NULL) { fwupd_error_convert(error); return FALSE; } /* only first? */ version = xb_node_get_attr(build_vector, "version"); if (version == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "buildVector version missing"); return FALSE; } fu_firmware_set_version(FU_FIRMWARE(self), version); if (!fu_jabra_gnp_firmware_parse_version(self, error)) return FALSE; dfu_pid = xb_silo_query_first(silo, "buildVector/targetUsbPids", error); if (dfu_pid == NULL) { fwupd_error_convert(error); return FALSE; } dfu_pid_str = xb_node_query_text(dfu_pid, "usbPid", error); if (dfu_pid_str == NULL) { fwupd_error_convert(error); return FALSE; } if (!fu_strtoull(dfu_pid_str, &val, 0x0, 0xFFFF, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "cannot parse usbPid of %s: ", dfu_pid_str); return FALSE; } self->dfu_pid = (guint16)val; /* success */ return TRUE; } static gboolean fu_jabra_gnp_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuJabraGnpFirmware *self = FU_JABRA_GNP_FIRMWARE(firmware); g_autoptr(FuFirmware) firmware_archive = fu_archive_firmware_new(); g_autoptr(FuFirmware) img_xml = NULL; g_autoptr(GBytes) img_blob = NULL; g_autoptr(GPtrArray) files = NULL; g_autoptr(XbBuilder) builder = xb_builder_new(); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_autoptr(XbSilo) silo = NULL; /* FuArchiveFirmware->parse */ fu_archive_firmware_set_format(FU_ARCHIVE_FIRMWARE(firmware_archive), FU_ARCHIVE_FORMAT_ZIP); fu_archive_firmware_set_compression(FU_ARCHIVE_FIRMWARE(firmware_archive), FU_ARCHIVE_COMPRESSION_NONE); if (!fu_firmware_parse_stream(firmware_archive, stream, 0x0, flags, error)) return FALSE; /* parse the XML metadata */ img_xml = fu_archive_firmware_get_image_fnmatch(FU_ARCHIVE_FIRMWARE(firmware_archive), "info.xml", error); if (img_xml == NULL) return FALSE; img_blob = fu_firmware_get_bytes(img_xml, error); if (img_blob == NULL) return FALSE; if (!xb_builder_source_load_bytes(source, img_blob, XB_BUILDER_SOURCE_FLAG_NONE, error)) { fwupd_error_convert(error); return FALSE; } xb_builder_import_source(builder, source); silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, error); if (silo == NULL) { fwupd_error_convert(error); return FALSE; } if (!fu_jabra_gnp_firmware_parse_info(self, silo, error)) return FALSE; files = xb_silo_query(silo, "buildVector/files/file", 0, error); if (files == NULL) { fwupd_error_convert(error); return FALSE; } for (guint i = 0; i < files->len; i++) { XbNode *n = g_ptr_array_index(files, i); g_autoptr(FuJabraGnpImage) img = fu_jabra_gnp_image_new(); g_autoptr(GError) error_local = NULL; if (!fu_jabra_gnp_image_parse(img, n, firmware_archive, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA)) { g_debug("ignoring image 0x%x: %s", i, error_local->message); continue; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } if (!fu_firmware_add_image_full(firmware, FU_FIRMWARE(img), error)) return FALSE; } /* success */ return TRUE; } guint16 fu_jabra_gnp_firmware_get_dfu_pid(FuJabraGnpFirmware *self) { g_return_val_if_fail(FU_IS_JABRA_GNP_FIRMWARE(self), G_MAXUINT16); return self->dfu_pid; } FuJabraGnpVersionData * fu_jabra_gnp_firmware_get_version_data(FuJabraGnpFirmware *self) { g_return_val_if_fail(FU_IS_JABRA_GNP_FIRMWARE(self), NULL); return &self->version_data; } static void fu_jabra_gnp_firmware_init(FuJabraGnpFirmware *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_VID_PID); fu_firmware_set_images_max(FU_FIRMWARE(self), 1024); } static void fu_jabra_gnp_firmware_class_init(FuJabraGnpFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_jabra_gnp_firmware_parse; firmware_class->export = fu_jabra_gnp_firmware_export; } FuFirmware * fu_jabra_gnp_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_JABRA_GNP_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/jabra-gnp/fu-jabra-gnp-firmware.h000066400000000000000000000011201501337203100224620ustar00rootroot00000000000000/* * Copyright 2023 GN Audio A/S * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_JABRA_GNP_FIRMWARE (fu_jabra_gnp_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuJabraGnpFirmware, fu_jabra_gnp_firmware, FU, JABRA_GNP_FIRMWARE, FuFirmware) typedef struct { guint8 major; guint8 minor; guint8 micro; } FuJabraGnpVersionData; FuFirmware * fu_jabra_gnp_firmware_new(void); guint16 fu_jabra_gnp_firmware_get_dfu_pid(FuJabraGnpFirmware *self); FuJabraGnpVersionData * fu_jabra_gnp_firmware_get_version_data(FuJabraGnpFirmware *self); fwupd-2.0.10/plugins/jabra-gnp/fu-jabra-gnp-image.c000066400000000000000000000071261501337203100217370ustar00rootroot00000000000000/* * Copyright 2023 GN Audio A/S * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-jabra-gnp-common.h" #include "fu-jabra-gnp-image.h" struct _FuJabraGnpImage { FuArchiveFirmware parent_instance; guint32 crc32; }; G_DEFINE_TYPE(FuJabraGnpImage, fu_jabra_gnp_image, FU_TYPE_FIRMWARE) static void fu_jabra_gnp_image_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuJabraGnpImage *self = FU_JABRA_GNP_IMAGE(firmware); fu_xmlb_builder_insert_kx(bn, "crc32", self->crc32); } gboolean fu_jabra_gnp_image_parse(FuJabraGnpImage *self, XbNode *n, FuFirmware *firmware_archive, GError **error) { const gchar *crc_str = NULL; const gchar *language; const gchar *name; const gchar *part_str = NULL; guint64 crc_expected = 0; guint64 partition = 0; g_autoptr(FuFirmware) img_archive = NULL; g_autoptr(GBytes) blob = NULL; /* only match on US English */ language = xb_node_query_text(n, "language", error); if (language == NULL) { fwupd_error_convert(error); return FALSE; } if (g_strcmp0(language, "English") != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "language was not 'English', got '%s'", language); return FALSE; } /* get the CRC */ crc_str = xb_node_query_text(n, "crc", error); if (crc_str == NULL) { fwupd_error_convert(error); return FALSE; } if (!fu_strtoull(crc_str, &crc_expected, 0x0, 0xFFFFFFFF, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "cannot parse crc of %s: ", crc_str); return FALSE; } /* get the partition number */ part_str = xb_node_query_text(n, "partition", NULL); if (part_str == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "partition missing"); return FALSE; } if (!fu_strtoull(part_str, &partition, 0x0, 0xFFFFFFFF, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "cannot parse partition of %s: ", part_str); return FALSE; } fu_firmware_set_idx(FU_FIRMWARE(self), partition); /* get the file pointed to by 'name' */ name = xb_node_get_attr(n, "name"); if (name == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "name missing"); return FALSE; } fu_firmware_set_id(FU_FIRMWARE(self), name); img_archive = fu_archive_firmware_get_image_fnmatch(FU_ARCHIVE_FIRMWARE(firmware_archive), name, error); if (img_archive == NULL) return FALSE; blob = fu_firmware_get_bytes(img_archive, error); if (blob == NULL) return FALSE; /* verify the CRC */ self->crc32 = fu_jabra_gnp_calculate_crc(blob); if (self->crc32 != (guint32)crc_expected) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "checksum invalid, got 0x%x, expected 0x%x", (guint)self->crc32, (guint)crc_expected); return FALSE; } /* success */ fu_firmware_set_bytes(FU_FIRMWARE(self), blob); return TRUE; } guint32 fu_jabra_gnp_image_get_crc32(FuJabraGnpImage *self) { g_return_val_if_fail(FU_IS_JABRA_GNP_IMAGE(self), G_MAXUINT32); return self->crc32; } static void fu_jabra_gnp_image_init(FuJabraGnpImage *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_NO_AUTO_DETECTION); } static void fu_jabra_gnp_image_class_init(FuJabraGnpImageClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->export = fu_jabra_gnp_image_export; } FuJabraGnpImage * fu_jabra_gnp_image_new(void) { return FU_JABRA_GNP_IMAGE(g_object_new(FU_TYPE_JABRA_GNP_IMAGE, NULL)); } fwupd-2.0.10/plugins/jabra-gnp/fu-jabra-gnp-image.h000066400000000000000000000010111501337203100217270ustar00rootroot00000000000000/* * Copyright 2023 GN Audio A/S * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_JABRA_GNP_IMAGE (fu_jabra_gnp_image_get_type()) G_DECLARE_FINAL_TYPE(FuJabraGnpImage, fu_jabra_gnp_image, FU, JABRA_GNP_IMAGE, FuFirmware) FuJabraGnpImage * fu_jabra_gnp_image_new(void); gboolean fu_jabra_gnp_image_parse(FuJabraGnpImage *self, XbNode *n, FuFirmware *firmware_archive, GError **error); guint32 fu_jabra_gnp_image_get_crc32(FuJabraGnpImage *self); fwupd-2.0.10/plugins/jabra-gnp/fu-jabra-gnp-plugin.c000066400000000000000000000020331501337203100221430ustar00rootroot00000000000000/* * Copyright 2023 GN Audio A/S * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-jabra-gnp-device.h" #include "fu-jabra-gnp-firmware.h" #include "fu-jabra-gnp-image.h" #include "fu-jabra-gnp-plugin.h" struct _FuJabraGnpPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuJabraGnpPlugin, fu_jabra_gnp_plugin, FU_TYPE_PLUGIN) static void fu_jabra_gnp_plugin_init(FuJabraGnpPlugin *self) { } static void fu_jabra_gnp_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "JabraGnpAddress"); fu_plugin_add_device_gtype(plugin, FU_TYPE_JABRA_GNP_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_JABRA_GNP_FIRMWARE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_JABRA_GNP_IMAGE); /* coverage */ } static void fu_jabra_gnp_plugin_class_init(FuJabraGnpPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_jabra_gnp_plugin_constructed; } fwupd-2.0.10/plugins/jabra-gnp/fu-jabra-gnp-plugin.h000066400000000000000000000003341501337203100221520ustar00rootroot00000000000000/* * Copyright 2023 GN Audio A/S * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuJabraGnpPlugin, fu_jabra_gnp_plugin, FU, JABRA_GNP_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/jabra-gnp/jabra-gnp.quirk000066400000000000000000000123201501337203100211500ustar00rootroot00000000000000# Jabra Evolve 65 SE Stereo [runtime] [USB\VID_0B0E&PID_24FC] Plugin = jabra_gnp # Jabra Evolve 65 SE Stereo [runtime] [USB\VID_0B0E&PID_24FD] Plugin = jabra_gnp # Jabra Evolve 65 SE Stereo [runtime] [USB\VID_0B0E&PID_24FE] Plugin = jabra_gnp # Jabra Evolve 75 SE [runtime] [USB\VID_0B0E&PID_2502] Plugin = jabra_gnp # Jabra Evolve 75 SE [runtime] [USB\VID_0B0E&PID_2503] Plugin = jabra_gnp # Jabra Evolve 75 SE [runtime] [USB\VID_0B0E&PID_2504] Plugin = jabra_gnp # Jabra Evolve 65e [runtime] [USB\VID_0B0E&PID_248E] Plugin = jabra_gnp # Jabra Evolve 65t [runtime] [USB\VID_0B0E&PID_2497] Plugin = jabra_gnp # Jabra Link 380 [runtime] [USB\VID_0B0E&PID_24C7] Plugin = jabra_gnp JabraGnpAddress = 0x01 # Jabra Link 380 [runtime] [USB\VID_0B0E&PID_24C8] Plugin = jabra_gnp JabraGnpAddress = 0x01 # Jabra Link 380 [runtime] [USB\VID_0B0E&PID_24C9] Plugin = jabra_gnp JabraGnpAddress = 0x01 # Jabra Link 380 [runtime] [USB\VID_0B0E&PID_24CA] Plugin = jabra_gnp JabraGnpAddress = 0x01 # Jabra Link 380 [runtime] [USB\VID_0B0E&PID_24E9] Plugin = jabra_gnp JabraGnpAddress = 0x01 # Jabra Link 390 [runtime] [USB\VID_0B0E&PID_2E50] Plugin = jabra_gnp JabraGnpAddress = 0x01 # Jabra Link 390 [runtime] [USB\VID_0B0E&PID_2E51] Plugin = jabra_gnp JabraGnpAddress = 0x01 # Jabra Link 390 [runtime] [USB\VID_0B0E&PID_2E56] Plugin = jabra_gnp JabraGnpAddress = 0x01 # Jabra Link 390 [runtime] [USB\VID_0B0E&PID_2E57] Plugin = jabra_gnp JabraGnpAddress = 0x01 # Jabra Evolve2 65 Mono [runtime] [USB\VID_0B0E&PID_24B3] Plugin = jabra_gnp # Jabra Evolve2 65 Mono [runtime] [USB\VID_0B0E&PID_24B4] Plugin = jabra_gnp # Jabra Evolve2 65 Mono [runtime] [USB\VID_0B0E&PID_24B5] Plugin = jabra_gnp # Jabra Evolve2 65 Mono [runtime] [USB\VID_0B0E&PID_24B6] Plugin = jabra_gnp # Jabra Evolve2 65 Stereo [runtime] [USB\VID_0B0E&PID_24B7] Plugin = jabra_gnp # Jabra Evolve2 65 Stereo [runtime] [USB\VID_0B0E&PID_24B8] Plugin = jabra_gnp # Jabra Evolve2 65 Stereo [runtime] [USB\VID_0B0E&PID_24A3] Plugin = jabra_gnp # Jabra Evolve2 65 Stereo [runtime] [USB\VID_0B0E&PID_24A4] Plugin = jabra_gnp # Jabra Evolve2 75 [runtime] [USB\VID_0B0E&PID_24D9] Plugin = jabra_gnp # Jabra Evolve2 75 [runtime] [USB\VID_0B0E&PID_24DA] Plugin = jabra_gnp # Jabra Evolve2 75 [runtime] [USB\VID_0B0E&PID_24DB] Plugin = jabra_gnp # Jabra Evolve2 85 [runtime] [USB\VID_0B0E&PID_24B9] Plugin = jabra_gnp # Jabra Evolve2 85 [runtime] [USB\VID_0B0E&PID_24BA] Plugin = jabra_gnp # Jabra Evolve2 85 [runtime] [USB\VID_0B0E&PID_24BB] Plugin = jabra_gnp # Jabra Evolve2 85 [runtime] [USB\VID_0B0E&PID_24BC] Plugin = jabra_gnp # Jabra Evolve2 75 [runtime] [USB\VID_0B0E&PID_24D9] Plugin = jabra_gnp # Jabra Speak2 40 [runtime] [USB\VID_0B0E&PID_AE6D] Plugin = jabra_gnp # Jabra Speak2 40 [runtime] [USB\VID_0B0E&PID_AE6B] Plugin = jabra_gnp # Jabra Speak2 55 [runtime] [USB\VID_0B0E&PID_AE6C] Plugin = jabra_gnp # Jabra Speak2 55 [runtime] [USB\VID_0B0E&PID_AE6A] Plugin = jabra_gnp # Jabra Speak2 55 [runtime] [USB\VID_0B0E&PID_AE70] Plugin = jabra_gnp # Jabra Speak2 75 [runtime] [USB\VID_0B0E&PID_24EF] Plugin = jabra_gnp # Jabra Speak2 75 [runtime] [USB\VID_0B0E&PID_24F0] Plugin = jabra_gnp # Jabra Speak2 75 [runtime] [USB\VID_0B0E&PID_24F1] Plugin = jabra_gnp # Jabra Evolve2 50 Mono [runtime] [USB\VID_0B0E&PID_2508] Plugin = jabra_gnp # Jabra Evolve2 50 Mono [runtime] [USB\VID_0B0E&PID_2509] Plugin = jabra_gnp # Jabra Evolve2 50 Mono [runtime] [USB\VID_0B0E&PID_250A] Plugin = jabra_gnp # Jabra Evolve2 50 Stereo [runtime] [USB\VID_0B0E&PID_2505] Plugin = jabra_gnp # Jabra Evolve2 50 Stereo [runtime] [USB\VID_0B0E&PID_2506] Plugin = jabra_gnp # Jabra Evolve2 50 Stereo [runtime] [USB\VID_0B0E&PID_2507] Plugin = jabra_gnp # Jabra Evolve2 55 Mono [runtime] [USB\VID_0B0E&PID_24F6] Plugin = jabra_gnp # Jabra Evolve2 55 Mono [runtime] [USB\VID_0B0E&PID_24F7] Plugin = jabra_gnp # Jabra Evolve2 55 Mono [runtime] [USB\VID_0B0E&PID_24F8] Plugin = jabra_gnp # Jabra Evolve2 55 Secure Stereo [runtime] [USB\VID_0B0E&PID_2512] Plugin = jabra_gnp # Jabra Evolve2 55 Secure Stereo [runtime] [USB\VID_0B0E&PID_2513] Plugin = jabra_gnp # Jabra Evolve2 55 Secure Stereo [runtime] [USB\VID_0B0E&PID_2514] Plugin = jabra_gnp # Jabra Evolve2 55 Stereo [runtime] [USB\VID_0B0E&PID_24F3] Plugin = jabra_gnp # Jabra Evolve2 55 Stereo [runtime] [USB\VID_0B0E&PID_24F4] Plugin = jabra_gnp # Jabra Evolve2 55 Stereo [runtime] [USB\VID_0B0E&PID_24F5] Plugin = jabra_gnp # Jabra Evolve2 65 Flex [runtime] [USB\VID_0B0E&PID_2517] Plugin = jabra_gnp # Jabra Evolve2 65 Flex [runtime] [USB\VID_0B0E&PID_2518] Plugin = jabra_gnp # Jabra Evolve2 65 Flex [runtime] [USB\VID_0B0E&PID_2519] Plugin = jabra_gnp # Jabra Evolve2 65 Flex Secure [runtime] [USB\VID_0B0E&PID_2523] Plugin = jabra_gnp # Jabra Evolve2 65 Flex Secure [runtime] [USB\VID_0B0E&PID_2524] Plugin = jabra_gnp # Jabra Evolve2 65 Flex Secure [runtime] [USB\VID_0B0E&PID_2525] Plugin = jabra_gnp # Jabra Link 390 [runtime] [USB\VID_0B0E&PID_2E50] Plugin = jabra_gnp JabraGnpAddress = 0x01 # Jabra Link 390 [runtime] [USB\VID_0B0E&PID_2E51] Plugin = jabra_gnp JabraGnpAddress = 0x01 # Jabra Link 390 [runtime] [USB\VID_0B0E&PID_2E56] Plugin = jabra_gnp JabraGnpAddress = 0x01 # Jabra Link 390 [runtime] [USB\VID_0B0E&PID_2E57] Plugin = jabra_gnp JabraGnpAddress = 0x01 fwupd-2.0.10/plugins/jabra-gnp/meson.build000066400000000000000000000010371501337203100203770ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginJabraGnp"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('jabra-gnp.quirk') plugin_builtins += static_library('fu_plugin_jabra_gnp', sources: [ 'fu-jabra-gnp-common.c', 'fu-jabra-gnp-device.c', 'fu-jabra-gnp-firmware.c', 'fu-jabra-gnp-image.c', 'fu-jabra-gnp-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files( 'tests/jabra-evolve2-75.json', ) fwupd-2.0.10/plugins/jabra-gnp/tests/000077500000000000000000000000001501337203100173765ustar00rootroot00000000000000fwupd-2.0.10/plugins/jabra-gnp/tests/jabra-evolve2-75.json000066400000000000000000000026731501337203100231710ustar00rootroot00000000000000{ "name": "Jabra Evolve2 75", "interactive": false, "steps": [ { "url": "335f10696cd57ae74b2115a9e44a39a2c5208e7e090d1f4e72bb91bc65a7480d-jabra-evolve2-75-1.8.13.cab", "components": [ { "version": "1.8.13", "protocol": "com.jabra.gnp", "guids": [ "354b39e6-e12e-557e-8beb-fa4128b0a98c", "dc636868-13f1-5c3b-bac5-c4432d447707", "b0870be4-7c69-50af-9b7d-fdfa8d849607" ] } ] }, { "url": "8fa6ff5d43c0cb5807f136c6edc90c146f3796b9228706ebed6f6348d9c358cd-jabra-evolve2-75-1.7.4.cab", "components": [ { "version": "1.7.4", "protocol": "com.jabra.gnp", "guids": [ "354b39e6-e12e-557e-8beb-fa4128b0a98c", "dc636868-13f1-5c3b-bac5-c4432d447707", "b0870be4-7c69-50af-9b7d-fdfa8d849607" ] } ] }, { "url": "acef8b8dc286659a6e783e49aaf55872d4ce6f0f0c50330452c5db756cb2d307-jabra-evolve2-75-1.11.7.cab", "emulation-url": "169609950e6182d1c0add7aae7b2627510b29651287312bcd6e924f76c167cf3-emulation.zip", "components": [ { "version": "1.11.7", "protocol": "com.jabra.gnp", "guids": [ "354b39e6-e12e-557e-8beb-fa4128b0a98c", "dc636868-13f1-5c3b-bac5-c4432d447707", "b0870be4-7c69-50af-9b7d-fdfa8d849607" ] } ] } ] } fwupd-2.0.10/plugins/jabra/000077500000000000000000000000001501337203100154525ustar00rootroot00000000000000fwupd-2.0.10/plugins/jabra/README.md000066400000000000000000000020661501337203100167350ustar00rootroot00000000000000--- title: Plugin: Jabra --- ## Introduction This plugin is used to detach Jabra Speak Gen 1 devices to DFU mode. ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_0B0E&PID_0412` ## Quirk Use This plugin uses the following plugin-specific quirks: ### JabraMagic Two magic bytes sent to detach into DFU mode. Since: 1.3.3 ## Update Behavior The device usually presents in runtime mode, but on detach re-enumerates with a different USB VID and PID in DFU APP mode. The device is then further detached by the `dfu` plugin. On DFU attach the device again re-enumerates back to the Jabra runtime mode. For this reason the `REPLUG_MATCH_GUID` internal device flag is used so that the bootloader and runtime modes are treated as the same device. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x0A12` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.3.3`. fwupd-2.0.10/plugins/jabra/fu-jabra-device.c000066400000000000000000000113651501337203100205500ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-jabra-device.h" struct _FuJabraDevice { FuUsbDevice parent_instance; gchar *magic; }; G_DEFINE_TYPE(FuJabraDevice, fu_jabra_device, FU_TYPE_USB_DEVICE) static void fu_jabra_device_to_string(FuDevice *device, guint idt, GString *str) { FuJabraDevice *self = FU_JABRA_DEVICE(device); fwupd_codec_string_append(str, idt, "Magic", self->magic); } static guint8 _fu_usb_device_get_interface_for_class(FuUsbDevice *usb_device, guint8 intf_class, GError **error) { g_autoptr(GPtrArray) intfs = NULL; intfs = fu_usb_device_get_interfaces(usb_device, error); if (intfs == NULL) return 0xff; for (guint i = 0; i < intfs->len; i++) { FuUsbInterface *intf = g_ptr_array_index(intfs, i); if (fu_usb_interface_get_class(intf) == intf_class) return fu_usb_interface_get_number(intf); } return 0xff; } /* slightly weirdly, this magic turns the device into appIDLE, so we * need the DFU plugin to further detach us into dfuIDLE */ static gboolean fu_jabra_device_prepare(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuJabraDevice *self = FU_JABRA_DEVICE(device); gsize magiclen = strlen(self->magic); guint8 adr = 0x00; guint8 rep = 0x00; guint8 iface_hid; guint8 buf[33] = {0x00}; g_autoptr(GError) error_local = NULL; /* parse string and create magic packet */ if (!fu_firmware_strparse_uint8_safe(self->magic, magiclen, 0, &rep, error)) return FALSE; if (!fu_firmware_strparse_uint8_safe(self->magic, magiclen, 2, &adr, error)) return FALSE; buf[0] = rep; buf[1] = adr; buf[2] = 0x00; buf[3] = 0x01; buf[4] = 0x85; buf[5] = 0x07; /* detach the HID interface from the kernel driver */ iface_hid = _fu_usb_device_get_interface_for_class(FU_USB_DEVICE(self), FU_USB_CLASS_HID, &error_local); if (iface_hid == 0xff) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot find HID interface: %s", error_local->message); return FALSE; } g_debug("claiming interface 0x%02x", iface_hid); if (!fu_usb_device_claim_interface(FU_USB_DEVICE(self), (gint)iface_hid, FU_USB_DEVICE_CLAIM_FLAG_KERNEL_DRIVER, &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot claim interface 0x%02x: %s", iface_hid, error_local->message); return FALSE; } /* send magic to device */ if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_CLASS, FU_USB_RECIPIENT_INTERFACE, 0x09, 0x0200 | rep, 0x0003, buf, 33, NULL, FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE, NULL, /* cancellable */ &error_local)) { g_debug("whilst sending magic: %s, ignoring", error_local->message); } /* wait for device to re-appear and be added to the dfu plugin */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_RESTART); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_jabra_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuJabraDevice *self = FU_JABRA_DEVICE(device); if (g_strcmp0(key, "JabraMagic") == 0) { if (value != NULL && strlen(value) == 4) { self->magic = g_strdup(value); return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "unsupported jabra quirk format"); return FALSE; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_jabra_device_init(FuJabraDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_COUNTERPART_GUIDS); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_set_remove_delay(FU_DEVICE(self), 20000); /* 10+10s! */ fu_device_add_protocol(FU_DEVICE(self), "org.usb.dfu"); } static void fu_jabra_device_finalize(GObject *object) { FuJabraDevice *self = FU_JABRA_DEVICE(object); g_free(self->magic); G_OBJECT_CLASS(fu_jabra_device_parent_class)->finalize(object); } static void fu_jabra_device_class_init(FuJabraDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->finalize = fu_jabra_device_finalize; device_class->to_string = fu_jabra_device_to_string; device_class->prepare = fu_jabra_device_prepare; device_class->set_quirk_kv = fu_jabra_device_set_quirk_kv; } fwupd-2.0.10/plugins/jabra/fu-jabra-device.h000066400000000000000000000004461501337203100205530ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_JABRA_DEVICE (fu_jabra_device_get_type()) G_DECLARE_FINAL_TYPE(FuJabraDevice, fu_jabra_device, FU, JABRA_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/jabra/fu-jabra-plugin.c000066400000000000000000000036401501337203100206040ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-jabra-device.h" #include "fu-jabra-plugin.h" struct _FuJabraPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuJabraPlugin, fu_jabra_plugin, FU_TYPE_PLUGIN) /* slightly weirdly, this takes us from appIDLE back into the actual * runtime mode where the device actually works */ static gboolean fu_jabra_plugin_cleanup(FuPlugin *plugin, FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GError) error_local = NULL; /* check for a property on the *dfu* FuDevice, which is also why we * can't just rely on using FuDevice->cleanup() */ if (!fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_ATTACH_EXTRA_RESET)) return TRUE; locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_RESTART); if (!fu_usb_device_reset(FU_USB_DEVICE(device), &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot reset USB device: %s [%i]", error_local->message, error_local->code); return FALSE; } /* wait for device to re-appear */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static void fu_jabra_plugin_init(FuJabraPlugin *self) { } static void fu_jabra_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "JabraMagic"); fu_plugin_add_device_gtype(plugin, FU_TYPE_JABRA_DEVICE); } static void fu_jabra_plugin_class_init(FuJabraPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_jabra_plugin_constructed; plugin_class->cleanup = fu_jabra_plugin_cleanup; } fwupd-2.0.10/plugins/jabra/fu-jabra-plugin.h000066400000000000000000000003511501337203100206050ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuJabraPlugin, fu_jabra_plugin, FU, JABRA_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/jabra/jabra.quirk000066400000000000000000000007371501337203100176150ustar00rootroot00000000000000# Jabra 410 [runtime] [USB\VID_0B0E&PID_0412] Plugin = jabra JabraMagic = 0201 CounterpartGuid = USB\VID_0B0E&PID_0411 # Jabra 510 [runtime] [USB\VID_0B0E&PID_0420] Plugin = jabra JabraMagic = 0201 CounterpartGuid = USB\VID_0B0E&PID_0421 # Jabra 710 [runtime] [USB\VID_0B0E&PID_2475] Plugin = jabra JabraMagic = 0508 CounterpartGuid = USB\VID_0B0E&PID_0982 # Jabra 810 [runtime] [USB\VID_0B0E&PID_2456] Plugin = jabra JabraMagic = 0508 CounterpartGuid = USB\VID_0B0E&PID_0971 fwupd-2.0.10/plugins/jabra/meson.build000066400000000000000000000007631501337203100176220ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginJabra"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('jabra.quirk') plugin_builtins += static_library('fu_plugin_jabra', sources: [ 'fu-jabra-plugin.c', 'fu-jabra-device.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files( 'tests/jabra-speak-410.json', 'tests/jabra-speak-510.json', 'tests/jabra-speak-710.json', ) fwupd-2.0.10/plugins/jabra/tests/000077500000000000000000000000001501337203100166145ustar00rootroot00000000000000fwupd-2.0.10/plugins/jabra/tests/jabra-speak-410.json000066400000000000000000000011131501337203100221650ustar00rootroot00000000000000{ "name": "Jabra Speak 410", "interactive": false, "steps": [ { "url": "eab97d7e745e372e435dbd76404c3929730ac082-Jabra-SPEAK_410-1.8.cab", "components": [ { "version": "1.8", "guids": [ "1764c519-4723-5514-baf9-3b42970de487" ] } ] }, { "url": "50a03efc5df333a948e159854ea40e1a3786c34c-Jabra-SPEAK_410-1.11.cab", "components": [ { "version": "1.11", "guids": [ "1764c519-4723-5514-baf9-3b42970de487" ] } ] } ] } fwupd-2.0.10/plugins/jabra/tests/jabra-speak-510.json000066400000000000000000000011151501337203100221700ustar00rootroot00000000000000{ "name": "Jabra Speak 510", "interactive": false, "steps": [ { "url": "45f88c50e79cfd30b6599df463463578d52f2fe9-Jabra-SPEAK_510-2.10.cab", "components": [ { "version": "2.10", "guids": [ "443b9b32-7603-5c3a-bb30-291a7d8d6dbd" ] } ] }, { "url": "c0523a98ef72508b5c7ddd687418b915ad5f4eb9-Jabra-SPEAK_510-2.14.cab", "components": [ { "version": "2.14", "guids": [ "443b9b32-7603-5c3a-bb30-291a7d8d6dbd" ] } ] } ] } fwupd-2.0.10/plugins/jabra/tests/jabra-speak-710.json000066400000000000000000000011151501337203100221720ustar00rootroot00000000000000{ "name": "Jabra Speak 710", "interactive": false, "steps": [ { "url": "d2910cdbc45cf172767d05e60d9e39a07a10d242-Jabra-SPEAK_710-1.10.cab", "components": [ { "version": "1.10", "guids": [ "0c503ad9-4969-5668-81e5-a3748682fc16" ] } ] }, { "url": "a5c627ae42de4e5c3ae3df28977f480624f96f66-Jabra-SPEAK_710-1.28.cab", "components": [ { "version": "1.28", "guids": [ "0c503ad9-4969-5668-81e5-a3748682fc16" ] } ] } ] } fwupd-2.0.10/plugins/kinetic-dp/000077500000000000000000000000001501337203100164225ustar00rootroot00000000000000fwupd-2.0.10/plugins/kinetic-dp/README.md000066400000000000000000000032071501337203100177030ustar00rootroot00000000000000--- title: Plugin: Kinetic DP --- ## Introduction This plugin supports updating FW for Kinetic DP converter chips. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in an unspecified binary file format. This plugin supports the following protocol ID: * com.kinet-ic.dp ## GUID Generation These devices use the standard DPAUX GUID values, e.g. * `DPAUX\OUI_0090CC24` (only-quirk) * `DPAUX\OUI_0090CC24&HWREV_10` (only-quirk) * `DPAUX\OUI_0090CC24&HWREV_10&DEVID_SYNAB2` (only-quirk) * `DPAUX\OUI_0090CC24&DEVID_SYNAB2` (only-quirk) These devices also use custom GUID values, e.g. * `MST\VEN_60AD&DEV_${dev_id}&CID_${customer_id}&CHW_${customer_board}` * `MST\VEN_60AD&DEV_${dev_id}&CID_${customer_id}` (only-quirk) * `MST\VEN_60AD&FAM_${family_id}` (only-quirk) ## Vendor ID Security The vendor ID is set from the PCI vendor, for example set to `DRM_DP_AUX_DEV:0x$(vid)` ## Requirements ### (Kernel) DP Aux Interface Kernel 4.6 introduced an DRM DP Aux interface for manipulation of the registers needed to access an DP hub. This patch can be backported to earlier kernels: ## Usage Supported devices will be displayed in `# fwupdmgr get-devices` output. ## External interface access This plugin requires read/write access to `/dev/drm_dp_aux*`. ## Version Considerations This plugin has been available since fwupd version `1.9.8`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Francis Lee @FrancisLeeKinetic fwupd-2.0.10/plugins/kinetic-dp/fu-kinetic-dp-device.c000066400000000000000000000167401501337203100224720ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * Copyright 2016 Mario Limonciello * Copyright 2021 Jeffrey Lin * Copyright 2022 Hai Su * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-kinetic-dp-device.h" typedef struct { FuKineticDpFamily family; FuKineticDpChip chip_id; FuKineticDpFwState fw_state; guint8 customer_id; guint8 customer_board; } FuKineticDpDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuKineticDpDevice, fu_kinetic_dp_device, FU_TYPE_DPAUX_DEVICE) #define GET_PRIVATE(o) (fu_kinetic_dp_device_get_instance_private(o)) static void fu_kinetic_dp_device_to_string(FuDevice *device, guint idt, GString *str) { FuKineticDpDevice *self = FU_KINETIC_DP_DEVICE(device); FuKineticDpDevicePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append(str, idt, "Family", fu_kinetic_dp_family_to_string(priv->family)); fwupd_codec_string_append(str, idt, "ChipId", fu_kinetic_dp_chip_to_string(priv->chip_id)); fwupd_codec_string_append(str, idt, "FwState", fu_kinetic_dp_fw_state_to_string(priv->fw_state)); fwupd_codec_string_append_hex(str, idt, "CustomerId", priv->customer_id); fwupd_codec_string_append_hex(str, idt, "CustomerBoard", priv->customer_board); } void fu_kinetic_dp_device_set_fw_state(FuKineticDpDevice *self, FuKineticDpFwState fw_state) { FuKineticDpDevicePrivate *priv = GET_PRIVATE(self); priv->fw_state = fw_state; } FuKineticDpFwState fu_kinetic_dp_device_get_fw_state(FuKineticDpDevice *self) { FuKineticDpDevicePrivate *priv = GET_PRIVATE(self); return priv->fw_state; } void fu_kinetic_dp_device_set_chip_id(FuKineticDpDevice *self, FuKineticDpChip chip_id) { FuKineticDpDevicePrivate *priv = GET_PRIVATE(self); priv->chip_id = chip_id; } static FuKineticDpFamily fu_kinetic_dp_device_chip_id_to_family(FuKineticDpChip chip_id) { if (chip_id == FU_KINETIC_DP_CHIP_PUMA_2900 || chip_id == FU_KINETIC_DP_CHIP_PUMA_2920) return FU_KINETIC_DP_FAMILY_PUMA; if (chip_id == FU_KINETIC_DP_CHIP_MUSTANG_5200) return FU_KINETIC_DP_FAMILY_MUSTANG; if (chip_id == FU_KINETIC_DP_CHIP_JAGUAR_5000) return FU_KINETIC_DP_FAMILY_JAGUAR; return FU_KINETIC_DP_FAMILY_UNKNOWN; } static const gchar * fu_kinetic_dp_device_get_name_for_chip_id(FuKineticDpChip chip_id) { if (chip_id == FU_KINETIC_DP_CHIP_JAGUAR_5000) return "KTM50X0"; if (chip_id == FU_KINETIC_DP_CHIP_MUSTANG_5200) return "KTM52X0"; if (chip_id == FU_KINETIC_DP_CHIP_PUMA_2900) return "MC2900"; return NULL; } gboolean fu_kinetic_dp_device_dpcd_read_oui(FuKineticDpDevice *self, guint8 *buf, gsize bufsz, GError **error) { if (bufsz < DPCD_SIZE_IEEE_OUI) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "aux dpcd read buffer size [0x%x] is too small to read IEEE OUI", (guint)bufsz); return FALSE; } if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), DPCD_ADDR_IEEE_OUI, buf, DPCD_SIZE_IEEE_OUI, FU_KINETIC_DP_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "aux dpcd read OUI failed: "); return FALSE; } return TRUE; } gboolean fu_kinetic_dp_device_dpcd_write_oui(FuKineticDpDevice *self, const guint8 *buf, GError **error) { if (!fu_dpaux_device_write(FU_DPAUX_DEVICE(self), DPCD_ADDR_IEEE_OUI, buf, DPCD_SIZE_IEEE_OUI, FU_KINETIC_DP_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "aux dpcd write OUI failed: "); return FALSE; } return TRUE; } static gboolean fu_kinetic_dp_device_ensure_customer(FuKineticDpDevice *self, GError **error) { FuKineticDpDevicePrivate *priv = GET_PRIVATE(self); /* board */ if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), DPCD_ADDR_CUSTOMER_BOARD, &priv->customer_board, sizeof(priv->customer_board), FU_KINETIC_DP_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "aux dpcd read customer board failed: "); return FALSE; } fu_device_add_instance_u8(FU_DEVICE(self), "CHW", priv->customer_board); /* id */ if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), DPCD_ADDR_CUSTOMER_ID, &priv->customer_id, sizeof(priv->customer_id), FU_KINETIC_DP_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "aux dpcd read customer ID failed: "); return FALSE; } fu_device_add_instance_u8(FU_DEVICE(self), "CID", priv->customer_id); if (!fu_device_build_instance_id_full(FU_DEVICE(self), FU_DEVICE_INSTANCE_FLAG_QUIRKS, error, "MST", "VEN", "DEV", "CID", NULL)) return FALSE; /* Kinetic EV board */ if (priv->customer_id == 0x0) { fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ENFORCE_REQUIRES); } /* success */ return fu_device_build_instance_id(FU_DEVICE(self), error, "MST", "VEN", "DEV", "CID", "CHW", NULL); } static gboolean fu_kinetic_dp_device_setup(FuDevice *device, GError **error) { FuKineticDpDevice *self = FU_KINETIC_DP_DEVICE(device); FuKineticDpDevicePrivate *priv = GET_PRIVATE(self); const gchar *chip_id_str; /* FuDpauxDevice->setup */ if (!FU_DEVICE_CLASS(fu_kinetic_dp_device_parent_class)->setup(device, error)) return FALSE; /* sanity check */ if (fu_dpaux_device_get_dpcd_ieee_oui(FU_DPAUX_DEVICE(device)) == 0x0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no IEEE OUI set"); return FALSE; } /* set up the device name */ chip_id_str = fu_kinetic_dp_device_get_name_for_chip_id(priv->chip_id); if (chip_id_str != NULL) fu_device_set_name(FU_DEVICE(self), chip_id_str); /* use the DPCD for the device */ fu_device_add_instance_u16(FU_DEVICE(self), "VEN", fu_dpaux_device_get_dpcd_ieee_oui(FU_DPAUX_DEVICE(device))); fu_device_add_instance_str(FU_DEVICE(self), "DEV", fu_dpaux_device_get_dpcd_dev_id(FU_DPAUX_DEVICE(device))); /* detect chip family */ priv->family = fu_kinetic_dp_device_chip_id_to_family(priv->chip_id); fu_device_add_instance_strup(FU_DEVICE(self), "FAM", fu_kinetic_dp_family_to_string(priv->family)); if (!fu_device_build_instance_id_full(FU_DEVICE(self), FU_DEVICE_INSTANCE_FLAG_QUIRKS, error, "MST", "VEN", "FAM", NULL)) return FALSE; /* read customer ID to get a more-specific GUID */ if (!fu_kinetic_dp_device_ensure_customer(self, error)) return FALSE; /* success */ return TRUE; } static void fu_kinetic_dp_device_class_init(FuKineticDpDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_kinetic_dp_device_setup; device_class->to_string = fu_kinetic_dp_device_to_string; } static void fu_kinetic_dp_device_init(FuKineticDpDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "com.kinet-ic.dp"); fu_device_set_vendor(FU_DEVICE(self), "Kinetic Technologies"); fu_device_build_vendor_id_u16(FU_DEVICE(self), "DRM_DP_AUX_DEV", 0x329A); fu_device_set_summary(FU_DEVICE(self), "DisplayPort Protocol Converter"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_VIDEO_DISPLAY); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); } fwupd-2.0.10/plugins/kinetic-dp/fu-kinetic-dp-device.h000066400000000000000000000032611501337203100224710ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2021 Jeffrey Lin * Copyright 2022 Hai Su * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-kinetic-dp-struct.h" struct _FuKineticDpDeviceClass { FuDpauxDeviceClass parent_class; }; #define FU_TYPE_KINETIC_DP_DEVICE (fu_kinetic_dp_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuKineticDpDevice, fu_kinetic_dp_device, FU, KINETIC_DP_DEVICE, FuDpauxDevice) /* OUI for KT */ #define MCA_OUI_BYTE_0 0x00 #define MCA_OUI_BYTE_1 0x60 #define MCA_OUI_BYTE_2 0xAD /* native DPCD fields defined in DP spec */ #define DPCD_ADDR_IEEE_OUI 0x00300 #define DPCD_SIZE_IEEE_OUI 3 #define DPCD_ADDR_BRANCH_DEV_ID_STR 0x00503 #define DPCD_ADDR_BRANCH_FW_SUB 0x00508 #define DPCD_ADDR_BRANCH_HW_REV 0x00509 #define DPCD_ADDR_BRANCH_FW_MAJ_REV 0x0050A #define DPCD_ADDR_BRANCH_FW_MIN_REV 0x0050B #define DPCD_ADDR_CUSTOMER_ID 0x00515 #define DPCD_ADDR_CUSTOMER_BOARD 0x0050F /* vendor-specific DPCD fields defined for Kinetic's usage */ #define DPCD_ADDR_BRANCH_FW_REV 0x0050C #define FU_KINETIC_DP_DEVICE_TIMEOUT 1000 void fu_kinetic_dp_device_set_fw_state(FuKineticDpDevice *self, FuKineticDpFwState fw_state); FuKineticDpFwState fu_kinetic_dp_device_get_fw_state(FuKineticDpDevice *self); void fu_kinetic_dp_device_set_chip_id(FuKineticDpDevice *self, FuKineticDpChip chip_id); gboolean fu_kinetic_dp_device_dpcd_read_oui(FuKineticDpDevice *self, guint8 *buf, gsize bufsz, GError **error); gboolean fu_kinetic_dp_device_dpcd_write_oui(FuKineticDpDevice *self, const guint8 *buf, GError **error); fwupd-2.0.10/plugins/kinetic-dp/fu-kinetic-dp-plugin.c000066400000000000000000000107741501337203100225320ustar00rootroot00000000000000/* * Copyright 2017 Mario Limonciello * Copyright 2017 Peichen Huang * Copyright 2017 Richard Hughes * Copyright 2022 Hai Su * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-kinetic-dp-plugin.h" #include "fu-kinetic-dp-puma-device.h" #include "fu-kinetic-dp-puma-firmware.h" #include "fu-kinetic-dp-secure-device.h" #include "fu-kinetic-dp-secure-firmware.h" #include "fu-kinetic-dp-struct.h" struct _FuKineticDpPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuKineticDpPlugin, fu_kinetic_dp_plugin, FU_TYPE_PLUGIN) static FuKineticDpDevice * fu_kinetic_dp_plugin_create_device(FuDpauxDevice *dpaux_device, GError **error) { FuKineticDpChip chip_id = 0; FuKineticDpFwState fw_state = 0; const gchar *dev_id = fu_dpaux_device_get_dpcd_dev_id(dpaux_device); g_autoptr(FuKineticDpDevice) dp_device = NULL; const struct { FuKineticDpChip chip_id; FuKineticDpFwState fw_state; const gchar *id_str; } map[] = {{FU_KINETIC_DP_CHIP_JAGUAR_5000, FU_KINETIC_DP_FW_STATE_IROM, "5010IR"}, {FU_KINETIC_DP_CHIP_JAGUAR_5000, FU_KINETIC_DP_FW_STATE_APP, "KT50X0"}, {FU_KINETIC_DP_CHIP_MUSTANG_5200, FU_KINETIC_DP_FW_STATE_IROM, "5210IR"}, {FU_KINETIC_DP_CHIP_MUSTANG_5200, FU_KINETIC_DP_FW_STATE_APP, "KT52X0"}, {FU_KINETIC_DP_CHIP_MUSTANG_5200, FU_KINETIC_DP_FW_STATE_APP, "KT5200"}, {FU_KINETIC_DP_CHIP_PUMA_2900, FU_KINETIC_DP_FW_STATE_IROM, "PUMA"}, {FU_KINETIC_DP_CHIP_PUMA_2900, FU_KINETIC_DP_FW_STATE_APP, "MC290"}, {FU_KINETIC_DP_CHIP_PUMA_2900, FU_KINETIC_DP_FW_STATE_APP, "MC2910"}}; g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* sanity check */ if (dev_id == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device has no DPCD device id"); return NULL; } /* find the device info by branch ID string */ for (guint32 i = 0; i < G_N_ELEMENTS(map); i++) { if (strncmp(dev_id, map[i].id_str, strlen(map[i].id_str)) == 0) { chip_id = map[i].chip_id; fw_state = map[i].fw_state; break; } } /* use the corresponding GType */ if (chip_id == FU_KINETIC_DP_CHIP_JAGUAR_5000 || chip_id == FU_KINETIC_DP_CHIP_MUSTANG_5200) { dp_device = FU_KINETIC_DP_DEVICE(g_object_new(FU_TYPE_KINETIC_DP_SECURE_DEVICE, NULL)); } else if (chip_id == FU_KINETIC_DP_CHIP_PUMA_2900) { dp_device = FU_KINETIC_DP_DEVICE(g_object_new(FU_TYPE_KINETIC_DP_PUMA_DEVICE, NULL)); } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "%s is not a supported Kinetic device", dev_id); return NULL; } fu_device_incorporate(FU_DEVICE(dp_device), FU_DEVICE(dpaux_device), FU_DEVICE_INCORPORATE_FLAG_ALL); fu_kinetic_dp_device_set_chip_id(dp_device, chip_id); fu_kinetic_dp_device_set_fw_state(dp_device, fw_state); return g_steal_pointer(&dp_device); } static gboolean fu_kinetic_dp_plugin_backend_device_added(FuPlugin *plugin, FuDevice *device, FuProgress *progress, GError **error) { FuKineticDpPlugin *self = FU_KINETIC_DP_PLUGIN(plugin); g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(FuKineticDpDevice) dev = NULL; /* check to see if this is device we care about? */ if (!FU_IS_DPAUX_DEVICE(device)) return TRUE; /* instantiate a new device */ dev = fu_kinetic_dp_plugin_create_device(FU_DPAUX_DEVICE(device), error); if (dev == NULL) return FALSE; locker = fu_device_locker_new(dev, error); if (locker == NULL) return FALSE; fu_plugin_device_add(FU_PLUGIN(self), FU_DEVICE(dev)); return TRUE; } static void fu_kinetic_dp_plugin_init(FuKineticDpPlugin *self) { } static void fu_kinetic_dp_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_udev_subsystem(plugin, "drm"); /* used for uevent only */ fu_plugin_add_device_udev_subsystem(plugin, "drm_dp_aux_dev"); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_KINETIC_DP_PUMA_FIRMWARE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_KINETIC_DP_SECURE_FIRMWARE); fu_plugin_add_device_gtype(plugin, FU_TYPE_KINETIC_DP_PUMA_DEVICE); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_KINETIC_DP_SECURE_DEVICE); /* coverage */ } static void fu_kinetic_dp_plugin_class_init(FuKineticDpPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_kinetic_dp_plugin_constructed; plugin_class->backend_device_added = fu_kinetic_dp_plugin_backend_device_added; } fwupd-2.0.10/plugins/kinetic-dp/fu-kinetic-dp-plugin.h000066400000000000000000000004431501337203100225270ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * Copyright 2022 Hai Su * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuKineticDpPlugin, fu_kinetic_dp_plugin, FU, KINETIC_DP_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/kinetic-dp/fu-kinetic-dp-puma-device.c000066400000000000000000000426041501337203100234300ustar00rootroot00000000000000/* * Copyright 2022 Hai Su * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-kinetic-dp-puma-device.h" #include "fu-kinetic-dp-puma-firmware.h" /* Kinetic proprietary DPCD fields for Puma in both application and ISP driver */ #define PUMA_DPCD_SINK_MODE_REG 0x0050D #define PUMA_DPCD_CMD_STATUS_REG 0x0050E #define PUMA_DPCD_DATA_ADDR 0x80000ul #define PUMA_DPCD_DATA_SIZE 0x8000ul /* 0x80000ul ~ 0x87FFF, 32 KB*/ #define PUMA_DPCD_DATA_ADDR_END (PUMA_DPCD_DATA_ADDR + PUMA_DPCD_DATA_SIZE - 1) #define PUMA_CHUNK_PROCESS_MAX_WAIT 10000 /* max wait time in ms to process 32KB chunk */ #define FU_KINETIC_DP_PUMA_REQUEST_FLASH_ERASE_TIME 2 /* typical erase time, ms */ #define POLL_INTERVAL_MS 20 /* check the status of installing FW images */ struct _FuKineticDpPumaDevice { FuKineticDpDevice parent_instance; guint16 read_flash_prog_time; guint16 flash_id; guint16 flash_size; }; G_DEFINE_TYPE(FuKineticDpPumaDevice, fu_kinetic_dp_puma_device, FU_TYPE_KINETIC_DP_DEVICE) static void fu_kinetic_dp_puma_device_to_string(FuDevice *device, guint idt, GString *str) { FuKineticDpPumaDevice *self = FU_KINETIC_DP_PUMA_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "ReadFlashProgTime", self->read_flash_prog_time); fwupd_codec_string_append_hex(str, idt, "FlashId", self->flash_id); fwupd_codec_string_append_hex(str, idt, "FlashSize", self->flash_size); } static gboolean fu_kinetic_dp_puma_device_wait_dpcd_cmd_status_cb(FuDevice *device, gpointer user_data, GError **error) { FuKineticDpPumaDevice *self = FU_KINETIC_DP_PUMA_DEVICE(device); guint8 status = 0; guint8 status_want = GPOINTER_TO_UINT(user_data); if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), PUMA_DPCD_CMD_STATUS_REG, &status, sizeof(status), FU_KINETIC_DP_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "failed to read PUMA_DPCD_CMD_STATUS_REG for status: "); return FALSE; } if (status != status_want) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "flash mode was %s, wanted %s", fu_kinetic_dp_puma_mode_to_string(status), fu_kinetic_dp_puma_mode_to_string(status_want)); return FALSE; } /* success */ return TRUE; } static gboolean fu_kinetic_dp_puma_device_wait_dpcd_sink_mode_cb(FuDevice *device, gpointer user_data, GError **error) { FuKineticDpPumaDevice *self = FU_KINETIC_DP_PUMA_DEVICE(device); guint8 status = 0; guint8 status_want = GPOINTER_TO_UINT(user_data); if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), PUMA_DPCD_SINK_MODE_REG, &status, sizeof(status), FU_KINETIC_DP_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "failed to read PUMA_DPCD_SINK_MODE_REG for status: "); return FALSE; } if (status != status_want) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "flash mode was %s, wanted %s", fu_kinetic_dp_puma_mode_to_string(status), fu_kinetic_dp_puma_mode_to_string(status_want)); return FALSE; } /* success */ return TRUE; } static gboolean fu_kinetic_dp_puma_device_enter_code_loading_mode(FuKineticDpPumaDevice *self, GError **error) { guint8 cmd = FU_KINETIC_DP_PUMA_REQUEST_CODE_LOAD_REQUEST; /* send cmd */ if (!fu_dpaux_device_write(FU_DPAUX_DEVICE(self), PUMA_DPCD_SINK_MODE_REG, &cmd, sizeof(cmd), FU_KINETIC_DP_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "failed to write PUMA_DPCD_SINK_MODE_REG with " "CODE_LOAD_REQUEST: "); return FALSE; } /* wait for the command to be processed */ if (!fu_device_retry_full(FU_DEVICE(self), fu_kinetic_dp_puma_device_wait_dpcd_sink_mode_cb, 5, POLL_INTERVAL_MS, GUINT_TO_POINTER(FU_KINETIC_DP_PUMA_REQUEST_CODE_LOAD_READY), error)) { g_prefix_error(error, "timeout waiting for REQUEST_FW_UPDATE_READY: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_kinetic_dp_puma_device_send_chunk(FuKineticDpPumaDevice *self, FuIOChannel *io_channel, GBytes *fw, GError **error) { g_autoptr(FuChunkArray) chunks = fu_chunk_array_new_from_bytes(fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 16); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_dpaux_device_write(FU_DPAUX_DEVICE(self), PUMA_DPCD_DATA_ADDR + fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), FU_KINETIC_DP_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "failed at 0x%x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } } /* success */ return TRUE; } static gboolean fu_kinetic_dp_puma_device_send_payload(FuKineticDpPumaDevice *self, FuIOChannel *io_channel, GBytes *fw, FuProgress *progress, guint32 wait_time_ms, gboolean ignore_error, GError **error) { g_autoptr(FuChunkArray) chunks = fu_chunk_array_new_from_bytes(fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, PUMA_DPCD_DATA_SIZE); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GBytes) chk_blob = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; /* send a maximum 32KB chunk of payload to AUX window */ chk_blob = fu_chunk_get_bytes(chk); if (!fu_kinetic_dp_puma_device_send_chunk(self, io_channel, chk_blob, error)) { g_prefix_error(error, "failed to AUX write at 0x%x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } /* check if data chunk received */ if (!fu_device_retry_full(FU_DEVICE(self), fu_kinetic_dp_puma_device_wait_dpcd_cmd_status_cb, wait_time_ms / POLL_INTERVAL_MS, POLL_INTERVAL_MS, GUINT_TO_POINTER(FU_KINETIC_DP_PUMA_MODE_CHUNK_PROCESSED), error)) { g_prefix_error(error, "timeout waiting for MODE_CHUNK_PROCESSED: "); return FALSE; } fu_progress_step_done(progress); } return TRUE; } static gboolean fu_kinetic_dp_puma_device_wait_drv_ready(FuKineticDpPumaDevice *self, FuIOChannel *io_channel, GError **error) { guint8 flashinfo[FU_STRUCT_KINETIC_DP_FLASH_INFO_SIZE] = {0}; g_autoptr(GByteArray) st = NULL; self->flash_id = 0; self->flash_size = 0; self->read_flash_prog_time = 10; g_debug("wait for isp driver ready..."); /* wait for the command to be processed */ if (!fu_device_retry_full(FU_DEVICE(self), fu_kinetic_dp_puma_device_wait_dpcd_sink_mode_cb, 20, POLL_INTERVAL_MS, GUINT_TO_POINTER(FU_KINETIC_DP_PUMA_REQUEST_CODE_BOOTUP_DONE), error)) { g_prefix_error(error, "timeout waiting for REQUEST_FW_UPDATE_READY: "); return FALSE; } if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), PUMA_DPCD_DATA_ADDR, flashinfo, sizeof(flashinfo), FU_KINETIC_DP_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "failed to read Flash Info from Isp Driver: "); return FALSE; } st = fu_struct_kinetic_dp_flash_info_parse(flashinfo, sizeof(flashinfo), 0x0, error); self->flash_id = fu_struct_kinetic_dp_flash_info_get_id(st); self->flash_size = fu_struct_kinetic_dp_flash_info_get_size(st); self->read_flash_prog_time = fu_struct_kinetic_dp_flash_info_get_erase_time(st); if (self->read_flash_prog_time == 0) self->read_flash_prog_time = FU_KINETIC_DP_PUMA_REQUEST_FLASH_ERASE_TIME; return TRUE; } static gboolean fu_kinetic_dp_puma_device_send_isp_drv(FuKineticDpPumaDevice *self, GBytes *fw, FuProgress *progress, GError **error) { FuIOChannel *io_channel = fu_udev_device_get_io_channel(FU_UDEV_DEVICE(self)); if (!fu_kinetic_dp_puma_device_enter_code_loading_mode(self, error)) { g_prefix_error(error, "enter code loading mode failed: "); return FALSE; } fu_kinetic_dp_puma_device_send_payload(self, io_channel, fw, progress, PUMA_CHUNK_PROCESS_MAX_WAIT, TRUE, error); if (!fu_kinetic_dp_puma_device_wait_drv_ready(self, io_channel, error)) { g_prefix_error(error, "wait for ISP driver ready failed: "); return FALSE; } if (self->flash_size >= 0x400) fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); if (self->flash_size == 0) { if (self->flash_id > 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "SPI flash not supported"); return FALSE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "SPI flash not connected"); return FALSE; } return TRUE; } static gboolean fu_kinetic_dp_puma_device_enable_fw_update_mode(FuKineticDpPumaDevice *self, FuKineticDpPumaFirmware *firmware, GError **error) { guint8 cmd; /* send cmd */ cmd = FU_KINETIC_DP_PUMA_REQUEST_FW_UPDATE_REQUEST; if (!fu_dpaux_device_write(FU_DPAUX_DEVICE(self), PUMA_DPCD_SINK_MODE_REG, &cmd, sizeof(cmd), FU_KINETIC_DP_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "failed to write PUMA_DPCD_SINK_MODE_REG with FW_UPDATE_REQUEST: "); return FALSE; } if (fu_kinetic_dp_device_get_fw_state(FU_KINETIC_DP_DEVICE(self)) == FU_KINETIC_DP_FW_STATE_APP) { guint8 flashinfo[FU_STRUCT_KINETIC_DP_FLASH_INFO_SIZE] = {0}; g_autoptr(GByteArray) st = NULL; /* Puma takes about 18ms (Winbond EF13) to get ISP driver ready for flash info */ fu_device_sleep(FU_DEVICE(self), 18); if (!fu_device_retry_full( FU_DEVICE(self), fu_kinetic_dp_puma_device_wait_dpcd_cmd_status_cb, 150, POLL_INTERVAL_MS, GUINT_TO_POINTER(FU_KINETIC_DP_PUMA_MODE_FLASH_INFO_READY), error)) { g_prefix_error(error, "timeout waiting for MODE_FLASH_INFO_READY: "); return FALSE; } /* get flash info */ if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), PUMA_DPCD_DATA_ADDR, flashinfo, sizeof(flashinfo), FU_KINETIC_DP_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "failed to read Flash Info: "); return FALSE; } st = fu_struct_kinetic_dp_flash_info_parse(flashinfo, sizeof(flashinfo), 0x0, error); self->flash_id = fu_struct_kinetic_dp_flash_info_get_id(st); self->flash_size = fu_struct_kinetic_dp_flash_info_get_size(st); self->read_flash_prog_time = fu_struct_kinetic_dp_flash_info_get_erase_time(st); /* save flash info need to do memcopy copy here */ if (self->flash_size == 0) { if (self->flash_id > 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "SPI flash not supported"); return FALSE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "SPI flash not connected"); return FALSE; } if (self->flash_size >= 0x400) fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); } /* checking for flash erase done */ g_debug("waiting for flash erasing..."); if (self->read_flash_prog_time) fu_device_sleep(FU_DEVICE(self), self->read_flash_prog_time); else fu_device_sleep(FU_DEVICE(self), FU_KINETIC_DP_PUMA_REQUEST_FLASH_ERASE_TIME); if (!fu_device_retry_full(FU_DEVICE(self), fu_kinetic_dp_puma_device_wait_dpcd_sink_mode_cb, 150, POLL_INTERVAL_MS, GUINT_TO_POINTER(FU_KINETIC_DP_PUMA_REQUEST_FW_UPDATE_READY), error)) { g_prefix_error(error, "timeout waiting for REQUEST_FW_UPDATE_READY: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_kinetic_dp_puma_device_setup(FuDevice *device, GError **error) { FuKineticDpPumaDevice *self = FU_KINETIC_DP_PUMA_DEVICE(device); guint8 dpcd_buf[3] = {0}; g_autofree gchar *version = NULL; /* FuKineticDpDevice->setup */ if (!FU_DEVICE_CLASS(fu_kinetic_dp_puma_device_parent_class)->setup(device, error)) return FALSE; /* read major and minor version */ if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), DPCD_ADDR_BRANCH_FW_MAJ_REV, dpcd_buf, 2, FU_KINETIC_DP_DEVICE_TIMEOUT, error)) return FALSE; /* read sub */ if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), DPCD_ADDR_BRANCH_FW_SUB, dpcd_buf + 2, 1, FU_KINETIC_DP_DEVICE_TIMEOUT, error)) return FALSE; version = g_strdup_printf("%1d.%03d.%02d", dpcd_buf[0], dpcd_buf[1], dpcd_buf[2]); fu_device_set_version(device, version); /* success */ return TRUE; } static gboolean fu_kinetic_dp_puma_device_prepare(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { guint8 mca_oui[DPCD_SIZE_IEEE_OUI] = {MCA_OUI_BYTE_0, MCA_OUI_BYTE_1, MCA_OUI_BYTE_2}; return fu_kinetic_dp_device_dpcd_write_oui(FU_KINETIC_DP_DEVICE(device), mca_oui, error); } static gboolean fu_kinetic_dp_puma_device_cleanup(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuKineticDpPumaDevice *self = FU_KINETIC_DP_PUMA_DEVICE(device); guint8 cmd = FU_KINETIC_DP_PUMA_REQUEST_CHIP_RESET_REQUEST; fu_device_sleep(FU_DEVICE(self), 3000); if (!fu_dpaux_device_write(FU_DPAUX_DEVICE(self), PUMA_DPCD_SINK_MODE_REG, &cmd, sizeof(cmd), FU_KINETIC_DP_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "failed to write PUMA_DPCD_SINK_MODE_REG with CHIP_RESET_REQUEST: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_kinetic_dp_puma_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuKineticDpPumaDevice *self = FU_KINETIC_DP_PUMA_DEVICE(device); FuKineticDpPumaFirmware *dp_firmware = FU_KINETIC_DP_PUMA_FIRMWARE(firmware); FuIOChannel *io_channel = fu_udev_device_get_io_channel(FU_UDEV_DEVICE(self)); g_autoptr(GBytes) app_fw_blob = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 90, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 5, NULL); /* only load driver if in IROM mode */ if (fu_kinetic_dp_device_get_fw_state(FU_KINETIC_DP_DEVICE(device)) != FU_KINETIC_DP_FW_STATE_APP) { g_autoptr(GBytes) isp_drv_blob = NULL; /* get image of ISP driver */ isp_drv_blob = fu_firmware_get_image_by_idx_bytes(firmware, FU_KINETIC_DP_FIRMWARE_IDX_ISP_DRV, error); if (isp_drv_blob == NULL) return FALSE; if (g_bytes_get_size(isp_drv_blob) > 0) { g_debug("loading isp driver because in IROM mode"); if (!fu_kinetic_dp_puma_device_send_isp_drv(self, isp_drv_blob, fu_progress_get_child(progress), error)) return FALSE; } } fu_progress_step_done(progress); /* enable FW update mode */ if (!fu_kinetic_dp_puma_device_enable_fw_update_mode(self, dp_firmware, error)) return FALSE; fu_progress_step_done(progress); /* send App FW image */ app_fw_blob = fu_firmware_get_image_by_idx_bytes(firmware, FU_KINETIC_DP_FIRMWARE_IDX_APP_FW, error); if (app_fw_blob == NULL) return FALSE; if (!fu_kinetic_dp_puma_device_send_payload(self, io_channel, app_fw_blob, fu_progress_get_child(progress), PUMA_CHUNK_PROCESS_MAX_WAIT, FALSE, error)) { g_prefix_error(error, "sending App Firmware payload failed: "); return FALSE; } fu_progress_step_done(progress); /* validate firmware image in Puma */ fu_device_sleep(FU_DEVICE(self), 100); if (!fu_device_retry_full(FU_DEVICE(self), fu_kinetic_dp_puma_device_wait_dpcd_sink_mode_cb, 100, POLL_INTERVAL_MS, GUINT_TO_POINTER(FU_KINETIC_DP_PUMA_REQUEST_FW_UPDATE_DONE), error)) { g_prefix_error(error, "validating App Firmware failed: "); return FALSE; } fu_progress_step_done(progress); /* success */ return TRUE; } static void fu_kinetic_dp_puma_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_kinetic_dp_puma_device_init(FuKineticDpPumaDevice *self) { self->read_flash_prog_time = 10; fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_KINETIC_DP_PUMA_FIRMWARE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); } static void fu_kinetic_dp_puma_device_class_init(FuKineticDpPumaDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_kinetic_dp_puma_device_to_string; device_class->setup = fu_kinetic_dp_puma_device_setup; device_class->prepare = fu_kinetic_dp_puma_device_prepare; device_class->cleanup = fu_kinetic_dp_puma_device_cleanup; device_class->write_firmware = fu_kinetic_dp_puma_device_write_firmware; device_class->set_progress = fu_kinetic_dp_puma_device_set_progress; } fwupd-2.0.10/plugins/kinetic-dp/fu-kinetic-dp-puma-device.h000066400000000000000000000005661501337203100234360ustar00rootroot00000000000000/* * Copyright 2022 Hai Su * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-kinetic-dp-device.h" #define FU_TYPE_KINETIC_DP_PUMA_DEVICE (fu_kinetic_dp_puma_device_get_type()) G_DECLARE_FINAL_TYPE(FuKineticDpPumaDevice, fu_kinetic_dp_puma_device, FU, KINETIC_DP_PUMA_DEVICE, FuKineticDpDevice) fwupd-2.0.10/plugins/kinetic-dp/fu-kinetic-dp-puma-firmware.c000066400000000000000000000224051501337203100240020ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * Copyright 2021 Jeffrey Lin * Copyright 2022 Hai Su * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-kinetic-dp-puma-device.h" #include "fu-kinetic-dp-puma-firmware.h" #include "fu-kinetic-dp-secure-device.h" struct _FuKineticDpPumaFirmware { FuFirmwareClass parent_instance; }; typedef struct { FuKineticDpChip chip_id; guint16 cmdb_version; guint32 cmdb_revision; } FuKineticDpPumaFirmwarePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuKineticDpPumaFirmware, fu_kinetic_dp_puma_firmware, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_kinetic_dp_puma_firmware_get_instance_private(o)) #define HEADER_LEN_ISP_DRV_SIZE 4 #define APP_ID_STR_LEN 4 #define FU_KINETIC_DP_PUMA_REQUEST_FW_HEADER_SIZE 50 #define FU_KINETIC_DP_PUMA_REQUEST_FW_HASH_SIZE 32 #define PUMA_STS_FW_PAYLOAD_SIZE \ ((512 * 1024) + FU_KINETIC_DP_PUMA_REQUEST_FW_HEADER_SIZE + \ (FU_KINETIC_DP_PUMA_REQUEST_FW_HASH_SIZE * 2)) /* Puma STD F/W SPI mapping */ #define FU_KINETIC_DP_PUMA_REQUEST_FW_STD_VER_START_ADDR (PUMA_STS_FW_PAYLOAD_SIZE - 52) /*0x8003E*/ /* Puma STD F/W CMDB */ #define FU_KINETIC_DP_PUMA_REQUEST_CMDB_SIZE 128 #define FU_KINETIC_DP_PUMA_REQUEST_FW_CMDB_SIG_SIZE 4 #define FU_KINETIC_DP_PUMA_REQUEST_FW_CMDB_START_ADDR 0x7FE52 #define FU_KINETIC_DP_PUMA_REQUEST_FW_CMDB_STD_VER_ADDR 0x7FE56 #define FU_KINETIC_DP_PUMA_REQUEST_FW_CMDB_REV_ADDR 0x7FE58 #define FU_KINETIC_DP_PUMA_REQUEST_FW_CMDB_REV_SIZE 3 static void fu_kinetic_dp_puma_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuKineticDpPumaFirmware *self = FU_KINETIC_DP_PUMA_FIRMWARE(firmware); FuKineticDpPumaFirmwarePrivate *priv = GET_PRIVATE(self); fu_xmlb_builder_insert_kv(bn, "chip_id", fu_kinetic_dp_chip_to_string(priv->chip_id)); fu_xmlb_builder_insert_kx(bn, "cmdb_version", priv->cmdb_version); fu_xmlb_builder_insert_kx(bn, "cmdb_revision", priv->cmdb_revision); } static gboolean fu_kinetic_dp_puma_firmware_parse_chip_id(GInputStream *stream, FuKineticDpChip *chip_id, GError **error) { const struct { FuKineticDpChip chip_id; guint32 offset; const gchar *app_id; } map[] = { {FU_KINETIC_DP_CHIP_PUMA_2900, 0x080042UL, "PUMA"} /* Puma 512KB */ }; for (guint32 i = 0; i < G_N_ELEMENTS(map); i++) { guint8 buf[4] = {APP_ID_STR_LEN}; if (!fu_input_stream_read_safe(stream, buf, sizeof(buf), 0x0, map[i].offset, sizeof(buf), error)) return FALSE; if (memcmp(buf, (const guint8 *)map[i].app_id, sizeof(buf)) == 0) { *chip_id = map[i].chip_id; return TRUE; } } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no valid Chip ID is found in the firmware"); return FALSE; } static gboolean fu_kinetic_dp_puma_firmware_parse_app_fw(FuKineticDpPumaFirmware *self, GInputStream *stream, GError **error) { FuKineticDpPumaFirmwarePrivate *priv = GET_PRIVATE(self); gsize streamsz = 0; gsize offset = 0; guint32 checksum = 0; guint32 code_size = 0; guint32 std_fw_ver = 0; guint8 tmpbuf = 0; guint8 checksum_actual; guint8 cmdb_sig[FU_KINETIC_DP_PUMA_REQUEST_FW_CMDB_SIG_SIZE] = {'P', 'M', 'D', 'B'}; g_autoptr(GByteArray) cmdb_tmp = NULL; g_autoptr(GByteArray) st = NULL; /* sanity check */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz < 512 * 1024) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "firmware payload size (0x%x) is not valid", (guint)streamsz); return FALSE; } /* calculate code size */ st = fu_struct_kinetic_dp_puma_header_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; offset += st->len; code_size += FU_STRUCT_KINETIC_DP_PUMA_HEADER_SIZE; for (guint i = 0; i < FU_STRUCT_KINETIC_DP_PUMA_HEADER_DEFAULT_OBJECT_COUNT; i++) { g_autoptr(GByteArray) st_obj = fu_struct_kinetic_dp_puma_header_info_parse_stream(stream, offset, error); if (st_obj == NULL) return FALSE; code_size += fu_struct_kinetic_dp_puma_header_info_get_length(st_obj) + FU_STRUCT_KINETIC_DP_PUMA_HEADER_INFO_SIZE; offset += st_obj->len; } if (code_size < (512 * 1024) + offset) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid firmware -- file size 0x%x is not reasonable", code_size); return FALSE; } /* get STD F/W version */ if (!fu_input_stream_read_u8(stream, FU_KINETIC_DP_PUMA_REQUEST_FW_STD_VER_START_ADDR, &tmpbuf, error)) return FALSE; std_fw_ver += (guint32)tmpbuf << 8; if (!fu_input_stream_read_u8(stream, FU_KINETIC_DP_PUMA_REQUEST_FW_STD_VER_START_ADDR + 1, &tmpbuf, error)) return FALSE; std_fw_ver += (guint32)tmpbuf << 16; if (!fu_input_stream_read_u8(stream, FU_KINETIC_DP_PUMA_REQUEST_FW_STD_VER_START_ADDR + 2, &tmpbuf, error)) return FALSE; std_fw_ver += (guint32)tmpbuf; fu_firmware_set_version_raw(FU_FIRMWARE(self), std_fw_ver); /* get cmbd block info */ cmdb_tmp = fu_input_stream_read_byte_array(stream, FU_KINETIC_DP_PUMA_REQUEST_FW_CMDB_START_ADDR, FU_KINETIC_DP_PUMA_REQUEST_CMDB_SIZE, NULL, error); if (cmdb_tmp == NULL) return FALSE; if (cmdb_tmp->len != FU_KINETIC_DP_PUMA_REQUEST_CMDB_SIZE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid firmware -- cmdb block invalid"); return FALSE; } if (memcmp(cmdb_tmp->data, cmdb_sig, sizeof(cmdb_sig)) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid firmware -- cmdb block not found"); return FALSE; } if (!fu_input_stream_read_u24(stream, FU_KINETIC_DP_PUMA_REQUEST_FW_CMDB_REV_ADDR, &checksum, G_LITTLE_ENDIAN, error)) return FALSE; checksum <<= 1; /* calculate crc for cmbd block */ checksum_actual = fu_sum8(cmdb_tmp->data, cmdb_tmp->len); if (checksum_actual == checksum) { if (!fu_input_stream_read_u16(stream, FU_KINETIC_DP_PUMA_REQUEST_FW_CMDB_STD_VER_ADDR, &priv->cmdb_version, G_BIG_ENDIAN, error)) return FALSE; if (!fu_input_stream_read_u24(stream, FU_KINETIC_DP_PUMA_REQUEST_FW_CMDB_REV_ADDR, &priv->cmdb_revision, G_BIG_ENDIAN, error)) return FALSE; } return TRUE; } static gboolean fu_kinetic_dp_puma_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuKineticDpPumaFirmware *self = FU_KINETIC_DP_PUMA_FIRMWARE(firmware); FuKineticDpPumaFirmwarePrivate *priv = GET_PRIVATE(self); gsize app_fw_size; gsize streamsz = 0; guint32 isp_drv_size = 0; g_autoptr(GInputStream) isp_drv_stream = NULL; g_autoptr(GInputStream) app_fw_stream = NULL; g_autoptr(FuFirmware) isp_drv_img = fu_firmware_new(); g_autoptr(FuFirmware) app_fw_img = fu_firmware_new(); /* parse firmware according to Kinetic's FW image format * FW binary = 4 bytes Header(Little-Endian) + ISP driver + App FW * 4 bytes Header: size of ISP driver */ if (!fu_input_stream_read_u32(stream, 0, &isp_drv_size, G_LITTLE_ENDIAN, error)) return FALSE; /* add ISP driver as a new image into firmware */ isp_drv_stream = fu_partial_input_stream_new(stream, HEADER_LEN_ISP_DRV_SIZE, isp_drv_size, error); if (isp_drv_stream == NULL) return FALSE; if (!fu_firmware_parse_stream(isp_drv_img, isp_drv_stream, 0x0, flags, error)) return FALSE; fu_firmware_set_idx(isp_drv_img, FU_KINETIC_DP_FIRMWARE_IDX_ISP_DRV); if (!fu_firmware_add_image_full(firmware, isp_drv_img, error)) return FALSE; /* add App FW as a new image into firmware */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz < HEADER_LEN_ISP_DRV_SIZE + isp_drv_size) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "stream was too small"); return FALSE; } app_fw_size = streamsz - HEADER_LEN_ISP_DRV_SIZE - isp_drv_size; app_fw_stream = fu_partial_input_stream_new(stream, HEADER_LEN_ISP_DRV_SIZE + isp_drv_size, app_fw_size, error); if (app_fw_stream == NULL) return FALSE; if (!fu_firmware_parse_stream(app_fw_img, app_fw_stream, 0x0, flags, error)) return FALSE; fu_firmware_set_idx(app_fw_img, FU_KINETIC_DP_FIRMWARE_IDX_APP_FW); if (!fu_firmware_add_image_full(firmware, app_fw_img, error)) return FALSE; /* figure out which chip App FW it is for */ if (!fu_kinetic_dp_puma_firmware_parse_chip_id(app_fw_stream, &priv->chip_id, error)) return FALSE; if (!fu_kinetic_dp_puma_firmware_parse_app_fw(self, app_fw_stream, error)) { g_prefix_error(error, "failed to parse info from Puma App firmware: "); return FALSE; } /* success */ return TRUE; } static void fu_kinetic_dp_puma_firmware_init(FuKineticDpPumaFirmware *self) { } static void fu_kinetic_dp_puma_firmware_class_init(FuKineticDpPumaFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_kinetic_dp_puma_firmware_parse; firmware_class->export = fu_kinetic_dp_puma_firmware_export; } fwupd-2.0.10/plugins/kinetic-dp/fu-kinetic-dp-puma-firmware.h000066400000000000000000000007311501337203100240050ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * Copyright 2021 Jeffrey Lin * Copyright 2022 Hai Su * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_KINETIC_DP_PUMA_FIRMWARE (fu_kinetic_dp_puma_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuKineticDpPumaFirmware, fu_kinetic_dp_puma_firmware, FU, KINETIC_DP_PUMA_FIRMWARE, FuFirmware) fwupd-2.0.10/plugins/kinetic-dp/fu-kinetic-dp-secure-device.c000066400000000000000000000720521501337203100237540ustar00rootroot00000000000000/* * Copyright 2021 Jeffrey Lin * Copyright 2022 Hai Su * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-kinetic-dp-secure-device.h" #include "fu-kinetic-dp-secure-firmware.h" #include "fu-kinetic-dp-struct.h" struct _FuKineticDpSecureDevice { FuKineticDpDevice parent_instance; guint16 read_flash_prog_time; guint16 flash_id; guint16 flash_size; gboolean isp_secure_auth_mode; FuKineticDpBank flash_bank; }; G_DEFINE_TYPE(FuKineticDpSecureDevice, fu_kinetic_dp_secure_device, FU_TYPE_KINETIC_DP_DEVICE) /* Kinetic proprietary DPCD fields for JaguarMustang, for both application and ISP driver */ #define DPCD_ADDR_CMD_STATUS_REG 0x0050D #define DPCD_ADDR_PARAM_REG 0x0050E /* DPCD registers are used while running ISP driver */ #define DPCD_ADDR_ISP_REPLY_LEN_REG 0x00513 #define DPCD_SIZE_ISP_REPLY_LEN_REG 1 #define DPCD_ADDR_ISP_REPLY_DATA_REG 0x00514 /* during ISP driver */ #define DPCD_SIZE_ISP_REPLY_DATA_REG 12 /* 0x00514 ~ 0x0051F*/ #define DPCD_ADDR_KT_AUX_WIN 0x80000ul #define DPCD_SIZE_KT_AUX_WIN 0x8000ul /* 0x80000ul ~ 0x87FFF, 32 KB */ #define DPCD_ADDR_KT_AUX_WIN_END (DPCD_ADDR_KT_AUX_WIN + DPCD_SIZE_KT_AUX_WIN - 1) #define DPCD_KT_CONFIRMATION_BIT 0x80 #define DPCD_KT_COMMAND_MASK 0x7F /* polling interval to check the status of installing FW images */ #define INSTALL_IMAGE_POLL_INTERVAL_MS 50 static void fu_kinetic_dp_secure_device_to_string(FuDevice *device, guint idt, GString *str) { FuKineticDpSecureDevice *self = FU_KINETIC_DP_SECURE_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "ReadFlashProgTime", self->read_flash_prog_time); fwupd_codec_string_append_hex(str, idt, "FlashId", self->flash_id); fwupd_codec_string_append_hex(str, idt, "FlashSize", self->flash_size); fwupd_codec_string_append_hex(str, idt, "IspSecureAuthMode", self->isp_secure_auth_mode); fwupd_codec_string_append(str, idt, "FlashBank", fu_kinetic_dp_bank_to_string(self->flash_bank)); } static gboolean fu_kinetic_dp_secure_device_read_param_reg(FuKineticDpSecureDevice *self, guint8 *dpcd_val, GError **error) { if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), DPCD_ADDR_PARAM_REG, dpcd_val, 1, FU_KINETIC_DP_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "failed to read DPCD_KT_PARAM_REG: "); return FALSE; } return TRUE; } static gboolean fu_kinetic_dp_secure_device_write_kt_prop_cmd(FuKineticDpSecureDevice *self, guint8 cmd_id, GError **error) { cmd_id |= DPCD_KT_CONFIRMATION_BIT; if (!fu_dpaux_device_write(FU_DPAUX_DEVICE(self), DPCD_ADDR_CMD_STATUS_REG, &cmd_id, sizeof(cmd_id), FU_KINETIC_DP_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "failed to write DPCD_KT_CMD_STATUS_REG: "); return FALSE; } return TRUE; } static gboolean fu_kinetic_dp_secure_device_clear_kt_prop_cmd(FuKineticDpSecureDevice *self, GError **error) { guint8 cmd_id = FU_KINETIC_DP_DPCD_CMD_STS_NONE; if (!fu_dpaux_device_write(FU_DPAUX_DEVICE(self), DPCD_ADDR_CMD_STATUS_REG, &cmd_id, sizeof(cmd_id), FU_KINETIC_DP_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "failed to write DPCD_KT_CMD_STATUS_REG: "); return FALSE; } return TRUE; } static gboolean fu_kinetic_dp_secure_device_send_kt_prop_cmd_cb(FuDevice *device, gpointer user_data, GError **error) { FuKineticDpSecureDevice *self = FU_KINETIC_DP_SECURE_DEVICE(device); guint8 status = (guint8)FU_KINETIC_DP_DPCD_CMD_STS_NONE; guint8 cmd_id = GPOINTER_TO_UINT(user_data); if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), DPCD_ADDR_CMD_STATUS_REG, &status, sizeof(status), FU_KINETIC_DP_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "failed to read DPCD_ADDR_CMD_STATUS_REG: "); return FALSE; } /* target responded */ if (status != (cmd_id | (guint8)DPCD_KT_CONFIRMATION_BIT)) { if (status != cmd_id) { status &= DPCD_KT_COMMAND_MASK; if (status == (guint8)FU_KINETIC_DP_DPCD_STS_CRC_FAILURE) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "chunk data CRC failed: "); return FALSE; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "Invalid value in DPCD_KT_CMD_STATUS_REG: 0x%x", status); return FALSE; } /* confirmation bit is cleared by sink, means sent command is processed */ return TRUE; } /* failed */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "waiting for prop cmd, got %s", fu_kinetic_dp_dpcd_to_string(status)); return FALSE; } static gboolean fu_kinetic_dp_secure_device_send_kt_prop_cmd(FuKineticDpSecureDevice *self, guint8 cmd_id, guint32 max_time_ms, guint16 poll_interval_ms, guint8 *status, GError **error) { if (!fu_kinetic_dp_secure_device_write_kt_prop_cmd(self, cmd_id, error)) return FALSE; /* wait for the sent proprietary command to be processed */ if (!fu_device_retry_full(FU_DEVICE(self), fu_kinetic_dp_secure_device_send_kt_prop_cmd_cb, max_time_ms / poll_interval_ms, poll_interval_ms, GUINT_TO_POINTER(cmd_id), error)) { g_prefix_error(error, "timeout waiting for prop command: "); return FALSE; } return TRUE; } static gboolean fu_kinetic_dp_secure_device_read_dpcd_reply_data_reg(FuKineticDpSecureDevice *self, guint8 *buf, gsize bufsz, guint8 *read_len, GError **error) { guint8 read_data_len; /* set the output to 0 */ *read_len = 0; if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), DPCD_ADDR_ISP_REPLY_LEN_REG, &read_data_len, 1, FU_KINETIC_DP_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "failed to read DPCD_ISP_REPLY_DATA_LEN_REG: "); return FALSE; } if (bufsz < read_data_len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "buffer size [%u] is not enough to read DPCD_ISP_REPLY_DATA_REG [%u]", (guint)bufsz, read_data_len); return FALSE; } if (read_data_len > 0) { if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), DPCD_ADDR_ISP_REPLY_DATA_REG, buf, read_data_len, FU_KINETIC_DP_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "failed to read DPCD_ISP_REPLY_DATA_REG: "); return FALSE; } *read_len = read_data_len; } return TRUE; } static gboolean fu_kinetic_dp_secure_device_write_dpcd_reply_data_reg(FuKineticDpSecureDevice *self, const guint8 *buf, gsize len, GError **error) { guint8 len_u8 = len; if (len > DPCD_SIZE_ISP_REPLY_DATA_REG) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "length bigger than DPCD_SIZE_ISP_REPLY_DATA_REG [%u]", (guint)len); return FALSE; } if (!fu_dpaux_device_write(FU_DPAUX_DEVICE(self), DPCD_ADDR_ISP_REPLY_DATA_REG, buf, len, FU_KINETIC_DP_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "failed to write DPCD_KT_REPLY_DATA_REG: "); return FALSE; } return fu_dpaux_device_write(FU_DPAUX_DEVICE(self), DPCD_ADDR_ISP_REPLY_LEN_REG, &len_u8, sizeof(len_u8), FU_KINETIC_DP_DEVICE_TIMEOUT, error); } static gboolean fu_kinetic_dp_secure_device_write_mca_oui(FuKineticDpSecureDevice *self, GError **error) { guint8 mca_oui[DPCD_SIZE_IEEE_OUI] = {MCA_OUI_BYTE_0, MCA_OUI_BYTE_1, MCA_OUI_BYTE_2}; return fu_kinetic_dp_device_dpcd_write_oui(FU_KINETIC_DP_DEVICE(self), mca_oui, error); } static gboolean fu_kinetic_dp_secure_device_enter_code_loading_mode(FuKineticDpSecureDevice *self, guint32 code_size, GError **error) { guint8 status = 0x0; guint8 buf[0x4] = {0x0}; if (fu_kinetic_dp_device_get_fw_state(FU_KINETIC_DP_DEVICE(self)) == FU_KINETIC_DP_FW_STATE_APP) { /* make DPCD 514 writable */ if (!fu_kinetic_dp_secure_device_send_kt_prop_cmd( self, FU_KINETIC_DP_DPCD_CMD_PREPARE_FOR_ISP_MODE, 500, 10, &status, error)) return FALSE; } /* update payload size to DPCD reply data reg first */ fu_memwrite_uint32(buf, code_size, G_LITTLE_ENDIAN); if (!fu_kinetic_dp_secure_device_write_dpcd_reply_data_reg(self, buf, sizeof(buf), error)) return FALSE; if (!fu_kinetic_dp_secure_device_send_kt_prop_cmd( self, FU_KINETIC_DP_DPCD_CMD_ENTER_CODE_LOADING_MODE, 500, 10, &status, error)) return FALSE; /* success */ return TRUE; } /** * fu_kinetic_dp_secure_device_crc16: * @buf: memory buffer * @bufsz: size of @buf * * Returns the cyclic redundancy check value for the given memory buffer. * This is a proprietary implementation only can be used in Kinetic's * Secure AUX-ISP protocol * * Returns: CRC value **/ static guint16 fu_kinetic_dp_secure_device_crc16(const guint8 *buf, gsize bufsize) { guint16 crc = 0x1021; for (gsize i = 0; i < bufsize; i++) { guint16 crc_tmp = crc; guint8 data = buf[i]; for (guint8 j = 8; j; j--) { guint8 flag = data ^ (crc_tmp >> 8); crc_tmp <<= 1; if (flag & 0x80) crc_tmp ^= 0x1021; data <<= 1; } crc = crc_tmp; } return crc; } static gboolean fu_kinetic_dp_secure_device_send_chunk(FuKineticDpSecureDevice *self, GBytes *fw, FuProgress *progress, GError **error) { g_autoptr(FuChunkArray) chunks = fu_chunk_array_new_from_bytes(fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 16); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_dpaux_device_write(FU_DPAUX_DEVICE(self), DPCD_ADDR_KT_AUX_WIN + fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), FU_KINETIC_DP_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "failed at 0x%x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_kinetic_dp_secure_device_send_payload(FuKineticDpSecureDevice *self, GBytes *fw, guint32 wait_time_ms, gint32 wait_interval_ms, FuProgress *progress, GError **error) { g_autoptr(FuChunkArray) chunks = fu_chunk_array_new_from_bytes(fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, DPCD_SIZE_KT_AUX_WIN); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GBytes) fw_chk = NULL; guint8 buf_crc16[0x4] = {0x0}; guint8 status = 0; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; /* send a maximum 32KB chunk of payload to AUX window */ fw_chk = fu_chunk_get_bytes(chk); if (!fu_kinetic_dp_secure_device_send_chunk(self, fw_chk, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to AUX write at 0x%x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } /* send the CRC16 of current 32KB chunk to DPCD_REPLY_DATA_REG */ fu_memwrite_uint32(buf_crc16, fu_kinetic_dp_secure_device_crc16(fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)), G_LITTLE_ENDIAN); if (!fu_kinetic_dp_secure_device_write_dpcd_reply_data_reg(self, buf_crc16, sizeof(buf_crc16), error)) { g_prefix_error(error, "failed to send CRC16 to reply data register: "); return FALSE; } /* notify that a chunk of payload has been sent to AUX window */ if (!fu_kinetic_dp_secure_device_send_kt_prop_cmd( self, FU_KINETIC_DP_DPCD_CMD_CHUNK_DATA_PROCESSED, wait_time_ms, wait_interval_ms, &status, error)) { g_prefix_error(error, "target failed to process payload chunk: "); return FALSE; } fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_kinetic_dp_secure_device_wait_dpcd_cmd_cleared_cb(FuDevice *device, gpointer user_data, GError **error) { FuKineticDpSecureDevice *self = FU_KINETIC_DP_SECURE_DEVICE(device); guint8 status = FU_KINETIC_DP_DPCD_CMD_STS_NONE; if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), DPCD_ADDR_CMD_STATUS_REG, &status, sizeof(status), FU_KINETIC_DP_DEVICE_TIMEOUT, error)) return FALSE; /* status is cleared by sink */ if (status == FU_KINETIC_DP_DPCD_CMD_STS_NONE) return TRUE; if ((status & DPCD_KT_CONFIRMATION_BIT) > 0) { /* status is not cleared but confirmation bit is cleared, * it means that target responded with failure */ if (status == FU_KINETIC_DP_DPCD_STS_INVALID_IMAGE) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid ISP driver"); return FALSE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to execute ISP driver"); return FALSE; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "waiting for sink to clear status"); return FALSE; } static gboolean fu_kinetic_dp_secure_device_wait_dpcd_cmd_cleared(FuKineticDpSecureDevice *self, guint16 wait_time_ms, guint16 poll_interval_ms, GError **error) { if (!fu_device_retry_full(FU_DEVICE(self), fu_kinetic_dp_secure_device_wait_dpcd_cmd_cleared_cb, wait_time_ms / poll_interval_ms, poll_interval_ms, NULL, error)) { g_prefix_error(error, "timeout waiting for DPCD_ISP_SINK_STATUS_REG: "); return FALSE; } return TRUE; } static gboolean fu_kinetic_dp_secure_device_execute_isp_drv(FuKineticDpSecureDevice *self, GError **error) { guint8 status; guint8 read_len; guint8 reply_data[6] = {0}; g_autoptr(GByteArray) st = NULL; /* in Jaguar, it takes about FU_KINETIC_DP_DEVICE_TIMEOUT ms to boot up and initialize */ self->flash_id = 0; self->flash_size = 0; self->read_flash_prog_time = 10; if (!fu_kinetic_dp_secure_device_write_kt_prop_cmd(self, FU_KINETIC_DP_DPCD_CMD_EXECUTE_RAM_CODE, error)) return FALSE; if (!fu_kinetic_dp_secure_device_wait_dpcd_cmd_cleared(self, 1500, 100, error)) return FALSE; if (!fu_kinetic_dp_secure_device_read_param_reg(self, &status, error)) return FALSE; if (status != FU_KINETIC_DP_DPCD_STS_SECURE_ENABLED && status != FU_KINETIC_DP_DPCD_STS_SECURE_DISABLED) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "waiting for ISP driver ready failed!"); return FALSE; } self->isp_secure_auth_mode = (status == FU_KINETIC_DP_DPCD_STS_SECURE_ENABLED); if (!fu_kinetic_dp_secure_device_read_dpcd_reply_data_reg(self, reply_data, sizeof(reply_data), &read_len, error)) { g_prefix_error(error, "failed to read flash ID and size: "); return FALSE; } st = fu_struct_kinetic_dp_flash_info_parse(reply_data, sizeof(reply_data), 0x0, error); self->flash_id = fu_struct_kinetic_dp_flash_info_get_id(st); self->flash_size = fu_struct_kinetic_dp_flash_info_get_size(st); self->read_flash_prog_time = fu_struct_kinetic_dp_flash_info_get_erase_time(st); if (self->read_flash_prog_time == 0) self->read_flash_prog_time = 10; /* one bank size in Jaguar is 1024KB */ if (self->flash_size >= 2048) fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); if (self->flash_size == 0) { if (self->flash_id > 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "SPI flash not supported"); return FALSE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "SPI flash not connected"); return FALSE; } /* success */ return TRUE; } static gboolean fu_kinetic_dp_secure_device_send_isp_drv(FuKineticDpSecureDevice *self, GBytes *fw, FuProgress *progress, GError **error) { if (!fu_kinetic_dp_secure_device_enter_code_loading_mode(self, g_bytes_get_size(fw), error)) { g_prefix_error(error, "enabling code-loading mode failed: "); return FALSE; } if (!fu_kinetic_dp_secure_device_send_payload(self, fw, 10000, 50, progress, error)) { g_prefix_error(error, "sending ISP driver payload failed: "); return FALSE; } g_debug("sending ISP driver payload..."); if (!fu_kinetic_dp_secure_device_execute_isp_drv(self, error)) { g_prefix_error(error, "ISP driver booting up failed: "); return FALSE; } return TRUE; } static gboolean fu_kinetic_dp_secure_device_enable_fw_update_mode(FuKineticDpSecureFirmware *firmware, FuKineticDpSecureDevice *self, GError **error) { guint8 status; guint8 buf[12] = {0}; /* send payload size to DPCD_MCA_REPLY_DATA_REG */ if (!fu_memwrite_uint32_safe(buf, sizeof(buf), 0, fu_kinetic_dp_secure_firmware_get_esm_payload_size(firmware), G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memwrite_uint32_safe(buf, sizeof(buf), 4, fu_kinetic_dp_secure_firmware_get_arm_app_code_size(firmware), G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memwrite_uint16_safe(buf, sizeof(buf), 8, fu_kinetic_dp_secure_firmware_get_app_init_data_size(firmware), G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memwrite_uint16_safe( buf, sizeof(buf), 10, (fu_kinetic_dp_secure_firmware_get_esm_xip_enabled(firmware) ? (1 << 15) : 0) | fu_kinetic_dp_secure_firmware_get_cmdb_block_size(firmware), G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_kinetic_dp_secure_device_write_dpcd_reply_data_reg(self, buf, sizeof(buf), error)) { g_prefix_error(error, "send payload size failed: "); return FALSE; } if (!fu_kinetic_dp_secure_device_send_kt_prop_cmd( self, FU_KINETIC_DP_DPCD_CMD_ENTER_FW_UPDATE_MODE, 200000, 500, &status, error)) { g_prefix_error(error, "entering F/W update mode failed: "); return FALSE; } return TRUE; } static gboolean fu_kinetic_dp_secure_device_send_app_fw(FuKineticDpSecureDevice *self, FuKineticDpSecureFirmware *firmware, GBytes *fw, FuProgress *progress, GError **error) { g_autoptr(GBytes) fw_app = NULL; g_autoptr(GBytes) fw_app_init = NULL; g_autoptr(GBytes) fw_app_data = NULL; g_autoptr(GBytes) fw_esm = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "send-sigs"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 38, "send-esm"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 57, "send-app"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 3, "send-initialized"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "send-app-id"); /* send ESM and App Certificates & RSA Signatures */ if (self->isp_secure_auth_mode) { g_autoptr(GBytes) fw_crt = NULL; fw_crt = fu_bytes_new_offset(fw, 0x0, FW_CERTIFICATE_SIZE * 2 + FW_RSA_SIGNATURE_BLOCK_SIZE * 2, error); if (fw_crt == NULL) return FALSE; if (!fu_kinetic_dp_secure_device_send_payload(self, fw_crt, 10000, 200, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to send certificates: "); return FALSE; } } fu_progress_step_done(progress); /* send ESM code */ fw_esm = fu_bytes_new_offset(fw, SPI_ESM_PAYLOAD_START, fu_kinetic_dp_secure_firmware_get_esm_payload_size(firmware), error); if (fw_esm == NULL) return FALSE; if (!fu_kinetic_dp_secure_device_send_payload(self, fw_esm, 10000, 200, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to send ESM payload: "); return FALSE; } fu_progress_step_done(progress); /* send App code */ fw_app = fu_bytes_new_offset(fw, SPI_APP_PAYLOAD_START, fu_kinetic_dp_secure_firmware_get_arm_app_code_size(firmware), error); if (fw_app == NULL) return FALSE; if (!fu_kinetic_dp_secure_device_send_payload(self, fw_app, 10000, 200, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to send App FW payload: "); return FALSE; } fu_progress_step_done(progress); /* send App initialized data */ fw_app_init = fu_bytes_new_offset(fw, fu_kinetic_dp_secure_firmware_get_esm_xip_enabled(firmware) ? SPI_APP_EXTEND_INIT_DATA_START : SPI_APP_NORMAL_INIT_DATA_START, fu_kinetic_dp_secure_firmware_get_app_init_data_size(firmware), error); if (fw_app_init == NULL) return FALSE; if (!fu_kinetic_dp_secure_device_send_payload(self, fw_app_init, 10000, 200, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to send App init data: "); return FALSE; } fu_progress_step_done(progress); /* send Application Identifier */ fw_app_data = fu_bytes_new_offset(fw, SPI_APP_ID_DATA_START, FU_STRUCT_KINETIC_DP_JAGUAR_FOOTER_SIZE, error); if (fw_app_data == NULL) return FALSE; if (!fu_kinetic_dp_secure_device_send_payload(self, fw_app_data, 10000, 200, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to send App ID data: "); return FALSE; } fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_kinetic_dp_secure_device_install_fw_images_cb(FuDevice *device, gpointer user_data, GError **error) { FuKineticDpSecureDevice *self = FU_KINETIC_DP_SECURE_DEVICE(device); guint8 status = 0; if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), DPCD_ADDR_CMD_STATUS_REG, &status, sizeof(status), FU_KINETIC_DP_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "failed to read DPCD_MCA_CMD_REG: "); return FALSE; } /* confirmation bit is cleared */ if ((status & DPCD_KT_CONFIRMATION_BIT) == 0) { if ((status & FU_KINETIC_DP_DPCD_CMD_INSTALL_IMAGES) > 0) return TRUE; g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to install images"); return FALSE; } /* failed */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "waiting for status, got %s", fu_kinetic_dp_dpcd_to_string(status)); return FALSE; } static gboolean fu_kinetic_dp_secure_device_install_fw_images(FuKineticDpSecureDevice *self, GError **error) { guint8 cmd_id = FU_KINETIC_DP_DPCD_CMD_INSTALL_IMAGES; if (!fu_kinetic_dp_secure_device_write_kt_prop_cmd(self, cmd_id, error)) { g_prefix_error(error, "failed to send DPCD command: "); return FALSE; } if (!fu_device_retry_full(FU_DEVICE(self), fu_kinetic_dp_secure_device_install_fw_images_cb, 1500, INSTALL_IMAGE_POLL_INTERVAL_MS, NULL, error)) { g_prefix_error(error, "timeout waiting for install command to be processed "); return FALSE; } /* success */ return TRUE; } static gboolean fu_kinetic_dp_secure_device_get_flash_bank_idx(FuKineticDpSecureDevice *self, GError **error) { guint8 status; guint8 buf[DPCD_SIZE_IEEE_OUI] = {0}; guint8 res = FU_KINETIC_DP_BANK_NONE; if (!fu_kinetic_dp_device_dpcd_read_oui(FU_KINETIC_DP_DEVICE(self), buf, sizeof(buf), error)) return FALSE; if (!fu_kinetic_dp_secure_device_write_mca_oui(self, error)) return FALSE; if (!fu_kinetic_dp_secure_device_send_kt_prop_cmd( self, FU_KINETIC_DP_DPCD_CMD_GET_ACTIVE_FLASH_BANK, 100, 20, &status, error)) return FALSE; if (!fu_kinetic_dp_secure_device_read_param_reg(self, &res, error)) return FALSE; if (!fu_kinetic_dp_secure_device_clear_kt_prop_cmd(self, error)) return FALSE; /* restore previous source OUI */ if (!fu_kinetic_dp_device_dpcd_write_oui(FU_KINETIC_DP_DEVICE(self), buf, error)) return FALSE; g_debug("secure aux got active flash bank 0x%x (0=BankA, 1=BankB, 2=TotalBanks)", res); self->flash_bank = (FuKineticDpBank)res; if (self->flash_bank == FU_KINETIC_DP_BANK_NONE) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "bank not NONE"); return FALSE; } /* success */ return TRUE; } static gchar * fu_kinetic_dp_secure_device_convert_version(FuDevice *device, guint64 version_raw) { guint8 buf[3] = {0}; fu_memwrite_uint24(buf, version_raw, G_LITTLE_ENDIAN); return g_strdup_printf("%1d.%03d.%02d", buf[2], buf[1], buf[0]); } static gboolean fu_kinetic_dp_secure_device_setup(FuDevice *device, GError **error) { FuKineticDpSecureDevice *self = FU_KINETIC_DP_SECURE_DEVICE(device); /* FuKineticDpDevice->setup */ if (!FU_DEVICE_CLASS(fu_kinetic_dp_secure_device_parent_class)->setup(device, error)) return FALSE; /* get flash info */ if (fu_kinetic_dp_device_get_fw_state(FU_KINETIC_DP_DEVICE(device)) == FU_KINETIC_DP_FW_STATE_APP) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_DUAL_IMAGE); if (!fu_kinetic_dp_secure_device_get_flash_bank_idx(self, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_kinetic_dp_secure_device_prepare(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuKineticDpSecureDevice *self = FU_KINETIC_DP_SECURE_DEVICE(device); return fu_kinetic_dp_secure_device_write_mca_oui(self, error); } static gboolean fu_kinetic_dp_secure_device_cleanup(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuKineticDpSecureDevice *self = FU_KINETIC_DP_SECURE_DEVICE(device); /* wait for flash clear to settle */ fu_device_sleep(FU_DEVICE(self), 2000); /* send reset command */ return fu_kinetic_dp_secure_device_write_kt_prop_cmd(self, FU_KINETIC_DP_DPCD_CMD_RESET_SYSTEM, error); } static gboolean fu_kinetic_dp_secure_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuKineticDpSecureDevice *self = FU_KINETIC_DP_SECURE_DEVICE(device); FuKineticDpSecureFirmware *dp_firmware = FU_KINETIC_DP_SECURE_FIRMWARE(firmware); g_autoptr(GBytes) app_fw_blob = NULL; g_autoptr(GBytes) isp_drv_blob = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 3, "isp"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 97, "app"); /* get image of ISP driver */ isp_drv_blob = fu_firmware_get_image_by_idx_bytes(firmware, FU_KINETIC_DP_FIRMWARE_IDX_ISP_DRV, error); if (isp_drv_blob == NULL) return FALSE; /* send ISP driver and execute it */ if (g_bytes_get_size(isp_drv_blob) > 0) { if (!fu_kinetic_dp_secure_device_send_isp_drv(self, isp_drv_blob, fu_progress_get_child(progress), error)) return FALSE; } fu_progress_step_done(progress); /* enable FW update mode */ if (!fu_kinetic_dp_secure_device_enable_fw_update_mode(dp_firmware, self, error)) return FALSE; /* get image of App FW */ app_fw_blob = fu_firmware_get_image_by_idx_bytes(firmware, FU_KINETIC_DP_FIRMWARE_IDX_APP_FW, error); if (app_fw_blob == NULL) return FALSE; if (!fu_kinetic_dp_secure_device_send_app_fw(self, dp_firmware, app_fw_blob, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* install FW images */ return fu_kinetic_dp_secure_device_install_fw_images(self, error); } static void fu_kinetic_dp_secure_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_kinetic_dp_secure_device_init(FuKineticDpSecureDevice *self) { self->read_flash_prog_time = 10; self->isp_secure_auth_mode = TRUE; fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_KINETIC_DP_SECURE_FIRMWARE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_retry_add_recovery(FU_DEVICE(self), FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, NULL); } static void fu_kinetic_dp_secure_device_class_init(FuKineticDpSecureDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_kinetic_dp_secure_device_to_string; device_class->prepare = fu_kinetic_dp_secure_device_prepare; device_class->cleanup = fu_kinetic_dp_secure_device_cleanup; device_class->setup = fu_kinetic_dp_secure_device_setup; device_class->write_firmware = fu_kinetic_dp_secure_device_write_firmware; device_class->set_progress = fu_kinetic_dp_secure_device_set_progress; device_class->convert_version = fu_kinetic_dp_secure_device_convert_version; } fwupd-2.0.10/plugins/kinetic-dp/fu-kinetic-dp-secure-device.h000066400000000000000000000045551501337203100237640ustar00rootroot00000000000000/* * Copyright 2021 Jeffrey Lin * Copyright 2022 Hai Su * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-kinetic-dp-device.h" #define FU_TYPE_KINETIC_DP_SECURE_DEVICE (fu_kinetic_dp_secure_device_get_type()) G_DECLARE_FINAL_TYPE(FuKineticDpSecureDevice, fu_kinetic_dp_secure_device, FU, KINETIC_DP_SECURE_DEVICE, FuKineticDpDevice) /* Flash Memory Map */ #define STD_FW_PAYLOAD_SIZE (1024 * 1024) #define CUSTOMER_PROJ_ID_OFFSET \ (STD_FW_PAYLOAD_SIZE - FU_STRUCT_KINETIC_DP_JAGUAR_FOOTER_SIZE + 15) /* 0xFFFEF */ #define CUSTOMER_FW_VER_OFFSET \ (STD_FW_PAYLOAD_SIZE - FU_STRUCT_KINETIC_DP_JAGUAR_FOOTER_SIZE + 16) /* 0xFFFF0 */ #define CUSTOMER_FW_VER_SIZE 2 #define FW_CERTIFICATE_SIZE (1 * 1024) #define FW_RSA_SIGNATURE_SIZE 256 #define FW_RSA_SIGNATURE_BLOCK_SIZE (1 * 1024) #define ESM_PAYLOAD_BLOCK_SIZE (256 * 1024) #define APP_CODE_NORMAL_BLOCK_SIZE (384 * 1024) #define APP_CODE_EXTEND_BLOCK_SIZE (640 * 1024) #define APP_INIT_DATA_BLOCK_SIZE (24 * 1024) #define CMDB_BLOCK_SIZE (4 * 1024) #define SPI_ESM_CERTIFICATE_START 0 #define SPI_APP_CERTIFICATE_START (SPI_ESM_CERTIFICATE_START + FW_CERTIFICATE_SIZE) /*0x00400*/ #define SPI_ESM_RSA_SIGNATURE_START (SPI_APP_CERTIFICATE_START + FW_CERTIFICATE_SIZE) /*0x00800*/ #define SPI_APP_RSA_SIGNATURE_START \ (SPI_ESM_RSA_SIGNATURE_START + FW_RSA_SIGNATURE_BLOCK_SIZE) /*0x00C00*/ #define SPI_ESM_PAYLOAD_START \ (SPI_APP_RSA_SIGNATURE_START + FW_RSA_SIGNATURE_BLOCK_SIZE) /*0x01000*/ #define SPI_APP_PAYLOAD_START (SPI_ESM_PAYLOAD_START + ESM_PAYLOAD_BLOCK_SIZE) /*0x41000*/ #define SPI_APP_NORMAL_INIT_DATA_START \ (SPI_APP_PAYLOAD_START + APP_CODE_NORMAL_BLOCK_SIZE) /*0xA1000*/ #define SPI_APP_EXTEND_INIT_DATA_START \ (SPI_APP_PAYLOAD_START + APP_CODE_EXTEND_BLOCK_SIZE) /*0xE1000*/ #define SPI_CMDB_BLOCK_START 0xFE000UL #define SPI_APP_ID_DATA_START (STD_FW_PAYLOAD_SIZE - FU_STRUCT_KINETIC_DP_JAGUAR_FOOTER_SIZE) fwupd-2.0.10/plugins/kinetic-dp/fu-kinetic-dp-secure-firmware.c000066400000000000000000000213711501337203100243270ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * Copyright 2021 Jeffrey Lin * Copyright 2022 Hai Su * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-kinetic-dp-secure-device.h" #include "fu-kinetic-dp-secure-firmware.h" struct _FuKineticDpSecureFirmware { FuFirmwareClass parent_instance; }; typedef struct { FuKineticDpChip chip_id; guint32 isp_drv_size; guint32 esm_payload_size; guint32 arm_app_code_size; guint16 app_init_data_size; guint16 cmdb_block_size; gboolean esm_xip_enabled; } FuKineticDpSecureFirmwarePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuKineticDpSecureFirmware, fu_kinetic_dp_secure_firmware, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_kinetic_dp_secure_firmware_get_instance_private(o)) #define HEADER_LEN_ISP_DRV_SIZE 4 #define APP_ID_STR_LEN 4 static void fu_kinetic_dp_secure_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuKineticDpSecureFirmware *self = FU_KINETIC_DP_SECURE_FIRMWARE(firmware); FuKineticDpSecureFirmwarePrivate *priv = GET_PRIVATE(self); fu_xmlb_builder_insert_kv(bn, "chip_id", fu_kinetic_dp_chip_to_string(priv->chip_id)); fu_xmlb_builder_insert_kx(bn, "isp_drv_size", priv->isp_drv_size); fu_xmlb_builder_insert_kx(bn, "esm_payload_size", priv->esm_payload_size); fu_xmlb_builder_insert_kx(bn, "arm_app_code_size", priv->arm_app_code_size); fu_xmlb_builder_insert_kx(bn, "app_init_data_size", priv->app_init_data_size); fu_xmlb_builder_insert_kx(bn, "cmdb_block_size", priv->cmdb_block_size); fu_xmlb_builder_insert_kb(bn, "esm_xip_enabled", priv->esm_xip_enabled); } static gboolean fu_kinetic_dp_secure_firmware_parse_chip_id(GInputStream *stream, FuKineticDpChip *chip_id, gboolean *esm_xip_enabled, GError **error) { const struct { FuKineticDpChip chip_id; guint32 offset; const gchar *app_id; gboolean esm_xip_enabled; } map[] = { {FU_KINETIC_DP_CHIP_JAGUAR_5000, 0x0FFFE4UL, "JAGR", FALSE}, /* 1024KB */ {FU_KINETIC_DP_CHIP_JAGUAR_5000, 0x0A7036UL, "JAGR", FALSE}, /* 670KB ANZU */ {FU_KINETIC_DP_CHIP_JAGUAR_5000, 0x0FFFE4UL, "JAGX", TRUE}, /* 1024KB (640KB) */ {FU_KINETIC_DP_CHIP_JAGUAR_5000, 0x0E7036UL, "JAGX", TRUE}, /* 670KB ANZU (640KB) */ {FU_KINETIC_DP_CHIP_MUSTANG_5200, 0x0FFFE4UL, "MSTG", FALSE}, /* 1024KB */ {FU_KINETIC_DP_CHIP_MUSTANG_5200, 0x0A7036UL, "MSTG", FALSE}, /* 670KB ANZU */ {FU_KINETIC_DP_CHIP_MUSTANG_5200, 0x0FFFE4UL, "MSTX", TRUE}, /* 1024KB (640KB) */ {FU_KINETIC_DP_CHIP_MUSTANG_5200, 0x0E7036UL, "MSTX", TRUE}, /* 670KB ANZU (640KB) */ }; for (guint32 i = 0; i < G_N_ELEMENTS(map); i++) { guint8 buf[4] = {APP_ID_STR_LEN}; if (!fu_input_stream_read_safe(stream, buf, sizeof(buf), 0x0, map[i].offset, sizeof(buf), error)) return FALSE; if (memcmp(buf, (const guint8 *)map[i].app_id, sizeof(buf)) == 0) { *chip_id = map[i].chip_id; *esm_xip_enabled = map[i].esm_xip_enabled; return TRUE; } } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no valid Chip ID is found in the firmware"); return FALSE; } guint32 fu_kinetic_dp_secure_firmware_get_esm_payload_size(FuKineticDpSecureFirmware *self) { FuKineticDpSecureFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_KINETIC_DP_SECURE_FIRMWARE(self), 0); return priv->esm_payload_size; } guint32 fu_kinetic_dp_secure_firmware_get_arm_app_code_size(FuKineticDpSecureFirmware *self) { FuKineticDpSecureFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_KINETIC_DP_SECURE_FIRMWARE(self), 0); return priv->arm_app_code_size; } guint16 fu_kinetic_dp_secure_firmware_get_app_init_data_size(FuKineticDpSecureFirmware *self) { FuKineticDpSecureFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_KINETIC_DP_SECURE_FIRMWARE(self), 0); return priv->app_init_data_size; } guint16 fu_kinetic_dp_secure_firmware_get_cmdb_block_size(FuKineticDpSecureFirmware *self) { FuKineticDpSecureFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_KINETIC_DP_SECURE_FIRMWARE(self), 0); return priv->cmdb_block_size; } gboolean fu_kinetic_dp_secure_firmware_get_esm_xip_enabled(FuKineticDpSecureFirmware *self) { FuKineticDpSecureFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_KINETIC_DP_SECURE_FIRMWARE(self), FALSE); return priv->esm_xip_enabled; } static gboolean fu_kinetic_dp_secure_firmware_parse_app_fw(FuKineticDpSecureFirmware *self, GInputStream *stream, GError **error) { FuKineticDpSecureFirmwarePrivate *priv = GET_PRIVATE(self); gsize streamsz = 0; guint32 app_code_block_size; guint32 std_fw_ver = 0; g_autoptr(GByteArray) st = NULL; /* sanity check */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz != STD_FW_PAYLOAD_SIZE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "firmware payload size (0x%x) is not valid", (guint)streamsz); return FALSE; } if (priv->esm_xip_enabled) { app_code_block_size = APP_CODE_EXTEND_BLOCK_SIZE; } else { app_code_block_size = APP_CODE_NORMAL_BLOCK_SIZE; } /* get FW info embedded in FW file */ st = fu_struct_kinetic_dp_jaguar_footer_parse_stream(stream, SPI_APP_ID_DATA_START, error); if (st == NULL) return FALSE; /* get standard FW version */ std_fw_ver = (guint32)(fu_struct_kinetic_dp_jaguar_footer_get_fw_ver(st) << 8); std_fw_ver += fu_struct_kinetic_dp_jaguar_footer_get_fw_rev(st); fu_firmware_set_version_raw(FU_FIRMWARE(self), std_fw_ver); /* get each block size from FW buffer */ priv->esm_payload_size = ESM_PAYLOAD_BLOCK_SIZE; priv->arm_app_code_size = app_code_block_size; priv->app_init_data_size = APP_INIT_DATA_BLOCK_SIZE; priv->cmdb_block_size = CMDB_BLOCK_SIZE; return TRUE; } static gboolean fu_kinetic_dp_secure_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuKineticDpSecureFirmware *self = FU_KINETIC_DP_SECURE_FIRMWARE(firmware); FuKineticDpSecureFirmwarePrivate *priv = GET_PRIVATE(self); gsize streamsz = 0; guint32 app_fw_payload_size = 0; g_autoptr(GInputStream) isp_drv_stream = NULL; g_autoptr(GInputStream) app_fw_stream = NULL; g_autoptr(FuFirmware) isp_drv_img = fu_firmware_new(); g_autoptr(FuFirmware) app_fw_img = fu_firmware_new(); /* parse firmware according to Kinetic's FW image format * FW binary = 4 bytes Header(Little-Endian) + ISP driver + App FW * 4 bytes Header: size of ISP driver */ if (!fu_input_stream_read_u32(stream, 0, &priv->isp_drv_size, G_LITTLE_ENDIAN, error)) return FALSE; /* app firmware payload size */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz < HEADER_LEN_ISP_DRV_SIZE + priv->isp_drv_size) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "stream was too small"); return FALSE; } app_fw_payload_size = streamsz - HEADER_LEN_ISP_DRV_SIZE - priv->isp_drv_size; /* add ISP driver as a new image into firmware */ isp_drv_stream = fu_partial_input_stream_new(stream, HEADER_LEN_ISP_DRV_SIZE, priv->isp_drv_size, error); if (isp_drv_stream == NULL) return FALSE; if (!fu_firmware_parse_stream(isp_drv_img, isp_drv_stream, 0x0, flags, error)) return FALSE; fu_firmware_set_idx(isp_drv_img, FU_KINETIC_DP_FIRMWARE_IDX_ISP_DRV); if (!fu_firmware_add_image_full(firmware, isp_drv_img, error)) return FALSE; /* add App FW as a new image into firmware */ app_fw_stream = fu_partial_input_stream_new(stream, HEADER_LEN_ISP_DRV_SIZE + priv->isp_drv_size, app_fw_payload_size, error); if (app_fw_stream == NULL) return FALSE; if (!fu_firmware_parse_stream(app_fw_img, app_fw_stream, 0x0, flags, error)) return FALSE; fu_firmware_set_idx(app_fw_img, FU_KINETIC_DP_FIRMWARE_IDX_APP_FW); if (!fu_firmware_add_image_full(firmware, app_fw_img, error)) return FALSE; /* figure out which chip App FW it is for */ if (!fu_kinetic_dp_secure_firmware_parse_chip_id(stream, &priv->chip_id, &priv->esm_xip_enabled, error)) return FALSE; if (!fu_kinetic_dp_secure_firmware_parse_app_fw(self, stream, error)) { g_prefix_error(error, "failed to parse info from Jaguar or Mustang App firmware: "); return FALSE; } /* success */ return TRUE; } static void fu_kinetic_dp_secure_firmware_init(FuKineticDpSecureFirmware *self) { } static void fu_kinetic_dp_secure_firmware_class_init(FuKineticDpSecureFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_kinetic_dp_secure_firmware_parse; firmware_class->export = fu_kinetic_dp_secure_firmware_export; } fwupd-2.0.10/plugins/kinetic-dp/fu-kinetic-dp-secure-firmware.h000066400000000000000000000020261501337203100243300ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * Copyright 2021 Jeffrey Lin * Copyright 2022 Hai Su * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_KINETIC_DP_SECURE_FIRMWARE (fu_kinetic_dp_secure_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuKineticDpSecureFirmware, fu_kinetic_dp_secure_firmware, FU, KINETIC_DP_SECURE_FIRMWARE, FuFirmware) guint32 fu_kinetic_dp_secure_firmware_get_esm_payload_size(FuKineticDpSecureFirmware *self); guint32 fu_kinetic_dp_secure_firmware_get_arm_app_code_size(FuKineticDpSecureFirmware *self); guint16 fu_kinetic_dp_secure_firmware_get_app_init_data_size(FuKineticDpSecureFirmware *self); guint16 fu_kinetic_dp_secure_firmware_get_cmdb_block_size(FuKineticDpSecureFirmware *self); gboolean fu_kinetic_dp_secure_firmware_get_esm_xip_enabled(FuKineticDpSecureFirmware *self); guint8 fu_kinetic_dp_secure_firmware_get_customer_project_id(FuKineticDpSecureFirmware *self); fwupd-2.0.10/plugins/kinetic-dp/fu-kinetic-dp.rs000066400000000000000000000053241501337203100214330ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] enum FuKineticDpFamily { Unknown, Mustang, Jaguar, Puma, } #[derive(ToString)] enum FuKineticDpChip { None, Bobcat_2800, Bobcat_2850, Pegasus, Mystique, Dp2vga, Puma_2900, Puma_2920, Jaguar_5000, Mustang_5200, } enum FuKineticDpDev { Host, Port1, Port2, Port3, // MaxNum, } #[derive(ToString)] enum FuKineticDpBank { A = 0, B = 1, None = 0xFF, } enum FuKineticDpFirmwareIdx { IspDrv, AppFw, } #[derive(ToString)] enum FuKineticDpFwState { None, Irom, BootCode, App, } #[derive(ParseStream, Default)] #[repr(C, packed)] struct FuStructKineticDpPumaHeader { _unknown: u8, object_count: u8 == 8, // certificate + ESM + Signature + hash + certificate + Puma App + Signature + hash } #[derive(ParseStream)] #[repr(C, packed)] struct FuStructKineticDpPumaHeaderInfo { type: u8, subtype: u8, length: u32le, } #[derive(ToString)] enum FuKineticDpPumaMode { ChunkProcessed = 0x03, ChunkReceived = 0x07, FlashInfoReady = 0xA1, UpdateAbort = 0x55, } enum FuKineticDpPumaRequest { ChipResetRequest = 0, CodeLoadRequest = 0x01, CodeLoadReady = 0x03, CodeBootupDone = 0x07, CmdbGetinfoReq = 0xA0, CmdbGetinfoRead = 0xA1, CmdbGetinfoInvalid = 0xA2, CmdbGetinfoDone = 0xA3, FlashEraseDone = 0xE0, FlashRraseFail = 0xE1, FlashRraseRequest = 0xEE, FwUpdateDone = 0xF8, FwUpdateReady = 0xFC, FwUpdateRequest = 0xFE, } #[derive(ParseStream)] #[repr(C, packed)] struct FuStructKineticDpJaguarFooter { app_id_struct_ver: u32le, app_id: [u8; 4], app_ver_id: u32le, fw_ver: u16be, fw_rev: u8, customer_fw_project_id: u8, customer_fw_ver: u16be, chip_rev: u8, is_fpga_enabled: u8, reserved: [u8; 12], } #[derive(Parse)] #[repr(C, packed)] struct FuStructKineticDpFlashInfo { id: u16be, size: u16be, erase_time: u16be, } #[derive(ToString)] enum FuKineticDpDpcd { // status CmdStsNone = 0x0, StsInvalidInfo = 0x01, StsCrcFailure = 0x02, StsInvalidImage = 0x03, StsSecureEnabled = 0x04, StsSecureDisabled = 0x05, StsSpiFlashFailure = 0x06, // command CmdPrepareForIspMode = 0x23, CmdEnterCodeLoadingMode = 0x24, CmdExecuteRamCode = 0x25, CmdEnterFwUpdateMode = 0x26, CmdChunkDataProcessed = 0x27, CmdInstallImages = 0x28, CmdResetSystem = 0x29, // other command CmdEnableAuxForward = 0x31, CmdDisableAuxForward = 0x32, CmdGetActiveFlashBank = 0x33, // 0x70 ~ 0x7F are reserved for other usage CmdReserved = 0x7f, } fwupd-2.0.10/plugins/kinetic-dp/meson.build000066400000000000000000000012141501337203100205620ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginKineticDp"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_builtin_kinetic_dp = static_library('fu_plugin_kinetic_dp', rustgen.process('fu-kinetic-dp.rs'), sources: [ 'fu-kinetic-dp-device.c', 'fu-kinetic-dp-plugin.c', 'fu-kinetic-dp-puma-device.c', 'fu-kinetic-dp-puma-firmware.c', 'fu-kinetic-dp-secure-device.c', 'fu-kinetic-dp-secure-firmware.c', ], include_directories: plugin_incdirs, c_args: [ cargs, ], link_with: plugin_libs, dependencies: plugin_deps, ) plugin_builtins += plugin_builtin_kinetic_dp endif fwupd-2.0.10/plugins/legion-hid2/000077500000000000000000000000001501337203100164745ustar00rootroot00000000000000fwupd-2.0.10/plugins/legion-hid2/README.md000066400000000000000000000017121501337203100177540ustar00rootroot00000000000000--- title: Plugin: Legion HID 2 --- ## Introduction The Legion HID plugin is used for interacting with the MCU on some Legion devices. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a packed binary file format. This plugin supports the following protocol ID: * `com.lenovo.legion-hid2` ## GUID Generation These devices use the standard DeviceInstanceId values, e.g. * `USB\VID_1A86&PID_E310` ## Update Behavior The device will restart after update. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x1A86` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `2.0.0`. Since: 2.0.0 ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: @superm1 fwupd-2.0.10/plugins/legion-hid2/fu-legion-hid2-child-device.c000066400000000000000000000114421501337203100236710ustar00rootroot00000000000000/* * Copyright 2025 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-legion-hid2-child-device.h" #include "fu-legion-hid2-device.h" #include "fu-legion-hid2-struct.h" struct _FuLegionHid2ChildDevice { FuDevice parent_instance; guint8 manufacturer; }; G_DEFINE_TYPE(FuLegionHid2ChildDevice, fu_legion_hid2_child_device, FU_TYPE_DEVICE) #define FU_LEGION_HID2_CHILD_DEVICE_TIMEOUT 200 /* ms */ static void fu_legion_hid2_child_device_to_string(FuDevice *device, guint idt, GString *str) { FuLegionHid2ChildDevice *self = FU_LEGION_HID2_CHILD_DEVICE(device); fwupd_codec_string_append_int(str, idt, "ChipManufacturer", self->manufacturer); } static gboolean fu_legion_hid2_child_device_transfer(FuLegionHid2ChildDevice *self, GByteArray *req, GByteArray *res, GError **error) { FuHidDevice *hid_dev = FU_HID_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); if (req != NULL) { if (!fu_hid_device_set_report(hid_dev, req->data[0], req->data, req->len, FU_LEGION_HID2_CHILD_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error)) { g_prefix_error(error, "failed to send packet: "); return FALSE; } } if (res != NULL) { if (!fu_hid_device_get_report(hid_dev, req->data[0], res->data, res->len, FU_LEGION_HID2_CHILD_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error)) { g_prefix_error(error, "failed to receive packet: "); return FALSE; } } return TRUE; } static gboolean fu_legion_hid2_child_device_probe(FuDevice *device, GError **error) { FuLegionHid2ChildDevice *self = FU_LEGION_HID2_CHILD_DEVICE(device); g_autoptr(GByteArray) cmd = fu_struct_legion_get_pl_test_new(); g_autoptr(GByteArray) tp_man = fu_struct_legion_get_pl_test_result_new(); if (fu_device_get_proxy(FU_DEVICE(self)) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return FALSE; } fu_struct_legion_get_pl_test_set_index(cmd, FU_LEGION_HID2_PL_TEST_TP_MANUFACTURER); if (!fu_legion_hid2_child_device_transfer(self, cmd, tp_man, error)) return FALSE; self->manufacturer = fu_struct_legion_get_pl_test_result_get_content(tp_man); switch (self->manufacturer) { case FU_LEGION_HID2_TP_MAN_BETTER_LIFE: fu_device_set_vendor(device, "Better Life"); fu_device_add_instance_strsafe(FU_DEVICE(self), "TP", "BL"); break; case FU_LEGION_HID2_TP_MAN_SIPO: fu_device_set_vendor(device, "SIPO"); fu_device_add_instance_strsafe(FU_DEVICE(self), "TP", "SIPO"); break; default: case FU_LEGION_HID2_TP_MAN_NONE: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no touchpad found"); return FALSE; } fu_device_build_instance_id(FU_DEVICE(self), NULL, "USB", "VID", "PID", "TP", NULL); return TRUE; } static gboolean fu_legion_hid2_child_device_setup(FuDevice *device, GError **error) { FuLegionHid2ChildDevice *self = FU_LEGION_HID2_CHILD_DEVICE(device); g_autoptr(GByteArray) cmd = fu_struct_legion_get_pl_test_new(); g_autoptr(GByteArray) tp_ver = fu_struct_legion_get_pl_test_result_new(); g_autofree gchar *version = NULL; if (fu_device_get_proxy(FU_DEVICE(self)) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return FALSE; } fu_struct_legion_get_pl_test_set_index(cmd, FU_LEGION_HID2_PL_TEST_TP_VERSION); if (!fu_legion_hid2_child_device_transfer(self, cmd, tp_ver, error)) return FALSE; version = g_strdup_printf("%d", fu_struct_legion_get_pl_test_result_get_content(tp_ver)); fu_device_set_version(device, version); return TRUE; } static void fu_legion_hid2_child_device_init(FuLegionHid2ChildDevice *self) { fu_device_set_name(FU_DEVICE(self), "Touchpad"); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PROXY_FALLBACK); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REFCOUNTED_PROXY); fu_device_add_protocol(FU_DEVICE(self), "com.lenovo.legion-hid2"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_NUMBER); fu_device_set_logical_id(FU_DEVICE(self), "touchpad"); } static void fu_legion_hid2_child_device_class_init(FuLegionHid2ChildDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_legion_hid2_child_device_to_string; device_class->setup = fu_legion_hid2_child_device_setup; device_class->probe = fu_legion_hid2_child_device_probe; } FuDevice * fu_legion_hid2_child_device_new(FuDevice *proxy) { return g_object_new(FU_TYPE_LEGION_HID2_CHILD_DEVICE, "proxy", proxy, NULL); } fwupd-2.0.10/plugins/legion-hid2/fu-legion-hid2-child-device.h000066400000000000000000000007131501337203100236750ustar00rootroot00000000000000/* * Copyright 2025 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_LEGION_HID2_CHILD_DEVICE (fu_legion_hid2_child_device_get_type()) G_DECLARE_FINAL_TYPE(FuLegionHid2ChildDevice, fu_legion_hid2_child_device, FU, LEGION_HID2_CHILD_DEVICE, FuDevice) FuDevice * fu_legion_hid2_child_device_new(FuDevice *proxy) G_GNUC_NON_NULL(1); fwupd-2.0.10/plugins/legion-hid2/fu-legion-hid2-device.c000066400000000000000000000363231501337203100226150ustar00rootroot00000000000000/* * Copyright 2024 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-legion-hid2-child-device.h" #include "fu-legion-hid2-device.h" #include "fu-legion-hid2-firmware.h" #include "fu-legion-hid2-struct.h" struct _FuLegionHid2Device { FuHidDevice parent_instance; }; G_DEFINE_TYPE(FuLegionHid2Device, fu_legion_hid2_device, FU_TYPE_HID_DEVICE) #define FU_LEGION_HID2_DEVICE_TIMEOUT 200 /* ms */ static gboolean fu_legion_hid2_device_transfer(FuLegionHid2Device *self, GByteArray *req, GByteArray *res, GError **error) { if (req != NULL) { if (!fu_hid_device_set_report(FU_HID_DEVICE(self), req->data[0], req->data, req->len, FU_LEGION_HID2_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error)) { g_prefix_error(error, "failed to send packet: "); return FALSE; } } if (res != NULL) { if (!fu_hid_device_get_report(FU_HID_DEVICE(self), res->data[0], res->data, res->len, FU_LEGION_HID2_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error)) { g_prefix_error(error, "failed to receive packet: "); return FALSE; } } return TRUE; } static gchar * fu_legion_hid2_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_device_get_version_format(device)); } static void fu_legion_hid2_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 6, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 76, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 17, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static gboolean fu_legion_hid2_device_ensure_version(FuDevice *device, GError **error) { guint32 version; g_autoptr(GByteArray) cmd = fu_struct_legion_get_version_new(); g_autoptr(GByteArray) result = fu_struct_legion_version_new(); if (!fu_legion_hid2_device_transfer(FU_LEGION_HID2_DEVICE(device), cmd, result, error)) return FALSE; version = fu_struct_legion_version_get_version(result); fu_device_set_version_raw(device, version); return TRUE; } static gboolean fu_legion_hid2_device_ensure_mcu_id(FuDevice *device, GError **error) { g_autoptr(GByteArray) cmd = fu_struct_legion_get_mcu_id_new(); g_autoptr(GByteArray) result = fu_struct_legion_mcu_id_new(); if (!fu_legion_hid2_device_transfer(FU_LEGION_HID2_DEVICE(device), cmd, result, error)) return FALSE; return TRUE; } static gboolean fu_legion_hid2_device_probe(FuDevice *device, GError **error) { if (fu_device_has_flag(FU_DEVICE(device), FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { fu_hid_device_set_interface(FU_HID_DEVICE(device), 0); fu_hid_device_set_ep_addr_in(FU_HID_DEVICE(device), 0x81); fu_hid_device_set_ep_addr_out(FU_HID_DEVICE(device), 1); } else { fu_hid_device_set_interface(FU_HID_DEVICE(device), 3); fu_hid_device_set_ep_addr_in(FU_HID_DEVICE(device), 0x84); fu_hid_device_set_ep_addr_out(FU_HID_DEVICE(device), 0x4); } /* FuHidDevice->probe */ if (!FU_DEVICE_CLASS(fu_legion_hid2_device_parent_class)->probe(device, error)) return FALSE; /* success */ return TRUE; } /* * older MCU firmware doesn't support TP child commands, so setup needs to * to be non-fatal or the MCU won't enumerate. */ static void fu_legion_hid2_device_setup_child(FuLegionHid2Device *self) { g_autoptr(FuDevice) child = fu_legion_hid2_child_device_new(FU_DEVICE(self)); g_autoptr(GError) error_child = NULL; if (!fu_device_probe(child, &error_child)) { g_info("%s", error_child->message); return; } if (!fu_device_setup(child, &error_child)) { g_info("%s", error_child->message); return; } fu_device_add_child(FU_DEVICE(self), child); } static gboolean fu_legion_hid2_device_setup(FuDevice *device, GError **error) { /* HidDevice->setup */ if (!FU_DEVICE_CLASS(fu_legion_hid2_device_parent_class)->setup(device, error)) return FALSE; /* can't use anything but write and reset in IAP mode */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) return TRUE; if (!fu_legion_hid2_device_ensure_version(device, error)) return FALSE; if (!fu_legion_hid2_device_ensure_mcu_id(device, error)) return FALSE; fu_legion_hid2_device_setup_child(FU_LEGION_HID2_DEVICE(device)); /* success */ return TRUE; } static FuFirmware * fu_legion_hid2_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { guint32 version; g_autofree gchar *version_str = NULL; g_autoptr(FuFirmware) firmware = fu_legion_hid2_firmware_new(); /* sanity check */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; version = fu_legion_hid2_firmware_get_version(firmware); if (fu_device_get_version_raw(device) > version) { version_str = fu_version_from_uint32(version, FWUPD_VERSION_FORMAT_QUAD); if ((flags & FWUPD_INSTALL_FLAG_FORCE) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "downgrading from %s to %s is not supported", fu_device_get_version(device), version_str); return NULL; } g_warning("firmware %s is a downgrade but is being force installed anyway", version_str); } return g_steal_pointer(&firmware); } static GByteArray * fu_legion_hid2_device_tlv(FuLegionHid2Device *self, GByteArray *cmd, GError **error) { g_autoptr(GByteArray) result = fu_struct_legion_iap_tlv_new(); const guint8 *value; guint8 expected; guint16 tag; if (fu_struct_legion_iap_tlv_get_tag(cmd) == FU_LEGION_IAP_HOST_TAG_IAP_UPDATE) expected = FU_LEGION_IAP_ERROR_IAP_CERTIFIED; else expected = FU_LEGION_IAP_ERROR_IAP_OK; if (!fu_legion_hid2_device_transfer(self, cmd, result, error)) return NULL; tag = fu_struct_legion_iap_tlv_get_tag(result); if (tag != FU_LEGION_IAP_DEVICE_TAG_IAP_ACK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to transmit TLV, result: %u", tag); return NULL; } value = fu_struct_legion_iap_tlv_get_value(result, NULL); if (value[0] != expected) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to transmit TLV, data: %u", value[0]); return NULL; } return g_steal_pointer(&result); } static gboolean fu_legion_hid2_device_unlock_flash(FuLegionHid2Device *self, GError **error) { g_autoptr(GByteArray) cmd = fu_struct_legion_iap_tlv_new(); g_autoptr(GByteArray) result = NULL; fu_struct_legion_iap_tlv_set_tag(cmd, FU_LEGION_IAP_HOST_TAG_IAP_UNLOCK); result = fu_legion_hid2_device_tlv(self, cmd, error); if (result == NULL) { g_prefix_error(error, "failed to unlock: "); return FALSE; } return TRUE; } static gboolean fu_legion_hid2_device_verify_signature(FuLegionHid2Device *self, GError **error) { g_autoptr(GByteArray) cmd = fu_struct_legion_iap_tlv_new(); g_autoptr(GByteArray) result = NULL; fu_struct_legion_iap_tlv_set_tag(cmd, FU_LEGION_IAP_HOST_TAG_IAP_UPDATE); result = fu_legion_hid2_device_tlv(self, cmd, error); if (result == NULL) { g_prefix_error(error, "failed to verify signature: "); return FALSE; } return TRUE; } static gboolean fu_legion_hid2_device_verify_code(FuLegionHid2Device *self, GError **error) { g_autoptr(GByteArray) cmd = fu_struct_legion_iap_tlv_new(); g_autoptr(GByteArray) result = NULL; fu_struct_legion_iap_tlv_set_tag(cmd, FU_LEGION_IAP_HOST_TAG_IAP_VERIFY); result = fu_legion_hid2_device_tlv(self, cmd, error); if (result == NULL) { g_prefix_error(error, "failed to verify code: "); return FALSE; } return TRUE; } static gboolean fu_legion_hid2_device_write_data_chunks(FuLegionHid2Device *self, FuChunkArray *chunks, FuProgress *progress, guint16 tag, GError **error) { fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GByteArray) req = fu_struct_legion_iap_tlv_new(); g_autoptr(GByteArray) res = NULL; fu_struct_legion_iap_tlv_set_tag(req, tag); chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_struct_legion_iap_tlv_set_value(req, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; fu_struct_legion_iap_tlv_set_length(req, fu_chunk_get_data_sz(chk)); res = fu_legion_hid2_device_tlv(self, req, error); if (res == NULL) { g_prefix_error(error, "failed to write data chunks: "); return FALSE; } fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_legion_hid2_device_wait_for_complete_cb(FuDevice *device, gpointer user_data, GError **error) { g_autoptr(GByteArray) cmd = fu_struct_legion_iap_tlv_new(); g_autoptr(GByteArray) result = NULL; const guint8 *value; fu_struct_legion_iap_tlv_set_tag(cmd, FU_LEGION_IAP_HOST_TAG_IAP_CARRY); result = fu_legion_hid2_device_tlv(FU_LEGION_HID2_DEVICE(device), cmd, error); if (result == NULL) { g_prefix_error(error, "failed to verify code: "); return FALSE; } value = fu_struct_legion_iap_tlv_get_value(result, NULL); if (value[1] < 100) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "device is %d percent done", value[1]); return FALSE; } return TRUE; } static gboolean fu_legion_hid2_device_write_data(FuLegionHid2Device *self, FuFirmware *firmware, FuProgress *progress, GError **error) { g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; stream = fu_firmware_get_image_by_id_stream(firmware, FU_FIRMWARE_ID_PAYLOAD, error); if (stream == NULL) return FALSE; chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_STRUCT_LEGION_IAP_TLV_SIZE_VALUE, error); if (chunks == NULL) return FALSE; if (!fu_legion_hid2_device_write_data_chunks(self, chunks, progress, FU_LEGION_IAP_HOST_TAG_IAP_DATA, error)) return FALSE; return TRUE; } static gboolean fu_legion_hid2_device_write_sig(FuLegionHid2Device *self, FuFirmware *firmware, FuProgress *progress, GError **error) { g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; stream = fu_firmware_get_image_by_id_stream(firmware, FU_FIRMWARE_ID_SIGNATURE, error); if (stream == NULL) return FALSE; chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_STRUCT_LEGION_IAP_TLV_SIZE_VALUE, error); if (chunks == NULL) return FALSE; if (!fu_legion_hid2_device_write_data_chunks(self, chunks, progress, FU_LEGION_IAP_HOST_TAG_IAP_SIGNATURE, error)) return FALSE; return TRUE; } static gboolean fu_legion_hid2_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuLegionHid2Device *self = FU_LEGION_HID2_DEVICE(device); g_return_val_if_fail(device != NULL, FALSE); g_return_val_if_fail(FU_IS_FIRMWARE(firmware), FALSE); fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 29, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 29, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 19, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 19, NULL); if (!fu_legion_hid2_device_unlock_flash(self, error)) return FALSE; fu_progress_step_done(progress); if (!fu_legion_hid2_device_write_data(self, firmware, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); if (!fu_legion_hid2_device_write_sig(self, firmware, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); if (!fu_legion_hid2_device_verify_signature(self, error)) return FALSE; fu_progress_step_done(progress); if (!fu_device_retry_full(device, fu_legion_hid2_device_wait_for_complete_cb, 50, 200, NULL, error)) return FALSE; fu_progress_step_done(progress); if (!fu_legion_hid2_device_verify_code(self, error)) return FALSE; fu_progress_step_done(progress); /* restart dev is moved to attach command! */ return TRUE; } static gboolean fu_legion_hid2_device_detach(FuDevice *device, FuProgress *progress, GError **error) { g_autoptr(GByteArray) cmd = NULL; g_autoptr(GByteArray) result = NULL; guint ret; if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) return TRUE; cmd = fu_struct_legion_start_iap_new(); result = fu_struct_legion_iap_result_new(); if (!fu_legion_hid2_device_transfer(FU_LEGION_HID2_DEVICE(device), cmd, result, error)) return FALSE; ret = fu_struct_legion_iap_result_get_ret(result); if (ret != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to enable IAP, result: %u", ret); return FALSE; } fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_legion_hid2_device_attach(FuDevice *device, FuProgress *progress, GError **error) { g_autoptr(GByteArray) cmd = NULL; g_autoptr(GByteArray) result = NULL; g_autoptr(GError) error_attach = NULL; if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) return TRUE; cmd = fu_struct_legion_iap_tlv_new(); fu_struct_legion_iap_tlv_set_tag(cmd, FU_LEGION_IAP_HOST_TAG_IAP_RESTART); result = fu_legion_hid2_device_tlv(FU_LEGION_HID2_DEVICE(device), cmd, &error_attach); if (result == NULL) g_debug("failed to attach: %s", error_attach->message); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static void fu_legion_hid2_device_init(FuLegionHid2Device *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_protocol(FU_DEVICE(self), "com.lenovo.legion-hid2"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); } static void fu_legion_hid2_device_class_init(FuLegionHid2DeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_legion_hid2_device_setup; device_class->probe = fu_legion_hid2_device_probe; device_class->prepare_firmware = fu_legion_hid2_device_prepare_firmware; device_class->convert_version = fu_legion_hid2_device_convert_version; device_class->write_firmware = fu_legion_hid2_device_write_firmware; device_class->detach = fu_legion_hid2_device_detach; device_class->attach = fu_legion_hid2_device_attach; device_class->set_progress = fu_legion_hid2_device_set_progress; } fwupd-2.0.10/plugins/legion-hid2/fu-legion-hid2-device.h000066400000000000000000000005041501337203100226120ustar00rootroot00000000000000/* * Copyright 2024 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_LEGION_HID2_DEVICE (fu_legion_hid2_device_get_type()) G_DECLARE_FINAL_TYPE(FuLegionHid2Device, fu_legion_hid2_device, FU, LEGION_HID2_DEVICE, FuHidDevice) fwupd-2.0.10/plugins/legion-hid2/fu-legion-hid2-firmware.c000066400000000000000000000057121501337203100231700ustar00rootroot00000000000000/* * Copyright 2024 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-legion-hid2-firmware.h" #include "fu-legion-hid2-struct.h" #define VERSION_OFFSET 0x1e0 struct _FuLegionHid2Firmware { FuFirmware parent_instance; guint32 version; }; G_DEFINE_TYPE(FuLegionHid2Firmware, fu_legion_hid2_firmware, FU_TYPE_FIRMWARE) static void fu_legion_hid2_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuLegionHid2Firmware *self = FU_LEGION_HID2_FIRMWARE(firmware); g_autofree gchar *version = fu_version_from_uint32(self->version, FWUPD_VERSION_FORMAT_QUAD); fu_xmlb_builder_insert_kv(bn, "version", version); } guint32 fu_legion_hid2_firmware_get_version(FuFirmware *firmware) { FuLegionHid2Firmware *self = FU_LEGION_HID2_FIRMWARE(firmware); return self->version; } static gboolean fu_legion_hid2_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuLegionHid2Firmware *self = FU_LEGION_HID2_FIRMWARE(firmware); g_autoptr(FuFirmware) img_payload = fu_firmware_new(); g_autoptr(FuFirmware) img_sig = fu_firmware_new(); g_autoptr(GInputStream) stream_payload = NULL; g_autoptr(GInputStream) stream_sig = NULL; g_autoptr(GByteArray) header = NULL; g_autoptr(GByteArray) version = NULL; header = fu_struct_legion_hid2_header_parse_stream(stream, 0x0, error); if (header == NULL) return FALSE; stream_sig = fu_partial_input_stream_new(stream, fu_struct_legion_hid2_header_get_sig_add(header), fu_struct_legion_hid2_header_get_sig_len(header), error); if (stream_sig == NULL) return FALSE; if (!fu_firmware_parse_stream(img_sig, stream_sig, 0x0, flags, error)) return FALSE; fu_firmware_set_id(img_sig, FU_FIRMWARE_ID_SIGNATURE); fu_firmware_add_image(firmware, img_sig); stream_payload = fu_partial_input_stream_new(stream, fu_struct_legion_hid2_header_get_data_add(header), fu_struct_legion_hid2_header_get_data_len(header), error); if (stream_payload == NULL) return FALSE; if (!fu_firmware_parse_stream(img_payload, stream_payload, 0x0, flags, error)) return FALSE; fu_firmware_set_id(img_payload, FU_FIRMWARE_ID_PAYLOAD); fu_firmware_add_image(firmware, img_payload); version = fu_struct_legion_hid2_version_parse_stream(stream, VERSION_OFFSET, error); if (version == NULL) return FALSE; self->version = fu_struct_legion_hid2_version_get_version(version); return TRUE; } static void fu_legion_hid2_firmware_init(FuLegionHid2Firmware *self) { } static void fu_legion_hid2_firmware_class_init(FuLegionHid2FirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_legion_hid2_firmware_parse; firmware_class->export = fu_legion_hid2_firmware_export; } FuFirmware * fu_legion_hid2_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_LEGION_HID2_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/legion-hid2/fu-legion-hid2-firmware.h000066400000000000000000000007361501337203100231760ustar00rootroot00000000000000/* * Copyright 2024 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_LEGION_HID2_FIRMWARE (fu_legion_hid2_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuLegionHid2Firmware, fu_legion_hid2_firmware, FU, LEGION_HID2_FIRMWARE, FuFirmware) FuFirmware * fu_legion_hid2_firmware_new(void); guint32 fu_legion_hid2_firmware_get_version(FuFirmware *firmware); fwupd-2.0.10/plugins/legion-hid2/fu-legion-hid2-plugin.c000066400000000000000000000020551501337203100226470ustar00rootroot00000000000000/* * Copyright 2024 Mario Limonciello * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-legion-hid2-child-device.h" #include "fu-legion-hid2-device.h" #include "fu-legion-hid2-firmware.h" #include "fu-legion-hid2-plugin.h" struct _FuLegionHid2Plugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuLegionHid2Plugin, fu_legion_hid2_plugin, FU_TYPE_PLUGIN) static void fu_legion_hid2_plugin_init(FuLegionHid2Plugin *self) { } static void fu_legion_hid2_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_LEGION_HID2_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_LEGION_HID2_CHILD_DEVICE); fu_plugin_set_device_gtype_default(plugin, FU_TYPE_LEGION_HID2_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_LEGION_HID2_FIRMWARE); } static void fu_legion_hid2_plugin_class_init(FuLegionHid2PluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_legion_hid2_plugin_constructed; } fwupd-2.0.10/plugins/legion-hid2/fu-legion-hid2-plugin.h000066400000000000000000000003731501337203100226550ustar00rootroot00000000000000/* * Copyright 2024 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuLegionHid2Plugin, fu_legion_hid2_plugin, FU, LEGION_HID2_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/legion-hid2/fu-legion-hid2.rs000066400000000000000000000052771501337203100215660ustar00rootroot00000000000000// Copyright 2024 Mario Limonciello // SPDX-License-Identifier: LGPL-2.1-or-later // Commands sent ove rXInput, DInput #[repr(u32)] enum FuLegionHid2Command { GetVersion = 0x01, GetMcuId = 0x02, GetPlTest = 0xDF, StartIap = 0xE1, IcReset = 0xEF, } #[repr(u8)] enum FuLegionHid2PlTest { TpManufacturer = 0x2, AgSensorManufacturer = 0x3, TpVersion = 0x4, } #[repr(u8)] enum FuLegionHid2TpMan { None = 0x0, BetterLife = 0x1, Sipo = 0x2, } #[repr(u8)] enum FuLegionHid2ReportId { Iap = 0x1, Communication = 0x4, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructLegionGetVersion { cmd: u8 == 0x01, } #[derive(New, Getters)] #[repr(C, packed)] struct FuStructLegionVersion { command: u8, version: u32le, reserved: [u8; 59], } #[derive(New, Default)] #[repr(C, packed)] struct FuStructLegionGetMcuId { cmd: u8 == 0x02, } #[derive(New, Getters)] #[repr(C, packed)] struct FuStructLegionMcuId { id: [u8; 12], reserved: [u8; 52], } #[derive(New, Default)] #[repr(C, packed)] struct FuStructLegionGetPlTest { cmd: u8 == 0xDF, index: u8, } #[derive(New, Getters)] #[repr(C, packed)] struct FuStructLegionGetPlTestResult { cmd: u8, index: u8, content: u8, reserved: [u8; 61], } #[derive(New, Default)] #[repr(C, packed)] struct FuStructLegionStartIap { cmd: u8 == 0xE1, data: [char; 7] == "UPGRADE", reserved: [u8; 57], } #[derive(New, Getters)] #[repr(C, packed)] struct FuStructLegionIapResult { ret: u8, reserved: [u8; 63], } //Reset in app mode, unused by fwupd //#[derive(New)] //#[repr(C, packed)] //struct FuStructLegionIcReset { // cmd: u8 == 0xEF, // data: [char; 7] == "ICRESET", // reserved: [u8; 57], //} // IAP commands enum FuLegionIapHostTag { IapUnlock = 0x5A80, IapData = 0x5A81, IapSignature = 0x5A82, IapUpdate = 0x5A83, IapCarry = 0x5A84, IapVerify = 0x5A85, IapRestart = 0x5A86, } enum FuLegionIapDeviceTag { IapAck = 0xA510, } enum FuLegionIapError { IapOk = 0x00, IapErr = 0x01, IapCertified = 0x02, IapNotCertified = 0x03, } #[derive(New, Setters, Getters)] #[repr(C, packed)] struct FuStructLegionIapTlv { tag: u16le, length: u16le, value: [u8; 60], } // Parsing firmware update header #[derive(Getters, ParseStream, Default)] #[repr(C, packed)] struct FuStructLegionHid2Header { magic: [char; 7] == "#Legion", reserved: [u8; 7], sig_add: u32le, sig_len: u32le, data_add: u32le, data_len: u32le, } #[derive(Getters, ParseStream, Default)] #[repr(C, packed)] struct FuStructLegionHid2Version { signature: [char; 7] == "VERSION", reserved: u8, version: u32le, } fwupd-2.0.10/plugins/legion-hid2/legion-hid2.quirk000066400000000000000000000010021501337203100216430ustar00rootroot00000000000000# Xinput [USB\VID_1A86&PID_E310] Plugin = legion_hid2 GType = FuLegionHid2Device CounterpartGuid = USB\VID_1A86&PID_FE10 FirmwareSizeMax = 0x2C000 Flags = use-runtime-version InstallDuration = 15 # Dinput [USB\VID_1A86&PID_E311] Plugin = legion_hid2 GType = FuLegionHid2Device CounterpartGuid = USB\VID_1A86&PID_FE10 FirmwareSizeMax = 0x2C000 Flags = use-runtime-version InstallDuration = 15 # IAP [USB\VID_1A86&PID_FE10] Plugin = legion_hid2 Flags = is-bootloader FirmwareSizeMax = 0x2C000 InstallDuration = 15 fwupd-2.0.10/plugins/legion-hid2/meson.build000066400000000000000000000010641501337203100206370ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginLegionHid2"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('legion-hid2.quirk') plugin_builtins += static_library('fu_plugin_legion_hid2', rustgen.process('fu-legion-hid2.rs'), sources: [ 'fu-legion-hid2-device.c', 'fu-legion-hid2-child-device.c', 'fu-legion-hid2-plugin.c', 'fu-legion-hid2-firmware.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files('tests/legion-hid2.json') fwupd-2.0.10/plugins/legion-hid2/tests/000077500000000000000000000000001501337203100176365ustar00rootroot00000000000000fwupd-2.0.10/plugins/legion-hid2/tests/legion-hid2.json000066400000000000000000000007131501337203100226330ustar00rootroot00000000000000{ "name": "Legion Hid2 device", "interactive": false, "steps": [ { "url": "7976f433d469946007e29682c84f87d87696c4a667d3f82fb2e22dd2d15f2d6c-0_0_3_6.cab", "emulation-url": "8897ce2e5215fdd13d033aff1700253afe84231f47cd34b9d2ecf797682eb3ab-legionhid2_0036.zip", "components": [ { "version": "0.0.3.6", "guids": [ "65619675-fec6-5035-801d-7f5e59fd9749" ] } ] } ] } fwupd-2.0.10/plugins/lenovo-thinklmi/000077500000000000000000000000001501337203100175125ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/README.md000066400000000000000000000006051501337203100207720ustar00rootroot00000000000000--- title: Plugin: Lenovo ThinkLMI --- ## Introduction This allows checking whether the firmware on a Lenovo system is configured to allow UEFI capsule updates using the thinklmi kernel module. ## External Interface Access This plugin requires: * read access to `/sys/class/firmware-attributes`. ## Version Considerations This plugin has been available since fwupd version `1.6.2`. fwupd-2.0.10/plugins/lenovo-thinklmi/fu-lenovo-thinklmi-plugin.c000066400000000000000000000100561501337203100247030ustar00rootroot00000000000000/* * Copyright 2021 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-lenovo-thinklmi-plugin.h" struct _FuLenovoThinklmiPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuLenovoThinklmiPlugin, fu_lenovo_thinklmi_plugin, FU_TYPE_PLUGIN) #define BIOS_SETTING_SLEEP_MODE "com.thinklmi.SleepState" #define BIOS_SETTING_BOOT_ORDER_LOCK "com.thinklmi.BootOrderLock" #define BIOS_SETTING_SECURE_ROLLBACK "com.thinklmi.SecureRollBackPrevention" static gboolean fu_lenovo_thinklmi_plugin_startup(FuPlugin *plugin, FuProgress *progress, GError **error) { const gchar *hwid = fu_context_get_hwid_value(fu_plugin_get_context(plugin), FU_HWIDS_KEY_MANUFACTURER); if (g_strcmp0(hwid, "LENOVO") != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported manufacturer"); return FALSE; } return TRUE; } static void fu_lenovo_thinklmi_plugin_cpu_registered(FuContext *ctx, FuDevice *device) { /* Ryzen 6000 doesn't support S3 even if the BIOS offers it */ if (fu_device_has_instance_id(device, "CPUID\\PRO_0&FAM_19&MOD_44", FU_DEVICE_INSTANCE_FLAG_VISIBLE)) { FwupdBiosSetting *attr = fu_context_get_bios_setting(ctx, BIOS_SETTING_SLEEP_MODE); if (attr != NULL) { g_debug("setting %s to read-only", fwupd_bios_setting_get_name(attr)); fwupd_bios_setting_set_read_only(attr, TRUE); } } } static void fu_lenovo_thinklmi_plugin_uefi_capsule_registered(FuContext *ctx, FuDevice *device) { FwupdBiosSetting *attr; /* check if boot order lock is turned on */ attr = fu_context_get_bios_setting(ctx, BIOS_SETTING_BOOT_ORDER_LOCK); if (attr == NULL) { g_debug("failed to find %s in cache", BIOS_SETTING_BOOT_ORDER_LOCK); return; } if (g_strcmp0(fwupd_bios_setting_get_current_value(attr), "Enable") == 0) { fu_device_inhibit(device, "uefi-capsule-bootorder", "BootOrder is locked in firmware setup"); } /* check if we're pending for a reboot */ if (fu_context_get_bios_setting_pending_reboot(ctx)) { fu_device_inhibit(device, "uefi-capsule-pending-reboot", "UEFI BIOS settings update pending reboot"); } } static void fu_lenovo_thinklmi_plugin_device_registered(FuPlugin *plugin, FuDevice *device) { if (g_strcmp0(fu_device_get_plugin(device), "uefi_capsule") == 0) { fu_lenovo_thinklmi_plugin_uefi_capsule_registered(fu_plugin_get_context(plugin), device); } else if (g_strcmp0(fu_device_get_plugin(device), "cpu") == 0) { fu_lenovo_thinklmi_plugin_cpu_registered(fu_plugin_get_context(plugin), device); } } static void fu_lenovo_thinklmi_plugin_add_security_attrs(FuPlugin *plugin, FuSecurityAttrs *attrs) { FwupdBiosSetting *bios_attr; FuContext *ctx = fu_plugin_get_context(plugin); g_autoptr(FwupdSecurityAttr) attr = NULL; bios_attr = fu_context_get_bios_setting(ctx, BIOS_SETTING_SECURE_ROLLBACK); if (bios_attr == NULL) { g_debug("failed to find %s in cache", BIOS_SETTING_SECURE_ROLLBACK); return; } attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_BIOS_ROLLBACK_PROTECTION); fu_security_attr_add_bios_target_value(attr, BIOS_SETTING_SECURE_ROLLBACK, "enable"); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_ENABLED); fu_security_attrs_append(attrs, attr); if (g_strcmp0(fwupd_bios_setting_get_current_value(bios_attr), "Disable") == 0) { fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); return; } fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_lenovo_thinklmi_plugin_init(FuLenovoThinklmiPlugin *self) { } static void fu_lenovo_thinklmi_plugin_class_init(FuLenovoThinklmiPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->startup = fu_lenovo_thinklmi_plugin_startup; plugin_class->device_registered = fu_lenovo_thinklmi_plugin_device_registered; plugin_class->add_security_attrs = fu_lenovo_thinklmi_plugin_add_security_attrs; } fwupd-2.0.10/plugins/lenovo-thinklmi/fu-lenovo-thinklmi-plugin.h000066400000000000000000000004421501337203100247060ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuLenovoThinklmiPlugin, fu_lenovo_thinklmi_plugin, FU, LENOVO_THINKLMI_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/lenovo-thinklmi/fu-self-test.c000066400000000000000000000156541501337203100222070ustar00rootroot00000000000000/* * Copyright 2021 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "../uefi-capsule/fu-uefi-capsule-plugin.h" #include "fu-bios-settings-private.h" #include "fu-context-private.h" #include "fu-device-private.h" #include "fu-lenovo-thinklmi-plugin.h" #include "fu-plugin-private.h" typedef struct { FuContext *ctx; FuPlugin *plugin_uefi_capsule; FuPlugin *plugin_lenovo_thinklmi; } FuTest; static void fu_test_plugin_device_added_cb(FuPlugin *plugin, FuDevice *device, gpointer user_data) { FuDevice **dev = (FuDevice **)user_data; *dev = device; } static gboolean fu_test_fatal_handler_cb(const gchar *log_domain, GLogLevelFlags log_level, const gchar *message, gpointer user_data) { return log_level >= G_LOG_LEVEL_MESSAGE; } static gboolean fu_test_self_init(FuTest *self, GError **error) { gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_test_log_set_fatal_handler(fu_test_fatal_handler_cb, NULL); ret = fu_context_load_quirks(ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE | FU_QUIRKS_LOAD_FLAG_NO_VERIFY, error); g_assert_no_error(*error); g_assert_true(ret); ret = fu_context_load_hwinfo(ctx, progress, FU_CONTEXT_HWID_FLAG_LOAD_CONFIG, error); g_assert_no_error(*error); g_assert_true(ret); ret = fu_context_reload_bios_settings(ctx, error); g_assert_no_error(*error); g_assert_true(ret); self->plugin_uefi_capsule = fu_plugin_new_from_gtype(fu_uefi_capsule_plugin_get_type(), ctx); ret = fu_plugin_runner_startup(self->plugin_uefi_capsule, progress, error); g_assert_no_error(*error); g_assert_true(ret); self->plugin_lenovo_thinklmi = fu_plugin_new_from_gtype(fu_lenovo_thinklmi_plugin_get_type(), ctx); ret = fu_plugin_runner_startup(self->plugin_lenovo_thinklmi, progress, error); g_assert_no_error(*error); g_assert_true(ret); self->ctx = fu_plugin_get_context(self->plugin_lenovo_thinklmi); return TRUE; } static FuDevice * fu_test_probe_fake_esrt(FuTest *self) { gboolean ret; gulong added_id; FuDevice *dev = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; added_id = g_signal_connect(FU_PLUGIN(self->plugin_uefi_capsule), "device-added", G_CALLBACK(fu_test_plugin_device_added_cb), &dev); ret = fu_plugin_runner_coldplug(self->plugin_uefi_capsule, progress, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_nonnull(dev); g_assert_true(fu_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE)); g_signal_handler_disconnect(self->plugin_uefi_capsule, added_id); return g_object_ref(dev); } static void fu_plugin_lenovo_thinklmi_bootorder_locked(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) dev = NULL; g_autoptr(GError) error = NULL; g_autofree gchar *test_dir = g_test_build_filename(G_TEST_DIST, "tests", "firmware-attributes", "locked", NULL); (void)g_setenv("FWUPD_SYSFSFWATTRIBDIR", test_dir, TRUE); ret = fu_context_reload_bios_settings(self->ctx, &error); g_assert_no_error(error); g_assert_true(ret); dev = fu_test_probe_fake_esrt(self); fu_plugin_runner_device_register(self->plugin_lenovo_thinklmi, dev); g_assert_true(fu_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN)); } static void fu_plugin_lenovo_thinklmi_bootorder_unlocked(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) dev = NULL; g_autoptr(GError) error = NULL; g_autofree gchar *test_dir = g_test_build_filename(G_TEST_DIST, "tests", "firmware-attributes", "unlocked", NULL); (void)g_setenv("FWUPD_SYSFSFWATTRIBDIR", test_dir, TRUE); ret = fu_context_reload_bios_settings(self->ctx, &error); g_assert_no_error(error); g_assert_true(ret); dev = fu_test_probe_fake_esrt(self); fu_plugin_runner_device_register(self->plugin_lenovo_thinklmi, dev); g_assert_true(fu_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE)); } static void fu_plugin_lenovo_thinklmi_reboot_pending(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) dev = NULL; g_autoptr(GError) error = NULL; g_autofree gchar *test_dir = g_test_build_filename(G_TEST_DIST, "tests", "firmware-attributes", "reboot-pending", NULL); (void)g_setenv("FWUPD_SYSFSFWATTRIBDIR", test_dir, TRUE); ret = fu_context_reload_bios_settings(self->ctx, &error); g_assert_no_error(error); g_assert_true(ret); dev = fu_test_probe_fake_esrt(self); fu_plugin_runner_device_register(self->plugin_lenovo_thinklmi, dev); g_assert_true(fu_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN)); } static void fu_test_self_free(FuTest *self) { if (self->plugin_uefi_capsule != NULL) g_object_unref(self->plugin_uefi_capsule); if (self->plugin_lenovo_thinklmi != NULL) g_object_unref(self->plugin_lenovo_thinklmi); g_free(self); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuTest, fu_test_self_free) #pragma clang diagnostic pop int main(int argc, char **argv) { g_autofree gchar *sysfsdir = NULL; g_autofree gchar *testdatadir = NULL; g_autofree gchar *confdir = NULL; g_autofree gchar *test_dir = NULL; g_autoptr(FuTest) self = g_new0(FuTest, 1); g_autoptr(GError) error = NULL; (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); /* starting thinklmi dir to make startup pass */ test_dir = g_test_build_filename(G_TEST_DIST, "tests", "firmware-attributes", "locked", NULL); (void)g_setenv("FWUPD_SYSFSFWATTRIBDIR", test_dir, TRUE); /* starting ESRT path */ testdatadir = g_test_build_filename(G_TEST_DIST, "tests", NULL); (void)g_setenv("FWUPD_SYSFSFWDIR", testdatadir, TRUE); /* change behavior of UEFI plugin for test mode */ sysfsdir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR_FW); (void)g_setenv("FWUPD_UEFI_ESP_PATH", sysfsdir, TRUE); (void)g_setenv("FWUPD_UEFI_TEST", "1", TRUE); /* to load fwupd.conf */ confdir = g_test_build_filename(G_TEST_DIST, "tests", "etc", "fwupd", NULL); (void)g_setenv("CONFIGURATION_DIRECTORY", confdir, TRUE); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); g_assert_cmpint(g_mkdir_with_parents("/tmp/fwupd-self-test/var/lib/fwupd", 0755), ==, 0); /* tests go here */ if (!fu_test_self_init(self, &error)) { g_test_skip(error->message); return 0; } g_test_add_data_func("/fwupd/plugin{lenovo-think-lmi:bootorder-locked}", self, fu_plugin_lenovo_thinklmi_bootorder_locked); g_test_add_data_func("/fwupd/plugin{lenovo-think-lmi:bootorder-unlocked}", self, fu_plugin_lenovo_thinklmi_bootorder_unlocked); g_test_add_data_func("/fwupd/plugin{lenovo-think-lmi:reboot-pending}", self, fu_plugin_lenovo_thinklmi_reboot_pending); return g_test_run(); } fwupd-2.0.10/plugins/lenovo-thinklmi/meson.build000066400000000000000000000021261501337203100216550ustar00rootroot00000000000000if allow_uefi_capsule cargs = ['-DG_LOG_DOMAIN="FuPluginLenovoThinkLmi"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_builtin_lenovo_thinklmi = static_library('fu_plugin_lenovo_thinklmi', sources: [ 'fu-lenovo-thinklmi-plugin.c', ], include_directories: plugin_incdirs, c_args: [ cargs, ], link_with: plugin_libs, dependencies: plugin_deps, ) plugin_builtins += plugin_builtin_lenovo_thinklmi if get_option('tests') env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) env.set('FWUPD_LOCALSTATEDIR', '/tmp/fwupd-self-test/var') e = executable( 'lenovo-thinklmi-self-test', sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: plugin_deps, link_with: [ fwupd, fwupdplugin, plugin_builtin_lenovo_thinklmi, plugin_builtin_uefi_capsule, ], c_args: [ cargs, '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('lenovo-thinklmi-self-test', e, env: env) endif endif fwupd-2.0.10/plugins/lenovo-thinklmi/tests/000077500000000000000000000000001501337203100206545ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/efi/000077500000000000000000000000001501337203100214175ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/efi/efivars/000077500000000000000000000000001501337203100230565ustar00rootroot00000000000000OsIndicationsSupported-8be4df61-93ca-11d2-aa0d-00e098032b8c000066400000000000000000000000141501337203100342640ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/efi/efivarsfwupd-ddc0ee61-e7f0-4e7d-acc5-c070a398838e-0-0abba7dc-e516-4167-bbf5-4d9d1c739416000066400000000000000000000003461501337203100362130ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/efi/efivars Ù{iÏ©Mƒ…™i ¼eY*@Zª—ZÕ~I™ Ê5MÈm†\EFI\fedora\fw\fwupd-697bd920-12cf-4da9-8385-996909bc6559.capÿfwupd-2.0.10/plugins/lenovo-thinklmi/tests/efi/esrt/000077500000000000000000000000001501337203100223745ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/efi/esrt/entries/000077500000000000000000000000001501337203100240455ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/efi/esrt/entries/entry0/000077500000000000000000000000001501337203100252665ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/efi/esrt/entries/entry0/capsule_flags000066400000000000000000000000051501337203100300140ustar00rootroot000000000000000xfe fwupd-2.0.10/plugins/lenovo-thinklmi/tests/efi/esrt/entries/entry0/fw_class000066400000000000000000000000451501337203100270110ustar00rootroot00000000000000ddc0ee61-e7f0-4e7d-acc5-c070a398838e fwupd-2.0.10/plugins/lenovo-thinklmi/tests/efi/esrt/entries/entry0/fw_type000066400000000000000000000000021501337203100266560ustar00rootroot000000000000001 fwupd-2.0.10/plugins/lenovo-thinklmi/tests/efi/esrt/entries/entry0/fw_version000066400000000000000000000000061501337203100273660ustar00rootroot0000000000000065586 fwupd-2.0.10/plugins/lenovo-thinklmi/tests/efi/esrt/entries/entry0/last_attempt_status000066400000000000000000000000021501337203100313050ustar00rootroot000000000000001 fwupd-2.0.10/plugins/lenovo-thinklmi/tests/efi/esrt/entries/entry0/last_attempt_version000066400000000000000000000000111501337203100314470ustar00rootroot0000000000000018472960 fwupd-2.0.10/plugins/lenovo-thinklmi/tests/efi/esrt/entries/entry0/lowest_supported_fw_version000066400000000000000000000000061501337203100330700ustar00rootroot0000000000000065582 fwupd-2.0.10/plugins/lenovo-thinklmi/tests/efi/fw_platform_size000066400000000000000000000000031501337203100247050ustar00rootroot0000000000000064 fwupd-2.0.10/plugins/lenovo-thinklmi/tests/etc/000077500000000000000000000000001501337203100214275ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/etc/fwupd/000077500000000000000000000000001501337203100225545ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/etc/fwupd/fwupd.conf000066400000000000000000000000341501337203100245450ustar00rootroot00000000000000[fwupd] Manufacturer=LENOVO fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/000077500000000000000000000000001501337203100246545ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/locked/000077500000000000000000000000001501337203100261155ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/locked/thinklmi/000077500000000000000000000000001501337203100277345ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/locked/thinklmi/attributes/000077500000000000000000000000001501337203100321225ustar00rootroot00000000000000BootOrderLock/000077500000000000000000000000001501337203100345535ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/locked/thinklmi/attributescurrent_value000066400000000000000000000000071501337203100373510ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/locked/thinklmi/attributes/BootOrderLockEnable display_name000066400000000000000000000000161501337203100371400ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/locked/thinklmi/attributes/BootOrderLockBootOrderLock possible_values000066400000000000000000000000171501337203100376730ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/locked/thinklmi/attributes/BootOrderLockEnable;Disable type000066400000000000000000000000141501337203100354520ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/locked/thinklmi/attributes/BootOrderLockenumeration pending_reboot000066400000000000000000000000021501337203100347540ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/locked/thinklmi/attributes0 fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/reboot-pending/000077500000000000000000000000001501337203100275705ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/reboot-pending/thinklmi/000077500000000000000000000000001501337203100314075ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/reboot-pending/thinklmi/attributes/000077500000000000000000000000001501337203100335755ustar00rootroot00000000000000BootOrderLock/000077500000000000000000000000001501337203100362265ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/reboot-pending/thinklmi/attributescurrent_value000066400000000000000000000000101501337203100410160ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/reboot-pending/thinklmi/attributes/BootOrderLockDisable display_name000066400000000000000000000000161501337203100406130ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/reboot-pending/thinklmi/attributes/BootOrderLockBootOrderLock possible_values000066400000000000000000000000171501337203100413460ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/reboot-pending/thinklmi/attributes/BootOrderLockEnable;Disable type000066400000000000000000000000141501337203100371250ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/reboot-pending/thinklmi/attributes/BootOrderLockenumeration pending_reboot000066400000000000000000000000021501337203100364270ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/reboot-pending/thinklmi/attributes1 fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/unlocked/000077500000000000000000000000001501337203100264605ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/unlocked/thinklmi/000077500000000000000000000000001501337203100302775ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/unlocked/thinklmi/attributes/000077500000000000000000000000001501337203100324655ustar00rootroot00000000000000BootOrderLock/000077500000000000000000000000001501337203100351165ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/unlocked/thinklmi/attributescurrent_value000066400000000000000000000000101501337203100377060ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/unlocked/thinklmi/attributes/BootOrderLockDisable display_name000066400000000000000000000000161501337203100375030ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/unlocked/thinklmi/attributes/BootOrderLockBootOrderLock possible_values000066400000000000000000000000171501337203100402360ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/unlocked/thinklmi/attributes/BootOrderLockEnable;Disable type000066400000000000000000000000141501337203100360150ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/unlocked/thinklmi/attributes/BootOrderLockenumeration pending_reboot000066400000000000000000000000021501337203100353170ustar00rootroot00000000000000fwupd-2.0.10/plugins/lenovo-thinklmi/tests/firmware-attributes/unlocked/thinklmi/attributes0 fwupd-2.0.10/plugins/linux-display/000077500000000000000000000000001501337203100171755ustar00rootroot00000000000000fwupd-2.0.10/plugins/linux-display/README.md000066400000000000000000000005751501337203100204630ustar00rootroot00000000000000--- title: Plugin: Linux Display --- ## Introduction This plugin checks if there are displays connected to each DRM device. The result will be used to inhibit devices that require a monitor to be attached. ## External Interface Access This plugin requires read access to `/sys/class/drm`. ## Version Considerations This plugin has been available since fwupd version `1.9.6`. fwupd-2.0.10/plugins/linux-display/fu-linux-display-plugin.c000066400000000000000000000065251501337203100240570ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-linux-display-plugin.h" struct _FuLinuxDisplayPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuLinuxDisplayPlugin, fu_linux_display_plugin, FU_TYPE_PLUGIN) static FuDisplayState fu_linux_display_plugin_get_display_state(FuLinuxDisplayPlugin *self) { FuDisplayState display_state = FU_DISPLAY_STATE_DISCONNECTED; GPtrArray *devices = fu_plugin_get_devices(FU_PLUGIN(self)); /* no devices detected */ if (devices->len == 0) return FU_DISPLAY_STATE_UNKNOWN; /* any connected display is good enough */ for (guint i = 0; i < devices->len; i++) { FuDrmDevice *drm_device = g_ptr_array_index(devices, i); if (fu_drm_device_get_state(drm_device) == FU_DISPLAY_STATE_CONNECTED) { display_state = FU_DISPLAY_STATE_CONNECTED; break; } } return display_state; } static void fu_linux_display_plugin_ensure_display_state(FuLinuxDisplayPlugin *self) { FuContext *ctx = fu_plugin_get_context(FU_PLUGIN(self)); if (!fu_plugin_has_flag(FU_PLUGIN(self), FWUPD_PLUGIN_FLAG_READY)) return; fu_context_set_display_state(ctx, fu_linux_display_plugin_get_display_state(self)); } static gboolean fu_linux_display_plugin_plugin_backend_device_added(FuPlugin *plugin, FuDevice *device, FuProgress *progress, GError **error) { FuLinuxDisplayPlugin *self = FU_LINUX_DISPLAY_PLUGIN(plugin); /* not us */ if (!FU_IS_DRM_DEVICE(device)) return TRUE; if (fu_drm_device_get_edid(FU_DRM_DEVICE(device)) != NULL) { if (!fu_device_setup(device, error)) return FALSE; fu_plugin_device_add(plugin, device); } fu_linux_display_plugin_ensure_display_state(self); return TRUE; } static gboolean fu_linux_display_plugin_plugin_ready(FuPlugin *plugin, FuProgress *progress, GError **error) { FuLinuxDisplayPlugin *self = FU_LINUX_DISPLAY_PLUGIN(plugin); fu_linux_display_plugin_ensure_display_state(self); return TRUE; } static gboolean fu_linux_display_plugin_plugin_backend_device_removed(FuPlugin *plugin, FuDevice *device, GError **error) { FuLinuxDisplayPlugin *self = FU_LINUX_DISPLAY_PLUGIN(plugin); fu_linux_display_plugin_ensure_display_state(self); return TRUE; } static gboolean fu_linux_display_plugin_plugin_backend_device_changed(FuPlugin *plugin, FuDevice *device, GError **error) { FuLinuxDisplayPlugin *self = FU_LINUX_DISPLAY_PLUGIN(plugin); if (!FU_IS_DRM_DEVICE(device)) return TRUE; fu_linux_display_plugin_ensure_display_state(self); return TRUE; } static void fu_linux_display_plugin_init(FuLinuxDisplayPlugin *self) { } static void fu_linux_display_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_udev_subsystem(plugin, "drm"); } static void fu_linux_display_plugin_class_init(FuLinuxDisplayPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_linux_display_plugin_constructed; plugin_class->ready = fu_linux_display_plugin_plugin_ready; plugin_class->backend_device_added = fu_linux_display_plugin_plugin_backend_device_added; plugin_class->backend_device_removed = fu_linux_display_plugin_plugin_backend_device_removed; plugin_class->backend_device_changed = fu_linux_display_plugin_plugin_backend_device_changed; } fwupd-2.0.10/plugins/linux-display/fu-linux-display-plugin.h000066400000000000000000000004341501337203100240550ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuLinuxDisplayPlugin, fu_linux_display_plugin, FU, LINUX_DISPLAY_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/linux-display/meson.build000066400000000000000000000005731501337203100213440ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginLinuxDisplay"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_builtins += static_library('fu_plugin_linux_display', sources: [ 'fu-linux-display-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/linux-lockdown/000077500000000000000000000000001501337203100173505ustar00rootroot00000000000000fwupd-2.0.10/plugins/linux-lockdown/README.md000066400000000000000000000005651501337203100206350ustar00rootroot00000000000000--- title: Plugin: Linux Kernel Lockdown --- ## Introduction This plugin checks if the currently running kernel is locked down. The result will be stored in an security attribute for HSI. ## External Interface Access This plugin requires read access to `/sys/sys/kernel/security`. ## Version Considerations This plugin has been available since fwupd version `1.5.0`. fwupd-2.0.10/plugins/linux-lockdown/fu-linux-lockdown-plugin.c000066400000000000000000000165441501337203100244070ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-linux-lockdown-plugin.h" #include "fu-linux-lockdown-struct.h" struct _FuLinuxLockdownPlugin { FuPlugin parent_instance; GFile *file; GFileMonitor *monitor; FuLinuxLockdown lockdown; }; G_DEFINE_TYPE(FuLinuxLockdownPlugin, fu_linux_lockdown_plugin, FU_TYPE_PLUGIN) static void fu_linux_lockdown_plugin_rescan(FuPlugin *plugin) { FuLinuxLockdownPlugin *self = FU_LINUX_LOCKDOWN_PLUGIN(plugin); gsize bufsz = 0; g_autofree gchar *buf = NULL; /* load file */ if (!g_file_load_contents(self->file, NULL, &buf, &bufsz, NULL, NULL)) { self->lockdown = FU_LINUX_LOCKDOWN_INVALID; } else if (g_strstr_len(buf, bufsz, "[none]") != NULL) { self->lockdown = FU_LINUX_LOCKDOWN_NONE; } else if (g_strstr_len(buf, bufsz, "[integrity]") != NULL) { self->lockdown = FU_LINUX_LOCKDOWN_INTEGRITY; } else if (g_strstr_len(buf, bufsz, "[confidentiality]") != NULL) { self->lockdown = FU_LINUX_LOCKDOWN_CONFIDENTIALITY; } else { self->lockdown = FU_LINUX_LOCKDOWN_UNKNOWN; } /* update metadata */ fu_plugin_add_report_metadata(plugin, "LinuxLockdown", fu_linux_lockdown_to_string(self->lockdown)); } static void fu_linux_lockdown_plugin_changed_cb(GFileMonitor *monitor, GFile *file, GFile *other_file, GFileMonitorEvent event_type, gpointer user_data) { FuPlugin *plugin = FU_PLUGIN(user_data); FuContext *ctx = fu_plugin_get_context(plugin); fu_linux_lockdown_plugin_rescan(plugin); fu_context_security_changed(ctx); } static gboolean fu_linux_lockdown_plugin_startup(FuPlugin *plugin, FuProgress *progress, GError **error) { FuLinuxLockdownPlugin *self = FU_LINUX_LOCKDOWN_PLUGIN(plugin); g_autofree gchar *path = NULL; g_autofree gchar *fn = NULL; path = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR_SECURITY); fn = g_build_filename(path, "lockdown", NULL); if (!g_file_test(fn, G_FILE_TEST_EXISTS)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Kernel doesn't offer lockdown support."); return FALSE; } self->file = g_file_new_for_path(fn); self->monitor = g_file_monitor(self->file, G_FILE_MONITOR_NONE, NULL, error); if (self->monitor == NULL) return FALSE; g_signal_connect(G_FILE_MONITOR(self->monitor), "changed", G_CALLBACK(fu_linux_lockdown_plugin_changed_cb), plugin); fu_linux_lockdown_plugin_rescan(plugin); return TRUE; } static gboolean fu_linux_lockdown_plugin_ensure_security_attr_flags(FuLinuxLockdownPlugin *self, FwupdSecurityAttr *attr, GError **error) { FuContext *ctx = fu_plugin_get_context(FU_PLUGIN(self)); FuEfivars *efivars = fu_context_get_efivars(ctx); const gchar *value; gboolean secureboot_enabled = FALSE; g_autoptr(GHashTable) cmdline = NULL; g_autoptr(GHashTable) config = NULL; /* get current args */ cmdline = fu_kernel_get_cmdline(error); if (cmdline == NULL) return FALSE; /* can we modify the args */ if (!fu_kernel_check_cmdline_mutable(error)) return FALSE; /* get build flags */ config = fu_kernel_get_config(error); if (config == NULL) return FALSE; if (!g_hash_table_contains(config, "CONFIG_LOCK_DOWN_KERNEL_FORCE_NONE")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "config does not have CONFIG_LOCK_DOWN_KERNEL_FORCE_NONE"); return FALSE; } /* we cannot change this */ if (!fu_efivars_get_secure_boot(efivars, &secureboot_enabled, error)) return FALSE; if (g_hash_table_contains(config, "CONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOT") && secureboot_enabled) { g_set_error_literal( error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "kernel lockdown cannot be changed when secure boot is enabled"); return FALSE; } /* set the current and target values */ value = g_hash_table_lookup(cmdline, "lockdown"); fwupd_security_attr_set_kernel_current_value(attr, value); if (g_strcmp0(value, "integrity") != 0) { fwupd_security_attr_set_kernel_target_value(attr, "lockdown=integrity"); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_CAN_FIX); } else { fwupd_security_attr_set_kernel_target_value(attr, "lockdown=none"); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_CAN_UNDO); } return TRUE; } static void fu_linux_lockdown_plugin_add_security_attrs(FuPlugin *plugin, FuSecurityAttrs *attrs) { FuLinuxLockdownPlugin *self = FU_LINUX_LOCKDOWN_PLUGIN(plugin); g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(GError) error_local = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_KERNEL_LOCKDOWN); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_ENABLED); fu_security_attrs_append(attrs, attr); /* we might be able to fix this */ if (!fu_linux_lockdown_plugin_ensure_security_attr_flags(self, attr, &error_local)) g_info("failed to ensure attribute fix flags: %s", error_local->message); if (self->lockdown == FU_LINUX_LOCKDOWN_UNKNOWN) { fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA); return; } /* load file */ if (self->lockdown == FU_LINUX_LOCKDOWN_INVALID) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); return; } if (self->lockdown == FU_LINUX_LOCKDOWN_NONE) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_OS); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_linux_lockdown_plugin_to_string(FuPlugin *plugin, guint idt, GString *str) { FuLinuxLockdownPlugin *self = FU_LINUX_LOCKDOWN_PLUGIN(plugin); fwupd_codec_string_append(str, idt, "Lockdown", fu_linux_lockdown_to_string(self->lockdown)); } static void fu_linux_lockdown_plugin_init(FuLinuxLockdownPlugin *self) { } static void fu_linux_lockdown_plugin_finalize(GObject *obj) { FuLinuxLockdownPlugin *self = FU_LINUX_LOCKDOWN_PLUGIN(obj); if (self->file != NULL) g_object_unref(self->file); if (self->monitor != NULL) { g_file_monitor_cancel(self->monitor); g_object_unref(self->monitor); } G_OBJECT_CLASS(fu_linux_lockdown_plugin_parent_class)->finalize(obj); } static gboolean fu_linux_lockdown_plugin_fix_host_security_attr(FuPlugin *plugin, FwupdSecurityAttr *attr, GError **error) { return fu_kernel_add_cmdline_arg("lockdown=integrity", error); } static gboolean fu_linux_lockdown_plugin_undo_host_security_attr(FuPlugin *plugin, FwupdSecurityAttr *attr, GError **error) { return fu_kernel_remove_cmdline_arg("lockdown=integrity", error); } static void fu_linux_lockdown_plugin_class_init(FuLinuxLockdownPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_linux_lockdown_plugin_finalize; plugin_class->to_string = fu_linux_lockdown_plugin_to_string; plugin_class->startup = fu_linux_lockdown_plugin_startup; plugin_class->add_security_attrs = fu_linux_lockdown_plugin_add_security_attrs; plugin_class->fix_host_security_attr = fu_linux_lockdown_plugin_fix_host_security_attr; plugin_class->undo_host_security_attr = fu_linux_lockdown_plugin_undo_host_security_attr; } fwupd-2.0.10/plugins/linux-lockdown/fu-linux-lockdown-plugin.h000066400000000000000000000004371501337203100244060ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuLinuxLockdownPlugin, fu_linux_lockdown_plugin, FU, LINUX_LOCKDOWN_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/linux-lockdown/fu-linux-lockdown.rs000066400000000000000000000003331501337203100233020ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] enum FuLinuxLockdown { Unknown, Invalid, None, Integrity, Confidentiality, } fwupd-2.0.10/plugins/linux-lockdown/meson.build000066400000000000000000000006611501337203100215150ustar00rootroot00000000000000if host_machine.system() == 'linux' and hsi cargs = ['-DG_LOG_DOMAIN="FuPluginLinuxLockdown"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_builtins += static_library('fu_plugin_linux_lockdown', rustgen.process('fu-linux-lockdown.rs'), sources: [ 'fu-linux-lockdown-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/linux-sleep/000077500000000000000000000000001501337203100166405ustar00rootroot00000000000000fwupd-2.0.10/plugins/linux-sleep/README.md000066400000000000000000000005301501337203100201150ustar00rootroot00000000000000--- title: Plugin: Linux Kernel Sleep --- ## Introduction This plugin checks if s3 sleep is available. The result will be stored in an security attribute for HSI. ## External Interface Access This plugin requires read access to `/sys/power/mem_sleep`. ## Version Considerations This plugin has been available since fwupd version `1.5.0`. fwupd-2.0.10/plugins/linux-sleep/fu-linux-sleep-plugin.c000066400000000000000000000035571501337203100231670ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-linux-sleep-plugin.h" struct _FuLinuxSleepPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuLinuxSleepPlugin, fu_linux_sleep_plugin, FU_TYPE_PLUGIN) static void fu_linux_sleep_plugin_add_security_attrs(FuPlugin *plugin, FuSecurityAttrs *attrs) { gsize bufsz = 0; g_autofree gchar *buf = NULL; g_autofree gchar *sysfsdir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR); g_autofree gchar *fn = g_build_filename(sysfsdir, "power", "mem_sleep", NULL); g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(GFile) file = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_SUSPEND_TO_RAM); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); fu_security_attrs_append(attrs, attr); /* load file */ file = g_file_new_for_path(fn); if (!g_file_load_contents(file, NULL, &buf, &bufsz, NULL, &error_local)) { g_warning("could not open %s: %s", fn, error_local->message); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); return; } if (g_strstr_len(buf, bufsz, "[deep]") != NULL) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_ENABLED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_OS); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_linux_sleep_plugin_init(FuLinuxSleepPlugin *self) { } static void fu_linux_sleep_plugin_class_init(FuLinuxSleepPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->add_security_attrs = fu_linux_sleep_plugin_add_security_attrs; } fwupd-2.0.10/plugins/linux-sleep/fu-linux-sleep-plugin.h000066400000000000000000000003721501337203100231640ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuLinuxSleepPlugin, fu_linux_sleep_plugin, FU, LINUX_SLEEP_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/linux-sleep/meson.build000066400000000000000000000006551501337203100210100ustar00rootroot00000000000000if host_machine.system() == 'linux' and hsi and (host_cpu == 'x86' or host_cpu == 'x86_64') cargs = ['-DG_LOG_DOMAIN="FuPluginLinuxSleep"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_builtins += static_library('fu_plugin_linux_sleep', sources: [ 'fu-linux-sleep-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/linux-swap/000077500000000000000000000000001501337203100165025ustar00rootroot00000000000000fwupd-2.0.10/plugins/linux-swap/README.md000066400000000000000000000005561501337203100177670ustar00rootroot00000000000000--- title: Plugin: Linux Swap --- ## Introduction This plugin checks if the currently available swap partitions and files are all encrypted. The result will be stored in an security attribute for HSI. ## External Interface Access This plugin requires read access to `/proc` ## Version Considerations This plugin has been available since fwupd version `1.5.0`. fwupd-2.0.10/plugins/linux-swap/fu-linux-swap-plugin.c000066400000000000000000000100071501337203100226570ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-linux-swap-plugin.h" #include "fu-linux-swap.h" struct _FuLinuxSwapPlugin { FuPlugin parent_instance; GFile *file; GFileMonitor *monitor; }; G_DEFINE_TYPE(FuLinuxSwapPlugin, fu_linux_swap_plugin, FU_TYPE_PLUGIN) static void fu_linux_swap_plugin_changed_cb(GFileMonitor *monitor, GFile *file, GFile *other_file, GFileMonitorEvent event_type, gpointer user_data) { FuPlugin *plugin = FU_PLUGIN(user_data); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_security_changed(ctx); } static gboolean fu_linux_swap_plugin_startup(FuPlugin *plugin, FuProgress *progress, GError **error) { FuLinuxSwapPlugin *self = FU_LINUX_SWAP_PLUGIN(plugin); g_autofree gchar *fn = NULL; g_autofree gchar *procfs = NULL; procfs = fu_path_from_kind(FU_PATH_KIND_PROCFS); fn = g_build_filename(procfs, "swaps", NULL); if (!g_file_test(fn, G_FILE_TEST_EXISTS)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Kernel doesn't offer swap support."); return FALSE; } self->file = g_file_new_for_path(fn); self->monitor = g_file_monitor(self->file, G_FILE_MONITOR_NONE, NULL, error); if (self->monitor == NULL) return FALSE; g_signal_connect(G_FILE_MONITOR(self->monitor), "changed", G_CALLBACK(fu_linux_swap_plugin_changed_cb), plugin); return TRUE; } static void fu_linux_swap_plugin_add_security_attrs(FuPlugin *plugin, FuSecurityAttrs *attrs) { FuLinuxSwapPlugin *self = FU_LINUX_SWAP_PLUGIN(plugin); gsize bufsz = 0; g_autofree gchar *buf = NULL; g_autoptr(FuLinuxSwap) swap = NULL; g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(GError) error_local = NULL; if (self->file == NULL) return; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_KERNEL_SWAP); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_ENCRYPTED); fu_security_attrs_append(attrs, attr); /* load list of swaps */ if (!g_file_load_contents(self->file, NULL, &buf, &bufsz, NULL, &error_local)) { g_autofree gchar *fn = g_file_get_path(self->file); g_warning("could not open %s: %s", fn, error_local->message); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); return; } swap = fu_linux_swap_new(buf, bufsz, &error_local); if (swap == NULL) { g_autofree gchar *fn = g_file_get_path(self->file); g_warning("could not parse %s: %s", fn, error_local->message); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); return; } /* none configured */ if (!fu_linux_swap_get_enabled(swap)) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); return; } /* add security attribute */ if (!fu_linux_swap_get_encrypted(swap)) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENCRYPTED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_OS); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_linux_swap_plugin_init(FuLinuxSwapPlugin *self) { } static void fu_linux_swap_plugin_finalize(GObject *obj) { FuLinuxSwapPlugin *self = FU_LINUX_SWAP_PLUGIN(obj); if (self->file != NULL) g_object_unref(self->file); if (self->monitor != NULL) { g_file_monitor_cancel(self->monitor); g_object_unref(self->monitor); } G_OBJECT_CLASS(fu_linux_swap_plugin_parent_class)->finalize(obj); } static void fu_linux_swap_plugin_class_init(FuLinuxSwapPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_linux_swap_plugin_finalize; plugin_class->startup = fu_linux_swap_plugin_startup; plugin_class->add_security_attrs = fu_linux_swap_plugin_add_security_attrs; } fwupd-2.0.10/plugins/linux-swap/fu-linux-swap-plugin.h000066400000000000000000000003671501337203100226740ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuLinuxSwapPlugin, fu_linux_swap_plugin, FU, LINUX_SWAP_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/linux-swap/fu-linux-swap.c000066400000000000000000000075561501337203100214020ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-linux-swap.h" struct _FuLinuxSwap { GObject parent_instance; guint encrypted_cnt; guint enabled_cnt; }; G_DEFINE_TYPE(FuLinuxSwap, fu_linux_swap, G_TYPE_OBJECT) static gchar * fu_linux_swap_strip_spaces(const gchar *line) { GString *str = g_string_new(NULL); for (guint i = 0; line[i] != '\0' && !g_ascii_isspace(line[i]); i++) g_string_append_c(str, line[i]); return g_string_free(str, FALSE); } static gboolean fu_linux_swap_verify_partition(FuLinuxSwap *self, const gchar *fn, GError **error) { g_autoptr(FuVolume) volume = NULL; /* this isn't technically encrypted, but isn't on disk in plaintext */ if (g_str_has_prefix(fn, "/dev/zram")) { g_debug("%s is zram, assuming encrypted", fn); self->encrypted_cnt++; return TRUE; } /* find the device */ volume = fu_volume_new_by_device(fn, error); if (volume == NULL) return FALSE; /* is this mount point encrypted */ if (fu_volume_is_encrypted(volume)) { g_debug("%s partition is encrypted", fn); self->encrypted_cnt++; } else { g_debug("%s partition is unencrypted", fn); } /* success */ return TRUE; } static gboolean fu_linux_swap_verify_file(FuLinuxSwap *self, const gchar *fn, GError **error) { guint32 devnum; g_autoptr(GFile) file = NULL; g_autoptr(GFileInfo) info = NULL; g_autoptr(FuVolume) volume = NULL; /* get the device number for the file */ file = g_file_new_for_path(fn); info = g_file_query_info(file, G_FILE_ATTRIBUTE_UNIX_DEVICE, G_FILE_QUERY_INFO_NONE, NULL, error); if (info == NULL) return FALSE; devnum = g_file_info_get_attribute_uint32(info, G_FILE_ATTRIBUTE_UNIX_DEVICE); /* find the device */ volume = fu_volume_new_by_devnum(devnum, error); if (volume == NULL) return FALSE; /* is this mount point encrypted */ if (fu_volume_is_encrypted(volume)) { g_debug("%s file is encrypted", fn); self->encrypted_cnt++; } else { g_debug("%s file is unencrypted", fn); } /* success */ return TRUE; } FuLinuxSwap * fu_linux_swap_new(const gchar *buf, gsize bufsz, GError **error) { g_autoptr(FuLinuxSwap) self = g_object_new(FU_TYPE_LINUX_SWAP, NULL); g_auto(GStrv) lines = NULL; /* look at each line in /proc/swaps */ if (bufsz == 0) bufsz = strlen(buf); lines = fu_strsplit(buf, bufsz, "\n", -1); if (g_strv_length(lines) > 2) { for (guint i = 1; lines[i] != NULL && lines[i][0] != '\0'; i++) { g_autofree gchar *fn = NULL; g_autofree gchar *ty = NULL; /* split */ if (g_utf8_strlen(lines[i], -1) < 45) continue; fn = fu_linux_swap_strip_spaces(lines[i]); ty = fu_linux_swap_strip_spaces(lines[i] + 40); /* partition, so use UDisks to see if backed by crypto */ if (g_strcmp0(ty, "partition") == 0) { self->enabled_cnt++; if (!fu_linux_swap_verify_partition(self, fn, error)) return NULL; } else if (g_strcmp0(ty, "file") == 0) { g_autofree gchar *base = NULL; g_autofree gchar *path = NULL; /* get the path to the file */ base = fu_path_from_kind(FU_PATH_KIND_HOSTFS_ROOT); path = g_build_filename(base, fn, NULL); self->enabled_cnt++; if (!fu_linux_swap_verify_file(self, path, error)) return NULL; } else { g_warning("unknown swap type: %s [%s]", ty, fn); } } } return g_steal_pointer(&self); } /* success if *all* the swap devices are encrypted */ gboolean fu_linux_swap_get_encrypted(FuLinuxSwap *self) { g_return_val_if_fail(FU_IS_LINUX_SWAP(self), FALSE); return self->enabled_cnt > 0 && self->enabled_cnt == self->encrypted_cnt; } gboolean fu_linux_swap_get_enabled(FuLinuxSwap *self) { g_return_val_if_fail(FU_IS_LINUX_SWAP(self), FALSE); return self->enabled_cnt > 0; } static void fu_linux_swap_class_init(FuLinuxSwapClass *klass) { } static void fu_linux_swap_init(FuLinuxSwap *self) { } fwupd-2.0.10/plugins/linux-swap/fu-linux-swap.h000066400000000000000000000007251501337203100213760ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_LINUX_SWAP (fu_linux_swap_get_type()) G_DECLARE_FINAL_TYPE(FuLinuxSwap, fu_linux_swap, FU, LINUX_SWAP, GObject) FuLinuxSwap * fu_linux_swap_new(const gchar *buf, gsize bufsz, GError **error); gboolean fu_linux_swap_get_enabled(FuLinuxSwap *self); gboolean fu_linux_swap_get_encrypted(FuLinuxSwap *self); fwupd-2.0.10/plugins/linux-swap/fu-self-test.c000066400000000000000000000053761501337203100211770ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-linux-swap.h" static void fu_linux_swap_none_func(void) { g_autoptr(FuLinuxSwap) swap = NULL; g_autoptr(GError) error = NULL; swap = fu_linux_swap_new("Filename\t\t\t\tType\t\tSize\tUsed\tPriority\n", 0, &error); g_assert_no_error(error); g_assert_nonnull(swap); g_assert_false(fu_linux_swap_get_enabled(swap)); g_assert_false(fu_linux_swap_get_encrypted(swap)); } static void fu_linux_swap_plain_func(void) { g_autoptr(FuLinuxSwap) swap = NULL; g_autoptr(GError) error = NULL; swap = fu_linux_swap_new("Filename\t\t\t\tType\t\tSize\tUsed\tPriority\n" "/dev/nvme0n1p4 partition\t5962748\t0\t-2\n", 0, &error); if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND) || g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_NAME_HAS_NO_OWNER) || g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN) || g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_SPAWN_EXEC_FAILED) || g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CONNECTION_REFUSED) || g_error_matches(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT) || g_error_matches(error, G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY) || g_error_matches(error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { g_test_skip(error->message); return; } g_assert_no_error(error); g_assert_nonnull(swap); } static void fu_linux_swap_encrypted_func(void) { g_autoptr(FuLinuxSwap) swap = NULL; g_autoptr(GError) error = NULL; swap = fu_linux_swap_new("Filename\t\t\t\tType\t\tSize\tUsed\tPriority\n" "/dev/dm-1 partition\t5962748\t0\t-2\n", 0, &error); if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND) || g_error_matches(error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) || g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN) || g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_SPAWN_EXEC_FAILED) || g_error_matches(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) { g_test_skip(error->message); return; } g_assert_no_error(error); g_assert_nonnull(swap); } int main(int argc, char **argv) { (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); (void)g_setenv("G_MESSAGES_DEBUG", "all", TRUE); /* tests go here */ g_test_add_func("/linux-swap/none", fu_linux_swap_none_func); g_test_add_func("/linux-swap/plain", fu_linux_swap_plain_func); g_test_add_func("/linux-swap/encrypted", fu_linux_swap_encrypted_func); return g_test_run(); } fwupd-2.0.10/plugins/linux-swap/meson.build000066400000000000000000000021141501337203100206420ustar00rootroot00000000000000if host_machine.system() == 'linux' and hsi cargs = ['-DG_LOG_DOMAIN="FuPluginLinuxSwap"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_builtin_linux_swap = static_library('fu_plugin_linux_swap', sources: [ 'fu-linux-swap-plugin.c', 'fu-linux-swap.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) plugin_builtins += plugin_builtin_linux_swap if get_option('tests') env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) e = executable( 'linux-swap-self-test', sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: plugin_deps, link_with: [ plugin_libs, plugin_builtin_linux_swap, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('linux-swap-self-test', e, env: env) # added to installed-tests endif endif fwupd-2.0.10/plugins/linux-tainted/000077500000000000000000000000001501337203100171605ustar00rootroot00000000000000fwupd-2.0.10/plugins/linux-tainted/README.md000066400000000000000000000005531501337203100204420ustar00rootroot00000000000000--- title: Plugin: Linux Kernel Tainted --- ## Introduction This plugin checks if the currently running kernel is tainted. The result will be stored in an security attribute for HSI. ## External Interface Access This plugin requires read access to `/sys/kernel/tainted`. ## Version Considerations This plugin has been available since fwupd version `1.5.0`. fwupd-2.0.10/plugins/linux-tainted/fu-linux-tainted-plugin.c000066400000000000000000000126101501337203100240150ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-linux-tainted-plugin.h" struct _FuLinuxTaintedPlugin { FuPlugin parent_instance; GFile *file; GFileMonitor *monitor; }; G_DEFINE_TYPE(FuLinuxTaintedPlugin, fu_linux_tainted_plugin, FU_TYPE_PLUGIN) #define KERNEL_TAINT_FLAG_PROPRIETARY_MODULE (1 << 0) #define KERNEL_TAINT_FLAG_MODULE_FORCE_LOAD (1 << 1) #define KERNEL_TAINT_FLAG_KERNEL_OUT_OF_SPEC (1 << 2) #define KERNEL_TAINT_FLAG_MODULE_FORCE_UNLOAD (1 << 3) #define KERNEL_TAINT_FLAG_PROCESSOR_MCE (1 << 4) #define KERNEL_TAINT_FLAG_BAD_PAGE (1 << 5) #define KERNEL_TAINT_FLAG_REQUESTED_BY_USERSPACE (1 << 6) #define KERNEL_TAINT_FLAG_KERNEL_DIED (1 << 7) #define KERNEL_TAINT_FLAG_ACPI_OVERRIDDEN (1 << 8) #define KERNEL_TAINT_FLAG_KERNEL_ISSUED_WARNING (1 << 9) #define KERNEL_TAINT_FLAG_STAGING_DRIVER_LOADED (1 << 10) #define KERNEL_TAINT_FLAG_FIRMWARE_WORKAROUND_APPLIED (1 << 11) #define KERNEL_TAINT_FLAG_EXTERNAL_MODULE_LOADED (1 << 12) #define KERNEL_TAINT_FLAG_UNSIGNED_MODULE_LOADED (1 << 13) #define KERNEL_TAINT_FLAG_SOFT_LOCKUP_OCCURRED (1 << 14) #define KERNEL_TAINT_FLAG_KERNEL_LIVE_PATCHED (1 << 15) #define KERNEL_TAINT_FLAG_AUXILIARY_TAINT (1 << 16) #define KERNEL_TAINT_FLAG_STRUCT_RANDOMIZATION_PLUGIN (1 << 17) #define KERNEL_TAINT_FLAG_IN_KERNEL_TEST (1 << 18) static void fu_linux_tainted_plugin_changed_cb(GFileMonitor *monitor, GFile *file, GFile *other_file, GFileMonitorEvent event_type, gpointer user_data) { FuPlugin *plugin = FU_PLUGIN(user_data); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_security_changed(ctx); } static gboolean fu_linux_tainted_plugin_startup(FuPlugin *plugin, FuProgress *progress, GError **error) { FuLinuxTaintedPlugin *self = FU_LINUX_TAINTED_PLUGIN(plugin); g_autofree gchar *fn = NULL; g_autofree gchar *procfs = NULL; procfs = fu_path_from_kind(FU_PATH_KIND_PROCFS); fn = g_build_filename(procfs, "sys", "kernel", "tainted", NULL); self->file = g_file_new_for_path(fn); self->monitor = g_file_monitor(self->file, G_FILE_MONITOR_NONE, NULL, error); if (self->monitor == NULL) return FALSE; g_signal_connect(G_FILE_MONITOR(self->monitor), "changed", G_CALLBACK(fu_linux_tainted_plugin_changed_cb), plugin); return TRUE; } static void fu_linux_tainted_plugin_add_security_attrs(FuPlugin *plugin, FuSecurityAttrs *attrs) { FuLinuxTaintedPlugin *self = FU_LINUX_TAINTED_PLUGIN(plugin); gsize bufsz = 0; guint64 value = 0; g_autofree gchar *buf = NULL; g_autofree gchar *str = NULL; g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(GError) error_local = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_KERNEL_TAINTED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_TAINTED); fu_security_attrs_append(attrs, attr); /* startup failed */ if (fu_plugin_has_flag(plugin, FWUPD_PLUGIN_FLAG_DISABLED)) { fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA); return; } /* load file */ if (!g_file_load_contents(self->file, NULL, &buf, &bufsz, NULL, &error_local)) { g_autofree gchar *fn = g_file_get_path(self->file); g_warning("could not open %s: %s", fn, error_local->message); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); return; } /* do not assume NUL terminated */ str = g_strndup(buf, bufsz); if (!fu_strtoull(str, &value, 0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, &error_local)) { g_warning("could not parse %s: %s", str, error_local->message); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); return; } /* only some taint flags are important */ if ((value & KERNEL_TAINT_FLAG_PROPRIETARY_MODULE) > 0 || (value & KERNEL_TAINT_FLAG_MODULE_FORCE_LOAD) > 0 || (value & KERNEL_TAINT_FLAG_MODULE_FORCE_UNLOAD) > 0 || (value & KERNEL_TAINT_FLAG_STAGING_DRIVER_LOADED) > 0 || (value & KERNEL_TAINT_FLAG_EXTERNAL_MODULE_LOADED) > 0 || (value & KERNEL_TAINT_FLAG_UNSIGNED_MODULE_LOADED) > 0 || (value & KERNEL_TAINT_FLAG_ACPI_OVERRIDDEN) > 0 || (value & KERNEL_TAINT_FLAG_AUXILIARY_TAINT) > 0) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_TAINTED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_OS); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_linux_tainted_plugin_init(FuLinuxTaintedPlugin *self) { } static void fu_linux_tainted_plugin_finalize(GObject *obj) { FuLinuxTaintedPlugin *self = FU_LINUX_TAINTED_PLUGIN(obj); if (self->file != NULL) g_object_unref(self->file); if (self->monitor != NULL) { g_file_monitor_cancel(self->monitor); g_object_unref(self->monitor); } G_OBJECT_CLASS(fu_linux_tainted_plugin_parent_class)->finalize(obj); } static void fu_linux_tainted_plugin_class_init(FuLinuxTaintedPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_linux_tainted_plugin_finalize; plugin_class->startup = fu_linux_tainted_plugin_startup; plugin_class->add_security_attrs = fu_linux_tainted_plugin_add_security_attrs; } fwupd-2.0.10/plugins/linux-tainted/fu-linux-tainted-plugin.h000066400000000000000000000004341501337203100240230ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuLinuxTaintedPlugin, fu_linux_tainted_plugin, FU, LINUX_TAINTED_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/linux-tainted/meson.build000066400000000000000000000006031501337203100213210ustar00rootroot00000000000000if host_machine.system() == 'linux' and hsi cargs = ['-DG_LOG_DOMAIN="FuPluginLinuxTainted"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_builtins += static_library('fu_plugin_linux_tainted', sources: [ 'fu-linux-tainted-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/logind/000077500000000000000000000000001501337203100156475ustar00rootroot00000000000000fwupd-2.0.10/plugins/logind/README.md000066400000000000000000000007071501337203100171320ustar00rootroot00000000000000--- title: Plugin: logind --- ## Introduction This plugin is used to ensure that the machine does not enter a low power mode when updates are being performed. ## Vendor ID Security This protocol does not create a device and thus requires no vendor ID set. ## External Interface Access This plugin requires access to the dbus interface `org.freedesktop.login1`. ## Version Considerations This plugin has been available since fwupd version `1.4.0`. fwupd-2.0.10/plugins/logind/fu-logind-plugin.c000066400000000000000000000076411501337203100212030ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fu-logind-plugin.h" struct _FuLogindPlugin { FuPlugin parent_instance; GDBusProxy *logind_proxy; gint logind_fd; }; G_DEFINE_TYPE(FuLogindPlugin, fu_logind_plugin, FU_TYPE_PLUGIN) static void fu_logind_plugin_to_string(FuPlugin *plugin, guint idt, GString *str) { FuLogindPlugin *self = FU_LOGIND_PLUGIN(plugin); fwupd_codec_string_append_hex(str, idt, "LogindFd", self->logind_fd); } static gboolean fu_logind_plugin_startup(FuPlugin *plugin, FuProgress *progress, GError **error) { FuLogindPlugin *self = FU_LOGIND_PLUGIN(plugin); g_autofree gchar *name_owner = NULL; self->logind_proxy = g_dbus_proxy_new_for_bus_sync( G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS | G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES, NULL, "org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", NULL, error); if (self->logind_proxy == NULL) { g_prefix_error(error, "failed to connect to logind: "); return FALSE; } name_owner = g_dbus_proxy_get_name_owner(self->logind_proxy); if (name_owner == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no owner for %s", g_dbus_proxy_get_name(self->logind_proxy)); return FALSE; } return TRUE; } static gboolean fu_logind_plugin_composite_prepare(FuPlugin *plugin, GPtrArray *devices, GError **error) { FuLogindPlugin *self = FU_LOGIND_PLUGIN(plugin); g_autoptr(GError) error_local = NULL; g_autoptr(GUnixFDList) out_fd_list = NULL; g_autoptr(GVariant) res = NULL; const gchar *what = "shutdown:sleep:idle:handle-power-key:handle-suspend-key:" "handle-hibernate-key:handle-lid-switch"; /* already inhibited */ if (self->logind_fd >= 0) return TRUE; /* not yet connected */ if (self->logind_proxy == NULL) { g_warning("no logind connection to use"); return TRUE; } /* block shutdown and idle */ res = g_dbus_proxy_call_with_unix_fd_list_sync( self->logind_proxy, "Inhibit", g_variant_new("(ssss)", what, PACKAGE_NAME, "Firmware Update in Progress", "block"), G_DBUS_CALL_FLAGS_NONE, -1, NULL, /* fd_list */ &out_fd_list, NULL, /* GCancellable */ &error_local); if (res == NULL) { g_warning("failed to Inhibit using logind: %s", error_local->message); return TRUE; } /* keep fd as cookie */ if (g_unix_fd_list_get_length(out_fd_list) != 1) { g_warning("invalid response from logind"); return TRUE; } self->logind_fd = g_unix_fd_list_get(out_fd_list, 0, NULL); g_debug("opened logind fd %i", self->logind_fd); return TRUE; } static gboolean fu_logind_plugin_composite_cleanup(FuPlugin *plugin, GPtrArray *devices, GError **error) { FuLogindPlugin *self = FU_LOGIND_PLUGIN(plugin); if (self->logind_fd < 0) return TRUE; g_debug("closed logind fd %i", self->logind_fd); if (!g_close(self->logind_fd, error)) return FALSE; self->logind_fd = -1; return TRUE; } static void fu_logind_plugin_init(FuLogindPlugin *self) { self->logind_fd = -1; } static void fu_logind_plugin_finalize(GObject *obj) { FuLogindPlugin *self = FU_LOGIND_PLUGIN(obj); if (self->logind_fd >= 0) g_close(self->logind_fd, NULL); if (self->logind_proxy != NULL) g_object_unref(self->logind_proxy); G_OBJECT_CLASS(fu_logind_plugin_parent_class)->finalize(obj); } static void fu_logind_plugin_class_init(FuLogindPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_logind_plugin_finalize; plugin_class->to_string = fu_logind_plugin_to_string; plugin_class->startup = fu_logind_plugin_startup; plugin_class->composite_cleanup = fu_logind_plugin_composite_cleanup; plugin_class->composite_prepare = fu_logind_plugin_composite_prepare; } fwupd-2.0.10/plugins/logind/fu-logind-plugin.h000066400000000000000000000003541501337203100212020ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuLogindPlugin, fu_logind_plugin, FU, LOGIND_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/logind/meson.build000066400000000000000000000005311501337203100200100ustar00rootroot00000000000000if libsystemd.found() cargs = ['-DG_LOG_DOMAIN="FuPluginLogind"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_builtins += static_library('fu_plugin_logind', sources: [ 'fu-logind-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/logitech-bulkcontroller/000077500000000000000000000000001501337203100212305ustar00rootroot00000000000000fwupd-2.0.10/plugins/logitech-bulkcontroller/README.md000066400000000000000000000052161501337203100225130ustar00rootroot00000000000000--- title: Plugin: Logitech Bulk Controller — Video Collaboration --- ## Introduction This plugin can upgrade the firmware on Logitech Video Collaboration products (Rally Bar and RallyBar Mini), using USB bulk transfer. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a packed binary file format. This plugin supports the following protocol ID: * `com.logitech.vc.proto` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_046D&PID_089B` ## Quirk Use This plugin uses the following plugin-specific quirks: ### `Flags=check-buffer-size` Query the device at startup to see if we can use a larger buffer size. Since: 1.9.7 ## Update Behavior The peripheral firmware is deployed when the device is in normal runtime mode, and the device will reset when the new firmware has been written. ## Design Notes There are two protocols in use, `LogitechBulkcontrollerSendSync` and `LogitechBulkcontrollerSendUpd` which correspond to the two different bulk endpoints. The "Sync" interface accepts protobuf-formatted binary data as described in `proto/`, encapsulated further in a `LogitechBulkcontrollerSendSyncReq` and returned as a `LogitechBulkcontrollerSendSyncRes`. The sequence IDs seem to be used to allow parallel queries, although in practice some of the IDs are hardcoded to zero even when setting them in the request. There seems to be two possible flows when writing using the bulk "Sync" interface: ```mermaid sequenceDiagram Host->>+Device: Write Device-->>-Host: Ack (Write) Host->>+Device: Uninit Device-->>-Host: Ack (Uninit) Device->>+Host: Read Host-->>-Device: Ack (Read) Device->>+Host: Uninit Host-->>-Device: Ack (Uninit) ``` or... ```mermaid sequenceDiagram Host->>+Device: Write Device-->>-Host: Ack (Write) Host->>+Device: Uninit Device->>+Host: Read Device-->>-Host: Ack (Uninit) Host-->>-Device: Ack (Read) Device->>+Host: Uninit Host-->>-Device: Ack (Uninit) ``` Additionally, the device seems to force re-enumeration at random times, presumably restarting due to a protocol error or interface timeout. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x046D` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.7.0`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Sanjay Sheth: @vcdmp fwupd-2.0.10/plugins/logitech-bulkcontroller/fu-logitech-bulkcontroller-child.c000066400000000000000000000036201501337203100277230ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-logitech-bulkcontroller-child.h" struct _FuLogitechBulkcontrollerChild { FuDevice parent_instance; }; G_DEFINE_TYPE(FuLogitechBulkcontrollerChild, fu_logitech_bulkcontroller_child, FU_TYPE_DEVICE) static gboolean fu_logitech_bulkcontroller_child_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDevice *proxy = fu_device_get_proxy(device); return fu_device_write_firmware(proxy, firmware, progress, flags, error); } static void fu_logitech_bulkcontroller_child_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 90, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 10, "reload"); } static void fu_logitech_bulkcontroller_child_init(FuLogitechBulkcontrollerChild *self) { fu_device_add_protocol(FU_DEVICE(self), "com.logitech.vc.proto"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_WEB_CAMERA); } static void fu_logitech_bulkcontroller_child_class_init(FuLogitechBulkcontrollerChildClass *klass) { FuDeviceClass *child_class = FU_DEVICE_CLASS(klass); child_class->write_firmware = fu_logitech_bulkcontroller_child_write_firmware; child_class->set_progress = fu_logitech_bulkcontroller_child_set_progress; } fwupd-2.0.10/plugins/logitech-bulkcontroller/fu-logitech-bulkcontroller-child.h000066400000000000000000000006231501337203100277300ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_LOGITECH_BULKCONTROLLER_CHILD (fu_logitech_bulkcontroller_child_get_type()) G_DECLARE_FINAL_TYPE(FuLogitechBulkcontrollerChild, fu_logitech_bulkcontroller_child, FU, LOGITECH_BULKCONTROLLER_CHILD, FuDevice) fwupd-2.0.10/plugins/logitech-bulkcontroller/fu-logitech-bulkcontroller-common.c000066400000000000000000000207701501337203100301350ustar00rootroot00000000000000/* * Copyright 1999-2021 Logitech, Inc. * All Rights Reserved * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-logitech-bulkcontroller-common.h" #include "usb_msg.pb-c.h" static void fu_logitech_bulkcontroller_proto_manager_set_header(FuDevice *device, Logi__Device__Proto__Header *header_msg) { gint64 timestamp_tv; g_return_if_fail(header_msg != NULL); /* make predictable */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED) || fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATION_TAG)) { header_msg->id = 0; header_msg->timestamp = g_strdup("0"); return; } timestamp_tv = g_get_real_time(); header_msg->id = g_uuid_string_random(); header_msg->timestamp = g_strdup_printf("%" G_GINT64_FORMAT, timestamp_tv / 1000); } GByteArray * fu_logitech_bulkcontroller_proto_manager_generate_get_device_info_request(FuDevice *device) { GByteArray *buf = g_byte_array_new(); Logi__Device__Proto__Header header_msg = LOGI__DEVICE__PROTO__HEADER__INIT; Logi__Device__Proto__GetDeviceInfoRequest get_deviceinfo_msg = LOGI__DEVICE__PROTO__GET_DEVICE_INFO_REQUEST__INIT; Logi__Device__Proto__UsbMsg usb_msg = LOGI__DEVICE__PROTO__USB_MSG__INIT; Logi__Device__Proto__Request request_msg = LOGI__DEVICE__PROTO__REQUEST__INIT; request_msg.payload_case = LOGI__DEVICE__PROTO__REQUEST__PAYLOAD_GET_DEVICE_INFO_REQUEST; request_msg.get_device_info_request = &get_deviceinfo_msg; fu_logitech_bulkcontroller_proto_manager_set_header(device, &header_msg); usb_msg.header = &header_msg; usb_msg.message_case = LOGI__DEVICE__PROTO__USB_MSG__MESSAGE_REQUEST; usb_msg.request = &request_msg; fu_byte_array_set_size(buf, logi__device__proto__usb_msg__get_packed_size(&usb_msg), 0x00); logi__device__proto__usb_msg__pack(&usb_msg, (unsigned char *)buf->data); g_free(header_msg.id); g_free(header_msg.timestamp); return buf; } GByteArray * fu_logitech_bulkcontroller_proto_manager_generate_transition_to_device_mode_request( FuDevice *device) { GByteArray *buf = g_byte_array_new(); Logi__Device__Proto__Header header_msg = LOGI__DEVICE__PROTO__HEADER__INIT; Logi__Device__Proto__TransitionToDeviceModeRequest transition_to_device_mode_msg = LOGI__DEVICE__PROTO__TRANSITION_TO_DEVICE_MODE_REQUEST__INIT; Logi__Device__Proto__UsbMsg usb_msg = LOGI__DEVICE__PROTO__USB_MSG__INIT; Logi__Device__Proto__Request request_msg = LOGI__DEVICE__PROTO__REQUEST__INIT; request_msg.payload_case = LOGI__DEVICE__PROTO__REQUEST__PAYLOAD_TRANSITION_TO_DEVICEMODE_REQUEST; request_msg.transition_to_devicemode_request = &transition_to_device_mode_msg; fu_logitech_bulkcontroller_proto_manager_set_header(device, &header_msg); usb_msg.header = &header_msg; usb_msg.message_case = LOGI__DEVICE__PROTO__USB_MSG__MESSAGE_REQUEST; usb_msg.request = &request_msg; fu_byte_array_set_size(buf, logi__device__proto__usb_msg__get_packed_size(&usb_msg), 0x00); logi__device__proto__usb_msg__pack(&usb_msg, (unsigned char *)buf->data); g_free(header_msg.id); g_free(header_msg.timestamp); return buf; } GByteArray * fu_logitech_bulkcontroller_proto_manager_generate_set_device_time_request(FuDevice *device, GError **error) { g_autofree gchar *olson_location = NULL; g_autoptr(GByteArray) buf = g_byte_array_new(); Logi__Device__Proto__Header header_msg = LOGI__DEVICE__PROTO__HEADER__INIT; Logi__Device__Proto__SetDeviceTimeRequest set_devicetime_msg = LOGI__DEVICE__PROTO__SET_DEVICE_TIME_REQUEST__INIT; Logi__Device__Proto__UsbMsg usb_msg = LOGI__DEVICE__PROTO__USB_MSG__INIT; Logi__Device__Proto__Request request_msg = LOGI__DEVICE__PROTO__REQUEST__INIT; /* the device expects an olson_location, not a timezone */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED) || fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATION_TAG)) { olson_location = g_strdup("Europe/London"); } else { olson_location = fu_common_get_olson_timezone_id(error); if (olson_location == NULL) return NULL; } request_msg.payload_case = LOGI__DEVICE__PROTO__REQUEST__PAYLOAD_SET_DEVICE_TIME_REQUEST; request_msg.set_device_time_request = &set_devicetime_msg; /* make predictable */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED) || fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATION_TAG)) { set_devicetime_msg.ts = 0; } else { set_devicetime_msg.ts = (g_get_real_time() / 1000) + SET_TIME_DELAY_MS; } set_devicetime_msg.time_zone = olson_location; fu_logitech_bulkcontroller_proto_manager_set_header(device, &header_msg); usb_msg.header = &header_msg; usb_msg.message_case = LOGI__DEVICE__PROTO__USB_MSG__MESSAGE_REQUEST; usb_msg.request = &request_msg; fu_byte_array_set_size(buf, logi__device__proto__usb_msg__get_packed_size(&usb_msg), 0x00); logi__device__proto__usb_msg__pack(&usb_msg, (unsigned char *)buf->data); g_free(header_msg.id); g_free(header_msg.timestamp); return g_steal_pointer(&buf); } static GByteArray * fu_logitech_bulkcontroller_usb_msg_parse_msg_response(Logi__Device__Proto__UsbMsg *usb_msg, FuLogitechBulkcontrollerProtoId *proto_id, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); if (usb_msg->response == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no USB response"); return NULL; } switch (usb_msg->response->payload_case) { case LOGI__DEVICE__PROTO__RESPONSE__PAYLOAD_GET_DEVICE_INFO_RESPONSE: if (usb_msg->response->get_device_info_response) { const gchar *tmp = usb_msg->response->get_device_info_response->payload; *proto_id = kProtoId_GetDeviceInfoResponse; if (tmp != NULL) g_byte_array_append(buf, (const guint8 *)tmp, strlen(tmp)); } break; case LOGI__DEVICE__PROTO__RESPONSE__PAYLOAD_TRANSITION_TO_DEVICEMODE_RESPONSE: if (usb_msg->response->transition_to_devicemode_response) { *proto_id = kProtoId_TransitionToDeviceModeResponse; if (!usb_msg->response->transition_to_devicemode_response->success) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "transition mode request failed. error: %u", (guint)usb_msg->response ->transition_to_devicemode_response->error); return NULL; } } break; default: break; }; /* success */ return g_steal_pointer(&buf); } static GByteArray * fu_logitech_bulkcontroller_usb_msg_parse_msg_event(Logi__Device__Proto__UsbMsg *usb_msg, FuLogitechBulkcontrollerProtoId *proto_id, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); if (usb_msg->response == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no USB event"); return NULL; } switch (usb_msg->event->payload_case) { case LOGI__DEVICE__PROTO__EVENT__PAYLOAD_KONG_EVENT: if (usb_msg->event->kong_event) { const gchar *tmp = usb_msg->event->kong_event->mqtt_event; *proto_id = kProtoId_KongEvent; if (tmp != NULL) g_byte_array_append(buf, (const guint8 *)tmp, strlen(tmp)); } break; case LOGI__DEVICE__PROTO__EVENT__PAYLOAD_HANDSHAKE_EVENT: if (usb_msg->event->handshake_event) *proto_id = kProtoId_HandshakeEvent; break; case LOGI__DEVICE__PROTO__EVENT__PAYLOAD_CRASH_DUMP_AVAILABLE_EVENT: *proto_id = kProtoId_CrashDumpAvailableEvent; break; default: break; }; /* success */ return g_steal_pointer(&buf); } GByteArray * fu_logitech_bulkcontroller_proto_manager_decode_message(const guint8 *data, guint32 len, FuLogitechBulkcontrollerProtoId *proto_id, GError **error) { g_autoptr(GByteArray) buf = NULL; Logi__Device__Proto__UsbMsg *usb_msg = logi__device__proto__usb_msg__unpack(NULL, len, (const unsigned char *)data); /* sanity check */ if (usb_msg == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "unable to unpack data"); return NULL; } switch (usb_msg->message_case) { case LOGI__DEVICE__PROTO__USB_MSG__MESSAGE_ACK: buf = g_byte_array_new(); *proto_id = kProtoId_Ack; break; case LOGI__DEVICE__PROTO__USB_MSG__MESSAGE_RESPONSE: buf = fu_logitech_bulkcontroller_usb_msg_parse_msg_response(usb_msg, proto_id, error); if (buf == NULL) return NULL; break; case LOGI__DEVICE__PROTO__USB_MSG__MESSAGE_EVENT: buf = fu_logitech_bulkcontroller_usb_msg_parse_msg_event(usb_msg, proto_id, error); if (buf == NULL) return NULL; break; default: buf = g_byte_array_new(); g_debug("ignoring invalid message case 0x%x", usb_msg->message_case); break; }; logi__device__proto__usb_msg__free_unpacked(usb_msg, NULL); return g_steal_pointer(&buf); } fwupd-2.0.10/plugins/logitech-bulkcontroller/fu-logitech-bulkcontroller-common.h000066400000000000000000000021771501337203100301430ustar00rootroot00000000000000/* * Copyright 1999-2021 Logitech, Inc. * All Rights Reserved * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "usb_msg.pb-c.h" #define SET_TIME_DELAY_MS 500 /* send future time to keep PC & device time as close as possible */ typedef enum { kProtoId_UnknownId, kProtoId_GetDeviceInfoResponse, kProtoId_TransitionToDeviceModeResponse, kProtoId_Ack, kProtoId_KongEvent, kProtoId_HandshakeEvent, kProtoId_CrashDumpAvailableEvent } FuLogitechBulkcontrollerProtoId; GByteArray * fu_logitech_bulkcontroller_proto_manager_generate_get_device_info_request(FuDevice *device) G_GNUC_NON_NULL(1); GByteArray * fu_logitech_bulkcontroller_proto_manager_generate_transition_to_device_mode_request( FuDevice *device) G_GNUC_NON_NULL(1); GByteArray * fu_logitech_bulkcontroller_proto_manager_generate_set_device_time_request(FuDevice *device, GError **error) G_GNUC_NON_NULL(1); GByteArray * fu_logitech_bulkcontroller_proto_manager_decode_message(const guint8 *data, guint32 len, FuLogitechBulkcontrollerProtoId *proto_id, GError **error) G_GNUC_NON_NULL(1, 3); fwupd-2.0.10/plugins/logitech-bulkcontroller/fu-logitech-bulkcontroller-device.c000066400000000000000000001305301501337203100301000ustar00rootroot00000000000000/* * Copyright 1999-2021 Logitech, Inc. * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fu-logitech-bulkcontroller-child.h" #include "fu-logitech-bulkcontroller-common.h" #include "fu-logitech-bulkcontroller-device.h" #include "fu-logitech-bulkcontroller-struct.h" #define HASH_TIMEOUT 30000 #define UPD_INTERFACE_SUBPROTOCOL_ID 117 #define SYNC_INTERFACE_SUBPROTOCOL_ID 118 #define BULK_TRANSFER_TIMEOUT 2500 #define HASH_VALUE_SIZE 16 #define MAX_RETRIES 5 #define MAX_SETUP_RETRIES 50 #define MAX_WAIT_COUNT 150 #define POST_INSTALL_SLEEP_DURATION 80 * 1000 /* ms */ enum { EP_OUT, EP_IN, EP_LAST }; struct _FuLogitechBulkcontrollerDevice { FuUsbDevice parent_instance; guint sync_ep[EP_LAST]; guint update_ep[EP_LAST]; guint sync_iface; guint update_iface; FuLogitechBulkcontrollerDeviceState status; FuLogitechBulkcontrollerUpdateState update_status; guint update_progress; /* percentage value */ gboolean is_sync_transfer_in_progress; GString *device_info_response_json; gsize transfer_bufsz; guint32 sequence_id; }; G_DEFINE_TYPE(FuLogitechBulkcontrollerDevice, fu_logitech_bulkcontroller_device, FU_TYPE_USB_DEVICE) static void fu_logitech_bulkcontroller_device_to_string(FuDevice *device, guint idt, GString *str) { FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "BufferSize", self->transfer_bufsz); fwupd_codec_string_append_hex(str, idt, "SyncIface", self->sync_iface); fwupd_codec_string_append_hex(str, idt, "UpdateIface", self->update_iface); fwupd_codec_string_append(str, idt, "State", fu_logitech_bulkcontroller_device_state_to_string(self->status)); fwupd_codec_string_append( str, idt, "UpdateState", fu_logitech_bulkcontroller_update_state_to_string(self->update_status)); if (self->device_info_response_json->len > 0) { fwupd_codec_string_append(str, idt, "DeviceInfoResponse", self->device_info_response_json->str); } fwupd_codec_string_append_hex(str, idt, "SequenceId", self->sequence_id); } static gboolean fu_logitech_bulkcontroller_device_probe(FuDevice *device, GError **error) { FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device); g_autoptr(GPtrArray) intfs = NULL; intfs = fu_usb_device_get_interfaces(FU_USB_DEVICE(self), error); if (intfs == NULL) return FALSE; for (guint i = 0; i < intfs->len; i++) { FuUsbInterface *intf = g_ptr_array_index(intfs, i); if (fu_usb_interface_get_class(intf) == FU_USB_CLASS_VENDOR_SPECIFIC && fu_usb_interface_get_protocol(intf) == 0x1) { if (fu_usb_interface_get_subclass(intf) == SYNC_INTERFACE_SUBPROTOCOL_ID) { g_autoptr(GPtrArray) endpoints = fu_usb_interface_get_endpoints(intf); self->sync_iface = fu_usb_interface_get_number(intf); if (endpoints == NULL) continue; for (guint j = 0; j < endpoints->len; j++) { FuUsbEndpoint *ep = g_ptr_array_index(endpoints, j); if (j == EP_OUT) self->sync_ep[EP_OUT] = fu_usb_endpoint_get_address(ep); else self->sync_ep[EP_IN] = fu_usb_endpoint_get_address(ep); } } else if (fu_usb_interface_get_subclass(intf) == UPD_INTERFACE_SUBPROTOCOL_ID) { g_autoptr(GPtrArray) endpoints = fu_usb_interface_get_endpoints(intf); self->update_iface = fu_usb_interface_get_number(intf); if (endpoints == NULL) continue; for (guint j = 0; j < endpoints->len; j++) { FuUsbEndpoint *ep = g_ptr_array_index(endpoints, j); if (j == EP_OUT) self->update_ep[EP_OUT] = fu_usb_endpoint_get_address(ep); else self->update_ep[EP_IN] = fu_usb_endpoint_get_address(ep); } } } } fu_usb_device_add_interface(FU_USB_DEVICE(self), self->update_iface); fu_usb_device_add_interface(FU_USB_DEVICE(self), self->sync_iface); return TRUE; } typedef struct { FuLogitechBulkcontrollerCmd cmd; guint32 sequence_id; GByteArray *data; } FuLogitechBulkcontrollerResponse; static FuLogitechBulkcontrollerResponse * fu_logitech_bulkcontroller_device_response_new(void) { FuLogitechBulkcontrollerResponse *response = g_new0(FuLogitechBulkcontrollerResponse, 1); response->data = g_byte_array_new(); return response; } static void fu_logitech_bulkcontroller_device_response_free(FuLogitechBulkcontrollerResponse *response) { if (response->data != NULL) g_byte_array_unref(response->data); g_free(response); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuLogitechBulkcontrollerResponse, fu_logitech_bulkcontroller_device_response_free) static gboolean fu_logitech_bulkcontroller_device_sync_send_cmd(FuLogitechBulkcontrollerDevice *self, FuLogitechBulkcontrollerCmd cmd, GByteArray *buf, GError **error) { g_autoptr(FuStructLogitechBulkcontrollerSendSyncReq) st_req = fu_struct_logitech_bulkcontroller_send_sync_req_new(); g_autofree gchar *str = NULL; /* increment */ self->sequence_id++; /* send */ fu_struct_logitech_bulkcontroller_send_sync_req_set_cmd(st_req, cmd); fu_struct_logitech_bulkcontroller_send_sync_req_set_sequence_id(st_req, self->sequence_id); if (buf != NULL) { fu_struct_logitech_bulkcontroller_send_sync_req_set_payload_length(st_req, buf->len); g_byte_array_append(st_req, buf->data, buf->len); } str = fu_struct_logitech_bulkcontroller_send_sync_req_to_string(st_req); g_debug("sending: %s", str); if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), self->sync_ep[EP_OUT], st_req->data, st_req->len, NULL, /* transferred */ BULK_TRANSFER_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to send sync bulk transfer: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_logitech_bulkcontroller_device_sync_send_ack(FuLogitechBulkcontrollerDevice *self, FuLogitechBulkcontrollerCmd cmd, GError **error) { g_autoptr(GByteArray) buf_ack = g_byte_array_new(); fu_byte_array_append_uint32(buf_ack, cmd, G_LITTLE_ENDIAN); if (!fu_logitech_bulkcontroller_device_sync_send_cmd(self, FU_LOGITECH_BULKCONTROLLER_CMD_ACK, buf_ack, error)) { g_prefix_error(error, "failed to send ack for %s: ", fu_logitech_bulkcontroller_cmd_to_string(cmd)); return FALSE; } return TRUE; } static FuLogitechBulkcontrollerResponse * fu_logitech_bulkcontroller_device_sync_wait_any(FuLogitechBulkcontrollerDevice *self, GError **error) { gsize actual_length = 0; g_autofree guint8 *buf = g_malloc0(self->transfer_bufsz); g_autoptr(FuStructLogitechBulkcontrollerSendSyncRes) st = NULL; g_autoptr(FuLogitechBulkcontrollerResponse) response = fu_logitech_bulkcontroller_device_response_new(); if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), self->sync_ep[EP_IN], buf, self->transfer_bufsz, &actual_length, BULK_TRANSFER_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to receive: "); return NULL; } fu_dump_raw(G_LOG_DOMAIN, "response", buf, MIN(actual_length, 12)); st = fu_struct_logitech_bulkcontroller_send_sync_res_parse(buf, self->transfer_bufsz, 0x0, error); if (st == NULL) return NULL; response->cmd = fu_struct_logitech_bulkcontroller_send_sync_res_get_cmd(st); response->sequence_id = fu_struct_logitech_bulkcontroller_send_sync_res_get_sequence_id(st); g_byte_array_append(response->data, buf + st->len, fu_struct_logitech_bulkcontroller_send_sync_res_get_payload_length(st)); /* no payload for UninitBuffer, skip check */ if ((response->cmd != FU_LOGITECH_BULKCONTROLLER_CMD_UNINIT_BUFFER) && (response->data->len == 0)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_READ, "failed to receive packet"); return NULL; } return g_steal_pointer(&response); } static GByteArray * fu_logitech_bulkcontroller_device_sync_wait_cmd(FuLogitechBulkcontrollerDevice *self, FuLogitechBulkcontrollerCmd cmd, guint32 sequence_id, GError **error) { g_autoptr(FuLogitechBulkcontrollerResponse) response = NULL; response = fu_logitech_bulkcontroller_device_sync_wait_any(self, error); if (response == NULL) return NULL; if (response->cmd != cmd) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "command invalid, expected %s and got %s", fu_logitech_bulkcontroller_cmd_to_string(cmd), fu_logitech_bulkcontroller_cmd_to_string(response->cmd)); return NULL; } /* verify the sequence ID */ if (response->sequence_id != sequence_id) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "sequence ID invalid, expected 0x%04x and got 0x%04x", sequence_id, response->sequence_id); return NULL; } /* success */ return g_steal_pointer(&response->data); } static gboolean fu_logitech_bulkcontroller_device_sync_wait_cmd_retry_cb(FuDevice *device, gpointer user_data, GError **error) { FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device); FuLogitechBulkcontrollerResponse *helper = (FuLogitechBulkcontrollerResponse *)user_data; helper->data = fu_logitech_bulkcontroller_device_sync_wait_cmd(self, helper->cmd, helper->sequence_id, error); if (helper->data == NULL) return FALSE; /* success */ return TRUE; } static GByteArray * fu_logitech_bulkcontroller_device_sync_wait_cmd_retry(FuLogitechBulkcontrollerDevice *self, FuLogitechBulkcontrollerCmd cmd, guint32 sequence_id, GError **error) { FuLogitechBulkcontrollerResponse helper = {.cmd = cmd, .sequence_id = sequence_id}; if (!fu_device_retry(FU_DEVICE(self), fu_logitech_bulkcontroller_device_sync_wait_cmd_retry_cb, MAX_RETRIES, &helper, error)) return NULL; return helper.data; } static gboolean fu_logitech_bulkcontroller_device_sync_check_ack_cmd(GByteArray *buf, FuLogitechBulkcontrollerCmd cmd, GError **error) { gchar ack_payload[6] = {0x0}; guint64 ack_cmd = 0; /* this is weird; base 10 number as ASCII as the ack payload... */ if (!fu_memcpy_safe((guint8 *)ack_payload, sizeof(ack_payload), 0x0, buf->data, buf->len, 0x0, sizeof(ack_payload) - 1, error)) { g_prefix_error(error, "failed to copy ack payload: "); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "ack_payload", (guint8 *)ack_payload, sizeof(ack_payload)); if (!fu_strtoull((const gchar *)ack_payload, &ack_cmd, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "failed to parse ack payload cmd: "); return FALSE; } g_debug("ack_cmd: %s [0x%x]", fu_logitech_bulkcontroller_cmd_to_string(ack_cmd), (guint)ack_cmd); if (ack_cmd != cmd) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "command invalid, expected %s and got %s", fu_logitech_bulkcontroller_cmd_to_string(cmd), fu_logitech_bulkcontroller_cmd_to_string(ack_cmd)); return FALSE; } /* success */ return TRUE; } static gboolean fu_logitech_bulkcontroller_device_sync_wait_ack_cb(FuDevice *device, gpointer user_data, GError **error) { FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device); FuLogitechBulkcontrollerResponse *helper = (FuLogitechBulkcontrollerResponse *)user_data; g_autoptr(GByteArray) buf = NULL; buf = fu_logitech_bulkcontroller_device_sync_wait_cmd(self, FU_LOGITECH_BULKCONTROLLER_CMD_ACK, self->sequence_id, error); if (buf == NULL) return FALSE; if (!fu_logitech_bulkcontroller_device_sync_check_ack_cmd(buf, helper->cmd, error)) return FALSE; /* success */ return TRUE; } /* send command and wait for ACK */ static gboolean fu_logitech_bulkcontroller_device_sync_wait_ack(FuLogitechBulkcontrollerDevice *self, FuLogitechBulkcontrollerCmd cmd, GError **error) { FuLogitechBulkcontrollerResponse helper = {.cmd = cmd}; return fu_device_retry_full(FU_DEVICE(self), fu_logitech_bulkcontroller_device_sync_wait_ack_cb, 10, 200, &helper, error); } static gboolean fu_logitech_bulkcontroller_device_sync_check_ack(FuLogitechBulkcontrollerDevice *self, FuLogitechBulkcontrollerResponse *response, FuLogitechBulkcontrollerCmd cmd, GError **error) { /* verify the sequence ID */ if (response->sequence_id != self->sequence_id) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "sequence ID invalid, expected 0x%04x and got 0x%04x", self->sequence_id, response->sequence_id); return FALSE; } return fu_logitech_bulkcontroller_device_sync_check_ack_cmd(response->data, cmd, error); } static GByteArray * fu_logitech_bulkcontroller_device_sync_write(FuLogitechBulkcontrollerDevice *self, GByteArray *req, GError **error) { g_autoptr(GByteArray) res_ack = NULL; g_autoptr(GByteArray) res_read = NULL; g_autoptr(GByteArray) buf = NULL; /* send host->device buffer-write */ if (!fu_logitech_bulkcontroller_device_sync_send_cmd( self, FU_LOGITECH_BULKCONTROLLER_CMD_BUFFER_WRITE, req, error)) { g_prefix_error(error, "failed to send request: "); return NULL; } /* wait device->host ack */ if (!fu_logitech_bulkcontroller_device_sync_wait_ack( self, FU_LOGITECH_BULKCONTROLLER_CMD_BUFFER_WRITE, error)) { g_prefix_error(error, "failed to wait for ack: "); return NULL; } /* send host->device buffer-uninit */ if (!fu_logitech_bulkcontroller_device_sync_send_cmd( self, FU_LOGITECH_BULKCONTROLLER_CMD_UNINIT_BUFFER, NULL, error)) { g_prefix_error(error, "failed to uninit buffer: "); return NULL; } /* wait device->host buffer-read|ack */ do { g_autoptr(FuLogitechBulkcontrollerResponse) response_tmp = NULL; response_tmp = fu_logitech_bulkcontroller_device_sync_wait_any(self, error); if (response_tmp == NULL) { g_prefix_error(error, "failed to wait for any: "); return NULL; } if (response_tmp->cmd == FU_LOGITECH_BULKCONTROLLER_CMD_ACK) { if (res_ack != NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "already received ack"); return NULL; } if (!fu_logitech_bulkcontroller_device_sync_check_ack( self, response_tmp, FU_LOGITECH_BULKCONTROLLER_CMD_UNINIT_BUFFER, error)) { g_prefix_error(error, "failed to check uninit buffer: "); return NULL; } res_ack = g_steal_pointer(&response_tmp->data); } else if (response_tmp->cmd == FU_LOGITECH_BULKCONTROLLER_CMD_BUFFER_READ) { if (res_read != NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "already received read-buffer"); return NULL; } res_read = g_steal_pointer(&response_tmp->data); } } while (res_ack == NULL || res_read == NULL); /* send host->device ack */ if (!fu_logitech_bulkcontroller_device_sync_send_ack( self, FU_LOGITECH_BULKCONTROLLER_CMD_BUFFER_READ, error)) { g_prefix_error(error, "failed to ack read buffer: "); return NULL; } /* wait device->host uninit */ buf = fu_logitech_bulkcontroller_device_sync_wait_cmd_retry( self, FU_LOGITECH_BULKCONTROLLER_CMD_UNINIT_BUFFER, 0x0, /* why? */ error); if (buf == NULL) { g_prefix_error(error, "failed to wait for uninit buffer: "); return NULL; } /* send host->device ack */ if (!fu_logitech_bulkcontroller_device_sync_send_ack( self, FU_LOGITECH_BULKCONTROLLER_CMD_UNINIT_BUFFER, error)) { g_prefix_error(error, "failed to ack uninit buffer: "); return NULL; } /* success */ return g_steal_pointer(&res_read); } static gboolean fu_logitech_bulkcontroller_device_upd_send_cmd(FuLogitechBulkcontrollerDevice *self, guint32 cmd, GBytes *buf, guint timeout, GError **error) { gsize actual_length = 0; g_autofree guint8 *buf_tmp = g_malloc0(self->transfer_bufsz); GByteArray buf_ack = {.data = buf_tmp, .len = self->transfer_bufsz}; g_autoptr(FuStructLogitechBulkcontrollerUpdateReq) buf_pkt = fu_struct_logitech_bulkcontroller_update_req_new(); fu_struct_logitech_bulkcontroller_update_req_set_cmd(buf_pkt, cmd); if (buf != NULL) { fu_struct_logitech_bulkcontroller_update_req_set_payload_length( buf_pkt, g_bytes_get_size(buf)); fu_byte_array_append_bytes(buf_pkt, buf); } fu_dump_raw(G_LOG_DOMAIN, "request", buf_pkt->data, MIN(buf_pkt->len, 12)); if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), self->update_ep[EP_OUT], buf_pkt->data, buf_pkt->len, NULL, /* transferred */ BULK_TRANSFER_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to send upd bulk transfer: "); fu_error_convert(error); return FALSE; } /* receiving ACK */ if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), self->update_ep[EP_IN], buf_tmp, self->transfer_bufsz, &actual_length, timeout, NULL, error)) { g_prefix_error(error, "failed to receive: "); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "response", buf_tmp, MIN(actual_length, 12)); if (fu_struct_logitech_bulkcontroller_update_res_get_cmd(&buf_ack) != FU_LOGITECH_BULKCONTROLLER_CMD_ACK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not CMD_ACK, got %s", fu_logitech_bulkcontroller_cmd_to_string( fu_struct_logitech_bulkcontroller_update_res_get_cmd(&buf_ack))); return FALSE; } if (fu_struct_logitech_bulkcontroller_update_res_get_cmd_req(&buf_ack) != cmd) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid upd message received, expected %s, got %s", fu_logitech_bulkcontroller_cmd_to_string(cmd), fu_logitech_bulkcontroller_cmd_to_string( fu_struct_logitech_bulkcontroller_update_res_get_cmd_req(&buf_ack))); return FALSE; } return TRUE; } static FwupdStatus fu_logitech_bulkcontroller_device_update_state_to_status( FuLogitechBulkcontrollerUpdateState update_state) { if (update_state == FU_LOGITECH_BULKCONTROLLER_UPDATE_STATE_DOWNLOADING) return FWUPD_STATUS_DEVICE_WRITE; if (update_state == FU_LOGITECH_BULKCONTROLLER_UPDATE_STATE_STARTING) return FWUPD_STATUS_DEVICE_VERIFY; if (update_state == FU_LOGITECH_BULKCONTROLLER_UPDATE_STATE_UPDATING) return FWUPD_STATUS_DEVICE_WRITE; if (update_state == FU_LOGITECH_BULKCONTROLLER_UPDATE_STATE_CURRENT) return FWUPD_STATUS_IDLE; return FWUPD_STATUS_UNKNOWN; } static gboolean fu_logitech_bulkcontroller_device_ensure_child(FuLogitechBulkcontrollerDevice *self, JsonObject *json_device, GError **error) { FuLogitechBulkcontrollerDeviceState status; GPtrArray *children = fu_device_get_children(FU_DEVICE(self)); g_autoptr(FuDevice) child = NULL; const gchar *name; const gchar *required_members[] = { "make", "model", "name", "status", "sw", "type", "vid", }; /* sanity check */ for (guint i = 0; i < G_N_ELEMENTS(required_members); i++) { if (!json_object_has_member(json_device, required_members[i])) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no %s", required_members[i]); return FALSE; } } if (g_strcmp0(json_object_get_string_member(json_device, "type"), "Sentinel") != 0) return TRUE; /* check status */ status = json_object_get_int_member(json_device, "status"); if (status != FU_LOGITECH_BULKCONTROLLER_DEVICE_STATE_ONLINE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "status is %s", fu_logitech_bulkcontroller_device_state_to_string(status)); return FALSE; } /* child already exists */ name = json_object_get_string_member(json_device, "name"); for (guint i = 0; i < children->len; i++) { FuDevice *child_tmp = g_ptr_array_index(children, i); if (g_strcmp0(fu_device_get_logical_id(child_tmp), name) == 0) { g_debug("found existing %s device, just updating version", name); fu_device_set_version(child_tmp, json_object_get_string_member(json_device, "sw")); return TRUE; } } /* create new child */ child = g_object_new(FU_TYPE_LOGITECH_BULKCONTROLLER_CHILD, "proxy", self, NULL); fu_device_add_private_flag(child, FU_DEVICE_PRIVATE_FLAG_REFCOUNTED_PROXY); fu_device_set_name(child, name); fu_device_set_vendor(child, json_object_get_string_member(json_device, "make")); fu_device_set_logical_id(child, name); fu_device_set_version(child, json_object_get_string_member(json_device, "sw")); if (json_object_has_member(json_device, "serial")) fu_device_set_serial(child, json_object_get_string_member(json_device, "serial")); fu_device_add_instance_strup(child, "MODEL", json_object_get_string_member(json_device, "model")); if (!fu_device_build_instance_id(child, error, "LOGI", "MODEL", NULL)) return FALSE; fu_device_add_child(FU_DEVICE(self), child); /* success */ return TRUE; } static gboolean fu_logitech_bulkcontroller_device_json_parser(FuLogitechBulkcontrollerDevice *self, GByteArray *decoded_pkt, GError **error) { JsonArray *json_devices; JsonNode *json_root; JsonObject *json_device; JsonObject *json_object; JsonObject *json_payload; g_autoptr(JsonParser) json_parser = json_parser_new(); /* parse JSON reply */ if (!json_parser_load_from_data(json_parser, (const gchar *)decoded_pkt->data, decoded_pkt->len, error)) { g_prefix_error(error, "failed to parse json data: "); return FALSE; } json_root = json_parser_get_root(json_parser); if (json_root == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "did not get JSON root"); return FALSE; } json_object = json_node_get_object(json_root); json_payload = json_object_get_object_member(json_object, "payload"); if (json_payload == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "did not get JSON payload"); return FALSE; } json_devices = json_object_get_array_member(json_payload, "devices"); if (json_devices == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "did not get JSON devices"); return FALSE; } json_device = json_array_get_object_element(json_devices, 0); if (json_device == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "did not get JSON device"); return FALSE; } if (json_object_has_member(json_device, "name")) fu_device_set_name(FU_DEVICE(self), json_object_get_string_member(json_device, "name")); if (json_object_has_member(json_device, "sw")) fu_device_set_version(FU_DEVICE(self), json_object_get_string_member(json_device, "sw")); if (json_object_has_member(json_device, "type")) { fu_device_add_instance_id_full(FU_DEVICE(self), json_object_get_string_member(json_device, "type"), FU_DEVICE_INSTANCE_FLAG_QUIRKS); } if (json_object_has_member(json_device, "status")) self->status = json_object_get_int_member(json_device, "status"); if (json_object_has_member(json_device, "updateStatus")) self->update_status = json_object_get_int_member(json_device, "updateStatus"); /* updateProgress only available while firmware upgrade is going on */ if (json_object_has_member(json_device, "updateProgress")) self->update_progress = json_object_get_int_member(json_device, "updateProgress"); /* ensure child pheripheral devices exist */ for (guint i = 1; i < json_array_get_length(json_devices); i++) { JsonObject *json_child = json_array_get_object_element(json_devices, i); g_autoptr(GError) error_local = NULL; if (!fu_logitech_bulkcontroller_device_ensure_child(self, json_child, &error_local)) g_warning("failed to add child: %s", error_local->message); } /* success */ return TRUE; } static gboolean fu_logitech_bulkcontroller_device_parse_info(FuLogitechBulkcontrollerDevice *self, GByteArray *buf, GError **error) { FuLogitechBulkcontrollerProtoId proto_id = kProtoId_UnknownId; g_autofree gchar *bufstr = NULL; g_autoptr(GByteArray) decoded_pkt = NULL; decoded_pkt = fu_logitech_bulkcontroller_proto_manager_decode_message(buf->data, buf->len, &proto_id, error); if (decoded_pkt == NULL) { g_prefix_error(error, "failed to unpack packet for device info request: "); return FALSE; } bufstr = fu_strsafe((const gchar *)decoded_pkt->data, decoded_pkt->len); g_debug("received device response: id: %u, length %u, data: %s", proto_id, buf->len, bufstr); if (proto_id != kProtoId_GetDeviceInfoResponse && proto_id != kProtoId_KongEvent) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "incorrect response for device info request"); return FALSE; } if (!fu_logitech_bulkcontroller_device_json_parser(self, decoded_pkt, error)) return FALSE; /* success */ g_string_assign(self->device_info_response_json, bufstr); return TRUE; } static gboolean fu_logitech_bulkcontroller_device_ensure_info_cb(FuDevice *device, gpointer user_data, GError **error) { FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device); g_autoptr(GByteArray) buf = NULL; gboolean send_req = *(gboolean *)user_data; /* sending GetDeviceInfoRequest. Device reports quite a few matrix, including status, * progress etc * Two ways to get data from device: * 1. Listen for the data broadcasted by device, while firmware upgrade is going on * 2. Make explicit request to the device. Used when data is needed before/after firmware * upgrade */ if (send_req) { g_autoptr(GByteArray) device_request = fu_logitech_bulkcontroller_proto_manager_generate_get_device_info_request( device); buf = fu_logitech_bulkcontroller_device_sync_write(self, device_request, error); if (buf == NULL) return FALSE; } else { /* poll the out interface */ buf = fu_logitech_bulkcontroller_device_sync_wait_cmd( self, FU_LOGITECH_BULKCONTROLLER_CMD_BUFFER_READ, 0x0, /* sequence_id */ error); if (buf == NULL) return FALSE; } return fu_logitech_bulkcontroller_device_parse_info(self, buf, error); } static gboolean fu_logitech_bulkcontroller_device_ensure_info(FuLogitechBulkcontrollerDevice *self, gboolean send_req, GError **error) { return fu_device_retry(FU_DEVICE(self), fu_logitech_bulkcontroller_device_ensure_info_cb, MAX_SETUP_RETRIES, &send_req, error); } static gboolean fu_logitech_bulkcontroller_device_upd_send_init_cmd_cb(FuDevice *device, gpointer user_data, GError **error) { FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device); return fu_logitech_bulkcontroller_device_upd_send_cmd(self, FU_LOGITECH_BULKCONTROLLER_CMD_INIT, NULL, BULK_TRANSFER_TIMEOUT, error); } static gboolean fu_logitech_bulkcontroller_device_write_fw(FuLogitechBulkcontrollerDevice *self, GInputStream *stream, FuProgress *progress, GError **error) { g_autoptr(FuChunkArray) chunks = NULL; chunks = fu_chunk_array_new_from_stream( stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, self->transfer_bufsz - FU_STRUCT_LOGITECH_BULKCONTROLLER_UPDATE_REQ_SIZE, error); if (chunks == NULL) return FALSE; fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GBytes) chk_blob = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; chk_blob = fu_chunk_get_bytes(chk); if (!fu_logitech_bulkcontroller_device_upd_send_cmd( self, FU_LOGITECH_BULKCONTROLLER_CMD_DATA_TRANSFER, chk_blob, BULK_TRANSFER_TIMEOUT, error)) { g_prefix_error(error, "failed to send data packet 0x%x: ", i); return FALSE; } fu_progress_step_done(progress); } return TRUE; } static gboolean fu_logitech_bulkcontroller_device_verify_cb(FuDevice *device, gpointer user_data, GError **error) { FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device); FuProgress *progress = FU_PROGRESS(user_data); g_autoptr(GError) error_local = NULL; g_autoptr(GByteArray) buf = NULL; /* poll the out interface */ buf = fu_logitech_bulkcontroller_device_sync_wait_cmd( self, FU_LOGITECH_BULKCONTROLLER_CMD_BUFFER_READ, 0x0, /* sequence_id */ &error_local); if (buf == NULL) { g_autoptr(GByteArray) device_request = NULL; g_debug("manually requesting as no pending request: %s", error_local->message); device_request = fu_logitech_bulkcontroller_proto_manager_generate_get_device_info_request( device); buf = fu_logitech_bulkcontroller_device_sync_write(self, device_request, error); if (buf == NULL) return FALSE; } if (!fu_logitech_bulkcontroller_device_parse_info(self, buf, error)) return FALSE; g_debug("firmware update status: %s, progress: %u", fu_logitech_bulkcontroller_update_state_to_string(self->update_status), self->update_progress); fu_progress_set_status( progress, fu_logitech_bulkcontroller_device_update_state_to_status(self->update_status)); /* existing device image version is same as newly pushed image? */ if (self->update_status == FU_LOGITECH_BULKCONTROLLER_UPDATE_STATE_ERROR || self->update_status == FU_LOGITECH_BULKCONTROLLER_UPDATE_STATE_CURRENT) return TRUE; /* only update the child if the percentage is bigger -- which means the progressbar * may stall, but will never go backwards */ if (self->update_progress > fu_progress_get_percentage(progress)) fu_progress_set_percentage(progress, self->update_progress); /* keep waiting */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "waiting for verify to finish"); return FALSE; } static gboolean fu_logitech_bulkcontroller_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device); gsize streamsz = 0; g_autofree gchar *base64hash = NULL; g_autofree gchar *md5_str = NULL; g_autoptr(GByteArray) md5_buf = NULL; g_autoptr(GByteArray) end_pkt = g_byte_array_new(); g_autoptr(GByteArray) start_pkt = g_byte_array_new(); g_autoptr(GInputStream) stream = NULL; g_autoptr(GBytes) end_pkt_blob = NULL; g_autoptr(GBytes) start_pkt_blob = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 4, "checksum-stream"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "init"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 51, "device-write-blocks"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "end-transfer"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "uninit"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 40, NULL); /* get default image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* calculate firmware checksum, weirdly as a base64 string */ md5_str = fu_firmware_get_checksum(firmware, G_CHECKSUM_MD5, error); md5_buf = fu_byte_array_from_string(md5_str, error); if (md5_buf == NULL) return FALSE; base64hash = g_base64_encode(md5_buf->data, md5_buf->len); fu_progress_step_done(progress); /* sending INIT. Retry if device is not in IDLE state to receive the file */ if (!fu_device_retry(device, fu_logitech_bulkcontroller_device_upd_send_init_cmd_cb, MAX_RETRIES, NULL, error)) { g_prefix_error(error, "failed to write init transfer packet: please reboot the device: "); return FALSE; } /* transfer sent */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; fu_byte_array_append_uint64(start_pkt, streamsz, G_LITTLE_ENDIAN); start_pkt_blob = g_bytes_new(start_pkt->data, start_pkt->len); if (!fu_logitech_bulkcontroller_device_upd_send_cmd( self, FU_LOGITECH_BULKCONTROLLER_CMD_START_TRANSFER, start_pkt_blob, BULK_TRANSFER_TIMEOUT, error)) { g_prefix_error(error, "failed to write start transfer packet: "); return FALSE; } fu_progress_step_done(progress); /* push each block to device */ if (!fu_logitech_bulkcontroller_device_write_fw(self, stream, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to write firmware: "); return FALSE; } fu_progress_step_done(progress); /* sending end transfer -- extend the bulk transfer timeout value, as android device takes * some time to calculate the hash and respond */ fu_byte_array_append_uint32(end_pkt, 1, G_LITTLE_ENDIAN); /* update */ fu_byte_array_append_uint32(end_pkt, 0, G_LITTLE_ENDIAN); /* force */ fu_byte_array_append_uint32(end_pkt, FU_LOGITECH_BULKCONTROLLER_CHECKSUM_TYPE_MD5, G_LITTLE_ENDIAN); g_byte_array_append(end_pkt, (const guint8 *)base64hash, strlen(base64hash)); end_pkt_blob = g_bytes_new(end_pkt->data, end_pkt->len); if (!fu_logitech_bulkcontroller_device_upd_send_cmd( self, FU_LOGITECH_BULKCONTROLLER_CMD_END_TRANSFER, end_pkt_blob, HASH_TIMEOUT, error)) { g_prefix_error(error, "failed to write end transfer packet: "); return FALSE; } fu_progress_step_done(progress); /* send uninit */ if (!fu_logitech_bulkcontroller_device_upd_send_cmd(self, FU_LOGITECH_BULKCONTROLLER_CMD_UNINIT, NULL, BULK_TRANSFER_TIMEOUT, error)) { g_prefix_error(error, "failed to write finish transfer packet: "); return FALSE; } fu_progress_step_done(progress); /* * image file pushed. Device validates and uploads new image on inactive partition. * Restart sync cb, to get the update progress * Normally status changes as follows: * While image being pushed: kUpdateStateCurrent->kUpdateStateDownloading (~5minutes) * After image push is complete: kUpdateStateDownloading->kUpdateStateReady * Validating image: kUpdateStateReady->kUpdateStateStarting * Uploading image: kUpdateStateStarting->kUpdateStateUpdating * Upload finished: kUpdateStateUpdating->kUpdateStateCurrent (~5minutes) * After upload is finished, device reboots itself */ if (!fu_device_retry_full(device, fu_logitech_bulkcontroller_device_verify_cb, 500, /* over 10 minutes */ 2500, /* ms */ fu_progress_get_child(progress), error)) return FALSE; if (self->update_status == FU_LOGITECH_BULKCONTROLLER_UPDATE_STATE_ERROR) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "firmware upgrade failed"); return FALSE; } fu_progress_step_done(progress); /* success! */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_logitech_bulkcontroller_device_set_time_cb(FuDevice *device, gpointer user_data, GError **error) { FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device); FuLogitechBulkcontrollerProtoId proto_id = kProtoId_UnknownId; g_autofree gchar *bufstr = NULL; g_autoptr(GByteArray) decoded_pkt = NULL; g_autoptr(GByteArray) device_request = NULL; g_autoptr(GByteArray) buf = NULL; /* send SetDeviceTimeRequest to sync device clock with host */ device_request = fu_logitech_bulkcontroller_proto_manager_generate_set_device_time_request(device, error); if (device_request == NULL) return FALSE; buf = fu_logitech_bulkcontroller_device_sync_write(self, device_request, error); if (buf == NULL) return FALSE; decoded_pkt = fu_logitech_bulkcontroller_proto_manager_decode_message(buf->data, buf->len, &proto_id, error); if (decoded_pkt == NULL) { g_prefix_error(error, "failed to unpack packet: "); return FALSE; } bufstr = fu_strsafe((const gchar *)decoded_pkt->data, decoded_pkt->len); g_debug("received device response while processing: id: %u, length %u, data: %s", proto_id, buf->len, bufstr); if (proto_id != kProtoId_Ack) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "incorrect response"); return FALSE; } /* success */ return TRUE; } static gboolean fu_logitech_bulkcontroller_device_set_time(FuLogitechBulkcontrollerDevice *self, GError **error) { return fu_device_retry(FU_DEVICE(self), fu_logitech_bulkcontroller_device_set_time_cb, MAX_SETUP_RETRIES, NULL, error); } static gboolean fu_logitech_bulkcontroller_device_transition_to_device_mode_cb(FuDevice *device, gpointer user_data, GError **error) { FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device); FuLogitechBulkcontrollerProtoId proto_id = kProtoId_UnknownId; g_autoptr(GByteArray) req = NULL; g_autoptr(GByteArray) res = NULL; g_autoptr(GByteArray) decoded_pkt = NULL; req = fu_logitech_bulkcontroller_proto_manager_generate_transition_to_device_mode_request( device); res = fu_logitech_bulkcontroller_device_sync_write(self, req, error); if (res == NULL) return FALSE; decoded_pkt = fu_logitech_bulkcontroller_proto_manager_decode_message(res->data, res->len, &proto_id, error); if (decoded_pkt == NULL) { g_prefix_error(error, "failed to unpack packet: "); return FALSE; } g_debug("received transition mode response: id: %u, length %u", proto_id, res->len); if (proto_id != kProtoId_TransitionToDeviceModeResponse) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "incorrect response"); return FALSE; } /* success */ return TRUE; } static gboolean fu_logitech_bulkcontroller_device_transition_to_device_mode(FuLogitechBulkcontrollerDevice *self, GError **error) { return fu_device_retry(FU_DEVICE(self), fu_logitech_bulkcontroller_device_transition_to_device_mode_cb, MAX_SETUP_RETRIES, NULL, error); } static gboolean fu_logitech_bulkcontroller_device_clear_queue_cb(FuDevice *device, gpointer user_data, GError **error) { FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device); gsize actual_length = 0; g_autofree guint8 *buf = g_malloc0(self->transfer_bufsz); g_autoptr(GError) error_local = NULL; if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), self->sync_ep[EP_IN], buf, self->transfer_bufsz, &actual_length, 250, /* ms */ NULL, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT)) { g_debug("timed out successfully"); return TRUE; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } /* failed */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "got valid data, so keep going"); return FALSE; } static gboolean fu_logitech_bulkcontroller_device_clear_queue(FuLogitechBulkcontrollerDevice *self, GError **error) { g_debug("clearing any bulk data"); return fu_device_retry(FU_DEVICE(self), fu_logitech_bulkcontroller_device_clear_queue_cb, 3, NULL, error); } static gboolean fu_logitech_bulkcontroller_device_check_buffer_size(FuLogitechBulkcontrollerDevice *self, GError **error) { g_autoptr(GByteArray) buf = NULL; g_autoptr(GError) error_local = NULL; if (!fu_logitech_bulkcontroller_device_sync_send_cmd( self, FU_LOGITECH_BULKCONTROLLER_CMD_CHECK_BUFFERSIZE, NULL, /* data */ error)) { g_prefix_error(error, "failed to send request: "); return FALSE; } buf = fu_logitech_bulkcontroller_device_sync_wait_cmd_retry( self, FU_LOGITECH_BULKCONTROLLER_CMD_CHECK_BUFFERSIZE, 0x0, /* always zero */ &error_local); if (buf != NULL) { self->transfer_bufsz = 16 * 1024; } else { g_debug("sticking to 8k buffersize: %s", error_local->message); } /* success */ return TRUE; } static gboolean fu_logitech_bulkcontroller_device_setup(FuDevice *device, GError **error) { FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device); /* the hardware is unable to handle requests -- firmware issue */ if (fu_device_has_private_flag(device, FU_LOGITECH_BULKCONTROLLER_DEVICE_FLAG_POST_INSTALL)) { fu_device_sleep(device, POST_INSTALL_SLEEP_DURATION); fu_device_remove_private_flag(device, FU_LOGITECH_BULKCONTROLLER_DEVICE_FLAG_POST_INSTALL); } /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_logitech_bulkcontroller_device_parent_class) ->setup(device, error)) { g_prefix_error(error, "failed to FuUsbDevice->setup: "); return FALSE; } /* empty the queue */ if (!fu_logitech_bulkcontroller_device_clear_queue(self, error)) { g_prefix_error(error, "failed to clear queue: "); return FALSE; } /* check if the device supports a 16kb transfer buffer */ if (fu_device_has_private_flag(device, FU_LOGITECH_BULKCONTROLLER_DEVICE_FLAG_CHECK_BUFFER_SIZE)) { if (!fu_logitech_bulkcontroller_device_check_buffer_size(self, error)) { g_prefix_error(error, "failed to check buffer size: "); return FALSE; } } /* device supports modes of Device (supported), Appliance and BYOD (both unsupported) */ if (!fu_logitech_bulkcontroller_device_transition_to_device_mode(self, error)) { g_prefix_error(error, "failed to transition to device_mode: "); return FALSE; } /* set device time */ if (!fu_logitech_bulkcontroller_device_set_time(self, error)) { g_prefix_error(error, "failed to set time: "); return FALSE; } /* load current device data */ if (!fu_logitech_bulkcontroller_device_ensure_info(self, TRUE, error)) { g_prefix_error(error, "failed to ensure info: "); return FALSE; } /* success */ return TRUE; } static void fu_logitech_bulkcontroller_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 90, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 10, "reload"); } static void fu_logitech_bulkcontroller_device_init(FuLogitechBulkcontrollerDevice *self) { self->transfer_bufsz = 8 * 1024; self->device_info_response_json = g_string_new(NULL); fu_device_add_protocol(FU_DEVICE(self), "com.logitech.vc.proto"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_INSTALL_PARENT_FIRST); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_RETRY_OPEN); fu_usb_device_set_claim_retry_count(FU_USB_DEVICE(self), 100); fu_device_retry_set_delay(FU_DEVICE(self), 1000); fu_device_set_remove_delay(FU_DEVICE(self), 10 * 60 * 1000); /* >1 min to finish init */ fu_device_register_private_flag(FU_DEVICE(self), FU_LOGITECH_BULKCONTROLLER_DEVICE_FLAG_CHECK_BUFFER_SIZE); fu_device_register_private_flag(FU_DEVICE(self), FU_LOGITECH_BULKCONTROLLER_DEVICE_FLAG_POST_INSTALL); /* these are unrecoverable */ fu_device_retry_add_recovery(FU_DEVICE(self), FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, NULL); fu_device_retry_add_recovery(FU_DEVICE(self), FWUPD_ERROR, FWUPD_ERROR_PERMISSION_DENIED, NULL); } static void fu_logitech_bulkcontroller_device_finalize(GObject *object) { FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(object); g_string_free(self->device_info_response_json, TRUE); G_OBJECT_CLASS(fu_logitech_bulkcontroller_device_parent_class)->finalize(object); } static void fu_logitech_bulkcontroller_device_class_init(FuLogitechBulkcontrollerDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_logitech_bulkcontroller_device_finalize; device_class->to_string = fu_logitech_bulkcontroller_device_to_string; device_class->write_firmware = fu_logitech_bulkcontroller_device_write_firmware; device_class->probe = fu_logitech_bulkcontroller_device_probe; device_class->setup = fu_logitech_bulkcontroller_device_setup; device_class->set_progress = fu_logitech_bulkcontroller_device_set_progress; } fwupd-2.0.10/plugins/logitech-bulkcontroller/fu-logitech-bulkcontroller-device.h000066400000000000000000000010751501337203100301060ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_LOGITECH_BULKCONTROLLER_DEVICE (fu_logitech_bulkcontroller_device_get_type()) G_DECLARE_FINAL_TYPE(FuLogitechBulkcontrollerDevice, fu_logitech_bulkcontroller_device, FU, LOGITECH_BULKCONTROLLER_DEVICE, FuUsbDevice) #define FU_LOGITECH_BULKCONTROLLER_DEVICE_FLAG_CHECK_BUFFER_SIZE "check-buffer-size" #define FU_LOGITECH_BULKCONTROLLER_DEVICE_FLAG_POST_INSTALL "post-install" fwupd-2.0.10/plugins/logitech-bulkcontroller/fu-logitech-bulkcontroller-plugin.c000066400000000000000000000047251501337203100301450ustar00rootroot00000000000000/* * Copyright 1999-2021 Logitech, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-logitech-bulkcontroller-child.h" #include "fu-logitech-bulkcontroller-device.h" #include "fu-logitech-bulkcontroller-plugin.h" struct _FuLogitechBulkcontrollerPlugin { FuPlugin parent_instance; gboolean post_install; }; G_DEFINE_TYPE(FuLogitechBulkcontrollerPlugin, fu_logitech_bulkcontroller_plugin, FU_TYPE_PLUGIN) static void fu_logitech_bulkcontroller_plugin_to_string(FuPlugin *plugin, guint idt, GString *str) { FuLogitechBulkcontrollerPlugin *self = FU_LOGITECH_BULKCONTROLLER_PLUGIN(plugin); fwupd_codec_string_append_bool(str, idt, "PostInstall", self->post_install); } static void fu_logitech_bulkcontroller_plugin_init(FuLogitechBulkcontrollerPlugin *self) { } static gboolean fu_logitech_bulkcontroller_plugin_write_firmware(FuPlugin *plugin, FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuLogitechBulkcontrollerPlugin *self = FU_LOGITECH_BULKCONTROLLER_PLUGIN(plugin); g_autoptr(FuDeviceLocker) locker = NULL; locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; if (!fu_device_write_firmware(device, firmware, progress, flags, error)) return FALSE; self->post_install = TRUE; return TRUE; } static gboolean fu_logitech_bulkcontroller_plugin_device_created(FuPlugin *plugin, FuDevice *device, GError **error) { FuLogitechBulkcontrollerPlugin *self = FU_LOGITECH_BULKCONTROLLER_PLUGIN(plugin); if (self->post_install) { fu_device_add_private_flag(device, FU_LOGITECH_BULKCONTROLLER_DEVICE_FLAG_POST_INSTALL); self->post_install = FALSE; } return TRUE; } static void fu_logitech_bulkcontroller_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_set_device_gtype_default(plugin, FU_TYPE_LOGITECH_BULKCONTROLLER_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_LOGITECH_BULKCONTROLLER_CHILD); /* coverage */ } static void fu_logitech_bulkcontroller_plugin_class_init(FuLogitechBulkcontrollerPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_logitech_bulkcontroller_plugin_constructed; plugin_class->write_firmware = fu_logitech_bulkcontroller_plugin_write_firmware; plugin_class->device_created = fu_logitech_bulkcontroller_plugin_device_created; plugin_class->to_string = fu_logitech_bulkcontroller_plugin_to_string; } fwupd-2.0.10/plugins/logitech-bulkcontroller/fu-logitech-bulkcontroller-plugin.h000066400000000000000000000004721501337203100301450ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuLogitechBulkcontrollerPlugin, fu_logitech_bulkcontroller_plugin, FU, LOGITECH_BULKCONTROLLER_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/logitech-bulkcontroller/fu-logitech-bulkcontroller.rs000066400000000000000000000031321501337203100270420ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] enum FuLogitechBulkcontrollerDeviceState { Unknown = -1, Offline, Online, Idle, InUse, AudioOnly, Enumerating, } #[derive(ToString)] enum FuLogitechBulkcontrollerUpdateState { Unknown = -1, Current, Available, Starting = 3, Downloading, Ready, Updating, Scheduled, Error, } #[repr(u32le)] #[derive(ToString)] enum FuLogitechBulkcontrollerCmd { CheckBuffersize = 0xCC00, Init = 0xCC01, StartTransfer = 0xCC02, DataTransfer = 0xCC03, EndTransfer = 0xCC04, Uninit = 0xCC05, BufferRead = 0xCC06, BufferWrite = 0xCC07, UninitBuffer = 0xCC08, Ack = 0xFF01, Timeout = 0xFF02, Nack = 0xFF03, } #[derive(New, ToString, Getters)] #[repr(C, packed)] struct FuStructLogitechBulkcontrollerSendSyncReq { cmd: FuLogitechBulkcontrollerCmd, payload_length: u32le, sequence_id: u32le, } #[derive(Parse)] #[repr(C, packed)] struct FuStructLogitechBulkcontrollerSendSyncRes { cmd: FuLogitechBulkcontrollerCmd, payload_length: u32le, sequence_id: u32le, } #[derive(New)] #[repr(C, packed)] struct FuStructLogitechBulkcontrollerUpdateReq { cmd: FuLogitechBulkcontrollerCmd, payload_length: u32le, } #[derive(Getters)] #[repr(C, packed)] struct FuStructLogitechBulkcontrollerUpdateRes { cmd: FuLogitechBulkcontrollerCmd, _payload_length: u32le, cmd_req: FuLogitechBulkcontrollerCmd, } enum FuLogitechBulkcontrollerChecksumType { Sha256, Sha512, Md5, } fwupd-2.0.10/plugins/logitech-bulkcontroller/logitech-bulkcontroller.quirk000066400000000000000000000010261501337203100271410ustar00rootroot00000000000000# TODO: revisit InstallDuration [USB\VID_046D&PID_089B] Plugin = logitech_bulkcontroller InstallDuration = 1500 [USB\VID_046D&PID_08D3] Plugin = logitech_bulkcontroller InstallDuration = 1500 Flags = is-mini [USB\VID_046D&PID_087C] Plugin = logitech_bulkcontroller InstallDuration = 1500 [USB\VID_046D&PID_0963] Plugin = logitech_bulkcontroller InstallDuration = 1500 # Sight [LOGI\MODEL_VU0067] InstallDuration = 1500 # Sight as Standalone device [USB\VID_046D&PID_087A] Plugin = logitech_bulkcontroller InstallDuration = 1500 fwupd-2.0.10/plugins/logitech-bulkcontroller/meson.build000066400000000000000000000015421501337203100233740ustar00rootroot00000000000000if protobufc.found() and protoc.found() cargs = ['-DG_LOG_DOMAIN="FuPluginLogitechBulkController"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('logitech-bulkcontroller.quirk') subdir('proto') plugin_builtins += static_library('fu_plugin_logitech_bulkcontroller', rustgen.process('fu-logitech-bulkcontroller.rs'), sources: [ generated, 'fu-logitech-bulkcontroller-common.c', 'fu-logitech-bulkcontroller-child.c', 'fu-logitech-bulkcontroller-device.c', 'fu-logitech-bulkcontroller-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) enumeration_data += files( 'tests/logi-rally-bar-setup.json', 'tests/logi-sight-setup.json', ) device_tests += files( 'tests/logi-rally-bar.json', 'tests/logi-sight.json', ) endif fwupd-2.0.10/plugins/logitech-bulkcontroller/proto/000077500000000000000000000000001501337203100223735ustar00rootroot00000000000000fwupd-2.0.10/plugins/logitech-bulkcontroller/proto/antiflicker.proto000066400000000000000000000012021501337203100257460ustar00rootroot00000000000000/* * Copyright 1999-2023 Logitech, Inc. * All Rights Reserved * * SPDX-License-Identifier: LGPL-2.1-or-later */ syntax = "proto3"; package logi.device.proto; option java_package = "com.logitech.vc.proto"; import "device_common.proto"; /** * This message data structure holds information about the * current AntiFlicker configuration. * */ message AntiFlickerConfiguration { enum Mode { NTSC_60HZ = 0; PAL_50HZ = 1; } Mode mode = 1; } message SetAntiFlickerConfigurationRequest { AntiFlickerConfiguration.Mode mode = 1; } message SetAntiFlickerConfigurationResponse { bool success = 1; repeated Error errors = 2; } fwupd-2.0.10/plugins/logitech-bulkcontroller/proto/ble_cfg.proto000066400000000000000000000007011501337203100250370ustar00rootroot00000000000000/* * Copyright 1999-2023 Logitech, Inc. * All Rights Reserved * * SPDX-License-Identifier: LGPL-2.1-or-later */ syntax = "proto3"; package logi.device.proto; option java_package = "com.logitech.vc.proto"; import "device_common.proto"; message SetBLECfgRequest { /** * (REQUIRED) If true, BLE is enabled and active otherwise disabled */ bool BLE_ON = 1; } message SetBLECfgResponse { bool success = 1; repeated Error errors = 2; } fwupd-2.0.10/plugins/logitech-bulkcontroller/proto/crash_info.proto000066400000000000000000000216611501337203100256010ustar00rootroot00000000000000/* * Copyright 1999-2023 Logitech, Inc. * All Rights Reserved * * SPDX-License-Identifier: LGPL-2.1-or-later */ syntax = "proto3"; package logi.device.proto; option java_package = "com.logitech.vc.proto"; import "device_common.proto"; /** * Kong as an Android device can accumulate * crash debug information during its operation. * When Kong is running in device mode, those * crash dump files need to be copied over to * PC and uploaded to S3. * Note, if Kong is running in host mode, uploaded * files, and then moved to device mode, will it * copy the same files over? * * This message requests that crash dump files be * copied over to PC * * EXPECTED RESPONSE * SendCrashDumpResponse * */ message SendCrashDumpRequest { /** * Unused. Reserved for future use. */ bool reserved = 1; } /** * Crash dump information. Most of these * are supplied by the crash analytics service, so lets * pass this information along. */ message CrashDumpInfo { /** * the filename */ string file_name = 1; /** * the serial number */ string device_id = 2; /** * the software version */ string software_version = 3; /** * the file size */ uint64 file_size = 4; /** * timestamp */ uint64 timestamp = 5; /** * md5 for file */ string md5 = 6; /** * the device type . Kong|Diddy */ string device_type = 7; /** * the device mode. Hosted|Appliance */ string device_mode = 8; /** * the report type. BugReport|EventLog,Diagnostics */ string report_type = 9; /** * the content type. application/zip | text/plain | application/json */ string content_type = 10; } /** * Response which contains the crash dump file name * information and bool value to indicate will send * file */ message SendCrashDumpResponse { /** * (OPTIONAL) * If crash dump exists, this variable * contains the file name of crash dump * that will be copied over. */ string crash_dump_file = 1; /** * (REQUIRED) * bool value to indicate will send file * true if sending file over. * false if no file to send. * If true, caller will look at CrashDumpInfo */ bool will_send_file = 2; /** * (OPTIONAL) * Crash dump info */ CrashDumpInfo crash_dump_info = 3; } message SendCrashDumpRequestv2 { /** * The attestation challenge. * (REQUIRED) */ string challenge = 1; /** * Time to live * (REQUIRED) */ int32 ttl = 2; } /** * Response which contains the crash dump file name * information, bool value to indicate will send * file, body of the request and signature */ message SendCrashDumpResponsev2 { /** * (OPTIONAL) * If crash dump exists, this variable * contains the file name of crash dump * that will be copied over. */ string crash_dump_file = 1; /** * (REQUIRED) * bool value to indicate will send file * true if sending file over. * false if no file to send. * If true, caller will look at CrashDumpInfo */ bool will_send_file = 2; /** * (OPTIONAL) * The get upload url body. This is a json string */ string body = 3; /** * (OPTIONAL) * The get upload url body signature. */ string signature = 4; } /** * This is event sent from PC or Kong to indicate * Success */ message SendCrashDumpEvent { /** * (REQUIRED) * Contains the file name of crash dump * that is being sent or in process of being * received */ string crash_dump_file = 1; /** * (REQUIRED) * Transfer state. * true indicates file was received without errors and bug report file was uploaded * false means an error occurred */ bool success = 2; } /** * Place holder for Android requesting that a crash dump copy * get initiated from PC side */ message CrashDumpAvailableEvent { /** * Unused. Reserved for future use. */ bool reserved = 1; } /** * Ask device to generate a bug report. This could be * for gathering logcat, system logs, etc. * Similar to SendCrashDumpRequestv2, but bug report generation is on * demand. * EXPECTED RESPONSE: * GenerateCrashDumpResponse * It should follow the same flow as described here * https://docs.google.com/document/d/1D5nx1nenDu9ucZbYPXlNNxFEN1tx3W7k044mvi74x28/edit#heading=h.a9wyfbpb2282 */ message GenerateCrashDumpRequest { /** * The attestation challenge. * (REQUIRED) */ string challenge = 1; /** * Time to live * (REQUIRED) */ int32 ttl = 2; /** * The note to include in the bug report. This could be empty. * (OPTIONAL) */ string note = 3; /** * (OPTIONAL) * serial number of the intended recipient of the command. * If empty, the receiver should handle the command as the intended recipient. This also * handles backward compatibility with older Sync app where serial number is not defined. */ string serial_number = 4; /** * (OPTIONAL) * Request reference for connected devices. This could be empty for backwards * compatibility. */ string request_ref = 5; } /** * Response which contains the * crash dump file name information, * bool value to indicate will send file, * body of the request and signature. * Similar to SendCrashDumpResponsev2, but bug report generation is on * demand. * It should follow the same flow as described here * https://docs.google.com/document/d/1D5nx1nenDu9ucZbYPXlNNxFEN1tx3W7k044mvi74x28/edit#heading=h.a9wyfbpb2282 */ message GenerateCrashDumpResponse { /** * (OPTIONAL) * If crash dump exists, this variable * contains the file name of crash dump * that will be copied over. */ string crash_dump_file = 1; /** * (REQUIRED) * bool value to indicate will send file * true if sending file over. * false if no file to send. */ bool will_send_file = 2; /** * (OPTIONAL) * The get upload url body. This is a json string */ string body = 3; /** * (OPTIONAL) * The get upload url body signature. */ string signature = 4; } /** * Ask device to copy test result. * EXPECTED RESPONSE: * SendTestResultResponse */ message SendTestResultRequest { /** * Unused. Reserved for future use. */ bool reserved = 1; } /** * Test result response. */ message SendTestResultResponse { /** * (OPTIONAL) * If test result file exists, this variable * contains the file name * that will be copied over. */ string test_result_file = 1; /** * (REQUIRED) * bool value to indicate will send file * true if sending file over. * false if no file to send. */ bool will_send_file = 2; } /** * Device can use to notify Sync app to call * GetMemfaultManifestRequest API with attestation... */ message InitiateMemfaultManifestRequestEvent { /** * Unused. Reserved for future use. */ bool reserved = 1; } /** * Request attested manifest from device to be used in retrieving memfault * settings from futen. * This is to be included in UsbMsg. * * EXPECTED RESPONSE * GetMemfaultManifestResponse * Added 10/12/2022 EE */ message GetMemfaultManifestRequest { /** * The device serial number. * Reserve for future versions to indicate target device. * (OPTIONAL) */ string serial = 1; /** * The attestation challenge. * (REQUIRED) */ string challenge = 2; /** * Time to live * (REQUIRED) */ int32 ttl = 3; /** * Sender to provide in case of issue responding to initiate request * i.e Sender is busy, firmware update in progress, etc. */ Error error = 4; } /** * GetMemfaultManifestRequest response. * The device should send this message after receiving GetMemfaultManifestRequest. * If the device encounters any issues, this message should be sent with the error field filled * out. An empty error field indicates success. */ message GetMemfaultManifestResponse { /** * The device serial number. * Reserve for future versions to indicate target device. * (OPTIONAL) */ string serial = 1; /** * The manifest body. This is a json string * (REQUIRED) */ string body = 2; /** * The manifest body signature. * (REQUIRED) */ string signature = 3; /** * If any error are encountered while processing the request, * the device should respond with this error field. * (OPTIONAL) */ Error error = 4; } /** * For sending memfault settings to device. * * EXPECTED RESPONSE * SendMemfaultSettingsResponse * Added 10/12/2022 EE */ message SendMemfaultSettingsRequest { /** * The device serial number. * Reserve for future versions to indicate target device. * (OPTIONAL) */ string serial = 1; /** * (REQUIRED) * The memfault settings in bytes. */ bytes memfault_settings = 2; /** * (REQUIRED) * The memfault settings md5 hash */ string md5 = 3; } /** * Response to SendMemfaultSettingsRequest request. * The device should send this message after receiving SendMemfaultSettingsRequest. * If the device encounters any issues, this message should be sent with the error field filled * out. An empty error field indicates success. */ message SendMemfaultSettingsResponse { /** * If any error are encountered while processing the request, * the device should respond with this error field. * (OPTIONAL) */ Error error = 1; } fwupd-2.0.10/plugins/logitech-bulkcontroller/proto/device_attestation.proto000066400000000000000000000011701501337203100273350ustar00rootroot00000000000000/* * Copyright 1999-2023 Logitech, Inc. * All Rights Reserved * * SPDX-License-Identifier: LGPL-2.1-or-later */ syntax = "proto3"; package logi.device.proto; option java_package = "com.logitech.vc.proto"; /** * Request for certificate chain * This is to be included in UsbMsg * EXPECTED RESPONSE * GetCertificateChainResponse */ message GetCertificateChainRequest { /** * attestation challenge */ string attestation = 1; /** * time to live */ int32 ttl = 2; } /** * Get certificate chain response */ message GetCertificateChainResponse { /** * array of certs */ repeated string certchain = 1; } fwupd-2.0.10/plugins/logitech-bulkcontroller/proto/device_common.proto000066400000000000000000000022631501337203100262720ustar00rootroot00000000000000/* * Copyright 1999-2023 Logitech, Inc. * All Rights Reserved * * SPDX-License-Identifier: LGPL-2.1-or-later */ syntax = "proto3"; package logi.device.proto; option java_package = "com.logitech.vc.proto"; /** * This error messages describe a failure that was encountered * by the Sync service and primarily consist of an error code * and a short, human-readable message. Therefore, if a client * receives a message with a field reserved for Error messages, * it is prudent that the application first check if there are * errors before doing any further processing of the message. */ message Error { /** * (REQUIRED) Error code. */ uint32 error_code = 1; /** * (OPTIONAL) Short, human-readable error message. If no * message is available, then this will be an empty string. */ string error_message = 2; /** * (OPTIONAL) A URI to a log file or some other document * that contains more detailed information about the error. * If such a file is not available, this will be an empty * string. */ string error_log_uri = 3; /** * (OPTIONAL) An optional JSON string with additional * metadata that may be useful to the client. */ string json_metadata = 4; } fwupd-2.0.10/plugins/logitech-bulkcontroller/proto/device_info.proto000066400000000000000000000011111501337203100257240ustar00rootroot00000000000000/* * Copyright 1999-2023 Logitech, Inc. * All Rights Reserved * * SPDX-License-Identifier: LGPL-2.1-or-later */ syntax = "proto3"; package logi.device.proto; option java_package = "com.logitech.vc.proto"; /** * Request Device information * This is to be included in UsbMsg * EXPECTED RESPONSE * GetDeviceInfoResponse */ message GetDeviceInfoRequest { /** * Unused. Reserved for future use. */ bool reserved = 1; } /** * Get device information response */ message GetDeviceInfoResponse { /** * payload contains actual mqtt message */ string payload = 1; } fwupd-2.0.10/plugins/logitech-bulkcontroller/proto/device_mode.proto000066400000000000000000000070241501337203100257260ustar00rootroot00000000000000/* * Copyright 1999-2023 Logitech, Inc. * All Rights Reserved * * SPDX-License-Identifier: LGPL-2.1-or-later */ syntax = "proto3"; package logi.device.proto; option java_package = "com.logitech.vc.proto"; /** * Behavior change as of 1/28/2021 EE * Kong sync-agent should not deprovision when this message is * received. If would just start forwarding events to PC when message is * received. * * (Legacy) * Request to transition to device mode * Kong could be provisioned in Host mode. This message * will ask Kong to deprovisioned/remove host mode provisioning * data. * This is to be included in UsbMsg * EXPECTED RESPONSE * TransitionToDeviceModeResponse */ message TransitionToDeviceModeRequest { /** * Unused. Reserved for future use. */ bool reserved = 1; /** * The sender of request => * Possible values: * 0 - PC (default) * 1 - COS device */ int32 sender = 2; } /** * Request to transition to device mode response */ message TransitionToDeviceModeResponse { /** * boolean value to indicate Kong was able to transition to * device mode. If Kong is not provisioned, should just respond * with true value. * set to false if error was encountered during transition, and Kong * wasn't able to transition (is this possible?) */ bool success = 1; /** * the error in integer if success was false */ int32 error = 2; /** * the error description */ string error_description = 3; } /** * Added 1/28/2021 EE * Request to deprovision Kong * This request is sent by PC sync-agent when PC * is provisioned. * Kong sync-agent should deprovision (if provisioned) * * EXPECTED RESPONSE * SetDeprovisionResponse */ message SetDeprovisionRequest { /** * Unused. Reserved for future use. */ bool reserved = 1; } /** * Response to deprovision request */ message SetDeprovisionResponse { /** * boolean value to indicate Kong was able to deprovision Kong. * If Kong is not provisioned, should just respond * with true value. * set to false if error was encountered during deprovisioning. */ bool success = 1; /** * the error in integer if success was false */ int32 error = 2; /** * the error description */ string error_description = 3; } /** * Added 3/22/2021 EE * For sending a certificate as data. There are currently * 2 known certificate that will be transferred - Root CA, and 802.1x cert. * Upon receipt, sync-agent should verify using the supplied hash * and write the data to the file system. * * EXPECTED RESPONSE * SendCertificateDataResponse */ message SendCertificateDataRequest { /** * The certificate type */ enum CertType { /** * Reserved. Do not use. */ RESERVED = 0; /** * Root CA */ ROOT_CA = 1; /** * 802.1x cert */ NET_CONFIG = 2; } /** * (REQUIRED) * The certificate type */ CertType cert_type = 1; /** * (REQUIRED) * the certificate file name */ string file_name = 2; /** * (REQUIRED) * the certificate data */ bytes cert_data = 3; /** * (REQUIRED) * the certificate md5 hash */ string md5 = 4; } /** * Response to SendCertificateData Request */ message SendCertificateDataResponse { /** * (REQUIRED) * boolean value to indicate data was received, hash verified . * set to false if error was encountered during transfer and verification. */ bool success = 1; /** * (OPTIONAL) * the error in integer if success was false */ int32 error = 2; /** * (OPTIONAL) * the error description if there are errors */ string error_description = 3; } fwupd-2.0.10/plugins/logitech-bulkcontroller/proto/device_request.proto000066400000000000000000000157761501337203100265070ustar00rootroot00000000000000/* * Copyright 1999-2023 Logitech, Inc. * All Rights Reserved * * SPDX-License-Identifier: LGPL-2.1-or-later */ syntax = "proto3"; package logi.device.proto; option java_package = "com.logitech.vc.proto"; /** * For reboot schedule request defined here * https://docs.google.com/document/d/10pG89Cw_siDvnmsKxdLQqutIqegH6ouSnMon_G28Nyc/edit#bookmark=id.k0bz8vzzaj9 */ message RebootSchedule { /** * The time to reboot. * If this is empty and ts is non-zero, it means to clear the reboot schedule */ string when = 1; /** * repeat mode defined here * https://docs.google.com/document/d/1yQp8Ju82bDuVfHmprP_5ToUJbitK7kOjmj_71Cqc-do/edit#heading=h.k8yyuyddqj1v */ uint32 repeat = 2; /** * timestamp the schedule request was made */ uint64 ts = 3; } /** * Request to reboot device * This is to be included in UsbMsg * * After the device receives this, the device should send a response, * followed by an mqtt event that conforms to device reboot or * schedule reboot defined here * https://docs.google.com/document/d/10pG89Cw_siDvnmsKxdLQqutIqegH6ouSnMon_G28Nyc/edit#bookmark=id.zcy4ldnyuij * and * https://docs.google.com/document/d/10pG89Cw_siDvnmsKxdLQqutIqegH6ouSnMon_G28Nyc/edit#bookmark=id.k0bz8vzzaj9 * * EXPECTED RESPONSE * RebootDeviceResponse */ message RebootDeviceRequest { /** * Unused. Reserved for future use. */ bool reserved = 1; /** * A timestamp indicating when the reboot request * was initiated. * The device should include this entry as part of the event information * it sends back to PC during a reboot request. */ uint64 iat = 2; /** * Below are newly defined attributes that includes rebootSchedule, * and also tries to keep the request in-line with cloud request * Note: * older versions of Sync app would not know about below */ /** * (REQUIRED)Reboot strategy defined in * https://docs.google.com/document/d/10pG89Cw_siDvnmsKxdLQqutIqegH6ouSnMon_G28Nyc/edit# */ uint32 strategy = 3; /** * (REQUIRED) Same behavior as in Raiden Backend API */ bool rebootNow = 4; /** * (OPTIONAL) Same behavior as in Raiden Backend API */ RebootSchedule schedule = 5; /** * (REQUIRED) For device to distinguish between older request and new one that * support schedule reboot. * This defaults to 0 for older Sync app. Sync app that supports * schedule reboot will pass with value 1. */ uint32 version = 6; /** * (OPTIONAL) * serial number of the intended recipient of the command. * If empty, the receiver should handle the command as the intended recipient. This also * handles backward compatibility with older Sync app where serial number is not defined. * Behavior for host and peripheral device: if command is for host device, host will * handle command and forward the command to peripheral device as well. If for peripheral * device, host will just forward command to peripheral device. */ string serial_number = 7; } /** * Reboot device response */ message RebootDeviceResponse { /** * bool value to indicate reboot was requested. If there are errors * while requesting a device to reboot, should set the value to false */ bool success = 1; } /** * This message requests that the speaker boost audio setting be changed. * The device should send a device info event after this setting request are handled. * * EXPECTED RESPONSE * SetSpeakerBoostResponse * */ message SetSpeakerBoostRequest { /** * (REQUIRED) The speaker boost setting to be set * * If value is 0, the request is to disable. If 1, * the request is to enable. */ int32 speaker_boost = 1; } message SetSpeakerBoostResponse { /** * (REQUIRED) set to true if the audio setting request was successfully sent, false * otherwise */ bool success = 1; } /** * This message requests that the noise reduction audio setting be changed. * The device should send a device info event after this setting request are handled. * * EXPECTED RESPONSE * SetNoiseReductionResponse * */ message SetNoiseReductionRequest { /** * (REQUIRED) The noise reduction setting to be set * * If value is 0, the request is to disable. If 1, * the request is to enable. */ int32 noise_reduction = 1; } message SetNoiseReductionResponse { /** * (REQUIRED) set to true if the audio setting request was successfully sent, false * otherwise */ bool success = 1; } /** * This message requests that the reverb mode audio setting be changed. * The device should send a device info event after this setting request are handled. * * EXPECTED RESPONSE * SetReverbModeResponse * */ message SetReverbModeRequest { /** * Reverb mode enumeration */ enum ReverbMode { DISABLED = 0; MILD = 1; NORMAL = 2; AGGRESSIVE = 3; } /** * (REQUIRED) The reverb mode setting to be set * * see Reverb mode enumeration */ ReverbMode reverb_mode = 1; } message SetReverbModeResponse { /** * (REQUIRED) set to true if the setting request was successfully sent, false otherwise */ bool success = 1; } /** * This message requests that the microphone eq mode audio setting be changed. * The device should send a device info event after this setting request are handled. * * EXPECTED RESPONSE * SetMicEQModeResponse * */ message SetMicEQModeRequest { /** * MicEQ mode enumeration */ enum MicEQMode { BASSBOOST = 0; NORMAL = 1; VOICEBOOST = 2; } /** * (REQUIRED) The microphone eq setting to be set * * see MicEQ mode enumeration */ MicEQMode mic_eq_mode = 1; } message SetMicEQModeResponse { /** * (REQUIRED) set to true if the setting request was successfully sent, false otherwise */ bool success = 1; } /** * This message requests that the speaker eq mode audio setting be changed. * The device should send a device info event after this setting request are handled. * * EXPECTED RESPONSE * SetSpeakerEQModeResponse * */ message SetSpeakerEQModeRequest { /** * SpeakerEQ mode enumeration */ enum SpeakerEQMode { BASSBOOST = 0; NORMAL = 1; VOICEBOOST = 2; } /** * (REQUIRED) The speaker eq setting to be set * * see SpeakerEQ mode enumeration */ SpeakerEQMode speaker_eq_mode = 1; } message SetSpeakerEQModeResponse { /** * (REQUIRED) set to true if the setting request was successfully sent, false otherwise */ bool success = 1; } /** * This message requests that the device forgets a peripheral. * After forgetting the peripheral, the device should send ForgetDeviceResponse * and indicate success or false. * The device should also send a device info event after sending the response * to indicate the new peripheral state. * Note: micpod uid comes from device in this format "uid": "33", * but Sync stores this as hex string , like uuid: "0x0021". * Sync app will send the uuid in hex string format. * EXPECTED RESPONSE * ForgetDeviceResponse * */ message ForgetDeviceRequest { /** * (REQUIRED) The uuid of peripheral to forget * */ string uuid = 1; } message ForgetDeviceResponse { /** * (REQUIRED) set to true if forget request was successfully handled */ bool success = 1; } fwupd-2.0.10/plugins/logitech-bulkcontroller/proto/device_time.proto000066400000000000000000000007171501337203100257420ustar00rootroot00000000000000/* * Copyright 1999-2023 Logitech, Inc. * All Rights Reserved * * SPDX-License-Identifier: LGPL-2.1-or-later */ syntax = "proto3"; package logi.device.proto; option java_package = "com.logitech.vc.proto"; /** * Request for setting device time * This is to be included in UsbMsg */ message SetDeviceTimeRequest { /** * utc timestamp. */ uint64 ts = 1; /** * the time zone. */ string time_zone = 2; } /** * Send an ack as the response */ fwupd-2.0.10/plugins/logitech-bulkcontroller/proto/firmware_update.proto000066400000000000000000000010611501337203100266340ustar00rootroot00000000000000/* * Copyright 1999-2023 Logitech, Inc. * All Rights Reserved * * SPDX-License-Identifier: LGPL-2.1-or-later */ syntax = "proto3"; package logi.device.proto; option java_package = "com.logitech.vc.proto"; /** * Request to start update * This is to be included in UsbMsg * EXPECTED RESPONSE * UpdateNowResponse */ message UpdateNowRequest { /** * Unused. Reserved for future use. */ bool reserved = 1; } /** * Update now response */ message UpdateNowResponse { /** * bool value to indicate update was started */ bool started = 1; } fwupd-2.0.10/plugins/logitech-bulkcontroller/proto/meson.build000066400000000000000000000011111501337203100245270ustar00rootroot00000000000000 gen = generator(protoc, \ output: ['@BASENAME@.pb-c.c', '@BASENAME@.pb-c.h'], arguments: ['--proto_path=@CURRENT_SOURCE_DIR@', '--c_out=@BUILD_DIR@', '@INPUT@']) src = [ 'antiflicker.proto', 'ble_cfg.proto', 'crash_info.proto', 'device_attestation.proto', 'device_common.proto', 'device_info.proto', 'device_mode.proto', 'device_request.proto', 'device_time.proto', 'firmware_update.proto', 'rightsight.proto', 'ota_manifest.proto', 'usb_msg.proto', ] generated = gen.process(src) fwupd-2.0.10/plugins/logitech-bulkcontroller/proto/ota_manifest.proto000066400000000000000000000026121501337203100261320ustar00rootroot00000000000000/* * Copyright 1999-2023 Logitech, Inc. * All Rights Reserved * * SPDX-License-Identifier: LGPL-2.1-or-later */ syntax = "proto3"; package logi.device.proto; option java_package = "com.logitech.vc.proto"; /** * Request device to create a GetManifestv2 body. See * https://docs.google.com/document/d/1l31A1TWhtJC0xR8GwuNtiGN4vPLURRsj5ZcC1uEIwVQ/edit#heading=h.ctbthi1iyxw1 * * * This is to be included in UsbMsg * * EXPECTED RESPONSE * GetManifestBodyResponse */ message GetManifestBodyRequest { /** * The attestation challenge. * (REQUIRED) */ string challenge = 1; /** * The manifest version. * (REQUIRED) */ string version = 2; /** * The channel. Dont use if empty or null * (OPTIONAL) */ string channel = 3; /** * The meta info in json format. This * field usually comes from PC. * (OPTIONAL) */ string meta_info = 4; /** * Time to live * (REQUIRED) */ int32 ttl = 5; /** * Serial number of attached device * (OPTIONAL) */ string serial_number = 6; /** * target version * (OPTIONAL) */ string target_version = 7; } /** * GetManifestv2 body response */ message GetManifestBodyResponse { /** * The get manifest body. This is a json string */ string body = 1; /** * The get manifest body signature. */ string signature = 2; /** * Serial number of attached device * (OPTIONAL) */ string serial_number = 3; } fwupd-2.0.10/plugins/logitech-bulkcontroller/proto/rightsight.proto000066400000000000000000000225371501337203100256450ustar00rootroot00000000000000/* * Copyright 1999-2023 Logitech, Inc. * All Rights Reserved * * SPDX-License-Identifier: LGPL-2.1-or-later */ syntax = "proto3"; package logi.device.proto; option java_package = "com.logitech.vc.proto"; import "device_common.proto"; /** * This message data structure holds information about the * current RightSight configuration. * Note: * status = 0 = disabled/not working|1 = ok * * 11/5/2021 * Added modes to support RightSight 2 */ message RightSightConfiguration { /** * Enumeration of modes for speaker tracking. * For RightSight v2 */ enum SpeakerTrackingMode { /** * This is the old rightsight that we know that uses * camera to track persons. When selected, sub modes are * dynamic or call start. */ GROUP_VIEW = 0; /** * This does not indicate a default value. * */ SPEAKER_VIEW = 1; /** * Grid view. Valid when RightSight version=6 * */ GRID_VIEW = 2; } /** * Enumeration of speaker detection speed when in speaker view * For RightSight v2 */ enum SpeakerDetectionSpeed { SLOW_SPEAKER_SPEED = 0; DEFAULT_SPEAKER_SPEED = 1; FAST_SPEAKER_SPEED = 2; } /** * Enumeration of frame speed when in group view * For RightSight v2 * */ enum FramingSpeed { SLOW_FRAME_SPEED = 0; DEFAULT_FRAME_SPEED = 1; FAST_FRAME_SPEED = 2; } /** * Enumeration of modes that the RightSight service can be in. */ enum Mode { /** * This does not indicate a default value. * */ DO_NOT_USE = 0; /** * The camera will continually pan, tilt, and zoom * to properly frame everyone during a meeting. */ DYNAMIC = 1; /** * The camera will pan, tilt, and zoom to properly in * the meeting only when the call starts. */ ON_CALL_START = 2; } /** * (REQUIRED) If true, RightSight is enabled and active. */ bool enabled = 1; /** * (REQUIRED) The current mode that RightSight is in. */ Mode mode = 2; /** * (REQUIRED) A timestamp indicating when the RightSight * settings were last modified. This is the number of * milliseconds since the epoch. */ uint64 last_modified = 3; /** * (OPTIONAL) The tracking mode. * For RightSight v2 */ SpeakerTrackingMode trackingMode = 4; /** * (OPTIONAL) Valid when tracking mode is set to speaker view. Indicate if pip is * enabled or not. * Valid if device report has version 2 */ bool pip = 5; /** * (OPTIONAL) * Valid if trackingMode is speaker view * For RightSight v2 */ bool reduceTransitions = 6; /** * (OPTIONAL) * Valid if trackingMode is speaker view * For RightSight v2 */ SpeakerDetectionSpeed speakerDetectionSpeed = 7; /** * (DEPRECATED) * Frame speed value. * For RightSight v2 */ FramingSpeed framingSpeed = 8; /** * (OPTIONAL) * Group view frame speed value. * For RightSight v2 */ FramingSpeed groupFramingSpeed = 9; /** * (OPTIONAL) * Speaker view frame speed value. * For RightSight v2 */ FramingSpeed speakerFramingSpeed = 10; /** * (OPTIONAL) * 1. Valid when tracking mode is set to speaker view. * 2. Valid if device report has RS version 8 and Sight camera is connected. * * Indicate to use Sight camera in Speaker view * Possible values -1 = DISABLED - For option to signify off, greyed out, unable to select 0 = OFF - is Off and able to toggle 1 = ON - is On and able to toggle */ int32 useSight = 11; /** * (OPTIONAL) * Valid if device report has RS version 8 and Sight camera is connected. * * Intelligently switch between cameras to show the best view. * Possible values -1 = DISABLED - For option to signify off, greyed out, unable to select 0 = OFF - is Off and able to toggle 1 = ON - is On and able to toggle */ int32 smartSwitching = 12; } /** * RightSight is an auto-framing feature that is available in Kong. * With RightSight enabled, your device will automatically pan, tilt, and zoom * the camera lens in order to capture all meeting participants * within the image frame. This feature can be set to one of two * modes: dynamic and on call start. When in dynamic mode, the * device will actively pan, tilt, and zoom the camera lens when * appropriate in order to keep all participants in frame during * the entire course of the meeting. When in on call start mode, * the camera lens will pan, tilt, and zoom to capture everybody * in frame only when the meeting starts. * * When RightSight is enabled, it is set * to dynamic mode by default. * * This message requests that the RightSight configuration * settings be changed. * * EXPECTED RESPONSE * SetRightSightConfigurationResponse * Note: For RightSight v1 */ message SetRightSightConfigurationRequest { /** * (REQUIRED) If true, requests that RightSight be * turned on. If false, indicates that * RightSight should be turned off. */ bool enabled = 1; /** * (REQUIRED) The mode for RightSight to be in. A value is * required, but if none is provided, then this will * default to DYNAMIC mode. * * If enabled is set to false, then this will effectively * do nothing as RightSight is turned off. */ RightSightConfiguration.Mode mode = 2; } /** * Response which contains the RightSight configuration that was * set as a result of the request. */ message SetRightSightConfigurationResponse { /** * (OPTIONAL) If any errors occurred while processing the * request, then this field should be set accordingly. */ repeated Error errors = 1; /** * (REQUIRED) The RightSight configuration that was set on * the product. */ RightSightConfiguration right_sight_configuration = 2; } /** * * This message requests that the RightSight configuration * settings be changed. * After handling this request, the device should send the response and an updated device report. * * EXPECTED RESPONSE * SetRightSightConfigurationResponse Support CollabOS 1.5: RightSight 2 Group View the only additional options will be the dynamic/on call start Speed option not available in 1.5 Speaker view only the additional option for picture in picture will be available reduce transitions will not be available in 1.5 both speed options will not be available in 1.5 CollabOS 1.6 (version=3): Speaker view speaker detection framing speed Group View framing speed (version=4) separate framing speed variables for frame speed speakerFramingSpeed groupFramingSpeed * Note: For RightSight v2 */ message SetRightSightConfigurationRequestv2 { /** * (REQUIRED) If true, requests that RightSight be * turned on. If false, indicates that * RightSight should be turned off. */ bool enabled = 1; /** * (REQUIRED) The tracking mode for RightSight */ RightSightConfiguration.SpeakerTrackingMode trackingMode = 2; /** * (REQUIRED) The mode for RightSight to be in. A value is * required, but if none is provided, then this will * default to DYNAMIC mode. * Valid in Group view */ RightSightConfiguration.Mode mode = 3; /** * (OPTIONAL) Picture-in-picture. * Valid in speaker view */ bool pip = 4; /** * (OPTIONAL) Speaker detection speed * Valid in speaker view */ RightSightConfiguration.SpeakerDetectionSpeed speakerDetectionSpeed = 5; /** * DEPRECATED * (OPTIONAL) The framing speed. * value could be different based on tracking mode */ RightSightConfiguration.FramingSpeed framingSpeed = 6; /** * Not supported * (OPTIONAL) Reduce transitions * Valid in speaker view * * bool reduceTransitions = 7; */ /** * (OPTIONAL) The group framing speed. */ RightSightConfiguration.FramingSpeed groupFramingSpeed = 8; /** * (OPTIONAL) The speaker framing speed. */ RightSightConfiguration.FramingSpeed speakerFramingSpeed = 9; /** * (OPTIONAL) * 1. Valid when tracking mode is set to speaker view. * 2. Valid if device report has RS version 8 and Sight camera is connected. * * Indicate to use Sight camera in Speaker view * * Note: * 1. Proto uses default values. When device report comes in and this attribute does not exists, * int value DISABLED will be used. * 2. Incoming request may also contain DISABLED value. Device can ignore if Sight is not connected. * 3. Sync app UI will search for Sight peripheral device in report to show toggle or not * Possible values -1 = DISABLED - For option to signify off, greyed out, unable to select 0 = OFF - is Off and able to toggle 1 = ON - is On and able to toggle */ int32 useSight = 10; /** * (OPTIONAL) * Valid if device report has RS version 8 and Sight camera is connected. * * Intelligently switch between cameras to show the best view. * Note: 03/29/23 Current design shows UI is grayed out, so value should be false by default for now. * * Note: * 1. Proto uses default values. When device report comes in and this attribute does not exists, * int value DISABLED will be used. * 2. Incoming request may also contain DISABLED value. Device can ignore if Sight is not connected. * 3. Sync app UI will search for Sight peripheral device in report to show toggle or not * Possible values -1 = DISABLED - For option to signify off, greyed out, unable to select 0 = OFF - is Off and able to toggle 1 = ON - is On and able to toggle */ int32 smartSwitching = 11; } fwupd-2.0.10/plugins/logitech-bulkcontroller/proto/usb_msg.proto000066400000000000000000000135211501337203100251210ustar00rootroot00000000000000/* * Copyright 1999-2023 Logitech, Inc. * All Rights Reserved * * SPDX-License-Identifier: LGPL-2.1-or-later */ syntax = "proto3"; package logi.device.proto; option java_package = "com.logitech.vc.proto"; import "device_info.proto"; import "firmware_update.proto"; import "crash_info.proto"; import "device_mode.proto"; import "device_attestation.proto"; import "rightsight.proto"; import "ota_manifest.proto"; import "device_time.proto"; import "ble_cfg.proto"; import "antiflicker.proto"; import "device_request.proto"; /** * * Header message to be included in UsbMsg. This contains * message metadata that aids in processing of messages */ message Header { /** * A unique id of the message. If responding after receiving * data, the value stored in this field should be used in the ack message msgId field */ string id = 1; /** * A timestamp indicating when the message was * sent. This is the number of milliseconds that have * elapsed since the epoch, in string format */ string timestamp = 2; } /** * The Ack message. * This is to be included in UsbMsg */ message Acknowledge { /** * The message Id. This should be the same value * in UsbMsg.Header.id field */ string msgId = 1; /** * The message processing result. true indicates message was * successfully processed, false otherwise. */ bool success = 2; } /** * The Kong Event message. * Anything that is not part of * Request/Response messaging, but is being sent to mqtt distributor * should be considered as a KongEvent, and forwarded to device host. * This is to be included in UsbMsg */ message KongEvent { /** * mqtt_event contains actual mqtt message */ string mqtt_event = 1; } /** * Sent by Kong sync-agent. * If Kong sync-agent starts-up and it is in Device mode, then * it can send this event. When PC sync-agent receives this event, * it should send a TransitionToDeviceModeRequest. * This is to be included in UsbMsg */ message HandshakeEvent { /** * Unused. Reserved for future use. */ bool reserved = 1; } /** * The enclosing message. * This is the root message of all messagesszx */ message UsbMsg { /** * Header for the message containing additional * message metadata. */ Header header = 1; /** * The actual message being sent. One of these must be * included */ oneof message { /** * Ack message */ Acknowledge ack = 2; /** * Request message */ Request request = 3; /** * Response message */ Response response = 4; /** * Event */ Event event = 5; } } /** * The Request message. * This is to be included in UsbMsg */ message Request { oneof payload { GetDeviceInfoRequest get_device_info_request = 2; UpdateNowRequest update_now_request = 3; SendCrashDumpRequest crash_dump_request = 4; TransitionToDeviceModeRequest transition_to_devicemode_request = 5; GetCertificateChainRequest get_certificate_chain_request = 6; SetRightSightConfigurationRequest set_right_sight_configuration_request = 7; GetManifestBodyRequest get_manifest_body_request = 8; SendCrashDumpRequestv2 crash_dump_request_v2 = 9; SetDeviceTimeRequest set_device_time_request = 10; SetAntiFlickerConfigurationRequest set_anti_flicker_configuration_request = 11; SetBLECfgRequest set_ble_cfg_request = 12; SetDeprovisionRequest set_deprovision_request = 13; RebootDeviceRequest reboot_device_request = 14; SetSpeakerBoostRequest speaker_boost_request = 15; SetNoiseReductionRequest noise_reduction_request = 16; SetReverbModeRequest reverb_mode_request = 17; GenerateCrashDumpRequest generate_bug_report_request = 18; SendCertificateDataRequest send_certificate_data_request = 19; SetMicEQModeRequest mic_eq_mode_request = 20; SetSpeakerEQModeRequest speaker_eq_mode_request = 21; ForgetDeviceRequest forget_request = 22; SetRightSightConfigurationRequestv2 set_rightsight_configuration_request_v2 = 23; SendTestResultRequest send_test_result_request = 24; GetMemfaultManifestRequest get_memfault_manifest_request = 25; SendMemfaultSettingsRequest send_memfault_settings_request = 26; } } /** * The Response message. * This is to be included in UsbMsg */ message Response { oneof payload { GetDeviceInfoResponse get_device_info_response = 2; UpdateNowResponse update_now_response = 3; SendCrashDumpResponse crash_dump_response = 4; TransitionToDeviceModeResponse transition_to_devicemode_response = 5; GetCertificateChainResponse get_certificate_chain_response = 6; SetRightSightConfigurationResponse set_right_sight_configuration_response = 7; GetManifestBodyResponse get_manifest_body_response = 8; SendCrashDumpResponsev2 crash_dump_response_v2 = 9; SetAntiFlickerConfigurationResponse set_anti_flicker_configuration_response = 11; SetBLECfgResponse set_ble_cfg_response = 12; SetDeprovisionResponse set_deprovision_response = 13; RebootDeviceResponse reboot_device_response = 14; SetSpeakerBoostResponse speaker_boost_response = 15; SetNoiseReductionResponse noise_reduction_response = 16; SetReverbModeResponse reverb_mode_response = 17; GenerateCrashDumpResponse generate_bug_report_response = 18; SendCertificateDataResponse send_certificate_data_response = 19; SetMicEQModeResponse mic_eq_response = 20; SetSpeakerEQModeResponse speaker_eq_response = 21; ForgetDeviceResponse forget_response = 22; SendTestResultResponse send_test_result_response = 24; GetMemfaultManifestResponse get_memfault_manifest_response = 25; SendMemfaultSettingsResponse send_memfault_settings_response = 26; } } /** * The Event message. * This is to be included in UsbMsg */ message Event { oneof payload { KongEvent kong_event = 1; SendCrashDumpEvent send_crash_dump_event = 2; CrashDumpAvailableEvent crash_dump_available_event = 3; HandshakeEvent handshake_event = 4; InitiateMemfaultManifestRequestEvent initiate_memfault_manifest_request_event = 5; } } fwupd-2.0.10/plugins/logitech-bulkcontroller/tests/000077500000000000000000000000001501337203100223725ustar00rootroot00000000000000fwupd-2.0.10/plugins/logitech-bulkcontroller/tests/logi-rally-bar-setup.json000066400000000000000000000274601501337203100272510ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUsbDevice", "PlatformId": "2-1.1", "Created": "2024-11-13T10:32:05.291707Z", "IdVendor": 1133, "IdProduct": 2203, "Device": 32, "USB": 800, "Manufacturer": 1, "DeviceClass": 239, "DeviceSubClass": 2, "DeviceProtocol": 1, "Product": 2, "SerialNumber": 3, "UsbBosDescriptors": [ { "DevCapabilityType": 22, "ExtraData": "AAI=" }, { "DevCapabilityType": 2, "ExtraData": "BgAAAA==" }, { "DevCapabilityType": 3, "ExtraData": "AA8AAQH0AQ==" } ], "UsbConfigDescriptors": [ { "Configuration": 4, "ConfigurationValue": 1 } ], "UsbInterfaces": [ { "Length": 9, "DescriptorType": 4, "InterfaceClass": 14, "InterfaceSubClass": 1, "Interface": 5, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 129, "Interval": 8, "MaxPacketSize": 16 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 1, "InterfaceClass": 14, "InterfaceSubClass": 2, "Interface": 6 }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 1, "AlternateSetting": 1, "InterfaceClass": 14, "InterfaceSubClass": 2, "Interface": 6, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 130, "Interval": 1, "MaxPacketSize": 1024 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 2, "InterfaceClass": 255, "InterfaceSubClass": 117, "InterfaceProtocol": 1, "Interface": 8, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 1, "MaxPacketSize": 1024 }, { "DescriptorType": 5, "EndpointAddress": 131, "MaxPacketSize": 1024 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 3, "InterfaceClass": 255, "InterfaceSubClass": 118, "InterfaceProtocol": 1, "Interface": 9, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 2, "MaxPacketSize": 1024 }, { "DescriptorType": 5, "EndpointAddress": 132, "MaxPacketSize": 1024 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 4, "InterfaceClass": 255, "InterfaceSubClass": 66, "InterfaceProtocol": 1, "Interface": 10, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 3, "MaxPacketSize": 1024 }, { "DescriptorType": 5, "EndpointAddress": 133, "MaxPacketSize": 1024 } ] } ], "UsbEvents": [ { "Id": "#d5a801ad", "Data": "usb" }, { "Id": "#ad8c58d3", "Data": "usb" }, { "Id": "#1075ed5c", "Data": "usb_device" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=147\nDEVNAME=bus/usb/002/020\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=46d/89b/20\nTYPE=239/2/1\nBUSNUM=002\nDEVNUM=020" }, { "Id": "#d432c663", "Data": "bus/usb/002/020" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c", "Data": "usb_device" }, { "Id": "#4693935e", "Data": "002" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=147\nDEVNAME=bus/usb/002/020\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=46d/89b/20\nTYPE=239/2/1\nBUSNUM=002\nDEVNUM=020" }, { "Id": "#1ab3ae0a", "Data": "020" }, { "Id": "#1fcf122d", "Data": "TG9naSBSYWxseSBCYXIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" }, { "Id": "#028c3a0e", "Data": "MjMxM0ZEMU4wOFoyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" }, { "Id": "#45324a3b", "Error": -7 }, { "Id": "#084996a0", "Data": "B8wAAAkAAAABAAAACgMSATAaAioA" }, { "Id": "#45324a3b", "Data": "Af8AAAUAAAABAAAANTIyMzE=" }, { "Id": "#0d787287", "Data": "CMwAAAAAAAACAAAA" }, { "Id": "#45324a3b", "Data": "Af8AAAUAAAACAAAANTIyMzI=" }, { "Id": "#45324a3b", "Error": -7 }, { "Id": "#190a7257", "Data": "B8wAAAkAAAADAAAACgMSATAaAioA" }, { "Id": "#45324a3b", "Data": "Af8AAAUAAAADAAAANTIyMzE=" }, { "Id": "#729bb67f", "Data": "CMwAAAAAAAAEAAAA" }, { "Id": "#45324a3b", "Data": "BswAAEgAAAAAAAAACkAKJGZhMTI1Yjg5LWExZGQtNDljZC1iNmNiLTlhMDMyN2FiNTIzMxIYMjAyNC0xMS0xM1QxMDozMjowNS4yNjNaIgQqAggB" }, { "Id": "#45324a3b", "Data": "Af8AAAUAAAAEAAAANTIyMzI=" }, { "Id": "#85286d96", "Data": "Af8AAAQAAAAFAAAABswAAA==" }, { "Id": "#45324a3b", "Data": "CMwAAAAAAAA=" }, { "Id": "#79e46e49", "Data": "Af8AAAQAAAAGAAAACMwAAA==" }, { "Id": "#28f95e67", "Data": "B8wAABgAAAAHAAAACgMSATAaEVIPEg1FdXJvcGUvTG9uZG9u" }, { "Id": "#45324a3b", "Data": "Af8AAAUAAAAHAAAANTIyMzE=" }, { "Id": "#7ca9ef6b", "Data": "CMwAAAAAAAAIAAAA" }, { "Id": "#45324a3b", "Data": "BswAAEYAAAAAAAAACkAKJDU2NDlmOTY5LTgwODMtNGQ2Ny1hNDQ0LWQ3Zjg4ZjFmODFkZBIYMjAyNC0xMS0xM1QxMDozMjowNS4yNzBaEgIQAQ==" }, { "Id": "#45324a3b", "Data": "Af8AAAUAAAAIAAAANTIyMzI=" }, { "Id": "#5356fcaf", "Data": "Af8AAAQAAAAJAAAABswAAA==" }, { "Id": "#45324a3b", "Data": "CMwAAAAAAAA=" }, { "Id": "#a09a9cff", "Data": "Af8AAAQAAAAKAAAACMwAAA==" }, { "Id": "#fb551259", "Data": "B8wAAAkAAAALAAAACgMSATAaAhIA" }, { "Id": "#45324a3b", "Data": "Af8AAAUAAAALAAAANTIyMzE=" }, { "Id": "#8c326a37", "Data": "CMwAAAAAAAAMAAAA" }, { "Id": "#45324a3b", "Data": "Af8AAAUAAAAMAAAANTIyMzI=" }, { "Id": "#45324a3b", "Data": "BswAAMkLAAAAAAAACkAKJGQyM2E3YTQ2LWI2ZWEtNDljMi04ZTEwLWRlOTUxOGI4Y2VlZhIYMjAyNC0xMS0xM1QxMDozMjowNS4yODFaIoQXEoEXCv4WeyJpZCI6IjBhZDA2ODVkLThmZmItNDg2Yi04NTMyLTdkYmM3NWQyNTk0YSIsInRzIjoiMTczMTQ5MzkyNTc1NyIsInR6T2Zmc2V0IjowLCJ0eXBlIjoyLCJwYXlsb2FkIjp7ImRldmljZXMiOlt7ImFodyI6IjEwIiwiYXVkaW9TZXR0aW5ncyI6eyJkZVJldmVyYk1vZGUiOjIsIm1pY0VRIjoxLCJub2lzZVJlZHVjdGlvbiI6MSwic3BlYWtlckJvb3N0IjowLCJzcGVha2VyRVEiOjEsInRzIjoxNzMxNDkzOTI1MjgxfSwiYXYiOiIxLjAuNzYzIiwiYm9vdFN0YXR1cyI6MCwiYnQiOnsib24iOjEsInRzIjoxNjY5MTU5NzE5MjY5fSwiYnVpbGRUeXBlIjoidXNlciIsImJ5b2RTZXR0aW5ncyI6eyJ0cyI6MTczMTQ5MzkyNTI4MSwid2FsbHBhcGVyIjowfSwiZGlzcGxheXMiOlt7Im5hbWUiOiJCdWlsdC1pbiBTY3JlZW4iLCJyZWZyZXNoUmF0ZSI6NjAsInJlc29sdXRpb24iOiIxOTIwIHggMTA4MCJ9XSwiZXRoZXJuZXQiOnsiaXAiOnsibW9kZSI6InN0YXRpYyJ9LCJpcHY2Ijp7Im1vZGUiOiJzdGF0aWMifSwicHJveHkiOnsibW9kZSI6Ik5PTkUiLCJwYWNVcmwiOiIiLCJzZXJ2ZXIiOiIifSwic3RhdHVzIjotMSwidHMiOjE2ODc1NDI4Mjc3MzJ9LCJoa2h3IjoiMTcwLjE3MC40ODA1OSIsImhrdiI6IjEuMS43OSIsImh3IjoiNC40IiwiaXAiOiIiLCJrZXlzVHlwZSI6InJlbGVhc2Uta2V5cyIsImxuYVNldHRpbmdzIjp7Im9uIjowLCJ0cyI6MTY4NzU0MjgyNzQ0MiwidXJsIjoiaHR0cHM6Ly8iLCJ1c2VybmFtZSI6ImFkbWluIn0sIm1ha2UiOiJMb2dpdGVjaCIsIm1hbmlmZXN0IjoiMTAuMTIuNTU2IiwibW9kZWwiOiJWUjAwMTkiLCJuYW1lIjoiUmFsbHkgQmFyIiwib3MiOiJMb2dpIENvbGxhYk9TIiwib3N2IjoiMC45MTcuNjQyIiwicGVyaXBoZXJhbHMiOnsiY2FtZXJhIjp7ImNvdW50Ijp7ImFjdHVhbCI6MSwiZXhwZWN0ZWQiOjF9LCJkYXRhIjpbeyJkZXZJZCI6IjEwMDQiLCJpc09ubGluZSI6dHJ1ZSwidHlwZSI6IlNlbnRpbmVsIiwidWlkIjoiMjMyNUxaNTFYWTU4In1dfSwibWljUG9kIjp7ImNvdW50Ijp7ImFjdHVhbCI6MSwiZXhwZWN0ZWQiOjF9LCJkYXRhIjpbeyJuYW1lIjoiTWljIFBvZCIsInN0YXR1cyI6MSwic3ciOiIyLjEuMyIsInVpZCI6IjMzIiwidXBkYXRlU3RhdHVzIjowLCJ2aWQiOiIweDA0NmQifV19LCJyZW1vdGUiOnsiY291bnQiOnsiYWN0dWFsIjoxLCJleHBlY3RlZCI6MX0sImRhdGEiOlt7ImJhdCI6MTAwLCJuYW1lIjoiUmVtb3RlIENvbnRyb2xsZXIiLCJwaWQiOiIweDA4RDUiLCJzdGF0dXMiOjAsInN3IjoiMi41IiwidWlkIjoiNDQ6NzM6RDY6REE6MTY6RjEiLCJ1cGRhdGVTdGF0dXMiOjAsInZpZCI6IjB4MDFEQSJ9XX19LCJwaWQiOiIweDg5OSIsInByb2MiOiJhcm02NC12OGEiLCJwcm92aWRlciI6eyJuYW1lIjoiQllPRCBNb2RlIiwidmVyc2lvbiI6IjEuMjkuMzcifSwicHRodyI6IjE2LjAuMCIsInB0diI6IjEuMC44MCIsInJhbSI6IjUuMCIsInJlYm9vdCI6eyJyZWFzb24iOiJyZWJvb3QiLCJ0cyI6MTY4NzU0MjgxNzE1OX0sInJlZ2lvbmFsU2V0dGluZ3MiOnsiY291bnRyeSI6IlVuaXRlZCBLaW5nZG9tIiwibGFuZyI6ImVuLUdCIiwidHMiOiIxNjk0NzA5NTA0MDU5IiwidHoiOiJFdXJvcGUvQmVsZmFzdCJ9LCJyaWdodFNpZ2h0Ijp7Imdyb3VwRnJhbWluZ1NwZWVkIjoyLCJtb2RlIjowLCJvbiI6MSwicGlwIjp0cnVlLCJzcGVha2VyRGV0ZWN0aW9uU3BlZWQiOjEsInNwZWFrZXJGcmFtaW5nU3BlZWQiOjEsInN0YXR1cyI6MSwidHJhY2tpbmdNb2RlIjowLCJ0cyI6MTY4NzU0MjgxNjA3NSwidmVyc2lvbiI6Nn0sInJvb21QZW9wbGVDb3VudCI6IjAiLCJzZXJpYWwiOiIyMzEzRkQxTjA4WjIiLCJzdGF0dXMiOjIsInN3IjoiMS45LjU1NiIsInR5cGUiOiJIb3N0ZWRLb25nIiwidWlkIjoiNDQ6NzM6RDY6Qzc6NzY6NkEiLCJ1cGRhdGVTdGF0dXMiOjAsInZpZCI6IjB4MDQ2ZCIsIndpZmkiOnsiaXAiOnsibW9kZSI6InN0YXRpYyJ9LCJpcHY2Ijp7Im1vZGUiOiJzdGF0aWMifSwicHJveHkiOnsibW9kZSI6Ik5PTkUiLCJwYWNVcmwiOiIiLCJzZXJ2ZXIiOiIifSwic3RhdHVzIjowLCJ0cyI6MTY4NzU0MjgyNzU3M30sInpmaHciOiIxNi4wLjAiLCJ6ZnYiOiIxLjAuNzkifSx7ImF1ZGlvU2V0dGluZ3MiOnsiZGVSZXZlcmJNb2RlIjoxLCJtaWNFUSI6MSwibm9pc2VSZWR1Y3Rpb24iOjEsInNwZWFrZXJCb29zdCI6MSwic3BlYWtlckVRIjoxLCJ0cyI6MTczMTQ5MzkyMjM3N30sImF2IjoiMC45MDEuNTAiLCJib290U3RhdHVzIjowLCJidCI6eyJvbiI6MCwidHMiOjE3MzE0OTM5MjIzNzd9LCJidWlsZFR5cGUiOiJ1c2VyIiwiZGlzcGxheXMiOltdLCJodyI6Im10ODE5NS40LjAiLCJpcCI6IiIsImtleXNUeXBlIjoicmVsZWFzZS1rZXlzIiwibWFrZSI6IkxvZ2l0ZWNoIiwibWFuaWZlc3QiOiIwLjAuMCIsIm1vZGVsIjoiVlUwMDY3IiwibmFtZSI6IlNpZ2h0Iiwib3MiOiJMb2dpIENvbGxhYk9TIiwib3N2IjoiMC45MDEuNTAiLCJwcm9jIjoiYXJtNjQtdjhhIiwicmFtIjoiMy4wIiwicmVib290Ijp7InJlYXNvbiI6ImNvbGQsY2hhcmdlciIsInRzIjoxNjg0ODc2NzUxNDUwfSwicmVnaW9uYWxTZXR0aW5ncyI6eyJ0cyI6IjE2ODQ4NzY3NTgzMTIifSwicmlnaHRTaWdodCI6eyJncm91cEZyYW1pbmdTcGVlZCI6MSwibW9kZSI6MCwib24iOjAsInBpcCI6dHJ1ZSwic3BlYWtlckRldGVjdGlvblNwZWVkIjoxLCJzcGVha2VyRnJhbWluZ1NwZWVkIjoxLCJzdGF0dXMiOjEsInRyYWNraW5nTW9kZSI6MCwidHMiOjE2ODQ4NzY3NTA2NDcsInZlcnNpb24iOjZ9LCJyb29tUGVvcGxlQ291bnQiOiIwIiwic2VyaWFsIjoiMjMyNUxaNTFYWTU4Iiwic3ciOiIwLjUuMCIsInR5cGUiOiJTZW50aW5lbCIsInVwZGF0ZVN0YXR1cyI6MCwidmlkIjoiMHgwNDZkIiwidnYiOiIwLjkwMS41MCIsInVpZCI6IjIzMjVMWjUxWFk1OCIsInN0YXR1cyI6MX1dfX0=" }, { "Id": "#5517b465", "Data": "Af8AAAQAAAANAAAABswAAA==" }, { "Id": "#45324a3b", "Data": "CMwAAAAAAAA=" }, { "Id": "#1d52135e", "Data": "Af8AAAQAAAAOAAAACMwAAA==" } ] } ] } fwupd-2.0.10/plugins/logitech-bulkcontroller/tests/logi-rally-bar.json000066400000000000000000000007501501337203100261040ustar00rootroot00000000000000{ "name": "Logi Rally Bar", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/logi-rally-bar-setup.json", "components": [ { "version": "1.9.556", "guids": [ "9608c52f-e60f-597d-a813-84c6f2177a89" ] }, { "name": "Sight", "version": "0.5.0", "guids": [ "79ca7ba2-bb52-5c6e-968c-94a006482562" ] } ] } ] } fwupd-2.0.10/plugins/logitech-bulkcontroller/tests/logi-sight-setup.json000066400000000000000000000217621501337203100265010ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUsbDevice", "PlatformId": "1-6", "Created": "2024-11-13T10:54:38.026097Z", "IdVendor": 1133, "IdProduct": 2170, "Device": 547, "USB": 528, "Manufacturer": 1, "DeviceClass": 239, "DeviceSubClass": 2, "DeviceProtocol": 1, "Product": 2, "SerialNumber": 3, "UsbBosDescriptors": [ { "DevCapabilityType": 42, "ExtraData": "AAM=" }, { "DevCapabilityType": 2, "ExtraData": "AAAAAA==" }, { "DevCapabilityType": 3, "ExtraData": "AA8AAQH0AQ==" }, { "DevCapabilityType": 10, "ExtraData": "AAEAAAAAEQAAMEAKALBACgA=" } ], "UsbConfigDescriptors": [ { "Configuration": 4, "ConfigurationValue": 1 } ], "UsbInterfaces": [ { "Length": 9, "DescriptorType": 4, "InterfaceClass": 14, "InterfaceSubClass": 1, "Interface": 5, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 129, "Interval": 8, "MaxPacketSize": 16 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 1, "InterfaceClass": 14, "InterfaceSubClass": 2, "Interface": 6 }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 1, "AlternateSetting": 1, "InterfaceClass": 14, "InterfaceSubClass": 2, "Interface": 6, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 130, "Interval": 1, "MaxPacketSize": 1024 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 1, "AlternateSetting": 2, "InterfaceClass": 14, "InterfaceSubClass": 2, "Interface": 6, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 130, "Interval": 1, "MaxPacketSize": 5120 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 2, "InterfaceClass": 255, "InterfaceSubClass": 117, "InterfaceProtocol": 1, "Interface": 8, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 1, "MaxPacketSize": 512 }, { "DescriptorType": 5, "EndpointAddress": 131, "MaxPacketSize": 512 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 3, "InterfaceClass": 255, "InterfaceSubClass": 118, "InterfaceProtocol": 1, "Interface": 9, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 2, "MaxPacketSize": 512 }, { "DescriptorType": 5, "EndpointAddress": 132, "MaxPacketSize": 512 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 4, "InterfaceClass": 3, "Interface": 10, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 133, "Interval": 4, "MaxPacketSize": 64 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 5, "InterfaceClass": 255, "InterfaceSubClass": 66, "InterfaceProtocol": 1, "Interface": 12, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 3, "MaxPacketSize": 512 }, { "DescriptorType": 5, "EndpointAddress": 134, "MaxPacketSize": 512 } ] } ], "UsbEvents": [ { "Id": "#4693935e", "Data": "001" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=101\nDEVNAME=bus/usb/001/102\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=46d/87a/223\nTYPE=239/2/1\nBUSNUM=001\nDEVNUM=102" }, { "Id": "#1ab3ae0a", "Data": "102" }, { "Id": "#1fcf122d", "Data": "TG9naSBTaWdodAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" }, { "Id": "#028c3a0e", "Data": "MjMyNUxaNTFYWTU4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" }, { "Id": "#45324a3b", "Error": -7 }, { "Id": "#084996a0", "Data": "B8wAAAkAAAABAAAACgMSATAaAioA" }, { "Id": "#45324a3b", "Data": "Af8AAAUAAAABAAAANTIyMzE=" }, { "Id": "#0d787287", "Data": "CMwAAAAAAAACAAAA" }, { "Id": "#45324a3b", "Data": "BswAAEgAAAAAAAAACkAKJDlmNmU1MjkzLTBiM2MtNDZiMC05NWQ0LTIyYzRhZTQ5ZmQ5OBIYMjAyNC0xMS0xM1QxMDo1NDozOC41MDhaIgQqAggB" }, { "Id": "#45324a3b", "Data": "Af8AAAUAAAACAAAANTIyMzI=" }, { "Id": "#d61f7af4", "Data": "Af8AAAQAAAADAAAABswAAA==" }, { "Id": "#45324a3b", "Data": "CMwAAAAAAAA=" }, { "Id": "#a280e80a", "Data": "Af8AAAQAAAAEAAAACMwAAA==" }, { "Id": "#5091e4b7", "Data": "B8wAABgAAAAFAAAACgMSATAaEVIPEg1FdXJvcGUvTG9uZG9u" }, { "Id": "#45324a3b", "Data": "Af8AAAUAAAAFAAAANTIyMzE=" }, { "Id": "#9ec27604", "Data": "CMwAAAAAAAAGAAAA" }, { "Id": "#45324a3b", "Data": "BswAAEYAAAAAAAAACkAKJDdmYzlkZjE1LWEwOWQtNDA1My1hZmJjLWEwNGQ5Y2I3YWY0ZhIYMjAyNC0xMS0xM1QxMDo1NDozOC41MTNaEgIQAQ==" }, { "Id": "#45324a3b", "Data": "Af8AAAUAAAAGAAAANTIyMzI=" }, { "Id": "#f9b0be46", "Data": "Af8AAAQAAAAHAAAABswAAA==" }, { "Id": "#45324a3b", "Data": "CMwAAAAAAAA=" }, { "Id": "#ea4acbcb", "Data": "Af8AAAQAAAAIAAAACMwAAA==" }, { "Id": "#0bdae261", "Data": "B8wAAAkAAAAJAAAACgMSATAaAhIA" }, { "Id": "#45324a3b", "Data": "Af8AAAUAAAAJAAAANTIyMzE=" }, { "Id": "#e7e001c1", "Data": "CMwAAAAAAAAKAAAA" }, { "Id": "#45324a3b", "Data": "Af8AAAUAAAAKAAAANTIyMzI=" }, { "Id": "#45324a3b", "Data": "BswAAP0DAAAAAAAACkAKJDZjNTc1ZGE2LTNmNzgtNDQ5ZS04NzEzLWQzMjQzMmEyYzU3ZBIYMjAyNC0xMS0xM1QxMDo1NDozOC41MThaIrgHErUHCrIHeyJpZCI6Ijc4OTg5OTQxLTU0MTItNDlmMy05MGVmLTE5MDJjMDAxMjc3MSIsInRzIjoiMTczMTQ5NTI3ODUyMSIsInR6T2Zmc2V0IjowLCJ0eXBlIjoyLCJwYXlsb2FkIjp7ImRldmljZXMiOlt7ImF1ZGlvU2V0dGluZ3MiOnsiZGVSZXZlcmJNb2RlIjoxLCJtaWNFUSI6MSwibm9pc2VSZWR1Y3Rpb24iOjEsInNwZWFrZXJCb29zdCI6MSwic3BlYWtlckVRIjoxLCJ0cyI6MTczMTQ5NTI3ODUxOH0sImF2IjoiMC45MDEuNTAiLCJib290U3RhdHVzIjowLCJidCI6eyJvbiI6MCwidHMiOjE3MzE0OTUyNzg1MTh9LCJidWlsZFR5cGUiOiJ1c2VyIiwiZGlzcGxheXMiOltdLCJodyI6Im10ODE5NS40LjAiLCJpcCI6IiIsImtleXNUeXBlIjoicmVsZWFzZS1rZXlzIiwibWFrZSI6IkxvZ2l0ZWNoIiwibWFuaWZlc3QiOiIwLjAuMCIsIm1vZGVsIjoiVlUwMDY3IiwibmFtZSI6IlNpZ2h0Iiwib3MiOiJMb2dpIENvbGxhYk9TIiwib3N2IjoiMC45MDEuNTAiLCJwcm9jIjoiYXJtNjQtdjhhIiwicmFtIjoiMy4wIiwicmVib290Ijp7InJlYXNvbiI6ImNvbGQsY2hhcmdlciIsInRzIjoxNjg0ODc2NzUxNDUwfSwicmVnaW9uYWxTZXR0aW5ncyI6eyJ0cyI6IjE2ODQ4NzY3NTgzMTIifSwicmlnaHRTaWdodCI6eyJncm91cEZyYW1pbmdTcGVlZCI6MSwibW9kZSI6MCwib24iOjAsInBpcCI6dHJ1ZSwic3BlYWtlckRldGVjdGlvblNwZWVkIjoxLCJzcGVha2VyRnJhbWluZ1NwZWVkIjoxLCJzdGF0dXMiOjEsInRyYWNraW5nTW9kZSI6MCwidHMiOjE2ODQ4NzY3NTA2NDcsInZlcnNpb24iOjZ9LCJyb29tUGVvcGxlQ291bnQiOiIwIiwic2VyaWFsIjoiMjMyNUxaNTFYWTU4Iiwic3RhdHVzIjowLCJzdyI6IjAuNS4wIiwidHlwZSI6IlNlbnRpbmVsIiwidWlkIjoiMDI6MDA6MDA6MDA6MDA6MDAiLCJ1cGRhdGVTdGF0dXMiOjAsInZpZCI6IjB4MDQ2ZCIsInZ2IjoiMC45MDEuNTAifV19fQ==" }, { "Id": "#fd4a2ae1", "Data": "Af8AAAQAAAALAAAABswAAA==" }, { "Id": "#45324a3b", "Data": "CMwAAAAAAAA=" }, { "Id": "#6dadfaf2", "Data": "Af8AAAQAAAAMAAAACMwAAA==" }, { "Id": "#0f6d472e", "Error": -1 } ] } ] } fwupd-2.0.10/plugins/logitech-bulkcontroller/tests/logi-sight.json000066400000000000000000000005111501337203100253300ustar00rootroot00000000000000{ "name": "Logi Sight (standalone)", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/logi-sight-setup.json", "components": [ { "version": "0.5.0", "guids": [ "8064915d-8c91-5f1e-a7a9-cd95f6538bfe" ] } ] } ] } fwupd-2.0.10/plugins/logitech-hidpp/000077500000000000000000000000001501337203100172735ustar00rootroot00000000000000fwupd-2.0.10/plugins/logitech-hidpp/README.md000066400000000000000000000132361501337203100205570ustar00rootroot00000000000000--- title: Plugin: Logitech HID++ --- ## Introduction This plugin can flash the firmware on: * Logitech Unifying USB receivers, both the Nordic (U0007) device and the Texas Instruments (U0008) versions * Logitech Bolt USB receivers * Unifying peripherals through the Unifying receiver * Peripherals through the Bolt receiver and directly through BLE This plugin will not work with the different "Nano" USB receiver (U0010) as it does not use the Unifying protocol. Some bootloader protocol information was taken from the [Mousejack](https://www.mousejack.com/) project, specifically logitech-usb-restore.py and unifying.py. Other documentation was supplied by Logitech. Additional constants were taken from the [https://pwr-Solaar.github.io/Solaar/](Solaar) project. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a vendor-specific format that appears to be a subset of the Intel HEX format. This plugin supports the following protocol IDs: * `com.logitech.unifying` * `com.logitech.unifyingsigned` ## GUID Generation The Unifying receivers and peripherals use the standard USB DeviceInstanceId values when in DFU mode: * `USB\VID_046D&PID_AAAA` When in runtime mode, the HID raw DeviceInstanceId values are used: * `HIDRAW\VEN_046D&MOD_B33B405B0000` * `HIDRAW\VEN_046D&MOD_B33B405B0000&ENT_05` * `HIDRAW\VEN_046D&DEV_C52B` * `HIDRAW\VEN_046D&DEV_C52B&ENT_05` The Bolt USB receiver and peripherals use HID raw DeviceInstanceId values regardless of their mode. This might change once these devices are handled by the Logitech Linux driver instead of by the generic hid driver. ## Vendor ID Security The vendor ID is set from the vendor ID, in this instance set to `USB:0x046D` in bootloader and `HIDRAW:0x046D` in runtime mode. ## Update Behavior Due to the variety of devices supported and the differences in how they're enumerated, the update behavior is slightly different between them. In all cases, the devices have to be put in bootloader mode to run the DFU process. While in bootloader mode, the user won't be able to use the device. For receivers, that also means that while they're in bootloader mode, the peripherals paired to them won't work during the update. A Unifying receiver presents in runtime mode, but on detach re-enumerates with a different USB PID in a bootloader mode. On attach the device again re-enumerates back to the runtime mode. For this reason the `REPLUG_MATCH_GUID` internal device flag is used so that the bootloader and runtime modes are treated as the same device. The Bolt receiver enumerates as a hidraw device both in runtime and bootloader mode, but with different HIDRAW devIDs. Peripherals paired to a receiver are enumerated as separate hidraw devices but those device files can't be used for DFU. Instead, all the DFU-related messages need to be piped through the hidraw device file of the receiver. They are polled and queried by the receiver and listed as its children. Note that this will likely change once the Logitech Linux driver supports Bolt devices. Bolt peripherals directly connected to the host through BLE are enumerated as individual hidraw devices and can be upgraded through their hidraw device files. ## Design Notes When a USB receiver is detected in bootloader mode we detach the hidraw driver from the kernel and use raw control transfers. This ensures that we don't accidentally corrupt the uploading firmware. For application firmware we use hidraw which means the hardware keeps working while probing, and also allows us to detect paired devices. ### How the code is organized Here's how the different devices are handled in the plugin: * Unifying receiver in runtime mode: FuLogitechHidppRuntimeUnifying (fu-logitech-hidpp-runtime-unifying.c) * Unifying receiver in bootloader mode: * Nordic chipset: FuLogitechHidppBootloaderNordic (fu-logitech-hidpp-bootloader-nordic.c) * TI chipset: FuLogitechHidppBootloaderTexas (fu-logitech-hidpp-bootloader-texas.c) * Bolt receiver in runtime mode: FuLogitechHidppRuntimeBolt (fu-logitech-hidpp-runtime-bolt.c) * Bolt receiver in bootloader mode and all peripherals: FuLogitechHidppDevice (fu-logitech-hidpp-device.c) FuLogitechHidppDevice effectively handles all devices that use the HID++2.0 protocol. Every device contains two updatable entities, the main application FW and the radio stack FW (SoftDevice). The latter will show up as a child device of the actual device and is handled by FuLogitechHidppRadio (fu-logitech-hidpp-radio.c), which simply defers to the parent device for most operations. ### Quirk Use Even though the same code handles multiple different devices, there are some inherent differences in them that makes it necessary to handle some exceptional behaviors sometimes. This plugin uses the following plugin-specific quirks: ### `Flags=rebind-attach` Some devices will have their device file unbound and re-bound after reset, so the device object can't be simply re-probed using the same file descriptor. Since: 1.7.0 ### `Flags=force-receiver-id` Used to differentiate the receiver device in FuLogitechHidppDevice, since the receiver has a specific HID++ ID. Since: 1.7.0 ### `Flags=ble` Differentiate devices in BLE mode. They require all the reports to be _long_. Since: 1.7.0 ### `Flags=is-signed` Device requires signed firmware. Since: 1.7.0 ### `Flags=no-request-required` No user-action is required for detach and attach. Since: 1.7.0 ### `Flags=add-radio` The device should add a softdevice (index 0x5), typically a radio. Since: 1.7.0 ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.3.4`. fwupd-2.0.10/plugins/logitech-hidpp/data/000077500000000000000000000000001501337203100202045ustar00rootroot00000000000000fwupd-2.0.10/plugins/logitech-hidpp/data/dump.csv.gz000066400000000000000000003104571501337203100223170ustar00rootroot00000000000000‹ɺIXdump.csv¬]Ýn]·Ž¾ïS, 7-°»G¢Dý§ðïA1™¶8I¯‚^É>i€Æ)l§=s7O36O2$%j/ÛÛ¶ä¨HS×ɦI~Ô§\ki}½¼þtsñûòóo×»åôâæb9Ù]Þ쮾¹ùøíògØ÷Õ×Ë7o¿]ÀüŒÅõ'6Ë—o·ô7þúë¯í ÿþööí§_}Mß~ýÛçåt÷v1i±æ…‹/¬!C6ȾÜý¹û}óêÍ—ïvÿÞ||q½ýx½ý|½9ý|µy¹»Üœ]]mNwnÎþØüs÷öÓջͫÏ?^\ý÷Wf³1ó˜­)ÿn䟓‹?n>_í–뛋«›Ý»å›£÷ï¯vï/nvßnÞ°/ä»Ã.ˆCøÂøåÿõúW¶hWÁ‹ûåòjw½»ùûòËß^_\½ßÝ,o?]^îÞ’ý¿oøc°ÿÙÝØmÎy ÉmMr˵XyCÉ3Ë«ŸÎݼ9¿ºø¸»~± Ëw ÷ëòæçÝÕ‡Oï>¼]^ø¸ûôùFÜqk»^ì&²ë·)¢Ø¥fÜæeÛ?üøÝGÿIæšòjÊÞ1•]l¦ Ëª)O¦PL˜²”I5e)b|ÊR`K ñùÔŸ7 žófñ¡¼ÅµÝ[yË£yKjÊ’)|‚¾¼e5å›)h3¦±¼Y©|/ÆëÄyûXâ¨æ¿[|ˆ%ÎÚµá[á¢ÌœµeïØÊäÆXê¬S[¾Ù*5g æNVB·ŒçÜ™@¥‹ÐLmØ¥€wrçC¢ÜYtðë"vpmÕN€­õ\q9ÖlýðãróïËeYÞЧ—ŸzùòW‚p9?_’]ÎíbìBL`P¨D dÌÉê"·,™åãʦٜ|º¼¹úôûòúêâòú_»«»)¥Æ‡85¶6Û Õˆ!Ó(%X'RŠÑ•PC Õy5É„ø”ýxÛ§uœöP”ÀNA‹yo±F ~ë¨ÆF¢„}”Õ`4ä"ôE%JoJ”¨‰ŠV£´îžOOFé–£#ú%ÓÞâËÑ(Ý>ÊhՠA¢ô%ÊØ…d"T,yŠÒsö­‹qoL¬X3¥ßG™u ÄD.º ÒÒ2y›cna"Ü%Î\k6[]ž´z| åiJ#Ø[1†¥)cÈA?u][²®Ãj{#Úâ]èÖ[ýdÉ,Òií6ÑÒþ¸bN·?U×–âFâ&*9›âB(íS÷êìõ/?3 TeÒ-Rü¥þ£¦-©¥\,‘Ç””Ï·@øé—×b§æž~7‹Ï‹w š²³ÀÞJ¢UU$Ð?ï  w®ïh k$~‚Ìí³¡[<‘j(D@gèGbùd UiqôÿHQ„“MÛ˜ÍÈŒõ“žó3eªD%ŽH•J,nCŠ­ªÝÝ=ÇbÈ\Õ_ÝtL®Vé‘ÊQv2bKÉþõr„ó‡·$éw×o¯>üqóéªÈìï éîË÷7¿}|Ù¨A"¹–h'Ú>ë^¸vê;S©Ä‚Ûâ=Y_> T¤àb÷”øAà2î³èîf18áÀ4‹Páï¶Hɲb$#fŸÄWäÁÑ»wäÕõ¦þ÷{c‹ QG<å,Çæˆ®öÃŽˆpµAIÊ:u„¸i°ZA°†ÓvÃiS8YÒš¢£ìû]"Üs,Ë^M7Fmd²/ŽùBÁÎßvŒ˜ý_Þ¾º¸ùðéòÿEâØP×ë7$ÂñâàÃ{ !WŒÕÁÔŠƒNt”s²qÇ¿W7W.ßw8–¬å¨:uLda´š¹TdC6‚:V6}gú<ƒ»že-·ìÅ3(;?ýðVm÷<³%eÜ&Zl™S¥êINg»¯úƒõòÍ«×G/_~{¯ÜL¬º–s”^5#Tr7#äY(ËòÁv„àieÛ1ímCë™°ŠZ6?l›ÓAÛ-PWb¤§´å”E¬äGÔ{Ï6…E¶m¬<²'Xî.i'R*TÀ]ÎóW1H#@F3wì)÷h=±8–ªc`Õ–]¥îÉF~Þ"ÎbYZ±Œ\éu¥øµD»ïìe6U¡P±Ä»²RlŽÏå'[H?À¶­ÿÊz©¶¯›|ñ¯’3HC 6¸½¬—¤knµ^^Ýuosëÿ¾— BóˆKÎöy‹G¹zšGžW±xäFV°X‰Z6ˆ'ôks)ù¬%[4‘§`¶˜„~iûzXõ›°‹ø$K}ê‘t«T¸”Vò€––EéƒüSÿZ²û¯3ýÿó¿âHÒ"+kÏz#w{]Åÿ^@²Tœ«ä)¨WRëx¡dã\Ž !ÙukäRmŽ’)ýËBá•/B”D£SeÑ/sºÐ _àl¡íà”¾y²œSØÇKö V •t/Ü÷cÞ:]À£,ܽ/Ò»wä‹~UGœ;;BýÑb 9JjkÕºBâ(# Xé1ÜMì`\sÈÎÆ&u$H0`¥=Œq Ük,—ÒwÁÖpl^§„¡/ÓÚ?\(´"J¬#*õý%uâ@áII|é'@‰§V¾…'q­e…§ÒðSðÀ Ÿ&u„Kꤞ,ÕæmÝôæUJ¨ñ†Çσ'(<´g8p=ðxëKx|©¶ÆÕ* JJü°rkM€G¥0]û.rÃBÖµÜTðl‘Åí<óú¯ÒÀN,vÂKµ5®ViPR’Æá™×÷ JÇtM¾xêpþx*YçZn¨Ò€´ì–šï öõ=v^߃* H‚R0¹ž`¤Ú‚r5ª4” Öv^߃* Ó5v uB!ëµÜP¥edËMPUnØ·zæõ=¨Ò€‘`úàréA¹U””à°r³óúTià˜®±k¨ YÔrSià"ýt¨ã:á™×÷ JêOéÇwu–jS®F•%%ãC;¯ïA•žé»†:¡uˆZn* ¼!ia¬ï±óúTi@ÝÕɾï)—‚'–jk\­Ò@R`|õÌë{P¥gº]CXÈ:ê]¨ÒÀóÅÀ؆:Ðϼ¾Uxg$˜®ÕT[l\­Ò ¤d?Ô¹+u„g^ßTx¦ëÐ5Ô‰…¬#Ôr * <’4HÚ÷Ükã€g^ßTxäÄvu¢+Õ¦\THJ¢'·y}OPià™®c×P'²ŽXË-¨4 ŽàqcÂÌ Ï¼¾'ª4À$˜>xB©6åê¨Ò ¤$“Û¼¾'ª4L×)uÁSÉ:¡>|€j#sï£CŽûZ8˜y}OTi,'6w“¤Ú²ruTi )Éã³a^ßU¦ë¾gCr!ëlµÜTǽ*·y}OTi@®$¶gïÉò8¥ÍÊÕQ¥AI ÷=0¯ï‰* Óuî"·\Èš”YG¥A–àÉc#Q˜×÷D•$x(˜Î¡Nv¥ÚW«4()êÀ¼¾'ª4L×¹k¨“ Yg}ª ª4‰šQ«}í[=õư)ð¨4ÙðH¦Üry6!7®ViÀ)ñ†÷žїÓTD~ìÃt ur%k½y6©4ˆV®)Ž­7¯ïI* ¢ Lßê‘;pÁ(W'•%%aøj©›×÷$•´­Š/žêÊAx((ñÔrK* hSÝòCܼ¾'©4ˆž»Vœ&Àá(W'•’;>Ôqóúž¤Ò€6!ñåéËqH’x¼–›JÚ„¶”Ü\ßÄÚÍë{’Jƒ8±}CŠ£ÜkPƒQaP2~ŸŽ›×õ$‘ÉÚvt ±ÕXTPÃóêA`æu˼.'«HLÎÐ3‘Áb-±¬" y¾ïË]:pó:œ¬" yNj_‡¹Â²ÓÜó:~øn67¯»É*“2ôt7™Õf®%–uóO67x/Ž›×ÙdÝüS ¤–œÊ³Q5ÝùK.ÆoÄqóÚš¬;bN†®™My¸´ÊtßO™ö}oÇñf2ºïóC–:oÃrÿt%å¬û¾¤ÃÏkZ< Ñ}?3/»®y/wOk¡éÎϾñ½ pëùɧ ™×ÐdÝù‰JZ»n`+WA•˜uï/éªÊÏkg²îý™¹Ùu^£.×@k4ºûgO›‹œÔøyÍLÖÝ?ó©®sR‹ÀTrÖý¿¤c|Nãçµ2Ö¨ÈLЮoPSZ«Gh¨ÈÑñ5w½FûÀ™×ÊX£" GʬïÔ€‘ÆÚ‰ ª$#þ¤6¯™±F…õå}Ï „£ô`£Z€Ÿô÷Æ.áøy 5E ”£€8˜®V¤Ú”¤ÉH^g$ŒÃ3¯©¡­Á׈˜©}è™O¶r+‚€M ƒŸ×ØXƒ pfc_ÉRmJÔd$¯2‚ãs?¯¹±&(<ÌÖØs÷ WClå$QÜØ6?¯Á±&*<È™íÔ$®6·çê˜×q×pü¼&Çš¤ð0]c쬮 Y;ÓÊ-)<‘ÖLˆcý§Ÿ×èX“žd$˜xœájs{®Îy‘ñ‰Ÿ×ëØrŽ9c™®±gdㄬÓr+Љ >ÌD»è“hæÁcmRO8³}cç¤ÚW[›W vxÎÙ"š(Í‹û«Ò,g¶¯ïñrêßsµJƒ’‘ñ'ªpbß* €é:öLt¼µ×S,¨4ž±§<Ö–âľT€ LOßãåÔ߸T”ŒŒupbß* åŒÉx Yë©?dBáÁ,ƒŽÁ3±ï•(³©o¨ãåÔl\ * $#iß÷Þ{´- û§ÒÀ[ßyÖx²zMÞ:•Å„gÎÜ¿ñsŽFS­<æ— ð» Îø Ùc¬¯6  £‘SÏË _T)8pìꓞ VJ9é—o 2rDzGÙ—›¶Ú…'§² ˜嵚8œWw+¯Öí£Iå9VXl\ÎNFó²ºbWyíQOz2˜W9]<)ç¨wäU.€…6Tå'š« ¤`FN|¬ÁóQH% ¤-åÀß<;¯ß?óË9 ‘ßGïãÁ`ä@wñ$UYðÈ{êk\Žë$>Ú™v¸ÿÁÊð·+ãY!¨ o¹<êçÖŃbP<Ú;:ð€U0ž<¦¥K^"ßmû—£ðß,!ËqÕ÷ñà‡ ª'®¾Í_þò ºRùöÒUà±ç…pP<\!ã<ä1˜ý»5<¸µ‰¨¼swHÿ0ï¸c>Fôè„}'7NùØJÚŸMRWx©ÆÊ;õ÷ŽÒxÈ“áŸïœ«ž¤î¼ŠŒÑ»p-ßÚQM°Œ)ÓõÐõ€D †²fÚâDއÔ"(¤ºiËMÔÀ;™?~(ÔÛƒì,óNh+Ã: ©Öy„nÞyFTFP<ä 0=7¬yOˆúž`m"Õ:´Ç¼]çèØÍ)Ñ(xZråÙ]×Åî{òE —ç]Õ“ØÞ~òD^£“¼ê+q|²kùy=–—¦ŸúDBå òš´`}z^^o{òyÕ–Ù£íÎ+¿<"$ÓÞècV&ÊS@|&ƒ{¤«»×àj4.³x~^Qf>ñf•׎aíAOžŸWÔ†™8M7«'Ÿ'°’WUShóÊDËë@½†U•¸ó/É«6Ë|‚©yí—Bw<ù‚¼:e$ÌÝy]—t·@m–}0Lç!\%â>r”rtºœ h$(oË[hÇá÷4zù=ñïÇGœîóóƒÁ 2R(Lÿø~§œÒç`•kë~ÇÓ½Îýîy!ª T<‚×Íê)<äÓ ›XĆÕ9Ø)W &•`·ŠÇÇ2R KÊK¢?â?=“ÃxXÙÁrùëããƒxĆG’âq êÁžsœJ0Y‚áŸ|uB5ëH<ÿ…Šå£¯I¢$8ˆGV<¢S<é{r[VI°0Þ‡>/„Cx´–8z%ñ'î~)‡¶3-憯Ò÷ãq$Áк>/ÙWÇÏÎãíÊ ËY–Ùð¸åÁ6<’óèúˆíuŽÁÂ: ®â‘ú×ÇóB8€Gh-1¿Ðö\4‰Ô¢Q<’YáÑ3ö-ÁK0g¡VWÙm¿Ï€ÿ¨ÔT‹ùNñH® ÅC×Gp°N‚ç«ç…p¶Ÿ§BxÈ%àØ.+„¶ŸóAoÞ‹ÝÍøI æNMµ5^FÇËñYÝyuz¶ŸóýIÚ~.Iãx$­…GË>çA@ÿû¨Îo<ÖBPðHÏÉ¥3 ¹\ˆMnjf^&àAù1st‚‚1´l—Å¢¤¹’ÚO~ãžü–%zÝ‚º¹Mxä<\ËÔø\!ÍÖùWCÐðpÀ£Ô®n#MŽó£ܘäòá+€Çèúʨgx-(ï×›—";Cî½°çu}ïnü¹üŽºÞMÏ÷‰·Ê‹§x´ OÁ!ÏVÄVCÐðh|¾ÏUÂH~P=:y‹h¤£ÛÖ}?b#ÁXæ&ëôLÏŠ'–¢á?onM[Ñkx$à‘ª"w²LÞø\!Ï®¯VCÐðHÀ#sxðõ?É#?ð üh Ã#ýÜŒ2ÌuM²GEa´¬¶ý LÁ£ñy°¶áq’ùQìqÊ<k!hx4>ß7rÃxp~äøœToà1rì[‚ñ’ì‡ç‘kl/V÷%Ê;òtül?¨ëÝ >·©Ê’gx ó 7>çA°[ÅÃŽ®¯VCPðÈàsj=²GJ|˜+á€@Ÿ“êíf¼`%˜ d+¾y—{Ë\ÿáðö¨ž\³£?-—NŠøœšRÌE}7¡ó ƒÏi€Çðþ|5 ð¹sÊ.ñàüˆÑ4w2uÔYïOÐû3øœô~g'÷禊ÙÕÙöÁ/ÔžïžgœõÀñw4<Àç¾Éu§x ?Àç4Öίw×BÐðŸ“X?tsb½?AïÏàsÙÝÌ  &#˜C{¾œè‘߬IJ%¯â>÷©*cgx$äøœxLðÇZàs/Sç|‡®å0|î)?fëí¦Šò-œžô*íÿ^¥$_ÿJj¢9×R‚GŸ;‚GË>çA@}× êƒ«!(xðyp÷.öƒ‰ó£YÕ›>§¯¨xŒ/•`D<ØS>qÏàþÜû#Fî(”¨öò·ï*ïˆÔ£á>güª¾›[†ð9‚Ÿß®… á>‰CÈìˆh€G&NoûÁú•iâÐÝ›¬"gôþs”jÄ‹–‰U\»Ÿà>¦*•gûAœ)às?Ïçk!hx€Ï£…ØêÚ`êùÁ§DRÙM>|…kõÝQýÃTñ@^á½ý=z?þ=_ÿÞ·ª}¾Å³!ý{§[¡_Ð🳒UoÇ9—>çA³.«!hx€Ï#okGòƒ¹¤v_ª)àó˜xŒ×¯ªxðøw+u+ÛþÉÛÕG?þí‹ãÑ🧦Tžâ Ÿó L÷ ¯† á>O†CÁƒúÓò†üŸ'ÊVO±3”`^‡Í=÷¦ZëC¿íÇçîÅ|¨ÍWä±Þž¤)•'xäfVD–æÇAˆó|¾Â_<öG±QÙ™­Šr³¿°ø<¥ãë]òƒWR^ঠúü°†øœ•üKçf„!<,ç‡E~€Ï íp[ÿîh?ƒ­âµ?óã>"?®çW,ðç€-ô§üæ-jxðyiJå)-? ø¼ðý…óx¬… àaŸGnF©·g¾51Z8ÊÅÍP•}š?| æÅÁþ“ŠÕ{Hüó‹ƒyÜZHßgLãó¸5¥ò ×òÃ4>çAXÁc- _,N‚Òöºçûþí{™ ÿ¬î­ À£Œàüö0nºÞ¾‚†GãóhdêÁƒóÃ!?ŸGò@ñ3¶ÐŒˆïÀ»Y®ó ¢ðU'Cu}eŸGÓ”Ê3<<ò£ñ9‚³óx¬… á‘€G€Øz±Þõœ­ÀÂ$Ž¿xŒîÏmHѼÕ,‰šw.3p^+烈[S€Gæyçdx±‡œ›_ï®… áQ€GáòÃÆ##š†ù øVo=´e«xð+©÷Š^:æÆ—ë!)xØÆç‘tpÁãw=17G„ý¿³ÇApóëݵ<,øÜv±õ¼ž˜Ù‡ ·~ 8þ «lŒ¸%˜ÒÄœžÔ¶'5Q"o¦ÞÛG¹TÃ|Ný î¢Þž[?ÃþßÙã ´zû°CÅjàs’òÝH½=s?Cný ¾o‘úüŒ£#âÔ¨ÏöÜO¾{ÿ¯=Û¸8úh’ÎK;e-øœúÜE}7·~†ý¿³ÇAð³õ«Õ4<Àç$åÝŽ’¹Ÿ!Gäøœú:ƒõD[Ń¿~/´ÏÚÿñrlÂý+²Øg½ZÃ|Ný ŒGÙ7¯¿ðˆÈð9B˜Ÿ¯ÖBÐ📔ïFê»™ûrD~€Ï©ŸÁÃp°ßÇVñ q ¨ÛgÞ7Åó°ô}hþå7u’íŸ<øö?ßÚùkÁçÔÏà.êí9"?Àç<q~}µ‚†øÜ»a<8?ò|Ný ~ƵL‚ñàÅ'ý+‡A6¿ÃS}Svú©*•gx´~†ý¿³ÇAˆóù±‚‚‡Ÿûܨì îgÈ­ŸÁ»-R?ïv¸èýþ~ƒnüZý󥿇ŸS?ƒ»ð±É­Ÿaÿïìq¦ûÛWCÐðŸ™:GðàühÝfm1P~äéú•ˆ$8?OSþÅo&CuÿáÀç¡)•gù‘‘às„é~êÕ4<ÀçÜŒ0¤°Ãoný ûWäÃWÑ~8×Oe†¥òõ“ÿêÏû¥á>§~wUßÍÈpð9Bs69½±ù¸Þ] AÃ|ÎÍCõ]îgÈ­`ÿІ¹"øVoí÷qU<Øó7²£Ûàû¥á>'ü‚ Ÿó L÷·¯† á>"¶^ŸWËÅ1Èð9¹"„Vo=ÿáªx‚>ÿ¾äó­½h¶½hñðàsÒÁÝU½½Ý3·ÿwö0~›=¯¶‚‚,ÙbmFȾf® ÀƒÏÉ!ÌꮊÇD>¾DrÈåøÉ>µ¾ëÁç©)•'x”­å‡ŸÓ x3Ÿk!hx€Ï¹!ðGÙãM>|E0Óü!âµYz²ãÙ|­î‹Èýq´-É_qíêw:àsêg¸Æ£e¸Ÿ'Þ7Íö_­† á>Ï]l½Â#1ÑäÃWàÆÑõ®«â ÿW¥èÈ׋ÏF6-úÍ;^é\Â[¦á>§~wQo/ègðàó,ŠÁd}w5 ð9IùC×bîg(ègðàóxŒîÏ]Nòº¿w¡¿wàsêgðõö‚~>Ï¢LîÏWCÐ📔_Åï‹üà~†Òü¬ŸÓWô·Öw]¾óúÖózÿ?üûÞ)xð9ù3\ãÑ2<€Ï‹(Óëݵ<øœZ2üˆþQØŸ¡4 «¸HÝaÖOÆUñ@ò:L­7¼ùdI|v¸°L½m*àsògðõö‚ŽŒ>/¢LÖKVCÐðŸSK†©·ög(ÍŸÁÂ.Q7@húǨ³«âP4½D|nåoWò¾šÌ†ÍÒIo Æç‰üüE{AGüáde|}µ‚†GãóD-~¤Þ^ØŸ¡8äG”a[kFÞØÔŽz=»sõÏØüêéOVÎÕóQþp‰üü…þQБ8„0¿Þ] AÃ#2Œç‡C~4>OÔ €{”GÍK]èA¹j™"oqÍW/üyàÏåúu«ÏWÏë÷õö‚Ž øÃÉ ÄÙój«!hx4>OÔ’1t¯uá{ úà—¨xŒóù£#×>~„/TìŸ$µ^¸Dý þ¢¾[ÐÏ8„iýc5øÃ%#Sçœèg€?\¢~†¦ç«g ƼøUâ‚éá9þÙÉkõª¥! Æç‰úü…þQÐÏ8„4?_­… áÑø<Ù.¶^¬w¹Ÿ¡ Ÿþpüa¶Þîªx@ÈÓkx÷¤–q¯Ê'ü¢Yu?¸Dý þ¢Þ^ÐÏ8„<Ûϰ‚†øœ¤|?¢îg(èg€?\¢~†0ÛßîªxðºÑ㊗ü¾‚ûø9Ѳ}ÿS\Ç鵟þp‰úüE}· Ÿþp2Óýí«!hx€ÏIÊ÷#õöÂý ý ð‡KÔÏJõ{̓õ]_ÅÃI=Õæ"¿Jw}ô¿f}¾ŸS?Ã%èg€?œ B™åóÕ4<Àç$åû‘úná~†‚~øÃ%êgˆ­¾;êGæ›yÔV]§åOñ¯?ÅÖùÍUøÃ%êgðõö‚~øÃñ „Voö{] AÁþp‰¤üA<8?ÐϸDý Àcô|­¯âµˆ¾õÃðéù@»*ê[0T§«o–a$Ò?£öïÂ.Q?+•gë+ô3ÀŽ!L÷·¯† á>÷¹QÙÜÏPÐϸDý ÑLî?|^b„šhK˜l‚G!É?™10l‡¾¿Yš¿…?\¢~UoG?üáxÂtûjàóÐÅÖ‹õ÷3”æÏ`áÇ_íäúÊwó¨Z/ã—W,ò'¹½bñËLÿˆø<ÄÌèÈ€?G¦ëí«!hx€Ï©%ÃÕÛÙŸ¡ Ÿþp‰º€Çh¿¨¯âÁq"²·6s<äoe6°u6ÐðŸS?C¸ªï¢Ÿþp<aÚ¿}5 ð9IùaÄ¿½p?CA?üáõ3ÄVßÕ?|WÙ8£oÃþf‰(­Ì¿*À.Q?Ã%èg€?œ ‚›__­… à¸ElÈîg(èg€?\¢~†è§óCÄÌ­f«óoáð(˜étÌ;·d×îÙ³ð‡KÔÏ®êíèg€?œ ´Éjàs’òñ ü0`—¨¡€Œ ¾ÝžÑò™ö²…Vò™¯ÑèS2OÀû‡û;¦ è0ˆK©I•' ¬=d‡aºÂ»ƒ†=¥Ffˆì89N§›b˜¬)úf UÈòÂÆ¯Öv«—É_=0+€ÓYÍ¿¨ñî! I@ê<aöÐÁj $ õ,óç5‰ìH–d h.{ˆí:ÊѶ8_5zrLÀ†§d×Ó^šhRˆýòÞ?€ÖYÐwçÇö%àu‡é:ïj $àõœ†!1’%Yf§û"œÃ‡³ä~œ†oõiÿ.Pè,1"U•)8Å%ò(`Í’vÛ¿!iY«8‡4ÉZ $°ŠÛ—Ã$’%Ö ž µÄÙ⻯JBŸom{™BÞ2¯`²åj¤$ w²)`Ùò4Kl˸ÅÉ8,@²ƒ ؽÈ:Â%V²ÄFÄH(Kfýe|ï£dK/V¤kMÞÒP–ïVn³´I£÷LNá¢þ¾‡€,½ó8äyHÖbÐ iôž7 µï|þ"YÃXÆñ—Ä2½âz!œü9Ù•tC9¦ø+K iâå)$‰Þè]Æ¡ÌïÙ×bÐ I€„­!‡²ÄY$ @’ŒïER—éHu&¯¢À5.›¦_žN\‰^ìqʬ,²ƒI£÷Lž a¤ì»’ØÀ8.Óõi¶Ñ:TUAb©<Á?—ÏWªÆû»â`—Yâ¿(Äï!Ô,qpŽãqˆÛì¾d5†¿88Çe!‹_M\^²¤u 8xÇñ—¤VûÕFBè•âÊÜþ¯zZ”¾ŠQk)ÞqÙ4ó’ÐBpöB4³bîj $Þ3w*Œ4¿ï’ŒxòáKÒlû{hÚ‚ýñJù> X™^úqOû¸Lí”B£w sÉj $ wë­]N\Üð°ÿC<€$ëå$—„v¼B{¥üaðm ¸ô‰ ôNJy¸¨Èï! ÑAï4qºåz5 Ð;÷+ Bd èŒg‰okzï”?d¶?¼vý.Š­ûÐ;‰åñ¢¼‡€,½Ó8D7Ï%k1h€ÞH²!©ÌÌqÄ?DÉ’ˆ‰ôNÝÉMsIh"õÆïY|²RzçËR‹B`h:CÐvï>rÙoUÑ<…$Ö—ƒ‘œŒƒûŸ(£ãI#|ü€d-ÉeoÚz™%QV\iC< j8H³•àPuzZǼO¼ÒkV…Óþö„Þa%—}¨"Úi%8µ¼äd¦½NVcÐ ½{¶Ö‚$ÉŠ+ÄH(KÂäî=T©aŸZ ž–ûúkKVâ/N®oáH@ïakœeIB–€Þy¬|µƒ è=˜qH$K2²ôNm)NCRµßc©/“­/ÓßH‹>qÞC¨Òæ)$Yzçq˜®¯Æ Az§¦*Ï^-‚³dIöˆ'¾$Íöć¦5àe²‡—ɶԷ‡H·¯è=”!Hè w‡8¿/Y‹Aƒô7žCG²$˾$ÄÓ ¡æƒÔ*Áv°m+4­áÇËž‡HÙ@Áþ‚¶r9úªožBÒnvpð•“q˜®¯Æ @_¹LW; BR CR<â$ñÉøŠëÞ¶Y´H‘^å;·{p EδJ—ù“ËÞVWÞœår,Uß<‡¤e ¬åd¦+Á«1h€Þév‡8Vœ/’%¥ ž µ ¤Vœ•°B?y!®¶×gëðÏqŠþ†ôž|Õ7ÏV\fC–€Þyò<½¯Å Az§ âP%Ølœ%;ß @BY¿ñA +´Ã®Eÿ–ßû?Vfcíþ[¹œJƒä$K̆,½ó8H%Ø6­Æ Az'O„QH¢@‚,½SBÞj‹üè•`¡j ~« úJ¿Â‘Ñ!½“¥@¼ªƒ,½Ó8¤éàÕ4H@ïtœ>ç‘,1ÈÐ{æ{¦éýpì@8ǵÌ$p™Ëä*¯ÊŽ¦Ù 8ØÌñ8’?Æ·¿!Y‹A6s¹È: ½#YÒŽå;ÍejB$£zI¬ZƒGÞŸ‡#¿£Cz§é×´›œæx’™å’Õ4H@ï%@¼Ø—+YÒ¼¼æøKò¬÷x¬Z…ãÂñµï\ƒôÎ]W•`ÓÌÌæ8„Ôš³ÍYË}@²ƒ èÎÕÇ¡J°awý2⩲$£+®XµÏÀu8,›þ€¤Ñ{¡Úiº°»ÞC@¢ƒÞiÒtq~5 ’Fï…ŽÖ§ÿ cØ``ÿdI$”%n:K‰×á¸í÷"Žs…ÒUqÞ8dI£w7ÉZ $”qH$K²¤Ñ{¡&„ì§¹Ä?¢øzÉœfRã`:WÈd ]çMspp“q˜îÒ^A®s…Ø'3Dï^²¤ù 8øÎjBȳ÷Lƪ5ì·ÿßz>ÔÚáÖ_zUU„ï\!ŸtU 6h€ñœŒƒŸ•°VcÐ q€¤@¼¢w/Yââɇ/Y€$¶†›ýwß‘TÑqÕ@¨êÛ=á4z/ÖAÒÞs2íîÏa/§Õ4H½êHCÅyã CÒÜÜç ×9NsI½šüp²µžomƒ~üpß½Ù¸Jƒôn›¾yÖÇeÐ>û9)Λq k5 Ð;õ¤¡â¼a¿ý"âɇ/$£eÇØ´†õèÝ™ªožfI@¢ƒÞyâü"x- лv$K7 šhOƒ„šršž¸ºÖ Ž;µÔìo’ñÿL"ó¤²]F©¬gº'‡ƒ]q±Ar¶n¶ &t2i~«¸ƒ Lè w2ŒA@ ¡,i•àÑÎùXµ†ý÷µœËTÕv/"¿&}úço­n$«yS¯ÝÔ ½“„ž®Šó¦9'8øÐÉ8LçWcÐ ½s'ÃPqÞDÉôÀ‰®Ð¹}@2zR16­áÆïÖãßöä‹vJý„”ù„ýéË´YÖ ½û¦ožB’% w‡HÖbÐ ½s'ÃX%8I– —ft…ŽîçÖ¦=zYU¬Z¥¸œR¸¶Åaî­¿³QP$ w’Ð A–€ÞyÊlÙq5 Ð{9tÉôÀ®ÐéýòÁvˆXµóQ#áPTm'in|uÝÃÒ=7$ ÷ÐôÍSH2²ôNã·y.Y‹AƒôNgï!É’%Yz' @2Úǫְ?d‘Ge›‘W¤é^YKÅl³Ÿÿž†¼(ÓK!Kº›¾yIËxÒñ8d3Ûǵƒ <éJ´ÐhÏïtÛ‘,A/\éøKJ+ÎÇÁ[AcÕœ8êâÜÿ|þ™•ìz+A¨W>Q8Ú­n®t…$ôtáb³‡Ð¶tBF›öw¢ÿ^¯Å AzçN†›=+ÄHöW¬Ì^D™ÚÙ îðÌyû›ôzÒ¿îóížètÊýMÝ{àûŸ7¾§@ƒôNzº,Î7/g:‡<íÔ±ƒ è;ÆŠó…[ë,z àMWè? -;¦ª5Ü UHéÒ6–~ÝéÉ_©®V¿^D—¾~x›:xÓ’ÐóU%ØnÈÐ; >…µƒ è:òˆYùˆH<âɇ/)nÑ^,}îêYp{³»1áó‹%nÓôz=k\$ wj‡€‰zçqh~åûOc×j $ wjÈ(Οܳ’‚x$ÔQfoMUkWgŸ½ß‰b‘«<(ï3]—ýbƒ Ç·vÑiMµu…Ú!òUqÞ¢u2°,ÿ>"ó;KÖbP E]¡6€<Ô9o¥¢&u…Ú!ÉhÙ1U­áý®/PrSËÞôªñ´¼¿|u`ã|Ð;µCä‹K)÷Z–À¥NÆ¡A2Ü4´ƒ 轈F;‰d Ú!àSW¨¢„iHDkç7ü2EÇîø/¶Àçsû/¹u%s°ö—À§®P;D¾:Ì`Ñ£:‡i•Õ4H@ï%´9ôi‡°è%€U]!O†§'.Ñ^r,–³|+‡÷ÌÖ#5ôžÝOW\°ª+$¡ç«6mk‘% w‡8Ÿ%k1h½§} ·qH$KÐKPÝêèK(KZ%xÔÖ&U­¡>¼;$:/UÂVoç [‡÷}˜Qµ÷êVGOâ«ä| ‰C–½·qˆó‹àµ4H ‘9t'Y‚^‚jX—¸Ç±C2z|4U­Á>iѸ?§MuAÌ¿pk7fßø35䛺⪆uô$Mß<³µ±ÍMÁUǺ6Óæ«1(TǺýQÌöb÷nd Ú!ªg]ý’2ë4”ªÖ`x›UžìÆc?¾N/þMKN>Emªžuô$®Arš%-Ñ«i]‡<+a­Æ Aâ‰çF²DÚ!,Ú!ªk}ÉþŠ•ÙkCSÕ2ˆ}òñWy½\®E#+›âý›l‡õ‰«ºÖÑ“4}ó´CT׺6Óó«1h@R8†!H‚@‚, :Ç_`k3(a¥ª5x6Ÿ¤7éÍ#>òšx  =øÿw»Õ•¤Ij„> ²$Ùã8”Y½d5 Ð;w2 U‚­XXôdл ;Û4—<Ú6ËðqË|cj´Dôƒ¡ûˆä‡÷‹7X›~™«Ë wî¸*Î[XdÐ;CÙæ *k1h€Þéþ($’%°"È wZ7VHÆSÕö±“ ¡ö? û‡¾]:äÞôI 'µU¾º/) w×ôÍsHZ–Ð;C1ó‹àµH èÝùFk—ˆm¸zw”%fz«X½¬<—JyÖEMèXúúYƒôîš¾yºUŒ-K èÆ ß9¶ƒ èÝÉ:‰dI»ÃÀл§,™uÑNUkWÇ>hŸõòõmëïœü-—-ªö^@ïÜpUœ·Yz§q( •àµ4H@ït‡B:Ì@5I‚½ô¾I‡dtÅ•«ÖPøÅ7ʿۿ>ù_™ÌC[¶l[U¯ÿ@z' ½\V‚ôNãPZ%xØih5 лï­k/ø/H¼@Oî_·ÙÎùÜ.:÷\µ¹ðýï.Z†ªD÷tJôá¾~y3;j€Þ©‚Ã9-;¢¢€Þy$§*Ÿ¬Å Az¢ÑŽL\ÒaÑQ@ïÁÑÒwÑðzÉ5Á‰õÓã{&ŸS+Á½žcþ‚Äo wj‡(—ÅùÖá7Ð;ô‡Êj !ñèÚ!‘vÛÚ!üzùÉè K¹j âYn›>nH}ñuQâÕ“ ßÉòøêA8 wj‡(WmÚ¶µCø ôÎã0]œ_AƒôE£‚D²¤õø ô)KÂ4—øf/Võðñ¾—’#¯vÐòéÿ=¥°mégÕÖÆo w’Я!)ÈÐ;Ãtq~5 Ð;w2ŒU‚‹dIA–€Þ#eIœÜ*æª5žŠ÷ç oê¶!S+KY¾_ÕYaã}]d èÇ!ÎC²ƒ è;!á,q²ôž(KfÝ´s;›ÁÍ´ô'·Ú¸ÛG!Uî{¶† H‰Sƒôžš¾yFïnC–€Þy¦+Á«1h€Þ“Ì¡8¹™ÁmÈÐ{ÊHFÛ!rÕüƒŸ³°›3¢Oôü6ÔÞ¨¯`H è=7}ólârífo@ï<Ó¶6«1(Ð{–9tà`œ“›\»™ÁÐ{¦,Á…–ƒ=Á¹j RÉìwá-=6µ¢Yz½h…ÉÝOûd€5H@ïÜp I»™ÁÐ;Cž§÷µ4H@ït#A*Î;¹™Áµ›¼½ç|€d´µ.W­áák,òÃ{«AÑ‚e4K@ïÜpU vYzçq˜¾Ôr5 Ð;ÝHP†ŠóNnfpYz/”%ezâºaçkÿ=¸íæá¨­`ŸÔ@äg Ð{iúæ)$Yzçq˜.ίƠAz/2‡Ž@"738‹,½—=K lmUÅ\µ†÷»vkÚ2ÈM4úoÊò»¯Í·òÎi4z7è8‡Yz/‰‹ÙÓô¾ƒI£w³É:‰d‰C–4z7ûJÍësgsÕ”´æwnÓWûÛ~V³Ä@«¾y ‰kYb½Ó8P1{:KÖbP ±¤aHœdI»ÖÀ[Hò’ñ‚Šh ¿æ[鼑)Zþ…»?4z7äæ_®Šóε,±ÎöqX‚d- ’FïÆÈ:‰dI»ÖÀÛFïÆP–ÌvÎçæeõ¦ZC–£~™(ôÏÃÕBj¯´rÛ­I$±Ar–%YÒèÆŠÙӬŠA‰Ì¡#xÉ, €„²ÄMo«ÖÐª×æF¯TzÐyþýåÛg`ÿêÛ¬G©Ai4z7ÖT}ódI²Çq˜68_Aƒ¤Ñ»±vÉ’€,iôn,e‰Ÿ,¨”ª5È{ó~ÔÉÖ¾)Å÷yøõúçü3k£†m+Ÿša‡· w«¾y I@–4z—qð³Öj $ w+sè$A²$ K@ï6 -;–¦5¼øñd×cÚ»UX€¨õæîì}>xëíÞÞ©bÚ9$-KèÇaÚÖf5zw‚à…ªèÄŠÀÅ ñäט0¹/)MkxÑ;T#·ª^ÓQ&Žåýâ0_=L Ð;w\BÒܼ½ó8„YUq5 л‹ÃD+xÄ“_bf=çKÕð&½ùM2SŸ‡þýIŠÎë~\ÞÞ]©úæ9$¡…zçqh†ö6«1h€Þ¹“a¨ìbH âiø}º0°µ4ì(íl‘yK»^È«öà÷nŸ„Ó¡]ƒôÎ]WÅy—è w‡é6íÕ4H@ï>@¼è ÞãfHZ/w wúƒâüà‰ÞRµ:}‘iÞ_¸}¦ÅáX,î¿ý$ wßôÍÓ,IHtÐ;Cš=Ñ»ƒ èÝ—#$§W  ¸ôN_HF+Á¥j û*W;KgZiji¦i`Ÿ“£š%ôlÕ7Ï!©}OÞƒÞy$Ã׬Ơ@âAïÁ5Z»œ¸ûq9ôxÐ{ا 3{ûhiW¥³-OäX~°/ž‡ù4“¿W%ˆü£V‚=è=¤HrKtz¢/L¯¸ÖbÐ ½s'Ã$Ù $ ñÊ’Ù6íÒ¼¬Ò!­K}fËïVáFƒ»t*„¿Õ—½G[%çS.ÉÈÐ;Ãt%x5 Ð;w2Œç³d z <è=ú$£çKJÕhoë¸<ÊOKSÓ£Â!*ÙíK˜Çï²£½ÇTõÍÓ,)ÈÐ{tGHFµ÷Õ4H@ï±k´W\R$KÐáAïô%¶uÎV‚K;›ñ¬ü—Ù”ýï[u÷të—‰5d Ð{2C ÑAïQô…i.Y‹Aƒôžl£µË,‘vvz§vk¦¹D´†;ÛPâ).¯”|ëç $ôžbÕ7Ï ñh‡ wcæ *k1(Ð{JÀö"€Þ©ÂÎ^PÚ]é®/å¬u¦É¹éRãÔ·Oj:лè=›ªožu¨x´CÐ; ù³Žÿ ÉZ $ ÷l9†,ñÒáÑ@ïÔag=TJÕdo»Ïvè:§³wþ“/Mß!r ,4§!@ï9VHN³íôNã`ìl;Äj $ ÷œ†!‘vvˆz§v;ë¡RªÖpç“1â#ÇþÈæêÅÙÿ®/™å¨ã§ ÜÐ{1Ur>‡Yz§q0n~âZ‹Aƒô^,dó‹Ý»—v߬|½Ó—’q.y·ïûÕ‚Š,1ˆ Ç>Ǥ!4ÿ£i(€ÞK¨úæÙ"Ø££#€Þy`kóýVý¦÷µ4H@ï%r #Yb­@ ¡2J+Î^âwkwp!îÍ]½¡ùö/É÷y'‰kb?ÊŽ±Ñ»Ý¶ÉY– £#‚Þy¦‹ó«1(ÄFïv3Í/ÁÞ&¤ ž|ø@2º¾U­aãM•ü@mæòäÚ®»•_öŽ­ÃÕÝ{t€ÄWÉù’æác£w‡Vœ>÷¾ƒ‰$cÉq‡ðÍÂGH"-R’2|=Ù­i þ°ŽºÑùñÌ·OtÊÜÈ«gßíéõ, €¤TÉùtÅÕÜ!| ö8a–KVcÐ iônÍÙüŠKÄÂ7w½ó—Ø8YP¹U­ax÷`4}‡ ‡›Ä‹äaxZ¾«4z·Ü˜á. ;¼C¢7z—qˆ³Öj $ øaHÄ£½#¦|ø@2ê|kw¥†pç¤<‘Ùüë4H IUr>¥÷fpác±Çqhüy«~C²ƒI$"›L\Þ $ ñ’}º°(ÎÓ{ho˜±zŠS…›,Ç¿U I wêj0Wz‰o>{‡éÎùÕHè½6— AÂeGöŽz·”%³žó·v6o•¯'Ê2—VM$ wêj0WÅyß .|½ó8´â|‡d- Ð;7— ç½\–áÑÞ‘@ï¶ zoUkÀk^ídìþ¯lîj ý\b}áÌwŸ@ïܘ¯è= K@ï< ’4Ú9¿ƒ èÝ9ÈæW‹à Y‚öŽz§/±e’ª5°›[º)QÔ7/’‘îT’HªÐ›@ïܘqu˜ÁG$:èǡ̶C¬Æ Azw©ÑÚe–Èe¾]–ááZg]>B2Ì%UkxП·=ð;m›–*Ôì÷µÙCƒôÎWÅy‘è w;}!ìj $ wo!›_e‰\–áÑÞ×:þ7«—ܪրr\äz©Aܤqë•!ù¸ÖYnÌÈWW»ïÃõŽC°M/ Û`Ae5¸ÖY9†‘,IRPIñ’ýs¸v°õVµ†'}HÜRû¦ÔßøÉÖoRÄ{òýöz*\ëö*$§‹àfpááZÇã`Í<—¬Å Azf)¨ä ñ4H¨/¤B2nþt«ZÃþ`~ìšÜ®Ê<ïÃ'âót?ºáZgC¨’ói–dd èÆÁNçWcÐ ½‡Øhí’,Y’‘% wê qvÑ7:ÓÎļؤ’{k0!KDûë•tH@ïq«’ó)—Àà®u<vú0Ãj $ ÷hÚz ‰dIA–€Þ©/ĹéW=›ÁOnZŸ¿ ôØtò9úÓPßÚN“;kê‡àZgcp­“qhzÉð-?«1h€Þc„F{EïrY†/ñä×8? ‰h éIüÜÚŸ–'ažr“­Sññ=S k¥†sIKt¸ÖÉ8øÙvˆÕHàZgÓ6‰”KF<ùð%€dTº·»?`ÂêqIî_ož èç´²#\ëdülq~5 Ð{ò<‡´CÐÊðéâ$ûtáf=çïUkø•Ü¿Þ< Ð;7f\]Ú}®u2a–KVcÐ ½§r€ä”KÂd è=S–´J°lÓ¾W­á+¹±Ä¯Âh®{aràMÕeé$ wn̸:Ì6d èÇaºs~5 Ð{öÃ$F²Ä K@ï9 -;Þ«ÖPktåc;¡úÍ‹ûÄ>ö—a\ë,7f\U‚ƒA–€Þ³_ƒd- Ð{.m½†D²Ä K@ï…²¤çGí6ïUkˆ[­Ÿî»~ô=9î)xð"~ÿ™1Ýé“§¶â p­³äS1IÍ’×:‡éâüj ! p­³ÅCb%K¬C<€„²¤U‚G[ëîí®tnø—³KrWúžý7iµyÕ‹žw:¶ñ¾é÷—¸ÖYò©°WÅùÐîûp­“q˜®¯Æ Az/²ùÅŠ+XÉ’ævàZÇ_HFïMkx”@畞Ô[…ùê"Ùfgf§M\®uŽLìUq>ØÜB½Ñ&í6WcÐ iôîÈkÃÆ¡—åâ|hn®uŽúBÜìa†{ÕN tƒÂD«i$@’F qHôFï2e>KÖbÐ I€$ 9Ÿ¸œH% ”$£[Å{Õè y%’94)Íe¬¯8éƒëŒ~ +ÀµÎ‘ÉKÎgeÇà%ÅÆÁMÛڬƠAÒèÝQK‡êœN²Ä#K½;ê%ðÛô¾¤ÞýÁ!ìO>º¸ÿ \ëµCØ«ÎùÐÚ!\ëxV Y‹A®uŽÚlâi‡Ͱ#ÀµŽ¿ÄÏ: Ý«Öp¬>Ø­öÐ~½U!+4zwdØ1IKt¸ÖqÎÌªŠ«1h4zwÔÒa‡ŠóA ;B3ìp­sÔKàg=çïí®ô™m¯ª—¸Ö92ì°Wó¡ut¸Öñ88;{ k5 Ð;µtØ¡â|ÃŽ% wê%$nxÅ%Zƒm‡1Î2ã² è ;ÜU%8d èݦ$×Z $ w×5Ú«‰K ;B3ìp­ã/ñ­8?Ú¦}oZÃÀ[%!ÓQ =K@ïdØq ID¢ƒÞè Ó×Z $ wjépCzIÃŽÐÜ.\ëõøÙà ÷ª5Œ¼U!ÿ…®uŽLÜU%8´ŽŽ×:‡éÎùÕHàZ稥à f‘%¬ÐÚ!\ëõøVœu‡¸W­áä­R+x$ wj‡pWó¡µC¸ÖÉ8,@²ƒ èÚÜX%XÚ!Bk‡p­sÔáÃäVñQµ†ïBïUOƒôNí K@ï<aV{_AƒôNm£H–dd èÚ!ül%øQµ†c‘´·>ñ™¿Û‹B ZQ¨³´ÚÇàZç¨Â]ç3²ôÎãÐ*ÁÃ>Á«1h€Þ© À¡8v¹xvˆ‘% wj‡ð³×<úÝâe±OȶÝã^½ÇãÆý¹Ýd¤AzMß<‡Yzçqh _.¾ƒ è=Ê:’%Ò ²ôNí>M®¸íîO¯To¯)µÇ£6{<Ûç*è½Õ,k£vˆkHZ;D€kŒCšŸ¸ÖbP k‹íÕ"XÚ!Bk‡p­ã/$£¨ª5Ðc?é9É.ô`Èçy…Yc¹U'Ñ·Zv„k£vwYœoí®u2­<|äg5 Ð;w2 ]¤"´^‚×:G6>OO\áx\¦ÔNŽ[¢µèÛu‡V¾Ó ½“„î®lmâÖ®u2¢—˜.Y‹AƒôžD£˜¸âÆgã†,½“M†ŸõœT­áyoݹcvß Ç>SÞs%‰:ÓÝ¿›fkàZçHBwWÅù¸!K@ï<e~âZ‹Aƒô^;† ‰ ²ôN6a›æ’ÔLøÞ·z–'öNh¯„@²]¸˜»j€ÞIBwWóÑ K@ï4~º¼ƒ è;†ŠóÑH–d èl2ÉhëÑîþ`³Mš;¾m¦µG=}“ìT.kËMß<‡¤e \ëxüô½«1(ÀµÎ•­ÑÚ5$’%è%€k#›Œ0ë4ô¨ZÃ_Uôx‘çÐ ×:Gú5$Íà"ÀµŽÇÁ›ù,Y‹Aƒô^Á‹W´’%h‡€kI°ÓÜþ¿„^¸Ö9j‡pWÅùˆv¸ÖqÞ¶ó%yxŵƒ 轈F;’%ÒÑK×:O6 ܯDÒ:lG4H½{’ÐYß<]qY$:èÆÁOçWcÐ iôî·®Ñ^œ{Ž›†"Ú!àZÇ_Z%x´CåQµyàãé‹z4 &ÞxYOWÜk@’F A;\ëd¦mmVcÐ )€$s #Y"ííp­óÔütK´†øæEc"ŽÜؾ 9H&½ÎŸ73¸Öyj‡ðWÅùˆv¸ÖÉ8øù,Y‹A®užÚ¼Ù*Fi‡ˆh‡€k§vˆÉðî]´y,O¹šÜ°v ÿâÏ›å¯ò/òUI¸àZç©Â_U‚#Ú!àZ'ã°ÉZ $äFkWeÇ(í½p­óäÐ9?hØñhZƒû÷ºñÓºÚȹOÅaër^592 AÒèÝ“„> ²$Øã8 sþ{îý ÉZ $Þ=w2 µiGq‡ˆè%€k'wˆ ÉøÁ¸gÕvF¼?»ú39ZÔ$ wÛôÍSH²¤Ñ»ŒÃt›öj $ wîdƒDÜ!"z àZçÉ"´â|lzV­á׋õëD€ èÝ5}ód èÇa’µ4H@ïNæÐ.wˆ‘% wGY[›ÁnÇgÕƵ8ŠHàZç¹ àêŽÞ[–ÀµNÆ!ÍîÞWcP kw2‡ŽdI”,iÖ ®užn y:KDk€÷už–ÔÇ3üúî®už»®Šó1¶,kŒCžíãZAƒôîÝ8$’%É"ž Ýf;çŸg3Ä‚DÔÒ¯Œ'ä~æ€àZç}Ó7O÷% Yzçq(ó×Z $ w/sè$I²$!K@ïtÓD˜-Î?ûÝ0N:¶(Õ­ûÝ;\ë|húæi–$d èÇ¡ÌO\k1h€ÞƒÌ¡CH–dd è.ˈÛ4$¢5|Wƒ|µORß6½Æ×:Ï]—dd èÆ!l³zÉj $ wº$b¹,#fd è.ˈfr_òìg3¾Ÿÿ/ qÊ/(ÀµÎsÀe%¸]–àZÇãà¡ò½Bù ÉZ $p­ótI„òœrYFl—e¸Öyº,ŒîÞŸýlÆf?®†Ügf¹Àh_ÍË]lÒV°S£êZàZçé² Y n—e¸Öñ8„ékVcÐ ½G™CG²D.Ëˆí¦‰×:OM±U‚GÏ—<ZC3áA9uÿ“®•h%»~íÚC…ôN, @‚,½Ó8„i•Õ4H@ïIæÐ!H8KÒ†,½SBDOððŠëspâ¶ÀG/Isû«fŸ#]NÈSqä_P/p­ótÁB¸jÓN²ôÎã0í9¿ƒ èìÆ I›H% wjB$ãû’zWú«Y³'¾P‚/3I˜‚µ‡‹n_?:TàZçé‚…pU N²ôÎãàæé}- Ð{–9tɃ,½SBœµµyV­á‘i5Oí™…Þ°=:¹Þ€hcŸuŸ‡“Mަk¸Öyº`!\]š›B€kŒƒŸ‡d-¸Öù,sè½'#YÒnšp­óÔ„Ã4½‹Öp¼ðÙeŠÂ³Oå—€-·FêÇGáZçé‚…pU NÍM!ÀµNÆ!Ìo×bÐ ½“BjÓNF²ÄZÄÓ ¡&„8Û9ÿìZÞÊÇ÷ì¹µãBÝÞ«m¥ èîˆW•à„ö¸ÖÉ8Äù‚ÊZ $ ÷"sèH–XÉXÀµÎ“p[%xôƸgÕ¨ÍüyÐÕŽ¡áUoX½7 ;¨[E¸Ö²"W•à„ö¸ÖÉ8Äyz_‹Aƒ¤Ñ{Ødɱ"HÐÞáZH¸Ži2K^Ukè¼z që‰.óp½·P‡¤’4 ´w¸ÖÉ8L·i¯Æ ARI†D´÷í®u´÷˜' *¯ª5È~ö¸·•Óoûš%rW­ãÞ¨ý‡÷ûlj^¸ÖÒÞÃU%8A{‡kŒ ÎG'®ÕHàZHsCmÚI´÷í®u´w@R'®W;›ñàÅ¡0â­©¢æó8Îl$HRÓN!ö×:‡»ôç¯Ôsï®uޝÇËâ|F–€Þy¦{‚WcÐ ½SÿÀ $Y²$#K@ï$\§0YãzU­>ñýÏojtÞøèÌ¥÷ªgûèwrý¸Ö:¾/+Á­} µNÆ¡U‚‡½WcP k]ˆ2‡Ž¬¸²dI;ÇáZH¸$ã[Åêeå©xª‡PaÆú;$ w:¾/+Á­} µNÆaúöÑÕ4H@ïQæÐ‘,)’%%"@BYÒ*Á£­u¯ª5C íϽõ2œÌAÝAi•׺@'ð’³}IA–€Þy¦+Á«1h€Þ©`Î’¼!K@ï$\’QoÇWÕª8èlì|øüqø\kÓŽp­ t?^U‚ó†,½ó8L÷¯Æ Az§þ8Ô¦7' K@ï$\§VœÏƒg_Ukèý<<öíðùëð¹vGo„k] øñªœ7d èÇa’µ4H@ïYæÐ,É›d‰A–€ÞI¸N¹/4ìxw/«úØïÃc?úç~;|®¹ÖE¸Ö: Ikˆp­“qȳ¬Æ @׺Ó0$F²ÄDÄH(Kf+Áïª5ôÇ6‡Ç~>·‡ÏÕ\ëÀW•àÜÚ"\ëd¦{‚WcÐ ½Sÿ@*Îg#Yb âipf ÎßýîúØ®?öÇçþð¹Vœp­ %V}ó‹,½ó8,@²ƒ è½È:Â%V²Ä"K@ï$\gx¨ ö¿«ÖÐ;Û>‡ÏÕ­"\ëâf† A–€ÞiÒ´‡Êj $Þã&sè$’%YÒè=’pÍt–ø¯px³xYÞõ‡÷ˬ¾—‹4H ‰Uß<…Ä!K½ó8¤éJðj $¤$ç—“,qÈ’H(Kì4$G/+Ë×Ú¢÷ü(][{Ñ9áZ·o_«¾y¶/É­} µŽÇ!M»C¬Æ @׺hd‚D²ÄÄÓ !á:Ï^uùnZžœKvÇ•>Q¤µÃê8@«˜vš%­} µŽÇ!Mß>ºƒ‰$2‡Ž@â%KÚ5®u‘„ëÜ*Á£Bï»j ûs&)ñ ßXt8­!¯—µ?=T"\ë"]0 ²$Øã8L÷¯Æ AÒè=RÿÀ($’%YÒè=’p HF;TÞUkž¯^zÕÚé™üyC· è´÷tU ÎYÒè]ÆaÚºy5 Ð;iÎi¨œE{ÏYz'í=ûI½ä]µ)R6åÑü-ž4 E¸ÖEÒÞÓU%8d èÇa’µ4H@ïÎB¼èPÉ¢½çv @„kIž½êòÝÏf`%"¢8¦ÞYp4Û}Ý4HàZé€kHZû@„kŒC˜½v5¸ÖEêHCmÚY®Èí€׺HÂuž-;¾ÛÙ yÎr8¦tÒà¡ù‰p­‹t @º*Îg´ÀµNÆ!ÎsÉZ $ wêHC•à,×äv @„k]$áŒ^ð®ZÃѶüØë„Fœtoë”_\׺HפËJ0ÚàZ'ã0mرƒ èúÒPOp–krB–€Þ=¯³¦³D´›¿yñÁ¬;%ÒBæF“ü4[›׺H× @‚,½ó8´Jpç’µ4H@ïÔ?0 ‰dIF–€ÞI¸Î³ÖÍï¦5°-èþüÆQ› - ]ýgÿ×w¦5‹+ü üW$ wº€õMÕŸ }®u2yžÞ×bÐ ½Sÿ@«Ë59#K@ï$\’ñÝ»h /±i.o$•nÕϰŸ9Yeu_׺HפËJ0ÚàZ'ã0í¡²ƒ \ëb”9tÉ’v @„k]$á:·Jð Ý¦‹ÑŸw%}ÄýF»®—ï”)¿ Az§k®!Aû\ëdZ%Ø Òûr $ ÷(sè$r @nú®u‘„k@2¸U4[»+½½LÒ\*Q`·køÂnj9×ÿ$ w²ŽO—•`´ÀµŽÇ!ðã{…ò’µ4H@ïÔ?0 gIÙ% w®Ë67q)ʲé+Ü|“L³ÜsTfÝGâzð/è=Áp­‹dŸ®*Áíp­ãqÈf’K–cÐ ½'™C )›H% w®K+Ξ{7[¿+½H«æþ¶j"H|á³|xüY^8 Ð;Yǧ+wˆ‚ö¸Öñ8’Ñ>®å4H@ïYæÐ½¤l’%Yz'áºÀ°cLÂ"c1)FðñpŸþÝË!¹ÙûâÕ.eS/<ØÔ­"\ë"YÇ_C‚ö¸Öñ8äYÃŽåHàZ© ç‹‘,iú®u‘„ë2Y 6[Õ$„zqýþó«Î·ýyg‚|VÇ Ð;YÇç«JpAû\ëx²›Ï’µ4H@ïÔ?·¡,1’%ÍC?µ.’p]üôŠK´†þ&ñ¤$;-¹VŠì/øoi~~·ùYôNÖñùÊ¢ }®u2n>KÖbÐ ½™CG ±’%Yz'áºLºiÓ»Õ¼¬¨èPúK¶IÞÇ˾ç÷;ÿ 6 Áµ.‘û=Crv¾¤ }®u2³†Ë1h4zO›9@r>qYɇ,iôž¶c– TDe p¸Ð_ÿžþáM6»ûiã?Ÿ|LùΦ oúe ’HBÕ7O'.´ÀµNÆaÖ°c9 ’H"Ç0‰“,qÈ’HxÊ`Hâľ¤Þ•nùÔ«áD÷ÿžr#‹%ƒö»á 2‡&¿–ÔnG¸Ö%³U1í4KÐ>×:‡Ù6íåHàZ—ŒÌ¡CH–ø ñ4HH¸.­ó³%¾Äßj8@F Aû\ëdf‹óË1h8@!^HXÅK–xxòáKÉà‘Ÿ=ÜýA®ÔBX•¼Ú$ü¤ ™znÚïh@Rj8§—o‰×:‡4y“õr $Þ“Ý8†‘,ñQ )ˆ§ABM%׉k°M[ÊæPh0í­Ú~±FG®»ûϦþü6?V\p­Kä~ÏúæéŠ+ ѽË8̶i/Ç Az·c$ðÅK% K@ïÔ„P&­›ÍVµ†ý!]©ÅkÛû–ä-L'Yz'÷û|Õ¦]²ôÎã0Û¦½ƒ èfÎ×:‡²Ío×bÐ ½û­ÑÚe–DÉ’XOƒÄ›$£YbªÖ€ûmŸÒsÓ*ÙtÇ07§uŽ4?*Áp­Kä~Ÿ¯Šóíp­ãq(³†Ë1h€Þ}€ xµâJ’%ÍC?ÂµŽ¾dßkMÖ¸LÕ¾ªsöEI{×-0ø+LŠ‘cÔ!½Ó ü|YœOHtÐ;C1“×&/Ç Az'ÿ<Ô9_RH2âi„íÉhqÞT­A qу_2)Ñ=¡boõ Ð;ÀÏ—•à„D½Ó8”ÙÎùå4H@ïäàŸ[%¸®Ç@’ù¿’‘% ÷@Yb'W\¦j _«$}b¾¼µy8=«»• \ëÀ¿†$·,kC±õ&ëm|âZ‹A®u‰üËX%8K–4ý׺)KÜdÙÑT­ÁÎìüñzIabOý:93}æ×6m¸Ö%:_.‹ó¹e \ëd¦+Á«1h€ÞÉÁ’"YÒ<ô#\ëR¤,A%x¬µn^V´blšâ(žÇ´³t‚Iüy­Ëà$ w:ÏúæYÓP)ÈÐ;ƒŸ]¯Æ Az¥ÑÚ劫H–d è=mHÆW\¢5XÃç]m}öÿ—}OºÿI§ÈXÉÞB½Á„MCp­KtŸõÍÓWA–€ÞyüdÓÐr $ ÷$sè5$vÛ(KöÿA–€ÞeI˜†¤zY…Zex¶f}û%–ñò¶É Az§øWì! K@ï<ÓÅùÕ4H@ïäà? IH% ÷¼ ÕÞMÕRt•eÝxIï ®u‰Nà—‹JðBÍ’×:‡ÙžàåþB’àZ—ÈÁ¿Œçíf$Kš‡~‚k]Ê”%“óÔ§!kú?Ô¸¯¿¨!«—e$¸Ö%:_.*Á{¾…zçqˆóô¾ƒ è=ËzMï{ ’%ÍC?Áµ.Ê’IÃŽ=„[3áû®jn¼hØÚ_iû’׺D'ðËŽŠ{ÈÐ;ì›ör $ ÷â‡!±’%Yz/”%yzÅuo;ß¾í½›"ö{rÄ#Q£÷׺D'ðYß<…Ä"K@ï<³mÚË1h€ÞÉÁ¿Œ¸iïH–XdI£÷¼Q–”I½Ä´»?¸‹Cz-?¿¸Rg>ëlí÷ì€ñ~©4zÏtŸÅ´sH% w‡Ywˆå4H½çMæÐHœd‰C–@ÉÌÄõlý´¥k¸¿lDŠtKßèµÇ·áÇ½Š ®u™Nà3$'û’=„–%p­“q˜uÓ^ŽA®uy“9tɒ桟àZ—ÍFsgi¦Å6k;ô›{jþÜyã?…ŸÚ¥@Òè=Ó ü’®²Äµ,k³§'®µ4H½grð$g5.»yɒ桟àZ—M$y¸ÂT­Á—^?•¹WõéÎ5ø«Ã?@’«¾y ‰G–4z§q Êé4$k1h@R8†‘E°§,ÙLÃÃRŠ´2ð¨~e«Ð`=½F†‹–aÈþ÷ù¢7L¼a(3^U¸ÓðhÜžéø}¹ª9¶çO¶ÀŒÑ}ûjس•ÙsŒ‰€H€eƤoбU_”ª#÷ÂñeÚ—‹ˆå€ÒéÔý±=ãs Ÿª×“ä±€†øÜ–A$¸k I*§ª ¥rÛîùhs®*^‡¼¹°ÖÍÂR2Zgc‚CÝ@Õ2Ï’¢eüéh¨l=9E­F `ºìXäk`€»}®ÊåIï‰Ef€¼yÂl¡d5 ·/£`Pf8dè›ôtÓJ‰ƒ·-ì¡Ävèø½Õëy´ÒZœèrpU³}Ý,mâ›§Ý*à WŒ©rã-3à'#Ъ¶vË£`¬E Ñ¼™1¯3ƒ;Ø[ÿz‚w\1”¾îÀïÚ1®ªò˜ûMãÆïR⦊̪VøQw\1±Ê\'`dF#pYGëå40Às ÊŒ‚̃2£•ÐG•>WÕ€7Ûù} ò'«}³,ôù´®qÅšÆï}FAf${0ÆZÀ‹µƒ`n[7r£Qx±”èÇô/qUØÔo䘺¿DîÖž>ôƒ&°lÉ¿°©ǸbcUÏ´¾ ÉÑ8\aºw5 p¸MÂ|²cCz€Åm>à1Z rUx°JLˆ8Íß|r^(‘ï6j¨ØWв81šÿ~‚]\q¦ §x´ü€[œ BœíY AÁnqÅɼ9€7¬ô«Ã+®P;€MÓDUÀºê~çR/v&.´É5õJ¢ùAÃDNçÞ“ÓæhW‡Uœ BšÇc- 0¹“©sÎ4«Ã(®PG€-àº* гŠfYê´=ÛY:Û>1õÓW¹0Š+tèÝ\yd W>q2y~™»‚†ÈÜÛQ<¸UÝ S.q…šìä=†¤…µeb}•l»t£u|Kú[®=ì¿ðÈ?t?¸Ä«èu†Õa'ƒ°€ÇZàs/SçœhS‡E\¡¾à1Îçõ¢ .«I§ý{k‡†®Çs§ê5 q%˜<às„2Û¼³‚†ø<ÈÔ9€7©ô¨Ã®Pk€k•ÜQç+WÑ÷)ش禱GOR³Q—»ð‡+!Vò (û°‡ãA°ÓýЫ!(xÀ®´?ˆçÔaWHY£F£®Š²•Ýs/=X8‹=Ùõ¯ÖÔªá>¦ª_§x´ü€7‚n†^ AÃ|eêÀƒÛÓ ºÓá WH\vf²×ÐU§~»šÿ>4‡½zGå>D=?Àç1Vì èû0†ãA°ÓÕõÕ4<ÀçQ¦Î<8?<ò|Nú²›mÄuU*MÇ®©W5s÷¡Ù¢žÎ.?NžÁ®$3‚ò|Nƒ`ílïÈjàsÒøÇðœù>'‰Ùµ‹$G»G\»YÜ_¥=åŸlÃí¾œÔ~ñ9<áJŠU“<Û@å‡%‚îŒ^ AÃ|žØ4pÎÈä0„+¤2;´ãо~x5›óvM–“ÃZ±®¥Å8sç’~~†p…dr{åŠ ~p2ÓeöÕ<àW²…˜wátÅB¹‰ÑäÃW¸Y# _5ƒ“s¾WßÞ‡¿ÂŒ?ðŸÓÉv{Ù«Þ2fp2~¶ñp5 ð9‰ýv¨Y}g"Â#!àÁöv“ûAß.ÔxÑI_Zò½?ÛG ÿÂڛ/ÑÒ%AXÁ:Ön¯,ª!÷à NaÚc5 ð9éýv¨¾ á‘às’›Ç¨^î«rЗ%ÍK^¢@×aJ³^=HÇ'í¬žàs:Ón¯ê»Püa'ƒ°€ÇZàs’üíP}7q~$äø¼°ƒeó ž¯êa[»óèq ‡Qj7_Úz7_f }?XMàöµ6h¿Æù>çA˜®·¯† á!|N!{ü‘8?Úmr©:ÀÕ¯p³ÎԾݣў•ŠÒ‡æÉÛ­zŸ“;}Ôð¨pô$¾sz¯TËðj×!ÍóÇZ ÕŽ%4*»¾:]®VE4Àc»\ž¬_ù*lr@ê§Ò«vá¿ÙA"pŒ\íÔýGµ£')U¨<½Ë¾exukƒ0íE²‚†‡kxÐi|;b|ar–ËrMÃÚº×÷ƒ¾Ý8ίÏ…Û»%"´ßjx„†·—žÔÈ`ƒ0mI½‚†Gá€ÇÅõⵋh€åG«ïŽ6#ú*È‹c ßgÀ¬˜û¡Tã7z’R•ÊsÑA²ÇA˜¶¼X AÃ#5<è(¾³Ïâž„hÔD<ÒwŸÌO< È‹ƒc{`9ÖÀ"OÇÔ¥‘ù@ªÎàs: n/ê»vC~{„2{¼c5 ð¹ Ê.ÍãüŸSŸ­·ûfÅ/Žô˜[Ý7É.wÿóñþ6!ú‹GÞÀçt"üšyŸÓ ¸mž?ÖBø‹GÞÀçN¦Î<8?Ú¹ø¼Ï©‹ÀÏÂøvËx ¾JÜ'óæV%rß|µUü­Ï¿wð9 ·õvkl‹|Nƒà¦vWCÐðŸÓÁü1< çG;Ÿ7ð9uøVß­'ú&¼«¯NÃ[û±8¡7‹Czÿ¸h-oàs:n/ê»Ö ?Àç4ÎÎ㱂†øÜËÔ9‚ç‡E~€Ï©‹À»ézɳ‘¡8Ò‘•ͻޱtoˆ€cm5S»i‡˜ó>§£áî¢k×Zäøœaµ4<Àç^¦Î‘óýœù>§.à1¾ÿñ@žØ'þ„,é9>Œ-wÈíì¿#7 íäMÞÀçt:Ü]ÔÛ­E~€ÏyÜl¿Ïjàs:¡?ˆç‡C~€Ï©‹ÀÏÞBè«xp{Ò³Ò—zY¯\û¥TUG;ï‘ øœˆ»+û ×òÀÏy¦›wWCPð0às²»w#õ]ë8?Úeqـϩ‹xŒêƒ¡Š{t¤îëÜ£¥#Ù¥p#,ÏÅ;+‘L­5¶g>§Cîk]Ë>çAð³ûÁÕ4<ÀçQ¦Î<8?ÚMqـϩ‹À‡I>U<¸ùZóys÷ؾt|•ý««/T÷ÚÔ4g>§sî—xxäøœõÝï÷7k!hx€ÏcÅÃs~xäøœºülÿnhâ4gÞ»²ðøœöþÒÙgUrâàs:êî.ê»Ö#?Àç<ÓõÝÕ4<Àçär=çG@~€Ï©‹xŒÏW®¶Û™ð‘¾\Òc¹ÿÕóùc½kÀçtÚÝ]™äøœaú.ÈÕ4<ÀçI¦Î<çG@~€Ï©‹ÀÏöS‡vxâIâZä{àü£^Á+ó¬ÌÆ|Ä©àaÁç©T<Îò#´ü°às„4Ÿk!(xXðy–©sÎhMú|«·ÖwC»ãøµ›EŸ¯úZí±EÖלûÙ_’-ø<ûªTžáÑ:²Ÿó äùüX AÃ|N-cxDÎ ðˆa½Y²uº™C‚7s¼€àó\ª2vê0†üŸó L›b¬† á>/2uŽàÁù‘àsRŸ}™¬'†*Ø­ýr'™ÄðÚúËu|½åY~€Ï‹¯JÌY~$äøœ¡ÌÏWk!hx€Ï‹L#–oœ ù>'õÙÏz~‡vx"·^_»üÈš(¶Úµ¡K|`m§^|“-ø¼”<às¿Íê«!hx4>7Ô0ˆçGF~4>7¤>‡Vßí/ í.ŒÐ‹|5ßûº°š—GæÉ;Û¡jý>Ù5>7¤Ÿ»«únÓϳk|΃àÍü|µ‚‚‡3À#@ü>7ñ±¬ŸÛM>|E0Óë«[kÖ—£¨[:„ñî }·'ß9*õáìðÈ#x´ wÎ"ðÓ×>®† áá€Gá©s ?2»·– Ñ4Úïªx ¾JÎÔYø[û%ŸÑÓõ(×øÜÐ)xwUooÙ{?ÝO½‚†GãsC-cx6p-È<(?`Ì0hYºx ÊÚÖ¶NÇÒ‚\¿p7}™¯á‘€G®ÊØÙúª ?Ÿó x7¿Þ] AÃ#Â!Œà!®ÈÆç†Ôgà1^¿ñ€ŽÈó½ {²ß^|5ÏgÊ$û]Å£ñ¹¡“ðþ¢¾ë6äG±ÇA˜îo_ AÃ|N-~¤¾ë6¶qÝàsRŸÃl½=´Ë/Ón>\˜x¬ÁeþäW?Cöàs: GË>çA˜®·¯† àáÁçÔ0ˆçG3ÆÏ|NêshõÝÑóQ¡Š½À’ô_®°Œó0ÿî‘«Co*\—_ë]>§“ðþÂ.áÀƒÏy¦ûwWCÐðŸS €©ï:¶ÆwÍ?{ð9©ÏÀct?›xð8”\ó1ð¬6Kg¿xþÕðŸÓIxåsŒ>çA³ëÝÕ4<ÀçN¦Î<8?,ò|Nês˜­ïÆæeè$¤àoŸþêkßÿžž†øœNÂ_â>çAˆóx¬… á>÷2uàÁùÎ"?Àç¤>‡Ùúnì¶Q¼?xÙïWLÃ|N'áýE}סÀƒÏy¦íŽWCÐðŸS À œù>'õxŒêQ±Š4çÞ¯ØþÜÓ|ÿ{ý>§“ðþ¢¾ëÐÀç<i–?VCPðàó Sçb“ßlæsŸ“úòäþ#¶«Á9ÍCª.ÿj_5ž}­’e ÿú÷ðz?uàs: ï/úw:øœ!Ïö—¬† á>2uŽàÁùÑœæsŸ“úÊdý*Vñ˜[·©•êòñÒ½íþàs: ‰:øœaº¾»‚†øœZ18¼b |N:¸¿¨ïºæh#øœ"­¾[/µÁc->O"¶ðGðŒGB4ÀcŸ%¢™Æ£´É·?+Júߦßë«>'œ•±³ýys4È|NƒÌl}w5 ðy±u:ÏéÐ Áçtš>ÚéýÇí°81ê+–?³þW~€ÏI÷õv‘às„0}«ãjàóGñ`?‡n€>'?à1êÏ«x ¤·¡þÃ*B-_Ûºtñ/²2ú©ŸGð9éàᢿÝEäøœ!L×wWCÐðŸÓ¨ìÎtDð9ùDœ7<¯«xñYîÃÒ=s ìÇ¿:Ý??GðyiJåÙ|•às7_/Y AÃ|^b£²+<ØÀ¡ ‚ÏÉ úéõ•ˆû‹SõÍzÇfÌÒ}„êà‘Ÿ[ÒÁÃU½½ùä>çA˜¾ßq5ÔøÜn2u^ŸwvìàÐ Ÿ[2¡óõÝ×!:àe[Ï’ûèØ¼Pqí´¤†‡¡*Çg— ¶{rj|.ƒ0}«àjxħù‘9?ÚiúœðHÀ#OÌWï¶8Ù/ܨôö–2ødj2ýºÝc„Ï­iJåY~däG°ÇA€_ø¨ŸåjÏ­‘©s΂üh|nÉŠ>âÒÍÁz{ê—÷,™ƒq‘ý™=_#ð#?ðxäGãs„8;_­† á‘€‡Lx΂üHÀƒò£ÕwGõôa…얨½Í±ÅéjK™†GãsËJþU½½ ?Š=Ât}w5 Æç–óâAùá7äGãsKnô1Oî?R?·bëy½Äožñˆˆ&¾¢ã1XOLU< £¶÷‡KÀ»R½ÝiU/Ÿ¿ðŸ“~GËð >çAȵ^¶ÁúÕjàs'bëÀõ›ìàÑ Áçägû©S¿ì¿Ìq~SxùUŃ¿e¯g½YÃ|N:x¸¨·{ødð¹Å`rÿ±‚†øœ›†ð`?n€ >'?€Ôꉣý%©Šbµ?¨ÙÚ{”«ñ6R}þ3/ÃþÞŸgð9éàᢾëáÁç4q›Çc- ð¹±uÎtdð9ùt<†óCÄ׊ndbðìþçÕ Ò|Øèý%ð‡³¤ƒ‡‹ú®‡@ŸÓ <ª®† á>÷¡MWx°€G7üá,ù$3½¾ñ@\ T«óo tq¦ø‹üá,éà×x´ü€?Bœö/Y AÁþp–›Æðàü@7üá,ù¤VOüÓñô‡~ýø\ÍøÃÙДÊ3<à8„Øê»i´Þ¾‚†øœ›F껞ý<ºàgÉxŒö—¤*èýøþ·…kx€ÏIõ]?øÃñ ¬à±‚†øçA€_Æwî7k!hx€ÏÉÒ ŽÔÛ}äüˆÈð9uÑþ’TŃ£ÇÝþZÝùæ¥ý‡[ Sû\|çšÏ>Üî:ŸøÃÙœ«RyŠò|΃0Ýß¾‚†ø<ˆ­ë«(ùQM>|EJ“õ«\ŃۃÝÔ¹zm·vÑL¢`ß{¹/ä³¢ü á>g%ÿ „ Ÿó Lû…¯† á>/ŽCÈdäMÃ\Ò¬_x®âÁþ­ä(Á¯eGt*SsiÈ:ú+ùáõþy^­ÀΖ¦Tž­¯RËpøÃÉ L×ÛWCPð€?œ-™C¸ÖÏ}ŠŒGA4À£ðÏ{8쵿;ÏBYÿØ·Kü~ÑlZÖßø¯ß'\à綦TžåGnù8„<ÖBÐðh|î6Ç! äGæüÈÑäö”¸r°ÿ*WñàÉO_5žaéÏGKÿ;+k¼~ü¨ïøÃ9Vò/ñ@~4>—A˜ö _ AÃ#ܦÎK<8?2ò#=?2úwõÁ\чôíþÓÓŸ…;Ì ·kP­TÚ¬~¿Z?œc%ÿªÞ^É!Mû—¬† áÑøÜ‘¥Á…ó£ ?Ÿ;:MŸgïƒÌUZËg>æêQþpJþ ¡9øÃñ ¤i?™Õ<à笃Øzî'6Êк üáø+r«ïŽö'ævxÂõë+ŸœæONù'§ÿSº’ùמ?ü üá+ù\oßßñŸx´ ‡?Gl[ï~gøïõîZàs›8„ëõUØã‘ ðÈg%ÿ¢Ÿ:äøœaºÞ¾‚†øÜÛQ<,ç‡E~€Ïɡ⑇ûr‚ÜÀ¸m¥=ùù®müöQåî/ð‡s¾)•gxØ–ð‡“A˜®ï®† à8çS£²K<8?lF4Àƒòc¶¿=WñàÉïԓ߯'¿kOÎë'çø“óýÙ‚yªóüá+ùõÝ`[~ÀNýÔ£ý%«!hx€ÏƒL×õöà8?œC4 rEÈqz¾z¶`Î$²hy1mªç? üá+ùWx8äøœ¡Õwí·Âùµ4<Àç!ñ8ËÇùáàsrEÈóõÝ*ˆa“ØÞ.«Qw僛b›àóÖñŸ“³º¨ï‡üŸó ¤Ùþ«Õ4<ÀçÑrë]Ïùá‘àsê"£úG®âÁƒ{È +òôEtͦµ=î|I¯ýçwbTõøÃ9röOõÝà‘àsÁÃ̬¯ÖBÐðŸG™:òÃs~xäøœº2껃ú`©âÎ}s»?·}27z²5pîšáÆ€§\ òüáù¤‹~êÐü üád¦ïƒ\ AÁþp.ÙQ<Ø ´ÓôþpŽº2îÜŸ—*x~q ?1•€Ä\Íн>Ò%Ì¿Àw•íÓzÑîc)ð‡stŽ<]ÔwChù8„2ÛŸ¸‚†øœó§‘z{œí4}?œ£.à1Zß-U< ¦±[ímõ·zòÕ=q<ïPO ëúüá#g¥òl¾ Èð9Âtûjàó,Sç‘ó#"?ÀçÔEPZ}wÔ·Tñ€Jè°ßä;Éè+µiÜðÏ/^ÚË'às:Gž.êí!"?Àç4yº¾»‚†ø<ËÔ9‚çGD~€Ï©‹ Ìúe”fÅ*yýq*2R‚G^οù.qJU¯…?œ£sä骾‹øÃñ äéúîjàsjÃ#q~$äøœÔgà1ÚŸXªxðí¥Íkv/Gi}³–jK”›Êçð‡stŽ<]ôStÀŽ!ÛÙýÇj ð‡sE¦Î<8?Úiú8Gês±“ûÒĦAºn7µKÏÞêj¤³åíÇ~þpžÎ‘ç«z":àǃ§ýÂWCÐðh|î7™:ðÈœí4}?œ'õ¹¸iþHíåzsëXäƒÃèé‘Ç´ãõ²‚GqtÀNaÚ/c5 <Ò0œù€G>à1ê×Wªx`¸±•®í“£óŽ*û,l^‡#܆æß_þpþpžÎ‘ç«ú.:à'ƒ0í—±‚†GãsO-y¨¾[8? ò£ñ¹'õ¹´úîh?ui—‡·ÐåG2[_¼'>> †Ôð(À#Veì tÀNaµ4< ð©s΂ü(Àƒò£ùeŒžç,U< UG>Òæ'–¥@ƒ* ×!Ôü€?œ§säùÂ/< þp2až?ÖBPð€?œ·v¸Q~Dèçð‡ó¤>—ÙþÝRÅjQà£DRNHO.õ¸¾8bL,Pëý»ð‡ó¤Ÿç‹ún„~8„8[o_ AÃ|NÒq©ïFÖÏ#ôsøÃyÒÏ+ìN6ˆ‡ˆ´äíÒ½Wxú‚äþA†QÏð9éçù¢¾¡ŸÃNaµ4<ÀçN¦Î®]¢3,¯úꉔ‚üá<éçù¢¾¡ŸÃNaÚŸz5øÃy/SçœÐÏáçI?£zí­ŠòBÁªº¬Ýkorе{\ÚŃvþ¼ÀΓ~ž/ê‰ú9üád¦û©WCÐðŸ™:ð`ý_ AÃ|NÒqñ§Ž¬ŸÇvš¾À޾¢ã1ºÞ½Uñ`;4[ÖÓvpòÊ'%Îþ¢þpžÎ‘³RyÒŸÑ8„2íO½‚†øœZòH}7:ò#‹Þ"š†GÜßÇ ýÔƒç£nU<ð¬kJxºµ›ªùèJ½àŽç_:¶o>ôù |N'áóE}7¢þp<ÅÌÏWk!hx€Ï© Ôw£÷Œò|)?ìt~ˆxxÉdŸ—¹oÍÞ‹™P.‘Ûžõ—¿ñ€?œ§“ð×x´ü€?B±³|¾‚‚üá<µ âÁùÑüô üá|²§“ð墾Ñ8„2ÝO½‚†øœZÊ6ÐÙO?6?ý8Ÿ(?Üä~ðÖ.Ï@ Ÿ–wtÒ+ñ ЄÏ³Êð‡ót¾\Ôw#:à'ƒ°€ÇZàól•]åûéLjüŸgÊÙû oU/”­¾ûG}þ‰Gi/×ß`ÄýÑpÀþgýáAꛂüá<ùé—«ú.:à'ƒ°€ÇZ ð‡óE¦Î<ØO?6?ý8_ÒQýüÖOع•âñô*ÙÔ_«§¯w‰ëõ+øÃòÓ¿Æ£åüádâ<k!hx4>›¸w±d?ý˜ ¢Éý+ãõö[ðô.ö§µ§ßJÿÏõ <| æ v#@?œ BóËö#[ AÃ#À! äGvŒGD4À#îxÌÖÛoU<§·zu²\%uT©Æº^&ôËŸþpN—«únF†'{„VoöÓ_ Aãñy +ÊP}7“__,ÈÆçÁP~äIýüVŃú¥ïG—÷ë¸,‰¿ÎÀ.ÐIøK< ò£ñ¹ Ât½}5 <Ľ‹ùªp~à4=üáè+ò6ëO}«âÁNƒÏÀñzÝöq|¼êŸtùRi÷&ZÚ«z”Ù`L“*O©)Nÿ¡=C™¯`­ñz@R8ˆ)d¹”Úzúކ‰Ý˜”áV —§ùVŸÈ}_*w£ýUÞôÞýQ@ê¶É•'‹Þ´ÄàìqªîkA¨˜€Ö­oŒvIÚ,cz<ÀdϳMîCîUD׊ަMÅ¡ôê½ý™ ~ä ˆÝ6Éò$OÒÖó̾Õª'g®Õ TL@ít;À &œ'¦ç ¸Ï{™iL ŒÖ¬>çª3‚Š Èeý‹æêdzž€Ý÷q zõ4&kA¨˜€Þ醀2R~O†óÄô<¿»LâðÜu¯b‚2ëZúðõcFP1Á»&]žbÒó ïü“a‡äÕ TL@ñ®4n»Ä„óÄö<Ç{>9Ùãp¯‚Âɬ«Î&0 ÷Ï0±È¸ÆÑ8PÝz’OVƒÐ0m\ðšßù²+YΓÖ$@ß‘ßaà“ºQL<ç‰ïyŽþ€Éhë^E†7û¤o¾¾hôB½uÔÚ_#¹›¤y6wyä œäd¦“WƒÐ0•\ˆ¹qÛ%&œ'¾ôx€ åI«ÍzeÝÛA‹ý9ïU\kz}i×Ñ<šèÀ®F–§p|j²æYžä Üäd¦o\ BÅŸdÀ$pžßãi˜$Ê“V­Ïß«Ø so1ÿî/~Å\½¸‰JÜù° {ÕßT1dzô‰IÏp|‡™½Óó^9h/åS¹Ç¬¿_‡7KbQ5ÅýQÀñÙT¹ù“žëàx‡<[ƒ\ BÅÏ= #eúÄ6©5Ðw4LòþŽ™YŸñ{Â"‘¸iÜëýex¹äx:¯ïO`,r¬˜œ­»bÏup<Ãt¥~5p|v“À˜ô<ÇgÊ“Yoë{D»Þù*p!›¼Žp­Ü1@v¯Ð0¹\(Mæ<Ë“ˆ<»œŒÃt]x5 ØË…ÚË0€ Û ¤äz< “²ç‰…ŸòàùÜ{•öøõø¸üOœCî¯ïÐ^¿1Ç—&už­»òs<¦Õ…‡iVƒP1Çs?ÃP]˜-RÊ=`’˜Œ×»Dz(?á¼{Kæã‹~ Ÿ3úZ&sqkrçYž¤ž'àx3í:¾„ŠIãø¸ÙÆmW˜°­@Ê=OÇÇòd¶Vÿ¨ÒCÏmæCbñ{½nŽ.Ò4Ÿl©×V`4¹%à “Üó¤q<ƒ™vBY BÅ$“4Œ çIîy’€I>`2ê-ð¨Ò­EîßÏ|7tÄׇ¿÷Œ0›‹ÜpUÎ=OŠ=Œƒ±ó˜¬¡bÒ8>ËA `Âö©ôëÙË}FÈ7“ÆñÑšŠÉÙZ¸ Oà:'ã0튲„ŠIãøhe½Æ$o”'ys=ž†‰¥<™õ½~´CÏ?ïT SÇûêävk‡2\}¬»`<m¬’çIžä­çIãx‡içëÕ TLÀñ6 c“ž'àx›˜ŒîãUz¸?ø!Ã÷“ßøF:™¨åg™ŸULÀñÎTÉó“ž'àx‡éÛ&WƒP1Ç;™G0a»lzž€ãåI˜æ‘Ⱥ[Šðû uûyõ¸ǽ™½¼øïbÃä÷þ$›ž'àx‡0«Ç¯¡bŽw2Ž`Âybzž€ãåÉl]øQ¥u*ŽÜæñáíö¬K~ ˜ÐEoª }–'y:‡éZýj&°¡‹ÞŽb¶ÙºOÃÄSž¤iŽé¡ðÓÒ¶êÎ=Íö_æ 5«+OËx Õ{Y÷GÇûX%Ï3LzOœèdÒ<&kA¨˜€ã½Ì£#˜pžØÜã&ù€ÉhmåQ¥‡‘I%¨V ÞÜýúз¥°ªžUÜL•×N1éyŽçq˜v'_ BÅdÀ„í²ëyŽ'-Û¶ºðèyÞG•ºgº÷H 8>ÄLzOéd¦o<\ BÅOMƒ˜pž¸ž'àxÒ²m« êñ*=yÑTLÀñÑTÉó“ž'àx‡2Ÿ'kA¨˜€ã©©ÀŒÔ…3ÛøgßóOZ¶›­ ?ªô@}ÐAÑpUö¾ ªG:týÆt1Æ*¯aÒ{ àL'ã0]«_ BÃÖt1Ê<:‚ ç‰Ï=`’˜Œöw=ªôpçµ"ù„¾(–o|%ÁÆNÒt(“Û o†~ÿ©ïaN“Áyw:»ÍsüZ*&àø$óè&l矃ëñ4LHËvf‘òƒ–óæ]Êc“:ĽSò¯òrɪ_ÅŸb•<Ïö'½§u<=Üãs×Z*&àxj*Ä„ó¤ëñ𨋤e“q>éªBñpÔÌ’qØÙë¦aŽ'=Þ\Ô…s×ãáRÇã`§]TVƒP1Çg™G¯¯…ɬÇç®Çç.’ïìdmåY¥‡cJg¾Òæþ¦0$_Ëçý5Ô0Ç“‰I×ãáTÇã`m½fxîZ BÅŸeÈÖãs×ãáUIwn²ù¬ÒÃWªç¨¤zé›ãÒ0Y]$=ž%ϳ¹«ëñp«“qp³5ÈÕ 4L`WI‡ÄõøÜõxøÕEÒãݬdz]fŽw*Õ$Õ!RSN<ÝÎtFÖEÒãíU]¸ëñp¬“qð³õ®Õ TLÀñEæÑL8OºϺHz|ÇdpÏølÒƒ©Ú-s­§ÊÑw35®õÃS~”Æñ‰ôx{Uîz<\ëd¦kõ«A¨˜4ŽO›Ì£×¶C™õøÜõxøÖ%Òã]˜ž»ügÚ,#_zmÆu‰ôøKLºç:ÜTùÝ£ö“µ TL0IGLÎò„õøÜõxx×%Òã;&ƒµúg•Òó£ÁCZÒ÷_¸Å¾×ªZ翊IãøDz¼½ª w=îu2¸q|îZ BŤq|"ÚÕ…YÏ]‡]"=ÞÍú™?«ôp{6#ãD“óë·>'Wyi˜ÀÀ.‘ÏòÚ‰ö›»;‡8Ûßµ„† ,ì’IÐϯhϬÇç’z<ùðýƒµ•g•_—F7Ï_>ñ%Ú¹“PßÚ§†§bÒ8>ÑQ}çlÝÕ{ àb'ã—•ï\ÿÕ/¼„ŠIãøDMv¨V_Èö¦l¶ÇÓ0!-ÛåÉ}ü³Jxƒ¼4§™ŸÈoª˜€ã騾½¨ —ÞS';‡éºðj*&àxj*°#uá²yƤç 8ž´ìLJkµ­gÿtÖÉnõR9´L·MðD íª—öþ(àx*êÚ‹ºpé=p³“qXÀd-p<5Ø‘ºpÙ8OLÏpÉ ß µ{–öGÇÓ1w{Ñ/\,rÎv<nº‡{5p¼Ä&Ö1&±ÇLâ“Ѿúg•úú½õÓŠ¹+•ZK¿µ–¢‹¿ú an—訾`²³êOLzž€ãiœ™_ ¯¡bŽÛ0&œ'®ç 8ž¼®w½0GbÅýÉ]$a4ÜÙ#ý»ßkŒbNýi¶ò 8žŽêÛ‹ºðžˆOãঽ=VƒP1LJÀA `â8O\Ïp<5&“ñ<éÁ¾úQþ‘cæ*&àx:ªï.z¸‹ëyާqpÓ®ç«A¨˜€ããÖ¸íÎßóO ~¶VÿªÒÃñ(ÿñ6Â?/Ý.?ï 7>w‰Žê_bÒ®6 ÿÐÇÁÍÖ…WƒP01ð¹KQæÑL<ç‰=`Byâ'9þU¥‡ãÙ2ÿà‹¤l»a¸Ô{ðöýƒU9ÞÀç.ÑQ}wQ«/ízúíqü¬ö»„Š 8>mØpžÓãi˜Pc‚Ó˜ˆô ‚Ͼdq¥Öµ_|Û­ù²gûyº\ÅOGõÝE]¸„ž'àx‡ioÕ TLÀñIæÑëZ} œ'¡ç 8ž€É¨çö«J8ëÿ}³>o7ØëýÂ>w)•*yžbÒóÏãÐ0‰v“µ TLÀñYæÑ< œ'±ç 8žü¬ïö«JåIè…7WtËïúOÕ…]EñzÕ[ìULÀñÙ`{ž€ãy¦oV\ BÅŸÃ(&‘ó$ö<ÇSc‚OÓ˜ˆôðL"í»"sflf>‰oQmoœ† |îR.Uò<© —ˆ<ÏŒCšÕOVƒÐ0Ï]*1‚ çI2=ž† 5&“Q?ÈW•P×>R¢82È1t쯡^[1ð¹KÅWyí,Oò>w2Ó~+«A¨˜€ã‹Ì£˜$Γ{<À„ò¤Õ…G{‰^UzHüêËêQZOÓbÙòä¿_þ9ö%Û?ÙW÷ú¹_Ÿ»TÊ&=OÀñ<y¶r5“ÆñyÛ†1á<É=OÇgjLð¸÷r°.üªÒµs<ëý–£©ë¤Ç¿ý•'ãóæ«äy6wåž'ãe¦o¾\ BÅ$“ÀA `ÂW”Üó$“xÀdœOêE!<ëÞù^#ë÷Ô×–›%oŸ÷é¨^õû£`Rªäy–'¹çI±ÇqXÀd-“ÆñÙÈw<ÞÌïOÖ‚P1iŸ©©`ʺ: Ô@!1;Øé|=»q|ÜÄÇ~ü!ã]µ`Ú,¢O¨ 4’Ïä;ï. Ã{=SËóHøéÊðj**`y+3é5*tm€ÀÒsE˜èW£Paå‡mÉ׳”O=`•ƒßU€å«Ø‘î ~¹Ýþ‘¹ÉÐWíåVP>Iôþ¢TLâ‚åóPLßÒ¸… (Ÿ´iï†&1'Ùâz¶€òI¥èìþ^ñÿ„E Tµ_¥öFÉõö{ú‡©nöÕeüuØÀý.“Jï/?HçC |Š8{b5 P~Ü çž·â‘ÜȰÀ˜ßÀ¿%´‚ñ¨Øõng:äy-î%ýk{¡ïËLkkL¯¤Â¼Lgø¯aA·ž EšíÆ[Bƒx™Ú üHÅ ,¡GXö-´šñ¨oú»Ê_KüzÊP[ŽXƽÙýgÏAùY…”s•CÏaAÊÃO†"ÏsËZ*, |ê8ð#EcróbX`Ïo`ƒ—IêeQ&h{%žèÜeàî¤wßBíXÿ:Ü¡_b`ƒ—é$¿¿(“º @ù<ÓµüÕ(TX@ùÔtàã·ɖг”Ojw…%ÿ· VŽßUœg/=üUpÇëþýôº: '¼œrƒå,[zÛ¬ðd(J=®=±@^‹B…”O}~¤¥˜Z/–س”O‚wlÅãQ3¼wÕ'ö•Œe;¦Ö—YÿgLæG¶€òé±ËZ*,ò î#]ßfc›:Ú#Êí[íã&û*ÞU©˜ºãM?—jàW¨ \x´ìAô”o”/C1]Ø_B…¥–4KXz¶À»±élyµS?û‹ÿ¬Ž‹äë¡¶í.°Ç+ÜQpÑúM§[ðÇ“¡˜.ì¯F¡Á¼BgüÃHï·ÙŠd Ü ò ¹–ñ}Ë»íåÁ³©#áR‹p«­;ï{=°ö£3 yÅ4¡ôØXäÉP,À²… ‹,©M¨×°H¶À,ÀÀ$¯H¦m'Ç,ìV•ŠýÁ}ä½/¿gR!sÊ¿Åx‘;­Œøé2Lò Ú ÎaéÙìq(fÝZ–£Pai”_¬L¨°˜³Ål=[広 ƹâ‹ÝªR‘^uJ–§Å‘Ež=¶Z“Iügþ (ŸÛ ® ûfëÙÒ(_†bÖd9 P>ŒÂ–ž- |º† ¶ ò  f·ºÿ2íæ—íÖwÆò§XÅo, í;ãȾå*, |ׄÒsXz¶€òy(f}£—£Paå;™PG`1’-¦g (Ÿn",ƒ»üý²„1¼ã‹ÚÆß¯öóýÀwYk°À-¯¸&”žÂ‚« ìòd(,e°&¶… ìòŠ“ u TiŒd .#00Ì+Ž+ês+1»µ;Hö'Mõ‘_ïúÃó]å¼Úðþ&Éo߈=ôš óŠoBéYMÌà6Ç<ŠVA΃”¿… (Ÿ\øÃPaßXÉÜG`à™Wè>‚OŠ1Œö»"·rv¿yû»¯+é_ÓçËÇw+^úÿÐ[à™W|JÏö-ÆölåóPÌ…,G¡ÂÊ'#þ0ÒNÝzKÏP¾ÏX[Â÷D©¨OšÚ÷öË-ä=ýÚñÎjP>·\´S/‚åóP”yÊ_‹B…”dBÅI¶¸ž- |º• mu<¾«fZžžæä•å ¨aE/¶# /Ú4«°€ò¹­à׳”OCgÛ—£Paå‡4‹d‹ëÙʧ‹ ̽‡÷-*x¡š½v†J}Ø gîV¬¯Ú«þ­ Ìó ÿWdƒ› Üóx(b« Ûïf·ß°¬E¡Á÷¼BŽü!Q¾—lÁÝþy…úÒdaÿ§´#ëýHþ~|éR I'?5X@ùd®*È—èñP–Ñ]þr*, ü(êH¶xÉ\O``¡W¨¯!Ù¹â‹ÝšRáþ=µ£[}Ϩ«÷õ/ßëq¶\è?yê¥JXèr€¥g (Ÿ†"ζ†/G¡ÂÊOºâ¹×ä¾>–lénpÑãoIRAŽ£§%÷ª±–˜Îq8¯c ÕùöX÷ ïÅöö¯Ü_DP~jBéY³« =åAù<®öÿäÂþr*, |²3ˆCd¼À’zD€…Êù~z%V• óÏs£áþCàs¸ôýþ9þÛJínåç&”žfKè)Êç¡@a|—¿… (Ÿ âPل°tOxéòø,ãÜR¯w/ÔÁ¾?ò}Ï÷ÈÏøÝ’Wpß°1¿‘#ŠÕ·“ðÒ+ÜVp .[00Ó“¡˜m _ŽBƒfz…L âPÙDÉ–n+;½’Yüj5±±cø”éâ*ÀÑ<¥—§^9_ˆChnö', |n+¸ª Ü·`à§'CÑ*È£ÞËQ¨°€òÉ×`É–î,G½BNÿ€eœ[D©xÈQªò¯ðK¶Çw™ (ŸÔÇË rêÙÊ硘µÛYŽB…”_"äÞ -ß$É–nS=þ–Ô ûƒ}bôÞ·~Dyj‰Ì½¹]÷+2×#Saå“AÂ,=åAù<±9L‡1‡éå(TX„òË>Œ[[~\f [$Ðn¥G”Û·À’G;_hqÒŽ |%xìñïyÿ™¾o©¾zô(¾ ¥§°t“„j¬×†bÖLd9 –XG1 »$ìÿÓ³¥Ê–4¹Ë7U©8™€ëÌp£ŒD¦¤°ÕZ¥T¡ô––-¶z뵡ȳ+±Õ(XlõÖÛŸ…üâX9K¶À)ÁVw=úÊ–I÷oZÁ‹ÜšëÁ©‘ÈÈ;EƒÅ5XÈ)!^öa•`«½^ŠYûïå(TX` mB½„¥H¶À,ÁVƒ=úÊ–VAþSNú ‹o+~yjd±GV-DJýÚ1«»|[ öèQJÕ¯O û¥gK°Ç¡(³+±Õ(TXBƒ…<Faál±[Ï–Ð`¡¾†yÄ¡ ²»»õlåS_`-웪Td¾KéS=ÞÆ7•íM%ËzŸ† (ŸìâUÙn=[@ù4+°¬E¡ÂÊw2¡Á"Ùbz¶€òÝ1[]Ýè®e®—ȈÇ|H}’Îl€*>ô†ôcGÖ€òÉ.áØ%Xʧ¡H¨ ·†þ†e- Êwa±K°°K°”O} ÙÌ;Ú¥"°›)\çýƒÏæ´ ä4âþ¯Ôåþë)k@ùd—¯*Èv Ö€òi(Òty5 P>yġ¾» »k@ùÔ×'­x¨!稚ëÙ‹­ŸD:ªMŠlO'=RjÙP>Ù%¤«Öpk{¶€òi(Ò¬Ïr*, |/ê,b—`mÏP>õ5d7½o¹µ’8þéqÜj÷ûÓ×n«,’*[Ê'»„Xz¶€òy(f[×£Paå‡mÉ׳”O} y¶°oªR±ÏÇ4×>ûº2³+ÊqN i/õËjñÅP>Ù%¤« ²u=[@ù<~òØÑr*, |òHC…}+v ÖõlåS_`<—oMsâ2µHQMŸ¡Ø z¯5X,(ŸìX(=Û·XØ%X Ê硘m _ŽBƒÅ‚ò£L¨C°H¶ÀiÀZP>õ5äVAtH²¦*ö¦Ÿû |÷µõ—1z}ßbAùtº>]ö-.6°”ÏCæ¹e- P~ {/*ÈÖK¶À.ÁZP>}KnäÁ«<÷€^­9A\N¿dU ¿“žMeÊýç{ë"Qaås[ÁUÙÂ.ÁZP>E« ›G+ÈkQ¨°€òÉ# U­Ø%XØ%X Ê'»À2x[­%ý_­áQíkh½ù;8P>Ù%¤« ² =åAù<³¶íËQ¨°€òÉ# U­Ø%ØÐ³”O} y¶5ܶ{Möw(¶L¸ý|ÏzE© (?å!Xz¶€òy(Òì$¶… (Ÿ<ÒPÙŠ]‚=[@ùÔ×ó$·Ø~2D–(·ÍÁ;¨ïL÷øŠQð¾cÞÂÇU,”Ov 骰oa—`(Ÿ‡bÖóe9 Ê'€*÷^q‹Ø%Øè{Dùð-€eT³U©¨Oúj? ²H‘Éþkß4oLœº-µu |n+¸2±ð|°”ÏCQ&O/G¡ÂÊ'Ó‡4r‡äJXr(¾%£°?Hù¶*2SËNîÄIŠïÚÇsüðGaßò‹‚¥ P>E™¥üÕ(TX@ùÜ1Kâö=‹ ë@ùô-e¶‚l«RÛ©çPíóïÞ#ŠVE½ÅÂ:P>µX¤ËÂ>Z,¬åSy›ÕòW£Paå‘{z­´XXt'XÊ'ëÀ2xî(rõâ±À*¥Øj‚ú¨îœä¡Ê§TXåRäÓ•M=åAùEÔ‰É]þj*,òÍ&rïå'nH²¹gK£|CNÅLOb±v²\i§ô€ü ,Þ–PõëÓlçƒõòy(ò¬çËr,Þ–8 K–léÝ ÞÊ;Yª´U©ø èµUŸîý*ù7ë€zç‹õò )ò¬_Ÿ_àù`½³‡¡Èv~%¶… K£|ÃÍc…ý,ÙÒ»|£|CNe¶°o«R!7ÅSÃÕW–Oí2YJçÕþù/X` U‘;Í–Ò³¥Q>EžÖ[V£Pa €%6ž»„¥H¶ôî e‹›¬ Û~e| (÷€ö•¦“~7WÿÖ²{ª~ï½õò )ò°ôlIö8n~[‹B…¥Q¾©Í#”_8[\ïNðò 9 \÷9(ÛªT¼|ê,>Žjó´Ýê8_U£_-i=(Ÿù|UØw[Ï–Fù2~V4^B…”oEîÈ·y¥g (ŸœʤÇþ(Çr_ù·¿gOÓÜ·¶¨ôðZöMÕ÷-”OŠ|¾ª ;x>ØÊ·ñËD¶¬E¡Á@ùN&Ô!X$[zwBå;>å3=‰=Z5‰öʇS¸Äî“;µq P>)ò×°ÀóÁP¾ub²T¹… (ßu¹÷â4˜3’-½Å"€òé[Jœ¦ügkõ\lÝw[À=ž–þ(ï½ßýg_tX@ùÔbÁ­Ä\o± |ŠVØÿónýÞN®E¡Âʧ¾‚ßø{c¡¨_^γò»IWûÄ|H…”O-ùª°ïz‹EåÓP”éÖðÕ(TX@ùÔW‡:ö´X¸ÞbAùi;À2Î-¾•Ä<Èfx‹ìø´Aà;5è=“éëÆ¾¶, |j±ÈWd×[,"(Ÿ†¢Ìš¹/G¡ÂÊO]î½Áœ´Xì{øQîßR¶VØí|qU©H25ñL^µýÝ’ó!¯Myí4X(Ÿœ`AÊ'P> E1³2Øj, ”Ÿrã¹Ël Q`)="ÀR°ŒsKlž¨ûÿox×ì~·{‚öåþÇ ÷ª/œ (ŸœòUǾë}" ”OCQ¦+È«Q¨°€ò©Q$ö]4 Ko±H üLÙ3÷Á†$×.——¼6í…ãþÏ&P[ù¬e˜ή6òsJÏaéÙʧ¡,Æ»«Q¨°€òs†Ü{5‰I‹…ë- ”ŸÙÐ}–ÜÞ3i?ظ ´=>œ_±^GKœúøÅ- ”O-°ô”åóP4+ž-/×¢Paåš#®`‘ ×[,(ÿ–²¡5|x+íxÛ–{Kbl%×'fLÉ"#…Ùʧ‹rYØï- ”_D˜ì|YB…”O}U…¿„EJ•p±° ”¿K‡e´TéªRÒGœ|é‡]ÿÜüºÌfP>¹X À‚âKåóPøùíäZ,¹Q¾¥F‘2VØ  ›åÛÍì°´Âþhûž«J…ë )Ÿœþ/bGùä)6áOZÛì AYÛ¨°4Ê·äbQ. û½O$7Ê—¡˜.ì¯F¡ÂâK8Â’Î`  ›`¡l‰ÕæmôH««JåøóçþK®¨‘µá K,¥ê×ç°ôl ö8aV\B…¥Q¾¥F‘2VØ ×],r£|k `/컪Tì4éÚrÒÜ?—“r…ƒœ½·#<,ò-¹X\ÃÒûDr£|ŠÙËs—£PaI€%´ õq±pÝÅ"'ÀBÙ’¦a¥b{0â¾!N| *½HO’y€î zSÝI¿¿ÅæXJÕ¯ÏaéÙRìq(Ò|y- –Fù–EFaálñÝÅ"7Ê·–²%O/E©Àn˜Ž….<©Û±Ü ¹iK Ÿ4.ò-¹X”«Â¾ï}"¥Q¾ Å´çËj,”oeBè|ñâbịEåÛx€et%æ«RaÚú‘®–çâdù³A 'g' (Ÿ\,X(=kvõ½O¤€òmXe5 P¾“ u [¼¸XøîbQ@ù޲fîƒ]•¾*þV ÷bÇÎJKb éÙëÅ—Ê'‹reÅã{ŸHå;Q'¦aY‹B…”O"ƒ°ˆ‹…ï-”ïöl1[]‰–*}U*üVƒxñ!ƒÄ÷ÐXß_¾7WþöO^åWñ¥€ò©Å¢\ö}o±( |'êÄ4,kQ¨°€ò©¯  ö½´XøÞb÷=ë `ßåûæÄ…bÑÖÛw°ôÿj­~ÝtX@ùÔbQ®*Ⱦ·XÀ}†b –µ(TX@ù^&ÔX¤ÅÂ÷ ¸ïYOÙb&)ß7¥"üŽéÉEÐD½×lqpß³Ôb1KË÷= *ƒOî[V£P`qpß³A&Ô!X$[Ðbáà¾ge *ÈÓ˜(ØþþTôx»lwލ]•î{–Z,ÊUaߣÅÂÁ}†‚ÊàÓÙ²… (Ÿ›#Æ`‘ î÷=â–ñ•˜(²ëýØŽI=cÂv ÛP~(U(=‡¥g (?„,Ãû–Õ(TX@ùQ&Ô!X$[|ÏP~¤lqÕ¸jÔÑW¥»­Wê»­¾ øSÑPaåG?‹ïÙÊ硘îØ_B…”eBE\,¼ïÙÊ”-­‚Åtkøj*, ü†a‰’-±g (?S¶à–ÖA½Å·“!bhÃM‡Òq¸®Ú:Úû½°‡V‹/î{6—*”žÃÒ³”ÏC‘f[,V£Paå—£)UFÉ–Ô³”_Ì–Ñ)|»ù„7\õ&æw­óÝù@Èq –ù IÖ÷-pß³ÅW¡ô–Ô³”ÏC‘æ³e- P~ {/´|Ÿ$[Rèå÷˜ù rU*øa#D9Ík÷{ó6ûKéëÆ@ƒî{¶ä!Xòpß“¡È³-«Qh°À}Ï–2KXJ(¾Å”iXD©Ø[£½^̶NØÏî;2ÝæÍÁ}Ïm¶ ¥§°ä A€òy(¦­xV£Pai”¿ïÔ9ŠnÉܾç³ïåö-þËè94¥Â¾Rñ|_ Ù>bUa €%U¡ôtœ‘òpß“¡(³ äÕ(TX`ÉÅ,Q`éÙËþ¢Ùm²&ªRA¦N/jÁ+µ/-ë峆îüXÛü€¥Q¾3¶Ár–-¥gK²‡¡0Óž/«Q¨°4Êß¹Pá¯&±"Ù÷=þ;ëùÚÉ[]BîϾ¯d© ylælÿ74–XšPzKOùFù„1³“Øj*,°¤Æs—ÙR‚À’{D€%`Ý·„vÓüóßëQWŽùIë™ãº¥N®z õ¤±ƒûžC[Á9,Hy¸ïñP3[|YBƒî{ÎÚÆsW ä°q³k€„ƒûÞÎ2;,­‚EëØ÷fpß²… (ßÅÆs—Ù".Ý î{ûO;,ðØì| U©ã)\"t¸¶ùÑ}õö=÷=Gм¹*ìÓS”ïD˜^ ¯E¡ÂÊçæˆAXØó%Øž- |o° i U©øhF|7c—ÈæC\~­m=ì)LõY¸ï9RäÍUkx°È¸ïÉPL›¹¯F¡Á÷=ç»Ü{5‰YÉ´X8¸ïñ·Ø0½@†WíÚ‰ý¢€‡áê÷¡ÏŠ:Äü/uî{Η!Xòpß“¡ó äµ(TX@ùÔW`ÜÐJLZ,Bo±€ûž£ ;kÅÚÉ& õ®Dö|¸sÝ[#azÏt»÷=|Õ¯Ï.f½Åî{2Ó…ýÕ(TX@ùÔWXÎ'1i±½Åî{ŽZ,ììÕ¡*5Þjíü|uš¼=ûÏÏæÏ¡Âʧ sUؽÅî{2ÓäÕ(TX@ùÔW`†ZôX„Þb÷=G-6µó-ÃÙ'®ív¸²ÉQ[<{µ¬^Á“(ŸZ,®aé-pß“¡˜öØ_B…”O}fÈŠ'H‹Eè-pßsÔbag;öCU* ››SgÂÖ3ýßWÕ Ü÷µX˜«Â~è-pß“¡˜¾5Š˜!½%ÈE!î{Žº\Ó[Fe°X•Šã.l"Ýz“Õƒ ¶÷Ï¥x!*, |rn0WäÐûDà¾ÇCa§ÍÜW£PaåS£ˆ*ì‡È¢qH=[@ùÔàf=_bU*þú Âps{ÓêÿÎ…¦W»ãAƒî{®¸*”žÂÒûDà¾ÇC±ËZ,pßsEäÞX’dKo±€ûž£î7ëùûÉzR¾x†&æ­5ñðŽ9µ)–õ*, |j±0Wû¡·XÀ}‡ÂNwì¯F¡ÂÊ?4G\qKªÙRzDùð-€% ¶†Çvó‰v3À™e½K£|O-ö²‚Ü[,à¾ÇAØVA&ui[Ö¢Pai”﩯ÀŽö¥Å"àŽ ÷=þ7{ynl'C´›N,ëUX`‰C°@ƒûž Å,kQ¨°$À’xB™Ä²È`¸cÃÁ}ÏSw‚käQ,¶›O´›Î,ë5Xå{rn°—…ýÞ'÷=ŠéÂþj*,ò=5Š ÂRDë-pßóÔàfoií¦yíf€3Ëz¸ïyj±°Wû¡·XÀ}O†bú Åj,pßóÔW`íPYZ,Bo±€ûž§ À2ºŒU©øº@¿$(ôÉ*,ò=µX0,§Z~o±€ûž E˜_‰­E¡ÂÒ(ß[Û&Ô+X¢´XÄÞb÷=O-.NÃ"J…rIêHüží¯½-wù)ƒÁ}ÏS‹…½êؽÅî{2Ó·´®F¡Âʧ¾Àr:‰Ei±ˆ½Åî{žZ,:,ƒ/±)Õ‘¾¼ê ð=2)INä ÿÊÇÁ}ÏS‹…½ª ÇÞb÷=Š4Ï-kQ¨°€òL¨#°H‹EìÝ pßó޲%MS¾(ε^;ÞæšUнÕÛNúª°€ò]JOa1=[@ù<Ó…ýÕ(TX@ùN&Ô!X$[zwÜ÷¼£lAa°ó%V¥‚,´[²KdtkàΗñ»¼'¿ Á÷=OŠü,ȸïÉPLöW£Ð`ûžçæˆ!ýh%[z‹Ü÷ø[\™^‰=›±ó`yObRaåS‹…½ò|‰½Åî{2e¾T¹… (ß‹Ü;’-Òb{wÜ÷¼OXÆ·“¸iå½Ê‹|Ôàn?‰óÄžÚÁ}Ï“"o¯*ÈÑ"åá¾'C1­·¬F¡ÂÊ"÷Á I±w'À}χc¶ŒŠÆ±*Õ6ðõm(§ ÜÞ'z Ü÷<)ò×°¸ž- | 7ݱ¿… (Ÿ›#Æ`q’-½;î{>ì`øÙÂ~úT* Õ,ª‹`bÃ6¬HoÚ4'¾=ñÝ*, |RäY(=Ä\ÏP> …›® ¯F¡ÂÊ"÷Žì[œdKïN€ûž{¶øYýÔnš¿ÑkŸ4\¨˜ßÇÐJ³ò«Ë,pßó¤ÈÛ«Â~ôȸïñP–áòj,pßó16ž»Ì/ÙÒ»à¾ç#e‹ÜN¦ªT<^‡C‡î÷wª{åM4$öÒ wÜ÷|Ú†`A¶À}‡ÂMöW£PaåssÄPa?zÉ–Þb÷=þïjCÒh‹EªJ…t¹»Ÿ ÃæQï{ÕõæÏ«¦Ü÷<µX¸« rì-pßã œ›íª\B…”ŸDîÉi±ˆ½Åî{žZ,|+ìê-©*_¢Þw4_i?`åS‹…»ª ÇÞb÷= 7{¾e5 P~¹wn±ˆ½;î{ž\,ülkxj7Ÿxêù¾HÓ~ßpNçFÞ4…«°€òI‘gXÎJ•±»XÀ}O†bº‚¼… (Ÿ›#Æ`‹Ø»à¾çÉŇé•Xl‡Aƒ¥f7z½˜)iGæÿ= ·øp…|ÿÛ;E•[<Ü÷|nBééJ .î{2Ófî«Q(°x¸ïyn޲â‰âbÑàá¾çÉÅÂÏz짪THËÁƒ9B¬ÕrPÄܸKA4ØíŸ·ô‰ (ŸywYA†‹…‡ûž Åta5 P~ ç.a‹ˆî÷=O7RøVA½²-5¥¢5†lûÖ,þK™ÌÐeÅ%ìÓ7éÕüà÷=Ïm×°ôlåóPÄùIl- –Fùa“ uɖܳ¥Q~ )<*ȃ/©*[ ‰ùÅ&>²ñ+iùV’T½ÅÃ}/sƒ»,ìçž-òe(Zù…àoXÖ¢PaI€% Ã"7RÄܳ%–x€e´T™ªRñ÷‚&(IR —3ooý¦ª“î{œÜe9÷l)ö8Ó)V£Pai”ŒL¨C°H¶”ž-òõ5øY+žT• k»ó¼4¸m¾:¡ÓíòíóÄŸèήî{ÁøªÈ®Äp#…‡ûž Ež‡e- ¸ï#ê,r#EÄî{úü¬çKj'CÄ…þöñø}g|tªÿu#…‡û^0¥Ârš-¸‘ÂÃ}‡Âs©Ò–™}ËZ*,ò÷=Æ8,œ- 7Rx¸ïêkÛô¾åÙÖ0æÅë™lèÈ åÙš2Çê¹×çG¶4ÊÖW¡ô –´õli”ÏC᧯]B…”oeB€%ÉiëÙʧ¾À2j¸›ÚMóÚþëÕJï/\ØZÃâíGûž‡û^°¥ ¥gû–´õlåÛ°ËZ*, |'ê,’-¦g (Ÿú‚™¦üw6‘ŽXy ,\PɶÙ^hçÈYDU'=Ü÷‚ó –³l1=[@ù4ÞÌ_Ö¢Paå“éà ,r#E2=[@ùÔ×Ð>¨åçæÄÅîõ‡Èé¿ï¹n< ˜L»4I¿[gœ Ü÷‚+U‘;£üÏ÷= og)5 ¸ï2}pC…ý$7R$ÜHáᾨ¯°ŒZˆæªTl÷jf¥ó›»zÓ»ÝDçhSvì¹Raå{_¹ÓlAC‚‡û…Ÿ.ì¯F¡ÂÊ÷2¡ŽÀ"7R$Ø%x¸ï’Áƒ›ÎÛŽN;ÓúÚDzõ• ‡åø‡Ÿ¢±‡û^ »†å4[lÏP>Å´Ïj*, ü ê,’-®g (Ÿdðà§a¥BtªQø¢WkJ‰?±¯öᯛô<Ü÷Ù%ø«Â~r=[@ù<~¶T¹… (Ÿ:Ë©hœÄ.!¹ž- |’Áì™{®JÅë¡vûªå£Çß<îy6÷s[Î`å“]‚¿2I®g (Ÿ‡bº°¿… (?Ê„:’-b—|ÏP>ÉàaÖó%·“!,«šÄ¥$Y÷ßye/qÜzçýšÎ-pß d—ÀŠÜé$††÷=Šé òj,pß Q&ÔXÄ.!Á.ÁÃ}/ f=_rU*zoz™¾ã¸×8R‹I…”Ov Ëé¾ î{2Ó×®F¡ÂÊ§Ž„QX$[ å{¸ï’ÁÃl©2W¥"Øz_¶ìïé"&P>iùþª‚œBÏP>Åtkøj*, ü$ê,¢å§Ð³”OZ>`Õ[rU*hü“u§1i°€òIË÷Wäz¶€òy(¦[ÃW£Paåg™PG(_´ü{¶€òIË­‚l‡)¿f寘:­ïŸ1©Î®î{´ükXbÏP>EžÏ–µ(TX@ù$`û¡Â~-?Åž- |Òòì‹E®J¦ÛàzLû¾ìñÖcÒ`û^ -ß_öS×òá¾'CQæÈkQh°À}/€í‡*ÈI´üÔµ|¸ïÒòìÇ~®J…)zLÔ5ꔘTX@ù¤åûË r×òá¾'C1ݾ… (¿È„:‹hù©kùpß ¤åGô VsU*$©õ·-þ{=¿O»©°€òIË€¥g (Ÿ†"L_º… K£ü¸É„:‹dK×òá¾IËfRoÉU©ø®Y°m ™ á¨Û“¯x²ûck”IË÷—ä®åÃ}‡"L»†¯F¡Â’K†E´üÔµ|¸ïEÒòãì-­ùãdÈ‹o¡}¿ji_`Ò‡/’/|e D –XJJÏaéÙRìa(´køj*,ò£‘ ud%&Z~êZ>Ü÷"iùÑNï[ªRñü>Àfù=£—ÌSsO/aülH‚û^$-?\V»–÷=аPA^‹Bƒî{ÑÈ„:’-¢å§®åÃ}/’–QAlß+U©8†"Gs¥]áã¼äAæSaq€¥ Á‚lûž Åty5 –Fù‘ìQX8[r×òá¾I˳ž/¥*ûª®ïÚ^µÿ׉ÛwÌ£º {öw Éê”÷½HZ~¸ª ç®åÃ}O†bºT¹… (Ÿì0TA΢åç®åÃ}/’–[yô^©JÅ qzjÇ$º?òþøïE‘éêû«‚ ÷½HZ~¸ª ç®åÃ}O†bº°¿… (ßÉ„:-Y´üܵ|¸ïEÒòãlkx©J…ñÔ|LÊñI6Ã'Ù¶ÚŸOG¬r½N(ª‡ô<Ü÷"iùáª5 Ø£°H¶t-î{‘´ü4[ª,í¦ùÏ'·¢ (Ÿ´üpUAÎ]ˇûEœîA^B…”Ovª gÑòs×òá¾IËO³æ"¥*Äýö‘ï‘7ÍÞœS>Ü÷"iù᪂œ»–÷= À21‰­E¡ÂÊÏ2¡Á"ÙÒµ|¸ïEÒòÓlkx©JÅ1Ó·ÃKæ­>¨°€òIË¿†¥kùpß㡈ӭá«Q¨°€òs†E´üܵ|¸ïEÒòÓ¬‹Ei7Í_2÷ù’±‘Š÷‘i°À}/’–¯*ȹkùpß㡈n>[Ö¢Ð`û^$;U³hù¹kùpß‹¤å–Q-¿T¥¢“âñ% úKfÀÊ'-?^V»–÷= 7Ÿ-kQ¨°€ò‹L¨#°ˆ–Ÿ»–÷½HZ~ò“5±[U*èñceÊ£mà‹ïo–úÅþÂÝîl_¯ï[à¾IËXÎö-]ˇûž Åta5 –FùiÛ +^8$eÑò3n ðpßãoIp±\‰Ý'Cîÿn/š’I¬àclÆ×cot9°t¹?«p¡ÂÒ(?‘S>tvÁaî pß“¡À½“£^•«Q¨°$Àâ°ÔS?`ÉV`ñ=¢|øÀ2zìèÖn>ÙhÝBGBßt·yïUH¬T¤ñ£E¬ÁRKªŠÜé$–‚(ö8 –m¼ú`5 –X2tÅ‹)rŽKîå÷¤8Yª¼U¥B^¬»Ø¢FÊtzŸ"Ÿ1äƒ"^L÷|•Ã5XྗŒ‚¥îz¸ïÉPÄY?±Õ(4XྗÈï?Žöåꃌ«<Ü÷]}f¯½U¥"³]ì€íýß›¯Ø ²ß­ÏЉ_»zâJƒÅ–&”žÂ‚«<Ü÷d(Zaߎ®ÄV£Paq€%qC°%÷ˆ eKš\ ßÚÉIvW©äõ5eýy UXå'Û„ÒSÊ/=[‚=ÅtÇþj*, |òûCöÔeãl)½;î{ü- ÷N¶XܪR9µ%Ù3ûÕ wGöˆ³¨²ï_”÷½DŠ|¼*ì—­§<(Ÿ‡"ÏSþZ*, |nŽÒ[Êæ–Ô#,û‹–f]ÃoU© ‡mÏ^]ôC=û<çJþ¹ƒû^"E>^™‹”­§<(ߊ:1-kQ¨°€ò]—{/(¿ll!Zz‹Ü÷ø[ËhMìV• yX!E*ìEjÉïv¤×˜.óŸåLJî{‰Z,®aA‹E€ûž E3s71ŽfËZ ,î{‰ú âÞR¤Å¢ Å"À}/Q‹EÞ&wù·vóÉ­¹¢øÚÚVmkÙE¥ÚÖÞ{kµ (ŸZ,X¿>ã–‚‹÷=Š4ݾ… (Ÿú âPa¿H‹EA‹E€û^¢‹Ü*ÈÛ0·Ü*øWku5·Ù õÝï– jUX@ùÔb¯ ûÅölåÓP–á>±Õ(TX@ùÔW‡\Ë´XÛ³”O-€e´‚|«J…œ60ÏÏr ÉÛ÷Ƀ± (ŸZ,`éÙʧ¡HÓ®á«Q¨°€ò©¯ U‹´X׳”O-¹öËð.ÿÑ’E%^¬÷Ÿw‹,}õ«WaåS‹E¼º´¸ž- |Š4ݾ… (Ÿú âPÇ~‘‹âz¶€ò©Å"ÏVoU©¸e>?œy‰(Êýÿéñª>÷—¬4Oó‹òá¾—¨Å"]ö Z,Ü÷x(Ò´=õj,pßKQ&Ô!X$[Ðbྗ¨Å°Œ¶XÜ'Cž|‹ƒ”ñòw÷»¼t¿é¯âK€û^¢‹teO]Ðbà¾'C±ËZ*, ü(jÄÒÙ$&--î{‰Z,²ŸÞN¾ÛŠÿ!'r-‹«ÛÖê›´wó¢k~Àʧ‹t¥·ß³”ÏCÑ û‚ø ËZ*, |ê+HCûEZ,JèÙʧ‹<{è½)þãšlò¼/+|ªM¨tßúÝ}/À}/Q‹EºêØ/¡g (?‰:1É-«Q¨°€ò©¯ µ†i±(¡g (ŸZ,Ëèvò^• Ú?ØzKîÎæ{œdÿ•X‰M¼ K¥º×©°€ò©Å"]UKèÙÊ硘6s_B…”ŸeBÉi±(±g (ŸZ,r« 6$Ý«R‘l_ŸPåâùq+®«y¤z] Ü÷µX¤++ž‚‹÷=Š8+¯F¡Á÷½”Tø«â‹´X”zDùð-¹UG›]ïU©¨·™w6á¨;ã­}˜Ú<í© XôI î{)çª_Ÿf úDÜ÷d(R-¾Ô‰ãºø²… (ŸEÒPa¿Äݤ­GÔ`¡îÀ2zÛѽ)†žwj9 ²¯aè4H©ôùˆ|…Ÿ¯‰­E¡Âʧ¾‚¤—ÌS…/³òý’À‹ÌÒ¨2û#[@ùÔbÁúõ),½Åî{2Ó®á«Q¨°€ò©¯ Û}ËŠd‹ =¢|øÀ2Zª|´›OxJ–"žŒyqÙoß×&ESeØcºë-pßË>ÁÒS”ÏCfÈ«Q¨°€ò©Q$Û¡l±‰aq[¨ÁBÝ fîƒ-v2$R»î‹¸Lüˆì-íoïVÿ3쨢À÷½\Õ¯Oaé}"pß“¡ˆ³Ü²… Ü÷25Šä½eÅ ,¡GX([Òä.ÿQ•Нš+›ýgx_5>¹’îâ¦g Ü÷rÈU¿>‡Ù÷=Ši+žÕ(TX@ùÔ(RUø«IÌÕl)=¢|ø– K^ ?ªRñ!¼[o^l}¤VMjý(UÂ}/G;‹GÊÃ}O†bZoYB…”G1’-Þ0,Þ÷ˆ,ÔnRf’M©Ø<÷Õ¢TŸ‡3X@ù1UýzߎÀÒS”ÏC‘gµüÕ(TX@ù17ž»Ü·x.Uöp€ ¥JžÜâ?ªLAO};"Ùó ”ÙG«!¨€€ì“ƒþ~¾¿§X¹@óôß—2Yž|Tu‚ùqxä|åyø¼œ ǽÌh€L`·'#Pæg­µ44`·—SDƒ˜¤ôX€F¦ãžÓìQâ!^A_ÎæŸndì|ûÕ@ £½œ›T}V•‰Àf‡ l³U–ÕT8ÀíÜœ2TÁgµËõh ÙOÜ‚ù{;ö¾½Jžîl ÇóÏ[NU@ÀëÔÞr 8{<+€¬Å V¯m)¤ŽìÑ´ÿ·³uûǧÑÖ‹›êÏþðóO‰^Ùš }rz¨R…ž ´ûÔj * €ä6}^BÒ¨À>¯Êœ„ô—~T©¡´Uc‹žÊ òô”ìó ·k\œ„èÇS`ž'ƒàç3d-Fê…\=†áÃ)ýh ŒóŠ¡ ™=ñ¨"Ýs͵Ñýýy¾þ¬±Žôu £RHªÒó =C©Ë L %«1¨€’Ÿ]´¬ð±”~(–yÅn@F§¬g•è¼f;eó`ú«>²ò¦½ú6÷íõEÀ2¯p‹ÆÅé‡~$†y2 ?+þd5 æë œŸ¯²ø@J=âÉmò¼ö³ rþ_÷Å;*ߦÚôâ>Â÷¿§¨»Žn5Q©ssÆ HrXåÉ ÄÙeïj * uòð(#vF*'qëÑ@òðuªÏ*)È{eÛ9Ácµ?Šÿ$ôIþÙ* un˸8ñÐÏÒÀ$O!ͪU«1¨€€Ô安¤Uõ#(0È+Ž2©îÔŸULpåcÕHñ¼Iÿ$Y:ñQAóÙ­R熌 £~öx2ӷۮƠRw‰c„2¤>5^qåÈè*ëYe„¾l͸àJÜ0÷Ÿ¦* u´bœdH?zc<„i)d5º·ƒ€ðÁ“~b¦xÅS†”i@ê@X¸º±öà¿øø¨Öi€À¯ÏE¹¨½Ãœ4ÀO¡ÌOYk1h€À¯øÔøì Ê~VvxÅïbZµwôîçgUöÝ\b)–2r™çãæû˜î?S ì“ýóú¡;¼Bå¢Ú GÏ3<Üe;zÊj * õ`¡¾ž¯²ØÏ³5^ îÈhqñYµ„ã-cT}·ë¬øpÀëýÏ¿êMqÒ8¨RGãÅ HrØàqÛtù}5:ùs”‘ò;3éç3`·ÿðÙjÍÕ²žUK8žÇñ~ô7£¥L±¥! hU@@êdQ.Êï¥'9H=ˆ‚0y|q5:9s”‘Û T¥gH=º ã"Z“úÉàž·Ì¾1°ò³±mV©“KÉ'-À¥gH=Ú õ¬ÕT@@êäÉQFì‰ eHéR”!³w<«–`¶ÃÙñ¿÷ðI›†o—]©n„í]!ˆruçÀÖR$Âõ®ÄÄëéeÖZ "®w{Þ¶ùóJ2ÜXÄÝ|§a’(IZÁwôØõ³Ê þA~;Þ"¥þ‡+†¯ëÅxX8ª˜€×S¬‚ò)&1€Øy¦o¬] BÅÄžÒ0&Ü´•0¡ëREÌJôÛ" ïJnæ&¦ç ¸ÇaúèÂj*&àö,³è&Òì`zž€Ý³;`2Zö}VQaŸj©OVºÊïÔË”·v3Ómo_®òno™† Ø=ÇLzž€ÞyülsÖj*& ÷,óè&œ'¦ç >Sž„i‚¯×Œð>6sùT:ÉoÍŒÔJ­(ñâÅM¬î{auWX꿲#²=OÀð<ÓÕ®¡b†/vËyb{ž€ã‹;`2>w½[Ú›Öдq%{ŸiSejDâTR#Òó>wû†·j™§˜ O`s'ãç—ÁkAh˜Àæn_~q#˜pžØÒã&”'qrîzUuA²šœ•ÝR]þýïih™òdaš^=±ðù s< 5¦Ê™g˜àÌE„njôÑj*&Ìñ„Ì£˜8ižó=žÜ¾ƒòdÖ…èU†ò¬O+þÉõ=’7îUûž ó¤¨b€Il˜œØtºž'Ìñ‡4ÉZ*&˜¤aL8O\Ï“L(OP–tïxUAy‰žÚ#+¡E}Ý%övô(ÆTQó´£±çI²Çq˜.¯¡b’&Ær˜ð9ã{ž¤†‰qLF9þÕîq7Uß‘‡§„¯%’`Bæh¶©÷=Dñ¶ãG‰UF;ŤçI±Çq˜¾0x5“LdÁ„óÄ÷<)À„ò¤çGÏ¿½ªÒ@eÒ7•±ñ~‘hyÃÆü{~>ä.ñøCqblGbÍ&y"¾v‡éÛ‚WƒÐ0_;BæÑ‘Ö_Γà{< ëHÙΓã îõÀXþX»D_ÏĆWÿ\Åo#‡s òDLíÚ8˜iôÕ TLÀñ609õìœ'¡ôx€I>`2Z~u{+yìz#¹C©Öôa»…'Ü~æ 8Þ™*pža{ž€ãi:&£û“Õ TLÀñNæÑ<‰œ'±ç 8ÞQž˜vqpòª’ƒ¬½ç ˆókbE›Ò>Q£.\¢Ž 8ÞÅLzž€ãiÌ´iýj*&àx'óè&œ'±ç 8ÞQžÌv¿ªê°/Y±.Y¨ qÿ³†4, á×4LÀñÞTÝù “ÔóOã`ìl½k5p¼·£˜$ΓÔóïÝ&"<‰17 ΦZÔÞ¿FêŽÔíqÿ—Õ6ÇèÀñ>V¥ól‚&‚èÀñ4ÆÍóÉZ&ïeÁ„ó¦Ñã=åɬ§Í«Iré¡eztÝYÚ;w’è_ S¥*ùFަjk§WÒ!O8žÇÁÍÖ WƒP1Ç™GGn¤ã< 8ž4l;[~µûCľâqÈüv顼qôs¦´ß'ê × 8>ÄLzž€ãyü¬Ê¸„Š 8ž$èALäJÍž'àxRâÉøþÒÃë]›<öW)ñùþÝänfC·TÐ -ÃC Ñ㣩’çé(=OÀñ<Ó.õ«A¨˜€ãI…6c.õbÚóOb¼m›n1tŽŒÄS£ìyE¶¨Oh˜€ãc¬’ç¹G=bÇó8L÷ͯ¡bŽ2™n‹Óc˜PžÌ^qúªÒ»Ö`©²‡RëwmUÁă㓹ÆÄv=Þƒãyâ|ž¬¡aâÁñIæÑˆêËÑãi˜oÓôÜUfªv_ÇÂ¥Èj?ëG*&àxÒãÍE­Þv=ÞƒãyÒl£Ýj*&àxÒ¡1‰ŒIéñ“|ÄdÐzà]¥é÷¨Mƒ"b·¾4ËZêQ·KzߊÇ“o.ê¶ëñÏã0ݯ½„Š 8>Ë<:€ ëñ¶ëñOz¼õ¥Wéa_b×vÿ@¯òB½#½Pd¶û5?«-ôуãIgyí„ãm×ã=8žÇaºV¿„Š 8>Ë<:‚ çI×ã=8žôxÛê£WϽÛU!…Z8{?Ý룶ºÿ¼ÿyü‰Oz¼¹²¯éz¼Çó8”yLÖ‚P1Ç“=† ëñ¶ëñOz¼½jöÝ|« …³ÿéx-ÿ,mBÞùóYí)©Zt¯=é&Oz¼¹¨ Û®Çp<ƒ¾Òt5 “Ž/2Ž`ÂyÒõøŽ'=Þm“ÿn¦U\3º¹zÎϼk· {ò"óyptü8CãxCz<ËkgsW×ã8žÆ˜ ¯…WƒP1io6™G0a=Þv=>4Ž7¤Ç»ÙÛLßUz¸ÇêHM,½›†~ ïõ“Ê+I¨?0 À$6Lμ:zž4Žçq°ÓuáÕ TL0IØpžt=>`Byb'Ï-¾«ô 8[_±ý…òžö`ò¢‘œêz°¿ò¤q¼!=Þ\Ùut=>${;Ý/¼„ŠIãxC:´3Pá<ñ®Ç“ßáp½ìðZ8á{Ö'[2rßߣ'oxë¶w§ÇÃ}:*&˜„*¯bÒs½q<Ç`›‡Ê°#íj*&˜Db O|`Lr˜$Òµ&­ßUzx¥ê¦ðæýwóûD?ëÞ&±q¼aÉðäz4ö8Óý«Ah˜ÄÆñ†š ªn{á{Øú GúclÏßáfûêßUzØCxeÅA WWPþÀOçñí•Ñy@®ÇÆñ2¾y åálØpždÛãi˜Pc‚›¯ ãÃmQv»òÃS‚/aó|×wȼÓ÷Œ |•<Ï0É=OÀñ<y¶Gb5p|ĈÇ#çIîyŽ§Æ„ŽÉØÜå¶*=Øæ¾.ïTf»…Àšö7ÄEŽEºÚ;Ži˜€ãC©’çY½+÷<Çó8äÉÚÊr*&àøÈM¶#y’9OJÏp<5&¸VÔãÝV¥j„*œÒz¡èj£@^Oh’.êùÍ}A£_[8>ú†ÉIž”ž'àø>™Ø3.¡bŽ2`Â7/ÚÒóO ~›ÆD¤óúx‰L ÇñîxßùI\õRã38>–*yžb‚<Éàx7[«_BÃ$ƒãÓ6Œ å‰Ã­‹1ƒã©1˜ jZn«Òƒø$îû«=cȤs#/)åý½(êsWÇ'_%Ï3‡Z\º38>qøäþd9p|’ytÀ•ï\t¸­0fp<5&x3W[q[•è2®,îs²B—ãp‡­ø]¾ÒÀoüöé}+ŸJ•×N1éyާqp³V+ËA¨˜€ã³Ì£#˜pž˜ž'àxjLðv®¶â¶Ï£œù{h¯g;º|£@h5cHZ?Ý5fp|ö˜˜ž'àøÌ½õ󘬡bŽÏaÃybzž€ã©1Á7ýdð¬ÃŽHæÁÌø±·öØÒpð é -aª˜€ãs©’ç)&=OÀñ4nÖÛc9p|Ù8ˆL8OlÏp<5&x7W[ÙÃéAú8÷ç—>Î;×»_üá¾ûÚgü>TXõuWÇ_%ÏSÇsäIÇó8¸y>Y Bä€ã‹Ì£˜ð­žú±€ã©1Á·ºð Îè¶*=ì¯èÕx‰Þr+Þ³ÎÌR¸ƒ¬Š 8¾”†É‰é¹Ežp<ƒŸÏ“µ TLÇÛmÆ„ó¶ú±4Ž·Ô˜à[]x°7u§4Ùtæçëó™%§ò¡ŠIãx»ù*yžå‰ëyÒ8^ÆaÖÛc9“L1r5ç‰ëy€ åɤ„Ûªôð|<³t¨9sÿ>p¦ÕÏΕLJ•×N1éy’ìq0Y BŤq¼54‹ Êç q<0¬wíáܯ؛,úö‰àÁz5ÕP|=º±o´öñ¥«'ÿ¼Îñ¥q¼¥£ú—˜øžëãefݶ—ƒP1)ÀÄs×~ôŽop¸" –L“Á‹˜¨­¦™öíÏÿäX¨núüX=ßoU§¨Õ¹+m˜ä*yžbÒr=mÆÇa¶‡{9“´`R8ˆë¤ãKn H[ãxkùìÙ4Ÿˆô }‚ûÂn-íÁ“Ï>QãT1io騾»¨ ;ô¤ÍÙã8Ìú­,¡bާ¦7ÒÃíøž—ý´ãIËöRŽ£½Dn«ÒÃy’ÿ}ULÀñtTß]Ø »ÐóÏãç÷ŒkA¨˜€ã©©ÀÔ…]à<‰=OÀñ¤eû2=w½›UIr=½_ìÓgyŽÝöc-œ6p<Õwua{ž€ãyf{¸—ƒP1ÇSSÁ&QîdêyŽ'-ÛO^D*÷áÔ£€[í‹¢#™¯Kgëýiñ|g2¿ô¿ê]iÇÓQ}wQv±ç 8žÇa“Õ TLÀñNæÑL8ORÏpw6ðùòiLDzØ“ùû±cí$À©;Ÿ<£FBUÓJð¹³tTŸ%Ï“>H—{ž€ãiü¬·Çr*&àø óèÈrœ'¹ç 8ž´l`2Êñ¦JÈp¹tšÎ=ÍÒTõŽÈ<:‚ çIéyŽì%1×WïL»7ýG,[úˆÅ4ÉHÅOGõ/1)=OÀñ<³Wa.¡bާ¦‚1L çIéyŽ'-;Lö ï±d9$9õÜùbŠ@3Bá¹€~¾}]¼xÄ>w–Žêû«Z=z |îdü<Ÿ¬¡aŸ;KM~¨.\ø:ÌÍôx&¤e“Á³ÎTéA\c œJûäx¶/ û«çùÕÓ0ÇÓQ}uý"z |îdf/Ä\BÅŸd¸sã1·Øã&”'az"Ò]ÔÍ·ÖËÄXLšÂzQéÎÿð²Mð¹³tTÿ“ž'àx‡0Ù/¼„Š 8>É<:‚‰\ÛóŸÙhz"ÒÃKR=óc{êP§ýP­~|½c’Œüôu|î,ÕgÉódâMÏpÙ7b¶¤ûÕ'øÜY:ªï/êÂÞô<Çó8Ìúp/¡bŽÏ2Ž`Âyb{ž€ã {-MjZ¦ÝàÎ'™u;ß÷§(DBÐ/1ÁçÎÒQ}Qöè)Hð¹“q˜® ¯¡aŸ;[dÀÄržØØã&”'yzR¥‡Ô,}xBö[]RÊTü*HRÃOGõ¯1AžÀçNơՅÿxõýÆd-p<5 bÂyâL§bâ¶í€Éø>G7/?HÒîÿ´¦éq½+Oª˜4ŽwtTß_Ô…½ëyŽçqX¨A®¡bÒ8Þm2Ž\DÎyâzž`By2_éÁlm¦ƒ^Îó^B[Ëw?=Õ|îÐ÷uaïzž4Ž—qXÀd-“LdÁ„óÄ÷\æýÃth8èªGg‚Ï#=þßó¤ØÃ8„Yoå TLÇ;Ò¡Ç0a=Þûž'˜2bœÜÇÛ*=|5xì«»ÞÙY>dmªòé˜ÀçΑÏ’çI Òw=>w<aÖÛc9 øÜ9Ò¡ýH]سﻟ;g·&£}õ¶Jµ­3ֶΨõtÛ ULÇ;ÒãY^;ÛŸt=>w<k¶Ì`²„ŠIãxgeÀ„õxßõxøÜ9Òã#êƒ=¶J;%>|툪]Í"nO{ŸèÄÿñT1Ç“ï/z¸}×ãásÇãfýV–ƒP1Ç[™GG0á<éz<|îœcïÔÉu—mW‡D^¬Hžûšç™3^æ 8žôøpQö]‡ÏŒÃt÷j*&àxÒ¡Ç0a=Þw=>wŽôx`2èßµÿ½o¯˜ø¹ïãðÑHx¨³öÛ ï:&àxÒãÃE]Øw=>w2¨A~¯S~c²„Š 8žtè0Rö¬Çû®ÇÃçΑý¤~b«ô€£±áÝê©”çÒ6}žøóR«&ð¹s¤Ç‡«d×ãás'ã0Ýý„† |ytÖã}×ãásçHazîé&XG§”åôåŽç»qûÉMïéñð¹s¤Ç_c‚<όì÷r*&àx_†1á<éz<|îéñÀdtb›ôpïo“{Õ· ²¶yÿ9A«aŽ'=>\ô û®ÇÃçNÆaÖóy9p<éÐa¨.Ìz¼ïz<|îéñ1NïOªô ÷R|œµŸgíáଊ 8žôøpUîz<|îd¦kõ«A¨˜€ãƒÌ£#˜pžt=>w.²·ø4Ç×£M{3‡ƒ³™kF&}žpþ=wãI÷3ú®ÇÃçNÆ!Íc²„Š 8>úQLX÷]‡Ï#=˜ŒÖ»l•ðmo.?¼XH•nAè½±FtއÏ#=>\Õ…»Ÿ;‡YÏçå 4LàsçH‡CuaÖãC×ãásçß#Ðü»ÏÎÙ*=ì)mM½£XÔl9…FÆîæã,ZÝti˜€ãIuáÐõxøÜÉ8ÌÞϸ„Š 8>É\Ô…C×ãásÇã¥.l¨R4ŠÉZ*&àxÒ¡ÃH]8°ºŸ;Gz|ÅdØ_xÿ{‘„ú¶'Ç"«ÝR_·¯Îè:;h˜€ãIý¡ëñð¹ãqˆÓÞ«A¨˜€ã³Ì£#˜pžt=>w®ðe0 “á¹ëÝ–‘2÷·)®T\mÀ‘5LàsçH5ÈÐõxøÜñ8¬`²„† |î\‘ytÖãC×ãásçHï˜ æ‰kW‡pNæTß'ÛúC¨7†ì )|)ô/&àxÒã¯1AžÀçŽÇ!N×…WƒP1Ç“=ˆ çI×ãásç7¾gg²GÂUéAÞ¯üú¹-D©Cp±n‹õJ‘ý_O ?zîàsçI÷3†®ÇÃçŽÇ!ÎÞϸ„ŠIãxO:t© ÖãC×ãásçIOnrâªô@Žú†ÉÛµ¸“çZR•¸ÍOµŸ;O‰/ê¡ëñð¹“qXÀd-“LdÁ„ó¤ëñð¹ó¤Ç'?¹?qUzØ“¿>­k]Üp+¥w}Ëö0ßü¡ŠIãxOzü%&]‡ÏŒÃt]x5“ÆñÞøQLX]‡Ï'=>µä oê‹o-éÃ'1n•iº~œôÜÁçΓ/ê¡ëñð¹“qhuáQÎå 4LàsçI‡Ž#uáÀz|èz<|î<éñÀd^Ô…C×ãás'ã0]«_ BŤq¼·2`Âz|èz<|î<éñiÖÛÃ5ë«ÃÁ2s0ÝÞ¯›\Àþ+OÀñ¤Ç_cÒó¤q¼ŒÃt¿ðj*&àx+óè&œ']‡Ïw0ÉÿÙáuWÂ+vx¿ª¯èM[òÇ_uaøÜyÒããE­>t=>w2­.<|žq5p<éÐc˜°ºŸ;Oz|JÓsWÆ‘Y™xÍ᤬kJï­0ç¿z«>Ü >wžôøxQ]‡ÏŒÃt]x5p¼“ytΓ®ÇÃçÎ{¾7s²¯Þµ£¼4Ù8š«Ky³¼èElEú»ÞŸ;Oz|¼ð|]‡ÏŒÃ´·Çj&ð¹ó^æÑLX]‡Ï'=>Íö »*=X™iÛ3wX.å‰Ï;Í æßÓÿ¸×!ÁçΓ¯jõ]‡ÏŒC™Õ´VƒP1Ç“=ˆ çI×ãásçI&£Ú¯«ÒCõÕ¿Õ Ulï”{Ö/ÖÛ‰T_wÁçΓ¯êÂ]‡ÏCšî^ BÅO:tª ³ºŸ;Oz|nµúѤkW‡È­Ò2 £ä-EmÛckÞûxøÜyÒããU]¸ëñð¹ãq&y²„Š 8>È<:‚ çI×ãásç#_~<Íñ"=˜R3üÎ=RdnþÝKÙm»U©òýkÝŸ;Ozü%&]‡ÏCšîá^ BÅý(&¬Ç‡®ÇÃçΓŸí4&¯Ö†sçšå×J w´ª }ëZˆ‚I†Ï'=>^Õ…¡ÇgøÜñ8$;ÉZ &>wžtè8Tf=>BÏð¹ói;`2^éáøúÔмZs{W1Ç“Ÿ.ú…#ôø Ÿ;`2Á'kA¨˜€ã“̣טDÖã#ôø Ÿ;Oz|v“ï«ô@}‚IIïZþMÅOzr—K§J¦G+õ GáXµ‡;ÃçΓŸ.<Ÿ£éyŽçqõüIÝŸ¬¡bŽ'z Öã£éyŽ'=˜ŒÖ»|•ð6dxêÑÉÅ"Ò-çmTLÀñ¤Ç§‹ºp4=OÀñ<³w.¡bŽÏ2Ž`Âyb{ž€ãIϳÞ¾ݸqT¬ž ‰¯¥’‚‘yÖU]Í<~¬»2|î<éñ颡ÇgøÜÉ8,`²„† |î|‘ytÖã#ôø Ÿ;Oz|“5H_¥‡º¡jgý÷,{mz¡âg]8ÃçΓ ò>w2q¶¶²„Š 8žtèAL8O ÇgøÜÒãs«Õž?ñ]zø¬ýÐOo/ŠñƫʬÞ÷›ásHOuáèzž€ãyfï\BŤq| :Ô…#ëñÑõ< À„ò$MÞµá«ô°¿GôØvTÓV1IÀ$Wy퓞'ãeÒ|ž¬¡b’€‰Ì£#˜pžøž'ãéñ¹Õ…Gµ__¥‡Þüpд©0tû÷Èüùá>„»ZïÊð¹ ¤Ç_bâ{ž{‡V6£úÉj*&ãƒñ£˜°}Ï“L(Ofk¾J¸ ·ôZñ¢þÅ] ·úÜŸ»@z|º¨ Gèñ>w2ÓuáÕ 4LàsH‡N#uáÈz|„ŸásH&£µ_¥:´ü¨BmýO¯wÚÿŠ|ÿ+ãëE?¨˜4Ž¤Ç³¼v¶?Ÿás'ãÐ0öT[ BŤq|°2`Âz|„ŸásH/Û4Ç‹ô°¿Dt Ÿ7ƒéÆ‹’=ÏyS¼µkôö­ÑÓPŽî›šásHOµúzž4ŽçqÈÓµúÕ TLÀñVæÑL8ObÏp<éñÀdÔSÍWé¡z]½šòƒB ><ü­Š 8žôøtQޱç 8žÆ!O÷ ¯¡bŽ'z ÖãcìyŽ'=¾ .<¨Ÿø*=¼î¼7¾ìnhu"×H׿b‰Î¥ŸyŽ'=>]x{ÄØóOã§û…WƒP1Ç;™G¯ÏŸDÖãcêyŽ'=˜ŒÏ]Ï£5¯ùgH|×·¬Âîs²DšÔ‰ Ÿ»@z|¾ª CÏð¹ãqÈÓuáÕ 4Làs¼Ì£yÂz|„ŸásH/­.<ê·â«ô°/äûÓZ¾×ÑÏF®É]à>q¿’þNp<éñט Oàs'ã0}àj*&àxÒ¡;&'w7EÖã#ôø Ÿ»@z|A rðÎLß®±ÕºÒ²)ƒÝ_·û¿|§[\òÞ2¬3oîW½ >wôø|Ñ/sÏpwôø|á푺Ÿ;‡éºðj*&àø$óè&œ']‡Ï] =¾Ìö ‡ƒõ_Êì èníöhñP¾ó/Äþ *&àxÒãóE]8u=>w2Ó=Ü«A¨˜€ãI‡ÄõøÔõxøÜÒã‰ÜŸ„*=ôçç®t,á©C]bIÁª˜€ãIÏuáÔõxøÜÉ8Lû ¯¡bŽÏ2^ïOëñ©ëñð¹ eûÏl­Þ5ªý†*=H,Ý”—×òX(ôóÖMÇõs¿>wôø|QƒL]‡ÏŒC™ÝŸ¬¡aŸ»Pþ¯·w뵤G®ÄÞý+êÑÚíä4f ì³/¶áñx`ižŒyHíq?XH-Ã?ߌrEžSÜ;Ijd »¾]ç’•Á•ÁÅŒ\”yt"OXOªÇÃç.”pÂd¶o%4顾\ѳ÷úºñcõ YŸ·_îNf°Ï“ûs<|îéñט OàsÇãPz]xzOÐnCLÀñ¤CObÂy¢z<|îâqlaRúTü­*tkK“ºnlËÎyy¾ÞÔVàsIÏç3&ÕãásÇãPÌúûÉ^CL:ÇGÒ¡óL]8±ŸT‡Ï]<(OÌbm%4é!³öVÃqý†3‡“üR|×CrÍã]|î"éñù¢_8©Ÿ;‡²\«ß bˆI&áçóãëñÉOÖk˜cÕó94éveÞ›ï#¶9¹NÅu~~?dO¾>Ĥs|4ví)€ÏC±íüøölŽ1qß0Ù bˆIçøHMy¦‡;y˘§cb|ÅdÕó94éánN·mÛîš©i:"J§K?ù5~‡Ï]4©Ék1A®ÃçŽÇ¡,Ÿ¸ÄøÜEj*(3µúäa˜”&yÒ754é¡ðç§é>ÕŽ|ªï²Žá¯ÈçÄǺ¹1ŸÀç.ZËSÐgL´§>w2èáþYzÉ^CL:ÇGj*(3uá8OBÐx:&–òdõ|ÆÐ¤‡º(¡ûŒœüÁ<Ûß¿Mÿ0î[Ï]´iÍ“Îñ2ËÞ»A 1ÇSSÁ$&œ'Qóo)OV=ŸƒZ_cɯö¡>hšç_ßΧz£ýÂç®>÷Mòü„‰öÀçNÆaÙÛc7ˆ!&àxj*(vBûM‘ó$jž€ã?a2;wžuõƒ¼ÛÉ…çž¼¯ü«ÕSkhÏ7yŽw©IžöŸ$í)€ÏŒCX}?Ù bˆ 8ÞÉ<:‘'‘ó$iž€ãåI\¬­Ä&=¼ŽÓ¡y|<‚g&ªé žŒãz| “Oy¢=ð¹“qXö|Þ b„ |yt“Äy’‚ÆÓ1ñþ„ÉìÙ²±IâGÝQ¼ŠO_dzmਯ[)¶ä÷\ºïûÍð¹‹>5ÉóÓ;£öÀçNÆa¹‡{7ˆ!&àxj*˜Ä„ó$0¡-øÜÕµK“×>b¢yŽçqX®Õï1ÄdÁ„ó¤hž€ãåIYž»Dzˆî·„Ï-á¿w¼Íœq»¿9 0Ãç.F;‰öÀçNÆ¡¬¾Ÿì1ÄOMs˜Γ¢yŽþŒÉôÜ%ÒCÂÄQ‘wkz ÖH°Làscj’çGL'ð¹£q :êb½k7ˆ&ð¹‹QæÑL(Oòqh<À¤æ‰A]xr?cìÖWþ{é'>º·ËïôxøÜÕ÷&¯}À$kO|îh“iíw7ˆ!&àø$óè5&ù°ŒIÐx:&©æ‰1Ës—Hg&töÚ—+¾¯$‡˜€ãSšÁDóŸ×¶—×Â{A 1ÇSSÁ$&œ'FóŸ(OV½=b“*ïYYIœá†2ü5œÊ»µ0|îb¶L Ÿ1ÑžøÜÑ8Pm{“½ †˜€ã©© ÌÔ…iÁO˜hž€ã³?a2»w.6é!=OQ|_Ô·(¾t"HCî Ÿ»:q6yíÃûIÖžøÜÑ8(& ïñ{A 1Çg™Gg0á<±š'àøLy²êí»ô`éþE)-!¶c?Gó>w±ØŽÉ‡<ÑžøÜÉ8,{{ì1Â>w±È<:‰å<±Aãé˜Ê“Uo¨[7ôÌkê.“³¡¯¿Ø¹$qù»>€oz$àsKj’çGL'ð¹“qðë뮽 †˜€ãKžÆ„óÄ0)'Lækn—\ïÙä¦ùT>Œüæý¯[¦)€ÞŽ}%¸wkaøÜ¥Ã6ÉóC 2kO|îd6j{A 1韙G'0qœ'Nó¤s|:(OV{¸cߺÁÒ3²‘ùãB³@¤/¾Î›þG˜$`’š¼ö)O´§>w2ËÞ»A 1IÀDæÑL8OT‡Ï]"=Þ¬ö Ç.=„qzÿxô2¼†gœeøÜ%c'0Q=>w2qµoe7ˆ!&ã“q³˜°ŸU‡Ï]"=Þ 9Ùs—šôðvŽ2ýu+ö'NÃß1)ð¹K&5Éó#&=O |îdÒ*&»A 0)ð¹K†Ý8¦0á<_às—H&³ŸúÖžêþN÷_çç'&Ì#ŸqXÿšS[Ç 1韬mòÚ'L ÇøÜÉ8,ŸÏ¸Ä“ÎñÉÊ<: ëñz|Ï]"=Þ¬Ö…S“"ûôÉZñh­XÉr@ÅÁ‡ˆm8^wøÜ%›:&ÞO‚æIçx‡¼ž'{A 1ÇÛ< çIÔ<Ç“Lf÷ǧ&=´gÏ$Ùïÿîi@ûœéæk räáp¼³M^û”'QóÏã°ÜýÄp¼sÄ&¬Çç¨yŽ'=Þ‹ŸôèotÁ³V÷çÌO¼…sˆ 8Þ¥&¯}ÄDóÏã°É^CLÀñNæÑL8O’æ 8žôx`2Ïñ¡OÅ’ê6ÒrÑð_ktŽÿêym}Ó“º7˜Àç.y; ôøŸ;³\«ß b„ |î’—ytÖã3ôøŸ»Dz¼]õ|NMzçK‚¢eÊ)¨zÿ¿5ÄïS“a’5OÀñ4fÙGb7ˆ!&àø óè&¬Çç¬yŽ'=Þöºðl_}êÖWÒºfhúmÆ£‰<쓚¥ÂJ%úGv„ 8>¤&¯}ÄDóOã°ƒÉ^CLÀñAæÑL8OŠæ 8žôx»ê푚ôp<¿uÔô>Çb<ý•N™|Ї0~?Ï]Šv“¢yŽçqX® ï1ÄO:40ùÔ™YÏEóOz¼õ˘ˆôàx«ÌÁþtèÁ?ÔÌçc×j߉¿>>ã¬Àç.ÅÔ$ÏOï'Ðã |îd–}$vƒaŸ»D:´™ª ³_ ÇøÜ%ÒãÉüû‰H÷/:˰ޭX)cH[2=}ÀŽÁ!&àød›¼ö!O ôøŸ;‡åîÝ †˜€ã“̣טÖã ôøŸ»Dz¼]­ §&=¾ÉÔ›leUÚûO§²!&àø”f0Ñ<Çó8,×êwƒbŽOyΣyŽ'=Þöº°›ÔOÒIz8šÆ»?›·å»ó |îR¶Mòü„‰Ñ<Çó8ôºð´_ýnCLÀñ¤C›™ºpa=¾Íp<éñÀd>ODz0\Ý6©ùz¾ÛÜÏB’²Tm½¿?ã¬Àç.‘o.êÂÅhž€ãyâjoênCLÀñYæÑL8O¬æ 8žôx›–1é¡&é¹™€?¤tŽöâá’ÀG˜Àç.‘‰ ôøŸ;‡åîÝ F˜Àç.™G'0a=¾@/ð¹K¤Ç[øHLÖVr—ºÇ¾e×}:„WõCýÓ²™œVió6æxøÜ%ÒãÍE­¾@/ð¹“qXîÞ bˆ 8žtèIL8O ÇøÜ%Òãíj­>÷£Cîìþ +äÌo¾øJdK¬À³@ýS&…!&ã3éñæ¢.\œæ 8žÇaÙóy7ˆ!&ãó!óè&¬Ç§yÒ9>“oËâZ8÷­ò4¹þ!ë³fî­^³=qûó1~‡Ï]&=Þ\x>§yÒ9^Æ¡¬ö¦î1Ä$“ ÐÑŠöíþøâ$OŠÆ“O×p«uáܤ‡ú(Ý<—øQr÷vÛ‰› è-Øjô;L:Çgc&0ñšëÅžb°GÛÿ'¾Û¿Ä“Îñ™š ÌL­¾xC˜x¯ñtLHËv«>ܹoÝxüúºñmß8íOá<8LÓ§áÉ&ð¹Ë&6yíÓÜ…ž‚Ÿ;{¬óÉ^#Làs—©©ÀÌÔ…‹ŒIÑx€ åÉj­>7éAÖ"8hÒ>Á¶}OÞë);TEúHøÜek&0ÑžøÜñ8ØåîÝ †˜tŽÏÔT`PþàUçIðOÇ„´l`R&ý s“ÚÙ«¼³ßšˆ!¿—%ÈV‘KÞô­Àç.ÛÈá\`¢yÒ9žÇÁÚÕ=A»A 1ÇSS™ñá.ó$hž€ãIËv«Þ¹I4 =ÌÆÓ3e{¶?ÈŽáɇ 1Ç;Ó$ÏO˜hO|îxl¯ —ù¹k/ˆ!&àxg;·]Í]‘ó$jž€ãIË&ós—H¢ü|ÅŸÏ”1ôX½^S˜€ã]lòÚ'Ž×žøÜÉ8,×êwƒbŽw2Î`Ây5OÀñ¤e;x>Oztæ~ôº„ã†#5TÃÇæ’)ƒÿní¨°ß0Ï]ö¦cò!O´§>w2˵úÝ F˜Àç.SSÁ&‰ó$y§cBZ60™Ý;—›ôpðƒóäÓ]êL+‚6}~ôb·ëßeG†!&àx›äùä |îd–=Ÿwƒbާ¦;UNœ'©h<À„ò¤×êgÏ*Ïj}U¸Ò}¨ EY=ü!Ðg;®wÁç.Ó$ÏOs—öÀçNÆ!¬çÉ^CLÀñÁB·½xgÌœ'Ùi<ùt ·ê·’›ôX}  …ð⯴úQaÓVNé{wVyÏ]¡cò!O²æ:8žÇ!®zªí1Ä"1‘'90&Yã&õsðö˜Ô~s“äi’½þÔjðj4>¬þXRŽ0ÇÇ£Iž1Ñ\Çó8,×…wƒbކƒ˜À¤¢]1)š'àxjLp½V?ÛK”ûÖ Ç«ÇÛϧÌÞOOwâÈS6Â>w9†&y~¤ Oàs'ã°É^#Làs—cä f0á<)Yã&”'y¹ÞõÂëVʾ÷à<¿É 5„/Þ5`⻾zøÜåtÌ`‚<ÏŒC^瓽 †˜€ã“™ÄÄ%JýÓˆ:*ÔšàÊâ[ciâË…yžä 8=ÓùÍØñÖsYë߆NÜNw•Κèù•„¦ hž‡b¹\¿ÅÐ|ŠÞ.^åk(A`ÑdÑSw‚[5])}ÿF}Û*]òyé‰Åâ;*&Öwî7¸³qì}>šî™ëÀ¾‡E³LÏCQV;ïv£Â¦Ï2›ÎÀb$[Œf ¸ž|¯Ïî *M‚ ÛäÛÏì$^ø¤ú'9yüz”òÆáþmAü p} –{Mkš- { ·\ Þb È>Ë„:‹d‹ÑlÝS‚7Ë“˜ëÕ'ìΓÀ‰óΫHú“=ر·ÎÖwK»;F°Àò.“‹>«ŸŸaA¶ÀóŽ‡Â™U}k7Š,ð¼ËÅ@íï*ò>>|W1‡•lë_Ea™|«/Mˆ­N76Š1< äBßͼ¹´~øbÍŽ><©} ¿tô#,)Û;Âu hãÜëÊnCX@ùt€i鮡x%iD€¥>h~µ©»4-¢~ »í;¬‘ø+7n¹ÍôžEþVàâñáÆw™{.Š÷5¤<œïx(\¯ÞOŸ”½Å–Nù…Np3•âJaXœfK§ürP¶¬ÖïK·Ã2´ni·ÌA$>I¾âŽö×fK§üBíî¢~_ƒÐlé”/C±lνÅ–XÂ4,N²Åi¶$ÀKž.ƒ•¦H¸„ëAø„ï Ô¿æ{›(†°ÀRšêö¡ VƒÐl)ö<ËõâÝ(†°tÊ/æà(¦`‘lñš-ò µ,øÕ“K%x>~ñ›—]ñ’Õ?>>'18àÚÄï.*Æ5d ,ðd(–Û»w£Á ¼bB‹âêȇú=É5€ _ů6Ý—¦KèÍ>NŸï§Ï¯ÓçñÆ:˜à)<ŠÐ(¿-Jg(/Š!,°”¾ü¸ÌŸõ€^±Ì‘ËÜÒ¬±Bszn‹™ÒÔ‰Òõýø+’‹Ü–NùÅvIô#,)#<ŠeC–Ý(†°€òÉÀÍŽk(N`Ñlå[Ê–¸¼k§³ßé,½¯×ötïõ_ |bR½}LÆ·/tLù°Â+èø ‹f (Ÿ‡"®î‡Øb (Ÿ< œŸz ’-ê 7¼Â+Ê´<‰Á kp㹟4ÂmÓOK5ŽÖN=‚”ïº0ú–¨ÙÊç¡X>Æq7Š!, |²%p3%ýŠd‹À¯¸°Ë£ûÓ¦Ù¾ï†öÚpé`/À›n2¿7S ã¦#â×µÑ+±ˆl#ž Åry7Š,pÄ+®tž»†E²E½à‰W~ÒWÄ+¾}ðmæöcn-^áN‚kX4[@ù<e[ö¢ÂÊ'‚YX$[Ô!Îx%Ôl ph™|¼5¥â°ã‰¹MØ«åue8Þ¸ß8ãÚï.Ü@jš- | ¿\AÞb (Ÿ, ÜLÛw E²EM`ŽW¨›°Ìnîºõýy°¨|òzÓòª†6àä¶çÍæ.˜ãÚï.+ÈY³”OCá—]Zv£ÂÊ2¡NÁ"Ù¢>ðÇ+ÔÐV û·¦TØL©}ÏÜ™à¾À5V(3Œ …·ë“Ø^CX@ùäUàæ û…³Åt·ºJ‡…Ú–él¥‚\—ù¤‘ʲ†Ùûbóqzu¦¯×¯¤1·Ô[åÓ&yUA6‡f (Ÿ†°L¯Äv£ÂÊO2¡NÀb'°h¶€ò©¯!¬6ߺR‘¿iHÔtÈBMüû“_Ö}¨>2ëå#X@ù´OÞ_˜«× 4[@ù<Ë…ýÝ(†°€ò“L¨S°H¶ÍP>õ5¿œ-¢TnÉ"·:šèÆïÊ 2›¯·…ýz+ |Ú*ï/úÀk š- |Šå ònCX@ùdZ0 ‹‘l1š- |êk«…ý[S*ôñòŸ/{ ìò í–÷Wdc-ðË“¡ë°ìE1‚~y%Ë„:‹d‹5Q‡…úœ[&w|ÝšR1~¼nôx=¸'áÁý .d<ÞÀʧ óþ¢¼lež År;ønCX@ùE&ÔX¬d‹`¡l‰Ë“XóÑò|³Žÿ´ü§iÃÿèÿ+üõìßÂʧ=óþª‚l¬f (Ÿ‡"®Sþ^CX@ùÔ‘àÑ¥ÿI3V²¥{ ÐUòé*aµÿøÖrç§'Ë-sXò„=ÙeëõjKN88aÊ7õlSä>Ââ4åAù<iUÛb‹P¾0üTÙ° BýOЈr¿Š?Á2Ÿ-_p zµöúáõ$‡¦Ä>ídwÔÉíØ~ªÞJ,©)rŸaÑ”ÊïC‘Vkb»Q a)€%sS°$†Åk¶ÀBÙ’—¹å~~ë>Zr /½‹Ezˆwè°ýF´ã ²=š{^½Ú< ‹G¶4û¼>y½ø²Å–fŸGQ¸iX¼d‹Q‡…úB¯ Ï6»ÞšRQoóv§sÏÊtã†^ˆ¥¯:ß¿íg{Ž6QÔ[q€%5¡ô3,È–æ ×‡bÙMz7Š!,°dŽbB4^²%`)'Xf[ÃoM©Æ×£Íõ™Ëõï9yì ÿ9„%tXh ½¿j 7A³%ØóP,öw£Â:,Öuž»Ì– Ù4[B‡…dðx4lÖ:äÖ” y¼ gº]i¹ßê|¡ù>ßÃʧ]ôþª°o‚fK²§¡\ª´e~ënCX@ùÔ‘0 ‹dKÔlå“ WÝC¾º¹‡’¸˜÷ŽÌµ=׫IîÂÝ°Þ (Ÿ6Òû« ²‰š- |ŠÐ+ÈÓVz»Q aåSG‚Ÿ*ì›(Ù5[@ù$ƒ–ÙÍ`_M©hÞ€±[ÿõ K]ƒÖd¥´1‚Åòi/½¿ª ›ˆlq |çN°üæœù–½(F°8P¾“ u É–thD€…²Å.¾·|5¥â){ y[³½·\-ºy‡Î5â¨O¬Þ (Ÿ¶Ó_Ã’-”OC– ¿w£ÂÊ§Ž„0WAN’-)hD’Á£[\‰}5¥Âð1Öõ~+_ÖÕ‚á¾]Y·¼ØbËò9 uáYׂfÔìZo”O;êÃea?i¶€òy(Ü:,{Q aåSGÂ,,’-Y³”O2x\uùjJ…¤x£ä!/VëéÉ<3°×À°!©Þ (Ÿ6Õ‡Ë rÖlåóPøunÙ‹b (?È„:K–lÉš- |’Áãjù«)t³,ë$ùdX2–7A~‘%­µýYõ ×[å“7@¸¬ gÍP>…_] ïF1„”dB‚E²¥h¶€òIañ½å«)õ–é…ëÉ)(Á›I6ÏÖõ”¶„zÕüõ+|AíáAùd °|ª d‹åóP,÷ ïF1‚Ńò©#a–"ÙR‚FÔa!<®ZMõK¶—?È[Þ|Ñ1û)ßù,„v°õÐ&¿Þ (ŸÂeY<(Ÿ‡"®öw£ÂÊ§Ž„0WØçà Œ=°”,³¥Ê¯~h ¢¹µ ÔtOV»9Òç/ ýwX@ùd®*ÈV<(Ÿ‡"®gË^CX@ùI&Ô X,Ÿ_Pÿ£ÙÊ'<¢yÒ€ç«+W"ш±8ÑÊY2ÔÿÕÖ6CX@ùd0‹f (Ÿ‡¢W[·¼‡e/Š!, ü”ça‘l1š- |’Ác^žÄpô{{¤ò 1èÔŒ—ÆZÆï-”O᪂lµ!Áƒòy(–Àw£ÂÊ§Ž„fJ•ÖH¶ÍP>Éà±,Ã"J9mÈéc,ÀRá·¯<ô+#X(Ÿ,ÂUÙjCBåóP,wìïF1‚%€ò³L¨3Ùb$[ì¡–r‚e¶}ï«)í–=¿…ýö¹ó×Í[X@ùt’Á5,Ú@ù<Ëvà»Q aå™Pg`±’-6hD’ÁSïAžíØÿjJE èù-Çé-éŸõ[ÃC ë­€òé0ƒpUA¶Ú@ù4ñXÏ–½(†°€ò©#aÉÕò(Ÿdðd–ßòŸ Kö7NÉ鉎e°Ð)ß–ÏBé§÷«Z~åÓPD³þ:¹Å–Nùæ u†[DË·ªå‡Nù†´|À2k»÷Õ• ÏoÀ/>TÑÓY—Tl½ñW̯ûíT×pï´üPKjŠÜÇlQ-?tÊ硈ËgxîF1„¥™Pg²E´|«Z~(€…²Å.Nb÷¦TÔÉÑü¾Ð7§ÇÎ|û™,±S¾!-ÿÕò£±§¡ˆËäÝ(F°ÄNù†ìIXDË·ªåÇNù†´ü„ òä{˽ï éA˜>й?ˆ,}›Þ¼åGXRJ?Âl‰òy(b¯ ÿ&½‡e/Š!,°dŽb ÉÕò£,åËì$vïJTˆD”)Å<Új8šÓzSµ!,ò iùñª‚lUËÁž‡Â­®Äv£ÂÒ)ßX™Pg`-ߪ–;åÒòÓª‹Å½)_R?«D±Õa^‡-ìœBuØqû^å“–?‹fK§| ¿>‰íE1„”oó<,’-ªåGP>iù©Wídù®Ã˳õõúV‡[×ÑddÏÚ<^‰EP>iùñª°oUË |Šå ònCX@ù$`Ç© ²-ߪ–Aù¤å§¸¨·Ü›RÄ~C6ëJ7‚µ](~¶c3_!, ”OZ~¼ª [Õò(Ÿ‡b–½(F°$P¾“ u ÉÕò(Ÿ´|À2«·Ü›Rñsö}èrE§‰ìx׃œ@ù¤åÇ«Ž}«Z~åóP,"¹ÅP¾— uÑò­jù ”OZ~Z=Fò~:áä4û¦{›}Û´À.œçia (Ÿ´|J?í4¶ªå'P>E¯ O÷‰íF1„”Oö,,’-ªå'P>iù°äþ"&Þ(möå]95¸Öiýl]ît®ÃãÝJ,òIË—dÕò(Ÿ‡b¹‚¼ÅP~ uÑò­jù ”OZ~Ê‹¥Ê{S*ê“t~Œ„8Ÿ²óÀWÓ×!, |Òòãe©RµüÊç¡,³'MíF1„”dB‚E²EµüÊ'-?•å÷–[Ÿ•¿øO9Pîß_Äü·å7ä Ê'-ÿÕò3(Ÿ‡b¹y7Š,”Oö$,¢å[Õò3(Ÿ´üŒ ò¤h|ïN\‘±¯[LïD¼µgîÁ/e·Û‡×É Ê'-?^VUËÏ |Št¬¿NîE1„”Ovœ« ‹–ïTËÏ |Òòóªç˽)ƒ1?x+Ÿ^'3(Ÿ´üxÕƒìTËÏ |Š´ìù²ÅP~’ u'Z¾S-?ƒòIËÏ«æ"w=3þœìÔŸ`•o¥Š¼¤qÐ+¶)b (Ÿ´ü X4[@ù4i¹‚¼ÅP~’ u ÉÕò3(Ÿ´üÜ+ȳ»ÁîM©ðœãØçöÅŸ… _Ÿ;ßÒ›lå“–ÏBé§ý-Nµü ʧ¡H½‚œge°Ý(†°€òIÀž„E´|§Z~å“–¯°L/_Ýÿ&ÛVŽ Ålh¶¾=ÛllŸ°P>iù骂ìTË/ |ŠäV÷·ìF1‚¥€ò³L¨S°H¶¨–_@ù¤åg·Ë£;q} š'½—Õ&&Ežèt­ßíÉ;†¢õV@ù¤å_âZ~åóP,›‹ìF1„”_dBE´|§Z~å“–ŸýbñåqR*„/ot×uª.¾}Úà¹%‘~æÆç¢ŒwƒP>iù骂ìTË/ | ô ÿ|ùzË^CX@ù$`ÏÂ"Ù¢Z~å“–ŸÃb©òѸX³«)vg?”ÌŸÉÆù4CGÏg7ñ\1„¥S¾%-?]Ujù”ÏC±\ªÜbK§|Kvšª ;Ñòjù¥S¾%-°ÌOb¢T<êjÞÐÛ0-î MÃâ®]?v°‘ž„ú5;.ì—XRSä>âÙÒ)_†bÙóe7Š!,°È„:‹d‹jù¥Ê–U{êGS*ÎþéÖÜ£m”¶R¼¼n|Ê·5G§|KZþ5,ÐòÍaìy(âj©r7Š,æè”oƒÜ{a.âDËwÞkDùt•Kþã1=‰…n4@®ÁwâÖ2ûèÞxµsÚF=„Å–Ø„ÒϰÑ)_†¢›‹„còÐÝ(†°8À’8Š™lñQ`)`É´2mÙb&_'M©@§Žy½8 Ö©Z§…ãÛ´0„¥S¾å7« ²CC‚9‚=EZ_‰íE1„¥S¾¥Ž„4UAvÁ0,A³¥S¾%<÷ òlÇþ£)5µe®æ@]UÔpÅZký å¾i1ÕÏå , |§`Ñlé”/C±\Øßb (Ÿ:fa‘l š- |’ÁsYΖÜåVêMÉàeÕóåÑO>ùÐó{@CÃ]÷=LJ?u¾¸¬Ùʧ¡ÈËû»Q aå™Pg`É’-Y³”O2x±ËÜò˜ èõ= ¡hlà¾gé¼ûtÕî²f (Ÿ†"ÛõlÙ‹b (?È„:±‘ÂeÉ–¬ÙÊ'°˜ÉŽýG?ùäc@þøаÅÂÀ}ÏÒ‘÷ù²‚Œ†÷= ·ž-{QŒ`ûž¥Ž„z?‹f (Ÿ‡b¹°¿ÅP>u$ÌÂÍP>Éàeµ‚üìJÅ©$!oÆîq+§Wg2Ý{щš_o`ågÓ„Ò°ÍP>ÅrònCX@ùÔ‘§ ûÞH¶ÍP>Éà Ë$å?ûÉ'(Uܾ…"/ÍE'æ,pß³tê}¾ª {4$¸ïÉP,{¾ìF1‚î{6Ë„:‹d‹)`¡lYµ§~v'®Ç÷çÌKí»Ã½“î{¶˜ˇ÷†÷=Š´>‰íE1„”_ì4,V²Åz¨ÃB28`™uv}v'®óí¿¾ß~üö]‡°€òKlBéÇl±š- |мúÞ²ÅP>u$ä© ²·’-V³”O2xé…ýY¯ÊgW*pûéûí›ïÁåöÝ!,òÝaš"÷§ÙÊç¡Ø€e/Š!,òÝ!ê ,N²Åi¶tÊw¤lT'K•ϦT´Û·ßo¿|Ϊ±#X `‰ –O51ï4[:åËPô ržíAÞbK,2¡N¼å{'Ùâ4[ `É€eå½E” ã¹è:?OçµþÿgmçÎ,pß«Üß„ÒÙ‚†÷=Š"dvñ›…e/Š,pßsÔ‘§*ÈÞK¶¨–÷=gêïÇ2,¢TÔPîžÎæËÚo¾ôÍóÕÎuï3pßs¤åç«d¯Z>Ü÷x(ŠYU'w£Ââ‹L¨S°H¶¨–÷½º$«°˜Åâ˳)­-¡‹ÜÔ¥tÿŽ<‚O±†ðov¸ï9iò¼‚Eµ|¸ïñP”å ònCX:å;+ê ,¢å{Õòá¾ç,e‹],ì?ûÎï{tô¾}ë(¹¿q±0pßs¤åOÀ¢ÙÒ)Ÿ‡¢,Ÿ;¹ÅP> س°H¶¨–÷=gó –ùIL”ŠÛƒÔºÛ¤ÔæGûõmV&£Tž%nlÝ1„”OZ~¹:àЫ–÷=Š²Üƒ¼ÅP> Øeª‚ìEË÷ªåÃ}¯.¨)qÑ5üÙ”ŠÛ³-QZ{Õ«ß{äo…ö-©5Ïo1pßs¤å—« ²W-î{2Ë…ýÝ(F°À}Ï9™P§`‘lQ-î{õ½’^—K•8i^nö&”é­¡¥?åsà?Ç-pßs¤å_âZ>Ü÷d(–ÏÜb (ß[Ƚ]•^´|ŸœF”õ*öË51({ÔÈV*êzã®ðöÐ瓚ªé¬FÿæÜI÷=çCSä>”‡ûž Eh]•TjšêªÜb (Ÿ: *ÈŸŽšöuŰd°¤,³)^ýä“£½a%^?Þ{(Ù| ¥~7ðOa凃§¢+X4åAù<½°ÿ[â»lÙb (?ðÆŽ©I¬^—`Éš- ü@Ù²êùòê;C^tï–R¾-cίȶq¬åÃ}Ï…0‹6$À}O†b¹5|7Š!, |êH˜…E²%k¶€òeKZ†Åþ^ä ß7Póù'ma“H=úkùpßsñhBéÇ•˜6$À}O†bÙ\d7Š,pßsÑ@î­Ü"Ön)’-ÅjDY¯¢°ÌÊ`¯¦TÔÛ Ïv°Éñú~â†dù !ÇÄ2nH‚ûž#‹J?ÂRòpß“¡Àq ?—“ï¸e7Š!, ü:Ï]fK½ Ã’4"Ài{Èâ¶£WS*ô©â#ÏY¨¼ÚŽöºÈ©³õÛ}ùî{Ž,Êea¿ åá¾'C‘WÈ»Q aå§c–B°„C³”ŸxëÑò$°£ýä)@[Ýõ†ÀaH¶XÙ^ÎÂÊ'‹€rUA‡f (Ÿ‡¢¬Ob{Q aå§ÀQLÀ/°h¶€òS<Á2{dÛ«)¿ï ¢cšØsÈ&?6„”O媂ÍP~ ª·.Ù¶ÅP~>ú„z ‹d‹ÑlågÚµÓ+ȳ2Ø«ï ­˜gXãàødj,ƒ9{ Ü÷Y\Ãb-pߣ¡PX¦EãÝ(F°À}Ïe™Pg`1’-&iD€¥f‹Y5y5¥âL´~4Ô"ÕØöWYçR¾Õlå—ÀeðEÏ—Ý(†°tÊ÷‡L¨¢q°’-N³¥S¾?x£îڙƓV<¯¦TÈ]‹•”’ÚÙ¿RÛãiûx5Éï–÷=ø&”~²â N³¥S¾ …[‡e/Š!,°„,Ÿ³ÅI¶8Í–X([V8|5¥ÂÍ}š Œ*®mÝâúòÆ¿ëA†ûž?JJ?NbÙ÷=ŠåÖðÝ(F°À}Ï™P§`‘lñV#ê°s‚ežòÕ‰ë»a`szº¢:t aé”ïoŠÜGX<²î{2°ìE1„Å™Pg`ñ’->iD€…²e½‚üÄû±mµ£ã«ÛÓ¦Ó¡šV5V|ëÆÅ¸ïyS¦`Ñl ö<h Ÿ=²m7Š!,ò½= ÷^è-ÁK¶àÔ÷=¾ `™Ý—ÿjJÅ·³KûÁÙ¿Ÿ¨Ùä¥q©î{Þv¡ô#,AS¾S¾ El5±'õ–Ý(†°€ò­‡Ü{KýW– åÓULl”?¹¥UŠ&oËÍÞOŸ§ÏÏÓç1åÃ}ÏÛ4KD |ŠKÈn –í(†°€ò­È½3“˜}´;î{Þòæè¢ñÜÙM©ÀÍÚ£»ÓçÓÃ÷õýáS>Ü÷¼³M¿þ Ž>0pß“¡Xm ߎb Ü÷<7GÌÁ"GíN€ûžwþË$·Ô€,Tpy†|×WYÿ>K¬ôPFŠ;;_à¾ç]J?Âlûž EZÔò·£ÂÊçæˆ0£·„ز¥hDùt“×J•õ]?®1Õ»6™µÖÉ­ß#K¯wæ"pßóÞÌÀ¢-pß“¡ÈmkOèÌ$¶ÅP~kŽ˜Éi±Ú÷=ïëïšE×p4¥‚RÛ°Âè1ʦÅwôàôAŒÊP¾M¿þ ‹¦<(Ÿ‡¢¬gË^CX@ùܧ(_ì‚¶XÀ}¯X&+È5 ÐŸ³'wêüÌ÷Üçc+ð8a凣 ¥ŸaÑ”åóP”ÅΗí(†°€òƒÊ½W°H‹EÈV#ʧ«Ø^ØŸlß«·,J…f}”¦_†%#P> …éäßNÒz?‰íE1„”¨ð—Ù"¯“8ÌÁÂ}¯bOiõGS*ï[£Òp}Ë»õ5fúyÇó , üèZ@+Èè±pßã ŒYŸÄö¢ÂʾO¨—ÙRäu‡9X¸ïùȼn­TYj'Íg]ô…Š|÷ØÎrÏí[_í”!, ü˜›~ý1[Ц<(Ÿ†Â¬ºÅP~,ó°d‚%š- üt–Ü—Ù°´!ì# ÏÙƒ§äã~:‡&|ÿÖ°Tiá¾ç“kúõ§†¤xh¶€òi(Ì깓ÛQ aå'ßyî –x8E³”OÝ ¶wìOVk@¢TÈýÞ|¿Ï·‰/.œã¾…ûžO¹ ¥Ÿ²%¢OÄÂ}O†bu#Åv#Xà¾W—ç}B½†E²‡9X¸ïyêN°½°?Ù'V¥âÉÇÌKƒè9l6¼ñá¦O>ÚáÍ$÷=ŸÝ ,è±pß“¡XíØßŽb (?ûiXŒd s°pßóÔX&ÕIt¥âqJpºÔ5úÅÜ_þ·Ó5G°€òsnúõgX4[@ù<«®áÛQ aåçþb%d ,Ü÷ø*vñ”V4¥Âö:Ÿ‹¼ì )íô€#òKÙétÀ!, übg`±šò |аþÞ²ÅP~q<¡Îd‹œHaaá¾ç‹'â啘(’éO¯+–ħÉ @½q™>ÃÊ/©µ|†ES”ÏC׳e/Š!, ü’Ñq•-r"E„‹……û_°ÌÖÄLS*° TOáõ&Å—z|xM¿·À}/]¿þ \,,Ü÷d(zk¸™”Á¶£Á÷½pXŽb&[ÄÅ"ÂÅÂÂ}/ÐïÚ´ø:išR—)¹>Uõ_zäF?žnèùbá¾Ðíñq  ÷=ŠU3÷í(†°8À’ú„z KXŠFX([òbMÌ4¥"rui©œWù¥á=q([¸ïîö¸*ìG¯Ùìy(ò:,{Q a锌=Áò±O,Š‹Eôš-ò¹X–ùIÌuwú¶è~¿ýH;D5ÊÔ~lK,±éןaÑlé”/CQVßòw£Â’Kâ(&öNFq±ˆ^³%Ê–²¸3M©øù0%ê×Ñ8ò·ÜÃÎ ÷½`MJ?Â4[Š=ô–ÉÃs·£ÂÒ)?XÛyîr‹4[:åj7q«…}Ów†x²rüÖéfNÇÒ ²ò![à¾lœ‚Ù÷= »Ú±¿Å¸ïj™…E².î{º,yv_~ (~ÎpD»胹ÿºy}QÂÊw¦é×)}"î{<vÕŠg;Š!, |j1SûQ\,"\,,Ü÷u'¸ÅŽýPêvÚÃtõ¢\W;_Cd ÷½àbJ?fKÔlåÓPìÀ²ÅP¾“ u É–¨Ùʧî·Ø±_ÂIóž_»ê‡pûe˯W}ùŠ¿"÷‰ü¾îÂʧ‹kX’f (Ÿ†ÂÚõ•Ø^CX@ù^&ÔX¤Å"&ÍP>µX8:Y3º3D²þa©ŸŠþúE½‰´Þ¬?ÆÄÙü¸Ž7G¶Y¸ïj±°—…ý¤ÙÊç¡XíØßŽb (Ÿú fa‘lIš- |ŸO°Ì_LS*ä–õuì °êësFª‡dUs§#Xྸ­à²‚Œ),Ü÷d(V­x¶£Á÷½dBEN¤ˆ0€°pß ²eµ°oúÉ'õN¿ø–=§Éà~Ñó¥$JÅàÙbÓ-ûu ËhXCX@ùäÜ`¯*ÈIà¾ÇCáŽuXö¢ÂʧŽ…åÓ{K’)’jùpß $ƒû^ª´“ÛŽlS*êÓcü(,îu7¥%þOÒfhŠhá¾Ó¹°¨–÷= Àò[ïaÙ‹b Ü÷â!êL¶ˆ–ŸTˇû^¤ß,“çNzÛO>é“=ºAOÆœ²þuŠx‹,q d Ü÷x(Üry7Š!,°È„:‹d‹jùpß‹Ç9[æ¹¥|Rúƒ•逳AàM Ž;EÅœ#ˆÑ@Æz Ü÷"iùöê”Ö¤Z>Ü÷x(œ]íAÞbK§üHö$,¢å'Õòá¾IË÷½T9+Û®T¼è Ú&õluï[fÃ‡Ü JwGó7u‘Üßbá¾IËwWÇ&Õòá¾ÇCá–K•»Q aI€%AW¼èK¢å'@X¸ïñUˤÇ~ ¨í yÒ+X†^ˆÚ•sôíÒ7>¡9­ÝT9Þ¤—¢Õˆ:,Ô×àÃò[~S*x2®ÿ´}²ÐêÉWðx’EÝ·Š¼B§Lþs (Ÿœ®a‰Hy¸ïÉP,wìïF1„”ïBŸP¯Ú÷Rô‹f (ŸúüjÙ6¥¢fa‹É´˜W+4&vâª\|Ûƒ ÷½èJJ?âÙÊ硈딿ÅP¾?N°|Ζ(Ù’4[@ùÔ×àÏõ¶)í’éùÕ ~wÞ@áâÕì諚, |ï›Púqœ4[@ù<«‡çnG1„”ïG1K’lIš- |êkð0sŸ® ‹RA9ðý"¸š—Éü XÉðS(nCcs ÷½èK‡åS¶ÀóÁÂ}O†b¹5|7Š,pß‹á€Ü{EùI²%(Ÿ®âW{mS*î|€ì¶tõòÐÏdRë?ÁÊç¶‚ËÂ><,Ü÷d(–=_v£ÂÊ'Ó‡IX²X‚F”OW,³+1×” Y?ê½?¾­ïõë…ž67^ Ã}/’"ï.+ÈÛŽà¾'CÑa™Þ ¶ÅP~¹w†ò3o;JÚ÷½HÖ¾ög{Ý7'.Ü8Y<’&Ú_3ÇÑ,µ|¸ïERäÝ¥Þ¢žpß“¡X.ìïF1„”ßš#f¸¥XE³”ONáX|tM©À½?Ù¬Æd îÕ§…ö×ünK+Ü÷")òî²°¯žpßã¡ðÇ*·ìF1„”EîÉ–ÂÙ’µ;î{‘œËlÙ5¥â8Eð”vP±y²tü ýyï3À×;Xà¾I‘wW䬞pßã¡Øe/Š,pß‹Éõ õ –|X%hDr¦½åOZˆÖ€D©0<õR{¢)òîÊó%«çÜ÷x(|/ìÏ5½ÅP~’ ubˇd‹v'À}/’Ó@X<´FÐT}ºñpôÂQøu "nO¤žžDËž!, |RäÝUkxVϸïñPøeÏ—Ý(†°€ò¹9¢Ì¬ÄèùaX¼F”OW,ó”O/b·£hFK—¨»§q¯»Ž!, |j±ðWä¬-pßã ¼[_‰íE1„”O}~ª°Ÿ¥Å"㌠÷=¾JpËÜ’à–"PF’Ž ´ìºÍëù±Û¸ø÷½HçJLÀ‚Â>Ü÷d(6`Ù‹b (ŸEüÔÑÙra?㌠÷½HÝ €ež[2Ì7oí~Ÿ©=vô?.¼Êû£}Pýœ¾8<‘ÂÁ}/Ò¹,”~*¾dô‰8¸ïÉPôÂþì!TÛQ `qpß‹Ô(âÍ,A`É`¡lY<ú Ôœ¸P÷¾‘’$žŽÇqzìêû¶ÔŸîྗè\ UAÎèqpß“¡è°˜ÙRånCX:å'jñS…ýì$[pƆƒû^¢î„–)_” 9´œ,8{mïj‘àüÚOa €%ÌÀâ4[:åËP,öw£ÂKœ‡E²Åi¶À’N°Ì:$¹¦TH(ÖÓ>âc9GMsy4æWî- ¿ÃÒ)?ѹþª°ŸfK²ç¡@køäÑÛQ a锟¨QÄOö³—lñš-òu'„U½Åõ“OÌ/'掙ªߊõ|xÃãÎê„mµŒòÎp×Á}/ѹþª‚œ½fK§|Џþ:¹Å–XTî½Z‰yÉâà¾ÇW «…}×”Š×£)êõèn§Ï_§Ï÷7[ZÜ÷2 Rî{2ÝŠgöDŠí(F°À}/Q£ˆŸÒ[²’qƆƒû^¢î„°jæîšR!‡i‰C­ã«ákî§JÕ„¶I´ K•î{‰Î•ðW‡çfô‰8¸ïÉP,·†ïF1„”O"~ªcŸ˜œaI`¡lYu w݉ËPɨ†U÷Ž×3´)÷E->õöS?Íä÷Å÷½D-þª°Ÿƒf (Ÿ‡bõðÜí(†°€òIññSä,-9j¶€ò©Å",žë}S*¤O¤F@;«z4ddËÑHµüæÚe0÷½D-×°DÍP>ŲÏnCX@ùÔWà§:ö³´Xä¨Ùʧ‹x,¾Nú¦TÈvHjÔå:…œpþ2Tª¨AH›ué?v +Èî{‰Z,üUa?GÍP> EXö|Ùb (Ÿú fa‘lIš- |j±ˆ½yÖóÅëÉ'¸_‰FVÁæÑ-M&1¸ï%ß…Ò° ÅÂÁ}‡°LOb»QŒ`û^òrïÕJLZ,rŠQ>]%.ž[r½×ÝÝß,Zn§p][ê aå“sÃ,Hy¸ïqÁ¬ÖÄv£ÂÊ÷"÷Î_;»f´X8¸ï%êNˆvߟ³ºh© ŠÇãtï¼w7Ö34I³ž4„”O-¬_3Z,Ü÷x(²çËnCX@ùÜ1§·H‹EΚ- |:Ìa–З–Z¹ÜcÝOe¦å$[ šD¯f…-#†°€òI‘R{=>À¢Ùʧ¡,Ó2ØnCX@ù¡ ŠKX$[Šf (Ÿsˆ«û¾+V”ô¨…¶KZ6uå@‰Üõ–ø  9‘v (?º¦_œÄŠf (Ÿ‡‚K•u‘]fß[v£ÂʾO¨—°È‰¹h¶€òéDŠØ+ȳû[|?ižÍÉêáÎepOšzïßßÂÞ–*á¾—H‘Wû'R8¸ïÉP¬žÒºÅ¸ï%nŽ˜„…³¥ ;ÁÁ}/щqñð\ïûÎî[GùÕ8÷l·ïø0ó(‡{^áŒ`å§.”~‚¥àD ÷=а¾@Þ‹b (?É„:K‘) ºÜ÷H{y¶!É7¥¢Ý¸á 쫹£½xBMTçÈÌø½î{‰ùpåùRÍP>EX•Áv£ÂÊçæˆ© r‘)ŠÑlåÓ‰qµ5Ü÷“æyY_çfZŸôÌöÎÖ£´ï¾‘Áྗ¸­àª°_Œf (Ÿ‡"®îÜb (ŸŽa˜„EN¤(F³”O'RÄ´<‰A©8"Ýìí«í@|ZÀ€8ÝÑ¢©!~ß[ྗȹ!\U‹ÑlåóP,ŸÒºÅP~–åÇ,’-V³”O} 1/×Äîèyµ€¾^ôZ\ÙÑK_«ìœàEhýë+½Ë¸ï%rnWäb‘-pß“¡H딿Ÿï¥"êÄëd±’-8ÌÁÁ}/Q_`™µâñM©8øaòœïô¨q|͈óÕöïH£=vC¯J÷½DÎ áªc¿Xd Ü÷d(–+È»Q aå—]ñ¢øR¬d Ü÷ø*IŠ/~ºcß7¥âëI¡ÜÓ-ß¿íHUï_ÂÒ)?](ý‹CÊÃ}O†bù8ÐÝ(†°tÊÏ(Àq@–>Ûû»lû^&ÓƒpUA.Ù÷=ŠhV³e7Š,pßËt–D˜²â)^²‡98¸ïeSΰLNb¡)òô´§ÊþJl¿‘îSz°Çv]QÚ–ûc Q÷½LΠ᪂\²î{<€ez¼Å–NùÙºÎs—°Éæàྗ©¯!­VCS*n§Æ*ÇìX©Jž¯›iÒåó¦ø÷½L„«Žý4[:åóPÄe+žÝ(†°€ò­L¨S°H¶DÍP>õ5$·¸@}gÈiö5î&üúu÷üR&6vF k‡°€òéÖ¯?rKÔlåóP¸õlÙ‹b (ŸÎ’˜„%J¶DÍP>õ5¤^ªt“Z~hJ…ãYùö û#¶šß£É–]…^Ïw d¸ïe:À ^µ†—¨ÙÊç¡Xv ßb (ßÉ„:‹dKÒlå;]‰åé¡)™›ò¢LÒDº’4‚î{™0ˆ—ä„lûž Årkøn#Xྗ½ƒ®xõ:™$[p"…ƒû_%…ÅâKhJ…“¶£‚7½%º…ý›aå“sÃ,Hy¸ïÉPt+žé}ù»Q aåÓ1 q®°/'RœHáྗ©¯!õ òlû^hJE}3>L›Œz ;*ø%v?'­ Ïñ$÷½L'RÄËÂ>N¤ppß“¡딿ÅP>Ãç*Èr"EÉš- |êk,³G„¾3ÄPgû!EØG«—‡¦¿œ€VW5rzãP>H1‹f (Ÿ‡bÙÌ}7Š!, |2}ˆsd9‘¢dÍP>õ5¤´œ-7¸ÓŽ9k샿å¸ÏêEOÛP>H/ ûE³”ÏC±\Øßb (ŸLËç ²œHQŠf (ŸúR¯ ÏTT$¦¼Ñr²~¨•ó€pîŠ_Ïw°À}/Ó‰ñ²°Ï÷=мË^#Xྗc‚Ü[)_ªYo(_N¤(%kDùtÀrLOb¢TH(¸åwÑ”{‹{ (ŸÛ ÂÕ$†c5Ü÷d(ºçK8¦)/Š!, |:W#ÎT-õ]ý·ô§uXÈ:"•åšXS*¤úm¨×°Õ&¾¿(Óy÷öŠìÆ”÷½Ìmñ3,5¤<Ü÷d(–=_v£Âʧs5"ôÐa¶ÔP‚À’4¢|ºÊ,íä“×Û2kÇ^ÿóïNÒspßˤÈÇ‹Öð„¦<(Ÿƒ8Ö³e/Š!, |nŽ˜ñ|©¡†E»ྗétŽ¼Ú±úÎ÷eÖk}Y„aå“"/°¼ç–„¦<(Ÿ†"ÁÌ}¶óe7Š!, üú„z ‹ñ‹f (Ÿ¬#r¯ ÏvUƦTxg~Ýóèå¼êìý##XྗI‘ž/5d Ü÷x(Òry7Š,pßË­9b ÉíN€û^&§Ü+ȳ}b±)çÅ=^‹3¿ ýØš ÛE†°€òKJ?ÂÏ÷=дܾÅP>7GÌtì×P$[´;î{™œ–É à±)Ã]9¯nž‚ž8|q (ŸÛ .*È5ÍP> Å,{Q aé”_Ž£óÜ5,’-N³¥S~!§Ü û³zKlJŹÝwý¼kùõ¬c<Ç”÷½Bm×°¨çÜ÷d(ÜêJl7Š!, °„ÅõJÌI¶hwÜ÷ø*€eÖ!)6¥¢Þ¾¹5s©ïÞä<}Π]ˆ;Ä–XrJ?â)_ìy(üjçËnCX `Q¹÷–̰h‹Ü÷ø*yÕc?6¥¢.`~?¨‘väŽÊo&1¸ïj±¸†E[,à¾WZŸÈby7Š,pß+ÔWºÞÒ‘7“·XÔÿ¨ÃB-yÕÌ=êΔˆ©¡ývýa‹,©é×´Cå=,Hy¸ïÉP,{¾ìF1„Å–ÌQ̼ås‹…Õp€I9a2?ƒ5®[;X–Î;s\,b+óâÖPvH?¸lq¿¿[Ãz¯PEºè «ÀwOa¹)|7„! {ê(H3%}¢”¤±t4¨­"¯ú¼Ä¦NØ/žŽì/Ÿéì¦úÁ¾x2æFZÞEV?øáñÒŽ{…Ú*®ÐÀÒ v{2Ë&/»! ÑÇS#:í>Õ&y»·NÁ wº@^-âǦJÄS§a}”ÄgãÁÓòCŽ8ç„ð|FÀ;½“KCº*âk~ƒÜ­èË,²Ã;µ„¤)e…[Àuò±S™;¹aî5|Pà®Ã@™ß–V®{Óó¶Cú™áV<½Bþ éÒ²½…àá°'ƒ°¬©ìÆ0ÄÃa¯P3Hš2l—µ–F“OWÈy±›qðÉ‹uÖmã[`5Çä,F9xîî_ñp×+djpözxxëÉ ,;èïÆ0„NîiÆhÇò*+h4j$É«Mß±im ê]éêÜë™$Ó“[ ¥ûuíðÄÃW¯AºèÅÇ.W=„e/—݆€€Ð©$Mé(´¾rš! tê8(½*<ÛûO§™ðë-Ùþµê}×»7lA÷Å3¯ã:ÑñfÊ¥“Cºh÷vš!àtO€üæ§õ½†€€Ó©õ#Í4{óƧR§^ƒH^ÈÑ<¯ åõVÖò–;Ö(¹3•%dù2ôçøì"/½B> éB=qš! u„lÚûiëƒÝ†€€Ô©é#Í´yó–¯Rç ™çQRj‹Y¶“顺6-ács ¥-Q¡ýä¸èr`H÷xW÷ðÐãAÈËg­îÆ0z…Ú=¦á7u¼§{øçê/(8•xÒð u/­Jv¥iròkÙ5×ãѶzÈ>¶! uò^HÍÝxK÷pÏãAXd7†! õ¨ríçU¿£ãLç<¾BéEùÙFÕÔT1ÊxÈX™ fc?–Ì5[yœù.C@êdXpöqxøæqÙ®zíÆ0¤N-if D F"œéá™W¨§ ¸ÅcjúÂùY*É;Ù^/ý–ì<¸…}H¬ ò…SKÐ$©Ó äåbün C@@êÔÜ‘gºìµÍ:uÙ.ÈÔ”…Äé^Ÿ«ók­ÿ¢­~ËÅÞ³nÚi¥C@@êÔ‘/xíí:ÏT{Ù3Ž‘¾yàI‹ÐY²à‡…õ¹,+-JÓy-úîµ(¯VC½Ð7¼ztºÃ ð‹ôͯB^_eíÅ0Äu@¨Yc v‹„W¤oîwtwd¶8u-g¡õLéA‰ÉÏ/Lχ_ßCšûÝJlZò'@4C‚=BY_eíÅ0$ÔW€°;Ñ¡)€¥HYž³î½q6q›lë6ûmÛŸ,c´S`„HêˆXÓÔËO’á¡9’ìy–·0ì1„$uH¨K#O¹­ˆIä¡Y’:&ÖýÑ8´vRÆMMO8¯Oä¼·bšqú¼Ú“›Å!& v:”"_tÊ›CÓ¤ØÓ8”ekûÝ †˜€Ù©WcΣyn·¹bÒ‹¾³»®S“ln‡#<ûqˆmßø­¹_Ö•Lý,Ô8ÞÞë=¸zò•ÿ š¼¹[.W¯sÉ^#L<ÈtþtIó#&È“ŽçqX¶!Ú b„IÇ™Gg0á<ÑÖŽ”'«uùÜD†owÎ{Ǿ|7NN¿BíªËÿÛ‹‚bŽ'Ñü< àx‡åžøÝ †˜€ã¹ya Ïy¢ÝÝ “Ù·øÜt†w¶TÙöd0n_ß~fˆ 8>vaóÓû‰×<Çó8ÄÕ×øÝ †˜€ãÉ¡L9ÝxΓ yŽ”'qñý$w›+ÛC‹4ØÂ²¯3l2Ê?öolS}ǧ®m~Ê“ yŽçqX.Ïï1ÄŸdÀ$pžÍp|¢<éõàùuWêNÖ7ñ¨µšáõÎp|Ò<Çó8,wÊï1Ä_dÀ$qž$Íp|©ybz]x¶‹+7åá%>^ú¬7Šîi¾E7ÄO>åªVŸ4OÀñu¨¶½œ'{A 1Ç—½öbO;ÃñÐx²^C1™íÎÎÝÔÊÿ\µ Ç×àYbÒ9Þ@¹S[‡ÿõõfá2Ä$“r‰=4ד=ƒ[å“Ý †˜tŽ7öà f< Œ8sh<[Ÿ1ÚZxvbé§´÷ÂP‹âÃK×ñN?Iàxë›äùѹNs½s¼ŒÃò§»A 1ÇÛÐ×—˜DÆDóo)OV7-Mz°?šý»d WDóêÏ]™ó“ Ž·¥ÉkwV#O28žÇa¹ƒ~7ˆ&OwpÌxÓ[ÃysDŸÁñ<¯:Ó—&=>ưÞgäZY3Ÿ>ÜÚ‡û£àoÙ±~’ÁñÎÍ`‚\Ïàx‡ecúÝ †˜€ãç &òİ*œ}Ç»úŒ™¸œ'"=Ô@ä¤Éç7 ³5Ï]–/ÜêI–=µ8>Ÿ}ˆ 8ÞuÉó#&Èõ ŽçqX® ï1Äï tÛϬeã`í/Èàxº0™Ý¯Xšôðàzj]¬µMû‘îüàƒ ¥SB£nmˆ 8ÞÛ&y~8´ÉZÍup<ÃòÞ†Ý †˜€ã½è¶3¶ìU§ýïë3fz­~¶ÞUšô íšòøÜ{h² “"J-4iƒ>Þð 8Þ§ŽÉ‡<±šëàx/zÃ2Ÿì1ÄÏý s˜°­£ödp¼§&άsïÝøŸ;-âɓ޶IžŸ0©£/àx/zÃâ{ün#L 8>¨n{Áñâé¨=O×&³z|éÒCl‹Fšë­†öò[¿=)ÛÔHxkŸËLÀñ!6Éó#&ÈõŽ¢7,sü^CLÀñ!õy”ò$}È1tÔ‰ާ Sk+¥IòQƒ³i﹎ÛÒäÃWi{î¿1$(àøh&0щŽçq@[ýÏÞçÉ^CLÀñÑž1ù4wq„Õ‰ާ `2?w‰ôp¼¾Íúê^»_½uí.5ùâx-\Àñ16ú#&š'àxs¬Ï]{A 1ÇÇÄAÌ`Ây¢=O=öXÆä«Ë¦‰O½c':DºNÈO:³¶jÈÚÒQ ÿ;ˆÙp|2Mòü´îÒ‰Žé„ÉÂ{ü^CLÀñÉöyô î‘°Ú#QÀñÔ#aÑW?©i•&=¼Ø¥ÊðbÅÇŸÏW½9Õ´G ü'&áǧØ1ù'è‘8>‰Þ°ü~²Ä“p€ãS‚–~ÁñÜ#aaµp<]£a2ð_é'‚ô–ô¶‹æà]4>¥åÞ¼ÏÚp|>š ýÑ$Í p|½a™ã÷‚bŽÏ†ƒ˜È>$ÓÂm!àø\Ÿ1»Z«/ÝÒÊ·¼÷ÊÑW??‡›¼Ñ#v{çpp|MòüˆID àxcWµßÝ †˜€ãsä f0akà¤yŽÏ”'½V?{,Sé[7ú ×5„5$ÎÑzòÑKæ´ºÒL1Ä_Ž L’æ 8>‹Þ°¼îÚ bˆ 8¾˜YLøxL›4OÀñÔ_Ð0ÉÓGüÞšô ‡PÉáNA`¹Îóv§ú曹2qŽhˆ 8¾„&CÄDóÏã°ÜW¿Äp|‰ÐÒ/ø$Iž'Ÿ®aWýæoMzJPzµˆî=¢ÀÝê5¢ôj¼)01àøRšäù ôyŽ/¢7´ó˜ò¤¦µÄÓ9ÞtÛ Lø`L ç…`:Çó5ìªÑM¥‡çóô¶{×·]¼‰5mâùŽOLçxK›®1ñˆ¡s¼ŒÃr­~7ˆ!&˜ø3&ŸNfÈ\«‡ùB0.Ÿ®aWõ“[“¤·¶NÂuOÏq{­ìßÔs[~Ã$“Ôdè˜dÄìyz]8ÆIGÝ †˜`’9ˆ >É\ƒ„ÿB0˜Ô îž{kÒÃíÕö]RP©%UŠØ79Ý[ŸÎmÌñ¦s¼åv+LŠæz²çqXÞë°Ä“ÎñÖ¸>^aR¸Y4O:Ç[Cy²ê³rë>Wüøœo¾ÜôæŸ]3m™?ìᦓÔdè˜hžtŽ—qHë¿Ä“Lò4&…mœ5O 0¡<éuáÙ½·~ìN½}}›reû´\ñ‰ío­m2ô‡ ‡ó<ƒ5ö<ËÞó»AŒ0±àxò€03}õîðŒIÒx:&Ô,bWûêoMzhîüª+«{ü‚üÌãM½+Xp¼M“VÎ83Xp¼½a9Oö‚bŽ·ºíçu—ãc0|$‚ÇÓ5€É¬ÎxkÒƒ—•Ì·Ü~òö:$´óšWøPû ïL ç&ð‘oEoŽ7ó¿Äp¼³}½q•'ì#áà#,8žšElYΑèV=ë yV’{8Ÿ@q6¯$_Ë[ûÊp¼‹M†þˆ‰æ:8žÇ¡¬öAî1Äïq­i9ö‘pVóOÍ"®×êͤ¦ukÒƒ„@ƒí[,R™¨ÔðÕð‡D¶dCLÀñÞL`b5OÀñ4¶×êë|É^CLÀñÔèa¦|èÙGÂYÍp<õ¸^«Ÿ­ ßšô_lòúR7}sôã¼îÌœ–mÂ{_¢;ëc“¡?b‚<Áƒ]î«ß b„ î,5z˜™Z½c ‡ó3,îøÀd~îéÁß»ý.‹vdšØïÜÿ~ÆúX?Å]}¨šäù xaxÜq v¹V¿Äp|0Û®òDNÐÀ.w–šEœ]~gÄÖH~ÐdKçE5FnA}õxo4]1LJÐ1ù°FŸG€Íƒ]>#v7ˆ!&àxjô°3uaLJh8¯yާþ·ê·rkÒÃÍp½ûØ:Ü>øpû=‚BÇ}õ>w6M†þ”'^óÏã°ì³Äp<5zØ™½ŽÏÑpÞk<ùt `2{ôø­Iîvrÿ:}¾Ÿ>?ôóp|ôMòüˆ‰æ:8žÇ¡ïu˜v°Ý bˆ 8>b"O|dLŠÆLê3æüb ò«IÔI€Û~égœ>›Ï˜ÀçÎÆ2 Óð¹“qXöÀÙ b„ |î*#Ïb§i8§àsg©YÄ…E>ùjÒƒ·§Ûv§Ïþô9\`ŽO¾ÉÐ1AžÀçNÆaÙg7ˆ!&àøúzãΜ¨àsg©YÄ­ÚÓ5éAÎ+l·Ú2?òÛ×ÍÞ¾†˜€ãSi’çÇc²4OÀñ<˜ì1ÄOÖÍð Ÿ©á¢Óxòé..ÖV¾šôPoRB€ ÙíÑgÏ5‰';0jÿûwøÜY²´¸ÆDsÏãÐõ“é½s»A 1Çgß¹í*Ob`L²ÆLê3æz­~ÖSí«[_õÚЋßvuZþ~rÈç¹ O–ö¢Vï¢æ:8>‹Þ°Ì'{A 1ÇçÝö"Oí?qðÅð¹ãk¸Õcb¿šô oYç-ñÃ…j«O=¨ð®ÞŸ;ËíWuaàs'ã°|pïn#LàsgÉ—ÃNÕê×êá‹àsgK}ÆÜj­þ«Içrv;Þ=¶“ Mæ Ì|Æ'åÿƒÛqG˜€ãÉâä:|îdòj r7ˆ!&àø’O˜|òMu‰kõ8`#ÀçÎR³ˆ[õFÿjÒË߲žÜRp¸vÛ‡o;þ-ïÓl6ü¼CsˆIçxG–,CèvÚçŸ;‡eoôÝ †˜tŽw„„ªÕóN{$às稿@1™ìïúꆤïºÃ³‰v¯?nÏ^ʳ­”7Ä$“ÔdèOy¢=ð¹“qØÀd/ˆ!& ˜ä>^bÂy¢=ð¹sÔ#áå<)Ýš×øoQâÓô” |ñ­~Ÿ;G=—˜h|îxܱZïÚ bˆIçxgdÀ„{$œöHÀçÎQ„_=¤ô«I?ïYÚ:»Uº_d£Jk5aŸ;G=öªV¯=ð¹ãqpË}õ»AŒ0Ï3y“ÂùZ˜”&ós×WßNÓöÒ¸_/ßëdjÆùjIë0éïøÿWÇj|îx€Ét/ÑnCL:Ç;+óèÄQ¾Ü#áµG>wŽz$¼]®w‰ô Ûšžë›i;C$4+™:Q×i³þüëÛÄõ p<õH\c¢yÒ9žÇÁÙu>Ù bˆ 8ÞÊ<:ƒ ç‰öÀçΑ¡†wËïŒ.™/ºgÏ^óŸjÜlïCž¿¾êWÔ›þ8èÃp<Éêî¢Vïæ 8žÇaùüØÝ †˜€ã©¿aÃy¢ýð¹säÁàýòûɳ—ŠêL[ï3ø_O¾ä¦X?¼¢ˆò)ìãµ0|îœë’çGL4OÀñ<Ë~+»A 1Ç;™Gg0á<ÑþøÜ9ò`&³úÉW—Jo± ½½À´e~à¿Ò¡ÿØX‡Ïó]òü 3zøHøÜÉ8,ïuØ b„ |ytæhxÎí/€Ï# ¿ÚÃ}oÒC‹‚í4 ûë•NQøÓ0ÇskÀE­ÞÃG"ÀçNơׅíì>­Ý †˜€ãÉ?ÁÍìuðì#ᵿ>w| `2›'÷&=Ðî Ó*G‰ Ùò×'Ôlûë{’Œ÷ŸÀçÎ….y~Â^>w2ËuáÝ †˜€ã¹¿a¦.ìù¬ ¯ýð¹säÁàãâ>­{“ž¼P[ ²Yöqú WUëo=Ç=ð¹s¡Kž1Ñ\Çó8ÄU>Ù bˆ 8žûfz¸=ûHxí/€Ï# œÉºð½bõžEùyž¿È+}9²ÂòþÀ!&àxn ¸ÂÄkž€ãy–kõ»A 1ÇG;‹ ûHx¯yŽ' ¿ÚWïÖW¼º——«;ïé?Ì÷—ĨsÁM >w.vÉóƒoª‡D€ÏŒC^Çd/ˆ&ð¹säŸàfjõž}$<|$|îùH“Y?È{“êLë\Klø\ºò-çÓ«/kÆŸ;Ç­Þè>>w2\ƒ¬o[Ó{±wƒbŽO¶sÛ&ì#áá#àsçÈG—e޽5êx´p"?_¢>èWøtCßk®CLÀñÜpÑWïÕG>w2e}ݵÄp<ù'LbÂy¢ ð¹stÖF8ßïMz¸1¶Ãô")?GÒ• í«‘-7²À¿1Ç“õ€»¨ û¨yާqðËuáÝ †˜€ã3¿üÎ`Âgmxõ`€Ï£Æ„°z†À½I¢fy²B»g9“êËQÞ‚&OÜ›$|îY¸‹º°š'àx‡Lö‚bŽ'7Sö|Ö†WøÜ9jL«gËÞ›ô@¯¹¥ôË7WËÊzü-9t„ |îY\b¢=ð¹ãqðËgËî1Â>w®È<: ŸµáÕƒ>wŽ´ì`k÷¾uã$n/0^^µ{þÔЈ-‡>w>wެÜE·×žøÜñ8x»¾îÚ bˆ 8¾¤iL8OpNE€Ï#-;¬öpß»õ&%ÛŸ5žð¬ß¸â›<éïéxUÖžøÜñ8øe¿úÝ †˜tŽ÷‡Ì£˜dΓ¬yÒ9Þ“–ºßÊìž {—¯HúêñÑy’l1|ûbâÏÒ³>Ä$“Øäµ˜hžtŽ—qX®Õï1Ä$™Gg0á<)š' ˜PžÀ}rïܽI’ÌrÛ¦¿ð’Ž}£66ÇVŠ5óK¦~\«‡Ï§ã.1ÑžøÜÉ8 .<{ónCL:Ç{j*˜Ã¤pžÍ“Îñž´ìÐk³ç:Ü›ôpð ×7߯{»ÿæ’ñÄ߽? 0ÂçÎÓñ þª.Œž‚Ÿ;‡å~áÝ ˜DøÜyj*ðSµúBy£ñ“|ÂdV¿7éá\¾C8×…ó#|î<1á/Î1 è)ˆð¹“që뮽 †˜tŽ÷¢LôÜ…Ã1&Qã阖ââ{ü£I-¯L(S®LéÕKy¹1äp<1Á˜|¨w…Có¤s¼ŒÃ²ßÊnCLÀñ6McÂyb4OÀñ¤e‡´8w=šô ¥Úˆ2tÿ,˵>7ÀÐÌ\Ÿ¯:§ 1Ç“g¿¨ £yŽçqèuá2;wí1ÄïX'œ˜»{£yŽ'-;¬öÕ?šôž­¯ã+´™‰›@R¢ˆž†×ûÎÛ½Ø>wž< üE¿p0š'àx‡å³ewƒbŽw2Î`Âyb5OÀñ¤e‡¼Ù b„ |î¼—ytö,ð,ˆð¹ó¤e‡^ž}?y4é¡føOdûV΋§²‘ÎÏïp®z£?šôø´Çï·í¶dIìÚ{ÈÛñLÀñ¤Çû‹dpš'àx‡Lö‚bŽ2Î`Âyâ5OÀñ¤ÇÇÕ~áG“*ãÕÅ Ýp „×.õƒ,Yĸ·>eÉ5n CoŸ;Oz¼`òáýÄkž€ãiÂr¿ðnCLÀñÑÎbÂz|ðš'àxÒã£]^ çnÍ["©k›Ôn’($Ìyðbrü~Ÿ;Oz¼¿èÐã#|îxB¯ ›yLö‚aŸ;O:´Ÿ© ÖãôøŸ;Oz<0™Ÿ»Dz¨OMbiˆv6q?mûÓ¶ˆêڥŘßxtFøÜyÒãÃE]8@ð¹ãqË~+»A 1Ç'™G'0a=>@ð¹ó¤ÇG·Ìñ·“•b jX‚b'¡Ù1ŸÀçΓ‰æ 8žÇa¹.¼Äp|JÓ˜pžDÍp<éñÀdvü£I—¥¬fÜñ¦‡;ÂçΓ.êÂ!jž€ãy–Ï1Ý bˆ 8>sCç &¬Ç‡¨yŽ'=>®úp?úÑëùÛ»m¼7S25(+¼0ðOŽ0Ç“.êÂ!jž€ãy–ýVvƒbŽÏ2Î`Ây’4OÀñ¤ÇÇU¿•G“Ì­­â“|ø¢Ž›J•‘5ˆãA;Ê›rR¸a}€ |î<éñ—˜@ð¹“qXîáÞ b„ |î|‘ytÖãôøŸ;Oz|ìuáÙ^¢G·¾º5éá9 Û‚Šúkx´>ÇwµøÜyÒãÃEw€ás'ã×ùd/ˆ!&àxÒ¡'1á<ásçI«>&=ü@«š6þj¢Ý;È·Æ“ã^¢Ÿ»@zdÍ“ÎñªüÀd–OžMzGß–ÁïW5´:ùihw:R¡…7˜$`›¼ö)O²æIçx‡eoôÝ †˜$`"óè &œ'Eó$Ê“¼øÎøì[7úãÓ ŽÖEøí‹ÒZÞ­»àsH¿Ä¤hž{‡¼Ê'»A 1éH‡žÃ„õøP4O:ÇÒãcYœ»žMzd&ӎIJ)ÏÌ–üÎÜ&…Ç­^øó |îéñáª.¬z<|îdÊê;ãn#LàsH‡SuaÖã£êñð¹ ¤ÇÇU¿•g“tÊ-ߦÜà[±›Îën¤o8>wôøpQŽªÇÃçNÆaÙÛc7ˆ!&フyô“Èz|T=>wÁjžä…¹Ë÷Ö¨Ì$)oY°}²·óâ.ÜÔ¼!&àxÒã“z|T=>w<qùÌÌÝ †˜€ãmšÆ„óDõxøÜÒãê“ý]Ïn}•›¯˜ïбûïÖ¶Sv†˜€ãIµú¨z<|îxâ²÷nCLÀñŽ÷¾Í`Âz|T=>wôøÔk³½©Ï&=`;@àÞ×Ñ·Ì~qÓ4'«™ôͰà&àxÒãÃE]8ªŸ;‡¸ì·²Äp¼“ytÎÕãásH&óy"ÒÃŽRHz· Ú’Ó›ásH=ÜQõxøÜñ8Äåºðn#Làs¼Ì£˜°U‡Ï] =>áÌÌÉZý³I"ŒjP%'‚úDžŠ©oûVàsHuá¨z<|îd–ë»A 1Ç“=‰ ç‰êñ𹠤ǧÕóŸ*= \æ ÄIƒheuîK´Kpè·ásHuá¨z<|îd–}$vƒbŽ2N`Âz|T=>wôx`2ÛßõlÒÃëø–ÞöÖ-⤘Ú-âì«-Ž0Ç“/jQõxøÜÉ8,÷pï1ÄdÁ„óDõxøÜÒãSX®­ˆôàåx0Þ,üé±â¯[™ŒÎ CLÀñ¤Ç_b¢z<|îdÂj½k7ˆ!&àøhg1a=>ªŸ»@z<0™Õ´žMz¨K–:Sܳ®N.¦ºÛÏЇ6Â>wôøxQŽªÇÃçNÆ!®¯»ö‚aŸ»@:tœ© GÖã£êñ𹠤ǧ^«O“ÞƒÏ&=œ íñ8…V(45´!&àxÒããE¿pT=>w2˜ì1ÄŸdÀ„õø¨z<|îéñ)-îÓz6éAÿ[zKhÏt íÅ/Ë#LÀñ¤Ç_c¢yŽçqXööØ bˆ 8>%h„Ÿ}‰"ëñþû>w|”—×Â"=û3½Ã›Ð^±…ö;&àx²uá¨=ð¹“qÈ«¾D»A 1ÇSSAœ© Çh“ ñtL²=a2{îÜ«[_Éã#é–ûFw‡¹Nô<ÿáý>w¬óãE]8jO|îd–ÏÌÜ bˆ 8žš âL]¸b@˜$Íp|>çÉ,Ç¿šô`Ù•Dˆ{Í·ç®Øú¶>w¡ “O5Hí)€ÏŒCY­Aî1Â>wš æ0Iœ')h<Ò²S¯ Ïú½šôð’Œ9½ÏÝ‚”ùaн6Ä_B“w| `2»×áu::¤ç¼lœ=».‰ ?™Zt2•÷­Àç.Rk@ºª ä:|î8†´|Žén#LàsÉÿ?¡.üá æXc’5`È/jQ?y5éáàgçÉ÷ìø„`z1þzñ–ó<ÎøÜEj ¸Æ¹Ÿ;œ™ù³—ó}žì1ÄÄ“rÂäÓ^ìtÐÙM þû>wÑRžøe>éÁñ¿níåªÆ"›þ‰_tTnüÐübާրtQ«O‡æI°çqXîáÞ bˆ 8žüÿ›–þyîJG`L’Æ“O×È«úÉ«IwÈ ¯n¼*¬¾¾Ï ¯±~Ÿ»H²zº¨ §CsÏãà×9~/ˆ!&àx›û%ÔLð¤åâ×i)§åJPcïÁŸ»èlÇä½ö›Œæ:8ÞŠÞ°Ìñ{A 1Ç·þ† Lø ¤ýð¹‹t†€`R™rvŸÖ«[_=¿=b5Ãe _㪫™/iè2ô¢u»½ñáŽð¹‹$«§‹Z}Â>w2ËÞ»AŒ0Ï]äþ†™Z}â3’öÀç.ÒyÕoåÕ¤‡[¡¶'™‡åOÓS6žó¡†ËkÌ'ð¹‹$«§‹ºpÂ>w2Ë>Ü»A 1Ç{Õm/ø„ÏHÚ#Ÿ;¾F^õF5éáÆ=ºð{ëÍÑkz´×ɾÛ;Ÿ»H=ט ×ás'ãÐÏgœ>{7ˆ!&àxê H3úI≤=ð¹‹Ô#‘ÓòZ¸I†_o-U·¿Ðõè®–rÖd!{Ëx¼ë%‚Ï]¤‰tQ«OÚ#Ÿ;‡eoÝ †˜€ã©7 ÍÔê÷H$í‘€Ï]¤‰¼zfæK·nÔ5÷ƒW-ÛóÑñޱ }µôâñƇ;Âç.RDº¨Õ'푀όÃF­~/ˆ!&àxê H3uáÄ=I{$às©G"÷îIí7]zàJ{ n½‚רʙéPå8Ôã|î"õH¤ oô„‰Ÿ;‡Õîí ˜$øÜÅhg1ቄ‰Ÿ»H=åX{?©ÿ’H²R!Q—möïç„Q)ï«Ù~¿aާ‰tQ«Oè‘Hð¹ãqÈ«µúí †˜€ã©7 ÍÔê÷H$ôH$øÜEê‘&“½Dõ_éá–y*Î͹®cêhÞŸíÏGæyøÕŸÁaD‚Ï]¤‰tQNAóÓ “ÙµðvCLÀñIæÑ L¸G"Íp<õH”žúú/¹Þ^Pɰ>\Ïa5á×óët®a?#_bާ‰kL4OÀñ‰Íîû>­Ÿóï{Lö‚bŽO2Î`Ây5OÀñäYÐ0™^ ‡£IáK‘o¢o[ê»ñ¦Ïçè†u៻H²zº¨Õ§¨yާqÈ«=ÜÛA 1Çg{Âäc ’= RÔ<Ç“gAqkë®p4éNnù¢{–“[î±Þ¸1Sq¿ìWë6ø øÜE’ÕYòü Ç'x$øÜñ8äUoôí F˜Àç.f™G'ò„= ú |î"y”E¿•ú/‰ô`õ¾`—O7_W3YMb%ê!&àx’ÕóE­>Á³ ÁçNÆÁ-î1Ýbˆ 8¾È<: {$ô$øÜEò,(‹µúú/‰ôÐ6*í\Ã|è‘߯ÒLãߺñ{|‚Ï]$Y=_Õê“æ 8žÇÁ/ö o1ÄÏý s˜pždÍp•Eoôú/Ȧýi’£½+ ʬ[ßt¾¾z©µ÷ 1)À$6ÉóSždÍ“Îñ2aOö‚bR€I‚n{QƒdÏ‚„þ‚Ÿ;¾F‰Ëï'·¾Ý?¼h“-ð¹fd¤ÛàÜv`U}aŸ»Ä­W˜Àw!ÁçNÆ!.êñÛAŒ0Ï]"ã…w2«=ÜÛA 1qÀ$r3˜PÏ]>˜Pž,zà„£IñÞêrõVã“5©qó@ñ~Ñ üd›Þ×L:Ç'n ¸¨ çCó$Øó8¬úpo1Ĥs|"ã…]˜Ì¿Ç7é!ssš¥}â#NíðÄ« ¶i$ù†ãás—l—È`šôP“õÒ²Ya>¨êHí˜6}Ê,eÇs|î·\Ôê³EžÀçŽÇ¡¬žÏ¸Äp¼? Û^Ì]–óý >wt ôºðä>­ŽHbneÅñŠÏ—´\?ºóD@ri;mîï¼Ñ|î’ï’çGLëð¹ãq(«=ÜÛA 1Ç{ÏALä‰ ŒIÖx€I8a2«i™&=ÈãCEÓº´ž¤›w¬ÌÏ´€<µê 1ÇskÀ…7z¶šëàx‡²zfævCLÀñ¾tn»ÂÄQoj}ü4žŽI PÙóì<ýjÚšöoÖ]ð¹K¡Kž1AžÀçŽÇ¡¬ú­l1Â>w)bÏyâÆÓ1‰”'nñÑô­u YZ†ÿX=z¶_’3©7"¼ÉøÜ¥Ø%ÏO˜xä |îdÜê;ãnCLÀñÑwn»Ä„óÄg˜Pž,öpÓ¤Ëk‘òÛÑ´IóÎç³þ°åaŽyÍp<Ãr­~7ˆ!&àøXf1 œ'AóŸ`2½Ç4˜¾uƒÃ!Œúgj —vNX¦‡îöúæK6ÄÏ­}õ9hž€ãy–ë»A 1Ç'ÏAÌ`Ây4OÀñ‰ò8s¾D5‘Œ?õnJ…]ÝÅ{?÷íN÷6K1ǧ.y~ÄDóÏã°zfævCLÀñIæÑk=>GΓ¨yŽÏ”'qùQ¤‡Û«ÝvÛÖú9Ó·5ýØr3À>w ­Ÿ0‰ÈøÜÉ8¬ú­l1Â>w)Ë<:‘'‘ó$f˜Pžôºðd_} çÖ[Õyì¦í!Ói»?óçp|î’ç§÷“ˆ<όê÷vCLÀñ¹Ìb’8O’Óx:&…ò¤× '÷ŸÔp¾zkTä:‘؇[Ö«Ÿ–¤zMN\L Íů®†˜€ãK—w4ŠÉìÙ²ÛA 1éŸÌ£3˜pžÀ³ Áç.›š'Æ,ÖVlߺqšrÛÔYŒx4ë+q~—f¶7|Ÿ»LžåªV_4O:ÇÓ8Pm{ñ=~7ˆ!&˜”ILÊAyRÍ“ÎñÙRž,z{ÔpDz®µsÈ{næ’Ýó§Œb\_·ŠnfbÒ9>“gA¹¨ —Có$Yª£.Ö»vƒbÒ9>Ó^ý2ã·Rس À³ Á玮qÂd’O¬Ju¶U´òäë®o§¯?ÞŸ!às—mj’çGL4×;Çs ‡]Õ´vƒbŽ·™ƒ˜ÉÚ‹]àYàs—m}ÆÌ¢ßJ Ç!œxºíç)œtúúë&ð¹ËÎvLÞ¿3x$øÜÉ8¸Õu×n#LàsW§YLس À³ Áç.;Ê¿Ï]v©IžŸòž >w2«~+ÛA 1Ç»ÌAÌ`ÂyÏ‚Ÿ»ìÊ “Ù÷x«ÒÃKÛ4•®­´) ­I|ñ“8Äïm“×>a¢=ð¹“qXõ«ßbˆ 8Þ»>^Ô {«yŽ÷”'ðáž^w­¯šnêm#Û]_:Ê×ó[LÀñ>Í`¢yŽçqXõFßbˆ 8Þç&óÄrž8Íp¼§<éuáYýÄ6é¡Ýv<Ýv9…“Ú¢þ™ù=àÍÜŽ¶IžŸ0ÑžøÜÉ8ÄÕ=A»A 1ÇÇAL`â8Oœæ 8>ø&ó|"ÒÃ+ôÿÅf…ñ|};R¯®ÌKç‚&ð¹Ë!5yíÇkO|îdVÏ1Ýb„ |îrytÎÝ(Oz­~¶GÂ6éˆñ«ù/¼nýC¡SÁ2{ÅÉþéŽ(CßÔŸ»mÇäCžhO|îd–{¸wƒbŽ2N`â9Ot¿?|îrô'Lfë¶ؗד»¨}½9€]~̽ñ«Oð¹Ë15Éó#&š'àx‡¼¾Þ bˆ 8>fè¶óD= àsÇ×0‹~+5‘Dj‚æVèúZEË”Øþl“3¿wEŽhˆ 8>™&y~ÂD= às'ãPÖß÷‚bŽO–ƒ¸öè,ìYPÔ³>w9ÕgÌôºðäYåÁ6éAYÑë=ëÝÏ/1ǧ8ƒ‰æ:8žÇ¡×ê§= vƒbŽO‰çщ¹‹= ŠzÀ箂óGoW½=lߺaè °A fôÅ1&ð¹ËÙ4ú&êYŸ;s¬¯…÷‚aŸ»œí,&ìYPÔ³>w9»&óë.òãž©áà÷/>Û‡˜€ãsl’ç§u—zÀçŽÇ˜,¬…÷‚bŽÏ º­ëá˜Oس à\‡Ÿ;¾†]õ[±Mz{÷×úù÷?åðOZÊÜÞé'ð¹Ëh ø„‰ú.ÀçŽc0½¯þ·çê=Ÿì1Ä_ 1‘'|®CÁ¹ >wuR®˜ØÅÚŠkÒÝgiÉ&&ϧQA¢s'3X7ö%‚Ï].]òü4w©ï|îx̲ßÊnCLÀñ%öõÆ%&´w®dÍp|I'Lfkõ®[_q û~>b}¯jŒ‰¤¬ï`u*‘0ßñIçørtÉó&껟;³¬Ÿì1Ĥs|9 tÛ‹µ0ŸëP´¿>w| ëþ°tž©@§²„ëú©>J†÷ÈömÌxʘdøÜ’Õ¯1鹞ás'ãàV÷3î1À$Ãç®Pƒ™ªÕó¹ý>wå¨Ï˜õ‹|âšô çK¾x[ÆóIQP‹Úéþåù2‰ëÞ» Ÿ»B²º¹ªÕÃw!ÃçNÆÁ¯¾3î1Ĥs|áþ†©Z=ŸëPÐ_ásWW¢'Lfûê]“êkïcô(Ùû·(ê=í›óã3|îŠé’çGL4O:ÇË8,×êwƒb€Ià f0á<)š'˜PžôZ}žôŠrMzø…sE2¼Lùé“LJÇäýºË‡&J²çX6ÁÙbJ'ù nb1\C¡L©ÿqQ>]°ÌZx¹~úº£.ÂÉü÷(啌­3†°€æ­k}H•„æ{çyŠî¹ò[¾¿ç”½(†°€ç¹ÇaƧ†–¤åÓUl\¬°¸&@1š_ö9Èÿ:6xæÈàöf§i†Û]¡N >?tyÕ z)2ÃîN†¢ÃòdÙk7Š,°»+Ô"`ü„ÜXC) š%2 ï 5KØ´øBïºqÚÜNü—MÔo`ÙS³Ä5,è–Èp¼“¡XîæÞb Øžº œëe@ßd ·KÔÿD(Ÿ®¢°L¿«lÕD#ÎtáÛä0„|OV ,I†)Ó;Š~¨iŒsæõÛQ aáSׇ SÙb2ÃbFX¨$ÙK÷³¶w®)õÇ£ùþ0=¿eýñýÂÊ÷¦) ÔÅù«)Êç¡XÖSv£ÂʧÆ3ÓÓ]Cq‹f (Ÿ:ìjW·kbD{˜â÷ †Ü»®‰ ç»âc‡åå[ÍP>ŲËnCX@ùÔû1 ‹d‹ÓlåSÓ;–ai~XbMÖW/ñùý™{|çËñ‹$ÌïJ0Mý º?2Üïx(ìr·ýn#Xà~W¨ýä©IÌI¶à¼‡ ÿ»B}€e~{t9õçóä¿aýeÍþw%Ä&…~†Ù< À2-víF1„”Rç¹Ëlq’-8ò!ïPë3Ë%cQ%Üé¼C2‘á²+ªwûéî'_ÂÊf¯ÙÊ¢E,ê]»Q aåSˆ™9w¶~O²Å(Ÿ®âVÝX\&âs|ãñµ (ŸÌÌE%¿¡)Ê"GÈJì˜oÙ‹b (?FŽb&[|bX¡–t‚e¶Hé›6A+ÊÑ'» (ŸüìE“w BS”OCa—–Ý(†°€ò©ÄΜªYC±‹f (Ÿzœ[Ìßä‰äß±bu÷ìÉt¼ËØárY`µúãë$úA2üðd(–kú»Q aåSCˆ©é×P$[bЈ:,Ô‰Xf+ȾÒ~â)Ê硈«“»Q aåSgÈ,,Q`ÉQ>]e–Øk¯–ÿGû±q¶À¯”< ´7ƒ<'7`/òkHùæ×‡by—ÄnCX`ÉÅ åg‘ÁŠfK,”-(ìOöû¦Th<´d”øà¤ãöó»·7“Xê°;KÑlIö<y–½(†°¤‹q}B½„¥H¶Í–Ôa¡^‡Âþôëä­¿ˆwz¤â)ˆgëL„céØº07›<º•ÔôëoùE³¥ØóP,{èìF1„¥–< g‹94[ `)'Xf]Ù|S*ÚùAý~/ÿ:‚¥9åÕ[±¶ ¥Ÿ²ÅhŸH³ÊkCáz¹Ì¿åïE1‚¥YåQ®óÜ,´`X’FÔa¡î¿z@­oJÅÏQïC@Ë*+é̦ï`åS‹…½ª m±hnym(Ë´sônCX@ùV&Ô)X$[´Å"€ò©ÅÂ÷Âþ¬Þâ»Rñ$ÛØ:Ìd{oB×ý‹Ì±ÛJ†=7d[¼aåS‹Å5,Úb@ùVÔ‰åIl/Š!, |nŽ˜ƒEZ,Œv'P>YRøU;ß”Š[ß4XW‘úùyúüêÎåúx‡°€òI‘gýúSMÌÍP> …[>s7Š!, |nŽ˜*ì#Ù¢Ý ”O®€eþuR”Šü" )ê§r™?Dúœ |¼K½~¥þ˜X aå“"ï®*ÈÆj¶€òi(Ëô£Ý(†°€ò½L¨3°XÉíN |2¦ð½°?Û¾úY$'s³Ö˜xzÈê«Y~¶m¸–·I`‰ |Rä'`A¶DP>…[­‰íF1‚%‚ò[sÄ,’-ÚAùäMXf÷O„¦Tжœ{³ø5îûë²9mæ¸íX@ù¤È»‹^ý²%‚òy(–uv£ÂÊçæˆ©Â¾q’-ÚAùdOá{ÙNÚ{‡¦Tõ¸©Â¾‘ £- ”O-~õ°‚Д ø/£kæ'ŽÀbÚo¥ÂÊÏ](ý ‹¦<(Ÿ‡b¹c7Š!, |ê+hrïU¶H‹…Á™9òé*g¦Nê-¡)‹çc¯h!yãÞÓÉdÒñó¦5<òs™EûD(Ÿ‡bÙi7Š!, |jqS…}“xï¤Ñ‹ʧîÀ2[ª ê´5|ó’9!Üx/O]÷ç_Ïø®‚œAù¥ ¥ŸaAÊgP> …ïäßZCßgË^#X2(Ÿ›#æ ûÒba´;!ƒòÉõ!¬:䇦TˆÝƒë|Ù^ý—œæ§ã÷– Ê/](ý ì7råÓPøåãlw£ÂÒ)ßÿ†›« gɘWäÜ)ßñC0Ë“˜(bôâÓÝí£=U–Ÿžæâø¼Ó2¥S~î”o¸­àÂx§¡ÙÒ)Ÿ‡Â/öw£ÂK˜‡E²Eý+r,”-vyÜ,·ä {µƒ–äH¸[¦““<¨ÿ aI€¥ ¥³E ,r²§¡ðËF/»Q aé”oȸÁÍö‹d‹ZXäNù†,,Ël‹E ¥â€:Á5¼ƒ•;ª#ññpÒ~dŸ?êùX:åÓ…Òϰh¶tÊç¡ðËzËbŸa)€E&Ô)X$[ÔÅ"—|¾Jw¯™Ý¤K ù'›yr<ýçET×ï…2—ËIÎΊ ?neô\þOoÅÄ~+¥³•P’zQ„Ÿƒkn³jãP:ýKϼ_+¡ÐU.| +‡r|ÂÁo¹þöî9äŽ~+¾©ÐŸžy{à™/Μ‡Âÿ!\{³}Bf¢˜B\‡ÅŽbâ™·GXŠFXâ –É—Â1]ÿ5,°ˆÜéê;û"Õ…õ…*›ß‚2ü¬eŒ‚%M}_Ξvxý¾zÿ–ÊÙ‡ªGöχ*4¨M•SÇÚÕpŒD¥Ô—r•¥ŠD:ù‰ˆ¢í÷bþŽòS;rÑ>ë#óWÏ¿þ÷ÿŽnåVûSA€Ç¸_ Ãcùb›úþþoÿþ¯ùZióA{ûÜ¥£ÿóõÿîÛÆù—ë”ð¿ýïÿîºýÛçã?ü¡ÿ’=ýRtŒâþÕÿþ§üÓ_þ‡_ÿݯõ×ÿñþÓŸþòëoÿüó÷÷wú›¿üéoÿü.Ïí^fôºD“ßý÷÷?{ð«^~ÕЯÖÅÈÒ?èwë”åÍi«ÊÊ?åWmâŽòµ6Éïº`¨ô¼ôÏ2!G–Ýjþ®ý³¥ýn¦Ñ +ÿl>ôWsÍ]çÏ*ŒÌC?)/J-´©’\„9J…;w'I’ƒþ÷?Ö[xüéÿùóßü©þçÿæþüŸÿò÷ÿð‡ÿùïþöOÿï¿>~ý›?ýÝúËÿõ¯£oW-¾_µÔ«^Q׉~KÐ_,uáº2”%¶ßõ±&HYJY˜É¯ò(´÷usšÒ+’9žú¢ŽeiÏI´%ÌþÁÈXÊÀ¡cùWõnû·õ¾þñí¿ÿú0|‘RÿÙ~ºÒQmŽEcTMd…¬õ*íV*‡’“1/†p‚¸9M~S¸šÜ.kÛÓjë2<×$7žÛâa ¸¿Ýw£Zt¦Õ5ãѯÈ*šfeêânKµÊoßî®2úÿùçÿôOÿðÿòç¿ÿ»7iCh×Ï®]ßRð±¯Ëû• åf3kts{‚-µ2æ£åE{ ~Üá_ýåþüwÿiêÖúƒfg\ßZ>=gù·[ãáƒwJ9úcf]à[‹Ò>.Y%É©{³¿Ý›±}Ø<;xOÔ‰­M|o}V¯Ñ‡Í;²Þæðýô‚ãý2í×ýW}û7ÿæ¿ùíÉ;Ú…¥E–gÆQŠúòM"u¿åhE¯Þ`€½w½N 2$GæS-1ZûÍvb˜àûes½l˜xä;u¸\×k„~Bæ^pçW²—¥];µá«ÓH½v””óý¥Q÷çâ.°ou€—m½J¼L˜Öô§G$ôGd7q³°F©Ïx»|IŒÍõØž6 ÂW¯‘ú5*δ9¸åíéü«Ÿ7ø‡oû×m:¶®ÝS<¾§ë5qà#l6ß”æ6B×0ä¬ÅqÅžô“s òµV8úE=?-‘oì húíÆh& ¨”æB׌gº›‰{3Il—M}Ì Å;Sæ\å(r”æ9A×°|kߺwǬô13oìzÌ"WâÙ·yGÐ5b½Fà1Ëýåo{ÌœécÆ6_3¯Éñ É7šþ>Yšó]ÃñâÉrŽ·rsÌ\'ëh)^;3fÆðafkt >ɦ=gIšëöÇ,ô1sŽójbÌØ .®´ýçt z$2Ï©·5| ¬?´ß¸‡Ä÷`„ñÎe§šç?îÂÒ¬ÑOY¯‚»Èr?UÍW ”÷Q_Œ[µ&þºñ¹Ð•têÈ&þºáö+?¨ˆlX@®ñ=KF–ÿtý=³ø¾tŒ¾ð.TÖ–C*‹ÜNшXÈï]®ȆëhË€ÖM9³ h×Áˆpâ¬N1úôÙŸœ¹‹0¢G¢´µt•ú«¾Î%†'àf¦C}T3ÃkÛðÖéÊ…>ßXs«.݃/4à‡Áð†£qhL‰“´åÓÉmò·|â‰1¼áh«˜-Ô†÷ò¡ý1¼mC]Ç Lá¼\“‡7þv7<¼J¹Áôˆ2 o—‚Üu‚â>n¤Ñ"AÃÉlxPëw?ËmP!=ó7½óûè¦6[%¢kϯ`¹ü±.­X{í1åø“v}qÓ°D ªsA¦§Þ‡ÒÆÆ| ʵ+È;«ñ¤¡NÍ õäN2(§.(zP‰\ )¨âë ¢ç5Ä?R!°Ý;Wt}je5çaÖ§ý‹ûáׯÇA EžÒÈÚÒ¸üÔî@ˆ³.Fë°9D¦¢ÜVféÃXÈv;ý§õšQ®É2ùÄ]Tµa! ú5ÏáFÓ¯™Ná~D7‹…zFݬ^ƒF“•ÝE×󙯾_4.*6âT÷ káúoáæ>“ —)e:‹5yÆ–Óz•>huAе‹áþ6ÉÅO•pÑýýš¥'_iJñͲ5c?jI-Ö«¸†îÂÃ<˜ÓÓNÛk~»èûpÏêS¿&æ?[¦Ã5NÂÅT•L4wôpÅes]ûóÖ0ÿÑE%\Âd(Nêa»¦ë ⸥ˆßk\W%9ÜŸ«§§LUVÑu}ÐS*‘†AíèŠ4jˆž“Ì[êžöýÖ0ÿ9®í7tÛêe]ûcfN¡'ˆ+®a˜†‹,Æ W¬Ÿ³ÅTÕº„ëUüÁwfX4!zý-Öï7ó}ì1ozÃSÀMÔ$”›ÁD’0ozšHx˜êÄÔB¯«Ì`UEÇ1±Oíö´ÔëöQÔëÿx:ýãr$rÅÒÕÂŪÊ¿&æWNÃò9e“bvúHb~õ4áð9Óìùa©ãp³´€ú_÷ƒL/Žo³ÆxX0Ó?.ÃâÐÈ1;,îû51ûÂSŠž7˜äóó[pföb¶u©,S1Uëc—±ÙyúæëÍ,/PtÍ:Ä‹M KÕú‹}N ÃÄQÑ{¥,¢Çs‡»^rf™éŽB»£«#l 5‰ ¨id{œ®‘4õí­ ³ÃHÕI¼Øvo&:µö_$em{}ômõ[§Á~Ñ"wspu"žŽ¿±?û@l 9È¢ù¹Ôi°]Å‘í*vžNóð`Q™C'šcÓ… ¡Å ‚õû8ÕX4 é#÷— çúHÑd-Ã%-ña€6iæÅãtÜ7·ùiF¬FsÂ8ù‰qÊšy©MÝ)´©»ÉH›ëíÜ{RyÏ/ÅþÑf×Þó{ãÌÿAÓùÿüoÿÛ{û_êÝý»?ýßÿþoÿü7¿þúÏÿ÷ŸþþŸþòÚÕ2J!WËõjþ©þz¿š¥rÈ1s9Ó#Ž”,Îò8&*¼sª ….zó³šj¤ÒçÔ2W·ÈŽ¥þr9º%ÃÒ:ÌýXÄHc,]3Ò¹W1V]õ7}¬æ®‹=0¢õ: ¿­öÕïý\€¹áâór]Z¸zÏ÷îäÞ£˜»œE[GºƒŸÐ€mp¥˜>²uaRè==ZöŒø¼’t¿¯%wC1An ¾þq…[æé¸R¸ü Z›û5ké–V]®tµ¦ ÏokÉ {ê2Ž[)Ŷ® }w“Þ¼;.ŸÑ5 ¹Ô_í‡?-ú‹w=®,ò!År_ϼËþËç~ëNn}EºÏHÖ:ñ•ú’ÅO]Ì}Mõ/ÿÔñ*ª¾Í×Å•Ìø Æÿ¶Ú\Õ_ùš¹>%žºsïÕïO§W;§½W%f¹L •]øqfÉæ­ñÒ€¯É†XŸ¹ú’ƒ›aÞç˜ )#xâÚîŒù'NÎ2ÃÑ{ç?‹áŠ0œe†ëWÛ_)a8VB:O"^_·¨•BÎEè¬ë€¼üÊúä®Þ¾ë׬ȥ‹ ë¡/œé}Š;|réìø†œn j*‡ÜùÔ$x›½G ¦*‰{Ô-G“L ¤íñüždè˜Úe1•1™Úüo¯ k° µvAbcê‘Ë>ˆ±eÁè³yߎöŸ?ÍÔk ³yÝö—[óŒ>tÂlVbèÿßoãʯÕ~zEe¸“Ƹ¤ïk l[QýÿÃmõ„Ûê>×(¥­¨æE¡²Âmû ùÕ~ê±3N¤òèô¹‹m ©vhaÖýÏ{î„Þ×í¥G}ôúÜeÛã 2Ví¹ë½ºÿâÏ]nKBºuÚ `ŽoåàðÛS½4¶}êr[5³‘ßI > wJxÒœx²ÿnŸp©å¦ý.á¸+óÄŠ¡”Ÿ¿+ÔE÷oþH­5ŒÃ˜þ´áuÖÆß~±aV¨øí ‡ú‡ûüÏù§øÓ¯üËßÿçÿü§¿ýÃÿñ×ÿ×?Quúñ§¿ùeúzfèwþ{üúÿ׿þÿÕÿl2út‹! fwupd-2.0.10/plugins/logitech-hidpp/data/dump.tdc000066400000000000000000025021401501337203100216510ustar00rootroot00000000000000TPDC¼?§7IX !Š!   ±,\¦% X7IX| ÜO¸f lÜ @`.    À ` ÿÿ 9à!€-e Àà-`-  > @`8¼%` ”Q¸à8 Ð ld3@¤@Ð@€=O2¹`@àv`v@kÀ•€-X&`'j–·àk #€7 è b8ÀZ€=à k`=!àuA"@€7ßá` mà uàu î`€=Öíà áàuàuÁÑ€7s Æ E1³à ë€uú "€ëaž €=j ¼ÀüáWàuàu‚À7Á#áÍ4aÍB@€uâ>rÀàuáÍA@€-ë¡ûká WáÍ `;À=âà kàááÍ 3@€urîâCáÍ`;À=húà áàuáÍBù@€u!>áÍ`;À=ü,uá WàuáÍ 3@€uÁ#’ã›ÓC›`;À=sK+À†á Í@c@@~?iáÍ`;À=à kàááÍ 3@€uû€ulà uáÍ`;À=ú,à áàuáÍ 3@€u™-CáÍ`;À=Ž9.àuáÍ 3@€uÁ#vñ å+…¢…i'E. ÔúA ;F`=1A5@G À= dýÿ Âé@3ÒK @ @7‘?€@ˆ@`6@€@`@ÀæB«Ã! % øC ,@7€Jý> J àJ `JÁ E-èF +@àB{Fà @' @x@ÇA FP`C Eä÷€ˆŒ ÀEáR@´ûûaR €@[¢ü€=H À= `á RJ! @R€@ û€@l `6@€@ aÂwE¥Kÿñ )¾AQ" +  IŒú€Iá Q`IáQáQ# >@€ŒÀB^á QáQ@x@ÆA AQ$€`Ej³6@áQüü@=% +@@4L„rÕââçà@¡BâHá/¶€{ââââ!' Gà@§´€@á "â @áÃar°A(àI  Iá `IáiqBá) B@a ÀBl@íâ‚á@x@ÆA A* àÒ‡¸€Òáá#«+à@ã·€@à É"£ @àÉ$5@B, àƒG B‡ àÉ! àBi‚œ- +@a àB@Uá Á @x@¿AB^Ë`Eñn7@¼  EãîAkýþcî/ @p@4:/8@áJå A0 .à@œ-€@å,A1àJÀJ ÀÉ `ÓåA2 "@@ÅàBy@ó`8@€Bãï @x@ÇA AR3`' Eÿå€Ó‹ ÀEáR@ÿÿÿaR4 @€ƒÃê€=âáR#±5à@?é€@ã ±" @ã±eA îNB¤6àI«è€Iá QåA7 B@€ÍÀB_ bÁ a[áQ@x@ÆA AQ,©``E…¡9@àˆá Q,¥@=-Ê`@4+€Æå@`y@í"A ($: @ €=þ¥€{á%è$!;à@x¤€@á @á (N A<àIÙ£€Iá $4 Iäw¥A= B@`ÍÀBåA@x@ÆA A.ãáR¨€ˆá$½á%A?à@¬§€@à É"£ @àÉ¥A@àƒ Bå$A!Ý`a àBAíₜ"åA@x@¿AB^B @`E ]:@åA@€! { =S;`=áJ#­áJ! D <à@³€@å,AEàJÀJ áåA*†`@ÅàBêƒ@x@ÇA AR`þ `EÔ€ÓáÛã ï0Ÿ!ÓH :áÈØ€=`SáÖêƒIà@B×€@p@Sà@"!“ã±+”Ó@Í#þàŠ®Ö€IàÈ R`SåA!â`a àBaåA@x@ÆA AQL€D€E <@NáÚá QeAM +@@4*E€Æç@`¼@åAN A;€=h’€{@™à=!$¶ââ ÉO )à@Ý€@o@à~€@ꃪªá APàI< Iâ áåAQ B@`ÍÀBtá $4êƒ@x@ÆA B£R @¿`E¸”€Òá%á#'Sà@ @â Y"£ @å ATàBt“€ƒá àBªƒU B@€ÆÀBŒ@Ùá™! åA@x@¿ABœV @`E'K=@åAcîW +@p@4‡ >@áJ#­áJ#^X .à@ê €@å,AYàJVÀJ á `‹á•¯ÅZ +@@ÅàBêƒ@x@ÇA AR[`C E4Â Ó Àˆã ïaR\ &áõÆ€=å"A]à@qÅ€@k@Sâ"!“åA1§®Bç^àIÝÄ€Iâ çåA_ B@QÀBêƒ@x@ÆA AQ`€`E»}?@áQeAa +@@4J€Æêƒb ;A;€=O‚€{ä-å Acà@Ê€€@á @á ¯}AdàI+ IâX¥‹åAe "@`ÍÀBêƒ@x@ÆA EAYèá«„€Òá$½ãö%Ag Gà@ @â Y"£ @ã«$5@Bh àBgƒ€ƒá àB¥Ai B@a àBåA@x@¿ABœj`C EB9@@NØ $Ãoãîn¢ ;cîk +àÆ\Þd`=ãî! là@¾Ü€@å,AmàJ*ÀJ ÀÉ dCåAn "@@ÅàBåA@x@ÇA ARo`C E•e@NàˆáRA@<@\ÀBÈA~à— Kì¦@x@ÑAA\h `E0Fa\÷*BCa\@°àÆní€=á\â¯(¯‰ {à@Ïì€@@VàÄ!\ @á"î@BŠ àB;àBàBáJvÆ aàB{@UáJ5Ï"¡@'@x@¿@­AJ|N @"§GaJ@ÀEâ §DDE…ÖÀ=m»€=` ƒÀ=厎€$ v~>[`v`à 8䪀$€8Xn€8Üäª_~aú 9! €=‘&r`váú!»‘ <à@ô$€@äªÿ'ë%B’ àJ`ÀJ ÁÃM ⤪“ +@¼ÀBé_ÀBäª@x@ÇA B”`C€E/߀Óbá‹…ý +aR• @€ƒá€=áRäª#M–à@nà€@á #M @ /@D3!‘@B— àƒÏ Áà B䘘 B@€ÆÀBëýÁJ ä˜@x@¿AAJ™ @`E¶šsb_Qúá J@-€žaJš àÆP—‰`=çHçH(Ãåõ! ›à@¯•€@âÀ@3ž @ç HàôARœàJÀJ ÀÉÀJëý +@a àB|æƒÀB&d¤  @x@ÇA ARž`C EPŠa⟟aRŸ 9áR*T€=âàƒÿNçH à@EQ€@çHÀ@Ž›çH¡àT¦ Óá \/ºâò®­¢ B@\ÀBÌC£´À—%þâ¯@x@ÑAA\£€`E ‹a\àˆÅ¿ãú  a\¤ =á\Z €=â¯#ú¥à@½ €@à~ÀÄ!\"¯ã ú¦àB)àBíS§ B@aàBx@ÙáJâ§@x@¿AAJ¨`C EÇ€Ëú|¡¢aJ© 9@p€ƒl…Œaˆâ§! â§#úªà@ʃ€@ä¿ÁJã ú#•ÿé¤A•«àJ6 Já •ãú¬ B@€ÎÀB~ dáRãú@x@ÇA AR­ .@`E!>aàˆâ ££aR® ;áRD€=áR#dáR2à¯à@^?€@‹5àÄ" @1 @DK "".€1 /C– !" Ò¿Aj° 0ࣽà áÁµä± B@a$àB â½ ä@x@ßA$Aj² @`E¨ù€ëáj¤¤aj³ ;àÆÌû€=áj@`E<,a áJ§¨aJ½ ;@p`=”ê€=æ %U¾à@óè€@å!U#÷ÿÔdA•¿àJ_ Já •åUÀ€€ÎÀBéP@x@ÇA ARÁàEI£‘aRàˆå U©©aR =áR©¥€=â#©åU!zÃà@…¤€@@Sâ" @+:` K  xANÄ @3 åà ÅãìÅ B@àB æ ¤"¢å9@x@ÃAANÆ @`EÐ^’aN⡪«aNÇ ;àÆ'“`=áN#«áN#ìÈà@‡€@áÍÁ!N @ã ìÉàBóàBãìÊ B@aàBãì@x@¿AAJeï `EÞÕ€ˆéB¬­aJÌ :áJF””aJ"qâX+ê! áJ#ìÍ )à@ ’€@ã ì"›ÿ—ÛA•ÎàJ  Já •ãìÏ B@a àBâ éB@x@ÇA ARÐ`C EëL•aàˆã ì®®aRÑ 9 €ƒ‹R€=â#«áR=iÒ <à@'N€@G)á“" @/ @! KUSB6mBo tL a<øe9J8µAhÓ (à`‡M€`àßÂþé@Ô B@€äÀBLHZää@x@ÝA"AhÕ @`Er–ah⻯°ahÖ ;àÆÕÅ€=áhâ»$×à@5 @u'àÄ!h @ä ØàB¡Ä€ƒà BäÙ B@aàBä@x@¿AAJÚ`C E—aJåQ±²aJÛ 9áJà=˜`=ä"Üà@B<€@ñ!D _4A•ÝàJ®; J á•äÞ +@RÀByä@x@ÇA ARß`C Eö€ˆñD³³aRàáRÍ÷€=á RcÅäAáà@- @á ã ¥Bâ B@ÀBÿÿùÃèG¬ @x@½Aã€`A²™b‘ÿÿp´ƒ.äà=£¾R{`£`ÿÿ>ÿO €kÀ-Žàk„S`kæ@: €=0Ë ÿ0`à ka à-‘ ”à-ÿÿz:a!T Aè  `=ØŸ* á Ûç5é .à@uÖ€@ã!. ;ÔsêàJáÕ J ÀÉÁãã.ë +@€ÎÀBzã.@x@ÇA B*ì`C EÀ aàˆáR à 8••aRí @€ƒÜ”€=åÌ&ôã.{¼îà@ù‘€@ô6sïàT\ TàÓÁ\ã ƒFð B@€ØÀBû¼ÀBç)@x@ÑAA\ñ @`EGL¡á\––a\ò ;á\N€=ä ‹â ¯')óà@wM€@á ç)ôàBãà ÁÁç)õ B@aàBz@Ù£âàBôs@x@¿AAJö`C EÍ¢aJç)—˜aJ÷ 9@p€ƒ(Æ€=â§(3â§+0øà@ˆÄ€@ã!ú#• ®A•ùàJôàJ áãúú +@€ÎÀBï@x@ÇA ARû`C EÛ~£aRàˆã ú™™aRü 9áRÛ€=áRãúLýà@€€@§@Sâ" @fà ôsÁmASþ @8w€KàÊÂcãñ„â `@`  K°ÿ ¡u• &ÿ&V ‘À…Ac4 X  [l|€[á c `[åP¿ÿ³5@? cÀB´ê éå @x@Ø2 =Ac6 @`EF7¯`Åâ¶©ªac7 = €=ªô€=ã ºò>8 =  @  @á  b÷á"õAJ9 ?  Buó€ƒà BàBáŸÿ&r:@? aàBùh@x8 9AAJ; @`ET®°aJ(eÂ/åÿr«5FX< = €=’É+ á J! = =  @ôÇ€@æX %#<…fX> G  J`ÀJ ÀÉÅP áR¿ÿî? , a àBæX@x@ÇA AR@``E{€aàˆáR`.66aRA  €=Kˆ€=äåT.LB =  @«ƒ€@ˆã b‚îL€ 8à·«B½C _  bÀb áj`£ã¢½D@? a$àB|ý që´@x@ßA$AjEàE<áj77ÿ+ÍFà=ý%²bû\dàÆÿÿ!IŸÿ2Š@´ €"å =  ÁîA€{á¨åª!¨H =  @8=€@1¥à~Ÿÿ4`@á¨Ã!¨9Ãgÿ„„ yA.TÔŒø›”hv¼ï/GÌï?ÿqI _  bž¡èãÀb%± £á¨iXWðJ@? a¨ÀB˜á ¨!©Ÿÿc@x@ßA$A,K E €ED€ëá,æ×+ÌL =  @uC€@à É"Õ @àÉ$g@BM ?  BÚB€Bâ µ! àB¦¾N@? a àB@éqëÌ@x@¿ABµO@/ `Eˆ÷€ˆæ¾89d P = €=¯·bóæ¾! Q =  @¶€@à Ä"w$aëÌåsR G  J{µ J áåsS@? a àBÿÿk¢ÿÿä@x@ÇA ARTàE–naàˆå s::aRU =  ƒ8v€=áR#ÊV =  @šq€@ås0€;à%jB½W _  bÀb ájåsX@? jÀByã Êås@x@ßA$AjYàE*áj;;ajZ =  ƒ¥-€=áj#„êà$[ =  @ @á ä\ ?  B`,€ƒà Bä] ? JÀBì ¹!KïÕ@x@¿AAJ^  €E£å€ˆ•b ô¤Nâµ@,<Qf¾_à=$ø„F‰•$ú =æ¾£Ë&¾` =  {©M*aÆáˆ& “`@3ÒBóa  @ëH€@ æ ¾!ˆ @æ¾bóÿæ¾» Aëb _  bL bᨠb ù`Lí¦¾c@? abÀB¤â ó!¨æ¾@x@ßA$A¨dàE7+a,å±RSbóe = €ƒjÀ=áj%páj%±f =  @ÊÁ€@å,±g G  J6ÀJ â4ˆtå±h@? aRàBå±@x@ÇA ARiàEEz,aRçÐâû`.TTaRj   ƒó€=áR%±k =  @U}€@å±`;àk%l _  bÁ|€bàáÁjå±m@? jÀBå±@x@ßA$AjnàEË5-ajáóá jUUajo =  ƒ[9€=é"ºp =  @¸8€@á $(%òãÅ"üDq ?  B BàÁÁJå±r@? JÀBå±@x@¿AAJsàERñ€Ëå±Vke±tà=Ó¤Få±u =  ÁWY=aÆáˆ% äF"óv =  @šT€@: ãû)ø @å±ÃBó屚aA¨w _  bûS€bᨠbá륱 a¨ÀBå±@x@ßA$A¨yþ €Eæ>a,ÿÿ lmbóz à €=$Ï€=ájå ±{ =  @…Í€@å,±QÑàJñÌ J \áµå±}@? a àBå±@x@ÇA AR~àEó…?aRàˆä FnnaR =  ƒš€=å"±€ =  @üˆ€@å±`;àd¨B½ _  bh bä ©å±‚@? jÀBå±@x@ßA$AjƒàEzA@ájooaj„ =  ƒúD€=â½%pä(+c… =  @W @á 屆 ?  B¶C€ƒâ,å±ÿ5û JÀBå±@x@¿AAJcs €Eý€ˆå±p…e±‰@à €=‚ƒ|届 =  =ePaÆáˆ% áˆ%±‹ =  @I`€@å ±!ˆ @å±bóå±úbAëAOàbª_€bᨠbå ±@? a$ÀBå±@x@ßA$A¨ŽàE•Qa,ÿÿ>†‡bó = €ƒ³Ú€=ä^(f =  @Ù€@b=íM‹'%ò­I¿ÿB>"Îö‰‘ G  J€Ø J àJå±’ , aRàBÿÿ Ä@x@ÇA AR“@C E¢‘RaRàˆå ±ˆˆaR” =  ƒH™€=â½!⽕ =  @ª”€@å±`;àEiB½– _  bÀb ájå±—@? j Bzî !ñ@x@ßA$Aj˜àE)MSáj‰‰aj™ =  ƒ¥P€=ájå ±š =  @ @á $( @ãÅ"üAJ› ?  BaO€ƒà Bëcœ@? JÀBCèá!Kñ@x@¿AAJàE°TaJ™ñŠŸe±žà=1ƒ|„ñŸ =  Á¹pc`{áˆ% âó  =  @ûk€@ñ $F @å±ÃBóå±Û£A¨¡ _  b] bᨠbáë«c¢@? a¨ÀBå±@x@ßA$A¨£àED&da,å± ¡R€¤ = €ƒzæ€=å"±¥ =  @Ûä€@h5å'±¦ G  JGÀJ àJå±ÿ   aRàBå±@x@ÇA AR¨@… `EQeaRáÛå ±¢¢aR© = €=ó¤€=áR(oª =  @U €@å±`;मB½« _  bÁŸ b áj屬@? a$àBëc@x@ßA$Aj­àEØXfáj££aj® =  ƒX\€=ä(%pä(+c¯ =  @¸[€@â rå±° ?  Bá Jå±±@? àB@ãCå±@x@¿AAJ²@/ €E_gaJ•層¹e±³à=à¤Fˆå±´ š |€{`|v`{áˆ% áˆ"óµà@¦w€@å ±!ˆ @å±bóå±:}†¶àb bᨠbå ±· B@`æÀB è ¥!¨öÇ@x@ßA$A¨¸àEó1wa,屺»bó¹ = €ƒò€=ájê(fº =  @yð€@â m' @ëc» G  Jåï J â¸å±¼@? aRàBñ@x@ÇA AR½àE©xaRàˆå ±¼¼aR¾ =  ƒ–°€=å"±¿ =  @ø«€@å± `;à…oD©À _  bdÀb ájå±Á@? jÀBå±@x@ßA$AjÂàE‡dyáj½½ajà =  ƒh€=â½å ±Ä =  @sg€@å$±Å ?  BÓf€Bá J屯@? JÀBëc@x@¿AAJÇàE zaJå±¾Óe±Èà=2£|å±É =  Áˆ‰`{áˆ% å±Ê =  @Uƒ€@ñ $F @å±ÃBóå±¥BóË _  b·‚€bᨠbå ±Ì@? a¨ÀB å±@x@ßA$A¨ÍàE¡=Ša,üyÔÕbóÎ = €ƒËý€=áj%páj%±Ï =  @,ü€@å,±Ð G  J˜û J äFå±Ñ@? aRàBå±@x@ÇA ARÒàE¯´‹aRàˆå ±ÖÖaRÓ =  ƒT¼€=áR(oÔ =  @··€@å± `;àålB½Õ _  b#Àb ájå±Ö@? jÀBå±@x@ßA$Aj×àE6pŒajå2á j××ajØ =  ƒ¶s€=ájå ±Ù =  @ @à~ÓÓ `@ãÅ"üAJÚ ?  Bvr€ƒá Jå±Û@? JàB@ëc@x@¿AAJÜ@/ €E¼+aJ屨íe±Ýà==>ƒ|öÇÞ =  {º“œ`{áˆ% äF"óß =  @ÿŽ€@ëc"óå±{¦A¨à _  bf bá ë!¨ £áë±á@? abÀBœå±@x@ßA$A¨âàEPIa,å±îïbóã = €ƒv ž`=ê"´ä =  @×€@â m'%òëcå G  JCÀJ â¸å±æ@? aRàBñ@x@ÇA ARçàE^À Ó@X£Aä FððaRè =  ƒÈ€=â½!â½é =  @fÀ@å±`;àÄ­B½ê _  bÒ b ÀáÄ©å±ë@? jÀBå±@x@ßA$AjìàEä{Ÿbàˆá jññají =  ƒU€=â½å ±î =  @°~€@à~Áå±ï ?  B Bä ƒäŸÿ[ð@? JÀB‹@¢oÀ…/löÇ@x@¿AAJñ@/ €Ek7 aJå±ò@=òà=ì¤Fëcó =  {iŸ¯`{áˆ% âóô =  @®š€@å ±$F @ëc"óå±ZgBóõ _  b bá ëå±ö@? abÀBå±@x@ßA$Aj÷àEÿT°a,å± VÇø = €ƒ)±`=ájä^%±ù =  @Š€@å,±ú G  Jö J â¸å±û@? aRàBå±@x@ÇA ARüàE Ì€ˆã„ä F  aRý =  ƒ®Ó€=áR(oþ =  @Ï€@å±`;à$y…!Ë`&'}Πb@6ä#å±$E`àBxè oüy@x@ßA$AjàE“‡²bàˆá j  aj = É ƒ‹€=ç%-ä(6Çà@wŠ€@áéÅ+å±àB׉€BàÁÀB$ ƒå ± B@JÀBñ@x@¿AAJàEC³aJå± !e±à=›U„Få± =ሠ«Â`{âó%páˆ"ó à@e¦€@ñ !ˆ @å±bó屺iAë àbÃ¥€bᨠb᨟ÿO B@a¨ÀBüy@x@ßA$A¨ÿ7¡ €E®`Ãa,å±"#bó ƒ >`=ß Ä`=âóå ±à@A€@å,±àJ­ J äFå± B@€ÎÀBå±@x@ÇA ARàE»×à ˆä F$$aR =áRM߀=â½áR"½à@°Ú€@å±`|àÿêàb bä ©å± B@jÀBå±@x@ßA$AjàEB“Åbáóá j%%aj =ájÒ–€=â½%-ä(%±à@. @á $( @ãÅK þ BàBŽ•€ƒàÁå± B@JÀBå±@x@¿AAJàEÉNÆaJå±&;e±à=Ja„Få± =áˆζÕ`{áˆ% áˆ"óà@²€@å ±'ô @å±ÃBóå±›ÿ Òàbr±€bᨠbá륱 B@a¨ÀBå±@x@ßA$A¨!àE]lÖa,üy<=bó" =ájŽ,×`=ä^"´#à@ð*€@å,±$àJ\ÀJA#éøå±% B@aRàBÿÿh@x@ÇA AR&àEjã€Óÿÿ"j>>aR' =áRë€=â½å ±(à@jæ€@å±`|àeÿN)àbÖå b@6ájå±* B@jÀBñ@x@ßA$Aj+àEñžØbáj??aj, =áj}¢€=áj%pä(%n-à@Ø¡€@á å±.àB9àBëc/ B@JÀBå±@x@¿AAJ0àExZÙaJå±@Ue±1à=ù¤FÿÿÞ2 =áˆÂè`{áˆ% áˆ%±3à@½€@¤ÿ ÿB!ˆ @å±bóå±û«Aë4àb! bá ë!¨ bå ±5 B@a¨ÀB¨î W.Xÿÿ,@x@ßA$A¨6àE xéa,å±VWbó7 =áj)8ê`=å"±8à@‹6€@å,±9àJ÷5€Jè ðå±: B@aRàBå±@x@ÇA AR;àEáÛé øXXaR< =áRÒö€=áR"½=à@1ò€@å±`|àDÿ²>àbñ b@6å±? B@jÀBÿÿ$@x@ßA$Aj@àE ªëbàˆá jYYajA =áj(®€=ä(å ±Bà@‡­€@à~Ë $( @ãÅ(®DCàBè¬àB$àB¶ÇD B@JàB@Ùá$öÇ@x@¿AAJEàE'fìaJå±Zoe±Fà=¨¤F‡ÿÿ,G =@®€Á(Îû`{áˆ% å±Hà@qÉ€@öÇ"óå±Úÿ!šIàbÐÈ€bá ë!¨ £áë«cJ B@`æÀB å±@x@ßA$A¨KàE»ƒüa,å±pqbóL =ájìCý`=ä^"´Mà@NB€@ÿ,ÿ NàJºA J â¸å±O B@aRàBå±@x@ÇA ARPàEÈú€ˆáÛä FrraRQ =áR}þá$R%±Rà@Üý ~è o+¤èo`|à¥y…SàbHÀb@6ájëcT B@jÀBå±@x@ßA$AjUàEO¶,àˆá jssajV =ájÓ¹€=ä(%-ä(+cWà@. @â rå±XàB“¸€ƒàÁÅô$ ƒäœyY B@JàB@Ùáå±@x@¿AAJZàEÖqÿbwå±t‰e±[à=W„„Få±\ =@®€ÁÛÙ, «áˆ% áˆ"ó]à@ Õ€@ñ !ˆ @âóå±;m^àbÔ€bᨠbᨖÇÿco `æÀBñ@x@ßA$A¨` ˆ@€Eja,届‹bóa = ?`=zO`=âó$áj%±bà@ÜM€@â m2€ @ÿÿ,càJHÀJ â¸å±d B@€ÎÀBöÇ@x@ÇA AReàEwaàˆä FŒŒaRf =áR€=áR"½gà@ €@å±`|à„ÿêhàbë b@6ä#å±i B@jÀBå±@x@ßA$AjjàEþÁà ˆá jajk =ájzÅ€=áj%-â½%±là@ÕÄ€@å$±màB: Bá Jå±n B@JàB@Ùáå±@x@¿AAJoàE„}bµ–ÿÿBŽ£e±pà=„Få±q =@®€Á†å!`{áˆ% áˆ"órà@Êà€@ëc"óå±ÿ Òsàb. bᨠ£å ±t B@`æÀB ëc@x@ßA$A¨uàE›"a,üy¤¥bóv =ájA[#`=ë"cwà@£Y€@â må±xàJÀJ â¸å±kM aRàB`àBÿÿ/¼@x@ÇA AReá `E&$aàˆä F¦ŸÿY+{àÆÎ€=â½&Ãâ½|à@.€@å± øàäÿN}àbš b@8á'å±~ B@a$àBå±@x@ßA$AjàE­Í€ˆå2á j§§b½€ =@퀃5Ñ€=ájå ±à@Ѐ@á )Ú @ãÅ(®Eô‚àBñÏàB屃 B@€ÆÀBöÇ@x@¿AAJ„àE3‰%bµëc¨½e±…à=´¤F屆 =áˆ9ñ4`{áˆ% âó‡à@}ì€@å ±!ˆ @ëc"óå±zÿ6ˆàbÝë€bâjí báë«c‰ B@a¨ÀBëc@x@ßA$A¨ŠàEǦ5a,ÿÿ* ¾¿bó‹ =ájøf6`=å"±Œà@Ze€@å,±àJÆd JA#äF屎 B@aRàBå±@x@ÇA ARàEÕ7aàˆä FÀÀaR =áR…%€=áR(o‘à@å €@å±`|àÅÿ²’àbQÀb@6áj屓 B@jÀB{è oÿÿÞ@x@ßA$Aj”àE[Ù ë@Y ˆá jÁÁaj• =ájäÜ€=áj%pä(+c–à@> @ã Å$( @ãÅ¥±—àB Û€ƒà B屘 B@JÀBÿÿ @x@¿AAJ™àEâ”8bµå±Â×e±šà=c§„Få±› = ÀÁèüG`{áˆ% áˆ"óœà@,ø€@ñ )ø @å±ÃBóå±[ÿ!šàb‹÷€bâj属 B@`æÀBå±@x@ßA$A¨ŸàEv²Ha,ëcØÙbó  =áj–rI`=çê"´¡à@ùp€@å,±¢àJeÀJ £½ÁRå±£ B@aRàBå±@x@ÇA AR¤àE„)Jaã„ä FÚÚaR¥ =áR41€=â½áR"½¦à@”,€@å± `|à$¸D©§àb bàáÁj屨 B@jÀBå±@x@ßA$Aj©àE å ë@Y¡óÿÿÿÿYŒ i 8ÛÛajª =@€ƒŠè€=â½%pä(%±«à@åç€@á 屬àBF BàÁÁJå±­ B@€ÆÀBëc@x@¿AAJ®àE‘ Kbµå±Üñe±¯à=³„Få±° =ሖ[`{áˆ%páˆ"ó±à@Û€@å ±!ˆ @å±bó屺rAë²àb: bᨠbç«c³ B@a¨ÀBå±@x@ßA$A¨´àE%¾€ëå±òóbóµ =ájE~\ajä^%±¶à@¨|€@å,±·àJÀJA#£½Âû屸 B@aRàBPõàBñ@x@ÇA AR¹àE25]aàˆä FôôaRº =áRÞ<€=â½å ±»à@?8€@å±"`|àyB½¼àb«7€bä ©å±½ B@jÀBå±@x@ßA$Aj¾àE¹ð€ˆájõõaj¿ =ájIô€=ájå ±Àà@£ó€@à~Âr `@ãÅ"üAJÁàBàBå±Â B@JÀBå±@x@¿AAJÃàE@¬^bµ™öÇö @=Äà=Á¤Fƒÿÿ,Å =ÀÁIn`{áˆ% 屯à@€@ñ ë c"óå±›³A¨Çàbí€bá ë!¨ £áë¥±È B@a¨ÀBå±@x@ßA$AjÉàEÔÉ€ˆ¼ Z¤çä^`.   AÊ ájŠoajå"±Ëà@cˆ€@å,±ÌàJχ J ÈÀÉÃ>å±Í B@aRàBå±@x@ÇA ARÎàEá@paŒ@Yá R!8 8aRÏ =@€ƒ‰H€=áR(oÐà@éC€@å±$`|àezB½ÑàbUÀb@6 áÁjå±Ò B@€æÀBå±@x@ßA$AjÓàEhü ë@Y ˆâ½AXajÔ =@€ƒèÿ€=ä(å ±Õà@F @æ ¹å±ÖàB¨þ€ƒàÁÁJ$ ƒäŸÿ,× B@€ÆàB@Ù¡ÀB2aÿÿÞ@x@¿AAJØàEï·qbµëc%e±Ùà=pÊ„FŽå±Ú =@®€Áô`{áˆ% å±Ûà@8€@å ±!ˆ @ëc"óå±ûÿ™)Üàb˜€bᨠb᨟ÿ,Ý B@`æÀBå±@x@ßA$A¨ÞàEƒÕ€ˆå±&'bóß =áj²•‚ájê(fàà@”€@ÿ,ÿÞáàJ‚“ J â¸å±â B@aRàBå±@x@ÇA ARãàELƒaáÛä F((aRä =áRDT€=å"±åà@¤O€@å±&`|àD»E±æàbÀb@6ájå±ç B@jÀBå±@x@ßA$AjèàE„áj))ajé =áj‹ €=ä(%-ä(1êà@é €@â r$( @ãÅ"ü@BëàBO Bá Jå±ì B@JÀB„@Ùáå±@x@¿AAJíàEžÃ€Ëëc*?e±îà=Ö„FŠå±ï =@®€Á£+”aÆáˆ% áˆ(¥ðà@ç&€@ñ ë c"óå±ÚqA¨ñàbK bᨠ£á륱ò B@`æÀB ù »!¨ßÿRõ@x@ßA$A¨óàE2á€ëÿÿÞ@Abóô =ájY¡•ájå ±õà@¸Ÿ€@è !j @ÿÿÞöàJ$ÀJ þâ¸å±÷ B@aRàB}@âûñ@x@ÇA ARø@/ `E?X–aàˆå ±BBaRù =áRî_€=â½!â½úà@O[€@å±(`|à¥|B½ûàb»Z b@6á'å±ü B@a$àBå±@x@ßA$AjýàEÆ—ájCCajþ =ájR€=áj%páj%±ÿà@°€@å$±àƒ Bá Jå± B@JÀBñ@x@¿AAJàEMÏ€Ëå±DYe± O`€ƒÎ¤Få± =áˆV7§aÆáˆ% áˆ%±à@™2€@ëc"óå±;¶Aëàbú1€bᨠ£å ± B@a$ÀBëc@x@ßA$A¨àEá쀈å±Z[bó =áj­¨ajê(f à@g«€@å,± àJÓª JA#äFå± B@aRàB|#—àBå±@x@ÇA AR àEîc©añ\\aR =áR‘k€=â½ ôâ½à@òf€@å±*`|à„½B½àb^Àb@6ájå± B@jÀBå±@x@ßA$AjàEuªajàˆç ]]aj =ájõ"€=ájå ±à@R @à~ÐëcàB±!€ƒà Bå± B@JÀBå±@x@¿AAJàEûÚ€ˆå±^se±à=í„F…ëc =áˆCºaÆáˆ% âóà@H>€@å ±)ø @ñÃBóå±wBóàb©=º`£ç !¨ bå ± B@a¨ÀBå±@x@ß =A¨ @€Eø€ˆÿÿÞtubó =ájø»`Æå"±à@"·€@å,± àJ޶ J äFå±! B@a àBå±@x@ÇA AR"àEo¼aå±vvaR# =áRHw€=áR(o$à@©r€@å±,`|àä¾B½%àbÀb@6ájå±& B@jÀBÿÿL6@x@ßA$Aj'àE$+½ajãFå ±wwaj( =áj´.€=ájå ±)à@ @à~å±*àBp-€ƒà B$ ƒä–Ç+ B@JÀBÿÿÞ@x@¿AAJ,àEªæ€ˆöÇxe±-à=+ùºaˆ†å±. =ሰNÍ`=å"±/à@óI€@Ÿÿ ÿè å±bóå±ztBó0àbS bâ ó!¨ bᨖÇ1 B@a¨ÀB¥ñ@x@ßA$A¨2àE>Îa,ÿÿ,Žbó3 =ájiÄ€=ájê+c4à@É€@å,±5àJ5ÀJA#äFå±6 B@aRàBå±@x@ÇA AR7àEL{ÏaR‹@Y ˆä FaR8 =áRÿ‚€=äFáR%±9à@\~€@ÿÿv¿ @èoK0.`|àÅB½:àbÈ}€bá jå±; B@jÀB~â ½ÿÿ @x@ßA$Aj<àEÒ6Ðajàˆá j‘‘aj= =ájV:€=â½%-éÚ1>à@´9€@å nëc?àB BàÁÅô$ ƒä…±@ B@JÀBëc@x@¿AAJAàEYò€Ëå±’§e±Bà=Ú¤Få±C =áˆ_ZàaÆâó% áˆ"óDà@¢U€@å ±`@âóÃBóå±[µAëEàb bᨠ£á¨…±F B@a¨ÀBëc@x@ßA$A¨GàEíáa,ëc¨©bóH =áj$Ѐ=áj$áj%±Ià@„΀@å,±JàJðÍ JA#¥(Âûå±K B@aRàB{ñ@x@ÇA ARLàEú†âaRëcªªaRM =áR¥Ž€=äFáR"½Nà@Š€@ëc0`|à%qB½Oàbs‰€bä ©å±P B@jÀBëc@x@ßA$AjQàEBãajáóå ±««ajR =ájýE€=áj%-â½%±Sà@Z @à~Ârå±TàB¹D€ƒà Bå±U B@JÀBå±@x@¿AAJVàEþ€ˆñ¬Áe±Wà=‰ƒ|ÿÿF„X =áˆfóaÆáˆ% áˆ"óYà@Ta€@ñ !ˆ @å±bóå±»mZàbµ`€bᨠbå ±[ B@a¨ÀBå±@x@ßA$A¨\àEœôa,öÇÂÃbó] =ájÏÛ€=å"±^à@/Ú€@å,±_àJ›Ù JA#å±` B@aRàBå±@x@ÇA ARaàE©’õaRå±ÄÄaRb =áR\š€=áR"½cà@½•€@å±2`|àÿêdàb)Àb@6ájå±e B@jÀBå±@x@ßA$AjfàE0Nöajàˆå ±ÅÅajg =áj¼Q€=ä(å ±hà@ @æ ¹$( @ãÅ(®EôiàB|P€ƒàÁÅôå±j B@JàB@Ùá,¯ÿÿÞ@x@¿AAJkàE· ÷aJëcÆÛe±là=<ƒ|å±m =@®€Á¸qÿ¿$áˆ% å±nà@ûl€@ëc"ó屚zA¨oàb` bᨠ£áë«cp B@`æÀBÿÿ,@x@ßA$A¨qàEK'a,å±ÜÝbór = ƒqç€=ájê"´sà@Òå€@ÿ,ÿÞtàJ>ÀJ â¸ëcu B@aRàBå±@x@ÇA ARvàEXžaRáÛä FÞÞaRw =áR÷¥€=å"±xà@X¡€@å±4`|àdÿNyàbÄ  b@6ä#å±z B@jÀBå±@x@ßA$>!šàEßY aj@Y ˆá jßßaj| =ájo]€=â½%pä(+c}à@Ì\€@â rå±~àB+ Bá Jå± B@JÀBëc@x@¿AAJ€àEf aJëcàõ@=à=ë¤F层 =áˆ}`{áˆ% áˆ(¥ƒà@²x€@£öÇ"óå±úyAë„àb bᨠ£å ±… B@a¨ÀBëc@x@ßA$Aj†àEú2a,ÿÿ ö÷bó‡ =áj(ó€=ä^%±ˆà@ˆñ€@è 2€%òÿÿÞ‰àJôð JA#£½` 届 B@aRàBå±@x@ÇA AR‹àEªaRëcøøaRŒ =áR©±€=â½!â½à@ ­€@å±6`|àErB½Žàbw¬ b@6a`ájå± B@jÀBå±@x@ßA$AjàEŽeájùùaj‘ =áji€=ájå ±’à@jh€@á 屓àBÊgàBå±” B@JÀBå±@x@¿AAJ•àE!aJëcú`=–à=š3ƒ|ÿÿ@Ò— =ሉ,`{áˆ% âó˜à@\„€@ñ $F @ñÃBóå±Ûÿ!š™àb¾ƒ€bá ë!¨ bå ±š B@a¨ÀBå±@x@ßA$A¨›àE©>-a,ëc Aœ =ájçþ€=å"±à@Gý€@å,±žàJ³ü JA#äF屟 B@aRàBå±@x@ÇA AR àE¶µ.aRàˆé øAR¡ =áRL½€=áR(o¢à@®¸€@å±8`|à¤y…£àbÀb@6áj層 B@jÀBÿÿF„@x@ßA$Aj¥àE=q/ájAj¦ =ájÍt€=ä(å ±§à@) @ë$c¨àB‰s€ƒá J$ ƒ#w` 6iœy©@ JÀBå±@x@¿AAJªàEÃ,0aJÿÿ5n)`=«à=E?ƒ|屬 =áˆÉ”?`{áˆ% å±­à@ €@ëc"óå±:m®àbm€bᨠ£á¨œy¯ B@a¨ÀBå±@x@ßA$A¨ÿŒ¸ €EWJ@a,ÿÿ,*+Bó± ƒájq A`=ájê+c²à@Ò€@å,±³àJ>ÀJ@ÜäFå±´ B@a àBå±@x@ÇA ARµàEeÁ Ó@Y ˆå ±,48'¶ =áR É€=å"±·à@mÄ€@å±:`|à…ÿê¸àbÙàb@6 áÌÎå±¹ B@jÀBå±@x@ßA$AjºàEì|Bbå2á j- &!j» =áj\€€=â½%-ä(1¼à@¼€@à~Áëc½àB Bá Jå±¾ B@JàB@ÙáöÇ@x@¿AAJ¿àEr8C!#ëc.!@=Àà=ó¤FñÁ =@®€Át R`{áˆ% áˆ(¥Âà@º›€@öÇ"óå±ÿ ÒÃàb bá ë!¨ £å ±Ä B@`æÀB ÿ ÿ !¨ÿÿô@x@ßA$AjÅàEVSa,å±DEBóÆ =áj8T`=ä^%±Çà@™€@â m,Î%òñÈàJÀJ þâ¸å±É B@aRàBzÿÿ,@x@ÇA ARÊàEÍ Ó@Y£Aä FFFARË =áRÅÔ€=â½!â½Ìà@ Ѐ@ÿÿÞ<`|àåw#¢ÍàbŒÏ€bàáÄ©å±Î B@jÀBâ ½ÿÿÞ@x@ßA$AjÏàEšˆUbàˆá jGB¡%±Ð = …€ƒ#Œ€=â½å ±Ñà@~‹€@à~Áå±ÒàB㊀Bä ƒä‹cÓ B@€ÆàB@Ù¢oÀ…!Küy@x@¿AAJÔàE!DVaJñH]`=Õà=¢¤Få±Ö =@®€Á¬e`{âó% âó×à@d§€@ñ !ˆ @ñbóå±{½BóØàbʦ€bá ëå±Ù B@`æÀBâ ó!¨å±@x@ßA$A¨ÚàEµafa,ÿÿÞ^_BóÛ =ájã!g`=âóä^%±Üà@D €@å,±ÝàJ° J þâ¸å±Þ B@aRàBå±@x@ÇA ARßàEÃØà ˆä F``ARà =áRdà€=áR(oáà@ÇÛ€@ëc>`|àĶB½âàb3Àb@6ä#å±ã B@jÀBÿÿ]L@x@ßA$AjäàEI”hbàˆá jaaAjå =ájÖ—€=áj%-ä(&¹æà@1 @å n$( @ån"ü@BçàB‘–€ƒàÁÅn$àB¥±è B@JÀBñ@x@¿AAJéàEÐOiaJå±bw`=êà=Qb„Fÿÿ]Lë =ሷx`{áˆ%páˆ"óìà@³€@å ±)ø @å±ÃBóå±Z|A¨íàby²€bᨠbáë±î B@a¨ÀBñ@x@ßA$A¨ïàEdmya,ëcxyBóð =áj‰-z`=çå ±ñà@ë+€@å,±òàJWÀJA#äFå±ó B@aRàBå±@x@ÇA ARôàErä€ÓãÇä FzzARõ =áRì€=â½áR"½öà@~ç€@å±@`|à$ŽB½÷àbêæ€bàáÄfå±ø B@jÀBå±@x@ßA$AjùàEøŸ{báóá j{{Ajú =áj|£€=â½%-ä(%±ûà@Ø¢€@á å±üàB8 BàÁÁJå±ý B@JÀBå±@x@¿AAJþàE[|aJå±|‘`=ÿà=n„Få±áˆ„Ë`{áˆ% áˆ"óà@ʾ€@ñ !ˆ @å±bó屺DAëàb( bᨠbå ± B@a¨ÀBå±`x@9 A¨ @€EyŒa,å±’“Bó =! €=,9`=ájä^(fà@Ž7€@å,±àJú6 J å ± B@a àBå±@x@ÇA AR àE ðà ˆä F””AR =áRÁ÷€=áR"½ à@%ó€@å±B`|àOB½ àb‘ò€bä ©å± B@jÀBå±@x@ßA$AjàE§«Žbáóá j••Aj =áj7¯€=ájå ±à@’®€@à~Ån$( @ãÅ"üAJàBó­àBå± B@JÀBå±@x@¿AAJàE.gaJ•ÿÿ; –«`=à=¯¤Füy =áˆ3Ïž`{áˆ% å±à@xÊž`@ëc"óå±›…A¨àb×É€bá ë!¨ £áë«c B@a¨ÀBå±@x@ßA$A¨àE„Ÿ`ëÿÿ,¬­Bó =ájãD `=ê"´à@EC€@å,±àJ±B JA#ëc B@aRàBÿÿF„@x@ÇA >àEÏûà ˆä F®®AR =áR|¡á$R%± à@Ûþ ~ á$F'EèoD`|àeLB½! Là£GÀb@6ájå±" B@jÀB|ó Ó ¿ÿ×Ò@x@ßA$Aj#àEV·,àˆá j¯¯Aj$ =àÆÒº€=ä(%pä(+c%à@1 @à~å±&àB’¹€ƒà B$ ƒä–Çÿö_ JàB@Ùá!KöÇ@x@¿$ €AJ( @€EÝr¢bwå±°Å`=)à=^…„FÿÿF„* =@®€{âÚ±`{áˆ% áˆ"ó+à@'Ö€@å ±"ó @âóå±û†Aë,àb†Õ€bᨠbá¨ÿ¨c- B@`æÀBå±@x@ßA$A¨.àEq²a,屯ÇBó/ =áj‰P³`=áj$áj%±0à@ìN€@ù 52€ @üy1àJXÀJA#â¸ñ2 B@aRàBå±@x@ÇA AR3àE~´aáÛä FÈÈAR4 =áR€=ç"½5à@~ €@å±F`|àDB½6àbê  b@6ájå±7 B@jÀBå±@x@ßA$Aj8àEÃà ˆá jÉÉAj9 =áj…Æ€=â½%-â½%±:à@àÅ€@â r$( @ãÅ"ü@B;àBE Bá Jå±< B@JàB áå±@x@¿AAJ=àEŒ~µbµÿÿBÊß`=>à= ‘„F‡ÿÿ* ? =@®€Á‘æÄ`{áˆ% áˆ"ó@à@Öá€@ñ $F @å±ÃBóå±ÚGA¨Aàb9 bᨠbá륱B B@`æÀB è ¥!¨üy@x@ßA$A¨CàE œÅa,å±àáBóD =ájD\Æ`=áj%páj"´Eà@¦Z€@â m!j @å±FàJÀJ â¸å±ÿÏ aRàBñ@x@ÇA ARH ˆ@`E-Çaàˆä FââARI =áRÕ€=áR"½Jà@5€@å±H`|à¥JB½Kàb¡ b@6ájå±L B@a$àBå±@x@ßA$AjMàE´Îà ˆá jããAjN =áj@Ò€=áj%pâ½%±Oà@›Ñ€@å$±PàBüЀBá Jå±Q B@JÀBñ@x@¿AAJRàE;ŠÈbµëcäù`=Sà=¼¤Få±T = ÀÁ@ò×`{áˆ% áˆ"óUà@„í€@ëc"óå±;€AëVàbäì€bᨠ£å ±W B@`æÀBëc@x@ßA$A¨XàEϧØa,å±úûBóY = …`ƒhÙ`=ê%±Zà@if€@å,±[àJÕe J äFå±\ B@€ÎÀBëc@x@ÇA AR]àEÜÚaÿÿôüüAR^ =áR”&€=â½&Ãâ½_à@ô!€@å±J`|à„‹B½`àb`Àb å±a B@jÀBå±@x@ßA$AjbàEcÚ€ëájýýAjc = …€ƒóÝ€=ájå ±dà@M @á ëceàB¯Ü€ƒà Bå±f B@€ÆÀBå±@x@¿AAJgàEé•ÛbµÿÿBþ@=hà=k¨„Fÿÿ/¼i =áˆïýê`{áˆ% âójà@3ù€@å ±)ø @ëc"óå±ABókàb“ø€bá¨ å ±l B@a¨ÀBå±@x@ßA$AjmàE}³ëa,üyBón =ájªsì`=å"±oà@ r€@å,±pàJxq JA#äFå±q B@aRàBå±@x@ÇA ARràE‹*íaàˆé øaRs =áR;2€=áR(otà@›-€@å±L`|àäˆB½uàbÀb@6ájëcv B@jÀBÿÿ@Ò@x@ßA$AjwàEæ€ëêäá jajx =ájšé€=ä(å ±yà@ôè€@à~öÇzàBV Bá Jå±{ B@JÀBå±@x@¿AAJ|àE˜¡îbµëc-e±}à=´„FÿÿWš~ =ሞ þ`{áˆ% å±à@â€@ñ ': @å±bóå±zBBó€àbB bá ë!¨ bë c B@a¨ÀBå±@x@ßA$A¨‚àE,¿€ëå±./bóƒ =ájXÿájê+c„à@»}€@å,±…àJ'ÀJA#äF屆 B@aRàBå±@x@ÇA AR‡àE:6.@Tàˆä F00aREZáRÑ=€=â½áR%±‰ ~à@29€@@Vá"½ @â½N`;àÅIB½Šàbž8 b@6áj屋 B@jÀBå±@x@ß@ÍAjÿ `EÀñà ˆá j11aj ƒá)Eõ€=çå ±Žà@£ô€@ã Å!j @ãÅ"üAJàBàB àB¼y B@aàB@Ùá5üy@x@¿AAJ‘àEG­bµå±2Ge±’à=ȤF‹öÇ“ =@®€ÁM`{áˆ% å±”à@€@ëc"óå±[ƒA¨•àbð€bâ ó!¨ £áë¶Ç– B@`æÀBå±@x@ßA$A¨—àEÛÊ€ˆå±HIbó˜ =áj ‹áj$áj"´™à@n‰€@å,±šàJÚˆ JA# @èðå±› B@aRàBÿÿWš@x@ÇA ARœàEéAaãÇä FJJaR =áR˜I€=â½áR%±žà@ùD€@Dãá$F @å±P`|à%y…H¼à£eÀb@6ájå±cM jÀBzù …ÿÿ,@x@ßA$Aj¡ ë@`Eoý ë@ ˆá jKKaj¢ =àÆïa¨ç%pâ½6Ç£à@M @à~ëc¤àB³ÿà ÁÀB$ ƒäŸÿ,¥ B@aàBÿÿWš@x@¿AAJ¦àEö¸ å±Lae±§à=wË„FÿÿQè¨ =áˆø $aˆâó% áˆ"ó©à@;€@bc¢ÁE"ó @âóå±»mªàbŸ€bá ë!¨ b᨟ÿ,« B@a¨ÀBüy@x@ßA$A¨¬àEŠÖ€ˆå±bcbó­ =áj–%áj$áj%±®à@!•€@å,±¯àJ” J áµå±° B@aRàBÿÿF„@x@ÇA AR±àE˜M&aå±ddRh² =áR?U€=â½áR"½³à@ P€@å±R`|àÿê´àb  bàáà å±µ B@jÀBå±@x@ßA$Aj¶àE 'ajáóå ±eeaj· = …€ƒ¢ €=å"±¸à@ @á $(ån"üEô¹àB^ €ƒàÁÁJ$àB¥±º B@€ÆÀBñ@x@¿AAJ»àE¥Ä€ˆå±f{@=¼à=&ׄFå±½ =!? Áª,7aÆáˆ% äF"ó¾à@î'€@Gßãûö Ç"ó屚ÿ Ò¿àbN bᨠ£å ±À B@a¨ÀBëc@x@ßA$AjÁàE9â€ëå±|}bó =ájl¢8ájä^"´Ãà@Ì €@å,±ÄàJ8ÀJA#å±Å B@aRàBå±@x@ÇA ARÆàEFY9aû~~aRÇ =áRí`€=â½áR"½Èà@N\€@å±T`|àdÿNÉàbº[ b@6ájå±Ê B@jÀBñ@x@ßA$AjËàEÍ:ájajÌ =ája€=áj%-ä(+cÍà@¾€@á å±ÎàBàBå±Ï B@JÀBå±@x@¿AAJÐàETЀËå±€•e±Ñà=Õ¤FüyÒ =áˆ]8JaÆáˆ% áˆ"óÓà@œ3€@LFâ!ˆ @ëc"óå±úOAëÔàbý2€bá ë!¨ bå ±Õ B@a¨ÀBÿÿ–@@x@ßA$A¨ÖàEè퀈ÿÿ,–—J× =ájÿ­Kajê%±Øà@_¬€@å,±ÙàJË«€Jè ðëcÚ B@aRàBÿÿ/¼@x@ÇA ARÛàEõdLaáÛé ø˜˜aRÜ =áR˜l€=áR"½Ýà@ùg€@å±V`|àEÿ²ÞàbeÀb@6å±ß B@jÀBëc@x@ßA$AjààE| Máj™™ajá =áj$€=ä(å ±âà@a#€@á $( @ãÅ"üDãàBÀ"àB$àB«cä B@JÀBå±@x@¿AAJåàEÜ€ˆå±š¯e±æà=„îJaˆÿÿ* ç = ÀÁD]`=áˆ% å±èà@K?€@ñ ë c"óå±Ûÿ!šéàb¬>€bá ë!¨ £áë±ê B@`æÀBñ@x@ßA$A¨ëàE—ù€ˆå±°±E±ì =ájɹ^ájê"´íà@*¸€@ù 5!j @ÿÿ îàJ–· J äFå±ï B@aRàBå±@x@ÇA ARðàE¤p_aáÛå ±²²aRñ =áRGx€=å"±òà@¨s€@å±X`|à¤y…óàbÀb@6ájaµâ½á•|ô B@jÀBå±@x@ßA$AjõàE+,`áj³³ajö =áj§/€=ä(%-ä(+c÷à@ @â rå±øàBg.€ƒá J$ ƒáJ¥±ù B@JàB@Ùá8üy@x@¿AAJúàE²ç€ˆå±´Ée±ûà=3ú„Få±ü =@®€Á³OpaÆáˆ% áˆ(¥ýà@öJ€@ñ !ˆ @ëc"óå±:mþàb[ bᨠbᨖÇÿ B@`æÀB ù »!¨ÿÿB@x@ßA$A¨@/ €EFqa,å±ÊËbó =ájpÅ€=áj%páj%±à@ÐÀ@â må±àJ<ÀJ@Üâ¸å± B@a àBñ@x`Ç =AR @`ES|raRàˆå ±ÌÌaR =áRýƒ€=áR"½à@_€@å±Z`|à…ÿêàbË~ b@6ájå± B@a$àBå±@x@ßA$Aj àEÚ7sájÍÍHo =ájj;€=áj%-â½%± à@Æ:€@á $( @ãÅ"ü@B àB&àBå± B@JÀBëc@x@¿AAJàE`ó€Ëÿÿ Îãe±à=â¤Få± = ÀÁf[ƒaÆáˆ% áˆ"óà@¨V€@ëc"óå±ÿ Òàb  bá ë!¨ £á륱 B@`æÀBöÇ@x@ßA$A¨àEô„a,öÇäåbó =ájÑ€=ê"´à@Ï€@å,±àJëΠJA#äFå± B@aRàB{ÿÿ; @x@ÇA ARàEˆ…aRàˆå ±ææaR =áR €=áR"½à@‹€@å±\`|àåÿNàbnŠ b@6ájå± B@jÀBÿÿ@Ò@x@ßA$AjàE‰C†ajêäá jççaj =ÀƒG€=ä(%pä(%±!à@uF€@á å±"àBÕEàB$ ƒäŒ®# B@JÀBå±@x@¿AAJ$àEÿ€ˆÿÿ¦èýe±%à=ƒ|ÿÿÞ& =ÀÁg–aÆáˆ% áˆ"ó'à@[b€@å ±!ˆ @ëc"óå±{ÿ6(àb½a€bâjþ" bᨋc) B@a¨ÀBå±@x@ßA$A¨*àE£—a,ÿÿ; þC‰A+ =ájÑÜ€=ájë c,à@2Û€@å,±-àJžÚ JA#äFå±. B@aRàBå±@x@ÇA AR/àE±“˜aRàˆä F!ðAR0 =áRW›€=å"±1à@¹–€@å±^`|àÄÿ²2àb%Àb@6ájB'â½±3 B@jÀBå±@x@ßA$Aj4àE8O™ajãFá jaj5 =ájÀR€=áj%-ä(%±6à@ @à~ÃÅ$( @ãÅ(®@B7àB|Q€ƒà Bå±8 B@JÀBÿÿnb@x@¿AAJ9àE¾ šaJ•ÿÿ/¼@=:à=Cƒ|ÿÿt; = ÀÁÄr©`{äF% áˆ%±<à@n€@ñ /ª @å±ÃBóå±ZJA¨=àbhm€bᨠbá륱> B@`æÀBå±@x@ßA$Aj?àER(ªa,ëcbó@ =áj|è€=áj#ºáj"´Aà@Ýæ€@å,±BàJIÀJ äFå±C B@aRàBå±@x@ÇA ARDàE`Ÿ«aRàˆä FaRE =áR§€=áR"½Fà@h¢€@å±``|à%\B½GàbÔ¡ b@6ájå±H B@jÀBå±@x@ßA$AjIàEæZ¬ájajJ =ájo^€=å"±Kà@Ê]€@á å±LàB*àBå±M B@JÀBëc@x@¿AAJNàEm­aJå±1e±Oà=î¤FñP =áˆ~¼`{áˆ% äF"óQà@´y€@ëc"óå±»–BóRàb bá ë!¨ £å ±S B@a¨ÀBöÇ@x@ßA$A¨TàE4½a,å±23bóU =áj'ô€=ájå ±Và@ˆò€@å,±WàJôñ J äFå±X B@aRàBÿÿ* @x@ÇA ARYàE«¾aRçå ±44aRZ =áR¨²€=áR"½[à@ ®€@å±b`|àB½\àbw­€bàáÊ[å±] B@jÀBÿÿ; @x@ßA$Aj^àE•f¿ajáóá j55aj_ =ájj€=ç+"ä(+c`à@yi€@á å±aàBÝh€BàÁÁJ$ ƒäèFb B@JàB@Ùâø:Ðüy@x@¿AAJcàE"ÀaJå±6Ke±dà=¤Fˆñe =@®€Á!ŠÏ`{áˆ% áˆ"ófà@g…€@ëc"ó屚WAëgàbÅ„€bᨠ£á¨‘h B@`æÀBëc@x@ßA$A¨iàE°?Ða,å±LMbój =ájåÿ€=ájå ±kà@Gþ€@â m,Î @ÿÿ,làJ³ý JA#â¸å±m B@aRàBå±@x@ÇA ARnàE½¶ÑaRàˆä FNNaRo =áRS¾€=å"±pà@¶¹€@å±d`|àdžB½qàb" bä ©å±r B@jÀBå±@x@ßA$AjsàEDrÒájOOajt =ájÌu€=â½%-ä(%±uà@( @á å±vàBˆt€ƒâ,å±w B@JÀBëc@x@¿AAJaz €EË-ÓaJå±Pee±m €=L@£|å±^páˆЕâ`{áˆ% áˆ%±{!à@‘€@å ±!ˆ @ñbóå±úTAë|àbt€bᨠbå ±} B@a$ÀBå±@x@ßA$A¨ÿ|ê €E_Kãa,ÿÿ,fgbó ƒá)„ ä`=ä^1€à@æ €@å,±àJRÀJ 层 B@a àBÿÿ$X@x@ÇA ARƒàElÂ Ó  ˆå ±hhaR„ =áR Ê€=â½&Ãâ½…à@lÅ€@å±f`|àE_B½†àbØÄ b@6 áÄf屇 B@jÀBcøàBÿÿ@Ò@x@ßA$AjˆàEó}åbàˆá jiiaj‰ =áj{€=ájå ±Šà@Ö€€@à~Á$( @ãÅ"üAJ‹àB7àBëcŒ B@JÀBå±@x@¿AAJàEz9æaJüyje±Žà=ûK„Fÿÿ5n =ሃ¡õ`{áˆ% âóà@Èœ€@ âM)ø @å±ÃBóå±Û•A¨‘àb' bá ë!¨ báë«c’ B@a¨ÀBå±@x@ßA$A¨“àEWöa,0ôÀëâóA€ƒbó” =ájIŽø`=å"±•à@ªŒ€@å,±–àJÀJ ®ÀÉÃ>ëc— B@aRàBëc@x@ÇA AR˜àE)EùaàˆáRA@„„aR™ =@€ƒÚL€=â½å ±šà@5H€@A¸å $F @â½ K €þuµÿ€% ”à +EB½›àb¡G€bàáÁj屜 B@€æÀBFüà¥å±@x@ßA$AjÿZˆ `E¯úáj……dfžàƒª¢ûïâû3 j 2?uŸ ᨬ€{âû#ÂäfTÀà@ë€@ÿÿ©á¨„ yA.TÔŒø›”hv¼ï/GÌï?üeA¨¡à£K bá ¨äf¢ B@bÀB¨@!$€ ÿÿ E@x@ßA$A,£ /àëЀëâÕ&Ûá,,Τà@, @ì$Î¥àB€ƒà B! ƒås’€¦ B@a àBä ®! ò€@x@¿ABµ§àE6¼€ˆð׆‡d AzáJZ|ûbóâw#ßáJ((© ~à@½z€@à Ä&Þ @ðתàJ)ÀJ þâås« B@RÀBx#ƒàBÿÿ–@x@ÇA AR¬àED3üaáÛå sˆˆaR­ =áß:€=áR-ã®à@<6€@ÿ.¨ƒã$ aRås%s€ ì (à C¯àb¨5€bá jås° B@jÀB~@à¥ås@x@ßA$Aj±@/ `EÊîà ˆá j‰‰es²à=Ò¢ûôw!~àƒås¿ÿVD%s³ = )€{·ô€{á¨å s´à@ð€@ÿ, à~„Í"û @á¨Ãá¨å sÖ#A¨µàbfï€bá ¨$f bäf•>¶ B@bÀBhWà¥!©ÿÿ¹@x@ßA$A,· E ã€Eûö€ˆá,$ïä(%s¸à@X @à É"Õ @àÉ$g@B¹àB·õ€ƒã—Áo! àB¥sº B@a àBÿÿY@x@¿ABµ»àEQªýd åsŠ‹d ¼ =áJ{jþ`=áJ%2áJ! ½à@Üh€@å,s¾àJHÀJA#âås¿ B@RÀBêç@x@ÇA ARÀàE^!ÿaàˆä ŒŒaRÁ =áR)€=áR%sÂà@_$€@ê ç€þQï€þäõd0`zÀÿxÃàbË#€bâ ½åsÄ B@jÀBQàá­ås@x@ßA$AjÅàEåÜ€ˆájajÆ =ájmà€=é"|Çà@É߀@á äÈàB) BàÁäÉ B@JÀBð™@x@¿AAJÊàEl˜/@Tàˆâ µŽŽf¾Ëà=í¤F{ï.æ ¾My &¾Ì =áˆ\ž€{áˆ&äF"óÍà@¦™€@ÿ3ãû!ˆ @æ¾,2â óì2¶ÿTÎàb báᨠbåô¦¾Ï B@a¨ÀBmñà¥"ôæ¾@x@ßA$>ÐàEóSa¨öKbóÑ =áj`=ájå ±Òà@e€@å,±ÓàJÑ J þáå±Ô B@aRàBÿÿb@x@ÇA ARÕàEËà ˆâ û‘‘aRÖ =áR¶Ò€=e6áÖá R"½×à@΀@”Cà@"½!“â½+%± Enter ICPäõ«ua øMHoØàbtÍ€bá jå±Ù B@jÀBTØà¥å±@x@ßA$AjÚàE‡†bàˆá j’’ajÛ =ájŠ€=â½)6ä()ºÜà@k‰€@å$±ÝàBψ€BàÁä ƒä.Þ B@JàB@Ùáð!Kï.@x@¿AAJßàEBaJàËá J““e±àà=T„Fwå±á =@®€ÁH€{áˆ% áˆ"óâà@HC€@û ýì pâ óå±\¿Aëãàbªá ᨠ£á¨Œpä B@`æÀBP³á ¨å±@x@ßA$A¨åàE”ý€ëëc”•bóæ =ájº½aæáj$áj+cçà@¼€@â m!j @ð×èàJ‡» J!áå±é B@aRàBå±@x@ÇA ARêàE¢taàˆâ û––aRë =áRD|€=å"±ìà@¦w€@ûý ;¬ u­"aà:£B½íàbÀb@.ä#å±î B@jÀBy@àBûý@x@ßA$Ajï@/ `E(0áj——dfðà=0þ‚û÷ð×ñ =á¨6€{âû#Ââû$fòà@e1€@—ç Z @çZÁ¨çZäf˜VA¨óàbÄá äfô B@bÀBWÌá¨!©äf@x@ßA$KEõ Eá,I8€ëá,&˜á,'öà@©7€@æPÀÉ(‡ @å0$g@B÷àB  Bâ µð×ø B@a àB@Ùâµ! ç@x@¿ABµùàE¯ë€Ëås˜™d ú =@p€ƒÛ«bóä ! ûà@>ª€@à ÄåsüàJª© J þáåsý B@€ÎÀBöK@x@ÇA ARþàE½b aâå sššaRÿ =áRxj€=â&…â#Êà@Ée€@˜ã Ê @ãÊC€þåuTÿ"€þ8 !¤à=B½àb5 bá jås B@jÀB‹ üá­ås@x@ßA$AjàEC ajàˆá j››aj =áj«!€=áj#Çáj$à@  @à~äàBo €ƒà Bä B@JÀB„@Ùä@x@¿AAJàEÊÙà ˆá Jœœf¾ à=Kì„Fzë% =@®€ÁÆß€{áˆ&}áˆ"ó à@Û€@ý $F @âó7–âóæ¾úAë àbfÚ€bᨠbåô«% B@`æÀBZ‚á ¨æ¾@x@ßA$A¨àEQ• bóðמbó =ájU `=å"±à@äS€@å,±àJP J‡!áå± B@aRàBå±@x@ÇA ARàE^ aÿÿ:fŸŸaR =áR€=â½%pâ½à@r€@û ýS€þaà €pB½àbÞ€bâ ½å± B@jÀBö‰@x@ßA$AjàEåÇà ˆä f  dfà=í¢ûøë% =á¨ÝÍ€{á¨#Âá¨à@É€@ä f)ø @äf,ÎA¨çZäfÏòA¨àbÈ€bá ¨& bä f B@a¨ÀBïŒ@x@ßA$K% Eá,Ѐˆá,ç à@oÏ€@à É!, @àÉHt'_ àBÑÎàB! àB²B! B@a àB÷ô@x@¿ABµ"àElƒd ås¡¢d # =áJ’C`=å"s$à@óA€@ÿ,ÿ4v%àJ_ÀJA#è,ås& B@RÀBûý@x@ÇA AR'àEyú€Óâä ££aR( =áR+á$R(1)à@ý ~ë%[„aà ‡¢B½*àbùü b@1ájås+ B@jÀBð™@x@ßA$Aj,àE¶,àˆá j¤¤es-à=„‚ûöås. =á¨ø»€{äF$Ïås/à@<·€@õ a¨ìÎåsA¨0àbœá ËÆ$f £ä©©Ú1 B@a¨ÀBås@x@ßA$Es2 Eá,$¾€ëá,$ïá,%03à@€½€@‡ ]ârås4àBༀBâ,á ƒá —¶5 B@a àBås@x@¿ABµ6àE‡qcâås¥¦d 7 = …€ƒž1`=áJé”&8à@0€@ê,ç9àJm/ J á•ås: B@€ÎÀBÿÿ @x@ÇA AR;àE”èà ˆä §§aR< =áR4ð€=æÆ%s=à@”ë€@å sc9aà ¶¼C>àbÀb@1ås? B@jÀB{ð™@x@ßA$Aj@àE¤bàˆá j¨¨esAà=#r‚ûÿÿæB = ¹ Á ª€{âû$ÏäF!¨Cà@Q¥€@å s$F @êça¨ås0ÿDàb·á äf bäfštE B@a¨ÀBÿÿ'@x@ßA$EsF Eá,3¬€ëá,$ïá,%sGà@‘«€@å$sHàB÷¡°â,á ƒá …sI B@a àBò@x@¿ABµJàE¢_bµås©ªd K =áJÓ`=ä %sLà@4€@å,sMàJ  JA#äåsN B@RÀBêç@x@ÇA AROàE¯Öà ˆä ««aRP = …€ƒLÞ€=âæÆ#ÊQà@§Ù€@ð [k*ç? à w¡CRàb bá jêçS B@€æÀBÿÿÎ@x@ßA$AjTàE6’bàˆá j¬¬esUà=>`‚ûêçV =á¨.˜€{á¨$ÏäF!¨Wà@q“€@ð [ô Âa¨åsñA¨XàbÒá äf £äf…sY B@a¨ÀBêç@x@ßA$EsZ Eá,^š€ëá,$ïá,%s[à@¹™€@áéÀÉ&  @àÉ$g@B\àBàB+ôàB¥s] B@a àBêç@x@¿ABµ^àE¼Mbµû­®d _ =áJä `=å"s`à@C €@å,saàJ¯  J åsb B@RÀBÿÿ? @x@ÇA ARcàEÊÄà ˆä ¯¯aRd =áR}Ì€=áR#Êeà@ÖÇ€@ÿ ÿ¨smÅ+)¤¨ºÿÿà7ÿœþfàbB bá jåsg B@jÀB‚êç@x@ßA$AjhàEQ€bèýá j°°aji =ájÕƒ€=ä"jà@2 @à~äkàB‘‚€ƒá Jäl B@JÀBä@x@¿AAJmàE×;aJàˆá J±±f¾nà=XN„F}ûo =áˆÌA€{âó&å‘"ópà@=€@ÿ ÿô%‘ @ì2â óÿÿA“ÿ[šqàbs<€báÁë!¨ båô¦¾r B@a¨ÀB â ó>vû@x@ßA$A¨sàE^÷€ˆå±²³bót =áj~·aæájéÒ&½uà@áµ€@å,±vàJMÀJ þå ±w B@aRàBû@x@ÇA ARxàElnaãÇâ û´´aRy =áRv€=áR"½zà@|q€@𠙃Ի‰‚Šƒà"Pç"»a GÂHo{àbèp€bàá½å±| B@jÀBzâ ½ÿÿ4@x@ßA$Aj}àEò)ajáóá jµµaj~ =ájf-€=å"±à@Ä,€@á $(éw"üAJ€àB" BàÁÁJ àB©º B@JÀBå±@x@¿AAJ‚àEyå€Ëàˆá J¶¶e±ƒà=ú¤F屄 =áˆeë€{áˆ% å±…à@µæ€@è ¥ì pâ óå±ã0A¨†àb báᨠ£å ±‡ B@a¨ÀBÿÿË@x@ßA$A¨ˆàE¡bóð×·¸bó‰ =áj8a`=ä^"´Šà@›_€@û,¿‹àJÀJA#ëcŒ B@aRàBå±@x@ÇA ARàE  aáÛâ û¹¹aRŽ =áRÁ€=áR%±à@€@ð דþã"E°ä“"» å‚a »B½àb‰€bàá½å±‘ B@jÀBå±@x@ßA$Aj’àE”Óà ˆá jººaj“ =áj ×€=ä(%pä(/l”à@jÖ€@â r展àBÌÕ€Bá Jå±– B@JàB@Ùâø&ýÿÿ ï@x@¿AAJ—àE!bµàËá J»»e±˜à=œ¤Fyëc™ =@®€Á •€{áˆ% áˆ"óšà@W€@—þ ³ @ëcâ ó屫ÿP6›àb·á Âó!¨ bå ±œ B@`æÀB÷–@x@ßA$A¨àE¡J"a¨ëc¼½bóž =ájÅ #`=ájë cŸà@( €@â m' @ÿÿ  àJ” J ⸠`JáR-—Ü¡ B@aRàBûý@x@ÇA AR¢àE¯Áà ˆâ û¾¾aR£ =áR_É€=å"±¤à@»Ä€@å ±£)õ‚åƒ:õKbé%‚øa ¹gD©¥àb' bá j屦 B@jÀBå±@x@ßA$Aj§àE6}$bÿÿÎí¿¿aj¨ =ájÊ€€=ájä(%±©à@$ @äåÃÅ屪àB†€ƒà B$ ƒâµ«c« B@JÀBÿÿ!y@x@¿AAJ¬àE¼8%aJáÓâ µÀÀe±­à==K„F~å±® =áˆÀ>€{äF% 屯à@ :€@ÿ ÿp$F @å±Ãâ óå±ÿ. °àbl9€báÁë!¨ bᨗԱ B@a¨ÀBå±@x@ßA$A¨²àECô€ˆå±ÁÂbó³ = …`ƒk´&aæáj%páj+c´à@β€@å,±µàJ:ÀJ å ±¶ B@€ÎÀBå±@x@ÇA AR·àEPk'aàˆâ ûÃÃaR¸ =áRõr€=äFáR(o¹à@Qn€@å ±³æ"»þeªâ"å‚)õëc:D©ºàb¼m€bàá½å±» B@jÀBÿÿ ˜@x@ßA$Aj¼àE×&(ájÄÄaj½ =ájc*€=â½â½%±¾à@¾)€@å n屿àB Bá J$ ƒäˆgÀ B@JÀBñ@x@¿AAJÁàE^â€Ëàˆâ µÅÅe±Âà=ߤFÿÿJà =áˆJè€{áˆ% äF"óÄà@—ã€@—ëcâ ó屺ÈAëÅàbúá Âó!¨ £á¨…±Æ B@a¨ÀBñ@x@ßA$A¨ÇàEå)bóñÆÇbóÈ =áj ^*`=ájä^%±Éà@o\€@å,±ÊàJÛ[ JA#ëcË B@aRàBÿÿî@x@ÇA ARÌàEò+aàˆâ ûÈÈaRÍ =áRš€=áR"½Îà@ö€@å ±Ãk`1¼ ¾)ïa ÎÿfþÏàbb bàá½å±Ð B@jÀBå±@x@ßA$AjÑàEyЀëàˆá jÉÉajÒ =ájÔ€=áj%-ä(%±Óà@`Ó€@å$±ÔàBÁÒ€BàÁÁJ$ ƒä…±Õ B@JÀBå±@x@¿AAJÖàEÿ‹,âµÊÊe±×à=ž„F屨 = ÀÁð‘€{áˆ% áˆ"óÙà@=€@å ±ë câ óå±j¶AëÚàbœŒ€bá ë!¨ £á¨…±Û B@`æÀBëc@x@ßA$A¨ÜàE†G-a¨ëcËÌbóÝ =áj«.`=ê%±Þà@ €@å,±ßàJy JA#å±à B@aRàBëc@x@ÇA ARáàE”¾à ˆä FÍÍaRâ =áRTÆ€=â½ç"½ãà@¤Á€@ÿ ÿ®Óð„ÿ­ð"äÌøuðï/ÿîa Ì^B½äàb bá jå±å B@jÀBŒöÇ@x@ßA$AjæàEz/bàˆá jÎÎajç =áj«}€=â½%-ä(%±èà@ @à~ÅnëcéàBf|€ƒà B$ ƒä…±ê B@JÀBå±@x@¿AAJëàE¡50áJÏÏe±ìà="H„Fÿÿ.†í =áˆ;€{âó% áˆ"óîà@ß6€@£ëcâ óå±h¬Aëïàb= bᨠ£á¨…±ð B@a¨ÀB¨â ó8pÿÿ,@x@ßA$A¨ñàE(ñ€ëëcÐÑbóò =ájM±1aæâóä^%±óà@¯¯€@å,±ôàJÀJ þå ±õ B@aRàBå±@x@ÇA ARöàE5h2aàˆä FÒÒaR÷ =áRòo€=â½áR"½øà@Ek€@å ± ã3þì3üîì˜@ þÃ`ÕcB½ùàb±j€bàáÄ©å±ú B@jÀBÿÿ.†@x@ßA$AjûàE¼#3ájÓÓajü =áj<'€=áj%-Á @3ÒAýà@—&€@á å±þàBø%àBå±ÿ B@JÀBå±@x@¿AAJ  €ECßà ˆâ µÔÔe±à=ĤFå± =AT€{?å€{áˆ% áˆ"óà@€à€@¤å ±ë câ óå±q‘Aëàbßá Âó!¨ £å ± B@`æÀBå±@x@ßA$A¨àEÉš4bó¼ Zª™á¨ ÕÖbó = `ƒóZ5`=å"±à@TY€@å,± àJÀX J ÀÉÁRüy B@€ÎÀBñ@x@ÇA AR àE×6aàˆáR@s××aR =@€ƒŒ€=â½å ± à@ç€@â ½$F @ù…K óÕðéäÎý"íøõðî„ Ò@žÀµÿi¼àbS bàáÁjå± B@€æÀBÿÿ@x@ßA$AjàE^Í€ëàˆá jØØaj =ájæÐ€=ájå ±à@A @à~Á!j @ãÅ(®DàB¢Ï€ƒˆ âµ$àB«c B@J¢ Bå±@x@¿AAJàEäˆ7âµÙÙe±à=f›„Få± =áˆÕŽ€{äF% å±à@Š€@ÿ ÿ@©!ˆ @âóå±ÿÞ˜àb€‰€bˆAy¥(Áë!¨ báë«c B@a¨ÀBñ@x@ßA$A¨àEkD8a¨å±ÚÛbó = …`ƒ™9`=áj$áj"´à@ú€@å,±àJfÀJ áRå± B@€ÎÀBå±@x@ÇA AR àEy»€Óòñä FÜÜaR! =áR.À=áRc"à@¾€@ëcþ­ð‘í3ý@˜PàµÿŽÜ#àbí½€bàá½å±$ B@jÀBÿPãÿÿ @x@ßA$Aj%àEÿv:báóá jÝÝaj& =ájƒz€=å"±'à@ßy€@á $(ån"üE±(àBC BàÁÁJ$àB¥±) B@JàB@ÙáJ!KÿÿÞ@x@¿AAJ*àE†2;áJÞÞ@=+à=E„Fÿÿ9ê, =@®€Á~8€{áˆ% äF"ó-à@À3€@ëcâ óå±µA¨.àb" bᨠ£å ±/ B@`æÀB¤ñ@x@ßA$Aj0àE î€ëÿÿ-qßßbó1 = …`ƒ­ò€=ájå ±2à@ ñ€@â m,Î @ÿÿÞ3àJyð J â¸å±4 B@€ÎÀB|LWâûÿÿJÂ@x@ÇA AR5àE“©<âûààaR6 =áRC±€=å"±7à@ ¬€@üyÕðò"ØýÕðê"Åðøa sÿN8àb  bä ©å±9 B@jÀBÿÿJÂ@x@ßA$Aj:àEe=ájááaj; = …€ƒ¢h€=â½%-ä(6Ç<à@ @á å±=àB^g€ƒâ µå±> B@€ÆÀBëc@x@¿AAJ?àE¡ >áJââe±@à=­¤Fòÿÿ*›A =!? Á•&€{áˆ% áˆ%±Bà@Ý!€@›î W @ëcâ óå±×—AëCàb= bᨠbå ±D B@a¨ÀB å±@x@ßA$A¨EàE(Ü€ëöÇãäbóF =ájKœ?aæáj%páj(fGà@®š€@å,±HàJÀJ ånå±I B@aRàBöÇ@x@ÇA ARJàE5S@aàˆé øååaRK =áRåZ€=å"±Là@AV€@å ±#£à(ð%¨å‚‚pƒàa ´B½Màb­U€bàá¿ÿO.å±N B@jÀBÿÿ @x@ßA$AjOàE¼AájææajP =áj4€=â½å ±Qà@’€@å$±RàBø€Bá Jå±S B@JÀBÿÿ(#@x@¿AAJTàECÊ€ˆàËâ µççe±Uà=ĤFuÿÿ V =áˆ3Ѐ{áˆ% äF%±Wà@Ë€@—å ±)ø @å±+câ óå±ðBóXàbßá Âó!¨ bå ±Y B@a¨ÀBñ@x@ßA$A¨ZàEÉ…Bbóñèébó[ =ájýEC`=ájå ±\à@`D€@å,±]àJÌC JA#å±^ B@aRàBå±@x@ÇA AR_àE×üà ˆâ ûêêaR` =ÀƒoDaRå"±aà@Ïÿ ~â ½)ø!“â½(o 38ð"£øàÅð%ððeµ ŒàÙB½bàb;Àb@&ájå±c B@jÀBÿÿ/@@x@ßA$AjdàE]¸,àˆá jëëaje =ájÞ»€=ájå ±fà@< @äåÃÅ!j @ãÅ(®AJgàBžº€ƒà B$àB±h B@JàB@Ùáñ@x@¿AAJiàEäsEbwáJììe±jà=e†„FÿÿÞk =@®€ÁÔy€{äF% å±là@!u€@—â ó @âóå±}üA¨màb€t€báÁë!¨ báë¶Çn B@`æÀBå±@x@ßA$A¨oàEk/F/@Tëcíîbóp = 0`ƒï€=áj$áj"´qà@òí€@å,±ràJ^ÀJ â¸ëcs B@€ÎÀBå±@x@ÇA ARtàEx¦GaRàˆä FïïaRu =áR$®€=áR(ovà@„©€@å ±CKW È8ðè"ìöíöÿÿÞ‘ÿ3Iwàbð¨ bA;ä#å±x B@jÀB|î !öÇ@x@ßA$AjyàEÿaHájððajz =áj‹e€=áj%-â½1{à@åd€@á å±|àBGàB$ ƒäŸÿÞ} B@JÀBñ@x@¿AAJ~àE†IáJññe±à=0ƒÇå±€ =áˆj#€{áˆ% áˆ"óà@¿€@“å ±ë câ óå±5ñAë‚àb" bá ë!¨ £á¨ÿÇЃ B@a¨ÀBâ ó"ôÿÿÞ@x@ßA$A¨„àE Ù€ëå±òóbó… =ájE™Jaæè¥å ±†à@§—€@å,±‡àJÀJ äF屈 B@aRàBå±@x@ÇA AR‰àEPKaàˆå ±ôôaRŠ =áRÎW€=â½áR"½‹à@.S€@å ±Söïö"¨‚…ƒðЃЂ@ŒàÁ8B½ŒàbšR b@&ájå± B@jÀBå±@x@ßA$AjŽàE¡ Lájõõaj =@Ý€ƒ5€=áj%-`±€3ÒAà@€@á 屑àBñ àB$ ƒä…±’ B@€ÆÀBå±@x@¿AAJ“àE'Çà ˆâ µööe±”à=©¤F展 =áˆÍ€{áˆ% áˆ"ó–à@dÈ€@ÿ ÿ* !ˆ @ëcâ óå±eÊAë—àbÃá ëc˜ B@a¨ÀBÿÿQý@x@ßA$A¨™àE®‚Mbóñ÷øbóš =ájãBN`=å"±›à@EA€@å,±œàJ±@ JA#äFå± B@aRàBå±@x@ÇA ARžàE¼ù€ˆîèâ ûùùaRŸ =áRhOaRâ½å ± à@Èü€~å ±co äsä“£ÅàùB½¡àb4Àb@(ájå±¢ B@jÀBå±@x@ßA$Aj£àEBµ,àˆá júúaj¤ =ájº¸€=áj%-ä(+c¥à@ @à~ÃÅ$( @ånK@B¦àBz·€ƒà Bå±§ B@JàB@Ùá)ºÿÿ,@x@¿AAJ¨àEÉpPbwàˆá Jûûe±©à=Jƒ„Fÿÿ,ª =@®€Áµv€{áˆ%páˆ%±«à@r€@å ±ë câ óå±]äA¨¬àbeq€báÁë!¨ £áë«c­ B@`æÀB˜ëc@x@ßA$A¨®àEP,Qa¨¼ Z¡îᨠüýbó¯ =ájyì€=å"±°à@Ûê€@â m!j$Ÿ }@ Ã! @Ž … AR±àJG Jå(ÁRå±² B@aRàBå±@x@ÇA AR³àE]£RaRÿÿþþaR´ =áR«€=áR"½µà@Y¦€@î !$F!Rèo sƒÅðŃÈÅ‚Èð£Å@ a ~ÿê¶àbÅ¥€b⠽屷 B@jÀB„â ½ñ@x@ßA$Aj¸àEä^Sájÿÿaj¹ =ájxb€=ä(%pä(%±ºà@Óa€@á !j @ãÅ¥±»àB4àB$àB«c¼ B@JÀBëc@x@¿AAJ½àEkTaJàËäA8@=¾à=ð¤Fëc¿ =@O€Ág €{áˆ% áˆ"óÀà@´€@ÿÿ Ò @âóå±Úÿ ÒÁàb báÄ^!¨ bá륱 B@`æÀBöÇ@x@ßA$AjÃàEñÕ€ëëcAÄ =áj–Uaæä^"´Åà@t”€@å,±ÆàJà“ J ¦{ÁRå±Ç B@aRàBÿÿ @x@ÇA ARÈàEÿLVaàˆâ û ARÉ = …€ƒ°T€=áR"½Êà@P€@ÿ ÿ,ƒe­"Qøä“pt Œàûÿ®Ëàb{O€bàáÁjå±Ì B@€æÀBëc@x@ßA$AjÍàE†Wajç¥á jajÎ =áj €=ä(%-ä(%±Ïà@i €@à~Áå±ÐàBÊ €Bá J$ ƒä‘Ñ B@JÀBå±@x@¿AAJÒàE Äà ˆá Je±Óà=Ö„FÿÿBÔ =áˆñÉ€{âó% áˆ"óÕà@BÅ€@ë câ óå±_ÿõ`Öàb¨á Áë!¨ £á¨–Ç× B@a¨ÀBöÇ@x@ßA$A¨ØàE“Xbóå±bóÙ =áj±?Y`=áj)Ïáj%±Úà@>€@å,±ÛàJ‚= JA# @áRå±Ü B@aRàBvLWàBÿÿ @x@ÇA ARÝàE¡ö€ˆãÇâ ûaRÞ =áR>þ€=áR"½ßà@¡ù€@ñ ““p ££“øt“õ‚ˆƒäa n–D©ààb  bàá½å±á B@jÀBxëc@x@ßA$AjâàE'²Zbáóá j  ajã =áj·µ€=å"±äà@ @á å±åàBs´€ƒàÁÁJ$å ±æ B@JÀBå±@x@¿AAJçàE®m[áJ  e±èà=/€„Få±é =ሖs€{áˆ% äF"óêà@èn€@å ±!ˆ @ëcâ óå±ÊdBóëàbJ bᨠbå ±ì B@a¨ÀB”â ó.Xüy@x@ßA$A¨íàE5)\ᨠ bóî =ájÍ-€=ájå ±ïà@-,€@å,±ðàJ™+ J þånå±ñ B@aRàBÿÿB@x@ÇA ARòàE»äà ˆä F  aRó =áRsì€=å"±ôà@Ðç€@ë c£st“h`£€ßŠƒ‰‚a ÿ`]õàb< bä ©å±ö B@jÀB~â ½ñ@x@ßA$Aj÷àEB ]â½  ajø =ájÒ£€=â½"óä(+cùà@0 @á $( @ãÅ(®@BúàBŽ¢€ƒâ µå±û B@JÀBå±@x@¿ø =AJü @€EÉ[^áJe±ýà=ѤFÿÿRõþ =ሹa€{áˆ% áˆ%±ÿà@]€@—ô 5\ @å±Ãâ óå±+A¨à£e\€bᨠbáë«c B@abÀBñ@x@ßA$A¨àEP_a¨ÿÿ,bó =áj{×€=ájå ±à@ÞÕ€@å,±àJJÀJA#ë å± B@aRàBöÇ@x@ÇA ARàE]Ž`aRàˆå ±aR =áR–€=å"± à@q‘€@ë c³äsïN`ï`í» å±îÿF„ àbÝ bA;ájå± B@jÀBÿÿ @x 9A$Aj @`EäIaájaj =ájXM€=â½%pä(%±à@¶L€@å$±àB Bá Jå± B@aàBå±@x@¿AAJàEkbaJàËâ µe±à=ì¤FÿÿL6 =áˆW €{áˆ% áˆ%±à@§€@ÿ ÿÞ': @å±Kâ óå±JÿF„àb báí bå ± B@a¨ÀBüy@x@ßA$A¨àEñÀ€ëâ1á ¨bó =ájŒÅ€=ä^(fà@íÀ@å,±àJYÀJA#äFå± B@aRàBöÇ@x@ÇA ARàEx|c/@Tàˆá RaR =áR)„€=â½ç"½à@|€@î! @â½ÃŠƒð£ßüÞú"‰ðP÷ ß@½À_AD©àbè~€bàá½å± B@jÀBÿÿ/¼@x@ßA$Aj!àEÿ7dájaj" =ájƒ;€=ájå ±#à@Þ:€@á $( @ãÅ"üAJ$àB?àBå±% B@JÀBå±@x@¿AAJ&àE…óà Ëâ µe±'à=ÁƒÇõÿÿ¦( =áˆ~ù€{äF% å±)à@¿ô€@ÿ ÿ$Xâ óå±û³A¨*àb! bá ë!¨ £áë«c+ B@a¨ÀBÿÿ$X@x@ßA$A¨,àE ¯ebóöÇbó- =áj2of`=áj$áj"´.à@“m€@å,±/àJÿl JA#å±0 B@aRàBå±@x@ÇA AR1àE&ga‡@Y ˆâ ûaR2 =áRÏ-€=áR%±3à@")€@ÿ7!“å± Óü©ð"»þüó ß@ ç @¸À>®B½4àbŽ(€bá jå±5 B@jàBñ@x@ßA$Aj6àE áà ˆá jaj7 =áj(å€=å"±8à@„ä€@á å±9àBèãàB$åô¼y: B@JàB@Ù¢oÇ?2a3}@'@x@¿AAJ;àE'hbµàËá Je±<à=¨¤FÿÿÞ= =@®@™£€{áˆ% äF"ó>à@až€@å ±!ˆ @âó屚ÿ®J?àbÃá Áe!¨ bå ±@ B@`æÀBå±@x@ßA$A¨AàE®Xia¨ñbóB =ájèj`=ä^%±Cà@I€@ÿ ÿb2€$^ÿÿÞDàJµ JA#â¸ëcE B@aRàBå±@x@ÇA ARFàE»Ï€ˆÿÿÞaRG =áRe×€=áR"½Hà@ÇÒ€@ñ  ãößú€Fç ò@>ˆöÇoXE±Iàb3ÀbA;ájå±J B@jÀBÿÿsZ@x@ßA$AjKàEB‹kbàˆä f  ajL =ájÂŽ€=ä(%-ä(1Mà@" @â r$( @ãÅ"ü@BNàB‚€ƒàÁäàBièFO B@JàB@Ùáå±@x@¿AAJPàEÉFláJ!!e±Qà=JY„FüyR =@®€Á±L€{áˆ% áˆ"óSà@H€@î W$F @å±6Çâ óå±˪A¨TàbeG€bᨠbáë«cU B@`æÀBüy@x@ßA$A¨VàEOma¨ëc"#bóW =ájv€=ä^"´Xà@ÖÀ€@â m!jå±YàJBÀJA#â¸å±Z B@aRàBÿÿ‰¦@x@ÇA AR[àE]ynaRàˆä F$$aR\ =áR€=áR"½]à@a|€@ÿ ÿ óŒƒç 1ú€2ã E½a jB½^àbÍ{€bàáÄfå±_ B@jÀBëc@x@ßA$Aj`àEä4oajå2á j%%aja =ájp8€=ä(%-ä(%±bà@Ì7€@à~Áå±càB, Bá J)º ƒä…±d B@JÀBñ@x@¿AAJeàEjð€Ëàˆá J&&e±fà=ë¤F|3ã|ƒ†ßÿ¡=\ Ag =áˆ_ö€{âó% áˆ"óhà@¡ñ€@ @a€tሠ@å±6Çóâóå±ÎâAëiàb báÁë!¨ bá¨ÿ¦j B@a¨ÀB â ó'Zÿÿ,@x@ßg =A¨k @€Eñ«pbóàˆá ¨''ból =áj’°€=âó%páj%±mà@õ®€@å,±nàJaÀJ@Ýáµå±o B@a àBüy@x@ÇA ARpàExgqáR((aRq =áR,o€=â½áR"½rà@Œj€@ëc€nã‹_fˆ‚Œƒãñ§MB½sàbøi bA;ájå±t B@jÀBüy@x@ßA$AjuàEþ"ráj))ajv =áj{&€=áj%-â½%±wà@Õ%€@å$±xàB7àB$ ƒä…±y B@JÀBÿÿ]L@x@¿AAJzàE…Þ€ËáÓä **e±{à=¬ƒÇóöÇ| =áˆuä€{äF% áˆ"ó}à@Â߀@—å ±)ø @å±Ãâ óå±ÿ• ~àb! báå± B@a¨ÀBÿÿ,@x@ßA$A¨€àE šsbóëc+,bó =áj0Zt`=ájä^%±‚à@“X€@å,±ƒàJÿW J äF屄 B@aRàBå±@x@ÇA AR…àEuaàˆâ û--aR† =áR½€=áR"½‡à@€@ëck_Z‰‚Šƒà£ö1a 8LD©ˆàb€bàá½å±‰ B@jÀB„è oÿÿÞ@x@ßA$AjŠàE Ìà ˆá j..aj‹ =áj,Ѐ=ájä(%±Œà@‡Ï€@å$±àBè΀Bá J屎 B@JÀBëc@x@¿AAJàE'ˆvâµ//e±à=¨¤Fÿÿô‘ =ሎ€{áˆáˆ"ó’à@`‰€@ÿÿ€Gâ ó屜¾Aë“àbÃá Âó!¨ £çZ‹c” B@a¨ÀBëc@x@ßA$A¨•àE®Cwa¨öÇ01bó– = ƒÆx`=ájå ±—à@(€@å,±˜àJ” J å ±™ B@aRàBöÇ@x@ÇA ARšàE»ºà ˆä F22aR› =áR[€=áR"½œà@»½€@ë c#€N…­vÇB€Òa ÈìB½àb'Àb@$ç属 B@jÀBÿÿô@x@ßA$AjŸàEBvybàˆá j33aj  =ájÒy€=ájå ±¡à@, @à~ÃÅñ¢àBŽx€ƒà B$ ƒä‹c£ B@JÀBå±@x@¿AAJ¤àEÈ1záJ44e±¥à=JD„F屦 =ስ7€{áˆ% ëc§à@3€@“ëcâ óå±lÿ€…¨àbd2€bᨠ£á¨…±© B@a¨ÀBÿÿ* @x@ßA$A¨ªàEO퀈ëc56bó« =áj|­{aæîWå ±¬à@Þ«€@å,±­àJJÀJA#äFëc® B@aRàBëc@x@ÇA AR¯àE]d|aéyä F77aR° =áR l€=â½áR%±±à@mg€@å ±3€ú€Æ€Ô€U€ò€)€€¦a õUE±²àbÙf b@$ájå±³ B@jÀBå±@x@ßA$Aj´àEã}ajàˆá j88ajµ =ájl#€=â½%-ä(+c¶à@Æ"€@à~å±·àB+àB屸 B@JàB@Ùá,¯ÿÿ,@x@¿AAJ¹àEjÛà Ëá J99e±ºà=ë¤FwöÇ» =@®€ÁVá€{âó% áˆ"ó¼à@§Ü€@”å ±': @ñ+câ óå±Q§Aë½àb bá ë!¨ bå ±¾ B@`æÀBÿÿ @x@ßA$A¨¿àEñ–~bóëc:;bóÀ =áj"W`=âóä^1Áà@„U€@â m2€ @ÿÿ,ÂàJðT€Jå(Ã>å±Ã B@aRàBå±@x@ÇA ARÄàEþ €aáÛâ û<>e±Ïà=—„F{å±Ð =áˆøŠ€{áˆ% âóÑà@I†€@å ±$F @å±6Çâ óå±¥ÿeòÒàb¨á Ä^!¨ báë«cÓ B@a¨ÀBå±@x@ßA$A¨ÔàE’@‚a¨ëc?@bóÕ =ájă`=å"±Öà@%ÿ€~å,±×àJ‘þ JA#ä屨 B@aRàBñ@x@ÇA ARÙàE ·àˆä FAAaRÚ =áRI¿€=áR(oÛà@¤º€@ö ÇSŒ…Q ä“£’ð£’ßa <|yÜàb bàá½å±Ý B@jÀBÿÿnb@x@ßA$AjÞàE's„bç¥á jBBajß =ájŸv€=ä(%-ä(+càà@úu€@à~Áå±áàB[ Bá J$ ƒä‘â B@JÀBå±@x@¿AAJãàE­.…aJàˆá JCCe±äà=2Aƒ|ëcå =ሞ4€{âó% áˆ"óæà@ã/€@ÿÿ,â ó屘|yçàbI báÁë!¨ £á¨‘è B@a¨ÀBœù »(¦ÿÿ,@x@ßA$A¨éàE4ê€ëå±DEbóê =áj^ª†aæájê(fëà@¿¨€@å,±ìàJ+ÀJ þå ±í B@aRàBå±@x@ÇA ARîàEBa‡aãÇâ ûFFaRï =áRçh€=áR"½ðà@Jd€@ë ccõÞó€ ¥´ = …`ƒãÙ€=ájä^%±?à@DØ€@å,±@àJ°× J ånå±A B@€ÎÀBå±@x@ÇA ARBàE»”aRè­XXaRC =áR\˜€=äFáR"½Dà@¿“€@ö Ç£N`͈ðí$´PÃõ‚ëa #ÿ,ÈEàb+Àb@$ájå±F B@jÀByå±@x@ßA$AjGàEAL•ajàˆç YYajH =ájÁO€=ájå ±Ià@ @à~a$( @ãÅ"üDJàBN€ƒà B$àB±K B@JàB@Ù`{à…&ýÿÿÞ@x@¿AAJLàEÈ–áJZZe±Mà=Iƒ|xëcN =@®€Á° €{áˆ% å±Oà@ €@ÿ ÿ,â ó屇ÿ2°Pàbd€bᨠ£áë«cQ B@`æÀBÿÿ$X@x@ßA$A¨RàEOÀˆñ[\bóS =áj…ƒ—aæå"±Tà@æ€@â m,Î$ŸÿÿÞUàJR Jå(Ã>å±V B@aRàBå±@x@ÇA ARWàE\:˜aáÛä F]]aRX =áRþA€=áR%±Yà@`=€@å ±³…« ¹##E‚#1sa  DfZàbÌ< b@$ä#å±[ B@jÀBå±@x@ßA$Aj\àEãõà ˆá j^^aj] = …€ƒ_ù€=ä(%-ä(+c^à@¿ø€@â rå±_àB Bá Jå±` B@€ÆàB@Ùáå±@x@¿AAJaàEj±™bµŒ 4ÀEá J__e±bà=ë¤Füyc =@®€Á^·€{áˆ% áˆ"ódà@¨²€@ë câ óå±°þAëeàb  bᨠ£å ±f B@`æÀBöÇ@x@ßA$A¨gàEðlša¨ëc`abóh =áj-›`=ä^(fià@+€@â må±jàJë* JA#â¸å±k B@aRàB{ñ@x@ÇA ARlàEþãà ˆâ ûbbaRm =áR ë€=áR"½nà@úæ€@ö ÇÃx0¯¯xåÂÀ0Àýa  “B½oàbf bá jå±p B@jÀBñ@x@ßA$AjqàE…Ÿœbå2á jccajr = …€ƒ £€=ä(å ±sà@i¢€@à~È$å±tàBÉ¡€Bá Jå±u B@€ÆÀBñ@x@¿AAJvàE [aJàˆá Jdde±wà=Œm„Fÿÿ* x =áˆø`€{âó% å±yà@B\€@ÿ ÿ…*$F @ñÃâ óå±®aBózàb§á Áë!¨ bå ±{ B@a¨ÀBñ@x@ßA$A¨|àE’ža¨å±efbó} =ájÅÖ€=áj+"áj%±~à@%Õ€@å,±àJ‘Ô JA#å±€ B@aRàBå±@x@ÇA ARàE ŸaRãÇâ ûggaR‚ =áR>•€=áR%±ƒà@˜€@å ±Óè`Ò¯"å·ÿÿߺ¯B½„àb bàá½å±… B@jÀBå±@x@ßA$Aj†àE&I ajáóá jhhaj‡ =ájªL€=å"±ˆà@ @á 屉àBfK€ƒàÁÁJ$&5äŸÿ,Š B@JÀBå±@x@¿AAJ‹àE­¡áJiie±Œà=.ƒ|å± =ሡ €{áˆ% äF"óŽà@è€@å ±!ˆ @å±6Çâ óå±]BóàbI bᨠb᨜y B@a¨ÀBüy@x@ßA$A¨‘àE4À€ëñjkbó’ =ájb€¢aæájå ±“à@Ã~€@ÿ,ÿ; ”àJ/ÀJ 展 B@aRàBå±@x@ÇA AR–àEA7£aáÛä FllaR— =áRø>€=å"±˜à@M:€@ó Ó"½!“â½ãåÿkZ¯å…ºía fgB½™àb¹9€bàᵿ屚 B@jÀB†öÇ@x@ßA$Aj›àEÈòà ˆá jmmajœ =ájHö€=â½%-ä(Cà@¥õ€@â r!j @ãÅ(®@BžàB Bá J屟 B@JàB@Ùâøñ@x@¿AAJ àEO®¤bµàËá Jnne±¡à=ФFöÇ¢ =@®€ÁG´€{áˆ% áˆ%±£à@Н€@Ÿÿ ÿÞë câ ó展A¨¤àbëá Âó!¨ £á륱¥ B@`æÀBÿÿ@Ò@x@ßA$A¨¦àEÕi¥a¨ëcopbó§ =áj*¦`=áj$áj"´¨à@d(€@â m!j @öÇ©àJÐ' JA#â¸ñª B@aRàBÿÿ @x@ÇA AR«àEãàà ˆâ ûqqaR¬ =áR†è€=å"±­à@ãã€@ÿ ÿ,ó`ë_Žƒ‚àa ™ÖB½®àbO bá j屯 B@jÀBÿÿ @x@ßA$Aj°àEjœ§bäfrraj± =ájòŸ€=áj%pâ½%±²à@S @à~ÃÅå±³àB²ž€ƒà B$ ƒä‹c´ B@JàB@Ùâøå±@x@¿AAJµàEðW¨aJáÓâ µsse±¶à=qj„FÿÿQè· =@®€Áà]€{äF% áˆ%±¸à@,Y€@—å ±!ˆ @è¥â óå±=$Aë¹àbŒX€báÁë!¨ bᨋcº B@`æÀBöÇ@x@ßA$A¨»àEw©á¨ttbó¼ = …`ƒ€=âó%páj%±½à@w€@å,±¾àJã J â¸å±¿ B@€ÎÀBÿÿ @x@ÇA ARÀàEþÎà ˆâ ûuuaRÁ =áRšÖ€=â½áR"½Âà@òÑ€@ñõ–¾£Ýõ‹dàWÿh°Ãàb^ bá jå±Ä B@jÀBÿÿ5n@x@ßA$AjÅàE„Šªâ½vvajÆ =ájŽ€=â½%-â½%±Çà@o€@á $( @ãÅ"ü@BÈàBÔŒàB$àB¥±É B@JàB@Ùå±@x@¿AAJÊàE F«áJwwe±Ëà=‚tÿÿÕµÌ =@®€ÁÿK€{âó% áˆ"óÍà@HG€@ñ )ø @å±Ãâ óå±óÿh°Îàb§á å±Ï B@`æÀB î W'Zÿÿ¦@x@ßA$A¨ÐàE’¬a¨ñxybóÑ =áj«Á€=âóä^"´Òà@ À€@â m!j @ëcÓàJy¿ J þâ¸å±Ô B@aRàBå±@x@ÇA ARÕàEŸx­aRàˆå ±zzaRÖ =áRH€€=â½!â½×à@£{€@ëcí` xÜ|Kda èdDfØàb bàá½å±Ù B@jÀBöÇ@x@ßA$AjÚàE&4®áj{{ajÛ =áj²7€=áj%-áj%±Üà@  @á å±ÝàBn6€ƒà Bå±Þ B@JÀBöÇ@x@¿AAJßàE­ïà ˆâ µ||e±àà=.ƒ|ÿÿ,á =áˆõ€{áˆ%páˆ%±âà@êð€@ÿÿ,â óå±Lÿk¤ãàbI bᨠ£ç«cä B@a¨ÀBëc@x@ßA$A¨åàE3«¯bó¼ Z§ á¨ }~bóæ =ájmk°`=å"±çà@Îi€@å,±èàJ:ÀJ ®ÀÉÄFå±é B@aRàBÿÿÞ@x@ÇA ARêàEA"±aÿÿ(aRë =áR *€=áR"½ìà@]%€@öÇ#u¶Èååð£Ýòè`a …‘D©íàbÉ$€bá jå±î B@jàBè oÿÿB@x@ßA$AjïàEÈÝà ˆâ½AX€€ajð =@€ƒTá€=ä(å ±ñà@¯à€@á å±òàBàBå±ó B@€ÆÀBå±@x@¿AAJôàEN™²bµàËá Je±õà=ФFÿÿ; ö =áˆ?Ÿ€{âó% å±÷à@„š€@ÿ ÿ* )ø @ëcâ óå±!cBóøàbêá Ä^!¨ bå ±ù B@a¨ÀBå±@x@ßA$A¨úàEÕT³a¨öÇ‚ƒbóû =ájÿ´`=ájê+cüà@`€@å,±ýàJÌ JA#¦{ÁRå±þ B@aRàBå±@x@ÇA ARÿàEãË€ˆôšâ û„„aR áRˆÓ€=áR%±à@ã΀@ë c3Ò¯"K[£à@T “àÍôB½àbO bá j屡’¡jÀB€å±`x |A$Aj@ a°i‡µbàˆá j……aj = >`=튀=å"±à@I @áéÂrå±àB©‰€ƒàÁÀB$&5ä–Ç B@€ÆÀBå±@x@¿AAJ àEðB¶áJ††e± à=qU„Fëc =áˆàH€{áˆ% å± à@*D€@ë câ óå±iBó àbŒC€bᨠ£á¨–Ç B@a¨ÀBå±@x@ßA$A¨àEwþà ˆâ ó‡‡bó = …`ƒ·aæájå ±à@o€@å,±àJÛ J ÄÂûå± B@€ÎÀBüy@x@ÇA ARàEý¹à ˆá RˆˆaR =áR­Á€=å"±à@½€@ë cCp€ Ó”P €a ¯B½àbn¼€bä ©å± B@jÀBÿÿ,@x@ßA$AjàE„u¸báóá j‰‰aj =áj y€=â½%-ä(1à@jx€@á å±àBÈw€BàÁµå± B@JÀBå±@x@¿AAJàE 1¹áJŠŠe±à=ÿ„Fÿÿ* =áˆ7€{áˆ% áˆ(¥!à@G2€@ÿÿ,â óå±¾ÿÞ"àb§á ᨠ£å ±# B@a¨ÀBÿÿ,@x@ßA$A¨$àE’ì€ëâ1â ó‹‹bó% =áj<ñ€=ájå ±&à@žï€@å,±'àJ ÀJ ©ÐÁRå±( B@aRàBëc@x@ÇA AR)àE¨ºâûŒŒaR* =áRͯ€=áR"½+à@$«€@å ± S~à.þ£ßúôðKla eëD©,àbª€bá jå±- B@jÀB…ëc@x@ßA$Aj.àEŸc»ájaj/ =áj#g€=ájå ±0à@~f€@á å±1àBßeàBå±2 B@JÀBå±@x@¿AAJ3àE&¼áJŽŽe±4à=.íƒÇüy5 =áˆ%€{áˆ% å±6à@\ €@å ±!ˆ @ñ4 â óå±ÁBó7àbÂá Ä^!¨ bå ±8 B@a¨ÀBëc@x@ßA$A¨9àE­Ú€ëüybó: =ájÞš½aæê1;à@?™€@å,±<àJ«˜ JA#å±= B@aRàBå±@x@ÇA AR>àEºQ¾aàˆç ‘‘aR? =áRkY€=â½ç%±@à@ÆT€@ñ cñ+l+làw˜B½Aàb2 bàá½å±B B@jÀBüy@x@ßA$AjCàEA ¿áj’’ajD =ájÍ€=ájå ±Eà@( @á $( @ãÅ"üAJFàB‰€ƒà B$àB±G B@JÀBå±@x@¿AAJHàEÇÈà ˆâ µ““e±Ià=IÛ„F{ÿÿ$XJ =ሴ΀{áˆ% å±Kà@Ê€@ñ ö Çâ óå±ÓjA¨LàbcÉ€bᨠ£áë±M B@a¨ÀBÿÿQè@x@ßA$A¨NàEN„Àâó””bóO =áj눀=å"±Pà@J‡€@å,±QàJ¶† JA#å±R B@aRàBñ@x@ÇA ARSàEÕ?ÁáR••aRT =áR˜G€=áR%±Uà@éB€@â ½$F%òèosq 1‹la φB½VàbU bá jå±W B@jÀBŠëc@x@ßA$AjXàE\û€ëð–ä f––ajY =ájìþ€=ä(%-ä(1Zà@I @à~ƹ!j @ãÅ¥±[àB¨ý€ƒá Jå±\ B@JÀBå±@x@¿AAJ]àEâ¶Ââµ——e±^à=ê¤Fñ_ =áˆ×¼€{âó% áˆ"ó`à@¸€@ÿÿtâ óå±ktAëaàb~·€bᨠ£á륱b B@a¨ÀB â ó?nÿÿÞ@x@ßA$A¨càEirÃa¨üy˜™bód =áj‘2Ä`=áj$áj"´eà@ð0€@å,±fàJ\ÀJ å±g B@aRàBå±@x@ÇA ARhàEwé€ÓãÇä FššaRi =áR.ñ€=áR"½jà@ì€@å ±ƒßúp"xÿäöØý€aÀÿ!dkàbëë€bàáÄ©å±l B@jÀBå±@x@ßA$AjmàEý¤Åbáóá j››ajn =áj…¨€=å"±oà@ã§€@á å±pàBA BàÁÁJå±q B@JÀBå±@x@¿AAJràE„`ÆáJœœe±sà=s„FÿÿBt =áˆxf€{áˆ% äF"óuà@Àa€@ÿ ÿ,â óå±Ûÿ'Lvàb  bᨠ£å ±w B@a¨ÀB å±@x@ßA$A¨xàE Ça¨ÿÿÞžbóy =€ƒ'Ü€=ä^%±zà@ŠÚ€@ÿ ÿ°,Î%òÿÿ {àJöÙ J å ±| B@aRàBÿÿñ`@x@ÇA AR}àE“ÈaRÿÿÞŸŸaR~ =áRÔš€=áR"½à@–€@èo!Rëc“~äð£ßüÞúu¿ÿÓD€ßk*[€àb„•€bâ ½å± B@jÀB—â ½ÿÿÞ@x@ßA$Aj‚àEŸNÉajàˆå ±  ajƒ =ájR€=ä(+"ä(!„à@yQ€@â rå±…àBßP€BàÁ屆 B@JÀBÿÿ¾@x@¿AAJ‡àE& ÊaJàËá J¡¡e±ˆà=§¤Fÿÿ@Ò‰ =áˆ.€{áˆ% áˆ"óŠà@b €@¯ÿÿôâ óå±{™Aë‹àbÂá ᨠ£å ±Œ B@a¨ÀB´ êS"ôëc@x@ßA$A¨àE¬Å€ëå±¢£bóŽ =ájÔ…Ëaæáj$áj%±à@7„€@ë,càJ£ƒ J þáüy‘ B@aRàBÿÿB@x@ÇA AR’àEº<Ìaàˆâ û¤¤aR“ =áRbD€=å"±”à@¾?€@ö Ç£ü Logiÿÿ¡¤¼êB½•àb* bàá½å±– B@jÀBâ ½å±@x@ßA$Aj—àEAø€ëäf¥¥aj˜ =ájÉû€=ájå ±™à@# @à~Á屚àB…ú€ƒà Bëc› B@JÀBÿÿt@x@¿AAJœàEdzÍⵦ¦e±à=HÆ„Fÿÿ ž =ሷ¹€{äF% äF%±Ÿà@µ€@—å ±)ø @è¥Ãâ óå±Bó àbc´€bâ ó!¨ bå ±¡ B@a¨ÀBüy@x@ßA$A¨¢àENoÎa¨å±§¨bó£ =ájz/Ï`=ájáj%±¤à@Ý-€@å,±¥àJIÀJA#æÁ屦 B@aRàBå±@x@ÇA AR§àE[æ Ó@Y ˆå ±©©aR¨ =áRî€=äFáR"½©à@`é€@å ±³e F hUSB @¸ÀÀoB½ªàbÌè€bä f屫 B@jÀB{å±@x@ßA$Aj¬àEâ¡Ðbájªªaj­ = …€ƒ^¥€=â½å ±®à@¹¤€@á 屯àB BàÁÅô$ ƒäŸÿ ° B@€ÆÀBñ@x@¿AAJ±àEi]ÑáJ««e±²à=ê¤Få±³ =áˆMc€{áˆ% å±´à@¢^€@“å ±!ˆ @å±Kâ óå±dBóµàb bᨠb᨟ÿ ¶ B@a¨ÀBÿÿ§V@x@ßA$A¨·àEðÒa¨ñ¬­bó¸ =ájÙ€=ájä^%±¹à@r×€@å,±ºàJÞÖ JA#¥(Âûå±» B@aRàBÿÿ,@x@ÇA AR¼àEýÓaRàˆå ±®®aR½ =áR­—€=áR%±¾à@ “€@ˆâ ½!“â½ÃRE³e+_v ra ‘zB½¿àby’ b@$ájå±À B@jÀBÿÿnb@x@ßA$AjÁàE„KÔáj¯¯aj =ájO€=áj%-ä(1Ãà@oN€@å$±ÄàBÐM€Bá Jå±Å B@JÀBå±@x@¿AAJÆàE ÕaJàËâ µ°°e±Çà=Œƒ|å±È =áˆ÷ €{áˆ% áˆ"óÉà@H€@å ±ë câ óå±5ˆAëÊàb§á Ä^!¨ £å ±Ë B@a¨ÀBÿÿ; @x@ßA$A¨ÌàE‘ÂÕa¨ñ±²bóÍ =áj‚Ö`=ê%±Îà@$€@å,±ÏàJ€ JA#äF d©áR-ˆ­Ð B@aRàBëc@x@ÇA ARÑàEŸ9×aàˆâ û³³aRÒ =áRGA€=â½%pâ½Óà@§<€@å ±ÓRQ%·12.0a ­B½ÔàbÀb@$ájå±Õ B@jÀBå±@x@ßA$AjÖàE%õ ë@Y ˆá j´´aj× =áj¶ø€=â½å ±Øà@ @à~ÃÅ)Ú @âr"üAJÙàBr÷€ƒå(ÀB$àB«cÚ B@JÀBñ@x@¿AAJÛàE¬°Øâµµµe±Üà=-ÄFÿÿÞÝ =ሜ¶€{âó% âóÞà@ê±€@—è ¥ @è¥â óå±·_A¨ßàbH bá ë!¨ báëi‹cà B@ ÀBñ@x@ßA$A¨áàE3lÙᨶ¶bóâ =áj×p€=âóä^"´ãà@7o€@å,±äàJ£n J!ýÂûå±å B@aRàBÿÿ; @x@ÇA ARæàEº'ÚaRã„ä F··aRç =áRd/€=áR(oèà@Â*€@ñ ã7_B%©0%±9`½Àúÿ]Léàb. bá jå±ê B@jÀBÿÿt@x@ßA$AjëàE@ã€ëàˆá j¸¸ajì =ájÄæ€=ä(%-ä(+cíà@! @áéÂrå±îàB€å€ƒàÁÀB$ ƒäˆgï B@JÀBëc@x@¿AAJðàEÇžÛa÷àˆá J¹¹e±ñà=ϤFõÿÿWšò = ÀÁ³¤€{áˆ% áˆ"óóà@ €@ÿ ÿ6ë câ óå±^ƒAëôàbcŸ€báᨠ£á¨…±õ B@`æÀBëc@x@ßA$A¨öàENZÜa¨ñº»bó÷ =ájuÝ`=ä^%±øà@Õ€@å,±ùàJA Jã >å±ú B@aRàB{ ©²Á•ÿÿ]L@x@ÇA ARûàE[Ñ Ó DÀEâ û¼¼aRü =áRÙ€=áR"½ýà@sÔ€@ÿ ÿ5nóê2K%@7à7þ%; ·àUÇB½þàbßÓ€bàáÁ'å±ÿ B@jÀB‚öÇ@x@ßA$Aj  `EâŒÞbàˆá j½½aj =ájn€=ä(%-ä(%±à@Ë€@â rå±àB* BàÁÁJå± B@aàBå±@x`¿ =AJ @€EiHßáJ¾¾e±à=ê¤FÿÿF„ =áˆ]N€{áˆ% áˆ"óà@¤I€@ÿ ÿ5n!ˆ @ëcóâóå±ñ5Aë àb bᨠbå ± B@abÀB â ó?nÿÿ,@x@ßA$A¨ àEïàa¨ñ¿Àbó =áj#Ä€=ä^%± à@‚€@å,±àJîÁ J þÅ(Âûå± B@aRàBÿÿ @x@ÇA ARàEýzáaRàˆä FÁÁaR =áR ‚€=áR"½à@~€@ñ 1d3`2Ï%€ÿÿ ‘¢ÿÞàbm} bA;æÙå± B@jÀBzâ ½ÿÿ,@x@ßA$AjàE„6âajå2á jÂÂaj = …€ƒð9€=ä(å ±à@M @à~¿ÿGŒ$( @ãÅ"üDàB¬8€ƒá Jå± B@€ÆÀBå±@x@¿AAJàE òà ˆá JÃÃe±à=ƒ|yÿÿô =áˆó÷€{âó% å±à@Fó€@ëcâ óå±ÿÞàb¦á Áë!¨ £áë«c B@a¨ÀBÿÿ]L@x@ßA$A¨ àE‘­ãbóå±ÄÅbó! =áj¼mä`=âóñ "à@l€@å,±#àJˆk J äFå±$ B@aRàBå±@x@ÇA AR%àEž$åaàˆâ ûÆÆaR& =áRf,€=â½áR%±'à@¯'€@ÿ ÿN$F @èoÿ €€ÿÿÿEDàDÿê(àb bä ©å±) B@jÀB’@lá­å±@x@ßA$Aj*àE%à€ëájÇÇaj+ =áj¥ã€=â½%pä(+c,à@ @á !j @ãÅ¥±-àBa àÁäàB±. B@JÀBå±@x@¿AAJ/àE¬›æâµÈÈe±0à=-®„FÿÿB1 =ሰ¡€{áˆ% áˆ"ó2à@蜀@«öÇâ óå±àÿ Ò3àbH bᨠ£á륱4 B@a¨ÀB°â ó"ôëc@x@ßA$A¨5àE3Wça¨ñÉÊbó6 =ájJè`=áj$áj"´7à@®€@ÿ,ÿB8àJ Já Rå±9 B@aRàBüy@x@ÇA AR:àE@΀Óàˆå ±ËËaR; =áRèÕ€=å"±<à@HÑ€@ë c# ¡•u%ßÿµ`mn!=àb´Ð b@"ånaöâ½á•|> B@jÀB{â ½å±@x@ßA$Aj?àEljébàˆá jÌÌaj@ =ájC€=â½%-â½%±Aà@¡Œ€@å$±BàB BàÁå±C B@JÀBÿÿB@x@¿AAJDàENEêaJàËá JÍÍe±Eà=ÏW„FÿÿBF =áˆ:K€{áˆ% áˆ%±Gà@ŠF€@ÿÿ,â óå±Ét Hàbêá ᨠ£âó¥±I B@a¨ÀBöÇ@x@ßA$A¨JàEÔëa¨ëcÎÏbóK =ájðÀ€=ájå ±Là@S¿€@ë,cMàJ¿¾ JA#äFå±N B@aRàBå±@x@ÇA AROàEâwìaRàˆâ ûÐÐaRP =áR–€=å"±Qà@öz€@ˆè o!“ëc3à)ç•)‘a *áD©RàbbÀb@$ájå±S B@jÀBå±@x@ßA$AjTàEh3íájÑÑajU =ájÕ6€=ájå ±Và@3 @äåÃÅëcWàB•5€ƒà B/l ƒä†üX B@JàB@Ùá&ýÿÿnb@x@¿AAJYàEïáÓâ µÒÒe±Zà=pëaˆÿÿÞ[ =@®€ÁÛô€{äF% å±\à@,ð€@å ±!ˆ @âó屎Bó]àb‹ï€báÁë!¨ bá¨XF^ B@`æÀBå±@x@ßA$A¨_àEvªîajàˆá ¨ÓÓbó` =áj¯€=âó$áj+caà@~­€@å,±bàJꬠJ â¸å±c B@aRàBÿÿ @x@ÇA ARdàEýeïáRÔÔaRe =áRºm€=â½áR(ofà@i€@ÿÿ,C•u‘•u&ÿà×®B½gàbh€bá jå±h B@jÀBÿÿ/¼@x@ßA$AjiàEƒ!ðájÕÕajj =áj %€=â½%-â½+ckà@g$€@á $( @ãÅ"ü@BlàBË#àB$àB¥±m B@JàB@Ùâøå±@x@¿AAJnàE Ýà ˆä ÖÖe±oà=«ƒ|ñÿÿ,p =@®€Áúâ€{âó% áˆ"óqà@DÞ€@ÿ ÿ'Lÿ ÿ,â óå±s\A¨ràb¦á å±s B@`æÀBÿÿÞ@x@ßA$A¨tàE‘˜ñbóñרbóu =áj¿Xò`=âóä^"´và@ W€@â m!j @Ï¢ 6Ã! !%ßÿåüw ŒV J â¸å±x B@aRàBå±@x@ÇA ARyàEžóaàˆâ ûÙÙaRz =àÆL€=áR"½{à@¦€@å ±SQ *ÿÀ ¡@ŒàJÿWš|àb bàá½å±} B@jÀBå±@x@ßA$Aj~àE%Ë€ëàˆá jÚÚaj =@Ý€ƒ΀=ä(%-ä(%±€à@ýÍ€@à~Áå±àB]àB层 B@€ÆàB@Ùå±@x@¿AAJƒàE¬†ôâµÛÛe±„à=-™„Fÿÿ … =@®€ÁœŒ€{áˆå ±†à@懀@å ±!ˆ @ëcâ óå±î½Aë‡àbH bá ë!¨ bç«cˆ B@`æÀBå±@x@ßA$A¨‰àE2Bõa¨ñÜÝbóŠ =ájeö`=ä^%±‹à@Å€@â m屌àJ1ÀJA#â¸å± B@aRàB{&àBÿÿ 9 @x@ÇA ARŽàE@¹ Ó DÀEÿÿ  A@ÞÞaR =@€ƒæÀ€=áR%±à@H¼€@å ± c… ¡•u–È¿ÿM€þB½‘àb´»€bàáĩ屒 B@€æÀBÿÿyÆ@x@ßA$Aj“àEÇt÷b‹@Y¡óá jßßaj” =ájSx€=ä(å ±•à@¯w€@ð Ò$( @ãÅ"üAJ–àB Bá J$àB«c— B@JÀBüy@x@¿AAJ˜àEM0øaJàˆá Jààe±™à=ΤF€ÿÿ,š =áˆ26€{âó% ç:"ó›à@„1€@ÿ ÿ,â óå±¼ A¨œàbéá Âó!¨ £á륱 B@a¨ÀBÿÿ¦@x@ßA$A¨žàEÔë€ëå±áâbóŸ =áj¬ùaæáj+"áj"´ à@oª€@å,±¡àJÛ© J £½ÁRå±¢ B@aRàBå±@x@ÇA AR£àEâbúaãÇâ ûããaR¤ =áRŒj€=áR"½¥à@êe€@å ±s6¼ •u ø&ÿàΔB½¦àbV bàáÁjå±§ B@jÀBÿÿB@x@ßA$Aj¨àEhûajáóá jääaj© =ájì!€=å"±ªà@I @á 屫àB¨ €ƒàÁÁJ$å ±¬ B@JÀBå±@x@¿AAJ­àEïÙà ˆá Jååe±®à=pì„Fÿÿ ¯ =áˆÛ߀{áˆ% äF"ó°à@*Û€@ë câ óå±jfBó±àb‹Ú€bᨠ£å ±² B@a¨ÀBöÇ@x@ßA$A¨³àEv•übóñæçbó´ =ájUý`=ä^%±µà@íS€@ù 5ëc¶àJYÀJA#å±· B@aRàBå±@x@ÇA AR¸àEƒ þaáÛâ ûèèaR¹ =áR.€=áR"½ºà@‹€@å ±ƒ1 0 16Î%ÿÿQèÊYB½»àb÷€bàáÄfå±¼ B@jÀBÿÿnb@x@ßA$Aj½àE Èà ˆá jééaj¾ =áj‚Ë€=ä(%-ä(1¿à@ßÊ€@â rå±ÀàBB Bá Jå±Á B@JàB@Ùâøüy@x@¿AAJÂàE‘ƒÿbµàËá Jêêe±Ãà=–„FxëcÄ =@®€Á‰€{áˆ% áˆ"óÅà@Ì„€@—ÿ m$F @ñ6Çâ óå±n«AëÆàb- báÂó!¨ bå ±Ç B@`æÀBñ@x@ßA$A¨ÈàE?ÿ"GëcëìbóÉ =áj:ÿ€=ájë cÊà@šý€@â m!j @öÇËàJÀJA#â¸ëcÌ B@aRàBÿÿ @x@ÇA ARÍàE%¶aRàˆâ ûííaRÎ =áRÔ½€=å"±Ïà@5¹€@ÿ ÿ,“ 8E­  8ÀÀa /ÛB½Ðàb¡¸ b@$ájå±Ñ B@jÀBÿÿB@x@ßA$AjÒàE¬qajãFá jîîajÓ =ájæ„Få±- =ሩـ{áˆå ±.à@õÔ€@ÿ ÿ,â óå±w Aë/àbU bᨠ£á¨‘0 B@a¨ÀBüy@x@ßA$Aj1àE@bóçãá ¨ E±2 =ájç“€=å"±3à@H’€@å,±4àJ´‘€JàÉÄFå±5 B@aRàBÿÿB@x@ÇA AR6àEÆJaRáÛá RaR7 =áR„R€=áR%±8à@ÞM€@ÿÿ5nãu6¾)ÿÀaÀ¯ùB½9àbJ bàáÁjå±: B@jÀBÿÿB@x@ßA$Aj;àEMájaj< =ájÝ €=ä(%-å±=à@9 @â rå±>àB™€ƒá Jå±? B@JÀBå±@x@¿AAJ@àEÔÁà ˆÿÿ ÿFe±Aà=ܤFÿÿnbB =áˆÄÇ€{áˆ% áˆ"óCà@À@ÿ ÿ5n`@ñÃâ óå± BóDàbp€bᨠ£å ±E B@a¨ÀBå±@x@ßA$A¨FàEZ}bóÿÿ, bóG =áj…=`=ä^+cHà@å;€@å,±IàJQÀJA#îå±J B@aRàBÿÿÞ@x@ÇA ARKàEhô Ó@Y ˆâ û  aRL =áRü€=áR"½Mà@t÷€@å ±óÿ|yÿ ÿ¦Á8B½Nàbàö€bàáå±O B@jÀBå±@x@ßA$AjPàEï¯bå2á j  ajQ = …€ƒs³€=ä(å ±Rà@ϲ€@à~Áå±SàB/ Bá Jå±T B@€ÆÀBå±@x@¿AAJUàEukaJóË  e±Và=ö¤Fÿÿ W =áˆjq€{âó% å±Xà@´l€@›Y‚£rሠ@å±(¥â óå±eÊBóYàb bᨠbå ±Z B@a¨ÀBå±@x@ßA$A¨[àEü&a¨å± bó\ =áj&ç€=âóïÂ%±]à@‡å€@å,±^àJóä JA#áµå±_ B@aRàBå±@x@ÇA AR`àE žaRàˆä FaRa =áR¸¥€=â½áR%±bà@¡€@ñ   ‘K^ÿÿÿ¦¶n!càb‚ €bä ©ëcd B@jÀBÿÿB@x@ßA$AjeàEYájajf =áj]€=â½å ±gà@q\€@á å±hàBÐ[€Bâ,Äkå±i B@JÀBå±@x@¿AAJjàEáJe±kà=˜'ƒ|å±l =ሀ{áˆ% å±mà@R€@—å ±ë câ óå±ÒE±nàb³á ᨠ£å ±o B@a¨ÀBå±@x@ßA$A¨pàEžÐ€ëâtä ^bóq =ájKÕ€=ájáj%±rà@®Ó€@å,±sàJÀJA#å±t B@aRàBÿÿ,@x@ÇA ARuàE$ŒâûaRv =áRÌ“€=áR%±wà@,€@å ±…•11 %¼a VÒB½xàb˜Ž€bá jå±y B@jÀB| á­ÿÿ5n@x@ßA$AjzàE«Gájaj{ =áj7K€=ájå ±|à@‘J€@á å±}àBóIàBå±~ B@JÀBå±@x@¿AAJàE2áJe±€à=:уÇóÿÿ* =ሠ€{áˆ% 层à@k€@å ±!ˆ @ëcâ óå±òh¥ƒàbÎá 屄 B@a¨ÀBüy@x@ßA$A¨…àE¹¾€ëå±bó† =ájXÀ=ê%±‡à@¹Á€@å,±ˆàJ% JàÉÁRëc‰ B@aRàBöÇ@x@ÇA ARŠàE?z âûaR‹ =áRù€=áR%±Œà@S}€@å ±#«^¡… •ÿÿ²º©-E±àb¿|€bä ©å±Ž B@jÀBñ@x@ßA$AjàEÆ5!ájaj =ájV9€=ä(å ±‘à@²8€@á $( @éw"üAJ’àBàB屓 B@JÀBå±@x@¿AAJ”àEMñà Ëé ºe±•à=U¿ƒÇöÇ– =áˆ=÷€{áˆ%på±—à@‡ò€@ü yë câ óå± ßA¨˜àbéá Ä^!¨ £áë¼y™ B@a¨ÀBëc@x@ßA$A¨šàEÓ¬"bóñbó› =ájúl#`=ä^"´œà@Zk€@å,±àJÆj JA#éµå±ž B@aRàBñ@x@ÇA ARŸàEá#$aàˆâ ûaR  =áR‹+€=áR%±¡à@é&€@å ±3«^A+^A‘…!à«B½¢àbU bàá½å±£ B@jÀBñ@x@ßA$Aj¤àEh߀ëç¥á jaj¥ =ájèâ€=ä(%pä(C¦à@D @à~Áå±§àB¤á€ƒá J$ ƒäŸÿB¨ B@JÀBå±@x@¿AAJ©àEîš%âµe±ªà=o­„Fñ« =áˆÛ €{âó% áˆ"ó¬à@%œ€@å ±!ˆ @ëcâ óå±°ÿ!š­àbŠ›€bᨠb᨟ÿ,® B@a¨ÀBëc@x@ßA$A¨¯àEuV&a¨å± bó° =áj”'`=ájñ ±à@ô€@å,±²àJ`ÀJA#å±³ B@aRàBå±@x@ÇA AR´àE‚Í Ó@Y ˆä F!!aRµ = +€ƒ-Õ€=áR"½¶à@‹Ð€@å ±C• B%­B+j$sà›·D©·àb÷Ï€bä ©å±¸ B@€æÀBå±@x@ßA$Aj¹àE ‰(báj""ajº =áj‘Œ€=å"±»à@î‹€@á $(ån(®AJ¼àBM BàÁÅôå±½ B@JÀBå±@x@¿AAJ¾àED)áJ##e±¿à=W„Fxÿÿ À =ሄJ€{áˆ% å±Áà@ËE€@ÿÿ,â óå±?EA¨Âàb, bᨠ£å ±Ã B@a¨ÀB ô 4 ÿÿL6@x@ßA$A¨ÄàE*a¨ájä^WÌ$'bóÅ =ájD7,`=ä^"´Æà@§5€@å,±ÇàJÀJ ®Å(Âû f=áR¿ÿh°È B@aRàBÿÿ @x@ÇA ARÉàE2î€ÓãÇáRA@((aRÊ =@€ƒáõ€=â½ç%±Ëà@>ñ€@å ±Sm+Å T àØýB½Ìàbªð€bàáÁjå±Í B@€æÀBÿÿô@x@ßA$AjÎàE¸©-báóá j))ajÏ =áj0­€=áj%pä(+cÐà@ެ€@à~Áå±ÑàBô«àBå±Ò B@JÀBÿÿL6@x@¿AAJÓàE?e.áJ**e±Ôà=³¤Fƒªã|ˆ¯@.¿ÿ 8AÕ áˆ+k€{áˆ% áˆ"óÖà@{f€@”D¬€tሠ@ëcâ óå±|Aë×àbÛá Áë!¨ bä^«cØ B@ ÀBëc@x@ßA$A¨ÙàEÆ /a¨ÿÿB+,bóÚ =ájòà€=å"±Ûà@U߀@ÿ,ÿ/¼ÜàJÁÞ J!áµå±Ý B@ àBå±@x@ÇA ARÞàEÓ—0aRãÇä F--aRß =áRkŸ€=áR"½àà@Ëš€@ÿÿô c 1 "îüyïB½áàb7Àb ã å±â B@jÀBÿÿô@x@ßA$AjãàEZS1áj..ajä =ájêV€=ä(å ±åà@D @â r屿àB¦U€ƒá J$ ƒä‘ç B@JÀBëc@x@¿AAJèàEá2aJàËâ µ//e±éà=b!ƒ|yéøå±"`@%±ê =áˆÉ€{áˆ% å±ëà@€@”@aâö Çâ ó ›”hv¼ï/GÌï?ºBóìàb}€báå±í B@a¨ÀBÿÿ* @x@ßA$A¨îàEgÊ€ˆñ01bóï =ájŒŠ3aæä^+cðà@@ë,cñàJZÀJA#áµå±ò B@aRàBÿÿ,@x@ÇA ARóàEuA4aàˆâ û22aRô =áRI€=áR%±õà@}D€@å ±s!"; a »^B½öàbéC b@$ájå±÷ B@jÀBå±@x@ßA$AjøàEûüà ˆÿÿ ÿ÷33ajù =ájt5a¨áj%-`±€3ÒAúà@Îÿ€~ã Åå±ûàB0àBå±ü B@JÀBÿÿt@x@¿AAJýàE‚¸ áÓá J44e±þà=Ë„FÿÿQèÿ =áˆf¾€{å"± à@»¹€@å ±!ˆ%òëcâ ó屬Aëàb báå± B@a¨ÀBÿÿ@Ò@x@ßA$A¨àE t6bµå±56bó =áj:47`=âóïÂ%±à@œ2€@å,±àJÀJ îäFå± B@aRàBå±@x@ÇA ARàEë Ó@Y¤Ïâ û77aR =áRÇò€=áR%± à@&î€@å ±ƒ%°1 …º”@¸À¶ùB½ àb’í b@% á½å± B@jÀB|ó ÓÿÿÞ@x@ßA$Aj àE¦8bàˆá j88aj =áj!ª€=ä(å ±à@|©€@å$±àBݨ€BàÁÁJå± B@JÀBëc@x@¿AAJàE$b9áJ99e±à=¥t6`=å± =áˆh€{áˆ%pç:"óà@ac€@ÿ ÿ5në câ óå±ÿôàbÀá ᨠ£í œy B@a¨ÀBñ@x@ßA$A¨àE«:ajâtâ ó::bó = …`ƒ^"€=ájå ±à@¿ €@å,±àJ+ JàÉÁRå± B@€ÎÀBüy@x@ÇA ARàE1Ù Ó@Y¤Ïá R;;aR =áRÐà€=å"±à@1Ü€@å ±“+\‚+\ 6Ç4ˆàŠÿ, àbÛ b  áÁjå±! B@jÀBÿÿ5n@x@ßA$Aj"àE¸”;b½àˆá j<?bó. =ájüË€=ájå ±/à@\Ê€@å,±0àJÈÉ JA#äFå±1 B@aRàBÿÿ; @x@ÇA AR2àEÓ‚>aRàˆä F@@aR3 =áRyŠ€=áR(o4à@Û…€@å ±£Ë\]%ºƒ aÀ½±Df5àbGÀb@%ájå±6 B@jÀBå±@x@ßA$Aj7àEZ>?ajå2á jAAaj8 =ájÚA€=áj%pä( = ž€ÁÍÿ€{è¥% áˆ"ó?à@û€@å ±ë câ óå±CAë@àb|ú€bᨠ£á륱A B@`æÀBå±@x@ßA$A¨BàEgµ@bóñCDbóC =áj’uA`=ájáj"´Dà@òs€@â m!j @ÿÿF„EàJ^ÀJA#â¸å±F B@aRàBëc@x@ÇA ARGàEu,BaäFEEaRH =áR+4€=äF!â½Ià@‰/€@ÿ ÿ,³";]4ñ]ÿÿQè)ÿ,Jàbõ.€bàáÈoå±K B@jÀBÿÿL6@x@ßA$AjLàEû瀈áóä fFFajM =áj{ë€=áj%páj%±Nà@Üê€@à~Áå±OàB;àBå±P B@JàB@Ùâøå±@x@¿AAJQàE‚£CbµàËá JGGe±Rà=¶„Fÿÿ; S =@®€Án©€{áˆ% áˆ%±Tà@½¤€@ö Çâ óå±ÿ,Uàb báÁë!¨ £å ±V B@`æÀBå±@x@ßA$A¨WàE _Da¨ÿÿ,HIbóX =áj<E`=å"±Yà@œ€@ÿ,ÿ,ZàJ Já Rå±[ B@aRàBëc@x@ÇA AR\àEÖ€Óàˆâ ûJJaR] =áR¹Ý€=â½å ±^à@Ù€@ë cÃ)ˆ6Ë€VÐà`õD©_àb†Ø b ä#å±` B@jÀBüy@x@ßA$AjaàE‘Fbàˆá jKKajb =@瀃%•€=H@—àÆå ±cà@‚”€@à~ÃÅå±dàBá“àB$&5äŸÿ,e B@€ÆÀBñ@x@¿AAJfàE$MGaJç…á JLLe±gà=¥¤Få±h =áˆS€{áˆ% å±ià@[N€@å ±)ø @ñÃâ óå±ÄBójàbÀá Áë!¨ bᨖÇk B@a¨ÀBüy@x@ßA$A¨làEªHa¨ëcMNbóm = …`ƒÆÈ€=å"±nà@%Ç€@è !j$ŸëcoàJ‘Æ J ãå±p B@€ÎÀBå±@x@ÇA ARqàE¸IaRàˆâ ûOOaRr =áR[‡€=áR(osà@¼‚€@å ±Ó¿ÿ , à¸HB½tàb(Àb@(äêå±u B@jÀBñ@x@ßA$AjvàE>;JájPPajw =ájÏ>€=áj%-ä(+cxà@, @äåÃÅå±yàB‹=€ƒà B$ ƒä…±z B@JÀBÿÿ,@x@¿AAJ{àEÅö€ˆáÓâ µQQe±|à=F ƒ|å±} =áˆ}ý€{å"±~à@Íø€@å ±!ˆ%Nå±Kâ ó屺Aëàb- báÁë!¨ bᨅ±€ B@a¨ÀBëc@x@ßA$A¨àEL²KbóëcRSbó‚ =áj€rL`=âó%pâó1ƒà@ãp€@å,±„àJOÀJA#äFå±… B@aRàBÿÿ @x@ÇA AR†àEY)Maàˆâ ûTTaR‡ = …€ƒ1€=áR%±ˆà@m,€@ñ ã@``!屓B½‰àbÙ+€bàá½å±Š B@€æÀBÿÿB@x@ßA$Aj‹àEàäà ˆá jUUA! =ájTè€=ä(%-â½%±à@²ç€@å n$( @ãÅ"ü@BŽàB Bá J$àB¥± B@JÀBÿÿB@x@¿AAJàEg NâµVVe±‘à=貄Fuÿÿ ’ =áˆS¦€{áˆ% áˆ"ó“à@£¡€@å ±)ø @å±+câ óå±7A¨”àb bᨠbá륱• B@a¨ÀBå±@x@ßA$A¨–àEî[Oa¨å±WXbó— =áj&P`=ä^"´˜à@ˆ€@å,±™àJô JA#屚 B@aRàBÿÿÞ@x@ÇA AR›àEûÒà ˆä FYYaRœ =áR£Ú€=áR"½à@Ö€@ë có@``"å±ÍxB½žàboÕ b@1áj屟 B@jÀBñ@x@ßA$Aj àE‚ŽQbàˆá jZZE±¡ =áj’€=ä(%-ä(%±¢à@`‘€@å$±£àB€BàÁÆü層 B@JÀBñ@x@¿AAJ¥àEJRáJ[[e±¦à=Š\„Füy§ = ÀÁñO€{âó% áˆ"ó¨à@EK€@å ±': @å±+câ óå±iŠAë©àb¤á ᨠbå ±ª B@`æÀBÿÿB@x@ßA$A¨«àESa¨ñ\]bó¬ =áj»Å€=âóë c­à@Ä€@å,±®àJŠÃ JA#äFëc¯ B@aRàBå±@x@ÇA AR°àE|TaR‹@Y ˆä F^^aR± =áRY„€=â½áR"½²à@¹€@å±@``#å±ð&XájccE±Ë =ájÎ)€=ájå ±Ìà@* @â r)Ú @ãÅ"üAJÍàBŠ(€ƒà Bå±Î B@JÀBå±@x@¿AAJÏàEÅáà ˆä dde±Ðà=ͤFÿÿ Ñ =ርç€{áˆ% å±Òà@ã€@å ±!ˆ @ëcâ óå±ðAA¨Óàbaâ€bᨠbáë±Ô B@a¨ÀBëc@x@ßA$A¨ÕàELYbóñefbóÖ =!!€ƒ~]Z`=ê"´×à@Þ[€@å,±ØàJJÀJ å ±Ù B@aRàBå±@x@ÇA ARÚàEY[aàˆâ ûggaRÛ =áR€=â½ç%±Üà@i€@å ±#@``%å±L2B½ÝàbÕ b@1çå±Þ B@jÀBüy@x@ßA$AjßàEàÏà ˆá jhhE±à =ájhÓ€=áj%pä(1áà@ÄÒ€@á $( @ãÅ"ü@BâàB$àBå±ã B@JÀBå±@x@¿AAJäàEg‹\bµç…á Jiie±åà=è¤Fÿÿ5næ =áˆS‘€{áˆ% áˆ"óçà@¢Œ€@å ±ö Çâ óå±èÀA¨èàb báÎW!¨ £á륱é B@a¨ÀBöÇ@x@ßA$A¨êàEíF]a¨ñjkbóë =áj ^`=å"±ìà@l€@å,±íàJØ JA#äFëcî B@aRàBå±@x@ÇA ARïàEû½à ˆâ ûllaRð =áR¥Å€=â½å ±ñà@Á€@å ±3@``0ÿÿÞš7B½òàbsÀ b@1ájå±ó B@jÀBå±@x@ßA$AjôàEy_bàˆá jmmE±õ =áj}€=â½%pä(%±öà@b|€@à~ÃÅå±÷àBÂ{€Bä ƒäÿtø B@JÀBÿÿ,@x@¿AAJùàE5`áJnne±úà=‰G„FöÇû =áˆô:€{âó% áˆ%±üà@C6€@öÇâ óå±>ÅAëýàb¤á Áëå±þ B@a¨ÀBå±@x@ßA$A¨ÿàEð€ëå±opbóáj¾°aaæâóä^(fà@¯€@å,±àJŠ® J ¤äFå± B@aRàBå±`x@9 AR @`Eœgbaàˆä FqqaR =â½Go€=áR"½à@¨j€@å ±C6Æà%3à fÙB½àbÀb@(å± B@a$àBå±@x@ßA$Aj àE##cájrraj =áj«&€=ä(%-ä(%± à@ @á å± àBg%€ƒà B$ ƒå ± B@JÀBëc@x@¿AAJàEªÞà ˆâ µsse±à=+ñ„Fëc =ሖä€{áˆå ±à@å߀@å ±ë câ óå±Â+AëàbF bᨠ£á¨ŸÿÞ B@a¨ÀBå±@x@ßA$A¨àE1šdbóñtubó =ájhZe`=ä^%±à@ÇX€@å,±àJ3ÀJA#äFå± B@aRàB|JeàBÿÿŽ@x@ÇA ARàE>faàˆâ ûvvaR =áRå€=áR%±à@F€@ÿÿÞ S›3D3~%ÀAa ½„B½àb²€bàáÈoëc B@jÀBå±@x@ßA$AjàEÅÌ€ˆüywwaj =áj]Ѐ=ä(%-å± à@ºÏ€@ë å±!àB Bá Jå±" B@JÀBå±@x@¿AAJ#àEKˆgâµxxe±$à=ͤFÿÿŽ% =áˆ<Ž€{áˆ% áˆ"ó&à@‡‰€@ÿÿF„â óå±vBó'àbçá Âó!¨ £å ±( B@a¨ÀBù »ÿÿ@Ò@x@ßA$A¨)àEÒChá¨yybó* =ájpH€=ájë c+à@ÒF€@å,±,àJ>ÀJ þånå±- B@aRàBÿÿQè@x@ÇA AR.àEYÿ Ó@Y¥÷å ±zzaR/ =áRiaå"±0à@i€@ÿÿbþcÿ2ö1!ůÿ€€@¸ÀVB½1àbÕ€bàá½å±2 B@jÀB„â ½ÿÿ/¼@x@ßA$Aj3àE຀ˆå2á j{{aj4 =ájh¾€=â½å ±5à@ǽ€@à~Áå±6àB( Bá J$ ƒä‘7 B@JàB@Ù¢oÀ…!Kÿÿ$X@x@¿AAJ8àEfvjbwàˆá J||e±9à=nD‚óÿÿQè: =@®€Á[|€{âó% å±;à@¤w€@›N,áë câ óå±òbBó<àb báÁe!¨ £á¨‹c= B@`æÀB â ó!¨ÿÿF„@x@ßA$A¨>àEí1ka¨ÿÿ* }~bó? =ájò€=âóå ±@à@xð€@â m!j @ÿÿôAàJäï J áµå±B B@aRàBå±@x@ÇA ARCàEû¨laRÿÿÞaRD =áR̰€=â½!â½Eà@ÿ«€@¸â ½$F @1€# %±s€ÿ@ @ÿ¿ÿÃy€ÜRB½Fàbk bä ©å±G B@jÀB¨â ½å±@x@ßA$AjHàEdmajáóä f€€ajI =ájh€=â½%-ájCJà@mg€@á !j @ãÅ.`@BKàBÍf€BàÁäàB¥±L B@JÀBëc@x@¿AAJMàE naJì®e±Nà=‰2ƒ|ÿÿ¡¤O =ሠ&€{áˆ% áˆ+cPà@B!€@Ãâ ó @âóå±x A¨Qàb¨¡á¨ bá륱R B@a¨ÀBÿŽ=ä¡"ôå±@x@ßA$A¨SàEÛ€ëàˆâ ó‚‚bóT = ``ƒ/à€=ä^"´Uà@Þ€@å,±VàJûÝ J áå±W B@€ÎÀBöÇ@x@ÇA ARXàE—obûàˆá RƒƒaRY =áR¸ž€=â½%pâ½Zà@š€@ö ǃaànÿ‚6[àb†™€bä ©å±\ B@jÀBñ@x@ßA$Aj]àEœRpáj„„aj^ =ájV€=áj%-áj%±_à@uU€@áéÅnå±`àBÔTàB)º ƒä‹ca B@JÀBå±@x@¿AAJbàE#qáJ……e±cà=+܃|ÿÿ÷d =ሠ€{áˆ% áˆceà@^€@”å ±)ø @å±Ãâ óå±Êÿˆfàb¿á Áë!¨ bᨋcg B@a¨ÀB”è ¥å±@x@ßA$A¨hàEªÉ€ëödž‡bói =ájщraæå"±jà@0ˆ€@å,±kàJœ‡ J þå ±l B@aRàBöÇ@x@ÇA ARmàE·@saàˆå ±ˆ%T63n =áRVH€=â½å ±oà@·C€@ \á$F @â½+c“  #&),ÿ™úÀ–oD©pàb#Àb@%è,å±q B@jÀBÿÿB@x@ßA$AjràE>ü ë@Y ˆá j‰‰ajs =ájÆÿ€=áj%-ä(%±tà@# @à~ÃÅ!j @ãÅ(®@BuàB‚þ€ƒà B$àB¥±v B@JÀBå±@x@¿AAJwàEÅ·tbµç…á JŠŠe±xà=FÊ„Fÿÿ$Xy =ሩ½€{áˆ% áˆ%±zà@ü¸€@å ±!ˆ @âóå±2A¨{àba báÁë!¨ bá륱| B@a¨ÀBÿÿB@x@ßA$A¨}àEKsua¨ÿÿ,‹:É$^~ =áj3v`=å"±à@Þ1€@å,±€àJJÀJ £½ÁRëc B@aRàBå±@x@ÇA AR‚àEYê Ó@Y ˆâ ûaRƒ =áRüñ€=áR"½„à@Yí€@!¯À~Á"½å ±£258;>ADGJÿÿ@ÔùWB½…àbÅì€bàáÀb屆 B@jÀBÿÿ5n@x@ßA$Aj‡àEߥwbàˆá jŽŽajˆ =ájp©€=áj%-ä(%±‰à@ͨ€@à~Á!j @ãÅ¥±ŠàB, BäàB¥±‹ B@JÀBÿÿÞ@x@¿AAJŒàEfaxáJe±à=ç¤F屎 =áˆRg€{å"±à@¢b€@å ±ë câ óå±]¥Aëàb bá ë屑 B@a¨ÀBÿÿ,@x@ßA$A¨’àEíya¨ëc‘bó“ =ájÝ€=âó$âóK”à@|Û€@å,±•àJèÚ JA#¥(Ã>å±– B@aRàBÿÿ* @x@ÇA AR—àEú“zaRàˆä F’’aR˜ =áRª›€=áR%±™à@ —€@ë c³   a  †B½šàbv– b@$ájå±› B@jÀBëc@x@ßA$AjœàEO{áj““aj =ájùR€=ä(%-â½%±žà@W @å n屟àB½Q€ƒá J$ ƒåô¥±  B@JÀBÿÿ* @x@¿AAJ¡àE |áJ””e±¢à=‰ƒ|ÿÿ* £ =áˆô€{áˆ*¿áˆ"ó¤à@D €@å ±!ˆ @ëcâ ó屨tAë¥àb¤á Ä^!¨ bᨑ¦ B@a¨ÀBå±@x@ßA$A¨§àEÆ€ë展–bó¨ =áj»†}aæä^K©à@…€@å,±ªàJ‰„ JA#äF屫 B@aRàBÿÿ$X@x@ÇA AR¬àEœ=~aàˆå ±——aR­ =áRç€ÓãÇä FœœaR =@þ€ƒæî€=â½!â½Ãà@Fê€@ˆñÓa u£€Â¹` ®pB½Äàb²é b@$áµå±Å B@€æÀBå±@x@ßA$AjÆàEÄ¢‚bàˆá jajÇ =ájH¦€=â½%-áj%±Èà@£¥€@áéÃÅ)Ú @ãÅ(®@BÉàB BàÁÀB)ºàB¥±Ê B@JÀBå±@x@¿AAJËàEK^ƒáJžže±Ìà=̤Fzÿÿ* Í =áˆ/d€{áˆ% áˆ%±Îà@„_€@-Iâ!ˆ @å±9»â óå± ‚A¨Ïàbçá ᨠbá륱РB@a¨ÀBöÇ@x@ßA$A¨ÑàEÒ„a¨ëcŸ bóÒ =ájöÙ€=ájä^"´Óà@YØ€@ÿ,ÿF„ÔàJÅ× JA#äFå±Õ B@aRàBå±@x@ÇA ARÖàEß…aRã„ä F¡¡aR× =áR˜˜€=å"±Øà@÷“€@å ±ã¸uæÒ’" ÑÞuÿÿ 0B½ÙàbcÀbA;ájå±Ú B@jÀBÿÿ]L@x@ßA$AjÛàEfL†áj¢¢ajÜ =ájîO€=â½%-ä(%±Ýà@I @â r$( @ãÅ"ü@BÞàBªN€ƒá Jå±ß B@JÀBå±@x@¿AAJààEí‡áJ££e±áà=rƒ|vå±â =áˆÙ €{áˆ% áˆ%±ãà@* €@å ±)ø @å±+câ óå±¹ÂA¨äàb‰€bᨠbá륱å B@a¨ÀBñ@x@ßA$A¨æàEsÃà ˆä ^¤¤bóç =ájÈ€=ä^"´èà@xÆ€@å,±éàJãÅ€Já Rå±ê B@aRàBÿÿÞ@x@ÇA ARëàEú~ˆâû¥¥aRì =áR¤†€=â½ç"½íà@‚€@ü yó8äõ•õÉõ”"¯uuŽa m4B½îàbn€bá jå±ï B@jÀBüy@x@ßA$AjðàE:‰áj¦¦ajñ =áj >€=áj%pä(%±òà@e=€@á å±óàBÅ<àBå±ô B@JÀBå±@x@¿AAJõàEö€ˆæ}ä §§e±öà=ă|ÿÿÞ÷ =áˆøû€{áˆ% áˆ"óøà@B÷€@2ûèB!ˆ @å±+câ óå±ÉÆAëùàb¤á ëcú B@a¨ÀBÿÿ…*@x@ßA$A¨ûàEޱŠbóñ¨©bóü =ájÅq‹`=å"±ýà@%p€@ñ,þàJ‘o JA#éøå±ÿ B@aRàB{ÿÿ¦@x@ÇA AR¢P`Eœ(Œaàˆâ ûªªaR =áRZ0€=â½å ±à@´+€@â ½ @â½C›Ñí£ñóñ¥å Ñà³°B½C2࣠ bàá½å± ¥@a$àB‚ÿ ÿ œÿÿB@x`ß =Aj @`E"ä€ëàˆá j««aj = ?`=»ç€=â½å ±à@ @à~Á$( @éwK x%ôàBw怃å(ÀBå± B@€ÆÀBÿÿ,@x@¿AAJ àE©Ÿâµ¬¬e± à=.²„FöÇ =ሥ€{âó%p .`@3e± @ ä €@4fãûë câ óå±BA¨àbE bá ë!¨ £áë«c B@`¥ÀB â ó"ôÿÿB@x@ßA$A¨àE0[Ža¨å±­®bó =á)_`=âó$ .`@3kc @ `Ä¿€@å,±àJ+ÀJ@på± B@€ÀBå±@x@ÇA ARàE=Ò Ó@Y ˆä F¯¯aR =áðÙ€=áR(oà@FÕ€@î !$F!“å±±0àëñç 5ÇÖà @¸ÀÿB½àb±Ô€bàáÄfå± B@jÀB†â ½å±@x@ßA$AjàEÄbàˆá j°°aj =ájL‘€=ä(%p .`@3ÒA @ €Ä©€@à~Á!j @ãÅ¥±àBàB$àB¶Ç B@€…ÀBëc@x@¿AAJàEKI‘áJ±±e± à=̤Fÿÿ* ! =áG?O€{áˆ% .`@3ÒBó" @ †J€@å ±!ˆ @âóå±[èAë#àbçá Áë!¨ bá륱$ B@`¥ÀBå±@x@ßA$A¨%àEÒ’a¨çãâ ó²²bó& =á)x €=ä^"´'à@Ú€@å,±(àJFÀJ ¥(ÁRëc) B@aRàBöÇ@x@ÇA AR*àEXÀ Ó@Y ˆá R³³aR+ =áR!È€=áR%±,à@pÀ@î!å ±#ãàDðúñóñ­Ç@¸À©ÿ‡è-àbÜ€bàáÁjå±. B@jÀBŒå±@x@ßA$Aj/àEß{“b½àˆá j´´aj0 =ájg€=ä(%- .`@3ÒCÅ1 @ €ÄÂ~€@ã<Á$( @ãÅ"ü@B2àB' BàÁÀB$àB¥±3 B@€…àB@˜¢oÀB&ýÿÿ$X@x@¿AAJ4àEf7”áJµµe±5à=n‚ïw „àƒÿÿ…*¿ÿéA6 = ) Áb=€{áˆ% .`@3ÒBó7 @ £8€@£ëcâ óå± ·A¨8àb bᨠ£á륱9 B@`¥ÀBÿ ÿ!š!¨ëc@x@ßA$A¨:àEìò€ëëc¶·bó; =á)³•aæáj$ .`@3e±< @ `ı€@â m,Î @ÿÿ,=àJë° J@pâ4` å±> B@€ÀBÿÿ$X@x@ÇA AR?àEúi–aàˆä F¸¸aR@ =á¯q€=å"±Aà@m€@ñ 3ÖàT÷ðÒ¯âå0åäàuB½Bàbrl€bàáÁjå±C B@jÀB„å±@x@ßA$AjDàE%—áj¹¹ajE =áj )€=â½%- .`@3ÒAF @ €Äd(€@å$±GàBÅ'€Bá Jå±H B@€…ÀBëc@x@¿AAJIàEáà ˆâ µººe±Jà=‰ó„Fÿÿ,K =áGüæ€{âó% .`@3ÒE±L @ Eâ€@›å ±': @ëcâ óå±¹‡AëMàb£á Âó!¨ bå ±N B@`¥ÀBëc@x@ßA$A¨OàEŽœ˜bóå±»¼bóP =á)»\™`=âó%p .`@3e±Q @ `Ä[€@å,±RàJ‰Z J@pe§áRå±S B@€ÀBå±@x@ÇA ARTàEœšaãÇâ û½½aRU =á]€=â½áR+cVà@°€@ë cCÿñ­%ª áSýDÖåPa íÿêWàb bá jå±X B@jàBå±@x@ßA$AjYàE"Ï€ëàˆá j¾¾ajZ =áj¶Ò€=â½å ±[à@ @áéÃÅ$( @ãÅ«c\àBrÑ€ƒàÁÀB$àB«c] B@JÀBå±@x@¿AAJ^àE©Š›bµÿÿ* ¿¿e±_à=*„Fÿÿ* ` =ሡ€{áˆ% .`@3bóa @ ã‹€@ë câ óå±IpE±bàbI bᨠ£áë«cc B@`¥ÀBå±@x@ßA$A¨dàE0Fœa¨ÿÿÞÀÁbóe =á)Y`=ájä^"´fà@»€@ÿ,ÿ,gàJ'ÀJ å±h B@aRàBå±@x@ÇA ARiàE=½€ÓáÛä FÂÂaRj =áRæÄ€=áR%±kà@IÀ€@ˆè o!“èoS0áåcpƒàpåröljÿ!dlàbµ¿ bA;Ÿÿ/ëÄfå±m B@jÀBÿÿQè@x@ßA$AjnàEÄxžbàˆá jÃÃajo =ájT|€=áj%- .`@3ÒCÅp @ €Ä°{€@â rå±qàB BàÁÁJå±r B@€…ÀBå±@x@¿AAJsàEK4ŸaJàËá JÄÄe±tà=̤F{ÿÿÞu =áG3:€{áˆ% .`@3ÒBóv @ …5€@ù» @âóå±-oAëwàbç¡á¨ bå ±x B@`¥ÀBÿÿB@x@ßA$A¨yàEÑï€ëëcÅÆbóz =á)û¯ aæê%±{à@\®€@ë,c|àJÈ­ JA#äFëc} B@aRàBå±@x@ÇA AR~àEßf¡aàˆâ ûÇÇaR =áRŒn€=â½%p .`@3b½€ @ €Äßi€@”å±cå +eâSûÿÿ,7àbK bàáÄf层 B@€¥ÀB‰ëc@x@ßA$AjƒàEf"¢ajäfÈÈaj„ =á)æ%€=ájå ±…à@A @á 屆àB¦$€ƒàÁÁJ屇 B@JÀBã C5 @'@x@¿AAJˆàEìÝ€ˆàËâ µÉÉe±‰à=mð„Fÿÿ¡¤Š =áˆäã€{áˆ% .`@3bó‹ @ @Ú'߀@Ÿâ ó5\ @å±Ãâ ó›ÿÿ–@µÿ ŒàbˆÞ€báᨠbå ± B@`¥ÀB¤â ó!¨Á¨@x@ßA$A¨ŽàEs™£bóå±ÊËbó =á)¥Y¤`=å"±à@X€@â m!j$ŸöÇ‘àJrW J þå ±’ B@aRàBå±@x@ÇA AR“àE€¥aàˆâ ûÌÌaR” =áR.€=â½! .`@3b½• @ €Ä‰€@ö Çs0ã1 ÷PååP0帀_€"òHo–àbõ€bä f`£â½áŸÿ[— B@€¥ÀBâ ½ÿÿ,@x@ßA$Aj˜àEÌ€ˆájÍÍaj™ =á){Ï€=ájå ±šà@Û΀@à~ÃÅ `@á(®AJ›àB;àB àB±œ B@JàB@Ùèª!Kå±@x@¿AAJàEއ¦âµÎÎe±žà=š„Fÿÿ,Ÿ =@®€Á~€{áˆ% .`@3bó  @ €@Ȉ€@ÿ ÿ,â ó屆A¨¡àb* bá ë!¨ £áë±¢ B@`¥ÀBÿÿ @x@ßA$A¨£àEC§a¨ñÏÐbó¤ =á)?¨`=å"±¥à@Ÿ€@â m屦àJ ÀJ â¸ëc§ B@aRàBÿÿÞ@x@ÇA AR¨àE"º Ó@Y ˆÿÿ ÿš‡ÑÑaR© =áR¸Á€=â½å ±ªà@½€@ÿÿBƒ-ë'íQZï` åq0ã§@¸À`ÐB½«àb†¼€bàáĩ屬 B@jÀBÿÿ; @x@ßA$Aj­àE©u©bàˆá jÒÒaj® =áj-y€=ájå ±¯à@‰x€@à~Áå±°àBéwàB$ ƒä†ü± B@JÀBñ@x@¿AAJ²àE01ªaJÿÿCˆÓÓe±³à=±¤Få±´ =ሠ7€{áˆ% .` e±µ @ j2€@ù »$F @ëcâ óå±Ä"Bó¶àbСè ¥!¨ bá¨XF· B@`¥ÀBÿÿô@x@ßA$A¨¸àE¶ì€ëâtâ óÔÔbó¹ =á)Pñ€=å"±ºà@²ï€@å,±»àJÀJA#¥(Ã>å±¼ B@aRàBÿÿÞ@x@ÇA AR½àE=¨«bûàˆá RÕÕaR¾ =áRí¯€=áR6Ç¿à@M«€@ö Ç“€¢E«“*˜a rB½Ààb¹ª b ájå±Á B@jÀBÿÿ$X@x@ßA$AjÂàEÄc¬ájÖÖajà =áj屿 B@aRàBå±@x@ÇA ARçàE€û²aÿÿ(ßßaRè =áRE³`=å"±éà@ þ ~ù …$F!“èo³E óT{S KªDa p¿D©êàb  bâ ½å±ë B@jÀBÿÿåü@x@ßA$AjìàE·,àˆä fààají =Àƒº€=â½%- .`@3ÒAî @ €Äê¹€@â r!j @ãÅ"ü@BïàBK BàÁå±ð B@€…ÀBå±@x@¿AAJñàEŽr´bwàˆá Jááe±òà=…„Få±ó =áGzx€{áˆ% .`@3ÒE±ô @ Ës€@å ±!ˆ @âóå±ÔMA¨õàb* báᨠbá륱ö B@`¥ÀBñ@x@ßA$A¨÷àE.µa¨ëcâãbóø =á)Aî€=áj$ .`@3e±ù @ `Ä£ì€@å,±úàJÀJ@péøå±û B@€ÀBzÿÿ$X@x@ÇA ARüàE"¥¶aRàˆâ ûääaRý =ᬀ=áR+cþà@¨€@ü yÃÖ;ŠSr÷SqßSqþä‚a ùàB½ÿàb†§€bàá½å±¿ÿROjÀBÿÿB@x@ßA$AjàE©`·ajå2á jååaj =áj1d€=áj%p õ`@3ÒAà@Œc€@à~Á$( @ãÅ¥±àBñb€Bá Jëc B@JàB@¥âøüy@x@¿AAJàE/¸aJàˆá Jææe±à=°¤FwÿÿÞ =@®€Á$"€{è¥% áˆ"ó à@m€@ÿ ÿBë câ óå±]Aë àbËá Áë!¨ £á륱 B@`æÀBÿÿ @x@ßA$A¨ àE¶×€ëå±çèbó =ájË—¹aæâóå ±à@-–€@â m!j @ÿÿ,àJ™• J â¸å± B@aRàBå±@x@ÇA ARàEÄNºaãÇâ ûééaR =áRlV€=áR"½à@ÀQ€@óÓ!RèoÓ3ð£ðõt?ðSq÷ƒÿÿ5n,ÿ!dàb, bàá½å± B@jÀBÿÿB@x@ß =Aj @`EJ »ajáóá jêêaj = ?`=Ò €=áj%pä(Cà@- @à~Áå±àBŽ €ƒà B$ ƒä‘ B@€ÆÀBëc@x@¿AAJàEÑÅà ˆá Jëëe±à=RØ„Fÿÿ áˆÉË€{áˆ% áˆ"óà@Ç€@ÿ ÿÞ!ˆ @âó屈ÿ'LàbmÆ€bᨠbᨑ B@a¨ÀBÿÿÞ@x@ßA$A¨!àEX¼bóñìíbó" =âóA½`=ç$áj(f#à@ã?€@ÿ,ÿB$àJOÀJA#å±% B@aRàBñ@x@ÇA AR&àEeø€ÓáÛâ ûîîaR' =áR6¾aRâ½áR"½(à@yû€~¨ñ ãtð"ñóúÀa ²©D©)àbåú€bàáÄ©å±* B@jÀB™ÿÿÞ@x@ßA$Aj+àEì³,àˆá jïïaj, =áj|·€=áj%-â½%±-à@×¶€@à~Á$( @ãÅ"ü@B.àB8àBå±/ B@JÀBå±@x@¿AAJ0àEso¿bwã>á Jððe±1à=ô¤Få±2 =áˆ{u€{áˆ% áˆ"ó3à@¨p€@¸â ó)ø @å±Ãâ óå±[A¨4àb báÁë!¨ bá륱5 B@a¨ÀB´â ó- ÿÿ @x@ßA$A¨6àEù*Àa¨ëcñòbó7 =áj+ë€=å"±8à@é€@ë,c9àJüè J þå ±: B@aRàBvñ@x@ÇA AR;àE¢ÁaRàˆâ ûóóaR< =áR¬©€=â½å ±=à@ÿ¤€@ë cóƒ,ïðä£Kiƒ,àÿÃa ‘B½>àbk bàá½å±? B@jÀBÿÿB@x@ßA$Aj@àEŽ]ÂajãFá jôôajA =ája€=â½%pä(%±Bà@i`€@á å±CàBÊ_€BàÁÁJ$ ƒä‹cD B@JÀBÿÿ5n@x@¿AAJEàEÃaJàËá Jõõe±Fà=•+ƒ|ÿÿÞG =ሀ{âó% áˆ%±Hà@N€@ å ±!ˆ @å±Kâ óå±)cAëIàb°á ᨠbá¨XFJ B@a¨ÀBñ@x@ßA$A¨KàE›Ô€ëá¨ööbóL =áj8Ù€=âóä^(fMà@—×€@å,±NàJÀJ ëcO B@aRàB|å±@x@ÇA ARPàE"Äâû÷÷aRQ =áRÙ—€=áR"½Rà@6“€@üy%± .àŸƒ-à”P"a –ÿ]LSàb¢’€bá jå±T B@jÀBÿÿ@Ò@x@ßA$AjUàE¨KÅájøøajV =áj5O€=áj%-ä(%±Wà@’N€@å n$( @ãÅ"ü@BXàBðM€Bá Jå±Y B@JÀBëc@x@¿AAJZàE/ÆáJùùe±[à=7ÕƒÇÿÿ@Ò\ =ሠ€{å"±]à@k€@ÿÿ â óå±2ÿ]L^àbËá å±_ B@a¨ÀBœëc@x@ßA$A¨`àE¶Â€ëÿÿÙúúbóa =ájXÇ€=ájå ±bà@ºÅ€@å,±càJ&ÀJ å±d B@aRàBöÇ@x@ÇA AReàE=~ÇbûãÇè ­ûûaRf =áRò…€=äFáR%±gà@E€@â ½$F @èoäÿþ¿¾÷¿{ôa ›“Dfhàb±€€bá jå±i B@jàBâ ½ÿÿô@x@ßA$AjjàEÃ9Èajâ½üüajk =ájS=€=â½%på±là@¯<€@áéÃÅ!j @å ±màB Bá Jå±n B@JÀBå±@x@¿AAJoàEJõ€ËáJýýe±pà=VÃÇôw!^ã|ÿÿ5nq = ) ÁNû€{áˆ*¿áˆ"órà@Œö€@§ëcâ óå±?aBósàbê¡á¨ £áë«ct B@a¨ÀB¬â ó"ôñ@x@ßA$A¨uàEѰÉbó¼ 5¡jä^Joþÿbóv =ájúpÊ`=áj$áj(fwà@\o€@ö,ÇxàJÈn J ®ÀÉÃ>üyy B@aRàBå±@x@ÇA ARzàEÞ'ËaáÛáRA@ A{ =@€ƒ/€=áR"½|à@â*€@ÿÿ #+bðp+eð€Ë" àZsB½}àbNÀb@&¢4Ájå±~ B@€æÀByâ ½å±@x@ßA$AjàEeã ë@Y ˆá jaj€ =ájõæ€=áj%pâ½+cà@P @à~Á层àB±å€ƒà Bå±ßÿuÆJÀBå±@x@¿AAJ„ ˆ@€EìžÌâµ@=…à=q±„Fwÿÿ,† =áˆؤ€{áˆ% áˆ"ó‡à@& €@—å ±`@è¥+câ óå±þAëˆàbˆŸ€bᨠ£å ±‰ B@abÀBÿÿÞ@x@ßA$AjŠàErZÍa¨öÇbó‹ =ájŒÎ`=G@—£1²å ±Œà@í€@ë,càJYÀJà ‹å±Ž B@aRàBå±@x@ÇA ARàE€Ñ Ó@Y¡ä F E± =áR5Ù€=â½ç"½‘à@ˆÔ€@”â ½$F @â½%±3 …"å7‰|@·à“B½’àbôÓ€bàáÁö屓 B@jÀBöÇ@x@ßA$Aj”àEÏbç¥á jaj• =áj‹€=ájå ±–à@æ€@á !j @ãÅ(®AJ—àBK BàÁÁJ屘 B@JÀBã C&ýÿÿ$X@x@¿AAJ™àEHÐaJàËá Je±šà=[„Fÿÿ* › =ሆN€{äF% 屜à@ÈI€@ÿÿ,â óå±7úA¨àb) báᨠ£áë«cž B@a¨ÀBÿÿ,@x@ßA$A¨ŸàEÑa¨å± bó  =áj2Ä€=áj$áj"´¡à@“€@â m' @ÿÿÞ¢àJÿÁ J ¥(ÁRå±£ B@aRàBå±@x@ÇA AR¤àE!{ÒaRàˆâ û  aR¥ =áRÏ‚€=äF!áR%±¦à@*~€@ö ÇCœªÂÔÐâÔ à5ÿB§àb–}€bä f屨 B@jÀBëc@x@ßA$Aj©àE¨6Óáj  Gª =áj0:€=áj%páj+c«à@Œ9€@áéÂr屬àBì8àBå±­ B@JÀBëc@x@¿AAJ®àE/òà ˆâ µ  e±¯à=°¤Füy° =áˆø€{áˆ% áˆ"ó±à@ió€@è ¥$F @è¥+câ ó屑MAë²àbËá Áë!¨ bå ±³ B@a¨ÀBÿÿ¦@x@ßA$A¨´àE¶­ÔbóÿÿÞ bóµ =ájèmÕ`=å"±¶à@Hl€@å,±·àJ´k JA#屸 B@aRàBÿÿ* @x@ÇA AR¹àEÃ$Öaàˆâ ûaRº =áRi,€=áR"½»à@Ë'€@ñ  SPä _æbð nñ"~ÍÿÿF„Þ¨B½¼àb7ÀbA;ájå±½ B@jÀBñ@x@ßA$Aj¾àEJà ë@Y ˆá jaj¿ =ájÖã€=ä(å ±Àà@2 @à~ÃÅå±ÁàB’ à B$ ƒäŸÿ, B@JÀBå±@x@¿AAJÃàEÑ›×bµç…á Je±Äà=R®„F{ñÅ =ሹ¡€{áˆ% 屯à@ €@å ±!ˆ @å±+câ óå±zZBóÇàbmœ€báÁë!¨ b᨟ÿ,È B@a¨ÀBå±@x@ßA$A¨ÉàEWWØa¨ëcbóÊ =ájŠÙ`=ä^+cËà@ê€@å,±ÌàJVÀJA#£½ÁRëcÍ B@aRàBå±@x@ÇA ARÎàEeΠÓ@Y ˆâ ûaRÏ =áRÖ€=áR%±Ðà@iÑ€@å ±cõ8õ9õ:QZïp!Òu8@¸Àêy…ÑàbÕРb@% áÁjå±Ò B@jÀBå±@x@ßA$AjÓàEë‰Úbàˆá jajÔ =áj|€=áj%-ä(+cÕà@ØŒ€@ã Å$( @ãÅ"ü@BÖàB8àB$àB¥±× B@JÀBÿÿÞ@x@¿AAJØàErEÛaJüye±Ùà=ó¤FëcÚ =áˆbK€{å"±Ûà@±F€@å ±ë câ óå±NmÜàb bâjÂó!¨å ±Ý B@a¨ÀBöÇ@x@ßA$A¨ÞàEùÜa¨ëcbóß =áj Á€=âóïÂ"´àà@€¿€@å,±áàJì¾ J äFå±â B@aRàBå±@x@ÇA ARãàExÝaRàˆä FaRä =Àƒ±€=áR%±åà@{€@å ± sLu9TƒG!%ä…» Œà”Dfæàb~z b@&ájå±ç B@jÀBzñ@x@ßA$AjèàE3Þájajé =áj 7€=ä(%-å±êà@j6€@å$±ëàBÍ5€Bá J$ ƒåôi‹cì B@JàB@ÙáöÇ@x@¿AAJíàEïà ˆâ µe±îà=•ƒ|öÇï =@®€Áõ€{áˆ% áˆ"óðà@Oð€@å ±': @ëcâ óå±¥fBóñàb°á å±ò B@`æÀBå±@x@ßA$A¨óàE›ªßbóå±bóô =!b€ƒÂjà`=ä^%±õà@!i€@â m'%òöÇöàJh J â¸å±÷ B@aRàBÿÿ @x@ÇA ARøàE¨!áaàˆâ ûaRù =áR?)€=áR"½úà@œ$€@ö ǃåqTB9¯rïñæ a Ìÿ²ûàb bàá½å±ü B@jÀBÿÿ @x@ßA$AjýàE/Ý ë@Y¤¬á jajþ =áj»à€=ä(%-ä(+cÿà@ @å$±%‚àƒ{߀ƒá Jå± B@JàB@U¡Áå±@x@¿AAJàEµ˜ââµ  e±ÆI€ƒ7«„F€ñ {@®€=®ž€{áˆ%páˆ"óà@ù™€@ÿ ÿ,â óå±hÿ!šàbY bᨠ£ .` ± `æÀBÿÿ§V@x@ßA$A¨àEå±! B@aRàB| ä‰ÿÿ²º@x@ÇA AR"àEëtèaã„ä F((aR# =áR—|€=å"±$à@÷w€@å ± £‚ÆàÃáæä…¯õ9 ýàÁB½%àbcÀb@&á'å±& B@jÀB{ñ@x@ßA$Aj'àEr0éáj))aj( =ájö3€=â½%pä(+c)à@T @â rå±*àB²2€ƒá Jå±+ B@JÀBå±@x@¿AAJ,àEùëà ˆâ µ**e±-à=zþ„Få±. =áˆåñ€{áˆáˆ%±/à@5í€@å ±)ø @ñÃâ ó ›”hv¼ï/GÌï?»3Aë0àb•ì€bᨠb .`å±1 a¨ÀBñ@x@ßA$A¨2àE§êbóëc+,bó3 =áj§gë`=ájå ±4à@ f€@å,±5àJve JA7äFå±6 B@aRàBÿÿ* @x@ÇA AR7àEìaàˆâ û--aR8 =áR<&€=áR"½9à@™!€@ñ ³ÿ-0à9ï´ó"ÿÿ,€´B½:àb bàáÈoå±; B@jÀBÿÿàJ@x@ßA$Aj<àEÚ€ëå2á j..aj= =áj|Ý€=ájå ±>à@ÙÜ€@á å±?àB@àBå±@ B@JÀB„ âøñ@x@¿AAJAàEš•íâµ//e±Bà=¨„Fÿÿ…*C =@®€Á‹›€{è¥% ç:"óDà@×–€@ñ !ˆ @å±+câ óå±$FBóEàb6 bá ë!¨ b .`å±F `æÀBÿÿ¦@x@ßA$A¨GàE!Qîa¨å±01bóH =ájQï`=ájå ±Ià@´€@å,±JàJ ÀJA7â¸å±K B@aRàBÿÿ›ò@x@ÇA ARLàE/È€ÓäF22aRM = …€ƒÖÏ€=äFáR"½Nà@7Ë€@ë cÿÿ,‚3àõ8£àëc1ÿ‚6Oàb£Ê€bàáå±P B@€æÀBëc@x@ßA$AjQàEµƒðbáóå ±33ajR =áj=‡€=ájå ±Sà@›†€@à~Á$( @ .`CűT àƒý…àB$àB¼yU B@JàB@Uâøå±@x@¿AAJVàEâ µ99e±là=_û„Få±m =áˆÊî€{áˆ% áˆ(¥nà@ê€@ÿÿ]Lâ óå±ÂïAëoàbzé€báÂó!¨ £ .`a¨Ÿÿ p a¨ÀBå±@x@ßA$A¨qàEd¤õbóëc:;bór =áj‘dö`=å"±sà@ób€@è '$Ÿ Ã! !% … ARtàJ_ÀJ å±u B@aRàBñ@x@ÇA ARvàEr÷aàˆâ û<>e±à=¥„F层 = 5 Ák˜€{å"±ƒà@¼“€@ë cãâóå±A1Aë„àb bâ ó!¨å ±… B@a¨ÀBå±@x@ßA$A¨†àENùa¨å±?@bó‡ =áj/ú`=âó%pâó.ˆà@‘ €@å,±‰àJý  JA#äFëcŠ B@aRàBå±@x@ÇA AR‹àEÅà ˆä FAAaRŒ =áRÈÌ€=áR%±à@ È€@ÿÿL6ó”P|1ÓïNp!Ƀa ›B½Žàb‹Ç€bá jå± B@jÀBÿÿL6@x@ßA$AjàEš€ûbàˆá jBBaj‘ = …€ƒ"„€=ä(%-â½%±’à@}ƒ€@å$±“àBâ‚€BàÁÅô$ ƒ .`å±” €ÆàB@Ùâøñ@x@¿AAJ•àE!<üáJCCe±–à=ª¤Fnÿÿ,— =@®€ÁB€{áˆ% áˆ"ó˜à@^=€@ÿÿ; â óå±?êAë™àb½á ᨠ£ .`a¨‹cš `æÀBÿÿ$X@x@ßA$A¨›àE¨÷€ëñDEbóœ =ájÕ·ýaæä^%±à@6¶€@â mëcžàJ¢µ JA7â¸å±Ÿ B@aRàBÿÿ$X@x@ÇA AR àEµnþaàˆä FFFaR¡ =áR^v€=áR"½¢à@±q€@è o)ø'Eù… (àü£à/õ‚î<õƒ€ñÑà÷TB½£àb bàá½å±¤ B@jàBñ@x@ßA$Aj¥àE<*ÿájGGaj¦ =ájÄ-€=ä(%-ä(%±§à@# @å$±¨àB„,€ƒá J屩 B@JàB@Ùâøå±@x@¿AAJªàEÂåà ˆâ µHHe±«à=Hø„Füy¬ =@®€Á»ë€{áˆ% áˆ"ó­à@ç€@å ±$F @âóÃâ óå±S¦Aë®àb_æ€bá ë!¨ b .`屯 `æÀBå±@x@ßA$A¨°àEI¡`ÌëcIJbó± =!!€ƒna`=áj$áj%±²à@Ð_€@â m!j @ñ³àJ<ÀJ â¸å±´ B@aRàBå±@x@ÇA ARµàEWaàˆâ ûKKaR¶ =áR €=å"±·à@k€@ëc î”l@Óï”ÿî”oPÿÿWš½IB½¸àb×€bàáå±¹ B@jÀBÿÿ]L@x@ßA$AjºàEÝÓà ˆá jLLaj» =ájn×€=ájå ±¼à@ÉÖ€@ã Å' @ .`CÅ"üAJ½ àƒ) Bá J$àB±¾ B@JÀBöÇ@x@¿AAJ¿àEd„Õàˆá JMMe±Àà=å¤Fÿÿ$XÁ =áP•€{äF% äF%±Âà@š€@›ÿ ÿ2° @c‘ ŸÿÒ(â óå±»A¨Ãàb báÂó!¨ b .`Aë«cÄ a¨ÀBñ@x@ßA$A¨ÅàEëJ`Ìàˆá ¨NNbóÆ = ¾€ƒ›O€=áj%páj"´Çà@ûM€@å,±ÈàJgÀJ å ±É B@aRàBÿÿ @x@ÇA ARÊàEraRãÇá ROOaRË =áR €=áR"½Ìà@z €@å ±#Žƒàõ:"ñÑî”þ@%¹a ÂyB½Íàbæ€bá jëcÎ B@jÀBÿÿ¡¤@x@ßA$AjÏàEøÁà ˆá jPPajÐ = …€ƒˆÅ€=áj%-â½+cÑà@åÄ€@á å±ÒàBDàB$ ƒ .`d–ÇÓ €ÆÀBå±@x@¿AAJÔàE}bµàËá JQQe±Õà=‡KƒÇÿÿF„Ö =áˆoƒ€{áˆ% áˆ"ó×à@»~€@å ±)ø @`Û E±+c#âóå±f‹AëØàb báå±Ù B@a¨ÀBÿÿÞ@x@ßA$A¨ÚàE9`Ìàˆá ¨RRbóÛ =áj°=€=è¥å ±Üà@<€@ˆ@SíÑ!j @ëcÝàJ~; JA#àJëcÞ B@aRàBöÇ@x@ÇA ARßàEŒôà ˆá RSSaRà =@þ€ƒIü€=â½!â½áà@¡÷€@å ±3+[ÿPà$ð\w4=ÜàíÿŠÜâàb  b擽å±ã B@€æÀBöÇ@x@ßA$AjäàE°â½TTajå =áj—³€=áj%-áj%±æà@ò²€@â rå±çàBSàBå±è B@JÀBå±@x@¿AAJéàEšk áJUUe±êà=¢¤FÿÿF„ë =ሎq€{áˆ%páˆ%±ìà@×l€@ÿÿ,â óå±IoAëíàb6 bá ë!¨ £ .`gZ–Çî a¨ÀB  %ÿÿŽ2¾ÿÿF„@x@ßA$A¨ïàE!' a¨¼ 5 Eä^c¿VWbóð =ájFç€=å"±ñà@§å€@å,±òàJÀJ ÈÀÉÄFå±ó B@aRàBñ@x@ÇA ARôàE.ž aRàˆáRA@XXaRõ =@€ƒߥ€=áR"½öà@:¡€@ÿÿÞ CðÒûUõ:Âû"€yåvÀŽÿ* ÷àb¦ €bàáÁjå±ø B@€æÀBñ@x@ßA$AjùàEµY ájYYajú =áj=]€=ä(å ±ûà@˜\€@æ ¹å±üàBù[€Bá J$ ƒ .`d‹cý JÀBå±@x@¿AAJþàE< aJàËâ µZZe±ÿà=½¤Fÿÿ,áˆ,€{áˆ% å±à@y€@ÿ ÿO*)ø @`Û «câ óå±*ÿ* àbØá Âó!¨ b¡¨…± B@a¨ÀBëc`x@9 A¨ @€EÂЀëâ1á ¨[[bó =âófÕ€=ájê1à@ÆÓ€@å,±àJ2ÀJ@ݦ{ÁRå± B@a àBñ@x@ÇA AR àEIŒbàˆá R\\aR =áR”€=å"± à@a€@ÿÿÞ Sàx38äõ9õ:ÿ ›a åæHo àbÍŽ b@$ájå± B@jÀBÿÿ$X@x@ßA$AjàEÐGáj]]aj =!b ƒXK€=â½%-ä(+cà@µJ€@äåÂrå±àB Bá J$ ƒä…± B@JÀBå±@x@¿AAJ`D EVáJ^^e±à=^уÇñ =áˆG €{áˆ% áˆ(¥à@’€@—öÇâ óå±AAëàbòá Áë!¨ £á¨…± B@a¨ÀBÿÿ* @x@ßA$A¨àEݾ€ëÿÿ,þ__bó =ájwÀ=ájå ±à@ÙÁ€@å,±àJEÀJA#äFå± B@aRàBëc@x@ÇA ARàEdzâû``aR =áR‚€=áR"½ à@x}€@ë cc%© åsõ8…t9"å8$þ`àÿÀÜ!àbä|€bá jaöâ½á„fßÿ™jÀBëc@x@ßA$Aj# ˆ@`Eë5ajå2ç aaaj$ =ájs9€=áj%-ä(%±%à@Í8€@á å±&àB3àB$ ƒáJ¥±' B@aàB@Ùâø/lÿÿL6@x@¿AAJ(àEqñà Ëá Jbbe±)à=}¿ƒÇìÿÿWš* =@®€Áb÷€{è¥% áˆ"ó+à@¯ò€@—å ±ë câ óå±4ÿÆÄ,àb  bá ë!¨ £á¨ÿbþ- B@`æÀBëc@x@ßA$A¨.àEø¬bóÿÿ,cdbó/ =áj)m`=ájå ±0à@‹k€@â m!j @öÇ1àJ÷j JA#â¸å±2 B@aRàBöÇ@x@ÇA AR3àE$aäFeeaR4 =áR¶+€=äF!â½5à@'€@ë cs``.$pKu9uÿÿ; °5D©6àb‚&€bàáÈoå±7 B@jÀBÿÿô@x@ßA$Aj8àEŒß€ˆáóä fffaj9 =ájã€=áj%-áj%±:à@oâ€@à~Áå±;àBÐáàB$ ƒä†ü< B@JàB@Ùâøå±@x@¿AAJ=àE›bµàËá Jgge±>à=”­`=ÿÿ,? =@®€Áÿ €{áˆ% áˆ%±@à@Qœ€@“å ±!ˆ @` Q.Wâ óå±ÇAëAàb¯á Áë!¨ bᨅ±B B@`æÀBÿÿ,@x@ßA$A¨CàEšV`ÌöÇhibóD =ájÃ`=å"±Eà@%€@ÿ,ÿ¦FàJ‘ J â¸å±G B@aRàBå±@x@ÇA ARHàE§Í€ˆâûjjaRI =áR<Õ€=â½å ±Jà@›Ð€@ë cƒ1 9u:)"°t“ôa …®B½Kàb bàá½å±L B@jÀB}ù …ÿÿ]L@x@ßA$AjMàE.‰bàˆä fkkajN =áj¶Œ€=áj%-ä(%±Oà@ @à~Á$( @ãÅ4@BPàBr‹€ƒà B$àB¥±Q B@JÀBñ@x@¿AAJRàEµDaJã>á Jlle±Sà=6W„Få±T =ሡJ€{áˆ% áˆ%±Uà@êE€@ü y)ø @`Û E±Ãâ óå±!\A¨VàbQ báÁë!¨ bá륱W B@a¨ÀBå±@x@ßA$A¨XàE;`ÌëcmnbóY =ájmÀ€=å"±Zà@ξ€@è !j$Ÿëc[àJ:ÀJA#å±\ B@aRàBñ@x@ÇA AR]àEIwaRàˆâ ûooaR^ =áRò~€=áR"½_à@Mz€@å ±“pä“ô` ²8¥ìÿÿ* ­ÿ œ`àb¹y€bàá½å±a B@jÀBñ@x@ßA$AjbàEÐ2ajãFá jppajc =ájX6€=ä(%-ä(%±dà@³5€@á å±eàB BàÁÁJ$ ƒä‹cf B@JÀBå±@x@¿AAJgàEVîà Ëá Jqqe±hà=Û¤Fÿÿbþi =áˆBô€{áˆ% áˆ"ójà@ï€@ü y!ˆ @b& E±+câ óå± ÿ„kàbòá ᨠbᨋcl B@a¨ÀBå±@x@ßA$A¨màEÝ©`Ìâtá ¨rrbónájŠ®€=ä^(foà@鬀@å,±pàJUÀJA#å±q B@aRàBüy@x@ÇA ARràEdeáRssaRs =â½m€=áR"½tà@|h€@å ±£:"€!‹_´mä“a D©uàbèg€bá jå±v B@jÀBüy@x@ßA$AjwàEê ájttajx =ájs$€=áj%-ä(%±yà@Ð#€@å n$( @ãÅ"ü@BzàB/àBå±{ B@JÀB‹@Ùâø,¯öÇ@x@¿AAJ|àEqÜ€ËáÓä uue±}à=yªƒÇüy~ =@®€Á]â€{å"±à@©Ý€@ÿ ÿ â ó屡ýA¨€àb  báå± B@`æÀBå±@x@ßA$A¨‚àEø—!bóÿÿÞvwbóƒ =áj3X"`=áj+"âó"´„à@“V€@å,±…àJÿU JA#â¸ñ† B@aRàBå±@x@ÇA AR‡àE#aàˆâ ûxxaRˆ =áR¥€=äFáR%±‰à@ý€@ÿ ÿ ³´´1õ9¶t@ŒàÙVB½Šàbi bàá½å±‹ B@jÀBƒñ@x@ßA$AjŒàEŒÊ€ëàˆá jyyaj =@Ý€ƒ΀=â½%pâ½%±Žà@vÍ€@å$±àBÔÌ€BàÁÁJå± B@€ÆÀBëc@x@¿AAJ‘àE†$âµzze±’à=”˜„Fÿÿ@Ò“ =ሌ€{áˆ*¿áˆ"ó”à@O‡€@å ±': @`Û «câ óå±}¤Aë•àb¯á ᨠbç«c– B@a¨ÀB â ó'Zÿÿ @x@ßA$A¨—àEšA%`Ìå±{:#A& =ájÍ&`=ájä^%±™à@0€@å,±šàJœÿ€Éâ ûå±› B@aRàBöÇ@x@ÇA ARœàE§¸ Ó@Y¦:ä F}}aR =áRcÀ€=å"±žà@»»€@å ±Ã“õ+d €-Lÿÿtš‰B½Ÿàb' bàá½å±  B@jÀBå±@x@ßA$Aj¡àE.t'bÿÿê~~aj¢ =áj®w€=â½å ±£à@  @å$±¤àBrv€ƒá Jå±ßÿ£FJÀBÿÿ@Ò@x@¿AAJ¦ ˆ@€E´/(áJe±§à=6Bƒ|ÿÿÚ˜¨ =ር5€{âó% 屩à@õ0€@å ±5\ @`Û E±6Çâ óå±>{BóªàbT bᨠbå ±« B@abÀBå±@x@ßA$A¨¬àE;ë€ëöÇ€bó­ =ájc«)a âóå ±®à@Æ©€@å,±¯àJ2ÀJ ¥(ÄFå±° B@aRàBå±@x@ÇA AR±àEIb*aãÇå ±‚‚aR² =áR j€=áR(o³à@ae€@ÿ ÿ* Óƒ7ïðäõ{õ|ÿlàþa ¤eB½´àbÍd€bá jå±µ B@jÀB‡è oüy@x@ßA$Aj¶àEÏ+ajàˆá jƒ&(o· =ájS!€=ájå ±¸à@­ €@à~Âr)Ú @ãÅ(®AJ¹àBàB屺 B@JÀBëc@x@¿AAJ»àEVÙà Ëá J„„e±¼à=פFÿÿB½ =áˆN߀{áˆ% å±¾à@“Ú€@ÿÿtâ óå±—A¨¿àbòá Áë!¨ £áë«cÀ B@a¨ÀBÿÿ* @x@ßA$A¨ÁàEÝ”,bóëc…†bó =áj U-`=çå ±Ãà@pS€@å,±ÄàJÜR€Jå(ÁRå±Å B@aRàBÿÿ@Ò@x@ÇA ARÆàEê .aãÇâ û‡$®$FÇ =áRš€=â½áR%±Èà@ê€@ù… @` Ho"½ã´?€î´ n@à´|yÉàbV bàáÁjå±Ê B@jÀBÿÿÚ˜@x@ßA$AjËàEqÇ€ëàˆá jˆˆajÌ =ájýÊ€=áj%pä(1Íà@W @à~Áå±ÎàB¹É€ƒà B$ ƒäœyÏ B@JÀBå±@x@¿AAJÐàEø‚/bàˆá J‰‰e±Ñà=y•„Få±Ò =áˆ숀{áˆ% áˆ"óÓà@1„€@£ÿ ÿÞë câ óå±|yÔàb”ƒ€báÁë!¨ £á¨œyÕ B@a¨ÀBëc@x@ßA$A¨ÖàE~>0ᨊŠbó× =áj.C€=å"±Øà@A€@ÿ,ÿÞÙàJû@ J ëcÚ B@aRàBÿÿ,@x@ÇA ARÛàEú€ˆáÛâ û‹‹aRÜ =áR¿1aáR"½Ýà@ý€~å ± óàþ´?ïDÿ€%µÿÿbþšéD©Þàb}ü€bàá½å±ß B@jÀBÿÿx@x@ßA$AjààEŒµ,àˆá jŒ&'á =áj¹€=ä(%-ä(%±âà@p¸€@â rå±ãàBз€BàÁÁJ$ ƒä…±ä B@JÀBå±@x@¿AAJåàEq2bwàËá Je±æà=?‚õw!^ã|ÿÿŠÜç = ) Á w€{áˆ% áˆ"óèà@Mr€@¤å ±!ˆ @`Û ¨¥â óå±>Aëéàb¯á ᨠbᨅ±ê B@a¨ÀBëc@x@ßA$A¨ëàE™,3`ÌñŽbóì =ájÌì€=ä^.íà@,ë€@ë,cîàJ˜ê JA#â4` ëcï B@aRàBÿÿnb@x@ÇA ARðàE§£4aRàˆâ ûaRñ =áRU«€=áR"½òà@¯¦€@öÇ Dÿï‰ w 5 Uÿ&JÀ27B½óàb bàáÁjå±ô B@jÀB3ká­ñ@x@ßA$AjõàE-_5áj‘‘ajö =áj®b€=áj%-ä(%±÷à@  @äåÁ$( @ãÅ"ü@BøàBja€ƒà Bå±ù B@JÀBÿÿ,@x@¿AAJúàE´6áJ’’e±û`‚ ƒ5-ƒ||ÿÿ@Òü =ሠ €{å"±ýà@ë€@ÿÿ â óå±–ÅA¨þàbP bâ ó!¨å ±ÿ B@a¨ÀBÿÿ,@x@ßA$A¨  €E;Ö€ëàˆä ^““bó = >`=ÜÚ€=áj/âó"´à@?Ù€@å,±àJ«Ø J å ± B@€ÎÀBöÇ@x`Ç =AR @`E‘7âû””aR =áRb™€=äFáR%±à@”€@ˆè o @` ˆo  ) 7 _ + C!Ÿÿ,V œ-B½àb.Àb@$g!§å± B@a$àBÿÿL6@x@ßA$Aj àEHM8`Ìàˆâ ½••aj =ájÉP€=áj%pâ½%± à@# @å nå± àB„O€ƒàÁÁJ$ ƒåô«c B@JÀBëc@x@¿AAJàEÏ9áJ––e±à=פFÿÿÃÐ =ሷ€{äF% áˆ"óà@ €@ÿÿôâ óå±8ßAëàbk €bᨠ£á¨‹c B@a¨ÀB#öã6(¦ÿÿ,@x@ßA$A¨àEVÄ€ˆöÇ—&.W =ájŠ„:aæáj$áj%±à@í‚€@å,±àJYÀJ þäFëc B@aRàBå±@x@ÇA ARàEc;;aàˆä F™™aR =áR C€=äFáR"½à@g>€@ÿÿ  #„"% •€@<àýôpba ÿ2zàbÓ=€bàáÄfå± B@jÀBÿÿB@x@ßA$AjàEêöà ˆá jššaj =ájÆú€=â½%-â½%±!à@! @å$±"àB‚ù€ƒá Jå±# B@JÀBå±@x@¿AAJ$àEq²áRaR0 =!  ƒ 1€=â½å ±1à@z,€@â ½)ø @` «c3€<€e­äÿPÁ€ @À¶D©2àbæ+€bâ4à å±3 B@jÀB‚è oñ@x@ßA$Aj4àEåà ˆâ ½žžaj5 = …€ƒ‘è€=ájå ±6à@íç€@á !j @ãÅ"üAJ7àBMàBå±8 B@€ÆÀBå±@x@¿AAJ9àEŒ ?bÿÿÔæŸŸe±:à=”nƒ|öÇ; =áˆ|¦€{áˆ% å±<à@Æ¡€@ñ !ˆ @`Û âóå±µDA¨=àb, bá ë!¨ báë«c> B@a¨ÀB*ûä¡"ô @'@x@ßA$A¨?àE\@`Ìàˆâ ó  bó@ \ ƒ¸`€=å"±Aà@_€@ˆ@Såf'ÿÿ¦BàJ†^ J àJå±C B@A*àBëc@x@ÇA ARDàE™AáR¡¡aRE =@þ€ƒM€=áR(oFà@¡€@”å±Ce­´ÿä€#` àÿÚ˜Gàb  bá jå±H B@€æÀB‡â ½å±@x@ßA$AjIàE Ó€ëàˆâ ½¢¢ajJ =ájœÖ€=ä(%pä(+cKà@úÕ€@è $$( @ãÅ¥±LàB\ BàÁÆü$àB±M B@JàB@ÙãC!Kÿÿô@x@¿AAJNàE§ŽBbµàËá J££e±Oà=®¤FñP =@®€Á£”€{áˆ% áˆ"óQà@ã€@ü y$F @`Û E±+câ ó屿"AëRàbC báᨠbá륱S B@`æÀB¨â ó!¨å±@x@ßA$A¨TàE-JC`Ìüy¤¥bóU =ájV D`=ä^"´Và@¸€@â m!jå±WàJ$ÀJ þâ¸å±X B@aRàBå±@x@ÇA ARYàE;Á Ó@Y£Aâ û¦¦aRZ =áRßÈ€=áR"½[à@?Ä€@ñ S€e¨ôp8€@ @¸ÀÛB½\àb«Ã€bàá½å±] B@jÀBöÇ@x@ßA$Aj^àEÁ|Ebàˆá j§§aj_ =ájR€€=áj%-ä(%±`à@¬€@ã Åå±aàBàBå±b B@JÀBüy@x@¿AAJcàEH8FáJ¨¨e±dà=ɤFñe =áˆ4>€{å"±fà@9€@ÿ ÿB!ˆ%N`Û E±4 â óå±çAëgàbäá Âó!¨ bå ±h B@a¨ÀBëc@x@ßA$A¨iàEÏó€ëöÇ©ªbój =ájû³Ga áj%pâó%±kà@^²€@å,±làJʱ JA#¥(ÁRå±m B@aRàBå±@x@ÇA ARnàEÜjHaàˆä F««aRo =áR‘r€=äFáR%±pà@Üm€@î! @` ¢½c+_€E­Q@À[&B½qàbH bàáÁjå±r B@jÀBè oëc@x@ßA$AjsàEc&I`Ìàˆá j¬¬ajt =ájï)€=â½å ±uà@J @å$±vàB«(€ƒàÁÁJ$ ƒäŸÿÞw B@JÀBñ@x@¿AAJxàEêáà ˆá J­­e±yà=kô„Få±z =áˆîç€{áˆ% äF"ó{à@'ã€@«î W)ø @b& Bó+câ óå±ÿÔBó|àb†â€bᨠb᨜y} B@a¨ÀB°â ó"ôëc@x@ßA$A¨~àEqJ`Ìå±®¯bó =áj•]K`=áj$ájK€à@÷[€@å,±àJcÀJ 层 B@aRàBz9>àB’h @x@ÇA ARƒàE~Laàˆâ û°!º8„ =áR;€=å"±…à@Ž€@ñ s6» äÿñ‹Ññ–ôp€à¡B½†àbú€bàáÄf屇 B@jàBâ ½å±@x@ß„ =Ajˆ @`EÐà ˆá j±±aj‰ =ájÓ€=â½%-â½+cŠà@èÒ€@å$±‹àBM Bá J屌 B@aàB@Ùâø!Kñ@x@¿AAJàE‹‹Mbµüy²²e±Žà= ž„Fvüy =@®€Áˆ‘€{áˆ% áˆ%±à@ÍŒ€@ÿÿôâ óå±ÿ­‘àb+ bᨠ£å ±’ B@`æÀB0­á ¨å±@x@ßA$A¨“àEGNa¨àˆâ ó³³bó”áj²K€=ä^%±•à@J€@â m'%òÿÿ5n–àJ~INàÉÄFëc— B@aRàBÿÿ @x@ÇA AR˜àE™Oàˆá R´´aR™ =â½G €=â½!⽚à@¡€@ü yƒ ñz…®´ÿu{lua dÉD©›àb  bâ4Áj屜 B@jÀBüy@x@ßA$AjàE ¾€ëå2á jµµajž =áj¨Á€=ájå ±Ÿà@ @á /Œ @ãÅ"üAJ àBdÀ€ƒà B屡 B@JÀBëc@x@¿AAJ¢àE¦yPâµ¶¶e±£à=®¤Fÿÿ/¼¤ =ማ€{äF% âó¥à@áz€@î W`@`Û «câ óå±À;A¨¦àbB bᨠ£áë«c§ B@a¨ÀB<ç Zå±@x@ßA$A¨¨àE-5Q`ÌöÇ·¸bó© =ájPõ€=ájä^"´ªà@°ó€@å,±«àJÀJ!á屬 B@aRàBå±@x@ÇA AR­àE;¬RaRäF¹¹aR® =áRݳ€=äFáR(o¯à@?¯€@ÿ ÿ “|®{¯|"ƒ\àƒ4ða lZB½°àb«®€bàáå±± B@jÀBÿÿ5n@x@ßA$Aj²àEÁgSajáóå ±ººaj³ =ájIk€=áj%pä(+c´à@¦j€@à~Áå±µàBàBå±¶ B@JÀBå±@x@¿AAJ·àEH#TáJ»»e±¸à=ɤF}ëc¹ =áˆ4)€{áˆ% áˆ"óºà@ƒ$€@îW @`Û E±+câ óå±ȨAë»àbäá Áë!¨ bå ±¼ B@a¨ÀBöÇ@x@ßA$A¨½àEÏÞ€ëâtâ ó¼¼bó¾ =ájyã€=å"±¿à@Ûá€@å,±ÀàJGÀJ îå±Á B@aRàBöÇ@x@ÇA ARÂàEUšUbàˆá R½½aRà =áR¢€=áR"½Äà@^€@ë c£làd?pñ_€ n a Ý#B½ÅàbÊœ€bå$±Æ B@jÀBƒè oñ@x@ßA$AjÇàEÜUVáj¾¾ajÈ =ájdY€=ä(å ±Éà@¿X€@á å±ÊàB  Bâ,ä ƒäèFË B@JÀBå±@x@¿AAJÌàEcWaJñ¿¿e±Íà=k߃Çÿÿ,Î = ÀÁ[€{áˆ% å±Ïà@¤€@ë câ óå±yÑBóÐàb bᨠ£á¨–ÇÑ B@`æÀBëc@x@ßA$A¨ÒàEêÌ€ëöÇÀÁbóÓ =ájXaæä^+cÔà@x‹€@å,±ÕàJ䊠J å±Ö B@aRàBöÇ@x@ÇA AR×àE÷CYaàˆå ±ÂÂaRØ =áRŒK€=áR%±Ùà@ÛF€@ÿÿ$X ³?p ñWÑhÑ0€ÑÑa òB½ÚàbG bàáÄfñÛ B@jÀBŒå±@x@ßA$AjÜàE~ÿ€ëàˆá jÃÃajÝ =ájZa¨ä(%-ä(+cÞà@a€@å$±ßàB€BàÁÁJ)º ƒä…±à B@JÀBå±@x@¿AAJáàE»€ˆàËá JÄÄe±âà=†Í„Füyã =áˆÁ€{áˆ% áˆ"óäà@B¼€@ÿ ÿ* ': @aè «câ óå±½Aëåàb¡á ᨠbᨅ±æ B@a¨ÀBÿÿÞ@x@ßA$A¨çàE‹v[`ÌñÅÆbóè =áj°6\`=ájïÂ%±éà@5€@å,±êàJ~4 JA#å±ë B@aRàBå±@x@ÇA ARìàE™íà ˆâ ûÇÇaRí =áRJõ€=å"±îà@¥ð€@ë c ÃhÁ0ƒ3tð àÿÓÿÿt]zB½ïàb bá jå±ð B@jÀBÿÿô@x@ßA$AjñàE©]bàˆá jÈÈ]äò =áj¸¬€=áj%-ä(%±óà@ @äåÃÅ$( @ãÅ"ü@BôàBt«€ƒà Bå±õ B@JÀB‹@Ùâøüy@x@¿AAJöàE¦d^aJëcÉÉe±÷à='w„F{ñø =@®€Ážj€{äF% áˆ%±ùà@èe€@ÿ ÿÞö Çâ óå±ùˆA¨úàbF bâjÁë!¨ £á륱û B@`æÀB è ¥!¨öÇ@x@ßA$A¨üàE- _a¨ëcÊËbóý =ájBà€=ájå ±þà@¤Þ€@å,±ÿàJÀJ þâ¸ñaK aRàBå±@x@ÇA AR@> @:—`aRàˆä FÌÌaR 6áRðž€=äFáR"½à@Jš€@ÿ ÿ Ó_P1£àþïn`6Ê3àÑt@ËÀ: @ÁRaájÍÍaj 6ájIV€=â½%pä(%±à@¥U€@å nå± àB Bá J$ ƒä‹c B@@àBëc@x@¿AAJ @> @HbáJÎÎe± @6 @ɤFå± 6áˆ4€{áˆ% áˆ"óà@‚€@—ÿ ÿÞ!ˆ @`Û «câ ó属ÎAëàbäá Âó!¨ bᨋc B@@6Ç BöÇ@x@ßA$A¨@> @6ÇÉ€ëå±ÏÐbó 6ájø‰ca ájä^(fà@Yˆ€@å,±àJŇ J@Üèjå± B@@àBüy@x@ÇA AR@> @5\@daàˆå ±ÑÑaR 6áRH€=áR"½à@äC€@ü yãN`ƒ5Ñ‚ÀÀx\|ÿÿŽzB½àbPÀb@ôájå± B@@àByè oöÇ@x@ßA$Aj@> @cü ë@ ˆá jÒÒaj 6ájïÿ€=áj%-ä(%±à@J @à~ÃÅ$( @ãÅ"ü@BàB«þ€ƒà B$àB¥± B@@àBå±@x@¿AAJ @> @é·eâµÓÓe±!@6 @kÊ„Fwëc" 6áˆÒ½€{è¥% áˆ"ó#à@$¹€@“å ±ë câ óå±ÞìA¨$àb…¸€bᨠ£á륱% B@@ÀBÿÿ* @x@ßA$A¨&@> @psfá¨ÔÔbó' 6ájx€=ájå ±(à@xv€@å,±)àJäu J@Ü£½Ã>å±* B@@àBÿÿQè@x@ÇA AR+@> @÷.gáRÕÕaR, 6áRš6€=áR"½-à@û1€@å ± ó}ÐÐÑaÑrÑQà@ËÀÍho.àbgÀb@%ájå±/ B@@àBÿÿ,@x@ßA$Aj0@> @6Çê€ëêäå ±ÖÖaj1 6ájî€=å"±2à@kí€@á å±3àBÊìàBå±4 B@@àBå±@x@¿AAJ5@> @¦hâµ××e±6@6 @ tƒ|÷ÿÿ; 7 6áˆñ«€{âó% å±8à@@§€@ÿ ÿ,â óå±inW9àb á Ä^!¨ £å ±: B@@ÀBëc@x@ßA$A¨;@> @‹aiá¨ØØbó< 6 >  :f€=ájå ±= 9à@›d€@å,±>àJÀJ ™äFå±? B@@àBñ@x@ÇA AR@@> @jáRÙÙaRA 6á·$€=áR%±Bà@ €@ñ ð€Å|ÑtËk@ËÀ)SHoCàb‚€bá jå±D B@@àB|ëc@x@ßA$AjE@> @˜Øà ˆå ±ÚÚajF 6áj!Ü€=áj*ßä(+cGà@|Û€@æ0ÃÅå±HàBÝÚàB$ ƒä‘I B@@àBöÇ@x@¿AAJJ@> @”kâµÛÛe±K@6 @'bƒÇôå±L 6ሚ€{ç:% áˆ"óMà@U•€@ÿ më câ ó屡AëNàb»á Áë!¨ £á¨XFO B@@ÀBëc@x@ßA$A¨P@> @¦Ola¨üyÜÝbóQ 6ájÏm`=ájå ±Rà@1€@å,±SàJ  J@Üéøå±T B@@àBå±@x@ÇA ARU@> @³Æà ˆä FÞÞaRV 6áRY΀=äFáR"½Wà@¼É€@‡î !$F @a² “Ó Ñ Ëk@ÀÓÿµxXàb' bá jå±Y B@@àBñ@x@ßA$AjZ@> @82‚n`Ìû.ßßaj[ 6ájÊ…€=â½%-ä(%±\à@& @å$±]àB†„€ƒá J$ ƒä…±^ B@@àBëc@x@¿AAJ_@> @Á=oáJààe±`@6 @BP„Fña 6ርC€{áˆ% áˆ"óbà@ÿ>€@ñ "ó @`Û âóå±w´Aëcàba bᨠbᨅ±d B@@ÀBå±@x@ßA$A¨e@> @Hù€ëöÇáâbóf 6ájy¹pa áj$áj3Êgà@Ú·€@å,±hàJFÀJ@Üå±i B@@àBöÇ@x@ÇA ARj@> @Upqaàˆå ±ããaRk 6áRÿw€=å"±là@Ys€@ÿÿ, #q là´Ñ‘@ËÀÎûB½màbÅr€bàáÈoå±n B@@àBüy@x@ßA$Ajo@> @82+rájääajp 6ájT/€=â½%-â½%±qà@°.€@å$±ràB Bá Jå±s B@@àB@Ùâø8ÿÿ¦@x@¿AAJt@> @bç€Ëàˆâ µååe±u@6 @ä¤FöÇv 6@®  Sí€{áˆ% áˆ%±w 9à@è€@ÿÿ,â óå±j Aëxàbÿ¡á ë!¨ £å ±y B@@ÀBœî W!¨ÿÿ,@x@ßA$A¨z@> @é¢sbóëcæçbó{ 6á)ct`=ájå ±|à@pa€@â m,Î @ÿÿ¦}àJÜ` J@Üâ¸ëc~ B@@àBå±@x@ÇA AR@> @6Çuaàˆâ ûèèaR€ 6áR¬!€=áR"½à@€@ñ 3€ nE°ñhqB"ÿÿQè}B½‚àbs€bàá屃 B@@àBå±@x@ßA$Aj„@> @}Õà ˆá jééaj… 6ájÙ€=çå ±†à@bØ€@ã Å)Ú @ãÅ(®AJ‡àBÁ×€Bá J$àB«cˆ B@@àB@Ùâø$å±@x@¿AAJ‰@> @‘vâµêêe±Š@6 @…£„F|ëc‹ 6@®  ô–€{âó% 屌 9à@?’€@— Oá!ˆ @`Û «câ óå±¢A¨àb á Âó!¨ báë«cŽ B@@6Ç Bå±@x@ßA$A¨@> @‹Lw`Ìëcëìbó 6 >  ½ x`=âó%páj"´‘ 9à@ €@â må±’àJŠ  J ™áµ `‹áR-Ž_“ B@@àBñ@x@ÇA AR”@> @5\Ãà ˆä FííaR• 6áNË€=â½áR%±–à@¨Æ€@ë cC`7€6ïÔ6@ïÓ@ËÀÑÿx—àb bá jëc˜ B@@àBÿÿ$X@x@ßA$Aj™@> @ybàˆá jîîajš 6áj›‚€=áj%-â½+c›à@÷€@à~ÃÅ$( @âr"ü@BœàBWàB àB¥± B@@àBñ@x@¿AAJž@> @¦:záJïïe±Ÿ@6 @'M„Få±  6ሒ@€{áˆ% áˆ"ó¡à@Ý;€@ë câ óå±uÿx¢àbB bá ë!¨ £á륱£ B@@ÀBüy@x@ßA$A¨¤@> @,ö€ëëcðñbó¥ 6áj_¶{aæå"±¦à@¿´€@å,±§àJ+ÀJ@Üçg屨 B@@àBëc@x@ÇA AR©@> @5\m|aàˆä FòòaRª 6áRüt€=â½å ±«à@Np€@ù …$F @` ¨oS”9P~0¯~ñÛú @ÀÚDf¬àbºo€bàáĩ屭 B@@àBŠî !ÿÿÞ@x@ßA$Aj®@> @Á(}`Ìêäá jóóaj¯ 6áj=,€=áj%pä(%±°à@™+€@á !j @ãÅ¥±±àBù*àB$àB¥±² B@@àBå±@x@¿AAJ³@> @Gäà ˆá Jôôe±´@6 @ȤFÿÿ$Xµ 6áˆDê€{äF% áˆ%±¶à@‚å€@ÿ ÿ@Ò!ˆ @b& âóå±»(Aë·àbãá Âó!¨ bá륱¸ B@@ÀBÿÿô@x@ßA$A¨¹@> @Ο~`Ìâtá ¨õõbóº 6ájd¤€=áj$@j @3kc»à@Æ¢€@å,±¼àJ2ÀJ@nå±½ B@@àBÿÿ$X@x@ÇA AR¾@> @6Ç[áRööaR¿ 6A  c€=áR"½À 9à@M^€@ÿ ÿ!d"½!“` ¢½ cüýÂýî`Ò¯"vÍ?@À•ÿÏ4Áàb¹]€bá jå±Â B@@àBå±@x@ßA$AjÃ@> @6Ç€`Ìå±÷÷ajÄ 6á)d€=å"±Åà@¾€@à~ÃÅ!jån¥±ÆàB àB$àB¥±Ç B@@àBå±@x@¿AAJÈ@> @bÒ€ËýÄøøe±É@6 @j ƒÇÿÿ$XÊ 6 >  bØ€{áˆ% äF"óË 9à@ŸÓ€@¨ñ /ª @b& Bó6Çâ óå±14E±Ìàb bâ ó!¨ bå ±Í B@@ÀBå±@x@ßA$A¨Î@> @6Ç`̼ ¤çå± ùúbóÏ 6á)N‚`=ä^"´Ðà@tL€@å,±ÑàJàK J ®ÀÉÃ>ëcÒ B@@àBå±@x@ÇA ARÓ@> @öƒaâûûûaRÔ 6 >  ¿ €=áR"½Õ 9à@€@ë csÿÿ*´? äñWüy¨bB½Öàb{€bä ©å±× B@@àB‹å±@x@ßA$AjØ@> @6ÇÀ€ˆájüüajÙ 6á)Ä€=ä(%-ä(+cÚà@`À@á å±ÛàBÁ€Bâ µ$ ƒä“ËÜ B@@àBå±@x@¿AAJÝ@> @|„âµýýe±Þ@6 @‰Ž„Fÿÿ$Xß 6 >  ‚€{áˆ% áˆ"óà 9à@E}€@ÿÿ$Xâ óå± Aëáàb¤¡á¨ £á¨Ÿÿ,â B@@ÀBå±@x@ßA$A¨ã@> @‹7…a¨å±þÿbóä 6á)¯÷€=áj)Ïáj%±åà@ö€@å,±æàJ}õ J@ܦ{å±ç B@@àBzF¥àBÿÿ@Ò@x@ÇA ARè@> @6Ç®†aRàˆçA@ Aé 6@  I¶€=å"±ê 9à@ ±€@è o$F!“` Ho"½ƒ‘€ÑñQÑ‘ÿÿ¡¤a–B½ëàb  bàáÁjå±ì B@@àB„å±@x@ßA$Ají@> @6Çj‡`Ìàˆá jajî 6 >  §m€=â½%-â½%±ï 9à@ @å$±ðàBcl€ƒàÁÁJå±ñ B@@àBå±@x@¿AAJò@> @¦%ˆaJàËá J@=ó@6 @+8ƒ|vüyô 6áGš+€{áˆ% áˆ%±õà@ã&€@ö Çâ óå±ÅdAëöàbB báᨠ£å ±÷ B@@ÀB â ó4  @'@x@ßA$Ajø@ @6Çá€ëöÇbóù 6ájQ¡‰aæáj$áj%±úà@³Ÿ€@å,±ûàJÀJ@Üånëcü B@@àBå±@x@ÇA ARý@> @6ÇXŠaàˆâ ûaRþ 6áRß_€=å"±ÿà@:[€@ÿ ÿ,“Ñÿÿ “@ËÀ&QB½à£¦Z€bàá½å± B@ ¬ îÀBÿÿ* @x@ßA$AjàEÀ‹áj G =ájM€=ájå ±à@¨€@ã Å' @ãÅ"üAJàB àBå± B@JÀBÿÿB@x@¿AAJàEGÏ€ËáÓâ µe±à=ȤFÿÿ$X =áˆ7Õ€{äF% äF%± à@…Ѐ@—è ¥ @b& ¨¥â ó层£A¨ àbãá Âó!¨ báë«c B@a¨ÀBÿÿ,@x@ßA$A¨ àEΊŒ`Ìá¨bó =ájz€=âóå ±à@Ú€@å,±àJFÀJA#å± B@aRàBüy@x@ÇA ARàEUFaRãÇâ û  aR =áRóM€=â½áR"½à@MI€@ë c £ÑaÑ ñK_E½a Òÿ œàb¹H€bá jå± B@jÀBÿÿ,@x@ßA$AjàEÛŽajàˆá j  aj =ájc€=áj%pä(+cà@À€@à~ÃÅå±àBàB$ ƒä‘ B@JÀBëc@x@¿AAJàEb½à Ëá J  e±à=j‹ƒÇöÿÿô = ÀÁVÀ{áˆ% áˆ"óà@¾€@ù »ö Çâ óå±vÿ„ àbþá Áë!¨ £á¨‘! B@`æÀBëc@x@ßA$A¨"àEéxbóÿÿÞ  bó# =áj 9`=å"±$à@€7€@å,±%àJì6€Jå(ÁRëc& B@aRàBå±@x@ÇA AR'àEöãÇâ ûaR( = …€ƒ™÷€=â½å ±)à@úò€@ÿÿ$X ³¿ÿ*ßÿóÑ Œàãÿ$X*àbfÀb@&çå±+ B@€æÀBÿÿ]L@x@ßA$Aj,àE}«‘bàˆá jaj- =ájý®€=áj%-ä(%±.à@Z @à~Ârå±/àB¹­€ƒà B$ ƒä…±0 B@JÀBå±@x@¿AAJ1àEg’áJ >2à=…y„Fÿÿ,3 =áˆìl€{áˆ% áˆ%±4à@?h€@ÿÿ$Xâ óå±Gÿ$X5àb á Áë!¨ £á¨…±6 B@a¨ÀBÿÿô@x@ßA$Aj7àEŠ"“a¨ñbó8 = …`ƒ®â€=å"±9à@ á€@å,±:àJyà J äFå±; B@€ÎÀBÿÿ* @x@ÇA AR<àE˜™”aRàˆä FaR= =áR?¡€=áR"½>à@˜œ€@ë cà ë_E½a sD©?àb bàá½å±@ B@jÀBëc@x@ßA$AjAàEU•ajç¥á jajB =áj“X€=ä(%-ä(%±Cà@ðW€@á å±DàBOàB$ ƒä…±E B@JÀBå±@x@¿AAJFàE¥–aJàËá Je±Gà=*#ƒ|yöÇH =ሚ€{âó% áˆ"óIà@á€@ë câ óå±¢AëJàbA báÂó!¨ £á¨…±K B@a¨ÀBëc@x@ßA$A¨LàE,Ì€ëàˆá ¨bóM =ájÛЀ=ájê3ÊNà@<Ï€@å,±OàJ¨Î J éøå±P B@aRàBöÇ@x@ÇA ARQàE³‡—bûÿÿÁaRR =áRP€=áR"½Sà@«Š€@å ±Ó\ë_ |ña ºB½Tàb bá jå±U B@jÀBöÇ@x@ßA$AjVàE:C˜ajå±ajW =áj¾F€=áj%-ä(%±Xà@ @à~ÃÅå±YàBzE€ƒà B$ ƒä…±Z B@JÀBöÇ@x@¿AAJ[àEÀþ€ˆáÓä e±\à=ȤFôñ] = ÀÁ´™aÆç:% áˆ"ó^à@þÿ€¼ü y!ˆ @`_ ¶Çâ óå±Aë_àb\ báÁë!¨ bᨅ±` B@`æÀBå±@x@ßA$A¨aàEGº,ëcbób =ájpzša ájå ±cà@Òx€@å,±dàJ>ÀJ å±e B@aRàBå±@x@ÇA ARfàET1›aàˆâ ûaRg =áR9€=äFáR"½hà@]4€@ü yãÖ¿K_E½a úvB½iàbÈ3€bàá½å±j B@jÀBÿÿ¦@x@ßA$AjkàEÛìà ˆá jajl =ájWð€=â½%-ä(%±mà@³ï€@å n$( @ãÅ(®@BnàB Bá J$àB¥±o B@JÀBŒ@Ùâøÿÿ¦@x@¿AAJpàEb¨œâµe±qà=ã¤Füyr =@®€ÁN®€{áˆ% áˆ"ósà@œ©€@—ü yö Çâ óå±^„A¨tàbþá Âó!¨ £á륱u B@`æÀBÿÿô@x@ßA$A¨vàEéca¨öÇ bów =áj$ž`=ájä^"´xà@s"€@å,±yàJß! JA#â¸ëcz B@aRàBöÇ@x@ÇA AR{àEöÚà ˆä F!!aR| =áR¬â€=å"±}à@Þ€@å ±óßÿ; ÿAa j†B½~àbrÝ€bá jå± B@jÀBå±@x@ßA$Aj€àE}–Ÿbàˆá j""aj =ájš€=â½%-ä(%±‚à@a™€@å$±ƒàBŘ€BàÁä ƒä‹c„ B@JàB@Ùâøå±@x@¿AAJ…àER áJ##e±†à=‰d„Fsñ‡ =@®€ÁôW€{áˆáˆ%±ˆà@>S€@å ±': @`Û «câ óå±ÎtAë‰àb ¡á ë届 B@`æÀBÿÿ,@x@ßA$A¨‹àEŠ ¡`Ìëc$%bóŒ = …`ƒ¬Í€=ájå ±à@ Ì€@â m' @ÿÿ* ŽàJyË J â¸å± B@€ÎÀBå±@x@ÇA ARàE˜„¢aRàˆä F&&aR‘ =áREŒ€=áR"½’à@ ‡€@ñ ÿ/±ÿÿ*a ¡@B½“àb  bàáÄfå±” B@jÀBå±@x@ßA$Aj•àE@£áj''aj– = …€ƒŸC€=ç%-å±—à@þB€@á $( @ån(®AJ˜àB^àB$àB¥±™ B@€ÆàB@Ùå±@x@¿AAJšàE¥ûà Ëâ µ((e±›à=&ƒ||屜 =@®€Á¤aÆâó%páˆ"óà@àü€¼ÿ ÿnb$F @`_ E±Ãâ óå±²A¨žàbA bá ë!¨ báë«cŸ B@`æÀBÿÿ›ò@x@ßA$A¨ àE,·,ëc)*bó¡ = ƒ^w¥a âó%páj"´¢à@¿u€@â m!j @å±£àJ+ÀJ â¸å±¤ B@aRàBñ@x@ÇA AR¥àE9.¦aàˆâ û++aR¦ =áR×5€=â½!áR"½§à@91€@ÿÿ, hqB"ƒ\ïð®¯}ÿÿh°J‡B½¨àb¥0 bA;ä#屩 B@jÀBÿÿF„@x@ßA$AjªàEÀéà ˆá j,,aj« =ájPí€=áj%-áj+c¬à@¬ì€@á å±­àB àB$ ƒä‹c® B@JÀBñ@x@¿AAJ¯àEG¥§âµ--e±°à=ȤFå±± =áˆ+«€{áˆ% áˆ"ó²à@}¦€@ÿÿ,â óå±îuAë³àbãá È¥!¨ £á¨–Ç´ B@a¨ÀBÿ ÿ|º- ßÿB@x@ßA$A¨µàEÍ`¨a¨ëc./bó¶ =ájì ©`=å"±·à@L€@å,±¸àJ¸ J þäFå±¹ B@aRàBëc@x@ÇA ARºàEÛ×à ˆä F00aR» =áR‰ß€=â½å ±¼à@ãÚ€@ë c#ƒ9Q/ƒ>tð£tƒð£a ŸKB½½àbO bá jå±¾ B@jÀBÿÿ,@x@ßA$Aj¿àEb“ªbàˆá j11ajÀ =ájÚ–€=áj%-ä(%±Áà@6 @à~ÃÅå±ÂàB–•€ƒà B$ ƒä…±Ã B@JÀBå±@x@¿AAJÄàEèN«áJ22e±Åà=ja„F屯 =áˆÝT€{äF% áˆ%±Çà@#P€@ÿÿ,â óå±;¹AëÈàb„O€bᨠ£á¨…±É B@a¨ÀBüy@x@ßA$A¨ÊàEo ¬a¨å±34bóË =áj¢Ê€=ájä^+cÌà@É€@å,±ÍàJnÈ J éøå±Î B@aRàBå±@x@ÇA ARÏàE}­aRòñä F55aRÐ =áR/‰€=áR"½Ñà@…„€@èo!“` N!(o3t\ðƒ9àþ£àýƒ<îð@Àû«B½Òàbñƒ€bàáÄ©å±Ó B@jÀB…ÿ ÿ²ÿÿ/¼@x@ßA$AjÔàE=®`Ìáóá j66ajÕ =ájƒ@€=å"±Öà@à?€@à~Áå±×àBCàB屨 B@JCÀB âø!Küy@x@¿AAJÙàEŠøà Ëá J77e±Úà= ƒ|xñÛ =@®€Á‚þ€{áˆ% å±Üà@Åù€@ ü y!ˆ @b& âóå±_YBóÝàb& bá ë!¨ bå ±Þ B@`æÀBñ@x@ßA$A¨ßàE´¯`Ìÿÿ‹88bóà =áj«¸€=ä^%±áà@ ·€@â mñâàJy¶ J â¸å±ã B@aRàBÿÿ* @x@ÇA ARäàE—o°aRàˆâ û99aRå =áRDw€=áR%±æà@œr€@ë cC£íðÃî”l@KÓƒ:à”ÿà·‘B½çàb bå$±è B@jÀBÿÿt@x@ßA$AjéàE+±áj::ajê =áj¢.€=ä(*ßä(+cëà@ý-€@á $( @ãÅ"ü@BìàB^ Bˆ âµ$àB«cí B@JÀBëc@x@¿AAJîàE¥æ€Ëàˆâ µ;;e±ïà=±¤FïÿÿÞð =ሙì€{áˆ% áˆ"óñà@âç€@ÿ ÿÞ$F @b& E±Ãâ óå±cA¨òàbA báÇ!¨ báë«có B@a¨ÀB è ¥'Zñ@x@ßA$A¨ôàE,¢²`ÌöÇ<=bóõ =áj\b³`=áj)Ïáj"´öà@¾`€@å,±÷àJ*ÀJ þâûå±ø B@aRàBöÇ@x@ÇA ARùàE9´aç>>aRú =áRò €=å"±ûà@I€@å ±SK` ”sP<0¯üyÓ-B½üàbµ€bá jëcý B@jÀBÿÿ* @x@ßA$AjþàEÀÔ€ˆáj??ajÿ =ájLØ€=â½%-â½%±à@§×€@å$±àB  Bá J$ ƒäÿyÆ B@JàB@Ùèª$ëc@x@¿AAJàEGµbµàËå ±@@e±à=ȤFzëc =@®€Á[–€{áˆ% áˆ%±à@ ‘€@ü y': @`Û E±.Wâ óå±wÿ­àbÿá å± B@`æÀBëc@x 9A$A¨ @€EÍK¶`ÌñABbó = ?`=ò ·`=ájå ± à@T €@â m' @üy àJÀ  J â¸å± B@€ÎÀBå±@x@ÇA ARàEÛ€ˆäFCCaR = …€ƒœÊ€=å"±à@óÅ€@å ±cÛƒ;à`&6Éàû£ñs© Œà&ÿ5nàb_ bá jå± B@€æÀB„è oñ@x@ßA$AjàEa~¸bàˆä fDDaj =ájâ€=áj%-ä(%±à@A @äåÃÅ$( @ån(®@BàB¢€€ƒà Bå± B@JàB@Ùå±@x@¿AAJàEè9¹aJâµEEe±à=iL„F{å± =@®€Áà?€{äF%páˆ%±à@*;€@ë câ ó层ÿ5nàbˆ:€bâjÁë!¨ £áë«c B@`æÀBëc@x@ßA$A¨àEoõ€ˆëcFGbó = ƒ”µºaæáj%páj"´ à@ö³€@å,±!àJbÀJ â¸å±" B@aRàBå±@x@ÇA AR#àE|l»aàˆä FHHaR$ =áR1t€=áR"½%à@ˆo€@ö Çsðú…þ6Âñs…ð‚õƒàÞšDf&àbôn€bàá½å±' B@jÀBöÇ@x@ßA$Aj(àE(¼ájIIaj) =ájw+€=áj%pâ½%±*à@Ò*€@á å±+àB3àB$ ƒä‹c, B@JÀBñ@x@¿AAJ-àEŠãà Ëâ µJJe±.à= ö„Få±/ =áˆzé€{áˆ% áˆ"ó0à@Àä€@ë câ óå±zhAë1àb& bá ë!¨ £á¨œy2 B@a¨ÀBÿÿ @x@ßA$A¨3àEŸ½bóå±KLbó4 = …`ƒ2_¾`=è¥å ±5à@“]€@å,±6àJÿ\ J å ±7 B@€ÎÀBå±@x@ÇA AR8àE¿aàˆâ ûMMaR9 =áR×€=â½áR"½:à@*€@ÿÿ@Ò ƒîð ü =@€ƒÕ€=áj%-ä(%±?à@tÔ€@á å±@àBÕÓàB$ ƒä…±A B@€ÆÀBå±@x@¿AAJBàE+ÀâµOOe±Cà=­¤Få±D =ሠ“€{áˆ% áˆ"óEà@eŽ€@ü y/ª @`Û Q6Çâ óå±_ÿ5nFàbÇá Âó!¨ bᨅ±G B@a¨ÀB¡â ó.XöÇ@x@ßA$A¨HàE²HÁ`ÌñPQbóI = ``ƒàÂ`=å"±Jà@A€@å,±KàJ­ J å ±L B@€ÎÀBå±@x@ÇA ARMàEÀ¿€ˆîèä FRRaRN = …€ƒiÇ€=â½å ±Oà@Ä€@ñ “ŸÿL1ƒ,ïðlà´ÿÿL6µÿPàb0 bá jëcQ B@€æÀBÿÿB@x@ßA$AjRàEF{Ãbàˆá jSSajS =ájÆ~€=áj%-ä(%±Tà@" @à~ÃÅå±UàB†}€ƒà B$ ƒä…±V B@JàB@Ùî\$öÇ@x@¿AAJWàEÍ6ÄáJTTe±Xà=NI„Fÿÿ,Y =@®€Á½<€{áˆ% áˆ%±Zà@8€@ÿ ÿÞ!ˆ @`Û E±(¥â ó屆Aë[àbi7€bᨠbᨅ±\ B@`æÀBëc@x@ßA$A¨]àETòà ˆâ óUUbó^ =ájõö€=å"±_à@Xõ€@â m'$ŸöÇ`àJÄô J â¸ña B@aRàBÿÿÚ˜@x@ÇA ARbàEÛ­Åbå±VVaRc =áR†µ€=áR"½dà@×°€@ë c£ÿÿL6Õÿq eàbC bàáÄ©å±f B@jÀBÿÿWš@x@ßA$AjgàEaiÆajáóâ ½WWajh =ájÝl€=ä(%-ä(%±ià@; @á $( @ãÅ4@BjàB™k€ƒàÁÁJå±k B@JÀBëc@x@¿AAJlàEè$ÇáJXXe±mà=ð¤Fÿÿ; n =áˆä*€{áˆ% áˆ"óoà@$&€@ÿ ÿWš$F @`Û E±+câ óå±qSA¨pàb„%€bᨠbá륱q B@a¨ÀB¨ëc@x@ßA$A¨ràEoà€ˆñYZbós =áj‹ Èa ä^"´tà@@‡@Såf!jå±uàJZÀJA#àJå±v B@aRàBå±@x@ÇA ARwàE|WÉaáÛä F[[aRx =@þ€ƒ(_€=áR"½yà@„Z€@ÿÿ$X ³ÿÿL6ƒ0tÿÿ¡¤÷íB½zàbðY€bàáÄfå±{ B@€æÀBâ ½üy@x@ßA$Aj|àEÊáj\\aj} =ájs€=ä(%pä(%±~à@Ñ€@â rå±àB7 Bá Jå±€ B@JÀB„@Ùâø!Këc@x@¿AAJàEŠÎà Ëâ µ]]e±‚à= á„Fuüyƒ =@®€ÁzÔ€{áˆ% áˆ"ó„à@ÆÏ€@—ÿÿ â óå±SAë…àb& bá ë!¨ £å ±† B@`æÀBëc@x@ßA$A¨‡àEŠËâó^^bóˆ =áj¯Ž€=áj%páj%±‰à@€@ˆ@Sâm届àJ|Œ JA#àJ屋 B@aRàBÿÿB@x@Lj =ARŒ @`E—EÌaRÿu__aR =@þ`=PM€=áR"½Žà@£H€@ë c Ã0àÿÓ”_P91àþ$`Ì èB½àb bá jå± B@€æÀBöÇ@x@ßA$Aj‘àEÍajàˆä f``aj’ =áj®€=ájå ±“à@  @ã Åå±”àBj€ƒàÁÆü$ ƒä‘• B@JÀBëc@x@¿AAJ–àE¥¼€ˆàËá Jaae±—à=¬¤Fõÿÿ ˜ =ሀ{áˆ% äF"ó™à@ß½€@£å ±$F @b& «câ óå±¥BóšàbA báᨠbᨑ› B@a¨ÀBÿÿÞ@x@ßA$A¨œàE+xÎ`ÌöÇbcbó =ájY8Ï`=ê%±žà@º6€@å,±ŸàJ&ÀJA#å±  B@aRàBöÇ@x@ÇA AR¡àE9ï Ó@Y ˆâ ûddaR¢ =áRêö€=â½ç"½£à@Eò€@ë cÓo`'î$/þ+aàn`ÿÿ,ƱB½¤àb±ñ€bàá½å±¥ B@jÀBöÇ@x@ßA$Aj¦àE¿ªÐbàˆá jeeaj§ =ájL®€=â½%-ä(+c¨à@§­€@à~Á屩àBàB屪 B@JÀBå±@x@¿AAJ«àEFfÑáJffe±¬à=ǤF|ëc­ =áˆ6l€{âó% áˆ"ó®à@g€@—å ±!ˆ @`Û E±.Wâ óå±bCAë¯àbâá Áë!¨ bå ±° B@a¨ÀBëc@x@ßA$A¨±àEÍ!Ò`Ìñghbó² =áj÷á€=âóä^%±³à@Xà€@å,±´àJÄß JA#¥(ÁRëcµ B@aRàBå±@x@ÇA AR¶àEÚ˜ÓaRàˆä FiiaR· =áRˆ €=â½áR"½¸à@⛀@å ±ã,à$?o`ñ¥kpée‚pa  :B½¹àbN bàáÁj屺 B@jÀBÿÿ$X@x@ßA$Aj»àEaTÔajìÎjjaj¼ =ájíW€=ájå ±½à@I @á $( @ãÅ"üAJ¾àB©V€ƒà B$àB«c¿ B@JÀBå±@x@¿AAJÀàEèÕaJàˆâ µkke±Áà=i"ƒ|€å±Â =áˆØ€{áˆ% å±Ãà@"€@î Wë câ ó屨ÈA¨Äàb„€báÂó!¨ £áë«cÅ B@a¨ÀBå±@x@ßA$A¨ÆàEnË€ˆëclmbóÇ =áj•‹Öaæå"±Èà@õ‰€@å,±ÉàJaÀJA#å±Ê B@aRàBñ@x@ÇA ARËàE|B×aàˆâ ûnnaRÌ =áR&J€=â½å ±Íà@„E€@å ± óêeƒ`ÑPÑ Kfÿÿ$Xòÿ²ÎàbðD€bàá½å±Ï B@jÀB~öÇ@x@ßA$AjÐàEþà ˆá jooajÑ =ájØa¨áj%-ä(+cÒà@ë€@á å±ÓàBKàB$ ƒä‘Ô B@JÀBå±@x@¿AAJÕàE‰¹à Ëá Jppe±Öà=Ì„Fÿÿ,× =áˆz¿€{äF% áˆ(¥Øà@ĺ€@å ±!ˆ @aè «câ óå±Vÿ!šÙàb% bá ë!¨ bá¨‘Ú B@a¨ÀBå±@x@ßA$A¨ÛàEuÙ`Ìå±qrbóÜ =áj+5Ú`=ájä^KÝà@‹3€@å,±ÞàJ÷2 JA#å±ß B@aRàBå±@x@ÇA ARààE쀈òñâ ûssaRá =áRÐó€=áR"½âà@.ï€@å± €½|ë`%¾à«qãàbšî€bá jå±ä B@jÀB}å±@x@ßA$AjåàE¤§Ûbàˆá jttajæ =áj0«€=å"±çà@ª€@á鯹$(ån"üEôèàBì©€BàÁÀB$àB¥±é B@JÀBå±@x@¿AAJêàE+cÜáJuue±ëà=¬¤Fñì =áˆi€{áˆ% å±íà@fd€@å ±ë câ óå±qîàbÇá ᨠ£å ±ï B@a¨ÀBå±@x@ßA$A¨ðàE²Ýa¨ñvwbóñ =ájÕÞ€=ä^"´òà@5Ý€@ÿ,ÿ,óàJ¡Ü JA#å±ô B@aRàBå±@x@ÇA ARõàE¿•ÞaRã„ä FxxaRö =áRr€=áR%±÷à@Ϙ€@å ±Ë`là´Ñßÿ Q`ÈÿNøàb; bàá½å±ù B@jÀBëc@x@ßA$AjúàEFQßájyyajû =ájÖT€=ä(%-ä(+cüà@3 @â rå±ýàB’S€ƒá Jå±þ B@JÀBå±@x@¿AAJÿàEÍ àáJzze±  €ƒNƒ|å± =ሽ€{áˆ% áˆ"óà@€@˜öÇâ óå±lÿ6àbi €bᨠ£å ± B@a$ÀBå±@x`ß =A¨ @€ESÈ€ˆëc{|bó =ájˆáaæä^%±à@Þ†€@ˆ@Såf'+¤Ä> ´Ã! $ … C>àJJÀJ àJöÇ B@a àBÿÿ]L@x@ÇA AR àEa?âaàˆå ±}}aR =@þ€ƒ G€=áR"½ à@iB€@ÿÿB #‘ÿÿF‚hqa »+Aj àbÕA€bàáå± B@€æÀBå±@x@ßA$AjàEèú€ˆÿÿêc~~aj =ájtþ€=ä(å ±à@Ñý€@å$±àB0àBå± B@JÀBå±@x@¿AAJàEn¶ãbµøe±à=ï¤FöÇ =áˆc¼€{âó% å±à@®·€@ÿÿ$Xâ óå±ÙBóàb bâ ó!¨ £å ± B@a¨ÀBå±@x@ßA$A¨àEõqäa¨å±€bó =áj2å`=âó%páj%±à@x0€@å,±àJä/ JA#å± B@aRàBå±@x@ÇA ARàEéà ˆå ±‚‚aR =áR¶ð€=â½áR%±!à@ì€@å ±3B"K^d?` n@àÿ÷"àb{ë€bä ©ëc# B@jÀBüy@x@ßA$Aj$àE‰¤æbáóá jƒƒaj% =áj¨€=â½å ±&à@w§€@á ëc'àBÕ¦€BàÁÆü$ ƒä–Ç( B@JÀBå±@x@¿AAJ)àE`çáJ„„e±*à=r„Fqå±+ =áˆf€{áˆ% äF"ó,à@La€@å ±!ˆ @`Û –Çâ óå±¼E±-àb¬á ᨠbᨖÇ. B@a¨ÀB ô ÿÿ¦@x@ßA$A¨/àE—è`Ìñ…†bó0 =ájªÛ€=ájä^%±1à@ Ú€@å,±2àJyÙ J þå ±3 B@aRàBÿÿ; @x@ÇA AR4àE¤’éaRàˆä F‡‡aR5 =áRLš€=å"±6à@¨•€@ñ C`ñzqBEµ´ÿ %´às)B½7àb bàá½å±8 B@jÀBÿÿB@x@ßA$Aj9àE+Nêájˆˆaj: =áj«Q€=â½%-ä(1;à@  @å$±<àBgP€ƒá Jå±= B@JÀBå±@x@¿AAJ>àE² ëaJàËâ µ‰‰e±?à=3ƒ|}å±@ =ሢ€{áˆ% áˆ%±Aà@î €@—ñ /ª @`Û E±+câ óå±×ÛAëBàbN báÂó!¨ bå ±C B@a¨ÀBëc@x@ßA$A¨DàE8Å€ëëcŠ‹bóE =áj`…ìa ájå ±Fà@À@å,±GàJ/ÀJ ëcH B@aRàBå±@x@ÇA ARIàEF<íaàˆâ ûŒŒaRJ = …€ƒõC€=áR"½Kà@J?€@ÿÿ/¼ S%¨Ñ‘e³? ÿÿQè*üB½Làb¶>€bàá½å±M B@€æÀB‡è oÿÿô@x@ßA$AjNàEÌ÷à ˆá jAO =ájQû€=çå ±Pà@®ú€@á )Ú @ãÅ(®AJQàB BäàB«cR B@JàB@Ùâø!Kÿÿô@x@¿AAJSàES³îⵎŽe±Tà=Ô¤Fÿÿ; U =@®€ÁG¹€{âó% å±Và@´€@ë câ ó屎A¨Wàbïá Âóå±X B@`æÀB â ó!¨ÿÿ* @x@ßA$A¨YàEÚnïa¨ëcbóZ =áj/ð`=âóå ±[à@e-€@å,±\àJÑ, J þâ¸öÇ] B@aRàBÿÿ¦@x@ÇA AR^àEçåà ˆä F‘‘aR_ =áRŸí€=â½áR%±`à@óè€@ù… @a² Ho%±cn+\?äÿ«g@À]ÿaàb_ bá jå±b B@jàBâ ½å±@x@ßA$AjcàEn¡ñ`Ìàˆá j’’ajd =ájò¤€=áj%-ä(+ceà@L @à~ÃÅå±fàB®£€ƒà B$ ƒåô¥±g B@JÀBëc@x@¿AAJhàEõ\òáJ““e±ià=vo„Få±j =áˆíb€{áˆå ±kà@1^€@¤ëcâ óå±ùÿèlàb‘]€bᨠ£á¨‘m B@a¨ÀBÿÿô@x@ßA$A¨nàE|óa¨å±”•bóo =áj¤Ø€=å"±pà@×€@å,±qàJrÖ JA#èjå±r B@aRàBëc@x@ÇA ARsàE‰ôaRàˆä F––aRt =áRI—€=â½%päF%±uà@™’€@å ±s…¬E·qBa D©vàb’€bàáÄ©å±w B@jÀBÿÿÚ˜@x@ßA$AjxàEKõáj——ajy =áj N€=áj%-áj%±zà@úM€@á å±{àB\àBå±| B@JÀBå±@x@¿AAJ}àE–öáJ˜˜e±~à=ƒ|å± = ÀÁ“ €{äF% áˆ"ó€à@Ó€@å ±!ˆ @`Û ¨¥â óå±»ñAëàb2 bá ë!¨ bå ±‚ B@`æÀBÿÿ* @x@ßA$A¨ƒàE€ëÿÿ–@™šbó„ =ájF‚÷a ájä^.…à@¨€€@å,±†àJÀJ 屇 B@aRÀBå±@x@ÇA > ½àE+9øaîèå ±››aR‰ =áRÛ@€=áR"½Šà@3<€@ü yƒ1ÓïN`3‚Žƒ£àda ÿ,È‹àbŸ;€bá j屌 B@jÀBÿÿQè@x@ßA$AjàE±ô€ˆÿÿ᜜ajŽ =áj=ø€=å"±à@˜÷€@á鯹$(%òãÅ"üDàBùö€Bá J$àB«c‘ B@JÀBå±@x@¿AAJ’àE8°ùbµàˆâ µe±“à=¹Â„F~öÇ” =áˆ,¶€{áˆ% 展à@u±€@ÿ ÿ5nö Çâ óå±¥ÿ2°–àbÔá Áë!¨ £áë«c— B@a¨ÀBñ@x@ßA$$^˜àE¿kúa¨ÿÿ›òžŸbó™ =ájä+û`=ä^"´šà@F*€@å,±›àJ²)€Jã >ëcœ B@aRàBå±@x@ÇA ARàEÌ âû  aRž =áRaê€=áR%±Ÿà@Àå€@ÿÿh° “`Ñ‘€äƒ,ð a 7VDf àb,Àb ç屡 B@jÀBÿÿÉ‚@x@ßA$Aj¢àESžübàˆä f¡¡aj£ = …€ƒß¡€=ä(%-ä(+c¤à@: @â rå±¥àB› €ƒàÁä ƒäŸÿ,¦ B@€ÆÀBå±@x@¿AAJ§àEÚYýáJ¢¢e±¨à=[l„Fëc© =áˆÆ_€{áˆ% áˆ"óªà@[€@ù» @`Û «câ ó屓¤Aë«àbvZ€bᨠbᨑ¬ B@a¨ÀB˜ô 4 öÇ@x@ßA$A¨­àE`þ`Ìëc£¤9»® =ájŠÕ€=ä^%±¯à@ëÓ€@å,±°àJWÀJ þäFå±± B@aRàBÿÿ$X@x@ÇA AR²àEnŒÿaRàˆä F¥¥aR³ =áR”€=áR"½´à@v€@ë c£àýÀ“üï-ÑÑàl`äa Ó‹B½µàb⎀bàáÄfå±¶ B@jÀBÿÿ…*@x@ßA$Aj·àEõG2 å2á j¦¦aj¸ =ájqK€=ä(%-ä(%±¹à@ÌJ€@à~Á屺àB- Bá J `ƒä…±» B@JÀBå±@x@¿AAJ¼àE{aJàˆá J§§e±½à=ü¤FÿÿF„¾ =áˆp €{âó% áˆ"ó¿à@¹€@ü yë câ óå±wyAëÀàb báÁë!¨ £á¨…±Á B@a¨ÀB å±@x@ßA$A¨ÂàE¿€ë屨©bóà =ájaæâóñ Äà@}€@å,±ÅàJí| J éøå±Æ B@aRàBå±@x@ÇA ARÇàE6aãÇâ ûªªaRÈ =áR±=€=â½áR"½Éà@9€@”ù …$F @fO ¨o³þÿ€%|à´âïNpÿÿ]LÿyÆÊàbp8€bàá½å±Ë B@jÀBüy@x@ßA$.!ÌàE–ñ€ˆáóá j««ajÍ =áj"õ€=â½å ±Îà@~ô€@á !j @ãÅ(®DÏàBÞó€BàÁÁJ$àB¥±Ð B@JÀBå±@x@¿AAJÑàE­bµàˆá J¬¬e±Òà=ž¤Få±Ó =ሳ€{áˆ% `ï€3bóÔà@W®€@ÿ ÿ5n!ˆ @âóå±%ÿyÆÕàb¹á ᨠbáë¥±Ö B@a¨ÀBüy@x@ßA$A¨×àE¤ha¨üy­®bóØ =ájÍ(`=áj$áj"´Ùà@/'€@ÿ ÿš' @ÿÿôÚàJš&€JârÁRå±Û B@aRàB{ åôÿÿ­@x@ÇA ARÜàE±ßà ˆâ û¯¯aRÝ =áRgç€=å"±Þà@¹â€@å ±ÃÈzyÀþÑ âÝ`ËDfßàb% bá jå±à B@jÀBÿÿ; @x@ßA$AjáàE8›bàˆá j°°ajâ =ájÄž€=â½%-â½+cãà@  @å$±äàB€€ƒàÁµå±å B@JÀBå±@x@¿AAJæàE¿VaJàËá J±±e±çà=@i„Få±è = ÀÁ·\€{áˆ% áˆ(¥éà@ùW€@Ÿè ¥$F @å±Ãâ óå±·9Aëêàb[ báᨠbå ±ë B@`æÀBå±@x@ßA$A¨ìàEE a¨ñ²³bóí =ájoÒ€=áj%páj%±îà@ÐЀ@ë,cïàJ<ÀJA#ånå±ð B@aRàBå±@x@ÇA ARñàES‰ aRàˆâ û´´aRò =áR‘€=áR"½óà@_Œ€@ÿ ÿBÓA¨ÿàbüá Âó'Z báë«c  a¨ÀBœñ@x@ßA$A¨àE绀ëñ·¸bó =áj | aæâóå ±à@jz€@å,±àJÖy JA"â¸å± B@aRàBñ@x@ÇA ARàEô2aàˆâ û¹¹aR =áRš:€=â½áR"½à@ü5€@ü yã,ñKeÐÑaÒû àÿÿB®€B½ àbhÀbA;ájå± B@jÀBÿÿ…*@x@ßA$Aj àE{î ë@Y ˆá jººaj =ájûñ€=áj%-ä(+c à@W @à~ÃÅ$( @ãÅ¥±àB·ð€ƒà B$àB¥± B@JÀBëc@x@¿AAJàEªâµ»»e±à=ƒ¼„Fÿÿ*  =áˆꯀ{áˆ% áˆ"óà@=«€@“å ±ë câ óå± rAëàbžá Áë!¨ £á륱 B@a¨ÀBÿÿ§V@x@ßA$A¨àE‰ea¨ñ¼½bó =áj·%`=å"±à@$€@å,±àJƒ# JA#£½ÁRå± B@aRàBëc@x@ÇA ARàE–Üà ˆä F¾¾aR =áR<ä€=â½å ±à@šß€@ë cóƒ]ð àƒ^U·ƒ_a ›ÿ@Òàb bá jå± B@jÀBÿÿ* @x@ßA$Aj àE˜bëc¿¿aj! =áj¡›€=áj%-ä(%±"à@ýš€@á å±#àB]àB$ ƒäœy$ B@JÀBå±@x@¿AAJ%àE£SaJàËâ µÀÀe±&à=%f„Fëc' =መY€{áˆ% áˆ%±(à@ÞT€@ÿ ÿÞ!ˆ @ëcâ óå±?ÿ@Ò)àb? báÄ^!¨ bá¨XF* B@a¨ÀB¡ëc@x@ßA$A¨+àE*a¨ëcÁÂbó, =ájMÏ€=å"±-à@­Í€@å,±.àJÀJA#å±/ B@aRàBå±@x@ÇA AR0àE8†aRàˆâ ûÃÃaR1 =áRê€=áR"½2à@D‰€@ÿÿ,%¯%¯`ðÂûÑ "äÿÿxE„D©3àb°ˆ€bàá½å±4 B@jÀBñ@x@ßA$Aj5àE¾AájÄÄaj6 =áj?E€=áj%-ä(%±7à@›D€@á $( @ãÅ«c8àBúCàB$àB¥±9 B@JÀBå±@x@¿AAJ:àEEýà ˆâ µÅÅe±;à=ƤFÿÿ@Ò< =áˆ=aÆå"±=à@€þ€¼ÿÿh°â óå±áÿ¤˜>àbáá Âó!¨å ±? B@a¨ÀBöÇ@x@ßA$A¨@àE̸,ëcÆÇbóA = ƒûxajâóê"´Bà@[w€@å,±CàJÇv€Jô ëcD B@aRàBå±@x@ÇA AREàEÙ/aéøÈÈaRF =áR˜7€=áR%±Gà@å2€@èo!“èoqBqB}|l?"ÿÿÿ¸l1ù$©HàbQ bä ©å±I B@jÀBŽè o ÿÆŽ@x@ßA$AjJàE`ë€ëàˆä fÉÉajK =ájèî€=ä(%-å±Là@E @â rå±MàB¤í€ƒàÁä ƒåô¥±N B@JÀBå±@x@¿AAJOàEç¦bµàˆá JÊÊe±Pà=h¹„FñQ =áˆ笀{áˆ% áˆ"óRà@"¨€@¨ñ !ˆ @âó屦êBóSàbƒ§€báᨠbᨋcT B@a¨ÀB¬â ó"ôÿÿ @x@ßA$A¨UàEmba¨ëcËÌbóV =áj"`=ä^%±Wà@ì €@å,±XàJXÀJ þë cY B@aRàBÿÿ5n@x@ÇA ARZàE{Ù Ó@Y«ìâ ûÍÍaR[ =áR&á€=áR"½\à@Ü€@ë c#ŒpAšƒ1àþ£àÿƒ7@¸À™áB½]àbëÛ€bàá½å±^ B@jÀB‚â ½å±@x@ßA$Aj_àE•bÿÿ; ÎÎaj` =ájŽ˜€=ä(%-ä(&¹aà@ë—€@å$±bàBJ Bá J)º ƒä‘c B@JÀBå±@x@¿AAJdàEˆPaJàˆâ µÏÏe±eà= c„Fÿÿôf =áˆyV€{âó% áˆ"ógà@ÄQ€@è ¥ö Çâ óå±=Aëhàb$ báÂó!¨ £á¨…±i B@a¨ÀBüy@x@ßA$A¨jàE a¨å±ÐÑbók =ájJÌ€=âó)Ïáj%±là@ªÊ€@å,±màJÀJA#¥(ÁRëcn B@aRàBå±@x@ÇA >oàEƒ aRãÇâ ûÒÒaRp =áRЊ€=â½áR"½qà@-†€@öÇ3îð£ïðƒ\àD€ñ}‘a 8B½ràb™…€bàáÁjå±s B@jÀBöÇ@x@ßA$AjtàE£>!ajáóá jÓÓaju =áj+B€=â½%-â½%±và@‰A€@á )Ú @ãÅ"ü@BwàBç@€BàÁÁJ$àB¥±x B@JÀBå±@x@¿AAJyàE*úà ˆá JÔÔe±zà=«¤Fÿÿ5n{ =áˆ"aÆáˆ% áˆ"ó|à@fû€¼—ëcâ ó屜ëA¨}àbÆá ᨠ£á륱~ B@a¨ÀBå±@x@ßA$A¨àE±µ,â1á ¨ÕÕbó€ =ájSº€=ájä^"´à@µ¸€@å,±‚àJ!ÀJ 屃 B@aRàBÿÿ,@x@ÇA AR„àE7q#bàˆá RÖÖaR… =áRèx€=áR"½†à@Ht€@å ±C"E­T%­"x\|àeFB½‡àb´s€bä ©å±ˆ B@jÀBÿÿ¦@x@ßA$Aj‰àE¾,$áj××ajŠ =ájJ0€=áj%-ä(%±‹à@¥/€@áéÃÅ屌àBàBå± B@JÀBå±@x@¿AAJŽàEEèà Ëâ µØØe±à=M¶ƒÇôÿÿh° =áˆ-î€{áˆ% áˆ"ó‘à@{é€@å ±ë câ óå±Á´Aë’àbáá Áë!¨ £å ±“ B@a¨ÀB”î W.Xñ@x@ßA$A¨”àEÌ£%bóöÇÙÚbó• =ájd&`=ê%±–à@bb€@å,±—àJÎa J þå ±˜ B@aRàBå±@x@ÇA AR™àEÙ'aàˆâ ûÛÛaRš =áRš"€=â½ç"½›à@í€@ÿÿBSƒ}ƒ-àû£àú£àù~aÀÕÿÞœàbY bàá½å± B@jÀBÿÿB@x@ßA$AjžàE`Ö€ëàˆá jÜÜajŸ =ájèÙ€=ájå ± à@C @à~Á屡àB¤Ø€ƒà B$ ƒä‘¢ B@JÀBå±@x@¿AAJ£àEç‘(bµç…á JÝÝe±¤à=h¤„F{ñ¥ =áˆÛ—€{áˆ% 屦à@!“€@ÿ ÿB!ˆ @öÇâ óå±qÿÞ§àbƒ’€báÁë!¨ bᨑ¨ B@a¨ÀB å±@x@ßA$A¨©àEmM)a¨ÿÿBÞßbóª =áj‹ *`=å"±«à@ì €@å,±¬àJXÀJ ëc­ B@aRàBå±@x@ÇA AR®àE{Ä Ó@Y ˆâ ûààaR¯ =áRÌ€=â½å ±°à@sÇ€@î ! @â½ c‘ƒ4à+mðÿÿ;  RHo±àb߯€bàá½å±² B@jÀBÿÿ,@x@ßA$Aj³àE€+býäááaj´ =áj†ƒ€=â½%-ä(+cµà@á‚€@á $( @ãÅ(®@B¶àBF BäàB¥±· B@JàB@Ù¢oÁ)ºÿÿB@x@¿AAJ¸àEˆ;,aJàËâ µââe±¹à= N„Füyº =@®€ÁxA€{âó% áˆc»à@Â<€@ö Çâ ó屩 A¨¼àb$ báÁeå±½ B@`æÀBñ@x@ßA$A¨¾àE÷€ëëcãäbó¿ =áj5·-aæâó$áj"´Àà@–µ€@å,±ÁàJÀJ â¸öÇ B@aRàBå±@x@ÇA ARÃàEn.aàˆâ ûååaRÄ =áRÒu€=áR"½Åà@$q€@ë csÿ1Óƒ5vÍ"ƒ,àà‘?B½Æàbp€bàá½å±Ç B@jÀBëc@x@ßA$AjÈàE£)/ájææajÉ =áj#-€=ä(%-â½%±Êà@,€@á å±ËàBß+àB$ ƒåô¥±Ì B@JÀBëc@x@¿AAJÍàE*倈æüççe±Îà=«¤FëcÏ =áˆë€{áˆå ±Ðà@hæ€@å ±!ˆ @è¥â óå±5ÍAëÑàbÊ¡á ë!¨ bᨋcÒ B@a¨ÀBå±@x@ßA$A¨ÓàE± 0bóå±èébóÔ =ájã`1`=ä^%±Õà@C_€@å,±ÖàJ¯^ JA#å±× B@aRàBÿÿÞ@x@ÇA ARØàE¾2aàˆå ±êêaRÙ =áRh€=áR%±Úà@€@ë c ƒþ£àªù{"E¹1îÿÿnb5B½Ûàb. bàáå±Ü B@jÀBëc@x@ßA$AjÝàEEÓ€ëàˆá jëëajÞ =ájÉÖ€=ä(%-å±ßà@% @ë $( @ãÅ"üAJààB…Õ€ƒàÁÁJ$àB¥±á B@JÀBå±@x@¿AAJâàEËŽ3âµììe±ãà=M¡„Få±ä =áˆÀ”€{âó% áˆ"óåà@€@ÿ ÿ â ó屑âA¨æàbg€bᨠ£á륱ç B@a¨ÀBñ@x@ßA$A¨èàERJ4a¨ÿÿ,íîbóé = ƒ} 5`=ájë cêà@Ý€@å,±ëàJIÀJ å ±ì B@aRàBå±@x@ÇA ARíàE`Á€ÓãÇä FïïaRî =áRÉ€=áR"½ïà@dÄ€@ü y“£ïðN"‚Žƒå4ð£å5å±:sB½ðàbÐÀbá jëcñ B@jÀBÿÿnb@x@ßA$AjòàEæ|6bàˆá jððajó =ájv€€=å"±ôà@Ó€@áéÅnå±õàB2 BàÁÀBå±ö B@JÀBå±@x@¿AAJ÷àEm87áJññe±øà=î¤Fñù =áˆa>€{áˆ% å±úà@¨9€@ë câ ó属ÿàJûàb  bᨠ£å ±ü B@a¨ÀBå±@x@ßA$A¨ýàEôó€ëëcòóbóþ =áj"´8aæájå ±ÿà@ƒ²€@‡@Së=ä @ÿÿ/¼ @7aï± J àJñ B@àBå±@x@ÇA ARàEk9aáÛä FôôaR@âàÆ¼r€=å"± ~à@n€@å ±£E¬££å6ð€ÿÿÃÐAÉE±àb…m€bàáÄfå± B@jÀBÿÿÞ@x@ßA$AjàEˆ&:ájõõaj =á)*€=â½*ßä(1 à@m)€@â rå± àBÐ(€Bá Jå± B@JàB@ÙãCöÇ@x@¿AAJ àEâà ˆâ µööe± à=ô„Fÿÿô =@®€Áÿç€{áˆ% áˆ(¥à@Jã€@—ù »$F @ñÃâ óå±å;Aëàb«á Âó!¨ bå ± B@`æÀBñ@x@ßA$A¨àE•;bóëc÷øbó =ájÈ]<`=áj%páj.à@(\€@â m!j @ .` Ãÿÿnb à‹”[ J (â¸å± B@aRàBÿÿ @x@ÇA ARàE£=aàˆâ ûùùaR =àÆj€=å"±à@»€@ÿÿQè³å7ðï$õ‚ä>õƒå8ðÿÿ§Vg3$©àb' bàá½å± B@jÀBÿÿ›ò@x@ßA$AjàE*Ѐëå2á júúaj =áj²Ó€=â½å ±à@ @à~Áå±àBnÒ€ƒá J$ ƒäŸÿ, B@JÀBëc@x@¿AAJ!àE°‹>âµûûe±"à=1ž„Fÿÿ # =ር‘€{âó% äF%±$à@쌀@ÿÿ; â óå±ÃÁBó%àbL bᨠ£á¨–Ç& B@a¨ÀBÿÿQè@x@ßA$A¨'àE7G?a¨ÿÿ,üýbó( =ájr@`=âó%páj%±)à@Ò€@å,±*àJ>ÀJA#èjå±+ B@aRàBå±@x@ÇA AR,àED¾€ÓùÃþþaR- =áRïÅ€=áR"½.à@IÁ€@ö ÇÃ$¥­9%¹õ‚a eB½/àbµÀ€bä ©å±0 B@jÀBÿÿô@x@ßA$Aj1àEËyAbájÿÿaj2 =ájS}€=áj%-â½&¹3à@±|€@á å±4àB Bâ µå±5 B@JÀBå±@x@¿AAJ6àER5BaJàˆæ ü@=7à=פFå±8 =áˆJ;€{áˆ% áˆ"ó9à@–6€@ÿÿF„â óå±ÁóAë:àbúá Ç!¨ £å ±; B@a¨ÀBÿÿL6@x@ßA$Aj<àEÙð€ëñA= =ájø°Caæçå ±>à@W¯€@å,±?àJî JA#å±@ B@aRàBå±@x@ÇA ARAàEægDaàˆâ ûaRB =áR•o€=â½áR"½Cà@îj€@å ±ÓK["u4јå:%³a õB½DàbZ bàá½å±E B@jÀBÿÿWš@x@ßA$AjFàEm#EájajG =ájõ&€=ájå ±Hà@R @á å±IàB±%€ƒà B$ ƒä‹cJ B@JÀBå±@x@¿AAJKàEôÞ€ˆæ}â µe±Là=uñ„Få±M =áˆèä€{áˆ% å±Nà@0à€@å ±!ˆ @ñ+câ óå±¹BóOàb߀báÂó!¨ bᨋcP B@a¨ÀB ÿÿ @x@ßA$A¨QàEzšFbóñbóR =ájšZG`=å"±Sà@ýX€@å,±TàJiÀJ å±U B@aRàBÿÿh°@x@ÇA ARVàEˆHaéøaRW =áR;€=â½å ±Xà@”€@å ±ãÑÑå;%© <  a  ÚB½Yàb bá jå±Z B@jÀBå±@x@ßW =Aj[ @`EÍ€ëàˆä f  aj\ = {€=‡Ð€=â½%-ä(+c]à@äÏ€@à~ÃÅ$( @ãÅ.`@B^àBK BäàB¥±_ B@aàBƒ@ÙâøöÇ@x@¿AAJ`àE•ˆIbµàËá J  e±aà=›F`=vÿÿBb =@®€Á‰Ž€{âó% áˆ(¥cà@щ€@ü y5\ @å±+câ ó屩(A¨dàb1 báÁëå±e B@`æÀBå±@x@ßA$A¨fàEDJajëc  bóg =ájDK`=âóä^"´hà@§€@å,±iàJÀJ â¸ñj B@aRàBå±@x@ÇA ARkàE)»€Óå±  aRl =áRá€=áR"½mà@9¾€@å ±óE±=%© %±> X2àjÿùÐnàb¥½€bá jå±o B@jÀBå±@x@ßA$AjpàE°vLbájajq =ájDz€=ä(%-ä(%±rà@¢y€@á å±sàBàB$ ƒåô¥±t B@JàB@Ùâøå±@x@¿AAJuàE72MaJàËå ±e±và=¼¤Fzå±w =@®€Á/8€{áˆ%páˆ"óxà@s3€@ÿ ÿÞ!ˆ @å±+câ óå±Îÿÿ¸yàbÓá å±z B@`æÀBÿÿ5n@x@ßA$A¨{àE¾í€ëå±bó| =ájÚ­Naæä^%±}à@<¬€@â m'%ò .`DFüy~ à‹¨« J@/â¸å± B@aRàBÿÿbþ@x@ÇA AR€àEËdOaàˆâ ûaR =àÆƒl€=áR"½‚à@Ûg€@ÿÿBE±?%© %±@  àÊçD©ƒàbG bàá½å±„ B@jÀBå±@x@ßA$Aj…àER Pájaj† =ájâ#€=ä(%-ä(%±‡à@< @‡@Øâr$( @ån"ü@BˆàBž"€ƒá J屉 B@JÀBñ@x@¿AAJŠàEØÛà ˆâ µe±‹à=ZîMaˆy屌 =áˆÍá€{áˆ%páˆ"óà@Ý€@å ±$F @å±Ãâ óå±nA¨ŽàbtÜ€bᨠbáë± B@a¨ÀB¡ñ@x@ßA$A¨àE_—Qajñbó‘ =áj”WR`=ä^"´’à@öU€@ˆ@Sâó!j屓àJbÀJA#àJå±” B@aRàBå±@x@ÇA AR•àEmSaãÇâ ûaR– =@þ€ƒ€=áR"½—à@u€@ëcE±A%©%±B Ã`®ÐB½˜àbá€bá jå±™ B@€æÀBå±@x@ßA$AjšàEóÉà ˆá jaj› =ájwÍ€=ä(%pÁ @3ÒAœà@ÒÌ€@á鯹å±àB3 Bá J属 B@JÀBå±@x@¿AAJŸàEz…Tbµàˆá Je± à=û¤Fz屡 =AT€Án‹€{áˆ% áˆ"ó¢à@·†€@ñ !ˆ @å±â óå± "Aë£àb báÁë!¨ bå ±¤ B@`æÀBñ@x@ßA$A¨¥àEAUa¨ëcbó¦ =áj&V`=ä^%±§à@ˆÿ€~å,±¨àJôþ€Jã >屩 B@aRàBå±@x@ÇA ARªàE¸áÛâ ûaR« =áR·¿€=áR"½¬à@»€@å ±#E±C%©@D Âz`bB½­àbzº€bàá½å±® B@jÀBÿÿÚ˜@x@ßA$Aj¯àE•sWbàˆá jaj° =ájw€=ä(å ±±à@tv€@â r$( @ÁJ ú"üAJ²àBÙu€BàÁÁJ$àB±³ B@JàB@Ùèªñ@x@¿AAJ´àE/XáJ@=µà=¤FöǶ =@®€Á5€{áˆ% å±·à@Y0€@›ÿÿ â óå±³ÿÚ˜¸àb¸á ᨠ£áë«c¹ B@`æÀBå±@x@ßA$AjºàE¢ê€ëëc bó» =ájÛªYaæáj+"áj"´¼à@=©€@â mëc½àJ©¨ J â¸å±¾ B@aRàBÿÿ$X@x@ÇA AR¿àE°aZaõ\!!aRÀ =áRei€=å"±Áà@¼d€@å ±3E©E%©ÑÑåF àp5DfÂàb( bá jå±Ã B@jÀBå±@x@ßÀ =AjÄ @`E7[aj‹@¡óå ±""ajÅ =áj¿ €=â½%-â½1Æà@ @å$±ÇàB€ƒá Jå±È B@aàB@Ù¡ÄKå±@x@¿AAJÉàE½Øà ˆá J##e±Êà=Bë„Fÿÿ¦Ë =@®€Á²Þ€{âó% áˆ(¥Ìà@ûÙ€@å ±': @ëcâ óå±ÔÇAëÍàbY bᨠbå ±Î B@`æÀB ñ@x@ßA$A¨ÏàED”\bóå±$%bóÐ =ájuT]`=âóå ±Ñà@×R€@â må±ÒàJCÀJ â¸å±Ó B@aRàBå±@x@ÇA ARÔàER ^aãÇâ û&&aRÕ =áR €=â½áR"½Öà@^€@ÿÿBC%© GðS"ŸS"û"1Óa æŠB½×àbÊ €bàáÄ#屨 B@jÀBÿÿnb@x@ßA$AjÙàEØÆ€ˆç''ajÚ =ájhÊ€=â½å ±Ûà@ÄÉ€@á $( @ãÅ(®AJÜàB$ Bâ µå±Ý B@JÀBŒ@Ùâøå±@x@¿AAJÞàE_‚_bµàˆâ µ((e±ßà=ä¤Få±à =@®€ÁSˆ€{áˆ% å±áà@•ƒ€@£ëcâ óå±BxA¨âàbûá Âó!¨ £áë«cã B@`æÀBå±@x@ßA$A¨äàEæ=`a¨ñ)*bóå =ájþ€=ájä^"´æà@yü€@‡@Sâmå±çàJåû J àJå±è B@aRàBå±@x@ÇA ARéàEó´aaRâû++aRê =@þ€ƒŒ¼€=áR%±ëà@ç·€@ë cSƒ,"ÿŸÿQé"ÿÿŠÜÁúB½ìàbS bàá½å±í B@€æÀBÿÿ* @x@ßA$AjîàEzpbáj,,ajï =áj t€=áj%p`±€3ÒAðà@es€@á å±ñàBÆràB$ ƒäŸÿ ò B@JÀBå±@x@¿AAJóàE,caJã>å ±--e±ôà=‚>ƒ|å±õ =áˆñ1€{áˆ% áˆ"óöà@;-€@ë câ óå±eAë÷àbá Âó!¨ £á¨Ÿÿ ø B@a¨ÀBÿÿ¦@x@ßA$A¨ùàE‡ç€ëëc./bóú =áj™§daæê%±ûà@ú¥€@ë,cüàJfÀJA#å±ý B@aRàBå±@x@ÇA ARþàE•^eaàˆâ û00aRÿ =áRVf€=â½ç"½à@©a€@ÿ ÿ,c}|n"@?‘äÿ" RàÇÿ¤bàb bàá½å± B@jÀBÿÿ5n@x@ßA$AjàEfajäf11aj =áj €=â½%-ä(1à@û€@á å±àB` BàÁÁJ$ ƒä…± B@JàB âøëc@x@¿AAJàE¢Õà Ëâ µ22e± à=#è„Fÿÿ$X =@®€ÁšÛ€{âó% áˆ"ó à@ÜÖ€@ÿ ÿ,â óå±coAë àb> bá ë!¨ £á¨…± B@`æÀBÿÿÞ@x@ßA$A¨àE)‘gbóå±34bó =ájSQh`=âóä^%±à@´O€@å,±àJ ÀJA#â¸ëc B@aRàBå±@x@ÇA ARàE6iaàˆâ û55aR = …€ƒð€=â½áR"½à@C €@å ± säuð6ä+g"ð Œà6]B½àb¯ €bä få± B@€æÀBå±@x@ßA$AjàE½Ã€ˆáj66aj =ájIÇ€=áj%-ä(%±à@¥Æ€@à~Ånå±àBàB$ ƒä…± B@JÀBëc@x@¿AAJàEDjâµ77e±à=ŤFÿÿ/¼ =áˆ<…€{áˆ% áˆ"ó à@~€€@å ±!ˆ @öÇâ óå±’¯Aë!àbàá Áë!¨ bᨅ±" B@a¨ÀBå±@x 9A$A¨# @€EË:ka¨ñ89bó$ = ?`=áú€=å"±%à@Aù€@å,±&àJ­ø J å ±' B@€ÎÀBÿÿ/¼@x@ÇA AR(àEرlaRàˆå ±::aR) =áRй€=áR"½*à@ä´€@å ±ƒƒ7ÿ]F"qBƒ\àÿÿÔæòÿ¸l+àbP bàá½å±, B@jÀBñ@x@ßA$Aj-àE_mmáj;;aj. =ájãp€=ä(%-ä(%±/à@? @æ ¹$( @ãÅ(®@B0àBŸo€ƒá J$àB¥±1 B@JÀBå±@x@¿AAJ2àEå(náJ<bó9 =áj‹¤oaæä^"´:à@뢀@å,±;àJWÀJA#ëc< B@aRàBå±@x@ÇA AR=àEz[paŒ@Y ˆå ±??aR> =áR(c€=áR"½?à@Š^€@ÿÿyÆ “8ð"‘ƒ8%¬\ðÿÿbþެDf@àbö] b  áå±A B@jÀByÿ 7ÿÿbþ@x@ßA$AjBàEqajàˆá j@@ajC =áj…€=áj%-ä(%±Dà@á€@ã Åå±EàB@ BàÁÁJ$ ƒä‹cF B@JÀBŒ á!KÿÿL6@x@¿C =AJG @€E‡Ò€Ëàˆá JAAe±Hà=å„FëcI =@®€{sØ€{å"±Jà@ÂÓ€@è¥%Nå±â óå±*^AëKàb# báᨠbᨋcL B@`æÀBå±@x@ßA$A¨MàEŽrâóBBbóN =áj§’€=ájïÂ%±Oà@ ‘€@å,±PàJv JA#â¸ñQ B@aRàBÿÿ P 5@ÇA ARR @`E•IsaRãÇâ ûCCaRS =áRMQ€=äFáR%±Tà@¥L€@ü y£à"1Óª©ƒ-tð£ê`Ì ùQB½Uàb bá jëcV B@a$àBÿÿB@x@ßA$AjWàEtajàˆá jDDajX =áj£€=â½%-å±Yà@þ€@áéÃÅå±ZàB_ BàÁÀBå±[ B@JÀBå±@x@¿AAJ\àE¢À€Ëàˆá JEEe±]à=ª¤Fóÿÿ]L^ =ሎƀ{áˆ% áˆ"ó_à@ÛÁ€@—ÿÿ,â óå±]£Bó`àb> báᨠ£å ±a B@a¨ÀBå±@x@ßA$A¨bàE)|ubóñFGbóc =ájY¦àEð Ó@Y£Aâ ûWWaR§ =áRÈ÷€=â½%p⽨à@'ó€@ë cãÒý"T333TøB9"3@¸Àpb½©àb“ò€bàáĩ屪 B@jÀBÿÿWš@x@ßA$Aj«àE¢«‚bàˆá jXXaj¬ =áj&¯€=áj%páj%±­à@®€@à~Á$( @ãÅ¥±®àBâ­àB$àB¼y¯ B@JÀBñ@x@¿AAJ°àE)gƒaJÿÿñYYe±±à=ª¤Få±² =áˆm€{áˆ% áˆ%±³à@bh€@˜å ±$F @å±+câ óå±Ôh¥´àbÉ¡è ¥!¨ bá륱µ B@a¨ÀBÿÿt@x@ßA$A¨¶àE¯"„a¨ëcZ[bó· =ájÙâ€=G@—¤œÂ²ê "´¸à@:á€@å,±¹àJ¦à Jà ‹å±º B@aRàBå±@x@ÇA AR»àE½™…aRÿÿ,\\aR¼ =áRz¡€=áR"½½à@Õœ€@å ±óï`ƒ‚àÓ”Pàð€a öD©¾àbA bá j屿 B@jÀBñ@x@ßA$AjÀàECU†ajàˆå ±]]ajÁ =ájÌX€=áj%-ä(%±Âà@' @ã Åå±ÃàBˆW€ƒà B$ ƒäŸÿ,Ä B@JÀB‹@Ùã„)ºÿÿ,@x@¿AAJÅàEʇáJ^^e±Æà=O#ƒ|ñÇ =@®€Á¶€{å"±Èà@€@å ±!ˆ%Nå±.Wâ óå± AëÉàbf€bâjÄê!¨ b᨟ÿ,Ê B@`æÀBüy@x@ßA$A¨ËàEQÌ€ˆëc_`bóÌ =ájƒŒˆaæâóê%±Íà@䊀@å,±ÎàJPÀJA#â¸å±Ï B@aRàBå±@x@ÇA ARÐàE^C‰aàˆä FaaaRÑ =áRK€=áR%±Òà@bF€@ÿÿÞ …¯@ä<‚àa šÿNÓàbÎE b@$ájå±Ô B@jÀBÿÿÞ@x@ßA$AjÕàEåþà ˆá jbbajÖ =!b ƒiŠa¨ä(%-å±×à@Å€@å n$( @ãÅ"üDØàB) Bá J$àB¥±Ù B@JàB@Ùáå±@x@¿AAJÚàElº€Ëàˆá Jcce±Ûà=õ¤FoñÜ =@®€ÁTÀ€{áˆ% áˆ"óÝà@¦»€@ÿ më câ óå±>ÿ6Þàb báå±ß B@`æÀBëc@x@ßA$A¨ààEóu‹bµå±debóá =áj6Œ`=ä^"´âà@e4€@â m!j%òöÇãàJÑ3 J â¸å±ä B@aRàB{ àB¿ÿ”Õ @x@ÇA ARåàEíà ˆâ ûffaRæ =áR¦ô€=áR"½çà@øï€@èo!RèoÔ%®"0}@ýà‹*[èàbd bá jå±é B@jÀBÿÿô@x@ßA$AjêàE‡¨bàˆá jggAë =áj¬€=ä(%-ä(!ìà@g«€@å$±íàBǪ€BàÁä ƒåô¥±î B@JàB@Ùâµå±@x@¿AAJïàE dŽáJhhe±ðà=v„Fññ =@®€Áj€{âóå ±òà@He€@ÿ ÿ* ': @âóå±/éAëóàb©á ᨠbᨋcô B@`æÀBÿÿ,@x@ßA$A¨õàE”a¨ñijbóö =ájÂ߀=âó$âó%±÷à@#Þ€@â må±øàJÝ JA#â¸å±ù B@aRàBå±@x@ÇA ARúàE¢–aRãÇä FkkaRû =áRLž€=â½áR%±üà@®™€@Œó Ó"½ @â½#ñaÒåq0àC#"Âa .àB½ýàb bá jå±þ B@jÀBëc@x@ßA$AjÿàE(R‘ajàˆá jllaj   €ƒ´U€=â½%-â½%±à@ @áéÃÅ!j @ãÅ"ü@BàBpT€ƒàÁÀBëc B@€ÆÀBöÇ`x@9 AJ @€E¯ ’áJmme±à=0 ƒ|ÿÿB =ማ€{áˆ% áˆ"óà@æ€@—â ó$F @âó+câ ó届A¨àbK bᨠbá륱 B@abÀBñ@x@ßA$A¨ àE6É€ëëcnobó =ájX‰“aæáj$áj"´ à@¹‡€@å,± àJ% Jã >å± B@aRàBå±@x@ÇA ARàEC@”aöÇppaR =áRþG€=å"±à@OC€@ë c 3‘"0Áú‘"@"Âz`²÷B½àb»B€bâ ½å± B@jÀBÿÿh°@x@ßA$AjàEÊûà ˆå ±qqaj =ájZÿ”a¨â½%pâ½%±à@·þ€@â rå±àB Bá Jå±aI¡JÀBå±@x@¿AAJàEQ·•a àˆá Jrre±à=Ò¤Få± =áˆM½€{áˆ% áˆ%±à@Œ¸€@ë câ óå±Aëàbíá çZ £Ã6!™¥± B@bóÀBÿÿh°@x@ßA$A¨àE×r–a¨ëcstbó =ájþ2—`=ájå ±!à@^1€@å,±"àJÊ0 J ëc# B@aRàBÿÿbþ@x@ÇA AR$àEå逈å±uuaR% =áR“ñ€=áR"½&à@ñì€@ë cC0G¹1 ïƒà`H¨àÍæB½'àb] bá jå±( B@jÀBÿÿt@x@ßA$Aj)àEl¥˜bÿÿávvaj* =ájø¨€=ájå ±+à@T @á å±,àB¸§€ƒà Bå±- B@JàB@Ùâøñ@x@¿AAJ.àEò`™aJàˆå ±wwe±/à=ws„FÿÿQè0 =@®€Áãf€{è¥% å±1à@.b€@ë câ óå±iBó2àbŽa€báå±3 B@`æÀBÿÿ @x@ßA$A¨4àEyša¨å±xybó5 =áj¤Ü€=ájå ±6à@Û€@â m,Î @öÇ7àJpÚ JA#â¸ëc8 B@aRàBëc@x@ÇA AR9àE‡“›aRéyâ ûzzaR: =áRA›€=äF!â½;à@—–€@ù …$F @î!6ÇS}€}ïëäýå±››B½<àb bàá½å±= B@jÀB†ÿ ÿ œÿÿ$X@x@ßA$Aj>àE Oœajáóá j{{aj? =áj™R€=ájå ±@à@úQ€@à~Á!j @éw.`AJAàBYàBå±B B@JàB@Ùâø!Kÿÿ,@x@¿AAJCàE” áJ||e±Dà=š`=ÿÿh°E =@®€Á€{áˆå ±Fà@Ï €@ÿÿåüâ óå±?iA¨Gàb0 bá ë!¨ £áë«cH B@`æÀBëc@x@ßA$A¨IàEÆ€ëÿÿ }~bóJ =ájZ†ža¨å"±Kà@º„€@‡@Sâmå±LàJ&ÀJA#àJå±M B@aRàBå±@x@ÇA ARNàE(=ŸaëcaRO =@þ€ƒ×D€=â½%på±Pà@0@€@Exá$F @å±c+[ àÿQïå0àa “B½Qàbœ?€bâ ½å±R B@€æÀBÿÿ$X@x@ßA$AjSàE¯ø€ˆáj€€ajT =áj?ü€=áj%páj1Uà@œû€@á !j @ãÅ¥±VàBûúàB$àB¼yW B@JÀBñ@x@¿AAJXàE6´ bµã>æ üe±Yà=·¤Fÿÿ,Z =áˆ"º€{áˆ% áˆ.W[à@mµ€@ÿ ÿ6$F @âóÃâ óå±7óAë\àbÒá Ç!¨ bá륱] B@a¨ÀBöÇ@x@ßA$A¨^àE¼o¡a¨å±‚ƒbó_ =ájì/¢`=å"±`à@K.€@è !j$^ëcaàJ·- J ä©å±b B@aRàBñ@x@ÇA ARcàEÊæà ˆâ û„„aRd =áRî€=áR"½eà@Þé€@ñ s} +d ïul»umà&ŒB½fàbJ bá jå±g B@jÀB~ëc@x@ßA$AjhàEP¢£bå±……aji =ájÝ¥€=ájä(%±jà@: @äåÃÅå±kàB™¤€ƒà B$ ƒäŸÿ l B@JÀBÿÿ @x@¿AAJmàE×]¤áJ††e±nà=Xp„Få±o =áˆÏc€{å"±pà@_€@å ±!ˆ%Nå±+câ ó层~Aëqàbw^€bâ ó!¨ b᨟ÿ,r B@a¨ÀB â ó- ÿÿô@x@ßA$A¨sàE^¥a¨ñ‡ˆbót =áj~Ù€=âó%pç(fuà@á×€@å,±vàJMÀJ þå ±w B@aRàBx åôÿÿÞ@xu 9A ARx @`Ek¦aRàˆå ±‰‰aRy =áR'˜€=áR%±zà@€“€@ëcƒ unÜuo¥upu}l{ `Ì œ_B½{àbë’€bàáÄ©å±| B@a$àBëc@x@ßA$Aj}àEòK§ájŠŠaj~ = …€ƒjO€=ä(%-â½%±à@ÈN€@å n$( @ãÅ"ü@B€àB. Bá J$àB¥± B@€ÆÀB„@Ùâµ$ @'@x@¿AAJ‚àEy¨áJ‹‹e±ƒà=ú¤Fuëc„ =@®@™i €{áˆ% áˆ"ó…à@µ€@ÿ m)ø @å±+câ óå±8­A¨†àb bᨠbá륱‡ B@`æÀBöÇ@x@ßA$A¨ˆàEÀëÿÿ,Œbó‰ =áj,ƒ©aæä^"´Šà@Ž€@å,±‹àJú€ JA#â¸ëcŒ B@aRàBÿÿ$X@x@ÇA ARàE :ªaàˆå ±ŽŽaRŽ =áRµA€=áR"½à@=€@ÿ ÿB“DTÝ‚±õpD{a  B½àb< b@$áj屑 B@jÀB{è oöÇ@x@ßA$Aj’àE”õà ˆá jaj“ =ájù€=ä(%-ä(%±”à@^ø€@å$±•àBÀ÷€Bá Jå±– B@JàB á!Kå±@x@¿AAJ—àE±«bµì®e±˜à=œ¤Füy™ =@®€Á·€{áˆáˆ"óšà@[²€@—è ¥ @å±+câ óå±+ÿAë›àb»¡á ë!¨ bå ±œ B@`æÀBå±@x@ßA$A¨àE¡l¬a¨ëc‘’bóž =ájÕ,­`=ájë cŸà@8+€@â m' @ñ àJ¤* J â¸å±¡ B@aRàBå±@x@ÇA AR¢àE¯ã€ˆâû““aR£ =áRgë€=å"±¤à@¿æ€@ü y £‚±ý 6Ï‚ a ’tB½¥àb+ bá j屦 B@jÀBëc@x@ßA$Aj§àE5Ÿ®bàˆå ±””aj¨ =áj¶¢€=ájå ±©à@ @ã ÅëcªàBq¡€ƒàÁÌ®$ ƒä‘« B@JÀBöÇ@x@¿AAJ¬àE¼Z¯aJⵕ•e±­à==m„Fÿÿ5n® =ሴ`€{äF% ç:%±¯à@ù[€@ÿ ÿ5n$F @å±+câ óå±6†Bó°àb\ bᨠbᨑ± B@a¨ÀBñ@x@ßA$A¨²àEC°á¨––bó³ =ájâ€=áj%páj+c´à@C€@å,±µàJ¯ J å±¶ B@aRàBüy@x@ÇA AR·àEÊÑ€ˆãÇä F——aR¸ =áRoÙ€=áR"½¹à@ÂÔ€@î !$F!“â½(o³ ïäýï}ÿÿ–@†B½ºàb. bàáÄfå±» B@jÀBÿÿô@x@ßA$Aj¼àEP±b½áóá j˜˜aj½ =ájЀ=áj%-â½+c¾à@, @à~Á!j @ãÅ"ü@B¿àBŒ€ƒà Bå±À B@JÀBå±@x@¿AAJÁàE×H²áJ™™e±Âà=ã¤Fñÿÿ@Òà =áˆÏN€{áˆ% áˆ"óÄà@J€@ÿ ÿ â óå±"bA¨ÅàbsI€bᨠ£áë¥±Æ B@a¨ÀBÿÿô@x@ßA$A¨ÇàE^³a¨ñš›bóÈ =áj˜Ä€=è¥$áj"´Éà@ù€@ÿ,ÿ,ÊàJeÀJA#å±Ë B@aRàBÿÿ$X@x@ÇA ARÌàEk{´aRáÛä FœœaRÍ =áRƒ€=â½áR"½Îà@o~€@ë cÃ}ïâ1zá1zñÿÿ,VÇB½ÏàbÛ}€bàáÄ©å±Ð B@jÀB‚ñ@x@ßA$AjÑàEò6µájajÒ =ájf:€=áj%pâ½%±Óà@Â9€@á å±ÔàB"àBå±Õ B@JÀBå±@x@¿AAJÖàEyò€Ëã>â µžže±×à=ú¤FÿÿBØ =áˆmø€{áˆ% áˆ"óÙà@¯ó€@ ñ ë câ óå±ò5AëÚàb bá ë!¨ £å ±Û B@a¨ÀBëc@x@ßA$A¨ÜàEÿ­¶bóñŸ bóÝ =ájn·`=å"±Þà@~l€@ë,cßàJêk J å±à B@aRàBëc@x@ÇA ARáàE %¸aàˆâ û¡¡aRâ =áR¿,€=â½å ±ãà@(€@å ± Óñ9Ò"ý+cpëc@ÙB½äàb'€bàáå±å B@jÀBÿÿ @x@ßA$AjæàE”à€ˆäf¢¢ajç =ájä€=ájå ±èà@xã€@á å±éàBØâ€BàÁÁJ$ ƒä‘ê B@JÀBå±@x@¿AAJëàEœ¹âµ££e±ìà=Ÿ®„FöÇí =ሢ€{áˆ% å±îà@U€@å ±!ˆ @îWâ óå±äÿZŽïàb¶á ᨠbᨑð B@a¨ÀBëc@x@ßA$A¨ñàE¡Wºa¨å±¤¥bóò =ájл`=å"±óà@0€@å,±ôàJœ JA#å±õ B@aRàBå±@x@ÇA ARöàE®Îà ˆå ±¦¦aR÷ =áR]Ö€=áR(oøà@³Ñ€@å ±ãË]}ƒ3ía Z‹E±ùàb bä fëcú B@jÀB…ëc@x@ßA$AjûàE5мbáóá j§§ajü =áj¹€=ä(%-ä(+cýà@ @á $( @ãÅ.`@BþàByŒ€ƒàÁäàB¥±ÿ B@JàB@Ùâøüy@x@¿AAJ  €E¼E½áJ¨¨e±à==X„Fÿÿh° =@®€{¸K€{áˆ% áˆ"óà@÷F€@ÿ ÿBë câ óå±þyA¨àbX bᨠ£á륱 B@`æÀBÿÿB@x@ßA$A¨àEC¾a¨ÿÿB©ªbó =áj~Á€=ä^"´à@Ý¿€@â m!j*Qüy àJIÀJA#⸠`JáR-ˆ­ B@aRàBÿÿ @x@ÇA AR àEPx¿aRàˆä F««aR =áRû€=áR"½ à@X{€@ÿ ÿ óU>%¨ñ›+iƒ-ïÿÿ¸l“ãB½àbÄz€bàáÄfå± B@jÀBÿÿ @x@ßA$AjàE×3Àajû.¬¬aj =áj_7€=ä(%-ä(%±à@À6€@å$±àB Bá Jëc B@JàB âøå±@x@¿AAJàE^ïà Ëâ µ­­e±à=ã¤Fñ =@®€ÁNõ€{áˆ% áˆ"óà@™ð€@ÿ ÿ â óå±7Aëàbúá Âó!¨ £ä^¥± B@`æÀBÿÿ,@x@ßA$A¨àEäªÁbóÿÿB®¯bó =ájkÂ`=ä^%±à@wi€@â må±àJãh JA#â¸å± B@aRàBå±@x@ÇA AR àEò!Ãaàˆâ û°°aR! =áR)€=áR"½"à@ö$€@ëc £íðÒ‘0}<äÿ1ÿÿ/¼*ÿ¡¤#àbb bàá½å±$ B@jÀBöÇ@x@ßA$Aj%àExÝ€ëàˆá j±±aj& =ájá€=ájå ±'à@^à€@å$±(àB½ßàB$ ƒäˆg) B@JÀBÿÿB@x@¿AAJ*àEÿ˜Äâµ²²e±+à=€«„Fÿÿ¦, = ÀÁóž€{å"±-à@7š€@ÿÿ,â ó屎ÿ¡¤.àb›™€bâjÂó!¨å ±/ B@`æÀB î WÿÿB@x@ßA$A¨0àE†TÅᨳ³bó1 =áj$Y€=áj+"ç%±2à@†W€@å,±3àJòV J þå ±4 B@aRàBüy@x@ÇA AR5àE ÆaRãÇä F´´aR6 =áRÅ€=áR%±7à@€@ÿÿ¦ÿ* .1¯}Ùñæ}?a ?w*[8àb‰€bá jå±9 B@jÀBÿÿŠÜ@x@ßA$Aj:àE“Ëà ˆá jµµaj; =ájÏ€=áj%-â½!<à@w΀@áéÃÅñ=àB×Í€Bá J$ ƒå ±> B@JÀBöÇ@x@¿AAJ?àE‡Çbµàˆá J¶¶@=@à="Uƒ|ôÿÿ,A =ሠ€{áˆ% áˆ"óBà@Xˆ€@—ü y!ˆ @1€# â óå±›…AëCàb¶á Áë!¨ bᨖÇD B@a¨ÀBëc@x@ßA$AjEàE¡BÈa¨ÿÿ,·¸bóF =ájÆÉ`=ájå ±Gà@(€@å,±HàJ”€Jã >ëcI B@aRàBå±@x@ÇA ARJàE®¹€ˆïª¹¹aRK =áRgÁ€=áR"½Là@º¼€@ÿÿÞ #ï‘0ñu *ñua ýÀB½Màb& bãݽå±N B@jÀBÿÿÞ@x@ßA$AjOàE5uÊbájººajP =ájÉx€=áj%-ä(%±Qà@$ @á $( @ãÅ(®@BRàB…w€ƒà B$àB«cS B@JÀBå±@x@¿AAJTàE¼0ËaJàˆå ±»»e±Uà==C„FÿÿôV =ሬ6€{áˆ% áˆ"óWà@ò1€@ë câ óå±Y2A¨XàbX báÂó!¨ £á륱Y B@a¨ÀBå±@x@ßA$A¨ZàEBì€ëüy¼½bó[ =ájh¬ÌaæîWå ±\à@ɪ€@å,±]àJ5ÀJA#ñ^ B@aRàBå±@x@ÇA AR_àEPcÍaàˆâ û¾¾aR` =áRõj€=H àÆÿ ÿS;"½aà@Xf€@ÿÿô3Ÿÿ, 0}r1¢ƒ}àD@zÀù$©bàbÄe b@%à£å±c B@jÀBÿÿ@Ò@x@ßA$AjdàE×Îaj  ˆá j¿¿aje =ájg"€=áj%-ä(!fà@Â!€@á å±gàB#àB$ ƒä‘h B@JÀBå±@x@¿AAJiàE]Úà Ëá JÀÀe±jà=â¤F€å±k =áˆnà€{äF% áˆ"ólà@¿Û€@ô @ëcâ ó屸 Aëmàb! bá ë!¨ bᨋcn B@a¨ÀBÿÿL6@x@ßA$A¨oàEä•ÏbóöÇÁÂbóp =ájVÐ`=ájå ±qà@gT€@å,±ràJÓS JA#£½Çëcs B@aRàBå±@x@ÇA ARtàEñ Ñaàˆâ ûÃÃaRu =áR£€=äFáR"½và@þ€@Œî ! @â½CØ+\ 0T¯Ò‘UŽa jB½wàbj bì Îå±x B@jÀBÿÿx@x@ßA$AjyàExÈ ë@Y¡óá jÄÄajz =ájøË€=áj%-ä(%±{à@T @áéÂrå±|àB´Ê€ƒà B$ ƒä…±} B@JÀBå±@x@¿AAJ~àEÿƒÒâµÅÅe±à=„–„F|å±€ =áˆ{áˆ% áˆ"óà@9…€@î W; @âó6Çâ óå±ÎçAë‚àb›„€bᨠbᨅ±ƒ B@a¨ÀBëc@x@ßA$A¨„àE†?Óa¨ÿÿ,ÆÇbó… =áj´ÿ€=å"±†à@þ€@‡@SŸÿ(ƒÂó!j$^ÿÿ,‡àJ€ý€Já R `JáR¿ÿ,ˆ B@aRàBÿÿÞ@x@ÇA AR‰àE“¶ÔaRàˆä FÈÈaRŠ = …€ƒE¾€=áR"½‹à@Ÿ¹€@ö ÇSƒ1tðÿÃF,àÔ Œà[PB½Œàb  bàáÁµå± B@€æÀBå±@x@ßA$AjŽàErÕajå±ÉÉaj =áj¦u€=ä(%-ä(%±à@ @æ ¹å±‘àBbt€ƒá Jå±’ B@JÀBå±@x@¿AAJ“àE¡-ÖaJÿÿBÊÊe±”à="@ƒ|ñ• =áˆ3€{áˆ% áˆ"ó–à@ß.€@ÿÿ â óå±ÿ¢Aë—àbA bá ë!¨ £ä^¥±˜ B@a¨ÀBÿÿ @x@ßA$A¨™àE'é€ëëcËÌbóš =ájV©×aæä^3Ê›à@¶§€@ë,cœàJ"ÀJA#åüå± B@aRàBå±@x@ÇA ARžàE5`Øaàˆå ±ÍÍaRŸ =áRÛg€=áR"½ à@=c€@ñ c!@òU•Q•ï åÿÿbþIB½¡àb©b b ájå±¢ B@jÀBñ@x@ߟ =Aj£ @`E»ÙájÎÎaj¤ =áj@€=ájå ±¥à@œ€@ã Å屦àBüàB$ ƒä‹c§ B@aàBÿÿ,@x@¿AAJ¨àEB×€ˆæüÏÏe±©à=äFëcª = ÀÁ*Ý€{å"±«à@}Ø€@ñ )ø%Nëcâ óå±±»Bó¬àbâ¡â ó!¨ bᨋc­ B@`æÀBÿÿ,@x@ßA$A¨®àEÉ’Úbóå±ÐÑbó¯ =ájôRÛ`=âó+"ç%±°à@XQ€@å,±±àJÄP JA#äFå±² B@aRàBwÿÿ5n@x@ÇA AR³àEÖ ÜaâûÒÒaR´ =áR}€=áR%±µà@Ú €@ñ  sõñàÒã‘+gÒ‘a @*B½¶àbF bá jå±· B@jÀBÿÿ @x@ßA$Aj¸àE]Å€ëàˆç ÓÓaj¹ =ájáÈ€=ä(%-â½+cºà@> @å$±»àBÇ€ƒàÁÌ®$ ƒä…±¼ B@JÀBëc@x@¿AAJ½àEä€ÝâµÔÔe±¾à=e“„F屿 =áˆÔ†€{áˆ% áˆ"óÀà@#‚€@—ô @å±+câ óå±äØAëÁàb„€bᨠbᨅ±Â B@a¨ÀBÿÿ@Ò@x@ßA$A¨ÃàEk<Þa¨ÿÿ,ÕÖbóÄ =ájü€=ájå ±Åà@íú€@å,±ÆàJYÀJ å±Ç B@aRàBÿÿ @x@ÇA ARÈàEx³ßaRå±××aRÉ =áR»€=å"±Êà@€¶€@ë cƒÒ|ws1¢ñæüy¹B½Ëàbìµ bA;ájå±Ì B@jÀBzè oÿÿ5n@x@ßA$AjÍàEÿnàajàˆå ±ØØajÎ = …€ƒ‡r€=â½%-ä(%±Ïà@äq€@å$±ÐàBG BàÁaTå±Ñ B@€ÆàB@Ù`8àB!K @'@x@¿AAJÒàE…*áaJâµÙÙe±Óà==ƒÇxöÇÔ =@®@™v0€{áˆ% áˆ%±Õà@Å+€@“å ±ë câ ó ›”hv¼ï/GÌï?ðAëÖàb% bᨠ£å ±× B@`æÀB™â ó!¨Á¨@x@ßA$A¨ØàE æ€ëÿÿ,ÚÛbóÙ =áj;¦âaæájå ±Úà@›¤€@â m!j @öÇÛàJÀJ þâ¸ëcÜ B@aRàBå±@x@ÇA ARÝàE]ãaôÜÜaRÞ =áRÉd€=å"±ßà@"`€@ö Ç“ |y*|y%½äa »üB½ààbŽ_€bá jå±á B@jÀBÿÿB@x@ßA$AjâàE äajàˆå ±ÝÝajã =áj%€=ájå ±äà@‚€@å$±åàBà€BàÁ屿 B@JÀBëc@x@¿AAJçàE'Ôà ˆá JÞÞe±èà=¬¤Fyå±é = ÀÁÚ€{äF% å±êà@cÕ€@—å ±2ž @ëcâ óå±BóëàbÃá ᨠbå ±ì B@`æÀBüy@x@ßA$A¨íàE®åbóüyßàbóî =ájÙOæ`=áj%páj1ïà@9N€@å,±ðàJ¥M€Jã >å±ñ B@aRàBå±@x@ÇA ARòàE»çaè­ááaRó =áRz€=áR(oôà@à €@ù…!“â½£ÿÿ5mÿHàýHàcÿ²ºõàb/ bä få±ö B@jÀB“@lêëc@x@ßA$Aj÷àEB€ëájââajø =ájÖÅ€=ájå ±ùà@3 @á $( @ãÅ"üDúàB’Ä€ƒà Bëcû B@JÀBå±@x@¿AAJüàEÉ}èâµããe±ýà=R„Få±þ =áˆɃ€{áˆ% äF"óÿà@€@«å ±)ø @âóÃâ óå±Çÿ²ºà£i~€bᨠbáë± B@a¨ÀBÿÿ$X@x@ßA$A¨àEO9éa¨öÇäåbó =ájù€=è¥$áj"´à@â÷€@å,±àJNÀJA#äå± B@aRàBÿÿL6@x@ÇA ARàE]°êaRäFææaR =áR¸€=â½áR"½ à@q³€@ö dzïƒ+îð£ïðäƒ/ð a ¤ÎDf àbݲ€bá jå± B@jÀBå±@x@ßA$Aj àEäkëajìÎççaj =ájto€=áj%pâ½1à@Ñn€@á å±àB0àBå± B@JÀBå±@x@¿AAJàEj'ìaJâµèèe±à=ï¤F~ëc =áˆ[-€{äF% áˆ"óà@ª(€@ÿ ÿÞ!ˆ @å±6Çâ óå±à@î{€@ë câ óå±q?àbN bá ë!¨ £ä^¥±@ B@a¨ÀBöÇ@x@ßA$A¨AàE46ôa¨ëcóôbóB =ájYö€=ä^(fCà@»ô€@ë,cDàJ'ÀJA#£½Øå±E B@aRàByÿÿÞ@x@ÇA ARFàEB­õaRäFõõaRG =áRö´€=áR"½Hà@V°€@å ±ãƒ0àÓ”PR}@ïQa ³vD©Iàb¯€bá jå±J B@jÀB{è oöÇ@x@ßA$AjKàEÉhöajäfööajL =ájUl€=ájå ±Mà@¯k€@ã Åå±NàBàB$ ƒäŸÿÞO B@JÀBÿÿ @x@¿AAJPàEO$÷aJâµ÷÷e±Qà=Ô¤FÿÿQèR =áˆ?*€{å"±Sà@Œ%€@å ±ë câ ó屄BóTàbï¡â ó!¨å ±U B@a¨ÀBñ@x@ßA$A¨VàEÖ߀ëå±øùbóW =ájóŸøaæâóïÂ%±Xà@Už€@å,±YàJÁ JA#å±Z B@aRàBå±@x@ÇA AR[àEãVùaâûúúaR\ =áRŒ^€=áR%±]à@ìY€@ÿ ÿÞ óIƒ.ïðp€ %¼a (lB½^àbX bä ©å±_ B@jÀBå±@x@ßA$Aj`àEjúajl¡óë cûûaja =áj€=ä(%-éÚ+cbà@]€@á å±càB¾€Bâ µ$ ƒå ±d B@JÀBëc@x@¿AAJeàEñÍ€ˆâµüüe±fà=và„FöÇg =áˆáÓ€{áˆ% áˆ"óhà@2Ï€@ÿ ÿÞ!ˆ @1a0ßÿ/¼â ó屌žAëiàb‘΀bᨠb᨟ÿ j B@a¨ÀBå±@x@ßA$A¨kàEx‰ûbóÿÿnbýþból =ájœIü`=ájå ±mà@þG€@å,±nàJjÀJA#£½ëco B@aRàBÿÿ$X@x@ÇA ARpàE…ýaäFÿÿaRq =áR:€=å"±rà@•€@ëc$@t$ðUÞ}$Qâa ò4B½sàb bá jå±t B@jÀBÿÿ›ò@x@ßA$AjuàE ¼€ëàˆå ±@Av =áj€¿€=â½%-ä(%±wà@Û¾€@å$±xàB@ BàÁµå±y B@JàB âø,¯öÇ@x@¿AAJzàE’wþbµàˆá J!º`={à=Š„Fnüy| =@®€Áƒ}€{áˆ% áˆ%±}à@Ðx€@—ÿÿ,â óå±VÆAë~àb/ bá ë!¨ £!n@ÿÿ…* B@`æÀBñ@x@ßA$A¨€àE3ÿa¨ÿÿ "EBó =áj>ó€=ájå ±‚à@ ñ€@â m,Î @€ÿ ÿ¸lƒàJ ÀJ@0â¸ñ„ B@aRàBå±@x@ÇA AR…àE'ªÿ¦˜àˆâ û"_$F† = …€ƒÔ±€=å"±‡à@/­€@å ±1z€ %¯K`ý%²fjÀcÿnbˆàb›¬€bàá" at屉 B@€æÀBå±@x@ßA$AjŠàE­eáj1|!j‹ =áj2i€=ájå ±Œà@h€@ã Å)Ú @ãÅ.`DàBíg€Bá J$àB±Ž B@JÀBëc@x@¿AAJàE4!áJ`=à=µ¤F{屑 =áˆ$'€{äF% å±’à@r"€@—#Qãû!ˆ @ëcâ óå±Çÿnb“àbÐá Âó!¨ báë«c” B@a¨ÀBå±@x@ßA$A¨•àE»Ü€ëÿÿôBó– =ájàœaæáj%páj"´—à@B›€@å,±˜àJ®š JA#d<áRå±™ B@aRàBå±@x@ÇA ARšàEÈSaàˆå ± AR› = …€ƒ}[€=áR(oœà@ØV€@ÿÿ,#VÇ ðàÿõ}ÔP¿ÿ•£€"0DfàbD bàá½å±ž B@€æÀBÿÿ* @x@ßA$AjŸàEOáj Aj  =áj߀=áj%-â½+c¡à@: @á $( @ãÅ"ü@B¢àB›€ƒà B$àB¥±£ B@JÀBå±@x@¿AAJ¤àEÖÊà ˆâ µ );@=¥à=WÝ„F屦 =áˆÂЀ{áˆ% áˆ"ó§à@ Ì€@ÿ ÿàJ)ø @å±+câ ó屆ÂA¨¨àbrË€bᨠbá륱© B@a¨ÀBñ@x@ßA$AjªàE\†bóëc Bó« =ájnF`=è¥å ±¬à@ÏD€@å,±­àJ;ÀJA#å±® B@aRàBå±@x@ÇA AR¯àEjý Ó@Y ˆâ ûAR° =áRaRâ½áR"½±à@r€@ë c3_õ~ƒ/àDðƒ-à0å@¸ÀßVB½²àbÞÿà áå±³ B@jÀBå±@x@ßA$Aj´àEñ¸€ëáj#ò+cµ =áju¼€=áj%- 5`@3ÒA¶à@л€@à~Áå±·àB1àB屸 B@JÀBå±@x@¿AAJ¹àEwt bwàËâ µ2c@=ºà=ù¤Få±» =AT€Ádz€{äF% áˆ"ó¼à@±u€@ü y!ˆ @å±Kâ óå±{¤Aë½àb báÁë!¨ bå ±¾ B@`æÀBå±@x@ßA$Aj¿àEþ/ a¨å±BóÀ =áj$ð€=ájä^(fÁà@…î€@å,±ÂàJñí J ¥(ÁRëcà B@aRàBå±@x@ÇA ARÄàE § aRï+â ûARÅ =áRÁ®€=äFáR"½Æà@ª€@å ±C} ‘úD…¯ä}àÿ8,Çàbˆ©€bá jëcÈ B@jÀBå±@x@ßA$AjÉàE’b ajàˆá j@u%±Ê =ájf€=ájå ±Ëà@^e€@à~Âr$( @ãÅ"üDÌàBÂdàB$àB«cÍ B@JàB@ÙâøöÇ@x@¿AAJÎàE áJ`=Ïà=š0ƒ|ÿÿ$XÐ =@®€Á $€{áˆ% å±Ñà@S€@å ±ë câ óå±¶ÿ>Òàbµá Áë!¨ £áë«cÓ B@`æÀBñ@x@ßA$A¨ÔàE Ù€ëñBóÕ =ájÊ™aæå"±Öà@+˜€@ÿ,ÿ ×àJ—— JA#â¸å±Ø B@aRàBå±@x@ÇA ARÙàE­PaãÇä FARÚ =áR[X€=áR%±Ûà@µS€@ñ S%¯ ð ÁƒÈ`DfÜàb! bàáöÇÝ B@jÀBå±@x@ßA$AjÞàE4 áj9%±ß =ájÄ€=ä(%-ä(1àà@  @â rå±áàB€€ƒá J$ ƒäŸÿÞâ B@JÀBëc@x@¿AAJãàE»Ç€ˆàËâ µ`=äà=<Ú„Fëcå =ÀÁ³Í€{áˆ% áˆ"óæà@õÈ€@ÿÿ!š @ÂÓŸÿQèâ ó屸AëçàbW bᨠb᨟ÿ,è B@a¨ÀBÿÿ; @x@ßA$A¨éàEAƒbóëcBóê =ájtC`=ä^(fëà@ÔA€@è '*Q#j`ü yìàJ@ÀJ@/å±í B@aRàBÿÿ; @x@ÇA ARîàEOú Ó@Y ˆâ û 6$Fï  +€ƒñá$R"½ðà@Ký€~ÿ ÿ8,$F!Râ½cïð‘2å¯ào Œà‹B½ñàb·ü€bàáå±ò B@€æÀBå±@x@ßA$AjóàEÖµ,äfAjô =ájZ¹€=ä(%-ä(%±õà@¶¸€@à~Á!j @ãÅ"ü@BöàBàBå±÷ B@JÀBŒ@Ù¢oÀ…ëc@x@¿AAJøàE\qbwÿÿÞ`=ùà=ݤF|üyú =@®€ÁTw€{áˆ% áˆ"óûà@—r€@å ±$F @âó6Çâ óå±/ôA¨üàbü¡â ó!¨ bá륱ý B@`æÀB ?á ¨ÿÿô@x@ßA$A¨þàEã,a¨àˆä ^ Bóÿ =áj„1€=áj$ájB´#;à@ç/€@å,±àJSÀJ!áëc B@aRàBÿÿ @x@ÇA ARàEjè Ó $Àˆá R!!AR =áð€=ç"½à@zë€@ÿÿ s`‘26ÁTŸÿþ ·àñ^B½àbæê b@&ä#aöâ½áŠ B@jÀBÿÿ @x@ßA$AjàEð£â½""Aj =áj}§€=áj%pâ½%± à@צ€@å$± àB9àB$ ƒáJ«c B@JÀBÿÿB@x@¿AAJ àEw_áJ#*¦@=à=-‚óÿÿ]L =áˆge€{äF% áˆ"ó'„à@´`€@—ÿÿ,â óå±Uÿèðàb bâ ó!¨ £á¨‹c B@a¨ÀBñ@x@ßA$AjàEþa¨ñ$%Bó =ájÛ€=ájå ±à@Ù€@å,±àJíØ JA#äFå± B@aRàByÿÿô@x@ÇA ARàE ’aRàˆå ±&&AR =áR´™€=äFáR"½à@•€@å ± ƒd`Cq€Sqï\‚a ª“D©àb” b@$ájå± B@jÀB|è oÿÿô@x@ßA$AjàE’Máj''Aj =ájQ€=â½%-ä(%±à@mP€@å$± àBÎO€Bá J$ ƒä†ü! B@JÀBëc@x@¿AAJ"àE áJ((`=#à=šƒ|ÿÿô$ =áˆý€{áˆ% áˆ"ó%à@R €@ÿ ÿBë câ óå±aAë&àbµá ÌÉ!¨ £á¨…±' B@a¨ÀBÿÿF„@x@ßA$A¨(àE Ä€ëå±)*Bó) =áj¬„aæájä^+c*à@ƒ€@å,±+àJz‚ JA#äFå±, B@aRàBöÇ@x@ÇA AR-àE­;aàˆÿÿ i A@++AR. =@€ƒ]C€=áR"½/à@µ>€@ö Ç“ÿ"‘ÙÒ‘"ƒFà$ÿÿ* =:B½0àb! bàá½aö⽫c1 B@€æÀBÿÿh°@x@ßA$Aj2àE4÷€ëàˆá j,,Aj3 =ájÄú€=áj%-ä(%±4à@ @à~Áñ5àB€ù€ƒà B$ ƒáJ¥±6 B@JÀBå±@x@¿AAJ7àEº²âµ--`=8à=<Å„Få±9 = ÀÁ¯¸€{è¥% áˆ"ó:à@ø³€@ÿ ÿÞ!ˆ @ñ.Wâ óå±™ÈAë;àbV bᨠbᨅ±< B@`æÀBñ@x@ßA$A¨=àEAna¨öÇ./Bó> =ájf. `=ájå ±?à@È,€@å,±@àJ4ÀJ å±A B@aRàBëc@x@ÇA ARBàEOå€Óéyä F00 €C =áRí€=äFáR"½Dà@_è€@å ±£`S`npaX$`a˜a þB½EàbËç€bá jå±F B@jÀBå±@x@ßA$AjGàEÕ !bàˆá j1#±!jH =ája¤€=áj%-ä(%±Ià@¼£€@à~å±JàBàBå±K B@JÀBå±@x@¿AAJLàE\\"áJ22`=Mà=ݤFüyN =áˆHb€{áˆ% áˆ"óOà@™]€@ù »ë câ óå±Û AëPàbøá Áë!¨ £å ±Q B@a¨ÀBÿÿÞ@x@ßA$A¨RàEã#a¨ëc34BóS =ájØ€=å"±Tà@bÖ€@å,±UàJÎÕ€Jå(ÁRëcV B@aRàBå±@x@ÇA ARWàEðŽ$aRãÇä F55ARX =!J ƒ¡–€=â½å ±Yà@ø‘€@å ±³~ƒ(}±`TF£­ Œà°âB½Zàbd bàáÁjå±[ B@jÀBÿÿ…*@x@ßA$Aj\àEwJ%áj66Aj] =ájN€=ájå ±^à@^M€@á )Ú @ãÅ.`AJ_àB¿LàB$àB«c` B@JÀBå±@x@¿AAJaàEþ&áJ77`=bà=ƒ|å±c =áˆò €{áˆ% å±dà@;€@ÿ ÿ5n!ˆ @'±`¿ÿ,â óå±A¨eàbš€bá ë!¨ báë«cf B@a¨ÀB ô 4  @'@x@ßA$A¨gàE„Áà ˆä ^88Bóh =ájÆ€=G rê×ä ^"´ià@}Ä€@‡@Së'$Ÿÿÿ,jàJéàJ ŠàJå±k B@A*àBÿÿ,@x@ÇA ARlàE }'âû99ARm =@þ€ƒ¾„€=áR(onà@€€@ö ÇÃà ä+Y xI|ƒ}{za r=B½oàb‹ b@$áµå±p B@€æÀBÿÿQè@x@ßA$AjqàE’8(áj::Ajr =áj<€=ä(%-ä(+csà@{;€@â r$( @ãÅ"ü@BtàBÚ:€Bá J$àB¥±u B@JÀBå±@x@¿AAJvàEô€ˆàËä ;;`=wà= ƒ|öüyx = ÀÁ ú€{áˆáˆ"óyà@Tõ€@ÿ ÿ â óå±ÖÏA¨zàbµá È¥!¨ £á륱{ B@`æÀBüy@x@ßA$A¨|àEŸ¯)bóñ<=Bó} =ájÖo*`=áj%pâó"´~à@6n€@ˆ@Sãûå±àJ¢m JA#àJå±€ B@aRàBÿÿL6@x@ÇA ARàE­&+aàˆâ û>>AR‚ =@þ€ƒD.€=å"±ƒà@¡)€@ÿ ÿBÓy­a€üà´Þ£à´ëcNÿüÄ„àb  bàá½å±… B@€æÀBÿÿ]L@x@ßA$!1†àE4â€ëÿÿ5n??Aj‡ =ájÀå€=áj%-â½%±ˆà@ @äåÁ屉àB|䀃à B$ ƒäœyŠ B@JÀB‹ âø)ºÿÿ* @x@¿AAJ‹àEº,âµ@@`=Œà=;°„FÿÿÞ =@®€Á¾£€{äFáˆ%±Žà@ Ÿ€@—ÿÿ,â ó›ÿÿWšêÿüÄàbjž€bâ ó!¨ £á¨–Ç B@`æÀBå±@x@ßA$A¨‘àEAY-a¨å±ABBó’ =ájt.`=ájå ±“à@Ô€@å,±”àJ@ÀJA#â¸å±• B@aRàBå±@x@ÇA AR–àENРÓ@Y ˆå ±CD5\— = +€ƒØ€=äFáRc˜à@cÓ€@å ±ã%¦ß  R"a’ Œà£ÿ; ™àbÏÒ€bä f屚 B@€æÀBå±@x@ßA$Aj›àEÕ‹/bájDDAjœ =áje€=â½%-å±à@ÃŽ€@á 属àB! BàÁÅô$ ƒä…±Ÿ B@JÀBëc@x@¿AAJ àE\G0áJEE`=¡à=ݤFÿÿŠÜ¢ =áˆLM€{áˆ% áˆ"ó£à@˜H€@å ±)ø @ñÃâ óå±ÿ; ¤àbøá ᨠbᨅ±¥ B@a¨ÀBå±@x@ßA$A¨¦àEã1a¨öÇFDB$^§ =ájö€=ájä^+c¨à@YÁ€@å,±©àJÅÀ JA#¥(ÁR屪 B@aRàBöÇ@x@ÇA AR«àEðy2aRàˆå ±HA6!R¬ =áR§€=áR"½­à@ü|€@ù …$F!“â½?7óRNaxVÌäý6Ìa lÿ¯Æ®àbh bàáÁj屯 B@jÀB‡ó ÓÿÿÞ@x@ßA$Aj°àEw53ájIIAj± =áj÷8€=áj%-ä(+c²à@U @å$±³àB³7€ƒá Jå±´ B@JÀBå±@x@¿AAJµàEýðà ˆâ µJJ`=¶à=ƒ|å±· =áˆöö€{áˆ% áˆ"ó¸à@:ò€@ÿ ÿ…*"ó @âóå±ÈtAë¹àbšñ€bá ë!¨ bå ±º B@a¨ÀBÿÿô@x@ßA$A¨»àE„¬4bóëcKLBó¼ =áj¨l5`=ê%±½à@ k€@å,±¾àJwj JA#屿 B@aRàBÿÿL6@x@ÇA ARÀàE’#6aàˆâ ûMMARÁ =áRA+€=â½%pâ½Âà@ž&€@ëcájXXAjð =ájà5€=ä(%-ä(%±ñà@: @á $( @ãÅ(®@BòàBœ4€ƒà B$àB¥±ó B@JÀBŒ áëc@x@¿AAJôàEâíà ˆâ µYY`=õà=dƒ|ÿÿWšö =@®€ÁÇó€{âó% áˆ"ó÷à@ï€@ô )ø @å±+câ ó展ÿ@Òøàb~î€bᨠbá륱ù B@`æÀBÿÿô@x@ßA$A¨úàEi©?bóå±Z[Bóû =ájvi@`=ájê"´üà@Øg€@å,±ýàJDÀJA#â¸å±þ B@aRàBå±@x@ÇA ARÿàEw Aaòñâ û\\AR {Àƒ'(€=áR"½à@‡#€@å ±3$paQ¥ïp[€SÕÿÿ¦EDfàbó"€bàáÇå± B@jÀBå±`x@9 Aj @`EýÛ€ˆáóá j]]Aj =áj‰ß€=å"±à@äÞ€@á å±àBE BàÁÁJå± B@aàBå±@x@¿AAJ àE„—Bbµàˆá J^^@= à=ª?`=ëc =áˆp€{áˆ% å± à@Á˜€@å ±!ˆ @å±(¥â óå±á}Bó àb  báᨠbå ± B@a¨ÀBñ@x@ßA$AjàE SCajñ_`Bó = …`ƒ0D`=ä^%±à@’€@ÿ,ÿ àJþ J å ± B@€ÎÀBå±@x@ÇA ARàEÊ€ˆáÛâ ûaaAR =áRÍÑ€=áR%±à@(Í€@ñ CÿJ«qKƒHàÿÿWššB½àb”Ì€bàá½å± B@jÀBñ@x@ßA$AjàEŸ…Ebàˆá jbbAj =áj/‰€=ä(*ßä(+cà@Šˆ€@â r$( @ãÅ"ü@BàB뇀BàÁÁJå± B@JÀBå±@x@¿AAJàE&AFaJàËá Jc"q@=à=«¤Fÿÿh° =áˆG€{áˆ% áˆ"ó!à@cB€@ñ ë câ óå±>áA¨"àb¡á¨ £áë«c# B@a¨ÀBñ@x@ßA$Aj$àE¬ü€ëëcdeBó% =ájÕ¼Gaæájë c&à@7»€@ë,c'àJ£º J ëc( B@aRàBÿÿ$X@x@ÇA AR)àEºsHaàˆâ ûffAR* =áRg{€=å"±+à@Æv€@ë cSH@B€:ƒEàÔP€@Œàƒ´B½,àb2Àb@&ájå±- B@jÀBÿÿ* @x@ßA$Aj.àEA/IajäfggAj/ =@Ý€ƒÁ2€=â½%pä(%±0à@ @à~Ånå±1àB}1€ƒà Bå±2 B@€ÆÀBå±@x@¿AAJ3àEÇꀈáÓâ µhh`=4à=Hý„Fÿÿô5 =ሳð€{áˆ% áˆ%±6à@ì€@ü y!ˆ @ëcâ óå±'FAë7àbcë€báÁë!¨ bå ±8 B@a¨ÀBëc@x@ßA$A¨9àEN¦Jbóå±ijBó: =ájkfK`=ájå ±;à@Íd€@å,±<àJ9ÀJA#äFñ= B@aRàBå±@x@ÇA AR>àE[Laàˆâ ûkkAR? =áR %€=å"±@à@d €@ë ccÃPQöþå" â(RààB½AàbЀbä få±B B@jÀBëc@x@ßA$AjCàEâØ€ˆájllAjD =ájfÜ€=â½å ±Eà@ÂÛ€@á $( @ãÅ«cFàB" BàÁäàB¶ÇG B@JÀBå±@x@¿AAJHàEi”Mâµmm`=Ià=ê¤Få±J =áˆQš€{áˆ% å±Kà@Ÿ•€@—ÿ ÿBë câ óå±DïBóLàb bᨠ£áëiXFM B@a¨ÀB”ÿ m8p @'@x@ßA$A¨NàEðONa¨ñnoBóO =ájO`=ájå ±Pà@~€@å,±QàJê  J þå ±R B@A*àBÿÿ¦@x@ÇA ARSàEýÆà ˆå ±ppART =áRšÎ€=áR(oUà@ýÉ€@ë cs`#G7ƒFtð"*Kà.ÖB½VàbiÀb@'çå±W B@jÀByâ ½ÿÿ @x@ßA$AjXàE„‚Pbàˆá jqqAjY =áj†€=áj%-ä(!Zà@l…€@å$±[àBÌ„€BàÁå±\ B@JÀBå±@x@¿AAJ]àE >QaJàËá Jrr`=^à=ŒP„Få±_ =áˆóC€{áˆ% áˆ"ó`à@E?€@ë câ ó届$Aëaàb§á ᨠ£å ±b B@a¨ÀBå±@x@ßA$A¨càE‘ù€ëëcstBód =áj³¹Raæê(feà@¸€@å,±fàJ€· J äFëcg B@aRàBëc@x@ÇA ARhàEŸpSaàˆâ ûuuARi = …€ƒXx€=â½ç"½jà@³s€@ÿÿ* ƒ" R U‚RþÿÿÞT.B½kàb bàá½å±l B@€æÀBÿÿF„@x@ßA$AjmàE%,TájvvAjn =áj®/€=â½å ±oà@  @á ëcpàBj.€ƒå(ÁJ/l ƒäŸÿÞq B@JÀBÿÿ¦@x@¿AAJràE¬ç€ˆàËâ µww`=sà=-ú„FÿÿF„t =ሜí€{âó% å±uà@çè€@ÿ ÿ5në câ óå±ðÜBóvàbH báᨠ£á¨‹cw B@a¨ÀBöÇ@x@ßA$A¨xàE3£UbóëcxyBóy =ájecV`=âóä^%±zà@Æa€@å,±{àJ2ÀJA#å±| B@aRàBå±@x@ÇA AR}àE@Waàˆâ ûzzAR~ =áRê!€=â½áR%±à@L€@ë c“+] ð"ƒ+Q/£tÈÿÿF„€ÐB½€àb¸ b ájå± B@jÀBÿÿ* @x@ßA$Aj‚àEÇÕà ˆá j{{Ajƒ =ájGÙ€=áj%-ä(+c„à@£Ø€@á å±…àBàB屆 B@JÀBëc@x@¿AAJ‡àEN‘Xâµ||`=ˆà=ϤF屉 =áˆ>—€{áˆ% áˆ"óŠà@ˆ’€@ü y!ˆ @öÇâ óå±$"Aë‹àbêá 屌 B@a¨ÀBå±@x@ßA$A¨àEÕLYa¨ÿÿ…*}~BóŽ =áj Z`=å"±à@k €@å,±àJ×  JA#äF dêç¿ÿ…*‘ B@aRàBñ@x@ÇA AR’àEâÀˆŒ@Y ˆä FAR“ =áRË€=áR"½”à@êÆ€@ë c£ä£ð£ð0á1zUŽƒ@¸ÀË B½•àbV bá jå±– B@jÀBëc@x@ßA$Aj—àEi[bàˆá j€€Aj˜ =ájõ‚€=ä(å ±™à@Q @à~ÃÅ$( @âr(®AJšàB±€ƒà Bå±› B@JÀBå±@x@¿AAJœàEï:\áJ`=à=qMY`=ñž =áˆÜ@€{âó%p屟à@&<€@ÿ ÿ5në câ óå±oøA¨ àb‹;€bᨠ£áë«c¡ B@a¨ÀBüy@x@ßA$A¨¢àEvö€ˆñ‚#ÇA£ =áj¥¶]a¨ájê"´¤à@µ€@å,±¥àJq´ JA#¥(Ã>屦 B@aRàBå±@x@ÇA AR§àE„m^aîèä F„„AR¨ =áR.u€=áR%±©à@Œp€@ü y³-à`UÿÃ-àA àHB½ªàbøo€bá j屫 B@@]àB}öÇ@x@ßA$Aj¬àE )_ajàˆá j……Aj­ =áj’,€=å"±®à@ï+€@áéÂr屯àBN BàÁÀBå±° B@JÀBå±@x@¿AAJ±àE‘ä€Ëàˆá J††`=²à=÷„Fëc³ =áˆ}ê_aÆáˆ% å±´à@Ìå€@ë câ óå±+ºBóµàb- báᨠ£å ±¶ B@a¨ÀBå±@x@ßA$A¨·àE `a,ñ‡@ª%±¸ =ájC`a`=ä^%±¹à@£^€@‡@Såf'%òÿÿ¦ºàJÀJA#àJå±» B@aRàBå±@x@ÇA AR¼àE%baáÛâ û‰‰AR½ =@þ€ƒÌ€=áR%±¾à@-€@ñ Ãð%|p1àð€àUa )0 ¿àb™ b@$ájå±À B@€æÀBñ@x@ßA$AjÁàE¬Òà ˆá jŠŠAj =áj(Ö€=ä(+"ä(!Ãà@…Õ€@â rå±ÄàBèÔ€Bá Jå±Å B@JàB@Ùá=Åÿÿ$X@x@¿AAJÆàE3Žcⵋ‹`=Çà=´¤Få±È =@®€Á”€{áˆ% áˆ"óÉà@n€@ÿ ÿB$F @ñÃâ óå±÷AëÊàbÏá å±Ë B@`æÀBå±@x@ßA$A¨ÌàE¹Ida¨ëcŒBóÍ =ájØ e`=áj%páj%±Îà@8€@â m!j @å±ÏàJ¤ JA#â¸å±Ð B@aRàBÿÿ5n@x@ÇA ARÑàEÇÀà ˆä FŽŽARÒ =áRrÈ€=å"±Óà@ÏÀ@ë cÓ•Q•ƒ/ïðƒ.àðda žñB½Ôàb; bá jå±Õ B@jÀBÿÿ5n@x@ßA$AjÖàEN|fbå2á jAj× =ájÞ€=â½å ±Øà@; @à~ÃÅå±ÙàBš~€ƒá J `ƒéºœyÚ B@JÀBëc@x@¿AAJÛàEÔ7gaJàˆá J`=Üà=UJ„Fÿÿ/¼Ý =áˆÅ=€{âó%päF%±Þà@9€@—ÿ ÿÞ!ˆ @å±?mâ óå±:Bóßàbp8€báÁë!¨ b᨜yà B@a¨ÀBöÇ@x@ßA$A¨áàE[ó€ˆå±‘’Bóâ =áj~³haæâó%páj%±ãà@Þ±€@å,±äàJJÀJA#å±å B@aRàBå±@xã 9A ARæ @`Ehjiaàˆâ û““ARç =áRr€=â½áR"½èà@}m€@ë c ã‘"â%¶à å `Ì ²UB½éàbél€bä ©å±ê B@a$àBëc@x@ßA$AjëàEï%jáj””Ajì =áj{)€=â½å ±íà@Ù(€@á $( @ãÅ"üAJîàB7 Bâ,äàB¥±ï B@JÀBå±î 5@¿AAJð @€Evá€Ëàˆâ µ••`=ñà=÷¤Få±ò =áˆZç€{áˆ% äF"óóà@®â€@ÿ ÿÞ)ø @å±+câ óå±§A¨ôàb báᨠbá륱õ B@abÀBô -  @'@x@ßA$A¨öàEýœkbóñ–—Bó÷ =áj]l`=ájä^"´øà@k[€@å,±ùàJ×Z J þå ±ú B@A*àByÿÿbþ@x@ÇA ARûàE maàˆâ û˜˜ARü =áR­€=áR"½ýà@€@ü yóä1 .àpÙ}0ïüyõkB½þàbr€bàá½å±ÿ B@jÀBÿÿ§V@x@ßA$Aj¦N`E‘Ïà ˆá j™™Aj =áj!Ó€=áj%-ä(1à@Ò€@å$±àBÝÑ€Bá J)º ƒä‹c B@aàBå±@x`¿ =AJ @€E‹nbµàËá Jšš`=à=™„Få± =ሑ€{áˆ% áˆ"óà@TŒ€@—ëcâ óå±Q™AëH¹à£´á Âó!¨ £á¨‹c ¥@abÀBñ@x@ßA$A¨ àEžFoa¨ëc›œBó =àÆÖp`=ê%± à@9€@å,±àJ¥ JA#ëc B@aRàBÿÿ5n@x@ÇA ARàE¬½à ˆâ ûAR =áRcÅ€=â½ç"½à@ÀÀ€@ñ.à` £à0æ£tð€äa •ÿB½àb, bá jå± B@jÀBÿÿ; @x@ßA$AjàE2yqbàˆá jžžAj = …€ƒ³|€=â½%-ä(%±à@ @à~ÃÅëcàBw{€ƒå(ÀB$ ƒä…± B@€ÆÀBƒ@Ùâø)ºöÇ@x@¿AAJàE¹4raJÿÿ⟟`=à=:G„Fÿÿ$X =@®€Á©:€{âó% áˆ"óà@ú5€@ëcâ óå±1 AëàbY bá ë!¨ £á¨…± B@`æÀBå±@x@ßA$A¨ àE@ð€ëàˆâ ó  Bó! =ájÛô€=âóä^%±"à@<ó€@å,±#àJ¨ò J â¸öÇ$ B@aRàBÿÿ @x@ÇA AR%àEÇ«sbûã„á R¡¡AR& =áRp³€=áR"½'à@Ë®€@è o!“î!%± ƒ0ðäƒH6È0àÿÿÿÃÐPýB½(àb7 bá jå±) B@jÀBÿÿ¦@x@ßA$Aj*àEMgtajàˆá j¢¢Aj+ =ájÅj€=ä(%-ä(%±,à@! @áéÅnå±-àBi€ƒàÁÀB$ ƒä…±. B@JÀBëc@x@¿AAJ/àEÔ"uáJ££`=0à=ܤFôÿÿWš1 =áˆÄ(€{áˆ% áˆ"ó2à@$€@œâ ó @âóå±ôAë3àbp#€bᨠbᨅ±4 B@a¨ÀBöÇ@x@ßA$A¨5àE[Þ€ˆñ¤¥Bó6 =ájžvaæä^%±7à@@å,±8àJZ Jã >å±9 B@aRàBå±@x@ÇA AR:àEhUwaáÛä F¦¦AR; =áR]€=áR"½<à@pX€@”â ½å ±#þîßPa€ó"Q`  üÃB½=àbÜW€bàáÄfå±> B@jÀBÿÿñ`@x@ßA$Aj?àEïxáj§§Aj@ =ájo€=ä(%-ä(%±Aà@Ë€@â rå±BàB+ Bá Jå±C B@JÀBå±@xA 9AAJD @€EvÌ€Ëàˆâ µ¨¨`=Eà=÷¤Fÿÿ F =áˆfÒ€{áˆ% áˆ"óGà@°Í€@å ±ö Çâ óå±X1!ëHàb báÂó!¨ £å ±I B@abÀBå±@x@ßA$A¨JàEü‡ybóñ©ªAK =áj#Hz`=ä^%±Là@ƒF€@å,±MàJïE JA#ëcN B@aRàBÿÿÞ@x@ÇA AROàE ÿà ˆâ û««ARP =áR°{á$R"½Qà@ €@Œå±3•ƒ-ïð"å8$þpó$a  ÿF„Ràbv€bá jå±S B@jÀBÿÿB@x@ßA$AjTàE‘º€ˆå2á j¬¬AjU =áj!¾€=ä(å ±Và@}½€@à~ÃÅå±WàBݼ€Bá J `ƒä‹cX B@JÀBå±@x@¿AAJYàEv|bwàˆá J­­`=Zà=˜ˆ„FÿÿÞ[ =áˆ|€{äF% å±\à@Rw€@—â ó @ëcâ ó屩ÿF„]àb³á Áë!¨ bᨋc^ B@a¨ÀBå±@x@ßA$A¨_àEž1}a¨å±®¯Bó` =ájÐñ€=âó)Ïáj%±aà@1ð€@å,±bàJï J å±c B@aRàBå±@x@ÇA ARdàE¬¨~aRãÇâ û°°ARe =áRR°€=â½áR%±fà@°«€@å ± C`ùå9`p%±ØA àöòHogàb bàá½å±h B@@]àBÿÿB@x@ßA$AjiàE2dajáóá j±±Ajj =áj¢g€=â½å ±kà@ÿf€@á å±làB^ BàÁÁJ$ ƒä…±m B@JÀBå±@x@¿AAJnàE¹€áJ²²`=oà=:2ƒ|å±p =ሥ%€{áˆ% äF"óqà@ð €@å ±ë câ óå±RBóràbU bᨠ£á¨…±s B@a¨ÀBöÇ@x@ßA$A¨tàE@Û€ëàˆâ ó³³Bóu =ájÙ߀=ájä^%±và@<Þ€@å,±wàJ¨Ý JA#å±x B@aRàBÿÿ,@x@ÇA ARyàEÆ–âû´´ARz =áRnž€=áR"½{à@Ï™€@å ±S%¦â$e¶:puva MB½|àb; bä ©å±} B@jÀBÿÿF„@x@ßA$Aj~àEMR‚ájµµAj =ájÉU€=áj%-ä(/)€à@# @â rå±àB…T€ƒà B层 B@JÀBå±@x@¿AAJƒàEÔ ƒáJ¶¶`=„à=ܤFóöÇ… =áˆÄ€{áˆ% áˆ"ó†à@€@—ëcâ óå±éóAë‡àbp€bᨠ£å ±ˆ B@a¨ÀBëc@x@ßA$A¨‰àE[É€ˆöÇ·¸BóŠ =áj“‰„aæê%±‹à@õ‡€@å,±ŒàJaÀJA#å± B@aRàByÿÿÞ@x@ÇA ARŽàEh@…aàˆç ¹¹AR =áR$H€=â½ç"½à@xC€@ö Çcuwƒ~àTþðTýð0a `hB½‘àbäB€bàáÊå±’ B@jàBÿ ÿ œŸÿ œ@x@ßA$Aj“àEïûà ˆá jººAj” =ájÿ€=ájå ±•à@Ùþ€@á å±–àB;àBå±— B@JÀBå±@x@¿AAJ˜àEv·†bµç…á J»»`=™à=÷¤FÿÿL6š =áˆn½€{áˆ% å±›à@²¸€@ å ±ë câ óå±ÄšBóœàb báÂó!¨ £å ± B@a¨ÀBÿÿbþ@x@ßA$A¨žàEür‡a¨ñ¼½BóŸ =áj 3ˆ`=å"± à@k1€@å,±¡àJ×0 J å±¢ B@aRàBå±@x@ÇA AR£àE êà ˆâ û¾¾AR¤ =áR¾ñ€=â½å ±¥à@í€@ë cså:´uv%¸e¸à´B½¦àb†ì€bá jå±§ B@jÀBÿÿ,@x@ßA$Aj¨àE¥‰bÿÿ“¿¿Aj© =áj!©€=â½å ±ªà@{¨€@á 屫àBݧ€Bä ƒäèF¬ B@JÀBÿÿ5n@x@¿AAJ­àEaŠaJàËâ µÀÀ`=®à=˜s„Fÿÿüį =ሠg€{âó% å±°à@Xb€@˜å ±!ˆ @öÇâ óå±sBó±àb·á å±² B@a¨ÀBëc@x@ßA$A¨³àEž‹a¨ëcÁÂBó´ =ájËÜ€=âóä^+cµà@-Û€@å,±¶àJ™Ú JA#å±· B@aRàByëc@x@ÇA AR¸àE«“ŒaRàˆâ ûÃÃAR¹ =áR`›€=áR+cºà@»–€@å ±ƒýð!8%µ…µÿÿ; ʇB½»àb' bàá½å±¼ B@jÀBå±@x@ßA$Aj½àE2OájÄÄAj¾ =Àƒ¾R€=ä(%-ä(1¿à@ @å n$( @ån"ü@BÀàBzQ€ƒá J)ºàB¥±Á B@JÀBëc@x@¿AAJÂàE¹ ŽáJÅÅ`=Ãà=:ƒ|ëcÄ =ሩ€{áˆ%páˆ"óÅà@ö €@å ±ë câ óå±nuA¨ÆàbU bᨠ£áë¶ÇÇ B@a¨ÀBå±@x@ßA$A¨ÈàE@Æ€ë屯ÇBóÉ =áje†aæä^"´Êà@Æ„€@å,±ËàJ2ÀJA#å±Ì B@aRàBÿÿÞ@x@ÇA ARÍàEM=aàˆå ±ÈÈARÎ =áRêD€=áR"½Ïà@E@€@å ±“1 Dð!†å:dpXõöÇ[xB½Ðàb±?€bàáå±Ñ B@jÀBå±@x@ßA$AjÒàEÔøà ˆá jÉÉAjÓ =áj\ü€=ä(%-ä(%±Ôà@·û€@å$±ÕàB Bá Jå±Ö B@JÀBå±@x@¿AAJ×àEZ´‘âµÊÊ`=Øà=ܤFå±Ù =áˆOº€{âó% áˆ"óÚà@˜µ€@ÿ ÿÞ': @ëcâ óå±ÿŠAëÛàböá Âó!¨ bå ±Ü B@a¨ÀB ô 4 ÿÿ¦@x@ßA$A¨ÝàEáo’a¨ñËÌBóÞ =áj0“`=ájë cßà@t.€@å,±ààJà- J å±á B@aRàBå±@x@ÇA ARâàEïæ€ˆãÇä FÍÍARã = …€ƒ î€=áR"½äà@óé€@è o"½!“â½£õwàÿ£à£Ïð£ïðÿÿô¸6$©åàb_ bá jëcæ B@€æàBâ ½öÇ@x@ßA$AjçàEu¢”bàˆá jÎÎAjè = …€ƒ¦€=å"±éà@]¥€@áéÃÅ!j%òãÅ(®AJêàB½¤€BàÁÀB$àB«cë B@€ÆÀBå±@x@¿AAJìàEü]•áJÏÏ`=íà=}p„F{ÿÿ…*î =áˆôc€{áˆ% å±ïà@6_€@£ñ ë câ óå±ÄA¨ðàb˜^€bᨠ£áë«cñ B@a¨ÀBöÇ@x@ßA$A¨òàEƒ–a¨ëcÐÑBóó =áj Ù€=áj$áj"´ôà@Ø€@ÿ,ÿ5nõàJn× JA#å±ö B@aRàBå±@x@ÇA AR÷àE—aRáÛä FÒÒARø = …€ƒV˜€=å"±ùà@ “€@èo!“å± ³õZ‚ðSþ"å:6È´ Œà£ÿ¦úàb  bàáÄfå±û B@€æÀB‘å±@x@ßA$AjüàEL˜ájÓÓAjý = …€ƒ›O€=â½%-â½&¹þà@÷N€@â rå±ÿàBW Bá Jå±!  €ÆÀBå±@x@¿AAJàEž™áJÔÔ`=à=ƒ|å± =ሖ €{áˆ% áˆ(¥à@Ø€@å ±!ˆ @âóå±ÿ¦àb: bᨠbå ± B@a¨ÀBå±@x@ßA$A¨àE$ÀëëcÕÖBó =ájVƒšaæáj$áj%± à@·€@ë,c àJ#ÀJA#å± B@aRàBÿÿ @x@ÇA AR àE2:›aàˆå ±××AR =áRàA€=å"±à@2=€@”è o"½!“â½ÃÿàDð"àTþð£å:ðàa Bÿq àbž<€bàáå± B@jÀBÿÿô@x@ß =Aj @`E¹õ€ˆå2á jØØAj = ?`==ù€=â½å ±à@™ø€@à~Á!j @ãÅ"üEôàBù÷€Bá Jå± B@€ÆÀBå±@x@¿AAJàE?±œâµÙÙ`=à=À¤Fÿÿ  = ÀÁ8·€{âó% äF%±à@z²€@å ±ë câ ó屿_A¨àbÛá Áë!¨ £áë«c B@`æÀB¤è ¥ñ@x@ßA$A¨àEÆla¨å±ÚÛBó =áj-ž`=âó$áj"´à@e+€@å,±àJÑ* J þå ± B@aRàBå±@x@ÇA AR!àEÓãà ˆä FÜÜAR" =áR‰ë€=áR"½#à@Üæ€@ñ Óý€D³å:U"ï¯a –ÿ2z$àbH bä ©ëc% B@jÀBå±@x@ßA$Aj&àEZŸŸbáóá jÝÝAj' =áj梀=áj%pâ½+c(à@B @à~ÃÅå±)àB¢¡€ƒà B$ ƒäŸÿ,* B@JÀBå±@x@¿AAJ+àEáZ áJÞÞ@=,à=bm„Få±- =áˆÙ`€{áˆ% áˆ"ó.à@\€@å ±!ˆ @è¥â óå±2ÿ8b/àb}[€bᨠb᨟ÿB0 B@a¨ÀBå±@x@ßA$Aj1àEh¡a¨ñßàBó2 =ájšÖ€=çå ±3à@úÔ€@å,±4àJfÀJA#ëc5 B@aRàBå±@x@ÇA AR6àEu¢aRàˆä FááAR7 =áR•€=â½áR"½8à@}€@ü yã:Gïp€¯:a D©9àbé€bàáÄ©å±: B@jÀBÿÿ@Ò@x@ßA$Aj;àEüH£ájââAj< =áj„L€=áj%-ä(%±=à@àK€@á $( @ãÅ(®@B>àB@àBå±? B@JÀBå±@x@¿AAJ@àEƒ¤aJç…â µãã`=Aà=ƒÇxöÇB =ÀÁo €{áˆ% áˆ"óCà@¹€@è ¥ë câ óå± ãA¨Dàb báÂó!¨ £á륱E B@a¨ÀB˜ëc@x@ßA$A¨FàE À€ëëcäåBóG =áj8€¥aæå"±Hà@˜~€@å,±IàJÀJA#å±J B@aRàBå±@x@ÇA ARKàE7¦aàˆâ ûææARL =áRÁ>€=â½å ±Mà@:€@å ±ó­:¯9ña"Ÿÿ;/à"a Hy…Nàb‹9€bàá½å±O B@jÀBå±@x@ßA$AjPàEòà ˆá jççAjQ =áj*ö€=â½%pä(%±Rà@†õ€@á å±SàBæô€Bä ƒä‹cT B@JÀBÿÿ @x@¿AAJUàE$®§âµèè`=Và=¥¤Fÿÿ; W =ሴ€{âó% áˆ%±Xà@_¯€@å ±!ˆ @ëcâ óå±ìmYàbÀá Âóå±Z B@a¨ÀBå±@x@ßA$A¨[àE«i¨a¨ëcéêBó\ = …`ƒÒ)©`=âóä^(f]à@2(€@å,±^àJž' J å ±_ B@€ÎÀBå±@x@ÇA AR`àE¸àà ˆä FëëARa =áRkè€=áR"½bà@Äã€@ÿÿ5nñI~ƒ(}q™ï"àuðàlëD©càb0 bá jå±d B@jÀB‚è oÿÿ,@x@ßA$AjeàE?œªbàˆá jììAjf =ájÏŸ€=ä(%-ä(%±gà@, @å n$( @ån"ü@BhàB‹ž€ƒàÁäàB¥±i B@JÀBëc@x@¿AAJjàEÆW«áJíí`=kà=Gj„Fñl =ሺ]€{áˆ%páˆ"ómà@Y€@ÿ më câ óå±ÈA¨nàbbX€bᨠ£áë«co B@a¨ÀB â ó"ôÿÿÞ@x@ßA$A¨pàEM¬a¨å±îïBóq =áj|Ó€=ä^"´rà@ÛÑ€@å,±sàJGÀJ þå ±t B@aRàBÿÿL6@x@ÇA ARuàEZŠ­aRàˆä FððARv =áR ’€=áR"½wà@f€@ëc¤$õ‚ä4õƒà"äƒ(a ”9B½xàbÒŒ€bàáÄfå±y B@jÀBëc@x@ßA$AjzàEáE®ájññAj{ =ájaI€=ä(%-ä(%±|à@¾H€@å$±}àB Bá J)º ƒä‹c~ B@JÀBå±@x@¿AAJàEg¯áJòò`=€à=é¤Få± =áˆX€{âó% áˆ"ó‚à@£€@ëcâ óå±0ËAëƒàb bᨠ£á¨–Ç„ B@a¨ÀBœå±@x@ßA$A¨…àEî¼€ëñóôBó† =áj}°aæájë c‡à@y{€@å,±ˆàJåz JA#ñ‰ B@aRàBå±@x@ÇA ARŠàEü3±aãÇå ±õõAR‹ =áR—;€=áR"½Œà@ø6€@ˆÿÿ,#ð)”Pnñ-0àdå±"ÍB½àbdÀbA;áj屎 B@jÀBÿÿQè@x@ßA$AjàE‚ï ë@Y ˆá jööAj =ájó€=å"±‘à@pò€@á鯹ëc’àBÎñ€BàÁÀB屓 B@JÀBå±@x@¿AAJ”àE «²bµàˆá J÷÷`=•à=н„Fñ– =áˆõ°€{áˆ% å±—à@A¬€@—â óë câ ó屆?Bó˜àb¥á ᨠ£å ±™ B@a¨ÀBñ@x@ßA$A¨šàEf³a¨ëcøùBó› =áj³&´`=ájå ±œà@%€@å,±àJƒ$€Jã >ÿÿ@Òž B@aRàBÿÿL6@x@ÇA ARŸàEÝ€ˆâûúúAR  =áRQå€=å"±¡à@¥à€@ÿÿÞ3GÈËcþ‚ÝààÿåB½¢!࣠bàá½å±£ B@jÀB‡è oñ@x@ßA$Aj¤àE$™µbàˆä fûûAj¥ =áj´œ€=â½*ßä(+c¦à@ @â rå±§àBp›€ƒàÁÁJ屨 B@JÀBå±@x@¿AAJ©àE«T¶aJÿÿ%£üü`=ªà=,g„F屫 =ሣZ€{áˆ% áˆ(¥¬à@ëU€@›å ±!ˆ @öÇâ óå±[Aë­àbK bᨠbå ±® B@a¨ÀBñ@x@ßA$A¨¯àE1·a¨¼è ¥ ýþBó° =ájYЀ=ájå ±±à@¼Î€@å,±²àJ(ÀJ ®ÀÉÂûå±³ B@aRàBy@ó©²ÀBÿÿ.Q @x@ÇA AR´àE?‡¸aRâûÿÿARµ =áR€=å"±¶à@_Š€@ñ CýîmpLï6À¤$Eºa =×B½·àbˉ€bá j屸 B@jÀBå±@x@ßA$Aj¹àEÆB¹ajå2å ±!_Ajº =ájNF€=â½å ±»à@¬E€@à~Â/$( @ãÅ4AJ¼àB Bá Jå±½ B@JàB@Ùâµ&ýÿÿQè@x@¿AAJ¾àELþ€Ëàˆá J'N@=¿à=ѤFvÿÿ,À =@®€Á=ºaÆâó% å±Áà@‰ÿ€¼ë câ óå±™%A¨Âàbèá Áë!¨ £áë±à B@`æÀBñ@x@ßA$AjÄàEÓ¹,ñbóÅ =ájz»ajâóå ±Æà@fx€@â m,Î @€ÿ ÿë®ÇàJÒw J@0â¸å±È B@aRàBå±@x@ÇA ARÉàEá0¼aãÇâ ûaRÊ =áR„8€=áR(oËà@Õ3€@óÓ!RèoSVÀ ÿ‚Þàþïnp3ÿÿ]LöÇB½ÌàbA bàá½å±Í B@jÀB‹å±@x@ßA$AjÎàEgì€ëáóá j rÏ =ájëï€=áj%pä(+cÐà@E @à~Áå±ÑàB§î€ƒà B$ ƒä–ÇÒ B@JÀBëc@x@¿AAJÓàEî§½bµàˆá Je±Ôà=oº„Fÿÿ$XÕ =áˆæ­€{áˆ% áˆ"óÖà@+©€@Ÿëcâ óå±R5Aë×àbЍ€báÁë!¨ £á¨–ÇØ B@a¨ÀBÿÿ @x@ßA$A¨ÙàEuc¾a¨ñbóÚ =áj‘#¿`=ç$áj(fÛà@ô!€@ÿ,ÿ¦ÜàJ_ Já Rå±Ý B@aRàBå±@x@ÇA ARÞàE‚Ú€Óàˆâ û  aRß =áRBâ€=â½áR"½àà@–Ý€@ñ c('LU¡1ßàKoà£ïB½áàb bàá½å±â B@jàBå±@x@ßA$AjãàE –Àbàˆá j  ajä =áj™€=áj%-â½%±åà@瘀@à~Á屿àBIàB$ ƒä…±ç B@JÀBå±@x@¿AAJèàEQÁaJç…á J  e±éà=d„Fzëcê =ሄW€{áˆ% áˆ"óëà@ÈR€@ÿÿ; â óå±Aëìàb, báÁë!¨ £á¨…±í B@a¨ÀBñ@x@ßA$A¨îàE Âa¨ëc  bóï =ájCÍ€=å"±ðà@¥Ë€@ë,cñàJÀJ ëcò B@aRàBå±@x@ÇA ARóàE$„ÃaRàˆâ ûaRô =áRØ‹€=â½å ±õà@0‡€@ÿ ÿÞ s ïU‚à+cµëcÿàJöàbœ†€bàá½å±÷ B@jÀB„å±@x@ßA$AjøàEª?Äájajù =áj3C€=â½%-ä(%±úà@B€@á å±ûàBïA€Bä ƒäÿ; ü B@JÀB‹@Ùâøñ@x@¿AAJýàE1û€ˆàËâ µ >þà=²¤Fÿÿ$Xÿ =@®€ÁÅaÆâó% áˆ%±"à@nü€¼îW @îWâ óå±R AëàbÍá Âóå± B@`æÀBüy@x@ßA$AjàE¸¶,öÇbó =ájÍvÆajG bâ,ÿÿÚ˜à@/u€@å,±àJ›t J Šà‹ëc B@aRàBå±@x@ÇA ARàEÅ-Çaàˆâ ûaR =áRz5€=áR"½ à@Ö0€@‹î !%ò⽃(àÿ$""ÆDñ,Dð€a ²·B½ àbA bàá½å± B@jÀB€å±@x@ßA$Aj àELé€ëàˆá j r =ájÌì€=ä(%-ä(%±à@' @å nå±àBŒë€ƒàÁÁJ$ ƒå ± B@JàB@Ùå±@x@¿AAJàEÓ¤Èâµ@=à=T·„FöÇ =@®€Áê€{áˆ%páˆ"óà@¦€@å ±5\ @âó+câ óå±EAëàbo¥€bᨠbᨋc B@`æÀBöÇ@x@ßA$AjàEZ`Éa¨ñbó =ájw Ê`=ä^Kà@Ø€@â m!j$^öÇàJDÀJA#â¸å± B@aRàBzüy@x@ÇA ARàEg× Ó@Y ˆä FaR =áR ߀=áR"½à@{Ú€@ü y“) €‹ƒ(àdpKÿñ@¸Àp'B½ àbçÙ€bàáÄfå±! B@jÀBå±@x@ßA$Aj"àEî’Ëbàˆá jaj# =Àƒv–€=ä(%-ä(C$à@Ñ•€@‡ RÁéÁå±%àB2 BàÁÀBå±& B@JÀBñ@x@¿AAJ'àEtNÌáJe±(à=ö`„Fñ) =áˆiT€{áˆ% áˆ"ó*à@²O€@ÿ ÿB!ˆ @å±Kâ óå±ÔÕAë+àb bá ë!¨ bå ±, B@a¨ÀBñ@x@ßA$A¨-àEû Íá¨bó. =áj§€=áj%páj%±/à@ €@å,±0àJs  J ¥(Âûå±1 B@aRàBÿÿ @x@ÇA AR2àE‚Åà ˆä FaR3 =áR1Í€=å"±4à@ŽÈ€@å ±£- à%¯ïð€ï´ïa Ÿ³B½5àbúÇ€bá jå±6 B@jÀB~ëc@x@ßA$Aj7àE Îâ½aj8 =áj™„€=â½å ±9à@öƒ€@ã Å$( @ãÅ"üAJ:àBU Bá J$àB±; B@JÀBå±@x@¿AAJ<àE<ÏáJe±=à=— ‚ÿÿåü> =ሀB€{âó% äF%±?à@Ë=€@—î W)ø @å±+câ óå±;AA¨@àb+ bᨠbáë«cA B@a¨ÀBëc@x@ßA$A¨BàEø€ëñ bóC = …`ƒA¸Ðaæâóå ±Dà@¡¶€@å,±EàJ ÀJ å ±F B@€ÎÀBå±@x@ÇA ARGàE$oÑaãÇå ±!!OªH =áR¾v€=áR"½Ià@ r€@ÿ ÿB³%­K]1‚Æà0à#äa z°B½JàbŒq€bàáÇå±K B@jÀBzå±@x@ßA$AjLàEª*Òajáóá j""ajM = …€ƒB.€=áj%-ä(+cNà@ -€@á å±OàBþ,€BàÁÁJå±P B@€ÆÀBå±@x@¿AAJQàE1æà ˆá J##e±Rà=¶¤FÿÿÉ‚S = ÀÁì€{áˆ% áˆ"óTà@mç€@“ Sé­!ˆ @å±+câ óå±ÞBAëUàbÍá ᨠbå ±V B@`æÀB”â ó8pÿÿ¦@x@ßA$A¨WàE¸¡Óbóñ$%bóX =ájçaÔ`=çå ±Yà@G`€@‡@Sáj' @ñZàJ³_ J þàJëc[ B@aRàBÿÿ$X@x@ÇA AR\àEÅÕaã„â û&&aR] =@þ€ƒ| €=â½!â½^à@Í€@ó Ó"½ @â½Ãÿàþä%¸ðï±ýîa ËÀB½_àb9 bàá½å±` B@€æÀB‹â ½ÿÿû@x@ßA$AjaàELÔ€ëàˆá j''ajb =ájÜ×€=ájå ±cà@9 @à~Á!j @ãÅ«cdàB˜Ö€ƒà Bå±e B@JÀBå±@x@¿AAJfàEÓÖbµã>á J((e±gà=T¢„FÿÿBh =áˆË•€{áˆ% âóià@ ‘€@ÿ ÿ5n$F @âó+câ óå±o2Bójàbo€bᨠbáë«ck B@a¨ÀB¤â ó"ôå±@x@ßA$A¨làEYK×a¨ëc)*bóm =áj Ø`=å"±nà@ð €@ë,coàJ\ÀJ þå ±p B@aRàBÿÿô@x@ÇA ARqàEg Ó@Y¦…â û++aRr =áRÊ€=â½%pâ½sà@sÅ€@ñ ÓÓ@®%°ïð1ë@¸À8B½tàbßÄ€bàáÄ©å±u B@jÀBâ ½å±@x@ßA$AjvàEî}Ùbäf,,LÎw =ájv€=áj%páj+cxà@Ó€€@á å±yàB2 BàÁÁJå±z B@JÀBå±@x@¿AAJ{àEt9ÚaJàËâ µ--e±|à=õ¤F~3:¢ó€  ±K >} =áˆ`?€{áˆ% áˆ+c~à@°:€@è¥ @å±+câ óå±¾ÊAëàb báᨠbå ±€ B@a¨ÀBÿÿ,@x@ßA$A¨àEûô€ëå±./bó‚ = …`ƒ#µÛaæå"±ƒà@†³€@å,±„àJò² J â4b4ëc… B@€ÎÀBå±@x@ÇA AR†àElÜaàˆâ û00aR‡ =áR°s€=áR"½ˆà@o€@ñ ã€6Åtð"+fðÿÿWš1B½‰àb}n€bä f届 B@jÀBñ@x@ßA$Aj‹àE'Ýáj11ajŒ =áj+€=ä(å ±à@u*€@áéÂr屎àB×)àB `ƒäŸÿÞ B@JàB@Ùbîà…&ý @'@x@¿AAJàEãà ˆâ µ22e±‘à=—õ„Fyå±’ =@®@™é€{áˆ% 屓à@Rä€@”Fáë câ ó展íBó”àb²á Áë!¨ £á¨Ÿÿ,• B@`æÀBå±@x@ßA$A¨–àEžÞbóñ34bó— =ájÍ^ß`=ä^.˜à@/]€@â m!j*Q`ò# Ã! !% … AR™àJ›\ J áµ `JáR-‚ûš B@aRàBÿÿÞ@x@ÇA AR›àEªàaàˆâ û55aRœ =áRb€=áR%±à@¾€@ë cóõw‚Ïà àÁ¶O™€a n¯Ajžàb* bàá½å±Ÿ B@jÀBëc@x@ßA$Aj àE1Ñ€ëàˆá j66aj¡ =áj¥Ô€=ä(å ±¢à@ÿÓ€@æ ¹å±£àBa BàÁÁJ$ ƒâµ¥±¤ B@JÀBëc@x@¿AAJ¥àE¸ŒábµàËá J77e±¦à=9Ÿ„Få±§ =ረ’€{áˆ% 屨à@ô€@ë câ óå±Ê]Bó©àbT báᨠ£á¨…±ª B@a¨ÀBœî W'Zñ@x@ßA$A¨«àE>Hâa¨ëc89Lά =ájgã`=ä^%±­à@É€@å,±®àJ5ÀJ þå ±¯ B@aRàBå±@x@ÇA AR°àEL¿ Ó@Y¦:â û::aR± =áR Ç€=áR%±²à@\€@â½'î!üàÿƒ(T:õƒïð€ý@¸ÀT>B½³àbÈÁ€bàá½å±´ B@jÀBöÇ@x@ßA$AjµàEÒzäbàˆá j;;aj¶ =ájW~€=áj%-ä(1·à@±}€@ã Å `@ãÅ"ü@B¸àBàBå±¹ B@JÀBÿÿB@x@¿AAJºàEY6åáJ<<$5»à=Ú¤FÿÿÞ¼ =áˆM<€{å"±½à@’7€@¤ëcâ óå±ðÌA¨¾àbõá Âó!¨å ±¿ B@a¨ÀBÿÿÞ@x@ßA$AjÀàEàñ€ëëc=>bóÁ =ájü±æaæáj$âó"´Âà@_°€@å,±ÃàJ˯ J ¥(ÁRëcÄ B@aRàBå±@x@ÇA ARÅàEíhçaÿÿ* ??aRÆ =áR¦p€=äFáR%±Çà@l€@ëcñ%TJE«þ /Úõa ,kcÈàbmk€bá jå±É B@jÀB€è oöÇ@x@ßA$AjÊàEt$èajàˆå ±@@HoË =ájü'€=â½å ±Ìà@W @å$±ÍàB¸&€ƒàÁµ$ ƒåô«cÎ B@JÀBëc@x@¿AAJÏàEûßà ˆá JAAe±Ðà=|ò„Få±Ñ =áˆïå€{áˆ% äF"óÒà@8á€@ÿÿ/¼â ó屈kcÓàb—à€bᨠ£á¨‹cÔ B@a¨ÀB â ó"ôëc@x@ßA$A¨ÕàE‚›ébóå±BCbóÖ =áj¦[ê`=ájä^%±×à@Z€@å,±ØàJtY J þå ±Ù B@aRàBÿÿB@x@ÇA ARÚàEëaàˆâ ûDDaRÛ =áR<€=å"±Üà@“€@ÿÿ¦#kYÿE­âkea ¡¨HoÝàbÿ€bàáÄfå±Þ B@jÀB„â ½å±@x@ßA$AjßàEÎà ˆá jEEajà =ájšÑ€=â½%-ä(+cáà@õЀ@å$±âàBZ Bá Jå±ã B@JàB@Ùèª!KöÇ@x@¿AAJäàEœ‰ìâµFFe±åà=œ„F屿 =@®€Á‘€{âó% áˆ%±çà@ÚŠ€@å ±`@îW+câ óå±ZAëèàb8 bᨠ£å ±é B@`æÀBå±@x@ßA$A¨êàE#Eía¨ñGHbóë =ájDî`=âóå ±ìà@¦€@â m!j @öÇíàJÀJ â¸ëcî B@aRàBå±@x@ÇA ARïàE1¼€ÓãÇä FIIaRð =áRáÀ=áR"½ñà@=¿€@ë c3‚Ê%­Sêe­‚Ëñ%a aÑB½òàb©¾€bá jå±ó B@jÀBëc@x@ßA$AjôàE·wïbàˆá jJJKcõ =áj?{€=ájå ±öà@›z€@á鯹ñ÷àBûy€BàÁÀBå±ø B@JÀBŒ@Ùâøå±@x@¿AAJùàE>3ðáJKKe±úà=¿E„Fÿÿ$Xû =@®€Á.9€{áˆ% å±üà@|4€@—ñ !ˆ @å±(¥â óå±Å#BóýàbÚá ᨠbå ±þ B@`æÀBöÇ@x@ßA$A¨ÿàEÅî€ëëcLMbó#ájî®ñaæç%páj+cà@P­€@ÿ,ÿ àJ¼¬ J ¤â¸å± B@aRàBñ`x@9 AR @`EÒeòaã„ä FNNaR =â½{m€=â½áR%±à@Öh€@ë cCTWïðT.õƒtˆðîÄT`Ì ³ŸB½àbB bàá½å± B@a$àBëc@x@ßA$Aj àEY!óájOOaj =ájå$€=ájå ± à@@ @á $( @ãÅ"üAJ àB¡#€ƒà Bå± B@JÀBå±@x@¿AAJàEàÜà ˆâ µPPe±à=aï„Få± = ÀÁÈâ€{áˆ% äF"óà@Þ€@å ±)ø @å±Ãâ óå±mA¨àb|Ý€bᨠbáë± B@`æÀBÿÿB@x@ßA$A¨àEf˜ôbóëcQRH¥ =áj„Xõ`=å"±à@éV€@ë,càJUÀJA#èjå± B@aRàBv àBÿÿ@Ò@x@ÇA ARàEtöaàˆâ ûSSaR =áR)€=â½â ½à@Œ€@ÿÿÞS%­t+_ÙKkòöÇÕ0 àbø b!&á'å± B@jÀByå±@x@ßA$AjàEûÊ€ˆç¥á jTTaj =áj‡Î€=áj%pä(! à@âÍ€@á å±!àBCàBå±" B@JÀBå±@x@¿AAJ#àE†÷âµUUe±$à=™„FÿÿQè% =áˆnŒ€{äF% áˆ%±&à@»‡€@ÿm @å±â óå±qÿ÷'àb bá ë!¨ bå ±( B@a¨ÀBÿÿÞ@x@ßA$A¨)àEBøá¨VVE±* =áj­F€=ájä^(f+à@ E€@å,±,àJxD JA#ånå±- B@aRàBÿÿ¦@x@ÇA AR.àEýà ˆä FWWaR/ =áRVùaáR"½0à@¯€@ë ccvÇ‚ÚE­úqa ‰D©1àb bá jå±2 B@jÀBÿÿ]L@x@ßA$Aj3àE¹€ëàˆá jXXaj4 =ájž¼€=ájå ±5à@û»€@æ0Ìkå±6àBZàB$ ƒäŸÿ ”7 B@JÀBÿÿ,@x@¿AAJ8àEœtúbwáJYYe±9à=¤¤Fÿÿ$X: =ሌz€{ç:% å±;à@Ôu€@œñ ë câ óå±f{Bó<àb8 báÁë!¨ £á¨œy= B@a¨ÀBñ@x@ßA$A¨>àE#0ûá¨ZZbó? =ájÕ4€=ájå ±@à@73€@å,±AàJ£2 J å±B B@aRàBöÇ@x@ÇA ARCàEªëà ˆä F[[aRD =áRGó€=äFáR%±Eà@ªî€@ë cs‚Û%­Tdï%·Ü@ à1œB½FàbÀb@(ájå±G B@jÀBxëc@x@ßA$AjHàE0§üâ½\\ajI =áj¹ª€=áj%-ä(+cJà@ @å$±KàBt©€ƒá J$ ƒä…±L B@JÀBëc@xJ 9AAJM @€E·býáJ]]e±Nà=¿¤Fÿÿ¸lO =ሣh€{äF% áˆ"óPà@õc€@ÿÿ$Xâ ó展nAëQàbS bᨠ£á¨…±R B@abÀBëc@x@ßA$A¨SàE>þá¨^^bóT =ájÞ"€=âóä^+cUà@>!€@å,±VàJª  JA#äFëcW B@aRàBÿÿ$X@x@ÇA ARXàEÅÙ€ˆ‹@Y ˆå ±__aRY =áRƒá€=â½áR"½Zà@ÝÜ€@ë cƒkY‚ÕE­ @¸À-òB½[àbI bä ©å±\ B@jÀBëc@x@ßA$Aj]àEK•ÿâ½``aj^ =ájÓ˜€=áj%-ä(%±_à@0 @â rëc`àB—€ƒà Bå±a B@JÀBå±@x@¿AAJbàEÒPÿSààˆâ µaa$5cà=Ú¤Fëcd =áˆÆV€{áˆ% áˆ"óeà@ R€@ë câ ó屉AëfàbnQ€báëcg B@a¨ÀB è ¥>"ÿÿÞ@x@ßA$AjhàEY a¨üybcbói =ájÌ€=å"±jà@ðÊ€@ü,ykàJ\ÀJ þÅ(ÁRå±l B@aRàBå±@x@ÇA ARmàEfƒaRáÛâ ûddaRn =áR‹€=â½å ±oà@z†€@ÿ ÿ,“%­ÖE­qKe×ÿÿ,dB½pàbæ…€bàáÁjå±q B@jÀB{â ½ÿÿÞ@x@ßA$AjràEí>ájeeajs =áj…B€=ájå ±tà@âA€@á å±uàBAàBå±v B@JÀBå±@x@¿AAJwàEtú€Ëã>â µffe±xà=õ¤Fÿÿ* y =áˆXaÆáˆå ±zà@«û€¼ÿ ÿ¦!ˆ @öÇâ óå±ÀBó{àb bá ë!¨ bë c| B@a¨ÀBâ ó"ôå±@x@ßA$A¨}àEúµ,üygh"~ =áj&vajå"±à@…t€@ë,c€àJñs J ªÚÄFå± B@aRàBöÇ@x@ÇA AR‚àE-aàˆâ ûiiaRƒ =áR¯4€=áR(o„à@ 0€@å ±£T+Y%·ØE·$ëcVöB½…àbx/€bàáÁj屆 B@jÀBÿÿ; @x„ 9A$Aj‡ @`E耈äfjjajˆ =ájì€=ä(å ±‰à@të€@à~Á$( @ãÅ.`AJŠàBÓêàB$àB±‹ B@aàBå±@x@¿AAJŒàE¤âµkke±à=–¶„F屎 =ሪ€{áˆ% ëcà@Q¥€@ü yö Çâ óå±òA¨àb±á Áë!¨ £á뱑 B@a¨ÀBöÇ@x@ßA$A¨’àEœ_a¨å±lmbó“ =ájÀ `=ä^"´”à@#€@å,±•àJ JA#å±– B@aRàBÿÿ* @x@ÇA AR—àE©Öà ˆå ±nnaR˜ = …€ƒ=Þ€=áR%±™à@žÙ€@ö dzS‚ÈàðN|ÿÿQè•tB½šàb  b‡ µájå±› B@€æÀBÿÿ* @x@ßA$AjœàE0’ báóá jooaj =áj¬•€=ä(%-ä(1žà@  @á 屟àBp”€ƒàÁä ƒä–Ç  B@JÀB„@Ùâø)ºÿÿB@x@¿AAJ¡àE·M áJppe±¢à=8`„Fuÿÿ* £ =@®€Á£S€{áˆ% áˆ"ó¤à@óN€@ë câ óå±1†Aë¥àbS bᨠ£á¨–Ǧ B@`æÀBöÇ@x@ßA$A¨§àE> a¨ñqrbó¨ =ájjÉ€=ä^%±©à@ÌÇ€@å,±ªàJ8ÀJA#â¸ëc« B@aRàBÿÿ @x@ÇA AR¬àEK€ aRàˆä FssaR­ =áRû‡€=áR"½®à@Sƒ€@ö Ç Ã(àÿ}O[}@a šèB½¯àb¿‚€bàáÄfå±° B@jÀBƒî !ñ@x@ßA$Aj±àEÒ;ájttaj² =ájJ?€=ä(%-ä(%±³à@¨>€@å$±´àB  Bá J)º ƒä…±µ B@JàB@Ùâø!Kå±@x@¿AAJ¶àEY÷à Ëâ µuu$5·à=Ú¤Fy屸 =@®€ÁMý€{áˆ% áˆ"ó¹à@•ø€@ÿ ÿôâ óå±>ÿÉ‚ºàbõá Âó!¨ £á¨…±» B@`æÀBöÇ@x@ßA$Aj¼àEß²bóëcvwbó½ =ájs`=ájõt%±¾à@rq€@â m,Î @ÿÿô¿àJÞp J â¸å±À B@aRàBå±@x@ÇA ARÁàEí)aàˆâ ûxxaR =áR¡1€=å"±Ãà@ù,€@å ± ÓNœ‚öà` ƒE»å±¬äD©Äàbe bàá½å±Å B@jÀBå±@x@ßA$AjÆàEså€ëàˆá jyySÓÇ =ájôè€=áj%-ä(%±Èà@N @å$±ÉàB°ç€ƒà B$ ƒä…±Ê B@JÀBÿÿÞ@x@¿AAJËàEú âµzze±Ìà={³„Fÿÿ¦Í =áˆꦀ{äF% áˆ%±Îà@7¢€@å ±,ì @öÇâ óå±AëÏàb–¡€bâjÂó!¨ bᨅ±Ð B@a¨ÀBñ@x@ßA$A¨ÑàE\a¨ëc{|bóÒ =áj±`=áj%páj%±Óà@€@å,±ÔàJ€ JA#å±Õ B@aRàBå±@x@ÇA ARÖàEŽÓà ˆä F}}aR× = …€ƒCÛ€=äFáR"½Øà@šÖ€@å ±ã%±Srþe®$Uñ0D Œà¶àB$ ƒä–Ç B@JÀB"Mâøëc@x@¿AAJàEG!áJŽŽe± $`€ƒZ„Få±! =@®€=qM€{áˆ% è¥"à@¾H€@å ±!ˆ @ñ(¥âóå±ï¶Bó#àb bá ë!¨ bᨖÇ$ B@`æÀBå±@x@ßA$A¨%àE"a¨ëcD^& =áj9À=å"±'à@šÁ€@)åfå±(àJÀJ IàJå±) B@aRàBñ@x@ÇA AR*àEz#aRàˆä F‘‘aR+ =@þ€ƒº€=áR.!,à@}€@å ±#ð"àÿîÄTð"ðtU/õ‚ÿÿbþ\­B½-àb|€bàáÄ©å±. B@€æÀBëc@x@ßA$Aj/àEœ5$ajç¥á j’’B½0 =áj,9€=ä(%-ä(11à@‡8€@à~Á$( @ãÅ4@B2àBè7€Bá J$àB¥±3 B@JÀBå±@x@¿AAJ4àE"ñà ˆá J““e±5à=£¤Få±6 =áˆ÷€{âó% áˆ"ó7à@\ò€@ÿÿôâ óå±øÿ¸l8àb¾á Áë!¨ £á륱9 B@a¨ÀBå±@x@ßA$A¨:àE©¬%bóˆ@Yªäá ¨””bó; =ájU±€=âóê"´<à@µ¯€@å,±=àJ!ÀJàÉÁRå±> B@aRàBÿÿB@x@ÇA AR?àE0h&áR••DF@ =áRão€=â½áR"½Aà@4k€@èo @èo34‚õƒà"äÿD«ïDýå±~hoBàb j€bá jå±C B@jÀBŠëc@x@ßA$AjDàE¶#'áj––ajE =ÀƒG'€=áj%-ä(%±Fà@¤&€@‡ RÃ<Ârå±GàBàB$ ƒä‹cH B@JÀB/à…ëc@x@¿AAJIàE=߀ËáÓä ——@=Jà=E­ƒÇÿÿ¦K =@®€Á5å€{äF% áˆ"óLà@yà€@ŸN¤Åሠ@âóå±ÚnWMàbÙá Àb!¨ bᨋcN B@`æÀBÿÿbþ@x@ßA$AjOàEÄš(bóëc˜™bóP =áj÷Z)`=áj$áj%±Qà@WY€@å,±RàJÃX JA#áµå±S B@aRàBå±@x@ÇA ARTàEÑ*aàˆâ ûššaRU =áRm€=äFáR"½Và@Å€@ÿÿ,Cÿï„"‚ŽƒtâðíaÀEéD©Wàb1 bàá½å±X B@jÀBÿÿÞ@x@ßA$AjYàEXÍ€ëàˆá j››ajZ =ájäЀ=â½%-â½%±[à@B @å$±\àB Ï€ƒàÁÁJ$ ƒä…±] B@JÀBëc@x@¿AAJ^àE߈+ⵜœe±_à=`›„F}ÿÿ,` =ÀÁÓŽ€{áˆ% áˆ"óaà@Š€@ÿÿ,â óå±áAëbàb{‰€bᨠ£á¨…±c B@a¨ÀB è ¥'Zÿÿ¦@x@ßA$A¨dàEfD,a¨öÇžbóe =áj™-`=ájä^%±fà@ü€@å,±gàJhÀJ å±h B@aRàBöÇ@x@ÇA ARiàEs»€ÓŒ@Y ˆä FŸŸaRj =áR.À=áR"½kà@‡¾€@å ±S`ïD£ð"}ÿÿô€…B½làbó½€bá jå±m B@jÀBå±@x@ßA$AjnàEúv.bàˆá j  ajo =ájrz€=áj%-ä(%±pà@Ðy€@‡@Ø¡éÅnëcqàB6 BàÁÀBå±r B@JÀBÿÿô@x@¿AAJsàE€2/áJ¡¡e±tà=E„Fÿÿôu = ÀÁu8€{áˆ% áˆ"óvà@½3€@å ±!ˆ @ëcâ óå±$wAëwàb bᨠbå ±x B@`æÀB¡å±@x@ßA$A¨yàEî€ëëc¢£bóz =áj;®0aæájå ±{à@ž¬€@å,±|àJ ÀJA#ã>ëc} B@aRàBå±@x@ÇA AR~àEe1aãÇä F¤¤aR =áRØl€=áR"½€à@)h€@ñ c1ïð£íð0ƒ1àÿ£àýànÿ'àb•g€bá j层 B@jÀBÿÿ]L 5@ßA$Ajƒ @`E› 2ajàˆá j¥¥aj„ =áj$€=ájå ±…à@q#€@à~Ån$( @ãÅ(®D†àBÓ"àB屇 B@aàB@Ùâø)ºÿÿô@x@¿AAJˆàE"Üà ˆá J¦¦e±‰à=£¤FÿÿBŠ =@®€Á"â€{áˆ% 屋à@_Ý€@§ñ ; @1! @# +câ óå±ÊA¨Œàb¾á Áë!¨ báë«c B@`æÀB¬å±@x@ßA$A¨ŽàE©—34@Tëc§¨bó =ájÅW4`=ïÂ"´à@(V€@â m!j%òÿÿÞ‘àJ”U€Jå(ÁRå±’ B@aRàBÿÿ$X@x@ÇA AR“àE¶5aãÇâ û©©aR” =áRb€=â½!⽕à@º€@ë c sïÃ-àÿ@€ÿa ÿ²º–$à£& bàáÁjå±— B@jÀBÿÿB–$€ßA$Aj˜ @`E=Ê€ëàˆá jªªaj™ =ájÅÍ€=áj%p`k€3ÒCÅšà@ @à~Áå±›àBÌ€ƒà B$ ƒä‘œ B@aàBñ@x@¿AAJàEÄ…6⵫«@=žà=E˜„F屟 =ሼ‹€{áˆ% áˆ(¥ à@‡€@ÿ ÿ›ò!ˆ @å±4 â óå±+ÿ²º¡àb`†€bᨠbᨑ¢ B@a¨ÀBöÇ@x@ßA$Aj£àEJA7a¨ëc¬­[&¤ = ƒ8`=å"±¥à@áÿ€~ˆ@Sè屦àJMÀJ àJå±§ B@ àBëc¦ 5@ÇA AR¨ @ 5\¸àˆä F®®AR© =@þ`=À€=â½å ±ªà@X»€@ë cƒ¥©þ ëcÜšD©«àbĺ€bàáĩ屬 B@€æÀBŒè oÿÿx@x@ßA$Aj­$`D€Eßs9bç¥á j¯¯aj® =ájow€=áj%-ä(1¯à@Év€@á $( @ ª@ (®@B°àB+àBå±± B@JÀBå±@x@¿AAJ²àEe/:aJàËá J°°e±³à=æ¤Fÿÿ,´ = Áb5€{äF% áˆ%±µà@¢0€@ÿÿh°â óå±xÿÿ¸¶àb báÂó!¨ £á륱· B@a¨ÀB¨ äì"ôöÇ@x@ßA$A¨¸àEìê€ëå±±²D^¹ =áj «;aæájä^"´ºà@k©€@å,±»àJר J þáå±¼ B@aRàBå±@x@ÇA AR½àEúaâó¶¶bóÎ =A6`ƒ9™€=áj$áj%±Ïà@š—€@å,±ÐàJÀJA#ä©å±Ñ B@€ÎÀBÿÿ@Ò@x@ÇA ARÒàEP?áR··aRÓ =áR³W€=å"±Ôà@S€@ÿÿ¦£u‰uˆUuÈuÍuÌua fLB½ÕàbR€bä ©å±Ö B@jÀBÿÿWš@x@ßA$Aj×àE› @áj¸¸HoØ =áj+€=â½%-â½+cÙà@ˆ€@á $( @ãÅ"ü@BÚàBç €Bâ µå±Û B@JÀBå±@x@¿AAJÜàE"Çà ˆå ±¹¹e±Ýà=*•ƒÇõw!^ã|€ ßÿ…*`µAˆÞ = ) ÁÍ€{áˆ% áˆcßà@]È€@ÿ ÿ¦/ª @å±+câ óå±¾A¨ààb¾á å±á B@a¨ÀBÿÿ¦@x@ßA$A¨âàE©‚AbóöǺ»bóã =ájËBB`=ájå ±äà@+A€@å,±åàJ—@ JA#‚*áR屿 B@aRàBÿÿÞ@x@ÇA ARçàE¶ùà ˆâ û¼¼aRè =áRaCaRå"±éà@Âü ~ëc³ËuÊÒÈuiujuk a æÉB½êàb.Àb@$ájaöä©á•|ë B@jÀBÿÿ; @x@ßA$AjìàE=µ,àˆá j½½D(í =ájŸ€=â½%pä(%±îà@" @å$±ïàB…·€ƒàÁä ƒáJ«cð B@JàB@Ùá,¯üy@x@¿AAJñàEÄpDbwàËá J¾¾@=òà=Eƒ„FxÿÿÞó =@®€Á°v€{áˆ%páˆ%±ôà@ÿq€@å ±': @å±(¥â óå±B;Aëõàb` báᨠbᨖÇö B@`æÀBå±@x@ßA$Aj÷àEJ,Ea¨ñ¿Àbóø = …`ƒ}ì€=ájå ±ùà@Ýê€@â m' @ l@ ÿÿ$XúàJIÀJ (â¸å±û B@€ÎÀBå±@x@ÇA ARüàEX£FaRàˆâ ûÁÁaRý =áR«€=H@—ãaâ½þà@X¦€@”å±Ã¯Œïuhõg¯ŠïBhågüyû>B½ÿàbÄ¥€bàá½å±%  jÀB†ñ@x@ßA$AjàEÞ^GájÂÂaj =ájWb€=G@–á j%-ájC %à@³a€@á $( @ãÅK f BàBàBëc B@JÀBëc@x@¿ =AJ @€EeHáJÃÃ@=à=ê¤Fÿÿ;  =áˆ] €{âó% áˆ%± à@¡€@ÿ ÿB$F @å±+câ óå±_ÌA¨ àb bá ë!¨ bá륱 B@abÀBüy@x@ßA$Aj àEìÕ€ëëcÄÅH¥ =áj–Iaæâó%páj"´à@s”€@å,±àJß“ JA#ãå± B@aRàBñ@x@ÇA ARàEùLJaàˆå ±ÆÆaR =áRT€=â½áR"½à@ñO€@ÿ ÿ¦Ó﵌ìUuäƒð"}pa ’²B½àb] bàáå± B@jÀBÿÿQè@x@ßA$AjàE€KájÇÇaj =áj €=áj%pâ½%nà@m €@á å±àBÌ àB$ ƒäŒ® B@JÀBå±@x@¿AAJàEÄà ˆâ µÈÈe±à=ˆÖ„FÿÿB =áˆ÷É€{áˆ% áˆ"óà@BÅ€@ÿm @å±+câ óå±6@Aëàb£á Âó!¨ bᨋc B@a¨ÀBÿÿ¦@x@ßA$A¨!àELbóëcÉÊbó" = …`ƒÁ?M`=å"±#à@ >€@å,±$àJŒ= J å ±% B@€ÎÀBëc@x@ÇA AR&àE›öà ˆâ ûËËG' = …€ƒJþ€=â½å ±(à@£ù€@ÿ ÿÞãï}ïƒZàTýð Œàw4B½)àb bá jëc* B@€æÀBÿÿB@x@ßA$Aj+àE"²Nbàˆá jÌÌaj, =áj®µ€=áj%-ä(%±-à@  @à~ÃÅå±.àBj´€ƒà B$ ƒä…±/ B@JÀBå±@x@¿AAJ0àE¨mOáJÍÍe±1à=*€„Få±2 =ሡs€{äF% áˆ%±3à@än€@ÿ ÿÞë câ óå±Óm4àbD bᨠ£á¨…±5 B@a¨ÀBëc@x@ßA$A¨6àE/)Pa¨å±ÎÏA7 =ájKé€=ájä^+c8à@®ç€@å,±9àJÀJA#ëc: B@aRàBÿÿ @x@ÇA AR;àE= QaRòñä FÐÐaR< =áRø§€=áR"½=à@Q£€@å ±óDð"uµÿu¥ Ò½äõ«üy‡ÿ»*>àb½¢€bá jå±? B@jÀB‚ñ@x@ßA$Aj@àEÃ[Rajàˆá jÑÑajA =ájC_€=å"±Bà@¡^€@áéå±CàBÿ]€BàÁÀBå±D B@JÀBŒ@ÙâøöÇ@x@¿AAJE%  €EJSáJÒÒe±Fà=ˤFÿÿôG =@®€{>€{áˆ% å±Hà@†€@ÿ ÿ* ': @ëcâ óå±#·GIàbæ€bᨠbå ±J B@`æÀBÿÿ¦@x@ßA$A¨KàEÑÒà ˆâ óÓÓE±L =ájs×€=ájå ±Mà@ÕÕ€@ˆ@Sâm' @öÇNàJAÀJ àJñO B@aRàBz àB ÿžî @xM 9A ARP @`EWŽTâûÔÔ rQ =@þ`= –€=å"±Rà@d‘€@屬u­ "¥SPþSuþS`Ì oÿtSàbЀbä ©å±T B@€æÀBÿÿ/¼@x@ßA$AjUàEÞIUájÕÕajV =ájbM€=â½*ßä(+cWà@¾L€@á $( @ãÅ(®@BXàB Bâ µå±Y B@JÀBå±@x@¿AAJZàEeVáJÖÖ@=[à=mÓƒÇÿÿnb\ =áˆU €{áˆ% áˆ(¥]à@Ÿ€@ÿ ÿ¦$F @å±Ãâ óå±Ë{A¨^àb bᨠbáë«c_ B@a¨ÀBñ@x@ßA$Aj`àEìÀ€ëöÇרD^a =ájWaæáj%páj"´bà@v€@å,±càJâ~ J ånå±d B@aRàBöÇ@x@ÇA >eàEù7Xaàˆç ÙÙaRf =áR£?€=å"±gà@ý:€@öÇqßäõ$S"÷S#ßS#ïõva ÿªhàbi bàáÊå±i B@jÀBÿÿ…*@x@ßA$AjjàE€ó€ëàˆá jÚÚajk =áj÷€=â½%pâ½%±là@\ö€@å$±màBÀõ€BàÁÁJå±n B@JàB âøëc@x@¿AAJo%  €E¯YbµàËá JÛÛe±pà=ˆÁ„Fwÿÿ,q =@®€{÷´€{áˆ% áˆ%±rà@A°€@—ÿ m @å±â óå±°8Aësàb£á ᨠbå ±t B@`æÀBå±@x@ßA$A¨uàEjZa¨ñÜÝ!v =áj¿*[`=ájå ±wà@ )€@â m' @ëcxàJŒ( JA#â¸å±y B@aRàBå±@x@ÇA ARzàE›áà ˆâ ûÞÞaR{ = …€ƒHé€=áR"½|à@£ä€@å ±#õwuÙÂ’÷u¥*u¤@Œà…ÄB½}àb bá jå±~ B@€æÀBå±@x@ßA$AjàE!\bŒ@Y¡óá jßßaj€ =@Ý€ƒ® €=çå ±à@ @äåÃÅ `@ãÅ(®AJ‚àBnŸ€ƒà B àB¶Çƒ B@€ÆàB@Ù¡À…层%`¿€ AJ„ @€E¨X]aJáÓá Jààe±…à=)k„F€å±† =@®€{˜^€{âó% 屇à@ãY€@å ±$F @å±+câ óå±!6A¨ˆàbD báÁe `báë«c‰ B@`æÀBå±@x@ßA$A¨ŠàE/^a¨ëcáâbó‹ =áj]Ô€=áj%páj"´Œà@¾Ò€@å,±àJ*ÀJA#⸠`‹áR-™ÃŽ B@aRàBå±@x@ÇA ARàE<‹_aRàˆâ ûããaR =áRÖ’€=áR%±‘à@8Ž€@å ±3Ò’"6¾ õ¬u­ "ÀàÀƒa 8‹B½’àb¤€bàá½áj¿ÿ “ B@jÀBzöÇ@x@ßA$Aj”àEÃF`ájääaj• =ájGJ€=ájå ±–à@£I€@á  `@âr"üAJ—àBàB$ ƒàB¥±˜ B@JÀBñ@x@¿AAJ™àEJaáJååe±šà=ˤFÿÿŠÜ› =áˆ:€{áˆ%päF"óœà@…€@—ëcâ ó屜yA¨àbæá Âó!¨ £á¨œyž B@a¨ÀBå±@x@ßA$A¨ŸàEѽ€ë屿çKc  =áj~baæè¥å ±¡à@c|€@å,±¢àJÏ{ J å±£ B@aRàBå±@x@ÇA AR¤àEÞ4caàˆå ±èèaR¥ =áRx<€=â½áR"½¦à@Ö7€@å ±CÀ‚À…À„À’S’þ6år àN&B½§àbB bàá½å±¨ B@jÀBÿÿt@x@ßA$Aj©àEeð€ëàˆá jééajª =ájõó€=áj%-ä(1«à@Q @à~Á屬àB±ò€ƒà B$ ƒäˆg­ B@JÀBå±@x@¿AAJ®àEë«dâµêê@=¯à=m¾„Få±° =áˆܱ€{áˆ% áˆ"ó±à@&­€@ÿ ÿÞ`@ëcâ óå±êÔAë²àb‡¬€bᨠ£á¨…±³ B@a¨ÀBè ¥ÿÿ$X@x@ßA$Aj´%  €Ergea¨ñëìbóµ =áj¡'f`=å"±¶à@&€@å,±·àJm% J@Ü屸 B@a àBå±@x@ÇA AR¹àE€Þ€ˆîèä FííGº =áR2æ€=â½å ±»à@”á€@ˆè o @â½3ÓSå"åX àƒƒt ð a ÄB½¼àbÀb@$ájå±½ B@jÀBzâ ½=Þ@'@x@ßA$Aj¾%`D€Ešgbàˆá jîîaj¿ = {€=†€=áj%-ä(%±Àà@㜀@à~å±ÁàBFàBå±Â B@A"àB@Ùáñ@x@¿AAJÃ%`€EUháJïïe±Äà=h„FÿÿBÅ =@®€{y[€{áˆ% áˆ%±Æà@ÈV€@ÿÿ6 @âóå±`÷AëÇàb) bá ë!¨ bå ±È B@`æÀBÿÿô@x@ßA$A¨ÉàEia¨ëcðñbóÊ =áj;Ñ€=å"±Ëà@›Ï€@â m'$^öÇÌàJ Jå(Ã>ñÍ B@aRàBå±@x@ÇA ARÎàE!ˆjaRáÛä FòòaRÏ =áR¸€=áR"½Ðà@‹€@ë ccàp %ªucC€%¸a  ÿÀÜÑàbŠ€bàáÁjå±Ò B@jÀBëc@x@ßA$AjÓàE¨CkájóóGÔ =áj$G€=ä(å ±Õà@F€@â rëcÖàBàE€Bá J$ ƒä‹c× B@JÀBëc@x@¿AAJØàE/ÿà ˆâ µôôe±Ùà=°¤FëcÚ = ÀÁ#laÆáˆå ±Ûà@j€@ÿ ÿÞ$F @å±6Çâ ó屩ÿÆÄÜàbËá Âó!¨ bᨋcÝ B@`æÀB è ¥ëc@x@ßA$A¨Þ%  €Eµº€ëâ1á ¨õõBóß =áj_¿€=áj%pç3Êàà@½€@ù 5!j @å±áàJ.ÀJ èjå±â B@a àBÿÿ/¼@x@ÇA ARãàEà@) @à~Áå±?àBÚ€ƒà B$ ƒä‹c@ B@JàB@Ùâøñ@x@¿AAJAàEГ}âµ  e±Bà=Q¦„Fÿÿ; C =@®€ÁÀ™€{âó% áˆ%±Dà@ •€@œëcâ ó层ÿžæEàbl”€bᨠ£á¨‹cF B@`æÀBñ@x@ßA$A¨GàEWO~a¨ëc bóH = …`ƒ`=âóä^(fIà@î €@â m2€ @üyJàJZÀJ â¸ñK B@€ÎÀBå±@x@ÇA ARLàEdÆ Ó@Y£Aä FaRM =áR ΀=áR"½Nà@lÉ€@ö ÇÃÐÐÐÐÐÐÐÐÐ@¸ÀN×D©OàbØÈ b@% áÄ©ëcP B@jÀBÿÿF„@x@ßA$AjQàEë€bàˆá jajR =áj{…€=ä(%-ä(%±Sà@ׄ€@é wå±TàB7 BàÁÁJå±U B@JÀBëc@x@¿AAJVàEr=áJe±Wà=ó¤FëcX =áˆZC€{áˆ% áˆ"óYà@¬>€@”å ±$F @ñÃâ óå±=Ÿ!ëZàb bᨠbå ±[ B@a¨ÀBñ@x@ßA$A¨\àEøø€ëëcbó] =áj#¹‚aæä^%±^à@ƒ·€@å,±_àJï¶ JA#äFå±` B@aRàBÿÿÞ@x@ÇA ARaàEpƒaàˆä FaRb =áR¬w€=áR"½cà@s€@å ±Óÿÿ(ðÐà2a zB½dàbzr b@$ájå±e B@jÀBå±@x@ßA$AjfàE+„ájajg =áj/€=ä(å ±hà@u.€@å$±iàBÕ-€Bá Jå±j B@JÀBå±@x@¿AAJkàEçà ˆâ µe±là=•ù„Få±m =áˆüì€{âó% å±nà@Nè€@å ±': @å±6Çâ óå±ÞæBóoàb¯á çZ bå ±p B@a¨ÀBå±@x@ßA$A¨qàEš¢…bóå±bór =ájÉb†`=áj+"áj%±sà@)a€@å,±tàJ•` JA#äFå±u B@aRàBå±@x@ÇA ARvàE¨‡aãÇâ ûaRw =áRf!€=áR%±xà@¸€@˜è o"½!“â½ã¯Œïü¯Šïýìÿ﵌òa §ÜB½yàb$ bàá½å±z B@jÀB‰ÿ ÿ œÿÿB@x@ßA$Aj{àE.Õ€ëáóá jaj| =áj¶Ø€=å"±}à@ @á !j%òãÅ"üAJ~àBr×€ƒàÁÁJå± B@JÀBå±@x@¿AAJ€àEµˆbµàˆá Je±à=6£„Fÿÿ ‚ =ስ–€{áˆ% äF"óƒà@ð‘€@ÿÿh°â óå±.A¨„àbQ báᨠ£áë±… B@a¨ÀBÿÿh°@x@ßA$A¨†àE>e±à=¢¤Få± = ÀÁ :€{âó% å±à@]5€@ë câ óå±§¸Bóàb¼á ᨠ£å ± B@`æÀBÿÿ @x@ßA$A¨àE§ï€ëÿÿ,?@bó =áj̯£aæáj%páj%±à@.®€@å,±àJš­ JA#äFå± B@aRàBå±@x@ÇA ARàEµf¤aãÇä FAAaR =áRMn€=áR%± à@©i€@ñ  cåipWuiåj”<„jàò3B½!àb bá jå±" B@jÀBñ@x@ßA$Aj#àE;"¥4 àˆá jBBaj$ =áj»%€=å"±%à@ @áéÃÅëc&àBw$€ƒàÁÀBå±' B@JÀBå±@x@¿AAJ(àEÂÝà ˆá JCCe±)à=Gð„Fÿÿ…** =ሪã€{áˆ% äF"ó+à@ÿÞ€@ÿ ÿ5n)ø @ñÃâ óå±VÁBó,àb^ bᨠbå ±- B@a¨ÀB”è ¥- ÿÿÞ@x@ßA$A¨.àEI™¦bóëcDEbó/ =ájaY§`=ájå ±0à@ÄW€@ÿ,ÿ 1àJ0ÀJ þå ±2 B@aRàBå±@x@ÇA AR3àEV¨aáÛâ ûFFaR4 =áR €=å"±5à@f€@å ±sñï`$ƒàpƒ€àpa C•B½6àbÒ€bàáÄfå±7 B@jÀBÿÿbþ@x@ßA$Aj8àEÝËà ˆá jGGaj9 =ájeÏ€=â½ë c:à@À΀@â rå±;àB! Bá Jå±< B@JÀBå±@x@¿AAJ=àEd‡©âµHHe±>à=å¤Fz8/ã|ÿÿ; `µA? = €ÁX€{áˆ% å±@à@¡ˆ€@›@aé­!ˆ @å±.Wâ óå±çgBóAàb bᨠbå ±B B@`æÀBñ@x@ßA$A¨CàEêBªa¨ëcIJbóD = ¾€ƒ«`=ájå ±Eà@q€@ë,cFàJÝ J áµëcG B@aRàBz"vàBñ@x@ÇA ARH' / `Eø¹à ˆä FKKaRI =áR‘Á€=å"±Jà@ì¼€@å ±ƒå à£tðäôð@ËÀ«òB½KàbX bá jå±L B@a$àBå±@x@ßA$AjMàEu¬bå2á jLLajN =ájy€=â½å ±Oà@jx€@à~ƹ$( @ãÅ.`AJPàBËw€Bá Jå±Q B@JÀBå±@x@¿AAJRàE1­aJàˆá JMMe±Sà=†C„Fÿÿ¦T =áˆö6€{âó% å±Uà@C2€@—å ±ë câ óå± mCVàb¡á Áë!¨ £áë¼yW B@a¨ÀBÿÿ* @x@ßA$A¨XàEŒì€ëå±NObóY =áj¥¬®aæâóå ±Zà@«€@å,±[àJsª JA#çgå±\ B@aRàBå±@x@ÇA AR]àE™c¯aàˆâ ûPPaR^ =áR6k€=áRc_à@’f€@å ± “‚ÆàDð<€T<€a J³B½`àbþe€bä ©å±a B@jÀBå±@x@ßA$AjbàE °ájQQajc =áj°"€=áj%p#º`@3ÒAdà@  @áéÃÅå±eàBl!€ƒà B$ ƒäŸÿ,f B@JÀBå±@x@¿AAJgàE§Úà ˆâ µRRe±hà=(í„Få±i =AT€Á—à€{áˆ% áˆ"ójà@åÛ€@å ±!ˆ @ëcâ óå±îAAëkàbC bᨠbá¨XFl B@`æÀBå±@x@ßA$A¨màE.–±bóñSTbón =ájKV²`=çå ±oà@¬T€@å,±pàJÀJA#å±q B@aRàBå±@x@ÇA ARràE; ³aàˆâ ûUUaRs =áRì€=â½áR"½tà@O€@ü y£`tU%¨`ï´ìëctÅB½uàb» bA;ájå±v B@jÀBÿÿ* @x@ßA$AjwàEÂÈà ˆá jVVajx =ájRÌ€=áj%-ä()wyà@­Ë€@á $( @ãÅ«czàBàBå±{ B@JÀBå±@x@¿AAJ|àEI„´bµç…á JWWe±}à=ʤFå±~ =áˆ-Š€{áˆ% áˆ"óà@~…€@”ëcâ óå±Ð7Aë€àbåá Ç!¨ £á륱 B@a¨ÀBÿÿ÷@x@ßA$A¨‚àEÏ?µa¨ëcXYbóƒ =ájõÿ€=å"±„à@Vþ€@å,±…àJÂý JA#äF dêáR¿ÿh°† B@aRàBå±@x@ÇA AR‡àEݶ¶aRàˆâ ûZZaRˆ =áR~¾€=â½å ±‰à@á¹€@å ±³ƒ àÓ”}Pàðåjpÿÿx÷ØB½ŠàbMÀbA;áj屋 B@jÀBå±@x@ßA$AjŒàEcr·áj[[aj =ájìu€=â½%pä(%±Žà@G @á å±àB¨t€ƒå(ä ƒâµ«c B@JÀBÿÿ @x@¿AAJ‘àEê-¸áJ\\e±’à=k@ƒ|ÿÿ$X“ =áˆÒ3€{âó% áˆ%±”à@$/€@å ±!ˆ @ëcâ óå±S*Aë•àb†.€bá ëå±– B@a¨ÀB”üy@x@ßA$A¨—àEq逈ëc]^bó˜ =ájƒ©¹aæâóä^(f™à@ä§€@å,±šàJPÀJA#äFå±› B@aRàBå±@x@ÇA ARœàE~`ºaÿÿ(Ÿ__aR =áR0h€=áR"½žà@’c€@å ±ÃjåkÓÿ(k\y`a ôB½Ÿàbþb b@$ájå±  B@jÀBå±@x@ßA$Aj¡àE»ajàˆç ``aj¢ =áj‘€=ä(%-ä(%±£à@í€@å n$( @ån"ü@B¤àBQ BàÁå±¥ B@JàB@ÙáÿÿB@x@¿AAJ¦àEŒ×€Ëàˆá Jaae±§à= ê„F{3:àƒüy¨ =@®€ÁtÝ€{áˆ%páˆ"ó©à@ÆØ€@”ëcâ óå±³A¨ªàb( báᨠ£áë«c« B@`æÀBå±@x@ßA$A¨¬àE“¼bóå±bcbó­ =ájES½`=ä^"´®à@¥Q€@â m82%òÿÿ ¯àJÀJA#â4` å±° B@aRàB{"vàBüy@x@ÇA AR±' / `E ¾aàˆâ ûddaR² =áRÒ€=áR"½³à@, €@ö ÇÓðåkp[uk ƒ~à0à@ËÀ+¥B½´àb˜ €bàáÁjå±µ B@a$àB a£àBáj@x@ßA$Aj¶àE§Åà ˆá jeeaj· =áj3É€=ä(%pä(%±¸à@È€@å$±¹àBïÇ€Bá J)º ƒä–Ǻ B@JÀBñ@x@¿AAJ»àE-¿âµffe±¼à=¯¤Få±½ =ሇ€{áˆ% áˆ"ó¾à@h‚€@ÿÿ8b @ëcâ óå±WAë¿àbÉá Âó!¨ bᨖÇÀ B@a¨ÀBâ ó"ôÿÿÞ@x@ßA$A¨ÁàE´<Àa¨ñghbó =ájÒü€=áj%páj%±Ãà@3û€@å,±ÄàJŸú J þäå±Å B@aRàBå±@x@ÇA ARÆàE³ÁaRãÇä FiiaRÇ =áRd»€=å"±Èà@¶€@å ±ãå0àC£ƒà`!A à;_B½Éàb. bá jå±Ê B@@]àBÿÿàJ@x@ßA$AjËàEHoÂajàˆá jjjajÌ =ájÔr€=â½%-â½%±Íà@1 @áéÃÅëcÎàBq€ƒàÁÀB$ ƒä…±Ï B@JÀBå±@x@¿AAJÐàEÏ*ÃáJkke±Ñà=P=ƒ|ñÒ =ሻ0€{áˆ% áˆ%±Óà@,€@—ëcâ ó屟­AëÔàbk+€bᨠ£á¨…±Õ B@a¨ÀBÿÿ¦@x@ßA$A¨ÖàEV怈ëclmbó× =áj€¦Äaæájå ±Øà@夀@å,±ÙàJQ Jã >å±Ú B@aRàBwëc@x@ÇA ARÛàEc]ÅaáÛä FnnaRÜ = …€ƒe€=å"±Ýà@_`€@Œâ ½$F!“èoóð²ƒàp‚ÆàTþðåq0 Œàu‚B½ÞàbË_Åàá¬Îå±ß B@€æÀBÿÿnbÞ 5@ßA$Ajà @`EêÆàˆá jooajá =áj‚€=â½%-ä(%±âà@߀@â r!j @ãÅ(®@BãàB> Bâ,¡Jå±ä B@aàBŒ@Ùì®)ºÿÿ$X@x@¿AAJåàEqÔ€Ëàˆá Jppe±æà=ò¤Få±ç =@®€Á]Ú€{áˆ% áˆ%±èà@¬Õ€@å ±!ˆ @âóå±ÑpA¨éàb  báᨠbá륱ê B@`æÀBå±@x@ßA$A¨ëàE÷Çbóëcqrbóì =ájPÈ`=áj$áj"´íà@~N€@å,±îàJêM JA#â¸å±ï B@aRàB|å±@x@ÇA ARðàEÉaàˆâ ûssaRñ =áR¨€=å"±òà@ €@å±àC#Srûäõvõw]aÀ–B½óàbq €bàá½å±ô B@jÀBå±@x@ßA$AjõàEŒÂ€ˆå2á jttajö =ájÆ€=â½â½%±÷à@qÅ€@à~Á$( @ãÅ"ü@BøàBÔÄ€Bá Jå±ù B@JàB@Ùâøå±@x@¿AAJúàE~Êâµuue±ûà=“„Fÿÿ¦ü =@®€Á„€{âó% äF%±ýà@N€@å ±5\ @å±Ãâ ó層dA¨þàb®á Áë!¨ bá륱ÿ B@`æÀBÿÿÞ@x@ßA$A¨(  €E™9Ëa¨å±v*xA!f =ájÀù€=ájå ±à@ ø€@â m!j @öÇàJŒ÷ J@Üâ¸å± B@a àBñ@x`Ç =AR @`E§°ÌaRãÇä FxxaR = ?`=M¸€=äF!â½à@¯³€@üy1 pÒƒ‚aàÓ”@ Œà.;B½àb bàá½å± B@€æÀBÿÿx@x@ßA$Aj àE-lÍajáóá jyyaj =áj½o€=áj%páj%± à@ @à~Áå± àByn€ƒà B$ ƒä‘ B@JÀBëc@x@¿AAJàE´'ÎáJzze±à=5:ƒ|ÿÿ5n =ሜ-€{áˆ% `ï€3ÒE±à@ð(€@ÿ ÿ¦!ˆ @å±â ó届ÉAëàbP bᨠbᨑ B@a¨ÀBüy@x@ßA$A¨àE;ã€ëàˆâ ó{$Æ%± =ájÍç€=å"±à@/æ€@å,±àJ›å JA#å± B@aRàB#ÞàBû @x@ÇA ARàEÁžÏâû|+Ë!R =áRf¦€=áR(oà@Æ¡€@å ±#àðƒ[à`ðàp ýà]{B½àb2 bå$± B@jÀB{ù …üy@x@ßA$AjàEHZÐáj}}aj =ájÄ]€=ä(%-ä(%±!à@ @á $( @ãÅ«c"àB€\€ƒâ µå±# B@JÀBå±@x@¿AAJ$àEÏÑaJŒ £>ä ~~e±%à=פFóÿÿyÆ& = i€ÁÀ{áˆ% áˆ"ó'à@€@ü y)ø @å±+câ óå±ù‰Aë(àbo€bᨠbá륱) B@`æÀBëc@x@ßA$A¨*àEVÑ€ˆöÇ€bó+ =ájƒ‘Òaæä^"´,à@ä€@å,±-àJPÀJA#¢rÈðëc. B@aRàBÿÿÞ- 5@ÇA AR/ @`EcHÓaàˆâ ûaR0 =áRP€=áR"½1à@kK€@öÇ3Dú"åzuð¤ýîþ¬ `Ì ÃB½2àb×J€bàáÁjëc3 B@a$àBÿÿÚ˜@x@ßA$Aj4àEêÔáj‚‚aj5 =ájn€=ä(%pä(%±6à@É€@å$±7àB* Bá Jå±8 B@JÀBå±@x@¿AAJ9àEp¿€Ëàˆâ µƒƒe±:à=ö¤Fÿÿ* ; =áˆeÅ€{áˆ% áˆ"ó<à@®À€@ÿ ÿ5n': @å±â óå±¶1Aë=àb  bá ë!¨ bå ±> B@a¨ÀB è ¥(¦ÿÿ,@x@ßA$A¨?àE÷zÕbóñ„…bó@ =áj;Ö`=ä^%±Aà@~9€@å,±BàJê8 J å±C B@aRàBå±@x@ÇA ARDàEòà ˆâ û††aRE =áR¶ù€=áR"½Fà@õ€@ö ÇCt,õ‚ä4€õƒîð® tàZÝB½Gàbô€bá jå±H B@jÀBÿÿyÆ@x@ßA$AjIàE‹­×bàˆá j‡‡ajJ =áj±€=ájå ±Kà@o°€@ã Å$( @ãÅ"üAJLàBϯ€BàÁÆü$àB±M B@JÀBå±@x@¿AAJNàEiØáJˆˆe±Oà=“{„F{ÿÿ P =áˆþn€{å"±Qà@Hj€@ÿ ÿ* ë câ óå±þ/A¨Ràb®á á¨å ±S B@a¨ÀBüy@x@ßA$A¨TàE™$Ùa¨ëc‰ŠbóU =áj¶ä€=ájõt"´Và@ã€@å,±WàJˆâ JA#å±X B@aRàBvñ@x@ÇA ARYàE¦›ÚaRàˆä F‹‹aRZ =áRX£€=äFáR%±[à@²ž€@å ±S.¥°tðt/õa ÏB½\àb bàá½å±] B@jÀBè oñ@x@ßA$Aj^àE-WÛájŒŒaj_ =ájµZ€=â½%-éÚ+c`à@ @å$±aàBqY€ƒá Jå±b B@JÀBå±@x@¿AAJcàE´ÜáJe±dà=5%ƒ|å±e =ሤ€{áˆ% áˆ"ófà@î€@å ±': @ëcâ óå±²=AëgàbP bᨠbç±h B@a¨ÀBñ@x@ßA$A¨iàE:΀ëëcŽbój =áj`ŽÝaæájä^%±kà@ÁŒ€@å,±làJ-ÀJA#å±m B@aRàB7²àBöÇ@x@ÇA ARnàEHEÞaàˆå ±aRo = p€ƒ M€=å"±pà@dH€@å ±c+_ ‚õƒà"­åy`a‚ Œà>ÓB½qàbÐG€bàáå±r B@€æÀBå±@x@ßA$AjsàEÏßáj‘‘ajt =ájO€=â½å ±uà@«€@å$±vàB Bá Jå±w B@JàB@Ùâø&ýÿÿÞ@x@¿AAJxàEU¼€Ëàˆâ µ’’e±yà=Ö¤Fxëcz =@®€ÁF€{âó% å±{à@½€@—ô ë câ ó屚!Bó|àbñá Âó!¨ £å ±} B@`æÀBå±@x@ßA$A¨~àEÜwàbó屓”bó =áj8á`=âóå ±€à@o6€@â m!j @ÿÿ,àJÛ5 J â¸öÇ‚ B@aRàBå±@x@ÇA ARƒàEêãÇâ û••aR„ =áR—ö€=áR(o…à@òñ€@å ±síÔ@%©U7üt-QàPuB½†àb^ bá jëc‡ B@jÀBå±@x@ßA$AjˆàEpªâbàˆá j––aj‰ =áj®€=ájå ±Šà@\­€@à~ÃÅ)Ú @ãÅK þ+¦‹àB¼¬àB$àB±Œ B@JÀBëc@x@¿AAJàE÷eãáJ——e±Žà=xx„Fÿÿ =!€ Áçk€{áˆ% å±à@2g€@ë câ óå±ô‡A¨‘àb“f€bá ë!¨ £áë«c’ B@a¨ÀBå±@x@ßA$A¨“àE~!äa¨ÿÿF„˜™bó” =áj°á€=ç%páj"´•à@à€@ÿ,ÿ@Ò–àJ}ß JA#å±— B@aRàBöÇ@x@ÇA AR˜àE‹˜åaRáÛä FššaR™ =áR9 €=â½áR%±šà@››€@ÿ ÿ, ƒbUçqŠïqƒt ÿÿÏ4^öB½›àbÀbA;áj屜 B@jÀBÿÿÞ@x@ßA$AjàETæáj››ajž =áj¢W€=áj%-â½%nŸà@þV€@á å± àB^àB$ ƒäŸÿÞ¡ B@JÀBå±@x@¿AAJ¢àE™çaJã>â µœœe±£à="ƒ|層 =ህ€{áˆ% áˆ"ó¥à@Ï€@ÿ ÿ,â óå±úAë¦àb5 bá ë!¨ £á¨ÿ5n§ B@a¨ÀBöÇ@x@ßA$A¨¨àEË€ëëcžbó© =ájV‹èaæå"±ªà@¶‰€@ë,c«àJ"ÀJA#äFëc¬ B@aRàBëc@x@ÇA AR­àE-Béaÿÿ(ŸŸaR® =áR×I€=â½å ±¯à@9E€@å ±“í…±ðí``$a ÒúB½°àb¥D b@$ájå±± B@jÀBå±@x@ßA$Aj²àE´ý€ˆäf  aj³ =áj8êa¨áj%-ä(%±´à@”€@á å±µàBôÿà Áÿÿô¶ B@JÀBå±@x@¿AAJ·àE:¹à Ëå ±¡¡e±¸à=»¤Fÿÿ ¹ =áˆ&¿€{áˆ% áˆ%±ºà@uº€@ÿ ÿ* !ˆ @öÇâ óå±vAë»àbÖá ᨠbå ±¼ B@a¨ÀBå±@x@ßA$A¨½àEÁtëbµå±¢£bó¾ =ájè4ì`=å"±¿à@H3€@å,±ÀàJ´2 J äFå±Á B@aRàBå±@x@ÇA ARÂàEÎëà ˆâ û¤¤aRà =áRuó€=áR"½Äà@Óî€@ñ £V¼q„t'+aÔð€A à¿B½Åàb? bå(½å±Æ B@@]àBÿÿ$X@x@ßA$AjÇàEU§íbáóá j¥¥ajÈ =ájÕª€=ä(å ±Éà@2 @á $( @ãÅ.`AJÊàB•©€ƒàÁÁJ)ºàB«cË B@JàB@ÙâøöÇ@x@¿AAJÌàEÜbîáJ¦¦e±Íà=]u„FöÇÎ =@®€ÁÈh€{áˆ% å±Ïà@d€@å ±5\ @å±Ãâ óå±èA¨Ðàbxc€bᨠbáë«cÑ B@`æÀBå±@x@ßA$A¨ÒàEcïa¨âtâ ó§§bóÓ =ájô"€=ájê"´Ôà@W!€@â m!j @öÇÕàJà  JA#â¸å±Ö B@aRàBÿÿB@x@ÇA AR×àEéÙà ˆá R¨¨aRØ =áR’á€=å"±Ùà@íÜ€@å ±³%¬6%¬äð€ Eqa ù B½ÚàbY bá jå±Û B@jÀBÿÿF„@x@ßA$AjÜàEp•ðb½àˆÿÿ ÿ]©©ajÝ =áj™€=â½%-ä(+cÞà@_˜€@ã<Ånå±ßàBÀ—€BàÁÀBå±à B@JÀBëc@x@¿AAJáàE÷PñáJªªe±âà=ÿ¤FÿÿBã = ÀÁãV€{áˆ% áˆ(¥äà@4R€@—ü y!ˆ @å±Kâ óå±]ÿAëåàb“Q€bᨠbå ±æ B@`æÀBå±@x@ßA$A¨çàE~ òa¨öÇ«¬bóè =ájªÌ€=áj%páj%±éà@ Ë€@å,±êàJxÊ JA#å±ë B@aRàBöÇ@x@ÇA ARìàE‹ƒóaRàˆä F­­aRí =áR0‹€=å"±îà@†€@ñ Ãtôðår0ã t,qŠt@Œàœ‹B½ïàbû… b@&ájå±ð B@jÀBÿÿ @x@ßA$AjñàE?ôáj®®ajò =@Ý€ƒšB€=â½å ±óà@õA€@å$±ôàBZ Bá Jå±õ B@€ÆàB@Ùáëc@x@¿AAJöàE˜ú€Ëàˆâ µ¯¯e±÷à= ƒ|ÿÿBø =@®€Á…õaÆâó% äF%±ùà@Öû€¼ÿ ÿ¦)ø @å±+câ óå±8yBóú(à£4 báçZ bå ±û B@`æÀBå±@x@ßA$A¨üàE¶,ñ°±bóý =áj8vöajâóå ±þà@št€@â m!j @ëcÿàJÀJA#â¸ëc)  aRàBå±@x@ÇA ARàE--÷aãÇâ û²²aR =áRÑ4€=áR"½à@10€@å ±Óð€e«äðt-QbAÀQŽB½àb/ b@%ájå± B@jÀBå±@x@ßA$AjàE³èà ˆá j³³aj =ájCì€=ájå ±à@Ÿë€@áéÃÅ)Ú @ãÅ(®AJ àBÿê€Bá J àB± B@JÀBëc@x 9AAJ @€E:¤øbµàˆá J´´e± à=»¤F{3âÿÿ@Ò =áˆ&ª€{áˆ% å±à@x¥€@å ±!ˆ @å±+câ óå±õ|A¨àbÖá Áë!¨ báëiœy B@abÀBå±@x@ßA$A¨àEÁ_ùᨵµbó =ájid€=ç%páj"´à@Éb€@å,±àJ5ÀJA#äFå± B@aRàBöÇ@x@ÇA ARàEHúaRãÇâ û¶¶aR =áRö"€=â½áR%±à@P€@ÿ ÿ¦ãUÕ€Ã3Î3ÎØùÿõàa k“B½àb¼€bàá½å± B@jÀB‚ÿÿB@x@ßA$AjàEÎÖ€ˆáóá j··aj =ájVÚ€=áj%pâ½1à@³Ù€@à~Á$( @ãÅ"ü@BàBàB àB¥± B@JÀBå±@x@¿AAJ àEU’ûbµàËá J¸¸@=!à=]`ƒÇöÿÿ$X" =áˆI˜€{áˆ% áˆ"ó#à@“€@ÿ ÿ¦`@å±+câ óå±ÏaA¨$àbñá Áë!¨ £á륱% B@a¨ÀB â ó- ÿÿ¦@x@ßA$Aj&àEÜMüa¨ñ¹ºbó' =áj÷ ý`=å"±(à@W €@ÿ,ÿÞ)àJ €JârÁRå±* B@aRàB|((åôÿÿB@x@ÇA AR+àEéÄà ˆâ û»»aR, =áR Ì€=áR"½-à@ùÇ€@å ±óùûï[`‹_:­€@ýà;&B½.àbe bá jå±/ B@jÀBå±@x@ßA$Aj0àEp€þbàˆá j¼¼aj1 = …€ƒøƒ€=ä(%pä(%±2à@U @æ ¹å±3àB´‚€ƒàÁµ$ ƒäŸÿÞ4 B@€ÆÀBå±@x@¿AAJ5àE÷;ÿaJàËá J½½e±6à=xN„FÿÿÞ7 =áˆçA€{áˆ% áˆ"ó8à@2=€@å ±!ˆ @å±Kâ ó屟ÔAë9àb“<€báᨠbᨋc: B@a¨ÀBÿÿ @x@ßA$A¨;àE}÷€ˆÿÿ ¾¿bó< =áj©·ÿšä^(f=à@¶€@ë,c>àJtµ JA#ëc? B@aRàBå±@x@ÇA AR@àE‹naàˆâ ûÀÀaRA =áR2v€=áR"½Bà@q€@üyÃ3ØüôY+]ðqààŸ¦B½Càbûp€bàá¿ÿêå±D B@jÀBÿÿ; @x@ßA$AjEàE*ájÁÁajF =ájš-€=áj%-ä(%±Gà@÷,€@ã Å$( @ãÅ"ü@BHàBVàBå±I B@JÀBÿÿWš@x@¿AAJJàE˜å€ËáÓâ µÂÂe±Kà=ø„FÿÿÞL =ሄë€{å"±Mà@Ðæ€@ÿÿ â óå±;TA¨Nàb4 bá¢ó!¨å ±O B@a¨ÀBñ@x@ßA$A¨PàE¡bóöÇÃÄbóQ =ájOa`=âóïÂ"´Rà@²_€@å,±SàJÀJA#å±T B@aRàBxJeàBëc@x@ÇA ARUàE,aàˆâ ûÅÅaRV =áRЀ=áR%±Wà@1€@‡ÿ 7!“èoD¶À±a +B½Xàbœ€bàá½å±Y B@jÀBÿÿ5n@x@ßA$AjZàE³Óà ˆá jÆÆaj[ =ájC×€=ä(%på±\à@¡Ö€@å$±]àBÿÕ€Bá J$ ƒåô«c^ B@JÀBëc@x@¿AAJ_àE:âµÇÇe±`à=»¤Få±a =áˆ&•€{áˆ% áˆ"óbà@v€@öÇâ óå±éBócàbÖá Âó `£á¨‹cd B@a¨ÀBå±@x@ßA$A¨eàEÁJa¨ñÈÉbóf =ájð `=áj$áj%±gà@S €@å,±hàJ¿ JA#ånñi B@aRàByå±@x@ÇA ARjàEÎÁà ˆä FÊÊaRk =áR†É€=å"±là@ÖÄ€@è o)ø!“å±#–À‚3à±a |[B½màbB bá jå±n B@jÀB‹ó Óÿÿ<‹@x@ßA$AjoàEU} bàˆá jËËajp =ájÅ€€=â½%-â½+cqà@# @å$±ràB‰€ƒàÁä ƒä‘s B@JÀB„@Ùâø!Kÿÿ$X@x@¿AAJtàEÛ8 áJÌÌe±uà=]K„Fuüyv =@®€ÁØ>€{áˆå ±wà@:€@£è ¥ö Çâ ó屨©Aëxàbw9€bᨠ£á¨…±y B@`æÀB©â ó!¨öÇ@x@ßA$A¨zàEbô€ˆëcÍÎbó{ =áj’´ aæáj$âó%±|à@õ²€@å,±}àJaÀJ þâ¸å±~ B@aRàBå±@x@ÇA ARàEpk aàˆä FÏÏaR€ =áRs€=å"±à@`n€@ñ 3ÿ!:6À±à[q‚àbÌm€bàáÄf屃 B@jÀBâ ½å±@x@ßA$Aj„àEö& ájÐÐaj… =Àƒ—*€=áj%-â½%±†à@ñ)€@‡ Râr' @ãÅ"ü@B‡àBR Bá J)ºàB¥±ˆ B@JÀBëc@x@¿AAJ‰àE}â€Ëàˆâ µÑÑe±Šà=þ¤Fÿÿx‹ =áˆmè€{äF% áˆ+cŒà@ºã€@ÿÿ$Xâ óå±ÿqàb báÂó!¨ £áë¥±Ž B@a¨ÀBöÇ@x@ßA$A¨àEžâóÒÒbó =áj£¢€=âóå ±‘à@¡€@å,±’àJp  JA#èj屓 B@aRàBÿÿÞ@x@ÇA AR”àE‹YaRãÇâ ûÓÓaR• =áR0a€=â½áR"½–à@“\€@ÿÿÞC‚3¶ÀDð <€a Bho—àbÿ[€bàá³Ó屘 B@jÀBÿÿh°@x@ßA$Aj™àEajáóá jÔÔajš =áj€=áj%-ä(%±›à@ù€@à~Á屜àBYàB$ ƒä‹c B@JÀBå±@x@¿AAJžàE˜Ðà Ëá JÕÕe±Ÿà= ¤Fõÿÿ,  =ÀÁ€Ö€{áˆ% áˆ"ó¡à@ÒÑ€@”ëcâ ó屿nW¢àb4 bá ë!¨ £á¨‹c£ B@a¨ÀBÿÿL6@x@ßA$A¨¤àEŒbóñÖ×bó¥ =€ƒUL`=å"±¦à@¶J€@ÿ,ÿ,§àJ"ÀJ å ±¨ B@aRàBå±@x@ÇA AR©àE,aáÛâ ûØØaRª =áRÊ €=áR"½«à@$€@ë cSQbuð¤U¡ÿt,qŠïa ›}D©¬àb€bàáÄ©fU⽿ÿÚ˜­ B@jÀBÿÿ/¼@x@ßA$Aj®àE³¾à ˆá jÙÙaj¯ =ájC€=ä(%-ä(%±°à@ŸÁ€@á å±±àBÿÀàB$ ƒáJ¥±² B@JÀBå±@x@¿AAJ³àE:zbµã>á JÚÚe±´à=»¤Fÿÿ µ =áˆ*€€{áˆ% áˆ"ó¶à@t{€@ÿÿ,â óå±?Aë·àbÖ¡á ë!¨ £á¨…±¸ B@a¨ÀBëc@x@ßA$A¨¹àEÀ5a¨ñÛÜbóº =ájóõ€=ä^.»à@Sô€@ë,c¼àJ¿ó JA#ëc½ B@aRàBÿÿ @x@ÇA AR¾àEάaRàˆâ ûÝÝaR¿ =áRt´€=áR"½Àà@Ö¯€@ë cckXQà@î€@ë c ÃZøvƒ+àý+aNYí@¸À>B½?àbqí€bàá½å±@ B@jÀBÿÿ@Ò@x@ßA$AjAàE˜¦,bç¥á jûûajB =áj ª€=ä(%-ä(%±Cà@{©€@à~Áå±DàBܨ€Bá J$ ƒä…±E B@JÀBå±@x@¿AAJFàEb-aJÿÿ,üüe±Gà=Ÿ¤FÿÿÞH = ÀÁ h€{âó% áˆ"óIà@Xc€@ÿ ÿ,â ó屚÷AëJàb¾¡á¨ £á¨…±K B@`æÀBÿÿ @x@ßA$A¨LàE¥.a¨ÿÿ* ýþbóM =áj×Ý€=G@—¤œÂ²ê .Nà@8Ü€@å,±OàJ¤Û Jà ‹ñP B@aRàBå±@x@ÇA ARQàE²”/aRâûÿÿaRR =áRhœ€=áR"½Sà@»—€@ÿ ÿN$F+¤î!3Ó ÓðíFUàü£àýE·þ‘a t2B½Tàb' bä ©å±U B@jÀBˆëc@x@ßA$AjVàE9P0ajáóå ± AW =ájÁS€=å"±Xà@ @á !jån"üAJYàB}R€ƒàÁÃAå±Z B@JÀBå±@x@¿AAJ[àEÀ 1áJ@=\à=Eƒ|w3äÿÿ; ] = ÀÁ´€{áˆ% å±^à@ú €@ öÇâ óå±ÐÀA¨_àb\ bᨠ£å ±` B@`æÀBëc@x@ßA$AjaàEGÇ€ëöÇbób =ájx‡2aæáj$áj"´cà@Ù…€@å,±dàJEÀJ â4` å±e B@aRàBÿÿÞ@x@ÇA ARfàET>3aàˆä FaRg =áR F€=å"±hà@\A€@”â ½!“å± ã¾ìð£íðeµî屑lB½iàbÈ@€bàáÁjå±j B@jÀBÿÿñ`@x@ßA$AjkàEÛùà ˆá jajl =áj_ý€=â½%pâ½+cmà@»ü€@å$±nàB Bá Jå±o B@JàB@Ùbîá2aÿÿ,@x@¿AAJpàEbµ4bµàËá Je±qà=ã¤Fëcr =@®€ÁZ»€{áˆ% áˆ(¥sà@œ¶€@ÿ ÿàJö Çâ óå±5žAëtàbþá Âó!¨ £å ±u B@`æÀB¤è ¥!¨ÿÿ,@x@ßA$A¨vàEèp5a¨ñbów =áj 16`=áj$áj%±xà@k/€@â m!j @ Í@ÿ ÿÞyàJ×. J@.â¸å±z B@aRàBå±@x@ÇA AR{àEöçà ˆâ û  aR| =áR¨ï€=å"±}à@ë€@ö Çóœe± ïÔPîa äJB½~àbnê€bá jå± B@jÀBÿÿô@x@ßA$Aj€àE|£7bàˆá j  aj =áj §€=ájå ±‚à@i¦€@äåÃÅëcƒàBÉ¥àB$ ƒä‘„ B@JÀBÿÿ,@x@¿AAJ…àE_8áJ  e±†à=„q„F|ëc‡ =áˆód€{äF% äF%±ˆà@>`€@ÿ ÿÞ!ˆ @è¥â óå±@¸Bó‰àbŸá Áë!¨ bá¨‘Š B@a¨ÀBÿÿ,@x@ßA$A¨‹àEŠ9a¨ëc  bóŒ =áj¸Ú€=áj%páj%±à@Ù€@å,±ŽàJ…Ø JA#å± B@aRàBå±@x@ÇA ARàE—‘:aRàˆä FaR‘ =áRI™€=áR"½’à@£”€@ëc1 ,à$ ÿa@ ÿa ¢°B½“àb bàá½å±” B@jÀBÿÿ @x@ßA$Aj•àEM;ájaj– =ájªP€=áj$œâ½+c—à@ @å$±˜àBfO€ƒá J$ ƒä…±™ B@JÀBñ@x@¿AAJšàE¥<áJ >›à=&ƒ|屜 =ሙ€{áˆ% áˆ"óà@Ü €@ÿÿ/¼â óå±BAëžàbA bᨠ£á¨ÿh°Ÿ B@a¨ÀB ëc@x@ßA$Aj àE,Ä€ëå±bó¡ =ájR„=aæè¥å ±¢à@²‚€@å,±£àJÀJ 層 B@aRàBå±@x@Ç¡ =AR¥ @`E9;>aàˆå ±aR¦ =áRóB€=â½áR"½§à@I>€@å ±Qke¨±Ù`ÿÿBóºB½¨àbµ=€bàá屩 B@a$àB†â ½ÿÿ,@x@ßA$AjªàEÀöà ˆá jaj« =ájPú€=áj%-ä(%±¬à@¬ù€@á )Ú @ãÅ(®@B­àB àBå±® B@JÀBå±@x@¿AAJ¯àEF²?âµe±°à=ÈÄ„Få±± =áˆ;¸€{áˆ% áˆ"ó²à@‚³€@ñ !ˆ @ëcâ óå±WHA¨³àbâá Âó!¨ bá륱´ B@a¨ÀB¡â ó"ôñ@x@ßA$A¨µàEÍm@a¨ñbó¶ =ájø-A`=å"±·à@X,€@å,±¸àJÄ+ J þå ±¹ B@aRàBå±@x@ÇA ARºàEÛ䀈éyä FaR» =áRì€=â½å ±¼à@çç€@ñ#ƒ*ïðäút*Qbÿ1þa ãûB½½àbS bá jëc¾ B@jÀBëc@x@ßA$Aj¿àEa Bbàˆá jajÀ =ájÙ£€=áj%pä(%±Áà@6 @à~ÃÅ `@ãÅ¥±ÂàB™¢€ƒà B$ ƒàB«cà B@JàB@Ùèª$/@'@x@¿AAJÄàEè[CáJe±Åà=in„Fÿÿ¡¤Æ =@®@™+"a€{áˆ% áˆ%±Çà@#]€@ÿ ÿÞ5\ @å±+câ óå±G AëÈàb„\€bᨠbᨋcÉ B@`æÀBëc@x@ßA$A¨ÊàEoDa¨ëcbóË =áj–×€=å"±Ìà@öÕ€@â m!j$ŸöÇÍàJb Jå(Ã>öÇÎ B@aRàBå±@x@ÇA ARÏàE|ŽEaRáÛä FaRÐ =áR/–€=áR"½Ñà@Œ‘€@ÿÿÞ3ƒ*

    =áj»€=áj%-ä(%±?à@ @à~ÃÅå±@àBwœ€ƒà B$ ƒä…±A B@JÀBñ@x@¿AAJBàE²UYáJ88e±Cà=3h„Få±D =ሦ[€{áˆ% áˆ"óEà@ëV€@ÿ ÿÞ; @îWÃâ óå±GAëFàbN bᨠbᨅ±G B@a¨ÀB è ¥'Zëc@x@ßA$A¨HàE9Za¨ñ9:bóI =ájmÑ€=è¥å ±Jà@ÏÏ€@å,±KàJ;ÀJ þå ±L B@aRàBå±@x@ÇA ARMàEFˆ[aRàˆä F;;aRN =áR€=â½áR"½Oà@R‹€@ñ “"àþ£àÿå~uð¤$õa *ZB½Pàb¾Š€bàáÄ©å±Q B@jÀBŒâ ½ëc@x@ßA$AjRàEÍC\áj<?bó^ =ájÿz^ajå"±_à@ay€@å,±`àJÍx J ëca B@aRàBå±@x@ÇA ARbàEè1_aàˆâ û@@aRc =áR9€=â½å ±dà@ì4€@ÿ ÿÞ£‚ä4õƒ"e¯õ‚äa äÑB½eàbX bàáå±f B@jÀBÿÿ/¼@x@ßA$AjgàEní€ëàˆá jAAajh =áj÷ð€=â½å ±ià@Q @à~Áå±jàB¶ï€ƒà B$ ƒä‹ck B@JàB@Ùâø$ñ@x@¿AAJlàEõ¨`âµBBe±mà=v»„Fÿÿ* n =@®€Á宀{âó% å±oà@2ª€@è ¥ë câ óå±@#Bópàb‘©€bᨠ£á¨‹cq B@`æÀBÿÿÞ@x@ßA$A¨ràE|daa¨ëcCDbós =áj­$b`=âóä^6Çtà@#€@â m!j @ÿÿ uàJ{"€Jç ñv B@aRàBå±@x@ÇA ARwàE‰Û€ˆáÛä FEEaRx =áR>ã€=áR(oyà@‰Þ€@ÿ ÿê$F!Rèo³E¯ëea B½zàbõÝ€bàáÄ©ëc{ B@jÀBè oŸÿ÷@x@ßA$Aj|àE—cbàˆá jFFaj} =áj”š€=ä(%-ä(+c~à@@â r!j @ãÅ"ü@BàBP BàÁÁJå±€ B@JÀBëc@x@¿AAJàE—RdáJGGe±‚à=e„Fëcƒ =ሗX€{áˆ% áˆ"ó„à@ÔS€@ë câ ó屦åA¨…àb3 bᨠ£á륱† B@a¨ÀBëc@x@ßA$A¨‡àEea¨öÇHIbóˆ =ájS΀=ä^"´‰à@´Ì€@å,±ŠàJ ÀJA#èj屋 B@aRàBÿÿ5n@x@ÇA ARŒàE+…faRàˆä FJJaR = …€ƒÈŒ€=áR"½Žà@'ˆ€@ë cÃ+WEµ"äƒ)ðå} ŒàøØB½àb“‡€bàáÄfå± B@€æÀBÿÿŽ@x@ßA$Aj‘àE²@gajå2á jKKaj’ =áj:D€=ä(%pä(%±“à@•C€@à~Áå±”àBöB€Bá J展 B@JÀBå±@x@¿AAJ–àE8üà ˆá JLLe±—à=¹¤Fÿÿ/¼˜ =áˆ)haÆâó% áˆ"ó™à@vý€¼ë câ óå±\*AëšàbÔá Áë!¨ £å ±› B@a¨ÀBëc@x@ßA$A¨œàE¿·,å±MNbó =ájíwiáj)Ïáj%±žà@Nv€@å,±ŸàJºu JA#å±  B@aRàBå±@x@ÇA AR¡àEÍ.jaãÇâ ûOOaR¢ =áRz6€=â½áR"½£à@Í1€@ü yÓ´ÿ~¬`ïp¡ÓÑa HB½¤àb9 bàá½å±¥ B@jÀBÿÿ; @x@ßA$Aj¦àESê€ëáóá jPPaj§ =ájÛí€=å"±¨à@7 @á 屩àB—쀃àÁÁJ$&5䑪 B@JÀBå±@x@¿AAJ«àEÚ¥kbµàˆá JQQe±¬à=[¸„Få±­ =áˆΫ€{áˆ% äF"ó®à@§€@ü y!ˆ @îWâ óå± ÿ'L¯àbv¦€báᨠbᨑ° B@a¨ÀBüy@x@ßA$A¨±àEaala¨ñRSbó² =ájŠ!m`=ájä^%±³à@ì€@ÿ ÿšñ´àJXÀJ å±µ B@aRàBå±@x@ÇA AR¶àEnØ€ÓáÛâ ûTTaR· =áRà€=å"±¸à@vÛ€@ˆù …!“â½ãŒd`%©Kc¡Óå}da ¶ïE±¹àbâÚ b@$áj屺 B@jÀBÿÿbþ@x@ßA$Aj»àEõ“nbàˆá jUUaj¼ =ájy—€=â½%-ä(+c½à@Õ–€@â r$( @ãÅ"ü@B¾àB5 BàÁäàB¥±¿ B@JÀBå±@x@¿AAJÀàE|OoaJàËá JVVe±Áà=ý¤FöÇ =áˆdU€{áˆ% áˆ%±Ãà@¶P€@ÿ ÿbþ$F @âóÃâ óå±A¨Äàb báᨠbá륱ŠB@a¨ÀBÿÿF„@x@ßA$A¨ÆàE pa¨ëcWXbóÇ =áj0Ë€=áj$áj"´Èà@‘É€@ë,cÉàJýÈ JA#äFñÊ B@aRàB{ àBõ\ @x@ÇA ARËàE‚qaRàˆâ ûYYaRÌ =áR¾‰€=å"±Íà@…€@ÿ ÿ óp&år0â!ëg ýà{B½Îàb„„€bàá½å±Ï B@jÀBÿÿ; @x@ßA$AjÐàE—=rajãFá jZZajÑ =ájA€=áj%-â½%±Òà@s@€@à~Áå±ÓàBÓ?àBëcÔ B@JÀBÿÿ; @x@¿AAJÕàEù€ˆáÓá J[[e±Öà=ž¤Fÿÿ$X× =ሠÿ€{äF% áˆ%±Øà@Xú€@ÿ ÿ â ó ›”hv¼ï/GÌï?»‰AëÙàb¹á Áë!¨ £å ±Ú B@a¨ÀBñ@x@ßA$A¨ÛàE¤´sbóå±\]bóÜ =ájÎtt`=ájå ±Ýà@/s€@å,±ÞàJ›r JA#ånå±ß B@aRàBå±@x@ÇA ARààE±+uaàˆâ û^^aRá =áR[3€=áR"½âà@¶.€@å±+_ÑŒÿ¿ñYÑKoູB½ãàb" bä ©å±ä B@jÀBå±@x@ßA$AjåàE8ç€ëÿÿ(¿__ajæ =ájÌê€=ájå ±çà@( @áéÃÅå±èàBˆé€ƒà B$ ƒä‘é B@JÀBëc@x@¿AAJêàE¿¢vâµ``e±ëà=@µ„F€ÿÿ@Òì =ÀÁ㨀{áˆ% å±íà@.¤€@å ±ë câ óå±KBóîàb£€bᨠ£á¨‘ï B@a¨ÀBå±@x@ßA$A¨ðàEF^wa¨ñabbóñ =ájlx`=è¥å ±òà@Ì€@å,±óàJ8ÀJA#å±ô B@aRàB{ëc@x@ÇA ARõàESÕ Ó@Y ˆå ±ccaRö =áRùÜ€=â½áR%±÷à@[Ø€@ñE±K†‘pÑdÿÀ}§B½øàbÇ× b@% áÄ©å±ù B@jÀBÿÿÃÐ@x@ßA$AjúàEÚybàˆá jd1êAû =ájb”€=áj%-ä(+cüà@¾“€@à~Áå±ýàBàBå±þ B@JÀBå±@x@¿AAJÿàEaLzaJç…á Jeee±,  €ƒâ¤Fëc =áˆMR€{áˆ% áˆ"óà@›M€@ù» @ñâ óå±ÙUAëàbýá Áë!¨ bå ± B@a$ÀBÿÿ* @x`ß =A¨ @€Eç{a¨ëcfgbó =ájÈ€=å"±à@vÆ€@å,±àJâÅ J@ÝäFëc B@a àBå±@x@ÇA AR àEõ~|aRàˆâ ûhhaR =áR¯†€=â½å ± à@‚€@â½ @â½#+^£à´+k¬ÿÿF„ñÿÉ‚ àbq€bàá½å± B@jÀBÿÿ@Ò@x@ßA$AjàE{:}ájiiaj =áj>€=â½å ±à@d=€@á å±àBÄ<€Bå(ÁJ$ ƒä‹c B@JÀBñ@x@¿AAJàEö€ˆàËâ µjje±à=ƒƒ|ÿÿ*  =áˆþû€{âó% `ï€3e±à@=÷€@¤ù »`@âó+câ óå±UÿÉ‚àbžá ᨠ£á¨‹c B@a¨ÀBÿÿ$X@x@ßA$A¨àE‰±~bóëcklbó =áj¸q`=âó$áj6Çà@p€@å,±àJ„o JA#å± B@aRàBå±@x@ÇA ARàE–(€aàˆâ ûmmaR =áRA0€=áR(o!à@¢+€@ë c 3‘"ƒ(ïð€ñì àpà‡SHo"àbÀb@(ájå±# B@jÀBëc@x@ßA$Aj$àEä ë  ˆá jnnaj% =áj­ç€=ä(%-â½+c&à@  @å nå±'àBi怃àÁ`Ëå±( B@JÀBëc@x@¿AAJ)àE¤Ÿâµooe±*à=%²„Fëc+ =ሥ€{áˆ% áˆ"ó,à@ß €@è¥ @å±+câ óå±#¡Aë-àb@ bᨠbå ±. B@a¨ÀBëc@x@ßA$A¨/àE*[‚a¨ëcpqbó0 = …`ƒVƒ`=ä^%±1à@µ€@å,±2àJ!ÀJ ýÂûëc3 B@€ÎÀBÿÿ5n@x@ÇA AR4àE8Ò Ó@Yczä FrraR5 =áRßÙ€=áR"½6à@8Õ€@ö ÇCQüu+gå °à6B½7àb¤Ô€bàáÁjå±8 B@jÀBÿÿ5n@x@ßA$Aj9àE¿„bàˆå ±ssaj: =ájK‘€=ä(å ±;à@¨€@å$±<àB BàÁÁJå±= B@JÀBå±@x@¿AAJ>àEEI…áJtte±?à=ǤFå±@ =áˆ6O€{âó% å±Aà@J€@ÿ ÿ6ë cCâóå±»ÄBóBàbáá ᨠ£å ±C B@a¨ÀBöÇ@x@ßA$A¨DàĔa¨å±uvbóE =ájóÄ€=âóë cFà@SÀ@å,±GàJ¿Â J ¥(ÁRå±H B@aRàBå±@x@ÇA ARIàEÚ{‡aRãÇä FwwaRJ =áRyƒ€=â½áR%±Kà@Ú~€@ë cS}ÔPz ¦ƒ)ïða ©+B½LàbFÀb@$ájå±M B@jÀBëc@x@ßA$AjNàE`7ˆajàˆá jxxajO =ájì:€=â½å ±Pà@J @áéÂrëcQàB¨9€ƒàÁÀBå±R B@JÀBå±@x@¿AAJSàEçòà ˆá Jyye±Tà=hƒ|ñU =áˆËø€{áˆ% å±Và@ô€@ÿ ÿÞ!ˆ @ëcâ óå± ÙBóWàbƒó€bᨠbå ±X B@a¨ÀBÿ ÿèÿÿ$X@x@ßA$A¨YàEn®‰bóñz{bóZ =áj•nŠ`=ájä^+c[à@ùl€@‡@Sãû' @ÿÿ¦\àJeÀJ þàJå±] B@aRàBÿÿ; @x@ÇA AR^àE{%‹aáÛâ û||aR_ =@þ€ƒ3-€=å"±`à@‹(€@ë c c­à0ç#å~‘ŸÿFƒåPà àbhÆ€bá ë!¨ £áë«c? B@a¨ÀBÿÿ/¼@x@ßA$A¨@àER°bóëc¯°bóA =áj„A±`=ä^"´Bà@å?€@å,±CàJQÀJ °ŒÄFëcD B@aRàBå±@x@ÇA AREàE`ø Ó@Y ˆâ û±±aRF =áR²á$R"½Gà@pû€~ñàü£àLpRtZ)øæpKåv@¸À=ÿÏ4HàbÜú€bàáÁjå±I B@jÀBñ@x@ßA$AjJàEæ³,àˆá j²²ajK =ájo·€=â½%-ä(+cLà@ʶ€@ã Åå±MàB* BàÁÁJ$ ƒä‘N B@JÀBå±@x@¿AAJOàEmo³bwàˆá J³³e±Pà=î¤FñQ =áˆUu€{å"±Rà@£p€@—ÿÿ,â óå±™öAëSàb  báá¨å ±T B@a¨ÀBÿÿL6@x@ßA$A¨UàEô*´a¨ëc´µbóV =áj%ë€=áj)Ïâó%±Wà@‡é€@å,±XàJóè JA#¥(ÁRå±Y B@ àBå±@x@ÇA ARZàE¢µaRàˆâ û¶¶aR[ =áR¯©€=äFáR%±\à@ ¥€@å ±#d`9ñìäþïn` t.Q! à*@B½]àbu¤€bàáÁjå±^ B@jÀBÿÿ @x@ßA$Aj_àEˆ]¶áj··aj` =ája€=â½%-â½%±aà@l`€@å$±bàBÌ_€Bá Jå±c B@JÀBå±@x@¿AAJdàE·áJ¸¸e±eà=+ƒ|å±f =áˆû€{H ãºüygà@I€@å ±ö Çâ óå±޲Aëhàb«á Âó!¨å ±i B@a¨ÀBñ@x@ßA$A¨jàE•Ô€ëëc¹ºbók =ájÔ¸aæájä^%±là@$“€@å,±màJ’ JA#áöå±n B@aRàBÿÿB@x@ÇA ARoàE£K¹aàˆå ±»»aRp =áReS€=å"±qà@·N€@óÓ!“ñ3b‘¾Kfýé ú£àÃa FB½ràb#N€bàá½å±s B@jÀB‰öÇ@x@ßA$AjtàE*ºáj¼¼aju =áj¦ €=â½å ±và@ @å$±wàBf €ƒá J$ ƒë cx B@JàB@Ùâø8ÿÿ @x@¿AAJyàE°Âà ˆâ µ½½e±zà=2Õ„FwÿÿL6{ =@®€Á©È€{âó% ç:(¥|à@ëÀ@Ÿëcâ ó屺´Bó}àbL bᨠ£á¨ŸÿÞ~ B@`æÀBÿÿ²º@x@ßA$A¨àE7~»bóÿÿt¾¿bó€ =áje>¼`=âó$áj%±à@Æ<€@â m' @ÿÿ ‚àJ2ÀJ â¸å±ƒ B@aRàBå±@x@ÇA AR„àEEõ€ÓãÇâ ûÀÀaR… =áRêü€=áR"½†à@Mø€@üyCêœ@äƒ(ð€î´üy¨EB½‡àb¹÷€bàá屈 B@jÀByå±@x@ßA$Aj‰àE˰½báóá jÁÁajŠ =ájS´€=áj%-â½+c‹à@¯³€@à~ÁñŒàBàBå± B@JÀBëc@x@¿AAJŽàERl¾áJÂÂe±à=Ó¤F|å± =áˆ:r€{áˆ% áˆ"ó‘à@m€@ÿ ÿ5n$F @è¥+câ óå± ·Aë’àbîá Áë!¨ bå ±“ B@a¨ÀBñ@x@ßA$A¨”àEÙ'¿a¨ñÃÄbó• =ájè€=ç%páj%±–à@hæ€@ÿ,ÿ —àJÔå JA#屘 B@aRàBñ@x@ÇA AR™àEæžÀaRãÇä FÅÅaRš =áR¦€=â½áR"½›à@ò¡€@å ±S×%¬à`uvuw+ha CaB½œàb^Àb@$ájëc B@jÀBzå±@x@ßA$AjžàEmZÁájÆÆajŸ =ájõ]€=ájå ± à@Q @á 屡àB±\€ƒà B$ ƒä–Ç¢ B@JÀBå±@x@¿AAJ£àEôÂaJã>â µÇÇe±¤à=u(ƒ|å±¥ =áˆØ€{áˆ% äF"ó¦à@*€@îW @å±(¥Sâóå±ç“Bó§àb€bᨠbᨋc¨ B@a¨ÀBÿÿ; @x¦ 9A$A¨© @€EzÑ€ˆñÈÉbóª =áj‘Ãaæå"±«à@ý€@ë,c¬àJiÀJ@Ý @çëc­ B@a ÀBëc@x@ÇA AR®àEˆHÄaàˆâ ûÊÊaR¯ =áR&P€=â½å ±°à@ˆK€@å ±cGÀ\uvïdpœÿÿ¦Eÿbþ±àbôJ bA;ájå±² B@jÀBå±@x@ßA$Aj³àEÅajäfËËaj´ =ájŸ€=áj%-ä(+cµà@û€@á å±¶àB[ BàÁéº ƒä…±· B@JÀBå±@x@¿AAJ¸àE•¿à Ëâ µÌÌe±¹à=Ò„Fÿÿ; º =áˆÅ€{áˆ% áˆ%±»à@ÐÀ€@å ±ë câ óå±áÿbþ¼àb1 bá ë'Z £á¨…±½ B@a¨ÀBöÇ@x@ßA$A¨¾àE{Æbóå±ÍÎbó¿ =áj7;Ç`=å"±Àà@—9€@å,±ÁàJÀJ äFå±Â B@aRàBå±@x@ÇA ARÃàE)ò Ó  ˆâ ûÏÏaRÄ =áRØù€=áR"½Åà@2õ€@ÿÿ5ns~ }{z‚y5µ@¸À®;D©Æàbžô€bä få±Ç B@jÀBüy@x@ßA$AjÈàE°­ÈbájÐÐajÉ =áj4±€=ä(%-ä(%±Êà@‘°€@á å±ËàBô¯€BàÁå±Ì B@JàB@Ù¢oÀBöÇ@x@¿AAJÍàE7iÉáJÑÑe±Îà=¸¤FxñÏ =@®€Á+o€{áˆ% áˆ"óÐà@rj€@ÿ ÿ* !ˆ @ëcâ óå± ÉAëÑàbÓá Áeå±Ò B@`æÀB î W!¨ÿÿB@x@ßA$A¨ÓàE¾$Êa¨ñÒÓbóÔ =ájåä€=ä^1Õà@Dã€@â m'*QöÇÖàJ°â J þâ¸å±× B@aRàBÿÿL6@x@ÇA ARØàEË›ËaRàˆå ±ÔÔaRÙ =áR^£€=áR"½Úà@¿ž€@ë c ƒ­àT‚7ð` ëcÊóB½Ûàb+ÀbA;ä#å±Ü B@jÀBëc@x@ßA$AjÝàERWÌájÕÕajÞ =ájâZ€=ä(å ±ßà@? @å$±ààBžY€ƒá J$ ƒéº‹cá B@JÀBëc@x@¿AAJâàEÙÍaJàËâ µÖÖe±ãà=Z%ƒ|ñä =áˆÅ€{áˆ%på±åà@€@ë câ óå±nBóæàbu€báå±ç B@a¨ÀBëc@x@ßA$A¨èàE_΀ˆëcרbóé =áj“ŽÎaæä^%±êà@òŒ€@å,±ëàJ^ÀJA#äFå±ì B@aRàBå±@x@ÇA ARíàEmEÏaàˆâ ûÙÙaRî =áRM€=áR%±ïà@qH€@ë c“åv´åwd`áXa ¨8B½ðàbÝG€bàá½å±ñ B@jÀBÿÿWš@x@ßA$AjòàEóÐájÚÚajó =áj€€=áj%-ä(+côà@Ý€@ã Å `@ån"ü@BõàB<àBå±ö B@JÀBÿÿ¦@x@¿AAJ÷àEz¼€ËáÓâ µÛÛe±øà=û¤Fñù = ÀÁf€{å"±úà@²½€@ÿÿ$Xâ óå± ÊA¨ûàb báÂó!¨&Uáë±ü B@`æÀBå±@x@ßA$A¨ýàExÑbóëcÜÝbóþ =áj,8Ò`=áj+"âó"´ÿà@Œ6€@å,±.à‹ø5 J å ± B@aRàBå±@x@ÇA ARàEïà ˆâ ûÞÞaR =áRªö€=äFáR%±à@ò€@ÿ ÿ £D«ï æ%¬àâzà ÿNàbrñ€bá jå± B@jÀBÿÿ/¼@x@ßA$AjàE•ªÓbŒ@Y§¥á jßßaj =áj!®€=â½å ± à@­€@å$± àBݬ€Bá J$ ƒä‹c B@JÀBëc@x@¿AAJ àEfÔaJàˆá Jààe± à=¤Fñ =ሠl€{áˆ% äF"óà@Xg€@—ÿ ÿÞ/ª @1a0ÿ¯üÃâ óå±ÿ6àb¸á å± B@a¨ÀBÿÿ$X@x@ßA$A¨àE£!Õa¨å±áâbó =ájÒá€=ájä^%±à@5à€@å,±àJ¡ß JA#£½ÁRëc B@aRàBÿÿ$X@x@ÇA ARàE°˜ÖaRàˆâ ûããaR =áRX €=å"±à@°›€@ë c ³&™åv$þpá? Ba ‘ Hoàb bàáÁjå± B@jÀBƒó Óÿÿ$X@x@ßA$AjàE7T×ájääaj =áj«W€=â½%-ä(+cà@  @å$±àBoV€ƒá Jå± B@JÀBÿÿ²º@x@¿AAJ!àE½ØáJååe±"à=?"Õ`=ÿÿ²º# =ሶ€{âó%páˆ%±$à@ú€@ÿ ÿ â óå±5þAë%àbY bᨠ£çZœy& B@a¨ÀBÿÿ @x@ßA$A¨'àEDË€ëñæçbó( =ájt‹Ùa¨âóå ±)à@׉€@å,±*àJCÀJA#å±+ B@aRàBå±@x@ÇA AR,àERBÚaãÇå ±èèaR- =áRíI€=áR"½.à@FE€@å ±ÃE¦G$1SFœ`Ÿÿý xb½/àb²D€bá jå±0 B@jÀBå±@x@ßA$Aj1àEØýà ˆá jééaj2 =áj`Ûa¨ájå ±3à@¾€@á ñ4àB àBå±5 B@JàB âø&ýÿÿB@x@¿AAJ6àE_¹à Ëá Jêêe±7à=à¤Fzëc8 = ž€ÁO¿€{áˆå ±9à@œº€@ë câ óå±Üh¥:àbûá È¥!¨ £å ±; B@`æÀBëc@x@ßA$A¨<àEætÜbµëcëìbó= =áj5Ý`=çå ±>à@y3€@ÿ,ÿ ?àJå2 JA#â¸ëc@ B@aRàBÿÿ; @x@ÇA ARAàEó뀈ãÇâ ûííaRB =áRŸó€=â½áR%±Cà@ÿî€@ñ ÓGÐ`äõvõwåxez`4a ^CHoDàbk bàá½å±E B@jÀBÿÿ›ò@x@ßA$AjFàEz§Þbàˆá jîîajG =ájþª€=ájå ±Hà@X @à~Áå±IàBº©€ƒà B$ ƒä‘J B@JÀBñ@x@¿AAJKàEcßáJïïe±Là=‚u„Få±M =áˆíh€{áˆ% ëcNà@9d€@ÿ ÿ,â óå±ú±BóOàbá Áë!¨ £á¨‹cP B@a¨ÀBöÇ@x@ßA$A¨QàE‡àa¨ÿÿôðñbóR =áj¸Þ€=å"±Sà@Ý€@è 2€$Ÿÿÿ,TàJ†Ü JA#å±U B@aRàBëc@x@ÇA ARVàE••áaRõ\òòaRW =áRA€=áR%±Xà@¡˜€@ÿ ÿ,ã•ï à-Q’ÀÀ8Ùa y…Yàb Àb@$ájå±Z B@jÀB{ñ@x@ßA$Aj[àEQâajÿÿ?‡óóaj\ =áj°T€=ä(%-ä(C]à@  @á å±^àBlS€ƒè g$ ƒä…±_ B@JÀBå±@x@¿AAJ`àE¢ ãaJâµôôe±aà='ƒ|å±b =ሓ€{âó% áˆ"ócà@ã €@”öÇâ óå±%mdàbB bá ë!¨ £á¨…±e B@a¨ÀBå±@x@ßA$A¨fàE)È€ëå±õöAg =ájVˆäaæáj%páj6Çhà@¸†€@å,±iàJ$ÀJA#äFëcj B@aRàBå±@x@ÇA ARkàE6?åaâû÷÷aRl =áRçF€=áR"½mà@GB€@å ±ó|}ÐÐQ€åxuða Ù!D©nàb³A€bä få±o B@jÀBå±@x@ßA$AjpàE½ú€ˆájøøajq =áj=þ€=å"±rà@˜ý€@á å±sàBýü€Bâ µ$å ±t B@JàB@Ùâøñ@x@¿AAJuàED¶æâµùùe±và=ɤFvñw =@®€Á4¼€{áˆ% äF"óxà@…·€@å ±!ˆ @üyKâ óå±}ÓBóyàbä¡á¨ bå ±z B@`æÀBå±@x@ßA$A¨{àEËqça¨¼ Z¡îîW úûbó| =ájô1è`=ä^%±}à@U0€@â mëc~àJÁ/ J ®ÀÉ̶å± B@aRàBÿÿ¦@x@ÇA AR€àEØè€ˆäFüüaR =áR™ð€=áR"½‚à@ðë€@öǤ$qŠàÿGáQ‡@ÿÿ¸l.ÿB½ƒàb\ bá j屄 B@jÀBÿÿÃÐ@x@ßA$Aj…àE_¤ébájýýaj† =ájë§€=ä(%-ä(+c‡à@F @å$±ˆàB§¦€ƒá J屉 B@JÀBëc@x@¿AAJŠàEæ_êaJì®þþe±‹à=kr„FëcŒ =áˆÚe€{áˆ% áˆ"óà@'a€@ÿ ÿ>$F @å±Ãâ ó届 AëŽàb†`€bá ë!¨ bå ± B@a¨ÀBöÇ@x@ßA$A¨àElëa¨üyÿKc‘ =áj¡Û€=ájë c’à@Ú€@å,±“àJoÙ JA#¦{å±” B@aRàBå±@x@ÇA AR•àEz’ìaRàˆçA@aR– =@€ƒ3š€=å"±—à@†•€@â ½(o!“â½%±õxÒ¯å à!äþtßÿ€ÞB½˜àbò”€bàáÁjå±™ B@€æàBñ@x@ßA$AjšàENíájaj› =ájQ€=ájå ±œà@èP€@ã Å!j @ãÅ(®AJàBIàB$àB±ž B@JÀBÿÿ @x@¿AAJŸàE‡ îáJ@= à= ƒ|ÿÿ/¼¡ =ሃ€{äF% å±¢à@Å €@ÿ ÿÔæ!ˆ @âóå±zbA¨£àb# bâ ó!¨ bá뱤 B@a¨ÀBÿÿ@Ò@x@ßA$Aj¥àEÅ€ëüybó¦ =áj/…ïaæáj$áj"´§à@‘ƒ€@å,±¨àJý‚ JA#屩 B@aRàBå±@x@ÇA ARªàE<ðaàˆå ±aR« =áRÉC€=äFáR(o¬à@#?€@üy#Qb$ZøæÓ”ÈPäÿ€a /B½­àb>€bàáå±® B@jÀBÿÿ¦@x@ßA$Aj¯àE¢÷à ˆá jaj° =áj2û€=â½%-â½+c±à@Žú€@å$±²àBîù€Bá J$ ƒä–dz B@JÀBëc@x@¿AAJ´àE)³ñâµe±µà=ª¤Fÿÿ; ¶ =ሹ€{áˆ% áˆc·à@c´€@—öÇâ óå±·ÝAë¸àbÅá Âó!¨ £á¨ÿüĹ B@a¨ÀBñ@x@ßA$A¨ºàE°nòa¨å±  bó» =ájá.ó`=ájä^%±¼à@B-€@ˆ@SŸÿWÁ`@üy½àJ®, JA#àJñ¾ B@aRàBÿÿ$X@x@ÇA AR¿àE½åà ˆä F  aRÀ =@þ€ƒfí€=áR"½Áà@Áè€@å ±3î´éï`G«ÁFcüy±vÇÂàb- bá jëcà B@€æÀBå±@x@ßA$AjÄàED¡ôbñ  ajÅ =ájÔ¤€=áj%-ä(%±Æà@/ @á )Ú @ãÅ"ü@BÇàB£€ƒà B$àB¥±È B@JÀBå±@x@¿AAJÉàEÊ\õaJàˆâ µ  e±Êà=Lo„Få±Ë =ሻb€{è¥% áˆ"óÌà@^€@å ±!ˆ @ëcâ óå±ÓA¨Íàbf]€báÄ©!¨ bá륱ΠB@a¨ÀBñ@x@ßA$A¨ÏàEQöá¨bóÐ =ájú€=ájå ±Ñà@Y€@å,±ÒàJÅ JA#å±Ó B@aRàBÿÿô@x@ÇA ARÔàEØÓà ˆâ ûaRÕ =áRƒÛ€=äFáR"½Öà@àÖ€@å ±CGSå0àäõv"Fça dÅB½×àbL bá j屨 B@jÀBÿÿô@x@ßA$AjÙàE_÷b½êäá jajÚ = …€ƒÛ’€=ájä(%±Ûà@8 @á $( @ãÅ¥±ÜàB—‘€ƒà B$àB¥±Ý B@€ÆÀBå±@x@¿AAJÞàEåJøaJàˆá Je±ßà=í¤F÷ÿÿnbà =áˆÖP€{äF% å±áà@!L€@ô )ø @å±+câ óå±À7AëâàbK€báå±ã B@a¨ÀBå±@x@ßA$A¨äàElùa¨üybóå =áj˜Æ€=ájä^"´æà@ûÄ€@å,±çàJgÀJ å±è B@aRàBÿÿ @x@ÇA ARéàEz}úaRäFaRê =áR)…€=áR%±ëà@~€€@ÿ7!“â½1S%¨ õw"®à$ý`%$ýa ÿŽìàbê€bàá½å±í B@jÀB†öÇ@x@ßA$AjîàE9ûajáóä fajï =ájx<€=å"±ðà@Ö;€@á å±ñàB< BàÁÁJå±ò B@JÀBÿÿô@xð 9AAJó @€E‡ô€Ëàˆá Je±ôà=ƒÇÿÿôõ =áˆú€{áˆ%på±öà@Ãõ€@ ñ !ˆ @âó屸ÿŽ÷àb# báᨠbç«cø B@abÀBÿÿô@x@ßA$A¨ùàE°übóå±bóú =ájFpý`=ä^%±ûà@©n€@ÿ,ÿBüàJÀJA#å±ý B@aRàBå±@x@ÇA ARþàE'þaáÛâ ûaRÿ =áRË.€=áR%±/à@'*€@ë cc`2$p;ñ¥‘¼îð£ïðñ Rà™ÐHoàb“)€bàá½å± B@jÀBëc@x@ßA$AjàE¢âà ˆá jaj =áj&æ€=ä(*ßä(+cà@„å€@â r$( @ãÅ"ü@BàBæä€Bá Jå± B@JàB@ÙâøÿÿÞ@x@¿AAJàE)žÿbµàËá Je± à=ª¤FyÿÿÞ =@®€Á¤€{áˆ% áˆ"ó à@eŸ€@ÿ ÿBë câ óå±="A¨ àbÅá Âó!¨ £áëivÇ B@`æÀB è ¥!¨ÿÿ5n@x@ßA$A¨àE¯Y6 /ëcbó =ájÏ`=áj)Ïáj"´à@2€@â m!j @öÇàJž J Íâ¸öÇ B@aRàBÿÿ @x@ÇA AR;``E½Ðà ˆâ ûaR =áRqØ€=å"±à@ÉÓ€@ÿ ÿÞs¥‘šeª­à0å"$™àÕßB½àb5 bá jå± B@a$àBÿÿ¦@x@ßA$AjàECŒböÇaj =ájØ€=áj%pâ½!à@2 @äåÃÅå±àB”Ž€ƒà B$ ƒäèF B@JÀB‹@Ùå±@x@¿AAJàEÊGaJáÓâ µ  e±à=KZ„F~å± =@®€ÁêM€{äF% áˆ%± à@7I€@—ëcâ óå±q-Aë!àb–H€báÁë!¨ £á¨…±" B@`æÀBñ@x@ßA$A¨#àEQa¨ñ!"bó$ =áj‰Ã€=áj%páj%±%à@ìÁ€@å,±&àJXÀJ ⸠`‹áR-‚û' B@aRàBå±@x@ÇA AR(àE^zaRàˆâ û##aR) =áR‚€=äFáR"½*à@c}€@‹â ½ @î! ƒ# "‘šä𣥲Á'`Vÿ; +àbÎ|€bàá½å±, B@jÀBÿÿbþ@x@ßA$Aj-àEå5áj$$aj. =ájm9€=â½%-â½%±/à@È8€@å nå±0àB) Bá J$ ƒÁ¥±1 B@JÀBöÇ@x@¿AAJ2àElñ€Ëæü%%e±3à=í¤Fÿÿ 4 =áˆ`÷€{áˆ% áˆ"ó5à@­ò€@å ±)ø @âó6Çâ óå±òAë6àb  bᨠbᨅ±7 B@a¨ÀBå±@x@ßA$A¨8àEó¬bóñ&'bó9 = …`ƒm`=áj$áj%±:à@yk€@å,±;àJåj J å ±< B@€ÎÀBÿÿÞ@x@ÇA AR=àE$ aàˆå ±((aR> =áR¨+€=áR"½?à@'€@ñ “C#6Å ¯à´uvuwa ù?B½@àbp&€bàáå±A B@jÀBå±@x@ßA$>BàE‡ßà ˆá j))ajC =áj ã€=áj%-â½%±Dà@fâ€@å$±EàBÇá€Bá Jå±F B@JÀBå±@x@¿AAJGàE › âµ**e±Hà=­„FÿÿôI =áˆú €{áˆ% áˆ"óJà@Kœ€@ÿ ÿWš': @å±9»â óå±]ÍAëKàb©á Âó!¨ bå ±L B@a¨ÀB™ñ@x@ßA$A¨MàE”V a¨ëc+,bóN =áj± `=ê%±Oà@€@å,±PàJ JA#ëcQ B@aRàBëc@x@ÇA ARRàE¢Íà ˆä F--aRS =áRbÕ€=â½â ½Tà@ºÐ€@ñ £%¯°à$ ÿE¶4þa &B½Uàb& bá jëcV B@jÀBÿÿô@x@ßA$AjWàE(‰ bàˆá j..ajX =ájµŒ€=â½å ±Yà@ @à~ÃÅ$( @ãÅ"üAJZàBp‹€ƒà B$àB«c[ B@JÀBå±@x@¿AAJ\àE¯DáJ//e±]à=0W„FÿÿÞ^ =ሣJ€{âó% å±_à@íE€@ÿ ÿ@Òë câ ó层ëA¨`àbK bᨠ£áë«ca B@a¨ÀBöÇ@x@ßA$A¨bàE6á¨00bóc =ájâ€=âóä^"´dà@B€@å,±eàJ® J å±f B@aRàBÿÿ @x@ÇA ARgàE½»€ˆéyä F11aRh =áRkÀ=áR(oià@ɾ€@ë c³ÿÿŠ×0æ QbÀ/‚B½jàb5 bàáÄ©å±k B@jÀB}è oÿÿ; @x@ßA$AjlàECw"À áóá j22ajm =áj»z€=ä(%-ä(+cnà@ @á å±oàB{y€ƒàÁÁJå±p B@JàB@ÙÿÿÔ!Kÿÿ5n@x@¿AAJqàEÊ2áJ33e±rà=Ò¤Fÿÿ­s =@®€Áº8€{áˆ% áˆ"ótà@4€@ÿ ÿÞ!ˆ @ëcâ ó屋pAëuàbf3€bᨠbå ±v B@`æÀBñ@x@ßA$A¨wàEQîà ˆâ ó44bóx =ájòò€=ájå ±yà@Uñ€@â m' @üyzàJÁð JA#â¸öÇ{ B@aRàBëc@x@ÇA AR|àEשâû55aR} =áR„±€=å"±~à@଀@å ±ÃÿÿŠ×ÿ…5a  XB½àbL bä ©å±€ B@jÀBÿÿ,@x@ßA$AjàE^eáj66aj‚ = …€ƒêh€=â½å ±ƒà@E @á $( @ãÅ(®AJ„àB¦g€ƒâ µå±… B@€ÆÀBëc@x@¿AAJ†àEå áJ77e±‡à=í¤Fÿÿ›òˆ =áˆÕ&€{áˆ% 屉à@""€@—ù »$F @å±6Çâ óå±®ªA¨Šàb!€bᨠbáë«c‹ B@a¨ÀBå±@x@ßA$A¨ŒàElÜ€ˆöÇ89bó =ájˆœaæáj%páj"´Žà@Ꚁ@å,±àJVÀJA#å± B@aRàBöÇ@x@ÇA AR‘àEySaàˆç ::aR’ =áR.[€=å"±“à@V€@ˆâ ½$F!“â½3ÓÓ@äõzT˵zuy"ïa v,B½”àbùU b@$áj展 B@jÀBÿÿ; @x@ßA$Aj–àEáj;;aj— =ájˆ€=â½%pâ½+c˜à@ã€@å$±™àBH Bá J屚 B@JàB@Ùáëc@x@¿AAJ›àE‡Êà Ëâ µ<bó¢ =áj:F`=áj$áj%±£à@œD€@â m' @ëc¤àJÀJA#â¸ëc¥ B@aRàBå±@x@ÇA AR¦àEý€Óÿu??aR§ =áRÇá$R"½¨à@'€@ˆå±ãðzåzÔ "ƒ(tðÿÿ]LÿB©àb“ÿ€áá j屪 B@jÀB|ñ@x@ßA$Aj«àE¡¸ ë@Y¡óä f@@aj¬ =áj2¼€=çå ±­à@»€@ã Å' @ãÅ"üD®àBîºàB$àB¶Ç¯ B@JÀBÿÿÞ@x@¿AAJ°àE(tbwáJAAe±±à=©¤FöDz =áˆz€{äF% äF"ó³à@fu€@å ±$F @è¥+câ óå±¹ÿB´àbÄá ò¾ báë«cµ B@a¨ÀBå±@x@ßA$A¨¶àE¯/a¨ëcBCbó· =ájàï€=âó%páj"´¸à@Bî€@å,±¹àJ®í JA#£½ÁR屺 B@aRàBñ@x@ÇA AR»àE¼¦aRàˆä FDDaR¼ = …€ƒU®€=â½áR"½½à@¸©€@å ±óäƒNaƒZàTûðäÿÿh°Þ°Df¾àb$Àb µáj屿 B@€æÀBÿÿyÆ@x@ßA$AjÀàECbájEEajÁ =áj·e€=áj%-â½+cÂà@ @á å±ÃàBsd€ƒà Bå±Ä B@JÀBñ@x@¿AAJÅàEÊáJFFe±Æà=K0ƒ|å±Ç =ሶ#€{áˆ% áˆ"óÈà@€@å ±!ˆ @å±.Wâ óå±zBAëÉàbf€bᨠbå ±Ê B@a¨ÀBå±@x@ßA$A¨ËàEQÙ€ˆå±GHbóÌ =ájf™ aæå"±Íà@Ç—€@å,±ÎàJ3ÀJA#å±Ï B@aRàBëc@x@ÇA ARÐàE^P!aàˆå ±IIaRÑ =áRÿW€=â½å ±Òà@bS€@å± ƒFðõzõxõyõ}‚àõa 0mB½ÓàbÎR b@$ájå±Ô B@jÀBå±@x@ßA$AjÕàEå "ájJJajÖ =ájm€=ájå ±×à@È€@á $( @ãÅ(®AJØàB)àBå±Ù B@JÀBå±@x@¿AAJÚàEkÇà Ëâ µKKe±Ûà=í¤Få±Ü =áˆ\Í€{äF% å±Ýà@¥È€@ÿÿôâ óå±”ŸA¨Þàb bá ë!¨ £áë«cß B@a¨ÀBöÇ@x@ßA$A¨ààEò‚#bóñLMbóá =áj C$`=ájä^"´âà@A€@å,±ãàJí@ J äFëcä B@aRàBz àBÿÿüÄ@x@ÇA ARåàEú€ˆï+â ûNNaRæ = p€ƒ¥%á$R(oçà@ý€~ÿÿÞ ~äõYàõ`äõõå±vB½èàblü€bá jå±é B@€æÀBÿÿ @x@ßA$AjêàE†µ,àˆá jOOajë =!! ƒþ¸€=å"±ìà@Z @áéÍÖå±íàBº·€ƒàÁÀB$åô±î B@JÀBå±@x@¿AAJïàE q&bwàˆá JPPe±ðà=Žƒ„Fwÿÿôñ =áˆw€{áˆ% å±òà@Gr€@ÿÿ$Xâ óå±ÒåBóóàb©á ᨠ£å ±ô B@a¨ÀBÿÿ$X@x@ßA$A¨õàE”,'a¨ëcQRbóö =ájÆì€=å"±÷à@'ë€@‡@Så#2€%òöÇøàJ“ê JA#àJå±ù B@aRàBå±@x@ÇA ARúàE¡£(aRâûSSaRû =@þ€ƒ[«€=áR%±üà@¥¦€@â½!Rî! #ƒ ð£ ÿïþmîà¿b½ýàb bàá½å±þ B@€æÀB‘öÇ@x@ßû =Ajÿ @`E(_)ájTTaj0   `=°b)`=ä(%-ä(1à@  @â rå±àBla€ƒá Jå± B@€ÆÀBå±`x@9 AJ @€E¯*a àˆå ±UUe±à=0-ƒ|ëc =ሯ €{áˆ% áˆ"óà@é€@¨ù »$F @âó+câ óå±h¥àbK báÂó!¨ bå ± B@abÀB¬â ó>"ÿÿ$X@x@ßA$A¨ àE5Ö€ëëcVWbó =áj`–+aæG rå%dáj+c à@À”€@ë,c àJ,ÀJ Šà‹å± B@aRàB{ëc@x@ÇA ARàECM,aàˆâ ûXXaR = …€ƒU€=áR"½à@SP€@â ½$Få ±3ðï´ôSþSýSûS ŒàÜAD©àb¿O€bàá½å± B@€æÀBÿ ÿÿÿ @x@ßA$AjàEÊ-ajå2á jYYaj =ájR €=ä(å ±à@® €@à~Á!j @ãÅ"üAJàB Bá J$àB«c B@JÀBå±@x@¿AAJàEPÄ€Ëàˆá JZZe±à=ѤFÿÿyÆ =áˆYÊ€{âó% äF"óà@‹Å€@³å ±!ˆ @å±(¥â óå±x³A¨àbìá Áë!¨ báë± B@a¨ÀB´å±@x@ßA$A¨àE×.bóå±[\bó =áj@/`=âó$áj"´!à@f>€@å,±"àJÒ= J ëc# B@aRàBå±@x@ÇA AR$àEäöà ˆâ û]]aR% =áR§þ€=â½áR"½&à@ñù€@ë cC÷SïSßäÿtZ/øQÁàÅÿŠÜ'àb] bä ©å±( B@jÀBëc@x@ßA$Aj)àEk²0bÿÿ—^^aj* =ájûµ€=â½%-â½+c+à@X @á $( @ãÅ"ü@B,àB·´€ƒâ µ$àB¥±- B@JÀBå±@x@¿AAJ.àEòm1áJ__e±/à=s€„F€ñ0 =áˆös€{áˆ% áˆ"ó1à@-o1`¼«å ±ë câ óå±a‰A¨2àb’n€bᨠ£á륱3 B@a¨ÀBëc@x@ßA$A¨4àEy)2`ëñ`abó5 =áj«é€=ájä^"´6à@ è€@ñ,7àJwç€Já Rëc8 B@aRàBÿÿ@Ò@x6 9A AR9 @`E† 3aRàˆå ±bbaR: =áRA¨€=G@Qæxì ¶"½;à@’£€@ÿÿ!“èoSTþðQÃT÷ðï´íSuý@zÀ,üB½<àbþ¢€bàáÈoå±= B@a$àBŠè oëc@x@ßA$Aj>àE \4ájccaj? =áj¥_€=â½%-`±€3ÒA@à@ @å$±AàBa^€ƒá Jå±B B@JÀBå±@x@¿AAJCàE”5aJàËâ µdde±Dà=*ƒ|tå±E =ሌ€{áˆ% áˆ"óFà@Ï€@ÿÿtâ ó屈AëGàb0 báÂó!¨ £å ±H B@a¨ÀBöÇ@x@ßA$A¨IàEÓ€ëëcefbóJ =ájI“6aæáj$áj%±Kà@©‘€@ë,cLàJÀJA#äêå±M B@aRàBå±@x@ÇA ARNàE(J7aàˆâ ûggaRO =áRÚQ€=áR"½Pà@0M€@”è o)ø!“å±cSu÷SuûSuþ"tc/õ‚ÿÿ]L3ÿ QàbœL€bàá½å±R B@jÀBÿÿ@Ò@x@ßA$AjSàE®8ájhhajT =áj7 €=çå ±Uà@“€@á !j @ãÅ"üDVàBó€BäàBiŸÿ2rW B@JÀBÿÿ @x@¿AAJXàE5Á€ˆàËâ µiie±Yà=¶¤Fÿÿ…*Z =áˆ-Ç€{âó% äF"ó[à@q€@å ±ë câ óå±—ÿ \àbÑá Âóå±] B@a¨ÀBå±@x@ßA$A¨^àE¼|9bóëcjkbó_ =ájÛ<:`=âó$áj"´`à@;;€@å,±aàJ§: J å±b B@aRàBñ@x@ÇA ARcàEÉóà ˆâ ûllaRd =áR|û€=â½áR"½eà@Õö€@ÿ ÿ,s4‚õƒ"màd`&äþîµàªÿ fàbA bá jå±g B@jÀBÿÿ; @x@ßA$AjhàEP¯;bàˆá jmmaji =ájè²€=áj%-â½1jà@E @à~ÃÅ `@å ±kàB¤±€ƒà B$ ƒàB¥±l B@JÀBëc@x@¿AAJmàE×j<áJnne±nà=X}„Få±o =áˆÇp€{áˆå ±pà@l€@ÿ ÿÞ!ˆ @è¥â óå±ÿ qàbsk€bᨠb᨟ÿ5nr B@a¨ÀBÿÿÞ@x@ßA$A¨sàE^&=a¨å±opbót =áj‰æ€=å"±uà@èä€@å,±vàJTÀJA#å±w B@aRàBëc@x@ÇA ARxàEk>aRàˆä FqqaRy =áR¥€=â½å ±zà@w €@ÿ ÿ ƒ $cptð€tc.pa ÈëD©{àb㟠b@$ájå±| B@jÀBÿÿnb@x@ßA$Aj}àEòX?ájrraj~ =ájv\€=áj%-å±à@Ó[€@á $( @ãÅ(®AJ€àB2àBå± B@JÀBå±@x@¿AAJ‚àEx@áJsse±ƒà=ú¤F屄 =áˆ]€{äF% áˆ(¥…à@°€@î Wë câ óå±lA¨†àb bá ë!¨ £á륱‡ B@a¨ÀBÿÿ§V@x@ßA$A¨ˆàEÿÏ€ëñtubó‰ =áj#Aaæájä^"´Šà@†Ž€@å,±‹àJò JA#äFñŒ B@aRàBÿÿL6@x@ÇA ARàE GBaîèå ±vvaRŽ =áR¬N€=áR"½à@ J€@å ±“àd`e«àðî´a «­B½àbyI b@$áj屑 B@jÀBzöÇ@x@ßA$Aj’àE“Cajüywwaj“ =áj'€=å"±”à@…€@áéëc•àBã€Bá J$åô«c– B@JÀBå±@x@¿AAJ—àE¾à ˆâ µxxe±˜à=›Ð„FÿÿŽ™ =áˆÄ€{áˆ% 屚à@V¿€@å ±': @ëcâ óå±_Bó›àb¶á Áë!¨ bå ±œ B@a¨ÀBÿÿô@x@ßA$A¨àE¡yDbóëcyzbóž =ájÍ9E`=ä^%±Ÿà@08€@å,± àJœ7€Jã >屡 B@aRàBå±@x@ÇA AR¢àE®ð€ˆã„â û{{aR£ =áR^ø€=áR%±¤à@¾ó€@å ±£Ü"ƒ*t‹‰ ßa JÿÝV¥àb*Àb@$屦 B@jÀBëc@x@ßA$Aj§àE5¬Fbàˆá j||aj¨ =áj¥¯€=ä(%-ä(1©à@ @â r$( @ãÅ"ü@BªàBi®€ƒàÁäàB¥±« B@JÀBÿÿWš@x@¿AAJ¬àE¼gGáJ}}e±­à==z„FÿÿWš® =ረm€{áˆ% áˆ"ó¯à@øh€@å ±ë câ óå±î…A¨°àbX bᨠ£áë«c± B@a¨ÀBå±@x@ßA$A¨²àEB#Ha¨Œ@Y£|â ó~~bó³ =ájõ'€=ájë c´à@W&€@‡@S ~°!j @ÿÿBµàJÃ% JàÉÀJå±¶ B@aRàBÿÿB@x@ÇA AR·àEÉÞ€ˆáRaR¸ =@þ€ƒ{æ€=å"±¹à@Ýá€@å ±³ ä æ! !!2a (žB½ºàbIÀb@$áµëc» B@€æÀBÿÿ¦@x@ßA$Aj¼àEPšIb½àˆâ ½€€aj½ =ájÄ€=â½%-ä(%±¾à@  @â r屿àB€œ€ƒàÁµ)º ƒäœyÀ B@JÀBëc@x@¿AAJÁàE×UJaJÿÿyÆe±Âà=â¤Fñÿÿbþà = ÀÁÃ[€{áˆ% áˆ%±Äà@W€@ÿÿ$Xâ ó屌lAëÅàbwV€bá ë!¨ £á¨–ÇÆ B@`æÀB”î WÿÿB@x@ßA$A¨ÇàE]Ka¨ñ‚ƒbóÈ =áj“Ñ€=áj%pájKÉà@ôÏ€@ë,cÊàJ`ÀJ þäFå±Ë B@aRàBüy@x@ÇA ARÌàEkˆLaRàˆä F„„aRÍ = …€ƒ €=áR"½Îà@ƒ‹€@å ±Ã!E!Q![!`!o Œàå¤B½Ïàbb@&å±Ð B@€æÀBå±@x@ßA$AjÑàEòCMajãFá j……ajÒ = …€ƒ‚G€=ç%-â½%±Óà@ÝF€@á å±ÔàB> BàÁå±Õ B@€ÆÀBÿÿ,@x@¿AAJÖàExÿà Ëá J††e±×à=ù¤FÿÿBØ =áˆ`NaÆâó% áˆ"óÙà@³€@å ±)ø @ñÃâ óå±AVAëÚàb bá ëå±Û B@a¨ÀBå±@x@ßA$A¨ÜàEÿº€ë屇$AÝ = …`ƒ!{Oajâóå ±Þà@‚y€@å,±ßàJîx J äFå±à B@€ÎÀBëc@x@ÇA ARáàE 2Paàˆâ û‰‰aRâ =áRÂ9€=â½áR"½ãà@5€@ÿÿ@Ò Óù!²!e!ÉJK!a ò­B½äàbˆ4€bàáÄfëcå B@jÀB‚è o @'@x@ßA$AjæàE“íà ˆá jŠŠajç =áj#ñ€=ájå ±èà@ð€@á å±éàBßïàB$ ƒa5 i‹cê B@A"àBëc@x@¿AAJëàE©Qⵋ‹e±ìà=›»„Få±í =ሠ¯€{áˆ%på±îà@Tª€@ÿÿ„ @å±â óå±VvÇïàb¶á Âó!¨ bᨋcð B@a¨ÀBÿÿ,@x@ßA$A¨ñàE¡dRa¨öÇŒbóò =áj×$S`=å"±óà@7#€@å,±ôàJ£" JA#éøå±õ B@aRàBëc@x@ÇA ARöàE®Ûà ˆä FŽŽaR÷ =áR\ã€=â½å ±øà@¾Þ€@'aá$F @â½ ãÕ!ÐQi â Uà`å± E±ùàb*Àb aájå±ú B@jÀBüy@x@ß÷ =Ajû @`E5—Tbàˆá jajü =ájÁš€=áj%-ä(+cýà@ @à~ÃÅ!j @ãÅ.`@BþàB}™€ƒà B$àB¥±ÿ B@aàBå±@x@¿AAJ¢€E»RUáJe±à==e„Få± =ሠX€{áˆ% áˆ(¥BÄ À¼òS€@ö Çâ ó ›”hv¼ï/GÌï?¦RA¨à£X bí £á륱 B@`¥ÀBÿÿ,@x@ßA$A¨àEBVa¨ëc‘’bó =á)e΀=å"±à@ÅÌ€@å,± àJ1ÀJA#å± B@aRàBå±@x@ÇA AR àEP…WaRàˆä F“-Ä,¶ =áRòŒ€=áR"½ à@Tˆ€@å ±óÀI–!Õå´a a•B½àbÀ‡ b@$ájå± B@jÀByëc@x@ßA$AjàEÖ@Xáj”#Ë!j = …€ƒcD€=áj%-ä(%±à@¿C€@ã Åå±àB Bá Jå± B@€ÆÀBå±@x@¿AAJàE]ü€Ëàˆâ µ••e±à=Þ¤Få± =áˆIYaÆå"±à@˜ý€¼!åâ!ˆ%N .`# +câ óå±ÅgAë 3à£ùá Ç!¨ bå ± B@a¨ÀBüy@x@ß =A¨ @€Eä·,ᨖ–bó =àÆ¼€=áj)Ïâó(fà@ðº€@å,±àJ\ÀJ@ÝäFëc B@a àBÿÿ@Ò@x@ÇA AR àEksZbãÇâ û——aR! =áR{€=äFáR%±"à@ov€@ñ!+VÀ?ve´ã+ià|êB½#àbÛu€bá jå±$ B@jÀBÿÿQè@x@ßA$Aj%àEñ.[ajàˆá j˜//%±& =ájq2€=â½å ±'à@Ì1€@áéÃÅ `@ãÅ"üAJ(àB- BàÁÀB$ ƒàB«c) B@JÀBå±@x@¿AAJ*àExê€Ëàˆá J™™e±+à=€¸ƒÇÿÿQè, = ÀÁdð€{áˆ% äF"ó-à@±ë€@—å ±ö Çâ ó›ëcØA¨.àb báᨠ£á¨‘/ B@`æÀBå±@x@ßA$A¨0àEÿ¥\bóñš›bó1 =ájf]`=ájä^"´2à@rd€@ÿ,ÿ,3àJÞc JA#å±4 B@aRàBå±@x@ÇA AR5àE ^aáÛâ ûœœaR6 =áR±$€=å"±7à@ €@ëc!‘ À<Ûe´ÿÿ¦ÑB½8àb| bA;ájå±9 B@jÀB|ëc@x@ßA$Aj:àE“Øà ˆá jaj; =ájÜ€=â½%-ä(+c<à@vÛ€@â rå±=àB×Ú€Bá Jå±> B@JÀBå±@x@¿AAJ?àE”_ⵞže±@à=›¦„Fzÿÿ$XA =ሚ€{áˆ% áˆ%±Bà@W•€@.´â!ˆ @ .`‹câ óå±uÿ6C 3ࣶá å±D B@a¨ÀBå±@x@ßA$A¨EàE O`a¨ñŸ bóF =àÆÉa`=ájå ±Gà@+€@ë,cHàJ—  JA#äFëcI B@aRàBüy@x@ÇA ARJàE®Æà ˆä F¡¡aRK =áRV΀=áR"½Là@¶É€@å ±#+VÅ´Jo!ÕÑa }cD©Màb"Àb@$ájå±N B@jÀBå±@x@ßA$AjOàE5‚bbå2á j¢¢ajP =áj¹…€=ájå ±Qà@ @à~ÃÅ$( @éw(®AJRàBy„€ƒà Bå±S B@JÀBã C=Åÿÿbþ@x@¿AAJTàE»=caJáÓá J££e±Uà=@P„Fwå±V = ž€Á§C€{áˆ%på±Wà@ù>€@å ±ë câ óå±Ù‘A¨XàbW báÁë!¨ £áë«cY B@`æÀBå±@x@ßA$A¨ZàEBù€ë層¥bó[ =ájg¹daæê"´\à@É·€@â m!j%ò`ò Ã! !% … AR]àJ5ÀJ äFå±^ B@aRàBëc@x@ÇA AR_àEOpeaàˆâ û¦¦aR` =áRüw€=â½!â½aà@\s€@å ±3ˆ àå´{দAjbàbÈr€bä få±c B@jÀBå±@x@ßA$AjdàEÖ+fáj§§aje =ájV/€=áj%pájCfà@±.€@áéÃÅå±gàBàB$ ƒäŸÿ,h B@JÀBëc@x@¿AAJiàE]ç€ËÿÿF¨¨e±jà=Þ¤F{å±k =áˆMí€{áˆ% áˆ(¥là@žè€@öÇâ óå±TAëmàbý¡á ë!¨ £á¨‘n B@a¨ÀBå±@x@ßA$A¨oàEä¢gbóñ©ªbóp =ájch`=å"±qà@za€@å,±ràJæ` JA#å±s B@aRàBëc@x@ÇA ARtàEñiaàˆå ±««aRu =áR’!€=â½å ±và@õ€@å ±C+Vˆ æ%´ !Õa y…wàbaÀb@$ájå±x B@jÀBüy@x@ßA$AjyàExÕ ë@Y ˆá j¬¬ajz =ájøØ€=áj%-ä(%±{à@S @à~å±|àB´×€ƒà B$ ƒä…±} B@JÀBå±@x@¿AAJ~àEÿjbµç…ÿÿ i A8­­e±à=€£„Få±€ =ÀÁã–€{áˆ% áˆ%±à@4’€@å ±)ø @ .`Q6Çâ óå±±m‚ 3ࣛ‘€báÁë!¨ bᨅ±ƒ B@a¨ÀBÿÿ,@x@ßA$A¨„àE…Lka¨ëc®¯bó… =àÆ³ l`=å"±†à@ €@å,±‡àJ€  J £½ÁRëcˆ B@aRàBå±@x@ÇA AR‰àE“Ãà ˆâ û°°aRŠ = …€ƒ@Ë€=áR"½‹à@£Æ€@å ± SYår0â|€uJ‡@ÿÿh°ñðD©ŒàbÀb µájå± B@€æÀBxöÇ@x@ßA$AjŽàEmbàˆá j±±aj = …€ƒ¢‚€=áj%-ä(%±à@ý€@ã Åëc‘àB^àB$ ƒä…±’ B@€ÆÀBÿÿB@x@¿AAJ“àE :náJ²²e±”à=!M„F展 = ÀÁˆ@€{å"±–à@Ú;€@å ±!ˆ%N .`E±+câ óå±UAë— 3à£< bâj W!¨ bᨅ±˜ B@`æÀBÿÿB@x@ßA$A¨™àE'ö€ëëc³´bóš =àÆU¶oaæâóê1›à@¶´€@å,±œàJ"ÀJA#aHáRå± B@aRàBå±@x@ÇA ARžàE4mpaàˆä FµµaRŸ =áRât€=áR%± à@Dp€@å ±c€pE² K†€aQi0áàvDB½¡àb°o b@(ájå±¢ B@jÀBëc@x@ßA$Aj£àE»(qáj¶¶aj¤ =áj3,€=ä(%-å±¥à@+€@å$±¦àBó*€Bá J$ ƒä…±§ B@JàB@ÙáöÇ@x@¿AAJ¨àEBäà ˆâ µ··e±©à=äFöǪ =@®€Á*ê€{áˆáˆ"ó«à@|å€@å ±ë câ óå±Ò¶Bó¬àbÞá å±­ B@`æÀBå±@x@ßA$A¨®àEÉŸrbó屸¹bó¯ =ájß_s`=ä^%±°à@?^€@â m!j%òöDZàJ«] JA#â¸å±² B@aRàB{7,àBÿÿ]L@x@ÇA AR³àEÖtaàˆâ ûººaR´ =áR|€=áR"½µà@Ö€@ÿ ÿÞså~UW6Ì€R¯}xÿÿ›ò5‹B½¶àbB bàá½aµäfá„f· B@jÀB á­ÿÿô@x@ßA$Aj¸àE]Ò€ëàˆá j»»aj¹ =ájáÕ€=ä(%-屺à@A @å$±»àB¡Ô€ƒàÁÁJå±¼ B@JàB@ÙáJ!Küy@x@¿AAJ½àEãuâµ¼¼e±¾à=i „F屿 =@®€ÁÔ“€{âóå ±Àà@€@›ÿÿ,â ó屑yBóÁàbŽ€bᨠ£âó«c B@`æÀBÿÿô@x@ßA$A¨ÃàEjIva¨ñ½¾bóÄ =áj€ w`=âó%pâó%±Åà@á€@â m屯àJMÀJ â¸å±Ç B@aRàBå±@x@ÇA ARÈàExÀ€Óÿÿ'T¿¿aRÉ = …€ƒ.È€=â½áR%±Êà@ˆÃ€@ÿÿ@Ò!ƒ^QYà`Lå!0àGS!þ Œàžb½Ëàbô€bá jå±Ì B@€æÀBå±@x@ßA$AjÍàEþ{xbàˆå ±ÀÀajÎ =áj†€=â½å ±Ïà@ã~€@å$±ÐàBB BàÁå±Ñ B@JÀBöÇ@x@¿AAJÒàE…7yáJÁÁe±Óà=J„F|üyÔ =áˆq=€{áˆ% äF"óÕà@¼8€@å ±$F @ .`Q6Çâ óå±:h¥Ö 3à£! bᨠbå ±× B@a¨ÀBüy@x@ßA$A¨ØàE ó€ëëcÂÃbóÙ =àÆ>³zaæájä^%±Úà@Ÿ±€@ÿ,ÿ ÛàJ ÀJA#å±Ü B@aRàBå±@x@ÇA ARÝàEj{aáÛä FÄÄaRÞ =áRÌq€=å"±ßà@)m€@ë c“­àTàDðäµð£Xa 8ôHoààb•l€bàáÄfå±á B@jÀB~ëc@x@ßA$AjâàE %|ájÅÅajã =áj()€=â½å ±äà@…(€@â r5> @ãÅ"üAJåàBä'€Bá Jëcæ B@JÀBå±@x@¿AAJçàE'áà ˆâ µÆÆe±èà=¨¤Få±é =áˆç€{áˆ% å±êà@bâ€@—ëcâ ó屜A¨ëàbÃá Âó!¨ £áë«cì B@a¨ÀBëc@x@ßA$A¨íàE­œ}bóëcÇÈbóî =ájØ\~`=ájå ±ïà@8[€@ë,cðàJ¤Z JA#å±ñ B@aRàB|ñ@x@ÇA ARòàE»aàˆâ ûÉÉaRó =áRY€=áR(oôà@»€@ö Ç£A%®º7Ô£ð€å@Àìÿõàb'Àb@%ájå±ö B@jÀBzå±@x@ßA$Aj÷àEBÏ€ëå2á jÊÊajø =ájÆÒ€=ájä(å± B@aRàBëc@x@ÇA ARàEþf†aÿÿŒÓÓaR =áR¹n€=â½å ±à@j€@å ±Ã<ÿ€ E¯ñÓäƒa  B½àb~i€bˆ@$ @ájå± B@jÀBÿÿWš@x@ßA$Aj!àE…"‡ajàˆç ÔÔaj" =áj&€=ájå ±#à@b%€@à~Ârå±$àBÁ$àB$ ƒä‹c% B@JÀBå±@x@¿AAJ&àE Þ€ˆç…á JÕÕe±'à=ð„Fñ( =áˆøã€{áˆ% å±)à@C߀@å ±)ø @+’`¶Çâ ó›ÿÿ$X­úBó*àb¨á Áë!¨ bᨋc+ B@a¨ÀBëc@x@ßA$A¨,àE’™ˆbóëcÖ×bó- =áj¾Y‰`=å"±.à@X€@å,±/àJ‰W JA#äFå±0 B@aRàBå±@x@ÇA AR1àE Šaàˆâ ûØØaR2 =áRO€=áR(o3à@°€@ñ Ó*ðŸÿ¸fVå$ÔPOa [B½4àbÀb@$ájå±5 B@jÀBñ@x@ßA$Aj6àE&Ì ë@Y ˆá jÙÙaj7 =áj£Ï€=áj%-ä(+c8à@ @äåÃÅå±9àB_΀ƒà B$ ƒä…±: B@JÀBÿÿ @x@¿AAJ;àE­‡‹âµÚÚe±<à=.š„Fëc= =!? Á€{å"±>à@鈀@˜ëcâ óå±ÿîAë?àbI bâjÁë!¨å ±@ B@a¨ÀBöÇ@x@ßA$A¨AàE4CŒá¨ÛÛbóB =ájÖG€=ájê9|Cà@8F€@å,±DàJ¤E JA#£½ÁRå±E B@aRàBzöÇ@x@ÇA ARFàE»þ€ˆã„ä FÜÜaRG =áRdaäFáR%±Hà@À@ü yãàTp ƒ+tð£t±ðñµ5B½Iàb/ bá jå±J B@jÀBÿÿ¦@x@ßA$AjKàEAº€ëàˆá jÝÝajL =ájѽ€=â½%-å±Mà@- @áéÂrå±NàB¼€ƒàÁÀB$ ƒå ±O B@JÀBëc@x@¿AAJPàEÈuŽbwàˆá JÞÞe±Qà=ФFôw!^ã|€  ±  AR = ) Á°{€{áˆ% áˆ"óSà@þv€@—å ±ë câ óå±ÇBóTàbd báᨠ£á¨‹cU B@a¨ÀB”ñ@x@ßA$A¨VàEO1a¨ÿÿÉ‚ßàbóW =ájxñ€=ájä^%±Xà@Úï€@å,±YàJF Jã >å±Z B@aRàBå±@x@ÇA AR[àE\¨aRáÛâ ûááaR\ =áR °€=å"±]à@d«€@å ± óŒÓ”Pŵa ùB½^àbЪ€bàá½å±_ B@jÀBÿÿÞ@x@ßA$Aj`àEãc‘ájââaja =ájkg€=â½%-ä(+cbà@Çf€@â rå±càB' Bá Jå±d B@JÀBå±@x@¿AAJeàEj’áJããe±fà=ï¤Fÿÿ g =áˆV%€{áˆ% áˆ%±hà@¤ €@—å ±!ˆ @ñKâ óå±¾ Aëiàb bᨠbå ±j B@a¨ÀBñ@x@ßA$A¨kàEðÚ€ëñäåból =áj›“aæájå ±mà@s™€@å,±nàJߘ JA#‡ÜäFå±o B@aRàB{ëc@x@ÇA ARpàEþQ”aàˆå ±ææaRq =áR·Y€=áR"½rà@U€@öÇ"t¶ð€ ËcÌðÿÿ,ùiB½sàb~T€bàáå±t B@jÀBå±@x@ßA$AjuàE… •ajå2á jççajv =áj€=ájå ±wà@`€@á $( @ãÅ(®AJxàBÅàBå±y B@JàB âøÿÿB@x@¿AAJzàE Éà ˆá Jèèe±{à=ŒÛ„Fxüy| =@®€Áü΀{è¥% å±}à@FÊ€@å ±ë câ óå±]›A¨~àb§á Âó!¨ £áë«c B@`æÀBñ@x} 9A$A¨€ @€E’„–bóå±éêbó =ájÄD—`=ájå ±‚à@%C€@â m!j @ÿÿ,ƒàJ‘B J@Ýâ¸å±„ B@a àBëc@x@ÇA AR…àE û€ˆäFëëaR† =áRA˜aRäF!⽇à@”þ€~”è o @èo"~ñ¬$%ø¦Uü£àÿÿx”B½ˆàb bàá½ëc‰ B@jÀB‰î !ÿÿô@x@ßA$AjŠàE&·,áóä fììaj‹ = { ƒªº€=áj%páj+cŒà@ @à~Áå±àBf¹€ƒà B$ ƒä–ÇŽ B@JÀBëc@x@¿AAJàE­r™bwàˆá Jííe±à=.…„F|屑 =ሥx€{áˆ% áˆ(¥’à@çs€@ â ó @âóå±0óAë“àbI báÁë!¨ bᨑ” B@a¨ÀB¤â ó"ôÿÿ,@x@ßA$A¨•àE4.šá¨îîbó– =ájÕ2€=å"±—à@81€@å,±˜àJ¤0 J þå ±™ B@aRàBxëc@x@ÇA ARšàEºéà ˆâ ûïïaR› =áRvñ€=áR"½œà@Óì€@ñ"#£àþ£à%´&øL$a Â+B½àb? bå$±ž B@jÀBÿÿB@x@ßA$AjŸàEA¥›â½ððaj  =áj±¨€=ä(%-ä(%±¡à@ @á å±¢àBu§€ƒâ µ$ ƒä…±£ B@JÀBÿÿh°@x@¿AAJ¤àEÈ`œáJññe±¥à=ФFîw!^é.öǦ = ) Á¸f€{áˆ% áˆ"ó§à@b€@ü y)ø @å±+câ óå±fÙAë¨àbda€bᨠbᨅ±© B@a¨ÀBëc@x@ßA$A¨ªàEOa¨âtä ^òòbó« =áj!€=áj)Ïáj.¬à@c€@å,±­àJÏ JA#â4` ëc® B@aRàBëc@x@ÇA AR¯àEÕ×à ˆá RóóaR° =áRƒß€=å"±±à@åÚ€@ÿÿ,"3Œ`<}d`@ÿÿ5nÜ­B½²àbQÀbA;a`èoëc³ B@jÀBÿÿ* @x@ßA$Aj´àE\“žb½àˆá jôôajµ =ájä–€=â½%-â½%±¶à@@ @ã<Ârå±·àB •€ƒàÁÀB屸 B@JÀBëc@x@¿AAJ¹àEãNŸáJõõe±ºà=ë¤Füy» = ÀÁËT€{áˆ% áˆ%±¼à@P€@“ëcâ óå±xÿ]L½àbO€bᨠ£å ±¾ B@`æÀBüy@x@ßA$A¨¿àEj  a¨âtâ óööbóÀ =áj€=ä^%±Áà@~ €@å,±ÂàJê €JàÉÂûå±Ã B@aRàBöÇ@x@ÇA ARÄàEðÅ€ˆáÛá R÷÷aRÅ =áR“Í€=â½ç"½Æà@ôÈ€@ë cC` t%~UOàðƒ*ààJSD©Çàb` bàáÁjå±È B@jÀB{î !ñ@x@ßA$AjÉàEw¡b½àˆá jøøajÊ = …€ƒû„€=ájå ±Ëà@X @à~Áå±ÌàB·ƒ€ƒà Bå±Í B@€ÆÀBå±@x@¿AAJÎàEþ<¢áJùùe±Ïà= ‚töëcÐ =áˆêB€{áˆ% å±Ñà@6>€@—å ±ë câ óå±î¡BóÒàbš=€bᨠ£å ±Ó B@a¨ÀBüy@x@ßA$A¨ÔàE„ø€ˆöÇúûbóÕ =áj°¸£aæå"±Öà@·€@å,±×àJ¶ JA#éøå±Ø B@aRàBñ@x@ÇA ARÙàE’o¤aÿÿBüüaRÚ =áRYw€=â½å ±Ûà@²r€@ü ySÿ"¯}xå~uð¤$õ‚a TûB½Üàb bá jå±Ý B@jÀBƒ á­å±@x@ßA$AjÞàE+¥ajÿÿ+uýýajß =áj™.€=ájå ±àà@ö-€@á å±áàBUàBå±â B@JÀBå±@x@¿AAJãàEŸæ€Ëâµþþe±äà=$ù„F~öÇå =መì€{äF% 屿à@ßç€@ÿ ÿB!ˆ @ñ.Wâ óå±ð Bóçàb? bá ë!¨ bå ±è B@a¨ÀB â ó"ôöÇ@x@ßA$A¨éàE&¢¦bóå±ÿAê =ájVb§`=ájä^+cëà@¹`€@å,±ìàJ%ÀJ þäå±í B@aRàBå±@x@ÇA ARîàE3¨aàˆé øaRï =áRã €=áR+cðà@<€@å ±cä4õƒ"¥±å±ÜŒB½ñàb¨€bì Îå±ò B@jÀBÿÿ¦@x@ßA$AjóàEºÔ€ˆájajô =áj:Ø€=å"±õà@˜×€@à~ëcöàBþÖàB$&5ä–Ç÷ B@JÀBöÇ@x@¿AAJøàEA©âµ@=ùà=ƤFqå±ú =áˆ9–€{áˆ% å±ûà@}‘€@ü yë câ óå±x~BóüàbÝá Áë!¨ £á¨–Çý B@a¨ÀBüy@x@ßA$AjþàEÈKªa¨ÿÿ bóÿ =ájø «`=ä^%±3à@Z €@å,±àJÆ  J cå ± B@aRàBÿÿQè@x@ÇA ARàEÕÂà ˆå ±aR =áRyÊ€=áR%±à@ÉÅ€@ù …(o'Eèos…± à"ïQk0áñSÿÿ]Lç÷B½àb5 bá jå± B@jÀB‹è oëc@x@ßA$AjàE\~¬bàˆá jaj =ájè€=ä(%-ä(6Ç à@B @æ ¹!j @ãÅBü B àB¤€€ƒàÁäàB¥± B@JÀBëc@x@¿AAJ àEã9­aJàËá Je±à=dL„Fÿÿ*  =áˆÛ?€{áˆ% áˆ"óà@;€@å ±!ˆ @âóå±CA¨àb:€báᨠbá륱 B@a¨ÀBå±@x@ßA$A¨àEiõ€ˆëc  bó =áj’µ®aæä^"´à@ô³€@å,±àJ`ÀJA#ïgëc B@aRàBå±@x@ÇA ARàEwl¯aàˆâ û  aR = …€ƒ3t€=áR"½à@‹o€@ë cƒðC!E®â ñà?k屇B½àb÷n€bàá½å± B@€æÀBƒå±@x@ßA$AjàEý'°áj  aj =áj~+€=áj%-ä(%nà@Ø*€@ã Å$( @ãÅ"ü@B àB:àB)ºàB¥±! B@JÀBÿÿ¦@x@¿AAJ"àE„ã€ËáÓâ µ  e±#à=ö„Fÿÿbþ$ =áˆpé€{å"±%à@½ä€@ÿÿ; â óå±+uA¨&àb  bá¿ÿÙ!¨å ±' B@a¨ÀBöÇ@x@ßA$A¨(àE Ÿ±bóëcbó) = …`ƒ;_²`=áj)Ïâó"´*à@ž]€@å,±+àJ ÀJ å ±, B@€ÎÀBå±@x@ÇA AR-àE³aàˆâ ûaR. =áRÙ€=äFáR%±/à@0€@å ±“E±%±þ0ãC! î¿ÿx$€Ê’B½0àbœ€bàá½å±1 B@jÀB„å±@x@ßA$Aj2àEŸÑà ˆá jaj3 = …€ƒ+Õ€=â½%-â½%±4à@†Ô€@å$±5àBçÓ€Bá J$ ƒåô¥±6 B@€ÆÀBëc@x@¿AAJ7àE&´âµe±8à=§¤Få±9 =ሓ€{áˆ% áˆ"ó:à@cŽ€@å ±': @ëcâ óå±n`Aë;àbÂá Âó!¨ bᨑ< B@a¨ÀB â ó.XöÇ@x@ßA$A¨=àE­Hµa¨å±bó> =ájÕ¶`=ájä^%±?à@7€@å,±@àJ£ J ëcA B@aRàBÿÿÞ@x@ÇA ARBàEº¿à ˆä FaRC =áRoÇ€=å"±Dà@΀@ÿ ÿÞ£ä%¨ €ñœ0æ år0äQa òLB½Eàb:Àb@$ájå±F B@jÀBÿÿ$X@x@ßA$AjGàEA{·bû.ajH =ájÁ~€=â½%-ä(%±Ià@ @å$±JàB}€ƒá J$ ƒä–ÇK B@JàB@Ùá$ÿÿL6@x@¿AAJLàEÇ6¸aJàˆâ µe±Mà=II„FzöÇN =@®€Á´<€{âó% áˆ%±Oà@8€@ÿ ÿÞö Çâ óå±V¾AëPàbc7€báå±Q B@`æÀBëc@x@ßA$A¨RàENò€ˆñbóS =ájw²¹aæâóå ±Tà@Ù°€@â m!j @ÿÿôUàJEÀJA#â¸ñV B@aRàBå±@x@ÇA ARWàE\iºaãÇâ ûaRX =áR q€=áR"½Yà@`l€@” Ùá(o!Rèo³ÃDðïÑŠ0àC""vÿ 8àøÕB½ZàbÌk€bá jå±[ B@jàBè oÿÿŠÜ@x@ßA$Aj\àEâ$»ajàˆá jaj] = …€ƒ^(€=áj%-ä(%±^à@½'€@à~ÃÅ!j @ån(®@B_àBàBå±` B@€ÆàB@Ùáð!Kå±@x@¿AAJaàEiàà Ëá Je±bà=ê¤Fÿÿ]Lc =@®€Á]æ€{áˆå ±dà@§á€@ë câ óå±\'A¨eàb bá ë!¨ £áë«cf B@`æÀBëc@x@ßA$A¨gàEð›¼bóëcbóh =áj\½`=ç$âó"´ià@sZ€@â må±jàJßY€Jå(Ã>å±k B@aRàBñ@x@ÇA ARlàEý¾aéøaRm =áR®€=â½áR%±nà@ €@ÿ ÿ Ãt"/õ‚ä4ƒ\~å$Ôa »éB½oàby€bâ ½å±p B@jÀBÿÿ÷@x@ßA$AjqàE„Îà ˆä f  ajr =ájÒ€=áj%pâ½%±sà@[Ñ€@á å±tàB¼ÐàB$ ƒä‹cu B@JÀBñ@x@¿AAJvàE Š¿bµàËá J!!e±wà=Œœ„Få±x =!€ Áó€{áˆ% áˆ"óyà@@‹€@ÿ m$F @è¥6Çâ óå±Aëzàb§á Ä^!¨ b Ï@ ¥±{ B@a¨ÀB”è ¥'Zñ@x@ßA$A¨|àE‘EÀá¨""bó} = ``ƒ>J€=å"±~à@žH€@ÿ,ÿQèàJ ÀJ èjå±€ B@€ÎÀBÿÿB@x@ÇA ARàEÁáR##aR‚ =áRË€=áR"½ƒà@ €@ë c Ó@PÅ€tð£tÿ Ia  ÈB½„àbŒ€bãݽå±… B@jÀB†â ½ëc@x@ßA$Aj†àEŸ¼à ˆä f$$aj‡ = …€ƒ'À€=ä(%-ä(%±ˆà@ˆ¿€@â r屉àBç¾€Bá J$ ƒ ¯@d…±Š B@€ÆàB@Ùëc@x@¿AAJ‹àE&xÂbµàËá J%%e±Œà=-FƒÇÿÿB =@®€Á~€{áˆ% áˆ"óŽà@ay€@ÿ ÿ,â óå±­:AëàbÂá Âó!¨ £á¨–Ç B@`æÀBÿÿ,@x@ßA$A¨‘àE¬3Ãa¨ñ&'bó’ =ájØó€=ä^+c“à@7ò€@â m'*Qñ”àJ£ñ JA#â¸å±• B@aRàBÿÿ@Ò@x@ÇA AR–àEºªÄaRàˆâ û((aR— =áR}²€=áR"½˜à@έ€@è o$F!RèoK "ãð£å$ %Ufå) a ±B½™àb: bàá½å±š B@jÀBÿÿÉ‚@x@ßA$Aj›àEAfÅajäf))ajœ =ájÍi€=áj%-ä(%±à@* @à~Á!j @ãÅ"ü@BžàB‰h€ƒà B$àB¥±Ÿ B@JÀBÿÿ,@x@¿AAJ àEÇ!ÆaJáÓâ µ**e±¡à=H4ƒ|ÿÿ@Ò¢ =ሻ'€{å"±£à@ÿ"€@å ±$F%Nâó+câ óå±íA¨¤àbc báÁë!¨ bá륱¥ B@a¨ÀBñ@x@ßA$A¨¦àENÝ€ëå±+,bó§ =ájqÇaæáj$âó"´¨à@Ñ›€@å,±©àJ=ÀJ 屪 B@aRàBå±@x@ÇA AR«àE[TÈaàˆâ û--aR¬ =áR\€=äFáR%±­à@`W€@å ±ó*%¥+ , - a –ëcþ B@aRàBå±@x@ÇA >!0àE[?ÖaáÛáRA@@@aR4€€ƒ G€=áR%±à@oB€@ÿÿB#3C ¡Läƒ,ð±ƒPQà “à…ÑB½àbÛA b@&¢4Ájå± B@€æÀBÿÿ; `x@9 Aj @`Eâúà ˆá jAAaj =A|`=^þ€=ä(*ßéÚ+cà@¾ý€@â rå±àB Bá Jå± B@€ÆàB@Ùáå±@x@¿AAJ àEi¶×âµBBe± à=ê¤FÿÿÞ =@®€ÁQ¼€{áˆ% áˆ"ó à@£·€@ÿÿ @öÇâ óå±!#Aë àb bᨠbå ± B@`æÀBÿÿÞ@x@ßA$A¨àEïqØa¨å±CDbó =áj"2Ù`=ä^%±à@‚0€@â m'+¤ÿÿ,àJî/ JA#â¸å± B@aRàBÿÿÞ@x@ÇA ARàEýèà ˆä FEEaR =áR«ð€=áR"½à@ì€@ë cCÿÃ0àñ:ïTýñ´u @a 6ÃB½àbqë€bá jå± B@jÀBñ@x@ßA$AjàE„¤Úbå2á jFFaj =áj¨€=ä(å ±à@`§€@à~ƹå±àBÀ¦àBå± B@JÀBñ@x@¿AAJàE `ÛaJáÓá JGGe±à=‹r„F|ëc =ÀÁöe€{áˆ% å±!à@Aa€@ë câ óå±’1Bó"àb¦á Áë!¨ £å ±# B@a¨ÀBÿÿô@x@ßA$A¨$àE‘Üá¨HHbó% =áj: €=áj%páj%±&à@€@å,±'àJ ÀJ å±( B@aRàBüy@x@ÇA AR)àE× Ó@Y ˆâ ûIIaR* =áRÌÞ€=å"±+à@,Ú€@ë c Su ä¡L±|€à‹h@¸À[B½,àb˜Ù b@% á½å±- B@jÀBÿÿ@Ò@x@ßA$Aj.àEž’Ýâ½JJaj/ =áj/–€=ájå ±0à@•€@å$±1àBï”àB$ ƒäèF2 B@JàB@Ùáëc@x@¿AAJ3àE%NÞáJKKe±4à=-‚ÿÿ²º5 =@®€ÁT€{äF% äF(¥6à@bO€@ÿ ÿ¦': @ëcâ óå±¶©Bó7àbÁá Âó!¨ b᨜y8 B@`æÀBå±@x@ßA$A¨9àE¬ ßa¨ÿÿ,LMbó: =ájÐÉ€=ájå ±;à@3È€@å,±<àJŸÇ JA#â¸ëc= B@aRàBå±@x@ÇA AR>àE¹€àaRàˆå ±NNaR? =áRqˆ€=áR"½@à@Ƀ€@ë c cƒP_QÃÿ0àUñ+fþñÿÿàJ½¹B½Aàb5 bàá½å±B B@jÀBÿÿ* @x@ßA$AjCàE@<áájOOajD =ájÌ?€=áj%-ä(1Eà@& @á $( @ãÅ(®@BFàBˆ>€ƒà B$àB¥±G B@JÀBëc@x@¿AAJHàEÇ÷à ˆâ µPPe±Ià=H ƒ|ÿÿ$XJ =ሳý€{áˆ% áˆ"óKà@ù€@ÿ ÿ¦/ª @å±Ãâ óå±K!ëLàbcø€bᨠbá륱M B@a¨ÀBå±@x@ßA$A¨NàEN³âbóå±QRbóO =!!€ƒrsã`=è¥å ±Pà@Ôq€@å,±QàJ@ÀJ å ±R B@aRàBå±@x@ÇA ARSàE[*äaàˆâ ûSSaRT = …€ƒ2€=â½áR"½Uà@c-€@ë csu Au ñI$ õ‚ä4õ ŒàùìB½VàbÏ, b@&çå±W B@€æÀBÿÿ* @x@ßA$AjXàEâåà ˆá jTTajY =ájfé€=áj%-ä(#ÅZà@Àè€@á å±[àB"àBå±\ B@JÀBå±@x@¿AAJ]àEh¡åâµUUe±^à=ê¤Få±_ = ÀÁY§€{äF% áˆ"ó`à@¥¢€@ÿÿ„ @å±6Çâ óå±]Aëaàb bá ë!¨ bå ±b B@`æÀBüy@x@ßA$A¨càEï\æá¨VVbód =áj›a€=ájä^(feà@û_€@å,±fàJgÀJ äFëcg B@aRàBöÇ@x@ÇA ARhàEvçáRWWaRi =áR( €=áR"½jà@Š€@å ±ƒƒàTõ tZ/øæ`C öÇ/OB½kàbö bA;ájå±l B@jÀBüy@x@ßA$AjmàEýÓ€ˆð–å ±XXajn =áj×€=å"±oà@éÖ€@à~Ìkå±pàBI Bá J$&5ä‘q B@JÀBå±@x@¿AAJràEƒèâµYYe±sà=‹]ƒÇÿÿÞt =áˆh•€{âó% å±uà@º€@ü yë câ ó屋½Bóvàb bᨠ£á¨‘w B@a¨ÀBÿÿ­@x@ßA$A¨xàE Kéá¨ZZbóy =áj·O€=âóå ±zà@N€@å,±{àJ†M JA#äFå±| B@aRàBëc@x@ÇA AR}àE‘êáR[[aR~ =áR=€=â½áR%±à@ €@å ±“€Œ`C €ƒ,àUa ±gB½€àb Àb ájå± B@jÀBöÇ@x@ßA$Aj‚àE€ëå2å ±\\ajƒ =ájœÅ€=áj%-ä(+c„à@öÄ€@à~å±…àBXàB屆 B@JÀB‹@ÙáöÇ@x@¿AAJ‡àEž}ëâµ]]e±ˆà=¦¤Följ =@®€Á†ƒ€{äF% áˆ"óŠà@Û~€@ö Çâ óå±ÿ²º‹àb: bâ ó!¨ £å ±Œ B@`æÀBÿÿ,@x@ßA$A¨àE%9ìá¨^^bóŽ =ájÐ=€=âóä^+cà@1<€@å,±àJ; JA#â¸å±‘ B@aRàBÿÿÞ@x@ÇA AR’àE¬ô€ˆÿÿ/¼__aR“ =áRQü€=â½áR"½”à@´÷€@å ±£W0à%® år0ã ÿÿ§VWµD©•àb ÀbA;ájå±– B@jÀBëc@x@ßA$Aj—àE2°íâ½``aj˜ = …€ƒ»³€=â½å ±™à@ @á ëcšàBv²€ƒà B$ ƒä‹c› B@€ÆÀBëc@x@¿AAJœàE¹kîáJaae±à=Á¤Fëcž =ሩq€{âó% 屟à@ôl€@ö Çâ óå±óGBó àbU bᨠ£á¨‹c¡ B@a¨ÀBñ@x@ßA$A¨¢àE@'ïa¨öÇbcbó£ =ájrç€=âóä^%±¤à@Óå€@å,±¥àJ?ÀJA#äF屦 B@aRàBå±@x@ÇA AR§àEMžðaRàˆé øddaR¨ =áRû¥€=â½áR%±©à@]¡€@å ±³IAÍõïAØõ ¡R±|à§ÐB½ªàbÉ  b@(áj屫 B@jÀBÿÿŠÜ@x@ßA$Aj¬àEÔYñájeeaj­ =ájL]€=áj%-ä(+c®à@¨\€@á 屯àBàBå±° B@JÀBå±@x@¿AAJ±àE[òáJffe±²à=ܤFÿÿÞ³ =áˆC€{áˆ% áˆ"ó´à@•€@ñ !ˆ @öÇâ óå±"Aëµàb÷á ÍO!¨ bå ±¶ B@a¨ÀBëc@x@ßA$A¨·àEáЀëÿÿôghbó¸ =áj‘óaæå"±¹à@t€@å,±ºàJàŽ JA#äFå±» B@aRàBöÇ@x@ÇA AR¼àEïGôaàˆå ±iiaR½ = …€ƒ¡O€=áR"½¾à@ûJ€@ÿ ÿ,ÀŸÿÖƒP)QÃÿ Œà7vÇ¿àbg bàá½å±À B@€æÀBÿÿ @x@ßA$AjÁàEvõájjjaj =ájæ€=ä(å ±Ãà@B @ñ$ÄàB¢€ƒá J$ ƒä‹cÅ B@JÀBå±@x@¿AAJÆàEü¾à ˆâ µkke±Çà=~Ñ„F{ÿÿ È =áˆéÄ€{âó% å±Éà@3À€@ÿ ÿ â ó屓vÇÊàb˜¿€bᨠ£á¨‹cË B@a¨ÀBÿÿ,@x@ßA$A¨ÌàEƒzöbó¼ Z¤ç᨟ÿôlmbóÍ =áj¦:÷`=ájê+cÎà@ 9€@å,±ÏàJv8 J ÈÀÉÄFå±Ð B@aRàBw@ó¡ ÀBÿÿ¸l@x@ÇA ARÑàE‘ñ€ˆãÇáRA@nnaRÒ =@€ƒ?ù€=áR%±Óà@ô€@ÿÿ@Ò#ÓT?0àñ:ïTûðîQ[à` àÍÿôÔàb  bá jå±Õ B@€æÀB}ÿÿ$X@x@ßA$AjÖàE­øbàˆá jooaj× =ájŸ°€=å"±Øà@ü¯€@áéÂ/)Úån"üIºÙàB[ BàÁÀBå±Ú B@JÀBå±@x@¿AAJÛàEžhùáJppe±Üà={„FëcÝ =ሊn€{áˆ% å±Þà@Ùi€@ÿ ÿ,â óå±iÿúßàb: bᨠ£å ±à B@a¨ÀBå±@x@ßA$A¨áàE%$úa¨ñqrbóâ =ájLä€=ä^"´ãà@¬â€@ÿ ÿþ=ä%òÿÿôäàJÀJA#ånå±å B@aRàBëc@x@ÇA ARæàE2›ûaRáÛä FssaRç =áRÝ¢€=áR%±èà@2ž€@î!!Rî!ã\p ý€~ÅAq"a ODféàbž€bàáÄfå±ê B@jÀB†å±@x@ßA$AjëàE¹Vüájttajì =ájAZ€=ä(%pä(1íà@žY€@â rå±îàB Bá Jå±ï B@JàB@Ùâøüy@x@¿AAJðàE@ýáJuue±ñà=Á¤FÿÿŠÜò = ž€Á8€{áˆ% áˆ"óóà@{€@ÿ ÿ/¼â óå±ëäAëôàbÜá Âó!¨ £å ±õ B@`æÀBÿÿ/¼@x@ßA$A¨öàEÆÍ€ëñvwbó÷ =ájùþaæáj$áj%±øà@YŒ€@â må±ùàJÅ‹ JA#â¸å±ú B@aRàB|ëc@x@ÇA ARûàEÔDÿaàˆå ±xxaRü =áRƒL€=å"±ýà@àG€@ë có±|€ÓƒZà0àXTþðua »“B½þàbL bàá½å±ÿ B@jÀB~å±@x@ßA$Aj5  `E[7@ å2á jy<áA œ = 0`=ë€=â½å ±à@H @à~Áå±àB§€ƒà B$ ƒä‘ B@€ÆÀBëc@x`¿ =AJ @€EỀˆáÓá Jzze±à=b΄Fÿÿ;  =áˆÑÁ€{áˆ% äF%±à@½€@— Ñãû!ˆ @è¥â óå±aBó àb}¼€báÁë!¨ bᨑ B@abÀBüy@x@ßA$A¨ àEhwbóå±{|bó =ájŸ7`=ájå ± à@ÿ5€@å,±àJkÀJA#áµå± B@aRàBå±@x@ÇA ARàEuî Ó@Y ˆâ û}0$F =áRö€=å"±à@nñ€@öÇ $ƒGàÿõ ÿJƒ@¸Àl|B½àbÚð€bä få± B@jÀBÿÿt@x@ßA$AjàEü©bÿÿ—~&!j =ájˆ­€=â½%-ä(+cà@欀@á $( @ãÅ(®@BàBD Bâ,äàB¥± B@JÀBå±@x@¿AAJàEƒeáJe±à=x„FöÇ =áˆsk€{áˆ% áˆ%±à@¿f€@ÿÿ,â óå±ÈŽA¨àb# bᨠ£á륱 B@a¨ÀBñ@x@ßA$A¨ àE !a¨ñ€:#"ó! =áj5á€=ájå ±"à@”߀@å,±#àJÀJ ¥(Âûå±$ B@aRàB}ëc@x@ÇA AR%àE˜aRàˆå ±‚$®!R& = { ƒΟ€=áR"½'à@+›€@ëc $JàþTõ +f áîD Œà€FB½(àb—š€bàáÁjå±) B@jÀBÿÿ÷@x@ßA$Aj*àEžSájƒƒaj+ =áj"W€=áj%-ä(%±,à@V€@á å±-àBÞUàB)º ƒä‹c. B@JÀBå±@x@¿AAJ/àE%aJæ}â µ„„e±0à=¦¤Fëc1 =ሀ{áˆ% áˆ"ó2à@a€@—ëcâ óå±$´Aë3àbÁá Âó!¨ £á¨‹c4 B@a¨ÀBëc@x@ßA$A¨5àE«Ê€ëëc…4q$^6 =ájÛŠ aæê(f7à@>‰€@å,±8àJªˆ JA#å±9 B@aRàBxå±@x@ÇA AR:àE¹A aàˆâ û‡‡aR; =áRTI€=â½ç"½<à@µD€@ÿÿÞ $#@õ ïdu`$+jñL$éÿÿ¦¢B½=àb!ÀbA;ájå±> B@jÀBÿÿB@x@ßA$Aj?àE?ý ë@Y ˆá jˆˆaj@ =áj  a¨â½%-ä(%±Aà@ýÿà ~ÃÅå±BàBd Bå(ÀB$ ƒä…±C B@JÀBƒ ¡ÀBöÇ@x@¿AAJDàEƸ áJ‰‰e±Eà=GË„FvëcF =@®€Á¶¾€{âó% áˆ"óGà@º€@ü y5\ @ñ+câ óå±ãAëHàbb¹€báÁe!¨ bᨅ±I B@`æÀBå±@x@ßA$A¨JàEMt bµëcŠ‹bóK =ájq4 `=âóä^%±Là@Ô2€@å,±MàJ@ÀJA#â¸ëcN B@aRàBå±@x@ÇA AROàEZë Ó@Y ˆä FŒŒaRP = +€ƒ ó€=â½áR"½Qà@fî€@ë c3‚ä4€õƒàõïuð¤$ê ŒàbËB½RàbÒí€bàá½å±S B@€æÀBÿÿ@Ò@x@ßA$AjTàEá¦bàˆá jajU =ájUª€=áj%-ä(%±Và@³©€@à~Áå±WàBàB$ ƒä…±X B@JàB@Ù¢oÀ…å±@x@¿AAJYàEhbáJŽŽe±Zà=é¤Fÿÿ$X[ =@®€ÁXh€{áˆ% áˆ"ó\à@¤c€@å ±!ˆ @å±(¥â ó屯9Aë]àb bá ë!¨ bᨅ±^ B@`æÀBå±@x@ßA$A¨_àEïa¨å±bó` =ájÞ€=å"±aà@yÜ€@â m'$Ÿÿÿ,bàJåÛ J â¸å±c B@aRàBÿÿ¦@x@ÇA ARdàEü”aRàˆä F‘‘aRe =áR œ€=áR"½fà@˜€@å ±CõŲ €äõëcÐ4B½gàbl—€bàáÄ#å±h B@jÀBëc@x@ßA$AjiàEƒPáj’’ajj =!! ƒ T€=ä(%-ä(%±kà@eS€@ë $( @ãÅ(®@BlàBÇR€Bá J$àB¥±m B@JÀBñ@x@¿AAJnàE áJ““e±oà=‹ƒ|yëcp =áˆö€{âó% áˆ"óqà@B €@ÿÿ â óå±tÆA¨ràb¥á Âó!¨ £á륱s B@a¨ÀBöÇ@x@ßA$A¨tàEÇ€ëñ”•bóu =ájÁ‡aæáj%páj"´và@#†€@å,±wàJ… JA#å±x B@aRàBå±@x@ÇA ARyàEž>aãÇå ±––aRz =áRFF€=áR"½{à@¢A€@å ±S ¡RßÿC-ÿÿt …B½|àb bá jå±} B@jÀBÿÿ,@x@ßA$Aj~àE$ú€ëàˆá j——aj =áj´ý€=å"±€à@ @áéÃÅå±àBpü€ƒàÁÀB$å ±‚ B@JÀBå±@x@¿AAJƒàE«µbµàˆá J˜˜e±„à=,È„Fëc… =ማ»€{áˆ% äF"ó†à@è¶€@ë câ óå±­wBó‡àbG báᨠ£å ±ˆ B@a¨ÀBëc@x@ßA$A¨‰àE2qa¨ëc™šbóŠ =ájV1`=ájå ±‹à@¹/€@ÿ,ÿ ŒàJ%ÀJA#å± B@aRàBå±@x@ÇA ARŽàE?è€ÓÿÿÁ››aR =áRôï€=å"±à@Cë€@â½!“1`òÿÌ@.!cT?0àP±Tûð€~ àÿWB½‘àb¯ê€b⠽屒 B@jÀB2á­ÿÿWš@x@ßA$Aj“àEÆ£bàˆä fœ#%9…” =ájB§€=â½%-ä(+c•à@¦€@â rå±–àB BàÁä ƒä‘— B@JàB@Ùâø!KÿÿQè@x@¿AAJ˜àEM_áJe±™à=ΤFvëcš =@®€ÁEe€{áˆ% áˆ%±›à@Š`€@ÿÿ â óå±[¥Aëœàbéá ᨠ£á¨‘ B@`æÀBÿÿ @x@ßA$A¨žàEÓa¨ÿÿôžŸbóŸ =ájèÚ€=áj$áj+c à@JÙ€@â mñ¡àJ¶Ø J â¸ñ¢ B@aRàBÿÿ$X@x@ÇA AR£àEá‘aRàˆä F  aR¤ =áR†™€=å"±¥à@ᔀ@ë c sÅAç"±|€Ýkkà=îB½¦àbM bàá½å±§ B@jÀBöÇ@x@ßA$Aj¨àEhMajå2á j¡¡aj© =ájøP€=â½%-â½%±ªà@S @à~Á屫àB´O€ƒá Jëc¬ B@JÀBëc@x@¿AAJ­àEîaJàˆá J¢¢e±®à=oƒ|ÿÿ¦¯ =áˆã€{âó% áˆ%±°à@, €@ÿ ÿÞ!ˆ @è¥â óå±™ÿÒ(±àbŠ €báÁë!¨ bå ±² B@a¨ÀB#öä¡'Zÿÿh°± 5@ßA$A¨³ @€EuÄ€ˆñ£&(¥´ =áj¦„aæâóå ±µà@ƒ€@å,±¶àJt‚ J@Ýå±· B@a àBå±@x@ÇA AR¸àE‚; aàˆâ û¥¥aR¹ =áR+C€=áR"½ºà@‹>€@å ±ƒ±ƒPQ Ã0à+ia :n!»àb÷=€bä ©å±¼ B@jÀB|â ½ëc@x@ßA$Aj½àE ÷€ˆáj¦¦aj¾ =áj…ú€=ájå ±¿à@áù€@á $( @ãÅ(®EôÀàBA BàÁäàB«cÁ B@JÀBå±@x@¿AAJÂàE²!âµ§§e±Ãà=Å„Få±Ä =ሀ¸€{áˆ% å±Åà@γ€@—ÿ ÿÞ)ø @å±+câ ó属t Æàb, bᨠbáë«cÇ B@a¨ÀB-ïä¡"ôå±@x@ßA$A¨ÈàEn"a¨ñ¨©bóÉ = ``ƒ<.#`=çå ±Êà@,€@å,±ËàJ ÀJ áëcÌ B@€ÎÀBå±@x@ÇA ARÍàE$å Ó@Y¦:å ±ªªaRÎ =áRÉì€=â½áR%±Ïà@$è€@å ±“Týð€+açåq0çÿÿBÀDfÐàbç€bàáÄfå±Ñ B@jÀBâ ½å±@x@ßA$AjÒàE« $bàˆá j««ajÓ =áj7¤€=áj%-ä(+cÔà@’£€@à~Áå±ÕàBó¢àB)º ƒäÿQèÖ B@JÀBå±@x@¿AAJ×àE2\%aJæ}á J¬¬e±Øà=³¤Få±Ù =áˆb€{áˆ% áˆ"óÚà@k]€@ô @å±4 â óå±»2AëÛàbÎá Áë!¨ bá¨‘Ü B@a¨ÀBüy@x@ßA$A¨ÝàE¸&a¨ëc­®bóÞ =ájÞ×€=å"±ßà@?Ö€@å,±ààJ«Õ J ¥(ÁRå±á B@aRàBå±@x@ÇA ARâàEÆŽ'aRàˆâ û¯¯aRã =áRƒ–€=â½å ±äà@Ö‘€@â½ @â½ £,à´Sq}6ÄÅÿÿF„ëvÇåàbB bàáÁj屿 B@jÀBÿÿÉ‚@x@ßA$AjçàELJ(áj°°ajè =ájÕM€=â½%-ä(%±éà@0 @á å±êàB‘L€ƒå(ÁJ$ ƒä…±ë B@JÀB‹@Ùâø&ýöÇ@x@¿AAJìàEÓ)áJ±±e±íà=Tƒ|ÿÿ* î =@®€ÁË €{âó% áˆ%±ïà@ €@ÿÿxâ óå±OvÇðàbo€bá ë!¨ £á¨…±ñ B@`æÀB0­á ¨ëc@x@ßA$A¨òàEZÁ€ˆëc²³bóó =áj€*aæâó$áj.ôà@á€@å,±õàJMÀJ!â¸å±ö B@aRàBå±@x@ÇA AR÷àEg8+aàˆå ±´´aRø = …€ƒ@€=áR"½ùà@k;€@ÿÿ* $³Î6Ä å#0á/S#ý‚ à ŒàMàD©úàb×:€bàáÄfå±û B@€æÀBå±@x@ßA$AjüàEîóà ˆá jµµajý =ájv÷€=ä(%-â½%±þà@Òö€@å nå±ÿàB2 Bá J$ ƒä…±ÿ&¸ÁJÀBëc@x@¿AAJàEu¯,âµ¶¶e±à=ú¤Fwüy =áˆmµ€{áˆ% áˆ"óà@¯°€@å ±!ˆ @è¥â óå±éAëàb bᨠbᨅ± B@bóÀBå±@x@ßA$A¨àEüj-a¨å±·¸bó =áj2+.`=ä^%± à@’)€@å,± àJþ( JA#èjå± B@aRàBÿÿ/¼@x@ÇA AR àE âà ˆä F¹¹aR =áR¯é€=áR"½à@ å€@ÿ ÿ$XÃp!ñ½õ S þ‚bàõ äa ã¸B½àbuä€bá jå± B@jÀBñ@x@ßA$AjàE/bàˆá jººaj =áj¡€=ä(%-ä(%±à@x €@å$±àBØŸ€BàÁÿÿ«_ ƒä…± B@JÀBå±@x@¿AAJàEY0áJ»»e±à=˜k„Fñ =áˆ_€{âó% áˆ"óà@QZ€@ÿ ÿ â óå±GJAëàb²á ᨠ£á¨XF B@a¨ÀBöÇ@x@ßA$A¨àE1a¨ñ¼½bó =ájÄÔ€=ájë cà@$Ó€@å,±àJÒ JA#å± B@aRàBå±@x@ÇA AR!àE«‹2aR‡@Y ˆä F¾¾aR" =áRQ“€=áR"½#à@§Ž€@ë cÓõ ‚àÿ´ÿäõ€@¸ÀŠB½$àb bá jå±% B@jÀB…î !öÇ@x@ßA$Aj&àE1G3ajÿÿ I¿¿aj' =ájÁJ€=å"±(à@ @áéÃÅ)Úån(®AJ)àB}I€ƒá Jëc* B@JÀBå±@x@¿AAJ+àE¸4aJàˆâ µÀÀe±,à=9ƒ|€ëc- =ሬ€{áˆ% å±.à@ï€@ÿ ÿÞ!ˆ @ëcâ óå±.öA¨/àbT báÁë!¨ bå ±06`a¨ÀB â ó"ôñ@x@ßA$A¨1àE?¾€ëàˆá ¨ÁÁbó2 =ájè€=ájå ±3à@KÁ€@å,±4àJ·À J þÅ(ÁRëc5 B@aRàBÿÿ @x@ÇA AR6àEÆy5bûãÇá RÂÂaR7 =áRa€=áR%±8à@Â|€@ÿ ÿ$Xã€lä1 ðå"0âÄTa qB½9àb. bàáÁjå±: B@jÀBÿÿô@x@ßA$Aj;àEL56ajáóá jÃÃaj< =ájÔ8€=áj%pä(+c=à@. @à~Á$( @ãÅ¥±>àB7€ƒà B$àB«c? B@JÀBå±@x@¿AAJ@àEÓðà ˆá JÄÄe±Aà=Û¤FÿÿWšB =ሻö€{áˆ% áˆ"óCà@ò€@“ÿÿ,â óå±ÕôAëDàboñ€bᨠ£áë«cE B@a¨ÀBÿÿL6@x@ßA$A¨FàEZ¬7âóÅÅbóG =áj ±€=ê"´Hà@j¯€@å,±IàJÖ® JA#ªÚÃ>å±J B@aRàBöÇ@x@ÇA ARKàEàg8áRÆÆaRL =áR†o€=â½ç"½Mà@áj€@ñ ópœr‚S"û€a ròB½NàbM bå$±O B@jÀBñ@x@ßA$AjPàEg#9ájÇÇajQ =ájó&€=áj%-ä(%±Rà@O @â rå±SàB¯%€ƒà Bå±T B@JÀBå±@x@¿AAJUàEîÞà ˆå ±ÈÈe±Và=ö¤FõÿÿŠÜW =áˆÞä€{áˆ% áˆ"óXà@(à€@ñ !ˆ @ëcâ óå±ÖAëYàbŠß€bᨠbå ±Z B@a¨ÀBñ@x@ßA$A¨[àEuš:bóöÇÉÊbó\ =áj£Z;`=å"±]à@Y€@å,±^àJoX JA#å±_ B@aRàBñ@x@ÇA AR`àE‚a¨öÇÎÏbóq = …`ƒA?`=ä^"´rà@¡€@å,±sàJ ÀJ å ±t B@€ÎÀBå±@x@ÇA ARuàE$» Ó@Y¦:â ûÐÐaRv =áR΀=áR%±wà@(¾€@å ±Ø+\ qûSqß"å#0å@¸À,Dfxàb”½€bàá½å±y B@jÀBå±@x@ßA$AjzàEªv@bàˆá jÑÑaj{ = …€ƒ3z€=áj%pä(+c|à@y€@ã Åå±}àBîx€BàÁÁJ$ ƒä†ü~ B@€ÆÀBå±@x@¿AAJàE12AáJÒÒe±€à=²¤FÿÿÞ =áˆ%8€{å"±‚à@l3€@å ±!ˆ%Nëcâ ó屈áAëƒàbÍá ᨠb᨜y„ B@a¨ÀB öÇ@x@ßA$A¨…àE¸í€ëâ1â óÓÓbó† =ájeò€=ájïÂ%±‡à@Èð€@å,±ˆàJ4ÀJ ¥(ÁRñ‰ B@aRàBöÇ@x@ÇA ARŠàE?©BbûãÇá RÔÔaR‹ =áRç°€=äFáR%±Œà@?¬€@å ±#S#ßñÇu ¼àü£àa ovCàb««€bá j屎 B@jÀBƒâ ½üy@x@ßA$AjàEÅdCajàˆá jÕÕaj =ájIh€=â½%-屑à@¤g€@áéÂr$( @ãÅ"üAJ’àB BàÁÀB屓 B@JÀBå±@x@¿AAJŸÿþˆ EL DáJÖÖe±•àƒTîƒÇöÇ– =áˆ<&€{áˆ% áˆ"ó—à@‰!€@ÿ ÿ¦ë c#âóå±Ë„A¨˜àbèá ᨠ£á륱™ B@a¨ÀBëc@x@ßA$A¨Ÿÿõ0ÀEÓÛ€ëñרbó› ƒájœEaæájä^"´œà@fš€@ÿ,ÿ* àJÒ™ J Ûå ±ŸÿîÝ¡ àBå±@x@ÇA ARŸ ˆ@˜àRFaã„ä FÙÙaR  =áRˆZ€=áR"½¡à@äU€@ÿÿô %3ýìõ äõ€+bäßÿ6á`7HB½¢àbP bàá½å±£ B@a$àBÿÿ$X@x@ßA$AjŸÿæØ EgGájÚÚaj¥ ƒájû€=áj%pä(&¹¦à@U @á å±§àB·€ƒà B屟ÿà}¡àBå±@x@¿AAJŸÿß}ÀEîÉà ˆâ µÛÛe±ª Æ@ÎoÜ„Fÿÿ/¼« =áˆÞÏ€{áˆ% áˆ"ó¬à@+Ë€@—ü y!ˆ @ëc3âó屓ºAë­àbŠÊ€bᨠbå ±ŸÿÙá$ÀBå±@x@ßA$A¨ŸÿØSÀEt…HbóñÜÝbó°àÆEI`=ê%±±à@ÿC€@ë,c²àJkÀJ Ûå ±ŸÿÒc¡ àBå±@x@ÇA ARŸÿÑc‚½‚ü Ó Àˆâ ûÞÞaRµàÆ2JaRâ½ç"½¶à@Šÿ€~ë cCS#ïKbäõ õ õ ·à|ÿ¸l·àböþ€bàá屟ÿËê¡$àB„ëc@x@ßA$AjŸÿ˱ E ¸,ÿÿë®ßßajºàÆ»€=ájå ±»à@÷º€@á $( @ãÅ"üD¼àBY Bè g屟ÿƹ¡àBå±@x@¿AAJŸÿÄIÀEsKbwÿÿ )ààe±¿ Æ@…±†„FÿÿÞÀ =屈y€{äF% å±Áà@Ðt€@ñ ë câ ó屨ÿ¸lÂàb/ bá ë!¨ £áë«cà B@a$ÀBñ@x@ßA$A¨Ÿÿ¼<ÀE/La¨å±áâbóÅ ƒáj7ï€=ájä^"´Æà@™í€@å,±ÇàJÀJ ÛÅ(ÄFñŸÿ·4¡ àBå±@x@ÇA ARŸÿ´é E#¦MaRàˆå ±ããaRÊàÆÌ­€=äFáR%±Ëà@$©€@å ±Sÿ› Ž"å$dp Êà×ÿùÐÌàb¨€bä fëcŸÿ°\¡$àBå±@x@ßA$AjŸÿ«o EªaNájääajÏàÆ*e€=áj%pä(+cÐà@…d€@áéÂrå±ÑàBæcàB$ ƒä–ÇÒ B@dàBå±@x@¿AAJÓàE1OáJååe±Ÿÿ¥Û ƒ²¤FÿÿBÕ {äF!#€{áˆ% áˆ"óÖà@n€@ÿÿ!š @ëcâ óå±sÿÿ¸×àbÍá Áë!¨ bᨖǟÿ¡$ÀBëc@x@ßA$A¨ŸÿŸ_ÀE¸Ø€ëã|ä ^ææbóÚàÆhÝ€=å"±Ûà@ÈÛ€@å,±ÜàJ4ÀJ Ûå ±Ÿÿ™o¡ àBüy@x@ÇA ARŸÿ˜o E>”PâûççaRßàÆ훀=áR"½àà@F—€@å ±cÅ€tð£å%ð£tIUfüyµªD©áàb²–€bá jå±â B@„fÀB‚ëc@x@ßA$AjŸÿ’½ EÅOQájèèajä ƒä(ES€=ä(%-ä(%±åà@¢R€@ã<ÃÅ屿àB Bá J$ ƒä…±Ÿÿ‹R¡àB@ÙùÀ8ÿÿ$X@x@¿AAJè ˆ@L RáJéée±Ÿÿˆ—À=TÙƒÇñÿÿÞê {@®€{@€{áˆ% áˆ"óëà@‡ €@ë câ óå±XAëìàbèá Áë!¨ £á¨…±í B@`æÀBëc@x@ßA$A¨Ÿÿ€TÀEÓÆ€ëöÇêëbóï ƒÀ= ‡Saæájê+cðà@i…€@â m,Î @ÕT tÃ! !% …ÿ±OñàJÕ„ J â¸ëcŸÿv|¡ àBÿÿbþ@x@ÇA ARŸÿz? Eà=Taàˆç ììaRôàÆ‹E€=å"±õà@ì@€@ÿÿB%s)ðäõ$u"ƒ,àðÿÿnbsÓöàbXÀb óáj屟ÿtª‚½àBzå±@x@ßA$Ajø ˆ@ƒgù ë@¡óá jííajù =â½÷ü€=â½%-ä(%±úà@T @å$±ûàB³û€ƒàÁä ƒä…±ŸÿnØ¡àBëc@x@¿AAJý ˆ@aJí´Uâµîîe±þà=oÇ„F|ñÿ =áˆÚº€{áˆ% áˆ%± ¬à@)¶€@ÿ ÿB': @ëcâ óå±+y»àb‰µ€bᨠbᨅ±Ÿÿf%¡bÀB™â ó'Zÿÿô@x@ßA$A¨ŸÿecÀEtpV ˜ñïðbó -á)£0W áj%páj%± 0à@/€@å,±àJo. J ýÂû屟ÿ`{¡ àBå±@x@ÇA AR ˆ@bû‚瀈ãÇä FññaR =á!ï€=å"± à@~ê€@ë cƒE¨ ÿÔ"­ƒ[t`Ì ²D© àbêé€bá j屟ÿZ¡$àB~â ½ÿÿ,@x@ßA$Aj ˆ@j£X`àˆá jòòaj -áj¦€=ä(%-â½%±à@@áéÂr$( @ãÅ"ü@BàBL BàÁÀB)ºàB¥±ŸÿSf¡àBå±@x@¿AAJŸÿRaÀE^Y ˜àˆá Jóóe±`-À=q„F}å± =áˆd€{áˆ% áˆcà@Ë_€@ÿ ÿ,â ó屫@A¨àb+ bᨠ£á륱ŸÿKà î ÀBœâ ó"ôå±@x@ßA$A¨ ˆ@€EZ`àˆá ¨ôôbó -ájÄ€=ä^"´à@&€@å,±àJ’ J@Ý©oÂû屟ÿE¯¡ àBöÇ@x@ÇA ARŸÿDG EÕ€ˆãÇá RõõaRàÆMÝ€=â½ç"½à@­Ø€@ë c“ðƒFà`Á@ƒZàDÿÿF„B½ àb bàáÁj屟ÿ>t¡$àBÿÿ@Ò@x@ßA$AjŸÿ=ΠE#‘[ ˜ ë Áóá jööaj# -â½§”€=áj%-ä(%±$à@ @à~Áå±%àBc“€ƒà B屟ÿ4Ú¡àBå±@x@¿AAJŸÿ7•ÀEªL\ ˜àˆá J÷÷e±Ÿÿ6%À=²¤Fôñ) kሚR€{áˆ% áˆ"ó*à@çM€@˜ÿÿ,â ó ›”hv¼ï/GÌï?´õAë+àbF bˆ óÂjÁë!¨ £å ±Ÿÿ0R¥kÀBå±@x@ßA$A¨- ˆ@…±1]`ÿÿ…*øùbó. -ájnÈ€=å"±/à@ÐÆ€@‡@Såfñ0àJ<ÀJ@ÝàJ屟ÿ&À¡ àBå±@x@ÇA ARŸÿ){ E>^ ˜ÿuúúaR3 -@þa놀=â½å ±4à@F‚€@ë c£Tý%³ Gïð¿u!ƒYtÿÿÉ‚ëB½5àb²€b⠽屟ÿ!— æÀBÿÿB@x@ßA$AjŸÿ Š€EÅ:_ ˜ájûûaj8 -ájY>€=ájå ±9à@´=€@á å±:àBàB$ ƒä‘ŸÿÝ¡àBå±@x@¿AAJŸÿ°ÀELö€Ëÿÿwüüe±ŸÿeÀ=ѤFüy>áDü€{áˆ% å±?à@÷€@ö Çâ óå±+Bó@àbì¡ä ^!¨ £á¨ÿ/¼Ÿÿ¥¡$ÀB ëc@x@ßA$A¨ŸÿýÀEÒ±` ˜ÿÿQèýþbóC -âóôqa å#±D 0à@Up€@ë,cEàJÁo J å ±Ÿÿ »¡ àBÿÿ$X@x@ÇA ARŸÿ ª Eà(b ˜#A Àˆè ­ÿÿaRH -á•0€=ç(oIà@è+€@èo%òóÓ³+c€ütÞëcŽ’¡ àBå±@x@ÇA AR’ EÒà ˆâ ûaRLeàÆ;Ú€=áR%±Jðà@–Õ€@ÿ ÿB Ãt ðäƒRð£ð€ +là Êà|B½HÂàb bä f屃ҡ$àBÿÿB@x@ßA$Aj‡• EŽfbáóá jajEàÆx‘€=ä(%-â½%±Cµà@Ô€@á å±CoàB4 BàÁµ$ ƒå ±à¡àBëc@x@¿AAJ€»ÀEIgáJ @=€-À=\@]ë ch káO€Máˆ% áˆ"óià@ÉJ€@è ¥/ª @è¥Ãâ ó屨}Aëjàb+ bᨠbᨋck B@o>ÀBñ@x@ßA$AjlàE@ÿÿ,bóm =áj?Å€=ájå ±nà@ Ã€@å,±oàJ ÀJ Íå ±p B@aRàBÿÿÞ@x@ÇA ARqàE#|An Ó Àˆä F  aRr = *€ƒу€=å"±sà@3€@ö ÇÓ<}YE¶ÿÏ7 ŒàÍ*B½tàbŸ~ b@&ò€å±u B@€æÀByëc@x@ßA$AjvàEª7BÝ ë Àˆá j  ajw =áj2;€=â½%-ä(%±xà@Ž:€@å$±yàBî9€BàÁå±z B@JÀBå±@x@¿AAJ{àE0óà ˆá J  e±|à=²¤Fñ} =áˆù€{áˆ% áˆ%±~à@kô€@ÿ ÿ,â óå±iØAëàbÍ¡á ë'Z £å ±€ B@a¨ÀBÿÿ5n@x@ßA$A¨àE·®†öÇ  bó‚ =ájÝn…ÿájå ±ƒà@>m€@å,±„àJªl JA#äFëc… B@aRàBå±@x@ÇA AR†àEÅ%EÉ Ó Àˆâ ûaR‡ =áRo-€=å"±ˆà@Ñ(€@å ±ã9KXo`-%±à$Zø ·àÖB½‰àb=Àb@&áj届 B@jÀBå±@x@ßA$Aj‹àEKá ë@Y ˆá jajŒ =ájÌä€=ájå ±à@( @ã ÅëcŽàB‡ã€ƒàÁå± B@JÀBå±@x@¿AAJàEÒœHƒàˆá J >‘à=S¯k`ÿÿÞ’ -ሢ€{äF% 屓à@ ž€@—ñ ë câ óå± $Bó”àbn€bᨠ£å ±• B@a¨ÀBëc@x@ßA$Aj–àEYXJ0ëcbó— =ájwФájáj1˜à@Ø€@ˆ@S£r°!j @ÿÿ ™àJDÀJ ÍàJ屚 B@aRàBå±@x@ÇA AR›àEfÏ Ó@Y Óâ ûaRœ =@þ€ƒøÖ€=áR(oà@ZÒ€@å ±óæp#-0àQÁD<„@¸Àà{B½žàbÆÑ b  áÁµå±Ÿ B@€æÀBÿÿ @x@ßA$Aj àE튌Þàˆá jaj¡ =ájuŽ€=ájå ±¢à@Ò€@å$±£àB1 BàÁÁJ)º ƒä–Ǥ B@JÀBå±@x@¿AAJ¥àEtFLÆàˆá Je±¦à=ù¤FxÿÿÞ§ =áˆXL€{áˆ% 屨à@«G€@ë câ óå±D‰Bó©àb bᨠ£á¨‘ª B@a¨ÀBî W9¼ÿÿÞ@x@ßA$A¨«àEúNsàˆá ¨bó¬ =áj€=è¥%páj%±­à@ó€@ÿ,ÿ,®àJ_ÀJ ÍäF屯 B@aRàBÿÿ…*@x@ÇA AR°àE½€ÓüyaR± =áRÅ€=â½áR%±²à@uÀ€@ÿÿ,&,àÿQ[à`QÃDð€ÿÿ¦´>B½³àbá¿€b⠽屴 B@jÀBöÇ@x@ßA$AjµàEyQ5àˆâ ½aj¶ =ájŒ|€=áj%-â½1·à@æ{€@á ëc¸àBHàBå±¹ B@JÀBå±@x@¿AAJºàE4’¶àËá Je±»à=–‚ÿÿÞ¼ =áˆ:€{áˆ% áˆ"ó½à@Ë5€@ÿÿ,â óå±ÌAë¾àb+ báçZ £å ±¿ B@a¨ÀBëc@x@ßA$A¨ÀàEð€ëñbóÁ =áj6°”Wå"±Âà@˜®€@ë,cÃàJÀJ éøå±Ä B@aRàBÿÿÞ@x@ÇA ARÅàE#gT) Ó Àˆâ ûaRÆ =áRÓn€=áR"½Çà@'j€@ÿÿQè&1 ÿQz±|€ÃäÿtI/ÿÿ…*‰-B½Èàb“i€bàá½å±É B@jÀB‡è oÿÿ,@x@ßA$AjÊàEª"U˜ç¥á jajË =áj2&€=ä(å ±Ìà@Œ%€@è $å±ÍàBî$àBå±Î B@JÀBå±@x@¿AAJÏàE0Þ€ˆáÓá Je±Ðà=±¤Fÿÿ/¼Ñ =áˆ$ä€{áˆ% `ï€3bóÒà@i߀@ÿÿ,â óå±-ßBóÓàbÌá Âó!¨ £å ±Ô B@a¨ÀB â ó"ôëc@x@ßA$A¨ÕàE·™Xå± bóÖ =ájÜY™ä^+c×à@>X€@å,±ØàJªW J ÍÅ(ÁRå±Ù B@aRàBå±@x@ÇA ARÚàEÄ™Óàˆâ û!!aRÛ =áRe€=áR%±Üà@¹€@å ±#‚ä4ƒõƒtÿðï´îÿÿÉ‚rÿÚ˜Ýàb% bä ©å±Þ B@jàBâ ½å±@x@ßA$AjßàEKÌ€ëáj""ajà =áj×Ï€=ä(å ±áà@2 @á å±âàB“΀ƒàÁµ$ ƒä‘ã B@JÀBå±@x@¿AAJäàEÒ‡œKàˆâ µ##e±åà=Wš„Fÿÿxæ =áˆÊ€{áˆ% å±çà@‰€@ÿ ÿbþ5\ @ÿÿ,â óå±ÖKE±èàbnˆ€báᨠbá¨ÿ é B@a¨ÀBÿÿÞ@x@ßA$A¨êàEYC\‘öÇ$%bóë =áj‰\ÓájïÂ%±ìà@ë€@å,±íàJWÀJ å ±î B@aRàBz ë¦ @' @x@ÇA ARïàEfº Ó DÀEâ û&&aRð =áR€=å"±ñà@n½€@ÿÿÞ&3‚RþƒFtð"ïQ ·àÿ¯ÆòàbÚ¼€bàá½å±ó B@ABàBñ@x@ßA$AjôàEíu`àˆá j''ajõ -áj}y€=ä(%-ä(1öà@Øx€@å$±÷àB9 BàÁÁJå±ø B@JÀBå±@x@¿AAJùàEt1ŸÿÙàËá J((e±úà=õ¤Fëcû =áˆd7€{áˆ% áˆ(¥üà@±2€@—ü y': @å±â óå±°Aëýàb báᨠbå ±þ B@a¨ÀBñ@x@ßA$A¨ÿàEúì€ëâ1á ¨))bó8ájšñ€=ájå ±à@úï€@å,±àJfÀJ ¤Å(ÁRëc B@aRàBÿÿ$X`x@9 AR @`E¨Ÿÿàˆá R**aR =â½?°€=áR"½à@•«€@ë cCk0áñSäðC!ïQk0â`Ì ˆÿÆŽàb bàáÁjå± B@a$àB†ëc@x@ßA$Aj àEd‚bÍàˆá j++aj =ájg€=ájå ± à@ìf€@à~Á$( @ãÅ(®D àBLàBå± B@JÀBå±@x@¿AAJàEŽƒáJ,,e±à=–íƒÇöÿÿ¦ =ሇ%€{è¥å ±à@Ê €@Ÿå ±ë câ óå±,ÿÌvàb* bá ë!¨ £áë«c B@a¨ÀBëc@x@ßA$A¨àEÛ€ëàˆâ ó--bó =ájÃ߀=ájå ±à@%Þ€@å,±àJ‘Ý JA#ë å± B@aRàBñ@x@ÇA ARàEœ–„âû..aR =áRHž€=äFáR%±à@œ™€@å ±S ñà?kE±%±þ0a ºÎDfàb bá jå± B@jàBå±@x@ßA$AjàE#RŸÿ Ÿèøâ ½//aj =áj³U€=áj%péÚ+c à@  @á å±!àBoT€ƒà B$ ƒä‘" B@JÀBå±@x@¿AAJ#àE© Ÿÿ ¤àˆá J00e±$à=±¤FÿÿL6% =ሢ€{äF% áˆ"ó&à@æ€@ñ !ˆ @ëcâ óå±@ˆaäF33aR/ =áRæG€=áR"½0à@FC€@ÿÿÞ &cãC! î0ä €ñœ0æa œGB½1àb²B€bàá½å±2 B@jÀBÿÿh°@x@ßA$>3àEÄû€ˆáóä f44aj4 =ájPÿ€=å"±5à@«þ€@á $(ån"üAJ6àB  BàÁÁJ$àB¥±7 B@JÀBå±@x@¿AAJ8àEK·‰bµàˆá J55e±9à=̤Fñ: =áˆ7½€{áˆ% å±;à@ˆ¸€@è ¥ë câ óå±8µA¨<àbçá ᨠ£å ±= B@a¨ÀB˜è ¥9¼•t@x@ßA$A¨>àEÒrŠa¨ÿÿô67bó? =áj3ŸÿÒä^"´@à@e1€@ÿ,ÿÞAàJÑ0 J å±B B@aRàBå±@x@Ç? =ARC @`Eß逈ÿÿÞ88aRD =áR„ñ€=áR%±Eà@Ûì€@ÿ ÿÞs år0äQÃDðïÑŠ0à`Ì ~úB½FàbG bãݽå±G B@a$àBÿÿF„@x@ßA$AjHàEf¥Œb½àˆä f99ajI =ájꨀ=ä(%-ä(+cJà@E @â rå±KàB¦§€ƒàÁÁJå±L B@JÀBå±@x@¿AAJMàEí`aJàËá J::e±Nà=ns„Få±O =áˆÝf€{áˆ% áˆ"óPà@*b€@—ñ !ˆ @ëcâ óå±ÚAëQàb‰a€báᨠbå ±R B@a¨ÀBöÇ@x@ßA$A¨SàEsŽa¨ëc;>aj^ =áj”R€=ájå ±_à@ïQ€@ã Å$( @ãÅ(®AJ`àBPàBå±a B@JÀBÿÿ* @x@¿AAJbàEŽ ŸÿÆáJ??e±cà=ƒÇÿÿQèd =ሂ€{äF% å±eà@Ð €@å ±ë câ óå±ÜÑA¨fàb. bâ ó!¨ £áë±g B@a¨ÀBå±@x@ßA$A¨hàEÆ€ëå±@Abói =áj:†’eïájå ±jà@œ„€@å,±kàJÀJA#å±l B@aRàBå±@x@ÇA /ªmàE"=“aàˆå ±BBaRn =áRØD€=äFáR(ooà@3@€@‹è o @èo“4õƒà"äƒ(ð)”ÿÿ@ÒCVB½pàbž?€bàáå±q B@jÀBÿÿ5n@x@ßA$AjràE©ø€ˆäfCCajs =áj9ü€=â½%pä(+ctà@•û€@å$±uàBõú€Bá J$ ƒä–Çv B@JÀBëc@x@¿AAJwàE0´”âµDDe±xà=±¤Få±y =ሺ€{áˆ% áˆ"ózà@fµ€@å ±': @âóå±çÿÚ˜{àbÌá Âó!¨ bᨖÇ| B@a¨ÀBÿÿ$X@x@ßA$A¨}àE·oŸÿüñEFbó~ =ájÔ/Ÿÿôáj$áj(fà@5.€@å,±€àJ¡- J å± B@aRàBÿÿ¦@x@ÇA AR‚àEÄæà ˆå ±GGaRƒ =áRiî€=áR"½„à@Äé€@ñ £PGtZ/øæÿÔÈ@7ïôÿÿàJFD©…àb0 bá jëc† B@jÀBå±@x@ßA$Aj‡àEK¢—äfHHajˆ =ájÓ¥€=áj%-â½%±‰à@. @á $( @ãÅ"ü@BŠàB¤€ƒà B$àB¥±‹ B@JÀBå±@x@¿AAJŒàEÑ]˜áJIIe±à=Wp„Fÿÿ5nŽ =áˆÂc€{áˆ% áˆ"óà@ _€@ÿÿF„â óå±²´A¨àbm^€bᨠ£á륱‘ B@a¨ÀBöÇ@x@ßA$A¨’àEX™a¨ëcJKbó“ =ájzÙ€=ê"´”à@Û×€@å,±•àJGÀJA#å±– B@aRàBëc@x@ÇA AR—àEfŸÿ$®àˆÿÿJº oA@LLaR˜ =@€ƒ˜€=â½ç"½™à@j“€@ñ ³3à$U00à*vÿåqöÇ­€B½šàbÖ’€bàáÈoå±› B@€æÀBå±@x@ßA$AjœàEìK›b½àˆá jMMaj =ájmO€=â½%-ä(%±žà@ÈN€@à~Á屟àB,àBå±  B@JàB@Ùâø:ÐÿÿWš@x@¿AAJ¡àEsœáJNNe±¢à=ô¤Fÿÿ* £ =@®€Ác €{âó% áˆ"ó¤à@­€@ÿ ÿ5n!ˆ @ëcâ óå± rAë¥àb bá ë!¨ bå ±¦ B@`æÀBñ@x@ßA$A¨§àEú€ëëcOPbó¨ =áj$ƒaæâóä^%±©à@…€@â m2€ @ÿÿ* ªàJñ€€Jî ¢ÿÿÞ« B@aRàBå±@x@ÇA AR¬àE:žaáÛÿå ±QQaR­ =áRµA€=â½!â½®à@=€@ÿ ÿ,à 1à$"QÆDð@ a ”&B½¯àbƒ< b@$ä#å±° B@jÀBÿÿ* @x@ßA$Aj±àEŽõà ˆá jRRaj² =ájù€=ájå ±³à@vø€@á $( @ãÅ"üAJ´àBÖ÷àB$àB«cµ B@JÀBëc@x@¿AAJ¶àE±ŸâµSSe±·à=–ÄF屸 =áˆý¶€{áˆ% âó¹à@O²€@ÿ ÿ,â óå±0ÔA¨ºàb±á Ç!¨ £áë«c» B@a¨ÀBñ@x@ßA$A¨¼àE›lŸÿ-âtâ óTTbó½ =ájEq€=å"±¾à@¨o€@ÿ,ÿ,¿àJÀJA#äFå±À B@aRàBÿÿ* @x@ÇA ARÁàE"(¡âûUUaR =áRÎ/€=áR(oÃà@*+€@ë cÓÿÿ*ƒ(a ÂÛB½Äàb–*€bâ4½å±Å B@jÀBî !ÿÿ¦@x@ßA$AjÆàE©ãà ˆâ ½VVajÇ =áj-ç€=ä(%-ä(+cÈà@‹æ€@â rå±ÉàBíå€Bá J$ ƒä–ÇÊ B@JàB@Ùâø!Këc@x@¿AAJËàE0Ÿ¢bµàËá JWWe±Ìà=7mƒÇÿÿ Í =@®€Á ¥€{áˆ% áˆ"óÎà@l €@ë câ óå±f)AëÏàb̡ᨠ£á¨ÿ Ð B@`æÀBëc@x@ßA$A¨ÑàE¶Z£a¨ñXYbóÒ = …`ƒêŸÿ1ùájê(fÓà@M€@â mëcÔàJ¹ J â¸å±Õ B@€ÎÀBÿÿÞ@x@ÇA ARÖàEÄÑà ˆâ ûZZaR× =áR€Ù€=å"±Øà@ØÔ€@ö Ç ãàÿQz) €²\},ðÿÿ¸l€æB½ÙàbD bá jå±Ú B@jÀBÿÿx@x@ßA$AjÛàEKŸÿ43äf[[ajÜ = …€ƒÛ€=â½%-ä(%±Ýà@5 @à~ƹå±ÞàB—€ƒà Bå±ß B@€ÆÀBëc@x@¿AAJààEÑHŸÿ5@áÓâ µ\\e±áà=R[„FÿÿÞâ =áˆÁN€{áˆ% áˆ%±ãà@J€@ö Çâ óå±$AëäàbmI€báÁë!¨ £å ±å B@a¨ÀBå±@x@ßA$A¨æàEX§å±]^bóç =ájpÄ€=ájå ±èà@Ó€@å,±éàJ?ÀJA#èjå±ê B@aRàBå±@x@ÇA ARëàEe{¨aRÿÿÁ__aRì =áR"ƒ€=å"±íà@z~€@î !$F!“óÓóƒP.QÃÿT0àÿÿ; â¡B½îàbæ}€bä få±ï B@jÀBå±@x@ßA$AjðàEì6©ajáóä f``ajñ =ájt:€=â½å ±òà@Ï9€@á !j @ãÅ(®AJóàB0 BàÁäàB«cô B@JÀBå±@x@¿AAJõàEsò€Ëàˆá Jaae±öà=ô¤Få±÷ =áˆgø€{áˆ% å±øà@¬ó€@ÿ ÿ¦!ˆ @âóå±FSA¨ùàb báᨠbáë«cú B@a¨ÀB î W- ÿÿB@x@ßA$A¨ûàEú­Ÿÿ;vöÇbcbóü =ájnŸÿ;náj$áj"´ýà@|l€@å,±þàJèk J þå ±ÿ B@aRàBöÇ@x@ÇA AR9  `E%¬äFddaR =áR»,€=áR(oà@(€@å±',àñ?ïT÷ð~ }{@Œà7:B½àb{'€bá jå± B@a$àBâ ½ñ@x`ß =Aj @`EŽàà ˆä feeaj =@Ý`=ä€=áj%-â½+cà@pã€@á $( @ãÅ"ü@BàBÒâàB)ºàB¥± B@€ÆÀBå±@x@¿AAJ àEœ­bµæ}á Jffe± à=–®„Få± =ሠ¢€{áˆ% áˆ"ó à@R€@å ±/ª @å±Ãâ ó屓ÈA¨àb±á å± B@a¨ÀBå±@x@ßA$A¨àE›W®a¨ñghbó =ájȯ`=ê"´à@*€@å,±àJ– J ëc B@aRàBëc@x@ÇA ARàE©Îà ˆâ ûiiaR =áRMÖ€=â½ç"½à@­Ñ€@üy 'z‚y5µ1à€±|ÿÿ¡¤ƒB½àbÀbA;ájå± B@jÀBÿÿh°@x@ßA$AjàE/ŠŸÿBMìÎjjaj =áj¼€=â½%-ä(%±à@ @á å±àB|Œ€ƒå(ä ƒåô¥± B@JàB@Ùá&ýöÇ@x@¿AAJàE¶E±cÊàËâ µkke± à=7X„FÿÿQè! =@®€Á¢K€{âó%páˆ"ó"à@óF€@è¥ @å±â ó屦qAë#àbR báᨠbᨖÇ$ B@`æÀBÿÿ¦@x@ßA$A¨%àE=²a¨ëclmbó& =ájfÁ€=âóä^%±'à@È¿€@å,±(àJ4ÀJA#â¸ñ) B@aRàBå±@x@ÇA AR*àEJx³aRàˆâ ûnnaR+ =áRÿ€=â½áR"½,à@V{€@öÇ'#Îå#0ãS#÷å~ÿ}€|a 9 B½-àbÂz€bàá½å±. B@jÀB…ëc@x@ßA$Aj/àEÑ3ŸÿFÚàˆá jooaj0 =áje7€=áj%-ä(%±1à@Ä6€@à~Áå±2àB%àBëc3 B@JàB@Ùâøå±@x@¿AAJ4àEXïà Ëá Jppe±5à=ݤFÿÿ 6 =@®€ÁLõ€{áˆ% áˆ"ó7à@•ð€@ÿÿF„â óå±ûAë8àbôá Áë!¨ £å ±9 B@`æÀBëc@x@ßA$A¨:àEÞªŸÿIˆâtá ¨qqbó; =áj£¯€=å"±<à@®€@è ,Î$ŸÿÿÞ=àJo­ J â¸å±> B@aRàBÿÿB@x@ÇA AR?àEef¶å±rraR@ =áRn€=áR"½Aà@qi€@ÿÿ, '3Å> "Kcþ$"õ‚àr†B½BàbÝh€bâ4½å±C B@jÀBÿÿyÆ@x@ßA$AjDàEì!·ájssajE =áj`%€=ä(å ±Fà@½$€@â rå±GàB  Bá J$ ƒäŸÿ,H B@JàB âøå±@x@¿AAJIàEsÝ€Ëàˆä tte±Jà={«ƒ|ÿÿ…*K =@®€Ácã€{áˆ% å±Là@®Þ€@ÿÿ,â óå±ÖtBóMàb báÂó!¨ £!n@ «cN B@`æÀBüy@x@ßA$A¨OàEù˜¸bóñuvbóP =áj(YŸÿNsáj%páj+cQà@ˆW€@â må±RàJôV JA#â¸å±S B@aRàBÿÿ; @x@ÇA ARTàEŸÿOBàˆâ ûwwaRU =áRº€=å"±Và@€@î!!“î!Cä4ƒõƒe°ÿuð¤ÿÿ…*…zB½Wàb{€bàá½å±X B@jÀBÿÿ; @x@ßA$AjYàEŽË€ˆå2á jxxajZ =ájÏ€=â½%-â½+c[à@{΀@à~Áå±\àBÚÍ€Bá J$ ƒä…±] B@JÀBöÇ@x@¿AAJ^àE‡ŸÿQºàˆá Jyye±_à=•™„Fÿÿx` =ሠ€{âó% áˆ(¥aà@Pˆ€@ö Çâ óå±!ˆAëbàb°á Áë!¨ £á¨ÿÞc B@a¨ÀBÿÿ5n@x@ßA$A¨dàE›B¼å±z{bóe =ájʽ`=âó$áj%±fà@*€@å,±gàJ– J å±h B@aRàBå±@x@ÇA ARiàE¨¹à ˆâ û||aRj =áR`Á€=â½áR"½kà@¹¼€@ñ  St-/õ‚ä4‚%¶å~Oza €ÿ¦làb% bä ©å±m B@jÀBÿÿŽ@x@ßA$AjnàE/u¾báóá j}}ajo = …€ƒ·x€=â½%-â½%±pà@ @á å±qàBsw€ƒàÁä ƒä…±r B@€ÆÀBå±@x@¿AAJsàE¶0¿aJø~~e±tà=7C„F}ÿÿ]Lu =ሦ6€{áˆ% áˆ"óvà@ò1€@ÿ ÿÞ!ˆ @è¥â óå±$ÿ¦wàbV bᨠbᨅ±x B@a¨ÀBöÇ@x@ßA$A¨yàE=ì€ëöÇ€bóz =ájh¬ŸÿWèájä^%±{à@Ǫ€@å,±|àJ3ÀJA#å±} B@aRàB} àBÿÿL6@x@ÇA AR~àEJcÁbûàˆä FaR =áRùj€=áR"½€à@Zf€@ü ycRYµàdÉp%QYàpàn!àbÆe b@(á'层 B@jÀBÿÿ; @x@ßA$AjƒàEÑÂáj‚‚aj„ =ája"€=áj%-ä(%±…à@¾!€@á $( @ãÅ"ü@B†àBàB)ºàB¥±‡ B@JÀBå±@x@¿AAJˆàEXڀˇ@Y¡Óâ µƒƒe±‰à=ݤFy届 =áˆDà€{áˆ% áˆ"ó‹à@”Û€@ÿ ÿbþ/ª @å±6Çâ óå±³t Œàbôá çZ bá륱 B@a¨ÀBå±@x@ßA$A¨ŽàEÞ•Ãbóñ„…bó =ájVŸÿ\ê"´à@qT€@å,±‘àJÝS JA#¢rÁRñ’ B@aRàBÿÿô@x@ÇA AR“àEì Ÿÿ]\àˆâ û††aR” =áR›€=â½ç"½•à@ô€@ë csUÁñVïðäõÿñSàBa ¡[Df–àb` bàáÁjå±— B@jÀBÿÿ @x@ßA$Aj˜àErÈ€ëàˆá j‡‡aj™ = …€ƒëË€=â½%-ä(%±šà@H @à~Áå±›àB¯Ê€ƒå(ÀB屜 B@€ÆÀBƒ@ÙâøöÇ@x@¿AAJàEùƒÆdàËá Jˆˆe±žà=z–„FÿÿWšŸ =@®€Á퉀{âó% áˆ"ó à@5…€@ü y!ˆ @å±6Çâ ó屩Aë¡àb•„€báᨠbå ±¢ B@`æÀBüy@x@ßA$A¨£àE€?Ça¨ëc‰Šbó¤ =áj¨ÿ€=âóä^%±¥à@ þ€@å,±¦àJwý J â¸å±§ B@aRàBå±@x@ÇA AR¨àE¶ÈaRàˆâ û‹‹aR© =áR9¾€=â½áR"½ªà@•¹€@ü yƒï´öx|?ÔC!öÇòšB½«àb bàá½å±¬ B@jÀBÿÿ; @x@ßA$Aj­àErŸÿcTàˆá jŒŒaj® =áj€u€=ájå ±¯à@Þt€@à~Á$( @ãÅ"üAJ°àB@àBå±± B@JàB âøå±@x@¿AAJ²àE›-Êâµe±³à=@ƒ|ÿÿ ´ =@®€Á‹3€{áˆ% å±µà@×.€@˜ÿ ÿ8bë câ óå±VhA¨¶àb7 bá ë!¨ £áë«c· B@`æÀBüy@x@ßA$A¨¸àE"é€ë屎bó¹ =ájJ©Ëaæå"±ºà@¬§€@â m!j$Ÿÿÿ,»àJÀJA#â¸å±¼ B@aRàBÿÿÞ@x@ÇA AR½àE/`Ìaàˆä FaR¾ =áRÃg€=áR%±¿à@#c€@ñ  “tåtps"ï\x$a ¨B½Ààbb b ájå±Á B@jÀBñ@x@ßA$AjÂàE¶Íáj‘‘ajà =áj>€=ä(%pä(+cÄà@˜€@ë å±ÅàBú€Bá J$ ƒä–ÇÆ B@JÀBñ@x@¿AAJÇàE<×à ˆâ µ’’e±Èà=¾¤FñÉ =áˆ!Ý€{âó% áˆ"óÊà@uØ€@ÿm @ëcâ óå±¶ZAëËàbØá Ç!¨ bá¨|yÌ B@a¨ÀBÿ ÿ Ò'Zÿÿô@x@ßA$A¨ÍàEÃ’Îbóñ“”bóÎ =ájðRÏ`=áj%páj(fÏà@RQ€@å,±ÐàJ¾P J þäFå±Ñ B@aRàBå±@x@ÇA ARÒàEÑ Ÿÿk~ãÇâ û••aRÓ =áRy€=áR"½Ôà@Ñ €@ñ £\vÿ@Õÿå$uð¤a TB½Õàb= bá jå±Ö B@jÀBñ@x@ßA$Aj×àEWÅ€ëàˆá j––ajØ =áj×È€=å"±Ùà@2 @áéÃÅå±ÚàB“Ç€ƒàÁÀB$å ±Û B@JÀBå±@x@¿AAJÜàEÞ€ÑcÊàˆá J——e±Ýà=_“Î`=ëcÞ =áˆÒ†€{áˆ% äF"óßà@‚€@ñ )ø @å±6Çâ óå±»¦Bóààbz€báᨠbå ±á B@a¨ÀB å±@x@ßA$A¨âàEe<Òajëc˜™bóã =áj‘ü€=ájå ±äà@ôú€@å,±åàJ` Jã >屿 B@aRàBå±@x@ÇA ARçàEr³ÓaRÿuššaRè = …€ƒ»€=å"±éà@v¶€@ñ  ³"ðuîõ " u ÿ Œà«ÑB½êàbâµ€bâ ½å±ë B@€æÀBñ@x@ßA$AjìàEùnŸÿqvàˆä f››ají =áj}r€=â½%-ä(+cîà@Øq€@â rå±ïàB= BàÁä ƒä‹cð B@JàB@Ùìñ)ºÿÿô@x@¿AAJñàE€*Ÿÿr{ⵜœe±òà==ƒÇöÇó = ž€Át0€{áˆ% áˆ%±ôà@Á+€@—ñ !ˆ @å±6Çâ óå±#Aëõàb  bᨠbᨋcö B@`æÀBñ@x@ßA$A¨÷àEæ€ëëcžbóø = …`ƒ'¦Ödœájå ±ùà@‰¤€@â m' @ñúàJõ£ J â¸å±û B@€ÎÀBÿÿ$X@x@ÇA ARüàE]×aâûŸŸaRý =áRÉd€=å"±þà@$`€@å ±Ãu JE«å~%´u IöÇÂÿ¯Æÿàb_€bá jå±:  jÀBå±@x@ßA$AjàE›Øajå2å ±  aj =áj#€=â½%-ä(%±à@~€@à~Ån$( @ãÅ.`@BàB߀Bá Jëc B@JÀBëc@x@¿AAJàE!Ôà ˆá J¡¡e±à=ʤFSÿÿ, =áˆÚ€{âó% áˆ%± à@_Õ€@—å ±$F @å±+câ óå±fÿµ® àb½á Áë!¨ bá륱 B@a¨ÀBå±@x@ßA$A¨ àE¨ŸÿxÉå±¢£bó =ájÝOŸÿxÁâó%páj"´à@?N€@å,±àJ«M JA#èjå± B@aRàBå±@x@ÇA ARàEµÛdFàˆâ û¤¤aR =áR^€=áR"½à@ €@ö ÇÓs<çC""t!/a }®Dfàb. bä ©å± B@jÀBxî ! ÿu@x@ßA$AjàE< ë@Y¡óá j¥¥aj =ájÌÅ€=áj%pâ½%±à@( @á å±àBˆÄ€ƒàÁä ƒä‹c B@JÀBå±@x@¿AAJàEÃ}Üⵦ¦e±à=D„Fÿÿ$X =ሯƒ€{áˆ% áˆ"óà@€@ÿ ÿÞ!ˆ @å±+câ óå±Ù\Aëàb_~€bᨠbᨋc B@a¨ÀBÿÿÞ@x@ßA$A¨!àEJ9Ýa¨ñ§¨bó" =ájsù€=çå ±#à@Õ÷€@‡@S£r°' @ëc$àJ@ Já Rå±% B@aRàBå±@x@ÇA AR&àEW°Ÿÿ~càˆä F©©aR' = …€ƒ¸€=â½!â½(à@c³€@ë cãvÇ ‚õƒäð"åP0á ÿÿnb6ÿÉ‚)àbϲ€bàáÁµå±* B@€æÀBå±@x@ßA$Aj+àEÞkŸÿˆàˆá jªªaj, =ájRo€=áj%-áj%±-à@­n€@à~Á$( @ãÅ(®@B.àBàB)ºàB¥±/ B@JÀBå±@x@¿A>0àEe'àdæ}á J««e±1à=æ¤Få±2 =áˆQ-€{áˆ% áˆ%±3à@ž(€@ÿ ÿ,â óå±’ÿÉ‚4àb báÁë!¨ £á륱5 B@a¨ÀBå±@x@ßA$A¨6àEëâ€ëëc¬­bó7 =áj!£áaæå"±8à@‚¡€@ë,c9àJî  JA#åüå±: B@aRàBå±@x@ÇA AR;àEùYâaàˆâ û®®aR< =áR¦a€=â½å ±=à@]€@ü yóà (SPþ"("Ca ¤MDf>àbm\€bàá½å±? B@jÀBå±@x@ßA$Aj@àEŸÿ„5àˆá j¯¯ajA =áj€=â½%-ä(%±Bà@c€@à~Áå±CàBÄ€Bä ƒä‹cD B@JÀBÿÿbþ@x@¿AAJEàEÑ€ˆàËá J°°e±Fà=‡ã„FÿÿL6G =áˆþÖ€{âóáˆ%±Hà@@Ò€@ÿÿtâ ó屿AëIàb¢á Áëå±J B@a¨ÀBÿÿ¦@x@ßA$A¨KàEŒŸÿ†ãå±±²bóL = …`ƒ³LŸÿ†ãâóä^(fMà@K€@å,±NàJ€J J å ±O B@€ÎÀBå±@x@ÇA ARPàEšæe±Œ@Y¦:â û³³aRQ = +€ƒD €=áR"½Rà@ž€@Œó Ó$F!“î!(C[CgB¢âå0ÿÿh°‡ªB½Sàb  bá jå±T B@€æÀBå±@x@ßA$AjUàE!¿€ëàˆá j´´ajV =áj©Â€=ä(%-å±Wà@ @å n!j @`ò" "üAJXàBeÁ€ƒàÁäàB¥±Y B@JÀBŒ@Ù¢oÀB2aüy@x@¿AAJZàE¨zçâµµµe±[à=)„Fëc\ =@®€Áœ€€{áˆå ±]à@æ{€@ëcâ óå±#ÿÏ4^àbH bᨠ£áë«c_ B@`æÀBöÇ@x@ßA$A¨`àE/6èa¨ñ¶·bóa =áj]ö€=ä^"´bà@½ô€@å,±càJ)ÀJ â¸ëcd B@aRàBÿÿ/¼@x@ÇA AReàE<­éaRàˆä F¸¸aRf =áRÒ´€=áR%±gà@,°€@å ± å" âSïC|äÿÿQèÒLDfhàb˜¯€bàáÄ#å±i B@jÀBå±@x@ßA$AjjàEÃhêáj¹¹ajk =ájGl€=ä(%-å±là@£k€@å$±màB Bá J)º ƒä‹cn B@JÀBå±@x@¿AAJoàEI$ëáJººe±pà=ˤFå±q =áˆ:*€{áˆ% áˆ"órà@„%€@å ±': @è¥â óå±v¾Bósàbåá Âó!¨ bᨖÇt B@a¨ÀBÿÿbþ@x@ßA$A¨uàEÐßëa¨ëc»¼bóv =áj÷Ÿì`=ä^%±wà@Wž€@å,±xàJàJA#å±y B@aRàBå±@x@ÇA ARzàEÞVíaãÇå ±½½aR{ =áRŒ^€=áR"½|à@æY€@ñ(#ð1”P$ï$ÿ1ÿƒa ÖB½}àbR bá jå±~ B@jÀBå±@x@ßA$AjàEdŸÿ’Oàˆá j¾¾aj€ =ájì€=ä(%-ä(Cà@I @áéÃÅ$( @ãÅ"ü@B‚àB¨€ƒàÁÀB$àB¥±ƒ B@JÀBå±@x@¿AAJ„àEëÍ€ˆî¿¿e±…à=làëbóñ† =áˆÛÓ€{áˆ% áˆ"ó‡à@&Ï€@›î W; @å±Ãâ óå±¥ÿ­ˆàb‹Î€bᨠbá륱‰ B@a¨ÀBöÇ@x@ßA$A¨ŠàEr‰Ÿÿ”ýëcÀÁbó‹ =áj”Iða¨áj/áj"´Œà@ùG€@å,±àJe Jã >ëcŽ B@aRàBwÿÿ* @x@ÇA ARàEñaáÛä FÂÂaR =áR.€=å"±‘à@‡€@å ±3(àþ$)±Sïðt).±Sàÿÿnbþ€Df’àbó€bàáÄf屓 B@jÀB‚üy@x@ßA$Aj”àE¼à ˆá jÃÃaj• =ájŽ¿€=â½%-â½%±–à@ë¾€@â rå±—àBJ Bá J屘 B@JÀBå±@x@¿AAJ™àEwòâµÄÄe±šà=Š„Fÿÿ­› =áˆy}€{áˆ% áˆ%±œà@Èx€@—å ±!ˆ @å±(¥â óå±Zÿh°àb) bᨠbå ±ž B@a¨ÀBå±@x@ßA$A¨ŸàE3Ÿÿ™ŠëcÅÆbó  =áj2ó€=ájå ±¡à@’ñ€@å,±¢àJþð J ëc£ B@aRàB|å±@x@ÇA AR¤àE!ªŸÿš—àˆä FÇÇaR¥ =áRñ€=áR"½¦à@!­€@ñ Ct .?Ê1 €Öƒ)å±§D©§àb¬€bàá屨 B@jÀBÿÿ@Ò@x@ßA$Aj©àE¨eõdfÿÿÍéÈÈajª =áj,i€=ájå ±«à@‰h€@å$±¬àBìg€Bá Jå±­ B@JàB@ÙâøöÇ@x@¿AAJ®àE.!öaJàˆâ µÉÉe±¯à=¯¤F}ÿÿ ° =@®€Á'€{G °àÁe å±±à@j"€@å ±ë câ óå±}Bó²àbÊá Âó!¨å ±³ B@`æÀBœè ¥!¨ÿÿ¦@x@ßA$A¨´àEµÜ€ëå±ÊËbóµ =ájèœ÷aæájå ±¶à@H›€@â m!j @ÿÿÞ·àJ´š J þáöëc¸ B@aRàBñ@x@ÇA AR¹àEÃSŸÿŸDãÇâ ûÌÌaRº =áRm[€=äF!â½»à@ËV€@å ±Såp;/ƒ*à`åa ú¸B½¼àb7 bàá½ëc½ B@jÀBå±@x@ßA$Aj¾àEIŸÿ iáóá jÍÍaj¿ =ájÉ€=ájå ±Àà@& @à~Á)Ú @éw.`AJÁàB…€ƒà B$àBi–Ç B@JÀBëc@x@¿AAJÃàEÐÊ€ˆñÎÎe±Äà=QÝödå±Å =áˆÄЀ{áˆ% âóÆà@Ì€@—ëcâ óå±^JA¨ÇàbpË€bᨠ£áë±È B@a¨ÀBå±@x@ßA$A¨ÉàEW†Ÿÿ£ñÏÐbóÊ =áj†Fûa¨å"±Ëà@æD€@ÿ,ÿ ÌàJRÀJA#èjå±Í B@aRàBå±@x@ÇA ARÎàEdý€ÓáÛä FÑÑaRÏ =áRüaRâ½å ±Ðà@h€@å ±c%®?)e® åpÿÿñ`®ôB½ÑàbÔÿà áÄ©å±Ò B@jÀBÿÿ* @x@ßA$AjÓàE븀ëájÒÒajÔ =ájo¼€=áj%-ä(1Õà@Ì»€@à~Áå±ÖàB+àBå±× B@JÀBŒ@Ùâø)ºÿÿ,@x@¿AAJØàErtýbwã>â µÓÓe±Ùà=ó¤Få±Ú =@®€Á^z€{áˆ% áˆ.WÛà@©u€@ü y)ø @ñ6Çâ óå± AëÜàb báÁë!¨ bå ±Ý B@`æÀBñ@x@ßA$A¨ÞàEø/þa¨ëcÔÕbóß =áj,ð€=å"±àà@‹î€@ë,cáàJ÷í J â¸å±â B@aRàBñ@x@ÇA ARãàE§Ÿÿ¨¹àˆâ ûÖÖaRä =áR±®€=áR"½åà@ª€@ÿÿô(sR`<·å²àÞ|B½æàb~© b@(ájå±ç B@jÀBÿÿyÆ@x@ßA$AjèàEŒbÿd¿àˆá j××ajé =ájf€=ájå ±êà@ne€@à~ÃÅå±ëàBÍd€Bä ƒä‹cì B@JÀBÿÿ @x@¿AAJíàEÿª(àËá JØØe±îà=”0ƒ|å±ï =áˆÿ#€{å"±ðà@O€@ÿÿ/¼â óå±zŽBóñàb¯á Áëå±ò B@a¨ÀBå±@x@ßA$A¨óàEšÙ€ëå±ÙÚbóô =ájÊ™c1âóê.õà@-˜€@å,±öàJ™— JA#äFå±÷ B@aRàBÿÿ; @x@ÇA ARøàE§Paàˆâ ûÛÛaRù =áRSX€=áR%±úà@³S€@å ±ƒåe²#å²a \iB½ûàbÀb@$ájå±ü B@jÀBëc@x@ßA$AjýàE. Ÿÿ®ƒàˆá jÜÜajþ =áj¢€=ä(%-éÚ+cÿà@ @å nå±;àƒf€ƒàÁä ƒå ± B@JÀB„ áëc@x@¿AAJàEµÇà ˆá JÝÝe±à=6Ú„FuöÇ =@®€Á¥Í€{áˆ*¿áˆcà@ñÈ€@ë câ óå±ø›AëàbQ bᨠ£á¨ŸÿÞ B@`æÀBñ@x@ßA$A¨àE<ƒd^ÿÿë®Þßbó =ájpC`=ä^%± à@ÒA€@å,± àJ>ÀJA#äFå±-Íȯj`'èF ;` >A8 y  `  @'  5  > @@Iú€Eˆ@`;ÿÿ    ààA =@`=í`ƒG "À=àƒÿ  @@3Òcà@Iý€~Œ % ~À@ `@1 @# K (“åR`<[ƒ+à` åà!þC ¬àbµü€bàáÀb`b B áaj B@€æÀB à¥áj@x@ßA$AjàEе,Œ DÀEá jááaj =A6€ƒT¹€=H <À=ÀÆ!jáj!à@²¸€@‡ ?à@!j @áK $ BàB Bá J àBiJ B@€ÆàB@ÙáJ!K‚µ@x@¿AAJàEVqbwàˆá Jââ@=à=؃`=}3#:àƒ€  ŒB‘ "ó =@®€ÁCw€{áˆáˆ"óà@“r€@“@a€táÉ!ˆ @âó ›”hv¼ï/GÌï?… A¨àbòá Â,!¨ báëiX$^ B@`æÀB™â ó!¨Á¨@x@ßA$AjàEÝ, aj¼ 5 ëá¨!‚ãäbó =ájí€=ájä^"´à@hë€@â m`@Ä> …Ã! @Ž … AR àJÔê J  ÉÁR `JáR-‚û! B@aRàBå±@x@ÇA AR"àEë£ aRãÇáRA@ååaR# =@€ƒ“«€=G "àÆ$FáR"½$à@ó¦€@ˆâ ½ @â½£påS%²"Ï¥² yàµÏAj%àb_Àb@&ájå±& B@€æÀB{â ½å±@x@ßA$Aj'àEq_ ajàˆá jææaj(; ] …€ƒc€=â½%pç%n)à@\b€@áéÂr$( @âr"ü@B*àB½a€BàÁÀB$àB¥±+ B@€ÆÀBŒ@Ùá!Kå±@x@¿AAJ,àEø aJæüççe±-à=y-ƒ|zå±. =@®€Áà €{áˆ% áˆ"ó/à@5€@å ±$F @âóÃâ óå±=A¨0àb˜€bᨠbá륱1 B@`æÀBâ ó!¨å±@x@ßA$A¨2àEÖ€ˆå±èébó3 = ``ƒ«– aæáj$áj"´4à@•€@‡@Sâm!j @å±5àJz” J àJå±6 B@€ÎÀBå±@x@ÇA AR7àEŒMaáÛä FêêaR8 =@þ€ƒ=U€=å"±9à@”P€@å±³¥²?ã…²a ¿xB½:àb bàáÄfå±; B@€æÀB„â ½å±@x@ßA$Aj<àE ájëëaj= = { ƒ— €=â½%-â½%±>à@ò €@â rå±?àBS Bá Jå±@ B@JÀBå±@x@¿AAJAàEšÄ€Ëàˆâ µììe±Bà=ׄFå±C =ሊʀ{áˆ% áˆ%±Dà@×Å€@—â ó @å±.Wâ ó届AëEàb6 báÂó!¨ bå ±F B@a¨ÀBœâ ó"ôå±@x@ßA$A¨GàE €bóëcíîbóH =ájI@`=áj%páj%±Ià@«>€@ë,cJàJÀJ þå ±K B@aRàBzLWæ?ñ@x@ÇA ARLàE.÷ Ó@Y Eâ ûïïaRM =áRÆþ€=áR"½Nà@&ú€@ë cÃåŲ&íäƒ(ð@¸ÀëB½Oàb’ù b@% á½å±P B@jÀB|â ½å±@x@ßA$AjQàEµ²bå2á jððajR =ájA¶€=ájå ±Sà@›µ€@á å±TàB BàÁÁJ$ ƒäŽU B@JÀBã C!Këc@x@¿AAJVàE;naJàËá Jññe±Wà=¼¤FwëcX =áˆ+t€{áˆ% äF"óYà@yo€@å ±)ø @å±+câ ó屸BóZàb×á ᨠbᨑ[ B@a¨ÀBå±@x@ßA$A¨\àEÂ)a¨å±òóbó] =ájëé€=ê%±^à@Mè€@â m!j%òëc_àJ¹ç JA#äFëc` B@aRàBëc@x@ÇA ARaàEÏ 8@Tàˆâ ûôôaRb =áR|¨€=â½!â½cà@Ø£€@ö ÇÓ1”P)t/øæý`à%@½ÀàB½dàbD bä få±e B@jÀB€å±@x@ßA$AjfàEV\ájõõajg =ájÚ_€=ájáj+chà@9 @áéÃÅå±iàBš^€ƒà Bå±j B@JàB@Ùâøå±@x@¿AAJkàEÝáJööe±là=^*ƒ|{å±m =@®€ÁÍ€{áˆ% âónà@€@˜ëcâ ó屪Aëoàby€bᨠ£å ±p B@`æÀBå±@x@ßA$A¨qàEdÓ€ˆñ÷øbór =áj“aæå"±sà@î‘€@â må±tàJZÀJA#â¸å±u B@aRàBëc@x@ÇA ARvàEqJaàˆå ±ùùaRw =áR*R€=áR(oxà@yM€@œâ ½%òèo.!ãàÿä3þï$·ÿî4Ç‚õƒa c“B½yàbåL€bàáÄ©å±z B@jÀBŒå±@x@ßA$Aj{àEøájúúaj| =áj€ €=ä(å ±}à@Û€@á å±~àB<àBå± B@JÀBñ@x@¿AAJ€àE~Áà Ëâ µûûe±à=Ô„Fñ‚ = 5 ÁwÇ€{áˆ% 屃à@´Â€@¨â ó$F @âó+câ óå±ÇaBó„àb bè ¥!¨ bå ±… B@a¨ÀB¤â ó'Zñ@x@ßA$A¨†àE}bóöÇüýbó‡ =áj;=`=ä^+cˆà@œ;€@å,±‰àJÀJ þå ±Š B@aRàBå±@x@ÇA AR‹àEô€Ó‹@Y¦:â ûþþaRŒ =áR¸û€=áR%±à@÷€@ñ óíð1 à$øäö1 €Ñ"@¸À‡‹B½Žàb‡ö b@% áå± B@jÀBxâ ½ÿÿ,@x@ßA$AjàE™¯bû.ÿÿaj‘ =áj"³€=ájå ±’à@}²€@ã Å屓àBݱ€Bá J$ ƒä‘” B@JÀBå±@x@¿AAJ•àE kaJàˆâ µ@=–à=¥¤Fëc— = ÀÁq€{å"±˜à@fl€@”å ±!ˆ%Nå±(¥â óå±#yBó™àbÈá Âó!¨ bᨑš B@`æÀB”â ó"ôå±@x@ßA$Aj›àE§&a¨ëcA%b \ ``ƒÑæ€=âó)Ïç%±à@2å€@å,±žàJžä€Jã >ëcŸ B@€ÎÀBå±@x@ÇA AR àE´ aRâûaR¡ =áRf¥€=áR%±¢à@À €@â ½!“â½)E¯ÿÃ"e·ð"ÿÿ,/¾B½£àb, bàá½ëc¤ B@jÀBâ ½å±@x@ßA$Aj¥àE;Y!ájaj¦ = …€ƒÃ\€=ä(%-â½1§à@ @â r$( @ãÅ"ü@B¨àBƒ[€ƒá J$àB¥±© B@€ÆàB@Ùç?!K ÿ ”@x@¿AAJªàEÂ"áJe±«à=C'ƒ|öǬ =@®€Á²€{áˆ% áˆ"ó­à@ü€@î Wë câ ó屋LA¨®àb^ bᨠ£á륱¯ B@`æÀBñ@x@ßA$A¨°àEHЀëöÇbó± =ájn#aæáj$áj"´²à@ÏŽ€@â m!j @`òÿ ÿÞ³àJ;ÀJ ,â¸å±´ B@aRàB{üy@x@ÇA ARµàEVG$aéøaR¶ =áRO€=å"±·à@jJ€@öÇ)7$/pAu$`Aƒå a q@B½¸àbÖI€bá jå±¹ B@jÀBå±@x@ßA$AjºàEÝ%ajå2ê   aj» =áj]€=â½%-â½%±¼à@½€@à~ƹå±½àB Bá Jå±¾ B@JàB@Ùå±@x@¿AAJ¿àEc¾€Ëàˆá J  e±Àà=ä¤F|öÇÁ =@®€ÁTÄ€{âó% áˆ%±Âà@ž¿€@—è ¥ @è¥â óå±Õ²AëÃàbÿá Áë!¨ bå ±Ä B@`æÀBå±@x@ßA$A¨ÅàEêy&bóå±  bóÆ =áj:'`=âó%páj%±Çà@i8€@â må±ÈàJÕ7 JA#â¸å±É B@aRàBå±@x@ÇA ARÊàE÷ð€ˆå±  aRË =áR¦ø€=â½áR"½Ìà@ô€@å ±#àAqå8Ó”På8d`a •B½Íàbtó€bä ©å±Î B@jÀByëc@x@ßA$AjÏàE~¬(bájajÐ =áj °€=â½å ±Ñà@g¯€@á å±ÒàBÆ®€Bâ µ$ ƒä‘Ó B@JÀBñ@x@¿AAJÔàEh)aJàˆå ±e±Õà=Šz„Få±Ö =áˆñm€{áˆ% äF"ó×à@éàE§-aJæ}â µe±êà=($ƒ|å±ë =ሓ€{áˆ%páˆ"óìà@â€@“!â!ˆ @å±â óå±µªAëíàbC báå±î B@a¨ÀBå±@x@ßA$A¨ïàE-Í€ëüybóð =ájd.aæê%±ñà@Ä‹€@å,±òàJ0ÀJA#áµëcó B@aRàBëc@x@ÇA ARôàE;D/aàˆâ ûaRõ =áRéK€=â½ç"½öà@GG€@ë cC A‡å8ÔðPAmå`½ÀëOB½÷àb³F€bàá½å±ø B@jÀB~å±@x@ßA$AjùàEÁÿà ˆá jajú =áj60`êâ½å ±ûà@’€@á $( @ë "üAJüàBö€BäàB«cý B@JàB@Ùâø&ýöÇ@x@¿AAJþàEH»€ˆàËá Je±ÿà=ɤFyñ< š €Á4Á€{âó%på±à@ƒ¼€@ü yë câ ó›ÿÿ¦O½A¨àb什báÂó'Z £áë¶Ç B@`æÀBå±`x@9 A¨ @€EÏv1bµëcbó =ájú62`=âóä^"´à@Z5€@å,±àJÆ4 J@Ýâ¸å± B@a àBå±@x@ÇA AR àEÜíà ˆâ ûaR =áR{õ€=â½áR%± à@Üð€@ë cSÓ”õ@%ªO™å8TÿÿÞAsB½ àbHÀbA;ájå± B@jÀBÿÿ¦@x@ßA$AjàEc©3bàˆá jaj =áj笀=áj%-ä(+cà@H @à~ÃÅå±àB§«€ƒà B$ ƒä‘ B@JàB@Ùáå±@x@¿AAJàEêd4áJe±à=kw„Fÿÿ$X =@®€ÁÖj€{áˆ% áˆ"óà@%f€@å ±!ˆ @ëcâ ó›å±åAëàb†e€bᨠb᨟ÿ, B@`æÀBå±@x@ßA$A¨àEp 5a¨ëc bó =ájœà€=å"±à@ûÞ€@â m'$ŸüyàJgÀJA#â¸å± B@aRàBñ@x@ÇA ARàE~—6aRõ\!!aR =áR1Ÿ€=áR"½ à@Šš€@â½!Râ½c(ðàÿT<õƒå9ðïT`zÀY”B½!àbö™€bá jå±" B@jÀB‚ëc@x@ßA$Aj#àES7áj""aj$ =áj•V€=ä(%-ä(%±%à@òU€@ë $( @ãÅ(®@B&àBQ Bá Jå±' B@JÀBñ@x@¿AAJ(àE‹8aJàˆæ ü##e±)à=!ƒÇñ* =ሀ€{bxãºe áˆ"ó+à@À@¤ñ $F%Nâó6Çâ óå±ýfA¨,àb' báÇ!¨ bá륱- B@a¨ÀB â ó'ZöÇ@x@ßA$A¨.àEÊ€ëå±$%bó/ = ``ƒ6Š9aæáj$áj"´0à@•ˆ€@å,±1àJÀJ áöå±2 B@€ÎÀBå±@x@ÇA AR3àE A:aãÇâ û&&aR4 =áR×H€=áR"½5à@(D€@ÿÿ!“â½3Ó så:ðïÄTðñÚ%³; a ý76àb”C€bàá½å±7 B@jÀBŠâ ½öÇ@x@ßA$Aj8àE¦ü€ˆç''aj9 =áj6;a¨å"±:à@”ÿ€~á å±;àBòþ€Bâ µ$åô«c< B@JÀBå±@x@¿AAJ=àE-¸ àˆâ µ((e±>à=²¤Fëc? =áˆ%¾€{áˆ% äF"ó@à@i¹€@ å ±!ˆ @âóå±Yÿ AàbÉá Âó!¨ bå ±B B@a¨ÀBÿÿB@x@ßA$A¨CàE´sbàˆä f,,ajN =ájÈ©€=â½%-â½+cOà@& @â r$( @ãÅ"ü@BPàBŒ¨€ƒàÁÁJ$àB¥±Q B@JÀB„@Ùâø&ýöÇ@x@¿AAJRàEÏa?aJàËá J--e±Sà=Pt„FuöÇT =@®€ÁÃg?`{áˆ% áˆ%±Uà@ c€@›å ±$F @å±+câ óå±§ÁA¨Vàbkb€báᨠbáë«cW B@`æÀBëc@x@ßA$A¨XàEU@a,ëc./bóY =ájÝ€=ájáj"´Zà@ðÛ€@ë,c[àJ\ÀJ â¸å±\ B@aRàBÿÿ$X@x@ÇA AR]àEc”AaRàˆâ û00aR^ =áRœ€=å"±_à@k—€@ñ “%­=QTå>@/a xÝB½`àb×–€bàá½å±a B@jÀB‡å±@x@ßA$AjbàEéOBáj11ajc =ájrS€=áj%-ä(%±dà@ÐR€@ã Åå±eàB2àBå±f B@JàB@Ùâøå±@x@¿AAJgàEp CáJ22e±hà=ñ¤Fÿÿ¦i =@®€Ál€{äF% áˆ%±jà@­ €@£å ±!ˆ @å±.Wâ óå±Ü/Aëkàb  bâ ó!¨ bå ±l B@`æÀB¨â ó!¨ñ@x@ßA$A¨màE÷Æ€ëå±34bón =áj'‡Daæájå ±oà@Š…€@å,±pàJö„ J þâ¸å±q B@aRàBå±@x@ÇA ARràE>Eaàˆå ±55aRs =áR¸E€=áR"½tà@A€@å ±£%±tˆ‹_Ttðïa ’íB½uàb|@€bàáå±v B@jÀB„â ½ñ@x@ßA$AjwàE‹ùà ˆá j66ajx =ájý€=ájå ±yà@uü€@á $( @ãÅ(®AJzàB×ûàB$àB«c{ B@JÀBñ@x@¿AAJ|àEµFâµ77e±}à=“Ç„Få±~ =ሻ€{áˆ% å±à@K¶€@Ÿå ±ë câ óå±6A¨€àb®á Âó!¨ £áë«c B@a¨ÀB â ó"ôå±@x@ßA$A¨‚àE™pGa¨ñ89bóƒ =ájÉ0H`=è¥å ±„à@+/€@å,±…àJ—. J þå ±† B@aRàBå±@x@ÇA AR‡àE¦çà ˆä F::aRˆ =áR^ï€=â½áR%±‰à@²ê€@å ±³q ò+i?…·a bó˜ =áj_Ú€=å"±™à@ÁØ€@å,±šàJ-ÀJA#î dêáR-‚û› B@aRàBå±@x@ÇA ARœàEH‘LaRû??aR =áR™€=â½å ±žà@P”€@ü yÃSú%­@QaåA%´a ¹­B½Ÿàb¼“€bá jå±  B@jÀBÿÿ/¼@x@ßA$Aj¡àEÎLMajàˆå ±@@aj¢ =!b ƒSP€=â½%-ä(%±£à@­O€@à~層àBàB$ ƒâµ¥±¥ B@JàB@Ùâø&ýöÇ@x@¿AAJ¦àEUNáJAAe±§à=Ö¤Fÿÿ$X¨ =@®€ÁQ€{âó% áˆ%±©à@’ €@ÿ ÿ,â óå±_Aëªàbñá Áë!¨ £á¨…±« B@`æÀBñ@x@ßA$A¨¬àEÜÀëëcBCbó­ =áj„Oaæâóä^.®à@c‚€@â m,Î @üy¯àJÏ€Jç å±° B@aRàBå±@x@ÇA AR±àEé:PaãÇä FDDaR² =áR†B€=áR"½³à@Ý=€@ü yÓ B@`æÀBñ@x@ßA$A¨?àE³4fa¨üybcbó@ =ájÞô€=âóä^%±Aà@>ó€@â m' @ÿÿÞBàJªò J â¸å±C B@aRàBå±@x@ÇA ARDàEÁ«gaRãÇâ ûddaRE =áR`³€=â½!â½Fà@Á®€@ë cC€ãt+ûä:úƒ(tðÈ`;ÿnbGàb- bàá½å±H B@jÀBÿÿ@Ò@x@ßA$AjIàEGghajáóá jeeajJ = …€ƒ×j€=â½%-áj1Kà@5 @á å±LàB“i€ƒàÁÁJ$ ƒä…±M B@€ÆÀBëc@x@¿AAJNàEÎ"iáJffe±Oà=O5ƒ|ÿÿ* P =ሲ(€{áˆ% áˆ(¥Qà@$€@ÿ ÿ@Ò$F @ÂÓ#!8+câ ó屟ŠAëRàbj#€bᨠbᨅ±S B@a¨ÀBëc@x@ßA$A¨TàEUÞ€ˆÿÿ@ÒghbóU =ájxžjaæájä^%±Và@ל€@å,±WàJCÀJA#å±X B@aRàB}UÉàBÿÿnb@x@ÇA ARYàEbUkaàˆä FiiaRZ =áR]€=áR"½[à@rX€@å ±S”P/ Q‹ma ÏPB½\àbÞW b@$á'å±] B@jÀB{üy@x@ßA$Aj^àEélájjjaj_ =ájm€=áj%-ä(%±`à@Ê€@á å±aàB)àBå±b B@JÀBå±@x@¿AAJcàEpÌ€Ëæ}â µkke±dà=ñ¤Få±e =áˆ\Ò€{áˆáˆ"ófà@¬Í€@å ±!ˆ @å±6Çâ óå±k¢Aëgàb  báçZ bå ±h B@a¨ÀBöÇ@x@ßA$A¨iàEö‡mâóllbój =áj•Œ€=ê%±kà@öŠ€@å,±làJbÀJA#äFëcm B@aRàBöÇ@x@ÇA ARnàE}CnáRmmaRo =áR.K€=áR"½pà@F€@ÿÿô*c` €1 €å€F¯à?«B½qàbíE€bá jå±r B@jàBå±@x@ßA$AjsàEÿà ˆä fnnajt =áj„oa¨ä(å ±uà@߀@á $( @`ò" "üAJvàB@àBå±w B@JÀBå±@x@¿AAJxàEŠºà Ëá Jooe±yà=’ˆƒÇöÇz = ÀÁ{À€{áˆ% ç:"ó{à@À»€@ ñ ë câ óå±›YA¨|àb' bå$±} B@`æÀBñ@x@ßA$A¨~àEvpbµñpqbó = …`ƒG6q`=ä^"´€à@¨4€@å,±àJÀJ 层 B@€ÎÀBå±@x@ÇA ARƒàEí€ÓáÛâ ûrraR„ =áR´ô€=áR"½…à@ð€@ÿÿ *s€å0àCÅ€€å±¼e±†àb{ï€bá j屇 B@jÀBÿÿ @x@ßA$AjˆàE¥¨rbüyssaj‰ =áj.¬€=áj%pä(+cŠà@‰«€@áéÉw屋àB骀Bá J屌 B@JÀBå±@x@¿AAJàE,dsaJàˆâ µtte±Žà=±¤FÿÿÞ =áˆj€{å"±à@fe€@ÿÿôâ óå±e±‘àbÈá Áë!¨&Uç±’ B@a¨ÀBå±@x@ßA$A¨“àE³ta¨âóuubó” =áj_$€=ájñ •à@¿"€@å,±–àJ+ÀJA#å±— B@aRàBöÇ@x@ÇA AR˜àE:Û€Óÿÿ(vvaR™ =áRÝâ€=äFáR%±šà@>Þ€@ü yƒ±Lå6d`äõ8õ9õa ÒªD©›àbªÝ€bä ©å±œ B@jÀBöÇ@x@ßA$AjàEÀ–ub½áóä fwwajž =ájPš€=â½å ±Ÿà@®™€@á å± àB  BàÁä ƒä–Ç¡ B@JÀBå±@x@¿AAJ¢àEGRváJxxe±£à=S ‚òüy¤ =áˆ3X€{áˆ% ç:"ó¥à@S€@ÿ ÿ â óå±vÿZަàbãá ᨠ£á¨–ǧ B@a¨ÀBñ@x@ßA$A¨¨àEÎ wa¨öÇyzbó© =áj΀=ájä^+cªà@eÌ€@ÿ,ÿB«àJÑË JA#屬 B@aRàB|JeàBöÇ@x@ÇA AR­àEÛ„xaRã„ä F{{aR® =áRzŒ€=áR"½¯à@Û‡€@ö Ç“:C""å#0âS#ûäÿÿQè(E±°àbGÀbA;á'å±± B@jÀBöÇ@x@ßA$Aj²àEb@yáj||aj³ =ájîC€=áj%-ä(+c´à@L @â rå±µàBªB€ƒá J)º ƒä…±¶ B@JÀBå±@x@¿AAJ·àEéû€ˆàËâ µ}}e±¸à=jƒ|öǹ =áˆÙzaÆáˆ% áˆ"óºà@%ý€¼—ñ !ˆ @öÇâ ó屿ÚAë»àb…ü€bᨠbᨅ±¼ B@a¨ÀBëc@x@ßA$A¨½àEo·,ÿÿyÆ~bó¾ =áj£w{ajê%±¿à@v€@ë,cÀàJru J äFöÇÁ B@aRàBÿÿF„@x@ÇA ARÂàE}.|aì¶€€aRà =áR46€=â½ç"½Äà@•1€@å ±£(ð1”P_$Zøæÿÿ (ÞB½ÅàbÀbA;áj屯 B@jÀBå±@x@ßA$AjÇàEê€ëäfajÈ =áj|í€=áj%-ä(%±Éà@Ùì€@á $( @ãÅ"ü@BÊàB@ BàÁéºàB¥±Ë B@JÀBÿÿF„@x@¿AAJÌàEŠ¥}bµàËå ±‚‚e±Íà=¸„Fr3(ìâÿÿBEO AÎ = €Áv«€{áˆ% áˆ"óÏà@Ǧ€@ü yö Çâ ó屌,A¨Ðàb& báᨠ£áë¥±Ñ B@`æÀBëc@x@ßA$A¨ÒàEa~a¨öǃ„bóÓ =áj9!`=å"±Ôà@œ€@å,±ÕàJÀJA#â4` å±Ö B@aRàBå±@x@ÇA AR×àEØ€Óå±……aRØ =áRÊ߀=â½å ±Ùà@+Û€@å ±³Ù  aRàBå±@x@ÇA ARàEb+‡aàˆâ ûaR =áRþ2€=áR%±à@V.€@å ±Ó¨€Ã3Î3ÎØùÿüy´B½àbÂ-€bàá½å± B@jÀBå±@x@ßA$AjàEèæ€ˆëcaj =ájuê€=áj%-ä(+cà@Ïé€@ã Åå± àB1àB$ ƒä–Ç B@JÀB‹@Ùâøëc@x@¿AAJ àEo¢ˆâµ‘‘e± à=ð¤F~ñ =@®€Ág¨€{å"±à@¬£€@ÿ ÿÞ!ˆ%Nå±Kâ óå±°FAëàb  bâ ó!¨ bᨖÇAf@`æÀBÿÿF„@x@ßA$@A ˆ@€Eö]‰a¨ÿÿB’“bó =áj#Š`=âóâó%±à@…€@å,±àJñ J@Ýâ¸ëc B@a àBå±@x@ÇA ARàEÕà ˆå ±””aR =áR¼Ü€=áR%±à@Ø€@å ±ãàýï]`1àÿk1ÿÿ¦ÒWB½àb×€bá jå± B@jÀBÿÿ; @x@ßA$AjàEŠ‹b展•aj =áj”€=ä(â½%±à@a“€@å$±àBÆ’€Bá J$ ƒä…± B@JàB@Ùâøå±@x@¿AAJ àELŒáJ––e±!à=’^„Fñ" =@®€Á R€{áˆ% ç:"ó#à@RM€@ÿ ÿ,â óå±v¥Aë$àb±¡á¨ £á¨…±% B@`æÀB ÿ ÿ„!¨$X @x@ßA$A¨&àE—a¨ëc—˜bó' =áj¸Ç€=ájáj%±(à@Æ€@â m,Î @ñ)àJ†Å J õ⸠`JáR¿ÿL6* B@A*àBÿÿ @x@ÇA AR+àE¥~ŽaRàˆÿÿ ÿ¸l™™aR, =áRR†€=å"±-à@©€@å ±ó€Ë‚aà´äðÿï"[a —ÿô.àb bàáëc/ B@jÀBå±@x@ßA$Aj0àE,:ájššaj1 =áj¨=€=â½%-ä(%±2à@ @å$±3àBd<€ƒá Jå±4 B@JÀBñ@x@¿AAJ5àE²õà ˆâ µ››e±6à=4ƒ|å±7 =ሧû€{âó% áˆ%±8à@ðö€@å ±ë câ óå±3ÿô9àbN bᨠ£ä^¥±: B@a¨ÀB å±@x@ßA$A¨;àE9±bó屜bó< =ájrq‘`=âó%pájK=à@Ôo€@å,±>àJ@ÀJ èjå±? B@aRàBå±@x@ÇA AR@àEG(’aãÇâ ûžžaRA =Àƒ0€=â½áR"½Bà@[+€@ÿÿ +äðï´õåy`•ï0 Œà;BD©CàbÇ*€bá jëcD B@jÀBÿÿB@x@ßA$AjEàEÍ〈ëcŸŸajF =!b ƒYç€=â½å ±Gà@µæ€@á鯹$( @ãÅ"üAJHàB Bá Jå±I B@JÀBå±@x@¿AAJJàETŸ“bµàˆâ µ  e±Kà=Õ¤FñL =áˆD¥€{áˆ% äF"óMà@’ €@—ü y)ø @ñÃâ ó屟°A¨Nàbðá Áë!¨ bá륱O B@a¨ÀBöÇ@x@ßA$A¨PàEÛZ”a¨üy¡¢bóQ =áj•`=ájä^"´Rà@r€@ÿ,ÿ SàJÞ JA#å±T B@aRàBå±@x@ÇA ARUàEèÑ€ˆâû££aRV =áRÙ€=áR"½Wà@øÔ€@å ±à ázäõzõxõy"*Kà0qXàbd bàá½å±Y B@jÀBÿÿ@Ò@x@ßA$AjZàEo–bàˆä f¤¤aj[ =áj‘€=áj%pä(!\à@^€@à~Áå±]àB¿àB$ ƒä‘^ B@JÀBå±@x@¿AAJ_àEöH—áJ¥¥e±`à=w[„Fÿÿôa =áˆÞN€{áˆ% áˆ"óbà@,J€@—å ±!ˆ @å±(¥â óå±”qcàb’I€bá ë!¨ bᨑd B@a¨ÀB”ëc@x@ßA$A¨eàE|˜a¨ëc¦§bóf =áj²Ä€=ê%±gà@À@ë,chàJ JA#å±i B@aRàBå±@x@ÇA ARjàEŠ{™aRàˆä F¨¨aRk =áRGƒ€=â½ç"½là@¢~€@ñ+#r~=}{z‚yÇ@Œà ’D©màb bàáÄ©å±n B@jÀBå±@x@ßA$AjoàE7šajÿÿ_©©ajp =@Ý€ƒ¥:€=áj%-ä(%±qà@ @á $( @ãÅ"ü@BràBa9€ƒè g$àB¥±s B@€ÆÀBå±@x@¿AAJtàE—ò€ˆì®ªªe±uà=ƒ|€ëcv =!? Á‹ø€{áˆ% áˆ"ówà@Õó€@ÿÿ,â óå±­`A¨xàb7 bá ë!¨á륱y B@a¨ÀBëc@x@ßA$A¨zàE®›bóàˆä ^««bó{ =ájÓ²€=å"±|à@2±€@å,±}àJž° JA#å±~ B@aRàB|ÿÿÞ@x@ÇA ARàE¥iœaRâû¬¬aR€ =áRPq€=áR"½à@±l€@ÿÿ +3µºà0â·à´ ÿ;ÙÀ”$©‚àbÀb@%áj屃 B@jÀBÿÿ¦@x@ßA$Aj„àE+%ajàˆâ ½­­aj… =áj°(€=áj%-ä(!†à@  @å n屇àBl'€ƒà B$ ƒåô¥±ˆ B@JÀBÿÿ,@x@¿AAJ‰àE²à€ˆáJ®®e±Šà=º¤Fÿÿ; ‹ =ሞæ€{å"±Œà@îá€@ÿÿ â óå±0óAëàbN báÈ¥!¨å ±Ž B@a¨ÀBÿÿ @x@ßA$A¨àE9œžbóñ¯°bó =ájY\Ÿ`=âóê(f‘à@¼Z€@å,±’àJ(ÀJA#äFöÇ“ B@aRàBÿÿB@x@ÇA AR”àEF aàˆä F±±aR• =áRò€=áRc–à@R€@å ±C´àÔP ‚Æà á¡a ÑîB½—àb¾ b@$áj屘 B@jÀBÿÿB@x@ßA$Aj™àEÍÎà ˆá j²²ajš =ájYÒ€=ä(%-å±›à@·Ñ€@å n屜àB Bá J$ ƒå ± B@JÀBëc@x@¿AAJžàETŠ¡âµ³³e±Ÿà=Ù¤Fÿÿ   =áˆ@€{áˆ% áˆ"ó¡à@‹€@ÿÿôâ óå±uBó¢àbðá å±£ B@a¨ÀBå±@x@ßA$A¨¤àEÛE¢a¨âtâ ó´´bó¥ = ƒ…J€=ájå ±¦à@çH€@å,±§àJS JàÉÁR屨 B@aRàBñ@x@ÇA AR©àEa£aRã„á RµµaRª =áR €=áR"½«à@i€@ñ S@Kc ãÀà‚ða ãÿ!d¬àbÕ€bàáÁjå±­ B@jÀBñ@x@ßA$Aj®àEè¼à ˆá j¶¶aj¯ =ájxÀ€=áj%-ä(+c°à@Ô¿€@â rå±±àB4 Bá Jå±² B@JÀBå±@x@¿AAJ³àEox¤âµ··e±´à=wFƒÇÿÿ5nµ =áˆc~€{áˆå ±¶à@©y€@ÿÿ]Lâ óå±Gÿ'L·àb  bᨠ£çZ–Ǹ B@a¨ÀB öÇ@x@ßA$A¨¹àEõ3¥a¨ÿÿ ¸¹bóº =ájô€=ê+c»à@€ò€@å,±¼àJìñ J éøå±½ B@aRàBå±@x@ÇA AR¾àE«¦aRñººaR¿ = …€ƒ¬²€=â½ç%±Àà@®€@ë ccàpƒð‚btð"ñiuD©Áàb{­ b ájå±Â B@€æÀBÿÿ­@x@ßA$AjÃàEŠf§ajå2å ±»»ajÄ =ájj€=ájå ±Åà@ui€@á 屯àBÖhàBå±Ç B@JÀBå±@x@¿AAJÈàE"¨aJâµ¼¼e±Éà=‘4ƒ|ÿÿ–@Ê =áˆý'€{äF% ç:"óËà@N#€@ñ ÿ ÿ,â óå±͇'Ìàb°¡á ë!¨ £å ±Í B@a¨ÀBüy@x@ßA$A¨ÎàE—Ý€ëüy½¾bóÏ =ájÉ©aæájä^%±Ðà@*œ€@å,±ÑàJ–› JA#äFå±Ò B@aRàBå±@x@ÇA ARÓàE¥Tªa@Y ˆä F¿¿aRÔ =áRf\€=äFáR"½Õà@ÁW€@ÿ 7 @1@˜ # (os‚tÿð%³tE´ëcÿ|„Öàb- bå(ëc× B@jÀBëc@x@ßA$AjØàE+«ajáóá jÀÀajÙ =áj«€=ájå ±Úà@ @à~Áå±ÛàBg€ƒà B$ ƒäŸÿ,Ü B@JÀBå±@x@¿AAJÝàE²Ë€ˆå±ÁÁe±Þà=3Þ„Få±ß =ሮр{áˆ% å±àà@ðÌ€@ â ó @âóå±¹ÿ‚láàbR bᨠbᨋcâ B@a¨ÀB¤ ¤Â.ÿÿB@x@ßA$A¨ãàE9‡¬bóá¨ÂÂbóä =ájÖ‹€=å"±åà@9Š€@å,±æàJ¥‰ J áå±ç B@aRàBöÇ@x@ÇA ARèàE¿B­aRàˆä FÃÃaRé =áRWJ€=áR%±êà@´E€@ÿ ÿÞƒ Óª©ƒ)tRa ¸wHoëàb  bå$±ì B@jÀBÿÿWš@x@ßA$AjíàEFþ ë@Y¡óá jÄÄajî = +€ƒÂ®a¨ä(%-ä()wïà@  @á å±ðàB~€ƒâ,ÃÅå±ñ B@€ÆÀBå±@x@¿AAJòàEÍ¹à ˆá JÅÅe±óà=Ù¤Fîÿÿ; ô = ÀÁ½¿€{áˆ% áˆ"óõà@ »€@ÿ ÿIxë câ óå±…Aëöàbiº€bᨠ£å ±÷ B@`æÀBÿÿ,@x@ßA$A¨øàETu¯bµâtá ¨ÆÆbóù =ájúy€=áj)Ïáj+cúà@\x€@å,±ûàJÈw JA#£½ÿÿ/¼ü B@aRàBëc@x@ÇA ARýàEÚ0°aRäFÇÇaRþ =áR|8€=å"±ÿà@Ö3€@å ±“ÿ%© ºkpée‚pÿÿ›òËB½?à£B bá jñ B@jÀBëc@x@ßA$AjàEaì€ëàˆâ ½ÈÈaj =ájñï€=â½å ±à@M @ã<Ârå±àB­î€ƒàÁÀBå± B@JÀBå±@x@¿AAJàEè§±âµÉÉe±à=ð¤Fôå± =áˆØ­€{áˆ% äF%± à@&©€@—ëcâ óå±oôBó àbˆ¨€bᨠ£å ± B@a¨ÀBüy@x@ßA$A¨ àEnc²a¨ñÊËbó =áj”#³`=ájå ±à@õ!€@å,±àJaÀJA#å± B@aRàB{ÿÿÞ@x@ÇA ARàE|Ú€Óå±ÌÌaR =áR%â€=áR"½à@ˆÝ€@ö Ç£eƒ`.A–ƒ(ïðÂûa Üÿ'àbôÜ b@$ájå± B@jÀBöÇ@x@ßA$AjàE–´bàˆå ±ÍÍaj =áj‹™€=ájå ±à@昀@à~Ånå±àBKàBå± B@JàB@Ùá/lÿÿŠÜ@x@¿AAJàE‰QµáJÎÎe±à=d„Fsÿÿ  =@®€ÁzW€{è¥% å±à@ÄR€@—å ±ë câ óå±xÿ,þ àb% bá ë!¨ £å ±! B@`æÀBëc@x@ßA$A¨"àE ¶a¨üyÏÐbó# =áj>Í€=ájå ±$à@ŸË€@â m!j @`ò# ÿÿ÷%àJ ÀJ (â¸ñ& B@aRàBëc@x@ÇA AR'àE„·aRÿÿ×ÑÑaR( =áRÏ‹€=äF!â½)à@*‡€@ë c³`$Òû à€üð àa É Ho*àb–†€bá jå±+ B@jÀBëc@x@ßA$Aj,àE¤?¸ajçÒÒaj- =áj$C€=ájå ±.à@€B€@á å±/àBàAàB$ ƒä–Ç0 B@JÀBëc@x@¿AAJ1àE+ûà ˆæ üÓÓe±2à=°¤FöÇ3 = ÀÁ¹aÆáˆ% âó4à@eü€¼ñ !ˆ @öÇâ óå±mRBó5àbÇá Ç!¨ bᨖÇ6 B@`æÀBå±@x@ßA$A¨7àE²¶,âóÔÔbó8?áj[»€=å"±9à@¾¹€@å,±:àJ*ÀJA#èjå±; B@aRàBöÇ@x@ÇA ARà@iu€@ü y ÀýU·€þ%¯%· Êàl1B½?àbÕt€bð å±@ B@jÀB‚ù … ÿ,È@x@ßA$AjAàE¿-»ájÖÖajB =áj71€=ä(%-ä(6ÇCà@•0€@á $( @ãÅ4@BDàBû/€Bâ,äàB¥±E B@JÀBÿÿF„@x@¿AAJFàEF逈ä××e±Gà=R·ƒÇêñH =áˆ>ï€{áˆ% áˆ"óIà@†ê€@ÿ ÿôâ óå±ÈÃA¨Jàbæ¡á¨ £á륱K B@a¨ÀB â ó"ô‚ó@x@ßA$A¨LàEͤ¼bóëcØÙbóM =ájíd½`=ä^"´Nà@Oc€@å,±OàJ»b J þå ±P B@aRàByñ@x@ÇA ARQàEÚ¾aâûÚÚaRR =áR’#€=áR"½Sà@ê€@å ±Óÿ1 €I1åóKfÿÿàJsÜB½TàbV bá jå±U B@jÀBÿÿ; @x@ßA$AjVàEa×€ëàˆç ÛÛajW =ájáÚ€=ä(%-ä(%±Xà@? @å$±YàB¡Ù€ƒàÁå±Z B@JàB@Ùèª$ñ@x@¿AAJ[àEè’¿bµîÜÜe±\à=i¥„FÿÿB] =@®€Áà˜€{áˆ% áˆ"ó^à@(”€@ÿÿ/¼â óå±×.!ë_àbˆ“€bá ëëc` B@`æÀBå±@x@ßA$A¨aàEnNÀa¨ñÝÞbób = ƒšÁ`=ájïÂ%±cà@ý €@â mñdàJiÀJ ⸠`‹ç¿ÿ5ne B@aRàBå±@x@ÇA ARfàE|Å€Óÿ ×£Aä FßßaRg =áR4Í€=å"±hà@ŒÈ€@å ± ãó£ðåóTD@£ÿÿ§V7#B½iàbøÇ€bá jå±j B@jÀBå±@x@ßA$AjkàEÂbçààajl =áj„€=ájå ±mà@郀@ã ÅëcnàBKàB$ ƒâµ«co B@JÀB‹@Ù¢oÅôå±@x@¿AAJpàE‰<ÃáJááe±qà= O„Fÿÿ@Òr =@®€Á}B€{äFå ±sà@Ê=€@å ±$F @ñÃâ ó屓ÑBótàb) bâ ó!¨ bᨑu B@`æÀBñ@x@ßA$A¨vàEø€ëëcâãbów =ájD¸Äaæájå ±xà@§¶€@å,±yàJÀJA#â¸å±z B@aRàBå±@x@ÇA AR{àEoÅaäFääaR| =áRÖv€=äFáR(o}à@-r€@å ±óTÝupD{€1äÿþîa €TB½~àb™q€bá jå± B@jÀBÿÿ; @x@ßA$Aj€àE¤*Æájååaj =áj8.€=â½%-éÚ)w‚à@“-€@å$±ƒàBô,€Bá J$ ƒä–Ç„ B@JÀBöÇ@x@¿AAJ…àE+怈âµææe±†à=°¤Fÿÿ; ‡ =áˆ#ì€{áˆ% áˆ"óˆà@lç€@å ±': @å±+cóâóå±$¦Aë‰àbˡᨠbᨅ±Š B@a¨ÀBëc@x@ßA$A¨‹àE²¡Çbóí ççbóŒ =áji¦€=ä^+cà@ʤ€@å,±ŽàJ6 Jã >å± B@aRàBüy@x@ÇA ARàE8]ÈaRâûèèaR‘ =áRâd€=â½å ±’à@<`€@å±,ýീî´a ã\B½“àb¨_€b⠽屔 B@jÀBöÇ@x@ßA$Aj•àE¿Éájééaj– =ájG€=áj%-ä(%±—à@£€@á $( @ãÅ"ü@B˜àBàBå±™ B@JÀBå±@x@¿AAJšàEFÔ€Ëâµêêe±›à=R¢ƒÇõöÇœ =áˆ:Ú€{áˆ% áˆ%±à@„Õ€@öÇâ óå±G®A¨žàbæ¡á ë!¨ £á륱Ÿ B@a¨ÀBëc@x@ßA$A¨ àEÌÊbóöÇëìbó¡ =ájPË`=å"±¢à@cN€@å,±£àJÏM J ëc¤ B@aRàBÿÿÞ@x@ÇA AR¥àEÚÌaâûííaR¦ =áR€€=áR"½§à@Ú €@å ±ëïpE´ÿ`Ô@a ]B½¨àbF bá j屩 B@jÀBÿÿ,@x@ßA$AjªàEa€ëèoîîaj« = …€ƒíÅ€=ä(%pä(%±¬à@I @á å±­àB©Ä€ƒà B$ ƒä‹c® B@€ÆÀBå±@x@¿AAJ¯àEç}Íâµïïe±°à=l„F|ÿÿÞ± =áˆ䃀{âó% áˆ"ó²à@&€@ÿÿôâ óå±ùëAë³àb‡~€bᨠ£á¨‹c´ B@a¨ÀBÿÿô@x@ßA$A¨µàEn9Îa¨å±ðñbó¶ =áj¥ù€=ájê(f·à@ø€@å,±¸àJq÷ JA#å±¹ B@aRàBå±@x@ÇA ARºàE|°ÏaRäFòòaR» =áR2¸€=áR"½¼à@”³€@ÿÿ ,#ïÓ”@ï´uE·a †-B½½àb bì Îå±¾ B@jÀBÿÿ @x@ßA$Aj¿àElÐajçóóajÀ = { ƒŽo€=å"±Áà@ën€@á å±ÂàBJ Bâ µå±Ã B@JÀBå±@x@¿AAJÄàE‰'ÑáJôôe±Åà=:ƒ|屯 =áˆy-€{áˆ% å±Çà@È(€@ÿÿ$Xâ óå±"ßBóÈàb) bᨠ£å ±É B@a¨ÀBÿÿB@xÇ 9A$A¨Ê @€Eã€ëâóõõbóË =áj¥ç€=ájå ±Ìà@æ€@å,±ÍàJtå J@Ýå±Î B@a àBöÇ@x@ÇA ARÏàE–žÒâûööaRÐ =áRC¦€=å"±Ñà@£¡€@å ± 3ð€Ç¬à‚ÇðE±}2ÀáæB½Òàb bä ©å±Ó B@jÀBÿÿ5n@x@ßA$AjÔàEZÓáj÷÷ajÕ =áj±]€=â½*ßä(+cÖà@  @á å±×àBm\€ƒâ µå±Ø B@JÀBå±@x@¿AAJÙàE¤ÔáJøøe±Úà=°¤FóñÛ =ሔ€{áˆ% áˆ(¥Üà@å€@ÿ ÿ5n!ˆ @öÇâ óå±EAëÝàbD bᨠbå ±Þ B@a¨ÀBå±@x@ßA$A¨ßàE+Ñ€ëüyùúbóà =ájO‘Õaæájå ±áà@±€@å,±âàJÀJ å±ã B@aRàBöÇ@x@ÇA ARäàE8HÖadø ˆÿ ÿ,ûûaRå =áRôO€=áR"½æà@TK€@å ±C‚È%©´%±É µå±„B½çàbÀJ b  áßÿžå±è B@jÀB|ÿ ÿ œÿÿ @x@ßA$AjéàE¿×ajèoüüajê = …€ƒK€=ájå ±ëà@¦€@å$±ìàB Bá Jå±í B@€ÆÀBå±@x@¿AAJîàEF¿€Ëÿÿ,Àýýe±ïà=ˤFvñð =áˆ:Å€{áˆ% å±ñà@‹À€@å ±ÿ ÿ,â óå±¼vBóòàbê¡á ë!¨ £å ±ó B@a¨ÀBå±@xñ 9A$A¨ô @€EÌzØbóöÇþÿbóõ =áj;Ù`=ê1öà@c9€@å,±÷àJÏ8 J@ÝäFüyø B@a àBëc@x@ÇA ARùàEÚñà ˆå ±`xAú =áRvù€=â½ç%±ûà@Öô€@å ±S‚Ê%©¶%±Ë ·å±™B½üàbBÀb@Âájå±ý B@jÀBå±@x@ßA$AjþàE`­Úbàˆá j"©Ajÿ =  ƒé°€=â½å ±7ãà@C @à~ƹ)Ú @ãÅ"üAJX&àB¤¯€ƒà B$àB¶Ç …@JÀBå±@x@¿AAJàEçhÛáJ`=à=p{„Fså± =áÓn€{âó% å±à@%j€@å ±!ˆ @ëcâ óå±=ÿF„àbƒi€bᨠbáë¶Ç B@a¨ÀBå±@x@ßA$A¨ àEn$Üá¨Bó =áj )€=âóä^"´ à@j'€@å,± àJÖ& JA#äFå± B@aRàBöÇ@x@ÇA ARàEõ߀ˆçä F60%± =áRŸç€=áR%±à@ã€@å ±c‚Ì%©¸%±Í ¹å±vÿ2zàbmâ bA;ájå± B@jÀBöÇ@x@ßA$AjàE{›Ýâ½+Ê!j =ájÿž€=ä(%-ä(1à@\ @áéå±àB»€ƒá Jå± B@JÀBå±@x@¿AAJàEWÞáJ`=à= %‚ññ =áˆî\€{áˆ% áˆ"óà@=X€@ö Çâ óå±Òÿ8bàbžá Áë!¨ £å ± B@a¨ÀBå±@x@ßA$A¨àE‰ßá¨Bó =áj2€=ájå ± à@•€@å,±!àJÀJ äFå±" B@aRàBëc@x@ÇA AR#àEÎ Ó  ˆå ±AR$ =áR°Õ€=å"±%à@ Ñ€@Œ&À~ÂZ(o!“èos‚Îðÿsƒ(àÿ$ºÿÿtÁÿ&àbxЀbä ©å±' B@jÀB%Jà¥ñ@x@ßA$Aj(àE–‰àâ½ Aj) =áj€=â½å ±*à@yŒ€@á !j @ãÅ(®Eô+àBÚ‹€Bâ,Á­$àB«c, B@JÀBå±@x@¿AAJ-àEEááJ `=.à=%‚óå±/ =áˆK€{áˆ% å±0à@ZF€@ÿÿ â óå±eÜA¨1àb¹á ᨠ£áë«c2 B@a¨ÀBÿÿ @x@ßA$A¨3àE¤âa¨âtä ^ Bó4 =ájC€=áj$áj"´5à@¤€@å,±6àJÀJ ¥(ÁRå±7 B@aRàBëc@x@ÇA AR8àE*¼ Ó@Y ˆá R AR9 =áRÔÀ=áR(o:à@.¿€@è o$F!“å± ƒ‚ä4õƒàþtÏ/õ ‚@¸À—ÿô;àbš¾€bàáÁjå±< B@jÀBÿÿ @x@ßA$Aj=àE±wãb½àˆá j Aj> =áj9{€=áj%-â½+c?à@•z€@à~Á!j @ãÅ"ü@B@àBõyàBå±A B@JÀBå±@x@¿AAJBàE83äáJ`=Cà=@‚õå±D = ÀÁ,9€{áˆ% áˆ"óEà@o4€@ÿ ÿL6ö ǃâóå±3²!ëFàbÔá Áë!¨ £á륱G B@`æÀBå±@x@ßA$A¨HàE¿î€ëÿÿWš5w.WI =ájå®åaæê"´Jà@E­€@å,±KàJ±¬ J ¥(ÁRå±L B@aRàBå±@x@ÇI =ARM @`EÌeæaàˆä F!ã!RN =áR‚mæ`=â½%pâ½Oà@àh€@ë c“õƒî1 à´ßI$£tÿÿF„ŸÐB½PàbL bàáÁjå±Q B@a$àBÿÿ@Ò@x@ßA$AjRàES!ça,àˆá jAjS = …€ƒÛ$€=áj%páj#ÅTà@7 @à~Áå±UàB—#€ƒà B$ ƒ"$`4•«cV B@€ÆÀBå±@x@¿AAJWàEÙÜà ˆá J`=Xà=[ï„FÿÿÞY =áˆÎâ€{áˆ% áˆ%±Zà@Þ€@ÿ ÿB!ˆ @è¥â óå±;"Aë[àbuÝ€bᨠb᨟ÿ \ B@a¨ÀB¡î W.Xÿÿ* @x@ßA$A¨]àE`˜èâó@u$^^ =áj€=å"±_à@p›€@å,±`àJÜš J å±a B@aRàBñ@x@ÇA ARbàEçSéáRARc =áR‹[€=áR"½dà@ãV€@ÿ ÿB £€üàÓð€ý Ô屇e±eàbO bá jå±f B@jÀBÿÿ$X@x@ßA$AjgàEnêaj‡@Y¡óä fAjh =ájö€=ä(%-ä(%±ià@P @à~å±jàB²€ƒá Jå±k B@JÀBå±@x@¿AAJlàEôÊà ˆá J`=mà=ü¤Fñn =áˆéЀ{âó% áˆ"óoà@-Ì€@ÿ ÿBë câ óå±#e±pàbË€bᨠ£å ±q B@a¨ÀBëc@x@ßA$A¨ràE{†ëbóàˆá ¨Bós =áj&‹€=âóê+ctà@‡‰€@å,±uàJóˆ J £½Ã>ëcv B@aRàBëc@x@ÇA ARwàEBìáRARx =áRÀI€=â½áR"½yà@E€@ñ ³%±þ%©Õ%±ÿ Öå±,?D©zàb~D€bá jå±{ B@jÀB‰è oöÇ@x@ßA$Aj|àE‰ý€ˆå2â ½Aj} = …€ƒía¨â½å ±~à@u€@à~Âr)Ú @ãÅ"üAJàBÕÿà ÁÀBå±€ B@€ÆÀBå±@x@¿AAJàE¹€ËáÓá J`=‚à=‡ƒ|ñƒ =ሿ€{áˆ% 屄à@Jº€@ñ !ˆ @ëcâ ó屈ÍA¨…àb«á ᨠbáëi‹c† B@a¨ÀBÿÿô@x@ßA$A¨‡àE–tîbµÿÿ/¼Bóˆ =ájÀ4ï`=ájä^"´‰à@!3€@å,±ŠàJ2 JA#屋 B@aRàBå±@x@ÇA ARŒàE£ëà ˆâ ûAR = …€ƒJó€=å"±Žà@¬î€@ÿ ÿ,ÂÈ%©×ð‚É Øå±“B½àb bä ©å± B@€æÀBÿÿ,@x@ßA$Aj‘àE*§ðbÿÿ Aj’ =áj²ª€=â½%pä(+c“à@ @á $( @ãÅ"ü@B”àBn©€ƒâ µå±• B@JÀBå±@x@¿AAJ–àE±bñaJàˆâ µ `=8ß`€ƒ2u„Fÿÿt˜ =áˆh€{áˆ% áˆ(¥™à@èc€@—ÿ më câ óå±°aA¨šàbM báå±› B@a$ÀBÿÿ,@x@ßA$A¨œàE8òa¨öÇ!"Bó =áj^Þ€=ájå ±žà@¾Ü€@å,±ŸàJ*ÀJA#å±  B@aRàB|ÿÿ; @x@ÇA AR¡àEE•óaRàˆâ û##AR¢ =áR€=áR"½£à@Y˜€@ë cÓ£tˆð£t  öÇñB½¤àbÅ—€bàá½å±¥ B@jÀB†ëc@x@ßA$Aj¦àEÌPôáj$$Aj§ =ájLT€=áj%pä(%±¨à@¨S€@á 屩àBàB屪 B@JÀBå±@x@¿AAJ«àES õaJæ}â µ%%`=¬à=Ô¤FöÇ­ =áˆO€{áˆå ±®à@Ž €@£å ±!ˆ @ëcâ óå±4Aë¯àbïá Âó!¨ bç«c° B@a¨ÀB¨â ó.XöÇ@x@ßA$A¨±àEÙÇ€ëëc&'Bó² =ájˆöaæê(f³à@d†€@å,±´àJÐ… J þå ±µ B@aRàBëc@x@ÇA AR¶àEç>÷aàˆâ û((AR· =áRF€=â½ç%±¸à@ïA€@ë cã+cÎ+[ÝŸÿã1”a :ûB½¹àb[Àb@$ç屺 B@jÀBÿÿyÆ@x@ßA$Aj»àEmú ë@Y ˆá j))Aj¼ =ájòý€=â½å ±½à@N @à~ÃÅ$( @ãÅ"üAJ¾àB²ü€ƒå(ÀB$àB¼y¿ B@JàB@Ù¡ÀB$ÿÿ@Ò@x@¿AAJÀàEôµøâµ**`=Áà=uÈ„FyÿÿB =@®€Áເ{âó% ç:"óÃà@/·€@è ¥ë câ ó属 A¨Äàb¶€bá ë!¨ £á륱ŠB@`æÀBëc@x@ßA$A¨ÆàE{qùa¨ëc+,BóÇ = …`ƒ²1ú`=âóä^"´Èà@0€@å,±ÉàJ~/ J â¸öÇÊ B@€ÎÀBå±@x@ÇA ARËàEˆèà ˆä F--ARÌ =áR7ð€=áR"½Íà@ë€@ü yóPtÞÿÙõƒä1 a ®qÎàbüê€bá jå±Ï B@jÀBÿÿÞ@x@ßA$AjÐàE¤ûbàˆá j..AjÑ =áj—§€=ä(%-ä(+cÒà@ø¦€@à~Å+å±ÓàBWàB$ ƒäŸÿ/¼Ô B@JàB@Ùå±@x@¿AAJÕàE–_üáJ//`=Öà=r„Fëc× =@®€ÁŠe€{áˆ% áˆ"óØà@Ñ`€@ÿ ÿ,â óå± qÙàb2 bá ë!¨ £á¨–ÇÚ B@`æÀBüy@x@ßA$A¨ÛàEýa¨ëc0,WAÜ =ájLÛ€=ä^%±Ýà@«Ù€@â m'%ò;` ÿÿWšÞàJÀJ (â¸å±ß B@aRàBñ@x@ÇA ARààE*’þaRàˆä F22ARá =áRÅ™€=áR"½âà@&•€@ëc-€ë‚ÏàT/Þðôàà£ÿ²ãàb’” b@(ä#å±ä B@jÀBëc@x@ßA$AjåàE±Mÿáj33Ajæ =áj9Q€=ä(%-ä(%±çà@–P€@ë å±èàBõO€Bá J$ ƒä…±é B@JÀBñ@x@¿AAJêàE7 9@Tàˆâ µ44`=ëà=¹¤Få±ì =ሠ€{âó% áˆ"óíà@s €@ëcâ óå±AëîàbÓá Ç!¨ £á¨…±ï B@a¨ÀB”ñ@x@ßA$A¨ðàE¾Ä€ëá¨55Bóñ = …`ƒhÉ€=âó%páj%±òà@ÊÇ€@å,±óàJ6ÀJ äFå±ô B@€ÎÀBzöÇ@x@ÇA ARõàEE€âû66ARö =áRþ‡€=â½áR"½÷à@Uƒ€@ëc-`ãàDð~Ð7a  fB½øàbÁ‚€bá jå±ù B@jÀBÿÿF„@x@ßA$AjúàEÌ;ajå2ä f7 Ó(oû =áj@?€=â½%-`±€3ÒAüà@Ÿ>€@à~ÃÅå±ýàBàB$ ƒä…±þ B@JÀBã Cñ@x@¿AAJÿàER÷€ËáÓá J88`=AN@€ƒZŃÇôÿÿB!Œ ¹ =Gý€{âó% áˆ"óà@ø€@ÿÿôâ ó屨”Aëàbîá Áë!¨ £á¨…± B@a$ÀBëc@x`ß =A¨ @€EÙ²bóëc9:A¤ =ájör`=âóä^%±à@Xq€@â mëcàJÄp J@Ýéøå± B@a àBå±@x@ÇA AR àEæ)aàˆâ û;;AR =áR˜1€=â½â ½ à@ë,€@ÿÿ` @--`<øK -#6Ç àýE­`€äýñ àAÿkn àbW bä ©å± B@jÀBÿÿÚ˜@x@ß =Aj @`Emå€ëáj<<Aj =ájýè€=â½%-ä(+cà@Y @á å±àB¹ç€ƒàÁä ƒä…± B@aàBëc@x@¿AAJàEô âµ==`=à=u³„Fÿÿ›ò =áˆ즀{áˆ% áˆ%±à@*¢€@§ü y$F @âó+câ óå±åÿqVàb¡€bᨠbᨅ± B@a¨ÀB¤ëc@x@ßA$A¨àE{\a¨ÿÿ5n>?Bó =áj´`=ä^%±à@€@å,±àJ JA#å± B@aRàBzëc@x@ÇA !àEˆÓà ˆå ±@@AR =áR=Û€=â½â ½!à@˜Ö€@ë c39‚Èàýaáƒàq#D©"àb bá jå±# B@jÀBÿÿF„@x@ßA$Aj$àE bàˆá jAAg 9% áj“’€=ájáj%±&à@î‘€@à~Ånå±'àBOàB)º ƒä…±( B@JÀBå±@x@¿AAJ)àE–J aJæ}á JBB`=*à=]„Få±+ =ሆP€{áˆáˆ%±,à@ÐK€@ë câ óå±ÕÿWš-àb2 báÁë!¨ £á¨…±. B@a¨ÀBÿÿL6@x@ßA$A¨/àE a¨ëcCDBó0 =ájFÆ€=å"±1à@§Ä€@å,±2àJÀJA#å±3 B@aRàBå±@x@ÇA AR4àE*} aRàˆâ ûEEAR5 =áRß„€=â½å ±6à@:€€@å ±Ctð‚bð"TðC""a ÿ¤D©7àb¦€bàá½å±8 B@jÀBå±@x@ßA$Aj9àE°8 ájFFAj: =áj5<€=â½%-éÚ%±;à@;€@á å±<àBõ:€Bä ƒä…±= B@JàB@Ùâøÿÿ,@x@¿AAJ>àE7ô€ˆàËâ µG!.@=?à=¸¤Fx3(ìàƒ€  ŒK 9@ @®€Á'ú€{âó% áˆ%±Aà@qõ€@î Wë câ óå±[VAëBàbÓá Âóå±C B@`æÀBå±@x@ßA$AjDàE¾¯bóÿÿ,HI FE ájìo`=âóä^+cFà@Mn€@å,±GàJ¹m J ‚*áRñH B@aRàBå±@x@ÇA ARIàEË&2C àˆâ ûJJARJ =áRm.€=áR"½Kà@Ç)€@å ±Sõ‚ä4ƒõƒ"õ‚Œƒàþt@½ÀTUB½Làb3 bàá½å±M B@jÀBå±@x@ßA$AjNàERâ€ëàˆá jKBy 9O ájÚå€=ä(%-ä(%±Pà@6 @å nå±QàB–䀃àÁÁJ$ ƒå ±R B@JÀBëc@x@¿AAJSàEÙâµLL`=Tà=Z°„FëcU =áˆÑ£€{áˆå ±Và@Ÿ€@ÿ ÿ¦!ˆ @ñ4 â óå±ð§AëWàbuž€bᨠbᨋcX B@a¨ÀBñ@x@ßA$A¨YàE`Ya¨å±MNBóZ =áj–`=ä^%±[à@ö€@å,±\àJbÀJA#å±] B@aRàBÿÿÞ@x@ÇA AR^àEmРÓ@Y ˆä FOOAR_ =áRØ€=áR%±`à@yÓ€@å ±c8/ø"å7$M`3$þpSŸÿ+QÀ¦TB½aàbåÒ€bàáÄfå±b B@jÀBå±@x@ßA$AjcàEô‹bàˆá jPPAjd =ájx€=ä(%-å±eà@ÔŽ€@å$±fàB4 BàÁÁJå±g B@JÀBå±@x@¿AAJhàEzGáJQQ`=ià=üY„Få±j =áˆkM€{âó% áˆ"ókà@µH€@ÿ ÿÞ`@å±6Çâ ó屦Bólàb bᨠ£å ±m B@a¨ÀBëc@x@ßA$A¨nàEá¨RRBóo =áj¢€=âóë cpà@€@å,±qàJq JA#¥(Âûëcr B@aRàBy-ÚàBÿÿx[ @x@ÇA ARsàEˆ¾à ˆä FSSARt =áR@Æ€=â½áR"½uà@˜Á€@å ±sÓ”P$¯8 ÓïNp€ßÿ Q`”ÿxvàb bá jå±w B@jÀBÿÿ* @x@ßA$AjxàEzb½å2á jTTAjy = …€ƒ‹}€=â½å ±zà@é|€@à~Âr `@ãÅ(®D{àBK Bá Jå±| B@€ÆàB@Ùâøñ@x@¿AAJ}àE•5aJàˆá JUU`=~à=¤Fÿÿ*  =@®€ÁŠ;€{âó% å±€à@Ò6€@ö Çâ óå±0ÿxàb1 báÁë!¨ £áë«c‚ B@`æÀBÿÿ,@x@ßA$A¨ƒàEñ€ëÿÿL6VWBó„ =ájL±aæâóä^"´…à@¯¯€@â m' @Ä>ÿ ÿ †àJÀJ â¸å±‡ B@aRàByå±@x@ÇA ARˆàE*haãÇâ ûXXAR‰ =áR½o€=áR%±Šà@k€@ÿ ÿ ƒª©{ÀÀx8|}aÀIªDf‹àbŠj€bàá½å±Œ B@jÀBÿÿWš@x@ßA$AjàE°#ajáóá jYYAjŽ =áj@'€=ájå ±à@›&€@á å±àBü%€BàÁÁJ$ ƒä–Ç‘ B@JÀBëc@x@¿AAJ’àE7ßà ˆá JZZ`=“à=¸¤Fÿÿnb” = ÀÁå€{áˆ% 展à@tà€@ÿ ÿL6$F @ëcâ óå±íÿàJ–àbÓá ᨠbᨑ— B@`æÀBÿÿ @x@ßA$A¨˜àE¾šbóñ[\Bó™ =ájâZ`=ç%páj%±šà@EY€@ù 5!j @å±›àJ±X JA#屜 B@aRàBÿÿ…*@x@ÇA ARàEËaã„â û]]ARž =áR€=â½!áR%±Ÿà@Ï€@ù …$F @â½3Ó“ÐÐa€!€&~a ëÇE± àb; bàá½å±¡ B@jÀBŒÿ ÿêìÎ@x@ßA$Aj¢àERÍ€ëàˆá j^^Aj£ =ájÒЀ=áj%-áj6Ǥà@, @à~Á!j @ãÅ"ü@B¥àBŽÏ€ƒà B屦 B@JÀBå±@x@¿AAJ§àEÙˆbµ‹@Y£>á J__`=¨à=Z›„F屩 =áˆÕŽ€{áˆ% áˆ"óªà@Š€@Ÿ\Û ¼áˆ @âóå±O5A¨«àby‰€bä ^!¨ bá륱¬ B@a¨ÀBöÇ@x@ßA$A¨­àE_D a¨ëc`aBó® =áj|!`=å"±¯à@Þ€@ë,c°àJJÀJA#áµëc± B@aRàBëc@x@ÇA AR²àEm» Ó@Y ˆâ ûbbAR³ =áRÀ=â½%pâ½´à@y¾€@ÿÿ@Ò-£}{zy8µäÿU@¸ÀϾB½µàbå½€bàáà å±¶ B@jÀBÿÿÏ4@x@ßA$Aj·àEóv"bàˆá jcCä 9¸ ájtz€=â½%páj%±¹à@Îy€@à~Á屺àB0 Bä ƒä‹c» B@JÀBÿÿx@x@¿AAJ¼àEz2#áJdd`=½à=û¤FöǾ =áˆj8€{âó% áˆ%±¿à@·3€@è ¥)ø @å±Ãâ óå±kLAëÀàb bá ëå±Á B@a¨ÀBöÇ@x@ßA$A¨ÂàEî€ëå±efBóà =áj&®$aæâóä^(fÄà@ˆ¬€@å,±ÅàJô« JA#¥(Ã>屯 B@aRàBå±@x@ÇA ARÇàEe%aàˆä FggARÈ =áR¯l€=áR"½Éà@ h€@å ±³L±`¦ï´ôC"`C"H¨àz¸B½Êàbvg€bàáÁjå±Ë B@jÀBå±@x@ßA$AjÌàE• &ájhhAjÍ =áj%$€=ä(%-ä(%±Îà@€#€@å nå±ÏàBá"€Bá J$ ƒå ±Ð B@JÀBëc@x@¿AAJÑàEÜà ˆâ µii`=Òà=¤FëcÓ = ÀÁ â€{áˆ%páˆ"óÔà@YÝ€@å ±!ˆ @å±(¥â óå±ÞJAëÕàb¸á Âó!¨ bá¨‘Ö B@`æÀBå±@x@ßA$A¨×àE£—'bóñjkBóØ =ájÔW(`=ä^%±Ùà@5V€@å,±ÚàJ¡U JA#å±Û B@aRàBÿÿÞ@x@ÇA ARÜàE°)aˆ@Y ˆâ ûllARÝ =áRU€=áR"½Þà@¨€@”Ex ~ÂZ"½'Eâ½Ã"€„å6$` $þ`$@¸ÀÇB½ßàb bàáÀb`bâ½áŠà B@jàBñ@x@ßA$AjáàE7Ê€ëàˆá jmmAjâ =ájÇÍ€=ä(%-ä(%±ãà@" @å$±äàBƒÌ€ƒàÁÁJ$ ƒáJ«cå B@JÀBå±@x@¿AAJæàE½…*âµnn`=çà=?˜„Få±è =ሶ‹€{áˆ% áˆ"óéà@û†€@ÿ ÿÞë câ óå±cõAëêàbZ bá ë!¨ £á¨…±ë B@a¨ÀBñ@x@ßA$A¨ìàEDA+a¨ëcopBóí =ájv,`=ä^%±îà@×ÿ€~å,±ïàJCÀJA#ä©ëcð B@aRàBå±@x@ÇA ARñàER¸áÛä FqqARò =áRû¿€=áR"½óà@^»€@ü yÓpKË€;S"ŸCÿÿB(B½ôàbʺ b ájå±õ B@jÀBxå±@x@ßA$AjöàEØs-bàˆá jrrAj÷ =áj]w€=áj%-ä(%±øà@¸v€@áéÅn' @ãÅ"ü@BùàB BàÁÀB$àB¥±ú B@JÀBå±@x@¿AAJûàE_/.áJss`=üà=à¤Fñý = ÀÁC5€{å"±þà@•0€@ü y!ˆ%Nè¥â ó屌ÿÌvÿàbûá ᨠbá륱B  `æÀBâ ó9¼ÿÿ; @x@ßA$A¨àEæê€ëëctuBó =áj«/9 máj)ÏâóKà@q©€@å,±àJݨ€Jã >å± B@aRàBå±@x@ÇA ARàEóa0aã„ä FvvAR =áR¡i€=äFáR%±à@ûd€@ñ ã"€€¡Lå" çÁÅ$™àÝzDf àbg bàá½å± B@jÀBÿÿ @x@ßA$Aj àEz1ájwwAj =ájþ €=ä(%-â½! à@Z @â r$( @ãÅK þ BàBº€ƒá Jå± B@JÀBå±@x@¿AAJàEÙà ˆâ µxx`=à=‚ë„Få± =áˆíÞ€{áˆ% áˆ"óà@;Ú€@—î W`@å±6Çâ óå±yˆA¨àbá Âó!¨ £á륱 B@a¨ÀBÿÿ; @x@ßA$A¨àE‡”2bóëcy:™Bó =áj±T3`=ájä^"´à@S€@å,±àJ~R JA#éøå± B@aRàBÿÿB@x@ÇA ARàE• 4aàˆâ û{;îAR =áR:€=áR"½à@€@ë có"ƒàôpQå"0âÁÂa ÿknàb Àb@$ájå± B@jÀByè oÿÿ,@x@ßA$Aj àEÇ€ëå2á j||Aj! =áj¤Ê€=áj%pä(%n"à@ @à~ÃÅå±#àBdÉ€ƒá J$ ƒä’`$ B@JàB á!K ÿ¨¡@x@¿AAJ%àE¢‚5âµ}}`=&à=#•„Fÿÿ¦' =@®€Á‹ˆ€{è¥% áˆ"ó(à@݃€@ë câ ó屪ÿqV)àb> bᨠ£á¨‘* B@`æÀBÿÿÞ@x@ßA$A¨+àE)>6a¨ÿÿ ÿô,&, =ájWþ€=ájå ±-à@¸ü€@â m' @ÿÿÞ.àJ$ÀJA#äFå±/ B@aRàBëc@x@ÇA AR0àE7µ7aRãÇä F€€AR1 =áRä¼€=äF!â½2à@7¸€@ù… @èoK .S"åq0âÑÿÑØa ÙD©3àb£·€bàáÄ©å±4 B@jÀBÿÿWš@x@ßA$Aj5àE½p8ajáóá jAj6 =áj=t€=áj%-áj%±7à@s€@à~Áå±8àBýràBå±9 B@JàB@Ùâøå±@x@¿AAJ:àED,9áJ‚‚`=;à=ɤFå±< =@®€Á<2€{áˆ% áˆ%±=à@-€@ÿ ÿL6$F @âóÃâ óå±4+Aë>àbàá Áë!¨ bå ±? B@`æÀBöÇ@x@ßA$A¨@àEËç€ëñƒ„BóA = ƒý§:aæå"±Bà@^¦€@ÿ,ÿ CàJÊ¥ J ⸠d©áR-ˆ­D B@aRàBå±@x@ÇA AREàEØ^;aãÇä F…… BF =áR~f€=â½%pâ½Gà@Øa€@ÿÿô.±LÁƒà´ Ñÿa èB½HàbD bàá½å±I B@jÀBÿÿF„@x@ßA$AjJàE_<áj††AjK =ájÛ€=ájå ±Là@7 @á å±MàB—€ƒà B$ ƒâµ«cN B@JÀBñ@x@¿AAJOàEæÕ€ˆã>â µ‡ !@=Pà=gè„Fÿÿ@ÒQ =áˆÚÛ€{áˆ% âóRà@×€@ü y!ˆ @å±â óå±LéBóSàb‚Ö€báÂó!¨ bᨋcT B@a¨ÀB î W'ZöÇ@x@ßA$AjUàEl‘=bóñˆ‰BóV =ájŸQ>`=å"±Wà@ÿO€@è '$ŸëcXàJkÀJ þå ±Y B@aRàB{Jeè­ÿÿ¦@x@ÇA ARZàEz?aàˆâ ûŠŠAR[ =áR €=áR(o\à@~ €@öÇ.#‹Y±ÈÁeµda ®ÿŽ]àbê €bàá½å±^ B@jÀB}â ½ñ@x@ßA$Aj_àEÄ?ajãFá j‹‹Aj` =ÀƒÇ€=áj%-ä(+caà@ÝÆ€@à~Á$( @ãÅ4@BbàB=àB$àB¥±c B@JÀB‹@Ùâµ!Kñ@x@¿AAJdàE‡@aJáÓá JŒŒ`=eà=’„Få±f =@®€Áw…€{å"±gà@€€@î W$F%Nå±+câ óå± ÿŽhàb# báÁë!¨ bá륱i B@`æÀBÿÿÞ@x@ßA$A¨jàE;Aa¨å±ŽBók =áj=û€=âó%pâó"´là@ù€@å,±màJ ÀJA#â¸ëcn B@aRàBå±@x@ÇA ARoàE²BaRàˆâ ûARp =áRʹ€=áR%±qà@(µ€@å ±3`6¼ x4|ý{zƒya ÿÞràb“´€bàá½å±s B@jÀB~å±@x@ßA$AjtàE¢mCájAju =áj*q€=ä(%-â½%±và@‡p€@å$±wàBêo€Bá J$ ƒäŽx B@JàB@Ùâøå±@x@¿AAJyàE))DáJ‘‘`=zà=ª¤Fñ{ =@®€Á/€{áˆ% áˆ"ó|à@d*€@œü y': @å±K â ó ›”hv¼ï/GÌï?¿ÿÞ}àbÅá Âó!¨ bᨋc~ B@`æÀBëc@x@ßA$A¨àE°ä€ëñ’“Bó€ =ájã¤Eaæä^%±à@B£€@â m'%òëc‚àJ®¢ J â¸å±ƒ B@aRàBÿÿQè@x@ÇA AR„àE½[Faàˆå ±””AR… =áRlc€=áR"½†à@Å^€@ñ Cþ+b ñ€u6Å@ààÖD©‡àb1 bàá½å±ˆ B@jÀBñ@x@ßA$Aj‰àEDGáj••AjŠ =ájÌ€=ä(%-ä(%±‹à@) @å$±ŒàBˆ€ƒá J$ ƒä…± B@JÀBñ@x@¿AAJŽàEÊÒà ˆâ µ––`=à=Lå„Fëc =ሿ؀{áˆ% áˆ"ó‘à@Ô€@ÿ ÿ* $F @å±+câ óå±¾$Aë’àbgÓ€bá ë!¨ bᨅ±“ B@a¨ÀBå±@x@ßA$A¨”àEQŽHbóëc—˜Bó• =áj„NI`=áj%páj%±–à@äL€@å,±—àJPÀJ 屘 B@aRàBå±@x@ÇA AR™àE_Jaàˆâ û™™ARš =áR €=å"±›à@o€@ÿÿ,.SÓ”@ƒàdup^ a b½œàbÛ b@$ájå± B@jÀBvëc@x@ßA$AjžàEåÀà ˆá jššAjŸ =ájrÄ€=áj%-Á @3ÒCÅ à@ÏÀ@ã Å)Ú @ãÅ"ü@B¡àB- Bá J$àB¥±¢ B@JÀBå±@x@¿AAJ£àEl|Kâµ››`=¤à=í¤F}ÿÿ@Ò¥ =AT€ÁT‚€{äF% áˆ%±¦à@¨}€@ÿÿ,â óå±½h¥§àb bᨠ£á륱¨ B@`æÀBÿÿ,@x@ßA$A¨©àEó7La¨ëcœBóª =ájø€=ájå ±«à@vö€@å,±¬àJâõ JA#äFëc­ B@aRàBå±@x@ÇA AR®àE¯MaRàˆä FžžAR¯ =áR—¶€=áR"½°à@ø±€@å ±c%© `à$Yøæ` ƒZàa æBDf±àbdÀb@$ájå±² B@jÀBÿÿ/¼@x@ßA$Aj³àE‡jNajÿÿtŸŸ=ä´ =ájn€=áj%-ä(+cµà@em€@å$±¶àBÃl€Bá Jå±· B@JÀBå±@x@¿AAJ¸àE&OaJàˆâ µ  `=¹à=8ƒ|屺 =áˆú+€{áˆ% áˆ"ó»à@J'€@å ±ë câ óå±B°Aë¼àbªá ÎW!¨ £å ±½ B@a¨ÀBÿÿ @x@ßA$A¨¾àE”á€ëëc¡¢Bó¿ =áj¬¡Paæè¥å ±Àà@ €@å,±ÁàJ{Ÿ J äFå±Â B@aRàBÿÿ/¼@x@ÇA ARÃàE¢XQaàˆâ û££ARÄ =áRM`€=â½áR"½Åà@®[€@å ±s0âD0áAåyp97ôàóèB½ÆàbÀb@(ájå±Ç B@jÀBå±@x@ßA$AjÈàE)Rajå2á j¤¤AjÉ =áj¹€=ájå ±Êà@ @á  `@ãÅ«cËàBu€ƒà B$ ƒàB«cÌ B@JÀBå±@x@¿AAJÍàE¯Ïà ˆá J¥¥`=Îà=0â„FÿÿÉ‚Ï =ሜՀ{äF% å±Ðà@ìЀ@å ±!ˆ @ñ+câ óå±WBóÑàbK bᨠbá¨‘Ò B@a¨ÀBå±@x@ßA$A¨ÓàE6‹Sbó屦§BóÔ =ájVKT`=ájä^"´Õà@¹I€@å,±ÖàJ%ÀJA#äFå±× B@aRàBå±@x@ÇA ARØàEDUaäF¨¨ARÙ =áRï €=äFáR%±Úà@H€@ö ÇƒÑÆ€ ÿÝ a [·B½Ûàb´€bàáÈoå±Ü B@jÀBƒñ@x@ßA$AjÝàEʽ€ˆáóä f©©AjÞ =ájJÁ€=áj%-ä(+cßà@¨À€@à~Á$( @ãÅ"ü@BààB àBå±á B@JàB@Ùâøüy@x@¿AAJâàEQyVbµàËá Jªª`=ãà=Ò¤Fÿÿ$Xä =@®€ÁI€{áˆ% áˆ"óåà@z€@ÿ ÿÞë câ óå±ÿEA¨.]à£íá Áë!¨ £á륱ç B@`æÀBÿÿ @x@ßA$A¨èàEØ4Wa¨ñ«¬Bóé =ájüô€=å"±êà@_ó€@ÿ,ÿ ëàJËò JA#â¸å±ì B@aRàBå±@x@ÇA ARíàEå«XaRâû­­ARî =áR‰³€=áR"½ïà@鮀@ë c“\w €€'U2$Ša nÿêðàbUÀb@$ájå±ñ B@jÀBëc@x@ßA$AjòàElgYáj®®Ajó =ájðj€=ä(%pä(%±ôà@J @â rå±õàB¬i€ƒá J$ ƒäœyö B@JÀBëc@x@¿AAJ÷àEó"ZaJàËå ±¯¯`=øà=t5ƒ|yñù =áˆÛ(€{áˆ% áˆ"óúà@/$€@ÿÿ,þ @ëcâ óå±Êÿ Òûàb#€báå±ü B@a¨ÀBöÇ@x@ßA$A¨ýàEyÞ€ˆëc°±Bóþ =ájžž[aæä^(fÿà@€@è '*Qÿÿ,CB Blœ J äFå±#…@aRàBÿÿ5n@x@ÇA ARàE‡U\aàˆâ û²²AR =àÆ/]€=áR"½à@X€@å ±£â@äõzT˵zuya Îú@íàbûW b@$å± B@jÀBå±@x@ßA$AjàE]ajãFá j³³Aj =áj’€=ä(%-ä( < à@ì€@à~ÃÅå± àBNàBå± B@JÀBå±@x@¿AAJ àE”Ì€ËáÓá J´´`= à=ß„Fëc =áGxÒ€{áˆ%páˆ"óà@ÍÍ€@å ±$F @å±à â óå±jAëàb0 báÁë!¨ bçZ‘ B@a¨ÀBî W- ÿÿB@x@ßA$A¨àEˆ^bóå±µ¶Bó =ájDH_`=ä^%±à@¦F€@å,±àJÀJ þäFå± B@aRàBy ä‰ÿÿB@x@ÇA ARàE(ÿ Ó DÀEâ û··AR =áRå`á$R"½à@9€@â ½$F â½3Ó³€ 1  €ÑáS ·àYsB½àb¥€bä ©ëc B@jàBâ ½ÿÿB@x@ßA$AjàE¯º€ˆáj¸¸Aj =áj+¾€=ä(å ±à@†½€@á !j @ãÅ"üAJàBç¼€BàÁäàB«c B@JÀBå±@x@¿AAJ!àE6vabwàˆâ µ¹¹`="à=·¤Få±# =áˆ2|€{áˆ% å±$à@sw€@ÿ ÿ¦!ˆ @å±â óå±ýA¨%àbÒá ᨠbá륱& B@a¨ÀBÿÿŠÜ@x@ßA$A¨'àE½1ba¨¼ Z¤¤á¨D½º»Bó( =ájáñ€=áj$áj"´)à@Cð€@å,±*àJ¯ï J ®ÀÉÁRå±+ B@aRàBÿÿ¦@x@ÇA AR,àEʨcaRàˆáRA@¼!sA- =@€ƒ{°€=ç%±.à@Ú«€@ë cÃ"ï"åzuð¤$ùt€5ÿÿ–@¬ÿÞ/àbFÀb Æájå±0 B@€æÀBÿÿ¾@x@ßA$Aj1àEQddáj½½Aj2 =ájÍg€=â½%-â½ <3 à@( @å$±4àBf€ƒá J)º ƒä‘5 B@JàB@˜£ÂÂø&ýÿÿô@x@¿AAJ6àEØeaJàËâ µ¾¾@=7à=Y2ƒ|ÿÿÃÐ8 =@®€ÁÈ%€{áˆ% áˆ"ó9à@!€@—ÿ ÿBë câ óå±ÿÞ:àbt €báÁe!¨ £á¨‹c; B@ ÀBœè ¥!¨ëc@x@ßA$Aj<àE^Û€ˆ$=Áîᨅ±¿ÀBó= =áj‡›faæájå ±>à@陀@â m!j @ñ?àJUÀJ ÈÀÉÁRå±@ B@aRàBå±@x@ÇA ARAàElRgaàˆáRA@ÁÁARB =@€ƒZ€=áR"½Cà@tU€@ü yÓ¨ü}Ÿÿï~"ƒ à­D©DàbàT€bàáÁjå±E B@€æÀB„â ½ëc@x@ßA$AjFàEò hájÂÂAjG = …€ƒo€=ç%-ä( <H à@΀@ã Å)Ú @ãÅ(®@BIàB. Bá J$àB¥±J B@€ÆàB@˜å±@x@¿AAJKàEyÉ€Ëàˆâ µÃÃ`=Là=þ¤FwöÇM =@®€ÁqÏ€{âó% áˆ"óNà@·Ê€@ÿÿQèâ óå±»_A¨Oàb báÂó!¨ £á륱P B@`æÀBüy@x@ßA$A¨QàE…ibóëcÄÅBóR =áj)Ej`=âó%páj"´Sà@‹C€@â må±TàJ÷B JA#â¸å±U B@aRàBñ@x@ÇA ARVàE üà ˆâ ûÆÆARW =áRºkaRâ½áR"½Xà@ÿ€~ñ  ã(ïðå" âS"ÿ/¿a êB½Yàb}þ€bá jå±Z B@jÀB…å±@x@ßA$Aj[àE”·,àˆá jÇÇAj\ =áj$»€=áj%-â½ <] à@º€@à~ÃÅå±^àBà¹àB$ ƒä‹c_ B@JÀBñ@x@¿AAJ`àEslbwàËá JÈÈ`=aà=œ¤Fÿÿ; b =ÀÁ y€{áˆ% áˆ"ócà@Qt€@å ±$F @ñ6Çâ óå±Nÿ‚ldàb·á Áë!¨ bᨋce B@a¨ÀBëc@x@ßA$A¨fàE¢.ma¨å±ÉÊBóg =ájÓî€=å"±hà@4í€@å,±iàJ ì JA# @áRå±j B@aRàBëc@x@ÇA ARkàE¯¥naRàˆâ ûËËARl = …€ƒh­€=â½å ±mà@è€@ë cóÿÃ̱L"x4| ‹àD©nàb/ bàá½`£â½¿ÿL6o B@€æÀBÿÿ@Ò@x@ßA$AjpàE6aoájÌÌAjq =áj¾d€=áj%-ä( <r à@ @á å±sàBzc€ƒà B$ ƒáJ¥±t B@JÀBå±@x@¿AAJuàE¼páJÍÍ`=và=>/ƒ|å±w =áG­"€{äF% áˆ%±xà@ö€@ÿ ÿ$Xâ óå±%ïAëyàbX bᨠ£á¨…±z B@a¨ÀBå±@x@ßA$A¨{àECØ€ëñÎÏBó| =ájm˜qaæájä^+c}à@Ζ€@å,±~àJ:ÀJA#å± B@aRàBå±@x@ÇA AR€àEQOraï+å ±ÐÐAR =áRW€=áR"½‚à@aR€@å±/}"å6$~p!¡Luàµÿ'ƒàbÍQ€bá j屄 B@jÀB€ëc@x@ßA$Aj…àE× sajàˆá jÑÑAj† =ájW€=å"±‡à@³ €@á鯹屈àB BàÁÀB屉 B@JàB@Ùâø2aöÇ@x@¿AAJŠàE^Æ€Ëàˆá JÒÒ`=‹à=ߤFñŒ =@®€ÁNÌ€{áˆ% å±à@˜Ç€@å ±ë câ óå±E±Žàbúá ᨠ£å ± B@`æÀBå±@x@ßA$A¨àEåtbóëcÓÔBó‘ =ájBu`=ä^%±’à@t@€@‡@Sâm!j%òöÇ“àJà? JA#àJëc” B@aRàBå±@x@ÇA AR•àEòø€ˆâûÕÕAR– = …€ƒ¤vá$R%±—à@þû€~å ± väõw"xö|‚}VÎ Œàò«B½˜àbj bàá½å±™ B@€æÀBëc@x@ßA$AjšàEy´,àˆä fÖÖAj› =áj¸€=ä(*ßä( <œ à@a·€@â rå±àBÁ¶€BàÁÁJ `ƒä‘ž B@JÀBëc@x@¿AAJŸàEpwbwàËá J××`= à=‚„Fëc¡ = s Áðu€{áˆ% áˆ"ó¢à@:q€@ÿ ÿ5n!ˆ @ñ(¥â óå±VYAë£àbœ¡á¨ bᨋc¤ B@a¨ÀBå±@x@ßA$A¨¥àE†+xa¨ëcØÙBó¦ =áj©ë€=å"±§à@ ê€@è 屨àJué JA#屩 B@aRàBÿÿ5n@x@ÇA ARªàE”¢yaRàˆâ ûÚÚAR« =áRFª€=áR"½¬à@ ¥€@ÿ 7"½'â½#y¯~1ññ¾ÿÿ›ò'ÿB­àb  bàáÄfå±® B@jÀBå±@x@ßA$Aj¯àE^zajäfÛÛAj° = …€ƒ«a€=ä(å ±±à@ @à~Á!j @ãÅ(®D²àBg`€ƒà Bå±³ B@€ÆÀBå±² 5@¿AAJ´ @€E¡{aJáÓâ µÜÜ`=µà=",ƒ|ÿÿ/¼¶ =ሑ€{áˆ% å±·à@Ü€@ÿÿ/¼â ó屃ÿB¸àb= báÁë!¨ £á륱¹ B@abÀBå±@x@ßA$A¨ºàE(Õ€ëå±ÝÞBó» =ájR•|aæáj$áj"´¼à@³“€@å,±½àJÀJA#å±¾ B@aRàBå±@x@ÇA AR¿àE5L}aŒ@Y ˆâ ûßßARÀ =áRèS€=å"±Áà@FO€@ÿÿ; /3ñ¯ñ9€Ùƒ.íð7ËõÿÿÔæãhDfÂàb²N€bä få±Ã B@jÀBÿÿ@Ò@x@ßA$AjÄàE¼~ajáóá jààAjÅ =ájD €=â½%pâ½ <Æ à@¡ €@á å±ÇàB BàÁä ƒä‹cÈ B@JÀBå±@x@¿AAJÉàECÀËàˆá Jáá`=Êà=ĤFå±Ë = ¹ Á3É€{áˆ% áˆ(¥Ìà@~Ä€@—è ¥ @è¥â óå±GšAëÍàbßá ᨠbᨋcÎ B@a¨ÀBå±@x@ßA$A¨ÏàEÊ~bóñâãBóÐ =ájø>€`=ájå ±Ñà@X=€@å,±ÒàJÄ< JA#¥(ÁRñÓ B@aRàB| àBÿÿB@x@ÇA ARÔàE×õà ˆâ ûääARÕ =áRjý€=å"±Öà@Çø€@å ±CŽƒ£àTÿ=­ƒ,ñ ýàóe±×àb3 bá j屨 B@jÀBÿÿ@Ò@x@ßA$AjÙàE^±bàˆá jååAjÚ =ájâ´€=â½%-ä( <Û à@? @å$±ÜàBž³€ƒàÁµ)º ƒä…±Ý B@JÀBå±@x@¿AAJÞàEål‚aJàËá Jææ`=ßà=f„Få±à =áGÙr€{áˆ% áˆ%±áà@ n€@ë câ óå±We±âàbm€báᨠ£á¨…±ã B@a¨ÀB ù »8pÿÿ @x@ßA$A¨äàEk(ƒa¨ëcçèBóå = ƒšè€=ä^+cæà@úæ€@å,±çàJfÀJ ånå±è B@aRàBëc@x@ÇA ARéàEyŸ„aRàˆâ ûééARê =áR+§€=â½â ½ëà@¢€@ÿÿB/S¶ûƒ/Q/£ëð0‘a ïÂD©ìàbù¡ b@$æÙå±í B@jÀBÿÿ²º@x@ßA$AjîàEÿZ…ájêêAjï =áj^€=â½%-ä( <ð à@ì]€@á ëcñàBL Bä ƒä…±ò B@JÀBÿÿL6@x@¿AAJóàE††áJëë`=ôà=)ƒÇÿÿ; õ =áGr€{âó% áˆ%±öà@€@ÿÿ/¼â óå±K0Aë÷àb" bá ë'Z £á¨…±ø B@a¨ÀBÿÿ/¼@x@ßA$A¨ùàE Ò€ëëcìíBóú =áj4’‡aæâóä^%±ûà@”€@å,±üàJÀJA#äFå±ý B@aRàBå±@x@ÇA ARþàEIˆaàˆå ±îîARÿ = { ƒ¹P€=â½áR"½Dà@L€@ë c cƒ2àTD¨ÿÃ%»ëcÁcB½àb‚K€bàáÈoå± B@jÀBè oÿÿB@x@ßA$AjàE¡‰ájïïAj =áj9€=áj%-ä(!§à@–€@á å±àBõàB$ ƒä…± B@JÀBëc@x@¿AAJàE(Àà ˆâ µðð`= à=©¤Få± =áˆÆ€{áˆ% áˆ"ó à@cÁ€@ÿ ÿ,â óå±e‘Aë àbÄá Âó!¨ £á¨…± B@a¨ÀBñ@x@ßA$A¨àE¯{Šbóçãá ¨ñ#ÇA =ájA€€=å"±à@£~€@‡@SåfüyàJÀJA#àJå± B@aRàBÿÿB@x@ÇA ARàE57‹aRãÇá Ròò!R =@þ€ƒâ>€=áR"½à@=:€@å ±s¶ýòÒ‘Ò"°àöÇ,\B½àb©9€bàá½å± B@€æÀBÿÿÞ@x@ßA$AjàE¼òà ˆá jóóAj =ájDö€=ä(%-ä(%±à@Ÿõ€@â rå±àB Bá J$ ƒä…± B@JÀBå±@x@¿AAJàEC®Œâµôô`=à=K|ƒÇÿÿ–@ = ¹ Á3´€{áˆ% áˆ"ó à@€¯€@ÿ ÿ,â ó屈®Aë!àbßá Âó!¨ £á¨…±" B@a¨ÀBå±@x@ßA$A¨#àEÉia¨ñõöBó$ = ƒò)Ž`=ájê+c%à@T(€@ë,c&àJÀ' J å ±' B@aRàBÿÿÞ@x@ÇA AR(àE×àà ˆä F÷÷AR) = …€ƒˆè€=å"±*à@Ûã€@ÿÿô /ƒçðäÿt±/õ‚ÿɃàþ Œà¾ÓB½+àbG bá jå±, B@€æàBëc@x@ßA$Aj-àE^œbå2á jøøAj. =áj=â½%-ä(%±/à@I @à~ÃÅå±0àBªž€ƒá J$ ƒä…±1 B@JÀBå±@x@¿AAJ2àEäWaJàˆá Jùù`=3à=ej„FÿÿÞ4 =áˆÝ]€{âó% áˆ%±5à@"Y€@ÿÿô/â óå±!Aë6àb€X€báÁë!¨ £á¨XF7 B@a¨ÀBÿÿ¦@x@ßA$A¨8àEk‘á¨úúBó9 = …`ƒ€=âóå ±:à@g€@å,±;àJÓ J å ±< B@€ÎÀBüy@x@ÇA AR=àEòÎà ˆâ ûûûAR> =áR°Ö€=áR"½?à@Ò€@å ±“tèe­‚õƒîðï´ÿÿ]LàŠB½@àbrÑ€bá jå±A B@jÀB†å±@x@ßA$AjBàExŠ’b½Œ@Y­Wá jüü €C =ájŽ€=ä(%-ä(%±Dà@a€@á å±EàBÁŒ€Bå ô$ ƒä…±F B@JÀBöÇ@x@¿AAJGàEÿE“áJýý`=Hà=‚úw!^¡ˆ€ ÿÿ²º`µAˆI = ) ÁûK€{H à=`=e áˆ"óJà@?G€@å ±!ˆ%Nÿÿ,â óå±DxAëKàbŸF€bá ë!¨ bᨅ±L B@a¨ÀBå±@x@ßA$A¨MàE†”a¨ÿÿ* þÿAjN =áj¹Á€=âóå ±Oà@À€@å,±PàJ…¿ JA#aì!î¡öñQ B@aRàBöÇ@x@ÇA ARRàE“x•aRàˆä F pARS =áRN€€=â½áR"½Tà@£{€@”ÿ ÿê @â½£ãÿ † ñ¯ñ9á~Ða ;ÿ¯ÆUàb bàáÁjå±V B@jÀBÿÿ5n@x@ßA$AjWàE4–áj AjX =áj¢7€=áj%-`±€3ÒCÅYà@ÿ6€@á  `@ãÅ.`@BZàB^àB$ ƒàB¥±[ B@JÀBëc@x@¿AAJ\àE¡ïà Ëâ µ@=]à=&ƒ|yÿÿ* ^ =ሙõ€{áˆ% áˆ"ó_à@Üð€@ÿÿF„â ó屟ÿµ®`àb= bá ë!¨ £á¨…±a B@a¨ÀBå±@x@ßA$AjbàE(«—bóüyBóc =ájOk˜`=å"±dà@®i€@å,±eàJÀJA#å±f B@aRàBëc@x@ÇA ARgàE5"™aàˆâ ûaRh =áRÔ)€=â½%pâ½ià@)%€@å ± ³}"àþ£àÿ£à6Ȭàa =Dfjàb•$€bàáå±k B@jÀBå±@x@ßA$AjlàE¼Ýà ˆá jajm =áj<á€=áj%-áj+cnà@™à€@á å±oàBøßàB$ ƒä‹cp B@JÀBå±@x@¿AAJqàEB™šâµe±rà=ĤFüys =áˆ7Ÿ€{áˆ% áˆ%±tà@zš€@å ±!ˆ @è¥â óå±¥ÏAëuàbÞá Âó!¨ bᨅ±v B@a¨ÀB¡î W?nÿÿÞ@x@ßA$A¨wàEÉT›a¨öÇ bóx =ájÙœ`=å"±yà@<€@å,±zàJ¨ J þå ±{ B@aRàBxÿÿ @x@ÇA AR|àE×Ë€ˆîèä F  aR} =áRŽÓ€=áR"½~à@ß΀@ù…%òâ½ÃÐð£t  ð£a  ‰B½àbK bá jëc€ B@jÀBŠâ ½+ @'@x@ßA$AjàE]‡bàˆá j  aj‚ =áj劀=ä(%-ä(%±ƒà@C @áéÃÅ$( @ãÅ«c„àB¡‰€ƒàÁÀB$àB¥±… B@A"àBå±@x@¿AAJ†àEäBžáJ  @=‡à=eU„F屈 =áˆàH€{áˆ% áˆ"ó‰à@ D€@¤î W`@âó+câ ó屨{AëŠàb€C€bᨠ£á륱‹ B@a¨ÀB¨â ó"ôå±@x@ßA$AjŒàEkþ€ˆëc Kc = ``ƒŸ¾Ÿaæä^"´Žà@½€@å,±àJn¼€Jã >ñ B@€ÎÀBå±@x@ÇA AR‘àExu aáÛä FaR’ =áR0}€=áR"½“à@ˆx€@ÿ ÿ¦ÓðQ "$h\{‚"$a ¡ÊB½”àbôw€bàáÄf展 B@jÀBÿÿWš@x@ßA$Aj–àEÿ0¡ájaj— =áj4€=ä(%-ä(%±˜à@Ý3€@â rå±™àBC Bá J屚 B@JÀB„@Ùî\$†ü@x@¿AAJ›àE†ì€Ëàˆâ µe±œà=ƒÿ„Fù2(ìàƒöÇEO A =@®€Ázò€{áˆ% áˆ"óžà@Âí€@ÿ ÿÞ!ˆ @å±?mâ óå±8AëŸàb" báÂó!¨ bå ±  B@`æÀB å±@x@ßA$A¨¡àE ¨¢bóëcbó¢ =ájâ µ  @=Ûà=ì¤F~ÿÿ,Ü =áˆWï€{áˆ% áˆ"óÝà@¨ê€@å ±)ø @ÂÓ#!8Ãâ óå±ú–AëÞàb báçZ bå ±ß B@a¨ÀBÿÿ¦@x@ßA$AjààEñ¤­bóëc!"bóá =áje®`=ê%±âà@|c€@ë,cãàJèb J äFëcä B@aRàBå±@x@ÇA ARåàEÿ¯aàˆâ û##aRæ =áR»#€=â½ç"½çà@€@” Ùá$F @â½%±€)åLp+åMTpþ€ 8àîáB½èàb{€bàá½`b⽿ÿF„é B@jàBó Óüy@x@ßA$AjêàE†×€ˆãFá j$$N!ë =ájÛ€=â½å ±ìà@\Ú€@á !j @á"üAJíàBÂÙ€BàÁÁJå±î B@JàB@Ùáð!KöÇ@x@¿AAJïàE “°âµ%%e±ðà=¥„Fÿÿ@Òñ =@®€Á™€{âó% å±òà@I”€@ÿÿ,â óå±JA¨óàb¨á ᨠ£áë«cô B@`æÀBÿÿÞ@x@ßA$A¨õàE“N±a¨å±&'bóö =ájȲ`=âó$áj"´÷à@* €@å,±øàJ–  JA#â¸å±ù B@aRàBå±@x@ÇA ARúàE Åà ˆä F((aRû =áRMÍ€=â½áR%±üà@©È€@öÇ0#KYåM0çTÃeêàr’B½ýàb bá jå±þ B@jÀBÿÿ; @x@ßA$AjÿàE'³bàˆá j))ajEáj¯„€=áj%pâ½+cà@  @à~ÃÅå±àBkƒ€ƒà Bå± B@JÀBŒBDâøå±`x@9 AJ @€E®<´áJ**e±à=/O„Fÿÿô =@®€{žB€{áˆ% áˆ"óà@ë=€@ÿ ÿ ë câ óå±Ö`AëàbJ bᨠ£å ± B@`æÀBöÇ@x@ßA$A¨ àE5ø€ëñ+,bó =ájV¸µaæå"± à@·¶€@å,± àJ#ÀJA#â¸å± B@aRàBÿÿ¦@x@ÇA ARàEBo¶aàˆä F--aR =áR÷v€=áR"½à@Rr€@ÿÿ,03P¯MN1ïð~aa kÿÉ‚àb¾q€bàáÄ©å± B@jÀBÿÿ$X@x@ßA$AjàEÉ*·áj..aj =ájY.€=ä(å ±à@´-€@‡ ârå±àB Bá Jå± B@JÀBå±@x@¿AAJàEOæ€Ëàˆâ µ//e±à=ѤFöÇ =áˆDì€{áˆ% å±à@ç€@ÿÿ,â óå±ÏLE±àbì¡á ë!¨ £å ± B@a¨ÀBÿÿ,@x@ßA$A¨àEÖ¡¸bóëc01bó =ájôa¹`=ä^+c!à@U`€@å,±"àJÁ_ J ã>å±# B@aRàBå±@x@ÇA AR$àEäºaàˆâ û22Oª% =Àƒy €=áR%±&à@Ü€@ö ÇC%Ç´tð"Q¹@ï Œàu+B½'àbHÀb@&ájå±( B@jÀBÿÿ¾@x@ßA$Aj)àEjÔ ë@Y ˆá j33aj* =ájó×€=ájå ±+à@N @ã Å `@ãÅ"üAJ,àB®Ö€ƒàÁÆü$ ƒàB¼y- B@JÀBå±@x@¿AAJ.àEñ»âµ44e±/à=r¢„Fëc0 =áˆÙ•€{å"±1à@+‘€@n¢ÁEë câ óå±Ñÿ¯ü2àb€bá¨å ±3 B@a¨ÀB”ô - ÿÿ @x@ßA$A¨4àExK¼a¨ëc56bó5 =áj¥ ½`=ájïÂ"´6à@ €@å,±7àJs €Jã >å±8 B@aRàBå±@x@ÇA AR9àE…€ˆáÛä F77aR: =áR3Ê€=äFáR%±;à@•Å€@å ±SÓ”Pí ç Qy àŸÿ^ˆ ³ÉDf<àbÀb@$ã å±= B@jÀByâ ½öÇ@x@ßA$Aj>àE ~¾bàˆá j88G? =áj€=â½%-éÚ1@à@쀀@â rå±AàBL BàÁÄkå±B B@JÀBå±@x@¿AAJCàE“9¿áJ99e±Dà=L„Få±E =áˆ{?€{áˆ% áˆ"óFà@Í:€@ü y!ˆ @ù»â óå±;AëGàb/ bᨠbçZŸÿ,H B@a¨ÀBå±@x@ßA$A¨IàEõ€ëëc:;bóJ =áj+µÀaæájä^%±Kà@Œ³€@å,±LàJø² JA#äFå±M B@aRàBÿÿ$X@x@ÇA ARNàE'lÁaàˆä F<>e±Yà=µõ„FÿÿF„Z =áˆ%é€{âó% å±[à@oä€@ÿÿF„â óå± A¨\àbÐá Áë!¨ £á륱] B@a¨ÀBöÇ@x@ßA$A¨^àE»žÃbóå±?@bó_ =ájÑ^Ä`=âóå ±`à@2]€@å,±aàJž\ J å±b B@aRàBå±@x@ÇA ARcàEÉÅaãÇâ ûAAaRd =áRr€=áR(oeà@Å€@ù …$F!“èosuð¤$´ÿåð4Ç‚õƒààÂßB½fàb1 bàá½å±g B@jÀBÿÿ¾@x@ßA$AjhàEOÑ€ëáóá jBBaji =ájÛÔ€=áj%pä(+cjà@7 @à~Á!j @ãÅ¥±kàB—Ó€ƒà B$àB±l B@JÀBå±@x@¿AAJmàEÖŒÆbµàˆá JCCe±nà=[Ÿ„Fÿÿ­o =áˆÎ’€{áˆ% áˆ"ópà@Ž€@ÿÿ/¼â óå±f-Aëqàbr€báÁë!¨ £á륱r B@ ÀBÿÿ,@x@ßA$A¨sàE]HÇa¨ñDEbót =ájÈ`=ç$áj"´uà@ð€@ÿ ÿš2€ @ ì@ Ã! !% … ARvàJ\ÀJ å±w B@aRàBñ@x@ÇA ARxàEj¿€ÓáÛâ ûFFaRy =áRÇ€=â½!áR"½zà@r€@ë cƒ"­+YÿpǬàDÿÿxÿ,{àbÞÁ€bàá½å±| B@jÀBÿÿ­@x@ßA$Aj}àEñzÉbàˆá jGGaj~ =áje~€=áj%-áj%±à@Á}€@à~Áå±€àB!àB$ ƒäŸÿB B@JÀBå±@x@¿AAJ‚àEx6ÊaJã>á JHHe±ƒà=ý¤F屄 =áˆl<€{áˆ% áˆ"ó…à@®7€@ÿ ÿÞ$F @è¥+câ óå±°ÿ,†àb báÁë!¨ bᨑ‡ B@a¨ÀB ô 4 öÇ@x@ßA$A¨ˆàEþñ€ëëcIJT ‰ =áj!²Ëaæå"±Šà@…°€@ë,c‹àJñ¯ J þå ±Œ B@aRàBw&àBÿÿWš@x@ÇA ARàE iÌaàˆâ ûKKARŽ =áR¶p€=â½å ±à@l€@ö Ç“ÇÞE¨Ç­€ ýàcqàb„k b@&ç屑 B@jÀBÿÿQè@x@ßA$Aj’àE“$ÍajãFá jLLaj“ =áj(€=â½%-ä(%±”à@{'€@á 展àBÛ&€BàÁä ƒä…±– B@JÀBÿÿ; @x@¿AAJ—àEà€ˆàËá JMM@=˜à=šòÊaˆÿÿ* ™ =áˆæ€{âó% áˆ%±šà@Tá€@ü y!ˆ @å±+câ óå±Çq›àbµá ᨠbᨅ±œ B@a¨ÀBÿÿB@x@ßA$AjàE ›Îajå±NObóž =áj×[Ï`=âóä^+cŸà@7Z€@å,± àJ£Y J äFöÇ¡ B@aRàB{å±@x@ÇA AR¢àE­Ðaàˆâ ûPPaR£ =áRL€=áR"½¤à@®€@å ±£ßE¨"ï\€Tí ç&a Ëÿô¥àb bä f屦 B@jÀBüy@x@ßA$Aj§àE4Πë@Y¡óá jQQaj¨ =áj¼Ñ€=ä(%-ä(%±©à@ @á $( @ãÅ"ü@BªàB|ЀƒàÁäàB¥±« B@JàB@Ù¡ÀB)ºßÿ* @x@¿AAJ¬àE»‰ÑâµRRe±­à=<œ„Fëc® =@®€Á§€{áˆ% áˆ"ó¯à@öŠ€@å ±ë câ óå±oÿú°àbW bᨠ£á륱± B@`æÀBå±@x@ßA$A¨²àEBEÒa¨âtâ óSSbó³ =ájÛI€=ájå ±´à@>H€@â m!j @ñµàJªG JA#â¸å±¶ B@aRàBÿÿB@x@ÇA AR·àEÈÓaRàˆá RTTaR¸ =áR€=å"±¹à@Ø€@ñ ³+c ßÀƒÀ‚1€Ã3ØüOa ¢fDfºàbD bàáÄ#å±» B@jÀBÿÿŠÜ@x@ßA$Aj¼àEO¼€ëàˆá jUULν =ájÏ¿€=â½%-ä(%±¾à@* @ã<Á屿àB‹¾€ƒàÁÀB/l ƒä‹cÀ B@JÀBñ@x@¿AAJÁàEÖwÔâµVVe±Âà=Þ¤FóÿÿF„à =áˆÊ}€{áˆ% áˆ%±Äà@y€@ü y!ˆ @ëcâ óå±”AëÅàbrx€bᨠbᨋcÆ B@a¨ÀB ñ@x@ßA$A¨ÇàE\3Õa¨ëcWXbóÈ =ájó€=áj%páj(fÉà@ãñ€@å,±ÊàJOÀJ å±Ë B@aRàBöÇ@x@ÇA ARÌàEjªÖaRàˆä FYYDFÍ =áR#²€=å"±Îà@z­€@å ± ÃЂЃðíQyTþ1×a ²B½Ïàb欀bàáÄfå±Ð B@jÀBå±@x@ßA$AjÑàEñe×ájZZajÒ =ájyi€=â½%-â½%±Óà@Ôh€@å$±ÔàB9 Bá Jå±Õ B@JàB@Ùâøëc@x@¿AAJÖàEw!ØáJ[[e±×à=ù¤Fÿÿ–@Ø =@®€Áh'€{âó% áˆ%±Ùà@µ"€@— òá)ø @å±6Çâ óå±AëÚàb bᨠbå ±Û B@`æÀBÿÿ,@x@ßA$A¨ÜàEþÜ€ëå±\]bóÝ =áj#Ùaæâóå ±Þà@…›€@â m!j @ëcßàJñš JA#áµëcà B@aRàBå±@x@ÇA ARáàE TÚaãÇå ±^^aRâ =áR´[€=áR"½ãà@ W€@å ±Óíð$ ð"<|ý6Ñ+ma z™B½äàbxV€bá jå±å B@jÀBå±@x@ßA$AjæàE’Ûajÿÿ* __ajç =áj€=ájå ±èà@y€@á  `@ãÅ(®AJéàBÚàBå±ê B@JÀBëc@x@¿AAJëàEËà ˆâ µ``@=ìà=šÝ„Fÿÿ* í =ሠр{áˆ% å±îà@WÌ€@ë câ óå±ÞkA¨ïàbµá È¥!¨ £áë«cð B@a¨ÀBëc@x@ßA$AjñàE †Übóÿÿ,abbóò =ájÕFÝ`=ç%páj"´óà@7E€@ÿ,ÿ,ôàJ£D J å±õ B@aRàBñ@x@ÇA ARöàE­ý€ˆãÇâ ûccaR÷ =áRVÞaRâ½áR%±øà@µ€@ÿ ÿ$Xãñ‹ma ÏB½ùàb! bàá½å±ú B@jÀB}î !ÿÿB@x@ßA$AjûàE4¹€ëàˆá jddLÎü =áj¨¼€=ájå ±ýà@ @à~Áå±þàBd»€ƒà B$ ƒä‘ÿ B@JÀBå±@x@¿AAJF  €E»tßbwã>á Jeee±à=<‡„FÿÿB =ሣz€{áˆ% äF"óà@ðu€@ÿ ÿ Ò)ø @ëcâ óå±kçBóàbW bᨠbᨑ B@abÀBÿÿB@x@ßA$A¨àEA0àa¨ëcfgbó =ájkð€=å"±à@Ìî€@ë,c àJ8ÀJA#å± B@aRàBëc@x@ÇA AR àEO§áaRàˆâ ûhhaR =áR¯€=â½å ± à@_ª€@å ±ós1 í$6Ï× 0ÿÿÞ•B½àbË©€bàáÄ©å± B@jÀBÿÿB@x@ßA$AjàEÖbâajäfiiaj =ájbf€=áj%-ä(1à@½e€@á å±àB BàÁÁJå± B@JÀBå±@x@¿AAJàE\ãaJàËâ µjje±à=ݤFÿÿB =áˆL$€{áˆ% áˆ%±à@–€@ÿ ÿ$Xâ óå±1oAëàbøá ᨠ£å ± B@a¨ÀBñ@x@ßA$A¨àEãÙ€ëå±klbó =ájšäaæå"±à@b˜€@å,±àJΗ JA#å± B@aRàBå±@x@ÇA AR àEðPåaàˆâ ûmmIø! =áR¦X€=áR"½"à@T€@å±1"àÿt¨"tQ/ø¦Kÿÿ$Xé‚B½#àbmS€bä få±$ B@ àBÿÿ* @x@ßA$Aj%àEw æájnnAj& =áj÷€=ä(å ±'à@S @á å±(àB·€ƒâ,ä ƒä‹c) B@JàB@Ùâø,¯ ÿS3@x@¿AAJ*àEþÇà ˆâ µooe±+à=Ú„Fÿÿ5n, =@®€ÁîÍ€{áˆ%  Ê@@3bó-à@8É€@ëcâ ó›”hvßÿàJMpBó.àbšÈ€bᨠ£á¨‹c/ B@`æÀBå±@x@ßA$A¨0àE…ƒçbóñpqbó1 =A6`ƒ·Cè`=ä^12à@B€@â m,Î*QöÇ3àJƒA JA#â¸öÇ4 B@€ÎÀB{ÿÿÞ@x@ÇA AR5àE’úà ˆâ ûrraR6 =áRLéá$R%±7à@¦ý€~å ±$ÿ„1çƒAàð"QÅa 7¥B½8àb bá jå±9 B@jÀBëc@x@ßA$Aj:àE¶,àˆá jssaj; =áj¹€=ä(%-ä(+c<à@ù¸€@å$±=àBY BàÁå±> B@JÀBëc@x@¿AAJ?àE qêbwàËá Jtte±@à=!„„FñA =ÀÁw€{áˆ% áˆ"óBà@Úr€@å ±': @ñ(¥â ó屓WAëCàb< báᨠbå ±D B@a¨ÀBå±@x@ßA$A¨EàE&-ëa¨ëcuvH¥F =ájPí€=áj%páj%±Gà@±ë€@å,±HàJÀJA#å±I B@aRàBå±@x@ÇA ARJàE4¤ìaRàˆâ ûwwaRK =áRÞ«€=ç"½Là@@§€@ÿÿ 1#P;åP0å6åM`dp.åa ƒÑB½Màb¬¦ b@$ájå±N B@jÀByöÇ@x@ßA$AjOàEº_íájxxajP = …€ƒCc€=ájâ½%±Qà@Ÿb€@ã Å$( @ãÅ"ü@BRàBÿaàBñS B@€ÆÀBÿÿB@x@¿AAJTàEAîáJyye±Uà=¤FñV =áˆ)!€{äF% äF"óWà@|€@ÿ ÿ* )ø @å±+câ óå±'#A¨XàbÝá å±Y B@a¨ÀBöÇ@x@ßA$A¨ZàEÈÖ€ëëcz{!Õ[ =ájú–ïaæájå ±\à@[•€@å,±]àJÇ” JA#äFëc^ B@aRàBå±@x@ÇA AR_àEÕMðaàˆå ±||aR` =áRˆU€=äFáR"½aà@áP€@ÿÿ,13Ke© &åHTÿ¿äàæ£B½bàbM bàá½å±c B@jÀBÿÿ¦@x@ßA$AjdàE\ ñáj}}B½e =ájì €=â½%pä(%±fà@I @å$±gàB¨ €ƒá Jå±h B@JÀBëc@x@¿AAJiàEãÄ€ˆÿÿw~~e±jà=dׄFå±k =áˆÛÊ€{áˆ%páˆ"ólà@"Æ€@ÿÿ,â óå±BQAëmàbƒÅ€bᨠ£ç±n B@a¨ÀB è ¥2¾Ò¾@x@ßA$A¨oàEj€òbóå±€bóp =áj˜@ó`=ájä^(fqà@ø>€@å,±ràJdÀJ þå ±s B@aRàBÿÿnb@x@ÇA ARtàEw÷ Ó@Y¦:å ±aRu =áRÿ€=áR"½và@{ú€@ë cCƒAð1ñ”P1 €öä@¸Àÿåüwàbçù b@% áå±x B@jÀBÿÿ¦@x@ßA$AjyàEþ²ôbàˆá j‚‚ajz =áj޶€=ájå ±{à@êµ€@à~Áëc|àBJàBå±} B@JÀBå±@x@¿AAJ~àE„nõáJƒƒ@=à= „Fÿÿô€ =áˆqt€{è¥% å±à@Ào€@ë câ óå±¥°E±‚àb  bá ë!¨ £å ±ƒ B@a¨ÀBÿÿô@x@ßA$Aj„àE *öa¨ÿÿyÆ„…bó… = ƒ.ê€=ájå ±†à@Žè€@å,±‡àJúç J äFëcˆ B@aRàBëc@x@ÇA AR‰àE¡÷aRéyä F††aRŠ =áRǨ€=äFáR%±‹à@)¤€@å ±SŰE°%a &UB½Œàb•£ b@$å± B@jÀBå±@x@ßA$AjŽàEŸ\øajàˆá j‡‡aj =áj/`€=ájå ±à@Œ_€@à~屑àBë^àB$ ƒäœy’ B@JÀBå±@x@¿AAJ“àE&ùáJˆˆe±”à=§¤Fÿÿ$X• =ሀ{áˆ% å±–à@a€@ÿ ÿ¦': @ñ+câ ó层§Bó—àbÂá Áë!¨ b᨜y˜ B@a¨ÀBå±@x@ßA$A¨™àE­Ó€ëëc‰ŠNWš =ájà“úaæå"±›à@@’€@#Pãû2€$ŸüyœàJ¬‘ J IäFå± B@aRàBå±@x@ÇA ARžàEºJûaãÇä F‹4y2hŸ =@þ€ƒiR€=â½!â½ à@ÊM€@å ±c"Q p ÓåK”åJ”àŒ}B½¡àb6Àb@)å±¢ B@€æÀBÿÿbþ@x@ßA$Aj£àEAüájŒ$Î!j¤ =ájÉ €=áj%-áj1¥à@& @á $( @ãÅ.`@B¦àB…€ƒà B$àB¥±§ B@JÀB âáÿÿ,@x@¿AAJ¨àEÈÁà ˆâ µe±©à=IÔ„Fñª =@®€Á°Ç€{áˆ% áˆ(¥«à@À@å ±$F @å±6Çâ óå±(A¨¬àbd€bᨠbá륱­ B@`æÀBöÇ@x@ßA$A¨®àEN}ýbóëcŽbó¯ =ájz=þ`=å"±°à@Ù;€@ë,c±àJEÀJA#äFå±² B@aRàBñ@x@ÇA AR³àE\ô Ó@Y ˆâ ûaR´ =áRü€=áR"½µà@d÷€@ÿ ÿ, s@%åK`R1ø1P@¸ÀØêB½¶àbÐö€bàáÈoöÇ· B@jÀBëc@x@ßA$Aj¸àEã¯ÿbç¥á j‘‘aj¹ =ájs³€=ä(%-ä(%±ºà@в€@à~Áå±»àB/ Bá Jå±¼ B@JÀBå±@x@¿AAJ½àEik:@Tàˆá J’’e±¾à=ê¤Fëc¿ =áˆVq€{âó% áˆ"óÀà@¡l€@ÿ ÿ,â óå±|AëÁàb báÁë!¨ £å ±Â B@a¨ÀBëc@x@ßA$A¨ÃàEð&a¨å±“”H¥Ä =ájç€=ájê(fÅà@wå€@å,±ÆàJãä J ¥(ÁRå±Ç B@aRàBxÿÿÞ@x@ÇA ARÈàEýaRàˆâ û••aRÉ =áR­¥€=áR"½Êà@¡€@è o$F!“èo.!ƒ ï$€ÿ„1€ðuP äÿa SýB½Ëàbn €bä ©å±Ì B@jÀBÿÿŽ@x@ßA$AjÍàE„Yáj––ajÎ =áj ]€=å"±Ïà@j\€@á !j%òãÅ"üAJÐàBÈ[€Bˆ âµ$àB«cÑ B@JÀBå±@x@¿AAJÒàE áJ——e±Óà=Œ'ƒ|å±Ô =ሀ{áˆ% å±Õà@G€@¤ÿÿ$Xâ óå±÷A¨Öàb§á Ä^!¨ £áë«c× B@a¨ÀBÿÿx@x@ßA$A¨ØàE’Ѐëñ˜™bóÙ =áj½aæáj$áj"´Úà@ €@å,±ÛàJŒŽ JA#å±Ü B@aRàBÿÿ¦@x@ÇA ARÝàEŸGaàˆå ±ššaRÞ = …€ƒ[O€=å"±ßà@»J€@ñ “ïpuT}uU}€ tT/øÿÿ,5ñB½ààb'Àb ájå±á B@€æÀBñ@x@ßA$AjâàE&áj››ajã =áj¦€=â½%-â½+cäà@ @å$±åàBj€ƒá J$ ƒä–Çæ B@JÀB„@Ùáñ@x@¿AAJçàE­¾€ˆàËâ µœœe±èà=.Ñ„Fuÿÿ$Xé =@®€Á™Ä€{áˆ% áˆ(¥êà@é¿€@ü y': @è¥â ó屑AëëàbI báå±ì B@`æÀBëc@x@ßA$A¨íàE3zbóëcžbóî =ájS: `=ájå ±ïà@¶8€@å,±ðàJ"ÀJA#â¸ëcñ B@aRàBå±@x@ÇA ARòàEAñ€Óÿÿ(ŸŸaRó =áRýø€=å"±ôà@Uô€@ÿ ÿÞ£ötU%§öï´äC$™à)YB½õàbÁó€bá jå±ö B@jÀBÿÿ…*@x@ßA$Aj÷àEǬ bàˆä f  N!ø =ájT°€=áj%-ä(!ùà@²¯€@ã Å$( @ån(®@BúàBàBå±û B@JàB@Ùâøå±@x@¿AAJüàENh áJ¡¡e±ýà=ϤFzå±þ =@®€ÁBn€{äF%páˆ%±ÿà@‹i€@ÿ ÿÞö Çâ óå±ÿÌvBà£êá å±bZ æÀB ÿÿÞ@x@ßA$A¨àEÕ# a¨ëc¢£bó =àÆýã€=ájå ±à@`â€@å,±àJÌá J â¸å± B@‚9ÀBå±@x@ÇA ARàEâš aRàˆä F¤¤aR = …€ƒŽ¢€=áR"½ à@æ€@”î !$F!“èo³1øƒAàÿÓ”P&ÿÿF„LòDf àbR bàá½å± B@€æÀBÿÿ/¼@x@ßA$Aj àEiVáj¥¥aj =ájåY€=áj%pä(%±à@? @á !j @å ±àB¡X€ƒà Bå± B@JÀBñ@x@¿AAJàEðáJ¦¦e±à=q$ƒ|å± =áˆä€{áˆå ±à@)€@ÿÿQèâ óå±èAëàbŒ€bᨠ£áë± B@a¨ÀBå±@x@ßA$A¨àEwÍ€ˆå±§¨H¥ =áj£aæ å%dâó(f +à@Œ€@å,±àJq‹ J Šà‹å± B@aRàBå±@x@ÇA ARàE„Daàˆå ±©©aR =á8L€=â½%páR%±à@ŒG€@å ±Ã×ïðï$@ð6Çé|{a ¹-B½àbøF€bàáå± B@jàBè oÿÿ/¼@x@ßA$Aj!àE ájªªaj" =áj›€=áj%páj%±#à@õ€@á å±$àBWàBå±% B@JÀBå±@x@¿AAJ&àE‘»à Ëâ µ««e±'à=΄Få±( =ሊÁ€{ ãº$F% áˆ"ó) +à@μ€@ÿÿQèâ óå±ÿZŽ*àb- bá ë!¨ £å ±+ B@a¨ÀB¤â ó"ôßÿB@x@ßA$A¨,àEwbóñ¬­bó- =á)97`= á jä^%±. +à@›5€@å,±/àJÀJ Šáöå±0 B@aRàBå±@x@ÇA AR1àE&î€Óï+â û®®aR2 =áÞõ€= àÆ"½áR"½3 +à@6ñ€@ñ  ÓP ï$€ÿBs%´ïuÿÿ,þ]D©4àb¢ð€bá jëc5 B@jÀBñ@x@ßA$Aj6àE¬©bàˆá j¯¯aj7 =á),­€=å"±8à@‡¬€@à~ƹå±9àBì«àB$&5äè0: B@JàB@Ùî\$ÿÿ* @x@¿AAJ;àE3eáJ°°e±<à=´¤Fÿÿ@Ò= =@®€Á#k€{áˆ% å±>à@pf€@ÿ ÿ,â óå±Z¯Bó?àbÏá Áë!¨ £á¨Ÿÿ @ B@`æÀBÿÿ$X@x@ßA$A¨AàEº a¨ëc±²bóB =ájßà€= â,ä ^%±C +à@A߀@â m2€$Ÿÿÿ DàJ­Þ€Jå(ÁRå±E B@aRàBå±@x@ÇA ARFàEÇ—aRãÇä F³³!G =áxŸ€= ãá R%±H +à@Óš€@ÿ ÿÞãCudÇ´tð"ÿÿ÷ÿ* Iàb? bàáÁjå±J B@jÀBÿÿ/¼@x@ßA$AjKàENSáj´´ajL =á)ÖV€=ä(%-ä(+cMà@1 @â rå±NàB’U€ƒá J$ ƒä…±O B@JÀBëc@x@¿AAJPàEÕáJµµe±Qà=V!ƒ|ëcR =áˆÅ€{áˆ% áˆ"óSà@€@å ±$F @ô +câ óå±¼ÿ* Tàbq€bᨠbᨅ±U B@a¨ÀBå±@x@ßA$A¨VàE[Ê€ˆëc¶·bóW =áj‘Šaæ ä^%±X +à@òˆ€@å,±YàJ^ÀJ Šà‹å±Z B@aRàBzÿÿÞ@x@ÇA AR[àEiAaàˆå ±¸¸aR\ =áI€= àÆå±] +à@yD€@ë có6ÁÃ%¨Atð"Q¹@Ç$`#ÿC^àbåC€bàáå±_ B@jÀBå±@x@ßA$Aj`àEðü€ˆå2á j¹¹aja =á)xa¨ä(%-ä(%±bà@Óÿà ~Áå±càB4 Bá Jå±d B@JÀBå±@x@¿AAJeàEv¸ àˆá Jººe±fà=÷¤F{üyg =áˆk¾€{ ì%"ó% áˆ%±h +à@´¹€@ü y!ˆ @å±6Çâ ó屇ÿIxiàb báÁë!¨ bå ±j B@a¨ÀB ñ@x@ßA$A¨kàEýsbµå±»¼ból =á)*4`=âó+"áj%±mà@Œ2€@å,±nàJø1 J áöå±o B@aRàBå±@x@ÇA ARpàE ëà ˆâ û½½aRq =áR¼ò€= àÆ"½áR"½r +à@î€@ÿÿÞ2ÿ]F í ç Qy` yàRD©sàb‹í€bå(½ëct B@jÀBxâ ½öÇ@x@ßA$AjuàE‘¦ báóá j¾¾ajv =á)ª€=â½å ±wà@q©€@á $( @ãÅ"üAJxàBѨ€BàÁÁJ$àB«cy B@JÀBå±@x@¿AAJzàEb!aJÿÿâ¿¿e±{à=™t„Få±| =áˆh€{áˆ% äF"ó}à@Vc€@ÿ ÿÞë câ óå±öàA¨~àb¸¡á¨ £áë«c B@a¨ÀBÿÿô@x@ßA$A¨€àEŸ"a¨ñÀÁbó =ájÀÝ€=ájä^"´‚à@"Ü€@‡![ â°!j @Éðÿ ÿnbƒàJÛ€Já R屄 B@aRàBÿÿ¦@x@ÇA AR…àE¬”#aRàˆä FÂÂaR† =@þ€ƒNœ€= áa⽇ +à@¨—€@ñ2€p`ŸÿyÅuð¤ yàÕB½ˆàb bàáÄf屉 B@€æÀBÿÿ; @x@ßA$AjŠàE3P$ájÃÃaj‹ =á)ÃS€=â½%-áj+cŒà@ @å$±àBR€ƒá J屎 B@JÀBå±@x@¿AAJàEº %aJàËâ µÄÄe±à=oƒ|Gëc‘ =ሪ€{áˆ% áˆ%±’à@ô €@—ÿ ÿ @ëcâ óå±¥'Aë“àbV báÂó!¨ bå ±” B@a¨ÀBñ@x@ßA$A¨•àE@Ç€ëëcÅÆbó– =ájj‡&aæáj%páj%±—à@Ë…€@ë,c˜àJ7ÀJA#åüå±™ B@aRàBå±@x@ÇA ARšàEN>'aàˆâ ûÇÇaR› =áRÿE€=áR"½œà@ZA€@ñ2#$Ä"ïTþpGîÓ”üyQqàbÆ@€bàá½å±ž B@jÀBå±@x@ßA$AjŸàEÔùà ˆá jÈÈaj  =ájeý€= â1'å ±¡ +à@Àü€@á ëc¢àB! Bä ƒä–Ç£ B@JÀB‹ âø,¯üy@x@¿AAJ¤àE[µ(âµÉÉe±¥à=ܤFÿÿ$X¦ =@®€ÁK»€{âó% äF"ó§à@–¶€@ñ )ø @å±+câ óå±õq¨àb÷á Âó `bᨖǩ B@`æÀBå±@x@ßA$A¨ªàEâp)a¨ëcÊË[&« =áj1*`= â,"óå ±¬ +à@i/€@å,±­àJÕ. J Šà‹å±® B@aRàBñ@x@ÇA AR¯àEïçà ˆä FÌÌaR° =áï€=â½áR"½±à@÷ê€@ë c 3ï çQ€ %·ëct©Ho²àbc bá jå±³ B@jÀBÿÿ; @x@ßA$Aj´àEv£+bàˆá jÍÍajµ =ájú¦€=áj%-ä(+c¶à@V @à~ÃÅå±·àB¶¥€ƒà B$ ƒä…±¸ B@JÀBëc@x@¿AAJ¹àEý^,áJÎÎe±ºà=~q„Få±» =áˆíd€{áˆ%páˆ"ó¼à@7`€@ü y!ˆ @å±(¥â óå±Ð[Aë½àb™_€bᨠbᨅ±¾ B@a¨ÀBå±@x@ßA$A¨¿àE„-a¨å±ÏÐbóÀ =áj²Ú€= å%å±Á +à@Ù€@å,±ÂàJ~Ø J Šå ±Ã B@aRàBëc@x@ÇA ARÄàE‘‘.aRàˆä FÑÑaRÅ =á7™€= àÆá R"½Æ +à@™”€@å ±C$´yD€@ü yS$p_åKdEJpWåI´@Kà[ÃB½Üàb§= b@&áöå±Ý B@€æÀBå±@x@ßA$AjÞàE¹öà ˆá j××ajß =@Ý€ƒAú€=å"±àà@žù€@á鯹å±áàBýø€Bá J$åô«câ B@€ÆÀBå±@x@¿AAJãàE@²3bµàˆá JØØe±äà=Á¤Fëcå =ÀÁ0¸€{áˆ% 屿à@{³€@å ±!ˆ @ëcâ óå±ÿ1BóçàbÜá Áë!¨ bå ±è B@a¨ÀBëc@x@ßA$A¨éàEÇm4a¨ëcÙÚQê =ájò-5`= ãºä ^%±ë +à@R,€@å,±ìàJ¾+€Jã >å±í B@aRàBå±@x@ÇA ARîàEÔ䀈âûÛÛaRï =áì€= áR%±ð +à@ìç€@ë ccSXþ€KCX€FåLpDå yàÿ»*ñàbX bàá½å±ò B@jÀBÿÿüÄ@x@ßA$AjóàE[ 6bàˆä fÜÜajô =á)㣀=ä(%-ä(+cõà@@ @â r$( @ãÅ"ü@BöàB£¢€ƒàÁÁJ$àB¥±÷ B@JàB ç?öÇ@x@¿AAJøàEâ[7áJÝÝe±ùà=cn„Fx3àƒÿÿÉ‚ú =@®€ÁÒa€{áˆ% áˆ"óûà@]€@ü yë câ óå±9ÿÁüàb~\€bᨠ£áë«cý B@`æÀBå±@x@ßA$A¨þàEh8a¨¼ Z¡îâó!‚ 8Þßbóÿ =áj“×€=ájõtB´ +à@óÕ€@â m!j @ÿÿ,àJ_ÀJ ®ÀÉÂûå± B@aRàBÿÿ; @x@ÇA ARàEvŽ9aRàˆáR@ÿààaR =@€ƒ1–€=!g Xì¶"½à@Ž‘€@å ±sK+\>åIdpÿ¸q yàhoàbú€bàáÁjå± B@€æÀBå±@x@ßA$AjàEýI:ajå2á jááaj =ájyM€= á j%-ä(%± +à@ÚL€@à~Áå± àB9 Bá Jå± B@JàB@˜â1å±@x@¿AAJ àEƒ;aJàˆá Jââ@=à=ƒÇ}å± =@®€Át €{âó% áˆ"óà@¿€@—ÿÿ,â óå±±nWàb báÁë!¨ £å ± B@`æÀBå±@x@ßA$AjàE Á€ëñãäbó =áj=bµàˆá Jçç@=#à=¦¤Få±$ =áGµ€{ ãº!ˆ% âó% +à@]°€@›å ±$F @ñÃâ óå±¹Bó&àbÁá ᨠb᨜y' B@a¨ÀBöÇ@x@ßA$Aj(àE¬j?a¨ñèéQ) =á)ß*@`= á jä^+c* +à@?)€@ÿ,ÿ +àJª(€JârÁRå±, B@aRàB}LWè­Ÿÿ%« @x@ÇA AR-àE¹áà ˆâ ûêêaR. =ádé€= à ƒá R(o/ +à@Åä€@ö Ç “”P¯M„€E¸ñyB½0àb1Àb ¢á'å±1 B@jÀBÿÿ; @x@ßA$Aj2àE@Abàˆá jëëaj3 =á)È €= àÆ!j%-ä(+c4 +à@% @à~Ârå±5àB„Ÿ€ƒà Bå±6 B@JÀBå±@x@¿AAJ7àEÇXBaJæ}á Jììe±8à=Hk?`=å±9 =áG³^€{ á ˆ% áˆ"ó: +à@Z€@ÿ ÿB!ˆ @å±Kâ óå±½‹Aë;àbcY€báÁë!¨ bå ±< B@a¨ÀBå±@x@ßA$A¨=àEMCajëcíîbó> =á)iÔ€=ê%±?à@ÌÒ€@ë,c@àJ8ÀJ ånå±A B@aRàBÿÿF„@ 5@ÇA ARB @`E[‹DaRàˆâ ûïïGC =áRö’€= àÆ$Fç"½D +à@SŽ€@ë c£+ZQ¾@ ï%º¯MQ yàcKB½Eàb¿€bàá½å±F B@a$àBëc@x@ßA$AjGàEáFEájððajH =á)bJ€=â½å ±Ià@¿I€@á $( @ãÅ"üAJJàB& BäàB«cK B@JÀBƒ@Ùâøñ@x@¿AAJLàEhFáJññe±Mà=é¤Fÿÿ* N =@®€ÁX€{ àÁ"ó% å±O +à@¤€@ü yë câ óå±ǹA¨Pàb bá ëå±Q B@`æÀBñ@x@ßA$A¨RàEï½€ëå±òóbóS =á)~Gaæâóä^"´Tà@v|€@å,±UàJâ{ JA#⸠`‹ã>-Ž_V B@aRàBå±@x@ÇA ARWàEü4Haàˆå ±ôôaRX =áR¤<€=áR%±Yà@ü7€@ÿ ÿB³&!ç%­|íTÿÓÿÿ5nÿT¦Zàbh bàáå±[ B@jÀBÿÿ/¼@x@ßA$Aj\àEƒð€ëàˆá jõõaj] =ájô€= â1$(%-ä(+c^ +à@aó€@à~Á `@âr"ü@B_àBÃòàB àB¥±` B@JàB@˜âøå±@x@¿AAJaàE ¬Iâµööe±bà=‹¾„Fÿÿ÷c =@®€Áþ±€{ àÁå ±d +à@F­€@ÿÿxâ óå±µÿZŽeàb¦á Áë!¨&Uáë±f B@`æÀB è ¥!¨ÿÿ5n@x@ßA$A¨gàE‘gJa¨üy÷øJh =á)Á'K`=ä^"´ià@#&€@â mefüyjàJ% J þáöå±k B@aRàBÿÿ/¼@x@ÇA ARlàEžÞ€ˆÿÿ!¢ùùARm =áR>æ€=áR%±nà@¢á€@ñ  Ã"ÃåM”åL”ÿ ”a BåDfoàbÀb@$ä#aµâ½¿ÿ p B@jÀBwâ ½ÿÿ$X@x@ßA$AjqàE%šLbájúúajr =áj±€= àÆ$(å ±s +à@  @ë å±tàBmœ€ƒá J$ ƒáJ¥±u B@JÀBñ@x@¿AAJvàE«UMáJûû@=wà=-h„Fñx =áG [€{ á ˆ% ç:"óy + -ìV€@ë câ ó屿BózàbK bᨠ£á¨–Ç{ B@`¥ÀBâ ó"ôå±@x@ßA$Aj|àE2Na¨ÿÿ,üýbó} =á)[Ñ€=ä^%±~à@½Ï€@å,±àJ)ÀJ þäFå±€ B@aRàBå±@x@ÇA ARàE@ˆOaR‹@Y¤Ïé øþþaR‚ =áRì€=áR"½ƒà@L‹€@å ±ÓdpåKEJpQÅPåÿÿ¦HhB½„àb¸Š bàáÈoå±… B@jÀBöÇ@x@ßA$Aj†àEÆCPajå±ÿ@•A‡ =ájNG€= Á¨ÀÆå±ˆ +à@©F€@å$±‰àB  Bá J届 B@JÀBå±@x@¿AAJ‹àEMÿ€Ëàˆâ µ!!@=Œà=Ò¤Fÿÿ5n =áG]QaÆ á ˆ% 屎 +à@²€@ö Çâ óå±ìšBóàb báÂ,!¨ £å ± B@a¨ÀBÿÿåü@x@ßA$Aj‘àEÔº€ëüyBó’ =á)ìzRájñ “à@Oy€@å,±”àJ»x€Jã >展 B@aRàBå±@x@ÇA AR–àEá1SaáÛâ ûaR— =áRž9€=å"±˜à@ñ4€@”ÿ 7)ø!“ù… ã0å åOp åMÿ…3!ça ù•B½™àb] bàá½E ⽫cš B@jàBè oëc@x@ßA$Aj›àEhí€ëàˆá jajœ =ájôð€= ä (*ßä(1 +à@O @â r!j @ãÅ(®@BžàB°ï€ƒàÁÁJ$àB«cŸ B@JÀBå±@x@¿AAJ àEï¨Tâµe±¡à=p»„Få±¢ =áG㮀{ ç :% áˆ(¥£ +à@,ª€@ÿ ÿ,â óå±]gA¨¤àb‹©€bᨠ£áë«c¥ B@a¨ÀBñ@x@ßA$A¨¦àEudUa¨öÇbó§ =á)Š$V`= á j#ºáj"´¨ +à@ì"€@å,±©àJXÀJ Šà‹å±ª B@aRàBzÿÿ,@x@ÇA AR«àEƒÛ Ó@YëcaR¬ =á$ã€=å"±­à@{Þ€@ö Çó6Ä+\ åP0åäÿåM@¸À8B½®àbçÝ€bàáÄf屯 B@jÀB„å±@x@ßA$Aj°àE —Wbå2å ±  aj± =áj’š€= á¨ÀÆëc² +à@홀@à~À@å±³àBR Bá Jå±´ B@JàB@˜¢oÀ…&ýÿÿ* @x@¿AAJµàERXaJàˆá J  e±¶à=e„FÿÿŽ· =@®€Á…X€{âó% äF%±¸à@ÎS€@å ±!ˆ @è¥â ó屦ÊBó¹àb, báÁe!¨ bå ±º B@`æÀBå±@x@ßA$A¨»àEYa¨å±  bó¼ =áj0΀=ájå ±½à@’Ì€@â möǾàJþË J â¸å±¿ B@aRàBëc@x@ÇA ARÀàE%…ZaRãÇâ û  aRÁ =áRÙŒ€= àÆ$FáR"½Â +à@5ˆ€@ÿÿ,3p…JT…JUï`\…à}B½Ãàb¡‡€bàá½å±Ä B@jÀBÿÿ@Ò@x@ßA$AjÅàE«@[ajáóá jajÆ =á)7D€= â1!j*ßä(+cÇ +à@’C€@à~Á$( @ãÅ(®@BÈàBóBàB$àB«cÉ B@JÀBëc@x@¿AAJÊàE2üà ˆá Je±Ëà=³¤F{3"<ã|ÿÿôÌ =áG"\aÆ à=`G!ˆ% áˆ"óÍ +à@pý€¼—î W$F @å±Ãâ óå±ÙæA¨ÎàbÎá À£!¨ báë«cÏ B@a¨ÀBÿÿ,@x@ßA$A¨ÐàE¹·,ñbóÑ =á)âw]ajå"±Òà@Dv€@ÿ,ÿ ÓàJ¯u€Jã >å±Ô B@aRàBëc@x@ÇA ARÕàEÆ.^aàˆâ ûaRÖ =áRk6€= âûâ½å ±× +à@Ê1€@å ±%ŽefåN`äõNuOÿÿnbÄÿh°Øàb6 bàáÀ£å±Ù B@jÀB}ëc@x@ßA$AjÚàEMê€ëàˆá jajÛ =á)Ùí€= á j%-ä(%±Ü +à@4 @à~Áå±ÝàB•쀃à B$ ƒäŸÿâÞ B@JÀBå±@x@¿AAJßàEÔ¥_bµç…á Je±àà=U¸„Få±á =áG¼«€{ á ˆ% áˆ%±â +à@ §€@ü y!ˆ @å±(¥â óå±`ÿh°ãàbp¦€báÁë!¨ b᨜yä B@a¨ÀB”â ó- üy@x@ßA$A¨åàEZa`a¨ëcbóæ =á)„!a`=å"±çà@å€@ë,cèàJQÀJ þäêå±é B@aRàBå±@x@ÇA ARêàEhØ Ó@Yh'â ûaRë =áR à€=áR"½ìà@pÛ€@üy3#‘‘PE³‘‡@ud€ °ày“D©íàbÜÚ b@& á½å±î B@jÀBÿÿF„@x@ßA$AjïàEî“bbàˆä fajð =áj{—€=áj%-ä(%±ñà@Ö–€@à~Á$( @ãÅ«còàB7 BäàB¥±ó B@JÀB‹@Ùá$ñ@x@¿AAJôàEuOcáJe±õà=ö¤Få±ö =@®€Á]U€{ àÁëc÷ +à@¯P€@”ëcâ óå±ÝaAëøàb bá ë å±ù B@`æÀBå±@x@ßA$A¨úàEü da¨å±bóû =á)*Ë€=âóê"´üà@‹É€@ˆ@Sâm,Î @ÿÿÞýàJ÷È JA#àJå±þ B@aRàBå±@x@ÇA ARÿàE ‚eaRàˆä FaRIáR·‰€=áR%±à@…€@ë c3udǵåf‘äõeõf “àVÖB½àb}„€bàáÄ©å± B@jÀBè oüy`x@9 Aj @`E=fájaj =Bi`=A€=a½â1$(%-å±à@t@€@å nå±àBØ?€Bá J `ƒåô¥± B@€ÆàB@ÙàÆ!Kå±@x@¿AAJ àEùà ˆâ µe± à=˜ ƒ|öÇ =@®€Áÿ€{áˆ*¿áˆc à@Qú€@ë câ óå±ò$Bó àb³á Âó!¨ £á¨‹c B@`æÀBñ@x@ßA$A¨àEž´gbóñ bó =ájÌth`=ä^%±à@,s€@â må±àJ˜r JA#â¸å± B@aRàBÿÿ/¼@x@ÇA ARàE«+iaàˆâ û!!aR =áRa3€=áR"½à@».€@å ±C"µ„Få± =@®€Á¡¨€{áˆ% å±!à@ó£€@ÿ ÿÞ$F @ñ6Çâ óå±ÚèBó"àbT£€bᨠbᨅ±# B@`æT£€B•è ¥!¨ñ@x@ßA$A¨$àE?^ka¨ëc$%bó% =ájel`=áj+"áj%±&à@Æ€@å,±'àJ2ÀJ â¸å±( B@aRàBå±@x@ÇA AR)àEMÕ€ÓãÇä F&&aR* =áRûÜ€=G@—àÆ)øáR%±+à@UØ€@ÿÿ,3SåTð~€¸%åHT@zÀlÿ@Ò,Ià£Á×€bá jå±- B@jÀBå±,I€ßA$Aj. @`EÓmbàˆá j''aj/ =ájT”€=áj%-â½C0à@°“€@áéÅnëc1àB BàÁÀBå±2 B@aàBå±@x@¿AAJ3àEZLnáJ((e±4à=Û¤Fÿÿ; 5 =áˆJR€{H@Õâ ó% áˆ"ó6à@•M€@ÿÿ,â óå±Èÿ@Ò7àböá ᨠ£å ±8 B@a¨ÀBëc@x@ßA$A¨9àEáoa¨ëc)*bó: =ájÈ€=ájä^K;à@pÆ€@å,±<àJÜÅ€Jã >å±= B@aRàBå±@x@ÇA AR>àEî~paRã„ä F++aR? =áR˜†€=áR"½@à@î€@ó Ó$F!“èo"½cÿ¿+gKg ëcíüD©AàbZ bàá½å±B B@jÀBÿÿŠÜ@x@ßA$AjCàEu:qáj,,ajD =áj>€=ájå ±Eà@^=€@â r!j @ãÅ"üAJFàB½<€Bá Jå±G B@JÀBå±@x@¿AAJHàEüõà ˆâ µ--e±Ià=}ƒ|å±J =áˆðû€{áˆ% å±Kà@7÷€@å ±ë câ óå±IA¨Làb˜ö€bᨠ£áë«cM B@a¨ÀB ëc@x@ßA$A¨NàE‚±rbóëc./bóO =áj•qs`=è¥$áj"´Pà@õo€@å,±QàJaÀJ ç¨å±R B@aRàBå±@x@ÇA ARSàE(taàˆâ û00aRT =áRF0€=â½áR%±Uà@ +€@ë csä+bþ€š+a%·ÿÿ/¼|b½Vàb  bàáŸÿÐå±W B@jÀB‚â ½ ÿ¼Ó@x@ßA$AjXàEä€ëå2á j11ajY = …€ƒ£ç€=áj%pâ½+cZà@ÿæ€@á å±[àB_àBå±\ B@€ÆÀBå±@x@¿AAJ]àEŸuâµ22e±^à=²„Fÿÿ¦_ =ሖ¥€{äF% `ï€3ÒBó`à@Ù €@ÿÿxâ ó屨h¥aàb9 bá ë!¨ £å ±b B@a¨ÀBÿÿyÆ@x@ßA$A¨càE$[v: å±34bód = 0`ƒSw`=ájä^%±eà@³€@å,±fàJÀJ å±g B@€ÎÀBå±@xe 9d ARh @`E2Ò€ÓäF55aRi = ?`=ÀÙ€=äFáR%±jà@"Õ€@ÿ ÿ,ƒP0ååM`´ tQ%Mø Œà$˜D©kàbŽÔ€bàáå±l B@€æÀBÿÿ; @x@ßA$AjmàE¸xbáóå ±66ajn = …€ƒ@‘€=ájå ±oà@€@à~Áå±pàBüàB$ ƒä–Çq B@ àBå±@x@¿AAJràE?IyáJ77e±sà=À¤Få±t = ½€Á/O€{áˆ%  +@@3bóuà@zJ€@ü yë câ óå±€jBóvàbÛá Áë!¨ £`¡ p «cw B@`æÀBñ@x@ßt =A¨x @€EÆza¨ñ89bóy =A6`=õÄ€=å"±zà@UÀ@‡@Såf!j$Ÿÿÿ,{àJÁ J@ÝàJëc| B@€ÎÀBå±@x@ÇA AR}àEÓ{{aRãÇä F::aR~ =@þ€ƒvƒ€=áR%±à@×~€@å ±“æ‘Qak`@Nÿÿ$X©ÿô€àbCÀb ájëc B@€æÀBå±@x@ßA$Aj‚àEZ7|áj;;ajƒ =ájÚ:€=ä(%-ä(+c„à@7 @â rå±…àB–9€ƒá J$ ƒa5„…±† B@JÀBå±@x@¿AAJ‡àEáò€ˆàËâ µ<bóŽ =ájn~`=ä^+cà@îl€@ë,càJZÀJA#äF屑 B@aRàBÿÿF„@x@ÇA AR’àEu%aÿÿ(Ÿ??aR“ =áR0-€=áR"½”à@(€@ÿ ÿ,£ å%¨6¾åKð£äñÔ£a ÿê•àbù'€bá jå±– B@jÀB~ á­ñ@x@ßA$Aj—àEüà€ˆãFä f@@aj˜ =áj„ä€=áj%-ä(%±™à@áã€@à~ÃÅ屚àB@àBå±› B@JÀB‹ áJ!KÿÿÞ@x@¿AAJœàE‚œ€bµáÓá JAAe±à=¯„Fëcž =@®€Án¢€{å"±Ÿà@º€@ë câ óå±°ÿ Ò àb báÁë!¨&çZŸÿ,¡ B@`æÀBÿÿ@Ò@x@ßA$A¨¢àE Xa¨å±BCbó£ =áj-‚`=âó+"âó%±¤à@€@å,±¥àJü JA#â¸å±¦ B@aRàBx àBõ\ @x@ÇA AR§àEÏà ˆâ ûDDaR¨ =áRÊÖ€=áR%±©à@'Ò€@å ±³ðåJd`=åMp?ô ýà]WD©ªàb’Ñ€bá j屫 B@jÀBÿÿ@Ò@x@ßA$Aj¬àEŠƒbàˆá jEEaj­ =ájŽ€=ä(å ±®à@w€@å$±¯àBÕŒ€BàÁä ƒä‹c° B@JÀBëc@x@¿AAJ±àE$F„áJFFe±²à=¥¤Få±³ =áˆL€{áˆäF"ó´à@`G€@ö Çâ óå±ùÿˆµàbÀá ᨠ£á¨…±¶ B@a¨ÀB è ¥'ZÿÿÞ@x@ßA$A¨·àE«…a¨ñGHbó¸ = ``ƒÚÁ€=ájå ±¹à@=À€@å,±ºàJ©¿ J ånå±» B@€ÎÀBÿÿ$X@x@ÇA AR¼àE¸x†aRàˆä FIIaR½ =áRh€€=å"±¾à@È{€@ñ  À(åMdpåR%°@ÿÿôÕE±¿àb4ÀbA;æÙå±À B@jÀBÿÿ5n@x@ßA$AjÁàE?4‡ájJJaj =áj³7€=â½%-èo+cÃà@ @å$±ÄàBw6€ƒá J)º ƒä…±Å B@JÀB„@Ùá$ëc@x@¿AAJÆàEÅïà ˆâ µKKe±Çà=Gƒ|ÿÿ§VÈ =@®€Á²õ€{áˆ% áˆ%±Éà@ñ€@ÿ ÿ â óå± 'AëÊàbbð€bá ë!¨ £á¨…±Ë B@`æÀBëc@x@ßA$A¨ÌàEL«ˆbóëcLMbóÍ =ájpk‰`=ájå ±Îà@Ói€@å,±ÏàJ?ÀJA#â¸å±Ð B@aRàBå±@x@ÇA ARÑàEZ"Šaàˆâ ûNNaRÒ =áR*€=áR"½Óà@f%€@å ±ÓåK´‘¥€åM´åa 9ÈB½ÔàbÒ$ b@$ájå±Õ B@jÀBå±@x@ßA$AjÖàEàÝà ˆá jOOaj× =ájaá€=ç%-ä(%±Øà@¿à€@ã ÅñÙàB  Bá J$ ƒä…±Ú B@JàB áå±@x@¿AAJÛàEg™‹âµPPe±Üà=è¤Fÿÿ5nÝ =@®€ÁSŸ€{âó% áˆ"óÞà@¤š€@å ±!ˆ @öÇâ óå±:Aëßàb bᨠbᨅ±à B@`æÀBå±@x@ßA$A¨áàEîTŒa¨ëcQRbóâ =áj`=âóå ±ãà@}€@â müyäàJé€J‡A#â¸å±å B@aRàBñ@x@Çâ =ARæ @`EûË€ˆáÛä FSSaRç =áR“Ó€=â½áR"½èà@ë΀@ÿ ÿÞãK´ åR`~€[åM`Ì  ·B½éàbW bàáÎ!å±ê B@a$àBÿÿ5n@x@ßA$AjëàE‚‡Žbàˆá jTTajì =áj ‹€=áj%-ä(%±íà@dŠ€@ˆ@Øâr$( @ãÅ4@BîàBƉàBå±ï B@JÀBñ@xí 9AAJð @€E CáJUUe±ñà=ŠUŒ`=å±ò =áˆõH€{áˆ% áˆ"óóà@BD€@› Ñáˆ$F @å±à â ó屯EA¨ôàb¥á Âó!¨ bá륱õ B@abÀBå±@x@ßA$A¨öàEþ€ëëcVWbó÷ =áj¸¾a¨å"±øà@½€@å,±ùàJ†¼ JA#áµå±ú B@aRàBëc@x@ÇA ARûàEu‘aàˆä FXXaRü =áR=}€=â½å ±ýà@•x€@å ±óKWK´…µ~aÀ' B½þàb bàá½å±ÿ B@jÀBå±@x@ßA$AjJ  `E$1’aj‡@¡óá jYYaj =áj°4€=áj%pä(%±à@  @á å±àBl3€ƒà B$ ƒä‹c B@aàBå±@x`¿ =AJ @€Eªìà ˆá JZZe±à=+ÿ„Fÿÿ*  =ሟò€{äF% áˆ%±à@çí€@ÿÿQèâ ó屃ùAë àbF bᨠ£á¨‹c B@abÀBöÇ@x@ßA$A¨ àE1¨“bóå±[\bó =ájVh”`=ájä^(f à@¸f€@å,±àJ$ÀJ £½¿ÿ×å± B@aRàBå±@x@ÇA ARàE?•aäF]]aR =áRó&€=áR"½à@O"€@üy4€G‘ K´eµa Ü/B½àb»!€bàáÁjå± B@jÀBù …ÿÿ,@x@ßA$AjàEÅÚ€ˆáóä f^^aj =ájAÞ€=å"±à@œÝ€@á å±àB BàÁÁJå± B@JàB@Ùâø!KöÇ@x@¿AAJàEL––bµŒ 4ÀEá J__e±à=ͤFÿÿ]L =@®€Á<œ€{áˆ% å±à@—€@ÿ ÿÞë câ óå±xÝBóàbì¡á¨ £å ± B@`æÀBëc@x@ßA$A¨ àEÓQ—a¨ñ`abó! =áj˜`=ä^%±"à@b€@ÿ,ÿ #àJΠJ â¸å±$ B@aRàBå±@x@ÇA AR%àEàÈ€ˆáÛâ ûbbaR& =!J ƒ…Ѐ=áR%±'à@ØË€@â ½$F'Eî!%±KW€3<}p/åS`+ Œàˆ¿B½(àbD bàáÄfå±) B@jàBå±@x@ßA$Aj*àEg„™bàˆá jccaj+ =áj燀=ä(*ßä(+c,à@B @â r!j @ãÅ"ü@B-àB£†€ƒàÁÁJå±. B@JÀBëc@x@¿AAJ/àEî?šaJàËá Jdde±0à=“R„FVÿÿ@Ò1 =áˆâE€{áˆ% áˆ"ó2à@+A€@ëcâ ó›ÿÿ÷,MA¨3àbŠ@€báᨠ£áë«c4 B@a¨ÀBëc@x@ßA$A¨5àEtû€ˆëcefbó6 =áj¡»›aæáj$áj"´7à@º€@è ' @€ Ã! !% … AR8àJo¹ J å±9 B@aRàBÿÿ5n@x@ÇA AR:àE‚rœaàˆâ ûggaR; =áR3z€=å"±<à@Žu€@ñ4#+\%¶åKdpa ÿ=àbút€bàá½å±> B@jÀBÿÿF„@x@ßA$Aj?àE .ajãFá jhhaj@ =áj•1€=áj%pâ½%±Aà@ð0€@à~Áå±BàBQàBå±C B@JÀBÿÿ @x@¿AAJDàEé€ËáÓá Jiie±Eà=ü„FñF =ÀÁƒï€{äF% áˆ%±Gà@Íê€@öÇâ óå±¢ÿèHàb+ báÁë!¨ £å ±I B@a¨ÀBå±@x@ßA$A¨JàE¥žbóå±jkbóK =ájCeŸ`=áj%páj%±Là@¥c€@å,±MàJÀJ å±N B@aRàBå±@x@ÇA AROàE# aàˆâ ûllaRP =áRÁ#€=äFáR"½Qà@€@å ±3|}€þ€åJ´ ÿÿ,IÅD©Ràbˆ€bä ©å±S B@jÀBå±@x@ßA$AjTàEª×€ˆájmmajU =áj:Û€=â½å ±Và@–Ú€@á å±WàBöÙ€BàÁä ƒä–ÇX B@JÀBëc@x@¿AAJYàE1“¡âµnne±Zà=²¤Få±[ =áˆ%™€{áˆ% äF"ó\à@k”€@ÿÿ â óå±í7Bó]àbÍá ᨠ£á¨–Ç^ B@a¨ÀBå±@x@ßA$A¨_àE¸N¢a¨ñopbó` =ájÍ£`=ájä^%±aà@. €@å,±bàJš  J å±c B@aRàBÿÿ¦@x@ÇA ARdàEÅÅà ˆå ±qqaRe =áRrÍ€=áR"½fà@ÕÈ€@ÿÿÞ 4CMpƒà¿ÿ¬Çàë!B½gàbAÀb@(ájaö⽿ÿ]Lh B@jÀByñ@x@ßA$AjiàEL¤bàˆá jrrajj =ájÜ„€=áj%-ä(+ckà@8 @å$±làB˜ƒ€ƒàÁä ƒáJ¥±m B@JÀBŒ@ÙáöÇ@x@¿AAJnàEÒ<¥áJsse±oà=TO„Få±p =@®€Á»B€{áˆ% áˆ"óqà@ >€@ÿ ÿÞ/ª @ô +câ óå±OÓAëràbo=€bá ëëcs B@`æÀBÿÿ¦@x@ßA$A¨tàEYø€ˆëctubóu =ájw¸¦aæê%±và@ض€@å,±wàJDÀJA#â¸öÇx B@aRàBëc@x@ÇA ARyàEgo§aàˆä FvvaRz =áR w€=â½ç"½{à@{r€@ë cSð~"Žef®¯Ža 9ÿ|àbçq€bàáÄfå±} B@jÀBÿÿ; @x@ßA$Aj~àEí*¨ájwwaj = …€ƒz.€=â½%-ä(%±€à@Õ-€@á ëcàB9àB$ ƒå ±‚ B@€ÆàB@Ùâøå±@x@¿AAJƒàEtæà Ëâ µxxe±„à=õ¤Fÿÿ$X… =@®€Ádì€{âó%páˆ"ó†à@®ç€@ö Çâ óå±WAë‡àb bá ë!¨ £á¨XF , B@`æÀBÿÿ* @x@ßA$A¨‰àEû¡©bóëcy$ÆAŠ =ájbª`=âóä^K‹à@v`€@â m' @öÇŒàJâ_ JA#â¸å± B@aRàB êàBÿÿ¦@x@ÇA ARŽàE«aàˆâ û{+Ë!R =áR® €=â½!â½à@€@ë ccab‘‘ÿÿQë ýàÍHB½‘àb| b@&ájå±’ B@jÀBÿÿ* @x@ßA$Aj“àEÔà ˆá j||aj” =ájØ€=áj%-áj%±•à@{×€@á å±–àBÛÖàB$ ƒä‘— B@JÀBëc@x@¿AAJ˜àE¬âµ}}e±™à=—¢„F屚 =áˆþ•€{áˆ% áˆ%±›à@P‘€@ÿ m$F @ëcâ óå±iºAëœàb²á È¥!¨ bᨅ± B@a¨ÀBëc@x@ßA$A¨žàEœK­a¨¼ Z£|âóD½~bóŸ =ájÓ ®`=å"± à@3 €@å,±¡àJŸ  J  ÉÁRå±¢ B@aRàB%IàBå±@x@ÇA AR£àEªÂà ˆáRA@€€aR¤ =@€ƒPÊ€=áR"½¥à@²Å€@å ±sd€ud:È€ðÇ´ àõpB½¦àbÀb@&ájå±§ B@€æÀBñ@x@ßA$Aj¨àE1~¯bîªá jaj© =ájµ€=ä(%-ä(%±ªà@ @à~Âr屫àBq€€ƒá J$ ƒä…±¬ B@JÀBå±@x@¿AAJ­àE·9°aJàˆá J‚‚e±®à=õ€ëñƒ„bó´ =ájyµ±aæájê+cµà@Ù³€@å,±¶àJEÀJA#äFå±· B@aRàBå±@x@ÇA AR¸àELl²aãÇâ û……aR¹ =áRîs€=áR"½ºà@Po€@å ±ƒ2ò ð"Ãåf”åe”"åNa áãB½»àb¼n€bàá½å±¼ B@jÀBå±@x@ßA$Aj½àEÒ'³ajáóá j††aj¾ =ájb+€=å"±¿à@¿*€@á $(ån"üAJÀàB BàÁÁJ$àB¥±Á B@JÀBå±@x@¿AAJÂàEYã€Ëàˆá J‡‡e±Ãà=Ú¤FëcÄ =áˆEé€{áˆ% å±Åà@”ä€@å ±ë câ óå±EA¨Æàbõá ᨠ£å ±Ç B@a¨ÀBÿÿ @x@ßA$A¨ÈàEàž´bóñˆ‰bóÉ =áj_µ`=ájå ±Êà@o]€@ÿ,ÿ ËàJÛ\ JA#å±Ì B@aRàBå±@x@ÇA ARÍàEí¶aâûŠŠaRÎ =áR´€=å"±Ïà@€@˜ÿ ÿ²$F!“èo“þ|ä%Oÿì>þÃï•fî•a ÈB½Ðàbq€bàá½å±Ñ B@jÀBŠüy@x@ßA$AjÒàEtÑà ˆä f‹‹ajÓ =ájüÔ€=â½%-ä(+cÔà@Y @â r!j @ãÅ¥±ÕàB¼Ó€ƒá Jå±Ö B@JàB@ÙâøöÇ@x@¿AAJ×àEûŒ·bµàËá JŒŒe±Øà=|Ÿ„FëcÙ =@®€Á÷’€{áˆ% áˆ(¥Úà@6Ž€@£â ó @âóå±*Ä!ëÛàb—€báÂó!¨ báë«cÜ B@`æÀBÿÿ÷@x@ßÙ =A¨Ý @€EH¸a¨ëcŽbóÞ =áj˜¹`=áj$áj"´ßà@ø€@â m' @öÇààJdÀJ â¸ëcá B@a àB4ôàBñ@x@ÇA ARâàE¿ Ó DÀˆâ ûaRã =áRFÇ€=å"±äà@ŸÂ€@â ½!Râ½£e"{zÇ‘÷åðÑõ ·à+¦B½åàb  bàá½å±æ B@jÀBÿÿL6@x@ßA$AjçàE{ºbãFá jajè =ájš~€=áj%pâ½#Åéà@û}€@à~Áå±êàBZàB$ ƒä‘ë B@JàB@Ùâøå±@x@¿AAJìàEœ6»aJáÓá J‘‘e±íà=I„Fÿÿ5nî =@®€Á<€{äF% áˆ%±ïà@Ø7€@ÿ ÿ â óå±TAëðàb8 báÁë!¨ £á¨X\yñ B@`æÀB è ¥!¨ ÿO*@x@ßA$A¨òàE#ò€ëå±’“bóó =ájB²¼aæáj$áj%±ôà@¢°€@å,±õàJÀJ þâ¸å±ö B@aRàBå±@x@ÇA AR÷àE0i½aàˆâ û””aRø =áRãp€=áR"½ùà@5l€@ë c ³ƒAîð£ïðÑõ£@ a .ÄB½úàb¡k€bä ©å±û B@jÀBëc@x@ßA$AjüàE·$¾áj••ajý =áj?(€=áj%-â½%±þà@œ'€@áéÃÅå±ÿàBû&àB$ ƒä…±CG@JÀBñ@x@¿AAJ#@€E>àà ˆâ µ––e±à=¿¤Få± =áˆ2æ€{áˆ% áˆ"óà@vá€@ë câ ó届6AëàbÚá Áë!¨ £á¨…± B@abÀB å±@x@ßA$A¨àEÅ›¿bóñ—˜bó =ájü[À`=è¥å ± à@[Z€@å,± àJÇY J èjå± B@aRàBå±@x@ÇA AR àEÒÁaàˆâ û™™aR =áRy€=â½áR"½à@Ö€@ÿ ÿ$X ÃðƒA£à±£ Dñ7a *ôB½àbB bàá½å± B@jÀBâ ½ÿÿ$X@x@ßA$AjàEY΀ëàˆá jššaj =ájéÑ€=áj%-ä(%±à@F @à~Áå±àB¥Ð€ƒà B$ ƒä…± B@JÀBå±@x@¿AAJàEà‰Âbµç…á J››e±à=aœ„Få± = ÀÁЀ{áˆ% áˆ"óà@‹€@è ¥/ª @îWÃâ ó屎Aëàb|Š€báÁë!¨ bᨅ± B@`æÀBÿÿÞ@x@ßA$A¨àEfEÃa¨ëcœbó =áj†Ä`=å"±à@é€@å,±àJUÀJA#å± B@aRàBxñ@x@Ç =AR! @`Et¼ Ó@ ˆâ ûžžaR" =áRÄ€=â½å ±#à@l¿€@å ±Ó+cñ,±oï%¯Càþ£àx@¸À¼ B½$àbؾ€bàá½å±% B@a$àBå±@x@ßA$Aj&àEúwÅbŒ@YájŸŸaj' =áj‹{€=â½%-ä(%±(à@èz€@á å±)àBFàB$ ƒä…±* B@JÀBå±@x@¿AAJ+àE3ÆaJàËâ µ  e±,à=F„F‚ÿÿ$X- =áˆu9€{âó% áˆ%±.à@½4€@ÿ ÿ â óå±þAë/àb báÂó!¨ £á¨…±0 B@a¨ÀBëc@x. 9A$A¨1 @€Eï€ëëc¡¢bó2 =áj(¯Çaæâóä^ <3 à@‹­€@å,±4àJ÷¬ J@Ý£½ÁRëc5 B@a àBxå±@x@ÇA AR6àEfÈaàˆâ û££aR7 =áÁm€=áR"½8à@i€@å ±ã΢çÎØø±Qåðäõa ÊgB½9àb‰h€bàáÁjå±: B@jÀBå±@x@ßA$Aj;àEœ!Éáj¤¤aj< =áj$%€=ä(%-ä(%±=à@‚$€@é wå±>àBä#€Bá J$ ƒä…±? B@JàB@Ùâø,¯ÿÿ/¼@x@¿AAJ@àE#Ýà ˆâ µ¥¥e±Aà=¤¤Fÿÿ§VB =@®€Áã€{áˆ% áˆ"óCà@_Þ€@å ±ë câ óå±n•AëDàb¿á Âó!¨ £á¨…±E B@`æÀBå±@x@ßA$A¨FàE©˜Êbóëc¦§AG =ájÒXË`=ä^ <H à@4W€@â m!j%ò€ÿ ÿ* IàJ V J@0â¸å±J B@aRàByå±@x@ÇA ARKàE·Ìaàˆâ û¨¨aRL = …€ƒo€=áR"½Mà@À@ÿ ÿê$F!Rî!K 4ó"S!û‹‚Šƒtð£ÿÿ$X£qB½Nàb/ bàá½å±O B@€æÀB‡ñ@x@ßA$AjPàE>Ë€ëàˆÿÿ i AX©©ajQ =@€ƒÆÎ€=ä(%-ä(%±Rà@  @å$±SàB‚Í€ƒàÁÁJ$ ƒä…±T B@€ÆÀBëc@x@¿AAJUàEĆÍ⵪ªe±Và=F™„Få±W = ÀÁ½Œ€{âó% áˆ"óXà@ˆ€@ ÿ m @âó屃AëYàb`‡€bᨠbᨅ±Z B@`æÀBÿÿh°@x@ßA$A¨[àEKBÎa¨å±«¬bó\ = …`ƒwÏ`=âó$áj <] à@Ú€@å,±^àJFÀJ å ±_ B@€ÎÀBå±@x@ÇA AR`àEY¹€ÓãÇä F­­aRa =á Á€=â½áR"½bà@i¼€@ëc5ð‹‚Šƒ££"«ª‘÷å±h÷B½càbÕ»€bá jå±d B@jÀBëc@x@ßA$AjeàEßtÐbàˆá j®®ajf =ájgx€=â½%-â½%±gà@Âw€@áéÅn' @ãÅ :@Bh àƒ# BàÁÀBå±i B@JÀBå±@x@¿AAJjàEf0ÑáJ¯¯e±kà=ç¤Fz3(ìé.ÿÿ–@l =áR6€{áˆ% áˆ"ómà@Ÿ1€@—å ±)ø @å±Ãâ óå±ÌA¨nàb bᨠbá륱o B@a¨ÀBÿÿ @x@ßA$A¨pàEíë€ëñ°±bóq =áj¬Òaæájä^ <r à@xª€@ÿ ÿš!j @ëcsàJä© JA#â4` ëct B@aRàBå±@x@ÇA ARuàEúbÓaáÛä F²²aRv =á§j€=å"±wà@öe€@ù …$F!Râ½%±ÿÕ ÍñËÑÍ£îð£a ƒlB½xàbb bàáÁjå±y B@jÀBŒëc@x@ßA$AjzàEÔáj³³aj{ =áj"€=â½%pä(%±|à@\!€@â r!j @ãÅ :@B} àƒ½ €Bá Jå±~ B@JÀBå±@x@¿AAJàEÚ€ˆàËâ µ´´e±€à=‰ì„Få± =áà€{áˆ% áˆ%±‚à@EÛ€@£å ±!ˆ @âóå±'žA¨ƒàb¤¡á¨ bá륱„ B@a¨ÀBÿÿ @x@ßA$A¨…àEŽ•Õbóëcµ¶bó† =áj»UÖ`=áj$áj <‡ à@T€@ë,cˆàJ‰S JA#e§äF屉 B@aRàBz((àBÿÿbþ@x@ÇA ARŠàEœ ×aàˆâ û··aR‹ =áP€=áR"½Œà@¬€@ë c#\s,ßÿ-/ñ7 ýà‘²B½àb bàá屎 B@jÀBÿÿ5n@x@ßA$AjàE#È€ëäf¸¸aj =áj§Ë€=áj%pâ½%±‘à@ @á $( @ãÅ :@B’ àƒgÊ€ƒàÁÁJ屓 B@JÀBã CöÇ@x@¿AAJ”àE©ƒØâµ¹¹e±•à=*–„FÿÿŠÜ– =ᙉ€{áˆ% áˆ"ó—à@ç„€@—å ±)ø @å±+câ óå±5@A¨˜àbE bá ë!¨ bá륱™ B@a¨ÀBÿÿ,@x@ßA$A¨šàE0?Ùa¨å±º»bó› =ájaÿ€=ê <œ à@Ãý€@â m!j%òëcàJ/ÀJA#属 B@aRàBëc@x@ÇA ARŸàE=¶ÚaRàˆå ±¼¼aR  =áî½€=â½!⽡à@J¹€@å ±3%¯ßÿ-.àþ£àà9RB½¢àb¶¸€bä fëc£ B@jÀBå±@x@ßA$Aj¤àEÄqÛáj½½aj¥ =ájPu€=áj%páj%±¦à@¯t€@áéÅnå±§àBàB$ ƒä–Ǩ B@JàB@Ùâøå±@x@¿AAJ©àEK-ÜáJ¾¾e±ªà=̤F{ñ« =@®€Á;3€{áˆ% áˆ%±¬à@ˆ.€@îW @å±.Wâ óå± Aë­àbçá Áë!¨ bᨖǮ B@`æÀBå±@x@ßA$A¨¯àEÒè€ëñ¿Àbó° =áj÷¨Ýaæå"±±à@X§€@â må±²àJĦ JA#â¸ëc³ B@aRàBëc@x@ÇA AR´àEß_Þaàˆå ±ÁÁaRµ =áR„g€=H@—àÆÿÿ޶à@ßb€@è o$F%òâ½Cxÿÿ-€ÿë@zÀ»ho·àbK bàá½å±¸ B@jÀBÿÿ5n@x@ßA$Aj¹àEfßájÂÂajº =ájæ€=áj%-ä(%±»à@A @á !j @ãÅ :@B¼ àƒ¢€ƒà B àB¥±½ B@JÀBñ@x@¿AAJ¾àEíÖ€ˆç…â µÃÃe±¿à=ré„FÿÿQèÀ =áÙÜ€{áˆ% áˆ%±Áà@"Ø€@ÿÿ,â óå±nWÂàb‰×€báÂó!¨ £á륱à B@a¨ÀBöÇ@x@ßA$A¨ÄàEs’àbóëcÄÅbóÅ =áj©Rá`=å"±Æà@ Q€@å,±ÇàJvP J äêå±È B@aRàBå±@x@ÇA ARÉàE âaàˆâ ûÆÆaRÊ =áR:€=áR"½Ëà@• €@å ±S$õ‚ä:õƒïðë$ a d DfÌàb bàá½å±Í B@jÀBëc@x@ßA$AjÎàEÅ€ëàˆÿÿ ÿ,ÇÇajÏ =ájÈ€=áj%pä(%±Ðà@ëÇ€@ã Åå±ÑàBLàB$ ƒä‹cÒ B@JÀBÿÿL6@x@¿AAJÓàEŽ€ãâµÈÈe±Ôà=“„FëcÕ =áˆ~†€{å"±Öà@È€@å ±!ˆ%Nè¥â óå±ÀùAë×àb* bâjÂó!¨ bᨋcØ B@a¨ÀBëc@x@ßA$A¨ÙàE<äa¨ëcÉÊbóÚ =áj;ü€=âó)Ïâó <Û à@œú€@å,±ÜàJÀJA#å±Ý B@aRàBå±@x@ÇA ARÞàE"³åaRàˆä FËËaRß =áܺ€=áR%±àà@6¶€@ñ  c:õƒåðäõë$%°`½ÀKy…áàb¢µ€bàá½å±â B@jÀBëc@x@ßA$AjãàE©næ`¬àˆá jÌÌajä =ájr€=ä(%-â½%±åà@yq€@å$±æàBÝp€BàÁÁJ$ ƒä…±ç B@JàB@Ùâøñ@x@¿AAJèàE0*çáJÍÍe±éà=±¤FöÇê =@®€Á 0€{áˆ% áˆ"óëà@j+€@—î W/ª @å±+câ óå±ïmìàbÌá ᨠbᨅ±í B@`æÀBå±@x@ßA$A¨îàE¶å€ëëcÎÏbóï = …`ƒô¥èaæájå ±ðà@U¤€@â m!j @öÇñàJÁ£ J â¸ëcò B@€ÎÀB{üy@x@ÇA ARóàEÄ\éaàˆä FÐÐaRô =áR†d€=å"±õà@Ø_€@óÓ!Râ½3Ósõƒ"ƒCtÆð£tÀðï$ÿa IYD©öàbD bàá½`b⽿ÿL6÷ B@jÀB‰ÿÿ,@x@ßA$AjøàEKêájÑÑajù =ájÓ€=â½%-ä(%±úà@/ @å$±ûàB€ƒá Jå±ü B@JÀBñ@x@¿AAJýàEÑÓà ˆâ µÒÒe±þà=Sæ„Fëcÿ =áˆÊÙ€{âó% áˆ%±Là@ Õ€@ÿ ÿ,â óå±í«AëàbmÔ€bᨠ£âó¥± B@a¨ÀB¤â ó'Zÿÿ* @x@ßA$A¨àEXëbóå±ÓÔbó =ájŠOì`=âó$áj!§à@ëM€@å,±àJWÀJ îå ± B@aRàBå±@x@ÇA ARàEfíaãÇâ ûÕÕaR =áR€=â½áR"½ à@f €@å ±ƒûä4ÿúë-ýê<üïÓ”@a %–B½ àbÒ€bá jå± B@jÀBå±@x@ßA$Aj àEìÁà ˆá jÖÖaj =áj|Å€=â½å ±à@ÙÄ€@á鯹/Œ @ãÅ"üAJàB8 Bá Jå± B@JÀBå±@x@¿AAJàEs}îbµàˆá J××e±à=ô¤Fÿÿ;  =áˆkƒ€{áˆ% äF"óà@®~€@å ±)ø @è¥+câ óå±dA¨àb báÁë!¨ ba“ ¥± B@a¨ÀBå±@x@ßA$A¨àEú8ïa¨ñØÙbó =ájù€=ájä^"´à@}÷€@‡@SíÑ!j @ëcàJéö JA#àJëc B@aRàBå±@x@ÇA ARàE°ðaRáÛâ ûÚÚaR =@þ€ƒ¹·€=áR"½à@³€@”î !$F!Râ½+c“$+a àú£à/±oå‚$ÿÿÿ¦èB½ àb{²€bàá½å±! B@€æÀBÿÿ§V@x@ßA$Aj"àEŽkñájÛÛaj# =ájo€=áj%pä(+c$à@jn€@á !j @ãÅ"ü@B%àBÊmàB$àB±& B@JÀBå±@x@¿AAJ'àE'òaJã>â µÜÜe±(à=–9ƒ|å±) =áˆ-€{áˆ% áˆ"ó*à@P(€@å ±!ˆ @âóå±§A¨+àb±¡á ë!¨ bá륱, B@a¨ÀB¨ëc@x@ßA$A¨-àE›â€ëëcÝÞbó. =áj¾¢óaæê"´/à@¡€@ë,c0àJŠ  JA#å±1 B@aRàBå±@x@ÇA AR2àE©Yôaÿÿ(ŸßßaR3 =áRca€=â½%pâ½4à@½\€@ü y£‚åƒ4ÿõƒà‚Œƒðíÿÿh°1ÿô5àb) bá jå±6 B@jÀB‚â ½ÿÿ/¼@x@ßA$Aj7àE0õajäfààaj8 =áj°€=áj%-áj%±9à@  @á $( @ãÅ"ü@B:àBl€ƒàÁÆü$àB¥±; B@JÀBå±@x@¿AAJ<àE¶Ð€ˆàËå ±ááe±=à=7ã„F}ÿÿÞ> =ሪր{áˆ% áˆ%±?à@ñÑ€@ü y)ø @å±+câ ó展ÿô@àbR báᨠbá륱A B@a¨ÀB â ó"ôñ@x@ßA$A¨BàE=Œöbóàˆá ¨ââbóC =áj߀=å"±Dà@A€@å,±EàJ­Ž J þå ±F B@aRàBÿÿ @x@ÇA ARGàEÄG÷áRããaRH =áR\O€=áR"½Ià@¼J€@ÿÿL65³€Ö"ÆÀàƒAðÆa åEDfJàb(Àb@$çå±K B@jÀB{â ½å±@x@ßA$AjLàEJøájääajM =ájÛ€=áj%-ä(%±Nà@5 @å nå±OàB—€ƒà Bå±P B@JÀBÿÿ,@x@¿AAJQàEѾ€ˆáÓä ååe±Rà=Ù¤Fów!^âÿÿ¦P/ AS = ) Á½Ä€{å"±Tà@À€@”î W%Nå±â óå±A·AëUàbm¿€báå±V B@a¨ÀBÿÿÞ@x@ßA$A¨WàEXzùbóñæçbóX =áj…:ú`=âóê(fYà@ç8€@å,±ZàJSÀJA#â4ÿÿ¦[ B@aRàBå±@x@ÇA AR\àEeñ Ó@Y ˆâ ûèèaR] =áRù€=áR%±^à@eô€@å ±ÃÁ%© BðåV´ @Á·@¸À0B½_àbÑó b@% áÁjaöèo¼y` B@jÀB|å±@x@ßA$AjaàEì¬ûbàˆá jééajb =ájx°€=ä(å ±cà@Ó¯€@å nå±dàB4 BàÁÁJå±e B@JÀBëc@x@¿AAJfàEshüáJêêe±gà=ô¤Fzëch =áˆ_n€{áˆ%pç:"óià@°i€@“å ±ë câ óå±+ÂBójàb bᨠ£âó«ck B@a¨ÀBå±@x@ßA$A¨làEú#ýa¨å±ëìbóm =ájä€=ájå ±nà@xâ€@å,±oàJäá JA#äFöÇp B@aRàBÿÿÞ@x@ÇA ARqàE›þaRàˆä FííaRr =áR´¢€=å"±sà@ž€@ÿ ÿÞÓ5áuð¤Åƒ%ðŃs5a hÓB½tàb{€bàáÄfå±u B@jÀBÿÿ @x@ßA$AjvàEŽVÿájîîajw =ájZ€=â½å ±xà@qY€@å$±yàBÒX€Bá Jå±z B@JÀBå±@x@¿AAJ{àE;@Tàˆâ µïïe±|à=–$ƒ|å±} =ሀ{âó% å±~à@R€@—å ±': @ëcâ óå±ÌÿqVàb°á Âó!¨ bå ±€ B@a¨ÀBÿÿÞ@x@ßA$A¨àE›Í€ëöÇðñbó‚ =ájÌaæâóå ±ƒà@.Œ€@å,±„àJš‹ JA#å±… B@aRàBå±@x@ÇA AR†àE©DaãÇâ ûòòaR‡ =áR]L€=áR(oˆà@¹G€@å ±ãü6·66L6a6na ØLE±‰àb% bá j届 B@jÀBå±@x@ßA$Aj‹àE/ajàˆá jóóajŒ =áj·€=ájå ±à@ @áéÃÅ$( @ãÅ.`AJŽàBs€ƒàÁÀB$àB¶Ç B@JÀBå±@x@¿AAJàE¶»à ˆá Jôôe±‘à=7΄F{ëc’ =ሦÁ€{áˆ% 屓à@ô¼€@å ±ë câ óå±|¾A¨”àbR bᨠ£áë«c• B@a¨ÀBå±@x@ßA$A¨–àE=wbóëcõöbó— =ájj7`=çå ±˜à@Ì5€@ÿ,ÿ,™àJ8ÀJA#屚 B@aRàBñ@x@ÇA AR›àEJî€ÓáÛâ û÷÷aRœ =áRëõ€=â½áR%±à@Nñ€@ñ óe°6¤Ñ»6ÈàÿÿL6Í¥B½žàbºð bA;áj屟 B@jÀByñ@x@ßA$Aj àEÑ©bàˆá jøøaj¡ =áj]­€=áj%-ä(6Ç¢à@¸¬€@à~Ånå±£àBàB層 B@JÀBå±@x@¿AAJ¥àEXeáJùùe±¦à=Ù¤Få±§ =áˆaàˆå ±aR =â½F€=äFáR"½à@vA€@ë c Csñw€oÑ»€kqÿÿ¡¤Â^B½àbâ@€bàáÄ©å± B@a$àB~â ½ÿÿB@x@ßA$Aj àEùùà ˆá jaj =ájyý€=â½å ± à@Öü€@å n$( @ãÅ"üAJ àB5 Bá J$àB¶Ç B@JÀBöÇ@x@¿AAJàE€µâµe±à=È„Få± = ÀÁp»€{áˆ% äF"óà@»¶€@—ÿ ÿÞ)ø @å±+câ óå±f¬A¨àb bᨠbáë± B@`æÀBœâ ó"ôüy@x@ßA$A¨àEqa¨å±bó =áj-1`=ájä^"´à@/€@å,±àJù. J þå ± B@aRàB|ÿÿ/¼@x@ÇA ARàEèà ˆä FaR =áRÂï€=áR"½à@ ë€@å ±Scq ±vuV€A|ƒa õÿêàbŒê€bá jå± B@jÀBå±@x@ßA$AjàE›£bàˆá jaj = …€ƒ'§€=áj%-ä(+c à@ƒ¦€@à~ƹå±!àBã¥àBå±" B@€ÆÀBå±@x@¿AAJ#àE!_áJe±$à=£¤Fÿÿx% =áˆe€{è¥% áˆ"ó&à@]`€@å ±!ˆ @å±+câ óå±Qÿ Ò'àb½á Áë!¨ bå ±( B@a¨ÀBå±@x@ßA$A¨)àE¨a¨ñbó* =ájÏÚ€=ájå ±+à@/Ù€@å,±,àJ›Ø JA#ïªëc- B@aRàBëc@x@ÇA AR.àE¶‘aRéyä FaR/ =áRX™€=äFáR"½0à@²”€@ÿÿ$X6c}…®€4%»a ÔD©1àb bá jå±2 B@jÀB‚è oëc@x@ßA$Aj3àEàEJÄ€ëëcbó? =ájy„"aæå"±@à@Ù‚€@å,±AàJE Jå(Ã>å±B B@aRàBå±@x@ÇA ARCàEW;#aÿÿ(aRD =áR C€=â½å ±Eà@k>€@ö Çs±+]Ñ\tÑÅ@8ààÁB½Fàb×= b@(çå±G B@jÀB{â ½å±@x@ßA$AjHàEÞöà ˆå ±  ajI =ájnú€=áj%-ä(+cJà@Ëù€@á å±KàB*àBå±L B@JÀBå±@x@¿AAJMàEe²$bµàËá J!!e±Nà=æÄ„Få±O =áˆM¸€{áˆ% áˆ(¥Pà@ ³€@ÿÿ Ò @ëcâ óå±eöAëQàb báÄ^!¨ bå ±R B@a¨ÀB”â ó"ôå±@x@ßA$A¨SàEëm%a¨ëc"#bóT = ``ƒ .&`=å"±Uà@j,€@å,±VàJÖ+ J äFå±W B@€ÎÀBñ@x@ÇA ARXàEùäà ˆâ û$$aRY =áR¸ì€=áR"½Zà@è€@ñ ƒ\r2\r-áwƒAå±¾ÿ|„[àbç€bá jå±\ B@jÀB~â ½å±@x@ßA$Aj]àE€ 'bç¥á j%%aj^ =áj¤€=ä(å ±_à@a£€@à~ÃÅå±`àBÀ¢€Bá J$ ƒäèFa B@JÀBå±@x@¿AAJbàE\(aJàˆá J&&e±cà=‡n„Få±d = ÀÁóa€{âó% å±eà@>]€@ÿ ÿ,â óå±ÿ‚lfàb¢á Áë!¨ £á¨ŸÿÞg B@`æÀB˜â ó"ôå±@x@ßA$A¨hàE)a¨å±'(bói =áj¹×€=ájê.jà@Ö€@å,±kàJˆÕ J þå ±l B@aRàBÿÿyÆ@x@ÇA ARmàEšŽ*aRàˆâ û))aRn =áRN–€=áR%±oà@«‘€@å ± “d p |}ö‘ ÿÿÞ_Hopàb bä ©å±q B@jÀBå±@x@ßA$AjràE!J+áj**ajs =áj©M€=H@—ì*e-ä(+ctà@ @á å±uàBeL€ƒâ,äå ±v B@JÀBå±@x@¿AAJwàE¨,áJ++e±xà=)ƒ|å±y =ሔ €{áˆ% áˆ"ózà@ä€@å ±!ˆ @ëcâ óå±¶­Aë{àbD bᨠbå ±| B@a¨ÀBå±@x@ßA$A¨}àE/Á€ëñ,-bó~ =ájV-aæájå ±à@º€@ÿ,ÿ¦€àJ% Já Rëc B@aRàByüy@x@ÇA AR‚àE<8.aàˆå ±..aRƒ =áRð?€=å"±„à@D;€@î!!“â½£~%®þQ 1ra ¢…B½…àb°:€bàáÄf屆 B@jÀB‡è oëc@x@ßA$Aj‡àEÃóà ˆá j//ajˆ =áj;÷€=â½%-ä(%±‰à@™ö€@å$±ŠàBÿõ€Bá J)º ƒä‹c‹ B@JÀBÿÿåü@x@¿AAJŒàEJ¯/bµàËá J00e±à=ˤFÿÿåüŽ =áˆBµ€{áˆ% áˆ%±à@†°€@Ÿÿÿ,â óå±wAëàbæá Âó!¨ £á¨‹c‘ B@a¨ÀB¤â ó"ôëc@x@ßA$A¨’àEÐj0a¨ëc12bó“ =áj+1`=áj$áj+c”à@c)€@ë,c•àJÏ( J þé1å±– B@aRàBå±@x@ÇA AR—àEÞáà ˆâ û33aR˜ =!J ƒ†é€=å"±™à@æä€@ö dzOàäõV"udÇ´t ŒàMB½šàbRÀb@&êœå±› B@jÀBöÇ@x@ßA$AjœàEd2bàˆá j44aj =ájí €=áj%-â½%±žà@K @äåÃÅ)Ú @ãÅ"ü@BŸàB­Ÿ€ƒà B$àB¥±  B@JàB@Ùá$ßÿô@x@¿AAJ¡àEëX3áJ55e±¢à=lk„Fÿÿ/¼£ =@®€ÁÛ^€{äF% áˆ%±¤à@(Z€@—å ±!ˆ @è¥â óå±²¿A¨¥àb‡Y€bâjÁë!¨ bá륱¦ B@`æÀBÿÿ,@x@ßA$A¨§àEr4a¨å±67bó¨ =áj¦Ô€=ájå ±©à@ Ó€@å,±ªàJuÒ JA#â¸å±« B@aRàBå±@x@ÇA AR¬àE‹5aRàˆä F88aR­ =áR+“€=áR"½®à@ƒŽ€@ë cÃð"ƒàÔ"ñpþÿÿWšU‰B½¯àbï€bàá½å±° B@jÀB„è oëc@x@ßA$Aj±àEG6áj99aj² =ájŽJ€=áj%-ä(%±³à@èI€@á $( @ãÅ¥±´àBJàB$àB¥±µ B@JÀBñ@x@¿AAJ¶àE7áJ::e±·à=ƒ|屸 =ሀ{áˆ% áˆ"ó¹à@Æ€@ÿ ÿL6ÿ ÿ â óå±ñ{Aëºàb) bá ë!¨ £á륱» B@a¨5€¥ â ó"ôëc@x@ßA$A¨¼àE¾€Eñ;>ajÇ =áj8ô€=áj%-ä(%±Èà@’ó€@á å±ÉàBôòàB$ ƒä‘Ê B@JÀBå±@x@¿AAJËàE.¬:bµÿÿâ??e±Ìà=°¤Få±Í =áˆ'²€{áˆ% áˆ"óÎà@o­€@ÿ ÿÞ!ˆ @ëcâ óå±b AëÏàbΡá ë!¨ bᨑРB@a¨ÀB¡å±@x@ßA$A¨ÑàEµg;a¨ëc@AbóÒ =ájÞ'<`=å"±Óà@@&€@å,±ÔàJ¬% JA#ëcÕ B@aRàBå±@x@ÇA ARÖàEÃÞ€ˆîèä FBBaR× = …€ƒoæ€=â½å ±Øà@Çá€@å ±ã"æüæý¨äöö®¯ Œà™’B½Ùàb3 bá j`£â½¿ÿF„Ú B@€æÀBå±@x@ßA$AjÛàEIš=bàˆá jCCajÜ =ájÉ€=H@—â1üyÝà@$ @à~ƹ$(ån«cÞàB‰œ€ƒà B$àB¥±ß B@JàB@ÙàÆ)ºñ@x@¿AAJààEÐU>áJDDe±áà=Qh„Fÿÿh°â =@®€ÁÄ[€{áˆ% å±ãà@ W€@å ±ë câ óå±=`BóäàblV€bᨠ£âó¥±ßÿô«`æÀBëc@x@ßA$A¨æ ˆ@€EW?a¨ëcEFbóç =áj„Ñ€=å"±èà@æÏ€@â m!j$Ÿÿÿ$XéàJR Jå(Ã> `JáR-ˆ­ê B@a àBå±@x@ÇA ARëàEdˆ@aRáÛä FGGaRì =áR€=áR(oíà@p‹€@ñ ó± kk€aña B½îàbÜŠ€bàáÁjå±ï B@jàBè oñ@x@ßA$AjðàEëCAájHHajñ =ájsG€=ä(%-ä(+còà@ÎF€@â rå±óàB/ Bá Jå±ô B@JÀBëc@x@¿AAJõàErÿ€Ëàˆâ µIIe±öà=ó¤Fzÿÿ/¼÷ =áˆnBaÆáˆ% áˆ"óøà@¯€@¤öÇâ óå±²êAëùàb báÂó!¨ £ä^¥±ú B@a¨ÀB¨â ó"ôñ@x@ßA$A¨ûàEøºBa,ëcJKbóü =áj&{C`=ä^(fýà@‡y€@å,±þàJóx J þèjå±ÿ B@aRàBzÿÿ,@x@ÇA AR¿ÿÃ`E2Daàˆâ ûLLaR =áR³9€=áR"½à@5€@”â ½'1 ±@# K 7Ñ "€Ö¨æü à&VB½àbr4€bàá½å± B@a$àBâ ½å±@x`ß =Aj @`E퀈å2á jMMaj =ájñ€=ä(å ±à@xð€@à~Áå±àBÙï€Bá J$ ƒäˆg B@aàBå±@x@¿AAJ àE©EâµNNe± à=”»„Fÿÿ/¼ =ሯ€{âó% å± à@Uª€@ â ó)ø @âóÃâ ó层¤Bóàb³á Áë!¨ bᨑ B@a¨ÀBÿÿ,@x@ßA$A¨àEšdFa¨å±OPbó =ájË$G`=âó$áj%±à@-#€@å,±àJ™" JA#îå± B@aRàBå±@x@ÇA ARàE¨Û€ˆãÇä FQQaR =áR]ã€=â½áR%±à@¸Þ€@â ½$F @â½%±æýL"Óí”ÿìd€”‡"ÿÿÔæ«ÿêàb$ bàá½å± B@jÀB€å±@x@ßA$AjàE.—Hbáóá jRRaj =áj²š€=â½%-â½+cà@ @á !j @ãÅ"ü@BàBn™€ƒàÁÁJ$àB¥± B@JÀBå±@x@¿AAJàEµRIáJSSe± à=6e„Få±! =ሡX€{áˆ% áˆ"ó"à@ëS€@›â ó @âóå±ÿ Ò#àbQ bᨠbá륱$ B@a¨ÀB˜â ó(¦ëc@x@ßA$A¨%àE<Ja¨ñTUbó& =ájm΀=áj$áj"´'à@ÏÌ€@‡@Såf' @ñ(àJ;ÀJ þàJå±) B@aRàBå±@x@ÇA AR*àEI…KaRáÛä FVVaR+ =@þ€ƒ€=å"±,à@eˆ€@œâ ½!Râ½#dÇÅtÿàTÿïMÿÿ,Áÿ` -àbч€bàáÄfå±. B@€æÀB‘â ½ëc@x@ßA$Aj/àEÐ@LájWWaj0 =áj`D€=â½%-â½C1à@¼C€@â rå±2àB Bá Jå±3 B@JÀBå±@x@¿AAJ4àEWüà Ëâ µXXe±5à=ؤFå±6 = ÀÁSMaÆáˆ% áˆ%±7à@‘ý€¼§â ó$F @âó+câ óå±eÿeò8àbóá Âó!¨ bå ±9 B@`æÀBñ@x@ßA$A¨:àEÝ·,ëcYZbó; =áj÷wNáj$áj%±<à@Xv€@ë,c=àJÄu JA#åüå±> B@aRàBÿÿ$X@x@ÇA AR?àEë.Oaàˆâ û[[aR@ =áR˜6€=â½áR"½Aà@ó1€@Œâ ½$F @â½+c3ë$"%¬ ÄøTÈhÿ­à:FD©Bàb_ bàá½å±C B@jÀBå±@x@ßA$AjDàEqê€ëàˆá j\\ajE =ájöí€=çå ±Fà@Q @à~Á!j @ãÅ"üAJGàB¶ì€ƒå ô$àB«cH B@JàB@Ùâø&ýüy@x@¿AAJIàEø¥Pâµ]]e±Jà=y¸„Fÿÿ/¼K =@®€Áè«€{âó% äF"óLà@3§€@—â ó @âó属´A¨Màb”¦€bá ë!¨ báë«cN B@`æÀBœâ ó!¨ëc@x@ßA$A¨OàEaQa¨ÿÿàJ^_bóP =ájµ!R`=âó$áj"´Qà@ €@å,±RàJ‚ J þâ¸å±S B@aRàBëc@x@ÇA ARTàEŒØà ˆä F``aRU =áR2à€=â½áR"½Và@ŒÛ€@å ±C"V¼æÃ”Qx"a èÿe¼WàbøÚ€bá jå±X B@jÀBÿÿ/¼@x@ßA$AjYàE”Sbàˆá jaaajZ =áj£—€=áj%-â½+c[à@ÿ–€@à~å±\àB_àB$ ƒä–Ç] B@JÀBëc@x@¿AAJ^àEšOTáJbbe±_à=bQ`=|üy` =ሊU€{áˆ% áˆ"óaà@ÔP€@˜å ±ë câ óå±L0Aëbàb6 bá ë!¨ £á¨–Çc B@a¨ÀBå±@x@ßA$A¨dàE! UajñcdAe =ájKË€=å"±fà@«É€@å,±gàJÀJ èjå±h B@aRàBëc@x@ÇA ARiàE.‚VaRàˆä FeeaRj =áRЉ€=áR"½kà@*…€@”â ½)ø%òèoStÿ&öt6öC!~øå±–ÿ!dlàb–„€bàáÄ©å±m B@jÀBëc@x@ßA$AjnàEµ=Wájffajo =ájAA€=ä(%-ä(%±pà@@€@á !j @ãÅ(®@BqàBý?àB$àB¥±r B@JÀBå±@x@¿AAJsàE;ùà ˆâ µgge±tà=½¤Fñu =áˆ0ÿ€{áˆ% áˆ"óvà@rú€@ü y!ˆ @âóå±2_A¨wàb×á Âó!¨ bá륱x B@a¨ÀB¡ëc@x@ßA$A¨yàE´Xbóñhibóz =ájítY`=ä^"´{à@Ms€@å,±|àJ¹r JA#å±} B@aRàBå±@x@ÇA AR~àEÐ+Zaîèâ ûjjaR =áRr3€=áR"½€à@Ì.€@ü yc¨tE³ø…³ààB½àb8 bá j层 B@jÀBå±@x@ßA$AjƒàEVç€ëàˆá jkkaj„ =ájÚê€=ä(%-ä(%±…à@7 @áéÃÅ$( @ãÅ"ü@B†àB–逃àÁÀB$àB¥±‡ B@JÀBå±@x@¿AAJˆàEÝ¢[bµàˆá Jlle±‰à=^µ„FëcŠ =áˆѨ€{áˆ% áˆ"ó‹à@¤€@ù »ë câ óå±¼A¨Œàby£€báᨠ£á륱 B@a¨ÀB å±@x@ßA$A¨ŽàEd^\a¨ëcmnbó =áj“]`=ä^"´à@ó€@å,±‘àJ_ Jã >ëc’ B@aRàBå±@x@ÇA AR“àEqÕ Ó@Y¦:â ûooaR” =áR,Ý€=áR"½•à@Ø€@ë csÿ"ƒà$Yøæp9åy@¸ÀúB½–àbí×€bàá½å±— B@jÀB†â ½üy@x@ßA$Aj˜àEø^bàˆá jppaj™ =ájt”€=ä(%-ä(%±šà@Ñ“€@â rå±›àB4 BàÁÁJ屜 B@JàB@Ù¢oÀB!K @'@x@¿AAJàEL_áJqqe±žà=_„FöÇŸ = 5 ÁsR€{áˆ% áˆ"ó à@ºM€@ÿ ÿ,â óå±^ëAë¡àb bᨠ£å ±¢ B@A€ÀBå±@x@ßA$A¨£àE`a¨ëcrsbó¤ =áj8È€=áj/áj%±¥à@˜Æ€@â m`@ÿÿ,¦àJÀJ â¸å±§ B@aRàBÿÿ; @x@ÇA AR¨àEaaRàˆä FttaR© =áRʆ€=å"±ªà@#‚€@ë cƒp5ñô.Æ€ ƒa ¨B½«àb€bàáÄ#屬 B@jÀBöÇ@x@ßA$Aj­àEš:bajå2á juuaj® =áj*>€=â½å ±¯à@‡=€@à~Áå±°àBæ<€Bá Jå±± B@JÀBëc@x@¿AAJ²àE öà ˆá Jvve±³à=¡¤Fÿÿ5n´ = ÀÁü€{âó% äF%±µà@\÷€@å ±$F @ñÃâ óå±§ZBó¶àb¼á Áë!¨ bå ±· B@`æÀBå±@x@ßA$A¨¸àE§±cbóå±wxbó¹ =ájÒqd`=âóå ±ºà@2p€@ˆ@Såf!j @å±»àJžo J àJå±¼ B@aRàBå±@x@ÇA AR½àEµ(eaãÇâ ûyyaR¾ =@þ€ƒd0€=â½!⽿à@Å+€@ü y“àdp!E°Ua ©ÿôÀàb1 bàá½å±Á B@€æÀBzëc@x@ßA$AjÂàE;ä€ëˆ@Y¡óá jzzajà =ájËç€=â½å ±Äà@) @á å±ÅàB‡æ€ƒàÁÁJ$ ƒäœyÆ B@JÀBå±@x@¿AAJÇàEŸfbµàˆá J{{e±Èà=C²„Få±É =ሲ¥€{áˆ% âóÊà@þ €@—ù » @å±Kâ óå± ãE±Ëàb^ báᨠb᨜yÌ B@a¨ÀBüy@x@ßA$A¨ÍàEI[ga¨ñ|}bóÎ =áj|h`=ä^+cÏà@Ü€@‡@S£rÁå±ÐàJHÀJA#àJå±Ñ B@aRàBñ@x@ÇA ARÒàEVÒ€ÓŒ@Y ˆâ û~~aRÓ =@þ€ƒùÙ€=â½å ±Ôà@ZÕ€@ˆâ ½ @â½£2$Šâ@äõzT@¸ÀêõB½ÕàbÆÔ b@% áÁµå±Ö B@€æÀBÿÿ5n@x@ßA$Aj×àEÝibájajØ =áju‘€=áj%-ä(1Ùà@Ò€@á å±ÚàB1àBå±Û B@JÀBå±@x@¿AAJÜàEdIjaJã>â µ€€e±Ýà=å¤Fÿÿ,Þ =áˆPO€{áˆ% áˆ+cßà@ J€@“â ó$F @âó+câ óå±NAëààb báÂó!¨ bå ±á B@a¨ÀBÿÿB@x@ßA$A¨âàEêka¨ëc‚bóã = ¾€ƒ Å€=å"±äà@mÀ@ë,cåàJÙ J äF屿 B@aRàBÿÿF„@x@ÇA ARçàEø{laRàˆâ ûƒƒaRè =áR¯ƒ€=â½%pâ½éà@ €@ë c³˵zuy"õ‚ä4‚õƒÿÿ,IìB½êàbx~€bàá½å±ë B@jÀBëc@x@ßA$AjìàE~7máj„„ají =áj ;€=â½å ±îà@h:€@á å±ïàBÇ9€Bä ƒä‹cð B@JÀBÿÿh°@x@¿AAJñàEó€ˆàËâ µ……e±òà=†ƒ|~å±ó =áˆõø€{âó â óôà@Aô€@ÿ ÿÞ!ˆ @å±+câ óå±íBóõàb¡á Âóå±ö B@a¨ÀBëc@x@ßA$A¨÷àEŒ®nbó屆‡bóø =áj¸no`=âóä^+cùà@m€@ˆ@Sé­' @ñúàJ‡l JA#àJå±û B@aRàBå±@x@ÇA >üàE™%paàˆâ ûˆˆaRý =@þ€ƒI-€=áR(oþà@¢(€@è o"½!Râ½ÃS"ƒà"ƒ,îð£ïða ÕÇB½ÿàb  bàá½å±O  €æÀBƒå±@x@ßA$AjàE á€ëàˆá j‰‰aj =áj¤ä€=ä(%-ç+cà@ @å n!j @ån"ü@BàBd〃àÁÁJ$àB¥± B@JàB@Ùâøüy@x@¿AAJàE§œqⵊŠe±à=(¯„Fyå± =@®€ÁŸ¢€{áˆ%páˆ"ó à@ã€@ÿ ÿÞ$F @âó+câ óå±q5A¨ àbC bᨠbáë± B@`æÀBÿÿ$X@x@ßA$A¨ àE.Xra¨ñ‹Œbó =ájVs`=ä^"´à@¸€@â m!j$^å±àJ$ÀJA#â¸å± B@aRàBÿÿ/¼@x@ÇA ARàE;Ï Ó@Y ˆä FaR =áRçÖ€=áR"½à@3Ò€@ÿ ÿê$F!Râ½(oÓ"ð£à"åHñh*B½àbŸÑ€bàáÄfå± B@jÀBå±@x@ßA$AjàEŠtbàˆá jŽŽaj =áj>Ž€=ä(%-ä(%±à@˜€@å$±àBúŒ€BàÁÁJå± B@JÀBñ@x@¿AAJàEHFuáJe±à=ʤFå± =áˆIL€{áˆ% áˆ"óà@…G€@¨è ¥ @âóå±ÌØAëàbå¡á ë!¨ bå ± B@a¨ÀB¬â ó'Zÿÿô@x@ßA$A¨!àEÏva¨ëc‘bó" =áj€=ä^%±#à@bÀ€@å,±$àJο J þÅ(Âûå±% B@aRàBå±@x@ÇA AR&àEÝxwaRáÛä F’’aR' =!J ƒ€€=áR"½(à@í{€@ö ÇãpÃåK”€åJ”P åP Œà =B½)àbYÀb@&¦“Ájå±* B@jÀBöÇ@x@ßA$Aj+àEc4xajàˆá j““aj, =ájì7€=ájä(%±-à@F @áéÁ' @ãÅ"ü@B.àB§6€ƒàÁÀB$àB«c/ B@JÀBå±@x@¿AAJ0àEêïà ˆá J””e±1à=kƒ|ÿÿ5n2 =áˆÎõ€{å"±3à@#ñ€@ö Çâ óå±®ÏA¨4àb†ð€bá¨å ±5 B@a¨ÀBÿÿyÆ@x@ßA$A¨6àEq«ybóëc•–bó7 =áj¡kz`=áj)Ïç"´8à@j€@å,±9àJpi€Jã >å±: B@aRàBå±@x@ÇA AR;àE~"{aáÛâ û——aR< =áR*€=äFáR%±=à@r%€@å ± ó0åuP "uPKaÿÿ²º›ÿ¾>àbÞ$ bA;å±? B@jÀBÿÿ§V@x@ßA$Aj@àEÞà ˆá j˜˜ajA =ájá€=â½%-â½%±Bà@èà€@â rå±CàBI Bá Jå±D B@JÀBå±@x@¿AAJEàEŒ™|âµ™™e±Fà= ¬„Få±G =áˆ|Ÿ€{áˆ% áˆ"óHà@Éš€@—ëcâ óå±?dAëIàb( bᨠ£ç±J B@a¨ÀBöÇ@x@ßA$A¨KàEU}a¨ëcš›bóL =áj?~`=ájä^%±Mà@¡€@å,±NàJ ÀJA#äFå±O B@aRàBÿÿ$X@x@ÇA ARPàE Ì Ó@Y ˆä FœœaRQ =áRÔÓ€=áR"½Rà@0Ï€@üy8MåJ$þ``$`¢@¸À±™B½SàbœÎ€bàáÊaö⽿ÿF„T B@jÀBÿÿ5n@x@ßA$AjUàE§‡bå2á jajV =áj‹€=ájå ±Wà@zŠ€@à~Áå±XàB߉€Bá Jå±Y B@JàB@Ù¢oÀ…/lÿÿB@x@¿AAJZàE-C€aJàˆá Jžže±[à=®¤FwöÇ\ =@®€ÁI€{è¥% å±]à@kD€@å ±`@ñÃâ óå±kBó^àbÉá Áe!¨ £âó¥±_ B@`æÀBå±@x@ßA$A¨`àE´þ€ë屟 bóa =ájݾaæájå ±bà@?½€@â m!j @öÇcàJ«¼ J â¸å±d B@aRàBëc@x@ÇA AReàEÂu‚aãÇâ û¡¡aRf =áRv}€=äF!â½gà@Òx€@å ±|}N€$ `T€Tå'Wà%qhàb> bàá½å±i B@jÀBå±@x@ßA$AjjàEH1ƒajáóá j¢¢ajk =ájÐ4€=ájå ±là@/ @à~Áå±màB3€ƒà B$ ƒäŸÿ,n B@JàB@Ù `À…å±@x@¿AAJoàEÏìà ˆá J££e±pà=Tÿ„Få±q =@®€Á¿ò€{H °àÁe âórà@ î€@ÿÿ,â óå±ÏE±sàbkí€bá¨å ±t B@`æÀBå±@x@ßA$A¨uàEV¨„bóñ¤¥bóv =áj{h…`=å"±wà@Ýf€@‡@Sâmå±xàJIÀJA#àJå±y B@aRàBå±@x@ÇA ARzàEc†aáÛâ û¦¦aR{ =@þ€ƒ'€=â½å ±|à@c"€@å ±#``$þ`$pq|a ;&B½}àbÏ!€bàáÄ#`£äf«c~ B@€æÀB}ù …ÿÿòË@x@ßA$AjàEêÚà ˆá j§§aj€ =ájzÞ€=áj%-ä(1à@ÕÝ€@á 层àB6àB屃 B@JÀBñ@x@¿AAJ„àEq–‡bµã>á J¨¨e±…à=ò¤Fÿÿ* † =áˆYœ€{áˆ% áˆ.W‡à@¦—€@å ±$F @ëcâ ó屟ÔAëˆàb  báÂó!¨ bâó«c‰ B@a¨ÀB”â ó"ôüy@x@ßA$A¨ŠàE÷Qˆa¨âtá ¨©©bó‹ =áj˜V€=å"±Œà@÷T€@è !j$ŸëcàJcÀJ þå ±Ž B@aRàBÿÿô@x@ÇA ARàE~ ‰áRªªaR =áR9€=áR"½‘à@’€@ÿÿ¦83}¤€<|}¨€4屟kc’àbþ€bá j屓 B@jÀBÿÿ¦@x@ßA$Aj”àEÉà ˆâ ½««aj• =ájÌ€=ä(å ±–à@îË€@ã Åå±—àBM Bá J$ ƒä‹c˜ B@JàB@Ùèª$ñ@x@¿A>™àEŒ„ŠbµàËá J¬¬e±šà=“RƒÇöÿÿÃЛ =@®€Á€Š€{áˆ% 屜à@Ç…€@œöÇâ óå±;kcàb( bá属 B@`æÀB å±@x@ßA$A¨ŸàE@‹a¨ñ­®bó  = ƒ1Œ`=áj%páj/o¡à@‘þ€~â må±¢àJýý J â¸ëc£ B@aRàBå±@x@ÇA AR¤àE ·àˆâ û¯¯aR¥ =áRϾ€=å"±¦à@(º€@å ±C}º€,|}Ô€$åaÍÀçHo§àb”¹€bàá½å±¨ B@jÀBå±@x@ßA$Aj©àE¦rbàˆá j°°ajª =áj3v€=áj%-â½+c«à@u€@ã Å屬àBît€BàÁÁJå±­ B@JÀBëc@x@¿AAJ®àE-.ŽáJ±±e±¯à=®¤Fÿÿ¦° =áˆ4€{bxæsñ±à@i/€@›å ±$F%òëcâ óå±CúAë²àbÉá ᨠbçZŸÿ¦³ B@a¨ÀBñ@x@ßA$A¨´àE´é€ëñ²³bóµ =ájë©aæájå ±¶à@K¨€@å,±·àJ·§ JA#óÎ屸 B@aRàBå±@x@ÇA AR¹àEÁ`aàˆä F´´aRº =áRmh€=äFáR+c»à@Íc€@ÿÿ,8STdpI2ÅPDåJ´!a ãSB½¼àb9Àb@$ájå±½ B@jÀB{è oñ@x@ßA$Aj¾àEH‘ájµµaj¿ =ájØ‘`=â½å ±Àà@6 @å$±ÁàB”€ƒá Jå±Â B@ àBå±@x@¿AAJÃàEÏ×à ˆâ µ¶¶e±Äà=Pê„Få±Å =ሻ݀{áˆ% ç:"óÆà@ Ù€@ÿ ÿÞ': @å±â óå±G¡BóÇàbkØ€bᨠbå ±È B@a¨ÀBÿÿ¦@x@ßA$A¨ÉàEU“’bµëc·¸bóÊ =ájaS“`=ájä^+cËà@ÄQ€@å,±ÌàJ0ÀJA#äFå±Í B@aRàBöÇ@x@ÇA ARÎàEc ”aàˆâ û¹¹aRÏ =áR €=áR"½Ðà@g €@ö ÇcåMuð¤$rõ‚ä4­‚ÿÿ]L]B½ÑàbÓ €bàáÈoå±Ò B@jÀBÿÿ¦@x@ßA$AjÓàEêÅà ˆá jººajÔ =ájfÉ€=ájå ±Õà@ÃÈ€@á $( @ãÅ"üAJÖàB"àBå±× B@JÀBå±@x@¿AAJØàEp•âµ»»e±Ùà=ò¤Fuÿÿ,Ú =áˆa‡€{è¥% å±Ûà@­‚€@—ëcâ óå±ùàA¨Üàb  bá ë!¨ £áë«cÝ B@a¨ÀBëc@x@ßA$A¨ÞàE÷<–a¨å±¼½bóß =áj#ý€=ájå ±àà@†û€@å,±áàJòú J å±â B@aRàBÿÿ¦@x@ÇA ARãàE´—aRéyä F¾¾aRä =áR¸»€=äFáR%±åà@·€@ñ s ~€&+^"$åM%à%àÚPB½æàb}¶€bá jå±ç B@jÀBÿÿ¦@x@ßA$AjèàE‹o˜ajÿÿ* ¿¿ajé =ájs€=áj%pä(1êà@ur€@á å±ëàB×qàB$ ƒä–Çì B@JàB@ÙâøöÇ@x@¿AAJíàE+™aJàËâ µÀÀe±îà=“=ƒ|ÿÿ$Xï =@®€Á1€{áˆ% áˆ"óðà@^,€@ÿ ÿ¦!ˆ @ëcâ óå±~¢Aëñàb¾á È¥!¨ bá¨XFò B@`æÀBÿÿ¦@x@ßA$A¨óàE™æ€ëñÁÂbóô =ájͦšaæå"±õà@0¥€@ÿ,ÿ,öàJœ¤ JA#â¸ëc÷ B@aRàBå±@x@ÇA ARøàE¦]›aâûÃÃaRù =áRNe€=â½å ±úà@ª`€@ë cƒà$¶k^­ÿå±¶éB½ûàb bàá½å±ü B@jÀBÿÿÞ@x@ßA$AjýàE-œájÄÄajþ =áj½€=áj%-ä(%±ÿà@ @á $( @ãÅ«cPàƒy€ƒà B$àB¥± B@JÀBëc@x@¿AAJàE´Ô€ˆã>å ±ÅÅe±à=5ç„Fÿÿ¦ =ሤڀ{áˆ% áˆ%±à@ðÕ€@ÿ ÿ,â óå±AëàbP bᨠ£á륱 B@a¨ÀBëc@x@ßA$A¨àE:bóëcÆÇbó =ájgPž`=å"± à@ÉN€@è 82$Ÿÿÿ, àJ5ÀJA#å± B@aRàByÿÿnb@x@ÇA AR àEHŸaàˆâ ûÈÈaR =áRì€=áR"½à@H €@å ±“%±´e±õƒ¥4Z@ýàÑB½àb´ €bàáå± B@jÀBñ@x@ßA$AjàEÏ€ˆäfÉÉaj =ájWÆ€=ä(%-ä(%±à@±Å€@à~Áå±àBàBå± B@JÀBå±@x@¿AAJàEU~ âµÊÊe±à=Ö¤Fÿÿô =áˆA„€{áˆ% áˆ"óà@Ž€@å ±!ˆ @ëcâ óå±uäAëàbñá Áë!¨ bå ± B@a¨ÀBöÇ@x@ßA$A¨àEÜ9¡a¨å±ËÌbó =ájùù€=ä^(fà@[ø€@å,± àJÇ÷ J å±! B@aRàBå±@x@ÇA AR"àEé°¢aRàˆå ±ÍÍaR# =áR¦¸€=áR"½$à@ò³€@ÿÿê'â½£%ä“üt“ý"õƒä“þta 9?B½%àb^ bä ©ëc& B@jÀBÿÿ* @x@ßA$Aj'àEpl£ájÎÎaj( =ájðo€=ä(å ±)à@K @á $( @ãÅ"üAJ*àB¬n€ƒâ,äàB«c+ B@JÀBå±@x@¿AAJ,àE÷'¤áJÏÏe±-à=x:ƒ|å±. =áˆó-€{áˆ% å±/à@4)€@£öÇâ óå±ÍA¨0àb“(€bᨠ£áë«c1 B@a¨ÀBÿÿh°@x@ßA$A¨2àE~〈¼ Z¤çä^iqÐÑbó3 =áj®£¥aæáj$áj"´4à@¢€@å,±5àJ|¡ J ÈÀÉÂûñ6 B@aRàBz@ó¡ ÀBÿÿ× @x@ÇA AR7àE‹Z¦aàˆáRA@ÒÒaR8 =@€ƒ8b€=å"±9à@—]€@ÿ ÿ,³“"äõVåHT`$à`%$ à‚|y:àbÀb@&á'å±; B@€æÀBÿÿ$X@x@ßA$Aj<àE§ájÓÓaj= =áj†€=â½%-â½+c>à@á€@å$±?àBF Bá J)º ƒäÿ§V@ B@JàB@ÙáöÇ@x@¿AAJAàE™Ñà Ëâ µÔÔe±Bà=ä„FvüyC =@®€Á…×€{áˆ% áˆ(¥Dà@ÖÒ€@ÿ ÿ,â óå±&|yEàb5 bá ë!¨ £á¨–ÇF B@`æÀBëc@x@ßA$A¨GàE¨âóÕÕbóH =áj¯‘€=ä^%±Ià@€@â mñJàJ JA#â¸å±K B@aRàBwå±@x@ÇA ARLàE¦H©áRÖÖaRM =áR@P€=â½ç"½Nà@šK€@ü yÃpSåITðpMåI“ÿåa kcOàb bá jå±P B@jÀBÿÿ @x@ßA$AjQàE-ªáj××ajR = …€ƒ±€=áj%-ä(%±Sà@  @á  `@ãÅ"ü@BTàBm€ƒà B$ ƒàB¥±U B@€ÆÀBëc@x@¿AAJVàE³¿à ˆå ±ØØe±Wà=»¤FÿÿBX =ሠŀ{äF% áˆ"óYà@ïÀ€@—ëcâ ó屺kcZàbO bᨠ£á¨…±[ B@a¨ÀBå±@x@ßA$A¨\àE:{«bóñÙÚbó] = …`ƒ];¬`=ájä^"´^à@½9€@å,±_àJ)ÀJ å ±` B@€ÎÀBÿÿL6@x@ÇA ARaàEHò€Óçâ ûÛÛaRb =áRþù€=äFáR"½cà@\õ€@ö Ç ÓHT€op?åI%à$òVÐa ¦DfdàbÈô€bá jå±e B@jÀBÿÿ­@x@ßA$AjfàEέ­bàˆá jÜÜajg =ájV±€=áj%-ä(%±hà@³°€@à~Í“å±iàBàBå±j B@JàB@Ùèªëc@x@¿AAJkàEUi®áJÝÝe±là=Ö¤Füym =@®€ÁEo€{áˆ% áˆ"ónà@j€@ö Çâ ó屦TAëoàbñá Áë!¨ £å ±p B@`æÀBüy@x@ßA$A¨qàEÜ$¯a¨ñÞßbór =ájå€=å"±sà@gã€@‡@SâmëctàJÓâ JA#àJëcu B@aRàBå±@x@ÇA ARvàEé›°aRãÇä FààaRw =@þ€ƒœ£€=áR"½xà@ùž€@å ±ã€*kd,+dp“ÿÿxÉõB½yàbe bàá½å±z B@€æÀBå±@x@ßA$Aj{àEpW±ájááaj| =ájèZ€=ä(å ±}à@I @â rå±~Pàƒ¨Y€ƒá J$ ƒäq B@JàB@Ùâøå±@x@¿AAJ€àE÷²áJââe±à=x%ƒ|ÿÿh°‚ =@®€Áç€{áˆ% 屃à@2€@å ±$F @ù»Ãâ óå±mBó„àb“€bᨠbᨋc… B@`æÀBå±@x@ßA$A¨†àE}΀ˆëcãäbó‡ =áj©Ž³aæä^+cˆà@€@â m!j*Qÿÿ,‰àJtŒ JA#â¸å±Š B@aRàBÿÿô@x@ÇA AR‹àE‹E´aàˆå ±ååaRŒ =áR>M€=áR%±à@—H€@ñ ó6Á€µ ÿ¿ua ½ÿ›òŽàb bàáå± B@jÀBñ@x@ßA$AjàEµajå2á jææaj‘ =ájž€=ä(%-ä(+c’à@û€@à~Á屓àBZ Bá Jå±” B@JÀBñ@x@¿AAJ•àE˜¼€Ëàˆá Jççe±–à=Ï„Fÿÿô— =ህ€{âó% áˆ"ó˜à@н€@ÿm @å±Kâ óå±`Aë™àb4 báÁë!¨ bå ±š B@a¨ÀBñ@x@ßA$A¨›àEx¶bóå±èébóœ =ájR8·`=âó%páj%±à@²6€@å,±žàJÀJA#屟 B@aRàBå±@x@ÇA AR àE,ï Ó  ˆâ ûêêaR¡ =áRÜö€=â½áR"½¢à@=ò€@üy9ðåI¤$Põ‚åð4­ª@¸ÀÝB½£àb©ñ€bä ©ëc¤ B@jÀBzÿÿ¦@x@ßA$Aj¥àE³ª¸bájëëaj¦ =ájC®€=â½å ±§à@¡­€@á 屨àBÿ¬€BàÁä ƒä‹c© B@JÀBå±@x@¿AAJªàE:f¹áJììe±«à=»x„F屬 =áˆ&l€{áˆ% äF"ó­à@vg€@ü y)ø @å±Ãâ ó屪/Bó®àbÖá ᨠbᨋc¯ B@a¨ÀBå±@x@ßA$A¨°àEÁ!ºa¨ÿÿ,íîbó± =ájØá€=ájä^%±²à@;à€@å,±³àJ§ß JA#¥(ÁRëc´ B@aRàBÿÿÞ@x@ÇA ARµàEΘ»aRàˆå ±ïïaR¶ =áR~ €=å"±·à@Þ›€@å ±ù¯%ÀàÀðÀƒÀ‚a YJB½¸àbJÀb@$ájå±¹ B@jÀBÿÿ$X@x@ßA$AjºàEUT¼ajÿÿôððaj» =ájÕW€=â½%-ä(+c¼à@3 @å$±½àB™V€ƒá J)º ƒä…±¾ B@JÀBÿÿ¾@x@¿AAJ¿àEܽaJ‹@Y¡Óâ µññe±Àà=]"ƒ|yÿÿ,Á =áˆÐ€{áˆ% áˆ%±Âà@€@ü y': @å±(¥â ó ›”hv¼ï/GÌï?ý¸AëÃàb|€bá ë!¨ bᨅ±Ä B@a¨ÀBñ@x@ßA$A¨ÅàEbË€ˆöÇòóbóÆ =ájš‹¾aæájå ±Çà@ý‰€@å,±ÈàJiÀJA#¢rå±É B@aRàBå±@x@ÇA ARÊàEpB¿aäFôôaRË =áRJ€=áR"½Ìà@|E€@öÇ9#À…À„À’S’þÀÐuÐÀaÀ+B½ÍàbèD€bá jå±Î B@jÀBÿÿB@x@ßA$AjÏàEöý€ˆájõõajÐ =ájƒÀa¨ç%-ä(%±Ñà@á€@ã Å$( @ãÅ(®@BÒàBCàB$àB¥±Ó B@JàB@ÙâøöÇ@x@¿AAJÔàE}¹€ËáJööe±Õà=Ì„Fÿÿ Ö =@®€Áq¿€{âó% áˆ"ó×à@¾º€@å ±ë câ óå±)Ù!TØàb bâ ó!¨ £á륱٠B@`æÀBå±@x@ßA$A¨ÚàEuÁbµüy÷øbóÛ =áj45Â`=âóå ±Üà@—3€@å,±ÝàJÀJA#â¸ëcÞ B@aRàBÿÿ¦@x@ÇA ARßàEì€ÓâûùùaRà =áR½ó€=â½áR"½áà@!ï€@ë c3ÀÀÀÀÀÀÀÂÄààzB½âàbî b ájå±ã B@jÀBxñ@x@ßA$AjäàE˜§Ãbájúúajå =áj(«€=áj%-ä(#Åæà@‚ª€@á å±çàBä©àB$ ƒä‹cè B@JÀBñ@x@¿AAJéàEcÄáJûûe±êà=¤u„Få±ë =ሠi€{áˆ% áˆ"óìà@_d€@ÿÿ‚l @ëcâ óå±DˆAëíàb¿¡á ë!¨ bᨋcî B@a¨ÀB”â ó'Z @'@x@ßA$A¨ïàE¦Åa¨í üübóð =ájM#€=å"±ñà@®!€@å,±òàJ Jè ðå±ó B@A*àBÿÿB@x@ÇA ARôàE,Ú€ÓäFýýaRõ =áRâá€=áR"½öà@DÝ€@å ±CǨà‰99´9¾a lèB½÷àb°Ü b@$å±ø B@jÀByâ ½ÿÿ5n@x@ßA$AjùàE³•Æb½ájþþajú =ájC™€=ä(%-ä(%±ûà@Ÿ˜€@â rå±üàBÿ—€Bá J$ ƒä…±ý B@JÀBå±@x@¿AAJþàE:QÇáJÿÿe±ÿà=F‚ôÿÿ@ÒQáˆ&W€{áˆ% áˆ"óà@xR€@å ±ë câ óå±ÈAëàbڡᨠ£á¨…± B@a¨ÀBå±`x@9 A¨ @€EÀ Èa¨üyA =âóóÌ€=ä^.à@SË€@å,±àJ¿Ê J@ÝäFå± B@a àBÿÿÞ@x@ÇA AR àE΃ÉaRàˆô aR =áRp‹€=áR"½ à@Ò†€@å ±S9Æ :::3:T :a NÖB½ àb>Àb@$ÿÿ<‹ëc B@jÀBå±@x@ßA$AjàEU?Êajå2á jaj =ájåB€=ä(%-ä(%±à@A @à~Óˆå±àB¡A€ƒá Jå± B@JÀBå±@x@¿AAJàEÛúà ˆá J @=à=d ƒ|töÇ =áˆÄËaÆâó% áˆ"óà@ü€¼å ±!ˆ @ëcSâóå±ê$Aëàbwû€bᨠbå ± B@a¨ÀBå±@x@ßA$AjàEb¶,å±bó =ájvÌajâóïÂ%±à@ñt€@å,±àJ]ÀJA#äFå± B@aRàBå±@x@ÇA ARàEp-ÍaãÇâ ûaR =áR5€=â½áR"½ à@`0€@ö Çcq$:T(:q,:T0:q4:TàÕ¯B½!àbÌ/€bàáÄ©å±" B@jÀBÿÿQè@x@ßA$Aj#àEö耈áóá jaj$ =Àƒ‚ì€=â½å ±%à@ßë€@á $( @ãÅ"üAJ&àB> BàÁÁJå±' B@JÀBå±@x@¿AAJ(àE}¤Îbµàˆá J  e±)à=þ¤Fÿÿ * =áˆiª€{áˆ% å±+à@¸¥€@ÿ ÿ,â ó›üyq]A¨,àb báᨠ£áë«c- B@a¨ÀBÿÿ,@x@ßA$A¨.àE`Ïa¨ÿÿ,  bó/ =áj Ð`=ájä^"´0à@{€@ù 5,Î @ÿÿ 1àJæ€Já Rå±2 B@aRàB|ÿÿ¦@x@ÇA AR3àE×€ˆïª  aR4 =áRÄÞ€=å"±5à@%Ú€@ë cs8:q<:T@:qD:ŒÇa Ó¡B½6àb‘Ù b@$çå±7 B@jÀBÿÿÞ@x@ßA$Aj8àE˜’Ñbàˆä f  aj9 =áj –€=â½%pä(C0;à@}•€@å$±;àBÜ”€BàÁä ƒäÿB< B@JÀBå±@x@¿AAJ=àENÒaJàËá Je±>à= ¤Få±? = ÀÁ T€{áˆ% áˆ(¥@à@ZO€@å ±': @ëcâ óå±wÿIxAàb»á ᨠbᨑB B@`æÀBå±@x@ßA$A¨CàE¥ Óa¨ëcbóD =ájÐÉ€=áj%páj%±Eà@0È€@ë,cFàJœÇ J äFå±G B@aRàBå±@x@ÇA ARHàE³€ÔaRàˆâ ûaRI =áRYˆ€=áR"½Jà@·ƒ€@ë c ƒèàõH£àõI K Ja o>D©Kàb# bàá½å±L B@jÀBÿÿô@x@ßA$AjMàE9<ÕájajN =áj¾?€=ç%-â½%±Oà@ @á $( @ãÅ"ü@BPàB~>€ƒå(ÁJëcQ B@JàB@Ùâø8ÿÿWš@x@¿AAJRàEÀ÷€ˆàËâ µe±Sà=A ƒ|ÿÿ T = ž€Á°ý€{âó% áˆ"óUà@üø€@—ÿÿ$Xâ óå±ËÌA¨Vàb\ báᨠ£á륱W B@`æÀBœù »!¨Üy@x@ßA$A¨XàEG³ÖâóbóY =ájÙ·€=âóå ±Zà@;¶€@å,±[àJ§µ J þâ¸å±\ B@aRàBÿÿÞ@x@ÇA AR]àEÎn×áRaR^ =áR‚v€=áR"½_à@Þq€@å ±“%¥M L O Na ÄòB½`àbJ bá jå±a B@jÀBÿÿÞ@x@ßA$AjbàET*Øájajc =ájÙ-€=áj%pä(%±dà@3 @á å±eàB”,€ƒà Bå±f B@JÀBëc@x@¿AAJgàEÛåà ˆå ±e±hà=ã¤Fóüyi =áˆËë€{å"±jà@ç€@ÿ ÿ¦!ˆ%Nëcâ óå±`Aëkàbwæ€bᨠbå ±l B@a¨ÀBå±@x@ßA$A¨màEb¡Ùbóñbón =ájaÚ`=âóå ±oà@ñ_€@å,±pàJ]ÀJA#èjå±q B@aRàBå±@x@ÇA ARràEoÛaàˆâ ûaRs =áR$ €=áR%±tà@{€@ÿÿ¦ 9£udÇ©t𣠶>`àнB½uàbç€bàáÈoå±v B@jÀBÿÿÚ˜@x@ßA$AjwàEöÓà ˆá jajx =áj‚×€=ä(å ±yà@ÝÖ€@ë $( @ãÅ«czàBB Bá Jå±{ B@JàB@Ùâø)ºëc@x@¿AAJ|àE}Üâµe±}à=þ¤FÿÿÞ~ =@®€Áq•€{áˆ% ç:"óà@º€@ÿ ÿ¦5\ @å±+câ óå±.OBó€àb bᨠbáë«c B@`æÀB ëc@x@ßA$A¨‚àEKÝa¨ñbóƒ =áj5 Þ`=ä^"´„à@– €@â m!j%ò€ Ã! $ … AR…àJÀJ â¸å±† B@aRàBÿÿÞ@x@ÇA AR‡àE€ÓöÇaRˆ =áRÚÉ€=áR"½‰à@-Å€@ÿ7!Râ½%± ³ŒQ½TýðSPûA6Ä«tNZàW®AjŠàb™Ä€bá j屋 B@jàBâ ½ÿÿÞ@x@ßA$AjŒàE˜}ßbå2å ±  aj =áj(€=ä(%pä(+cŽà@ƒ€€@à~ƹå±àBä€Bá Jå± B@JÀBëc@x@¿AAJ‘àE9àaJàˆá J!!e±’à=Ÿ¤Fÿÿnb“ =áˆ?€{âó% áˆ"ó”à@\:€@ŸBºãû!ˆ @âóå±ó\Aë•àbºá Áë!¨ bå ±– B@a¨ÀBÿÿQè@x@ßA$A¨—àE¥ô€ëå±"#bó˜ =ájδáaæâó$áj%±™à@0³€@å,±šàJœ² JA#áµ `‹áR¿ÿàJ› B@aRàBå±@x@ÇA ARœàE³kâaãÇâ û$$aR =áRPs€=â½áR"½žà@³n€@ü yÃð…©ðåP â0ÿÿ; ç„B½Ÿàb bàá½å±  B@jÀBÿÿB@x@ßA$Aj¡àE9'ãajç%%aj¢ =ájÍ*€=â½å ±£à@) @á $( @âr"üAJ¤àB‰)€ƒâ µ$àB¼y¥ B@JÀBå±@x@¿AAJ¦àEÀâà ˆâ µ&&e±§à=Aõ„FÿÿÞ¨ = ÀÁ¨è€{áˆ% äF"ó©à@úã€@ü y)ø @å±+câ óå±CvA¨ªàb` bᨠbáë«c« B@`æÀBÿÿŠÜ@x@ßA$A¨¬àEGžäbóñ'(bó­ = …`ƒl^å`=ájä^"´®à@Î\€@ù 5!j @ëc¯àJ:ÀJ å ±° B@€ÎÀBå±@x@ÇA AR±àETæaáÛâ û))aR² =áR €=áR"½³à@h€@ñ Ó僃 àt^ð€t a èB½´àbÔ€bàáå±µ B@jÀB ç_ëc@x@ßA$Aj¶àEÛÐà ˆá j**aj· =ájkÔ€=áj%-ä(+c¸à@ÇÓ€@â rå±¹àB' Bá J屺 B@JÀBå±@x@¿AAJ»àEbŒçbµàËá J++e±¼à=ã¤Fëc½ =áˆN’€{áˆ% áˆ"ó¾à@œ€@—ëcâ óå±LåAë¿àbþá Âó!¨ £å ±À B@a¨ÀBÿÿ,@x@ßA$A¨ÁàEèGèa¨ëc,-bó =ájé`=ê%±Ãà@€@ë,cÄàJë JA#äëcÅ B@aRàBå±@x@ÇA ARÆàEö¾à ˆâ û..aRÇ =áR—Æ€=â½&Ãâ½Èà@úÁ€@ë cãK[ á0å ucår åa Jÿ,ÉàbfÀb@$ájëcÊ B@jÀBÿÿB@x@ßA$AjËàE}zêbäf//ajÌ =ájå}€=â½å ±Íà@@ @á å±ÎàB¥|€ƒàÁä ƒäŸÿÞÏ B@JàB á&ýöÇ@x@¿AAJÐàE6ëaJàËâ µ00e±Ñà=„H„Fÿÿ¾Ò =@®€Áë;€{âó% âóÓà@=7€@ô )ø @ëcâ óå±îÿ,ÔàbŸá ᨠb᨟ÿÞÕ B@`æÀBÿÿB@x@ßA$A¨ÖàEŠñ€ëå±12bó× =áj°±ìaæâóä^%±Øà@°€@å,±ÙàJ}¯ JA#â¸å±Ú B@aRàBå±@x@ÇA ARÛàE—híaàˆâ û33aRÜ =áRIp€=â½áR(oÝà@¤k€@ë cóCSPþCPCPQ½Da k}HoÞàb bä få±ß B@jÀBëc@x@ßA$AjààE$îáj44ajá =áj¦'€=áj%-ä(+câà@ @áéÃÅå±ãàBf&€ƒà B)º ƒä…±ä B@JàB@Ùâøå±@x@¿AAJåàE¥ßà ˆâ µ55e±æà=&ò„Fÿÿ ç =@®€Áå€{áˆ% áˆ"óèà@ßà€@ÿÿh°â óå±ÏAëéàbA bᨠ£á¨…±ê B@`æÀBöÇ@x@ßA$A¨ëàE,›ïbóÿÿ/¼67bóì =áj^[ð`=å"±íà@¾Y€@â m'$ŸñîàJ*ÀJA#â¸å±ï B@aRàBÿÿô@x@ÇA ARðàE9ñaàˆâ û88aRñ =áRë€=áR"½òà@E€@å±:¶ÈðC õP€a Œ×B½óàb±€bàáÄ©å±ô B@jÀBå±@x@ßA$AjõàEÀÍà ˆá j99ajö =ájLÑ€=ä(%-ä(%±÷à@¨Ð€@æ ¹å±øàB Bá Jå±ù B@JÀBñ@x@¿AAJúàEF‰òâµ::e±ûà=ȤFñü =áˆ7€{áˆ% áˆ"óýà@Š€@ÿÿ,â óå±(%Aëþàbã¡á ë!¨ £å ±ÿ B@a¨ÀBÿÿÞ@x@ßA$A¨R  €EÍDóa¨ëc;>aj =ájêz€=ájå ± à@F @ã Åå± àB¥y€ƒàÁÆü$ ƒä‹c B@JÀBå±@x@¿AAJàEè2öaJýÄ??e±à=iE„Fëc =áˆÜ8€{å"±à@'4€@ÿÿ â óå±VBóàbˆ3€bá¨å ± B@a¨ÀBå±@x@ßA$A¨àEoëc@Abó =áj¡®÷aæáj+"ç%±à@­€@å,±àJn¬ JA#å± B@aRàBå±@x@ÇA ARàE|eøaàˆä FBBaR =áR+m€=äFáR%±à@Œh€@öÇ:#(QÈ€dÿ •´+g€Ya ÉÔB½àbøg b@$ájå± B@jÀBÿÿ¦@x@ßA$AjàE!ùájCCaj =áj“$€=â½%-â½+c!à@ð#€@å$±"àBO Bá Jå±# B@JÀBå±@x@¿AAJ$àEŠÜ€Ëàˆâ µDDe±%à= ï„Få±& =áˆvâ€{áˆ% áˆ"ó'à@ÅÝ€@“ü y/ª @öÇâ óå±mÿ™4(àb& báçZ bçZ‘) B@a¨ÀBüy@x@ß& =A¨* @€E˜úbóëcEFbó+ =ájCXû`=ájä^%±,à@£V€@å,±-àJÀJ@ÝäFñ. B@a àBÿÿ¦@x@ÇA AR/àEüaàˆâ ûGGaR0 =áRÑ€=å"±1à@.€@ë c3Ǫtkbü`p a @žD©2àbš€bàá½å±3 B@jÀBÿÿ¦@x@ßA$Aj4àE¥Êà ˆá jHHaj5 =áj5΀=â½å ±6à@’Í€@å$±7àBñÌ€Bá Jå±8 B@JÀBå±@x@¿AAJ9àE+†ýâµIIe±:à=¬¤Fÿÿ¦; =ሌ€{âó% å±<à@g‡€@—å ±,ì @å±(¥â óå±älBó=àbÇá Âó!¨ ba“${ ¥±> B@a¨ÀBëc@x@ßA$A¨?àE²Aþa¨å±JKbó@ =ájÝÿ`=âóå ±Aà@=€@å,±BàJ©ÿ€Éâ ûå±C B@aRàBå±@x@ÇA ARDàEÀ¸€ÓãÇä FLLaRE =áRfÀ€=áR(oFà@È»€@ë c C6»€E5¹€@+g0GöÇÜóB½Gàb4 bàá½å±H B@jÀBëc@x@ßA$AjIàEFt<@Táóá jMMajJ =ájÎw€=ájå ±Kà@+ @à~Á$( @ãÅ(®AJLàBŠv€ƒà B$àB±M B@JÀBå±@x@¿AAJNàEÍ/áJNNe±Oà=NBƒ|å±P =ስ5€{áˆ% `ï€3bóQà@ 1€@ë câ óå±xA¨Ràbi0€bᨠ£á륱S B@a¨ÀBÿÿ,@x@ßA$A¨TàET뀈üyOPbóU =ájƒ«aæçå ±Và@ã©€@ÿ ÿš,Î @üyWàJOÀJA#ëcX B@aRàBñ@x@ÇA ARYàEabaáÛä FQQaRZ =áRj€=â½!â½[à@ie€@å ±S8Ǩà$èQ´ÎÃÎØùa xàB½\àbÕd b@$ájå±] B@jÀBÿÿQè@x@ßA$Aj^àEèájRRaj_ =ájp!€=áj%-áj1`à@Í €@á å±aàB,àBå±b B@JÀBå±@x@¿AAJcàEoÙ€Ëã>â µSSe±dà=ð¤Få±e =áˆ[߀{áˆ% áˆcfà@¦Ú€@ö Çâ óå±ÜAëgàb  báÇ!¨ £å ±h B@a¨ÀBñ@x@ßA$A¨iàEõ”bóëcTUbój =áj!U`=å"±kà@€S€@ë,clàJìR JA#äFëcm B@aRàBëc@x@ÇA ARnàE aàˆâ ûVVaRo = …€ƒ¶€=â½å ±pà@€@ñ  cQ­€Ã3Øü<€ð€ÿÿ5nçB½qàb€bàá½å±r B@€æÀBÿÿ* @x@ßA$AjsàEŠÇ€ˆãFá jWWajt =ájË€=â½å ±uà@oÊ€@á å±vàBÎÉ€BàÁÁJ$ ƒäœyw B@JÀBÿÿÔæ@x@¿AAJxàEƒâµXXe±yà=‘•„FÿÿŽz =ሉ€{âó% å±{à@L„€@å ±)ø @ñ6Çâ óå±CnW|àb¬á ᨠbᨖÇ} B@a¨ÀBñ@x@ßA$A¨~àE—> a¨å±YZbó =!b€ƒ³þ€=âóä^.€à@ý€@å,±àJ‚ü J ÿÿ@Ò‚ B@aRàBÿÿQè@x@ÇA ARƒàE¤µ aRàˆä F[[aR„ =áRH½€=áR(o…à@¥¸€@å ±s+`äë`%¾àIoE±†àb bä f屇 B@jÀBöÇ@x@ßA$AjˆàE+q áj\\aj‰ =áj£t€=ä(%-ä(+cŠà@ @á 屋àBgs€ƒâ,ä ƒä…±Œ B@JÀBÿÿ]L@x@¿AAJàE², áJ]]e±Žà=3?ƒ|ÿÿ›ò =ሢ2€{áˆ% áˆ"óà@î-€@å ±!ˆ @å±6Çâ óå±íAë‘àbN bᨠbᨅ±’ B@a¨ÀBå±@x@ßA$A¨“àE9è€ëÿÿWš^_bó” =áje¨ aæä^%±•à@Ǧ€@å,±–àJ3ÀJA#å±— B@aRàBÿÿ/¼@x@ÇA AR˜àEF_aàˆå ±``aR™ =áRúf€=áR"½šà@Rb€@ÿ ÿ ƒ«`ªðÐÐÐÐa ë‡B½›àb¾a€bàáÄf屜 B@jÀBƒÿÿ¦@x@ßA$AjàEÍájaaajž =ájQ€=ä(%-ä(%±Ÿà@¯€@å$± àB Bá Jëc¡ B@JàB@ÙâøÿÿB@x@¿AAJ¢àETÖà Ëâ µbbe±£à=Ù¤F層 =@®€ÁDÜ€{áˆ% áˆ"ó¥à@×€@å ±ë câ óå±OuAë¦àbðá Âó!¨ £å ±§ B@`æÀBå±@x@ßA$A¨¨àEÚ‘bóëccdbó© =ájR`=ájë cªà@eP€@â m!j @öÇ«àJÑO JA#â¸ñ¬ B@aRàBå±@x@ÇA AR­àEèaàˆâ ûeeaR® =áR€=å"±¯à@ð €@ö Ç“ÐÐÐÐÐÐВЄå±Â(B½°àb\ÀbA;ájå±± B@jÀBöÇ@x@ßA$Aj²àEnÄ ë@Y ˆá jffaj³ =áj÷Ç€=ájå ±´à@Q @å$±µàB²Æ€ƒàÁä ƒäÿF„¶ B@JÀBöÇ@x@¿AAJ·àEõâµgge±¸à=v’„Fÿÿ5n¹ =áˆÝ…€{äF% 屺à@2€@ü yë câ óå±fÚBó»àb‘€€bᨠ£á¨‹c¼ B@a¨ÀBüy@x@ßA$A¨½àE|;á¨hhbó¾ =áj'@€=áj%páj+c¿à@ˆ>€@å,±ÀàJô= J £½Âûå±Á B@aRàBüy@x@ÇA ARÂàE÷€ˆãÇä FiiaRà =áR þ€=áR(oÄà@ûù€@ñ £…ЂЃÐðÐà2ÿt¨a q•B½Åàbg bá j屯 B@jÀBÿÿô@x@ßA$AjÇàE‰²â½jjajÈ = …€ƒ ¶€=áj%-â½+cÉà@iµ€@á $( @ãÅ"ü@BÊàBÉ´àB)ºàB¥±Ë B@€ÆàB@Ùâøëc@x@¿AAJÌàEnáJkke±Íà=<‚tõw „àƒ€  Œ  AÎ = ) Át€{áˆ% áˆ"óÏà@Ko€@—ÿ ÿÞ)ø @ñ6Çâ óå±ÕgA¨Ðàb¬á Ä^!¨ báë¥±Ñ B@a¨ÀBëc@x@ßA$A¨ÒàE—)a¨ÿÿ,lmbóÓ =!b€ƒÉé€=è¥å ±Ôà@*è€@ÿ,ÿ,ÕàJ–ç J â4b4å±Ö B@aRàBÿÿ$X@x@ÇA AR×àE¤ aRãÇå ±nnaRØ =áRJ¨€=â½áR"½Ùà@¤£€@”â ½$F @â½3Ó³"ÿä4ÿþïx"Ç«tÿÿë®-ÿ!dÚàb bàáÁjå±Û B@jÀB‚ñ@x@ßA$AjÜàE+\ájooajÝ =áj»_€=áj%-ä(%±Þà@ @á !j @ãÅ¥±ßàBw^€ƒà B$àB¥±à B@JÀBëc@x@¿AAJáàE²áJppe±âà=3*ƒ|ÿÿBã =ሪ€{áˆ% áˆ"óäà@ì€@¤â ó @âó屉ÿ'LåàbN bᨠbá륱æ B@a¨ÀBÿÿ$X@x@ßA$A¨çàE8Ó€ëñqrbóè =ájc“aæå"±éà@Ñ€@è '$^ñêàJ/ÀJA#e§äFå±ë B@aRàBëc@x@ÇA ARìàEFJaàˆå ±ssaRí =áRàQ€=áR"½îà@:M€@ö ÇÃÇ®à"äÿåfEe`,bÿÿ/¼|kD©ïàb¦L€bàáå±ð B@jÀBëc@x@ßA$AjñàEÍajç¥á jttajò = …€ƒM €=ä(%-ä(%±óà@©€@á å±ôàB  BàÁÁJå±õ B@€Æ  Bå±@x@¿AAJöàESÁà Ëá Juue±÷à=Ô¤Få±ø =áˆGÇ€{áˆ% áˆ"óùà@ŽÂ€@ÿ ÿ$Xâ ó屨™Aëúàbïá ᨠ£å ±û B@a¨ÀB è ¥- ÿÿQè@x@ßA$A¨üàEÚ|bó¼ 5¤çᨠvwbóý =áj =`=ä^(fþà@m;€@å,±ÿàJÙ: J ®ÀÉÁRå±S  aRàBå±@x@ÇA ARàEçóà ˆáRA@xxaR =@€ƒšû€=áR"½à@øö€@ñ Ób¬apaõ‚Œƒä“þï àÀB½àbd bä fëc B@€æÀBÿÿ$X@x@ßA$AjàEn¯ báóá jyyaj =ájò²€=ä(å ±à@O @á å± àB®±€ƒàÁµå± B@JÀBå±@x@¿AAJ àEõj!áJzze± à=v}„Fÿÿ5n =áˆåp€{áˆ% å±à@0l€@ü y!ˆ @ëcâ óå±¶2Bóàb‘k€bᨠbå ± B@a¨ÀBñ@x@ßA$A¨àE|&"a¨ñ{|bó =áj«æ€=ä^%±à@ å€@å,±àJvä JA#¦{Âûå± B@aRàBÿÿ @x@ÇA ARàE‰#aRàˆä F}}aR =áR8¥€=áR%±à@‘ €@ë cã$ýä4Ç‚õƒîðåffa ÿbþàbýŸ€bàáÁjå± B@jÀBñ@x@ßA$AjàEY$ajÿÿ(¿~~aj =áj¤\€=ä(å ±à@ @å$±àB`[€ƒá Jå± B@JÀBå±@x@¿AAJ àE—%aJ‹ £>â µe±!à='ƒ|€ÿÿF„" =ÀÁ€{áˆ% å±#à@Ö€@ÿÿÏ4â óå±§ÿbþ$àb7 bá ë!¨ £å ±% B@a¨ÀB ëc@x@ßA$A¨&àEЀëëc€bó' =ájX&aæáj0Ôáj%±(à@¸Ž€@ˆ@S¢'Ãûñ)àJ$ÀJ àJå±* B@aRàBå±@x@ÇA AR+àE+G'aàˆâ û‚‚aR, =@þ€ƒÒN€=å"±-à@/J€@ë cópeï´Îǵï%³a _Ho.àb›I€bàáÁµå±/ B@€æÀBÿÿ @x@ßA$Aj0àE±(ájƒƒaj1 =áj*€=ájå ±2à@‡€@ã Å)Ú @ãÅ"üAJ3àBæàB$àBiŸÿ,4 B@JÀBÿÿB@x@¿AAJ5àE8¾€ˆáÓâ µ„„e±6à=Õ¤Faå±7 =ሠĀ{äF% äF(¥8à@t¿€@ÿ ÿ,â óå±4­A¨9àbÔá Âó!¨ £áë¶Ç: B@a¨ÀBÿÿ,@x@ßA$A¨;àE¿y)bóëc…†bó< = …`ƒê9*`=ájå ±=à@J8€@å,±>àJ¶7 J å ±? B@€ÎÀBå±@x@ÇA AR@àEÌðà ˆâ û‡‡aRA =áRø€=áR"½Bà@àó€@ÿÿÞ;Eepïd`ud"åHÿÿt®ÊB½CàbLÀb çå±D B@ àBÿÿÞ@x@ßA$AjEàES¬+bàˆá jˆ! AF = €ƒׯ€=áj%-ä(6ÇGà@5 @å$±HàB“®€ƒàÁä ƒä…±I B@€ÆÀBëc@x@¿AAJJàEÚg,áJ‰‰e±Kà=[z„Fÿÿ¦L =ሾm€{áˆ% áˆ"óMà@i€@å ±)ø @ñÃâ óå± 8AëNàbvh€bᨠb᨟ÿÞO B@a¨ÀBÿÿQè@x@ßA$A¨PàEa#-a¨å±Š‹bóQ =ájŒã€=è¥å ±Rà@ïá€@å,±SàJ[ÀJA#äFëcT B@aRàBÿÿô@x@ÇA ARUàEnš.aRàˆä FŒ+$FV =áR¢€=â½áR"½Wà@r€@$£á)ø @â½%±påPÇ0åtð€x¹À†B½XàbÞœ b@%ájå±Y B@jÀBå±@x@ßA$AjZàEõU/ájaj[ =áj…Y€=H@—àÆe-ä(%±\à@âX€@á !jån«c]àBAàB)ºàB¥±^ B@JÀBå±@x@¿AAJ_àE{0áJŽŽe±`à=ý¤Få±a =áˆh€{äF% áˆ"óbà@·€@”â ó @âóå±"ïAëcàb bá ë!¨ bå ±d B@a¨ÀBÿÿ$X@x@ßA$A¨eàEÍ€ëÿÿ,5$^f =áj&1aæáj$áj"´gà@‰‹€@å,±hàJõŠ JA#ãå±i B@aRàBå±@x@ÇA ARjàED2aéyå ±‘‘aRk =áR»K€=äFáR"½là@G€@!¯á$F @â½#äð~3%å!0á 8à_|B½màb€F€bá jå±n B@jÀBÿÿô@x@ßA$AjoàE–ÿà ˆá j’’ajp = …€ƒ 3a¨áj%-â½%±qà@h€@á !j @ãÅ"ü@BràBÎàB$àB¥±s B@€ÆÀBÿÿ¦@x@¿AAJtàE»à ˆá J““e±uà=ž¤Fÿÿ¾v =áˆÁ€{áˆ% áˆ"ówà@Y¼€@ÿÿ,â óå±ûŽA¨xàb¹á í £áë«cy B@a¨ÀB öÇ@x@ßA$A¨zàE¤v4bµëc”•bó{ =ájÔ65`=å"±|à@75€@ÿ,ÿ }àJ£4 J å±~ B@aRàBå±@x@ÇA ARàE±í€ˆãÇâ û––aR€ =áRaõ€=áR"½à@¹ð€@å ±3S!ý‘qƒäÿ'SàBàߘB½‚àb% bàá½å±ƒ B@jÀBå±@x@ßA$Aj„àE8©6bàˆá j——aj… =áj¼¬€=ä(%-ä(%±†à@ @â r屇àB|«€ƒàÁÁJ屈 B@JàB@Ùâø?) @x@¿AAJ‰àE¿d7áJ˜˜e±Šà=@w„Fÿÿ¡¤‹ = •@™³j€{áˆ% áˆ"óŒà@ûe€@å ±!ˆ @è¥â óå±{jAëàb[ bᨠbå ±Ž B@`æÀBå±@x@ßA$A¨àEE 8a¨ëc™šbó =ájvà€=ä^(f‘à@ØÞ€@â m2€*ÿÿ ’àJDÀJ â¸ëc“ B@aRàBÿÿ¦@x@ÇA AR”àES—9aRàˆä F›>Ú,¶• =áR÷ž€=áR"½–à@Wš€@ñ Cï´õ€ å! àåT` a ÿµx—àbÙ b@$áj屘 B@jÀBñ@x@ßA$Aj™àEÚR:ajå2á jœ#Ë!jš =ájfV€=ä(å ±›à@ÀU€@à~Ån$( @ãÅ"üDœàB"àBå± B@JÀBñ@x@¿AAJžàE`;aJáÓá Je±Ÿà=á¤Fÿÿ¦  =ÀÁE€{âó% 屡à@™€@ñ $F @å±+câ óå±4ÿ»`¢àbüá Áë!¨ báë«c£ B@a¨ÀBöÇ@x@ßA$A¨¤àEçÉ€ëÿÿ,žŸbó¥ =ájŠa¨â½%pâ½+c°à@^ÿ€~á !j @ãÅ"ü@B±àB¿þ€BàÁäàB±² B@JÀBå±@x@¿AAJ³àE¸ àˆâ µ¢¢e±´à=ƒÊ„Få±µ =áˆú½€{áˆ% áˆ"ó¶à@?¹€@$£ãû!ˆ @âó屡ÿeò·àbžá ᨠbá륱¸ B@a¨ÀBÿÿô@x@ßA$A¨¹àE‰s?bµñ£¤bóº =áj©3@`=áj$áj"´»à@ 2€@å,±¼àJw1 JA#å±½ B@aRàBz[{àBÿÿÔæ@x@ÇA AR¾àE–êà ˆâ û¥¥aR¿ =áRKò€=å"±Àà@¢í€@ñ c‚Žƒåð£äðï$‘‹{àÇãDfÁàb bá jå±Â B@jÀB„å±@x@ßA$AjÃàE¦Abàˆá j¦¦ajÄ =áj¥©€=â½%-â½%±Åà@ @å$±ÆàBe¨€ƒàÁä ƒäŸÿ,Ç B@JàB âµ&ýñ@x@¿A>ÈàE¤aBaJàËá J§§e±Éà=%t„FöÇÊ =@®€Á˜g€{áˆ% áˆ%±Ëà@áb€@.´áë câ óå±cAëÌàb@ báᨠ£á¨Ÿÿ,Í B@`æÀBñ@x@ßA$A¨ÎàE*Ca¨ñ¨©bóÏ = ƒSÝ€=ájå ±Ðà@µÛ€@â m!j @ñÑàJ!ÀJ â¸ëcÒ B@aRàBå±@x@ÇA ARÓàE8”DaRàˆâ ûªªaRÔ =áRÜ›€=áR"½Õà@8—€@Œâ ½(o!RèoszyñÜ1õU"a }QB½Öàb¤–€bàá½å±× B@jÀBÿÿë®@x@ßA$AjØàE¾OEáj««ajÙ =áj?S€=ç%-ä(%±Úà@™R€@á !j @ãÅ(®@BÛàBûQ€BäàB¥±Ü B@JÀBÿÿ @x@¿AAJÝàEE FáJ¬¬e±Þà=ʤFwÿÿ ß =áˆ5€{âó% áˆ"óàà@ƒ €@—â ó @âóå±Ù£A¨áàbáá Âóñâ B@a¨ÀBÿÿô@x@ßA$A¨ãàEÌÆ€ëëc­®bóä =ájõ†Gaæâó$áj"´åà@W…€@å,±æàJÄ J ë å±ç B@aRàBñ@x@ÇA ARèàEÙ=Haàˆå ±¯¯aRé =áR†E€=â½áR"½êà@Ý@€@ë cƒ%° µ~}{%¿àq.B½ëàbI bàá½å±ì B@jI B…ëc@x@ßA$AjíàE`ù€ëàˆá j°°ajî =ájèü€=áj%-â½%±ïà@C @à~Áå±ðàB¤û€ƒà B$ ƒåô¥±ñ B@JÀBñ@x@¿AAJòàEç´Iâµ±±e±óà=hÇ„Fÿÿh°ô =áˆÛº€{áˆ%páˆ"óõà@$¶€@8­ãû)ø @å±6Çâ óå±ÕÜAëöàbƒµ€bᨠbᨋc÷ B@a¨ÀBëc@x@ßA$A¨øàEnpJa¨å±²³bóù =áj“0K`=å"±úà@ô.€@å,±ûàJ`ÀJ å±ü B@aRàBëc@x@ÇA ARýàE{ç Ó@Y ˆä F´´aRþ = +€ƒ4ï€=â½å ±ÿà@‡ê€@ÿÿ!d @â½6Ç“y!µäõ$þýûqƒ Œà21B½Tà£óé€bàáÄ©å± B@€æÀB‰å±@x@ßA$AjàE£Lbàˆá jµµaj =áj’¦€=áj%-ä(%±à@í¥€@à~Áå±àBNàBå± B@JÀBå±@x@¿AAJàEˆ^MáJ¶¶e±à= q„FöÇ =áˆ}d€{äF% áˆ%± à@¾_€@5¹ ßÿÿJ€!ˆ @âóå±–ÃAë àb$ bá ë!¨ bå ± B@a¨ÀBå±@x@ßA$A¨ àENa¨ñ·¸bó =áj9Ú€=áj$áj+cà@šØ€@å,±àJÀJ áµëc B@aRàBå± 5@ÇA AR @`E‘OaRîèä F¹¹aR =áRΘ€=áR"½à@)”€@ë c£+\‹\y%º‘`Ì ‚PB½àb•“€bá jå± B@a$àBñ@x@ßA$AjàE£LPajàˆá jººaj =áj#P€=å"±à@O€@áéå±àBßN€BàÁÀB$&5ä–Ç B@JÀBå±@x@¿AAJàE*QáJ»»e±à=«¤Fëc =ሀ{áˆ% äF"óà@d €@ë câ óå±&¢Bó àbÆá ᨠ£á¨‹c! B@a¨ÀBñ@x@ßA$A¨"àE±Ã€ëëc¼½bó# =ájçƒRaæä^%±$à@H‚€@ÿ,ÿ %àJ´ JA#å±& B@aRàBå±@x@ÇA AR'àE¾:Saã„ä F¾¾aR( =áRdB€=áR"½)à@Æ=€@ÿ ÿÞ³ª‘zƒy` a ÷)B½*àb2Àb@$ájå±+ B@jÀByëc@x@ßA$Aj,àEEö€ëÿÿ* ¿¿aj- =ájÑù€=ä(%-ä(+c.à@- @â r)Ú @ãÅ"ü@B/àB‘ø€ƒá Jå±0 B@JàB@Ùáüy@x@¿AAJ1àE̱TâµÀÀe±2à=MÄ„Få±3 =@®€Á¸·€{áˆ% áˆ"ó4à@ ³€@“ëcâ óå±SÛA¨5àbl²€bᨠ£á륱6 B@`æÀBÿÿ$X@x@ßA$A¨7àERmUa¨ëcÁÂbó8 =áj|-V`=ájë c9à@Ý+€@â m2€ @üy:àJIÀJA#â¸ëc; B@aRàB{ÿÿ,@x@ÇA AR<àE`ä Ó@Y ˆå ±ÃÃaR= =áRì€=å"±>à@pç€@å ±Ãƒy‘V¿y4 屩WB½?àbÜæ bàáÈoå±@ B@jÀBå±@x@ßA$AjAàEçŸWbå2á jÄÄajB =ájo£€=â½ä(%±Cà@Ï¢€@à~Áå±DàB/ Bá Jå±E B@JàB@Ù¢oÀ…å±@x@¿AAJFàEm[XaJàˆá JÅÅe±Gà=î¤F|üyH =@®€ÁVa€{âó% å±Ià@¨\€@å ±$F @ñ6Çâ óå± ¥AëJàb  báÁe!¨ bå ±K B@`æÀBå±@x@ßA$A¨LàEôYa¨å±ÆÇbóM =áj×€=âó%páj(fNà@Õ€@â m!j @å±OàJëÔ JA#â¸å±P B@aRàBå±@x@ÇA ARQàEŽZaRàˆâ ûÈÈaRR =áRœ•€=â½!áR(oSà@þ€@Œè o @â½6ÇÓ%±Q %² ~a VðB½Tàbj bä ©å±U B@jÀBå±@x@ßA$AjVàEˆI[ájÉÉajW =ájM€=â½å ±Xà@qL€@á å±YàBÐK€Bâ,ä ƒä‘Z B@JÀBñ@x@¿AAJ[àE\áJÊÊe±\à=ƒ|å±] =áˆû €{áˆ% âó^à@F€@—â ó @âóå±òBó_àb«á dhå±` B@a¨ÀBÿÿ$X@x@ßA$A¨aàE–À€ëñËÌbób =ájÄ€]aæáj$áj%±cà@$€@å,±dàJ~ JA#aHáRå±e B@aRàBÿÿ/¼@x@ÇA ARfàE£7^aàˆå ±ÍÍaRg =áRU?€=áR%±hà@«:€@ÿ ÿBã¶Æq%°äÿa ˆÿ5niàb bàá½å±j B@jÀB†ñ@x@ßA$AjkàE*ó€ëàˆá jÎÎajl =áj²ö€=áj%-â½+cmà@ @à~Áå±nàBnõ€ƒà B$ ƒå ±o B@JÀBå±@x@¿AAJpàE±®_bµæ}á JÏÏe±qà=2Á„Få±r =ሩ´€{áˆ%páˆ"ósà@쯀@ÿ ÿB)ø @å±Ãâ óå±,ÿ5ntàbM báÁë!¨ bᨖÇu B@a¨ÀBÿÿB@x@ßA$A¨vàE7j`a¨ëcÐÑbów =ájZ*a`=ê%±xà@º(€@å,±yàJ&ÀJA#å±z B@aRàBëc@x@ÇA AR{àEEá Ó@Y ˆâ ûÒÒaR| =áRûè€=â½ç"½}à@Yä€@ë cót/øäöt /ñÊï´ÿÿQèÆÿÌ@~àbÅã€bàá½å± B@jÀB~å±@x@ßA$Aj€àEËœbböÇÓÓaj =ájT €=â½%-ä(%±‚à@°Ÿ€@á 屃àB Bä ƒä‹c„ B@JàB@Ù¢oÁñ@x@¿AAJ…àERXcaJàËâ µÔÔe±†à=Ó¤Fÿÿ/¼‡ =@®€ÁF^€{âó% áˆ"óˆà@Y€@ü y!ˆ @å±.Wâ óå±bÿÒ(‰àbîá Áe届 B@`æÀB â ó!¨ÿÿL6@x@ßA$A¨‹àEÙda¨ëcÕÖbóŒ =ájüÓ€=âóä^%±à@\Ò€@å,±ŽàJÈÑ J þâ¸ëc B@aRàBå±@x@ÇA ARàEæŠeaRàˆâ û××aR‘ =áR’€=áR"½’à@æ€@ÿÿ,<"+Y~+h@ a ©CD©“àbR bàá½å±” B@jÀBÿÿF„@x@ßA$Aj•àEmFfájØØaj– =ájñI€=ä(%-ä(%±—à@N @å n$( @ån"ü@B˜àB­H€ƒá J$àB¥±™ B@JÀBëc@x@¿AAJšàEôgáJÙÙe±›à=uƒ|ëcœ =áˆè€{áˆ%páˆ"óà@/€@å ±ë câ óå± ±A¨žàb€bᨠ£áë«cŸ B@a¨ÀBå±@x@ßA$A¨ àE{½€ˆå±ÚÛbó¡ =áj®}haæä^"´¢à@ |€@å,±£àJy{ J èj層 B@aRàBñ@x@ÇA AR¥àEˆ4iaàˆå ±ÜÜaR¦ =áR/<€=áR"½§à@|7€@â ½$F'Eèo<%­}" %´ÿÿôßÿ8,¨àbè6€bàá屩 B@jÀBŽè oÿÿ$X@x@ßA$AjªàEðà ˆá jÝÝaj« =áj“ó€=ä(%-ä(%±¬à@ðò€@å$±­àBO Bá Jå±® B@JÀBå±@x@¿AAJ¯àE•«jâµÞÞe±°à=¾„Få±± =ሒ±€{âó% áˆ"ó²à@Õ¬€@ÿÿ <â óå±{ÿ>³àb5 bᨠ£å ±´ B@a¨ÀBñ@x@ßA$A¨µàEgka¨ñßàbó¶ =ájH'l`=áj$áj%±·à@§%€@å,±¸àJÀJA#å±¹ B@aRàBå±@x@ÇA ARºàE*Þ€ÓãÇä FááaR» =áRÝå€=áR"½¼à@2á€@öÇ<#å!0çS!‘zƒy@ŒàûD©½àbžà€bá jëc¾ B@jÀBöÇ@x@ßA$Aj¿àE°™mbàˆá jââajÀ =@Ý€ƒ@€=å"±Áà@žœ€@á鯹'%òãÅ"üAJÂàBü›€BàÁÀBå±Ã B@€ÆÀBå±@x@¿AAJÄàE7UnáJããe±Åà=¼¤Fyÿÿ,Æ =ÀÁ#[€{áˆ% äF"óÇà@oV€@— Sãûë câ óå±_A¨ÈàbÓá ᨠ£áë«cÉ B@a¨ÀBüy@x@ßA$A¨ÊàE¾oa¨ëcäåbóË =ájáЀ=ájå ±Ìà@EÏ€@ó ƒ!j @ÿÿ,ÍàJ±Î JA#áµëcÎ B@aRàBÿÿ; @x@ÇA ARÏàEˇpaRã„ä FææaRÐ =áR{€=å"±Ñà@ÏŠ€@å ±3µ€%´ æ~Æ‘Da ?B½Òàb; bàá½å±Ó B@jÀBÿÿ/¼@x@ßA$AjÔàERCqájççajÕ =ájâF€=â½%pä(+cÖà@@ @â rå±×àBžE€ƒá J屨 B@JÀBå±@x@¿AAJÙàEÙþà ˆâ µèèe±Úà=Zƒ|öÇÛ = ÀÁÍraÆáˆ% áˆ%±Üà@€@ÿ ÿ* !ˆ @îWâ óå±›ÿÐÝàbuÿá Âó!¨ bå ±Þ B@`æÀBñ@x@ßA$A¨ßàE_º€ëëcéêbóà =áj‡zsáj%páj(fáà@êx€@ë,câàJVÀJ å±ã B@aRàBÿÿ$X@x@ÇA ARäàEm1taàˆâ ûëëaRå =áR9€=â½áR"½æà@q4€@å ±C"S!¿‚ŽƒtðƒààÐkcçàbÝ3€bàá½å±è B@jÀBå±@x@ßA$AjéàEô쀈å2á jììajê =ájlð€=ájå ±ëà@Êï€@à~Á$( @ãÅ"üEôìàB0 Bá J$àB¶Çí B@JÀBÿÿF„@x@¿AAJîàEz¨uâµííe±ïà=û¤Fÿÿ/¼ð =áˆo®€{è¥% äF"óñà@·©€@å ±)ø @å±6Çâ óå±tkcòàb bᨠbáë«có B@a¨ÀBå±@x@ßA$A¨ôàEdva¨å±îïbóõ =áj1$w`=ájä^"´öà@”"€@å,±÷àJÀJ å±ø B@aRàBëc@x@ÇA ARùàEÛ Ó@Y ˆä FððaRú = { ƒºâ€=äFáR"½ûà@Þ€@å ±S%¬ £ð"å"0áS"ý Œàý°Dfüàb{Ý€bä ©å±ý B@jàBöÇ@x@ßA$AjþàE•–xbájññajÿ =áj!š€=áj%-ä(+cUà@™€@à~å±àBá˜àBå± B@JàB@Ù¢oÀ…8ÿÿ5n@x@¿AAJàERyáJòòe±à=¤Fzñ =@®€ÁX€{áˆ% áˆ"óà@XS€@ù» @å±4 â óå±YBAëàb¸á Áe!¨ bå ± B@`æÀBå±@x@ßA$A¨ àE£ za¨ñóôbó =ájÃÍ€=å"± à@%Ì€@â m'$Ÿ€ÿ ÿÔæ àJ‘Ë J@0â¸ñ B@aRàBëc@x@ÇA ARàE°„{aRàˆå ±õõaR =áR`Œ€=â½!â½à@À‡€@ÿÿô¨ü}@Œà~B½;àbsÚ€bá jå±< B@jàBëc@x@ßA$Aj=àEz“ƒbàˆå ± A> =@Ý€ƒú–€=ä(%pâ½%±?à@U @å$±@àBº•€ƒàÁéº ƒäÿŽA B@€ÆàB@Ùèªñ@x@¿AAJBàEO„áJ@=Cà=†a„FrñD =@®€ÁõT€{áˆ% áˆ"óEà@>P€@ñ ë câ óå±ÚîAëFàbá ᨠ£á¨ŸÿôG B@`æÀB â ó!¨ÿÿB@x@ßA$AjHàEˆ …a¨ñbóI =áj¸Ê€=ájå ±Jà@É€@â m!j @ñKàJ†È J þâ¸ëcL B@aRàBÿÿ/¼@x@ÇA ARMàE•†aRàˆä FaRN =áRN‰€=å"±Oà@©„€@ÿÿô<“"KX S!ïtðï$‘ÿwÉÀ‹B½Pàb bàá½å±Q B@jÀBÿÿ5n@x@ßA$AjRàE=‡áj E±S =áj¬@€=â½%-ä(%±Tà@ @å$±UàBh?€ƒá J)º ƒä…±V B@JÀBñ@x@¿AAJWàE¢øà ˆâ µe±Xà=$ ƒ|zå±Y =ሓþ€{âó% áˆ%±Zà@àù€@—ëcâ óå±¹yAë[àb> bᨠ£á¨…±\ B@a¨ÀBÿÿ5n@x@ßA$A¨]àE)´ˆbóñbó^ = …`ƒJt‰`=âó%páj+c_à@¬r€@å,±`àJÀJ èjå±a B@€ÎÀBå±@x@ÇA ARbàE7+ŠaãÇâ û  aRc =áRÜ2€=â½áR"½dà@3.€@ë c £‘°‘Ô"~Ÿÿ$`ÿÿ/¼0N$©eàbŸ-€bá jå±f B@jÀBÿÿQè@x@ßA$AjgàE½æà ˆá j  ajh =ájEê€=â½%-â½!ià@¡é€@á鯹/Œ @ãÅ"ü@BjàB Bá Jå±k B@JÀBå±@x@¿AAJlàED¢‹bµàˆá J  e±mà=ŤFÿÿ5nn =áˆ<¨€{áˆ% áˆ"óoà@‚£€@ÿÿ$Xâ óå±_öA¨pàbàá Áë!¨ £á륱q B@a¨ÀBñ@x@ßA$A¨ràEË]Œa¨ëc  bós = …`ƒü`=ájä^"´tà@^€@å,±uàJÊ€Jã >å±v B@€ÎÀBå±@x@ÇA ARwàEØÔ€ˆâûaRx =áRÜ€=áR"½yà@ä×€@ü y³ƒy"å!0å S!ß‘ªÿÿB÷ÿ'zàbPÀbA;çå±{ B@jÀBÿÿ/¼@x@ßA$Aj|àE_Žbàˆä faj} =ájï“€=áj%pä(%±~à@J @à~ÃÅå±àB«’€ƒà B$ ƒä‹c€ B@JÀBå±@x@¿AAJàEæKáJ >‚à=g^„F屃 =áˆÎQ€{áˆ% áˆ"ó„à@ M€@ÿ ÿ5n!ˆ @ù»â óå±Sÿ,þ…àb‚L€bᨠbᨋc† B@a¨ÀBÿÿ5n@x@ßA$Aj‡àEla¨ëcbóˆ = …`ƒšÇ€=ê%±‰à@ûÅ€@å,±ŠàJgÀJ äF屋 B@€ÎÀBå±@x@ÇA ARŒàEz~‘aRàˆä FaR =áR+†€=â½ç"½Žà@~€@ë cÿÿBäŸÿB”ÿÿBø—D©àbꀀbàáÄ©å± B@jÀBÿÿL6@x@ßA$Aj‘àE:’ajç¥á jaj’ =áj‰=€=áj%-ä(%±“à@ä<€@á $( @ãÅ"ü@B”àBEàB展 B@JÀBå±@x@¿AAJ–àE‡õà Ëá Je±—à=ƒÇÿÿ¦˜ =áˆxû€{äF% áˆ"ó™à@Áö€@ö Çâ óå±\eA¨šàb# bá ë!¨ £á륱› B@a¨ÀBœöÇ@x@ßA$A¨œàE±“bóå±bó =áj8q”`=ájä^"´žà@™o€@å,±ŸàJÀJA#éøå±  B@aRàBå±@x@ÇA AR¡àE(•aàˆâ ûaR¢ =áRÅ/€=áR"½£à@ +€@ÿ ÿ* Óq x|ƒ‘ö‘Ôa YçB½¤àbŒ*€bå(å±¥ B@jÀBöÇ@x@ßA$Aj¦àE¢ã€ˆájaj§ =Àƒ*ç€=å"±¨à@†æ€@à~Á屩àBêåàB$åô«cª B@JàB@Ùâø:Ðÿÿ @x@¿AAJ«àE)Ÿ–âµe±¬à=®¤FsöÇ­ =@®€Á!¥€{áˆ% å±®à@c €@ÿ ÿ,â óå±ýBó¯àbÅá Áë!¨ £å ±° B@`æÀBñ@x@ßA$A¨±àE°Z—a¨ñbó² = …`ƒâ˜`=ä^%±³à@B€@â müy´àJ® J â¸å±µ B@€ÎÀBÿÿ/¼@x@ÇA AR¶àE½Ñà ˆå ±aR· =áRƒÙ€=áR%±¸à@ÍÔ€@è o$F'Eî! ãC!"ÿ±ï$ýÿ§_xa çMB½¹àb9 bá jëcº B@jÀB‘è oÿÿ* @x@ßA$Aj»àED™bàˆá jaj¼ =ájÌ€=ä(%-ä(+c½à@( @æ ¹!j @ãÅ"ü@B¾àBˆ€ƒàÁäàB¥±¿ B@JÀBëc@x@¿AAJÀàEËHšaJ~v¥÷á Je±Áà=L[„Fñ =áˆÏN€{áˆ% áˆ"óÃà@ J€@¬ü y$F @âó6Çâ óå±C¿A¨ÄàbkI€bá ëå±Å B@a¨ÀB¬â ó"ôÿÿ,@x@ßA$A¨ÆàEQ›a¨ëc !bóÇ =áj€Ä€=ä^"´Èà@à€@å,±ÉàJLÀJ þÂrÂûå±Ê B@aRàBå±@x@ÇA ARËàE_{œaRàˆâ û""aRÌ = …€ƒƒ€=áR"½Íà@g~€@ö Çó8|‚}{zy®"år0ä Œà2FB½ÎàbÓ} b@&£ÝÁjå±Ï B@€æÀBöÇ@x@ßA$AjÐàEå6áj##ajÑ =ájj:€=áj%-ä(%±Òà@Æ9€@ã Åå±ÓàB&àB$ ƒåô¥±Ô B@JÀBÿÿ @x@¿AAJÕàElò€ËáÓâ µ$$e±Öà=ƒÇd3âÿÿ²º`µA× =áˆPø€{å"±Øà@£ó€@“@aâ!ˆå±Kâ óå±–´AëÙàb báÂó!¨ bᨖÇÚ B@a¨ÀBå±@x@ßA$A¨ÛàEó­žbóëc%&bóÜ =áj%nŸ`=áj)Ïâó%±Ýà@†l€@å,±ÞàJòk JA#áµå±ß B@aRàBå±@x@ÇA ARààE% aàˆâ û''aRá =áR¯,€=äFáR%±âà@(€@å±= s‘çC#"ïpBöàâDB½ãàb|' b@&ájå±ä B@jÀBÿÿÚ˜@x@ßA$AjåàE‡àà ˆá j((ajæ =ájä€=â½%-â½%±çà@lã€@å$±èàBËâ€Bá J$ ƒäÿ é B@JÀBëc@x@¿AAJêàEœ¡âµ))e±ëà=®„FöÇì =áˆú¡€{áˆ% áˆ"óíà@I€@å ±ë câ óå±F¶Aëîàbªá å±ï B@a¨ÀBÿÿ/¼@x@ßA$A¨ðàE•W¢a¨å±*+bóñ =áj·£`=ájáj%±òà@€@å,±óàJƒ JA#äFå±ô B@aRàBÿÿ5n@x@ÇA ARõàE¢Îà ˆä F,,aRö =áRYÖ€=å"±÷à@®Ñ€@”Exá(o!“î!%±~€ ïÓ”~P~ ¯a :ÿ|„øàb bá jå±ù B@jÀB†è oñ@x@ßA$AjúàE)Фbàˆá j--ajû =áj±€=â½%-ä(%±üà@ @å$±ýàBqŒ€ƒàÁä ƒå ±þ B@JàB@Ùáð!KöÇ@x@¿AAJÿàE¯E¥áJ..e±V  €ƒ1X„Fÿÿ­ =@®€=¨K€{âóå ±à@ëF€@ÿ ÿÞ"ó @âó属ÿ‚làbK bᨠbᨋc B@`æÀBöÇ@x`ß =A¨ @€E6¦a¨ñ/0bó =ájeÁ€=áj$âó%±à@Å¿€@â m' @ÿÿBàJ1ÀJ@Ýâ¸å± B@a àBëc@x@ÇA AR àEDx§aRãÇä F11aR =áRê€=äF!áR(o à@D{€@üy =#"7Ë‚7àVËdp8a 9šD© àb°z€bá jå± B@jÀBÿÿF„@x@ßA$AjàEÊ3¨ajàˆá j22aj =ájR7€=áj%-áj%±à@¯6€@à~Ån$( @ãÅK þ BàBàBëc B@JÀBëc@x@¿AAJàEQïà Ëá J33e±à=Ò¤F}ñ =áˆEõ€{áˆ% áˆ"óà@ð€@ÿÿ/¼â óå±hA¨àbíá Áë!¨ £á륱 B@a¨ÀB è ¥'ZöÇ@x@ßA$A¨àEت©bóëc45bó =áj kª`=å"±à@ki€@å,±àJ×h€Jå(ÁRå± B@aRàBå±@x@ÇA ARàEå!«aãÇâ û66aR =áRŒ)€=â½å ±!à@í$€@Œëc3Ñ¡t!Ñ®£àÑ™ðäÿt8/àmßB½"àbY bàáÁjå±# B@jÀBÿÿŽ@x@ßA$Aj$àElÝ€ëàˆá j77aj% =ájôà€=áj%pä(%n&à@Q @à~Áå±'àB°ß€ƒà B$ ƒä‘( B@JÀBå±@x@¿AAJ)àEó˜¬âµ88e±*à=t«©`=å±+ =áˆ㞀{áˆ% áˆ%±,à@.š€@ÿÿw @1a0"… (¥â óå±É-Aë-àb™€bᨠbᨋc. B@a¨ÀBÿÿÞ@x@ßA$A¨/àEyT­ajëc9:bó0 =áj©®`=å"±1à@€@å,±2àJt JA#ëc3 B@aRàBñ@x@ÇA AR4àE‡Ëà ˆä F;;aR5 =áR.Ó€=áR"½6à@΀@â½%òâ½Cõ‚ä4‚õƒàþï$ýä3üa ¨ÿ 7àbëÍ€bá jå±8 B@jÀBŠè oñ@x@ßA$Aj9àE‡¯bç¥á j<àE”B°aJàˆá J==e±?à=U„Få±@ =ሑH€{âó% áˆ"óAà@ÐC€@ÿ ÿL6/ª @âó+câ óå± ÿ Bàb0 báÁë!¨ bå ±C B@a¨ÀB¨â ó"ôëc@x@ßA$A¨DàEþ€ëàˆá ¨>>bóE =áj©±aæâó$áj.Fà@ €@å,±GàJw J å±H B@aRàBz>ðàBÿÿŽ@x@ÇA ARIàE¢¹€ˆ DÀEá R??aRJ =áRKÁ€=â½áR"½Kà@¦¼€@ñ Sƒ,àú£à-õ‚ê<õƒîÿÿ]L"2D©Làb bá jå±M B@jÀBÿÿ @x@ßA$AjNàE(u²bàˆá j@@ajO =áj±x€=ájå ±Pà@  @å$±QàBmw€ƒà B$ ƒä‹cR B@JÀBÿÿ,@x@¿AAJSàE¯0³áJAAe±Tà=·¤FôÿÿÔæU =ሧ6€{äF% äF"óVà@í1€@ö Çâ ó屆ÀBóWàbK bâjå±X B@a¨ÀBöÇ@x@ßA$A¨YàE6ì€ëëcBCbóZ =áj_¬´aæájä^%±[à@Áª€@å,±\àJ-ÀJA#å±] B@aRàBå±@x@ÇA AR^àECcµaÿÿ/¼DDaR_ =áRõj€=äFáR"½`à@Pf€@ñ  cï´Ø "1 Qàyÿ@Òaàb»e€bá jå±b B@jÀBÿÿÏ4@x@ßA$AjcàEʶajàˆå ±EEajd =áj:"€=â½%-ä(+ceà@–!€@å nñfàBö €BàÁä ƒå ±g B@JÀBëc@x@¿AAJhàEQÚà ˆá JFFe±ià=Ò¤Fÿÿ j =áˆ=à€{áˆå ±kà@‹Û€@—ô ë câ óå±Ýÿ@Òlàbíá ᨠ£á¨‘m B@a¨ÀBÿÿ,@x@ßA$A¨nàEØ•·bóÿÿ5nGHbóo =ájýU¸`=ájä^%±pà@^T€@å,±qàJÊS JA#å±r B@aRàBöÇ@x@ÇA ARsàEå ¹aàˆâ ûIIaRt =áR’€=áR%±uà@í€@ë csýљхñ$Ññ ÑlÀbD©vàbY bàá¿ÿq å±w B@jÀBå±@x@ßA$AjxàElÈ€ëàˆá jJJajy =ájüË€=áj%-å±zà@W @à~Áå±{àB¸Ê€ƒà Bëc| B@JÀBå±@x@¿AAJ}àEòƒºâµKKe±~à=t–„Få± =áˆ㉀{áˆ% áˆ"ó€à@-…€@ü y!ˆ @ñ6Çâ ó屯ÿwàbŽ„€bᨠbå ±‚ B@a¨ÀBñ@x@ßA$A¨ƒàEy?»a¨ëcLMbó„ =áj¯ÿ€=ê%±…à@þ€@å,±†àJ|ý J 屇 B@aRàBëc@x@ÇA ARˆàE‡¶¼aRéyä FNNaR‰ =áRH¾€=â½ç"½Šà@›¹€@ÿ ÿÞƒ‚;౟‚Bàÿí$ å±iSE±‹àb bá j屌 B@jÀBÿÿ$X@x@ßA$AjàE r½ajàˆá jOOajŽ =ájŠu€=â½å ±à@åt€@à~å±àBIàB屑 B@JàB@Ùâø5ÿÿÞ@x@¿AAJ’àE”-¾áJPPe±“à=@ƒ|ÿÿ ” =@®€ÁŒ3€{âó% 展à@Î.€@ÿ ÿ¦ë câ óå±Í¡Bó–àb0.€bá ë!¨ £å ±— B@`æÀBñ@x@ßA$A¨˜àE逈ëcQRbó™ =áj1©¿aæâóä^%±šà@’§€@â m!j @ÿÿÞ›àJþ¦€Jå(Ã>ÿÿ,œ B@aRàBå±@x@ÇA ARàE(`ÀaáÛä FSSaRž =áRÞg€=â½!⽟à@0c€@ü y“‚CE«Ñ"%´ÿÿ Çÿ$X àbœb€bàáÁj屡 B@jÀBüy@x@ßA$Aj¢àE¯ÁájTTaj£ =áj7€=ájå ±¤à@“€@á )Ú @ãÅ"üD¥àBóàB$àB¶Ç¦ B@JÀBëc@x@¿AAJ§àE6×à ˆâ µUUe±¨à=·¤F屩 =áˆ.Ý€{áˆ% âóªà@pØ€@ü y!ˆ @ëcâ óå±cÿ$X«àbÒá Âó!¨ bá뱬 B@a¨ÀBå±@x@ßA$A¨­àE¼’ÂbóëcVWbó® =ájçRÃ`=å"±¯à@GQ€@å,±°àJ³P JA#èjå±± B@aRàBñ@x@ÇA AR²àEÊ Äaàˆâ ûXXaR³ =áRx€=áR+c´à@Ò €@ë c£õ‚ä bàá½å±¶ B@jÀBñ@x@ßA$Aj·àEQÅ€ëç¥á jYYaj¸ =ájÕÈ€=ä(%-ä(6ǹà@1 @à~Á$( @ãÅ¥±ºàB‘Ç€ƒˆAâµ$àB¥±» B@JÀBå±@x@¿AAJ¼àE×€ÅâµZZe±½à=X“„Få±¾ =áˆ̆€{âó% áˆ"ó¿à@‚€@ë câ óå±äÿ¯üÀàbs€bᨠ£á륱Á B@a¨ÀBÿÿ @x@ßA$A¨ÂàE^<Æa¨å±[\bóà =ájü€=ájê"´Äà@áú€@å,±ÅàJMÀJ âû屯 B@aRàBå±@x@ÇA ARÇàEl³ÇaRãÇä F]]aRÈ =áR&»€=áR"½Éà@|¶€@å ±³å±=E±ÿÿWšâD©Êàbèµ€bàáÄ©å±Ë B@jÀBÿÿ§V@x@ßA$AjÌàEònÈajáóá j^^ajÍ =ájzr€=å"±Îà@×q€@á å±ÏàB6 BàÁÁJå±Ð B@JÀBå±@x@¿AAJÑàEy*ÉaJÿ/__e±Òà=ú¤Få±Ó =áˆu0€{áˆ% å±Ôà@¸+€@ÿ ÿ,â óå±½BóÕàb bᨠ£å ±Ö B@a¨ÀBëc@x@ßA$A¨×àEæ€ëñ`abóØ =áj*¦Êaæájå ±Ùà@‹¤€@ÿ,ÿnbÚàJ÷£ JA#å±Û B@aRàBå±@x@ÇA ARÜàE ]ËaáÛä FbbaRÝ =áRÌd€=å"±Þà@`€@ñ Ãå±>E± a ¿'B½ßàb‰_€bàáÄfå±à B@jÀBñ@x@ßA$AjáàE”Ìájccajâ =áj€=â½*ßä(+cãà@u€@â rå±äàBØ€Bá Jå±å B@JàB@ÙâøöÇ@x@¿AAJæàEÔ€ˆàËâ µdde±çà=œ¤Fÿÿ¦è =@®€ÁÚ€{áˆ% áˆ(¥éà@VÕ€@å ±5\ @ñÃâ óå±ÕAëêàb·á Âó!¨ bå ±ë B@`æÀBå±@x@ßè =A¨ì @€E¡Íbóëcefbóí =ájÀOÎ`=ájå ±îà@ N€@â m!j @öÇïàJŒM J@Ýÿÿ© ëcð B@a àBÿÿB@x@ÇA ARñàE¯Ïaàˆâ ûggaRò = …€ƒj€=å"±óà@» €@˜ÿ ÿê$F!Râ½?7Óå±?E±; ÷B½ôàb' bàá½å±õ B@€æÀBå±@x@ßA$AjöàE5€ëàˆá jhhaj÷ =ájÆÅ€=ájå ±øà@# @äåÁ!j @ãÅ.`AJùàB‚Ä€ƒà B$àB±ú B@JÀBÿÿ @x@¿AAJûàE¼}Ðâµiie±üà==„FÿÿBý =ሸƒ€{äF% å±þà@ø~€@£â ó @âóå±SêA¨ÿàbX bâjÁë!¨ báë±W  a¨ÀBÿÿB@x@ßA$A¨àEC9Ña¨å±jkbó =áj^ù€=áj$áj"´à@¾÷€@å,±àJ*ÀJA"å± B@aRàBå±@x@ÇA ARàEP°ÒaRàˆä FllaR =áR ¸€=áR(oà@]³€@å ±ãå±@E± a øUB½ àbȲ€bàá½å± B@jÀB‹ÿ ÿêÿÿô@x@ßA$Aj àE×kÓajïÊmmaj =ájko€=áj%-â½+c à@Èn€@á å±àB'àB$ ƒäÿ/¼ B@JÀBñ@x@¿AAJàE^'ÔaJàËâ µnne±à=ߤFÿÿ$X =áˆV-€{áˆ% áˆ"óà@š(€@ë câ óå±\§Aëàbúá Âó!¨ £á¨Ÿÿ  B@a¨ÀBëc@x@ßA$A¨àEåâ€ëñopbó =áj£Õaæè¥å ±à@w¡€@å,±àJã  JA#å± B@aRàBå±@x@ÇA ARàEòYÖaàˆâ ûqqaR =áR¥a€=â½áR"½à@ú\€@ö Çóå±AE±ÿÿÔæ¾ÿ` àbf bàá½å± B@jÀBÿÿh°@x 9A$Aj @`Ey×ájrraj! =ájý€=áj%-ä(%±"à@Z @á å±#àB¹€ƒà B$ ƒä…±$ B@aàBå±@x@¿AAJ%àEÿÐà ˆâ µsse±&à=ã„Fëc' =áˆøÖ€{áˆ% áˆ"ó(à@;Ò€@ü y!ˆ @ëcâ óå±ÿeò)àb›Ñ€bᨠbᨅ±* B@a¨ÀB¥è ¥(¦ÿÿ¦@x@ßA$A¨+àE†ŒØbóëctubó, =áj¦LÙ`=å"±-à@ K€@å,±.àJuJ J þå ±/ B@aRàBÿÿyÆ@x@ÇA AR0àE”Úaàˆâ ûvvaR1 =áR; €=áR"½2à@˜€@ÿÿô>å±"‚Œƒtÿÿ¦òÔD©3àb bàáå±4 B@jÀB~â ½ëc@x@ßA$Aj5àE¿€ëàˆá jwwaj6 =áj§Â€=áj%-ä(%±7à@ @à~Á$( @ãÅ.`@B8àBbÁ€ƒà Bå±9 B@JÀBå±@x@¿AAJ:àE¡zÛâµxxe±;à="„Få±< =ሕ€€{å"±=à@Ý{€@œñ 5\%Nå±Ãâ óå±V&A¨>àb= bᨠbá륱? B@a¨ÀB â ó"ôå±@x@ßA$A¨@àE(6Üa¨ëcyzbóA =áj\ö€=âóê"´Bà@¿ô€@å,±CàJ+ Jô öÇD B@aRàBå±@x@ÇA AREàE5­ÝaRáÛä F{{aRF =áRÑ´€=áR%±Gà@-°€@å ±£ïðÑŠtfÿÿøa k‰B½Hàb™¯€bàáÄ©ëcI B@jÀBâ ½å±@x@ßA$AjJàE¼hÞáj||ajK =áj8l€=ä(%på±Là@–k€@â rå±MàBøj€Bá Jå±N B@JàB@Ùî\!KÿÿB@x@¿AAJOàEC$ßáJ}}e±Pà=ĤFÿÿŽQ =@®€Á7*€{áˆ% áˆ"óRà@%€@å ±!ˆ @å±(¥â óå±Ï{BóSàbßá Âó!¨ bå ±T B@`æÀBå±@x@ßA$A¨UàEÉ߀ëÿÿ¦~#[AV =ájöŸàaæä^%±Wà@Xž€@â m'%òüyXàJÄ J â¸å±Y B@aRàByÿÿ* @x@ÇA ARZàE×Váaàˆå ±€€aR[ =áRw^€=áR"½\à@×Y€@ÿÿF„># Ñ‚BÑ‘‚;àTO±a CbB½]àbCÀb@$ájå±^ B@jÀB&´à BÿÿàJ@x@ßA$Aj_àE^âajå2á jaj` =ájê€=ä(å ±aà@D @à~ÃÅ$( @€ "üAJbàB¦€ƒá J$àB±c B@JÀBëc@x@¿AAJdàEäÍà ˆá J‚‚e±eà=ià„FvöÇf =áˆÍÓ€{âó% å±gà@!Ï€@ÿÿF„â óå±çA¨hàb€Î€bᨠ£áë«ci B@a¨ÀBÿÿbþ@x@ßA$A¨jàEk‰ãbóñƒ„bók = …`ƒIä`=âó%páj"´là@òG€@å,±màJ^ÀJ äFå±n B@€ÎÀBå±@x@ÇA ARoàEyåaãÇâ û……aRp =áR)€=â½áR%±qà@‰€@ë c3Ÿ‚JE¯CE¯ÿí$a *PB½ràbõ€bàáÄ©å±s B@jÀBå±@x@ßA$AjtàEÿ»€ˆáóá j††aju =áj‡¿€=â½%-â½1và@â¾€@á å±wàBC BàÁÁJå±x B@JÀBå±@x@¿AAJyàE†wæbµàˆá J‡‡e±zà=Š„Fzå±{ =áˆr}€{áˆ% áˆ"ó|à@¿x€@—ñ !ˆ @ëcâ ó屎ÿÉ‚}àb" báᨠbå ±~ B@a¨ÀB˜è ¥(¦ñ@x@ßA$A¨àE 3ça¨ñˆ‰bó€ =áj=ó€=ájä^%±à@ ñ€@‡åfëc‚àJ ÀJ Iå ±ƒ B@aRàBå±@x@ÇA AR„àEªèaRáÛâ ûŠŠaR… =@þ€ƒα€=áR"½†à@&­€@ÿÿ* >C KcD\t@ Eà±ND©‡àb’¬€bàá½å±ˆ B@€æÀB(á­ëc@x@ßA$Aj‰àE¡eéáj‹‹ajŠ =áj1i€=ájå ±‹à@Œh€@â r$( @ãÅ"üAJŒàBíg€Bá Jå± B@JÀBå±@x@¿AAJŽàE(!êaJàËâ µŒŒe±à=©¤Få± =áˆ'€{áˆ% 屑à@e"€@›å ±$F @å±+câ óå±¼A¨’àbÄá Âó!¨ báë«c“ B@a¨ÀB â ó"ôå±@x@ßA$A¨”àE®Ü€ëñŽbó• =áj¿œëaæê"´–à@!›€@ë,c—àJš J þäëc˜ B@aRàBå±@x@ÇA AR™àE¼Sìaàˆâ ûaRš =áRx[€=â½ç%±›à@ÈV€@ÿÿÞ>SE«E«F@ Ña èÿQèœàb4 bàá½å± B@jÀBŒâ ½å±@x@ßA$AjžàECíajäfajŸ =ájÀ=â½%pä(+c à@ @á 屡àBƒ€ƒàÁÁJ$ ƒäÿÞ¢ B@JàB@Ùò£!KöÇ@x@¿AAJ£àEÉÊ€ˆàËâ µ‘‘e±¤à=JÝ„Fw34Pàƒÿÿ]L¥ =@®€ÁÅЀ{âó% áˆ"ó¦à@Ì€@¤å ±!ˆ @å±â óå±LÿQè§àbeË€báᨠb᨟ÿ,¨ B@`æÀBÿÿ @x@ßA$A¨©àEP†îbóå±’“bóª =ájFï`=âóä^%±«à@ãD€@å,±¬àJOÀJA#â4` å±­ B@aRàBå±@x@ÇA AR®àE]ý Ó@Y`~â û”2Ð8¯ =áR ðaRâ½áR"½°à@^€@”â ½ @â½c6ÅGE«EµHàÿ °à DD©±àbÉÿà áÁjå±² B@jÀB-¹á$àBëc@x@ßA$Aj³àE一ëáj••aj´ =ájd¼€=áj%-ä(%±µà@¿»€@à~ÀÄ$( @ãÅ"ü@B¶àB àB àB¥±· B@JÀBëc@x@¿AAJ¸àEktñbwàËå ±––e±¹à=ì¤Fÿÿ5nº = ÀÁgz€{áˆ% áˆ"ó»à@¨u€@å ±ë câ ó屯¶A¨¼àb báÁë!¨ £á륱½ B@`æÀBå±@x@ßA$A¨¾àEò/òa¨ñ—1}$^¿ =ájð€=å"±Àà@pî€@å,±ÁàJÜí J ¥(ÁRå±Â B@aRàBÿÿ/¼@x@ÇA ARÃàEÿ¦óaRàˆâ û™™aRÄ =áR ®€=áR"½Åà@÷©€@å ±sí$E«IEµÑàAÝB½Æàbc bàáÁjå±Ç B@jÀBñ@x@ßA$AjÈàE†bôájššajÉ = …€ƒf€=ä(%pä(%±Êà@qe€@ë å±ËàBÒd€Bá J$ ƒä‹cÌ B@€ÆÀBå±@x@¿AAJÍàE õaJÿÿ››e±Îà=Ž0ƒ|ñÏ =ሠ$€{áˆ% `ï€3ÒBóÐà@N€@å ±!ˆ @è¥â óå±å/AëÑàb­¡á ë!¨ bᨋcÒ B@a¨ÀBå±@x@ßA$A¨ÓàE“Ù€ëëcœbóÔ =áj±™öaæä^(fÕà@˜€@å,±ÖàJ~— JA#å±× B@aRàBå±@x@ÇA ARØàE¡P÷aàˆå ±žžaRÙ =áRJX€=áR%±Úà@­S€@ÿ ÿ,ƒ"ð‚8à‚Œƒ££"àTa ‹±B½ÛàbÀb@$ájå±Ü B@jÀBxëc@x@ßA$AjÝàE' øajäfŸŸajÞ =áj°€=áj%-ä(%±ßà@  @ã Å$( @ãÅ"ü@BààBk€ƒá J$àB¥±á B@JÀBå±@x@¿AAJâàE®Çà ˆâ µ  e±ãà=/Ú„Fñä =ሺ̀{å"±åà@ É€@è ¥ë câ óå±/CA¨æàbnÈ€bá¨å ±ç B@a¨ÀB/Z –Ÿÿ" Èè.XöÇ@x@ßA$A¨èàE5ƒùbóëc¡¢bóé =ájRCú`=áj/âó"´êà@´A€@å,±ëàJ  Jã >ëcì B@aRàBå±@x@ÇA ARíàEBú Ó@Y¡˜â û££aRî = +€ƒìûaRäFáR%±ïà@Fý€~ü y“ðÄTðÿ"õe°ï"ƒ ŒàÄœB½ðàb²ü€bàáÂzå±ñ B@€æÀBâ ½ñ@x@ßA$AjòàEɵ,àˆá j¤¤ajó =ájQ¹€=â½%-â½%±ôà@­¸€@â r `@å ±õàB  BàÁÁJå±ö B@JÀBå±@x@¿AAJ÷àEPqübwàˆá J¥¥e±øà=ѤFñù =áˆ@w€{áˆ% áˆ"óúà@Šr€@ü y!ˆ @ëcâ óå±`nAëûàbìá ᨠbáë«cü B@a¨ÀBÿÿ]L@x@ßA$A¨ýàEÖ,ýa¨ëc¦§bóþ = …`ƒ$«€=ájä^%±ÿà@aë€@å,±Xà‹Íê J Å(ÁRå± B@€ÎÀBÿÿ/¼@x@ÇA ARàEä£þaRàˆâ û¨-2h =!J ƒ¦«€=å"±à@ø¦€@ü y£,àþ£àÿõ‚Žƒ"ðå~ Œàpÿ$Xàbd bàáÁjå± B@jÀBöÇ@x@ßA$AjàEk_ÿajå2á j©©aj =ájçb€=â½å ± à@C @à~Á$( @ãÅ"üD àB£a€ƒá Jå± B@JÀBå±@x@¿AAJ àEñ=@Tàˆá Jªªe± à=r-ƒ|ÿÿ5n =áˆê €{âó% å±à@,€@ÿ ÿ* ë câ óå±Ôÿ$Xàb€báÁë!¨ £á륱 B@a¨ÀB(= –ëc@x@ßA$A¨àExÖ€ˆå±«¬bó =áj¦–aæâóå ±à@•€@å,±àJs” J!áå± B@aRàBå±@x@ÇA ARàE†MaãÇâ û­­aR =áRCU€=áR(oà@žP€@ë c³%ª£ðÿncƒ,àü"a SÛDfàb  bàá½å± B@jÀBëc@x@ßA$AjàE ajáóá j®®aj =áj” €=áj%pä(+cà@ð €@à~Áå±àBPàB$ ƒä–Ç B@JÀBå±@x@¿AAJ!àE“Äà Ëá J¯¯e±"à=ׄFå±# =ሃʀ{áˆ% áˆ"ó$à@ÎÅ€@ë câ ó屟ÿôT%àb/ bá ë!¨ £á¨XF& B@a¨ÀBëc@x@ßA$A¨'àE€bóñ°±bó( =áj@@`=çå ±)à@¡>€@‡ Ùæv' @ÿÿô*àJ ÀJ IàJå±+ B@aRàBñ@x@ÇA AR,àE'÷€Óõ\²²aR- =@þ€ƒÙþ€=â½!â½.à@3ú€@å ±Ã­¬S!ûVÇåð¯@ˆà¹ÿ'/àbŸù€bâ ½å±0 B@@]àB‚è oñ@x@ßA$Aj1àE®²bàˆä f³³aj2 =áj>¶€=áj%-áj%±3à@šµ€@à~å±4àBú´àB$ ƒä…±5 B@JÀBå±@x@¿AAJ6àE5naJã>á J´´e±7à=¶¤Få±8 =áˆ!t€{áˆ% áˆ%±9à@ko€@ÿ ÿ¦$F @ñÃâ óå±ÿ,þ:àbÑá Áë!¨ bᨅ±; B@a¨ÀB˜â ó"ôöÇ@x@ßA$A¨<àE»)a¨ëcµ¶bó= =ájîé€=å"±>à@Nè€@ë,c?àJºç J þå ±@ B@aRàBëc@x@ÇA ARAàEÉ  aRàˆâ û··aRB =áRs¨€=áR"½Cà@É£€@ù …)ø%òâ½K >Ó®Ñú£ïð¯®ÑúÑŠïa oD©Dàb5 bàá½å±E B@jÀB…â ½å±@x@ßA$AjFàEP\ ajäf¸¸ajG =ájÔ_€=áj%-ä(%±Hà@0 @á !j @ãűIàB^€ƒàÁÁJ$àB¥±J B@JÀBÿÿ; @x@¿AAJKàEÖ aJàËâ µ¹¹e±Là=W*ƒ|å±M =áˆÎ€{å"±Nà@€@ÿÿ/¼â óå±¹AëOàbr€báá¨å ±P B@a¨ÀBñ@x@ßA$A¨QàE]Ó€ˆå±º»bóR =áj”“ aæâó$âó"´Sà@ô‘€@å,±TàJ`ÀJA#ë®ëcU B@aRàBå±@x@ÇR =ARV @`EjJ aàˆâ û¼¼aRW = ?`=R€=áR%±Xà@oM€@ÿ ÿ¦ãðÑŠ£åðäõõõõ Œà6ÿCYàbÛL€bä få±Z B@€æÀB}å±@x@ßA$Aj[àEñáj½½aj\ =ájm €=ä(%-â½%±]à@Ê€@á å±^àB- Bâ,ä ƒåô¥±_ B@JàB@Ùâø&ýÿÿB@x@¿AAJ`àExÁ€Ëàˆâ µ¾¾e±aà=ù¤FÿÿF„b =@®€ÁhÇ€{áˆ% áˆ"ócà@³Â€@ù »ë câ óå±’ÿIxdàb báᨠ£á¨‹ce B@`æÀBñ@x@ßA$A¨fàEÿ|bóÿÿ—é¿¿bóg =áj¤€=ájå ±hà@€€@â m!j @ñiàJs JA#â¸å±j B@aRàBÿÿB@x@ÇA ARkàE…8aRàˆâ ûÀÀaRl =áR>@€=å"±mà@;€@â ½$F!Rëcóõõ"Óï”îd€”€a îÇD©nàbù:€bàá½å±o B@jÀBÿÿô@x@ßA$>pàE ôájÁÁajq =ájœ÷€=â½%-ä(%±rà@÷ö€@å$±sàBX Bá Jëct B@JÀBñ@x@¿AAJuàE“¯áJÂÂe±và=Ÿ}ƒ|ïÿÿtw =ላµ€{áˆ% áˆ%±xà@а€@ÿ ÿ@Ò"ó @âóå±J5Aëyàb/ bᨠbå ±z B@a¨ÀBëc@x@ßA$A¨{àEka¨ëcÃÄbó| =áj:+`=áj$áj+c}à@œ)€@å,±~àJÀJA#å± B@aRàBöÇ@x@ÇA AR€àE'â Ó  ˆå ±ÅÅaR =áRëé€=áR"½‚à@7å€@ó Ó"½!“â½ ?@"Ãï”E¯P@¸À2íB½ƒàb£ä€bàá屄 B@jÀBëc@x@ßA$Aj…àE®bàˆá jÆÆaj† =áj6¡€=ájå ±‡à@‘ €@å$±ˆàBöŸ€BàÁÁJ屉 B@JàB@Ù¢oÀBëc@x@¿AAJŠàE4YáJÇÇe±‹à=¶¤Fÿÿ/¼Œ =@®€Á9_€{è¥% äF"óà@rZ€@« òá)ø @âóà â óå±–BóŽàbÐá Áe!¨ bå ± B@`æÀB°â ó!¨öÇ@x@ßA$A¨àE»a¨ÿÿ$XÈÉbó‘ =ájèÔ€=áj$áj%±’à@JÓ€@â m!j @ëc“àJ¶Ò J þáµëc” B@aRàBëc@x@ÇA AR•àEÉ‹aRãÇä FÊÊaR– =áR…“€=äF!áR"½—à@ÕŽ€@ëc?""£ð‚9àÿí"‚:a ˜ÿîl˜àbA bàá½ëc™ B@jÀBëc@x@ßA$AjšàEOGajáóá jËËaj› =áj×J€=ájå ±œà@2 @à~Á' @ãÅ"üDàB“I€ƒà B$àB±ž B@JÀBëc@x@¿AAJŸàEÖáJÌÌe± à=Wƒ|ÿÿ ¡ =áˆÒ€{áˆ% âó¢à@€@ÿÿôâ óå±<ïA¨£àbr€bᨠ£á뱤 B@a¨ÀB¨å±@x@ßA$A¨¥àE]¾€ˆÿÿ,ÍÎbó¦ =ájŠ~aæå"±§à@ì|€@ÿ,ÿ,¨àJXÀJA#çg屩 B@aRàBå±@x@ÇA ARªàEj5aáÛä FÏÏaR« =áR'=€=â½å ±¬à@z8€@å ±#%¨ $"å!0ãS!÷ñ»a óÜB½­àbæ7€bàáĩ屮 B@jÀBÿÿ @x@ßA$Aj¯àEñðà ˆá jÐÐaj° =ájyô€=áj%-`±€3ÒA±à@Ôó€@á å±²àB5àBå±³ B@JÀBå±@x@¿AAJ´àEx¬bµã>á JÑÑe±µà=ù¤Få±¶ =áˆt²€{áˆ% áˆ(¥·à@µ­€@å ±)ø @ëcâ óå±W.Aë¸àb bá ë!¨ bå ±¹ B@a¨ÀBå±@x@ßA$A¨ºàEþga¨àˆá ¨ÒÒbó» =ájŸl€=å"±¼à@þj€@ë,c½àJjÀJA#å±¾ B@aRàBÿÿQè@x@ÇA AR¿àE…#áRÓÓaRÀ =áR0+€=áR"½Áà@&€@üy?3ñ^àBï´öåRp€ à3QB½Âàbù%€bá jå±Ã B@jÀBÿÿL6@x@ßA$AjÄàE ßà ˆâ ½ÔÔajÅ =ájâ€=ä(å ±Æà@íá€@ä对å±ÇàBL Bá J$ ƒäÿ/¼È B@JÀBå±@x@¿AAJÉàE“šbµàËá JÕÕe±Êà=šhƒÇööÇË =ሃ €{áˆ% å±Ìà@Λ€@è¥ @å±â óå±—£BóÍàb/ báÁë!¨ b᨜yÎ B@a¨ÀBüy@x@ßA$A¨ÏàEV a¨öÇÖ×bóÐ =ájL!`=ájê.Ñà@¬€@å,±ÒàJÀJA#å±Ó B@aRàBå±@x@ÇA ARÔàE'Í Ó@Y ˆâ ûØØaRÕ =áRÒÔ€=å"±Öà@#Ѐ@ö ÇC+]â%¬ÿ~ÆÑÃÿÿÃÐÖïB½×àbÏ€bàá½å±Ø B@jÀBŠî !ÿÿ/¼@x@ßA$AjÙàE­ˆ"bàˆá jÙÙajÚ =áj6Œ€=áj%-ä( =áRÿ$€=áR"½?à@O €@ÿ7+¤èo “IF³E²²àýîa  kB½@àb»€bàáå±A B@jÀB‹î !üy@x@ßA$AjBàEÖØ€ˆç¥á jòòajC =ájfÜ€=ä(%-ä(%±Dà@ÀÛ€@à~Áå±EàB" Bá Jå±F B@JÀBå±@x@¿AAJGàE\”5âµóóe±Hà=ݤFëcI = ÀÁQš€{âó% áˆ"óJà@••€@ÿ ÿÞ!ˆ @âóå±­™AëKàbøá Áë!¨ bå ±L B@`æÀB â ó"ôñ@x@ßA$A¨MàEãO6a¨å±ôõbóN =áj7`=áj$áj%±Oà@b€@å,±PàJÎ  J þå ±Q B@aRàBå±@x@ÇA ARRàEðÆà ˆä FööaRS =!J ƒ­Î€=áR"½Tà@ùÉ€@î!!“â½£üñ´àýIú£  ŒàI"B½Uàbe bä ©ëcV B@jÀBâ ½å±@x@ßA$AjWàEw‚8báóá j÷÷ajX =áj÷…€=å"±Yà@R @á $(%òãÅ"üAJZàB³„€ƒàÁäàB«c[ B@JÀBå±@x@¿AAJ\àEþ=9aJüyøøe±]à=P„Få±^ =áˆD€{áˆ% äF"ó_à@??€@§ÿÿôâ óå±íÐA¨`àbž>€bᨠ£áë«ca B@a¨ÀB¬â ó"ôå±@x@ßA$A¨bàE…ù€ˆÿÿ,ùúbóc =áj±¹:aæáj$áj"´dà@¸€@å,±eàJ· J þå ±f B@aRàBÿÿ¦@x@ÇA ARgàE’p;aâûûûaRh =áRKx€=å"±ià@–s€@å ±³IúC!"ïðäõõÿ"ÿÿQèn}B½jàb bá jå±k B@jÀBÿÿ¦@x@ßA$AjlàE,<ájüüajm =áj…/€=â½%-â½+cnà@à.€@å$±oàBE Bá J)º ƒä‘p B@JàB î\$öÇ@x@¿AAJqàE ç€Ëÿÿ(aýýe±rà=%ú„Fÿÿ¦s = ž€Á¨í€{áˆ% áˆ%±tà@áè€@ÿ ÿ¦': @è¥â óå±ÊAëuàb@ bá ë!¨ bᨑv B@`æÀBÿÿ¦@x@ßA$A¨wY`D E&£=bóÿÿ¦þÿbóx =À=Sc>`=ájå ±yà@µa€@â m2€ @ l@ Ã! (€ … ARzàJ!ÀJ â¸öÇ{ B@aRàBå±@x@ÇA AR|àE4?aàˆé ø A} =áRà!€=áR"½~à@4€@ö ÇÃIF"õ‚ä4ƒõƒäÿÿxßBAjàb €bàáÊå±€ B@jàBè oëc@x@ßA$AjàEºÕà ˆá jaj‚ = …€ƒCÙ€=ç%-ä(%±ƒà@žØ€@‡ Râr$( @ãÅ(®@B„àBÿ×àB$àB¥±… B@€ÆÀBÿÿ @x@¿AAJ†àEA‘@âµ@=‡à=ƤFÿÿbþˆ =áˆ=—€{âó% áˆ"ó‰à@’€@ÿ ÿ â óå±{ÿ»`ŠàbÝá Âó!¨ £á륱‹ B@a¨ÀBÿÿô@x@ßA$AjŒàEÈLAa¨üybó =ájù B`=âó%páj"´Žà@[ €@å,±àJÇ  JA#å± B@aRàBñ@x@ÇA AR‘àEÕÃà ˆä FaR’ =áR†Ë€=â½áR"½“à@ÙÆ€@ö ÇÓ"}{zy¯~a ò÷Df”àbE bá j展 B@jÀBÿÿ¦@x@ßA$Aj–àE\Cbàˆá j G— =ájì‚€=áj%-â½%±˜à@G @à~ÃÅå±™àB¨€ƒà B$ ƒä‹cš B@JÀBñ@x@¿AAJ›àEã:DáJe±œà=dM„Fÿÿ¦ =áˆÛ@€{áˆ% áˆ"óžà@ <€@ü y!ˆ @ëcâ óå±VAëŸàb;€bᨠbᨋc  B@a¨ÀBüy@x@ßA$A¨¡àEjö€ˆå± bó¢ = …`ƒ“¶Eaæå"±£à@ô´€@å,±¤àJ`ÀJ å ±¥ B@€ÎÀBëc@x@ÇA AR¦àEwmFaàˆä F  aR§ = …€ƒ$u€=â½å ±¨à@wp€@ë cãå"0ã S"÷€~ű$ÌÖ`>õB½©àbão€bàáĩ屪 B@€æÀBå±@x@ßA$Aj«àEþ(Gáj  aj¬ =ájŽ,€=áj%-ä(%±­à@é+€@á $( @ãÅ(®@B®àBJàB屯 B@JÀBå±@x@¿AAJ°àE„äà Ëâ µ  e±±à=÷„Få±² =áˆyê€{äF% áˆ%±³à@¾å€@å ±/ª @å±à â ó屚A¨´àb  bá ë!¨ bá륱µ B@a¨ÀB î W.XöÇ@x@ßA$A¨¶àE  Hâó  bó· = ``ƒ¬¤€=ájä^"´¸à@ £€@å,±¹àJw¢ J å ±º B@€ÎÀBÿÿô@x@ÇA AR»àE’[IáRaR¼ =áRAc€=áR"½½à@’^€@ë có"S!þÇåð£äð£åa Tÿפ¾àbþ]€bá j屿 B@jÀBÿÿ¦@x@ßA$AjÀàEJajð–ä fajÁ =áj©€=å"±Âà@ @à~ƹå±ÃàBe€ƒá J$åô«cÄ B@JÀBå±@x@¿AAJÅàEŸÒà ˆá J >Æà=§¤Fÿÿ$XÇ =ሠ؀{âó% å±Èà@ÛÓ€@¨ü y!ˆ @å±Kâ óå±ðÿÝŒÉàb; bᨠbå ±Ê B@a¨ÀB¬å±@x@ßA$AjËàE&ŽKbóû&bóÌ = …`ƒÜ’€=âóáj%±Íà@>‘€@å,±ÎàJª J å ±Ï B@€ÎÀBëc@x@ÇA ARÐàE­ILaRàˆâ ûaRÑ =áRvQ€=â½áR%±Òà@ÁL€@üy@%¥   a ¸ÈHoÓàb- bàáÄ©å±Ô B@jÀBüy@x@ßA$AjÕàE4Majå2á jajÖ =áj¨€=áj%-ä(+c×à@ @à~Á$( @ãÅ"ü@BØàBd€ƒà Bå±Ù B@JÀBöÇ@x@¿AAJÚàEºÀ€ˆáÓá Je±Ûà=ƤFðÿÿ* Ü =ሺƀ{äF% áˆ"óÝà@øÁ€@ÿÿ,â óå±:A¨ÞàbV báÁë!¨ £áë«cß B@a¨ÀBå±@x@ßA$A¨ààEA|NbóÿÿÞbóá =ájvàENÞ€ëáóä f**aj? =ÀƒÎá€=áj%-ä(+c@à@) @à~ÃÅå±AàBŠà€ƒà Bå±B B@ àBŒ@Ùâø!KŽ@x@¿AAJCàEÕ™^âµ++e±Dà=ݤFÿÿñ`E =@®€ÁÉŸ€{áˆáˆ"óFà@›€@ö Çâ óå±.rAëGàbqš€bᨠ£å ±H B@`æÀB â ó!¨ÿÿÞ@x@ßA$A¨IàE\U_a¨ã|â ó,,bóJ =áj Z€=ê%±Kà@lX€@å,±LàJØW J þâ¸ëcM B@aRàBÿÿh°@x@ÇA >NàEâ`aRàˆá R--aRO =áR€=áR"½Pà@ê€@Œè o$F'Eî!c11E¶%´Pa ºXB½QàbV bàáÄ©å±R B@jÀBëc@x@ßA$AjSàEiÌ€ëàˆá j..ajT =ájñÏ€=ä(å ±Uà@N @à~Á!j @ãÅ"üAJVàB­Î€ƒà Bå±W B@JÀBå±@x@¿AAJXàEð‡aâµ//e±Yà=ø¤FõñZ =áˆà€{áˆ% ç:"ó[à@+‰€@ÿ ÿ Òö Çâ ó屪A¨\àbŒˆ€bᨠ£áë«c] B@a¨ÀBëc@x@ßA$A¨^àEwCba¨üy01bó_ = …`ƒªc`=ä^"´`à@ €@å,±aàJu J èjå±b B@€ÎÀBñ@x@ÇA ARcàE„ºà ˆä F22aRd =áR'€=áR"½eà@ˆ½€@ˆå±s± ðÇ>çuàtƒB½fàbô¼ b@(ájå±g B@jÀBzè oëc@x@ßA$AjhàE vdbàˆá j33aji =áj›y€=ä(%pä(+cjà@øx€@èîå±kàBW BàÁÀBå±l B@JÀBŒ@Ùá!Këc@x@¿AAJmàE‘1eáJ44e±nà=D„Fÿÿto =@®€Áz7€{âó% áˆ"ópà@Í2€@ÿÿ6 @è¥â óå±ÐqAëqàb- bᨠbå ±r B@`æÀB”â ó!¨Í @x@ßA$A¨sàEí€ëàˆâ ó55bót =ájºñ€=âó)Ïáj%±uà@ð€@å,±vàJˆï J þâ¸å±w B@aRàBñ@x@ÇA ARxàEŸ¨fâû66aRy =áR\°€=â½áR"½zà@·«€@ë cƒ4˜å:ð"‚Žƒåa z®B½{àb# bá jå±| B@jÀBñ@x@ßA$Aj}àE&dgajå2â ½77aj~ =ájªg€=â½å ±à@ @à~Ånëc€àBff€ƒá Jå± B@JÀBå±@x@¿AAJ‚àE¬haJŒ@Y¥÷á J88e±ƒà=´¤Fñ„ =ሡ%€{âó% äF"ó…à@î €@ö Çâ óå±Þ\Bó†àbL bᨠ£å ±‡ B@a¨ÀBëc@x@ßA$A¨ˆàE3Û€ëüy9:bó‰ =ájd›iaæâóä^%±Šà@Æ™€@å,±‹àJ2ÀJA#¢rÃ>屌 B@aRàBå±@x@ÇA ARàEARjaãÇâ û;;aRŽ =áRõY€=áR"½à@QU€@ö Ç“ð£å Æå Æ£å ðÿÿxƒ B½àb½T€bàáÁj屑 B@jÀBå±@x@ßA$Aj’àEÇ kajáóá j<?bóž =ájòDm`=çå ±Ÿà@TC€@‡@Såf2€ @ßÿ"åÿ ÿ/¼ àJ¿B€JârÁR屡 B@aRàBå±@x@ÇA AR¢àEâûà ˆâ û@@aR£ =@þ€ƒ“naRâ½!⽤à@îþ€~ë c £$Ñå ðï$@ a ‚B½¥àbZ bá j屦 B@€æÀBÿÿ @x@ßA$Aj§àEi·,àˆá jAAaj¨ =ájýº€=ájå ±©à@X @à~Âr屪àB¹¹€ƒà B屫 B@JÀBå±@x@¿AAJ¬àEðrobwæ}á JBBe±­à=u…„Fÿÿ/¼® =áˆÜx€{áˆ% âó¯à@)t€@ö Çâ óå±¹pBó°àbŒs€báÁë!¨ £å ±± B@a¨ÀBÿÿŠÜ@x@ßA$A¨²àEv.pa¨ëcCDbó³ =áj¨î€=G@—åpä ^+c´à@ í€@ë,cµàJuì Jà ‹å±¶ B@aRàBå±@x@ÇA AR·àE„¥qaRî_EEaR¸ =áR9­€=â½å ±¹à@”¨€@å ±³1ÍåÑt ÿÿ5n%½B½ºàb bá jå±» B@jÀBå±@x@ßA$Aj¼àE arajàˆä fFFaj½ = { ƒ›d€=â½å ±¾à@öc€@à~ÃÅ屿àBW Bä ƒäèFÀ B@JÀBÿÿ¦@x@¿AAJÁàE‘sáJGGe±Âà=/ƒ|ÿÿ…*à =áˆ"€{âó% å±Äà@Ë€@ÿ ÿ â óå±OBóÅàb- bá ë `£á¨ŸÿBÆ B@a¨ÀBñ@x@ßA$A¨ÇàEØ€ëå±HIbóÈ =ájF˜taæâóä^%±Éà@§–€@å,±ÊàJÀJA#æ=å±Ë B@aRàBå±@x@ÇA ARÌàE%Ouaàˆä FJJaRÍ =áR×V€=áR1Îà@1R€@üyÃé£ðVÀ££"©äýñ¤šB½ÏàbQ€bàáÄ©å±Ð B@jÀBå±@x@ßA$AjÑàE¬ vájKKajÒ =áj0€=ä(%-ä( B@€ÆÀBëc@x@¿AAJ?àENŸ‡bµàËá Jcce±@à=Ó¤FüyA =áˆ>¥€{áˆ% áˆ"óBà@ˆ €@öÇâ óå±^ØAëCàbêá Âó!¨ £å ±D B@a¨ÀBëc@x@ßA$A¨EàEÔZˆa¨ñdebóF =áj ‰`=ä^%±Gà@k€@å,±HàJ× JA#å±I B@aRàBå±@x@ÇA ARJàEâÑà ˆâ ûffaRK =áRŒÙ€=áR"½Là@æÔ€@ó Ó)ø'Å‘_7K A#i1Í%¬%¬íðé"ka ‘B½MàbR bá jå±N B@jÀBå±@x@ßA$AjOàEhŠbàˆá jggajP =ájñ€=ájå ±Qà@M @äåÃÅ!j @ãÅ"üAJRàB¬€ƒàÁÀB$àB«cS B@JÀBå±@x@¿AAJTàEïH‹áJhhe±Uà=p[„Fÿÿ¦V =áˆÛN€{å"±Wà@&J€@—â ó)ø%Nâó+câ óå±5A¨Xàb‹I€bᨠbáë«cY B@a¨ÀBÿÿ @x@ßA$A¨ZàEvŒa¨ÿÿBijbó[ =áj Ä€=áj$ç"´\à@À@å,±]àJm JA#å±^ B@aRàBå±@x@ÇA AR_àEƒ{aRàˆä FkkaR` =Àƒ:ƒ`=äFáR%±aà@“~€@å ±3ýÎí%«&Š%¸ Œà5(B½bàbÿ}`£àáÄfå±c B@jÀBÿÿ; @x@ßA$AjdàE 7Ž`ˆÿÿûllaje =ájš:€=â½â½+cfà@÷9€@å$±gàBV Bá Jå±h B@JÀBå±@x@¿AAJiàE‘ò€Ëàˆâ µmme±jà=ƒ|€ÿÿ…*k =ህø€{áˆ% äF"ólà@Ìó€@›å ±': @å±(¥â ó屑ÚAëmàb- báÂó!¨ bå ±n B@a¨ÀB ÿ m2¾ÿÿ$X@x@ßA$A¨oàE®bóëcnobóp =áj>n`=ájä^%±qà@žl€@å,±ràJ ÀJ þå ±s B@aRàB|ÿÿ5n@x@ÇA ARtàE%%‘aàˆâ ûppaRu =áRÏ,€=áR"½và@-(€@ñ CkZ'%®1ia –ÿ‚6wàb™'€bàá½å±x B@jÀB~â ½ÿÿ,@x@ßA$AjyàE¬àà ˆá jqqajz =áj ä€=ájå ±{à@|ã€@á $( @ãÅ"üD|àBàâàB$àB«c} B@JàB èª!K @'@x@¿AAJ~àE2œ’âµrre±à=´¤FÿÿB€ =@®@™#¢€{è¥% å±à@n€@ë câ óå±2ÿˆ‚àbÎá Âó!¨ £áë«cƒ B@`æÀBñ@x@ßA$A¨„àE¹W“a¨å±stbó… =ájè”`=ájå ±†à@H€@â m,Î @öLJàJ´ J â¸öLj B@aRàBëc@x@ÇA AR‰àEÇ΀ˆ‡@Y ˆä FuuaRŠ =áR}Ö€=äF!⽋à@×Ñ€@ë c S$õ‚ä4õƒà%´ ëcÊ–DfŒàbC bàá½å± B@jÀBëc@x@ßA$AjŽàEMŠ•báóá jvvaj =ájÍ€=áj%-áj+cà@* @à~Á屑àB‰Œ€ƒà B$ ƒäŸÿ,’ B@JÀBëc@x@¿AAJ“àEÔE–áJwwe±”à=UX„Fÿÿ@Ò• =áˆÈK€{áˆ% áˆ(¥–à@G€@ÿ ÿB!ˆ @ëcâ óå±ndAë—àbpF€bᨠb᨟ÿ,˜ B@a¨ÀB è ¥'Zëc@x@ßA$A¨™àE[—a¨üyxybóš =ájvÁ€=å"±›à@Ö¿€@ÿ,ÿ$XœàJBÀJ þÅ(Ã>å± B@aRàBå±@x@ÇA ARžàEhx˜aRáÛä FzzaRŸ =áR€€=â½å ± à@d{€@ë ccKc "íðéuð¤"©BPà=B½¡àbÐz€bàáÁjå±¢ B@jÀBÿÿ @x@ßA$Aj£àEï3™áj{{aj¤ =áj7€=áj%-ä(%±¥à@Ü6€@á $( @ãÅ«c¦àB;àBå±§ B@JÀBå±@x@¿AAJ¨àEvï€Ëã>â µ||e±©à=÷¤F屪 =áˆfõ€{áˆ% áˆ%±«à@±ð€@ü y)ø @å±6Çâ ó屫ÏAë¬àb bá ë!¨ bá륱­ B@a¨ÀBëc@x@ßA$A¨®àEüªšbóÿÿyÆ}~bó¯ =áj(k›`=å"±°à@‡i€@ë,c±àJóh JA#ªÚÄFå±² B@aRàBñ@x@ÇA AR³àE "œaõ\aR´ =áRÁ)€=áR"½µà@%€@å ±säýï-1¿ÿ÷¼a S+¶àbŠ$€bá jå±· B@jÀBñ@x@ßA$Aj¸àE‘Ý€ˆäf€€aj¹ =áj!á€=ä(%pä(!ºà@~à€@à~Ârå±»àBÝßàB$ ƒä‹c¼ B@JÀBå±@x@¿AAJ½àE™bµáÓå ±e±¾à=˜«„F屿 =ሟ€{áˆ% áˆ"óÀà@Sš€@ë câ óå±÷ÿO*Áàb³á Áë!¨ £á¨‹c B@a¨ÀBå±@x@ßA$A¨ÃàEžTža¨ñ‚ƒbóÄ =ájÊŸ`=ä^(fÅà@-€@å,±ÆàJ™ J å±Ç B@aRàBÿÿ–@@x@ÇA ARÈàE«Ëà ˆâ û„„aRÉ =áRWÓ€=áR"½Êà@¸Î€@ÿ ÿ ƒtBðtZ)øæ`ÿ ’a +ZD©Ëàb$ bä ©å±Ì B@jÀBÿÿ@Ò@x@ßA$AjÍàE2‡ báóá j……ajÎ =ájŠ€=ä(%-ä(%±Ïà@  @á å±ÐàB~‰€ƒàÁä ƒä…±Ñ B@JÀBå±Ð 5@¿AAJÒ @€E¹B¡áJ††e±Ó[`< =:U„Få±Ô =à=¥H€{áˆ% áˆ"óÕà@õC€@ë câ ó屨AëÖàbU bᨠ£á¨…±× B@abÀBÿÿ,@x@ßA$A¨ØàE@þ€ëñ‡ˆbóÙ =ájc¾¢aæájïÂ%±Úà@Ƽ€@ˆ@SÿÿæÁ,Î @`ò# Ã!ÿÿ…*ÛàJ2¼ J (àJëcÜ B@aRàBÿÿô@x@ÇA ARÝàEMu£aàˆä F‰‰aRÞ =@þ€ƒ}€=å"±ßà@ax€@ÿ ÿvÒ$F!Rî!?7“1 Òû þ1ÀMý££àüàaB½ààbÍw€bàáÄfå±á B@€æÀB“Q‚áøÿÿ^·@x@ßA$AjâàEÔ0¤ájŠŠajã =ájP4€=â½%-ä(%±äà@®3€@å$±åàB Bá J)º ƒä…±æ B@JÀBÿÿ¦@x@¿AAJçàE[ìà Ëâ µ‹‹e±èà=ܤFuÿÿ,é =áˆ_ò€{áˆ% áˆ%±êà@—í€@ÿÿŠÜâ óå±ÅöAëëàb÷á Âó!¨ £á¨…±ì B@a¨ÀBÿÿŠÜ@x@ßA$A¨íàEá§¥bóëcŒbóî =ájùg¦`=áj$áj%±ïà@\f€@å,±ðàJÈe JA#äå±ñ B@aRàBå±@x@ÇA ARòàEï§aàˆâ ûŽŽaRó =áR¯&€=å"±ôà@"€@ÿ ÿå ±£Mý+[ àûM`1À]ýì]a 66B½õàbo!€bàá½å±ö B@jÀB‡å±@x@ßA$Aj÷àEuÚà ˆá jajø =ájêÝ€=áj%-â½%±ùà@D @å$±úàB¥Ü€ƒá J$ ƒä…±û B@JÀBëc@x@¿AAJüàEü•¨âµe±ýà=}¨¥`=ÿÿ¦þ =áˆô›€{äF% áˆ%±ÿà@9—€@ÿ ÿ²º)ø @è¥6Çâ óå±’ÄAë\ࣘ–€bᨠbᨅ± B@a¨ÀBÿÿ…*@x@ßA$A¨àEƒQ©ajëc‘’bó =áj«ª`=ájå ±à@€@å,±àJz JA#å± B@aRàBå±@x@ÇA ARàEÈà ˆä F““aR =áR@Ѐ=áR"½ à@˜Ë€@å ±³ýë]ô`€"‚Ža `1B½ àb bá jå± B@jÀB„å±@x@ßA$Aj àE„«bàˆá j””aj = …€ƒ—‡€=áj%-ä(%±à@ñ†€@à~ƹ' @ãÅ(®@BàBSàBå± B@€ÆÀBå±@x@¿AAJàEž?¬áJ••e±à=R„Få± =ሒE€{áˆ% áˆ"óà@×@€@å ±!ˆ @å±+câ óå±ÄÃA¨àb: bá ë!¨ bá륱 B@a¨ÀB â ó.Xÿÿ,@x@ßA$A¨àE$û€ëëc–—bó =ájU»­aæè¥å ±à@·¹€@å,±àJ#ÀJ þå ± B@aRàBå±@x@ÇA ARàE2r®aàˆä F˜˜aR =áRæy€=â½áR"½à@Bu€@ü yãàý%¨ƒà"$õ‚ä4a S…B½àb®t€bàáÄ©å± B@jÀBÿ$Åç_ñ@x@ßA$Aj!àE¹-¯áj™™aj" =ájE1€=áj%pä(%±#à@Ÿ0€@á $( @ãÅ¥±$àBàB$àB«c% B@JÀBå±@x@¿AAJ&àE?é€Ëÿ/šše±'à=Á¤Få±( =áˆ8ï€{äF% áˆ"ó)à@€ê€@ü yë câ óå±÷wAë*àbß¡á ë!¨ £á륱+ B@a¨ÀBå±@x@ßA$A¨,àEƤ°bóÿÿ,›œbó- = ƒód±`=ájä^"´.à@Uc€@å,±/àJÁb J äñ0 B@aRàBå±@x@ÇA AR1àEÔ²aï+å ±aR2 =áR€#€=áR"½3à@Ü€@å ±Óõ%¬uð¤$E°a ‹sB½4àbH bàáå±5 B@jÀBÿÿÞ@x@ßA$Aj6àEZ×€ëáóá jžžaj7 = …€ƒâÚ€=å"±8à@= @á å±9àB¢Ù€ƒàÁÁJ$å ±: B@€ÆàB@Ùæü&ýÿÿB@x@¿AAJ;àEá’³bµŒ 4ÀEá JŸŸe±<à=b¥„Fÿÿ¡¤= =@®€ÁÕ˜€{áˆ% å±>à@"”€@ü y!ˆ @ëcâ óå±/Bó?àb“€bᨠbå ±@ B@`æÀBÿÿ,@x@ßA$A¨AàEhN´a¨ñ ¡bóB =áj‘µ`=ä^%±Cà@ó €@ÿ,ÿ DàJ_ÀJA#â¸å±E B@aRàBå±@x@ÇA ARFàEuÅ€ÓáÛâ û¢¢aRG =áR*Í€=áR%±Hà@È€@ÿ ÿBãE°©ÿÿäa S;B½IàbíÇ€bàáÄfëcJ B@jÀBñ@x@ßA$AjKàEü€¶bàˆá j££ajL =ájŒ„€=ä(%-ä(+cMà@烀@â r$( @ãÅ"ü@BNàBH BàÁÁJå±O B@JÀBëc@x@¿AAJPàEƒ<·aJàËá J¤¤e±Qà=O„FëcR =áˆwB€{áˆ% áˆ"óSà@À=€@ë câ óå±÷ÉA¨Tàb bᨠ£áë«cU B@a¨ÀBëc@x@ßA$A¨VàE ø€ëñ¥¦bóW = ƒ2¸¸aæájë cXà@”¶€@è ,Î @ÿÿ,YàJÀJ å ±Z B@aRàBÿÿ5n@x@ÇA AR[àEo¹aàˆâ û§§aR\ = { ƒÌv€=å"±]à@'r€@ë cóí´ô¼t@ð"2ÅÿÿnbQÿL6^àb“q€bàáÄfå±_ B@jÀBñ@x@ßA$Aj`àEž*ºajäf¨¨aja =áj..€=áj%pä(%±bà@‰-€@à~Áå±càBê,àBå±d B@JÀBÿÿQè@x@¿AAJeàE$怈áÓâ µ©©e±fà=¥¤FÿÿF„g =áˆì€{äF% áˆ%±hà@bç€@—ÿÿ$Xâ óå±õÿL6iàbÀá Áë!¨ £å ±j B@a¨ÀBëc@x@ßA$A¨kàE«¡»bó屪«ból = …`ƒÀa¼`=áj%páj(fmà@"`€@å,±nàJŽ_ J å ±o B@€ÎÀBå±@x@ÇA ARpàE¸½aàˆâ û¬¬aRq = …€ƒf €=äFáR"½rà@É€@ÿÿ BiåP0ådåJdp^åMpëc>ÿ` sàb5 bä ©å±t B@€æÀBxù …öÇ@x@ßA$AjuàE?Ô ë@Y­Wá j­­ajv =ájÏ×€=â½å ±wà@+ @á å±xàB‹Ö€ƒàÁä ƒäŸÿ,y B@JÀBëc@x@¿AAJzàEƾâµ®®e±{à=G¢„Få±| =ሪ•€{áˆ% äF"ó}à@ü€@“å ±)ø @ñÃâ ó屚ÿeò~àbb bᨠb᨟ÿ, B@a¨ÀBâ ó"ôüy@x@ßA$A¨€àEMK¿a¨ñ¯°bó =ájv À`=ájä^%±‚à@× €@å,±ƒàJCÀJ £½Âû `‹áR-‚û„ B@aRàBÿÿ¦@x@ÇA AR…àEZ Ó@Y ˆä F±±aR† =áRÊ€=áR"½‡à@jÅ€@ëcBåOdENpuV€GåMd@¸ÀÉÿÆŽˆàbÖÄ€bàáÁj屉 B@jÀBÿÿ5n@x@ßA$AjŠàEá}Ábàˆá j²²aj‹ =áji€=áj%-ä(+cŒà@Å€€@å$±àB% BàÁÁJ)º ƒâµ¥±Ž B@JÀBå±@x@¿AAJàEh9ÂaJ‹@Yâµ³³e±à=é¤F屑 =áˆX?€{áˆ% áˆ"ó’à@¦:€@ëcâ óå±m’Aë“àb bá ë!¨ £á¨…±” B@a¨ÀBÿÿB@x@ßA$A¨•àEîô€ëüy´µbó– =áj µÃaæê%±—à@m³€@å,±˜àJÙ² JA#¢rÂûå±™ B@aRàBëc@x@ÇA ARšàEükÄaàˆä F¶¶aR› =áR­s€=â½ç"½œà@o€@å ± #p(åS`$åK´ %½a ˆ¤B½àbtn€bàáÁj属 B@jÀBå±@x@ßA$AjŸàE‚'Åáj··aj  =áj+€=â½%-ä(C¡à@b*€@á ëc¢àBÆ)àB$ ƒäˆg£ B@JàB@Ùâø)ºüy@x@¿AAJ¤àE ãà ˆâ µ¸¸e±¥à=Šõ„Fÿÿ/¼¦ =@®€Áùè€{âó% áˆ"ó§à@Cä€@ü yë câ óå±,VAë¨àb¥á Âó!¨ £á¨…±© B@`æÀBñ@x@ßA$A¨ªàEžÆbóëc¹ºbó« =ájÂ^Ç`=âóä^%±¬à@#]€@â m!j @öÇ­àJ\ JA#â¸å±® B@aRàBå±@x@ÇA AR¯àEÈaàˆâ û»»aR° =áRS€=â½!â½±à@­€@å ±3‹_€+%³%³a G‡B½²àb bàá½å±³ B@jÀBÿÿ/¼@x@ßA$Aj´àE$Ñ€ëàˆá j¼¼ajµ =áj¤Ô€=áj%-áj%±¶à@ @à~Áå±·àBdÓ€ƒà B$ ƒä…±¸ B@JàB@Ùâøå±@x@¿AAJ¹àE«ŒÉâµ½½e±ºà=,ŸÆ`=ÿÿ@Ò» =@®€Á›’€{áˆ% áˆ%±¼à@å€@å ±!ˆ @ñ+câ óå±ãuAë½àbG bᨠbᨅ±¾ B@`æÀBå±@x@ßA$A¨¿àE1HÊajëc¾¿bóÀ =ájpË`=å"±Áà@Ѐ@â må±ÂàJ<ÀJ â¸å±Ã B@aRàBñ@x@ÇA ARÄàE?¿ Ó  ˆä FÀÀaRÅ =áR寀=áR"½Æà@G€@ö ÇCd…³€åM´@¸Àob½Çàb³Á b@% áÄ©å±È B@jÀByó ÓöÇ@x@ßA$AjÉàEÆzÌbàˆá jÁÁajÊ =ájN~€=ä(%-ä(%±Ëà@ª}€@ï )$( @ãÅ(®@BÌàB  BàÁÁJå±Í B@JÀBñ@x@¿AAJÎàEL6ÍáJÂÂe±Ïà=ΤFñÐ =áˆ1<€{âó% áˆ"óÑà@ƒ7€@ÿ ÿ6$F @å±6Çâ óå±Ëh¥Òàbèá ᨠbáë¥±Ó B@a¨ÀBöÇ@x@ßA$A¨ÔàEÓñ€ëüyÃÄbóÕ =áj²Îaæájê"´Öà@n°€@å,±×àJÚ¯ JA#äF屨 B@aRàBå±@x@ÇA ARÙàEáhÏaãÇä FÅÅaRÚ =áRp€=áR"½Ûà@ík€@ë cS+Z 11%¼a qÜàbY bàá½å±Ý B@jÀB}å±@x@ßA$AjÞàEg$Ðajáóá jÆÆajß =ájã'€=å"±àà@@ @á å±áàBŸ&€ƒàÁÁJå±â B@JÀBå±@x@¿AAJãàEîßà ˆá JÇÇe±äà=oò„F|3ã|ÿ&1 \ Aå = €ÁÞå€{áˆ% 屿à@)á€@ë câ óå±§qçàbŠà€bᨠ£å ±è B@`æÀBëc@x@ßA$A¨éàEu›ÑbóñÈÉbóê =áj[Ò`=ájå ±ëà@ðY€@ÿ,ÿ ìàJ\ÀJA#‚*âûå±í B@aRàBå±@x@ÇA ARîàE‚ÓaáÛâ ûÊÊaRï =áR)€=å"±ðà@Š€@ë cc uV…OW7"äõV%a @ÿ]Lñàbö b ájå±ò B@jÀBÿÿ/¼@x@ßA$AjóàE Îà ˆá jËËajô =áj‰Ñ€=â½ä(+cõà@æÐ€@â rå±öàBI Bá Jå±÷ B@JàB@ÙáöÇ@x@¿AAJøàE‰ÔbµàËá JÌÌe±ùà=œ„Ftå±ú =@®€Á|€{áˆ% å±ûà@ËŠ€@ÿ ÿ,â óå±ä®Aëüàb, bᨠ£å ±ý B@`æÀBüy@x@ßA$A¨þàEEÕa¨ñÍÎbóÿ =ájIÖ`=ájå ±]à@©€@â m,Î @öÇàJÀJ câ¸å± B@aRàBÿÿ; @x@ÇA ARàE$¼ Ó $Àˆâ ûÏÏaR =áRÓÀ=å"±à@(¿€@”HÑãÀ$F!Rî!3ÓsïTþt¨€Ã3ØüT ·à–ÿ@Òàb”¾€bàáÊå± B@jÀB†ëc@x@ßA$AjàE«w×bäfÐÐaj =áj+{€=â½å ± à@ˆz€@à~Á!j @ãÅ.`D àBçyàBå± B@JÀBëc@x@¿AAJ àE13ØaJáÓâ µÑÑe±à=²¤F}å± =áˆ)9€{áˆ% å±à@m4€@ÿÿ$Xâ óå±2ÿ@ÒàbÍá Áë!¨ £áë± B@a¨ÀBÿÿ* @x@ßA$A¨àE¸î€ëá¨ÒÒbó =ájNó€=ä^"´à@°ñ€@å,±àJÀJA#ä©å± B@aRàBÿÿô@x@ÇA ARàE?ªÙâûÓÓaR =áRë±€=â½%pâ½à@C­€@屃>ôþï ç Ç­à^ðÇßa D)Dfàb¯¬€bá jå± B@jÀBÿÿô@x@ßA$AjàEÅeÚájÔÔaj =ájNi€=â½%páj+cà@¨h€@á å± àB Bå ô$ ƒäœy! B@JàB@Ùâøëc@x@¿AAJ"àEL!ÛáJÕÕe±#à=TïƒÇÿÿÃÐ$ =@®€Á@'€{âó% áˆ4 %à@‰"€@ÿÿôâ ó ›”hv¼ï/GÌï?àÛAë&àbèá å±' B@`æÀB è ¥!¨ÿÿ @x@ßA$A¨(àEÓÜ€ëñÖ×bó) = ``ƒÜaæâóä^%±*à@j›€@å,±+àJÖš J â¸å±, B@€ÎÀBå±@x@ÇA AR-àEàSÝaàˆè ­ØØaR. =áR‰[€=áR"½/à@äV€@ö Ç“€ Ǭe¬Þ "a ÔUB½0àbP bàá½å±1 B@jÀBÿÿô@x@ßA$Aj2àEgÞájÙÙaj3 =ájó€=ä(%-ä(%±4à@N @å nå±5àB¯€ƒá Jå±6 B@JÀBëc@x@¿AAJ7àEîÊà ˆâ µÚÚe±8à=oÝ„Fzëc9 =áˆÞЀ{áˆå ±:à@+Ì€@ö Çâ óå±p§Aë;àbŠË€bᨠ£çZŸÿ,< B@a¨ÀBöÇ@x@ßA$A¨=àEu†ßbóâtá ¨ÛÛbó> =áj‹€=ájå ±?à@}‰€@å,±@àJ鈀JàÉÄFå±A B@aRàBöÇ@x@ÇA ARBàEûAàaRáÛá RÜÜaRC =áR¢I€=å"±Dà@ûD€@ë c£Ç×ïð$@ðï´ó"äa Ì#B½Eàbg bàáÁjå±F B@jÀB‚è oÿÿ,@x@ßA$AjGàE‚ý€ëàˆá jÝÝajH =ájîáa¨â½å ±Ià@K @â rå±JàB®ÿà ÁÁJå±K B@JàB@Ùëc@x@¿AAJLàE ¹ áJÞÞe±Mà=‡ƒ|ñw „àƒ€ Üy`µ\yN = ) Áý¾€{áˆ% ç:cOà@Dº€@ÿ ÿbþ5\ @ô +câ óå±hÑBóPàb¥¹€báᨠbå ±Q B@a¨ÀBëc@x@ßA$A¨RàEtâbµÿÿ,ßàbóS =ájº4ã`=ájå ±Tà@3€@â m!j @öÇUàJ†2 J ‚*áRå±V B@aRàBöÇ@x@ÇA ARWàE뀈Œ@Y ˆä FááaRX =áRTó€=å"±Yà@­î€@å ±³þtP.øäöî´ õäõTõ@¸ÀòoB½Zàb bá jå±[ B@jÀBå±@x@ßA$Aj\àE$§äbå2á jââaj] =áj¬ª€=â½å ±^à@  @à~ÃÅå±_àBh©€ƒá Jå±` B@JÀBëc@x@¿AAJaàEªbåaJàˆá Jããe±bà=/u„Fyëcc =ሟh€{âó% å±dà@æc€@å ±!ˆ @å±6Çâ óå±VBóeàbF báÁë!¨ bå ±f B@a¨ÀBå±e 5@ßA$A¨g @€E1æa¨å±äåbóh =ájTÞ€=âó%páj1ià@´Ü€@å,±jàJ ÀJ@Ý¥(ÁRå±k B@a àBå±@x@ÇA ARlàE>•çaRàˆâ ûææaRm = …€ƒ휀=áRcnà@G˜€@å ± ÃUþtQ.øv%²õï`Jöà\ûB½oàb³—€bä ©å±p B@€æÀBå±@x@ßA$AjqàEÅPèajçççajr =ájMT€=ájå ±sà@ªS€@áéÂr$( @`ò" "üAJtàB àB$àB¶Çu B@JÀBå±@x@¿AAJvàEL éaJàËâ µèèe±wà=ͤFå±x =áˆD€{áˆ% äF"óyà@ˆ €@ü y)ø @å±+câ óå±ø A¨zàbèá Áë!¨ ba“aëi‘{ B@a¨ÀBüy@x@ßA$A¨|àEÓÇ€ë¼ Z¤çᨠéêbó} =ájþ‡êaæçå ±~à@]†€@å,±àJÉ… J ®ÀÉÁRå±€ B@aRàBå±@x@ÇA ARàEà>ëaàˆáRA@ëëaR‚ =@€ƒ‡F€=â½áR"½ƒà@èA€@ÿ ÿÞÓäõP€åPÔPuP€ àîœB½„àbTÀb@&¢4Ájå±… B@€æÀBÿÿŠÜ@x@ßA$Aj†àEgú ë@Y ˆá jììaj‡ =ájïý€=áj%-ä(6Ljà@L @à~Á屉àB«ü€ƒà B$ ƒäœyŠ B@JÀBå±@x@¿AAJ‹àEîµìbµç…á Jííe±Œà=oÈ„Fÿÿ, =áˆÒ»€{áˆ% áˆ"óŽà@%·€@”ÿ m @å±+câ óå±JnAëàbж€báÁë!¨ ba“"‡ ¥± B@a¨ÀBÿÿB@x@ßA$A¨‘àEtqía¨ëcîïbó’ =áj˜1î`=å"±“à@û/€@å,±”àJgÀJA#£½ÁR展 B@aRàBÿÿ]L@x@ÇA AR–àE‚è Ó@Y ˆâ ûððaR— =áR5ð€=áR"½˜à@’ë€@ü yãuPäõdq[Ç®tð@¸ÀÖ_B½™àbþê€bàáÁj屚 B@jÀBÿÿbþ@x@ßA$Aj›àE¤ïbàˆá jññajœ =áj‘§€=áj%-ä(%±à@@à~Á属àBM Bä ƒä…±Ÿ B@JÀBÿÿ5n@x@¿AAJ àE_ðáJòòe±¡à=r„Få±¢ =áˆ{e€{å"±£à@Ë`€@å ±ë câ óå±r­Aë¤àb+ bá ëå±¥ B@a¨ÀBÿÿ @x@ßA$A¨¦àEñá¨óóbó§ =áj¹€=âóê.¨à@€@å,±©àJ† JA#¥(Ã>屪 B@aRàBÿÿ$X@x@ÇA AR«àEÖ€ˆã„ä FôôaR¬ =áRJÞ€=áR%±­à@¥Ù€@å ± óÇàäð£ðÇÝ ¬å±X.B½®àb bá j屯 B@jÀBÿÿ,@x@ßA$Aj°àE#’òâ½õõaj± =áj³•€=ä(%-å±²à@ @áéÂrå±³àBo”€ƒá J$ ƒå ±´ B@JÀBëc@x@¿AAJµàEªMóáJööe±¶à=²¤Fôüy· =ሖS€{áˆ*¿áˆ"ó¸à@àN€@ö Çâ óå±üÜBó¹àbF bᨠ£á¨‘º B@a¨ÀBå±@x@ßA$A¨»àE1 ôá¨÷÷bó¼ = …`ƒÍ €=ájå ±½à@- €@å,±¾àJ™  J å ±¿ B@€ÎÀBñ@x@ÇA ARÀàE¸Ä€ˆãÇå ±øøaRÁ =áRjÌ€=áR"½Âà@ÀÇ€@è o$F!“î!CÇÞ%ª­ ß ©t?àþÿNôÃàb, bàáÄ©å±Ä B@jÀBÿÿB@x@ßA$AjÅàE>€õâ½ùùajÆ = …€ƒ΃€=áj%-ä(+cÇà@, @á !j @ãÅ(®@BÈàBŠ‚€ƒâ µå±É B@€ÆÀBå±@x@¿A>ÊàEÅ;öáJúúe±Ëà=ͤFÿÿ÷Ì =ስA€{áˆ% áˆ"óÍà@=€@å ±ë câ óå±Z…A¨Îàba<€bᨠ£áë¥±Ï B@a¨ÀBÿÿÞ@x@ßA$A¨ÐàEL÷€ˆÿÿŠÜûübóÑ =ájƒ·÷aæê"´Òà@ãµ€@ÿ ÿþ!j$^ÿÿ,ÓàJOÀJA#ëcÔ B@aRàBÿÿÞ@x@ÇA ARÕàEYnøaûýýaRÖ =áRv€=â½!â½×à@iq€@üyCk` qGLjt0ð£qa ÑöB½ØàbÕp€bâ ½å±Ù B@jÀBƒ\æá­ÿÿÞ@x@ßA$AjÚàEà)ùájþþajÛ =ájh-€=áj%páj%±Üà@Å,€@á å±ÝàB$àBå±Þ B@JÀBå±@x@¿AAJßàEgå€Ë‹@Y¡è gÿÿe±àà=è¤Füyá =áˆWë€{áˆ% áˆ%±âà@¢æ€@ÿ ÿB!ˆ @è¥âóå±uÿk¤ãàb bä ^!¨ bå ±ä B@a¨ÀBëc@x@ßA$A¨åàEí úbóöÇAæ =ájaû`=å"±çà@|_€@ë,cèàJè^ JA#¢rÉøñé B@aRàBöÇ@x@ÇA ARêàEûüaàˆâ ûaRë =áR¦€=â½å ±ìà@€@üyC#%ªât+_ðä øta µÙD©íàbs b@$ájå±î B@jÀBüy@x@ßA$AjïàE‚Ó€ˆãFá jajð =áj×€=â½å ±ñà@cÖ€@á $( @ãÅ(®AJòàBÂÕ€BàÁµ$àB¶Çó B@JÀBöÇ@x@¿AAJôàEýâµ@=õà=‰¡„F~ÿÿ,ö =áˆô”€{âó% å±÷à@D€@î W)ø @å±+câ óå±+A¨øàb¤á ᨠbáë«cù B@a¨ÀBå±@x@ßA$AjúàEJþa¨å±bóû =áj³ ÿ`=âóä^"´üà@ €@å,±ýàJ‚ JA#äF `‹áR¿ÿF„þ B@aRàBå±@x@ÇA ARÿàEœÁà ˆä F G^áRLÉ€=áR(oà@­Ä€@å ±3%§ ãtðһҼǫå±iÿ àb bä få± B@jÀBÿÿ* `x@9 Aj @`E#}/ÞìÎaj =⽋€€=ä(%-ä(+cà@é€@á å±àBO Bâ µå± B@aàBÿÿt@x@¿AAJ àEª8aJàˆâ µ  e± à=+K„Fÿÿ =ሖ>€{áˆ% áˆ"ó à@æ9€@å ±!ˆ @å±(¥â óå±Íÿ àbF báå± B@a¨ÀBå±@x@ßA$A¨àE1ô€ëñ  bó =ája´aæä^%±à@ò€@å,±àJ/ÀJA#ÿ1çáRå± B@aRàBÿÿ¦@x@ÇA ARàE>kaàˆâ û  aR =áRòr€=áR"½à@Fn€@â½'Eâ½CðA¢t.'t 1a ^ÿ]Làb²m€bàá½å± B@jÀB‡ñ@x@ßA$AjàEÅ&ajò€  aj =áj1*€=ä(å ±à@)€@å$±àBñ(€Bá Jå± B@JàB@Ùâø2a @'@x@¿AAJàEK áJe±à=ͤFÿÿ, =@®@™(¥è€{áˆ%på±!à@Œã€@¤ÿ ÿÞë câ óå±úÿ]L"àbì¡á ë!¨ £ê«c# B@`æÀB¨â ó!¨ÿÿ$X@x@ßA$A¨$àEÒbóëcbó% =ájö]`=áj$áj%±&à@Y\€@â m!j @öÇ'àJÅ[ J â¸å±( B@aRàBå±@x@ÇA AR)àEàaàˆÿÿÿÿÃÐaR* =áRx€=å"±+à@Ѐ@å ±St@%© Pð"u @óa •J[,àb< bàáå±- B@jÀBöÇ@x@ßA$Aj.àEfЀëàˆá jaj/ =áj÷Ó€=ájå ±0à@Q @ã Å)Ú @ãÅ"üAJ1àB²Ò€ƒàÁÁJ$àB±2 B@JÀBöÇ@x@¿AAJ3àEí‹âµe±4à=nž„Fÿÿ$X5 =áˆá‘€{äFäF(¥6à@&€@ÿ ÿB!ˆ @è¥â óå±1A¨7àb‰Œ€bᨠá륱8 B@a¨ÀB å±@x@ßA$A¨9àEtG á¨bó: =ájL€=áj%på±;à@|J€@å,±<àJèI J å±= B@aRàBüy@x@ÇA AR>àEû aRãÇä FaR? =áR° €=áR"½@à@ €@ÿÿÞ Ccäõ "ÇÖtð! ðÿÿQèõB½Aàbw€bá jëcB B@jÀBâ ½üy@x@ßA$AjCàE¾à ˆá jajD =áj€=áj%-â½1Eà@aÁ€@á $( @å ±FàBÁÀàBå±G B@JàB@Ùâø!Këc@x@¿AAJHàEz bµàËá Je±Ià=Hƒ|õÿÿÞJ =@®€Áø€{áˆ% áˆ"óKà@C{€@—ëcâ óå±°AëLàb¤á çZ £á륱M B@`æÀBÿÿ,@x@ßA$A¨NàE5 á¨bóO =áj4:€=è¥å ±Pà@—8€@â mëcQàJÀJA#â¸ëcR B@aRàBöÇ@x@ÇA ARSàEñ Ó@Y ˆâ ûaRT =áRÅø€=áR"½Uà@ô€@ÿ ÿ,s*‚ó%° äð"¯q›¥@¸À4þB½VàbŠó€b擽å±W B@jÀBÿÿ¦@x@ßA$AjXàEœ¬ â½ajY =áj °€=ä(%pä(%±Zà@j¯€@á å±[àBЮ€Bâ,ÁJå±\ B@JÀBöÇ@x@¿AAJ]àE#háJe±^à=+6‚îå±_ =áˆn€{áˆ% áˆ"ó`à@_i€@ÿÿ/¼Câ óå± Aëaàb¿á ᨠ£å ±b B@a¨ÀBå±@x@ßA$A¨càEª#a¨ÿÿ¦bód =ájÆã€=ä^.eà@(â€@å,±fàJ”á J ¥(ÁRå±g B@aRàBñ@x@ÇA ARhàE·š@àˆå ±aRi =áRk¢€=áR"½jà@¿€@ñ ƒúçäõõÿï-“õò ¼à¡ÿL6kàb+ bá jå±l B@jÀBöÇ@x@ßA$@×màE>Vb½õ|ajn =ájÆY€=ä(å ±oà@$ @å$±pàB†X€ƒá Jå±q B@JàB@Ùâøëc@x@¿AAJràEÅaJàËâ µ  e±sà=F$ƒ|öÇt =@®€ÁÁ€{áˆ% å±uà@€@ö Çâ óå±ÿL6vàba€báÄ^!¨ £å ±w B@`æÀBöÇ@x@ßA$A¨xàEKÍ€ˆÿÿF„!"bóy =ájaæájë czà@â‹€@â mëc{àJNÀJA#â¸å±| B@aRàBå±@x@ÇA AR}àEYDaàˆâ û##aR~ =áRñK€=å"±à@QG€@ÿ ÿ, “ï´ ôt+hàDð"a ïHo€àb½F b ájå± B@jÀBÿÿÞ@x@ßA$Aj‚àEßÿ€ˆå±$$ajƒ =ájpa¨ájå ±„à@Ê€@ã Åëc…àB,àB$ ƒäŸÿ¦† B@JÀBÿÿÞ@x@¿AAJ‡àEf»€ËáÓâ µ%%e±ˆà=ë¤Följ =áˆJÁ€{äFå ±Šà@Ÿ¼€@ÿ ÿL6!ˆ @öÇâ óå±¾Bó‹àb bá屌 B@a¨ÀBô 'ZÞ"@x@ßA$A¨àEívbµÿÿ,&'bóŽ =áj7`=ájå ±à@t5€@å,±àJà4 J þäF屑 B@aRàBå±@x@ÇA AR’àEúíà ˆâ û((aR“ = …€ƒ¯õ€=äFáRc”à@ ñ€@ö Ç£ƒàÔPtQð€ƒ Œàwÿ•àbvð€bá jå±– B@€æv Bÿÿ¦@x@ßA$Aj—àE©bàˆá j))aj˜ =áj­€=â½%-éÚ1™à@l¬€@å$±šàBÍ«€BàÁä ƒå ±› B@JÀBüy@x@¿AAJœàEeáJ**e±à=‰w„F属 =áˆøj€{áˆ%páˆ"óŸà@Ef€@—öÇâ óå±Óÿè àb¤á ᨠ£á¨Ÿÿ¦¡ B@a¨ÀBñ@x@ßA$A¨¢àE a¨ñ+,bó£ =áj¯à€=ájä^1¤à@߀@å,±¥àJ}Þ JA#éøå±¦ B@aRàBÿÿ$X@x@ÇA AR§àEœ—aRàˆä F--aR¨ =áRLŸ€=áR"½©à@¤š€@ö dzà$ûðƒ~à0á†ÿ1®À{ÔD©ªàb bàá½å±« B@jÀBÿÿL6@x@ßA$Aj¬àE#Sáj..aj­ =áj³V€=áj%-ä(%±®à@  @á )Ú @ãÅ"ü@B¯àBoU€ƒà Bëc° B@JÀBå±@x@¿AAJ±àE©áJ//e±²à=+!ƒ|å±³ =ሞ€{è¥% áˆ"ó´à@ç€@ÿ ÿ¦!ˆ @ëcâ óå±ß&A¨µàbE bᨠbá륱¶ B@a¨ÀB ëc@x@ßA$A¨·àE0Ê€ëÿÿÞ01bó¸ =ájQŠaæájå ±¹à@³ˆ€@å,±ºàJÀJ å±» B@aRàBëc@x@ÇA AR¼àE>Aaéyå ±22aR½ =áRîH€=äFáR"½¾à@ND€@ñ Ã8å8$ `a$Ü`qÿÿ5n²ÿ‚6¿àbºC bA;ájå±À B@jÀB|â ½ÿÿ,@x@ßA$AjÁàEÄüà ˆá j33aj =ájL a¨áj%pä(%±Ãà@§ÿà ~ƹ$( @ãÅ¥±ÄàBàBå±Å B@JÀBå±@x@¿AAJÆàEK¸ áÓá J44e±Çà=̤F{ÿÿôÈ =áˆ7¾€{áˆ% áˆ"óÉà@ˆ¹€@ù »5\ @å±Ãâ óå±ÿˆÊàbçá Áë!¨ báë¥±Ë B@a¨ÀBÿÿB@x@ßA$A¨ÌàEÒs!bµá¨55bóÍ =ájrx€=å"±Îà@Òv€@å,±ÏàJ>ÀJA#äFëcÐ B@aRàBÿÿÞ@x@ÇA ARÑàEX/"áR66aRÒ =áR7€=áR"½Óà@i2€@œî !$F%òâ½?7Óüå9ð£å: ; <ða )~D©ÔàbÕ1€b擽å±Õ B@jÀBå±@x@ßA$AjÖàEßꀈáóä f77aj× =ájgî€=ä(%pä(%±Øà@Äí€@á !j @ãÅ¥±ÙàB# BàÁÁJ$àB¶ÇÚ B@JÀBå±@x@¿AAJÛàEf¦#âµ88e±Üà=ntƒÇÿÿL6Ý =áˆf¬€{áˆ% áˆ"óÞà@¡§€@¨â ó @âóå±ÿèðßàb bᨠbá륱à B@a¨ÀB¬â ó(¦öÇ@x@ßA$A¨áàEía$a¨ñ9:bóâ =áj"%`=ä^"´ãà@| €@ÿ,ÿ* äàJç€Já Rå±å B@aRàBÿÿ/¼@x@ÇA ARæàEúØà ˆä F;;aRç =áR¥à€=áR"½èà@Ü€@ö Çãå=ð0}9{ ‘a A„D©éàbnÛ€bá jå±ê B@jÀBÿÿ; @x@ßA$AjëàE”&bàˆá j<?bó÷ =ájÅË€=áj)Ïáj%±øà@%Ê€@ë,cùàJ‘É J ÈÀÉÁRå±ú B@aRàBå±@x@ÇA ARûàEœ‚)aRàˆáRA@@@aRü =@€ƒ[Š€=å"±ýà@°…€@î !)ø!“èoóÒ¯?ïý¯>‘rÿÿ¦y…þàb bàáÁjå±ÿ B@€æÀBÿÿ5n@x@ßA$Aj_  `E">*ájAAaj =áj§A€=ájå ±à@ @äåÁ!j @ãÅ"üDàBc@€ƒà Bëc B@aàBÿÿ,@x`¿ =AJ @€E©ù€ˆáÓâ µBBe±à=* ƒ|ÿÿÞ =ሡÿ€{äF% äF%±à@åú€@ÿ ÿôâ ó屪m àbE báÁë!¨ £áë«c B@abÀBÿÿL6@x@ßA$A¨ àE0µ+bóëcCDbó =ájcu,`=áj$áj"´ à@Ãs€@å,±àJ/ÀJA#¦{ÁRå± B@aRàBå±@x@ÇA ARàE=,-aàˆâ ûEEaR =áRõ3€=äFáR"½à@M/€@å±Dïð¯A%°@‘r£ð£ïÿÿ@Ò ÌDfàb¹.€bˆA;ájå± B@jÀBÿÿ¦@x@ßA$AjàEÄçà ˆá jFFaj =ájPë€=â½%pâ½+cà@®ê€@å nå±àB  BˆAáJ$ ƒäŸÿÞ B@JÀBëc@x@¿AAJàEK£.âµGGe±à=̤Få± =áˆ?©€{áˆ% áˆ"óà@ƒ¤€@å ±ë câ óå±­>Aëàbçá Ä^!¨ £á¨Ÿÿ, B@a¨ÀBüy@x@ßA$A¨ àEÒ^/a¨å±HIbó! =ájù0`=ä^%±"à@X€@å,±#àJÄ J âûå±$ B@aRàBå±@x@ÇA AR%àEßÕà ˆä FJJaR& =áRžÝ€=â½å ±'à@÷Ø€@ÿÿ,DåBðàÿñïpßÿ}€p‚B½(àbc bá jå±) B@jÀBå±@x@ßA$Aj*àEf‘1bàˆá jKKaj+ =ájö”€=áj%-ä(%±,à@S @à~ÃÅå±-àB²“€ƒà B$ ƒä…±. B@JÀBå±@x@¿AAJ/àEìL2áJLLe±0à=n_„Få±1 =áˆáR€{áˆ% áˆ%±2à@(N€@ù» @îWâ óå±ÔpAë3àbˆM€bᨠbᨅ±4 B@a¨ÀB¡öÇ@x@ß1 =A¨5 @€Es3a¨ÿÿ,MNbó6 =áj£È€=å"±7à@Ç€@å,±8àJrÆ J@Ýå±9 B@a àBÿÿ¦@x@ÇA AR:àE4aRéyä FOOaR; =áR<‡€=â½å ±<à@‘‚€@ë c#ð%¦ àÿ€üå=ñ»ZB½=àbý€bá jå±> B@jÀBÿÿô@x@ßA$Aj?àE;5ajàˆá jPPaj@ =áj>€=áj%-ä(%±Aà@Ý=€@à~å±BàBCàBå±C B@JÀBÿÿ¦@x@¿AAJDàEŽöà Ëá JQQe±Eà= ƒ|vÿÿ,F =ሆü€{áˆ% áˆ%±Gà@Ê÷€@ÿ ÿÌvë câ ó屨AëHàb* bá ë!¨ £å ±I B@a¨ÀBñ@x@ßA$A¨JàE²6âóRRbóK =áj¸¶€=å"±Là@µ€@å,±MàJ…´ JA#å±N B@aRàBzW»àBÿÿ¼³ @x@ÇA AROàEœm7aRå±SSaRP =áREu€=áR"½Qà@ p€@ë c3å<ðŸÿ':%»å9a ßB½Ràb  bàáÄ©å±S B@jÀBÿÿB@x@ßA$AjTàE")8ajáóä fTTajU = ƒª,€=ä(å ±Và@ @á ëcWàBf+€ƒàÁÁJ$ ƒä‹cX B@JÀBëc@x@¿AAJYàE©äà ˆá JUUe±Zà=±¤Fôÿÿ$X[ =ሙê€{áˆ% å±\à@ãå€@ë câ óå±{ìBó]àbE bᨠ£á¨‹c^ B@a¨ÀBüy@x@ßA$A¨_àE0 9bóñVWbó` =ájU`:`=ájê1aà@·^€@ÿ,ÿ,bàJ#ÀJ ånñc B@aRàBå±@x@ÇA ARdàE=;aáÛâ ûXXaRe =áRï€=å"±fà@Q€@ÿÿ DCðTÝ%ª àõp0‘{Òà«¡B½gàb½ b@(ájå±h B@jÀByî !ÿÿ @x@ßA$AjiàEÄÒ€ˆÿÿ¦YYajj =ájHÖ€=â½%-ä(+ckà@¤Õ€@â rå±làB Bá Jå±m B@JàB@Ùá!Kÿÿ/¼@x@¿AAJnàEKŽ`=ájå ±và@\€@â m!j @ÿÿ; wàJÈ J þÂrÈ­å±x B@aRàBÿÿ @x@ÇA ARyàEßÀà ˆâ û]]aRz =áRÈ€=å"±{à@ïÀ@ñ Sƒå>ðàýa‘³a Ÿÿê|àb[ bá jå±} B@jÀB‰â ½å±@x@ßA$Aj~àEe|?bàˆá j^^aj =ájî€=ájå ±€à@N @ã Åå±àB®~€ƒà B$ ƒä‹c‚ B@JàB@Ùå±@x@¿AAJƒàEì7@aJÿdû Eá J__e±„à=mJ„FÿÿÞ… =@®€Áì=€{äF% 屆à@/9€@ü y!ˆ @öÇâ ó屟ÿ Ò‡àb8€bâjÄ^!¨ bᨋcˆ B@`æÀBñ@x@ßA$A¨‰àEsó€ˆöÇ`abóŠ =áj™³Aaæáj%páj+c‹à@ú±€@å,±ŒàJfÀJA#¢rÁRå± B@aRàBå±@x@ÇA ARŽàE€jBaàˆâ ûbbaR =áRr€=áR(oà@m€@ÿÿ Dcå?U"ïuvuw"ïa gŽHo‘àbìl€bàáÁjå±’ B@jÀBÿÿL6@x 9A$Aj“ @`E&Cájccaj” =áj—)€=áj%-â½+c•à@ó(€@á $( @ãÅ"ü@B–àBSàB$àB¥±— B@aàBñ@x@¿AAJ˜àEŽáà Ëâ µdde±™à='ô„FdöÇš =áˆ~ç€{áˆ% áˆ"ó›à@Éâ€@—ÿÿ â óå±Ã|A¨œàb* bá ë!¨ £á륱 B@a¨ÀBñ@x@ßA$A¨žàEDbóñefbóŸ =áj;]E`=è¥å ± à@›[€@å,±¡àJÀJ å±¢ B@aRàBå±@x@ÇA AR£àE"Faàˆâ ûggaR¤ =áRÌ€=â½áR"½¥à@"€@ë csþí$ ÿä>"}l{ ÿÿ* lúB½¦àbŽ€bàáå±§ B@jÀBÿÿ @x@ßA$Aj¨àE©Ïà ˆá jhhaj© =ájÓ€=áj%-ä(%±ªà@yÒ€@á 屫àBÙÑàB$ ƒä‹c¬ B@JÀBå±@xª 9AAJ­ @€E/‹Gâµiie±®à=±¤Fëc¯ =áˆ(‘€{áˆ% áˆ"ó°à@jŒ€@ü y!ˆ @ëcâ óå±ÈAë±àbÌ¡í bᨋc² B@abÀBëc@x@ßA$A¨³àE¶FHa¨ëcjkbó´ =ájÙI`=å"±µà@9€@å,±¶àJ¥ JA#å±· B@aRàBå±@x@ÇA AR¸àEÄ½à ˆä FllaR¹ =áR~Å€=áR"½ºà@ØÀ€@ü yƒ1íð£ëU>ƒ2à`ÿa î[B½»àbD bá jëc¼ B@jÀBÿÿF„@x@ßA$Aj½àEJyJbàˆá jmmaj¾ =ájË|€=áj%-ä(%±¿à@' @à~ƹ$( @ãÅ«cÀàB†{€ƒà Bå±Á B@JÀBå±@x@¿AAJÂàEÑ4KáJnne±Ãà=RG„Få±Ä =áˆÅ:€{å"±Åà@ 6€@ü y5\%Nå±+câ óå±J©AëÆàbm5€bᨠbáë¥±Ç B@a¨ÀBÿÿ @x@ßA$A¨ÈàEXð€ˆÿÿ5nopbóÉ =áj‡°Laæâóê"´Êà@简@å,±ËàJS Jç ñÌ B@aRàBå±@x@ÇA ARÍàEegMaáÛä FqqaRÎ =áRo€=áR%±Ïà@mj€@ë c “ƒ1à/øæÿÃE´A à\?B½ÐàbÙi€bàáÄ©å±Ñ B@@]àBëc@x@ßA$AjÒàEì"NájrrajÓ =ájp&€=ä(%på±Ôà@Í%€@â rå±ÕàB, Bá Jå±Ö B@JÀBå±@x@¿AAJ×àEsÞ€ËýÄsse±Øà=ô¤Få±Ù =áˆoä€{áˆ% áˆ"óÚà@²ß€@ë câ óå±øÍBóÛàb bᨠ£å ±Ü B@a¨ÀBëc@x@ßA$A¨ÝàEù™ObóüytubóÞ =áj)ZP`=ä^%±ßà@ˆX€@å,±ààJôW J ëcá B@aRàBÿÿB@x@ÇA ARâàEQaâûvvaRã =áR¾€=áR"½äà@€@ö Ç£ð€åÒ‘Ò"ñØÙÒ‘ÿÿ@ÒÁÿbþåàb‡€bá j屿 B@jÀBÿÿ$X@x@ßA$AjçàEŽÌ€ˆå2ç wwajè =ájЀ=ä(å ±éà@wÏ€@à~ƹå±êàBÖ΀Bá Jå±ë B@JÀBå±@x@¿AAJìàEˆRbµàˆá Jxxe±íà=•š„FÿÿBî =áˆý€{âó% å±ïà@P‰€@ÿ ÿ,â óå±eE±ðàb°á Áë!¨ £å ±ñ B@a¨ÀBÿÿ,@x@ßA$A¨òàE›CSa¨ñyzbóó =ájÎT`=âóë côà@.€@å,±õàJš JA#å±ö B@aRàBå±@xô 9A AR÷ @`E©º€ˆãÇâ û{{aRø =áRT€=â½áR%±ùà@­½€@ë c ³‘«ïTùý<}0Ò‘ñ¹`Ì MãB½úàb bàá½å±û B@a$àBÿÿQè@x@ßA$AjüàE/vUbáóá j||ajý =áj¿y€=â½å ±þà@ @á å±ÿàB{x€ƒàÁÁJ$ ƒä–Ç``JÀBå±@x@¿AAJàE¶1VáJ}}e±à=7D„Få± =ሪ7€{áˆ% €ð`3bóà@î2€@ÿ ÿ,â óå±éBóàbR bᨠ£á¨–Ç B@a¨ÀBñ@x@ßA$A¨àE=í€ëñ~bó =ájt­Waæájä^+c à@Ô«€@ÿ ÿš2€ @ÿÿÞ àJ@ÀJ å± B@aRàBå±@x@ÇA AR àEJdXaëc€€aR = …€ƒ l€=áR%±à@bg€@å ±Ãäÿ‘«¿Cq€Sq÷ëc}ÿ]LàbÎf€bâ ½å± B@€æÀBÿÿô@x@ßA$AjàEÑYajàˆå ±aj =ája#€=áj%-ä(6Çà@¾"€@à~Ånå±àBàBå± B@JÀBå±@x@¿AAJàEXÛ€Ëã>á J‚‚e±à=ݤFyÿÿ, =áˆTá€{áˆ% áˆ"óà@˜Ü€@å ±$F @öÇâ óå±ÙAëàbøá Áë!¨ bå ± B@a¨ÀBñ@x@ßA$A¨àEÞ–Zbóëcƒ„bó =ájW[`=ê%±à@iU€@ë,càJÕT JA#ÿÿnb B@aRàBÿÿ¦@x@ÇA AR!àEì \aå±……aR" =áRŸ€=â½&Ãâ½#à@ð€@ÿÿZX @â½K DÓBñ«äÿtZ/øvÿïa 6ÿNô$àb\ bá jå±% B@jÀB‹ÿÿ @x@ßA$Aj&àEsÉ€ëÿÿ÷††aj' =ájçÌ€=â½å ±(à@D @á å±)àB«Ë€ƒå(ä ƒä‹c* B@JÀBƒ@ÙâøÿÿB@x@¿AAJ+àEù„]bµàËå ±‡‡e±,à=~—„Fÿÿ¦- =@®€ÁõŠ€{âó% âó.à@5†€@ÿ ÿWš!ˆ @âóå±’ïE±/àb•…€báᨠbᨋc0 B@`æÀBÿÿWš@x@ßA$A¨1àE€@^ᨈˆbó2 =áj£E€=âó$áj%±3à@D€@å,±4àJpC JA#â¸üy5 B@aRàBÿÿô@x@ÇA AR6àEüà ˆâ û‰‰aR7 =áR´_aáR(o8à@ÿ€~ÿ ÿ,ã¾ñÀ%§œäð£ðULäÿÿ@ÒpàBѹ€BàÁäàB¥±? B@JÀBëc@x@¿AAJ@àEs`bwàˆá J‹‹e±Aà=A‚ÿÿ¦B =áˆy€{å"±Cà@Nt€@ü yë câ óå±ÔÎ!ëD`ࣰá á¨å ±E B@a¨ÀBñ@x@ßA$A¨FàE›.aa¨ÿÿ ŒbóG =ájÁî€=âóå ±Hà@"í€@å,±IàJŽì J å±J B@aRàBå±@x@ÇA ARKàE¨¥baRàˆâ ûŽŽaRL =áRZ­€=áR%±Mà@¼¨€@ÿ ÿ¦óï´â€Â± ó‘Öäÿ÷Àe"½Nàb(Àb@%ájå±O B@jÀBÿÿ¦@x@ßA$AjPàE/acájajQ =áj›d€=ä(%-å±Rà@÷c€@å$±SàB[ Bá J/l ƒåô¥±T B@JàB@Ùáëc@x@¿AAJUàE¶daJîe±Và=7/ƒ|wñW =@®€Á¢"€{áˆáˆ"óXà@ô€@ü y': @ëcâ óå±¢—BóYàbV bᨠbᨋcZ B@`æÀBüy@x@ßA$A¨[àE<Ø€ëñ‘’bó\ =áj&™eaæájå ±]à@‡—€@â m' @öÇ^àJó– JA#â¸ëc_ B@aRàB{ÿÿ* @x@ÇA AR`àEJOfaâû““aRa =áRðV€=å"±bà@JR€@ÿÿ,Eƒ[ðìŒp ƒÂÝ`*ÅB½càb¶Q€bá jå±d B@jÀBñ@x@ßA$AjeàEÑ gajå2ç ””ajf =ájY€=â½%-å±gà@µ €@à~Éw$( @ãÅ«chàB Bá Jå±i B@JÀBëc@x@¿AAJjàEWÆ€Ëⵕ•e±kà=Ù¤Fÿÿ$Xl =áˆLÌ€{âó% áˆ%±mà@–Ç€@—ÿÿôâ ó屎7Bónàb÷¡á¨ £áëi…±o B@a¨ÀBÿÿô@x@ßl =A¨p @€EÞhbóå±–—bóq =ájBi`=âó%páj"´rà@e@€@å,±sàJÑ? J@Ýå±t B@a àBå±@x@ÇA ARuàEìø€ˆãÇä F˜˜aRv =áRzjaRâ½áR"½wà@Üû€~å ±tð£tÑð€Ñ3pÑ,ëcžCB½xàbH bàáÄ©å±y B@jÀBëc@x@ßA$AjzàEr´,áóá j™™aj{ =ájú·€=â½%pâ½,k|à@W @á å±}àB¶¶€ƒàÁÁJå±~ B@JÀBå±@x@¿AAJàEùokbwàˆá Jšš@=€à=~‚„Fxëc =áˆéu€{áˆ% áˆ"ó‚à@4q€@—å ±!ˆ @È…#(¥â óå±:±Aëƒàb•p€báᨠbå ±„ B@a¨ÀBå±@x@ßA$Aj…àE€+la¨ñ›œbó† =áj®ë€=ájä^%±‡à@ê€@ÿ,ÿ,ˆàJ{é JA#屉 B@aRàBå±@x@ÇA ARŠàE¢maRáÛâ ûaR‹ =áR/ª€=áR"½Œà@¥€@å ±#ƒ1‚xü|€}ñ²äa ¾ÇB½àbù¤€bàá½å±Ž B@jÀBÿÿ @x@ßA$AjàE^nájžžaj =áj a€=ájå ±‘à@ü`€@á $( @ãÅ"üAJ’àB\àB屓 B@JÀBå±@x@¿AAJ”àE›oaJýÄŸŸe±•à=,ƒ|ÿÿB– = ÀÁ€{áˆ% å±—à@Ú€@å ±/ª @å±+câ óå±5A¨˜àb; bä ^!¨ báëi‹c™ B@`æÀBå±@x@ßA$A¨šàE!Õ€ëëc ¡bó› =ájP•paæê"´œà@°“€@ë,càJÀJA#属 B@aRàBå±@x@ÇA ARŸàE/Lqaàˆå ±¢¢aR  =áRáS€=â½ç%±¡à@;O€@üyE3«ðƒ3ðÑL@¡×ï$ ÿàÅÿIB¢àb§N€bàáå±£ B@jÀBÿÿ @x@ßA$Aj¤àE¶rajãFá j££aj¥ =ájB €=â½%pä(+c¦à@ž €@á å±§àBþ €BàÁÁJ `ƒäŸÿ,¨ B@JÀB‹@ÙâøöÇ@x@¿AAJ©àE<ÀˆàËá J¤¤e±ªà=ɤFqëc« =@®€Á0É€{âó% áˆ"ó¬à@wÄ€@ÿ ÿ¦!ˆ @å±â óå±aÿO*­àbØá ᨠbᨅ±® B@`æÀB ô !¨ÿÿ5n@x@ßA$A¨¯àEÃ~sbóå±¥¦bó° =áj?t`=âóä^%±±à@b=€@å,±²àJÎ< J þ⸠`‹áR-èF³ B@aRàBå±@x@ÇA AR´àEÐõà ˆâ û§§aRµ =áR{ý€=áR"½¶à@Ùø€@ë cCV¿ £àÿÄTð$áõ‚ÿÿÏ4¤D©·àbE bä fëc¸ B@jÀB}â ½ÿÿ5n@x@ßA$Aj¹àEW±ubáóá j¨¨ajº =ájß´€=ä(å ±»à@< @á $( @âr"üAJ¼àBŸ³€ƒàÁäàB¥±½ B@JàB@Ùå±@x@¿AAJ¾àEÞlvaJöÇ©©e±¿à=_„FñÀ =@®€ÁÒr€{áˆ% å±Áà@n€@ÿ ÿ Òë câ óå±£VA¨Âàb~m€bᨠ£á륱à B@`æÀBëc@x@ßA$A¨ÄàEe(wa¨ñª«bóÅ =ájœè€=ä^"´Æà@ûæ€@â m!j%òüyÇàJgÀJ â¸å±È B@aRàBÿÿB@x@ÇA ARÉàErŸxaRâû¬¬aRÊ =áR-§€=áR%±Ëà@†¢€@ë cS4­‚€=áR"½õà@7:€@å ± s0Dð«à6Ï\„a Ãqöàb£9 b@$ájå±÷ B@jÀBÿÿyÆ@x@ßA$AjøàEµòà ˆá j¶¶ajù =áj9ö€=ájå ±úà@˜õ€@á å±ûàBùôàBå±ü B@JàB@Ùá2aÿÿL6@x@¿AAJýàE<®€bµàËá J··e±þà=D|ƒÇÿÿBÿ =@®€Á(´€{áˆ%på±aà@z¯€@å ±!ˆ @å±(¥â óå±gqàbØá å± B@`æÀBÿÿyÆ@x@ßA$A¨àEÃia¨ÿÿ$X¸¹bó =ájü)‚`=è¥å ±à@^(€@ÿ,ÿ,àJÊ' J îâ¸å± B@aRàByå±@x@ÇA ARàEÐà€ˆñººaR =áRmè€=â½áR%± à@Ðã€@å ±ƒÀ£àuð¤$ùtÑDÐa %ÂHo àb<Àb@$ä#å± B@jÀByöÇ@x@ßA$Aj àEWœƒbàˆä f»»aj =ájߟ€=ájå ±à@: @à~ÃÅ$( @ï).`AJàB›ž€ƒà Bå± B@JÀBëc@x@¿AAJàEÞW„aJâµ¼¼e±à=_j„F{üy =áˆÆ]€{áˆ%på±à@Y€@ÿ ÿ'Lë câ óå±0A¨àb~X€bᨠ£áë¶Ç B@a¨ÀBëc@x@ßA$A¨àEd…a¨ñ½¾bó =áj–Ó€=å"±à@÷Ñ€@è !j$ŸöÇàJcÀJA#äFå± B@aRàBëc@x@ÇA ARàErІaRÿ × ˆä F¿¿aR =áR3’€=áR%±à@Ž€@ö Ç “~ 1'L$a ØÿüÄ àbúŒ€bá jaµâ½áä! B@jÀBÿÿ¦@x@ßA$Aj"àEùE‡ajç¥á jÀÀaj# =áj…I€=ä(%pä(6Ç$à@àH€@á å±%àBA BàÁÅôå±& B@JÀBå±@x@¿AAJ'àEˆaJå±ÁÁe±(à=ƒÇå±) = 5 Ás€{áˆ% áˆ"ó*à@½€@ÿ ÿ,â óå±|ÿüÄ+àb bá ë!¨ £âó¥±, B@a¨ÀBüy@x@ßA$A¨-àE½€ëå±ÂÃbó. =áj4}‰aæä^(f/à@•{€@å,±0àJÀJA#¥(Âûå±1 B@aRàBå±@x@ÇA AR2àE4ŠaâûÄÄaR3 =áRÉ;€=áR"½4à@7€@è o$F'î!£õ‚ä4õƒà$ ýä3üïÑa ±GD©5àbˆ6€bä få±6 B@jÀBˆëc@x@ßA$Aj7àEšï€ˆáóå ±ÅÅaj8 =ájó€=ä(å ±9à@rò€@á !j @ãÅ"üAJ:àBÖñ€BàÁµ$àB¿ÿ,; B@JàB@Ùâøñ@x@¿AAJ<àE!«‹bµàˆá JÆÆe±=à=¦¤Fsëc> =@®€Á±€{áˆ% å±?à@[¬€@ÿÿ$Xâ óå±µA¨@àb½á ao!¨ £á륱A B@`æÀBüy@x@ßA$A¨BàE¨fŒa¨ÿÿxÇÈbóC =ájæ&`=ä^"´Dà@F%€@â mëcEàJ²$ JA#aHáRå±F B@aRàBÿÿÞ@x@ÇA ARGàEµÝ€ˆå±ÉÉaRH =áRgå€=áR%±Ià@¹à€@å ±³UÑ:ÿÑUàü£àýï¾å±@{B½Jàb% bá jå±K B@jÀBÿÿ]L@x@ßA$AjLàE<™ŽbájÊÊajM =ájÄœ€=ä(%-ä(+cNà@  @å$±OàB€›€ƒá J$ ƒäŒ®P B@JÀBëc@x@¿AAJQàEÃTaJàËå ±ËËe±Rà=Hg„FëcS = ÀÁ»Z€{áˆ% áˆ"óTà@ýU€@ÿ ÿ5n': @è¥â óå±ä‰AëUàb_ báå±V B@`æÀBå±@x@ßA$A¨WàEIa¨âóÌÌbóX =ájö€=áj)Ïáj%±Yà@Y€@å,±ZàJÅ JA#å±[ B@aRàBöÇ@x@ÇA AR\àEÐËà ˆâ ûÍÍaR] =áRwÓ€=áR"½^à@È΀@ÿ ÿ œ"½!“â½Ã:ÿ%§Å°œÑ:a ¿B½_àb4 bá jå±` B@jÀBÿÿ5n@x@ßA$AjaàEW‡‘b½àˆá jÎÎajb =ájÏŠ€=áj%-â½%±cà@- @ã Å!j @ån"ü@BdàB“‰€ƒàÁäàB¥±e B@JÀBÿÿt@x@¿AAJfàEÝB’aJäÏÏe±gà=é¤Fëÿÿnbh =áˆÚH€{è¥%páˆ"óià@D€@å ±)ø @âó+câ óå±¹MA¨jàb}C€bᨠbáë«ck B@a¨ÀBå±@x@ßA$A¨làEdþ€ˆÿÿL6ÐÑbóm =ájˆ¾“aæáj$áj"´nà@ë¼€@å,±oàJWÀJA#å±p B@aRàBxüy@x@ÇA ARqàEru”aÿÿðÒÒaRr =áR%}€=äFáR"½sà@†x€@ü yÓð¡9ä6ÄðÑLP<ï$0a „%B½tàbòw b@$ájå±u B@jÀBÿÿ­@x@ßA$AjvàEø0•ajàˆå ±ÓÓajw =áj€4€=áj%-â½%±xà@Þ3€@à~Ånå±yàB@3àBëcz B@JàB áñ@x@¿AAJ{àE쀈å±ÔÔe±|à=ÿ„Fÿÿ¡¤} = 5 Áoò€{áˆ% áˆ"ó~à@¿í€@îW @å±Kâ óå± ×Aëàb bá ë!¨ bå ±€ B@a¨ÀBÿÿ,@x@ßA$A¨àE¨–âóÕÕbó‚ =áj¡¬€=å"±ƒà@«€@â m'$Ÿüy„àJnª JA#â¸ñ… B@aRàBöÇ@x@ÇA AR†àEŒc—áRÖÖaR‡ =áR2k€=áR"½ˆà@f€@ü yãÿŸÿ/µtð£tAÿÿBÿ|„‰àbùe€bê [届 B@jÀBüy@x@ßA$Aj‹àE˜ajäf××ajŒ =áj—"€=ä(å ±à@ó!€@á 屎àBW Bâ µ$ ƒä‘ B@JàB@Ùâøå±@x@¿AAJàEšÚ€Ëàˆæ üØØe±‘à=¢¤FÿÿÞ’ =@®€Á’à€{áˆ% 屓à@ÔÛ€@ö Çâ óå±)ÿ‚l”àb6 báÇ!¨ £á¨Ÿÿ¦• B@`æÀBëc@x@ßA$A¨–àE!–™bóÿÿÞÙÚbó— =ájCVš`=ä^+c˜à@¤T€@ÿ,ÿÞ™àJ Já R屚 B@aRàBöÇ@x@ÇA AR›àE. ›aàˆâ ûÛÛaRœ =áRØ€=áR%±à@2€@ÿ ÿ¦ó€ßÿ/µÿ ŽÀ@Œàyÿ¡¤žàbž€bàá½å±Ÿ B@jÀBÿÿ5n@x@ßA$Aj àEµÈ€ˆå±ÜÜaj¡ =@Ý€ƒAÌ€=ä(%-ä(+c¢à@Ë€@å$±£àBýÊ€Bá J$ ƒä…±¤ B@€ÆÀBöÇ@x@¿AAJ¥àE<„œbµàËâ µÝÝe±¦à=Á¤Föǧ =áˆ$Š€{áˆ% áˆ"ó¨à@v…€@ÿ ÿ â óå±Ýÿ¡¤©àbØá Âó!¨ £á¨…±ª B@a¨ÀB”ÿ m'Zÿÿ$X@x@ßA$A¨«àEÂ?a¨üyÞßbó¬ =ájìÿ€=áj+"áj%±­à@Mþ€@è ëc®àJ¹ý J þèj屯 B@aRàBå±@x@ÇA AR°àEжžaRäFààaR± =áRN¿€=å"±²à@¨º€@ëcF£ñÈŸÿ ‰~RàÅÿt³àb bá jå±´ B@jÀBå±@x@ßA$AjµàEVrŸájááaj¶ =ájëu€=áj%-â½%±·à@G @äåÃÅëc¸àB§t€ƒà B$ ƒä…±¹ B@JÀBÿÿ* @x@¿AAJºàEÝ- áJââe±»à=^@ƒ|€ÿÿ,¼ =áˆÑ3€{äF% áˆ%±½à@/€@›ãû$F @ñÃâ óå±aÿt¾àb}.€bâ ó!¨ bᨅ±¿ B@a¨ÀBÿÿÞ@x@ßA$A¨ÀàEd逈öÇãäbóÁ =áj¢©¡aæájå ±Âà@¨€@å,±ÃàJo§ J îå±Ä B@aRàBå±@x@ÇA ARÅàEq`¢aå±ååaRÆ =áR h€=äFáR"½Çà@yc€@å ±6à àð€ÀàÿñA àS®D©Èàbåb€bá j`£â½¿ÿ É B@@]àBÿÿ$X@x@ßA$AjÊàEø£ájææajË = …€ƒŒ€=â½%-ä(%±Ìà@é€@å nå±ÍàBH Bá J$ ƒáJ¥±Î B@€ÆÀBëc@x@¿AAJÏàE×€Ëâµççe±Ðà=ê„F|å±Ñ =áˆoÝ€{áˆ% áˆ"óÒà@ºØ€@å ±!ˆ @å±(¥âóå±÷\AëÓàb bᨠbᨅ±Ô B@a¨ÀBöÇ@x@ßA$A¨ÕàE“¤bóñèébóÖ =áj,S¥`=ájä^+c×à@Q€@å,±ØàJüP JA#å±Ù B@aRàBüy@x@ÇA ARÚàE ¦aâûêêaRÛ =áR½€=áR"½Üà@ €@üyF#ïŸÿ›éð"1%¹ÿÿ]Lú”B½Ýàb‹  bA;ájå±Þ B@jÀBÿÿ$X@x@ßA$AjßàEšÅ€ˆájëëajà =ájÉ€=áj%-ä(%±áà@vÈ€@á $( @ãÅ"ü@BâàBÖÇàBå±ã B@JÀBå±@x@¿AAJäàE §âµììe±åà=¦¤F屿 =ÀÁ‡€{áˆ% áˆ"óçà@`‚€@ñ ë câ óå±^fA¨èàbÀ¡á ë!¨ £á륱é B@a¨ÀB™ñ@x@ßA$A¨êàE§<¨a¨ëcíîbóë = …`ƒÖü€=ê"´ìà@6û€@å,±íàJ¢ú J äFëcî B@€ÎÀBëc@x@ÇA ARïàEµ³©aR‹@Y¤Ïô ïïaRð =áRW»€=â½ç"½ñà@µ¶€@öÇF3ƒ2E¨ìð£íðKn@¸Àj B½òàb! bá jå±ó B@jÀBÿÿ@Ò@x@ßA$AjôàE;oªajå±ððajõ =ájÇr€=áj%pä(%±öà@$ @á å±÷àB‡q€ƒà B$ ƒä‘ø B@JàB@Ù¢oÑ›5 @'@x@¿AAJùàEÂ*«áJññe±úà=G=ƒ|ÿÿ* û =@®@™¶0€{áˆ% áˆ"óüà@,€@ÿÿ Ò @ëcâ óå±ÎøAëýàbb+€bᨠbᨋcþ B@`æÀBñ@x@ßA$A¨ÿàEI怈âóòòbóbAb ƒïê€=å"±à@Qé€@â m'$Ÿÿÿ,àJ½è J â¸å± B@aRàBÿÿ `x@9 AR @`EС¬bûå±óóaR =áR„©€=áR"½à@Ô¤€@â½!Râ½ C"5ð¨ü}+c3àÿÔ`Ì æ/B½àb@ bð å± B@a$àB‹è oÿÿ; @x@ßA$Aj àEV]­ajâ½ôôaj = …€ƒÖ`€=H â1e-ä(%± à@1 @á å± àB’_€ƒâ µå± B@€ÆÀBëc@x@¿AAJàEÝ®áJõõe±à=é¤Fóÿÿ  =áˆÕ€{áˆ% áˆ"óà@€@ÿÿ,â óå±BÝAëàb}€bᨠ£å ± B@a¨ÀB â ó"ôüy@x@ßA$A¨àEdÔ€ˆâóööbó =ájÙ€=áj$ájKà@t×€@å,±àJàÖ J þãå± B@aRàBëc@x@ÇA ARàEê¯âû÷÷aR = …€ƒ¡—€=å"±à@û’€@ö ÇS"ÿ*õ‚ÿ®ñþ›B½àbg bä ©å± B@€æÀBüy@x@ßA$AjàEqK°ájøøaj =ájýN€=â½*ßâ½%± à@Z @á å±!àB¹M€ƒâ µå±" B@JÀBå±@x@¿AAJ#àEø±áJùùe±$à=Õƒ|ÿÿë®% =áˆè €{áˆ% áˆ%±&à@3€@—ü y!ˆ @è¥â óå±ZiAë'àb˜€bᨠbå ±( B@a¨ÀBöÇ@x@ßA$A¨)àE€ˆÿÿ,úûbó* =áj­‚²aæájå ±+à@€@å,±,àJ}€ JA#îå±- B@aRàBöÇ@x@ÇA AR.àEŒ9³açüüaR/ =áR2A€=áR"½0à@<€@ñ cåw`àa ŒñB½1àbü;€bá jå±2 B@jÀBñ@x@ßA$Aj3àEõ€ˆÿÿ+uýýaj4 =áj£ø€=ájå ±5à@ÿ÷€@á $( @ãÅ(®AJ6àB_àBå±7 B@JÀBå±@x@¿AAJ8àEš°´bµ@Y£>ó Ëþþe±9à=ÄFÿÿ,: =áˆ޶€{áˆ% å±;à@ݱ€@å ±/ª @å±+câ óå±(A¨<àb> bç !¨ báë±= B@a¨ÀBå±@x@ßA$A¨>àE lµa¨å±ÿ zA? =ájG,¶`=ê"´@à@§*€@å,±AàJÀJ ¢rÓÎå±B B@aRàBëc@x@ÇA ARCàE.ã Ó  ˆâ û qARD =  ƒðê€=â½ç%±Eà@Næ€@å ±s$p#Ñœ`SuþÌÿÿÞ0B½Fàbºå€bàáÁjå±G B@jÀBå±@x@ßA$AjHàE´ž·bàˆá j žAjI =áj-¢€=â½%pä(+cJà@‰¡€@à~Áå±KàBì àB$ ƒä–ÇL B@JàB@Ù¢oÀ…/löÇ@x@¿AAJMàE;Z¸áJ`=Nà=ĤFÿÿQèO =@®€Á+`€{âó% áˆ"óPà@v[€@ô @å±9»â óå±”îAëQàb×á Áe!¨ bᨖÇR B@`æÀBöÇ@x@ßA$A¨SàE¹a¨ÿÿ* 1|%±T =ájáÕ€=âóä^%±Uà@AÔ€@â m' @öÇVàJ­Ó JA#â¸å±W B@aRàBå±@x@ÇA ARXàEÏŒºaRàˆä FARY =áRz”€=áR"½Zà@Û€@ÿ ÿ,ƒ+"HK/}/Na  ÿàJ[àbGÀb@$ájå±\ B@jÀBÿÿ,@x@ßA$Aj]àEVH»áj(p"½^ =ájÚK€=ä(%-ä(%±_à@; @á å±`àBšJ€ƒà B$ ƒä…±a B@JàB@Ùáå±@x@¿AAJbàEݼáJ`=cà=^ƒ|ÿÿ,d =@®€ÁÉ €{áˆ% áˆ"óeà@€@ÿ ÿ¦$F @å±+câ óå±­&Aëfàby€bᨠbᨅ±g B@`æÀBëc@x@ßA$A¨hàEc¿à ˆä ^ Bói =ájÄ€=ä^%±jà@p€@‡@Sâm!jå±kàJÜÁ JA#àJå±l B@aRàBüy@x@ÇA ARmàEêz½âû ARn =@þ€ƒ£‚€=áR"½oà@~€@å ±“b± S ‘Ö"Ñ£EïöÇËÿ=Þpàbn} bA;ájå±q B@€æÀBÿÿF„@x@ßA$AjràEq6¾áj Ajs =ájñ9€=ä(%-ä(Ctà@L @â rå±uàB±8€ƒá Jå±v B@JàB@Ùáå±@x@¿AAJwàEøñà ˆä  `=xà=Àƒ|ïüyy = 5 Áä÷€{áˆ% áˆ"ózà@5ó€@ÿ ÿÞ!ˆ @å±+câ óå±oÂAë{àb”ò€bᨠbå ±| B@a¨ÀBå±@x@ßA$A¨}àE~­¿bóñ Bó~ =áj§mÀ`=áj%páj%±à@ l€@â må±€àJuk J â¸å± B@aRàBÿÿ$X@x@ÇA AR‚àEŒ$Áaàˆâ û!)øƒ =áRA,€=å"±„à@œ'€@ñ £åq åå$ÔPå" ãa Xÿ2z…àb bàáÎ!屆 B@jÀBÿÿ$X@x@ßA$Aj‡àEà€ëå2á jAjˆ =áj“ã€=â½å ±‰à@òâ€@à~Á$( @ãÅ"üDŠàBSàB屋 B@JÀBã Cå±@x@¿AAJŒàE™›Ââµ`=à=®„Fÿÿ¦Ž =ሡ€{áˆ% #)`@3e±à@ל€@ÿ ÿB$F @å±+câ óå±üÿ8bàb5 bâ ó!¨ báë«c‘ B@a¨ÀB 0®ä¡!¨ÿÿ,@x@ßA$A¨’àE WÃá¨Bó“ =A6`ƒÄ[€=ä^"´”à@$Z€@â m!j%òñ•àJY J þáå±– B@€ÎÀBñ@x@ÇA AR—àE§ÄáRAR˜ =áRI€=â½!â½™à@«€@ë c³ å# ã å0ä åS`-Mà#ÉDfšàbÀb@'çå±› B@jÀBñ@x@ßA$AjœàE-Πë@Y ˆå ±@u' =ájºÑ€=â½%páj+cžà@ @à~屟àBzЀƒå ô$ ƒä‘  B@JàB@Ù¡À…$öÇ@x@¿AAJ¡àE´‰Åâµ`=¢à=¼¤Fòëc£ =@®€Á €{âó% áˆ+c¤à@@ñ ': @å±+câ ó屇;Aë¥àbP bá ë!¨ bᨑ¦ B@`æÀBëc@x@ßA$A¨§àE;EÆa¨öÇBó¨ = …`ƒrÇ`=âóä^%±©à@Ò€@å,±ªàJ>ÀJ â¸å±« B@€ÎÀBå±@x@ÇA AR¬àEH¼ Ó@Y£Aä FAR­ =áRÿÀ=â½áR"½®à@X¿€@ÿ ÿÞà àÔ}@~¬$@¸À°ØB½¯àbľ€bàáÄ#å±° B@jÀBƒè oÿÿÊí@x@ßA$Aj±àEÏwÈbàˆá j'ø%±² =ájK{€=áj%-ä(C³à@¬z€@à~Á$( @ãÅ"ü@B´àB àB$àB¥±µ B@JàB@Ù¢oÀ…!KßÿB@x@¿AAJ¶àEV3ÉáJ`=·à=×E„F}ÿÿ¦¸ =@®€ÁJ9€{áˆ% áˆ"ó¹à@‘4€@œÿ ÿÞ)ø @1! @# +câ ó ›”hv¼ï/GÌï?*A¨ºàbòá Áe!¨ bá륱» B@`æÀBëc@x@ßA$A¨¼àEÝî€ëå±Bó½ =áj¯Êaæå"±¾à@w­€@â m!j$Ÿ#j`4ˆÃ! !% … AR¿àJ㬠J â¸å±À B@aRàBÿÿbþ@x@ÇA ARÁàEêeËaàˆä FAR =áR¥m€=áR"½Ãà@öh€@ÿ7!Râ½(oÓäð£ßû~¬µï0àa ÿ¡¤Äàbb bàá½å±Å B@jÀBŠå±@x@ßA$AjÆàEq!ÌájAjÇ =ájý$€=ä(%-ä(CÈà@Z @ë `@ãÅ¥±ÉàB¹#€ƒá J$ ƒàB¥±Ê B@JÀBÿÿÞ@x@¿AAJËàE÷Ü€ˆÿÿ ”`=Ìà=yï„FüyÍ =áˆôâ€{âó% áˆ"óÎà@7Þ€@ÿÿh°â ó屸ÿ¡¤Ïàb—Ý€bᨠ£á¨‹cÐ B@a¨ÀBÿÿ5n@x@ßA$A¨ÑàE~˜ÍbóöÇ !BóÒ =áj¢XÎ`=áj$áj"´Óà@W€@å,±ÔàJmV JA#å±Õ B@aRàBå±@x@ÇA ARÖàEŒÏaãÇå ±";â)ø× =áR7€=áR"½Øà@Œ€@î!!“å±ãCu"åw$þ`p#ñ#EDà[óD©Ùàbø€bá j`b⽿ÿ* Ú B@jÀB†@lá­ëc@x@ßA$AjÛàEËà ˆá j#I[!jÜ =ájªÎ€=å"±Ýà@ @á鯹å±ÞàBfÍ€ƒá Jå±ß B@JÀBå±@x@¿AAJààE™†Ðbµàˆá J$$`=áà=F™„FQëcâ =ሕŒ€{áˆ% äF"óãà@Õ‡€@£ëcâ óå±"Â'äàb5 báÁë!¨ £âó¥±å B@a¨ÀB¨â ó"ô ¿ÿ O@x@ßA$A¨æàE BÑa¨ëc%&Bóç =áj?Ò`=áj$áj%±èà@£€@ó ƒëcéàJÀJ þäëcê B@aRàBÿÿB@x@ÇA ARëàE-¹€ÓáÛâ û''ARì =áRÙÀ€=å"±íà@5¼€@ü yów"Ñœ` ñÐ`äõvõw&Hà®ÃB½îàb¡»€bàá½å±ï B@jÀBâ ½å±@x@ßA$AjðàE´tÓbàˆá j((Ajñ =áj8x€=â½*ßâ½#Åòà@–w€@â rå±óàBôv€BàÁÁJ$ ƒä–Çô B@JÀBå±@x@¿AAJõàE;0ÔáJ))`=öà=¼¤Fñ÷ =áˆ+6€{áˆ% áˆ%±øà@w1€@—å ±!ˆ @è¥â óå± 1Aëùàb×á ᨠbᨋcú B@a¨ÀBœâ ó"ôå±@x@ßA$A¨ûàEÁë€ëëc*+Bóü =ájí«Õaæájå ±ýà@Pª€@ë,cþàJ¼© J þå ±ÿ B@aRàBÿÿÞ@x@ÇA ARBB@`EÏbÖaàˆä F,,AR"€áR{j€=å"±à@×e€@å±GåYp ñ#àµ`B`Ì Š+B½àbC bàá½å± B@a$àBå±@x`ß =Aj @`EV×ajå2á j--Aj =ájÒ!€=â½%-ä(%±à@0 @à~Á$( @ãÅ(®@BàB’ €ƒá Jå± B@aàB@Ùì®$öÇ@x@¿AAJ àEÜÙà ˆá J..`= à=]ì„Fzëc =@®€ÁÍ߀{âó% ሠ< à@Û€@å ±)ø @å±Ãâ óå±.ÙA¨àbxÚ€bᨠbá륱 B@`æÀBå±@x@ßA$A¨àEc•Øbóàˆá ¨//Bó =á)"š€=ájáj"´à@ƒ˜€@â m!j @öÇàJï— JA#â¸ëc B@aRàBÿÿÞ@x@ÇA ARàEêPÙáR00AR =áR›X€=äF!â½à@öS€@üyG"ïÓ”Pï`"Q¬à=sB½àbb bá jå± B@jÀBÿÿ$X@x@ßA$AjàEp Úáj1#±1 =ájù€=â½%páj%±à@T @á å±àBµ€ƒå ôå± B@JÀB‹@Ùâøå±@x@¿AAJàE÷Ç€ˆàËä 22`= à=ÿ¤Fõÿÿ,! =@®€ÁçÍ€{âó% ሠ<" à@1É€@ü y!ˆ @!ÿ`\yâ óå±™Aë#àb“È€báÇ!¨ bå ±$ B@`æÀBå±@x@ßA$A¨%àE~ƒÛbóëc34Bó& =á)¬CÜ`=âóä^%±'à@ B€@å,±(àJyA JA# @áRå±) B@aRàBå±@x@ÇA AR*àE‹úà ˆâ û55AR+ =áRAÝá$R <, à@›ý€~ë c#K]Å“ÿ ð]à¯wB½-àb bá jå±. B@jÀBå±@x@ßA$Aj/àE¶,àˆá j66Aj0 =á)–¹€=ä(å ±1à@ò¸€@å n$( @ãÅ"üAJ2àBR BàÁäàB±3 B@JÀBñ@x@¿AAJ4àE™qÞbwàˆá J77`=5à=„„Fÿÿ 6 =ሉw€{áˆ% å±7à@Ór€@ÿ m)ø @å±+câ óå± …A¨8àb5 báᨠbáë«c9 B@a¨ÀBå±@x@ßA$A¨:àE -ßa¨öÇ89Bó; =ájNí€=å"±<à@®ë€@å,±=àJÀJA#å±> B@aRàBÿÿÞ@x@ÇA AR?àE-¤àaRàˆâ û::AR@ =áR׫€=áR <A à@9§€@ÿÿ G3uY"Ñ£KVu6‚¬àà„ÿ»*Bàb¥¦ b@(ájå±C B@jÀByó ÓöÇ@x@ßA$AjDàE´_ááj;; FE =á)€@å,±gàJZ Jã >å±h B@aRàBå±@x@ÇA ARiàEp÷ Ó@Ygƒâ ûDDARj =áR+ÿ€=å"±kà@€ú€@èo!“èoSåw´ 1àÿõ`ñw °àùpAùlàbìù€bàáÄfå±m B@jÀBÿÿÞ@x@ßj =Ajn @`E÷²èbàˆä fEEAjo = ?`=‡¶€=â½%-ä(%±pà@äµ€@â rñqàBC BàÁÁJ)º ƒä…±r B@€ÆÀBå±@x@¿AAJsàE~néáJFF`=tà=ÿ¤Få±u =áˆvt€{áˆ% ሠ<v à@¹o€@Ÿå ±!ˆ @âóå±]‚Aëwàb bᨠbᨅ±x B@a¨ÀBÿÿ @x@ßA$A¨yàE*êá¨GDB%±z =á)®.€=ä^3Ê{à@-€@ÿ,ÿÞ|àJ}, JA#¥(Âûå±} B@aRàBx àBÿÿ‰ @x@ÇA AR~àE‹å€ˆáÛä FHB¡!R =áR;í€=â½%pâ½€à@“è€@å ±c"Ñœ`Suþ~¬`ïà¶n!àbÿç€bàáÁj层 B@jÀB„î !ñ@x@ßA$AjƒàE¡ëâ½IIAj„ =áj¢¤€=áj%-áj%±…à@ü£€@á $( @ãÅ"ü@B†àB^àB屇 B@JÀBå±@x@¿AAJˆàE™\ìaJã>â µJJ`=‰à= ¤Fÿÿh°Š = ÀÁb€{áˆ% ሠ<‹ à@Ñ]€@ÿÿF„â óå±t Œàb5 báÂó!¨ £á륱 B@`æÀB â ó"ôñ@x@ßA$A¨ŽàEía¨ñKLBó =á)PØ€=å"±à@²Ö€@ë,c‘àJÀJ þånå±’ B@aRàBÿÿÞ@x@ÇA AR“àE-îaRàˆâ ûMMAR” =áRé–€=áR <• à@A’€@ñ  s` Ìï`Ÿÿ ŽåYa Èho–àb­‘€bàá½å±— B@jÀBÿÿ¦@x@ßA$Aj˜àE´JïajãFá jNNAj™ =á)àb¨g€bàá½å±? B@jÀB‰å±@x@ßA$Aj@àE³ ájttAjA =áj+$€=áj%pä(%±Bà@‡#€@å$±CàBç"€Bá Jå±D B@JÀBëc@x@¿AAJEàE:Üà ˆâ µuu`=Fà=»¤Få±G =áˆ.â€{áˆ%páˆ"óHà@pÝ€@ÿ ÿ@Ò': @å±Kâ óå±}èAëIàbÖá Âó!¨ bç«cJ B@a¨ÀBëc@x@ßA$A¨KàEÁ— bóå±vwBóL =ájêW `=è¥å ±Mà@KV€@å,±NàJ·U J å±O B@aRàBå±@x@ÇA ARPàEÎaàˆâ ûxxARQ =áR{€=â½áR"½Rà@΀@å±HÔPB_ÿƒ(àþï+úàÿFB½Sàb: bàá½å±T B@jÀBå±@x@ßA$AjUàEUÊ€ëàˆá jyyAjV =ájÕÍ€=ájå ±Wà@0 @à~Á$( @ãÅ«cXàB‘Ì€ƒà B$àB«cY B@JÀBå±@x@¿AAJZàEÛ…âµzz`=[à=]˜„Få±\ =áˆÌ‹€{äF% å±]à@‡€@ÿÿ,â óå±[´Bó^àbw†€bᨠ£á륱_ B@a¨ÀBöÇ@x@ßA$A¨`àEbAa¨ñ{|Bóa =ájx`=ájä^"´bà@Ùÿ€~å,±càJEÀJ å±d B@aRàBå±@x@ÇA AReàEp¸éyä F}}ARf =áRÀ€=äFáR%±gà@x»€@üyHp-ÑàTd` üyÃðB½hàb亀bá jå±i B@jÀBÿÿB@x@ßA$AjjàEösbÿÿ—~~Ajk =áj‚w€=áj%-ä(+clà@Þv€@á å±màBBàBå±n B@JàB@Ùâø2aüy@x@¿AAJoàE}/áJ`=pà=þ¤Fÿÿ$Xq =@®€Áq5€{áˆ% áˆ"órà@»0€@ü y!ˆ @ëcâ óå±gAësàb bá ë!¨ bå ±t B@`æÀBå±@x@ßA$A¨uàEë€ëëc€Bóv =áj*«aæå"±wà@‹©€@ÿ,ÿ xàJ÷¨ JA#â¸ñy B@aRàBå±@x@ÇA ARzàEbaáÛå ±‚‚AR{ =áRÇi€=áR"½|à@!e€@üyH#¾ïÒð~Ða ¤±B½}àbd€bàá>ä 4å±~ B@jÀBå±@x@ßA$AjàE˜ájƒ=MA€ =Àƒ!€=ä(å ±à@t €@â r$( @ãÅ(®AJ‚àBÔ€Bá J àB«cƒ B@JàB@Ùaƒáå±@x@¿AAJ„àEÙà ˆâ µ„„`=…à= ¤Fëc† =ÀÁ߀{áˆ%  +@@3bó‡à@YÚ€@ë câ óå±!¦'ˆàb»á Âó!¨ £áë«c‰ B@a¨ÀBå±@x@ßA$A¨ŠàE¥”bóëc…†Bó‹ =A6`ƒÌT`=ä^"´Œà@,S€@â m82*Qÿÿ àJ˜R JA#â¸å±Ž B@€Î˜ B{ÿÿ,@x@ÇA ARàE³ aˆ@Y ˆâ û‡Cé$F =áRQ€=áR%±‘à@«€@ñ 37E¨VÂý/9ÿ„xÀ§JB½’àb bàá½å±“ B@jÀBå±@x@ßA$Aj”àE:Ç€ëå2á jˆ@ª!j• =ájÂÊ€=ä(%pä(#Å–à@ @à~Áå±—àB~É€ƒà B$ ƒäèF˜ B@JÀBñ@x@¿AAJ™àEÀ‚ⵉ‰`=šà=A•„Fëc› = ÀÁ´ˆ€{áˆ% áˆ"óœà@÷ƒ€@ÿÿ â ó屸Aëàb\ bâ ó!¨ £á¨Ÿÿ,ž B@`æÀB ù »'Zÿÿ,@x@ßA$A¨ŸàEG>a¨å±Š‹Bó  =ájmþ€=áj%páj%±¡à@Îü€@å,±¢àJ:ÀJ ¥(Ã>å±£ B@aRàBå±@x@ÇA AR¤àETµaRàˆä FŒOÌ$F¥ =áR½€=å"±¦à@a¸€@ñ Cà@% @à~Áå±?àB‡€ƒà Bå±@ B@JÀBã Cå±@x@¿AAJAàEÍÏ€ˆáÓâ µ±±`=Bà=Nâ„Fÿÿ/¼C =áˆÉÕ€{áˆ% áˆ%±Dà@ Ñ€@ÿÿ$Xâ óå±"·AëEàbiЀbáÁë!¨ £å ±F B@a¨ÀBëc@x@ßA$A¨GàET‹8bóàˆá ¨²²BóH =ájã€=ä^+cIà@DŽ€@â m=ä$^ÿÿBJàJ° JA#èjå±K B@aRàBÿÿô@x@ÇA ARLàEÛF9áR³³ARM =áR”N€=â½!â½Nà@ïI€@ë cÃ1Kf tÛE¯4a nB½Oàb[ bá jëcP B@jÀBÿÿB@x@ßA$AjQàEa:áj´´AjR =ájÚ€=â½å ±Sà@5 @á å±TàBš€ƒå$±U B@JàB@Ùâøå±@x@¿AAJVàEè½€ˆàËä µµ`=Wà=ð‹ƒ|ñw „àƒ€  Œ¿ÿ’çAX = ) ÁØÃ€{âó% âóYà@"¿€@ù »$F @è¥Ãâ óå±Êÿ|ºZàb„¾€báå±[ B@a¨ÀBÿÿ @x@ßA$A¨\àEoy;bóñ¶·Aj] =áj¡9<`=âóä^%±^à@8€@å,±_àJn7 JA#‚*áRå±` B@aRàBå±@x@ÇA ARaàE|ðà ˆâ û¸¸ARb =áR.ø€=â½áR(ocà@ˆó€@å ±ÓE¯kf»à‚åa œÿ dàbôò€bá jå±e B@jÀBÿÿÞ@x@ßA$AjfàE¬=bàˆá j¹¹Ajg =áj‡¯€=ájå ±hà@㮀@à~ÃÅå±iàBCàB)º ƒéºÿôj B@JÀBöÇ@x@¿AAJkàEŠg>áJºº`=là= z„Fÿÿ¦m =áˆzm€{áˆ%på±nà@Äh€@å ±!ˆ @å±+câ óå±8ÿ oàb& bá ë!¨ bᨑp B@a¨ÀBå±@x@ßA$A¨qàE#?a¨å±»¼Bór =áj;ã€=å"±sà@›á€@å,±tàJÀJA#å±u B@aRàBÿÿÞ@x@ÇA ARvàEš@aRàˆä F½½ARw =áRÀ¡€=áR%±xà@€@å ±ãð1$£tð%«àßð~a cSKcyàb†œ€bàáÄ©å±z B@jÀBëc@x@ßA$Aj{àE¥UAáj¾¾Aj| = …€ƒ-Y€=ä(%-ä(1}à@‰X€@ë $( @ãÅ(®@B~àBéW€Bá J$àB¥± B@€ÆÀBå±@x@¿AAJ€àE+BaJÿÿ+U¿¿`=à=­¤F层 =ሠ€{âó% áˆ"óƒà@j€@å ±ë câ óå±Ç¡A¨„àbˡᨠ£á륱… B@a¨ÀBå±@x@ßA$A¨†àE²Ì€ëöÇÀÁBó‡ = …`ƒÕŒCaæájê"´ˆà@5‹€@å,±‰àJ¡Š J å ±Š B@€ÎÀBå±@x@ÇA AR‹àEÀCDaãÇå ±ÂÂARŒ =áR^K€=áR"½à@¼F€@ÿ ÿ¦óÐ7/¯/9/ëcwªB½Žàb( bá jå± B@jÀBÿÿ¦@x@ßA$AjàEFÿ€ëàˆá jÃÃAj‘ =ájÊEa¨å"±’à@' @á鯹屓àB†€ƒàÁÀBå±” B@JÀBå±@x@¿AAJ•àEÍºà ˆá JÄÄ`=–à=VÍBaJtüy— =ሽÀ€{áˆ% `ï€3bó˜à@¼€@—ÿ ÿÞ!ˆ @ëcâ óå±ÓXBó™àbi»€bᨠbå ±š B@a¨ÀBå±@x@ßA$A¨›àETvFajàˆá ¨ÅÅBóœ =áj{€=ájå ±à@hy€@å,±žàJÔx JA#ëcŸ B@aRàByÿÿ* @x@ÇA AR àEÚ1GáRÆÆAR¡ =áR†9€=áR%±¢à@ç4€@å± Ióð£åóð"t·/õVÆÿÿÞ ÿB£àbS bä ©å±¤ B@jÀB{üy@x@ßA$Aj¥àEaí€ëáóâ ½ÇÇAj¦ =ájíð€=áj*ßä(+c§à@H @á $( @ãÅ(®@B¨àB©ï€ƒàÁ屩 B@JÀBå±@x@¿AAJªàEè¨HâµÈÈ`=«à=ð¤FóöǬ =áˆØ®€{áˆáˆ"ó­à@%ª€@ÿÿ,â óå±ÿB®àb„©€bᨠ£áë«c¯ B@a¨ÀBå±@x@ßA$A¨°àEodIa¨ñÉÊBó± =ájÓ$J`=ê"´²à@6#€@ù 5,Î%òüy³àJ¡"€Já R `JáR-”´ B@aRàBå±@x@ÇA ARµàE|Ûà ˆä FËË9…¶ =áR,ã€=â½!äF"½·à@€Þ€@”è o @èo Iƒàþ"t³/…® a y…¸àbìÝ€bá jå±¹ B@jàBå±@x@ßA$AjºàE—Kbàˆá jÌÌAj» = …€ƒ‹š€=ájáj%±¼à@噀@à~Ånå±½àBGàBëc¾ B@€ÆÀBå±@x@¿AAJ¿àEŠRLaJæ}á JÍÍ`=Àà= e„Fÿÿ,Á =ሂX€{áˆ% âóÂà@ÆS€@ÿÿôâ ó屡mÃàb& báÁë!¨ £ä^¥±Ä B@a¨ÀBÿÿô@x@ßA$A¨ÅàEMa¨ñÎÏBóÆ =áj9΀=å"±Çà@›Ì€@ë,cÈàJÀJA#ëcÉ B@aRàBå±@x@ÇA ARÊàE…NaRàˆâ ûÐÐARË =áRÂŒ€=â½â ½Ìà@ˆ€@öÇI#"¬6ÁÐð£tð"uèa æ¿D©Íàb†‡€bàá½å±Î B@jÀBÿÿWš@x@ßA$AjÏàE¤@OájÑÑAjÐ =áj-D€=â½å ±Ñà@‡C€@á å±ÒàBéB€Bä ƒäˆgÓ B@JÀBÿÿ$X@x@¿AAJÔàE+ü€ˆàËâ µÒ0•@=Õà=¬¤FöÇÖ =áˆPaÆâó% å±×à@hý€¼ÿ ÿ Ò)ø @è¥+câ óå±BMBóØàbÇá Âóå±Ù B@a¨ÀB˜è ¥ÿÿ* @x@ßA$AjÚàE²·,á¨ÓÓBóÛ =ájZ¼€=âóä^+cÜà@ºº€@å,±ÝàJ&ÀJ þå ±Þ B@aRàBöÇ@x@ÇA ARßàE9sQbã„â ûÔÔARà =áRÓz€=áR+cáà@5v€@ÿÿWšI3Òè…Œó…Šó…Íó…Ìó àÎÙB½âàb¡u b@(çå±ã B@jÀByâ ½ÿÿ* @x@ßA$AjäàE¿.Rajàˆá jÕÕAjå =ájH2€=áj%-ä(+cæà@¤1€@áéÃÅå±çàB BàÁÀB$ ƒå ±è B@JÀBëc@x@¿AAJéàEFê€Ëàˆá JÖÖ`=êà=N¸ƒÇõñë =áˆ2ð€{å"±ìà@ë€@ÿ ÿWš!ˆå±â óå±j+Aëíàbâá ᨠbá¨XFî B@a¨ÀBå±@x@ßA$A¨ïàEÍ¥SbóñרBóð =ájóeT`=ájå ±ñà@Td€@å,±òàJÀc€Jã >ñó B@aRàBå±@x@ÇA ARôàEÚUaâûÙÙARõ = …€ƒ‘$€=äFáR%±öà@ê€@ë cCèý"ì0çÃtûtø1{ ŒàŒÿê÷àbV bàá½å±ø B@€æÀBÿÿÞ@x@ßA$AjùàEaØ€ëàˆä fÚÚAjú =ájåÛ€=â½%-å±ûà@B @â r$( @ãÅ(®DüàB¡Ú€ƒàÁÁJå±ý B@JÀBŒ@Ùç?&ýÿÿô@x@¿AAJþàEè“VâµÛÛ`=ÿà=i¦„F|3àƒ€ßÿ S§ 4 fáˆÜ™€{áˆ% áˆ"óà@#•€@›@a€táEë câ óå±(ÿ Òàb„”€bᨠ£á륱 B@a¨ÀBÿÿô`x@9 A¨ @ nOWá¨ÜÜAj =B`= T€=ä^"´à@ƒR€@ö,ÇàJïQ J ×áµå± B@€ÎÀBx"vàB ÿk¬ @x@ÇA AR àEõ XáRÝÝAR = p€ƒ­€=â½å ± à@ €@ü ySÓ1‡@vøv"Ãtÿû ŒàŸÿª àbu €bâ ½å± B@€æÀB€ëc@x@ß =Aj @`E|Æà ˆå ±ÞÞAj =ájøÉ€=áj%pä(+cà@R @á å±àB´È€ƒà Bå± B@aàBå±@x@¿AAJàE‚Ybµ‹@Y¤ôá Jßß`=à= Pƒ|ÿÿŽ =áˆ÷‡€{áˆ% áˆ%±à@@ƒ€@å ±!ˆ @ñ+câ ó屟ÿ¯üàb£¡ä ^!¨ bå ± B@a¨ÀBüy@x@ßA$A¨àE‰=Za¨öÇàáBó = …`ƒªý€=å"±à@ ü€@ë,càJxû J ÂrÈ­å± B@€ÎÀBöÇ@x@ÇA ARàE—´[aRàˆâ ûââAR =áRC¼€=â½å ± à@Ÿ·€@å ±ct1{Ã1‡Pvvÿ"à·ÕD©!àb  bàáÁjå±" B@jÀBå±@x@ßA$Aj#àEp\ajãFá jããAj$ =áj®s€=â½å ±%à@ @á $( @ãÅ(®AJ&àBjr€ƒàÁ¿ÿS3$àB¶Ç' B@JÀB‹@Ù¥kÀBëc@x@¿AAJ(àE¤+]aJàËá Jää`=)à=%>ƒ|öÇ* =@®€Á1€{âó% å±+à@á,€@ö Çâ óå±'A¨,àb@ báÁe!¨ £áë«c- B@`æÀBñ@x@ßA$A¨.àE+ç€ëå±åæBó/ = …`ƒX§^aæâóä^"´0à@º¥€@å,±1àJ&ÀJ â¸å±2 B@€ÎÀBå±@x@ÇA AR3àE8^_aàˆâ ûççAR4 =áRåe€=áR(o5à@Aa€@å ±sí&öì6ö"œú¨æõƒå±¢B½6àb­`€bä få±7 B@jÀBÿÿyÆ@x@ßA$Aj8àE¿`ájè=M/Œ9 =ájG€=ä(%-ä(+c:à@¢€@á å±;àB Bâ,ä ƒäœy< B@JàB@Ùå±@x@¿AAJ=àEFÕ€Ëàˆâ µéé`=>à=ǤFÿÿyÆ? =@®€Á6Û€{áˆ% áˆ"ó@à@ƒÖ€@å ±!ˆ @ëcâ óå±4PAëAàbâá ᨠbᨖÇB B@`æÀBëc@x@ßA$A¨CàEÍabóâtá ¨êê ÃD =ájl•€=ájå ±Eà@Í“€@â m2€ @ÿÿÞFàJ9ÀJ â¸å±G B@aRàBöÇ@x@ÇA ARHàESLbaRàˆá RëëARI =áRúS€=å"±Jà@[O€@ü yƒæõ‚"ë•‚åƒd€øêd€˜a ãÿyÆKàbÇN b@$ájå±L B@jÀBÿÿàJ@x@ßA$AjMàEÚcájììAjN =ájf €=â½%-ä(%±Oà@à €@ã<ÃÅ$( @ãÅ(®@BPàB" Bá J)ºàB¥±Q B@JÀBñ@x@¿AAJRàEaÀËàˆâ µíí`=Sà=i‘ƒÇüyT =áˆMÉ€{áˆ% áˆ%±Uà@œÄ€@ü y$F @å±Ãâ óå±GÿyÆVàbýá Áë!¨ bá륱W B@a¨ÀBëc@x@ßA$A¨XàEç~dbóëcîïBóY =áj?e`=áj%páj"´Zà@r=€@å,±[àJÞ< JA#äFå±\ B@aRàBÿÿ; @x@ÇA AR]àEõõà ˆâ ûððAR^ =áR°ý€=å"±_à@ù€@” Ùá$F!“â½3Ó“¨"Q/?n?¹?^àÿÿÉ‚žÿ÷`àbqø€bá jå±a B@jÀBÿÿ; @x@ßA$AjbàE|±fbàˆá jññAjc =ájµ€=â½%-â½%±dà@a´€@å$±eàBij€BàÁä ƒä‹cf B@JàB@Ùáðñ@x@¿AAJgàEmgáJòò`=hà=„„FÿÿQèi =@®€Áûr€{âó% áˆ%±jà@>n€@ÿÿQèâ óå±:ÿ÷kàbžá ᨠ£á¨‹cl B@`æÀB¤ á ¨ÿÿQè@x@ßA$A¨màE‰(ha¨å±óôBón =áj°è€=âó$áj%±oà@ç€@â m' @ëcpàJ|æ J!áå±q B@aRàBå±@x@ÇA ARràE—ŸiaRãÇä FõõARs =áRA§€=áR"½tà@Ÿ¢€@ñ £ï´õ±àTþ°a :ŸD©uàb  bá jå±v B@jÀB~â ½ÿÿÞ@x@ßA$AjwàE[jajàˆá jööAjx =áj^€=áj%-â½%±yà@þ]€@à~ÃÅ' @ãÅ"ü@BzàB]àB)ºàB¥±{ B@JàB@Ùä ÿÿ,@x@¿AAJ|àE¤káJ÷÷`=}à=%)ƒ|ÿÿ@Ò~ =@®€Á”€{áˆ% áˆ"óà@à€@—è ¥$F @è¥+câ ó属mA¨€àb@ bá ë!¨ bá륱 B@`æÀBñ@x@ßA$A¨‚àE+Ò€ëÿÿÞøùBóƒ = …`ƒZ’laæç%páj"´„à@º€@ÿ,ÿ,…àJ&ÀJ â¸å±† B@€ÎÀBñ@x@ÇA AR‡àE8ImaáÛä FúúARˆ =áRÏP€=â½áR"½‰à@(L€@ë c³à|$ÿì>­ü0ãD+úàýÿÉ‚Šàb”K€bàáĩ屋 B@jÀBÿÿ@Ò@x@ßA$AjŒàE¿nájûûAj =ájG€=áj%-â½%±Žà@¤€@á å±àBàB$ ƒä‹c B@JÀBñ@x@¿AAJ‘àEFÀ€Ëÿÿ,üü`=’à=ǤF屓 =áˆ>Æ€{áˆ% áˆ"ó”à@Á€@ÿ ÿôâ óå±Y…Aë•àbæ¡á ë!¨ £á¨‹c– B@a¨ÀB è ¥(¦ëc@x@ßA$A¨—àEÌ{obó¼ 5§ ä^ ýþBó˜ =ájø;p`=å"±™à@W:€@è '$ŸëcšàJÃ9 J ®ÀÉÄFå±› B@aRàBëc@x@ÇA ARœàEÚò€ˆÿÿ-ÿÿAR =áRú€=áR"½žà@âõ€@å ±Ãü1FkbðÄTý|a ¹B½ŸàbN bá jå±  B@jÀBñ@x@ßA$Aj¡àEa®qbç¥â½AX Aj¢ =@€ƒå±€=ä(%-ä(%±£à@B @á 層àB¡°€ƒàÁµ$ ƒä…±¥ B@€Æ¡ Bå±@x@¿AAJ¦àEçiraJàËá J-@=§à=l|„Fyÿÿ ¨ =áˆßo€{áˆ% áˆ"ó©à@#k€@å ±$F @ëcâ óå±§KAëªàbƒj€báᨠbᨅ±« B@a¨ÀBñ@x@ßA$Aj¬àEn%sá¨Bó­ =áj *€=áj%páj+c®à@n(€@å,±¯àJÚ' JA#¦{ÁRå±° B@aRàBÿÿ; @x@ÇA AR±àEõàà ˆâ ûaR² =áR–è€=å"±³à@ñã€@ÿ ÿB Ó£àuð¤-ýì5ðKea Åÿ` ´àb] bá jå±µ B@jÀBÿÿÞ@x@ßA$Aj¶àE{œtâ½aj· =áj €=áj%-â½%±¸à@oŸ€@å nå±¹àBОàB屺 B@JÀBÿÿÞ@x@¿AAJ»àEXuáJe±¼à= &‚tÿÿ ½ =áˆî]€{äF% áˆ%±¾à@8Y€@ÿ ÿ â óå±aÿeò¿àbžá Ä^!¨ £å ±À B@a¨ÀBüy@x@ßA$A¨ÁàE‰va¨ñbó =áj®Ó€=ájå ±Ãà@Ò€@å,±ÄàJ|Ñ JA#å±Å B@aRàBå±@x@ÇA ARÆàE–ŠwaRàˆå ±aRÇ =áRP’€=äFáR"½Èà@¢€@ÿÿê @èo9…ãDðü?³àý1ú£àa ›µD©Éàb bàá½å±Ê B@jÀBÿÿŠÜ@x@ßA$AjËàEFxáj  ajÌ =áj¥I€=â½å ±Íà@ @å$±ÎàBaH€ƒá J$ ƒä‹cÏ B@JÀBëc@x@¿AAJÐàE¤yáJ  @=Ñà=%ƒ|ÿÿ Ò =ሠ€{áˆ% å±Óà@Þ€@§öÇâ óå±?GBóÔàb@ bᨠ£á¨‹cÕ B@a¨ÀBÿÿL6@x@ßA$AjÖàE+½€ëå±  Kc× = …`ƒ`}zaæáj$áj+cØà@Á{€@å,±ÙàJ-ÀJ å ±Ú B@€ÎÀBöÇ@x@ÇA ARÛàE84{aàˆå ±  ARÜ = …€ƒý;€=áR%±Ýà@P7€@ñ ó%§ C!"í0ç¨æþà Œà¥>B½Þàb¼6€bàáå±ß B@€æÀBå±@x@ßA$AjààE¿ïà ˆá jajá =áj7ó€=áj%-â½+câà@’ò€@á ëcãàBóñàBå±ä B@JÀBå±@x@¿AAJåàEE«|âµe±æà=ǤFwñç =áˆ>±€{è¥% áˆ"óèà@€¬€@ÿ ÿ,óâóå±ÌAëéàbáá Âó!¨ £å ±ê B@a¨ÀB¤öÇ@x@ßA$A¨ëàEÌf}a¨ÿÿ,D^ì = …`ƒâ&~`=ájå ±íà@C%€@å,±îàJ¯$ J å ±ï B@€ÎÀBëc@x@ÇA ARðàEÚÝ€ˆéyä FaRñ =áRså€=äFáR"½òà@Îà€@ñJtüÓQ$@v"E²a QÚB½óàb: bá jëcô B@jÀBÿÿF„@x@ßA$AjõàE`™bàˆá jajö =ájðœ€=ájå ±÷à@L @à~ÃÅå±øàB¬›€ƒà B$ ƒä‹cù B@JÀBå±@x@¿AAJúàEçT€áJe±ûà=hg„Fÿÿ$Xü =áˆ×Z€{áˆ% å±ýà@!V€@ÿÿF„â ó ›”hv¼ï/GÌï?õ(BóþàbƒU€bᨠ£á¨‹cÿ B@a¨ÀBÿÿ,@x@ßA$A¨g  €Ená¨E± =áj€=å"±à@v€@å,±àJâ J@Üñ B@a àBÿÿ$X@x`Ç =AR @`EôËà ˆä FaR =áR¬Ó€=áR%±à@Ï€@ëcJÃtüÃQ$Pv"í&ö`Ì Éb½àbm΀bæ“Ä©å± B@a$àBüy@x@ßA$Aj àE{‡‚â½B½ =ájÿŠ€=ä(%-ä(+c à@] @á å± àB»‰€ƒâ µå± B@JÀBå±@x@¿AAJàECƒáJe±à= ‚îÿÿ/¼ =áˆþH€{áˆ% áˆ"óà@>D€@¤ñ !ˆ @ô â óå±mh¥àbžá Âó!¨ bå ± B@a¨ÀBñ@x@ßA$A¨àE‰þ€ëâ1ä ^bó =áj„aæájêKà@€@å,±àJí JA#å± B@aRàBz àBÿÿ* @x@ÇA ARàEºà ˆá RDF =áR­Á€=å"±à@½€@‡â ½!“â½#"îd€øìd€˜¨"®à ýàZvÇàb{¼€bá jå± B@jÀByî !áj@x@ßA$AjàE–u…bàˆá jaj =ájy€=â½å ±!à@vx€@å$±"àBÖw€BàÁä ƒä‹c# B@JÀBå±@x@¿AAJ$àE1†áJ@=%à=%ÿ„Füy& =ሠ7€{áˆ% å±'à@W2€@—â óÿ ÿ,Jâ óå±þvÇ(àb¹á ᨠ£á¨‹c) B@a¨ÀBüy@x@ßA$Aj*àE¤ì€ëâtâ óbó+ =ájDñ€=ä^%±,à@¤ï€@å,±-àJÀJA#ånÿÿ,. B@aRàB{å±@x@ÇA AR/àE*¨‡âûaR0 =áRÕ¯€=â½%pâ½1à@.«€@ë c3ÿt!%~õ‚ä4‚õƒïðÿÿbþYEJ[2àbšª€bá jå±3 B@jÀBÿÿ @x@ßA$Aj4àE±cˆajÿÿ Iaj5 =áj1g€=áj%-áj+c6à@Žf€@á )Ú @ãÅ"ü@B7àBíeàB)ºàB¥±8 B@JÀBå±@x@¿AAJ9àE8‰?@TàËä   e±:à=@íƒÇúëc; = n€Á8%€{áˆ% áˆ+c<à@{ €@ë câ óå±ýÿh°=àbÜá å±> B@`æÀBöÇ@x@ßA$A¨?àE¾Ú€ëöÇ!"bó@ =ájöšŠaæå"±Aà@U™€@å,±BàJÁ˜ J å±C B@aRàB|å±@x@ÇA ARDàEÌQ‹aàˆâ û##KcE =áRwY€=â½å ±Fà@ÈT€@ÿÿ,JC¯àÿt'%~"Q`¼îð£à~¼DfGàb4 bàá½å±H B@jÀBÿÿnb@x@ßA$AjIàES Œaj‡@Y¡óá j$$AjJ =ájã€=áj%-ä(%±Kà@@ @á å±LàBŸ€ƒà Bå±M B@JÀBå±@x@¿AAJNàEÙÈà ˆá J%%@=Oà=ZÛ„F~üyP = ÀÁÒ΀{äFå ±Qà@Ê€@ÿÿ â óå±ÚNAëRàbuÉ€bᨠ£ç«cS B@`æÀBå±@x@ßA$AjTàE`„bóå±&'bóU =áj„DŽ`=ájä^(fVà@çB€@å,±WàJSÀJA#£½ÄFëcX B@aRàBöÇ@x@ÇA ARYàEnû€Ó‡@Y ˆâ û((aRZ =áRá$R(o[à@zþ€~ÿÿ¦JSïðE«š%«ïð1¯@¸ÀÈ:B½\àbæý€bàáÁjå±] B@jÀBÿÿ¦@x@ßA$Aj^àEô¶,áóá j))aj_ =ájxº€=å"±`à@Ö¹€@á å±aàB4 BàÁÁJå±b B@JÀBå±@x@¿AAJcàE{rbwàˆá J**e±dà=ü¤Fÿÿôe =áˆkx€{áˆ% ç:"ófà@·s€@è¥ @ô â óå±lÈBógàb báᨠbå ±h B@a¨ÀBüy@x@ßA$A¨iàE.‘a¨ÿÿÞ+,bój =áj6î€=å"±kà@™ì€@‡@S¤ÝÁ2€%òÿÿôlàJÀJA#àJå±m B@aRàBå±@x@ÇA ARnàE¥’aRáÛâ û--aRo =@þ€ƒϬ€=áR"½pà@'¨€@ÿ ÿ,cà$ ÿV¿4þ%°®öÇ,B½qàb“§€bàáÁµå±r B@€æÀBñ@x@ßA$AjsàE–`“áj..ajt =ájd€=ä(ë cuà@pc€@â rå±vàBÒb€Bá Jå±w B@JàB@Ùâø8ŸÿCˆ@x@¿AAJxàE”aJàËâ µ//e±yà=ž¤Fÿÿôz =@®€Á"€{áˆ% å±{à@Y€@ÿ ÿB$F @å±+câ ó屈ÿôT|àb¹á Âó!¨ bå ±} B@`æÀB ù »!¨Á¨@x@ßA$A¨~àE£×€ëÿÿ¦01bó =áj×—•aæáj%páj+c€à@:–€@â m!j @å±àJ¦• J â¸å±‚ B@aRàBÿÿ; @x@ÇA ARƒàE±N–aàˆâ û22aR„ =áReV€=å"±…à@ÁQ€@ë csƒðC!@e­ƒ ðàN§E±†àb- bàá½å±‡ B@jÀBÿÿ; @x@ßA$AjˆàE8 —ajäf33Q‰ =áj¼ €=ájå ±Šà@ @à~Á `@ãÅ"üAJ‹àBx €ƒà B$ ƒàB¶ÇŒ B@JÀBÿÿô@x@¿AAJàE¾Å€ˆáÓâ µ44e±Žà=?Ø„Fÿÿ]L =ሮˀ{äF% äF(¥à@ûÆ€@å ±!ˆ @å±.Wâ óå±êUA¨‘àbZ báÁë!¨ b᨜y’ B@a¨ÀBëc@x@ßA$A¨“àEE˜bóàˆá ¨55bó” =ájØ…€=âó%páj"´•à@9„€@å,±–àJ¥ƒ JA#å±— B@aRàBÿÿ,@x@Ç” =AR˜ @`EÌ<™áR66aR™ =áR}D€=â½áR"½šà@Ø?€@å ±ƒC"%­ ¯à´Kåq0à `Ì ~£B½›àbD bá j屜 B@a$àBÿÿ @x@ßA$AjàERø€ëàˆâ ½77ajž =ájÛû€=â½%-â½6ÇŸà@6 @à~ÃÅ$( @ãÅ"ü@B àB–ú€ƒà B$àB¥±¡ B@JÀBñ@x@¿AAJ¢àEÙ³šâµ88e±£à=á¤Fõüy¤ =áˆÁ¹€{âó% áˆ"ó¥à@µ€@ÿÿ; â óå±ÚQA¨¦àbu´€bᨠ£á륱§ B@a¨ÀB”ëc@x@ßA$A¨¨àE`o›a¨ëc9:H¥© =áj’/œ`=âóä^"´ªà@ó-€@å,±«àJ_ÀJA#屬 B@aRàBå±@x@ÇA AR­àEmæ Ó@Y ˆä F;;aR® =áR#î€=â½áR"½¯à@}é€@å ±“q4àQŸCq S"ï"õ:S"@¸Àé×B½°àbéè€bàáĩ山 B@jÀBÿÿWš@x@ßA$Aj²àEô¡bàˆá j<?bó¾ =áj8Ù€=å"±¿à@˜×€@å,±ÀàJÀJ ¥(Ã>ñÁ B@aRàBÿÿÞ@x@ÇA ARÂàE aRàˆä F@@aRà =áRÁ—€=áR"½Äà@#“€@ÿÿF„J£ŸC"Cq"qVu6u7ña %B½Åàb’ b@$áj屯 B@jÀBÿÿ @x@ßA$AjÇàE–K¡ájAAHoÈ =ájO€=ä(å ±Éà@nN€@ð Òå±ÊàBÎM€Bá J$ ƒäŸÿ Ë B@JÀBå±@x@¿AAJÌàE¢áJBBe±Íà=ž¤Fÿÿ$XÎ =ሠ€{âó% å±Ïà@S€@ü y`@å±+câ óå±ïBóÐàb¸á Ä^!¨ £á¨XFÑ B@a¨ÀBÿÿ @xÏ 9A$A¨Ò @€E£Â€ëå±CDbóÓ =ájÖ‚£aæájê+cÔà@6€@å,±ÕàJ¢€ J@Ý @áRå±Ö B@a àBå±@xÔ 9A AR× @`E±9¤aãÇå ±EEaRØ =áR[A€=áR%±Ùà@¹<€@ë c³8ï$þ``$p%ÿÿåüÅÞB½Úàb% bàá½aöâ½á„fÛ B@a$àBÿÿx@x@ßA$AjÜàE7õ€ëáóá jFFajÝ =áj«ø€=å"±Þà@ @á å±ßàBg÷€ƒàÁÁJ$å ±à B@JÀBå±@x@¿AAJáàE¾°¥bµàˆá JGGe±âà=?ÄFëcã =ሮ¶€{áˆ% å±äà@ù±€@å ±!ˆ @å±(¥â óå±a,BóåàbZ báᨠbâ󥱿 B@a¨ÀBöÇ@x@ßA$A¨çàEEl¦á¨HHbóè =ájîp€=ájå ±éà@Qo€@å,±êàJ½n JA#å±ë B@aRàBüy@x@ÇA ARìàEË'§áRIIaRí =áRˆ/€=å"±îà@à*€@î!!“â½ÃLàõ9£€ƒNàü£àýìa B½ïàbL bä ©å±ð B@jÀBÿÿ,@x@ßA$AjñàERã€ëáóä fJJajò = …€ƒÒæ€=â½%-ä(1óà@- @á $( @ãÅ(®@BôàBŽå€ƒàÁäàB«cõ B@€ÆÀBå±@x@¿AAJöàEÙž¨âµKKe±÷à=á¤FóöÇø =áˆŤ€{áˆ% áˆ(¥ùà@ €@ü yë câ óå±ÛîA¨úàbuŸ€bᨠ£á륱û B@a¨ÀBëc@x@ßA$A¨üàE`Z©a¨öÇLMNWý =ájª`=áj$áj"´þà@ò€@å,±ÿàJ^ÀJA#å±h  aRàBöÇ@x@ÇA ARàEmÑ Ó@X ˆä FNNaR =áR%Ù€=áR"½à@…Ô€@ˆ  ~Ä$F!“å±Óõ9:€ äõ9ƒKàõ:Q 8àAÒB½àbñÓ b@&àbå± B@jÀB|ù …ÿÿ¦@x@ßA$AjàEôŒ«bàˆá jOOaj =áj€€=áj%-â½%±à@Ú€@à~áj @ãÅ"ü@B àB<àBëc B@JÀBå±@x@¿AAJ àE{H¬aJæ}á JPPe± à=ü¤Fÿÿ, = ÀÁgN€{áˆ% áˆ"óà@¸I€@ü y"ó @âóå±å A¨àb báÁë!¨ bá륱 B@`æÀBå±@x@ßA$A¨àE­a¨ñQRbó =áj"Ä€=ê"´à@„€@å,±àJðÁ JA#äFñ B@aRàBëc@x@ÇA ARàE{®aRàˆâ ûSSaR =áR€=â½%pâ½à@~€@ñ  ã¡€-L\y<…õa ;{B½àb‹}€bàá½å± B@jÀBÿÿ@Ò@x@ßA$AjàE•6¯ájTTaj =áj:€=â½%páj%±à@h9€@á $( @ãÅ"ü@BàBÎ8€BäàB«c B@JàB@Ùâø&ýÿÿ @x@¿AAJ àEò€ˆàËÿÿ ÿ¯¾UUe±!à=¤Fÿÿ5n" =@®€Áø€{âó% áˆ%±#à@Yó€@ÿ ÿ/¼â ó屟‰A¨$àb¸á Âóå±% B@`æÀB è ¥!¨ÿÿ @x@ßA$A¨&àE£­°âóVVbó' =áj7²€=âóä^"´(à@—°€@å,±)àJÀJ þâ¸å±* B@aRàBöÇ@x@ÇA AR+àE*i±aRã„â ûWWaR, =áRÐp€=áR"½-à@.l€@å ±ó:£àõ; < = a ªaB½.àbšk€bá jå±/ B@jÀBöÇ@x@ßA$Aj0àE°$²ajàˆá jXXaj1 =áj4(€=ä(%-ä(%±2à@‘'€@áéÃÅå±3àBð&€BàÁÀB$ ƒåô¥±4 B@JÀBëc@x@¿AAJ5àE7àà ˆá JYYe±6à=?®ƒÇÿÿÞ7 =áˆ+æ€{áˆå ±8à@rá€@å ±!ˆ @ëcâ ó屓Aë9àbÓá ᨠb᨜y: B@a¨ÀBå±@x@ßA$A¨;àE¾›³bóñZ[bó< =ájì[´`=ájå ±=à@MZ€@è 2€ @€ Ã! !% …l¶>àJ¹Y J èjå±? B@aRàBå±@x@ÇA AR@àE˵aã„â û\\aRA =áR~€=å"±Bà@Û€@å±K>%¥? @ A àŒãB½CàbG bàá½å±D B@jÀBÿÿ¦@x@ßA$AjEàER΀ëàˆá j]]ajF =ájÚÑ€=â½%-å±Gà@7 @â r$( @ãÅ(®AJHàBšÐ€ƒàÁÁJå±I B@JàB@Ùâø)ºëc@x@¿AAJJàEÙ‰¶âµ^^@=Kà=Zœ„Fx3#ŸàƒÿÿxL =@®€ÁÍ€{áˆ% áˆ(¥Mà@‹€@öÇâ óå±(A¨NàbuŠ€bᨠ£á륱O B@`æÀBå±@x@ßA$AjPàE_E·a¨ñ_`bóQ =ájޏ`=áj%páj"´Rà@î€@â må±SàJZÀJ â4` å±T B@aRàBÿÿ* @x@ÇA ARUàEm¼ Ó@Y`~ä FaaaRV =áRÄ€=å"±Wà@u¿€@å ±B%¥C D E  °àB2B½Xàbá¾€bàáÁjå±Y B@jÀBå±@x@ßA$AjZàEôw¹bå2å ±bbY…[ =áj|{€=â½%pâ½+c\à@Ùz€@à~Áå±]àB8 Bá Jå±^ B@JÀBëc@x@¿[ =AJ_ @€Ez3ºaJàˆá Jcce±`à=ÿ¤Fÿÿ¦a =áˆo9€{âó% áˆ%±bà@¶4€@å ±!ˆ @ëcâ ó屿ÀAëcàb báÁë!¨ bå ±d B@abÀBå±@x@ßA$A¨eàEï€ëå±debóf =áj,¯»aæâóå ±gà@Œ­€@å,±hàJø¬ J ¥(ÁRå±i B@aRàBå±@x@ÇA ARjàEf¼aàˆâ ûffaRk =áR±m€=áR"½là@i€@å ±#F%¥GC"`Q¤CÿGa ÛøB½màb{h€bä ©å±n B@jÀBå±@x@ßA$AjoàE•!½ájggajp =áj%€=ájå ±qà@z$€@áéÂr$( @ãÅ(®AJràBÙ#àB$àB±s B@JÀBå±@x@¿AAJtàEÝà ˆâ µhhe±uà=¤Fÿÿ* v =ሠã€{áˆ% å±wà@XÞ€@å ±)ø @å±+câ óå± A¨xàb¸á Áë!¨ báë«cy B@a¨ÀBœöÇ@x@ßA$A¨zàE£˜¾âóiibó{ =áj9€=çå ±|à@››€@å,±}àJÀJA#å±~ B@aRàBüy@x@ÇA ARàE)T¿áRjjaR€ =áRÎ[€=áR%±à@)W€@å ±3"C"­àõ4å~õ5àèB½‚àb•V€bá j屃 B@jÀB€â ½ÿÿ,@x@ßA$Aj„àE°Àájkkaj… =áj<€=ä(%-ä(+c†à@—€@á 屇àBøàB$ ƒäŸÿÛˆ B@JÀBå±@x@¿AAJ‰àE7Ëà ˆå ±lle±Šà=?™ƒÇóÿÿÞ‹ =áˆ'Ñ€{áˆ% áˆ"óŒà@tÌ€@ÿÿ â óå±»AëàbÓá 屎 B@a¨ÀBå±@x@ßA$A¨àE¾†ÁbóöÇmnbó =ájëFÂ`=ä^(f‘à@LE€@å,±’àJ¸D JA#屓 B@aRàBÿÿÞ@x@ÇA AR”àEËýà ˆâ ûooaR• =áRxÃá$R"½–à@Ï€@ÿÿ/¼KC¯àõ6+g7 8 a OÿB½—àb; bá j屘 B@jÀB„å±@x@ßA$Aj™àER¹€ëàˆá jppajš =ájâ¼€=ä(%-ä(%±›à@= @ì k屜àBž»€ƒàÁä ƒå ± B@JÀBå±@x@¿AAJžàEØtÄbwàˆá Jqq@=Ÿà=Z‡„FÿÿÞ  =áˆÍz€{áˆå ±¡à@v€@ü yë câ óå±ë Aë¢àbtu€báᨠ£á¨œy£ B@a¨ÀB¡â ó(¦ÿÿ,@x@ßA$Aj¤àE_0Åá¨rrbó¥ =áj5€=ájë c¦à@g3€@å,±§àJÓ2 J 屨 B@aRàBñ@x@ÇA AR©àEæëà ˆâ ûssaRª =áR‘ó€=ç%±«à@òî€@ÿÿ KS9£Keu4ƒGàõ5ÿÿ$XŸÿ|„¬àb^ÀbA;ÿÿ­å±­ B@jÀBzâ ½ëc@x@ßA$Aj®àEm§Æb½å2á jttaj¯ =ájñª€=â½%-å±°à@N @à~ÃÅå±±àB­©€ƒá Jëc² B@JÀBå±@x@¿AAJ³àEóbÇaJàˆá Juue±´à=û¤Föëcµ =áˆàh€{âó% áˆ"ó¶à@/d€@ÿÿ â óå±;ÿ‚l·àbc€báÁë!¨ £å ±¸ B@a¨ÀB˜â ó"ôå±@x@ßA$A¨¹àEzÈa¨öÇv!‹Aº = ``ƒ±Þ€=âóå ±»à@Ý€@å,±¼àJ}Ü J äFöǽ B@€ÎÀBå±@x@ÇA AR¾àEˆ•ÉaRãÇâ ûxxaR¿ =áR:€=áR"½Àà@˜˜€@ñ c1®à`p-@å}´ààüHoÁàb bàá½å±Â B@jÀB~â ½å±@x@ßA$AjÃàEQÊajáóá jyyajÄ =áj’T€=ájå ±Åà@ïS€@à~Á屯àBNàB$ ƒäèFÇ B@JÀBå±@x@¿AAJÈàE• ËáJzze±Éà=ƒ|öÇÊ =áˆ}€{áˆ% å±Ëà@Ñ €@å ±ë câ óå±DBóÌàb1 bá ë!¨ £á¨‹cÍ B@a¨ÀB”â ó"ôå±@x@ßA$A¨ÎàEÈ€ëñ{|bóÏ =ájGˆÌaæG rå%å ±Ðà@§†€@ÿ,ÿÞÑàJÀJ Šà‹å±Ò B@aRàB{ ê;ÿÿV/ @x@ÇA ARÓàE)?ÍaáÛä F}}aRÔ =áRäF€=â½ç%±Õà@AB€@å ±sÿä€å}‚ ðuvu ýà/ÿ$XÖàb­A€bàáÄ©å±× B@jÀBâ ½å±@x@ßA$AjØàE°ú€ˆÿÿQè~~!Ù =áj8þ€=áj%-ä(1Úà@•ý€@á å±ÛàBôüàB$ ƒä…±Ü B@JÀBå±@x@¿AAJÝàE7¶Îbµÿÿxe±Þà=¸¤FÿÿÞß =áˆ'¼€{áˆ% áˆ"óàà@r·€@˜ÿ ÿè @üyKâ ó屋ÿ$Xáàbסä ^!¨ bᨅ±â B@a¨ÀBëc@x@ßA$A¨ãàE½qÏa¨ÿÿ* €bóä =ájí1Ð`=å"±åà@L0€@ë,cæàJ¸/ J ånå±ç B@aRàBëc@x@ÇA ARèàEËèà ˆå ±‚‚aRé =áRvð€=â½å ±êà@Óë€@å ±ƒw"Kf$ý``$F¯àûÿNôëàb? bá jå±ì B@jÀBå±@x@ßA$AjíàER¤ÑbãFá jƒƒajî =ájâ§€=â½%-ä(%±ïà@? @á å±ðàBž¦€ƒàÁÆü$ ƒä…±ñ B@JÀBÿÿ@Ò@x@¿AAJòàEØ_ÒaJàËá J„„e±óà=Yr„F~å±ô =áˆÈe€{âó% áˆ%±õà@a€@å ±ë cƒâóå±_ÿTÜöàbt`€báᨠ£á¨…±÷ B@a¨ÀBüy@xõ 9A$A¨ø @€E_Óa¨å±…†bóù =ájÛ€=âóä^Kúà@âÙ€@å,±ûàJNÀJ@Ýå±ü B@a àBÿÿL6@x@ÇA ARýàEl’ÔaRàˆâ û‡,Y)øþ =áRš€=áR"½ÿà@u•€@ö Ç“pKYw"`öÇ“D©ià£à” b ájå± B@jÀB{ëc@x@ßA$AjàEóMÕájˆ"`!j = …€ƒsQ€=ä(%-ä(%±à@ÑP€@å nå±àB7 Bá J `ƒä…± B@€ÆÀB„@Ùâø,¯ÿÿB@x@¿AAJàEz ÖáJ‰‰e±à=û¤Fÿÿë® =@®€Áf€{áˆ% áˆ"ó à@¶ €@”ëcâ óå±7÷Aë àb bᨠ£á¨…± B@`æÀBëc@x@ßA$A¨ àEÅ€ëñŠ'Â"ó =áj9…×aæä^%±à@›ƒ€@#P"JúÃ'%òÿÿôàJÀJ IàJëc B@aRàBÿÿÞ@x@ÇA ARàE<Øaàˆå ±Œ.Ç!R =@þ€ƒÂC€=áR"½à@?€@ÿ ÿ,£e¨ "äÿt/ÑYïÿÿL6~PB½àb†>€bàáÈoå± B@€æÀBƒå±@x@ßA$AjàE•÷à ˆá jaj =ájû€=ä(å ±à@{ú€@å$±àBÝù€Bá Jå± B@JàB@ÙãCå±@x@¿AAJàE³ÙⵎŽe±à=¤FÿÿB =@®€Á¹€{áˆ% å±à@X´€@ÿ ÿ â óå±Ú¢Bó àb¸¡á ë!¨ £å ±! B@`æÀB â ó!¨öÇ@x@ßA$A¨"àE¢nÚa¨ëcbó# =áj¾.Û`=áj%páj%±$à@!-€@â må±%àJ, J þâ¸å±& B@aRàBå±@x@ÇA AR'àE°åà ˆä F‘‘E±( =áRhí€=å"±)à@Àè€@èo!“óÓ³ï´ôäÿï"[äð a I‰B½*àb, bá jå±+ B@jÀBå±@x@ßA$Aj,àE6¡Übàˆá j’’Aj- = …€ƒ·¤€=ájå ±.à@ @ã Åëc/àBr£€ƒàÁÆü$ ƒä‹c0 B@€ÆÀB'ÿâø$ëc@x@¿AAJ1àE½\ÝáJ““e±2à=>o„FÿÿÞ3 =@®€Áµb€{äF% äF(¥4à@ú]€@Ÿëcâ óå±íÿ,þ5àbY bᨠ£á¨‹c6 B@`æÀB¤å±@x@ßA$A¨7àEDÞa¨ëc”•bó8 =ájlØ€=áj$áj%±9à@ÏÖ€@å,±:àJ; Jã >å±; B@aRàBå±@x@ÇA ><àEQßaRáÛä F––aR= =áR—€=äFáR"½>à@a’€@ö ÇÃõä‚að"å7‰KòaÀÅVE±?àbÍ‘€bàáÄfå±@ B@jÀBÿÿB@x@ßA$AjAàEØJàáj——E±B =áj\N€=â½%-â½1Cà@·M€@â r `@ãÅ"ü@BDàB Bá Jå±E B@JÀBå±@x@¿AAJFàE_ááJ˜˜e±Gà=à¤Få±H = ÀÁO €{áˆ% áˆ"óIà@œ€@—å ±)ø @è¥Ãâ óå±a¤A¨Jàbûá ¿ÿ#!¨ bá륱K B@`æÀBöÇ@x@ßA$A¨LàEåÁ€ëëc™šbóM =áj‚âaæä^"´Nà@p€€@å,±OàJÜ JA#îå±P B@aRàBå±@x@ÇA ARQàEó8ãaàˆå ±››aRR =áR›@€=â½å ±Sà@û;€@ö ÇÓLL/LRL^²MÐMa Û|B½TàbgÀb@$ájå±U B@jÀB|è oÿÿ,@x@ßA$AjVàEzô€ëå2á jœœajW =ájø€=ájä(%±Xià@`÷€@á å±YàBÂöàB$ ƒä‹cZ B@JÀBå±@x@¿AAJ[àE°äâµe±\à=„F{üy] = s Áñµ€{äF% å±^à@=±€@˜å ±!ˆ @å±Kâ ó屎Aë_àbœá å±` B@a¨ÀBå±@x@ßA$A¨aàE‡kåa¨ÿÿQ螟H¥b =áj¼+æ`=ájä^%±cà@*€@å,±dàJŠ) JA#äFå±e B@aRàBå±@x@ÇA ARfàE•âæaäF  aRg =áR9ê€=äFáR(ohà@™å€@ˆâ ½ @â½ ãFÑMTÔM½×Mö6ÂNFÿÿë®TKB½iàb bàá½å±j B@jÀBå±@x@ßA$AjkàEžçajáóå ±¡¡ajl =ájŸ¡€=áj%-ä(%±mà@ú €@à~Á$( @ån"ü@BnàB[àBå±o B@JÀBå±@x@¿AAJpàE¢YèáJ¢¢e±qà=#l„Få±r =ሎ_€{áˆ%páˆ"ósà@ßZ€@”â óë câ óå±ðÿ¤˜tàb> bá ë!¨ £áë«cu B@a¨ÀBüy@x@ßA$A¨vàE)éa¨ñ£¤bów =ájFÕ€=å"±xà@¨Ó€@‡@Såf!j$^üyyàJÀJA#àJå±z B@aRàBå±@x@ÇA AR{àE6ŒêaRáÛä F¥¥aR| =@þ€ƒã“€=áR"½}à@>€@Œâ ½$F!Rå±ó90àCq€Sqþå90ãa n6Df~àbªŽ€bàáÄ©å± B@€æÀBñ@x@ßA$Aj€àE½Gëáj¦¦aj =ájEK€=ä(%p€@3ÒA‚à@ J€@â r!j @ãÅ¥±ƒàB Bá J$àB«c„ B@JÀBå±@x@¿AAJ…àEDìaJàËâ µ§§${†à=ŤFñ‡ = ÀÁ4 €{áˆ% áˆ"óˆà@€@ë câ óå±ÊÄAë‰àbàá Âó!¨ £áë¥±Š B@`æÀBëc@x@ßA$Aj‹àEʾ€ëñ¨©KcŒ = …`ƒø~íaæä^"´à@Y}€@ë,cŽàJÅ| J å ± B@€ÎÀBÿÿ$X@x@ÇA ARàEØ5îaàˆâ ûªªaR‘ =áR=€=áR"½’à@ð8€@ëcLCr%®r÷%®ä a kc“àb\Àb@$çå±” B@jÀBxñ@x@ßA$Aj•àE^ñ ë@Y ˆá j««aj– =ájçô€=áj%-ä(+c—à@B @äåÃÅ屘àB£ó€ƒà B$ ƒä‘™ B@JÀB‹@Ù¡À…2aüy@x@¿AAJšàEå¬ï⵬¬@=›à=f¿„Fëcœ =@®€Áɲï`{å"±à@®€@ëcâ óå±¥kcžàb­€bâjÁe!¨å ±Ÿ B@`æÀBâ ó!¨ÿÿ,@x@ßA$Aj àElhða,àˆâ ó­­E±¡ =ájm€=áj)Ïâó%±¢à@hk€@å,±£àJÔj J â¸ëc¤ B@aRàBÿÿB@x@ÇA AR¥àEó#ñáR®®aR¦ =áRŠ+€=äFáR%±§à@ë&€@å ±ÁKSrïÁKå9‚Æ0ãa WÿF„¨àbWÀb@$áj屩 B@jÀBÿÿ$X@x@ßA$AjªàEyß ë@Y ˆâ ½¯¯aj« =ájã€=áj%-â½%±¬à@câ€@å nå±­àBÁá€BàÁä ƒå ±® B@JÀBëc@x@¿AAJ¯àE›òâµ°°e±°à=iƒ|ÿÿ$X± = s Áð €{äF% áˆ"ó²à@<œ€@—î W @îWâ óå±óÿF„³àbœá ᨠbá¨XF´ B@a¨ÀBëc@x@ßA$A¨µàE‡Vóa¨ñ±²E±¶ =áj¢ô`=ájä^%±·à@€@å,±¸àJn€Jã >å±¹ B@aRàBå±@x@ÇA ARºàE”Í€ˆã„ä F³³aR» =áR?Õ€=áR"½¼à@ Ð€@ñ L#àDðÁKàTý å8 áa J§D©½àb  bàá½aöâ½áо B@jÀB{è oüy@x@ßA$Aj¿àE‰õbàˆá j´´ajÀ =Àƒ¯Œ€=áj%-ä(%±Áà@  @à~Áå±ÂàBk‹€ƒà B)º ƒáJ«cà B@JÀBå±@x@¿AAJÄàE¢DöáJµµe±Åà=#W„Fÿÿ$XÆ =ሎJ€{áˆ% áˆ"óÇà@ÚE€@å ±5\ @å±+câ óå±îÿyÆÈàb> bᨠbᨅ±É B@a¨ÀBöÇ@x@ßA$A¨ÊàE(÷a¨ñ¶·bóË =ájLÀ€=è¥å ±Ìà@«¾€@å,±ÍàJÀJA#©oÃ>å±Î B@aRàBå±@x@ÇA ARÏàE6wøaRàˆä F¸¸aRÐ =áRé~€=â½áR"½Ñà@Bz€@è o @â½%±3%¨q à äÿ-0a výD©Òàb®y€bàáÁjå±Ó B@jÀBÿÿ @x@ßA$AjÔàE½2ùajç¥á j¹¹ajÕ =ájM6€=áj%-ä(%±Öà@ª5€@á å±×àB àB屨 B@JÀBå±@x@¿AAJÙàECîà Ëá Jºº${Úà=ĤFÿÿ¦Û =áˆ8ô€{äF% áˆ"óÜà@ï€@œâ ó @âóå±ÒAëÝàbßá Âó!¨ bå ±Þ B@a¨ÀBÿÿB@x@ßA$AjßàEÊ©úbóå±»¼bóà =ájjû`=áj$áj+cáà@eh€@å,±âàJÑg J å±ã B@aRàBÿÿ¦@x@ÇA ARäàE× üaàˆâ û½½aRå =áRs(€=áR"½æà@Ð#€@ë c Cà"ÃDðï´1ÿÿ@Ò“;B½çàb< bå(½å±è B@jÀBÿÿ/¼@x@ßA$AjéàE^Ü€ëáj¾¾N!ê =ájÞ߀=å"±ëà@< @á å±ìàB¢Þ€ƒàÁÁJ$&5䌮í B@JÀBÿÿ¦@x@¿AAJîàEå—ýbµŒ@Y£>â µ¿¿@=ïà=fª„Fÿÿ¦ð =áˆÙ€{áˆ% äF"óñà@%™€@ü yë câ óå±7ÉBóòàb…˜€bᨠ£á¨‹có B@a¨ÀBñ@x@ßA$AjôàElSþa¨ñÀÁbóõ =ájœÿ`=ä^%±öà@þ€@å,±÷àJjÀJA#¢rÂûëcø B@aRàBÿÿ,@x@ÇA ARùàEyÊ Ó@Y ˆâ ûÂÂaRú =áR!Ò€=áR"½ûà@yÍ€@ë c S‚3å8ð£å9Q0àÿÿ*  .B½üàbåÌ€bàáÁjå±ý B@jÀBëc@x@ßA$AjþàE†@@Tàˆá jÃÃE±ÿ =áj„‰€=ä(%-ä(+cjà@∀@å$±àBD Bˆ [Â,ÁJå± B@JàB@Ù¢oÀB2aüy@x@¿AAJàE‡AaJàËá JÄÄe±à=0T„FQÿÿ  =@®€Á{G€{áˆ% áˆ"óà@ÃB€@ë câ ó屯ÜAëàb# báÁe!¨ £å ± B@`æÀB ô !¨üy@x@ßA$A¨ àE ý€ëëcÅÆbó =áj)½aæájë c à@Œ»€@â m' @ÿÿÞ àJøº J þâ¸å± B@aRàBå±@x@ÇA ARàEtaàˆâ ûÇÇaR =áR»{€=å"±à@w€@ü y c¡ å8d` pBÿÿ§Vò‚B½àb‡v bA;ájå± B@jÀBöÇ@x@ßA$/ŒàE¡/ájÈÈaj =áj*3€=ájå ±à@„2€@ã ÅëcàBå1€Bá J$ ƒä‹c B@JÀBñ@x@¿AAJàE(ëà ˆâ µÉÉe±à=©¤FÿÿÞ =ሠñ€{äF% å±à@aì€@“ñ $F @ñ6Çâ óå±VpBóàbÄá å± B@a¨ÀBÿÿ,@x@ßA$A¨àE¯¦bóëcÊËH¥ =ájÛf`=áj%páj+c à@>e€@å,±!àJªd J äFå±" B@aRàBå±@x@ÇA AR#àE¼aàˆâ ûÌÌaR$ =áRa%€=äFáR(o%à@À €@å ±s«àÔ@‚Æà0à.å:àB½&àb,Àb@(ájå±' B@jÀBÿÿB@x@ßA$Aj(àECÙ ë@Y ˆá jÍÍB½) = +€ƒ¿Ü€=â½%-â½+c*à@ @‡ RÀ~ÃÅå±+àB{Û€ƒàÁÀBå±, B@€ÆÀBå±@x@¿AAJ-àEÊ”âµÎÎe±.à=K§„Få±/ =ሶš€{áˆ%páˆ"ó0à@–€@å ±!ˆ @å±9»â ó屦õAë1àbf•€bᨠbçZ‘2 B@a¨ÀBüy@x@ßA$A¨3àEPP a¨ëcÏÐbó4 =ájm `=ájä^%±5à@Ï€@å,±6àJ;ÀJA#£½Âûå±7 B@aRàBÿÿ @x@ÇA AR8àE^Ç Ó@Y ˆä FÑÑaR9 =áRö΀=áR"½:à@VÊ€@å ±ƒƒ`å:ð€tðå8ÿÿ* ¦€B½;àbÂÉ bàáÁjå±< B@jÀBå±@x@ßA$Aj=àEå‚ bå2á jÒÒaj> =áj]†€=ájå ±?à@·…€@á $( @ãÅ"üAJ@àBàBå±A B@JàB@Ù¢oÁ/lñ@x@¿AAJBàEk> aJàËá JÓÓ@=Cà=ì¤FÿÿtD =@®€ÁXD€{è¥% å±Eà@©?€@å ±ë câ óå±rA¨Fàb báÁe!¨ £á륱G B@`æÀBå±@x@ßA$AjHàEòù€ëå±ÔÕbóI =áj#º aæájå ±Jà@…¸€@â m!j @ñKàJñ· JA#â¸å±L B@aRàBëc@x@ÇA ARMàEqa‡@Y ˆâ ûÖÖaRN =áR¬x€=äF!â½Oà@ t€@å ± “ôtð€ä ðä@¸ÀíÿÌ@Pàbxs€bàá½å±Q B@jÀBå±@x@ßA$AjRàE†,ajáóá j××ajS =áj0€=áj%páj+cTà@m/€@à~Áå±UàBÎ.àB$ ƒä‘V B@JÀBëc@x@¿AAJWàE èà ˆá JØØe±Xà=Žú„FÿÿBY =áˆùí€{áˆ% áˆ(¥Zà@Jé€@ÿ ÿB!ˆ @ëcâ óå±I>Aë[àb©á Áë!¨ bᨋc\ B@a¨ÀBå±@x@ßA$A¨]àE”£bóñÙÚbó^ =ájÁc`=å"±_à@#b€@ÿ,ÿ¦`àJŽa€Jã >å±a B@aRàBëc@x@ÇA ARbàE¡aàˆâ ûÛÛOªc =áRF"€=áR"½dà@¥€@ÿ ÿ,£‚ ð‚b€ Ác8ÀXâB½eàb bàá½å±f B@jÀBå±@x@ßA$AjgàE(Ö€ëàˆá jÜÜajh =áj¸Ù€=`ï§ZÀÆe-ä(%±ià@ @à~À@$(ån«cjàBtØ€ƒà B$àB¥±k B@JÀBå±@x@¿AAJlàE¯‘bµç…á JÝÝe±mà=0¤„Fñn =ሣ—€{áˆ% áˆ"óoà@ì’€@ü y)ø @å±6Çâ óå±üAëpàbK báÁë!¨ bå ±q B@a¨ÀBüy@x@ßA$A¨ràE5Ma¨ÿÿL6Þßbós =ájk `=ä^"´tà@Ì €@ë,cuàJ8ÀJ ãå±v B@aRàBå±@x@ÇA ARwàECÄ Ó@Y ˆâ ûààaRx =áRØË€=áR"½yà@3Ç€@å ±³11+fÁK<~@¸À¿B½zàbŸÆ€bàá½å±{ B@jÀBÿÿ$X@x@ßA$Aj|àEÉbàˆá jááaj} =ájVƒ€=áj%-ä(%±~à@±‚€@ã Åå±àBàB$ ƒä‹c€ B@JÀBÿÿ¦@x@¿AAJàEP;áJââe±‚à=ѤFëcƒ =áˆ8A€{å"±„à@Š<€@ë câ óå±ÿk¤…àbìá Âó!¨å ±† B@a¨ÀB”ÿÿ,@x@ßA$A¨‡àE×ö€ëëcãäbóˆ =ájí¶aæâóïÂ%±‰à@Nµ€@å,±ŠàJº´ JA#¥(ÁR屋 B@aRàBå±@x@ÇA ARŒàEämaàˆä FååaR =áR–u€=áR%±Žà@ðp€@å ±ÃpCå9Ó”På9pÿÿë®XAD©àb\ bàáÁjå± B@jÀBâ ½ÿÿô@x@ßA$Aj‘àEk)ájææaj’ =ájó,€=H@—â1ëc“à@O @å$±”àB¯+€ƒá J$å ±• B@JÀBëc@x@¿AAJ–àEòäà ˆâ µççe±—à=s÷„F屘 =áˆâê€{áˆ% `ï€3bó™à@,æ€@›ÿÿ,â óå±ü³BóšàbŽå€bᨠ£çZ‘› B@a¨ÀBÿÿ @x@ßA$A¨œàEy bóå±èébó =ájš``=ájå ±žà@û^€@å,±ŸàJgÀJA#ãå±  B@aRàBÿÿ]L@x@ÇA AR¡àE†aàˆâ ûêêaR¢ =áR4€=å"±£à@Ž€@å ±ÓÁHå9ÿ"z¯9 ˜Ea € B½¤àbú€bàáå±¥ B@jÀBå±@x@ßA$Aj¦àE Óà ˆá jëëaj§ =ájÖ€=â½%-ä(+c¨à@éÕ€@å$±©àBI Bá J屪 B@JÀBå±@x@¿AAJ«àE“Žâµììe±¬à=¡„Få±­ =ሄ”€{âó% áˆ(¥®à@΀@—!ãû,ì @ñKâ óå±$ûAë¯àb/ bᨠbå ±° B@a¨ÀBå±@x@ßA$A¨±àEJa¨üyíîbó² =ájP `=âóå ±³à@±€@å,±´àJÀJA#áµå±µ B@aRàBå±@x@ÇA AR¶àE(Á€ÓãÇä FïïaR· =áRÕÈ€=áR"½¸à@0Ä€@å ±ã D¼¯9t!/õ‚ä4ƒõƒa Ï›B½¹àbœÃ€bàá屺 B@jÀBå±@x@ßA$Aj»àE®|!báóá jððaj¼ =áj6€€=ájå ±½à@“€@á $( @ãÅ.`AJ¾àBò~€BàÁÁJ屿 B@JÀBå±@x@¿AAJÀàE58"áJññe±Áà=¶¤F|ÿÿB =áˆ%>€{áˆ% å±Ãà@p9€@å ±ö Çâ óå±kiA¨ÄàbÑá ᨠ£áë«cÅ B@a¨ÀBå±@x@ßA$A¨ÆàE¼ó€ëëcòóbóÇ =ájâ³#aæçå ±Èà@C²€@ü,yÉàJ¯± JA#å±Ê B@aRàBñ@x@ÇA ARËàEÉj$aã„ä FôôaRÌ =áRgr€=â½áR%±Íà@Ám€@ÿ ÿôóÿ* %±Te±‚å±]|yÎàb- bàá½ñÏ B@jÀB‚ñ@x@ßA$AjÐàEP&%ájõõ]äÑ =ájØ)€=áj%pä(+cÒà@4 @á å±ÓàB”(€ƒà Bå±Ô B@JÀBå±@x@¿AAJÕàE×ဈã>â µööe±Öà=Xô„Få±× =áˆÃç€{áˆ% áˆ"óØà@ ã€@ü y!ˆ @ëcâ óå±ù|yÙàbsâ€báÂó!¨ bå ±Ú B@a¨ÀBÿÿ,@x@ßA$A¨ÛàE]&bóüy÷øbóÜ =áj„]'`=å"±Ýà@è[€@ë,cÞàJTÀJA#å±ß B@aRàBw àBÿÿnb@x@ÇA ARààEk(aÿÿ!¢ùùaRá =áR€=â½å ±âà@k€@å±M%±å±3 B@aRàBå±@x@ÇA AR4àEñº€ˆâû  aR5 =áR¥Â€=å"±6à@õ½€@ñ  Cþ¡ßå0à:6ÎÁa –ƒB½7kà£a bàá½å±8 B@jÀB‹è oëc@x@ßA$Aj9àExv7bàˆä faj: =ájìy€=â½å ±;à@J @â r$( @ãÅ"üAJ<àB¨x€ƒàÁÁJ$àB±= B@JÀBå±@x@¿AAJ>àEÿ18áJe±?à=€D„Få±@ =ÀÁû7€{áˆ% äF%±Aà@;3€@£ Sìf)ø @å±+câ óå±2qA¨Bàb›2€bᨠbáë«cC B@a¨ÀB¨â ó"ôëc@x@ßA$A¨DàE…퀈ëcQE =áj­­9aæä^"´Fà@¬€@å,±GàJ|« J áµå±H B@aRàBÿÿ@Ò@x@ÇA ARIàE“d:aàˆä FaRJ =áRJl€=â½ç"½Kà@§g€@ë cSHå%¯ `ñ‰Ñ”î”aÀ¡…B½Lk࣠bàáÄfå±M B@jÀBÿÿ…*@x@ßA$AjNàE ;ajå2á jajO =áj–#€=áj%-ä(6ÇPà@ó"€@á å±QàBRàBå±R B@JÀBå±@x@¿AAJSàE Ûà Ëá Je±Tà=!î„FÿÿF„U = ÀÁ‘á€{äF% áˆ"óVà@ÝÜ€@—å ±!ˆ @å±+câ óå±wAëWàb< bá ë!¨ bå ±X B@`æÀBÿÿÞ@x@ßA$A¨YàE'—aäFaR_ =áRà€=äFáR"½`à@=€@å ±cPPï”%©@H| Óàˆ8B½aàb©€bàáå±b B@jÀBå±@x@ßA$>càE»É€ˆáóä fajd =Àƒ;Í€=ájå ±eà@™Ì€@à~Á$( @ãÅ"üAJfàBûËàB$àB«cg B@JàB@Ùâø)ºöÇ@x@¿AAJhàEB…?bµàËá Je±ià=ׄFÿÿ$Xj =@®€Á2‹€{áˆ% å±kà@~†€@î Wë câ óå±,ÊA¨làbÞá Áë!¨ £áë«cm B@`æÀBå±@x@ßA$A¨nàEÉ@@a¨ñbóo =ájõA`=å"±pà@Xÿ€~ÿ,ÿ qàJÄþ JA#â¸ëcr B@aRàBå±@x@ÇA ARsàEÖ·âûaRt =áRŠ¿€=áR%±uà@Úº€@â ½$F%òèosƒ*‘pþýÑsƒ\a ´B½vàbF bàá½ëcw B@jÀBñ@x@ßA$AjxàE]sBbàˆä fajy =ájív€=ä(%-ä(+czà@G @â r!j @ãÅ¥±{àB©u€ƒàÁÁJ$àB¥±| B@JÀBëc@x@¿AAJ}àEä.CaJàËá Je±~à=eAƒ|ÿÿ…* =áˆÜ4€{áˆ% áˆ"ó€à@ 0€@ ëcâ óå±ÛFAëàb€/€báᨠ£á륱‚ B@a¨ÀB¤ñ@x@ßA$A¨ƒàEjꀈëc bó„ = ƒ›ªDaæä^"´…à@ý¨€@è '$^üy†àJiÀJ å ±‡ B@aRàBÿÿ5n@x@ÇA ARˆàExaEaàˆâ û!!aR‰ =áR,i€=áR"½Šà@Œd€@ÿÿ5nMƒt|ð€E·‚ÀÀxa êB½‹àbøc b@$ç屌 B@jÀB{â ½öÇ@x@ßA$AjàEþFáj""ajŽ =ájƒ €=áj%-ä(%±à@Ý€@ã Åå±àB?àB屑 B@JÀBÿÿ @x@¿AAJ’àE…Ø€ËáÓâ µ##e±“à= ë„Fvÿÿ ” =áˆiÞ€{å"±•à@¾Ù€@ÿ ÿ $F%Nè¥6Çâ óå±NðAë–àb! báÿÿx— B@a¨ÀBÿÿF„@x@ßA$A¨˜àE ”Gbóå±$%bó™ =áj4TH`=áj%pâó%±šà@—R€@å,±›àJÀJA#äFëcœ B@aRàBå±@x@ÇA ARàE Iaàˆâ û&&aRž =áRÊI`=äFáR%±Ÿà@)€@å ±“\|ƒ}ÐÐaƒ(£a ÿŽ kࣕ I`£àá½å±¡ B@jÀBÿÿ5n@x@ßA$Aj¢àE ÆIàˆá j''aj£ =áj$Ê€=â½å ±¤à@ÉI`~å$±¥àBàÈ€Bá J屦 B@JÀBëc@x@¿AAJ§àE'‚J`Ëàˆá J((e±¨à=¨¤Fñ© = ÀÁˆ€{áˆ%päF"óªà@dƒ€@ÿ ÿ@Ò': @å±(¥â óå±4ÿŽ«àbÃá Âó!¨ bë c¬ B@`æÀBÿÿ,@x@ßA$A¨­àE®=Ka¨ñ)*bó® =ájÚý€=ájä^%±¯à@<ü€@å,±°àJ¨û JA#å±± B@aRàBÿÿ¦@x@ÇA AR²àE»´LaRàˆâ û++aR³ =áR`¼€=å"±´à@»·€@ö Ç£à$\¿ÿ$Rå:ð a ¦’Hoµkà£' bàá½å±¶ B@jÀBÿÿ5n@x@ßA$Aj·àEBpMáj,,aj¸ = ƒÂs€=â½å ±¹à@ @å$±ºàB‚r€ƒá J$ ƒäŸÿ5n» B@JàB@Ùâø,¯öÇ@x@¿AAJ¼àEÈ+NáJ--e±½à=J>ƒ|ëc¾ =@®€Á¹1€{áˆ% 屿à@-€@—ñ ë câ óå±`BóÀàbd,€bᨠ£á¨Ÿÿ/¼Á B@`æÀBî W!¨ÿÿ,@x@ßA$A¨ÂàEO瀈ëc./bóà =ájp§Oaæájå ±Äà@Ò¥€@â m!j @ñÅàJ>ÀJ þâ¸ëcÆ B@aRàBå±@x@ÇA ARÇàE]^PaãÇå ±00aRÈ =áRf€=áR(oÉà@ia€@î !(o!Rèo³Áÿÿ¢0à0åa csB½Êkà£Õ`€bá jå±Ë B@jàBâ ½ñ@x@ßA$AjÌàEãQajàˆá j11ajÍ =ájs€=áj%-ä(1Îà@΀@à~ƹ!j @`ò (®@BÏàB/àBå±Ð B@JÀBëc@x@¿AAJÑàEjÕà Ëá J22e±Òà=ë¤Fÿÿ/¼Ó =áˆbÛ€{áˆ% áˆ"óÔà@¨Ö€@ÿ ÿB!ˆ @âóå±ÇA¨Õàb bá ë!¨ báë¥±Ö B@a¨ÀBöÇ@x@ßA$A¨×àEñRbóëc34bóØ =ájQS`=ê"´Ùà@|O€@å,±ÚàJèN€Jå(Ã>å±Û B@aRàBñ@x@ÇA ARÜàEþTaáÛâ û55aRÝ =áR³€=â½%pâ½Þà@ €@ÿ ÿ$XÃ8pÁK$ü`$þ`$pa ¾•B½ßàbz €bàáÁj`£â½¿ÿôà B@jÀBÿÿ/¼@x@ßA$AjáàE…Ãà ˆá j66ajâ =ájÇ€=áj%páj%±ãà@pÆ€@á $( @á"ü@BäàBÑÅàB$àB«cå B@JÀBå±@x@¿AAJæàE Uâµ77e±çà=‘„Få±è =ህ€{áˆ% áˆ%±éà@I€€@ÿ ÿ$Xâ óå±gA¨êàb¨á Âó!¨ £á륱ë B@a¨ÀB è ¥(¦ëc@x@ßA$A¨ìàE’:Va¨ëc89bóí =!b€ƒ¼ú€=å"±îà@ù€@å,±ïàJ‰ø J ë cð B@aRàBëc@x@ÇA ARñàE ±WaRàˆä F::aRò =áRM¹€=áR"½óà@°´€@ö ÇÓz B€o 5E qa ô¾B½ôàbÀb@$ï‡å±õ B@jÀBxâ ½ëc@x@ßA$AjöàE'mXajç¥á j;;aj÷ =áj«p€=ä(%-ä(%±øà@ @à~ÃÅå±ùàBgo€ƒá J$ ƒä‘ú B@JÀBå±@x@¿AAJûàE­(YaJàˆá J<bó =ájb¤Zaæájê(fà@â€@å,±àJ/ÀJA"äFëc B@aRàBå±@x@ÇA ARàEB[[a‹@Y ˆâ û??aR =áRób€=áR"½à@N^€@ë cãªDÖ€b m€]€Vÿÿ¦½¤B½ àbº]€bä ©å± B@jÀBöÇ@x@ßA$Aj àEÈ\ajáóá j@@aj =ájD€=å"± à@ €@á å±àB BàÁäå ± B@JÀBå±@x@¿AAJàEOÒ€Ëàˆá JAAe±à=ФFå± = ¹ Á?Ø€{áˆ% å±à@‰Ó€@ë câ óå±VBóàbëá ᨠ£å ± B@a¨ÀBœè ¥(¦ëc@x@ßA$A¨àEÖ]bóñBCbó =ájN^`=ájå ±à@iL€@ÿ,ÿ àJÔK€JârÁRå± B@aRàB{ÿÿ5n@x@ÇA ARàEã_aàˆâ ûDDaR =áR• €=å"±à@÷€@ë có€Rå8´Iå9´C å:ÿÿbþ‹ÿIBàbcÀbA;¦“Ájå± B@jÀByâ ½ëc@x@ßA$Aj àEjÀ ë  ˆá jEEaj! =ájòÀ=â½%-ä(C"à@N @å$±#àB²Â€ƒàÁÁJ)º ƒä‹c$ B@JàB@Ù¡ÀB!Küy@x@¿AAJ%àEñ{`bµàËá JFFe±&à=rŽ„Fÿÿbþ' =@®€ÁÙ€{áˆ% áˆ(¥(à@+}€@ÿ ÿ,â óå±/ÿO*)àb|€báÁe!¨ £á¨‹c* B@`æÀBÿÿQè@x@ßA$A¨+àEw7aa¨ëcGHbó, =áj•÷€=ájå ±-à@öõ€@â müy.àJbÀJA#â¸å±/ B@aRàBå±@x@ÇA AR0àE…®baRàˆâ ûIIaR1 =áR+¶€=å"±2à@…±€@ÿÿ, NPC€A%µL%µTa /ÿ/¼3àbñ°€bàá½å±4 B@jÀBöÇ@x@ßA$Aj5àE jcájJJaj6 =ájm€=áj%-ä(%±7à@ðl€@ã Åëc8àBPàB$ ƒä…±9 B@JàB@Ùâøå±@x@¿AAJ:àE’%dáJKK@=;à=8ƒÇÿÿ/¼< =@®€Á‚+€{äF% áˆ%±=à@Í&€@—ÿÿ,â ó屋ÿ/¼>àb. bâ ó!¨ £á¨…±? B@`æÀBëc@x@ßA$Aj@àEá€ëå±LMbóA =ájK¡eaæájå ±Bà@¬Ÿ€@å,±CàJÀJ â¸å±D B@aRàBå±@x@ÇA AREàE&Xfaàˆå ±NNaRF =áR¼_€=áR"½Gà@[€@ëc Nå:Ó”@@@ÿÿ,1ÿ œHàbŠZ bA;ájå±I B@jÀBÿÿ/¼@x@ßA$AjJàE­gájOOajK =áj1€=áj%-ä(%±Là@€@á å±MàBíàB$ ƒä…±N B@JÀBñ@x@¿AAJOàE4Ïà ˆâ µPPe±Pà=µ¤Få±Q =áˆÕ€{áˆ% áˆ"óRà@kЀ@ëcâ ó展ÿ„SàbÐá È¥!¨ £á¨…±T B@a¨ÀBöÇ@x@ßA$A¨UàE»ŠhbóñQRbóV =ájíJi`=è¥å ±Wà@MI€@å,±XàJ¹H JA#äFå±Y B@aRàBå±@x@ÇA ARZàEÈjaàˆâ ûSSaR[ =áRj €=â½áR"½\à@Ì€@å ± #:´u¯:%Œ€+gXÃ`ínD©]àb8Àb@"ájå±^ B@jÀBzñ@x@ßA$Aj_àEO½ ë@Y ˆá jTTaj` =áj×À€=áj%-Á @3ÒAaà@3 @à~ÃÅå±bàB“¿€ƒà B$ ƒä…±c B@JÀBŒ@¥¡À…ëc@x@¿AAJdàEÕxk@ àˆá JUUe±eà=W‹„Få±f =@®€ÁÂ~€{áˆ% áˆ"ógà@z€@ü y/ª @üy+câ óå±IœAëhàbqy€báÁe!¨ bᨅ±i B@`æÀB™â ó!¨öÇ@x@ßA$A¨jàE\4la¨ëcVWbók =ájô€=å"±là@ßò€@ˆ@Sâm!jÿÿ màJKÀJ þàJå±n B@aRàBå±@x@ÇA ARoàEj«maRï+â ûXXaRp =@þ€ƒ³€=áR"½qà@z®€@ñ 31 IV¿TDú€ üy³XB½ràbæ­€bá jå±s B@€æÀB}â ½öÇ@x@ßA$AjtàEðfnajàˆá jYYaju =ájpj€=ä(%-ä(+cvà@Íi€@à~ÃÅå±wàB0àB$ ƒä…±x B@JàB@ÙãC!KöÇ@x@¿AAJyàEw"oáJZZe±zà=ø¤FÿÿF„{ =@®€Ág(€{áˆ% áˆ"ó|à@²#€@ÿÿ'L @å±+câ ó屪Aë}àb bá ë!¨ bá¨ÿ¡¤~ B@`æÀBñ@x@ßA$A¨àEþÝ€ëëc[\bó€ =áj žpaæä^Kà@mœ€@â m层àJÙ›€Jå(Ã>屃 B@aRàBå±@x@ÇA AR„àE UqaáÛä F]]aR… =áRÂ\€=áR"½†à@X€@ÿÿ,NC€-Lå6d`äõàÞÿIB‡àb‡W€bàáÁj屈 B@jÀBÿÿQè@x@ßA$Aj‰àE’ráj^^ajŠ =áj"€=ä(%-ä(%±‹à@€@â r屌àBÞ€Bá J$ ƒä…± B@JÀBëc@x@¿AAJŽàEÌ€ˆÿÿM__e±à=šÞ„Fëc =áˆÒ€{áˆ% áˆ"ó‘à@XÍ€@ÿÿ,Nâ óå±zÿO*’àb¹¡á¨ £á¨…±“ B@a¨ÀB è ¥'Zëc@x@ßA$A¨”àEŸ‡sbóëc`abó• =ájËGt`=ä^%±–à@*F€@å,±—àJ–E J þèj屘 B@aRàBÿÿ@Ò@x@ÇA AR™àE­þà ˆå ±bbaRš =áRXuá$R"½›à@µ€@ë c S8õ9õ:"õ‚ÿ]I"ðÿÿ5næn!œàb! bá jå± B@jÀB~â ½ëc@x@ßA$AjžàE4º€ëå2á jccajŸ =áj¼½€=ä(%-ä(%± à@ @à~ƹ屡àBx¼€ƒá Jå±¢ B@JÀBå±@x@¿AAJ£àEºuvbwàˆá Jdde±¤à=;ˆ„Fÿÿ@Ò¥ =ራ{€{äF% áˆ"ó¦à@öv€@—ü y!ˆ @ëcâ óå±Bt §àbV báÁë!¨ bå ±¨ B@a¨ÀBëc@x@ßA$A¨©àEA1wa¨å±efbóª =ájxñ€=âó0Ôáj%±«à@Øï€@å,±¬àJDÀJA#îå±­ B@aRàBå±@x@ÇA AR®àEN¨xaRàˆâ ûggaR¯ =áR°€=â½áR"½°à@W«€@î! @â½c‚ÆàTþðSrû"~}aÀ’éD©±àbê€bä ©å±² B@jÀBÿÿWš@x@ßA$Aj³àEÕcyájhhaj´ =ájag€=â½å ±µà@¿f€@á $( @ãÅ"üAJ¶àB Bâ,äàB«c· B@JÀBå±@x@¿AAJ¸àE\záJiie±¹à=ݤF屺 =áˆP%€{áˆ% äF"ó»à@” €@ÿÿF„â óå±6A¨¼àbøá ᨠ£áë«c½ B@a¨ÀBëc@x@ßA$A¨¾àEãÚ€ëñjkbó¿ =áj›{aæáj$áj"´Àà@v™€@‡@SåfñÁàJᘀJâ ûå±Â B@aRàB}ÿÿ @x@ÇA ARÃàEðQ|aàˆå ±llaRÄ =@þ€ƒ›Y€=áR"½Åà@øT€@ë csÿ]A \µÑmƒ\tà…B½Æàbd bàá½å±Ç B@€æÀBÿÿ@Ò@x@ßA$AjÈàEw }ájmmajÉ =áj€=áj%-â½+cÊà@d€@á å±ËàBÃàB)º ƒä‘Ì B@JÀBå±@x@¿AAJÍàEþÈ€ˆæ}â µnne±Îà=Û„Få±Ï =áˆê΀{áˆ% áˆ"óÐà@:Ê€@ÿ ÿ,â óå±!íAëÑàbšÉ€báÂó!¨ £á¨‘Ò B@a¨ÀBÿÿ/¼@x@ßA$A¨ÓàE„„~bóëcopbóÔ =áj´D`=ê%±Õà@C€@ë,cÖàJƒB JA#åüå±× B@ àBÿÿ@Ò@x@ÇA ARØàE’ûà ˆâ ûqqaRÙ =áR=€aRâ½ç"½Úà@šþ€~å ±ƒÑ”%³ €yüG² ÿÿ]L$ÿ‚6Ûàb bá jå±Ü B@jÀBå±@x@ßA$AjÝàE·,àˆá jrrajÞ =ájº€=â½%-ä(%±ßà@ê¹€@à~ÃÅå±ààBQ Bä ƒä…±á B@JÀBƒ@Ùâø2aüy@x@¿AAJâàEŸrbwàËá Jsse±ãà= …„Fÿÿ/¼ä = ž€Áx€{âó% áˆ"óåà@Ûs€@î W)ø @îW+câ óå±€Aëæàb; báÁëå±ç B@`æÀBñ@x@ßA$A¨èàE&.‚a¨ëctubóé =ájVî€=âóä^%±êà@¹ì€@å,±ëàJ%ÀJA#⸠`‹âû-‚ûì B@aRàBå±@x@ÇA ARíàE3¥ƒaRàˆâ ûvvaRî =áR߬€=â½áR"½ïà@;¨€@å ±“\ðx]|ƒ}"ƒ)ïð£ía vfB½ðàb§§€bàá½`£áj¿ÿ* ñ B@jÀBÿÿ¦@xï 9A$Ajò @`Eº`„ájwwAó =ájBd€=áj%-ä(%±ôà@ c€@á  `@á"ü@BõàBàB àB¥±ö B@aàB âøå±@x@¿AAJ÷àEA…áJxxe±øà=¤Fzÿÿ@Òù =@®€Á1"€{áˆ%páˆ"óúà@}€@å ±!ˆ @å±?mâ óå±Ò”A¨ûàbÝá Âó!¨ báë«cü B@`æÀBå±@x@ßA$A¨ýàEÈ×€ëå±yzbóþ =ájè—†aæå"±ÿà@J–€@â mcûÿÿ5nmà‹¶• J â¸å± B@aRàBÿÿ/¼@x@ÇA ARàEÕN‡aàˆå ±{{aR =áRyV€=áR"½à@ÙQ€@å ±£ðQ *à$þ``/`a VB½àbE bàá½aµâ½¥± B@jÀBÿÿF„@x@ßA$AjàE\ ˆáj|8š%± =ájà €=H@—äêe-ä(%± à@: @ë $(%òᥱ àBœ €ƒá J$àB¥± B@JÀBŒ àÆå±@x@¿AAJ àEâÅà ˆâ µ}}e± à=dØ„Fyå± =@®€ÁÓË€{âó% áˆ"óà@Ç€@å ±$F @å±+câ ó屡ÿ'Làb~Æ€bᨠbá륱 B@`æÀBå±@x@ßA$A¨àEi‰bóÿÿt~bó =ájŽAŠ`=ájê"´à@ð?€@å,±àJ\ÀJA#â¸å± B@aRàBå±@x@ÇA ARàEwø€ÓãÇâ û€€aR =áR'‹á$R"½à@‡û€~ÿ ÿB³@$pT+_à$ Ñ‘1 a `ÿ2zàbóú b ájå± B@jÀBå±@x@ßA$AjàEý³,àˆá jaj =áj‰·€=å"±à@ä¶€@á鯹å±àBE BàÁÀBå± B@JÀBå±@x@¿AAJ!àE„oŒbwàˆá J‚‚e±"à= ‚„Fñ# =áˆpu€{áˆ% å±$à@Áp€@ö Çâ óå±Ä•'%àb  báᨠ£å ±& B@a¨ÀBöÇ@x@ßA$A¨'àE +a¨ëcƒ„bó( = …`ƒ+ë€=ájå ±)à@Žé€@å,±*àJúè€Jã >å±+ B@€ÎÀBå±@x@ÇA AR,àE¢ŽaRáÛâ û……aR- =áRÍ©€=ç%±.à@$¥€@ÿ ÿ ÂyÈ~ €:eµ0Ña uÿÿ‚/àb¤€bàá½å±0 B@jÀB„ÿÿÞ@x@ßA$Aj1àEŸ]áj††aj2 =áj/a€=â½*ßä(#Å3à@Š`€@â rå±4àBë_€Bá Jå±5 B@JÀBå±@x@¿AAJ6àE&áJ‡‡e±7à=§¤Fñ8 =ሀ{áˆ% áˆ"ó9à@c€@›ÿ ÿÞë câ óå±ÑAë:àbÂá Âó!¨ £å ±; B@a¨ÀB â ó- ÿÿ @x@ßA$A¨<àE¬Ô€ëüyˆ‰bó= =ájÍ”‘aæájå ±>à@/“€@å,±?àJ›’ J þéøå±@ B@aRàB2àB @' @x@ÇA ARAàEºK’aàˆå ±ŠŠaRB =áR^S€=áR"½Cà@¶N€@å ±ÓKW‚yÝ%µ €&%µ ýà– B½Dàb" bàá½å±E B@ABàBå±@x@ßA$AjFàEA“ajå2á j‹‹ajG =ájÉ €=ájå ±Hà@$ @à~Áå±IàB‰ €ƒá J$ ƒä™}J B@JàB@Ùìñ$üy@x@¿AAJKàEÇÂà ˆá JŒŒe±Là=HÕ„F{ñM =@®€Á¼È€{è¥% å±Nà@Ä€@å ±!ˆ @ñ6Çâ óå±2øBóOàbcÀbᨠb᨟ÿ,P B@`æÀBå±@x@ßA$A¨QàEN~”bó屎bóR =ájk>•`=ájå ±Sà@Í<€@â m' @öÇTàJ9ÀJ â¸å±U B@aRàBëc@x@ÇA ARVàE[õ Ó@Y ˆâ ûaRW =áRý€=äF!â½Xà@lø€@å ±ãà$@q ‚yç%µ€@¸ÀØyB½YàbØ÷€b‡@% áÄ©å±Z B@jÀBå±@x@ßW =>[ @`Eâ°–bájaj\ =ájr´€=áj%-áj+c]à@ͳ€@à~Á$( @ãÅ4@B^àB.àBå±_ B@aàBëc@x@¿AAJ`àEil—áJ‘‘e±aà=ê¤Få±b =áˆ]r€{H âO8P% áˆ(¥cà@¦m€@ÿ ÿôâ óå±|‹A¨dàb bˆà £!¨ £á륱e B@a¨ÀBå±@x@ßA$A¨fàEð'˜á¨’’bóg =áj¤,€=å"±hà@+€@å,±iàJp* J áöå±j B@aRàB<àBëc@x@ÇA ARkàEvãà ˆå ±““aRl =áR)ë€=áR"½mà@‡æ€@‹î !$F%òèoó+Wà$P¥µö~ ýàÍB½nàbòå€bá jå±o B@jÀBÿÿô@x@ßA$AjpàEýž™â½””ajq =ájy¢€=ä(%pä(%±rà@Ö¡€@æ ¹!j @ãÅ¥±sàB9 Bá J$àB«ct B@JàB@Ùâøëc@x@¿AAJuàE„ZšáJ••e±và=Œ(‚ñw „àƒ€  Œ¿ÿé>w = ) Át`€{áˆ% áˆ"óxà@¿[€@ü y!ˆ @âóå±iòAëyàb  bᨠbá륱z B@a¨ÀBüy@x@ßA$A¨{àE ›a¨öÇ–—bó| = …`ƒBÖ€=ä^"´}à@¡Ô€@â mëc~àJ ÀJ â4?" å± B@€ÎÀB|å±@x@ÇA AR€àEœaRàˆå ±˜˜aR =áR¿”€=áR"½‚à@€@ÿÿ,O "Mäÿÿ 1B½ƒàbˆ€bàá¡j屄 B@jÀBå±@x@ßA$Aj…àEŸHáj™™aj† =ájL€=ä(%-ä(%±‡à@|K€@å$±ˆàBÛJ€Bá J屉 B@JàB@Ùbîáå±@x@¿AAJŠàE%žáJšše±‹à=§¤F|ñŒ =@®€Á €{áˆ% áˆ"óà@a€@å ±$F @å±Ãâ ó展ðAëŽàbÁá Âó!¨ bå ± B@`æÀBÿÿ]L@x@ßA$A¨àE¬¿€ëñ›œbó‘ =áj»Ÿaæáj)Ïáj%±’à@~€@â m!j @ñ“àJ‡} JA#â¸å±” B@aRàBå±@x@ÇA AR•àEº6 aãÇå ±aR– =áRe>€=G@—àÆ.!!áR"½—à@¾9€@”ëcO)ð1 ÑqKjÿÿÿto!B½˜àb* bá jå±™ B@jÀB‚ù …ÿÿ$X@x@ßA$AjšàE@ò€ëàˆá jžžaj› =áj¼õ€=â½å ±œà@ @áéÃÅ)Ú @ãÅK þC#¯àBxô€ƒàÁÀB$àB«cž B@JÀBñ@xœ 9AAJŸ @€EÇ­¡@@Œ@¢wá JŸŸe± à=HÀ„Fÿÿô¡ =ሿ³€{H  {Á"ó% âó¢à@¯€@Ÿâ ó @å±â óå±ËÓA¨£àbg®€bᨠbáë«c¤ B@abÀBñ@x@ßA$A¨¥m`€ENi¢a¨ëc ¡bó¦ =áji)£`=ájä^K§à@É'€@å,±¨àJ5 Jã >屩 B@a àBå±@x@ÇA ARªàE[à Ó@Y¢â û¢¢aR« =áRè€=å"±¬à@cã€@ë c #ðïñzÀÀÑ•Ÿÿng@¸À—ÿ$X­àbÏâ€bàáÃaå±® B@jÀBå±@x@ßA$Aj¯àE⛤bàˆá j££aj° =ájrŸ€=â½%-ä(%n±à@О€@â rå±²àB. BàÁÁJå±³ B@JÀBå±@x@¿AAJ´àEiW¥áJ¤¤e±µà=ê¤Få±¶ =áˆY]€{áˆ% áˆc·à@¥X€@—å ±)ø @å±+câ ó ›”hv¼ï/GÌï?3ÿ$X¸àb bᨠbå ±¹ B@a¨ÀBñ@x@ßA$A¨ºm`D Eï¦a¨ëc¥¦bó» =ájÓ€=ájå ±¼à@rÑ€@å,±½àJÞРJ ÛÅ(Âûå±¾ B@aRàBüy@x@ÇA AR¿àEý‰§aRàˆä F§§aRÀ =áR ‘€=áR"½Áà@õŒ€@ë c3ak\ ñ@ñc@Ö"ïñnà@.D©Âàba bàáÁjå±Ã B@ àB‡å±@x@ßA$AjÄàE„E¨ajå2á j¨¨ajÅ =ÀƒüH€=ájå ±Æà@Y @á å±ÇàBÀG€ƒà Bå±È B@JÀB„ ¨!Á,¯ÿÿ,@x@¿AAJÉàE ©aJàˆÿÿ D38 J ©©e±Êà=‹ƒ|ÿÿ Ë =@O€Á€{è¥% å±Ìà@G€@ë câ óå±äÜBóÍàb¦á Áe!¨ £!@W­«cÎ B@`æÀB¤â ó!¨ ÿ‹@x@ßA$A¨ÏàE‘¼€ëᨪªbóÐ =áj<Á€=ájå ±Ñà@¿€@å,±ÒàJ ÀJ þâ¸å±Ó B@aRàBzöÇ@x@ÇA ARÔàExªâû««aRÕ =áRÁ€=áR%±Öà@{€@ ÙãÀ$F!“èo.!Cþ} "äÿ mà´" 8àÈB½×àbˆz€bá j屨 B@jÀBÿÿ¦× 5@ßA$AjÙ @`EŸ3«aj屬¬ajÚ = ?`=#7€=ájå ±Ûà@~6€@à~ÃÅ!j @ãÅ.`AJÜàBß5àB$àB±Ý B@€ÆÀB‹@Ùáð$å±@x@¿AAJÞàE%áÓå ±­­e±ßà=-½ƒÇôüyà =@®€Áõ€{ç:% €@3bóáà@_ð€@ÿÿ,â óå±»:A¨âàbÁá Áë!¨ £á륱ã B@`æÀBëc@x@ßA$A¨äàE¬ª¬bóëc®¯bóå =A6`ƒÞj­`=âó$ájKæà@?i€@å,±çàJ«h J â¸å±è B@€ÎÀBå±@x@ÇA ARéàE¹!®aàˆâ û°°aRê =áRk)€=áR%±ëà@Æ$€@å ±Sï´ó"Ñœƒ(àÿå±L'B½ìàb1 bàá½å±í B@jÀBÿÿWš@x@ßA$AjîàE@Ý€ëàˆá j±±ajï =áj¼à€=ä(%-â½1ðà@ @å$±ñàBx߀ƒàÁÁJ$ ƒäŸÿ ò B@JÀBñ@x@¿AAJóàEǘ¯âµ²²e±ôà=H«„Fÿÿ]Lõ =ሷž€{áˆ% áˆ"óöà@š€@å ±': @1a0# (¥â óå±èÕAë÷àbc™€bᨠbá¨ÿF„ø B@a¨ÀBå±@x@ßA$A¨ùàENT°a¨âtÿí ³³bóú =ájöX€=ájå ±ûà@VW€@å,±üàJÂV JA#å±ý B@aRàBöÇ@x@ÇA ARþàEÔ±aRàˆá R´´aRÿ =áR|€=å"±nà@Ü€@ÿÿ¦Ocƒ)àðàÔ"uð ¤$ RàÍÿÝVàbHÀb@&ájå± B@jÀBÿÿ¦@x@ßA$AjàE[Ë ë $Àˆá jµµaj =ájë΀=â½%-ä(%±à@I @ã<Ån$( @ãÅ(®@BàB§Í€ƒàÁÀB àB¥± B@JÀBå±@x@¿AAJàE↲âµ¶¶e± à=ê¤Föëc =áˆÎŒ€{áˆ% áˆ%± à@ˆ€@“î Wö Çâ óå±iÿã> àb~‡€bᨠ£á륱 B@a¨ÀBÿÿ¦@x@ßA$A¨àEhB³a¨ëc·¸bó =ájˆ´`=ájå ±à@ë€@å,±àJWÀJA#äF `‹áR¿ÿ/¼ B@aRàBy àBÿÿô@x@ÇA ARàEv¹ Ó DÀEä F¹¹aR =áR-Á€=áR"½à@м€@ü ysÿåð4p%­¤$fùt‚ ·à[Dfàbö»€bàáÄfå± B@jÀBüy@x@ßA$AjàEýtµbàˆá jººaj =áj}x€=áj%pä(%±à@Úw€@à~Áå±àB9àBå± B@JÀBå±@x@¿AAJàEƒ0¶áJ»»e±à=C„Füy =áˆx6€{è¥% áˆ"ó à@À1€@ÿÿôâ óå±ÿíAë!àb bá ë!¨ £ä^¥±" B@a¨ÀBÿÿ,@x@ßA$A¨#àE ì€ëå±¼½bó$ = ƒ:¬·aæájå ±%à@ª€@å,±&àJ ÀJ Å(Ã>å±' B@aRàBÿÿ5n@x@ÇA AR(àEc¸aéyä F¾¾aR) =áRËj€=H@—æ5á R"½*à@ f€@ü yƒ5ðú{"¯9ïÿ­8í1@zÀ\B½+àbŒe€bàáŸÿûå±,n€SjàB àæç@x@ßA$Aj-àEž¹@ ?ÿÿ“¿¿aj. = /€ƒ""€=ájå ±/à@€!€@â rå±0àBâ àB$ ƒä‘1 B@€ÆàB áJ!KöÇ@x@¿AAJ2àE%Úà ˆâ µÀÀe±3à=¦¤Fÿÿ§V4 =@®€Áà€{áˆ% å±5à@aÛ€@ÿ ÿ›òë câ óå±øÿ 6àbÁá Âó!¨ £á¨‘7 B@`æÀBüy@x@ßA$A¨8àE¬•ºbóâtá ¨ÁÁbó9 =ájSš€=å"±:à@´˜€@â m!j%ò`ò# Ã! !% … DF;àJ ÀJ â¸å±< B@aRàBöÇ@x@ÇA AR=àE2Q»áRÂÂaR> = …€ƒÜX€=áR%±?à@7T€@ö Ç“Ïð£ïð"ä1ð àý Œàlÿ5n@àb£S€bå$±A B@€æÀBüy@x@ßA$AjBàE¹ ¼ájÃÃajC =áj=€=ä(%-ä(CDà@™€@á å±EàBù€Bâ µ$ ƒä…±F B@JÀBëc@x@¿C =AJG @€E@Èà ˆä ÄÄe±Hà=H–ƒÇüyI = }€{0΀{áˆ% áˆ"óJà@zÉ€@ö Çâ óå±ȤAëKàbÜá å±L B@`æÀBöÇ@x@ßA$A¨MàEǃ½bó¼ Z¤çá¨%" 8ÅÆbóN =ájôC¾`=áj%páj3ÊOà@UB€@å,±PàJÁA J ®ÀÉÁRå±Q B@aRàB{@ó¡ ÀBñ@x@ÇA ARRàEÔúà ˆáRA@ÇÇaRS =@€ƒ‚¿aRå"±Tà@Üý€~å ±£T<¯‚þñ°ñc@ï"ƒ: à“JB½UàbH bá jëcV B@€æÀBüy@x@ßA$AjWàE[¶,àˆá jÈÈajX =ájß¹€=â½%-â½%±Yà@; @å$±ZàB›¸€ƒàÁÂrå±[ B@JÀBå±@x@¿AAJ\àEâqÀbwàËá JÉÉe±]à=c„„Fÿÿ$X^ =áˆÒw€{áˆå ±_à@s€@ñ )ø @üyÃâ óå±7¸Aë`àb~r€báᨠbçZ‹ca B@a¨ÀBå±@x@ßA$A¨bàEh-Áa¨å±ÊËbóc =áj’í€=ájâó%±dà@óë€@å,±eàJ_ÀJA#ånå±f B@aRàBå±@x@ÇA ARgàEv¤ÂaRàˆâ ûÌÌaRh =áR¬€=å"±ià@n§€@”ù …)ø!“â½3Ó³îKc íñnþ¿ªa c=B½jàbÚ¦€bàáñkn€RjÀB‰ñ@x@ßA$AjlàEü_ÃájÍÍajm =ájd€=ájå ±nà@mc€@ã Å!j @ãÅ"üAJoàBÍbàBå±p B@JÀBÿÿÞ@xn 9AAJq @€EƒÄáJÎÎe±rà=.ƒÇ4(ìã|ÿÿ$XB‘ As =áˆ{!€{äF% ç:+ctà@¾€@ÿ ÿWš!ˆ @âóå±ÇÏA¨uàb bâ ó!¨ bá륱v B@abÀBñ@x@ßA$A¨wàE ×€ëëcÏÐbóx =áj(—Åaæáj$áj"´yà@‰•€@å,±zàJõ” JA#â4„Få±{n@-RàBå±@x@ÇA AR|àENÆaàˆå ±ÑÑaR} =áRÁU€=áR"½~à@#Q€@ü yé{Àƒ:àþ£àªø!àË„B½àbP€bàáÁjëc€ B@b½àBÿÿt@x@ßA$AjàEž ÇájÒÒaj‚ =áj& €=áj%pâ½+cƒà@‚ €@á $( @ãÅ"ü@B„àBâ àB$àB±… B@JÀBëc@x@¿AAJ†àE%Åà ˆâ µÓÓe±‡à=¦¤Fÿÿ$Xˆ =áˆË€{áˆ% áˆ"ó‰à@\Æ€@ë câ óå±ovA¨ŠàbÁá Âó!¨ £á륱‹ B@a¨ÀBÿÿ,@x@ßA$A¨ŒàE¬€Èbóè&á ¨ÔÔbó =áj9…€=è¥å ±Žà@œƒ€@å,±àJ JàÉÁRå± B@aRàBüy@x@ÇA AR‘àE2<ÉaRãÇá RÕÕaR’ =áRîC€=áR"½“à@F?€@ñ Ó¬}Ð~år a úB½”àb²>€bàáÁj展 B@jÀBƒëc@x@ßA$Aj–àE¹÷à ˆá jÖÖaj— =ájIû€=ä(%-ä(%±˜à@£ú€@á å±™àBàB$ ƒä–Çš B@JÀBå±@x@¿AAJ›àE@³Êâµ××e±œà=HƒÇòÿÿÞ = ÀÁ4¹€{áˆ% áˆ"óžà@|´€@ö Çâ óå±^îAëŸàbÜá Âó!¨ £á¨ÿ   B@`æÀB â ó>"ÿÿô@x@ßA$A¨¡àEÆnË@ /ñØÙbó¢ =ájï.Ì`=ä^(f£à@Q-€@å,±¤àJ½, J Íë c¥ B@aRàBÿÿÞ@x@ÇA AR¦àEÔåà ˆä FÚÚaR§ =áR„í€=áR"½¨à@äè€@ë cãáP°Srý÷àùô`a 9ÿWš©àbP bá j屪 B@jÀBÿÿ @x@ßA$Aj«àE[¡Íbç¥á jÛÛaj¬ =áj㤀=ä(%-ä(%±­à@= @à~ÃÅå±®àBŸ£€ƒá J$ ƒä…±¯ B@JÀBå±@x@¿AAJ°àEá\ÎaJàˆá JÜÜe±±à=bo„Fzÿÿ/¼² =áˆÎb€{âó% áˆ"ó³à@^€@ÿ ÿ/¼â óå±”Aë´àb}]€báÁë!¨ £á¨…±µ B@a¨ÀBëc@x@ßA$A¨¶àEhÏa¨öÇÝÞbó· =áj¡Ø€=ájë c¸à@×€@ˆ@SðÊ`@ÿÿ,¹àJoÖ JA#àJëcº B@aRàBy3ŒàBüy¹ 5¸ A AR» @`EuÐaRÿÿ-ßßaR¼ =@þ`=*—€=áR"½½à@†’€@å ±óPˆ£à$`^`{$`@ýàRŽB½¾àbò‘€bä ©å±¿ B@€æÀBÿÿ @x@ßA$AjÀàEüJÑajáóä fààajÁ =áj„N€=å"±Âà@ßM€@á å±ÃàB@ BàÁb¿$å ±Än`JÀBå±@x@¿AAJÅàEƒÒáJááe±Æà=ƒ|å±Ç = Ä€Ás €{áˆ% å±Èà@À€@—W)a;áE!ˆ @öÇâ óå±ö|BóÉàb bᨠbå ±Ê B@`æÀBüy@x@ßA$A¨ËàE €ëöÇâãbóÌ = …`ƒ"‚Óaæájå ±Íà@„€€@ˆ@Sájå±ÎàJð J àJå±Ï B@€ÎÀBÿÿÞ@x@ÇA ARÐàE9Ôaàˆä FääaRÑ =@þ€ƒÄ@€=å"±Òà@<€@å± P°úàÿ+]ïp‚aa ’å*[Óàb‹;€bàáÄfå±Ô B@€æÀBÿÿÞ@x@ßA$AjÕàEžôà ˆá jååajÖ =ájø€=â½%-ä(!×à@y÷€@‡ â½$( @ãÅ.`@BØàBÞö€Bá Jå±Ù B@JàB@Ùà…/lÿÿ @x@¿AAJÚàE%°ÕbµàËá Jææe±Ûà=¦¤Fÿÿ* Ü =@®€Á¶€{áˆ% áˆ(¥Ýà@b±€@—å ±$F @å±Ãâ óå±6A¨ÞàbÁá Âó!¨ báë«cß B@`æÀBå±@x@ßA$A¨ààE«kÖa¨ëcçèbóá =ájØ+×`=ájå ±âà@:*€@â m!j @ëcãàJ¦) J â¸å±ä B@aRàBå±@x@ÇA ARåàE¹âà ˆâ ûééaRæ =áRmê€=áR"½çà@Éå€@å ±ïð€ï%ªðäût~aÀ-B½èàb5 bá jå±é B@jÀBå±@x@ßA$AjêàE?žØbàˆá jêêajë =ájÄ¡€=ç%pä(%±ìà@ @à~ÃÅå±íàB€ €ƒå(ÀB$ ƒä‘î B@JÀBÿÿ,@x@¿AAJïàEÆYÙáJëëe±ðà=Gl„FÿÿÞñ = ÀÁ¶_€{âó% áˆ"óòà@[€@—å ±!ˆ @å±(¥â óå±4ßAëóàbbZ€bá ë!¨ bᨑô B@`æÀBå±@x@ßA$A¨õàEMÚa¨ëcìíbóö =ájvÕ€=âó%páj(f÷à@ØÓ€@å,±øàJDÀJA#å±ù B@aRàBñ@x@ÇA ARúàEZŒÛaRàˆä FîîaRû =áR”€=â½áR"½üà@j€@üyP#¨€Ã3Î3ÎØùÿùa î B½ýàbÖŽ€bàáÄfå±þ B@jÀB…ù …ÿÿô@x@ßA$AjÿàEáGÜájïïajo ] €ƒqK€=áj%-â½%±à@ÌJ€@á $( @ãÅ"ü@BàB-àB$àB¥± B@€ÆÀBŒ@Ùâø!Këc 5@¿AAJ @€EhÝáJððe±à=é¤Få± =@®€{` €{áˆ% áˆ"óà@¥€@ÿÿôâ óå±JûA¨àb bá ë!¨ £á륱 B@`æÀBÿÿÞ@x@ßA$A¨ àEï¾€ëå±ñòbó = …`ƒÞaæå"± à@y}€@å,± àJå| J â¸ëc B@€ÎÀBëc@x@ÇA ARàEü5ßaàˆå ±óóaR =áR©=€=â½å ±à@9€@ë c3àýï]`ë"[àp a ÆB½àbp8€bàáå± B@jÀBÿÿB@x@ßA$AjàEƒñà ˆá jôôaj =áj õ€=áj%-ä(%±à@fô€@á å±àBÇóàB$ ƒä‹c B@JÀBå±@x@¿AAJàE ­àâµõõe±à=‹¿„FöÇ =áˆò²€{äF% áˆ%±à@?®€@ü y!ˆ @ëcâ óå±¼4Aëàb¥á Âó!¨ bᨋc B@a¨ÀB”è ¥'Zÿÿ,@x@ßA$A¨àEháa¨ñö÷bó =ájž(â`=ájä^(f!à@'€@å,±"àJo& J èjå±# B@aRàBv[{àBüy@x@ÇA AR$àEžß€ˆîèä FøøaR% =áRSç€=áR"½&à@®â€@å ± C[tð¯&A€%¯[äàæB½'àb bá jëc( B@jÀBñ@x@ßA$Aj)àE$›ãbàˆá jùùaj* =áj°ž€=å"±+à@  @áéÃÅ$(ån"üAJ,àBl€ƒàÁÀB$àB¥±- B@JÀBå±@x@¿AAJ.àE«VäáJúúe±/à=,i„Fëc0 =ሗ\€{áˆ% å±1à@åW€@å ±5\ @å±+câ ó屬A¨2àbG bᨠbå ±3 B@a¨ÀBÿÿ,@x@ßA$A¨4àE2åa¨ÿÿôûübó5 =ájhÒ€=ä^"´6à@ÉЀ@‡@Så#!j%òöÇ7àJ5ÀJA#àJå±8 B@aRàBëc@x@ÇA AR9àE?‰æaRÿÿ,ýýaR: =@þ€ƒí€=G@—àÆaâ½;à@GŒ€@ñ  Sð ë´Æ"Cr6ÇU+@zÀö_B½<àb³‹€bâ ½å±= B@€æÀBëc@x@ßA$Aj>àEÆDçájþþaj? =ájJH€=ä(%-áj+c@à@¦G€@â rå±AàB  Bá Jå±B B@JàB@Ùâ1)ºñ@x@¿AAJCàEMèáJÿÿe±Dà=ΤFå±E =@®€ÁA€{áˆ% áˆ(¥Fà@‹€@—öÇâ óå±R­AëGàbí¡á¨ £å ±H B@`æÀBöÇ@x@ßA$A¨IàEÓ»€ëëcAJ =ájý{éaæáj%páj%±Kà@^z€@â må±LàJÊy JA#â¸å±M B@aRàBÿÿô@x@ÇA ARNàEá2êaàˆé øaRO =áR:€=å"±Pà@é5€@å ±c0ÿ0à%«¸D%¾a uB½QàbU bàáÊå±R B@jÀBå±@x@ßA$AjSàEhî€ëå2á j B½T =ájäñ€=â½å ±Uà@D @à~Áå±VàB¤ð€ƒá Jå±W B@JàB@Ùâøå±@x@¿AAJXàEî©ëâµ@=Yà={¼„Fpÿÿ Z =@®€Á߯€{âó% äF%±[à@)«€@›å ±$F @ëcâ óå±9‡Bó\àbŠª€bᨠbå ±] B@`æÀBå±@x@ßA$Aj^àEueìa¨å±bó_ =áj«%í`=âóå ±`à@ $€@â m!j @ëcaàJx# J â¸å±b B@aRàBå±@x@ÇA ARcàE‚Üà ˆä FaRd =áR,ä€=áR"½eà@߀@ˆâ ½$F!Râ½ëc¶ B@aRàByÿÿÞ@x@ÇA AR·àE‚Ç Ó@Y¤ä FaR¸ = +€ƒ?Ï€=å"±¹à@–Ê€@ë c³,àý$U¿ÿ¸kï"ïz Œàñ±D©ºàb bàáÄfå±» B@€æÀBÿÿ…*@x@ßA$Aj¼àE ƒübàˆá jaj½ =áj•†€=â½ä(%±¾à@ð…€@â r屿àBQ BàÁÁJå±À B@JÀBå±@x@¿AAJÁàE>ýáJe±Âà=Q„Fÿÿ à =ሄD€{áˆ% å±Äà@Í?€@ü y!ˆ @å±+câ óå±UCAëÅàb, bᨠbå ±Æ B@a¨ÀBëc@x@ßA$A¨ÇàEú€ëñbóÈ =áj?ºþaæájå ±Éà@¡¸€@å,±ÊàJ ÀJ ¥(Âûå±Ë B@aRàBzå±@x@ÇA ARÌàE$qÿaÿÿÞaRÍ =áRÕx€=å"±Îà@0t€@ë cÃl`zn{t+ûä:úa Y1B½Ïàbœs€bá jå±Ðo€RjÀBÿÿ @x@ßA$AjÑàE«,A å2å ±  ajÒ =áj+0€=â½å ±Óà@†/€@à~Âr$( @ãÅ.`AJÔàBë.€Bá Jå±Õ B@JàB@Ùâø,¯9/@'@x@¿AAJÖàE1èà ˆá J!!e±×à=²¤FÿÿÞØ =@®@™*î€{âó% å±Ùà@oé€@ÿ ÿnbë câ óå±ýÃA¨ÚàbÍá Áë!¨ £áë«cÛ B@`æÀBÿÿô@x@ßA$A¨ÜàE¸£bóå±"#bóÝ =ájåc`=ájå ±Þà@Gb€@â m!j @ÿÿ,ßàJ³a JA#â¸å±à B@aRàBëc@x@ÇA ARáàEÅaàˆâ û$$aRâ =áR^"€=äF!â½ãà@º€@å ±Ó‹‚õƒàµŠ{‹|ô`ßa #JB½äàb& bä ©å±å B@jÀBå±@x@ßA$AjæàELÖ€ëáj%%ajç =ájØÙ€=áj%páj+cèà@7 @à~ÃÅå±éàB˜Ø€ƒà B$ ƒä‘ê B@JàB@Ùâøå±@x@¿AAJëàEÓ‘âµ&&e±ìà=T¤„Få±í =@®€Á×€{áˆ% áˆ4 îà@“€@ü y!ˆ @ëcâ ó屇ÿL6ïàbo’€bᨠbᨑð B@`æÀBüy@x@ßA$A¨ñàEZMá¨''bóò =ájR€=å"±óà@bP€@â må±ôàJÎO JA#â¸å±õ B@aRàB{ëc@x@ÇA ARöàEàáR((aR÷ =áR€=áR"½øà@í €@‹ÿ ÿê%òâ½ ãè"Ò‘9Ò6Ã1ïða D©ùàbX bá jå±ú B@jÀBÿÿŠÜ@x@ßA$AjûàEgÄ€ëàˆç ))ajü =ájßÇ€=ä(%-ä(%±ýà@< @æ ¹$( @ãÅ«cþàBŸÆ€ƒàÁÅôå±ÿ B@JàB@Ùâøå±@x@¿AAJp  €Eîâµ**e±à=ö¤FñöÇ =@®€{Ú…€{áˆ% áˆ"óà@)€@”â ó$F @âó6Çâ óå±)AëàbŠ€€bᨠbá륱 B@`æÀBöÇ@x@ßA$A¨àEu;a¨öÇ+,bó =áj¤û€=ä^"´à@ú€@â m!j$^ëc àJoù JA#â¸å± B@aRàB|å±@x@ÇA AR àE‚² aRàˆä F--aR =áR5º€=áR"½ à@–µ€@ÿ ÿÞóGïp%¬ð àÿÿQèwB½àbÀbA;ájå± B@jÀBzüy@x@ßA$AjàE n áj..aj =áj…q€=ä(%pä(%±à@æp€@å$±àBE Bá Jå± B@JàB@Ùáå±@x@¿AAJàE) áJ//e±à=<ƒ|ÿÿÞ =@®€Á|/€{áˆ% áˆ"óà@Ë*€@å ±': @å±6Çâ óå±ÓàAëàb+ bᨠbå ± B@`æÀBÿÿàJ@x@ßA$A¨àEå€ëñ01bó =ájM¥ aæájáj%±à@­£€@â må±àJÀJA#â¸å± B@aRàBå±@x@ÇA AR àE$\ aãÇå ±22aR! =áR×c€=å"±"à@0_€@üy Q““ÿ"í­+d21/àœ3B½#àbœ^€bá jå±$ B@jÀB‚å±@x@ßA$Aj%àEªajàˆá j33aj& =ájB€=â½å ±'à@ €@áéË $( @ãÅ"üAJ(àBþ€BàÁÀBå±) B@JÀBŒ âøå±@x@¿AAJ*àE1Óà ˆá J44e±+à=²¤FÿÿÞ, =@®€ÁÙ€{áˆ% å±-à@iÔ€@ü y$F @å±Ãâ óå±8ÁA¨.àbÍá ᨠbáë«c/ B@`æÀBëc@x@ßA$A¨0àE¸Žâó55bó1 =ájf“€=ä^"´2à@È‘€@å,±3àJ4ÀJA#⸠f=áR¿ÿt4 B@aRàBöÇ@x@ÇA AR5àE?JaRãÇâ û66aR6 =áRãQ€=â½ç(o7à@?M€@öÇQÓ” @t ð‘aÃa œ»B½8àb«L€bàá½å±9 B@jÀBöÇ@x@ßA$Aj:àEÅajáóá j77aj; =áj= €=áj%pä(+c<à@œ€@à~Áå±=àBýàBå±> B@JàB@Ùâøå±@x@¿AAJ?àELÁà ˆá J88e±@à=TƒÇÿÿh°A =@®€Á<Ç€{áˆ% áˆ"óBà@ŠÂ€@—ñ !ˆ @å±â óå±IAëCàbèá Áë!¨ bä^¥±D B@`æÀBöÇ@x@ßA$A¨EàEÓ|bóñ9:bóF =áj=`=å"±Gà@j;€@‡@Sâm'$ŸñHàJÖ: JA#àJå±I B@aRàBå±@x@ÇA ARJàEàó€ˆãÇâ û;;aRK =@þ€ƒ}û€=â½!â½Là@Øö€@ë c#ƒ2/¶ýÒ‘"îð£ïa ÑÅB½MàbD bàá½å±N B@€æÀBÿÿ$X@x@ßA$AjOàEg¯bàˆá j<á J==e±Uà=o}„FüyV =áˆÚp€{áˆ% âóWà@#l€@ÿ ÿB$F @å±+câ óå±u7A¨XàbŠk€báÁë!¨ bá륱Y B@a¨ÀBëc@x@ßA$A¨ZàEt&a¨ÿÿ/¼>?bó[ =ájžæ€=å"±\à@ÿä€@è !j$Ÿå±]àJkÀJA#å±^ B@aRàBzöÇ@x@ÇA AR_àE‚aRàˆâ û@@aR` =áRC¥€=áR(oaà@Ž €@â ½$F!Râ½(o3ð£íð"äÿD«ïTýýäÿàÅÿbþbàbúŸ€bàá½å±c B@jÀBñ@x@ßA$AjdàE YajãFá jAAaje =áj}\€=áj%-ä(+cfà@Ø[€@à~Á!j @ãÅ¥±gàB9àB$àB¥±h B@JÀBÿÿô@x@¿AAJiàEaJáÓá JBBe±jà='ƒÇå±k =ሀ{å"±là@É€@¬ëcâ óå±avAëmàb+ báÁë!¨å ±n B@a¨ÀB¬â ó- ÿÿô@x@ßA$A¨oàEЀëöÇCDbóp =áj<aæâó$âó"´qà@Ž€@å,±ràJ ÀJ þå ±s B@aRàBå±@x@ÇA ARtàE#Gaàˆâ ûEEaRu =áRÙN€=áR%±và@4J€@ñ  CïÒ„"Â1`1a •ÚB½wàb I€bä ©å±x B@jÀBñ@x@ßA$AjyàEªájFFajz =Àƒ€=ä(â½%±{à@z€@á å±|àBÚ€Bâ,ä ƒåô¥±} B@JÀBëc@x@¿AAJ~àE1¾à ˆâ µGGe±à=²¤Fw3(ìé.€ ßÿ²ºEO !²€ =áˆ!Ä€{áˆáˆ"óà@k¿€@ÿ ÿÞ)ø @è¥+câ óå±1(Aë‚àbÍá ᨠb᨟ÿÞƒ B@a¨ÀBñ@x@ßA$A¨„àE¸y!@ñHIbó… =ájá9`=ájå ±†à@B8€@å,±‡àJ®7 J Íâ4b4ñˆ B@aRàBÿÿÞ@x@ÇA AR‰àEÅðà ˆâ ûJJaRŠ =áR{ø€=å"±‹à@Õó€@ñ SÙÒ‘ÿÛFàpëcPB½ŒàbA bá jå± B@jÀBñ@x@ßA$AjŽàEL¬bàˆá jKKaj =áj̯€=â½%-èo%±à@( @å$±‘àBˆ®€ƒàÁµ)º ƒäèF’ B@JÀBå±@x@¿AAJ“àEÒg áJLLe±”à=Tz„Fëc• =áˆËm€{áˆ% áˆ%±–à@ i€@ÿ ÿB': @å±.Wâ óå±ôoAë—àboh€bá ë!¨ bᨅ±˜ B@a¨ÀBÿÿB@x@ßA$A¨™àEY#!a¨ëcMNbóš =áj‹ã€=ájå ±›à@ìá€@å,±œàJXÀJA#e§âûå± B@aRàBå±@x@ÇA ARžàEgš"aRàˆä FOOaRŸ =áR ¢€=å"± à@o€@ÿÿÞQc"1’ÀÀxÑ|}ÐÐà!B½¡àbÛœ b@(ájå±¢ B@jÀBÿÿ5n@x@ßA$Aj£àEíU#ájPPaj¤ =ájvY€=áj%-ä(%±¥à@ÒX€@ã Å$( @ãÅ.`@B¦àB1 Bá J$àBi…±§ B@JÀBå±@x@¿AAJ¨àEt$áJQQe±©à=õ¤FÿÿÞª =áˆ\€{äF% áˆ%±«à@¯€@ÿÿ$Xâ óå±…ìA¨¬àb bᨠ£á륱­ B@a¨ÀBÿÿ5n@x@ßA$A¨®àEûÌ€ëüyRSbó¯ =áj%aæájå ±°à@v‹€@å,±±àJ⊀Jã >å±² B@aRàBå±@x@ÇA AR³àED&aáÛå ±TTaR´ =áR²K€=áR"½µà@ G€@”NƒåN$F!“èos1€1‡@äõxÒ¯"~a ì†B½¶àbxF€bàáÊå±· B@jÀB‚ó Óÿÿ* @x@ßA$Aj¸àEÿà ˆá jUUaj¹ =áj'a¨áj%-ä(%±ºà@{€@á !j @ãÅ¥±»àBÛàBå±¼ B@JÀBå±@x@¿AAJ½àE»à ˆá JVVe±¾à=—Í„F屿 =ሠÁ€{áˆ% áˆ"óÀà@M¼€@ë câ óå±HtAëÁàb²á Âó!¨ £á륱 B@a¨ÀBÿÿ$X@x@ßA$A¨ÃàEœv(bµëcWXbóÄ =áj³6)`=è¥$áj"´Åà@5€@å,±ÆàJ4 J ä©å±Ç B@aRàBå±@x@ÇA ARÈàEªíà ˆâ ûYYaRÉ =áRPõ€=â½áR"½Êà@²ð€@ë cƒäõyxåxÔ "Âa yÛB½ËàbÀb@$ájå±Ì B@jÀBÿÿB@x@ßA$AjÍàE1©*bç¥á jZZajÎ =áj©¬€=áj%pâ½%±Ïà@ @á å±ÐàBe«€ƒà B$ ƒä‹cÑ B@JÀBå±@x@¿AAJÒàE·d+aJàˆá J[[e±Óà=8w„Fÿÿ Ô =ሤj€{äF% áˆ"óÕà@òe€@è ¥ö Çâ óå±Ý)AëÖàbS báå±× B@a¨ÀBÿÿ,@x@ßA$A¨ØàE> ,a¨å±\]bóÙ =ájià€=ájä^%±Úà@ÉÞ€@å,±ÛàJ5ÀJA#äFå±Ü B@aRàBå±@x@ÇA >ÝàEL—-aRäF^^aRÞ =áRþž€=áR"½ßà@\š€@ü y“¯åxuð¤$ùt€5ðú{a  ÐB½ààbÈ™€bàá½å±á B@jÀB}ëc@x@ßA$AjâàEÒR.ajÿÿ+u__ajã =áj^V€=å"±äà@»U€@á å±åàB Bâ µå±æ B@JàB@Ùâø,¯ÿÿ* @x@¿AAJçàEY/aJàˆå ±``e±èà=Ú¤Fëcé =@®€ÁM€{áˆå ±êà@˜€@ü y!ˆ @îWâ óå±"Bóëàbùá Âó!¨ bçZ–Çì B@`æÀBüy@x@ßA$A¨íàEàÉ€ëñabbóî =ájŠ0aæä^%±ïà@gˆ€@‡@Sâm'%òÿÿÞðàJÓ‡ JA#àJå±ñ B@aRàBå±@x@ÇA ARòàEí@1aâûccaRó =@þ€ƒ”H€=áR%±ôà@ñC€@å ±£"ƒ*tðƒGàÿÒa QB½õàb] bàá½å±ö B@€æÀBÿÿ$X@x@ßA$Aj÷àEtü€ëàˆä fddajø =áj2a¨ä(*ßéÚ+cùà@aÿ€~â r$( @ãÅ"ü@BúàBÀþ€BàÁÁJ$àBi‹cû B@JÀBëc@x@¿AAJüàEû· àËá Jeee±ýà=|Ê„Få±þ = ÀÁë½€{áˆ% áˆ"óÿà@6¹€@å ±$F @å±6Çâ óå±¹£A¨qࣗ¸€báᨠbá륱 B@`æÀBå±@x@ßA$A¨àEs3bµëcfgbó =áj´34`=áj%páj"´à@2€@è !j @å±àJ€1 JA#å± B@aRàBÿÿ¦@x@ÇA ARàEêà ˆâ ûhhaR = …€ƒ:ò€=å"± à@—í€@å ±³àopå" â4Q`G7 Œàÿô àb bá jå± B@€æÀBå±@x@ßA$Aj àE¦5bãFá jiiaj =áj¢©€=áj%-â½%±à@ÿ¨€@à~ÃÅå±àB^àBå± B@JÀB‹@Ùâøëc@x@¿AAJàEœa6aJáÓá Jjje±à=t„Fñ =@®€ÁŒg€{äF% áˆ%±à@Øb€@—ù » @å±?mâ óå±°ÿôàb8 báÁë!¨ bå ± B@`æÀBå±@x@ßA$A¨àE#7a¨å±klbó =ájFÝ€=áj%páj%±à@¦Û€@å,±àJÀJA#â¸ëc B@aRàB| àBÿÿh°@x@ÇA ARàE0”8aRàˆâ ûmmaR =áRÜ›€=äFáR"½à@5—€@ÿ ÿÞÀ+ƒEàuð„åðð£tBöàfD©àb –€bàá¿ÿ Iå± B@jÀBƒñ@x@ßA$Aj!àE·O9ájnnaj" = …€ƒGS€=â½å ±#à@¥R€@å$±$àB Bá J$ ƒä‹c% B@€ÆÀBëc@x@¿AAJ&àE> :áJooe±'à=¿¤Få±( =áˆ*€{áˆ% äF"ó)à@v €@ÿ ÿ$Xâ ó›ÿÿÏ4¤ÿŠÜ*àbÚá Âó!¨ £á¨‘+ B@a¨ÀBöÇ@x@ßA$A¨,àEÅÆ€ëñpqbó- =ájì†;aæájä^%±.à@O…€@å,±/àJ»„ JA#ånå±0 B@aRàBÿÿ¦@x@ÇA AR1àEÒ=à@¶€@å ±!ˆ @ëcâ óå±NCAë?àb|µ€bç !¨ bᨅ±@ B@a¨ÀB è ¥- ÿÿô@x@ßA$A¨AàEfp>a¨ëcuvbóB =ájŠ0?`=ê%±Cà@í.€@å,±DàJYÀJ þå ±E B@aRàBxëc@x@ÇA ARFàEtç Ó@Y¦:ä FwwaRG =áR+ï€=â½ç"½Hà@ˆê€@ñ ã+`Qþä6Î6Ì*@¸ÀœOB½Iàbôé€bàáÄ©å±J B@jÀBÿÿQè@x@ßA$AjKàEú¢@bàˆá jxxajL =áj¦€=â½%-ä(%±Mà@Ü¥€@à~Á$( @ãÅ"ü@BNàBBàB$àB¥±O B@JÀB„@Ù¢oÀ…$üy@x@¿AAJPàE^AáJyye±Qà=q„FvÿÿôR =@®€Áqd€{âó% áˆ"óSà@½_€@ö Çâ óå±8½A¨Tàb bá ë!¨ £á륱U B@`æÀBñ@x@ßA$A¨VàEBa¨ëcz{bóW =áj8Ú€=âóä^"´Xà@›Ø€@å,±YàJ Jç ëcZ B@aRàBå±@x@ÇA AR[àE‘CaRáÛä F||aR\ =áRŘ€=áR"½]à@”€@ë cóàÿö€ëç¥á j‚‚ajv =ájÆù€=ä(%-ä(%±wà@  @à~Á$( @ãÅ"ü@BxàB‚ø€ƒá J$àB¥±y B@JÀBñ@x@¿AAJzàEıHⵃƒ@={à=IÄ„Fëc| = ÀÁ±·€{âó% áˆ"ó}à@ý²€@ëcâ óå±¹8A¨~àb` bᨠ£á륱 B@`æÀBöÇ@x@ßA$Aj€àEKmIa¨å±„…bó = ƒx-J`=ájë c‚à@Ú+€@å,±ƒàJFÀJ å ±„ B@aRàBå±@x@ÇA AR…àEYä€ÓãÇä F††aR† =áRì€=áR"½‡à@]ç€@ëcRS"ï"~Ô}™ï"öÇÄÿIBˆàbÉæ€bàáÄ©ëc‰ B@jÀBëc@x@ßA$AjŠàEߟKbáóá j‡‡aj‹ =ájo£€=å"±Œà@Ê¢€@á å±àB+ BàÁÁJ屎 B@JÀBå±@x@¿AAJàEf[LáJˆˆe±à=ç¤Fÿÿ]L‘ =áˆ^a€{áˆ% å±’à@£\€@ÿÿôâ óå±`E±“àb bᨠ£å ±” B@a¨ÀBëc@x@ßA$A¨•àEíMᨉ‰bó– =áj”€=ájå ±—à@õ€@å,±˜àJaÀJA#ëc™ B@aRàBÿÿ @x@ÇA ARšàEsÒ Ó@Y ˆä FŠŠaR› =áR Ú€=áR%±œà@hÕ€@å ±#ƒTà€ü6ÁU ý+nàdìB½àbÔÔ€bä ©å±ž B@jÀBÿÿ* @x@ßA$AjŸàEúN⽋‹aj  =áj†‘€=áj*ßä(+c¡à@â€@áéÅnå±¢àBBàBå±£ B@JÀBå±@x@¿AAJ¤àEIOáJŒŒe±¥à=‰‚õÿÿQè¦ =áˆqO€{áˆ% áˆ"ó§à@¼J€@ü y/ª @ñ+câ óå±Àÿ'L¨àb bá ë!¨ bå ±© B@a¨ÀBöÇ@x@ßA$A¨ªàEPa¨öÇŽbó« =áj:Å€=ê.¬à@šÃ€@å,±­àJÀJA#¥(Ã>ñ® B@aRàBå±@x@ÇA AR¯àE|QaRàˆå ±aR° =áR˃€=â½ç"½±à@%€@å ±3ƒV%©þ%±W ÿå±åiD©²àb‘~€bàáÁjå±³ B@jÀBÿÿ$X@x@ßA$Aj´àEœ7Rájajµ =áj,;€=ájå ±¶à@ˆ:€@á å±·àBè9àB$ ƒä–Ǹ B@JÀBå±@x@¿AAJ¹àE#ó€ˆç…â µ‘‘e±ºà=¤¤FÿÿB» =áˆù€{áˆ% å±¼à@]ô€@ÿÿ> @å±+câ óå±A›Bó½àb¿á Âó!¨ bᨖǾ B@a¨ÀB ä¡>"ÿÿ,@x@ßA$A¨¿àE©®Sbóñ’“bóÀ =ájØnT`=å"±Áà@8m€@å,±ÂàJ¤l J å±Ã B@aRàBå±@x@ÇA ARÄàE·%Uaàˆâ û””aRÅ =áRe-€=áR%±Æà@»(€@ÿ ÿ!d$F%òâ½CƒXàÿ£ð1ûþƒZàDà-KB½Çàb' bàá½å±È B@jÀBÿÿ¡¤@x@ßA$AjÉàE=á€ëàˆá j••ajÊ =ájÎä€=áj%-ä(+cËà@* @à~Á!j @ãÅ(®@BÌàB‰ã€ƒà B$àB¥±Í B@JÀBå±@x@¿AAJÎàEÄœVâµ––e±Ïà=E¯„Få±Ð =ሼ¢€{å"±Ñà@ÿ€@ü yë câ ó屉¹A¨Òàb` bá¨å ±Ó B@a¨ÀBñ@x@ßA$A¨ÔàEKXWA@Tëc—˜bóÕ = 0`ƒzX`=âó$âó"´Öà@Ú€@å,±×àJFÀJ å ±Ø B@€ÎÀBå±@x@ÇA ARÙàEXÏ Ó@Y¦:ä F™™aRÚ =áR ×€=áR%±Ûà@hÒ€@ü ySðDð"ƒ-ëQÿ¶à@¸ÀdB½ÜàbÔÑ€bàáÄ©å±Ý B@jÀBÿÿô@x@ßA$AjÞàEߊYbàˆá jššajß =ájgŽ€=ä(%-â½%±àà@Ä€@ê—Áå±áàB# BàÁÀBå±â B@JÀBå±@x@¿AAJãàEfFZáJ››e±äà=ç¤Få±å =áˆVL€{áˆ% áˆ"óæà@¡G€@ü y!ˆ @è¥â óå±¼–Aëçàb bᨠbç«cè B@a¨ÀBñ@x@ßA$A¨éàEì[a¨ëcœbóê =áj€=ä^%±ëà@gÀ€@å,±ìàJÓ¿ JA#¥(Âûñí B@aRàB|ÿÿ @x@ÇA ARîàEúx\aRàˆä FžžaRï =áR­€€=áRcðà@|€@ÿÿ/¼ Rcƒtð·àƒu ¸àa CB½ñàbz{ b@$ájå±ò B@jÀBÿÿ/¼@x@ßA$AjóàE4]aj‹@Y ˆá jŸŸajô =áj 8€=ä(å ±õà@f7€@å$±öàBÅ6€Bá Jå±÷ B@JÀBå±@x@¿AAJøàEðà ˆá J  e±ùà=ˆƒ|ÿÿÞú =áˆðõ€{âó% å±ûà@Cñ€@“!¢¿ÿ…ë câ óå±çðBóüàb£á Àb!¨ £å ±ý B@a¨ÀB”ñ@x@ßA$A¨þàEŽ«^bó屡¢bóÿ =ájÉk_`=âóáj%±rà@)j€@å,±àJ•i J cáµå± B@aRàBå±@x@ÇA ARàEœ"`aãÇâ û££aR =áRK*€=â½áR%±à@¬%€@å ±s%±v%©¹%±wðuèÿÿ§VæB½àb bàá½å± B@jÀBå±@x@ßA$AjàE"Þ ë@Y£©á j¤¤aj =áj¢á€=â½å ± à@ @á )Ú @ãÅBü+¦ àB^à€ƒàÁÁJ$àB± B@JÀBå±@x@¿AAJ àE©™abµàˆá J¥¥e±à=*¬„Fÿÿô =!? ÁŸ€{áˆ% å±à@ @å ±!ˆ @ëcâ óå±BbA¨àbE báᨠbáë«c B@a¨ÀBÿ ÿTÜöÇ@x@ßA$A¨àE0Uba¨ñ¦§bó =ájWc`=ájä^"´à@·€@‡@S£rÁ' @ Í@ÿ ÿë®àJ#ÀJ@.àJå± B@aRàBå±@x@ÇA ARàE=Ì€ÓáÛâ û¨¨aR =@þ€ƒôÓ€=áR%±à@QÏ€@ñ ƒõõõöïÓ”@%1a N7B½àb½Î€bàáÁµå± B@€æÀBÿÿô@x@ßA$AjàEćdbàˆá j©©aj =ájL‹€=áj%-ä(%nà@ªŠ€@â r$( @ãÅ"ü@B àB BàÁÁJå±! B@JÀBå±@x@¿AAJ"àEKCeaJàËá Jªªe±#à=̤Få±$ =áˆ;I€{áˆ% áˆ"ó%à@‡D€@—ëcâ óå±êÅA¨&àbçá ᨠ£á륱' B@a¨ÀBñ@x@ßA$A¨(àEÑþ€ëëc«¬bó) =ájõ¾faæê"´*à@X½€@ë,c+àJļ JA#å±, B@aRàBÿÿô@x@ÇA AR-àEßugaàˆâ û­­aR. =áR¢}€=â½&Ãâ½/à@÷x€@ü y“àû£àú£àù‚uƒža 'ûB½0àbc bàá½å±1 B@jÀBÿÿ÷@x@ßA$Aj2àEf1hajäf®®aj3 =ájÚ4€=â½%páj%±4à@7 @á å±5àBž3€ƒàÁÁJå±6 B@JÀBƒ@Ùâø)ºÿÿ¦@x@¿AAJ7àEì쀈àËâ µ¯¯e±8à=mÿ„FÿÿB9 =@®€Áäò€{âó% áˆ%±:à@(î€@ å ±!ˆ @ëcâ ó屃 Aë;àbˆí€báᨠbå ±< B@`æÀBüy@x@ßA$A¨=àEs¨ibóå±°±bó> =áj‡hj`=âóä^%±?à@êf€@å,±@àJVÀJ â¸ëcA B@aRàBå±@x@ÇA ARBàE€kaàˆâ û²²aRC =áR4'€=â½áR"½Dà@"€@ÿ ÿÞ£õñtm/õ‚ä4ƒõƒàõó€a •žB½Eàbù!€bä få±F B@jÀBÿÿB@x@ßA$AjGàEÛ€ˆáj³³ajH =áj‹Þ€=ájå ±Ià@éÝ€@à~ÃÅ$( @ãÅ"üAJJàBKàBå±K B@JàB@Ùâøå±@x@¿AAJLàEŽ–lâµ´´e±Mà=©„FzöÇN =@®€Á‚œ€{áˆ% å±Oà@Ê—€@ÿ ÿÞ/ª @å±Ãâ óå±1lA¨Pàb* bá ë!¨ báë«cQ B@`æÀB öÇ@x@ßA$A¨RàERma¨ã|ä ^µµbóS =áj¼V€=å"±Tà@U€@â m!j$ŸñUàJ‰Tm`Éá Rå±V B@ àBÿÿbþ@x@ÇA ARWàE› nàˆá R¶¶aRX =áRI€=áR%±Yà@£€@ñ ³ÕÒè èýQöQöµàeó! à…HB½Zàb bá jå±[ B@jÀBÿÿB@x@ßA$Aj\àE"É€ëàˆá j··aj] =ájžÌ€=ä(%pä(+c^à@úË€@ã<å±_àB^ BàÁÀB$ ƒäÿ]L` B@JàB@Ùå±@x@¿AAJaàE©„o⵸¸e±bà=±¤Fðw „àƒÿÿbþ`µ gc = ) Á™Š€{áˆ% áˆ"ódà@ã…€@ÿ ÿB': @å±?mâ óå±!ºAëeàbE bᨠb᨟ÿ f B@a¨ÀBñ@x@ßA$A¨gàE/@p!@ëc¹ºbóh =ájUq`=áj%páj(fià@¶þ€~â må±jàJ"ÀJ Íâ4b>å±k B@aRàBÿÿÞ@x@ÇA ARlàE=·àˆä F»»aRm =áRÓ¾€=å"±nà@5º€@ˆî !!“â½Ãá JÇÇe±¡à=”„Få±¢ =áˆr‡€{áˆ% áˆ"ó£à@Ä‚€@”î Wë câ óå±2ÕAë¤àb* bá ë!¨ £á륱¥ B@a¨ÀBÿÿÞ@x@ßA$A¨¦àE={a¨àˆá ¨ÈÈbó§ =áj¶A€=å"±¨à@@€@ë,c©àJ„? JA#äFëcª B@aRàBÿÿ/¼@x@ÇA AR«àE›øà ˆá RÉÉaR¬ =áRO|aáR"½­à@Ÿû€~ÿÿ%òèoóóð"¯ó€"ð£êÿÿ; ]ÿ¾®àb  bá j屯 B@jÀB‹î !ÿÿh°@x@ßA$Aj°àE"´,àˆá jÊÊaj± =ájª·€=ä(%pä(%±²à@ @äåË å±³àBf¶€ƒàÁÀB$ ƒä–Ç´ B@JÀBå±@x@¿AAJµàE©o}bwàËá JËËe±¶à=°¤FóöÇ· =ሩu€{áˆ% áˆ"ó¸à@åp€@ÿÿxâ óå±ùÑAë¹àbE báᨠ£á¨–Ǻ B@a¨ÀBÿÿx@x@ßA$A¨»àE/+~a¨ÿÿ,ÌÍbó¼ =áj`ë€=ä^(f½à@Âé€@å,±¾àJ.ÀJA#屿 B@aRàBå±@x@ÇA ARÀàE=¢aRàˆâ ûÎÎaRÁ =Àƒõ©€=áR"½Âà@U¥€@ëcS£éð"ðO äƒ9ð  Œà-mB½ÃàbÁ¤ b@&ájå±Ä B@jÀBÿÿ; @x@ßA$AjÅàEÃ]€ájÏÏajÆ =ájPa€=áj%-ä(%±Çà@ª`€@ã Åå±ÈàB àBå±É B@JÀBÿÿWš@x@¿AAJÊàEJáJÐÐe±Ëà=ˤFÿÿ,Ì =áˆ6€{å"±Íà@ƒ€@—ëcâ ó屉ŸAëÎàbæá å±Ï B@a¨ÀBÿÿ; @x@ßA$A¨ÐàEÑÔ€ëñÑÒbóÑ =ájù”‚aæáj)Ïâó%±Òà@\“€@å,±ÓàJÈ’ JA#äFëcÔ B@aRàBå±@x@ÇA ARÕàEÞKƒaàˆå ±ÓÓaRÖ =áR“S€=äFáR%±×à@æN€@î !$F @ëcSàýÔ@aé~ƒ)OöÇQÿ* ØàbR bàá½å±Ù B@jàBëc@x@ßA$AjÚàEe„ájÔÔajÛ =ájí €=â½å ±Üà@H @å$±ÝàB© €ƒá Jå±Þ B@JÀBëc@x@¿AAJßàEìÂà ˆâ µÕÕe±àà=mÕ„Få±á =áˆäÈ€{áˆ*¿äF"óâà@)Ä€@ÿÿ; â óå±õÿ* ãàbˆÃ€bᨠ£ë cä B@a¨ÀBÿÿ @x@ßA$A¨åàEs~…bóâtá ¨ÖÖbóæ =áj ƒ€=ä^%±çà@k€@å,±èàJ×€€JàÉÄFå±é B@aRàBöÇ@x@ÇA ARêàEù9†aRáÛá R××aRë =áRŸA€=â½%pâ½ìà@ù<€@ÿÿ S#ƒ0àÿ+[‘:õƒï+da µpHoíàbe bàáÁjå±î B@jÀBÿÿ5n@x@ßA$AjïàE€õ€ëàˆá jØØajð =ájù€=ájå ±ñà@lø€@à~Á' @ãÅ"üAJòàBÌ÷àBå±ó B@JÀBå±@x@¿A>ôàE±‡âµÙÙe±õà=ƒ|õñö =áˆó¶€{áˆ% âó÷à@>²€@ÿ ÿÞö Çâ ó层A¨øàb£á Áë!¨ £áë±ù B@a¨ÀBëc@x@ßA$A¨úàElˆa¨ñÚÛbóû =áj¸,‰`=å"±üà@+€@å,±ýàJ„* JA#ëcþ B@aRàBÿÿ,@x@ÇA ARÿàE›ãà ˆä FÜÜaRsáRIë€=â½å ±à@Ÿæ€@ë c3*àôÿî‘GE¬)% “àóGB½àb  bá jå± B@jÀB†ëc`x@9 Aj @`E"ŸŠbç¥á jÝÝaj =⽪¢€=áj%pä(1à@ @á å±àBf¡€ƒà B$ ƒä–Ç B@aàBå±@x@¿AAJ àE¨Z‹aJàˆá JÞÞe± à=)m„Fÿÿ¦ =áˆ`€{äF% áˆ+c à@ã[€@ÿ ÿB!ˆ @îWâ óå±WµAë àbD báå± B@a¨ÀBÿÿB@x@ßA$A¨àE/Œa¨å±ßàbó =ájbÖ€=ájä^(fà@ÂÔ€@å,±àJ.ÀJ å± B@aRàBå±@x@ÇA ARàE €{áˆ%på±!à@…€@å ±ë câ óå±dVA¨"àbæá ᨠ£çZœy# B@`æÀBå±@x@ßA$A¨$àEÑ¿€ëöÇäåbó% =ájôaæä^"´&à@S~€@â m!j%òÿÿô'àJ¿} J â¸ñ( B@aRàBÿÿ* @x@ÇA AR)àEÞ6‘aàˆå ±ææaR* =áR…>€=áR%±+à@â9€@å ±Sq 3%®qêeºa ‹ÿ* ,àbN bàá½å±- B@jÀBÿÿ; @x@ßA$Aj.àEeò€ëàˆá jççaj/ =ájñõ€=ä(%pä(+c0à@N @å$±1àB­ô€ƒàÁÁJå±2 B@JÀBëc@x@¿AAJ3àEì­’bµàËá Jèèe±4à=mÀ„F|å±5 =áˆܳ€{áˆ% áˆ"ó6à@'¯€@ÿÿ @ëcâ óå±/ÿ* 7àbˆ®€báᨠbå ±8 B@a¨ÀBÿÿô@x@ßA$A¨9àEri“a¨â1á ¨éébó: =ájn€=áj%páj%±;à@rl€@å,±<àJÞk JA#å±= B@aRàBzÿÿ; @x@ÇA AR>àEù$”áRêêaR? =áR®,€=å"±@à@(€@ö Çcƒ+qTQ2àdUàD©Aàbq'€bá jå±B B@jÀBÿÿÔæ@x@ßA$AjCàE€àà ˆâ ½ëëajD =ájøã€=â½å ±Eà@S @‡ ârëcFàB¸â€ƒá Jå±G B@JàB@Ùà…ëc@x@¿AAJHàEœ•âµììe±Ià=jƒ|ÿÿ¦J =@®€Áû¡€{áˆ% äF%±Kà@D€@ö Çâ óå±¶ôBóLàb¢á å±M B@`æÀB¡î W!¨ÿÿL6@x@ßA$A¨NàEW–á¨ííbóO =áj5\€=ä^%±Pà@•Z€@â mëcQàJÀJ þâ¸ëcR B@aRàBñ@x@ÇA ARSàE—áRîîaRT =áR¾€=â½ç"½Uà@€@å ±sÿ‘.k[ƒ7+g‘ÿÿ²ºÚÜB½Vàb„€bá jå±W B@jÀBüy@x@ßA$AjXàE›Î€ˆå2å ±ïïajY =áj#Ò€=ájå ±Zà@Ñ€@á å±[àBßÐàBå±\ B@JÀBëc@x@¿AAJ]àE!Š˜bµÿÿh°ððe±^à=)XƒÇöüy_ =ሀ{äFå ±`à@`‹€@ÿÿ/¼â óå±~.BóaàbÁ¡á ë!¨ £ë cb B@a¨ÀBÿÿ,@x@ßA$A¨càE¨E™a¨ÿÿ¦ñòbód = ƒÓš`=ájä^%±eà@3€@å,±fàJŸ J å ±g B@aRàBå±@x@ÇA ARhàE¶¼€ˆÿÿEóóaRi =áRpÄ€=áR%±jà@ο€@ñ ƒ%«1%«qò…·ÿÿÃÐÿkàb: bå(Èoå±l B@jÀBöÇ@x@ßA$AjmàE @á !j @ãÅ(®@B…àBŸh€ƒâ µå±† B@JÀBëc@x@¿AAJ‡àEÞ!ŸáJùùe±ˆà=ê¤Fÿÿô‰ =áˆâ'€{áˆ% áˆ(¥Šà@#€@§ÿ ÿB!ˆ @âó屟ÿ ‹àb~"€bᨠbá륱Œ B@a¨ÀBÿÿô@x@ßA$A¨àEeÝ€ˆ¼ Z¤çîWUÓúûbóŽ =áj‰ aæáj$áj"´à@뛀@å,±àJWÀJ ®ÀÉɸ屑 B@aRàBöÇ@x@ÇA AR’àErT¡açüüaR“ =áR'\€=å"±”à@‚W€@ë c£‘aVÁ.%¸‘õ<†a 0ÿ; •àbîV€bá jå±– B@jÀBÿÿ¸l@x@ßA$Aj—àEù¢ájýýaj˜ =áj…€=â½%pâ½%±™à@à€@å$±šàBE Bá Jå±› B@JàB £ÂÂøÿÿ,@x@¿AAJœàE€Ë€Ëîþþe±à=Þ„Fÿÿ@Òž = ž€ÁxÑ€{áˆ% áˆ%±Ÿà@ÁÌ€@ö Çâ óå±”lAë àb  bá ë!¨ £å ±¡ B@`æÀB öÇ@x@ßA$A¨¢àE‡£bóÿÿÞÿ )£ = ƒ3G¤ ä ^%±¤à@•E€@â mëc¥àJÀJ â¸å±¦ B@aRàBëc@x@ÇA AR§àEþ Ó@Y£AçA@aR¨ =@€ƒÜ¥aRâ½ç"½©à@8€@å ±³ƒ6E­ E­ƒ8à ààSB½ªàb¤€bàáÄ#屫 B@€æÀBå±@x@ßA$Aj¬àEš¹à ˆá jaj­ =áj'½€=çå ±®à@¼€@á )Ú @ãÅ"üAJ¯àBâ»àB$àB±° B@JÀBëc@x@¿AAJ±àE!u¦bwàËá J@=²à=¦¤Fÿÿ¾³ =áˆ{€{âó% å±´à@_v€@—ëcâ óå±D¡A¨µàb½á Âó!¨ £áë«c¶ B@a¨ÀBñ@x@ßA$Aj·àE¨0§a¨ÿÿÞbó¸ =ájÙð€=âóä^"´¹à@;ï€@å,±ºàJ§î JA#¥(ÁRå±» B@aRàBå±@x@ÇA AR¼àEµ§¨aRàˆâ ûaR½ =áR^¯€=â½áR%±¾à@¹ª€@å ±ÃdUÿî‘nE­-E¹ÿÿBáXB½¿àb% bàáÁjå±À B@jÀBÿÿ@Ò@x@ßA$AjÁàE€bàá½å±) B@€æÀBÿÿnb@x@ßA$Aj*àEÝ÷€ˆŒ@Y¡óá jaj+ =áj^û€=â½å ±,à@»ú€@á å±-àB!àB$ ƒä‹c. B@JÀBÿÿ§V@x@¿AAJ/àEd³»âµ  e±0à=å¤Fÿÿ; 1 =áˆT¹€{âó% å±2à@ ´€@öÇâ óå±8ŠBó3àb bá ë!¨ £á¨‹c4 B@a¨ÀBüy@x@ßA$A¨5àEën¼a¨ëc!"bó6 = ¾€ƒ/½`=âóä^%±7à@~-€@å,±8àJê, J ýÄF `‹áR-‚û9 B@aRàBå±@x@ÇA AR:àEøåà ˆä F##aR; =áRœí€=áR%±<à@øè€@å ±#6Áu¥«"îÄTðëc)ÿ“L=àbd bá jå±> B@jÀBå±@x@ßA$Aj?àE¡¾bàˆá j$$aj@ =áj÷¤€=ä(%-ä(+cAà@U @é wå±BàB·£€ƒàÁµ$ ƒâµ¥±C B@JàB@Ù¦¶ÀB)ºŽ@x@¿AAJDàE]¿aJæü%%e±Eà=‡o„Fÿÿ§VF =@®€Áúb€{áˆ% áˆ"óGà@F^€@å ±ë câ óå±ÿ™4Hàb¦¡á¨ £á¨…±I B@`æÀBå±@x@ßA$A¨JàEÀa¨å±&'bóK =áj±Ø€=ä^%±Là@×€@â m!j%òÿÿôMàJÖ JA#â¸å±N B@aRàBÿÿÞ@x@ÇA AROàEšÁaRàˆä F((aRP =áRF—€=áR"½Qà@¦’€@å ±3le¯"àþE®fõ‚a žÄD©Ràb bàáÄ#å±S B@jÀBñ@x@ßA$AjTàE!KÂáj))ajU =áj±N€=ä(%-`±€3ÒAVà@  @å$±WàBmM€ƒá Jå±X B@JÀBñ@x@¿AAJYàE§ÃáJ**e±Zà=)ƒ|å±[ =መ €{âó% áˆ"ó\à@ä€@å ±': @ÿÿ,â óå±:6Aë]àbC bᨠbå ±^ B@a¨ÀBå±@x@ß[ =A¨_ @€E.€ëñ+,bó` =ájV‚Äaæâó%páj%±aà@¹€€@å,±bàJ%ÀJ å±c B@a àBå±@x@ÇA ARdàE<9ÅaãÇå ±--aRe =áRØ@€=â½áR"½fà@4<€@å ±Cä%®E¬ge¹ñ‡äB½gàb ;€bá jå±h B@jÀBëc@x@ßA$AjiàEÂôà ˆá j..ajj = …€ƒJø€=â½å ±kà@¥÷€@á鯹$( @ãÅ"üAJlàB Bá Jå±m B@€ÆÀBå±@x@¿AAJnàEI°Æbµàˆá J//e±oà=ʤFñp =áˆ5¶€{áˆ% äF"óqà@‚±€@—ÿ ÿB)ø @å±6Çâ óå±#A¨ràbåá Áë!¨ báë«cs B@a¨ÀBöÇ@x@ßA$A¨tàEÐkÇA@Tá¨00bóu =ájwp€=ájä^"´và@Øn€@å,±wàJDÀJ@Îå±x B@aRàBÿÿ* @x@ÇA ARyàEV'ÈáR11aRz =áRì.€=áR"½{à@G*€@å ±Se®k¥® a i¸B½|àb³)€bä ©å±} B@jÀBÿÿB@x@ßA$Aj~àEÝ áóä f22aj =áj]æ€=áj%pä(1€à@¹å€@à~ÃÅå±àBàB$ ƒä“Ë‚ B@JÀBå±@x@¿AAJƒàEdžÉâµ33e±„à=llƒÇÿÿL6… =áˆT¤€{áˆ% áˆ"ó†à@ŸŸ€@—ãû!ˆ @å±+câ óå±ÍJAë‡àb bá ë!¨ bᨑˆ B@a¨ÀBëc@x@ßA$A¨‰àEëYÊa¨ã|â ó44bóŠ = ¾€ƒ^€=ê%±‹à@ó\€@å,±ŒàJ_ÀJ áµñ B@aRàBöÇ@x@ÇA ARŽàEqËaRàˆá R55aR =áR €=áR"½à@e€@å ±cð$på®ðßÿBó€FcB½‘àbÑ€bàáĩ屒 B@jÀBëc@x@ßA$Aj“àEøÐà ˆá j66aj” =ájxÔ€=ä(%-ä(%±•à@ÖÓ€@á $( @ãÅ"ü@B–àB8àB$àB¥±— B@JàB@ÙèªöÇ@x@¿AAJ˜àEŒÌâµ77e±™à=‡ZƒÇòÿÿ5nš =@®€Áo’€{áˆ% áˆ"ó›à@»€@ñ ë ccâóå±â‘A¨œàb bá ë!¨ £á륱 B@`æÀBå±@x@ßA$A¨žàEHÍa¨å±88bóŸ =áj¬L€=ájë c à@K€@â m!j @öÇ¡àJzJ€JàÉÄFå±¢ B@aRàBÿÿB@x@ÇA AR£àEŒÎaRáÛä F99aR¤ =áR: €=å"±¥à@”€@ÿÿ; TsÅ®ƒmtðÀÍ€´1B½¦àb bàáÁjå±§ B@jÀBëc@x@ßA$Aj¨àE¿€ëàˆá j::aj© = { ƒ—€=â½%-ä(%±ªà@óÁ€@â r屫àBW BàÁÁJ$ ƒä‹c¬ B@JàB@Ùå±@x@¿AAJ­àEšzÏâµ;;e±®à=¢Hƒ|ÿÿ@Ò¯ =@®€ÁŠ€€{áˆ% áˆ%±°à@Ô{€@ÿ ÿ* !ˆ @ëcâ óå±ÃAë±àb6 bᨠbᨋc² B@`æÀBå±@x@ßA$A¨³àE 6Ða¨öÇ<=bó´ =ájNö€=áj%páj(fµà@¯ô€@â må±¶àJÀJA#â¸å±· B@aRàBñ@x@ÇA AR¸àE.­ÑaRàˆä F>>aR¹ =áRð´€=å"±ºà@B°€@è o"½!“⽃%¥  'ð£NZà¡òB½»àb®¯€bàáÄfå±¼ B@jÀB‰ÿ ÿ œÿÿû@x@ßA$Aj½àEµhÒajÿÿ¦??aj¾ =áj9l€=â½%-â½%±¿à@•k€@å$±ÀàBõj€Bá Jå±Á B@JÀBñ@x@¿AAJÂàE;$ÓaJàˆâ µ@@e±Ãà=¼¤F€ÿÿ¦Ä =áˆ4*€{âó% áˆ%±Åà@v%€@bºãû$F @âó6Çâ óå±"“!ëÆàb×á Âó!¨ bå ±Ç B@a¨ÀBÿÿ@Ò@x@ßA$A¨ÈàEÂ߀ëå±ABbóÉ =ájìŸÔaæâó$áj%±Êà@Mž€@å,±ËàJ¹ JA#áµå±Ì B@aRàBå±@x@ÇA ARÍàEÐVÕaãÇâ ûCCaRÎ =áR‘^€=áR"½Ïà@äY€@ÿ ÿ²å ±“%¨ ðƒxt ` £a íAB½ÐàbP bàá½å±Ñ B@jÀBå±@x@ßA$AjÒàEVÖajáóá jDDajÓ =ájæ€=ájå ±Ôà@B @à~Á!j @ãÅ"üAJÕàB¢€ƒà B$àB«cÖ B@JÀBå±@x@¿AAJ×àEÝÍà ˆá JEEe±Øà=^à„FÿÿQèÙ =áˆÕÓ€{áˆ% äF"óÚà@Ï€@ÿÿh°â óå±I³A¨Ûàby΀bᨠ£áë«cÜ B@a¨ÀBå±@x@ßA$A¨ÝàEd‰×bóÿÿÞFGbóÞ =áj’IØ`=çå ±ßà@óG€@‡@Såf' @ñààJ_ÀJ àJå±á B@aRàBñ@x@ÇA ARâàEqÙaáÛâ ûHHaRã =@þ€ƒ'€=â½!â½äà@€@å ±£t%©& ð"Ãa šëB½åàbí€bàáĩ屿 B@€æÀB‚(Ïá­ëc@x@ßA$AjçàEø»à ˆá jIIajè =ájˆ¿€=áj%-áj)wéà@ä¾€@á å±êàBDàBå±ë B@JÀBå±@x@¿AAJìàEwÚbµã>á JJJe±íà=Š„Få±î = 5 Ás}€{áˆ% áˆ%±ïà@µx€@ÿ ÿL6$F @ëcâ óå±>Aëðàb báÂó!¨ b€ ¥±ñ B@a¨ÀB â ó"ôÿÿô@x@ßA$A¨òàE3Ûa¨ëcKLbóó =áj,ó€=å"±ôà@Œñ€@ë,cõàJøð J þäå±ö B@aRàBëc@x@ÇA AR÷àEªÜaRàˆâ ûMMaRø =áRÁ±€=áR"½ùà@­€@¤PÜæ¹$F%òâ½(o³ÿE¦àa d«B½úàbs¬€bàá½å±û B@jÀB•â ½å±@x@ßA$AjüàE™eÝájNNajý =áj*i€=ájå ±þà@†h€@á !j @ãÅ«cÿàBæg€BäàB«cu  JÀBÿÿyÆ@x@¿AAJàE !ÞáJOOe±à=¡¤Få± = €Á('€{å"±à@["€@°â ó%Nâóå±ÀYBóàb¼á Âóå± B@`æÀB´â ó"ôå±@x@ßA$A¨àE§Ü€ëå±PQbó =ájÚœßaæâó$ç"´ à@:›€@å,± àJ¦š J þä©å± B@aRàBå±@x@ÇA AR àE´Sàaàˆå ±RRaR =áRw[€=áRcà@ÄV€@ÿ ÿe¼$F!“â½Ãe° Ãåx$ÿÿä4å±CúB½àb0 bàá½å± B@jÀBŽâ ½å±@x@ßA$AjàE;áájSSaj =ájÀ=ä(%-â½+cà@  @å n!j @å ±àBƒ€ƒá J$àB¥± B@JàB ç¢!Kÿÿô@x@¿AAJàEÂÊà ˆâ µTTe±à=CÝ„FÿÿQè =@®€ÁÂЀ{áˆ%páˆ"óà@ýË€@¨å ±ë câ óå±çAëàb^ bᨠ£áë«c B@`æÀB¬â ó!¨å±@x@ßA$A¨àEI†âbóñUVbó =ájtFã`=ä^%±à@ÓD€@â m!j$^ñàJ?ÀJ þâ¸å± B@aRàBÿÿ/¼@x@ÇA AR!àEVý Ó@Y£Aâ ûWWaR" =áRäá$R"½#à@^€@ÿ ÿ,Óþ|} Ëí"€üàõl@¸ÀÿüÄ$àbÊÿà áå±% B@jÀBñ@x@ßA$Aj&àEݸ€ëájXXaj' =ájm¼€=ä(%-ä(%±(à@Ê»€@å$±)àB) BàÁÁJå±* B@JÀBñ@x@¿AAJ+àEctåbwàˆâ µYYe±,à=å¤Fëc- =áˆXz€{áˆ% áˆ"ó.à@Ÿu€@ÿ ÿ,â ó屸ÿüÄ/àbÿá ᨠ£å ±0 B@a¨ÀB¡å±@x@ßA$A¨1àEê/æa¨ëcZ[bó2 =ájð€=áj%páj%±3à@yî€@å,±4àJåí JA#¥(ÁRå±5 B@aRàBå±@x@ÇA AR6àEø¦çaRãÇâ û\\aR7 =áR‡®€=ç"½8à@è©€@ÿ ÿ ã£àõm n o"äõ:a b¥D©9àbT bá jëc: B@jÀBÿÿ5n@x@ßA$Aj;àE~bèajàˆá j]]aj< =ájf€=â½å ±=à@le€@áéÂrëc>àBÊd€BàÁÀBå±? B@JÀBå±@x@¿AAJ@àEéaJÿÿ$X^^e±Aà=†0ƒ|ÿÿ/¼B =áˆù#€{áˆ% äF"óCà@E€@—ëcâ ó屯WBóDàb¥¡á¨ £å ±E B@a¨ÀBÿÿÞ@x@ßA$A¨FàEŒÙ€ëëc_`bóG =ájÙêaæájå ±Hà@#˜€@å,±IàJ—€Jã >å±J B@aRàBå±@x@ÇA ARKàE™PëaáÛä FaaaRL =áR4X€=áR"½Mà@•S€@ÿ ÿ5nó9…78…67u4u6S"a WsB½NàbÀb@$çå±O B@jÀBÿÿ$X@x@ßA$AjPàE ìájbbajQ =áj¨€=ájå ±Rà@ @á å±SàBd€ƒà Bå±T B@JÀBå±@x@¿AAJUàE§Çà ˆâ µcce±Và=,Ú„Fÿÿ$XW =áˆÍ€{áˆ% å±Xà@ãÈ€@ÿ ÿ5n!ˆ @1a0# Kâ óå±óBóYàbC bᨠbå ±Z B@a¨ÀB”ëc@x@ßA$A¨[àE-ƒíbóëcdebó\ =ájQCî`=ê+c]à@´A€@å,±^àJ ÀJA#äFå±_ B@aRàBxÿÿ5n@x@ÇA AR`àE;ú Ó  ˆâ ûffaRa = +€ƒÎïaRâ½ç%±bà@+ý€~ëc UŸ"ƒ+àþ+b‚ŽƒàP€à©ÇB½càb—ü€bàáÊå±d B@€æÀBÿÿô@x@ßA$AjeàEµ,ç¥á jggajf =ájB¹€=ájå ±gà@Ÿ¸€@á $( @ãÅ"üAJhàBþ·àBå±i B@JÀBå±@x@¿AAJjàEHqðbwàËá Jhhe±kà=ɤF~ÿÿ l =áˆ5w€{äF% å±mà@„r€@ÿ ÿ„5\ @å±Ãâ óå± 5A¨nàbäá Âó!¨ báë¶Ço B@a¨ÀBÿÿ$X@x@ßA$A¨pàEÏ,ña¨å±ijbóq =ájïì€=ájä^"´rà@Rë€@å,±sàJ¾ê JA#¥(ÁRå±t B@aRàBå±@x@ÇA ARuàEÝ£òaRäFkkaRv =áR«€=áR%±wà@馀@öÇUð¤$õ‚ä4õƒàÿ"%a ŒB½xàbU bàáÁjå±y B@jÀBöÇ@x@ßA$AjzàEc_óajáóä fllaj{ =ájÇb€=å"±|à@% @à~Áå±}àB‹a€ƒà B$åôiÿ¸l~ B@JÀBÿÿ; @x@¿AAJàEêôáJmme±€à=k-ƒ|uå± =áˆâ €{áˆ% 层à@&€@ÿ ÿÞ!ˆ @å±â óå±£~Bóƒàb†€bᨠbå ±„ B@a¨ÀBÿÿ @x@ßA$A¨…àEqÖ€ˆñno!Ò† =áj¥–õ ä ^%±‡à@•€@‡@Såf'%òüyˆàJs”€Já R屉 B@aRàBÿÿ/¼@x@ÇA ARŠàE~Möaàˆä FppaR‹ =@þ€ƒ:U€=å"±Œà@’P€@å ±#àTƒ}ðà®Ný"a erB½àbþO€bàáĩ屎 B@€æÀBÿÿÃÐ@x@ßA$AjàE ÷ájqqaj =áj… €=ä(%-ä(€@â m!jå±àJÀJ â¸å±ž B@aRàBå±@x@ÇA ARŸàE ÷ Ó@Y£Aâ ûuuaR  =áRÌþ€=G  =Å a⽡à@$ú€@å ±3ƒàÿåzuð¤"ð‘@zÀ;Â$©¢àbù€bàáÀ£å±£ B@jÀBå±@x@ßA$Aj¤àE¦²úbàˆá jvvaj¥ =áj+¶€=áj%páj!¦à@…µ€@ã Åå±§àBç´àB$ ƒä‹c¨ B@JÀBÿÿ @x@¿AAJ©àE-nûaJŒ@Y¢wá Jwwe±ªà=®¤Fÿÿ5n« =áˆt€{å"±¬à@jo€@ÿÿ,â ó屟0Aë­àbÍ¡âjÂó!¨å ±® B@a¨ÀBñ@x@ßA$A¨¯àE´)üa¨àˆá ¨xxbó° =ájW.€=áj%pâó%±±à@¸,€@å,±²àJ$ÀJA#¢rÁRå±³ B@aRàBÿÿ @x@ÇA AR´àE;å€ÓãÇá RyyaRµ =áRøì€=áR(o¶à@Sè€@Œÿ 7$F!“èo"½ CïTD ÿÃtÿÏ;a B½·àb¿ç€bá j屸 B@jÀBÿÿ* @x@ßA$Aj¹àEÁ ýâ½zzajº =ájJ¤€=å"±»à@©£€@á !j ån"üAJ¼àB àB$àB¥±½ B@JàB@Ùâøëc@x@¿AAJ¾àEH\þáJ{{e±¿à=P*‚ÿÿ5nÀ =@®€Á8b€{âó% äF"óÁà@ƒ]€@—â ó)ø @âó+câ óå±)åA¨Âàbäá Ä^!¨ bçZŸÿ* à B@`æÀBÿÿ,@x@ßA$A¨ÄàEÏÿa¨ñ|}bóÅ =ájù×€=âó$áj"´Æà@ZÖ€@â m!j @ñÇàJÆÕ€Jå(ÁRå±È B@aRàBÿÿ¦@x@ÇA ARÉàEÜŽB@Tè­~~aRÊ =áR‚–€=â½!áR"½Ëà@ä‘€@å ±S‚õƒ"uð6Ì –Ì@½ÀTNB½ÌàbP bä ©å±Í B@jÀBÿÿÞ@x@ßA$AjÎàEcJájajÏ =áj÷M€=áj%-áj+cÐà@S @á å±ÑàB³L€ƒà B$ ƒävÇÒ B@JÀBüy@x@¿AAJÓàEêaJàˆè g€€e±Ôà=kƒ|ÿÿ* Õ =áˆÚ €{áˆ% áˆ"óÖà@$€@ñ !ˆ @å±(¥â óå±ð¼Aë×àb†€báÄ^!¨ bᨅ±Ø B@a¨ÀBå±@x@ßA$A¨ÙàEpÁ€ˆñ‚VÇÚ =áj—aæG@—çÞï Â%±Ûà@÷€@å,±ÜàJcÀJà ‹ dêáR-‚ûÝ B@aRàBëc@x@ÇA ARÞàE~8aàˆâ ûƒƒaRß =áR@@€=â½å ±àà@’;€@ÿ 7"½ @â½cƒà"ð£å& ' (ÿÿxçB½áàbþ:€bàá½å±â B@jÀBŠÿ ÿêÿÿô@x@ßA$AjãàEô€ˆç¥á j„„ajä =Àƒ÷€=áj%-ä(%±åà@Ýö€@á !j @âr"ü@BæàB=àB$àB¥±ç B@JÀBå±@x@¿AAJèàE‹¯âµ……e±éà=Â`=ÿÿôê =ሌµ€{äF% áˆ%±ëà@ư€@ÿÿôâ óå±§A¨ìàb' bá ë!¨ £á륱í B@a¨ÀBÿÿô@x@ßA$A¨îàEkajàˆâ ó††bóï =ájÄo€=áj$áj"´ðà@&n€@å,±ñàJ’m JA#æ=å±ò B@aRàBöÇ@x@ÇA ARóàE™&áR‡‡aRô =áR).€=áR"½õà@‰)€@ë cs£"t5%hõht5gõg"ÿÿñ`IÈB½öàbõ(€bá jå±÷ B@jÀBÿÿ @x@ßA$AjøàE  屈ˆajù =áj¬å€=áj%-â½%±úà@ @à~ƹå±ûàBh䀃à B$ ƒäˆgü B@JÀBöÇ@x@¿AAJýàE¦âµ‰‰e±þà=®¤Fóÿÿ@Òÿ =ሖ£€{ç:% áˆ"óvà@㞀@—ñ !ˆ @è¥â óå±í:AëàbB bâ ó!¨ bᨋc B@a¨ÀBëc@x@ßA$A¨àE-Y a¨ëcŠ‹bó =ájY `=ájå ±à@¼€@å,±àJ(ÀJ îå ± B@aRàBå±@x@ÇA ARàE:РÓ@Y¦:ç Œ%T2’ =áR÷×€=äFáR"½ à@OÓ€@ü yƒƒEðƒYý—B¡)àÿä"‘ à xC v  »Ò B ‡  ` ÿ  @ áèF B@àBƒ `8àB @'@x 9 > @@Á‹ `ˆˆ@ Eÿÿ  @´ A =@`=I€=H "À=ÀÆÿ  @@3ÒAà@¤Ž€@á  `@áK J!JàB BàÁÀƒ àBiJ B@€ÆÀBŒ@¥¡ÀB!KJ@x@¿AAJàEHG áJŽŽ@=à=ÉY `=z3"<àƒ€  Œ  !ˆ =@®€Á =áˆýä€{äF% áˆ(¥?à@;à€@¤â óë câ óå±'ÊAë@àb á Âó!¨ £á륱A B@a¨ÀB¤â ó"ôå±@x@ßA$A¨BàE‹šbó屘™bóC = ``ƒÂZ`=áj$áj"´Dà@"Y€@å,±EàJŽX J ånå±F B@€ÎÀBå±@x@ÇA ARGàE™aì¶ššaRH =áR3€=áR"½Ià@•€@ˆâ ½$F!“å±³Ô0"ð àTD"a S÷B½JàbÀb@$æÙå±K B@jÀByâ ½å±@x@ßA$AjLàEÍ ë@Y ˆä f›//)ÚM =áj§Ð€=å"±Nà@ @áéÃÅ!jån"üAJOàBcÏ€ƒàÁÀB$àB¥±P B@JÀBå±@x@¿AAJQàE¦ˆbµàˆá Jœœe±Rà='›„Få±S =áˆ’Ž€{áˆ% äF"óTà@ቀ@è¥ @âóå±÷A¨UàbB bá2kT!¨ bå ±V B@a¨ÀB˜â ó"ôå±@x@ßA$A¨WàE-Da¨ëcžbóX =áj``=ä^"´Yà@À€@‡ ´`ýá'$^ñZàJ,ÀJ BàJå±[ B@aRàBå±@x@ÇA AR\àE:»€ÓŒ@Y Óâ ûŸŸaR] =@þ€ƒõ€=áR"½^à@N¾€@1rà~ÂZ$F!Râ½Ã®àÿt-%~"Óí”ìd€” 8à߯B½_àbº½€bâ ½å±` B@€æÀB‚â ½å±@x@ßA$AjaàEÁvbàˆá j  ajb =ájAz€=ä(%-ä(+ccà@žy€@â r!j @ãÅ"ü@BdàB BàÁÁ­$àB¥±e B@JàB@Ù¢oÀB!KöÇ@x@¿AAJfàEH2aJàËá J¡¡e±gà=ɤFxëch =@®€Á<8€{áˆ% áˆ"óià@ƒ3€@'—á$F @âó6Çâ óå±{4A¨jàbä¡á¨ báë«ck B@`æÀB â ó!¨å±@x@ßA$A¨làEÎí€ëëc¢£bóm =ájõ­aæáj$áj"´nà@U¬€@â m!j @å±oàJÁ« J þáµå±p B@aRàB|ñ@x@ÇA ARqàEÜdaàˆâ û¤-Ä)ør =áRŸl€=å"±sà@ðg€@óÓ!Râ½6ÇÓ€"ÿt~¨"ƒ+àþa ú›B½tàb\ bàáÄ#å±u B@jÀBŠâ ½å±@x@ßA$îà8Ž¡`8صàqŸÿ­'“!UEdC¸ï =@õ‚N¶Ö·`=`í ¯(f` ÿåa&´ðà@Õ€@‡@S`6à@`@æ´€ ^@Ý”C ñàJƒÔ J àJ `J@ D†-Fò B@€ÎÀB{KwàB xŸÿ  @x@ÇA ARóàEe¸aã ÃR J 8eeARô =@€ƒA’€=áRêû!Rõà@Ÿ€@è¡“ `@áRK%¨WçARöàJÀJ á `JáRiR÷ B@€ÎàB@dáR –ë@x@ÇA ARøàEìJ¹aRÿÿ—fgARù =áRBº`=dàƒâ ¥(rúà@£€@á !R"æá!‘@BûàBàBàBáJü B@JÀBz@ÙàÆ-˜"—@'@x@¿AAJýàEùÁà Ëâ hheKþà=ŸÐ€=` ƒÀ=åöÿà8>LÎaƒ`à 8ä w  @Í/‰Î`8ä ƒ¢Aú =! €=ZAå`=ãM!¹èY!»à@»?€@ã Mä  @{ë%BàJ'ÀJ Á£˜ `‹ @ä  B@a àBä @x`Ç =B @`Eú Ó@á‹ä ££AR =@Ö`=æû€=áRä #Mà@C @à~¡#M @á!‘@BàB¢à ÁÀBä˜ B@€ÆÀBõNÀB ä˜@x@¿AAJ àEµæb_Qúá J N¤ÂAJ =áJ3²ü`=áJ¡ ÿÿ ø à@—°€@âÀ@í ó‡>àôAR àJÀJ ®ÀÉÀJíó B@RÀBíóÀB$¡„  @x@ÇA ARàEÝjýaàˆÄbâÃÃAR =áR½m€=áR"\ãðà@l€@è‘ÀÄ0± @ç>àJyk€JàÉÀJ,2 Jâè»ý B@RÀBŒç>ÀB%ôâ¥@x@ÇA ARàEd&þáRÄÄAR =áRH)€=á"Rà@¢'€@‹@S¤oÀÄ"¥!áR Ãm+ÅÖ¯B¥àJ Já$R B@RÀBáR@x@ÇA ARàEëá€Óàˆâ ¥ÅÅAR = { ƒä€=áR,øà@'ã€@Ë RáR‚¥ @PK?ALàD‡â€DàÃÁ—⟠B@LÀBÐ@ÛáÚ⟠5@ÁAAL @`ErÿbŸëºá LÆÆAL =@p`=@Ÿ€=áLçã& à@¢ž€@à~Á!L @㧦!àBàBñÆ" B@€ÆÀBéÞÀ…÷x@x@¿AAJ#àEøX` ë)ÇÈAJ$ = /€ƒb@Þá J ,%à@¿€@çUÀÄ#ê!Jãê&#…ÿé¤A•&àJ+ Jâ æ' B@€ÎÀB dâæ@x@ÇA AR(àEЀÓàˆâ ÉÉAR) =áRÞÒ€=â$üæ*à@<Ñ€@æ T ¶AR+àJ¢à ÉÁãð, B@RÀB„á R"¦æ@x@ÇA AR-àEŒ‹":áRÊÊAR. =áRmŽ€=íÏÀÆ)6á R/à@ÉŒ€@á R"¥ @â¥1 >ouXAR0àJ( JâÀ‹áR1 B@RÀBçã@x@ÇA AR2àEGáRËF3?š3 =áRïI€=â"¥4à@JH€@á R%CáRK !0WšAR5àJ¯à ÉáR6 B@RàBâ¥@x@ÇA AR7àEšáRÌÌAR8 =áRz€=á"R9à@×€@ëÜáR Ã";RäAR:àJ6ÀJA#ç¦â¥; B@RÀBâ¥@x@ÇA AR<àE!¾€Óçjå KÍÍAR= =áRÁ€=á"R>à@\¿€@á Râ ¥  u ­(4?àJ½à Éâ¥@ B@RÀBáR@x@ÇA ARAàE§yâ¥ÎÎ :B =áRƒ|€=á"RCà@áz€@á Rå KƒùÚPARDàJCÀJA#â¥E B@RàBãø@x@ÇA ARFàE.5áRÏÏARG =áR8€=á"RHà@k6€@åK"”#ù‚Q:ARIàJÊà Éâ¥J B@RÀBâ¥@x@ÇA ARKàEµð€ÓáÛã øÐÐARL =áR•ó€=á"RMà@ðñ€@á Râ ¥  ARNàJQ JàÉáRO B@RÀBáR@x@ÇA ARPàE;¬â¥ÑÑARQ =áR¯€=é"DRà@y­€@á Rå KTÓGóaxgSàJØ É%g!K  ³æžT B@RÀB‹ãø@x@ÇA ARUàEÂgáRÒÒARV =áRžj€=â"¥Wà@úh€@á Râ ¥"]#ùƒ·KB¥XàJ^ Jâ$¥Y B@RàBáR@x@ÇA ARZàEI# áRÓÓAR[ =áR©%€=áR"Í\à@ƒ$€@ @SâZ‚¥5¸` K |\]àFåà Åâ¡^ B@NÀB áN@x@ÃAAN_àEÐÞ€Ïÿÿ ÔÕAN` =@ú€ƒ&œ aŒ!& ¿ãh-9/žîŒ(õaà@†›€@â V!N @ñ *bàBòšàBñ*c B@€ÆÀB{@ÙàÆñ*@x@¿AAJdàEÝU a üTÖ×AJe =@p€ƒ8 `=áJ ,fà@”€@ñ!*%BÿÔdA•gàJ Já •ñ*h B@€ÎÀBñ*@x@ÇA ARiàEëÌ€ÓåÈç 娨ARj = { ƒKÏ€=áR,ðâ 'kà@%΀@@Sâ" @ãì  xANlàF‡Í€FàÅãìm B@NÀB ãì@x@ÃAANnàEqˆ "6â¡ÙÚANo =@ú€ƒÇE`=å;#«áN#ìpà@( @â V!N @ã ìqàB”D€ƒà Bãìr B@€ÆÀBãì@x@¿AAJsàEÿ€ˆå7ÛÜAJt =áJæ½aJ"q" å7 ,uà@A¼€@ã ì"›ÿ—ÛA•vàJ­»€Já Rãìw B@RÀB€â õ@x@ÇA >xàEŒv çàˆã ìÝÝARy =áRly€=â#«â jzà@Çw€@é ," @óÄ KUSB fAR{àJ( Já Rê| B@RÀBëÒ@x@ÇA AR}àE2 çàˆá RÞÞAR~ =áRó4€=á"Rà@P3€@á Rê  Rec†bAR€àJ¯à ÉåC B@RÀBáR@x@ÇA AR‚àEší€Ó‹@Y£ºá RßßARƒ =áR~ð€=á"R„à@Ùî€@ñ â ¥eiv!TAÂAR…àJ: Jæ$–† B@RÀBáR@x@ÇA AR‡àE ©â¥ààARˆ =áRL«€=áR<û‰à@fª€@5tÁÑÂZ‚¥ @\ûÃrÚïALŠàDÈà ÃÀDãò‹ B@LÀBÌ@Û¢YÀB*†ù @x@ÁAALŒàE§daLÿÿ6áâAL =@p€ƒý!`=æ˜'ãŽà@^ @ä ú"Ÿç ãàBÊ €ƒà Bçã B@€ÆÀBüû@x@¿AAJ‘àEµÛ€ˆçããäAJ’ =áJ šá$J ,“à@k˜€@ã ê"—!Jæ\û _ÿ/”àJ×— JA#âZçã• B@RÀBÿÿ+Âçã@x@ÇA AR–àEÂR ç Ó Àˆå =ååAR— =áRüS€=ã êg¢çãB˜à@^ @à~ÀÄã ¥B™ B@ÀBæ@áJC¥ÿI’@x@½AšàAIa%Eð 1‰üáI@òæ.A› =@€ËUL iã ¡ jœà@*T€@â!V |¿ÿäàJ–S J ÀÉÁNâVž B@€ÎÀBÿÿ$°ÀBâV@x@ÇA ARŸàEmaàˆáR à 8//aR  =@€ƒI€=â V0Å)ùâVaR¡à@¤€@è ç$ô @ã©Kÿÿ #¢àJ  JàÉÁâ ¡F£ B@€ÎàBë æG@x@ÇA AR¤àEôÉ Ó â ¥A@00aR¥ =@€ƒØÌ€=á"R¦à@1Ë€@ÿ,ÿä§àJÊ€JàÉáR¨ B@€ÎÀBÿÿä@x@ÇA AR©àEz… â¥11aRª =áRš‡€=áR(ç«à@¶†€@ÿ&ÿä¬àD Dâ$Ÿ­ B@LÀBèç@x@ÁAAL®àEA áL22aL¯ =áLÏB€=áL'[ãò(ç°à@1 @á #ò @ð ˱àBà Á—èç² B@JÀBèç@x@¿AAJ³àEˆü èç34aJ´ =áJòº aˆê2&µà@S¹€@å =è ç$Ø ®A•¶àJ¿¸ JA#¤´ÁRæ· B@RÀBÿÿMmÀBæ@x@ÇA AR¸àE•s aâæ 55aR¹ =áRuv€=â&Oâ!Rºà@Ót€@á R" @æÿÿä»àJ1 JàÉÁæ¼ B@RÀBî%@x@ÇA AR½àE/ áR66aR¾ =áR1€=áRD¿à@T0€@§@ØâZ"¥!R@  Ã1kBžÀàC¸à ÂáKÁ B@KÀB¨@Úà†çÜ@x@ÀAAKÂàE£ê€ÌáÔâ ž77aKà =@p€ƒqì€=ãñ%<Äà@Óë€@à~Á"žå <ÅàB?àBå<Æ B@€ÆÀBy@ÙáJùõ@x@¿AAJÇàE)¦b–ïo89aJÈ =@p€ƒWe`=â–! ãéÉà@¸c€@ë!Í%<T–|“ÊàJ$ÀJA#áå<Ë B@€ÎÀBî$@x@ÇA ARÌàE7aàˆâ ::aRÍ =áR €=áR#¨áRÎà@t€@á R" @å<ÏàJÓà ÉÂèå<Ð B@RÀBå<@x@ÇA ARÑàE¾Ø€Óçá R;;aRÒ =áRšÛ€=á"RÓà@úÙ€@æ …<áRÿÿ !ÔàJZÀJA#â¥áRÕ B@RÀBí @x@ÇA ARÖàED”â¥<>aRá =áR2€=á"Râà@ €@åKÿÿ !ãàJîà ÉáRä B@RÀBáR@x@ÇA ARåàEØÆ€ÓÿÿH3??aRæ =áRµÉ€=é<&]`±€3kÛçà@È€@á Râ ¥ÿÿ !èàJxÇ J må Ké B@RÀBÿÿÀ@x@ÇA ARêàE_‚â¥@@aRë =áR[…€=â"¥ìà@·ƒ€@á Rå Kÿÿ !íàJÀJA#æžî B@RÀBâ¥@x@ÇA ARïàEæ=áRAAaRð =áRÆ@€=á"Rñà@!?€@åKÿÿ !òàJ‚>€Já$Ró B@RÀBáR@x@ÇA ARôàEmù€ˆèzé DBBaRõ =áRMü€=á"Röà@ªú€@á Râ ¥ ÿGóaÿ !÷àJ  Já$Rø B@RÀBáR@x@ÇA ARùàEó´â¥CCaRú =áRÓ·€=á"Rûà@/¶€@á Rå Kÿÿ !üàJµ€Já$Rý B@RÀBáR@x@ÇA ARþàEzpáRDDaRÿ =áRÚr€=áRFÆxà@´q€@ @Så‚¥ÿÿ4ÿÿ !àF Fá$N B@NÀBÿÿ !@x@ÃAANàE,áNEEaN =@ú€ƒÑ-€=áNïß1*à@1 @á !N @ö gàBà ÁÉ8ñ* B@€ÆÀB|@ÙáÔñ*@x@¿AAJàEˆç ögFGaJ =@p€ƒÜ¥aˆBqàƒ;¥! é8 à@>¤€@ç å#ì @î„ @{'%A• àJª£ J à‹ñ* B@€ÎÀBy1àB ÿ'~ @x@ÇA AR àE•^aàˆæ ’HHaR =@Ö€ƒq`€=âê‹"à@Í_€@çÂZ" @â àB1 BàÁÀBïÏ B@€ÆàB@Ùâ"žÿÿ9@x@¿AAJàEaJâIJaJ =@p€ƒ³Ø€=áJ"\âà@×€@à Äâ Ÿÿ ÒÔëA•àJ~Ö J þáâ B@€ÎÀB}â@x@ÇA ARàE)‘aRàˆâ KKaR =áR‘“€=áR"\áR$à@p’€@@Sâ‚æŠU7“ANàFÑà Åâ¡ B@NÀBá Nâ¡@x@ÃAANàE°LáNLLaN =@ú€ƒ}N€=áNâ¡#ìà@àM€@áÍÁ!N @ã ìàBLàBæŠ B@€ÆÀB÷µ@x@¿AAJ!àE7aJãìMNaJ" = …€ƒèÆ€="q!ŸÿÂÁ æŠ#à@IÅ€@ã$ìÝKA•$àJµÄ J Šà‹ãì% B@€ÎÀBüò@x@ÇA AR&àEDaRàˆã ìOOaR' =áR@‚€=âãì<ò(à@€€@ë Ê" @çÝcìÿÿ ¿)àJ JàÉÁÞæ’* B@RàBãð@x@ÇA AR+àEË: áRPPaR, = { ƒï=€=á"R-à@J<€@á R&’áRÿÿ ¿.àJ«;€Já$R/ B@RÀBí@x@ÇA AR0àERö€ˆè±â ¥QQaR1 =áRNù€=ãø!æ’"¥2à@«÷€@ã øâ ¥ÿÿ ¿3àJ  JàÉâ¥4 B@RÀB‹â¥@x@ÇA AR5àEر!â¥RRaR6 =áR$´€=â¥E7à@<³€@ÿ&ÿ ¿8àDœ²€Dâ$Ÿ9 B@LÀBÿÿ*»@x@ÁAAL:àE_m"áLSSaL; =áLRo€=âŸ'¢âŸ'ã<à@³n€@á "Ÿ @ç ã=àBàBçã> B@JÀBÿÿ×@x@¿AAJ?àEæ(#aJçãTUaJ@ =áJxç€=áJ#êAà@Øå€@ã ê%=!JæëÐBàJDÀJ èoçãC B@RÀBÿ™@x@ÇA ARDàEóŸ$aRàˆå =VVaRE =áRk¢€=ãê!zFà@I¡€@ÿÿëÐGàF¯ €FàÅå9H B@NÀBå9@x@ÃAANIàEz[%áNWWaNJ =áNf]€=áN"`ãìKà@Æ\€@á ãìLàB2àBãìM B@JÀBò[@x@¿AAJNàE&aJãìXYaJO =áJ¡Õ€="q" ëÐPà@Ô€@ã$ìËÐQàJoÓ J Šà‹ãìR B@RÀBò[@x@ÇA ARSàEŽ'aRàˆã ìZZaRT =áR‘€=âé *Uà@j€@ê }" @å?cìëÐVàJÊà ÉãðW B@RÀBê}@x@ÇA ARXàE•I(áR[[aRY =áR™L€=á"RZà@÷J€@á R&’áRëÐ[àJY JâáR\ B@RàBåC@x@ÇA AR]àE)áR\\aR^ =áRü€=ãø!`±€3gå_à@X€@á Râ ¥ëÐ`àJ·à ÉáRa B@RÀBâ¥@x@ÇA ARbàE¢À€ÓáÛã ø]]aRc =áRö€=â¥+Ðdà@  @ë&ÐeàDnÁ€…àÃáLf B@LÀBëÐ@x@ÁAALgàE)|*âŸ^^aLh =áL*~€=áLç ãià@}€@á "Ÿ @ë ÐjàBùà ÁÁJçãk B@JÀBó´@x@¿AAJlàE°7+aJçã_`aJm =áJÅö€=ãê! ãênà@&õ€@ç$ãסoxà‹’ô JA#èoçãp B@RÀBó´@x@ÇA >qàE½®,aRàˆã êaaaRr =áRM±€=âå=!zsà@*°€@ë(ÐtàF‰¯€FàÅ¡ó°u B@NÀBþ,@x@ÃAANvàEDj-áNbbaNw =áNl€=!& ¿ê…/½xà@|k€@á ãìyàBèà ÁÁJãìz B@JÀBï½@x@¿AAJ{àEË%.aJãìcdaJ| =áJiä€=â™ã ì}à@Éâ€@ã$ìËÐ~àJ5ÀJA#áÖãì B@RÀBÿÿ&@x@ÇA AR€àEØœ/aRàˆã ìeeaR =áR䟀=áRãì1‚à@>ž€@á R" @ëЃàJ  J áRãð„ B@RÀBé*@x@ÇA AR…àE_X0áRffaR† = …€ƒW[€=á"R‡à@´Y€@ê }ëЈàJ Já$R‰ B@€ÎàBëÐ@x@ÇA ARŠàEå1áRggaR‹ =áRÚ€=åC"dçåŒà@5€@á Râ ¥ëÐàJ•€Já$RŽ B@RÀBâ¥@x@ÇA ARàElÏà ˆã øhhaR =áRÌÑ€=â¥+Бà@æÐ€@ÿ ÿy‚¥`òÿÿ%a’àDL Dá$L“ B@LÀBÈ@Ûä5ÿ…@x@ÁAAL”àEóŠ2âŸiiaL• =@p€ƒߌ€=áLæ˜'ã–à@? @á "Ÿ @ë ЗàB«‹€ƒà Bç㘠B@€ÆÀBó´@x@¿AAJ™àEzF3aJ•ÿÿSæâ—@sjlaJš =áJ£À4`=ãê! ãê›à@¿€@æ "— @æÃ!‰‘Öÿ‰æœàJo¾ J âZçã B@RÀB÷¡@x@ÇA ARžàEy5aàˆáRA@mmaRŸ =@€ƒ{€=âå=" à@xz€@ü ”" @â ¡àBÚà ÁÊçÛ¢ B@€ÆàB@Ùãè@x@¿AAJ£àE”46aJÿÿÚnoA¤ =@p€ƒó€=áJ"\â¥à@wñ€@Œ@SàÄâ "{ÊÿHæ¦àJãð€JâÁRâ§ B@€ÎÀBâ@x@ÇA AR¨àE¢«7aRàˆâ ppaR© =@þ€ƒÒ®€=áR"\áRªà@/­€@â ‚ãð K ¡•ûÿ†º«àJެ JA#á⥬ B@€ÎÀBŒé.@x@ÇA AR­àE)g8áRqqaR® =áR)j€=á"R¯à@„h€@á R"¥!“áR Ãu%r´D;°àJéà ÉÂ¥áR± B@RàBáR@x@ÇA AR²àE¯"9áRrraR³ =áRË%€=á"R´à@&$€@êà)çÿ*ðµàJ‡#€Jâ$¥¶ B@RÀBâ¥@x@ÇA AR·àE6Þà ˆã øssaR¸ =áRá€=á"R¹à@w߀@á Râ ¥ •)K`=ãóëå-0Òà@¦D€@á "  @í 0ÓàBàBïÎÔ B@€ÆÀBÿÿ=@x@¿AAJÕàEåþ€Ëÿÿ~¦â˜@sz}aJÖ =áJ’4AaJâ˜! å>×à@õ2€@êŠ @êŠOÎ@}×ÜA•ØàJaÀJ@¯âZí0Ù B@RÀB"5àBÿÿà@x@ÇA ARÚàEí€Óÿÿ l~~aRÛ =@Ô€ƒdî€=â æ‘j?Üà@Ä ~á ë ’BÝ B@€ƒÀBç@áJÿÿ50@x@½AÞàA‡¨Bbã¡€aß =áÂgC`=á ÿÿ¬àà@"f€@è è$ïâVO‡Ô¶£BVáàJŽe J á âVâ B@NÀBï‡@x@ÇA ARãàE”Daá×ã©A@,WaRä =@€ƒ€"€=ã©è èåà@Û €@è è$ô @ã©O‡¡…‡?ARæàJ@ JàÉç•ç B@€ÎàBã ©æG@x@ÇA ARèàEÛ€Óàˆá R‚‚aRé = `€ƒ'Þ€=á"Rêà@„Ü€@äüáR à ¡•u½vARëàJãà ÉáRì B@€ÎÀBìá@x@ÇA ARíàE¢–E⥃ƒaRî = …€ƒ‚™€=á"Rïà@Ý—€@á Râ ¥pØ ^¬ARðàJ>ÀJ ãøå GFñ B@€Î> BáR@x@ÇA ARòàE(RFáR„„aRó =áRU€=á"Rôà@nS€@á R%Kâ¥)$€•u ¡ARõàJÐà Éâ¥ö B@RàBãø@x@ÇA AR÷àE¯ GáR……aRø =áR»€=á"Rùà@€@åK ø&ÿq(ARúàJw€Jâ$¥û B@RÀBŒáR@x@ÇA ARüàE6Éà ˆå K††aRý =áR*Ì€=á"Rþà@…Ê€@çñ⥠0 1•TmARÿàJæà Éâ¥y  RÀBáR@x@ÇA ARàE½„Hb¥èzá R‡‡aR =áRÁ‡€=á"Rà@†€@â¥0Ø% 8ò¢ARàJy…€JàÉáR B@RÀBï‡@x@ÇA ARàEC@IaRã.á Rˆ$`: =áRSC€=á"Rà@¯A€@åKB¡ 8÷|½ àJÀJA#ê—æž B@RàBãø@x@ÇA AR àEÊû Ó@Y ˆá R‰‰aR =áRÊþ€=á"R à@$ý€@á Rç ñ ÀÀ FUB¥àJ†ü JàÉ⥠B@RÀBãø@x@ÇA ARàEQ·J⥊ŠaR =áR5º€=á"Rà@‘¸€@á Rç ñ+ç'òÒARàJõà ÉáR B@RàBâ¥@x@ÇA ARàE×rKáR‹‹aR =áRÜu€=î&]ðçà@3t€@åK &ÿ©«ARàJ“s€Jâ$¥ B@RÀBåK@x@ÇA ARàE^.LáRŒ# %K =áRb1€=â"¥à@¼/€@á Râ ¥*ÿ4ÖÍÌARàJ Já$R B@RÀBáR@x@ÇA ARàEåé€Óàˆå KaR =áRÑì€=á"R!à@1ë€@é Då K €#ù•ºAR"àJ‘ê€JàÉãø# B@RàBãø@x@ÇA AR$àEl¥Mb¥èzá RŽŽaR% =áRp¨€=á"R&à@ʦ€@åK u% ‚AR'àJ, JàÉáR( B@RÀB# ;ŸÿÀBñ6@x@ÇA AR)àEò`NaRáÛá R:‡#ø* =áRòc€=á"R+à@Kb€@ú ƒu%AR,àJªa€JàÉÁéD- B@RÀBãø@x@ÇA AR.àEyOáR g!R/ =áR‘€=á"R0à@è€@á Rå K À¼ÿ ˆÏÎAR1àJI Já$R2 B@RÀB ê ~â¥@x@ÇA AR3àEØ€Óàˆâ ¥‘‘aR4 =áRÜÚ€=á"R5à@:Ù€@åK%I?mÁŽAR6àJ à Éâ¥7 B@RÀB2! ;áR@x@ÇA AR8àE†“P⥒’aR9 = p€ƒw–€=é"D:à@Ï”€@çñIC)lœAR;àJ/ Jå$K< B@€ÎÀB$ãáR@x@ÇA AR=àE OQáR““aR> =áRuQ€=â¥!z?à@HP€@@Sâ‡ñÿÿ-oÿ)BŸ·AN@àF©à Åâ¡A B@NÀBáN@x@ÃAANBàE” RaNÿÿ ”4£EGC =@ú€ƒêÇ€=áNëæ>Dà@K @äsÁ!N @þ EàB·Æ€ƒà BþF B@€ÆÀBþ@x@¿AAJGàE¡SaJ*ªÂå?@s–›aJH =áJY.W`=áJKIà@»,€@é8!Júo~B9טA•JàJ'ÀJ@¯âäûÂK B@RÀBÿÿº@x@ÇA ARLàEÊæ Ó@Y ˆáRA@œœaRM =@€ƒ*è€=á Rÿî„~Nà@Šç€@à~Âè í>O B@€ƒÀBÿ ÿIþ@x@½APàAP¢Xb㡞aQ =áù`Y`=$È oæÇþRà@[_€@âV "wBVSàJÇ^ J Šà‹âVT B@NÀBÿÿq@x@ÇA ARUàE^ZaÿÿqŸŸaRV =áR^€=â V!Rð ÛWà@²€@@Sá“$ô @ã©Kÿ<É…gýARXàJ Já Râ ¡FY B@RÀB”D àìâ@x@ÇA ARZàEåÔ€Óèã ©  aR[ =@þ€ƒEØ€=á"R\à@ŸÖ€@ê <ë ¿ÿWd"AR]àJ Já$R^ B@€ÎÀBî5@x@ÇA AR_àEk[b¥àˆá R¡¡aR` =áRK“€=á"Raà@¤‘€@ì ââ ¥ÿ  "ÿ®\bàJ JàÉåG"¥c B@RÀBáR@x@ÇA ARdàEòK\áR¢¢aRe =áRÚN€=á"Rfà@1M€@á Râ ¥‘ÀCûYõB¥gàJ’L€Já$Rh B@RÀBãø@x@ÇA ARiàEy]áR££aRj =áR] €=á"Rkà@»€@ã øâ ¥%F•u|IARlàJ! Já$Rm B@RÀBî5@x@ÇA ARnàEàÓ@Y¥‘ã ø¤¤aRo =áRäÅ€=çñ&]÷zpà@<Ä€@î5.5 |×ARqàJœà Éãør B@RÀBî5@x@ÇA ARsàE†~^⥥¥aRt =áRŠ€=â"¥uà@å€@â¥!NcûÙWARvàJF Jâ$¥w B@RÀBåK@x@ÇA ARxàE :_áR¦¦aRy =áR=€=á"Rzà@j;€@â¥ÿ #û •v'{àJÉà Éâ¥| B@RÀBåK@x@ÇA AR}àE”õ€ÓáÛã ø§§aR~ =áR¨ø€=á"Rà@ÿö€@æ žå Kucû¶ÿ¹€àJ` JàÉáR B@RÀBáR@x@ÇA AR‚àE±`⥨¨aRƒ = …€ƒ_´€=å"K„à@¼²€@í=%Œ l@ à A)BA‘ÿ?¢…àJÀJ (ÆáR† B@€ÎàBçñ@x@ÇA AR‡àE¡laáR©©aRˆ =áR¥o€=â"¥‰à@ým€@⥠…!• B ¶EKŠàJ] Já$R‹ B@RÀBâ¥@x@ÇA ARŒàE((báRªªaR =áRÄ*€=áR 'Žà@)€@'@S¨pÂZŠ—`ò à B&B;AOàGäà ÆÀGî B@OÀB(â¢@x@ÄA AO‘àE¯ã€ÐÿÿN«¬TÕ’ =@û€ƒ%¡caãõ3IéA4Õ“à@… €@â W%H @ô Õ”àBñŸàBôÕ• B@€ÆÀBôÕ@x@¿AAJ–àE¼Zda ”A!Ê’@ÿÿÿÏ™@ 8­ìAJ— =áJ— €O Br" €= F` 1+! áJ4Õ˜à@øž€@å@ @å@ŸÿÌ¥pA•™àJdÀJ Šà‹ò~š B@RÀB{â ÿÿð@x@ÇA AR›àEOWaàˆáRA@ííaRœ =@€ƒI[€=âDà@‹Z€@¨ cá‚ @AØK¾AKžàC÷Y€CàÂÁ×➟ B@€ÇÀB™@Úb ¢ÿÿKo@x@ÀAAK àEÕ‚áKîîaK¡ =@p€ƒš€=áK'žâž#é¢à@ö€@‡ àÄ!K @ã é£àBVàBå4¤ B@€ÆÀBë !KÿÿJ@x@¿AAJ¥àE\΀ËE˜8§ Ëâ–c¿ïøaJ¦ =áJ½l‰aˆ&\à=ÁÎãé§à@k€@ã éøA•¨àJ‰j J ŠÀÉÀ‹ãé© B@RÀBÿÿ.@x@ÇA ARªàEŸ!ŠaàˆáRA@ùùaR« =@€ƒO&€=â$ûâJ¬à@Ì$€@k@S¡ÑÁ" @ }@AÙKÿƒµ:ºrãAQ­àI7 IàÈÀIãï® B@€ÍÀB_å ;ãï@x@ÆA AQ¯àE&Ý€Òàˆá Qúú@=°à=|ifÌjí ½@ô@Æ¿ÿœÿÔÝ#@¡F̱ =A;€Á†á€{ââáW²à@`Þ€@ !eà~‡ )g @% @ ÃAГ• `¨•Aœ³àVÂá ÀV$A Vä„iqU^´ B@`ÚÀB Eòမ ÿð@x@ÓAA µ E `Eæã€ßá % â°%N¶à@D @å$N·àB¢â€ƒâ ÀÉ! ƒá vk¸ B@€ÆÀBì a! åN@x@¿ABk¹àE­˜‹cû™ÿÿ+øûýcûº =áJ‹`=áJ"BáJ)8»à@î€@å,N¼àJZÀJ þâåN½ B@RÀBû·@x@ÇA AR¾àEAË€ÓŒ@Y¢¥ã üþþaR¿ =áRüÏ€=áR%NÀà@y΀@l@S ~ÂZå Nñ )¾BçÁàIåÍ€Iá QåN B@QÀBåN@x@ÆA AQÃàEȆŽbfáQÿÿaQÄ = Ó ƒ€Š€=H á'ó#kâ¤#ïÅà@݉€@á !Q @ã¬"ã@BÆàB<àBãïÇ B@JÀBãï@x@¿AAJÈàENBaJàËâ œ(³@=Éà=ÔTŒ`=ôä-æ ™†˜ExÊ = €ÁçF€{è)&áˆ"ÚËà@_E€@è) @è)"ÚóqAÌàI¿D€Iå Â! IáÒ¦ŒÍ B@`ÍÀBo@îãg%æŒ@x@ÆA AQÎàEÕý€ˆÿÿ8AQÏ =áQ1¾aáQå Ðà@”¼€@å,ÑàJÀJ þáåÒ B@aRàBÿÿšf@x@ÇA ARÓàEãt‘aáÛâ âaRÔ = { ƒšy€=H àÆ$-áR"¤Õà@x€@å""ANeÖàIƒw€Iá Qå× B@QÀBå@x@ÆA AQØàEi0’aQàˆá Qd4Ùà=ø¹‚cä@—ã(êÎA AÚ =@ €Á)3€{á#©ä4Ûà@£1€@á ŠÎä4ÃD4)ÍD4ÜàI IáÇYä4Ý B@`ÍÀBl!òá ê Á@x@ÆA AÞ Eáz5€Òãö&á&Ñßà@×4€@à ÉæÑààB=àB! ƒåAŠÁá B@a àB„@Ùâá êÁ@x@¿ABœâàEðë€ËåA,Ÿ"^ã =@p€ƒF¬“bÚáJãî'Ýäà@§ª€@å,AåàJÀJA#áåAæ B@€ÎÀBÿÿÑ@x@ÇA ARçàEþb”O@TåA ARè =áR³g€=áR%Aéà@.f€@p@Sá"!“ã±0ñ.ªÓBçêàIše€IàÈÂaåAë B@QÀB`ã ±Žs@x@ÆA AQìàE„•aQêÁeAíà=¨‚cæ áåAî =A;€Á(#€{Hà=ƒf&Ñä-!ïà@œ!€@o@Sà~€@! @á ªªá AðàIü €Iâ á$4 Iä4ñ B@`ÍÀBtá !@x@ÆA B£ò E@¿`E%€ˆâ£$½åAóà@Ü$€@â Y! @àÉ$5A ôàB< Bá àB¥Aõ B@€ÆÀBì@x@¿ABœöàE Ú€ËåA  cî÷ =áJfš–bÚâ^#­áJ! øà@Ƙ€@å,AùàJ2ÀJA#â©åAú B@RÀBõR@x@ÇA ARûàEQ—aãï  aRü =áRÐU€=âáR#±ýà@MT€@í( @ã±3R®B¤þàI¹S€Iâ çåAÿ B@QÀB^ã ±åA@x@ÆA AQz  `EŸ ˜aQáÚê ƒ  eAà=.–‚cè@PâåA =á_€{ââ$¶ââ!à@Ù €@á ꃯ}AàI; IáÅ‹åA B@IÀBêƒ@x@ÆA EA Eá³€Òá$½á*ƒà@ @â YåAàBo€ƒá ƒå„¥A B@a àBåA@x@¿ABœ àE&È€ˆÿÿ¸  cî =áJƒˆ™bÚãî%A à@冀@å,A àJQÀJA#ã(‡ëåA B@RÀBõE@x@ÇA ARàE3?šaàˆã ïaR =áR0D€=â%â#±à@«B€@ê!ƒAÝÿ8àI IàÈÁQåA B@QÀBaåA@x@ÆA AQàEºú€Òàˆá Q >à=M„‚cå@—âåA =á^ÿ€{áå Aà@Ñý€@á @@˜ [/L@ àI2 IáÁåA B@aÀBêƒ@x@ÆA EA Eá¾›báá$½â£%Aà@ @à ÉåAàBz€ƒà B! ƒå A B@a àBåA@x@¿AB^àEA¶€ˆÿÿ cî =áJ vœaJå"A à@u€@‡ âÝbç' $/ÿÿÁÿÖ!àJlt J IàJåA" B@RÀBÿÿÁ@x@ÇA AR#àEN-aáÛã ïaR$ =@þ€ƒ 2€=áR%A%à@†0€@ã±!Rã±€@~áÿ!Ï&àIò/€IàÈÁœåA' B@€ÍÀBbåA@x@ÆA AQ(àEÕèà ˆá QeA)à=dr‚ÿÿ7åA* =á™ë€{ä-%ä-!+à@ê€@ï Å"â @á5!àïDw,àIqé€Iá `Iä4Å- B@aÀBpá ,ÏÅ@x@ÆA EA. Eáá$½á%A/à@^í€@ã « `@àÉ)w@B0àB½ì€Bá ƒàBªƒ1 B@a àBåA@x@¿ABœ2àE\¤žcîåAcî3 =áJ´dŸ`=áJ#­áJ! 4à@c€@å,A5àJ‚b€Já RåA6 B@RÀBõ@x@ÇA AR7àEi aàˆã ïaR8 =áR% €=áR#±9à@¡€@ø ¹"!“ã±HóCMÜ—B¤:àI  IàÈåA; B@QÀBõ@x@ÆA AQ<àEðÖ€Òàˆá QeA=à=~`‚õ> =á„Û€{á%ââ!?à@ÿÙ€@ï Å! @á ÝA@àI` IáÁåAA B@aÀBïÅ@ 5@ÆA EAB  `EðÝ€Òá$½á%ACà@K @å$ADàB¬Ü€ƒá ƒå AE B@€ÆÀBŒ@Ùi´ ©¾&NúI@x@¿ABœFàEw’¡cîÿÿA5cîG =@p€ƒ×É£`=áJ#­áJ%AHà@7È€@ï,ÅIàJ£Ç J þáåAJ B@€ÎÀB|DSáRÿÿE@x@ÇA ARKàE‘€¤aàˆã ïaRL =áRL…€=áR#±Mà@ʃ€@ã±!“ã±€ î@eANàI6 Iâ çêƒO B@QÀBõ@x@ÆA AQPàE<¥áQeAQà=™N¢`=öÿÿÁR =áÔ>€{á%ââ!Sà@R=€@g@SâTê ƒAB'@sÄDwTàI´á ÃñåAU B@aÀBh!òàŒ$4êƒ@x@ÆA EAV E@¿`EXA€Òá$½á%AWà@¶@€@â Y&U @äþ$5@BXàB Bãeá àBªƒY B@€ÆÀBåA@x@¿ABœZàEŸ÷€Ëêƒ cî[ =áJÆ·¦bœáJâ ^\à@8¶€@ì â!J @ãî!áÿÀ]àIšµ€IáQ Iᔬâ^ B@QÀBò$@x@ÆA AQ_àE¬n§aÿÿžD!"aQ` =áQ /¨`=áQ$ÿâœ"]aà@k-€@æ,“bàJ×, J ãûæ“c B@RÀBñ@x@ÇA ARdàEºåà ˆæ “##aRe =áR{ê€=áR%fà@öè€@ë Õƒµ@ƒ7CögàIb Iá Qæ“h B@QÀBöY@x@ÆA AQiàEA¡©bf‹@Y¡Úá Q$$f“jà=Ó*ñk =á©¥€{á#©ââW!yà@¢€@ÿ"ÿï@T620@ì€ÑÿÛbmàVá¡å Öæ n B@aœÀB=›ÁVÅÖæ @x@ÓAF o Eá ¨€ßá &á & pà@]§€@à É%U @å $B@BqàB½¦€Bâ ©! àB¦ r B@a àBæ @x@¿AB©sàEÇ\ªb©ÿÿë%&cût =áJ«`=õ! uà@~€@å,NvàJê JA#£sÂåNw B@RÀBÿÿ o@x@ÇA ARxàEÕÓ€ˆŒ@Y ˆã ü''aRy =áRØ€=â#•â%Nzà@ ×€@å"N#¹‚ìB¤{àIyÖ€Iá QåN| B@QÀBÿÿ o@x@ÆA AQ}àE[¬bfàˆá Q((eN~à=ê€Æÿÿ o =ጕ€{ââå N€à@e’€@ â—$- @% @ K&Þ0¢äÀ @† ×ÿ>àVÈ‘€Vä „$A Vä„©‚ B@aœÀB åN@x@ÓAENƒ E@™`Eä—€ˆâ°$Êâ°%N„à@A @áÝá @àÉ$B@B…àB¨–€ƒà B! àB¥N† B@€ÆÀBÿÿ |@x@¿AB©‡àEâJ­b©ªmRã ûÏA9)÷AJˆ =@€ƒÚ¼ÿP@’âkãû! ‰à@<»€@å,NŠàJ¨º J@¤ÅÁ•åN‹ B@€ÎÀBÿÿø@x@ÇA ARŒàE‹sQ@Tàˆã üøøAR =áR>m`=áR%NŽà@“x€@å Nñ }Ÿÿ>àIÿw€IàÈÁQåN B@QÀBåN@x@ÆA AQ‘àE/aàˆá QùùAQ’ = Ó ƒÂ2€=H âckãï“à@  @ä ¹%ã¬"ãCï”àB‚1€ƒàÁÁJãï• B@JàB âßõ!@x@¿AAJ–àE˜êà ˆá Júú`=—à=®d-éoä-ý ÿ£Aˆ˜ =@®€Á]í€{âÚ&áˆ"Ú™à@Õë€@ô ò |ÿ"Úßÿ>šàI4 Iá Šå¦Œ› B@`ÍÀBý@x@ÆA AœàE¦âÚûûAQ =áQúª€=H@—â"Úå žà@[©€@â T!Q @ÿÿ6ŸàJǨ Jà ‹å  B@aRàBð@x@ÇA AR¡àE¦aaRêÎüüAR¢ =áR_f€=áR"¤£à@Úd€@å"2ßÿ>¤àIF Iá Qå¥ B@QÀBð@x@ÆA AQ¦àE-aQðýýAQ§ =áQå €=áQ%>ãö)o¨à@@ @á å©àB¥€ƒà Båª B@JàB ãkå@x@¿AAJ«àE³Ø€ˆâœþþ@=¬à=Fb‚[ÿÿí =@®€ÁwÛ€{áˆ$ôáˆ"Ú®à@òÙ€@å !ˆ @õBÚ¿ÿ‰¯àIS Iá Iå ° B@`ÍÀBý>@x@ÆA AQ±àE:”bÚ0 áÕç@sÿ+ø² =áQˆÈ`=G@#âå³à@çÆ€@â Tå´àJSÀJà ‹åµ B@aRàBü1@x@ÇA AR¶àE:aàˆáRA@aR· =@€ƒ타=áR"¤¸à@f‚€@ÿ ÿ¶ÿÿ|¹àIÒ€IàÈÉ´åº B@€ÍÀBÿÿ¶@x@ÆA AQ»àEÁ:aQê€á QaQ¼ =áQi>€=áQå ½à@Æ=€@á #ö @`òAÊÿ¾àB% BàÁÁJå¿ B@JÀBÿÿ*}@x@¿AAJÀàEGöà Ëá J@=Áà=5Rˆ.ä-ê ÿ `$- =ሠù€{H@ä -$ôåÃà@ƒ÷€@â Ú$- @åjÿÿÿÇÄàIãá á IáÒªÿÅ B@aÀBêÿ@x@ÆA AQÆàEαaQâ[á bÚÇ =áQ©¶€=âÚ*¾áQ-›Èà@ µ€@å,ÉàJv´ JA#áÝåÊ B@aRàBÿÿô@x@ÇA ARËàEUmáRaRÌ =áRr€=áR%Íà@…p€@ê"ÿ3ΟÿºÎàIño€Iá QåÏ B@QÀB\+bá”ÿÿº@x@ÆA AQÐàEÛ(áQaQÑ =áQ”,€=áQ%>â¤*ÿÒà@ï+€@å <åÓàBPàBåÔ B@JÀBå@x@¿AAJÕàEbä€ËáÓã ïeÖà=ñ¢Ú÷× =áˆúè€{å"Øà@tç€@ê"ÿÿÿÙàIÖæ€IáåÚ B@aÀBêÿ@x@ÆA AÛàEìfL@T71³ã ÿÿÓŸÿ.ªú "8Üà=Ö™R €¤óÿÿŸÿuÿÿ @uÝà7sIg`ud–·à u€uè(N€uÞà=]U àuàußà7éŸcƆQ¸àuâ{Ð q è.àà=n¬ÑVÀàëàááà- \`k2˜à áàáâà=õgÒà káWàáãà7rš5áÍäà=\¦Ø?[ àáàuáÍåà7ù¡_áÍæà=âaÙàuáÍçà7Á‡áÍé¸aÍèà=ô¸Š\ÀêàëáÍéà-‘h$áÍêà={t‹à káWáÍëà7ø¡4áÍìà=ⲑ] Û€áÍíà7baáÍîà=hn’àuáÍïà7ÁÉ0%‹Aã›[(%¹KYð =åÙƺ_ çbê ñà@iÄ€@ê,òàJÕàJ ÀÉ Ò`Óêó B@eÚàBú‡@x@ÇA EdôàE¼|»aàˆáRA@,êaRõ =@€ƒp€=èµáR*öà@ì€@ê ƒ³ ‚ÿ·ê÷àIX IàÈÁQêø B@€ÍÀBú‡@x@ÆA AQùàEC8¼áQaQú =áQÿ;€=è~ê ûà@Y @ê$üàB»:€ƒá Jêý B@JÀBÿÿÇ@x@¿AAJþàEÉó€ˆõ@=ÿà=u…Q( ä-èµ§].KôD-{áˆ.ø€{âÚ.üë&žà@ õ€@ÿ!ÿqBÚà\KçàViô€Vᜠ—ï ” B@aœÀB E,dr ¤|,ðÿÿ `x@9 A^ @€EP¯½bçâ0¿ >å ŒBÖÿ3® =@`=tä|`@Lá^!5á^/”à@×â€@å,ŒàJCÀJ@ ÉÁåŒ B@€ÎÀBÿÿd@x@ÇA AR àE1›}aàˆå ŒaR =áRÍŸ€=ä:áR%Œ à@Mž€@å+Œ àI¹€IàÈÁQåŒ B@QÀBï”@x@ÆA AQàE¸V~áQÿ;¢à=Gm¾bâäs¾ ¿‚âä Aƒ`µDA =á[€{á#©ââ$Aà@òW€@  ^à~ˆ$/Ò @% @ßÿdäAàVT Vá œ%Þ Vä A B@aœÀBäA@x@ÓAA  Eá h]€ßá á 0óà@Â\€@à É! @àÉ$B@BàB$àB! àB¿ÿd B@a àBæë@x@¿AB©àE?b©iìåNÿ‘³%AJ =áJæ?a@’å"Nà@H€@å,NàJ´ J@â¶åN B@RÀByGœàBÿÿ–@x@Ç =AR @`E§¹à ˆå N&¿ÿ  =áR[¾€=áR%Nà@Û¼€@å+N àIG Iá QåN! B@a àB[á Qôã@x@ÆA AQ"àE.u@bfÿãßÿ # =áQæx€=ã"ï$à@@ @ê$Û%àB¢w€ƒà Bãï& B@JÀBãï@x@¿AAJ'àE´0AaJáÓâ œßÿ þ(à=6Ðd-ôêä-æ ™„`µD-){áˆì6€{áˆ&æ™*à@É3€@æ ™šc% w@F™Ãæ™+àV, VáÆð!œ —åϺp, B@aœÀBêÛ@x@ÓAAœ-àE;ìAaœêÛ)4A. = …`ƒ[!b á^éˆ&˜/à@¾€@å,Œ0àJ*ÀJ åIåŒ1 B@€ÎÀBåŒ@x@ÇA AR2àEØ Ó@Y¥Òâ ï//AR3 =áRÑÜ€=å"Œ4à@LÛ€@å+Œ5àI¸Ú€IàȤåŒ6 B@QÀBÿÿ!e@x@ÆA AQ7àE£“bfàˆá Q00`=8à=^¨Bbâ¹uêÛ9 =á㙀{ä"A:à@¾–€@ï )º#êäAKäA;àV VáÁœäA< B@aœÀBêÛ@x@ÓAB°= Eá Kœ€ßá *Wåb*Û>à@¦›€@æ ¨! @å $B@B?àB Bá àBªÛ@ B@a àBæë@x@¿AB©AàE*Ob©êÛ17}B =áJÙ?Ã`=áJ"BáJ! Cà@;>€@å,NDàJ§= JA#¤ÅÂ_åNE B@RÀBÿ¿@x@ÇA ARFàE’öà ˆã ü88aRG =áRý€=å"NHà@Šû€@å+NIàIöú€Iá QaÝâçáŸÿ>aJ B@QÀBåN@x@ÆA AQKàE²Äbfàˆá Q99aQL =áQɵ€=áQ#kâ¤#ïMà@( @ã$ïNàB‰´€ƒà B$ü ƒáJ£ïO B@JàB@Ùâßÿÿ@@x@¿AAJPàEŸmÅáJ::@=Qà=L `=’îêÛR =@®€Áÿq€{ä-&Xáˆ1uSà@Ùn€@æ ™ê$ÛTàV; Vâ^Ä9!œ —ᜟÿ-NU B@`ÚÀBæ™@x@ÓAA^VàE~³‘{|ü0Wà=g¿Jc`’@¢ ü¦ýþXà7o›ò_ü0Yà=ízKàuü0Zà7&)ÆbJæx;ŸÿÅ“[ =âJ͆`uãÓ"!âJ&x\à@/€@æ,x]àJ› JAx¡wÂ>æx^ B@b>àByð ñT@x@ÇA B>_àEŽÐà ˆå &BŸÿÅ“` =áR>Õ€=áR1Taà@ºÓ€@æ+xbàI& Iá Qæxc B@QÀBü0@x@ÆA AQdàEŒ‡bfå¶á QCŸÿÅ’e =áQµ€=äïâ¤&xfà@ @à~ÂYêhgàBqŽ€ƒá J$Û ƒäÛèFh B@JÀBêh@x@¿AAJiàE›GˆaJàˆá JDD@=jà=Iå„-Èõ‚ñTk =áˆüK€{âÚ%àä-&xlà@ØH€@æ x!ˆ @%a0qTímàV7 VáÁß!œ Vᜆxn B@aœÀBæx@x@ÓAA^oàE"‰aœñTEJMPp =á^?8Hd@’á^!5á^%Œqà@¡6€@å,ŒràJ ÀJ@åŒs B@aRàBåŒ@x@ÇA ARtàEï Ó@Y ˆâ ïKKaRu =áR°ó€=áR%Œvà@+ò€@å+ŒwàI—ñ€IàȤåŒx B@QÀBì@x@ÆA AQyàEŠªIbfàˆá QLL@=zà=E¿‚âñT{ =á{åÊ#©ââ$A|à@Å«€@ä Aê$º}àV& VáÁœäA~ B@aœÀBÿÿ&ä@x@ÓAD Eá F±€ßá á &ë€à@¡°€@à É%b @å $B@BàBàB! àB¦ë‚ B@a àB@Ù£µÁOíd@x@¿ABkƒàEfJb©êÛMSAJ„ =@p€ƒ½V e ãû! …à@U€@à Ä"k"ƒÿÿ†àJŠT J@áêÛ‡ B@€ÎÀBñT@x@ÇA ARˆàEy aàˆã üTTaR‰ =áR*€=â!ã¾%NŠà@¥€@å+N‹àI IàÈÂaåNŒ B@QÀBÿÿ@x@ÆA AQàEÿÈ€Òàˆá QUUaQŽ =áQ¬Ì€=â¤#káQ#ïà@ @à~ÁãïàBlË€ƒë ãï‘ B@JàB@Ùâßãï@x@¿AAJ’àE†„ âœVV@=“à=4"Kd-ÅêÛ” =@®€Á戀{âÚæ ™•à@À…€@ü 0!ˆ @%! @\0êÛ–àV" Vá ß!œ VåϪۗ B@`ÚÀBêÛ@x@ÓAA^˜àE @ aœâÿÿ W\EŒ™ = …`ƒ+uÌ`=á^ä9%Œšà@Œs€@â a匛àJør J â¬åŒœ B@€ÎÀBÿÿ!¢@x@ÇA ARàEî+ÍaáÛä :]]aRž =áR­0€=ä:áR%ŒŸà@*/€@å+Œ àI–.€IàÈĄ匡 B@QÀBÿÿ8T@x@ÆA AQ¢àEuçà ˆá Q^^@=£à= þ‚âßÿÿ ¤ =áÑë€{ââ#©åÊ*Û¥à@®è€@ê8Û¦àV Vá œäA§ B@aœÀBÿÿs@x@ÓAD¨ Eá )î€ßá æ ë©à@†í€@ã ¸%b @å $BG.ªàBéì€Bá àBªÛ« B@a àB@Ùæë@x@¿ABk¬àEü¢ÎcûêÛ_eÿ ­ =@p€ƒ¦“Žf@’ãû! ®à@ ’€@à ÄåN¯àJu‘ J@áåN° B@€ÎÀBÿÿ \@x@ÇA AR±àEdJaâã üffaR² =áR#O€=â#•ã¾%N³à@ M€@å+N´àI  Iá QåNµ B@QÀBõ·@x@ÆA AQ¶àEêaQàˆá QggaQ· = …€ƒ– €=áQ#káQ*Û¸à@ô€@à~ÆVãï¹àBZàB$ü ƒä2£ïº B@€ÆÀBÿÿ0a@x@¿AAJ»àEqÁà Ëá Jhhÿ §¼à=ö`Ïd-ðÿÿ ½ =ሥǀ{áˆæ ™¾à@}Ä€@ !ãâ!ˆ @êÛ¿àVÝÀVá ß!œ VáœqWTÀ B@aœÀBñu@x@ÓAAœÁàEÀ›ò3,i"_ÿÿ¿ÿþ–`w@=Âà=‰{KuõOlà =¿ÿÆWZ“`=Ãà=ø|‘cclÓ&À{â6 7ižbÄà=9’`=9¡$à=÷ Ð @ B.Åà=ÜO¸`=` ÷ÿÿÿÌAëˆ@`¹`@!2à@(à/ à<@ àiƒ càˆà‚ àà@`V <no e> @þÿÿ@á@`$ àá(~ûÀà àáÀ`QàŠ@ @ 2@Š @`à m!¢ªª  à"@ àŠ@ cà  !z" "€1 W  ÿéó " @™à¾ UŸÿÉ<B!`otL ad!jr áj@y@Y àß @ß+Å! !@8`~ @ à&ßT 7 T  ßÀß;`ß  ø æ ø” ‚   º@· ] ƒ!`’áÐ mÁR!c iv á  þ0 V@!s b ë` ‰ <Ð(`e ßÿ'hIä`à@AààààÀS A@¡E3@Q@@I/à¡A @B @àÀM„£ ; 7‰Ø@G@à1I  à@àM¥d°¬ @ài› `¿ÿD@iè"s ÀiáYY @(€ÊàMu @zÞ àiààÁ_ B@™fwupd-2.0.10/plugins/logitech-hidpp/data/lsusb-U0007-bootloader.txt000066400000000000000000000034761501337203100247500ustar00rootroot00000000000000Bus 001 Device 036: ID 046d:aaaa Logitech, Inc. Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 0 bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 32 idVendor 0x046d Logitech, Inc. idProduct 0xaaaa bcdDevice 1.02 iManufacturer 1 iProduct 2 iSerial 0 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 34 bNumInterfaces 1 bConfigurationValue 1 iConfiguration 4 bmAttributes 0x80 (Bus Powered) MaxPower 98mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 1 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 0 bInterfaceProtocol 0 iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.11 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 25 Report Descriptors: ** UNAVAILABLE ** Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0020 1x 32 bytes bInterval 1 fwupd-2.0.10/plugins/logitech-hidpp/data/lsusb-U0007.txt000066400000000000000000000101341501337203100226050ustar00rootroot00000000000000Bus 001 Device 049: ID 046d:c52b Logitech, Inc. Unifying Receiver Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 0 bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 8 idVendor 0x046d Logitech, Inc. idProduct 0xc52b Unifying Receiver bcdDevice 12.07 iManufacturer 1 Logitech iProduct 2 USB Receiver iSerial 0 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 84 bNumInterfaces 3 bConfigurationValue 1 iConfiguration 4 RQR12.07_B0029 bmAttributes 0xa0 (Bus Powered) Remote Wakeup MaxPower 98mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 1 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 1 Boot Interface Subclass bInterfaceProtocol 1 Keyboard iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.11 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 59 Report Descriptor: (length is 59) Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0008 1x 8 bytes bInterval 8 Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 1 bAlternateSetting 0 bNumEndpoints 1 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 1 Boot Interface Subclass bInterfaceProtocol 2 Mouse iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.11 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 148 Report Descriptors: ** UNAVAILABLE ** Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x82 EP 2 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0008 1x 8 bytes bInterval 2 Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 2 bAlternateSetting 0 bNumEndpoints 1 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 0 bInterfaceProtocol 0 iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.11 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 93 Report Descriptors: ** UNAVAILABLE ** Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x83 EP 3 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0020 1x 32 bytes bInterval 2 Device Status: 0x0000 (Bus Powered) fwupd-2.0.10/plugins/logitech-hidpp/data/lsusb-U0008-bootloader-old.txt000066400000000000000000000057551501337203100255270ustar00rootroot00000000000000 Bus 003 Device 036: ID 046d:aaac Logitech, Inc. Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 0 bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 32 idVendor 0x046d Logitech, Inc. idProduct 0xaaac bcdDevice 3.01 iManufacturer 1 Logitech iProduct 2 USB BootLoader iSerial 0 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 34 bNumInterfaces 1 bConfigurationValue 1 iConfiguration 4 BOT03.01_B0008 bmAttributes 0x80 (Bus Powered) MaxPower 98mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 1 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 0 bInterfaceProtocol 0 iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.11 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 25 Report Descriptor: (length is 25) Item(Global): Usage Page, data= [ 0xb0 0xff ] 65456 (null) Item(Local ): Usage, data= [ 0x01 ] 1 (null) Item(Main ): Collection, data= [ 0x01 ] 1 Application Item(Global): Report Size, data= [ 0x08 ] 8 Item(Global): Report Count, data= [ 0x20 ] 32 Item(Global): Logical Minimum, data= [ 0x00 ] 0 Item(Global): Logical Maximum, data= [ 0xff 0x00 ] 255 Item(Local ): Usage, data= [ 0x01 ] 1 (null) Item(Main ): Input, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Local ): Usage, data= [ 0x01 ] 1 (null) Item(Main ): Output, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Main ): End Collection, data=none Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0020 1x 32 bytes bInterval 1 Device Status: 0x0000 (Bus Powered) fwupd-2.0.10/plugins/logitech-hidpp/data/lsusb-U0008-bootloader.txt000066400000000000000000000057551501337203100247530ustar00rootroot00000000000000 Bus 003 Device 039: ID 046d:aaac Logitech, Inc. Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 0 bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 32 idVendor 0x046d Logitech, Inc. idProduct 0xaaac bcdDevice 3.00 iManufacturer 1 Logitech iProduct 2 USB BootLoader iSerial 0 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 34 bNumInterfaces 1 bConfigurationValue 1 iConfiguration 4 BOT03.00_B0006 bmAttributes 0x80 (Bus Powered) MaxPower 98mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 1 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 0 bInterfaceProtocol 0 iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.11 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 25 Report Descriptor: (length is 25) Item(Global): Usage Page, data= [ 0xb0 0xff ] 65456 (null) Item(Local ): Usage, data= [ 0x01 ] 1 (null) Item(Main ): Collection, data= [ 0x01 ] 1 Application Item(Global): Report Size, data= [ 0x08 ] 8 Item(Global): Report Count, data= [ 0x20 ] 32 Item(Global): Logical Minimum, data= [ 0x00 ] 0 Item(Global): Logical Maximum, data= [ 0xff 0x00 ] 255 Item(Local ): Usage, data= [ 0x01 ] 1 (null) Item(Main ): Input, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Local ): Usage, data= [ 0x01 ] 1 (null) Item(Main ): Output, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Main ): End Collection, data=none Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0020 1x 32 bytes bInterval 1 Device Status: 0x0000 (Bus Powered) fwupd-2.0.10/plugins/logitech-hidpp/data/lsusb-U0008-old.txt000066400000000000000000000426521501337203100233740ustar00rootroot00000000000000 Bus 003 Device 033: ID 046d:c52b Logitech, Inc. Unifying Receiver Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 0 bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 32 idVendor 0x046d Logitech, Inc. idProduct 0xc52b Unifying Receiver bcdDevice 24.01 iManufacturer 1 Logitech iProduct 2 USB Receiver iSerial 0 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 84 bNumInterfaces 3 bConfigurationValue 1 iConfiguration 4 RQR24.01_B0023 bmAttributes 0xa0 (Bus Powered) Remote Wakeup MaxPower 98mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 1 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 1 Boot Interface Subclass bInterfaceProtocol 1 Keyboard iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.11 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 59 Report Descriptor: (length is 59) Item(Global): Usage Page, data= [ 0x01 ] 1 Generic Desktop Controls Item(Local ): Usage, data= [ 0x06 ] 6 Keyboard Item(Main ): Collection, data= [ 0x01 ] 1 Application Item(Global): Usage Page, data= [ 0x07 ] 7 Keyboard Item(Local ): Usage Minimum, data= [ 0xe0 ] 224 Control Left Item(Local ): Usage Maximum, data= [ 0xe7 ] 231 GUI Right Item(Global): Logical Minimum, data= [ 0x00 ] 0 Item(Global): Logical Maximum, data= [ 0x01 ] 1 Item(Global): Report Size, data= [ 0x01 ] 1 Item(Global): Report Count, data= [ 0x08 ] 8 Item(Main ): Input, data= [ 0x02 ] 2 Data Variable Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Main ): Input, data= [ 0x03 ] 3 Constant Variable Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Global): Report Count, data= [ 0x05 ] 5 Item(Global): Usage Page, data= [ 0x08 ] 8 LEDs Item(Local ): Usage Minimum, data= [ 0x01 ] 1 NumLock Item(Local ): Usage Maximum, data= [ 0x05 ] 5 Kana Item(Main ): Output, data= [ 0x02 ] 2 Data Variable Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Global): Report Count, data= [ 0x01 ] 1 Item(Global): Report Size, data= [ 0x03 ] 3 Item(Main ): Output, data= [ 0x01 ] 1 Constant Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Global): Report Count, data= [ 0x06 ] 6 Item(Global): Report Size, data= [ 0x08 ] 8 Item(Global): Logical Minimum, data= [ 0x00 ] 0 Item(Global): Logical Maximum, data= [ 0xa4 0x00 ] 164 Item(Global): Usage Page, data= [ 0x07 ] 7 Keyboard Item(Local ): Usage Minimum, data= [ 0x00 ] 0 No Event Item(Local ): Usage Maximum, data= [ 0xa4 0x00 ] 164 ExSel Item(Main ): Input, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Main ): End Collection, data=none Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0008 1x 8 bytes bInterval 8 Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 1 bAlternateSetting 0 bNumEndpoints 1 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 1 Boot Interface Subclass bInterfaceProtocol 2 Mouse iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.11 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 148 Report Descriptor: (length is 148) Item(Global): Usage Page, data= [ 0x01 ] 1 Generic Desktop Controls Item(Local ): Usage, data= [ 0x02 ] 2 Mouse Item(Main ): Collection, data= [ 0x01 ] 1 Application Item(Global): Report ID, data= [ 0x02 ] 2 Item(Local ): Usage, data= [ 0x01 ] 1 Pointer Item(Main ): Collection, data= [ 0x00 ] 0 Physical Item(Global): Usage Page, data= [ 0x09 ] 9 Buttons Item(Local ): Usage Minimum, data= [ 0x01 ] 1 Button 1 (Primary) Item(Local ): Usage Maximum, data= [ 0x10 ] 16 (null) Item(Global): Logical Minimum, data= [ 0x00 ] 0 Item(Global): Logical Maximum, data= [ 0x01 ] 1 Item(Global): Report Count, data= [ 0x10 ] 16 Item(Global): Report Size, data= [ 0x01 ] 1 Item(Main ): Input, data= [ 0x02 ] 2 Data Variable Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Global): Usage Page, data= [ 0x01 ] 1 Generic Desktop Controls Item(Global): Logical Minimum, data= [ 0x01 0xf8 ] 63489 Item(Global): Logical Maximum, data= [ 0xff 0x07 ] 2047 Item(Global): Report Size, data= [ 0x0c ] 12 Item(Global): Report Count, data= [ 0x02 ] 2 Item(Local ): Usage, data= [ 0x30 ] 48 Direction-X Item(Local ): Usage, data= [ 0x31 ] 49 Direction-Y Item(Main ): Input, data= [ 0x06 ] 6 Data Variable Relative No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Global): Logical Minimum, data= [ 0x81 ] 129 Item(Global): Logical Maximum, data= [ 0x7f ] 127 Item(Global): Report Size, data= [ 0x08 ] 8 Item(Global): Report Count, data= [ 0x01 ] 1 Item(Local ): Usage, data= [ 0x38 ] 56 Wheel Item(Main ): Input, data= [ 0x06 ] 6 Data Variable Relative No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Global): Usage Page, data= [ 0x0c ] 12 Consumer Item(Local ): Usage, data= [ 0x38 0x02 ] 568 AC Pan Item(Global): Report Count, data= [ 0x01 ] 1 Item(Main ): Input, data= [ 0x06 ] 6 Data Variable Relative No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Main ): End Collection, data=none Item(Main ): End Collection, data=none Item(Global): Usage Page, data= [ 0x0c ] 12 Consumer Item(Local ): Usage, data= [ 0x01 ] 1 Consumer Control Item(Main ): Collection, data= [ 0x01 ] 1 Application Item(Global): Report ID, data= [ 0x03 ] 3 Item(Global): Report Size, data= [ 0x10 ] 16 Item(Global): Report Count, data= [ 0x02 ] 2 Item(Global): Logical Minimum, data= [ 0x01 ] 1 Item(Global): Logical Maximum, data= [ 0x8c 0x02 ] 652 Item(Local ): Usage Minimum, data= [ 0x01 ] 1 Consumer Control Item(Local ): Usage Maximum, data= [ 0x8c 0x02 ] 652 (null) Item(Main ): Input, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Main ): End Collection, data=none Item(Global): Usage Page, data= [ 0x01 ] 1 Generic Desktop Controls Item(Local ): Usage, data= [ 0x80 ] 128 System Control Item(Main ): Collection, data= [ 0x01 ] 1 Application Item(Global): Report ID, data= [ 0x04 ] 4 Item(Global): Report Size, data= [ 0x02 ] 2 Item(Global): Report Count, data= [ 0x01 ] 1 Item(Global): Logical Minimum, data= [ 0x01 ] 1 Item(Global): Logical Maximum, data= [ 0x03 ] 3 Item(Local ): Usage, data= [ 0x82 ] 130 System Sleep Item(Local ): Usage, data= [ 0x81 ] 129 System Power Down Item(Local ): Usage, data= [ 0x83 ] 131 System Wake Up Item(Main ): Input, data= [ 0x60 ] 96 Data Array Absolute No_Wrap Linear No_Preferred_State Null_State Non_Volatile Bitfield Item(Global): Report Size, data= [ 0x06 ] 6 Item(Main ): Input, data= [ 0x03 ] 3 Constant Variable Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Main ): End Collection, data=none Item(Global): Usage Page, data= [ 0xbc 0xff ] 65468 (null) Item(Local ): Usage, data= [ 0x88 ] 136 (null) Item(Main ): Collection, data= [ 0x01 ] 1 Application Item(Global): Report ID, data= [ 0x08 ] 8 Item(Local ): Usage Minimum, data= [ 0x01 ] 1 (null) Item(Local ): Usage Maximum, data= [ 0xff ] 255 (null) Item(Global): Logical Minimum, data= [ 0x01 ] 1 Item(Global): Logical Maximum, data= [ 0xff 0x00 ] 255 Item(Global): Report Size, data= [ 0x08 ] 8 Item(Global): Report Count, data= [ 0x01 ] 1 Item(Main ): Input, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Main ): End Collection, data=none Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x82 EP 2 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0008 1x 8 bytes bInterval 2 Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 2 bAlternateSetting 0 bNumEndpoints 1 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 0 bInterfaceProtocol 0 iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.11 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 98 Report Descriptor: (length is 98) Item(Global): Usage Page, data= [ 0x00 0xff ] 65280 (null) Item(Local ): Usage, data= [ 0x01 ] 1 (null) Item(Main ): Collection, data= [ 0x01 ] 1 Application Item(Global): Report ID, data= [ 0x10 ] 16 Item(Global): Report Size, data= [ 0x08 ] 8 Item(Global): Report Count, data= [ 0x06 ] 6 Item(Global): Logical Minimum, data= [ 0x00 ] 0 Item(Global): Logical Maximum, data= [ 0xff 0x00 ] 255 Item(Local ): Usage, data= [ 0x01 ] 1 (null) Item(Main ): Input, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Local ): Usage, data= [ 0x01 ] 1 (null) Item(Main ): Output, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Main ): End Collection, data=none Item(Global): Usage Page, data= [ 0x00 0xff ] 65280 (null) Item(Local ): Usage, data= [ 0x02 ] 2 (null) Item(Main ): Collection, data= [ 0x01 ] 1 Application Item(Global): Report ID, data= [ 0x11 ] 17 Item(Global): Report Size, data= [ 0x08 ] 8 Item(Global): Report Count, data= [ 0x13 ] 19 Item(Global): Logical Minimum, data= [ 0x00 ] 0 Item(Global): Logical Maximum, data= [ 0xff 0x00 ] 255 Item(Local ): Usage, data= [ 0x02 ] 2 (null) Item(Main ): Input, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Local ): Usage, data= [ 0x02 ] 2 (null) Item(Main ): Output, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Main ): End Collection, data=none Item(Global): Usage Page, data= [ 0x00 0xff ] 65280 (null) Item(Local ): Usage, data= [ 0x04 ] 4 (null) Item(Main ): Collection, data= [ 0x01 ] 1 Application Item(Global): Report ID, data= [ 0x20 ] 32 Item(Global): Report Size, data= [ 0x08 ] 8 Item(Global): Report Count, data= [ 0x0e ] 14 Item(Global): Logical Minimum, data= [ 0x00 ] 0 Item(Global): Logical Maximum, data= [ 0xff 0x00 ] 255 Item(Local ): Usage, data= [ 0x41 ] 65 (null) Item(Main ): Input, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Local ): Usage, data= [ 0x41 ] 65 (null) Item(Main ): Output, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Global): Report ID, data= [ 0x21 ] 33 Item(Global): Report Count, data= [ 0x1f ] 31 Item(Global): Logical Minimum, data= [ 0x00 ] 0 Item(Global): Logical Maximum, data= [ 0xff 0x00 ] 255 Item(Local ): Usage, data= [ 0x42 ] 66 (null) Item(Main ): Input, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Local ): Usage, data= [ 0x42 ] 66 (null) Item(Main ): Output, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Main ): End Collection, data=none Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x83 EP 3 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0020 1x 32 bytes bInterval 2 Device Status: 0x0000 (Bus Powered) fwupd-2.0.10/plugins/logitech-hidpp/data/lsusb-U0008.txt000066400000000000000000000426511501337203100226170ustar00rootroot00000000000000Bus 003 Device 032: ID 046d:c52b Logitech, Inc. Unifying Receiver Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 0 bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 32 idVendor 0x046d Logitech, Inc. idProduct 0xc52b Unifying Receiver bcdDevice 24.05 iManufacturer 1 Logitech iProduct 2 USB Receiver iSerial 0 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 84 bNumInterfaces 3 bConfigurationValue 1 iConfiguration 4 RQR24.05_B0029 bmAttributes 0xa0 (Bus Powered) Remote Wakeup MaxPower 98mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 1 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 1 Boot Interface Subclass bInterfaceProtocol 1 Keyboard iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.11 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 59 Report Descriptor: (length is 59) Item(Global): Usage Page, data= [ 0x01 ] 1 Generic Desktop Controls Item(Local ): Usage, data= [ 0x06 ] 6 Keyboard Item(Main ): Collection, data= [ 0x01 ] 1 Application Item(Global): Usage Page, data= [ 0x07 ] 7 Keyboard Item(Local ): Usage Minimum, data= [ 0xe0 ] 224 Control Left Item(Local ): Usage Maximum, data= [ 0xe7 ] 231 GUI Right Item(Global): Logical Minimum, data= [ 0x00 ] 0 Item(Global): Logical Maximum, data= [ 0x01 ] 1 Item(Global): Report Size, data= [ 0x01 ] 1 Item(Global): Report Count, data= [ 0x08 ] 8 Item(Main ): Input, data= [ 0x02 ] 2 Data Variable Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Main ): Input, data= [ 0x03 ] 3 Constant Variable Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Global): Report Count, data= [ 0x05 ] 5 Item(Global): Usage Page, data= [ 0x08 ] 8 LEDs Item(Local ): Usage Minimum, data= [ 0x01 ] 1 NumLock Item(Local ): Usage Maximum, data= [ 0x05 ] 5 Kana Item(Main ): Output, data= [ 0x02 ] 2 Data Variable Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Global): Report Count, data= [ 0x01 ] 1 Item(Global): Report Size, data= [ 0x03 ] 3 Item(Main ): Output, data= [ 0x01 ] 1 Constant Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Global): Report Count, data= [ 0x06 ] 6 Item(Global): Report Size, data= [ 0x08 ] 8 Item(Global): Logical Minimum, data= [ 0x00 ] 0 Item(Global): Logical Maximum, data= [ 0xa4 0x00 ] 164 Item(Global): Usage Page, data= [ 0x07 ] 7 Keyboard Item(Local ): Usage Minimum, data= [ 0x00 ] 0 No Event Item(Local ): Usage Maximum, data= [ 0xa4 0x00 ] 164 ExSel Item(Main ): Input, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Main ): End Collection, data=none Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0008 1x 8 bytes bInterval 8 Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 1 bAlternateSetting 0 bNumEndpoints 1 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 1 Boot Interface Subclass bInterfaceProtocol 2 Mouse iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.11 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 148 Report Descriptor: (length is 148) Item(Global): Usage Page, data= [ 0x01 ] 1 Generic Desktop Controls Item(Local ): Usage, data= [ 0x02 ] 2 Mouse Item(Main ): Collection, data= [ 0x01 ] 1 Application Item(Global): Report ID, data= [ 0x02 ] 2 Item(Local ): Usage, data= [ 0x01 ] 1 Pointer Item(Main ): Collection, data= [ 0x00 ] 0 Physical Item(Global): Usage Page, data= [ 0x09 ] 9 Buttons Item(Local ): Usage Minimum, data= [ 0x01 ] 1 Button 1 (Primary) Item(Local ): Usage Maximum, data= [ 0x10 ] 16 (null) Item(Global): Logical Minimum, data= [ 0x00 ] 0 Item(Global): Logical Maximum, data= [ 0x01 ] 1 Item(Global): Report Count, data= [ 0x10 ] 16 Item(Global): Report Size, data= [ 0x01 ] 1 Item(Main ): Input, data= [ 0x02 ] 2 Data Variable Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Global): Usage Page, data= [ 0x01 ] 1 Generic Desktop Controls Item(Global): Logical Minimum, data= [ 0x01 0xf8 ] 63489 Item(Global): Logical Maximum, data= [ 0xff 0x07 ] 2047 Item(Global): Report Size, data= [ 0x0c ] 12 Item(Global): Report Count, data= [ 0x02 ] 2 Item(Local ): Usage, data= [ 0x30 ] 48 Direction-X Item(Local ): Usage, data= [ 0x31 ] 49 Direction-Y Item(Main ): Input, data= [ 0x06 ] 6 Data Variable Relative No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Global): Logical Minimum, data= [ 0x81 ] 129 Item(Global): Logical Maximum, data= [ 0x7f ] 127 Item(Global): Report Size, data= [ 0x08 ] 8 Item(Global): Report Count, data= [ 0x01 ] 1 Item(Local ): Usage, data= [ 0x38 ] 56 Wheel Item(Main ): Input, data= [ 0x06 ] 6 Data Variable Relative No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Global): Usage Page, data= [ 0x0c ] 12 Consumer Item(Local ): Usage, data= [ 0x38 0x02 ] 568 AC Pan Item(Global): Report Count, data= [ 0x01 ] 1 Item(Main ): Input, data= [ 0x06 ] 6 Data Variable Relative No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Main ): End Collection, data=none Item(Main ): End Collection, data=none Item(Global): Usage Page, data= [ 0x0c ] 12 Consumer Item(Local ): Usage, data= [ 0x01 ] 1 Consumer Control Item(Main ): Collection, data= [ 0x01 ] 1 Application Item(Global): Report ID, data= [ 0x03 ] 3 Item(Global): Report Size, data= [ 0x10 ] 16 Item(Global): Report Count, data= [ 0x02 ] 2 Item(Global): Logical Minimum, data= [ 0x01 ] 1 Item(Global): Logical Maximum, data= [ 0x8c 0x02 ] 652 Item(Local ): Usage Minimum, data= [ 0x01 ] 1 Consumer Control Item(Local ): Usage Maximum, data= [ 0x8c 0x02 ] 652 (null) Item(Main ): Input, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Main ): End Collection, data=none Item(Global): Usage Page, data= [ 0x01 ] 1 Generic Desktop Controls Item(Local ): Usage, data= [ 0x80 ] 128 System Control Item(Main ): Collection, data= [ 0x01 ] 1 Application Item(Global): Report ID, data= [ 0x04 ] 4 Item(Global): Report Size, data= [ 0x02 ] 2 Item(Global): Report Count, data= [ 0x01 ] 1 Item(Global): Logical Minimum, data= [ 0x01 ] 1 Item(Global): Logical Maximum, data= [ 0x03 ] 3 Item(Local ): Usage, data= [ 0x82 ] 130 System Sleep Item(Local ): Usage, data= [ 0x81 ] 129 System Power Down Item(Local ): Usage, data= [ 0x83 ] 131 System Wake Up Item(Main ): Input, data= [ 0x60 ] 96 Data Array Absolute No_Wrap Linear No_Preferred_State Null_State Non_Volatile Bitfield Item(Global): Report Size, data= [ 0x06 ] 6 Item(Main ): Input, data= [ 0x03 ] 3 Constant Variable Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Main ): End Collection, data=none Item(Global): Usage Page, data= [ 0xbc 0xff ] 65468 (null) Item(Local ): Usage, data= [ 0x88 ] 136 (null) Item(Main ): Collection, data= [ 0x01 ] 1 Application Item(Global): Report ID, data= [ 0x08 ] 8 Item(Local ): Usage Minimum, data= [ 0x01 ] 1 (null) Item(Local ): Usage Maximum, data= [ 0xff ] 255 (null) Item(Global): Logical Minimum, data= [ 0x01 ] 1 Item(Global): Logical Maximum, data= [ 0xff 0x00 ] 255 Item(Global): Report Size, data= [ 0x08 ] 8 Item(Global): Report Count, data= [ 0x01 ] 1 Item(Main ): Input, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Main ): End Collection, data=none Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x82 EP 2 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0008 1x 8 bytes bInterval 2 Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 2 bAlternateSetting 0 bNumEndpoints 1 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 0 bInterfaceProtocol 0 iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.11 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 98 Report Descriptor: (length is 98) Item(Global): Usage Page, data= [ 0x00 0xff ] 65280 (null) Item(Local ): Usage, data= [ 0x01 ] 1 (null) Item(Main ): Collection, data= [ 0x01 ] 1 Application Item(Global): Report ID, data= [ 0x10 ] 16 Item(Global): Report Size, data= [ 0x08 ] 8 Item(Global): Report Count, data= [ 0x06 ] 6 Item(Global): Logical Minimum, data= [ 0x00 ] 0 Item(Global): Logical Maximum, data= [ 0xff 0x00 ] 255 Item(Local ): Usage, data= [ 0x01 ] 1 (null) Item(Main ): Input, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Local ): Usage, data= [ 0x01 ] 1 (null) Item(Main ): Output, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Main ): End Collection, data=none Item(Global): Usage Page, data= [ 0x00 0xff ] 65280 (null) Item(Local ): Usage, data= [ 0x02 ] 2 (null) Item(Main ): Collection, data= [ 0x01 ] 1 Application Item(Global): Report ID, data= [ 0x11 ] 17 Item(Global): Report Size, data= [ 0x08 ] 8 Item(Global): Report Count, data= [ 0x13 ] 19 Item(Global): Logical Minimum, data= [ 0x00 ] 0 Item(Global): Logical Maximum, data= [ 0xff 0x00 ] 255 Item(Local ): Usage, data= [ 0x02 ] 2 (null) Item(Main ): Input, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Local ): Usage, data= [ 0x02 ] 2 (null) Item(Main ): Output, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Main ): End Collection, data=none Item(Global): Usage Page, data= [ 0x00 0xff ] 65280 (null) Item(Local ): Usage, data= [ 0x04 ] 4 (null) Item(Main ): Collection, data= [ 0x01 ] 1 Application Item(Global): Report ID, data= [ 0x20 ] 32 Item(Global): Report Size, data= [ 0x08 ] 8 Item(Global): Report Count, data= [ 0x0e ] 14 Item(Global): Logical Minimum, data= [ 0x00 ] 0 Item(Global): Logical Maximum, data= [ 0xff 0x00 ] 255 Item(Local ): Usage, data= [ 0x41 ] 65 (null) Item(Main ): Input, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Local ): Usage, data= [ 0x41 ] 65 (null) Item(Main ): Output, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Global): Report ID, data= [ 0x21 ] 33 Item(Global): Report Count, data= [ 0x1f ] 31 Item(Global): Logical Minimum, data= [ 0x00 ] 0 Item(Global): Logical Maximum, data= [ 0xff 0x00 ] 255 Item(Local ): Usage, data= [ 0x42 ] 66 (null) Item(Main ): Input, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Local ): Usage, data= [ 0x42 ] 66 (null) Item(Main ): Output, data= [ 0x00 ] 0 Data Array Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Main ): End Collection, data=none Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x83 EP 3 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0020 1x 32 bytes bInterval 2 Device Status: 0x0000 (Bus Powered) fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-bootloader-nordic.c000066400000000000000000000233431501337203100263600ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-logitech-hidpp-bootloader-nordic.h" #include "fu-logitech-hidpp-common.h" #include "fu-logitech-hidpp-struct.h" struct _FuLogitechHidppBootloaderNordic { FuLogitechHidppBootloader parent_instance; }; G_DEFINE_TYPE(FuLogitechHidppBootloaderNordic, fu_logitech_hidpp_bootloader_nordic, FU_TYPE_LOGITECH_HIDPP_BOOTLOADER) static gchar * fu_logitech_hidpp_bootloader_nordic_get_hw_platform_id(FuLogitechHidppBootloader *self, GError **error) { g_autoptr(FuLogitechHidppBootloaderRequest) req = fu_logitech_hidpp_bootloader_request_new(); req->cmd = FU_LOGITECH_HIDPP_BOOTLOADER_CMD_GET_HW_PLATFORM_ID; if (!fu_logitech_hidpp_bootloader_request(self, req, error)) { g_prefix_error(error, "failed to get HW ID: "); return NULL; } return g_strndup((const gchar *)req->data, req->len); } static gchar * fu_logitech_hidpp_bootloader_nordic_get_fw_version(FuLogitechHidppBootloader *self, GError **error) { guint16 micro; g_autoptr(FuLogitechHidppBootloaderRequest) req = fu_logitech_hidpp_bootloader_request_new(); req->cmd = FU_LOGITECH_HIDPP_BOOTLOADER_CMD_GET_FW_VERSION; if (!fu_logitech_hidpp_bootloader_request(self, req, error)) { g_prefix_error(error, "failed to get firmware version: "); return NULL; } /* RRRxx.yy_Bzzzz * 012345678901234*/ micro = (guint16)fu_logitech_hidpp_buffer_read_uint8((const gchar *)req->data + 10) << 8; micro += fu_logitech_hidpp_buffer_read_uint8((const gchar *)req->data + 12); return fu_logitech_hidpp_format_version( "RQR", fu_logitech_hidpp_buffer_read_uint8((const gchar *)req->data + 3), fu_logitech_hidpp_buffer_read_uint8((const gchar *)req->data + 6), micro); } static gboolean fu_logitech_hidpp_bootloader_nordic_setup(FuDevice *device, GError **error) { FuLogitechHidppBootloader *self = FU_LOGITECH_HIDPP_BOOTLOADER(device); g_autofree gchar *hw_platform_id = NULL; g_autofree gchar *version_fw = NULL; g_autoptr(GError) error_local = NULL; /* FuLogitechHidppBootloader->setup */ if (!FU_DEVICE_CLASS(fu_logitech_hidpp_bootloader_nordic_parent_class) ->setup(device, error)) return FALSE; /* get MCU */ hw_platform_id = fu_logitech_hidpp_bootloader_nordic_get_hw_platform_id(self, error); if (hw_platform_id == NULL) return FALSE; g_debug("hw-platform-id=%s", hw_platform_id); /* get firmware version, which is not fatal */ version_fw = fu_logitech_hidpp_bootloader_nordic_get_fw_version(self, &error_local); if (version_fw == NULL) { g_warning("failed to get firmware version: %s", error_local->message); fu_device_set_version(device, "RQR12.00_B0000"); } else { fu_device_set_version(device, version_fw); } return TRUE; } static gboolean fu_logitech_hidpp_bootloader_nordic_write_signature(FuLogitechHidppBootloader *self, guint16 addr, guint8 len, const guint8 *data, GError **error) { g_autoptr(FuLogitechHidppBootloaderRequest) req = fu_logitech_hidpp_bootloader_request_new(); req->cmd = 0xC0; req->addr = addr; req->len = len; memcpy(req->data, data, req->len); /* nocheck:blocked */ if (!fu_logitech_hidpp_bootloader_request(self, req, error)) { g_prefix_error(error, "failed to write sig @0x%02x: ", addr); return FALSE; } if (req->cmd == FU_LOGITECH_HIDPP_BOOTLOADER_CMD_WRITE_RAM_BUFFER_INVALID_ADDR) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to write @%04x: signature is too big", addr); return FALSE; } return TRUE; } static gboolean fu_logitech_hidpp_bootloader_nordic_write(FuLogitechHidppBootloader *self, guint16 addr, guint8 len, const guint8 *data, GError **error) { g_autoptr(FuLogitechHidppBootloaderRequest) req = fu_logitech_hidpp_bootloader_request_new(); req->cmd = FU_LOGITECH_HIDPP_BOOTLOADER_CMD_WRITE; req->addr = addr; req->len = len; if (req->len > 28) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to write @%04x: data length too large %02x", addr, req->len); return FALSE; } memcpy(req->data, data, req->len); /* nocheck:blocked */ if (!fu_logitech_hidpp_bootloader_request(self, req, error)) { g_prefix_error(error, "failed to transfer fw @0x%02x: ", addr); return FALSE; } if (req->cmd == FU_LOGITECH_HIDPP_BOOTLOADER_CMD_WRITE_INVALID_ADDR) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to write @%04x: invalid address", addr); return FALSE; } if (req->cmd == FU_LOGITECH_HIDPP_BOOTLOADER_CMD_WRITE_VERIFY_FAIL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to write @%04x: failed to verify flash content", addr); return FALSE; } if (req->cmd == FU_LOGITECH_HIDPP_BOOTLOADER_CMD_WRITE_NONZERO_START) { g_debug("wrote %d bytes at address %04x, value %02x", req->len, req->addr, req->data[0]); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to write @%04x: only 1 byte write of 0xff supported", addr); return FALSE; } if (req->cmd == FU_LOGITECH_HIDPP_BOOTLOADER_CMD_WRITE_INVALID_CRC) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to write @%04x: invalid CRC", addr); return FALSE; } return TRUE; } static gboolean fu_logitech_hidpp_bootloader_nordic_erase(FuLogitechHidppBootloader *self, guint16 addr, GError **error) { g_autoptr(FuLogitechHidppBootloaderRequest) req = fu_logitech_hidpp_bootloader_request_new(); req->cmd = FU_LOGITECH_HIDPP_BOOTLOADER_CMD_ERASE_PAGE; req->addr = addr; req->len = 0x01; if (!fu_logitech_hidpp_bootloader_request(self, req, error)) { g_prefix_error(error, "failed to erase fw @0x%02x: ", addr); return FALSE; } if (req->cmd == FU_LOGITECH_HIDPP_BOOTLOADER_CMD_ERASE_PAGE_INVALID_ADDR) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to erase @%04x: invalid page", addr); return FALSE; } if (req->cmd == FU_LOGITECH_HIDPP_BOOTLOADER_CMD_ERASE_PAGE_NONZERO_START) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to erase @%04x: byte 0x00 is not 0xff", addr); return FALSE; } return TRUE; } static gboolean fu_logitech_hidpp_bootloader_nordic_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuLogitechHidppBootloader *self = FU_LOGITECH_HIDPP_BOOTLOADER(device); const FuLogitechHidppBootloaderRequest *payload; guint16 addr; g_autoptr(GBytes) fw = NULL; g_autoptr(GPtrArray) reqs = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); if (fu_device_has_private_flag(device, FU_LOGITECH_HIDPP_BOOTLOADER_FLAG_IS_SIGNED)) { fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 4, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 13, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "device-write0"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 82, "reset vector"); } else { fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 22, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 72, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "device-write0"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 6, "reset-vector"); } /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* erase firmware pages up to the bootloader */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_ERASE); for (addr = fu_logitech_hidpp_bootloader_get_addr_lo(self); addr < fu_logitech_hidpp_bootloader_get_addr_hi(self); addr += fu_logitech_hidpp_bootloader_get_blocksize(self)) { if (!fu_logitech_hidpp_bootloader_nordic_erase(self, addr, error)) return FALSE; } fu_progress_step_done(progress); /* transfer payload */ reqs = fu_logitech_hidpp_bootloader_parse_requests(self, fw, error); if (reqs == NULL) return FALSE; fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); for (guint i = 1; i < reqs->len; i++) { gboolean res; payload = g_ptr_array_index(reqs, i); if (payload->cmd == FU_LOGITECH_HIDPP_BOOTLOADER_CMD_WRITE_SIGNATURE) { res = fu_logitech_hidpp_bootloader_nordic_write_signature(self, payload->addr, payload->len, payload->data, error); } else { res = fu_logitech_hidpp_bootloader_nordic_write(self, payload->addr, payload->len, payload->data, error); } if (!res) return FALSE; fu_progress_set_percentage_full(fu_progress_get_child(progress), i + 1, reqs->len); } fu_progress_step_done(progress); /* send the first managed packet last, excluding the reset vector */ payload = g_ptr_array_index(reqs, 0); if (!fu_logitech_hidpp_bootloader_nordic_write(self, payload->addr + 1, payload->len - 1, payload->data + 1, error)) return FALSE; fu_progress_step_done(progress); /* reset vector */ if (!fu_logitech_hidpp_bootloader_nordic_write(self, 0x0000, 0x01, payload->data, error)) return FALSE; fu_progress_step_done(progress); /* success! */ return TRUE; } static void fu_logitech_hidpp_bootloader_nordic_class_init(FuLogitechHidppBootloaderNordicClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_logitech_hidpp_bootloader_nordic_write_firmware; device_class->setup = fu_logitech_hidpp_bootloader_nordic_setup; } static void fu_logitech_hidpp_bootloader_nordic_init(FuLogitechHidppBootloaderNordic *self) { } fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-bootloader-nordic.h000066400000000000000000000007031501337203100263600ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-logitech-hidpp-bootloader.h" #define FU_TYPE_LOGITECH_HIDPP_BOOTLOADER_NORDIC (fu_logitech_hidpp_bootloader_nordic_get_type()) G_DECLARE_FINAL_TYPE(FuLogitechHidppBootloaderNordic, fu_logitech_hidpp_bootloader_nordic, FU, LOGITECH_HIDPP_BOOTLOADER_NORDIC, FuLogitechHidppBootloader) fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-bootloader-texas.c000066400000000000000000000204241501337203100262230ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-logitech-hidpp-bootloader-texas.h" #include "fu-logitech-hidpp-common.h" #include "fu-logitech-hidpp-struct.h" struct _FuLogitechHidppBootloaderTexas { FuLogitechHidppBootloader parent_instance; }; G_DEFINE_TYPE(FuLogitechHidppBootloaderTexas, fu_logitech_hidpp_bootloader_texas, FU_TYPE_LOGITECH_HIDPP_BOOTLOADER) static gboolean fu_logitech_hidpp_bootloader_texas_erase_all(FuLogitechHidppBootloader *self, GError **error) { g_autoptr(FuLogitechHidppBootloaderRequest) req = fu_logitech_hidpp_bootloader_request_new(); req->cmd = FU_LOGITECH_HIDPP_BOOTLOADER_CMD_FLASH_RAM; req->len = 0x01; /* magic number */ req->data[0] = 0x00; /* magic number */ if (!fu_logitech_hidpp_bootloader_request(self, req, error)) { g_prefix_error(error, "failed to erase all pages: "); return FALSE; } return TRUE; } static gboolean fu_logitech_hidpp_bootloader_texas_compute_and_test_crc(FuLogitechHidppBootloader *self, GError **error) { g_autoptr(FuLogitechHidppBootloaderRequest) req = fu_logitech_hidpp_bootloader_request_new(); req->cmd = FU_LOGITECH_HIDPP_BOOTLOADER_CMD_FLASH_RAM; req->len = 0x01; /* magic number */ req->data[0] = 0x03; /* magic number */ if (!fu_logitech_hidpp_bootloader_request(self, req, error)) { g_prefix_error(error, "failed to compute and test CRC: "); return FALSE; } if (req->cmd == FU_LOGITECH_HIDPP_BOOTLOADER_CMD_FLASH_RAM_WRONG_CRC) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "CRC is incorrect"); return FALSE; } return TRUE; } static gboolean fu_logitech_hidpp_bootloader_texas_flash_ram_buffer(FuLogitechHidppBootloader *self, guint16 addr, GError **error) { g_autoptr(FuLogitechHidppBootloaderRequest) req = fu_logitech_hidpp_bootloader_request_new(); req->cmd = FU_LOGITECH_HIDPP_BOOTLOADER_CMD_FLASH_RAM; req->addr = addr; req->len = 0x01; /* magic number */ req->data[0] = 0x01; /* magic number */ if (!fu_logitech_hidpp_bootloader_request(self, req, error)) { g_prefix_error(error, "failed to flash ram buffer @%04x: ", addr); return FALSE; } if (req->cmd == FU_LOGITECH_HIDPP_BOOTLOADER_CMD_FLASH_RAM_INVALID_ADDR) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to flash ram buffer @%04x: invalid flash page", addr); return FALSE; } if (req->cmd == FU_LOGITECH_HIDPP_BOOTLOADER_CMD_FLASH_RAM_PAGE0_INVALID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to flash ram buffer @%04x: invalid App JMP vector", addr); return FALSE; } if (req->cmd == FU_LOGITECH_HIDPP_BOOTLOADER_CMD_FLASH_RAM_INVALID_ORDER) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to flash ram buffer @%04x: page flashed before page 0", addr); return FALSE; } return TRUE; } static gboolean fu_logitech_hidpp_bootloader_texas_clear_ram_buffer(FuLogitechHidppBootloader *self, GError **error) { g_autoptr(FuLogitechHidppBootloaderRequest) req = fu_logitech_hidpp_bootloader_request_new(); req->cmd = FU_LOGITECH_HIDPP_BOOTLOADER_CMD_FLASH_RAM; req->addr = 0x0000; req->len = 0x01; /* magic number */ req->data[0] = 0x02; /* magic number */ if (!fu_logitech_hidpp_bootloader_request(self, req, error)) { g_prefix_error(error, "failed to clear ram buffer @%04x: ", req->addr); return FALSE; } return TRUE; } static gboolean fu_logitech_hidpp_bootloader_texas_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuLogitechHidppBootloader *self = FU_LOGITECH_HIDPP_BOOTLOADER(device); const FuLogitechHidppBootloaderRequest *payload; g_autoptr(GBytes) fw = NULL; g_autoptr(GPtrArray) reqs = NULL; g_autoptr(FuLogitechHidppBootloaderRequest) req = fu_logitech_hidpp_bootloader_request_new(); /* progress */ fu_progress_set_id(progress, G_STRLOC); if (fu_device_has_private_flag(device, FU_LOGITECH_HIDPP_BOOTLOADER_FLAG_IS_SIGNED)) { fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 3, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 1, "clear"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 18, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 79, NULL); } else { fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 11, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 1, "clear"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 75, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 12, NULL); } /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* transfer payload */ reqs = fu_logitech_hidpp_bootloader_parse_requests(self, fw, error); if (reqs == NULL) return FALSE; /* erase all flash pages */ if (!fu_logitech_hidpp_bootloader_texas_erase_all(self, error)) return FALSE; fu_progress_step_done(progress); /* set existing RAM buffer to 0xff's */ if (!fu_logitech_hidpp_bootloader_texas_clear_ram_buffer(self, error)) return FALSE; fu_progress_step_done(progress); /* write to RAM buffer */ for (guint i = 0; i < reqs->len; i++) { payload = g_ptr_array_index(reqs, i); /* check size */ if (payload->len != 16) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "payload size invalid @%04x: got 0x%02x", payload->addr, payload->len); return FALSE; } /* build packet */ req->cmd = payload->cmd; /* signature addresses do not need to fit inside 128 bytes */ if (req->cmd == FU_LOGITECH_HIDPP_BOOTLOADER_CMD_WRITE_SIGNATURE) req->addr = payload->addr; else req->addr = payload->addr % 0x80; req->len = payload->len; if (!fu_memcpy_safe(req->data, req->len, 0x0, /* dst */ payload->data, payload->len, 0x0, /* src */ payload->len, error)) return FALSE; if (!fu_logitech_hidpp_bootloader_request(self, req, error)) { g_prefix_error(error, "failed to write ram buffer @0x%02x: ", req->addr); return FALSE; } if (req->cmd == FU_LOGITECH_HIDPP_BOOTLOADER_CMD_WRITE_RAM_BUFFER_INVALID_ADDR) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to write ram buffer @%04x: invalid location", req->addr); return FALSE; } if (req->cmd == FU_LOGITECH_HIDPP_BOOTLOADER_CMD_WRITE_RAM_BUFFER_OVERFLOW) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to write ram buffer @%04x: invalid size 0x%02x", req->addr, req->len); return FALSE; } /* flush RAM buffer to EEPROM */ if ((payload->addr + 0x10) % 0x80 == 0 && req->cmd != FU_LOGITECH_HIDPP_BOOTLOADER_CMD_WRITE_SIGNATURE) { guint16 addr_start = payload->addr - (7 * 0x10); g_debug("addr flush @ 0x%04x for 0x%04x", payload->addr, addr_start); if (!fu_logitech_hidpp_bootloader_texas_flash_ram_buffer(self, addr_start, error)) { g_prefix_error(error, "failed to flash ram buffer @0x%04x: ", addr_start); return FALSE; } } /* update progress */ fu_progress_set_percentage_full(fu_progress_get_child(progress), i + 1, reqs->len); } fu_progress_step_done(progress); /* check CRC */ if (!fu_logitech_hidpp_bootloader_texas_compute_and_test_crc(self, error)) return FALSE; fu_progress_step_done(progress); /* success! */ return TRUE; } static gboolean fu_logitech_hidpp_bootloader_texas_setup(FuDevice *device, GError **error) { /* FuLogitechHidppBootloader->setup */ if (!FU_DEVICE_CLASS(fu_logitech_hidpp_bootloader_texas_parent_class)->setup(device, error)) return FALSE; fu_device_set_version(device, "RQR24.00_B0000"); return TRUE; } static void fu_logitech_hidpp_bootloader_texas_class_init(FuLogitechHidppBootloaderTexasClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_logitech_hidpp_bootloader_texas_write_firmware; device_class->setup = fu_logitech_hidpp_bootloader_texas_setup; } static void fu_logitech_hidpp_bootloader_texas_init(FuLogitechHidppBootloaderTexas *self) { } fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-bootloader-texas.h000066400000000000000000000006761501337203100262370ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-logitech-hidpp-bootloader.h" #define FU_TYPE_LOGITECH_HIDPP_BOOTLOADER_TEXAS (fu_logitech_hidpp_bootloader_texas_get_type()) G_DECLARE_FINAL_TYPE(FuLogitechHidppBootloaderTexas, fu_logitech_hidpp_bootloader_texas, FU, LOGITECH_HIDPP_BOOTLOADER_TEXAS, FuLogitechHidppBootloader) fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-bootloader.c000066400000000000000000000323551501337203100251070ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-logitech-hidpp-bootloader.h" #include "fu-logitech-hidpp-common.h" #include "fu-logitech-hidpp-hidpp.h" #include "fu-logitech-hidpp-struct.h" typedef struct { guint16 flash_addr_lo; guint16 flash_addr_hi; guint16 flash_blocksize; } FuLogitechHidppBootloaderPrivate; #define FU_LOGITECH_HIDPP_DEVICE_EP1 0x81 #define FU_LOGITECH_HIDPP_DEVICE_EP3 0x83 G_DEFINE_TYPE_WITH_PRIVATE(FuLogitechHidppBootloader, fu_logitech_hidpp_bootloader, FU_TYPE_HID_DEVICE) #define GET_PRIVATE(o) (fu_logitech_hidpp_bootloader_get_instance_private(o)) static void fu_logitech_hidpp_bootloader_to_string(FuDevice *device, guint idt, GString *str) { FuLogitechHidppBootloader *self = FU_LOGITECH_HIDPP_BOOTLOADER(device); FuLogitechHidppBootloaderPrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append_hex(str, idt, "FlashAddrHigh", priv->flash_addr_hi); fwupd_codec_string_append_hex(str, idt, "FlashAddrLow", priv->flash_addr_lo); fwupd_codec_string_append_hex(str, idt, "FlashBlockSize", priv->flash_blocksize); } FuLogitechHidppBootloaderRequest * fu_logitech_hidpp_bootloader_request_new(void) { FuLogitechHidppBootloaderRequest *req = g_new0(FuLogitechHidppBootloaderRequest, 1); return req; } GPtrArray * fu_logitech_hidpp_bootloader_parse_requests(FuLogitechHidppBootloader *self, GBytes *fw, GError **error) { const gchar *tmp; g_auto(GStrv) lines = NULL; g_autoptr(GPtrArray) reqs = NULL; guint32 last_addr = 0; reqs = g_ptr_array_new_with_free_func(g_free); tmp = g_bytes_get_data(fw, NULL); lines = g_strsplit_set(tmp, "\n\r", -1); for (guint i = 0; lines[i] != NULL; i++) { g_autoptr(FuLogitechHidppBootloaderRequest) payload = NULL; guint8 rec_type = 0x00; guint16 offset = 0x0000; guint16 addr = 0x0; gboolean exit = FALSE; gsize linesz = strlen(lines[i]); /* skip empty lines */ tmp = lines[i]; if (linesz < 5) continue; payload = fu_logitech_hidpp_bootloader_request_new(); payload->len = fu_logitech_hidpp_buffer_read_uint8(tmp + 0x01); if (payload->len > 28) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "firmware data invalid: too large %u bytes", payload->len); return NULL; } if (!fu_firmware_strparse_uint16_safe(tmp, linesz, 0x03, &addr, error)) return NULL; payload->addr = addr; payload->cmd = FU_LOGITECH_HIDPP_BOOTLOADER_CMD_WRITE_RAM_BUFFER; rec_type = fu_logitech_hidpp_buffer_read_uint8(tmp + 0x07); switch (rec_type) { case 0x00: /* data */ break; case 0x01: /* EOF */ exit = TRUE; break; case 0x03: /* start segment address */ /* this is used to specify the start address, it is doesn't matter in this context so we can safely ignore it */ continue; case 0x04: /* extended linear address */ if (!fu_firmware_strparse_uint16_safe(tmp, linesz, 0x09, &offset, error)) return NULL; if (offset != 0x0000) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "extended linear addresses with offset different from " "0 are not supported"); return NULL; } continue; case 0x05: /* start linear address */ /* this is used to specify the start address, it is doesn't matter in this context so we can safely ignore it */ continue; case 0xFD: /* custom - vendor */ /* record type of 0xFD indicates signature data */ payload->cmd = FU_LOGITECH_HIDPP_BOOTLOADER_CMD_WRITE_SIGNATURE; break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "intel hex file record type %02x not supported", rec_type); return NULL; } if (exit) break; /* read the data, but skip the checksum byte */ for (guint j = 0; j < payload->len; j++) { const gchar *ptr = tmp + 0x09 + (j * 2); if (ptr[0] == '\0') { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "firmware data invalid: expected %u bytes", payload->len); return NULL; } payload->data[j] = fu_logitech_hidpp_buffer_read_uint8(ptr); } /* no need to bound check signature addresses */ if (payload->cmd == FU_LOGITECH_HIDPP_BOOTLOADER_CMD_WRITE_SIGNATURE) { g_ptr_array_add(reqs, g_steal_pointer(&payload)); continue; } /* skip the bootloader */ if (payload->addr > fu_logitech_hidpp_bootloader_get_addr_hi(self)) { g_debug("skipping write @ %04x", payload->addr); continue; } /* skip the header */ if (payload->addr < fu_logitech_hidpp_bootloader_get_addr_lo(self)) { g_debug("skipping write @ %04x", payload->addr); continue; } /* make sure firmware addresses only go up */ if (payload->addr < last_addr) { g_debug("skipping write @ %04x", payload->addr); continue; } last_addr = payload->addr; /* pending */ g_ptr_array_add(reqs, g_steal_pointer(&payload)); } if (reqs->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "firmware data invalid: no payloads found"); return NULL; } return g_steal_pointer(&reqs); } guint16 fu_logitech_hidpp_bootloader_get_addr_lo(FuLogitechHidppBootloader *self) { FuLogitechHidppBootloaderPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_LOGITECH_HIDPP_BOOTLOADER(self), 0x0000); return priv->flash_addr_lo; } guint16 fu_logitech_hidpp_bootloader_get_addr_hi(FuLogitechHidppBootloader *self) { FuLogitechHidppBootloaderPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_LOGITECH_HIDPP_BOOTLOADER(self), 0x0000); return priv->flash_addr_hi; } guint16 fu_logitech_hidpp_bootloader_get_blocksize(FuLogitechHidppBootloader *self) { FuLogitechHidppBootloaderPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_LOGITECH_HIDPP_BOOTLOADER(self), 0x0000); return priv->flash_blocksize; } static gboolean fu_logitech_hidpp_bootloader_attach(FuDevice *device, FuProgress *progress, GError **error) { FuLogitechHidppBootloader *self = FU_LOGITECH_HIDPP_BOOTLOADER(device); g_autoptr(FuLogitechHidppBootloaderRequest) req = fu_logitech_hidpp_bootloader_request_new(); req->cmd = FU_LOGITECH_HIDPP_BOOTLOADER_CMD_REBOOT; if (!fu_logitech_hidpp_bootloader_request(self, req, error)) { g_prefix_error(error, "failed to attach back to runtime: "); return FALSE; } fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_logitech_hidpp_bootloader_set_bl_version(FuLogitechHidppBootloader *self, GError **error) { guint16 build; guint8 major; guint8 minor; g_autofree gchar *version = NULL; g_autoptr(FuLogitechHidppBootloaderRequest) req = fu_logitech_hidpp_bootloader_request_new(); /* call into hardware */ req->cmd = FU_LOGITECH_HIDPP_BOOTLOADER_CMD_GET_BL_VERSION; if (!fu_logitech_hidpp_bootloader_request(self, req, error)) { g_prefix_error(error, "failed to get firmware version: "); return FALSE; } /* BOTxx.yy_Bzzzz * 012345678901234 */ build = (guint16)fu_logitech_hidpp_buffer_read_uint8((const gchar *)req->data + 10) << 8; build += fu_logitech_hidpp_buffer_read_uint8((const gchar *)req->data + 12); major = fu_logitech_hidpp_buffer_read_uint8((const gchar *)req->data + 3); minor = fu_logitech_hidpp_buffer_read_uint8((const gchar *)req->data + 6); version = fu_logitech_hidpp_format_version("BOT", major, minor, build); if (version == NULL) { g_prefix_error(error, "failed to format firmware version: "); return FALSE; } fu_device_set_version_bootloader(FU_DEVICE(self), version); if ((major == 0x01 && minor >= 0x04) || (major == 0x03 && minor >= 0x02)) { fu_device_add_private_flag(FU_DEVICE(self), FU_LOGITECH_HIDPP_BOOTLOADER_FLAG_IS_SIGNED); fu_device_add_protocol(FU_DEVICE(self), "com.logitech.unifyingsigned"); } else { fu_device_add_protocol(FU_DEVICE(self), "com.logitech.unifying"); } return TRUE; } static gboolean fu_logitech_hidpp_bootloader_setup(FuDevice *device, GError **error) { FuLogitechHidppBootloader *self = FU_LOGITECH_HIDPP_BOOTLOADER(device); FuLogitechHidppBootloaderPrivate *priv = GET_PRIVATE(self); g_autoptr(FuLogitechHidppBootloaderRequest) req = fu_logitech_hidpp_bootloader_request_new(); /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_logitech_hidpp_bootloader_parent_class)->setup(device, error)) return FALSE; /* get memory map */ req->cmd = FU_LOGITECH_HIDPP_BOOTLOADER_CMD_GET_MEMINFO; if (!fu_logitech_hidpp_bootloader_request(self, req, error)) { g_prefix_error(error, "failed to get meminfo: "); return FALSE; } if (req->len != 0x06) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to get meminfo: invalid size %02x", req->len); return FALSE; } /* parse values */ priv->flash_addr_lo = fu_memread_uint16(req->data + 0, G_BIG_ENDIAN); priv->flash_addr_hi = fu_memread_uint16(req->data + 2, G_BIG_ENDIAN); priv->flash_blocksize = fu_memread_uint16(req->data + 4, G_BIG_ENDIAN); /* get bootloader version */ return fu_logitech_hidpp_bootloader_set_bl_version(self, error); } gboolean fu_logitech_hidpp_bootloader_request(FuLogitechHidppBootloader *self, FuLogitechHidppBootloaderRequest *req, GError **error) { gsize actual_length = 0; guint8 buf_request[32]; guint8 buf_response[32]; /* build packet */ memset(buf_request, 0x00, sizeof(buf_request)); buf_request[0x00] = req->cmd; buf_request[0x01] = req->addr >> 8; buf_request[0x02] = req->addr & 0xff; buf_request[0x03] = req->len; if (!fu_memcpy_safe(buf_request, sizeof(buf_request), 0x04, /* dst */ req->data, sizeof(req->data), 0x0, /* src */ sizeof(req->data), error)) return FALSE; /* send request */ fu_dump_raw(G_LOG_DOMAIN, "host->device", buf_request, sizeof(buf_request)); if (!fu_hid_device_set_report(FU_HID_DEVICE(self), 0x0, buf_request, sizeof(buf_request), FU_LOGITECH_HIDPP_DEVICE_TIMEOUT_MS, FU_HID_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to send data: "); return FALSE; } /* no response required when rebooting */ if (req->cmd == FU_LOGITECH_HIDPP_BOOTLOADER_CMD_REBOOT) { g_autoptr(GError) error_ignore = NULL; if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), FU_LOGITECH_HIDPP_DEVICE_EP1, buf_response, sizeof(buf_response), &actual_length, FU_LOGITECH_HIDPP_DEVICE_TIMEOUT_MS, NULL, &error_ignore)) { g_debug("ignoring: %s", error_ignore->message); } else { fu_dump_raw(G_LOG_DOMAIN, "device->host", buf_response, actual_length); } return TRUE; } /* get response */ memset(buf_response, 0x00, sizeof(buf_response)); if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), FU_LOGITECH_HIDPP_DEVICE_EP1, buf_response, sizeof(buf_response), &actual_length, FU_LOGITECH_HIDPP_DEVICE_TIMEOUT_MS, NULL, error)) { g_prefix_error(error, "failed to get data: "); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "device->host", buf_response, actual_length); /* parse response */ if ((buf_response[0x00] & 0xf0) != req->cmd) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid command response of %02x, expected %02x", buf_response[0x00], req->cmd); return FALSE; } req->cmd = buf_response[0x00]; req->addr = ((guint16)buf_response[0x01] << 8) + buf_response[0x02]; req->len = buf_response[0x03]; if (req->len > 28) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid data size of %02x", req->len); return FALSE; } memset(req->data, 0x00, 28); if (req->len > 0) memcpy(req->data, buf_response + 0x04, req->len); /* nocheck:blocked */ return TRUE; } static void fu_logitech_hidpp_bootloader_replace(FuDevice *device, FuDevice *donor) { fu_device_incorporate_flag(device, donor, FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_incorporate_flag(device, donor, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); } static void fu_logitech_hidpp_bootloader_init(FuLogitechHidppBootloader *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_USB_RECEIVER); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_name(FU_DEVICE(self), "Unifying Receiver"); fu_device_set_summary(FU_DEVICE(self), "Miniaturised USB wireless receiver (bootloader)"); fu_device_set_remove_delay(FU_DEVICE(self), FU_LOGITECH_HIDPP_DEVICE_TIMEOUT_MS); fu_device_register_private_flag(FU_DEVICE(self), FU_LOGITECH_HIDPP_BOOTLOADER_FLAG_IS_SIGNED); fu_usb_device_add_interface(FU_USB_DEVICE(self), 0x00); } static void fu_logitech_hidpp_bootloader_class_init(FuLogitechHidppBootloaderClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_logitech_hidpp_bootloader_to_string; device_class->attach = fu_logitech_hidpp_bootloader_attach; device_class->setup = fu_logitech_hidpp_bootloader_setup; device_class->replace = fu_logitech_hidpp_bootloader_replace; } fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-bootloader.h000066400000000000000000000027631501337203100251140ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_LOGITECH_HIDPP_BOOTLOADER (fu_logitech_hidpp_bootloader_get_type()) G_DECLARE_DERIVABLE_TYPE(FuLogitechHidppBootloader, fu_logitech_hidpp_bootloader, FU, LOGITECH_HIDPP_BOOTLOADER, FuHidDevice) struct _FuLogitechHidppBootloaderClass { FuHidDeviceClass parent_class; }; #define FU_LOGITECH_HIDPP_BOOTLOADER_FLAG_IS_SIGNED "is-signed" /* packet to and from device */ typedef struct __attribute__((packed)) { /* nocheck:blocked */ guint8 cmd; guint16 addr; guint8 len; guint8 data[28]; } FuLogitechHidppBootloaderRequest; FuLogitechHidppBootloaderRequest * fu_logitech_hidpp_bootloader_request_new(void); #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuLogitechHidppBootloaderRequest, g_free); #pragma clang diagnostic pop GPtrArray * fu_logitech_hidpp_bootloader_parse_requests(FuLogitechHidppBootloader *self, GBytes *fw, GError **error); gboolean fu_logitech_hidpp_bootloader_request(FuLogitechHidppBootloader *self, FuLogitechHidppBootloaderRequest *req, GError **error); guint16 fu_logitech_hidpp_bootloader_get_addr_lo(FuLogitechHidppBootloader *self); guint16 fu_logitech_hidpp_bootloader_get_addr_hi(FuLogitechHidppBootloader *self); guint16 fu_logitech_hidpp_bootloader_get_blocksize(FuLogitechHidppBootloader *self); fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-common.c000066400000000000000000000021241501337203100242340ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include #include "fu-logitech-hidpp-common.h" guint8 fu_logitech_hidpp_buffer_read_uint8(const gchar *str) { guint64 tmp; gchar buf[3] = {0x0, 0x0, 0x0}; memcpy(buf, str, 2); /* nocheck:blocked */ tmp = g_ascii_strtoull(buf, NULL, 16); /* nocheck:blocked */ return tmp; } guint16 fu_logitech_hidpp_buffer_read_uint16(const gchar *str) { guint64 tmp; gchar buf[5] = {0x0, 0x0, 0x0, 0x0, 0x0}; memcpy(buf, str, 4); /* nocheck:blocked */ tmp = g_ascii_strtoull(buf, NULL, 16); /* nocheck:blocked */ return tmp; } gchar * fu_logitech_hidpp_format_version(const gchar *name, guint8 major, guint8 minor, guint16 build) { GString *str = g_string_new(NULL); for (guint i = 0; i < 3; i++) { if (g_ascii_isspace(name[i]) || name[i] == '\0') continue; g_string_append_c(str, name[i]); } g_string_append_printf(str, "%02x.%02x_B%04x", major, minor, build); return g_string_free(str, FALSE); } fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-common.h000066400000000000000000000021021501337203100242350ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_LOGITECH_HIDPP_DEVICE_VID 0x046d #define FU_LOGITECH_HIDPP_DEVICE_PID_RUNTIME 0xC52B #define FU_LOGITECH_HIDPP_DEVICE_PID_BOOTLOADER_NORDIC 0xAAAA #define FU_LOGITECH_HIDPP_DEVICE_PID_BOOTLOADER_NORDIC_PICO 0xAAAE #define FU_LOGITECH_HIDPP_DEVICE_PID_BOOTLOADER_TEXAS 0xAAAC #define FU_LOGITECH_HIDPP_DEVICE_PID_BOOTLOADER_TEXAS_PICO 0xAAAD #define FU_LOGITECH_HIDPP_DEVICE_PID_BOOTLOADER_BOLT 0xAB07 /* Signed firmware are very long to verify on the device */ #define FU_LOGITECH_HIDPP_DEVICE_TIMEOUT_MS 30000 /* Polling intervals (ms) */ #define FU_HIDPP_DEVICE_POLLING_INTERVAL 30000 #define FU_HIDPP_RECEIVER_RUNTIME_POLLING_INTERVAL 5000 #define FU_HIDPP_VERSION_BLE 0xFE guint8 fu_logitech_hidpp_buffer_read_uint8(const gchar *str); guint16 fu_logitech_hidpp_buffer_read_uint16(const gchar *str); gchar * fu_logitech_hidpp_format_version(const gchar *name, guint8 major, guint8 minor, guint16 build); fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-device.c000066400000000000000000002052541501337203100242140ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-logitech-hidpp-common.h" #include "fu-logitech-hidpp-device.h" #include "fu-logitech-hidpp-hidpp.h" #include "fu-logitech-hidpp-radio.h" #include "fu-logitech-hidpp-rdfu-struct.h" #include "fu-logitech-hidpp-runtime-bolt.h" #include "fu-logitech-hidpp-struct.h" #include "fu-logitech-rdfu-firmware.h" typedef struct { guint8 cached_fw_entity; /* * Device index: * - FU_LOGITECH_HIDPP_DEVICE_IDX_RECEIVER for the receiver or BLE devices * - pairing slot for paired Bolt devices. */ guint8 device_idx; guint16 hidpp_pid; guint8 hidpp_version; gchar *model_id; GPtrArray *feature_index; /* of FuLogitechHidppHidppMap */ FuLogitechHidppRdfuState rdfu_state; guint8 rdfu_capabilities; guint16 rdfu_block; guint32 rdfu_pkt; guint32 rdfu_wait; } FuLogitechHidppDevicePrivate; typedef struct { guint8 idx; guint16 feature; } FuLogitechHidppHidppMap; #define FU_LOGITECH_HIDPP_DEVICE_DATA_PKT_LONG 16 /* max attempts to resume after non-critical errors */ #define FU_LOGITECH_HIDPP_DEVICE_RDFU_MAX_RETRIES 10 G_DEFINE_TYPE_WITH_PRIVATE(FuLogitechHidppDevice, fu_logitech_hidpp_device, FU_TYPE_UDEV_DEVICE) #define GET_PRIVATE(o) (fu_logitech_hidpp_device_get_instance_private(o)) typedef enum { FU_HIDPP_DEVICE_KIND_KEYBOARD, FU_HIDPP_DEVICE_KIND_REMOTE_CONTROL, FU_HIDPP_DEVICE_KIND_NUMPAD, FU_HIDPP_DEVICE_KIND_MOUSE, FU_HIDPP_DEVICE_KIND_TOUCHPAD, FU_HIDPP_DEVICE_KIND_TRACKBALL, FU_HIDPP_DEVICE_KIND_PRESENTER, FU_HIDPP_DEVICE_KIND_RECEIVER, FU_HIDPP_DEVICE_KIND_LAST } FuLogitechHidppDeviceKind; void fu_logitech_hidpp_device_set_device_idx(FuLogitechHidppDevice *self, guint8 device_idx) { FuLogitechHidppDevicePrivate *priv; g_return_if_fail(FU_IS_HIDPP_DEVICE(self)); priv = GET_PRIVATE(self); priv->device_idx = device_idx; } guint16 fu_logitech_hidpp_device_get_hidpp_pid(FuLogitechHidppDevice *self) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_HIDPP_DEVICE(self), G_MAXUINT16); return priv->hidpp_pid; } void fu_logitech_hidpp_device_set_hidpp_pid(FuLogitechHidppDevice *self, guint16 hidpp_pid) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_HIDPP_DEVICE(self)); priv->hidpp_pid = hidpp_pid; } static void fu_logitech_hidpp_device_set_model_id(FuLogitechHidppDevice *self, const gchar *model_id) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_HIDPP_DEVICE(self)); if (g_strcmp0(priv->model_id, model_id) == 0) return; g_free(priv->model_id); priv->model_id = g_strdup(model_id); } static const gchar * fu_logitech_hidpp_device_get_icon(FuLogitechHidppDeviceKind kind) { if (kind == FU_HIDPP_DEVICE_KIND_KEYBOARD) return FU_DEVICE_ICON_INPUT_KEYBOARD; if (kind == FU_HIDPP_DEVICE_KIND_REMOTE_CONTROL) return FU_DEVICE_ICON_PDA; // ish if (kind == FU_HIDPP_DEVICE_KIND_NUMPAD) return FU_DEVICE_ICON_INPUT_DIALPAD; if (kind == FU_HIDPP_DEVICE_KIND_MOUSE) return FU_DEVICE_ICON_INPUT_MOUSE; if (kind == FU_HIDPP_DEVICE_KIND_TOUCHPAD) return FU_DEVICE_ICON_INPUT_TOUCHPAD; if (kind == FU_HIDPP_DEVICE_KIND_TRACKBALL) return FU_DEVICE_ICON_INPUT_MOUSE; // ish if (kind == FU_HIDPP_DEVICE_KIND_PRESENTER) return FU_DEVICE_ICON_PDA; // ish if (kind == FU_HIDPP_DEVICE_KIND_RECEIVER) return FU_DEVICE_ICON_USB_RECEIVER; return NULL; } static const gchar * fu_logitech_hidpp_device_get_summary(FuLogitechHidppDeviceKind kind) { if (kind == FU_HIDPP_DEVICE_KIND_KEYBOARD) return "Unifying Keyboard"; if (kind == FU_HIDPP_DEVICE_KIND_REMOTE_CONTROL) return "Unifying Remote Control"; if (kind == FU_HIDPP_DEVICE_KIND_NUMPAD) return "Unifying Number Pad"; if (kind == FU_HIDPP_DEVICE_KIND_MOUSE) return "Unifying Mouse"; if (kind == FU_HIDPP_DEVICE_KIND_TOUCHPAD) return "Unifying Touchpad"; if (kind == FU_HIDPP_DEVICE_KIND_TRACKBALL) return "Unifying Trackball"; if (kind == FU_HIDPP_DEVICE_KIND_PRESENTER) return "Unifying Presenter"; if (kind == FU_HIDPP_DEVICE_KIND_RECEIVER) return "Unifying Receiver"; return NULL; } static gboolean fu_logitech_hidpp_device_ping(FuLogitechHidppDevice *self, GError **error) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(GError) error_local = NULL; g_autoptr(FuLogitechHidppHidppMsg) msg = fu_logitech_hidpp_msg_new(); GPtrArray *children = NULL; /* handle failure */ msg->report_id = FU_LOGITECH_HIDPP_REPORT_ID_SHORT; msg->device_id = priv->device_idx; msg->sub_id = 0x00; /* rootIndex */ msg->function_id = 0x01 << 4; /* ping */ msg->data[0] = 0x00; msg->data[1] = 0x00; msg->data[2] = 0xaa; /* user-selected value */ msg->hidpp_version = priv->hidpp_version; if (!fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self), msg, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { priv->hidpp_version = 1; return TRUE; } if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNREACHABLE); return TRUE; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } /* device no longer asleep */ fu_device_remove_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNREACHABLE); children = fu_device_get_children(FU_DEVICE(self)); for (guint i = 0; i < children->len; i++) { FuDevice *radio = g_ptr_array_index(children, i); fu_device_remove_flag(radio, FWUPD_DEVICE_FLAG_UNREACHABLE); } /* if the device index is unset, grab it from the reply */ if (priv->device_idx == FU_LOGITECH_HIDPP_DEVICE_IDX_WIRED && msg->device_id != FU_LOGITECH_HIDPP_DEVICE_IDX_WIRED) { priv->device_idx = msg->device_id; g_debug("device index is %02x", priv->device_idx); } /* format version in BCD format */ if (priv->hidpp_version != FU_HIDPP_VERSION_BLE) { /* minor version is in msg->data[1] */; priv->hidpp_version = msg->data[0]; } /* success */ return TRUE; } static gboolean fu_logitech_hidpp_device_poll(FuDevice *device, GError **error) { FuLogitechHidppDevice *self = FU_HIDPP_DEVICE(device); FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); const guint timeout = 1; /* ms */ g_autoptr(GError) error_local = NULL; g_autoptr(FuLogitechHidppHidppMsg) msg = fu_logitech_hidpp_msg_new(); g_autoptr(FuDeviceLocker) locker = NULL; /* open */ locker = fu_device_locker_new(self, error); if (locker == NULL) return FALSE; /* flush pending data */ msg->device_id = priv->device_idx; msg->hidpp_version = priv->hidpp_version; if (!fu_logitech_hidpp_receive(FU_UDEV_DEVICE(self), msg, timeout, &error_local)) { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT)) { g_warning("failed to get pending read: %s", error_local->message); return TRUE; } /* no data to receive */ g_clear_error(&error_local); } /* just ping */ if (fu_device_has_private_flag(device, FU_LOGITECH_HIDPP_DEVICE_FLAG_REBIND_ATTACH)) { if (!fu_logitech_hidpp_device_ping(self, &error_local)) { g_warning("failed to ping %s: %s", fu_device_get_name(device), error_local->message); return TRUE; } } /* this is the first time the device has been active */ if (priv->feature_index->len == 0) { fu_device_probe_invalidate(FU_DEVICE(self)); if (!fu_device_setup(FU_DEVICE(self), error)) return FALSE; } /* success */ return TRUE; } static void fu_logitech_hidpp_device_map_to_string(FuLogitechHidppHidppMap *map, guint idt, GString *str) { g_autofree gchar *title = g_strdup_printf("Feature%02x", map->idx); g_autofree gchar *tmp = g_strdup_printf("%s [0x%04x]", fu_logitech_hidpp_feature_to_string(map->feature), map->feature); fwupd_codec_string_append(str, idt, title, tmp); } static void fu_logitech_hidpp_device_to_string(FuDevice *device, guint idt, GString *str) { FuLogitechHidppDevice *self = FU_HIDPP_DEVICE(device); FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append_int(str, idt, "HidppVersion", priv->hidpp_version); fwupd_codec_string_append_int(str, idt, "HidppPid", priv->hidpp_pid); fwupd_codec_string_append_hex(str, idt, "DeviceIdx", priv->device_idx); fwupd_codec_string_append(str, idt, "ModelId", priv->model_id); for (guint i = 0; i < priv->feature_index->len; i++) { FuLogitechHidppHidppMap *map = g_ptr_array_index(priv->feature_index, i); fu_logitech_hidpp_device_map_to_string(map, idt, str); } } static guint8 fu_logitech_hidpp_device_feature_get_idx(FuLogitechHidppDevice *self, guint16 feature) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); for (guint i = 0; i < priv->feature_index->len; i++) { FuLogitechHidppHidppMap *map = g_ptr_array_index(priv->feature_index, i); if (map->feature == feature) return map->idx; } return 0x00; } static gboolean fu_logitech_hidpp_device_create_radio_child(FuLogitechHidppDevice *self, guint8 entity, guint16 build, GError **error) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); FuContext *ctx = fu_device_get_context(FU_DEVICE(self)); g_autofree gchar *instance_id = NULL; g_autofree gchar *logical_id = NULL; g_autofree gchar *radio_version = NULL; g_autoptr(FuLogitechHidppRadio) radio = NULL; GPtrArray *children = fu_device_get_children(FU_DEVICE(self)); /* sanity check */ if (priv->model_id == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "model ID not set"); return FALSE; } radio_version = g_strdup_printf("0x%.4x", build); radio = fu_logitech_hidpp_radio_new(ctx, entity); fu_device_incorporate(FU_DEVICE(radio), FU_DEVICE(self), FU_DEVICE_INCORPORATE_FLAG_PHYSICAL_ID); /* * Use the parent logical id as well as the model id for the * logical id of the radio child device. This allows the radio * devices of two devices of the same type (same device type, * BLE mode) to coexist correctly. */ logical_id = g_strdup_printf("%s-%s", fu_device_get_logical_id(FU_DEVICE(self)), priv->model_id); fu_device_set_logical_id(FU_DEVICE(radio), logical_id); instance_id = g_strdup_printf("HIDRAW\\VEN_%04X&MOD_%s&ENT_05", fu_device_get_vid(FU_DEVICE(self)), priv->model_id); fu_device_add_instance_id(FU_DEVICE(radio), instance_id); fu_device_set_version(FU_DEVICE(radio), radio_version); if (!fu_device_setup(FU_DEVICE(radio), error)) return FALSE; /* remove old radio device if it already existed */ for (guint i = 0; i < children->len; i++) { FuDevice *child = g_ptr_array_index(children, i); if (g_strcmp0(fu_device_get_physical_id(FU_DEVICE(radio)), fu_device_get_physical_id(child)) == 0 && g_strcmp0(fu_device_get_logical_id(FU_DEVICE(radio)), fu_device_get_logical_id(child)) == 0) { fu_device_remove_child(FU_DEVICE(self), child); break; } } fu_device_add_child(FU_DEVICE(self), FU_DEVICE(radio)); return TRUE; } static gboolean fu_logitech_hidpp_device_fetch_firmware_info(FuLogitechHidppDevice *self, GError **error) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); guint8 idx; guint8 entity_count; g_autoptr(FuLogitechHidppHidppMsg) msg_count = fu_logitech_hidpp_msg_new(); gboolean radio_ok = FALSE; gboolean app_ok = FALSE; /* get the feature index */ idx = fu_logitech_hidpp_device_feature_get_idx(self, FU_LOGITECH_HIDPP_FEATURE_I_FIRMWARE_INFO); if (idx == 0x00) return TRUE; /* get the entity count */ msg_count->report_id = FU_LOGITECH_HIDPP_REPORT_ID_SHORT; msg_count->device_id = priv->device_idx; msg_count->sub_id = idx; msg_count->function_id = 0x00 << 4; /* getCount */ msg_count->hidpp_version = priv->hidpp_version; if (!fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self), msg_count, error)) { g_prefix_error(error, "failed to get firmware count: "); return FALSE; } entity_count = msg_count->data[0]; g_debug("firmware entity count is %u", entity_count); /* get firmware, bootloader, hardware versions */ for (guint8 i = 0; i < entity_count; i++) { guint16 build; g_autofree gchar *version = NULL; g_autofree gchar *name = NULL; g_autoptr(FuLogitechHidppHidppMsg) msg = fu_logitech_hidpp_msg_new(); msg->report_id = FU_LOGITECH_HIDPP_REPORT_ID_SHORT; msg->device_id = priv->device_idx; msg->sub_id = idx; msg->function_id = 0x01 << 4; /* getInfo */ msg->hidpp_version = priv->hidpp_version; msg->data[0] = i; if (!fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self), msg, error)) { g_prefix_error(error, "failed to get firmware info: "); return FALSE; } /* use the single available slot, otherwise -- the slot which is not active */ if (msg->data[0] == 0) { if (!app_ok || FU_BIT_IS_CLEAR(msg->data[8], 0)) priv->cached_fw_entity = i; app_ok = TRUE; } if (msg->data[1] == 0x00 && msg->data[2] == 0x00 && msg->data[3] == 0x00 && msg->data[4] == 0x00 && msg->data[5] == 0x00 && msg->data[6] == 0x00 && msg->data[7] == 0x00) { g_debug("no version set for entity %u", i); continue; } name = g_strdup_printf("%c%c%c", msg->data[1], msg->data[2], msg->data[3]); build = ((guint16)msg->data[6]) << 8 | msg->data[7]; version = fu_logitech_hidpp_format_version(name, msg->data[4], msg->data[5], build); g_debug("firmware entity 0x%02x version is %s", i, version); if (msg->data[0] == 0) { /* set version from the active entity */ if (FU_BIT_IS_SET(msg->data[8], 0)) fu_device_set_version(FU_DEVICE(self), version); } else if (msg->data[0] == 1) { fu_device_set_version_bootloader(FU_DEVICE(self), version); } else if (msg->data[0] == 2) { fu_device_set_metadata(FU_DEVICE(self), "version-hw", version); } else if (msg->data[0] == 5 && fu_device_has_private_flag(FU_DEVICE(self), FU_LOGITECH_HIDPP_DEVICE_FLAG_ADD_RADIO)) { if (!fu_logitech_hidpp_device_create_radio_child(self, i, build, error)) { g_prefix_error(error, "failed to create radio: "); return FALSE; } radio_ok = TRUE; } } /* the device is probably in bootloader mode and the last SoftDevice FW upgrade failed */ if (fu_device_has_private_flag(FU_DEVICE(self), FU_LOGITECH_HIDPP_DEVICE_FLAG_ADD_RADIO) && !radio_ok) { g_debug("no radio found, creating a fake one for recovery"); if (!fu_logitech_hidpp_device_create_radio_child(self, 1, 0, error)) { g_prefix_error(error, "failed to create radio: "); return FALSE; } } /* not an error, the device just doesn't support this */ return TRUE; } static gboolean fu_logitech_hidpp_device_fetch_model_id(FuLogitechHidppDevice *self, GError **error) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); guint8 idx; guint64 pid_tmp = 0; g_autoptr(FuLogitechHidppHidppMsg) msg = fu_logitech_hidpp_msg_new(); g_autoptr(GString) str = g_string_new(NULL); /* get the (optional) feature index */ idx = fu_logitech_hidpp_device_feature_get_idx(self, FU_LOGITECH_HIDPP_FEATURE_I_FIRMWARE_INFO); if (idx == 0x00) return TRUE; msg->report_id = FU_LOGITECH_HIDPP_REPORT_ID_SHORT; msg->device_id = priv->device_idx; msg->sub_id = idx; msg->function_id = 0x00 << 4; /* getDeviceInfo */ msg->hidpp_version = priv->hidpp_version; if (!fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self), msg, error)) { g_prefix_error(error, "failed to get the model ID: "); return FALSE; } /* ignore extendedModelID in data[13] */ for (guint i = 7; i < 13; i++) g_string_append_printf(str, "%02X", msg->data[i]); fu_logitech_hidpp_device_set_model_id(self, str->str); /* truncate to 4 chars and convert to a PID */ g_string_set_size(str, 4); if (!fu_strtoull(str->str, &pid_tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_16, error)) { g_prefix_error(error, "failed to parse the model ID: "); return FALSE; } fu_device_set_pid(FU_DEVICE(self), (guint32)pid_tmp); /* add one more instance ID */ fu_device_add_instance_u16(FU_DEVICE(self), "VEN", fu_device_get_vid(FU_DEVICE(self))); fu_device_add_instance_str(FU_DEVICE(self), "MOD", priv->model_id); return fu_device_build_instance_id(FU_DEVICE(self), error, "HIDRAW", "VEN", "MOD", NULL); } static gboolean fu_logitech_hidpp_device_fetch_battery_level(FuLogitechHidppDevice *self, GError **error) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); /* try using HID++2.0 */ if (priv->hidpp_version >= 2.f) { guint8 idx; /* try the Unified Battery feature first */ idx = fu_logitech_hidpp_device_feature_get_idx( self, FU_LOGITECH_HIDPP_FEATURE_UNIFIED_BATTERY); if (idx != 0x00) { gboolean socc = FALSE; /* state of charge capability */ g_autoptr(FuLogitechHidppHidppMsg) msg = fu_logitech_hidpp_msg_new(); msg->report_id = FU_LOGITECH_HIDPP_REPORT_ID_SHORT; msg->device_id = priv->device_idx; msg->sub_id = idx; msg->function_id = 0x00 << 4; /* get_capabilities */ msg->hidpp_version = priv->hidpp_version; if (!fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self), msg, error)) { g_prefix_error(error, "failed to get battery info: "); return FALSE; } if (msg->data[1] & 0x02) socc = TRUE; msg->function_id = 0x01 << 4; /* get_status */ if (!fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self), msg, error)) { g_prefix_error(error, "failed to get battery info: "); return FALSE; } if (socc) { fu_device_set_battery_level(FU_DEVICE(self), msg->data[0]); } else { switch (msg->data[1]) { case 1: /* critical */ fu_device_set_battery_level(FU_DEVICE(self), 5); break; case 2: /* low */ fu_device_set_battery_level(FU_DEVICE(self), 20); break; case 4: /* good */ fu_device_set_battery_level(FU_DEVICE(self), 55); break; case 8: /* full */ fu_device_set_battery_level(FU_DEVICE(self), 90); break; default: g_warning("unknown battery level: 0x%02x", msg->data[1]); break; } } return TRUE; } /* fall back to the legacy Battery Level feature */ idx = fu_logitech_hidpp_device_feature_get_idx( self, FU_LOGITECH_HIDPP_FEATURE_BATTERY_LEVEL_STATUS); if (idx != 0x00) { g_autoptr(FuLogitechHidppHidppMsg) msg = fu_logitech_hidpp_msg_new(); msg->report_id = FU_LOGITECH_HIDPP_REPORT_ID_SHORT; msg->device_id = priv->device_idx; msg->sub_id = idx; msg->function_id = 0x00 << 4; /* GetBatteryLevelStatus */ msg->hidpp_version = priv->hidpp_version; if (!fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self), msg, error)) { g_prefix_error(error, "failed to get battery info: "); return FALSE; } if (msg->data[0] != 0x00) fu_device_set_battery_level(FU_DEVICE(self), msg->data[0]); return TRUE; } } /* try HID++1.0 battery mileage */ if (priv->hidpp_version == 1) { g_autoptr(FuLogitechHidppHidppMsg) msg = fu_logitech_hidpp_msg_new(); msg->report_id = FU_LOGITECH_HIDPP_REPORT_ID_SHORT; msg->device_id = priv->device_idx; msg->sub_id = FU_LOGITECH_HIDPP_SUBID_GET_REGISTER; msg->function_id = FU_LOGITECH_HIDPP_REGISTER_BATTERY_MILEAGE << 4; msg->hidpp_version = priv->hidpp_version; if (fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self), msg, NULL)) { if (msg->data[0] != 0x7F) fu_device_set_battery_level(FU_DEVICE(self), msg->data[0]); else g_warning("unknown battery level: 0x%02x", msg->data[0]); return TRUE; } /* try HID++1.0 battery status instead */ msg->function_id = FU_LOGITECH_HIDPP_REGISTER_BATTERY_STATUS << 4; if (fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self), msg, NULL)) { switch (msg->data[0]) { case 1: /* 0 - 10 */ fu_device_set_battery_level(FU_DEVICE(self), 5); break; case 3: /* 11 - 30 */ fu_device_set_battery_level(FU_DEVICE(self), 20); break; case 5: /* 31 - 80 */ fu_device_set_battery_level(FU_DEVICE(self), 55); break; case 7: /* 81 - 100 */ fu_device_set_battery_level(FU_DEVICE(self), 90); break; default: g_warning("unknown battery percentage: 0x%02x", msg->data[0]); break; } return TRUE; } } /* not an error, the device just doesn't support any of the methods */ return TRUE; } /* wrapper function to reuse the pre-rustgen communication */ static gboolean fu_logitech_hidpp_device_transfer_msg(FuLogitechHidppDevice *self, GByteArray *msg, GError **error) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); FuLogitechHidppHidppMsg *hidpp_msg = NULL; g_return_val_if_fail(msg != NULL, FALSE); /* enlarge the size since fu_logitech_hidpp_transfer() returns the answer in the * same message */ fu_byte_array_set_size(msg, sizeof(FuLogitechHidppHidppMsg), 0); hidpp_msg = (FuLogitechHidppHidppMsg *)msg->data; hidpp_msg->hidpp_version = priv->hidpp_version; if (!fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self), hidpp_msg, error)) return FALSE; /* validate and cleanup the function_id from Application ID */ if ((hidpp_msg->function_id & 0x0f) != FU_LOGITECH_HIDPP_HIDPP_MSG_SW_ID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "expected application ID = %i, got %u", FU_LOGITECH_HIDPP_HIDPP_MSG_SW_ID, (guint)(hidpp_msg->function_id & 0x0f)); return FALSE; } hidpp_msg->function_id &= 0xf0; return TRUE; } static gboolean fu_logitech_hidpp_device_rdfu_get_capabilities(FuLogitechHidppDevice *self, GError **error) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); guint8 idx; g_autoptr(FuStructLogitechHidppRdfuGetCapabilities) msg = fu_struct_logitech_hidpp_rdfu_get_capabilities_new(); g_autoptr(GByteArray) response = NULL; priv->rdfu_capabilities = 0; /* set empty */ /* get the feature index */ idx = fu_logitech_hidpp_device_feature_get_idx(self, FU_LOGITECH_HIDPP_FEATURE_RDFU); if (idx == 0x00) return TRUE; fu_struct_logitech_hidpp_rdfu_get_capabilities_set_device_id(msg, priv->device_idx); fu_struct_logitech_hidpp_rdfu_get_capabilities_set_sub_id(msg, idx); g_debug("read capabilities"); if (!fu_logitech_hidpp_device_transfer_msg(self, msg, error)) { g_prefix_error(error, "failed to get capabilities: "); return FALSE; } response = fu_struct_logitech_hidpp_rdfu_capabilities_parse(msg->data, msg->len, 0, error); if (response == NULL) return FALSE; priv->rdfu_capabilities = fu_struct_logitech_hidpp_rdfu_capabilities_get_capabilities(response); return TRUE; } static gboolean fu_logitech_hidpp_device_rdfu_start_dfu(FuLogitechHidppDevice *self, GByteArray *magic, GError **error) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); guint8 idx; guint8 status; g_autoptr(FuStructLogitechHidppRdfuStartDfu) msg = fu_struct_logitech_hidpp_rdfu_start_dfu_new(); g_autoptr(GByteArray) response = NULL; idx = fu_logitech_hidpp_device_feature_get_idx(self, FU_LOGITECH_HIDPP_FEATURE_RDFU); if (idx == 0x00) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "RDFU feature is required for startDfu"); return FALSE; } fu_struct_logitech_hidpp_rdfu_start_dfu_set_device_id(msg, priv->device_idx); fu_struct_logitech_hidpp_rdfu_start_dfu_set_sub_id(msg, idx); fu_struct_logitech_hidpp_rdfu_start_dfu_set_fw_entity(msg, priv->cached_fw_entity); if (!fu_struct_logitech_hidpp_rdfu_start_dfu_set_magic(msg, magic->data, magic->len, error)) return FALSE; g_debug("startDfu"); if (!fu_logitech_hidpp_device_transfer_msg(self, msg, error)) { g_prefix_error(error, "startDfu failed: "); return FALSE; } response = fu_struct_logitech_hidpp_rdfu_start_dfu_response_parse(msg->data, msg->len, 0, error); if (response == NULL) return FALSE; status = fu_struct_logitech_hidpp_rdfu_start_dfu_response_get_status_code(msg); if (status != FU_LOGITECH_HIDPP_RDFU_RESPONSE_CODE_DATA_TRANSFER_READY) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR, "unexpected status 0x%x = %s", status, fu_logitech_hidpp_rdfu_response_code_to_string(status)); return FALSE; } priv->rdfu_state = FU_LOGITECH_HIDPP_RDFU_STATE_TRANSFER; return TRUE; } static gboolean fu_logitech_hidpp_device_rdfu_check_status(FuLogitechHidppDevice *self, GByteArray *response, GError **error); static void fu_logitech_hidpp_device_rdfu_set_state(FuLogitechHidppDevice *self, FuLogitechHidppRdfuState state) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); priv->rdfu_state = state; priv->rdfu_block = 0; priv->rdfu_pkt = 0; } static gboolean fu_logitech_hidpp_device_rdfu_status_data_transfer_ready(FuLogitechHidppDevice *self, GByteArray *response, GError **error) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); guint16 block; g_autoptr(GByteArray) params = NULL; priv->rdfu_state = FU_LOGITECH_HIDPP_RDFU_STATE_TRANSFER; params = fu_struct_logitech_hidpp_rdfu_data_transfer_ready_parse( response->data, response->len, FU_STRUCT_LOGITECH_HIDPP_RDFU_RESPONSE_OFFSET_STATUS_CODE, error); if (params == NULL) return FALSE; block = fu_struct_logitech_hidpp_rdfu_data_transfer_ready_get_block_id(params); if (block != 0 && block <= priv->rdfu_block) { /* additional protection from misbehaviored devices */ fu_logitech_hidpp_device_rdfu_set_state(self, FU_LOGITECH_HIDPP_RDFU_STATE_RESUME_DFU); return TRUE; } priv->rdfu_block = block; priv->rdfu_pkt = 0; return TRUE; } static gboolean fu_logitech_hidpp_device_rdfu_status_data_transfer_wait(FuLogitechHidppDevice *self, GByteArray *response, GError **error) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); guint retry = 0; g_autoptr(GByteArray) params = NULL; params = fu_struct_logitech_hidpp_rdfu_data_transfer_wait_parse( response->data, response->len, FU_STRUCT_LOGITECH_HIDPP_RDFU_RESPONSE_OFFSET_STATUS_CODE, error); if (params == NULL) return FALSE; /* set the delay to wait the next event */ priv->rdfu_wait = fu_struct_logitech_hidpp_rdfu_data_transfer_wait_get_delay_ms(params); /* if we already in waiting loop just leave to avoid recursion */ if (priv->rdfu_state == FU_LOGITECH_HIDPP_RDFU_STATE_WAIT) return TRUE; priv->rdfu_state = FU_LOGITECH_HIDPP_RDFU_STATE_WAIT; while (priv->rdfu_state == FU_LOGITECH_HIDPP_RDFU_STATE_WAIT) { g_autoptr(GByteArray) wait_response = NULL; g_autoptr(FuLogitechHidppHidppMsg) msg_in_wait = fu_logitech_hidpp_msg_new(); g_autoptr(GError) error_local = NULL; /* wait for requested time or event */ if (!fu_logitech_hidpp_receive(FU_UDEV_DEVICE(self), msg_in_wait, priv->rdfu_wait * 2, /* be tolerant */ &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT)) { g_debug("ignored error: %s", error_local->message); /* let's try to reset with getDfuStatus */ fu_logitech_hidpp_device_rdfu_set_state( self, FU_LOGITECH_HIDPP_RDFU_STATE_RESUME_DFU); return TRUE; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } wait_response = fu_struct_logitech_hidpp_rdfu_response_parse( (guint8 *)msg_in_wait, fu_logitech_hidpp_msg_get_payload_length(msg_in_wait), 0, error); if (wait_response == NULL) return FALSE; /* check the message and set the new delay if requested additional wait */ if (!fu_logitech_hidpp_device_rdfu_check_status(self, wait_response, error)) return FALSE; /* too lot attempts in a raw, let's fail everything */ if (retry++ > FU_LOGITECH_HIDPP_DEVICE_RDFU_MAX_RETRIES) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "too lot of wait requests in a raw"); return FALSE; } } return TRUE; } static gboolean fu_logitech_hidpp_device_rdfu_status_pkt_ack(FuLogitechHidppDevice *self, GByteArray *response, GError **error) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); guint32 pkt; g_autoptr(GByteArray) params = NULL; priv->rdfu_state = FU_LOGITECH_HIDPP_RDFU_STATE_TRANSFER; params = fu_struct_logitech_hidpp_rdfu_dfu_transfer_pkt_ack_parse( response->data, response->len, FU_STRUCT_LOGITECH_HIDPP_RDFU_RESPONSE_OFFSET_STATUS_CODE, error); if (params == NULL) return FALSE; pkt = fu_struct_logitech_hidpp_rdfu_dfu_transfer_pkt_ack_get_pkt_number(params); /* expecting monotonic increase */ if (pkt != priv->rdfu_pkt + 1) { /* additional protection from misbehaviored devices */ if (pkt <= priv->rdfu_pkt) { fu_logitech_hidpp_device_rdfu_set_state( self, FU_LOGITECH_HIDPP_RDFU_STATE_NOT_STARTED); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "expecting ack %u for block %u, got %u", priv->rdfu_pkt + 1, priv->rdfu_block, pkt); return FALSE; } /* probably skipped the ACK, try to resume */ fu_logitech_hidpp_device_rdfu_set_state(self, FU_LOGITECH_HIDPP_RDFU_STATE_RESUME_DFU); return TRUE; } priv->rdfu_pkt = pkt; return TRUE; } static gboolean fu_logitech_hidpp_device_rdfu_check_status(FuLogitechHidppDevice *self, GByteArray *response, GError **error) { guint8 status = fu_struct_logitech_hidpp_rdfu_start_dfu_response_get_status_code(response); switch (status) { case FU_LOGITECH_HIDPP_RDFU_RESPONSE_CODE_DFU_NOT_STARTED: fu_logitech_hidpp_device_rdfu_set_state(self, FU_LOGITECH_HIDPP_RDFU_STATE_NOT_STARTED); break; case FU_LOGITECH_HIDPP_RDFU_RESPONSE_CODE_DATA_TRANSFER_READY: /* ready for transfer, next block requested */ if (!fu_logitech_hidpp_device_rdfu_status_data_transfer_ready(self, response, error)) return FALSE; break; case FU_LOGITECH_HIDPP_RDFU_RESPONSE_CODE_DATA_TRANSFER_WAIT: /* device requested to wait */ if (!fu_logitech_hidpp_device_rdfu_status_data_transfer_wait(self, response, error)) return FALSE; break; case FU_LOGITECH_HIDPP_RDFU_RESPONSE_CODE_DFU_TRANSFER_PKT_ACK: /* ok to transfer next packet */ if (!fu_logitech_hidpp_device_rdfu_status_pkt_ack(self, response, error)) return FALSE; break; case FU_LOGITECH_HIDPP_RDFU_RESPONSE_CODE_DFU_TRANSFER_COMPLETE: /* success! Apply and reboot */ fu_logitech_hidpp_device_rdfu_set_state(self, FU_LOGITECH_HIDPP_RDFU_STATE_APPLY); break; case FU_LOGITECH_HIDPP_RDFU_RESPONSE_CODE_INVALID_BLOCK: case FU_LOGITECH_HIDPP_RDFU_RESPONSE_CODE_DFU_STATE_ERROR: /* let's try to reset with getDfuStatus */ fu_logitech_hidpp_device_rdfu_set_state(self, FU_LOGITECH_HIDPP_RDFU_STATE_RESUME_DFU); break; case FU_LOGITECH_HIDPP_RDFU_RESPONSE_CODE_DFU_APPLY_PENDING: /* device is already waiting to apply the unknown deferred update. * Let's restart the update.*/ default: /* reset state */ fu_logitech_hidpp_device_rdfu_set_state(self, FU_LOGITECH_HIDPP_RDFU_STATE_NOT_STARTED); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "unexpected status 0x%x (%s)", status, fu_logitech_hidpp_rdfu_response_code_to_string(status)); return FALSE; } return TRUE; } static gboolean fu_logitech_hidpp_device_rdfu_get_dfu_status(FuLogitechHidppDevice *self, GError **error) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); guint8 idx; g_autoptr(FuStructLogitechHidppRdfuGetDfuStatus) msg = fu_struct_logitech_hidpp_rdfu_get_dfu_status_new(); g_autoptr(GByteArray) response = NULL; g_autoptr(GError) error_local = NULL; idx = fu_logitech_hidpp_device_feature_get_idx(self, FU_LOGITECH_HIDPP_FEATURE_RDFU); if (idx == 0x00) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "RDFU feature is required for getDfuStatus"); return FALSE; } fu_struct_logitech_hidpp_rdfu_get_dfu_status_set_device_id(msg, priv->device_idx); fu_struct_logitech_hidpp_rdfu_get_dfu_status_set_sub_id(msg, idx); fu_struct_logitech_hidpp_rdfu_get_dfu_status_set_fw_entity(msg, priv->cached_fw_entity); g_debug("getDfuStatus for entity %u", priv->cached_fw_entity); if (!fu_logitech_hidpp_device_transfer_msg(self, msg, error)) { g_prefix_error(error, "getDfuStatus failed: "); return FALSE; } response = fu_struct_logitech_hidpp_rdfu_response_parse(msg->data, msg->len, 0, error); if (response == NULL) return FALSE; return fu_logitech_hidpp_device_rdfu_check_status(self, response, error); } static gboolean fu_logitech_hidpp_device_rdfu_apply_dfu(FuLogitechHidppDevice *self, guint8 fw_entity, GError **error) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); guint8 idx; guint8 flags = FU_LOGITECH_HIDPP_RDFU_APPLY_FLAGS_FORCE_DFU_BIT; g_autoptr(FuStructLogitechHidppRdfuApplyDfu) msg = fu_struct_logitech_hidpp_rdfu_apply_dfu_new(); FuLogitechHidppHidppMsg *hidpp_msg = NULL; g_autoptr(GByteArray) response = NULL; idx = fu_logitech_hidpp_device_feature_get_idx(self, FU_LOGITECH_HIDPP_FEATURE_RDFU); if (idx == 0x00) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "RDFU feature is required for startDfu"); return FALSE; } if (priv->rdfu_state != FU_LOGITECH_HIDPP_RDFU_STATE_APPLY) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unable to apply the update"); return FALSE; } g_debug("applyDfu for entity %u", fw_entity); fu_struct_logitech_hidpp_rdfu_apply_dfu_set_device_id(msg, priv->device_idx); fu_struct_logitech_hidpp_rdfu_apply_dfu_set_sub_id(msg, idx); fu_struct_logitech_hidpp_rdfu_apply_dfu_set_fw_entity(msg, fw_entity); fu_struct_logitech_hidpp_rdfu_apply_dfu_set_flags(msg, flags); /* re-use pre-rustgen send */ hidpp_msg = (FuLogitechHidppHidppMsg *)msg->data; hidpp_msg->hidpp_version = priv->hidpp_version; /* don't expect the reply for forced applyDfu */ if (!fu_logitech_hidpp_send(FU_UDEV_DEVICE(self), hidpp_msg, FU_LOGITECH_HIDPP_DEVICE_TIMEOUT_MS, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_logitech_hidpp_device_rdfu_transfer_pkt(FuLogitechHidppDevice *self, const guint8 *data, const gsize datasz, GError **error) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); guint8 idx; g_autoptr(FuStructLogitechHidppRdfuTransferDfuData) msg = fu_struct_logitech_hidpp_rdfu_transfer_dfu_data_new(); g_autoptr(GByteArray) response = NULL; g_autoptr(GError) error_local = NULL; idx = fu_logitech_hidpp_device_feature_get_idx(self, FU_LOGITECH_HIDPP_FEATURE_RDFU); if (idx == 0x00) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "RDFU feature is required for startDfu"); return FALSE; } fu_struct_logitech_hidpp_rdfu_transfer_dfu_data_set_device_id(msg, priv->device_idx); fu_struct_logitech_hidpp_rdfu_transfer_dfu_data_set_sub_id(msg, idx); if (!fu_struct_logitech_hidpp_rdfu_transfer_dfu_data_set_data(msg, data, datasz, error)) return FALSE; g_debug("transferDfuData"); if (!fu_logitech_hidpp_device_transfer_msg(self, msg, error)) { g_prefix_error(error, "transferDfuData failed: "); return FALSE; } response = fu_struct_logitech_hidpp_rdfu_response_parse(msg->data, msg->len, 0, error); if (response == NULL) return FALSE; return fu_logitech_hidpp_device_rdfu_check_status(self, response, error); } static gboolean fu_logitech_hidpp_device_rdfu_transfer_data(FuLogitechHidppDevice *self, GPtrArray *blocks, GError **error) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); const GByteArray *block = NULL; g_autofree guint8 *data = g_malloc0(FU_LOGITECH_HIDPP_DEVICE_DATA_PKT_LONG); g_debug("send: block=%u, pkt=%04x", priv->rdfu_block, priv->rdfu_pkt); if (blocks->len < priv->rdfu_block) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "requested invalid block %u", priv->rdfu_block); return FALSE; } block = (GByteArray *)blocks->pdata[priv->rdfu_block]; if (!fu_memcpy_safe(data, FU_LOGITECH_HIDPP_DEVICE_DATA_PKT_LONG, 0, block->data, block->len, priv->rdfu_pkt * FU_LOGITECH_HIDPP_DEVICE_DATA_PKT_LONG, FU_LOGITECH_HIDPP_DEVICE_DATA_PKT_LONG, error)) { return FALSE; } return fu_logitech_hidpp_device_rdfu_transfer_pkt(self, data, FU_LOGITECH_HIDPP_DEVICE_DATA_PKT_LONG, error); } static gboolean fu_logitech_hidpp_device_feature_search(FuLogitechHidppDevice *self, guint16 feature, GError **error) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); FuLogitechHidppHidppMap *map; g_autoptr(FuLogitechHidppHidppMsg) msg = fu_logitech_hidpp_msg_new(); /* find the idx for the feature */ msg->report_id = FU_LOGITECH_HIDPP_REPORT_ID_SHORT; msg->device_id = priv->device_idx; msg->sub_id = 0x00; /* rootIndex */ msg->function_id = 0x00 << 4; /* getFeature */ msg->data[0] = feature >> 8; msg->data[1] = feature; msg->data[2] = 0x00; msg->hidpp_version = priv->hidpp_version; if (!fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self), msg, error)) { g_prefix_error(error, "failed to get idx for feature %s [0x%04x]: ", fu_logitech_hidpp_feature_to_string(feature), feature); return FALSE; } /* zero index */ if (msg->data[0] == 0x00) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "feature %s [0x%04x] not found", fu_logitech_hidpp_feature_to_string(feature), feature); return FALSE; } /* add to map */ map = g_new0(FuLogitechHidppHidppMap, 1); map->idx = msg->data[0]; map->feature = feature; g_ptr_array_add(priv->feature_index, map); g_debug("added feature %s [0x%04x] as idx %02x", fu_logitech_hidpp_feature_to_string(feature), feature, map->idx); return TRUE; } static gboolean fu_logitech_hidpp_device_probe(FuDevice *device, GError **error) { FuLogitechHidppDevice *self = FU_HIDPP_DEVICE(device); FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); /* nearly... */ fu_device_build_vendor_id_u16(device, "USB", 0x046D); /* * All devices connected to a Bolt receiver share the same * physical id, make them unique by using their pairing slot * (device index) as a basis for their logical id. */ if (priv->device_idx != FU_LOGITECH_HIDPP_DEVICE_IDX_WIRED && priv->device_idx != FU_LOGITECH_HIDPP_DEVICE_IDX_RECEIVER) { g_autoptr(GString) id_str = g_string_new(NULL); g_string_append_printf(id_str, "DEV_IDX=%d", priv->device_idx); fu_device_set_logical_id(device, id_str->str); } /* success */ return TRUE; } static void fu_logitech_hidpp_device_vid_pid_notify_cb(FuDevice *device, GParamSpec *pspec, gpointer user_data) { if (fu_device_get_pid(device) == 0x0) return; /* this is a non-standard extension, but essentially API */ fu_device_add_instance_u16(device, "VID", fu_device_get_vid(device)); fu_device_add_instance_u16(device, "PID", fu_device_get_pid(device)); fu_device_build_instance_id(device, NULL, "UFY", "VID", "PID", NULL); } static gboolean fu_logitech_hidpp_device_write_firmware_pkt(FuLogitechHidppDevice *self, guint8 idx, guint8 cmd, const guint8 *data, GError **error); static gboolean fu_logitech_hidpp_device_setup(FuDevice *device, GError **error) { FuLogitechHidppDevice *self = FU_HIDPP_DEVICE(device); FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); guint8 idx; const guint16 map_features[] = {FU_LOGITECH_HIDPP_FEATURE_GET_DEVICE_NAME_TYPE, FU_LOGITECH_HIDPP_FEATURE_I_FIRMWARE_INFO, FU_LOGITECH_HIDPP_FEATURE_BATTERY_LEVEL_STATUS, FU_LOGITECH_HIDPP_FEATURE_UNIFIED_BATTERY, FU_LOGITECH_HIDPP_FEATURE_DFU_CONTROL, FU_LOGITECH_HIDPP_FEATURE_DFU_CONTROL_SIGNED, FU_LOGITECH_HIDPP_FEATURE_DFU_CONTROL_BOLT, FU_LOGITECH_HIDPP_FEATURE_DFU, FU_LOGITECH_HIDPP_FEATURE_RDFU, FU_LOGITECH_HIDPP_FEATURE_ROOT}; if (fu_device_has_private_flag(device, FU_LOGITECH_HIDPP_DEVICE_FLAG_BLE)) { priv->hidpp_version = FU_HIDPP_VERSION_BLE; priv->device_idx = FU_LOGITECH_HIDPP_DEVICE_IDX_RECEIVER; /* * BLE devices might not be ready for ping right after * they come up -> wait a bit before pinging. */ fu_device_sleep(device, 1000); /* ms */ } if (fu_device_has_private_flag(device, FU_LOGITECH_HIDPP_DEVICE_FLAG_FORCE_RECEIVER_ID)) priv->device_idx = FU_LOGITECH_HIDPP_DEVICE_IDX_RECEIVER; /* ping device to get HID++ version */ if (!fu_logitech_hidpp_device_ping(self, error)) return FALSE; /* did not get ID */ if (priv->device_idx == FU_LOGITECH_HIDPP_DEVICE_IDX_WIRED) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no HID++ ID"); return FALSE; } /* add known root for HID++2.0 */ g_ptr_array_set_size(priv->feature_index, 0); if (priv->hidpp_version >= 2.f) { FuLogitechHidppHidppMap *map = g_new0(FuLogitechHidppHidppMap, 1); map->idx = 0x00; map->feature = FU_LOGITECH_HIDPP_FEATURE_ROOT; g_ptr_array_add(priv->feature_index, map); } /* map some *optional* HID++2.0 features we might use */ for (guint i = 0; map_features[i] != FU_LOGITECH_HIDPP_FEATURE_ROOT; i++) { g_autoptr(GError) error_local = NULL; if (!fu_logitech_hidpp_device_feature_search(self, map_features[i], &error_local)) { g_debug("%s", error_local->message); if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { /* timed out, so not trying any more */ break; } } } /* get the model ID, typically something like B3630000000000 */ if (!fu_logitech_hidpp_device_fetch_model_id(self, error)) return FALSE; /* try using HID++2.0 */ idx = fu_logitech_hidpp_device_feature_get_idx( self, FU_LOGITECH_HIDPP_FEATURE_GET_DEVICE_NAME_TYPE); if (idx != 0x00) { const gchar *tmp; g_autoptr(FuLogitechHidppHidppMsg) msg = fu_logitech_hidpp_msg_new(); msg->report_id = FU_LOGITECH_HIDPP_REPORT_ID_SHORT; msg->device_id = priv->device_idx; msg->sub_id = idx; msg->function_id = 0x02 << 4; /* getDeviceType */ msg->hidpp_version = priv->hidpp_version; if (!fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self), msg, error)) { g_prefix_error(error, "failed to get device type: "); return FALSE; } /* add nice-to-have data */ tmp = fu_logitech_hidpp_device_get_summary(msg->data[0]); if (tmp != NULL) fu_device_set_summary(FU_DEVICE(device), tmp); tmp = fu_logitech_hidpp_device_get_icon(msg->data[0]); if (tmp != NULL) fu_device_add_icon(FU_DEVICE(device), tmp); } idx = fu_logitech_hidpp_device_feature_get_idx(self, FU_LOGITECH_HIDPP_FEATURE_DFU_CONTROL); if (idx != 0x00) { fu_device_add_flag(FU_DEVICE(device), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(device), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_remove_flag(FU_DEVICE(device), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); fu_device_add_protocol(FU_DEVICE(self), "com.logitech.unifying"); } idx = fu_logitech_hidpp_device_feature_get_idx(self, FU_LOGITECH_HIDPP_FEATURE_DFU_CONTROL_BOLT); if (idx == 0x00) idx = fu_logitech_hidpp_device_feature_get_idx( self, FU_LOGITECH_HIDPP_FEATURE_DFU_CONTROL_SIGNED); if (idx != 0x00) { /* check the feature is available */ g_autoptr(FuLogitechHidppHidppMsg) msg = fu_logitech_hidpp_msg_new(); msg->report_id = FU_LOGITECH_HIDPP_REPORT_ID_SHORT; msg->device_id = priv->device_idx; msg->sub_id = idx; msg->function_id = 0x00 << 4; /* getDfuStatus */ msg->hidpp_version = priv->hidpp_version; if (!fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self), msg, error)) { g_prefix_error(error, "failed to get DFU status: "); return FALSE; } if ((msg->data[2] & 0x01) > 0) { g_warning("DFU mode not available"); } else { fu_device_remove_flag(FU_DEVICE(device), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); } fu_device_add_protocol(FU_DEVICE(device), "com.logitech.unifyingsigned"); fu_device_add_flag(FU_DEVICE(device), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(device), FWUPD_DEVICE_FLAG_UPDATABLE); } idx = fu_logitech_hidpp_device_feature_get_idx(self, FU_LOGITECH_HIDPP_FEATURE_DFU); if (idx != 0x00) { fu_device_add_flag(FU_DEVICE(device), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); fu_device_add_flag(FU_DEVICE(device), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(device), FWUPD_DEVICE_FLAG_UPDATABLE); if (fu_device_get_version(device) == NULL) { g_info("repairing device in bootloader mode"); fu_device_set_version(FU_DEVICE(device), "MPK00.00_B0000"); } /* we do not actually know which protocol when in recovery mode, * so force the metadata to have the specific regex set up */ fu_device_add_protocol(FU_DEVICE(self), "com.logitech.unifying"); fu_device_add_protocol(FU_DEVICE(self), "com.logitech.unifyingsigned"); } /* get the firmware information */ if (!fu_logitech_hidpp_device_fetch_firmware_info(self, error)) return FALSE; /* get the battery level */ if (!fu_logitech_hidpp_device_fetch_battery_level(self, error)) return FALSE; idx = fu_logitech_hidpp_device_feature_get_idx(self, FU_LOGITECH_HIDPP_FEATURE_RDFU); if (idx != 0x00) { /* get RDFU capabilities */ if (!fu_logitech_hidpp_device_rdfu_get_capabilities(self, error)) return FALSE; fu_device_add_protocol(FU_DEVICE(self), "com.logitech.rdfu"); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_LOGITECH_RDFU_FIRMWARE); fu_device_add_flag(FU_DEVICE(device), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); } /* poll for pings to track active state */ fu_device_set_poll_interval(device, FU_HIDPP_DEVICE_POLLING_INTERVAL); return TRUE; } static gboolean fu_logitech_hidpp_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuLogitechHidppDevice *self = FU_HIDPP_DEVICE(device); FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); guint8 idx; g_autoptr(FuLogitechHidppHidppMsg) msg = fu_logitech_hidpp_msg_new(); idx = fu_logitech_hidpp_device_feature_get_idx(self, FU_LOGITECH_HIDPP_FEATURE_RDFU); if (idx != 0x00) { g_debug("RDFU supported, no need to switch to bootloader mode"); return TRUE; } /* sanity check */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in bootloader mode, skipping"); return TRUE; } /* these may require user action */ idx = fu_logitech_hidpp_device_feature_get_idx(self, FU_LOGITECH_HIDPP_FEATURE_DFU_CONTROL_BOLT); if (idx == 0x00) idx = fu_logitech_hidpp_device_feature_get_idx(self, FU_LOGITECH_HIDPP_FEATURE_DFU_CONTROL); if (idx != 0x00) { FuDevice *parent; g_autoptr(FwupdRequest) request = fwupd_request_new(); g_autoptr(GError) error_local = NULL; msg->report_id = FU_LOGITECH_HIDPP_REPORT_ID_LONG; msg->device_id = priv->device_idx; msg->sub_id = idx; msg->function_id = 0x01 << 4; /* setDfuControl */ msg->data[0] = 0x01; /* enterDfu */ msg->data[1] = 0x00; /* dfuControlParam */ msg->data[2] = 0x00; /* unused */ msg->data[3] = 0x00; /* unused */ msg->data[4] = 'D'; msg->data[5] = 'F'; msg->data[6] = 'U'; msg->hidpp_version = priv->hidpp_version; msg->flags = FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_IGNORE_SUB_ID | FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_LONGER_TIMEOUT; if (!fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self), msg, &error_local)) { if (fu_device_has_private_flag( device, FU_LOGITECH_HIDPP_DEVICE_FLAG_NO_REQUEST_REQUIRED)) { g_debug("ignoring %s", error_local->message); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to put device into DFU mode: "); return FALSE; } fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* so we detect off then on */ parent = fu_device_get_parent(device); if (parent != NULL) fu_device_set_poll_interval(parent, 500); /* generate a message if not already set */ if (!fu_device_has_private_flag( device, FU_LOGITECH_HIDPP_DEVICE_FLAG_NO_REQUEST_REQUIRED)) { if (fu_device_get_update_message(device) == NULL) { g_autofree gchar *str = NULL; str = g_strdup_printf( "%s needs to be manually restarted to complete the update. " "Please turn it off and on.", fu_device_get_name(device)); fu_device_set_update_message(device, str); } fwupd_request_set_message(request, fu_device_get_update_message(device)); fwupd_request_add_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_IMMEDIATE); fwupd_request_set_id(request, FWUPD_REQUEST_ID_REMOVE_REPLUG); if (!fu_device_emit_request(device, request, progress, error)) return FALSE; } return TRUE; } /* this can reboot all by itself */ idx = fu_logitech_hidpp_device_feature_get_idx(self, FU_LOGITECH_HIDPP_FEATURE_DFU_CONTROL_SIGNED); if (idx != 0x00) { msg->report_id = FU_LOGITECH_HIDPP_REPORT_ID_LONG; msg->device_id = priv->device_idx; msg->sub_id = idx; msg->function_id = 0x01 << 4; /* setDfuControl */ msg->data[0] = 0x01; /* startDfu */ msg->data[1] = 0x00; /* dfuControlParam */ msg->data[2] = 0x00; /* unused */ msg->data[3] = 0x00; /* unused */ msg->data[4] = 'D'; msg->data[5] = 'F'; msg->data[6] = 'U'; msg->flags = FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_IGNORE_SUB_ID; if (!fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self), msg, error)) { g_prefix_error(error, "failed to put device into DFU mode: "); return FALSE; } fu_device_sleep(device, 200); /* ms */ return fu_logitech_hidpp_device_setup(FU_DEVICE(self), error); } /* we don't know how */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no method to detach"); return FALSE; } static gboolean fu_logitech_hidpp_device_check_status(guint8 status_raw, GError **error) { FuLogitechHidppStatus status = status_raw & 0x7f; const gchar *status_str = fu_logitech_hidpp_status_to_string(status); switch (status) { case FU_LOGITECH_HIDPP_STATUS_PACKET_SUCCESS: case FU_LOGITECH_HIDPP_STATUS_DFU_SUCCESS: case FU_LOGITECH_HIDPP_STATUS_DFU_SUCCESS_ENTITY_RESTART_REQUIRED: case FU_LOGITECH_HIDPP_STATUS_DFU_SUCCESS_SYSTEM_RESTART_REQUIRED: /* success */ g_debug("ignoring: %s", status_str); return TRUE; break; case FU_LOGITECH_HIDPP_STATUS_COMMAND_IN_PROGRESS: case FU_LOGITECH_HIDPP_STATUS_WAIT_FOR_EVENT: case FU_LOGITECH_HIDPP_STATUS_BLOCKED_COMMAND: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, status_str); break; case FU_LOGITECH_HIDPP_STATUS_GENERIC_ERROR04: case FU_LOGITECH_HIDPP_STATUS_GENERIC_ERROR10: case FU_LOGITECH_HIDPP_STATUS_BAD_VOLTAGE: case FU_LOGITECH_HIDPP_STATUS_DFU_NOT_STARTED: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, status_str); break; case FU_LOGITECH_HIDPP_STATUS_UNKNOWN12: case FU_LOGITECH_HIDPP_STATUS_BAD_MAGIC_STRING: case FU_LOGITECH_HIDPP_STATUS_BAD_FIRMWARE: case FU_LOGITECH_HIDPP_STATUS_UNSUPPORTED_ENCRYPTION_MODE: case FU_LOGITECH_HIDPP_STATUS_UNSUPPORTED_COMMAND: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, status_str); break; case FU_LOGITECH_HIDPP_STATUS_INVALID: case FU_LOGITECH_HIDPP_STATUS_BAD_SEQUENCE_NUMBER: case FU_LOGITECH_HIDPP_STATUS_ADDRESS_OUT_OF_RANGE: case FU_LOGITECH_HIDPP_STATUS_UNALIGNED_ADDRESS: case FU_LOGITECH_HIDPP_STATUS_BAD_SIZE: case FU_LOGITECH_HIDPP_STATUS_MISSING_PROGRAM_DATA: case FU_LOGITECH_HIDPP_STATUS_MISSING_CHECK_DATA: case FU_LOGITECH_HIDPP_STATUS_FIRMWARE_CHECK_FAILURE: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, status_str); break; case FU_LOGITECH_HIDPP_STATUS_PROGRAM_FAILED_TO_WRITE: case FU_LOGITECH_HIDPP_STATUS_PROGRAM_FAILED_TO_VERIFY: case FU_LOGITECH_HIDPP_STATUS_ERASE_FAILURE: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, status_str); break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "unhandled status value 0x%02x", status); break; } return FALSE; } static gboolean fu_logitech_hidpp_device_write_firmware_pkt(FuLogitechHidppDevice *self, guint8 idx, guint8 cmd, const guint8 *data, GError **error) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); guint32 packet_cnt; g_autoptr(FuLogitechHidppHidppMsg) msg = fu_logitech_hidpp_msg_new(); g_autoptr(GError) error_local = NULL; /* send firmware data */ msg->report_id = FU_LOGITECH_HIDPP_REPORT_ID_LONG; msg->device_id = priv->device_idx; msg->sub_id = idx; msg->function_id = cmd << 4; /* dfuStart or dfuCmdDataX */ msg->hidpp_version = priv->hidpp_version; /* enable transfer workaround for devices paired to Bolt receiver */ if (priv->device_idx != FU_LOGITECH_HIDPP_DEVICE_IDX_WIRED && priv->device_idx != FU_LOGITECH_HIDPP_DEVICE_IDX_RECEIVER) msg->flags = FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_RETRY_STUCK; if (!fu_memcpy_safe(msg->data, sizeof(msg->data), 0x0, /* dst */ data, 16, 0x0, /* src */ 16, error)) return FALSE; if (!fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self), msg, error)) { g_prefix_error(error, "failed to supply program data: "); return FALSE; } /* check error */ if (!fu_memread_uint32_safe(msg->data, sizeof(msg->data), 0x0, &packet_cnt, G_BIG_ENDIAN, error)) return FALSE; g_debug("packet_cnt=0x%04x", packet_cnt); if (fu_logitech_hidpp_device_check_status(msg->data[4], &error_local)) return TRUE; /* fatal error */ if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_BUSY)) { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } /* wait for the HID++ notification */ g_debug("ignoring: %s", error_local->message); for (guint retry = 0; retry < 10; retry++) { g_autoptr(FuLogitechHidppHidppMsg) msg2 = fu_logitech_hidpp_msg_new(); msg2->flags = FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_IGNORE_FNCT_ID; if (!fu_logitech_hidpp_receive(FU_UDEV_DEVICE(self), msg2, 15000, error)) return FALSE; if (fu_logitech_hidpp_msg_is_reply(msg, msg2)) { g_autoptr(GError) error2 = NULL; if (!fu_logitech_hidpp_device_check_status(msg2->data[4], &error2)) { g_debug("got %s, waiting a bit longer", error2->message); continue; } return TRUE; } g_debug("got wrong packet, continue to wait..."); } /* nothing in the queue */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "failed to get event after timeout"); return FALSE; } static gboolean fu_logitech_hidpp_device_write_firmware_dfu(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuLogitechHidppDevice *self = FU_HIDPP_DEVICE(device); FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); gsize sz = 0; const guint8 *data; guint8 cmd = 0x04; guint8 idx; g_autoptr(GBytes) fw = NULL; /* if we're in bootloader mode, we should be able to get this feature */ idx = fu_logitech_hidpp_device_feature_get_idx(self, FU_LOGITECH_HIDPP_FEATURE_DFU); if (idx == 0x00) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no DFU feature available"); return FALSE; } /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* flash hardware -- the first data byte is the fw entity */ data = g_bytes_get_data(fw, &sz); if (priv->cached_fw_entity != data[0]) { g_debug("updating cached entity 0x%x with 0x%x", priv->cached_fw_entity, data[0]); priv->cached_fw_entity = data[0]; } fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); for (gsize i = 0; i < sz / 16; i++) { /* send packet and wait for reply */ g_debug("send data at addr=0x%04x", (guint)i * 16); if (!fu_logitech_hidpp_device_write_firmware_pkt(self, idx, cmd, data + (i * 16), error)) { g_prefix_error(error, "failed to write @0x%04x: ", (guint)i * 16); return FALSE; } /* use sliding window */ cmd = (cmd + 1) % 4; /* update progress-bar */ fu_progress_set_percentage_full(progress, (i + 1) * 16, sz); } return TRUE; } static gboolean fu_logitech_hidpp_device_write_firmware_rdfu(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuLogitechHidppDevice *self = FU_HIDPP_DEVICE(device); FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); FuLogitechRdfuFirmware *entity_fw = NULL; guint8 idx; guint retry = 0; g_autoptr(GBytes) fw = NULL; g_autoptr(GByteArray) magic = NULL; g_autoptr(GPtrArray) blocks = NULL; g_autoptr(GError) error_local = NULL; g_autofree gchar *entity_id = g_strdup_printf("%u", priv->cached_fw_entity); g_autofree gchar *model_id = NULL; idx = fu_logitech_hidpp_device_feature_get_idx(self, FU_LOGITECH_HIDPP_FEATURE_RDFU); if (idx == 0x00) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no RDFU feature available"); return FALSE; } entity_fw = (FuLogitechRdfuFirmware *)fu_firmware_get_image_by_id(firmware, entity_id, error); if (entity_fw == NULL) return FALSE; model_id = fu_logitech_rdfu_firmware_get_model_id(entity_fw, error); if (model_id == NULL) return FALSE; if (g_strcmp0(priv->model_id, model_id) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "firmware for model %s, but the target is %s", model_id, priv->model_id); return FALSE; } magic = fu_logitech_rdfu_firmware_get_magic(entity_fw, error); if (magic == NULL) return FALSE; blocks = fu_logitech_rdfu_firmware_get_blocks(entity_fw, error); if (blocks == NULL) return FALSE; fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); fu_progress_set_id(progress, G_STRLOC); /* check if we in update mode already */ if (!fu_logitech_hidpp_device_rdfu_get_dfu_status(self, &error_local)) { if (error_local->message != NULL) g_debug("forcing startDFU, reason %s", error_local->message); /* try to drop the inner state at device */ fu_logitech_hidpp_device_rdfu_set_state(self, FU_LOGITECH_HIDPP_RDFU_STATE_NOT_STARTED); } /* device requested to start or restart for some reason */ if (priv->rdfu_state == FU_LOGITECH_HIDPP_RDFU_STATE_NOT_STARTED) { if (!fu_logitech_hidpp_device_rdfu_start_dfu(self, magic, error)) return FALSE; } while (priv->rdfu_state == FU_LOGITECH_HIDPP_RDFU_STATE_TRANSFER) { /* update progress-bar here to avoid jumps caused dfu-transfer-complete */ fu_progress_set_percentage_full(progress, priv->rdfu_block, blocks->len); /* send packet and wait for reply */ if (!fu_logitech_hidpp_device_rdfu_transfer_data(self, blocks, error)) return FALSE; /* additional protection from misbehaviored devices */ if (priv->rdfu_state != FU_LOGITECH_HIDPP_RDFU_STATE_TRANSFER) { if (!fu_logitech_hidpp_device_rdfu_get_dfu_status(self, error)) return FALSE; /* too many soft restarts, let's fail everything */ if (retry++ > FU_LOGITECH_HIDPP_DEVICE_RDFU_MAX_RETRIES) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "too lot recover attempts"); return FALSE; } } } g_debug("RDFU supported, applying the update"); if (!fu_logitech_hidpp_device_rdfu_apply_dfu(self, priv->cached_fw_entity, error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_logitech_hidpp_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuLogitechHidppDevice *self = FU_HIDPP_DEVICE(device); guint8 idx; g_autoptr(GBytes) fw = NULL; /* device should support either RDFU or DFU mode */ idx = fu_logitech_hidpp_device_feature_get_idx(self, FU_LOGITECH_HIDPP_FEATURE_RDFU); if (idx != 0x00) { return fu_logitech_hidpp_device_write_firmware_rdfu(device, firmware, progress, flags, error); } /* if we're in bootloader mode, we should be able to get this feature */ idx = fu_logitech_hidpp_device_feature_get_idx(self, FU_LOGITECH_HIDPP_FEATURE_DFU); if (idx != 0x00) { return fu_logitech_hidpp_device_write_firmware_dfu(device, firmware, progress, flags, error); } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no DFU or RDFU feature available"); return FALSE; } static gboolean fu_logitech_hidpp_device_reprobe_cb(FuDevice *device, gpointer user_data, GError **error) { return fu_logitech_hidpp_device_setup(device, error); } gboolean fu_logitech_hidpp_device_attach(FuLogitechHidppDevice *self, guint8 entity, FuProgress *progress, GError **error) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); FuDevice *device = FU_DEVICE(self); guint8 idx; g_autoptr(FuLogitechHidppHidppMsg) msg = fu_logitech_hidpp_msg_new(); g_autoptr(GError) error_local = NULL; idx = fu_logitech_hidpp_device_feature_get_idx(self, FU_LOGITECH_HIDPP_FEATURE_RDFU); if (idx == 0x00) { /* sanity check for DFU*/ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in runtime mode, skipping"); return TRUE; } /* if we're in bootloader mode, we should be able to get this feature */ idx = fu_logitech_hidpp_device_feature_get_idx(self, FU_LOGITECH_HIDPP_FEATURE_DFU); if (idx == 0x00) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no DFU feature available"); return FALSE; } /* reboot back into firmware mode */ msg->report_id = FU_LOGITECH_HIDPP_REPORT_ID_LONG; msg->device_id = priv->device_idx; msg->sub_id = idx; msg->function_id = 0x05 << 4; /* restart */ msg->data[0] = entity; /* fwEntity */ msg->hidpp_version = priv->hidpp_version; msg->flags = FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_IGNORE_SUB_ID | FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_IGNORE_SWID | /* inferred? */ FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_LONGER_TIMEOUT; if (!fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self), msg, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_READ) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_debug("ignoring '%s' on reset", error_local->message); } else { g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to restart device: "); return FALSE; } } } if (fu_device_has_private_flag(device, FU_LOGITECH_HIDPP_DEVICE_FLAG_REBIND_ATTACH)) { fu_device_set_poll_interval(device, 0); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); } else { /* device file hasn't been unbound/re-bound, just probe again */ if (!fu_device_retry(device, fu_logitech_hidpp_device_reprobe_cb, 10, NULL, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_logitech_hidpp_device_attach_cached(FuDevice *device, FuProgress *progress, GError **error) { FuLogitechHidppDevice *self = FU_HIDPP_DEVICE(device); FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); if (fu_device_has_private_flag(device, FU_LOGITECH_HIDPP_DEVICE_FLAG_REBIND_ATTACH)) fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return fu_logitech_hidpp_device_attach(self, priv->cached_fw_entity, progress, error); } static gboolean fu_logitech_hidpp_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuLogitechHidppDevice *self = FU_HIDPP_DEVICE(device); if (g_strcmp0(key, "LogitechHidppModelId") == 0) { fu_logitech_hidpp_device_set_model_id(self, value); return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_logitech_hidpp_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 97, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reload"); } static void fu_logitech_hidpp_device_finalize(GObject *object) { FuLogitechHidppDevice *self = FU_HIDPP_DEVICE(object); FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); g_ptr_array_unref(priv->feature_index); g_free(priv->model_id); G_OBJECT_CLASS(fu_logitech_hidpp_device_parent_class)->finalize(object); } static gboolean fu_logitech_hidpp_device_cleanup(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDevice *parent = fu_device_get_parent(device); if (parent != NULL) fu_device_set_poll_interval(parent, FU_HIDPP_RECEIVER_RUNTIME_POLLING_INTERVAL); return TRUE; } static void fu_logitech_hidpp_device_class_init(FuLogitechHidppDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_logitech_hidpp_device_finalize; device_class->setup = fu_logitech_hidpp_device_setup; device_class->write_firmware = fu_logitech_hidpp_device_write_firmware; device_class->attach = fu_logitech_hidpp_device_attach_cached; device_class->detach = fu_logitech_hidpp_device_detach; device_class->poll = fu_logitech_hidpp_device_poll; device_class->to_string = fu_logitech_hidpp_device_to_string; device_class->probe = fu_logitech_hidpp_device_probe; device_class->set_quirk_kv = fu_logitech_hidpp_device_set_quirk_kv; device_class->cleanup = fu_logitech_hidpp_device_cleanup; device_class->set_progress = fu_logitech_hidpp_device_set_progress; } static void fu_logitech_hidpp_device_init(FuLogitechHidppDevice *self) { FuLogitechHidppDevicePrivate *priv = GET_PRIVATE(self); priv->device_idx = FU_LOGITECH_HIDPP_DEVICE_IDX_WIRED; priv->feature_index = g_ptr_array_new_with_free_func(g_free); fu_logitech_hidpp_device_rdfu_set_state(self, FU_LOGITECH_HIDPP_RDFU_STATE_NOT_STARTED); fu_device_add_request_flag(FU_DEVICE(self), FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); fu_device_set_vid(FU_DEVICE(self), FU_LOGITECH_HIDPP_DEVICE_VID); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_vendor(FU_DEVICE(self), "Logitech"); fu_device_retry_set_delay(FU_DEVICE(self), 1000); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); fu_device_register_private_flag(FU_DEVICE(self), FU_LOGITECH_HIDPP_DEVICE_FLAG_FORCE_RECEIVER_ID); fu_device_register_private_flag(FU_DEVICE(self), FU_LOGITECH_HIDPP_DEVICE_FLAG_BLE); fu_device_register_private_flag(FU_DEVICE(self), FU_LOGITECH_HIDPP_DEVICE_FLAG_REBIND_ATTACH); fu_device_register_private_flag(FU_DEVICE(self), FU_LOGITECH_HIDPP_DEVICE_FLAG_NO_REQUEST_REQUIRED); fu_device_register_private_flag(FU_DEVICE(self), FU_LOGITECH_HIDPP_DEVICE_FLAG_ADD_RADIO); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_USER_REPLUG); fu_device_set_battery_threshold(FU_DEVICE(self), 20); g_signal_connect(FU_DEVICE(self), "notify::vid", G_CALLBACK(fu_logitech_hidpp_device_vid_pid_notify_cb), NULL); g_signal_connect(FU_DEVICE(self), "notify::pid", G_CALLBACK(fu_logitech_hidpp_device_vid_pid_notify_cb), NULL); } FuLogitechHidppDevice * fu_logitech_hidpp_device_new(FuUdevDevice *parent) { return g_object_new(FU_TYPE_HIDPP_DEVICE, "proxy", parent, "device-file", fu_udev_device_get_device_file(parent), NULL); } fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-device.h000066400000000000000000000023641501337203100242160ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_HIDPP_DEVICE (fu_logitech_hidpp_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuLogitechHidppDevice, fu_logitech_hidpp_device, FU, HIDPP_DEVICE, FuUdevDevice) struct _FuLogitechHidppDeviceClass { FuUdevDeviceClass parent_class; /* TODO: overridable methods */ }; #define FU_LOGITECH_HIDPP_DEVICE_FLAG_FORCE_RECEIVER_ID "force-receiver-id" #define FU_LOGITECH_HIDPP_DEVICE_FLAG_BLE "ble" #define FU_LOGITECH_HIDPP_DEVICE_FLAG_REBIND_ATTACH "rebind-attach" #define FU_LOGITECH_HIDPP_DEVICE_FLAG_NO_REQUEST_REQUIRED "no-request-required" #define FU_LOGITECH_HIDPP_DEVICE_FLAG_ADD_RADIO "add-radio" void fu_logitech_hidpp_device_set_device_idx(FuLogitechHidppDevice *self, guint8 device_idx); guint16 fu_logitech_hidpp_device_get_hidpp_pid(FuLogitechHidppDevice *self); void fu_logitech_hidpp_device_set_hidpp_pid(FuLogitechHidppDevice *self, guint16 hidpp_pid); gboolean fu_logitech_hidpp_device_attach(FuLogitechHidppDevice *self, guint8 entity, FuProgress *progress, GError **error); FuLogitechHidppDevice * fu_logitech_hidpp_device_new(FuUdevDevice *parent); fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-hidpp-msg.c000066400000000000000000000130571501337203100246430ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-logitech-hidpp-hidpp-msg.h" #include "fu-logitech-hidpp-hidpp.h" #include "fu-logitech-hidpp-struct.h" FuLogitechHidppHidppMsg * fu_logitech_hidpp_msg_new(void) { return g_new0(FuLogitechHidppHidppMsg, 1); } gsize fu_logitech_hidpp_msg_get_payload_length(FuLogitechHidppHidppMsg *msg) { if (msg->report_id == FU_LOGITECH_HIDPP_REPORT_ID_SHORT) return 0x07; if (msg->report_id == FU_LOGITECH_HIDPP_REPORT_ID_LONG) return 0x14; if (msg->report_id == FU_LOGITECH_HIDPP_REPORT_ID_VERY_LONG) return 0x2f; if (msg->report_id == HIDPP_REPORT_NOTIFICATION) return 0x08; return 0x0; } const gchar * fu_logitech_hidpp_msg_fcn_id_to_string(FuLogitechHidppHidppMsg *msg) { g_return_val_if_fail(msg != NULL, NULL); switch (msg->sub_id) { case FU_LOGITECH_HIDPP_SUBID_SET_REGISTER: case FU_LOGITECH_HIDPP_SUBID_GET_REGISTER: case FU_LOGITECH_HIDPP_SUBID_SET_LONG_REGISTER: case FU_LOGITECH_HIDPP_SUBID_GET_LONG_REGISTER: case FU_LOGITECH_HIDPP_SUBID_SET_VERY_LONG_REGISTER: case FU_LOGITECH_HIDPP_SUBID_GET_VERY_LONG_REGISTER: return fu_logitech_hidpp_register_to_string(msg->function_id); break; default: break; } return NULL; } gboolean fu_logitech_hidpp_msg_is_reply(FuLogitechHidppHidppMsg *msg1, FuLogitechHidppHidppMsg *msg2) { g_return_val_if_fail(msg1 != NULL, FALSE); g_return_val_if_fail(msg2 != NULL, FALSE); if (msg1->device_id != msg2->device_id && msg1->device_id != FU_LOGITECH_HIDPP_DEVICE_IDX_WIRED && msg2->device_id != FU_LOGITECH_HIDPP_DEVICE_IDX_WIRED) return FALSE; if (msg1->flags & FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_IGNORE_SUB_ID || msg2->flags & FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_IGNORE_SUB_ID) return TRUE; if (msg1->sub_id != msg2->sub_id) return FALSE; if (msg1->flags & FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_IGNORE_FNCT_ID || msg2->flags & FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_IGNORE_FNCT_ID) return TRUE; if (msg1->function_id != msg2->function_id) return FALSE; return TRUE; } /* HID++ error */ gboolean fu_logitech_hidpp_msg_is_error(FuLogitechHidppHidppMsg *msg, GError **error) { g_return_val_if_fail(msg != NULL, FALSE); if (msg->sub_id == FU_LOGITECH_HIDPP_SUBID_ERROR_MSG) { const gchar *text = fu_logitech_hidpp_err_to_string(msg->data[1]); switch (msg->data[1]) { case FU_LOGITECH_HIDPP_ERR_INVALID_SUBID: case FU_LOGITECH_HIDPP_ERR_TOO_MANY_DEVICES: case FU_LOGITECH_HIDPP_ERR_REQUEST_UNAVAILABLE: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, text); break; case FU_LOGITECH_HIDPP_ERR_INVALID_ADDRESS: case FU_LOGITECH_HIDPP_ERR_INVALID_VALUE: case FU_LOGITECH_HIDPP_ERR_ALREADY_EXISTS: case FU_LOGITECH_HIDPP_ERR_INVALID_PARAM_VALUE: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, text); break; case FU_LOGITECH_HIDPP_ERR_CONNECT_FAIL: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, text); break; case FU_LOGITECH_HIDPP_ERR_BUSY: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, text); break; case FU_LOGITECH_HIDPP_ERR_UNKNOWN_DEVICE: case FU_LOGITECH_HIDPP_ERR_RESOURCE_ERROR: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, text); break; case FU_LOGITECH_HIDPP_ERR_WRONG_PIN_CODE: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_AUTH_FAILED, "the pin code was wrong"); break; default: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "generic failure"); } return FALSE; } if (msg->sub_id == FU_LOGITECH_HIDPP_SUBID_ERROR_MSG_20) { const gchar *text = fu_logitech_hidpp_err2_to_string(msg->data[1]); switch (msg->data[1]) { case FU_LOGITECH_HIDPP_ERR2_INVALID_ARGUMENT: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "Invalid argument 0x%02x", msg->data[2]); break; case FU_LOGITECH_HIDPP_ERR2_OUT_OF_RANGE: case FU_LOGITECH_HIDPP_ERR2_HW_ERROR: case FU_LOGITECH_HIDPP_ERR2_INVALID_FEATURE_INDEX: case FU_LOGITECH_HIDPP_ERR2_INVALID_FUNCTION_ID: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, text); break; case FU_LOGITECH_HIDPP_ERR2_BUSY: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "busy"); break; case FU_LOGITECH_HIDPP_ERR2_UNSUPPORTED: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, text); break; default: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "generic failure"); break; } return FALSE; } return TRUE; } void fu_logitech_hidpp_msg_copy(FuLogitechHidppHidppMsg *msg_dst, const FuLogitechHidppHidppMsg *msg_src) { g_return_if_fail(msg_dst != NULL); g_return_if_fail(msg_src != NULL); memset(msg_dst->data, 0x00, sizeof(msg_dst->data)); msg_dst->device_id = msg_src->device_id; msg_dst->sub_id = msg_src->sub_id; msg_dst->function_id = msg_src->function_id; memcpy(msg_dst->data, msg_src->data, sizeof(msg_dst->data)); /* nocheck:blocked */ } /* filter HID++1.0 messages */ gboolean fu_logitech_hidpp_msg_is_hidpp10_compat(FuLogitechHidppHidppMsg *msg) { g_return_val_if_fail(msg != NULL, FALSE); if (msg->sub_id == 0x40 || msg->sub_id == 0x41 || msg->sub_id == 0x49 || msg->sub_id == 0x4b || msg->sub_id == 0x8f) { return TRUE; } return FALSE; } gboolean fu_logitech_hidpp_msg_verify_swid(FuLogitechHidppHidppMsg *msg) { g_return_val_if_fail(msg != NULL, FALSE); if ((msg->function_id & 0x0f) != FU_LOGITECH_HIDPP_HIDPP_MSG_SW_ID) return FALSE; return TRUE; } fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-hidpp-msg.h000066400000000000000000000035051501337203100246450ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include typedef enum { FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_NONE, FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_LONGER_TIMEOUT = 1 << 0, FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_IGNORE_SUB_ID = 1 << 1, FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_IGNORE_FNCT_ID = 1 << 2, FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_IGNORE_SWID = 1 << 3, FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_RETRY_STUCK = 1 << 4, /*< private >*/ FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_LAST } FuLogitechHidppHidppMsgFlags; typedef struct __attribute__((packed)) { /* nocheck:blocked */ guint8 report_id; guint8 device_id; guint8 sub_id; guint8 function_id; /* funcId:software_id */ guint8 data[47]; /* maximum supported by Windows XP SP2 */ /* not included in the packet sent to the hardware */ guint32 flags; guint8 hidpp_version; } FuLogitechHidppHidppMsg; /* this is specific to fwupd */ #define FU_LOGITECH_HIDPP_HIDPP_MSG_SW_ID 0x07 #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuLogitechHidppHidppMsg, g_free); #pragma clang diagnostic pop FuLogitechHidppHidppMsg * fu_logitech_hidpp_msg_new(void); void fu_logitech_hidpp_msg_copy(FuLogitechHidppHidppMsg *msg_dst, const FuLogitechHidppHidppMsg *msg_src); gsize fu_logitech_hidpp_msg_get_payload_length(FuLogitechHidppHidppMsg *msg); gboolean fu_logitech_hidpp_msg_is_reply(FuLogitechHidppHidppMsg *msg1, FuLogitechHidppHidppMsg *msg2); gboolean fu_logitech_hidpp_msg_is_hidpp10_compat(FuLogitechHidppHidppMsg *msg); gboolean fu_logitech_hidpp_msg_is_error(FuLogitechHidppHidppMsg *msg, GError **error); gboolean fu_logitech_hidpp_msg_verify_swid(FuLogitechHidppHidppMsg *msg); const gchar * fu_logitech_hidpp_msg_fcn_id_to_string(FuLogitechHidppHidppMsg *msg); fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-hidpp.c000066400000000000000000000157711501337203100240640ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-logitech-hidpp-common.h" #include "fu-logitech-hidpp-hidpp.h" #include "fu-logitech-hidpp-struct.h" static gchar * fu_logitech_hidpp_msg_to_string(FuLogitechHidppHidppMsg *msg) { g_autoptr(GError) error = NULL; g_autoptr(GString) flags_str = g_string_new(NULL); g_autoptr(GString) str = g_string_new(NULL); g_return_val_if_fail(msg != NULL, NULL); if (msg->flags == FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_NONE) { g_string_append(flags_str, "none"); } else { if (msg->flags & FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_LONGER_TIMEOUT) g_string_append(flags_str, "longer-timeout,"); if (msg->flags & FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_IGNORE_SUB_ID) g_string_append(flags_str, "ignore-sub-id,"); if (msg->flags & FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_IGNORE_FNCT_ID) g_string_append(flags_str, "ignore-fnct-id,"); if (msg->flags & FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_IGNORE_SWID) g_string_append(flags_str, "ignore-swid,"); if (flags_str->len > 0) g_string_truncate(flags_str, flags_str->len - 1); } g_string_append_printf(str, "flags: %02x [%s]\n", msg->flags, flags_str->str); g_string_append_printf(str, "report-id: %02x [%s]\n", msg->report_id, fu_logitech_hidpp_report_id_to_string(msg->report_id)); g_string_append_printf(str, "device-id: %02x [%s]\n", msg->device_id, fu_logitech_hidpp_device_idx_to_string(msg->device_id)); g_string_append_printf(str, "sub-id: %02x [%s]\n", msg->sub_id, fu_logitech_hidpp_subid_to_string(msg->sub_id)); g_string_append_printf(str, "function-id: %02x [%s]\n", msg->function_id, fu_logitech_hidpp_msg_fcn_id_to_string(msg)); if (!fu_logitech_hidpp_msg_is_error(msg, &error)) { g_string_append_printf(str, "error: %s\n", error->message); } if (str->len > 0) g_string_truncate(str, str->len - 1); return g_string_free(g_steal_pointer(&str), FALSE); } gboolean fu_logitech_hidpp_send(FuUdevDevice *udev_device, FuLogitechHidppHidppMsg *msg, guint timeout, GError **error) { gsize len = fu_logitech_hidpp_msg_get_payload_length(msg); FuIOChannelFlags write_flags = FU_IO_CHANNEL_FLAG_FLUSH_INPUT; g_autofree gchar *str = NULL; /* only for HID++2.0 */ if (msg->hidpp_version >= 2.f) msg->function_id |= FU_LOGITECH_HIDPP_HIDPP_MSG_SW_ID; /* force long reports for BLE-direct devices */ if (msg->hidpp_version == FU_HIDPP_VERSION_BLE) { msg->report_id = FU_LOGITECH_HIDPP_REPORT_ID_LONG; len = 20; } fu_dump_raw(G_LOG_DOMAIN, "host->device", (guint8 *)msg, len); /* debugging */ str = fu_logitech_hidpp_msg_to_string(msg); g_debug("%s", str); /* only use blocking IO when it will be a short timeout for reboot */ if ((msg->flags & FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_LONGER_TIMEOUT) == 0) write_flags |= FU_IO_CHANNEL_FLAG_USE_BLOCKING_IO; /* HID */ if (!fu_udev_device_write(udev_device, (const guint8 *)msg, len, timeout, write_flags, error)) { g_prefix_error(error, "failed to send: "); return FALSE; } /* success */ return TRUE; } gboolean fu_logitech_hidpp_receive(FuUdevDevice *udev_device, FuLogitechHidppHidppMsg *msg, guint timeout, GError **error) { gsize read_size = 0; g_autofree gchar *str = NULL; if (!fu_udev_device_read(udev_device, (guint8 *)msg, sizeof(FuLogitechHidppHidppMsg), &read_size, timeout, FU_IO_CHANNEL_FLAG_SINGLE_SHOT, error)) { g_prefix_error(error, "failed to receive: "); return FALSE; } /* check long enough, but allow returning oversize packets */ fu_dump_raw(G_LOG_DOMAIN, "device->host", (guint8 *)msg, read_size); if (read_size < fu_logitech_hidpp_msg_get_payload_length(msg)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "message length too small, " "got %" G_GSIZE_FORMAT " expected %" G_GSIZE_FORMAT, read_size, fu_logitech_hidpp_msg_get_payload_length(msg)); return FALSE; } /* debugging */ str = fu_logitech_hidpp_msg_to_string(msg); g_debug("%s", str); /* success */ return TRUE; } gboolean fu_logitech_hidpp_transfer(FuUdevDevice *udev_device, FuLogitechHidppHidppMsg *msg, GError **error) { guint timeout = FU_LOGITECH_HIDPP_DEVICE_TIMEOUT_MS; guint ignore_cnt = 0; g_autoptr(FuLogitechHidppHidppMsg) msg_tmp = fu_logitech_hidpp_msg_new(); /* increase timeout for some operations */ if (msg->flags & FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_LONGER_TIMEOUT) timeout *= 10; /* send request */ if (!fu_logitech_hidpp_send(udev_device, msg, timeout, error)) return FALSE; /* keep trying to receive until we get a valid reply */ while (1) { msg_tmp->hidpp_version = msg->hidpp_version; if (msg->flags & FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_RETRY_STUCK) { g_autoptr(GError) error_local = NULL; /* retry the send once case the device is "stuck" */ if (!fu_logitech_hidpp_receive(udev_device, msg_tmp, 1000, &error_local)) { if (!fu_logitech_hidpp_send(udev_device, msg, timeout, error)) return FALSE; if (!fu_logitech_hidpp_receive(udev_device, msg_tmp, timeout, error)) { g_prefix_error(error, "failed to receive: "); return FALSE; } } } else { if (!fu_logitech_hidpp_receive(udev_device, msg_tmp, timeout, error)) { g_prefix_error(error, "failed to receive: "); return FALSE; } } /* we don't know how to handle this report packet */ if (fu_logitech_hidpp_msg_get_payload_length(msg_tmp) == 0x0) { g_debug("HID++1.0 report 0x%02x has unknown length, ignoring", msg_tmp->report_id); continue; } /* maybe something is also writing to the device? -- * we can't use the SwID as this is a HID++2.0 feature */ if (!fu_logitech_hidpp_msg_is_error(msg_tmp, error)) return FALSE; /* is valid reply */ if (fu_logitech_hidpp_msg_is_reply(msg, msg_tmp)) break; /* to ensure compatibility when an HID++ 2.0 device is * connected to an HID++ 1.0 receiver, any feature index * corresponding to an HID++ 1.0 sub-identifier which could be * sent by the receiver, must be assigned to a dummy feature */ if (msg->hidpp_version >= 2.f) { if (fu_logitech_hidpp_msg_is_hidpp10_compat(msg_tmp)) { g_debug("ignoring HID++1.0 reply"); continue; } /* not us */ if ((msg->flags & FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_IGNORE_SWID) == 0) { if (!fu_logitech_hidpp_msg_verify_swid(msg_tmp)) { g_debug("ignoring reply with SwId 0x%02i, expected 0x%02i", msg_tmp->function_id & 0x0f, FU_LOGITECH_HIDPP_HIDPP_MSG_SW_ID); continue; } } } /* hardware not responding */ if (ignore_cnt++ > 10) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "too many messages to ignore"); return FALSE; } g_debug("ignoring message %u", ignore_cnt); }; /* copy over data */ fu_logitech_hidpp_msg_copy(msg, msg_tmp); return TRUE; } fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-hidpp.h000066400000000000000000000024501501337203100240570ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include /* * Based on the HID++ documentation provided by Nestor Lopez Casado at: * https://drive.google.com/folderview?id=0BxbRzx7vEV7eWmgwazJ3NUFfQ28&usp=sharing */ #define HIDPP_REPORT_NOTIFICATION 0x01 /* * Bolt registers */ #define BOLT_REGISTER_HIDPP_REPORTING 0x00 #define BOLT_REGISTER_CONNECTION_STATE 0x02 #define BOLT_REGISTER_DEVICE_ACTIVITY 0xB3 #define BOLT_REGISTER_PAIRING_INFORMATION 0xB5 #define BOLT_REGISTER_PERFORM_DEVICE_DISCOVERY 0xC0 #define BOLT_REGISTER_PERFORM_DEVICE_PAIRING 0xC1 #define BOLT_REGISTER_RESET 0xF2 #define BOLT_REGISTER_RECEIVER_FW_INFORMATION 0xF4 #define BOLT_REGISTER_DFU_CONTROL 0xF5 #define BOLT_REGISTER_UNIQUE_IDENTIFIER 0xFB #include "fu-logitech-hidpp-hidpp-msg.h" gboolean fu_logitech_hidpp_send(FuUdevDevice *udev_device, FuLogitechHidppHidppMsg *msg, guint timeout, GError **error); gboolean fu_logitech_hidpp_receive(FuUdevDevice *udev_device, FuLogitechHidppHidppMsg *msg, guint timeout, GError **error); gboolean fu_logitech_hidpp_transfer(FuUdevDevice *udev_device, FuLogitechHidppHidppMsg *msg, GError **error); fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-plugin.c000066400000000000000000000031701501337203100242440ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-logitech-hidpp-bootloader-nordic.h" #include "fu-logitech-hidpp-bootloader-texas.h" #include "fu-logitech-hidpp-common.h" #include "fu-logitech-hidpp-device.h" #include "fu-logitech-hidpp-plugin.h" #include "fu-logitech-hidpp-runtime-bolt.h" #include "fu-logitech-hidpp-runtime-unifying.h" #include "fu-logitech-rdfu-firmware.h" struct _FuLogitechHidppPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuLogitechHidppPlugin, fu_logitech_hidpp_plugin, FU_TYPE_PLUGIN) static void fu_logitech_hidpp_plugin_init(FuLogitechHidppPlugin *self) { } static void fu_logitech_hidpp_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "LogitechHidppModelId"); fu_plugin_add_udev_subsystem(plugin, "hidraw"); fu_plugin_add_rule(plugin, FU_PLUGIN_RULE_CONFLICTS, "unifying"); fu_plugin_add_device_gtype(plugin, FU_TYPE_LOGITECH_HIDPP_BOOTLOADER_NORDIC); fu_plugin_add_device_gtype(plugin, FU_TYPE_LOGITECH_HIDPP_BOOTLOADER_TEXAS); fu_plugin_add_device_gtype(plugin, FU_TYPE_HIDPP_RUNTIME_UNIFYING); fu_plugin_add_device_gtype(plugin, FU_TYPE_HIDPP_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_HIDPP_RUNTIME_BOLT); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_LOGITECH_RDFU_FIRMWARE); } static void fu_logitech_hidpp_plugin_class_init(FuLogitechHidppPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_logitech_hidpp_plugin_constructed; } fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-plugin.h000066400000000000000000000004371501337203100242540ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuLogitechHidppPlugin, fu_logitech_hidpp_plugin, FU, LOGITECH_HIDPP_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-radio.c000066400000000000000000000076261501337203100240560ustar00rootroot00000000000000/* * Copyright 2021 Ricardo Cañuelo * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-logitech-hidpp-device.h" #include "fu-logitech-hidpp-radio.h" struct _FuLogitechHidppRadio { FuDevice parent_instance; guint8 entity; }; G_DEFINE_TYPE(FuLogitechHidppRadio, fu_logitech_hidpp_radio, FU_TYPE_DEVICE) static void fu_logitech_hidpp_radio_to_string(FuDevice *device, guint idt, GString *str) { FuLogitechHidppRadio *self = FU_HIDPP_RADIO(device); fwupd_codec_string_append_hex(str, idt, "Entity", self->entity); } static gboolean fu_logitech_hidpp_radio_attach(FuDevice *device, FuProgress *progress, GError **error) { FuLogitechHidppRadio *self = FU_HIDPP_RADIO(device); FuDevice *parent = fu_device_get_parent(device); g_autoptr(FuDeviceLocker) locker = NULL; /* open */ locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return fu_logitech_hidpp_device_attach(FU_HIDPP_DEVICE(parent), self->entity, progress, error); } static gboolean fu_logitech_hidpp_radio_detach(FuDevice *device, FuProgress *progress, GError **error) { FuDevice *parent = fu_device_get_parent(device); g_autoptr(FuDeviceLocker) locker = NULL; /* open */ locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; if (!fu_device_has_flag(parent, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return fu_device_detach_full(parent, progress, error); } static gboolean fu_logitech_hidpp_radio_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDevice *parent = fu_device_get_parent(device); g_autoptr(FuDeviceLocker) locker = NULL; /* open */ locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; return fu_device_write_firmware(parent, firmware, progress, flags, error); } static void fu_logitech_hidpp_radio_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 96, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 3, "reload"); } static void fu_logitech_hidpp_radio_init(FuLogitechHidppRadio *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_set_vendor(FU_DEVICE(self), "Logitech"); fu_device_set_name(FU_DEVICE(self), "Radio"); fu_device_set_install_duration(FU_DEVICE(self), 270); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_BATTERY); fu_device_add_protocol(FU_DEVICE(self), "com.logitech.unifyingsigned"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_HEX); } static void fu_logitech_hidpp_radio_class_init(FuLogitechHidppRadioClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->detach = fu_logitech_hidpp_radio_detach; device_class->attach = fu_logitech_hidpp_radio_attach; device_class->write_firmware = fu_logitech_hidpp_radio_write_firmware; device_class->to_string = fu_logitech_hidpp_radio_to_string; device_class->set_progress = fu_logitech_hidpp_radio_set_progress; } FuLogitechHidppRadio * fu_logitech_hidpp_radio_new(FuContext *ctx, guint8 entity) { FuLogitechHidppRadio *self = NULL; self = g_object_new(FU_TYPE_LOGITECH_HIDPP_RADIO, "context", ctx, NULL); self->entity = entity; return self; } fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-radio.h000066400000000000000000000006411501337203100240510ustar00rootroot00000000000000/* * Copyright 2021 Ricardo Cañuelo * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_LOGITECH_HIDPP_RADIO (fu_logitech_hidpp_radio_get_type()) G_DECLARE_FINAL_TYPE(FuLogitechHidppRadio, fu_logitech_hidpp_radio, FU, HIDPP_RADIO, FuDevice) FuLogitechHidppRadio * fu_logitech_hidpp_radio_new(FuContext *ctx, guint8 entity); fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-rdfu.rs000066400000000000000000000077671501337203100241300ustar00rootroot00000000000000// Copyright 2024 Denis Pynkin // SPDX-License-Identifier: LGPL-2.1-or-later // the same as FuLogitechHidppReportId, need for the header #[repr(u8)] enum FuLogitechHidppRdfuReportId { Short = 0x10, Long = 0x11, VeryLong = 0x12, Notification = 0x01, } #[repr(u8)] enum FuLogitechHidppRdfuState { NotStarted, Transfer, Wait, // waiting the event from the device ResumeDfu, // for soft recover Apply, } #[repr(u8)] enum FuLogitechHidppRdfuFunc { GetCapabilities = 0, StartDfu = 1 << 4, GetDfuStatus = 2 << 4, ApplyDfu = 3 << 4, TransferDfuData = 4 << 4, } // For both status and error codes #[derive(ToString)] #[repr(u8)] enum FuLogitechHidppRdfuResponseCode { // Status Codes DfuNotStarted = 0x01, DataTransferReady = 0x02, DataTransferWait = 0x03, DfuTransferComplete = 0x04, DfuApplyPending = 0x05, DfuTransferPktAck = 0x06, DfuAbort = 0x07, // Error Codes InvalidMagicString = 0x80, InvalidFwEntity = 0x81, DeviceBusy = 0x82, DeviceOperationFailure = 0x83, NotSupported = 0x84, DfuStateError = 0x85, InvalidBlock = 0x86, GenericError = 0xFF, } #[derive(Default, Parse)] struct FuStructLogitechHidppRdfuResponse { report_id: FuLogitechHidppRdfuReportId == Long, device_id: u8, sub_id: u8, function_id: FuLogitechHidppRdfuFunc, fw_entity: u8, status_code: FuLogitechHidppRdfuResponseCode, parameters: [u8; 14], } #[repr(u8)] enum FuLogitechHidppRdfuCapabilities { ResumableDfuBit = 1, DeferrableDfuBit = 1 << 1, ForcibleDfuBit = 1 << 2, } #[derive(New, Default)] struct FuStructLogitechHidppRdfuGetCapabilities { report_id: FuLogitechHidppRdfuReportId == Long, device_id: u8, sub_id: u8, function_id: FuLogitechHidppRdfuFunc == GetCapabilities, data: [u8; 16], } #[derive(Default, Parse)] struct FuStructLogitechHidppRdfuCapabilities { report_id: FuLogitechHidppRdfuReportId == Long, device_id: u8, sub_id: u8, function_id: FuLogitechHidppRdfuFunc == GetCapabilities, capabilities: u8, data: [u8; 15], } #[derive(New, Default)] struct FuStructLogitechHidppRdfuGetDfuStatus { report_id: FuLogitechHidppRdfuReportId == Long, device_id: u8, sub_id: u8, function_id: FuLogitechHidppRdfuFunc == GetDfuStatus, fw_entity: u8, } #[derive(New, Default)] struct FuStructLogitechHidppRdfuStartDfu { report_id: FuLogitechHidppRdfuReportId == Long, device_id: u8, sub_id: u8, function_id: FuLogitechHidppRdfuFunc == StartDfu, fw_entity: u8, magic: [u8; 10], } #[derive(Default, Parse)] struct FuStructLogitechHidppRdfuStartDfuResponse { report_id: FuLogitechHidppRdfuReportId == Long, device_id: u8, sub_id: u8, function_id: FuLogitechHidppRdfuFunc == StartDfu, fw_entity: u8, status_code: FuLogitechHidppRdfuResponseCode, status_params: u8, additional_status_params: u8, } #[derive(New, Default)] struct FuStructLogitechHidppRdfuTransferDfuData { report_id: FuLogitechHidppRdfuReportId == Long, device_id: u8, sub_id: u8, function_id: FuLogitechHidppRdfuFunc == TransferDfuData, data: [u8; 16], } #[derive(Default, Parse)] struct FuStructLogitechHidppRdfuDataTransferReady { status_code: FuLogitechHidppRdfuResponseCode == DataTransferReady, block_id: u16be, } #[derive(Default, Parse)] struct FuStructLogitechHidppRdfuDataTransferWait { status_code: FuLogitechHidppRdfuResponseCode == DataTransferWait, delay_ms: u16be, } #[derive(Default, Parse)] struct FuStructLogitechHidppRdfuDfuTransferPktAck { status_code: FuLogitechHidppRdfuResponseCode == DfuTransferPktAck, pkt_number: u32be, } #[repr(u8)] enum FuLogitechHidppRdfuApplyFlags { DeferDfuBit = 1, ForceDfuBit = 1 << 1, } #[derive(New, Default)] struct FuStructLogitechHidppRdfuApplyDfu { report_id: FuLogitechHidppRdfuReportId == Long, device_id: u8, sub_id: u8, function_id: FuLogitechHidppRdfuFunc == ApplyDfu, fw_entity: u8, flags: u8, } fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-runtime-bolt.c000066400000000000000000000401621501337203100253710ustar00rootroot00000000000000/* * Copyright 2021 Ricardo Cañuelo * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-logitech-hidpp-common.h" #include "fu-logitech-hidpp-device.h" #include "fu-logitech-hidpp-hidpp.h" #include "fu-logitech-hidpp-radio.h" #include "fu-logitech-hidpp-runtime-bolt.h" #include "fu-logitech-hidpp-struct.h" struct _FuLogitechHidppRuntimeBolt { FuLogitechHidppRuntime parent_instance; guint8 pairing_slots; }; G_DEFINE_TYPE(FuLogitechHidppRuntimeBolt, fu_logitech_hidpp_runtime_bolt, FU_TYPE_HIDPP_RUNTIME) static gboolean fu_logitech_hidpp_runtime_bolt_detach(FuDevice *device, FuProgress *progress, GError **error) { FuLogitechHidppRuntime *self = FU_HIDPP_RUNTIME(device); g_autoptr(FuLogitechHidppHidppMsg) msg = fu_logitech_hidpp_msg_new(); g_autoptr(GError) error_local = NULL; msg->report_id = FU_LOGITECH_HIDPP_REPORT_ID_LONG; msg->device_id = FU_LOGITECH_HIDPP_DEVICE_IDX_RECEIVER; msg->sub_id = FU_LOGITECH_HIDPP_SUBID_SET_LONG_REGISTER; msg->function_id = BOLT_REGISTER_DFU_CONTROL; msg->data[0] = 1; /* Enable DFU */ msg->data[4] = 'P'; msg->data[5] = 'R'; msg->data[6] = 'E'; msg->hidpp_version = 1; msg->flags = FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_LONGER_TIMEOUT; if (!fu_logitech_hidpp_send(FU_UDEV_DEVICE(self), msg, FU_LOGITECH_HIDPP_DEVICE_TIMEOUT_MS, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_WRITE) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_debug("failed to detach to bootloader: %s", error_local->message); } else { g_prefix_error(&error_local, "failed to detach to bootloader: "); g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static void fu_logitech_hidpp_runtime_bolt_to_string(FuDevice *device, guint idt, GString *str) { FuLogitechHidppRuntimeBolt *self = FU_HIDPP_RUNTIME_BOLT(device); fwupd_codec_string_append_int(str, idt, "PairingSlots", self->pairing_slots); } static FuLogitechHidppDevice * fu_logitech_hidpp_runtime_bolt_find_paired_device(FuDevice *device, guint16 hidpp_pid) { GPtrArray *children = fu_device_get_children(device); for (guint i = 0; i < children->len; i++) { FuDevice *child = g_ptr_array_index(children, i); if (FU_IS_HIDPP_DEVICE(child) && fu_logitech_hidpp_device_get_hidpp_pid(FU_HIDPP_DEVICE(child)) == hidpp_pid) return FU_HIDPP_DEVICE(g_object_ref(child)); } return NULL; } static gchar * fu_logitech_hidpp_runtime_bolt_query_device_name(FuLogitechHidppRuntime *self, guint8 slot, GError **error) { g_autoptr(FuLogitechHidppHidppMsg) msg = fu_logitech_hidpp_msg_new(); g_autoptr(GString) dev_name = g_string_new(NULL); guint namelen; msg->report_id = FU_LOGITECH_HIDPP_REPORT_ID_SHORT; msg->device_id = FU_LOGITECH_HIDPP_DEVICE_IDX_RECEIVER; msg->sub_id = FU_LOGITECH_HIDPP_SUBID_GET_LONG_REGISTER; msg->function_id = BOLT_REGISTER_PAIRING_INFORMATION; msg->data[0] = 0x60 | slot; /* device name */ msg->data[1] = 1; msg->hidpp_version = 1; if (!fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self), msg, error)) { g_prefix_error(error, "failed to retrieve the device name for slot %d: ", slot); return NULL; } namelen = msg->data[2]; g_string_append_len(dev_name, (const char *)(&(msg->data[3])), namelen); return g_string_free(g_steal_pointer(&dev_name), FALSE); } static gboolean fu_logitech_hidpp_runtime_bolt_update_paired_device(FuLogitechHidppRuntimeBolt *self, FuLogitechHidppHidppMsg *msg, GError **error) { FuLogitechHidppRuntime *runtime = FU_HIDPP_RUNTIME(self); gboolean reachable = FALSE; guint16 hidpp_pid; g_autoptr(FuLogitechHidppDevice) child = NULL; if ((msg->data[0] & 0x40) == 0) reachable = TRUE; hidpp_pid = (msg->data[1] << 8) | msg->data[2]; child = fu_logitech_hidpp_runtime_bolt_find_paired_device(FU_DEVICE(self), hidpp_pid); if (child != NULL) { g_debug("%s [%s] is reachable:%i", fu_device_get_name(FU_DEVICE(child)), fu_device_get_name(FU_DEVICE(child)), reachable); if (reachable) { g_autoptr(FuDeviceLocker) locker = NULL; /* known paired & reachable */ fu_device_probe_invalidate(FU_DEVICE(child)); locker = fu_device_locker_new(FU_DEVICE(child), error); if (locker == NULL) { g_prefix_error(error, "cannot rescan paired device: "); return FALSE; } fu_device_remove_flag(FU_DEVICE(child), FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); } else { GPtrArray *children = NULL; /* any successful 'ping' will clear this */ fu_device_add_flag(FU_DEVICE(child), FWUPD_DEVICE_FLAG_UNREACHABLE); children = fu_device_get_children(FU_DEVICE(child)); for (guint i = 0; i < children->len; i++) { FuDevice *radio = g_ptr_array_index(children, i); fu_device_add_flag(radio, FWUPD_DEVICE_FLAG_UNREACHABLE); } } } else if (reachable) { g_autofree gchar *name = NULL; /* unknown paired device, reachable state */ name = fu_logitech_hidpp_runtime_bolt_query_device_name(runtime, msg->device_id, error); if (name == NULL) return FALSE; child = fu_logitech_hidpp_device_new(FU_UDEV_DEVICE(self)); fu_device_set_name(FU_DEVICE(child), name); fu_logitech_hidpp_device_set_device_idx(child, msg->device_id); fu_logitech_hidpp_device_set_hidpp_pid(child, hidpp_pid); if (!fu_device_open(FU_DEVICE(child), error)) return FALSE; if (!fu_device_probe(FU_DEVICE(child), error)) return FALSE; if (!fu_device_setup(FU_DEVICE(child), error)) return FALSE; fu_device_add_child(FU_DEVICE(self), FU_DEVICE(child)); } else { /* unknown paired device, unreachable state */ g_warning("unknown paired device 0x%0x in slot %d (unreachable)", hidpp_pid, msg->device_id); } return TRUE; } static void fu_logitech_hidpp_runtime_bolt_poll_peripherals(FuDevice *device) { FuLogitechHidppRuntime *self = FU_HIDPP_RUNTIME(device); FuLogitechHidppRuntimeBolt *bolt = FU_HIDPP_RUNTIME_BOLT(device); for (guint i = 1; i <= bolt->pairing_slots; i++) { g_autofree gchar *name = NULL; g_autoptr(FuLogitechHidppHidppMsg) msg = fu_logitech_hidpp_msg_new(); g_autoptr(GError) error_local = NULL; guint16 hidpp_pid; name = fu_logitech_hidpp_runtime_bolt_query_device_name(self, i, &error_local); if (name == NULL) { g_debug("cannot query paired device name for slot %u", i); continue; } msg->report_id = FU_LOGITECH_HIDPP_REPORT_ID_SHORT; msg->device_id = FU_LOGITECH_HIDPP_DEVICE_IDX_RECEIVER; msg->sub_id = FU_LOGITECH_HIDPP_SUBID_GET_LONG_REGISTER; msg->function_id = BOLT_REGISTER_PAIRING_INFORMATION; msg->data[0] = 0x50 | i; /* pairing information */ msg->hidpp_version = 1; if (!fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self), msg, &error_local)) continue; hidpp_pid = (msg->data[2] << 8) | msg->data[3]; if ((msg->data[1] & 0x40) == 0) { /* paired device is reachable */ g_autoptr(FuLogitechHidppDevice) child = NULL; child = fu_logitech_hidpp_device_new(FU_UDEV_DEVICE(device)); fu_device_set_install_duration(FU_DEVICE(child), 270); fu_device_add_private_flag(FU_DEVICE(child), FU_LOGITECH_HIDPP_DEVICE_FLAG_ADD_RADIO); fu_device_set_name(FU_DEVICE(child), name); fu_logitech_hidpp_device_set_device_idx(child, i); fu_logitech_hidpp_device_set_hidpp_pid(child, hidpp_pid); if (!fu_device_open(FU_DEVICE(child), &error_local)) continue; if (!fu_device_probe(FU_DEVICE(child), &error_local)) continue; if (!fu_device_setup(FU_DEVICE(child), &error_local)) continue; fu_device_add_child(device, FU_DEVICE(child)); } } } static gboolean fu_logitech_hidpp_runtime_bolt_process_notification(FuLogitechHidppRuntimeBolt *self, FuLogitechHidppHidppMsg *msg) { g_autoptr(GError) error_local = NULL; /* HID++1.0 error */ if (!fu_logitech_hidpp_msg_is_error(msg, &error_local)) { g_warning("failed to get pending read: %s", error_local->message); return TRUE; } /* unifying receiver notification */ if (msg->report_id == FU_LOGITECH_HIDPP_REPORT_ID_SHORT) { switch (msg->sub_id) { case FU_LOGITECH_HIDPP_SUBID_DEVICE_CONNECTION: case FU_LOGITECH_HIDPP_SUBID_DEVICE_DISCONNECTION: case FU_LOGITECH_HIDPP_SUBID_DEVICE_LOCKING_CHANGED: if (!fu_logitech_hidpp_runtime_bolt_update_paired_device(self, msg, &error_local)) { g_warning("failed to update paired device status: %s", error_local->message); return FALSE; } break; case FU_LOGITECH_HIDPP_SUBID_LINK_QUALITY: g_debug("ignoring link quality message"); break; case FU_LOGITECH_HIDPP_SUBID_ERROR_MSG: g_debug("ignoring error message"); break; default: g_debug("unknown SubID %02x", msg->sub_id); break; } } return TRUE; } static FuLogitechHidppHidppMsg * fu_logitech_hidpp_runtime_bolt_find_newest_msg(GPtrArray *msgs, guint8 device_id, guint8 sub_id) { for (guint i = 0; i < msgs->len; i++) { FuLogitechHidppHidppMsg *msg = g_ptr_array_index(msgs, msgs->len - (i + 1)); if (msg->device_id == device_id && msg->sub_id == sub_id) return msg; } return NULL; } static gboolean fu_logitech_hidpp_runtime_bolt_poll(FuDevice *device, GError **error) { FuLogitechHidppRuntime *runtime = FU_HIDPP_RUNTIME(device); FuLogitechHidppRuntimeBolt *self = FU_HIDPP_RUNTIME_BOLT(device); const guint timeout = 1; /* ms */ g_autoptr(GPtrArray) msgs = g_ptr_array_new_with_free_func(g_free); /* open -- not a locker as we have no kernel driver */ if (!fu_device_open(device, error)) return FALSE; /* drain all the pending messages into the array */ for (guint i = 0; i < 50; i++) { g_autoptr(FuLogitechHidppHidppMsg) msg = fu_logitech_hidpp_msg_new(); g_autoptr(GError) error_local = NULL; msg->hidpp_version = 1; if (!fu_logitech_hidpp_receive(FU_UDEV_DEVICE(runtime), msg, timeout, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT)) break; if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_debug("ignoring: %s", error_local->message); break; } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "error polling Bolt receiver: "); return FALSE; } g_ptr_array_add(msgs, g_steal_pointer(&msg)); } /* process messages in order, but discard any message with a newer version */ for (guint i = 0; i < msgs->len; i++) { FuLogitechHidppHidppMsg *msg = g_ptr_array_index(msgs, i); FuLogitechHidppHidppMsg *msg_newest; /* find the newest message with the matching device and sub-IDs */ msg_newest = fu_logitech_hidpp_runtime_bolt_find_newest_msg(msgs, msg->device_id, msg->sub_id); if (msg != msg_newest) { g_debug("ignoring duplicate message device-id:%02x [%s] sub-id:%02x [%s]", msg->device_id, fu_logitech_hidpp_device_idx_to_string(msg->device_id), msg->sub_id, fu_logitech_hidpp_subid_to_string(msg->sub_id)); continue; } fu_logitech_hidpp_runtime_bolt_process_notification(self, msg); } return TRUE; } static gboolean fu_logitech_hidpp_runtime_bolt_setup_internal(FuDevice *device, GError **error) { FuContext *ctx = fu_device_get_context(device); FuLogitechHidppRuntime *self = FU_HIDPP_RUNTIME(device); FuLogitechHidppRuntimeBolt *bolt = FU_HIDPP_RUNTIME_BOLT(device); g_autoptr(FuLogitechHidppHidppMsg) msg = fu_logitech_hidpp_msg_new(); msg->report_id = FU_LOGITECH_HIDPP_REPORT_ID_SHORT; msg->device_id = FU_LOGITECH_HIDPP_DEVICE_IDX_RECEIVER; msg->sub_id = FU_LOGITECH_HIDPP_SUBID_GET_LONG_REGISTER; msg->function_id = BOLT_REGISTER_PAIRING_INFORMATION; msg->data[0] = 0x02; /* FW Version (contains the number of pairing slots) */ msg->hidpp_version = 1; if (!fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self), msg, error)) { g_prefix_error(error, "failed to fetch the number of pairing slots: "); return FALSE; } bolt->pairing_slots = msg->data[8]; /* * TODO: Iterate only over the first three entity indexes for * now. */ for (guint i = 0; i < 3; i++) { guint16 version_raw = 0; g_autofree gchar *version = NULL; g_autoptr(FuLogitechHidppRadio) radio = NULL; g_autoptr(GString) radio_version = NULL; msg->report_id = FU_LOGITECH_HIDPP_REPORT_ID_SHORT; msg->device_id = FU_LOGITECH_HIDPP_DEVICE_IDX_RECEIVER; msg->sub_id = FU_LOGITECH_HIDPP_SUBID_GET_LONG_REGISTER; msg->function_id = BOLT_REGISTER_RECEIVER_FW_INFORMATION; msg->data[0] = i; msg->hidpp_version = 1; if (!fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self), msg, error)) { g_prefix_error(error, "failed to read device config: "); return FALSE; } switch (msg->data[0]) { case 0: /* main application */ if (!fu_memread_uint16_safe(msg->data, sizeof(msg->data), 0x03, &version_raw, G_BIG_ENDIAN, error)) return FALSE; version = fu_logitech_hidpp_format_version("MPR", msg->data[1], msg->data[2], version_raw); fu_device_set_version(device, version); break; case 1: /* bootloader */ if (!fu_memread_uint16_safe(msg->data, sizeof(msg->data), 0x03, &version_raw, G_BIG_ENDIAN, error)) return FALSE; version = fu_logitech_hidpp_format_version("BOT", msg->data[1], msg->data[2], version_raw); fu_device_set_version_bootloader(device, version); break; case 5: /* SoftDevice */ radio_version = g_string_new(NULL); radio = fu_logitech_hidpp_radio_new(ctx, i); fu_device_add_instance_u16(FU_DEVICE(radio), "VEN", fu_device_get_vid(device)); fu_device_add_instance_u16(FU_DEVICE(radio), "DEV", fu_device_get_pid(device)); fu_device_add_instance_u8(FU_DEVICE(radio), "ENT", msg->data[0]); fu_device_incorporate(FU_DEVICE(radio), FU_DEVICE(device), FU_DEVICE_INCORPORATE_FLAG_PHYSICAL_ID); fu_device_set_logical_id(FU_DEVICE(radio), "Receiver_SoftDevice"); if (!fu_device_build_instance_id(FU_DEVICE(radio), error, "HIDRAW", "VEN", "DEV", "ENT", NULL)) return FALSE; if (!fu_memread_uint16_safe(msg->data, sizeof(msg->data), 0x03, &version_raw, G_BIG_ENDIAN, error)) return FALSE; g_string_append_printf(radio_version, "0x%.4x", version_raw); fu_device_set_version(FU_DEVICE(radio), radio_version->str); fu_device_add_child(device, FU_DEVICE(radio)); break; default: break; } } /* enable HID++ notifications */ if (!fu_logitech_hidpp_runtime_enable_notifications(self, error)) { g_prefix_error(error, "failed to enable notifications: "); return FALSE; } fu_logitech_hidpp_runtime_bolt_poll_peripherals(device); /* success */ return TRUE; } static gboolean fu_logitech_hidpp_runtime_bolt_setup(FuDevice *device, GError **error) { g_autoptr(GError) error_local = NULL; for (guint i = 0; i < 5; i++) { g_clear_error(&error_local); /* HID++1.0 devices have to sleep to allow Solaar to talk to * the device first -- we can't use the SwID as this is a * HID++2.0 feature */ fu_device_sleep(device, 200); /* ms */ if (fu_logitech_hidpp_runtime_bolt_setup_internal(device, &error_local)) return TRUE; if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA)) { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } static void fu_logitech_hidpp_runtime_bolt_class_init(FuLogitechHidppRuntimeBoltClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->detach = fu_logitech_hidpp_runtime_bolt_detach; device_class->setup = fu_logitech_hidpp_runtime_bolt_setup; device_class->poll = fu_logitech_hidpp_runtime_bolt_poll; device_class->to_string = fu_logitech_hidpp_runtime_bolt_to_string; } static void fu_logitech_hidpp_runtime_bolt_init(FuLogitechHidppRuntimeBolt *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_USER_REPLUG); fu_device_set_vendor(FU_DEVICE(self), "Logitech"); fu_device_set_name(FU_DEVICE(self), "Bolt Receiver"); fu_device_add_protocol(FU_DEVICE(self), "com.logitech.unifyingsigned"); } fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-runtime-bolt.h000066400000000000000000000006361501337203100254000ustar00rootroot00000000000000/* * Copyright 2021 Ricardo Cañuelo * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-logitech-hidpp-runtime.h" #define FU_TYPE_HIDPP_RUNTIME_BOLT (fu_logitech_hidpp_runtime_bolt_get_type()) G_DECLARE_FINAL_TYPE(FuLogitechHidppRuntimeBolt, fu_logitech_hidpp_runtime_bolt, FU, HIDPP_RUNTIME_BOLT, FuLogitechHidppRuntime) fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-runtime-unifying.c000066400000000000000000000140041501337203100262550ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-logitech-hidpp-common.h" #include "fu-logitech-hidpp-hidpp.h" #include "fu-logitech-hidpp-runtime-unifying.h" #include "fu-logitech-hidpp-struct.h" struct _FuLogitechHidppRuntimeUnifying { FuLogitechHidppRuntime parent_instance; }; G_DEFINE_TYPE(FuLogitechHidppRuntimeUnifying, fu_logitech_hidpp_runtime_unifying, FU_TYPE_HIDPP_RUNTIME) #define GET_PRIVATE(o) (fu_logitech_hidpp_runtime_unifying_get_instance_private(o)) static gboolean fu_logitech_hidpp_runtime_unifying_detach(FuDevice *device, FuProgress *progress, GError **error) { FuLogitechHidppRuntime *self = FU_HIDPP_RUNTIME(device); g_autoptr(FuLogitechHidppHidppMsg) msg = fu_logitech_hidpp_msg_new(); g_autoptr(GError) error_local = NULL; msg->report_id = FU_LOGITECH_HIDPP_REPORT_ID_SHORT; msg->device_id = FU_LOGITECH_HIDPP_DEVICE_IDX_RECEIVER; msg->sub_id = FU_LOGITECH_HIDPP_SUBID_SET_REGISTER; msg->function_id = FU_LOGITECH_HIDPP_REGISTER_DEVICE_FIRMWARE_UPDATE_MODE; msg->data[0] = 'I'; msg->data[1] = 'C'; msg->data[2] = 'P'; msg->hidpp_version = 1; msg->flags = FU_LOGITECH_HIDPP_HIDPP_MSG_FLAG_LONGER_TIMEOUT; if (!fu_logitech_hidpp_send(FU_UDEV_DEVICE(self), msg, FU_LOGITECH_HIDPP_DEVICE_TIMEOUT_MS, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_WRITE) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_debug("failed to detach to bootloader: %s", error_local->message); } else { g_prefix_error(&error_local, "failed to detach to bootloader: "); g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_logitech_hidpp_runtime_unifying_setup_internal(FuDevice *device, GError **error) { FuLogitechHidppRuntime *self = FU_HIDPP_RUNTIME(device); guint8 config[10]; g_autofree gchar *version_fw = NULL; /* read all 10 bytes of the version register */ memset(config, 0x00, sizeof(config)); for (guint i = 0x01; i < 0x05; i++) { g_autoptr(FuLogitechHidppHidppMsg) msg = fu_logitech_hidpp_msg_new(); /* workaround a bug in the 12.01 firmware, which fails with * INVALID_VALUE when reading MCU1_HW_VERSION */ if (i == 0x03) continue; msg->report_id = FU_LOGITECH_HIDPP_REPORT_ID_SHORT; msg->device_id = FU_LOGITECH_HIDPP_DEVICE_IDX_RECEIVER; msg->sub_id = FU_LOGITECH_HIDPP_SUBID_GET_REGISTER; msg->function_id = FU_LOGITECH_HIDPP_REGISTER_DEVICE_FIRMWARE_INFORMATION; msg->data[0] = i; msg->hidpp_version = 1; if (!fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self), msg, error)) { g_prefix_error(error, "failed to read device config: "); return FALSE; } if (!fu_memcpy_safe(config, sizeof(config), i * 2, /* dst */ msg->data, sizeof(msg->data), 0x1, /* src */ 2, error)) return FALSE; } /* get firmware version */ version_fw = fu_logitech_hidpp_format_version("RQR", config[2], config[3], (guint16)config[4] << 8 | config[5]); fu_device_set_version(device, version_fw); /* get bootloader version */ if (fu_logitech_hidpp_runtime_get_version_bl_major(self) > 0) { g_autofree gchar *version_bl = NULL; version_bl = fu_logitech_hidpp_format_version( "BOT", fu_logitech_hidpp_runtime_get_version_bl_major(self), config[8], config[9]); fu_device_set_version_bootloader(FU_DEVICE(device), version_bl); /* is the USB receiver expecting signed firmware */ if ((fu_logitech_hidpp_runtime_get_version_bl_major(self) == 0x01 && config[8] >= 0x04) || (fu_logitech_hidpp_runtime_get_version_bl_major(self) == 0x03 && config[8] >= 0x02)) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_protocol(device, "com.logitech.unifyingsigned"); } } if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD)) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_protocol(device, "com.logitech.unifying"); } /* enable HID++ notifications */ if (!fu_logitech_hidpp_runtime_enable_notifications(self, error)) { g_prefix_error(error, "failed to enable notifications: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_logitech_hidpp_runtime_unifying_setup(FuDevice *device, GError **error) { g_autoptr(GError) error_local = NULL; for (guint i = 0; i < 5; i++) { g_clear_error(&error_local); /* HID++1.0 devices have to sleep to allow Solaar to talk to * the device first -- we can't use the SwID as this is a * HID++2.0 feature */ fu_device_sleep(device, 200); /* ms */ if (fu_logitech_hidpp_runtime_unifying_setup_internal(device, &error_local)) return TRUE; if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA)) { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } static void fu_logitech_hidpp_runtime_unifying_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 70, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 4, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 27, "reload"); } static void fu_logitech_hidpp_runtime_unifying_class_init(FuLogitechHidppRuntimeUnifyingClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->detach = fu_logitech_hidpp_runtime_unifying_detach; device_class->setup = fu_logitech_hidpp_runtime_unifying_setup; device_class->set_progress = fu_logitech_hidpp_runtime_unifying_set_progress; } static void fu_logitech_hidpp_runtime_unifying_init(FuLogitechHidppRuntimeUnifying *self) { } fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-runtime-unifying.h000066400000000000000000000006461501337203100262710ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-logitech-hidpp-runtime.h" #define FU_TYPE_HIDPP_RUNTIME_UNIFYING (fu_logitech_hidpp_runtime_unifying_get_type()) G_DECLARE_FINAL_TYPE(FuLogitechHidppRuntimeUnifying, fu_logitech_hidpp_runtime_unifying, FU, HIDPP_RUNTIME_UNIFYING, FuLogitechHidppRuntime) fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-runtime.c000066400000000000000000000140011501337203100244240ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-logitech-hidpp-common.h" #include "fu-logitech-hidpp-hidpp.h" #include "fu-logitech-hidpp-runtime.h" #include "fu-logitech-hidpp-struct.h" typedef struct { guint8 version_bl_major; } FuLogitechHidppRuntimePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuLogitechHidppRuntime, fu_logitech_hidpp_runtime, FU_TYPE_HIDRAW_DEVICE) #define GET_PRIVATE(o) (fu_logitech_hidpp_runtime_get_instance_private(o)) guint8 fu_logitech_hidpp_runtime_get_version_bl_major(FuLogitechHidppRuntime *self) { FuLogitechHidppRuntimePrivate *priv; g_return_val_if_fail(FU_IS_HIDPP_RUNTIME(self), 0); priv = GET_PRIVATE(self); return priv->version_bl_major; } gboolean fu_logitech_hidpp_runtime_enable_notifications(FuLogitechHidppRuntime *self, GError **error) { g_autoptr(FuLogitechHidppHidppMsg) msg = fu_logitech_hidpp_msg_new(); msg->report_id = FU_LOGITECH_HIDPP_REPORT_ID_SHORT; msg->device_id = FU_LOGITECH_HIDPP_DEVICE_IDX_RECEIVER; msg->sub_id = FU_LOGITECH_HIDPP_SUBID_SET_REGISTER; msg->function_id = FU_LOGITECH_HIDPP_REGISTER_HIDPP_NOTIFICATIONS; msg->data[0] = 0x00; msg->data[1] = 0x05; /* Wireless + SoftwarePresent */ msg->data[2] = 0x00; msg->hidpp_version = 1; return fu_logitech_hidpp_transfer(FU_UDEV_DEVICE(self), msg, error); } static gboolean fu_logitech_hidpp_runtime_probe(FuDevice *device, GError **error) { FuLogitechHidppRuntime *self = FU_HIDPP_RUNTIME(device); FuLogitechHidppRuntimePrivate *priv = GET_PRIVATE(self); g_autoptr(FuDevice) device_usb = NULL; g_autoptr(FuDevice) device_usb_iface = NULL; /* FuHidrawDevice->probe */ if (!FU_DEVICE_CLASS(fu_logitech_hidpp_runtime_parent_class)->probe(device, error)) return FALSE; /* generate bootloader-specific GUID */ device_usb = fu_device_get_backend_parent_with_subsystem(device, "usb:usb_device", NULL); if (device_usb != NULL) { g_autofree gchar *devid2 = NULL; g_autofree gchar *prop_interface = NULL; if (!fu_device_probe(device_usb, error)) return FALSE; switch (fu_usb_device_get_release(FU_USB_DEVICE(device_usb)) & 0xFF00) { case 0x1200: /* Nordic */ devid2 = g_strdup_printf("USB\\VID_%04X&PID_%04X", fu_device_get_vid(device), (guint)FU_LOGITECH_HIDPP_DEVICE_PID_BOOTLOADER_NORDIC); fu_device_add_instance_id_full(device, devid2, FU_DEVICE_INSTANCE_FLAG_QUIRKS | FU_DEVICE_INSTANCE_FLAG_VISIBLE | FU_DEVICE_INSTANCE_FLAG_COUNTERPART); priv->version_bl_major = 0x01; break; case 0x2400: /* Texas */ devid2 = g_strdup_printf("USB\\VID_%04X&PID_%04X", fu_device_get_vid(device), (guint)FU_LOGITECH_HIDPP_DEVICE_PID_BOOTLOADER_TEXAS); fu_device_add_instance_id_full(device, devid2, FU_DEVICE_INSTANCE_FLAG_QUIRKS | FU_DEVICE_INSTANCE_FLAG_VISIBLE | FU_DEVICE_INSTANCE_FLAG_COUNTERPART); priv->version_bl_major = 0x03; break; case 0x0500: /* Bolt */ device_usb_iface = fu_device_get_backend_parent_with_subsystem(device, "usb:usb_interface", error); if (device_usb_iface == NULL) return FALSE; prop_interface = fu_udev_device_read_property(FU_UDEV_DEVICE(device_usb_iface), "INTERFACE", error); if (prop_interface == NULL) return FALSE; if (g_strcmp0(prop_interface, "3/0/0") != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "skipping hidraw device"); return FALSE; } devid2 = g_strdup_printf("USB\\VID_%04X&PID_%04X", fu_device_get_vid(device), (guint)FU_LOGITECH_HIDPP_DEVICE_PID_BOOTLOADER_BOLT); fu_device_add_instance_id_full(device, devid2, FU_DEVICE_INSTANCE_FLAG_COUNTERPART); priv->version_bl_major = 0x03; break; default: g_warning("bootloader release 0x%04x invalid", (guint)fu_usb_device_get_release(FU_USB_DEVICE(device_usb))); break; } } return TRUE; } static void fu_logitech_hidpp_runtime_vid_pid_notify_cb(FuDevice *device, GParamSpec *pspec, gpointer user_data) { if (fu_device_get_pid(device) == 0x0) return; /* this is a non-standard extension, but essentially API */ fu_device_add_instance_u16(device, "VID", fu_device_get_vid(device)); fu_device_add_instance_u16(device, "PID", fu_device_get_pid(device)); fu_device_build_instance_id(device, NULL, "UFY", "VID", "PID", NULL); /* for vendor */ fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "USB", "VID", NULL); } static void fu_logitech_hidpp_runtime_class_init(FuLogitechHidppRuntimeClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_logitech_hidpp_runtime_probe; } static void fu_logitech_hidpp_runtime_init(FuLogitechHidppRuntime *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_EMULATION_TAG); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_vid(FU_DEVICE(self), FU_LOGITECH_HIDPP_DEVICE_VID); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_USB_RECEIVER); fu_device_set_name(FU_DEVICE(self), "Unifying Receiver"); fu_device_set_summary(FU_DEVICE(self), "Miniaturised USB wireless receiver"); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_set_poll_interval(FU_DEVICE(self), FU_HIDPP_RECEIVER_RUNTIME_POLLING_INTERVAL); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); g_signal_connect(FU_DEVICE(self), "notify::vid", G_CALLBACK(fu_logitech_hidpp_runtime_vid_pid_notify_cb), NULL); g_signal_connect(FU_DEVICE(self), "notify::pid", G_CALLBACK(fu_logitech_hidpp_runtime_vid_pid_notify_cb), NULL); } fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-runtime.h000066400000000000000000000011461501337203100244370ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_HIDPP_RUNTIME (fu_logitech_hidpp_runtime_get_type()) G_DECLARE_DERIVABLE_TYPE(FuLogitechHidppRuntime, fu_logitech_hidpp_runtime, FU, HIDPP_RUNTIME, FuHidrawDevice) struct _FuLogitechHidppRuntimeClass { FuHidrawDeviceClass parent_class; }; gboolean fu_logitech_hidpp_runtime_enable_notifications(FuLogitechHidppRuntime *self, GError **error); guint8 fu_logitech_hidpp_runtime_get_version_bl_major(FuLogitechHidppRuntime *self); fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp-self-test.c000066400000000000000000000016061501337203100246560ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-logitech-hidpp-common.h" static void fu_logitech_hidpp_common(void) { guint8 u8; guint16 u16; g_autofree gchar *ver1 = NULL; u8 = fu_logitech_hidpp_buffer_read_uint8("12"); g_assert_cmpint(u8, ==, 0x12); u16 = fu_logitech_hidpp_buffer_read_uint16("1234"); g_assert_cmpint(u16, ==, 0x1234); ver1 = fu_logitech_hidpp_format_version(" A ", 0x87, 0x65, 0x4321); g_assert_cmpstr(ver1, ==, "A87.65_B4321"); } int main(int argc, char **argv) { (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); /* tests go here */ g_test_add_func("/unifying/common", fu_logitech_hidpp_common); return g_test_run(); } fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-hidpp.rs000066400000000000000000000111201501337203100231440ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] enum FuLogitechHidppFeature { Root = 0x0000, IFeatureSet = 0x0001, IFirmwareInfo = 0x0003, GetDeviceNameType = 0x0005, DfuControl = 0x00C1, DfuControlSigned = 0x00C2, DfuControlBolt = 0x00C3, Dfu = 0x00D0, Rdfu = 0x00D1, BatteryLevelStatus = 0x1000, UnifiedBattery = 0x1004, KbdReprogrammableKeys = 0x1B00, SpecialKeysButtons = 0x1B04, MousePointerBasic = 0x2200, AdjustableDpi = 0x2201, AdjustableReportRate = 0x8060, ColorLedEffects = 0x8070, OnboardProfiles = 0x8100, MouseButtonSpy = 0x8110, } #[derive(ToString)] enum FuLogitechHidppDeviceIdx { Wired = 0x00, Receiver = 0xFF, } #[derive(ToString)] enum FuLogitechHidppReportId { Short = 0x10, Long = 0x11, VeryLong = 0x12, } // HID++1.0 registers #[derive(ToString)] enum FuLogitechHidppRegister { HidppNotifications = 0x00, EnableIndividualFeatures = 0x01, BatteryStatus = 0x07, BatteryMileage = 0x0D, Profile = 0x0F, LedStatus = 0x51, LedIntensity = 0x54, LedColor = 0x57, OpticalSensorSettings = 0x61, CurrentResolution = 0x63, UsbRefreshRate = 0x64, GenericMemoryManagement = 0xA0, HotControl = 0xA1, ReadMemory = 0xA2, DeviceConnectionDisconnection = 0xB2, PairingInformation = 0xB5, DeviceFirmwareUpdateMode = 0xF0, DeviceFirmwareInformation = 0xF1, } #[derive(ToString)] enum FuLogitechHidppSubid { VendorSpecificKeys = 0x03, PowerKeys = 0x04, Roller = 0x05, MouseExtraButtons = 0x06, BatteryChargingLevel = 0x07, UserInterfaceEvent = 0x08, FLockStatus = 0x09, CalculatorResult = 0x0A, MenuNavigate = 0x0B, FnKey = 0x0C, BatteryMileage = 0x0D, UartRx = 0x0E, BacklightDurationUpdate = 0x17, DeviceDisconnection = 0x40, DeviceConnection = 0x41, DeviceDiscovery = 0x42, PinCodeRequest = 0x43, ReceiverWorkingMode = 0x44, ErrorMessage = 0x45, RfLinkChange = 0x46, Hci = 0x48, LinkQuality = 0x49, DeviceLockingChanged = 0x4a, WirelessDeviceChange = 0x4B, Acl = 0x51, VoipTelephonyEvent = 0x5B, Led = 0x60, GestureAndAir = 0x65, TouchpadMultiTouch = 0x66, Traceability = 0x78, SetRegister = 0x80, GetRegister = 0x81, SetLongRegister = 0x82, GetLongRegister = 0x83, SetVeryLongRegister = 0x84, GetVeryLongRegister = 0x85, ErrorMsg = 0x8F, ErrorMsg_20 = 0xFF, } enum FuLogitechHidppBootloaderCmd { GeneralError = 0x01, Read = 0x10, Write = 0x20, WriteInvalidAddr = 0x21, WriteVerifyFail = 0x22, WriteNonzeroStart = 0x23, WriteInvalidCrc = 0x24, ErasePage = 0x30, ErasePageInvalidAddr = 0x31, ErasePageNonzeroStart = 0x33, GetHwPlatformId = 0x40, GetFwVersion = 0x50, GetChecksum = 0x60, Reboot = 0x70, GetMeminfo = 0x80, GetBlVersion = 0x90, GetInitFwVersion = 0xa0, ReadSignature = 0xb0, WriteRamBuffer = 0xc0, WriteRamBufferInvalidAddr = 0xc1, WriteRamBufferOverflow = 0xc2, FlashRam = 0xd0, FlashRamInvalidAddr = 0xd1, FlashRamWrongCrc = 0xd2, FlashRamPage0Invalid = 0xd3, FlashRamInvalidOrder = 0xd4, WriteSignature = 0xe0, } // HID++1.0 error codes #[derive(ToString)] enum FuLogitechHidppErr { Success, InvalidSubid, InvalidAddress, InvalidValue, ConnectFail, TooManyDevices, AlreadyExists, Busy, UnknownDevice, ResourceError, RequestUnavailable, InvalidParamValue, WrongPinCode, } // HID++2.0 error codes #[derive(ToString)] enum FuLogitechHidppErr2 { NoError, Unknown, InvalidArgument, OutOfRange, HwError, LogitechInternal, InvalidFeatureIndex, InvalidFunctionId, Busy, Unsupported, } #[derive(ToString)] enum FuLogitechHidppStatus { Invalid, PacketSuccess, DfuSuccess, WaitForEvent, GenericError04, DfuSuccessEntityRestartRequired, DfuSuccessSystemRestartRequired, GenericError10 = 0x10, BadVoltage, Unknown12, UnsupportedEncryptionMode, BadMagicString, EraseFailure, DfuNotStarted, BadSequenceNumber, UnsupportedCommand, CommandInProgress, AddressOutOfRange, UnalignedAddress, BadSize, MissingProgramData, MissingCheckData, ProgramFailedToWrite, ProgramFailedToVerify, BadFirmware, FirmwareCheckFailure, BlockedCommand, } fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-rdfu-firmware.c000066400000000000000000000304071501337203100244210ustar00rootroot00000000000000/* * Copyright 2024 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-logitech-rdfu-firmware.h" #include "json-glib/json-glib.h" #define FU_LOGITECH_RDFU_FIRMWARE_VERSION 1 #define FU_LOGITECH_RDFU_MAGIC_ASCII_SIZE 22 /* 0x + 10 hex */ struct _FuLogitechRdfuFirmware { FuFirmware parent_instance; gchar *payload_name; gchar *model_id; GByteArray *magic; GPtrArray *blocks; /* GByteArray */ }; G_DEFINE_TYPE(FuLogitechRdfuFirmware, fu_logitech_rdfu_firmware, FU_TYPE_FIRMWARE) gchar * fu_logitech_rdfu_firmware_get_model_id(FuLogitechRdfuFirmware *self, GError **error) { return g_strdup(self->model_id); } GByteArray * fu_logitech_rdfu_firmware_get_magic(FuLogitechRdfuFirmware *self, GError **error) { return g_byte_array_ref(self->magic); } GPtrArray * fu_logitech_rdfu_firmware_get_blocks(FuLogitechRdfuFirmware *self, GError **error) { return g_ptr_array_ref(self->blocks); } static gboolean fu_logitech_rdfu_firmware_block_add(FuLogitechRdfuFirmware *self, JsonNode *node, GError **error) { JsonObject *json_obj; const gchar *block_str; g_autoptr(GByteArray) block = NULL; g_autoptr(GError) error_local = NULL; if (node == NULL || !JSON_NODE_HOLDS_OBJECT(node)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "RDFU firmware contains incorrect payloads node"); return FALSE; } json_obj = json_node_get_object(node); if (!json_object_has_member(json_obj, "data")) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "payload %s has no data key", self->payload_name); return FALSE; } block_str = json_object_get_string_member_with_default(json_obj, "data", NULL); if (block_str == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "payload %s has no data", self->payload_name); return FALSE; } if (strlen(block_str) % 2) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "payload %s has incorrect size %u", self->payload_name, (guint)strlen(block_str)); return FALSE; } block = fu_byte_array_from_string(block_str, &error_local); if (block == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "unable to serialize payload %s", self->payload_name); return FALSE; } g_ptr_array_add(self->blocks, g_steal_pointer(&block)); return TRUE; } static gboolean fu_logitech_rdfu_firmware_entry_add(FuFirmware *firmware, JsonNode *node, GError **error) { FuLogitechRdfuFirmware *self = FU_LOGITECH_RDFU_FIRMWARE(firmware); JsonObject *json_obj; guint str_offset = 0; guint64 entity; guint64 revision; guint64 build; const gchar *entity_str; const gchar *magic_str; const gchar *payload_str; const gchar *model_id_str; const gchar *name_str; const gchar *revision_str; const gchar *build_str; g_autofree gchar *version = NULL; g_autofree gchar *tmp = NULL; g_autoptr(GByteArray) model_id = NULL; if (node == NULL || !JSON_NODE_HOLDS_OBJECT(node)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "incorrect contents node"); return FALSE; } json_obj = json_node_get_object(node); if (!json_object_has_member(json_obj, "entity")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "has no entity"); return FALSE; } entity_str = json_object_get_string_member_with_default(json_obj, "entity", NULL); if (!fu_strtoull(entity_str, &entity, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_firmware_set_id(FU_FIRMWARE(self), entity_str); magic_str = json_object_get_string_member_with_default(json_obj, "magicStr", NULL); if (magic_str == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "has no magic"); return FALSE; } if (strlen(magic_str) != FU_LOGITECH_RDFU_MAGIC_ASCII_SIZE) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "has incorrect magic"); return FALSE; } payload_str = json_object_get_string_member_with_default(json_obj, "payload", NULL); if (payload_str == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "has no payload"); return FALSE; } g_debug("RDFU firmware for entity %u payload = %s", (guint)entity, payload_str); if (!json_object_has_member(json_obj, "modelId")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "has no modelId"); return FALSE; } model_id_str = json_object_get_string_member_with_default(json_obj, "modelId", NULL); /* just to validate if modelId is in a hexadecimal string format */ if (g_str_has_prefix(model_id_str, "0x")) str_offset = 2; model_id = fu_byte_array_from_string(model_id_str + str_offset, error); if (model_id == NULL) return FALSE; if (!json_object_has_member(json_obj, "name")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "has no name"); return FALSE; } name_str = json_object_get_string_member_with_default(json_obj, "name", NULL); if (!json_object_has_member(json_obj, "revision")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "has no revision"); return FALSE; } revision_str = json_object_get_string_member_with_default(json_obj, "revision", NULL); if (!fu_strtoull(revision_str, &revision, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; if (!json_object_has_member(json_obj, "build")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "has no build"); return FALSE; } build_str = json_object_get_string_member_with_default(json_obj, "build", NULL); /* should be in BCD format already but let's be tolerant to absent leading 0 */ if (!fu_strtoull(build_str, &build, 0, G_MAXUINT16, FU_INTEGER_BASE_16, error)) { return FALSE; } /* skip "0x" prefix */ self->magic = fu_byte_array_from_string(magic_str + 2, error); if (self->magic == NULL) return FALSE; /* model id should be in same format as returned for device by getFwInfo */ tmp = fu_byte_array_to_string(model_id); self->model_id = g_ascii_strup(tmp, -1); self->payload_name = g_strdup(payload_str); self->blocks = g_ptr_array_new_with_free_func((GDestroyNotify)g_byte_array_unref); version = g_strdup_printf("%s.%02x_B%04x", name_str, (guint)revision, (guint)build); fu_firmware_set_version(firmware, version); return TRUE; } static gboolean fu_logitech_rdfu_firmware_compare_payload(gconstpointer item, gconstpointer payload) { FuLogitechRdfuFirmware *entity_fw = (FuLogitechRdfuFirmware *)item; if (g_strcmp0(entity_fw->payload_name, payload) != 0) return FALSE; return TRUE; } static gboolean fu_logitech_rdfu_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { JsonNode *json_root_node; JsonObject *json_obj; JsonObject *json_payloads_obj; JsonArray *contents; const gchar *firmware_str; guint64 firmware_ver; gsize streamsz; g_autoptr(JsonParser) parser = json_parser_new(); g_autoptr(GList) payloads_list = NULL; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (!g_seekable_seek(G_SEEKABLE(stream), 0, G_SEEK_SET, NULL, error)) { g_prefix_error(error, "seek to start: "); return FALSE; } if (!json_parser_load_from_stream(parser, stream, NULL, error)) { g_prefix_error(error, "failed to parse RDFU firmware: "); return FALSE; } json_root_node = json_parser_get_root(parser); if (json_root_node == NULL || !JSON_NODE_HOLDS_OBJECT(json_root_node)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "RDFU firmware has no root"); return FALSE; } json_obj = json_node_get_object(json_root_node); if (!json_object_has_member(json_obj, "fileVersion")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "RDFU firmware has invalid format"); return FALSE; } firmware_str = json_object_get_string_member_with_default(json_obj, "fileVersion", "0"); if (!fu_strtoull(firmware_str, &firmware_ver, 1, FU_LOGITECH_RDFU_FIRMWARE_VERSION, FU_INTEGER_BASE_AUTO, error)) return FALSE; if (!json_object_has_member(json_obj, "contents")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "unable to find contents"); return FALSE; } contents = json_object_get_array_member(json_obj, "contents"); if (json_array_get_length(contents) < 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "empty contents array"); return FALSE; } /* adding blocks to the entity FW */ for (guint i = 0; i < json_array_get_length(contents); i++) { JsonNode *node = json_array_get_element(contents, i); g_autoptr(FuFirmware) entity_fw = FU_FIRMWARE(g_object_new(FU_TYPE_LOGITECH_RDFU_FIRMWARE, NULL)); if (!fu_logitech_rdfu_firmware_entry_add(entity_fw, node, error)) { g_prefix_error(error, "RDFU firmware contents[%u]: ", i); return FALSE; } fu_firmware_add_image(firmware, entity_fw); } if (!json_object_has_member(json_obj, "payloads")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "unable to find payloads"); return FALSE; } json_payloads_obj = json_object_get_object_member(json_obj, "payloads"); payloads_list = json_object_get_members(json_payloads_obj); for (GList *l = payloads_list; l != NULL; l = g_list_next(l)) { guint index = 0; JsonArray *blocks; FuLogitechRdfuFirmware *entity_fw = NULL; g_autoptr(GPtrArray) images = fu_firmware_get_images(firmware); if (!g_ptr_array_find_with_equal_func(images, l->data, fu_logitech_rdfu_firmware_compare_payload, &index)) continue; entity_fw = (FuLogitechRdfuFirmware *)images->pdata[index]; g_debug("found payload %s for entity %s", (gchar *)l->data, fu_firmware_get_id(FU_FIRMWARE(entity_fw))); json_obj = json_object_get_object_member(json_payloads_obj, l->data); if (!json_object_has_member(json_obj, "blocks")) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "payload %s has no blocks", (gchar *)l->data); return FALSE; } blocks = json_object_get_array_member(json_obj, "blocks"); if (json_array_get_length(contents) < 1) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "empty blocks for payload %s", (gchar *)l->data); return FALSE; } /* adding blocks to the entity FW */ for (guint i = 0; i < json_array_get_length(blocks); i++) { JsonNode *block = json_array_get_element(blocks, i); if (!fu_logitech_rdfu_firmware_block_add(entity_fw, block, error)) { g_prefix_error(error, "unable to parse block %u: ", i); return FALSE; } } g_debug("added payload %s with %u blocks", entity_fw->payload_name, entity_fw->blocks->len); } /* success */ return TRUE; } static void fu_logitech_rdfu_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuLogitechRdfuFirmware *self = FU_LOGITECH_RDFU_FIRMWARE(firmware); if (self->model_id != NULL) fu_xmlb_builder_insert_kv(bn, "modelId", self->model_id); if (self->payload_name != NULL) fu_xmlb_builder_insert_kv(bn, "payload", self->payload_name); if (self->magic != NULL) { g_autofree gchar *magic = fu_byte_array_to_string(self->magic); fu_xmlb_builder_insert_kv(bn, "magic", magic); } if (self->blocks != NULL) fu_xmlb_builder_insert_kx(bn, "blocks", self->blocks->len); } static void fu_logitech_rdfu_firmware_finalize(GObject *object) { FuLogitechRdfuFirmware *self = FU_LOGITECH_RDFU_FIRMWARE(object); g_free(self->model_id); g_free(self->payload_name); if (self->magic != NULL) g_byte_array_unref(self->magic); if (self->blocks != NULL) g_ptr_array_unref(self->blocks); G_OBJECT_CLASS(fu_logitech_rdfu_firmware_parent_class)->finalize(object); } static void fu_logitech_rdfu_firmware_init(FuLogitechRdfuFirmware *self) { } static void fu_logitech_rdfu_firmware_class_init(FuLogitechRdfuFirmwareClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_logitech_rdfu_firmware_finalize; firmware_class->parse = fu_logitech_rdfu_firmware_parse; firmware_class->export = fu_logitech_rdfu_firmware_export; } fwupd-2.0.10/plugins/logitech-hidpp/fu-logitech-rdfu-firmware.h000066400000000000000000000013411501337203100244210ustar00rootroot00000000000000/* * Copyright 2024 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_LOGITECH_RDFU_FIRMWARE (fu_logitech_rdfu_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuLogitechRdfuFirmware, fu_logitech_rdfu_firmware, FU, LOGITECH_RDFU_FIRMWARE, FuFirmware) struct _FuLogitechRdfuFirmwareClass { FuFirmwareClass parent_class; }; gchar * fu_logitech_rdfu_firmware_get_model_id(FuLogitechRdfuFirmware *self, GError **error); GByteArray * fu_logitech_rdfu_firmware_get_magic(FuLogitechRdfuFirmware *self, GError **error); GPtrArray * fu_logitech_rdfu_firmware_get_blocks(FuLogitechRdfuFirmware *self, GError **error); fwupd-2.0.10/plugins/logitech-hidpp/logitech-hidpp.quirk000066400000000000000000000122171501337203100232530ustar00rootroot00000000000000# Unifying Receiver [HIDRAW\VEN_046D&DEV_C52B] Plugin = logitech_hidpp GType = FuLogitechHidppRuntimeUnifying VendorId = USB:0x046D InstallDuration = 30 # Bolt Receiver (runtime) [HIDRAW\VEN_046D&DEV_C548] Plugin = logitech_hidpp GType = FuLogitechHidppRuntimeBolt VendorId = USB:0x046D InstallDuration = 30 # Bolt Receiver (bootloader) [HIDRAW\VEN_046D&DEV_AB07] Plugin = logitech_hidpp Name = Bolt Receiver Vendor = Logitech GType = FuLogitechHidppDevice CounterpartGuid = HIDRAW\VEN_046D&DEV_C548 InstallDuration = 30 Flags = rebind-attach,force-receiver-id,replug-match-guid,add-radio,counterpart-visible LogitechHidppModelId = B601C5480000 # Bolt receiver radio (bootloader) [HIDRAW\VEN_046D&MOD_B601C5480000&ENT_05] CounterpartGuid = HIDRAW\VEN_046D&DEV_C548&ENT_05 Flags = is-bootloader # Nordic [USB\VID_046D&PID_AAAA] Plugin = logitech_hidpp GType = FuLogitechHidppBootloaderNordic FirmwareSizeMin = 0x4000 CounterpartGuid = HIDRAW\VEN_046D&DEV_C52B InstallDuration = 30 # Nordic Pico [USB\VID_046D&PID_AAAE] Plugin = logitech_hidpp GType = FuLogitechHidppBootloaderNordic FirmwareSizeMin = 0x4000 CounterpartGuid = HIDRAW\VEN_046D&DEV_C52B InstallDuration = 30 # Texas [USB\VID_046D&PID_AAAC] Plugin = logitech_hidpp GType = FuLogitechHidppBootloaderTexas FirmwareSizeMin = 0x4000 CounterpartGuid = HIDRAW\VEN_046D&DEV_C52B InstallDuration = 18 # Texas Pico [USB\VID_046D&PID_AAAD] Plugin = logitech_hidpp GType = FuLogitechHidppBootloaderTexas FirmwareSizeMin = 0x4000 CounterpartGuid = HIDRAW\VEN_046D&DEV_C52B InstallDuration = 18 # Texas Pico (another variant) / C-U0016 [USB\VID_046D&PID_AAE5] Plugin = logitech_hidpp GType = FuLogitechHidppBootloaderTexas FirmwareSizeMin = 0x4000 CounterpartGuid = HIDRAW\VEN_046D&DEV_C52B InstallDuration = 18 # Texas Unknown # https://github.com/Logitech/fw_updates/blob/1ddde61310573ecea54c4204b401393a90f4bbae/RQR24/RQR24.11/RQR24.11_B0036.txt#L22 [USB\VID_046D&PID_AAF8] Plugin = logitech_hidpp GType = FuLogitechHidppBootloaderTexas FirmwareSizeMin = 0x4000 CounterpartGuid = HIDRAW\VEN_046D&DEV_C52B InstallDuration = 18 # K780 (through Unifying receiver) [HIDRAW\VEN_046D&DEV_405B] Plugin = logitech_hidpp GType = FuLogitechHidppDevice ParentGuid = HIDRAW\VEN_046D&DEV_C52B InstallDuration = 150 # MR0077 [HIDRAW\VEN_046D&MOD_B02800000000] Plugin = logitech_hidpp GType = FuLogitechHidppDevice Flags = add-radio InstallDuration = 270 # MR0077 (BLE direct) [HIDRAW\VEN_046D&DEV_B028] Plugin = logitech_hidpp GType = FuLogitechHidppDevice Flags = add-radio,ble,rebind-attach InstallDuration = 270 # YR0073 [HIDRAW\VEN_046D&MOD_B36300000000] Plugin = logitech_hidpp GType = FuLogitechHidppDevice Flags = add-radio InstallDuration = 270 # YR0073 (BLE direct) [HIDRAW\VEN_046D&DEV_B363] Plugin = logitech_hidpp GType = FuLogitechHidppDevice Flags = add-radio,ble,rebind-attach InstallDuration = 270 # M650 [HIDRAW\VEN_046D&MOD_B02A00000000] Plugin = logitech_hidpp GType = FuLogitechHidppDevice Flags = add-radio,no-request-required InstallDuration = 270 # M650 (BLE direct) [HIDRAW\VEN_046D&DEV_B02A] Plugin = logitech_hidpp GType = FuLogitechHidppDevice Flags = add-radio,ble,rebind-attach,no-request-required InstallDuration = 270 # M750 [HIDRAW\VEN_046D&MOD_B02C00000000] Plugin = logitech_hidpp GType = FuLogitechHidppDevice Flags = add-radio,no-request-required InstallDuration = 270 # M750 (BLE direct) [HIDRAW\VEN_046D&DEV_B02C] Plugin = logitech_hidpp GType = FuLogitechHidppDevice Flags = add-radio,ble,rebind-attach,no-request-required InstallDuration = 270 # M650 For Business [HIDRAW\VEN_046D&MOD_B03200000000] Plugin = logitech_hidpp GType = FuLogitechHidppDevice Flags = add-radio,no-request-required InstallDuration = 270 # M650 For Business (BLE direct) [HIDRAW\VEN_046D&DEV_B032] Plugin = logitech_hidpp GType = FuLogitechHidppDevice Flags = add-radio,ble,rebind-attach,no-request-required InstallDuration = 270 # M550 [HIDRAW\VEN_046D&MOD_B02B00000000] Plugin = logitech_hidpp GType = FuLogitechHidppDevice Flags = add-radio,no-request-required InstallDuration = 270 # M550 (BLE direct) [HIDRAW\VEN_046D&DEV_B02B] Plugin = logitech_hidpp GType = FuLogitechHidppDevice Flags = add-radio,ble,rebind-attach,no-request-required InstallDuration = 270 # K650 [HIDRAW\VEN_046D&MOD_B36F00000000] Plugin = logitech_hidpp GType = FuLogitechHidppDevice Flags = add-radio,no-request-required InstallDuration = 270 # K650 (BLE direct) [HIDRAW\VEN_046D&DEV_B36F] Plugin = logitech_hidpp GType = FuLogitechHidppDevice Flags = add-radio,ble,rebind-attach,no-request-required InstallDuration = 270 # K650 For Business [HIDRAW\VEN_046D&MOD_B37000000000] Plugin = logitech_hidpp GType = FuLogitechHidppDevice Flags = add-radio,no-request-required InstallDuration = 270 # K650 For Business (BLE direct) [HIDRAW\VEN_046D&DEV_B370] Plugin = logitech_hidpp GType = FuLogitechHidppDevice Flags = add-radio,ble,rebind-attach,no-request-required InstallDuration = 270 # MX Mechanical [HIDRAW\VEN_046D&MOD_B34E00000000] Plugin = logitech_hidpp GType = FuLogitechHidppDevice Flags = ~add-radio InstallDuration = 308 # MX Mechanical (BLE direct) [HIDRAW\VEN_046D&DEV_B34E] Plugin = logitech_hidpp GType = FuLogitechHidppDevice Flags = ~add-radio,ble,no-request-required InstallDuration = 650 fwupd-2.0.10/plugins/logitech-hidpp/meson.build000066400000000000000000000040521501337203100214360ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginLogitechHidpp"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('logitech-hidpp.quirk') plugin_builtin_logitech_hidpp = static_library('fu_plugin_logitech_hidpp', rustgen.process('fu-logitech-hidpp.rs'), rustgen.process('fu-logitech-hidpp-rdfu.rs'), sources: [ 'fu-logitech-hidpp-plugin.c', 'fu-logitech-hidpp-bootloader.c', 'fu-logitech-hidpp-bootloader-nordic.c', 'fu-logitech-hidpp-bootloader-texas.c', 'fu-logitech-hidpp-common.c', 'fu-logitech-hidpp-hidpp.c', 'fu-logitech-hidpp-device.c', 'fu-logitech-hidpp-hidpp-msg.c', 'fu-logitech-hidpp-runtime.c', 'fu-logitech-hidpp-runtime-unifying.c', 'fu-logitech-hidpp-runtime-bolt.c', 'fu-logitech-hidpp-radio.c', 'fu-logitech-rdfu-firmware.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) plugin_builtins += plugin_builtin_logitech_hidpp device_tests += files( 'tests/logitech-bolt-receiver.json', 'tests/logitech-k780.json', 'tests/logitech-m650.json', 'tests/logitech-m750.json', 'tests/logitech-mr0077.json', 'tests/logitech-rqr12.json', 'tests/logitech-rqr12-signed.json', 'tests/logitech-rqr24.json', 'tests/logitech-rqr24-signed.json', ) enumeration_data += files( 'tests/logitech-bolt-receiver-setup.json', ) if get_option('tests') env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) e = executable( 'logitech-hidpp-self-test', sources: [ 'fu-logitech-hidpp-self-test.c', ], include_directories: plugin_incdirs, dependencies: plugin_deps, link_with: [ plugin_libs, plugin_builtin_logitech_hidpp, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ cargs, '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('logitech-hidpp-self-test', e, env: env) endif endif fwupd-2.0.10/plugins/logitech-hidpp/tests/000077500000000000000000000000001501337203100204355ustar00rootroot00000000000000fwupd-2.0.10/plugins/logitech-hidpp/tests/logitech-bolt-receiver-setup.json000066400000000000000000000230101501337203100270200ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUdevDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:08.1/0000:c5:00.3/usb1/1-1/1-1:1.2/0003:046D:C548.004F/hidraw/hidraw8", "DeviceFile": "/dev/hidraw8", "Subsystem": "hidraw", "Vendor": 1133, "Model": 50504, "Created": "2025-02-10T14:37:09.457851Z", "Events": [ { "Id": "#d5a801ad", "Data": "hidraw" }, { "Id": "#ad8c58d3" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "MAJOR=241\nMINOR=8\nDEVNAME=hidraw8" }, { "Id": "#d432c663", "Data": "hidraw8" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c" }, { "Id": "#193a680c", "GType": "FuUdevDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:08.1/0000:c5:00.3/usb1/1-1/1-1:1.2/0003:046D:C548.004F/hidraw/hidraw8" }, { "Id": "#d5a801ad", "Data": "hid" }, { "Id": "#ad8c58d3", "Data": "hid-generic" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "DRIVER=hid-generic\nHID_ID=0003:0000046D:0000C548\nHID_NAME=Logitech USB Receiver\nHID_PHYS=usb-0000:c5:00.3-1/input2\nHID_UNIQ=\nMODALIAS=hid:b0003g0001v0000046Dp0000C548" }, { "Id": "#d432c663" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c" }, { "Id": "#b8e66ef6", "Data": "0003:0000046D:0000C548" }, { "Id": "#89cef97f", "Data": "Logitech USB Receiver" }, { "Id": "#3e5408a4", "Data": "" }, { "Id": "#00f97b18", "Data": "usb-0000:c5:00.3-1/input2" }, { "Id": "#19c26604", "GType": "FuUsbDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:08.1/0000:c5:00.3/usb1/1-1/1-1:1.2/0003:046D:C548.004F/hidraw/hidraw8", "PhysicalId": "usb-0000:c5:00.3-1/input2" }, { "Id": "#d5a801ad", "Data": "usb" }, { "Id": "#ad8c58d3", "Data": "usb" }, { "Id": "#1075ed5c", "Data": "usb_device" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=45\nDEVNAME=bus/usb/001/046\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=46d/c548/500\nTYPE=0/0/0\nBUSNUM=001\nDEVNUM=046" }, { "Id": "#d432c663", "Data": "bus/usb/001/046" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c", "Data": "usb_device" }, { "Id": "#4693935e", "Data": "001" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=45\nDEVNAME=bus/usb/001/046\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=46d/c548/500\nTYPE=0/0/0\nBUSNUM=001\nDEVNUM=046" }, { "Id": "#1ab3ae0a", "Data": "046" }, { "Id": "#9a347f1f", "Data": "EgEAAgAAAEBtBEjFAAUBAgABCQJUAAMBBKAxCQQAAAEDAQEACSERAQABIkMABwWBA0AAAQkEAQABAwECAAkhEQEAASKFAAcFggNAAAEJBAIAAQMAAAAJIREBAAEiNgAHBYMDQAAB" }, { "Id": "#624d3a4d" }, { "Id": "#193a680c", "GType": "FuUdevDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:08.1/0000:c5:00.3/usb1/1-1/1-1:1.2/0003:046D:C548.004F/hidraw/hidraw8", "PhysicalId": "usb-0000:c5:00.3-1/input2" }, { "Id": "#d5a801ad", "Data": "hid" }, { "Id": "#ad8c58d3", "Data": "hid-generic" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "DRIVER=hid-generic\nHID_ID=0003:0000046D:0000C548\nHID_NAME=Logitech USB Receiver\nHID_PHYS=usb-0000:c5:00.3-1/input2\nHID_UNIQ=\nMODALIAS=hid:b0003g0001v0000046Dp0000C548" }, { "Id": "#d432c663" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c" }, { "Id": "#b8e66ef6", "Data": "0003:0000046D:0000C548" }, { "Id": "#3e5408a4", "Data": "" }, { "Id": "#19c26604", "GType": "FuUsbDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:08.1/0000:c5:00.3/usb1/1-1/1-1:1.2/0003:046D:C548.004F/hidraw/hidraw8", "PhysicalId": "usb-0000:c5:00.3-1/input2" }, { "Id": "#d5a801ad", "Data": "usb" }, { "Id": "#ad8c58d3", "Data": "usb" }, { "Id": "#1075ed5c", "Data": "usb_device" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=45\nDEVNAME=bus/usb/001/046\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=46d/c548/500\nTYPE=0/0/0\nBUSNUM=001\nDEVNUM=046" }, { "Id": "#d432c663", "Data": "bus/usb/001/046" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c", "Data": "usb_device" }, { "Id": "#4693935e", "Data": "001" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=45\nDEVNAME=bus/usb/001/046\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=46d/c548/500\nTYPE=0/0/0\nBUSNUM=001\nDEVNUM=046" }, { "Id": "#1ab3ae0a", "Data": "046" }, { "Id": "#9a347f1f", "Data": "EgEAAgAAAEBtBEjFAAUBAgABCQJUAAMBBKAxCQQAAAEDAQEACSERAQABIkMABwWBA0AAAQkEAQABAwECAAkhEQEAASKFAAcFggNAAAEJBAIAAQMAAAAJIREBAAEiNgAHBYMDQAAB" }, { "Id": "#624d3a4d" }, { "Id": "#d0550a1a", "GType": "FuUdevDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:08.1/0000:c5:00.3/usb1/1-1/1-1:1.2/0003:046D:C548.004F/hidraw/hidraw8", "PhysicalId": "usb-0000:c5:00.3-1/input2" }, { "Id": "#d5a801ad", "Data": "usb" }, { "Id": "#ad8c58d3", "Data": "usbhid" }, { "Id": "#1075ed5c", "Data": "usb_interface" }, { "Id": "#bddbca22", "Data": "DEVTYPE=usb_interface\nDRIVER=usbhid\nPRODUCT=46d/c548/500\nTYPE=0/0/0\nINTERFACE=3/0/0\nMODALIAS=usb:v046DpC548d0500dc00dsc00dp00ic03isc00ip00in02" }, { "Id": "#d432c663" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c", "Data": "usb_interface" }, { "Id": "#44482569", "Data": "3/0/0" }, { "Id": "#502ea0ea" }, { "Id": "#e2a16343", "Data": "Ef+DtQIFAAAIAbYQBgAAAAAAAAA=" }, { "Id": "#dea3d98a" }, { "Id": "#e2a16343", "Data": "Ef+D9AEwAAAFslGYrgAAAAAAAAA=" }, { "Id": "#c334b520" }, { "Id": "#e2a16343", "Data": "Ef+D9AAFAAAINbP2iwAAAAAAAAA=" }, { "Id": "#47582770" }, { "Id": "#e2a16343", "Data": "Ef+D9AUFAAESAAAAAAAAAAAAAAA=" }, { "Id": "#e2a16343", "Error": 19, "ErrorMsg": "timeout" }, { "Id": "#182845d0" }, { "Id": "#e2a16343", "Data": "EP+AAAAAAA==" }, { "Id": "#27c22eeb" }, { "Id": "#e2a16343", "Data": "Ef+DtWEBDU1YIE1hc3RlciAzIEI=" }, { "Id": "#ff7e60c1" }, { "Id": "#e2a16343", "Data": "Ef+DtVECKLDTI8oFAQACCgEAAAA=" }, { "Id": "#6c9a3379" }, { "Id": "#e2a16343", "Data": "Ef+DtWIBCU1YIEtleXMgQgAAAAA=" }, { "Id": "#98dfc13f" }, { "Id": "#e2a16343", "Data": "Ef+DtVJBY7MSIKuDAQABFAEAAAA=" }, { "Id": "#e4075abf" }, { "Id": "#e2a16343", "Data": "Ef+DtWMBC1BvbGx1eCBBUFAxAAA=" }, { "Id": "#dcaa5ab3" }, { "Id": "#e2a16343", "Data": "Ef+DtVNBTrOtHRBEAgABFAEAAAA=" }, { "Id": "#49152771" }, { "Id": "#e2a16343", "Data": "EP+Pg7UIAA==" }, { "Id": "#f507d0bf" }, { "Id": "#e2a16343", "Data": "EP+Pg7UIAA==" }, { "Id": "#cdc373b0" }, { "Id": "#e2a16343", "Data": "EP+Pg7UIAA==" } ] } ] } fwupd-2.0.10/plugins/logitech-hidpp/tests/logitech-bolt-receiver.json000066400000000000000000000021261501337203100256670ustar00rootroot00000000000000{ "name": "Logitech Bolt receiver", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/logitech-bolt-receiver-setup.json", "components": [ { "version": "MPR05.00_B0008", "protocol": "com.logitech.unifyingsigned", "guids": [ "f70d9241-2656-546b-a8e4-211bac348667" ] } ] }, { "url": "c35ade1237da4e1ff90d031cc2b2218c32ee53c01b92b014aae2d2226aed2e35-Logitech-MPR05-MPR05.00_B0009.cab", "components": [ { "version": "MPR05.00_B0009", "protocol": "com.logitech.unifyingsigned", "guids": [ "f70d9241-2656-546b-a8e4-211bac348667" ] } ] }, { "url": "f1e3ba268ae4e1d3c029392d4f203a976970b162521296a2e9a9a31f314c0c46-Logitech-MPR05-MPR05.01_B0010.cab", "components": [ { "version": "MPR05.01_B0010", "protocol": "com.logitech.unifyingsigned", "guids": [ "f70d9241-2656-546b-a8e4-211bac348667" ] } ] } ] } fwupd-2.0.10/plugins/logitech-hidpp/tests/logitech-k780.json000066400000000000000000000011571501337203100236210ustar00rootroot00000000000000{ "name": "Logitech K780", "interactive": false, "steps": [ { "url": "454f1034f5efeb531163d90852ef33afd3fafeeb-Logitech-K780-MPK01.02_B0021.cab", "components": [ { "version": "MPK01.02_B0021", "guids": [ "3932ba15-2bbe-5bbb-817e-6c74e7088509" ] } ] }, { "url": "b0dffe84c6d3681e7ae5f27509781bc1cf924dd7-Logitech-K780-MPK01.03_B0024.cab", "components": [ { "version": "MPK01.03_B0024", "guids": [ "3932ba15-2bbe-5bbb-817e-6c74e7088509" ] } ] } ] } fwupd-2.0.10/plugins/logitech-hidpp/tests/logitech-m650.json000066400000000000000000000014131501337203100236120ustar00rootroot00000000000000{ "name": "Logitech M650", "interactive": false, "steps": [ { "url": "6531c7a26d54f9dacbc24f81e8b26e37dd630fe22a417cd2ce6e13ea3b6fd105-Logitech-RBM16-RBM16.00_B0009.cab", "components": [ { "version": "RBM16.00_B0009", "protocol": "com.logitech.unifyingsigned", "guids": [ "bb3fe644-ed3c-55a4-a506-191e65974b04" ] } ] }, { "url": "422d8c719d859b4ef321d95aa9e21f8d0d899ac924b9ca3ed76b1d745224e8e4-Logitech-RBM16-RBM16.00_B0010.cab", "components": [ { "version": "RBM16.00_B0010", "protocol": "com.logitech.unifyingsigned", "guids": [ "bb3fe644-ed3c-55a4-a506-191e65974b04" ] } ] } ] } fwupd-2.0.10/plugins/logitech-hidpp/tests/logitech-m750.json000066400000000000000000000005641501337203100236210ustar00rootroot00000000000000{ "name": "Logitech M750", "interactive": false, "steps": [ { "url": "597a12cca95b9dcf19e82e5add970a45bf658de8c82dc329cc271a6c50d68752-Logitech-RBM18-RBM18.00_B0010.cab", "components": [ { "version": "RBM18.00_B0010", "guids": [ "b0904956-9081-5ce4-9490-bee057a2d577" ] } ] } ] } fwupd-2.0.10/plugins/logitech-hidpp/tests/logitech-mr0077.json000066400000000000000000000014141501337203100240600ustar00rootroot00000000000000{ "name": "Logitech MR0077", "interactive": true, "steps": [ { "url": "7b86a6cb5747607f38e78259734dcf0d1afc02d2116b7386ac00c15e70eece72-Logitech-RBM14-RBM14_00_B0007.cab", "components": [ { "version": "RBM14.00_B0007", "protocol": "com.logitech.unifyingsigned", "guids": [ "b2b50d12-c3df-5980-b5e9-6cc6c34b3037" ] } ] }, { "url": "1cec33151e0f206df9b353a56e318adebb9a9ba9f330b452336eef169f5b1f3c-Logitech-RBM14-RBM14_00_B0008.cab", "components": [ { "version": "RBM14.00_B0008", "protocol": "com.logitech.unifyingsigned", "guids": [ "b2b50d12-c3df-5980-b5e9-6cc6c34b3037" ] } ] } ] } fwupd-2.0.10/plugins/logitech-hidpp/tests/logitech-rqr12-signed.json000066400000000000000000000011441501337203100253420ustar00rootroot00000000000000{ "name": "Logitech Unifying Receiver (RQR12 SIGNED)", "interactive": false, "steps": [ { "url": "2443cef8b1dae48751d3c60dab22210733e57036-Logitech-Unifying-RQR12.11_B0032.cab", "emulation-url": "5c4e16b209915a41bd3b6c24da67e868c7147a38e49ca3abc70d3efd8bef4feb-Logitech-Unifying-RQR12.11_B0032.zip", "components": [ { "version": "RQR12.11_B0032", "protocol": "com.logitech.unifyingsigned", "guids": [ "9d131a0c-a606-580f-8eda-80587250b8d6", "d637baf7-3ab5-502a-8169-2545302e44e2" ] } ] } ] } fwupd-2.0.10/plugins/logitech-hidpp/tests/logitech-rqr12.json000066400000000000000000000017521501337203100241000ustar00rootroot00000000000000{ "name": "Logitech Unifying Receiver (RQR12)", "interactive": false, "steps": [ { "url": "6e5ab5961ec4c577bff198ebb465106e979cf686-Logitech-Unifying-RQR12.05_B0028.cab", "emulation-url": "b87fe1c0b562780e4ab05459422af6ce4bd5d8914c9c3c519b793d9baf6c52b2-Logitech-Unifying-RQR12.05_B0028.zip", "components": [ { "version": "RQR12.05_B0028", "protocol": "com.logitech.unifying", "guids": [ "9d131a0c-a606-580f-8eda-80587250b8d6" ] } ] }, { "url": "938fec082652c603a1cdafde7cd25d76baadc70d-Logitech-Unifying-RQR12.07_B0029.cab", "emulation-url": "af83dc257e32b7a18656b3e11b269ab5badf303d5897c11d5f8ceb2d3117b70e-Logitech-Unifying-RQR12.07_B0029.zip", "components": [ { "version": "RQR12.07_B0029", "protocol": "com.logitech.unifying", "guids": [ "9d131a0c-a606-580f-8eda-80587250b8d6" ] } ] } ] } fwupd-2.0.10/plugins/logitech-hidpp/tests/logitech-rqr24-signed.json000066400000000000000000000013441501337203100253470ustar00rootroot00000000000000{ "name": "Logitech Unifying Receiver (RQR24 SIGNED)", "interactive": false, "steps": [ { "url": "6a0134766c26c73cc08f3d30f0bb84d94d035b50f702e39506abdcab9a8f658f-Logitech-Unifying-RQR24.11_B0036.cab", "emulation-url": "67a9b3088294dc0f7b2d5397ba2eb6c5cd514e6c2899f84a77cb5930e5bb17ab-Logitech-Unifying-RQR24.11_B0036.zip", "components": [ { "version": "RQR24.11_B0036", "protocol": "com.logitech.unifyingsigned", "guids": [ "cc4cbfa9-bf9d-540b-b92b-172ce31013c1", "87fd7145-3913-50c8-bfcb-86f85006d7d1", "111c9951-f819-5c48-93ef-205a8f8b96c1", "40410bd7-57eb-5c82-9eac-abf893861221" ] } ] } ] } fwupd-2.0.10/plugins/logitech-hidpp/tests/logitech-rqr24.json000066400000000000000000000017521501337203100241030ustar00rootroot00000000000000{ "name": "Logitech Unifying Receiver (RQR24)", "interactive": false, "steps": [ { "url": "82b90b2614a9a4d0aced1ab8a4a99e228c95585c-Logitech-Unifying-RQ024.03_B0027.cab", "emulation-url": "a441bd637ef26e23f8ce2d184d8a88ea63a75123c1a8375fc41701231980c922-Logitech-Unifying-RQ024.03_B0027.zip", "components": [ { "version": "RQR24.03_B0027", "protocol": "com.logitech.unifying", "guids": [ "cc4cbfa9-bf9d-540b-b92b-172ce31013c1" ] } ] }, { "url": "4511b9b0d123bdbe8a2007233318ab215a59dfe6-Logitech-Unifying-RQR24.05_B0029.cab", "emulation-url": "56c390da7584e0beefc8d92171e3b16c5e8f9406ef00e5eff3ade4ddcd13d455-Logitech-Unifying-RQR24.05_B0029.zip", "components": [ { "version": "RQR24.05_B0029", "protocol": "com.logitech.unifying", "guids": [ "cc4cbfa9-bf9d-540b-b92b-172ce31013c1" ] } ] } ] } fwupd-2.0.10/plugins/logitech-rallysystem/000077500000000000000000000000001501337203100205575ustar00rootroot00000000000000fwupd-2.0.10/plugins/logitech-rallysystem/README.md000066400000000000000000000025421501337203100220410ustar00rootroot00000000000000--- title: Plugin: Logitech Rally System --- ## Introduction This plugin can upgrade the firmware on Logitech Video Collaboration products (RallySystem). ioctl is used to query system version and serial number from Audio module of RallySystem. USB bulk transfer is used to push new firmware image to TableHub module of RallySystem ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a packed binary file format. This plugin supports the following protocol ID: * `com.logitech.vc.rallysystem` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_046D&DEV_088F` ## Update Behavior The peripheral firmware is deployed when the device is in normal runtime mode, and the device will reset when the new firmware has been written. ## Design Notes ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x046D` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb` and the ioctl interfaces `HIDIOCGFEATURE`, `HIDIOCSFEATURE` and `HIDIOCGINPUT`. ## Version Considerations This plugin has been available since fwupd version `1.9.7`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Sanjay Sheth: @vcdmp fwupd-2.0.10/plugins/logitech-rallysystem/fu-logitech-rallysystem-audio-device.c000066400000000000000000000122451501337203100300570ustar00rootroot00000000000000/* * Copyright 1999-2023 Logitech, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-logitech-rallysystem-audio-device.h" #include "fu-logitech-rallysystem-struct.h" #define TOPOLOGY_DATA_LEN 513 /* plus 1 byte for the report id */ #define SERIAL_NUMBER_REQUEST_DATA_LEN 49 #define SERIAL_NUMBER_RESPONSE_DATA_LEN 128 struct _FuLogitechRallysystemAudioDevice { FuHidrawDevice parent_instance; }; G_DEFINE_TYPE(FuLogitechRallysystemAudioDevice, fu_logitech_rallysystem_audio_device, FU_TYPE_HIDRAW_DEVICE) static gboolean fu_logitech_rallysystem_audio_device_set_version(FuLogitechRallysystemAudioDevice *self, GError **error) { guint8 buf[TOPOLOGY_DATA_LEN] = {0x3E, 0x0}; guint32 fwversion = 0; /* setup HID report to query current device version */ if (!fu_hidraw_device_get_feature(FU_HIDRAW_DEVICE(self), buf, sizeof(buf), FU_IOCTL_FLAG_RETRY, error)) { return FALSE; } if (!fu_memread_uint24_safe( buf, sizeof(buf), 0xB8, /* topology size of 12 bytes * 15 elements, plus an offset */ &fwversion, G_BIG_ENDIAN, error)) return FALSE; fu_device_set_version_raw(FU_DEVICE(self), fwversion); /* success */ return TRUE; } static gboolean fu_logitech_rallysystem_audio_device_set_serial(FuLogitechRallysystemAudioDevice *self, GError **error) { guint8 buf_req[SERIAL_NUMBER_REQUEST_DATA_LEN] = {0x28, 0x85, 0x08, 0xBB, 0x1B, 0x00, 0x01, 0x30, 0, 0, 0, 0x0C}; guint8 buf_res[SERIAL_NUMBER_RESPONSE_DATA_LEN] = {0x29, 0x0}; g_autoptr(GByteArray) st = NULL; g_autoptr(GString) serial = g_string_new(NULL); /* setup HID report for serial number request */ if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), buf_req, sizeof(buf_req), FU_IOCTL_FLAG_RETRY, error)) return FALSE; /* wait 100ms for device to consume request and prepare for response */ fu_device_sleep(FU_DEVICE(self), 100); /* setup HID report to query serial number */ if (!fu_hidraw_device_get_feature(FU_HIDRAW_DEVICE(self), buf_res, sizeof(buf_res), FU_IOCTL_FLAG_RETRY, error)) { return FALSE; } /* desired serial number format: PID:YYYYMMDD:EthernetMacAddress */ st = fu_struct_audio_serial_number_parse(buf_res, sizeof(buf_res), 0x0, error); if (st == NULL) return FALSE; g_string_append_printf(serial, "%04x:%04u%02u%02u:", fu_struct_audio_serial_number_get_pid(st), fu_struct_audio_serial_number_get_year(st), fu_struct_audio_serial_number_get_month(st), fu_struct_audio_serial_number_get_day(st)); for (guint i = 0x0; i < FU_STRUCT_AUDIO_SERIAL_NUMBER_SIZE_MAC_ADDRESS; i++) g_string_append_printf(serial, "%02x", st->data[i]); fu_device_set_serial(FU_DEVICE(self), serial->str); /* success */ return TRUE; } static gboolean fu_logitech_rallysystem_audio_device_setup(FuDevice *device, GError **error) { FuLogitechRallysystemAudioDevice *self = FU_LOGITECH_RALLYSYSTEM_AUDIO_DEVICE(device); if (!fu_logitech_rallysystem_audio_device_set_version(self, error)) return FALSE; if (!fu_logitech_rallysystem_audio_device_set_serial(self, error)) return FALSE; return TRUE; } static void fu_logitech_rallysystem_audio_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 0, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 100, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static gchar * fu_logitech_rallysystem_audio_device_convert_version(FuDevice *device, guint64 version_raw) { guint8 major = 0; guint8 minor = 0; guint8 build = 0; /* * device reports system version in 3 bytes: major.minor.build * convert major.minor.build -> major.minor.0.build */ major = (version_raw >> 16) & 0xFF; minor = (version_raw >> 8) & 0xFF; build = (version_raw >> 0) & 0xFF; version_raw = (((guint32)major) << 24) | (((guint32)minor) << 16) | (((guint32)build) << 0); return fu_version_from_uint32(version_raw, fu_device_get_version_format(device)); } static void fu_logitech_rallysystem_audio_device_init(FuLogitechRallysystemAudioDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "com.logitech.vc.rallysystem"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_retry_set_delay(FU_DEVICE(self), 1000); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_NONBLOCK); } static void fu_logitech_rallysystem_audio_device_class_init(FuLogitechRallysystemAudioDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_logitech_rallysystem_audio_device_setup; device_class->set_progress = fu_logitech_rallysystem_audio_device_set_progress; device_class->convert_version = fu_logitech_rallysystem_audio_device_convert_version; } fwupd-2.0.10/plugins/logitech-rallysystem/fu-logitech-rallysystem-audio-device.h000066400000000000000000000006331501337203100300620ustar00rootroot00000000000000/* * Copyright 1999-2023 Logitech, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_LOGITECH_RALLYSYSTEM_AUDIO_DEVICE (fu_logitech_rallysystem_audio_device_get_type()) G_DECLARE_FINAL_TYPE(FuLogitechRallysystemAudioDevice, fu_logitech_rallysystem_audio_device, FU, LOGITECH_RALLYSYSTEM_AUDIO_DEVICE, FuHidrawDevice) fwupd-2.0.10/plugins/logitech-rallysystem/fu-logitech-rallysystem-plugin.c000066400000000000000000000061111501337203100270120ustar00rootroot00000000000000/* * Copyright 1999-2023 Logitech, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-logitech-rallysystem-audio-device.h" #include "fu-logitech-rallysystem-plugin.h" #include "fu-logitech-rallysystem-tablehub-device.h" struct _FuLogitechRallysystemPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuLogitechRallysystemPlugin, fu_logitech_rallysystem_plugin, FU_TYPE_PLUGIN) static void fu_logitech_rallysystem_plugin_init(FuLogitechRallysystemPlugin *self) { } static void fu_logitech_rallysystem_plugin_device_added(FuPlugin *plugin, FuDevice *device) { GPtrArray *devices; /* * composite device is composed of multiple sub-devices: audio, video, tablehub, speakers. * Each sub-device has their own unique firmware version. * Audio sub-device has topology and system version information of all sub-devices. * Tablehub device is responsible to push firmware images to all sub-devices. * Since only tablehub can accept firmware image, its guid is used in metainfo file. * Ask here is for tablehub, to provide system version, so that application can use single * guid to query system version and check metainfo to determine if upgraded is needed * or not. Following logic reads the system version information from audio and * overwrite local version information of tablehub with this system version * Note: Multiple instances of same sub-device, not supported configuration (e.g. no t * tablehubs or audio) */ if (g_strcmp0(fu_device_get_plugin(device), "logitech_rallysystem") == 0) { if (FU_IS_LOGITECH_RALLYSYSTEM_AUDIO_DEVICE(device)) { devices = fu_plugin_get_devices(plugin); for (guint i = 0; i < devices->len; i++) { FuDevice *device_tmp = g_ptr_array_index(devices, i); if (FU_IS_LOGITECH_RALLYSYSTEM_TABLEHUB_DEVICE(device_tmp)) { fu_device_set_version(device_tmp, fu_device_get_version(device)); g_debug("overwriting tablehub version to: %s", fu_device_get_version(device)); break; } } } else if (FU_IS_LOGITECH_RALLYSYSTEM_TABLEHUB_DEVICE(device)) { devices = fu_plugin_get_devices(plugin); for (guint i = 0; i < devices->len; i++) { FuDevice *device_tmp = g_ptr_array_index(devices, i); if (FU_IS_LOGITECH_RALLYSYSTEM_AUDIO_DEVICE(device_tmp)) { fu_device_set_version(device, fu_device_get_version(device_tmp)); g_debug("overwriting tablehub version to %s", fu_device_get_version(device_tmp)); break; } } } } } static void fu_logitech_rallysystem_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_udev_subsystem(plugin, "hidraw"); fu_plugin_add_device_gtype(plugin, FU_TYPE_LOGITECH_RALLYSYSTEM_TABLEHUB_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_LOGITECH_RALLYSYSTEM_AUDIO_DEVICE); } static void fu_logitech_rallysystem_plugin_class_init(FuLogitechRallysystemPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_logitech_rallysystem_plugin_constructed; plugin_class->device_added = fu_logitech_rallysystem_plugin_device_added; } fwupd-2.0.10/plugins/logitech-rallysystem/fu-logitech-rallysystem-plugin.h000066400000000000000000000004401501337203100270160ustar00rootroot00000000000000/* * Copyright 1999-2023 Logitech, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuLogitechRallysystemPlugin, fu_logitech_rallysystem_plugin, FU, LOGITECH_RALLYSYSTEM_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/logitech-rallysystem/fu-logitech-rallysystem-tablehub-device.c000066400000000000000000000320671501337203100305500ustar00rootroot00000000000000/* * Copyright 1999-2023 Logitech, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-logitech-rallysystem-struct.h" #include "fu-logitech-rallysystem-tablehub-device.h" enum { EP_OUT, EP_IN, EP_LAST }; #define FU_LOGITECH_RALLYSYSTEM_TABLEHUB_DEVICE_IOCTL_TIMEOUT 3000 /* 3 sec */ #define FU_LOGITECH_RALLYSYSTEM_TABLEHUB_DEVICE_IOCTL_PROGRESS_TIMEOUT 90000 /* 90 sec */ struct _FuLogitechRallysystemTablehubDevice { FuUsbDevice parent_instance; guint bulk_ep[EP_LAST]; }; G_DEFINE_TYPE(FuLogitechRallysystemTablehubDevice, fu_logitech_rallysystem_tablehub_device, FU_TYPE_USB_DEVICE) static void fu_logitech_rallysystem_tablehub_device_to_string(FuDevice *device, guint idt, GString *str) { FuLogitechRallysystemTablehubDevice *self = FU_LOGITECH_RALLYSYSTEM_TABLEHUB_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "EpBulkIn", self->bulk_ep[EP_IN]); fwupd_codec_string_append_hex(str, idt, "EpBulkOut", self->bulk_ep[EP_OUT]); } static gboolean fu_logitech_rallysystem_tablehub_device_probe(FuDevice *device, GError **error) { FuLogitechRallysystemTablehubDevice *self = FU_LOGITECH_RALLYSYSTEM_TABLEHUB_DEVICE(device); guint8 bulk_iface = G_MAXUINT8; g_autoptr(GPtrArray) intfs = NULL; intfs = fu_usb_device_get_interfaces(FU_USB_DEVICE(self), error); if (intfs == NULL) return FALSE; for (guint i = 0; i < intfs->len; i++) { FuUsbInterface *intf = g_ptr_array_index(intfs, i); if (fu_usb_interface_get_class(intf) == FU_USB_CLASS_VENDOR_SPECIFIC) { g_autoptr(GPtrArray) endpoints = fu_usb_interface_get_endpoints(intf); bulk_iface = fu_usb_interface_get_number(intf); if (endpoints == NULL) continue; for (guint j = 0; j < endpoints->len; j++) { FuUsbEndpoint *ep = g_ptr_array_index(endpoints, j); if (j == EP_OUT) self->bulk_ep[EP_OUT] = fu_usb_endpoint_get_address(ep); else self->bulk_ep[EP_IN] = fu_usb_endpoint_get_address(ep); } } } if (bulk_iface == G_MAXUINT8) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no bulk interface found"); return FALSE; } fu_usb_device_add_interface(FU_USB_DEVICE(self), bulk_iface); return TRUE; } static gboolean fu_logitech_rallysystem_tablehub_device_send(FuLogitechRallysystemTablehubDevice *self, guint8 *buf, gsize bufsz, GError **error) { gsize actual_length = 0; if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), self->bulk_ep[EP_OUT], buf, bufsz, &actual_length, FU_LOGITECH_RALLYSYSTEM_TABLEHUB_DEVICE_IOCTL_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to send using bulk transfer: "); return FALSE; } if (bufsz != actual_length) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to send full packet using bulk transfer"); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "RallysystemBulkTx", buf, bufsz); return TRUE; } static gboolean fu_logitech_rallysystem_tablehub_device_recv(FuLogitechRallysystemTablehubDevice *self, guint8 *buf, gsize bufsz, guint timeout, GError **error) { gsize actual_length = 0; if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), self->bulk_ep[EP_IN], buf, bufsz, &actual_length, timeout, NULL, error)) { g_prefix_error(error, "failed to receive using bulk transfer: "); return FALSE; } if (bufsz != actual_length) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to receive full packet using bulk transfer"); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "RallysystemBulkRx", buf, bufsz); return TRUE; } static gboolean fu_logitech_rallysystem_tablehub_device_write_fw(FuLogitechRallysystemTablehubDevice *self, GInputStream *stream, FuProgress *progress, GError **error) { g_autoptr(FuChunkArray) chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 0x200, error); if (chunks == NULL) return FALSE; fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autofree guint8 *data_mut = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; data_mut = fu_memdup_safe(fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error); if (data_mut == NULL) return FALSE; if (!fu_logitech_rallysystem_tablehub_device_send(self, data_mut, fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "failed to send data packet 0x%x: ", i); return FALSE; } fu_progress_step_done(progress); } return TRUE; } static gboolean fu_logitech_rallysystem_tablehub_device_progress_cb(FuDevice *device, gpointer user_data, GError **error) { FuLogitechRallysystemTablehubDevice *self = FU_LOGITECH_RALLYSYSTEM_TABLEHUB_DEVICE(device); guint8 buf[FU_STRUCT_USB_PROGRESS_RESPONSE_SIZE] = {0x0}; g_autoptr(GByteArray) st_res = NULL; if (!fu_logitech_rallysystem_tablehub_device_recv( self, buf, sizeof(buf), FU_LOGITECH_RALLYSYSTEM_TABLEHUB_DEVICE_IOCTL_PROGRESS_TIMEOUT, error)) { g_prefix_error(error, "failed to get progress report: "); return FALSE; } st_res = fu_struct_usb_progress_response_parse(buf, sizeof(buf), 0x0, error); if (st_res == NULL) return FALSE; if (fu_struct_usb_progress_response_get_completed(st_res) != 100) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "percentage only %u%%", fu_struct_usb_progress_response_get_completed(st_res)); return FALSE; } /* success */ return TRUE; } static gboolean fu_logitech_rallysystem_tablehub_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuLogitechRallysystemTablehubDevice *self = FU_LOGITECH_RALLYSYSTEM_TABLEHUB_DEVICE(device); gsize streamsz = 0; guint8 buf[FU_STRUCT_USB_FIRMWARE_DOWNLOAD_RESPONSE_SIZE] = {0x0}; g_autoptr(GInputStream) stream = NULL; g_autoptr(GByteArray) st_req = fu_struct_usb_firmware_download_request_new(); g_autoptr(GByteArray) st_res = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "init"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 4, "device-write-blocks"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 35, "uninit"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 60, NULL); /* get default image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; fu_struct_usb_firmware_download_request_set_len(st_req, streamsz); if (!fu_struct_usb_firmware_download_request_set_fw_version(st_req, fu_device_get_version(device), error)) { g_prefix_error(error, "failed to copy download mode payload: "); return FALSE; } if (!fu_logitech_rallysystem_tablehub_device_send(self, st_req->data, st_req->len, error)) { g_prefix_error(error, "failed to set download mode: "); return FALSE; } if (!fu_logitech_rallysystem_tablehub_device_recv( self, buf, sizeof(buf), FU_LOGITECH_RALLYSYSTEM_TABLEHUB_DEVICE_IOCTL_TIMEOUT, error)) { g_prefix_error( error, "failed to receive set download mode response: please reboot the device: "); return FALSE; } st_res = fu_struct_usb_firmware_download_response_parse(buf, sizeof(buf), 0x0, error); if (st_res == NULL) return FALSE; fu_progress_step_done(progress); /* push each block to device */ if (!fu_logitech_rallysystem_tablehub_device_write_fw(self, stream, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* image file pushed. Device validates and uploads new image on inactive partition. * After upload is finished, device reboots itself */ if (!fu_device_retry_full(FU_DEVICE(self), fu_logitech_rallysystem_tablehub_device_progress_cb, 210, 1000, NULL, error)) { g_prefix_error(error, "failed to wait for 100pc: "); return FALSE; } fu_progress_step_done(progress); /* return no error since table hub may not come back right after reboot, it goes straight * to update camera/tv if needed and will be disappear until it finished the tasks */ fu_device_sleep_full(FU_DEVICE(self), 7 * 60 * 1000, fu_progress_get_child(progress)); fu_progress_step_done(progress); /* success! */ fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_logitech_rallysystem_tablehub_device_send_init_cmd_cb(FuDevice *device, gpointer user_data, GError **error) { FuLogitechRallysystemTablehubDevice *self = FU_LOGITECH_RALLYSYSTEM_TABLEHUB_DEVICE(device); guint8 buf[FU_STRUCT_USB_INIT_RESPONSE_SIZE] = {0x0}; g_autoptr(GByteArray) st_req = fu_struct_usb_init_request_new(); g_autoptr(GByteArray) st_res = NULL; if (!fu_logitech_rallysystem_tablehub_device_send(self, st_req->data, st_req->len, error)) { g_prefix_error(error, "failed to send init packet: "); return FALSE; } if (!fu_logitech_rallysystem_tablehub_device_recv( self, buf, sizeof(buf), FU_LOGITECH_RALLYSYSTEM_TABLEHUB_DEVICE_IOCTL_TIMEOUT, error)) { g_prefix_error(error, "failed to receive init packet: "); return FALSE; } st_res = fu_struct_usb_init_response_parse(buf, sizeof(buf), 0x0, error); if (st_res == NULL) { g_prefix_error(error, "failed to get correct init packet: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_logitech_rallysystem_tablehub_device_setup(FuDevice *device, GError **error) { FuLogitechRallysystemTablehubDevice *self = FU_LOGITECH_RALLYSYSTEM_TABLEHUB_DEVICE(device); guint8 buf[FU_STRUCT_USB_READ_VERSION_RESPONSE_SIZE] = {0x0}; g_autofree gchar *fw_version = NULL; g_autoptr(GByteArray) st_req = fu_struct_usb_read_version_request_new(); g_autoptr(GByteArray) st_res = NULL; /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_logitech_rallysystem_tablehub_device_parent_class) ->setup(device, error)) return FALSE; /* sending INIT. Retry if device is not in IDLE state to receive the data */ if (!fu_device_retry(device, fu_logitech_rallysystem_tablehub_device_send_init_cmd_cb, 5, NULL, error)) { g_prefix_error(error, "failed to write init packet: please reboot the device: "); return FALSE; } /* query tablehub firmware version */ if (!fu_logitech_rallysystem_tablehub_device_send(self, st_req->data, st_req->len, error)) { g_prefix_error(error, "failed to send tablehub firmware version request: " "please reboot the device: "); return FALSE; } if (!fu_logitech_rallysystem_tablehub_device_recv( self, buf, sizeof(buf), FU_LOGITECH_RALLYSYSTEM_TABLEHUB_DEVICE_IOCTL_TIMEOUT, error)) { g_prefix_error(error, "failed to get response for tablehub firmware " "version request: please reboot the device: "); return FALSE; } st_res = fu_struct_usb_read_version_response_parse(buf, sizeof(buf), 0x0, error); if (st_res == NULL) return FALSE; fw_version = fu_struct_usb_read_version_response_get_fw_version(st_res); fu_device_set_version(FU_DEVICE(self), fw_version); /* success! */ return TRUE; } static void fu_logitech_rallysystem_tablehub_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 55, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 45, "reload"); } static void fu_logitech_rallysystem_tablehub_device_init(FuLogitechRallysystemTablehubDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "com.logitech.vc.rallysystem"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_set_install_duration(FU_DEVICE(self), 5 * 60); fu_device_set_remove_delay(FU_DEVICE(self), 5 * 60 * 1000); /* wait for subcomponent */ } static void fu_logitech_rallysystem_tablehub_device_class_init(FuLogitechRallysystemTablehubDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_logitech_rallysystem_tablehub_device_to_string; device_class->write_firmware = fu_logitech_rallysystem_tablehub_device_write_firmware; device_class->probe = fu_logitech_rallysystem_tablehub_device_probe; device_class->setup = fu_logitech_rallysystem_tablehub_device_setup; device_class->set_progress = fu_logitech_rallysystem_tablehub_device_set_progress; } fwupd-2.0.10/plugins/logitech-rallysystem/fu-logitech-rallysystem-tablehub-device.h000066400000000000000000000007261501337203100305520ustar00rootroot00000000000000/* * Copyright 1999-2023 Logitech, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_LOGITECH_RALLYSYSTEM_TABLEHUB_DEVICE \ fu_logitech_rallysystem_tablehub_device_get_type() G_DECLARE_FINAL_TYPE(FuLogitechRallysystemTablehubDevice, fu_logitech_rallysystem_tablehub_device, FU, LOGITECH_RALLYSYSTEM_TABLEHUB_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/logitech-rallysystem/fu-logitech-rallysystem.rs000066400000000000000000000033301501337203100257200ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(Parse)] #[repr(C, packed)] struct FuStructAudioSerialNumber { mac_address: [u8; 6], pid: u16le, year: u16le, month: u8, day: u8, } #[repr(u16le)] enum FuUsbCmdId { Init = 0xCC01, FirmwareDownload = 0xCC03, ReadVersion = 0xCC07, } #[repr(u16le)] enum FuUsbCmdStatus { Ok = 0x0, // inferred Req = 0xFFFF, InitReq = 0xBEEF, InitReqAck = 0x0999, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructUsbInitRequest { id: FuUsbCmdId == Init, status: FuUsbCmdStatus == InitReq, len: u32le == 0, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructUsbInitResponse { id: FuUsbCmdId == Init, status: FuUsbCmdStatus == InitReqAck, len: u32le == 0, // inferred } #[derive(New, Default)] #[repr(C, packed)] struct FuStructUsbFirmwareDownloadRequest { id: FuUsbCmdId == FirmwareDownload, status: FuUsbCmdStatus == Req, len: u32le, fw_version: [char; 16], } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructUsbFirmwareDownloadResponse { id: FuUsbCmdId == FirmwareDownload, status: FuUsbCmdStatus == Ok, len: u32le, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructUsbReadVersionRequest { id: FuUsbCmdId == ReadVersion, status: FuUsbCmdStatus == Req, len: u32le == 0, } #[repr(u32le)] enum FuUsbImageState { New = 0x0, Valid = 0x1, Invalid = 0x2, } #[derive(Parse)] #[repr(C, packed)] struct FuStructUsbReadVersionResponse { fw_version: [char; 16], img_state: FuUsbImageState, } #[derive(Parse)] #[repr(C, packed)] struct FuStructUsbProgressResponse { completed: u32le, } fwupd-2.0.10/plugins/logitech-rallysystem/logitech-rallysystem.quirk000066400000000000000000000004541501337203100260230ustar00rootroot00000000000000# Logitech Rally System Audio, resides in TVHub [HIDRAW\VEN_046D&DEV_0885] Plugin = logitech_rallysystem GType = FuLogitechRallysystemAudioDevice # Logitech Rally System TableHub [USB\VID_046D&PID_088F] Plugin = logitech_rallysystem GType = FuLogitechRallysystemTablehubDevice Flags = signed-paylod fwupd-2.0.10/plugins/logitech-rallysystem/meson.build000066400000000000000000000011261501337203100227210ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginLogitechRallysystem"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('logitech-rallysystem.quirk') plugin_builtins += static_library('fu_plugin_logitech_rallysystem', rustgen.process('fu-logitech-rallysystem.rs'), sources: [ 'fu-logitech-rallysystem-plugin.c', 'fu-logitech-rallysystem-audio-device.c', 'fu-logitech-rallysystem-tablehub-device.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/logitech-scribe/000077500000000000000000000000001501337203100174365ustar00rootroot00000000000000fwupd-2.0.10/plugins/logitech-scribe/README.md000066400000000000000000000024011501337203100207120ustar00rootroot00000000000000--- title: Plugin: Logitech Scribe --- ## Introduction This plugin can upgrade the firmware on Logitech Video Collaboration products (Scribe), using USB bulk transfer. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a packed binary file format. This plugin supports the following protocol ID: * `com.logitech.vc.scribe` ## GUID Generation These devices use the standard UDEV DeviceInstanceId values, e.g. * `VIDEO4LINUX\VEN_046D&DEV_08E2` ## Quirk Use This plugin uses the following plugin-specific quirks: ## Update Behavior The peripheral firmware is deployed when the device is in normal runtime mode, and the device will reset when the new firmware has been written. ## Design Notes ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x046D` ## External Interface Access This plugin requires read access to `/dev/bus/usb`, `/dev/video0`. This plugin requires the `UVCIOC_CTRL_QUERY` ioctl interface. ## Version Considerations This plugin has been available since fwupd version `1.8.8`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Sanjay Sheth: @vcdmp fwupd-2.0.10/plugins/logitech-scribe/fu-logitech-scribe-device.c000066400000000000000000000475361501337203100245310ustar00rootroot00000000000000/* * Copyright 1999-2022 Logitech, Inc. * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include #include #include "fu-logitech-scribe-device.h" #include "fu-logitech-scribe-struct.h" /* UPD interface follows TLV (Type, Length, Value) protocol */ /* Payload size limited to 8k for UPD interfaces */ #define UPD_PACKET_HEADER_SIZE (2 * sizeof(guint32)) #define HASH_TIMEOUT 1500 #define MAX_DATA_SIZE 8192 /* 8k */ #define PAYLOAD_SIZE MAX_DATA_SIZE - UPD_PACKET_HEADER_SIZE #define UPD_INTERFACE_SUBPROTOCOL_ID 101 #define BULK_TRANSFER_TIMEOUT 1000 #define HASH_VALUE_SIZE 16 #define LENGTH_OFFSET 0x4 #define COMMAND_OFFSET 0x0 #define MAX_RETRIES 5 #define MAX_HANDSHAKE_RETRIES 3 #define MAX_WAIT_COUNT 150 #define SESSION_TIMEOUT 1000 #define FU_LOGITECH_SCRIBE_CHECKSUM_KIND_MD5 2 #define FU_LOGITECH_SCRIBE_VERSION_SIZE 1024 /* max size of version data returned */ #define FU_LOGITECH_SCRIBE_PROTOCOL_ID 0x1 enum { EP_OUT, EP_IN, EP_LAST }; enum { BULK_INTERFACE_UPD }; #define FU_LOGITECH_SCRIBE_DEVICE_IOCTL_TIMEOUT 5000 /* ms */ /* 2 byte for get len query */ #define kDefaultUvcGetLenQueryControlSize 2 const guchar kLogiCameraVersionSelector = 1; const guchar kLogiUvcXuDevInfoCsEepromVersion = 3; const guint kLogiVideoImageVersionMaxSize = 32; const guchar kLogiVideoAitInitiateSetMMPData = 1; const guchar kLogiVideoAitFinalizeSetMMPData = 1; const guchar kLogiUnitIdAccessMmp = 6; const guchar kLogiUvcXuAitCustomCsSetMmp = 4; const guchar kLogiUvcXuAitCustomCsGetMmpResult = 5; const guchar kLogiUnitIdPeripheralControl = 11; const guchar kLogiUnitIdCameraVersion = 8; const guchar kLogiAitSetMmpCmdFwBurning = 1; struct _FuLogitechScribeDevice { FuV4lDevice parent_instance; guint update_ep[EP_LAST]; guint update_iface; }; G_DEFINE_TYPE(FuLogitechScribeDevice, fu_logitech_scribe_device, FU_TYPE_V4L_DEVICE) static gboolean fu_logitech_scribe_device_send(FuLogitechScribeDevice *self, FuUsbDevice *usb_device, GByteArray *buf, gint interface_id, GError **error) { gsize transferred = 0; gint ep; GCancellable *cancellable = NULL; g_return_val_if_fail(buf != NULL, FALSE); if (interface_id == BULK_INTERFACE_UPD) { ep = self->update_ep[EP_OUT]; } else { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "interface is invalid"); return FALSE; } if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(usb_device), ep, (guint8 *)buf->data, buf->len, &transferred, BULK_TRANSFER_TIMEOUT, cancellable, error)) { g_prefix_error(error, "failed to send using bulk transfer: "); return FALSE; } return TRUE; } static gboolean fu_logitech_scribe_device_recv(FuLogitechScribeDevice *self, FuUsbDevice *usb_device, GByteArray *buf, gint interface_id, guint timeout, GError **error) { gsize received_length = 0; gint ep; g_return_val_if_fail(buf != NULL, FALSE); if (interface_id == BULK_INTERFACE_UPD) { ep = self->update_ep[EP_IN]; } else { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "interface is invalid"); return FALSE; } if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(usb_device), ep, buf->data, buf->len, &received_length, timeout, NULL, error)) { g_prefix_error(error, "failed to receive: "); return FALSE; } return TRUE; } static gboolean fu_logitech_scribe_device_send_upd_cmd(FuLogitechScribeDevice *self, FuUsbDevice *usb_device, guint32 cmd, GByteArray *buf, GError **error) { guint32 cmd_tmp = 0x0; guint timeout = BULK_TRANSFER_TIMEOUT; g_autoptr(GByteArray) buf_pkt = g_byte_array_new(); g_autoptr(GByteArray) buf_ack = g_byte_array_new(); fu_byte_array_append_uint32(buf_pkt, cmd, G_LITTLE_ENDIAN); /* Type(T) : Command type */ fu_byte_array_append_uint32(buf_pkt, buf != NULL ? buf->len : 0, G_LITTLE_ENDIAN); /*Length(L) : Length of payload */ if (buf != NULL) { g_byte_array_append(buf_pkt, buf->data, buf->len); /* Value(V) : Actual payload data */ } if (!fu_logitech_scribe_device_send(self, usb_device, buf_pkt, BULK_INTERFACE_UPD, error)) return FALSE; /* receiving INIT ACK */ fu_byte_array_set_size(buf_ack, MAX_DATA_SIZE, 0x00); /* extending the bulk transfer timeout value, as android device takes some time to calculate Hash and respond */ if (FU_LOGITECH_SCRIBE_USB_CMD_END_TRANSFER == cmd) timeout = HASH_TIMEOUT; if (!fu_logitech_scribe_device_recv(self, usb_device, buf_ack, BULK_INTERFACE_UPD, timeout, error)) return FALSE; if (!fu_memread_uint32_safe(buf_ack->data, buf_ack->len, COMMAND_OFFSET, &cmd_tmp, G_LITTLE_ENDIAN, error)) return FALSE; if (cmd_tmp != FU_LOGITECH_SCRIBE_USB_CMD_ACK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not FU_LOGITECH_SCRIBE_USB_CMD_ACK, got %x", cmd); return FALSE; } if (!fu_memread_uint32_safe(buf_ack->data, buf_ack->len, UPD_PACKET_HEADER_SIZE, &cmd_tmp, G_LITTLE_ENDIAN, error)) return FALSE; if (cmd_tmp != cmd) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid upd message received, expected %x, got %x", cmd, cmd_tmp); return FALSE; } return TRUE; } static gboolean fu_logitech_scribe_device_compute_hash_cb(const guint8 *buf, gsize bufsz, gpointer user_data, GError **error) { GChecksum *checksum = (GChecksum *)user_data; g_checksum_update(checksum, buf, bufsz); return TRUE; } static gchar * fu_logitech_scribe_device_compute_hash(GInputStream *stream, GError **error) { guint8 md5buf[HASH_VALUE_SIZE] = {0}; gsize data_len = sizeof(md5buf); g_autoptr(GChecksum) checksum = g_checksum_new(G_CHECKSUM_MD5); if (!fu_input_stream_chunkify(stream, fu_logitech_scribe_device_compute_hash_cb, checksum, error)) return NULL; g_checksum_get_digest(checksum, (guint8 *)&md5buf, &data_len); return g_base64_encode(md5buf, sizeof(md5buf)); } static gboolean fu_logitech_scribe_device_ioctl_buffer_cb(FuIoctl *self, gpointer ptr, guint8 *buf, gsize bufsz, GError **error) { struct uvc_xu_control_query *query = (struct uvc_xu_control_query *)ptr; query->data = buf; query->size = bufsz; return TRUE; } static gboolean fu_logitech_scribe_device_query_data_size(FuLogitechScribeDevice *self, guchar unit_id, guchar control_selector, guint16 *data_size, GError **error) { guint8 buf[kDefaultUvcGetLenQueryControlSize] = {0x0}; struct uvc_xu_control_query query = { .unit = unit_id, .selector = control_selector, .query = UVC_GET_LEN, }; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); /* include these when generating the emulation event */ fu_ioctl_add_key_as_u16(ioctl, "Request", UVCIOC_CTRL_QUERY); fu_ioctl_add_key_as_u8(ioctl, "Unit", query.unit); fu_ioctl_add_key_as_u8(ioctl, "Selector", query.selector); fu_ioctl_add_key_as_u8(ioctl, "Query", query.query); fu_ioctl_add_mutable_buffer(ioctl, NULL, buf, sizeof(buf), fu_logitech_scribe_device_ioctl_buffer_cb); if (!fu_ioctl_execute(ioctl, UVCIOC_CTRL_QUERY, (guint8 *)&query, sizeof(query), NULL, FU_LOGITECH_SCRIBE_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_NONE, error)) return FALSE; /* convert the data byte to int */ *data_size = buf[1] << 8 | buf[0]; g_debug("data size query response, size: %u unit: 0x%x selector: 0x%x", *data_size, (guchar)unit_id, (guchar)control_selector); fu_dump_raw(G_LOG_DOMAIN, "UVC_GET_LEN", buf, kDefaultUvcGetLenQueryControlSize); /* success */ return TRUE; } static gboolean fu_logitech_scribe_device_get_xu_control(FuLogitechScribeDevice *self, guchar unit_id, guchar control_selector, guint8 *buf, guint16 bufsz, GError **error) { struct uvc_xu_control_query query = { .unit = unit_id, .selector = control_selector, .query = UVC_GET_CUR, }; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); /* include these when generating the emulation event */ fu_ioctl_add_key_as_u16(ioctl, "Request", UVCIOC_CTRL_QUERY); fu_ioctl_add_key_as_u8(ioctl, "Unit", query.unit); fu_ioctl_add_key_as_u8(ioctl, "Selector", query.selector); fu_ioctl_add_key_as_u8(ioctl, "Query", query.query); fu_ioctl_add_mutable_buffer(ioctl, NULL, buf, bufsz, fu_logitech_scribe_device_ioctl_buffer_cb); if (!fu_ioctl_execute(ioctl, UVCIOC_CTRL_QUERY, (guint8 *)&query, sizeof(query), NULL, FU_LOGITECH_SCRIBE_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_NONE, error)) return FALSE; g_debug("received get xu control response, size: %u unit: 0x%x selector: 0x%x", bufsz, (guchar)unit_id, (guchar)control_selector); fu_dump_raw(G_LOG_DOMAIN, "UVC_GET_CUR", buf, bufsz); /* success */ return TRUE; } static void fu_logitech_scribe_device_to_string(FuDevice *device, guint idt, GString *str) { FuLogitechScribeDevice *self = FU_LOGITECH_SCRIBE_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "UpdateIface", self->update_iface); fwupd_codec_string_append_hex(str, idt, "UpdateEpOut", self->update_ep[EP_OUT]); fwupd_codec_string_append_hex(str, idt, "UpdateEpIn", self->update_ep[EP_IN]); } static gboolean fu_logitech_scribe_device_probe(FuDevice *device, GError **error) { /* interested in lowest index only e,g, video0, ignore low format siblings like * video1/video2/video3 etc */ if (fu_v4l_device_get_index(FU_V4L_DEVICE(device)) != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "only device with lower index supported"); return FALSE; }; /* success */ return TRUE; } static gboolean fu_logitech_scribe_device_send_upd_init_cmd_cb(FuDevice *device, gpointer user_data, GError **error) { FuLogitechScribeDevice *self = FU_LOGITECH_SCRIBE_DEVICE(device); return fu_logitech_scribe_device_send_upd_cmd(self, user_data, FU_LOGITECH_SCRIBE_USB_CMD_INIT, NULL, error); } static gboolean fu_logitech_scribe_device_write_fw(FuLogitechScribeDevice *self, FuUsbDevice *usb_device, GInputStream *stream, FuProgress *progress, GError **error) { g_autoptr(FuChunkArray) chunks = NULL; chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, PAYLOAD_SIZE, error); if (chunks == NULL) return FALSE; fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GByteArray) data_pkt = g_byte_array_new(); /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; g_byte_array_append(data_pkt, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); if (!fu_logitech_scribe_device_send_upd_cmd( self, usb_device, FU_LOGITECH_SCRIBE_USB_CMD_DATA_TRANSFER, data_pkt, error)) { g_prefix_error(error, "failed to send data packet 0x%x: ", i); return FALSE; } fu_progress_step_done(progress); } return TRUE; } static gboolean fu_logitech_scribe_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuLogitechScribeDevice *self = FU_LOGITECH_SCRIBE_DEVICE(device); gsize streamsz = 0; g_autofree gchar *base64hash = NULL; g_autoptr(GByteArray) end_pkt = g_byte_array_new(); g_autoptr(GByteArray) start_pkt = g_byte_array_new(); g_autoptr(GInputStream) stream = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(FuUsbInterface) intf = NULL; g_autoptr(GPtrArray) endpoints = NULL; g_autoptr(FuUsbDevice) usb_device = NULL; g_autoptr(FuDeviceLocker) locker = NULL; /* get USB parent */ usb_device = FU_USB_DEVICE( fu_device_get_backend_parent_with_subsystem(device, "usb:usb_device", error)); if (usb_device == NULL) return FALSE; /* re-open with new device set */ locker = fu_device_locker_new(usb_device, error); if (locker == NULL) return FALSE; /* find the correct interface */ intf = fu_usb_device_get_interface(FU_USB_DEVICE(usb_device), FU_USB_CLASS_VENDOR_SPECIFIC, UPD_INTERFACE_SUBPROTOCOL_ID, FU_LOGITECH_SCRIBE_PROTOCOL_ID, error); if (intf == NULL) return FALSE; endpoints = fu_usb_interface_get_endpoints(intf); if (endpoints == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to get usb device endpoints"); return FALSE; } self->update_iface = fu_usb_interface_get_number(intf); for (guint j = 0; j < endpoints->len; j++) { FuUsbEndpoint *ep = g_ptr_array_index(endpoints, j); if (j == EP_OUT) self->update_ep[EP_OUT] = fu_usb_endpoint_get_address(ep); else self->update_ep[EP_IN] = fu_usb_endpoint_get_address(ep); } fu_usb_device_add_interface(usb_device, self->update_iface); g_debug("usb data, iface: %u ep_out: %u ep_in: %u", self->update_iface, self->update_ep[EP_OUT], self->update_ep[EP_IN]); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "init"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "start-transfer"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, "device-write-blocks"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "end-transfer"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 5, "uninit"); /* get default image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* sending INIT. Retry if device is not in IDLE state to receive the file */ if (!fu_device_retry(device, fu_logitech_scribe_device_send_upd_init_cmd_cb, MAX_RETRIES, usb_device, error)) { g_prefix_error(error, "failed to write init transfer packet: please reboot the device: "); return FALSE; } fu_progress_step_done(progress); /* start transfer */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; fu_byte_array_append_uint64(start_pkt, streamsz, G_LITTLE_ENDIAN); if (!fu_logitech_scribe_device_send_upd_cmd(self, usb_device, FU_LOGITECH_SCRIBE_USB_CMD_START_TRANSFER, start_pkt, error)) { g_prefix_error(error, "failed to write start transfer packet: "); return FALSE; } fu_progress_step_done(progress); /* push each block to device */ if (!fu_logitech_scribe_device_write_fw(self, usb_device, stream, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* end transfer */ base64hash = fu_logitech_scribe_device_compute_hash(stream, error); if (base64hash == NULL) return FALSE; fu_byte_array_append_uint32(end_pkt, 1, G_LITTLE_ENDIAN); /* update */ fu_byte_array_append_uint32(end_pkt, 0, G_LITTLE_ENDIAN); /* force */ fu_byte_array_append_uint32(end_pkt, FU_LOGITECH_SCRIBE_CHECKSUM_KIND_MD5, G_LITTLE_ENDIAN); /* checksum type */ g_byte_array_append(end_pkt, (const guint8 *)base64hash, strlen(base64hash)); if (!fu_logitech_scribe_device_send_upd_cmd(self, usb_device, FU_LOGITECH_SCRIBE_USB_CMD_END_TRANSFER, end_pkt, error)) { g_prefix_error(error, "failed to write end transfer packet: "); return FALSE; } fu_progress_step_done(progress); /* uninitialize */ /* no need to wait for ACK message, perhaps device reboot already in progress, ignore */ if (!fu_logitech_scribe_device_send_upd_cmd(self, usb_device, FU_LOGITECH_SCRIBE_USB_CMD_UNINIT, NULL, &error_local)) { g_debug( "failed to receive acknowledgment for uninitialize request, ignoring it: %s", error_local->message); } fu_progress_step_done(progress); /* * image file pushed. Device validates and uploads new image on inactive partition. Reboots * wait for RemoveDelay duration */ /* success! */ return TRUE; } static gboolean fu_logitech_scribe_device_ensure_version(FuLogitechScribeDevice *self, GError **error) { guint32 fwversion = 0; guint16 data_len = 0; g_autofree guint8 *query_data = NULL; /* query current device version */ if (!fu_logitech_scribe_device_query_data_size(self, kLogiUnitIdCameraVersion, kLogiCameraVersionSelector, &data_len, error)) return FALSE; if (data_len > FU_LOGITECH_SCRIBE_VERSION_SIZE) { g_prefix_error(error, "version packet was too large at 0x%x bytes: ", data_len); return FALSE; } query_data = g_malloc0(data_len); if (!fu_logitech_scribe_device_get_xu_control(self, kLogiUnitIdCameraVersion, kLogiCameraVersionSelector, query_data, data_len, error)) return FALSE; /* little-endian data. MinorVersion byte 0, MajorVersion byte 1, BuildVersion byte 3 & 2 */ fwversion = (query_data[1] << 24) + (query_data[0] << 16) + (query_data[3] << 8) + query_data[2]; fu_device_set_version_raw(FU_DEVICE(self), fwversion); /* success */ return TRUE; } static gboolean fu_logitech_scribe_device_setup(FuDevice *device, GError **error) { FuLogitechScribeDevice *self = FU_LOGITECH_SCRIBE_DEVICE(device); /* FuV4lDevice->setup */ if (!FU_DEVICE_CLASS(fu_logitech_scribe_device_parent_class)->setup(device, error)) return FALSE; /* only interested in video capture device */ if ((fu_v4l_device_get_caps(FU_V4L_DEVICE(device)) & FU_V4L_CAP_VIDEO_CAPTURE) == 0) { g_autofree gchar *caps = fu_v4l_cap_to_string(fu_v4l_device_get_caps(FU_V4L_DEVICE(self))); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "only video capture device are supported, got %s", caps); return FALSE; } return fu_logitech_scribe_device_ensure_version(self, error); } static void fu_logitech_scribe_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static gchar * fu_logitech_scribe_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_device_get_version_format(device)); } static void fu_logitech_scribe_device_init(FuLogitechScribeDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "com.logitech.vc.scribe"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_retry_set_delay(FU_DEVICE(self), 1000); } static void fu_logitech_scribe_device_class_init(FuLogitechScribeDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_logitech_scribe_device_to_string; device_class->write_firmware = fu_logitech_scribe_device_write_firmware; device_class->probe = fu_logitech_scribe_device_probe; device_class->setup = fu_logitech_scribe_device_setup; device_class->set_progress = fu_logitech_scribe_device_set_progress; device_class->convert_version = fu_logitech_scribe_device_convert_version; } fwupd-2.0.10/plugins/logitech-scribe/fu-logitech-scribe-device.h000066400000000000000000000005631501337203100245230ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_LOGITECH_SCRIBE_DEVICE (fu_logitech_scribe_device_get_type()) G_DECLARE_FINAL_TYPE(FuLogitechScribeDevice, fu_logitech_scribe_device, FU, LOGITECH_SCRIBE_DEVICE, FuV4lDevice) fwupd-2.0.10/plugins/logitech-scribe/fu-logitech-scribe-plugin.c000066400000000000000000000015331501337203100245530ustar00rootroot00000000000000/* * Copyright 1999-2022 Logitech, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-logitech-scribe-device.h" #include "fu-logitech-scribe-plugin.h" struct _FuLogitechScribePlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuLogitechScribePlugin, fu_logitech_scribe_plugin, FU_TYPE_PLUGIN) static void fu_logitech_scribe_plugin_init(FuLogitechScribePlugin *self) { } static void fu_logitech_scribe_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_udev_subsystem(plugin, "video4linux"); fu_plugin_add_device_gtype(plugin, FU_TYPE_LOGITECH_SCRIBE_DEVICE); } static void fu_logitech_scribe_plugin_class_init(FuLogitechScribePluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_logitech_scribe_plugin_constructed; } fwupd-2.0.10/plugins/logitech-scribe/fu-logitech-scribe-plugin.h000066400000000000000000000004421501337203100245560ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuLogitechScribePlugin, fu_logitech_scribe_plugin, FU, LOGITECH_SCRIBE_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/logitech-scribe/fu-logitech-scribe.rs000066400000000000000000000007561501337203100234670ustar00rootroot00000000000000// Copyright 2024 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later enum FuLogitechScribeUsbCmd { CheckBuffersize = 0xCC00, Init = 0xCC01, StartTransfer = 0xCC02, DataTransfer = 0xCC03, EndTransfer = 0xCC04, Uninit = 0xCC05, BufferRead = 0xCC06, BufferWrite = 0xCC07, UninitBuffer = 0xCC08, Ack = 0xFF01, Timeout = 0xFF02, Nack = 0xFF03, } fwupd-2.0.10/plugins/logitech-scribe/logitech-scribe.quirk000066400000000000000000000001441501337203100235550ustar00rootroot00000000000000[VIDEO4LINUX\VEN_046D&DEV_08E2] Plugin = logitech_scribe InstallDuration = 120 RemoveDelay = 120000 fwupd-2.0.10/plugins/logitech-scribe/meson.build000066400000000000000000000010011501337203100215700ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginLogitechScribe"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('logitech-scribe.quirk') plugin_builtins += static_library('fu_plugin_logitech_scribe', rustgen.process('fu-logitech-scribe.rs'), sources: [ 'fu-logitech-scribe-device.c', 'fu-logitech-scribe-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/logitech-tap/000077500000000000000000000000001501337203100167535ustar00rootroot00000000000000fwupd-2.0.10/plugins/logitech-tap/README.md000066400000000000000000000026731501337203100202420ustar00rootroot00000000000000--- title: Plugin: Logitech Tap — Video Collaboration --- ## Introduction This plugin can upgrade the firmware on Logitech Video Collaboration products (Tap), using ioctl. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a packed binary file format. This plugin supports the following protocol ID: * `com.logitech.hardware.tap` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `HIDRAW\VEN_046D&DEV_0872` * `VIDEO4LINUX\VEN_046D&DEV_0876` ## Quirk Use This plugin uses the following plugin-specific quirks: ### `Flags=sensor-needs-reboot` Firmware updated for HDMI component, trigger composite device reboot. ## Update Behavior The peripheral firmware is deployed when the device is in normal runtime mode, and the device will reset when the new firmware has been written. ## Design Notes ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x046D` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. This plugin requires the ioctl interfaces: `UVCIOC_CTRL_QUERY`, `HIDIOCGFEATURE`, `HIDIOCSFEATURE`, `HIDIOCGINPUT`. ## Version Considerations This plugin has been available since fwupd version `1.9.2`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Sanjay Sheth: @vcdmp fwupd-2.0.10/plugins/logitech-tap/fu-logitech-tap-hdmi-device.c000066400000000000000000000407261501337203100242740ustar00rootroot00000000000000/* * Copyright 1999-2023 Logitech, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include #include #include "fu-logitech-tap-hdmi-device.h" #define FU_LOGITECH_TAP_HDMI_DEVICE_IOCTL_TIMEOUT 5000 /* ms */ #define XU_INPUT_DATA_LEN 8 /* 2 byte for get len query */ #define kDefaultUvcGetLenQueryControlSize 2 const guint8 kLogiTapCameraVersionSelector = 1; const guint8 kLogiTapUvcXuAitCustomCsGetMmpResult = 5; const guint8 kLogiTapHdmiVerSetData = 0x0B; const guint8 kLogiUnitIdVidCapExtension = 0x06; const guint8 kLogiHdmiVerGetSelector = 2; const guint8 kLogiTapAitSetMmpCmdFwBurning = 0x01; const guint8 kLogiTapVideoAitInitiateSetMMPData = 1; const guint kLogiDefaultImageBlockSize = 32; const guint8 kLogiUvcXuAitCustomCsSetFwData = 0x03; const guint8 kLogiTapUvcXuAitCustomCsSetMmp = 4; const guint kLogiDefaultAitSleepIntervalMs = 1000; /* when finalize Ait, max polling duration is 120sec */ const guint kLogiDefaultAitFinalizeMaxPollingDurationMs = 120000; const guint8 kLogiDefaultAitSuccessValue = 0x00; const guint8 kLogiDefaultAitFailureValue = 0x82; struct _FuLogitechTapHdmiDevice { FuV4lDevice parent_instance; }; G_DEFINE_TYPE(FuLogitechTapHdmiDevice, fu_logitech_tap_hdmi_device, FU_TYPE_V4L_DEVICE) static gboolean fu_logitech_tap_hdmi_device_ioctl_buffer_cb(FuIoctl *self, gpointer ptr, guint8 *buf, gsize bufsz, GError **error) { struct uvc_xu_control_query *query = (struct uvc_xu_control_query *)ptr; query->data = buf; query->size = bufsz; return TRUE; } static gboolean fu_logitech_tap_hdmi_device_query_data_size(FuLogitechTapHdmiDevice *self, guint8 unit_id, guint8 control_selector, guint16 *data_size, GError **error) { guint8 buf[kDefaultUvcGetLenQueryControlSize] = {0x0}; struct uvc_xu_control_query query = { .unit = unit_id, .selector = control_selector, .query = UVC_GET_LEN, }; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); g_debug("data size query request, unit: 0x%x selector: 0x%x", unit_id, control_selector); /* include these when generating the emulation event */ fu_ioctl_add_key_as_u16(ioctl, "Request", UVCIOC_CTRL_QUERY); fu_ioctl_add_key_as_u8(ioctl, "Unit", query.unit); fu_ioctl_add_key_as_u8(ioctl, "Selector", query.selector); fu_ioctl_add_key_as_u8(ioctl, "Query", query.query); fu_ioctl_add_mutable_buffer(ioctl, NULL, buf, sizeof(buf), fu_logitech_tap_hdmi_device_ioctl_buffer_cb); if (!fu_ioctl_execute(ioctl, UVCIOC_CTRL_QUERY, (guint8 *)&query, sizeof(query), NULL, FU_LOGITECH_TAP_HDMI_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_RETRY, error)) return FALSE; /* convert the data byte to int */ if (!fu_memread_uint16_safe(buf, sizeof(buf), 0x0, data_size, G_LITTLE_ENDIAN, error)) return FALSE; g_debug("data size query response, size: %u unit: 0x%x selector: 0x%x", *data_size, unit_id, control_selector); fu_dump_raw(G_LOG_DOMAIN, "UVC_GET_LENRes", query.data, kDefaultUvcGetLenQueryControlSize); /* success */ return TRUE; } static gboolean fu_logitech_tap_hdmi_device_get_xu_control(FuLogitechTapHdmiDevice *self, guint8 unit_id, guint8 control_selector, guint8 *buf, gsize bufsz, GError **error) { struct uvc_xu_control_query query = { .unit = unit_id, .selector = control_selector, .query = UVC_GET_CUR, }; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); /* include these when generating the emulation event */ fu_ioctl_add_key_as_u16(ioctl, "Request", UVCIOC_CTRL_QUERY); fu_ioctl_add_key_as_u8(ioctl, "Unit", query.unit); fu_ioctl_add_key_as_u8(ioctl, "Selector", query.selector); fu_ioctl_add_key_as_u8(ioctl, "Query", query.query); fu_ioctl_add_mutable_buffer(ioctl, NULL, buf, bufsz, fu_logitech_tap_hdmi_device_ioctl_buffer_cb); if (!fu_ioctl_execute(ioctl, UVCIOC_CTRL_QUERY, (guint8 *)&query, sizeof(query), NULL, FU_LOGITECH_TAP_HDMI_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_RETRY, error)) return FALSE; g_debug("received get xu control response, size: %u unit: 0x%x selector: 0x%x", query.size, unit_id, control_selector); fu_dump_raw(G_LOG_DOMAIN, "UVC_GET_CURRes", query.data, query.size); /* success */ return TRUE; } static gboolean fu_logitech_tap_hdmi_device_set_xu_control(FuLogitechTapHdmiDevice *self, guint8 unit_id, guint8 control_selector, guint8 *buf, gsize bufsz, GError **error) { struct uvc_xu_control_query query = { .unit = unit_id, .selector = control_selector, .query = UVC_SET_CUR, }; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); /* include these when generating the emulation event */ fu_ioctl_add_key_as_u16(ioctl, "Request", UVCIOC_CTRL_QUERY); fu_ioctl_add_key_as_u8(ioctl, "Unit", query.unit); fu_ioctl_add_key_as_u8(ioctl, "Selector", query.selector); fu_ioctl_add_key_as_u8(ioctl, "Query", query.query); fu_ioctl_add_mutable_buffer(ioctl, NULL, buf, bufsz, fu_logitech_tap_hdmi_device_ioctl_buffer_cb); if (!fu_ioctl_execute(ioctl, UVCIOC_CTRL_QUERY, (guint8 *)&query, sizeof(query), NULL, FU_LOGITECH_TAP_HDMI_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_RETRY, error)) return FALSE; g_debug("received set xu control response, size: %u unit: 0x%x selector: 0x%x", (guint)bufsz, unit_id, control_selector); /* success */ return TRUE; } static gboolean fu_logitech_tap_hdmi_device_ait_initiate_update(FuLogitechTapHdmiDevice *self, GError **error) { guint16 data_len = 0; g_autofree guint8 *mmp_get_data = NULL; guint8 ait_initiate_update[XU_INPUT_DATA_LEN] = {kLogiTapAitSetMmpCmdFwBurning, 0, 0, kLogiTapVideoAitInitiateSetMMPData, 0, 0, 0, 0}; if (!fu_logitech_tap_hdmi_device_set_xu_control(self, kLogiUnitIdVidCapExtension, kLogiTapUvcXuAitCustomCsSetMmp, ait_initiate_update, sizeof(ait_initiate_update), error)) return FALSE; if (!fu_logitech_tap_hdmi_device_query_data_size(self, kLogiUnitIdVidCapExtension, kLogiTapUvcXuAitCustomCsGetMmpResult, &data_len, error)) return FALSE; if (data_len > XU_INPUT_DATA_LEN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "initiate query packet was too large at 0x%x bytes: ", data_len); return FALSE; } mmp_get_data = g_malloc0(data_len); if (!fu_logitech_tap_hdmi_device_get_xu_control(self, kLogiUnitIdVidCapExtension, kLogiTapUvcXuAitCustomCsGetMmpResult, (guint8 *)mmp_get_data, data_len, error)) return FALSE; if (mmp_get_data[0] != kLogiDefaultAitSuccessValue) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to initialize AIT update, invalid result data: 0x%x", mmp_get_data[0]); return FALSE; } /* success */ return TRUE; } static gboolean fu_logitech_tap_hdmi_device_ait_finalize_update(FuLogitechTapHdmiDevice *self, GError **error) { guint duration_ms = 0; guint8 ait_finalize_update[XU_INPUT_DATA_LEN] = {kLogiTapAitSetMmpCmdFwBurning, kLogiTapVideoAitInitiateSetMMPData, 0, 0, 0, 0, 0, 0}; fu_device_sleep(FU_DEVICE(self), 4 * kLogiDefaultAitSleepIntervalMs); /* 4 sec */ if (!fu_logitech_tap_hdmi_device_set_xu_control(self, kLogiUnitIdVidCapExtension, kLogiTapUvcXuAitCustomCsSetMmp, ait_finalize_update, sizeof(ait_finalize_update), error)) return FALSE; fu_device_sleep(FU_DEVICE(self), kLogiDefaultAitSleepIntervalMs); /* 1 sec */ /* poll for burning fw result or return failure if it hits max polling */ for (int pass = 0;; pass++) { g_autofree guint8 *mmp_get_data = NULL; guint16 data_len = 0; fu_device_sleep(FU_DEVICE(self), kLogiDefaultAitSleepIntervalMs); /* 1 sec */ duration_ms = duration_ms + kLogiDefaultAitSleepIntervalMs; if (!fu_logitech_tap_hdmi_device_query_data_size( self, kLogiUnitIdVidCapExtension, kLogiTapUvcXuAitCustomCsGetMmpResult, &data_len, error)) return FALSE; mmp_get_data = g_malloc0(data_len); if (!fu_logitech_tap_hdmi_device_get_xu_control( self, kLogiUnitIdVidCapExtension, kLogiTapUvcXuAitCustomCsGetMmpResult, (guint8 *)mmp_get_data, data_len, error)) return FALSE; if (mmp_get_data[0] == kLogiDefaultAitSuccessValue) { if (pass == 0) fu_device_sleep(FU_DEVICE(self), 8 * 1000); break; } else if (mmp_get_data[0] == kLogiDefaultAitFailureValue) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to finalize image burning, invalid result data: 0x%x", mmp_get_data[0]); return FALSE; } if (duration_ms > kLogiDefaultAitFinalizeMaxPollingDurationMs) { /* if device never returns 0x82 or 0x00, bail out */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to finalize image burning, duration_ms: %u", duration_ms); return FALSE; } } /* success */ return TRUE; } static gboolean fu_logitech_tap_hdmi_device_write_fw(FuLogitechTapHdmiDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { /* init */ if (!fu_logitech_tap_hdmi_device_ait_initiate_update(self, error)) return FALSE; /* write */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* if needed, pad the last block to kLogiDefaultImageBlockSize size, * so that device always gets each block of kLogiDefaultImageBlockSize */ g_autofree guint8 *data_pkt = g_malloc0(kLogiDefaultImageBlockSize); /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_memcpy_safe(data_pkt, kLogiDefaultImageBlockSize, 0x0, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0x0, fu_chunk_get_data_sz(chk), error)) return FALSE; if (!fu_logitech_tap_hdmi_device_set_xu_control(self, kLogiUnitIdVidCapExtension, kLogiUvcXuAitCustomCsSetFwData, (guint8 *)data_pkt, kLogiDefaultImageBlockSize, error)) return FALSE; fu_progress_step_done(progress); } /* uninit */ if (!fu_logitech_tap_hdmi_device_ait_finalize_update(self, error)) return FALSE; /* signal for sensor device to trigger composite device reboot */ fu_device_add_private_flag(FU_DEVICE(self), FU_LOGITECH_TAP_HDMI_DEVICE_FLAG_SENSOR_NEEDS_REBOOT); return TRUE; } static gboolean fu_logitech_tap_hdmi_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuLogitechTapHdmiDevice *self = FU_LOGITECH_TAP_HDMI_DEVICE(device); g_autofree gchar *old_firmware_version = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* for troubleshooting purpose */ old_firmware_version = g_strdup(fu_device_get_version(device)); g_debug("update %s firmware", old_firmware_version); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); /* get image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* write */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, kLogiDefaultImageBlockSize, error); if (chunks == NULL) return FALSE; if (!fu_logitech_tap_hdmi_device_write_fw(self, chunks, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_logitech_tap_hdmi_device_ensure_version(FuLogitechTapHdmiDevice *self, GError **error) { guint16 bufsz = 0; guint8 set_data[XU_INPUT_DATA_LEN] = {kLogiTapHdmiVerSetData, 0, 0, 0, 0, 0, 0, 0}; guint16 build = 0; guint16 minor = 0; guint16 major = 0; g_autofree guint8 *buf = NULL; if (!fu_logitech_tap_hdmi_device_set_xu_control(self, kLogiUnitIdVidCapExtension, kLogiTapCameraVersionSelector, set_data, sizeof(set_data), error)) return FALSE; /* query current device version */ if (!fu_logitech_tap_hdmi_device_query_data_size(self, kLogiUnitIdVidCapExtension, kLogiHdmiVerGetSelector, &bufsz, error)) return FALSE; if (bufsz > XU_INPUT_DATA_LEN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "version query packet was too large at 0x%x bytes: ", bufsz); return FALSE; } buf = g_malloc0(bufsz); if (!fu_logitech_tap_hdmi_device_get_xu_control(self, kLogiUnitIdVidCapExtension, kLogiHdmiVerGetSelector, buf, bufsz, error)) return FALSE; /* MajorVersion bytes 3&2, MinorVersion bytes 5&4, BuildVersion bytes 7&6 */ if (!fu_memread_uint16_safe(buf, bufsz, 0x2, &major, G_BIG_ENDIAN, error)) return FALSE; if (!fu_memread_uint16_safe(buf, bufsz, 0x4, &minor, G_BIG_ENDIAN, error)) return FALSE; if (!fu_memread_uint16_safe(buf, bufsz, 0x6, &build, G_BIG_ENDIAN, error)) return FALSE; fu_device_set_version(FU_DEVICE(self), g_strdup_printf("%i.%i.%i", major, minor, build)); /* success */ return TRUE; } static gboolean fu_logitech_tap_hdmi_device_setup(FuDevice *device, GError **error) { FuLogitechTapHdmiDevice *self = FU_LOGITECH_TAP_HDMI_DEVICE(device); /* FuV4lDevice->setup */ if (!FU_DEVICE_CLASS(fu_logitech_tap_hdmi_device_parent_class)->setup(device, error)) return FALSE; /* only interested in video capture device */ if ((fu_v4l_device_get_caps(FU_V4L_DEVICE(self)) & FU_V4L_CAP_VIDEO_CAPTURE) == 0) { g_autofree gchar *caps = fu_v4l_cap_to_string(fu_v4l_device_get_caps(FU_V4L_DEVICE(self))); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "only video capture device are supported, got %s", caps); return FALSE; } return fu_logitech_tap_hdmi_device_ensure_version(self, error); } static gboolean fu_logitech_tap_hdmi_device_probe(FuDevice *device, GError **error) { /* interested in lowest index only e,g, video0, ignore low format siblings like * video1/video2/video3 etc */ if (fu_v4l_device_get_index(FU_V4L_DEVICE(device)) != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "only device with lower index supported"); return FALSE; }; /* success */ return TRUE; } static void fu_logitech_tap_hdmi_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_logitech_tap_hdmi_device_init(FuLogitechTapHdmiDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "com.logitech.hardware.tap"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_retry_set_delay(FU_DEVICE(self), 1000); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_NONBLOCK); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_register_private_flag(FU_DEVICE(self), FU_LOGITECH_TAP_HDMI_DEVICE_FLAG_SENSOR_NEEDS_REBOOT); } static void fu_logitech_tap_hdmi_device_class_init(FuLogitechTapHdmiDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_logitech_tap_hdmi_device_probe; device_class->setup = fu_logitech_tap_hdmi_device_setup; device_class->set_progress = fu_logitech_tap_hdmi_device_set_progress; device_class->write_firmware = fu_logitech_tap_hdmi_device_write_firmware; } fwupd-2.0.10/plugins/logitech-tap/fu-logitech-tap-hdmi-device.h000066400000000000000000000006771501337203100243020ustar00rootroot00000000000000/* * Copyright 1999-2023 Logitech, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_LOGITECH_TAP_HDMI_DEVICE (fu_logitech_tap_hdmi_device_get_type()) G_DECLARE_FINAL_TYPE(FuLogitechTapHdmiDevice, fu_logitech_tap_hdmi_device, FU, LOGITECH_TAP_HDMI_DEVICE, FuV4lDevice) #define FU_LOGITECH_TAP_HDMI_DEVICE_FLAG_SENSOR_NEEDS_REBOOT "sensor-needs-reboot" fwupd-2.0.10/plugins/logitech-tap/fu-logitech-tap-plugin.c000066400000000000000000000065361501337203100234150ustar00rootroot00000000000000/* * Copyright 1999-2023 Logitech, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-logitech-tap-hdmi-device.h" #include "fu-logitech-tap-plugin.h" #include "fu-logitech-tap-sensor-device.h" #include "fu-logitech-tap-touch-device.h" struct _FuLogitechTapPlugin { FuPlugin parent_instance; FuDevice *hdmi_device; /* ref */ FuDevice *sensor_device; /* ref */ FuDevice *touch_device; /* ref */ }; G_DEFINE_TYPE(FuLogitechTapPlugin, fu_logitech_tap_plugin, FU_TYPE_PLUGIN) static gboolean fu_logitech_tap_plugin_composite_cleanup(FuPlugin *plugin, GPtrArray *devices, GError **error) { FuLogitechTapPlugin *self = FU_LOGITECH_TAP_PLUGIN(plugin); /* check if HDMI firmware successfully upgraded and signal for SENSOR to trigger composite * reboot is set */ for (guint i = 0; i < devices->len; i++) { FuDevice *dev = g_ptr_array_index(devices, i); if ((g_strcmp0(fu_device_get_plugin(dev), "logitech_tap") == 0) && (FU_IS_LOGITECH_TAP_HDMI_DEVICE(dev)) && (fu_device_has_private_flag( dev, FU_LOGITECH_TAP_HDMI_DEVICE_FLAG_SENSOR_NEEDS_REBOOT)) && self->hdmi_device != NULL) { g_debug("device needs reboot"); if (!fu_logitech_tap_sensor_device_reboot_device( FU_LOGITECH_TAP_SENSOR_DEVICE(fu_device_get_proxy(dev)), error)) return FALSE; fu_device_add_flag(dev, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); break; } } return TRUE; } static void fu_logitech_tap_plugin_init(FuLogitechTapPlugin *self) { } static void fu_logitech_tap_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_udev_subsystem(plugin, "video4linux"); fu_plugin_add_udev_subsystem(plugin, "hidraw"); fu_plugin_add_device_gtype(plugin, FU_TYPE_LOGITECH_TAP_HDMI_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_LOGITECH_TAP_SENSOR_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_LOGITECH_TAP_TOUCH_DEVICE); } static void fu_logitech_tap_plugin_device_registered(FuPlugin *plugin, FuDevice *device) { FuLogitechTapPlugin *self = FU_LOGITECH_TAP_PLUGIN(plugin); if (g_strcmp0(fu_device_get_plugin(device), "logitech_tap") != 0) return; if (FU_IS_LOGITECH_TAP_HDMI_DEVICE(device)) { g_set_object(&self->hdmi_device, device); if (self->sensor_device != NULL) fu_device_set_proxy(self->hdmi_device, self->sensor_device); } if (FU_IS_LOGITECH_TAP_SENSOR_DEVICE(device)) { g_set_object(&self->sensor_device, device); if (self->hdmi_device != NULL) fu_device_set_proxy(self->hdmi_device, self->sensor_device); } } static void fu_logitech_tap_plugin_finalize(GObject *obj) { FuLogitechTapPlugin *self = FU_LOGITECH_TAP_PLUGIN(obj); if (self->hdmi_device != NULL) g_object_unref(self->hdmi_device); if (self->sensor_device != NULL) g_object_unref(self->sensor_device); if (self->touch_device != NULL) g_object_unref(self->touch_device); G_OBJECT_CLASS(fu_logitech_tap_plugin_parent_class)->finalize(obj); } static void fu_logitech_tap_plugin_class_init(FuLogitechTapPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_logitech_tap_plugin_finalize; plugin_class->constructed = fu_logitech_tap_plugin_constructed; plugin_class->device_registered = fu_logitech_tap_plugin_device_registered; plugin_class->composite_cleanup = fu_logitech_tap_plugin_composite_cleanup; } fwupd-2.0.10/plugins/logitech-tap/fu-logitech-tap-plugin.h000066400000000000000000000003541501337203100234120ustar00rootroot00000000000000/* * Copyright 1999-2023 Logitech, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuLogitechTapPlugin, fu_logitech_tap_plugin, FU, LOGITECH_TAP_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/logitech-tap/fu-logitech-tap-sensor-device.c000066400000000000000000000262261501337203100246630ustar00rootroot00000000000000/* * Copyright 2023 Logitech, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include #ifdef HAVE_IOCTL_H #include #endif #include #include "fu-logitech-tap-sensor-device.h" #include "fu-logitech-tap-struct.h" #define FU_LOGITECH_TAP_SENSOR_DEVICE_IOCTL_TIMEOUT 50000 /* ms */ #ifndef HIDIOCGINPUT #define HIDIOCGINPUT(len) _IOC(_IOC_READ, 'H', 0x0A, len) #endif const guint kLogiDefaultSensorSleepIntervalMs = 50; struct _FuLogitechTapSensorDevice { FuHidrawDevice parent_instance; }; G_DEFINE_TYPE(FuLogitechTapSensorDevice, fu_logitech_tap_sensor_device, FU_TYPE_HIDRAW_DEVICE) static gboolean fu_logitech_tap_sensor_device_get_feature(FuLogitechTapSensorDevice *self, guint8 *data, guint datasz, GError **error) { g_autoptr(GError) error_local = NULL; /* try HIDIOCGINPUT request in case of failure */ if (!fu_hidraw_device_get_feature(FU_HIDRAW_DEVICE(self), data, datasz, FU_IOCTL_FLAG_RETRY, &error_local)) { g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); g_debug("failed to send get request, retrying: %s", error_local->message); if (!fu_ioctl_execute(ioctl, HIDIOCGINPUT(datasz), data, datasz, NULL, FU_LOGITECH_TAP_SENSOR_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_RETRY, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_logitech_tap_sensor_device_enable_tde(FuDevice *device, GError **error) { FuLogitechTapSensorDevice *self = FU_LOGITECH_TAP_SENSOR_DEVICE(device); g_autoptr(FuStructLogitechTapSensorHidReq) st = fu_struct_logitech_tap_sensor_hid_req_new(); fu_struct_logitech_tap_sensor_hid_req_set_cmd(st, FU_LOGITECH_TAP_SENSOR_HID_SET_CMD_TDE); fu_struct_logitech_tap_sensor_hid_req_set_payload_byte1( st, FU_LOGITECH_TAP_SENSOR_HID_TDE_MODE_SELECTOR); fu_struct_logitech_tap_sensor_hid_req_set_payload_byte2( st, FU_LOGITECH_TAP_SENSOR_HID_TDE_MODE_ENABLE); return fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), st->data, st->len, FU_IOCTL_FLAG_RETRY, error); } static gboolean fu_logitech_tap_sensor_device_disable_tde(FuDevice *device, GError **error) { FuLogitechTapSensorDevice *self = FU_LOGITECH_TAP_SENSOR_DEVICE(device); g_autoptr(FuStructLogitechTapSensorHidReq) st = fu_struct_logitech_tap_sensor_hid_req_new(); fu_struct_logitech_tap_sensor_hid_req_set_cmd(st, FU_LOGITECH_TAP_SENSOR_HID_SET_CMD_TDE); fu_struct_logitech_tap_sensor_hid_req_set_payload_byte1( st, FU_LOGITECH_TAP_SENSOR_HID_TDE_MODE_SELECTOR); fu_struct_logitech_tap_sensor_hid_req_set_payload_byte2( st, FU_LOGITECH_TAP_SENSOR_HID_TDE_MODE_DISABLE); return fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), st->data, st->len, FU_IOCTL_FLAG_RETRY, error); } gboolean fu_logitech_tap_sensor_device_reboot_device(FuLogitechTapSensorDevice *self, GError **error) { g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FuStructLogitechTapSensorHidReq) st = fu_struct_logitech_tap_sensor_hid_req_new(); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 100, "attach"); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_RESTART); /* need to reopen the device, as at composite_cleanup time, device is already closed */ if (!fu_device_open(FU_DEVICE(self), error)) return FALSE; /* enable/disable TDE mode */ locker = fu_device_locker_new_full(FU_DEVICE(self), (FuDeviceLockerFunc)fu_logitech_tap_sensor_device_enable_tde, (FuDeviceLockerFunc)fu_logitech_tap_sensor_device_disable_tde, error); if (locker == NULL) return FALSE; /* setup HID report for power cycle */ fu_struct_logitech_tap_sensor_hid_req_set_cmd(st, FU_LOGITECH_TAP_SENSOR_HID_SET_CMD_REBOOT); fu_struct_logitech_tap_sensor_hid_req_set_payload_byte1( st, FU_LOGITECH_TAP_SENSOR_HID_REBOOT_PIN_CLR); fu_struct_logitech_tap_sensor_hid_req_set_payload_byte2( st, FU_LOGITECH_TAP_SENSOR_HID_REBOOT_PWR); if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), st->data, st->len, FU_IOCTL_FLAG_RETRY, error)) return FALSE; fu_struct_logitech_tap_sensor_hid_req_set_payload_byte2( st, FU_LOGITECH_TAP_SENSOR_HID_REBOOT_RST); if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), st->data, st->len, FU_IOCTL_FLAG_RETRY, error)) return FALSE; fu_device_sleep(FU_DEVICE(self), 2000); /* 2 sec */ fu_struct_logitech_tap_sensor_hid_req_set_payload_byte1( st, FU_LOGITECH_TAP_SENSOR_HID_REBOOT_PIN_SET); fu_struct_logitech_tap_sensor_hid_req_set_payload_byte2( st, FU_LOGITECH_TAP_SENSOR_HID_REBOOT_PWR); if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), st->data, st->len, FU_IOCTL_FLAG_RETRY, error)) return FALSE; fu_device_sleep(FU_DEVICE(self), 2000); /* 2 sec */ fu_struct_logitech_tap_sensor_hid_req_set_payload_byte2( st, FU_LOGITECH_TAP_SENSOR_HID_REBOOT_RST); if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), st->data, st->len, FU_IOCTL_FLAG_RETRY, error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_logitech_tap_sensor_device_set_version(FuLogitechTapSensorDevice *self, GError **error) { guint32 version = 0; g_autoptr(FuStructLogitechTapSensorHidReq) st_req = fu_struct_logitech_tap_sensor_hid_req_new(); g_autoptr(FuStructLogitechTapSensorHidRes) st_res = fu_struct_logitech_tap_sensor_hid_res_new(); fu_struct_logitech_tap_sensor_hid_req_set_cmd(st_req, FU_LOGITECH_TAP_SENSOR_HID_SET_CMD_VERSION); fu_struct_logitech_tap_sensor_hid_req_set_payload_byte1( st_req, FU_LOGITECH_TAP_SENSOR_HID_COLOSSUS_APP_GET_VERSION); fu_struct_logitech_tap_sensor_hid_res_set_cmd(st_res, FU_LOGITECH_TAP_SENSOR_HID_GET_CMD_VERSION); /* setup HID report to query current device version */ if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), st_req->data, st_req->len, FU_IOCTL_FLAG_RETRY, error)) return FALSE; if (!fu_logitech_tap_sensor_device_get_feature(self, (guint8 *)st_res->data, st_res->len, error)) return FALSE; /* MinorVersion byte 3, MajorVersion byte 4, BuildVersion byte 2 & 1 */ if (!fu_memread_uint32_safe((guint8 *)st_res->data, st_res->len, 0x01, &version, G_LITTLE_ENDIAN, error)) return FALSE; fu_device_set_version_raw(FU_DEVICE(self), version); /* success */ return TRUE; } static gboolean fu_logitech_tap_sensor_device_set_serial(FuLogitechTapSensorDevice *self, GError **error) { g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GString) serial_number = g_string_new(NULL); g_autoptr(FuStructLogitechTapSensorHidReq) st_req = fu_struct_logitech_tap_sensor_hid_req_new(); fu_struct_logitech_tap_sensor_hid_req_set_cmd( st_req, FU_LOGITECH_TAP_SENSOR_HID_SET_CMD_SERIAL_NUMBER); fu_struct_logitech_tap_sensor_hid_req_set_payload_byte1( st_req, FU_LOGITECH_TAP_SENSOR_HID_SERIAL_NUMBER_SET_REPORT_BYTE1); fu_struct_logitech_tap_sensor_hid_req_set_payload_byte2( st_req, FU_LOGITECH_TAP_SENSOR_HID_SERIAL_NUMBER_SET_REPORT_BYTE2); fu_struct_logitech_tap_sensor_hid_req_set_payload_byte3( st_req, FU_LOGITECH_TAP_SENSOR_HID_SERIAL_NUMBER_SET_REPORT_BYTE3); fu_struct_logitech_tap_sensor_hid_req_set_payload_byte4( st_req, FU_LOGITECH_TAP_SENSOR_HID_SERIAL_NUMBER_SET_REPORT_BYTE4); /* enable/disable TDE mode */ locker = fu_device_locker_new_full(FU_DEVICE(self), (FuDeviceLockerFunc)fu_logitech_tap_sensor_device_enable_tde, (FuDeviceLockerFunc)fu_logitech_tap_sensor_device_disable_tde, error); if (locker == NULL) return FALSE; /* setup HID report for serial number */ if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), st_req->data, st_req->len, FU_IOCTL_FLAG_RETRY, error)) return FALSE; fu_device_sleep(FU_DEVICE(self), kLogiDefaultSensorSleepIntervalMs); /* 50 ms */ /* serial number is a 12-byte-string that is stored in MCU */ /* each get request fetches 1 word (4 bytes), so iterate 3 times */ for (int index = 1; index <= 3; index++) { g_autoptr(FuStructLogitechTapSensorHidRes) st_res = fu_struct_logitech_tap_sensor_hid_res_new(); fu_struct_logitech_tap_sensor_hid_res_set_cmd( st_res, FU_LOGITECH_TAP_SENSOR_HID_GET_CMD_SERIAL_NUMBER); if (!fu_logitech_tap_sensor_device_get_feature(self, (guint8 *)st_res->data, st_res->len, error)) return FALSE; g_string_append_printf(serial_number, "%c%c%c%c", st_res->data[1], st_res->data[2], st_res->data[3], st_res->data[4]); } fu_device_set_serial(FU_DEVICE(self), serial_number->str); /* success */ return TRUE; } static gboolean fu_logitech_tap_sensor_device_setup(FuDevice *device, GError **error) { FuLogitechTapSensorDevice *self = FU_LOGITECH_TAP_SENSOR_DEVICE(device); if (!fu_logitech_tap_sensor_device_set_version(self, error)) return FALSE; if (!fu_logitech_tap_sensor_device_set_serial(self, error)) return FALSE; return TRUE; } static void fu_logitech_tap_sensor_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 0, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 100, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static gchar * fu_logitech_tap_sensor_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_device_get_version_format(device)); } static void fu_logitech_tap_sensor_device_init(FuLogitechTapSensorDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "com.logitech.hardware.tap"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_retry_set_delay(FU_DEVICE(self), 1000); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_NONBLOCK); } static void fu_logitech_tap_sensor_device_class_init(FuLogitechTapSensorDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_logitech_tap_sensor_device_setup; device_class->set_progress = fu_logitech_tap_sensor_device_set_progress; device_class->convert_version = fu_logitech_tap_sensor_device_convert_version; } fwupd-2.0.10/plugins/logitech-tap/fu-logitech-tap-sensor-device.h000066400000000000000000000007321501337203100246620ustar00rootroot00000000000000/* * Copyright 2023 Logitech, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_LOGITECH_TAP_SENSOR_DEVICE (fu_logitech_tap_sensor_device_get_type()) G_DECLARE_FINAL_TYPE(FuLogitechTapSensorDevice, fu_logitech_tap_sensor_device, FU, LOGITECH_TAP_SENSOR_DEVICE, FuHidrawDevice) gboolean fu_logitech_tap_sensor_device_reboot_device(FuLogitechTapSensorDevice *self, GError **error); fwupd-2.0.10/plugins/logitech-tap/fu-logitech-tap-touch-common.h000066400000000000000000000003101501337203100245140ustar00rootroot00000000000000/* * Copyright 2024 Logitech, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define FU_LOGITECH_TAP_TOUCH_IC_NAME 0x2511 #define FU_LOGITECH_TAP_TOUCH_SUPPORTED_PROTOCOL_VERSION 0x03 fwupd-2.0.10/plugins/logitech-tap/fu-logitech-tap-touch-device.c000066400000000000000000000716151501337203100244760ustar00rootroot00000000000000/* * Copyright 2024 Logitech, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include #ifdef HAVE_IOCTL_H #include #endif #include #include "fu-logitech-tap-struct.h" #include "fu-logitech-tap-touch-common.h" #include "fu-logitech-tap-touch-device.h" #include "fu-logitech-tap-touch-firmware.h" #define FU_LOGITECH_TAP_TOUCH_IOCTL_TIMEOUT 5000 /* ms */ #define FU_LOGITECH_TAP_TOUCH_HID_SET_DATA_LEN 64 #define FU_LOGITECH_TAP_TOUCH_HID_GET_DATA_LEN 64 #define FU_LOGITECH_TAP_TOUCH_HID_RESPONSE_OFFSET \ 4 /* Skip first 4 header bytes from response buffer */ #define FU_LOGITECH_TAP_TOUCH_HID_REPORT_ID 0x03 #define FU_LOGITECH_TAP_TOUCH_TRANSFER_BLOCK_SIZE 32 #define FU_LOGITECH_TAP_TOUCH_AP_MODE 0x5A /* device in Application mode */ #define FU_LOGITECH_TAP_TOUCH_BL_MODE 0x55 /* device in Bootloader mode */ #define FU_LOGITECH_TAP_TOUCH_MAX_GET_RETRY_COUNT 50 #define FU_LOGITECH_TAP_TOUCH_MAX_BUSY_CHECK_RETRY_COUNT 50 #define FU_LOGITECH_TAP_TOUCH_MAX_FW_WRITE_RETRIES 3 #define FU_LOGITECH_TAP_TOUCH_SYSTEM_READY 0x50 /* wait and retry if device not ready */ /* usb bus type */ #define FU_LOGITECH_TAP_TOUCH_DEVICE_INFO_BUS_TYPE 0x03 struct _FuLogitechTapTouchDevice { FuHidrawDevice parent_instance; }; G_DEFINE_TYPE(FuLogitechTapTouchDevice, fu_logitech_tap_touch_device, FU_TYPE_HIDRAW_DEVICE) static gboolean fu_logitech_tap_touch_device_get_feature_cb(FuDevice *device, gpointer user_data, GError **error) { FuLogitechTapTouchDevice *self = FU_LOGITECH_TAP_TOUCH_DEVICE(device); GByteArray *outbuffer = (GByteArray *)user_data; gboolean ret; guint8 report_id = 0; g_autoptr(GError) error_local = NULL; ret = fu_udev_device_pread(FU_UDEV_DEVICE(self), 0x0, (guint8 *)outbuffer->data, outbuffer->len, &error_local); if (!fu_memread_uint8_safe((guint8 *)outbuffer->data, outbuffer->len, 0x00U, &report_id, error)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to read report id: "); return FALSE; } if (!ret) { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL) || report_id != FU_LOGITECH_TAP_TOUCH_HID_REPORT_ID) { g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to read report: "); return FALSE; } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to read report: "); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "HidGetFeatureResponse", outbuffer->data, outbuffer->len); /* success */ return TRUE; } static gboolean fu_logitech_tap_touch_device_hid_transfer(FuLogitechTapTouchDevice *self, GByteArray *st_req, guint delay, GByteArray *buf_res, GError **error) { fu_byte_array_set_size(st_req, FU_LOGITECH_TAP_TOUCH_HID_SET_DATA_LEN, 0x0); if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), st_req->data, st_req->len, FU_IOCTL_FLAG_RETRY, error)) { g_prefix_error(error, "failed to send packet to touch panel: "); return FALSE; } /* check if there is a corresponding get report request. * If so, wait for specified duration before submitting get report */ if (buf_res != NULL) { fu_byte_array_set_size(buf_res, FU_LOGITECH_TAP_TOUCH_HID_GET_DATA_LEN, 0x0); fu_device_sleep(FU_DEVICE(self), delay); if (!fu_device_retry_full(FU_DEVICE(self), fu_logitech_tap_touch_device_get_feature_cb, FU_LOGITECH_TAP_TOUCH_MAX_GET_RETRY_COUNT, delay, buf_res, error)) { g_prefix_error(error, "failed to receive packet from touch panel: "); return FALSE; } } /* success */ return TRUE; } static gboolean fu_logitech_tap_touch_device_enable_tde(FuDevice *device, GError **error) { FuLogitechTapTouchDevice *self = FU_LOGITECH_TAP_TOUCH_DEVICE(device); g_autoptr(FuStructLogitechTapTouchHidReq) st = fu_struct_logitech_tap_touch_hid_req_new(); /* hid report to put device into suspend mode */ fu_struct_logitech_tap_touch_hid_req_set_payload_len(st, 0x02); fu_struct_logitech_tap_touch_hid_req_set_response_len(st, 0x00); fu_struct_logitech_tap_touch_hid_req_set_cmd( st, FU_STRUCT_LOGITECH_TAP_TOUCH_HID_CMD_SET_TDE_TEST_MODE); fu_byte_array_append_uint8(st, 0x01); return fu_logitech_tap_touch_device_hid_transfer(self, st, 0, NULL, error); } static gboolean fu_logitech_tap_touch_device_disable_tde(FuDevice *device, GError **error) { FuLogitechTapTouchDevice *self = FU_LOGITECH_TAP_TOUCH_DEVICE(device); g_autoptr(FuStructLogitechTapTouchHidReq) st = fu_struct_logitech_tap_touch_hid_req_new(); fu_struct_logitech_tap_touch_hid_req_set_payload_len(st, 0x02); fu_struct_logitech_tap_touch_hid_req_set_response_len(st, 0x0); fu_struct_logitech_tap_touch_hid_req_set_cmd( st, FU_STRUCT_LOGITECH_TAP_TOUCH_HID_CMD_SET_TDE_TEST_MODE); fu_byte_array_append_uint8(st, 0x00); return fu_logitech_tap_touch_device_hid_transfer(self, st, 0, NULL, error); } static gboolean fu_logitech_tap_touch_device_write_enable(FuLogitechTapTouchDevice *self, gboolean in_ap, gboolean write_ap, guint32 end, guint32 checksum, GError **error) { guint8 delay; g_autoptr(FuStructLogitechTapTouchHidReq) st = fu_struct_logitech_tap_touch_hid_req_new(); if (in_ap) { fu_struct_logitech_tap_touch_hid_req_set_payload_len(st, 0x03); delay = 100; } else { fu_struct_logitech_tap_touch_hid_req_set_payload_len(st, 0x0A); delay = 10; } fu_struct_logitech_tap_touch_hid_req_set_response_len(st, 0x0); fu_struct_logitech_tap_touch_hid_req_set_cmd( st, FU_STRUCT_LOGITECH_TAP_TOUCH_HID_CMD_WRITE_ENABLE); fu_byte_array_append_uint8(st, 0x5A); fu_byte_array_append_uint8(st, 0xA5); if (end > 0) { fu_byte_array_append_uint8(st, write_ap ? 0x00 : 0x01); fu_byte_array_append_uint24(st, end, G_BIG_ENDIAN); fu_byte_array_append_uint24(st, checksum, G_BIG_ENDIAN); } /* hid report to enable writing */ if (!fu_logitech_tap_touch_device_hid_transfer(self, st, 0, NULL, error)) return FALSE; /* mode switch delay for application/bootloader */ fu_device_sleep(FU_DEVICE(self), delay); /* success */ return TRUE; } static gboolean fu_logitech_tap_touch_device_check_busy_cb(FuDevice *device, gpointer user_data, GError **error) { FuLogitechTapTouchDevice *self = FU_LOGITECH_TAP_TOUCH_DEVICE(device); guint8 hid_response = 0; g_autoptr(GByteArray) buf_res = g_byte_array_new(); g_autoptr(FuStructLogitechTapTouchHidReq) st = fu_struct_logitech_tap_touch_hid_req_new(); /* hid report to query device busy or idle status */ fu_struct_logitech_tap_touch_hid_req_set_payload_len(st, 0x01); fu_struct_logitech_tap_touch_hid_req_set_response_len(st, 0x01); fu_struct_logitech_tap_touch_hid_req_set_cmd( st, FU_STRUCT_LOGITECH_TAP_TOUCH_HID_CMD_GET_SYS_BUSY_STATUS); if (!fu_logitech_tap_touch_device_hid_transfer(self, st, 5, buf_res, error)) return FALSE; if (!fu_memread_uint8_safe(buf_res->data, buf_res->len, FU_LOGITECH_TAP_TOUCH_HID_RESPONSE_OFFSET, &hid_response, error)) return FALSE; if (hid_response != FU_LOGITECH_TAP_TOUCH_SYSTEM_READY) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "device busy check failed, got: 0x%02x, expected: %i", hid_response, FU_LOGITECH_TAP_TOUCH_SYSTEM_READY); return FALSE; } /* success */ return TRUE; } static gboolean fu_logitech_tap_touch_device_get_crc(FuLogitechTapTouchDevice *self, guint16 *crc, guint8 datasz, GError **error) { g_autoptr(FuStructLogitechTapTouchHidReq) st = fu_struct_logitech_tap_touch_hid_req_new(); fu_struct_logitech_tap_touch_hid_req_set_payload_len(st, 0x01); if (crc != NULL) fu_struct_logitech_tap_touch_hid_req_set_response_len(st, datasz); else fu_struct_logitech_tap_touch_hid_req_set_response_len(st, 0x0); fu_struct_logitech_tap_touch_hid_req_set_cmd( st, FU_STRUCT_LOGITECH_TAP_TOUCH_HID_CMD_GET_AP_CRC); /* hid report to query crc info of dataflash/pflash (DF/AP) block */ if (crc != NULL) { g_autoptr(GByteArray) buf_res = g_byte_array_new(); if (!fu_logitech_tap_touch_device_hid_transfer(self, st, 100, buf_res, error)) return FALSE; if (!fu_memread_uint16_safe(buf_res->data, buf_res->len, FU_LOGITECH_TAP_TOUCH_HID_RESPONSE_OFFSET, crc, G_LITTLE_ENDIAN, error)) return FALSE; } else { if (!fu_logitech_tap_touch_device_hid_transfer(self, st, 0, NULL, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_logitech_tap_touch_device_get_mcu_mode(FuLogitechTapTouchDevice *self, guint8 *mcu_mode, GError **error) { g_autoptr(GByteArray) buf_res = g_byte_array_new(); g_autoptr(FuStructLogitechTapTouchHidReq) st = fu_struct_logitech_tap_touch_hid_req_new(); fu_struct_logitech_tap_touch_hid_req_set_payload_len(st, 0x01); fu_struct_logitech_tap_touch_hid_req_set_response_len(st, 0x2); fu_struct_logitech_tap_touch_hid_req_set_cmd( st, FU_STRUCT_LOGITECH_TAP_TOUCH_HID_CMD_GET_MCU_MODE); /* hid report to query current mode, application (AP) or bootloader (BL) mode */ if (!fu_logitech_tap_touch_device_hid_transfer(self, st, 100, buf_res, error)) return FALSE; return fu_memread_uint8_safe(buf_res->data, buf_res->len, FU_LOGITECH_TAP_TOUCH_HID_RESPONSE_OFFSET, mcu_mode, error); } static gboolean fu_logitech_tap_touch_device_check_ic_name(FuLogitechTapTouchDevice *self, GError **error) { guint16 ic_name = 0; g_autoptr(FuStructLogitechTapTouchHidReq) st = fu_struct_logitech_tap_touch_hid_req_new(); g_autoptr(GByteArray) buf_res = g_byte_array_new(); fu_struct_logitech_tap_touch_hid_req_set_payload_len(st, 0x01); fu_struct_logitech_tap_touch_hid_req_set_response_len(st, 0x20); fu_struct_logitech_tap_touch_hid_req_set_cmd( st, FU_STRUCT_LOGITECH_TAP_TOUCH_HID_CMD_GET_MCU_VERSION); if (!fu_logitech_tap_touch_device_hid_transfer(self, st, 100, buf_res, error)) return FALSE; if (!fu_memread_uint16_safe(buf_res->data, buf_res->len, FU_LOGITECH_TAP_TOUCH_HID_RESPONSE_OFFSET, &ic_name, G_LITTLE_ENDIAN, error)) return FALSE; if (ic_name != FU_LOGITECH_TAP_TOUCH_IC_NAME) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to get supported ic: %x", ic_name); return FALSE; } /* success */ return TRUE; } static gboolean fu_logitech_tap_touch_device_ensure_version(FuLogitechTapTouchDevice *self, GError **error) { guint64 version_raw = 0; g_autoptr(FuStructLogitechTapTouchHidReq) st = fu_struct_logitech_tap_touch_hid_req_new(); g_autoptr(GByteArray) buf_res = g_byte_array_new(); /* * hid report to query version info * Firmware updater available/supported from only 2 display panel vendors. * All vendors use same VID/PID, only way to determine supported vendor is to analyze * version. Version is 8 bytes, and fifth byte determines supported or not. * * Currently only supported values are: 0x03 or 0x04. * Create unique GUID for each supported vendor to match 'provides' value in metainfo. */ fu_struct_logitech_tap_touch_hid_req_set_payload_len(st, 0x01); fu_struct_logitech_tap_touch_hid_req_set_response_len(st, 0x08); fu_struct_logitech_tap_touch_hid_req_set_cmd( st, FU_STRUCT_LOGITECH_TAP_TOUCH_HID_CMD_GET_FIRMWARE_VERSION); if (!fu_logitech_tap_touch_device_hid_transfer(self, st, 100, buf_res, error)) return FALSE; if (!fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { guint8 version_2511 = 0; if (!fu_memread_uint8_safe(buf_res->data, buf_res->len, 8, &version_2511, error)) return FALSE; if (version_2511 == 0x03) { fu_device_add_instance_str(FU_DEVICE(self), "2511", "TM"); } else if (version_2511 == 0x04) { fu_device_add_instance_str(FU_DEVICE(self), "2511", "SW"); } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to get supported vendor: %x", version_2511); return FALSE; } if (!fu_device_build_instance_id(FU_DEVICE(self), error, "HIDRAW", "VEN", "DEV", "2511", NULL)) return FALSE; } if (!fu_memread_uint64_safe(buf_res->data, buf_res->len, FU_LOGITECH_TAP_TOUCH_HID_RESPONSE_OFFSET, &version_raw, G_BIG_ENDIAN, error)) return FALSE; if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { fu_device_set_version_bootloader_raw(FU_DEVICE(self), version_raw); } else { fu_device_set_version_raw(FU_DEVICE(self), version_raw); } /* success */ return TRUE; } static gboolean fu_logitech_tap_touch_device_check_protocol(FuLogitechTapTouchDevice *self, GError **error) { guint8 protocol_version[3] = {0}; g_autoptr(FuStructLogitechTapTouchHidReq) st = fu_struct_logitech_tap_touch_hid_req_new(); g_autoptr(GByteArray) buf_res = g_byte_array_new(); /* * hid report to query device protocol info * in application mode only V3 (3.1.0) supported * in bootloader mode only 1.7.ff supported */ fu_struct_logitech_tap_touch_hid_req_set_payload_len(st, 0x01); fu_struct_logitech_tap_touch_hid_req_set_response_len(st, 0x03); fu_struct_logitech_tap_touch_hid_req_set_cmd( st, FU_STRUCT_LOGITECH_TAP_TOUCH_HID_CMD_GET_PROTOCOL_VERSION); if (!fu_logitech_tap_touch_device_hid_transfer(self, st, 100, buf_res, error)) return FALSE; if (!fu_memcpy_safe(protocol_version, sizeof(protocol_version), 0x0, /* dst */ buf_res->data, buf_res->len, FU_LOGITECH_TAP_TOUCH_HID_RESPONSE_OFFSET, /* src */ sizeof(protocol_version), error)) return FALSE; if ((protocol_version[0] != FU_LOGITECH_TAP_TOUCH_SUPPORTED_PROTOCOL_VERSION) && (!fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER))) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to get supported protocol ver: %x", protocol_version[0]); return FALSE; } g_debug("touch panel protocol version: %x.%x.%x", protocol_version[0], protocol_version[1], protocol_version[2]); /* success */ return TRUE; } static gboolean fu_logitech_tap_touch_device_setup(FuDevice *device, GError **error) { FuLogitechTapTouchDevice *self = FU_LOGITECH_TAP_TOUCH_DEVICE(device); struct hidraw_devinfo hid_raw_info = {0x0}; g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) g_debug("entering in BL MODE"); if (!fu_ioctl_execute(ioctl, HIDIOCGRAWINFO, (guint8 *)&hid_raw_info, sizeof(hid_raw_info), NULL, FU_LOGITECH_TAP_TOUCH_IOCTL_TIMEOUT, FU_IOCTL_FLAG_NONE, error)) return FALSE; if (hid_raw_info.bustype != FU_LOGITECH_TAP_TOUCH_DEVICE_INFO_BUS_TYPE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "incorrect bustype=0x%x, expected usb", hid_raw_info.bustype); return FALSE; } /* enable/disable TDE mode */ locker = fu_device_locker_new_full(FU_DEVICE(self), (FuDeviceLockerFunc)fu_logitech_tap_touch_device_enable_tde, (FuDeviceLockerFunc)fu_logitech_tap_touch_device_disable_tde, error); if (locker == NULL) return FALSE; /* wait 1 sec for suspend mode */ fu_device_sleep(FU_DEVICE(self), 1000); /* hid report to query MCU info, only FU_LOGITECH_TAP_TOUCH_IC_NAME supported */ if (!fu_logitech_tap_touch_device_check_protocol(self, error)) return FALSE; if (!fu_logitech_tap_touch_device_check_ic_name(self, error)) return FALSE; /* get version */ return fu_logitech_tap_touch_device_ensure_version(self, error); } static gboolean fu_logitech_tap_touch_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuLogitechTapTouchDevice *self = FU_LOGITECH_TAP_TOUCH_DEVICE(device); guint8 mcu_mode = 0; /* sanity check */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in bootloader mode, skipping"); return TRUE; } /* cannot use locker, device goes into bootloader mode here, looses connectivity */ if (!fu_logitech_tap_touch_device_enable_tde(device, error)) return FALSE; if (!fu_logitech_tap_touch_device_get_mcu_mode(self, &mcu_mode, error)) return FALSE; /* hid report to enable write and switch to bootloader (BL) mode */ if (mcu_mode == FU_LOGITECH_TAP_TOUCH_AP_MODE) { g_autoptr(FuStructLogitechTapTouchHidReq) st = fu_struct_logitech_tap_touch_hid_req_new(); if (!fu_logitech_tap_touch_device_write_enable(self, TRUE, FALSE, 0, 0, error)) return FALSE; fu_struct_logitech_tap_touch_hid_req_set_payload_len(st, 0x01); fu_struct_logitech_tap_touch_hid_req_set_response_len(st, 0x0); fu_struct_logitech_tap_touch_hid_req_set_cmd( st, FU_STRUCT_LOGITECH_TAP_TOUCH_HID_CMD_SET_BL_MODE); if (!fu_logitech_tap_touch_device_hid_transfer(self, st, 0, NULL, error)) return FALSE; } fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* success */ return TRUE; } static gboolean fu_logitech_tap_touch_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuLogitechTapTouchDevice *self = FU_LOGITECH_TAP_TOUCH_DEVICE(device); g_autoptr(FuStructLogitechTapTouchHidReq) st = fu_struct_logitech_tap_touch_hid_req_new(); /* sanity check */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in runtime mode, skipping"); return TRUE; } /* hid report to enable write and switch to application (AP) mode */ if (!fu_logitech_tap_touch_device_write_enable(self, FALSE, FALSE, 0, 0, error)) return FALSE; fu_struct_logitech_tap_touch_hid_req_set_payload_len(st, 0x01); fu_struct_logitech_tap_touch_hid_req_set_response_len(st, 0x0); fu_struct_logitech_tap_touch_hid_req_set_cmd( st, FU_STRUCT_LOGITECH_TAP_TOUCH_HID_CMD_SET_AP_MODE); if (!fu_logitech_tap_touch_device_hid_transfer(self, st, 0, NULL, error)) return FALSE; /* mode switch delay for application/bootloader */ fu_device_sleep(FU_DEVICE(self), 100); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* success */ return TRUE; } static gboolean fu_logitech_tap_touch_device_probe(FuDevice *device, GError **error) { /* FuUdevDevice->probe */ if (!FU_DEVICE_CLASS(fu_logitech_tap_touch_device_parent_class)->probe(device, error)) return FALSE; /* ignore unsupported subsystems */ if (g_strcmp0(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device)), "hidraw") != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "is not correct subsystem=%s, expected hidraw", fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device))); return FALSE; } /* set the physical ID */ return fu_udev_device_set_physical_id(FU_UDEV_DEVICE(device), "hid", error); } static gboolean fu_logitech_tap_touch_device_write_blocks(FuLogitechTapTouchDevice *self, FuFirmware *img, guint16 firmware_checksum, gboolean in_ap, FuProgress *progress, GError **error) { guint16 device_checksum = 0; g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(GInputStream) stream = NULL; stream = fu_firmware_get_stream(img, error); if (stream == NULL) return FALSE; chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_LOGITECH_TAP_TOUCH_TRANSFER_BLOCK_SIZE, error); if (chunks == NULL) return FALSE; /* progress */ g_debug("updating %s block. end:0x%x, checksum:0x%x", in_ap ? "AP" : "DF", (guint)fu_firmware_get_offset(img), firmware_checksum); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); /* hid report to enable write */ if (!fu_logitech_tap_touch_device_write_enable(self, FALSE, in_ap ? TRUE : FALSE, fu_firmware_get_offset(img), firmware_checksum, error)) return FALSE; fu_device_sleep(FU_DEVICE(self), 10); for (guint32 i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuStructLogitechTapTouchHidReq) st = fu_struct_logitech_tap_touch_hid_req_new(); g_autoptr(GByteArray) chunk_buf = g_byte_array_new(); g_autoptr(FuChunk) chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; /* ensure each packet is FU_LOGITECH_TAP_TOUCH_TRANSFER_BLOCK_SIZE bytes */ g_byte_array_append(chunk_buf, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); /* write packet */ fu_struct_logitech_tap_touch_hid_req_set_payload_len(st, 1 + chunk_buf->len); fu_struct_logitech_tap_touch_hid_req_set_response_len(st, 0x00); fu_struct_logitech_tap_touch_hid_req_set_cmd( st, FU_STRUCT_LOGITECH_TAP_TOUCH_HID_CMD_WRITE_DATA); g_byte_array_append(st, chunk_buf->data, chunk_buf->len); /* resize the last packet */ if ((i == (fu_chunk_array_length(chunks) - 1)) && (fu_chunk_get_data_sz(chk) < FU_LOGITECH_TAP_TOUCH_TRANSFER_BLOCK_SIZE)) fu_byte_array_set_size(st, 37, in_ap ? 0xFF : 0x0); if (!fu_logitech_tap_touch_device_hid_transfer(self, st, 0, NULL, error)) return FALSE; fu_device_sleep(FU_DEVICE(self), 2); if (!fu_device_retry_full(FU_DEVICE(self), fu_logitech_tap_touch_device_check_busy_cb, FU_LOGITECH_TAP_TOUCH_MAX_BUSY_CHECK_RETRY_COUNT, 5, NULL, error)) { g_prefix_error(error, "failed to get idle state for %s: ", in_ap ? "AP" : "DF"); return FALSE; } fu_progress_step_done(progress); } /* done with writing dataflash/pflash (DF/AP) block */ fu_device_sleep(FU_DEVICE(self), 50); /* verify crc */ if (!fu_logitech_tap_touch_device_get_crc(self, NULL, 0, error)) return FALSE; if (!fu_device_retry_full(FU_DEVICE(self), fu_logitech_tap_touch_device_check_busy_cb, FU_LOGITECH_TAP_TOUCH_MAX_BUSY_CHECK_RETRY_COUNT, 5, NULL, error)) { g_prefix_error(error, "failed to crc for %s, device busy", in_ap ? "AP" : "DF"); return FALSE; } if (!fu_logitech_tap_touch_device_get_crc(self, &device_checksum, 4, error)) return FALSE; if (device_checksum != firmware_checksum) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "crc check failed for %s, expected 0x%04x and got 0x%04x", in_ap ? "AP" : "DF", firmware_checksum, device_checksum); return FALSE; } g_info("device checksum for %s. checksum:0x%x", in_ap ? "AP" : "DF", device_checksum); /* success */ return TRUE; } typedef struct { FuFirmware *firmware; /* noref */ FuProgress *progress; /* noref */ } FuLogitechTapTouchWriteHelper; static gboolean fu_logitech_tap_touch_device_write_chunks_cb(FuDevice *device, gpointer user_data, GError **error) { FuLogitechTapTouchDevice *self = FU_LOGITECH_TAP_TOUCH_DEVICE(device); FuLogitechTapTouchWriteHelper *helper = (FuLogitechTapTouchWriteHelper *)user_data; guint16 ap_checksum; guint16 df_checksum; g_autoptr(FuStructLogitechTapTouchHidReq) st = fu_struct_logitech_tap_touch_hid_req_new(); g_autoptr(FuFirmware) ap_img = NULL; g_autoptr(FuFirmware) df_img = NULL; /* progress */ fu_progress_set_id(helper->progress, G_STRLOC); fu_progress_add_step(helper->progress, FWUPD_STATUS_DEVICE_ERASE, 3, "erase"); fu_progress_add_step(helper->progress, FWUPD_STATUS_DEVICE_WRITE, 3, "write-df-blocks"); fu_progress_add_step(helper->progress, FWUPD_STATUS_DEVICE_WRITE, 94, "write-ap-blocks"); ap_checksum = fu_logitech_tap_touch_firmware_get_ap_checksum( FU_LOGITECH_TAP_TOUCH_FIRMWARE(helper->firmware)); df_checksum = fu_logitech_tap_touch_firmware_get_df_checksum( FU_LOGITECH_TAP_TOUCH_FIRMWARE(helper->firmware)); /* get images */ ap_img = fu_firmware_get_image_by_id(helper->firmware, "ap", error); if (ap_img == NULL) return FALSE; df_img = fu_firmware_get_image_by_id(helper->firmware, "df", error); if (df_img == NULL) return FALSE; /* hid report to enable write */ if (!fu_logitech_tap_touch_device_write_enable(self, FALSE, FALSE, 0xF01F, 0, error)) return FALSE; fu_device_sleep(FU_DEVICE(self), 5); /* write_data */ fu_struct_logitech_tap_touch_hid_req_set_payload_len(st, 0x21); fu_struct_logitech_tap_touch_hid_req_set_response_len(st, 0x0); fu_struct_logitech_tap_touch_hid_req_set_cmd( st, FU_STRUCT_LOGITECH_TAP_TOUCH_HID_CMD_WRITE_DATA); fu_byte_array_set_size(st, 37, 0xFF); /* 4 (req header) + 1 (cmd) + FU_LOGITECH_TAP_TOUCH_TRANSFER_BLOCK_SIZE (data buffer) */ if (!fu_logitech_tap_touch_device_hid_transfer(self, st, 0, NULL, error)) return FALSE; fu_device_sleep(FU_DEVICE(self), 500); /* erase flash */ fu_progress_step_done(helper->progress); /* write firmware to data flash (DF) block */ if (!fu_logitech_tap_touch_device_write_blocks(self, df_img, df_checksum, FALSE, fu_progress_get_child(helper->progress), error)) return FALSE; fu_progress_step_done(helper->progress); /* write firmware to pflash (AP) block */ if (!fu_logitech_tap_touch_device_write_blocks(self, ap_img, ap_checksum, TRUE, fu_progress_get_child(helper->progress), error)) return FALSE; fu_progress_step_done(helper->progress); /* success */ return TRUE; } static gboolean fu_logitech_tap_touch_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuLogitechTapTouchDevice *self = FU_LOGITECH_TAP_TOUCH_DEVICE(device); FuLogitechTapTouchWriteHelper helper = {.firmware = firmware, .progress = progress}; g_autoptr(FuDeviceLocker) locker = NULL; /* enable/disable TDE mode */ locker = fu_device_locker_new_full(FU_DEVICE(self), (FuDeviceLockerFunc)fu_logitech_tap_touch_device_enable_tde, (FuDeviceLockerFunc)fu_logitech_tap_touch_device_disable_tde, error); if (locker == NULL) return FALSE; /* vendor recommendation is to retry few time */ return fu_device_retry_full(device, fu_logitech_tap_touch_device_write_chunks_cb, FU_LOGITECH_TAP_TOUCH_MAX_FW_WRITE_RETRIES, 100, &helper, error); } static void fu_logitech_tap_touch_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 10, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 80, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 10, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static gchar * fu_logitech_tap_touch_device_convert_version(FuDevice *device, guint64 version_raw) { /* convert 8 byte version in to human readable format. e.g. convert 0x0600000003000004 into * 6000.3004*/ return g_strdup_printf("%01x%01x%01x%01x.%01x%01x%01x%01x", (guint)((version_raw >> 56) & 0xFF), (guint)((version_raw >> 48) & 0xFF), (guint)((version_raw >> 40) & 0xFF), (guint)((version_raw >> 32) & 0xFF), (guint)((version_raw >> 24) & 0xFF), (guint)((version_raw >> 16) & 0xFF), (guint)((version_raw >> 8) & 0xFF), (guint)((version_raw >> 0) & 0xFF)); } static void fu_logitech_tap_touch_device_init(FuLogitechTapTouchDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "com.logitech.hardware.tap"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PAIR); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_COUNTERPART_GUIDS); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_set_firmware_size_min(FU_DEVICE(self), FU_LOGITECH_TAP_TOUCH_MIN_FW_FILE_SIZE); fu_device_set_firmware_size_max(FU_DEVICE(self), FU_LOGITECH_TAP_TOUCH_MAX_FW_FILE_SIZE); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_LOGITECH_TAP_TOUCH_FIRMWARE); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_NONBLOCK); } static void fu_logitech_tap_touch_device_class_init(FuLogitechTapTouchDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_logitech_tap_touch_device_probe; device_class->setup = fu_logitech_tap_touch_device_setup; device_class->set_progress = fu_logitech_tap_touch_device_set_progress; device_class->convert_version = fu_logitech_tap_touch_device_convert_version; device_class->detach = fu_logitech_tap_touch_device_detach; device_class->write_firmware = fu_logitech_tap_touch_device_write_firmware; device_class->attach = fu_logitech_tap_touch_device_attach; } fwupd-2.0.10/plugins/logitech-tap/fu-logitech-tap-touch-device.h000066400000000000000000000005561501337203100244770ustar00rootroot00000000000000/* * Copyright 2024 Logitech, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_LOGITECH_TAP_TOUCH_DEVICE (fu_logitech_tap_touch_device_get_type()) G_DECLARE_FINAL_TYPE(FuLogitechTapTouchDevice, fu_logitech_tap_touch_device, FU, LOGITECH_TAP_TOUCH_DEVICE, FuHidrawDevice) fwupd-2.0.10/plugins/logitech-tap/fu-logitech-tap-touch-firmware.c000066400000000000000000000204521501337203100250440ustar00rootroot00000000000000/* * Copyright 2024 Logitech, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-logitech-tap-touch-common.h" #include "fu-logitech-tap-touch-firmware.h" struct _FuLogitechTapTouchFirmware { FuFirmware parent_instance; guint32 mapping_version; guint16 fw_ic_name; guint32 protocol_version; guint16 ap_checksum; guint16 df_checksum; }; G_DEFINE_TYPE(FuLogitechTapTouchFirmware, fu_logitech_tap_touch_firmware, FU_TYPE_FIRMWARE) /* * mapping info addr in firmware file 0x2020: * 3 bytes mapping version * 3 bytes protocol version * 6 bytes ic name */ #define TAP_TOUCH_MAPPING_INFO_ADDR 0x2020 #define TAP_TOUCH_AP_START 0x2000 #define TAP_TOUCH_DF_START 0xF000 guint16 fu_logitech_tap_touch_firmware_get_ap_checksum(FuLogitechTapTouchFirmware *self) { g_return_val_if_fail(FU_LOGITECH_TAP_TOUCH_FIRMWARE(self), 0); return self->ap_checksum; } guint16 fu_logitech_tap_touch_firmware_get_df_checksum(FuLogitechTapTouchFirmware *self) { g_return_val_if_fail(FU_LOGITECH_TAP_TOUCH_FIRMWARE(self), 0); return self->df_checksum; } static void fu_logitech_tap_touch_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuLogitechTapTouchFirmware *self = FU_LOGITECH_TAP_TOUCH_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "fw_ic_name", self->fw_ic_name); fu_xmlb_builder_insert_kx(bn, "protocol_version", self->protocol_version); if (flags & FU_FIRMWARE_EXPORT_FLAG_INCLUDE_DEBUG) { fu_xmlb_builder_insert_kx(bn, "mapping_version", self->mapping_version); fu_xmlb_builder_insert_kx(bn, "ap_checksum", self->ap_checksum); fu_xmlb_builder_insert_kx(bn, "df_checksum", self->df_checksum); } } static gboolean fu_logitech_tap_touch_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { gsize streamsz = 0; /* validate firmware file size, typically between 60k to 75k */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz > FU_LOGITECH_TAP_TOUCH_MAX_FW_FILE_SIZE || streamsz < FU_LOGITECH_TAP_TOUCH_MIN_FW_FILE_SIZE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "unexpected firmware size, got 0x%x expected 0x%x", (guint32)streamsz, (guint32)FU_LOGITECH_TAP_TOUCH_MAX_FW_FILE_SIZE); return FALSE; } /* success */ return TRUE; } static gboolean fu_logitech_tap_touch_firmware_calculate_ap_crc_cb(const guint8 *buf, gsize bufsz, gpointer user_data, GError **error) { guint32 *ap_check = (guint32 *)user_data; const guint16 ap_polynomial = 0x8408; for (gsize i = 0; i < bufsz; i++) { *ap_check ^= buf[i]; for (guint8 idx = 0; idx < 8; idx++) { if (*ap_check & 0x01) *ap_check = (*ap_check >> 1) ^ ap_polynomial; else *ap_check = *ap_check >> 1; } } return TRUE; } static gboolean fu_logitech_tap_touch_firmware_calculate_basic_cb(const guint8 *buf, gsize bufsz, gpointer user_data, GError **error) { guint32 *df_check = (guint32 *)user_data; for (gsize i = 0; i < bufsz; i++) *df_check += buf[i]; return TRUE; } static gboolean fu_logitech_tap_touch_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuLogitechTapTouchFirmware *self = FU_LOGITECH_TAP_TOUCH_FIRMWARE(firmware); const guint32 ap_start = TAP_TOUCH_AP_START; const guint32 df_start = TAP_TOUCH_DF_START; gsize ap_end_offset = 0; gsize streamsz; guint32 ap_end; guint32 df_end; guint32 version_raw_major = 0; guint32 version_raw_minor = 0; guint64 version_raw; guint8 protocol_id = 0; g_autoptr(FuFirmware) ap_img = fu_firmware_new(); g_autoptr(FuFirmware) df_img = fu_firmware_new(); g_autoptr(GInputStream) ap_stream = NULL; /* temp stream to calculate ap crc, it is few bytes smaller */ g_autoptr(GInputStream) ap_stream_crc = NULL; g_autoptr(GInputStream) df_stream = NULL; const gchar *image_end_magic = "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFFILITek AP CRC "; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; /* file firmware version */ if (!fu_input_stream_read_u32(stream, 0x2030, &version_raw_major, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_input_stream_read_u32(stream, 0xF004, &version_raw_minor, G_BIG_ENDIAN, error)) return FALSE; version_raw = (((guint64)version_raw_major) << 32) | version_raw_minor; fu_firmware_set_version_raw(firmware, version_raw); /* mapping info: mapping version, protocol version, ic name */ if (!fu_input_stream_read_u24(stream, TAP_TOUCH_MAPPING_INFO_ADDR, &self->mapping_version, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_input_stream_read_u24(stream, TAP_TOUCH_MAPPING_INFO_ADDR + 3, &self->protocol_version, G_LITTLE_ENDIAN, error)) return FALSE; /* read and validate protocol id and ic name */ if (!fu_input_stream_read_u8(stream, TAP_TOUCH_MAPPING_INFO_ADDR + 5, &protocol_id, error)) return FALSE; if (!fu_input_stream_read_u16(stream, TAP_TOUCH_MAPPING_INFO_ADDR + 6, &self->fw_ic_name, G_LITTLE_ENDIAN, error)) return FALSE; if (protocol_id != FU_LOGITECH_TAP_TOUCH_SUPPORTED_PROTOCOL_VERSION || self->fw_ic_name != FU_LOGITECH_TAP_TOUCH_IC_NAME) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to validate firmware, " "protocol version: %x, fw ic name:%x", protocol_id, self->fw_ic_name); return FALSE; } /* read and validate magic tag, determine ap block end location */ df_end = streamsz; if (!fu_input_stream_find(stream, (const guint8 *)image_end_magic, strlen(image_end_magic), &ap_end_offset, error)) { g_prefix_error(error, "failed to find anchor: "); return FALSE; } ap_end = ap_end_offset + 32 + 2; /* get crc for pflash (AP) */ ap_stream = fu_partial_input_stream_new(stream, ap_start, ap_end - ap_start, error); if (ap_stream == NULL) return FALSE; ap_stream_crc = fu_partial_input_stream_new(stream, ap_start, (ap_end - 2) - ap_start, error); if (ap_stream_crc == NULL) return FALSE; if (!fu_input_stream_chunkify(ap_stream_crc, fu_logitech_tap_touch_firmware_calculate_ap_crc_cb, &self->ap_checksum, error)) return FALSE; fu_firmware_set_id(ap_img, "ap"); fu_firmware_set_offset(ap_img, ap_end); if (!fu_firmware_set_stream(ap_img, ap_stream, error)) return FALSE; fu_firmware_add_image(firmware, ap_img); /* calculate basic checksum for dataflash (DF) */ df_stream = fu_partial_input_stream_new(stream, df_start, df_end - df_start, error); if (df_stream == NULL) return FALSE; if (!fu_input_stream_chunkify(df_stream, fu_logitech_tap_touch_firmware_calculate_basic_cb, &self->df_checksum, error)) return FALSE; fu_firmware_set_id(df_img, "df"); fu_firmware_set_offset(df_img, df_end); if (!fu_firmware_set_stream(df_img, df_stream, error)) return FALSE; fu_firmware_add_image(firmware, df_img); /* success */ return TRUE; } static gchar * fu_logitech_tap_touch_firmware_convert_version(FuFirmware *firmware, guint64 version_raw) { /* convert 8 byte version in to human readable format. e.g. convert * 0x0600000003000004 into 6000.3004*/ return g_strdup_printf("%01x%01x%01x%01x.%01x%01x%01x%01x", (guint)((version_raw >> 56) & 0xFF), (guint)((version_raw >> 48) & 0xFF), (guint)((version_raw >> 40) & 0xFF), (guint)((version_raw >> 32) & 0xFF), (guint)((version_raw >> 24) & 0xFF), (guint)((version_raw >> 16) & 0xFF), (guint)((version_raw >> 8) & 0xFF), (guint)((version_raw >> 0) & 0xFF)); } static void fu_logitech_tap_touch_firmware_init(FuLogitechTapTouchFirmware *self) { fu_firmware_set_version_format(FU_FIRMWARE(self), FWUPD_VERSION_FORMAT_PAIR); } static void fu_logitech_tap_touch_firmware_class_init(FuLogitechTapTouchFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->convert_version = fu_logitech_tap_touch_firmware_convert_version; firmware_class->parse = fu_logitech_tap_touch_firmware_parse; firmware_class->export = fu_logitech_tap_touch_firmware_export; firmware_class->validate = fu_logitech_tap_touch_firmware_validate; } fwupd-2.0.10/plugins/logitech-tap/fu-logitech-tap-touch-firmware.h000066400000000000000000000012351501337203100250470ustar00rootroot00000000000000/* * Copyright 2024 Logitech, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_LOGITECH_TAP_TOUCH_FIRMWARE (fu_logitech_tap_touch_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuLogitechTapTouchFirmware, fu_logitech_tap_touch_firmware, FU, LOGITECH_TAP_TOUCH_FIRMWARE, FuFirmware) #define FU_LOGITECH_TAP_TOUCH_MAX_FW_FILE_SIZE (256 * 1024) #define FU_LOGITECH_TAP_TOUCH_MIN_FW_FILE_SIZE 0x6600 guint16 fu_logitech_tap_touch_firmware_get_ap_checksum(FuLogitechTapTouchFirmware *self); guint16 fu_logitech_tap_touch_firmware_get_df_checksum(FuLogitechTapTouchFirmware *self); fwupd-2.0.10/plugins/logitech-tap/fu-logitech-tap.rs000066400000000000000000000044211501337203100223120ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later enum FuLogitechTapTouchDirection { Out = 0x0, In = 0x0, } #[repr(u8)] enum FuStructLogitechTapTouchHidCmd { GetFirmwareVersion = 0x40, GetProtocolVersion = 0x42, GetMcuVersion = 0x61, GetSysBusyStatus = 0x80, GetMcuMode = 0xC0, SetApMode = 0xC1, SetBlMode = 0xC2, WriteData = 0xC3, WriteEnable = 0xC4, GetApCrc = 0xC7, GetDfCrc = 0xCA, SetTdeTestMode = 0xF2, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructLogitechTapTouchHidReq { report_id: u8 == 0x03, // response buffer is always going to be less than 64 bytes for this hardware/plugin // if response buffer is more than 64 bytes (for some hardware/interfaces), then use A4 here res_size_supported_id: u8 == 0xA3, payload_len: u8, // bytes to read from response response_len: u8, cmd: FuStructLogitechTapTouchHidCmd, //payload goes here } #[repr(u8)] enum FuLogitechTapSensorHidTdeMode { Disable = 0x0, Enable = 0x01, Selector = 0x02, } // device version #[repr(u8)] enum FuLogitechTapSensorHidColossusApp { GetVersion = 0x04, } // serial number of the device #[repr(u8)] enum FuLogitechTapSensorHidSerialNumber { SetReportByte1 = 0x0, SetReportByte4 = 0x0, SetReportByte3 = 0x0E, SetReportByte2 = 0x70, } #[repr(u8)] enum FuLogitechTapSensorHidReboot { PinClr = 0x05, PinSet = 0x06, Pwr = 0x2D, Rst = 0x2E, } #[repr(u8)] enum FuLogitechTapSensorHidSetCmd { // put device into suspend/operational mode Tde = 0x1A, Reboot = 0x1A, Version = 0x1B, SerialNumber = 0x1C, } #[repr(u8)] enum FuLogitechTapSensorHidGetCmd { Version = 0x19, SerialNumber = 0x1D, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructLogitechTapSensorHidReq { cmd: FuLogitechTapSensorHidSetCmd, //payload goes here payload_byte1: u8, payload_byte2: u8, payload_byte3: u8 = 0x0, payload_byte4: u8 = 0x0, } #[derive(New)] #[repr(C, packed)] struct FuStructLogitechTapSensorHidRes { cmd: FuLogitechTapSensorHidGetCmd, //payload goes here payload_byte1: u8, payload_byte2: u8, payload_byte3: u8, payload_byte4: u8, } fwupd-2.0.10/plugins/logitech-tap/logitech-tap.quirk000066400000000000000000000011671501337203100224150ustar00rootroot00000000000000# Logitech Flare PIR Sensor [HIDRAW\VEN_046D&DEV_0872] Plugin = logitech_tap GType = FuLogitechTapSensorDevice # Logitech Tap HDMI Capture [VIDEO4LINUX\VEN_046D&DEV_0876] Plugin = logitech_tap GType = FuLogitechTapHdmiDevice InstallDuration = 120 RemoveDelay = 120000 # Logitech Tap Touch Screen [HIDRAW\VEN_222A&DEV_0001] Plugin = logitech_tap GType = FuLogitechTapTouchDevice CounterpartGuid = HIDRAW\VEN_222A&DEV_FF51 RemoveDelay = 120000 # Logitech Tap Touch Screen bootloader [HIDRAW\VEN_222A&DEV_FF51] Plugin = logitech_tap GType = FuLogitechTapTouchDevice CounterpartGuid = HIDRAW\VEN_222A&DEV_0001 Flags = is-bootloader fwupd-2.0.10/plugins/logitech-tap/meson.build000066400000000000000000000011511501337203100211130ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginLogitechTap"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('logitech-tap.quirk') plugin_builtins += static_library('fu_plugin_logitech_tap', rustgen.process('fu-logitech-tap.rs'), sources: [ 'fu-logitech-tap-plugin.c', 'fu-logitech-tap-hdmi-device.c', 'fu-logitech-tap-sensor-device.c', 'fu-logitech-tap-touch-device.c', 'fu-logitech-tap-touch-firmware.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/mediatek-scaler/000077500000000000000000000000001501337203100174255ustar00rootroot00000000000000fwupd-2.0.10/plugins/mediatek-scaler/README.md000066400000000000000000000067751501337203100207230ustar00rootroot00000000000000--- title: Plugin: Mediatek Display Controller --- ## Introduction This plugin updates the firmware of DisplayPort device made by Mediatek. These devices communicate over I²C, via the DisplayPort aux channel. Devices are declared by kernel graphic driver, and queried with custom DDC/CI command to ensure the target devie. This plugin polls every drm dp aux device to find out the `i2c-dev` that is being used for DDC/CI communication. Devices should respond to a vendor specific command otherwise the display controller is ignored as unsupported. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in an unspecified binary file format, which is written to the partition of the device flash that is not currently running. This plugin supports the following protocol ID: * `com.mediatek.scaler` ## GUID Generation Devices use instance ID and the ingredients are vary according to fwupd version. ### fwupd 1.9.6 Devices use instance ID that is composed of `Subsystem ID`, `Subsystem Model`, and the `Hardware Version`. The hardware version is read from the device. * `DISPLAY\VID_1028&PID_0C88&HWVER_2.1.2.1` (metadata) * `DISPLAY\VID_1028&PID_0C8A&HWVER_2.1.2.1` (metadata) ### fwupd 1.9.10 The DDC/CI commands used to test the target device have been restricted to run on specific hardware only. Typically by adding a quirk file to match the system `VID` and `PID` from the `PCI` bus. * `DISPLAY\VID_1028&PID_0C88&HWVER_2.1.2.1` (metadata) * `DISPLAY\VID_1028&PID_0C8A&HWVER_2.1.2.1` (metadata) * `PCI\VID_1028&PID_0C88` (only-quirks) * `PCI\VID_1028&PID_0C8A` (only-quirks) ### fwupd 2.0.0 The enumeration of `i2c` devices has been centralized to daemon libfwupdplugin and declared as a `proxy` device for the `drm` subsystem device. The instance ID will be composed of `VEN`, `DEV` and the `HWVER`. The instance ID `VEN` is equivalent to the `mfg id` in device `EDID` while `DEV` is the `product code`. * `DRM\VEN_DEL&DEV_4340&HWVER_2.1.2.1` (quirks and metadata) * `DRM\VEN_DEL&DEV_7430&HWVER_3.1.5.1` (quirks and metadata) Example below for the information in EDID: ```shell $ sudo apt install ddcutil $ sudo ddcutil detect Display 1 I2C bus: /dev/i2c-14 DRM connector: card1-DP-2 EDID synopsis: Mfg id: DEL - Dell Inc. Model: OptiPlex AIO Product code: 17216 (0x4340) Serial number: Binary serial number: 1145581640 (0x44483048) Manufacture year: 2023, Week: 30 VCP version: 2.1 ``` ## Update Behavior The firmware is deployed when the device is in normal runtime mode, and the device will reset when the new firmware has been written. On some hardware the DRM device may not enumerate if there is no monitor actually plugged in. ## Vendor ID security The vendor ID is set from the PCI vendor, for instance `PCI:0x1028` on Dell systems. ## Quirk Use This plugin uses the following plugin-specific quirks: ### `bank2-only` Install firmware to bank 2 only. Since: 2.0.0 ## External Interface Access This plugin requires access to i2c buses associated with the specified DisplayPort aux channel, for instance `/dev/i2c-5` and `/dev/drm_dp_aux3`. Note that the device number changes in various situations. ## Version Considerations This plugin has been available since fwupd version `1.9.6`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Crag Wang: @CragW * Greg Lo: @GregLo007 fwupd-2.0.10/plugins/mediatek-scaler/fu-mediatek-scaler-common.c000066400000000000000000000005211501337203100245170ustar00rootroot00000000000000/* * Copyright 2023 Dell Technologies * Copyright 2023 Mediatek Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-mediatek-scaler-common.h" gchar * fu_mediatek_scaler_version_to_string(guint32 val) { return g_strdup_printf("%u.%u.%u", val & 0xff, (val >> 24) & 0xff, (val >> 16) & 0xff); } fwupd-2.0.10/plugins/mediatek-scaler/fu-mediatek-scaler-common.h000066400000000000000000000003401501337203100245230ustar00rootroot00000000000000/* * Copyright 2023 Dell Technologies * Copyright 2023 Mediatek Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include gchar * fu_mediatek_scaler_version_to_string(guint32 val); fwupd-2.0.10/plugins/mediatek-scaler/fu-mediatek-scaler-device.c000066400000000000000000000715761501337203100245100ustar00rootroot00000000000000/* * Copyright 2023 Dell Technologies * Copyright 2023 Mediatek Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include #include "fu-mediatek-scaler-common.h" #include "fu-mediatek-scaler-device.h" #include "fu-mediatek-scaler-firmware.h" #include "fu-mediatek-scaler-struct.h" #define DDC_DATA_LEN_DFT 0x80 #define DDC_DATA_FRAGEMENT_SIZE 0x0B /* 11 bytes for each DDC write */ #define DDC_DATA_PAGE_SIZE 0x1000 /* 4K bytes for each block page */ #define DDC_RW_MAX_RETRY_CNT 10 /* supported display controller type */ #define FU_MEDIATEK_SCALER_SUPPORTED_CONTROLLER_TYPE 0x00005605 /* timeout duration in ms to i2c-dev operation */ #define FU_MEDIATEK_SCALER_DEVICE_IOCTL_TIMEOUT 5000 /* delay time before a ddc read or write */ #define FU_MEDIATEK_SCALER_DDC_MSG_DELAY_MS 50 /* delay time before a ddc read or write */ #define FU_MEDIATEK_SCALER_CHUNK_SENT_DELAY_MS 1 /* interval in ms between the poll to check device status */ #define FU_MEDIATEK_SCALER_DEVICE_POLL_INTERVAL 1000 /* maximum retries for polliing the device existence */ #define FU_MEDIATEK_SCALER_DEVICE_PRESENT_RETRY 100 /* firmware payload size */ #define FU_MEDIATEK_SCALER_FW_SIZE_MAX 0x100000 /* device private flag */ #define FWUPD_MEDIATEK_SCALER_FLAG_BANK2_ONLY "bank2-only" typedef struct { FuChunk *chk; guint32 sent_sz; } FuMediatekScalerWriteChunkHelper; struct _FuMediatekScalerDevice { FuDrmDevice parent_instance; guint8 randval_cnt; }; G_DEFINE_TYPE(FuMediatekScalerDevice, fu_mediatek_scaler_device, FU_TYPE_DRM_DEVICE) static gboolean fu_mediatek_scaler_device_ddc_write(FuMediatekScalerDevice *self, GByteArray *st_req, GError **error) { FuI2cDevice *i2c_proxy = FU_I2C_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); guint8 chksum = 0; g_autoptr(GByteArray) ddc_msgbox_write = g_byte_array_new(); const guint8 ddc_wfmt[] = {FU_DDC_I2C_ADDR_HOST_DEVICE, st_req->len | DDC_DATA_LEN_DFT}; /* write = addr_src, sizeof(cmd + op + data), cmd, op, data, checksum */ g_byte_array_append(ddc_msgbox_write, ddc_wfmt, sizeof(ddc_wfmt)); g_byte_array_append(ddc_msgbox_write, st_req->data, st_req->len); chksum ^= FU_DDC_I2C_ADDR_DISPLAY_DEVICE; for (gsize i = 0; i < ddc_msgbox_write->len; i++) chksum ^= ddc_msgbox_write->data[i]; g_byte_array_append(ddc_msgbox_write, &chksum, 1); /* print the raw data */ fu_dump_raw(G_LOG_DOMAIN, "DDC/CI write message", ddc_msgbox_write->data, ddc_msgbox_write->len); return fu_i2c_device_write(i2c_proxy, ddc_msgbox_write->data, ddc_msgbox_write->len, error); } static GByteArray * fu_mediatek_scaler_device_ddc_read(FuMediatekScalerDevice *self, GByteArray *st_req, GError **error) { FuI2cDevice *i2c_proxy = FU_I2C_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); guint8 buf[0x40] = {0x00}; /* default 64 bytes */ gsize report_data_sz = 0; guint8 checksum = 0; guint8 checksum_hw = 0; g_autoptr(GByteArray) st_res = g_byte_array_new(); /* write for read */ if (!fu_mediatek_scaler_device_ddc_write(self, st_req, error)) return NULL; /* DDCCI spec requires host to wait at least 50 - 200ms before next message */ fu_device_sleep(FU_DEVICE(self), FU_MEDIATEK_SCALER_DDC_MSG_DELAY_MS); /* read into tmp buffer */ if (!fu_i2c_device_read(i2c_proxy, buf, sizeof(buf), error)) return NULL; /* read buffer = addr(src) + length + data + checksum */ fu_dump_raw(G_LOG_DOMAIN, "DDC/CI read buffer", buf, sizeof(buf)); /* verify read buffer: [0] == source address */ if (buf[0] != FU_DDC_I2C_ADDR_DISPLAY_DEVICE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid read buffer: addr(src) expected 0x%02x, got 0x%02x.", (guint)FU_DDC_I2C_ADDR_DISPLAY_DEVICE, buf[0]); return NULL; } /* verify read buffer: [1] as the length of data */ if (buf[1] <= DDC_DATA_LEN_DFT) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid read buffer: size 0x%02x must greater than 0x%02x.", buf[1], (guint)DDC_DATA_LEN_DFT); return NULL; } /* verify read buffer: overflow guard from the length of data */ report_data_sz = buf[1] - DDC_DATA_LEN_DFT; if (report_data_sz + 3 > sizeof(buf)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid read buffer: size 0x%02x exceeded 0x%02x", (guint)report_data_sz, (guint)sizeof(buf)); return NULL; } /* verify read buffer: match the checksum */ checksum ^= FU_DDC_I2C_ADDR_CHECKSUM; for (gsize i = 0; i < report_data_sz + 2; i++) checksum ^= buf[i]; if (!fu_memread_uint8_safe(buf, sizeof(buf), report_data_sz + 2, &checksum_hw, error)) return NULL; if (checksum_hw != checksum) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid read buffer, checksum expected 0x%02x, got 0x%02x.", checksum, checksum_hw); return NULL; } /* truncate the last byte which is the checksum value */ g_byte_array_append(st_res, buf, report_data_sz + 2); /* print the raw data */ fu_dump_raw(G_LOG_DOMAIN, "DDC/CI read report", st_res->data, st_res->len); return g_steal_pointer(&st_res); } static gboolean fu_mediatek_scaler_device_set_ddc_priority(FuMediatekScalerDevice *self, FuDdcciPriority priority, GError **error) { g_autoptr(GByteArray) st_req = fu_struct_ddc_cmd_new(); g_autoptr(GError) error_local = NULL; fu_struct_ddc_cmd_set_vcp_code(st_req, FU_DDC_VCP_CODE_PRIORITY); fu_byte_array_append_uint8(st_req, priority); if (!fu_mediatek_scaler_device_ddc_write(self, st_req, &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to set priority %s [0x%x], unsupported display: %s", fu_ddcci_priority_to_string(priority), priority, error_local->message); return FALSE; } fu_device_sleep(FU_DEVICE(self), FU_MEDIATEK_SCALER_DDC_MSG_DELAY_MS); return TRUE; } static gboolean fu_mediatek_scaler_device_display_is_connected(FuMediatekScalerDevice *self, GError **error) { FuDevice *proxy = fu_device_get_proxy(FU_DEVICE(self)); g_autoptr(GByteArray) st_req = fu_struct_ddc_cmd_new(); g_autoptr(GByteArray) st_res = NULL; g_autoptr(GError) error_local = NULL; guint8 randval_req = 0; guint8 randval1 = self->randval_cnt++; guint8 randval2 = self->randval_cnt++; fu_struct_ddc_cmd_set_vcp_code(st_req, FU_DDC_VCP_CODE_SUM); fu_byte_array_append_uint8(st_req, randval1); fu_byte_array_append_uint8(st_req, randval2); st_res = fu_mediatek_scaler_device_ddc_read(self, st_req, &error_local); if (st_res == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to read report: %s", error_local->message); return FALSE; } if (!fu_memread_uint8_safe(st_res->data, st_res->len, 3, &randval_req, error)) return FALSE; /* device unique feature */ if (randval_req != (guint8)(randval1 + randval2)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsuccessful display feature test, expected 0x%02x, got 0x%02x.", (guint8)(randval1 + randval2), randval_req); return FALSE; } g_info("found mediatek display controller: %s, i2c-dev: %s", fu_udev_device_get_device_file(FU_UDEV_DEVICE(self)), fu_udev_device_get_device_file(FU_UDEV_DEVICE(proxy))); return TRUE; } static gboolean fu_mediatek_scaler_device_display_is_connected_cb(FuDevice *device, gpointer user_data, GError **error) { FuMediatekScalerDevice *self = FU_MEDIATEK_SCALER_DEVICE(device); return fu_mediatek_scaler_device_display_is_connected(self, error); } static gchar * fu_mediatek_scaler_device_get_hardware_version(FuDevice *device, GError **error) { FuMediatekScalerDevice *self = FU_MEDIATEK_SCALER_DEVICE(device); g_autoptr(GByteArray) st_req = fu_struct_ddc_cmd_new(); g_autoptr(GByteArray) st_res = NULL; guint8 verbuf[4] = {0}; /* get the hardware version */ fu_struct_ddc_cmd_set_vcp_code(st_req, FU_DDC_VCP_CODE_VERSION); fu_byte_array_append_uint8(st_req, 0x00); st_res = fu_mediatek_scaler_device_ddc_read(self, st_req, error); if (st_res == NULL) return NULL; if (!fu_memread_uint8_safe(st_res->data, st_res->len, 3, &verbuf[0], error)) return NULL; if (!fu_memread_uint8_safe(st_res->data, st_res->len, 2, &verbuf[1], error)) return NULL; if (!fu_memread_uint8_safe(st_res->data, st_res->len, 5, &verbuf[2], error)) return NULL; if (!fu_memread_uint8_safe(st_res->data, st_res->len, 4, &verbuf[3], error)) return NULL; return g_strdup_printf("%x.%x.%x.%x", verbuf[0], verbuf[1], verbuf[2], verbuf[3]); } static gboolean fu_mediatek_scaler_device_ensure_firmware_version(FuMediatekScalerDevice *self, GError **error) { guint32 version_raw = 0x0; g_autoptr(GByteArray) st_res = NULL; g_autoptr(GByteArray) st_req = fu_struct_ddc_cmd_new(); /* get the installed firmware version */ fu_struct_ddc_cmd_set_vcp_code(st_req, FU_DDC_VCP_CODE_VERSION); fu_byte_array_append_uint8(st_req, 0x01); st_res = fu_mediatek_scaler_device_ddc_read(self, st_req, error); if (st_res == NULL) return FALSE; if (!fu_memread_uint32_safe(st_res->data, st_res->len, 2, &version_raw, G_LITTLE_ENDIAN, error)) return FALSE; fu_device_set_version_raw(FU_DEVICE(self), version_raw); return TRUE; } static gboolean fu_mediatek_scaler_device_open(FuDevice *device, GError **error) { FuMediatekScalerDevice *self = FU_MEDIATEK_SCALER_DEVICE(device); FuI2cDevice *i2c_proxy = FU_I2C_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); /* FuUdevDevice->open */ if (!FU_DEVICE_CLASS(fu_mediatek_scaler_device_parent_class)->open(device, error)) return FALSE; /* set the target address -- should be safe */ if (!fu_i2c_device_set_address(i2c_proxy, FU_DDC_I2C_ADDR_DISPLAY_DEVICE >> 1, FALSE, error)) return FALSE; /* we know this is a Mediatek scaler now */ if (fu_device_get_version_raw(device) != 0x0) { if (!fu_mediatek_scaler_device_set_ddc_priority(self, FU_DDCCI_PRIORITY_UP, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_mediatek_scaler_device_close(FuDevice *device, GError **error) { FuMediatekScalerDevice *self = FU_MEDIATEK_SCALER_DEVICE(device); FuI2cDevice *i2c_proxy = FU_I2C_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); /* set the target address */ if (!fu_i2c_device_set_address(i2c_proxy, FU_DDC_I2C_ADDR_DISPLAY_DEVICE >> 1, FALSE, error)) return FALSE; /* reset DDC priority */ if (!fu_mediatek_scaler_device_set_ddc_priority(self, FU_DDCCI_PRIORITY_NORMAL, error)) return FALSE; /* success */ return FU_DEVICE_CLASS(fu_mediatek_scaler_device_parent_class)->close(device, error); } static gboolean fu_mediatek_scaler_device_verify_controller_type(FuMediatekScalerDevice *self, GError **error) { g_autoptr(GByteArray) st_req = fu_struct_ddc_cmd_new(); g_autoptr(GByteArray) st_res = NULL; g_autoptr(GError) error_local = NULL; guint32 controller_type = 0; fu_struct_ddc_cmd_set_opcode(st_req, FU_DDC_OPCODE_GET_VCP); fu_struct_ddc_cmd_set_vcp_code(st_req, FU_DDC_VCP_CODE_CONTROLLER_TYPE); st_res = fu_mediatek_scaler_device_ddc_read(self, st_req, error); if (st_res == NULL) return FALSE; if (!fu_memread_uint32_safe(st_res->data, st_res->len, st_res->len - 4, &controller_type, G_BIG_ENDIAN, error)) return FALSE; /* restrict to specific controller type */ if (controller_type != FU_MEDIATEK_SCALER_SUPPORTED_CONTROLLER_TYPE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "0x%x is not supported", controller_type); return FALSE; } /* success */ fu_device_sleep(FU_DEVICE(self), FU_MEDIATEK_SCALER_DDC_MSG_DELAY_MS); return TRUE; } static gboolean fu_mediatek_scaler_device_setup(FuDevice *device, GError **error) { FuMediatekScalerDevice *self = FU_MEDIATEK_SCALER_DEVICE(device); g_autofree gchar *hw_ver = NULL; /* verify the controller type */ if (!fu_mediatek_scaler_device_verify_controller_type(self, error)) { g_prefix_error(error, "invalid controller type: "); return FALSE; } /* mediatek display is connected */ if (!fu_mediatek_scaler_device_display_is_connected(self, error)) return FALSE; /* prioritize DDC/CI -- FuDevice->open() did not do this as the version is not set */ if (!fu_mediatek_scaler_device_set_ddc_priority(self, FU_DDCCI_PRIORITY_UP, error)) return FALSE; /* set hardware version */ hw_ver = fu_mediatek_scaler_device_get_hardware_version(device, error); if (hw_ver == NULL) return FALSE; fu_device_add_instance_str(device, "HWVER", hw_ver); if (!fu_device_build_instance_id(device, error, "DRM", "VEN", "DEV", "HWVER", NULL)) return FALSE; /* get details */ if (!fu_mediatek_scaler_device_ensure_firmware_version(self, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_mediatek_scaler_device_set_recv_info(FuDevice *device, gsize fw_sz, GError **error) { FuMediatekScalerDevice *self = FU_MEDIATEK_SCALER_DEVICE(device); g_autoptr(GByteArray) st_req = fu_struct_ddc_cmd_new(); fu_struct_ddc_cmd_set_vcp_code(st_req, FU_DDC_VCP_CODE_UPDATE_PREP); fu_byte_array_append_uint32(st_req, fw_sz, G_LITTLE_ENDIAN); return fu_mediatek_scaler_device_ddc_write(self, st_req, error); } static gboolean fu_mediatek_scaler_device_get_data_ack_size(FuDevice *device, guint32 *ack_sz, GError **error) { FuMediatekScalerDevice *self = FU_MEDIATEK_SCALER_DEVICE(device); g_autoptr(GByteArray) st_req = fu_struct_ddc_cmd_new(); g_autoptr(GByteArray) st_res = NULL; fu_struct_ddc_cmd_set_vcp_code(st_req, FU_DDC_VCP_CODE_UPDATE_ACK); st_res = fu_mediatek_scaler_device_ddc_read(self, st_req, error); if (st_res == NULL) return FALSE; if (!fu_memread_uint32_safe(st_res->data, st_res->len, 2, ack_sz, G_LITTLE_ENDIAN, error)) return FALSE; return TRUE; } static gboolean fu_mediatek_scaler_device_prepare_update_cb(FuDevice *device, gpointer user_data, GError **error) { guint32 acksz = 0; gsize fw_sz = *(gsize *)user_data; /* set the file length that to be transmit*/ if (!fu_mediatek_scaler_device_set_recv_info(device, fw_sz, error)) return FALSE; /* extra delay time needed */ fu_device_sleep(device, 100); /* device accepted the file length for data transition */ if (!fu_mediatek_scaler_device_get_data_ack_size(device, &acksz, error)) return FALSE; if (fw_sz != (gsize)acksz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "device nak the incoming filesize, requested: %" G_GSIZE_FORMAT ", ack: %u", fw_sz, acksz); return FALSE; } return TRUE; } static gboolean fu_mediatek_scaler_device_prepare_update(FuDevice *device, gsize fw_sz, GError **error) { if (!fu_device_retry_full(device, fu_mediatek_scaler_device_prepare_update_cb, DDC_RW_MAX_RETRY_CNT, 10, /* ms */ &fw_sz, error)) { g_prefix_error(error, "failed to prepare update: "); return FALSE; } return TRUE; } static gboolean fu_mediatek_scaler_device_set_data(FuMediatekScalerDevice *self, FuChunk *chk, GError **error) { g_autoptr(FuChunkArray) chk_slices = NULL; g_autoptr(GBytes) chk_bytes = fu_chunk_get_bytes(chk); /* smaller slices to accodomate pch variants */ chk_slices = fu_chunk_array_new_from_bytes(chk_bytes, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, DDC_DATA_FRAGEMENT_SIZE); for (guint i = 0; i < fu_chunk_array_length(chk_slices); i++) { g_autoptr(FuChunk) chk_slice = NULL; g_autoptr(GByteArray) st_req = fu_struct_ddc_cmd_new(); chk_slice = fu_chunk_array_index(chk_slices, i, error); if (chk_slice == NULL) return FALSE; fu_struct_ddc_cmd_set_vcp_code(st_req, FU_DDC_VCP_CODE_SET_DATA); g_byte_array_append(st_req, fu_chunk_get_data(chk_slice), (guint)fu_chunk_get_data_sz(chk_slice)); if (!fu_mediatek_scaler_device_ddc_write(self, st_req, error)) { g_prefix_error(error, "failed to send firmware to device: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), FU_MEDIATEK_SCALER_CHUNK_SENT_DELAY_MS); } return TRUE; } static gboolean fu_mediatek_scaler_device_get_staged_data(FuMediatekScalerDevice *self, guint16 *chksum, guint32 *pktcnt, GError **error) { g_autoptr(GByteArray) st_req = fu_struct_ddc_cmd_new(); g_autoptr(GByteArray) st_res = NULL; fu_struct_ddc_cmd_set_vcp_code(st_req, FU_DDC_VCP_CODE_GET_STAGED); st_res = fu_mediatek_scaler_device_ddc_read(self, st_req, error); if (st_res == NULL) return FALSE; if (!fu_memread_uint16_safe(st_res->data, st_res->len, 2, chksum, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memread_uint32_safe(st_res->data, st_res->len, 4, pktcnt, G_LITTLE_ENDIAN, error)) return FALSE; return TRUE; } static gboolean fu_mediatek_scaler_device_check_sent_info(FuMediatekScalerDevice *self, FuChunk *chk, guint32 sent_size, GError **error) { guint16 chksum = 0; guint16 sum16 = 0; guint32 pktcnt = 0; if (!fu_mediatek_scaler_device_get_staged_data(self, &chksum, &pktcnt, error)) { g_prefix_error(error, "failed to get the staged data: "); return FALSE; } /* verify the staged packets on chip */ if (sent_size != pktcnt) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "data packet size mismatched, expected: %X, chip got: %X", sent_size, pktcnt); return FALSE; } /* verify the checksum on chip */ sum16 = fu_sum16(fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); if (sum16 != chksum) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "data packet checksum mismatched, expected: %X, chip got: %X", sum16, chksum); return FALSE; } return TRUE; } static gboolean fu_mediatek_scaler_device_run_isp(FuMediatekScalerDevice *self, guint16 chksum, GError **error) { g_autoptr(GByteArray) st_req = fu_struct_ddc_cmd_new(); fu_struct_ddc_cmd_set_vcp_code(st_req, FU_DDC_VCP_CODE_COMMIT_FW); fu_byte_array_append_uint16(st_req, chksum, G_LITTLE_ENDIAN); return fu_mediatek_scaler_device_ddc_write(self, st_req, error); } static gboolean fu_mediatek_scaler_device_commit_firmware(FuMediatekScalerDevice *self, GInputStream *stream, GError **error) { guint16 sum16 = 0; if (!fu_input_stream_compute_sum16(stream, &sum16, error)) return FALSE; if (!(fu_mediatek_scaler_device_run_isp(self, sum16, error))) { g_prefix_error(error, "failed to commit firmware: "); return FALSE; } return TRUE; } static gboolean fu_mediatek_scaler_device_set_isp_reboot(FuMediatekScalerDevice *self, GError **error) { g_autoptr(GByteArray) st_req = fu_struct_ddc_cmd_new(); g_autoptr(GError) error_local = NULL; /* device will reboot after this, so the write will timed out fail */ fu_struct_ddc_cmd_set_vcp_code(st_req, FU_DDC_VCP_CODE_REBOOT); if (!fu_mediatek_scaler_device_ddc_write(self, st_req, &error_local)) { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT)) { g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to set isp reboot: "); return FALSE; } } return TRUE; } static gboolean fu_mediatek_scaler_device_get_isp_status(FuMediatekScalerDevice *self, guint8 *isp_status, GError **error) { g_autoptr(GByteArray) st_req = fu_struct_ddc_cmd_new(); g_autoptr(GByteArray) st_res = NULL; fu_struct_ddc_cmd_set_vcp_code(st_req, FU_DDC_VCP_CODE_GET_ISP_MODE); st_res = fu_mediatek_scaler_device_ddc_read(self, st_req, error); if (st_res == NULL) return FALSE; if (!fu_memread_uint8_safe(st_res->data, st_res->len, 2, isp_status, error)) return FALSE; return TRUE; } static gboolean fu_mediatek_scaler_device_is_update_success_cb(FuDevice *device, gpointer user_data, GError **error) { FuMediatekScalerDevice *self = FU_MEDIATEK_SCALER_DEVICE(device); guint8 isp_status = 0; if (!fu_mediatek_scaler_device_get_isp_status(self, &isp_status, error)) return FALSE; if (isp_status != FU_MEDIATEK_SCALER_ISP_STATUS_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "incorrect isp status, expected: 0x%x, got: 0x%x", (guint)FU_MEDIATEK_SCALER_ISP_STATUS_SUCCESS, isp_status); return FALSE; } return TRUE; } static gboolean fu_mediatek_scaler_device_verify(FuDevice *device, GError **error) { if (!fu_device_retry_full(device, fu_mediatek_scaler_device_display_is_connected_cb, FU_MEDIATEK_SCALER_DEVICE_PRESENT_RETRY, FU_MEDIATEK_SCALER_DEVICE_POLL_INTERVAL, NULL, error)) { g_prefix_error(error, "display controller did not reconnect after %u retries: ", (guint)FU_MEDIATEK_SCALER_DEVICE_PRESENT_RETRY); return FALSE; } /* ensure isp status */ if (!fu_device_retry_full(device, fu_mediatek_scaler_device_is_update_success_cb, FU_MEDIATEK_SCALER_DEVICE_PRESENT_RETRY, FU_MEDIATEK_SCALER_DEVICE_POLL_INTERVAL, NULL, error)) return FALSE; return TRUE; } static gboolean fu_mediatek_scaler_device_chunk_data_is_blank(FuChunk *chk) { const guint8 *data = fu_chunk_get_data(chk); for (gsize idx = 0; idx < fu_chunk_get_data_sz(chk); idx++) if (data[idx] != 0xFF) return FALSE; return TRUE; } static gboolean fu_mediatek_scaler_device_set_data_fast_forward(FuMediatekScalerDevice *self, guint32 sent_sz, GError **error) { g_autoptr(GByteArray) st_req = fu_struct_ddc_cmd_new(); fu_struct_ddc_cmd_set_vcp_code(st_req, FU_DDC_VCP_CODE_SET_DATA_FF); fu_byte_array_append_uint32(st_req, sent_sz, G_LITTLE_ENDIAN); return fu_mediatek_scaler_device_ddc_write(self, st_req, error); } static gboolean fu_mediatek_scaler_device_write_chunk(FuDevice *device, gpointer user_data, GError **error) { FuMediatekScalerDevice *self = FU_MEDIATEK_SCALER_DEVICE(device); FuMediatekScalerWriteChunkHelper *helper = (FuMediatekScalerWriteChunkHelper *)user_data; /* fast forward if possible */ if (fu_mediatek_scaler_device_chunk_data_is_blank(helper->chk)) { /* fast forward if chunk is empty */ if (!fu_mediatek_scaler_device_set_data_fast_forward(self, helper->sent_sz, error)) return FALSE; } else { /* set data per fragment size */ if (!fu_mediatek_scaler_device_set_data(self, helper->chk, error)) return FALSE; } /* verify the sent data chunk */ if (!fu_mediatek_scaler_device_check_sent_info(self, helper->chk, helper->sent_sz, error)) { /* restore the data size counter */ if (!fu_mediatek_scaler_device_set_data_fast_forward( self, helper->sent_sz - fu_chunk_get_data_sz(helper->chk), error)) return FALSE; } /* ff to reset the checksum */ return fu_mediatek_scaler_device_set_data_fast_forward(self, helper->sent_sz, error); } static gboolean fu_mediatek_scaler_device_write_firmware_impl(FuMediatekScalerDevice *self, GInputStream *stream, FuProgress *progress, GError **error) { guint32 sent_sz = 0x0; g_autoptr(FuChunkArray) chunks = NULL; chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, DDC_DATA_PAGE_SIZE, error); if (chunks == NULL) return FALSE; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { FuMediatekScalerWriteChunkHelper helper_wchunk = {0x0}; g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; /* data size already sent to chip */ sent_sz += fu_chunk_get_data_sz(chk); /* retry writing data chunk */ helper_wchunk.chk = chk; helper_wchunk.sent_sz = sent_sz; if (!fu_device_retry_full(FU_DEVICE(self), fu_mediatek_scaler_device_write_chunk, DDC_RW_MAX_RETRY_CNT, FU_MEDIATEK_SCALER_DDC_MSG_DELAY_MS, &helper_wchunk, error)) { g_prefix_error(error, "writing chunk exceeded the maximum retries"); return FALSE; } /* write chunk successfully, update the progress */ fu_progress_step_done(progress); g_debug("data size sent to chip: 0x%x", sent_sz); } return TRUE; } static gboolean fu_mediatek_scaler_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuMediatekScalerDevice *self = FU_MEDIATEK_SCALER_DEVICE(device); gsize fw_size = 0; g_autoptr(GInputStream) stream = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "prepare"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 76, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "commit"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 12, "verify"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "reset"); /* get default image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* prepare the device to accept firmware image */ if (!fu_input_stream_size(stream, &fw_size, error)) return FALSE; if (!fu_mediatek_scaler_device_prepare_update(device, fw_size, error)) return FALSE; fu_progress_step_done(progress); /* write firmware to device */ if (!fu_mediatek_scaler_device_write_firmware_impl(self, stream, progress, error)) return FALSE; fu_progress_step_done(progress); /* send ISP command to commit the update */ if (!fu_mediatek_scaler_device_commit_firmware(self, stream, error)) return FALSE; fu_progress_step_done(progress); /* verify display and ISP status; for bank 1 devices 0xF8 will do self-reboot */ if (!fu_mediatek_scaler_device_verify(device, error)) return FALSE; fu_progress_step_done(progress); /* for bank 2 update */ if (fu_device_has_private_flag(device, FWUPD_MEDIATEK_SCALER_FLAG_BANK2_ONLY)) { /* send reboot command to take effect immediately */ if (!(fu_mediatek_scaler_device_set_isp_reboot(self, error))) return FALSE; /* ensure device is back */ if (!fu_device_retry_full(device, fu_mediatek_scaler_device_display_is_connected_cb, FU_MEDIATEK_SCALER_DEVICE_PRESENT_RETRY, FU_MEDIATEK_SCALER_DEVICE_POLL_INTERVAL, NULL, error)) { g_prefix_error(error, "display controller did not reconnect after %u retries: ", (guint)FU_MEDIATEK_SCALER_DEVICE_PRESENT_RETRY); return FALSE; } } fu_progress_step_done(progress); /* success */ return TRUE; } static FuFirmware * fu_mediatek_scaler_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { g_autoptr(FuFirmware) firmware = fu_mediatek_scaler_firmware_new(); if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; g_info("firmware version old: %s, new: %s", fu_device_get_version(device), fu_firmware_get_version(firmware)); return g_steal_pointer(&firmware); } static gchar * fu_mediatek_scaler_device_convert_version(FuDevice *self, guint64 version_raw) { return fu_mediatek_scaler_version_to_string(version_raw); } static void fu_mediatek_scaler_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 99, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_mediatek_scaler_device_init(FuMediatekScalerDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PROXY_FOR_OPEN); fu_device_set_vendor(FU_DEVICE(self), "Mediatek"); fu_device_add_protocol(FU_DEVICE(self), "com.mediatek.scaler"); fu_device_set_name(FU_DEVICE(self), "Display Controller"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_VIDEO_DISPLAY); fu_device_set_firmware_size_max(FU_DEVICE(self), FU_MEDIATEK_SCALER_FW_SIZE_MAX); fu_device_register_private_flag(FU_DEVICE(self), FWUPD_MEDIATEK_SCALER_FLAG_BANK2_ONLY); } static void fu_mediatek_scaler_device_class_init(FuMediatekScalerDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->convert_version = fu_mediatek_scaler_device_convert_version; device_class->setup = fu_mediatek_scaler_device_setup; device_class->open = fu_mediatek_scaler_device_open; device_class->close = fu_mediatek_scaler_device_close; device_class->prepare_firmware = fu_mediatek_scaler_device_prepare_firmware; device_class->write_firmware = fu_mediatek_scaler_device_write_firmware; device_class->reload = fu_mediatek_scaler_device_setup; device_class->set_progress = fu_mediatek_scaler_device_set_progress; } fwupd-2.0.10/plugins/mediatek-scaler/fu-mediatek-scaler-device.h000066400000000000000000000006001501337203100244710ustar00rootroot00000000000000/* * Copyright 2023 Dell Technologies * Copyright 2023 Mediatek Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_MEDIATEK_SCALER_DEVICE (fu_mediatek_scaler_device_get_type()) G_DECLARE_FINAL_TYPE(FuMediatekScalerDevice, fu_mediatek_scaler_device, FU, MEDIATEK_SCALER_DEVICE, FuDrmDevice) fwupd-2.0.10/plugins/mediatek-scaler/fu-mediatek-scaler-firmware.c000066400000000000000000000045161501337203100250530ustar00rootroot00000000000000/* * Copyright 2023 Dell Technologies * Copyright 2023 Mediatek Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-mediatek-scaler-common.h" #include "fu-mediatek-scaler-firmware.h" #define MTK_FW_OFFSET_VERSION 0x7118 #define MTK_FW_OFFSET_TIMESTAMP_DATE 0x7200 #define MTK_FW_OFFSET_TIMESTAMP_TIME 0x720c #define MTK_FW_TIMESTAMP_DATE_SIZE 11 #define MTK_FW_TIMESTAMP_TIME_SIZE 8 struct _FuMediatekScalerFirmware { FuFirmwareClass parent_instance; }; G_DEFINE_TYPE(FuMediatekScalerFirmware, fu_mediatek_scaler_firmware, FU_TYPE_FIRMWARE) static gboolean fu_mediatek_scaler_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { guint32 ver_tmp = 0x0; guint8 buf_date[MTK_FW_TIMESTAMP_DATE_SIZE] = {0}; guint8 buf_time[MTK_FW_TIMESTAMP_TIME_SIZE] = {0}; g_autofree gchar *fw_version = NULL; g_autofree gchar *fw_date = NULL; g_autofree gchar *fw_time = NULL; /* read version from firmware */ if (!fu_input_stream_read_u32(stream, MTK_FW_OFFSET_VERSION, &ver_tmp, G_LITTLE_ENDIAN, error)) return FALSE; fw_version = fu_mediatek_scaler_version_to_string(ver_tmp); fu_firmware_set_version(firmware, fw_version); /* read timestamp from firmware */ if (!fu_input_stream_read_safe(stream, buf_date, sizeof(buf_date), 0x0, MTK_FW_OFFSET_TIMESTAMP_DATE, sizeof(buf_date), error)) return FALSE; fw_date = fu_strsafe((const gchar *)buf_date, sizeof(buf_date)); if (!fu_input_stream_read_safe(stream, buf_time, sizeof(buf_time), 0x0, MTK_FW_OFFSET_TIMESTAMP_TIME, sizeof(buf_time), error)) return FALSE; fw_time = fu_strsafe((const gchar *)buf_time, sizeof(buf_time)); g_info("firmware timestamp: %s, %s", fw_time, fw_date); return TRUE; } static void fu_mediatek_scaler_firmware_init(FuMediatekScalerFirmware *self) { } static void fu_mediatek_scaler_firmware_class_init(FuMediatekScalerFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_mediatek_scaler_firmware_parse; } FuFirmware * fu_mediatek_scaler_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_MEDIATEK_SCALER_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/mediatek-scaler/fu-mediatek-scaler-firmware.h000066400000000000000000000006761501337203100250630ustar00rootroot00000000000000/* * Copyright 2023 Dell Technologies * Copyright 2023 Mediatek Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_MEDIATEK_SCALER_FIRMWARE (fu_mediatek_scaler_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuMediatekScalerFirmware, fu_mediatek_scaler_firmware, FU, MEDIATEK_SCALER_FIRMWARE, FuFirmware) FuFirmware * fu_mediatek_scaler_firmware_new(void); fwupd-2.0.10/plugins/mediatek-scaler/fu-mediatek-scaler-plugin.c000066400000000000000000000017511501337203100245330ustar00rootroot00000000000000/* * Copyright 2023 Dell Technologies * Copyright 2023 Mediatek Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-mediatek-scaler-device.h" #include "fu-mediatek-scaler-firmware.h" #include "fu-mediatek-scaler-plugin.h" struct _FuMediatekScalerPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuMediatekScalerPlugin, fu_mediatek_scaler_plugin, FU_TYPE_PLUGIN) static void fu_mediatek_scaler_plugin_init(FuMediatekScalerPlugin *self) { } static void fu_mediatek_scaler_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_udev_subsystem(plugin, "drm"); fu_plugin_add_device_gtype(plugin, FU_TYPE_MEDIATEK_SCALER_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_MEDIATEK_SCALER_FIRMWARE); } static void fu_mediatek_scaler_plugin_class_init(FuMediatekScalerPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_mediatek_scaler_plugin_constructed; } fwupd-2.0.10/plugins/mediatek-scaler/fu-mediatek-scaler-plugin.h000066400000000000000000000004571501337203100245420ustar00rootroot00000000000000/* * Copyright 2023 Dell Technologies * Copyright 2023 Mediatek Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuMediatekScalerPlugin, fu_mediatek_scaler_plugin, FU, MEDIATEK_SCALER_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/mediatek-scaler/fu-mediatek-scaler.rs000066400000000000000000000017401501337203100234370ustar00rootroot00000000000000/* * Copyright 2023 Dell Technologies * Copyright 2023 Mediatek Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #[repr(u8)] enum FuDdcOpcode { GetVcp = 0x01, // standard get vcp feature Req = 0xCC, // vendor specific opcode } #[repr(u8)] enum FuDdcVcpCode { ControllerType = 0xC8, // standard display controller type Priority = 0x90, UpdatePrep = 0xF2, UpdateAck = 0xF3, SetData = 0xF4, GetStaged = 0xF5, SetDataFf = 0xF6, CommitFw = 0xF7, GetIspMode = 0xF8, Reboot = 0xFB, Sum = 0xFE, Version = 0xFF, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructDdcCmd { opcode: FuDdcOpcode = Req, vcp_code: FuDdcVcpCode, } #[repr(u8)] enum FuDdcI2cAddr{ DisplayDevice = 0x6E, HostDevice = 0x51, Checksum = 0x50, } #[derive(ToString)] #[repr(u8)] enum FuDdcciPriority{ Normal, Up, } #[repr(u8)] enum FuMediatekScalerIspStatus { Busy = 0x00, Failure, Success, Idle = 0x99, } fwupd-2.0.10/plugins/mediatek-scaler/mediatek-scaler.quirk000066400000000000000000000001641501337203100235350ustar00rootroot00000000000000[DRM\VEN_DEL&DEV_4340] Plugin = mediatek_scaler Flags = bank2-only [DRM\VEN_DEL&DEV_7430] Plugin = mediatek_scaler fwupd-2.0.10/plugins/mediatek-scaler/meson.build000066400000000000000000000012251501337203100215670ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginMediatekScaler"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('mediatek-scaler.quirk') plugin_builtins += static_library('fu_plugin_mediatek_scaler', rustgen.process( 'fu-mediatek-scaler.rs', # fuzzing ), sources: [ 'fu-mediatek-scaler-device.c', 'fu-mediatek-scaler-firmware.c', 'fu-mediatek-scaler-common.c', 'fu-mediatek-scaler-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: [ cargs, '-DLOCALSTATEDIR="' + localstatedir + '"', ], dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/meson.build000066400000000000000000000102371501337203100165400ustar00rootroot00000000000000# some of these are controlled by meson tristate features plugin_deps = [ gio, giounix, gnutls, gmodule, libusb, libarchive, libjsonglib, libxmlb, libdrm_amdgpu, protobufc, fwupdplugin_rs_dep, ] plugins = { 'uefi-capsule': false, 'acpi-dmar': false, 'acpi-facp': false, 'acpi-ivrs': false, 'acpi-phat': false, 'algoltek-usb': false, 'algoltek-usbcr': false, 'amd-kria': false, 'amd-pmc': false, 'amd-gpu': false, 'analogix': false, 'android-boot': false, 'asus-hid': false, 'ata': false, 'aver-hid': false, 'bcm57xx': false, 'bios': false, 'bnr-dp': false, 'ccgx': false, 'ccgx-dmc': false, 'cfu': false, 'ch341a': false, 'ch347': false, 'corsair': false, 'cpu': false, 'cros-ec': false, 'dell': false, 'dell-dock': false, 'dell-kestrel': false, 'dfu': false, 'ebitdo': false, 'elantp': false, 'elanfp': false, 'elan-kbd': false, 'emmc': false, 'ep963x': false, 'fastboot': false, 'flashrom': false, 'focalfp': false, 'fpc': false, 'fresco-pd': false, 'genesys': false, 'genesys-gl32xx': false, 'goodix-moc': false, 'goodix-tp': false, 'gpio': false, 'hailuck': false, 'hpi-cfu': false, 'huddly-usb': false, 'hughski-colorhug': false, 'intel-amt': false, 'intel-cvs': false, 'intel-gsc': false, 'intel-mchi': false, 'intel-mkhi': false, 'intel-usb4': false, 'iommu': false, 'jabra': false, 'jabra-file': false, 'jabra-gnp': false, 'kinetic-dp': false, 'legion-hid2': false, 'lenovo-thinklmi': false, 'linux-display': false, 'linux-lockdown': false, 'linux-sleep': false, 'linux-swap': false, 'linux-tainted': false, 'logind': false, 'logitech-hidpp': false, 'logitech-bulkcontroller': false, 'logitech-rallysystem': false, 'logitech-scribe': false, 'logitech-tap': false, 'mediatek-scaler': false, 'modem-manager': false, 'msr': false, 'mtd': false, 'nordic-hid': false, 'nvme': false, 'parade-lspcon': false, 'parade-usbhub': false, 'pci-bcr': false, 'pci-mei': false, 'pci-psp': false, 'pixart-rf': false, 'powerd': false, 'qc-firehose': false, 'qc-s5gen2': false, 'qsi-dock': false, 'realtek-mst': false, 'redfish': false, 'rp-pico': false, 'rts54hid': false, 'rts54hub': false, 'steelseries': false, 'scsi': false, 'synaptics-cape': false, 'synaptics-cxaudio': false, 'synaptics-mst': false, 'synaptics-prometheus': false, 'synaptics-rmi': false, 'synaptics-vmm9': false, 'system76-launch': false, 'test': false, 'telink-dfu':false, 'thelio-io': false, 'thunderbolt': false, 'ti-tps6598x': false, 'tpm': false, 'uefi-db': false, 'uefi-dbx': false, 'uefi-esrt': false, 'uefi-kek': false, 'uefi-mok': false, 'uefi-pk': false, 'uefi-recovery': false, 'uefi-sbat': false, 'uf2': false, 'upower': false, 'usi-dock': false, 'vbe': false, 'vli': false, 'wacom-raw': false, 'wacom-usb': false, 'wistron-dock': false, } umockdev_tests = [] umockdev_ioctls = [] device_tests = [] enumeration_data = [] foreach plugin, enabled: plugins subdir(plugin) endforeach if umockdev_integration_tests.allowed() envs = environment() envs.set('CACHE_DIRECTORY', join_paths(meson.project_build_root(), 'cache')) envs.set('DAEMON_BUILDDIR', join_paths(meson.project_build_root(), 'src')) envs.set('FWUPD_DATADIR_QUIRKS', join_paths(meson.project_build_root())) envs.set('GI_TYPELIB_PATH', join_paths(meson.project_build_root(), 'libfwupd')) envs.set('LD_LIBRARY_PATH', join_paths(meson.project_build_root(), 'libfwupd')) envs.set('PYTHONPATH', join_paths(meson.project_source_root(), 'data', 'tests')) envs.set('STATE_DIRECTORY', join_paths(meson.project_build_root(), 'state')) foreach suite: umockdev_tests r = run_command(unittest_inspector, suite, check: true, env: envs) unit_tests = r.stdout().strip().split('\n') foreach ut: unit_tests test(ut, python3, args: [suite, ut], is_parallel: false, env: envs) endforeach install_data(suite, install_dir: installed_test_datadir, ) endforeach foreach ioctl: umockdev_ioctls install_data(ioctl, install_dir: installed_test_datadir, ) endforeach endif fwupd-2.0.10/plugins/modem-manager/000077500000000000000000000000001501337203100171045ustar00rootroot00000000000000fwupd-2.0.10/plugins/modem-manager/README.md000066400000000000000000000107711501337203100203710ustar00rootroot00000000000000--- title: Plugin: ModemManager --- ## Introduction This plugin adds support for devices managed by ModemManager. ## GUID Generation These device use the ModemManager "Firmware Device IDs" as the GUID, e.g. * `USB\VID_413C&PID_81D7&REV_0318&CARRIER_VODAFONE` (only if not using `Flags=use-branch`) * `USB\VID_413C&PID_81D7&REV_0318` * `USB\VID_413C&PID_81D7` * `PCI\VID_105B&PID_E0AB&REV_0000&CARRIER_VODAFONE` (only if not using `Flags=use-branch`) * `PCI\VID_105B&PID_E0AB&REV_0000` * `PCI\VID_105B&PID_E0AB` ## Quirk Use This plugin uses the following plugin-specific quirk: ### ModemManagerBranchAtCommand AT command to execute to determine the firmware branch currently installed on the modem. Since: 1.7.4 ### ModemManagerFirehoseProgFile Firehose program file to use during the QCDM switch to EDL (Emergency Download) mode. Since: 1.8.10 ### `Flags=use-branch` Use the carrier (e.g. `VODAFONE`) as the device branch name so that `fwupdmgr sync` can downgrade the firmware as required. This is now the recommended mode for all modem devices with a carrier-specific firmware image, although it requires that the firmware branch is also set in the firmware metadata. Since: 1.9.8 ### `Flags=detach-at-fastboot-has-no-response` If no AT response is expected when entering fastboot mode. ## Vendor ID Security The vendor ID is set from the USB or PCI vendor, for example `USB:0x413C` `PCI:0x105B` ## Update method: fastboot If the device supports the 'fastboot' update method, it must also report which AT command should be used to trigger the modem reboot into fastboot mode. Once the device is in fastboot mode, the firmware upgrade process will happen as defined e.g. in the 'flashfile.xml' file. Every file included in the CAB that is not listed in the associated 'flashfile.xml' will be totally ignored during the fastboot upgrade procedure. Update Protocol: `com.google.fastboot` For this reason the `REPLUG_MATCH_GUID` internal device flag is used so that the fastboot and runtime modes are treated as the same device. ## Update method: qmi-pdc If the device supports the 'qmi-pdc' update method, the contents of the CAB file should include files named as 'mcfg.*.mbn' which will be treated as MCFG configuration files to download into the device using the Persistent Device Configuration QMI service. If a device supports both 'fastboot' and 'qmi-pdc' methods, the fastboot operation will always be run before the QMI operation, so that e.g. the full partition where the MCFG files are stored can be wiped out before installing the new ones. Update protocol: `com.qualcomm.qmi_pdc` For this reason the `REPLUG_MATCH_GUID` internal device flag is used so that the fastboot and runtime modes are treated as the same device. ## Update method: mbim-qdu If the device supports the 'mbim-qdu' update method, the contents of the CAB file should include a package named as 'Firmware_*.7z' which is a compressed ota.bin file that will be downloaded to the ota partition of the device. Update protocol: `com.qualcomm.mbim_qdu` ## Update method: firehose If the device supports the 'firehose' update method, it should have QCDM port exposed and the contents of the CAB file should contain 'firehose-rawprogram.xml'. The device is then switched to the emergency download mode (EDL) and flashed with files described in 'firehose-rawprogram.xml'. Update protocol: `com.qualcomm.firehose` ## Update method: cinterion-fdl If the device supports the 'cinterion-fdl' update method, it should have an AT-port exposed. The device is then switched to Firmware Download Modem (FDL) and flashed with the content of the firmware file. After an update, the device will not replug until an ignition is sent, or the device is rebooted. Update protocol: `com.cinterion.fdl` ## Update method: dfota If the device supports the 'dfota' update method, it should have an AT-port exposed. The device is then switched to data mode and downloads the firmware to the ota partition via the AT-port. DFOTA updates require a specific firmware version to be installed on the device, since the update only contains a diff between the installed and target version. Update protocol: `com.quectel.dfota` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb` and `/dev/bus/pci`. ## Version Considerations This plugin has been available since fwupd version `1.2.6`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Aleksander Morgado: @aleksander0m fwupd-2.0.10/plugins/modem-manager/fu-mm-backend.c000066400000000000000000000372041501337203100216640ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-mm-backend.h" #include "fu-mm-common.h" #include "fu-mm-fastboot-device.h" #include "fu-mm-firehose-device.h" #include "fu-mm-mhi-qcdm-device.h" #include "fu-mm-qcdm-device.h" #include "fu-mm-dfota-device.h" #include "fu-mm-fdl-device.h" #include "fu-mm-mbim-device.h" #include "fu-mm-qmi-device.h" struct _FuMmBackend { FuBackend parent_instance; MMManager *manager; gboolean manager_ready; GFileMonitor *modem_power_monitor; }; G_DEFINE_TYPE(FuMmBackend, fu_mm_backend, FU_TYPE_BACKEND) /* out-of-tree modem-power driver is unsupported */ #define FU_MM_BACKEND_MODEM_POWER_SYSFS_PATH "/sys/class/modem-power" static void fu_mm_backend_to_string(FuBackend *backend, guint idt, GString *str) { FuMmBackend *self = FU_MM_BACKEND(backend); fwupd_codec_string_append_bool(str, idt, "ManagerReady", self->manager_ready); } static void fu_mm_backend_device_inhibit(FuMmBackend *self, FuMmDevice *device) { const gchar *inhibition_uid = fu_mm_device_get_inhibition_uid(FU_MM_DEVICE(device)); g_autoptr(GError) error_local = NULL; if (inhibition_uid == NULL) return; g_debug("inhibit modemmanager device with uid %s", inhibition_uid); if (!mm_manager_inhibit_device_sync(self->manager, inhibition_uid, NULL, &error_local)) g_debug("ignoring: %s", error_local->message); } static void fu_mm_backend_device_uninhibit(FuMmBackend *self, FuMmDevice *device) { const gchar *inhibition_uid = fu_mm_device_get_inhibition_uid(FU_MM_DEVICE(device)); g_autoptr(GError) error_local = NULL; if (inhibition_uid == NULL) return; g_debug("uninhibit modemmanager device with uid %s", inhibition_uid); if (!mm_manager_uninhibit_device_sync(self->manager, inhibition_uid, NULL, &error_local)) g_debug("ignoring: %s", error_local->message); } static void fu_mm_backend_device_inhibited_notify_cb(FuDevice *device, GParamSpec *pspec, gpointer user_data) { FuMmBackend *self = FU_MM_BACKEND(user_data); if (fu_mm_device_get_inhibited(FU_MM_DEVICE(device))) { fu_mm_backend_device_inhibit(self, FU_MM_DEVICE(device)); return; } fu_mm_backend_device_uninhibit(self, FU_MM_DEVICE(device)); } static FuDevice * fu_mm_backend_probe_gtype(FuMmBackend *self, MMObject *omodem, GError **error) { FuContext *ctx = fu_backend_get_context(FU_BACKEND(self)); MMModemFirmware *modem_fw = mm_object_peek_modem_firmware(omodem); const gchar **device_ids; g_autofree gchar *device_ids_str = NULL; g_autoptr(MMFirmwareUpdateSettings) update_settings = NULL; /* use the instance IDs provided by ModemManager to find the correct GType */ update_settings = mm_modem_firmware_get_update_settings(modem_fw); device_ids = mm_firmware_update_settings_get_device_ids(update_settings); if (device_ids == NULL || device_ids[0] == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "modem did not specify any device IDs"); return NULL; } for (guint i = 0; device_ids[i] != NULL; i++) { g_autofree gchar *guid = fwupd_guid_hash_string(device_ids[i]); const gchar *gtypestr = fu_context_lookup_quirk_by_id(ctx, guid, FU_QUIRKS_GTYPE); if (gtypestr != NULL) { GType gtype = g_type_from_name(gtypestr); if (gtype == G_TYPE_INVALID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unknown GType name %s", gtypestr); return NULL; } return g_object_new(gtype, "context", ctx, NULL); } } /* failed */ device_ids_str = g_strjoinv(", ", (gchar **)device_ids); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no explicit GType for %s", device_ids_str); return NULL; } static FuDevice * fu_mm_backend_probe_gtype_fallback(FuMmBackend *self, MMObject *omodem, GError **error) { MMModem *modem = mm_object_peek_modem(omodem); FuContext *ctx = fu_backend_get_context(FU_BACKEND(self)); MMModemFirmware *modem_fw; MMModemFirmwareUpdateMethod update_methods; MMModemPortInfo *used_ports = NULL; guint n_used_ports = 0; #if MM_CHECK_VERSION(1, 26, 0) MMModemPortInfo *ignored_ports = NULL; guint n_ignored_ports = 0; #endif // MM_CHECK_VERSION(1, 26, 0) const gchar **device_ids; guint64 ports_bitmask = 0; GType gtype = G_TYPE_INVALID; g_autoptr(MMFirmwareUpdateSettings) update_settings = NULL; struct { GType gtype; MMModemPortType port_type; MMModemFirmwareUpdateMethod method; } map[] = { { FU_TYPE_MM_FASTBOOT_DEVICE, MM_MODEM_PORT_TYPE_AT, MM_MODEM_FIRMWARE_UPDATE_METHOD_FASTBOOT, }, { FU_TYPE_MM_QMI_DEVICE, MM_MODEM_PORT_TYPE_QMI, MM_MODEM_FIRMWARE_UPDATE_METHOD_QMI_PDC | MM_MODEM_FIRMWARE_UPDATE_METHOD_FASTBOOT, }, { FU_TYPE_MM_MBIM_DEVICE, MM_MODEM_PORT_TYPE_MBIM, MM_MODEM_FIRMWARE_UPDATE_METHOD_MBIM_QDU, }, { FU_TYPE_MM_QCDM_DEVICE, MM_MODEM_PORT_TYPE_QCDM, MM_MODEM_FIRMWARE_UPDATE_METHOD_MBIM_QDU, }, { FU_TYPE_MM_MHI_QCDM_DEVICE, MM_MODEM_PORT_TYPE_QCDM, MM_MODEM_FIRMWARE_UPDATE_METHOD_FIREHOSE, }, { FU_TYPE_MM_FIREHOSE_DEVICE, MM_MODEM_PORT_TYPE_AT, MM_MODEM_FIRMWARE_UPDATE_METHOD_FIREHOSE | MM_MODEM_FIRMWARE_UPDATE_METHOD_SAHARA, }, { FU_TYPE_MM_FDL_DEVICE, MM_MODEM_PORT_TYPE_AT, MM_MODEM_FIRMWARE_UPDATE_METHOD_CINTERION_FDL, }, { FU_TYPE_MM_DFOTA_DEVICE, MM_MODEM_PORT_TYPE_AT, MM_MODEM_FIRMWARE_UPDATE_METHOD_DFOTA, }, }; modem_fw = mm_object_peek_modem_firmware(omodem); update_settings = mm_modem_firmware_get_update_settings(modem_fw); update_methods = mm_firmware_update_settings_get_method(update_settings); if (update_methods == MM_MODEM_FIRMWARE_UPDATE_METHOD_NONE) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "does not support firmware updates"); return NULL; } if (!mm_modem_get_ports(modem, &used_ports, &n_used_ports)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to get port information"); return NULL; } for (guint i = 0; i < n_used_ports; i++) { g_debug("found port %s: %s", used_ports[i].name, fu_mm_device_port_type_to_string(used_ports[i].type)); FU_BIT_SET(ports_bitmask, used_ports[i].type); } mm_modem_port_info_array_free(used_ports, n_used_ports); #if MM_CHECK_VERSION(1, 26, 0) if (!mm_modem_get_ignored_ports(modem, &ignored_ports, &n_ignored_ports)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to get port information"); return NULL; } for (guint i = 0; i < n_ignored_ports; i++) { g_debug("found port %s: %s", ignored_ports[i].name, fu_mm_device_port_type_to_string(ignored_ports[i].type)); FU_BIT_SET(ports_bitmask, ignored_ports[i].type); } mm_modem_port_info_array_free(ignored_ports, n_ignored_ports); #endif // MM_CHECK_VERSION(1, 26, 0) /* find the correct GType */ for (guint i = 0; i < G_N_ELEMENTS(map); i++) { if (FU_BIT_IS_SET(ports_bitmask, map[i].port_type) && update_methods == map[i].method) { gtype = map[i].gtype; break; } } if (gtype == G_TYPE_INVALID) { g_autofree gchar *methods_str = mm_modem_firmware_update_method_build_string_from_mask(update_methods); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "update method %s not supported", methods_str); return NULL; } /* it's much better to be explicit, so ask the user to provide this information to us */ device_ids = mm_firmware_update_settings_get_device_ids(update_settings); if (device_ids != NULL && device_ids[0] != NULL) { g_autofree gchar *device_ids_str = g_strjoinv(", ", (gchar **)device_ids); #ifdef SUPPORTED_BUILD g_debug("no explicit GType for %s, falling back to %s", device_ids_str, g_type_name(gtype)); #else g_warning("no explicit GType for %s, falling back to %s", device_ids_str, g_type_name(gtype)); g_warning("Please see " "https://github.com/fwupd/fwupd/wiki/Daemon-Warning:-FuMmDevice-GType"); #endif } /* success */ return g_object_new(gtype, "context", ctx, NULL); } static FuDevice * fu_mm_backend_device_create_from_omodem(FuMmBackend *self, MMObject *omodem, GError **error) { g_autoptr(FuDevice) device = NULL; g_autoptr(GError) error_local = NULL; /* create device and probe */ device = fu_mm_backend_probe_gtype(self, omodem, &error_local); if (device == NULL) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_debug("ignoring, and trying legacy fallback: %s", error_local->message); device = fu_mm_backend_probe_gtype_fallback(self, omodem, error); if (device == NULL) return NULL; } else { g_propagate_error(error, g_steal_pointer(&error_local)); return NULL; } } if (!fu_mm_device_probe_from_omodem(FU_MM_DEVICE(device), omodem, error)) return NULL; /* fastboot extra properties */ if (FU_IS_MM_FASTBOOT_DEVICE(device)) { MMModemFirmware *modem_fw = mm_object_peek_modem_firmware(omodem); g_autoptr(MMFirmwareUpdateSettings) update_settings = mm_modem_firmware_get_update_settings(modem_fw); const gchar *tmp = mm_firmware_update_settings_get_fastboot_at(update_settings); if (tmp == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "modem does not set fastboot command"); return NULL; } fu_mm_fastboot_device_set_detach_at(FU_MM_FASTBOOT_DEVICE(device), tmp); } /* success */ return g_steal_pointer(&device); } static void fu_mm_backend_ensure_modem_power_inhibit(FuMmBackend *self, FuDevice *device) { if (g_file_test(FU_MM_BACKEND_MODEM_POWER_SYSFS_PATH, G_FILE_TEST_EXISTS)) { fu_device_inhibit(device, "modem-power", "The modem-power kernel driver cannot be used"); } else { fu_device_uninhibit(device, "modem-power"); } } static void fu_mm_backend_device_add(FuMmBackend *self, MMObject *omodem) { g_autoptr(GError) error = NULL; g_autoptr(FuDevice) device = NULL; device = fu_mm_backend_device_create_from_omodem(self, omodem, &error); if (device == NULL) { g_debug("ignoring: %s", error->message); return; } /* inhibit MmManager when required */ g_signal_connect(device, "notify::inhibited", G_CALLBACK(fu_mm_backend_device_inhibited_notify_cb), self); fu_mm_backend_ensure_modem_power_inhibit(self, device); fu_backend_device_added(FU_BACKEND(self), device); } static void fu_mm_backend_device_added_cb(MMManager *manager, MMObject *omodem, FuMmBackend *self) { FuDevice *device = fu_backend_lookup_by_id(FU_BACKEND(self), mm_object_get_path(omodem)); if (device != NULL) { g_autoptr(GError) error_local = NULL; g_debug("modem came back, rescanning"); if (!fu_mm_device_probe_from_omodem(FU_MM_DEVICE(device), omodem, &error_local)) g_debug("ignoring: %s", error_local->message); // FIXME: perhaps need to mm_firmware_update_settings_get_fastboot_at() } fu_mm_backend_device_add(self, omodem); } static void fu_mm_backend_device_removed_cb(MMManager *manager, MMObject *omodem, FuBackend *backend) { FuDevice *device = fu_backend_lookup_by_id(backend, mm_object_get_path(omodem)); if (device == NULL) return; if (fu_mm_device_get_inhibited(FU_MM_DEVICE(device))) { g_debug("inhibited modem %s, ignoring", fu_device_get_backend_id(device)); return; } g_debug("removed modem: %s", fu_device_get_backend_id(device)); fu_backend_device_removed(backend, device); } static void fu_mm_backend_modem_power_changed_cb(GFileMonitor *monitor, GFile *file, GFile *other_file, GFileMonitorEvent event_type, gpointer user_data) { FuMmBackend *self = FU_MM_BACKEND(user_data); GPtrArray *devices = fu_backend_get_devices(FU_BACKEND(self)); for (guint i = 0; i < devices->len; i++) { FuDevice *device = g_ptr_array_index(devices, i); fu_mm_backend_ensure_modem_power_inhibit(self, device); } } static gboolean fu_mm_backend_setup(FuBackend *backend, FuBackendSetupFlags flags, FuProgress *progress, GError **error) { FuMmBackend *self = FU_MM_BACKEND(backend); g_autoptr(GDBusConnection) connection = NULL; g_autoptr(GFile) file = g_file_new_for_path(FU_MM_BACKEND_MODEM_POWER_SYSFS_PATH); connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, error); if (connection == NULL) return FALSE; self->manager = mm_manager_new_sync(connection, G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START, NULL, error); if (self->manager == NULL) return FALSE; /* detect presence of unsupported modem-power driver */ self->modem_power_monitor = g_file_monitor(file, G_FILE_MONITOR_NONE, NULL, error); if (self->modem_power_monitor == NULL) return FALSE; g_signal_connect(self->modem_power_monitor, "changed", G_CALLBACK(fu_mm_backend_modem_power_changed_cb), self); /* success */ return TRUE; } static void fu_mm_backend_teardown_manager(FuMmBackend *self) { if (self->manager_ready) { g_debug("ModemManager no longer available"); g_signal_handlers_disconnect_by_func(self->manager, G_CALLBACK(fu_mm_backend_device_added_cb), self); g_signal_handlers_disconnect_by_func(self->manager, G_CALLBACK(fu_mm_backend_device_removed_cb), self); self->manager_ready = FALSE; } } static void fu_mm_backend_setup_manager(FuMmBackend *self) { const gchar *version = mm_manager_get_version(self->manager); g_autolist(MMObject) list = NULL; if (fu_version_compare(version, MM_REQUIRED_VERSION, FWUPD_VERSION_FORMAT_TRIPLET) < 0) { g_warning("ModemManager %s is available, but need at least %s", version, MM_REQUIRED_VERSION); return; } g_info("ModemManager %s is available", version); g_signal_connect(G_DBUS_OBJECT_MANAGER(self->manager), "object-added", G_CALLBACK(fu_mm_backend_device_added_cb), self); g_signal_connect(G_DBUS_OBJECT_MANAGER(self->manager), "object-removed", G_CALLBACK(fu_mm_backend_device_removed_cb), self); list = g_dbus_object_manager_get_objects(G_DBUS_OBJECT_MANAGER(self->manager)); for (GList *l = list; l != NULL; l = g_list_next(l)) { MMObject *modem = MM_OBJECT(l->data); fu_mm_backend_device_add(self, modem); } self->manager_ready = TRUE; } static void fu_mm_backend_name_owner_changed(FuMmBackend *self) { g_autofree gchar *name_owner = g_dbus_object_manager_client_get_name_owner( G_DBUS_OBJECT_MANAGER_CLIENT(self->manager)); if (name_owner != NULL) fu_mm_backend_setup_manager(self); else fu_mm_backend_teardown_manager(self); } static void fu_mm_backend_name_owner_notify_cb(MMManager *manager, GParamSpec *pspec, FuMmBackend *self) { fu_mm_backend_name_owner_changed(self); } static gboolean fu_mm_backend_coldplug(FuBackend *backend, FuProgress *progress, GError **error) { FuMmBackend *self = FU_MM_BACKEND(backend); g_signal_connect(MM_MANAGER(self->manager), "notify::name-owner", G_CALLBACK(fu_mm_backend_name_owner_notify_cb), self); fu_mm_backend_name_owner_changed(self); return TRUE; } static void fu_mm_backend_init(FuMmBackend *self) { } static void fu_mm_backend_finalize(GObject *object) { FuMmBackend *self = FU_MM_BACKEND(object); if (self->manager != NULL) g_object_unref(self->manager); if (self->modem_power_monitor != NULL) g_object_unref(self->modem_power_monitor); G_OBJECT_CLASS(fu_mm_backend_parent_class)->finalize(object); } static void fu_mm_backend_class_init(FuMmBackendClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuBackendClass *backend_class = FU_BACKEND_CLASS(klass); object_class->finalize = fu_mm_backend_finalize; backend_class->to_string = fu_mm_backend_to_string; backend_class->setup = fu_mm_backend_setup; backend_class->coldplug = fu_mm_backend_coldplug; } FuBackend * fu_mm_backend_new(FuContext *ctx) { return g_object_new(FU_TYPE_MM_BACKEND, "name", "modem-manager", "context", ctx, NULL); } fwupd-2.0.10/plugins/modem-manager/fu-mm-backend.h000066400000000000000000000005121501337203100216610ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_MM_BACKEND (fu_mm_backend_get_type()) G_DECLARE_FINAL_TYPE(FuMmBackend, fu_mm_backend, FU, MM_BACKEND, FuBackend) FuBackend * fu_mm_backend_new(FuContext *ctx); fwupd-2.0.10/plugins/modem-manager/fu-mm-common.c000066400000000000000000000024221501337203100215570ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-mm-common.h" const gchar * fu_mm_device_port_type_to_string(MMModemPortType port_type) { if (port_type == MM_MODEM_PORT_TYPE_NET) return "net"; if (port_type == MM_MODEM_PORT_TYPE_AT) return "at"; if (port_type == MM_MODEM_PORT_TYPE_QCDM) return "qcdm"; if (port_type == MM_MODEM_PORT_TYPE_GPS) return "gps"; if (port_type == MM_MODEM_PORT_TYPE_QMI) return "qmi"; if (port_type == MM_MODEM_PORT_TYPE_MBIM) return "mbim"; if (port_type == MM_MODEM_PORT_TYPE_IGNORED) return "ignored"; return NULL; } MMModemPortType fu_mm_device_port_type_from_string(const gchar *port_type) { if (g_strcmp0(port_type, "net") == 0) return MM_MODEM_PORT_TYPE_NET; if (g_strcmp0(port_type, "at") == 0) return MM_MODEM_PORT_TYPE_AT; if (g_strcmp0(port_type, "qcdm") == 0) return MM_MODEM_PORT_TYPE_QCDM; if (g_strcmp0(port_type, "gps") == 0) return MM_MODEM_PORT_TYPE_GPS; if (g_strcmp0(port_type, "qmi") == 0) return MM_MODEM_PORT_TYPE_QMI; if (g_strcmp0(port_type, "mbim") == 0) return MM_MODEM_PORT_TYPE_MBIM; if (g_strcmp0(port_type, "ignored") == 0) return MM_MODEM_PORT_TYPE_IGNORED; return MM_MODEM_PORT_TYPE_UNKNOWN; } fwupd-2.0.10/plugins/modem-manager/fu-mm-common.h000066400000000000000000000005101501337203100215600ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include const gchar * fu_mm_device_port_type_to_string(MMModemPortType port_type); MMModemPortType fu_mm_device_port_type_from_string(const gchar *port_type); fwupd-2.0.10/plugins/modem-manager/fu-mm-device.c000066400000000000000000000601071501337203100215320ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-mm-common.h" #include "fu-mm-device.h" /** * FuMmDevice * * A modem manager device. * * See also: #FuUdevDevice */ /* not strictly last, but the last we care about */ #define MM_MODEM_PORT_TYPE_LAST (MM_MODEM_PORT_TYPE_IGNORED + 1) typedef struct { gboolean inhibited; gchar *branch_at; gchar *inhibition_uid; gchar *port[MM_MODEM_PORT_TYPE_LAST]; } FuMmDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuMmDevice, fu_mm_device, FU_TYPE_UDEV_DEVICE); #define GET_PRIVATE(o) (fu_mm_device_get_instance_private(o)) #define FU_MM_DEVICE_AT_RETRIES 3 #define FU_MM_DEVICE_AT_DELAY 3000 /* ms */ enum { PROP_0, PROP_INHIBITED, PROP_LAST }; static void fu_mm_device_set_branch_at(FuMmDevice *self, const gchar *branch_at) { FuMmDevicePrivate *priv = GET_PRIVATE(self); if (g_strcmp0(priv->branch_at, branch_at) == 0) return; g_free(priv->branch_at); priv->branch_at = g_strdup(branch_at); } static void fu_mm_device_to_string(FuDevice *device, guint idt, GString *str) { FuMmDevice *self = FU_MM_DEVICE(device); FuMmDevicePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append(str, idt, "BranchAt", priv->branch_at); fwupd_codec_string_append_bool(str, idt, "Inhibited", priv->inhibited); fwupd_codec_string_append(str, idt, "InhibitionUid", priv->inhibition_uid); for (guint i = 0; i < MM_MODEM_PORT_TYPE_LAST; i++) { if (priv->port[i] != NULL) { g_autofree gchar *title = g_strdup_printf("Port[%s]", fu_mm_device_port_type_to_string(i)); fwupd_codec_string_append(str, idt, title, priv->port[i]); } } } const gchar * fu_mm_device_get_inhibition_uid(FuMmDevice *self) { FuMmDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_MM_DEVICE(self), NULL); return priv->inhibition_uid; } void fu_mm_device_set_inhibited(FuMmDevice *self, gboolean inhibited) { FuMmDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_MM_DEVICE(self)); if (priv->inhibited == inhibited) return; priv->inhibited = inhibited; g_object_notify(G_OBJECT(self), "inhibited"); } gboolean fu_mm_device_get_inhibited(FuMmDevice *self) { FuMmDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_MM_DEVICE(self), FALSE); return priv->inhibited; } gboolean fu_mm_device_set_device_file(FuMmDevice *self, MMModemPortType port_type, GError **error) { FuMmDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_MM_DEVICE(self), FALSE); g_return_val_if_fail(port_type < MM_MODEM_PORT_TYPE_LAST, FALSE); if (priv->port[port_type] == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no port for %s", fu_mm_device_port_type_to_string(port_type)); return FALSE; } fu_udev_device_set_device_file(FU_UDEV_DEVICE(self), priv->port[port_type]); return TRUE; } static gboolean fu_mm_device_writeln(const gchar *fn, const gchar *buf, GError **error) { g_autoptr(FuIOChannel) io = NULL; io = fu_io_channel_new_file(fn, FU_IO_CHANNEL_OPEN_FLAG_WRITE, error); if (io == NULL) return FALSE; return fu_io_channel_write_raw(io, (const guint8 *)buf, strlen(buf), 1000, FU_IO_CHANNEL_FLAG_NONE, error); } gboolean fu_mm_device_set_autosuspend_delay(FuMmDevice *self, guint timeout_ms, GError **error) { g_autofree gchar *autosuspend_delay_filename = NULL; g_autofree gchar *buf = g_strdup_printf("%u", timeout_ms); /* autosuspend delay updated for a proper firmware update */ autosuspend_delay_filename = g_build_filename(fu_device_get_physical_id(FU_DEVICE(self)), "/power/autosuspend_delay_ms", NULL); if (!g_file_test(autosuspend_delay_filename, G_FILE_TEST_EXISTS)) { g_debug("%s does not exist, so skipping", autosuspend_delay_filename); return TRUE; } return fu_mm_device_writeln(autosuspend_delay_filename, buf, error); } static void fu_mm_device_add_instance_id(FuMmDevice *self, const gchar *device_id) { if (g_pattern_match_simple("???\\VID_????", device_id)) { g_autofree gchar *vendor_id = g_strdup_printf("USB:0x%s", device_id + 8); fu_device_add_instance_id_full(FU_DEVICE(self), device_id, FU_DEVICE_INSTANCE_FLAG_QUIRKS); fu_device_add_vendor_id(FU_DEVICE(self), vendor_id); return; } if (g_pattern_match_simple("???\\VID_????&PID_????", device_id) || g_pattern_match_simple("???\\VID_????&PID_????&NAME_*", device_id)) { fu_device_add_instance_id(FU_DEVICE(self), device_id); return; } if (g_pattern_match_simple("???\\VID_????&PID_????&REV_????", device_id) || g_pattern_match_simple("???\\VID_????&PID_????&REV_????&NAME_*", device_id)) { if (fu_device_has_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_INSTANCE_ID_REV)) fu_device_add_instance_id(FU_DEVICE(self), device_id); return; } if (g_pattern_match_simple("???\\VID_????&PID_????&REV_????&CARRIER_*", device_id)) { if (!fu_device_has_private_flag(FU_DEVICE(self), FU_MM_DEVICE_FLAG_USE_BRANCH)) fu_device_add_instance_id(FU_DEVICE(self), device_id); return; } if (g_pattern_match_simple("???\\SSVID_????&SSPID_????&REV_????&CARRIER_*", device_id)) { if (!fu_device_has_private_flag(FU_DEVICE(self), FU_MM_DEVICE_FLAG_USE_BRANCH)) fu_device_add_instance_id(FU_DEVICE(self), device_id); return; } g_warning("failed to add instance ID %s", device_id); } static void fu_mm_device_add_port(FuMmDevice *self, MMModemPortType port_type, const gchar *device_file) { FuMmDevicePrivate *priv = GET_PRIVATE(self); if (port_type >= MM_MODEM_PORT_TYPE_LAST) return; if (priv->port[port_type] != NULL) return; priv->port[port_type] = g_strdup(device_file); } gboolean fu_mm_device_probe_from_omodem(FuMmDevice *self, MMObject *omodem, GError **error) { FuMmDevicePrivate *priv = GET_PRIVATE(self); MMModemFirmware *modem_fw = mm_object_peek_modem_firmware(omodem); MMModem *modem = mm_object_peek_modem(omodem); MMModemPortInfo *used_ports = NULL; guint n_used_ports = 0; #if MM_CHECK_VERSION(1, 26, 0) MMModemPortInfo *ignored_ports = NULL; guint n_ignored_ports = 0; #endif // MM_CHECK_VERSION(1, 26, 0) const gchar **device_ids; const gchar *sysfs_path; const gchar *version; g_autoptr(MMFirmwareUpdateSettings) update_settings = NULL; /* inhibition uid is the modem interface 'Device' property, which may * be the device sysfs path or a different user-provided id */ priv->inhibition_uid = mm_modem_dup_device(modem); /* get the sysfs path for the MM physical device */ sysfs_path = mm_modem_get_physdev(modem); if (sysfs_path == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no physdev set"); return FALSE; } fu_device_set_physical_id(FU_DEVICE(self), sysfs_path); /* get GUIDs */ update_settings = mm_modem_firmware_get_update_settings(modem_fw); device_ids = mm_firmware_update_settings_get_device_ids(update_settings); if (device_ids == NULL || device_ids[0] == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "modem did not specify any device IDs"); return FALSE; } /* get version string, which is fw_ver+config_ver */ version = mm_firmware_update_settings_get_version(update_settings); if (version == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "modem did not specify a firmware version"); return FALSE; } fu_device_set_backend_id(FU_DEVICE(self), mm_object_get_path(omodem)); /* look for the AT and QMI/MBIM ports */ if (!mm_modem_get_ports(modem, &used_ports, &n_used_ports)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to get port information"); return FALSE; } for (guint i = 0; i < n_used_ports; i++) { g_autofree gchar *device_file = g_strdup_printf("/dev/%s", used_ports[i].name); if (used_ports[i].type >= MM_MODEM_PORT_TYPE_LAST) continue; if (used_ports[i].type == MM_MODEM_PORT_TYPE_IGNORED && g_pattern_match_simple("wwan*qcdm*", used_ports[i].name)) { fu_mm_device_add_port(self, MM_MODEM_PORT_TYPE_QCDM, device_file); } else { fu_mm_device_add_port(self, used_ports[i].type, device_file); } } mm_modem_port_info_array_free(used_ports, n_used_ports); #if MM_CHECK_VERSION(1, 26, 0) if (!mm_modem_get_ignored_ports(modem, &ignored_ports, &n_ignored_ports)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to get ignored port information"); return FALSE; } for (guint i = 0; i < n_ignored_ports; i++) { g_autofree gchar *device_file = g_strdup_printf("/dev/%s", ignored_ports[i].name); if (ignored_ports[i].type >= MM_MODEM_PORT_TYPE_LAST) continue; fu_mm_device_add_port(self, ignored_ports[i].type, device_file); } mm_modem_port_info_array_free(ignored_ports, n_ignored_ports); #endif // MM_CHECK_VERSION(1, 26, 0) /* add properties to fwupd device */ if (mm_modem_get_manufacturer(modem) != NULL) fu_device_set_vendor(FU_DEVICE(self), mm_modem_get_manufacturer(modem)); if (mm_modem_get_model(modem) != NULL) fu_device_set_name(FU_DEVICE(self), mm_modem_get_model(modem)); /* only for modems that opt-in */ if (fu_device_has_private_flag(FU_DEVICE(self), FU_MM_DEVICE_FLAG_USE_BRANCH)) fu_device_set_branch(FU_DEVICE(self), mm_modem_get_carrier_configuration(modem)); fu_device_set_version(FU_DEVICE(self), version); /* filter these */ for (guint i = 0; device_ids[i] != NULL; i++) fu_mm_device_add_instance_id(self, device_ids[i]); /* fix up vendor name */ if (g_strcmp0(fu_device_get_vendor(FU_DEVICE(self)), "QUALCOMM INCORPORATED") == 0) fu_device_set_vendor(FU_DEVICE(self), "Qualcomm"); /* success */ return TRUE; } typedef struct { const gchar *cmd; gsize count; gboolean has_response; GBytes *blob; } FuMmDeviceAtCmdHelper; static gboolean fu_mm_device_at_cmd_cb(FuDevice *device, gpointer user_data, GError **error) { FuMmDevice *self = FU_MM_DEVICE(device); FuMmDeviceAtCmdHelper *helper = (FuMmDeviceAtCmdHelper *)user_data; const gchar *buf; gsize bufsz = 0; g_autofree gchar *at_res_safe = NULL; g_autofree gchar *cmd_cr = g_strdup_printf("%s\r\n", helper->cmd); g_autoptr(GBytes) at_req = NULL; g_autoptr(GBytes) at_res = NULL; /* command */ g_debug("req: %s", helper->cmd); at_req = g_bytes_new(cmd_cr, strlen(cmd_cr)); if (!fu_udev_device_write_bytes(FU_UDEV_DEVICE(self), at_req, 1500, FU_IO_CHANNEL_FLAG_FLUSH_INPUT, error)) { g_prefix_error(error, "failed to write %s: ", helper->cmd); return FALSE; } /* AT command has no response, return TRUE */ if (!helper->has_response) { g_debug("no response expected for AT command: '%s', assuming succeed", helper->cmd); return TRUE; } /* response */ at_res = fu_udev_device_read_bytes(FU_UDEV_DEVICE(self), helper->count, 1500, FU_IO_CHANNEL_FLAG_SINGLE_SHOT, error); if (at_res == NULL) { g_prefix_error(error, "failed to read response for %s: ", helper->cmd); return FALSE; } at_res_safe = fu_strsafe_bytes(at_res, 32); g_debug("res: %s", at_res_safe); /* * the first time the modem returns may be the command itself with one \n missing. * this is because the modem AT has enabled echo */ buf = g_bytes_get_data(at_res, &bufsz); if (g_strrstr_len(buf, bufsz, helper->cmd) != NULL && bufsz == strlen(helper->cmd) + 1) { g_bytes_unref(at_res); at_res = fu_udev_device_read_bytes(FU_UDEV_DEVICE(self), helper->count, 1500, FU_IO_CHANNEL_FLAG_SINGLE_SHOT, error); if (at_res == NULL) { g_prefix_error(error, "failed to read response for %s: ", helper->cmd); return FALSE; } buf = g_bytes_get_data(at_res, &bufsz); } if (bufsz < 6) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to read valid response for %s", helper->cmd); return FALSE; } /* return error if AT command failed */ if (g_strrstr_len(buf, bufsz, "\r\nOK\r\n") == NULL && g_strrstr_len(buf, bufsz, "\r\nCONNECT\r\n") == NULL) { g_autofree gchar *tmp = g_strndup(buf + 2, bufsz - 4); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to read valid response for %s: %s", helper->cmd, tmp); return FALSE; } /* success */ helper->blob = g_steal_pointer(&at_res); return TRUE; } gboolean fu_mm_device_at_cmd(FuMmDevice *self, const gchar *cmd, gboolean has_response, GError **error) { FuMmDeviceAtCmdHelper helper = {.cmd = cmd, .count = 64, .has_response = has_response}; if (!fu_device_retry_full(FU_DEVICE(self), fu_mm_device_at_cmd_cb, FU_MM_DEVICE_AT_RETRIES, FU_MM_DEVICE_AT_DELAY, &helper, error)) return FALSE; /* success, but remove the perhaps-unused buffer */ if (helper.blob != NULL) g_bytes_unref(helper.blob); return TRUE; } static GBytes * fu_mm_device_at_cmd_full(FuMmDevice *self, const gchar *cmd, gsize count, GError **error) { FuMmDeviceAtCmdHelper helper = {.cmd = cmd, .count = count, .has_response = TRUE}; if (!fu_device_retry_full(FU_DEVICE(self), fu_mm_device_at_cmd_cb, FU_MM_DEVICE_AT_RETRIES, FU_MM_DEVICE_AT_DELAY, &helper, error)) return NULL; return helper.blob; } static gboolean fu_mm_device_ensure_branch(FuMmDevice *self, GError **error) { FuMmDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(GBytes) blob = NULL; g_auto(GStrv) parts = NULL; /* nothing to do if there is no AT port available or * ModemManagerBranchAtCommand quirk is not set */ if (priv->branch_at == NULL) return TRUE; /* not supported if the devices is signed */ if (fu_device_has_flag(self, FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD)) return TRUE; /* example AT+GETFWBRANCH response: "\r\nFOSS-002 \r\n\r\nOK\r\n" */ blob = fu_mm_device_at_cmd_full(self, priv->branch_at, 64, error); if (blob == NULL) return FALSE; parts = fu_strsplit_bytes(blob, "\r\n", -1); for (guint i = 0; parts[i] != NULL; i++) { if (g_strcmp0(parts[i], "") != 0 && g_strcmp0(parts[i], "OK") != 0) { g_info("firmware branch reported as '%s'", parts[i]); fu_device_set_branch(FU_DEVICE(self), parts[i]); break; } } /* success */ return TRUE; } static void fu_mm_device_ensure_payload_quectel(FuMmDevice *self) { const gchar *version = fu_device_get_version(FU_DEVICE(self)); g_autofree gchar *name = NULL; g_autoptr(GError) error_qsec = NULL; g_autoptr(GError) error_qcfg = NULL; g_autoptr(GBytes) blob = NULL; const gchar *signed_versions[] = {"EM05GFAR07A07M1G_01.005.01.005", "EM05CEFCR08A16M1G_LNV"}; /* newer firmware */ blob = fu_mm_device_at_cmd_full(self, "AT+QSECBOOT=\"status\"", 64, &error_qsec); if (blob == NULL) { g_debug("ignoring: %s", error_qsec->message); } else { /* AT+QSECBOOT="status" response: `\r\n+QSECBOOT: "STATUS",1\r\n\r\nOK\r\n` */ g_auto(GStrv) parts = fu_strsplit_bytes(blob, "\r\n", -1); for (guint i = 0; parts[i] != NULL; i++) { if (g_strcmp0(parts[i], "+QSECBOOT: \"status\",1") == 0) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); break; } if (g_strcmp0(parts[i], "+QSECBOOT: \"status\",0") == 0) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); break; } } return; } /* older firmware */ blob = fu_mm_device_at_cmd_full(self, "AT+QCFG=\"secbootstat\"", 64, &error_qcfg); if (blob == NULL) { g_debug("ignoring: %s", error_qcfg->message); } else { g_auto(GStrv) parts = fu_strsplit_bytes(blob, "\r\n", -1); for (guint i = 0; parts[i] != NULL; i++) { if (g_strcmp0(parts[i], "+QCFG: \"secbootstat\",1") == 0) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); break; } if (g_strcmp0(parts[i], "+QCFG: \"secbootstat\",0") == 0) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); break; } } return; } /* find model name and compare with table from Quectel */ if (version == NULL) return; for (guint i = 0; i < G_N_ELEMENTS(signed_versions); i++) { if (strncmp(version, signed_versions[i], 6) == 0) { if (fu_version_compare(version, signed_versions[i], FWUPD_VERSION_FORMAT_PLAIN) >= 0) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); } else { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); } return; } } } static void fu_mm_device_ensure_payload(FuMmDevice *self) { if (fu_device_has_vendor_id(FU_DEVICE(self), "USB:0x2C7C") || fu_device_has_vendor_id(FU_DEVICE(self), "PCI:0x1EAC")) { fu_mm_device_ensure_payload_quectel(self); } else if (fu_device_has_vendor_id(FU_DEVICE(self), "USB:0x2CB7")) { fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_SAVE_INTO_BACKUP_REMOTE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); } } static gboolean fu_mm_device_setup(FuDevice *device, GError **error) { FuMmDevice *self = FU_MM_DEVICE(device); g_autoptr(GError) error_local = NULL; if (!fu_mm_device_ensure_branch(self, &error_local)) g_debug("failed to set firmware branch: %s", error_local->message); fu_mm_device_ensure_payload(self); /* success */ return TRUE; } static gboolean fu_mm_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuMmDevice *self = FU_MM_DEVICE(device); if (g_strcmp0(key, "ModemManagerBranchAtCommand") == 0) { fu_mm_device_set_branch_at(self, value); return TRUE; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static gboolean fu_mm_device_from_json(FuDevice *device, JsonObject *json_object, GError **error) { FuMmDevice *self = FU_MM_DEVICE(device); const gchar *tmp; /* FuUdevDevice->from_json */ if (!FU_DEVICE_CLASS(fu_mm_device_parent_class)->from_json(device, json_object, error)) return FALSE; /* optional properties */ tmp = json_object_get_string_member_with_default(json_object, "Version", NULL); if (tmp != NULL) fu_device_set_version(device, tmp); tmp = json_object_get_string_member_with_default(json_object, "PhysicalId", NULL); if (tmp != NULL) fu_device_set_physical_id(device, tmp); tmp = json_object_get_string_member_with_default(json_object, "BranchAt", NULL); if (tmp != NULL) fu_mm_device_set_branch_at(self, tmp); /* specified by ModemManager, unusually */ if (json_object_has_member(json_object, "DeviceIds")) { JsonArray *json_array = json_object_get_array_member(json_object, "DeviceIds"); for (guint i = 0; i < json_array_get_length(json_array); i++) { const gchar *instance_id = json_array_get_string_element(json_array, i); fu_mm_device_add_instance_id(self, instance_id); } } /* ports */ if (json_object_has_member(json_object, "Ports")) { JsonObject *json_ports = json_object_get_object_member(json_object, "Ports"); g_autoptr(GList) keys = json_object_get_members(json_ports); for (GList *l = keys; l != NULL; l = l->next) { const gchar *port_type = l->data; fu_mm_device_add_port(self, fu_mm_device_port_type_from_string(port_type), json_object_get_string_member(json_ports, port_type)); } } /* success */ return TRUE; } static void fu_mm_device_add_json(FuDevice *device, JsonBuilder *builder, FwupdCodecFlags flags) { FuMmDevice *self = FU_MM_DEVICE(device); FuMmDevicePrivate *priv = GET_PRIVATE(self); GPtrArray *instance_ids = fu_device_get_instance_ids(device); GPtrArray *vendor_ids = fu_device_get_vendor_ids(device); /* FuUdevDevice->add_json */ FU_DEVICE_CLASS(fu_mm_device_parent_class)->add_json(device, builder, flags); /* optional properties */ fwupd_codec_json_append(builder, "GType", G_OBJECT_TYPE_NAME(self)); if (fu_device_get_version(device) != NULL) fwupd_codec_json_append(builder, "Version", fu_device_get_version(device)); if (fu_device_get_physical_id(device) != NULL) fwupd_codec_json_append(builder, "PhysicalId", fu_device_get_physical_id(device)); if (priv->branch_at != NULL) fwupd_codec_json_append(builder, "BranchAt", priv->branch_at); /* specified by ModemManager, unusually */ json_builder_set_member_name(builder, "DeviceIds"); json_builder_begin_array(builder); for (guint i = 0; i < instance_ids->len; i++) { const gchar *instance_id = g_ptr_array_index(instance_ids, i); json_builder_add_string_value(builder, instance_id); } for (guint i = 0; i < vendor_ids->len; i++) { const gchar *vendor_id = g_ptr_array_index(vendor_ids, i); if (g_str_has_prefix(vendor_id, "USB:0x")) { g_autofree gchar *id = g_strdup_printf("USB\\VID_%s", vendor_id + 6); json_builder_add_string_value(builder, id); } if (g_str_has_prefix(vendor_id, "PCI:0x")) { g_autofree gchar *id = g_strdup_printf("PCI\\VEN_%s", vendor_id + 6); json_builder_add_string_value(builder, id); } } json_builder_end_array(builder); /* ports always specified */ json_builder_set_member_name(builder, "Ports"); json_builder_begin_object(builder); for (guint i = 0; i < MM_MODEM_PORT_TYPE_LAST; i++) { if (priv->port[i] != NULL) { fwupd_codec_json_append(builder, fu_mm_device_port_type_to_string(i), priv->port[i]); } } json_builder_end_object(builder); } static void fu_mm_device_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuMmDevice *self = FU_MM_DEVICE(object); FuMmDevicePrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_INHIBITED: g_value_set_boolean(value, priv->inhibited); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_mm_device_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuMmDevice *self = FU_MM_DEVICE(object); switch (prop_id) { case PROP_INHIBITED: fu_mm_device_set_inhibited(self, g_value_get_boolean(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_mm_device_init(FuMmDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_ARCHIVE_FIRMWARE); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_RUNTIME_VERSION); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_VERFMT); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_INSTANCE_ID_REV); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_summary(FU_DEVICE(self), "Mobile broadband device"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_MODEM); fu_device_register_private_flag(FU_DEVICE(self), FU_MM_DEVICE_FLAG_USE_BRANCH); fu_device_add_possible_plugin(FU_DEVICE(self), "modem_manager"); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); } static void fu_mm_device_finalize(GObject *object) { FuMmDevice *self = FU_MM_DEVICE(object); FuMmDevicePrivate *priv = GET_PRIVATE(self); for (guint i = 0; i < MM_MODEM_PORT_TYPE_LAST; i++) g_free(priv->port[i]); g_free(priv->branch_at); g_free(priv->inhibition_uid); G_OBJECT_CLASS(fu_mm_device_parent_class)->finalize(object); } static void fu_mm_device_class_init(FuMmDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); GParamSpec *pspec; object_class->finalize = fu_mm_device_finalize; object_class->get_property = fu_mm_device_get_property; object_class->set_property = fu_mm_device_set_property; device_class->setup = fu_mm_device_setup; device_class->to_string = fu_mm_device_to_string; device_class->set_quirk_kv = fu_mm_device_set_quirk_kv; device_class->from_json = fu_mm_device_from_json; device_class->add_json = fu_mm_device_add_json; pspec = g_param_spec_boolean("inhibited", NULL, NULL, FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_INHIBITED, pspec); } fwupd-2.0.10/plugins/modem-manager/fu-mm-device.h000066400000000000000000000025041501337203100215340ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include #define FU_TYPE_MM_DEVICE (fu_mm_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuMmDevice, fu_mm_device, FU, MM_DEVICE, FuUdevDevice) #define FU_MM_DEVICE_FLAG_USE_BRANCH "use-branch" /* less ifdefs */ #if !MM_CHECK_VERSION(1, 24, 0) #define MM_MODEM_FIRMWARE_UPDATE_METHOD_DFOTA (1 << 5) #define MM_MODEM_FIRMWARE_UPDATE_METHOD_CINTERION_FDL (1 << 6) #endif struct _FuMmDeviceClass { FuUdevDeviceClass parent_class; }; void fu_mm_device_set_inhibited(FuMmDevice *self, gboolean inhibited) G_GNUC_NON_NULL(1); gboolean fu_mm_device_get_inhibited(FuMmDevice *self) G_GNUC_NON_NULL(1); const gchar * fu_mm_device_get_inhibition_uid(FuMmDevice *self) G_GNUC_NON_NULL(1); gboolean fu_mm_device_set_device_file(FuMmDevice *self, MMModemPortType port_type, GError **error) G_GNUC_NON_NULL(1); gboolean fu_mm_device_probe_from_omodem(FuMmDevice *self, MMObject *omodem, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_mm_device_at_cmd(FuMmDevice *self, const gchar *cmd, gboolean has_response, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_mm_device_set_autosuspend_delay(FuMmDevice *self, guint timeout_ms, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/plugins/modem-manager/fu-mm-dfota-device.c000066400000000000000000000327151501337203100226310ustar00rootroot00000000000000/* * Copyright 2024 TDT AG * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-mm-dfota-device.h" struct _FuMmDfotaDevice { FuMmDevice parent_instance; }; G_DEFINE_TYPE(FuMmDfotaDevice, fu_mm_dfota_device, FU_TYPE_MM_DEVICE) #define FU_MM_DFOTA_DEVICE_FILENAME "dfota_update.bin" #define FU_MM_DFOTA_DEVICE_FOTA_READ_TIMEOUT_SECS 90 #define FU_MM_DFOTA_DEVICE_FOTA_RESTART_TIMEOUT_SECS 15 #define FU_MM_DFOTA_DEVICE_TIMEOUT_SECS 5 static gboolean fu_mm_dfota_device_probe(FuDevice *device, GError **error) { FuMmDfotaDevice *self = FU_MM_DFOTA_DEVICE(device); return fu_mm_device_set_device_file(FU_MM_DEVICE(self), MM_MODEM_PORT_TYPE_AT, error); } static gboolean fu_mm_dfota_device_setup(FuDevice *device, GError **error) { FuMmDfotaDevice *self = FU_MM_DFOTA_DEVICE(device); g_autoptr(GError) error_local = NULL; if (!fu_mm_device_at_cmd(FU_MM_DEVICE(self), "AT+QFLST=?", TRUE, error)) { g_prefix_error(error, "listing files not supported: "); return FALSE; } /* if listing firmware file does not fail, there is an old firmware file to remove */ if (!fu_mm_device_at_cmd(FU_MM_DEVICE(self), "AT+QFLST=\"UFS:" FU_MM_DFOTA_DEVICE_FILENAME "\"", TRUE, &error_local)) { g_debug("no old firmware found in filesystem: %s", error_local->message); return TRUE; } g_debug("found orphaned firmware file; trying to delete it"); if (!fu_mm_device_at_cmd(FU_MM_DEVICE(self), "AT+QFDEL=\"" FU_MM_DFOTA_DEVICE_FILENAME "\"", TRUE, error)) { g_prefix_error(error, "failed to delete existing firmware file: "); return FALSE; } /* success */ return TRUE; } /* compute 16 bit checksum based on bitwise XOR */ static gboolean fu_mm_dfota_device_compute_checksum_cb(const guint8 *buf, gsize bufsz, gpointer user_data, GError **error) { guint16 *checksum = (guint16 *)user_data; for (gsize i = 0; i < bufsz; i += 2) { guint16 word = buf[i] << 8; if (i < bufsz - 1) word |= buf[i + 1]; *checksum ^= word; } return TRUE; } static gboolean fu_mm_dfota_device_upload_chunk(FuMmDfotaDevice *self, FuChunk *chk, GError **error) { g_autoptr(GBytes) ack_bytes = NULL; g_autoptr(GRegex) ack_regex = NULL; g_autoptr(GBytes) chunk_bytes = NULL; const gchar *ack_result = NULL; gsize ack_size; gsize chunk_size; gsize acks_expected; ack_regex = g_regex_new("^A+$", 0, 0, NULL); chunk_size = g_bytes_get_size(fu_chunk_get_bytes(chk)); /* expect one byte as response for every 1024 bytes sent */ acks_expected = chunk_size / 1024; /* pad every chunk to 2048 bytes to received correct amount of ACKs */ chunk_bytes = fu_bytes_pad(fu_chunk_get_bytes(chk), 0x800, 0xFF); if (!fu_udev_device_write_bytes(FU_UDEV_DEVICE(self), chunk_bytes, 1500, FU_IO_CHANNEL_FLAG_NONE, error)) { g_prefix_error(error, "failed to upload firmware to the device: "); return FALSE; } if (acks_expected == 0) return TRUE; ack_bytes = fu_udev_device_read_bytes(FU_UDEV_DEVICE(self), acks_expected, FU_MM_DFOTA_DEVICE_TIMEOUT_SECS * 1000, FU_IO_CHANNEL_FLAG_NONE, error); if (ack_bytes == NULL) { g_prefix_error(error, "failed to read response: "); return FALSE; } ack_result = g_bytes_get_data(ack_bytes, &ack_size); if (ack_size != acks_expected) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "expected %" G_GSIZE_FORMAT " ACKs, got %" G_GSIZE_FORMAT, acks_expected, ack_size); return FALSE; } if (!g_regex_match(ack_regex, ack_result, 0, NULL)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "expected ACKs (A), got %s", ack_result); return FALSE; } return TRUE; } static gboolean fu_mm_dfota_device_parse_upload_result(FuMmDfotaDevice *self, guint16 *checksum, gsize *size, GError **error) { guint64 tmp; const gchar *result = NULL; g_autofree gchar *checksum_match = NULL; g_autofree gchar *size_match = NULL; g_autoptr(GBytes) result_bytes = NULL; g_autoptr(GMatchInfo) match_info = NULL; g_autoptr(GRegex) result_regex = NULL; /* +QFUPL: , */ result_regex = g_regex_new("\\r\\n\\+QFUPL:\\s*(\\d+),([0-9a-f]+)\\r\\n", 0, 0, error); if (result_regex == NULL) { g_prefix_error(error, "failed to build regex: "); return FALSE; } result_bytes = fu_udev_device_read_bytes(FU_UDEV_DEVICE(self), 4096, FU_MM_DFOTA_DEVICE_TIMEOUT_SECS * 1000, FU_IO_CHANNEL_FLAG_SINGLE_SHOT, error); if (result_bytes == NULL) { g_prefix_error(error, "failed to read AT+QFUPL response: "); return FALSE; } result = g_bytes_get_data(result_bytes, NULL); if (g_strrstr(result, "\r\nOK\r\n") == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "upload command exited with error"); return FALSE; } if (!g_regex_match(result_regex, result, 0, &match_info)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "could not match QFUPL response"); return FALSE; } if (!g_match_info_matches(match_info)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "could not match size and checksum"); return FALSE; } /* success, so convert to integers */ size_match = g_match_info_fetch(match_info, 1); checksum_match = g_match_info_fetch(match_info, 2); g_debug("parsed checksum '%s' and size '%s'", checksum_match, size_match); if (!fu_strtoull(size_match, &tmp, 0x0, G_MAXSIZE, FU_INTEGER_BASE_10, error)) return FALSE; if (size != NULL) *size = tmp; if (!fu_strtoull(checksum_match, &tmp, 0x0, G_MAXUINT16, FU_INTEGER_BASE_16, error)) return FALSE; if (checksum != NULL) *checksum = tmp; /* success */ return TRUE; } static gboolean fu_mm_dfota_device_upload_stream(FuMmDfotaDevice *self, GInputStream *stream, GError **error) { gsize size = 0; gsize size_parsed = 0; guint16 checksum = 0; guint16 checksum_parsed = 0; g_autoptr(FuChunkArray) chunks = NULL; chunks = fu_chunk_array_new_from_stream(stream, 0x0, FU_CHUNK_PAGESZ_NONE, 0x800, error); if (chunks == NULL) return FALSE; for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_mm_dfota_device_upload_chunk(self, chk, error)) { g_prefix_error(error, "failed at chunk %u: ", i); return FALSE; } if (i % 100 == 0) g_debug("wrote chunk %u/%u", i, fu_chunk_array_length(chunks) - 1); } /* check result */ if (!fu_input_stream_size(stream, &size, error)) return FALSE; if (!fu_input_stream_chunkify(stream, fu_mm_dfota_device_compute_checksum_cb, &checksum, error)) return FALSE; if (!fu_mm_dfota_device_parse_upload_result(self, &checksum_parsed, &size_parsed, error)) return FALSE; if (size != size_parsed) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "firmware size mismatch - expected 0x%x, but was 0x%x", (guint)size, (guint)size_parsed); return FALSE; } if (checksum != checksum_parsed) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "checksum mismatch - expected 0x%04x, but was 0x%04x", checksum, checksum_parsed); return FALSE; } /* success */ return TRUE; } static gboolean fu_mm_dfota_device_parse_fota_response(FuMmDfotaDevice *self, const gchar *response, FuProgress *progress, gboolean *finished, GError **error) { g_autoptr(GRegex) fota_regex = NULL; g_autoptr(GMatchInfo) match_info = NULL; g_autofree gchar *status_match = NULL; g_autofree gchar *status_number_match = NULL; guint64 status_number = 0; /* +QIND: "FOTA",""(,)? */ fota_regex = g_regex_new("\\+QIND:\\s*\"FOTA\",\"([A-Z]+)\"(,(\\d+))?", 0, 0, error); if (fota_regex == NULL) { g_prefix_error(error, "failed to build regex: "); return FALSE; } if (!g_regex_match(fota_regex, response, 0, &match_info)) { /* * Log and continue on unexpected responses because devices * may incorrectly return an incomplete status message 1-2 * times. */ g_debug("got unexpected response '%s'", response); return TRUE; } if (!g_match_info_matches(match_info)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "could not match fota status"); return FALSE; } status_match = g_match_info_fetch(match_info, 1); if (g_strcmp0(status_match, "START") == 0) { g_debug("update started successfully"); return TRUE; } /* expect status and number, which means four matches in the above regex */ if (g_match_info_get_match_count(match_info) != 4) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "badly formatted message '%s'", response); return FALSE; } status_number_match = g_match_info_fetch(match_info, 3); if (!g_ascii_string_to_unsigned(status_number_match, 10, 0, G_MAXUINT, &status_number, error)) return FALSE; if (g_strcmp0(status_match, "UPDATING") == 0) { fu_progress_set_percentage(progress, (guint)status_number); return TRUE; } if (g_strcmp0(status_match, "END") == 0) { if (status_number != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "update exited with error code %" G_GUINT64_FORMAT, status_number); return FALSE; } g_debug("updated finished successfully"); *finished = TRUE; return TRUE; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unhandled fota status '%s'", status_match); return FALSE; } static gboolean fu_mm_dfota_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuMmDfotaDevice *self = FU_MM_DFOTA_DEVICE(device); gboolean finished = FALSE; while (!finished) { g_autoptr(GBytes) bytes = NULL; g_autofree gchar *result = NULL; bytes = fu_udev_device_read_bytes(FU_UDEV_DEVICE(self), -1, FU_MM_DFOTA_DEVICE_FOTA_READ_TIMEOUT_SECS * 1000, FU_IO_CHANNEL_FLAG_SINGLE_SHOT, error); if (bytes == NULL) return FALSE; result = fu_strsafe_bytes(bytes, G_MAXSIZE); if (result == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_READ, "no data read from device"); return FALSE; } /* ignore empty responses */ g_strstrip(result); if (strlen(result) == 0) continue; if (!fu_mm_dfota_device_parse_fota_response(self, result, progress, &finished, error)) return FALSE; } return TRUE; } static gboolean fu_mm_dfota_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuMmDfotaDevice *self = FU_MM_DFOTA_DEVICE(device); g_autofree gchar *upload_cmd = NULL; g_autoptr(GInputStream) stream = NULL; /* get default stream */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* put the device into upload mode */ upload_cmd = g_strdup_printf("AT+QFUPL=\"%s\",%" G_GSIZE_FORMAT ",5,1", FU_MM_DFOTA_DEVICE_FILENAME, fu_firmware_get_size(firmware)); if (!fu_mm_device_at_cmd(FU_MM_DEVICE(self), upload_cmd, TRUE, error)) { g_prefix_error(error, "failed to enable upload mode: "); return FALSE; } if (!fu_mm_dfota_device_upload_stream(self, stream, error)) return FALSE; if (!fu_mm_device_at_cmd(FU_MM_DEVICE(self), "AT+QFOTADL=\"/data/ufs/" FU_MM_DFOTA_DEVICE_FILENAME "\"", TRUE, error)) { g_prefix_error(error, "failed to start update: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), FU_MM_DFOTA_DEVICE_FOTA_RESTART_TIMEOUT_SECS * 1000); /* success */ return TRUE; } static gboolean fu_mm_dfota_device_prepare(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuMmDfotaDevice *self = FU_MM_DFOTA_DEVICE(device); fu_mm_device_set_inhibited(FU_MM_DEVICE(self), TRUE); return TRUE; } static gboolean fu_mm_dfota_device_cleanup(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuMmDfotaDevice *self = FU_MM_DFOTA_DEVICE(device); fu_mm_device_set_inhibited(FU_MM_DEVICE(self), FALSE); return TRUE; } static void fu_mm_dfota_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 97, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reload"); } static void fu_mm_dfota_device_init(FuMmDfotaDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "com.quectel.dfota"); } static void fu_mm_dfota_device_class_init(FuMmDfotaDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_mm_dfota_device_probe; device_class->attach = fu_mm_dfota_device_attach; device_class->prepare = fu_mm_dfota_device_prepare; device_class->cleanup = fu_mm_dfota_device_cleanup; device_class->setup = fu_mm_dfota_device_setup; device_class->set_progress = fu_mm_dfota_device_set_progress; device_class->write_firmware = fu_mm_dfota_device_write_firmware; } fwupd-2.0.10/plugins/modem-manager/fu-mm-dfota-device.h000066400000000000000000000005421501337203100226270ustar00rootroot00000000000000/* * Copyright 2024 TDT AG * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-mm-device.h" #define FU_TYPE_MM_DFOTA_DEVICE (fu_mm_dfota_device_get_type()) G_DECLARE_FINAL_TYPE(FuMmDfotaDevice, fu_mm_dfota_device, FU, MM_DFOTA_DEVICE, FuMmDevice) fwupd-2.0.10/plugins/modem-manager/fu-mm-fastboot-device.c000066400000000000000000000113541501337203100233510ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-mm-fastboot-device.h" struct _FuMmFastbootDevice { FuMmDevice parent_instance; gchar *detach_at; }; G_DEFINE_TYPE(FuMmFastbootDevice, fu_mm_fastboot_device, FU_TYPE_MM_DEVICE) #define FU_MM_FASTBOOT_DEVICE_FLAG_DETACH_AT_NO_RESPONSE "detach-at-fastboot-has-no-response" static void fu_mm_fastboot_device_to_string(FuDevice *device, guint idt, GString *str) { FuMmFastbootDevice *self = FU_MM_FASTBOOT_DEVICE(device); fwupd_codec_string_append(str, idt, "DetachAt", self->detach_at); } void fu_mm_fastboot_device_set_detach_at(FuMmFastbootDevice *self, const gchar *detach_at) { g_return_if_fail(FU_IS_MM_FASTBOOT_DEVICE(self)); g_return_if_fail(detach_at != NULL); g_free(self->detach_at); self->detach_at = g_strdup(detach_at); } static gboolean fu_mm_fastboot_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuMmFastbootDevice *self = FU_MM_FASTBOOT_DEVICE(device); gboolean has_response = TRUE; /* expect response for fastboot AT command */ if (fu_device_has_private_flag(FU_DEVICE(self), FU_MM_FASTBOOT_DEVICE_FLAG_DETACH_AT_NO_RESPONSE)) { has_response = FALSE; } if (!fu_mm_device_at_cmd(FU_MM_DEVICE(self), "AT", TRUE, error)) return FALSE; if (!fu_mm_device_at_cmd(FU_MM_DEVICE(self), self->detach_at, has_response, error)) { g_prefix_error(error, "rebooting into fastboot not supported: "); return FALSE; } /* success */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_mm_fastboot_device_probe(FuDevice *device, GError **error) { FuMmFastbootDevice *self = FU_MM_FASTBOOT_DEVICE(device); return fu_mm_device_set_device_file(FU_MM_DEVICE(self), MM_MODEM_PORT_TYPE_AT, error); } static gboolean fu_mm_fastboot_device_from_json(FuDevice *device, JsonObject *json_object, GError **error) { FuMmFastbootDevice *self = FU_MM_FASTBOOT_DEVICE(device); const gchar *tmp; /* FuMmDevice->from_json */ if (!FU_DEVICE_CLASS(fu_mm_fastboot_device_parent_class) ->from_json(device, json_object, error)) return FALSE; /* optional properties */ tmp = json_object_get_string_member_with_default(json_object, "DetachAt", NULL); if (tmp != NULL) fu_mm_fastboot_device_set_detach_at(self, tmp); /* success */ return TRUE; } static void fu_mm_fastboot_device_add_json(FuDevice *device, JsonBuilder *builder, FwupdCodecFlags flags) { FuMmFastbootDevice *self = FU_MM_FASTBOOT_DEVICE(device); /* FuMmDevice->add_json */ FU_DEVICE_CLASS(fu_mm_fastboot_device_parent_class)->add_json(device, builder, flags); /* optional properties */ if (self->detach_at != NULL) fwupd_codec_json_append(builder, "DetachAt", self->detach_at); } static void fu_mm_fastboot_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 97, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reload"); } static void fu_mm_fastboot_device_init(FuMmFastbootDevice *self) { fu_device_set_remove_delay(FU_DEVICE(self), 20000); fu_device_add_protocol(FU_DEVICE(self), "com.google.fastboot"); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_register_private_flag(FU_DEVICE(self), FU_MM_FASTBOOT_DEVICE_FLAG_DETACH_AT_NO_RESPONSE); fu_device_add_instance_id_full(FU_DEVICE(self), "USB\\VID_18D1&PID_D00D", FU_DEVICE_INSTANCE_FLAG_COUNTERPART); fu_device_add_instance_id_full(FU_DEVICE(self), "USB\\VID_2CB7&PID_D00D", FU_DEVICE_INSTANCE_FLAG_COUNTERPART); } static void fu_mm_fastboot_device_finalize(GObject *object) { FuMmFastbootDevice *self = FU_MM_FASTBOOT_DEVICE(object); g_free(self->detach_at); G_OBJECT_CLASS(fu_mm_fastboot_device_parent_class)->finalize(object); } static void fu_mm_fastboot_device_class_init(FuMmFastbootDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_mm_fastboot_device_finalize; device_class->set_progress = fu_mm_fastboot_device_set_progress; device_class->detach = fu_mm_fastboot_device_detach; device_class->probe = fu_mm_fastboot_device_probe; device_class->to_string = fu_mm_fastboot_device_to_string; device_class->from_json = fu_mm_fastboot_device_from_json; device_class->add_json = fu_mm_fastboot_device_add_json; } fwupd-2.0.10/plugins/modem-manager/fu-mm-fastboot-device.h000066400000000000000000000006721501337203100233570ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-mm-device.h" #define FU_TYPE_MM_FASTBOOT_DEVICE (fu_mm_fastboot_device_get_type()) G_DECLARE_FINAL_TYPE(FuMmFastbootDevice, fu_mm_fastboot_device, FU, MM_FASTBOOT_DEVICE, FuMmDevice) void fu_mm_fastboot_device_set_detach_at(FuMmFastbootDevice *self, const gchar *detach_at) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/plugins/modem-manager/fu-mm-fdl-device.c000066400000000000000000000233121501337203100222720ustar00rootroot00000000000000/* * Copyright 2024 TDT AG * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-mm-fdl-device.h" #include "fu-mm-fdl-struct.h" #ifdef HAVE_TERMIOS_H #include #endif #ifdef HAVE_TERMIOS_H #define FU_CINTERION_FDL_DEFAULT_BAUDRATE B115200 #endif #define FU_CINTERION_FDL_MAX_READ_RETRIES 100 #define FU_CINTERION_FDL_MAX_WRITE_RETRIES 10 #define FU_CINTERION_FDL_SIZE_BYTES 2 struct _FuMmFdlDevice { FuMmDevice parent_instance; }; G_DEFINE_TYPE(FuMmFdlDevice, fu_mm_fdl_device, FU_TYPE_MM_DEVICE) static gboolean fu_mm_fdl_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuMmFdlDevice *self = FU_MM_FDL_DEVICE(device); if (!fu_mm_device_at_cmd(FU_MM_DEVICE(self), "AT", TRUE, error)) return FALSE; if (!fu_mm_device_at_cmd(FU_MM_DEVICE(self), "AT^SFDL", TRUE, error)) { g_prefix_error(error, "enabling firmware download mode not supported: "); return FALSE; } /* wait 15 s before reopening port */ fu_device_sleep(device, 15000); return TRUE; } static gboolean fu_mm_fdl_device_wait_ready_cb(FuDevice *device, gpointer user_data, GError **error) { FuMmFdlDevice *self = FU_MM_FDL_DEVICE(device); gsize bytes_read = 0; guint8 buf[1] = {0}; if (!fu_udev_device_read(FU_UDEV_DEVICE(self), buf, sizeof(buf), &bytes_read, 100, FU_IO_CHANNEL_FLAG_USE_BLOCKING_IO, error)) { return FALSE; } if (bytes_read != 1 || buf[0] != FU_MM_CINTERION_FDL_RESPONSE_OK) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_READ, "invalid response"); return FALSE; } /* success */ g_debug("start signal read"); return TRUE; } static gboolean fu_mm_fdl_device_write_chunk(FuMmFdlDevice *self, GBytes *size_bytes, GBytes *chunk_bytes, GError **error) { if (!fu_udev_device_write_bytes(FU_UDEV_DEVICE(self), size_bytes, 1500, FU_IO_CHANNEL_FLAG_USE_BLOCKING_IO, error)) { return FALSE; } if (!fu_udev_device_write_bytes(FU_UDEV_DEVICE(self), chunk_bytes, 1500, FU_IO_CHANNEL_FLAG_USE_BLOCKING_IO, error)) { return FALSE; } return TRUE; } static gboolean fu_mm_fdl_device_read_response(FuMmFdlDevice *self, FuMmCinterionFdlResponse *response, GError **error) { guint8 buf[1] = {0}; gsize bytes_read = 0; if (!fu_udev_device_read(FU_UDEV_DEVICE(self), buf, sizeof(buf), &bytes_read, 100, FU_IO_CHANNEL_FLAG_USE_BLOCKING_IO, error)) { return FALSE; } if (bytes_read != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_READ, "invalid response"); return FALSE; } switch (buf[0]) { case FU_MM_CINTERION_FDL_RESPONSE_OK: case FU_MM_CINTERION_FDL_RESPONSE_RETRY: case FU_MM_CINTERION_FDL_RESPONSE_BUSY: *response = buf[0]; break; default: *response = FU_MM_CINTERION_FDL_RESPONSE_UNKNOWN; break; } /* success */ return TRUE; } typedef struct { GBytes *size_bytes; GBytes *chunk_bytes; } FuMmFdlDeviceWriteHelper; static void fu_mm_fdl_device_write_helper_free(FuMmFdlDeviceWriteHelper *helper) { if (helper->size_bytes) g_object_unref(helper->size_bytes); if (helper->chunk_bytes) g_object_unref(helper->chunk_bytes); g_free(helper); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuMmFdlDeviceWriteHelper, fu_mm_fdl_device_write_helper_free) static gboolean fu_mm_fdl_device_read_chunk_cb(FuDevice *device, gpointer user_data, GError **error) { FuMmFdlDevice *self = FU_MM_FDL_DEVICE(device); FuMmCinterionFdlResponse *response = (FuMmCinterionFdlResponse *)user_data; if (!fu_mm_fdl_device_read_response(self, response, error)) return FALSE; /* retry reading response */ if (*response == FU_MM_CINTERION_FDL_RESPONSE_BUSY) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "response busy"); return FALSE; } if (*response == FU_MM_CINTERION_FDL_RESPONSE_UNKNOWN) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "response unknown"); return FALSE; } /* success */ return TRUE; } static gboolean fu_mm_fdl_device_write_chunk_cb(FuDevice *device, gpointer user_data, GError **error) { FuMmFdlDevice *self = FU_MM_FDL_DEVICE(device); FuMmFdlDeviceWriteHelper *helper = (FuMmFdlDeviceWriteHelper *)user_data; FuMmCinterionFdlResponse response = FU_MM_CINTERION_FDL_RESPONSE_UNKNOWN; if (!fu_mm_fdl_device_write_chunk(self, helper->size_bytes, helper->chunk_bytes, error)) return FALSE; if (!fu_device_retry_full(FU_DEVICE(self), fu_mm_fdl_device_read_chunk_cb, FU_CINTERION_FDL_MAX_READ_RETRIES, 10, /* ms */ &response, error)) return FALSE; /* stop reading and retry write */ if (response == FU_MM_CINTERION_FDL_RESPONSE_RETRY) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "response retry"); return FALSE; } /* success */ return TRUE; } static gboolean fu_mm_fdl_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuMmFdlDevice *self = FU_MM_FDL_DEVICE(device); guint chunk = 0; gsize offset = 0; gsize fw_len = fu_firmware_get_size(firmware); g_autoptr(GBytes) fw = NULL; /* wait to be ready */ if (!fu_device_retry_full(device, fu_mm_fdl_device_wait_ready_cb, FU_CINTERION_FDL_MAX_READ_RETRIES, 100, /* ms */ NULL, error)) return FALSE; /* send each [variable-sized] section */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; while (offset < fw_len) { g_autoptr(FuMmFdlDeviceWriteHelper) helper = g_new0(FuMmFdlDeviceWriteHelper, 1); g_autoptr(GBytes) size_bytes = NULL; g_autoptr(GBytes) chunk_bytes = NULL; guint16 chunk_size = 0; helper->size_bytes = g_bytes_new_from_bytes(fw, offset, FU_CINTERION_FDL_SIZE_BYTES); if (!fu_memread_uint16_safe(g_bytes_get_data(helper->size_bytes, NULL), g_bytes_get_size(helper->size_bytes), 0x0, &chunk_size, G_LITTLE_ENDIAN, error)) return FALSE; offset += FU_CINTERION_FDL_SIZE_BYTES; helper->chunk_bytes = g_bytes_new_from_bytes(fw, offset, chunk_size); offset += chunk_size; if (!fu_device_retry_full(FU_DEVICE(self), fu_mm_fdl_device_write_chunk_cb, FU_CINTERION_FDL_MAX_WRITE_RETRIES, 10, /* ms */ helper, error)) { g_prefix_error(error, "could not write chunk %u", chunk); return FALSE; } if (chunk % 100 == 0) g_debug("wrote chunk %u successfully", chunk); fu_progress_set_percentage_full(progress, offset, fw_len); chunk++; } if (fw_len != offset) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "expected %" G_GSIZE_FORMAT " bytes, but wrote %" G_GSIZE_FORMAT, fw_len, offset); return FALSE; } /* success */ return TRUE; } static gboolean fu_mm_fdl_device_probe(FuDevice *device, GError **error) { FuMmFdlDevice *self = FU_MM_FDL_DEVICE(device); return fu_mm_device_set_device_file(FU_MM_DEVICE(self), MM_MODEM_PORT_TYPE_AT, error); } static gboolean fu_mm_fdl_device_set_io_flags(FuMmFdlDevice *self, GError **error) { #ifdef HAVE_TERMIOS_H gint fd = fu_io_channel_unix_get_fd(fu_udev_device_get_io_channel(FU_UDEV_DEVICE(self))); struct termios tio = { .c_cflag = CS8 | CREAD | CLOCAL | HUPCL | FU_CINTERION_FDL_DEFAULT_BAUDRATE, }; if (tcsetattr(fd, TCSANOW, &tio) != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "could not set termios attributes"); return FALSE; } return TRUE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Not supported as not found"); return FALSE; #endif } static gboolean fu_mm_fdl_device_open(FuDevice *device, GError **error) { FuMmFdlDevice *self = FU_MM_FDL_DEVICE(device); /* FuUdevDevice->open */ if (!FU_DEVICE_CLASS(fu_mm_fdl_device_parent_class)->open(device, error)) return FALSE; return fu_mm_fdl_device_set_io_flags(self, error); } static gboolean fu_mm_fdl_device_prepare(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuMmFdlDevice *self = FU_MM_FDL_DEVICE(device); fu_mm_device_set_inhibited(FU_MM_DEVICE(self), TRUE); return TRUE; } static gboolean fu_mm_fdl_device_cleanup(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuMmFdlDevice *self = FU_MM_FDL_DEVICE(device); fu_mm_device_set_inhibited(FU_MM_DEVICE(self), FALSE); return TRUE; } static void fu_mm_fdl_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 97, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reload"); } static void fu_mm_fdl_device_init(FuMmFdlDevice *self) { fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); fu_device_add_protocol(FU_DEVICE(self), "com.cinterion.fdl"); } static void fu_mm_fdl_device_class_init(FuMmFdlDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->open = fu_mm_fdl_device_open; device_class->probe = fu_mm_fdl_device_probe; device_class->detach = fu_mm_fdl_device_detach; device_class->prepare = fu_mm_fdl_device_prepare; device_class->cleanup = fu_mm_fdl_device_cleanup; device_class->set_progress = fu_mm_fdl_device_set_progress; device_class->write_firmware = fu_mm_fdl_device_write_firmware; } fwupd-2.0.10/plugins/modem-manager/fu-mm-fdl-device.h000066400000000000000000000005301501337203100222740ustar00rootroot00000000000000/* * Copyright 2024 TDT AG * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-mm-device.h" #define FU_TYPE_MM_FDL_DEVICE (fu_mm_fdl_device_get_type()) G_DECLARE_FINAL_TYPE(FuMmFdlDevice, fu_mm_fdl_device, FU, MM_FDL_DEVICE, FuMmDevice) fwupd-2.0.10/plugins/modem-manager/fu-mm-fdl.rs000066400000000000000000000003331501337203100212350ustar00rootroot00000000000000/* * Copyright 2024 TDT AG * * SPDX-License-Identifier: LGPL-2.1-or-later */ #[repr(u8)] enum FuMmCinterionFdlResponse { Ok = 0x01, Retry = 0x02, Unknown = 0x03, Busy = 0x04, } fwupd-2.0.10/plugins/modem-manager/fu-mm-firehose-device.c000066400000000000000000000057501501337203100233370ustar00rootroot00000000000000/* * Copyright 2020 Aleksander Morgado * Copyright 2021 Ivan Mikhanchuk * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-mm-firehose-device.h" struct _FuMmFirehoseDevice { FuMmDevice parent_instance; }; G_DEFINE_TYPE(FuMmFirehoseDevice, fu_mm_firehose_device, FU_TYPE_MM_DEVICE) static gboolean fu_mm_firehose_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuMmFirehoseDevice *self = FU_MM_FIREHOSE_DEVICE(device); if (!fu_mm_device_at_cmd(FU_MM_DEVICE(self), "AT", TRUE, error)) return FALSE; if (!fu_mm_device_at_cmd(FU_MM_DEVICE(self), "AT^SFIREHOSE", TRUE, error)) { g_prefix_error(error, "enabling firmware download mode not supported: "); return FALSE; } /* success */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_mm_firehose_device_probe(FuDevice *device, GError **error) { FuMmFirehoseDevice *self = FU_MM_FIREHOSE_DEVICE(device); return fu_mm_device_set_device_file(FU_MM_DEVICE(self), MM_MODEM_PORT_TYPE_AT, error); } static gboolean fu_mm_firehose_device_prepare(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuMmFirehoseDevice *self = FU_MM_FIREHOSE_DEVICE(device); fu_mm_device_set_inhibited(FU_MM_DEVICE(self), TRUE); return TRUE; } static gboolean fu_mm_firehose_device_cleanup(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuMmFirehoseDevice *self = FU_MM_FIREHOSE_DEVICE(device); fu_mm_device_set_inhibited(FU_MM_DEVICE(self), FALSE); return TRUE; } static void fu_mm_firehose_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 97, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reload"); } static void fu_mm_firehose_device_init(FuMmFirehoseDevice *self) { fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); fu_device_add_protocol(FU_UDEV_DEVICE(self), "com.qualcomm.firehose"); } static void fu_mm_firehose_device_class_init(FuMmFirehoseDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_mm_firehose_device_probe; device_class->detach = fu_mm_firehose_device_detach; device_class->prepare = fu_mm_firehose_device_prepare; device_class->cleanup = fu_mm_firehose_device_cleanup; device_class->set_progress = fu_mm_firehose_device_set_progress; } fwupd-2.0.10/plugins/modem-manager/fu-mm-firehose-device.h000066400000000000000000000007031501337203100233350ustar00rootroot00000000000000/* * Copyright 2020 Aleksander Morgado * Copyright 2021 Ivan Mikhanchuk * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-mm-device.h" #define FU_TYPE_MM_FIREHOSE_DEVICE (fu_mm_firehose_device_get_type()) G_DECLARE_FINAL_TYPE(FuMmFirehoseDevice, fu_mm_firehose_device, FU, MM_FIREHOSE_DEVICE, FuMmDevice) fwupd-2.0.10/plugins/modem-manager/fu-mm-mbim-common.c000066400000000000000000000111221501337203100224760ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-mm-mbim-common.h" typedef struct { gboolean ret; GMainLoop *loop; GCancellable *cancellable; guint timeout_id; MbimDevice *mbim_device; MbimMessage *mbim_message; GError *error; } _MbimDeviceHelper; static gboolean _mbim_device_helper_timeout_cb(gpointer user_data) /* nocheck:name */ { _MbimDeviceHelper *helper = (_MbimDeviceHelper *)user_data; g_cancellable_cancel(helper->cancellable); helper->timeout_id = 0; return G_SOURCE_REMOVE; } static _MbimDeviceHelper * _mbim_device_helper_new(guint timeout_ms) /* nocheck:name */ { _MbimDeviceHelper *helper = g_new0(_MbimDeviceHelper, 1); helper->cancellable = g_cancellable_new(); helper->loop = g_main_loop_new(NULL, FALSE); helper->timeout_id = g_timeout_add(timeout_ms, _mbim_device_helper_timeout_cb, helper); return helper; } static void _mbim_device_helper_free(_MbimDeviceHelper *helper) /* nocheck:name */ { if (helper->timeout_id != 0) g_source_remove(helper->timeout_id); g_object_unref(helper->cancellable); g_main_loop_unref(helper->loop); g_free(helper); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(_MbimDeviceHelper, _mbim_device_helper_free) static void _mbim_device_new_cb(GObject *source, GAsyncResult *res, gpointer user_data) /* nocheck:name */ { _MbimDeviceHelper *helper = (_MbimDeviceHelper *)user_data; helper->mbim_device = mbim_device_new_finish(res, &helper->error); g_main_loop_quit(helper->loop); } MbimDevice * _mbim_device_new_sync(GFile *file, guint timeout_ms, GError **error) /* nocheck:name */ { g_autoptr(_MbimDeviceHelper) helper = _mbim_device_helper_new(timeout_ms); g_return_val_if_fail(G_IS_FILE(file), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); mbim_device_new(file, helper->cancellable, _mbim_device_new_cb, helper); g_main_loop_run(helper->loop); return helper->mbim_device; } static void _mbim_device_open_cb(GObject *source, GAsyncResult *res, gpointer user_data) /* nocheck:name */ { _MbimDeviceHelper *helper = (_MbimDeviceHelper *)user_data; helper->ret = mbim_device_open_full_finish(helper->mbim_device, res, &helper->error); g_main_loop_quit(helper->loop); } gboolean _mbim_device_open_sync(MbimDevice *mbim_device, /* nocheck:name */ guint timeout_ms, GError **error) { g_autoptr(_MbimDeviceHelper) helper = _mbim_device_helper_new(timeout_ms); g_return_val_if_fail(MBIM_IS_DEVICE(mbim_device), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); mbim_device_open_full(mbim_device, MBIM_DEVICE_OPEN_FLAGS_PROXY, 10, helper->cancellable, _mbim_device_open_cb, helper); g_main_loop_run(helper->loop); return helper->ret; } static void _mbim_device_close_cb(GObject *source, GAsyncResult *res, gpointer user_data) /* nocheck:name */ { _MbimDeviceHelper *helper = (_MbimDeviceHelper *)user_data; helper->ret = mbim_device_close_finish(helper->mbim_device, res, &helper->error); g_main_loop_quit(helper->loop); } gboolean _mbim_device_close_sync(MbimDevice *mbim_device, /* nocheck:name */ guint timeout_ms, GError **error) { g_autoptr(_MbimDeviceHelper) helper = _mbim_device_helper_new(timeout_ms); g_return_val_if_fail(MBIM_IS_DEVICE(mbim_device), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); mbim_device_close(mbim_device, 5, helper->cancellable, _mbim_device_close_cb, helper); g_main_loop_run(helper->loop); return helper->ret; } static void _mbim_device_command_cb(GObject *source, GAsyncResult *res, gpointer user_data) /* nocheck:name */ { _MbimDeviceHelper *helper = (_MbimDeviceHelper *)user_data; g_autoptr(MbimMessage) response = NULL; response = mbim_device_command_finish(helper->mbim_device, res, &helper->error); if (response != NULL) { if (mbim_message_response_get_result(response, MBIM_MESSAGE_TYPE_COMMAND_DONE, &helper->error)) { helper->mbim_message = g_steal_pointer(&response); } } g_main_loop_quit(helper->loop); } MbimMessage * _mbim_device_command_sync(MbimDevice *mbim_device, /* nocheck:name */ MbimMessage *mbim_message, guint timeout_ms, GError **error) { g_autoptr(_MbimDeviceHelper) helper = _mbim_device_helper_new(timeout_ms); g_return_val_if_fail(MBIM_IS_DEVICE(mbim_device), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); mbim_device_command(mbim_device, mbim_message, 2 * timeout_ms * 1000, helper->cancellable, _mbim_device_command_cb, helper); g_main_loop_run(helper->loop); return helper->mbim_message; } fwupd-2.0.10/plugins/modem-manager/fu-mm-mbim-common.h000066400000000000000000000013461501337203100225120ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include MbimDevice * _mbim_device_new_sync(GFile *file, guint timeout_ms, GError **error); /* nocheck:name */ gboolean _mbim_device_open_sync(MbimDevice *mbim_device, /* nocheck:name */ guint timeout_ms, GError **error) G_GNUC_NON_NULL(1); gboolean _mbim_device_close_sync(MbimDevice *mbim_device, /* nocheck:name */ guint timeout_ms, GError **error) G_GNUC_NON_NULL(1); MbimMessage * _mbim_device_command_sync(MbimDevice *mbim_device, /* nocheck:name */ MbimMessage *mbim_message, guint timeout_ms, GError **error) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/plugins/modem-manager/fu-mm-mbim-device.c000066400000000000000000000320411501337203100224500ustar00rootroot00000000000000/* * Copyright 2021 Jarvis Jiang * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-mm-mbim-common.h" #include "fu-mm-mbim-device.h" struct _FuMmMbimDevice { FuMmDevice parent_instance; MbimDevice *mbim_device; }; G_DEFINE_TYPE(FuMmMbimDevice, fu_mm_mbim_device, FU_TYPE_MM_DEVICE) #define FU_MM_MBIM_DEVICE_MAX_OPEN_ATTEMPTS 8 #define FU_MM_MBIM_DEVICE_TIMEOUT_MS 1500 static gboolean fu_mm_mbim_device_ensure_firmware_version(FuMmMbimDevice *self, GError **error) { g_autofree gchar *firmware_version = NULL; g_autoptr(MbimMessage) request = NULL; g_autoptr(MbimMessage) response = NULL; request = mbim_message_device_caps_query_new(NULL); response = _mbim_device_command_sync(self->mbim_device, request, 10 * 1000, error); if (response == NULL) return FALSE; if (!mbim_message_device_caps_response_parse(response, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &firmware_version, NULL, error)) { g_debug("failed to parse caps-query response: "); return FALSE; } g_debug("[%s] modem query caps firmware version: %s", mbim_device_get_path_display(self->mbim_device), firmware_version); /* success */ return TRUE; } static gboolean fu_mm_mbim_device_detach_to_edl(FuMmMbimDevice *self, GError **error) { g_autoptr(GError) error_local = NULL; g_autoptr(MbimMessage) request = NULL; g_autoptr(MbimMessage) response = NULL; request = mbim_message_qdu_quectel_reboot_set_new(MBIM_QDU_QUECTEL_REBOOT_TYPE_EDL, NULL); response = _mbim_device_command_sync(self->mbim_device, request, 5 * 1000, &error_local); if (response == NULL) { /* MBIM port goes away */ if (g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { g_debug("ignoring: %s", error_local->message); return TRUE; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } /* success */ return TRUE; } static gboolean fu_mm_mbim_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuMmMbimDevice *self = FU_MM_MBIM_DEVICE(device); /* use special command for Quectel MBIM devices */ if (fu_device_get_vid(device) == 0x2C7C) { if (!fu_mm_mbim_device_detach_to_edl(self, error)) return FALSE; } /* success */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static GArray * fu_mm_mbim_device_get_checksum(GBytes *blob) { gsize file_size; gsize hash_size; GArray *digest; g_autoptr(GChecksum) checksum = NULL; /* get checksum, to be used as unique id */ file_size = g_bytes_get_size(blob); hash_size = g_checksum_type_get_length(G_CHECKSUM_SHA256); checksum = g_checksum_new(G_CHECKSUM_SHA256); g_checksum_update(checksum, g_bytes_get_data(blob, NULL), file_size); /* libqmi expects a GArray of bytes, not a GByteArray */ digest = g_array_sized_new(FALSE, FALSE, sizeof(guint8), hash_size); g_array_set_size(digest, hash_size); g_checksum_get_digest(checksum, (guint8 *)digest->data, &hash_size); return digest; } static gboolean fu_mm_mbim_device_write_chunk(FuMmMbimDevice *self, FuChunk *chk, GError **error) { g_autoptr(MbimMessage) request = NULL; g_autoptr(MbimMessage) response = NULL; request = mbim_message_qdu_file_write_set_new(fu_chunk_get_data_sz(chk), fu_chunk_get_data(chk), NULL); response = _mbim_device_command_sync(self->mbim_device, request, 20 * 1000, error); if (response == NULL) return FALSE; if (!mbim_message_qdu_file_write_response_parse(response, error)) { g_prefix_error(error, "failed to parse write-chunk response: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_mm_mbim_device_write_chunks(FuMmMbimDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; chk = fu_chunk_array_index(chunks, 0, error); if (chk == NULL) { g_prefix_error(error, "failed to get chunk: "); return FALSE; } if (!fu_mm_mbim_device_write_chunk(self, chk, error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_mm_mbim_device_write(FuMmMbimDevice *self, const gchar *filename, GBytes *blob, FuProgress *progress, GError **error) { guint32 out_max_transfer_size = 0; g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(GArray) checksum = fu_mm_mbim_device_get_checksum(blob); g_autoptr(MbimMessage) action_start_req = NULL; g_autoptr(MbimMessage) action_start_res = NULL; g_autoptr(MbimMessage) file_open_req = NULL; g_autoptr(MbimMessage) file_open_res = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "start-update"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 2, "file-open"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 97, "send-chunks"); /* start update */ action_start_req = mbim_message_qdu_update_session_set_new(MBIM_QDU_SESSION_ACTION_START, MBIM_QDU_SESSION_TYPE_LE, NULL); action_start_res = _mbim_device_command_sync(self->mbim_device, action_start_req, 10 * 1000, error); if (action_start_res == NULL) return FALSE; if (!mbim_message_qdu_update_session_response_parse(action_start_res, NULL, NULL, NULL, NULL, NULL, NULL, error)) { g_prefix_error(error, "failed to parse action-start response: "); return FALSE; } g_debug("[%s] successfully request modem to update session", mbim_device_get_path_display(self->mbim_device)); fu_progress_step_done(progress); /* get the max transfer size */ file_open_req = mbim_message_qdu_file_open_set_new(MBIM_QDU_FILE_TYPE_LITTLE_ENDIAN_PACKAGE, g_bytes_get_size(blob), NULL); file_open_res = _mbim_device_command_sync(self->mbim_device, file_open_req, 10 * 1000, error); if (file_open_res == NULL) return FALSE; if (!mbim_message_qdu_file_open_response_parse(file_open_res, &out_max_transfer_size, NULL, error)) { g_prefix_error(error, "failed to parse file-open response: "); return FALSE; } fu_progress_step_done(progress); /* send chunks */ chunks = fu_chunk_array_new_from_bytes(blob, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, out_max_transfer_size); if (!fu_mm_mbim_device_write_chunks(self, chunks, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_mm_mbim_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuMmMbimDevice *self = FU_MM_MBIM_DEVICE(device); XbNode *part = NULL; const gchar *filename = NULL; const gchar *csum; g_autofree gchar *csum_actual = NULL; g_autofree gchar *version = NULL; g_autoptr(FuArchive) archive = NULL; g_autoptr(GBytes) data_part = NULL; g_autoptr(GBytes) data_xml = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(XbBuilder) builder = xb_builder_new(); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_autoptr(XbSilo) silo = NULL; /* decompress entire archive ahead of time */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; archive = fu_archive_new_stream(stream, FU_ARCHIVE_FLAG_IGNORE_PATH, error); if (archive == NULL) return FALSE; /* load the manifest of operations */ data_xml = fu_archive_lookup_by_fn(archive, "flashfile.xml", error); if (data_xml == NULL) return FALSE; if (!xb_builder_source_load_bytes(source, data_xml, XB_BUILDER_SOURCE_FLAG_NONE, error)) return FALSE; xb_builder_import_source(builder, source); silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, error); if (silo == NULL) return FALSE; part = xb_silo_query_first(silo, "parts/part", error); if (part == NULL) return FALSE; filename = xb_node_get_attr(part, "filename"); csum = xb_node_get_attr(part, "MD5"); data_part = fu_archive_lookup_by_fn(archive, filename, error); if (data_part == NULL) return FALSE; csum_actual = g_compute_checksum_for_bytes(G_CHECKSUM_MD5, data_part); if (g_strcmp0(csum, csum_actual) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "[%s] MD5 not matched", filename); return FALSE; } g_debug("[%s] MD5 matched", filename); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); if (!fu_mm_mbim_device_write(self, filename, data_part, progress, error)) return FALSE; /* read back new version */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ); if (!fu_mm_mbim_device_ensure_firmware_version(self, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_mm_mbim_device_probe(FuDevice *device, GError **error) { FuMmMbimDevice *self = FU_MM_MBIM_DEVICE(device); return fu_mm_device_set_device_file(FU_MM_DEVICE(self), MM_MODEM_PORT_TYPE_MBIM, error); } static gboolean fu_mm_mbim_device_open_cb(FuDevice *device, gpointer user_data, GError **error) { FuMmMbimDevice *self = FU_MM_MBIM_DEVICE(device); return _mbim_device_open_sync(self->mbim_device, FU_MM_MBIM_DEVICE_TIMEOUT_MS, error); } static gboolean fu_mm_mbim_device_open(FuDevice *device, GError **error) { FuMmMbimDevice *self = FU_MM_MBIM_DEVICE(device); g_autoptr(GFile) mbim_device_file = g_file_new_for_path(fu_udev_device_get_device_file(FU_UDEV_DEVICE(self))); /* create and open */ g_clear_object(&self->mbim_device); self->mbim_device = _mbim_device_new_sync(mbim_device_file, FU_MM_MBIM_DEVICE_TIMEOUT_MS, error); if (self->mbim_device == NULL) return FALSE; return fu_device_retry(device, fu_mm_mbim_device_open_cb, FU_MM_MBIM_DEVICE_MAX_OPEN_ATTEMPTS, NULL, error); } static gboolean fu_mm_mbim_device_close(FuDevice *device, GError **error) { FuMmMbimDevice *self = FU_MM_MBIM_DEVICE(device); g_autoptr(MbimDevice) mbim_device = NULL; /* sanity check */ if (self->mbim_device == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no mbim_device"); return FALSE; } mbim_device = g_steal_pointer(&self->mbim_device); return _mbim_device_close_sync(mbim_device, FU_MM_MBIM_DEVICE_TIMEOUT_MS, error); } static gboolean fu_mm_mbim_device_prepare(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuMmMbimDevice *self = FU_MM_MBIM_DEVICE(device); /* autosuspend delay updated for a proper firmware update */ if (!fu_mm_device_set_autosuspend_delay(FU_MM_DEVICE(self), 20000, error)) return FALSE; fu_mm_device_set_inhibited(FU_MM_DEVICE(self), TRUE); return TRUE; } static gboolean fu_mm_mbim_device_cleanup(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuMmMbimDevice *self = FU_MM_MBIM_DEVICE(device); if (!fu_mm_device_set_autosuspend_delay(FU_MM_DEVICE(self), 2000, error)) return FALSE; fu_mm_device_set_inhibited(FU_MM_DEVICE(self), FALSE); return TRUE; } static void fu_mm_mbim_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 97, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reload"); } static void fu_mm_mbim_device_init(FuMmMbimDevice *self) { fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); fu_device_add_protocol(FU_DEVICE(self), "com.qualcomm.mbim_qdu"); } static void fu_mm_mbim_device_finalize(GObject *object) { FuMmMbimDevice *self = FU_MM_MBIM_DEVICE(object); g_warn_if_fail(self->mbim_device == NULL); G_OBJECT_CLASS(fu_mm_mbim_device_parent_class)->finalize(object); } static void fu_mm_mbim_device_class_init(FuMmMbimDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->finalize = fu_mm_mbim_device_finalize; device_class->open = fu_mm_mbim_device_open; device_class->close = fu_mm_mbim_device_close; device_class->probe = fu_mm_mbim_device_probe; device_class->detach = fu_mm_mbim_device_detach; device_class->prepare = fu_mm_mbim_device_prepare; device_class->cleanup = fu_mm_mbim_device_cleanup; device_class->set_progress = fu_mm_mbim_device_set_progress; device_class->write_firmware = fu_mm_mbim_device_write_firmware; } fwupd-2.0.10/plugins/modem-manager/fu-mm-mbim-device.h000066400000000000000000000005511501337203100224560ustar00rootroot00000000000000/* * Copyright 2021 Jarvis Jiang * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-mm-device.h" #define FU_TYPE_MM_MBIM_DEVICE (fu_mm_mbim_device_get_type()) G_DECLARE_FINAL_TYPE(FuMmMbimDevice, fu_mm_mbim_device, FU, MM_MBIM_DEVICE, FuMmDevice) fwupd-2.0.10/plugins/modem-manager/fu-mm-mhi-qcdm-device.c000066400000000000000000000141671501337203100232340ustar00rootroot00000000000000/* * Copyright 2020 Aleksander Morgado * Copyright 2021 Ivan Mikhanchuk * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-mm-mhi-qcdm-device.h" struct _FuMmMhiQcdmDevice { FuMmQcdmDevice parent_instance; FuKernelSearchPathLocker *search_path_locker; gchar *firehose_prog_file; }; G_DEFINE_TYPE(FuMmMhiQcdmDevice, fu_mm_mhi_qcdm_device, FU_TYPE_MM_QCDM_DEVICE) static gboolean fu_mm_mhi_qcdm_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuMmMhiQcdmDevice *self = FU_MM_MHI_QCDM_DEVICE(device); /* sanity check */ if (self->firehose_prog_file == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "Firehose prog filename is not set for the device"); return FALSE; } /* override the baseclass to do nothing; we're handling this in ->write_firmware() */ return TRUE; } static FuKernelSearchPathLocker * fu_mm_mhi_qcdm_device_search_path_locker_new(FuMmMhiQcdmDevice *self, GError **error) { g_autofree gchar *cachedir = NULL; g_autofree gchar *mm_fw_dir = NULL; g_autoptr(FuKernelSearchPathLocker) locker = NULL; /* create a directory to store firmware files for modem-manager plugin */ cachedir = fu_path_from_kind(FU_PATH_KIND_CACHEDIR_PKG); mm_fw_dir = g_build_filename(cachedir, "modem-manager", "firmware", NULL); if (g_mkdir_with_parents(mm_fw_dir, 0700) == -1) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to create '%s': %s", mm_fw_dir, g_strerror(errno)); return NULL; } locker = fu_kernel_search_path_locker_new(mm_fw_dir, error); if (locker == NULL) return NULL; return g_steal_pointer(&locker); } static gboolean fu_mm_mhi_qcdm_device_prepare(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuMmMhiQcdmDevice *self = FU_MM_MHI_QCDM_DEVICE(device); /* in the case of MHI PCI modems, the mhi-pci-generic driver reads the firehose binary * from the firmware-loader and writes it to the modem */ self->search_path_locker = fu_mm_mhi_qcdm_device_search_path_locker_new(self, error); if (self->search_path_locker == NULL) return FALSE; /* success */ return TRUE; } static gboolean fu_mm_mhi_qcdm_device_cleanup(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuMmMhiQcdmDevice *self = FU_MM_MHI_QCDM_DEVICE(device); /* restore the firmware search path */ g_clear_object(&self->search_path_locker); /* success */ return TRUE; } static gboolean fu_mm_mhi_qcdm_device_copy_firehose_prog(FuMmMhiQcdmDevice *self, GBytes *prog, GError **error) { const gchar *path = fu_kernel_search_path_locker_get_path(self->search_path_locker); g_autofree gchar *fn = g_build_filename(path, "qcom", self->firehose_prog_file, NULL); if (!fu_path_mkdir_parent(fn, error)) return FALSE; return fu_bytes_set_contents(fn, prog, error); } static gboolean fu_mm_mhi_qcdm_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuMmMhiQcdmDevice *self = FU_MM_MHI_QCDM_DEVICE(device); g_autoptr(GBytes) firehose_prog = NULL; /* firehose modems that use mhi_pci drivers require firehose binary * to be present in the firmware-loader search path. */ firehose_prog = fu_firmware_get_image_by_id_bytes(firmware, "firehose-prog.mbn", error); if (firehose_prog == NULL) return FALSE; if (!fu_mm_mhi_qcdm_device_copy_firehose_prog(self, firehose_prog, error)) return FALSE; /* trigger emergency download mode; this takes us to the EDL execution environment */ if (!FU_DEVICE_CLASS(fu_mm_mhi_qcdm_device_parent_class)->detach(device, progress, error)) return FALSE; /* success */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED); return TRUE; } static gboolean fu_mm_mhi_qcdm_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuMmMhiQcdmDevice *self = FU_MM_MHI_QCDM_DEVICE(device); if (g_strcmp0(key, "ModemManagerFirehoseProgFile") == 0) { self->firehose_prog_file = g_strdup(value); return TRUE; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_mm_mhi_qcdm_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 97, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reload"); } static void fu_mm_mhi_qcdm_device_init(FuMmMhiQcdmDevice *self) { fu_device_set_remove_delay(FU_DEVICE(self), 5000); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); fu_device_add_protocol(FU_UDEV_DEVICE(self), "com.qualcomm.firehose"); } static void fu_mm_mhi_qcdm_device_finalize(GObject *object) { FuMmMhiQcdmDevice *self = FU_MM_MHI_QCDM_DEVICE(object); g_free(self->firehose_prog_file); G_OBJECT_CLASS(fu_mm_mhi_qcdm_device_parent_class)->finalize(object); } static void fu_mm_mhi_qcdm_device_class_init(FuMmMhiQcdmDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->finalize = fu_mm_mhi_qcdm_device_finalize; device_class->detach = fu_mm_mhi_qcdm_device_detach; device_class->prepare = fu_mm_mhi_qcdm_device_prepare; device_class->cleanup = fu_mm_mhi_qcdm_device_cleanup; device_class->write_firmware = fu_mm_mhi_qcdm_device_write_firmware; device_class->set_quirk_kv = fu_mm_mhi_qcdm_device_set_quirk_kv; device_class->set_progress = fu_mm_mhi_qcdm_device_set_progress; } fwupd-2.0.10/plugins/modem-manager/fu-mm-mhi-qcdm-device.h000066400000000000000000000005471501337203100232360ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-mm-qcdm-device.h" #define FU_TYPE_MM_MHI_QCDM_DEVICE (fu_mm_mhi_qcdm_device_get_type()) G_DECLARE_FINAL_TYPE(FuMmMhiQcdmDevice, fu_mm_mhi_qcdm_device, FU, MM_MHI_QCDM_DEVICE, FuMmQcdmDevice) fwupd-2.0.10/plugins/modem-manager/fu-mm-plugin.c000066400000000000000000000043731501337203100215740ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-mm-backend.h" #include "fu-mm-device.h" #include "fu-mm-fastboot-device.h" #include "fu-mm-firehose-device.h" #include "fu-mm-mhi-qcdm-device.h" #include "fu-mm-qcdm-device.h" #include "fu-mm-dfota-device.h" #include "fu-mm-fdl-device.h" #include "fu-mm-mbim-device.h" #include "fu-mm-qmi-device.h" #define FU_MODEM_MANAGER_PLUGIN(o) fu_plugin_get_data(FU_PLUGIN(o)) static void fu_mm_plugin_load(FuContext *ctx) { fu_context_add_quirk_key(ctx, "ModemManagerBranchAtCommand"); } static gboolean fu_mm_plugin_backend_device_added(FuPlugin *plugin, FuDevice *device, FuProgress *progress, GError **error) { g_autoptr(FuDeviceLocker) locker = NULL; /* ignore anything from other backends, e.g. usb */ if (!FU_IS_MM_DEVICE(device)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported"); return FALSE; } locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; fu_plugin_device_add(plugin, device); /* success */ return TRUE; } static void fu_mm_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); g_autoptr(FuBackend) backend = fu_mm_backend_new(ctx); fu_context_add_backend(ctx, backend); fu_plugin_add_device_gtype(plugin, FU_TYPE_MM_DEVICE); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_MM_DFOTA_DEVICE); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_MM_FASTBOOT_DEVICE); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_MM_FDL_DEVICE); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_MM_FIREHOSE_DEVICE); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_MM_MBIM_DEVICE); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_MM_MHI_QCDM_DEVICE); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_MM_QCDM_DEVICE); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_MM_QMI_DEVICE); /* coverage */ } void fu_plugin_init_vfuncs(FuPluginVfuncs *vfuncs) { vfuncs->load = fu_mm_plugin_load; vfuncs->constructed = fu_mm_plugin_constructed; vfuncs->backend_device_added = fu_mm_plugin_backend_device_added; } fwupd-2.0.10/plugins/modem-manager/fu-mm-qcdm-device.c000066400000000000000000000102571501337203100224550ustar00rootroot00000000000000/* * Copyright 2020 Aleksander Morgado * Copyright 2021 Ivan Mikhanchuk * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-mm-qcdm-device.h" G_DEFINE_TYPE(FuMmQcdmDevice, fu_mm_qcdm_device, FU_TYPE_MM_DEVICE) static gboolean fu_mm_qcdm_device_cmd(FuMmQcdmDevice *self, const guint8 *buf, gsize bufsz, GError **error) { g_autoptr(GBytes) qcdm_req = NULL; g_autoptr(GBytes) qcdm_res = NULL; /* command */ qcdm_req = g_bytes_new(buf, bufsz); fu_dump_bytes(G_LOG_DOMAIN, "writing", qcdm_req); if (!fu_udev_device_write_bytes(FU_UDEV_DEVICE(self), qcdm_req, 1500, FU_IO_CHANNEL_FLAG_FLUSH_INPUT, error)) { g_prefix_error(error, "failed to write qcdm command: "); return FALSE; } /* response */ qcdm_res = fu_udev_device_read_bytes(FU_UDEV_DEVICE(self), bufsz, 1500, FU_IO_CHANNEL_FLAG_SINGLE_SHOT, error); if (qcdm_res == NULL) { g_prefix_error(error, "failed to read qcdm response: "); return FALSE; } fu_dump_bytes(G_LOG_DOMAIN, "read", qcdm_res); /* command == response */ if (g_bytes_compare(qcdm_res, qcdm_req) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to read valid qcdm response"); return FALSE; } /* success */ return TRUE; } static gboolean fu_mm_qcdm_device_switch_to_edl_cb(FuDevice *device, gpointer userdata, GError **error) { FuMmQcdmDevice *self = FU_MM_QCDM_DEVICE(device); const guint8 buf[] = {0x4B, 0x65, 0x01, 0x00, 0x54, 0x0F, 0x7E}; /* when the QCDM port does not exist anymore, we are detached */ if (!g_file_test(fu_udev_device_get_device_file(FU_UDEV_DEVICE(self)), G_FILE_TEST_EXISTS)) return TRUE; return fu_mm_qcdm_device_cmd(self, buf, sizeof(buf), error); } static gboolean fu_mm_qcdm_device_detach(FuDevice *device, FuProgress *progress, GError **error) { /* retry up to 30 times until the QCDM port goes away */ if (!fu_device_retry_full(device, fu_mm_qcdm_device_switch_to_edl_cb, 30, 1000, NULL, error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_mm_qcdm_device_probe(FuDevice *device, GError **error) { FuMmQcdmDevice *self = FU_MM_QCDM_DEVICE(device); return fu_mm_device_set_device_file(FU_MM_DEVICE(self), MM_MODEM_PORT_TYPE_QCDM, error); } static gboolean fu_mm_qcdm_device_prepare(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuMmQcdmDevice *self = FU_MM_QCDM_DEVICE(device); fu_mm_device_set_inhibited(FU_MM_DEVICE(self), TRUE); return TRUE; } static gboolean fu_mm_qcdm_device_cleanup(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuMmQcdmDevice *self = FU_MM_QCDM_DEVICE(device); fu_mm_device_set_inhibited(FU_MM_DEVICE(self), FALSE); return TRUE; } static void fu_mm_qcdm_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 97, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reload"); } static void fu_mm_qcdm_device_init(FuMmQcdmDevice *self) { fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); fu_device_add_protocol(FU_UDEV_DEVICE(self), "com.qualcomm.firehose"); } static void fu_mm_qcdm_device_class_init(FuMmQcdmDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_mm_qcdm_device_probe; device_class->detach = fu_mm_qcdm_device_detach; device_class->prepare = fu_mm_qcdm_device_prepare; device_class->cleanup = fu_mm_qcdm_device_cleanup; device_class->set_progress = fu_mm_qcdm_device_set_progress; } fwupd-2.0.10/plugins/modem-manager/fu-mm-qcdm-device.h000066400000000000000000000007641501337203100224640ustar00rootroot00000000000000/* * Copyright 2020 Aleksander Morgado * Copyright 2021 Ivan Mikhanchuk * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-mm-device.h" #define FU_TYPE_MM_QCDM_DEVICE (fu_mm_qcdm_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuMmQcdmDevice, fu_mm_qcdm_device, FU, MM_QCDM_DEVICE, FuMmDevice) struct _FuMmQcdmDeviceClass { FuMmDeviceClass parent_class; }; fwupd-2.0.10/plugins/modem-manager/fu-mm-qmi-device.c000066400000000000000000000652511501337203100223230ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * Copyright 2019 Aleksander Morgado * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-mm-qmi-device.h" struct _FuMmQmiDevice { FuMmDevice parent_instance; QmiDevice *qmi_device; QmiClientPdc *qmi_client; GArray *active_id; }; G_DEFINE_TYPE(FuMmQmiDevice, fu_mm_qmi_device, FU_TYPE_MM_DEVICE) #define FU_QMI_PDC_MAX_OPEN_ATTEMPTS 8 typedef struct { GMainLoop *mainloop; QmiDevice *qmi_device; QmiClientPdc *qmi_client; GError *error; guint open_attempts; } FuMmQmiDeviceOpenContext; static void fu_mm_qmi_device_qmi_device_open_attempt(FuMmQmiDeviceOpenContext *ctx); static void fu_mm_qmi_device_qmi_device_open_abort_ready(GObject *qmi_device, GAsyncResult *res, gpointer user_data) { FuMmQmiDeviceOpenContext *ctx = (FuMmQmiDeviceOpenContext *)user_data; g_warn_if_fail(ctx->error != NULL); /* ignore errors when aborting open */ qmi_device_close_finish(QMI_DEVICE(qmi_device), res, NULL); ctx->open_attempts--; if (ctx->open_attempts == 0) { g_clear_object(&ctx->qmi_client); g_clear_object(&ctx->qmi_device); g_main_loop_quit(ctx->mainloop); return; } /* retry */ g_clear_error(&ctx->error); fu_mm_qmi_device_qmi_device_open_attempt(ctx); } static void fu_mm_qmi_device_open_abort(FuMmQmiDeviceOpenContext *ctx) { qmi_device_close_async(ctx->qmi_device, 15, NULL, fu_mm_qmi_device_qmi_device_open_abort_ready, ctx); } static void fu_mm_qmi_device_qmi_device_allocate_client_ready(GObject *qmi_device, GAsyncResult *res, gpointer user_data) { FuMmQmiDeviceOpenContext *ctx = (FuMmQmiDeviceOpenContext *)user_data; ctx->qmi_client = QMI_CLIENT_PDC( qmi_device_allocate_client_finish(QMI_DEVICE(qmi_device), res, &ctx->error)); if (ctx->qmi_client == NULL) { fu_mm_qmi_device_open_abort(ctx); return; } g_main_loop_quit(ctx->mainloop); } static void fu_mm_qmi_device_qmi_device_open_cb(GObject *qmi_device, GAsyncResult *res, gpointer user_data) { FuMmQmiDeviceOpenContext *ctx = (FuMmQmiDeviceOpenContext *)user_data; if (!qmi_device_open_finish(QMI_DEVICE(qmi_device), res, &ctx->error)) { fu_mm_qmi_device_open_abort(ctx); return; } qmi_device_allocate_client(ctx->qmi_device, QMI_SERVICE_PDC, QMI_CID_NONE, 5, NULL, fu_mm_qmi_device_qmi_device_allocate_client_ready, ctx); } static void fu_mm_qmi_device_qmi_device_open_attempt(FuMmQmiDeviceOpenContext *ctx) { g_debug("trying to open QMI device..."); qmi_device_open( ctx->qmi_device, QMI_DEVICE_OPEN_FLAGS_AUTO | /* detect QMI and MBIM ports */ QMI_DEVICE_OPEN_FLAGS_EXPECT_INDICATIONS | /* pdc requires indications */ QMI_DEVICE_OPEN_FLAGS_PROXY, /* all comms through the proxy */ 5, NULL, fu_mm_qmi_device_qmi_device_open_cb, ctx); } static void fu_mm_qmi_device_qmi_device_new_ready(GObject *source, GAsyncResult *res, gpointer user_data) { FuMmQmiDeviceOpenContext *ctx = (FuMmQmiDeviceOpenContext *)user_data; ctx->qmi_device = qmi_device_new_finish(res, &ctx->error); if (ctx->qmi_device == NULL) { g_main_loop_quit(ctx->mainloop); return; } fu_mm_qmi_device_qmi_device_open_attempt(ctx); } static gboolean fu_mm_qmi_device_open(FuDevice *device, GError **error) { FuMmQmiDevice *self = FU_MM_QMI_DEVICE(device); g_autoptr(GMainLoop) mainloop = g_main_loop_new(NULL, FALSE); g_autoptr(GFile) qmi_device_file = NULL; FuMmQmiDeviceOpenContext ctx = { .mainloop = mainloop, .qmi_device = NULL, .qmi_client = NULL, .error = NULL, .open_attempts = FU_QMI_PDC_MAX_OPEN_ATTEMPTS, }; qmi_device_file = g_file_new_for_path(fu_udev_device_get_device_file(FU_UDEV_DEVICE(self))); qmi_device_new(qmi_device_file, NULL, fu_mm_qmi_device_qmi_device_new_ready, &ctx); g_main_loop_run(mainloop); /* either we have all device, client and config list set, or otherwise error is set */ if (ctx.qmi_device != NULL && ctx.qmi_client != NULL) { g_warn_if_fail(!ctx.error); self->qmi_device = ctx.qmi_device; self->qmi_client = ctx.qmi_client; /* success */ return TRUE; } g_propagate_error(error, ctx.error); return FALSE; } typedef struct { GMainLoop *mainloop; QmiDevice *qmi_device; QmiClientPdc *qmi_client; GError *error; } FuMmQmiDeviceCloseContext; static void fu_mm_qmi_device_qmi_device_close_ready(GObject *qmi_device, GAsyncResult *res, gpointer user_data) { FuMmQmiDeviceCloseContext *ctx = (FuMmQmiDeviceCloseContext *)user_data; /* ignore errors when closing if we had one already set when releasing client */ qmi_device_close_finish(QMI_DEVICE(qmi_device), res, (ctx->error == NULL) ? &ctx->error : NULL); g_clear_object(&ctx->qmi_device); g_main_loop_quit(ctx->mainloop); } static void fu_mm_qmi_device_qmi_device_release_client_ready(GObject *qmi_device, GAsyncResult *res, gpointer user_data) { FuMmQmiDeviceCloseContext *ctx = (FuMmQmiDeviceCloseContext *)user_data; qmi_device_release_client_finish(QMI_DEVICE(qmi_device), res, &ctx->error); g_clear_object(&ctx->qmi_client); qmi_device_close_async(ctx->qmi_device, 15, NULL, fu_mm_qmi_device_qmi_device_close_ready, ctx); } static gboolean fu_mm_qmi_device_close(FuDevice *device, GError **error) { FuMmQmiDevice *self = FU_MM_QMI_DEVICE(device); g_autoptr(GMainLoop) mainloop = g_main_loop_new(NULL, FALSE); FuMmQmiDeviceCloseContext ctx = { .mainloop = mainloop, .qmi_device = g_steal_pointer(&self->qmi_device), .qmi_client = g_steal_pointer(&self->qmi_client), }; /* sanity check */ if (ctx.qmi_device == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no qmi_device"); return FALSE; } qmi_device_release_client(ctx.qmi_device, QMI_CLIENT(ctx.qmi_client), QMI_DEVICE_RELEASE_CLIENT_FLAGS_RELEASE_CID, 5, NULL, fu_mm_qmi_device_qmi_device_release_client_ready, &ctx); g_main_loop_run(mainloop); /* we should always have both device and client cleared, and optionally error set */ if (ctx.error != NULL) { g_propagate_error(error, ctx.error); return FALSE; } return TRUE; } #define QMI_LOAD_CHUNK_SIZE 0x400 typedef struct { GMainLoop *mainloop; QmiClientPdc *qmi_client; GError *error; gulong indication_id; guint timeout_id; GBytes *blob; GArray *digest; gsize offset; guint token; } FuMmQmiDeviceWriteContext; static void fu_mm_qmi_device_load_config(FuMmQmiDeviceWriteContext *ctx); static gboolean fu_mm_qmi_device_load_config_timeout(gpointer user_data) { FuMmQmiDeviceWriteContext *ctx = user_data; ctx->timeout_id = 0; g_signal_handler_disconnect(ctx->qmi_client, ctx->indication_id); ctx->indication_id = 0; g_set_error_literal(&ctx->error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "couldn't load mcfg: timed out"); g_main_loop_quit(ctx->mainloop); return G_SOURCE_REMOVE; } static void fu_mm_qmi_device_load_config_indication(QmiClientPdc *client, QmiIndicationPdcLoadConfigOutput *output, FuMmQmiDeviceWriteContext *ctx) { gboolean frame_reset; guint32 remaining_size; guint16 error_code = 0; g_source_remove(ctx->timeout_id); ctx->timeout_id = 0; g_signal_handler_disconnect(ctx->qmi_client, ctx->indication_id); ctx->indication_id = 0; if (!qmi_indication_pdc_load_config_output_get_indication_result(output, &error_code, &ctx->error)) { g_main_loop_quit(ctx->mainloop); return; } if (error_code != 0) { /* when a given mcfg file already exists in the device, an "invalid id" error is * returned; the error naming here is a bit off, as the same protocol error number * is used both for 'invalid id' and 'invalid qos id' */ if (error_code == QMI_PROTOCOL_ERROR_INVALID_QOS_ID) { g_debug("file already available in device"); g_main_loop_quit(ctx->mainloop); return; } g_set_error(&ctx->error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "couldn't load mcfg: %s", qmi_protocol_error_get_string((QmiProtocolError)error_code)); g_main_loop_quit(ctx->mainloop); return; } if (qmi_indication_pdc_load_config_output_get_frame_reset(output, &frame_reset, NULL) && frame_reset) { g_set_error(&ctx->error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "couldn't load mcfg: sent data discarded"); g_main_loop_quit(ctx->mainloop); return; } if (!qmi_indication_pdc_load_config_output_get_remaining_size(output, &remaining_size, &ctx->error)) { g_prefix_error(&ctx->error, "couldn't load remaining size: "); g_main_loop_quit(ctx->mainloop); return; } if (remaining_size == 0) { g_debug("finished loading mcfg"); g_main_loop_quit(ctx->mainloop); return; } g_debug("loading next chunk (%u bytes remaining)", remaining_size); fu_mm_qmi_device_load_config(ctx); } static void fu_mm_qmi_device_load_config_ready(GObject *qmi_client, GAsyncResult *res, gpointer user_data) { FuMmQmiDeviceWriteContext *ctx = (FuMmQmiDeviceWriteContext *)user_data; g_autoptr(QmiMessagePdcLoadConfigOutput) output = NULL; output = qmi_client_pdc_load_config_finish(QMI_CLIENT_PDC(qmi_client), res, &ctx->error); if (output == NULL) { g_main_loop_quit(ctx->mainloop); return; } if (!qmi_message_pdc_load_config_output_get_result(output, &ctx->error)) { g_main_loop_quit(ctx->mainloop); return; } /* after receiving the response to our request, we now expect an indication * with the actual result of the operation */ g_warn_if_fail(ctx->indication_id == 0); ctx->indication_id = g_signal_connect(QMI_CLIENT_PDC(ctx->qmi_client), "load-config", G_CALLBACK(fu_mm_qmi_device_load_config_indication), ctx); /* don't wait forever */ g_warn_if_fail(ctx->timeout_id == 0); ctx->timeout_id = g_timeout_add_seconds(5, fu_mm_qmi_device_load_config_timeout, ctx); } static void fu_mm_qmi_device_load_config(FuMmQmiDeviceWriteContext *ctx) { g_autoptr(QmiMessagePdcLoadConfigInput) input = NULL; g_autoptr(GArray) chunk = NULL; gsize full_size; gsize chunk_size; g_autoptr(GError) error = NULL; input = qmi_message_pdc_load_config_input_new(); qmi_message_pdc_load_config_input_set_token(input, ctx->token++, NULL); full_size = g_bytes_get_size(ctx->blob); if ((ctx->offset + QMI_LOAD_CHUNK_SIZE) > full_size) chunk_size = full_size - ctx->offset; else chunk_size = QMI_LOAD_CHUNK_SIZE; chunk = g_array_sized_new(FALSE, FALSE, sizeof(guint8), chunk_size); g_array_set_size(chunk, chunk_size); if (!fu_memcpy_safe((guint8 *)chunk->data, chunk_size, 0x0, /* dst */ (const guint8 *)g_bytes_get_data(ctx->blob, NULL), /* src */ g_bytes_get_size(ctx->blob), ctx->offset, chunk_size, &error)) { g_critical("failed to copy chunk: %s", error->message); } qmi_message_pdc_load_config_input_set_config_chunk(input, QMI_PDC_CONFIGURATION_TYPE_SOFTWARE, ctx->digest, full_size, chunk, NULL); ctx->offset += chunk_size; qmi_client_pdc_load_config(ctx->qmi_client, input, 10, NULL, fu_mm_qmi_device_load_config_ready, ctx); } static GArray * fu_mm_qmi_device_get_checksum(GBytes *blob) { gsize file_size; gsize hash_size; GArray *digest; g_autoptr(GChecksum) checksum = NULL; /* get checksum, to be used as unique id */ file_size = g_bytes_get_size(blob); hash_size = g_checksum_type_get_length(G_CHECKSUM_SHA1); checksum = g_checksum_new(G_CHECKSUM_SHA1); g_checksum_update(checksum, g_bytes_get_data(blob, NULL), file_size); /* libqmi expects a GArray of bytes, not a GByteArray */ digest = g_array_sized_new(FALSE, FALSE, sizeof(guint8), hash_size); g_array_set_size(digest, hash_size); g_checksum_get_digest(checksum, (guint8 *)digest->data, &hash_size); return digest; } static GArray * fu_mm_qmi_device_write(FuMmQmiDevice *self, const gchar *filename, GBytes *blob, GError **error) { g_autoptr(GMainLoop) mainloop = g_main_loop_new(NULL, FALSE); g_autoptr(GArray) digest = fu_mm_qmi_device_get_checksum(blob); FuMmQmiDeviceWriteContext ctx = { .mainloop = mainloop, .qmi_client = self->qmi_client, .error = NULL, .indication_id = 0, .timeout_id = 0, .blob = blob, .digest = digest, .offset = 0, .token = 0, }; fu_mm_qmi_device_load_config(&ctx); g_main_loop_run(mainloop); if (ctx.error != NULL) { g_propagate_error(error, ctx.error); return NULL; } return g_steal_pointer(&digest); } typedef struct { GMainLoop *mainloop; QmiClientPdc *qmi_client; GError *error; gulong indication_id; guint timeout_id; GArray *digest; guint token; } FuMmQmiDeviceActivateContext; static gboolean fu_mm_qmi_device_activate_config_timeout(gpointer user_data) { FuMmQmiDeviceActivateContext *ctx = user_data; ctx->timeout_id = 0; g_signal_handler_disconnect(ctx->qmi_client, ctx->indication_id); ctx->indication_id = 0; /* not an error, the device may go away without sending the indication */ g_main_loop_quit(ctx->mainloop); return G_SOURCE_REMOVE; } static void fu_mm_qmi_device_activate_config_indication(QmiClientPdc *client, QmiIndicationPdcActivateConfigOutput *output, FuMmQmiDeviceActivateContext *ctx) { guint16 error_code = 0; g_source_remove(ctx->timeout_id); ctx->timeout_id = 0; g_signal_handler_disconnect(ctx->qmi_client, ctx->indication_id); ctx->indication_id = 0; if (!qmi_indication_pdc_activate_config_output_get_indication_result(output, &error_code, &ctx->error)) { g_main_loop_quit(ctx->mainloop); return; } if (error_code != 0) { g_set_error(&ctx->error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "couldn't activate config: %s", qmi_protocol_error_get_string((QmiProtocolError)error_code)); g_main_loop_quit(ctx->mainloop); return; } /* assume ok */ g_debug("successful activate configuration indication: assuming device reset is ongoing"); g_main_loop_quit(ctx->mainloop); } static void fu_mm_qmi_device_activate_config_ready(GObject *qmi_client, GAsyncResult *res, gpointer user_data) { FuMmQmiDeviceActivateContext *ctx = (FuMmQmiDeviceActivateContext *)user_data; g_autoptr(QmiMessagePdcActivateConfigOutput) output = NULL; output = qmi_client_pdc_activate_config_finish(QMI_CLIENT_PDC(qmi_client), res, &ctx->error); if (output == NULL) { /* If we didn't receive a response, this is a good indication that the device * reset itself, we can consider this a successful operation. * Note: not using g_error_matches() to avoid matching the domain, because the * error may be either QMI_CORE_ERROR_TIMEOUT or MBIM_CORE_ERROR_TIMEOUT (same * numeric value), and we don't want to build-depend on libmbim just for this. */ if (ctx->error->code == QMI_CORE_ERROR_TIMEOUT) { g_debug("request to activate configuration timed out: assuming device " "reset is ongoing"); g_clear_error(&ctx->error); } g_main_loop_quit(ctx->mainloop); return; } if (!qmi_message_pdc_activate_config_output_get_result(output, &ctx->error)) { g_main_loop_quit(ctx->mainloop); return; } /* When we activate the config, if the operation is successful, we'll just * see the modem going away completely. So, do not consider an error the timeout * waiting for the Activate Config indication, as that is actually a good * thing. */ g_warn_if_fail(ctx->indication_id == 0); ctx->indication_id = g_signal_connect(QMI_CLIENT_PDC(ctx->qmi_client), "activate-config", G_CALLBACK(fu_mm_qmi_device_activate_config_indication), ctx); /* don't wait forever */ g_warn_if_fail(ctx->timeout_id == 0); ctx->timeout_id = g_timeout_add_seconds(5, fu_mm_qmi_device_activate_config_timeout, ctx); } static void fu_mm_qmi_device_activate_config(FuMmQmiDeviceActivateContext *ctx) { g_autoptr(QmiMessagePdcActivateConfigInput) input = NULL; input = qmi_message_pdc_activate_config_input_new(); qmi_message_pdc_activate_config_input_set_config_type(input, QMI_PDC_CONFIGURATION_TYPE_SOFTWARE, NULL); qmi_message_pdc_activate_config_input_set_token(input, ctx->token++, NULL); g_debug("activating selected configuration..."); qmi_client_pdc_activate_config(ctx->qmi_client, input, 5, NULL, fu_mm_qmi_device_activate_config_ready, ctx); } static gboolean fu_mm_qmi_device_set_selected_config_timeout(gpointer user_data) { FuMmQmiDeviceActivateContext *ctx = user_data; ctx->timeout_id = 0; g_signal_handler_disconnect(ctx->qmi_client, ctx->indication_id); ctx->indication_id = 0; g_set_error_literal(&ctx->error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "couldn't set selected config: timed out"); g_main_loop_quit(ctx->mainloop); return G_SOURCE_REMOVE; } static void fu_mm_qmi_device_set_selected_config_indication(QmiClientPdc *client, QmiIndicationPdcSetSelectedConfigOutput *output, FuMmQmiDeviceActivateContext *ctx) { guint16 error_code = 0; g_source_remove(ctx->timeout_id); ctx->timeout_id = 0; g_signal_handler_disconnect(ctx->qmi_client, ctx->indication_id); ctx->indication_id = 0; if (!qmi_indication_pdc_set_selected_config_output_get_indication_result(output, &error_code, &ctx->error)) { g_main_loop_quit(ctx->mainloop); return; } if (error_code != 0) { g_set_error(&ctx->error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "couldn't set selected config: %s", qmi_protocol_error_get_string((QmiProtocolError)error_code)); g_main_loop_quit(ctx->mainloop); return; } g_debug("current configuration successfully selected..."); /* now activate config */ fu_mm_qmi_device_activate_config(ctx); } static void fu_mm_qmi_device_set_selected_config_ready(GObject *qmi_client, GAsyncResult *res, gpointer user_data) { FuMmQmiDeviceActivateContext *ctx = (FuMmQmiDeviceActivateContext *)user_data; g_autoptr(QmiMessagePdcSetSelectedConfigOutput) output = NULL; output = qmi_client_pdc_set_selected_config_finish(QMI_CLIENT_PDC(qmi_client), res, &ctx->error); if (output == NULL) { g_main_loop_quit(ctx->mainloop); return; } if (!qmi_message_pdc_set_selected_config_output_get_result(output, &ctx->error)) { g_main_loop_quit(ctx->mainloop); return; } /* after receiving the response to our request, we now expect an indication * with the actual result of the operation */ g_warn_if_fail(ctx->indication_id == 0); ctx->indication_id = g_signal_connect(QMI_CLIENT_PDC(ctx->qmi_client), "set-selected-config", G_CALLBACK(fu_mm_qmi_device_set_selected_config_indication), ctx); /* don't wait forever */ g_warn_if_fail(ctx->timeout_id == 0); ctx->timeout_id = g_timeout_add_seconds(5, fu_mm_qmi_device_set_selected_config_timeout, ctx); } static void fu_mm_qmi_device_set_selected_config(FuMmQmiDeviceActivateContext *ctx) { g_autoptr(QmiMessagePdcSetSelectedConfigInput) input = NULL; input = qmi_message_pdc_set_selected_config_input_new(); qmi_message_pdc_set_selected_config_input_set_type_with_id_v2( input, QMI_PDC_CONFIGURATION_TYPE_SOFTWARE, ctx->digest, NULL); qmi_message_pdc_set_selected_config_input_set_token(input, ctx->token++, NULL); g_debug("selecting current configuration..."); qmi_client_pdc_set_selected_config(ctx->qmi_client, input, 10, NULL, fu_mm_qmi_device_set_selected_config_ready, ctx); } static gboolean fu_mm_qmi_device_activate(FuMmQmiDevice *self, GError **error) { g_autoptr(GMainLoop) mainloop = g_main_loop_new(NULL, FALSE); FuMmQmiDeviceActivateContext ctx = { .mainloop = mainloop, .qmi_client = self->qmi_client, .error = NULL, .indication_id = 0, .timeout_id = 0, .digest = self->active_id, .token = 0, }; fu_mm_qmi_device_set_selected_config(&ctx); g_main_loop_run(mainloop); if (ctx.error != NULL) { g_propagate_error(error, ctx.error); return FALSE; } return TRUE; } static gboolean fu_mm_qmi_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuMmQmiDevice *self = FU_MM_QMI_DEVICE(device); /* ignore action if there is no active id specified */ if (self->active_id == NULL) return TRUE; return fu_mm_qmi_device_activate(self, error); } static gboolean fu_mm_qmi_device_detach(FuDevice *device, FuProgress *progress, GError **error) { // FuMmQmiDevice *self = FU_MM_QMI_DEVICE(device); return TRUE; } static gboolean fu_mm_qmi_device_probe(FuDevice *device, GError **error) { FuMmQmiDevice *self = FU_MM_QMI_DEVICE(device); return fu_mm_device_set_device_file(FU_MM_DEVICE(self), MM_MODEM_PORT_TYPE_QMI, error); } typedef struct { gchar *filename; GBytes *bytes; GArray *digest; gboolean active; } FuMmQmiDeviceFileInfo; static void fu_mm_qmi_device_file_info_free(FuMmQmiDeviceFileInfo *file_info) { g_clear_pointer(&file_info->digest, g_array_unref); g_free(file_info->filename); g_bytes_unref(file_info->bytes); g_free(file_info); } typedef struct { FuMmQmiDevice *self; GError *error; GPtrArray *file_infos; } FuMmQmiDeviceArchiveIterateCtx; static gboolean fu_mm_qmi_device_should_be_active(const gchar *version, const gchar *filename) { g_auto(GStrv) split = NULL; g_autofree gchar *carrier_id = NULL; /* The filename of the mcfg file is composed of a "mcfg." prefix, then the * carrier code, followed by the carrier version, and finally a ".mbn" * prefix. Here we try to guess, based on the carrier code, whether the * specific mcfg file should be activated after the firmware upgrade * operation. * * This logic requires that the previous device version includes the carrier * code also embedded in the version string. E.g. "xxxx.VF.xxxx". If we find * this match, we assume this is the active config to use. */ split = g_strsplit(filename, ".", -1); if (g_strv_length(split) < 4) return FALSE; if (g_strcmp0(split[0], "mcfg") != 0) return FALSE; carrier_id = g_strdup_printf(".%s.", split[1]); return (g_strstr_len(version, -1, carrier_id) != NULL); } static gboolean fu_mm_qmi_device_archive_iterate_mcfg_cb(FuArchive *archive, const gchar *filename, GBytes *bytes, gpointer user_data, GError **error) { FuMmQmiDeviceArchiveIterateCtx *ctx = user_data; FuMmQmiDeviceFileInfo *file_info; /* filenames should be named as 'mcfg.*.mbn', e.g.: mcfg.A2.018.mbn */ if (!g_str_has_prefix(filename, "mcfg.") || !g_str_has_suffix(filename, ".mbn")) return TRUE; file_info = g_new0(FuMmQmiDeviceFileInfo, 1); file_info->filename = g_strdup(filename); file_info->bytes = g_bytes_ref(bytes); file_info->active = fu_mm_qmi_device_should_be_active(fu_device_get_version(FU_DEVICE(ctx->self)), filename); g_ptr_array_add(ctx->file_infos, file_info); return TRUE; } static gboolean fu_mm_qmi_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuMmQmiDevice *self = FU_MM_QMI_DEVICE(device); g_autoptr(FuArchive) archive = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(GPtrArray) file_infos = g_ptr_array_new_with_free_func((GDestroyNotify)fu_mm_qmi_device_file_info_free); gint active_i = -1; FuMmQmiDeviceArchiveIterateCtx archive_context = { .self = self, .error = NULL, .file_infos = file_infos, }; /* decompress entire archive ahead of time */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; archive = fu_archive_new_stream(stream, FU_ARCHIVE_FLAG_IGNORE_PATH, error); if (archive == NULL) return FALSE; /* process the list of MCFG files to write */ if (!fu_archive_iterate(archive, fu_mm_qmi_device_archive_iterate_mcfg_cb, &archive_context, error)) return FALSE; for (guint i = 0; i < file_infos->len; i++) { FuMmQmiDeviceFileInfo *file_info = g_ptr_array_index(file_infos, i); file_info->digest = fu_mm_qmi_device_write(self, file_info->filename, file_info->bytes, &archive_context.error); if (file_info->digest == NULL) { g_prefix_error(&archive_context.error, "Failed to write file '%s':", file_info->filename); break; } /* if we wrongly detect more than one, just assume the latest one; this * is not critical, it may just take a bit more time to perform the * automatic carrier config switching in ModemManager */ if (file_info->active) active_i = i; } /* set expected active configuration */ if (active_i >= 0 && self->active_id != NULL) { FuMmQmiDeviceFileInfo *file_info = g_ptr_array_index(file_infos, active_i); self->active_id = g_array_ref(file_info->digest); } if (archive_context.error != NULL) { g_propagate_error(error, archive_context.error); return FALSE; } /* success */ return TRUE; } static gboolean fu_mm_qmi_device_prepare(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuMmQmiDevice *self = FU_MM_QMI_DEVICE(device); fu_mm_device_set_inhibited(FU_MM_DEVICE(self), TRUE); return TRUE; } static gboolean fu_mm_qmi_device_cleanup(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuMmQmiDevice *self = FU_MM_QMI_DEVICE(device); fu_mm_device_set_inhibited(FU_MM_DEVICE(self), FALSE); return TRUE; } static void fu_mm_qmi_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 97, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reload"); } static void fu_mm_qmi_device_init(FuMmQmiDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "com.qualcomm.qmi_pdc"); } static void fu_mm_qmi_device_finalize(GObject *object) { FuMmQmiDevice *self = FU_MM_QMI_DEVICE(object); if (self->active_id) g_array_unref(self->active_id); G_OBJECT_CLASS(fu_mm_qmi_device_parent_class)->finalize(object); } static void fu_mm_qmi_device_class_init(FuMmQmiDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->finalize = fu_mm_qmi_device_finalize; device_class->attach = fu_mm_qmi_device_attach; device_class->detach = fu_mm_qmi_device_detach; device_class->open = fu_mm_qmi_device_open; device_class->close = fu_mm_qmi_device_close; device_class->prepare = fu_mm_qmi_device_prepare; device_class->cleanup = fu_mm_qmi_device_cleanup; device_class->probe = fu_mm_qmi_device_probe; device_class->set_progress = fu_mm_qmi_device_set_progress; device_class->write_firmware = fu_mm_qmi_device_write_firmware; } fwupd-2.0.10/plugins/modem-manager/fu-mm-qmi-device.h000066400000000000000000000005521501337203100223210ustar00rootroot00000000000000/* * Copyright 2019 Aleksander Morgado * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-mm-device.h" #define FU_TYPE_MM_QMI_DEVICE (fu_mm_qmi_device_get_type()) G_DECLARE_FINAL_TYPE(FuMmQmiDevice, fu_mm_qmi_device, FU, MM_QMI_DEVICE, FuMmDevice) fwupd-2.0.10/plugins/modem-manager/fu-self-test.c000066400000000000000000000015151501337203100215700ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-mm-device.h" static void fu_mm_device_func(void) { gboolean ret; g_autoptr(FuMmDevice) mm_device = g_object_new(FU_TYPE_MM_DEVICE, NULL); g_autoptr(GError) error = NULL; fu_device_set_physical_id(FU_DEVICE(mm_device), "/tmp"); ret = fu_mm_device_set_autosuspend_delay(mm_device, 1500, &error); g_assert_no_error(error); g_assert_true(ret); fu_mm_device_set_inhibited(mm_device, TRUE); g_assert_true(fu_mm_device_get_inhibited(mm_device)); fu_mm_device_set_inhibited(mm_device, FALSE); g_assert_false(fu_mm_device_get_inhibited(mm_device)); } int main(int argc, char **argv) { g_test_init(&argc, &argv, NULL); g_test_add_func("/mm/device", fu_mm_device_func); return g_test_run(); } fwupd-2.0.10/plugins/modem-manager/meson.build000066400000000000000000000035711501337203100212540ustar00rootroot00000000000000libmm_glib = dependency('mm-glib', version: '>= 1.22.0', required: get_option('plugin_modem_manager')) libqmi_glib = dependency('qmi-glib', version: '>= 1.32.0', required: get_option('plugin_modem_manager')) libmbim_glib = dependency('mbim-glib', version: '>= 1.28.0', required: get_option('plugin_modem_manager')) if libmm_glib.found() and \ libqmi_glib.found() and \ libmbim_glib.found() and \ get_option('plugin_modem_manager').allowed() cargs = ['-DG_LOG_DOMAIN="FuPluginMm"'] cargs +=['-DMM_REQUIRED_VERSION="1.10.0"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('modem-manager.quirk') shared_module('fu_plugin_modem_manager', rustgen.process( 'fu-mm-fdl.rs', ), sources: [ 'fu-mm-backend.c', 'fu-mm-common.c', 'fu-mm-device.c', 'fu-mm-dfota-device.c', 'fu-mm-fastboot-device.c', 'fu-mm-fdl-device.c', 'fu-mm-firehose-device.c', 'fu-mm-mbim-common.c', 'fu-mm-mbim-device.c', 'fu-mm-mhi-qcdm-device.c', 'fu-mm-plugin.c', 'fu-mm-qcdm-device.c', 'fu-mm-qmi-device.c', ], include_directories: plugin_incdirs, install: true, install_rpath: libdir_pkg, install_dir: libdir_pkg, c_args: cargs, link_with: plugin_libs, dependencies: [ plugin_deps, libmm_glib, libqmi_glib, libmbim_glib, ], ) device_tests += files( 'tests/qc-eg25ggc-fastboot.json', ) enumeration_data += files( 'tests/qc-eg25ggc-fastboot-setup.json', ) if get_option('tests') e = executable( 'modem-manager-self-test', sources: [ 'fu-self-test.c', 'fu-mm-common.c', 'fu-mm-device.c', ], include_directories: plugin_incdirs, dependencies: [ plugin_deps, libmm_glib, libqmi_glib, libmbim_glib, ], link_with: [ plugin_libs, ], c_args: [ cargs, ], ) test('modem-manager-self-test', e) endif endif fwupd-2.0.10/plugins/modem-manager/modem-manager.quirk000066400000000000000000000066061501337203100227020ustar00rootroot00000000000000 # DW5821e [USB\VID_413C&PID_81D7] Summary = Dell DW5821e LTE modem CounterpartGuid = USB\VID_413C&PID_81D6 # DW5821e/eSIM [USB\VID_413C&PID_81E0] Summary = Dell DW5821e/eSIM LTE modem CounterpartGuid = USB\VID_413C&PID_81E1 # T77W968 [USB\VID_0489&PID_E0B4] Summary = Foxconn T77w968 LTE modem CounterpartGuid = USB\VID_0489&PID_E0B7 # T77W968/eSIM [USB\VID_0489&PID_E0B5] Summary = Foxconn T77w968/eSIM LTE modem CounterpartGuid = USB\VID_0489&PID_E0B8 # T99W265 with MBIM only [USB\VID_0489&PID_E0DA] Summary = Foxconn T99W265 LTE modem with MBIM only # T99W265 with MBIM and Serial port [USB\VID_0489&PID_E0DB] Summary = Foxconn T99W265 LTE modem with MBIM and Serial # T99W175 based on QC LE1.2 [PCI\VID_105B&PID_E0AB] Summary = Foxconn T99W175 5G modem # T99W175(DW5930e with eSIM) [PCI\VID_105B&PID_E0B0] Summary = Foxconn T99W175 5G modem with eSIM for DW5930e # T99W175(DW5930e without eSIM) [PCI\VID_105B&PID_E0B1] Summary = Foxconn T99W175 5G modem without eSIM for DW5930e # T99W175 based on QC LE1.4 [PCI\VID_105B&PID_E0BF] Summary = Foxconn T99W175 5G modem # T99W175 variant [PCI\VID_105B&PID_E0C3] Summary = Foxconn T99W175 5G modem # T99W368 Foxconn variant based on SDX65 [PCI\VID_105B&PID_E0D8] Summary = Foxconn T99W368 5G modem # T99W373 Foxconn variant based on SDX62 [PCI\VID_105B&PID_E0D9] Summary = Foxconn T99W373 5G modem # T99W373(DW5932e with eSIM) based on SDX62 [PCI\VID_105B&PID_E0F5] Summary = Foxconn T99W373 5G modem with eSIM for DW5932e # T99W373 (DW5932e without eSIM) based on SDX62 [PCI\VID_105B&PID_E0F9] Summary = Foxconn T99W373 5G modem without eSIM for DW5932e # Quectel EG25-G/EC25 [USB\VID_2C7C&PID_0125] GType = FuMmFastbootDevice Summary = Quectel EG25-G/EC25 modem Flags = detach-at-fastboot-has-no-response Plugin = modem_manager [USB\VID_2C7C&PID_0125&REV_0318&NAME_EG25GGB] ModemManagerBranchAtCommand = AT+GETFWBRANCH # Quectel EM120 firehose prog file [PCI\VID_1EAC&PID_1001] GType = FuMmMhiQcdmDevice ModemManagerFirehoseProgFile = prog_firehose_sdx24.mbn # Quectel EM160 firehose prog file [PCI\VID_1EAC&PID_1002] GType = FuMmMhiQcdmDevice CounterpartGuid = PCI\VEN_1EAC&DEV_1002 ModemManagerFirehoseProgFile = prog_firehose_sdx24.mbn # Quectel EM160 firehose prog file [PCI\VID_1EAC&PID_100D] GType = FuMmMhiQcdmDevice CounterpartGuid = PCI\VEN_1EAC&DEV_100D ModemManagerFirehoseProgFile = prog_firehose_sdx24.mbn # Quectel RM520 firehose prog file [PCI\VID_1EAC&PID_1004] GType = FuMmMhiQcdmDevice CounterpartGuid = PCI\VEN_1EAC&DEV_1004 ModemManagerFirehoseProgFile = prog_firehose_sdx6x.elf # Quectel RM520 firehose prog file [PCI\VID_1EAC&PID_1007] GType = FuMmMhiQcdmDevice CounterpartGuid = PCI\VEN_1EAC&DEV_1007 ModemManagerFirehoseProgFile = prog_firehose_sdx6x.elf # Fibocom FM101 [USB\VID_2CB7&PID_01A2] GType = FuMmFastbootDevice Summary = Fibocom FM101-GL Module Flags = detach-at-fastboot-has-no-response # Cinterion PLS8 [USB\VID_1E2D&PID_0061] GType = FuMmFdlDevice Summary = Cinterion PLS8 AT^SFDL update Plugin = modem_manager # Netprisma LCUR57 firehose prog file [PCI\VID_203E&PID_1000] GType = FuMmMhiQcdmDevice ModemManagerFirehoseProgFile = prog_firehose_sdx24.mbn # Netprisma FCUN69 firehose prog file [PCI\VID_203E&PID_1001] GType = FuMmMhiQcdmDevice ModemManagerFirehoseProgFile = prog_firehose_sdx6x.elf # Fibocom NL668-EAU [USB\VID_1508&PID_1001] GType = FuMmMhiQcdmDevice ModemManagerFirehoseProgFile = prog_nand_firehose_9x07.mbn Flags = disable-zlp fwupd-2.0.10/plugins/modem-manager/tests/000077500000000000000000000000001501337203100202465ustar00rootroot00000000000000fwupd-2.0.10/plugins/modem-manager/tests/qc-eg25ggc-fastboot-setup.json000066400000000000000000000035551501337203100257520ustar00rootroot00000000000000{ "UsbDevices": [ { "Created": "2025-03-05T15:58:10.344062Z", "GType": "FuMmFastbootDevice", "BackendId": "/org/freedesktop/ModemManager1/Modem/1", "DeviceFile": "/dev/ttyUSB2", "Events": [ { "Id": "#f697c6f3" }, { "Id": "#1f7c4061", "Data": "DQpFUlJPUg0K" }, { "Id": "#f697c6f3" }, { "Id": "#1f7c4061", "Data": "DQpFUlJPUg0K" }, { "Id": "#f697c6f3" }, { "Id": "#1f7c4061", "Data": "DQpFUlJPUg0K" }, { "Id": "#ea95ac24" }, { "Id": "#1f7c4061", "Data": "DQpFUlJPUg0K" }, { "Id": "#ea95ac24" }, { "Id": "#1f7c4061", "Data": "DQpFUlJPUg0K" }, { "Id": "#ea95ac24" }, { "Id": "#1f7c4061", "Data": "DQpFUlJPUg0K" }, { "Id": "#685152b7" }, { "Id": "#1f7c4061", "Error": 19, "ErrorMsg": "timeout" }, { "Id": "#685152b7" }, { "Id": "#1f7c4061", "Data": "DQorUUNGRzogInNlY2Jvb3RzdGF0IiwwDQoNCk9LDQo=" } ], "Version": "EG25GGCR07A02M1G_30.202.30.202", "PhysicalId": "/sys/devices/pci0000:00/0000:00:08.1/0000:c5:00.3/usb1/1-2", "BranchAt": "AT+GETFWBRANCH", "DeviceIds": [ "USB\\VID_2C7C&PID_0125&REV_0318&NAME_EG25GGC", "USB\\VID_2C7C&PID_0125&REV_0318", "USB\\VID_2C7C&PID_0125", "USB\\VID_2C7C" ], "Ports": { "net": "/dev/wwp197s0f3u2i4", "at": "/dev/ttyUSB2", "gps": "/dev/ttyUSB1", "qmi": "/dev/cdc-wdm0" }, "DetachAt": "AT+QFASTBOOT" } ] } fwupd-2.0.10/plugins/modem-manager/tests/qc-eg25ggc-fastboot.json000066400000000000000000000006331501337203100246060ustar00rootroot00000000000000{ "name": "Quectel EG25GGC (Fastboot)", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/qc-eg25ggc-fastboot-setup.json", "components": [ { "version": "EG25GGCR07A02M1G_30.202.30.202", "protocol": "com.google.fastboot", "guids": [ "7b52b338-326c-5061-8cc6-d95c247758ad" ] } ] } ] } fwupd-2.0.10/plugins/msr/000077500000000000000000000000001501337203100151745ustar00rootroot00000000000000fwupd-2.0.10/plugins/msr/README.md000066400000000000000000000011171501337203100164530ustar00rootroot00000000000000--- title: Plugin: MSR --- ## Introduction This plugin checks if the Model-specific registers (MSRs) indicate the Direct Connect Interface (DCI) is enabled. DCI allows debugging of Intel processors using the USB3 port. DCI should always be disabled and locked on production hardware as it allows the attacker to disable other firmware protection methods. The result will be stored in a security attribute for HSI. ## External Interface Access This plugin requires read access to `/sys/class/msr`. ## Version Considerations This plugin has been available since fwupd version `1.5.0`. fwupd-2.0.10/plugins/msr/fu-msr-plugin.c000066400000000000000000000572241501337203100200570ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-msr-plugin.h" typedef union { guint32 data; struct { guint32 enabled : 1; guint32 rsrvd : 29; guint32 locked : 1; guint32 debug_occurred : 1; } __attribute__((packed)) fields; /* nocheck:blocked */ } FuMsrIa32Debug; typedef union { guint64 data; struct { guint64 rsrvd : 25; guint64 gds_ctrl : 1; guint64 gds_no : 1; } __attribute__((packed)) fields; /* nocheck:blocked */ } FuMsrIa32ArchCapabilities; typedef union { guint64 data; struct { guint64 rngds_mitg_dis : 1; guint64 rtm_allow : 1; guint64 rtm_locked : 1; guint64 fb_clear_dis : 1; guint64 gds_mitg_dis : 1; guint64 gds_mitg_lock : 1; } __attribute__((packed)) fields; /* nocheck:blocked */ } FuMsrIa32McuOptCtrl; typedef union { guint64 data; struct { guint32 lock_ro : 1; guint32 enable : 1; guint32 key_select : 1; guint32 save_key_for_standby : 1; guint32 policy_encryption_algo : 4; guint32 reserved1 : 23; guint32 bypass_enable : 1; guint32 mk_tme_keyid_bits : 4; guint32 reserved2 : 12; guint32 mk_tme_crypto_algs : 16; } __attribute__((packed)) fields; /* nocheck:blocked */ } FuMsrIa32TmeActivation; typedef union { guint32 data; struct { guint32 unknown0 : 23; /* 0 -> 22 inc */ guint32 sme_is_enabled : 1; guint32 unknown1 : 8; } __attribute__((packed)) fields; /* nocheck:blocked */ } FuMsrAMD64Syscfg; typedef union { guint32 data; struct { guint32 sev_is_enabled : 1; guint32 unknown0 : 31; } __attribute__((packed)) fields; /* nocheck:blocked */ } FuMsrAMD64Sev; typedef union { guint64 data; struct { guint64 smm_locked : 1; guint64 unknown0 : 30; guint64 smm_base_lock : 1; guint64 unknown1 : 1; guint64 smm_pg_cfg_lock : 1; } __attribute__((packed)) fields; /* nocheck:blocked */ } FuMsrAMD64HwCR; struct _FuMsrPlugin { FuPlugin parent_instance; gboolean ia32_debug_supported; gboolean ia32_tme_supported; gboolean ia32_arch_capabilities_supported; gboolean ia32_mcu_opt_ctrl_supported; FuMsrIa32Debug ia32_debug; FuMsrIa32TmeActivation ia32_tme_activation; FuMsrIa32ArchCapabilities ia32_arch_capabilities; FuMsrIa32McuOptCtrl ia32_mcu_opt_ctrl; gboolean amd64_syscfg_supported; gboolean amd64_sev_supported; gboolean amd64_hwcfg_supported; FuMsrAMD64Syscfg amd64_syscfg; FuMsrAMD64Sev amd64_sev; FuMsrAMD64HwCR amd64_hwcfg; }; G_DEFINE_TYPE(FuMsrPlugin, fu_msr_plugin, FU_TYPE_PLUGIN) #define PCI_MSR_IA32_DEBUG_INTERFACE 0xc80 #define PCI_MSR_IA32_TME_ACTIVATION 0x982 #define PCI_MSR_IA32_BIOS_SIGN_ID 0x8b #define PCI_MSR_IA32_ARCH_CAPABILITIES 0x10a #define PCI_MSR_IA32_MCU_OPT_CTRL 0x123 #define PCI_MSR_AMD64_SYSCFG 0xC0010010 #define PCI_MSR_AMD64_SEV 0xC0010131 #define PCI_MSR_AMD64_HWCFG 0xc0010015 static void fu_msr_plugin_to_string(FuPlugin *plugin, guint idt, GString *str) { FuMsrPlugin *self = FU_MSR_PLUGIN(plugin); if (self->ia32_debug_supported) { fwupd_codec_string_append_bool(str, idt, "Ia32DebugInterfaceEnabled", self->ia32_debug.fields.enabled); fwupd_codec_string_append_bool(str, idt, "Ia32DebugInterfaceLocked", self->ia32_debug.fields.locked); fwupd_codec_string_append_bool(str, idt, "Ia32DebugInterfaceDebugOccurred", self->ia32_debug.fields.debug_occurred); } if (self->ia32_tme_supported) { fwupd_codec_string_append_bool(str, idt, "Ia32TmeActivateLockRo", self->ia32_tme_activation.fields.lock_ro); fwupd_codec_string_append_bool(str, idt, "Ia32TmeActivateEnable", self->ia32_tme_activation.fields.enable); fwupd_codec_string_append_bool(str, idt, "Ia32TmeActivateBypassEnable", self->ia32_tme_activation.fields.bypass_enable); } if (self->ia32_mcu_opt_ctrl_supported) { fwupd_codec_string_append_bool(str, idt, "GdsMitgDis", self->ia32_mcu_opt_ctrl.fields.gds_mitg_dis > 0); fwupd_codec_string_append_bool(str, idt, "GdsMitgLock", self->ia32_mcu_opt_ctrl.fields.gds_mitg_lock > 0); } if (self->amd64_syscfg_supported) { fwupd_codec_string_append_bool(str, idt, "Amd64SyscfgSmeIsEnabled", self->amd64_syscfg.fields.sme_is_enabled); } if (self->amd64_sev_supported) { fwupd_codec_string_append_bool(str, idt, "Amd64SevIsEnabled", self->amd64_sev.fields.sev_is_enabled); } if (self->amd64_hwcfg_supported) { fwupd_codec_string_append_bool(str, idt, "Amd64SmmLock", self->amd64_hwcfg.fields.smm_locked > 0); fwupd_codec_string_append_bool(str, idt, "Amd64SmmPgCfgLock", self->amd64_hwcfg.fields.smm_pg_cfg_lock > 0); fwupd_codec_string_append_bool(str, idt, "Amd64SmmBaseLock", self->amd64_hwcfg.fields.smm_base_lock > 0); } } static gboolean fu_msr_plugin_startup(FuPlugin *plugin, FuProgress *progress, GError **error) { FuMsrPlugin *self = FU_MSR_PLUGIN(plugin); guint eax = 0; guint ebx = 0; guint ecx = 0; guint edx = 0; if (!g_file_test("/dev/cpu", G_FILE_TEST_IS_DIR)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "missing kernel support"); return FALSE; } /* sdbg is supported: https://en.wikipedia.org/wiki/CPUID */ if (fu_cpu_get_vendor() == FU_CPU_VENDOR_INTEL) { if (!fu_cpuid(0x01, NULL, NULL, &ecx, NULL, error)) return FALSE; self->ia32_debug_supported = ((ecx >> 11) & 0x1) > 0; if (!fu_cpuid(0x07, NULL, NULL, &ecx, &edx, error)) return FALSE; self->ia32_tme_supported = ((ecx >> 13) & 0x1) > 0; self->ia32_arch_capabilities_supported = ((edx >> 29) & 0x1) > 0; self->ia32_mcu_opt_ctrl_supported = ((edx >> 9) & 0x1) > 0; } /* indicates support for SME and SEV */ if (fu_cpu_get_vendor() == FU_CPU_VENDOR_AMD) { if (!fu_cpuid(0x8000001f, &eax, &ebx, NULL, NULL, error)) return FALSE; g_debug("SME/SEV check MSR: eax 0%x, ebx 0%x", eax, ebx); self->amd64_syscfg_supported = ((eax >> 0) & 0x1) > 0; self->amd64_sev_supported = ((eax >> 1) & 0x1) > 0; self->amd64_hwcfg_supported = TRUE; } return TRUE; } static gboolean fu_msr_plugin_backend_device_added(FuPlugin *plugin, FuDevice *device, FuProgress *progress, GError **error) { FuMsrPlugin *self = FU_MSR_PLUGIN(plugin); FuDevice *device_cpu = fu_plugin_cache_lookup(plugin, "cpu"); guint8 buf[8] = {0x0}; g_autoptr(FuDeviceLocker) locker = NULL; g_autofree gchar *basename = NULL; /* interesting device? */ if (!FU_IS_UDEV_DEVICE(device)) return TRUE; if (g_strcmp0(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device)), "msr") != 0) return TRUE; /* we only care about the first processor */ basename = g_path_get_basename(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device))); if (g_strcmp0(basename, "msr0") != 0) return TRUE; /* open the config */ fu_device_set_physical_id(FU_DEVICE(device), "msr"); locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; /* grab Intel MSR */ if (self->ia32_debug_supported) { if (!fu_udev_device_pread(FU_UDEV_DEVICE(device), PCI_MSR_IA32_DEBUG_INTERFACE, buf, sizeof(buf), error)) { g_prefix_error(error, "could not read IA32_DEBUG_INTERFACE: "); return FALSE; } if (!fu_memread_uint32_safe(buf, sizeof(buf), 0x0, &self->ia32_debug.data, G_LITTLE_ENDIAN, error)) return FALSE; } if (self->ia32_tme_supported) { if (!fu_udev_device_pread(FU_UDEV_DEVICE(device), PCI_MSR_IA32_TME_ACTIVATION, buf, sizeof(buf), error)) { g_prefix_error(error, "could not read IA32_TME_ACTIVATION: "); return FALSE; } if (!fu_memread_uint64_safe(buf, sizeof(buf), 0x0, &self->ia32_tme_activation.data, G_LITTLE_ENDIAN, error)) return FALSE; } if (self->ia32_arch_capabilities_supported) { if (!fu_udev_device_pread(FU_UDEV_DEVICE(device), PCI_MSR_IA32_ARCH_CAPABILITIES, buf, sizeof(buf), error)) { g_prefix_error(error, "could not read IA32_ARCH_CAPABILITIES: "); return FALSE; } if (!fu_memread_uint64_safe(buf, sizeof(buf), 0x0, &self->ia32_arch_capabilities.data, G_LITTLE_ENDIAN, error)) return FALSE; } if (self->ia32_mcu_opt_ctrl_supported) { if (!fu_udev_device_pread(FU_UDEV_DEVICE(device), PCI_MSR_IA32_MCU_OPT_CTRL, buf, sizeof(buf), error)) { g_prefix_error(error, "could not read IA32_MCU_OPT_CTRL: "); return FALSE; } if (!fu_memread_uint64_safe(buf, sizeof(buf), 0x0, &self->ia32_mcu_opt_ctrl.data, G_LITTLE_ENDIAN, error)) return FALSE; } /* grab AMD MSRs */ if (self->amd64_syscfg_supported) { if (!fu_udev_device_pread(FU_UDEV_DEVICE(device), PCI_MSR_AMD64_SYSCFG, buf, sizeof(buf), error)) { g_prefix_error(error, "could not read PCI_MSR_AMD64_SYSCFG: "); return FALSE; } if (!fu_memread_uint32_safe(buf, sizeof(buf), 0x0, &self->amd64_syscfg.data, G_LITTLE_ENDIAN, error)) return FALSE; } if (self->amd64_sev_supported) { if (!fu_udev_device_pread(FU_UDEV_DEVICE(device), PCI_MSR_AMD64_SEV, buf, sizeof(buf), error)) { g_prefix_error(error, "could not read PCI_MSR_AMD64_SEV: "); return FALSE; } if (!fu_memread_uint32_safe(buf, sizeof(buf), 0x0, &self->amd64_sev.data, G_LITTLE_ENDIAN, error)) return FALSE; } if (self->amd64_hwcfg_supported) { if (!fu_udev_device_pread(FU_UDEV_DEVICE(device), PCI_MSR_AMD64_HWCFG, buf, sizeof(buf), error)) { g_prefix_error(error, "could not read PCI_MSR_AMD66_HWCFG: "); return FALSE; } if (!fu_memread_uint64_safe(buf, sizeof(buf), 0x0, &self->amd64_hwcfg.data, G_LITTLE_ENDIAN, error)) return FALSE; } /* get microcode version */ if (device_cpu != NULL) { guint32 ver_raw; guint8 offset; if (!fu_cpuid(0x1, NULL, NULL, NULL, NULL, error)) return FALSE; if (!fu_udev_device_pread(FU_UDEV_DEVICE(device), PCI_MSR_IA32_BIOS_SIGN_ID, buf, sizeof(buf), error)) { g_prefix_error(error, "could not read IA32_BIOS_SIGN_ID: "); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "IA32_BIOS_SIGN_ID", buf, sizeof(buf)); offset = fu_cpu_get_vendor() == FU_CPU_VENDOR_AMD ? 0x0 : 0x4; if (!fu_memread_uint32_safe(buf, sizeof(buf), offset, &ver_raw, G_LITTLE_ENDIAN, error)) return FALSE; if (ver_raw != 0 && ver_raw != G_MAXUINT32) fu_device_set_version_raw(device_cpu, ver_raw); } /* success */ return TRUE; } static void fu_msr_plugin_device_registered(FuPlugin *plugin, FuDevice *dev) { if (g_strcmp0(fu_device_get_plugin(dev), "cpu") == 0) { fu_plugin_cache_add(plugin, "cpu", dev); return; } } static void fu_msr_plugin_add_security_attr_dci_enabled(FuPlugin *plugin, FuSecurityAttrs *attrs) { FuMsrPlugin *self = FU_MSR_PLUGIN(plugin); FuDevice *device = fu_plugin_cache_lookup(plugin, "cpu"); g_autoptr(FwupdSecurityAttr) attr = NULL; /* this MSR is only valid for a subset of Intel CPUs */ if (fu_cpu_get_vendor() != FU_CPU_VENDOR_INTEL) return; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_PLATFORM_DEBUG_ENABLED); if (device != NULL) fwupd_security_attr_add_guids(attr, fu_device_get_guids(device)); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); fu_security_attrs_append(attrs, attr); /* check fields */ if (!self->ia32_debug_supported) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_SUPPORTED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); return; } if (self->ia32_debug.fields.enabled) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_ENABLED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_msr_plugin_add_security_attr_intel_gds(FuPlugin *plugin, FuSecurityAttrs *attrs) { FuMsrPlugin *self = FU_MSR_PLUGIN(plugin); FuDevice *device = fu_plugin_cache_lookup(plugin, "cpu"); const gchar *mitigations_required; g_autoptr(FwupdSecurityAttr) attr = NULL; /* this MSR is only valid for a subset of Intel CPUs */ if (fu_cpu_get_vendor() != FU_CPU_VENDOR_INTEL) return; if (device == NULL) return; /* only specific CPUs are affected by GDS */ mitigations_required = fu_device_get_metadata(device, FU_DEVICE_METADATA_CPU_MITIGATIONS_REQUIRED); if (g_strcmp0(mitigations_required, "gds") != 0) return; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_INTEL_GDS); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_ENABLED); fu_security_attrs_append(attrs, attr); fwupd_security_attr_add_guids(attr, fu_device_get_guids(device)); /* processor is not vulnerable */ if (self->ia32_arch_capabilities.fields.gds_no) { fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); return; } /* enumeration for support of both IA32_MCU_OPT_CTRL[4] and IA32_MCU_OPT_CTRL[5] */ if (!self->ia32_arch_capabilities.fields.gds_ctrl) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* GDS mitigation has to be enabled [and locked] */ if (self->ia32_mcu_opt_ctrl.fields.gds_mitg_dis) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW); return; } if (self->ia32_mcu_opt_ctrl.fields.gds_mitg_lock) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_LOCKED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_msr_plugin_add_security_attr_intel_tme_enabled(FuPlugin *plugin, FuSecurityAttrs *attrs) { FuMsrPlugin *self = FU_MSR_PLUGIN(plugin); g_autoptr(FwupdSecurityAttr) attr = NULL; /* this MSR is only valid for a subset of Intel CPUs */ if (fu_cpu_get_vendor() != FU_CPU_VENDOR_INTEL) return; /* create attr (which should already have been created in the cpu plugin) */ attr = fu_security_attrs_get_by_appstream_id(attrs, FWUPD_SECURITY_ATTR_ID_ENCRYPTED_RAM, NULL); if (attr == NULL) { attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_ENCRYPTED_RAM); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_ENCRYPTED); fu_security_attrs_append(attrs, attr); } /* check fields */ if (!self->ia32_tme_supported) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_SUPPORTED); return; } if (!self->ia32_tme_activation.fields.enable) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); fwupd_security_attr_remove_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW); return; } if (self->ia32_tme_activation.fields.bypass_enable) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENCRYPTED); fwupd_security_attr_remove_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW); return; } if (!self->ia32_tme_activation.fields.lock_ro) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_LOCKED); fwupd_security_attr_remove_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } } static void fu_msr_plugin_add_security_attr_dci_locked(FuPlugin *plugin, FuSecurityAttrs *attrs) { FuMsrPlugin *self = FU_MSR_PLUGIN(plugin); FuDevice *device = fu_plugin_cache_lookup(plugin, "cpu"); g_autoptr(FwupdSecurityAttr) attr = NULL; /* this MSR is only valid for a subset of Intel CPUs */ if (fu_cpu_get_vendor() != FU_CPU_VENDOR_INTEL) return; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_PLATFORM_DEBUG_LOCKED); if (device != NULL) fwupd_security_attr_add_guids(attr, fu_device_get_guids(device)); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_LOCKED); fu_security_attrs_append(attrs, attr); /* check fields */ if (!self->ia32_debug_supported) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_SUPPORTED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); return; } if (!self->ia32_debug.fields.locked) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_LOCKED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static gboolean fu_msr_plugin_safe_kernel_for_sme(FuPlugin *plugin, GError **error) { g_autofree gchar *min = fu_plugin_get_config_value(plugin, "MinimumSmeKernelVersion"); return fu_kernel_check_version(min, error); } static gboolean fu_msr_plugin_kernel_enabled_sme(GError **error) { const gchar *flags; g_autoptr(GHashTable) cpu_attrs = NULL; cpu_attrs = fu_cpu_get_attrs(error); if (cpu_attrs == NULL) return FALSE; flags = g_hash_table_lookup(cpu_attrs, "flags"); if (flags != NULL) { g_auto(GStrv) tokens = g_strsplit(flags, " ", -1); for (guint i = 0; tokens[i] != NULL; i++) { if (g_strcmp0(tokens[i], "sme") == 0) return TRUE; } } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "sme support not enabled by kernel"); return FALSE; } static void fu_msr_plugin_add_security_attr_amd_sme_enabled(FuPlugin *plugin, FuSecurityAttrs *attrs) { FuMsrPlugin *self = FU_MSR_PLUGIN(plugin); FuDevice *device = fu_plugin_cache_lookup(plugin, "cpu"); g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(GError) error_local = NULL; /* this MSR is only valid for a subset of AMD CPUs */ if (fu_cpu_get_vendor() != FU_CPU_VENDOR_AMD) return; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_ENCRYPTED_RAM); if (device != NULL) fwupd_security_attr_add_guids(attr, fu_device_get_guids(device)); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_ENCRYPTED); fu_security_attrs_append(attrs, attr); /* check fields */ if (!self->amd64_syscfg_supported) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_SUPPORTED); return; } if (!self->amd64_syscfg.fields.sme_is_enabled) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENCRYPTED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } if (!fu_msr_plugin_safe_kernel_for_sme(plugin, &error_local)) { g_debug("unable to properly detect SME: %s", error_local->message); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_UNKNOWN); return; } if (!(fu_msr_plugin_kernel_enabled_sme(&error_local))) { g_debug("%s", error_local->message); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENCRYPTED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_OS); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); fwupd_security_attr_add_obsolete(attr, "pci_psp"); } static void fu_msr_plugin_add_security_attr_amd_hwcr(FuPlugin *plugin, FuSecurityAttrs *attrs) { FuMsrPlugin *self = FU_MSR_PLUGIN(plugin); FuDevice *device = fu_plugin_cache_lookup(plugin, "cpu"); gboolean sinkclose_vuln = FALSE; g_autoptr(FwupdSecurityAttr) attr1 = NULL; g_auto(GStrv) mitigations = NULL; /* this MSR is only valid for a subset of AMD CPUs */ if (fu_cpu_get_vendor() != FU_CPU_VENDOR_AMD) return; /* check fields */ if (!self->amd64_hwcfg_supported) return; if (device != NULL) { const gchar *needed = fu_device_get_metadata(device, FU_DEVICE_METADATA_CPU_MITIGATIONS_REQUIRED); if (needed != NULL) mitigations = g_strsplit(needed, ",", -1); } if (mitigations != NULL) { for (guint i = 0; mitigations[i] != NULL; i++) { /* check for sinkclose vulnerability */ if (g_strcmp0(mitigations[i], "sinkclose") == 0) { guint64 min = fu_device_get_metadata_integer( device, FU_DEVICE_METADATA_CPU_SINKCLOSE_MICROCODE_VER); g_debug("microcode version: %" G_GUINT64_FORMAT ", sinkclose microcode version: %" G_GUINT64_FORMAT, fu_device_get_version_raw(device), min); if (fu_device_get_version_raw(device) < min) { g_info("vulnerable to sinkclose"); sinkclose_vuln = TRUE; } continue; } } } /* create attr */ attr1 = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_AMD_SMM_LOCKED); if (device != NULL) fwupd_security_attr_add_guids(attr1, fu_device_get_guids(device)); fwupd_security_attr_set_result_success(attr1, FWUPD_SECURITY_ATTR_RESULT_LOCKED); fu_security_attrs_append(attrs, attr1); if (sinkclose_vuln) { fwupd_security_attr_set_result(attr1, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); fwupd_security_attr_add_flag(attr1, FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE); } else if (!self->amd64_hwcfg.fields.smm_locked || !self->amd64_hwcfg.fields.smm_base_lock) { fwupd_security_attr_set_result(attr1, FWUPD_SECURITY_ATTR_RESULT_NOT_LOCKED); fwupd_security_attr_add_flag(attr1, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); } else fwupd_security_attr_add_flag(attr1, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_msr_plugin_add_security_attrs(FuPlugin *plugin, FuSecurityAttrs *attrs) { fu_msr_plugin_add_security_attr_dci_enabled(plugin, attrs); fu_msr_plugin_add_security_attr_dci_locked(plugin, attrs); fu_msr_plugin_add_security_attr_amd_sme_enabled(plugin, attrs); fu_msr_plugin_add_security_attr_intel_tme_enabled(plugin, attrs); fu_msr_plugin_add_security_attr_intel_gds(plugin, attrs); fu_msr_plugin_add_security_attr_amd_hwcr(plugin, attrs); } static gboolean fu_msr_plugin_modify_config(FuPlugin *plugin, const gchar *key, const gchar *value, GError **error) { const gchar *keys[] = {"MinimumSmeKernelVersion", NULL}; if (!g_strv_contains(keys, key)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "config key %s not supported", key); return FALSE; } return fu_plugin_set_config_value(plugin, key, value, error); } static void fu_msr_plugin_init(FuMsrPlugin *self) { } static void fu_msr_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_udev_subsystem(plugin, "msr"); /* defaults changed here will also be reflected in the fwupd.conf man page */ fu_plugin_set_config_default(plugin, "MinimumSmeKernelVersion", "5.18.0"); } static void fu_msr_plugin_class_init(FuMsrPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_msr_plugin_constructed; plugin_class->to_string = fu_msr_plugin_to_string; plugin_class->startup = fu_msr_plugin_startup; plugin_class->backend_device_added = fu_msr_plugin_backend_device_added; plugin_class->add_security_attrs = fu_msr_plugin_add_security_attrs; plugin_class->device_registered = fu_msr_plugin_device_registered; plugin_class->modify_config = fu_msr_plugin_modify_config; } fwupd-2.0.10/plugins/msr/fu-msr-plugin.h000066400000000000000000000003431501337203100200520ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuMsrPlugin, fu_msr_plugin, FU, MSR_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/msr/fwupd-msr.conf000066400000000000000000000000041501337203100177610ustar00rootroot00000000000000msr fwupd-2.0.10/plugins/msr/meson.build000066400000000000000000000006701501337203100173410ustar00rootroot00000000000000if hsi and has_cpuid cargs = ['-DG_LOG_DOMAIN="FuPluginMsr"'] plugins += {meson.current_source_dir().split('/')[-1]: true} if libsystemd.found() install_data(['fwupd-msr.conf'], install_dir: systemd_modules_load_dir, ) endif plugin_builtins += static_library('fu_plugin_msr', sources: [ 'fu-msr-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/mtd/000077500000000000000000000000001501337203100151575ustar00rootroot00000000000000fwupd-2.0.10/plugins/mtd/README.md000066400000000000000000000046471501337203100164510ustar00rootroot00000000000000--- title: Plugin: MTD --- ## Introduction The Memory Technology Device (MTD) interface is a way of abstracting flash devices as if they were normal block devices. See for more details. This plugin supports the following protocol ID: * `org.infradead.mtd` ## GUID Generation These devices use custom DeviceInstanceId values built from the device `NAME` and DMI data, e.g. * `MTD\NAME_Factory` * `MTD\VENDOR_foo&NAME_baz` * `MTD\VENDOR_foo&PRODUCT_bar&NAME_baz` If the `FirmwareGType` quirk is set for the device then the firmware is read back from the device at daemon startup and parsed for the version number. In the event the firmware has multiple child images then the device GUIDs are used as firmware IDs. ## Update Behavior The MTD device is erased in chunks, written and then read back to verify. Although fwupd can read and write a raw image to the MTD partition there is no automatic way to get the *existing* version number. By providing the `GType` fwupd can read the MTD partition and discover additional metadata about the image. For instance, adding a quirk like: [MTD\VENDOR_PINE64&PRODUCT_PinePhone-Pro&NAME_spi1.0] FirmwareGType = FuUswidFirmware ... and then append or insert the image into the MTD image with prepared SBOM metadata: pip install uswid uswid --load uswid.ini --save metadata.uswid This would allow fwupd to read the MTD image data, look for a [uSWID](https://github.com/hughsie/python-uswid) data section and then parse the metadata from that. Any of the firmware formats supported by `fwupdtool get-firmware-gtypes` that can provide a version can be used. ## Quirk Use This plugin uses the following plugin-specific quirks: ### MtdMetadataOffset The offset to start searching within the MTD partition when using `FirmwareGType`. This is provided to avoid dumping a huge amount of MTD data to access a tiny chunk of data that will not be before a known offset. Since: 1.9.1 ### MtdMetadataSize The size of data to read from the MTD partition when using `FirmwareGType`. This is provided to avoid dumping a huge amount of MTD data to access a tiny chunk of data. Since: 1.9.1 ## Vendor ID Security The vendor ID is set from the system vendor, for example `DMI:LENOVO` ## External Interface Access This plugin requires read/write access to `/dev/mtd`. ## Version Considerations This plugin has been available since fwupd version `1.7.2`. fwupd-2.0.10/plugins/mtd/fu-mtd-device.c000066400000000000000000000423611501337203100177620ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #ifdef HAVE_MTD_USER_H #include #endif #include "fu-mtd-device.h" #include "fu-mtd-ifd-device.h" struct _FuMtdDevice { FuUdevDevice parent_instance; guint64 erasesize; guint64 metadata_offset; guint64 metadata_size; }; G_DEFINE_TYPE(FuMtdDevice, fu_mtd_device, FU_TYPE_UDEV_DEVICE) #define FU_MTD_DEVICE_IOCTL_TIMEOUT 5000 /* ms */ static void fu_mtd_device_to_string(FuDevice *device, guint idt, GString *str) { FuMtdDevice *self = FU_MTD_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "EraseSize", self->erasesize); fwupd_codec_string_append_hex(str, idt, "MetadataOffset", self->metadata_offset); fwupd_codec_string_append_hex(str, idt, "MetadataSize", self->metadata_size); } static FuFirmware * fu_mtd_device_read_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuMtdDevice *self = FU_MTD_DEVICE(device); const gchar *fn; g_autoptr(FuFirmware) firmware = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(GInputStream) stream_partial = NULL; /* read contents at the search offset */ fn = fu_udev_device_get_device_file(FU_UDEV_DEVICE(self)); if (fn == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Not supported as no device file"); return NULL; } stream = fu_input_stream_from_path(fn, error); if (stream == NULL) { g_prefix_error(error, "failed to open device: "); return NULL; } if (self->metadata_size > 0) { stream_partial = fu_partial_input_stream_new(stream, self->metadata_offset, self->metadata_size, error); if (stream_partial == NULL) return NULL; } else { stream_partial = g_object_ref(stream); } firmware = g_object_new(fu_device_get_firmware_gtype(FU_DEVICE(self)), NULL); if (!fu_firmware_parse_stream(firmware, stream_partial, 0x0, FU_FIRMWARE_PARSE_FLAG_CACHE_STREAM, error)) { g_prefix_error(error, "failed to parse image: "); return NULL; } /* success */ return g_steal_pointer(&firmware); } static gboolean fu_mtd_device_metadata_load(FuMtdDevice *self, GError **error) { GPtrArray *instance_ids; g_autoptr(FuFirmware) firmware_child = NULL; g_autoptr(GFile) file = NULL; g_autoptr(GPtrArray) imgs = NULL; g_autoptr(FuFirmware) firmware = NULL; /* read firmware from stream */ firmware = fu_mtd_device_read_firmware(FU_DEVICE(self), NULL, error); if (firmware == NULL) return FALSE; /* add each IFD image as a sub-device */ imgs = fu_firmware_get_images(firmware); if (FU_IS_IFD_FIRMWARE(firmware)) { for (guint i = 0; i < imgs->len; i++) { FuIfdImage *img = g_ptr_array_index(imgs, i); g_autoptr(FuMtdIfdDevice) child = fu_mtd_ifd_device_new(FU_DEVICE(self), img); fu_device_add_child(FU_DEVICE(self), FU_DEVICE(child)); } return TRUE; } /* find the firmware child that matches any of the device GUID, then use the first * child that have a version, and finally use the main firmware as a fallback */ instance_ids = fu_device_get_instance_ids(FU_DEVICE(self)); for (guint i = 0; i < instance_ids->len; i++) { const gchar *instance_id = g_ptr_array_index(instance_ids, i); g_autofree gchar *guid = fwupd_guid_hash_string(instance_id); firmware_child = fu_firmware_get_image_by_id(firmware, guid, NULL); if (firmware_child != NULL) break; } for (guint i = 0; i < imgs->len; i++) { FuFirmware *firmare_tmp = g_ptr_array_index(imgs, i); if (fu_firmware_get_version(firmare_tmp) != NULL || fu_firmware_get_version_raw(firmare_tmp) != 0) { firmware_child = g_object_ref(firmare_tmp); break; } } if (firmware_child == NULL) firmware_child = g_object_ref(firmware); /* copy over the version */ if (fu_firmware_get_version(firmware_child) != NULL) { fu_device_set_version(FU_DEVICE(self), /* nocheck:set-version */ fu_firmware_get_version(firmware_child)); } if (fu_firmware_get_version_raw(firmware_child) != G_MAXUINT64) { fu_device_set_version_raw(FU_DEVICE(self), fu_firmware_get_version_raw(firmware_child)); } /* success */ return TRUE; } static gboolean fu_mtd_device_setup(FuDevice *device, GError **error) { FuMtdDevice *self = FU_MTD_DEVICE(device); GType firmware_gtype = fu_device_get_firmware_gtype(device); g_autoptr(GError) error_local = NULL; /* nothing to do */ if (firmware_gtype == G_TYPE_INVALID) return TRUE; if (!fu_mtd_device_metadata_load(self, &error_local)) { g_warning("no version metadata found: %s", error_local->message); return TRUE; } /* success */ return TRUE; } static gboolean fu_mtd_device_open(FuDevice *device, GError **error) { g_autoptr(GError) error_local = NULL; /* FuUdevDevice->open */ if (!FU_DEVICE_CLASS(fu_mtd_device_parent_class)->open(device, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_PERMISSION_DENIED)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, error_local->message); return FALSE; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } /* success */ return TRUE; } static gboolean fu_mtd_device_probe(FuDevice *device, GError **error) { FuContext *ctx = fu_device_get_context(device); FuMtdDevice *self = FU_MTD_DEVICE(device); const gchar *vendor; guint64 flags = 0; guint64 size = 0; g_autofree gchar *attr_flags = NULL; g_autofree gchar *attr_size = NULL; g_autofree gchar *attr_name = NULL; g_autoptr(GError) error_local = NULL; /* FuUdevDevice->probe */ if (!FU_DEVICE_CLASS(fu_mtd_device_parent_class)->probe(device, error)) return FALSE; /* set physical ID */ if (!fu_udev_device_set_physical_id(FU_UDEV_DEVICE(device), "mtd", error)) return FALSE; /* flags have to exist */ attr_flags = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(self), "flags", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, &error_local); if (attr_flags == NULL) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no MTD flags"); return FALSE; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } if (!fu_strtoull(attr_flags, &flags, 0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error)) return FALSE; /* get name */ attr_name = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(device), "name", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (attr_name != NULL) fu_device_set_name(FU_DEVICE(self), attr_name); /* set vendor ID as the BIOS vendor */ vendor = fu_context_get_hwid_value(ctx, FU_HWIDS_KEY_MANUFACTURER); fu_device_build_vendor_id(device, "DMI", vendor); /* use vendor and product as an optional instance ID prefix */ fu_device_add_instance_strsafe(device, "NAME", attr_name); fu_device_add_instance_strsafe(device, "VENDOR", vendor); fu_device_add_instance_strsafe(device, "PRODUCT", fu_context_get_hwid_value(ctx, FU_HWIDS_KEY_PRODUCT_NAME)); fu_device_build_instance_id(device, NULL, "MTD", "NAME", NULL); fu_device_build_instance_id(device, NULL, "MTD", "VENDOR", "NAME", NULL); fu_device_build_instance_id(device, NULL, "MTD", "VENDOR", "PRODUCT", "NAME", NULL); /* get properties about the device */ attr_size = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(self), "size", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, error); if (attr_size == NULL) return FALSE; if (!fu_strtoull(attr_size, &size, 0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_device_set_firmware_size_max(device, size); #ifdef HAVE_MTD_USER_H if ((flags & MTD_NO_ERASE) == 0) { g_autofree gchar *attr_erasesize = NULL; attr_erasesize = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(self), "erasesize", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, error); if (attr_erasesize == NULL) return FALSE; if (!fu_strtoull(attr_erasesize, &self->erasesize, 0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error)) return FALSE; } if (flags & MTD_WRITEABLE) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); } #endif /* success */ return TRUE; } static gboolean fu_mtd_device_erase(FuMtdDevice *self, GInputStream *stream, FuProgress *progress, GError **error) { #ifdef HAVE_MTD_USER_H g_autoptr(FuChunkArray) chunks = NULL; chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, self->erasesize, error); if (chunks == NULL) return FALSE; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); /* erase each chunk */ for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { struct erase_info_user erase = {0x0}; g_autoptr(FuChunk) chk = NULL; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; erase.start = fu_chunk_get_address(chk); erase.length = fu_chunk_get_data_sz(chk); if (!fu_ioctl_execute(ioctl, 2, (guint8 *)&erase, sizeof(erase), NULL, FU_MTD_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_NONE, error)) { g_prefix_error(error, "failed to erase @0x%x: ", (guint)erase.start); return FALSE; } fu_progress_step_done(progress); } /* success */ return TRUE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Not supported as mtd-user.h is unavailable"); return FALSE; #endif } static gboolean fu_mtd_device_write(FuMtdDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); /* rewind */ if (!fu_udev_device_seek(FU_UDEV_DEVICE(self), 0x0, error)) { g_prefix_error(error, "failed to rewind: "); return FALSE; } /* write each chunk */ for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_udev_device_pwrite(FU_UDEV_DEVICE(self), fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "failed to write @0x%x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_mtd_device_verify(FuMtdDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); /* verify each chunk */ for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autofree guint8 *buf = NULL; g_autoptr(FuChunk) chk = NULL; g_autoptr(GBytes) blob1 = NULL; g_autoptr(GBytes) blob2 = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; buf = g_malloc0(fu_chunk_get_data_sz(chk)); if (!fu_udev_device_pread(FU_UDEV_DEVICE(self), fu_chunk_get_address(chk), buf, fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "failed to read @0x%x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } blob1 = fu_chunk_get_bytes(chk); blob2 = g_bytes_new_static(buf, fu_chunk_get_data_sz(chk)); if (!fu_bytes_compare(blob1, blob2, error)) { g_prefix_error(error, "failed to verify @0x%x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_mtd_device_write_verify(FuMtdDevice *self, GInputStream *stream, FuProgress *progress, GError **error) { g_autoptr(FuChunkArray) chunks = NULL; chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 10 * 1024, error); if (chunks == NULL) return FALSE; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 50, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 50, NULL); /* write */ if (!fu_mtd_device_write(self, chunks, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* verify */ if (!fu_mtd_device_verify(self, chunks, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static GBytes * fu_mtd_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuMtdDevice *self = FU_MTD_DEVICE(device); gsize bufsz = fu_device_get_firmware_size_max(device); g_autofree guint8 *buf = g_malloc0(bufsz); g_autoptr(GPtrArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ); /* read each chunk */ chunks = fu_chunk_array_mutable_new(buf, bufsz, 0x0, 0x0, 10 * 1024); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_udev_device_pread(FU_UDEV_DEVICE(self), fu_chunk_get_address(chk), fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "failed to read @0x%x: ", (guint)fu_chunk_get_address(chk)); return NULL; } fu_progress_step_done(progress); } /* success */ return g_bytes_new_take(g_steal_pointer(&buf), bufsz); } static gboolean fu_mtd_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuMtdDevice *self = FU_MTD_DEVICE(device); gsize streamsz = 0; g_autoptr(GInputStream) stream = NULL; /* get data to write */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz > fu_device_get_firmware_size_max(device)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware too large, got 0x%x, expected <= 0x%x", (guint)streamsz, (guint)fu_device_get_firmware_size_max(device)); return FALSE; } /* just one step required */ if (self->erasesize == 0) return fu_mtd_device_write_verify(self, stream, progress, error); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 50, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 50, NULL); /* erase */ if (!fu_mtd_device_erase(self, stream, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* write */ if (!fu_mtd_device_write_verify(self, stream, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_mtd_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuMtdDevice *self = FU_MTD_DEVICE(device); /* load from quirks */ if (g_strcmp0(key, "MtdMetadataOffset") == 0) { return fu_strtoull(value, &self->metadata_offset, 0x0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error); } if (g_strcmp0(key, "MtdMetadataSize") == 0) { return fu_strtoull(value, &self->metadata_size, 0x100, FU_FIRMWARE_SEARCH_MAGIC_BUFSZ_MAX, FU_INTEGER_BASE_AUTO, error); } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_mtd_device_init(FuMtdDevice *self) { self->metadata_size = FU_FIRMWARE_SEARCH_MAGIC_BUFSZ_MAX; fu_device_set_summary(FU_DEVICE(self), "Memory Technology Device"); fu_device_add_protocol(FU_DEVICE(self), "org.infradead.mtd"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_REBOOT); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_SIGNED); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_DRIVE_SSD); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_SYNC); } static void fu_mtd_device_class_init(FuMtdDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->open = fu_mtd_device_open; device_class->probe = fu_mtd_device_probe; device_class->setup = fu_mtd_device_setup; device_class->to_string = fu_mtd_device_to_string; device_class->dump_firmware = fu_mtd_device_dump_firmware; device_class->read_firmware = fu_mtd_device_read_firmware; device_class->write_firmware = fu_mtd_device_write_firmware; device_class->set_quirk_kv = fu_mtd_device_set_quirk_kv; } fwupd-2.0.10/plugins/mtd/fu-mtd-device.h000066400000000000000000000004351501337203100177630ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_MTD_DEVICE (fu_mtd_device_get_type()) G_DECLARE_FINAL_TYPE(FuMtdDevice, fu_mtd_device, FU, MTD_DEVICE, FuUdevDevice) fwupd-2.0.10/plugins/mtd/fu-mtd-ifd-device.c000066400000000000000000000055721501337203100205250ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-ifd-struct.h" #include "fu-mtd-ifd-device.h" struct _FuMtdIfdDevice { FuDevice parent_instance; FuIfdImage *img; }; G_DEFINE_TYPE(FuMtdIfdDevice, fu_mtd_ifd_device, FU_TYPE_DEVICE) static void fu_mtd_ifd_device_add_security_attr_desc(FuMtdIfdDevice *self, FuSecurityAttrs *attrs) { g_autoptr(FwupdSecurityAttr) attr = NULL; FuIfdAccess ifd_access_global = FALSE; FuIfdRegion regions[] = {FU_IFD_REGION_BIOS, FU_IFD_REGION_ME, FU_IFD_REGION_EC}; /* create attr */ attr = fu_device_security_attr_new(FU_DEVICE(self), FWUPD_SECURITY_ATTR_ID_SPI_DESCRIPTOR); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_LOCKED); fu_security_attrs_append(attrs, attr); /* check each */ for (guint i = 0; i < G_N_ELEMENTS(regions); i++) { FuIfdAccess ifd_access = fu_ifd_image_get_access(self->img, regions[i]); fwupd_security_attr_add_metadata(attr, fu_ifd_region_to_string(regions[i]), fu_ifd_access_to_string(ifd_access)); ifd_access_global |= ifd_access; } if (ifd_access_global & FU_IFD_ACCESS_WRITE) { fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_mtd_ifd_device_add_security_attrs(FuDevice *device, FuSecurityAttrs *attrs) { FuMtdIfdDevice *self = FU_MTD_IFD_DEVICE(device); if (self->img == NULL) return; if (fu_firmware_get_idx(FU_FIRMWARE(self->img)) == FU_IFD_REGION_DESC) fu_mtd_ifd_device_add_security_attr_desc(self, attrs); } static gboolean fu_mtd_ifd_device_probe(FuDevice *device, GError **error) { FuMtdIfdDevice *self = FU_MTD_IFD_DEVICE(device); if (self->img != NULL) { FuIfdRegion region = fu_firmware_get_idx(FU_FIRMWARE(self->img)); fu_device_set_name(device, fu_ifd_region_to_name(region)); fu_device_set_logical_id(device, fu_ifd_region_to_string(region)); fu_device_add_instance_str(device, "REGION", fu_ifd_region_to_string(region)); } if (!fu_device_build_instance_id(device, error, "IFD", "REGION", NULL)) return FALSE; /* success */ return TRUE; } static void fu_mtd_ifd_device_init(FuMtdIfdDevice *self) { fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_COMPUTER); } static void fu_mtd_ifd_device_class_init(FuMtdIfdDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_mtd_ifd_device_probe; device_class->add_security_attrs = fu_mtd_ifd_device_add_security_attrs; } FuMtdIfdDevice * fu_mtd_ifd_device_new(FuDevice *parent, FuIfdImage *img) { FuMtdIfdDevice *self = g_object_new(FU_TYPE_MTD_IFD_DEVICE, "parent", parent, "proxy", parent, NULL); self->img = g_object_ref(img); return self; } fwupd-2.0.10/plugins/mtd/fu-mtd-ifd-device.h000066400000000000000000000005721501337203100205250ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-mtd-device.h" #define FU_TYPE_MTD_IFD_DEVICE (fu_mtd_ifd_device_get_type()) G_DECLARE_FINAL_TYPE(FuMtdIfdDevice, fu_mtd_ifd_device, FU, MTD_IFD_DEVICE, FuDevice) FuMtdIfdDevice * fu_mtd_ifd_device_new(FuDevice *parent, FuIfdImage *img); fwupd-2.0.10/plugins/mtd/fu-mtd-plugin.c000066400000000000000000000024651501337203100200220ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-mtd-device.h" #include "fu-mtd-ifd-device.h" #include "fu-mtd-plugin.h" struct _FuMtdPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuMtdPlugin, fu_mtd_plugin, FU_TYPE_PLUGIN) static gboolean fu_mtd_plugin_startup(FuPlugin *plugin, FuProgress *progress, GError **error) { #ifndef HAVE_MTD_USER_H g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Not compiled with mtd support"); return FALSE; #endif return TRUE; } static void fu_mtd_plugin_init(FuMtdPlugin *self) { } static void fu_mtd_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "MtdMetadataOffset"); fu_context_add_quirk_key(ctx, "MtdMetadataSize"); fu_plugin_add_device_udev_subsystem(plugin, "mtd"); fu_plugin_set_device_gtype_default(plugin, FU_TYPE_MTD_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_MTD_IFD_DEVICE); /* coverage */ } static void fu_mtd_plugin_class_init(FuMtdPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_mtd_plugin_constructed; plugin_class->startup = fu_mtd_plugin_startup; } fwupd-2.0.10/plugins/mtd/fu-mtd-plugin.h000066400000000000000000000003431501337203100200200ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuMtdPlugin, fu_mtd_plugin, FU, MTD_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/mtd/fu-self-test.c000066400000000000000000000061221501337203100176420ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-context-private.h" #include "fu-mtd-device.h" #include "fu-udev-device-private.h" static void fu_test_mtd_device_func(void) { gsize bufsz; gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuDevice) device = NULL; g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(FuFirmware) firmware = NULL; g_autoptr(FuProgress) progress = fu_progress_new(NULL); g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GBytes) fw2 = NULL; g_autoptr(GBytes) fw = NULL; g_autoptr(GError) error = NULL; g_autoptr(GRand) rand = g_rand_new_with_seed(0); /* do not save silo */ ret = fu_context_load_quirks(ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_context_load_hwinfo(ctx, progress, FU_CONTEXT_HWID_FLAG_LOAD_SMBIOS, &error); g_assert_no_error(error); g_assert_true(ret); /* create device */ device = g_object_new(FU_TYPE_MTD_DEVICE, "context", ctx, "backend-id", "/sys/devices/virtual/mtd/mtd0", NULL); locker = fu_device_locker_new(device, &error); if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND) || g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_test_skip("no permission to read mtdram device"); return; } g_assert_no_error(error); g_assert_nonnull(locker); if (!g_file_test(fu_udev_device_get_device_file(FU_UDEV_DEVICE(device)), G_FILE_TEST_EXISTS)) { g_test_skip("/dev/mtd0 doesn't exist"); return; } if (g_strcmp0(fu_device_get_name(device), "mtdram test device") != 0) { g_test_skip("device is not mtdram test device"); return; } bufsz = fu_device_get_firmware_size_max(device); g_assert_cmpint(bufsz, ==, 0x400000); /* create a random payload exactly the same size */ for (gsize i = 0; i < bufsz; i++) fu_byte_array_append_uint8(buf, g_rand_int_range(rand, 0x00, 0xFF)); fw = g_bytes_new(buf->data, buf->len); /* write with a verify */ firmware = fu_firmware_new_from_bytes(fw); ret = fu_device_write_firmware(device, firmware, progress, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); /* dump back */ fu_progress_reset(progress); fw2 = fu_device_dump_firmware(device, progress, &error); g_assert_no_error(error); g_assert_nonnull(fw2); /* verify */ ret = fu_bytes_compare(fw, fw2, &error); g_assert_no_error(error); g_assert_true(ret); } int main(int argc, char **argv) { g_autofree gchar *testdatadir = NULL; (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); (void)g_setenv("FWUPD_MTD_VERBOSE", "1", TRUE); testdatadir = g_test_build_filename(G_TEST_DIST, "tests", NULL); (void)g_setenv("FWUPD_SYSFSFWDIR", testdatadir, TRUE); (void)g_setenv("FWUPD_SYSFSFWATTRIBDIR", testdatadir, TRUE); (void)g_setenv("CONFIGURATION_DIRECTORY", testdatadir, TRUE); g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); g_test_add_func("/mtd/device", fu_test_mtd_device_func); return g_test_run(); } fwupd-2.0.10/plugins/mtd/meson.build000066400000000000000000000021311501337203100173160ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginMtd"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('mtd.quirk') plugin_builtin_mtd = static_library('fu_plugin_mtd', sources: [ 'fu-mtd-plugin.c', 'fu-mtd-device.c', 'fu-mtd-ifd-device.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) plugin_builtins += plugin_builtin_mtd if get_option('tests') env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) e = executable( 'mtd-self-test', sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: plugin_deps, link_with: [ plugin_libs, plugin_builtin_mtd, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ cargs, '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('mtd-self-test', e, env: env) # added to installed-tests endif endif fwupd-2.0.10/plugins/mtd/mtd.quirk000066400000000000000000000006611501337203100170230ustar00rootroot00000000000000[MTD\VENDOR_PINE64&PRODUCT_PinePhone-Pro&NAME_spi1.0] Name = Tow-Boot platform firmware FirmwareGType = FuUswidFirmware # Intel GPUs [MTD\NAME_i915.spi.1024.DAM] Flags = no-probe [MTD\NAME_i915.spi.1024.DESCRIPTOR] Flags = no-probe [MTD\NAME_i915.spi.1024.GSC] Flags = no-probe [MTD\NAME_i915.spi.1024.OptionROM] Flags = no-probe # Intel SPI Controller [MTD\NAME_BIOS] Name = Internal SPI Controller #FirmwareGType = FuIfdFirmware fwupd-2.0.10/plugins/nordic-hid/000077500000000000000000000000001501337203100164135ustar00rootroot00000000000000fwupd-2.0.10/plugins/nordic-hid/README.md000066400000000000000000000113301501337203100176700ustar00rootroot00000000000000--- title: Plugin: Nordic HID --- ## Introduction This plugin is able to update the firmware for the hardware supported by [nRF Desktop](https://www.nordicsemi.com/Products/Reference-designs/nRF-Desktop) application reference design developed as part of the [nRF Connect SDK](https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/applications/nrf_desktop/README.html). Tested with the following devices: * [nRF52840 Development Kit](https://docs.nordicsemi.com/bundle/ncs-latest/page/zephyr/boards/nordic/nrf52840dk/doc/index.html) * [nRF52840 Dongle](https://docs.nordicsemi.com/bundle/ncs-latest/page/zephyr/boards/nordic/nrf52840dongle/doc/index.html) * Boards specific to the nRF Desktop reference design: * nRF52840 Gaming Mouse * nRF52832 Desktop Keyboard The plugin is using Nordic Semiconductor [HID configuration channel](https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/applications/nrf_desktop/doc/config_channel.html) to perform devices update. ## Firmware Format The cabinet file contains ZIP archive prepared by Nordic Semiconductor. This ZIP archive includes either 1 or 2 signed image blobs for the target device (one firmware blob per application update slot) and the `manifest.json` file with the metadata description. At the moment only [nRF Secure Immutable Bootloader](https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/samples/bootloader/README.html) ("B0") is supported and tested. The [MCUboot bootloader](https://docs.nordicsemi.com/bundle/ncs-latest/page/mcuboot/wrapper.html) support is experimental. When a device uses MCUboot bootloader, the plugin supports application update in one of the following modes: * Swap mode - MCUboot bootloader swaps the application images located on the secondary and primary slots before booting the new image from the primary slot. * Direct-xip mode - MCUboot bootloader boots a new application image directly from either primary or secondary image slot (the bootloader boots application image with higher version). The used application update mode depends on configuration of the MCUboot bootloader. This plugin supports the following protocol ID: * Nordic HID Config Channel: `com.nordic.hidcfgchannel` ## GUID Generation For GUID generation the target board name, bootloader name and generation are used in addition to standard HIDRAW DeviceInstanceId values. The generation string is an application-specific property that allows to distinguish configurations that use the same board and bootloader, but are not interoperable. GUID examples: * `HIDRAW\VEN_1915&DEV_52DE&BOARD_nrf52840dk&BL_B0&GEN_default` * `HIDRAW\VEN_1915&DEV_52DE&BOARD_nrf52840dk&BL_MCUBOOT&GEN_office` Because handling of the generation parameter was introduced later, it is not supported by older versions of fwupd. To ensure compatibility with firmware updates that were released before introducing the support for the generation parameter, devices with the `default` generation report an additional GUID that omits the generation parameter. GUID examples (devices with generation set to `default` or without support for the generation parameter): * `HIDRAW\VEN_1915&DEV_52DE&BOARD_nrf52840dk&BL_B0` * `HIDRAW\VEN_1915&DEV_52DE&BOARD_nrf52840dk&BL_MCUBOOT` ## Quirk Use This plugin also uses the following plugin-specific quirks: ### NordicHidBootloader Explicitly set the expected bootloader type. Only the `B0` bootloader can be set by the quirk file. Other values can only be provided by device. This quirk must be set for devices without support of `bootloader variant` DFU option. ### NordicHidGeneration Explicitly set the expected generation. Only the `default` generation can be set by the quirk file. Other values can only be provided by device. This quirk must be set for devices that do not support the `devinfo` DFU option. ## Update Behavior The firmware is deployed when the device is in normal runtime mode, and the device will reset when the new firmware has been written. ## Vendor ID Security The vendor ID is set from the HID vendor ID, in this instance set to `HIDRAW:0x1915`. ## External Interface Access This plugin requires ioctl `HIDIOCSFEATURE` and `HIDIOCGFEATURE` access. ## Version Considerations This plugin has been available since fwupd version `1.7.3`. The format version of the `dfu_application.zip` file generated by the nRF Connect SDK build system was updated from `0` to `1` since the [nRF Connect SDK v2.7.0 release](https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/releases_and_maturity/releases/release-notes-2.7.0.html). The format version `1` is supported by the fwupd since the `1.9.25` release. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Marek Pieta: @MarekPieta fwupd-2.0.10/plugins/nordic-hid/fu-nordic-hid-archive.c000066400000000000000000000241461501337203100226350ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * Copyright 2021 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-nordic-hid-archive.h" #include "fu-nordic-hid-firmware-b0.h" #include "fu-nordic-hid-firmware-mcuboot.h" /* the plugin currently supports version format of either 0 or 1 */ #define MIN_VERSION_FORMAT 0 #define MAX_VERSION_FORMAT 1 struct _FuNordicHidArchive { FuFirmwareClass parent_instance; }; G_DEFINE_TYPE(FuNordicHidArchive, fu_nordic_hid_archive, FU_TYPE_FIRMWARE) static const gchar * fu_nordic_hid_archive_parse_file_get_bootloader_name(JsonObject *obj, GError **error) { if (json_object_has_member(obj, "version_B0")) return "B0"; if (json_object_has_member(obj, "version_MCUBOOT")) return "MCUBOOT"; if (json_object_has_member(obj, "version_MCUBOOT+XIP")) return "MCUBOOT+XIP"; g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "only B0 and MCUboot bootloaders are supported"); return NULL; } static FuFirmware * fu_nordic_hid_archive_parse_file_image_create(const gchar *bootloader_name, GError **error) { if (g_strcmp0(bootloader_name, "B0") == 0) return g_object_new(FU_TYPE_NORDIC_HID_FIRMWARE_B0, NULL); if (g_strcmp0(bootloader_name, "MCUBOOT") == 0 || g_strcmp0(bootloader_name, "MCUBOOT+XIP") == 0) return g_object_new(FU_TYPE_NORDIC_HID_FIRMWARE_MCUBOOT, NULL); g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "only B0 and MCUboot bootloaders are supported"); return NULL; } static gchar * fu_nordic_hid_archive_parse_file_get_board_name(JsonObject *obj, gint64 manifest_ver, GError **error) { g_auto(GStrv) board_split = NULL; const gchar *board_name_readout = NULL; if (!json_object_has_member(obj, "board")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "manifest invalid as has no target board information"); return NULL; } board_name_readout = json_object_get_string_member(obj, "board"); if (board_name_readout == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "manifest invalid as has no target board information"); return NULL; } if (manifest_ver == 0) { /* for manifest "format-version" of "0", the board name is represented only * by part of the string before the "_" symbol */ board_split = g_strsplit(board_name_readout, "_", -1); if (board_split[0] == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "manifest invalid as has no board information"); return NULL; } return g_strdup(board_split[0]); } /* duplicate string for consistent memory management */ return g_strdup(board_name_readout); } static gboolean fu_nordic_hid_archive_parse_file_get_flash_area_id_v1(JsonObject *obj, guint *flash_area_id, const gchar *bootloader_name, guint files_cnt, GError **error) { const gchar *image_idx_str = NULL; const gchar *slot_str = NULL; gint64 image_idx = -1; gint64 slot = -1; /* for MCUboot bootloader with swap, if only a single image is available, * the "image_index" and "slot" properties may be omitted */ if (g_strcmp0(bootloader_name, "MCUBOOT") == 0 && files_cnt == 1) { *flash_area_id = 0; return TRUE; } if (!json_object_has_member(obj, "image_index")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "missing image_index property"); return FALSE; } image_idx_str = json_object_get_string_member(obj, "image_index"); if (image_idx_str == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "missing image_index property"); return FALSE; } if (!fu_strtoll(image_idx_str, &image_idx, G_MININT64, G_MAXINT64, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "fu_strtoll failed for image_index:"); return FALSE; } if (!json_object_has_member(obj, "slot")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "missing slot property"); return FALSE; } slot_str = json_object_get_string_member(obj, "slot"); if (slot_str == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "missing slot property"); return FALSE; } if (!fu_strtoll(slot_str, &slot, G_MININT64, G_MAXINT64, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "fu_strtoll failed for slot:"); return FALSE; } if (image_idx != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "unsupported image_index property"); return FALSE; } if (slot != 0 && slot != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "unsupported slot property"); return FALSE; } *flash_area_id = slot; return TRUE; } static gboolean fu_nordic_hid_archive_parse_file_get_flash_area_id(JsonObject *obj, guint *flash_area_id, gint64 manifest_ver, guint file_idx, const gchar *bootloader_name, guint files_cnt, GError **error) { /* for manifest version 0, the images are expected to be listed in strict order */ if (manifest_ver == 0) { *flash_area_id = file_idx; return TRUE; } if (manifest_ver == 1) { return fu_nordic_hid_archive_parse_file_get_flash_area_id_v1(obj, flash_area_id, bootloader_name, files_cnt, error); } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "unsupported manifest version"); return FALSE; } static gboolean fu_nordic_hid_archive_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { JsonNode *json_root_node; JsonObject *json_obj; JsonArray *json_files; gint64 manifest_ver; guint files_cnt = 0; g_autoptr(FuArchive) archive = NULL; g_autoptr(GBytes) manifest = NULL; g_autoptr(JsonParser) parser = json_parser_new(); /* load archive */ archive = fu_archive_new_stream(stream, FU_ARCHIVE_FLAG_IGNORE_PATH, error); if (archive == NULL) return FALSE; manifest = fu_archive_lookup_by_fn(archive, "manifest.json", error); if (manifest == NULL) return FALSE; /* parse JSON */ if (!json_parser_load_from_data(parser, (const gchar *)g_bytes_get_data(manifest, NULL), (gssize)g_bytes_get_size(manifest), error)) { g_prefix_error(error, "manifest not in JSON format: "); return FALSE; } json_root_node = json_parser_get_root(parser); if (json_root_node == NULL || !JSON_NODE_HOLDS_OBJECT(json_root_node)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "manifest invalid as has no root"); return FALSE; } json_obj = json_node_get_object(json_root_node); if (!json_object_has_member(json_obj, "format-version")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "manifest has invalid format"); return FALSE; } manifest_ver = json_object_get_int_member(json_obj, "format-version"); if (manifest_ver < MIN_VERSION_FORMAT || manifest_ver > MAX_VERSION_FORMAT) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "unsupported manifest version"); return FALSE; } json_files = json_object_get_array_member(json_obj, "files"); if (json_files == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "manifest invalid as has no 'files' array"); return FALSE; } files_cnt = json_array_get_length(json_files); if (files_cnt == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "manifest invalid as contains no update images"); return FALSE; } for (guint i = 0; i < files_cnt; i++) { const gchar *filename = NULL; const gchar *bootloader_name = NULL; guint flash_area_id; guint image_addr = 0; JsonObject *obj = json_array_get_object_element(json_files, i); g_autoptr(FuFirmware) image = NULL; g_autofree gchar *board_name = NULL; g_autofree gchar *fwupd_image_id = NULL; g_autoptr(GBytes) blob = NULL; if (!json_object_has_member(obj, "file")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "manifest invalid as has no file name for the image"); return FALSE; } filename = json_object_get_string_member(obj, "file"); blob = fu_archive_lookup_by_fn(archive, filename, error); if (blob == NULL) return FALSE; bootloader_name = fu_nordic_hid_archive_parse_file_get_bootloader_name(obj, error); if (bootloader_name == NULL) return FALSE; image = fu_nordic_hid_archive_parse_file_image_create(bootloader_name, error); if (image == NULL) return FALSE; board_name = fu_nordic_hid_archive_parse_file_get_board_name(obj, manifest_ver, error); if (board_name == NULL) return FALSE; if (!fu_nordic_hid_archive_parse_file_get_flash_area_id(obj, &flash_area_id, manifest_ver, i, bootloader_name, files_cnt, error)) return FALSE; /* used image ID format: __N, i.e "nrf52840dk_B0_bank0" */ fwupd_image_id = g_strdup_printf("%s_%s_bank%01u", board_name, bootloader_name, flash_area_id); if (!fu_firmware_parse_bytes(image, blob, 0x0, flags | FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error)) return FALSE; fu_firmware_set_id(image, fwupd_image_id); fu_firmware_set_idx(image, i); if (json_object_has_member(obj, "load_address")) { image_addr = json_object_get_int_member(obj, "load_address"); fu_firmware_set_addr(image, image_addr); } if (!fu_firmware_add_image_full(firmware, image, error)) return FALSE; } /* success */ return TRUE; } static void fu_nordic_hid_archive_init(FuNordicHidArchive *self) { fu_firmware_set_images_max(FU_FIRMWARE(self), 1024); } static void fu_nordic_hid_archive_class_init(FuNordicHidArchiveClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_nordic_hid_archive_parse; } fwupd-2.0.10/plugins/nordic-hid/fu-nordic-hid-archive.h000066400000000000000000000005021501337203100226300ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_NORDIC_HID_ARCHIVE (fu_nordic_hid_archive_get_type()) G_DECLARE_FINAL_TYPE(FuNordicHidArchive, fu_nordic_hid_archive, FU, NORDIC_HID_ARCHIVE, FuFirmware) fwupd-2.0.10/plugins/nordic-hid/fu-nordic-hid-cfg-channel.c000066400000000000000000001346761501337203100233730ustar00rootroot00000000000000/* * Copyright 2021 Ricardo Cañuelo * Copyright 2021 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-nordic-hid-archive.h" #include "fu-nordic-hid-cfg-channel.h" #define HID_REPORT_ID 6 #define REPORT_SIZE 30 #define REPORT_DATA_MAX_LEN (REPORT_SIZE - 5) #define HWID_LEN 8 #define PEERS_CACHE_LEN 16 #define END_OF_TRANSFER_CHAR 0x0a #define INVALID_PEER_ID 0xFF #define SELF_PEER_ID 0x00 #define FU_NORDIC_HID_CFG_CHANNEL_RETRIES 10 #define FU_NORDIC_HID_CFG_CHANNEL_RETRY_DELAY 50 /* ms */ #define FU_NORDIC_HID_CFG_CHANNEL_DFU_RETRY_DELAY 500 /* ms */ #define FU_NORDIC_HID_CFG_CHANNEL_PEERS_POLL_INTERVAL 2000 /* ms */ typedef enum { CONFIG_STATUS_PENDING, CONFIG_STATUS_GET_MAX_MOD_ID, CONFIG_STATUS_GET_HWID, CONFIG_STATUS_GET_BOARD_NAME, CONFIG_STATUS_INDEX_PEERS, CONFIG_STATUS_GET_PEER, CONFIG_STATUS_SET, CONFIG_STATUS_FETCH, CONFIG_STATUS_SUCCESS, CONFIG_STATUS_TIMEOUT, CONFIG_STATUS_REJECT, CONFIG_STATUS_WRITE_FAIL, CONFIG_STATUS_DISCONNECTED, CONFIG_STATUS_GET_PEERS_CACHE, CONFIG_STATUS_FAULT = 99, } FuNordicCfgStatus; typedef enum { DFU_STATE_INACTIVE, DFU_STATE_ACTIVE, DFU_STATE_STORING, DFU_STATE_CLEANING, } FuNordicCfgSyncState; typedef struct __attribute__((packed)) { /* nocheck:blocked */ guint8 report_id; guint8 recipient; guint8 event_id; guint8 status; guint8 data_len; guint8 data[REPORT_DATA_MAX_LEN]; } FuNordicCfgChannelMsg; typedef struct { guint8 idx; gchar *name; } FuNordicCfgChannelModuleOption; typedef struct { guint8 idx; gchar *name; GPtrArray *options; /* of FuNordicCfgChannelModuleOption */ } FuNordicCfgChannelModule; typedef struct { guint8 status; guint8 *buf; gsize bufsz; } FuNordicCfgChannelRcvHelper; typedef struct { guint8 dfu_state; guint32 img_length; guint32 img_csum; guint32 offset; guint16 sync_buffer_size; } FuNordicCfgChannelDfuInfo; G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuNordicCfgChannelMsg, g_free); G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuNordicCfgChannelDfuInfo, g_free); struct _FuNordicHidCfgChannel { FuHidrawDevice parent_instance; gboolean dfu_support; gboolean peers_cache_support; guint8 peers_cache[PEERS_CACHE_LEN]; gchar *board_name; gchar *bl_name; gchar *generation; guint16 vid; guint16 pid; guint8 flash_area_id; guint32 flashed_image_len; guint8 peer_id; FuUdevDevice *parent_udev; GPtrArray *modules; /* of FuNordicCfgChannelModule */ }; G_DEFINE_TYPE(FuNordicHidCfgChannel, fu_nordic_hid_cfg_channel, FU_TYPE_HIDRAW_DEVICE) static FuNordicHidCfgChannel * fu_nordic_hid_cfg_channel_new(guint8 id, FuNordicHidCfgChannel *parent); static void fu_nordic_hid_cfg_channel_module_option_free(FuNordicCfgChannelModuleOption *opt) { g_free(opt->name); g_free(opt); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuNordicCfgChannelModuleOption, fu_nordic_hid_cfg_channel_module_option_free); static void fu_nordic_hid_cfg_channel_module_free(FuNordicCfgChannelModule *mod) { if (mod->options != NULL) g_ptr_array_unref(mod->options); g_free(mod->name); g_free(mod); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuNordicCfgChannelModule, fu_nordic_hid_cfg_channel_module_free); static FuUdevDevice * fu_nordic_hid_cfg_channel_get_udev_device(FuNordicHidCfgChannel *self, GError **error) { /* ourselves */ if (self->peer_id == SELF_PEER_ID) return FU_UDEV_DEVICE(self); /* parent */ if (self->parent_udev == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no parent for peer 0x%02x", self->peer_id); return NULL; } return self->parent_udev; } static gboolean fu_nordic_hid_cfg_channel_send(FuNordicHidCfgChannel *self, guint8 *buf, gsize bufsz, GError **error) { FuUdevDevice *udev_device = fu_nordic_hid_cfg_channel_get_udev_device(self, error); if (udev_device == NULL) return FALSE; return fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(udev_device), buf, bufsz, FU_IOCTL_FLAG_NONE, error); } static gboolean fu_nordic_hid_cfg_channel_receive(FuNordicHidCfgChannel *self, guint8 *buf, gsize bufsz, GError **error) { g_autoptr(FuNordicCfgChannelMsg) recv_msg = g_new0(FuNordicCfgChannelMsg, 1); FuUdevDevice *udev_device = fu_nordic_hid_cfg_channel_get_udev_device(self, error); if (udev_device == NULL) return FALSE; for (gint i = 1; i < 100; i++) { recv_msg->report_id = HID_REPORT_ID; recv_msg->recipient = self->peer_id; if (!fu_hidraw_device_get_feature(FU_HIDRAW_DEVICE(udev_device), (guint8 *)recv_msg, sizeof(*recv_msg), FU_IOCTL_FLAG_NONE, error)) return FALSE; /* if the device is busy it return 06 00 00 00 00 response */ if (recv_msg->report_id == HID_REPORT_ID && (recv_msg->recipient | recv_msg->event_id | recv_msg->status | recv_msg->data_len)) break; fu_device_sleep(FU_DEVICE(self), 1); /* ms */ } if (!fu_memcpy_safe(buf, bufsz, 0, (guint8 *)recv_msg, sizeof(*recv_msg), 0, sizeof(*recv_msg), error)) { return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "Received", buf, bufsz); /* * [TODO]: Possibly add the report-id fix for Bluez versions < 5.56: * https://github.com/bluez/bluez/commit/35a2c50437cca4d26ac6537ce3a964bb509c9b62 * * See fu_pxi_ble_device_get_feature() in * plugins/pixart-rf/fu-pxi-ble-device.c for an example. */ return TRUE; } static gboolean fu_nordic_hid_cfg_channel_receive_cb(FuDevice *device, gpointer user_data, GError **error) { FuNordicCfgChannelRcvHelper *args = (FuNordicCfgChannelRcvHelper *)user_data; FuNordicHidCfgChannel *self = FU_NORDIC_HID_CFG_CHANNEL(device); FuNordicCfgChannelMsg *recv_msg = NULL; if (!fu_nordic_hid_cfg_channel_receive(self, args->buf, args->bufsz, error)) return FALSE; recv_msg = (FuNordicCfgChannelMsg *)args->buf; if (recv_msg->status != args->status) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "received status: 0x%02x, expected: 0x%02x", recv_msg->status, args->status); return FALSE; } /* success */ return TRUE; } /* * fu_nordic_hid_cfg_channel_get_event_id: * @module_name: module name, NULL for generic operations * @option_name: option name, NULL for generic module operations * * Construct Event ID from module and option names. * * Returns: %TRUE if module/option pair found */ static gboolean fu_nordic_hid_cfg_channel_get_event_id(FuNordicHidCfgChannel *self, const gchar *module_name, const gchar *option_name, guint8 *event_id) { FuNordicCfgChannelModule *mod = NULL; guint id = 0; *event_id = 0; /* for generic operations */ if (module_name == NULL) return TRUE; for (id = 0; id < self->modules->len; id++) { mod = g_ptr_array_index(self->modules, id); if (g_strcmp0(module_name, mod->name) == 0) break; } if (mod == NULL || id > 0x0f) return FALSE; *event_id = id << 4; /* for generic module operations */ if (option_name == NULL) return TRUE; for (guint i = 0; i < mod->options->len && i <= 0x0f; i++) { FuNordicCfgChannelModuleOption *opt = g_ptr_array_index(mod->options, i); if (g_strcmp0(option_name, opt->name) == 0) { *event_id = (id << 4) + opt->idx; return TRUE; } } /* module have no requested option */ return FALSE; } static gboolean fu_nordic_hid_cfg_channel_cmd_send_by_id(FuNordicHidCfgChannel *self, guint8 event_id, guint8 status, guint8 *data, guint8 data_len, GError **error) { g_autoptr(FuNordicCfgChannelMsg) msg = g_new0(FuNordicCfgChannelMsg, 1); msg->report_id = HID_REPORT_ID; msg->recipient = self->peer_id; msg->event_id = event_id; msg->status = status; msg->data_len = 0; if (data != NULL) { if (data_len > REPORT_DATA_MAX_LEN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "requested to send %d bytes, while maximum is %d", data_len, REPORT_DATA_MAX_LEN); return FALSE; } if (!fu_memcpy_safe(msg->data, REPORT_DATA_MAX_LEN, 0, data, data_len, 0, data_len, error)) return FALSE; msg->data_len = data_len; } if (!fu_nordic_hid_cfg_channel_send(self, (guint8 *)msg, sizeof(*msg), error)) { g_prefix_error(error, "failed to send: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_nordic_hid_cfg_channel_cmd_send(FuNordicHidCfgChannel *self, const gchar *module_name, const gchar *option_name, guint8 status, guint8 *data, guint8 data_len, GError **error) { guint8 event_id = 0; if (!fu_nordic_hid_cfg_channel_get_event_id(self, module_name, option_name, &event_id)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "requested non-existing module %s with option %s", module_name, option_name); return FALSE; } if (!fu_nordic_hid_cfg_channel_cmd_send_by_id(self, event_id, status, data, data_len, error)) { g_prefix_error(error, "failed to send: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_nordic_hid_cfg_channel_cmd_receive(FuNordicHidCfgChannel *self, guint8 status, FuNordicCfgChannelMsg *res, GError **error) { FuNordicCfgChannelRcvHelper helper; res->report_id = HID_REPORT_ID; helper.status = status; helper.buf = (guint8 *)res; helper.bufsz = sizeof(*res); if (!fu_device_retry(FU_DEVICE(self), fu_nordic_hid_cfg_channel_receive_cb, FU_NORDIC_HID_CFG_CHANNEL_RETRIES, &helper, error)) { g_prefix_error(error, "Failed on receive: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_nordic_hid_cfg_channel_is_cached_peer_connected(guint8 peer_cache_val) { return (peer_cache_val % 2) != 0; } static void fu_nordic_hid_cfg_channel_check_children_update_pending_cb(FuDevice *device, GParamSpec *pspec, gpointer user_data) { FuNordicHidCfgChannel *self = FU_NORDIC_HID_CFG_CHANNEL(user_data); GPtrArray *children = fu_device_get_children(FU_DEVICE(self)); gboolean update_pending = FALSE; for (guint i = 0; i < children->len; i++) { FuDevice *peer = g_ptr_array_index(children, i); if (fu_device_has_private_flag(peer, FU_DEVICE_PRIVATE_FLAG_UPDATE_PENDING)) { update_pending = TRUE; break; } } if (update_pending) { fu_device_add_problem(FU_DEVICE(self), FWUPD_DEVICE_PROBLEM_UPDATE_PENDING); } else { fu_device_remove_problem(FU_DEVICE(self), FWUPD_DEVICE_PROBLEM_UPDATE_PENDING); } } static void fu_nordic_hid_cfg_channel_add_peer(FuNordicHidCfgChannel *self, guint8 peer_id) { g_autoptr(GError) error_local = NULL; g_autoptr(FuNordicHidCfgChannel) peer = NULL; peer = fu_nordic_hid_cfg_channel_new(peer_id, self); /* ensure that the general quirk content for Nordic HID devices is applied */ fu_device_add_instance_id_full(FU_DEVICE(peer), "HIDRAW\\VEN_1915", FU_DEVICE_INSTANCE_FLAG_QUIRKS); if (!fu_device_setup(FU_DEVICE(peer), &error_local)) { g_debug("failed to discover peer 0x%02x: %s", peer_id, error_local->message); return; } g_debug("peer 0x%02x discovered", peer_id); /* if any of the peripherals have a pending update, inhibit the dongle */ g_signal_connect(FU_DEVICE(peer), "notify::private-flags", G_CALLBACK(fu_nordic_hid_cfg_channel_check_children_update_pending_cb), self); fu_device_add_child(FU_DEVICE(self), FU_DEVICE(peer)); /* prohibit to close parent's communication descriptor */ fu_device_add_private_flag(FU_DEVICE(peer), FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_OPEN); } static void fu_nordic_hid_cfg_channel_remove_peer(FuNordicHidCfgChannel *self, guint8 peer_id) { GPtrArray *children = fu_device_get_children(FU_DEVICE(self)); /* remove child device if already discovered */ for (guint i = 0; i < children->len; i++) { FuDevice *child_dev = g_ptr_array_index(children, i); FuNordicHidCfgChannel *child = FU_NORDIC_HID_CFG_CHANNEL(child_dev); if (child->peer_id == peer_id) { fu_device_remove_child(FU_DEVICE(self), child_dev); break; } } } static void fu_nordic_hid_cfg_channel_remove_disconnected_peers(FuNordicHidCfgChannel *self, guint8 peers_cache[PEERS_CACHE_LEN]) { for (guint i = 0; i < PEERS_CACHE_LEN; i++) { guint8 peer_id = i + 1; if (peers_cache == NULL || !fu_nordic_hid_cfg_channel_is_cached_peer_connected(peers_cache[i])) { fu_nordic_hid_cfg_channel_remove_peer(self, peer_id); if (peers_cache != NULL) self->peers_cache[i] = peers_cache[i]; } } } static gboolean fu_nordic_hid_cfg_channel_index_peers_cmd(FuNordicHidCfgChannel *self, gboolean *cmd_supported, GError **error) { g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); g_autoptr(GError) error_local = NULL; *cmd_supported = FALSE; if (!fu_nordic_hid_cfg_channel_cmd_send(self, NULL, NULL, CONFIG_STATUS_INDEX_PEERS, NULL, 0, error)) { g_prefix_error(error, "INDEX_PEERS cmd_send failed: "); return FALSE; } if (fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_DISCONNECTED, res, &error_local)) { /* forwarding configuration channel to peers not supported */ return TRUE; } /* Peers available */ if (!fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, res, error)) { g_prefix_error(error, "INDEX_PEERS cmd_receive failed: "); return FALSE; } *cmd_supported = TRUE; return TRUE; } static gboolean fu_nordic_hid_cfg_channel_get_next_peer_id_cmd(FuNordicHidCfgChannel *self, guint8 *peer_id, GError **error) { g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); if (!fu_nordic_hid_cfg_channel_cmd_send(self, NULL, NULL, CONFIG_STATUS_GET_PEER, NULL, 0, error)) { g_prefix_error(error, "GET_PEER cmd_send failed: "); return FALSE; } if (!fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, res, error)) { g_prefix_error(error, "GET_PEER cmd_receive failed: "); return FALSE; } *peer_id = res->data[8]; return TRUE; } static gboolean fu_nordic_hid_cfg_channel_read_peers_cache_cmd(FuNordicHidCfgChannel *self, gboolean *cmd_supported, guint8 peers_cache[PEERS_CACHE_LEN], GError **error) { g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); g_autoptr(GError) error_local = NULL; *cmd_supported = FALSE; if (!fu_nordic_hid_cfg_channel_cmd_send(self, NULL, NULL, CONFIG_STATUS_GET_PEERS_CACHE, NULL, 0, error)) { g_prefix_error(error, "GET_PEERS_CACHE cmd_send failed: "); return FALSE; } if (fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_DISCONNECTED, res, &error_local)) { /* configuration channel peers cache not supported */ return TRUE; } /* configuration channel peer caching available */ if (!fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, res, error)) { g_prefix_error(error, "GET_PEERS_CACHE cmd_receive failed: "); return FALSE; } if (!fu_memcpy_safe(peers_cache, PEERS_CACHE_LEN, 0, res->data, PEERS_CACHE_LEN, 0, PEERS_CACHE_LEN, error)) return FALSE; *cmd_supported = TRUE; return TRUE; } static gboolean fu_nordic_hid_cfg_channel_update_peers(FuNordicHidCfgChannel *self, guint8 peers_cache[PEERS_CACHE_LEN], GError **error) { gboolean peers_supported = FALSE; guint8 peer_id; guint cnt = 0; if (!fu_nordic_hid_cfg_channel_index_peers_cmd(self, &peers_supported, error)) return FALSE; if (!peers_supported) return TRUE; /* a device that does not support peers caching, would drop all of the peers because it * cannot determine if the previously discovered peer is still connected */ fu_nordic_hid_cfg_channel_remove_disconnected_peers(self, peers_cache); while (cnt++ <= 0xFF) { if (!fu_nordic_hid_cfg_channel_get_next_peer_id_cmd(self, &peer_id, error)) return FALSE; /* end of the list */ if (peer_id == INVALID_PEER_ID) break; g_debug("detected peer: 0x%02x", peer_id); if (peers_cache == NULL) { /* allow to properly discover dongles without peers cache support */ fu_nordic_hid_cfg_channel_add_peer(self, peer_id); } else { guint8 idx = peer_id - 1; if (self->peers_cache[idx] != peers_cache[idx] && fu_nordic_hid_cfg_channel_is_cached_peer_connected(peers_cache[idx])) { fu_nordic_hid_cfg_channel_remove_peer(self, peer_id); fu_nordic_hid_cfg_channel_add_peer(self, peer_id); self->peers_cache[idx] = peers_cache[idx]; } } } if (peer_id != INVALID_PEER_ID) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "too many peers detected"); return FALSE; } return TRUE; } static gboolean fu_nordic_hid_cfg_channel_setup_peers(FuNordicHidCfgChannel *self, GError **error) { gboolean peers_supported = FALSE; gboolean peers_cache_supported = FALSE; guint8 peers_cache[PEERS_CACHE_LEN] = {0x00}; if (self->peer_id != SELF_PEER_ID) { /* device connected through dongle cannot support peers */ return TRUE; } /* Send index peers command to a device before accessing peers cache. This is done to * prevent assertion failure on peripheral with legacy firmware that enables debug logs. */ if (!fu_nordic_hid_cfg_channel_index_peers_cmd(self, &peers_supported, error)) return FALSE; if (!peers_supported) return TRUE; if (!fu_nordic_hid_cfg_channel_read_peers_cache_cmd(self, &peers_cache_supported, peers_cache, error)) return FALSE; if (!peers_cache_supported) { if (!fu_nordic_hid_cfg_channel_update_peers(self, NULL, error)) return FALSE; } else { if (!fu_nordic_hid_cfg_channel_update_peers(self, peers_cache, error)) return FALSE; /* device must be kept open to allow polling */ if (!fu_device_open(FU_DEVICE(self), error)) return FALSE; /* mark device as supporting peers cache, ensure periodic polling for peers */ self->peers_cache_support = TRUE; fu_device_set_poll_interval(FU_DEVICE(self), FU_NORDIC_HID_CFG_CHANNEL_PEERS_POLL_INTERVAL); } return TRUE; } static gboolean fu_nordic_hid_cfg_channel_poll_peers(FuDevice *device, GError **error) { FuNordicHidCfgChannel *self = FU_NORDIC_HID_CFG_CHANNEL(device); gboolean peers_cache_supported = FALSE; guint8 peers_cache[PEERS_CACHE_LEN] = {0x00}; if (!fu_nordic_hid_cfg_channel_read_peers_cache_cmd(self, &peers_cache_supported, peers_cache, error)) return FALSE; if (!self->peers_cache_support || !peers_cache_supported) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unexpected poll of device without peers caching support"); return FALSE; } /* skip update if not needed */ if (!memcmp(self->peers_cache, peers_cache, PEERS_CACHE_LEN)) return TRUE; if (!fu_nordic_hid_cfg_channel_update_peers(self, peers_cache, error)) return FALSE; return TRUE; } static gboolean fu_nordic_hid_cfg_channel_get_board_name_cb(FuDevice *device, gpointer user_data, GError **error) { FuNordicHidCfgChannel *self = FU_NORDIC_HID_CFG_CHANNEL(device); g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); if (!fu_nordic_hid_cfg_channel_cmd_send(self, NULL, NULL, CONFIG_STATUS_GET_BOARD_NAME, NULL, 0, error)) return FALSE; if (!fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, res, error)) return FALSE; self->board_name = fu_memstrsafe(res->data, res->data_len, 0x0, res->data_len, error); if (self->board_name == NULL) return FALSE; /* success */ return TRUE; } static gboolean fu_nordic_hid_cfg_channel_get_bl_name(FuNordicHidCfgChannel *self, GError **error) { guint8 event_id = 0; g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); /* query for the bootloader name if the board support it */ if (fu_nordic_hid_cfg_channel_get_event_id(self, "dfu", "module_variant", &event_id)) { if (!fu_nordic_hid_cfg_channel_cmd_send(self, "dfu", "module_variant", CONFIG_STATUS_FETCH, NULL, 0, error)) return FALSE; if (!fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, res, error)) return FALSE; /* check if not set via quirk */ if (self->bl_name != NULL) { g_autofree gchar *tmp = g_strndup((const gchar *)res->data, res->data_len); g_debug("Bootloader readout '%s' overrides bootloader from quirk '%s'", tmp, self->bl_name); g_free(self->bl_name); } self->bl_name = fu_memstrsafe(res->data, res->data_len, 0x0, res->data_len, error); if (self->bl_name == NULL) return FALSE; } else { g_debug("the board has no support of bootloader runtime detection"); } /* always use the bank 0 for MCUBOOT bootloader that swaps images */ if (g_strcmp0(self->bl_name, "MCUBOOT") == 0) self->flash_area_id = 0; if (self->bl_name == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "the bootloader is not detected nor set via quirk"); return FALSE; } /* success */ return TRUE; } static gboolean fu_nordic_hid_cfg_channel_get_devinfo(FuNordicHidCfgChannel *self, GError **error) { guint8 event_id = 0; g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); /* query for the devinfo if the board supports it */ if (fu_nordic_hid_cfg_channel_get_event_id(self, "dfu", "devinfo", &event_id)) { gchar *generation; if (!fu_nordic_hid_cfg_channel_cmd_send(self, "dfu", "devinfo", CONFIG_STATUS_FETCH, NULL, 0, error)) return FALSE; if (!fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, res, error)) return FALSE; if (!fu_memread_uint16_safe(res->data, REPORT_SIZE, 0x00, &self->vid, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memread_uint16_safe(res->data, REPORT_SIZE, 0x02, &self->pid, G_LITTLE_ENDIAN, error)) return FALSE; generation = fu_memstrsafe(res->data, res->data_len, 0x4, res->data_len - 0x04, error); if (generation == NULL) return FALSE; /* check if not set via quirk */ if (self->generation != NULL) { g_debug("generation readout '%s' overrides generation from quirk '%s'", generation, self->generation); g_free(self->generation); } self->generation = generation; } else { g_debug("the board has no support of devinfo runtime detection"); } if (self->generation == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "the generation is not detected nor set via quirk"); return FALSE; } /* success */ return TRUE; } /* * NOTE: * For devices connected directly to the host, * hw_id = HID_UNIQ = logical_id. */ static gboolean fu_nordic_hid_cfg_channel_get_hwid(FuNordicHidCfgChannel *self, GError **error) { guint8 hw_id[HWID_LEN] = {0x0}; g_autofree gchar *physical_id = NULL; g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); if (!fu_nordic_hid_cfg_channel_cmd_send(self, NULL, NULL, CONFIG_STATUS_GET_HWID, NULL, 0, error)) return FALSE; if (!fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, res, error)) return FALSE; if (!fu_memcpy_safe(hw_id, HWID_LEN, 0, res->data, REPORT_DATA_MAX_LEN, 0, HWID_LEN, error)) return FALSE; /* allows to detect the single device connected via several interfaces */ physical_id = g_strdup_printf("%s-%02x%02x%02x%02x%02x%02x%02x%02x", self->board_name, hw_id[0], hw_id[1], hw_id[2], hw_id[3], hw_id[4], hw_id[5], hw_id[6], hw_id[7]); fu_device_set_physical_id(FU_DEVICE(self), physical_id); /* avoid inheriting name from the dongle */ if (self->peer_id != SELF_PEER_ID) fu_device_set_name(FU_DEVICE(self), physical_id); /* success */ return TRUE; } static gboolean fu_nordic_hid_cfg_channel_load_module_opts(FuNordicHidCfgChannel *self, FuNordicCfgChannelModule *mod, GError **error) { for (guint8 i = 0; i < 0xFF; i++) { g_autoptr(FuNordicCfgChannelModuleOption) opt = NULL; g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); if (!fu_nordic_hid_cfg_channel_cmd_send_by_id(self, mod->idx << 4, CONFIG_STATUS_FETCH, NULL, 0, error)) return FALSE; if (!fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, res, error)) return FALSE; /* res->data: option name */ if (res->data[0] == END_OF_TRANSFER_CHAR) break; opt = g_new0(FuNordicCfgChannelModuleOption, 1); opt->name = fu_memstrsafe(res->data, res->data_len, 0x0, res->data_len, error); if (opt->name == NULL) return FALSE; opt->idx = i; g_ptr_array_add(mod->options, g_steal_pointer(&opt)); } /* success */ return TRUE; } static gboolean fu_nordic_hid_cfg_channel_load_module_info(FuNordicHidCfgChannel *self, guint8 module_idx, GError **error) { g_autoptr(FuNordicCfgChannelModule) mod = g_new0(FuNordicCfgChannelModule, 1); mod->idx = module_idx; mod->options = g_ptr_array_new_with_free_func( (GDestroyNotify)fu_nordic_hid_cfg_channel_module_option_free); if (!fu_nordic_hid_cfg_channel_load_module_opts(self, mod, error)) return FALSE; /* module description is the 1st loaded option */ if (mod->options->len > 0) { FuNordicCfgChannelModuleOption *opt = g_ptr_array_index(mod->options, 0); mod->name = g_strdup(opt->name); if (!g_ptr_array_remove_index(mod->options, 0)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "cannot remove option"); return FALSE; } } /* success */ g_ptr_array_add(self->modules, g_steal_pointer(&mod)); return TRUE; } static gboolean fu_nordic_hid_cfg_channel_get_modinfo(FuNordicHidCfgChannel *self, GError **error) { g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); if (!fu_nordic_hid_cfg_channel_cmd_send(self, NULL, NULL, CONFIG_STATUS_GET_MAX_MOD_ID, NULL, 0, error)) return FALSE; if (!fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, res, error)) return FALSE; /* res->data[0]: maximum module idx */ for (guint i = 0; i <= res->data[0]; i++) { if (!fu_nordic_hid_cfg_channel_load_module_info(self, i, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_nordic_hid_cfg_channel_dfu_fwinfo(FuNordicHidCfgChannel *self, GError **error) { guint16 ver_rev; guint32 ver_build_nr; g_autofree gchar *version = NULL; g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); if (!fu_nordic_hid_cfg_channel_cmd_send(self, "dfu", "fwinfo", CONFIG_STATUS_FETCH, NULL, 0, error)) return FALSE; if (!fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, res, error)) return FALSE; /* parsing fwinfo answer */ /* TODO: add banks amount into quirk */ if (res->data[0] > 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid flash area returned by device"); return FALSE; } /* set the target flash ID area */ self->flash_area_id = res->data[0] ^ 1; if (!fu_memread_uint32_safe(res->data, REPORT_SIZE, 0x01, &self->flashed_image_len, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memread_uint16_safe(res->data, REPORT_SIZE, 0x07, &ver_rev, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memread_uint32_safe(res->data, REPORT_SIZE, 0x09, &ver_build_nr, G_LITTLE_ENDIAN, error)) return FALSE; version = g_strdup_printf("%u.%u.%u.%u", res->data[4], res->data[5], ver_rev, ver_build_nr); fu_device_set_version(FU_DEVICE(self), version); /* success */ return TRUE; } static gboolean fu_nordic_hid_cfg_channel_dfu_reboot(FuNordicHidCfgChannel *self, GError **error) { g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); if (!fu_nordic_hid_cfg_channel_cmd_send(self, "dfu", "reboot", CONFIG_STATUS_FETCH, NULL, 0, error)) return FALSE; if (!fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, res, error)) return FALSE; if (res->data_len != 1 || res->data[0] != 0x01) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "reboot data was invalid"); return FALSE; } /* success */ return TRUE; } static gboolean fu_nordic_hid_cfg_channel_dfu_sync_cb(FuDevice *device, gpointer user_data, GError **error) { FuNordicHidCfgChannel *self = FU_NORDIC_HID_CFG_CHANNEL(device); FuNordicCfgChannelRcvHelper *args = (FuNordicCfgChannelRcvHelper *)user_data; g_autoptr(FuNordicCfgChannelMsg) recv_msg = g_new0(FuNordicCfgChannelMsg, 1); /* allow to sync buffer more precisely and without annoying messages * it may take some time and depending on device workload */ for (gint i = 1; i < 30; i++) { if (!fu_nordic_hid_cfg_channel_cmd_send(self, "dfu", "sync", CONFIG_STATUS_FETCH, NULL, 0, error)) return FALSE; recv_msg->report_id = HID_REPORT_ID; fu_device_sleep(device, 2); /* ms */ if (!fu_nordic_hid_cfg_channel_receive(self, (guint8 *)recv_msg, sizeof(*recv_msg), error)) { return FALSE; } if (recv_msg->data_len != 0x0F) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "incorrect length of reply"); return FALSE; } if (recv_msg->data[0] == DFU_STATE_INACTIVE || recv_msg->data[0] == DFU_STATE_ACTIVE) { break; } } if (recv_msg->data[0] != args->status) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "sync received status: 0x%02x, expected: 0x%02x", recv_msg->data[0], args->status); return FALSE; } return fu_memcpy_safe(args->buf, args->bufsz, 0, (guint8 *)recv_msg, sizeof(*recv_msg), 0, sizeof(*recv_msg), error); } static gboolean fu_nordic_hid_cfg_channel_dfu_sync(FuNordicHidCfgChannel *self, FuNordicCfgChannelDfuInfo *dfu_info, guint8 expecting_state, GError **error) { FuNordicCfgChannelRcvHelper helper; g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); helper.status = expecting_state; helper.buf = (guint8 *)res; helper.bufsz = sizeof(*res); if (!fu_device_retry_full(FU_DEVICE(self), fu_nordic_hid_cfg_channel_dfu_sync_cb, FU_NORDIC_HID_CFG_CHANNEL_RETRIES, FU_NORDIC_HID_CFG_CHANNEL_DFU_RETRY_DELAY, &helper, error)) { g_prefix_error(error, "failed on dfu sync: "); return FALSE; } dfu_info->dfu_state = res->data[0]; if (!fu_memread_uint32_safe(res->data, REPORT_SIZE, 0x01, &dfu_info->img_length, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memread_uint32_safe(res->data, REPORT_SIZE, 0x05, &dfu_info->img_csum, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memread_uint32_safe(res->data, REPORT_SIZE, 0x09, &dfu_info->offset, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memread_uint16_safe(res->data, REPORT_SIZE, 0x0D, &dfu_info->sync_buffer_size, G_LITTLE_ENDIAN, error)) return FALSE; return TRUE; } static gboolean fu_nordic_hid_cfg_channel_dfu_start(FuNordicHidCfgChannel *self, gsize img_length, guint32 img_crc, guint32 offset, GError **error) { guint8 data[REPORT_DATA_MAX_LEN] = {0}; g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); /* sanity check */ if (img_length > G_MAXUINT32) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "payload was too large"); return FALSE; } if (!fu_memwrite_uint32_safe(data, REPORT_DATA_MAX_LEN, 0x00, (guint32)img_length, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memwrite_uint32_safe(data, REPORT_DATA_MAX_LEN, 0x04, img_crc, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memwrite_uint32_safe(data, REPORT_DATA_MAX_LEN, 0x08, offset, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_nordic_hid_cfg_channel_cmd_send(self, "dfu", "start", CONFIG_STATUS_SET, data, 0x0C, error)) return FALSE; return fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, res, error); } static gboolean fu_nordic_hid_cfg_channel_generate_ids(FuNordicHidCfgChannel *self, GError **error) { FuDevice *device = FU_DEVICE(self); /* generate IDs */ fu_device_add_instance_strsafe(device, "BOARD", self->board_name); fu_device_add_instance_strsafe(device, "BL", self->bl_name); fu_device_add_instance_strsafe(device, "GEN", self->generation); /* If available, use VID and PID fetched in devinfo. Otherwise, use hardcoded VID and PID of * 0x00 only for devices connected via dongle. This prevents from inheriting VID and PID of * the dongle. */ if ((self->vid != 0x00 && self->pid != 0x00) || (self->peer_id != SELF_PEER_ID)) { fu_device_add_instance_u16(device, "VEN", self->vid); fu_device_add_instance_u16(device, "DEV", self->pid); } /* For the default generation, generate GUID without the generation parameter. * Required for compatibility with already released application images. */ if (g_strcmp0(self->generation, "default") == 0) { if (!fu_device_build_instance_id(device, error, "HIDRAW", "VEN", "DEV", "BOARD", "BL", NULL)) { g_prefix_error(error, "failed to add ID without generation: "); return FALSE; } } if (!fu_device_build_instance_id(device, error, "HIDRAW", "VEN", "DEV", "BOARD", "BL", "GEN", NULL)) { g_prefix_error(error, "failed to add complete ID: "); return FALSE; } return TRUE; } static gboolean fu_nordic_hid_cfg_channel_direct_discovery(FuNordicHidCfgChannel *self, GError **error) { g_autoptr(GError) error_board_name = NULL; g_autoptr(GError) error_fwinfo = NULL; FuDevice *device = FU_DEVICE(self); /* Get the board name. The first configuration channel operation is used to check if * hidraw instance supports the protocol. In case of failure, the hidraw instance is ignored * and predefined error code is returned to suppress warning log. This is needed to properly * handle hidraw instances that do not handle configuration channel requests. A device may * not support configuration channel at all (no configuration channel HID feature report). * The configuration channel requests are handled only by the first HID instance on device * (other instances reject the configuration channel operations). * * If the HID device is connected over BLE, the configuration channel operations right after * reconnection may fail with an ioctl error. Retry after a delay to ensure that the device * will be properly recognized by the fwupd tool. */ if (!fu_device_retry_full(device, fu_nordic_hid_cfg_channel_get_board_name_cb, 3, 50, NULL, &error_board_name)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Get board name failed: %s", error_board_name->message); return FALSE; } /* set the physical id based on board name and HW id to detect if the device is connected * via several interfaces */ if (!fu_nordic_hid_cfg_channel_get_hwid(self, error)) return FALSE; /* detect available modules first */ if (!fu_nordic_hid_cfg_channel_get_modinfo(self, error)) return FALSE; /* generate the custom visible name for the device if absent */ if (fu_device_get_name(device) == NULL) { const gchar *physical_id = NULL; physical_id = fu_device_get_physical_id(device); fu_device_set_name(device, physical_id); } /* get device info and version */ if (!fu_nordic_hid_cfg_channel_dfu_fwinfo(self, &error_fwinfo)) /* lack of firmware info support indicates that device does not support DFU. */ return TRUE; /* detect bootloader type */ if (!fu_nordic_hid_cfg_channel_get_bl_name(self, error)) return FALSE; /* detect vendor ID, product ID and generation */ if (!fu_nordic_hid_cfg_channel_get_devinfo(self, error)) return FALSE; /* generate device IDs. */ if (!fu_nordic_hid_cfg_channel_generate_ids(self, error)) return FALSE; self->dfu_support = TRUE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); return TRUE; } static gboolean fu_nordic_hid_cfg_channel_setup(FuDevice *device, GError **error) { FuNordicHidCfgChannel *self = FU_NORDIC_HID_CFG_CHANNEL(device); if (!fu_nordic_hid_cfg_channel_direct_discovery(self, error)) return FALSE; if (!fu_nordic_hid_cfg_channel_setup_peers(self, error)) return FALSE; return TRUE; } static void fu_nordic_hid_cfg_channel_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 97, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reload"); } static void fu_nordic_hid_cfg_channel_module_to_string(FuNordicCfgChannelModule *mod, guint idt, GString *str) { for (guint i = 0; i < mod->options->len; i++) { FuNordicCfgChannelModuleOption *opt = g_ptr_array_index(mod->options, i); g_autofree gchar *title = g_strdup_printf("Option%02x", i); fwupd_codec_string_append(str, idt, title, opt->name); } } static void fu_nordic_hid_cfg_channel_to_string(FuDevice *device, guint idt, GString *str) { FuNordicHidCfgChannel *self = FU_NORDIC_HID_CFG_CHANNEL(device); fwupd_codec_string_append(str, idt, "BoardName", self->board_name); fwupd_codec_string_append_hex(str, idt, "PeerId", self->peer_id); fwupd_codec_string_append_hex(str, idt, "VendorId", self->vid); fwupd_codec_string_append_hex(str, idt, "ProductId", self->pid); if (self->dfu_support) { fwupd_codec_string_append(str, idt, "Bootloader", self->bl_name); fwupd_codec_string_append(str, idt, "Generation", self->generation); fwupd_codec_string_append_hex(str, idt, "FlashAreaId", self->flash_area_id); fwupd_codec_string_append_hex(str, idt, "FlashedImageLen", self->flashed_image_len); } for (guint i = 0; i < self->modules->len; i++) { FuNordicCfgChannelModule *mod = g_ptr_array_index(self->modules, i); g_autofree gchar *title = g_strdup_printf("Module%02x", i); fwupd_codec_string_append(str, idt, title, mod->name); fu_nordic_hid_cfg_channel_module_to_string(mod, idt + 1, str); } } static gboolean fu_nordic_hid_cfg_channel_write_firmware_chunk(FuNordicHidCfgChannel *self, FuChunk *chk, gboolean is_last, GError **error) { guint32 chunk_len; guint32 offset = 0; guint8 sync_state = DFU_STATE_ACTIVE; g_autoptr(FuNordicCfgChannelDfuInfo) dfu_info = g_new0(FuNordicCfgChannelDfuInfo, 1); chunk_len = fu_chunk_get_data_sz(chk); while (offset < chunk_len) { guint8 data_len; guint8 data[REPORT_DATA_MAX_LEN] = {0}; g_autoptr(FuNordicCfgChannelMsg) res = g_new0(FuNordicCfgChannelMsg, 1); data_len = ((offset + REPORT_DATA_MAX_LEN) < chunk_len) ? REPORT_DATA_MAX_LEN : (guint8)(chunk_len - offset); if (!fu_memcpy_safe(data, REPORT_DATA_MAX_LEN, 0, fu_chunk_get_data(chk), chunk_len, offset, data_len, error)) { return FALSE; } if (!fu_nordic_hid_cfg_channel_cmd_send(self, "dfu", "data", CONFIG_STATUS_SET, data, data_len, error)) return FALSE; if (!fu_nordic_hid_cfg_channel_cmd_receive(self, CONFIG_STATUS_SUCCESS, res, error)) return FALSE; offset += data_len; } /* sync should return inactive for the last chunk */ if (is_last) sync_state = DFU_STATE_INACTIVE; return fu_nordic_hid_cfg_channel_dfu_sync(self, dfu_info, sync_state, error); } static gboolean fu_nordic_hid_cfg_channel_write_firmware_blob(FuNordicHidCfgChannel *self, GInputStream *stream, FuProgress *progress, GError **error) { g_autoptr(FuNordicCfgChannelDfuInfo) dfu_info = g_new0(FuNordicCfgChannelDfuInfo, 1); g_autoptr(FuChunkArray) chunks = NULL; if (!fu_nordic_hid_cfg_channel_dfu_sync(self, dfu_info, DFU_STATE_ACTIVE, error)) return FALSE; chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, dfu_info->sync_buffer_size, error); if (chunks == NULL) return FALSE; fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { gboolean is_last; g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; is_last = i == fu_chunk_array_length(chunks) - 1; if (!fu_nordic_hid_cfg_channel_write_firmware_chunk(self, chk, is_last, error)) { g_prefix_error(error, "chunk %u: ", fu_chunk_get_idx(chk)); return FALSE; } fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_nordic_hid_cfg_channel_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuNordicHidCfgChannel *self = FU_NORDIC_HID_CFG_CHANNEL(device); gsize streamsz = 0; guint32 checksum; guint64 val = 0; g_autofree gchar *csum_str = NULL; g_autofree gchar *image_id = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(FuNordicCfgChannelDfuInfo) dfu_info = g_new0(FuNordicCfgChannelDfuInfo, 1); /* select correct firmware per target board, bootloader and bank */ image_id = g_strdup_printf("%s_%s_bank%01u", self->board_name, self->bl_name, self->flash_area_id); firmware = fu_firmware_get_image_by_id(firmware, image_id, error); if (firmware == NULL) return FALSE; /* explicitly request a custom checksum calculation */ csum_str = fu_firmware_get_checksum(firmware, -1, error); if (csum_str == NULL) return FALSE; /* expecting checksum string in hex */ if (!fu_strtoull(csum_str, &val, 0, G_MAXUINT32, FU_INTEGER_BASE_16, error)) return FALSE; checksum = (guint32)val; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 98, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, NULL); /* TODO: check if there is unfinished operation before? */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; if (!fu_nordic_hid_cfg_channel_dfu_sync(self, dfu_info, DFU_STATE_INACTIVE, error)) return FALSE; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (!fu_nordic_hid_cfg_channel_dfu_start(self, streamsz, checksum, 0x0 /* offset */, error)) return FALSE; fu_progress_step_done(progress); /* write */ if (!fu_nordic_hid_cfg_channel_write_firmware_blob(self, stream, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* attach */ if (!fu_nordic_hid_cfg_channel_dfu_reboot(self, error)) return FALSE; fu_progress_step_done(progress); return TRUE; } static gboolean fu_nordic_hid_cfg_channel_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuNordicHidCfgChannel *self = FU_NORDIC_HID_CFG_CHANNEL(device); if (g_strcmp0(key, "NordicHidBootloader") == 0) { if (g_strcmp0(value, "B0") != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "can be only 'B0' in quirk"); return FALSE; } self->bl_name = g_strdup(value); return TRUE; } if (g_strcmp0(key, "NordicHidGeneration") == 0) { if (g_strcmp0(value, "default") != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "can be only 'default' in quirk"); return FALSE; } self->generation = g_strdup(value); return TRUE; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_nordic_hid_cfg_channel_finalize(GObject *object) { FuNordicHidCfgChannel *self = FU_NORDIC_HID_CFG_CHANNEL(object); g_free(self->board_name); g_free(self->bl_name); g_free(self->generation); g_ptr_array_unref(self->modules); G_OBJECT_CLASS(fu_nordic_hid_cfg_channel_parent_class)->finalize(object); } static void fu_nordic_hid_cfg_channel_class_init(FuNordicHidCfgChannelClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); device_class->set_progress = fu_nordic_hid_cfg_channel_set_progress; device_class->set_quirk_kv = fu_nordic_hid_cfg_channel_set_quirk_kv; device_class->setup = fu_nordic_hid_cfg_channel_setup; device_class->poll = fu_nordic_hid_cfg_channel_poll_peers; device_class->to_string = fu_nordic_hid_cfg_channel_to_string; device_class->write_firmware = fu_nordic_hid_cfg_channel_write_firmware; object_class->finalize = fu_nordic_hid_cfg_channel_finalize; } static void fu_nordic_hid_cfg_channel_init(FuNordicHidCfgChannel *self) { self->modules = g_ptr_array_new_with_free_func((GDestroyNotify)fu_nordic_hid_cfg_channel_module_free); fu_device_set_vendor(FU_DEVICE(self), "Nordic"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); fu_device_add_protocol(FU_DEVICE(self), "com.nordic.hidcfgchannel"); fu_device_retry_set_delay(FU_DEVICE(self), FU_NORDIC_HID_CFG_CHANNEL_RETRY_DELAY); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_NORDIC_HID_ARCHIVE); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); } static FuNordicHidCfgChannel * fu_nordic_hid_cfg_channel_new(guint8 id, FuNordicHidCfgChannel *parent) { FuNordicHidCfgChannel *self = g_object_new(FU_TYPE_NORDIC_HID_CFG_CHANNEL, "context", fu_device_get_context(FU_DEVICE(parent)), NULL); fu_device_incorporate(FU_DEVICE(self), FU_DEVICE(parent), FU_DEVICE_INCORPORATE_FLAG_BACKEND_ID); self->peer_id = id; self->parent_udev = FU_UDEV_DEVICE(parent); return self; } fwupd-2.0.10/plugins/nordic-hid/fu-nordic-hid-cfg-channel.h000066400000000000000000000006011501337203100233540ustar00rootroot00000000000000/* * Copyright 2021 Ricardo Cañuelo * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_NORDIC_HID_CFG_CHANNEL (fu_nordic_hid_cfg_channel_get_type()) G_DECLARE_FINAL_TYPE(FuNordicHidCfgChannel, fu_nordic_hid_cfg_channel, FU, NORDIC_HID_CFG_CHANNEL, FuHidrawDevice) fwupd-2.0.10/plugins/nordic-hid/fu-nordic-hid-firmware-b0.c000066400000000000000000000072761501337203100233340ustar00rootroot00000000000000/* * Copyright 2021 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-nordic-hid-firmware-b0.h" #define UPDATE_IMAGE_MAGIC_COMMON 0x281ee6de #define UPDATE_IMAGE_MAGIC_FWINFO 0x8fcebb4c #define UPDATE_IMAGE_MAGIC_NRF52 0x00003402 #define UPDATE_IMAGE_MAGIC_NRF53 0x00003502 struct _FuNordicHidFirmwareB0 { FuNordicHidFirmwareClass parent_instance; }; G_DEFINE_TYPE(FuNordicHidFirmwareB0, fu_nordic_hid_firmware_b0, FU_TYPE_NORDIC_HID_FIRMWARE) static GByteArray * fu_nordic_hid_firmware_b0_write(FuFirmware *firmware, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GBytes) blob = NULL; fu_byte_array_append_uint32(buf, UPDATE_IMAGE_MAGIC_COMMON, G_LITTLE_ENDIAN); fu_byte_array_append_uint32(buf, UPDATE_IMAGE_MAGIC_FWINFO, G_LITTLE_ENDIAN); fu_byte_array_append_uint32(buf, UPDATE_IMAGE_MAGIC_NRF52, G_LITTLE_ENDIAN); fu_byte_array_append_uint32(buf, 0x00, G_LITTLE_ENDIAN); fu_byte_array_append_uint32(buf, 0x00, G_LITTLE_ENDIAN); /* version */ fu_byte_array_append_uint32(buf, 0x63, G_LITTLE_ENDIAN); blob = fu_firmware_get_bytes_with_patches(firmware, error); if (blob == NULL) return NULL; fu_byte_array_append_bytes(buf, blob); return g_steal_pointer(&buf); } static gboolean fu_nordic_hid_firmware_b0_read_fwinfo(FuFirmware *firmware, GInputStream *stream, GError **error) { guint32 magic_common; guint32 magic_fwinfo; guint32 magic_compat; guint32 offset; guint32 hdr_offset[5] = {0x0000, 0x0200, 0x400, 0x800, 0x1000}; guint8 ver_major = 0; guint8 ver_minor = 0; guint16 ver_rev = 0; guint32 ver_build_nr = 0; g_autofree gchar *version = NULL; /* find correct offset to fwinfo */ for (guint32 i = 0; i < G_N_ELEMENTS(hdr_offset); i++) { offset = hdr_offset[i]; if (!fu_input_stream_read_u32(stream, offset, &magic_common, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_input_stream_read_u32(stream, offset + 0x04, &magic_fwinfo, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_input_stream_read_u32(stream, offset + 0x08, &magic_compat, G_LITTLE_ENDIAN, error)) return FALSE; /* version */ if (!fu_input_stream_read_u32(stream, offset + 0x14, &ver_build_nr, G_LITTLE_ENDIAN, error)) return FALSE; if (magic_common != UPDATE_IMAGE_MAGIC_COMMON || magic_fwinfo != UPDATE_IMAGE_MAGIC_FWINFO) continue; switch (magic_compat) { case UPDATE_IMAGE_MAGIC_NRF52: case UPDATE_IMAGE_MAGIC_NRF53: /* currently only the build number is saved into the image */ version = g_strdup_printf("%u.%u.%u.%u", ver_major, ver_minor, ver_rev, ver_build_nr); fu_firmware_set_version(firmware, version); return TRUE; default: break; } } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "unable to validate the update binary"); return FALSE; } static gboolean fu_nordic_hid_firmware_b0_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { if (!FU_FIRMWARE_CLASS(fu_nordic_hid_firmware_b0_parent_class) ->parse(firmware, stream, flags, error)) return FALSE; return fu_nordic_hid_firmware_b0_read_fwinfo(firmware, stream, error); } static void fu_nordic_hid_firmware_b0_init(FuNordicHidFirmwareB0 *self) { } static void fu_nordic_hid_firmware_b0_class_init(FuNordicHidFirmwareB0Class *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_nordic_hid_firmware_b0_parse; firmware_class->write = fu_nordic_hid_firmware_b0_write; } fwupd-2.0.10/plugins/nordic-hid/fu-nordic-hid-firmware-b0.h000066400000000000000000000006121501337203100233240ustar00rootroot00000000000000/* * Copyright 2021 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-nordic-hid-firmware.h" #define FU_TYPE_NORDIC_HID_FIRMWARE_B0 (fu_nordic_hid_firmware_b0_get_type()) G_DECLARE_FINAL_TYPE(FuNordicHidFirmwareB0, fu_nordic_hid_firmware_b0, FU, NORDIC_HID_FIRMWARE_B0, FuNordicHidFirmware) fwupd-2.0.10/plugins/nordic-hid/fu-nordic-hid-firmware-mcuboot.c000066400000000000000000000106521501337203100244730ustar00rootroot00000000000000/* * Copyright 2021 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-nordic-hid-firmware-mcuboot.h" #define IMAGE_MAGIC 0x96f3b83d #define IMAGE_TLV_INFO_MAGIC 0x6907 #define IMAGE_TLV_PROT_INFO_MAGIC 0x6908 struct _FuNordicHidFirmwareMcuboot { FuNordicHidFirmwareClass parent_instance; }; G_DEFINE_TYPE(FuNordicHidFirmwareMcuboot, fu_nordic_hid_firmware_mcuboot, FU_TYPE_NORDIC_HID_FIRMWARE) static GByteArray * fu_nordic_hid_firmware_mcuboot_write(FuFirmware *firmware, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GBytes) blob = fu_firmware_get_bytes_with_patches(firmware, error); if (blob == NULL) return NULL; /* https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/mcuboot/design.html#image-format */ fu_byte_array_append_uint32(buf, IMAGE_MAGIC, G_LITTLE_ENDIAN); /* load_addr */ fu_byte_array_append_uint32(buf, 0x00, G_LITTLE_ENDIAN); /* hdr_size */ fu_byte_array_append_uint16(buf, 0x20, G_LITTLE_ENDIAN); /* protect_tlv_size */ fu_byte_array_append_uint16(buf, 0x00, G_LITTLE_ENDIAN); /* img_size */ fu_byte_array_append_uint32(buf, (guint32)g_bytes_get_size(blob), G_LITTLE_ENDIAN); /* flags */ fu_byte_array_append_uint32(buf, 0x00, G_LITTLE_ENDIAN); /* version */ fu_byte_array_append_uint8(buf, 0x01); fu_byte_array_append_uint8(buf, 0x02); fu_byte_array_append_uint16(buf, 0x03, G_LITTLE_ENDIAN); fu_byte_array_append_uint32(buf, 0x63, G_LITTLE_ENDIAN); /* pad */ fu_byte_array_append_uint32(buf, 0xffffffff, G_LITTLE_ENDIAN); /* payload */ fu_byte_array_append_bytes(buf, blob); /* TLV magic and total */ fu_byte_array_append_uint16(buf, IMAGE_TLV_INFO_MAGIC, G_LITTLE_ENDIAN); fu_byte_array_append_uint16(buf, 0x00, G_LITTLE_ENDIAN); return g_steal_pointer(&buf); } /* simple validation of the image */ static gboolean fu_nordic_hid_firmware_mcuboot_validate(FuFirmware *firmware, GInputStream *stream, GError **error) { guint32 magic; guint16 hdr_size; guint32 img_size; guint8 ver_major; guint8 ver_minor; guint16 ver_rev; guint32 ver_build_nr; guint16 magic_tlv; g_autofree gchar *version = NULL; if (!fu_input_stream_read_u32(stream, 0, &magic, G_LITTLE_ENDIAN, error)) return FALSE; if (magic != IMAGE_MAGIC) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "incorrect image magic"); return FALSE; } /* ignore load_addr */ if (!fu_input_stream_read_u16(stream, 8, &hdr_size, G_LITTLE_ENDIAN, error)) return FALSE; /* ignore protect_tlv_size */ if (!fu_input_stream_read_u32(stream, 12, &img_size, G_LITTLE_ENDIAN, error)) return FALSE; /* ignore TLVs themselves * https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/mcuboot/design.html#protected-tlvs * check the magic values only */ if (!fu_input_stream_read_u16(stream, hdr_size + img_size, &magic_tlv, G_LITTLE_ENDIAN, error)) return FALSE; if (magic_tlv != IMAGE_TLV_INFO_MAGIC && magic_tlv != IMAGE_TLV_PROT_INFO_MAGIC) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "incorrect TLV info magic"); return FALSE; } /* version */ if (!fu_input_stream_read_u8(stream, 0x14, &ver_major, error)) return FALSE; if (!fu_input_stream_read_u8(stream, 0x15, &ver_minor, error)) return FALSE; if (!fu_input_stream_read_u16(stream, 0x16, &ver_rev, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_input_stream_read_u32(stream, 0x18, &ver_build_nr, G_LITTLE_ENDIAN, error)) return FALSE; version = g_strdup_printf("%u.%u.%u.%u", ver_major, ver_minor, ver_rev, ver_build_nr); fu_firmware_set_version(firmware, version); return TRUE; } static gboolean fu_nordic_hid_firmware_mcuboot_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { if (!FU_FIRMWARE_CLASS(fu_nordic_hid_firmware_mcuboot_parent_class) ->parse(firmware, stream, flags, error)) return FALSE; return fu_nordic_hid_firmware_mcuboot_validate(firmware, stream, error); } static void fu_nordic_hid_firmware_mcuboot_init(FuNordicHidFirmwareMcuboot *self) { } static void fu_nordic_hid_firmware_mcuboot_class_init(FuNordicHidFirmwareMcubootClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_nordic_hid_firmware_mcuboot_parse; firmware_class->write = fu_nordic_hid_firmware_mcuboot_write; } fwupd-2.0.10/plugins/nordic-hid/fu-nordic-hid-firmware-mcuboot.h000066400000000000000000000006431501337203100244770ustar00rootroot00000000000000/* * Copyright 2021 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-nordic-hid-firmware.h" #define FU_TYPE_NORDIC_HID_FIRMWARE_MCUBOOT (fu_nordic_hid_firmware_mcuboot_get_type()) G_DECLARE_FINAL_TYPE(FuNordicHidFirmwareMcuboot, fu_nordic_hid_firmware_mcuboot, FU, NORDIC_HID_FIRMWARE_MCUBOOT, FuNordicHidFirmware) fwupd-2.0.10/plugins/nordic-hid/fu-nordic-hid-firmware.c000066400000000000000000000041131501337203100230200ustar00rootroot00000000000000/* * Copyright 2021 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-nordic-hid-firmware.h" typedef struct { guint32 crc32; } FuNordicHidFirmwarePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuNordicHidFirmware, fu_nordic_hid_firmware, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_nordic_hid_firmware_get_instance_private(o)) static void fu_nordic_hid_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuNordicHidFirmware *self = FU_NORDIC_HID_FIRMWARE(firmware); FuNordicHidFirmwarePrivate *priv = GET_PRIVATE(self); fu_xmlb_builder_insert_kx(bn, "crc32", priv->crc32); } static gchar * fu_nordic_hid_firmware_get_checksum(FuFirmware *firmware, GChecksumType csum_kind, GError **error) { FuNordicHidFirmware *self = FU_NORDIC_HID_FIRMWARE(firmware); FuNordicHidFirmwarePrivate *priv = GET_PRIVATE(self); if (!fu_firmware_has_flag(firmware, FU_FIRMWARE_FLAG_HAS_CHECKSUM)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unable to calculate the checksum of the update binary"); return NULL; } return g_strdup_printf("%x", priv->crc32); } static gboolean fu_nordic_hid_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuNordicHidFirmware *self = FU_NORDIC_HID_FIRMWARE(firmware); FuNordicHidFirmwarePrivate *priv = GET_PRIVATE(self); priv->crc32 = 0x01; if (!fu_input_stream_compute_crc32(stream, FU_CRC_KIND_B32_STANDARD, &priv->crc32, error)) return FALSE; /* success */ return TRUE; } static void fu_nordic_hid_firmware_init(FuNordicHidFirmware *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); } static void fu_nordic_hid_firmware_class_init(FuNordicHidFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->export = fu_nordic_hid_firmware_export; firmware_class->get_checksum = fu_nordic_hid_firmware_get_checksum; firmware_class->parse = fu_nordic_hid_firmware_parse; } fwupd-2.0.10/plugins/nordic-hid/fu-nordic-hid-firmware.h000066400000000000000000000006461501337203100230340ustar00rootroot00000000000000/* * Copyright 2021 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_NORDIC_HID_FIRMWARE (fu_nordic_hid_firmware_get_type()) G_DECLARE_DERIVABLE_TYPE(FuNordicHidFirmware, fu_nordic_hid_firmware, FU, NORDIC_HID_FIRMWARE, FuFirmware) struct _FuNordicHidFirmwareClass { FuFirmwareClass parent_class; }; fwupd-2.0.10/plugins/nordic-hid/fu-nordic-hid-plugin.c000066400000000000000000000024071501337203100225060ustar00rootroot00000000000000/* * Copyright 2021 Ricardo Cañuelo * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-nordic-hid-archive.h" #include "fu-nordic-hid-cfg-channel.h" #include "fu-nordic-hid-firmware-b0.h" #include "fu-nordic-hid-firmware-mcuboot.h" #include "fu-nordic-hid-plugin.h" struct _FuNordicHidPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuNordicHidPlugin, fu_nordic_hid_plugin, FU_TYPE_PLUGIN) static void fu_nordic_hid_plugin_init(FuNordicHidPlugin *self) { } static void fu_nordic_hid_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "NordicHidBootloader"); fu_plugin_add_udev_subsystem(plugin, "hidraw"); fu_plugin_add_device_gtype(plugin, FU_TYPE_NORDIC_HID_CFG_CHANNEL); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_NORDIC_HID_ARCHIVE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_NORDIC_HID_FIRMWARE_B0); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_NORDIC_HID_FIRMWARE_MCUBOOT); } static void fu_nordic_hid_plugin_class_init(FuNordicHidPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_nordic_hid_plugin_constructed; } fwupd-2.0.10/plugins/nordic-hid/fu-nordic-hid-plugin.h000066400000000000000000000003671501337203100225160ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuNordicHidPlugin, fu_nordic_hid_plugin, FU, NORDIC_HID_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/nordic-hid/meson.build000066400000000000000000000013101501337203100205500ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginNordicHid"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('nordic-hid.quirk') plugin_builtins += static_library('fu_plugin_nordic_hid', sources: [ 'fu-nordic-hid-plugin.c', 'fu-nordic-hid-cfg-channel.c', 'fu-nordic-hid-firmware.c', 'fu-nordic-hid-firmware-b0.c', 'fu-nordic-hid-firmware-mcuboot.c', 'fu-nordic-hid-archive.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) endif device_tests += files('tests/nordic-hid-nrf52840-mcuboot.json', 'tests/nordic-hid-nrf52840-b0.json', ) fwupd-2.0.10/plugins/nordic-hid/nordic-hid.quirk000066400000000000000000000002731501337203100215120ustar00rootroot00000000000000# Nordic Semiconductor ASA HID Devices [HIDRAW\VEN_1915] Plugin = nordic_hid Flags = no-generic-guids GType = FuNordicHidCfgChannel NordicHidBootloader = B0 NordicHidGeneration = default fwupd-2.0.10/plugins/nordic-hid/tests/000077500000000000000000000000001501337203100175555ustar00rootroot00000000000000fwupd-2.0.10/plugins/nordic-hid/tests/nordic-hid-b0.builder.xml000066400000000000000000000001451501337203100242430ustar00rootroot00000000000000 aGVsbG8gd29ybGQ= fwupd-2.0.10/plugins/nordic-hid/tests/nordic-hid-mcuboot.builder.xml000066400000000000000000000001521501337203100254100ustar00rootroot00000000000000 aGVsbG8gd29ybGQ= fwupd-2.0.10/plugins/nordic-hid/tests/nordic-hid-nrf52840-b0.json000066400000000000000000000007731501337203100241640ustar00rootroot00000000000000{ "name": "nRF52840 DK (nRF52 Desktop) B0 variant", "interactive": false, "steps": [ { "url": "1f2931d9573f54266fffed606ea4fe8cc9fce5a9876be365ae74292f55dc3d34-nordic-nrf52840dk-0.0.0.2.cab", "emulation-url": "19ebd9d8f941f80dc6c582fcb66da5db99cb571fad642a0d27fa32d0f76b1387-nordic-nrf52840dk-0.0.0.2.zip", "components": [ { "version": "0.0.0.2", "guids": [ "22952036-c346-5755-9646-7bf766b28922" ] } ] } ] } fwupd-2.0.10/plugins/nordic-hid/tests/nordic-hid-nrf52840-mcuboot.json000066400000000000000000000012711501337203100253250ustar00rootroot00000000000000{ "name": "nRF52840 DK (nRF52 Desktop) MCUBoot variant", "interactive": false, "steps": [ { "url": "904ff137256218bc617661bb4fc2bfddad19522c7e54773d1fd0290b54adfcee-nordic-nrf52840dk-mcuboot-0.0.0_2.cab", "components": [ { "version": "0.0.0.2", "guids": [ "43b38427-fdf5-5400-a23c-f3eb7ea00e7c" ] } ] }, { "url": "2eb21f9439f0c4928c14548ff5c5c73ad6590d23fa704c58c4afa88168bcea90-nordic-nrf52840dk-mcuboot-0.0.0_3.cab", "components": [ { "version": "0.0.0.3", "guids": [ "43b38427-fdf5-5400-a23c-f3eb7ea00e7c" ] } ] } ] } fwupd-2.0.10/plugins/nvme/000077500000000000000000000000001501337203100153405ustar00rootroot00000000000000fwupd-2.0.10/plugins/nvme/README.md000066400000000000000000000037201501337203100166210ustar00rootroot00000000000000--- title: Plugin: NVMe --- ## Introduction This plugin adds support for NVMe storage hardware. Devices are enumerated from the Identify Controller data structure and can be updated with appropriate firmware file. Firmware is sent in 4kB chunks and activated on next reboot. The device GUID is read from the vendor specific area and if not found then generated from the trimmed model string. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in an unspecified binary file format. This plugin supports the following protocol ID: * `org.nvmexpress` ## GUID Generation These device use the NVMe DeviceInstanceId values, e.g. * `NVME\VEN_1179&DEV_010F` The FRU globally unique identifier (FGUID) is also added from the CNS if set. Please refer to this document for more details on how to add support for [FGUID](https://nvmexpress.org/wp-content/uploads/NVM_Express_Revision_1.3.pdf). Additionally, an extra instance IDs containing the version may be added. * `NVME\VEN_1179&DEV_010F&VER_3B2QGXA7` Additionally, for NVMe drives with Dell vendor firmware two extra GUIDs are added: * `STORAGE-DELL-${component-id}` and any optional GUID saved in the vendor extension block. ## Update Behavior The firmware is deployed when the device is in normal runtime mode, but it is only activated when the system is either restarted or in some cases shutdown. ## Quirk Use This plugin uses the following plugin-specific quirks: ### NvmeBlockSize The block size used for NVMe writes Since: 1.1.3 ### `Flags=force-align` Force alignment of the firmware file. Since 1.2.4 ### `Flags=commit-ca3` Replace, and activate immediately rather than on next reset. Since 1.8.15 ## Vendor ID Security The vendor ID is set from the udev vendor, for example set to `NVME:0x1179` ## External Interface Access This plugin requires ioctl `NVME_IOCTL_ADMIN_CMD` access. ## Version Considerations This plugin has been available since fwupd version `1.1.2`. fwupd-2.0.10/plugins/nvme/fu-nvme-common.c000066400000000000000000000123561501337203100203560ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-nvme-common.h" const gchar * fu_nvme_status_to_string(guint32 status) { switch (status) { case NVME_SC_SUCCESS: return "Command completed successfully"; case NVME_SC_INVALID_OPCODE: return "Associated command opcode field is not valid"; case NVME_SC_INVALID_FIELD: return "Unsupported value in a defined field"; case NVME_SC_CMDID_CONFLICT: return "Command identifier is already in use"; case NVME_SC_DATA_XFER_ERROR: return "Error while trying to transfer the data or metadata"; case NVME_SC_POWER_LOSS: return "Command aborted due to power loss notification"; case NVME_SC_INTERNAL: return "Internal error"; case NVME_SC_ABORT_REQ: return "Command Abort request"; case NVME_SC_ABORT_QUEUE: return "Delete I/O Submission Queue request"; case NVME_SC_FUSED_FAIL: return "Other command in a fused operation failing"; case NVME_SC_FUSED_MISSING: return "Missing Fused Command"; case NVME_SC_INVALID_NS: return "Namespace or the format of that namespace is invalid"; case NVME_SC_CMD_SEQ_ERROR: return "Protocol violation in a multicommand sequence"; case NVME_SC_SANITIZE_FAILED: return "No recovery actions has been successfully completed"; case NVME_SC_SANITIZE_IN_PROGRESS: return "A sanitize operation is in progress"; case NVME_SC_LBA_RANGE: return "LBA exceeds the size of the namespace"; case NVME_SC_NS_WRITE_PROTECTED: return "Namespace is write protected by the host"; case NVME_SC_CAP_EXCEEDED: return "Capacity of the namespace to be exceeded"; case NVME_SC_NS_NOT_READY: return "Namespace is not ready to be accessed"; case NVME_SC_RESERVATION_CONFLICT: return "Conflict with a reservation on the accessed namespace"; case NVME_SC_CQ_INVALID: return "Completion Queue does not exist"; case NVME_SC_QID_INVALID: return "Invalid queue identifier specified"; case NVME_SC_QUEUE_SIZE: return "Invalid queue size"; case NVME_SC_ABORT_LIMIT: return "Outstanding Abort commands has exceeded the limit"; case NVME_SC_ABORT_MISSING: return "Abort command is missing"; case NVME_SC_ASYNC_LIMIT: return "Outstanding Async commands has been exceeded"; case NVME_SC_FIRMWARE_SLOT: return "Slot is invalid or read only"; case NVME_SC_FIRMWARE_IMAGE: return "Image specified for activation is invalid"; case NVME_SC_INVALID_VECTOR: return "Creation failed due to an invalid interrupt vector"; case NVME_SC_INVALID_LOG_PAGE: return "Log page indicated is invalid"; case NVME_SC_INVALID_FORMAT: return "LBA Format specified is not supported"; case NVME_SC_FW_NEEDS_CONV_RESET: return "commit was successful, but activation requires reset"; case NVME_SC_INVALID_QUEUE: return "Failed to delete the I/O Completion Queue specified"; case NVME_SC_FEATURE_NOT_SAVEABLE: return "Feature Identifier does not support a saveable value"; case NVME_SC_FEATURE_NOT_CHANGEABLE: return "Feature Identifier is not able to be changed"; case NVME_SC_FEATURE_NOT_PER_NS: return "Feature Identifier specified is not namespace specific"; case NVME_SC_FW_NEEDS_SUBSYS_RESET: return "Commit was successful, activation requires NVM Subsystem"; case NVME_SC_FW_NEEDS_RESET: return "Commit was successful, activation requires a reset"; case NVME_SC_FW_NEEDS_MAX_TIME: return "Would exceed the Maximum Time for Firmware Activation"; case NVME_SC_FW_ACTIVATE_PROHIBITED: return "Image specified is being prohibited from activation"; case NVME_SC_OVERLAPPING_RANGE: return "Image has overlapping ranges"; case NVME_SC_NS_INSUFFICENT_CAP: return "Requires more free space than is currently available"; case NVME_SC_NS_ID_UNAVAILABLE: return "Number of namespaces supported has been exceeded"; case NVME_SC_NS_ALREADY_ATTACHED: return "Controller is already attached to the namespace"; case NVME_SC_NS_IS_PRIVATE: return "Namespace is private"; case NVME_SC_NS_NOT_ATTACHED: return "Controller is not attached to the namespace"; case NVME_SC_THIN_PROV_NOT_SUPP: return "Thin provisioning is not supported by the controller"; case NVME_SC_CTRL_LIST_INVALID: return "Controller list provided is invalid"; case NVME_SC_BP_WRITE_PROHIBITED: return "Trying to modify a Boot Partition while it is locked"; case NVME_SC_BAD_ATTRIBUTES: return "Bad attributes"; case NVME_SC_WRITE_FAULT: return "Write data could not be committed to the media"; case NVME_SC_READ_ERROR: return "Read data could not be recovered from the media"; case NVME_SC_GUARD_CHECK: return "End-to-end guard check failure"; case NVME_SC_APPTAG_CHECK: return "End-to-end application tag check failure"; case NVME_SC_REFTAG_CHECK: return "End-to-end reference tag check failure"; case NVME_SC_COMPARE_FAILED: return "Miscompare during a Compare command"; case NVME_SC_ACCESS_DENIED: return "Access denied"; case NVME_SC_UNWRITTEN_BLOCK: return "Read from an LBA range containing a unwritten block"; case NVME_SC_ANA_PERSISTENT_LOSS: return "Namespace is in the ANA Persistent Loss state"; case NVME_SC_ANA_INACCESSIBLE: return "Namespace being in the ANA Inaccessible state"; case NVME_SC_ANA_TRANSITION: return "Namespace transitioning between Async Access states"; default: return "Unknown"; } } fwupd-2.0.10/plugins/nvme/fu-nvme-common.h000066400000000000000000000060371501337203100203620ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include enum { /* * Generic Command Status: */ NVME_SC_SUCCESS = 0x0, NVME_SC_INVALID_OPCODE = 0x1, NVME_SC_INVALID_FIELD = 0x2, NVME_SC_CMDID_CONFLICT = 0x3, NVME_SC_DATA_XFER_ERROR = 0x4, NVME_SC_POWER_LOSS = 0x5, NVME_SC_INTERNAL = 0x6, NVME_SC_ABORT_REQ = 0x7, NVME_SC_ABORT_QUEUE = 0x8, NVME_SC_FUSED_FAIL = 0x9, NVME_SC_FUSED_MISSING = 0xa, NVME_SC_INVALID_NS = 0xb, NVME_SC_CMD_SEQ_ERROR = 0xc, NVME_SC_SGL_INVALID_LAST = 0xd, NVME_SC_SGL_INVALID_COUNT = 0xe, NVME_SC_SGL_INVALID_DATA = 0xf, NVME_SC_SGL_INVALID_METADATA = 0x10, NVME_SC_SGL_INVALID_TYPE = 0x11, NVME_SC_SGL_INVALID_OFFSET = 0x16, NVME_SC_SGL_INVALID_SUBTYPE = 0x17, NVME_SC_SANITIZE_FAILED = 0x1C, NVME_SC_SANITIZE_IN_PROGRESS = 0x1D, NVME_SC_NS_WRITE_PROTECTED = 0x20, NVME_SC_LBA_RANGE = 0x80, NVME_SC_CAP_EXCEEDED = 0x81, NVME_SC_NS_NOT_READY = 0x82, NVME_SC_RESERVATION_CONFLICT = 0x83, /* * Command Specific Status: */ NVME_SC_CQ_INVALID = 0x100, NVME_SC_QID_INVALID = 0x101, NVME_SC_QUEUE_SIZE = 0x102, NVME_SC_ABORT_LIMIT = 0x103, NVME_SC_ABORT_MISSING = 0x104, NVME_SC_ASYNC_LIMIT = 0x105, NVME_SC_FIRMWARE_SLOT = 0x106, NVME_SC_FIRMWARE_IMAGE = 0x107, NVME_SC_INVALID_VECTOR = 0x108, NVME_SC_INVALID_LOG_PAGE = 0x109, NVME_SC_INVALID_FORMAT = 0x10a, NVME_SC_FW_NEEDS_CONV_RESET = 0x10b, NVME_SC_INVALID_QUEUE = 0x10c, NVME_SC_FEATURE_NOT_SAVEABLE = 0x10d, NVME_SC_FEATURE_NOT_CHANGEABLE = 0x10e, NVME_SC_FEATURE_NOT_PER_NS = 0x10f, NVME_SC_FW_NEEDS_SUBSYS_RESET = 0x110, NVME_SC_FW_NEEDS_RESET = 0x111, NVME_SC_FW_NEEDS_MAX_TIME = 0x112, NVME_SC_FW_ACTIVATE_PROHIBITED = 0x113, NVME_SC_OVERLAPPING_RANGE = 0x114, NVME_SC_NS_INSUFFICENT_CAP = 0x115, NVME_SC_NS_ID_UNAVAILABLE = 0x116, NVME_SC_NS_ALREADY_ATTACHED = 0x118, NVME_SC_NS_IS_PRIVATE = 0x119, NVME_SC_NS_NOT_ATTACHED = 0x11a, NVME_SC_THIN_PROV_NOT_SUPP = 0x11b, NVME_SC_CTRL_LIST_INVALID = 0x11c, NVME_SC_BP_WRITE_PROHIBITED = 0x11e, /* * I/O Command Set Specific - NVM commands: */ NVME_SC_BAD_ATTRIBUTES = 0x180, NVME_SC_INVALID_PI = 0x181, NVME_SC_READ_ONLY = 0x182, NVME_SC_ONCS_NOT_SUPPORTED = 0x183, /* * I/O Command Set Specific - Fabrics commands: */ NVME_SC_CONNECT_FORMAT = 0x180, NVME_SC_CONNECT_CTRL_BUSY = 0x181, NVME_SC_CONNECT_INVALID_PARAM = 0x182, NVME_SC_CONNECT_RESTART_DISC = 0x183, NVME_SC_CONNECT_INVALID_HOST = 0x184, NVME_SC_DISCOVERY_RESTART = 0x190, NVME_SC_AUTH_REQUIRED = 0x191, /* * Media and Data Integrity Errors: */ NVME_SC_WRITE_FAULT = 0x280, NVME_SC_READ_ERROR = 0x281, NVME_SC_GUARD_CHECK = 0x282, NVME_SC_APPTAG_CHECK = 0x283, NVME_SC_REFTAG_CHECK = 0x284, NVME_SC_COMPARE_FAILED = 0x285, NVME_SC_ACCESS_DENIED = 0x286, NVME_SC_UNWRITTEN_BLOCK = 0x287, /* * Path-related Errors: */ NVME_SC_ANA_PERSISTENT_LOSS = 0x301, NVME_SC_ANA_INACCESSIBLE = 0x302, NVME_SC_ANA_TRANSITION = 0x303, NVME_SC_DNR = 0x4000, }; const gchar * fu_nvme_status_to_string(guint32 status); fwupd-2.0.10/plugins/nvme/fu-nvme-device.c000066400000000000000000000402441501337203100203220ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fu-nvme-common.h" #include "fu-nvme-device.h" #define FU_NVME_ID_CTRL_SIZE 0x1000 struct _FuNvmeDevice { FuPciDevice parent_instance; guint pci_depth; guint64 write_block_size; }; #define FU_NVME_COMMIT_ACTION_CA0 0b000 /* replace only */ #define FU_NVME_COMMIT_ACTION_CA1 0b001 /* replace, and activate on next reset */ #define FU_NVME_COMMIT_ACTION_CA2 0b010 /* activate on next reset */ #define FU_NVME_COMMIT_ACTION_CA3 0b011 /* replace, and activate immediately */ #define FU_NVME_DEVICE_FLAG_FORCE_ALIGN "force-align" #define FU_NVME_DEVICE_FLAG_COMMIT_CA3 "commit-ca3" G_DEFINE_TYPE(FuNvmeDevice, fu_nvme_device, FU_TYPE_PCI_DEVICE) #define FU_NVME_DEVICE_IOCTL_TIMEOUT 5000 /* ms */ static void fu_nvme_device_to_string(FuDevice *device, guint idt, GString *str) { FuNvmeDevice *self = FU_NVME_DEVICE(device); fwupd_codec_string_append_int(str, idt, "PciDepth", self->pci_depth); } /* @addr_start and @addr_end are *inclusive* to match the NMVe specification */ static gchar * fu_nvme_device_get_string_safe(const guint8 *buf, guint16 addr_start, guint16 addr_end) { GString *str; g_return_val_if_fail(buf != NULL, NULL); g_return_val_if_fail(addr_start < addr_end, NULL); str = g_string_new_len(NULL, addr_end + addr_start + 1); for (guint16 i = addr_start; i <= addr_end; i++) { gchar tmp = (gchar)buf[i]; /* skip leading spaces */ if (g_ascii_isspace(tmp) && str->len == 0) continue; if (g_ascii_isprint(tmp)) g_string_append_c(str, tmp); } /* nothing found */ if (str->len == 0) { g_string_free(str, TRUE); return NULL; } return g_strchomp(g_string_free(str, FALSE)); } static gchar * fu_nvme_device_get_guid_safe(const guint8 *buf, guint16 addr_start) { if (!fu_common_guid_is_plausible(buf + addr_start)) return NULL; return fwupd_guid_to_string((const fwupd_guid_t *)(buf + addr_start), FWUPD_GUID_FLAG_MIXED_ENDIAN); } static gboolean fu_nvme_device_ioctl_buffer_cb(FuIoctl *self, gpointer ptr, guint8 *buf, gsize bufsz, GError **error) { struct nvme_admin_cmd *cmd = (struct nvme_admin_cmd *)ptr; return fu_memcpy_safe((guint8 *)&cmd->addr, sizeof(cmd->addr), 0x0, (guint8 *)&buf, sizeof(buf), 0x0, sizeof(buf), error); } static gboolean fu_nvme_device_submit_admin_passthru(FuNvmeDevice *self, struct nvme_admin_cmd *cmd, guint8 *buf, gsize bufsz, GError **error) { guint32 err; gint rc = 0; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); /* include these when generating the emulation event */ fu_ioctl_set_name(ioctl, "Nvme"); fu_ioctl_add_key_as_u8(ioctl, "Opcode", cmd->opcode); fu_ioctl_add_key_as_u8(ioctl, "Cdw10", cmd->cdw10); fu_ioctl_add_key_as_u8(ioctl, "Cdw11", cmd->cdw11); fu_ioctl_add_mutable_buffer(ioctl, NULL, buf, bufsz, fu_nvme_device_ioctl_buffer_cb); if (!fu_ioctl_execute(ioctl, NVME_IOCTL_ADMIN_CMD, cmd, sizeof(*cmd), &rc, FU_NVME_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_NONE, error)) { g_prefix_error(error, "failed to issue admin command 0x%02x: ", cmd->opcode); return FALSE; } /* check the error code */ err = rc & 0x3ff; switch (err) { case NVME_SC_SUCCESS: /* devices are always added with _NEEDS_REBOOT, so ignore */ case NVME_SC_FW_NEEDS_CONV_RESET: case NVME_SC_FW_NEEDS_SUBSYS_RESET: case NVME_SC_FW_NEEDS_RESET: return TRUE; default: break; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Not supported: %s", fu_nvme_status_to_string(err)); return FALSE; } static gboolean fu_nvme_device_identify_ctrl(FuNvmeDevice *self, guint8 *buf, gsize bufsz, GError **error) { struct nvme_admin_cmd cmd = { .opcode = 0x06, .nsid = 0x00, .addr = 0x0, /* memory address of data */ .data_len = bufsz, .cdw10 = 0x01, .cdw11 = 0x00, }; return fu_nvme_device_submit_admin_passthru(self, &cmd, buf, bufsz, error); } static gboolean fu_nvme_device_fw_commit(FuNvmeDevice *self, guint8 slot, guint8 action, guint8 bpid, GError **error) { struct nvme_admin_cmd cmd = { .opcode = 0x10, .cdw10 = (bpid << 31) | (action << 3) | slot, }; return fu_nvme_device_submit_admin_passthru(self, &cmd, NULL, 0, error); } static gboolean fu_nvme_device_fw_download(FuNvmeDevice *self, guint32 addr, const guint8 *buf, gsize bufsz, GError **error) { struct nvme_admin_cmd cmd = { .opcode = 0x11, .addr = 0x0, /* memory address of data */ .data_len = bufsz, .cdw10 = (bufsz >> 2) - 1, /* convert to DWORDs */ .cdw11 = addr >> 2, /* convert to DWORDs */ }; g_autofree guint8 *buf_mut = fu_memdup_safe(buf, bufsz, error); if (buf_mut == NULL) return FALSE; return fu_nvme_device_submit_admin_passthru(self, &cmd, buf_mut, bufsz, error); } static void fu_nvme_device_parse_cns_maybe_dell(FuNvmeDevice *self, const guint8 *buf) { g_autofree gchar *component_id = NULL; g_autofree gchar *devid = NULL; g_autofree gchar *guid_efi = NULL; /* add extra component ID if set */ component_id = fu_nvme_device_get_string_safe(buf, 0xc36, 0xc3d); if (component_id == NULL || !g_str_is_ascii(component_id) || strlen(component_id) < 6) { g_debug("invalid component ID, skipping"); return; } /* do not add the FuUdevDevice instance IDs as generic firmware * should not be used on these OEM-specific devices */ fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_NO_AUTO_INSTANCE_IDS); /* add instance ID *and* GUID as using no-auto-instance-ids */ devid = g_strdup_printf("STORAGE-DELL-%s", component_id); fu_device_add_instance_id(FU_DEVICE(self), devid); /* also add the EFI GUID */ guid_efi = fu_nvme_device_get_guid_safe(buf, 0x0c26); if (guid_efi != NULL) fu_device_add_instance_id(FU_DEVICE(self), guid_efi); } static gboolean fu_nvme_device_parse_cns(FuNvmeDevice *self, const guint8 *buf, gsize sz, GError **error) { guint8 fawr; guint8 fwug; guint8 nfws; guint8 s1ro; g_autofree gchar *gu = NULL; g_autofree gchar *mn = NULL; g_autofree gchar *sn = NULL; g_autofree gchar *sr = NULL; /* wrong size */ if (sz != FU_NVME_ID_CTRL_SIZE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to parse blob, expected 0x%04x bytes", (guint)FU_NVME_ID_CTRL_SIZE); return FALSE; } /* get sanitized string from CNS -- see the following doc for offsets: * NVM-Express-1_3c-2018.05.24-Ratified.pdf */ sn = fu_nvme_device_get_string_safe(buf, 4, 23); if (sn != NULL) fu_device_set_serial(FU_DEVICE(self), sn); mn = fu_nvme_device_get_string_safe(buf, 24, 63); if (mn != NULL) fu_device_set_name(FU_DEVICE(self), mn); sr = fu_nvme_device_get_string_safe(buf, 64, 71); if (sr != NULL) fu_device_set_version(FU_DEVICE(self), sr); /* firmware update granularity (FWUG) */ fwug = buf[319]; if (fwug != 0x00 && fwug != 0xff) self->write_block_size = ((guint64)fwug) * 0x1000; /* firmware slot information */ fawr = (buf[260] & 0x10) >> 4; nfws = (buf[260] & 0x0e) >> 1; s1ro = buf[260] & 0x01; g_debug("fawr: %u, nr fw slots: %u, slot1 r/o: %u", fawr, nfws, s1ro); /* FRU globally unique identifier (FGUID) */ gu = fu_nvme_device_get_guid_safe(buf, 127); if (gu != NULL) fu_device_add_instance_id(FU_DEVICE(self), gu); /* Dell helpfully provide an EFI GUID we can use in the vendor offset, * but don't have a header or any magic we can use -- so check if the * component ID looks plausible and the GUID is "sane" */ fu_nvme_device_parse_cns_maybe_dell(self, buf); /* fall back to the device description */ if (mn != NULL && fu_device_get_guids(FU_DEVICE(self))->len == 0) { g_debug("no vendor GUID, falling back to mn"); fu_device_add_instance_id(FU_DEVICE(self), mn); } return TRUE; } static gboolean fu_nvme_device_pci_probe(FuNvmeDevice *self, GError **error) { g_autoptr(FuDevice) pci_donor = NULL; pci_donor = fu_device_get_backend_parent_with_subsystem(FU_DEVICE(self), "pci", error); if (pci_donor == NULL) return FALSE; if (!fu_device_probe(pci_donor, error)) return FALSE; fu_device_incorporate(FU_DEVICE(self), pci_donor, FU_DEVICE_INCORPORATE_FLAG_VENDOR | FU_DEVICE_INCORPORATE_FLAG_VENDOR_IDS | FU_DEVICE_INCORPORATE_FLAG_VID | FU_DEVICE_INCORPORATE_FLAG_PID | FU_DEVICE_INCORPORATE_FLAG_PHYSICAL_ID); fu_pci_device_set_revision(FU_PCI_DEVICE(self), fu_pci_device_get_revision(FU_PCI_DEVICE(pci_donor))); fu_pci_device_set_subsystem_vid(FU_PCI_DEVICE(self), fu_pci_device_get_subsystem_vid(FU_PCI_DEVICE(pci_donor))); fu_pci_device_set_subsystem_pid(FU_PCI_DEVICE(self), fu_pci_device_get_subsystem_pid(FU_PCI_DEVICE(pci_donor))); if (!fu_device_build_instance_id(FU_DEVICE(self), error, "NVME", "VEN", "DEV", NULL)) return FALSE; if (!fu_device_build_instance_id_full(FU_DEVICE(self), FU_DEVICE_INSTANCE_FLAG_QUIRKS, error, "NVME", "VEN", NULL)) return FALSE; fu_device_build_instance_id(FU_DEVICE(self), NULL, "NVME", "VEN", "DEV", "SUBSYS", NULL); /* success */ return TRUE; } static gboolean fu_nvme_device_probe(FuDevice *device, GError **error) { FuNvmeDevice *self = FU_NVME_DEVICE(device); /* copy the PCI-specific instance parts and make them NVME for GUID compat */ if (!fu_nvme_device_pci_probe(self, error)) return FALSE; /* fix up vendor name so we can remove it from the product name */ if (g_strcmp0(fu_device_get_vendor(FU_DEVICE(device)), "Samsung Electronics Co Ltd") == 0) fu_device_set_vendor(FU_DEVICE(device), "Samsung"); /* look at the PCI depth to work out if in an external enclosure */ self->pci_depth = fu_udev_device_get_subsystem_depth(FU_UDEV_DEVICE(device), "pci"); if (self->pci_depth <= 2) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE); } /* most devices need at least a warm reset, but some quirked drives * need a full "cold" shutdown and startup */ if (!fu_device_has_private_flag(device, FU_NVME_DEVICE_FLAG_COMMIT_CA3) && !fu_device_has_flag(self, FWUPD_DEVICE_FLAG_EMULATED) && !fu_device_has_flag(self, FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN)) fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT); return TRUE; } static gboolean fu_nvme_device_setup(FuDevice *device, GError **error) { FuNvmeDevice *self = FU_NVME_DEVICE(device); guint8 buf[FU_NVME_ID_CTRL_SIZE] = {0x0}; /* get and parse CNS */ if (!fu_nvme_device_identify_ctrl(self, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to identify %s: ", fu_device_get_physical_id(FU_DEVICE(self))); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "CNS", buf, sizeof(buf)); if (!fu_nvme_device_parse_cns(self, buf, sizeof(buf), error)) return FALSE; /* add one extra instance ID so that we can match bad firmware */ fu_device_add_instance_strsafe(device, "VER", fu_device_get_version(device)); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "NVME", "VEN", "DEV", "VER", NULL); /* success */ return TRUE; } static gboolean fu_nvme_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuNvmeDevice *self = FU_NVME_DEVICE(device); g_autoptr(GBytes) fw2 = NULL; g_autoptr(GBytes) fw = NULL; g_autoptr(FuChunkArray) chunks = NULL; guint64 block_size = self->write_block_size > 0 ? self->write_block_size : 0x1000; guint8 commit_action = FU_NVME_COMMIT_ACTION_CA1; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 20, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 80, "commit"); /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* some vendors provide firmware files whose sizes are not multiples * of blksz *and* the device won't accept blocks of different sizes */ if (fu_device_has_private_flag(device, FU_NVME_DEVICE_FLAG_FORCE_ALIGN)) { fw2 = fu_bytes_align(fw, block_size, 0xff); } else { fw2 = g_bytes_ref(fw); } /* write each block */ chunks = fu_chunk_array_new_from_bytes(fw2, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, block_size); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_nvme_device_fw_download(self, fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "failed to write chunk %u: ", i); return FALSE; } fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)i + 1, (gsize)fu_chunk_array_length(chunks)); } fu_progress_step_done(progress); /* commit */ if (fu_device_has_private_flag(device, FU_NVME_DEVICE_FLAG_COMMIT_CA3)) commit_action = FU_NVME_COMMIT_ACTION_CA3; if (!fu_nvme_device_fw_commit(self, 0x00, /* let controller choose */ commit_action, 0x00, /* boot partition identifier */ error)) { g_prefix_error(error, "failed to commit to auto slot: "); return FALSE; } fu_progress_step_done(progress); /* success! */ return TRUE; } static gboolean fu_nvme_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuNvmeDevice *self = FU_NVME_DEVICE(device); if (g_strcmp0(key, "NvmeBlockSize") == 0) { guint64 tmp = 0; if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->write_block_size = tmp; return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_nvme_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 80, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 20, "reload"); } static void fu_nvme_device_init(FuNvmeDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_SIGNED); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_FLAGS); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_RETRY_OPEN); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_summary(FU_DEVICE(self), "NVM Express solid state drive"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_DRIVE_HARDDISK); fu_device_add_protocol(FU_DEVICE(self), "org.nvmexpress"); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_device_register_private_flag(FU_DEVICE(self), FU_NVME_DEVICE_FLAG_FORCE_ALIGN); fu_device_register_private_flag(FU_DEVICE(self), FU_NVME_DEVICE_FLAG_COMMIT_CA3); } static void fu_nvme_device_class_init(FuNvmeDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_nvme_device_to_string; device_class->set_quirk_kv = fu_nvme_device_set_quirk_kv; device_class->setup = fu_nvme_device_setup; device_class->write_firmware = fu_nvme_device_write_firmware; device_class->probe = fu_nvme_device_probe; device_class->set_progress = fu_nvme_device_set_progress; } FuNvmeDevice * fu_nvme_device_new_from_blob(FuContext *ctx, const guint8 *buf, gsize sz, GError **error) { g_autoptr(FuNvmeDevice) self = NULL; self = g_object_new(FU_TYPE_NVME_DEVICE, "context", ctx, NULL); if (!fu_nvme_device_parse_cns(self, buf, sz, error)) return NULL; return g_steal_pointer(&self); } fwupd-2.0.10/plugins/nvme/fu-nvme-device.h000066400000000000000000000006141501337203100203240ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_NVME_DEVICE (fu_nvme_device_get_type()) G_DECLARE_FINAL_TYPE(FuNvmeDevice, fu_nvme_device, FU, NVME_DEVICE, FuPciDevice) FuNvmeDevice * fu_nvme_device_new_from_blob(FuContext *ctx, const guint8 *buf, gsize sz, GError **error); fwupd-2.0.10/plugins/nvme/fu-nvme-plugin.c000066400000000000000000000015161501337203100203600ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-nvme-device.h" #include "fu-nvme-plugin.h" struct _FuNvmePlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuNvmePlugin, fu_nvme_plugin, FU_TYPE_PLUGIN) static void fu_nvme_plugin_init(FuNvmePlugin *self) { } static void fu_nvme_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "NvmeBlockSize"); fu_plugin_add_device_udev_subsystem(plugin, "nvme"); fu_plugin_add_device_gtype(plugin, FU_TYPE_NVME_DEVICE); } static void fu_nvme_plugin_class_init(FuNvmePluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_nvme_plugin_constructed; } fwupd-2.0.10/plugins/nvme/fu-nvme-plugin.h000066400000000000000000000003461501337203100203650ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuNvmePlugin, fu_nvme_plugin, FU, NVME_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/nvme/fu-self-test.c000066400000000000000000000061141501337203100200240ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-context-private.h" #include "fu-device-private.h" #include "fu-nvme-device.h" static void fu_nvme_cns_func(void) { gboolean ret; gsize sz; g_autofree gchar *data = NULL; g_autofree gchar *path = NULL; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuNvmeDevice) dev = NULL; g_autoptr(GError) error = NULL; ret = fu_context_load_quirks(ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE, &error); g_assert_no_error(error); g_assert_true(ret); path = g_test_build_filename(G_TEST_DIST, "tests", "TOSHIBA_THNSN5512GPU7.bin", NULL); if (!g_file_test(path, G_FILE_TEST_EXISTS)) { g_test_skip("Missing TOSHIBA_THNSN5512GPU7.bin"); return; } ret = g_file_get_contents(path, &data, &sz, &error); g_assert_no_error(error); g_assert_true(ret); dev = fu_nvme_device_new_from_blob(ctx, (guint8 *)data, sz, &error); g_assert_no_error(error); g_assert_nonnull(dev); fu_device_convert_instance_ids(FU_DEVICE(dev)); g_assert_cmpstr(fu_device_get_name(FU_DEVICE(dev)), ==, "THNSN5512GPU7 TOSHIBA"); g_assert_cmpstr(fu_device_get_version(FU_DEVICE(dev)), ==, "410557LA"); g_assert_cmpstr(fu_device_get_serial(FU_DEVICE(dev)), ==, "37RSDEADBEEF"); g_assert_cmpstr(fu_device_get_guid_default(FU_DEVICE(dev)), ==, "e1409b09-50cf-5aef-8ad8-760b9022f88d"); } static void fu_nvme_cns_all_func(void) { const gchar *fn; g_autofree gchar *path = NULL; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(GDir) dir = NULL; /* may or may not exist */ path = g_test_build_filename(G_TEST_DIST, "tests", "blobs", NULL); if (!g_file_test(path, G_FILE_TEST_EXISTS)) return; dir = g_dir_open(path, 0, NULL); while ((fn = g_dir_read_name(dir)) != NULL) { gsize sz; g_autofree gchar *data = NULL; g_autofree gchar *filename = NULL; g_autoptr(FuNvmeDevice) dev = NULL; g_autoptr(GError) error = NULL; filename = g_build_filename(path, fn, NULL); g_print("parsing %s... ", filename); if (!g_file_get_contents(filename, &data, &sz, &error)) { g_print("failed to load %s: %s\n", filename, error->message); continue; } dev = fu_nvme_device_new_from_blob(ctx, (guint8 *)data, sz, &error); if (dev == NULL) { g_print("failed to load %s: %s\n", filename, error->message); continue; } g_assert_cmpstr(fu_device_get_name(FU_DEVICE(dev)), !=, NULL); g_assert_cmpstr(fu_device_get_version(FU_DEVICE(dev)), !=, NULL); g_assert_cmpstr(fu_device_get_serial(FU_DEVICE(dev)), !=, NULL); g_print("done\n"); } } int main(int argc, char **argv) { g_autofree gchar *testdatadir = NULL; (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); testdatadir = g_test_build_filename(G_TEST_DIST, "tests", NULL); (void)g_setenv("FWUPD_SYSFSFWATTRIBDIR", testdatadir, TRUE); /* tests go here */ g_test_add_func("/fwupd/cns", fu_nvme_cns_func); g_test_add_func("/fwupd/cns{all}", fu_nvme_cns_all_func); return g_test_run(); } fwupd-2.0.10/plugins/nvme/meson.build000066400000000000000000000025301501337203100175020ustar00rootroot00000000000000nvme_header = cc.has_header('linux/nvme_ioctl.h', required: false) if nvme_header and host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginNvme"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('nvme.quirk') plugin_builtin_nvme = static_library('fu_plugin_nvme', sources: [ 'fu-nvme-plugin.c', 'fu-nvme-common.c', 'fu-nvme-device.c', ], include_directories: plugin_incdirs, c_args: [ cargs, '-DLOCALSTATEDIR="' + localstatedir + '"', ], link_with: plugin_libs, dependencies: plugin_deps, ) plugin_builtins += plugin_builtin_nvme device_tests += files( 'tests/lenovo-pcsn720.json', ) if get_option('tests') env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) env.set('FWUPD_DATADIR_QUIRKS', meson.current_source_dir()) e = executable( 'nvme-self-test', sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: plugin_deps, link_with: [ plugin_libs, plugin_builtin_nvme, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('nvme-self-test', e, env: env) # added to installed-tests endif endif fwupd-2.0.10/plugins/nvme/nvme.quirk000066400000000000000000000017661501337203100173740ustar00rootroot00000000000000# Phison [NVME\VEN_1987] Flags = force-align,needs-shutdown # Kingston [NVME\VEN_2646] Flags = needs-shutdown # KIOXIA [NVME\VEN_1179] Flags = signed-payload [NVME\VEN_1E0F] Flags = signed-payload # Samsung [NVME\VEN_144D] Flags = signed-payload # SSSTC [NVME\VEN_14A4] Flags = signed-payload [NVME\VEN_1E95] Flags = signed-payload # SK Hynix [NVME\VEN_1C5C] Flags = signed-payload [NVME\VEN_1C5C&DEV_1D59] Flags = commit-ca3 # Kingston [NVME\VEN_2646&DEV_5013] Flags = signed-payload [NVME\VEN_2646&DEV_500A] Flags = signed-payload [NVME\VEN_2646&DEV_500B] Flags = unsigned-payload [NVME\VEN_2646&DEV_5012] Flags = unsigned-payload # Western Digital [NVME\VEN_101C] Flags = signed-payload # Solidigm [NVME\VEN_025E&DEV_F1AB] Flags = needs-shutdown [NVME\VEN_144D&DEV_A80A&VER_2B2QGXA7] Issue = https://www.pugetsystems.com/support/guides/critical-samsung-ssd-firmware-update/ [NVME\VEN_144D&DEV_A80A&VER_3B2QGXA7] Issue = https://www.pugetsystems.com/support/guides/critical-samsung-ssd-firmware-update/ fwupd-2.0.10/plugins/nvme/tests/000077500000000000000000000000001501337203100165025ustar00rootroot00000000000000fwupd-2.0.10/plugins/nvme/tests/lenovo-pcsn720.json000066400000000000000000000007731501337203100221000ustar00rootroot00000000000000{ "name": "Lenovo PCSN720 NVMe Drive", "interactive": false, "steps": [ { "url": "8e2a52b74fcdbcf2f82bf2e0f6b750d6f098be887bb1e4c6acba3b63454a4f73-Lenovo-WD-PCSN720-NVMe-10190101.cab", "emulation-url": "a97b2f699d80f30ac24c3dfda4ed51a7935bdf0289b899d5be1a3dffdd2c8843-Lenovo-WD-PCSN720-NVMe-10190101.zip", "components": [ { "version": "10190101", "guids": [ "237776ee-0bcd-5fe9-8dc8-6984a2d36ba0" ] } ] } ] } fwupd-2.0.10/plugins/parade-lspcon/000077500000000000000000000000001501337203100171235ustar00rootroot00000000000000fwupd-2.0.10/plugins/parade-lspcon/README.md000066400000000000000000000033621501337203100204060ustar00rootroot00000000000000--- title: Plugin: Parade LSPCON --- ## Introduction This plugin updates the firmware of HDMI level shifter and protocol converter (LSPCON) devices made by Parade Technologies, such as the PS175. These devices communicate over I²C, via either the DisplayPort aux channel or a dedicated bus- this plugin uses a dedicated bus declared by system firmware for, flashing, and reads the device firmware version from DPCD. Quirks specify the DisplayPort bus over which DPCD is read for a given system. Firmware is stored on an external flash attached to an SPI bus on the device. The attached flash is assumed to be compatible with the W25Q20 series of devices, in particular supporting a 64k Block Erase command (0xD8) with 24-bit address and Write Enable for Volatile Status Register (0x05). ## Firmware Format The device firmware is in an unspecified binary format that is written directly to an inactive partition of the Flash attached to the device. This plugin supports the following protocol ID: * `com.paradetech.ps176` ## GUID Generation Devices use an extra instance ID derived from SMBIOS, e.g. * `I2C\NAME_1AF80175:00:00&FAMILY_Google_Hatch` ## Vendor ID security The vendor ID is specified by system firmware (such as ACPI tables) and is part of the device's name as read from sysfs. ## External Interface Access This plugin requires access to the DisplayPort aux channel to read DPCD, such as `/dev/drm_dp_aux0` as well as the i2c bus attached to the device, such as `/dev/i2c-7`. ## Version Considerations This plugin has been available since fwupd version `1.6.2`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * PingYao Chen: @Pingyao5115 fwupd-2.0.10/plugins/parade-lspcon/fu-parade-lspcon-device.c000066400000000000000000000703641501337203100236760ustar00rootroot00000000000000/* * Copyright 2021 Peter Marheine * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-dpaux-struct.h" #include "fu-parade-lspcon-device.h" #include "fu-parade-lspcon-struct.h" #define SPI_CTL_NOREAD 0x04 /* set to do a write-only transaction */ #define SPI_CTL_TRIGGER 0x01 /* set to begin executing command */ #define SPI_STATUS_BP_MASK 0x03 /* byte programming */ #define SPI_STATUS_SE_MASK 0x0C /* sector erase */ #define SPI_STATUS_CE_MASK 0x30 /* chip erase */ #define WR_PROTECT_DISABLE 0x10 #define FU_PARADE_LSPCON_DEVICE_IOCTL_TIMEOUT 5000 /* ms */ /* * user1: 0x10000 - 0x20000 * user2: 0x20000 - 0x30000 * flag: 0x00002 - 0x00004 */ struct _FuParadeLspconDevice { FuI2cDevice parent_instance; FuParadeLspconDeviceKind kind; guint8 active_partition; guint8 page_offset; }; G_DEFINE_TYPE(FuParadeLspconDevice, fu_parade_lspcon_device, FU_TYPE_I2C_DEVICE) static void fu_parade_lspcon_device_to_string(FuDevice *device, guint idt, GString *str) { FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(device); fwupd_codec_string_append(str, idt, "Kind", fu_parade_lspcon_device_kind_to_string(self->kind)); fwupd_codec_string_append_hex(str, idt, "ActivePartition", self->active_partition); fwupd_codec_string_append_hex(str, idt, "PageOffset", self->page_offset); } static gboolean fu_parade_lspcon_device_open(FuDevice *device, GError **error) { FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(device); if (!FU_DEVICE_CLASS(fu_parade_lspcon_device_parent_class)->open(device, error)) return FALSE; /* general assumption is that page 2 is selected: code that uses another address * should use an address guard to ensure it gets reset */ return fu_i2c_device_set_address(FU_I2C_DEVICE(self), self->page_offset + FU_PARADE_LSPCON_I2C_ADDR_PAGE2, FALSE, error); } /** * creates a scope in which the device's target I2C address is something * other than page 2, and resets it to page 2 when the scope is left. */ typedef struct { FuParadeLspconDevice *device; } FuParadeLspconI2cAddressGuard; static FuParadeLspconI2cAddressGuard * fu_parade_lspcon_device_i2c_address_guard_new(FuParadeLspconDevice *self, FuParadeLspconI2cAddr new_address, GError **error) { FuParadeLspconI2cAddressGuard *out; if (!fu_i2c_device_set_address(FU_I2C_DEVICE(self), self->page_offset + new_address, FALSE, error)) return NULL; out = g_new0(FuParadeLspconI2cAddressGuard, 1); out->device = self; return out; } static void fu_parade_lspcon_device_i2c_address_guard_free(FuParadeLspconI2cAddressGuard *guard) { FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(guard->device); g_autoptr(GError) error_local = NULL; if (!fu_i2c_device_set_address(FU_I2C_DEVICE(self), self->page_offset + FU_PARADE_LSPCON_I2C_ADDR_PAGE2, FALSE, &error_local)) { g_warning("failed to set page2 back: %s", error_local->message); } g_free(guard); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuParadeLspconI2cAddressGuard, fu_parade_lspcon_device_i2c_address_guard_free); static gboolean fu_parade_lspcon_device_write_register(FuParadeLspconDevice *self, guint8 register_addr, guint8 value, GError **error) { guint8 transaction[] = {register_addr, value}; return fu_i2c_device_write(FU_I2C_DEVICE(self), transaction, sizeof(transaction), error); } static gboolean fu_parade_lspcon_device_read_register(FuParadeLspconDevice *self, guint8 register_addr, guint8 *value, GError **error) { FuI2cDevice *i2c_device = FU_I2C_DEVICE(self); if (!fu_i2c_device_write(i2c_device, ®ister_addr, 0x1, error)) return FALSE; return fu_i2c_device_read(i2c_device, value, 0x1, error); } /* map the page containing the given address into page 7 */ static gboolean fu_parade_lspcon_device_map_page(FuParadeLspconDevice *self, guint32 address, GError **error) { if (!fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_FLASH_ADDR_HI, address >> 16, error)) return FALSE; return fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_FLASH_ADDR_LO, address >> 8, error); } /* wait until the specified register masked with mask reads the expected * value, up to 10 seconds */ static gboolean fu_parade_lspcon_device_poll_register(FuParadeLspconDevice *self, guint8 register_address, guint8 mask, guint8 expected, GError **error) { guint8 value; g_autoptr(GTimer) timer = g_timer_new(); do { if (!fu_parade_lspcon_device_read_register(self, register_address, &value, error)) return FALSE; if ((value & mask) == expected) return TRUE; } while (g_timer_elapsed(timer, NULL) <= 10.0); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "register %x did not read %x (mask %x) within 10 seconds: read %x", register_address, expected, mask, value); return FALSE; } static gboolean fu_parade_lspcon_device_flash_read(FuParadeLspconDevice *self, guint32 base_address, guint8 *data, const gsize len, FuProgress *progress, GError **error) { FuI2cDevice *i2c_device = FU_I2C_DEVICE(self); gsize offset = 0; while (offset < len) { /* page 7 reads always start from the base of the mapped window- we'll * read the whole page then pull out the parts we care about, using the * full page everywhere except possibly in the first and last reads */ guint8 page_data[256] = {0x0}; guint8 page_data_start = base_address & 0xFF; gsize page_data_take = MIN((gssize)len, 256 - page_data_start); g_autoptr(FuParadeLspconI2cAddressGuard) guard = NULL; if (!fu_parade_lspcon_device_map_page(self, base_address, error)) return FALSE; guard = fu_parade_lspcon_device_i2c_address_guard_new(self, FU_PARADE_LSPCON_I2C_ADDR_PAGE7, error); if (guard == NULL) return FALSE; if (!fu_i2c_device_read(i2c_device, page_data, 256, error)) return FALSE; if (!fu_memcpy_safe(data, len, offset, page_data, sizeof(page_data), page_data_start, page_data_take, error)) return FALSE; base_address += page_data_take; offset += page_data_take; fu_progress_set_percentage_full(progress, offset, len); } return TRUE; } static gboolean fu_parade_lspcon_device_flash_transmit_command(FuParadeLspconDevice *self, const guint8 *command, gsize command_len, GError **error) { /* write length field is 4 bits wide */ g_return_val_if_fail(command_len > 0 && command_len <= 16, FALSE); /* fill transmit buffer */ for (gsize i = 0; i < command_len; i++) { if (!fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_WR_FIFO, command[i], error)) return FALSE; } /* set command length */ if (!fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_SPI_LEN, command_len - 1, error)) return FALSE; /* execute operation */ return fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_SPI_CTL, SPI_CTL_NOREAD | SPI_CTL_TRIGGER, error); } /* * set the flash Write Enable Latch, permitting the next program, erase or * status register write operation. */ static gboolean fu_parade_lspcon_device_flash_enable_write(FuParadeLspconDevice *self, GError **error) { const guint8 write_enable[] = {0x06}; return fu_parade_lspcon_device_flash_transmit_command(self, write_enable, sizeof(write_enable), error); } static gboolean fu_parade_lspcon_device_flash_read_status(FuParadeLspconDevice *self, guint8 *value, GError **error) { if (!fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_WR_FIFO, 0x05, error)) return FALSE; if (!fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_SPI_LEN, 0, error)) return FALSE; if (!fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_SPI_CTL, SPI_CTL_TRIGGER, error)) return FALSE; /* wait for command completion */ if (!fu_parade_lspcon_device_poll_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_SPI_CTL, SPI_CTL_TRIGGER, 0, error)) return FALSE; /* read SR value */ return fu_parade_lspcon_device_read_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_RD_FIFO, value, error); } /* poll the flash status register for operation completion */ static gboolean fu_parade_lspcon_device_flash_wait_ready(FuParadeLspconDevice *self, GError **error) { g_autoptr(GTimer) timer = g_timer_new(); do { guint8 status_register; if (!fu_parade_lspcon_device_flash_read_status(self, &status_register, error)) return FALSE; /* BUSY bit clears on completion */ if ((status_register & 1) == 0) return TRUE; /* flash operations generally take between 1ms and 4s; polling * at 1000 Hz is still quite responsive and not overly slow */ fu_device_sleep(FU_DEVICE(self), 1); /* ms */ } while (g_timer_elapsed(timer, NULL) <= 10.0); g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "flash did not become ready within 10 seconds"); return FALSE; } static gboolean fu_parade_lspcon_device_prepare(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(device); const guint8 write_sr_volatile[] = {0x50}; const guint8 write_sr_disable_bp[] = { 0x01, /* write SR */ 0x80, /* write protect follows /WP signal, no block protection */ 0x00}; /* deassert flash /WP */ if (!fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_WR_PROTECT, WR_PROTECT_DISABLE, error)) return FALSE; /* disable flash protection until next power-off */ if (!fu_parade_lspcon_device_flash_transmit_command(self, write_sr_volatile, sizeof(write_sr_volatile), error)) return FALSE; if (!fu_parade_lspcon_device_flash_transmit_command(self, write_sr_disable_bp, sizeof(write_sr_disable_bp), error)) return FALSE; /* wait for SR write to complete */ return fu_parade_lspcon_device_flash_wait_ready(self, error); } static gboolean fu_parade_lspcon_device_cleanup(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(device); const guint8 write_sr_volatile[] = {0x50}; const guint8 write_sr_enable_bp[] = {0x01, 0x8c, 0x00}; /* re-enable flash protection */ if (!fu_parade_lspcon_device_flash_transmit_command(self, write_sr_volatile, sizeof(write_sr_volatile), error)) return FALSE; if (!fu_parade_lspcon_device_flash_transmit_command(self, write_sr_enable_bp, sizeof(write_sr_enable_bp), error)) return FALSE; fu_progress_step_done(progress); /* reassert /WP to flash */ return fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_WR_PROTECT, 0, error); } static gboolean fu_parade_lspcon_device_flash_write(FuParadeLspconDevice *self, guint32 base_address, GInputStream *stream, FuProgress *progress, GError **error) { FuI2cDevice *i2c_device = FU_I2C_DEVICE(self); const guint8 unlock_writes[] = {0xaa, 0x55, 0x50, 0x41, 0x52, 0x44}; g_autoptr(FuChunkArray) chunks = NULL; /* unlock map writes by writing the magic sequence */ for (gsize i = 0; i < sizeof(unlock_writes); i++) { if (!fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_MAP_WRITE, unlock_writes[i], error)) return FALSE; } /* reset clt2SPI, required before write */ if (self->kind == FU_PARADE_LSPCON_DEVICE_KIND_PS175) { if (!fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_SPICFG3, 0x20, error)) return FALSE; fu_device_sleep(FU_DEVICE(self), 100); /* ms */ if (!fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_SPICFG3, 0, error)) return FALSE; } chunks = fu_chunk_array_new_from_stream(stream, base_address, FU_CHUNK_PAGESZ_NONE, 256, error); if (chunks == NULL) return FALSE; fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (gsize i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chunk = NULL; guint32 address; guint32 chunk_size; guint8 write_data[257] = {0x0}; g_autoptr(FuParadeLspconI2cAddressGuard) guard = NULL; /* prepare chunk */ chunk = fu_chunk_array_index(chunks, i, error); if (chunk == NULL) return FALSE; /* map target address range in page 7 */ address = fu_chunk_get_address(chunk); if (!fu_parade_lspcon_device_map_page(self, address, error)) return FALSE; /* write data to page 7 memory window */ guard = fu_parade_lspcon_device_i2c_address_guard_new(self, FU_PARADE_LSPCON_I2C_ADDR_PAGE7, error); if (guard == NULL) return FALSE; /* page write is prefixed with an offset: * we always start from offset 0 */ chunk_size = fu_chunk_get_data_sz(chunk); if (!fu_memcpy_safe(write_data, sizeof(write_data), 1, fu_chunk_get_data(chunk), chunk_size, 0, chunk_size, error)) return FALSE; if (!fu_i2c_device_write(i2c_device, write_data, chunk_size + 1, error)) return FALSE; /* update progress */ fu_progress_step_done(progress); } /* re-lock map writes */ return fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_MAP_WRITE, 0, error); } static gboolean fu_parade_lspcon_device_flash_erase_block(FuParadeLspconDevice *self, guint32 base_address, guint32 size, GError **error) { const guint8 block_erase[] = {0xd8, base_address >> 16, base_address >> 8, base_address}; g_debug("flash erase block at %#x", base_address); if (!fu_parade_lspcon_device_flash_enable_write(self, error)) return FALSE; /* GPIO7=IROMW, GPIO3=interrupt/gpio, SPIROM writable */ if (self->kind == FU_PARADE_LSPCON_DEVICE_KIND_PS185) { if (!fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_IOCFG1, 0x88, error)) return FALSE; if (!fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_SPICFG3, 0x08, error)) return FALSE; if (!fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_I_ROM_CTRL, 0x03, error)) return FALSE; if (!fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_ROM_WP_CFG, 0x07, error)) return FALSE; } if (!fu_parade_lspcon_device_flash_transmit_command(self, block_erase, sizeof(block_erase), error)) return FALSE; /* wait for command completion */ if (!fu_parade_lspcon_device_poll_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_SPI_STATUS, SPI_STATUS_SE_MASK, 0, error)) return FALSE; /* wait for flash to complete erase */ if (!fu_parade_lspcon_device_flash_wait_ready(self, error)) return FALSE; /* GPIO7=GPIO, SPIROM non-writable */ if (self->kind == FU_PARADE_LSPCON_DEVICE_KIND_PS185) { if (!fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_IOCFG1, 0x00, error)) return FALSE; if (!fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_SPICFG3, 0x00, error)) return FALSE; if (!fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_I_ROM_CTRL, 0x06, error)) return FALSE; if (!fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_ROM_WP_CFG, 0x00, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_parade_lspcon_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(device); gsize blocksz = fu_device_get_firmware_size_max(device); /* if the boot partition is active we could flash either, but prefer * the first */ const guint8 target_partition = self->active_partition == 1 ? 2 : 1; const guint32 target_address = target_partition << 16; const guint8 flag_data[] = {0x55, 0xaa, target_partition, 1 - target_partition}; g_autofree guint8 *readback_buf = NULL; g_autoptr(GByteArray) buf = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(GInputStream) flag_data_stream = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 5, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 70, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 25, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 3, "device-write-boot"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 2, "device-verify-boot"); /* erase entire target partition (one flash block) */ if (!fu_parade_lspcon_device_flash_erase_block(self, target_address, blocksz, error)) { g_prefix_error(error, "failed to erase flash partition %d: ", target_partition); return FALSE; } fu_progress_step_done(progress); /* write image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; if (!fu_parade_lspcon_device_flash_write(self, target_address, stream, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to write firmware to partition %d: ", target_partition); return FALSE; } fu_progress_step_done(progress); /* read back written image to verify */ readback_buf = g_malloc0(blocksz); if (!fu_parade_lspcon_device_flash_read(self, target_address, readback_buf, blocksz, fu_progress_get_child(progress), error)) return FALSE; buf = fu_input_stream_read_byte_array(stream, target_address, blocksz, NULL, error); if (buf == NULL) return FALSE; if (!fu_memcmp_safe(buf->data, buf->len, 0x0, readback_buf, blocksz, 0x0, blocksz, error)) { g_prefix_error(error, "flash contents do not match: "); return FALSE; } fu_progress_step_done(progress); /* erase flag partition */ if (!fu_parade_lspcon_device_flash_erase_block(self, 0, blocksz, error)) return FALSE; /* write flag indicating device should boot the target partition */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); flag_data_stream = G_INPUT_STREAM(g_memory_input_stream_new_from_data(flag_data, sizeof(flag_data), NULL)); if (!fu_parade_lspcon_device_flash_write(self, 0, flag_data_stream, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* verify flag partition */ if (!fu_parade_lspcon_device_flash_read(self, 0, readback_buf, sizeof(flag_data), fu_progress_get_child(progress), error)) return FALSE; if (!fu_memcmp_safe(flag_data, sizeof(flag_data), 0x0, readback_buf, blocksz, 0x0, MIN(sizeof(flag_data), blocksz), error)) { g_prefix_error(error, "flag partition contents do not match: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_parade_lspcon_device_set_dev_id(FuParadeLspconDevice *self, const gchar *dev_id, GError **error) { g_autofree gchar *model = g_strndup(dev_id, 3); g_autofree gchar *name = g_strdup_printf("PS%s", model); g_autofree gchar *kind = g_strdup_printf("ps%s", model); /* fallback name */ if (fu_device_get_name(FU_DEVICE(self)) == NULL) fu_device_set_name(FU_DEVICE(self), name); /* detect kind */ self->kind = fu_parade_lspcon_device_kind_from_string(kind); if (self->kind == FU_PARADE_LSPCON_DEVICE_KIND_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "%s not supported", kind); return FALSE; } /* success */ return TRUE; } static gboolean fu_parade_lspcon_device_set_ieee_oui(FuParadeLspconDevice *self, guint32 ieee_oui, GError **error) { g_autofree gchar *vid = g_strdup_printf("%06X", ieee_oui); fu_device_build_vendor_id(FU_DEVICE(self), "OUI", vid); fu_device_add_instance_str(FU_DEVICE(self), "VID", vid); return fu_device_build_instance_id_full(FU_DEVICE(self), FU_DEVICE_INSTANCE_FLAG_QUIRKS, error, "OUI", "VID", NULL); } static gboolean fu_parade_lspcon_device_ensure_dpcd(FuParadeLspconDevice *self, GError **error) { guint8 buf[FU_STRUCT_DPAUX_DPCD_SIZE] = {0x0}; g_autofree gchar *dev_id = NULL; g_autoptr(FuParadeLspconI2cAddressGuard) guard = NULL; g_autoptr(FuStructDpauxDpcd) st = NULL; guard = fu_parade_lspcon_device_i2c_address_guard_new(self, FU_PARADE_LSPCON_I2C_ADDR_PAGE1, error); if (guard == NULL) return FALSE; for (guint i = 0; i < sizeof(buf); i++) { if (!fu_parade_lspcon_device_read_register(self, FU_PARADE_LSPCON_PAGE1_ADDR_DPCD + i, buf + i, error)) return FALSE; } st = fu_struct_dpaux_dpcd_parse(buf, sizeof(buf), 0x0, error); if (st == NULL) return FALSE; dev_id = fu_struct_dpaux_dpcd_get_dev_id(st); if (dev_id != NULL) { if (!fu_parade_lspcon_device_set_dev_id(self, dev_id, error)) return FALSE; } if (fu_struct_dpaux_dpcd_get_ieee_oui(st) != 0x0) { if (!fu_parade_lspcon_device_set_ieee_oui(self, fu_struct_dpaux_dpcd_get_ieee_oui(st), error)) return FALSE; } fu_device_set_version_raw(FU_DEVICE(self), fu_struct_dpaux_dpcd_get_fw_ver(st)); /* success */ return TRUE; } static gboolean fu_parade_lspcon_device_ensure_active_flash_partition_internal(FuParadeLspconDevice *self, GError **error) { guint8 data = 0x0; g_autoptr(FuParadeLspconI2cAddressGuard) guard = NULL; /* read currently-running flash partition number */ guard = fu_parade_lspcon_device_i2c_address_guard_new(self, FU_PARADE_LSPCON_I2C_ADDR_PAGE5, error); if (guard == NULL) return FALSE; if (!fu_parade_lspcon_device_read_register(self, FU_PARADE_LSPCON_PAGE5_ADDR_ACTIVE_PARTITION, &data, error)) return FALSE; self->active_partition = data; return TRUE; } static gboolean fu_parade_lspcon_device_ensure_active_flash_partition(FuParadeLspconDevice *self, GError **error) { guint8 page_offsets[] = {0x0, 0x40}; /* try to find the correct page offset */ for (guint i = 0; i < G_N_ELEMENTS(page_offsets); i++) { g_autoptr(GError) error_local = NULL; self->page_offset = page_offsets[i]; if (!fu_parade_lspcon_device_ensure_active_flash_partition_internal(self, &error_local)) { g_debug("ignoring: %s", error_local->message); } else { g_debug("got flash partition with page offset 0x%x", self->page_offset); return TRUE; } } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "page offset could not be auto-detected"); return FALSE; } static gboolean fu_parade_lspcon_device_setup(FuDevice *device, GError **error) { FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(device); FuContext *context = fu_device_get_context(device); /* try to also find the correct page offset */ if (!fu_parade_lspcon_device_ensure_active_flash_partition(self, error)) return FALSE; /* verify active partition */ if (self->active_partition < 1 || self->active_partition > 3) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unexpected active flash partition: %d", self->active_partition); return FALSE; } /* DPCD is mirrored into PAGE1 */ if (!fu_parade_lspcon_device_ensure_dpcd(self, error)) return FALSE; /* unique enough for a firmware match */ fu_device_add_instance_str(device, "FAMILY", fu_context_get_hwid_value(context, FU_HWIDS_KEY_FAMILY)); return fu_device_build_instance_id(device, error, "I2C", "NAME", "FAMILY", NULL); } static gboolean fu_parade_lspcon_device_set_mpu_running(FuParadeLspconDevice *self, gboolean running, GError **error) { /* reset */ if (!fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_MPU, 0xc0, error)) return FALSE; /* release reset, set MPU active or not */ return fu_parade_lspcon_device_write_register(self, FU_PARADE_LSPCON_PAGE2_ADDR_MPU, running ? 0 : 0x40, error); } static gboolean fu_parade_lspcon_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(device); return fu_parade_lspcon_device_set_mpu_running(self, FALSE, error); } static gboolean fu_parade_lspcon_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(device); return fu_parade_lspcon_device_set_mpu_running(self, TRUE, error); } static GBytes * fu_parade_lspcon_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuParadeLspconDevice *self = FU_PARADE_LSPCON_DEVICE(device); gsize blocksz = fu_device_get_firmware_size_max(device); g_autofree guint8 *buf = g_malloc0(blocksz); if (!fu_parade_lspcon_device_flash_read(self, self->active_partition * blocksz, buf, blocksz, progress, error)) return NULL; return g_bytes_new_take(g_steal_pointer(&buf), blocksz); } static void fu_parade_lspcon_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static gchar * fu_parade_lspcon_device_convert_version(FuDevice *device, guint64 version_raw) { return g_strdup_printf("%u.%u", (guint)(version_raw >> 16) & 0xFF, (guint)(version_raw >> 8) & 0xFF); } static void fu_parade_lspcon_device_init(FuParadeLspconDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "com.paradetech.ps176"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_VIDEO_DISPLAY); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_NO_GENERIC_GUIDS); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); fu_device_set_firmware_size(FU_DEVICE(self), 0x10000); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PAIR); } static void fu_parade_lspcon_device_class_init(FuParadeLspconDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_parade_lspcon_device_to_string; device_class->setup = fu_parade_lspcon_device_setup; device_class->open = fu_parade_lspcon_device_open; device_class->reload = fu_parade_lspcon_device_setup; device_class->detach = fu_parade_lspcon_device_detach; device_class->prepare = fu_parade_lspcon_device_prepare; device_class->write_firmware = fu_parade_lspcon_device_write_firmware; device_class->cleanup = fu_parade_lspcon_device_cleanup; device_class->attach = fu_parade_lspcon_device_attach; device_class->dump_firmware = fu_parade_lspcon_device_dump_firmware; device_class->set_progress = fu_parade_lspcon_device_set_progress; device_class->convert_version = fu_parade_lspcon_device_convert_version; } fwupd-2.0.10/plugins/parade-lspcon/fu-parade-lspcon-device.h000066400000000000000000000005541501337203100236750ustar00rootroot00000000000000/* * Copyright 2021 Peter Marheine * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_PARADE_LSPCON_DEVICE (fu_parade_lspcon_device_get_type()) G_DECLARE_FINAL_TYPE(FuParadeLspconDevice, fu_parade_lspcon_device, FU, PARADE_LSPCON_DEVICE, FuI2cDevice) fwupd-2.0.10/plugins/parade-lspcon/fu-parade-lspcon-plugin.c000066400000000000000000000015171501337203100237270ustar00rootroot00000000000000/* * Copyright 2021 Peter Marheine * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-parade-lspcon-device.h" #include "fu-parade-lspcon-plugin.h" struct _FuParadeLspconPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuParadeLspconPlugin, fu_parade_lspcon_plugin, FU_TYPE_PLUGIN) static void fu_parade_lspcon_plugin_init(FuParadeLspconPlugin *self) { } static void fu_parade_lspcon_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_udev_subsystem(plugin, "i2c"); fu_plugin_add_device_gtype(plugin, FU_TYPE_PARADE_LSPCON_DEVICE); } static void fu_parade_lspcon_plugin_class_init(FuParadeLspconPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_parade_lspcon_plugin_constructed; } fwupd-2.0.10/plugins/parade-lspcon/fu-parade-lspcon-plugin.h000066400000000000000000000004341501337203100237310ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuParadeLspconPlugin, fu_parade_lspcon_plugin, FU, PARADE_LSPCON_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/parade-lspcon/fu-parade-lspcon.rs000066400000000000000000000025611501337203100226350ustar00rootroot00000000000000// Copyright 2024 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(FromString, ToString)] enum FuParadeLspconDeviceKind { Unknown, Ps175, Ps185, } enum FuParadeLspconPage1Addr { Dpcd = 0xC0, // page1, DPCD } enum FuParadeLspconPage2Addr { Spicfg3 = 0x82, FlashAddrLo = 0x8E, // 24-bit flash address that gets mapped into page 7 FlashAddrHi = 0x8F, // writing = 0x01,0x42 will map the 256 bytes from 0x420100 into page 7 WrFifo = 0x90, // 16-deep SPI write and read buffer FIFOs RdFifo = 0x91, SpiLen = 0x92, // low nibble is write operation length, high nibble is read commands SpiCtl = 0x93, SpiStatus = 0x9E, // 1=operation-begins, 2=cmd-sent, 0=complete Iocfg1 = 0xA6, IRomCtrl = 0xB0, WrProtect = 0xB3, // permit flash write operations Mpu = 0xBC, // MPU control register MapWrite = 0xDA, // write a magic to enable writes to page 7 RomWpCfg = 0xF6, } enum FuParadeLspconPage5Addr { ActivePartition = 0x0E, } // device registers are split into pages, where each page has its own I2C address enum FuParadeLspconI2cAddr { Page1 = 0x09, Page2 = 0x0A, Page3 = 0x0B, Page4 = 0x0C, Page5 = 0x0D, Page6 = 0x0E, Page7 = 0x0F, } fwupd-2.0.10/plugins/parade-lspcon/meson.build000066400000000000000000000010671501337203100212710ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginParadeLspcon"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('parade-lspcon.quirk') plugin_builtins += static_library('fu_plugin_parade_lspcon', rustgen.process( 'fu-parade-lspcon.rs', ), sources: [ 'fu-parade-lspcon-device.c', 'fu-parade-lspcon-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: [ cargs, '-DLOCALSTATEDIR="' + localstatedir + '"', ], dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/parade-lspcon/parade-lspcon.quirk000066400000000000000000000000751501337203100227320ustar00rootroot00000000000000# Parade PS175 [I2C\NAME_1AF80175:00] Plugin = parade_lspcon fwupd-2.0.10/plugins/parade-usbhub/000077500000000000000000000000001501337203100171155ustar00rootroot00000000000000fwupd-2.0.10/plugins/parade-usbhub/README.md000066400000000000000000000033741501337203100204030ustar00rootroot00000000000000--- title: Plugin: Parade USB Hub --- ## Introduction Parade USB hubs such as PS5512 have a built-in SPI-Master engine for programming external SPI ROM flash. The USB2.0 hub device can do control transfers to control an internal mailbox. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a packed binary file format. This plugin supports the following protocol ID: * `com.paradetech.usbhub` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_273F&PID_1001` ## Update Behavior * Load 384KB SPI ROM data buffer * Specify target SPI ROM bank, ex: HUB_FW#1 or HUB_FW#2 * Set UFP Disconnect Flag register to notify firmware that we are doing firmware update * Acquire SPI Master * Unprotect SPI ROM bank * Erase SPI ROM bank * Update SPI ROM bank * Protect SPI ROM bank * Verify checksum for the just updated SPI ROM bank It takes around 90 seconds to update the FW#1 64KB firmware. FW#2 is the factory default with a known good firmware. Please do a full power cycle to make new firmware take effect. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x273F` ## Quirk Use This plugin uses the following plugin-specific quirks: ### ParadeUsbhubChip Set the chip used, e.g. `ps188` or `ps5512`, defaulting to the latter. Since: 2.0.2 ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `2.0.0`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Hub Lin: @hublin2024 * Jimmy Tu: @jimmytu5167 * Andy Chu: @andychu5168 fwupd-2.0.10/plugins/parade-usbhub/fu-parade-usbhub-common.h000066400000000000000000000003061501337203100237050ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_PARADE_USBHUB_SPI_ROM_SIZE 0x60000 fwupd-2.0.10/plugins/parade-usbhub/fu-parade-usbhub-device.c000066400000000000000000001072251501337203100236570ustar00rootroot00000000000000/* * Copyright 2023 Parade Technology, Ltd * Copyright 2024 Richard Hughes * * Based on PS8830_FwUpd_SampleCode.cpp, v1.0.1.0 * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-parade-usbhub-common.h" #include "fu-parade-usbhub-device.h" #include "fu-parade-usbhub-firmware.h" #include "fu-parade-usbhub-struct.h" struct _FuParadeUsbhubDevice { FuUsbDevice parent_instance; FuCfiDevice *cfi_device; FuParadeUsbhubChip chip; guint32 spi_address; }; G_DEFINE_TYPE(FuParadeUsbhubDevice, fu_parade_usbhub_device, FU_TYPE_USB_DEVICE) #define FU_PARADE_USBHUB_DEVICE_TIMEOUT 1500 /* ms */ #define FU_PARADE_USBHUB_SPI_ROM_BANK_SIZE 0x10000 #define FU_PARADE_USBHUB_SPI_ROM_ADDRESS_BANK4_HUB_FIRMWARE_1 0x40000 #define FU_PARADE_USBHUB_SPI_ROM_ADDRESS_BANK5_HUB_FIRMWARE_2 0x50000 #define FU_PARADE_USBHUB_SPI_ROM_ERASE_SIZE 4096u #define FU_PARADE_USBHUB_SPI_ROM_CHECKSUM_BUFFER_SIZE 0xFFFF #define FU_PARADE_USBHUB_DMA_SRAM_ADDRESS 0xF800 #define FU_PARADE_USBHUB_DMA_SRAM_SIZE 1024 #define FU_PARADE_USBHUB_DEVICE_SPI_BURST_DBI_MAX 4 #define FU_PARADE_USBHUB_DEVICE_MMIO_BURST_WRITE_MAX 16 #define FU_PARADE_USBHUB_DEVICE_SPI_RETRY_COUNT 100 #define FU_PARADE_USBHUB_DEVICE_SPI_RETRY_DELAY 50 /* ms */ static void fu_parade_usbhub_device_to_string(FuDevice *device, guint idt, GString *str) { FuParadeUsbhubDevice *self = FU_PARADE_USBHUB_DEVICE(device); fwupd_codec_string_append(str, idt, "Chip", fu_parade_usbhub_chip_to_string(self->chip)); fwupd_codec_string_append_hex(str, idt, "SpiAddress", self->spi_address); } static gboolean fu_parade_usbhub_device_mmio_read_u8(FuParadeUsbhubDevice *self, guint16 address, guint8 *data, GError **error) { return fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, FU_PARADE_USBHUB_DEVICE_REQUEST_READ, 0x0, /* always 0 */ address, data, sizeof(*data), NULL, FU_PARADE_USBHUB_DEVICE_TIMEOUT, NULL, error); } static gboolean fu_parade_usbhub_device_mmio_read(FuParadeUsbhubDevice *self, guint16 address, guint8 *buf, gsize bufsz, GError **error) { for (gsize i = 0; i < bufsz; i++) { if (!fu_parade_usbhub_device_mmio_read_u8(self, address + i, buf + i, error)) return FALSE; } return TRUE; } static gboolean fu_parade_usbhub_device_mmio_write_raw(FuParadeUsbhubDevice *self, guint16 address, guint8 *buf, gsize bufsz, GError **error) { return fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, FU_PARADE_USBHUB_DEVICE_REQUEST_WRITE, 0x0, /* always 0 */ address, buf, bufsz, NULL, FU_PARADE_USBHUB_DEVICE_TIMEOUT, NULL, error); } static gboolean fu_parade_usbhub_device_mmio_write(FuParadeUsbhubDevice *self, guint16 address, guint8 *buf, gsize bufsz, GError **error) { for (gsize i = 0; i < bufsz; i++) { if (!fu_parade_usbhub_device_mmio_write_raw(self, address + i, buf + i, sizeof(guint8), error)) return FALSE; } return TRUE; } static gboolean fu_parade_usbhub_device_mmio_write_u8(FuParadeUsbhubDevice *self, guint16 address, guint8 data, GError **error) { return fu_parade_usbhub_device_mmio_write(self, address, &data, sizeof(data), error); } static gboolean fu_parade_usbhub_device_mmio_write_u16(FuParadeUsbhubDevice *self, guint16 address, guint16 data, GError **error) { guint8 buf[2] = {0}; fu_memwrite_uint16(buf, data, G_LITTLE_ENDIAN); return fu_parade_usbhub_device_mmio_write(self, address, buf, sizeof(buf), error); } static gboolean fu_parade_usbhub_device_mmio_write_u24(FuParadeUsbhubDevice *self, guint16 address, guint32 data, GError **error) { guint8 buf[3] = {0}; fu_memwrite_uint24(buf, data, G_LITTLE_ENDIAN); return fu_parade_usbhub_device_mmio_write(self, address, buf, sizeof(buf), error); } static gboolean fu_parade_usbhub_device_mmio_set_bit(FuParadeUsbhubDevice *self, guint16 address, gsize bit_offset, GError **error) { guint8 val = 0; if (!fu_parade_usbhub_device_mmio_read_u8(self, address, &val, error)) return FALSE; FU_BIT_SET(val, bit_offset); return fu_parade_usbhub_device_mmio_write_u8(self, address, val, error); } static gboolean fu_parade_usbhub_device_mmio_clear_bit(FuParadeUsbhubDevice *self, guint16 address, gsize bit_offset, GError **error) { guint8 val = 0; if (!fu_parade_usbhub_device_mmio_read_u8(self, address, &val, error)) return FALSE; FU_BIT_CLEAR(val, bit_offset); return fu_parade_usbhub_device_mmio_write_u8(self, address, val, error); } static gboolean fu_parade_usbhub_device_spi_rom_wait_done_cb(FuDevice *device, gpointer user_data, GError **error) { FuParadeUsbhubDevice *self = FU_PARADE_USBHUB_DEVICE(device); guint8 val = 0; if (!fu_parade_usbhub_device_mmio_read_u8(self, FU_PARADE_USBHUB_DEVICE_ADDR_STATUS, &val, error)) return FALSE; if ((val & FU_PARADE_USBHUB_DEVICE_STATUS_FLAG_SPI_DONE) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not done"); return FALSE; } return TRUE; } static gboolean fu_parade_usbhub_device_spi_rom_wait_done(FuParadeUsbhubDevice *self, GError **error) { return fu_device_retry_full(FU_DEVICE(self), fu_parade_usbhub_device_spi_rom_wait_done_cb, FU_PARADE_USBHUB_DEVICE_SPI_RETRY_COUNT, FU_PARADE_USBHUB_DEVICE_SPI_RETRY_DELAY, NULL, error); } static gboolean fu_parade_usbhub_device_spi_read_dma_dbi(FuParadeUsbhubDevice *self, guint8 spi_command, guint32 spi_address, gsize spi_command_size, guint8 *buf, gsize bufsz, GError **error) { if (!fu_parade_usbhub_device_mmio_write_u8(self, FU_PARADE_USBHUB_DEVICE_ADDR_DMA_SIZE, spi_command_size, error)) return FALSE; if (!fu_parade_usbhub_device_mmio_write_u8(self, FU_PARADE_USBHUB_DEVICE_ADDR_READ_SIZE, bufsz, error)) return FALSE; /* SPI command */ if (spi_command_size > 0) { guint8 buf_spi[4] = {0}; g_return_val_if_fail(spi_command_size <= sizeof(buf_spi), FALSE); /* write data to SPI buffer */ buf_spi[0] = spi_command; fu_memwrite_uint24(buf_spi + 1, spi_address, G_BIG_ENDIAN); if (!fu_parade_usbhub_device_mmio_write(self, FU_PARADE_USBHUB_DEVICE_ADDR_DATA, buf_spi, spi_command_size, error)) return FALSE; } /* trigger read */ if (!fu_parade_usbhub_device_mmio_write_u8(self, FU_PARADE_USBHUB_DEVICE_ADDR_STATUS, FU_PARADE_USBHUB_DEVICE_STATUS_FLAG_TRIGGER_DBI, error)) return FALSE; /* polling status bit */ if (!fu_parade_usbhub_device_spi_rom_wait_done(self, error)) return FALSE; /* read data buffer */ return fu_parade_usbhub_device_mmio_read(self, FU_PARADE_USBHUB_DEVICE_ADDR_DATA, buf, bufsz, error); } static gboolean fu_parade_usbhub_device_spi_write_dma_dbi(FuParadeUsbhubDevice *self, guint8 spi_command, guint32 spi_address, gsize spi_command_size, guint8 *buf, gsize bufsz, GError **error) { if (spi_command_size > 0) { guint8 buf_spi[4] = {0}; g_return_val_if_fail(spi_command_size <= sizeof(buf_spi), FALSE); if (!fu_parade_usbhub_device_mmio_write_u8(self, FU_PARADE_USBHUB_DEVICE_ADDR_DMA_SIZE, spi_command_size, error)) return FALSE; if (!fu_parade_usbhub_device_mmio_write_u8(self, FU_PARADE_USBHUB_DEVICE_ADDR_READ_SIZE, 0, error)) return FALSE; /* write data */ buf_spi[0] = spi_command; fu_memwrite_uint24(buf_spi + 1, spi_address, G_BIG_ENDIAN); if (!fu_parade_usbhub_device_mmio_write(self, FU_PARADE_USBHUB_DEVICE_ADDR_DATA, buf_spi, spi_command_size, error)) return FALSE; /* trigger write */ if (!fu_parade_usbhub_device_mmio_write_u8( self, FU_PARADE_USBHUB_DEVICE_ADDR_STATUS, FU_PARADE_USBHUB_DEVICE_STATUS_FLAG_TRIGGER_DBI | FU_PARADE_USBHUB_DEVICE_STATUS_FLAG_WRITE, error)) return FALSE; if (!fu_parade_usbhub_device_spi_rom_wait_done(self, error)) return FALSE; } if (bufsz > 0) { if (!fu_parade_usbhub_device_mmio_write_u8(self, FU_PARADE_USBHUB_DEVICE_ADDR_DMA_SIZE, bufsz, error)) return FALSE; if (!fu_parade_usbhub_device_mmio_write_u8(self, FU_PARADE_USBHUB_DEVICE_ADDR_READ_SIZE, 0, error)) return FALSE; /* write data and trigger */ if (!fu_parade_usbhub_device_mmio_write(self, FU_PARADE_USBHUB_DEVICE_ADDR_DATA, buf, bufsz, error)) return FALSE; if (!fu_parade_usbhub_device_mmio_write_u8( self, FU_PARADE_USBHUB_DEVICE_ADDR_STATUS, FU_PARADE_USBHUB_DEVICE_STATUS_FLAG_TRIGGER_DBI | FU_PARADE_USBHUB_DEVICE_STATUS_FLAG_WRITE, error)) return FALSE; if (!fu_parade_usbhub_device_spi_rom_wait_done(self, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_parade_usbhub_device_spi_data_read(FuParadeUsbhubDevice *self, guint8 spi_command, guint32 spi_address, gsize spi_command_size, guint8 *buf, gsize bufsz, GError **error) { g_autoptr(GPtrArray) chunks = NULL; /* no data */ if (bufsz == 0) { return fu_parade_usbhub_device_spi_read_dma_dbi(self, spi_command, spi_address, spi_command_size, NULL, 0, error); } /* blocks of data */ chunks = fu_chunk_array_mutable_new(buf, bufsz, spi_address, 0x0, FU_PARADE_USBHUB_DEVICE_SPI_BURST_DBI_MAX); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_parade_usbhub_device_spi_read_dma_dbi(self, spi_command, fu_chunk_get_address(chk), spi_command_size, fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_parade_usbhub_device_spi_data_write(FuParadeUsbhubDevice *self, guint8 spi_command, guint32 spi_address, gsize spi_command_size, guint8 *buf, gsize bufsz, GError **error) { g_autoptr(GPtrArray) chunks = NULL; /* no data */ if (bufsz == 0) { return fu_parade_usbhub_device_spi_write_dma_dbi(self, spi_command, spi_address, spi_command_size, NULL, 0, error); } /* blocks of data */ chunks = fu_chunk_array_mutable_new(buf, bufsz, spi_address, 0x0, FU_PARADE_USBHUB_DEVICE_SPI_BURST_DBI_MAX); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_parade_usbhub_device_spi_write_dma_dbi(self, spi_command, fu_chunk_get_address(chk), spi_command_size, fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_parade_usbhub_device_spi_data_write_ex(FuParadeUsbhubDevice *self, guint8 *buf, gsize bufsz, GError **error) { if (!fu_parade_usbhub_device_mmio_write_u8(self, FU_PARADE_USBHUB_DEVICE_ADDR_DMA_SIZE, bufsz, error)) return FALSE; if (!fu_parade_usbhub_device_mmio_write_u8(self, FU_PARADE_USBHUB_DEVICE_ADDR_READ_SIZE, 0, error)) return FALSE; /* write data and trigger */ if (!fu_parade_usbhub_device_mmio_write(self, FU_PARADE_USBHUB_DEVICE_ADDR_DATA, buf, bufsz, error)) return FALSE; if (!fu_parade_usbhub_device_mmio_write_u8(self, FU_PARADE_USBHUB_DEVICE_ADDR_STATUS, FU_PARADE_USBHUB_DEVICE_STATUS_FLAG_TRIGGER_DBI | FU_PARADE_USBHUB_DEVICE_STATUS_FLAG_WRITE, error)) return FALSE; return fu_parade_usbhub_device_spi_rom_wait_done(self, error); } static gboolean fu_parade_usbhub_device_spi_write_command(FuParadeUsbhubDevice *self, guint8 spi_command, guint32 spi_address, gsize spi_command_size, GError **error) { return fu_parade_usbhub_device_spi_data_write(self, spi_command, spi_address, spi_command_size, NULL, 0, error); } static gboolean fu_parade_usbhub_device_enable_spi_master(FuParadeUsbhubDevice *self, GError **error) { return fu_parade_usbhub_device_mmio_set_bit(self, FU_PARADE_USBHUB_DEVICE_ADDR_SPI_MASTER, 4, error); } static gboolean fu_parade_usbhub_device_disable_spi_master(FuParadeUsbhubDevice *self, GError **error) { return fu_parade_usbhub_device_mmio_clear_bit(self, FU_PARADE_USBHUB_DEVICE_ADDR_SPI_MASTER, 4, error); } static gboolean fu_parade_usbhub_device_spi_wait_status_cb(FuDevice *device, gpointer user_data, GError **error) { FuParadeUsbhubDevice *self = FU_PARADE_USBHUB_DEVICE(device); guint8 spi_cmd_read_status = 0; guint8 val = 0; if (!fu_cfi_device_get_cmd(self->cfi_device, FU_CFI_DEVICE_CMD_READ_STATUS, &spi_cmd_read_status, error)) return FALSE; if (!fu_parade_usbhub_device_spi_data_read(self, spi_cmd_read_status, 0, /* SPI addr */ 1, &val, sizeof(val), error)) return FALSE; if (val & 0b1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "status invalid"); return FALSE; } /* success */ return TRUE; } static gboolean fu_parade_usbhub_device_spi_wait_status(FuParadeUsbhubDevice *self, GError **error) { return fu_device_retry_full(FU_DEVICE(self), fu_parade_usbhub_device_spi_wait_status_cb, FU_PARADE_USBHUB_DEVICE_SPI_RETRY_COUNT, FU_PARADE_USBHUB_DEVICE_SPI_RETRY_DELAY, NULL, error); } static gboolean fu_parade_usbhub_device_acquire_spi_master(FuParadeUsbhubDevice *self, GError **error) { if (self->chip == FU_PARADE_USBHUB_CHIP_PS188) { return fu_parade_usbhub_device_mmio_set_bit( self, FU_PARADE_USBHUB_DEVICE_ADDR_SPI_MASTER_ACQUIRE2, 1, error); } return fu_parade_usbhub_device_mmio_set_bit(self, FU_PARADE_USBHUB_DEVICE_ADDR_SPI_MASTER_ACQUIRE, 7, error); } static gboolean fu_parade_usbhub_device_spi_rom_chip_unprotect(FuParadeUsbhubDevice *self, GError **error) { guint8 status = 0; guint8 status_new = 0; guint8 buf_spi[2]; guint8 spi_cmd_write_en = 0; guint8 spi_cmd_read_status = 0; guint8 spi_cmd_write_status = 0; /* read status */ if (!fu_cfi_device_get_cmd(self->cfi_device, FU_CFI_DEVICE_CMD_READ_STATUS, &spi_cmd_read_status, error)) return FALSE; if (!fu_parade_usbhub_device_spi_data_read(self, spi_cmd_read_status, 0, /* SPI addr */ 1, &status, sizeof(status), error)) return FALSE; if (FU_BIT_IS_CLEAR(status, 2) && FU_BIT_IS_CLEAR(status, 3) && FU_BIT_IS_CLEAR(status, 7)) return TRUE; FU_BIT_CLEAR(status, 2); /* BP0 */ FU_BIT_CLEAR(status, 3); /* BP1 */ FU_BIT_CLEAR(status, 7); /* SRWD */ /* write enable */ if (!fu_cfi_device_get_cmd(self->cfi_device, FU_CFI_DEVICE_CMD_WRITE_EN, &spi_cmd_write_en, error)) return FALSE; if (!fu_parade_usbhub_device_spi_write_command(self, spi_cmd_write_en, 0, 1, error)) return FALSE; /* write status */ if (!fu_cfi_device_get_cmd(self->cfi_device, FU_CFI_DEVICE_CMD_WRITE_STATUS, &spi_cmd_write_status, error)) return FALSE; buf_spi[0] = spi_cmd_write_status; buf_spi[1] = status; if (!fu_parade_usbhub_device_spi_data_write_ex(self, buf_spi, sizeof(buf_spi), error)) return FALSE; if (!fu_parade_usbhub_device_spi_wait_status(self, error)) return FALSE; /* check status */ if (!fu_parade_usbhub_device_spi_data_read(self, spi_cmd_read_status, 0, /* SPI addr */ 1, &status_new, sizeof(status_new), error)) return FALSE; if (status_new != status) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "status was 0x%x, expected 0x%x", status_new, status); return FALSE; } /* success */ return TRUE; } static gboolean fu_parade_usbhub_device_spi_rom_erase_sector(FuParadeUsbhubDevice *self, guint32 spi_address, GError **error) { guint8 spi_cmd_write_en = 0; guint8 spi_cmd_sector_erase = 0; /* has to be aligned */ if (spi_address % FU_PARADE_USBHUB_SPI_ROM_ERASE_SIZE != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "SPI address 0x%x not aligned to 0x%x", spi_address, FU_PARADE_USBHUB_SPI_ROM_ERASE_SIZE); return FALSE; } /* write enable */ if (!fu_cfi_device_get_cmd(self->cfi_device, FU_CFI_DEVICE_CMD_WRITE_EN, &spi_cmd_write_en, error)) return FALSE; if (!fu_parade_usbhub_device_spi_write_command(self, spi_cmd_write_en, 0, 1, error)) return FALSE; /* sector erase */ if (!fu_cfi_device_get_cmd(self->cfi_device, FU_CFI_DEVICE_CMD_SECTOR_ERASE, &spi_cmd_sector_erase, error)) return FALSE; if (!fu_parade_usbhub_device_spi_write_command(self, spi_cmd_sector_erase, spi_address, 4, error)) return FALSE; /* check status */ return fu_parade_usbhub_device_spi_wait_status(self, error); } static gboolean fu_parade_usbhub_device_spi_rom_erase(FuParadeUsbhubDevice *self, gsize bufsz, FuProgress *progress, GError **error) { g_autoptr(GPtrArray) chunks = NULL; chunks = fu_chunk_array_new(NULL, bufsz, self->spi_address, 0, FU_PARADE_USBHUB_SPI_ROM_ERASE_SIZE); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_parade_usbhub_device_spi_rom_erase_sector(self, fu_chunk_get_address(chk), error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_parade_usbhub_device_sram_set_page(FuParadeUsbhubDevice *self, guint8 index_of_sram_page, GError **error) { guint8 val = 0; if (!fu_parade_usbhub_device_mmio_read_u8(self, FU_PARADE_USBHUB_DEVICE_ADDR_SRAM_PAGE, &val, error)) return FALSE; val &= 0xF0; val |= index_of_sram_page; return fu_parade_usbhub_device_mmio_write_u8(self, FU_PARADE_USBHUB_DEVICE_ADDR_SRAM_PAGE, val, error); } static gboolean fu_parade_usbhub_device_sram_page_write(FuParadeUsbhubDevice *self, guint32 sram_address, guint8 *buf, gsize bufsz, GError **error) { guint page = G_MAXUINT; g_autoptr(GPtrArray) chunks = NULL; chunks = fu_chunk_array_mutable_new(buf, bufsz, sram_address, 0x1000, FU_PARADE_USBHUB_DEVICE_MMIO_BURST_WRITE_MAX); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); /* setup sram address */ if (page != fu_chunk_get_page(chk)) { page = fu_chunk_get_page(chk); if (!fu_parade_usbhub_device_sram_set_page(self, page, error)) return FALSE; } /* write data to sram */ if (!fu_parade_usbhub_device_mmio_write_raw(self, 0x6000 + fu_chunk_get_address(chk), fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_parade_usbhub_device_spi_rom_write_trigger(FuParadeUsbhubDevice *self, guint16 sram_address, guint32 spi_address, gsize dma_size, GError **error) { if (!fu_parade_usbhub_device_mmio_write_u24(self, FU_PARADE_USBHUB_DEVICE_ADDR_SPI_ADDR, spi_address, error)) return FALSE; if (!fu_parade_usbhub_device_mmio_write_u16(self, FU_PARADE_USBHUB_DEVICE_ADDR_SRAM_ADDR, sram_address, error)) return FALSE; if (!fu_parade_usbhub_device_mmio_write_u16(self, FU_PARADE_USBHUB_DEVICE_ADDR_DMA_SIZE, dma_size, error)) return FALSE; return fu_parade_usbhub_device_mmio_write_u8( self, FU_PARADE_USBHUB_DEVICE_ADDR_STATUS, FU_PARADE_USBHUB_DEVICE_STATUS_FLAG_TRIGGER_SPI | FU_PARADE_USBHUB_DEVICE_STATUS_FLAG_WRITE, error); } static gboolean fu_parade_usbhub_device_spi_rom_write(FuParadeUsbhubDevice *self, GByteArray *blob, FuProgress *progress, GError **error) { g_autoptr(GPtrArray) chunks = NULL; /* disable DBI timeout */ if (!fu_parade_usbhub_device_mmio_write_u8(self, FU_PARADE_USBHUB_DEVICE_ADDR_DBI_TIMEOUT, 0, error)) return FALSE; /* write sram scratch buffer then trigger DMA */ chunks = fu_chunk_array_mutable_new(blob->data, blob->len, self->spi_address, 0, /* page */ FU_PARADE_USBHUB_DMA_SRAM_SIZE); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_parade_usbhub_device_sram_page_write(self, FU_PARADE_USBHUB_DMA_SRAM_ADDRESS, fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; if (!fu_parade_usbhub_device_spi_rom_write_trigger( self, FU_PARADE_USBHUB_DMA_SRAM_ADDRESS, fu_chunk_get_address(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; if (!fu_parade_usbhub_device_spi_rom_wait_done(self, error)) return FALSE; fu_progress_step_done(progress); } /* enable DBI timeout */ return fu_parade_usbhub_device_mmio_write_u8(self, FU_PARADE_USBHUB_DEVICE_ADDR_DBI_TIMEOUT, 0x0F, error); } static gboolean fu_parade_usbhub_device_spi_rom_chip_protect(FuParadeUsbhubDevice *self, GError **error) { guint8 status = 0; guint8 status_new = 0; guint8 buf_spi[2]; guint8 spi_cmd_write_en = 0; guint8 spi_cmd_read_status = 0; guint8 spi_cmd_write_status = 0; /* read status */ if (!fu_cfi_device_get_cmd(self->cfi_device, FU_CFI_DEVICE_CMD_READ_STATUS, &spi_cmd_read_status, error)) return FALSE; if (!fu_parade_usbhub_device_spi_data_read(self, spi_cmd_read_status, 0, 1, &status, sizeof(status), error)) return FALSE; if (FU_BIT_IS_SET(status, 2) && FU_BIT_IS_SET(status, 3) && FU_BIT_IS_CLEAR(status, 7)) return TRUE; FU_BIT_SET(status, 2); /* BP0 */ FU_BIT_SET(status, 3); /* BP1 */ FU_BIT_CLEAR(status, 7); /* SRWD */ /* write enable */ if (!fu_cfi_device_get_cmd(self->cfi_device, FU_CFI_DEVICE_CMD_WRITE_EN, &spi_cmd_write_en, error)) return FALSE; if (!fu_parade_usbhub_device_spi_write_command(self, spi_cmd_write_en, 0, 1, error)) return FALSE; /* write status */ if (!fu_cfi_device_get_cmd(self->cfi_device, FU_CFI_DEVICE_CMD_WRITE_STATUS, &spi_cmd_write_status, error)) return FALSE; buf_spi[0] = spi_cmd_write_status; buf_spi[1] = status; if (!fu_parade_usbhub_device_spi_data_write_ex(self, buf_spi, sizeof(buf_spi), error)) return FALSE; /* check status */ if (!fu_parade_usbhub_device_spi_wait_status(self, error)) return FALSE; if (!fu_parade_usbhub_device_spi_data_read(self, spi_cmd_read_status, 0, 1, &status_new, sizeof(status_new), error)) return FALSE; if (status_new != status) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "status was 0x%x, expected 0x%x", status_new, status); return FALSE; } /* success */ return TRUE; } static gboolean fu_parade_usbhub_device_calculate_checksum(FuParadeUsbhubDevice *self, guint32 spi_address, gsize size, GError **error) { if (!fu_parade_usbhub_device_mmio_write_u24(self, FU_PARADE_USBHUB_DEVICE_ADDR_SPI_ADDR, spi_address, error)) return FALSE; if (!fu_parade_usbhub_device_mmio_write_u16(self, FU_PARADE_USBHUB_DEVICE_ADDR_DMA_SIZE, size, error)) return FALSE; if (!fu_parade_usbhub_device_mmio_write_u8( self, FU_PARADE_USBHUB_DEVICE_ADDR_STATUS, FU_PARADE_USBHUB_DEVICE_STATUS_FLAG_CHECKSUM | FU_PARADE_USBHUB_DEVICE_STATUS_FLAG_TRIGGER_SPI, error)) return FALSE; return fu_parade_usbhub_device_spi_rom_wait_done(self, error); } static gboolean fu_parade_usbhub_device_spi_rom_checksum(FuParadeUsbhubDevice *self, gsize size, guint32 *checksum, GError **error) { guint8 buf_csum[4] = {0}; g_autoptr(GPtrArray) chunks = NULL; /* acquire and enable SPI master after internal reset */ if (!fu_parade_usbhub_device_acquire_spi_master(self, error)) return FALSE; if (!fu_parade_usbhub_device_enable_spi_master(self, error)) return FALSE; /* calculate checksum internally */ chunks = fu_chunk_array_new(NULL, size, self->spi_address, 0x0, FU_PARADE_USBHUB_SPI_ROM_CHECKSUM_BUFFER_SIZE); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_parade_usbhub_device_calculate_checksum(self, fu_chunk_get_address(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; } /* read calculated checksum */ if (!fu_parade_usbhub_device_mmio_read(self, FU_PARADE_USBHUB_DEVICE_ADDR_DATA, buf_csum, sizeof(buf_csum), error)) return FALSE; *checksum = fu_memread_uint32(buf_csum, G_LITTLE_ENDIAN); /* success */ return TRUE; } static gboolean fu_parade_usbhub_device_set_ufp_disconnect_flag(FuParadeUsbhubDevice *self, GError **error) { guint8 val = 0; if (!fu_parade_usbhub_device_mmio_read_u8(self, FU_PARADE_USBHUB_DEVICE_ADDR_UFP_DISCONNECT, &val, error)) return FALSE; val &= 0x0F; val |= 0xB0; return fu_parade_usbhub_device_mmio_write_u8(self, FU_PARADE_USBHUB_DEVICE_ADDR_UFP_DISCONNECT, val, error); } static gboolean fu_parade_usbhub_device_ensure_version(FuParadeUsbhubDevice *self, GError **error) { guint8 buf[4] = {0}; if (!fu_parade_usbhub_device_mmio_read_u8(self, FU_PARADE_USBHUB_DEVICE_ADDR_VERSION_A, buf + 0, error)) return FALSE; if (!fu_parade_usbhub_device_mmio_read_u8(self, FU_PARADE_USBHUB_DEVICE_ADDR_VERSION_B, buf + 1, error)) return FALSE; if (!fu_parade_usbhub_device_mmio_read_u8(self, FU_PARADE_USBHUB_DEVICE_ADDR_VERSION_C, buf + 2, error)) return FALSE; if (!fu_parade_usbhub_device_mmio_read_u8(self, FU_PARADE_USBHUB_DEVICE_ADDR_VERSION_D, buf + 3, error)) return FALSE; fu_device_set_version_raw(FU_DEVICE(self), fu_memread_uint32(buf, G_LITTLE_ENDIAN)); /* success */ return TRUE; } static gboolean fu_parade_usbhub_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuParadeUsbhubDevice *self = FU_PARADE_USBHUB_DEVICE(device); if (!fu_parade_usbhub_device_acquire_spi_master(self, error)) { g_prefix_error(error, "failed to acquire SPI master: "); return FALSE; } if (!fu_parade_usbhub_device_enable_spi_master(self, error)) { g_prefix_error(error, "failed to enable SPI master: "); return FALSE; } if (!fu_parade_usbhub_device_spi_rom_chip_unprotect(self, error)) { g_prefix_error(error, "failed to unprotect SPI ROM: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_parade_usbhub_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuParadeUsbhubDevice *self = FU_PARADE_USBHUB_DEVICE(device); if (!fu_parade_usbhub_device_spi_rom_chip_protect(self, error)) { g_prefix_error(error, "failed to protect SPI ROM: "); return FALSE; } if (!fu_parade_usbhub_device_disable_spi_master(self, error)) { g_prefix_error(error, "failed to disable SPI master: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_parade_usbhub_device_setup(FuDevice *device, GError **error) { FuParadeUsbhubDevice *self = FU_PARADE_USBHUB_DEVICE(device); /* HidDevice->setup */ if (!FU_DEVICE_CLASS(fu_parade_usbhub_device_parent_class)->setup(device, error)) return FALSE; /* get the version from the hardware */ if (!fu_parade_usbhub_device_ensure_version(self, error)) { g_prefix_error(error, "failed to get version: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_parade_usbhub_device_prepare(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuParadeUsbhubDevice *self = FU_PARADE_USBHUB_DEVICE(device); if (self->chip == FU_PARADE_USBHUB_CHIP_PS188) { /* prevent staying in high-power charging mode if UFP is disconnected */ if (!fu_parade_usbhub_device_set_ufp_disconnect_flag(self, error)) { g_prefix_error(error, "failed to set UFP disconnect flag: "); return FALSE; } } return TRUE; } static gboolean fu_parade_usbhub_device_cleanup(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { g_autoptr(FwupdRequest) request = fwupd_request_new(); fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_IMMEDIATE); fwupd_request_set_id(request, FWUPD_REQUEST_ID_REMOVE_REPLUG); fwupd_request_add_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); if (!fu_device_emit_request(device, request, progress, error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static FuFirmware * fu_parade_usbhub_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { g_autoptr(FuFirmware) firmware = fu_parade_usbhub_firmware_new(); if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; return g_steal_pointer(&firmware); } static gboolean fu_parade_usbhub_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuParadeUsbhubDevice *self = FU_PARADE_USBHUB_DEVICE(device); g_autoptr(GInputStream) stream = NULL; g_autoptr(GByteArray) blob = NULL; guint32 checksum; guint32 checksum_new; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 33, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 66, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 1, NULL); /* get default image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* get bank 4 slice */ blob = fu_input_stream_read_byte_array(stream, self->spi_address, FU_PARADE_USBHUB_SPI_ROM_BANK_SIZE, NULL, error); if (blob == NULL) return FALSE; /* SPI ROM update */ if (!fu_parade_usbhub_device_spi_rom_erase(self, blob->len, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); if (!fu_parade_usbhub_device_spi_rom_write(self, blob, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* compare checksum */ if (!fu_parade_usbhub_device_spi_rom_checksum(self, blob->len, &checksum_new, error)) { g_prefix_error(error, "failed to get ROM checksum: "); return FALSE; } checksum = fu_crc32(FU_CRC_KIND_B32_MPEG2, blob->data, blob->len); if (checksum != checksum_new) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "checksum was 0x%x, expected 0x%x", checksum_new, checksum); return FALSE; } fu_progress_step_done(progress); /* success! */ return TRUE; } static void fu_parade_usbhub_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 96, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static gchar * fu_parade_usbhub_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_device_get_version_format(device)); } static gboolean fu_parade_usbhub_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuParadeUsbhubDevice *self = FU_PARADE_USBHUB_DEVICE(device); if (g_strcmp0(key, "ParadeUsbhubChip") == 0) { self->chip = fu_parade_usbhub_chip_from_string(value); if (self->chip != FU_PARADE_USBHUB_CHIP_UNKNOWN) return TRUE; g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid ParadeUsbhubChip"); return FALSE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no supported"); return FALSE; } static void fu_parade_usbhub_device_init(FuParadeUsbhubDevice *self) { self->spi_address = FU_PARADE_USBHUB_SPI_ROM_ADDRESS_BANK4_HUB_FIRMWARE_1; fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_USER_REPLUG); fu_device_set_firmware_size(FU_DEVICE(self), FU_PARADE_USBHUB_SPI_ROM_SIZE); fu_device_add_protocol(FU_DEVICE(self), "com.paradetech.usbhub"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_USB_HUB); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SELF_RECOVERY); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_request_flag(FU_DEVICE(self), FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); } static void fu_parade_usbhub_device_constructed(GObject *object) { FuParadeUsbhubDevice *self = FU_PARADE_USBHUB_DEVICE(object); self->chip = FU_PARADE_USBHUB_CHIP_PS5512; self->cfi_device = fu_cfi_device_new(fu_device_get_context(FU_DEVICE(self)), NULL); } static void fu_parade_usbhub_device_finalize(GObject *object) { FuParadeUsbhubDevice *self = FU_PARADE_USBHUB_DEVICE(object); if (self->cfi_device != NULL) g_object_unref(self->cfi_device); G_OBJECT_CLASS(fu_parade_usbhub_device_parent_class)->finalize(object); } static void fu_parade_usbhub_device_class_init(FuParadeUsbhubDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->constructed = fu_parade_usbhub_device_constructed; object_class->finalize = fu_parade_usbhub_device_finalize; device_class->to_string = fu_parade_usbhub_device_to_string; device_class->setup = fu_parade_usbhub_device_setup; device_class->prepare = fu_parade_usbhub_device_prepare; device_class->cleanup = fu_parade_usbhub_device_cleanup; device_class->attach = fu_parade_usbhub_device_attach; device_class->detach = fu_parade_usbhub_device_detach; device_class->set_quirk_kv = fu_parade_usbhub_device_set_quirk_kv; device_class->prepare_firmware = fu_parade_usbhub_device_prepare_firmware; device_class->write_firmware = fu_parade_usbhub_device_write_firmware; device_class->set_progress = fu_parade_usbhub_device_set_progress; device_class->convert_version = fu_parade_usbhub_device_convert_version; } fwupd-2.0.10/plugins/parade-usbhub/fu-parade-usbhub-device.h000066400000000000000000000005511501337203100236560ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_PARADE_USBHUB_DEVICE (fu_parade_usbhub_device_get_type()) G_DECLARE_FINAL_TYPE(FuParadeUsbhubDevice, fu_parade_usbhub_device, FU, PARADE_USBHUB_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/parade-usbhub/fu-parade-usbhub-firmware.c000066400000000000000000000042341501337203100242300ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-parade-usbhub-common.h" #include "fu-parade-usbhub-firmware.h" #include "fu-parade-usbhub-struct.h" struct _FuParadeUsbhubFirmware { FuFirmware parent_instance; }; G_DEFINE_TYPE(FuParadeUsbhubFirmware, fu_parade_usbhub_firmware, FU_TYPE_FIRMWARE) static gboolean fu_parade_usbhub_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_parade_usbhub_hdr_validate_stream(stream, offset, error); } static gboolean fu_parade_usbhub_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { gsize streamsz = 0; guint32 version_raw = 0; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz != FU_PARADE_USBHUB_SPI_ROM_SIZE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "wrong file size, expected 0x%x and got 0x%x", (guint)FU_PARADE_USBHUB_SPI_ROM_SIZE, (guint)streamsz); return FALSE; } /* read out FW#1 version */ if (!fu_input_stream_read_u32(stream, 0x41000, &version_raw, G_LITTLE_ENDIAN, error)) return FALSE; fu_firmware_set_version_raw(firmware, version_raw); /* success */ return TRUE; } static gchar * fu_parade_usbhub_firmware_convert_version(FuFirmware *firmware, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_firmware_get_version_format(firmware)); } static void fu_parade_usbhub_firmware_init(FuParadeUsbhubFirmware *self) { fu_firmware_set_version_format(FU_FIRMWARE(self), FWUPD_VERSION_FORMAT_QUAD); } static void fu_parade_usbhub_firmware_class_init(FuParadeUsbhubFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->convert_version = fu_parade_usbhub_firmware_convert_version; firmware_class->validate = fu_parade_usbhub_firmware_validate; firmware_class->parse = fu_parade_usbhub_firmware_parse; } FuFirmware * fu_parade_usbhub_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_PARADE_USBHUB_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/parade-usbhub/fu-parade-usbhub-firmware.h000066400000000000000000000006451501337203100242370ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_PARADE_USBHUB_FIRMWARE (fu_parade_usbhub_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuParadeUsbhubFirmware, fu_parade_usbhub_firmware, FU, PARADE_USBHUB_FIRMWARE, FuFirmware) FuFirmware * fu_parade_usbhub_firmware_new(void); fwupd-2.0.10/plugins/parade-usbhub/fu-parade-usbhub-plugin.c000066400000000000000000000017671501337203100237220ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-parade-usbhub-device.h" #include "fu-parade-usbhub-firmware.h" #include "fu-parade-usbhub-plugin.h" struct _FuParadeUsbhubPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuParadeUsbhubPlugin, fu_parade_usbhub_plugin, FU_TYPE_PLUGIN) static void fu_parade_usbhub_plugin_init(FuParadeUsbhubPlugin *self) { } static void fu_parade_usbhub_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "ParadeUsbhubChip"); fu_plugin_add_device_gtype(plugin, FU_TYPE_PARADE_USBHUB_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_PARADE_USBHUB_FIRMWARE); } static void fu_parade_usbhub_plugin_class_init(FuParadeUsbhubPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_parade_usbhub_plugin_constructed; } fwupd-2.0.10/plugins/parade-usbhub/fu-parade-usbhub-plugin.h000066400000000000000000000004341501337203100237150ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuParadeUsbhubPlugin, fu_parade_usbhub_plugin, FU, PARADE_USBHUB_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/parade-usbhub/fu-parade-usbhub.rs000066400000000000000000000020721501337203100226160ustar00rootroot00000000000000// Copyright 2024 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ValidateStream, Default)] #[repr(C, packed)] struct FuStructParadeUsbhubHdr { signature: u16be == 0x55AA, } enum FuParadeUsbhubDeviceRequest { Read = 0x40, Write = 0x41, } enum FuParadeUsbhubDeviceAddr { Status = 0x5000, Data = 0x5001, // u32 SpiAddr = 0x5005, // u24 SramAddr = 0x5008, // u16 DmaSize = 0x500C, ReadSize = 0x500D, DbiTimeout = 0x5819, UfpDisconnect = 0x584B, SpiMasterAcquire = 0x5824, SpiMaster = 0x5826, SramPage = 0x5879, VersionA = 0x5C0E, VersionB = 0x5C0F, VersionC = 0x5C11, VersionD = 0x5C12, SpiMasterAcquire2 = 0xE2B3, // for PS188 } enum FuParadeUsbhubDeviceStatusFlag { Write = 0b00000001, TriggerSpi = 0b00000010, TriggerDbi = 0b00000100, Checksum = 0b00001000, SpiDone = 0b10000000, } #[derive(FromString, ToString)] enum FuParadeUsbhubChip { Unknown, Ps5512, Ps188, } fwupd-2.0.10/plugins/parade-usbhub/meson.build000066400000000000000000000011001501337203100212470ustar00rootroot00000000000000if libusb.found() cargs = ['-DG_LOG_DOMAIN="FuPluginParadeUsbhub"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('parade-usbhub.quirk') plugin_builtins += static_library('fu_plugin_parade_usbhub', rustgen.process('fu-parade-usbhub.rs'), sources: [ 'fu-parade-usbhub-device.c', 'fu-parade-usbhub-firmware.c', 'fu-parade-usbhub-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files( 'tests/parade-ps5512evb.json', ) endif fwupd-2.0.10/plugins/parade-usbhub/parade-usbhub.quirk000066400000000000000000000010341501337203100227120ustar00rootroot00000000000000# PS5511 devboard [USB\VID_1DA0&PID_5511] Plugin = parade_usbhub [USB\VID_1DA0&PID_55A1] Plugin = parade_usbhub # PS5512 devboard [USB\VID_1DA0&PID_5512] Plugin = parade_usbhub [USB\VID_1DA0&PID_551D] Plugin = parade_usbhub # PS188 [USB\VID_1DA0&PID_0188] Plugin = parade_usbhub ParadeUsbhubChip = ps188 [USB\VID_1DA0&PID_2188] Plugin = parade_usbhub ParadeUsbhubChip = ps188 # PS188 - Lenovo [USB\VID_17EF&PID_10B7] Plugin = parade_usbhub ParadeUsbhubChip = ps188 [USB\VID_17EF&PID_10B8] Plugin = parade_usbhub ParadeUsbhubChip = ps188 fwupd-2.0.10/plugins/parade-usbhub/tests/000077500000000000000000000000001501337203100202575ustar00rootroot00000000000000fwupd-2.0.10/plugins/parade-usbhub/tests/parade-ps5512evb.json000066400000000000000000000016061501337203100240430ustar00rootroot00000000000000{ "name": "Parade PS5512 EVB", "interactive": true, "steps": [ { "url": "f8b4534e1cec74910797f9de77b8821a4989672b503a7d0362238c2266d80a40-Parade-PS5512_EVB-1.1.0.21.cab", "emulation-url": "dff3d1869ac203dc6531fd43eab0172c635ca24d3bf94619635267e68915addb-Parade-PS5512_EVB-1.1.0.21.zip", "components": [ { "version": "1.1.0.21", "guids": [ "92ac3e32-0949-593d-8fa4-7e9b1561836f" ] } ] }, { "url": "2de6371c452521a6913945ab45c0e0585982e0448762fd984a0e55570872fc2a-Parade-PS5512_EVB-1.1.0.22.cab", "emulation-url": "2b9b7ac4bb571687ccaa51287e2b67569b22517520222e6c70fa976df2a2b72d-Parade-PS5512_EVB-1.1.0.22.zip", "components": [ { "version": "1.1.0.22", "guids": [ "92ac3e32-0949-593d-8fa4-7e9b1561836f" ] } ] } ] } fwupd-2.0.10/plugins/pci-bcr/000077500000000000000000000000001501337203100157125ustar00rootroot00000000000000fwupd-2.0.10/plugins/pci-bcr/README.md000066400000000000000000000006361501337203100171760ustar00rootroot00000000000000--- title: Plugin: PCI BCR — BIOS Control Register --- ## Introduction This plugin checks if the system SPI chip is locked. The result will be stored in an security attribute for HSI. ## External Interface Access This plugin requires read access to the config space of PCI devices (`/sys/class/pci_bus/*/device/config`) ## Version Considerations This plugin has been available since fwupd version `1.5.0`. fwupd-2.0.10/plugins/pci-bcr/config000066400000000000000000000001001501337203100170710ustar00rootroot00000000000000†€P¡1€ª."fwupd-2.0.10/plugins/pci-bcr/fu-pci-bcr-plugin.c000066400000000000000000000165211501337203100213060ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-pci-bcr-plugin.h" struct _FuPciBcrPlugin { FuPlugin parent_instance; gboolean has_device; guint8 bcr_addr; guint8 bcr; }; G_DEFINE_TYPE(FuPciBcrPlugin, fu_pci_bcr_plugin, FU_TYPE_PLUGIN) #define BCR_WPD (1 << 0) #define BCR_BLE (1 << 1) #define BCR_SMM_BWP (1 << 5) static void fu_pci_bcr_plugin_to_string(FuPlugin *plugin, guint idt, GString *str) { FuPciBcrPlugin *self = FU_PCI_BCR_PLUGIN(plugin); fwupd_codec_string_append_bool(str, idt, "HasDevice", self->has_device); fwupd_codec_string_append_hex(str, idt, "BcrAddr", self->bcr_addr); fwupd_codec_string_append_hex(str, idt, "Bcr", self->bcr); } static void fu_pci_bcr_plugin_set_updatable(FuPlugin *plugin, FuDevice *dev) { FuPciBcrPlugin *self = FU_PCI_BCR_PLUGIN(plugin); if ((self->bcr & BCR_WPD) == 0 && (self->bcr & BCR_BLE) > 0) { fu_device_inhibit(dev, "bcr-locked", "BIOS locked"); } else { fu_device_uninhibit(dev, "bcr-locked"); } } static void fu_pci_bcr_plugin_device_registered(FuPlugin *plugin, FuDevice *dev) { FuPciBcrPlugin *self = FU_PCI_BCR_PLUGIN(plugin); if (g_strcmp0(fu_device_get_plugin(dev), "cpu") == 0 || g_strcmp0(fu_device_get_plugin(dev), "flashrom") == 0) { guint tmp = fu_device_get_metadata_integer(dev, "PciBcrAddr"); if (tmp != G_MAXUINT && self->bcr_addr != tmp) { g_info("overriding BCR addr from 0x%02x to 0x%02x", self->bcr_addr, tmp); self->bcr_addr = tmp; } } if (g_strcmp0(fu_device_get_plugin(dev), "flashrom") == 0 && fu_device_has_private_flag(dev, FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE)) { /* PCI\VEN_8086 added first */ if (self->has_device) { fu_pci_bcr_plugin_set_updatable(plugin, dev); return; } fu_plugin_cache_add(plugin, "main-system-firmware", dev); } } static void fu_pci_bcr_plugin_add_security_attr_bioswe(FuPlugin *plugin, FuSecurityAttrs *attrs) { FuPciBcrPlugin *self = FU_PCI_BCR_PLUGIN(plugin); FuDevice *msf_device = fu_plugin_cache_lookup(plugin, "main-system-firmware"); g_autoptr(FwupdSecurityAttr) attr = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_SPI_BIOSWE); if (msf_device != NULL) fwupd_security_attr_add_guids(attr, fu_device_get_guids(msf_device)); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); fu_security_attrs_append(attrs, attr); /* no device */ if (!self->has_device) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND); return; } /* load file */ if ((self->bcr & BCR_WPD) == 1) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_ENABLED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_pci_bcr_plugin_add_security_attr_ble(FuPlugin *plugin, FuSecurityAttrs *attrs) { FuPciBcrPlugin *self = FU_PCI_BCR_PLUGIN(plugin); FuDevice *msf_device = fu_plugin_cache_lookup(plugin, "main-system-firmware"); g_autoptr(FwupdSecurityAttr) attr = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_SPI_BLE); if (msf_device != NULL) fwupd_security_attr_add_guids(attr, fu_device_get_guids(msf_device)); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_ENABLED); fu_security_attrs_append(attrs, attr); /* no device */ if (!self->has_device) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND); return; } /* load file */ if ((self->bcr & BCR_BLE) == 0) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_pci_bcr_plugin_add_security_attr_smm_bwp(FuPlugin *plugin, FuSecurityAttrs *attrs) { FuPciBcrPlugin *self = FU_PCI_BCR_PLUGIN(plugin); FuDevice *msf_device = fu_plugin_cache_lookup(plugin, "main-system-firmware"); g_autoptr(FwupdSecurityAttr) attr = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_SPI_SMM_BWP); if (msf_device != NULL) fwupd_security_attr_add_guids(attr, fu_device_get_guids(msf_device)); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_LOCKED); fu_security_attrs_append(attrs, attr); /* no device */ if (!self->has_device) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND); return; } /* load file */ if ((self->bcr & BCR_SMM_BWP) == 0) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_LOCKED); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static gboolean fu_pci_bcr_plugin_backend_device_added(FuPlugin *plugin, FuDevice *device, FuProgress *progress, GError **error) { FuPciBcrPlugin *self = FU_PCI_BCR_PLUGIN(plugin); FuDevice *device_msf; g_autofree gchar *device_file = NULL; g_autoptr(FuDeviceLocker) locker = NULL; /* not supported */ if (self->bcr_addr == 0x0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "BCR not supported on this platform"); return FALSE; } /* interesting device? */ if (!FU_IS_PCI_DEVICE(device)) return TRUE; /* open the config */ device_file = g_build_filename(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device)), "config", NULL); fu_udev_device_set_device_file(FU_UDEV_DEVICE(device), device_file); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(device), FU_IO_CHANNEL_OPEN_FLAG_READ); locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; /* grab BIOS Control Register */ if (!fu_udev_device_pread(FU_UDEV_DEVICE(device), self->bcr_addr, &self->bcr, 1, error)) { g_prefix_error(error, "could not read BCR: "); return FALSE; } /* main-system-firmware device added first, probably from flashrom */ device_msf = fu_plugin_cache_lookup(plugin, "main-system-firmware"); if (device_msf != NULL) fu_pci_bcr_plugin_set_updatable(plugin, device_msf); /* success */ self->has_device = TRUE; return TRUE; } static void fu_pci_bcr_plugin_add_security_attrs(FuPlugin *plugin, FuSecurityAttrs *attrs) { /* only Intel */ if (fu_cpu_get_vendor() != FU_CPU_VENDOR_INTEL) return; /* add attrs */ fu_pci_bcr_plugin_add_security_attr_bioswe(plugin, attrs); fu_pci_bcr_plugin_add_security_attr_ble(plugin, attrs); fu_pci_bcr_plugin_add_security_attr_smm_bwp(plugin, attrs); } static void fu_pci_bcr_plugin_init(FuPciBcrPlugin *self) { /* this is true except for some Atoms */ self->bcr_addr = 0xdc; } static void fu_pci_bcr_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "PciBcrAddr"); fu_plugin_add_udev_subsystem(plugin, "pci"); } static void fu_pci_bcr_plugin_class_init(FuPciBcrPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_pci_bcr_plugin_constructed; plugin_class->to_string = fu_pci_bcr_plugin_to_string; plugin_class->add_security_attrs = fu_pci_bcr_plugin_add_security_attrs; plugin_class->device_registered = fu_pci_bcr_plugin_device_registered; plugin_class->backend_device_added = fu_pci_bcr_plugin_backend_device_added; } fwupd-2.0.10/plugins/pci-bcr/fu-pci-bcr-plugin.h000066400000000000000000000003561501337203100213120ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuPciBcrPlugin, fu_pci_bcr_plugin, FU, PCI_BCR_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/pci-bcr/meson.build000066400000000000000000000005641501337203100200610ustar00rootroot00000000000000if hsi cargs = ['-DG_LOG_DOMAIN="FuPluginPciBcr"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('pci-bcr.quirk') plugin_builtins += static_library('fu_plugin_pci_bcr', sources: [ 'fu-pci-bcr-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/pci-bcr/pci-bcr.quirk000066400000000000000000000003421501337203100203050ustar00rootroot00000000000000# ISA bridge i.e. 00:1F.0 # -> drivers/mfd/lpc_ich.c [PCI\VEN_8086&CLASS_060100] Plugin = pci_bcr # PCI devices, i.e. 00:1F.5 # -> drivers/mtd/spi-nor/controllers/intel-spi-pci.c [PCI\VEN_8086&CLASS_0C8000] Plugin = pci_bcr fwupd-2.0.10/plugins/pci-mei/000077500000000000000000000000001501337203100157165ustar00rootroot00000000000000fwupd-2.0.10/plugins/pci-mei/README.md000066400000000000000000000006061501337203100171770ustar00rootroot00000000000000--- title: Plugin: PCI MEI --- ## Introduction This plugin checks if the ME is in Manufacturing Mode. The result will be stored in an security attribute for HSI. ## External Interface Access This plugin requires read access to the config space of PCI devices (`/sys/class/pci_bus/*/device/config`) ## Version Considerations This plugin has been available since fwupd version `1.5.0`. fwupd-2.0.10/plugins/pci-mei/fu-mei-common.c000066400000000000000000000071261501337203100205400ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-mei-common.h" #include "fu-mei-struct.h" static gint fu_mei_common_cmp_version(FuMeiVersion *vers1, FuMeiVersion *vers2) { guint16 vers1buf[] = { vers1->major, vers1->minor, vers1->hotfix, vers1->buildno, }; guint16 vers2buf[] = { vers2->major, vers2->minor, vers2->hotfix, vers2->buildno, }; for (guint i = 0; i < 4; i++) { if (vers1buf[i] < vers2buf[i]) return -1; if (vers1buf[i] > vers2buf[i]) return 1; } return 0; } FuMeiIssue fu_mei_common_is_csme_vulnerable(FuMeiVersion *vers) { struct { guint8 major_eq; guint8 minor_eq; guint8 hotfix_ge; } verdata[] = {{11, 8, 92}, {11, 12, 92}, {11, 22, 92}, {12, 0, 90}, {13, 0, 60}, {13, 30, 30}, {13, 50, 20}, {14, 1, 65}, {14, 5, 45}, {15, 0, 40}, {15, 40, 20}, {0, 0, 0}}; for (guint i = 0; verdata[i].major_eq != 0; i++) { if (vers->major == verdata[i].major_eq && vers->minor == verdata[i].minor_eq) { return vers->hotfix >= verdata[i].hotfix_ge ? FU_MEI_ISSUE_PATCHED : FU_MEI_ISSUE_VULNERABLE; } } return FU_MEI_ISSUE_NOT_VULNERABLE; } FuMeiIssue fu_mei_common_is_txe_vulnerable(FuMeiVersion *vers) { struct { guint8 major_eq; guint8 minor_eq; guint8 hotfix_ge; } verdata[] = {{3, 1, 92}, {4, 0, 45}, {0, 0, 0}}; for (guint i = 0; verdata[i].major_eq != 0; i++) { if (vers->major == verdata[i].major_eq && vers->minor == verdata[i].minor_eq) { return vers->hotfix >= verdata[i].hotfix_ge ? FU_MEI_ISSUE_PATCHED : FU_MEI_ISSUE_VULNERABLE; } } return FU_MEI_ISSUE_NOT_VULNERABLE; } FuMeiIssue fu_mei_common_is_sps_vulnerable(FuMeiVersion *vers) { if (vers->major == 3 || vers->major > 5) return FU_MEI_ISSUE_NOT_VULNERABLE; if (vers->major == 4) { if (vers->hotfix < 44) return FU_MEI_ISSUE_VULNERABLE; if (vers->platform == 0xA) { /* Purley */ FuMeiVersion ver2 = { .major = 4, .minor = 1, .hotfix = 4, .buildno = 339, }; if (fu_mei_common_cmp_version(vers, &ver2) < 0) return FU_MEI_ISSUE_VULNERABLE; } else if (vers->platform == 0xE) { /* Bakerville */ FuMeiVersion ver2 = { .major = 4, .minor = 0, .hotfix = 4, .buildno = 112, }; if (fu_mei_common_cmp_version(vers, &ver2) < 0) return FU_MEI_ISSUE_VULNERABLE; } else if (vers->platform == 0xB) { /* Harrisonville */ FuMeiVersion ver2 = { .major = 4, .minor = 0, .hotfix = 4, .buildno = 193, }; if (fu_mei_common_cmp_version(vers, &ver2) < 0) return FU_MEI_ISSUE_VULNERABLE; } else if (vers->platform == 0x9) { /* Greenlow */ FuMeiVersion ver2 = { .major = 4, .minor = 1, .hotfix = 4, .buildno = 88, }; if (vers->minor < 1) return FU_MEI_ISSUE_NOT_VULNERABLE; if (fu_mei_common_cmp_version(vers, &ver2) < 0) return FU_MEI_ISSUE_VULNERABLE; } else if (vers->platform == 0xD) { /* MonteVista */ FuMeiVersion ver2 = { .major = 4, .minor = 8, .hotfix = 4, .buildno = 51, }; if (fu_mei_common_cmp_version(vers, &ver2) < 0) return FU_MEI_ISSUE_VULNERABLE; } return FU_MEI_ISSUE_NOT_VULNERABLE; } if (vers->major == 5) { if (vers->platform == 0x10) { /* Mehlow */ FuMeiVersion ver2 = {5, 1, 3, 89}; if (fu_mei_common_cmp_version(vers, &ver2) < 0) return FU_MEI_ISSUE_VULNERABLE; } return FU_MEI_ISSUE_NOT_VULNERABLE; } return FU_MEI_ISSUE_PATCHED; } fwupd-2.0.10/plugins/pci-mei/fu-mei-common.h000066400000000000000000000007171501337203100205440ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-mei-struct.h" typedef struct { guint8 platform; guint8 major; guint8 minor; guint8 hotfix; guint16 buildno; } FuMeiVersion; FuMeiIssue fu_mei_common_is_csme_vulnerable(FuMeiVersion *vers); FuMeiIssue fu_mei_common_is_txe_vulnerable(FuMeiVersion *vers); FuMeiIssue fu_mei_common_is_sps_vulnerable(FuMeiVersion *vers); fwupd-2.0.10/plugins/pci-mei/fu-mei.rs000066400000000000000000000151221501337203100174470ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] enum FuMeiFamily { Unknown, Sps, Txe, Me, Csme, // 11 to 17 Csme18, } #[derive(ToString)] enum FuMeiIssue { Unknown, NotVulnerable, Vulnerable, Patched, } # HFS1 Current Working State Values #[repr(u4)] enum FuMeHfsCws { Reset, Initializing, Recovery, Test, Disabled, Normal, Wait, Transition, InvalidCpu, Halt = 0x0E, } # HFS1 Current Operation State Values #[repr(u3)] enum FuMeHfsState { Preboot, M0WithUma, M0PowerGated, _Reserved, M3WithoutUma, M0WithoutUma, BringUp, Error, } # HFS Current Operation Mode Values #[repr(u4)] enum FuMeHfsMode { Normal, _Reserved, Debug, Disable, OverrideJumper, OverrideMei, Unknown6, EnhancedDebug, } # HFS Error Code Values #[repr(u4)] enum FuMeHfsError { NoError, UncategorizedFailure, Disabled, ImageFailure, DebugFailure, } #[repr(u2)] enum FuMeHfsEnforcementPolicy { Nothing, ShutdownTo, ShutdownNow, Shutdown_30mins, } #[repr(u3)] enum FuMeiFirmwareSku { Consumer = 0x02, Corporate = 0x03, Lite = 0x05, } /* CSME11 - Host Firmware Status register 1 */ #[derive(Parse)] #[repr(C, packed)] struct FuMeiCsme11Hfsts1 { _working_state: FuMeHfsCws, mfg_mode: u1, _fpt_bad: u1, _operation_state: FuMeHfsState, _fw_init_complete: u1, _ft_bup_ld_flr: u1, _update_in_progress: u1, _error_code: FuMeHfsError, operation_mode: FuMeHfsMode, _reset_count: u4, _boot_options_present: u1, _bist_finished: u1, _bist_test_state: u1, _bist_reset_request: u1, _current_power_source: u2, _d3_support_valid: u1, _d0i3_support_valid: u1, } /* CSME11 - Host Firmware Status register 2 */ #[repr(C, packed)] struct FuMeiCsme11Hfsts2 { nftp_load_failure: u1, icc_prog_status: u2, invoke_mebx: u1, cpu_replaced: u1, _rsvd0: u1, mfs_failure: u1, warm_reset_rqst: u1, cpu_replaced_valid: u1, low_power_state: u1, me_power_gate: u1, ipu_needed: u1, forced_safe_boot: u1, _rsvd1: u2, listener_change: u1, status_data: u8, current_pmevent: u4, phase: u4, } /* CSME11 - Host Firmware Status register 3 */ #[repr(C, packed)] struct FuMeiCsme11Hfsts3 { chunk0: u1, chunk1: u1, chunk2: u1, chunk3: u1, fw_sku: FuMeiFirmwareSku, encrypt_key_check: u1, pch_config_change: u1, ibb_verification_result: u1, ibb_verification_done: u1, reserved_11: u3, actual_ibb_size: u14, number_of_chunks: u2, encrypt_key_override: u1, power_down_mitigation: u1, } /* CSME11 - Host Firmware Status register 4 */ #[repr(C, packed)] struct FuMeiCsme11Hfsts4 { _rsvd0: u9, enforcement_flow: u1, sx_resume_type: u1, _rsvd1: u1, tpms_disconnected: u1, rvsd2: u1, fwsts_valid: u1, boot_guard_self_test: u1, _rsvd3: u16, } /* CSME11 - Host Firmware Status register 5 */ #[repr(C, packed)] struct FuMeiCsme11Hfsts5 { acm_active: u1, valid: u1, result_code_source: u1, error_status_code: u5, acm_done_sts: u1, timeout_count: u7, scrtm_indicator: u1, inc_boot_guard_acm: u4, inc_key_manifest: u4, inc_boot_policy: u4, _rsvd0: u2, start_enforcement: u1, } /* CSME11 - Host Firmware Status register 6 */ #[derive(Parse)] #[repr(C, packed)] struct FuMeiCsme11Hfsts6 { force_boot_guard_acm: u1, _cpu_debug_disable: u1, _bsp_init_disable: u1, _protect_bios_env: u1, _rsvd0: u2, error_enforce_policy: FuMeHfsEnforcementPolicy, _measured_boot: u1, verified_boot: u1, _boot_guard_acmsvn: u4, _kmsvn: u4, _bpmsvn: u4, _key_manifest_id: u4, _boot_policy_status: u1, _error: u1, boot_guard_disable: u1, _fpf_disable: u1, fpf_soc_lock: u1, _txt_support: u1, } /* CSME18 - Host Firmware Status register 1 */ #[derive(Parse)] #[repr(C, packed)] struct FuMeiCsme18Hfsts1 { _working_state: FuMeHfsCws, spi_protection_mode: u1, _fpt_bad: u1, _operation_state: FuMeHfsState, _fw_init_complete: u1, _ft_bup_ld_flr: u1, _update_in_progress: u1, _error_code: FuMeHfsError, operation_mode: FuMeHfsMode, _reset_count: u4, _boot_options_present: u1, _invoke_enhanced_debug_mode: u1, _bist_test_state: u1, _bist_reset_request: u1, _current_power_source: u2, _d3_support_valid: u1, _d0i3_support_valid: u1, } /* CSME18 - Host Firmware Status register 2 */ #[repr(C, packed)] struct FuMeiCsme18Hfsts2 { nftp_load_failure: u1, icc_prog_status: u2, invoke_mebx: u1, cpu_replaced: u1, _rsvd0: u1, mfs_failure: u1, warm_reset_rqst: u1, cpu_replaced_valid: u1, low_power_state: u1, me_power_gate: u1, ipu_needed: u1, _rsvd1: u2, cse_way_to_disabled: u1, listener_change: u1, status_data: u8, current_pmevent: u4, phase: u4, } /* CSME18 - Host Firmware Status register 3 */ #[repr(C, packed)] struct FuMeiCsme18Hfsts3 { reserved: u4, fw_sku: FuMeiFirmwareSku, transactional_state: u1, storage_proxy_present: u1, reserved: u2, rpmc_status_values: u4, rpmc_device_extended_status: u3, bios_rpmc_status: u4, bios_rpmc_device_extended_status: u3, reserved: u7, } /* CSME18 - Host Firmware Status register 4 */ #[repr(C, packed)] struct FuMeiCsme18Hfsts4 { _rsvd0: u2, flash_log_exist: u1, _rsvd1: u29, } #[repr(u5)] enum FuMeiCsme18ErrorStatusCode { Success, BootGuardInitializationFailed, KmVerificationFailed, BpmVerificationFailed, IbbVerificationFailed, FitProcessingFailed, DmaSetupFailed, NemSetupFailed, TpmSetupFailed, IbbMeasurementFailed, _Unknown, MeConnectionFailed, } /* CSME18 - Host Firmware Status register 5 */ #[derive(Parse)] #[repr(C, packed)] struct FuMeiCsme18Hfsts5 { btg_acm_active: u1, valid: u1, _result_code_source: u1, _error_status_code: FuMeiCsme18ErrorStatusCode, acm_done_sts: u1, _timeout_count: u7, _scrtm_indicator: u1, _txt_support: u1, _btg_profile: u3, _cpu_debug_disabled: u1, _bsp_init_disabled: u1, _bsp_boot_policy_manifest_execution_status: u1, _btg_token_applied: u1, _btg_status: u4, _reserved: u2, _start_enforcement: u1, } /* CSME18 - Host Firmware Status register 6 */ #[derive(Parse)] #[repr(C, packed)] struct FuMeiCsme18Hfsts6 { _reserved0: u21, _manufacturing_lock: u1, _reserved1: u8, fpf_soc_configuration_lock: u1, _sx_resume_type: u1, } fwupd-2.0.10/plugins/pci-mei/fu-pci-mei-plugin.c000066400000000000000000000625231501337203100213210ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-mei-common.h" #include "fu-mei-struct.h" #include "fu-pci-mei-plugin.h" struct _FuPciMeiPlugin { FuPlugin parent_instance; FuDevice *pci_device; guint8 hfsts_buf[7][4]; /* 1-6, 0 unused */ FuMeiFamily family; FuMeiVersion vers; FuMeiIssue issue; }; G_DEFINE_TYPE(FuPciMeiPlugin, fu_pci_mei_plugin, FU_TYPE_PLUGIN) static void fu_pci_mei_plugin_to_string(FuPlugin *plugin, guint idt, GString *str) { FuPciMeiPlugin *self = FU_PCI_MEI_PLUGIN(plugin); if (self->pci_device) { fwupd_codec_string_append(str, idt, "PciDevice", fu_device_get_id(self->pci_device)); } for (guint i = 1; i < G_N_ELEMENTS(self->hfsts_buf); i++) { g_autofree gchar *title = g_strdup_printf("Hfsts%u", i); fwupd_codec_string_append_hex( str, idt, title, fu_memread_uint32(self->hfsts_buf[i], G_LITTLE_ENDIAN)); } fwupd_codec_string_append(str, idt, "Family", fu_mei_family_to_string(self->family)); fwupd_codec_string_append_int(str, idt, "VersionPlatform", self->vers.platform); fwupd_codec_string_append_int(str, idt, "VersionMajor", self->vers.major); fwupd_codec_string_append_int(str, idt, "VersionMinor", self->vers.minor); fwupd_codec_string_append_int(str, idt, "VersionHotfix", self->vers.hotfix); fwupd_codec_string_append_int(str, idt, "VersionBuildno", self->vers.buildno); fwupd_codec_string_append(str, idt, "Issue", fu_mei_issue_to_string(self->issue)); } static FuMeiFamily fu_pci_mei_plugin_detect_family(FuPlugin *plugin) { FuPciMeiPlugin *self = FU_PCI_MEI_PLUGIN(plugin); guint8 ver = self->vers.major; if (ver == 0) return FU_MEI_FAMILY_UNKNOWN; if (ver == 1 || ver == 2) { g_autoptr(FuMeiCsme11Hfsts1) hfsts1 = NULL; hfsts1 = fu_mei_csme11_hfsts1_parse(self->hfsts_buf[1], sizeof(self->hfsts_buf[1]), 0x0, NULL); if (hfsts1 == NULL) return FU_MEI_FAMILY_UNKNOWN; if (fu_mei_csme11_hfsts1_get_operation_mode(hfsts1) == 0xf) return FU_MEI_FAMILY_SPS; return FU_MEI_FAMILY_TXE; } if (ver == 3 || ver == 4 || ver == 5) return FU_MEI_FAMILY_TXE; if (ver == 6 || ver == 7 || ver == 8 || ver == 9 || ver == 10) return FU_MEI_FAMILY_ME; if (ver >= 11 && ver <= 17) return FU_MEI_FAMILY_CSME; return FU_MEI_FAMILY_CSME18; } static gboolean fu_pci_mei_plugin_parse_fwvers(FuPlugin *plugin, const gchar *fwvers, GError **error) { FuPciMeiPlugin *self = FU_PCI_MEI_PLUGIN(plugin); guint64 tmp64 = 0; g_auto(GStrv) lines = NULL; g_auto(GStrv) sections = NULL; g_auto(GStrv) split = NULL; /* we only care about the first version */ lines = g_strsplit(fwvers, "\n", -1); if (g_strv_length(lines) < 1) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "expected data, got %s", fwvers); return FALSE; } /* split platform : version */ sections = g_strsplit(lines[0], ":", -1); if (g_strv_length(sections) != 2) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "expected platform:major.minor.micro.build, got %s", lines[0]); return FALSE; } /* parse platform and versions */ if (!fu_strtoull(sections[0], &tmp64, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "failed to process platform version %s: ", sections[0]); return FALSE; } self->vers.platform = tmp64; split = g_strsplit(sections[1], ".", -1); if (g_strv_length(split) != 4) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "expected major.minor.micro.build, got %s", sections[1]); return FALSE; } if (!fu_strtoull(split[0], &tmp64, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "failed to process major version %s: ", split[0]); return FALSE; } self->vers.major = tmp64; if (!fu_strtoull(split[1], &tmp64, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "failed to process minor version %s: ", split[1]); return FALSE; } self->vers.minor = tmp64; if (!fu_strtoull(split[2], &tmp64, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "failed to process hotfix version %s: ", split[2]); return FALSE; } self->vers.hotfix = tmp64; if (!fu_strtoull(split[3], &tmp64, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "failed to process buildno version %s: ", split[3]); return FALSE; } self->vers.buildno = tmp64; /* check the AMT version for issues using the data from: * https://downloadcenter.intel.com/download/28632 */ self->family = fu_pci_mei_plugin_detect_family(plugin); if (self->family == FU_MEI_FAMILY_CSME || self->family == FU_MEI_FAMILY_CSME18) self->issue = fu_mei_common_is_csme_vulnerable(&self->vers); else if (self->family == FU_MEI_FAMILY_TXE) self->issue = fu_mei_common_is_txe_vulnerable(&self->vers); else if (self->family == FU_MEI_FAMILY_SPS) self->issue = fu_mei_common_is_sps_vulnerable(&self->vers); g_debug("%s version parsed as %u.%u.%u", fu_mei_family_to_string(self->family), self->vers.major, self->vers.minor, self->vers.hotfix); return TRUE; } static gboolean fu_pci_mei_plugin_backend_device_added(FuPlugin *plugin, FuDevice *device, FuProgress *progress, GError **error) { FuPciMeiPlugin *self = FU_PCI_MEI_PLUGIN(plugin); const guint hfs_cfg_addrs[] = {0x0, 0x40, 0x48, 0x60, 0x64, 0x68, 0x6c}; g_autofree gchar *device_file = NULL; g_autofree gchar *fwvers = NULL; g_autoptr(FuDeviceLocker) locker = NULL; /* interesting device? */ if (!FU_IS_PCI_DEVICE(device)) return TRUE; /* open the config */ device_file = g_build_filename(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device)), "config", NULL); fu_udev_device_set_device_file(FU_UDEV_DEVICE(device), device_file); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(device), FU_IO_CHANNEL_OPEN_FLAG_READ); locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; /* grab MEI config registers */ g_set_object(&self->pci_device, device); for (guint i = 1; i < G_N_ELEMENTS(hfs_cfg_addrs); i++) { if (!fu_udev_device_pread(FU_UDEV_DEVICE(self->pci_device), hfs_cfg_addrs[i], self->hfsts_buf[i], sizeof(self->hfsts_buf[i]), error)) { g_prefix_error(error, "could not read HFS%u: ", i); return FALSE; } } /* check firmware version */ fwvers = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(device), "mei/mei0/fw_ver", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (fwvers != NULL) { if (!fu_pci_mei_plugin_parse_fwvers(plugin, fwvers, error)) return FALSE; } /* success */ return TRUE; } static void fu_pci_mei_plugin_add_attrs_csme11_manufacturing_mode(FuPlugin *plugin, FuMeiCsme11Hfsts1 *hfsts1, FuSecurityAttrs *attrs) { FuPciMeiPlugin *self = FU_PCI_MEI_PLUGIN(plugin); g_autoptr(FwupdSecurityAttr) attr = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_MEI_MANUFACTURING_MODE); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_LOCKED); fu_security_attrs_append(attrs, attr); /* no device */ if (self->pci_device == NULL) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND); return; } /* Manufacturing Mode */ fwupd_security_attr_add_metadata(attr, "kind", fu_mei_family_to_string(self->family)); if (fu_mei_csme11_hfsts1_get_mfg_mode(hfsts1)) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_LOCKED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_pci_mei_plugin_add_attrs_csme18_manufacturing_mode(FuPlugin *plugin, FuMeiCsme18Hfsts1 *hfsts1, FuSecurityAttrs *attrs) { FuPciMeiPlugin *self = FU_PCI_MEI_PLUGIN(plugin); g_autoptr(FwupdSecurityAttr) attr = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_MEI_MANUFACTURING_MODE); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_LOCKED); fu_security_attrs_append(attrs, attr); /* no device */ if (self->pci_device == NULL) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND); return; } /* Manufacturing Mode, BIOS has access to the SPI descriptor */ if (fu_mei_csme18_hfsts1_get_spi_protection_mode(hfsts1)) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_LOCKED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* running in debug mode */ if (fu_mei_csme18_hfsts1_get_operation_mode(hfsts1) == FU_ME_HFS_MODE_DEBUG || fu_mei_csme18_hfsts1_get_operation_mode(hfsts1) == FU_ME_HFS_MODE_ENHANCED_DEBUG) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_pci_mei_plugin_add_attrs_csme11_override_strap(FuPlugin *plugin, FuMeiCsme11Hfsts1 *hfsts1, FuSecurityAttrs *attrs) { FuPciMeiPlugin *self = FU_PCI_MEI_PLUGIN(plugin); g_autoptr(FwupdSecurityAttr) attr = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_MEI_OVERRIDE_STRAP); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_LOCKED); fu_security_attrs_append(attrs, attr); /* no device */ if (self->pci_device == NULL) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND); return; } /* Flash Descriptor Security Override Strap */ fwupd_security_attr_add_metadata(attr, "kind", fu_mei_family_to_string(self->family)); if (fu_mei_csme11_hfsts1_get_operation_mode(hfsts1) == FU_ME_HFS_MODE_OVERRIDE_JUMPER) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_LOCKED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_pci_mei_plugin_add_attrs_csme18_override_strap(FuPlugin *plugin, FuMeiCsme18Hfsts1 *hfsts1, FuSecurityAttrs *attrs) { FuPciMeiPlugin *self = FU_PCI_MEI_PLUGIN(plugin); g_autoptr(FwupdSecurityAttr) attr = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_MEI_OVERRIDE_STRAP); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_LOCKED); fu_security_attrs_append(attrs, attr); /* no device */ if (self->pci_device == NULL) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND); return; } /* Flash Descriptor Security Override Strap */ fwupd_security_attr_add_metadata(attr, "kind", fu_mei_family_to_string(self->family)); if (fu_mei_csme18_hfsts1_get_operation_mode(hfsts1) == FU_ME_HFS_MODE_OVERRIDE_JUMPER) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_LOCKED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_pci_mei_plugin_add_attrs_csme11_bootguard_enabled(FuPlugin *plugin, FuMeiCsme11Hfsts6 *hfsts6, FuSecurityAttrs *attrs) { FuPciMeiPlugin *self = FU_PCI_MEI_PLUGIN(plugin); g_autoptr(FwupdSecurityAttr) attr = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_ENABLED); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_ENABLED); fu_security_attrs_append(attrs, attr); /* no device */ if (self->pci_device == NULL) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND); return; } /* disabled at runtime? */ if (fu_mei_csme11_hfsts6_get_boot_guard_disable(hfsts6)) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_pci_mei_plugin_add_attrs_csme18_bootguard_enabled(FuPlugin *plugin, FuMeiCsme18Hfsts5 *hfsts5, FuSecurityAttrs *attrs) { FuPciMeiPlugin *self = FU_PCI_MEI_PLUGIN(plugin); g_autoptr(FwupdSecurityAttr) attr = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_ENABLED); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_ENABLED); fu_security_attrs_append(attrs, attr); /* no device */ if (self->pci_device == NULL) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND); return; } /* disabled at runtime? */ if (!fu_mei_csme18_hfsts5_get_valid(hfsts5)) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_pci_mei_plugin_add_attrs_csme11_bootguard_verified(FuPlugin *plugin, FuMeiCsme11Hfsts6 *hfsts6, FuSecurityAttrs *attrs) { FuPciMeiPlugin *self = FU_PCI_MEI_PLUGIN(plugin); g_autoptr(FwupdSecurityAttr) attr = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_VERIFIED); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_VALID); fu_security_attrs_append(attrs, attr); /* no device */ if (self->pci_device == NULL) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND); return; } /* actively disabled */ if (fu_mei_csme11_hfsts6_get_boot_guard_disable(hfsts6)) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); return; } /* measured boot is not sufficient, verified is required */ if (!fu_mei_csme11_hfsts6_get_verified_boot(hfsts6)) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_pci_mei_plugin_add_attrs_csme11_bootguard_acm(FuPlugin *plugin, FuMeiCsme11Hfsts6 *hfsts6, FuSecurityAttrs *attrs) { FuPciMeiPlugin *self = FU_PCI_MEI_PLUGIN(plugin); g_autoptr(FwupdSecurityAttr) attr = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_ACM); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_VALID); fu_security_attrs_append(attrs, attr); /* no device */ if (self->pci_device == NULL) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND); return; } /* actively disabled */ if (fu_mei_csme11_hfsts6_get_boot_guard_disable(hfsts6)) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); return; } /* ACM protection required */ if (!fu_mei_csme11_hfsts6_get_force_boot_guard_acm(hfsts6)) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_pci_mei_plugin_add_attrs_csme18_bootguard_acm(FuPlugin *plugin, FuMeiCsme18Hfsts5 *hfsts5, FuSecurityAttrs *attrs) { FuPciMeiPlugin *self = FU_PCI_MEI_PLUGIN(plugin); g_autoptr(FwupdSecurityAttr) attr = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_ACM); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_VALID); fu_security_attrs_append(attrs, attr); /* no device */ if (self->pci_device == NULL) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND); return; } /* ACM protection required */ if (!fu_mei_csme18_hfsts5_get_btg_acm_active(hfsts5)) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } if (!fu_mei_csme18_hfsts5_get_acm_done_sts(hfsts5)) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_pci_mei_plugin_add_attrs_csme11_bootguard_policy(FuPlugin *plugin, FuMeiCsme11Hfsts6 *hfsts6, FuSecurityAttrs *attrs) { FuPciMeiPlugin *self = FU_PCI_MEI_PLUGIN(plugin); g_autoptr(FwupdSecurityAttr) attr = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_POLICY); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_VALID); fu_security_attrs_append(attrs, attr); /* no device */ if (self->pci_device == NULL) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND); return; } /* actively disabled */ if (fu_mei_csme11_hfsts6_get_boot_guard_disable(hfsts6)) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); return; } /* policy must be to immediately shutdown or after 30 mins -- the latter isn't ideal but * we've been testing for this accidentally for a long time now */ if (fu_mei_csme11_hfsts6_get_error_enforce_policy(hfsts6) != FU_ME_HFS_ENFORCEMENT_POLICY_SHUTDOWN_NOW && fu_mei_csme11_hfsts6_get_error_enforce_policy(hfsts6) != FU_ME_HFS_ENFORCEMENT_POLICY_SHUTDOWN_30MINS) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_pci_mei_plugin_add_attrs_csme11_bootguard_otp(FuPlugin *plugin, FuMeiCsme11Hfsts6 *hfsts6, FuSecurityAttrs *attrs) { FuPciMeiPlugin *self = FU_PCI_MEI_PLUGIN(plugin); g_autoptr(FwupdSecurityAttr) attr = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_OTP); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_VALID); fu_security_attrs_append(attrs, attr); /* no device */ if (self->pci_device == NULL) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND); return; } /* actively disabled */ if (fu_mei_csme11_hfsts6_get_boot_guard_disable(hfsts6)) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); return; } /* ensure vendor set the FPF OTP fuse */ if (!fu_mei_csme11_hfsts6_get_fpf_soc_lock(hfsts6)) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_pci_mei_plugin_add_attrs_csme18_bootguard_otp(FuPlugin *plugin, FuMeiCsme18Hfsts6 *hfsts6, FuSecurityAttrs *attrs) { FuPciMeiPlugin *self = FU_PCI_MEI_PLUGIN(plugin); g_autoptr(FwupdSecurityAttr) attr = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_OTP); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_VALID); fu_security_attrs_append(attrs, attr); /* no device */ if (self->pci_device == NULL) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND); return; } /* ensure vendor set the FPF configuration fuse */ if (!fu_mei_csme18_hfsts6_get_fpf_soc_configuration_lock(hfsts6)) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_pci_mei_plugin_add_attrs_mei_version(FuPlugin *plugin, FuSecurityAttrs *attrs) { FuPciMeiPlugin *self = FU_PCI_MEI_PLUGIN(plugin); g_autofree gchar *version = NULL; g_autoptr(FwupdSecurityAttr) attr = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_MEI_VERSION); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_VALID); fu_security_attrs_append(attrs, attr); /* not enabled */ if (self->pci_device == NULL) { fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA); return; } /* format version as string */ version = g_strdup_printf("%u:%u.%u.%u.%u", self->vers.platform, self->vers.major, self->vers.minor, self->vers.hotfix, self->vers.buildno); if (self->issue == FU_MEI_ISSUE_UNKNOWN) { g_warning("ME family not supported for %s", version); return; } fwupd_security_attr_add_metadata(attr, "version", version); fwupd_security_attr_add_metadata(attr, "kind", fu_mei_family_to_string(self->family)); /* Flash Descriptor Security Override Strap */ if (self->issue == FU_MEI_ISSUE_VULNERABLE) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_pci_mei_plugin_add_security_attrs(FuPlugin *plugin, FuSecurityAttrs *attrs) { FuPciMeiPlugin *self = FU_PCI_MEI_PLUGIN(plugin); g_autoptr(FwupdSecurityAttr) attr_cpu = NULL; /* only Intel */ if (fu_cpu_get_vendor() != FU_CPU_VENDOR_INTEL) return; /* CPU supported */ attr_cpu = fu_security_attrs_get_by_appstream_id(attrs, FWUPD_SECURITY_ATTR_ID_SUPPORTED_CPU, NULL); if (attr_cpu != NULL) fwupd_security_attr_add_flag(attr_cpu, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); /* run CSME-specific tests depending on version */ if (self->family == FU_MEI_FAMILY_CSME) { g_autoptr(FuMeiCsme11Hfsts1) hfsts1 = NULL; g_autoptr(FuMeiCsme11Hfsts6) hfsts6 = NULL; /* CSME 11 to 17 */ hfsts1 = fu_mei_csme11_hfsts1_parse(self->hfsts_buf[1], sizeof(self->hfsts_buf[1]), 0x0, NULL); if (hfsts1 == NULL) return; hfsts6 = fu_mei_csme11_hfsts6_parse(self->hfsts_buf[6], sizeof(self->hfsts_buf[6]), 0x0, NULL); if (hfsts6 == NULL) return; fu_pci_mei_plugin_add_attrs_csme11_manufacturing_mode(plugin, hfsts1, attrs); fu_pci_mei_plugin_add_attrs_csme11_override_strap(plugin, hfsts1, attrs); fu_pci_mei_plugin_add_attrs_csme11_bootguard_enabled(plugin, hfsts6, attrs); fu_pci_mei_plugin_add_attrs_csme11_bootguard_verified(plugin, hfsts6, attrs); fu_pci_mei_plugin_add_attrs_csme11_bootguard_acm(plugin, hfsts6, attrs); fu_pci_mei_plugin_add_attrs_csme11_bootguard_policy(plugin, hfsts6, attrs); fu_pci_mei_plugin_add_attrs_csme11_bootguard_otp(plugin, hfsts6, attrs); } else if (self->family == FU_MEI_FAMILY_CSME18) { g_autoptr(FuMeiCsme18Hfsts1) hfsts1 = NULL; g_autoptr(FuMeiCsme18Hfsts5) hfsts5 = NULL; g_autoptr(FuMeiCsme18Hfsts6) hfsts6 = NULL; /* CSME 18+ */ hfsts1 = fu_mei_csme18_hfsts1_parse(self->hfsts_buf[1], sizeof(self->hfsts_buf[1]), 0x0, NULL); if (hfsts1 == NULL) return; hfsts5 = fu_mei_csme18_hfsts5_parse(self->hfsts_buf[5], sizeof(self->hfsts_buf[5]), 0x0, NULL); if (hfsts5 == NULL) return; hfsts6 = fu_mei_csme18_hfsts6_parse(self->hfsts_buf[6], sizeof(self->hfsts_buf[6]), 0x0, NULL); if (hfsts6 == NULL) return; fu_pci_mei_plugin_add_attrs_csme18_manufacturing_mode(plugin, hfsts1, attrs); fu_pci_mei_plugin_add_attrs_csme18_override_strap(plugin, hfsts1, attrs); fu_pci_mei_plugin_add_attrs_csme18_bootguard_enabled(plugin, hfsts5, attrs); fu_pci_mei_plugin_add_attrs_csme18_bootguard_acm(plugin, hfsts5, attrs); fu_pci_mei_plugin_add_attrs_csme18_bootguard_otp(plugin, hfsts6, attrs); } else { g_autoptr(FwupdSecurityAttr) attr = NULL; /* not supported */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_ENABLED); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_SUPPORTED); fu_security_attrs_append(attrs, attr); return; } /* all */ fu_pci_mei_plugin_add_attrs_mei_version(plugin, attrs); } static void fu_pci_mei_plugin_init(FuPciMeiPlugin *self) { } static void fu_pci_mei_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_udev_subsystem(plugin, "pci"); } static void fu_pci_mei_plugin_finalize(GObject *obj) { FuPciMeiPlugin *self = FU_PCI_MEI_PLUGIN(obj); if (self->pci_device != NULL) g_object_unref(self->pci_device); G_OBJECT_CLASS(fu_pci_mei_plugin_parent_class)->finalize(obj); } static void fu_pci_mei_plugin_class_init(FuPciMeiPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_pci_mei_plugin_finalize; plugin_class->constructed = fu_pci_mei_plugin_constructed; plugin_class->add_security_attrs = fu_pci_mei_plugin_add_security_attrs; plugin_class->backend_device_added = fu_pci_mei_plugin_backend_device_added; plugin_class->to_string = fu_pci_mei_plugin_to_string; } fwupd-2.0.10/plugins/pci-mei/fu-pci-mei-plugin.h000066400000000000000000000003561501337203100213220ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuPciMeiPlugin, fu_pci_mei_plugin, FU, PCI_MEI_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/pci-mei/meson.build000066400000000000000000000006641501337203100200660ustar00rootroot00000000000000if hsi cargs = ['-DG_LOG_DOMAIN="FuPluginPciMei"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('pci-mei.quirk') plugin_builtins += static_library('fu_plugin_pci_mei', rustgen.process( 'fu-mei.rs', ), sources: [ 'fu-pci-mei-plugin.c', 'fu-mei-common.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/pci-mei/pci-mei.quirk000066400000000000000000000000451501337203100203150ustar00rootroot00000000000000[PCI\DRIVER_mei_me] Plugin = pci_mei fwupd-2.0.10/plugins/pci-psp/000077500000000000000000000000001501337203100157465ustar00rootroot00000000000000fwupd-2.0.10/plugins/pci-psp/README.md000066400000000000000000000015751501337203100172350ustar00rootroot00000000000000--- title: Plugin: PCI PSP — Platform Secure Processor --- ## Introduction This plugin checks all information reported from the AMD Platform Secure processor into the operating system on select SOCs. The lack of these sysfs files does *NOT* indicate the lack of these security features, it only indicates the lack of the ability to export it to the operating system. The availability of the sysfs files indicates that the PSP supports exporting this information into the operating system. ## External Interface Access This plugin requires read only access to attributes located within `/sys/bus/pci/devices/`. ## Version Considerations This plugin has been available since fwupd version `1.8.0`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Mario Limonciello: @superm1 fwupd-2.0.10/plugins/pci-psp/fu-pci-psp-device.c000066400000000000000000000325101501337203100213330ustar00rootroot00000000000000/* * Copyright 2022 Advanced Micro Devices Inc. * All rights reserved. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-pci-psp-device.h" #define FU_CPU_AGESA_SMBIOS_TYPE 40 #define FU_CPU_AGESA_SMBIOS_LENGTH 0x0E #define FU_CPU_AGESA_SMBIOS_OFFSET 4 struct _FuPciPspDevice { FuUdevDevice parent_instance; gboolean supported; }; G_DEFINE_TYPE(FuPciPspDevice, fu_pci_psp_device, FU_TYPE_UDEV_DEVICE) static gboolean fu_pci_psp_device_ensure_agesa_version(FuPciPspDevice *self, GError **error) { const gchar *agesa_stream; g_auto(GStrv) split = NULL; g_autofree gchar *summary = NULL; /* get the AGESA stream e.g. `AGESA!V9 StrixKrackanPI-FP8 1.1.0.0a` */ agesa_stream = fu_device_get_smbios_string(FU_DEVICE(self), FU_CPU_AGESA_SMBIOS_TYPE, FU_CPU_AGESA_SMBIOS_LENGTH, FU_CPU_AGESA_SMBIOS_OFFSET, error); if (agesa_stream == NULL) { g_prefix_error(error, "no SMBIOS data: "); return FALSE; } split = g_strsplit(agesa_stream, " ", 3); if (g_strv_length(split) != 3) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid format: %s", agesa_stream); return FALSE; } summary = g_strdup_printf("AGESA %s %s", split[1], split[2]); fu_device_set_summary(FU_DEVICE(self), summary); return TRUE; } static gboolean fu_pci_psp_device_probe(FuDevice *device, GError **error) { FuPciPspDevice *self = FU_PCI_PSP_DEVICE(device); g_autofree gchar *attr_bootloader_version = NULL; g_autofree gchar *attr_tee_version = NULL; g_autoptr(GError) error_agesa = NULL; g_autoptr(GError) error_boot = NULL; g_autoptr(GError) error_tee = NULL; attr_bootloader_version = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(device), "bootloader_version", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, &error_boot); if (attr_bootloader_version == NULL) g_info("failed to read bootloader version: %s", error_boot->message); else fu_device_set_version_bootloader(device, attr_bootloader_version); attr_tee_version = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(device), "tee_version", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, &error_tee); if (attr_tee_version == NULL) g_info("failed to read bootloader version: %s", error_tee->message); else fu_device_set_version(device, attr_tee_version); if (!fu_pci_psp_device_ensure_agesa_version(self, &error_agesa)) g_info("failed to read AGESA stream: %s", error_agesa->message); return TRUE; } static gboolean fu_pci_psp_device_get_attr(FwupdSecurityAttr *attr, const gchar *path, const gchar *file, gboolean *out, GError **error) { guint64 val = 0; g_autofree gchar *fn = g_build_filename(path, file, NULL); g_autofree gchar *buf = NULL; gsize bufsz = 0; if (!g_file_get_contents(fn, &buf, &bufsz, error)) { g_prefix_error(error, "could not open %s: ", fn); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA); return FALSE; } if (!fu_strtoull(buf, &val, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; *out = val ? TRUE : FALSE; return TRUE; } static void fu_pci_psp_device_set_valid_data(FuDevice *device, FuSecurityAttrs *attrs) { FuPciPspDevice *self = FU_PCI_PSP_DEVICE(device); g_autoptr(FwupdSecurityAttr) attr = NULL; if (self->supported) return; /* CPU supported */ self->supported = TRUE; attr = fu_security_attrs_get_by_appstream_id(attrs, FWUPD_SECURITY_ATTR_ID_SUPPORTED_CPU, NULL); if (attr != NULL) fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static FwupdSecurityAttr * fu_pci_psp_device_get_security_attr(FuDevice *device, FuSecurityAttrs *attrs, const gchar *appstream_id) { g_autoptr(FwupdSecurityAttr) attr = NULL; attr = fu_security_attrs_get_by_appstream_id(attrs, appstream_id, NULL); if (attr == NULL) { attr = fu_device_security_attr_new(device, appstream_id); fu_security_attrs_append(attrs, attr); } else if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA)) { g_debug("found missing data on old attribute, repopulating"); fwupd_security_attr_remove_flag(attr, FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA); } return g_steal_pointer(&attr); } static void fu_pci_psp_device_add_security_attrs_tsme(FuDevice *device, const gchar *path, FuSecurityAttrs *attrs) { g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(GError) error_local = NULL; gboolean val; attr = fu_pci_psp_device_get_security_attr(device, attrs, FWUPD_SECURITY_ATTR_ID_ENCRYPTED_RAM); if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)) { g_debug("ignoring already populated attribute"); return; } fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_ENCRYPTED); if (!fu_pci_psp_device_get_attr(attr, path, "tsme_status", &val, &error_local)) { fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA); g_debug("%s", error_local->message); return; } fu_pci_psp_device_set_valid_data(device, attrs); /* BIOS knob used on Lenovo systems */ fu_security_attr_add_bios_target_value(attr, "com.thinklmi.TSME", "enable"); if (!val) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENCRYPTED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW); return; } fwupd_security_attr_add_obsolete(attr, "msr"); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_pci_psp_device_add_security_attrs_fused_part(FuDevice *device, const gchar *path, FuSecurityAttrs *attrs) { g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(GError) error_local = NULL; gboolean val; attr = fu_pci_psp_device_get_security_attr(device, attrs, FWUPD_SECURITY_ATTR_ID_PLATFORM_FUSED); if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)) { g_debug("ignoring already populated attribute"); return; } fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_LOCKED); if (!fu_pci_psp_device_get_attr(attr, path, "fused_part", &val, &error_local)) { fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA); g_debug("%s", error_local->message); return; } fu_pci_psp_device_set_valid_data(device, attrs); if (!val) { g_debug("part is not fused"); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_LOCKED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_pci_psp_device_add_security_attrs_debug_locked_part(FuDevice *device, const gchar *path, FuSecurityAttrs *attrs) { g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(GError) error_local = NULL; gboolean val; attr = fu_pci_psp_device_get_security_attr(device, attrs, FWUPD_SECURITY_ATTR_ID_PLATFORM_DEBUG_LOCKED); if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)) { g_debug("ignoring already populated attribute"); return; } fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_LOCKED); if (!fu_pci_psp_device_get_attr(attr, path, "debug_lock_on", &val, &error_local)) { fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA); g_debug("%s", error_local->message); return; } fu_pci_psp_device_set_valid_data(device, attrs); if (!val) { g_debug("debug lock disabled"); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_LOCKED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_pci_psp_device_add_security_attrs_rollback_protection(FuDevice *device, const gchar *path, FuSecurityAttrs *attrs) { g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(GError) error_local = NULL; gboolean val; attr = fu_pci_psp_device_get_security_attr(device, attrs, FWUPD_SECURITY_ATTR_ID_AMD_ROLLBACK_PROTECTION); if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)) { g_debug("ignoring already populated attribute"); return; } fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_ENABLED); if (!fu_pci_psp_device_get_attr(attr, path, "anti_rollback_status", &val, &error_local)) { fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA); g_debug("%s", error_local->message); return; } fu_pci_psp_device_set_valid_data(device, attrs); if (!val) { g_debug("rollback protection not enforced"); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW); return; } fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_pci_psp_device_add_security_attrs_rom_armor(FuDevice *device, const gchar *path, FuSecurityAttrs *attrs) { g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(GError) error_local = NULL; gboolean val; /* create attr */ attr = fu_pci_psp_device_get_security_attr(device, attrs, FWUPD_SECURITY_ATTR_ID_AMD_SPI_WRITE_PROTECTION); if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)) { g_debug("ignoring already populated attribute"); return; } fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_ENABLED); if (!fu_pci_psp_device_get_attr(attr, path, "rom_armor_enforced", &val, &error_local)) { fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA); g_debug("%s", error_local->message); return; } fu_pci_psp_device_set_valid_data(device, attrs); if (!val) { g_debug("ROM armor not enforced"); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_pci_psp_device_add_security_attrs_rpmc(FuDevice *device, const gchar *path, FuSecurityAttrs *attrs) { g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(GError) error_local = NULL; gboolean val; /* create attr */ attr = fu_pci_psp_device_get_security_attr(device, attrs, FWUPD_SECURITY_ATTR_ID_AMD_SPI_REPLAY_PROTECTION); if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)) { g_debug("ignoring already populated attribute"); return; } fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_ENABLED); if (!fu_pci_psp_device_get_attr(attr, path, "rpmc_spirom_available", &val, &error_local)) { fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA); g_debug("%s", error_local->message); return; } fu_pci_psp_device_set_valid_data(device, attrs); if (!val) { g_debug("no RPMC compatible SPI rom present"); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_SUPPORTED); return; } if (!fu_pci_psp_device_get_attr(attr, path, "rpmc_production_enabled", &val, &error_local)) { g_debug("%s", error_local->message); return; } if (!val) { g_debug("no RPMC compatible SPI rom present"); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_pci_psp_device_add_security_attrs(FuDevice *device, FuSecurityAttrs *attrs) { FuPciPspDevice *self = FU_PCI_PSP_DEVICE(device); const gchar *sysfs_path = NULL; if (device != NULL) sysfs_path = fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device)); /* ccp not loaded */ if (sysfs_path == NULL) return; self->supported = FALSE; fu_pci_psp_device_add_security_attrs_tsme(device, sysfs_path, attrs); fu_pci_psp_device_add_security_attrs_fused_part(device, sysfs_path, attrs); fu_pci_psp_device_add_security_attrs_debug_locked_part(device, sysfs_path, attrs); fu_pci_psp_device_add_security_attrs_rollback_protection(device, sysfs_path, attrs); fu_pci_psp_device_add_security_attrs_rpmc(device, sysfs_path, attrs); fu_pci_psp_device_add_security_attrs_rom_armor(device, sysfs_path, attrs); } static void fu_pci_psp_device_init(FuPciPspDevice *self) { fu_device_set_name(FU_DEVICE(self), "Secure Processor"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_COMPUTER); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_HOST_CPU_CHILD); fu_device_set_vendor(FU_DEVICE(self), "Advanced Micro Devices, Inc."); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_physical_id(FU_DEVICE(self), "pci-psp"); } static void fu_pci_psp_device_class_init(FuPciPspDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_pci_psp_device_probe; device_class->add_security_attrs = fu_pci_psp_device_add_security_attrs; } fwupd-2.0.10/plugins/pci-psp/fu-pci-psp-device.h000066400000000000000000000010351501337203100213360ustar00rootroot00000000000000/* * Copyright 2022 Advanced Micro Devices Inc. * All rights reserved. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_PCI_PSP_DEVICE (fu_pci_psp_device_get_type()) G_DECLARE_FINAL_TYPE(FuPciPspDevice, fu_pci_psp_device, FU, PCI_PSP_DEVICE, FuUdevDevice) fwupd-2.0.10/plugins/pci-psp/fu-pci-psp-plugin.c000066400000000000000000000017611501337203100213760ustar00rootroot00000000000000/* * Copyright 2022 Advanced Micro Devices Inc. * All rights reserved. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-pci-psp-device.h" #include "fu-pci-psp-plugin.h" struct _FuPciPspPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuPciPspPlugin, fu_pci_psp_plugin, FU_TYPE_PLUGIN) static void fu_pci_psp_plugin_init(FuPciPspPlugin *self) { } static void fu_pci_psp_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_udev_subsystem(plugin, "pci"); fu_plugin_add_device_gtype(plugin, FU_TYPE_PCI_PSP_DEVICE); } static void fu_pci_psp_plugin_class_init(FuPciPspPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_pci_psp_plugin_constructed; } fwupd-2.0.10/plugins/pci-psp/fu-pci-psp-plugin.h000066400000000000000000000010221501337203100213710ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * Copyright 2022 Advanced Micro Devices Inc. * All rights reserved. * * This file is provided under a dual MIT/LGPLv2 license. When using or * redistributing this file, you may do so under either license. * AMD Chooses the MIT license part of Dual MIT/LGPLv2 license agreement. * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include G_DECLARE_FINAL_TYPE(FuPciPspPlugin, fu_pci_psp_plugin, FU, PCI_PSP_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/pci-psp/meson.build000066400000000000000000000011461501337203100201120ustar00rootroot00000000000000if hsi and (host_cpu == 'x86' or host_cpu == 'x86_64') cargs = ['-DG_LOG_DOMAIN="FuPluginPciPsp"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('pci-psp.quirk') plugin_builtins += static_library('fu_plugin_pci_psp', sources: [ 'fu-pci-psp-plugin.c', 'fu-pci-psp-device.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) umockdev_tests += files('pci_psp_test.py') device_tests += files( 'tests/pci-psp-strix-enumerate.json', ) enumeration_data += files( 'tests/pci-psp-strix.json', ) endif fwupd-2.0.10/plugins/pci-psp/pci-psp.quirk000066400000000000000000000000421501337203100203720ustar00rootroot00000000000000[PCI\DRIVER_ccp] Plugin = pci_psp fwupd-2.0.10/plugins/pci-psp/pci_psp_test.py000077500000000000000000000456731501337203100210360ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2024 Mario Limonciello # # SPDX-License-Identifier: LGPL-2.1-or-later import gi import os import sys import unittest from fwupd_test import FwupdTest try: gi.require_version("Fwupd", "2.0") from gi.repository import Fwupd # pylint: disable=wrong-import-position except ValueError: # when called from unittest-inspector this might not pass, we'll fail later # anyway in actual use pass class PciPspNoHsi(FwupdTest): """A system that doesn't support exporting any HSI attributes or versions""" def setUp(self): super().setUp() self.testbed.add_from_string( """P: /devices/pci0000:20/0000:20:08.1/0000:23:00.1 E: DRIVER=ccp E: PCI_CLASS=108000 E: PCI_ID=1022:1486 E: PCI_SUBSYS_ID=17AA:1046 E: PCI_SLOT_NAME=0000:23:00.1 E: MODALIAS=pci:v00001022d00001486sv000017AAsd00001046bc10sc80i00 E: SUBSYSTEM=pci E: ID_PCI_CLASS_FROM_DATABASE=Encryption controller E: ID_PCI_SUBCLASS_FROM_DATABASE=Encryption controller E: ID_VENDOR_FROM_DATABASE=Advanced Micro Devices, Inc. [AMD] E: ID_MODEL_FROM_DATABASE=Starship/Matisse Cryptographic Coprocessor PSPCPP A: aer_dev_correctable=RxErr 0BadTLP 0BadDLLP 0Rollover 0Timeout 0NonFatalErr 0CorrIntErr 0HeaderOF 0TOTAL_ERR_COR 0 A: aer_dev_fatal=Undefined 0DLP 0SDES 0TLP 0FCP 0CmpltTO 0CmpltAbrt 0UnxCmplt 0RxOF 0MalfTLP 0ECRC 0UnsupReq 0ACSViol 0UncorrIntErr 0BlockedTLP 0AtomicOpBlocked 0TLPBlockedErr 0PoisonTLPBlocked 0TOTAL_ERR_FATAL 0 A: aer_dev_nonfatal=Undefined 0DLP 0SDES 0TLP 0FCP 0CmpltTO 0CmpltAbrt 0UnxCmplt 0RxOF 0MalfTLP 0ECRC 0UnsupReq 0ACSViol 0UncorrIntErr 0BlockedTLP 0AtomicOpBlocked 0TLPBlockedErr 0PoisonTLPBlocked 0TOTAL_ERR_NONFATAL 0 A: ari_enabled=0 A: broken_parity_status=0 A: class=0x108000 H: config=2210861406041000000080101000800000000000000000000000D0C300000000000000000000E0C300000000AA174610000000004800000000000000FF010000 A: consistent_dma_mask_bits=48 A: current_link_speed=16.0 GT/s PCIe A: current_link_width=16 A: d3cold_allowed=1 A: device=0x1486 A: dma_mask_bits=48 L: driver=../../../../bus/pci/drivers/ccp A: driver_override=(null) A: enable=1 L: firmware_node=../../../LNXSYSTM:00/LNXSYBUS:00/PNP0A08:02/device:8f/device:91 L: iommu=../../0000:20:00.2/iommu/ivhd2 L: iommu_group=../../../../kernel/iommu_groups/37 A: irq=256 A: link/l0s_aspm=0 A: link/l1_aspm=0 A: local_cpulist=0-23 A: local_cpus=00000000,00000000,00000000,00ffffff A: max_link_speed=16.0 GT/s PCIe A: max_link_width=16 A: modalias=pci:v00001022d00001486sv000017AAsd00001046bc10sc80i00 A: msi_bus=1 A: msi_irqs/257=msix A: msi_irqs/258=msix A: numa_node=-1 A: pools=poolinfo - 0.1ccp-1_q4 0 0 64 0ccp-1_q3 0 0 64 0ccp-1_q2 0 0 64 0 A: power/control=on A: power/runtime_active_time=2767353428 A: power/runtime_status=active A: power/runtime_suspended_time=0 A: power/wakeup=disabled A: power/wakeup_abort_count= A: power/wakeup_active= A: power/wakeup_active_count= A: power/wakeup_count= A: power/wakeup_expire_count= A: power/wakeup_last_time_ms= A: power/wakeup_max_time_ms= A: power/wakeup_total_time_ms= A: power_state=D0 A: reset_method=flr A: resource=0x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x00000000c3d00000 0x00000000c3dfffff 0x00000000000402000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x00000000c3e00000 0x00000000c3e01fff 0x00000000000402000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x0000000000000000 A: revision=0x00 A: subsystem_device=0x1046 A: subsystem_vendor=0x17aa A: vendor=0x1022 """ ) self.testbed.add_from_string( """P: /devices/pci0000:20/0000:20:08.1 E: DRIVER=pcieport E: PCI_CLASS=60400 E: PCI_ID=1022:1484 E: PCI_SUBSYS_ID=1046:17AA E: PCI_SLOT_NAME=0000:20:08.1 E: MODALIAS=pci:v00001022d00001484sv00001046sd000017AAbc06sc04i00 E: SUBSYSTEM=pci E: ID_PCI_CLASS_FROM_DATABASE=Bridge E: ID_PCI_SUBCLASS_FROM_DATABASE=PCI bridge E: ID_PCI_INTERFACE_FROM_DATABASE=Normal decode E: ID_VENDOR_FROM_DATABASE=Advanced Micro Devices, Inc. [AMD] E: ID_MODEL_FROM_DATABASE=Starship/Matisse Internal PCIe GPP Bridge 0 to bus[E:B] A: aer_dev_correctable=RxErr 0BadTLP 0BadDLLP 0Rollover 0Timeout 0NonFatalErr 0CorrIntErr 0HeaderOF 0TOTAL_ERR_COR 0 A: aer_dev_fatal=Undefined 0DLP 0SDES 0TLP 0FCP 0CmpltTO 0CmpltAbrt 0UnxCmplt 0RxOF 0MalfTLP 0ECRC 0UnsupReq 0ACSViol 0UncorrIntErr 0BlockedTLP 0AtomicOpBlocked 0TLPBlockedErr 0PoisonTLPBlocked 0TOTAL_ERR_FATAL 0 A: aer_dev_nonfatal=Undefined 0DLP 0SDES 0TLP 0FCP 0CmpltTO 0CmpltAbrt 0UnxCmplt 0RxOF 0MalfTLP 0ECRC 0UnsupReq 0ACSViol 0UncorrIntErr 0BlockedTLP 0AtomicOpBlocked 0TLPBlockedErr 0PoisonTLPBlocked 0TOTAL_ERR_NONFATAL 0 A: aer_rootport_total_err_cor=0 A: aer_rootport_total_err_fatal=0 A: aer_rootport_total_err_nonfatal=0 A: ari_enabled=0 A: broken_parity_status=0 A: class=0x060400 H: config=22108414070410000000040610000100000000000000000020232300F1010000C0C3E0C3F1FF01000000000000000000000000005000000000000000FF011200 A: consistent_dma_mask_bits=32 A: current_link_speed=16.0 GT/s PCIe A: current_link_width=16 A: d3cold_allowed=1 A: device=0x1484 A: dma_mask_bits=32 L: driver=../../../bus/pci/drivers/pcieport A: driver_override=(null) A: enable=2 L: firmware_node=../../LNXSYSTM:00/LNXSYBUS:00/PNP0A08:02/device:8f L: iommu=../0000:20:00.2/iommu/ivhd2 L: iommu_group=../../../kernel/iommu_groups/33 A: irq=43 A: local_cpulist=0-23 A: local_cpus=00000000,00000000,00000000,00ffffff A: max_link_speed=16.0 GT/s PCIe A: max_link_width=16 A: modalias=pci:v00001022d00001484sv00001046sd000017AAbc06sc04i00 A: msi_bus=1 A: msi_irqs/43=msi A: numa_node=-1 A: power/autosuspend_delay_ms=100 A: power/control=auto A: power/runtime_active_time=2767353433 A: power/runtime_status=active A: power/runtime_suspended_time=0 A: power/wakeup=enabled A: power/wakeup_abort_count=0 A: power/wakeup_active=0 A: power/wakeup_active_count=0 A: power/wakeup_count=0 A: power/wakeup_expire_count=0 A: power/wakeup_last_time_ms=0 A: power/wakeup_max_time_ms=0 A: power/wakeup_total_time_ms=0 A: power_state=D0 A: reset_method=pm A: resource=0x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x00000000c3c00000 0x00000000c3efffff 0x00000000000002000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x0000000000000000 A: revision=0x00 A: secondary_bus_number=35 A: subordinate_bus_number=35 A: subsystem_device=0x17aa A: subsystem_vendor=0x1046 A: vendor=0x1022 """ ) def test_pci_psp_device(self): """Verify the PCI PSP device is detected correctly""" self.start_daemon() devices = Fwupd.Client().get_devices() count = 0 for dev in devices: if dev.get_plugin() != "pci_psp": continue self.assertEqual(dev.get_name(), "Secure Processor") self.assertEqual(dev.get_version(), None) self.assertEqual(dev.get_version_bootloader(), None) count += 1 self.assertEqual(count, 1) def test_pci_psp_hsi(self): """Verify the PCI PSP HSI attributes are detected correctly""" self.start_daemon() attrs = Fwupd.Client().get_host_security_attrs() for attr in attrs: if attr.get_plugin() != "pci_psp": continue if attr.get_appstream_id() == "org.fwupd.hsi.PlatformFused": self.assertEqual(attr.get_name(), "Fused platform") self.assertEqual(attr.get_result(), Fwupd.SecurityAttrResult.UNKNOWN) self.assertEqual(attr.get_level(), 1) elif attr.get_appstream_id() == "org.fwupd.hsi.EncryptedRam": self.assertEqual(attr.get_name(), "Encrypted RAM") self.assertEqual(attr.get_result(), Fwupd.SecurityAttrResult.UNKNOWN) self.assertEqual(attr.get_level(), 4) elif attr.get_appstream_id() == "org.fwupd.hsi.Amd.RollbackProtection": self.assertEqual(attr.get_name(), "Processor rollback protection") self.assertEqual(attr.get_result(), Fwupd.SecurityAttrResult.UNKNOWN) self.assertEqual(attr.get_level(), 4) elif attr.get_appstream_id() == "org.fwupd.hsi.Amd.SpiReplayProtection": self.assertEqual(attr.get_name(), "SPI replay protection") self.assertEqual(attr.get_result(), Fwupd.SecurityAttrResult.UNKNOWN) self.assertEqual(attr.get_level(), 3) elif attr.get_appstream_id() == "org.fwupd.hsi.PlatformDebugLocked": self.assertEqual(attr.get_name(), "Platform debugging") self.assertEqual(attr.get_result(), Fwupd.SecurityAttrResult.UNKNOWN) self.assertEqual(attr.get_level(), 2) elif attr.get_appstream_id() == "org.fwupd.hsi.Amd.SpiWriteProtection": self.assertEqual(attr.get_name(), "SPI write protection") self.assertEqual(attr.get_result(), Fwupd.SecurityAttrResult.UNKNOWN) self.assertEqual(attr.get_level(), 2) class PciPspWithHsi(FwupdTest): """A system that can export both HSI attributes and versions""" def setUp(self): super().setUp() self.testbed.add_from_string( """P: /devices/pci0000:00/0000:00:08.1/0000:c1:00.2 E: DRIVER=ccp E: ID_MODEL_FROM_DATABASE=Family 19h (Model 74h) CCP/PSP 3.0 Device E: ID_PATH=pci-0000:c1:00.2 E: ID_PATH_TAG=pci-0000_c1_00_2 E: ID_PCI_CLASS_FROM_DATABASE=Encryption controller E: ID_PCI_SUBCLASS_FROM_DATABASE=Encryption controller E: ID_VENDOR_FROM_DATABASE=Advanced Micro Devices, Inc. [AMD] E: MODALIAS=pci:v00001022d000015C7sv0000F111sd00000006bc10sc80i00 E: PCI_CLASS=108000 E: PCI_ID=1022:15C7 E: PCI_SLOT_NAME=0000:c1:00.2 E: PCI_SUBSYS_ID=F111:0006 E: SUBSYSTEM=pci A: anti_rollback_status=0 A: ari_enabled=0 A: bootloader_version=00.2d.00.78 A: broken_parity_status=0 A: class=0x108000 H: config=2210C715070410000000801010008000000000000000000000004090000000000000000000C05C900000000011F10600000000004800000000000000FF030000 A: consistent_dma_mask_bits=48 A: current_link_speed=16.0 GT/s PCIe A: current_link_width=16 A: d3cold_allowed=1 A: debug_lock_on=1 A: device=0x15c7 A: dma_mask_bits=48 L: driver=../../../../bus/pci/drivers/ccp A: driver_override=(null) A: enable=1 L: firmware_node=../../../LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/device:16/device:18 A: fused_part=1 A: hsp_tpm_available=0 L: iommu=../../0000:00:00.2/iommu/ivhd0 L: iommu_group=../../../../kernel/iommu_groups/16 A: irq=109 A: link/l0s_aspm=0 A: link/l1_aspm=0 A: local_cpulist=0-11 A: local_cpus=0fff A: max_link_speed=16.0 GT/s PCIe A: max_link_width=16 A: modalias=pci:v00001022d000015C7sv0000F111sd00000006bc10sc80i00 A: msi_bus=1 A: msi_irqs/110=msix A: msi_irqs/111=msix A: numa_node=-1 A: power/control=on A: power/runtime_active_time=17568317 A: power/runtime_status=active A: power/runtime_suspended_time=0 A: power/wakeup=disabled A: power/wakeup_abort_count= A: power/wakeup_active= A: power/wakeup_active_count= A: power/wakeup_count= A: power/wakeup_expire_count= A: power/wakeup_last_time_ms= A: power/wakeup_max_time_ms= A: power/wakeup_total_time_ms= A: power_state=D0 A: resource=0x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000090400000 0x00000000904fffff 0x00000000000402000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x00000000905cc000 0x00000000905cdfff 0x00000000000402000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x0000000000000000 A: revision=0x00 A: rom_armor_enforced=1 A: rpmc_production_enabled=0 A: rpmc_spirom_available=1 A: subsystem_device=0x0006 A: subsystem_vendor=0xf111 A: tee_version=00.2d.00.78 A: tsme_status=0 A: vendor=0x1022 """ ) self.testbed.add_from_string( """P: /devices/pci0000:00/0000:00:08.1 E: DRIVER=pcieport E: ID_PATH=pci-0000:00:08.1 E: ID_PATH_TAG=pci-0000_00_08_1 E: ID_PCI_CLASS_FROM_DATABASE=Bridge E: ID_PCI_INTERFACE_FROM_DATABASE=Normal decode E: ID_PCI_SUBCLASS_FROM_DATABASE=PCI bridge E: ID_VENDOR_FROM_DATABASE=Advanced Micro Devices, Inc. [AMD] E: MODALIAS=pci:v00001022d000014EBsv00000006sd0000F111bc06sc04i00 E: PCI_CLASS=60400 E: PCI_ID=1022:14EB E: PCI_SLOT_NAME=0000:00:08.1 E: PCI_SUBSYS_ID=0006:F111 E: SUBSYSTEM=pci A: ari_enabled=0 A: broken_parity_status=0 A: class=0x060400 H: config=2210EB14070410000000040610008100000000000000000000C1C1001111000000905090010071107800000078000000000000005000000000000000FF010200 A: consistent_dma_mask_bits=32 A: current_link_speed=16.0 GT/s PCIe A: current_link_width=16 A: d3cold_allowed=1 A: device=0x14eb A: dma_mask_bits=32 L: driver=../../../bus/pci/drivers/pcieport A: driver_override=(null) A: enable=2 L: firmware_node=../../LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/device:16 L: iommu=../0000:00:00.2/iommu/ivhd0 L: iommu_group=../../../kernel/iommu_groups/7 A: irq=42 A: local_cpulist=0-11 A: local_cpus=0fff A: max_link_speed=16.0 GT/s PCIe A: max_link_width=16 A: modalias=pci:v00001022d000014EBsv00000006sd0000F111bc06sc04i00 A: msi_bus=1 A: msi_irqs/42=msi A: numa_node=-1 A: power/autosuspend_delay_ms=100 A: power/control=auto A: power/runtime_active_time=17568337 A: power/runtime_status=active A: power/runtime_suspended_time=0 A: power/wakeup=disabled A: power/wakeup_abort_count= A: power/wakeup_active= A: power/wakeup_active_count= A: power/wakeup_count= A: power/wakeup_expire_count= A: power/wakeup_last_time_ms= A: power/wakeup_max_time_ms= A: power/wakeup_total_time_ms= A: power_state=D0 A: reset_method=pm A: resource=0x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000000000 0x0000000000000000 0x00000000000000000x0000000000001000 0x0000000000001fff 0x00000000000001010x0000000090000000 0x00000000905fffff 0x00000000000002000x0000007800000000 0x00000078107fffff 0x00000000001022010x0000000000000000 0x0000000000000000 0x0000000000000000 A: revision=0x00 A: secondary_bus_number=193 A: subordinate_bus_number=193 A: subsystem_device=0xf111 A: subsystem_vendor=0x0006 A: vendor=0x1022 """ ) def test_pci_psp_device(self): """Verify the PCI PSP device is detected correctly""" self.start_daemon() devices = Fwupd.Client().get_devices() count = 0 for dev in devices: if dev.get_plugin() != "pci_psp": continue self.assertEqual(dev.get_name(), "Secure Processor") self.assertEqual(dev.get_version(), "00.2d.00.78") self.assertEqual(dev.get_version_bootloader(), "00.2d.00.78") count += 1 self.assertEqual(count, 1) def test_pci_psp_hsi(self): """Verify the PCI PSP HSI attributes are detected correctly""" self.start_daemon() attrs = Fwupd.Client().get_host_security_attrs() for attr in attrs: if attr.get_plugin() != "pci_psp": continue if attr.get_appstream_id() == "org.fwupd.hsi.PlatformFused": self.assertEqual(attr.get_name(), "Fused platform") self.assertEqual(attr.get_result(), Fwupd.SecurityAttrResult.LOCKED) self.assertEqual(attr.get_level(), 1) elif attr.get_appstream_id() == "org.fwupd.hsi.EncryptedRam": self.assertEqual(attr.get_name(), "Encrypted RAM") self.assertEqual( attr.get_result(), Fwupd.SecurityAttrResult.NOT_SUPPORTED ) self.assertEqual(attr.get_level(), 4) elif attr.get_appstream_id() == "org.fwupd.hsi.Amd.RollbackProtection": self.assertEqual(attr.get_name(), "Processor rollback protection") self.assertEqual( attr.get_result(), Fwupd.SecurityAttrResult.NOT_ENABLED ) self.assertEqual(attr.get_level(), 4) elif attr.get_appstream_id() == "org.fwupd.hsi.Amd.SpiReplayProtection": self.assertEqual(attr.get_name(), "SPI replay protection") self.assertEqual( attr.get_result(), Fwupd.SecurityAttrResult.NOT_ENABLED ) self.assertEqual(attr.get_level(), 3) elif attr.get_appstream_id() == "org.fwupd.hsi.PlatformDebugLocked": self.assertEqual(attr.get_name(), "Platform debugging") self.assertEqual(attr.get_result(), Fwupd.SecurityAttrResult.LOCKED) self.assertEqual(attr.get_level(), 2) elif attr.get_appstream_id() == "org.fwupd.hsi.Amd.SpiWriteProtection": self.assertEqual(attr.get_name(), "SPI write protection") self.assertEqual(attr.get_result(), Fwupd.SecurityAttrResult.ENABLED) self.assertEqual(attr.get_level(), 2) if __name__ == "__main__": # run ourselves under umockdev if "umockdev" not in os.environ.get("LD_PRELOAD", ""): os.execvp("umockdev-wrapper", ["umockdev-wrapper", sys.executable] + sys.argv) prog = unittest.main(exit=False) if prog.result.errors or prog.result.failures: sys.exit(1) # Translate to skip error if prog.result.testsRun == len(prog.result.skipped): sys.exit(77) fwupd-2.0.10/plugins/pci-psp/tests/000077500000000000000000000000001501337203100171105ustar00rootroot00000000000000fwupd-2.0.10/plugins/pci-psp/tests/pci-psp-strix-enumerate.json000066400000000000000000000005231501337203100245100ustar00rootroot00000000000000{ "name": "pci psp strix (enumerate only)", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/pci-psp-strix.json", "components": [ { "version": "00.3e.04.74", "guids": [ "92c96a95-c187-5183-ae27-eab2f47e0f63" ] } ] } ] } fwupd-2.0.10/plugins/pci-psp/tests/pci-psp-strix.json000066400000000000000000000055271501337203100225360ustar00rootroot00000000000000{ "FwupdVersion": "2.0.9", "UsbDevices": [ { "Created": "2025-05-19T11:44:58.764803Z", "GType": "FuUdevDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:08.1/0000:c1:00.2", "Subsystem": "pci", "Driver": "ccp", "BindId": "0000:c1:00.2", "Vendor": 4130, "Model": 6112, "Events": [ { "Id": "GetSymlinkTarget:Attr=subsystem", "Data": "pci" }, { "Id": "GetSymlinkTarget:Attr=driver", "Data": "ccp" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "DRIVER=ccp\nPCI_CLASS=108000\nPCI_ID=1022:17E0\nPCI_SUBSYS_ID=F111:000B\nPCI_SLOT_NAME=0000:c1:00.2\nMODALIAS=pci:v00001022d000017E0sv0000F111sd0000000Bbc10sc80i00" }, { "Id": "ReadProp:Key=DEVNAME" }, { "Id": "ReadAttr:Attr=vendor", "Data": "0x1022" }, { "Id": "ReadAttr:Attr=device", "Data": "0x17e0" }, { "Id": "ReadAttr:Attr=class", "Data": "0x108000" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "ReadAttr:Attr=uevent", "Data": "DRIVER=ccp\nPCI_CLASS=108000\nPCI_ID=1022:17E0\nPCI_SUBSYS_ID=F111:000B\nPCI_SLOT_NAME=0000:c1:00.2\nMODALIAS=pci:v00001022d000017E0sv0000F111sd0000000Bbc10sc80i00" }, { "Id": "ReadProp:Key=DEVNAME" }, { "Id": "ReadAttr:Attr=vendor", "Data": "0x1022" }, { "Id": "ReadAttr:Attr=device", "Data": "0x17e0" }, { "Id": "ReadAttr:Attr=class", "Data": "0x108000" }, { "Id": "ReadProp:Key=DEVTYPE" }, { "Id": "GetBackendParent:Subsystem=i2c", "Error": 8, "ErrorMsg": "no parent with subsystem i2c" }, { "Id": "ReadAttr:Attr=class", "Data": "0x108000" }, { "Id": "ReadAttr:Attr=revision", "Data": "0x00" }, { "Id": "ReadAttr:Attr=subsystem_vendor", "Data": "0xf111" }, { "Id": "ReadAttr:Attr=subsystem_device", "Data": "0x000b" }, { "Id": "ReadProp:Key=PCI_SLOT_NAME", "Data": "0000:c1:00.2" }, { "Id": "ReadAttr:Attr=bootloader_version", "Data": "74.04.3e.00" }, { "Id": "ReadAttr:Attr=tee_version", "Data": "00.3e.04.74" }, { "Id": "GetSmbiosString:Type=0x28,Length=0x0e,Offset=0x04", "Data": "AGESA!V9 StrixKrackanPI-FP8 1.1.0.0a" } ] } ] } fwupd-2.0.10/plugins/pixart-rf/000077500000000000000000000000001501337203100163075ustar00rootroot00000000000000fwupd-2.0.10/plugins/pixart-rf/README.md000066400000000000000000000027031501337203100175700ustar00rootroot00000000000000--- title: Plugin: PixArt RF --- ## Introduction This plugin allows the user to update any supported Pixart RF Device using a custom HID-based OTA protocol ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in an unspecified binary file format. This plugin supports the following protocol ID: * `com.pixart.rf` ## GUID Generation These devices use the standard HIDRAW DeviceInstanceId values for both Pixart Imaging, Inc and Primax Electronics, Ltd, e.g. * `HIDRAW\VEN_093A&DEV_2801` * `HIDRAW\VEN_0461&DEV_4EEF` * `HIDRAW\VEN_0461&DEV_4EEF&NAME_${NAME}` * `HIDRAW\VEN_0461&DEV_4EEF&MODEL_${MODEL_NAME}` Additionally, a custom GUID values including the name is used, e.g. ## Update Behavior The firmware is deployed when the device is in normal runtime mode, and the device will reset when the new firmware has been written. ## Quirk Use This plugin uses the following plugin-specific quirks: ### `Flags=is-hpac` Use HPAC firmware header. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x093A` ## External Interface Access This plugin requires ioctl `HIDIOCSFEATURE` and `HIDIOCGFEATURE` access. ## Version Considerations This plugin has been available since fwupd version `1.5.5`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Sam Chen: @sam412081go fwupd-2.0.10/plugins/pixart-rf/fu-pxi-ble-device.c000066400000000000000000000742051501337203100216700ustar00rootroot00000000000000/* * Copyright 2020 Jimmy Yu * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #ifdef HAVE_HIDRAW_H #include #include #endif #include "fu-pxi-ble-device.h" #include "fu-pxi-common.h" #include "fu-pxi-firmware.h" #include "fu-pxi-struct.h" #define PXI_HID_DEV_OTA_INPUT_REPORT_ID 0x05 #define PXI_HID_DEV_OTA_RETRANSMIT_REPORT_ID 0x06 #define PXI_HID_DEV_OTA_FEATURE_REPORT_ID 0x07 #define PXI_HID_DEV_OTA_REPORT_USAGE_PAGE 0xff02u #define PXI_HID_DEV_OTA_RETRANSMIT_USAGE_PAGE 0xff01u #define PXI_HID_DEV_OTA_NOTIFY_USAGE_PAGE 0xff00u #define ERR_COMMAND_SUCCESS 0x0 #define FU_PXI_DEVICE_OBJECT_SIZE_MAX 4096 /* bytes */ #define FU_PXI_BLE_DEVICE_OTA_BUF_SZ 512 /* bytes */ #define FU_PXI_BLE_DEVICE_NOTIFY_RET_LEN 4 /* bytes */ #define FU_PXI_BLE_DEVICE_FW_INFO_RET_LEN 8 /* bytes */ #define FU_PXI_BLE_DEVICE_NOTIFY_TIMEOUT_MS 5000 #define FU_PXI_BLE_DEVICE_SET_REPORT_RETRIES 30 struct _FuPxiBleDevice { FuHidrawDevice parent_instance; struct ota_fw_state fwstate; guint8 retransmit_id; guint8 feature_report_id; guint8 input_report_id; gchar *model_name; }; G_DEFINE_TYPE(FuPxiBleDevice, fu_pxi_ble_device, FU_TYPE_HIDRAW_DEVICE) #ifdef HAVE_HIDRAW_H static gboolean fu_pxi_ble_device_get_raw_info(FuPxiBleDevice *self, struct hidraw_devinfo *info, GError **error) { g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); if (!fu_ioctl_execute(ioctl, HIDIOCGRAWINFO, (guint8 *)info, sizeof(*info), NULL, FU_PXI_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_NONE, error)) { return FALSE; } return TRUE; } #endif static void fu_pxi_ble_device_to_string(FuDevice *device, guint idt, GString *str) { FuPxiBleDevice *self = FU_PXI_BLE_DEVICE(device); fwupd_codec_string_append(str, idt, "ModelName", self->model_name); fu_pxi_ota_fw_state_to_string(&self->fwstate, idt, str); fwupd_codec_string_append_hex(str, idt, "RetransmitID", self->retransmit_id); fwupd_codec_string_append_hex(str, idt, "FeatureReportID", self->feature_report_id); fwupd_codec_string_append_hex(str, idt, "InputReportID", self->input_report_id); } static FuFirmware * fu_pxi_ble_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuPxiBleDevice *self = FU_PXI_BLE_DEVICE(device); g_autoptr(FuFirmware) firmware = fu_pxi_firmware_new(); if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; if (fu_device_has_private_flag(device, FU_PXI_DEVICE_FLAG_IS_HPAC) && fu_pxi_firmware_is_hpac(FU_PXI_FIRMWARE(firmware))) { guint32 hpac_fw_size = 0; g_autoptr(GInputStream) stream_new = NULL; if (!fu_input_stream_read_u32(stream, 9, &hpac_fw_size, G_LITTLE_ENDIAN, error)) return NULL; stream_new = fu_partial_input_stream_new(stream, 9, hpac_fw_size + 264, error); if (stream_new == NULL) return NULL; if (!fu_firmware_set_stream(firmware, stream_new, error)) return NULL; } else if (!fu_device_has_private_flag(device, FU_PXI_DEVICE_FLAG_IS_HPAC) && !fu_pxi_firmware_is_hpac(FU_PXI_FIRMWARE(firmware))) { const gchar *model_name; /* check is compatible with hardware */ model_name = fu_pxi_firmware_get_model_name(FU_PXI_FIRMWARE(firmware)); if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_VID_PID) == 0) { if (self->model_name == NULL || model_name == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "legacy device or firmware detected, " "--force required"); return NULL; } if (g_strcmp0(self->model_name, model_name) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "incompatible firmware, got %s, expected %s.", model_name, self->model_name); return NULL; } } } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "The firmware is incompatible with the device"); return NULL; } return g_steal_pointer(&firmware); } #ifdef HAVE_HIDRAW_H static gboolean fu_pxi_ble_device_set_feature_cb(FuDevice *device, gpointer user_data, GError **error) { GByteArray *req = (GByteArray *)user_data; return fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(device), req->data, req->len, FU_IOCTL_FLAG_NONE, error); } #endif static gboolean fu_pxi_ble_device_set_feature(FuPxiBleDevice *self, GByteArray *req, GError **error) { return fu_device_retry(FU_DEVICE(self), fu_pxi_ble_device_set_feature_cb, FU_PXI_BLE_DEVICE_SET_REPORT_RETRIES, req, error); } static gboolean fu_pxi_ble_device_get_feature(FuPxiBleDevice *self, guint8 *buf, guint bufsz, GError **error) { if (!fu_hidraw_device_get_feature(FU_HIDRAW_DEVICE(self), buf, bufsz, FU_IOCTL_FLAG_NONE, error)) { return FALSE; } /* prepend the report-id and cmd for versions of bluez that do not have * https://github.com/bluez/bluez/commit/35a2c50437cca4d26ac6537ce3a964bb509c9b62 */ if (bufsz > 2 && buf[0] != self->feature_report_id) { g_debug("doing fixup for old bluez version"); memmove(buf + 2, buf, bufsz - 2); buf[0] = self->feature_report_id; buf[1] = 0x0; } return TRUE; } static gboolean fu_pxi_ble_device_search_hid_feature_report_id(FuFirmware *descriptor, guint16 usage_page, guint8 *report_id, GError **error) { g_autoptr(FuFirmware) item_id = NULL; g_autoptr(FuHidReport) report = NULL; /* check ota retransmit feature report usage page exists */ report = fu_hid_descriptor_find_report(FU_HID_DESCRIPTOR(descriptor), error, "usage-page", usage_page, "usage", 0x01, "feature", 0x02, NULL); if (report == NULL) return FALSE; /* find report-id */ item_id = fu_firmware_get_image_by_id(FU_FIRMWARE(report), "report-id", error); if (item_id == NULL) return FALSE; /* success */ *report_id = fu_hid_report_item_get_value(FU_HID_REPORT_ITEM(item_id)); return TRUE; } static gboolean fu_pxi_ble_device_search_hid_input_report_id(FuFirmware *descriptor, guint16 usage_page, guint8 *report_id, GError **error) { g_autoptr(FuHidReport) report = NULL; g_autoptr(FuFirmware) item_id = NULL; /* check ota retransmit feature report usage page exist or not */ report = fu_hid_descriptor_find_report(FU_HID_DESCRIPTOR(descriptor), error, "usage-page", usage_page, "usage", 0x01, "input", 0x02, NULL); if (report == NULL) return FALSE; /* find report-id */ item_id = fu_firmware_get_image_by_id(FU_FIRMWARE(report), "report-id", error); if (item_id == NULL) return FALSE; /* success */ *report_id = fu_hid_report_item_get_value(FU_HID_REPORT_ITEM(item_id)); return TRUE; } static gboolean fu_pxi_ble_device_check_support_report_id(FuPxiBleDevice *self, GError **error) { #ifdef HAVE_HIDRAW_H gint desc_size = 0; g_autoptr(FuFirmware) descriptor = fu_hid_descriptor_new(); g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); g_autoptr(GBytes) fw = NULL; g_autoptr(GError) error_local1 = NULL; g_autoptr(GError) error_local2 = NULL; g_autoptr(GError) error_local3 = NULL; g_autoptr(GError) error_local = NULL; struct hidraw_report_descriptor rpt_desc = {0x0}; /* Get Report Descriptor Size */ if (!fu_ioctl_execute(ioctl, HIDIOCGRDESCSIZE, (guint8 *)&desc_size, sizeof(desc_size), NULL, FU_PXI_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_NONE, error)) return FALSE; rpt_desc.size = desc_size; if (!fu_ioctl_execute(ioctl, HIDIOCGRDESC, (guint8 *)&rpt_desc, sizeof(rpt_desc), NULL, FU_PXI_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_NONE, error)) return FALSE; fu_dump_raw(G_LOG_DOMAIN, "HID descriptor", rpt_desc.value, rpt_desc.size); /* parse the descriptor, but use the defaults if it fails */ fw = g_bytes_new(rpt_desc.value, rpt_desc.size); if (!fu_firmware_parse_bytes(descriptor, fw, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error_local)) { g_debug("failed to parse descriptor: %s", error_local->message); return TRUE; } /* check ota retransmit feature report usage page exists */ if (!fu_pxi_ble_device_search_hid_feature_report_id(descriptor, PXI_HID_DEV_OTA_RETRANSMIT_USAGE_PAGE, &self->retransmit_id, &error_local1)) { g_debug("failed to parse descriptor: %s", error_local1->message); } g_debug("usage-page: 0x%x retransmit_id: %d", PXI_HID_DEV_OTA_RETRANSMIT_USAGE_PAGE, self->retransmit_id); /* check ota feature report usage page exists */ if (!fu_pxi_ble_device_search_hid_feature_report_id(descriptor, PXI_HID_DEV_OTA_REPORT_USAGE_PAGE, &self->feature_report_id, &error_local2)) { g_debug("failed to parse descriptor: %s", error_local2->message); } g_debug("usage-page: 0x%x feature_report_id: %d", PXI_HID_DEV_OTA_RETRANSMIT_USAGE_PAGE, self->feature_report_id); /* check ota notify input report usage page exist or not */ if (!fu_pxi_ble_device_search_hid_input_report_id(descriptor, PXI_HID_DEV_OTA_NOTIFY_USAGE_PAGE, &self->input_report_id, &error_local3)) { g_debug("failed to parse descriptor: %s", error_local3->message); } g_debug("usage-page: 0x%x input_report_id: %d", PXI_HID_DEV_OTA_RETRANSMIT_USAGE_PAGE, self->input_report_id); /* success */ return TRUE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, " not available"); return FALSE #endif } static gboolean fu_pxi_ble_device_fw_ota_check_retransmit(FuPxiBleDevice *self, GError **error) { g_autoptr(GByteArray) req = g_byte_array_new(); /* write fw ota retransmit command to reset the ota state */ fu_byte_array_append_uint8(req, self->retransmit_id); fu_byte_array_append_uint8(req, FU_PXI_DEVICE_CMD_FW_OTA_RETRANSMIT); return fu_pxi_ble_device_set_feature(self, req, error); } static gboolean fu_pxi_ble_device_check_support_resume(FuPxiBleDevice *self, FuFirmware *firmware, FuProgress *progress, GError **error) { g_autoptr(GBytes) fw = NULL; g_autoptr(FuChunkArray) chunks = NULL; guint16 checksum_tmp = 0x0; /* get the default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* check offset is invalid or not */ chunks = fu_chunk_array_new_from_bytes(fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_PXI_DEVICE_OBJECT_SIZE_MAX); if (self->fwstate.offset > fu_chunk_array_length(chunks)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "offset from device is invalid: " "got 0x%x, current maximum 0x%x", self->fwstate.offset, fu_chunk_array_length(chunks)); return FALSE; } /* calculate device current checksum */ for (guint i = 0; i < self->fwstate.offset; i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; checksum_tmp += fu_sum16(fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); } /* check current file is different with previous fw bin or not */ if (self->fwstate.checksum != checksum_tmp) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "checksum is different from previous fw: " "got 0x%04x, expected 0x%04x", self->fwstate.checksum, checksum_tmp); return FALSE; } /* success */ return TRUE; } static gboolean fu_pxi_ble_device_wait_notify(FuPxiBleDevice *self, goffset port, guint8 *status, guint16 *checksum, GError **error) { g_autoptr(GTimer) timer = g_timer_new(); guint8 res[FU_PXI_BLE_DEVICE_OTA_BUF_SZ] = {0}; guint8 cmd_status = 0x0; /* skip the wrong report id ,and keep polling until result is correct */ while (g_timer_elapsed(timer, NULL) * 1000.f < FU_PXI_BLE_DEVICE_NOTIFY_TIMEOUT_MS) { if (!fu_udev_device_pread(FU_UDEV_DEVICE(self), port, res, (FU_PXI_BLE_DEVICE_NOTIFY_RET_LEN + 1) - port, error)) return FALSE; if (res[0] == self->input_report_id) break; } /* timeout */ if (res[0] != self->input_report_id) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Timed-out waiting for HID report"); return FALSE; } /* get the opcode if status is not null */ if (status != NULL) { guint8 status_tmp = 0x0; if (!fu_memread_uint8_safe(res, sizeof(res), 0x1, &status_tmp, error)) return FALSE; /* need check command result if command is fw upgrade */ if (status_tmp == FU_PXI_DEVICE_CMD_FW_UPGRADE) { if (!fu_memread_uint8_safe(res, sizeof(res), 0x2, &cmd_status, error)) return FALSE; if (cmd_status != ERR_COMMAND_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "cmd status was 0x%02x", cmd_status); return FALSE; } } /* propagate */ *status = status_tmp; } if (checksum != NULL) { if (!fu_memread_uint16_safe(res, sizeof(res), 0x3, checksum, G_LITTLE_ENDIAN, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_pxi_ble_device_fw_object_create(FuPxiBleDevice *self, FuChunk *chk, GError **error) { guint8 opcode = 0; g_autoptr(GByteArray) req = g_byte_array_new(); /* request */ fu_byte_array_append_uint8(req, self->feature_report_id); fu_byte_array_append_uint8(req, FU_PXI_DEVICE_CMD_FW_OBJECT_CREATE); fu_byte_array_append_uint32(req, fu_chunk_get_address(chk), G_LITTLE_ENDIAN); fu_byte_array_append_uint32(req, fu_chunk_get_data_sz(chk), G_LITTLE_ENDIAN); if (!fu_pxi_ble_device_set_feature(self, req, error)) return FALSE; /* check object create success or not */ if (!fu_pxi_ble_device_wait_notify(self, 0x0, &opcode, NULL, error)) return FALSE; if (opcode != FU_PXI_DEVICE_CMD_FW_OBJECT_CREATE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "FwObjectCreate opcode got 0x%02x, expected 0x%02x", opcode, (guint)FU_PXI_DEVICE_CMD_FW_OBJECT_CREATE); return FALSE; } /* success */ return TRUE; } static gboolean fu_pxi_ble_device_write_payload(FuPxiBleDevice *self, FuChunk *chk, GError **error) { g_autoptr(GByteArray) req = g_byte_array_new(); fu_byte_array_append_uint8(req, self->feature_report_id); g_byte_array_append(req, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); return fu_pxi_ble_device_set_feature(self, req, error); } static gboolean fu_pxi_ble_device_write_chunk(FuPxiBleDevice *self, FuChunk *chk, GError **error) { guint32 prn = 0; guint16 checksum; guint16 checksum_device = 0; g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(GBytes) chk_bytes = fu_chunk_get_bytes(chk); /* send create fw object command */ if (!fu_pxi_ble_device_fw_object_create(self, chk, error)) return FALSE; /* write payload */ chunks = fu_chunk_array_new_from_bytes(chk_bytes, fu_chunk_get_address(chk), FU_CHUNK_PAGESZ_NONE, self->fwstate.mtu_size); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk2 = NULL; /* prepare chunk */ chk2 = fu_chunk_array_index(chunks, i, error); if (chk2 == NULL) return FALSE; if (!fu_pxi_ble_device_write_payload(self, chk2, error)) return FALSE; prn++; /* wait notify from device when PRN over threshold write or * offset reach max object sz or write offset reach fw length */ if (prn >= self->fwstate.prn_threshold || i == fu_chunk_array_length(chunks) - 1) { guint8 opcode = 0; if (!fu_pxi_ble_device_wait_notify(self, 0x0, &opcode, &checksum_device, error)) return FALSE; if (opcode != FU_PXI_DEVICE_CMD_FW_WRITE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "FwWrite opcode invalid 0x%02x", opcode); return FALSE; } prn = 0; } } /* the last chunk */ checksum = fu_sum16(fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); self->fwstate.checksum += checksum; if (checksum_device != self->fwstate.checksum) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "checksum fail, got 0x%04x, expected 0x%04x", checksum_device, checksum); return FALSE; } /* success */ return TRUE; } static gboolean fu_pxi_ble_device_reset(FuPxiBleDevice *self, GError **error) { g_autoptr(GByteArray) req = g_byte_array_new(); fu_byte_array_append_uint8(req, self->feature_report_id); fu_byte_array_append_uint8(req, FU_PXI_DEVICE_CMD_FW_MCU_RESET); /* OTA reset command */ fu_byte_array_append_uint8(req, FU_PXI_OTA_DISCONNECT_REASON_RESET); if (!fu_pxi_ble_device_set_feature(self, req, error)) { g_prefix_error(error, "failed to reset: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_pxi_ble_device_fw_ota_init(FuPxiBleDevice *self, GError **error) { g_autoptr(GByteArray) req = g_byte_array_new(); /* write fw ota init command */ fu_byte_array_append_uint8(req, self->feature_report_id); fu_byte_array_append_uint8(req, FU_PXI_DEVICE_CMD_FW_OTA_INIT); return fu_pxi_ble_device_set_feature(self, req, error); } static gboolean fu_pxi_ble_device_fw_ota_init_new(FuPxiBleDevice *self, gsize bufsz, GError **error) { guint8 res[FU_PXI_BLE_DEVICE_OTA_BUF_SZ] = {0x0}; guint8 fw_version[10] = {0x0}; g_autoptr(GByteArray) req = g_byte_array_new(); /* write fw ota init new command */ fu_byte_array_append_uint8(req, self->feature_report_id); fu_byte_array_append_uint8(req, FU_PXI_DEVICE_CMD_FW_OTA_INIT_NEW); fu_byte_array_append_uint32(req, bufsz, G_LITTLE_ENDIAN); fu_byte_array_append_uint8(req, 0x0); /* OTA setting */ g_byte_array_append(req, fw_version, sizeof(fw_version)); if (!fu_pxi_ble_device_set_feature(self, req, error)) return FALSE; /* delay for BLE device read command */ fu_device_sleep(FU_DEVICE(self), 10); /* ms */ /* read fw ota init new command */ res[0] = self->feature_report_id; res[1] = FU_PXI_DEVICE_CMD_FW_OTA_INIT_NEW; if (!fu_pxi_ble_device_get_feature(self, res, sizeof(res), error)) return FALSE; /* shared state */ if (!fu_pxi_ota_fw_state_parse(&self->fwstate, res, sizeof(res), 0x05, error)) return FALSE; if (self->fwstate.spec_check_result != FU_PXI_OTA_SPEC_CHECK_RESULT_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "FwInitNew spec check fail: %s [0x%02x]", fu_pxi_ota_spec_check_result_to_string(self->fwstate.spec_check_result), self->fwstate.spec_check_result); return FALSE; } /* success */ return TRUE; } static gboolean fu_pxi_ble_device_fw_upgrade(FuPxiBleDevice *self, FuFirmware *firmware, FuProgress *progress, GError **error) { const gchar *version; guint8 fw_version[5] = {0x0}; guint8 opcode = 0; guint16 checksum; g_autoptr(GBytes) fw = NULL; g_autoptr(GByteArray) req = g_byte_array_new(); fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; checksum = fu_sum16_bytes(fw); fu_byte_array_append_uint8(req, self->feature_report_id); fu_byte_array_append_uint8(req, FU_PXI_DEVICE_CMD_FW_UPGRADE); fu_byte_array_append_uint32(req, g_bytes_get_size(fw), G_LITTLE_ENDIAN); fu_byte_array_append_uint16(req, checksum, G_LITTLE_ENDIAN); if (!fu_device_has_private_flag(FU_DEVICE(self), FU_PXI_DEVICE_FLAG_IS_HPAC)) { version = fu_firmware_get_version(firmware); if (!fu_memcpy_safe(fw_version, sizeof(fw_version), 0x0, /* dst */ (guint8 *)version, strlen(version), 0x0, /* src */ strlen(version), error)) return FALSE; } g_byte_array_append(req, fw_version, sizeof(fw_version)); /* send fw upgrade command */ if (!fu_pxi_ble_device_set_feature(self, req, error)) return FALSE; fu_dump_raw(G_LOG_DOMAIN, "fw upgrade", req->data, req->len); /* wait fw upgrade command result */ if (!fu_pxi_ble_device_wait_notify(self, 0x1, &opcode, NULL, error)) { g_prefix_error(error, "FwUpgrade command fail, " "fw-checksum: 0x%04x fw-size: %" G_GSIZE_FORMAT ": ", checksum, g_bytes_get_size(fw)); return FALSE; } if (opcode != FU_PXI_DEVICE_CMD_FW_UPGRADE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "FwUpgrade opcode invalid 0x%02x", opcode); return FALSE; } /* success */ return TRUE; } static gboolean fu_pxi_ble_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuPxiBleDevice *self = FU_PXI_BLE_DEVICE(device); g_autoptr(GBytes) fw = NULL; g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(GError) error_local = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "ota-init"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 0, "check-support-resume"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 0, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, NULL); /* get the default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* send fw ota retransmit command to reset status */ if (!fu_pxi_ble_device_fw_ota_check_retransmit(self, error)) { g_prefix_error(error, "failed to OTA check retransmit: "); return FALSE; } /* send fw ota init command */ if (!fu_pxi_ble_device_fw_ota_init(self, error)) return FALSE; if (!fu_pxi_ble_device_fw_ota_init_new(self, g_bytes_get_size(fw), error)) return FALSE; fu_progress_step_done(progress); /* prepare write fw into device */ chunks = fu_chunk_array_new_from_bytes(fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_PXI_DEVICE_OBJECT_SIZE_MAX); if (!fu_pxi_ble_device_check_support_resume(self, firmware, fu_progress_get_child(progress), &error_local)) { g_debug("do not resume: %s", error_local->message); self->fwstate.offset = 0; self->fwstate.checksum = 0; } fu_progress_step_done(progress); /* write fw into device */ for (guint i = self->fwstate.offset; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_pxi_ble_device_write_chunk(self, chk, error)) return FALSE; fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)self->fwstate.offset + 1, (gsize)fu_chunk_array_length(chunks)); } fu_progress_step_done(progress); /* fw upgrade command */ if (!fu_pxi_ble_device_fw_upgrade(self, firmware, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* send device reset command */ if (!fu_pxi_ble_device_reset(self, error)) return FALSE; fu_progress_step_done(progress); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* success */ return TRUE; } static gboolean fu_pxi_ble_device_fw_get_info(FuPxiBleDevice *self, GError **error) { guint8 res[FU_PXI_BLE_DEVICE_OTA_BUF_SZ] = {0x0}; guint8 opcode = 0x0; guint16 checksum = 0; guint16 hpac_ver = 0; g_autofree gchar *version_str = NULL; g_autoptr(GByteArray) req = g_byte_array_new(); fu_byte_array_append_uint8(req, self->feature_report_id); fu_byte_array_append_uint8(req, FU_PXI_DEVICE_CMD_FW_GET_INFO); if (!fu_pxi_ble_device_set_feature(self, req, error)) return FALSE; /* delay for BLE device read command */ fu_device_sleep(FU_DEVICE(self), 10); /* ms */ res[0] = self->feature_report_id; res[1] = FU_PXI_DEVICE_CMD_FW_GET_INFO; if (!fu_pxi_ble_device_get_feature(self, res, FU_PXI_BLE_DEVICE_FW_INFO_RET_LEN + 3, error)) return FALSE; if (!fu_memread_uint8_safe(res, sizeof(res), 0x4, &opcode, error)) return FALSE; if (opcode != FU_PXI_DEVICE_CMD_FW_GET_INFO) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "FwGetInfo opcode invalid 0x%02x", opcode); return FALSE; } /* set current version */ if (!fu_device_has_private_flag(FU_DEVICE(self), FU_PXI_DEVICE_FLAG_IS_HPAC)) { version_str = g_strndup((gchar *)res + 0x6, 5); } else { if (!fu_memread_uint16_safe(res, FU_PXI_BLE_DEVICE_OTA_BUF_SZ, 9, &hpac_ver, G_BIG_ENDIAN, error)) return FALSE; version_str = fu_pxi_hpac_version_info_parse(hpac_ver); } fu_device_set_version(FU_DEVICE(self), version_str); /* add current checksum */ if (!fu_memread_uint16_safe(res, sizeof(res), 0xb, &checksum, G_LITTLE_ENDIAN, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_pxi_ble_device_get_model_info(FuPxiBleDevice *self, GError **error) { guint8 res[FU_PXI_BLE_DEVICE_OTA_BUF_SZ] = {0x0}; guint8 opcode = 0x0; guint8 model_name[FU_PXI_DEVICE_MODEL_NAME_LEN] = {0x0}; g_autoptr(GByteArray) req = g_byte_array_new(); fu_byte_array_append_uint8(req, self->feature_report_id); fu_byte_array_append_uint8(req, FU_PXI_DEVICE_CMD_FW_OTA_GET_MODEL); if (!fu_pxi_ble_device_set_feature(self, req, error)) return FALSE; /* delay for BLE device read command */ fu_device_sleep(FU_DEVICE(self), 10); /* ms */ res[0] = self->feature_report_id; if (!fu_pxi_ble_device_get_feature(self, res, sizeof(res), error)) return FALSE; if (!fu_memread_uint8_safe(res, sizeof(res), 0x4, &opcode, error)) return FALSE; /* old firmware */ if (opcode != FU_PXI_DEVICE_CMD_FW_OTA_GET_MODEL) return TRUE; /* get model from res */ if (!fu_memcpy_safe(model_name, sizeof(model_name), 0x0, /* dst */ (guint8 *)res, sizeof(res), 0x6, /* src */ sizeof(model_name), error)) return FALSE; g_clear_pointer(&self->model_name, g_free); if (model_name[0] != 0x00 && model_name[0] != 0xFF) self->model_name = g_strndup((gchar *)model_name, sizeof(model_name)); /* success */ return TRUE; } static gboolean fu_pxi_ble_device_setup_guid(FuPxiBleDevice *self, GError **error) { #ifdef HAVE_HIDRAW_H FuDevice *device = FU_DEVICE(self); struct hidraw_devinfo hid_raw_info = {0x0}; g_autoptr(GString) dev_name = NULL; g_autoptr(GString) model_name = NULL; /* extra GUID with device name */ if (!fu_pxi_ble_device_get_raw_info(self, &hid_raw_info, error)) return FALSE; dev_name = g_string_new(fu_device_get_name(device)); g_string_ascii_up(dev_name); g_string_replace(dev_name, " ", "_", 0); /* extra GUID with model name*/ model_name = g_string_new(self->model_name); g_string_ascii_up(model_name); g_string_replace(model_name, " ", "_", 0); /* generate IDs */ fu_device_add_instance_u16(device, "VEN", hid_raw_info.vendor); fu_device_add_instance_u16(device, "DEV", hid_raw_info.product); fu_device_add_instance_str(device, "NAME", dev_name->str); fu_device_add_instance_str(device, "MODEL", model_name->str); if (!fu_device_build_instance_id(device, error, "HIDRAW", "VEN", "DEV", "NAME", NULL)) return FALSE; if (!fu_device_build_instance_id(device, error, "HIDRAW", "VEN", "DEV", "MODEL", NULL)) return FALSE; #endif return TRUE; } static gboolean fu_pxi_ble_device_setup(FuDevice *device, GError **error) { FuPxiBleDevice *self = FU_PXI_BLE_DEVICE(device); if (!fu_pxi_ble_device_check_support_report_id(self, error)) { g_prefix_error(error, "failed to check report id: "); return FALSE; } if (!fu_pxi_ble_device_fw_ota_check_retransmit(self, error)) { g_prefix_error(error, "failed to OTA check retransmit: "); return FALSE; } if (!fu_pxi_ble_device_fw_ota_init(self, error)) { g_prefix_error(error, "failed to OTA init: "); return FALSE; } if (!fu_pxi_ble_device_fw_get_info(self, error)) { g_prefix_error(error, "failed to get info: "); return FALSE; } if (!fu_pxi_ble_device_get_model_info(self, error)) { g_prefix_error(error, "failed to get model: "); return FALSE; } if (!fu_pxi_ble_device_setup_guid(self, error)) { g_prefix_error(error, "failed to setup GUID: "); return FALSE; } return TRUE; } static void fu_pxi_ble_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_pxi_ble_device_init(FuPxiBleDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_build_vendor_id_u16(FU_DEVICE(self), "USB", 0x093A); fu_device_add_protocol(FU_DEVICE(self), "com.pixart.rf"); fu_device_retry_set_delay(FU_DEVICE(self), 50); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); self->retransmit_id = PXI_HID_DEV_OTA_RETRANSMIT_REPORT_ID; self->feature_report_id = PXI_HID_DEV_OTA_FEATURE_REPORT_ID; self->input_report_id = PXI_HID_DEV_OTA_INPUT_REPORT_ID; fu_device_register_private_flag(FU_DEVICE(self), FU_PXI_DEVICE_FLAG_IS_HPAC); fu_device_set_remove_delay(FU_DEVICE(self), 10000); } static void fu_pxi_ble_device_finalize(GObject *object) { FuPxiBleDevice *self = FU_PXI_BLE_DEVICE(object); g_free(self->model_name); G_OBJECT_CLASS(fu_pxi_ble_device_parent_class)->finalize(object); } static void fu_pxi_ble_device_class_init(FuPxiBleDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->finalize = fu_pxi_ble_device_finalize; device_class->setup = fu_pxi_ble_device_setup; device_class->to_string = fu_pxi_ble_device_to_string; device_class->write_firmware = fu_pxi_ble_device_write_firmware; device_class->prepare_firmware = fu_pxi_ble_device_prepare_firmware; device_class->set_progress = fu_pxi_ble_device_set_progress; } fwupd-2.0.10/plugins/pixart-rf/fu-pxi-ble-device.h000066400000000000000000000005441501337203100216700ustar00rootroot00000000000000/* * Copyright 2020 Jimmy Yu * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_PXI_BLE_DEVICE (fu_pxi_ble_device_get_type()) G_DECLARE_FINAL_TYPE(FuPxiBleDevice, fu_pxi_ble_device, FU, PXI_BLE_DEVICE, FuHidrawDevice) fwupd-2.0.10/plugins/pixart-rf/fu-pxi-common.c000066400000000000000000000103051501337203100211500ustar00rootroot00000000000000/* * Copyright 2021 Jimmy Yu * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-pxi-common.h" #include "fu-pxi-struct.h" void fu_pxi_ota_fw_state_to_string(struct ota_fw_state *fwstate, guint idt, GString *str) { fwupd_codec_string_append_hex(str, idt, "Status", fwstate->status); fwupd_codec_string_append_hex(str, idt, "NewFlow", fwstate->new_flow); fwupd_codec_string_append_hex(str, idt, "CurrentObjectOffset", fwstate->offset); fwupd_codec_string_append_hex(str, idt, "CurrentChecksum", fwstate->checksum); fwupd_codec_string_append_hex(str, idt, "MaxObjectSize", fwstate->max_object_size); fwupd_codec_string_append_hex(str, idt, "MtuSize", fwstate->mtu_size); fwupd_codec_string_append_hex(str, idt, "PacketReceiptNotificationThreshold", fwstate->prn_threshold); fwupd_codec_string_append( str, idt, "SpecCheckResult", fu_pxi_ota_spec_check_result_to_string(fwstate->spec_check_result)); } gboolean fu_pxi_ota_fw_state_parse(struct ota_fw_state *fwstate, const guint8 *buf, gsize bufsz, gsize offset, GError **error) { if (!fu_memread_uint8_safe(buf, bufsz, offset + 0x00, &fwstate->status, error)) return FALSE; if (!fu_memread_uint8_safe(buf, bufsz, offset + 0x01, &fwstate->new_flow, error)) return FALSE; if (!fu_memread_uint16_safe(buf, bufsz, offset + 0x2, &fwstate->offset, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memread_uint16_safe(buf, bufsz, offset + 0x4, &fwstate->checksum, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memread_uint32_safe(buf, bufsz, offset + 0x06, &fwstate->max_object_size, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memread_uint16_safe(buf, bufsz, offset + 0x0A, &fwstate->mtu_size, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memread_uint16_safe(buf, bufsz, offset + 0x0C, &fwstate->prn_threshold, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memread_uint8_safe(buf, bufsz, offset + 0x0E, &fwstate->spec_check_result, error)) return FALSE; /* success */ return TRUE; } gboolean fu_pxi_composite_receiver_cmd(guint8 opcode, guint8 sn, guint8 target, GByteArray *wireless_mod_cmd, GByteArray *ota_cmd, GError **error) { guint8 checksum = 0x0; guint8 hid_sn = sn; guint8 len = 0x0; guint8 ota_sn = sn + 1; guint8 rf_cmd_code = FU_PXI_BLE_DEVICE_RF_CMD_CODE; guint8 rid = PXI_HID_WIRELESS_DEV_OTA_REPORT_ID; if (ota_cmd == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "ota cmd is NULL"); return FALSE; } /* append ota dispatch header */ fu_byte_array_append_uint8(wireless_mod_cmd, opcode); /* wireless module ota op code */ fu_byte_array_append_uint8(wireless_mod_cmd, ota_sn); /* wireless module ota command sn */ /* append ota command length and content */ for (guint idx = 0; idx < ota_cmd->len; idx++) fu_byte_array_append_uint8(wireless_mod_cmd, ota_cmd->data[idx]); /* append target of wireless module and hid command serial number */ g_byte_array_prepend(wireless_mod_cmd, &target, 0x01); /* target */ g_byte_array_prepend(wireless_mod_cmd, &hid_sn, 0x01); /* hid command sn */ /* prepend length and rf command code, the param len not include "hid_sn" byte and "target" * byte */ len = wireless_mod_cmd->len - 2; g_byte_array_prepend(wireless_mod_cmd, &len, 0x01); g_byte_array_prepend(wireless_mod_cmd, &rf_cmd_code, 0x01); /* command code */ /* prepend checksum */ checksum = fu_sum8(wireless_mod_cmd->data, wireless_mod_cmd->len); g_byte_array_prepend(wireless_mod_cmd, &checksum, 0x01); /* prepend feature report id */ g_byte_array_prepend(wireless_mod_cmd, &rid, 0x01); return TRUE; } gchar * fu_pxi_hpac_version_info_parse(const guint16 hpac_ver) { return g_strdup_printf("%u%u.%u%u.%u", (guint)(hpac_ver / 10000), (guint)((hpac_ver / 1000) % 10), (guint)((hpac_ver / 100) % 10), (guint)((hpac_ver / 10) % 10), (guint)(hpac_ver % 10)); } fwupd-2.0.10/plugins/pixart-rf/fu-pxi-common.h000066400000000000000000000033321501337203100211570ustar00rootroot00000000000000/* * Copyright 2021 Jimmy Yu * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_PXI_DEVICE_FLAG_IS_HPAC "is-hpac" #define PXI_HID_WIRELESS_DEV_OTA_REPORT_ID 0x03 #define FU_PXI_BLE_DEVICE_RF_CMD_CODE 0x65u #define FU_PXI_BLE_DEVICE_RF_CMD_HID_SN 0x0 #define FU_PXI_BLE_DEVICE_RF_CMD_HID_SN 0x0 #define FU_PXI_WIRELESS_DEVICE_TARGET_RECEIVER 0 #define FU_PXI_RECEIVER_DEVICE_OTA_BUF_SZ 64 /* bytes */ #define FU_PXI_DEVICE_MODEL_NAME_LEN 12 /* bytes */ #define FU_PXI_DEVICE_OBJECT_SIZE_MAX 4096 /* bytes */ #define FU_PXI_WIRELESS_DEVICE_RETRY_MAXIMUM 1000 #define FU_PXI_DEVICE_IOCTL_TIMEOUT 5000 /* ms */ /* pixart device model structure */ struct ota_fw_dev_model { guint8 status; guint8 name[FU_PXI_DEVICE_MODEL_NAME_LEN]; guint8 type; guint8 target; guint8 version[5]; guint16 checksum; }; /* pixart fw info structure */ struct ota_fw_info { guint8 status; guint8 version[5]; guint16 checksum; }; struct ota_fw_state { guint8 status; guint8 new_flow; guint16 offset; guint16 checksum; guint32 max_object_size; guint16 mtu_size; guint16 prn_threshold; guint8 spec_check_result; }; gboolean fu_pxi_composite_receiver_cmd(guint8 opcode, guint8 sn, guint8 target, GByteArray *wireless_mod_cmd, GByteArray *ota_cmd, GError **error); void fu_pxi_ota_fw_state_to_string(struct ota_fw_state *fwstate, guint idt, GString *str); gboolean fu_pxi_ota_fw_state_parse(struct ota_fw_state *fwstate, const guint8 *buf, gsize bufsz, gsize offset, GError **error); gchar * fu_pxi_hpac_version_info_parse(const guint16 hpac_ver); fwupd-2.0.10/plugins/pixart-rf/fu-pxi-firmware.c000066400000000000000000000206271501337203100215040ustar00rootroot00000000000000/* * Copyright 2020 Jimmy Yu * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-pxi-common.h" #include "fu-pxi-firmware.h" #define PIXART_RF_FW_HEADER_SIZE 32 /* bytes */ #define PIXART_RF_FW_HEADER_TAG_OFFSET 24 /* The hpac header is start from 821st byte from the end */ #define PIXART_RF_FW_HEADER_HPAC_POS_FROM_END 821 #define PIXART_RF_FW_HEADER_HPAC_VERSION_POS_FROM_END 823 #define PIXART_RF_FW_HEADER_MAGIC 0x55AA55AA55AA55AA struct _FuPxiFirmware { FuFirmware parent_instance; gchar *model_name; gboolean is_hpac; }; G_DEFINE_TYPE(FuPxiFirmware, fu_pxi_firmware, FU_TYPE_FIRMWARE) const gchar * fu_pxi_firmware_get_model_name(FuPxiFirmware *self) { g_return_val_if_fail(FU_IS_PXI_FIRMWARE(self), NULL); return self->model_name; } static void fu_pxi_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuPxiFirmware *self = FU_PXI_FIRMWARE(firmware); fu_xmlb_builder_insert_kv(bn, "model_name", self->model_name); } static gboolean fu_pxi_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { guint64 magic = 0; FuPxiFirmware *self = FU_PXI_FIRMWARE(firmware); gsize streamsz = 0; /* is a footer, in normal bin file, the header is starts from the 32nd byte from the end. */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz < PIXART_RF_FW_HEADER_SIZE) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "stream was too small"); return FALSE; } if (!fu_input_stream_read_u64(stream, streamsz - PIXART_RF_FW_HEADER_SIZE + PIXART_RF_FW_HEADER_TAG_OFFSET, &magic, G_BIG_ENDIAN, error)) { g_prefix_error(error, "failed to read magic: "); return FALSE; } if (magic != PIXART_RF_FW_HEADER_MAGIC) { /* if the magic number is not found, then start from the 821st byte from the end for * HPAC header */ if (!fu_input_stream_read_u64(stream, streamsz - PIXART_RF_FW_HEADER_HPAC_POS_FROM_END + PIXART_RF_FW_HEADER_TAG_OFFSET, &magic, G_BIG_ENDIAN, error)) { g_prefix_error(error, "failed to read magic: "); return FALSE; } if (magic != PIXART_RF_FW_HEADER_MAGIC) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid magic, expected 0x%08X got 0x%08X", (guint32)PIXART_RF_FW_HEADER_MAGIC, (guint32)magic); return FALSE; } self->is_hpac = TRUE; } /* success */ return TRUE; } static gboolean fu_pxi_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuPxiFirmware *self = FU_PXI_FIRMWARE(firmware); gsize streamsz = 0; guint32 version_raw = 0; guint8 fw_header[PIXART_RF_FW_HEADER_SIZE]; guint8 model_name[FU_PXI_DEVICE_MODEL_NAME_LEN] = {0x0}; guint16 hpac_ver = 0; g_autofree gchar *version = NULL; /* get fw header from buf */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (fu_pxi_firmware_is_hpac(self)) { if (streamsz < PIXART_RF_FW_HEADER_HPAC_VERSION_POS_FROM_END) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "image is too small"); return FALSE; } if (!fu_input_stream_read_safe(stream, fw_header, sizeof(fw_header), 0x0, streamsz - PIXART_RF_FW_HEADER_HPAC_POS_FROM_END, sizeof(fw_header), error)) { g_prefix_error(error, "failed to read fw header: "); return FALSE; } } else { if (streamsz < sizeof(fw_header)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "image is too small"); return FALSE; } if (!fu_input_stream_read_safe(stream, fw_header, sizeof(fw_header), 0x0, streamsz - sizeof(fw_header), sizeof(fw_header), error)) { g_prefix_error(error, "failed to read fw header: "); return FALSE; } } fu_dump_raw(G_LOG_DOMAIN, "fw header", fw_header, sizeof(fw_header)); /* set fw version */ if (fu_pxi_firmware_is_hpac(self)) { if (!fu_input_stream_read_u16(stream, streamsz - PIXART_RF_FW_HEADER_HPAC_VERSION_POS_FROM_END, &hpac_ver, G_BIG_ENDIAN, error)) return FALSE; version_raw = hpac_ver; version = fu_pxi_hpac_version_info_parse(hpac_ver); } else { version_raw = (((guint32)(fw_header[0] - '0')) << 16) + (((guint32)(fw_header[2] - '0')) << 8) + (guint32)(fw_header[4] - '0'); version = fu_version_from_uint32(version_raw, FWUPD_VERSION_FORMAT_DELL_BIOS); } fu_firmware_set_version_raw(firmware, version_raw); fu_firmware_set_version(firmware, version); /* nocheck:set-version */ /* set fw model name */ if (!fu_memcpy_safe(model_name, sizeof(model_name), 0x0, fw_header, sizeof(fw_header), 0x05, sizeof(model_name), error)) { g_prefix_error(error, "failed to get fw model name: "); return FALSE; } self->model_name = g_strndup((gchar *)model_name, sizeof(model_name)); /* success */ return TRUE; } static gboolean fu_pxi_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { FuPxiFirmware *self = FU_PXI_FIRMWARE(firmware); const gchar *tmp; /* optional properties */ tmp = xb_node_query_text(n, "model_name", NULL); if (tmp != NULL) self->model_name = g_strdup(tmp); /* success */ return TRUE; } static GByteArray * fu_pxi_firmware_write(FuFirmware *firmware, GError **error) { FuPxiFirmware *self = FU_PXI_FIRMWARE(firmware); guint8 fw_header[PIXART_RF_FW_HEADER_SIZE] = {0x0}; guint64 version_raw = fu_firmware_get_version_raw(firmware); g_autoptr(GByteArray) buf = NULL; g_autoptr(GBytes) blob = NULL; const guint8 tag[] = { 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, }; /* data first */ blob = fu_firmware_get_bytes_with_patches(firmware, error); if (blob == NULL) return NULL; if (fu_pxi_firmware_is_hpac(self)) { buf = g_byte_array_sized_new(g_bytes_get_size(blob)); fu_byte_array_append_bytes(buf, blob); return g_steal_pointer(&buf); } buf = g_byte_array_sized_new(g_bytes_get_size(blob) + sizeof(fw_header)); fu_byte_array_append_bytes(buf, blob); /* footer */ if (!fu_memcpy_safe(fw_header, sizeof(fw_header), PIXART_RF_FW_HEADER_TAG_OFFSET, /* dst */ tag, sizeof(tag), 0x0, /* src */ sizeof(tag), error)) return NULL; fw_header[0] = ((version_raw >> 16) & 0xff) + '0'; fw_header[1] = '.'; fw_header[2] = ((version_raw >> 8) & 0xff) + '0'; fw_header[3] = '.'; fw_header[4] = ((version_raw >> 0) & 0xff) + '0'; if (!g_ascii_isdigit(fw_header[0]) || !g_ascii_isdigit(fw_header[2]) || !g_ascii_isdigit(fw_header[4])) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot write invalid version number 0x%x", (guint)version_raw); return NULL; } if (self->model_name != NULL) { gsize model_namesz = MIN(strlen(self->model_name), FU_PXI_DEVICE_MODEL_NAME_LEN); if (!fu_memcpy_safe(fw_header, sizeof(fw_header), 0x05, /* dst */ (const guint8 *)self->model_name, model_namesz, 0x0, /* src */ model_namesz, error)) { g_prefix_error(error, "failed to get fw model name: "); return NULL; } } g_byte_array_append(buf, fw_header, sizeof(fw_header)); return g_steal_pointer(&buf); } static void fu_pxi_firmware_init(FuPxiFirmware *self) { } static void fu_pxi_firmware_finalize(GObject *object) { FuPxiFirmware *self = FU_PXI_FIRMWARE(object); g_free(self->model_name); G_OBJECT_CLASS(fu_pxi_firmware_parent_class)->finalize(object); } static void fu_pxi_firmware_class_init(FuPxiFirmwareClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_pxi_firmware_finalize; firmware_class->validate = fu_pxi_firmware_validate; firmware_class->parse = fu_pxi_firmware_parse; firmware_class->build = fu_pxi_firmware_build; firmware_class->write = fu_pxi_firmware_write; firmware_class->export = fu_pxi_firmware_export; } FuFirmware * fu_pxi_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_PXI_FIRMWARE, NULL)); } gboolean fu_pxi_firmware_is_hpac(FuPxiFirmware *self) { return self->is_hpac; } fwupd-2.0.10/plugins/pixart-rf/fu-pxi-firmware.h000066400000000000000000000007701501337203100215060ustar00rootroot00000000000000/* * Copyright 2020 Jimmy Yu * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_PXI_DEVICE_MODEL_NAME_LEN 12 /* bytes */ #define FU_TYPE_PXI_FIRMWARE (fu_pxi_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuPxiFirmware, fu_pxi_firmware, FU, PXI_FIRMWARE, FuFirmware) FuFirmware * fu_pxi_firmware_new(void); const gchar * fu_pxi_firmware_get_model_name(FuPxiFirmware *self); gboolean fu_pxi_firmware_is_hpac(FuPxiFirmware *self); fwupd-2.0.10/plugins/pixart-rf/fu-pxi-plugin.c000066400000000000000000000024361501337203100211640ustar00rootroot00000000000000/* * Copyright 2020 Jimmy Yu * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-pxi-ble-device.h" #include "fu-pxi-firmware.h" #include "fu-pxi-plugin.h" #include "fu-pxi-receiver-device.h" #include "fu-pxi-wireless-device.h" struct _FuPxiPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuPxiPlugin, fu_pxi_plugin, FU_TYPE_PLUGIN) static void fu_pxi_plugin_init(FuPxiPlugin *self) { } static void fu_pxi_plugin_object_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_set_name(plugin, "pixart_rf"); } static void fu_pxi_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_udev_subsystem(plugin, "hidraw"); fu_plugin_add_device_gtype(plugin, FU_TYPE_PXI_BLE_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_PXI_RECEIVER_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_PXI_WIRELESS_DEVICE); /* coverage */ fu_plugin_add_firmware_gtype(plugin, "pixart", FU_TYPE_PXI_FIRMWARE); } static void fu_pxi_plugin_class_init(FuPxiPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->constructed = fu_pxi_plugin_object_constructed; plugin_class->constructed = fu_pxi_plugin_constructed; } fwupd-2.0.10/plugins/pixart-rf/fu-pxi-plugin.h000066400000000000000000000003431501337203100211640ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuPxiPlugin, fu_pxi_plugin, FU, PXI_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/pixart-rf/fu-pxi-receiver-device.c000066400000000000000000000661111501337203100227270ustar00rootroot00000000000000/* * Copyright 2020 Jimmy Yu * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-pxi-firmware.h" #include "fu-pxi-receiver-device.h" #include "fu-pxi-struct.h" #include "fu-pxi-wireless-device.h" struct _FuPxiReceiverDevice { FuHidrawDevice parent_instance; struct ota_fw_state fwstate; guint8 sn; }; G_DEFINE_TYPE(FuPxiReceiverDevice, fu_pxi_receiver_device, FU_TYPE_HIDRAW_DEVICE) static void fu_pxi_receiver_device_to_string(FuDevice *device, guint idt, GString *str) { FuPxiReceiverDevice *self = FU_PXI_RECEIVER_DEVICE(device); fu_pxi_ota_fw_state_to_string(&self->fwstate, idt, str); } static FuFirmware * fu_pxi_receiver_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { g_autoptr(FuFirmware) firmware = fu_pxi_firmware_new(); if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; if (fu_device_has_private_flag(device, FU_PXI_DEVICE_FLAG_IS_HPAC) && fu_pxi_firmware_is_hpac(FU_PXI_FIRMWARE(firmware))) { guint32 hpac_fw_size = 0; g_autoptr(GInputStream) stream_new = NULL; if (!fu_input_stream_read_u32(stream, 9, &hpac_fw_size, G_LITTLE_ENDIAN, error)) return NULL; stream_new = fu_partial_input_stream_new(stream, 9, hpac_fw_size + 264, error); if (stream_new == NULL) return NULL; if (!fu_firmware_set_stream(firmware, stream_new, error)) return NULL; } else if (fu_device_has_private_flag(device, FU_PXI_DEVICE_FLAG_IS_HPAC) != fu_pxi_firmware_is_hpac(FU_PXI_FIRMWARE(firmware))) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "The firmware is incompatible with the device"); return NULL; } return g_steal_pointer(&firmware); } static gboolean fu_pxi_receiver_device_fw_ota_init_new(FuPxiReceiverDevice *device, gsize bufsz, GError **error) { guint8 fw_version[10] = {0x0}; g_autoptr(GByteArray) receiver_device_cmd = g_byte_array_new(); g_autoptr(GByteArray) ota_cmd = g_byte_array_new(); FuPxiReceiverDevice *self = FU_PXI_RECEIVER_DEVICE(device); fu_byte_array_append_uint8(ota_cmd, 0X06); /* ota init new command length */ fu_byte_array_append_uint8(ota_cmd, FU_PXI_DEVICE_CMD_FW_OTA_INIT_NEW); /* ota init new op code */ fu_byte_array_append_uint32(ota_cmd, bufsz, G_LITTLE_ENDIAN); /* fw size */ fu_byte_array_append_uint8(ota_cmd, 0x0); /* ota setting */ g_byte_array_append(ota_cmd, fw_version, sizeof(fw_version)); /* ota version */ self->sn++; if (!fu_pxi_composite_receiver_cmd(FU_PXI_DEVICE_CMD_FW_OTA_INIT_NEW, self->sn, FU_PXI_WIRELESS_DEVICE_TARGET_RECEIVER, receiver_device_cmd, ota_cmd, error)) return FALSE; return fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), receiver_device_cmd->data, receiver_device_cmd->len, FU_IOCTL_FLAG_NONE, error); } static gboolean fu_pxi_receiver_device_fw_ota_ini_new_check(FuPxiReceiverDevice *device, GError **error) { FuPxiReceiverDevice *self = FU_PXI_RECEIVER_DEVICE(device); guint8 buf[FU_PXI_RECEIVER_DEVICE_OTA_BUF_SZ] = {0x0}; g_autoptr(GByteArray) receiver_device_cmd = g_byte_array_new(); g_autoptr(GByteArray) ota_cmd = g_byte_array_new(); /* ota command */ fu_byte_array_append_uint8(ota_cmd, 0x1); /* ota command length */ fu_byte_array_append_uint8(ota_cmd, FU_PXI_DEVICE_CMD_FW_OTA_INIT_NEW_CHECK); /* ota command */ self->sn++; /* get pixart wireless module ota command */ if (!fu_pxi_composite_receiver_cmd(FU_PXI_DEVICE_CMD_FW_OTA_INIT_NEW_CHECK, self->sn, FU_PXI_WIRELESS_DEVICE_TARGET_RECEIVER, receiver_device_cmd, ota_cmd, error)) return FALSE; if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), receiver_device_cmd->data, receiver_device_cmd->len, FU_IOCTL_FLAG_NONE, error)) return FALSE; /* delay for wireless module device read command */ fu_device_sleep(FU_DEVICE(device), 5); /* ms */ buf[0] = PXI_HID_WIRELESS_DEV_OTA_REPORT_ID; if (!fu_hidraw_device_get_feature(FU_HIDRAW_DEVICE(self), buf, sizeof(buf), FU_IOCTL_FLAG_NONE, error)) return FALSE; /* shared state */ return fu_pxi_ota_fw_state_parse(&self->fwstate, buf, sizeof(buf), 0x09, error); } static gboolean fu_pxi_receiver_device_get_cmd_response(FuPxiReceiverDevice *self, guint8 *buf, guint bufsz, GError **error) { guint16 retry = 0; while (1) { guint8 sn = 0x0; memset(buf, 0, bufsz); buf[0] = PXI_HID_WIRELESS_DEV_OTA_REPORT_ID; fu_device_sleep(FU_DEVICE(self), 5); /* ms */ if (!fu_hidraw_device_get_feature(FU_HIDRAW_DEVICE(self), buf, bufsz, FU_IOCTL_FLAG_NONE, error)) return FALSE; if (!fu_memread_uint8_safe(buf, bufsz, 0x4, &sn, error)) return FALSE; if (self->sn != sn) retry++; else break; if (retry == FU_PXI_WIRELESS_DEVICE_RETRY_MAXIMUM) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "reach retry maximum, hid sn fail, " "got 0x%04x, expected 0x%04x", sn, self->sn); return FALSE; } } return TRUE; } static gboolean fu_pxi_receiver_device_check_crc(FuDevice *device, guint16 checksum, GError **error) { FuPxiReceiverDevice *self = FU_PXI_RECEIVER_DEVICE(device); guint8 buf[FU_PXI_RECEIVER_DEVICE_OTA_BUF_SZ] = {0x0}; guint8 status = 0x0; g_autoptr(GByteArray) receiver_device_cmd = g_byte_array_new(); g_autoptr(GByteArray) ota_cmd = g_byte_array_new(); /* ota check crc command */ fu_byte_array_append_uint8(ota_cmd, 0x3); /* ota command length */ fu_byte_array_append_uint8(ota_cmd, FU_PXI_DEVICE_CMD_FW_OTA_CHECK_CRC); /* ota command */ fu_byte_array_append_uint16(ota_cmd, checksum, G_LITTLE_ENDIAN); /* checkesum */ /* increase the serial number */ self->sn++; /* get pixart wireless module for ota check crc command */ if (!fu_pxi_composite_receiver_cmd(FU_PXI_DEVICE_CMD_FW_OTA_CHECK_CRC, self->sn, FU_PXI_WIRELESS_DEVICE_TARGET_RECEIVER, receiver_device_cmd, ota_cmd, error)) return FALSE; if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), receiver_device_cmd->data, receiver_device_cmd->len, FU_IOCTL_FLAG_NONE, error)) return FALSE; if (!fu_pxi_receiver_device_get_cmd_response(self, buf, sizeof(buf), error)) return FALSE; if (!fu_memread_uint8_safe(buf, sizeof(buf), 0x5, &status, error)) return FALSE; if (status == FU_PXI_WIRELESS_MODULE_OTA_RSP_CODE_ERROR) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "checksum error: expected 0x%04x", checksum); return FALSE; } /* success */ return TRUE; } static gboolean fu_pxi_receiver_device_fw_object_create(FuDevice *device, FuChunk *chk, GError **error) { FuPxiReceiverDevice *self = FU_PXI_RECEIVER_DEVICE(device); guint8 buf[FU_PXI_RECEIVER_DEVICE_OTA_BUF_SZ] = {0x0}; guint8 status = 0x0; g_autoptr(GByteArray) receiver_device_cmd = g_byte_array_new(); g_autoptr(GByteArray) ota_cmd = g_byte_array_new(); /* ota object create command */ fu_byte_array_append_uint8(ota_cmd, 0x9); /* ota command length */ fu_byte_array_append_uint8(ota_cmd, FU_PXI_DEVICE_CMD_FW_OBJECT_CREATE); /* ota command */ fu_byte_array_append_uint32(ota_cmd, fu_chunk_get_address(chk), G_LITTLE_ENDIAN); fu_byte_array_append_uint32(ota_cmd, fu_chunk_get_data_sz(chk), G_LITTLE_ENDIAN); /* increase the serial number */ self->sn++; /* get pixart wireless module for ota object create command */ if (!fu_pxi_composite_receiver_cmd(FU_PXI_DEVICE_CMD_FW_OBJECT_CREATE, self->sn, FU_PXI_WIRELESS_DEVICE_TARGET_RECEIVER, receiver_device_cmd, ota_cmd, error)) return FALSE; if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), receiver_device_cmd->data, receiver_device_cmd->len, FU_IOCTL_FLAG_NONE, error)) return FALSE; if (!fu_pxi_receiver_device_get_cmd_response(self, buf, sizeof(buf), error)) return FALSE; if (!fu_memread_uint8_safe(buf, sizeof(buf), 0x5, &status, error)) return FALSE; if (status != FU_PXI_WIRELESS_MODULE_OTA_RSP_CODE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "cmd rsp check fail: %s [0x%02x]", fu_pxi_wireless_module_ota_rsp_code_to_string(status), status); return FALSE; } /* success */ return TRUE; } static gboolean fu_pxi_receiver_device_write_payload(FuDevice *device, FuChunk *chk, GError **error) { FuPxiReceiverDevice *self = FU_PXI_RECEIVER_DEVICE(device); g_autoptr(GByteArray) ota_cmd = g_byte_array_new(); g_autoptr(GByteArray) receiver_device_cmd = g_byte_array_new(); /* ota write payload command */ fu_byte_array_append_uint8(ota_cmd, fu_chunk_get_data_sz(chk)); /* ota command length */ g_byte_array_append(ota_cmd, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); /* payload content */ /* increase the serial number */ self->sn++; /* get pixart wireless module for writes payload command */ if (!fu_pxi_composite_receiver_cmd(FU_PXI_DEVICE_CMD_FW_OTA_PAYLOAD_CONTENT, self->sn, FU_PXI_WIRELESS_DEVICE_TARGET_RECEIVER, receiver_device_cmd, ota_cmd, error)) return FALSE; return fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), receiver_device_cmd->data, receiver_device_cmd->len, FU_IOCTL_FLAG_NONE, error); } static gboolean fu_pxi_receiver_device_write_chunk(FuDevice *device, FuChunk *chk, GError **error) { FuPxiReceiverDevice *self = FU_PXI_RECEIVER_DEVICE(device); guint32 prn = 0; g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(GBytes) chk_bytes = fu_chunk_get_bytes(chk); /* send create fw object command */ if (!fu_pxi_receiver_device_fw_object_create(device, chk, error)) return FALSE; /* write payload */ chunks = fu_chunk_array_new_from_bytes(chk_bytes, fu_chunk_get_address(chk), FU_CHUNK_PAGESZ_NONE, self->fwstate.mtu_size); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk2 = NULL; /* prepare chunk */ chk2 = fu_chunk_array_index(chunks, i, error); if (chk2 == NULL) return FALSE; /* calculate checksum of each payload packet */ self->fwstate.checksum += fu_sum16(fu_chunk_get_data(chk2), fu_chunk_get_data_sz(chk2)); if (!fu_pxi_receiver_device_write_payload(device, chk2, error)) return FALSE; prn++; /* check crc at fw when PRN over threshold write or * offset reach max object sz or write offset reach fw length */ if (prn >= self->fwstate.prn_threshold || i == fu_chunk_array_length(chunks) - 1) { if (!fu_pxi_receiver_device_check_crc(device, self->fwstate.checksum, error)) return FALSE; prn = 0; } } /* success */ return TRUE; } static gboolean fu_pxi_receiver_device_fw_upgrade(FuDevice *device, FuFirmware *firmware, FuProgress *progress, GError **error) { FuPxiReceiverDevice *self = FU_PXI_RECEIVER_DEVICE(device); const gchar *version; guint8 fw_version[5] = {0x0}; guint8 res[FU_PXI_RECEIVER_DEVICE_OTA_BUF_SZ] = {0x0}; guint8 result = 0x0; g_autoptr(GByteArray) ota_cmd = g_byte_array_new(); g_autoptr(GByteArray) receiver_device_cmd = g_byte_array_new(); g_autoptr(GBytes) fw = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 5, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 95, NULL); fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* ota fw upgrade command */ fu_byte_array_append_uint8(ota_cmd, 0x0c); /* ota fw upgrade command length */ fu_byte_array_append_uint8( ota_cmd, FU_PXI_DEVICE_CMD_FW_UPGRADE); /* ota fw upgrade command opccode */ fu_byte_array_append_uint32(ota_cmd, g_bytes_get_size(fw), G_LITTLE_ENDIAN); /* ota fw upgrade command fw size */ fu_byte_array_append_uint16(ota_cmd, fu_sum16_bytes(fw), G_LITTLE_ENDIAN); /* ota fw upgrade command checksum */ if (!fu_device_has_private_flag(FU_DEVICE(self), FU_PXI_DEVICE_FLAG_IS_HPAC)) { version = fu_firmware_get_version(firmware); if (!fu_memcpy_safe(fw_version, sizeof(fw_version), 0x0, /* dst */ (guint8 *)version, strlen(version), 0x0, /* src */ sizeof(fw_version), error)) return FALSE; } g_byte_array_append(ota_cmd, fw_version, sizeof(fw_version)); fu_dump_raw(G_LOG_DOMAIN, "ota_cmd ", ota_cmd->data, ota_cmd->len); self->sn++; /* get pixart wireless module ota command */ if (!fu_pxi_composite_receiver_cmd(FU_PXI_DEVICE_CMD_FW_UPGRADE, self->sn, FU_PXI_WIRELESS_DEVICE_TARGET_RECEIVER, receiver_device_cmd, ota_cmd, error)) return FALSE; fu_progress_step_done(progress); /* send ota fw upgrade command */ if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), receiver_device_cmd->data, receiver_device_cmd->len, FU_IOCTL_FLAG_NONE, error)) return FALSE; /* delay for wireless module device read command */ fu_device_sleep(device, 5); /* ms */ if (!fu_pxi_receiver_device_get_cmd_response(self, res, sizeof(res), error)) return FALSE; if (!fu_memread_uint8_safe(res, sizeof(res), 0x5, &result, error)) return FALSE; if (result != FU_PXI_WIRELESS_MODULE_OTA_RSP_CODE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "cmd rsp check fail: %s [0x%02x]", fu_pxi_wireless_module_ota_rsp_code_to_string(result), result); return FALSE; } fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_pxi_receiver_device_reset(FuDevice *device, GError **error) { FuPxiReceiverDevice *self = FU_PXI_RECEIVER_DEVICE(device); g_autoptr(GByteArray) receiver_device_cmd = g_byte_array_new(); g_autoptr(GByteArray) ota_cmd = g_byte_array_new(); /* ota mcu reset command */ fu_byte_array_append_uint8(ota_cmd, 0x1); /* ota mcu reset command */ fu_byte_array_append_uint8(ota_cmd, FU_PXI_DEVICE_CMD_FW_MCU_RESET); /* op code */ fu_byte_array_append_uint8(ota_cmd, FU_PXI_OTA_DISCONNECT_REASON_RESET); self->sn++; /* get pixart wireless module ota command */ if (!fu_pxi_composite_receiver_cmd(FU_PXI_DEVICE_CMD_FW_MCU_RESET, self->sn, FU_PXI_WIRELESS_DEVICE_TARGET_RECEIVER, receiver_device_cmd, ota_cmd, error)) return FALSE; /* send ota mcu reset command */ return fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), receiver_device_cmd->data, receiver_device_cmd->len, FU_IOCTL_FLAG_NONE, error); } static gboolean fu_pxi_receiver_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuPxiReceiverDevice *self = FU_PXI_RECEIVER_DEVICE(device); g_autoptr(GBytes) fw = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 9, "ota-init"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 90, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, NULL); /* get the default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* send fw ota init command */ if (!fu_pxi_receiver_device_fw_ota_init_new(self, g_bytes_get_size(fw), error)) return FALSE; if (!fu_pxi_receiver_device_fw_ota_ini_new_check(self, error)) return FALSE; fu_progress_step_done(progress); chunks = fu_chunk_array_new_from_bytes(fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_PXI_DEVICE_OBJECT_SIZE_MAX); /* prepare write fw into device */ self->fwstate.offset = 0; self->fwstate.checksum = 0; /* write fw into device */ for (guint i = self->fwstate.offset; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_pxi_receiver_device_write_chunk(device, chk, error)) return FALSE; fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)i + self->fwstate.offset + 1, (gsize)fu_chunk_array_length(chunks)); } fu_progress_step_done(progress); /* fw upgrade command */ if (!fu_pxi_receiver_device_fw_upgrade(device, firmware, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* delay for wireless module device read command */ fu_device_sleep(device, 5); /* ms */ /* send device reset command */ if (!fu_pxi_receiver_device_reset(device, error)) return FALSE; fu_progress_step_done(progress); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* success */ return TRUE; } static gboolean fu_pxi_receiver_device_get_peripheral_info(FuPxiReceiverDevice *self, struct ota_fw_dev_model *model, guint idx, GError **error) { guint8 buf[FU_PXI_RECEIVER_DEVICE_OTA_BUF_SZ] = {0x0}; guint16 checksum = 0; guint16 hpac_ver = 0; g_autofree gchar *version_str = NULL; g_autoptr(GByteArray) ota_cmd = g_byte_array_new(); g_autoptr(GByteArray) receiver_device_cmd = g_byte_array_new(); fu_byte_array_append_uint8(ota_cmd, 0x1); /* ota init new command length */ fu_byte_array_append_uint8(ota_cmd, FU_PXI_DEVICE_CMD_FW_OTA_GET_MODEL); fu_byte_array_append_uint8(ota_cmd, idx); self->sn++; if (!fu_pxi_composite_receiver_cmd(FU_PXI_DEVICE_CMD_FW_OTA_GET_MODEL, self->sn, FU_PXI_WIRELESS_DEVICE_TARGET_RECEIVER, receiver_device_cmd, ota_cmd, error)) return FALSE; if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), receiver_device_cmd->data, receiver_device_cmd->len, FU_IOCTL_FLAG_NONE, error)) return FALSE; fu_device_sleep(FU_DEVICE(self), 5); /* ms */ buf[0] = PXI_HID_WIRELESS_DEV_OTA_REPORT_ID; if (!fu_hidraw_device_get_feature(FU_HIDRAW_DEVICE(self), buf, sizeof(buf), FU_IOCTL_FLAG_NONE, error)) return FALSE; fu_dump_raw(G_LOG_DOMAIN, "model_info", buf, sizeof(buf)); if (!fu_memread_uint8_safe(buf, sizeof(buf), 0x9, &model->status, error)) return FALSE; if (!fu_memcpy_safe(model->name, FU_PXI_DEVICE_MODEL_NAME_LEN, 0x0, /* dst */ buf, FU_PXI_RECEIVER_DEVICE_OTA_BUF_SZ, 0xa, /* src */ FU_PXI_DEVICE_MODEL_NAME_LEN, error)) return FALSE; if (!fu_memread_uint8_safe(buf, sizeof(buf), 0x16, &model->type, error)) return FALSE; if (!fu_memread_uint8_safe(buf, sizeof(buf), 0x17, &model->target, error)) return FALSE; if (!fu_memcpy_safe(model->version, 5, 0x0, /* dst */ buf, sizeof(buf), 0x18, /* src */ 5, error)) return FALSE; if (!fu_memread_uint16_safe(buf, sizeof(buf), 0x1D, &checksum, G_LITTLE_ENDIAN, error)) return FALSE; /* set current version and model name */ model->checksum = checksum; g_debug("checksum %x", model->checksum); if (!fu_device_has_private_flag(FU_DEVICE(self), FU_PXI_DEVICE_FLAG_IS_HPAC)) { version_str = g_strndup((gchar *)model->version, 5); } else { if (!fu_memread_uint16_safe(model->version, 5, 3, &hpac_ver, G_BIG_ENDIAN, error)) return FALSE; version_str = fu_pxi_hpac_version_info_parse(hpac_ver); } g_debug("version_str %s", version_str); return TRUE; } static gboolean fu_pxi_receiver_device_get_peripheral_num(FuPxiReceiverDevice *device, guint8 *num_of_models, GError **error) { FuPxiReceiverDevice *self = FU_PXI_RECEIVER_DEVICE(device); guint8 buf[FU_PXI_RECEIVER_DEVICE_OTA_BUF_SZ] = {0x0}; g_autoptr(GByteArray) ota_cmd = g_byte_array_new(); g_autoptr(GByteArray) receiver_device_cmd = g_byte_array_new(); fu_byte_array_append_uint8(ota_cmd, 0x1); /* ota init new command length */ fu_byte_array_append_uint8( ota_cmd, FU_PXI_DEVICE_CMD_FW_OTA_GET_NUM_OF_MODELS); /* ota init new op code */ self->sn++; if (!fu_pxi_composite_receiver_cmd(FU_PXI_DEVICE_CMD_FW_OTA_GET_NUM_OF_MODELS, self->sn, FU_PXI_WIRELESS_DEVICE_TARGET_RECEIVER, receiver_device_cmd, ota_cmd, error)) return FALSE; if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), receiver_device_cmd->data, receiver_device_cmd->len, FU_IOCTL_FLAG_NONE, error)) return FALSE; fu_device_sleep(FU_DEVICE(device), 5); /* ms */ buf[0] = PXI_HID_WIRELESS_DEV_OTA_REPORT_ID; if (!fu_hidraw_device_get_feature(FU_HIDRAW_DEVICE(self), buf, sizeof(buf), FU_IOCTL_FLAG_NONE, error)) return FALSE; fu_dump_raw(G_LOG_DOMAIN, "buf from get model num", buf, sizeof(buf)); if (!fu_memread_uint8_safe(buf, sizeof(buf), 0xA, num_of_models, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_pxi_receiver_device_add_peripherals(FuPxiReceiverDevice *device, guint idx, GError **error) { struct ota_fw_dev_model model = {0x0}; guint16 hpac_ver = 0; g_autofree gchar *model_name = NULL; g_autofree gchar *model_version = NULL; /* get the all wireless peripherals info */ if (!fu_pxi_receiver_device_get_peripheral_info(device, &model, idx, error)) return FALSE; if (!fu_device_has_private_flag(FU_DEVICE(device), FU_PXI_DEVICE_FLAG_IS_HPAC)) { model_version = g_strndup((gchar *)model.version, 5); } else { if (!fu_memread_uint16_safe(model.version, 5, 3, &hpac_ver, G_BIG_ENDIAN, error)) return FALSE; model_version = fu_pxi_hpac_version_info_parse(hpac_ver); } model_name = g_strndup((gchar *)model.name, FU_PXI_DEVICE_MODEL_NAME_LEN); /* idx 0 is for local_device */ if (idx == 0) { fu_device_set_version(FU_DEVICE(device), model_version); fu_device_add_instance_u16(FU_DEVICE(device), "VEN", fu_device_get_vid(FU_DEVICE(device))); fu_device_add_instance_u16(FU_DEVICE(device), "DEV", fu_device_get_pid(FU_DEVICE(device))); fu_device_add_instance_str(FU_DEVICE(device), "MODEL", model_name); if (!fu_device_build_instance_id(FU_DEVICE(device), error, "HIDRAW", "VEN", "DEV", "MODEL", NULL)) return FALSE; } else { FuContext *ctx = fu_device_get_context(FU_DEVICE(device)); g_autoptr(FuPxiWirelessDevice) child = fu_pxi_wireless_device_new(ctx, &model); g_autofree gchar *logical_id = g_strdup_printf("IDX:0x%02x", idx); fu_device_add_instance_u16(FU_DEVICE(child), "VEN", fu_device_get_vid(FU_DEVICE(device))); fu_device_add_instance_u16(FU_DEVICE(child), "DEV", fu_device_get_pid(FU_DEVICE(device))); fu_device_add_instance_str(FU_DEVICE(child), "MODEL", model_name); if (!fu_device_build_instance_id(FU_DEVICE(child), error, "HIDRAW", "VEN", "DEV", "MODEL", NULL)) return FALSE; fu_device_set_name(FU_DEVICE(child), model_name); fu_device_set_version(FU_DEVICE(child), model_version); fu_device_set_logical_id(FU_DEVICE(child), logical_id); fu_device_add_child(FU_DEVICE(device), FU_DEVICE(child)); } return TRUE; } static gboolean fu_pxi_receiver_device_setup_guid(FuPxiReceiverDevice *self, GError **error) { g_autoptr(GString) dev_name = g_string_new(fu_device_get_name(FU_DEVICE(self))); g_string_replace(dev_name, " ", "_", 0); fu_device_add_instance_strup(FU_DEVICE(self), "NAME", dev_name->str); return fu_device_build_instance_id(FU_DEVICE(self), error, "HIDRAW", "VEN", "DEV", "NAME", NULL); } static gboolean fu_pxi_receiver_device_check_peripherals(FuPxiReceiverDevice *device, GError **error) { guint8 num = 0; /* add wireless peripherals */ if (!fu_pxi_receiver_device_get_peripheral_num(device, &num, error)) return FALSE; g_debug("peripheral num: %u", num); for (guint8 idx = 0; idx < num; idx++) { if (!fu_pxi_receiver_device_add_peripherals(device, idx, error)) return FALSE; } return TRUE; } static gboolean fu_pxi_receiver_device_setup(FuDevice *device, GError **error) { FuPxiReceiverDevice *self = FU_PXI_RECEIVER_DEVICE(device); if (!fu_pxi_receiver_device_setup_guid(self, error)) { g_prefix_error(error, "failed to setup GUID: "); return FALSE; } if (!fu_pxi_receiver_device_fw_ota_init_new(self, 0x0000, error)) { g_prefix_error(error, "failed to OTA init new: "); return FALSE; } if (!fu_pxi_receiver_device_fw_ota_ini_new_check(self, error)) { g_prefix_error(error, "failed to OTA init new check: "); return FALSE; } if (!fu_pxi_receiver_device_check_peripherals(self, error)) { g_prefix_error(error, "failed to add wireless module: "); return FALSE; } return TRUE; } static gboolean fu_pxi_receiver_device_probe(FuDevice *device, GError **error) { g_autofree gchar *iface_nr = NULL; g_autoptr(FuDevice) usb_parent = NULL; /* check USB interface number */ usb_parent = fu_device_get_backend_parent_with_subsystem(device, "usb", error); if (usb_parent == NULL) return FALSE; iface_nr = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(usb_parent), "bInterfaceNumber", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, error); if (iface_nr == NULL) return FALSE; if (g_strcmp0(iface_nr, "01") != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "only USB interface 1 supported"); return FALSE; } /* success */ return TRUE; } static void fu_pxi_receiver_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 98, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static void fu_pxi_receiver_device_init(FuPxiReceiverDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_USB_RECEIVER); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_build_vendor_id_u16(FU_DEVICE(self), "USB", 0x093A); fu_device_add_protocol(FU_DEVICE(self), "com.pixart.rf"); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_PXI_FIRMWARE); fu_device_register_private_flag(FU_DEVICE(self), FU_PXI_DEVICE_FLAG_IS_HPAC); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); fu_device_set_remove_delay(FU_DEVICE(self), 10000); } static void fu_pxi_receiver_device_class_init(FuPxiReceiverDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_pxi_receiver_device_to_string; device_class->setup = fu_pxi_receiver_device_setup; device_class->probe = fu_pxi_receiver_device_probe; device_class->write_firmware = fu_pxi_receiver_device_write_firmware; device_class->prepare_firmware = fu_pxi_receiver_device_prepare_firmware; device_class->set_progress = fu_pxi_receiver_device_set_progress; } fwupd-2.0.10/plugins/pixart-rf/fu-pxi-receiver-device.h000066400000000000000000000006331501337203100227310ustar00rootroot00000000000000/* * Copyright 2020 Jimmy Yu * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-pxi-common.h" #define FU_TYPE_PXI_RECEIVER_DEVICE (fu_pxi_receiver_device_get_type()) G_DECLARE_FINAL_TYPE(FuPxiReceiverDevice, fu_pxi_receiver_device, FU, PXI_RECEIVER_DEVICE, FuHidrawDevice) fwupd-2.0.10/plugins/pixart-rf/fu-pxi-wireless-device.c000066400000000000000000000603011501337203100227530ustar00rootroot00000000000000/* * Copyright 2020 Jimmy Yu * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #ifdef HAVE_HIDRAW_H #include #include #endif #include "fu-pxi-common.h" #include "fu-pxi-firmware.h" #include "fu-pxi-receiver-device.h" #include "fu-pxi-struct.h" #include "fu-pxi-wireless-device.h" #define FU_PXI_WIRELESS_DEV_DELAY_MS 50 #define FU_PXI_WIRELESS_DEV_PAYLOAD_DELAY_MS 15 struct _FuPxiWirelessDevice { FuDevice parent_instance; struct ota_fw_state fwstate; guint8 sn; struct ota_fw_dev_model model; }; G_DEFINE_TYPE(FuPxiWirelessDevice, fu_pxi_wireless_device, FU_TYPE_DEVICE) static void fu_pxi_wireless_device_to_string(FuDevice *device, guint idt, GString *str) { FuPxiWirelessDevice *self = FU_PXI_WIRELESS_DEVICE(device); fu_pxi_ota_fw_state_to_string(&self->fwstate, idt, str); fwupd_codec_string_append(str, idt, "ModelName", (gchar *)self->model.name); fwupd_codec_string_append_hex(str, idt, "ModelType", self->model.type); fwupd_codec_string_append_hex(str, idt, "ModelTarget", self->model.target); } static FuPxiReceiverDevice * fu_pxi_wireless_device_get_parent(FuDevice *self, GError **error) { FuDevice *parent = fu_device_get_parent(FU_DEVICE(self)); if (parent == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "no parent set"); return NULL; } return FU_PXI_RECEIVER_DEVICE(FU_UDEV_DEVICE(parent)); } static FuFirmware * fu_pxi_wireless_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuPxiReceiverDevice *parent; g_autoptr(FuFirmware) firmware = fu_pxi_firmware_new(); parent = fu_pxi_wireless_device_get_parent(device, error); if (parent == NULL) return NULL; if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; if (fu_device_has_private_flag(FU_DEVICE(parent), FU_PXI_DEVICE_FLAG_IS_HPAC) && fu_pxi_firmware_is_hpac(FU_PXI_FIRMWARE(firmware))) { guint32 hpac_fw_size = 0; g_autoptr(GInputStream) stream_new = NULL; if (!fu_input_stream_read_u32(stream, 9, &hpac_fw_size, G_LITTLE_ENDIAN, error)) return NULL; stream_new = fu_partial_input_stream_new(stream, 9, hpac_fw_size + 264, error); if (stream_new == NULL) return NULL; if (!fu_firmware_set_stream(firmware, stream_new, error)) return NULL; } else if (fu_device_has_private_flag(FU_DEVICE(parent), FU_PXI_DEVICE_FLAG_IS_HPAC) != fu_pxi_firmware_is_hpac(FU_PXI_FIRMWARE(firmware))) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "The firmware is incompatible with the device"); return NULL; } return g_steal_pointer(&firmware); } static gboolean fu_pxi_wireless_device_get_cmd_response(FuPxiWirelessDevice *device, guint8 *buf, guint bufsz, GError **error) { FuPxiReceiverDevice *parent; guint16 retry = 0; guint8 status = 0x0; parent = fu_pxi_wireless_device_get_parent(FU_DEVICE(device), error); if (parent == NULL) return FALSE; while (1) { guint8 sn = 0x0; memset(buf, 0, bufsz); buf[0] = PXI_HID_WIRELESS_DEV_OTA_REPORT_ID; fu_device_sleep(FU_DEVICE(device), FU_PXI_WIRELESS_DEV_DELAY_MS); /* ms */ if (!fu_hidraw_device_get_feature(FU_HIDRAW_DEVICE(parent), buf, bufsz, FU_IOCTL_FLAG_NONE, error)) return FALSE; if (!fu_memread_uint8_safe(buf, bufsz, 0x4, &sn, error)) return FALSE; if (device->sn != sn) retry++; else break; if (retry == FU_PXI_WIRELESS_DEVICE_RETRY_MAXIMUM) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "reach retry maximum hid sn fail, got 0x%04x, expected 0x%04x", sn, device->sn); return FALSE; } /*if wireless device not reply to receiver, keep to wait */ if (!fu_memread_uint8_safe(buf, bufsz, 0x5, &status, error)) return FALSE; if (status == FU_PXI_WIRELESS_MODULE_OTA_RSP_CODE_NOT_READY) { retry = 0x0; g_debug("FU_PXI_WIRELESS_MODULE_OTA_RSP_CODE_NOT_READY"); } } return TRUE; } static gboolean fu_pxi_wireless_device_check_crc(FuDevice *device, guint16 checksum, GError **error) { FuPxiReceiverDevice *parent; FuPxiWirelessDevice *self = FU_PXI_WIRELESS_DEVICE(device); guint8 buf[FU_PXI_RECEIVER_DEVICE_OTA_BUF_SZ] = {0x0}; guint16 checksum_device = 0x0; guint8 status = 0x0; g_autoptr(GByteArray) receiver_cmd = g_byte_array_new(); g_autoptr(GByteArray) ota_cmd = g_byte_array_new(); /* proxy */ parent = fu_pxi_wireless_device_get_parent(device, error); if (parent == NULL) return FALSE; /* ota check crc command */ fu_byte_array_append_uint8(ota_cmd, 0x3); /* ota command length */ fu_byte_array_append_uint8(ota_cmd, FU_PXI_DEVICE_CMD_FW_OTA_CHECK_CRC); /* ota command */ fu_byte_array_append_uint16(ota_cmd, checksum, G_LITTLE_ENDIAN); /* checkesum */ /* increase the serial number */ self->sn++; if (!fu_pxi_composite_receiver_cmd(FU_PXI_DEVICE_CMD_FW_OTA_CHECK_CRC, self->sn, FU_PXI_WIRELESS_DEVICE_TARGET_RECEIVER, receiver_cmd, ota_cmd, error)) return FALSE; if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(parent), receiver_cmd->data, receiver_cmd->len, FU_IOCTL_FLAG_NONE, error)) return FALSE; if (!fu_pxi_wireless_device_get_cmd_response(self, buf, sizeof(buf), error)) return FALSE; fu_dump_raw(G_LOG_DOMAIN, "crc buf", buf, sizeof(buf)); if (!fu_memread_uint8_safe(buf, sizeof(buf), 0x5, &status, error)) return FALSE; if (!fu_memread_uint16_safe(buf, sizeof(buf), 0x6, &checksum_device, G_LITTLE_ENDIAN, error)) return FALSE; if (status == FU_PXI_WIRELESS_MODULE_OTA_RSP_CODE_ERROR) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "checksum fail, got 0x%04x, expected 0x%04x", checksum_device, checksum); return FALSE; } if (status != FU_PXI_WIRELESS_MODULE_OTA_RSP_CODE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "status:%s", fu_pxi_wireless_module_ota_rsp_code_to_string(status)); return FALSE; } /* success */ return TRUE; } static gboolean fu_pxi_wireless_device_fw_object_create(FuDevice *device, FuChunk *chk, GError **error) { FuPxiReceiverDevice *parent; FuPxiWirelessDevice *self = FU_PXI_WIRELESS_DEVICE(device); guint8 buf[FU_PXI_RECEIVER_DEVICE_OTA_BUF_SZ] = {0x0}; guint8 status = 0x0; g_autoptr(GByteArray) receiver_cmd = g_byte_array_new(); g_autoptr(GByteArray) ota_cmd = g_byte_array_new(); /* proxy */ parent = fu_pxi_wireless_device_get_parent(device, error); if (parent == NULL) return FALSE; /* ota object create command */ fu_byte_array_append_uint8(ota_cmd, 0x9); /* ota command length */ fu_byte_array_append_uint8(ota_cmd, FU_PXI_DEVICE_CMD_FW_OBJECT_CREATE); /* ota command */ fu_byte_array_append_uint32(ota_cmd, fu_chunk_get_address(chk), G_LITTLE_ENDIAN); fu_byte_array_append_uint32(ota_cmd, fu_chunk_get_data_sz(chk), G_LITTLE_ENDIAN); /* increase the serial number */ self->sn++; /* get pixart wireless module for ota object create command */ if (!fu_pxi_composite_receiver_cmd(FU_PXI_DEVICE_CMD_FW_OBJECT_CREATE, self->sn, self->model.target, receiver_cmd, ota_cmd, error)) return FALSE; if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(parent), receiver_cmd->data, receiver_cmd->len, FU_IOCTL_FLAG_NONE, error)) return FALSE; /* delay for wireless module device get command response*/ fu_device_sleep(device, FU_PXI_WIRELESS_DEV_DELAY_MS); if (!fu_pxi_wireless_device_get_cmd_response(self, buf, sizeof(buf), error)) return FALSE; if (!fu_memread_uint8_safe(buf, sizeof(buf), 0x5, &status, error)) return FALSE; if (status != FU_PXI_WIRELESS_MODULE_OTA_RSP_CODE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "cmd rsp check fail: %s [0x%02x]", fu_pxi_wireless_module_ota_rsp_code_to_string(status), status); return FALSE; } /* success */ return TRUE; } static gboolean fu_pxi_wireless_device_write_payload(FuDevice *device, FuChunk *chk, GError **error) { FuPxiReceiverDevice *parent; FuPxiWirelessDevice *self = FU_PXI_WIRELESS_DEVICE(device); g_autoptr(GByteArray) ota_cmd = g_byte_array_new(); g_autoptr(GByteArray) receiver_cmd = g_byte_array_new(); /* proxy */ parent = fu_pxi_wireless_device_get_parent(device, error); if (parent == NULL) return FALSE; /* ota write payload command */ fu_byte_array_append_uint8(ota_cmd, fu_chunk_get_data_sz(chk)); /* ota command length */ g_byte_array_append(ota_cmd, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); /* payload content */ /* increase the serial number */ self->sn++; /* get pixart wireless module for ota write payload command */ if (!fu_pxi_composite_receiver_cmd(FU_PXI_DEVICE_CMD_FW_OTA_PAYLOAD_CONTENT, self->sn, self->model.target, receiver_cmd, ota_cmd, error)) return FALSE; if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(parent), receiver_cmd->data, receiver_cmd->len, FU_IOCTL_FLAG_NONE, error)) return FALSE; /* delay for each payload packet */ fu_device_sleep(device, FU_PXI_WIRELESS_DEV_PAYLOAD_DELAY_MS); /* success */ return TRUE; } static gboolean fu_pxi_wireless_device_write_chunk(FuDevice *device, FuChunk *chk, GError **error) { FuPxiWirelessDevice *self = FU_PXI_WIRELESS_DEVICE(device); guint32 prn = 0; g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(GBytes) chk_bytes = fu_chunk_get_bytes(chk); /* send create fw object command */ if (!fu_pxi_wireless_device_fw_object_create(device, chk, error)) return FALSE; /* write payload */ chunks = fu_chunk_array_new_from_bytes(chk_bytes, fu_chunk_get_address(chk), FU_CHUNK_PAGESZ_NONE, self->fwstate.mtu_size); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk2 = NULL; /* prepare chunk */ chk2 = fu_chunk_array_index(chunks, i, error); if (chk2 == NULL) return FALSE; /* calculate checksum of each payload packet */ self->fwstate.checksum += fu_sum16(fu_chunk_get_data(chk2), fu_chunk_get_data_sz(chk2)); if (!fu_pxi_wireless_device_write_payload(device, chk2, error)) return FALSE; prn++; /* check crc at fw when PRN over threshold write or * offset reach max object sz or write offset reach fw length */ if (prn >= self->fwstate.prn_threshold || i == (fu_chunk_array_length(chunks) - 1)) { if (!fu_pxi_wireless_device_check_crc(device, self->fwstate.checksum, error)) return FALSE; prn = 0; } } /* success */ return TRUE; } static gboolean fu_pxi_wireless_device_fw_ota_preceding(FuDevice *device, GError **error) { FuPxiReceiverDevice *parent; FuPxiWirelessDevice *self = FU_PXI_WIRELESS_DEVICE(device); g_autoptr(GByteArray) ota_cmd = g_byte_array_new(); g_autoptr(GByteArray) receiver_cmd = g_byte_array_new(); /* proxy */ parent = fu_pxi_wireless_device_get_parent(device, error); if (parent == NULL) return FALSE; fu_byte_array_append_uint8(ota_cmd, 0x01); /* ota preceding command length */ fu_byte_array_append_uint8(ota_cmd, FU_PXI_DEVICE_CMD_FW_OTA_PRECEDING); /* ota preceding op code */ /* increase the serial number */ self->sn++; if (!fu_pxi_composite_receiver_cmd(FU_PXI_DEVICE_CMD_FW_OTA_PRECEDING, self->sn, self->model.target, receiver_cmd, ota_cmd, error)) return FALSE; return fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(parent), receiver_cmd->data, receiver_cmd->len, FU_IOCTL_FLAG_NONE, error); } static gboolean fu_pxi_wireless_device_fw_ota_init_new(FuDevice *device, gsize bufsz, GError **error) { FuPxiReceiverDevice *parent; FuPxiWirelessDevice *self = FU_PXI_WIRELESS_DEVICE(device); guint8 buf[FU_PXI_RECEIVER_DEVICE_OTA_BUF_SZ] = {0x0}; guint8 status = 0x0; guint8 fw_version[10] = {0x0}; g_autoptr(GByteArray) ota_cmd = g_byte_array_new(); g_autoptr(GByteArray) receiver_cmd = g_byte_array_new(); /* proxy */ parent = fu_pxi_wireless_device_get_parent(device, error); if (parent == NULL) return FALSE; fu_byte_array_append_uint8(ota_cmd, 0X06); /* ota init new command length */ fu_byte_array_append_uint8(ota_cmd, FU_PXI_DEVICE_CMD_FW_OTA_INIT_NEW); /* ota init new op code */ fu_byte_array_append_uint32(ota_cmd, bufsz, G_LITTLE_ENDIAN); /* fw size */ fu_byte_array_append_uint8(ota_cmd, 0x0); /* ota setting */ g_byte_array_append(ota_cmd, fw_version, sizeof(fw_version)); /* ota version */ /* increase the serial number */ self->sn++; if (!fu_pxi_composite_receiver_cmd(FU_PXI_DEVICE_CMD_FW_OTA_INIT_NEW, self->sn, self->model.target, receiver_cmd, ota_cmd, error)) return FALSE; if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(parent), receiver_cmd->data, receiver_cmd->len, FU_IOCTL_FLAG_NONE, error)) return FALSE; /* delay for wireless module device get command response*/ fu_device_sleep(device, FU_PXI_WIRELESS_DEV_DELAY_MS); if (!fu_pxi_wireless_device_get_cmd_response(self, buf, sizeof(buf), error)) return FALSE; if (!fu_memread_uint8_safe(buf, sizeof(buf), 0x5, &status, error)) return FALSE; if (status != FU_PXI_WIRELESS_MODULE_OTA_RSP_CODE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "cmd rsp check fail: %s [0x%02x]", fu_pxi_wireless_module_ota_rsp_code_to_string(status), status); return FALSE; } /* success */ return TRUE; } static gboolean fu_pxi_wireless_device_fw_ota_ini_new_check(FuDevice *device, GError **error) { FuPxiReceiverDevice *parent; FuPxiWirelessDevice *self = FU_PXI_WIRELESS_DEVICE(device); guint8 buf[FU_PXI_RECEIVER_DEVICE_OTA_BUF_SZ] = {0x0}; guint8 status = 0x0; g_autoptr(GByteArray) ota_cmd = g_byte_array_new(); g_autoptr(GByteArray) receiver_cmd = g_byte_array_new(); /* proxy */ parent = fu_pxi_wireless_device_get_parent(device, error); if (parent == NULL) return FALSE; /* ota command */ fu_byte_array_append_uint8(ota_cmd, 0x1); /* ota command length */ fu_byte_array_append_uint8(ota_cmd, FU_PXI_DEVICE_CMD_FW_OTA_INIT_NEW_CHECK); /* ota command */ /* increase the serial number */ self->sn++; /* get pixart wireless module ota command */ if (!fu_pxi_composite_receiver_cmd(FU_PXI_DEVICE_CMD_FW_OTA_INIT_NEW_CHECK, self->sn, self->model.target, receiver_cmd, ota_cmd, error)) return FALSE; if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(parent), receiver_cmd->data, receiver_cmd->len, FU_IOCTL_FLAG_NONE, error)) return FALSE; /* delay for wireless module device get command response*/ fu_device_sleep(device, FU_PXI_WIRELESS_DEV_DELAY_MS); if (!fu_pxi_wireless_device_get_cmd_response(self, buf, sizeof(buf), error)) return FALSE; if (!fu_memread_uint8_safe(buf, sizeof(buf), 0x5, &status, error)) return FALSE; if (status != FU_PXI_WIRELESS_MODULE_OTA_RSP_CODE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "cmd rsp check fail: %s [0x%02x]", fu_pxi_wireless_module_ota_rsp_code_to_string(status), status); return FALSE; } /* shared state */ return fu_pxi_ota_fw_state_parse(&self->fwstate, buf, sizeof(buf), 0x09, error); } static gboolean fu_pxi_wireless_device_fw_upgrade(FuDevice *device, FuFirmware *firmware, FuProgress *progress, GError **error) { FuPxiReceiverDevice *parent; FuPxiWirelessDevice *self = FU_PXI_WIRELESS_DEVICE(device); guint8 buf[FU_PXI_RECEIVER_DEVICE_OTA_BUF_SZ] = {0x0}; guint8 status = 0x0; const gchar *version; guint8 fw_version[5] = {0x0}; g_autoptr(GByteArray) ota_cmd = g_byte_array_new(); g_autoptr(GByteArray) receiver_cmd = g_byte_array_new(); g_autoptr(GBytes) fw = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 5, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 95, NULL); /* proxy */ parent = fu_pxi_wireless_device_get_parent(device, error); if (parent == NULL) return FALSE; fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* ota fw upgrade command */ fu_byte_array_append_uint8(ota_cmd, 0x0c); /* ota fw upgrade command length */ fu_byte_array_append_uint8( ota_cmd, FU_PXI_DEVICE_CMD_FW_UPGRADE); /* ota fw upgrade command opccode */ fu_byte_array_append_uint32(ota_cmd, g_bytes_get_size(fw), G_LITTLE_ENDIAN); /* ota fw upgrade command fw size */ fu_byte_array_append_uint16(ota_cmd, fu_sum16_bytes(fw), G_LITTLE_ENDIAN); /* ota fw upgrade command checksum */ if (!fu_device_has_private_flag(FU_DEVICE(parent), FU_PXI_DEVICE_FLAG_IS_HPAC)) { version = fu_firmware_get_version(firmware); if (!fu_memcpy_safe(fw_version, sizeof(fw_version), 0x0, /* dst */ (guint8 *)version, strlen(version), 0x0, /* src */ sizeof(fw_version), error)) return FALSE; } g_byte_array_append(ota_cmd, fw_version, sizeof(fw_version)); self->sn++; /* get pixart wireless module ota command */ if (!fu_pxi_composite_receiver_cmd(FU_PXI_DEVICE_CMD_FW_UPGRADE, self->sn, self->model.target, receiver_cmd, ota_cmd, error)) return FALSE; fu_progress_step_done(progress); /* send ota fw upgrade command */ if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(parent), receiver_cmd->data, receiver_cmd->len, FU_IOCTL_FLAG_NONE, error)) return FALSE; /* delay for wireless module device get command response*/ fu_device_sleep(device, FU_PXI_WIRELESS_DEV_DELAY_MS); if (!fu_pxi_wireless_device_get_cmd_response(self, buf, sizeof(buf), error)) return FALSE; if (!fu_memread_uint8_safe(buf, sizeof(buf), 0x5, &status, error)) return FALSE; if (status != FU_PXI_WIRELESS_MODULE_OTA_RSP_CODE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "cmd rsp check fail: %s [0x%02x]", fu_pxi_wireless_module_ota_rsp_code_to_string(status), status); return FALSE; } fu_progress_step_done(progress); return TRUE; } static gboolean fu_pxi_wireless_device_reset(FuDevice *device, GError **error) { FuPxiReceiverDevice *parent; FuPxiWirelessDevice *self = FU_PXI_WIRELESS_DEVICE(device); g_autoptr(GByteArray) ota_cmd = g_byte_array_new(); g_autoptr(GByteArray) receiver_cmd = g_byte_array_new(); /* proxy */ parent = fu_pxi_wireless_device_get_parent(device, error); if (parent == NULL) return FALSE; /* ota mcu reset command */ fu_byte_array_append_uint8(ota_cmd, 0x1); /* ota mcu reset command */ fu_byte_array_append_uint8( ota_cmd, FU_PXI_DEVICE_CMD_FW_MCU_RESET); /* ota mcu reset command op code */ fu_byte_array_append_uint8( ota_cmd, FU_PXI_OTA_DISCONNECT_REASON_RESET); /* ota mcu reset command reason */ self->sn++; /* get pixart wireless module ota command */ if (!fu_pxi_composite_receiver_cmd(FU_PXI_DEVICE_CMD_FW_MCU_RESET, self->sn, self->model.target, receiver_cmd, ota_cmd, error)) return FALSE; /* send ota mcu reset command to device*/ if (!fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(parent), receiver_cmd->data, receiver_cmd->len, FU_IOCTL_FLAG_NONE, error)) return FALSE; self->sn++; /* send ota mcu reset command to receiver */ g_byte_array_set_size(receiver_cmd, 0); if (!fu_pxi_composite_receiver_cmd(FU_PXI_DEVICE_CMD_FW_MCU_RESET, self->sn, FU_PXI_WIRELESS_DEVICE_TARGET_RECEIVER, receiver_cmd, ota_cmd, error)) return FALSE; return fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(parent), receiver_cmd->data, receiver_cmd->len, FU_IOCTL_FLAG_NONE, error); } static gboolean fu_pxi_wireless_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuPxiReceiverDevice *parent; FuPxiWirelessDevice *self = FU_PXI_WIRELESS_DEVICE(device); g_autoptr(GBytes) fw = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 9, "ota-init"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 90, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, NULL); /* get the default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* send preceding command */ if (!fu_pxi_wireless_device_fw_ota_preceding(device, error)) return FALSE; /* send fw ota init command */ if (!fu_pxi_wireless_device_fw_ota_init_new(device, g_bytes_get_size(fw), error)) return FALSE; if (!fu_pxi_wireless_device_fw_ota_ini_new_check(device, error)) return FALSE; fu_progress_step_done(progress); chunks = fu_chunk_array_new_from_bytes(fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_PXI_DEVICE_OBJECT_SIZE_MAX); /* prepare write fw into device */ self->fwstate.offset = 0; self->fwstate.checksum = 0; /* write fw into device */ for (guint i = self->fwstate.offset; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_pxi_wireless_device_write_chunk(device, chk, error)) return FALSE; fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)i + self->fwstate.offset + 1, (gsize)fu_chunk_array_length(chunks)); } fu_progress_step_done(progress); /* fw upgrade command */ if (!fu_pxi_wireless_device_fw_upgrade(device, firmware, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* send device reset command */ fu_device_sleep(device, FU_PXI_WIRELESS_DEV_DELAY_MS); if (!fu_pxi_wireless_device_reset(device, error)) return FALSE; parent = fu_pxi_wireless_device_get_parent(device, error); if (parent == NULL) return FALSE; fu_progress_step_done(progress); fu_device_add_flag(FU_DEVICE(parent), FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* success */ return TRUE; } static void fu_pxi_wireless_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 98, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static void fu_pxi_wireless_device_init(FuPxiWirelessDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_OPEN); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_build_vendor_id_u16(FU_DEVICE(self), "USB", 0x093A); fu_device_add_protocol(FU_DEVICE(self), "com.pixart.rf"); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_PXI_FIRMWARE); fu_device_set_remove_delay(FU_DEVICE(self), 10000); } static void fu_pxi_wireless_device_class_init(FuPxiWirelessDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_pxi_wireless_device_write_firmware; device_class->to_string = fu_pxi_wireless_device_to_string; device_class->prepare_firmware = fu_pxi_wireless_device_prepare_firmware; device_class->set_progress = fu_pxi_wireless_device_set_progress; } FuPxiWirelessDevice * fu_pxi_wireless_device_new(FuContext *ctx, struct ota_fw_dev_model *model) { FuPxiWirelessDevice *self = NULL; self = g_object_new(FU_TYPE_PXI_WIRELESS_DEVICE, "context", ctx, NULL); self->model.status = model->status; for (guint idx = 0; idx < FU_PXI_DEVICE_MODEL_NAME_LEN; idx++) self->model.name[idx] = model->name[idx]; self->model.type = model->type; self->model.target = model->target; self->sn = model->target; return self; } fwupd-2.0.10/plugins/pixart-rf/fu-pxi-wireless-device.h000066400000000000000000000007341501337203100227640ustar00rootroot00000000000000/* * Copyright 2020 Jimmy Yu * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-pxi-common.h" #define FU_TYPE_PXI_WIRELESS_DEVICE (fu_pxi_wireless_device_get_type()) G_DECLARE_FINAL_TYPE(FuPxiWirelessDevice, fu_pxi_wireless_device, FU, PXI_WIRELESS_DEVICE, FuDevice) FuPxiWirelessDevice * fu_pxi_wireless_device_new(FuContext *ctx, struct ota_fw_dev_model *model); fwupd-2.0.10/plugins/pixart-rf/fu-pxi.rs000066400000000000000000000021601501337203100200640ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] enum FuPxiOtaSpecCheckResult { Ok = 1, FwOutOfBounds = 2, ProcessIllegal = 3, Reconnect = 4, FwImgVersionError = 5, DeviceLowBattery = 6, } #[derive(ToString)] enum FuPxiWirelessModuleOtaRspCode { Ok, Finish, Fail, Error, WritePktLenError, WritePktTotalSizeError, ReadPktLenError, NotReady, } enum FuPxiOtaDisconnectReason { CodeJump = 1, UpdateDone = 2, Reset, } // OTA rsp code for wireless module enum FuPxiWirelessModuleType { Mouse, Keyboard, Receiver, } enum FuPxiDeviceCmd { FwOtaInit = 0x10, FwWrite = 0x17, FwUpgrade = 0x18, FwMcuReset = 0x22, FwGetInfo = 0x23, FwObjectCreate = 0x25, FwOtaInitNew = 0x27, FwOtaRetransmit = 0x28, FwOtaDisconnect = 0x29, FwOtaGetNumOfModels = 0x2A, FwOtaGetModel = 0x2B, FwOtaPayloadContent = 0x40, FwOtaCheckCrc = 0x41, FwOtaInitNewCheck = 0x42, FwOtaPreceding = 0x44, } fwupd-2.0.10/plugins/pixart-rf/fu-self-test.c000066400000000000000000000040661501337203100207770ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-pxi-firmware.h" static void fu_pxi_firmware_xml_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autofree gchar *csum1 = NULL; g_autofree gchar *csum2 = NULL; g_autofree gchar *xml_out = NULL; g_autofree gchar *xml_src = NULL; g_autoptr(FuFirmware) firmware1 = fu_pxi_firmware_new(); g_autoptr(FuFirmware) firmware2 = fu_pxi_firmware_new(); g_autoptr(FuFirmware) firmware3 = fu_pxi_firmware_new(); g_autoptr(GBytes) fw = NULL; g_autoptr(GError) error = NULL; /* build and write */ filename = g_test_build_filename(G_TEST_DIST, "tests", "pixart.builder.xml", NULL); ret = g_file_get_contents(filename, &xml_src, NULL, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_firmware_build_from_xml(firmware1, xml_src, &error); g_assert_no_error(error); g_assert_true(ret); fw = fu_firmware_write(firmware1, &error); g_assert_no_error(error); g_assert_nonnull(fw); csum1 = fu_firmware_get_checksum(firmware1, G_CHECKSUM_SHA1, &error); g_assert_no_error(error); g_assert_cmpstr(csum1, ==, "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"); /* ensure we can parse */ ret = fu_firmware_parse_bytes(firmware3, fw, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); /* ensure we can round-trip */ xml_out = fu_firmware_export_to_xml(firmware1, FU_FIRMWARE_EXPORT_FLAG_NONE, &error); g_assert_no_error(error); ret = fu_firmware_build_from_xml(firmware2, xml_out, &error); g_assert_no_error(error); g_assert_true(ret); csum2 = fu_firmware_get_checksum(firmware2, G_CHECKSUM_SHA1, &error); g_assert_cmpstr(csum1, ==, csum2); } int main(int argc, char **argv) { (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); /* tests go here */ g_test_add_func("/pxi/firmware{xml}", fu_pxi_firmware_xml_func); return g_test_run(); } fwupd-2.0.10/plugins/pixart-rf/meson.build000066400000000000000000000027741501337203100204630ustar00rootroot00000000000000if cc.has_header('linux/hidraw.h', required: false) cargs = ['-DG_LOG_DOMAIN="FuPluginPixartRf"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('pixart-rf.quirk') plugin_builtin_pxi = static_library('fu_plugin_pxi', rustgen.process( 'fu-pxi.rs', # fuzzing ), sources: [ 'fu-pxi-plugin.c', 'fu-pxi-common.c', # fuzzing 'fu-pxi-ble-device.c', 'fu-pxi-receiver-device.c', 'fu-pxi-wireless-device.c', 'fu-pxi-firmware.c', # fuzzing ], include_directories: plugin_incdirs, c_args: cargs, dependencies: plugin_deps, link_with: plugin_libs, ) plugin_builtins += plugin_builtin_pxi device_tests += files( 'tests/hp-910-bt-keyboard.json', 'tests/hp-910-bt-mouse.json', 'tests/pixart-rf-2862-dongle.json', 'tests/pixart-rf-GCBBTM25.json', ) if get_option('tests') install_data(['tests/pixart.builder.xml'], install_dir: join_paths(installed_test_datadir, 'tests')) env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) e = executable( 'pxi-self-test', sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: plugin_deps, link_with: [ plugin_libs, plugin_builtin_pxi, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('pxi-self-test', e, env: env) endif endif fwupd-2.0.10/plugins/pixart-rf/pixart-rf.quirk000066400000000000000000000113431501337203100213020ustar00rootroot00000000000000# Pixart 2801 [HIDRAW\VEN_093A&DEV_2801] Plugin = pixart_rf GType = FuPxiBleDevice # Pixart 2860 [HIDRAW\VEN_093A&DEV_2860] Plugin = pixart_rf GType = FuPxiBleDevice # Pixart 2802 [HIDRAW\VEN_093A&DEV_2802] Plugin = pixart_rf GType = FuPxiBleDevice # Pixart 2822 [HIDRAW\VEN_093A&DEV_2822] Plugin = pixart_rf GType = FuPxiBleDevice # Pixart 2861 [HIDRAW\VEN_093A&DEV_2861] Plugin = pixart_rf GType = FuPxiReceiverDevice # Pixart 2862 [HIDRAW\VEN_093A&DEV_2862] Plugin = pixart_rf GType = FuPxiReceiverDevice # Pixart 2452 [HIDRAW\VEN_093A&DEV_2452] Plugin = pixart_rf GType = FuPxiReceiverDevice # Primax Ryder Mouse [HIDRAW\VEN_0461&DEV_4F28] Plugin = pixart_rf GType = FuPxiBleDevice # Primax Mouse [HIDRAW\VEN_0461&DEV_4EEF] Plugin = pixart_rf GType = FuPxiBleDevice # Primax US KB [HIDRAW\VEN_0461&DEV_4EEE] Plugin = pixart_rf GType = FuPxiBleDevice # Primax UK KB [HIDRAW\VEN_0461&DEV_4EF4] Plugin = pixart_rf GType = FuPxiBleDevice # Primax JP KB [HIDRAW\VEN_0461&DEV_4EF5] Plugin = pixart_rf GType = FuPxiBleDevice # Chicony Cheetos keyboard [HIDRAW\VEN_04F2&DEV_2051] Plugin = pixart_rf GType = FuPxiBleDevice # Chicony Cheetos keyboard v2 [HIDRAW\VEN_04F2&DEV_2125] Plugin = pixart_rf GType = FuPxiBleDevice # Chicony Cheetos keyboard UK [HIDRAW\VEN_04F2&DEV_2188] Plugin = pixart_rf GType = FuPxiBleDevice # Chicony Cheetos keyboard v2 UK [HIDRAW\VEN_04F2&DEV_2189] Plugin = pixart_rf GType = FuPxiBleDevice # Chicony Cheetos mouse [HIDRAW\VEN_04F2&DEV_2103] Plugin = pixart_rf GType = FuPxiBleDevice # Elecom Bluetooth Keyboard [HIDRAW\VEN_056E&DEV_1095] Plugin = pixart_rf GType = FuPxiBleDevice # Targus BT Mouse AMB844 [HIDRAW\VEN_1048&DEV_1013] Plugin = pixart_rf GType = FuPxiBleDevice # Targus BT Keyboard AKB872 [HIDRAW\VEN_1048&DEV_3003] Plugin = pixart_rf GType = FuPxiBleDevice # Targus BT Keyboard AKB869 [HIDRAW\VEN_1048&DEV_3004] Plugin = pixart_rf GType = FuPxiBleDevice # AKR123 Bluetooth Keyboard US [HIDRAW\VEN_04F2&DEV_2199] Plugin = pixart_rf GType = FuPxiBleDevice # AKR123 Bluetooth Keyboard UK [HIDRAW\VEN_04F2&DEV_2174] Plugin = pixart_rf GType = FuPxiBleDevice # AMR120 Bluetooth Mouse [HIDRAW\VEN_04F2&DEV_2198] Plugin = pixart_rf GType = FuPxiBleDevice # j5 Wireless Keyboard [HIDRAW\VEN_2DE5&DEV_2125] Plugin = pixart_rf GType = FuPxiBleDevice # j5 Wireless Mouse [HIDRAW\VEN_2DE5&DEV_2103] Plugin = pixart_rf GType = FuPxiBleDevice # CTL Bluetooth Keyboard [HIDRAW\VEN_04F2&DEV_2179] Plugin = pixart_rf GType = FuPxiBleDevice # CTL Bluetooth Mouse [HIDRAW\VEN_04F2&DEV_2178] Plugin = pixart_rf GType = FuPxiBleDevice # MW150BT [HIDRAW\VEN_04F2&DEV_2177] Plugin = pixart_rf GType = FuPxiBleDevice # CKW150BTUK [HIDRAW\VEN_04F2&DEV_2176] Plugin = pixart_rf GType = FuPxiBleDevice # CKW150BTUS [HIDRAW\VEN_04F2&DEV_2175] Plugin = pixart_rf GType = FuPxiBleDevice # Primax Black Mocha KB [HIDRAW\VEN_0461&DEV_4EFA] Plugin = pixart_rf GType = FuPxiBleDevice # Primax Vivaldi2 Mouse [HIDRAW\VEN_03F0&DEV_614A] Plugin = pixart_rf GType = FuPxiBleDevice # HP 320 BTKB [HIDRAW\VEN_0461&DEV_4F01] Plugin = pixart_rf GType = FuPxiBleDevice # Pixart 2403 [HIDRAW\VEN_093A&DEV_2403] Plugin = pixart_rf GType = FuPxiReceiverDevice # Pixart 2463 [HIDRAW\VEN_093A&DEV_2463] Plugin = pixart_rf GType = FuPxiReceiverDevice # Pixart 2832 [HIDRAW\VEN_093A&DEV_2832] Plugin = pixart_rf GType = FuPxiBleDevice # Pixart 2851 [HIDRAW\VEN_093A&DEV_2851] Plugin = pixart_rf GType = FuPxiBleDevice # Pixart 2852 [HIDRAW\VEN_093A&DEV_2852] Plugin = pixart_rf GType = FuPxiBleDevice # Twinkies Mouse [HIDRAW\VEN_03F0&DEV_6D4A] Plugin = pixart_rf GType = FuPxiBleDevice Flags = is-hpac # Twinkies Keyboard [HIDRAW\VEN_03F0&DEV_7C4A] Plugin = pixart_rf GType = FuPxiBleDevice Flags = is-hpac # Twinkies Dongle [HIDRAW\VEN_03F0&DEV_6841] Plugin = pixart_rf GType = FuPxiReceiverDevice Flags = is-hpac # Rata BLE Mouse [HIDRAW\VEN_03F0&DEV_7F4A] Plugin = pixart_rf GType = FuPxiBleDevice Flags = is-hpac # Remi BLE Mouse [HIDRAW\VEN_03F0&DEV_804A] Plugin = pixart_rf GType = FuPxiBleDevice Flags = is-hpac # Pixart 2404 [HIDRAW\VEN_093A&DEV_2404] Plugin = pixart_rf GType = FuPxiReceiverDevice # Pixart 4206 [HIDRAW\VEN_093A&DEV_4206] Plugin = pixart_rf GType = FuPxiReceiverDevice # Pixart 2440 [HIDRAW\VEN_093A&DEV_2440] Plugin = pixart_rf GType = FuPxiReceiverDevice # Pixart 2418 [HIDRAW\VEN_093A&DEV_2418] Plugin = pixart_rf GType = FuPxiReceiverDevice # Pixart 2752 [HIDRAW\VEN_093A&DEV_2752] Plugin = pixart_rf GType = FuPxiBleDevice # Pixart 2840 [HIDRAW\VEN_093A&DEV_2840] Plugin = pixart_rf GType = FuPxiBleDevice # Pixart 2818 [HIDRAW\VEN_093A&DEV_2818] Plugin = pixart_rf GType = FuPxiBleDevice # HP 400/405 [HIDRAW\VEN_03F0&DEV_9E4A] Plugin = pixart_rf GType = FuPxiBleDevice # HP Dongle [HIDRAW\VEN_03F0&DEV_9F4A] Plugin = pixart_rf GType = FuPxiReceiverDevice fwupd-2.0.10/plugins/pixart-rf/tests/000077500000000000000000000000001501337203100174515ustar00rootroot00000000000000fwupd-2.0.10/plugins/pixart-rf/tests/hp-910-bt-keyboard.json000066400000000000000000000005771501337203100234740ustar00rootroot00000000000000{ "name": "HP 910 White BT Keyboard (US)", "interactive": false, "steps": [ { "url": "57a202956b7dedef59e5a396d3593237f0789730426571d2f53df2747ccef860-HP-910-white-bt-keyboard-us-2.2.3.cab", "components": [ { "version": "2.2.3", "guids": [ "7300b3c6-d58f-5527-815d-be4c72753e67" ] } ] } ] } fwupd-2.0.10/plugins/pixart-rf/tests/hp-910-bt-mouse.json000066400000000000000000000005611501337203100230150ustar00rootroot00000000000000{ "name": "HP 910 White BT Mouse", "interactive": false, "steps": [ { "url": "85bf9dca11caefcdc58112e2f0487a8596971e3a078d01ead7547ab792d51378-hp-910-white-bt-mouse-2.2.0.cab", "components": [ { "version": "2.2.0", "guids": [ "3d9c8d7e-6b08-5e16-92bc-bd34ec06174b" ] } ] } ] } fwupd-2.0.10/plugins/pixart-rf/tests/pixart-rf-2862-dongle.json000066400000000000000000000015121501337203100241240ustar00rootroot00000000000000{ "name": "PixArt 2862 Dongle", "interactive": false, "steps": [ { "url": "b2b4e0bbcd4a1f5ec007f5176ff7727d73d7fdb44d0d2836a18b7c2aaaa0aba1-dongle_v0100.cab", "emulation-url": "82b8f88a655960f14f365a857829fb1947560d919a7d5169b046dcf684d43765-dongle_v0100.zip", "components": [ { "version": "1.0.0", "guids": [ "ac997c21-38a5-5907-83f9-c178515be665" ] } ] }, { "url": "66ca80d83ac1e1ccc8da9d4d24e1b73bfc1eeb0421cad54d7c98e3038fe25c92-dongle_v0101.cab", "emulation-url": "7c7add0b4825221c67d823b200b1523c8b5bfa445789864bac0eabd2b11b6c93-dongle_v0101.zip", "components": [ { "version": "1.0.1", "guids": [ "ac997c21-38a5-5907-83f9-c178515be665" ] } ] } ] } fwupd-2.0.10/plugins/pixart-rf/tests/pixart-rf-GCBBTM25.json000066400000000000000000000012621501337203100233640ustar00rootroot00000000000000{ "name": "MOUSE-GCBBTM25", "interactive": false, "steps": [ { "url": "https://www.fwupd.org/downloads/c5b40f0e4a425c7332513e099a4fd8600feb33e1893b590156fafdbbfe8f0929-GCBBTM25_v0001.cab", "components": [ { "version": "0.0.1", "guids": [ "657c927f-1889-596d-a0f1-d2abd9c067ab" ] } ] }, { "url": "https://www.fwupd.org/downloads/b6bfa9c1f47252fb28d67bc3bf65bb9f11e2a3c1a32645450efdbdb64350c3c4-GCBBTM25_v0100.cab", "components": [ { "version": "1.0.0", "guids": [ "657c927f-1889-596d-a0f1-d2abd9c067ab" ] } ] } ] } fwupd-2.0.10/plugins/pixart-rf/tests/pixart.builder.xml000066400000000000000000000002451501337203100231300ustar00rootroot00000000000000 0x00010203 Test aGVsbG8gd29ybGQ= fwupd-2.0.10/plugins/powerd/000077500000000000000000000000001501337203100156735ustar00rootroot00000000000000fwupd-2.0.10/plugins/powerd/README.md000066400000000000000000000007601501337203100171550ustar00rootroot00000000000000--- title: Plugin: Powerd --- ## Introduction This plugin is used to ensure that some updates are not done on battery power and that there is sufficient battery power for certain updates that are. ## Vendor ID Security This protocol does not create a device and thus requires no vendor ID set. ## External interface access This plugin requires access to the `org.chromium.PowerManager` DBus interface. ## Version Considerations This plugin has been available since fwupd version `1.6.2`. fwupd-2.0.10/plugins/powerd/fu-powerd-plugin.c000066400000000000000000000127311501337203100212470ustar00rootroot00000000000000/* * Copyright 2021 Twain Byrnes * Copyright 2021 George Popoola * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-powerd-plugin.h" struct _FuPowerdPlugin { FuPlugin parent_instance; GDBusProxy *proxy; /* nullable */ }; G_DEFINE_TYPE(FuPowerdPlugin, fu_powerd_plugin, FU_TYPE_PLUGIN) typedef enum { FU_POWERD_BATTERY_STATE_UNKNOWN, FU_POWERD_BATTERY_STATE_CHARGING, FU_POWERD_BATTERY_STATE_DISCHARGING, FU_POWERD_BATTERY_STATE_EMPTY, FU_POWERD_BATTERY_STATE_FULLY_CHARGED, } FuPowerdBatteryState; typedef enum { FU_POWERD_EXTERNAL_POWER_UNKNOWN, FU_POWERD_EXTERNAL_POWER_AC, FU_POWERD_EXTERNAL_POWER_USB, FU_POWERD_EXTERNAL_POWER_DISCONNECTED } FuPowerdExternal; static gboolean fu_powerd_plugin_create_suspend_file(GError **error) { g_autofree gchar *lockdir = NULL; g_autofree gchar *inhibitsuspend_filename = NULL; g_autofree gchar *getpid_str = NULL; lockdir = fu_path_from_kind(FU_PATH_KIND_LOCKDIR); inhibitsuspend_filename = g_build_filename(lockdir, "power_override", "fwupd.lock", NULL); getpid_str = g_strdup_printf("%d", getpid()); if (!g_file_set_contents(inhibitsuspend_filename, getpid_str, -1, error)) { g_prefix_error(error, "lock file unable to be created: "); return FALSE; } return TRUE; } static gboolean fu_powerd_plugin_delete_suspend_file(GError **error) { g_autoptr(GError) local_error = NULL; g_autofree gchar *lockdir = NULL; g_autoptr(GFile) inhibitsuspend_file = NULL; lockdir = fu_path_from_kind(FU_PATH_KIND_LOCKDIR); inhibitsuspend_file = g_file_new_build_filename(lockdir, "power_override", "fwupd.lock", NULL); if (!g_file_delete(inhibitsuspend_file, NULL, &local_error) && !g_error_matches(local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { g_propagate_prefixed_error(error, g_steal_pointer(&local_error), "lock file unable to be deleted"); return FALSE; } return TRUE; } static void fu_powerd_plugin_rescan(FuPlugin *plugin, GVariant *parameters) { FuContext *ctx = fu_plugin_get_context(plugin); guint32 power_type; guint32 current_state; gdouble current_level; g_variant_get(parameters, "(uud)", &power_type, ¤t_state, ¤t_level); /* checking if percentage is invalid */ if (current_level < 1 || current_level > 100) current_level = FWUPD_BATTERY_LEVEL_INVALID; fu_context_set_battery_level(ctx, current_level); /* plugged in */ if (power_type == FU_POWERD_EXTERNAL_POWER_AC || power_type == FU_POWERD_EXTERNAL_POWER_USB) { fu_context_set_power_state(ctx, FU_POWER_STATE_AC); return; } if (current_state == FU_POWERD_BATTERY_STATE_FULLY_CHARGED || current_state == FU_POWERD_BATTERY_STATE_CHARGING) fu_context_set_power_state(ctx, FU_POWER_STATE_AC); else fu_context_set_power_state(ctx, FU_POWER_STATE_BATTERY); } static void fu_powerd_plugin_proxy_changed_cb(GDBusProxy *proxy, const gchar *sender_name, const gchar *signal_name, GVariant *parameters, FuPlugin *plugin) { if (!g_str_equal(signal_name, "BatteryStatePoll")) return; fu_powerd_plugin_rescan(plugin, parameters); } static gboolean fu_powerd_plugin_startup(FuPlugin *plugin, FuProgress *progress, GError **error) { FuPowerdPlugin *self = FU_POWERD_PLUGIN(plugin); g_autofree gchar *name_owner = NULL; if (!fu_powerd_plugin_delete_suspend_file(error)) return FALSE; /* establish proxy for method call to powerd */ self->proxy = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, NULL, "org.chromium.PowerManager", "/org/chromium/PowerManager", "org.chromium.PowerManager", NULL, error); if (self->proxy == NULL) { g_prefix_error(error, "failed to connect to powerd: "); return FALSE; } name_owner = g_dbus_proxy_get_name_owner(self->proxy); if (name_owner == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no service that owns the name for %s", g_dbus_proxy_get_name(self->proxy)); return FALSE; } fu_powerd_plugin_rescan(plugin, g_dbus_proxy_call_sync(self->proxy, "GetBatteryState", NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, G_SOURCE_REMOVE)); g_signal_connect(G_DBUS_PROXY(self->proxy), "g-signal", G_CALLBACK(fu_powerd_plugin_proxy_changed_cb), plugin); return TRUE; } static gboolean fu_powerd_plugin_prepare(FuPlugin *plugin, FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { return fu_powerd_plugin_create_suspend_file(error); } static gboolean fu_powerd_plugin_cleanup(FuPlugin *plugin, FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { return fu_powerd_plugin_delete_suspend_file(error); } static void fu_powerd_plugin_init(FuPowerdPlugin *self) { } static void fu_powerd_plugin_finalize(GObject *obj) { FuPowerdPlugin *self = FU_POWERD_PLUGIN(obj); if (self->proxy != NULL) g_object_unref(self->proxy); G_OBJECT_CLASS(fu_powerd_plugin_parent_class)->finalize(obj); } static void fu_powerd_plugin_class_init(FuPowerdPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_powerd_plugin_finalize; plugin_class->startup = fu_powerd_plugin_startup; plugin_class->cleanup = fu_powerd_plugin_cleanup; plugin_class->prepare = fu_powerd_plugin_prepare; } fwupd-2.0.10/plugins/powerd/fu-powerd-plugin.h000066400000000000000000000003541501337203100212520ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuPowerdPlugin, fu_powerd_plugin, FU, POWERD_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/powerd/meson.build000066400000000000000000000004751501337203100200430ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginPowerd"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_builtins += static_library('fu_plugin_powerd', sources: [ 'fu-powerd-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) fwupd-2.0.10/plugins/qc-firehose/000077500000000000000000000000001501337203100166005ustar00rootroot00000000000000fwupd-2.0.10/plugins/qc-firehose/README.md000066400000000000000000000051041501337203100200570ustar00rootroot00000000000000--- title: Plugin: Qc Firehose --- ## Introduction Qualcomm MSM based devices contain a special mode of operation, called Emergency Download Mode. In this mode, the device identifies itself as Qualcomm HS-USB 9008 through USB, and can communicate with a PC host. EDL is implemented by the SoC ROM code (also called PBL). The EDL mode itself implements the Qualcomm Sahara protocol, which accepts an OEM-digitally-signed payload over USB. ## Firmware Format The daemon will decompress the cabinet archive and then extract a zip payload. Inside this payload there must be: * One saraha binary with the `.mbn` extension that is suitable for the target platform * One manifest file matching `rawprogram*.xml` that has sections with `` and `` * Each file referenced by the manifest must also be included This plugin supports the following protocol ID: * `com.qualcomm.firehose` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_05C6&PID_9008` ## Update Behavior The Firehose protocol is really two protocols in a trenchcoat. When the device is plugged in, **it** sends a `Hello` *Sahara* protocol packet which is then acknowledged by the host. The device then requests the sahara binary blob which the host chunks up and sends to the device at the offset requested. Once completed the *first* write phase is complete and we signal to the daemon that another write is required. We then switch to the *mostly* XML-based protocol called Firehose. The plugin then sends a default suggested configuration in XML format to the device, which then sends back what it actually wants. Then the plugin opens the XML manifest and processes each ``, `` and `` section in the specific order. Any `` is also loaded from the archive, but is sent in binary using the *Sahara* protocol rather than with *Firehose*. Finally the device is rebooted, again using Firehose, returning the device to runtime "modem" mode. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x05C6` ## Quirk Use This plugin uses the following plugin-specific quirks: ### `Flags=no-zlp` The device should not have the Zero Length Packet sent. Since: 2.0.7 ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `2.0.7`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Richard Hughes: @hughsie fwupd-2.0.10/plugins/qc-firehose/fu-qc-firehose-impl-common.c000066400000000000000000000037251501337203100240150ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-qc-firehose-impl-common.h" /** * fu_qc_firehose_impl_retry: * @self: (nullable): a #FuQcFirehoseImpl * @timeout_ms: timeout total * @func: (scope caller): a #FuQcFirehoseImplRetryFunc * @user_data: pointer for @func * @error: (nullable): optional return location for an error * * Retry @func up to 100 times, but if the function keeps replying with "timeout" then this will * abort with a failure after @timeout_ms. * * NOTE: we can't use `GTimer` or `g_usleep()` here as we want to do this in ~0 time when emulating, * so keep a counter of the timeout total and assume that @func is limited to 500ms. * * Returns: %TRUE for success **/ gboolean fu_qc_firehose_impl_retry(FuQcFirehoseImpl *self, guint timeout_ms, FuQcFirehoseImplRetryFunc func, gpointer user_data, GError **error) { const guint retry_cnt = 100; const guint retry_timeout = 500; /* ms */ guint total_ms = 0; g_return_val_if_fail(func != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* try up to retry_cnt tries, but always less than timeout_ms */ for (guint i = 0; total_ms < timeout_ms; i++) { gboolean done = FALSE; g_autoptr(GError) error_local = NULL; /* sanity check */ if (i >= retry_cnt) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "retry limit %u reached: ", retry_cnt); return FALSE; } if (!func(self, &done, retry_timeout, user_data, &error_local)) { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT)) { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } g_debug("ignoring: %s", error_local->message); total_ms += retry_timeout; } else if (done) { return TRUE; } } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "timed out after %ums", total_ms); return FALSE; } fwupd-2.0.10/plugins/qc-firehose/fu-qc-firehose-impl-common.h000066400000000000000000000010551501337203100240140ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-qc-firehose-impl.h" typedef gboolean (*FuQcFirehoseImplRetryFunc)(FuQcFirehoseImpl *self, gboolean *done, guint timeout_ms, gpointer user_data, GError **error) G_GNUC_WARN_UNUSED_RESULT; gboolean fu_qc_firehose_impl_retry(FuQcFirehoseImpl *self, guint timeout_ms, FuQcFirehoseImplRetryFunc func, gpointer user_data, GError **error) G_GNUC_NON_NULL(3); fwupd-2.0.10/plugins/qc-firehose/fu-qc-firehose-impl.c000066400000000000000000000643111501337203100225250ustar00rootroot00000000000000/* * Copyright 2021 Quectel Wireless Solutions Co., Ltd. * Ivan Mikhanchuk * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-qc-firehose-impl-common.h" #include "fu-qc-firehose-impl.h" G_DEFINE_INTERFACE(FuQcFirehoseImpl, fu_qc_firehose_impl, G_TYPE_OBJECT) static void fu_qc_firehose_impl_default_init(FuQcFirehoseImplInterface *iface) { } static GByteArray * fu_qc_firehose_impl_read(FuQcFirehoseImpl *self, guint timeout_ms, GError **error) { FuQcFirehoseImplInterface *iface; g_return_val_if_fail(FU_IS_QC_FIREHOSE_IMPL(self), NULL); iface = FU_QC_FIREHOSE_IMPL_GET_IFACE(self); if (iface->read == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "iface->read not implemented"); return NULL; } return (*iface->read)(self, timeout_ms, error); } static gboolean fu_qc_firehose_impl_write(FuQcFirehoseImpl *self, const guint8 *buf, gsize bufsz, guint timeout_ms, GError **error) { FuQcFirehoseImplInterface *iface; g_return_val_if_fail(FU_IS_QC_FIREHOSE_IMPL(self), FALSE); iface = FU_QC_FIREHOSE_IMPL_GET_IFACE(self); if (iface->write == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "iface->write not implemented"); return FALSE; } return (*iface->write)(self, buf, bufsz, timeout_ms, error); } static gboolean fu_qc_firehose_impl_has_function(FuQcFirehoseImpl *self, FuQcFirehoseFunctions func) { FuQcFirehoseImplInterface *iface; g_return_val_if_fail(FU_IS_QC_FIREHOSE_IMPL(self), FALSE); iface = FU_QC_FIREHOSE_IMPL_GET_IFACE(self); if (iface->has_function == NULL) return FALSE; return (*iface->has_function)(self, func); } static void fu_qc_firehose_impl_add_function(FuQcFirehoseImpl *self, FuQcFirehoseFunctions func) { FuQcFirehoseImplInterface *iface; g_return_if_fail(FU_IS_QC_FIREHOSE_IMPL(self)); iface = FU_QC_FIREHOSE_IMPL_GET_IFACE(self); if (iface->add_function == NULL) return; return (*iface->add_function)(self, func); } typedef gboolean (*FuQcFirehoseImplReadFunc)(FuQcFirehoseImpl *self, XbNode *xn, gboolean *done, GError **error) G_GNUC_WARN_UNUSED_RESULT; static gboolean fu_qc_firehose_impl_read_xml_init_log(FuQcFirehoseImpl *self, XbNode *xn, gboolean *done, GError **error) { const gchar *text = xb_node_get_attr(xn, "value"); if (text == NULL) return TRUE; if (g_str_has_prefix(text, "Supported Functions: ")) { g_auto(GStrv) split = g_strsplit(text + 21, " ", -1); for (guint i = 0; split[i] != NULL; i++) { fu_qc_firehose_impl_add_function( self, fu_qc_firehose_functions_from_string(split[i])); } } /* success */ return TRUE; } static gboolean fu_qc_firehose_impl_read_xml_init_cb(FuQcFirehoseImpl *self, XbNode *xn, gboolean *done, GError **error) { g_autoptr(GPtrArray) xn_logs = NULL; /* logs to the console */ xn_logs = xb_node_query(xn, "log", 0, NULL); if (xn_logs != NULL) { for (guint i = 0; i < xn_logs->len; i++) { XbNode *xn_log = g_ptr_array_index(xn_logs, i); if (!fu_qc_firehose_impl_read_xml_init_log(self, xn_log, done, error)) return FALSE; } } /* success */ return TRUE; } static gboolean fu_qc_firehose_impl_read_xml_nop_log(FuQcFirehoseImpl *self, XbNode *xn, gboolean *done, GError **error) { const gchar *text = xb_node_get_attr(xn, "value"); if (text == NULL) return TRUE; if (g_str_has_prefix(text, "INFO: ")) { if (g_str_has_prefix(text + 6, "End of supported functions")) { *done = TRUE; return TRUE; } fu_qc_firehose_impl_add_function(self, fu_qc_firehose_functions_from_string(text + 6)); } /* success */ return TRUE; } static gboolean fu_qc_firehose_impl_read_xml_nop_cb(FuQcFirehoseImpl *self, XbNode *xn, gboolean *done, GError **error) { g_autoptr(GPtrArray) xn_logs = NULL; /* logs to the console */ xn_logs = xb_node_query(xn, "log", 0, NULL); if (xn_logs != NULL) { for (guint i = 0; i < xn_logs->len; i++) { XbNode *xn_log = g_ptr_array_index(xn_logs, i); if (!fu_qc_firehose_impl_read_xml_nop_log(self, xn_log, done, error)) return FALSE; } } /* success */ return TRUE; } typedef struct { FuFirmware *firmware; gboolean no_zlp; gboolean rawmode; guint64 max_payload_size; FuQcFirehoseImplReadFunc read_func; } FuQcFirehoseImplHelper; static gboolean fu_qc_firehose_impl_read_xml_cb(FuQcFirehoseImpl *self, gboolean *done, guint timeout_ms, gpointer user_data, GError **error) { FuQcFirehoseImplHelper *helper = (FuQcFirehoseImplHelper *)user_data; const gchar *tmp; g_autofree gchar *xml = NULL; g_autoptr(GByteArray) buf = NULL; g_autoptr(GPtrArray) xn_logs = NULL; g_autoptr(XbNode) xn_data = NULL; g_autoptr(XbNode) xn_response = NULL; g_autoptr(XbSilo) silo = NULL; buf = fu_qc_firehose_impl_read(self, timeout_ms, error); if (buf == NULL) return FALSE; xml = g_strndup((const gchar *)buf->data, buf->len); if (xml == NULL || xml[0] == '\0') { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no string data"); return FALSE; } g_debug("XML response: %s", xml); silo = xb_silo_new_from_xml(xml, error); if (silo == NULL) { fwupd_error_convert(error); return FALSE; } /* parse response */ xn_data = xb_silo_query_first(silo, "data", error); if (xn_data == NULL) { fwupd_error_convert(error); return FALSE; } /* special case handling */ if (helper->read_func != NULL) return helper->read_func(self, xn_data, done, error); /* logs to the console, no other processing */ xn_logs = xb_node_query(xn_data, "log", 0, NULL); if (xn_logs != NULL) { for (guint i = 0; i < xn_logs->len; i++) { XbNode *xn_log = g_ptr_array_index(xn_logs, i); g_debug("%s", xb_node_get_attr(xn_log, "value")); } } /* from configure */ xn_response = xb_node_query_first(xn_data, "response", NULL); if (xn_response == NULL) return TRUE; /* switch to binary mode? */ tmp = xb_node_get_attr(xn_response, "rawmode"); if (tmp != NULL) { if (g_strcmp0(tmp, "true") == 0) { helper->rawmode = TRUE; } else if (g_strcmp0(tmp, "false") == 0) { helper->rawmode = FALSE; } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid rawmode value: %s", tmp); return FALSE; } } /* device is giving us a better value */ if (g_strcmp0(xb_node_get_attr(xn_response, "value"), "NAK") == 0) { tmp = xb_node_get_attr(xn_response, "MaxPayloadSizeToTargetInBytes"); if (tmp == NULL) { tmp = xb_node_get_attr(xn_response, "MaxPayloadSizeToTargetInBytesSupported"); } if (tmp != NULL) { if (!fu_strtoull(tmp, &helper->max_payload_size, 0x0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "failed to parse MaxPayloadSizeToTargetInBytes:"); return FALSE; } g_debug("max payload size now 0x%x", (guint)helper->max_payload_size); } } /* success */ if (g_strcmp0(xb_node_get_attr(xn_response, "value"), "ACK") != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid data @value, expected ACK and got %s", xb_node_get_attr(xn_response, "value")); return FALSE; } /* success */ *done = TRUE; return TRUE; } static gboolean fu_qc_firehose_impl_read_xml(FuQcFirehoseImpl *self, guint timeout_ms, FuQcFirehoseImplHelper *helper, GError **error) { /* retry a few times */ return fu_qc_firehose_impl_retry(self, timeout_ms, fu_qc_firehose_impl_read_xml_cb, helper, error); } static gboolean fu_qc_firehose_impl_write_xml_xb(FuQcFirehoseImpl *self, gboolean *done, guint timeout_ms, gpointer user_data, GError **error) { GString *xml = (GString *)user_data; /* write XML string to the device */ if (!fu_qc_firehose_impl_write(self, (const guint8 *)xml->str, xml->len, timeout_ms, error)) return FALSE; /* success */ *done = TRUE; return TRUE; } static gboolean fu_qc_firehose_impl_write_xml(FuQcFirehoseImpl *self, XbBuilderNode *bn, GError **error) { g_autoptr(GString) xml_fixed = NULL; g_autofree gchar *xml = NULL; xml = xb_builder_node_export( bn, XB_NODE_EXPORT_FLAG_ADD_HEADER | XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE | XB_NODE_EXPORT_FLAG_FORMAT_INDENT | XB_NODE_EXPORT_FLAG_COLLAPSE_EMPTY, error); if (xml == NULL) return FALSE; xml_fixed = g_string_new(xml); /* firehose is *very* picky about XML and will not accept empty elements */ if (fu_version_compare(xb_version_string(), "0.3.22", FWUPD_VERSION_FORMAT_TRIPLET) < 0) { g_string_replace(xml_fixed, ">\n ", " />", 0); g_string_replace(xml_fixed, ">\n ", " />", 0); g_string_replace(xml_fixed, ">\n ", " />", 0); g_string_replace(xml_fixed, ">\n ", " />", 0); g_string_replace(xml_fixed, ">\n ", " />", 0); g_string_replace(xml_fixed, ">\n ", " />", 0); g_string_replace(xml_fixed, ">\n ", " />", 0); } g_debug("XML request: %s", xml_fixed->str); /* retry a few times */ return fu_qc_firehose_impl_retry(self, 2500, fu_qc_firehose_impl_write_xml_xb, xml_fixed, error); } static gboolean fu_qc_firehose_impl_send_configure(FuQcFirehoseImpl *self, const gchar *storage, gboolean ignore_nak, FuQcFirehoseImplHelper *helper, GError **error) { g_autofree gchar *max_payload_size_str = NULL; g_autoptr(XbBuilderNode) bn = xb_builder_node_new("data"); g_autoptr(GError) error_local = NULL; /* */ max_payload_size_str = g_strdup_printf("%" G_GUINT64_FORMAT, helper->max_payload_size); xb_builder_node_insert_text(bn, "configure", NULL, "MemoryName", storage, "MaxPayloadSizeToTargetInBytes", max_payload_size_str, "Verbose", "0", "ZlpAwareHost", helper->no_zlp ? "0" : "1", "AlwaysValidate", "0", "MaxDigestTableSizeInBytes", "2048", "SkipStorageInit", "0", NULL); if (!fu_qc_firehose_impl_write_xml(self, bn, error)) return FALSE; if (!fu_qc_firehose_impl_read_xml(self, 5000, helper, &error_local)) { /* we're sending our initial suggestion */ if (ignore_nak && g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_debug("ignoring, as we've got updated config: %s", error_local->message); return TRUE; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } /* success */ return TRUE; } static gboolean fu_qc_firehose_impl_configure(FuQcFirehoseImpl *self, const gchar *storage, FuQcFirehoseImplHelper *helper, GError **error) { guint64 max_payload_size_old = helper->max_payload_size; /* sanity check */ if (!fu_qc_firehose_impl_has_function(self, FU_QC_FIREHOSE_FUNCTIONS_CONFIGURE)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "configure is not supported"); return FALSE; } /* retry if remote proposed different size */ if (!fu_qc_firehose_impl_send_configure(self, storage, TRUE, helper, error)) return FALSE; if (max_payload_size_old != helper->max_payload_size) { if (!fu_qc_firehose_impl_send_configure(self, storage, FALSE, helper, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_qc_firehose_impl_erase(FuQcFirehoseImpl *self, XbNode *xn, FuQcFirehoseImplHelper *helper, GError **error) { g_autoptr(XbBuilderNode) bn = xb_builder_node_new("data"); g_autoptr(XbBuilderNode) bc = xb_builder_node_insert(bn, xb_node_get_element(xn), NULL); const gchar *names[] = { "PAGES_PER_BLOCK", "SECTOR_SIZE_IN_BYTES", "num_partition_sectors", "start_sector", }; /* sanity check */ if (!fu_qc_firehose_impl_has_function(self, FU_QC_FIREHOSE_FUNCTIONS_ERASE)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "erase is not supported"); return FALSE; } for (guint i = 0; i < G_N_ELEMENTS(names); i++) { const gchar *value = xb_node_get_attr(xn, names[i]); if (value != NULL) xb_builder_node_set_attr(bc, names[i], value); } if (!fu_qc_firehose_impl_write_xml(self, bn, error)) return FALSE; return fu_qc_firehose_impl_read_xml(self, 30000, helper, error); } static gboolean fu_qc_firehose_impl_write_blocks(FuQcFirehoseImpl *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_qc_firehose_impl_write(self, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 500, error)) return FALSE; /* update progress */ fu_progress_step_done(progress); } /* success */ return TRUE; } static gchar * fu_qc_firehose_impl_convert_to_image_id(const gchar *filename, GError **error) { g_autofree gchar *filename_safe = NULL; /* sanity check */ if (filename == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no firmware value"); return NULL; } filename_safe = g_strdup(filename); g_strdelimit(filename_safe, "\\", '/'); return g_path_get_basename(filename_safe); } static gboolean fu_qc_firehose_impl_program(FuQcFirehoseImpl *self, XbNode *xn, FuQcFirehoseImplHelper *helper, FuProgress *progress, GError **error) { guint64 sector_size = xb_node_get_attr_as_uint(xn, "SECTOR_SIZE_IN_BYTES"); guint64 num_sectors = xb_node_get_attr_as_uint(xn, "num_partition_sectors"); const gchar *filename = xb_node_get_attr(xn, "filename"); g_autofree gchar *filename_basename = NULL; g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(GBytes) blob = NULL; g_autoptr(GBytes) blob_padded = NULL; g_autoptr(XbBuilderNode) bn = xb_builder_node_new("data"); g_autoptr(XbBuilderNode) bc = xb_builder_node_insert(bn, xb_node_get_element(xn), NULL); const gchar *names[] = { "PAGES_PER_BLOCK", "SECTOR_SIZE_IN_BYTES", "filename", "num_partition_sectors", "physical_partition_number", "start_sector", "last_sector", }; /* sanity check */ if (!fu_qc_firehose_impl_has_function(self, FU_QC_FIREHOSE_FUNCTIONS_PROGRAM)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "program is not supported"); return FALSE; } /* skip any empty filenames */ filename_basename = fu_qc_firehose_impl_convert_to_image_id(filename, error); if (filename_basename == NULL) return FALSE; blob = fu_firmware_get_image_by_id_bytes(helper->firmware, filename_basename, error); if (blob == NULL) return FALSE; /* copy across */ for (guint i = 0; i < G_N_ELEMENTS(names); i++) { const gchar *value = xb_node_get_attr(xn, names[i]); if (value != NULL) xb_builder_node_set_attr(bc, names[i], value); } if (!fu_qc_firehose_impl_write_xml(self, bn, error)) return FALSE; if (!fu_qc_firehose_impl_read_xml(self, 2500, helper, error)) { g_prefix_error(error, "failed to setup: "); return FALSE; } /* sanity check */ if (!helper->rawmode) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device did enter rawmode"); return FALSE; } /* the num_partition_sectors is wrong in the autogenerated XML file for some reason */ if (num_sectors * sector_size < g_bytes_get_size(blob)) { g_autofree gchar *num_sectors_str = NULL; num_sectors = g_bytes_get_size(blob) / sector_size; if ((g_bytes_get_size(blob) % sector_size) != 0) num_sectors++; /* we also have to modify what we sent the device... */ g_debug("fixing num_sectors to 0x%x", (guint)num_sectors); num_sectors_str = g_strdup_printf("%u", (guint)num_sectors); xb_builder_node_set_attr(bc, "num_partition_sectors", num_sectors_str); } /* write data */ blob_padded = fu_bytes_pad(blob, num_sectors * sector_size, 0xFF); chunks = fu_chunk_array_new_from_bytes(blob_padded, 0x0, 0x0, helper->max_payload_size); if (!fu_qc_firehose_impl_write_blocks(self, chunks, progress, error)) return FALSE; if (!fu_qc_firehose_impl_read_xml(self, 30000, helper, error)) return FALSE; /* sanity check */ if (helper->rawmode) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device did leave rawmode"); return FALSE; } /* success */ return TRUE; } static gboolean fu_qc_firehose_impl_apply_patch(FuQcFirehoseImpl *self, XbNode *xn, FuQcFirehoseImplHelper *helper, GError **error) { g_autoptr(XbBuilderNode) bn = xb_builder_node_new("data"); g_autoptr(XbBuilderNode) bc = xb_builder_node_insert(bn, xb_node_get_element(xn), NULL); const gchar *names[] = { "SECTOR_SIZE_IN_BYTES", "byte_offset", "filename", "physical_partition_number", "size_in_bytes", "start_sector", "value", }; /* sanity check */ if (!fu_qc_firehose_impl_has_function(self, FU_QC_FIREHOSE_FUNCTIONS_PATCH)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "patch is not supported"); return FALSE; } for (guint i = 0; i < G_N_ELEMENTS(names); i++) { const gchar *value = xb_node_get_attr(xn, names[i]); if (value != NULL) xb_builder_node_set_attr(bc, names[i], value); } if (!fu_qc_firehose_impl_write_xml(self, bn, error)) return FALSE; return fu_qc_firehose_impl_read_xml(self, 5000, helper, error); } static gboolean fu_qc_firehose_impl_set_bootable(FuQcFirehoseImpl *self, guint part, FuQcFirehoseImplHelper *helper, GError **error) { g_autoptr(XbBuilderNode) bn = xb_builder_node_new("data"); g_autofree gchar *partstr = g_strdup_printf("%u", part); /* */ xb_builder_node_insert_text(bn, "setbootablestoragedrive", NULL, "value", partstr, NULL); if (!fu_qc_firehose_impl_write_xml(self, bn, error)) return FALSE; if (!fu_qc_firehose_impl_read_xml(self, 500, helper, error)) { g_prefix_error(error, "failed to mark partition %u as bootable: ", part); return FALSE; } g_debug("partition %u is now bootable", part); return TRUE; } gboolean fu_qc_firehose_impl_reset(FuQcFirehoseImpl *self, GError **error) { g_autoptr(GError) error_local = NULL; g_autoptr(XbBuilderNode) bn = xb_builder_node_new("data"); FuQcFirehoseImplHelper helper = {0x0}; /* message); } else { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } /* success */ return TRUE; } static gboolean fu_qc_firehose_impl_erase_targets(FuQcFirehoseImpl *self, GPtrArray *xns, FuQcFirehoseImplHelper *helper, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, xns->len); /* each action in the list */ for (guint i = 0; i < xns->len; i++) { XbNode *xn = g_ptr_array_index(xns, i); if (!fu_qc_firehose_impl_erase(self, xn, helper, error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_qc_firehose_impl_program_targets(FuQcFirehoseImpl *self, GPtrArray *xns, FuQcFirehoseImplHelper *helper, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, xns->len); /* each action in the list */ for (guint i = 0; i < xns->len; i++) { XbNode *xn = g_ptr_array_index(xns, i); const gchar *filename = xb_node_get_attr(xn, "filename"); if (filename == NULL || g_strcmp0(filename, "") != 0) { if (!fu_qc_firehose_impl_program(self, xn, helper, fu_progress_get_child(progress), error)) return FALSE; } else { g_debug("skipping as filename not provided"); } fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_qc_firehose_impl_patch_targets(FuQcFirehoseImpl *self, GPtrArray *xns, FuQcFirehoseImplHelper *helper, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, xns->len); /* each action in the list */ for (guint i = 0; i < xns->len; i++) { XbNode *xn = g_ptr_array_index(xns, i); if (!fu_qc_firehose_impl_apply_patch(self, xn, helper, error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static guint64 fu_qc_firehose_impl_find_bootable(FuQcFirehoseImpl *self, GPtrArray *xns) { for (guint i = 0; i < xns->len; i++) { XbNode *xn = g_ptr_array_index(xns, i); const gchar *filename = xb_node_get_attr(xn, "filename"); if (filename == NULL) continue; if (g_pattern_match_simple("*xbl.mbn", filename) || g_pattern_match_simple("*xbl_a.mbn", filename) || g_pattern_match_simple("*sbl1.mbn", filename)) { return xb_node_get_attr_as_uint(xn, "physical_partition_number"); } } return G_MAXUINT64; } static gboolean fu_qc_firehose_impl_send_nop(FuQcFirehoseImpl *self, FuQcFirehoseImplHelper *helper, GError **error) { g_autoptr(XbBuilderNode) bn = xb_builder_node_new("data"); /* */ xb_builder_node_insert_text(bn, "nop", NULL, NULL); if (!fu_qc_firehose_impl_write_xml(self, bn, error)) return FALSE; return fu_qc_firehose_impl_read_xml(self, 500, helper, error); } gboolean fu_qc_firehose_impl_setup(FuQcFirehoseImpl *self, GError **error) { FuQcFirehoseImplHelper helper = { .read_func = fu_qc_firehose_impl_read_xml_init_cb, }; g_autoptr(GError) error_local = NULL; /* clear buffer, looking for pending messages */ if (!fu_qc_firehose_impl_read_xml(self, 2000, &helper, &error_local)) { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT)) { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } g_debug("ignoring: %s", error_local->message); } /* no supported functions, poke the device */ if (!fu_qc_firehose_impl_has_function(self, FU_QC_FIREHOSE_FUNCTIONS_CONFIGURE)) { helper.read_func = fu_qc_firehose_impl_read_xml_nop_cb; if (!fu_qc_firehose_impl_send_nop(self, &helper, error)) { g_prefix_error(error, "failed to send NOP: "); return FALSE; } } /* success */ return TRUE; } gboolean fu_qc_firehose_impl_write_firmware(FuQcFirehoseImpl *self, FuFirmware *firmware, gboolean no_zlp, FuProgress *progress, GError **error) { const gchar *fnglob = "firehose-rawprogram.xml|rawprogram_*.xml"; g_autoptr(GBytes) blob = NULL; g_autoptr(GPtrArray) xns_erase = NULL; g_autoptr(GPtrArray) xns_program = NULL; g_autoptr(GPtrArray) xns_patch = NULL; g_autoptr(XbBuilder) builder = xb_builder_new(); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_autoptr(XbSilo) silo = NULL; FuQcFirehoseImplHelper helper = { .no_zlp = no_zlp, .rawmode = FALSE, .max_payload_size = 0x100000, .firmware = firmware, }; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 20, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 80, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "patch"); /* load XML */ blob = fu_firmware_get_image_by_id_bytes(firmware, fnglob, error); if (blob == NULL) { g_prefix_error(error, "failed to find %s: ", fnglob); return FALSE; } if (!xb_builder_source_load_bytes(source, blob, XB_BUILDER_SOURCE_FLAG_NONE, error)) { g_prefix_error(error, "failed to load %s: ", fnglob); fwupd_error_convert(error); return FALSE; } xb_builder_import_source(builder, source); silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, error); if (silo == NULL) { g_prefix_error(error, "failed to compile %s: ", fnglob); fwupd_error_convert(error); return FALSE; } /* hardcode storage */ if (!fu_qc_firehose_impl_configure(self, "nand", &helper, error)) { g_prefix_error(error, "failed to configure: "); return FALSE; } fu_progress_step_done(progress); /* erase */ xns_erase = xb_silo_query(silo, "data/erase", 0, NULL); if (xns_erase != NULL) { if (!fu_qc_firehose_impl_erase_targets(self, xns_erase, &helper, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to erase targets: "); return FALSE; } } fu_progress_step_done(progress); /* program */ xns_program = xb_silo_query(silo, "data/program", 0, NULL); if (xns_program != NULL) { if (!fu_qc_firehose_impl_program_targets(self, xns_program, &helper, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to program targets: "); return FALSE; } } fu_progress_step_done(progress); /* patch */ xns_patch = xb_silo_query(silo, "data/patch", 0, NULL); if (xns_patch != NULL) { if (!fu_qc_firehose_impl_patch_targets(self, xns_patch, &helper, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to patch targets: "); return FALSE; } } fu_progress_step_done(progress); /* find the bootable partition */ if (xns_program != NULL && fu_qc_firehose_impl_has_function(self, FU_QC_FIREHOSE_FUNCTIONS_SETBOOTABLESTORAGEDRIVE)) { guint64 bootable = fu_qc_firehose_impl_find_bootable(self, xns_program); if (bootable != G_MAXUINT64) { g_debug("setting partition %u bootable", (guint)bootable); if (!fu_qc_firehose_impl_set_bootable(self, (guint)bootable, &helper, error)) { g_prefix_error(error, "failed to set bootable: "); return FALSE; } } } /* success */ return TRUE; } fwupd-2.0.10/plugins/qc-firehose/fu-qc-firehose-impl.h000066400000000000000000000023511501337203100225260ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-qc-firehose-struct.h" #define FU_TYPE_QC_FIREHOSE_IMPL (fu_qc_firehose_impl_get_type()) G_DECLARE_INTERFACE(FuQcFirehoseImpl, fu_qc_firehose_impl, FU, QC_FIREHOSE_IMPL, GObject) struct _FuQcFirehoseImplInterface { GTypeInterface g_iface; GByteArray *(*read)(FuQcFirehoseImpl *self, guint timeout_ms, GError **error)G_GNUC_NON_NULL(1); gboolean (*write)(FuQcFirehoseImpl *self, const guint8 *buf, gsize bufsz, guint timeout_ms, GError **error) G_GNUC_NON_NULL(1); gboolean (*has_function)(FuQcFirehoseImpl *self, FuQcFirehoseFunctions func) G_GNUC_NON_NULL(1); void (*add_function)(FuQcFirehoseImpl *self, FuQcFirehoseFunctions func) G_GNUC_NON_NULL(1); }; gboolean fu_qc_firehose_impl_setup(FuQcFirehoseImpl *self, GError **error) G_GNUC_NON_NULL(1); gboolean fu_qc_firehose_impl_write_firmware(FuQcFirehoseImpl *self, FuFirmware *firmware, gboolean no_zlp, FuProgress *progress, GError **error) G_GNUC_NON_NULL(1, 2, 4); gboolean fu_qc_firehose_impl_reset(FuQcFirehoseImpl *self, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/plugins/qc-firehose/fu-qc-firehose-plugin.c000066400000000000000000000016511501337203100230600ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-qc-firehose-plugin.h" #include "fu-qc-firehose-raw-device.h" #include "fu-qc-firehose-usb-device.h" struct _FuQcFirehosePlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuQcFirehosePlugin, fu_qc_firehose_plugin, FU_TYPE_PLUGIN) static void fu_qc_firehose_plugin_init(FuQcFirehosePlugin *self) { } static void fu_qc_firehose_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_QC_FIREHOSE_USB_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_QC_FIREHOSE_RAW_DEVICE); fu_plugin_add_udev_subsystem(plugin, "wwan"); } static void fu_qc_firehose_plugin_class_init(FuQcFirehosePluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_qc_firehose_plugin_constructed; } fwupd-2.0.10/plugins/qc-firehose/fu-qc-firehose-plugin.h000066400000000000000000000003721501337203100230640ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuQcFirehosePlugin, fu_qc_firehose_plugin, FU, QC_FIREHOSE_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/qc-firehose/fu-qc-firehose-raw-device.c000066400000000000000000000167741501337203100236240ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-qc-firehose-impl.h" #include "fu-qc-firehose-raw-device.h" #include "fu-qc-firehose-struct.h" #define FU_QC_FIREHOSE_RAW_DEVICE_RAW_BUFFER_SIZE (4 * 1024) struct _FuQcFirehoseRawDevice { FuUdevDevice parent_instance; FuQcFirehoseFunctions supported_functions; }; static void fu_qc_firehose_raw_device_impl_iface_init(FuQcFirehoseImplInterface *iface); G_DEFINE_TYPE_WITH_CODE(FuQcFirehoseRawDevice, fu_qc_firehose_raw_device, FU_TYPE_UDEV_DEVICE, G_IMPLEMENT_INTERFACE(FU_TYPE_QC_FIREHOSE_IMPL, fu_qc_firehose_raw_device_impl_iface_init)) static void fu_qc_firehose_raw_device_to_string(FuDevice *device, guint idt, GString *str) { FuQcFirehoseRawDevice *self = FU_QC_FIREHOSE_RAW_DEVICE(device); g_autofree gchar *functions = fu_qc_firehose_functions_to_string(self->supported_functions); fwupd_codec_string_append(str, idt, "SupportedFunctions", functions); } static gboolean fu_qc_firehose_raw_device_impl_has_function(FuQcFirehoseImpl *impl, FuQcFirehoseFunctions func) { FuQcFirehoseRawDevice *self = FU_QC_FIREHOSE_RAW_DEVICE(impl); return (self->supported_functions & func) > 0; } static void fu_qc_firehose_raw_device_impl_add_function(FuQcFirehoseImpl *impl, FuQcFirehoseFunctions func) { FuQcFirehoseRawDevice *self = FU_QC_FIREHOSE_RAW_DEVICE(impl); self->supported_functions |= func; } static gboolean fu_qc_firehose_raw_device_impl_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuQcFirehoseRawDevice *self = FU_QC_FIREHOSE_RAW_DEVICE(device); if (self->supported_functions == FU_QC_FIREHOSE_FUNCTIONS_NONE) { if (!fu_qc_firehose_impl_setup(FU_QC_FIREHOSE_IMPL(self), error)) { g_prefix_error(error, "failed to setup before write: "); return FALSE; } } return fu_qc_firehose_impl_write_firmware(FU_QC_FIREHOSE_IMPL(self), firmware, FALSE, progress, error); } static gboolean fu_qc_firehose_raw_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuQcFirehoseRawDevice *self = FU_QC_FIREHOSE_RAW_DEVICE(device); /* if called in recovery we have no supported functions */ if (self->supported_functions == 0 || (self->supported_functions & FU_QC_FIREHOSE_FUNCTIONS_POWER) > 0) { if (!fu_qc_firehose_impl_reset(FU_QC_FIREHOSE_IMPL(self), error)) return FALSE; } /* success */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_qc_firehose_raw_device_probe(FuDevice *device, GError **error) { const gchar *device_file; g_autoptr(FuDevice) pci_parent = NULL; /* sanity check */ device_file = fu_udev_device_get_device_file(FU_UDEV_DEVICE(device)); if (device_file == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no device file"); return FALSE; } if (!g_pattern_match_simple("/dev/wwan*firehose*", device_file)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not a firehose wwan port"); return FALSE; } /* use the PCI parent to set the physical ID */ pci_parent = fu_device_get_backend_parent_with_subsystem(device, "pci", error); if (pci_parent == NULL) return FALSE; if (!fu_device_probe(pci_parent, error)) return FALSE; fu_device_incorporate( device, pci_parent, FU_DEVICE_INCORPORATE_FLAG_SUPERCLASS | FU_DEVICE_INCORPORATE_FLAG_PHYSICAL_ID | FU_DEVICE_INCORPORATE_FLAG_INSTANCE_IDS | FU_DEVICE_INCORPORATE_FLAG_VENDOR_IDS); /* FuUdevDevice->probe */ return FU_DEVICE_CLASS(fu_qc_firehose_raw_device_parent_class)->probe(device, error); } static gboolean fu_qc_firehose_raw_device_setup(FuDevice *device, GError **error) { FuQcFirehoseRawDevice *self = FU_QC_FIREHOSE_RAW_DEVICE(device); /* allow old emulations to run until we merge the new ModemManager plugin */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) return TRUE; return fu_qc_firehose_impl_setup(FU_QC_FIREHOSE_IMPL(self), error); } static void fu_qc_firehose_raw_device_set_progress(FuDevice *device, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 99, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reload"); } static GByteArray * fu_qc_firehose_raw_device_impl_read(FuQcFirehoseImpl *impl, guint timeout_ms, GError **error) { FuQcFirehoseRawDevice *self = FU_QC_FIREHOSE_RAW_DEVICE(impl); gsize actual_len = 0; g_autoptr(GByteArray) buf = g_byte_array_new(); fu_byte_array_set_size(buf, FU_QC_FIREHOSE_RAW_DEVICE_RAW_BUFFER_SIZE, 0x00); if (!fu_udev_device_read(FU_UDEV_DEVICE(self), buf->data, buf->len, &actual_len, timeout_ms, FU_IO_CHANNEL_FLAG_SINGLE_SHOT, error)) { g_prefix_error(error, "failed to do bulk transfer (read): "); return NULL; } g_byte_array_set_size(buf, actual_len); fu_dump_raw(G_LOG_DOMAIN, "rx packet", buf->data, buf->len); return g_steal_pointer(&buf); } static gboolean fu_qc_firehose_raw_device_impl_write(FuQcFirehoseImpl *impl, const guint8 *buf, gsize bufsz, guint timeout_ms, GError **error) { FuQcFirehoseRawDevice *self = FU_QC_FIREHOSE_RAW_DEVICE(impl); return fu_udev_device_write(FU_UDEV_DEVICE(self), buf, bufsz, timeout_ms, FU_IO_CHANNEL_FLAG_FLUSH_INPUT, error); } static void fu_qc_firehose_raw_device_impl_iface_init(FuQcFirehoseImplInterface *iface) { iface->read = fu_qc_firehose_raw_device_impl_read; iface->write = fu_qc_firehose_raw_device_impl_write; iface->has_function = fu_qc_firehose_raw_device_impl_has_function; iface->add_function = fu_qc_firehose_raw_device_impl_add_function; } static void fu_qc_firehose_raw_device_init(FuQcFirehoseRawDevice *self) { fu_device_set_name(FU_DEVICE(self), "Firehose"); fu_device_add_protocol(FU_DEVICE(self), "com.qualcomm.firehose"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_BCD); fu_device_set_version(FU_DEVICE(self), "0.0"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_ARCHIVE_FIRMWARE); fu_device_set_remove_delay(FU_DEVICE(self), 60000); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); } static void fu_qc_firehose_raw_device_class_init(FuQcFirehoseRawDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_qc_firehose_raw_device_to_string; device_class->write_firmware = fu_qc_firehose_raw_device_impl_write_firmware; device_class->set_progress = fu_qc_firehose_raw_device_set_progress; device_class->probe = fu_qc_firehose_raw_device_probe; device_class->setup = fu_qc_firehose_raw_device_setup; device_class->attach = fu_qc_firehose_raw_device_attach; } fwupd-2.0.10/plugins/qc-firehose/fu-qc-firehose-raw-device.h000066400000000000000000000005631501337203100236160ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_QC_FIREHOSE_RAW_DEVICE (fu_qc_firehose_raw_device_get_type()) G_DECLARE_FINAL_TYPE(FuQcFirehoseRawDevice, fu_qc_firehose_raw_device, FU, QC_FIREHOSE_RAW_DEVICE, FuUdevDevice) fwupd-2.0.10/plugins/qc-firehose/fu-qc-firehose-sahara-impl.c000066400000000000000000000175031501337203100237630ustar00rootroot00000000000000/* * Copyright 2021 Quectel Wireless Solutions Co., Ltd. * Ivan Mikhanchuk * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-qc-firehose-sahara-impl.h" #include "fu-qc-firehose-struct.h" G_DEFINE_INTERFACE(FuQcFirehoseSaharaImpl, fu_qc_firehose_sahara_impl, G_TYPE_OBJECT) #define FU_QC_FIREHOSE_SAHARA_IMPL_TIMEOUT_MS 500 static void fu_qc_firehose_sahara_impl_default_init(FuQcFirehoseSaharaImplInterface *iface) { } static GByteArray * fu_qc_firehose_sahara_impl_read(FuQcFirehoseSaharaImpl *self, GError **error) { FuQcFirehoseSaharaImplInterface *iface; g_return_val_if_fail(FU_IS_QC_FIREHOSE_SAHARA_IMPL(self), NULL); iface = FU_QC_FIREHOSE_SAHARA_IMPL_GET_IFACE(self); if (iface->read == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "iface->read not implemented"); return NULL; } return (*iface->read)(self, FU_QC_FIREHOSE_SAHARA_IMPL_TIMEOUT_MS, error); } static gboolean fu_qc_firehose_sahara_impl_write(FuQcFirehoseSaharaImpl *self, const guint8 *buf, gsize bufsz, GError **error) { FuQcFirehoseSaharaImplInterface *iface; g_return_val_if_fail(FU_IS_QC_FIREHOSE_SAHARA_IMPL(self), FALSE); iface = FU_QC_FIREHOSE_SAHARA_IMPL_GET_IFACE(self); if (iface->write == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "iface->write not implemented"); return FALSE; } return (*iface->write)(self, buf, bufsz, FU_QC_FIREHOSE_SAHARA_IMPL_TIMEOUT_MS, error); } static gboolean fu_qc_firehose_sahara_impl_hello(FuQcFirehoseSaharaImpl *self, GByteArray *buf, GError **error) { g_autoptr(FuQcFirehoseSaharaPktHello) st = NULL; g_autoptr(FuQcFirehoseSaharaPktHelloResp) st_resp = fu_qc_firehose_sahara_pkt_hello_resp_new(); /* re-parse and reply */ st = fu_qc_firehose_sahara_pkt_hello_parse(buf->data, buf->len, 0x0, error); if (st == NULL) return FALSE; fu_qc_firehose_sahara_pkt_hello_resp_set_mode(st_resp, fu_qc_firehose_sahara_pkt_hello_get_mode(st)); return fu_qc_firehose_sahara_impl_write(self, st_resp->data, st_resp->len, error); } static gboolean fu_qc_firehose_sahara_impl_read32(FuQcFirehoseSaharaImpl *self, GByteArray *buf, GBytes *blob, GError **error) { g_autoptr(FuQcFirehoseSaharaPktRead) st = NULL; g_autoptr(GBytes) blob_chunk = NULL; /* re-parse and reply */ st = fu_qc_firehose_sahara_pkt_read_parse(buf->data, buf->len, 0x0, error); if (st == NULL) return FALSE; blob_chunk = fu_bytes_new_offset(blob, fu_qc_firehose_sahara_pkt_read_get_offset(st), fu_qc_firehose_sahara_pkt_read_get_length(st), error); if (blob_chunk == NULL) { g_prefix_error(error, "failed to get bootloader chunk: "); return FALSE; } return fu_qc_firehose_sahara_impl_write(self, g_bytes_get_data(blob_chunk, NULL), g_bytes_get_size(blob_chunk), error); } static gboolean fu_qc_firehose_sahara_impl_read64(FuQcFirehoseSaharaImpl *self, GByteArray *buf, GBytes *blob, GError **error) { g_autoptr(FuQcFirehoseSaharaPktRead64) st = NULL; g_autoptr(GBytes) blob_chunk = NULL; /* re-parse and reply */ st = fu_qc_firehose_sahara_pkt_read64_parse(buf->data, buf->len, 0x0, error); if (st == NULL) return FALSE; blob_chunk = fu_bytes_new_offset(blob, fu_qc_firehose_sahara_pkt_read64_get_offset(st), fu_qc_firehose_sahara_pkt_read64_get_length(st), error); if (blob_chunk == NULL) { g_prefix_error(error, "failed to get bootloader chunk: "); return FALSE; } return fu_qc_firehose_sahara_impl_write(self, g_bytes_get_data(blob_chunk, NULL), g_bytes_get_size(blob_chunk), error); } static gboolean fu_qc_firehose_sahara_impl_eoi(FuQcFirehoseSaharaImpl *self, GByteArray *buf, GError **error) { FuQcFirehoseSaharaStatus status; g_autoptr(FuQcFirehoseSaharaPktEndOfImage) st = NULL; g_autoptr(FuQcFirehoseSaharaPktDone) st_resp = fu_qc_firehose_sahara_pkt_done_new(); /* re-parse and reply */ st = fu_qc_firehose_sahara_pkt_end_of_image_parse(buf->data, buf->len, 0x0, error); if (st == NULL) return FALSE; status = fu_qc_firehose_sahara_pkt_end_of_image_get_status(st); if (status != FU_QC_FIREHOSE_SAHARA_STATUS_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid image status for EndOfImage 0x%x: %s", status, fu_qc_firehose_sahara_status_to_string(status)); return FALSE; } return fu_qc_firehose_sahara_impl_write(self, st_resp->data, st_resp->len, error); } static gboolean fu_qc_firehose_sahara_impl_done(FuQcFirehoseSaharaImpl *self, GByteArray *buf, GError **error) { FuQcFirehoseSaharaStatus status; g_autoptr(FuQcFirehoseSaharaPktDoneResp) st = NULL; /* re-parse */ st = fu_qc_firehose_sahara_pkt_done_resp_parse(buf->data, buf->len, 0x0, error); if (st == NULL) return FALSE; status = fu_qc_firehose_sahara_pkt_done_resp_get_status(st); if (status != FU_QC_FIREHOSE_SAHARA_STATUS_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid image status for Done 0x%x: %s", status, fu_qc_firehose_sahara_status_to_string(status)); return FALSE; } return TRUE; } gboolean fu_qc_firehose_sahara_impl_write_firmware(FuQcFirehoseSaharaImpl *self, FuFirmware *firmware, FuProgress *progress, GError **error) { const gchar *fnglob = "firehose-prog.mbn|prog_nand*.mbn|prog_firehose*"; gboolean done = FALSE; g_autoptr(GBytes) blob = NULL; blob = fu_firmware_get_image_by_id_bytes(firmware, fnglob, error); if (blob == NULL) { g_prefix_error(error, "failed to find %s: ", fnglob); return FALSE; } for (guint i = 0; i < G_MAXUINT16 && !done; i++) { FuQcFirehoseSaharaCommandId cmd_id; g_autoptr(FuQcFirehoseSaharaPkt) pkt = NULL; g_autoptr(GByteArray) buf = NULL; buf = fu_qc_firehose_sahara_impl_read(self, error); if (buf == NULL) { g_prefix_error(error, "failed to get device response: "); return FALSE; } /* check if we're already loaded, perhaps from MHI-QCDM */ if (i == 0) { g_autofree gchar *str = fu_strsafe((const gchar *)buf->data, buf->len); if (str != NULL && g_str_has_prefix(str, "data, buf->len, 0x0, error); if (pkt == NULL) return FALSE; if (buf->len != fu_qc_firehose_sahara_pkt_get_hdr_length(pkt)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid packet header"); return FALSE; } /* parse the response */ cmd_id = fu_qc_firehose_sahara_pkt_get_command_id(pkt); switch (cmd_id) { case FU_QC_FIREHOSE_SAHARA_COMMAND_ID_HELLO: if (!fu_qc_firehose_sahara_impl_hello(self, buf, error)) return FALSE; break; case FU_QC_FIREHOSE_SAHARA_COMMAND_ID_READ: if (!fu_qc_firehose_sahara_impl_read32(self, buf, blob, error)) return FALSE; break; case FU_QC_FIREHOSE_SAHARA_COMMAND_ID_END_OF_IMAGE: if (!fu_qc_firehose_sahara_impl_eoi(self, buf, error)) return FALSE; break; case FU_QC_FIREHOSE_SAHARA_COMMAND_ID_DONE_RESPONSE: if (!fu_qc_firehose_sahara_impl_done(self, buf, error)) return FALSE; done = TRUE; break; case FU_QC_FIREHOSE_SAHARA_COMMAND_ID_READ64: if (!fu_qc_firehose_sahara_impl_read64(self, buf, blob, error)) return FALSE; break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid command ID 0x%x: %s", cmd_id, fu_qc_firehose_sahara_command_id_to_string(cmd_id)); return FALSE; } } if (!done) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "transferring sahara never completed"); return FALSE; } /* success */ return TRUE; } fwupd-2.0.10/plugins/qc-firehose/fu-qc-firehose-sahara-impl.h000066400000000000000000000016031501337203100237620ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_QC_FIREHOSE_SAHARA_IMPL (fu_qc_firehose_sahara_impl_get_type()) G_DECLARE_INTERFACE(FuQcFirehoseSaharaImpl, fu_qc_firehose_sahara_impl, FU, QC_FIREHOSE_SAHARA_IMPL, GObject) struct _FuQcFirehoseSaharaImplInterface { GTypeInterface g_iface; GByteArray *(*read)(FuQcFirehoseSaharaImpl *self, guint timeout_ms, GError **error)G_GNUC_NON_NULL(1); gboolean (*write)(FuQcFirehoseSaharaImpl *self, const guint8 *buf, gsize bufsz, guint timeout_ms, GError **error) G_GNUC_NON_NULL(1); }; gboolean fu_qc_firehose_sahara_impl_write_firmware(FuQcFirehoseSaharaImpl *self, FuFirmware *firmware, FuProgress *progress, GError **error) G_GNUC_NON_NULL(1, 2, 3); fwupd-2.0.10/plugins/qc-firehose/fu-qc-firehose-usb-device.c000066400000000000000000000274431501337203100236170ustar00rootroot00000000000000/* * Copyright 2021 Quectel Wireless Solutions Co., Ltd. * Ivan Mikhanchuk * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-qc-firehose-impl.h" #include "fu-qc-firehose-sahara-impl.h" #include "fu-qc-firehose-struct.h" #include "fu-qc-firehose-usb-device.h" #define FU_QC_FIREHOSE_USB_DEVICE_NO_ZLP "no-zlp" #define FU_QC_FIREHOSE_USB_DEVICE_RAW_BUFFER_SIZE (4 * 1024) struct _FuQcFirehoseUsbDevice { FuUsbDevice parent_instance; guint8 ep_in; guint8 ep_out; gsize maxpktsize_in; gsize maxpktsize_out; FuQcFirehoseFunctions supported_functions; }; static void fu_qc_firehose_usb_device_impl_iface_init(FuQcFirehoseImplInterface *iface); static void fu_qc_firehose_usb_device_sahara_impl_iface_init(FuQcFirehoseSaharaImplInterface *iface); G_DEFINE_TYPE_WITH_CODE(FuQcFirehoseUsbDevice, fu_qc_firehose_usb_device, FU_TYPE_USB_DEVICE, G_IMPLEMENT_INTERFACE(FU_TYPE_QC_FIREHOSE_IMPL, fu_qc_firehose_usb_device_impl_iface_init) G_IMPLEMENT_INTERFACE(FU_TYPE_QC_FIREHOSE_SAHARA_IMPL, fu_qc_firehose_usb_device_sahara_impl_iface_init)) static void fu_qc_firehose_usb_device_to_string(FuDevice *device, guint idt, GString *str) { FuQcFirehoseUsbDevice *self = FU_QC_FIREHOSE_USB_DEVICE(device); g_autofree gchar *functions = fu_qc_firehose_functions_to_string(self->supported_functions); fwupd_codec_string_append_hex(str, idt, "EpIn", self->ep_in); fwupd_codec_string_append_hex(str, idt, "EpOut", self->ep_out); fwupd_codec_string_append_hex(str, idt, "MaxpktsizeIn", self->maxpktsize_in); fwupd_codec_string_append_hex(str, idt, "MaxpktsizeOut", self->maxpktsize_out); fwupd_codec_string_append(str, idt, "SupportedFunctions", functions); } static gboolean fu_qc_firehose_usb_device_impl_has_function(FuQcFirehoseImpl *impl, FuQcFirehoseFunctions func) { FuQcFirehoseUsbDevice *self = FU_QC_FIREHOSE_USB_DEVICE(impl); return (self->supported_functions & func) > 0; } static void fu_qc_firehose_usb_device_impl_add_function(FuQcFirehoseImpl *impl, FuQcFirehoseFunctions func) { FuQcFirehoseUsbDevice *self = FU_QC_FIREHOSE_USB_DEVICE(impl); self->supported_functions |= func; } static GByteArray * fu_qc_firehose_usb_device_read(FuQcFirehoseUsbDevice *self, guint timeout_ms, GError **error) { gsize actual_len = 0; g_autoptr(GByteArray) buf = g_byte_array_new(); fu_byte_array_set_size(buf, FU_QC_FIREHOSE_USB_DEVICE_RAW_BUFFER_SIZE, 0x00); if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), self->ep_in, buf->data, buf->len, &actual_len, timeout_ms, NULL, error)) { g_prefix_error(error, "failed to do bulk transfer (read): "); return NULL; } g_byte_array_set_size(buf, actual_len); fu_dump_raw(G_LOG_DOMAIN, "rx packet", buf->data, buf->len); return g_steal_pointer(&buf); } static gboolean fu_qc_firehose_usb_device_write(FuQcFirehoseUsbDevice *self, const guint8 *buf, gsize sz, guint timeout_ms, GError **error) { gsize actual_len = 0; g_autoptr(GPtrArray) chunks = NULL; g_autoptr(GByteArray) bufmut = g_byte_array_sized_new(sz); /* copy const data to mutable GByteArray */ g_byte_array_append(bufmut, buf, sz); chunks = fu_chunk_array_mutable_new(bufmut->data, bufmut->len, 0, 0, self->maxpktsize_out); if (chunks->len > 1) g_debug("split into %u chunks", chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); fu_dump_raw(G_LOG_DOMAIN, "tx packet", fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), self->ep_out, fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), &actual_len, timeout_ms, NULL, error)) { g_prefix_error(error, "failed to do bulk transfer (write data): "); return FALSE; } if (actual_len != fu_chunk_get_data_sz(chk)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "only wrote %" G_GSIZE_FORMAT "bytes", actual_len); return FALSE; } } /* sent zlp packet if needed */ if (!fu_device_has_private_flag(FU_DEVICE(self), FU_QC_FIREHOSE_USB_DEVICE_NO_ZLP) && sz % self->maxpktsize_out == 0) { if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), self->ep_out, NULL, 0, NULL, timeout_ms, NULL, error)) { g_prefix_error(error, "failed to do bulk transfer (write zlp): "); return FALSE; } } /* success */ return TRUE; } static void fu_qc_firehose_usb_device_parse_eps(FuQcFirehoseUsbDevice *self, GPtrArray *endpoints) { for (guint i = 0; i < endpoints->len; i++) { FuUsbEndpoint *ep = g_ptr_array_index(endpoints, i); if (fu_usb_endpoint_get_direction(ep) == FU_USB_DIRECTION_DEVICE_TO_HOST) { self->ep_in = fu_usb_endpoint_get_address(ep); self->maxpktsize_in = fu_usb_endpoint_get_maximum_packet_size(ep); } else { self->ep_out = fu_usb_endpoint_get_address(ep); self->maxpktsize_out = fu_usb_endpoint_get_maximum_packet_size(ep); } } } static gboolean fu_qc_firehose_usb_device_probe(FuDevice *device, GError **error) { FuQcFirehoseUsbDevice *self = FU_QC_FIREHOSE_USB_DEVICE(device); g_autoptr(GPtrArray) intfs = NULL; /* most devices have a BCD version of 0.0 (i.e. unset), but we still want to show the * device in gnome-firmware -- allow overwriting if the descriptor has something better */ fu_device_set_version(device, "0.0"); /* FuUsbDevice->probe */ if (!FU_DEVICE_CLASS(fu_qc_firehose_usb_device_parent_class)->probe(device, error)) return FALSE; /* parse usb interfaces and find suitable endpoints */ intfs = fu_usb_device_get_interfaces(FU_USB_DEVICE(self), error); if (intfs == NULL) return FALSE; for (guint i = 0; i < intfs->len; i++) { FuUsbInterface *intf = g_ptr_array_index(intfs, i); if (fu_usb_interface_get_class(intf) == 0xFF && fu_usb_interface_get_subclass(intf) == 0xFF && (fu_usb_interface_get_protocol(intf) == 0xFF || fu_usb_interface_get_protocol(intf) == 0x11)) { g_autoptr(GPtrArray) endpoints = fu_usb_interface_get_endpoints(intf); if (endpoints == NULL || endpoints->len == 0) continue; fu_qc_firehose_usb_device_parse_eps(self, endpoints); fu_usb_device_add_interface(FU_USB_DEVICE(self), fu_usb_interface_get_number(intf)); return TRUE; } } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no update interface found"); return FALSE; } static gboolean fu_qc_firehose_usb_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuQcFirehoseUsbDevice *self = FU_QC_FIREHOSE_USB_DEVICE(device); /* if called in recovery we have no supported functions */ if (self->supported_functions == 0 || (self->supported_functions & FU_QC_FIREHOSE_FUNCTIONS_POWER) > 0) { if (!fu_qc_firehose_impl_reset(FU_QC_FIREHOSE_IMPL(self), error)) return FALSE; } /* success */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_qc_firehose_usb_device_impl_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuQcFirehoseUsbDevice *self = FU_QC_FIREHOSE_USB_DEVICE(device); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 5, "sahara"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 95, "firehose"); /* load the sahara binary */ if (!fu_qc_firehose_sahara_impl_write_firmware(FU_QC_FIREHOSE_SAHARA_IMPL(self), firmware, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* use firehose XML */ if (!fu_qc_firehose_impl_setup(FU_QC_FIREHOSE_IMPL(self), error)) return FALSE; if (!fu_qc_firehose_impl_write_firmware( FU_QC_FIREHOSE_IMPL(self), firmware, fu_device_has_private_flag(device, FU_QC_FIREHOSE_USB_DEVICE_NO_ZLP), fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static void fu_qc_firehose_usb_device_replace(FuDevice *device, FuDevice *donor) { if (fu_device_has_private_flag(donor, FU_QC_FIREHOSE_USB_DEVICE_NO_ZLP)) fu_device_add_private_flag(device, FU_QC_FIREHOSE_USB_DEVICE_NO_ZLP); } static void fu_qc_firehose_usb_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 99, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reload"); } static GByteArray * fu_qc_firehose_usb_device_sahara_impl_read(FuQcFirehoseSaharaImpl *impl, guint timeout_ms, GError **error) { FuQcFirehoseUsbDevice *self = FU_QC_FIREHOSE_USB_DEVICE(impl); return fu_qc_firehose_usb_device_read(self, timeout_ms, error); } static GByteArray * fu_qc_firehose_usb_device_impl_read(FuQcFirehoseImpl *impl, guint timeout_ms, GError **error) { FuQcFirehoseUsbDevice *self = FU_QC_FIREHOSE_USB_DEVICE(impl); return fu_qc_firehose_usb_device_read(self, timeout_ms, error); } static gboolean fu_qc_firehose_usb_device_sahara_impl_write(FuQcFirehoseSaharaImpl *impl, const guint8 *buf, gsize sz, guint timeout_ms, GError **error) { FuQcFirehoseUsbDevice *self = FU_QC_FIREHOSE_USB_DEVICE(impl); return fu_qc_firehose_usb_device_write(self, buf, sz, timeout_ms, error); } static gboolean fu_qc_firehose_usb_device_impl_write(FuQcFirehoseImpl *impl, const guint8 *buf, gsize sz, guint timeout_ms, GError **error) { FuQcFirehoseUsbDevice *self = FU_QC_FIREHOSE_USB_DEVICE(impl); return fu_qc_firehose_usb_device_write(self, buf, sz, timeout_ms, error); } static void fu_qc_firehose_usb_device_impl_iface_init(FuQcFirehoseImplInterface *iface) { iface->read = fu_qc_firehose_usb_device_impl_read; iface->write = fu_qc_firehose_usb_device_impl_write; iface->has_function = fu_qc_firehose_usb_device_impl_has_function; iface->add_function = fu_qc_firehose_usb_device_impl_add_function; } static void fu_qc_firehose_usb_device_sahara_impl_iface_init(FuQcFirehoseSaharaImplInterface *iface) { iface->read = fu_qc_firehose_usb_device_sahara_impl_read; iface->write = fu_qc_firehose_usb_device_sahara_impl_write; } static void fu_qc_firehose_usb_device_init(FuQcFirehoseUsbDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "com.qualcomm.firehose"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_BCD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_ARCHIVE_FIRMWARE); fu_device_set_remove_delay(FU_DEVICE(self), 60000); fu_device_register_private_flag(FU_DEVICE(self), FU_QC_FIREHOSE_USB_DEVICE_NO_ZLP); fu_usb_device_add_interface(FU_USB_DEVICE(self), 0x00); } static void fu_qc_firehose_usb_device_class_init(FuQcFirehoseUsbDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_qc_firehose_usb_device_to_string; device_class->probe = fu_qc_firehose_usb_device_probe; device_class->replace = fu_qc_firehose_usb_device_replace; device_class->write_firmware = fu_qc_firehose_usb_device_impl_write_firmware; device_class->attach = fu_qc_firehose_usb_device_attach; device_class->set_progress = fu_qc_firehose_usb_device_set_progress; } fwupd-2.0.10/plugins/qc-firehose/fu-qc-firehose-usb-device.h000066400000000000000000000005621501337203100236150ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_QC_FIREHOSE_USB_DEVICE (fu_qc_firehose_usb_device_get_type()) G_DECLARE_FINAL_TYPE(FuQcFirehoseUsbDevice, fu_qc_firehose_usb_device, FU, QC_FIREHOSE_USB_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/qc-firehose/fu-qc-firehose.rs000066400000000000000000000050331501337203100217640ustar00rootroot00000000000000// Copyright 2025 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToBitString, FromString)] enum FuQcFirehoseFunctions { None = 0, Program = 1 << 0, Configure = 1 << 1, Power = 1 << 2, Read = 1 << 3, Getstorageinfo = 1 << 4, Erase = 1 << 5, Nop = 1 << 6, Setbootablestoragedrive = 1 << 7, Patch = 1 << 8, Ufs = 1 << 9, Emmc = 1 << 10, Xml = 1 << 11, Peek = 1 << 12, Poke = 1 << 13, Firmwarewrite = 1 << 14, Benchmark = 1 << 15, Getcrc16digest = 1 << 16, Getsha256digest = 1 << 17, } #[repr(u32le)] #[derive(ToString)] enum FuQcFirehoseSaharaCommandId { NoCmd, Hello, HelloResponse, Read, EndOfImage, Done, DoneResponse, Reset, ResetResponse, Read64 = 0x12, } #[repr(u32le)] #[derive(ToString)] enum FuQcFirehoseSaharaStatus { Success, Failed, } #[derive(Parse)] struct FuQcFirehoseSaharaPkt { command_id: FuQcFirehoseSaharaCommandId, hdr_length: u32le, } #[derive(Default, Parse)] struct FuQcFirehoseSaharaPktHello { command_id: FuQcFirehoseSaharaCommandId == Hello, _hdr_length: u32le, _version: u32le, _compatible: u32le, _max_len: u32le, mode: u32le, } #[derive(Default, New)] struct FuQcFirehoseSaharaPktHelloResp { command_id: FuQcFirehoseSaharaCommandId == HelloResponse, hdr_length: u32le == $struct_size, version: u32le == 2, compatible: u32le == 1, status: FuQcFirehoseSaharaStatus == Success, mode: u32le, reserved: [u32; 6], } #[derive(Default, Parse)] struct FuQcFirehoseSaharaPktRead { command_id: FuQcFirehoseSaharaCommandId == Read, hdr_length: u32le == $struct_size, _image: u32le, offset: u32le, length: u32le, } #[derive(Default, Parse)] struct FuQcFirehoseSaharaPktRead64 { command_id: FuQcFirehoseSaharaCommandId == Read64, hdr_length: u32le == $struct_size, _image: u64le, offset: u64le, length: u64le, } #[derive(Default, Parse)] struct FuQcFirehoseSaharaPktEndOfImage { command_id: FuQcFirehoseSaharaCommandId == EndOfImage, hdr_length: u32le == $struct_size, _image: u32le, status: FuQcFirehoseSaharaStatus, } #[derive(Default, New)] struct FuQcFirehoseSaharaPktDone { command_id: FuQcFirehoseSaharaCommandId == Done, hdr_length: u32le == $struct_size, } #[derive(Default, Parse)] struct FuQcFirehoseSaharaPktDoneResp { command_id: FuQcFirehoseSaharaCommandId == DoneResponse, hdr_length: u32le == $struct_size, status: FuQcFirehoseSaharaStatus, } fwupd-2.0.10/plugins/qc-firehose/fu-self-test.c000066400000000000000000000054471501337203100212740ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-qc-firehose-impl-common.h" typedef struct { guint cnt; guint cnt_done; FwupdError error_code; } FooHelper; static gboolean fu_qc_firehose_retry_cb(FuQcFirehoseImpl *self, gboolean *done, guint timeout_ms, gpointer user_data, GError **error) { FooHelper *helper = (FooHelper *)user_data; helper->cnt++; if (helper->cnt_done > 0 && helper->cnt == helper->cnt_done) *done = TRUE; return TRUE; } static void fu_qc_firehose_retry_true_func(void) { gboolean ret; g_autoptr(GError) error = NULL; FooHelper helper = {0}; ret = fu_qc_firehose_impl_retry(NULL, 2500, fu_qc_firehose_retry_cb, &helper, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL); g_assert_false(ret); g_assert_cmpint(helper.cnt, ==, 100); } static void fu_qc_firehose_retry_done_func(void) { gboolean ret; g_autoptr(GError) error = NULL; FooHelper helper = { .cnt_done = 10, }; ret = fu_qc_firehose_impl_retry(NULL, 2500, fu_qc_firehose_retry_cb, &helper, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(helper.cnt, ==, 10); } static gboolean fu_qc_firehose_retry_error_cb(FuQcFirehoseImpl *self, gboolean *done, guint timeout_ms, gpointer user_data, GError **error) { FooHelper *helper = (FooHelper *)user_data; helper->cnt++; g_set_error_literal(error, FWUPD_ERROR, (gint)helper->error_code, "timeout"); return FALSE; } static void fu_qc_firehose_retry_timeout_func(void) { gboolean ret; g_autoptr(GError) error = NULL; FooHelper helper = { .error_code = FWUPD_ERROR_TIMED_OUT, }; ret = fu_qc_firehose_impl_retry(NULL, 2500, fu_qc_firehose_retry_error_cb, &helper, &error); g_assert_false(ret); g_assert_error(error, FWUPD_ERROR, (gint)helper.error_code); g_assert_cmpint(helper.cnt, ==, 5); } static void fu_qc_firehose_retry_invalid_func(void) { gboolean ret; g_autoptr(GError) error = NULL; FooHelper helper = { .error_code = FWUPD_ERROR_INVALID_DATA, }; ret = fu_qc_firehose_impl_retry(NULL, 2500, fu_qc_firehose_retry_error_cb, &helper, &error); g_assert_false(ret); g_assert_error(error, FWUPD_ERROR, (gint)helper.error_code); g_assert_cmpint(helper.cnt, ==, 1); } int main(int argc, char **argv) { (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); (void)g_setenv("G_MESSAGES_DEBUG", "all", TRUE); g_test_init(&argc, &argv, NULL); g_test_add_func("/qc-firehose/retry{true}", fu_qc_firehose_retry_true_func); g_test_add_func("/qc-firehose/retry{done}", fu_qc_firehose_retry_done_func); g_test_add_func("/qc-firehose/retry{timeout}", fu_qc_firehose_retry_timeout_func); g_test_add_func("/qc-firehose/retry{invalid}", fu_qc_firehose_retry_invalid_func); return g_test_run(); } fwupd-2.0.10/plugins/qc-firehose/meson.build000066400000000000000000000026161501337203100207470ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginQcFirehose"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('qc-firehose.quirk') plugin_builtins += static_library('fu_plugin_qc_firehose', rustgen.process('fu-qc-firehose.rs'), sources: [ 'fu-qc-firehose-impl.c', 'fu-qc-firehose-impl-common.c', 'fu-qc-firehose-sahara-impl.c', 'fu-qc-firehose-usb-device.c', 'fu-qc-firehose-raw-device.c', 'fu-qc-firehose-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files( 'tests/qc-ec25au.json', 'tests/qc-eg25ggc.json', 'tests/qc-em160r.json', ) enumeration_data += files( 'tests/qc-ec25au-setup.json', 'tests/qc-em160r-setup.json', ) if get_option('tests') env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) e = executable( 'qc-firehose-self-test', rustgen.process('fu-qc-firehose.rs'), sources: [ 'fu-self-test.c', 'fu-qc-firehose-impl-common.c', ], include_directories: plugin_incdirs, dependencies: [ plugin_deps, platform_deps, ], link_with: [ fwupd, fwupdplugin, ], c_args: [ cargs, '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('qc-firehose-self-test', e, env: env) endif fwupd-2.0.10/plugins/qc-firehose/qc-firehose.quirk000066400000000000000000000002571501337203100220660ustar00rootroot00000000000000[USB\VID_05C6&PID_9008] Plugin = qc_firehose GType = FuQcFirehoseUsbDevice # for /dev/wwan0firehose0 [WWAN\TYPE_WWAN_PORT] Plugin = qc_firehose GType = FuQcFirehoseRawDevice fwupd-2.0.10/plugins/qc-firehose/tests/000077500000000000000000000000001501337203100177425ustar00rootroot00000000000000fwupd-2.0.10/plugins/qc-firehose/tests/qc-ec25au-setup.json000066400000000000000000000034011501337203100234560ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUsbDevice", "PlatformId": "1-2", "Created": "2025-02-24T10:37:23.997079Z", "IdVendor": 1478, "IdProduct": 36872, "USB": 512, "Manufacturer": 1, "Product": 2, "UsbConfigDescriptors": [ { "ConfigurationValue": 1 } ], "UsbInterfaces": [ { "Length": 9, "DescriptorType": 4, "InterfaceClass": 255, "InterfaceSubClass": 255, "InterfaceProtocol": 255, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 129, "MaxPacketSize": 512 }, { "DescriptorType": 5, "EndpointAddress": 1, "MaxPacketSize": 512 } ] } ], "UsbEvents": [ { "Id": "#4693935e", "Data": "001" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=63\nDEVNAME=bus/usb/001/064\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=5c6/9008/0\nTYPE=0/0/0\nBUSNUM=001\nDEVNUM=064" }, { "Id": "#1ab3ae0a", "Data": "064" }, { "Id": "#9a347f1f", "Data": "EgEAAgAAAEDGBQiQAAABAgABCQIgAAEBAIABCQQAAAL///8ABwWBAgACAAcFAQIAAgA=" }, { "Id": "#624d3a4d" }, { "Id": "#4693935e", "Data": "001" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=63\nDEVNAME=bus/usb/001/064\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=5c6/9008/0\nTYPE=0/0/0\nBUSNUM=001\nDEVNUM=064" }, { "Id": "#1ab3ae0a", "Data": "064" } ] } ] } fwupd-2.0.10/plugins/qc-firehose/tests/qc-ec25au.json000066400000000000000000000004761501337203100223310ustar00rootroot00000000000000{ "name": "Qualcomm EC25AU", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/qc-ec25au-setup.json", "components": [ { "version": "0.0", "guids": [ "9d5ddb63-31c7-55cc-a11e-67b5fd871113" ] } ] } ] } fwupd-2.0.10/plugins/qc-firehose/tests/qc-eg25ggc.json000066400000000000000000000007031501337203100224610ustar00rootroot00000000000000{ "name": "Qualcomm EG25GGC", "interactive": false, "steps": [ { "url": "b1cd72c75053f5c29e55784ef30e5d4be722c1aacc2400995b1ee248f07be7d6-EG25GGC-0.0.cab", "emulation-url": "0f21d648b33736fa0f8d1ec36805627291428c5a6197cc2443988ccb80df0fa4-emulation.zip", "components": [ { "version": "0.0", "guids": [ "9d5ddb63-31c7-55cc-a11e-67b5fd871113" ] } ] } ] } fwupd-2.0.10/plugins/qc-firehose/tests/qc-em160r-setup.json000066400000000000000000000066731501337203100234220ustar00rootroot00000000000000{ "UsbDevices": [ { "Created": "2025-03-17T10:59:26.658090Z", "GType": "FuUdevDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:1c.6/0000:08:00.0/mhi0/wwan/wwan0/wwan0firehose0", "DeviceFile": "/dev/wwan0firehose0", "Subsystem": "wwan", "Devtype": "wwan_port", "Events": [ { "Id": "#d5a801ad", "Data": "wwan" }, { "Id": "#ad8c58d3" }, { "Id": "#1075ed5c", "Data": "wwan_port" }, { "Id": "#bddbca22", "Data": "MAJOR=244\nMINOR=0\nDEVNAME=wwan0firehose0\nDEVTYPE=wwan_port" }, { "Id": "#d432c663", "Data": "wwan0firehose0" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c", "Data": "wwan_port" }, { "Id": "#285d9a88", "GType": "FuPciDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:1c.6/0000:08:00.0", "PhysicalId": "PCI_SLOT_NAME=0000:08:00.0" }, { "Id": "#d5a801ad", "Data": "pci" }, { "Id": "#ad8c58d3", "Data": "mhi-pci-generic" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "DRIVER=mhi-pci-generic\nPCI_CLASS=FF0000\nPCI_ID=1EAC:100D\nPCI_SUBSYS_ID=1EAC:5004\nPCI_SLOT_NAME=0000:08:00.0\nMODALIAS=pci:v00001EACd0000100Dsv00001EACsd00005004bcFFsc00i00" }, { "Id": "#d432c663" }, { "Id": "#9b895db2", "Data": "0x1eac" }, { "Id": "#66f3e150", "Data": "0x100d" }, { "Id": "#d410b6c7", "Data": "0xff0000" }, { "Id": "#1075ed5c" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "DRIVER=mhi-pci-generic\nPCI_CLASS=FF0000\nPCI_ID=1EAC:100D\nPCI_SUBSYS_ID=1EAC:5004\nPCI_SLOT_NAME=0000:08:00.0\nMODALIAS=pci:v00001EACd0000100Dsv00001EACsd00005004bcFFsc00i00" }, { "Id": "#d432c663" }, { "Id": "#9b895db2", "Data": "0x1eac" }, { "Id": "#66f3e150", "Data": "0x100d" }, { "Id": "#d410b6c7", "Data": "0xff0000" }, { "Id": "#1075ed5c" }, { "Id": "#fbe018fe" }, { "Id": "#d410b6c7", "Data": "0xff0000" }, { "Id": "#bf29d2f6", "Data": "0x00" }, { "Id": "#269abd81", "Data": "0x1eac" }, { "Id": "#360cec38", "Data": "0x5004" }, { "Id": "#d2629d83", "Data": "0000:08:00.0" }, { "Id": "#ad8c58d3" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c", "Data": "wwan_port" }, { "Id": "#bddbca22", "Data": "MAJOR=244\nMINOR=0\nDEVNAME=wwan0firehose0\nDEVTYPE=wwan_port" }, { "Id": "#fbe018fe" } ] } ] } fwupd-2.0.10/plugins/qc-firehose/tests/qc-em160r.json000066400000000000000000000005041501337203100222470ustar00rootroot00000000000000{ "name": "Qualcomm EM160R (raw)", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/qc-em160r-setup.json", "components": [ { "version": "0.0", "guids": [ "541f24d9-e376-5dc0-abe6-28073755bcad" ] } ] } ] } fwupd-2.0.10/plugins/qc-s5gen2/000077500000000000000000000000001501337203100160775ustar00rootroot00000000000000fwupd-2.0.10/plugins/qc-s5gen2/README.md000066400000000000000000000044631501337203100173650ustar00rootroot00000000000000--- title: Plugin: qc-s5gen2 --- ## Introduction Firmware Update Plug-in for Qualcomm Voice & Music Series 5 Gen 1 and Gen 2, and Series 3 Gen 1, Gen 2 and Gen 3. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a packed binary file format. The DFU file format is covered in documentation from Qualcomm, referenced by 80-CH281-1. This plugin supports the following protocol ID: * `com.qualcomm.s5gen2` ## GUID Generation These devices use the standard DeviceInstanceId values, e.g. * `USB\VID_0A12&PID_4007` Pair 0A12:4007 is shared among vendors and shouldn't be used for the single device. Also these devices use a custom GUID generation scheme, please use GUID based on manufacturer and product names in case of shared VID:PID pair: * `USB\VID_0A12&PID_4007&MANUFACTURER_{iManufacturer}&PRODUCT_{iProduct}` Typically, BlueTooth devices should be detected by GAIA primary service with if default vendor ID has been used: * `BLUETOOTH\GATT_00001100-d102-11e1-9b23-00025b00a5a5` For firmware file, it is recommended to use the unique GUID generated from the variant read from the device, for instance: * `BLUETOOTH\GAIA_QCC5171` If needed to use own vendor ID for communication, the name detected by BlueZ backend should be used in quirk file: * `BLUETOOTH\NAME_QCC5171` ## Update Behavior The device is updated in runtime mode and rebooted with a new version. The commit command should be used after the update process is done, otherwise the device will reboot with the previous firmware version. The upgrade protocol and update behivior are specified in documentation from Qualcomm, referenced by 80-CH281-1 and 80-CU043-1. It is expected that OS is responsible for the correct BLE reconnection during update of BlueTooth devices. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x0A12` ## Quirk Use This plugin uses the following plugin-specific quirks: * no specific quirks ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.9.16`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Denis Pynkin: @d4s fwupd-2.0.10/plugins/qc-s5gen2/fu-qc-s5gen2-ble-device.c000066400000000000000000000434341501337203100223640ustar00rootroot00000000000000/* * Copyright 2024 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-qc-s5gen2-ble-device.h" #include "fu-qc-s5gen2-ble-struct.h" #include "fu-qc-s5gen2-device.h" #include "fu-qc-s5gen2-impl.h" #include "fu-qc-s5gen2-struct.h" #define FU_QC_S5GEN2_GAIA_V3_SUPPORTED_VERSION_MAJOR 3 #define FU_QC_S5GEN2_GAIA_V3_DEFAULT_VENDOR 0x001d #define FU_QC_S5GEN2_GAIA_V3_HDR_SZ 4 #define FU_QC_S5GEN2_BLE_DEVICE_SEND "00001101-d102-11e1-9b23-00025b00a5a5" #define FU_QC_S5GEN2_BLE_DEVICE_RECV "00001102-d102-11e1-9b23-00025b00a5a5" #define FU_QC_S5GEN2_BLE_DEVICE_DATA "00001103-d102-11e1-9b23-00025b00a5a5" #define FU_QC_S5GEN2_BLE_DEVICE_TIMEOUT 15000 /* ms */ #define FU_QC_S5GEN2_BLE_DEVICE_BUFFER_SZ 256 #define FU_QC_S5GEN2_GAIA_PROTOCOL_VERSION 0x03 #define FU_QC_S5GEN2_BLE_DEVICE_AQUIRE_RETRIES 25 #define FU_QC_S5GEN2_BLE_DEVICE_AQUIRE_DELAY 200 typedef struct { guint8 core; guint8 dfu; } gaia_features_version_t; struct _FuQcS5gen2BleDevice { FuBluezDevice parent_instance; guint16 vid_v3; FuIOChannel *io_cmd; gint32 mtu; gaia_features_version_t feature; }; static void fu_qc_s5gen2_ble_device_impl_iface_init(FuQcS5gen2ImplInterface *iface); G_DEFINE_TYPE_WITH_CODE(FuQcS5gen2BleDevice, fu_qc_s5gen2_ble_device, FU_TYPE_BLUEZ_DEVICE, G_IMPLEMENT_INTERFACE(FU_TYPE_QC_S5GEN2_IMPL, fu_qc_s5gen2_ble_device_impl_iface_init)) static gboolean fu_qc_s5gen2_ble_device_notify_release(FuQcS5gen2BleDevice *self, GError **error) { if (self->io_cmd == NULL) return (TRUE); g_object_unref(self->io_cmd); self->io_cmd = NULL; self->mtu = 0; return TRUE; } static gboolean fu_qc_s5gen2_ble_device_notify_acquire(FuQcS5gen2BleDevice *self, GError **error) { if (self->io_cmd != NULL) return (TRUE); self->io_cmd = fu_bluez_device_notify_acquire(FU_BLUEZ_DEVICE(self), FU_QC_S5GEN2_BLE_DEVICE_RECV, &(self->mtu), error); if (self->io_cmd == NULL) { self->mtu = 0; return (FALSE); } g_debug("MTU = %d", self->mtu); return TRUE; } static gboolean fu_qc_s5gen2_ble_device_send(FuQcS5gen2BleDevice *self, guint8 *data, gsize data_len, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); buf = g_byte_array_append(buf, data, data_len); if (!fu_bluez_device_write(FU_BLUEZ_DEVICE(self), FU_QC_S5GEN2_BLE_DEVICE_SEND, buf, error)) return FALSE; return TRUE; } static gboolean fu_qc_s5gen2_ble_device_recv(FuQcS5gen2BleDevice *self, guint8 *data_in, gsize data_len, gsize *read_len, GError **error) { if (!fu_io_channel_read_raw(self->io_cmd, data_in, (self->mtu < (gint32)data_len) ? (gsize)self->mtu : data_len, read_len, FU_QC_S5GEN2_BLE_DEVICE_TIMEOUT, FU_IO_CHANNEL_FLAG_SINGLE_SHOT, error)) return FALSE; fu_dump_raw(G_LOG_DOMAIN, "Read from device:", data_in, *read_len); return TRUE; } static gboolean fu_qc_s5gen2_ble_device_msg_out(FuQcS5gen2Impl *impl, guint8 *data, gsize data_len, GError **error) { FuQcS5gen2BleDevice *self = FU_QC_S5GEN2_BLE_DEVICE(impl); guint8 buf[FU_QC_S5GEN2_BLE_DEVICE_BUFFER_SZ] = {0}; gsize read_len; g_autoptr(GByteArray) req = fu_struct_qc_gaia_v3_upgrade_control_cmd_new(); g_autoptr(GByteArray) validate = NULL; fu_struct_qc_gaia_v3_upgrade_control_cmd_set_vendor_id(req, self->vid_v3); g_byte_array_append(req, data, data_len); if (!fu_qc_s5gen2_ble_device_send(self, req->data, req->len, error)) return FALSE; if (!fu_qc_s5gen2_ble_device_recv(self, buf, sizeof(buf), &read_len, error)) return FALSE; validate = fu_struct_qc_gaia_v3_upgrade_control_ack_parse(buf, read_len, 0, error); if (validate == NULL) { return FALSE; } return TRUE; } static gboolean fu_qc_s5gen2_ble_device_msg_in(FuQcS5gen2Impl *impl, guint8 *data_in, gsize data_len, gsize *read_len, GError **error) { FuQcS5gen2BleDevice *self = FU_QC_S5GEN2_BLE_DEVICE(impl); gsize bufsz = 0; g_autofree guint8 *buf = NULL; bufsz = ((gsize)self->mtu < data_len + FU_QC_S5GEN2_GAIA_V3_HDR_SZ) ? (gsize)self->mtu : data_len + FU_QC_S5GEN2_GAIA_V3_HDR_SZ; buf = g_malloc0(bufsz); if (!fu_io_channel_read_raw(self->io_cmd, buf, bufsz, read_len, FU_QC_S5GEN2_BLE_DEVICE_TIMEOUT, FU_IO_CHANNEL_FLAG_SINGLE_SHOT, error)) return FALSE; fu_dump_raw(G_LOG_DOMAIN, "Read from device:", buf, *read_len); if (*read_len <= FU_QC_S5GEN2_GAIA_V3_HDR_SZ) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "got %lu bytes, less or equal to GAIA header", (unsigned long)*read_len); return FALSE; } /* don't need GAIA header for upper layer */ *read_len -= FU_QC_S5GEN2_GAIA_V3_HDR_SZ; if (!fu_memcpy_safe(data_in, data_len, 0, buf, bufsz, FU_QC_S5GEN2_GAIA_V3_HDR_SZ, *read_len, error)) return FALSE; return TRUE; } static gboolean fu_qc_s5gen2_ble_device_req_connect(FuQcS5gen2Impl *impl, GError **error) { FuQcS5gen2BleDevice *self = FU_QC_S5GEN2_BLE_DEVICE(impl); guint8 buf[FU_QC_S5GEN2_BLE_DEVICE_BUFFER_SZ] = {0}; gsize read_len; g_autoptr(GByteArray) req = fu_struct_qc_gaia_v3_upgrade_connect_cmd_new(); g_autoptr(GByteArray) validate = NULL; fu_struct_qc_gaia_v3_upgrade_connect_cmd_set_vendor_id(req, self->vid_v3); if (!fu_qc_s5gen2_ble_device_notify_acquire(self, error)) return FALSE; if (!fu_qc_s5gen2_ble_device_send(self, req->data, req->len, error)) return FALSE; if (!fu_qc_s5gen2_ble_device_recv(self, buf, sizeof(buf), &read_len, error)) return FALSE; validate = fu_struct_qc_gaia_v3_upgrade_connect_ack_parse(buf, read_len, 0, error); if (validate == NULL) return FALSE; return TRUE; } static gboolean fu_qc_s5gen2_ble_device_req_disconnect(FuQcS5gen2Impl *impl, GError **error) { FuQcS5gen2BleDevice *self = FU_QC_S5GEN2_BLE_DEVICE(impl); guint8 buf[FU_QC_S5GEN2_BLE_DEVICE_BUFFER_SZ] = {0}; gsize read_len; g_autoptr(GByteArray) req = fu_struct_qc_gaia_v3_upgrade_disconnect_cmd_new(); g_autoptr(GByteArray) validate = NULL; fu_struct_qc_gaia_v3_upgrade_disconnect_cmd_set_vendor_id(req, self->vid_v3); if (!fu_qc_s5gen2_ble_device_send(self, req->data, req->len, error)) return FALSE; if (!fu_qc_s5gen2_ble_device_recv(self, buf, sizeof(buf), &read_len, error)) return FALSE; validate = fu_struct_qc_gaia_v3_upgrade_disconnect_ack_parse(buf, read_len, 0, error); if (validate == NULL) return FALSE; return fu_qc_s5gen2_ble_device_notify_release(self, error); } static gboolean fu_qc_s5gen2_ble_device_data_size(FuQcS5gen2Impl *impl, gsize *data_sz, GError **error) { /* TODO: atm for GAIA only */ gsize headers_sz = FU_STRUCT_QC_DATA_SIZE + FU_QC_S5GEN2_GAIA_V3_HDR_SZ + 3; FuQcS5gen2BleDevice *self = FU_QC_S5GEN2_BLE_DEVICE(impl); if ((gsize)self->mtu <= headers_sz) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "MTU is not sufficient"); return FALSE; } *data_sz = (gsize)self->mtu - headers_sz; return TRUE; } static gboolean fu_qc_s5gen2_ble_device_get_api(FuQcS5gen2BleDevice *self, GError **error) { guint8 buf[FU_QC_S5GEN2_BLE_DEVICE_BUFFER_SZ] = {0}; gsize read_len; guint8 api_major; guint8 api_minor; g_autoptr(GByteArray) req = fu_struct_qc_gaia_v3_api_req_new(); g_autoptr(GByteArray) resp = NULL; fu_struct_qc_gaia_v3_api_req_set_vendor_id(req, self->vid_v3); if (!fu_qc_s5gen2_ble_device_send(self, req->data, req->len, error)) return FALSE; if (!fu_qc_s5gen2_ble_device_recv(self, buf, sizeof(buf), &read_len, error)) return FALSE; resp = fu_struct_qc_gaia_v3_api_parse(buf, read_len, 0, error); if (resp == NULL) return FALSE; api_major = fu_struct_qc_gaia_v3_api_get_major(resp); api_minor = fu_struct_qc_gaia_v3_api_get_minor(resp); if (api_major < FU_QC_S5GEN2_GAIA_V3_SUPPORTED_VERSION_MAJOR) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "GAIA protocol %u.%u is not supported", api_major, api_minor); return FALSE; } g_debug("GAIA API version: %u.%u", api_major, api_minor); return TRUE; } /* read the list of supported features from device */ static gboolean fu_qc_s5gen2_ble_device_get_features(FuQcS5gen2BleDevice *self, gboolean next, GError **error) { guint8 buf[FU_QC_S5GEN2_BLE_DEVICE_BUFFER_SZ] = {0}; gsize read_len; g_autoptr(GByteArray) req = fu_struct_qc_gaia_v3_supported_features_req_new(); g_autoptr(GByteArray) resp = NULL; fu_struct_qc_gaia_v3_supported_features_req_set_vendor_id(req, self->vid_v3); fu_struct_qc_gaia_v3_supported_features_req_set_command( req, (next == FALSE) ? FU_QC_GAIA_V3_CMD_GET_SUPPORTED_FEATURES_REQ : FU_QC_GAIA_V3_CMD_GET_SUPPORTED_FEATURES_NEXT_REQ); if (!fu_qc_s5gen2_ble_device_send(self, req->data, req->len, error)) return FALSE; if (!fu_qc_s5gen2_ble_device_recv(self, buf, sizeof(buf), &read_len, error)) return FALSE; resp = fu_struct_qc_gaia_v3_supported_features_parse(buf, read_len, 0, error); if (resp == NULL) return FALSE; /* must be odd: header 5B + feature pairs */ if ((read_len & 0x01) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "got incorrect features list"); return FALSE; } /* parse feature:version pairs */ for (gsize i = FU_STRUCT_QC_GAIA_V3_SUPPORTED_FEATURES_SIZE; i < read_len && i < sizeof(buf) - 1; i += 2) { switch (buf[i]) { case FU_QC_GAIA_V3_FEATURES_CORE: self->feature.core = buf[i + 1]; g_debug("Core feature version: %u", self->feature.core); break; case FU_QC_GAIA_V3_FEATURES_DFU: self->feature.dfu = buf[i + 1]; g_debug("DFU feature version: %u", self->feature.dfu); break; default: break; } } /* request the rest of the list */ if (fu_struct_qc_gaia_v3_supported_features_get_more_features(resp) == FU_QC_MORE_MORE) return fu_qc_s5gen2_ble_device_get_features(self, TRUE, error); return TRUE; } static gboolean fu_qc_s5gen2_ble_device_get_serial(FuQcS5gen2BleDevice *self, GError **error) { guint8 buf[FU_QC_S5GEN2_BLE_DEVICE_BUFFER_SZ] = {0}; gsize read_len; g_autoptr(GByteArray) req = fu_struct_qc_gaia_v3_serial_req_new(); g_autoptr(GByteArray) validate = NULL; g_autofree gchar *serial = NULL; fu_struct_qc_gaia_v3_serial_req_set_vendor_id(req, self->vid_v3); if (!fu_qc_s5gen2_ble_device_send(self, req->data, req->len, error)) return FALSE; if (!fu_qc_s5gen2_ble_device_recv(self, buf, sizeof(buf), &read_len, error)) return FALSE; /* Check if response is valid */ validate = fu_struct_qc_gaia_v3_serial_parse(buf, FU_STRUCT_QC_GAIA_V3_SERIAL_SIZE, 0, error); if (validate == NULL) return FALSE; serial = fu_strsafe((gchar *)(buf + FU_STRUCT_QC_GAIA_V3_SERIAL_SIZE), read_len - FU_STRUCT_QC_GAIA_V3_SERIAL_SIZE); if (serial != NULL) fu_device_set_serial(FU_DEVICE(self), serial); return TRUE; } static gboolean fu_qc_s5gen2_ble_device_get_variant(FuQcS5gen2BleDevice *self, GError **error) { guint8 buf[FU_QC_S5GEN2_BLE_DEVICE_BUFFER_SZ] = {0}; gsize read_len; g_autoptr(GByteArray) req = fu_struct_qc_gaia_v3_variant_req_new(); g_autoptr(GByteArray) validate = NULL; g_autofree gchar *variant = NULL; fu_struct_qc_gaia_v3_variant_req_set_vendor_id(req, self->vid_v3); if (!fu_qc_s5gen2_ble_device_send(self, req->data, req->len, error)) return FALSE; if (!fu_qc_s5gen2_ble_device_recv(self, buf, sizeof(buf), &read_len, error)) return FALSE; /* check if response is valid */ validate = fu_struct_qc_gaia_v3_variant_parse(buf, FU_STRUCT_QC_GAIA_V3_VARIANT_SIZE, 0, error); if (validate == NULL) return FALSE; variant = fu_strsafe((gchar *)(buf + FU_STRUCT_QC_GAIA_V3_VARIANT_SIZE), read_len - FU_STRUCT_QC_GAIA_V3_VARIANT_SIZE); if (variant == NULL) { g_debug("read non-printable device variant, skipping"); return TRUE; } /* create the GUID based on variant read from device */ fu_device_add_instance_str(FU_DEVICE(self), "GAIA", variant); fu_device_build_instance_id_full(FU_DEVICE(self), FU_DEVICE_INSTANCE_FLAG_VISIBLE | FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "BLUETOOTH", "GAIA", NULL); return TRUE; } static gboolean fu_qc_s5gen2_ble_device_register_notification(FuQcS5gen2BleDevice *self, GError **error) { guint8 buf[FU_QC_S5GEN2_BLE_DEVICE_BUFFER_SZ] = {0}; gsize read_len = 0; g_autoptr(GByteArray) req = fu_struct_qc_gaia_v3_register_notification_cmd_new(); g_autoptr(GByteArray) validate = NULL; /* register only for update feature */ fu_struct_qc_gaia_v3_register_notification_cmd_set_vendor_id(req, self->vid_v3); if (!fu_qc_s5gen2_ble_device_send(self, req->data, req->len, error)) return FALSE; if (!fu_qc_s5gen2_ble_device_recv(self, buf, sizeof(buf), &read_len, error)) return FALSE; /* Check if response is valid */ validate = fu_struct_qc_gaia_v3_register_notification_ack_parse( buf, FU_STRUCT_QC_GAIA_V3_REGISTER_NOTIFICATION_ACK_SIZE, 0, error); if (validate == NULL) return FALSE; return TRUE; } static gboolean fu_qc_s5gen2_ble_device_set_transport_protocol(FuQcS5gen2BleDevice *self, guint32 version, GError **error) { guint8 buf[FU_QC_S5GEN2_BLE_DEVICE_BUFFER_SZ] = {0}; gsize read_len; g_autoptr(GByteArray) req = fu_struct_qc_gaia_v3_set_transport_info_req_new(); g_autoptr(GByteArray) validate = NULL; fu_struct_qc_gaia_v3_set_transport_info_req_set_vendor_id(req, self->vid_v3); fu_struct_qc_gaia_v3_set_transport_info_req_set_key(req, 0x07); fu_struct_qc_gaia_v3_set_transport_info_req_set_value(req, version); if (!fu_qc_s5gen2_ble_device_send(self, req->data, req->len, error)) return FALSE; if (!fu_qc_s5gen2_ble_device_recv(self, buf, sizeof(buf), &read_len, error)) return FALSE; validate = fu_struct_qc_gaia_v3_set_transport_info_parse(buf, read_len, 0, error); if (validate == NULL) return FALSE; return TRUE; } static void fu_qc_s5gen2_ble_device_to_string(FuDevice *device, guint idt, GString *str) { FuQcS5gen2BleDevice *self = FU_QC_S5GEN2_BLE_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "GaiaVendorId", self->vid_v3); fwupd_codec_string_append_hex(str, idt, "GaiaCoreFeatureVersion", self->feature.core); fwupd_codec_string_append_hex(str, idt, "GaiaDfuFeatureVersion", self->feature.dfu); } static gboolean fu_qc_s5gen2_ble_device_notify_acquire_cb(FuDevice *device, gpointer user_data, GError **error) { FuQcS5gen2BleDevice *self = FU_QC_S5GEN2_BLE_DEVICE(device); if (!fu_qc_s5gen2_ble_device_notify_release(self, error)) return FALSE; return fu_qc_s5gen2_ble_device_notify_acquire(self, error); } static gboolean fu_qc_s5gen2_ble_device_probe(FuDevice *device, GError **error) { FuQcS5gen2BleDevice *self = FU_QC_S5GEN2_BLE_DEVICE(device); g_autofree gchar *vendor_id = NULL; if (!FU_DEVICE_CLASS(fu_qc_s5gen2_ble_device_parent_class)->probe(device, error)) return FALSE; /* after reboot the device might appear too fast */ if (!fu_device_retry_full(device, fu_qc_s5gen2_ble_device_notify_acquire_cb, FU_QC_S5GEN2_BLE_DEVICE_AQUIRE_RETRIES, FU_QC_S5GEN2_BLE_DEVICE_AQUIRE_DELAY, NULL, error)) return FALSE; if (!fu_qc_s5gen2_ble_device_get_api(self, error)) return FALSE; if (!fu_qc_s5gen2_ble_device_get_features(self, FALSE, error)) return FALSE; if (!fu_qc_s5gen2_ble_device_get_serial(self, error)) return FALSE; if (!fu_qc_s5gen2_ble_device_get_variant(self, error)) return FALSE; if (self->feature.core >= 2) { if (!fu_qc_s5gen2_ble_device_set_transport_protocol( self, FU_QC_S5GEN2_GAIA_PROTOCOL_VERSION, error)) return FALSE; } /* set vendor ID to avoid update error */ vendor_id = g_strdup_printf("BLUETOOTH:%04X", self->vid_v3); fu_device_add_vendor_id(device, vendor_id); if (!fu_qc_s5gen2_ble_device_register_notification(self, error)) return FALSE; return fu_qc_s5gen2_ble_device_notify_release(FU_QC_S5GEN2_BLE_DEVICE(device), error); } static gboolean fu_qc_s5gen2_ble_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuQcS5gen2BleDevice *self = FU_QC_S5GEN2_BLE_DEVICE(device); guint64 tmp = 0; if (g_strcmp0(key, "AudioS5gen2Gaia3VendorId") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->vid_v3 = tmp; return TRUE; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_qc_s5gen2_ble_device_init(FuQcS5gen2BleDevice *self) { self->vid_v3 = FU_QC_S5GEN2_GAIA_V3_DEFAULT_VENDOR; self->mtu = 0; self->feature.core = 0; self->feature.dfu = 0; fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_remove_delay(FU_DEVICE(self), FU_QC_S5GEN2_DEVICE_REMOVE_DELAY); fu_device_add_protocol(FU_DEVICE(self), "com.qualcomm.s5gen2"); } static void fu_qc_s5gen2_ble_device_finalize(GObject *object) { FuQcS5gen2BleDevice *self = FU_QC_S5GEN2_BLE_DEVICE(object); if (self->io_cmd != NULL) g_object_unref(self->io_cmd); G_OBJECT_CLASS(fu_qc_s5gen2_ble_device_parent_class)->finalize(object); } static void fu_qc_s5gen2_ble_device_impl_iface_init(FuQcS5gen2ImplInterface *iface) { iface->msg_in = fu_qc_s5gen2_ble_device_msg_in; iface->msg_out = fu_qc_s5gen2_ble_device_msg_out; iface->req_connect = fu_qc_s5gen2_ble_device_req_connect; iface->req_disconnect = fu_qc_s5gen2_ble_device_req_disconnect; iface->data_size = fu_qc_s5gen2_ble_device_data_size; } static void fu_qc_s5gen2_ble_device_class_init(FuQcS5gen2BleDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_qc_s5gen2_ble_device_finalize; device_class->to_string = fu_qc_s5gen2_ble_device_to_string; device_class->probe = fu_qc_s5gen2_ble_device_probe; device_class->set_quirk_kv = fu_qc_s5gen2_ble_device_set_quirk_kv; } fwupd-2.0.10/plugins/qc-s5gen2/fu-qc-s5gen2-ble-device.h000066400000000000000000000005571501337203100223700ustar00rootroot00000000000000/* * Copyright 2024 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_QC_S5GEN2_BLE_DEVICE (fu_qc_s5gen2_ble_device_get_type()) G_DECLARE_FINAL_TYPE(FuQcS5gen2BleDevice, fu_qc_s5gen2_ble_device, FU, QC_S5GEN2_BLE_DEVICE, FuBluezDevice) fwupd-2.0.10/plugins/qc-s5gen2/fu-qc-s5gen2-ble.rs000066400000000000000000000111531501337203100213220ustar00rootroot00000000000000// Copyright 2024 Denis Pynkin // SPDX-License-Identifier: LGPL-2.1-or-later #[repr(u8)] enum FuQcGaiaV3Features { Core = 0x00, Dfu = 0x06, } // Commands: 80-CH482-1, 80-CF422-1, 80-CF378-1 #[repr(u16be)] enum FuQcGaiaV3Cmd { GetApiReq = 0x0000, GetApiResp = 0x0100, GetSupportedFeaturesReq = 0x0001, GetSupportedFeaturesResp = 0x0101, GetSupportedFeaturesNextReq = 0x0002, GetSupportedFeaturesNextResp = 0x0102, GetSerialReq = 0x0003, GetSerialResp = 0x0103, GetVariantReq = 0x0004, GetVariantResp = 0x0104, RegisterNotificationCmd = 0x0007, RegisterNotificationAck = 0x0107, GetTransportInfoReq = 0x000c, GetTransportInfoResp = 0x010c, SetTransportInfoReq = 0x000d, SetTransportInfoResp = 0x010d, GetSystemInfoReq = 0x0011, GetSystemInfoResp = 0x0111, UpgradeConnectCmd = 0x0c00, UpgradeConnectAck = 0x0d00, UpgradeDisconnectCmd = 0x0c01, UpgradeDisconnectAck = 0x0d01, UpgradeControlCmd = 0x0c02, UpgradeControlAck = 0x0d02, } #[repr(u8)] enum FuQcGaiaCmdStatus { Success = 0x00, NotSupported = 0x01, InsufficientResources = 0x03, InvalidParameter = 0x05, IncorrectState = 0x06, InProgerss = 0x07, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcGaiaV3ApiReq { vendorId: u16be, command: FuQcGaiaV3Cmd == GetApiReq, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructQcGaiaV3Api { vendorId: u16be, command: FuQcGaiaV3Cmd == GetApiResp, major: u8, minor: u8, } #[repr(u8)] enum FuQcMore { More = 1, Last = 0, } #[derive(New)] #[repr(C, packed)] struct FuStructQcGaiaV3SupportedFeaturesReq { vendorId: u16be, command: FuQcGaiaV3Cmd, } #[derive(Parse)] #[repr(C, packed)] struct FuStructQcGaiaV3SupportedFeatures { vendorId: u16be, command: FuQcGaiaV3Cmd, moreFeatures: FuQcMore, // variable length } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcGaiaV3SerialReq { vendorId: u16be, command: FuQcGaiaV3Cmd == GetSerialReq, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructQcGaiaV3Serial { vendorId: u16be, command: FuQcGaiaV3Cmd == GetSerialResp, // variable string } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcGaiaV3VariantReq { vendorId: u16be, command: FuQcGaiaV3Cmd == GetVariantReq, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructQcGaiaV3Variant { vendorId: u16be, command: FuQcGaiaV3Cmd == GetVariantResp, // variable string } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcGaiaV3GetTransportInfoReq { vendorId: u16be, command: FuQcGaiaV3Cmd == GetTransportInfoReq, key: u8, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructQcGaiaV3GetTransportInfo { vendorId: u16be, command: FuQcGaiaV3Cmd == GetTransportInfoResp, key: u8, value: u32be, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcGaiaV3SetTransportInfoReq { vendorId: u16be, command: FuQcGaiaV3Cmd == SetTransportInfoReq, key: u8, value: u32be, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructQcGaiaV3SetTransportInfo { vendorId: u16be, command: FuQcGaiaV3Cmd == SetTransportInfoResp, key: u8, value: u32be, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcGaiaV3UpgradeConnectCmd { vendorId: u16be, command: FuQcGaiaV3Cmd == UpgradeConnectCmd, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructQcGaiaV3UpgradeConnectAck { vendorId: u16be, command: FuQcGaiaV3Cmd == UpgradeConnectAck, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcGaiaV3UpgradeDisconnectCmd { vendorId: u16be, command: FuQcGaiaV3Cmd == UpgradeDisconnectCmd, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructQcGaiaV3UpgradeDisconnectAck { vendorId: u16be, command: FuQcGaiaV3Cmd == UpgradeDisconnectAck, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcGaiaV3RegisterNotificationCmd { vendorId: u16be, command: FuQcGaiaV3Cmd == RegisterNotificationCmd, feature: u8 == 0x06, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructQcGaiaV3RegisterNotificationAck { vendorId: u16be, command: FuQcGaiaV3Cmd == RegisterNotificationAck, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcGaiaV3UpgradeControlCmd { vendorId: u16be, command: FuQcGaiaV3Cmd == UpgradeControlCmd, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructQcGaiaV3UpgradeControlAck { vendorId: u16be, command: FuQcGaiaV3Cmd == UpgradeControlAck, status: FuQcGaiaCmdStatus == Success, } fwupd-2.0.10/plugins/qc-s5gen2/fu-qc-s5gen2-device.c000066400000000000000000000556631501337203100216330ustar00rootroot00000000000000/* * Copyright 2023 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-qc-s5gen2-device.h" #include "fu-qc-s5gen2-firmware.h" #include "fu-qc-s5gen2-impl.h" #include "fu-qc-s5gen2-struct.h" #define FU_QC_S5GEN2_DEVICE_DATA_REQ_SLEEP 1000 /* ms */ #define FU_QC_S5GEN2_DEVICE_SEND_DELAY 2 /* ms */ /* 100ms delay requested by device as a rule */ #define FU_QC_S5GEN2_DEVICE_VALIDATION_RETRIES (60000 / 100) struct _FuQcS5gen2Device { FuDevice parent_instance; guint32 file_id; guint8 file_version; guint16 battery_raw; FuQcResumePoint resume_point; }; G_DEFINE_TYPE(FuQcS5gen2Device, fu_qc_s5gen2_device, FU_TYPE_DEVICE) static void fu_qc_s5gen2_device_to_string(FuDevice *device, guint idt, GString *str) { FuQcS5gen2Device *self = FU_QC_S5GEN2_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "FileId", self->file_id); fwupd_codec_string_append_hex(str, idt, "FileVersion", self->file_version); fwupd_codec_string_append_hex(str, idt, "BatteryRaw", self->battery_raw); } static gboolean fu_qc_s5gen2_device_msg_out(FuQcS5gen2Device *self, guint8 *data, gsize data_len, GError **error) { FuDevice *proxy = fu_device_get_proxy(FU_DEVICE(self)); if (proxy == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return FALSE; } return fu_qc_s5gen2_impl_msg_out(FU_QC_S5GEN2_IMPL(proxy), data, data_len, error); } static gboolean fu_qc_s5gen2_device_msg_in(FuQcS5gen2Device *self, guint8 *buf, gsize bufsz, gsize *read_len, GError **error) { FuDevice *proxy = fu_device_get_proxy(FU_DEVICE(self)); g_autoptr(GByteArray) err_msg = NULL; g_autoptr(GError) error_local = NULL; if (proxy == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return FALSE; } if (!fu_qc_s5gen2_impl_msg_in(FU_QC_S5GEN2_IMPL(proxy), buf, bufsz, read_len, error)) return FALSE; if (*read_len > bufsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "read 0x%x bytes, buffer is 0x%x", (guint)*read_len, (guint)bufsz); return FALSE; } /* error detected */ err_msg = fu_struct_qc_error_ind_parse(buf, *read_len, 0, &error_local); if (err_msg != NULL) { guint16 code = fu_struct_qc_error_ind_get_error_code(err_msg); g_autoptr(GByteArray) confirm = fu_struct_qc_error_res_new(); /* confirm and stop */ fu_struct_qc_error_res_set_error_code(confirm, code); if (!fu_qc_s5gen2_impl_msg_out(FU_QC_S5GEN2_IMPL(proxy), confirm->data, confirm->len, error)) return FALSE; g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "unexpected error (0x%x)", code); return FALSE; } return TRUE; } static gboolean fu_qc_s5gen2_device_cmd_req_disconnect(FuQcS5gen2Device *self, GError **error) { FuDevice *proxy = fu_device_get_proxy(FU_DEVICE(self)); if (proxy == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return FALSE; } return fu_qc_s5gen2_impl_req_disconnect(FU_QC_S5GEN2_IMPL(proxy), error); } static gboolean fu_qc_s5gen2_device_cmd_req_connect(FuQcS5gen2Device *self, GError **error) { FuDevice *proxy = fu_device_get_proxy(FU_DEVICE(self)); if (proxy == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return FALSE; } return fu_qc_s5gen2_impl_req_connect(FU_QC_S5GEN2_IMPL(proxy), error); } /* variable data amount depending on channel */ static gboolean fu_qc_s5gen2_device_data_size(FuQcS5gen2Device *self, gsize *data_sz, GError **error) { FuDevice *proxy = fu_device_get_proxy(FU_DEVICE(self)); if (proxy == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return FALSE; } return fu_qc_s5gen2_impl_data_size(FU_QC_S5GEN2_IMPL(proxy), data_sz, error); } static gboolean fu_qc_s5gen2_device_cmd_abort(FuQcS5gen2Device *self, GError **error) { guint8 data[FU_STRUCT_QC_ABORT_SIZE] = {0}; gsize read_len; g_autoptr(GByteArray) req = fu_struct_qc_abort_req_new(); g_autoptr(GByteArray) reply = NULL; if (!fu_qc_s5gen2_device_msg_out(self, req->data, req->len, error)) return FALSE; if (!fu_qc_s5gen2_device_msg_in(self, data, sizeof(data), &read_len, error)) return FALSE; reply = fu_struct_qc_abort_parse(data, read_len, 0, error); if (reply == NULL) return FALSE; return TRUE; } static gboolean fu_qc_s5gen2_device_cmd_sync(FuQcS5gen2Device *self, GError **error) { guint8 data[FU_STRUCT_QC_SYNC_SIZE] = {0}; gsize read_len; g_autoptr(GByteArray) req = fu_struct_qc_sync_req_new(); g_autoptr(GByteArray) reply = NULL; fu_struct_qc_sync_req_set_file_id(req, self->file_id); if (!fu_qc_s5gen2_device_msg_out(self, req->data, req->len, error)) return FALSE; if (!fu_qc_s5gen2_device_msg_in(self, data, sizeof(data), &read_len, error)) return FALSE; reply = fu_struct_qc_sync_parse(data, read_len, 0, error); if (reply == NULL) return FALSE; if (self->file_version != fu_struct_qc_sync_get_protocol_version(reply)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "unsupported firmware protocol version on device %u, expected %u", fu_struct_qc_sync_get_protocol_version(reply), self->file_version); return FALSE; } if (self->file_id != fu_struct_qc_sync_get_file_id(reply)) { g_autoptr(GError) error_local = NULL; /* reset the update state */ if (!fu_qc_s5gen2_device_cmd_abort(self, &error_local)) g_debug("failed to abort: %s", error_local->message); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "unexpected file ID from the device (%u), expected (%u)", fu_struct_qc_sync_get_file_id(reply), self->file_id); return FALSE; } self->resume_point = fu_struct_qc_sync_get_resume_point(reply); return TRUE; } static gboolean fu_qc_s5gen2_device_cmd_start(FuQcS5gen2Device *self, GError **error) { guint8 data[FU_STRUCT_QC_START_SIZE] = {0}; gsize read_len; FuQcStartStatus status; g_autoptr(GByteArray) req = fu_struct_qc_start_req_new(); g_autoptr(GByteArray) reply = NULL; if (!fu_qc_s5gen2_device_msg_out(self, req->data, req->len, error)) return FALSE; if (!fu_qc_s5gen2_device_msg_in(self, data, sizeof(data), &read_len, error)) return FALSE; reply = fu_struct_qc_start_parse(data, read_len, 0, error); if (reply == NULL) return FALSE; status = fu_struct_qc_start_get_status(reply); if (status != FU_QC_START_STATUS_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "status failure in upgrade (%s)", fu_qc_start_status_to_string(status)); return FALSE; } /* mostly for debug: save raw battery level */ self->battery_raw = fu_struct_qc_start_get_battery_level(reply); return TRUE; } static gboolean fu_qc_s5gen2_device_cmd_start_data(FuQcS5gen2Device *self, GError **error) { g_autoptr(GByteArray) req = fu_struct_qc_start_data_req_new(); if (!fu_qc_s5gen2_device_msg_out(self, req->data, req->len, error)) return FALSE; fu_device_sleep(FU_DEVICE(self), FU_QC_S5GEN2_DEVICE_DATA_REQ_SLEEP); return TRUE; } static gboolean fu_qc_s5gen2_device_cmd_validation(FuQcS5gen2Device *self, GError **error) { guint16 delay_ms; guint8 data[FU_STRUCT_QC_IS_VALIDATION_DONE_SIZE] = {0}; gsize read_len = 0; g_autoptr(GByteArray) req = fu_struct_qc_validation_req_new(); g_autoptr(GByteArray) reply = NULL; g_autoptr(GError) error_local = NULL; if (!fu_qc_s5gen2_device_msg_out(self, req->data, req->len, error)) return FALSE; if (!fu_qc_s5gen2_device_msg_in(self, data, sizeof(data), &read_len, error)) return FALSE; if (read_len > sizeof(data)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "read 0x%x bytes, larger than inbound buffer (0x%x bytes)", (guint)read_len, (guint)FU_STRUCT_QC_VALIDATION_REQ_SIZE); return FALSE; } /* ignore the error */ reply = fu_struct_qc_transfer_complete_ind_parse(data, sizeof(data), 0, &error_local); /* check if validation is complete */ if (reply != NULL) return TRUE; reply = fu_struct_qc_is_validation_done_parse(data, sizeof(data), 0, error); if (reply == NULL) return FALSE; delay_ms = fu_struct_qc_is_validation_done_get_delay(reply); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "validation of the image is not complete, waiting (%u) ms", delay_ms); fu_device_sleep(FU_DEVICE(self), delay_ms); return FALSE; } static gboolean fu_qc_s5gen2_device_validation_cb(FuDevice *device, gpointer user_data, GError **error) { return fu_qc_s5gen2_device_cmd_validation(FU_QC_S5GEN2_DEVICE(device), error); } static gboolean fu_qc_s5gen2_device_cmd_transfer_complete(FuQcS5gen2Device *self, GError **error) { /* reboot immediately */ FuQcTransferAction action = FU_QC_TRANSFER_ACTION_INTERACTIVE; g_autoptr(GByteArray) req = fu_struct_qc_transfer_complete_new(); fu_struct_qc_transfer_complete_set_action(req, action); /* if reboot immediately, the write might return error */ return fu_qc_s5gen2_device_msg_out(self, req->data, req->len, error); } static gboolean fu_qc_s5gen2_device_cmd_proceed_to_commit(FuQcS5gen2Device *self, GError **error) { guint8 data[FU_STRUCT_QC_COMMIT_REQ_SIZE] = {0}; gsize read_len; g_autoptr(GByteArray) req = fu_struct_qc_proceed_to_commit_new(); g_autoptr(GByteArray) reply = NULL; fu_struct_qc_proceed_to_commit_set_action(req, FU_QC_COMMIT_ACTION_PROCEED); if (!fu_qc_s5gen2_device_msg_out(self, req->data, req->len, error)) return FALSE; if (!fu_qc_s5gen2_device_msg_in(self, data, sizeof(data), &read_len, error)) return FALSE; reply = fu_struct_qc_commit_req_parse(data, read_len, 0, error); if (reply == NULL) return FALSE; return TRUE; } static gboolean fu_qc_s5gen2_device_cmd_commit_cfm(FuQcS5gen2Device *self, GError **error) { guint8 data[FU_STRUCT_QC_COMPLETE_SIZE] = {0}; gsize read_len; g_autoptr(GByteArray) req = fu_struct_qc_commit_cfm_new(); g_autoptr(GByteArray) reply = NULL; fu_struct_qc_commit_cfm_set_action(req, FU_QC_COMMIT_CFM_ACTION_UPGRADE); if (self->resume_point != FU_QC_RESUME_POINT_POST_COMMIT) { if (!fu_qc_s5gen2_device_msg_out(self, req->data, req->len, error)) return FALSE; } if (!fu_qc_s5gen2_device_msg_in(self, data, sizeof(data), &read_len, error)) return FALSE; reply = fu_struct_qc_complete_parse(data, read_len, 0, error); if (reply == NULL) return FALSE; return TRUE; } static gboolean fu_qc_s5gen2_device_ensure_version(FuQcS5gen2Device *self, GError **error) { guint8 ver_raw[FU_STRUCT_QC_VERSION_SIZE] = {0}; g_autofree gchar *ver_str = NULL; gsize read_len; g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GByteArray) version = NULL; g_autoptr(GByteArray) version_req = fu_struct_qc_version_req_new(); locker = fu_device_locker_new_full(FU_DEVICE(self), (FuDeviceLockerFunc)fu_qc_s5gen2_device_cmd_req_connect, (FuDeviceLockerFunc)fu_qc_s5gen2_device_cmd_req_disconnect, error); if (locker == NULL) return FALSE; if (!fu_qc_s5gen2_device_msg_out(self, version_req->data, version_req->len, error)) return FALSE; if (!fu_qc_s5gen2_device_msg_in(self, ver_raw, sizeof(ver_raw), &read_len, error)) return FALSE; version = fu_struct_qc_version_parse(ver_raw, read_len, 0, error); if (version == NULL) return FALSE; ver_str = g_strdup_printf("%u.%u.%u", fu_struct_qc_version_get_major(version), fu_struct_qc_version_get_minor(version), fu_struct_qc_version_get_config(version)); fu_device_set_version(FU_DEVICE(self), ver_str); return TRUE; } static gboolean fu_qc_s5gen2_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuQcS5gen2Device *self = FU_QC_S5GEN2_DEVICE(device); g_autoptr(FuDeviceLocker) locker = NULL; locker = fu_device_locker_new_full(device, (FuDeviceLockerFunc)fu_qc_s5gen2_device_cmd_req_connect, (FuDeviceLockerFunc)fu_qc_s5gen2_device_cmd_req_disconnect, error); if (locker == NULL) { g_prefix_error(error, "failed to connect: "); return FALSE; } if (!fu_qc_s5gen2_device_cmd_sync(self, error)) { g_prefix_error(error, "failed to cmd-sync: "); return FALSE; } if (!fu_qc_s5gen2_device_cmd_start(self, error)) { g_prefix_error(error, "failed to cmd-start: "); return FALSE; } g_debug("resume point: %s", fu_qc_resume_point_to_string(self->resume_point)); if (self->resume_point != FU_QC_RESUME_POINT_POST_REBOOT && self->resume_point != FU_QC_RESUME_POINT_COMMIT && self->resume_point != FU_QC_RESUME_POINT_POST_COMMIT) { g_autoptr(GError) error_local = NULL; /* reset the update state */ if (!fu_qc_s5gen2_device_cmd_abort(self, &error_local)) g_debug("failed to abort: %s", error_local->message); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "unexpected resume point (%s)", fu_qc_resume_point_to_string(self->resume_point)); return FALSE; } if (self->resume_point == FU_QC_RESUME_POINT_POST_REBOOT) { if (!fu_qc_s5gen2_device_cmd_proceed_to_commit(self, error)) { g_prefix_error(error, "failed to cmd-proceed-to-commit: "); return FALSE; } self->resume_point = FU_QC_RESUME_POINT_COMMIT; } g_debug("resume point: %s", fu_qc_resume_point_to_string(self->resume_point)); if (!fu_qc_s5gen2_device_cmd_commit_cfm(self, error)) { g_prefix_error(error, "failed to cmd-commit: "); return FALSE; } self->resume_point = FU_QC_RESUME_POINT_POST_COMMIT; g_debug("resume point: %s", fu_qc_resume_point_to_string(self->resume_point)); /* success */ return TRUE; } static gboolean fu_qc_s5gen2_device_reload(FuDevice *device, GError **error) { FuQcS5gen2Device *self = FU_QC_S5GEN2_DEVICE(device); if (!fu_qc_s5gen2_device_ensure_version(self, error)) { g_prefix_error(error, "failed to ensure version on reload: "); return FALSE; } return TRUE; } static gboolean fu_qc_s5gen2_device_setup(FuDevice *device, GError **error) { FuQcS5gen2Device *self = FU_QC_S5GEN2_DEVICE(device); if (!fu_qc_s5gen2_device_ensure_version(self, error)) { g_prefix_error(error, "failed to ensure version: "); return FALSE; } return TRUE; } static gboolean fu_qc_s5gen2_device_write_bucket(FuQcS5gen2Device *self, GBytes *data, FuQcMoreData last, GError **error) { gsize data_sz = 0; g_autoptr(FuChunkArray) chunks = NULL; if (!fu_qc_s5gen2_device_data_size(self, &data_sz, error)) return FALSE; chunks = fu_chunk_array_new_from_bytes(data, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, data_sz); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(GByteArray) pkt = fu_struct_qc_data_new(); g_autoptr(FuChunk) chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; fu_struct_qc_data_set_data_len(pkt, fu_chunk_get_data_sz(chk) + 1); /* only the last block of the last bucket should have flag LAST */ if ((i + 1) == fu_chunk_array_length(chunks)) fu_struct_qc_data_set_last_packet(pkt, last); else fu_struct_qc_data_set_last_packet(pkt, FU_QC_MORE_DATA_MORE); pkt = g_byte_array_append(pkt, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); if (pkt == NULL) return FALSE; if (!fu_qc_s5gen2_device_msg_out(self, pkt->data, FU_STRUCT_QC_DATA_SIZE + fu_chunk_get_data_sz(chk), error)) return FALSE; /* wait between packets sending */ fu_device_sleep(FU_DEVICE(self), FU_QC_S5GEN2_DEVICE_SEND_DELAY); } return TRUE; } static gboolean fu_qc_s5gen2_device_write_blocks(FuQcS5gen2Device *self, GBytes *bytes, FuProgress *progress, GError **error) { const gsize blobsz = g_bytes_get_size(bytes); guint32 cur_offset = 0; FuQcMoreData more_data = FU_QC_MORE_DATA_MORE; /* progress */ fu_progress_set_id(progress, G_STRLOC); /* device is requesting data from the host */ do { guint8 buf_in[FU_STRUCT_QC_DATA_REQ_SIZE] = {0}; gsize read_len; guint32 data_sz; guint32 data_offset; g_autoptr(GByteArray) data_req = NULL; g_autoptr(GBytes) data_out = NULL; if (!fu_qc_s5gen2_device_msg_in(self, buf_in, sizeof(buf_in), &read_len, error)) return FALSE; data_req = fu_struct_qc_data_req_parse(buf_in, read_len, 0, error); if (data_req == NULL) return FALSE; /* requested data */ data_sz = fu_struct_qc_data_req_get_fw_data_len(data_req); data_offset = fu_struct_qc_data_req_get_fw_data_offset(data_req); /* FIXME: abort for now */ if (data_sz == 0) { g_autoptr(GError) error_local = NULL; /* reset the update state */ if (!fu_qc_s5gen2_device_cmd_abort(self, &error_local)) g_debug("failed to abort: %s", error_local->message); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "requested 0x%x bytes", (guint)data_sz); return FALSE; } cur_offset += data_offset; /* requested data might be larger than the single packet payload */ if (blobsz < (cur_offset + data_sz)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "unexpected firmware data requested: offset=%u, size=%u", cur_offset, data_sz); return FALSE; } more_data = (blobsz <= (cur_offset + data_sz)) ? FU_QC_MORE_DATA_LAST_PACKET : FU_QC_MORE_DATA_MORE; data_out = g_bytes_new_from_bytes(bytes, cur_offset, data_sz); if (!fu_qc_s5gen2_device_write_bucket(self, data_out, more_data, error)) return FALSE; /* update progress */ fu_progress_set_percentage_full(progress, data_sz + cur_offset, blobsz); cur_offset += data_sz; g_debug("written 0x%x bytes of 0x%x", (guint)cur_offset, (guint)blobsz); /* FIXME: petentially infinite loop if device requesting wrong data? some counter or timeout? */ } while (more_data != FU_QC_MORE_DATA_LAST_PACKET); /* success */ return TRUE; } static FuFirmware * fu_qc_s5gen2_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuQcS5gen2Device *self = FU_QC_S5GEN2_DEVICE(device); g_autoptr(FuFirmware) firmware = fu_qc_s5gen2_firmware_new(); if (!fu_firmware_parse_stream(firmware, stream, 0, flags, error)) return NULL; self->file_version = fu_qc_s5gen2_firmware_get_protocol_version(FU_QC_S5GEN2_FIRMWARE(firmware)); self->file_id = fu_qc_s5gen2_firmware_get_id(FU_QC_S5GEN2_FIRMWARE(firmware)); return g_steal_pointer(&firmware); } static gboolean fu_qc_s5gen2_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuQcS5gen2Device *self = FU_QC_S5GEN2_DEVICE(device); g_autoptr(GBytes) fw = NULL; if (!fu_qc_s5gen2_device_cmd_req_connect(self, error)) return FALSE; /* sync requires ID of the firmware calculated */ if (!fu_qc_s5gen2_device_cmd_sync(self, error)) return FALSE; if (self->resume_point == FU_QC_RESUME_POINT_START) { /* reset the update state for the case if data partially written */ if (!fu_qc_s5gen2_device_cmd_abort(self, error)) return FALSE; if (!fu_qc_s5gen2_device_cmd_sync(self, error)) return FALSE; } if (!fu_qc_s5gen2_device_cmd_start(self, error)) return FALSE; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 83, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 17, NULL); /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; g_debug("resume point: %s", fu_qc_resume_point_to_string(self->resume_point)); if (self->resume_point == FU_QC_RESUME_POINT_START) { if (!fu_qc_s5gen2_device_cmd_start_data(self, error)) return FALSE; if (!fu_qc_s5gen2_device_write_blocks(self, fw, fu_progress_get_child(progress), error)) return FALSE; self->resume_point = FU_QC_RESUME_POINT_PRE_VALIDATE; } fu_progress_step_done(progress); g_debug("resume point: %s", fu_qc_resume_point_to_string(self->resume_point)); if (self->resume_point == FU_QC_RESUME_POINT_PRE_VALIDATE) { /* send validation request */ /* get the FU_QC_OPCODE_TRANSFER_COMPLETE_IND during 60000ms or fail */ if (!fu_device_retry_full(device, fu_qc_s5gen2_device_validation_cb, FU_QC_S5GEN2_DEVICE_VALIDATION_RETRIES, 0, /* custom delay based on value in response */ NULL, error)) return FALSE; self->resume_point = FU_QC_RESUME_POINT_PRE_REBOOT; } fu_progress_step_done(progress); g_debug("resume point: %s", fu_qc_resume_point_to_string(self->resume_point)); if (self->resume_point == FU_QC_RESUME_POINT_PRE_REBOOT) { /* complete & reboot the device */ g_autoptr(GError) error_local = NULL; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); fu_qc_s5gen2_device_cmd_transfer_complete(self, &error_local); if (error_local != NULL) g_debug("expected error during auto reboot: %s", error_local->message); self->resume_point = FU_QC_RESUME_POINT_POST_REBOOT; } return TRUE; } static void fu_qc_s5gen2_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 98, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reload"); } static void fu_qc_s5gen2_device_replace(FuDevice *device, FuDevice *donor) { FuQcS5gen2Device *self = FU_QC_S5GEN2_DEVICE(device); FuQcS5gen2Device *self_donor = FU_QC_S5GEN2_DEVICE(donor); self->file_id = self_donor->file_id; self->file_version = self_donor->file_version; self->battery_raw = self_donor->battery_raw; self->resume_point = self_donor->resume_point; } static void fu_qc_s5gen2_device_init(FuQcS5gen2Device *self) { fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_remove_delay(FU_DEVICE(self), FU_QC_S5GEN2_DEVICE_REMOVE_DELAY); fu_device_add_protocol(FU_DEVICE(self), "com.qualcomm.s5gen2"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SELF_RECOVERY); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PROXY_FOR_OPEN); } static void fu_qc_s5gen2_device_class_init(FuQcS5gen2DeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_qc_s5gen2_device_to_string; device_class->setup = fu_qc_s5gen2_device_setup; device_class->reload = fu_qc_s5gen2_device_reload; device_class->attach = fu_qc_s5gen2_device_attach; device_class->prepare_firmware = fu_qc_s5gen2_device_prepare_firmware; device_class->write_firmware = fu_qc_s5gen2_device_write_firmware; device_class->set_progress = fu_qc_s5gen2_device_set_progress; device_class->replace = fu_qc_s5gen2_device_replace; } fwupd-2.0.10/plugins/qc-s5gen2/fu-qc-s5gen2-device.h000066400000000000000000000005641501337203100216260ustar00rootroot00000000000000/* * Copyright 2023 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_QC_S5GEN2_DEVICE_REMOVE_DELAY 90000 /* ms */ #define FU_TYPE_QC_S5GEN2_DEVICE (fu_qc_s5gen2_device_get_type()) G_DECLARE_FINAL_TYPE(FuQcS5gen2Device, fu_qc_s5gen2_device, FU, QC_S5GEN2_DEVICE, FuDevice) fwupd-2.0.10/plugins/qc-s5gen2/fu-qc-s5gen2-firmware.c000066400000000000000000000101171501337203100221710ustar00rootroot00000000000000/* * Copyright 2023 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-qc-s5gen2-firmware.h" #include "fu-qc-s5gen2-fw-struct.h" struct _FuQcS5gen2Firmware { FuFirmware parent_instance; guint32 file_id; guint8 protocol_ver; gchar *device_variant; }; G_DEFINE_TYPE(FuQcS5gen2Firmware, fu_qc_s5gen2_firmware, FU_TYPE_FIRMWARE) guint8 fu_qc_s5gen2_firmware_get_protocol_version(FuQcS5gen2Firmware *self) { return self->protocol_ver; } /* generated ID unique for the firmware */ guint32 fu_qc_s5gen2_firmware_get_id(FuQcS5gen2Firmware *self) { return self->file_id; } static void fu_qc_s5gen2_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuQcS5gen2Firmware *self = FU_QC_S5GEN2_FIRMWARE(firmware); fu_xmlb_builder_insert_kv(bn, "device_variant", self->device_variant); fu_xmlb_builder_insert_kx(bn, "protocol_version", self->protocol_ver); fu_xmlb_builder_insert_kx(bn, "generated_file_id", self->file_id); } static gboolean fu_qc_s5gen2_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_qc_fw_update_hdr_validate_stream(stream, offset, error); } static gboolean fu_qc_s5gen2_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuQcS5gen2Firmware *self = FU_QC_S5GEN2_FIRMWARE(firmware); const guint8 *device_variant; gsize config_offset = 26; guint16 config_ver; g_autofree gchar *ver_str = NULL; g_autoptr(GByteArray) hdr = NULL; /* FIXME: deal with encrypted? */ hdr = fu_struct_qc_fw_update_hdr_parse_stream(stream, 0x0, error); if (hdr == NULL) return FALSE; /* protocol version */ self->protocol_ver = fu_struct_qc_fw_update_hdr_get_protocol(hdr) - '0'; device_variant = fu_struct_qc_fw_update_hdr_get_dev_variant(hdr, NULL); self->device_variant = fu_strsafe((const gchar *)device_variant, 8); config_offset += fu_struct_qc_fw_update_hdr_get_upgrades(hdr) * 4; if (!fu_input_stream_read_u16(stream, config_offset, &config_ver, G_BIG_ENDIAN, error)) return FALSE; ver_str = g_strdup_printf("%u.%u.%u", fu_struct_qc_fw_update_hdr_get_major(hdr), fu_struct_qc_fw_update_hdr_get_minor(hdr), config_ver); fu_firmware_set_version(firmware, ver_str); if (!fu_firmware_set_stream(firmware, stream, error)) return FALSE; if (!fu_input_stream_compute_crc32(stream, FU_CRC_KIND_B32_STANDARD, &self->file_id, error)) return FALSE; /* success */ return TRUE; } static GByteArray * fu_qc_s5gen2_firmware_write(FuFirmware *firmware, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GBytes) fw = NULL; /* data first */ fw = fu_firmware_get_bytes_with_patches(firmware, error); if (fw == NULL) return NULL; fu_byte_array_append_bytes(buf, fw); /* success */ return g_steal_pointer(&buf); } static void fu_qc_s5gen2_firmware_init(FuQcS5gen2Firmware *self) { self->device_variant = NULL; self->file_id = 0xFFFFFFFF; fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_STORED_SIZE); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_VID_PID); } static void fu_qc_s5gen2_firmware_finalize(GObject *object) { FuQcS5gen2Firmware *self = FU_QC_S5GEN2_FIRMWARE(object); g_free(self->device_variant); G_OBJECT_CLASS(fu_qc_s5gen2_firmware_parent_class)->finalize(object); } static void fu_qc_s5gen2_firmware_class_init(FuQcS5gen2FirmwareClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *klass_firmware = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_qc_s5gen2_firmware_finalize; klass_firmware->validate = fu_qc_s5gen2_firmware_validate; klass_firmware->parse = fu_qc_s5gen2_firmware_parse; klass_firmware->write = fu_qc_s5gen2_firmware_write; klass_firmware->export = fu_qc_s5gen2_firmware_export; } FuFirmware * fu_qc_s5gen2_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_QC_S5GEN2_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/qc-s5gen2/fu-qc-s5gen2-firmware.h000066400000000000000000000010051501337203100221720ustar00rootroot00000000000000/* * Copyright 2023 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_QC_S5GEN2_FIRMWARE (fu_qc_s5gen2_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuQcS5gen2Firmware, fu_qc_s5gen2_firmware, FU, QC_S5GEN2_FIRMWARE, FuFirmware) FuFirmware * fu_qc_s5gen2_firmware_new(void); guint8 fu_qc_s5gen2_firmware_get_protocol_version(FuQcS5gen2Firmware *self); guint32 fu_qc_s5gen2_firmware_get_id(FuQcS5gen2Firmware *self); fwupd-2.0.10/plugins/qc-s5gen2/fu-qc-s5gen2-fw.rs000066400000000000000000000006351501337203100211770ustar00rootroot00000000000000// Copyright 2023 Denis Pynkin // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ParseStream, ValidateStream, Default)] #[repr(C, packed)] struct FuStructQcFwUpdateHdr { magic1: u32be == 0x41505055, magic2: u16be == 0x4844, magic3: u8 == 0x52, protocol: u8, length: u32be, dev_variant: [u8; 8], major: u16be, minor: u16be, upgrades: u16be, } fwupd-2.0.10/plugins/qc-s5gen2/fu-qc-s5gen2-hid-device.c000066400000000000000000000174051501337203100223650ustar00rootroot00000000000000/* * Copyright 2023 Denis Pynkin * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-qc-s5gen2-device.h" #include "fu-qc-s5gen2-hid-device.h" #include "fu-qc-s5gen2-hid-struct.h" #include "fu-qc-s5gen2-impl.h" #include "fu-qc-s5gen2-struct.h" #define HID_IFACE 0x01 #define HID_EP_IN 0x82 #define HID_EP_OUT 0x01 #define FU_QC_S5GEN2_HID_DEVICE_TIMEOUT 0 /* ms */ #define FU_QC_S5GEN2_HID_DEVICE_MAX_TRANSFER_SIZE 255 struct _FuQcS5gen2HidDevice { FuHidDevice parent_instance; }; static void fu_qc_s5gen2_hid_device_impl_iface_init(FuQcS5gen2ImplInterface *iface); G_DEFINE_TYPE_WITH_CODE(FuQcS5gen2HidDevice, fu_qc_s5gen2_hid_device, FU_TYPE_HID_DEVICE, G_IMPLEMENT_INTERFACE(FU_TYPE_QC_S5GEN2_IMPL, fu_qc_s5gen2_hid_device_impl_iface_init)) static gboolean fu_qc_s5gen2_hid_device_msg_out(FuQcS5gen2Impl *impl, guint8 *data, gsize data_len, GError **error) { FuQcS5gen2HidDevice *self = FU_QC_S5GEN2_HID_DEVICE(impl); g_autoptr(GByteArray) msg = fu_struct_qc_hid_data_transfer_new(); fu_struct_qc_hid_data_transfer_set_payload_len(msg, data_len); if (!fu_struct_qc_hid_data_transfer_set_payload(msg, data, data_len, error)) return FALSE; return fu_hid_device_set_report(FU_HID_DEVICE(self), 0x00, msg->data, FU_STRUCT_QC_HID_DATA_TRANSFER_SIZE, FU_QC_S5GEN2_HID_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error); } static gboolean fu_qc_s5gen2_hid_device_msg_in(FuQcS5gen2Impl *impl, guint8 *data, gsize data_len, gsize *read_len, GError **error) { FuQcS5gen2HidDevice *self = FU_QC_S5GEN2_HID_DEVICE(impl); guint8 buf[FU_STRUCT_QC_HID_RESPONSE_SIZE] = {0x0}; g_autoptr(GByteArray) msg = NULL; if (!fu_hid_device_get_report(FU_HID_DEVICE(self), 0x00, buf, FU_STRUCT_QC_HID_RESPONSE_SIZE, FU_QC_S5GEN2_HID_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error)) return FALSE; msg = fu_struct_qc_hid_response_parse(buf, FU_STRUCT_QC_HID_RESPONSE_SIZE, 0, error); if (msg == NULL) return FALSE; if (!fu_memcpy_safe(data, data_len, 0, msg->data, msg->len, FU_STRUCT_QC_HID_RESPONSE_OFFSET_PAYLOAD, fu_struct_qc_hid_response_get_payload_len(msg), error)) return FALSE; *read_len = fu_struct_qc_hid_response_get_payload_len(msg); return TRUE; } static gboolean fu_qc_s5gen2_hid_device_msg_cmd(FuQcS5gen2Impl *impl, guint8 *data, gsize data_len, GError **error) { FuQcS5gen2HidDevice *self = FU_QC_S5GEN2_HID_DEVICE(impl); g_autoptr(GByteArray) msg = fu_struct_qc_hid_command_new(); fu_struct_qc_hid_command_set_payload_len(msg, data_len); if (!fu_struct_qc_hid_command_set_payload(msg, data, data_len, error)) return FALSE; return fu_hid_device_set_report(FU_HID_DEVICE(self), 0x03, msg->data, FU_STRUCT_QC_HID_COMMAND_SIZE, 0, FU_HID_DEVICE_FLAG_IS_FEATURE, error); } static gboolean fu_qc_s5gen2_hid_device_cmd_req_disconnect(FuQcS5gen2Impl *impl, GError **error) { g_autoptr(GByteArray) req = fu_struct_qc_disconnect_req_new(); return fu_qc_s5gen2_hid_device_msg_cmd(impl, req->data, req->len, error); } static gboolean fu_qc_s5gen2_hid_device_cmd_req_connect(FuQcS5gen2Impl *impl, GError **error) { guint8 data_in[FU_STRUCT_QC_UPDATE_STATUS_SIZE] = {0x0}; gsize read_len; FuQcStatus update_status; g_autoptr(GByteArray) req = fu_struct_qc_connect_req_new(); g_autoptr(GByteArray) st = NULL; if (!fu_qc_s5gen2_hid_device_msg_cmd(impl, req->data, req->len, error)) return FALSE; if (!fu_qc_s5gen2_hid_device_msg_in(impl, data_in, sizeof(data_in), &read_len, error)) return FALSE; st = fu_struct_qc_update_status_parse(data_in, read_len, 0, error); if (st == NULL) return FALSE; update_status = fu_struct_qc_update_status_get_status(st); switch (update_status) { case FU_QC_STATUS_SUCCESS: break; case FU_QC_STATUS_ALREADY_CONNECTED_WARNING: g_debug("device is already connected"); break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid update status (%s)", fu_qc_status_to_string(update_status)); return FALSE; } return TRUE; } static gboolean fu_qc_s5gen2_hid_device_data_size(FuQcS5gen2Impl *impl, gsize *data_sz, GError **error) { if (FU_QC_S5GEN2_HID_DEVICE_MAX_TRANSFER_SIZE <= FU_STRUCT_QC_DATA_SIZE + 2) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "MTU is not sufficient"); return FALSE; } *data_sz = FU_QC_S5GEN2_HID_DEVICE_MAX_TRANSFER_SIZE - FU_STRUCT_QC_DATA_SIZE - 2; return TRUE; } static gboolean fu_qc_s5gen2_hid_device_probe(FuDevice *device, GError **error) { FuHidDevice *hid_device = FU_HID_DEVICE(device); FuUsbInterface *iface = NULL; g_autoptr(GPtrArray) ifaces = NULL; ifaces = fu_usb_device_get_interfaces(FU_USB_DEVICE(device), error); if (ifaces == NULL) return FALSE; /* need the second HID interface */ if (ifaces->len <= HID_IFACE) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "transitional device detected"); return FALSE; } iface = g_ptr_array_index(ifaces, HID_IFACE); if (fu_usb_interface_get_class(iface) != FU_USB_CLASS_HID) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "target interface is not HID"); return FALSE; } fu_hid_device_set_interface(hid_device, HID_IFACE); fu_hid_device_set_ep_addr_in(hid_device, HID_EP_IN); fu_hid_device_set_ep_addr_out(hid_device, HID_EP_OUT); /* FuHidDevice->probe */ if (!FU_DEVICE_CLASS(fu_qc_s5gen2_hid_device_parent_class)->probe(device, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_qc_s5gen2_hid_device_setup(FuDevice *device, GError **error) { guint idx; /* FuHidDevice->setup */ if (!FU_DEVICE_CLASS(fu_qc_s5gen2_hid_device_parent_class)->setup(device, error)) return FALSE; fu_device_add_instance_u16(device, "VID", fu_device_get_vid(device)); fu_device_add_instance_u16(device, "PID", fu_device_get_pid(device)); idx = fu_usb_device_get_manufacturer_index(FU_USB_DEVICE(device)); if (idx != 0x00) { g_autofree gchar *tmp = NULL; tmp = fu_usb_device_get_string_descriptor(FU_USB_DEVICE(device), idx, NULL); if (tmp != NULL) fu_device_add_instance_str(device, "MANUFACTURER", tmp); } idx = fu_usb_device_get_product_index(FU_USB_DEVICE(device)); if (idx != 0x00) { g_autofree gchar *tmp = NULL; tmp = fu_usb_device_get_string_descriptor(FU_USB_DEVICE(device), idx, NULL); if (tmp != NULL) fu_device_add_instance_str(device, "PRODUCT", tmp); } return fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS | FU_DEVICE_INSTANCE_FLAG_VISIBLE, error, "USB", "VID", "PID", "MANUFACTURER", "PRODUCT", NULL); } static void fu_qc_s5gen2_hid_device_init(FuQcS5gen2HidDevice *self) { fu_hid_device_add_flag(FU_HID_DEVICE(self), FU_HID_DEVICE_FLAG_RETRY_FAILURE); fu_device_set_remove_delay(FU_DEVICE(self), FU_QC_S5GEN2_DEVICE_REMOVE_DELAY); fu_device_set_battery_threshold(FU_DEVICE(self), 0); } static void fu_qc_s5gen2_hid_device_impl_iface_init(FuQcS5gen2ImplInterface *iface) { iface->msg_in = fu_qc_s5gen2_hid_device_msg_in; iface->msg_out = fu_qc_s5gen2_hid_device_msg_out; iface->req_connect = fu_qc_s5gen2_hid_device_cmd_req_connect; iface->req_disconnect = fu_qc_s5gen2_hid_device_cmd_req_disconnect; iface->data_size = fu_qc_s5gen2_hid_device_data_size; } static void fu_qc_s5gen2_hid_device_class_init(FuQcS5gen2HidDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_qc_s5gen2_hid_device_probe; device_class->setup = fu_qc_s5gen2_hid_device_setup; } fwupd-2.0.10/plugins/qc-s5gen2/fu-qc-s5gen2-hid-device.h000066400000000000000000000005551501337203100223700ustar00rootroot00000000000000/* * Copyright 2023 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_QC_S5GEN2_HID_DEVICE (fu_qc_s5gen2_hid_device_get_type()) G_DECLARE_FINAL_TYPE(FuQcS5gen2HidDevice, fu_qc_s5gen2_hid_device, FU, QC_S5GEN2_HID_DEVICE, FuHidDevice) fwupd-2.0.10/plugins/qc-s5gen2/fu-qc-s5gen2-hid.rs000066400000000000000000000012651501337203100213270ustar00rootroot00000000000000// Copyright 2023 Denis Pynkin // SPDX-License-Identifier: LGPL-2.1-or-later #[repr(u8)] enum FuQcReportId { Command = 3, DataTransfer = 5, Response = 6, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcHidCommand { report_id: FuQcReportId == Command, payload_len: u8, payload: [u8; 61], } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructQcHidResponse { report_id: FuQcReportId == Response, payload_len: u8, payload: [u8; 11], } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcHidDataTransfer { report_id: FuQcReportId == DataTransfer, payload_len: u8, payload: [u8; 253], } fwupd-2.0.10/plugins/qc-s5gen2/fu-qc-s5gen2-impl.c000066400000000000000000000051431501337203100213210ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-qc-s5gen2-impl.h" G_DEFINE_INTERFACE(FuQcS5gen2Impl, fu_qc_s5gen2_impl, G_TYPE_OBJECT) static void fu_qc_s5gen2_impl_default_init(FuQcS5gen2ImplInterface *iface) { } gboolean fu_qc_s5gen2_impl_msg_in(FuQcS5gen2Impl *self, guint8 *data, gsize data_len, gsize *read_len, GError **error) { FuQcS5gen2ImplInterface *iface; g_return_val_if_fail(FU_IS_QC_S5GEN2_IMPL(self), FALSE); iface = FU_QC_S5GEN2_IMPL_GET_IFACE(self); if (iface->msg_in == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "iface->msg_in not implemented"); return FALSE; } return (*iface->msg_in)(self, data, data_len, read_len, error); } gboolean fu_qc_s5gen2_impl_msg_out(FuQcS5gen2Impl *self, guint8 *data, gsize data_len, GError **error) { FuQcS5gen2ImplInterface *iface; g_return_val_if_fail(FU_IS_QC_S5GEN2_IMPL(self), FALSE); iface = FU_QC_S5GEN2_IMPL_GET_IFACE(self); if (iface->msg_out == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "iface->msg_out not implemented"); return FALSE; } return (*iface->msg_out)(self, data, data_len, error); } gboolean fu_qc_s5gen2_impl_req_connect(FuQcS5gen2Impl *self, GError **error) { FuQcS5gen2ImplInterface *iface; g_return_val_if_fail(FU_IS_QC_S5GEN2_IMPL(self), FALSE); iface = FU_QC_S5GEN2_IMPL_GET_IFACE(self); if (iface->req_connect == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "iface->req_connect not implemented"); return FALSE; } return (*iface->req_connect)(self, error); } gboolean fu_qc_s5gen2_impl_req_disconnect(FuQcS5gen2Impl *self, GError **error) { FuQcS5gen2ImplInterface *iface; g_return_val_if_fail(FU_IS_QC_S5GEN2_IMPL(self), FALSE); iface = FU_QC_S5GEN2_IMPL_GET_IFACE(self); if (iface->req_disconnect == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "iface->req_connect not implemented"); return FALSE; } return (*iface->req_disconnect)(self, error); } gboolean fu_qc_s5gen2_impl_data_size(FuQcS5gen2Impl *self, gsize *data_sz, GError **error) { FuQcS5gen2ImplInterface *iface; g_return_val_if_fail(FU_IS_QC_S5GEN2_IMPL(self), FALSE); iface = FU_QC_S5GEN2_IMPL_GET_IFACE(self); if (iface->data_size == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "iface->data_size not implemented"); return FALSE; } return (*iface->data_size)(self, data_sz, error); } fwupd-2.0.10/plugins/qc-s5gen2/fu-qc-s5gen2-impl.h000066400000000000000000000024771501337203100213350ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_QC_S5GEN2_IMPL (fu_qc_s5gen2_impl_get_type()) G_DECLARE_INTERFACE(FuQcS5gen2Impl, fu_qc_s5gen2_impl, FU, QC_S5GEN2_IMPL, GObject) struct _FuQcS5gen2ImplInterface { GTypeInterface g_iface; gboolean (*msg_in)(FuQcS5gen2Impl *self, guint8 *data, gsize data_len, gsize *read_len, GError **error); gboolean (*msg_out)(FuQcS5gen2Impl *self, guint8 *data, gsize data_len, GError **error); gchar *(*get_version)(FuQcS5gen2Impl *self, GError **error); gboolean (*req_connect)(FuQcS5gen2Impl *self, GError **error); gboolean (*req_disconnect)(FuQcS5gen2Impl *self, GError **error); gboolean (*data_size)(FuQcS5gen2Impl *self, gsize *datasz, GError **error); }; gboolean fu_qc_s5gen2_impl_msg_in(FuQcS5gen2Impl *self, guint8 *data, gsize data_len, gsize *read_len, GError **error); gboolean fu_qc_s5gen2_impl_msg_out(FuQcS5gen2Impl *self, guint8 *data, gsize data_len, GError **error); gboolean fu_qc_s5gen2_impl_req_connect(FuQcS5gen2Impl *self, GError **error); gboolean fu_qc_s5gen2_impl_req_disconnect(FuQcS5gen2Impl *self, GError **error); gboolean fu_qc_s5gen2_impl_data_size(FuQcS5gen2Impl *self, gsize *data_sz, GError **error); fwupd-2.0.10/plugins/qc-s5gen2/fu-qc-s5gen2-plugin.c000066400000000000000000000022441501337203100216550ustar00rootroot00000000000000/* * Copyright 2023 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-qc-s5gen2-ble-device.h" #include "fu-qc-s5gen2-device.h" #include "fu-qc-s5gen2-firmware.h" #include "fu-qc-s5gen2-hid-device.h" #include "fu-qc-s5gen2-plugin.h" struct _FuQcS5gen2Plugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuQcS5gen2Plugin, fu_qc_s5gen2_plugin, FU_TYPE_PLUGIN) static void fu_qc_s5gen2_plugin_init(FuQcS5gen2Plugin *self) { } static void fu_qc_s5gen2_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "AudioS5gen2Gaia3VendorId"); fu_plugin_add_device_gtype(plugin, FU_TYPE_QC_S5GEN2_BLE_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_QC_S5GEN2_HID_DEVICE); fu_plugin_set_device_gtype_default(plugin, FU_TYPE_QC_S5GEN2_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_QC_S5GEN2_FIRMWARE); } static void fu_qc_s5gen2_plugin_class_init(FuQcS5gen2PluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_qc_s5gen2_plugin_constructed; } fwupd-2.0.10/plugins/qc-s5gen2/fu-qc-s5gen2-plugin.h000066400000000000000000000003741501337203100216640ustar00rootroot00000000000000/* * Copyright 2023 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuQcS5gen2Plugin, fu_qc_s5gen2_plugin, FU, AUDIO_S5GEN2_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/qc-s5gen2/fu-qc-s5gen2.rs000066400000000000000000000124631501337203100205670ustar00rootroot00000000000000// Copyright 2023 Denis Pynkin // SPDX-License-Identifier: LGPL-2.1-or-later #[repr(u8)] // Upgrade protocol OpCode enum FuQcOpcode { StartReq = 0x01, StartCfm = 0x02, DataBytesReq = 0x03, Data = 0x04, AbortReq = 0x07, AbortCfm = 0x08, TransferCompleteInd = 0x0B, TransferCompleteRes = 0x0C, ProceedToCommit = 0x0E, CommitReq = 0x0F, CommitCfm = 0x10, ErrorInd = 0x11, CompleteInd = 0x12, SyncReq = 0x13, SyncCfm = 0x14, StartDataReq = 0x15, IsValidationDoneReq = 0x16, IsValidationDoneCfm = 0x17, HostVersionReq = 0x19, HostVersionCfm = 0x1A, ErrorRes = 0x1F, } #[repr(u8)] enum FuQcReq { Connect = 0x02, Disconnect = 0x07, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcConnectReq { req: FuQcReq == Connect, data_len: u16be, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcDisconnectReq { req: FuQcReq == Disconnect, data_len: u16be, } #[derive(ToString)] #[repr(u8)] enum FuQcStatus { Success = 0, // Operation succeeded UnexpectedError, // Operation failed AlreadyConnectedWarning, // Already connected InProgress, // Requested operation failed, an upgrade is in progress Busy, // UNUSED InvalidPowerState, // Invalid power management state } #[derive(Parse)] #[repr(C, packed)] struct FuStructQcUpdateStatus { status: FuQcStatus, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcVersionReq { opcode: FuQcOpcode == HostVersionReq, data_len: u16be == 0x00, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructQcVersion { status: FuQcOpcode == HostVersionCfm, data_len: u16be == 0x0006, major: u16be, minor: u16be, config: u16be, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcAbortReq { opcode: FuQcOpcode == AbortReq, data_len: u16be = 0x00, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructQcAbort { opcode: FuQcOpcode == AbortCfm, data_len: u16be = 0x00, } #[derive(ToString)] #[repr(u8)] enum FuQcResumePoint { Start = 0, PreValidate, PreReboot, PostReboot, Commit, PostCommit, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcSyncReq { opcode: FuQcOpcode == SyncReq, data_len: u16be = 0x04, fileId: u32be, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructQcSync { opcode: FuQcOpcode == SyncCfm, data_len: u16be = 0x06, resume_point: FuQcResumePoint, file_id: u32be, protocolVersion: u8, } #[derive(ToString)] #[repr(u8)] enum FuQcStartStatus { Success, Failure, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcStartReq { opcode: FuQcOpcode == StartReq, data_len: u16be = 0x00, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructQcStart { opcode: FuQcOpcode == StartCfm, data_len: u16be = 0x0003, status: FuQcStartStatus, battery_level: u16be, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcStartDataReq { opcode: FuQcOpcode == StartDataReq, data_len: u16be = 0x00, } #[repr(u8)] enum FuQcMoreData { More, LastPacket, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructQcDataReq { opcode: FuQcOpcode == DataBytesReq, data_len: u16be = 0x0008, fw_data_len: u32be, fw_data_offset: u32be, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcData { opcode: FuQcOpcode == Data, data_len: u16be, last_packet: FuQcMoreData, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcValidationReq { opcode: FuQcOpcode == IsValidationDoneReq, data_len: u16be = 0x00, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructQcIsValidationDone { opcode: FuQcOpcode == IsValidationDoneCfm, data_len: u16be, delay: u16be, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructQcTransferCompleteInd { opcode: FuQcOpcode == TransferCompleteInd, data_len: u16be, } #[repr(u8)] enum FuQcTransferAction { Interactive, Abort, Silent, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcTransferComplete { opcode: FuQcOpcode == TransferCompleteRes, data_len: u16be = 0x01, action: FuQcTransferAction, } #[repr(u8)] enum FuQcCommitAction { Proceed, NotProceed, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcProceedToCommit { opcode: FuQcOpcode == ProceedToCommit, data_len: u16be = 0x01, action: FuQcCommitAction, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructQcCommitReq { opcode: FuQcOpcode == CommitReq, data_len: u16be = 0x00, } #[repr(u8)] enum FuQcCommitCfmAction { Upgrade, Rollback, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcCommitCfm { opcode: FuQcOpcode == CommitCfm, data_len: u16be = 0x01, action: FuQcCommitCfmAction, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructQcComplete { opcode: FuQcOpcode == CompleteInd, data_len: u16be = 0x00, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructQcErrorInd { opcode: FuQcOpcode == ErrorInd, data_len: u16be = 0x02, error_code: u16be, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructQcErrorRes { opcode: FuQcOpcode == ErrorRes, data_len: u16be = 0x02, error_code: u16be, } fwupd-2.0.10/plugins/qc-s5gen2/meson.build000066400000000000000000000013411501337203100202400ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginQcS5gen2"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('qc-s5gen2.quirk') plugin_builtins += static_library('fu_plugin_qc_s5gen2', rustgen.process('fu-qc-s5gen2.rs'), rustgen.process('fu-qc-s5gen2-ble.rs'), rustgen.process('fu-qc-s5gen2-hid.rs'), rustgen.process('fu-qc-s5gen2-fw.rs'), sources: [ 'fu-qc-s5gen2-device.c', 'fu-qc-s5gen2-ble-device.c', 'fu-qc-s5gen2-hid-device.c', 'fu-qc-s5gen2-firmware.c', 'fu-qc-s5gen2-plugin.c', 'fu-qc-s5gen2-impl.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files('tests/qualcomm-qcc5171.json') fwupd-2.0.10/plugins/qc-s5gen2/qc-s5gen2.quirk000066400000000000000000000013631501337203100206630ustar00rootroot00000000000000# 5171 devboard connected via USB [USB\VID_0A12&PID_4007] Plugin = qc_s5gen2 Flags = no-generic-guids ProxyGType = FuQcS5gen2HidDevice # 5171 devboard connected via BT [BLUETOOTH\NAME_QCC5171] Plugin = qc_s5gen2 Flags = enforce-requires ProxyGType = FuQcS5gen2BleDevice AudioS5gen2Gaia3VendorId = 0x001D # by GAIA primary service with default vendor ID [BLUETOOTH\GATT_00001100-d102-11e1-9b23-00025b00a5a5] Plugin = qc_s5gen2 ProxyGType = FuQcS5gen2BleDevice AudioS5gen2Gaia3VendorId = 0x001D # GID8 headset [USB\VID_18D1&PID_800C] Plugin = qc_s5gen2 ProxyGType = FuQcS5gen2HidDevice # Transitional GID8 [USB\VID_0A12&PID_4007&MANUFACTURER_GID8&PRODUCT_GID8] Plugin = qc_s5gen2 ProxyGType = FuQcS5gen2HidDevice CounterpartGuid = USB\VID_18D1&PID_800C fwupd-2.0.10/plugins/qc-s5gen2/tests/000077500000000000000000000000001501337203100172415ustar00rootroot00000000000000fwupd-2.0.10/plugins/qc-s5gen2/tests/qualcomm-qcc5171.json000066400000000000000000000005471501337203100230420ustar00rootroot00000000000000{ "name": "Qualcomm QCC5171", "interactive": false, "steps": [ { "url": "084ebf794bfcb56b7c749238b4c6ed211342738cece664d02b0cbe285eb646f8-Qualcomm_qcc517X_1.1.2.cab", "components": [ { "version": "1.1.2", "guids": [ "f3eb444d-46de-59da-97b1-7a281cb6a778" ] } ] } ] } fwupd-2.0.10/plugins/qsi-dock/000077500000000000000000000000001501337203100161055ustar00rootroot00000000000000fwupd-2.0.10/plugins/qsi-dock/README.md000066400000000000000000000017251501337203100173710ustar00rootroot00000000000000--- title: Plugin: QSI Dock --- ## Introduction This plugin uses the MCU to write all the dock firmware components. The MCU version is provided by the DMC bcdDevice. This plugin supports the following protocol ID: * `com.qsi.dock` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_047D&PID_808D` ## Update Behavior The device usually presents in runtime mode, but on detach re-enumerates with the same USB PID in an unlocked mode. On attach the device again re-enumerates back to the runtime locked mode. ## Vendor ID Security The vendor ID is set from the USB vendor. ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.8.8`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Kevin Chen: @hssinf fwupd-2.0.10/plugins/qsi-dock/fu-qsi-dock-child-device.c000066400000000000000000000047351501337203100227220ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-qsi-dock-child-device.h" #include "fu-qsi-dock-mcu-device.h" struct _FuQsiDockChildDevice { FuDevice parent_instance; guint8 chip_idx; }; G_DEFINE_TYPE(FuQsiDockChildDevice, fu_qsi_dock_child_device, FU_TYPE_DEVICE) void fu_qsi_dock_child_device_set_chip_idx(FuQsiDockChildDevice *self, guint8 chip_idx) { self->chip_idx = chip_idx; } static void fu_qsi_dock_child_device_to_string(FuDevice *device, guint idt, GString *str) { FuQsiDockChildDevice *self = FU_QSI_DOCK_CHILD_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "ChipIdx", self->chip_idx); } /* use the parents parser */ static FuFirmware * fu_qsi_dock_child_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuDevice *parent = fu_device_get_parent(device); if (parent == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no parent"); return NULL; } return fu_device_prepare_firmware(parent, stream, progress, flags, error); } /* only update this specific child component */ static gboolean fu_qsi_dock_child_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuQsiDockChildDevice *self = FU_QSI_DOCK_CHILD_DEVICE(device); FuDevice *parent = fu_device_get_parent(device); if (parent == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no parent"); return FALSE; } return fu_qsi_dock_mcu_device_write_firmware_with_idx(FU_QSI_DOCK_MCU_DEVICE(parent), firmware, self->chip_idx, progress, flags, error); } static void fu_qsi_dock_child_device_init(FuQsiDockChildDevice *self) { fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_OPEN); } static void fu_qsi_dock_child_device_class_init(FuQsiDockChildDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_qsi_dock_child_device_to_string; device_class->prepare_firmware = fu_qsi_dock_child_device_prepare_firmware; device_class->write_firmware = fu_qsi_dock_child_device_write_firmware; } FuDevice * fu_qsi_dock_child_device_new(FuContext *ctx) { return g_object_new(FU_TYPE_QSI_DOCK_CHILD_DEVICE, "context", ctx, NULL); } fwupd-2.0.10/plugins/qsi-dock/fu-qsi-dock-child-device.h000066400000000000000000000007751501337203100227270ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_QSI_DOCK_CHILD_DEVICE (fu_qsi_dock_child_device_get_type()) G_DECLARE_FINAL_TYPE(FuQsiDockChildDevice, fu_qsi_dock_child_device, FU, QSI_DOCK_CHILD_DEVICE, FuDevice) FuDevice * fu_qsi_dock_child_device_new(FuContext *ctx); void fu_qsi_dock_child_device_set_chip_idx(FuQsiDockChildDevice *self, guint8 chip_idx); fwupd-2.0.10/plugins/qsi-dock/fu-qsi-dock-common.h000066400000000000000000000033361501337203100216730ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * Copyright 2022 Kevin Chen * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_QSI_DOCK_REPORT_ID 5 #define FU_QSI_DOCK_FIRMWARE_IDX_NONE 0x00 #define FU_QSI_DOCK_FIRMWARE_IDX_DMC_PD 0x01 #define FU_QSI_DOCK_FIRMWARE_IDX_DP 0x02 #define FU_QSI_DOCK_FIRMWARE_IDX_TBT4 0x04 #define FU_QSI_DOCK_FIRMWARE_IDX_USB3 0x08 #define FU_QSI_DOCK_FIRMWARE_IDX_USB2 0x10 #define FU_QSI_DOCK_FIRMWARE_IDX_AUDIO 0x20 #define FU_QSI_DOCK_FIRMWARE_IDX_I225 0x40 #define FU_QSI_DOCK_FIRMWARE_IDX_MCU 0x80 typedef struct __attribute__((packed)) { /* nocheck:blocked */ guint8 DMC[5]; guint8 PD[5]; guint8 DP5x[5]; guint8 DP6x[5]; guint8 TBT4[5]; guint8 USB3[5]; guint8 USB2[5]; guint8 AUDIO[5]; guint8 I255[5]; guint8 MCU[2]; guint8 bcdVersion[2]; } FuQsiDockIspVersionInMcu; typedef enum { FU_QSI_DOCK_CMD1_BOOT = 0x11, FU_QSI_DOCK_CMD1_SYSTEM = 0x31, FU_QSI_DOCK_CMD1_MCU = 0x51, FU_QSI_DOCK_CMD1_SPI = 0x61, FU_QSI_DOCK_CMD1_I2C_VMM = 0x71, FU_QSI_DOCK_CMD1_I2C_CCG = 0x81, FU_QSI_DOCK_CMD1_MASS_MCU = 0xC0, FU_QSI_DOCK_CMD1_MASS_SPI, FU_QSI_DOCK_CMD1_MASS_I2C_VMM, FU_QSI_DOCK_CMD1_MASS_I2C_CY, } FuQsiDockCmd; typedef enum { FU_QSI_DOCK_CMD2_CMD_DEVICE_STATUS, FU_QSI_DOCK_CMD2_CMD_SET_BOOT_MODE, FU_QSI_DOCK_CMD2_CMD_SET_AP_MODE, FU_QSI_DOCK_CMD2_CMD_ERASE_AP_PAGE, FU_QSI_DOCK_CMD2_CMD_CHECKSUM, FU_QSI_DOCK_CMD2_CMD_DEVICE_VERSION, FU_QSI_DOCK_CMD2_CMD_DEVICE_PCB_VERSION, FU_QSI_DOCK_CMD2_CMD_DEVICE_SN, } FuQsiDockCmd2_0x51; typedef enum { FU_QSI_DOCK_CMD2_SPI_EXTERNAL_FLASH_INI, FU_QSI_DOCK_CMD2_SPI_EXTERNAL_FLASH_ERASE, FU_QSI_DOCK_CMD2_SPI_EXTERNAL_FLASH_CHECKSUM, } FuQsiDockCmd2_0x61; fwupd-2.0.10/plugins/qsi-dock/fu-qsi-dock-mcu-device.c000066400000000000000000000401241501337203100224130ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * Copyright 2022 Kevin Chen * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-qsi-dock-child-device.h" #include "fu-qsi-dock-common.h" #include "fu-qsi-dock-mcu-device.h" struct _FuQsiDockMcuDevice { FuHidDevice parent_instance; }; G_DEFINE_TYPE(FuQsiDockMcuDevice, fu_qsi_dock_mcu_device, FU_TYPE_HID_DEVICE) #define FU_QSI_DOCK_MCU_DEVICE_TIMEOUT 90000 /* ms */ #define FU_QSI_DOCK_TX_ISP_LENGTH 61 #define FU_QSI_DOCK_TX_ISP_LENGTH_MCU 60 #define FU_QSI_DOCK_EXTERN_FLASH_PAGE_SIZE 256 static gboolean fu_qsi_dock_mcu_device_tx(FuQsiDockMcuDevice *self, guint8 CmdPrimary, guint8 CmdSecond, const guint8 *inbuf, gsize inbufsz, GError **error) { guint8 buf[64] = {FU_QSI_DOCK_REPORT_ID, CmdPrimary, CmdSecond}; return fu_hid_device_set_report(FU_HID_DEVICE(self), FU_QSI_DOCK_REPORT_ID, buf, sizeof(buf), FU_QSI_DOCK_MCU_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error); } static gboolean fu_qsi_dock_mcu_device_rx(FuQsiDockMcuDevice *self, guint8 *outbuf, gsize outbufsz, GError **error) { guint8 buf[64]; if (!fu_hid_device_get_report(FU_HID_DEVICE(self), FU_QSI_DOCK_REPORT_ID, buf, sizeof(buf), FU_QSI_DOCK_MCU_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error)) { return FALSE; } if (outbuf != NULL) { if (!fu_memcpy_safe(outbuf, outbufsz, 0x0, /* dst */ buf, sizeof(buf), 0x5, /* src */ outbufsz, error)) return FALSE; } return TRUE; } static gboolean fu_qsi_dock_mcu_device_txrx(FuQsiDockMcuDevice *self, guint8 cmd1, guint8 cmd2, const guint8 *inbuf, gsize inbufsz, guint8 *outbuf, gsize outbufsz, GError **error) { if (!fu_qsi_dock_mcu_device_tx(self, cmd1, cmd2, inbuf, inbufsz, error)) return FALSE; return fu_qsi_dock_mcu_device_rx(self, outbuf, outbufsz, error); } static gboolean fu_qsi_dock_mcu_device_get_status(FuQsiDockMcuDevice *self, GError **error) { guint8 response = 0; guint8 cmd1 = FU_QSI_DOCK_CMD1_MCU; guint8 cmd2 = FU_QSI_DOCK_CMD2_CMD_DEVICE_STATUS; if (!fu_qsi_dock_mcu_device_txrx(self, cmd1, cmd2, &cmd1, sizeof(cmd1), &response, sizeof(response), error)) return FALSE; /* success */ return TRUE; } static gboolean fu_qsi_dock_mcu_device_enumerate_children(FuQsiDockMcuDevice *self, GError **error) { guint8 outbuf[49] = {0}; struct { const gchar *name; guint8 chip_idx; gsize offset; } components[] = { {"MCU", FU_QSI_DOCK_FIRMWARE_IDX_MCU, G_STRUCT_OFFSET(FuQsiDockIspVersionInMcu, MCU)}, {"bcdVersion", FU_QSI_DOCK_FIRMWARE_IDX_NONE, G_STRUCT_OFFSET(FuQsiDockIspVersionInMcu, bcdVersion)}, {NULL, 0, 0}}; for (guint i = 0; components[i].name != NULL; i++) { const guint8 *val = outbuf + components[i].offset; g_autofree gchar *version = NULL; g_autoptr(FuDevice) child = NULL; child = fu_qsi_dock_child_device_new(fu_device_get_context(FU_DEVICE(self))); if (g_strcmp0(components[i].name, "bcdVersion") == 0) { if ((val[0] == 0x00 && val[1] == 0x00) || (val[0] == 0xFF && val[1] == 0xFF)) { g_debug("ignoring %s", components[i].name); continue; } version = g_strdup_printf("%x.%x.%02x", val[0] & 0xFu, (guint)(val[0] >> 4), val[1]); g_debug("ignoring %s --> %s", components[i].name, version); continue; } if (g_strcmp0(components[i].name, "MCU") == 0) { if ((val[0] == 0x00 && val[1] == 0x00) || (val[0] == 0xFF && val[1] == 0xFF)) { g_debug("ignoring %s", components[i].name); continue; } version = g_strdup_printf("%X.%X", val[0], val[1]); fu_device_set_version_format(child, FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_version(child, version); fu_device_set_name(child, "Dock Management Controller"); } else { g_warning("unhandled %s", components[i].name); } /* add virtual device */ fu_device_add_instance_u16(child, "VID", fu_device_get_vid(FU_DEVICE(self))); fu_device_add_instance_u16(child, "PID", fu_device_get_pid(FU_DEVICE(self))); fu_device_add_instance_str(child, "CID", components[i].name); if (!fu_device_build_instance_id(child, error, "USB", "VID", "PID", "CID", NULL)) return FALSE; if (fu_device_get_name(child) == NULL) fu_device_set_name(child, components[i].name); fu_device_set_logical_id(child, components[i].name); fu_qsi_dock_child_device_set_chip_idx(FU_QSI_DOCK_CHILD_DEVICE(child), components[i].chip_idx); fu_device_add_child(FU_DEVICE(self), child); } /* success */ return TRUE; } static gboolean fu_qsi_dock_mcu_device_setup(FuDevice *device, GError **error) { FuQsiDockMcuDevice *self = FU_QSI_DOCK_MCU_DEVICE(device); /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_qsi_dock_mcu_device_parent_class)->setup(device, error)) return FALSE; /* get status and component versions */ if (!fu_qsi_dock_mcu_device_get_status(self, error)) return FALSE; if (!fu_qsi_dock_mcu_device_enumerate_children(self, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_qsi_dock_mcu_device_checksum(FuQsiDockMcuDevice *self, guint32 checksum, guint32 length, GError **error) { guint8 buf[64] = {FU_QSI_DOCK_REPORT_ID, FU_QSI_DOCK_CMD1_SPI, FU_QSI_DOCK_CMD2_SPI_EXTERNAL_FLASH_CHECKSUM, 0}; guint8 fw_length[4]; guint8 checksum_val[2]; fu_memwrite_uint32(fw_length, length, G_LITTLE_ENDIAN); fu_memwrite_uint16(checksum_val, checksum, G_LITTLE_ENDIAN); /* fw length */ if (!fu_memcpy_safe(buf, sizeof(buf), 0x03, /* dst */ fw_length, sizeof(fw_length), 0x0, sizeof(fw_length), error)) return FALSE; /* checksum */ if (!fu_memcpy_safe(buf, sizeof(buf), 0x07, /* dst */ checksum_val, sizeof(checksum_val), 0x0, sizeof(checksum_val), error)) return FALSE; /* SetReport+GetReport */ if (!fu_hid_device_set_report(FU_HID_DEVICE(self), FU_QSI_DOCK_REPORT_ID, buf, sizeof(buf), FU_QSI_DOCK_MCU_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error)) return FALSE; memset(buf, 0x0, sizeof(buf)); if (!fu_hid_device_get_report(FU_HID_DEVICE(self), FU_QSI_DOCK_REPORT_ID, buf, sizeof(buf), FU_QSI_DOCK_MCU_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error)) return FALSE; /* MCU Checksum Compare Result 0:Pass 1:Fail */ if (buf[2] != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "checksum did not match"); return FALSE; } return TRUE; } static gboolean fu_qsi_dock_mcu_device_write_chunk(FuQsiDockMcuDevice *self, FuChunk *chk_page, guint32 *checksum_tmp, FuProgress *progress, GError **error) { g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(GBytes) chk_bytes = fu_chunk_get_bytes(chk_page); chunks = fu_chunk_array_new_from_bytes(chk_bytes, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_QSI_DOCK_TX_ISP_LENGTH_MCU); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; guint8 checksum_buf[FU_QSI_DOCK_TX_ISP_LENGTH_MCU] = {0x0}; guint8 buf[64] = { FU_QSI_DOCK_REPORT_ID, FU_QSI_DOCK_CMD1_MASS_SPI, }; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; buf[2] = fu_chunk_get_data_sz(chk); /* SetReport */ if (!fu_memcpy_safe(buf, sizeof(buf), 0x04, /* dst */ fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0x0, /* src */ fu_chunk_get_data_sz(chk), error)) return FALSE; if (!fu_hid_device_set_report(FU_HID_DEVICE(self), FU_QSI_DOCK_REPORT_ID, buf, sizeof(buf), FU_QSI_DOCK_MCU_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error)) return FALSE; /* sum checksum value */ if (!fu_memcpy_safe(checksum_buf, sizeof(checksum_buf), 0x0, /* dst */ fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0x0, /* src */ fu_chunk_get_data_sz(chk), error)) return FALSE; *checksum_tmp += fu_sum32(checksum_buf, sizeof(checksum_buf)); /* GetReport */ memset(buf, 0x0, sizeof(buf)); if (!fu_hid_device_get_report(FU_HID_DEVICE(self), FU_QSI_DOCK_REPORT_ID, buf, sizeof(buf), FU_QSI_DOCK_MCU_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error)) return FALSE; /* MCU ACK 0:Pass 1:Fail */ if (buf[2] != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "ACK error for chunk %u", i); return FALSE; } fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_qsi_dock_mcu_device_write_chunks(FuQsiDockMcuDevice *self, FuChunkArray *chunks, guint32 *checksum, FuProgress *progress, GError **error) { fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_qsi_dock_mcu_device_write_chunk(self, chk, checksum, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to write chunk 0x%x: ", i); return FALSE; } fu_progress_step_done(progress); } return TRUE; } static gboolean fu_qsi_dock_mcu_device_wait_for_spi_initial_ready_cb(FuDevice *device, gpointer user_data, GError **error) { FuQsiDockMcuDevice *self = FU_QSI_DOCK_MCU_DEVICE(device); guint8 buf[64] = {FU_QSI_DOCK_REPORT_ID, FU_QSI_DOCK_CMD1_SPI, FU_QSI_DOCK_CMD2_SPI_EXTERNAL_FLASH_INI}; /* SetReport+GetReport */ if (!fu_hid_device_set_report(FU_HID_DEVICE(self), FU_QSI_DOCK_REPORT_ID, buf, sizeof(buf), FU_QSI_DOCK_MCU_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error)) return FALSE; memset(buf, 0x0, sizeof(buf)); if (!fu_hid_device_get_report(FU_HID_DEVICE(self), FU_QSI_DOCK_REPORT_ID, buf, sizeof(buf), FU_QSI_DOCK_MCU_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_qsi_dock_mcu_device_wait_for_spi_erase_ready_cb(FuQsiDockMcuDevice *self, guint32 length, gpointer user_data, GError **error) { guint8 buf[64] = {FU_QSI_DOCK_REPORT_ID, FU_QSI_DOCK_CMD1_SPI, FU_QSI_DOCK_CMD2_SPI_EXTERNAL_FLASH_ERASE}; guint32 offset = 0; guint8 fw_length[4]; guint8 flash_offset[4]; fu_memwrite_uint32(fw_length, length, G_LITTLE_ENDIAN); fu_memwrite_uint32(flash_offset, offset, G_LITTLE_ENDIAN); /* write erase flash size */ if (!fu_memcpy_safe(buf, sizeof(buf), 0x03, /* dst */ fw_length, sizeof(fw_length), 0x0, sizeof(fw_length), error)) return FALSE; if (!fu_memcpy_safe(buf, sizeof(buf), 0x07, /* dst */ flash_offset, sizeof(flash_offset), 0x0, sizeof(flash_offset), error)) return FALSE; /* SetReport+GetReport */ if (!fu_hid_device_set_report(FU_HID_DEVICE(self), FU_QSI_DOCK_REPORT_ID, buf, sizeof(buf), FU_QSI_DOCK_MCU_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error)) return FALSE; memset(buf, 0x0, sizeof(buf)); if (!fu_hid_device_get_report(FU_HID_DEVICE(self), FU_QSI_DOCK_REPORT_ID, buf, sizeof(buf), FU_QSI_DOCK_MCU_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error)) return FALSE; /* success */ return TRUE; } gboolean fu_qsi_dock_mcu_device_write_firmware_with_idx(FuQsiDockMcuDevice *self, FuFirmware *firmware, guint8 chip_idx, FuProgress *progress, FwupdInstallFlags flags, GError **error) { guint32 checksum_val = 0; g_autoptr(GBytes) fw_align = NULL; g_autoptr(GBytes) fw = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 90, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 10, NULL); /* align data */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; fw_align = fu_bytes_align(fw, FU_QSI_DOCK_EXTERN_FLASH_PAGE_SIZE, 0x0); /* initial external flash */ if (!fu_device_retry(FU_DEVICE(self), fu_qsi_dock_mcu_device_wait_for_spi_initial_ready_cb, 30, NULL, error)) { g_prefix_error(error, "failed to wait for initial: "); return FALSE; } if (!fu_qsi_dock_mcu_device_wait_for_spi_erase_ready_cb(self, g_bytes_get_size(fw), fu_progress_get_child(progress), error)) return FALSE; /* write external flash */ chunks = fu_chunk_array_new_from_bytes(fw_align, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_QSI_DOCK_EXTERN_FLASH_PAGE_SIZE); if (!fu_qsi_dock_mcu_device_write_chunks(self, chunks, &checksum_val, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* verify flash data */ if (!fu_qsi_dock_mcu_device_checksum(self, checksum_val, g_bytes_get_size(fw), error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_qsi_dock_mcu_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { return fu_qsi_dock_mcu_device_write_firmware_with_idx(FU_QSI_DOCK_MCU_DEVICE(device), firmware, 0xFF, progress, flags, error); } static gboolean fu_qsi_dock_mcu_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuQsiDockMcuDevice *self = FU_QSI_DOCK_MCU_DEVICE(device); if (!fu_qsi_dock_mcu_device_get_status(self, error)) return FALSE; /* success */ return TRUE; } static void fu_qsi_dock_mcu_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_qsi_dock_mcu_device_init(FuQsiDockMcuDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_hid_device_add_flag(FU_HID_DEVICE(self), FU_HID_DEVICE_FLAG_AUTODETECT_EPS); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_INHIBIT_CHILDREN); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_NO_SERIAL_NUMBER); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_NUMBER); fu_device_add_protocol(FU_DEVICE(self), "com.qsi.dock"); } static void fu_qsi_dock_mcu_device_class_init(FuQsiDockMcuDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_qsi_dock_mcu_device_setup; device_class->attach = fu_qsi_dock_mcu_device_attach; device_class->set_progress = fu_qsi_dock_mcu_device_set_progress; device_class->write_firmware = fu_qsi_dock_mcu_device_write_firmware; } fwupd-2.0.10/plugins/qsi-dock/fu-qsi-dock-mcu-device.h000066400000000000000000000011301501337203100224120ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_QSI_DOCK_MCU_DEVICE (fu_qsi_dock_mcu_device_get_type()) G_DECLARE_FINAL_TYPE(FuQsiDockMcuDevice, fu_qsi_dock_mcu_device, FU, QSI_DOCK_MCU_DEVICE, FuHidDevice) gboolean fu_qsi_dock_mcu_device_write_firmware_with_idx(FuQsiDockMcuDevice *self, FuFirmware *firmware, guint8 chip_idx, FuProgress *progress, FwupdInstallFlags flags, GError **error); fwupd-2.0.10/plugins/qsi-dock/fu-qsi-dock-plugin.c000066400000000000000000000016401501337203100216700ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * Copyright 2022 Kevin Chen * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-qsi-dock-child-device.h" #include "fu-qsi-dock-mcu-device.h" #include "fu-qsi-dock-plugin.h" struct _FuQsiDockPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuQsiDockPlugin, fu_qsi_dock_plugin, FU_TYPE_PLUGIN) static void fu_qsi_dock_plugin_init(FuQsiDockPlugin *self) { } static void fu_qsi_dock_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_set_device_gtype_default(plugin, FU_TYPE_QSI_DOCK_MCU_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_QSI_DOCK_CHILD_DEVICE); /* coverage */ } static void fu_qsi_dock_plugin_class_init(FuQsiDockPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_qsi_dock_plugin_constructed; } fwupd-2.0.10/plugins/qsi-dock/fu-qsi-dock-plugin.h000066400000000000000000000003611501337203100216740ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuQsiDockPlugin, fu_qsi_dock_plugin, FU, QSI_DOCK_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/qsi-dock/meson.build000066400000000000000000000006551501337203100202550ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginQsiDock"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('qsi-dock.quirk') plugin_builtins += static_library('fu_plugin_qsi_dock', sources: [ 'fu-qsi-dock-child-device.c', 'fu-qsi-dock-mcu-device.c', 'fu-qsi-dock-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) fwupd-2.0.10/plugins/qsi-dock/qsi-dock.quirk000066400000000000000000000001051501337203100206700ustar00rootroot00000000000000[USB\VID_047D&PID_808D] Plugin = qsi_dock GType = FuQsiDockMcuDevice fwupd-2.0.10/plugins/realtek-mst/000077500000000000000000000000001501337203100166235ustar00rootroot00000000000000fwupd-2.0.10/plugins/realtek-mst/README.md000066400000000000000000000034501501337203100201040ustar00rootroot00000000000000--- title: Plugin: Realtek MST --- ## Introduction This plugin updates the firmware of DisplayPort MST hub devices made by Realtek, such as the RTD2141b and RTD2142. These devices communicate over I²C, via the DisplayPort aux channel. Devices are declared by system firmware, and quirks specify the aux channel to which the device is connected for a given system. System firmware must specify the device's presence because while they can be identified partially through the presence of Realtek's OUI in the Branch Device OUI fields of DPCD (DisplayPort Configuration Data), they do not have unique Device Identification strings. This plugin was neither written, verified, supported or endorsed by Realtek Semiconductor Corp. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in an unspecified binary file format, which is written to the partition of the device flash that is not currently running. This plugin supports the following protocol ID: * `com.realtek.rtd2142` ## GUID Generation These devices use the standard I2C DeviceInstanceId values, e.g. * `I2C\NAME_1AF80175:00` In the case where the I2C name is generic, we can also use a per-system HWID value, for example: * `[I2C\NAME_AUX-C-DDI-C-PHY-C&HWID_9c908a5c-090e-5eb4-a7ba-a2ef8845a6b9]` ## Vendor ID security The vendor ID is specified by system firmware (such as ACPI tables). ## External Interface Access This plugin requires access to i2c buses associated with the specified DisplayPort aux channel, usually `/dev/i2c-5` or similar. ## Version Considerations This plugin has been available since fwupd version `1.6.2`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Peter Marheine: @tari fwupd-2.0.10/plugins/realtek-mst/fu-realtek-mst-device.c000066400000000000000000000635311501337203100230740ustar00rootroot00000000000000/* * Copyright 2021 Peter Marheine * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fu-realtek-mst-device.h" #include "fu-realtek-mst-struct.h" /* firmware debug address */ #define I2C_ADDR_DEBUG 0x35 /* programming address */ #define I2C_ADDR_ISP 0x4a /* write set to begin executing, cleared when done */ #define CMD_ERASE_BUSY 0x01 /* register for erase commands */ #define CMD_OPCODE_ERASE_SECTOR 0x20 #define CMD_OPCODE_ERASE_BLOCK 0xD8 /* register for read commands */ #define CMD_OPCODE_READ 0x03 /* register for write commands */ #define CMD_OPCODE_WRITE 0x02 /* when bit is set in mode register, ISP mode is active */ #define MCU_MODE_ISP (1 << 7) /* write set to begin write, reset by device when complete */ #define MCU_MODE_WRITE_BUSY (1 << 5) /* when bit is clear, write buffer contains data */ #define MCU_MODE_WRITE_BUF (1 << 4) /* GPIO configuration/access registers */ #define REG_GPIO88_CONFIG 0x104F #define REG_GPIO88_VALUE 0xFE3F /* flash chip properties */ #define FLASH_SIZE 0x100000 #define FLASH_SECTOR_SIZE 4096 #define FLASH_BLOCK_SIZE 65536 /* MST flash layout */ #define FLASH_USER1_ADDR 0x10000 #define FLASH_FLAG1_ADDR 0xfe304 #define FLASH_USER2_ADDR 0x80000 #define FLASH_FLAG2_ADDR 0xff304 #define FLASH_USER_SIZE 0x70000 struct _FuRealtekMstDevice { FuI2cDevice parent_instance; FuRealtekMstDeviceFlashBank active_bank; FuRealtekMstDeviceDualBankMode mode; }; G_DEFINE_TYPE(FuRealtekMstDevice, fu_realtek_mst_device, FU_TYPE_I2C_DEVICE) #define FU_REALTEK_MST_DEVICE_IOCTL_TIMEOUT 5000 /* ms */ static void fu_realtek_mst_device_to_string(FuDevice *device, guint idt, GString *str) { FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE(device); fwupd_codec_string_append(str, idt, "ActiveBank", fu_realtek_mst_device_flash_bank_to_string(self->active_bank)); fwupd_codec_string_append(str, idt, "Mode", fu_realtek_mst_device_dual_bank_mode_to_string(self->mode)); } static gboolean fu_realtek_mst_device_write_register(FuRealtekMstDevice *self, guint8 address, guint8 value, GError **error) { const guint8 command[] = {address, value}; return fu_i2c_device_write(FU_I2C_DEVICE(self), command, sizeof(command), error); } static gboolean fu_realtek_mst_device_write_register_multi(FuRealtekMstDevice *self, guint8 address, const guint8 *data, gsize count, GError **error) { g_autofree guint8 *command = g_malloc0(count + 1); memcpy(command + 1, data, count); /* nocheck:blocked */ command[0] = address; return fu_i2c_device_write(FU_I2C_DEVICE(self), command, count + 1, error); } static gboolean fu_realtek_mst_device_read_register(FuRealtekMstDevice *self, guint8 address, guint8 *value, GError **error) { if (!fu_i2c_device_write(FU_I2C_DEVICE(self), &address, 0x1, error)) return FALSE; return fu_i2c_device_read(FU_I2C_DEVICE(self), value, 0x1, error); } static gboolean fu_realtek_mst_device_set_indirect_address(FuRealtekMstDevice *self, guint16 address, GError **error) { if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_INDIRECT_LO, 0x9F, error)) return FALSE; if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_INDIRECT_HI, address >> 8, error)) return FALSE; return fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_INDIRECT_LO, address, error); } static gboolean fu_realtek_mst_device_read_register_indirect(FuRealtekMstDevice *self, guint16 address, guint8 *value, GError **error) { if (!fu_realtek_mst_device_set_indirect_address(self, address, error)) return FALSE; return fu_realtek_mst_device_read_register(self, FU_REALTEK_MST_REG_INDIRECT_HI, value, error); } static gboolean fu_realtek_mst_device_write_register_indirect(FuRealtekMstDevice *self, guint16 address, guint8 value, GError **error) { if (!fu_realtek_mst_device_set_indirect_address(self, address, error)) return FALSE; return fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_INDIRECT_HI, value, error); } /** * Wait until a device register reads an expected value. * * Waiting up to @timeout_seconds, poll the given @address for the read value * bitwise-ANDed with @mask to be equal to @expected. * * Returns an error if the timeout expires or in case of an I/O error. */ static gboolean fu_realtek_mst_device_poll_register(FuRealtekMstDevice *self, guint8 address, guint8 mask, guint8 expected, guint timeout_seconds, GError **error) { guint8 value; g_autoptr(GTimer) timer = g_timer_new(); if (!fu_realtek_mst_device_read_register(self, address, &value, error)) return FALSE; while ((value & mask) != expected && g_timer_elapsed(timer, NULL) <= timeout_seconds) { fu_device_sleep(FU_DEVICE(self), 1); /* ms */ if (!fu_realtek_mst_device_read_register(self, address, &value, error)) return FALSE; } if ((value & mask) == expected) return TRUE; g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "register %x still reads %x after %us, wanted %x (mask %x)", address, value, timeout_seconds, expected, mask); return FALSE; } static gboolean fu_realtek_mst_device_set_gpio88(FuRealtekMstDevice *self, gboolean level, GError **error) { guint8 value; /* ensure pin is configured as push-pull GPIO */ if (!fu_realtek_mst_device_read_register_indirect(self, REG_GPIO88_CONFIG, &value, error)) return FALSE; if (!fu_realtek_mst_device_write_register_indirect(self, REG_GPIO88_CONFIG, (value & 0xF0) | 1, error)) return FALSE; /* set output level */ g_debug("set pin 88 = %d", level); if (!fu_realtek_mst_device_read_register_indirect(self, REG_GPIO88_VALUE, &value, error)) return FALSE; return fu_realtek_mst_device_write_register_indirect(self, REG_GPIO88_VALUE, (value & 0xFE) | (level != FALSE), error); } static gboolean fu_realtek_mst_device_setup(FuDevice *device, GError **error) { FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE(device); const guint8 request[] = {0x01}; guint8 response[11] = {0x0}; g_autofree gchar *version = NULL; if (!fu_i2c_device_set_address(FU_I2C_DEVICE(self), I2C_ADDR_DEBUG, FALSE, error)) { g_prefix_error(error, "failed to ensure address: "); return FALSE; } /* switch to DDCCI mode */ if (!fu_realtek_mst_device_write_register(self, 0xca, 0x09, error)) return FALSE; /* wait for mode switch to complete */ fu_device_sleep(FU_DEVICE(self), 200); /* ms */ /* request dual bank state and read back */ if (!fu_i2c_device_write(FU_I2C_DEVICE(self), request, sizeof(request), error)) return FALSE; if (!fu_i2c_device_read(FU_I2C_DEVICE(self), response, sizeof(response), error)) return FALSE; if (response[0] != 0xCA || response[1] != 9) { /* unexpected response code or length usually means the current * firmware doesn't support dual-bank mode at all */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unexpected response code %#x, length %d", response[0], response[1]); return FALSE; } /* enable flag, assume anything other than 1 is unsupported */ if (response[2] != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "dual-bank mode is not enabled"); return FALSE; } self->mode = response[3]; if (self->mode != FU_REALTEK_MST_DEVICE_DUAL_BANK_MODE_DIFF) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unexpected dual bank mode value %#x", self->mode); return FALSE; } self->active_bank = response[4]; if (self->active_bank >= FU_REALTEK_MST_DEVICE_FLASH_BANK_LAST) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unexpected active flash bank value %#x", self->active_bank); return FALSE; } if (self->active_bank == FU_REALTEK_MST_DEVICE_FLASH_BANK_USER1) { version = g_strdup_printf("%u.%u", response[5], response[6]); } else if (self->active_bank == FU_REALTEK_MST_DEVICE_FLASH_BANK_USER2) { version = g_strdup_printf("%u.%u", response[7], response[8]); } fu_device_set_version(device, version); /* last two bytes of response are reserved */ return TRUE; } static gboolean fu_realtek_mst_device_flash_iface_read(FuRealtekMstDevice *self, guint32 address, guint8 *buf, const gsize buf_size, FuProgress *progress, GError **error) { gsize bytes_read = 0; guint8 byte; const guint8 req[] = {0x70}; g_return_val_if_fail(address < FLASH_SIZE, FALSE); g_return_val_if_fail(buf_size <= FLASH_SIZE, FALSE); g_debug("read %#" G_GSIZE_MODIFIER "x bytes from %#08x", buf_size, address); /* read must start one byte prior to the desired address and ignore the * first byte of data, since the first read value is unpredictable */ address = (address - 1) & 0xFFFFFF; if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_CMD_ADDR_HI, address >> 16, error)) return FALSE; if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_CMD_ADDR_MID, address >> 8, error)) return FALSE; if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_CMD_ADDR_LO, address, error)) return FALSE; if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_READ_OPCODE, CMD_OPCODE_READ, error)) return FALSE; /* ignore first byte of data */ if (!fu_i2c_device_write(FU_I2C_DEVICE(self), req, sizeof(req), error)) return FALSE; if (!fu_i2c_device_read(FU_I2C_DEVICE(self), &byte, 0x1, error)) return FALSE; while (bytes_read < buf_size) { /* read up to 256 bytes in one transaction */ gsize read_len = buf_size - bytes_read; if (read_len > 256) read_len = 256; if (!fu_i2c_device_read(FU_I2C_DEVICE(self), buf + bytes_read, read_len, error)) return FALSE; bytes_read += read_len; fu_progress_set_percentage_full(progress, bytes_read, buf_size); } return TRUE; } static gboolean fu_realtek_mst_device_flash_iface_erase_sector(FuRealtekMstDevice *self, guint32 address, GError **error) { /* address must be 4k-aligned */ g_return_val_if_fail((address & 0xFFF) == 0, FALSE); g_debug("sector erase %#08x-%#08x", address, address + FLASH_SECTOR_SIZE); /* sector address */ if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_CMD_ADDR_HI, address >> 16, error)) return FALSE; if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_CMD_ADDR_MID, address >> 8, error)) return FALSE; if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_CMD_ADDR_LO, address, error)) return FALSE; /* command type + WREN */ if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_CMD_ATTR, 0xB8, error)) return FALSE; if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_ERASE_OPCODE, CMD_OPCODE_ERASE_SECTOR, error)) return FALSE; /* begin operation and wait for completion */ if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_CMD_ATTR, 0xB8 | CMD_ERASE_BUSY, error)) return FALSE; return fu_realtek_mst_device_poll_register(self, FU_REALTEK_MST_REG_CMD_ATTR, CMD_ERASE_BUSY, 0, 10, error); } static gboolean fu_realtek_mst_device_flash_iface_erase_block(FuRealtekMstDevice *self, guint32 address, GError **error) { /* address must be 64k-aligned */ g_return_val_if_fail((address & 0xFFFF) == 0, FALSE); g_debug("block erase %#08x-%#08x", address, address + FLASH_BLOCK_SIZE); /* block address */ if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_CMD_ADDR_HI, address >> 16, error)) return FALSE; if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_CMD_ADDR_MID, 0, error)) return FALSE; if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_CMD_ADDR_LO, 0, error)) return FALSE; /* command type + WREN */ if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_CMD_ATTR, 0xB8, error)) return FALSE; if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_ERASE_OPCODE, CMD_OPCODE_ERASE_BLOCK, error)) return FALSE; /* begin operation and wait for completion */ if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_CMD_ATTR, 0xB8 | CMD_ERASE_BUSY, error)) return FALSE; return fu_realtek_mst_device_poll_register(self, FU_REALTEK_MST_REG_CMD_ATTR, CMD_ERASE_BUSY, 0, 10, error); } static gboolean fu_realtek_mst_device_flash_iface_write(FuRealtekMstDevice *self, guint32 address, GBytes *data, FuProgress *progress, GError **error) { gsize total_size = g_bytes_get_size(data); g_autoptr(FuChunkArray) chunks = fu_chunk_array_new_from_bytes(data, address, FU_CHUNK_PAGESZ_NONE, 256); g_debug("write %#" G_GSIZE_MODIFIER "x bytes at %#08x", total_size, address); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chunk = NULL; guint32 chunk_address; guint32 chunk_size; /* prepare chunk */ chunk = fu_chunk_array_index(chunks, i, error); if (chunk == NULL) return FALSE; chunk_address = fu_chunk_get_address(chunk); chunk_size = fu_chunk_get_data_sz(chunk); /* write opcode */ if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_WRITE_OPCODE, CMD_OPCODE_WRITE, error)) return FALSE; /* write length */ if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_WRITE_LEN, chunk_size - 1, error)) return FALSE; /* target address */ if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_CMD_ADDR_HI, chunk_address >> 16, error)) return FALSE; if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_CMD_ADDR_MID, chunk_address >> 8, error)) return FALSE; if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_CMD_ADDR_LO, chunk_address, error)) return FALSE; /* ensure write buffer is empty */ if (!fu_realtek_mst_device_poll_register(self, FU_REALTEK_MST_REG_MCU_MODE, MCU_MODE_WRITE_BUF, MCU_MODE_WRITE_BUF, 10, error)) { g_prefix_error(error, "failed waiting for write buffer to clear: "); return FALSE; } /* write data into FIFO */ if (!fu_realtek_mst_device_write_register_multi(self, FU_REALTEK_MST_REG_WRITE_FIFO, fu_chunk_get_data(chunk), chunk_size, error)) return FALSE; /* begin operation and wait for completion */ if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_MCU_MODE, MCU_MODE_ISP | MCU_MODE_WRITE_BUSY, error)) return FALSE; if (!fu_realtek_mst_device_poll_register(self, FU_REALTEK_MST_REG_MCU_MODE, MCU_MODE_WRITE_BUSY, 0, 10, error)) { g_prefix_error(error, "timed out waiting for write at %#x to complete: ", address); return FALSE; } fu_progress_set_percentage_full(progress, i + 1, fu_chunk_array_length(chunks)); } return TRUE; } static gboolean fu_realtek_mst_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE(device); if (!fu_i2c_device_set_address(FU_I2C_DEVICE(self), I2C_ADDR_ISP, FALSE, error)) return FALSE; /* Switch to programming mode (stops regular operation) */ if (!fu_realtek_mst_device_write_register(self, FU_REALTEK_MST_REG_MCU_MODE, MCU_MODE_ISP, error)) return FALSE; g_debug("wait for ISP mode ready"); if (!fu_realtek_mst_device_poll_register(self, FU_REALTEK_MST_REG_MCU_MODE, MCU_MODE_ISP, MCU_MODE_ISP, 60, error)) return FALSE; /* magic value makes the MCU clock run faster than normal; this both * helps programming performance and fixes flakiness where register * writes sometimes get nacked for no apparent reason */ if (!fu_realtek_mst_device_write_register_indirect(self, 0x06A0, 0x74, error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); /* Disable hardware write protect, assuming Flash ~WP is connected to * device pin 88, a GPIO. */ return fu_realtek_mst_device_set_gpio88(self, 1, error); } static gboolean fu_realtek_mst_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE(device); /* write an inactive bank: USER2 if USER1 is active, otherwise USER1 * (including if the boot bank is active) */ guint32 base_addr = self->active_bank == FU_REALTEK_MST_DEVICE_FLASH_BANK_USER1 ? FLASH_USER2_ADDR : FLASH_USER1_ADDR; guint32 flag_addr = self->active_bank == FU_REALTEK_MST_DEVICE_FLASH_BANK_USER1 ? FLASH_FLAG2_ADDR : FLASH_FLAG1_ADDR; const guint8 flag_data[] = {0xaa, 0xaa, 0xaa, 0xff, 0xff}; g_autofree guint8 *readback_buf = g_malloc0(FLASH_USER_SIZE); g_autoptr(GBytes) firmware_bytes = NULL; /* sanity check */ firmware_bytes = fu_firmware_get_bytes(firmware, error); if (firmware_bytes == NULL) return FALSE; if (g_bytes_get_size(firmware_bytes) != FLASH_USER_SIZE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid size, got 0x%x, expected 0x%x", (guint)g_bytes_get_size(firmware_bytes), (guint)FLASH_USER_SIZE); return FALSE; } /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 20, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 70, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 9, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "flag"); if (!fu_i2c_device_set_address(FU_I2C_DEVICE(self), I2C_ADDR_ISP, FALSE, error)) return FALSE; /* erase old image */ g_debug("erase old image from %#x", base_addr); for (guint32 offset = 0; offset < FLASH_USER_SIZE; offset += FLASH_BLOCK_SIZE) { if (!fu_realtek_mst_device_flash_iface_erase_block(self, base_addr + offset, error)) return FALSE; fu_progress_set_percentage_full(fu_progress_get_child(progress), offset + FLASH_BLOCK_SIZE, FLASH_USER_SIZE); } fu_progress_step_done(progress); /* write new image */ g_debug("write new image to %#x", base_addr); if (!fu_realtek_mst_device_flash_iface_write(self, base_addr, firmware_bytes, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* verify */ if (!fu_realtek_mst_device_flash_iface_read(self, base_addr, readback_buf, FLASH_USER_SIZE, fu_progress_get_child(progress), error)) return FALSE; if (memcmp(g_bytes_get_data(firmware_bytes, NULL), readback_buf, FLASH_USER_SIZE) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "flash contents after write do not match firmware image"); return FALSE; } fu_progress_step_done(progress); /* Erase old flag and write new one. The MST appears to modify the * flag value once booted, so we always write the same value here and * it picks up what we've updated. */ if (!fu_realtek_mst_device_flash_iface_erase_sector(self, flag_addr & ~(FLASH_SECTOR_SIZE - 1), error)) return FALSE; if (!fu_realtek_mst_device_flash_iface_write( self, flag_addr, g_bytes_new_static(flag_data, sizeof(flag_data)), fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static FuFirmware * fu_realtek_mst_device_read_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE(device); guint32 bank_address; g_autofree guint8 *image_bytes = NULL; g_autoptr(GBytes) firmware_bytes = NULL; if (self->active_bank == FU_REALTEK_MST_DEVICE_FLASH_BANK_USER1) bank_address = FLASH_USER1_ADDR; else if (self->active_bank == FU_REALTEK_MST_DEVICE_FLASH_BANK_USER2) bank_address = FLASH_USER2_ADDR; else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot read firmware from bank %u", self->active_bank); return NULL; } image_bytes = g_malloc0(FLASH_USER_SIZE); if (!fu_i2c_device_set_address(FU_I2C_DEVICE(self), I2C_ADDR_ISP, FALSE, error)) return NULL; if (!fu_realtek_mst_device_flash_iface_read(self, bank_address, image_bytes, FLASH_USER_SIZE, progress, error)) return NULL; firmware_bytes = g_bytes_new_take(g_steal_pointer(&image_bytes), FLASH_USER_SIZE); return fu_firmware_new_from_bytes(firmware_bytes); } static GBytes * fu_realtek_mst_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE(device); g_autofree guint8 *flash_contents = g_malloc0(FLASH_SIZE); if (!fu_i2c_device_set_address(FU_I2C_DEVICE(self), I2C_ADDR_ISP, FALSE, error)) return NULL; fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ); if (!fu_realtek_mst_device_flash_iface_read(self, 0, flash_contents, FLASH_SIZE, progress, error)) return NULL; fu_progress_set_status(progress, FWUPD_STATUS_IDLE); return g_bytes_new_take(g_steal_pointer(&flash_contents), FLASH_SIZE); } static gboolean fu_realtek_mst_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuRealtekMstDevice *self = FU_REALTEK_MST_DEVICE(device); guint8 value; if (!fu_i2c_device_set_address(FU_I2C_DEVICE(self), I2C_ADDR_ISP, FALSE, error)) return FALSE; /* re-enable hardware write protect via GPIO */ if (!fu_realtek_mst_device_set_gpio88(self, 0, error)) return FALSE; if (!fu_realtek_mst_device_read_register(self, FU_REALTEK_MST_REG_MCU_MODE, &value, error)) return FALSE; if ((value & MCU_MODE_ISP) != 0) { g_autoptr(GError) error_local = NULL; g_debug("resetting device to exit ISP mode"); /* Set register EE bit 2 to request reset. This write can fail * spuriously, so we ignore the write result and verify the device is * no longer in programming mode after giving it time to reset. */ if (!fu_realtek_mst_device_read_register(self, 0xEE, &value, error)) return FALSE; if (!fu_realtek_mst_device_write_register(self, 0xEE, value | 2, &error_local)) { g_debug("write spuriously failed, ignoring: %s", error_local->message); } /* allow device some time to reset */ fu_device_sleep(device, 1000); /* ms */ /* verify device has exited programming mode and actually reset */ if (!fu_realtek_mst_device_read_register(self, FU_REALTEK_MST_REG_MCU_MODE, &value, error)) return FALSE; if ((value & MCU_MODE_ISP) == MCU_MODE_ISP) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NEEDS_USER_ACTION, "device failed to reset when requested"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN); return FALSE; } } else { g_debug("device is already in normal mode"); } fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); return TRUE; } static void fu_realtek_mst_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static void fu_realtek_mst_device_init(FuRealtekMstDevice *self) { self->active_bank = FU_REALTEK_MST_DEVICE_FLASH_BANK_LAST; fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PAIR); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_NO_GENERIC_GUIDS); fu_device_add_protocol(FU_DEVICE(self), "com.realtek.rtd2142"); fu_device_set_vendor(FU_DEVICE(self), "Realtek"); fu_device_build_vendor_id_u16(FU_DEVICE(self), "PCI", 0x10EC); fu_device_set_summary(FU_DEVICE(self), "DisplayPort MST hub"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_VIDEO_DISPLAY); fu_device_set_firmware_size(FU_DEVICE(self), FLASH_USER_SIZE); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); } static void fu_realtek_mst_device_class_init(FuRealtekMstDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_realtek_mst_device_to_string; device_class->setup = fu_realtek_mst_device_setup; device_class->detach = fu_realtek_mst_device_detach; device_class->attach = fu_realtek_mst_device_attach; device_class->write_firmware = fu_realtek_mst_device_write_firmware; device_class->reload = fu_realtek_mst_device_setup; device_class->read_firmware = fu_realtek_mst_device_read_firmware; device_class->dump_firmware = fu_realtek_mst_device_dump_firmware; device_class->set_progress = fu_realtek_mst_device_set_progress; } fwupd-2.0.10/plugins/realtek-mst/fu-realtek-mst-device.h000066400000000000000000000005061501337203100230720ustar00rootroot00000000000000/* * Copyright 2021 Peter Marheine * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_REALTEK_MST_DEVICE (fu_realtek_mst_device_get_type()) G_DECLARE_FINAL_TYPE(FuRealtekMstDevice, fu_realtek_mst_device, FU, REALTEK_MST_DEVICE, FuI2cDevice) fwupd-2.0.10/plugins/realtek-mst/fu-realtek-mst-plugin.c000066400000000000000000000014671501337203100231330ustar00rootroot00000000000000/* * Copyright 2021 Peter Marheine * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-realtek-mst-device.h" #include "fu-realtek-mst-plugin.h" struct _FuRealtekMstPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuRealtekMstPlugin, fu_realtek_mst_plugin, FU_TYPE_PLUGIN) static void fu_realtek_mst_plugin_init(FuRealtekMstPlugin *self) { } static void fu_realtek_mst_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_udev_subsystem(plugin, "i2c"); fu_plugin_add_device_gtype(plugin, FU_TYPE_REALTEK_MST_DEVICE); } static void fu_realtek_mst_plugin_class_init(FuRealtekMstPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_realtek_mst_plugin_constructed; } fwupd-2.0.10/plugins/realtek-mst/fu-realtek-mst-plugin.h000066400000000000000000000003721501337203100231320ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuRealtekMstPlugin, fu_realtek_mst_plugin, FU, REALTEK_MST_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/realtek-mst/fu-realtek-mst.rs000066400000000000000000000013151501337203100220310ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] enum FuRealtekMstDeviceDualBankMode { UserOnly, Diff, Copy, UserOnlyFlag, } #[derive(ToString)] enum FuRealtekMstDeviceFlashBank { Boot, User1, User2, } enum FuRealtekMstReg { CmdAttr = 0x60, EraseOpcode = 0x61, CmdAddrHi = 0x64, // for commands CmdAddrMid = 0x65, CmdAddrLo = 0x66, ReadOpcode = 0x6A, WriteOpcode = 0x6D, McuMode = 0x6F, // mode register address WriteFifo = 0x70, // write data into write buffer WriteLen = 0x71, // number of bytes to write minus 1 IndirectLo = 0xF4, IndirectHi = 0xF5, } fwupd-2.0.10/plugins/realtek-mst/meson.build000066400000000000000000000010531501337203100207640ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginRealtekMst"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('realtek-mst.quirk') plugin_builtins += static_library('fu_plugin_realtek_mst', rustgen.process( 'fu-realtek-mst.rs', ), sources: [ 'fu-realtek-mst-device.c', 'fu-realtek-mst-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: [ cargs, '-DLOCALSTATEDIR="' + localstatedir + '"', ], dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/realtek-mst/realtek-mst.quirk000066400000000000000000000004471501337203100221350ustar00rootroot00000000000000# Manufacturer + ProductSku -> Google + Hatch [I2C\NAME_10EC2141:00&HWID_9c908a5c-090e-5eb4-a7ba-a2ef8845a6b8] Plugin = realtek_mst Name = RTD2142 # Manufacturer + ProductSku -> Google + Kaisa [I2C\NAME_10EC2142:00&HWID_9c908a5c-090e-5eb4-a7ba-a2ef8845a6b8] Plugin = realtek_mst Name = RTD2142 fwupd-2.0.10/plugins/redfish/000077500000000000000000000000001501337203100160175ustar00rootroot00000000000000fwupd-2.0.10/plugins/redfish/README.md000066400000000000000000000101041501337203100172720ustar00rootroot00000000000000--- title: Plugin: Redfish --- ## Introduction Redfish is an open industry standard specification and schema that helps enable simple and secure management of modern scalable platform hardware. By specifying a RESTful interface and utilizing JSON and OData, Redfish helps customers integrate solutions within their existing tool chains. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in an unspecified binary file format. This plugin supports the following protocol ID: * `org.dmtf.redfish` ## GUID Generation These devices use the provided GUID provided in the `SoftwareId` property without modification if it is a valid GUID. On HPE machines the `Hpe/DeviceClass` and `Hpe/Targets[]` GUIDs are also added if provided. If the `SoftwareId` property is not a GUID then the vendor instance ID is used instead: * `REDFISH\VENDOR_${RedfishManufacturer}&SOFTWAREID_${RedfishSoftwareId}` Additionally, on Dell hardware the SystemID is also used: * `REDFISH\VENDOR_${RedfishManufacturer}&SYSTEMID_${RedfishSystemID}&SOFTWAREID_${RedfishSoftwareId}` * `REDFISH\VENDOR_${RedfishManufacturer}&SYSTEMID_${RedfishSystemID}&SOFTWAREID` (only-quirks) Additionally, this Instance ID is added for quirk and parent matching: * `REDFISH\VENDOR_${RedfishManufacturer}&ID_${RedfishId}` ## Update Behavior The firmware will be deployed as appropriate. The Redfish API does not specify when the firmware will actually be written to the SPI device. ## Vendor ID Security No vendor ID is set as there is no vendor field in the schema. ## Quirk Use This plugin uses the following plugin-specific quirks: ### RedfishResetPreDelay Delay in ms to use before querying the manager after a cleanup reset, default 0ms. Since: 1.8.0 ### RedfishResetPostDelay Delay in ms to use before querying /redfish/v1/UpdateService after a cleanup reset, default 0ms. Since: 1.8.0 ### `Flags=wildcard-targets` Do not specify the `odata.id` in the multipart update Targets array and allow the BMC to deploy the firmware onto all compatible hardware. To use this option the payload must contain metadata that restricts it to a specific SoftwareId. ### `Flags=no-manager-reset-request` The BMC device will auto-reboot and so fwupd should not explicitly call `/redfish/v1/Managers/1/Actions/Manager.Reset`. Since: 1.9.11 ### `Flags=is-backup` The device is the other half of a dual image firmware. ### `Flags=unsigned-build` Use unsigned development builds. ### `Flags=manager-reset` Reset the manager (typically the BMC) after updating this device. ## Setting Service IP Manually The service IP may not be automatically discoverable due to the absence of Type 42 entry in SMBIOS. In this case, you have to specify the service IP to RedfishUri in /etc/fwupd/redfish.conf Take HPE Gen10 for example, the service IP can be found with the following command: ```shell ilorest --nologo list --selector=EthernetInterface. -j ``` This command lists all network interfaces, and the Redfish service IP belongs to one of "Manager Network" Interfaces. For example: ```json { "@odata.context": "/redfish/v1/$metadata#EthernetInterface.EthernetInterface", "@odata.id": "/redfish/v1/Managers/1/EthernetInterfaces/1/", "@odata.type": "#EthernetInterface.v1_0_3.EthernetInterface", "Description": "Configuration of this Manager Network Interface", "HostName": "myredfish", "IPv4Addresses": [ { "SubnetMask": "255.255.255.0", "AddressOrigin": "DHCP", "Gateway": "192.168.0.1", "Address": "192.168.0.133" } ], ... ``` In this example, the service IP is "192.168.0.133". Since the conventional HTTP port is 80 and HTTPS port is 443, we can set RedfishUri to either "" or "" and verify the uri with ```shell curl http://192.168.0.133:80/redfish/v1/ ``` or ```shell curl -k https://192.168.0.133:443/redfish/v1/ ``` ## External Interface Access This requires HTTP access to a given URL. ## Version Considerations This plugin has been available since fwupd version `1.1.0`. fwupd-2.0.10/plugins/redfish/fu-ipmi-device.c000066400000000000000000000501261501337203100207720ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include #include #include #include #include #include #include #include "fu-ipmi-device.h" #define FU_IPMI_DEVICE_TIMEOUT 1500 /* ms */ #define FU_IPMI_TRANSACTION_RETRY_COUNT 5 #define FU_IPMI_TRANSACTION_RETRY_DELAY 200 /* ms */ /* not defined in linux/ipmi_msgdefs.h */ #define IPMI_SET_USER_ACCESS 0x43 #define IPMI_SET_USER_NAME 0x45 #define IPMI_GET_USER_NAME 0x46 #define IPMI_SET_USER_PASSWORD 0x47 #define IPMI_PASSWORD_DISABLE_USER 0x00 #define IPMI_PASSWORD_ENABLE_USER 0x01 #define IPMI_PASSWORD_SET_PASSWORD 0x02 #define IPMI_PASSWORD_TEST_PASSWORD 0x03 /* these are not provided in ipmi_msgdefs.h */ #define IPMI_INVALID_COMMAND_ON_LUN_ERR 0xC2 #define IPMI_OUT_OF_SPACE_ERR 0xC4 #define IPMI_CANCELLED_OR_INVALID_ERR 0xC5 #define IPMI_OUT_OF_RANGE_ERR 0xC9 #define IPMI_CANNOT_RETURN_DATA_ERR 0xCA #define IPMI_NOT_FOUND_ERR 0xCB #define IPMI_INVALID_DATA_FIELD_ERR 0xCC #define IPMI_COMMAND_ILLEGAL_ERR 0xCD #define IPMI_RESPONSE_NOT_PROVIDED_ERR 0xCE #define IPMI_DUPLICATED_REQUEST_ERR 0xCF #define IPMI_SDR_IN_UPDATE_MODE_ERR 0xD0 #define IPMI_DESTINATION_UNAVAILABLE_ERR 0xD3 #define IPMI_INSUFFICIENT_PRIVILEGE_ERR 0xD4 #define IPMI_COMMAND_DISABLED_ERR 0xD6 #ifndef IPMI_DEVICE_IN_UPDATE_MODE_ERR #define IPMI_DEVICE_IN_UPDATE_MODE_ERR 0xD1 #endif #ifndef IPMI_DEVICE_IN_INIT_ERR #define IPMI_DEVICE_IN_INIT_ERR 0xD2 #endif struct _FuIpmiDevice { FuUdevDevice parent_instance; glong seq; guint8 device_id; guint8 device_rev; guint8 version_ipmi; }; G_DEFINE_TYPE(FuIpmiDevice, fu_ipmi_device, FU_TYPE_UDEV_DEVICE) #define FU_IPMI_DEVICE_IOCTL_TIMEOUT 5000 /* ms */ static void fu_ipmi_device_to_string(FuDevice *device, guint idt, GString *str) { FuIpmiDevice *self = FU_IPMI_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "DeviceId", self->device_id); fwupd_codec_string_append_hex(str, idt, "DeviceRev", self->device_rev); fwupd_codec_string_append_hex(str, idt, "VersionIpmi", self->version_ipmi); } static gboolean fu_ipmi_device_ioctl_buffer_cb(FuIoctl *self, gpointer ptr, guint8 *buf, gsize bufsz, GError **error) { struct ipmi_req *req = (struct ipmi_req *)ptr; req->msg.data = buf; req->msg.data_len = (guint16)bufsz; return TRUE; } static gboolean fu_ipmi_device_ioctl_addr_cb(FuIoctl *self, gpointer ptr, guint8 *buf, gsize bufsz, GError **error) { struct ipmi_req *req = (struct ipmi_req *)ptr; req->addr = buf; req->addr_len = bufsz; return TRUE; } static gboolean fu_ipmi_device_send(FuIpmiDevice *self, guint8 netfn, guint8 cmd, const guint8 *buf, gsize bufsz, GError **error) { struct ipmi_system_interface_addr addr = { .addr_type = IPMI_SYSTEM_INTERFACE_ADDR_TYPE, .channel = IPMI_BMC_CHANNEL, }; struct ipmi_req req = { .msgid = self->seq++, .msg.netfn = netfn, .msg.cmd = cmd, }; g_autofree guint8 *buf2 = NULL; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); if (buf != NULL) { buf2 = fu_memdup_safe(buf, bufsz, error); if (buf2 == NULL) return FALSE; fu_dump_raw(G_LOG_DOMAIN, "ipmi-send", buf2, bufsz); } /* include these when generating the emulation event */ fu_ioctl_add_key_as_u16(ioctl, "Request", IPMICTL_SEND_COMMAND); fu_ioctl_add_key_as_u8(ioctl, "Msgid", req.msgid); fu_ioctl_add_key_as_u8(ioctl, "MsgNetfn", req.msg.netfn); fu_ioctl_add_key_as_u8(ioctl, "MsgCmd", req.msg.cmd); fu_ioctl_add_const_buffer(ioctl, NULL, buf2, bufsz, fu_ipmi_device_ioctl_buffer_cb); fu_ioctl_add_const_buffer(ioctl, NULL, (const guint8 *)&addr, sizeof(addr), fu_ipmi_device_ioctl_addr_cb); return fu_ioctl_execute(ioctl, IPMICTL_SEND_COMMAND, (guint8 *)&req, sizeof(req), NULL, FU_IPMI_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_NONE, error); } static gboolean fu_ipmi_device_recv(FuIpmiDevice *self, guint8 *netfn, guint8 *cmd, glong *seq, guint8 *buf, gsize bufsz, gsize *len, /* optional, out */ GError **error) { struct ipmi_addr addr = {0}; struct ipmi_recv recv = { .addr = (guint8 *)&addr, .addr_len = sizeof(addr), .msg.data = buf, .msg.data_len = bufsz, }; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); fu_ioctl_add_key_as_u16(ioctl, "Request", IPMICTL_RECEIVE_MSG_TRUNC); if (!fu_ioctl_execute(ioctl, IPMICTL_RECEIVE_MSG_TRUNC, (guint8 *)&recv, sizeof(recv), NULL, FU_IPMI_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_NONE, error)) return FALSE; fu_dump_raw(G_LOG_DOMAIN, "ipmi-recv", buf, bufsz); if (netfn != NULL) *netfn = recv.msg.netfn; if (cmd != NULL) *cmd = recv.msg.cmd; if (seq != NULL) *seq = recv.msgid; if (len != NULL) *len = (gsize)recv.msg.data_len; return TRUE; } static gboolean fu_ipmi_device_lock(GObject *device, GError **error) { FuIpmiDevice *self = FU_IPMI_DEVICE(device); FuIOChannel *io_channel = fu_udev_device_get_io_channel(FU_UDEV_DEVICE(self)); struct flock lock = {.l_type = F_WRLCK, .l_whence = SEEK_SET}; if (fcntl(fu_io_channel_unix_get_fd(io_channel), F_SETLKW, &lock) == -1) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "error locking IPMI device: %m"); return FALSE; } return TRUE; } static gboolean fu_ipmi_device_unlock(GObject *device, GError **error) { FuIpmiDevice *self = FU_IPMI_DEVICE(device); FuIOChannel *io_channel = fu_udev_device_get_io_channel(FU_UDEV_DEVICE(self)); struct flock lock = {.l_type = F_UNLCK}; if (fcntl(fu_io_channel_unix_get_fd(io_channel), F_SETLKW, &lock) == -1) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "error unlocking IPMI device: %m"); return FALSE; } return TRUE; } static const gchar * fu_ipmi_device_errcode_to_string(guint8 errcode) { if (errcode == IPMI_CC_NO_ERROR) return "no-error"; if (errcode == IPMI_NODE_BUSY_ERR) return "node-busy"; if (errcode == IPMI_INVALID_COMMAND_ERR) return "invalid-command"; if (errcode == IPMI_TIMEOUT_ERR) return "timeout"; if (errcode == IPMI_ERR_MSG_TRUNCATED) return "msg-truncated"; if (errcode == IPMI_REQ_LEN_INVALID_ERR) return "req-len-invalid"; if (errcode == IPMI_REQ_LEN_EXCEEDED_ERR) return "req-len-exceeded"; if (errcode == IPMI_DEVICE_IN_UPDATE_MODE_ERR) return "device-in-update-mode"; if (errcode == IPMI_DEVICE_IN_INIT_ERR) return "device-in-init"; if (errcode == IPMI_NOT_IN_MY_STATE_ERR) return "not-in-my-state"; if (errcode == IPMI_LOST_ARBITRATION_ERR) return "lost-arbitration"; if (errcode == IPMI_BUS_ERR) return "bus-error"; if (errcode == IPMI_NAK_ON_WRITE_ERR) return "nak-on-write"; if (errcode == IPMI_ERR_UNSPECIFIED) return "unspecified"; /* these are not defined in ipmi_msgdefs.h but used in reality */ if (errcode == IPMI_INVALID_COMMAND_ON_LUN_ERR) return "invalid-command-on-lun"; if (errcode == IPMI_OUT_OF_SPACE_ERR) return "out-of-space"; if (errcode == IPMI_CANCELLED_OR_INVALID_ERR) return "cancelled-or-invalid"; if (errcode == IPMI_OUT_OF_RANGE_ERR) return "out-of-range"; if (errcode == IPMI_CANNOT_RETURN_DATA_ERR) return "cannot-return-data"; if (errcode == IPMI_NOT_FOUND_ERR) return "not-found"; if (errcode == IPMI_INVALID_DATA_FIELD_ERR) return "invalid-data-field"; if (errcode == IPMI_COMMAND_ILLEGAL_ERR) return "command-illegal"; if (errcode == IPMI_RESPONSE_NOT_PROVIDED_ERR) return "response-not-provided"; if (errcode == IPMI_DUPLICATED_REQUEST_ERR) return "duplicated-request"; if (errcode == IPMI_SDR_IN_UPDATE_MODE_ERR) return "sdr-in-update-mode"; if (errcode == IPMI_DESTINATION_UNAVAILABLE_ERR) return "destination-unavailable"; if (errcode == IPMI_INSUFFICIENT_PRIVILEGE_ERR) return "insufficient-privilege"; if (errcode == IPMI_COMMAND_DISABLED_ERR) return "command-disabled"; return "unknown"; } static gboolean fu_ipmi_device_errcode_to_error(guint8 errcode, GError **error) { /* success */ if (errcode == IPMI_CC_NO_ERROR) return TRUE; /* data not found, seemingly Lenovo specific */ if (errcode == IPMI_INVALID_DATA_FIELD_ERR || errcode == IPMI_NOT_FOUND_ERR) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "CC error: %s [0x%02X]", fu_ipmi_device_errcode_to_string(errcode), errcode); return FALSE; } /* fallback */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "CC error: %s [0x%02X]", fu_ipmi_device_errcode_to_string(errcode), errcode); return FALSE; } typedef struct { guint8 netfn; guint8 cmd; const guint8 *req_buf; gsize req_bufsz; guint8 *resp_buf; gsize resp_bufsz; gsize *resp_len; gint timeout_ms; } FuIpmiDeviceTransactionHelper; static gboolean fu_ipmi_device_transaction_cb(FuDevice *device, gpointer user_data, GError **error) { FuIpmiDevice *self = FU_IPMI_DEVICE(device); FuIOChannel *io_channel = fu_udev_device_get_io_channel(FU_UDEV_DEVICE(self)); FuIpmiDeviceTransactionHelper *helper = (FuIpmiDeviceTransactionHelper *)user_data; GPollFD pollfds[1]; gsize resp_buf2sz = helper->resp_bufsz + 1; gsize resp_len2 = 0; g_autoptr(GTimer) timer = g_timer_new(); g_autoptr(FuDeviceLocker) lock = NULL; g_autofree guint8 *resp_buf2 = g_malloc0(resp_buf2sz); lock = fu_device_locker_new_full(self, fu_ipmi_device_lock, fu_ipmi_device_unlock, error); if (lock == NULL) return FALSE; if (!fu_ipmi_device_send(self, helper->netfn, helper->cmd, helper->req_buf, helper->req_bufsz, error)) return FALSE; pollfds[0].fd = fu_io_channel_unix_get_fd(io_channel); pollfds[0].events = POLLIN; for (;;) { guint8 resp_netfn = 0; guint8 resp_cmd = 0; glong seq = 0; gint rc; rc = g_poll(pollfds, 1, helper->timeout_ms - (g_timer_elapsed(timer, NULL) * 1000.f)); if (rc < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "poll() error %m"); return FALSE; } if (rc == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "timeout waiting for response " "(netfn %d, cmd %d)", helper->netfn, helper->cmd); return FALSE; } if (!(pollfds[0].revents & POLLIN)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unexpected status"); return FALSE; } if (!fu_ipmi_device_recv(self, &resp_netfn, &resp_cmd, &seq, resp_buf2, resp_buf2sz, &resp_len2, error)) return FALSE; if (seq != self->seq - 1) { g_debug("out-of-sequence reply: " "expected %ld, got %ld", self->seq, seq); if (g_timer_elapsed(timer, NULL) * 1000.f >= helper->timeout_ms) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "timed out"); return FALSE; } } else { if (!fu_ipmi_device_errcode_to_error(resp_buf2[0], error)) return FALSE; if (helper->resp_buf != NULL) { if (!fu_memcpy_safe(helper->resp_buf, helper->resp_bufsz, 0x0, /* dst */ resp_buf2, resp_buf2sz, 0x01, /* src */ helper->resp_bufsz, error)) return FALSE; } if (helper->resp_len != NULL) *helper->resp_len = resp_len2 - 1; g_debug("IPMI netfn: %02x->%02x, cmd: %02x->%02x", helper->netfn, resp_netfn, helper->cmd, resp_cmd); break; } } return TRUE; } static gboolean fu_ipmi_device_transaction(FuIpmiDevice *self, guint8 netfn, guint8 cmd, const guint8 *req_buf, gsize req_bufsz, guint8 *resp_buf, /* optional */ gsize resp_bufsz, gsize *resp_len, /* optional, out */ gint timeout_ms, GError **error) { FuIpmiDeviceTransactionHelper helper = { .netfn = netfn, .cmd = cmd, .req_buf = req_buf, .req_bufsz = req_bufsz, .resp_buf = resp_buf, .resp_bufsz = resp_bufsz, .resp_len = resp_len, .timeout_ms = timeout_ms, }; fu_device_retry_add_recovery(FU_DEVICE(self), FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, NULL); return fu_device_retry_full(FU_DEVICE(self), fu_ipmi_device_transaction_cb, FU_IPMI_TRANSACTION_RETRY_COUNT, FU_IPMI_TRANSACTION_RETRY_DELAY, &helper, error); } static gboolean fu_ipmi_device_probe(FuDevice *device, GError **error) { FuIpmiDevice *self = FU_IPMI_DEVICE(device); const gchar *physical_ids[] = {"/dev/ipmi0", "/dev/ipmi/0", "/dev/ipmidev/0", NULL}; /* look for the IPMI device */ for (guint i = 0; physical_ids[i] != NULL; i++) { gboolean exists_fn = FALSE; if (!fu_device_query_file_exists(device, physical_ids[i], &exists_fn, error)) return FALSE; if (exists_fn) { fu_device_set_physical_id(FU_DEVICE(self), physical_ids[i]); return TRUE; } } /* cannot continue */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no BMC device found"); return FALSE; } static gboolean fu_ipmi_device_setup(FuDevice *device, GError **error) { FuIpmiDevice *self = FU_IPMI_DEVICE(device); gsize resp_len = 0; guint8 resp[16] = {0}; /* get IPMI versions */ if (!fu_ipmi_device_transaction(self, IPMI_NETFN_APP_REQUEST, IPMI_GET_DEVICE_ID_CMD, NULL, 0, resp, sizeof(resp), &resp_len, FU_IPMI_DEVICE_TIMEOUT, error)) return FALSE; if (resp_len == 11 || resp_len == 15) { guint8 bcd; g_autoptr(GString) str = g_string_new(NULL); self->device_id = resp[0]; self->device_rev = resp[1]; bcd = resp[3] & 0x0f; bcd += 10 * (resp[4] >> 3); /* rev1.rev2.aux_revision */ g_string_append_printf(str, "%u.%02u", resp[2], bcd); if (resp_len == 15) { g_string_append_printf(str, ".%02x%02x%02x%02x", resp[11], resp[12], resp[13], resp[14]); } fu_device_set_version(device, str->str); bcd = resp[4] & 0x0f; bcd += 10 * (resp[4] >> 4); self->version_ipmi = bcd; } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to parse DEVICE_ID_CMD response (sz: %" G_GSIZE_FORMAT ")", resp_len); return FALSE; } /* success */ return TRUE; } gchar * fu_ipmi_device_get_user_password(FuIpmiDevice *self, guint8 user_id, GError **error) { const guint8 req[1] = {user_id}; guint8 resp[0x10] = {0}; gsize resp_len = 0; g_return_val_if_fail(FU_IS_IPMI_DEVICE(self), NULL); g_return_val_if_fail(user_id != 0x0, NULL); /* run transaction */ if (!fu_ipmi_device_transaction(self, IPMI_NETFN_APP_REQUEST, IPMI_GET_USER_NAME, req, sizeof(req), resp, sizeof(resp), &resp_len, FU_IPMI_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "failed to get username: "); return NULL; } if (resp_len != sizeof(resp)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to retrieve username from IPMI, got 0x%x bytes", (guint)resp_len); return NULL; } /* success */ return fu_memstrsafe(resp, sizeof(resp), 0x0, resp_len, error); } gboolean fu_ipmi_device_set_user_name(FuIpmiDevice *self, guint8 user_id, const gchar *username, GError **error) { guint8 req[0x11] = {user_id}; gsize username_sz; g_return_val_if_fail(FU_IS_IPMI_DEVICE(self), FALSE); g_return_val_if_fail(user_id != 0x0, FALSE); g_return_val_if_fail(username != NULL, FALSE); /* copy into buffer */ username_sz = strlen(username); if (!fu_memcpy_safe(req, sizeof(req), 0x1, /* dst */ (guint8 *)username, username_sz, 0x0, /* src */ username_sz, error)) { g_prefix_error(error, "username invalid: "); return FALSE; } /* run transaction */ if (!fu_ipmi_device_transaction(self, IPMI_NETFN_APP_REQUEST, IPMI_SET_USER_NAME, req, sizeof(req), NULL, /* resp */ 0, NULL, FU_IPMI_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "failed to set user %02x name: ", user_id); return FALSE; } /* success */ return TRUE; } gboolean fu_ipmi_device_set_user_enable(FuIpmiDevice *self, guint8 user_id, gboolean value, GError **error) { guint8 op = value ? IPMI_PASSWORD_ENABLE_USER : IPMI_PASSWORD_DISABLE_USER; const guint8 req[] = {user_id, op}; g_return_val_if_fail(FU_IS_IPMI_DEVICE(self), FALSE); g_return_val_if_fail(user_id != 0x0, FALSE); /* run transaction */ if (!fu_ipmi_device_transaction(self, IPMI_NETFN_APP_REQUEST, IPMI_SET_USER_PASSWORD, req, sizeof(req), NULL, /* resp */ 0, NULL, FU_IPMI_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "failed to set user %02x enable: ", user_id); return FALSE; } /* success */ return TRUE; } gboolean fu_ipmi_device_set_user_password(FuIpmiDevice *self, guint8 user_id, const gchar *password, GError **error) { guint8 req[0x12] = {user_id, IPMI_PASSWORD_SET_PASSWORD}; gsize password_sz; g_return_val_if_fail(FU_IS_IPMI_DEVICE(self), FALSE); g_return_val_if_fail(user_id != 0x0, FALSE); g_return_val_if_fail(password != NULL, FALSE); /* copy into buffer */ password_sz = strlen(password); if (!fu_memcpy_safe(req, sizeof(req), 0x2, /* dst */ (guint8 *)password, password_sz, 0x0, /* src */ password_sz, error)) { g_prefix_error(error, "password invalid: "); return FALSE; } /* run transaction */ if (!fu_ipmi_device_transaction(self, IPMI_NETFN_APP_REQUEST, IPMI_SET_USER_PASSWORD, req, sizeof(req), NULL, /* resp */ 0, NULL, FU_IPMI_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "failed to set user %02x password: ", user_id); return FALSE; } /* success */ return TRUE; } gboolean fu_ipmi_device_set_user_priv(FuIpmiDevice *self, guint8 user_id, guint8 priv_limit, guint8 channel, GError **error) { const guint8 req[] = {channel, user_id, priv_limit, 0x0}; g_return_val_if_fail(FU_IS_IPMI_DEVICE(self), FALSE); g_return_val_if_fail(user_id != 0x0, FALSE); g_return_val_if_fail(channel <= 0x0F, FALSE); g_return_val_if_fail(priv_limit <= 0x0F, FALSE); /* run transaction */ if (!fu_ipmi_device_transaction(self, IPMI_NETFN_APP_REQUEST, IPMI_SET_USER_ACCESS, req, sizeof(req), NULL, /* resp */ 0, NULL, FU_IPMI_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "failed to set user %02x privs of 0x%02x, 0x%02x: ", user_id, priv_limit, channel); return FALSE; } /* success */ return TRUE; } gboolean fu_ipmi_device_set_user_group_redfish_enable_advantech(FuIpmiDevice *self, guint8 user_id, GError **error) { const guint8 req[] = {0x39, 0x28, 0x0, user_id, 0x3, 0x1}; guint8 resp[0x3] = {0}; gsize resp_len = 0; g_return_val_if_fail(FU_IS_IPMI_DEVICE(self), FALSE); g_return_val_if_fail(user_id != 0x0, FALSE); /* run transaction */ if (!fu_ipmi_device_transaction(self, 0x2e, 0x08, req, sizeof(req), resp, sizeof(resp), &resp_len, FU_IPMI_DEVICE_TIMEOUT, error)) { g_prefix_error(error, "failed to set user %02x redfish group enable: ", user_id); return FALSE; } /* success */ return TRUE; } static void fu_ipmi_device_init(FuIpmiDevice *self) { fu_device_set_name(FU_DEVICE(self), "IPMI"); fu_device_set_summary(FU_DEVICE(self), "Intelligent Platform Management Interface"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_COMPUTER); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); } static void fu_ipmi_device_class_init(FuIpmiDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_ipmi_device_probe; device_class->setup = fu_ipmi_device_setup; device_class->to_string = fu_ipmi_device_to_string; } FuIpmiDevice * fu_ipmi_device_new(FuContext *ctx) { FuIpmiDevice *self; self = g_object_new(FU_TYPE_IPMI_DEVICE, "context", ctx, "device-file", "/dev/ipmi0", NULL); return self; } fwupd-2.0.10/plugins/redfish/fu-ipmi-device.h000066400000000000000000000021351501337203100207740ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_IPMI_DEVICE (fu_ipmi_device_get_type()) G_DECLARE_FINAL_TYPE(FuIpmiDevice, fu_ipmi_device, FU, IPMI_DEVICE, FuUdevDevice) FuIpmiDevice * fu_ipmi_device_new(FuContext *ctx); gchar * fu_ipmi_device_get_user_password(FuIpmiDevice *self, guint8 user_id, GError **error); gboolean fu_ipmi_device_set_user_name(FuIpmiDevice *self, guint8 user_id, const gchar *username, GError **error); gboolean fu_ipmi_device_set_user_password(FuIpmiDevice *self, guint8 user_id, const gchar *password, GError **error); gboolean fu_ipmi_device_set_user_enable(FuIpmiDevice *self, guint8 user_id, gboolean value, GError **error); gboolean fu_ipmi_device_set_user_priv(FuIpmiDevice *self, guint8 user_id, guint8 priv_limit, guint8 channel, GError **error); gboolean fu_ipmi_device_set_user_group_redfish_enable_advantech(FuIpmiDevice *self, guint8 user_id, GError **error); fwupd-2.0.10/plugins/redfish/fu-redfish-backend.c000066400000000000000000000536031501337203100216130ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-redfish-backend.h" #include "fu-redfish-common.h" #include "fu-redfish-hpe-device.h" #include "fu-redfish-legacy-device.h" #include "fu-redfish-multipart-device.h" #include "fu-redfish-request.h" #include "fu-redfish-smbios.h" #include "fu-redfish-smc-device.h" struct _FuRedfishBackend { FuBackend parent_instance; gchar *hostname; gchar *username; gchar *password; gchar *session_key; guint port; gchar *vendor; gchar *version; gchar *uuid; gchar *update_uri_path; gchar *push_uri_path; gboolean use_https; gboolean cacheck; gboolean wildcard_targets; gint64 max_image_size; /* bytes */ gchar *system_id; GType device_gtype; GHashTable *request_cache; /* str:GByteArray */ CURLSH *curlsh; }; G_DEFINE_TYPE(FuRedfishBackend, fu_redfish_backend, FU_TYPE_BACKEND) const gchar * fu_redfish_backend_get_vendor(FuRedfishBackend *self) { return self->vendor; } const gchar * fu_redfish_backend_get_version(FuRedfishBackend *self) { return self->version; } const gchar * fu_redfish_backend_get_uuid(FuRedfishBackend *self) { return self->uuid; } FuRedfishRequest * fu_redfish_backend_request_new(FuRedfishBackend *self) { FuRedfishRequest *request = g_object_new(FU_TYPE_REDFISH_REQUEST, NULL); CURL *curl; CURLU *uri; g_autofree gchar *user_agent = NULL; g_autofree gchar *port = g_strdup_printf("%u", self->port); /* set the cache location */ fu_redfish_request_set_cache(request, self->request_cache); fu_redfish_request_set_curlsh(request, self->curlsh); /* set up defaults */ curl = fu_redfish_request_get_curl(request); uri = fu_redfish_request_get_uri(request); (void)curl_url_set(uri, CURLUPART_SCHEME, self->use_https ? "https" : "http", 0); (void)curl_url_set(uri, CURLUPART_HOST, self->hostname, 0); (void)curl_url_set(uri, CURLUPART_PORT, port, 0); (void)curl_easy_setopt(curl, CURLOPT_CURLU, uri); /* since DSP0266 makes Basic Authorization a requirement, * it is safe to use Basic Auth for all implementations */ (void)curl_easy_setopt(curl, CURLOPT_HTTPAUTH, (glong)CURLAUTH_BASIC); (void)curl_easy_setopt(curl, CURLOPT_TIMEOUT, (glong)180); (void)curl_easy_setopt(curl, CURLOPT_USERNAME, self->username); (void)curl_easy_setopt(curl, CURLOPT_PASSWORD, self->password); /* setup networking */ user_agent = g_strdup_printf("%s/%s", PACKAGE_NAME, PACKAGE_VERSION); (void)curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent); (void)curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 60L); if (!self->cacheck) { (void)curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); (void)curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); } /* success */ return request; } static gboolean fu_redfish_backend_coldplug_member(FuRedfishBackend *self, JsonObject *member, GError **error) { g_autoptr(FuDevice) dev = NULL; g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GError) error_local = NULL; /* create of the correct type */ dev = g_object_new(self->device_gtype, "context", fu_backend_get_context(FU_BACKEND(self)), "backend", self, "member", member, NULL); /* Dell specific currently */ if (self->system_id != NULL) fu_device_add_instance_str(dev, "SYSTEMID", self->system_id); /* some vendors do not specify the Targets array when updating */ if (self->wildcard_targets) fu_device_add_private_flag(dev, FU_REDFISH_DEVICE_FLAG_WILDCARD_TARGETS); /* probe + setup */ locker = fu_device_locker_new(dev, &error_local); if (locker == NULL) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_debug("failed to setup: %s", error_local->message); return TRUE; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } if (self->max_image_size != 0) fu_device_set_firmware_size_max(dev, (guint64)self->max_image_size); fu_backend_device_added(FU_BACKEND(self), dev); return TRUE; } static gboolean fu_redfish_backend_coldplug_collection(FuRedfishBackend *self, JsonObject *collection, GError **error) { JsonArray *members = json_object_get_array_member(collection, "Members"); for (guint i = 0; i < json_array_get_length(members); i++) { JsonObject *json_obj; JsonObject *member_id; const gchar *member_uri; g_autoptr(FuRedfishRequest) request = fu_redfish_backend_request_new(self); member_id = json_array_get_object_element(members, i); member_uri = json_object_get_string_member(member_id, "@odata.id"); if (member_uri == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no @odata.id string"); return FALSE; } /* create the device for the member */ if (!fu_redfish_request_perform(request, member_uri, FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON, error)) return FALSE; json_obj = fu_redfish_request_get_json_object(request); if (!fu_redfish_backend_coldplug_member(self, json_obj, error)) return FALSE; } return TRUE; } static gboolean fu_redfish_backend_coldplug_inventory(FuRedfishBackend *self, JsonObject *inventory, GError **error) { JsonObject *json_obj; const gchar *collection_uri; g_autoptr(FuRedfishRequest) request = fu_redfish_backend_request_new(self); if (inventory == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no inventory object"); return FALSE; } collection_uri = json_object_get_string_member(inventory, "@odata.id"); if (collection_uri == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no @odata.id string"); return FALSE; } if (!fu_redfish_request_perform(request, collection_uri, FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON, error)) return FALSE; json_obj = fu_redfish_request_get_json_object(request); return fu_redfish_backend_coldplug_collection(self, json_obj, error); } static void fu_redfish_backend_check_wildcard_targets(FuRedfishBackend *self) { g_autoptr(GPtrArray) devices = fu_backend_get_devices(FU_BACKEND(self)); g_autoptr(GHashTable) device_by_id0 = g_hash_table_new(g_str_hash, g_str_equal); /* does the SoftwareId exist from a different device */ for (guint i = 0; i < devices->len; i++) { FuDevice *device_old; FuDevice *device_tmp = g_ptr_array_index(devices, i); GPtrArray *ids = fu_device_get_instance_ids(device_tmp); const gchar *id0 = g_ptr_array_index(ids, 0); device_old = g_hash_table_lookup(device_by_id0, id0); if (device_old == NULL) { g_hash_table_insert(device_by_id0, (gpointer)device_tmp, (gpointer)id0); continue; } fu_device_add_flag(device_tmp, FWUPD_DEVICE_FLAG_WILDCARD_INSTALL); fu_device_add_flag(device_old, FWUPD_DEVICE_FLAG_WILDCARD_INSTALL); } } static void fu_redfish_backend_set_session_key(FuRedfishBackend *self, const gchar *session_key) { g_free(self->session_key); self->session_key = g_strdup(session_key); } static size_t fu_redfish_backend_session_headers_callback(char *ptr, size_t size, size_t nmemb, void *userdata) { FuRedfishBackend *self = FU_REDFISH_BACKEND(userdata); if ((size * nmemb) > 16 && g_ascii_strncasecmp(ptr, "X-Auth-Token:", 13) == 0) { g_autofree gchar *session_key = NULL; /* The string also includes \r\n at the end */ session_key = g_strndup(ptr + 14, (size * nmemb) - 16); fu_redfish_backend_set_session_key(self, session_key); } return size * nmemb; } gboolean fu_redfish_backend_create_session(FuRedfishBackend *self, GError **error) { g_autoptr(FuRedfishRequest) request = fu_redfish_backend_request_new(self); g_autoptr(JsonBuilder) builder = json_builder_new(); json_builder_begin_object(builder); json_builder_set_member_name(builder, "UserName"); json_builder_add_string_value(builder, self->username); json_builder_set_member_name(builder, "Password"); json_builder_add_string_value(builder, self->password); json_builder_end_object(builder); (void)curl_easy_setopt(fu_redfish_request_get_curl(request), CURLOPT_HEADERDATA, self); (void)curl_easy_setopt(fu_redfish_request_get_curl(request), CURLOPT_HEADERFUNCTION, fu_redfish_backend_session_headers_callback); /* create URI and poll */ if (!fu_redfish_request_perform_full(request, "/redfish/v1/SessionService/Sessions", "POST", builder, FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON, error)) return FALSE; if (fu_redfish_backend_get_session_key(self) == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to get session key"); return FALSE; } /* success */ return TRUE; } static void fu_redfish_backend_set_push_uri_path(FuRedfishBackend *self, const gchar *push_uri_path) { g_free(self->push_uri_path); self->push_uri_path = g_strdup(push_uri_path); } static gboolean fu_redfish_backend_has_smc_update_path(JsonObject *update_svc) { JsonObject *tmp_obj; const gchar *tmp_str; if (!json_object_has_member(update_svc, "Actions")) return FALSE; tmp_obj = json_object_get_object_member(update_svc, "Actions"); if (tmp_obj == NULL || !json_object_has_member(tmp_obj, "#UpdateService.StartUpdate")) return FALSE; tmp_obj = json_object_get_object_member(tmp_obj, "#UpdateService.StartUpdate"); if (tmp_obj == NULL || !json_object_has_member(tmp_obj, "target")) return FALSE; tmp_str = json_object_get_string_member(tmp_obj, "target"); return g_str_equal(tmp_str, "/redfish/v1/UpdateService/Actions/UpdateService.StartUpdate"); } static gboolean fu_redfish_backend_coldplug(FuBackend *backend, FuProgress *progress, GError **error) { FuRedfishBackend *self = FU_REDFISH_BACKEND(backend); JsonObject *json_obj; g_autoptr(FuRedfishRequest) request = fu_redfish_backend_request_new(self); /* nothing set */ if (self->update_uri_path == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "no update_uri_path"); return FALSE; } /* get the update service */ if (!fu_redfish_request_perform(request, self->update_uri_path, FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON, error)) return FALSE; json_obj = fu_redfish_request_get_json_object(request); if (!json_object_has_member(json_obj, "ServiceEnabled")) { if (!json_object_get_boolean_member(json_obj, "ServiceEnabled")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "service is not enabled"); return FALSE; } } if (self->push_uri_path == NULL && json_object_has_member(json_obj, "MultipartHttpPushUri")) { const gchar *tmp = json_object_get_string_member(json_obj, "MultipartHttpPushUri"); if (tmp != NULL) { if (g_strcmp0(self->vendor, "SMCI") == 0 && fu_redfish_backend_has_smc_update_path(json_obj)) { self->device_gtype = FU_TYPE_REDFISH_SMC_DEVICE; } else { self->device_gtype = FU_TYPE_REDFISH_MULTIPART_DEVICE; } fu_redfish_backend_set_push_uri_path(self, tmp); } } if (self->push_uri_path == NULL && json_object_has_member(json_obj, "HttpPushUri")) { const gchar *tmp = json_object_get_string_member(json_obj, "HttpPushUri"); if (tmp != NULL) { if (self->vendor != NULL && g_str_equal(self->vendor, "HPE")) self->device_gtype = FU_TYPE_REDFISH_HPE_DEVICE; else self->device_gtype = FU_TYPE_REDFISH_LEGACY_DEVICE; fu_redfish_backend_set_push_uri_path(self, tmp); } } if (self->push_uri_path == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "HttpPushUri and MultipartHttpPushUri are invalid"); return FALSE; } if (json_object_has_member(json_obj, "MaxImageSizeBytes")) { self->max_image_size = json_object_get_int_member(json_obj, "MaxImageSizeBytes"); } if (json_object_has_member(json_obj, "FirmwareInventory")) { JsonObject *tmp = json_object_get_object_member(json_obj, "FirmwareInventory"); return fu_redfish_backend_coldplug_inventory(self, tmp, error); } if (json_object_has_member(json_obj, "SoftwareInventory")) { JsonObject *tmp = json_object_get_object_member(json_obj, "SoftwareInventory"); return fu_redfish_backend_coldplug_inventory(self, tmp, error); } /* work out if we have multiple devices with the same SoftwareId */ if (self->wildcard_targets) fu_redfish_backend_check_wildcard_targets(self); /* success */ return TRUE; } static void fu_redfish_backend_set_update_uri_path(FuRedfishBackend *self, const gchar *update_uri_path) { /* not changed */ if (g_strcmp0(self->update_uri_path, update_uri_path) == 0) return; g_free(self->update_uri_path); self->update_uri_path = g_strdup(update_uri_path); } static gboolean fu_redfish_backend_setup_dell_member(FuRedfishBackend *self, const gchar *member_uri, GError **error) { JsonObject *dell_obj; JsonObject *dell_system_obj; JsonObject *json_obj; JsonObject *oem_obj; g_autoptr(FuRedfishRequest) request = fu_redfish_backend_request_new(self); if (!fu_redfish_request_perform(request, member_uri, FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON, error)) return FALSE; json_obj = fu_redfish_request_get_json_object(request); if (!json_object_has_member(json_obj, "Oem")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no Oem in Member"); return FALSE; } oem_obj = json_object_get_object_member(json_obj, "Oem"); if (oem_obj == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no valid Oem in Member"); return FALSE; } if (!json_object_has_member(oem_obj, "Dell")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no OEM/Dell in Member"); return FALSE; } dell_obj = json_object_get_object_member(oem_obj, "Dell"); if (dell_obj == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no valid OEM/Dell in Member"); return FALSE; } if (!json_object_has_member(dell_obj, "DellSystem")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no OEM/Dell/DellSystem in Member"); return FALSE; } dell_system_obj = json_object_get_object_member(dell_obj, "DellSystem"); if (dell_system_obj == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no valid OEM/Dell/DellSystem in Member"); return FALSE; } if (!json_object_has_member(dell_system_obj, "SystemID")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no OEM/Dell/DellSystem/SystemID in Member"); return FALSE; } /* success */ self->system_id = g_strdup_printf("%04X", (guint16)json_object_get_int_member(dell_system_obj, "SystemID")); return TRUE; } static gboolean fu_redfish_backend_setup_dell(FuRedfishBackend *self, GError **error) { JsonObject *member; JsonArray *members; JsonObject *json_obj; const gchar *member_uri; g_autoptr(FuRedfishRequest) request = fu_redfish_backend_request_new(self); if (!fu_redfish_request_perform(request, "/redfish/v1/Systems", FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON, error)) return FALSE; json_obj = fu_redfish_request_get_json_object(request); if (!json_object_has_member(json_obj, "Members")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no Members object"); return FALSE; } members = json_object_get_array_member(json_obj, "Members"); if (json_array_get_length(members) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "empty Members array"); return FALSE; } member = json_array_get_object_element(members, 0); member_uri = json_object_get_string_member(member, "@odata.id"); return fu_redfish_backend_setup_dell_member(self, member_uri, error); } static gboolean fu_redfish_backend_setup(FuBackend *backend, FuBackendSetupFlags flags, FuProgress *progress, GError **error) { FuRedfishBackend *self = FU_REDFISH_BACKEND(backend); JsonObject *json_obj; JsonObject *json_update_service = NULL; const gchar *data_id; const gchar *version = NULL; g_autoptr(FuRedfishRequest) request = fu_redfish_backend_request_new(self); /* sanity check */ if (self->port == 0 || self->port > G_MAXUINT16) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid port specified: 0x%x", self->port); return FALSE; } /* try to connect */ if (!fu_redfish_request_perform(request, "/redfish/v1/", FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON, error)) return FALSE; json_obj = fu_redfish_request_get_json_object(request); if (json_object_has_member(json_obj, "ServiceVersion")) { version = json_object_get_string_member(json_obj, "ServiceVersion"); } else if (json_object_has_member(json_obj, "RedfishVersion")) { version = json_object_get_string_member(json_obj, "RedfishVersion"); } if (version != NULL) { g_free(self->version); self->version = g_strdup(version); } if (json_object_has_member(json_obj, "UUID")) { g_free(self->uuid); self->uuid = g_strdup(json_object_get_string_member(json_obj, "UUID")); } if (json_object_has_member(json_obj, "Vendor")) { g_free(self->vendor); self->vendor = g_strdup(json_object_get_string_member(json_obj, "Vendor")); } if (g_strcmp0(self->vendor, "Dell") == 0) { if (!fu_redfish_backend_setup_dell(self, error)) return FALSE; } if (json_object_has_member(json_obj, "UpdateService")) json_update_service = json_object_get_object_member(json_obj, "UpdateService"); if (json_update_service == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no UpdateService object"); return FALSE; } data_id = json_object_get_string_member(json_update_service, "@odata.id"); if (data_id == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no @odata.id string"); return FALSE; } fu_redfish_backend_set_update_uri_path(self, data_id); return TRUE; } static void fu_redfish_backend_invalidate(FuBackend *backend) { FuRedfishBackend *self = FU_REDFISH_BACKEND(backend); g_hash_table_remove_all(self->request_cache); } void fu_redfish_backend_set_hostname(FuRedfishBackend *self, const gchar *hostname) { g_free(self->hostname); self->hostname = g_strdup(hostname); } void fu_redfish_backend_set_port(FuRedfishBackend *self, guint port) { self->port = port; } void fu_redfish_backend_set_https(FuRedfishBackend *self, gboolean use_https) { self->use_https = use_https; } void fu_redfish_backend_set_cacheck(FuRedfishBackend *self, gboolean cacheck) { self->cacheck = cacheck; } void fu_redfish_backend_set_wildcard_targets(FuRedfishBackend *self, gboolean wildcard_targets) { self->wildcard_targets = wildcard_targets; } void fu_redfish_backend_set_username(FuRedfishBackend *self, const gchar *username) { g_free(self->username); self->username = g_strdup(username); } const gchar * fu_redfish_backend_get_username(FuRedfishBackend *self) { return self->username; } void fu_redfish_backend_set_password(FuRedfishBackend *self, const gchar *password) { g_free(self->password); self->password = g_strdup(password); } const gchar * fu_redfish_backend_get_push_uri_path(FuRedfishBackend *self) { return self->push_uri_path; } const gchar * fu_redfish_backend_get_session_key(FuRedfishBackend *self) { return self->session_key; } static void fu_redfish_backend_to_string(FuBackend *backend, guint idt, GString *str) { FuRedfishBackend *self = FU_REDFISH_BACKEND(backend); fwupd_codec_string_append(str, idt, "Hostname", self->hostname); fwupd_codec_string_append(str, idt, "Username", self->username); fwupd_codec_string_append_bool(str, idt, "Password", self->password != NULL); fwupd_codec_string_append(str, idt, "SessionKey", self->session_key); fwupd_codec_string_append_int(str, idt, "Port", self->port); fwupd_codec_string_append(str, idt, "UpdateUriPath", self->update_uri_path); fwupd_codec_string_append(str, idt, "PushUriPath", self->push_uri_path); fwupd_codec_string_append_bool(str, idt, "UseHttps", self->use_https); fwupd_codec_string_append_bool(str, idt, "Cacheck", self->cacheck); fwupd_codec_string_append_bool(str, idt, "WildcardTargets", self->wildcard_targets); fwupd_codec_string_append_hex(str, idt, "MaxImageSize", self->max_image_size); fwupd_codec_string_append(str, idt, "SystemId", self->system_id); fwupd_codec_string_append(str, idt, "DeviceGType", g_type_name(self->device_gtype)); } static void fu_redfish_backend_finalize(GObject *object) { FuRedfishBackend *self = FU_REDFISH_BACKEND(object); g_hash_table_unref(self->request_cache); curl_share_cleanup(self->curlsh); g_free(self->update_uri_path); g_free(self->push_uri_path); g_free(self->hostname); g_free(self->username); g_free(self->password); g_free(self->session_key); g_free(self->vendor); g_free(self->version); g_free(self->uuid); g_free(self->system_id); G_OBJECT_CLASS(fu_redfish_backend_parent_class)->finalize(object); } static void fu_redfish_backend_class_init(FuRedfishBackendClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuBackendClass *backend_class = FU_BACKEND_CLASS(klass); backend_class->coldplug = fu_redfish_backend_coldplug; backend_class->setup = fu_redfish_backend_setup; backend_class->invalidate = fu_redfish_backend_invalidate; backend_class->to_string = fu_redfish_backend_to_string; object_class->finalize = fu_redfish_backend_finalize; } static void fu_redfish_backend_init(FuRedfishBackend *self) { self->use_https = TRUE; self->device_gtype = FU_TYPE_REDFISH_DEVICE; self->request_cache = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_byte_array_unref); self->curlsh = curl_share_init(); curl_share_setopt(self->curlsh, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE); curl_share_setopt(self->curlsh, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS); curl_share_setopt(self->curlsh, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION); } FuRedfishBackend * fu_redfish_backend_new(FuContext *ctx) { return FU_REDFISH_BACKEND(g_object_new(FU_REDFISH_TYPE_BACKEND, "name", "redfish", "can-invalidate", TRUE, "context", ctx, NULL)); } fwupd-2.0.10/plugins/redfish/fu-redfish-backend.h000066400000000000000000000030371501337203100216140ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-redfish-request.h" #define FU_REDFISH_TYPE_BACKEND (fu_redfish_backend_get_type()) G_DECLARE_FINAL_TYPE(FuRedfishBackend, fu_redfish_backend, FU, REDFISH_BACKEND, FuBackend) FuRedfishBackend * fu_redfish_backend_new(FuContext *ctx); const gchar * fu_redfish_backend_get_vendor(FuRedfishBackend *self); const gchar * fu_redfish_backend_get_version(FuRedfishBackend *self); const gchar * fu_redfish_backend_get_uuid(FuRedfishBackend *self); void fu_redfish_backend_set_hostname(FuRedfishBackend *self, const gchar *hostname); void fu_redfish_backend_set_username(FuRedfishBackend *self, const gchar *username); const gchar * fu_redfish_backend_get_username(FuRedfishBackend *self); void fu_redfish_backend_set_password(FuRedfishBackend *self, const gchar *password); void fu_redfish_backend_set_port(FuRedfishBackend *self, guint port); void fu_redfish_backend_set_https(FuRedfishBackend *self, gboolean use_https); void fu_redfish_backend_set_cacheck(FuRedfishBackend *self, gboolean cacheck); void fu_redfish_backend_set_wildcard_targets(FuRedfishBackend *self, gboolean wildcard_targets); const gchar * fu_redfish_backend_get_push_uri_path(FuRedfishBackend *self); const gchar * fu_redfish_backend_get_session_key(FuRedfishBackend *self); gboolean fu_redfish_backend_create_session(FuRedfishBackend *self, GError **error); FuRedfishRequest * fu_redfish_backend_request_new(FuRedfishBackend *self); fwupd-2.0.10/plugins/redfish/fu-redfish-common.c000066400000000000000000000060231501337203100215060ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-redfish-common.h" gchar * fu_redfish_common_buffer_to_ipv4(const guint8 *buffer) { GString *str = g_string_new(NULL); for (guint i = 0; i < 4; i++) { g_string_append_printf(str, "%u", buffer[i]); if (i != 3) g_string_append(str, "."); } return g_string_free(str, FALSE); } gchar * fu_redfish_common_buffer_to_ipv6(const guint8 *buffer) { GString *str = g_string_new(NULL); for (guint i = 0; i < 16; i += 4) { g_string_append_printf(str, "%02x%02x%02x%02x", buffer[i + 0], buffer[i + 1], buffer[i + 2], buffer[i + 3]); if (i != 12) g_string_append(str, ":"); } return g_string_free(str, FALSE); } gchar * fu_redfish_common_buffer_to_mac(const guint8 *buffer) { GString *str = g_string_new(NULL); for (guint i = 0; i < 6; i++) { g_string_append_printf(str, "%02X", buffer[i]); if (i != 5) g_string_append(str, ":"); } return g_string_free(str, FALSE); } gchar * fu_redfish_common_fix_version(const gchar *version) { g_auto(GStrv) split = NULL; g_return_val_if_fail(version != NULL, NULL); /* not valid */ if (g_strcmp0(version, "-*") == 0) return NULL; /* find the section preficed with "v" */ split = g_strsplit(version, " ", -1); for (guint i = 0; split[i] != NULL; i++) { if (g_str_has_prefix(split[i], "v")) { g_debug("using %s for %s", split[i] + 1, version); return g_strdup(split[i] + 1); } } /* find the thing with dots */ for (guint i = 0; split[i] != NULL; i++) { if (g_strstr_len(split[i], -1, ".")) { if (g_strcmp0(split[i], version) != 0) g_debug("using %s for %s", split[i], version); return g_strdup(split[i]); } } /* we failed to do anything clever */ return g_strdup(version); } /* parses a Lenovo XCC-format version like "11A-1.02" */ gboolean fu_redfish_common_parse_version_lenovo(const gchar *version, gchar **out_build, /* out */ gchar **out_version, /* out */ GError **error) { g_auto(GStrv) versplit = g_strsplit(version, "-", -1); /* sanity check */ if (g_strv_length(versplit) != 2) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not two sections"); return FALSE; } if (strlen(versplit[0]) != 3) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid length first section"); return FALSE; } /* milestone */ if (!g_ascii_isdigit(versplit[0][0]) || !g_ascii_isdigit(versplit[0][1])) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "milestone number invalid"); return FALSE; } /* build is only one letter from A -> Z */ if (!g_ascii_isalpha(versplit[0][2])) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "build letter invalid"); return FALSE; } /* success */ if (out_build != NULL) *out_build = g_strdup(versplit[0]); if (out_version != NULL) *out_version = g_strdup(versplit[1]); return TRUE; } fwupd-2.0.10/plugins/redfish/fu-redfish-common.h000066400000000000000000000021101501337203100215040ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include /* SMBIOS */ #define REDFISH_SMBIOS_TABLE_TYPE 0x2a /* 42 */ #define REDFISH_PROTOCOL_REDFISH_OVER_IP 0x04 /* EFI */ #define REDFISH_EFI_INFORMATION_GUID "16faa37e-4b6a-4891-9028-242de65a3b70" #define REDFISH_EFI_INFORMATION_INDICATIONS "RedfishIndications" #define REDFISH_EFI_INFORMATION_FW_CREDENTIALS "RedfishFWCredentials" #define REDFISH_EFI_INFORMATION_OS_CREDENTIALS "RedfishOSCredentials" #define REDFISH_EFI_INDICATIONS_FW_CREDENTIALS 0x00000001 #define REDFISH_EFI_INDICATIONS_OS_CREDENTIALS 0x00000002 /* shared */ gchar * fu_redfish_common_buffer_to_ipv4(const guint8 *buffer); gchar * fu_redfish_common_buffer_to_ipv6(const guint8 *buffer); gchar * fu_redfish_common_buffer_to_mac(const guint8 *buffer); gchar * fu_redfish_common_fix_version(const gchar *version); gboolean fu_redfish_common_parse_version_lenovo(const gchar *version, gchar **out_build, gchar **out_version, GError **error); fwupd-2.0.10/plugins/redfish/fu-redfish-device.c000066400000000000000000001011161501337203100214540ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-redfish-backend.h" #include "fu-redfish-common.h" #include "fu-redfish-device.h" typedef struct { FuRedfishBackend *backend; JsonObject *member; guint64 milestone; gchar *build; guint reset_pre_delay; /* default of 0ms */ guint reset_post_delay; /* default of 0ms */ } FuRedfishDevicePrivate; enum { PROP_0, PROP_BACKEND, PROP_MEMBER, PROP_LAST }; G_DEFINE_TYPE_WITH_PRIVATE(FuRedfishDevice, fu_redfish_device, FU_TYPE_DEVICE) #define GET_PRIVATE(o) (fu_redfish_device_get_instance_private(o)) static void fu_redfish_device_to_string(FuDevice *device, guint idt, GString *str) { FuRedfishDevice *self = FU_REDFISH_DEVICE(device); FuRedfishDevicePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append_hex(str, idt, "Milestone", priv->milestone); fwupd_codec_string_append(str, idt, "Build", priv->build); fwupd_codec_string_append_int(str, idt, "ResetPretDelay", priv->reset_pre_delay); fwupd_codec_string_append_int(str, idt, "ResetPostDelay", priv->reset_post_delay); } static void fu_redfish_device_set_device_class(FuRedfishDevice *self, const gchar *tmp) { if (g_strcmp0(tmp, "NetworkController") == 0) { fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_NETWORK_WIRED); return; } if (g_strcmp0(tmp, "MassStorageController") == 0) { fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_DRIVE_MULTIDISK); return; } if (g_strcmp0(tmp, "DisplayController") == 0) { fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_VIDEO_DISPLAY); return; } if (g_strcmp0(tmp, "DockingStation") == 0) { fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_DOCK); return; } if (g_strcmp0(tmp, "WirelessController") == 0) { fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_NETWORK_WIRELESS); return; } g_debug("no icon mapping for %s", tmp); } static gboolean fu_redfish_device_probe_related_pcie_item(FuRedfishDevice *self, const gchar *uri, GError **error) { FuRedfishDevicePrivate *priv = GET_PRIVATE(self); JsonObject *json_obj; guint64 vendor_id = 0x0; guint64 model_id = 0x0; guint64 subsystem_vendor_id = 0x0; guint64 subsystem_model_id = 0x0; g_autoptr(FuRedfishRequest) request = fu_redfish_backend_request_new(priv->backend); /* get URI */ if (!fu_redfish_request_perform(request, uri, FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON | FU_REDFISH_REQUEST_PERFORM_FLAG_USE_CACHE, error)) return FALSE; json_obj = fu_redfish_request_get_json_object(request); /* optional properties */ if (json_object_has_member(json_obj, "DeviceClass")) { const gchar *tmp = json_object_get_string_member(json_obj, "DeviceClass"); if (tmp != NULL && tmp[0] != '\0') fu_redfish_device_set_device_class(self, tmp); } if (json_object_has_member(json_obj, "VendorId")) { const gchar *tmp = json_object_get_string_member(json_obj, "VendorId"); if (tmp != NULL && tmp[0] != '\0') { if (!fu_strtoull(tmp, &vendor_id, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; } } if (json_object_has_member(json_obj, "DeviceId")) { const gchar *tmp = json_object_get_string_member(json_obj, "DeviceId"); if (tmp != NULL && tmp[0] != '\0') { if (!fu_strtoull(tmp, &model_id, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; } } if (json_object_has_member(json_obj, "SubsystemVendorId")) { const gchar *tmp = json_object_get_string_member(json_obj, "SubsystemVendorId"); if (tmp != NULL && tmp[0] != '\0') { if (!fu_strtoull(tmp, &subsystem_vendor_id, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; } } if (json_object_has_member(json_obj, "SubsystemId")) { const gchar *tmp = json_object_get_string_member(json_obj, "SubsystemId"); if (tmp != NULL && tmp[0] != '\0') { if (!fu_strtoull(tmp, &subsystem_model_id, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; } } /* add vendor ID */ fu_device_build_vendor_id_u16(FU_DEVICE(self), "PCI", vendor_id); /* add more instance IDs if possible */ if (vendor_id != 0x0) fu_device_add_instance_u16(FU_DEVICE(self), "VEN", vendor_id); if (model_id != 0x0) fu_device_add_instance_u16(FU_DEVICE(self), "DEV", model_id); if (subsystem_vendor_id != 0x0 && subsystem_model_id != 0x0) { g_autofree gchar *subsys = NULL; subsys = g_strdup_printf("%04X%04X", (guint)subsystem_vendor_id, (guint)subsystem_model_id); fu_device_add_instance_str(FU_DEVICE(self), "SUBSYS", subsys); } fu_device_build_instance_id(FU_DEVICE(self), NULL, "PCI", "VEN", "DEV", NULL); fu_device_build_instance_id(FU_DEVICE(self), NULL, "PCI", "VEN", "DEV", "SUBSYS", NULL); /* success */ return TRUE; } static gboolean fu_redfish_device_probe_related_pcie_functions(FuRedfishDevice *self, const gchar *uri, GError **error) { FuRedfishDevicePrivate *priv = GET_PRIVATE(self); JsonObject *json_obj; g_autoptr(FuRedfishRequest) request = fu_redfish_backend_request_new(priv->backend); /* get URI */ if (!fu_redfish_request_perform(request, uri, FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON | FU_REDFISH_REQUEST_PERFORM_FLAG_USE_CACHE, error)) return FALSE; json_obj = fu_redfish_request_get_json_object(request); if (json_object_has_member(json_obj, "Members")) { JsonArray *members_array = json_object_get_array_member(json_obj, "Members"); for (guint i = 0; i < json_array_get_length(members_array); i++) { JsonObject *related_item; related_item = json_array_get_object_element(members_array, i); if (json_object_has_member(related_item, "@odata.id")) { const gchar *id = json_object_get_string_member(related_item, "@odata.id"); if (!fu_redfish_device_probe_related_pcie_item(self, id, error)) return FALSE; } } } /* success */ return TRUE; } static gboolean fu_redfish_device_probe_related_item(FuRedfishDevice *self, const gchar *uri, GError **error) { FuRedfishDevicePrivate *priv = GET_PRIVATE(self); JsonObject *json_obj; g_autoptr(FuRedfishRequest) request = fu_redfish_backend_request_new(priv->backend); /* get URI */ if (!fu_redfish_request_perform(request, uri, FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON | FU_REDFISH_REQUEST_PERFORM_FLAG_USE_CACHE, error)) return FALSE; json_obj = fu_redfish_request_get_json_object(request); /* optional properties */ if (json_object_has_member(json_obj, "SerialNumber")) { const gchar *tmp = json_object_get_string_member(json_obj, "SerialNumber"); if (tmp != NULL && tmp[0] != '\0' && g_strcmp0(tmp, "N/A") != 0) fu_device_set_serial(FU_DEVICE(self), tmp); } if (json_object_has_member(json_obj, "HotPluggable")) { /* this is better than the heuristic we get from the device name */ if (json_object_get_boolean_member(json_obj, "HotPluggable")) fu_device_remove_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); else fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); } /* sometimes an array, sometimes an object! */ if (json_object_has_member(json_obj, "PCIeFunctions")) { JsonNode *pcie_functions = json_object_get_member(json_obj, "PCIeFunctions"); if (JSON_NODE_HOLDS_OBJECT(pcie_functions)) { JsonObject *obj = json_node_get_object(pcie_functions); if (json_object_has_member(obj, "@odata.id")) { const gchar *id = json_object_get_string_member(obj, "@odata.id"); if (!fu_redfish_device_probe_related_pcie_functions(self, id, error)) return FALSE; } } } return TRUE; } /* parses a Lenovo XCC-format version like "11A-1.02" */ static gboolean fu_redfish_device_set_version_lenovo(FuRedfishDevice *self, const gchar *version, GError **error) { FuRedfishDevicePrivate *priv = GET_PRIVATE(self); g_autofree gchar *out_build = NULL; g_autofree gchar *out_version = NULL; /* split up Lenovo format */ if (!fu_redfish_common_parse_version_lenovo(version, &out_build, &out_version, error)) return FALSE; /* split out milestone */ priv->milestone = g_ascii_strtoull(out_build, NULL, 10); /* nocheck:blocked */ /* odd numbered builds are unsigned */ if (priv->milestone % 2 != 0) { fu_device_add_private_flag(FU_DEVICE(self), FU_REDFISH_DEVICE_FLAG_UNSIGNED_BUILD); } /* build is only one letter from A -> Z */ if (!g_ascii_isalpha(out_build[2])) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "build letter invalid"); return FALSE; } priv->build = g_strndup(out_build + 2, 1); fu_device_set_version(FU_DEVICE(self), out_version); fu_device_set_version_format(FU_DEVICE(self), fu_version_guess_format(out_version)); return TRUE; } static void fu_redfish_device_set_version(FuRedfishDevice *self, const gchar *tmp) { /* OEM specific */ if (g_strcmp0(fu_device_get_vendor(FU_DEVICE(self)), "Lenovo") == 0) { g_autoptr(GError) error_local = NULL; if (!fu_redfish_device_set_version_lenovo(self, tmp, &error_local)) { g_debug("failed to parse Lenovo version %s: %s", tmp, error_local->message); } } /* fallback */ if (fu_device_get_version(FU_DEVICE(self)) == NULL) { g_autofree gchar *ver = fu_redfish_common_fix_version(tmp); if (ver != NULL) { fu_device_set_version(FU_DEVICE(self), ver); fu_device_set_version_format(FU_DEVICE(self), fu_version_guess_format(ver)); } } } static void fu_redfish_device_set_version_lowest(FuRedfishDevice *self, const gchar *tmp) { /* OEM specific */ if (g_strcmp0(fu_device_get_vendor(FU_DEVICE(self)), "Lenovo") == 0) { g_autoptr(GError) error_local = NULL; g_autofree gchar *out_version = NULL; if (!fu_redfish_common_parse_version_lenovo(tmp, NULL, &out_version, &error_local)) { g_debug("failed to parse Lenovo version %s: %s", tmp, error_local->message); } fu_device_set_version_lowest(FU_DEVICE(self), out_version); } /* fallback */ if (fu_device_get_version_lowest(FU_DEVICE(self)) == NULL) { g_autofree gchar *ver = fu_redfish_common_fix_version(tmp); fu_device_set_version_lowest(FU_DEVICE(self), ver); } } static void fu_redfish_device_set_name(FuRedfishDevice *self, const gchar *name) { /* useless */ if (g_str_has_prefix(name, "Firmware:")) name += 9; /* device type */ if (g_str_has_prefix(name, "DEVICE-")) { name += 7; fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); } else if (g_str_has_prefix(name, "DISK-")) { name += 5; fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_DRIVE_HARDDISK); } else if (g_str_has_prefix(name, "POWER-")) { name += 6; fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_AC_ADAPTER); fu_device_set_summary(FU_DEVICE(self), "Redfish power supply unit"); } else { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); } /* heuristics */ if (g_strcmp0(name, "BMC") == 0) fu_device_set_summary(FU_DEVICE(self), "Redfish baseboard management controller"); if (g_str_has_suffix(name, "HBA") == 0) fu_device_set_summary(FU_DEVICE(self), "Redfish host bus adapter"); /* success */ fu_device_set_name(FU_DEVICE(self), name); } static void fu_redfish_device_set_vendor(FuRedfishDevice *self, const gchar *vendor) { g_autofree gchar *vendor_upper = NULL; /* fixup a common mistake */ if (g_strcmp0(vendor, "LEN") == 0 || g_strcmp0(vendor, "LNVO") == 0) vendor = "Lenovo"; fu_device_set_vendor(FU_DEVICE(self), vendor); /* add vendor-id */ vendor_upper = g_ascii_strup(vendor, -1); g_strdelimit(vendor_upper, " ", '_'); fu_device_build_vendor_id(FU_DEVICE(self), "REDFISH", vendor_upper); } static void fu_redfish_device_smc_license_check(FuRedfishDevice *self) { FuRedfishBackend *backend = fu_redfish_device_get_backend(self); g_autoptr(FuRedfishRequest) request = fu_redfish_backend_request_new(backend); g_autoptr(GError) error_local = NULL; /* see if we don't get an license error */ if (!fu_redfish_request_perform(request, fu_redfish_backend_get_push_uri_path(backend), FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { fu_device_add_problem(FU_DEVICE(self), FWUPD_DEVICE_PROBLEM_MISSING_LICENSE); } else { g_debug("supermicro license check returned %s", error_local->message); } } } static gboolean fu_redfish_device_probe_oem_hpe(FuRedfishDevice *self, JsonObject *json_object, GError **error) { if (json_object_has_member(json_object, "DeviceClass")) { const gchar *guid = json_object_get_string_member(json_object, "DeviceClass"); if (guid != NULL) fu_device_add_instance_id(FU_DEVICE(self), guid); } if (json_object_has_member(json_object, "Targets")) { JsonArray *json_array = json_object_get_array_member(json_object, "Targets"); for (guint i = 0; i < json_array_get_length(json_array); i++) { const gchar *guid = json_array_get_string_element(json_array, i); if (guid != NULL) fu_device_add_instance_id(FU_DEVICE(self), guid); } } return TRUE; } static gboolean fu_redfish_device_probe_oem_dell(FuRedfishDevice *self, JsonObject *json_object, GError **error) { JsonObject *software_info; /* ignore */ if (!json_object_has_member(json_object, "DellSoftwareInventory")) return TRUE; software_info = json_object_get_object_member(json_object, "DellSoftwareInventory"); if (json_object_has_member(software_info, "Status")) { const gchar *status = json_object_get_string_member(software_info, "Status"); if (g_strcmp0(status, "AvailableForInstallation") == 0) fu_device_add_private_flag(FU_DEVICE(self), FU_REDFISH_DEVICE_FLAG_IS_BACKUP); } /* it does not seem that Dell allows targeting a device when updating */ fu_device_add_private_flag(FU_DEVICE(self), FU_REDFISH_DEVICE_FLAG_WILDCARD_TARGETS); /* SYSTEMID is set by the backend */ if (!fu_device_build_instance_id_full(FU_DEVICE(self), FU_DEVICE_INSTANCE_FLAG_QUIRKS, error, "REDFISH", "VENDOR", "SYSTEMID", NULL)) return FALSE; return fu_device_build_instance_id(FU_DEVICE(self), error, "REDFISH", "VENDOR", "SYSTEMID", "SOFTWAREID", NULL); } static gboolean fu_redfish_device_probe(FuDevice *dev, GError **error) { FuRedfishDevice *self = FU_REDFISH_DEVICE(dev); FuRedfishDevicePrivate *priv = GET_PRIVATE(self); JsonObject *member = priv->member; const gchar *software_id = NULL; /* sanity check */ if (priv->member == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no member"); return FALSE; } /* required to POST later */ if (!json_object_has_member(member, "@odata.id")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no @odata.id string"); return FALSE; } fu_device_set_physical_id(dev, "Redfish-Inventory"); fu_device_set_logical_id(dev, json_object_get_string_member(member, "@odata.id")); if (json_object_has_member(member, "Id")) { const gchar *tmp = json_object_get_string_member(member, "Id"); if (tmp != NULL) fu_device_set_backend_id(dev, tmp); } fu_device_add_instance_str(dev, "ID", fu_device_get_backend_id(dev)); /* device properties */ if (json_object_has_member(member, "Manufacturer")) { const gchar *tmp = json_object_get_string_member(member, "Manufacturer"); if (tmp != NULL && tmp[0] != '\0') fu_redfish_device_set_vendor(self, tmp); } else { fu_redfish_device_set_vendor(self, fu_redfish_backend_get_vendor(priv->backend)); } fu_device_add_instance_strsafe(dev, "VENDOR", fu_device_get_vendor(dev)); /* the version can encode the instance ID suffix */ if (json_object_has_member(member, "Version")) { const gchar *tmp = json_object_get_string_member(member, "Version"); if (tmp != NULL && tmp[0] != '\0') fu_redfish_device_set_version(self, tmp); } /* ReleaseDate may or may not have a timezone */ if (json_object_has_member(member, "ReleaseDate")) { const gchar *tmp = json_object_get_string_member(member, "ReleaseDate"); if (tmp != NULL && tmp[0] != '\0' && g_strcmp0(tmp, "00:00:00Z") != 0) { g_autoptr(GDateTime) dt = NULL; g_autoptr(GTimeZone) tz = g_time_zone_new_utc(); dt = g_date_time_new_from_iso8601(tmp, tz); if (dt != NULL) { guint64 unixtime = (guint64)g_date_time_to_unix(dt); fu_device_set_version_build_date(dev, unixtime); } else { g_warning("failed to parse ISO8601 %s", tmp); } } } /* some vendors use a GUID, others use an ID like BMC-AFBT-10 */ if (json_object_has_member(member, "SoftwareId")) software_id = json_object_get_string_member(member, "SoftwareId"); if (software_id != NULL) { g_autofree gchar *software_id_lower = g_ascii_strdown(software_id, -1); if (fwupd_guid_is_valid(software_id_lower)) { fu_device_add_instance_id(dev, software_id_lower); } else { fu_device_add_instance_str(dev, "SOFTWAREID", software_id); if (fu_device_has_private_flag(dev, FU_REDFISH_DEVICE_FLAG_UNSIGNED_BUILD)) fu_device_add_instance_str(dev, "TYPE", "UNSIGNED"); fu_device_build_instance_id(dev, NULL, "REDFISH", "VENDOR", "SOFTWAREID", "TYPE", NULL); } } /* get vendor-specific properties too */ if (json_object_has_member(member, "Oem")) { JsonObject *oem = json_object_get_object_member(member, "Oem"); if (oem != NULL && json_object_has_member(oem, "Hpe")) { JsonObject *hpe = json_object_get_object_member(oem, "Hpe"); if (!fu_redfish_device_probe_oem_hpe(self, hpe, error)) return FALSE; } if (oem != NULL && json_object_has_member(oem, "Dell")) { JsonObject *json_oem = json_object_get_object_member(oem, "Dell"); if (!fu_redfish_device_probe_oem_dell(self, json_oem, error)) return FALSE; } } /* used for quirking and parenting */ fu_device_build_instance_id_full(dev, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "REDFISH", "VENDOR", "ID", NULL); if (json_object_has_member(member, "Name")) { const gchar *tmp = json_object_get_string_member(member, "Name"); if (tmp != NULL && tmp[0] != '\0') fu_redfish_device_set_name(self, tmp); } if (json_object_has_member(member, "LowestSupportedVersion")) { const gchar *tmp = json_object_get_string_member(member, "LowestSupportedVersion"); if (tmp != NULL && tmp[0] != '\0') fu_redfish_device_set_version_lowest(self, tmp); } if (json_object_has_member(member, "Description")) { const gchar *tmp = json_object_get_string_member(member, "Description"); if (tmp != NULL && tmp[0] != '\0') fu_device_set_summary(dev, tmp); } /* reasons why the device might not be updatable */ if (json_object_has_member(member, "Updateable")) { if (json_object_get_boolean_member(member, "Updateable")) fu_device_add_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE); } /* not useful to export */ if (fu_device_has_private_flag(dev, FU_REDFISH_DEVICE_FLAG_IS_BACKUP)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "%s is a backup partition", fu_device_get_backend_id(dev)); return FALSE; } /* use related items to set extra instance IDs */ if (fu_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE) && json_object_has_member(member, "RelatedItem")) { JsonArray *related_item_array = json_object_get_array_member(member, "RelatedItem"); for (guint i = 0; i < json_array_get_length(related_item_array); i++) { JsonObject *related_item; related_item = json_array_get_object_element(related_item_array, i); if (json_object_has_member(related_item, "@odata.id")) { const gchar *id = json_object_get_string_member(related_item, "@odata.id"); if (!fu_redfish_device_probe_related_item(self, id, error)) return FALSE; } } } /* for Supermicro check whether we have a proper Redfish license installed */ if (g_strcmp0("SMCI", fu_device_get_vendor(dev)) == 0) fu_redfish_device_smc_license_check(self); /* success */ return TRUE; } FuRedfishBackend * fu_redfish_device_get_backend(FuRedfishDevice *self) { FuRedfishDevicePrivate *priv = GET_PRIVATE(self); return priv->backend; } typedef struct { gchar *location; gboolean completed; GHashTable *messages_seen; FuProgress *progress; } FuRedfishDevicePollCtx; gboolean fu_redfish_device_parse_message_id(FuRedfishDevice *self, const gchar *message_id, const gchar *message, FuProgress *progress, GError **error) { /* ignore */ if (g_pattern_match_simple("TaskEvent.*.TaskProgressChanged", message_id) || g_pattern_match_simple("TaskEvent.*.TaskCompletedWarning", message_id) || g_pattern_match_simple("TaskEvent.*.TaskCompletedOK", message_id) || g_pattern_match_simple("Base.*.Success", message_id)) return TRUE; /* set flags */ if (g_pattern_match_simple("Base.*.ResetRequired", message_id)) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_REBOOT); return TRUE; } /* set error code */ if (g_pattern_match_simple("Update.*.AwaitToActivate", message_id)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NEEDS_USER_ACTION, message); return FALSE; } if (g_pattern_match_simple("Update.*.TransferFailed", message_id)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, message); return FALSE; } if (g_pattern_match_simple("Update.*.ActivateFailed", message_id)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, message); return FALSE; } if (g_pattern_match_simple("Update.*.VerificationFailed", message_id) || g_pattern_match_simple("LenovoFirmwareUpdateRegistry.*.UpdateVerifyFailed", message_id)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, message); return FALSE; } if (g_pattern_match_simple("Update.*.ApplyFailed", message_id) || g_pattern_match_simple("iLO.*.UpdateFailed", message_id)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, message); return FALSE; } /* set status */ if (g_pattern_match_simple("Update.*.TargetDetermined", message_id)) { fu_progress_set_status(progress, FWUPD_STATUS_LOADING); return TRUE; } if (g_pattern_match_simple("LenovoFirmwareUpdateRegistry.*.UpdateAssignment", message_id)) { fu_progress_set_status(progress, FWUPD_STATUS_LOADING); return TRUE; } if (g_pattern_match_simple("LenovoFirmwareUpdateRegistry.*.PayloadApplyInProgress", message_id)) { fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); return TRUE; } if (g_pattern_match_simple("LenovoFirmwareUpdateRegistry.*.PayloadApplyCompleted", message_id)) { fu_progress_set_status(progress, FWUPD_STATUS_IDLE); return TRUE; } if (g_pattern_match_simple("LenovoFirmwareUpdateRegistry.*.UpdateVerifyInProgress", message_id)) { fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_VERIFY); return TRUE; } if (g_pattern_match_simple("Update.*.TransferringToComponent", message_id)) { fu_progress_set_status(progress, FWUPD_STATUS_LOADING); return TRUE; } if (g_pattern_match_simple("Update.*.VerifyingAtComponent", message_id)) { fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_VERIFY); return TRUE; } if (g_pattern_match_simple("Update.*.UpdateInProgress", message_id)) { fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); return TRUE; } if (g_pattern_match_simple("Update.*.UpdateSuccessful", message_id)) { fu_progress_set_status(progress, FWUPD_STATUS_IDLE); return TRUE; } if (g_pattern_match_simple("Update.*.InstallingOnComponent", message_id)) { fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); return TRUE; } /* nothing to do */ return TRUE; } static gboolean fu_redfish_device_poll_task_once(FuRedfishDevice *self, FuRedfishDevicePollCtx *ctx, GError **error) { FuRedfishDevicePrivate *priv = GET_PRIVATE(self); JsonObject *json_obj; const gchar *message = "Unknown failure"; const gchar *state_tmp; g_autoptr(FuRedfishRequest) request = fu_redfish_backend_request_new(priv->backend); /* create URI and poll */ if (!fu_redfish_request_perform(request, ctx->location, FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON, error)) return FALSE; /* percentage is optional */ json_obj = fu_redfish_request_get_json_object(request); if (json_object_has_member(json_obj, "PercentComplete")) { gint64 pc = json_object_get_int_member(json_obj, "PercentComplete"); if (pc >= 0 && pc <= 100) fu_progress_set_percentage(ctx->progress, (guint)pc); } /* print all messages we've not seen yet */ if (json_object_has_member(json_obj, "Messages")) { JsonArray *json_msgs = json_object_get_array_member(json_obj, "Messages"); guint json_msgs_sz = json_array_get_length(json_msgs); for (guint i = 0; i < json_msgs_sz; i++) { JsonObject *json_message = json_array_get_object_element(json_msgs, i); const gchar *message_id = NULL; g_autofree gchar *message_key = NULL; /* set additional device properties */ if (json_object_has_member(json_message, "MessageId")) message_id = json_object_get_string_member(json_message, "MessageId"); if (json_object_has_member(json_message, "Message")) message = json_object_get_string_member(json_message, "Message"); /* ignore messages we've seen before */ message_key = g_strdup_printf("%s;%s", message_id, message); if (g_hash_table_contains(ctx->messages_seen, message_key)) { g_debug("ignoring %s", message_key); continue; } g_hash_table_add(ctx->messages_seen, g_steal_pointer(&message_key)); /* use the message */ g_debug("message #%u [%s]: %s", i, message_id, message); if (!fu_redfish_device_parse_message_id(self, message_id, message, ctx->progress, error)) return FALSE; } } /* use taskstate to set context */ if (!json_object_has_member(json_obj, "TaskState")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no TaskState for task manager"); return FALSE; } state_tmp = json_object_get_string_member(json_obj, "TaskState"); g_debug("TaskState now %s", state_tmp); if (g_strcmp0(state_tmp, "Completed") == 0) { ctx->completed = TRUE; return TRUE; } if (g_strcmp0(state_tmp, "Cancelled") == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Task was cancelled"); return FALSE; } if (g_strcmp0(state_tmp, "Exception") == 0 || g_strcmp0(state_tmp, "UserIntervention") == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, message); return FALSE; } /* try again */ return TRUE; } static FuRedfishDevicePollCtx * fu_redfish_device_poll_ctx_new(FuProgress *progress, const gchar *location) { FuRedfishDevicePollCtx *ctx = g_new0(FuRedfishDevicePollCtx, 1); ctx->messages_seen = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); ctx->location = g_strdup(location); ctx->progress = g_object_ref(progress); return ctx; } static void fu_redfish_device_poll_ctx_free(FuRedfishDevicePollCtx *ctx) { g_hash_table_unref(ctx->messages_seen); g_object_unref(ctx->progress); g_free(ctx->location); g_free(ctx); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuRedfishDevicePollCtx, fu_redfish_device_poll_ctx_free) #pragma clang diagnostic pop gboolean fu_redfish_device_poll_task(FuRedfishDevice *self, const gchar *location, FuProgress *progress, GError **error) { const guint timeout = 2400; g_autoptr(GTimer) timer = g_timer_new(); g_autoptr(FuRedfishDevicePollCtx) ctx = fu_redfish_device_poll_ctx_new(progress, location); /* sleep and then reprobe hardware */ do { fu_device_sleep(FU_DEVICE(self), 1000); /* ms */ if (!fu_redfish_device_poll_task_once(self, ctx, error)) return FALSE; if (ctx->completed) { fu_progress_finished(progress); return TRUE; } } while (g_timer_elapsed(timer, NULL) < timeout); /* success */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to poll %s for success after %u seconds", location, timeout); return FALSE; } guint fu_redfish_device_get_reset_pre_delay(FuRedfishDevice *self) { FuRedfishDevicePrivate *priv = GET_PRIVATE(self); return priv->reset_pre_delay; } guint fu_redfish_device_get_reset_post_delay(FuRedfishDevice *self) { FuRedfishDevicePrivate *priv = GET_PRIVATE(self); return priv->reset_post_delay; } static gboolean fu_redfish_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuRedfishDevice *self = FU_REDFISH_DEVICE(device); FuRedfishDevicePrivate *priv = GET_PRIVATE(self); guint64 tmp = 0; if (g_strcmp0(key, "RedfishResetPreDelay") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->reset_pre_delay = tmp; return TRUE; } if (g_strcmp0(key, "RedfishResetPostDelay") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->reset_post_delay = tmp; return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_redfish_device_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuRedfishDevice *self = FU_REDFISH_DEVICE(object); FuRedfishDevicePrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_BACKEND: g_value_set_object(value, priv->backend); break; case PROP_MEMBER: g_value_set_pointer(value, priv->member); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_redfish_device_set_member(FuRedfishDevice *self, JsonObject *member) { FuRedfishDevicePrivate *priv = GET_PRIVATE(self); if (priv->member != NULL) { json_object_unref(priv->member); priv->member = NULL; } if (member != NULL) priv->member = json_object_ref(member); } static void fu_redfish_device_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuRedfishDevice *self = FU_REDFISH_DEVICE(object); FuRedfishDevicePrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_BACKEND: g_set_object(&priv->backend, g_value_get_object(value)); break; case PROP_MEMBER: fu_redfish_device_set_member(self, g_value_get_pointer(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_redfish_device_init(FuRedfishDevice *self) { fu_device_set_summary(FU_DEVICE(self), "Redfish device"); fu_device_add_protocol(FU_DEVICE(self), "org.dmtf.redfish"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_NAME); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_VERFMT); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_ICON); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_VENDOR); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_SIGNED); fu_device_register_private_flag(FU_DEVICE(self), FU_REDFISH_DEVICE_FLAG_IS_BACKUP); fu_device_register_private_flag(FU_DEVICE(self), FU_REDFISH_DEVICE_FLAG_UNSIGNED_BUILD); fu_device_register_private_flag(FU_DEVICE(self), FU_REDFISH_DEVICE_FLAG_WILDCARD_TARGETS); fu_device_register_private_flag(FU_DEVICE(self), FU_REDFISH_DEVICE_FLAG_MANAGER_RESET); fu_device_register_private_flag(FU_DEVICE(self), FU_REDFISH_DEVICE_FLAG_NO_MANAGER_RESET_REQUEST); } static void fu_redfish_device_finalize(GObject *object) { FuRedfishDevice *self = FU_REDFISH_DEVICE(object); FuRedfishDevicePrivate *priv = GET_PRIVATE(self); if (priv->backend != NULL) g_object_unref(priv->backend); if (priv->member != NULL) json_object_unref(priv->member); g_free(priv->build); G_OBJECT_CLASS(fu_redfish_device_parent_class)->finalize(object); } static void fu_redfish_device_class_init(FuRedfishDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); GParamSpec *pspec; FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->get_property = fu_redfish_device_get_property; object_class->set_property = fu_redfish_device_set_property; object_class->finalize = fu_redfish_device_finalize; device_class->to_string = fu_redfish_device_to_string; device_class->probe = fu_redfish_device_probe; device_class->set_quirk_kv = fu_redfish_device_set_quirk_kv; /** * FuRedfishDevice:backend: * * The backend that added the device. */ pspec = g_param_spec_object("backend", NULL, NULL, FU_TYPE_BACKEND, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_BACKEND, pspec); /** * FuRedfishDevice:member: * * The JSON root member for the device. */ pspec = g_param_spec_pointer("member", NULL, NULL, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_MEMBER, pspec); } fwupd-2.0.10/plugins/redfish/fu-redfish-device.h000066400000000000000000000023651501337203100214670ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-redfish-backend.h" #define FU_TYPE_REDFISH_DEVICE (fu_redfish_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuRedfishDevice, fu_redfish_device, FU, REDFISH_DEVICE, FuDevice) struct _FuRedfishDeviceClass { FuDeviceClass parent_class; }; #define FU_REDFISH_DEVICE_FLAG_IS_BACKUP "is-backup" #define FU_REDFISH_DEVICE_FLAG_UNSIGNED_BUILD "unsigned-build" #define FU_REDFISH_DEVICE_FLAG_MANAGER_RESET "manager-reset" #define FU_REDFISH_DEVICE_FLAG_WILDCARD_TARGETS "wildcard-targets" #define FU_REDFISH_DEVICE_FLAG_NO_MANAGER_RESET_REQUEST "no-manager-reset-request" FuRedfishBackend * fu_redfish_device_get_backend(FuRedfishDevice *self); gboolean fu_redfish_device_parse_message_id(FuRedfishDevice *self, const gchar *message_id, const gchar *message, FuProgress *progress, GError **error); gboolean fu_redfish_device_poll_task(FuRedfishDevice *self, const gchar *location, FuProgress *progress, GError **error); guint fu_redfish_device_get_reset_pre_delay(FuRedfishDevice *self); guint fu_redfish_device_get_reset_post_delay(FuRedfishDevice *self); fwupd-2.0.10/plugins/redfish/fu-redfish-hpe-device.c000066400000000000000000000245661501337203100222430ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * Copyright 2025 Arno Dubois * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-redfish-hpe-device.h" #include "fu-redfish-request.h" struct _FuRedfishHpeDevice { FuRedfishDevice parent_instance; }; G_DEFINE_TYPE(FuRedfishHpeDevice, fu_redfish_hpe_device, FU_TYPE_REDFISH_DEVICE) typedef struct curl_slist _curl_slist; G_DEFINE_AUTOPTR_CLEANUP_FUNC(_curl_slist, curl_slist_free_all) G_DEFINE_AUTOPTR_CLEANUP_FUNC(curl_mime, curl_mime_free) static gboolean fu_redfish_hpe_device_attach(FuDevice *dev, FuProgress *progress, GError **error) { FuRedfishHpeDevice *self = FU_REDFISH_HPE_DEVICE(dev); FuRedfishBackend *backend = fu_redfish_device_get_backend(FU_REDFISH_DEVICE(self)); JsonObject *json_obj; g_autoptr(FuRedfishRequest) request = fu_redfish_backend_request_new(backend); /* create URI and poll */ if (!fu_redfish_request_perform(request, "/redfish/v1/UpdateService", FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON, error)) return FALSE; /* percentage is optional */ json_obj = fu_redfish_request_get_json_object(request); if (json_object_has_member(json_obj, "Oem")) { JsonObject *oem = json_object_get_object_member(json_obj, "Oem"); if (oem != NULL && json_object_has_member(oem, "Hpe")) { JsonObject *hpe = json_object_get_object_member(oem, "Hpe"); const gchar *status = json_object_get_string_member(hpe, "State"); /* if we are in an idle-ish state, we can proceed */ if (g_strcmp0(status, "Idle") == 0 || g_strcmp0(status, "Error") == 0 || g_strcmp0(status, "Complete") == 0) { return TRUE; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "device is busy"); return FALSE; } } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unknown failure"); return FALSE; } typedef struct { gboolean completed; FuProgress *progress; } FuRedfishHpeDevicePollCtx; static FuRedfishHpeDevicePollCtx * fu_redfish_hpe_device_poll_ctx_new(FuProgress *progress) { FuRedfishHpeDevicePollCtx *ctx = g_new0(FuRedfishHpeDevicePollCtx, 1); ctx->progress = g_object_ref(progress); return ctx; } static void fu_redfish_hpe_device_poll_ctx_free(FuRedfishHpeDevicePollCtx *ctx) { g_object_unref(ctx->progress); g_free(ctx); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuRedfishHpeDevicePollCtx, fu_redfish_hpe_device_poll_ctx_free) #pragma clang diagnostic pop static gboolean fu_redfish_hpe_device_poll_task_once(FuRedfishDevice *self, FuRedfishHpeDevicePollCtx *ctx, GError **error) { FuRedfishBackend *backend = fu_redfish_device_get_backend(FU_REDFISH_DEVICE(self)); JsonObject *json_obj; const gchar *message = "Unknown failure"; g_autoptr(FuRedfishRequest) request = fu_redfish_backend_request_new(backend); /* create URI and poll */ if (!fu_redfish_request_perform(request, "/redfish/v1/UpdateService", FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON, error)) return FALSE; /* percentage is optional */ json_obj = fu_redfish_request_get_json_object(request); if (json_object_has_member(json_obj, "Oem")) { JsonObject *oem = json_object_get_object_member(json_obj, "Oem"); if (oem != NULL && json_object_has_member(oem, "Hpe")) { JsonObject *hpe = json_object_get_object_member(oem, "Hpe"); const gchar *status = json_object_get_string_member(hpe, "State"); if (g_strcmp0(status, "Error") == 0) { /* default error, will be replaced by something more fitting * in fu_redfish_device_parse_message_id if we can */ if (json_object_has_member(hpe, "Result")) { JsonObject *result = json_object_get_object_member(hpe, "Result"); const gchar *message_id = NULL; if (json_object_has_member(result, "MessageId")) message_id = json_object_get_string_member(result, "MessageId"); if (json_object_has_member(result, "Message")) message = json_object_get_string_member(result, "Message"); /* use the message */ g_debug("message [%s]: %s", message_id, message); if (!fu_redfish_device_parse_message_id(self, message_id, message, ctx->progress, error)) return FALSE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, message); return FALSE; } if (json_object_has_member(hpe, "FlashProgressPercent")) { gint64 pc = json_object_get_int_member(hpe, "FlashProgressPercent"); if (pc >= 0 && pc <= 100) fu_progress_set_percentage(ctx->progress, (guint)pc); } if (g_strcmp0(status, "Writing") == 0 || g_strcmp0(status, "Updating") == 0) { fu_progress_set_status(ctx->progress, FWUPD_STATUS_DEVICE_WRITE); } else if (g_strcmp0(status, "Verifying") == 0) { fu_progress_set_status(ctx->progress, FWUPD_STATUS_DEVICE_VERIFY); } else if (g_strcmp0(status, "Complete") == 0) { ctx->completed = TRUE; fu_progress_set_status(ctx->progress, FWUPD_STATUS_IDLE); } } } /* try again */ return TRUE; } static gboolean fu_redfish_hpe_device_poll_task(FuRedfishDevice *self, FuProgress *progress, GError **error) { const guint timeout = 2400; g_autoptr(FuRedfishHpeDevicePollCtx) ctx = fu_redfish_hpe_device_poll_ctx_new(progress); g_autoptr(GTimer) timer = g_timer_new(); /* sleep and then reprobe hardware */ do { fu_device_sleep(FU_DEVICE(self), 1000); /* ms */ if (!fu_redfish_hpe_device_poll_task_once(self, ctx, error)) return FALSE; if (ctx->completed) { fu_progress_finished(progress); return TRUE; } } while (g_timer_elapsed(timer, NULL) < timeout); /* success */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to poll for success after %u seconds", timeout); return FALSE; } static gboolean fu_redfish_hpe_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuRedfishHpeDevice *self = FU_REDFISH_HPE_DEVICE(device); FuRedfishBackend *backend = fu_redfish_device_get_backend(FU_REDFISH_DEVICE(self)); CURL *curl; const gchar *sessionkey; curl_mimepart *part; g_autofree gchar *sessionkey_kv = NULL; g_autoptr(curl_mime) mime = NULL; g_autoptr(_curl_slist) hs = NULL; g_autoptr(FuRedfishRequest) request = NULL; g_autoptr(GBytes) fw = NULL; g_autoptr(GString) json_str = g_string_new(NULL); g_autoptr(JsonBuilder) builder = json_builder_new(); g_autoptr(JsonGenerator) json_generator = json_generator_new(); g_autoptr(JsonNode) json_root = NULL; /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_WAITING_FOR_AUTH, 3, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DOWNLOADING, 10, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 5, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 82, NULL); /* create session */ if (!fu_redfish_backend_create_session(backend, error)) return FALSE; sessionkey = fu_redfish_backend_get_session_key(backend); fu_progress_step_done(progress); /* create the multipart request */ request = fu_redfish_backend_request_new(backend); curl = fu_redfish_request_get_curl(request); mime = curl_mime_init(curl); /* create header */ json_builder_begin_object(builder); json_builder_set_member_name(builder, "UpdateRepository"); json_builder_add_boolean_value(builder, FALSE); json_builder_set_member_name(builder, "UpdateTarget"); json_builder_add_boolean_value(builder, TRUE); json_builder_set_member_name(builder, "ETag"); json_builder_add_string_value(builder, "atag"); json_builder_end_object(builder); /* export as a string */ json_root = json_builder_get_root(builder); json_generator_set_pretty(json_generator, TRUE); json_generator_set_root(json_generator, json_root); json_generator_to_gstring(json_generator, json_str); part = curl_mime_addpart(mime); curl_mime_name(part, "sessionKey"); (void)curl_mime_data(part, sessionkey, CURL_ZERO_TERMINATED); part = curl_mime_addpart(mime); curl_mime_name(part, "parameters"); (void)curl_mime_type(part, "application/json"); (void)curl_mime_data(part, json_str->str, CURL_ZERO_TERMINATED); g_debug("request: %s", json_str->str); part = curl_mime_addpart(mime); curl_mime_name(part, "files[]"); (void)curl_mime_type(part, "application/octet-stream"); (void)curl_mime_filename(part, "firmware.fwpkg"); (void)curl_mime_data(part, g_bytes_get_data(fw, NULL), g_bytes_get_size(fw)); (void)curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime); sessionkey_kv = g_strconcat("sessionKey=", sessionkey, NULL); (void)curl_easy_setopt(curl, CURLOPT_COOKIE, sessionkey_kv); hs = curl_slist_append(hs, g_strconcat("X-Auth-Token: ", sessionkey, NULL)); (void)curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hs); fu_progress_step_done(progress); if (!fu_redfish_request_perform(request, fu_redfish_backend_get_push_uri_path(backend), 0, error)) return FALSE; if (fu_redfish_request_get_status_code(request) != 200) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to upload using HPE specific method: %li", fu_redfish_request_get_status_code(request)); return FALSE; } if (!fu_redfish_hpe_device_poll_task(FU_REDFISH_DEVICE(self), fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static void fu_redfish_hpe_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 3, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 96, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_redfish_hpe_device_init(FuRedfishHpeDevice *self) { } static void fu_redfish_hpe_device_class_init(FuRedfishHpeDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->attach = fu_redfish_hpe_device_attach; device_class->write_firmware = fu_redfish_hpe_device_write_firmware; device_class->set_progress = fu_redfish_hpe_device_set_progress; } fwupd-2.0.10/plugins/redfish/fu-redfish-hpe-device.h000066400000000000000000000006331501337203100222350ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * Copyright 2025 Arno Dubois * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-redfish-device.h" #define FU_TYPE_REDFISH_HPE_DEVICE (fu_redfish_hpe_device_get_type()) G_DECLARE_FINAL_TYPE(FuRedfishHpeDevice, fu_redfish_hpe_device, FU, REDFISH_HPE_DEVICE, FuRedfishDevice) fwupd-2.0.10/plugins/redfish/fu-redfish-legacy-device.c000066400000000000000000000122001501337203100227110ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-redfish-legacy-device.h" #include "fu-redfish-request.h" struct _FuRedfishLegacyDevice { FuRedfishDevice parent_instance; }; G_DEFINE_TYPE(FuRedfishLegacyDevice, fu_redfish_legacy_device, FU_TYPE_REDFISH_DEVICE) static gboolean fu_redfish_legacy_device_detach(FuDevice *dev, FuProgress *progress, GError **error) { FuRedfishLegacyDevice *self = FU_REDFISH_LEGACY_DEVICE(dev); FuRedfishBackend *backend = fu_redfish_device_get_backend(FU_REDFISH_DEVICE(self)); g_autoptr(FuRedfishRequest) request = fu_redfish_backend_request_new(backend); g_autoptr(JsonBuilder) builder = json_builder_new(); /* create header */ json_builder_begin_object(builder); json_builder_set_member_name(builder, "HttpPushUriTargetsBusy"); json_builder_add_boolean_value(builder, TRUE); json_builder_set_member_name(builder, "HttpPushUriTargets"); json_builder_begin_array(builder); json_builder_add_string_value(builder, fu_device_get_logical_id(FU_DEVICE(self))); json_builder_end_array(builder); json_builder_end_object(builder); /* patch the two fields */ return fu_redfish_request_perform_full(request, "/redfish/v1/UpdateService", "PATCH", builder, FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON | FU_REDFISH_REQUEST_PERFORM_FLAG_USE_ETAG, error); } static gboolean fu_redfish_legacy_device_attach(FuDevice *dev, FuProgress *progress, GError **error) { FuRedfishLegacyDevice *self = FU_REDFISH_LEGACY_DEVICE(dev); FuRedfishBackend *backend = fu_redfish_device_get_backend(FU_REDFISH_DEVICE(self)); g_autoptr(FuRedfishRequest) request = fu_redfish_backend_request_new(backend); g_autoptr(JsonBuilder) builder = json_builder_new(); /* create header */ json_builder_begin_object(builder); json_builder_set_member_name(builder, "HttpPushUriTargetsBusy"); json_builder_add_boolean_value(builder, FALSE); json_builder_set_member_name(builder, "HttpPushUriTargets"); json_builder_begin_array(builder); json_builder_end_array(builder); json_builder_end_object(builder); /* patch the two fields */ return fu_redfish_request_perform_full(request, "/redfish/v1/UpdateService", "PATCH", builder, FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON | FU_REDFISH_REQUEST_PERFORM_FLAG_USE_ETAG, error); } static gboolean fu_redfish_legacy_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuRedfishLegacyDevice *self = FU_REDFISH_LEGACY_DEVICE(device); FuRedfishBackend *backend = fu_redfish_device_get_backend(FU_REDFISH_DEVICE(self)); CURL *curl; JsonObject *json_obj; const gchar *location; g_autoptr(FuRedfishRequest) request = NULL; g_autoptr(GBytes) fw = NULL; /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* POST data */ request = fu_redfish_backend_request_new(backend); curl = fu_redfish_request_get_curl(request); (void)curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); (void)curl_easy_setopt(curl, CURLOPT_POSTFIELDS, g_bytes_get_data(fw, NULL)); (void)curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)g_bytes_get_size(fw)); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); if (!fu_redfish_request_perform(request, fu_redfish_backend_get_push_uri_path(backend), FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON, error)) return FALSE; /* poll the task for progress */ json_obj = fu_redfish_request_get_json_object(request); if (!json_object_has_member(json_obj, "@odata.id")) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no task returned for %s", fu_redfish_backend_get_push_uri_path(backend)); return FALSE; } location = json_object_get_string_member(json_obj, "@odata.id"); return fu_redfish_device_poll_task(FU_REDFISH_DEVICE(self), location, progress, error); } static void fu_redfish_legacy_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 5, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 93, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_redfish_legacy_device_init(FuRedfishLegacyDevice *self) { fu_device_set_summary(FU_DEVICE(self), "Redfish legacy device"); } static void fu_redfish_legacy_device_class_init(FuRedfishLegacyDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->attach = fu_redfish_legacy_device_attach; device_class->detach = fu_redfish_legacy_device_detach; device_class->write_firmware = fu_redfish_legacy_device_write_firmware; device_class->set_progress = fu_redfish_legacy_device_set_progress; } fwupd-2.0.10/plugins/redfish/fu-redfish-legacy-device.h000066400000000000000000000005701501337203100227250ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-redfish-device.h" #define FU_TYPE_REDFISH_LEGACY_DEVICE (fu_redfish_legacy_device_get_type()) G_DECLARE_FINAL_TYPE(FuRedfishLegacyDevice, fu_redfish_legacy_device, FU, REDFISH_LEGACY_DEVICE, FuRedfishDevice) fwupd-2.0.10/plugins/redfish/fu-redfish-multipart-device.c000066400000000000000000000134471501337203100235040ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-redfish-device.h" #include "fu-redfish-multipart-device.h" #include "fu-redfish-request.h" struct _FuRedfishMultipartDevice { FuRedfishDevice parent_instance; }; G_DEFINE_TYPE(FuRedfishMultipartDevice, fu_redfish_multipart_device, FU_TYPE_REDFISH_DEVICE) G_DEFINE_AUTOPTR_CLEANUP_FUNC(curl_mime, curl_mime_free) static GString * fu_redfish_multipart_device_get_parameters(FuRedfishMultipartDevice *self) { g_autoptr(GString) str = g_string_new(NULL); g_autoptr(JsonBuilder) builder = json_builder_new(); g_autoptr(JsonGenerator) json_generator = json_generator_new(); g_autoptr(JsonNode) json_root = NULL; /* create header */ json_builder_begin_object(builder); json_builder_set_member_name(builder, "Targets"); json_builder_begin_array(builder); if (!fu_device_has_private_flag(FU_DEVICE(self), FU_REDFISH_DEVICE_FLAG_WILDCARD_TARGETS)) { const gchar *logical_id = fu_device_get_logical_id(FU_DEVICE(self)); json_builder_add_string_value(builder, logical_id); } json_builder_end_array(builder); json_builder_set_member_name(builder, "@Redfish.OperationApplyTime"); json_builder_add_string_value(builder, "Immediate"); json_builder_end_object(builder); /* export as a string */ json_root = json_builder_get_root(builder); json_generator_set_pretty(json_generator, TRUE); json_generator_set_root(json_generator, json_root); json_generator_to_gstring(json_generator, str); return g_steal_pointer(&str); } static size_t fu_redfish_multipart_device_location_headers_callback(char *ptr, size_t size, size_t nmemb, void *location) { char **loc_str = (char **)location; if ((size * nmemb) > 16 && g_ascii_strncasecmp(ptr, "Location:", 9) == 0) { /* The string also includes \r\n at the end */ *loc_str = g_strndup(ptr + 10, (size * nmemb) - 12); } return size * nmemb; } static gboolean fu_redfish_multipart_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuRedfishMultipartDevice *self = FU_REDFISH_MULTIPART_DEVICE(device); FuRedfishBackend *backend = fu_redfish_device_get_backend(FU_REDFISH_DEVICE(self)); CURL *curl; JsonObject *json_obj; curl_mimepart *part; const gchar *location; g_autoptr(curl_mime) mime = NULL; g_autoptr(FuRedfishRequest) request = NULL; g_autoptr(GBytes) fw = NULL; g_autoptr(GString) params = NULL; /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* create the multipart request */ request = fu_redfish_backend_request_new(backend); curl = fu_redfish_request_get_curl(request); mime = curl_mime_init(curl); params = fu_redfish_multipart_device_get_parameters(self); part = curl_mime_addpart(mime); curl_mime_name(part, "UpdateParameters"); (void)curl_mime_type(part, "application/json"); (void)curl_mime_data(part, params->str, CURL_ZERO_TERMINATED); g_debug("request: %s", params->str); part = curl_mime_addpart(mime); curl_mime_name(part, "UpdateFile"); (void)curl_mime_type(part, "application/octet-stream"); (void)curl_mime_filename(part, "firmware.bin"); (void)curl_mime_data(part, g_bytes_get_data(fw, NULL), g_bytes_get_size(fw)); (void)curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime); (void)curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, fu_redfish_multipart_device_location_headers_callback); (void)curl_easy_setopt(fu_redfish_request_get_curl(request), CURLOPT_HEADERDATA, &location); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); if (!fu_redfish_request_perform(request, fu_redfish_backend_get_push_uri_path(backend), FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON, error)) return FALSE; if (fu_redfish_request_get_status_code(request) != 202) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to upload: %li", fu_redfish_request_get_status_code(request)); return FALSE; } /* prefer the header, otherwise fall back to the response */ if (location == NULL || g_utf8_strlen(location, 1) == 0) { json_obj = fu_redfish_request_get_json_object(request); if (json_object_has_member(json_obj, "TaskMonitor")) { const gchar *tmp = json_object_get_string_member(json_obj, "TaskMonitor"); g_debug("task manager for cleanup is %s", tmp); } /* poll the task for progress */ if (!json_object_has_member(json_obj, "@odata.id")) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no task returned for %s", fu_redfish_backend_get_push_uri_path(backend)); return FALSE; } location = json_object_get_string_member(json_obj, "@odata.id"); } return fu_redfish_device_poll_task(FU_REDFISH_DEVICE(self), location, progress, error); } static void fu_redfish_multipart_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_redfish_multipart_device_init(FuRedfishMultipartDevice *self) { fu_device_set_summary(FU_DEVICE(self), "Redfish multipart device"); } static void fu_redfish_multipart_device_class_init(FuRedfishMultipartDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_redfish_multipart_device_write_firmware; device_class->set_progress = fu_redfish_multipart_device_set_progress; } fwupd-2.0.10/plugins/redfish/fu-redfish-multipart-device.h000066400000000000000000000006071501337203100235030ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-redfish-device.h" #define FU_TYPE_REDFISH_MULTIPART_DEVICE (fu_redfish_multipart_device_get_type()) G_DECLARE_FINAL_TYPE(FuRedfishMultipartDevice, fu_redfish_multipart_device, FU, REDFISH_MULTIPART_DEVICE, FuRedfishDevice) fwupd-2.0.10/plugins/redfish/fu-redfish-network-device.c000066400000000000000000000126701501337203100231510ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-redfish-network-device.h" #include "fu-redfish-network.h" struct _FuRedfishNetworkDevice { GObject parent_instance; gchar *object_path; }; G_DEFINE_TYPE(FuRedfishNetworkDevice, fu_redfish_network_device, G_TYPE_OBJECT) gboolean fu_redfish_network_device_get_state(FuRedfishNetworkDevice *self, FuRedfishNetworkDeviceState *state, GError **error) { g_autoptr(GVariant) retval = NULL; g_autoptr(GDBusProxy) proxy = NULL; g_return_val_if_fail(FU_IS_REDFISH_NETWORK_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* connect to device */ proxy = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS, NULL, NETWORK_MANAGER_SERVICE_NAME, self->object_path, NETWORK_MANAGER_INTERFACE_DEVICE, NULL, error); if (proxy == NULL) return FALSE; retval = g_dbus_proxy_get_cached_property(proxy, "State"); if (retval == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "could not find State"); return FALSE; } if (state != NULL) *state = g_variant_get_uint32(retval); return TRUE; } gboolean fu_redfish_network_device_connect(FuRedfishNetworkDevice *self, GError **error) { g_autoptr(GDBusProxy) proxy = NULL; g_autoptr(GTimer) timer = g_timer_new(); g_autoptr(GVariant) success = NULL; g_return_val_if_fail(FU_IS_REDFISH_NETWORK_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* connect to manager */ proxy = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS, NULL, NETWORK_MANAGER_SERVICE_NAME, NETWORK_MANAGER_PATH, NETWORK_MANAGER_INTERFACE, NULL, error); if (proxy == NULL) return FALSE; /* activate with some good defaults */ success = g_dbus_proxy_call_sync(proxy, "ActivateConnection", g_variant_new("(ooo)", "/", self->object_path, "/"), G_DBUS_CALL_FLAGS_NONE, -1, NULL, error); if (success == NULL) return FALSE; /* wait until the network interface comes up */ do { FuRedfishNetworkDeviceState state = FU_REDFISH_NETWORK_DEVICE_STATE_UNKNOWN; if (!fu_redfish_network_device_get_state(self, &state, error)) return FALSE; g_debug("%s device state is now %s [%u]", self->object_path, fu_redfish_network_device_state_to_string(state), state); if (state == FU_REDFISH_NETWORK_DEVICE_STATE_ACTIVATED) return TRUE; g_usleep(50 * 1000); } while (g_timer_elapsed(timer, NULL) < 5.f); /* timed out */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "could not activate connection"); return FALSE; } gchar * fu_redfish_network_device_get_address(FuRedfishNetworkDevice *self, GError **error) { g_autofree gchar *address = NULL; g_autoptr(GDBusProxy) proxy = NULL; g_autoptr(GDBusProxy) proxy2 = NULL; g_autoptr(GVariant) addr_data = NULL; g_autoptr(GVariant) ip4_config = NULL; g_return_val_if_fail(FU_IS_REDFISH_NETWORK_DEVICE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* connect to device */ proxy = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS, NULL, NETWORK_MANAGER_SERVICE_NAME, self->object_path, NETWORK_MANAGER_INTERFACE_DEVICE, NULL, error); if (proxy == NULL) return NULL; ip4_config = g_dbus_proxy_get_cached_property(proxy, "Ip4Config"); if (ip4_config == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "could not find IPv4 config"); return NULL; } proxy2 = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS, NULL, NETWORK_MANAGER_SERVICE_NAME, g_variant_get_string(ip4_config, NULL), NETWORK_MANAGER_INTERFACE_IP4_CONFIG, NULL, error); if (proxy2 == NULL) return NULL; addr_data = g_dbus_proxy_get_cached_property(proxy2, "AddressData"); if (addr_data != NULL) { g_autoptr(GVariant) addr_data0 = g_variant_get_child_value(addr_data, 0); g_autoptr(GVariantDict) dict = g_variant_dict_new(addr_data0); g_variant_dict_lookup(dict, "address", "s", &address); } if (address == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "could not find IP address for device"); return NULL; } /* success */ return g_steal_pointer(&address); } FuRedfishNetworkDevice * fu_redfish_network_device_new(const gchar *object_path) { FuRedfishNetworkDevice *self = g_object_new(FU_TYPE_REDFISH_NETWORK_DEVICE, NULL); self->object_path = g_strdup(object_path); return self; } static void fu_redfish_network_device_init(FuRedfishNetworkDevice *self) { } static void fu_redfish_network_device_finalize(GObject *object) { FuRedfishNetworkDevice *self = FU_REDFISH_NETWORK_DEVICE(object); g_free(self->object_path); G_OBJECT_CLASS(fu_redfish_network_device_parent_class)->finalize(object); } static void fu_redfish_network_device_class_init(FuRedfishNetworkDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_redfish_network_device_finalize; } fwupd-2.0.10/plugins/redfish/fu-redfish-network-device.h000066400000000000000000000014511501337203100231510ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-redfish-struct.h" #define FU_TYPE_REDFISH_NETWORK_DEVICE (fu_redfish_network_device_get_type()) G_DECLARE_FINAL_TYPE(FuRedfishNetworkDevice, fu_redfish_network_device, FU, REDFISH_NETWORK_DEVICE, GObject) FuRedfishNetworkDevice * fu_redfish_network_device_new(const gchar *object_path); gboolean fu_redfish_network_device_get_state(FuRedfishNetworkDevice *self, FuRedfishNetworkDeviceState *state, GError **error); gchar * fu_redfish_network_device_get_address(FuRedfishNetworkDevice *self, GError **error); gboolean fu_redfish_network_device_connect(FuRedfishNetworkDevice *self, GError **error); fwupd-2.0.10/plugins/redfish/fu-redfish-network.c000066400000000000000000000141151501337203100217100ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-redfish-network.h" typedef struct { FuContext *ctx; FuRedfishNetworkDevice *device; const gchar *mac_addr; guint16 vid; guint16 pid; } FuRedfishNetworkMatchHelper; static gboolean fu_redfish_network_device_match_device(FuRedfishNetworkMatchHelper *helper, const gchar *object_path, GError **error) { g_autoptr(GDBusProxy) proxy = NULL; /* connect to device */ proxy = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, NULL, NETWORK_MANAGER_SERVICE_NAME, object_path, NETWORK_MANAGER_INTERFACE_DEVICE, NULL, error); if (proxy == NULL) { g_prefix_error(error, "failed to connect to interface %s: ", object_path); return FALSE; } /* compare MAC address different */ if (helper->mac_addr != NULL) { const gchar *mac_addr = NULL; g_autoptr(GVariant) hw_address = NULL; hw_address = g_dbus_proxy_get_cached_property(proxy, "HwAddress"); if (hw_address == NULL) return TRUE; mac_addr = g_variant_get_string(hw_address, NULL); /* verify */ g_debug("mac_addr=%s", mac_addr); if (g_strcmp0(mac_addr, helper->mac_addr) == 0) helper->device = fu_redfish_network_device_new(object_path); } /* compare VID:PID */ if (helper->vid != 0x0 && helper->pid != 0x0) { g_autoptr(FuBackend) udev_backend = NULL; g_autoptr(FuDevice) udev_device = NULL; g_autoptr(FuDevice) phys_device = NULL; g_autoptr(GVariant) udi = NULL; udi = g_dbus_proxy_get_cached_property(proxy, "Udi"); if (udi == NULL) return TRUE; udev_backend = fu_context_get_backend_by_name(helper->ctx, "udev", error); if (udev_backend == NULL) return FALSE; udev_device = fu_backend_create_device(udev_backend, g_variant_get_string(udi, NULL), error); if (udev_device == NULL) return FALSE; /* get the deepest USB parent device, falling back to the PCI device */ phys_device = fu_device_get_backend_parent_with_subsystem(udev_device, "usb:usb_device", NULL); if (phys_device == NULL) { phys_device = fu_device_get_backend_parent_with_subsystem(udev_device, "pci", NULL); } if (phys_device == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no physical device found for %s", fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(udev_device))); return FALSE; } /* verify */ g_debug("%s: 0x%04x, 0x%04x", g_variant_get_string(udi, NULL), fu_device_get_vid(phys_device), fu_device_get_pid(phys_device)); if (fu_device_get_vid(phys_device) == helper->vid && fu_device_get_pid(phys_device) == helper->pid) helper->device = fu_redfish_network_device_new(object_path); } /* assume success */ return TRUE; } static gboolean fu_redfish_network_device_match(FuRedfishNetworkMatchHelper *helper, GError **error) { g_autoptr(GDBusProxy) proxy = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(GVariant) devices = NULL; g_auto(GStrv) paths = NULL; /* get devices */ proxy = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES | G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS, NULL, NETWORK_MANAGER_SERVICE_NAME, NETWORK_MANAGER_PATH, NETWORK_MANAGER_INTERFACE, NULL, &error_local); if (proxy == NULL) { if (g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_DBUS_ERROR) || g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY) || g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "D-Bus is not running"); return FALSE; } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to construct proxy for %s: ", NETWORK_MANAGER_SERVICE_NAME); return FALSE; } devices = g_dbus_proxy_call_sync(proxy, "GetDevices", NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error_local); if (devices == NULL) { if (g_error_matches(error_local, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN) || g_error_matches(error_local, G_DBUS_ERROR, G_DBUS_ERROR_NAME_HAS_NO_OWNER)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "NetworkManager is not running"); return FALSE; } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to call GetDevices() on %s: ", g_dbus_proxy_get_name(proxy)); return FALSE; } /* look at each device */ g_variant_get(devices, "(^ao)", &paths); for (guint i = 0; paths[i] != NULL; i++) { g_autoptr(GError) error_loop = NULL; g_debug("device %u: %s", i, paths[i]); if (!fu_redfish_network_device_match_device(helper, paths[i], &error_loop)) { if (g_error_matches(error_loop, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_debug("ignoring: %s", error_loop->message); continue; } g_propagate_error(error, g_steal_pointer(&error_loop)); return FALSE; } if (helper->device != NULL) break; } if (helper->device == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "could not find device"); return FALSE; } return TRUE; } FuRedfishNetworkDevice * fu_redfish_network_device_for_mac_addr(FuContext *ctx, const gchar *mac_addr, GError **error) { FuRedfishNetworkMatchHelper helper = { .ctx = ctx, .mac_addr = mac_addr, }; if (!fu_redfish_network_device_match(&helper, error)) { g_prefix_error(error, "missing %s: ", mac_addr); return NULL; } return helper.device; } FuRedfishNetworkDevice * fu_redfish_network_device_for_vid_pid(FuContext *ctx, guint16 vid, guint16 pid, GError **error) { FuRedfishNetworkMatchHelper helper = { .ctx = ctx, .vid = vid, .pid = pid, }; if (!fu_redfish_network_device_match(&helper, error)) { g_prefix_error(error, "missing 0x%04x:0x%04x: ", vid, pid); return NULL; } return helper.device; } fwupd-2.0.10/plugins/redfish/fu-redfish-network.h000066400000000000000000000015451501337203100217200ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-redfish-network-device.h" #define NETWORK_MANAGER_SERVICE_NAME "org.freedesktop.NetworkManager" #define NETWORK_MANAGER_INTERFACE "org.freedesktop.NetworkManager" #define NETWORK_MANAGER_INTERFACE_IP4_CONFIG "org.freedesktop.NetworkManager.IP4Config" #define NETWORK_MANAGER_INTERFACE_DEVICE "org.freedesktop.NetworkManager.Device" #define NETWORK_MANAGER_PATH "/org/freedesktop/NetworkManager" FuRedfishNetworkDevice * fu_redfish_network_device_for_mac_addr(FuContext *ctx, const gchar *mac_addr, GError **error) G_GNUC_NON_NULL(1, 2); FuRedfishNetworkDevice * fu_redfish_network_device_for_vid_pid(FuContext *ctx, guint16 vid, guint16 pid, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/plugins/redfish/fu-redfish-plugin.c000066400000000000000000000617221501337203100215230ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #ifdef HAVE_LINUX_IPMI_H #include "fu-ipmi-device.h" #endif #include "fu-ipmi-device.h" #include "fu-redfish-backend.h" #include "fu-redfish-common.h" #include "fu-redfish-device.h" #include "fu-redfish-hpe-device.h" #include "fu-redfish-legacy-device.h" #include "fu-redfish-multipart-device.h" #include "fu-redfish-network.h" #include "fu-redfish-plugin.h" #include "fu-redfish-smbios.h" #include "fu-redfish-smc-device.h" #include "fu-redfish-struct.h" #define FU_REDFISH_PLUGIN_CLEANUP_RETRIES_DELAY 10 /* seconds */ struct _FuRedfishPlugin { FuPlugin parent_instance; FuRedfishBackend *backend; FuRedfishSmbios *smbios; /* nullable */ }; G_DEFINE_TYPE(FuRedfishPlugin, fu_redfish_plugin, FU_TYPE_PLUGIN) static void fu_redfish_plugin_to_string(FuPlugin *plugin, guint idt, GString *str) { FuRedfishPlugin *self = FU_REDFISH_PLUGIN(plugin); fu_backend_add_string(FU_BACKEND(self->backend), idt, str); if (self->smbios != NULL) { g_autofree gchar *smbios = fu_firmware_to_string(FU_FIRMWARE(self->smbios)); fwupd_codec_string_append(str, idt, "Smbios", smbios); } fwupd_codec_string_append(str, idt, "Vendor", fu_redfish_backend_get_vendor(self->backend)); fwupd_codec_string_append(str, idt, "Version", fu_redfish_backend_get_version(self->backend)); fwupd_codec_string_append(str, idt, "UUID", fu_redfish_backend_get_uuid(self->backend)); } static gchar * fu_redfish_plugin_generate_password(guint length) { GString *str = g_string_sized_new(length); /* get a random password string */ while (str->len < length) { gchar tmp = (gchar)g_random_int_range(0x0, 0xff); /* nocheck:blocked */ if (g_ascii_isalnum(tmp)) g_string_append_c(str, tmp); } return g_string_free(str, FALSE); } static gboolean fu_redfish_plugin_change_expired(FuPlugin *plugin, GError **error) { FuRedfishPlugin *self = FU_REDFISH_PLUGIN(plugin); g_autofree gchar *password_new = fu_redfish_plugin_generate_password(15); g_autofree gchar *uri = NULL; g_autoptr(FuRedfishRequest) request = NULL; g_autoptr(JsonBuilder) builder = json_builder_new(); /* select correct, falling back to default for old fwupd versions */ uri = fu_plugin_get_config_value(plugin, "UserUri"); if (uri == NULL) { uri = g_strdup("/redfish/v1/AccountService/Accounts/2"); if (!fu_plugin_set_config_value(plugin, "UserUri", uri, error)) return FALSE; } /* now use Redfish to change the temporary password to the actual password */ request = fu_redfish_backend_request_new(self->backend); json_builder_begin_object(builder); json_builder_set_member_name(builder, "Password"); json_builder_add_string_value(builder, password_new); json_builder_end_object(builder); if (!fu_redfish_request_perform_full(request, uri, "PATCH", builder, FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON | FU_REDFISH_REQUEST_PERFORM_FLAG_USE_ETAG, error)) return FALSE; fu_redfish_backend_set_password(self->backend, password_new); /* success */ return fu_plugin_set_config_value(plugin, "Password", password_new, error); } static gboolean fu_redfish_plugin_coldplug(FuPlugin *plugin, FuProgress *progress, GError **error) { FuRedfishPlugin *self = FU_REDFISH_PLUGIN(plugin); g_autoptr(GPtrArray) devices = NULL; g_autoptr(GError) error_local = NULL; /* get the list of devices */ if (!fu_backend_coldplug(FU_BACKEND(self->backend), progress, &error_local)) { /* did the user password expire? */ if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_AUTH_EXPIRED)) { if (!fu_redfish_plugin_change_expired(plugin, error)) return FALSE; if (!fu_backend_coldplug(FU_BACKEND(self->backend), progress, error)) { fu_plugin_add_flag(plugin, FWUPD_PLUGIN_FLAG_AUTH_REQUIRED); return FALSE; } } else { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } devices = fu_backend_get_devices(FU_BACKEND(self->backend)); for (guint i = 0; i < devices->len; i++) { FuDevice *device = g_ptr_array_index(devices, i); if (fu_context_has_hwid_flag(fu_plugin_get_context(plugin), "reset-required")) fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT); fu_plugin_device_add(plugin, device); } /* this is no longer relevant */ if (devices->len > 0) { fu_plugin_add_rule(plugin, FU_PLUGIN_RULE_CONFLICTS, "bios"); fu_plugin_add_rule(plugin, FU_PLUGIN_RULE_CONFLICTS, "uefi_capsule"); } return TRUE; } void fu_redfish_plugin_set_credentials(FuPlugin *plugin, const gchar *username, const gchar *password) { FuRedfishPlugin *self = FU_REDFISH_PLUGIN(plugin); fu_redfish_backend_set_username(self->backend, username); fu_redfish_backend_set_password(self->backend, password); } gboolean fu_redfish_plugin_reload(FuPlugin *plugin, FuProgress *progress, GError **error) { FuRedfishPlugin *self = FU_REDFISH_PLUGIN(plugin); fu_backend_invalidate(FU_BACKEND(self->backend)); return fu_backend_setup(FU_BACKEND(self->backend), FU_BACKEND_SETUP_FLAG_NONE, progress, error); } static gboolean fu_redfish_plugin_discover_uefi_credentials(FuPlugin *plugin, GError **error) { FuContext *ctx = fu_plugin_get_context(plugin); FuEfivars *efivars = fu_context_get_efivars(ctx); FuRedfishPlugin *self = FU_REDFISH_PLUGIN(plugin); gsize bufsz = 0; guint32 indications = 0x0; g_autofree gchar *userpass_safe = NULL; g_autofree guint8 *buf = NULL; g_auto(GStrv) split = NULL; g_autoptr(GBytes) userpass = NULL; /* get the uint32 specifying if there are EFI variables set */ if (!fu_efivars_get_data(efivars, REDFISH_EFI_INFORMATION_GUID, REDFISH_EFI_INFORMATION_INDICATIONS, &buf, &bufsz, NULL, error)) return FALSE; if (!fu_memread_uint32_safe(buf, bufsz, 0x0, &indications, G_LITTLE_ENDIAN, error)) return FALSE; if ((indications & REDFISH_EFI_INDICATIONS_OS_CREDENTIALS) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no indications for OS credentials"); return FALSE; } /* read the correct EFI var for runtime */ userpass = fu_efivars_get_data_bytes(efivars, REDFISH_EFI_INFORMATION_GUID, REDFISH_EFI_INFORMATION_OS_CREDENTIALS, NULL, error); if (userpass == NULL) return FALSE; /* it might not be NUL terminated */ userpass_safe = g_strndup(g_bytes_get_data(userpass, NULL), g_bytes_get_size(userpass)); split = g_strsplit(userpass_safe, ":", -1); if (g_strv_length(split) != 2) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid format for username:password, got '%s'", userpass_safe); return FALSE; } fu_redfish_backend_set_username(self->backend, split[0]); fu_redfish_backend_set_password(self->backend, split[1]); return TRUE; } static gboolean fu_redfish_plugin_discover_smbios_table(FuPlugin *plugin, GError **error) { FuRedfishPlugin *self = FU_REDFISH_PLUGIN(plugin); FuContext *ctx = fu_plugin_get_context(plugin); const gchar *smbios_data_fn; g_autoptr(GPtrArray) type42_tables = NULL; /* in self tests */ smbios_data_fn = g_getenv("FWUPD_REDFISH_SMBIOS_DATA"); if (smbios_data_fn != NULL) { g_autoptr(FuRedfishSmbios) smbios = fu_redfish_smbios_new(); if (!fu_firmware_build_from_filename(FU_FIRMWARE(smbios), smbios_data_fn, error)) { g_prefix_error(error, "failed to build SMBIOS entry type 42: "); return FALSE; } g_set_object(&self->smbios, smbios); return TRUE; } /* is optional */ type42_tables = fu_context_get_smbios_data(ctx, REDFISH_SMBIOS_TABLE_TYPE, FU_SMBIOS_STRUCTURE_LENGTH_ANY, NULL); if (type42_tables == NULL) return TRUE; for (guint i = 0; i < type42_tables->len; i++) { GBytes *type42_blob = g_ptr_array_index(type42_tables, i); g_autoptr(FuRedfishSmbios) smbios = fu_redfish_smbios_new(); if (!fu_firmware_parse_bytes(FU_FIRMWARE(smbios), type42_blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error)) { g_prefix_error(error, "failed to parse SMBIOS entry type 42: "); return FALSE; } if (fu_redfish_smbios_get_interface_type(smbios) == FU_REDFISH_SMBIOS_INTERFACE_TYPE_NETWORK) { g_set_object(&self->smbios, smbios); return TRUE; } } /* success */ return TRUE; } static gboolean fu_redfish_plugin_autoconnect_network_device(FuRedfishPlugin *self, GError **error) { FuContext *ctx = fu_plugin_get_context(FU_PLUGIN(self)); g_autofree gchar *hostname = NULL; g_autoptr(FuRedfishNetworkDevice) device = NULL; /* we have no data */ if (self->smbios == NULL) return TRUE; /* get IP, falling back to hostname, then MAC, then VID:PID */ hostname = g_strdup(fu_redfish_smbios_get_ip_addr(self->smbios)); if (hostname == NULL) hostname = g_strdup(fu_redfish_smbios_get_hostname(self->smbios)); if (device == NULL) { const gchar *mac_addr = fu_redfish_smbios_get_mac_addr(self->smbios); if (mac_addr != NULL) { g_autoptr(GError) error_network = NULL; device = fu_redfish_network_device_for_mac_addr(ctx, mac_addr, &error_network); if (device == NULL) g_debug("failed to get device: %s", error_network->message); } } if (device == NULL) { guint16 vid = fu_redfish_smbios_get_vid(self->smbios); guint16 pid = fu_redfish_smbios_get_pid(self->smbios); if (vid != 0x0 && pid != 0x0) { g_autoptr(GError) error_network = NULL; device = fu_redfish_network_device_for_vid_pid(ctx, vid, pid, &error_network); if (device == NULL) g_debug("failed to get device: %s", error_network->message); } } /* autoconnect device if required */ if (device != NULL) { FuRedfishNetworkDeviceState state = FU_REDFISH_NETWORK_DEVICE_STATE_UNKNOWN; if (!fu_redfish_network_device_get_state(device, &state, error)) return FALSE; g_info("device state is now %s [%u]", fu_redfish_network_device_state_to_string(state), state); if (state == FU_REDFISH_NETWORK_DEVICE_STATE_DISCONNECTED) { if (!fu_redfish_network_device_connect(device, error)) return FALSE; } if (hostname == NULL) { hostname = fu_redfish_network_device_get_address(device, error); if (hostname == NULL) return FALSE; } } if (hostname == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no hostname"); return FALSE; } fu_redfish_backend_set_hostname(self->backend, hostname); fu_redfish_backend_set_port(self->backend, fu_redfish_smbios_get_port(self->smbios)); return TRUE; } #ifdef HAVE_LINUX_IPMI_H static gboolean fu_redfish_plugin_ipmi_create_user(FuPlugin *plugin, GError **error) { FuRedfishPlugin *self = FU_REDFISH_PLUGIN(plugin); const gchar *username_fwupd = "fwupd"; guint8 user_id = G_MAXUINT8; g_autofree gchar *password_new = fu_redfish_plugin_generate_password(15); g_autofree gchar *password_tmp = fu_redfish_plugin_generate_password(15); g_autofree gchar *uri = NULL; g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(FuIpmiDevice) device = fu_ipmi_device_new(fu_plugin_get_context(plugin)); g_autoptr(FuRedfishRequest) request = NULL; g_autoptr(JsonBuilder) builder = json_builder_new(); /* create device */ locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; /* check for existing user, and if not then remember the first spare slot */ for (guint8 i = 2; i < 0xFF; i++) { g_autofree gchar *username = fu_ipmi_device_get_user_password(device, i, NULL); if (username == NULL && user_id == G_MAXUINT8) { g_debug("KCS slot %u free", i); user_id = i; continue; } if (g_strcmp0(username, username_fwupd) == 0) { g_debug("%s user exists in KCS slot %u", username, i); user_id = i; break; } } if (user_id == G_MAXUINT8) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "all KCS slots full, cannot create user"); return FALSE; } /* create a user with appropriate permissions */ if (!fu_ipmi_device_set_user_name(device, user_id, username_fwupd, error)) return FALSE; if (!fu_ipmi_device_set_user_enable(device, user_id, TRUE, error)) return FALSE; if (!fu_ipmi_device_set_user_priv(device, user_id, 0x4, 1, error)) return FALSE; if (!fu_ipmi_device_set_user_password(device, user_id, password_tmp, error)) return FALSE; /* OEM specific for Advantech manufacture */ if (fu_context_has_hwid_guid(fu_plugin_get_context(plugin), "18789130-a714-53c0-b025-fa93801d3995")) { if (!fu_ipmi_device_set_user_group_redfish_enable_advantech(device, user_id, error)) return FALSE; } fu_redfish_backend_set_username(self->backend, username_fwupd); fu_redfish_backend_set_password(self->backend, password_tmp); /* wait for Redfish to sync */ g_usleep(2 * G_USEC_PER_SEC); /* XCC is the only BMC implementation that does not map the user_ids 1:1 */ if (fu_context_has_hwid_guid(fu_plugin_get_context(plugin), "42f00735-c9ab-5374-bd63-a5deee5881e0")) user_id -= 1; /* now use Redfish to change the temporary password to the actual password */ request = fu_redfish_backend_request_new(self->backend); uri = g_strdup_printf("/redfish/v1/AccountService/Accounts/%u", (guint)user_id); json_builder_begin_object(builder); json_builder_set_member_name(builder, "Password"); json_builder_add_string_value(builder, password_new); json_builder_end_object(builder); if (!fu_redfish_request_perform_full(request, uri, "PATCH", builder, FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON | FU_REDFISH_REQUEST_PERFORM_FLAG_USE_ETAG, error)) return FALSE; fu_redfish_backend_set_password(self->backend, password_new); /* success */ if (!fu_plugin_set_config_value(plugin, "UserUri", uri, error)) return FALSE; if (!fu_plugin_set_config_value(plugin, "Username", username_fwupd, error)) return FALSE; if (!fu_plugin_set_config_value(plugin, "Password", password_new, error)) return FALSE; return TRUE; } #endif static gboolean fu_redfish_plugin_startup(FuPlugin *plugin, FuProgress *progress, GError **error) { FuRedfishPlugin *self = FU_REDFISH_PLUGIN(plugin); #ifdef HAVE_LINUX_IPMI_H gboolean credentials_invalid = FALSE; #endif g_autofree gchar *password = NULL; g_autofree gchar *redfish_uri = NULL; g_autofree gchar *username = NULL; #ifdef HAVE_LINUX_IPMI_H g_autofree gchar *user_uri = NULL; #endif g_autoptr(GError) error_uefi = NULL; /* optional */ if (!fu_redfish_plugin_discover_smbios_table(plugin, error)) return FALSE; if (!fu_redfish_plugin_autoconnect_network_device(self, error)) return FALSE; if (!fu_redfish_plugin_discover_uefi_credentials(plugin, &error_uefi)) { g_debug("failed to get username and password automatically: %s", error_uefi->message); } /* override with the conf file */ redfish_uri = fu_plugin_get_config_value(plugin, "Uri"); if (redfish_uri != NULL) { const gchar *ip_str = NULL; g_auto(GStrv) split = NULL; guint64 port = 0; if (g_str_has_prefix(redfish_uri, "https://")) { fu_redfish_backend_set_https(self->backend, TRUE); ip_str = redfish_uri + strlen("https://"); port = 443; } else if (g_str_has_prefix(redfish_uri, "http://")) { fu_redfish_backend_set_https(self->backend, FALSE); ip_str = redfish_uri + strlen("http://"); port = 80; } else { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid scheme"); return FALSE; } split = g_strsplit(ip_str, ":", 2); fu_redfish_backend_set_hostname(self->backend, split[0]); if (g_strv_length(split) > 1) { if (!fu_strtoull(split[1], &port, 0, G_MAXUINT64, FU_INTEGER_BASE_10, error)) return FALSE; } if (port == 0 || port == G_MAXUINT64) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no valid port specified"); return FALSE; } fu_redfish_backend_set_port(self->backend, port); } username = fu_plugin_get_config_value(plugin, "Username"); if (username != NULL) fu_redfish_backend_set_username(self->backend, username); password = fu_plugin_get_config_value(plugin, "Password"); if (password != NULL) fu_redfish_backend_set_password(self->backend, password); fu_redfish_backend_set_cacheck(self->backend, fu_plugin_get_config_value_boolean(plugin, "CACheck")); if (fu_context_has_hwid_flag(fu_plugin_get_context(plugin), "wildcard-targets")) fu_redfish_backend_set_wildcard_targets(self->backend, TRUE); #ifdef HAVE_LINUX_IPMI_H /* test if the existing credentials work */ user_uri = fu_plugin_get_config_value(plugin, "UserUri"); if (username != NULL && password != NULL && user_uri != NULL) { g_autoptr(FuRedfishRequest) request = fu_redfish_backend_request_new(self->backend); g_autoptr(GError) error_local = NULL; if (!fu_redfish_request_perform(request, user_uri, FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_AUTH_FAILED)) { credentials_invalid = TRUE; } else { g_propagate_prefixed_error( error, g_steal_pointer(&error_local), "existing username and password did not work: "); return FALSE; } } } /* we got neither a type 42 entry or config value, lets try IPMI */ if (fu_redfish_backend_get_username(self->backend) == NULL || credentials_invalid) { if (!fu_context_has_hwid_flag(fu_plugin_get_context(plugin), "ipmi-create-user")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no username and password specified, " "and no vendor quirk for 'ipmi-create-user'"); return FALSE; } if (!fu_plugin_get_config_value_boolean(plugin, "IpmiDisableCreateUser")) { g_info("attempting to [re-]create user using IPMI"); if (!fu_redfish_plugin_ipmi_create_user(plugin, error)) return FALSE; } } #endif return fu_backend_setup(FU_BACKEND(self->backend), FU_BACKEND_SETUP_FLAG_NONE, progress, error); } static gboolean fu_redfish_plugin_cleanup_setup_cb(FuDevice *device, gpointer user_data, GError **error) { FuRedfishPlugin *self = FU_REDFISH_PLUGIN(user_data); FuProgress *progress = fu_progress_new(G_STRLOC); /* the network adaptor might not auto-connect when coming back */ if (!fu_redfish_plugin_autoconnect_network_device(self, error)) return FALSE; return fu_backend_setup(FU_BACKEND(self->backend), FU_BACKEND_SETUP_FLAG_NONE, progress, error); } static gboolean fu_redfish_plugin_cleanup_coldplug_cb(FuDevice *device, gpointer user_data, GError **error) { FuRedfishPlugin *self = FU_REDFISH_PLUGIN(user_data); FuProgress *progress = fu_progress_new(G_STRLOC); if (!fu_redfish_plugin_autoconnect_network_device(self, error)) return FALSE; return fu_redfish_plugin_coldplug(FU_PLUGIN(self), progress, error); } static gboolean fu_redfish_plugin_cleanup(FuPlugin *plugin, FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuRedfishPlugin *self = FU_REDFISH_PLUGIN(plugin); guint64 reset_timeout = 0; g_autofree gchar *restart_timeout_str = NULL; g_autoptr(FuRedfishRequest) request = fu_redfish_backend_request_new(self->backend); g_autoptr(GPtrArray) devices = NULL; /* nothing to do */ if (!fu_device_has_private_flag(device, FU_REDFISH_DEVICE_FLAG_MANAGER_RESET)) return TRUE; /* update failed; don't reboot BMC */ if (fu_device_get_update_state(device) == FWUPD_UPDATE_STATE_FAILED) return TRUE; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "manager-reboot"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 5, "pre-delay"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 67, "poll-manager"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 18, "post-delay"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 9, "recoldplug"); /* ask the BMC to reboot */ if (!fu_device_has_private_flag(device, FU_REDFISH_DEVICE_FLAG_NO_MANAGER_RESET_REQUEST)) { g_autoptr(JsonBuilder) builder = json_builder_new(); json_builder_begin_object(builder); json_builder_set_member_name(builder, "ResetType"); json_builder_add_string_value(builder, "ForceRestart"); json_builder_end_object(builder); if (!fu_redfish_request_perform_full(request, "/redfish/v1/Managers/1/Actions/Manager.Reset", "POST", builder, FU_REDFISH_REQUEST_PERFORM_FLAG_NONE, error)) { g_prefix_error(error, "failed to reset manager: "); return FALSE; } } fu_progress_step_done(progress); /* remove all the devices */ devices = fu_backend_get_devices(FU_BACKEND(self->backend)); for (guint i = 0; i < devices->len; i++) { FuDevice *device_tmp = g_ptr_array_index(devices, i); fu_backend_device_removed(FU_BACKEND(self->backend), device_tmp); } /* work around manager bugs... */ fu_backend_invalidate(FU_BACKEND(self->backend)); fu_device_sleep_full(device, fu_redfish_device_get_reset_pre_delay(FU_REDFISH_DEVICE(device)), fu_progress_get_child(progress)); fu_progress_step_done(progress); /* read the config file to work out how long to wait */ restart_timeout_str = fu_plugin_get_config_value(plugin, "ManagerResetTimeout"); if (!fu_strtoull(restart_timeout_str, &reset_timeout, 1, 86400, FU_INTEGER_BASE_AUTO, error)) return FALSE; /* wait for the BMC to come back */ if (!fu_device_retry_full(device, fu_redfish_plugin_cleanup_setup_cb, reset_timeout / FU_REDFISH_PLUGIN_CLEANUP_RETRIES_DELAY, FU_REDFISH_PLUGIN_CLEANUP_RETRIES_DELAY * 1000, self, error)) { g_prefix_error(error, "manager failed to come back from setup: "); return FALSE; } fu_progress_step_done(progress); /* work around manager bugs... */ fu_device_sleep_full(device, fu_redfish_device_get_reset_post_delay(FU_REDFISH_DEVICE(device)), fu_progress_get_child(progress)); fu_progress_step_done(progress); /* get the new list of devices */ if (!fu_device_retry_full(device, fu_redfish_plugin_cleanup_coldplug_cb, reset_timeout / FU_REDFISH_PLUGIN_CLEANUP_RETRIES_DELAY, FU_REDFISH_PLUGIN_CLEANUP_RETRIES_DELAY * 1000, self, error)) { g_prefix_error(error, "manager failed to come back from coldplug: "); return FALSE; } fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_redfish_plugin_modify_config(FuPlugin *plugin, const gchar *key, const gchar *value, GError **error) { const gchar *keys[] = {"CACheck", "IpmiDisableCreateUser", "ManagerResetTimeout", "Password", "Uri", "Username", "UserUri", NULL}; if (!g_strv_contains(keys, key)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "config key %s not supported", key); return FALSE; } return fu_plugin_set_config_value(plugin, key, value, error); } static void fu_redfish_plugin_init(FuRedfishPlugin *self) { } static void fu_redfish_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); FuRedfishPlugin *self = FU_REDFISH_PLUGIN(plugin); fu_context_add_quirk_key(ctx, "RedfishResetPreDelay"); fu_context_add_quirk_key(ctx, "RedfishResetPostDelay"); self->backend = fu_redfish_backend_new(ctx); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_REDFISH_SMBIOS); fu_plugin_add_flag(plugin, FWUPD_PLUGIN_FLAG_SECURE_CONFIG); fu_plugin_add_device_gtype(plugin, FU_TYPE_REDFISH_HPE_DEVICE); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_REDFISH_LEGACY_DEVICE); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_REDFISH_MULTIPART_DEVICE); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_IPMI_DEVICE); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_REDFISH_SMC_DEVICE); /* coverage */ /* defaults changed here will also be reflected in the fwupd.conf man page */ fu_plugin_set_config_default(plugin, "CACheck", "false"); fu_plugin_set_config_default(plugin, "IpmiDisableCreateUser", "false"); fu_plugin_set_config_default(plugin, "ManagerResetTimeout", "1800"); /* seconds */ fu_plugin_set_config_default(plugin, "Password", NULL); fu_plugin_set_config_default(plugin, "Uri", NULL); fu_plugin_set_config_default(plugin, "Username", NULL); fu_plugin_set_config_default(plugin, "UserUri", NULL); } static void fu_redfish_plugin_finalize(GObject *obj) { FuRedfishPlugin *self = FU_REDFISH_PLUGIN(obj); if (self->smbios != NULL) g_object_unref(self->smbios); if (self->backend != NULL) g_object_unref(self->backend); G_OBJECT_CLASS(fu_redfish_plugin_parent_class)->finalize(obj); } static void fu_redfish_plugin_class_init(FuRedfishPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_redfish_plugin_finalize; plugin_class->constructed = fu_redfish_plugin_constructed; plugin_class->to_string = fu_redfish_plugin_to_string; plugin_class->startup = fu_redfish_plugin_startup; plugin_class->coldplug = fu_redfish_plugin_coldplug; plugin_class->cleanup = fu_redfish_plugin_cleanup; plugin_class->modify_config = fu_redfish_plugin_modify_config; } fwupd-2.0.10/plugins/redfish/fu-redfish-plugin.h000066400000000000000000000006641501337203100215260ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuRedfishPlugin, fu_redfish_plugin, FU, REDFISH_PLUGIN, FuPlugin) void fu_redfish_plugin_set_credentials(FuPlugin *plugin, const gchar *username, const gchar *password); gboolean fu_redfish_plugin_reload(FuPlugin *plugin, FuProgress *progress, GError **error); fwupd-2.0.10/plugins/redfish/fu-redfish-request.c000066400000000000000000000237201501337203100217110ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-redfish-request.h" struct _FuRedfishRequest { GObject parent_instance; CURL *curl; CURLU *uri; GByteArray *buf; glong status_code; JsonParser *json_parser; JsonObject *json_obj; GHashTable *cache; /* nullable */ }; G_DEFINE_TYPE(FuRedfishRequest, fu_redfish_request, G_TYPE_OBJECT) typedef gchar curlptr; G_DEFINE_AUTOPTR_CLEANUP_FUNC(curlptr, curl_free) JsonObject * fu_redfish_request_get_json_object(FuRedfishRequest *self) { g_return_val_if_fail(FU_IS_REDFISH_REQUEST(self), NULL); return self->json_obj; } CURL * fu_redfish_request_get_curl(FuRedfishRequest *self) { g_return_val_if_fail(FU_IS_REDFISH_REQUEST(self), NULL); return self->curl; } CURLU * fu_redfish_request_get_uri(FuRedfishRequest *self) { g_return_val_if_fail(FU_IS_REDFISH_REQUEST(self), NULL); return self->uri; } glong fu_redfish_request_get_status_code(FuRedfishRequest *self) { g_return_val_if_fail(FU_IS_REDFISH_REQUEST(self), G_MAXLONG); return self->status_code; } static gboolean fu_redfish_request_load_json(FuRedfishRequest *self, GByteArray *buf, GError **error) { JsonNode *json_root; g_autoptr(GString) str = g_string_new(NULL); g_autoptr(JsonGenerator) json_generator = json_generator_new(); /* load */ if (buf->data == NULL || buf->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "there was no JSON payload"); return FALSE; } if (!json_parser_load_from_data(self->json_parser, (const gchar *)buf->data, (gssize)buf->len, error)) { return FALSE; } json_root = json_parser_get_root(self->json_parser); if (json_root == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no JSON root node"); return FALSE; } if (!JSON_NODE_HOLDS_OBJECT(json_root)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no JSON root object"); return FALSE; } self->json_obj = json_node_get_object(json_root); if (self->json_obj == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no JSON object"); return FALSE; } /* dump for humans */ json_generator_set_pretty(json_generator, TRUE); json_generator_set_root(json_generator, json_root); json_generator_to_gstring(json_generator, str); g_debug("response: %s", str->str); /* unauthorized */ if (json_object_has_member(self->json_obj, "error")) { FwupdError error_code = FWUPD_ERROR_INTERNAL; JsonObject *json_error; const gchar *id = NULL; const gchar *msg = "Unknown failure"; /* extended error present */ json_error = json_object_get_object_member(self->json_obj, "error"); if (json_object_has_member(json_error, "@Message.ExtendedInfo")) { JsonArray *json_error_array; json_error_array = json_object_get_array_member(json_error, "@Message.ExtendedInfo"); if (json_array_get_length(json_error_array) > 0) { JsonObject *json_error2; json_error2 = json_array_get_object_element(json_error_array, 0); if (json_object_has_member(json_error2, "MessageId")) id = json_object_get_string_member(json_error2, "MessageId"); if (json_object_has_member(json_error2, "Message")) msg = json_object_get_string_member(json_error2, "Message"); } } else { if (json_object_has_member(json_error, "code")) id = json_object_get_string_member(json_error, "code"); if (json_object_has_member(json_error, "message")) msg = json_object_get_string_member(json_error, "message"); } if (g_strcmp0(id, "Base.1.8.AccessDenied") == 0) error_code = FWUPD_ERROR_AUTH_FAILED; else if (g_strcmp0(id, "Base.1.8.PasswordChangeRequired") == 0) error_code = FWUPD_ERROR_AUTH_EXPIRED; else if (g_strcmp0(id, "SMC.1.0.OemLicenseNotPassed") == 0) error_code = FWUPD_ERROR_NOT_SUPPORTED; else if (g_strcmp0(id, "SMC.1.0.OemFirmwareAlreadyInUpdateMode") == 0 || g_strcmp0(id, "SMC.1.0.OemBiosUpdateIsInProgress") == 0) error_code = FWUPD_ERROR_ALREADY_PENDING; g_set_error_literal(error, FWUPD_ERROR, error_code, msg); return FALSE; } /* success */ return TRUE; } gboolean fu_redfish_request_perform(FuRedfishRequest *self, const gchar *path, FuRedfishRequestPerformFlags flags, GError **error) { CURLcode res; g_autofree gchar *str = NULL; g_autoptr(curlptr) uri_str = NULL; g_return_val_if_fail(FU_IS_REDFISH_REQUEST(self), FALSE); g_return_val_if_fail(path != NULL, FALSE); g_return_val_if_fail(self->status_code == 0, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* already in cache? */ if (flags & FU_REDFISH_REQUEST_PERFORM_FLAG_USE_CACHE && self->cache != NULL) { GByteArray *buf = g_hash_table_lookup(self->cache, path); if (buf != NULL) { if (flags & FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON) return fu_redfish_request_load_json(self, buf, error); g_byte_array_unref(self->buf); self->buf = g_byte_array_ref(buf); return TRUE; } } /* do request */ (void)curl_url_set(self->uri, CURLUPART_PATH, path, 0); (void)curl_url_get(self->uri, CURLUPART_URL, &uri_str, 0); res = curl_easy_perform(self->curl); curl_easy_getinfo(self->curl, CURLINFO_RESPONSE_CODE, &self->status_code); str = g_strndup((const gchar *)self->buf->data, self->buf->len); g_debug("%s: %s [%li]", uri_str, str, self->status_code); /* check result */ if (res != CURLE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to request %s: %s", uri_str, curl_easy_strerror(res)); return FALSE; } /* invalid user */ if (fu_redfish_request_get_status_code(self) == 401) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_AUTH_FAILED, "authentication failed"); return FALSE; } /* load JSON */ if (flags & FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON && self->buf->len > 0) { if (!fu_redfish_request_load_json(self, self->buf, error)) { g_prefix_error(error, "failed to parse %s: ", uri_str); return FALSE; } } /* save to cache */ if (self->cache != NULL) g_hash_table_insert(self->cache, g_strdup(path), g_byte_array_ref(self->buf)); /* success */ return TRUE; } typedef struct curl_slist _curl_slist; G_DEFINE_AUTOPTR_CLEANUP_FUNC(_curl_slist, curl_slist_free_all) static void fu_redfish_request_reset(FuRedfishRequest *self) { self->status_code = 0; self->json_obj = NULL; g_byte_array_set_size(self->buf, 0); } gboolean fu_redfish_request_perform_full(FuRedfishRequest *self, const gchar *path, const gchar *request, JsonBuilder *builder, FuRedfishRequestPerformFlags flags, GError **error) { g_autofree gchar *etag_header = NULL; g_autoptr(_curl_slist) hs = NULL; g_autoptr(GString) str = g_string_new(NULL); g_autoptr(JsonGenerator) json_generator = json_generator_new(); g_autoptr(JsonNode) json_root = NULL; g_return_val_if_fail(FU_IS_REDFISH_REQUEST(self), FALSE); g_return_val_if_fail(path != NULL, FALSE); g_return_val_if_fail(request != NULL, FALSE); g_return_val_if_fail(builder != NULL, FALSE); /* get etag */ if (flags & FU_REDFISH_REQUEST_PERFORM_FLAG_USE_ETAG) { JsonObject *json_obj; if (!fu_redfish_request_perform(self, path, FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON, error)) { g_prefix_error(error, "failed to request etag: "); return FALSE; } json_obj = fu_redfish_request_get_json_object(self); if (json_object_has_member(json_obj, "@odata.etag")) { etag_header = g_strdup_printf("If-Match: %s", json_object_get_string_member(json_obj, "@odata.etag")); } /* allow us to re-use the request */ fu_redfish_request_reset(self); } /* export as a string */ json_root = json_builder_get_root(builder); json_generator_set_pretty(json_generator, TRUE); json_generator_set_root(json_generator, json_root); json_generator_to_gstring(json_generator, str); g_debug("request to %s: %s", path, str->str); /* patch */ (void)curl_easy_setopt(self->curl, CURLOPT_CUSTOMREQUEST, request); (void)curl_easy_setopt(self->curl, CURLOPT_POSTFIELDS, str->str); (void)curl_easy_setopt(self->curl, CURLOPT_POSTFIELDSIZE, (long)str->len); hs = curl_slist_append(hs, "Content-Type: application/json"); if (etag_header != NULL) hs = curl_slist_append(hs, etag_header); (void)curl_easy_setopt(self->curl, CURLOPT_HTTPHEADER, hs); return fu_redfish_request_perform(self, path, flags, error); } static size_t fu_redfish_request_write_cb(char *ptr, size_t size, size_t nmemb, void *userdata) { GByteArray *buf = (GByteArray *)userdata; gsize realsize = size * nmemb; g_byte_array_append(buf, (const guint8 *)ptr, realsize); return realsize; } void fu_redfish_request_set_cache(FuRedfishRequest *self, GHashTable *cache) { g_return_if_fail(FU_IS_REDFISH_REQUEST(self)); g_return_if_fail(cache != NULL); g_return_if_fail(self->cache == NULL); self->cache = g_hash_table_ref(cache); } void fu_redfish_request_set_curlsh(FuRedfishRequest *self, CURLSH *curlsh) { g_return_if_fail(FU_IS_REDFISH_REQUEST(self)); g_return_if_fail(curlsh != NULL); (void)curl_easy_setopt(self->curl, CURLOPT_SHARE, curlsh); } static void fu_redfish_request_init(FuRedfishRequest *self) { self->curl = curl_easy_init(); self->uri = curl_url(); self->buf = g_byte_array_new(); self->json_parser = json_parser_new(); (void)curl_easy_setopt(self->curl, CURLOPT_WRITEFUNCTION, fu_redfish_request_write_cb); (void)curl_easy_setopt(self->curl, CURLOPT_WRITEDATA, self->buf); } static void fu_redfish_request_finalize(GObject *object) { FuRedfishRequest *self = FU_REDFISH_REQUEST(object); if (self->cache != NULL) g_hash_table_unref(self->cache); g_object_unref(self->json_parser); g_byte_array_unref(self->buf); curl_easy_cleanup(self->curl); curl_url_cleanup(self->uri); G_OBJECT_CLASS(fu_redfish_request_parent_class)->finalize(object); } static void fu_redfish_request_class_init(FuRedfishRequestClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_redfish_request_finalize; } fwupd-2.0.10/plugins/redfish/fu-redfish-request.h000066400000000000000000000024771501337203100217240ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include #define FU_TYPE_REDFISH_REQUEST (fu_redfish_request_get_type()) G_DECLARE_FINAL_TYPE(FuRedfishRequest, fu_redfish_request, FU, REDFISH_REQUEST, GObject) typedef enum { FU_REDFISH_REQUEST_PERFORM_FLAG_NONE = 0, FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON = 1 << 0, FU_REDFISH_REQUEST_PERFORM_FLAG_USE_CACHE = 1 << 1, FU_REDFISH_REQUEST_PERFORM_FLAG_USE_ETAG = 1 << 2, } FuRedfishRequestPerformFlags; gboolean fu_redfish_request_perform(FuRedfishRequest *self, const gchar *path, FuRedfishRequestPerformFlags flags, GError **error); gboolean fu_redfish_request_perform_full(FuRedfishRequest *self, const gchar *path, const gchar *request, JsonBuilder *builder, FuRedfishRequestPerformFlags flags, GError **error); JsonObject * fu_redfish_request_get_json_object(FuRedfishRequest *self); CURL * fu_redfish_request_get_curl(FuRedfishRequest *self); void fu_redfish_request_set_curlsh(FuRedfishRequest *self, CURLSH *curlsh); CURLU * fu_redfish_request_get_uri(FuRedfishRequest *self); glong fu_redfish_request_get_status_code(FuRedfishRequest *self); void fu_redfish_request_set_cache(FuRedfishRequest *self, GHashTable *cache); fwupd-2.0.10/plugins/redfish/fu-redfish-smbios.c000066400000000000000000000275261501337203100215250ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-redfish-common.h" #include "fu-redfish-smbios.h" #include "fu-redfish-struct.h" struct _FuRedfishSmbios { FuFirmwareClass parent_instance; FuRedfishSmbiosInterfaceType interface_type; guint16 port; gchar *hostname; gchar *mac_addr; gchar *ip_addr; guint16 vid; guint16 pid; }; G_DEFINE_TYPE(FuRedfishSmbios, fu_redfish_smbios, FU_TYPE_FIRMWARE) FuRedfishSmbiosInterfaceType fu_redfish_smbios_get_interface_type(FuRedfishSmbios *self) { return self->interface_type; } guint16 fu_redfish_smbios_get_port(FuRedfishSmbios *self) { return self->port; } guint16 fu_redfish_smbios_get_vid(FuRedfishSmbios *self) { return self->vid; } guint16 fu_redfish_smbios_get_pid(FuRedfishSmbios *self) { return self->pid; } const gchar * fu_redfish_smbios_get_hostname(FuRedfishSmbios *self) { return self->hostname; } const gchar * fu_redfish_smbios_get_mac_addr(FuRedfishSmbios *self) { return self->mac_addr; } const gchar * fu_redfish_smbios_get_ip_addr(FuRedfishSmbios *self) { return self->ip_addr; } static void fu_redfish_smbios_set_hostname(FuRedfishSmbios *self, const gchar *hostname) { g_free(self->hostname); self->hostname = g_strdup(hostname); } static void fu_redfish_smbios_set_mac_addr(FuRedfishSmbios *self, const gchar *mac_addr) { g_free(self->mac_addr); self->mac_addr = g_strdup(mac_addr); } static void fu_redfish_smbios_set_ip_addr(FuRedfishSmbios *self, const gchar *ip_addr) { g_free(self->ip_addr); self->ip_addr = g_strdup(ip_addr); } static void fu_redfish_smbios_set_port(FuRedfishSmbios *self, guint16 port) { self->port = port; } static void fu_redfish_smbios_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuRedfishSmbios *self = FU_REDFISH_SMBIOS(firmware); fu_xmlb_builder_insert_kv(bn, "interface_type", fu_redfish_smbios_interface_type_to_string(self->interface_type)); fu_xmlb_builder_insert_kx(bn, "port", self->port); fu_xmlb_builder_insert_kv(bn, "hostname", self->hostname); fu_xmlb_builder_insert_kv(bn, "mac_addr", self->mac_addr); fu_xmlb_builder_insert_kv(bn, "ip_addr", self->ip_addr); fu_xmlb_builder_insert_kx(bn, "vid", self->vid); fu_xmlb_builder_insert_kx(bn, "pid", self->pid); } static gboolean fu_redfish_smbios_build(FuFirmware *firmware, XbNode *n, GError **error) { FuRedfishSmbios *self = FU_REDFISH_SMBIOS(firmware); const gchar *tmp; guint64 tmpu; /* optional properties */ tmpu = xb_node_query_text_as_uint(n, "port", NULL); if (tmpu != G_MAXUINT64) fu_redfish_smbios_set_port(self, (guint16)tmpu); tmpu = xb_node_query_text_as_uint(n, "vid", NULL); if (tmpu != G_MAXUINT64) self->vid = (guint16)tmpu; tmpu = xb_node_query_text_as_uint(n, "pid", NULL); if (tmpu != G_MAXUINT64) self->pid = (guint16)tmpu; tmp = xb_node_query_text(n, "hostname", NULL); if (tmp != NULL) fu_redfish_smbios_set_hostname(self, tmp); tmp = xb_node_query_text(n, "mac_addr", NULL); if (tmp != NULL) fu_redfish_smbios_set_mac_addr(self, tmp); tmp = xb_node_query_text(n, "ip_addr", NULL); if (tmp != NULL) fu_redfish_smbios_set_ip_addr(self, tmp); /* success */ return TRUE; } static gboolean fu_redfish_smbios_parse_interface_data(FuRedfishSmbios *self, GInputStream *stream, gsize offset, GError **error) { gsize offset_mac_addr = G_MAXSIZE; gsize offset_vid_pid = G_MAXSIZE; guint8 interface_type = 0x0; /* parse the data depending on the interface type */ if (!fu_input_stream_read_u8(stream, offset, &interface_type, error)) return FALSE; g_debug("interface_type: %s [0x%x]", fu_redfish_interface_type_to_string(interface_type), interface_type); offset++; switch (interface_type) { case FU_REDFISH_INTERFACE_TYPE_USB_NETWORK: case FU_REDFISH_INTERFACE_TYPE_PCI_NETWORK: offset_vid_pid = 0x00; break; case FU_REDFISH_INTERFACE_TYPE_USB_NETWORK_V2: offset_vid_pid = 0x01; offset_mac_addr = 0x06; break; case FU_REDFISH_INTERFACE_TYPE_PCI_NETWORK_V2: offset_vid_pid = 0x01; offset_mac_addr = 0x09; break; default: g_debug("unknown Network Interface"); break; } /* MAC address */ if (offset_mac_addr != G_MAXSIZE) { guint8 mac_addr[6] = {0x0}; g_autofree gchar *mac_addr_str = NULL; if (!fu_input_stream_read_safe(stream, mac_addr, sizeof(mac_addr), 0x0, /* dst */ offset + offset_mac_addr, /* src */ sizeof(mac_addr), error)) return FALSE; mac_addr_str = fu_redfish_common_buffer_to_mac(mac_addr); fu_redfish_smbios_set_mac_addr(self, mac_addr_str); } /* VID:PID */ if (offset_vid_pid != G_MAXSIZE) { if (!fu_input_stream_read_u16(stream, offset + offset_vid_pid, &self->vid, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_input_stream_read_u16(stream, offset + offset_vid_pid + 0x02, &self->pid, G_LITTLE_ENDIAN, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_redfish_smbios_parse_over_ip(FuRedfishSmbios *self, GInputStream *stream, gsize offset, GError **error) { guint8 hostname_length; guint8 service_ip_address_format; g_autoptr(GByteArray) st = NULL; /* port + IP address */ st = fu_struct_redfish_protocol_over_ip_parse_stream(stream, offset, error); if (st == NULL) return FALSE; fu_redfish_smbios_set_port(self, fu_struct_redfish_protocol_over_ip_get_service_ip_port(st)); service_ip_address_format = fu_struct_redfish_protocol_over_ip_get_service_ip_address_format(st); if (service_ip_address_format == FU_REDFISH_IP_ADDRESS_FORMAT_V4) { const guint8 *ip_address = fu_struct_redfish_protocol_over_ip_get_service_ip_address(st, NULL); g_autofree gchar *tmp = fu_redfish_common_buffer_to_ipv4(ip_address); fu_redfish_smbios_set_ip_addr(self, tmp); } else if (service_ip_address_format == FU_REDFISH_IP_ADDRESS_FORMAT_V6) { const guint8 *ip_address = fu_struct_redfish_protocol_over_ip_get_service_ip_address(st, NULL); g_autofree gchar *tmp = fu_redfish_common_buffer_to_ipv6(ip_address); fu_redfish_smbios_set_ip_addr(self, tmp); } else { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "address format is invalid"); return FALSE; } /* hostname */ hostname_length = fu_struct_redfish_protocol_over_ip_get_service_hostname_len(st); if (hostname_length > 0) { g_autofree gchar *hostname = g_malloc0(hostname_length + 1); if (!fu_input_stream_read_safe(stream, (guint8 *)hostname, hostname_length, 0x0, /* dst */ offset + st->len, /* seek */ hostname_length, error)) return FALSE; fu_redfish_smbios_set_hostname(self, hostname); } /* success */ return TRUE; } static gboolean fu_redfish_smbios_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuRedfishSmbios *self = FU_REDFISH_SMBIOS(firmware); gsize offset = 0; gsize streamsz = 0; g_autoptr(GByteArray) st = NULL; /* check size */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz < 0x09) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "SMBIOS entry too small: %" G_GSIZE_FORMAT, streamsz); return FALSE; } /* parse */ st = fu_struct_redfish_smbios_type42_parse_stream(stream, offset, error); if (st == NULL) return FALSE; /* check length */ if (fu_struct_redfish_smbios_type42_get_length(st) != streamsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "size of table 0x%x does not match binary 0x%x", fu_struct_redfish_smbios_type42_get_length(st), (guint)streamsz); return FALSE; } /* check length */ offset += FU_STRUCT_REDFISH_SMBIOS_TYPE42_SIZE; if (fu_struct_redfish_smbios_type42_get_data_length(st) > 0) { if (!fu_redfish_smbios_parse_interface_data(self, stream, offset, error)) return FALSE; } offset += fu_struct_redfish_smbios_type42_get_data_length(st); /* parse protocol records */ self->interface_type = fu_struct_redfish_smbios_type42_get_interface_type(st); if (self->interface_type == FU_REDFISH_SMBIOS_INTERFACE_TYPE_NETWORK) { guint8 protocol_rcds = 0; if (!fu_input_stream_read_u8(stream, offset, &protocol_rcds, error)) return FALSE; offset += 1; g_debug("protocol_rcds: %u", protocol_rcds); for (guint i = 0; i < protocol_rcds; i++) { guint8 protocol_id = 0; guint8 protocol_sz = 0; if (!fu_input_stream_read_u8(stream, offset, &protocol_id, error)) return FALSE; if (!fu_input_stream_read_u8(stream, offset + 0x1, &protocol_sz, error)) return FALSE; if (protocol_id == REDFISH_PROTOCOL_REDFISH_OVER_IP) { if (!fu_redfish_smbios_parse_over_ip(self, stream, offset + 0x2, error)) return FALSE; } else { g_debug("ignoring protocol ID 0x%02x", protocol_id); } offset += protocol_sz + 1; } } /* success */ return TRUE; } static GByteArray * fu_redfish_smbios_write(FuFirmware *firmware, GError **error) { FuRedfishSmbios *self = FU_REDFISH_SMBIOS(firmware); gsize hostname_sz = 0; g_autoptr(GByteArray) st = fu_struct_redfish_protocol_over_ip_new(); g_autoptr(GByteArray) buf = g_byte_array_new(); if (self->hostname != NULL) hostname_sz = strlen(self->hostname); fu_byte_array_append_uint8(buf, REDFISH_SMBIOS_TABLE_TYPE); fu_byte_array_append_uint8(buf, 0x6D + hostname_sz); /* length */ fu_byte_array_append_uint16(buf, 0x1234, G_LITTLE_ENDIAN); /* handle */ fu_byte_array_append_uint8(buf, FU_REDFISH_SMBIOS_INTERFACE_TYPE_NETWORK); fu_byte_array_append_uint8(buf, 0x09); /* iface datalen */ fu_byte_array_append_uint8(buf, FU_REDFISH_INTERFACE_TYPE_USB_NETWORK); /* iface */ fu_byte_array_append_uint16(buf, self->vid, G_LITTLE_ENDIAN); /* iface:VID */ fu_byte_array_append_uint16(buf, self->pid, G_LITTLE_ENDIAN); /* iface:PID */ fu_byte_array_append_uint8(buf, 0x02); /* iface:serialsz */ fu_byte_array_append_uint8(buf, 0x03); /* iType */ fu_byte_array_append_uint8(buf, 'S'); /* iface:serial */ fu_byte_array_append_uint8(buf, 'n'); /* iface:serial */ fu_byte_array_append_uint8(buf, 0x1); /* nr protocol rcds */ /* protocol record */ fu_byte_array_append_uint8(buf, REDFISH_PROTOCOL_REDFISH_OVER_IP); fu_byte_array_append_uint8(buf, st->len + hostname_sz); if (self->hostname != NULL) hostname_sz = strlen(self->hostname); fu_struct_redfish_protocol_over_ip_set_service_ip_port(st, self->port); fu_struct_redfish_protocol_over_ip_set_service_ip_address_format( st, FU_REDFISH_IP_ADDRESS_FORMAT_V4); fu_struct_redfish_protocol_over_ip_set_service_ip_assignment_type( st, FU_REDFISH_IP_ASSIGNMENT_TYPE_STATIC); fu_struct_redfish_protocol_over_ip_set_service_hostname_len(st, hostname_sz); g_byte_array_append(buf, st->data, st->len); if (hostname_sz > 0) g_byte_array_append(buf, (guint8 *)self->hostname, hostname_sz); return g_steal_pointer(&buf); } static void fu_redfish_smbios_finalize(GObject *object) { FuRedfishSmbios *self = FU_REDFISH_SMBIOS(object); g_free(self->hostname); g_free(self->mac_addr); g_free(self->ip_addr); G_OBJECT_CLASS(fu_redfish_smbios_parent_class)->finalize(object); } static void fu_redfish_smbios_init(FuRedfishSmbios *self) { } static void fu_redfish_smbios_class_init(FuRedfishSmbiosClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_redfish_smbios_finalize; firmware_class->parse = fu_redfish_smbios_parse; firmware_class->write = fu_redfish_smbios_write; firmware_class->build = fu_redfish_smbios_build; firmware_class->export = fu_redfish_smbios_export; } FuRedfishSmbios * fu_redfish_smbios_new(void) { return FU_REDFISH_SMBIOS(g_object_new(FU_TYPE_REDFISH_SMBIOS, NULL)); } fwupd-2.0.10/plugins/redfish/fu-redfish-smbios.h000066400000000000000000000015271501337203100215230ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-redfish-struct.h" #define FU_TYPE_REDFISH_SMBIOS (fu_redfish_smbios_get_type()) G_DECLARE_FINAL_TYPE(FuRedfishSmbios, fu_redfish_smbios, FU, REDFISH_SMBIOS, FuFirmware) FuRedfishSmbios * fu_redfish_smbios_new(void); FuRedfishSmbiosInterfaceType fu_redfish_smbios_get_interface_type(FuRedfishSmbios *self); guint16 fu_redfish_smbios_get_port(FuRedfishSmbios *self); guint16 fu_redfish_smbios_get_vid(FuRedfishSmbios *self); guint16 fu_redfish_smbios_get_pid(FuRedfishSmbios *self); const gchar * fu_redfish_smbios_get_hostname(FuRedfishSmbios *self); const gchar * fu_redfish_smbios_get_mac_addr(FuRedfishSmbios *self); const gchar * fu_redfish_smbios_get_ip_addr(FuRedfishSmbios *self); fwupd-2.0.10/plugins/redfish/fu-redfish-smc-device.c000066400000000000000000000205601501337203100222370ustar00rootroot00000000000000/* * Copyright 2022 Kai Michaelis * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-redfish-backend.h" #include "fu-redfish-common.h" #include "fu-redfish-device.h" #include "fu-redfish-smc-device.h" struct _FuRedfishSmcDevice { FuRedfishDevice parent_instance; }; G_DEFINE_TYPE(FuRedfishSmcDevice, fu_redfish_smc_device, FU_TYPE_REDFISH_DEVICE) G_DEFINE_AUTOPTR_CLEANUP_FUNC(curl_mime, curl_mime_free) static const gchar * fu_redfish_smc_device_get_task(JsonObject *json_obj) { JsonObject *tmp_obj; JsonArray *tmp_ary; const gchar *msgid; if (!json_object_has_member(json_obj, "Accepted")) return NULL; tmp_obj = json_object_get_object_member(json_obj, "Accepted"); if (tmp_obj == NULL || !json_object_has_member(tmp_obj, "@Message.ExtendedInfo")) return NULL; tmp_ary = json_object_get_array_member(tmp_obj, "@Message.ExtendedInfo"); if (tmp_ary == NULL || json_array_get_length(tmp_ary) != 1) return NULL; tmp_obj = json_array_get_object_element(tmp_ary, 0); if (tmp_obj == NULL) return NULL; msgid = json_object_get_string_member(tmp_obj, "MessageId"); if (g_strcmp0(msgid, "SMC.1.0.OemSimpleupdateAcceptedMessage") != 0) return NULL; tmp_ary = json_object_get_array_member(tmp_obj, "MessageArgs"); if (tmp_ary == NULL) return NULL; if (json_array_get_length(tmp_ary) != 1) return NULL; return json_array_get_string_element(tmp_ary, 0); } static GString * fu_redfish_smc_device_get_parameters(FuRedfishSmcDevice *self) { g_autoptr(GString) str = g_string_new(NULL); g_autoptr(JsonBuilder) builder = json_builder_new(); g_autoptr(JsonGenerator) json_generator = json_generator_new(); g_autoptr(JsonNode) json_root = NULL; /* create header */ /* https://supermicro.com/manuals/other/RedishRefGuide.pdf */ json_builder_begin_object(builder); json_builder_set_member_name(builder, "Targets"); json_builder_begin_array(builder); json_builder_add_string_value(builder, "/redfish/v1/Systems/1/Bios"); json_builder_end_array(builder); json_builder_set_member_name(builder, "@Redfish.OperationApplyTime"); json_builder_add_string_value(builder, "OnStartUpdateRequest"); json_builder_set_member_name(builder, "Oem"); json_builder_begin_object(builder); json_builder_set_member_name(builder, "Supermicro"); json_builder_begin_object(builder); json_builder_set_member_name(builder, "BIOS"); json_builder_begin_object(builder); json_builder_set_member_name(builder, "PreserveME"); json_builder_add_boolean_value(builder, TRUE); json_builder_set_member_name(builder, "PreserveNVRAM"); json_builder_add_boolean_value(builder, TRUE); json_builder_set_member_name(builder, "PreserveSMBIOS"); json_builder_add_boolean_value(builder, TRUE); json_builder_set_member_name(builder, "BackupBIOS"); json_builder_add_boolean_value(builder, FALSE); json_builder_end_object(builder); json_builder_end_object(builder); json_builder_end_object(builder); json_builder_end_object(builder); /* export as a string */ json_root = json_builder_get_root(builder); json_generator_set_pretty(json_generator, TRUE); json_generator_set_root(json_generator, json_root); json_generator_to_gstring(json_generator, str); return g_steal_pointer(&str); } static gboolean fu_redfish_smc_device_start_update(FuDevice *device, FuProgress *progress, GError **error) { FuRedfishBackend *backend = fu_redfish_device_get_backend(FU_REDFISH_DEVICE(device)); JsonObject *json_obj; CURL *curl; const gchar *location = NULL; g_autoptr(FuRedfishRequest) request = fu_redfish_backend_request_new(backend); g_autoptr(GError) error_local = NULL; curl = fu_redfish_request_get_curl(request); (void)curl_easy_setopt(curl, CURLOPT_POSTFIELDS, ""); if (!fu_redfish_request_perform( request, "/redfish/v1/UpdateService/Actions/UpdateService.StartUpdate", FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) fu_device_add_problem(device, FWUPD_DEVICE_PROBLEM_UPDATE_PENDING); g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } json_obj = fu_redfish_request_get_json_object(request); location = fu_redfish_smc_device_get_task(json_obj); if (location == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no task returned for %s", fu_redfish_backend_get_push_uri_path(backend)); return FALSE; } return fu_redfish_device_poll_task(FU_REDFISH_DEVICE(device), location, progress, error); } static gboolean fu_redfish_smc_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuRedfishSmcDevice *self = FU_REDFISH_SMC_DEVICE(device); FuRedfishBackend *backend = fu_redfish_device_get_backend(FU_REDFISH_DEVICE(self)); CURL *curl; JsonObject *json_obj; curl_mimepart *part; const gchar *location = NULL; g_autoptr(curl_mime) mime = NULL; g_autoptr(FuRedfishRequest) request = NULL; g_autoptr(GBytes) fw = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(GString) params = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 50, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 50, "apply"); /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* create the multipart for uploading the image request */ request = fu_redfish_backend_request_new(backend); curl = fu_redfish_request_get_curl(request); mime = curl_mime_init(curl); params = fu_redfish_smc_device_get_parameters(self); part = curl_mime_addpart(mime); curl_mime_name(part, "UpdateParameters"); (void)curl_mime_type(part, "application/json"); (void)curl_mime_data(part, params->str, CURL_ZERO_TERMINATED); g_debug("request: %s", params->str); part = curl_mime_addpart(mime); curl_mime_name(part, "UpdateFile"); (void)curl_mime_type(part, "application/octet-stream"); (void)curl_mime_filename(part, "firmware.bin"); (void)curl_mime_data(part, g_bytes_get_data(fw, NULL), g_bytes_get_size(fw)); (void)curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); if (!fu_redfish_request_perform(request, fu_redfish_backend_get_push_uri_path(backend), FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_ALREADY_PENDING)) fu_device_add_problem(device, FWUPD_DEVICE_PROBLEM_UPDATE_PENDING); g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } if (fu_redfish_request_get_status_code(request) != 202) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to upload: %li", fu_redfish_request_get_status_code(request)); return FALSE; } json_obj = fu_redfish_request_get_json_object(request); /* poll the verify task for progress */ location = fu_redfish_smc_device_get_task(json_obj); if (location == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no task returned for %s", fu_redfish_backend_get_push_uri_path(backend)); return FALSE; } if (!fu_redfish_device_poll_task(FU_REDFISH_DEVICE(self), location, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); if (!fu_redfish_smc_device_start_update(device, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); return TRUE; } static void fu_redfish_smc_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 98, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_redfish_smc_device_init(FuRedfishSmcDevice *self) { fu_device_set_summary(FU_DEVICE(self), "Redfish Supermicro device"); } static void fu_redfish_smc_device_class_init(FuRedfishSmcDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_redfish_smc_device_write_firmware; device_class->set_progress = fu_redfish_smc_device_set_progress; } fwupd-2.0.10/plugins/redfish/fu-redfish-smc-device.h000066400000000000000000000007561501337203100222510ustar00rootroot00000000000000/* * Copyright 2022 Kai Michaelis * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-redfish-backend.h" #include "fu-redfish-device.h" #define FU_TYPE_REDFISH_SMC_DEVICE (fu_redfish_smc_device_get_type()) G_DECLARE_FINAL_TYPE(FuRedfishSmcDevice, fu_redfish_smc_device, FU, REDFISH_SMC_DEVICE, FuRedfishDevice) struct _FuRedfishSmcDeviceClass { FuRedfishDeviceClass parent_class; }; fwupd-2.0.10/plugins/redfish/fu-redfish.rs000066400000000000000000000035151501337203100204250ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructRedfishProtocolOverIp { service_uuid: Guid, host_ip_assignment_type: u8, host_ip_address_format: u8, host_ip_address: [u8; 16], host_ip_mask: [u8; 16], service_ip_assignment_type: u8, service_ip_address_format: u8, service_ip_address: [u8; 16], service_ip_mask: [u8; 16], service_ip_port: u16le, service_ip_vlan_id: u32le, service_hostname_len: u8, // optional service_hostname goes here } #[derive(ToString)] enum FuRedfishInterfaceType { UsbNetwork = 0x02, PciNetwork = 0x03, UsbNetworkV2 = 0x04, PciNetworkV2 = 0x05, } enum FuRedfishIpAssignmentType { Static = 0x00, Dhcp = 0x02, AutoConfig = 0x03, HostSelect = 0x04, } enum FuRedfishIpAddressFormat { Unknown = 0x00, V4 = 0x01, V6 = 0x02, } #[repr(u8)] #[derive(ToString)] enum FuRedfishSmbiosInterfaceType { Unknown = 0x00, Kcs = 0x02, 8250Uart = 0x03, 16450Uart = 0x04, 16550Uart = 0x05, 16650Uart = 0x06, 16750Uart = 0x07, 16850Uart = 0x08, Mctp = 0x3F, Network = 0x40, Oem = 0xF0, } #[derive(ParseStream, Default)] #[repr(C, packed)] struct FuStructRedfishSmbiosType42 { type: u8 == 42, length: u8, handle: u16le, interface_type: FuRedfishSmbiosInterfaceType = Network, data_length: u8, // data: [u8; data_length], // protocol_cnt: u8, // protocol_records } #[derive(ToString)] enum FuRedfishNetworkDeviceState { Unknown = 0, Unmanaged = 10, Unavailable = 20, Disconnected = 30, Prepare = 40, Config = 50, NeedAuth = 60, IpConfig = 70, IpCheck = 80, Secondaries = 90, Activated = 100, Deactivating = 110, Failed = 120, } fwupd-2.0.10/plugins/redfish/fu-self-test.c000066400000000000000000000522001501337203100205000ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-context-private.h" #include "fu-device-private.h" #ifdef HAVE_LINUX_IPMI_H #include "fu-ipmi-device.h" #endif #include "fu-plugin-private.h" #include "fu-redfish-common.h" #include "fu-redfish-network.h" #include "fu-redfish-plugin.h" #include "fu-redfish-smc-device.h" #include "fu-redfish-struct.h" typedef struct { FuPlugin *plugin; FuPlugin *smc_plugin; FuPlugin *unlicensed_plugin; FuPlugin *hpe_plugin; FuPlugin *dell_plugin; } FuTest; static void fu_test_self_init(FuTest *self) { gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; ret = fu_context_load_quirks(ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE | FU_QUIRKS_LOAD_FLAG_NO_VERIFY, &error); g_assert_no_error(error); g_assert_true(ret); /* load the config file */ ret = fu_context_load_hwinfo(ctx, progress, FU_CONTEXT_HWID_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); /* generic BMC */ self->plugin = fu_plugin_new_from_gtype(fu_redfish_plugin_get_type(), ctx); ret = fu_plugin_runner_startup(self->plugin, progress, &error); if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE)) { g_test_skip("no redfish.py running"); g_clear_error(&error); } else { g_assert_no_error(error); g_assert_true(ret); ret = fu_plugin_runner_coldplug(self->plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); } /* supermicro BMC */ self->smc_plugin = fu_plugin_new_from_gtype(fu_redfish_plugin_get_type(), ctx); ret = fu_plugin_runner_startup(self->smc_plugin, progress, &error); if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE)) { g_test_skip("no redfish.py running"); g_clear_error(&error); } else { g_assert_no_error(error); g_assert_true(ret); fu_redfish_plugin_set_credentials(self->smc_plugin, "smc_username", "password2"); ret = fu_redfish_plugin_reload(self->smc_plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_plugin_runner_coldplug(self->smc_plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); } /* unlicensed supermicro BMC */ self->unlicensed_plugin = fu_plugin_new_from_gtype(fu_redfish_plugin_get_type(), ctx); ret = fu_plugin_runner_startup(self->unlicensed_plugin, progress, &error); if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE)) { g_test_skip("no redfish.py running"); g_clear_error(&error); } else { g_assert_no_error(error); g_assert_true(ret); fu_redfish_plugin_set_credentials(self->unlicensed_plugin, "unlicensed_username", "password2"); ret = fu_redfish_plugin_reload(self->unlicensed_plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_plugin_runner_coldplug(self->unlicensed_plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); } /* HPE BMC */ self->hpe_plugin = fu_plugin_new_from_gtype(fu_redfish_plugin_get_type(), ctx); ret = fu_plugin_runner_startup(self->hpe_plugin, progress, &error); if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE)) { g_test_skip("no redfish.py running"); g_clear_error(&error); } else { g_assert_no_error(error); g_assert_true(ret); fu_redfish_plugin_set_credentials(self->hpe_plugin, "hpe_username", "password2"); /* We just changed the credentials and need to resetup again to discover the vendor * Here we need to get a device to query the backend */ ret = fu_redfish_plugin_reload(self->hpe_plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_plugin_runner_coldplug(self->hpe_plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); } /* Dell BMC */ self->dell_plugin = fu_plugin_new_from_gtype(fu_redfish_plugin_get_type(), ctx); ret = fu_plugin_runner_startup(self->dell_plugin, progress, &error); if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE)) { g_test_skip("no redfish.py running"); } else { g_assert_no_error(error); g_assert_true(ret); fu_redfish_plugin_set_credentials(self->dell_plugin, "dell_username", "password2"); /* We just changed the credentials and need to resetup again to discover the vendor * Here we need to get a device to query the backend */ ret = fu_redfish_plugin_reload(self->dell_plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_plugin_runner_coldplug(self->dell_plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); } } static void fu_test_redfish_ipmi_func(void) { #ifdef HAVE_LINUX_IPMI_H gboolean ret; g_autoptr(FuIpmiDevice) device = fu_ipmi_device_new(NULL); g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GError) error = NULL; g_autofree gchar *username = NULL; g_autofree gchar *str = NULL; /* sanity check */ if (!g_file_test("/dev/ipmi0", G_FILE_TEST_EXISTS)) { g_test_skip("no IPMI hardware"); return; } /* create device */ locker = fu_device_locker_new(device, &error); if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_PERMISSION_DENIED)) { g_test_skip("permission denied for access to IPMI hardware"); return; } g_assert_no_error(error); g_assert_nonnull(locker); str = fu_device_to_string(FU_DEVICE(device)); g_debug("%s", str); /* add user that can do redfish commands */ if (g_getenv("FWUPD_REDFISH_SELF_TEST") == NULL) { g_test_skip("not doing destructive tests"); return; } ret = fu_ipmi_device_set_user_name(device, 0x04, "fwupd", &error); g_assert_no_error(error); g_assert_true(ret); username = fu_ipmi_device_get_user_password(device, 0x04, &error); g_assert_no_error(error); g_assert_nonnull(username); g_debug("username=%s", username); ret = fu_ipmi_device_set_user_enable(device, 0x04, TRUE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_ipmi_device_set_user_priv(device, 0x04, 0x4, 1, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_ipmi_device_set_user_password(device, 0x04, "Passw0rd123", &error); g_assert_no_error(error); g_assert_true(ret); #else g_test_skip("no linux/ipmi.h, so skipping"); #endif } static void fu_test_redfish_common_func(void) { const guint8 buf[16] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}; g_autofree gchar *ipv4 = NULL; g_autofree gchar *ipv6 = NULL; g_autofree gchar *maca = NULL; ipv4 = fu_redfish_common_buffer_to_ipv4(buf); g_assert_cmpstr(ipv4, ==, "0.1.2.3"); ipv6 = fu_redfish_common_buffer_to_ipv6(buf); g_assert_cmpstr(ipv6, ==, "00010203:04050607:08090a0b:0c0d0e0f"); maca = fu_redfish_common_buffer_to_mac(buf); g_assert_cmpstr(maca, ==, "00:01:02:03:04:05"); } static void fu_test_redfish_common_version_func(void) { struct { const gchar *in; const gchar *op; } strs[] = {{"1.2.3", "1.2.3"}, {"P50 v1.2.3 PROD", "1.2.3"}, {"P50 1.2.3 DEV", "1.2.3"}, {NULL, NULL}}; for (guint i = 0; strs[i].in != NULL; i++) { g_autofree gchar *tmp = fu_redfish_common_fix_version(strs[i].in); g_assert_cmpstr(tmp, ==, strs[i].op); } } static void fu_test_redfish_common_lenovo_func(void) { struct { const gchar *in; gboolean ret; const gchar *build; const gchar *version; } values[] = {{"11A-1.02", TRUE, "11A", "1.02"}, {"11A-0.00", TRUE, "11A", "0.00"}, {"99Z-9.99", TRUE, "99Z", "9.99"}, {"9-9-9.99", FALSE, NULL, NULL}, {"999-9.99", FALSE, NULL, NULL}, {"ACB-9.99", FALSE, NULL, NULL}, {NULL, FALSE, NULL, NULL}}; for (guint i = 0; values[i].in != NULL; i++) { gboolean ret; g_autofree gchar *build = NULL; g_autofree gchar *version = NULL; ret = fu_redfish_common_parse_version_lenovo(values[i].in, &build, &version, NULL); g_assert_cmpint(ret, ==, values[i].ret); g_assert_cmpstr(build, ==, values[i].build); g_assert_cmpstr(version, ==, values[i].version); } } static void fu_test_redfish_network_mac_addr_func(void) { FuRedfishNetworkDeviceState state = FU_REDFISH_NETWORK_DEVICE_STATE_UNKNOWN; gboolean ret; g_autofree gchar *ip_addr = NULL; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuRedfishNetworkDevice) device = NULL; g_autoptr(GError) error = NULL; device = fu_redfish_network_device_for_mac_addr(ctx, "00:13:F7:29:C2:D8", &error); if (device == NULL && g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_test_skip("no hardware"); return; } if (device == NULL && g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_autofree gchar *str = g_strdup_printf("not supported: %s", error->message); g_test_skip(str); return; } g_assert_no_error(error); g_assert_nonnull(device); ret = fu_redfish_network_device_get_state(device, &state, &error); g_assert_no_error(error); g_assert_true(ret); if (state == FU_REDFISH_NETWORK_DEVICE_STATE_DISCONNECTED) { ret = fu_redfish_network_device_connect(device, &error); g_assert_no_error(error); g_assert_true(ret); } ip_addr = fu_redfish_network_device_get_address(device, &error); g_assert_no_error(error); g_assert_nonnull(ip_addr); } static void fu_test_redfish_network_vid_pid_func(void) { g_autofree gchar *ip_addr = NULL; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuRedfishNetworkDevice) device = NULL; g_autoptr(GError) error = NULL; device = fu_redfish_network_device_for_vid_pid(ctx, 0x0707, 0x0201, &error); if (device == NULL && g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_test_skip("no hardware"); return; } if (device == NULL && g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_autofree gchar *str = g_strdup_printf("not supported: %s", error->message); g_test_skip(str); return; } g_assert_no_error(error); g_assert_nonnull(device); ip_addr = fu_redfish_network_device_get_address(device, &error); g_assert_no_error(error); g_assert_nonnull(ip_addr); } static void fu_test_redfish_devices_func(gconstpointer user_data) { FuDevice *dev; FuTest *self = (FuTest *)user_data; GPtrArray *devices; g_autofree gchar *devstr0 = NULL; g_autofree gchar *devstr1 = NULL; devices = fu_plugin_get_devices(self->plugin); g_assert_nonnull(devices); if (devices->len == 0) { g_test_skip("no redfish support"); return; } g_assert_cmpint(devices->len, ==, 2); /* BMC */ dev = g_ptr_array_index(devices, 1); devstr0 = fu_device_to_string(dev); g_debug("%s", devstr0); g_assert_cmpstr(fu_device_get_id(dev), ==, "62c1cd95692c5225826cf8568a460427ea3b1827"); g_assert_cmpstr(fu_device_get_name(dev), ==, "BMC Firmware"); g_assert_cmpstr(fu_device_get_vendor(dev), ==, "Lenovo"); g_assert_cmpstr(fu_device_get_version(dev), ==, "1.02"); g_assert_cmpstr(fu_device_get_version_lowest(dev), ==, "0.12"); g_assert_cmpint(fu_device_get_version_format(dev), ==, FWUPD_VERSION_FORMAT_PAIR); g_assert_cmpint(fu_device_get_version_build_date(dev), ==, 1552608000); g_assert_true(fu_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE)); g_assert_true(fu_device_has_protocol(dev, "org.dmtf.redfish")); g_assert_true( fu_device_has_guid(dev, "REDFISH\\VENDOR_Lenovo&SOFTWAREID_UEFI-AFE1-6&TYPE_UNSIGNED")); g_assert_true(fu_device_has_vendor_id(dev, "REDFISH:LENOVO")); g_assert_true(fu_device_has_guid(dev, "aa148d2e-6e09-453e-bc6f-63baa5f5ccc4")); g_assert_true(fu_device_has_guid(dev, "00000000-0000-0000-0000-000000000229")); g_assert_true(fu_device_has_guid(dev, "00000000-0000-0000-0000-000001413436")); /* BIOS */ dev = g_ptr_array_index(devices, 0); devstr1 = fu_device_to_string(dev); g_debug("%s", devstr1); g_assert_cmpstr(fu_device_get_id(dev), ==, "562313e34c756a05a2e878861377765582bbf971"); g_assert_cmpstr(fu_device_get_name(dev), ==, "BIOS Firmware"); g_assert_cmpstr(fu_device_get_vendor(dev), ==, "Contoso"); g_assert_cmpstr(fu_device_get_version(dev), ==, "1.45"); g_assert_cmpstr(fu_device_get_serial(dev), ==, "12345"); g_assert_cmpstr(fu_device_get_version_lowest(dev), ==, "1.10"); g_assert_cmpint(fu_device_get_version_format(dev), ==, FWUPD_VERSION_FORMAT_PAIR); g_assert_cmpint(fu_device_get_version_build_date(dev), ==, 1552608000); g_assert_true(fu_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE)); g_assert_true(fu_device_has_icon(dev, FU_DEVICE_ICON_NETWORK_WIRED)); g_assert_true(fu_device_has_protocol(dev, "org.dmtf.redfish")); g_assert_true(fu_device_has_guid(dev, "fee82a67-6ce2-4625-9f44-237ad2402c28")); g_assert_true(fu_device_has_guid(dev, "a6d3294e-37e5-50aa-ae2f-c0c457af16f3")); g_assert_true(fu_device_has_vendor_id(dev, "REDFISH:CONTOSO")); } static void fu_test_redfish_unlicensed_devices_func(gconstpointer user_data) { FuDevice *dev; FuTest *self = (FuTest *)user_data; GPtrArray *devices; devices = fu_plugin_get_devices(self->unlicensed_plugin); g_assert_nonnull(devices); if (devices->len == 0) { g_test_skip("no redfish support"); return; } g_assert_cmpint(devices->len, ==, 2); dev = g_ptr_array_index(devices, 0); g_assert_true(FU_IS_REDFISH_SMC_DEVICE(dev)); g_assert_true(fu_device_has_inhibit( dev, fwupd_device_problem_to_string(FWUPD_DEVICE_PROBLEM_MISSING_LICENSE))); dev = g_ptr_array_index(devices, 1); g_assert_true(FU_IS_REDFISH_SMC_DEVICE(dev)); g_assert_true(fu_device_has_inhibit( dev, fwupd_device_problem_to_string(FWUPD_DEVICE_PROBLEM_MISSING_LICENSE))); } static void fu_test_redfish_smc_devices_func(gconstpointer user_data) { FuDevice *dev; FuTest *self = (FuTest *)user_data; GPtrArray *devices; devices = fu_plugin_get_devices(self->smc_plugin); g_assert_nonnull(devices); if (devices->len == 0) { g_test_skip("no redfish support"); return; } g_assert_cmpint(devices->len, ==, 2); dev = g_ptr_array_index(devices, 0); g_assert_true(FU_IS_REDFISH_SMC_DEVICE(dev)); dev = g_ptr_array_index(devices, 1); g_assert_true(FU_IS_REDFISH_SMC_DEVICE(dev)); } static void fu_test_redfish_dell_devices_func(gconstpointer user_data) { FuDevice *dev; FuTest *self = (FuTest *)user_data; GPtrArray *devices; devices = fu_plugin_get_devices(self->dell_plugin); g_assert_nonnull(devices); if (devices->len == 0) { g_test_skip("no redfish support"); return; } g_assert_cmpint(devices->len, ==, 2); dev = g_ptr_array_index(devices, 1); g_assert_true(FU_IS_REDFISH_DEVICE(dev)); g_assert_true( fu_device_has_guid(dev, "REDFISH\\VENDOR_Lenovo&SYSTEMID_0C60&SOFTWAREID_UEFI-AFE1-6")); } static void fu_test_redfish_hpe_update_func(gconstpointer user_data) { FuDevice *dev; FuTest *self = (FuTest *)user_data; GPtrArray *devices; gboolean ret; g_autoptr(GError) error = NULL; g_autoptr(GBytes) blob_fw = NULL; g_autoptr(FuFirmware) stream_fw = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); /* progress */ fu_progress_add_flag(progress, FU_PROGRESS_FLAG_NO_PROFILE); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 50, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 50, NULL); devices = fu_plugin_get_devices(self->hpe_plugin); g_assert_nonnull(devices); if (devices->len == 0) { g_test_skip("no redfish support"); return; } g_assert_cmpint(devices->len, ==, 2); /* BMC */ dev = g_ptr_array_index(devices, 0); blob_fw = g_bytes_new_static("hello", 5); stream_fw = fu_firmware_new_from_bytes(blob_fw); ret = fu_plugin_runner_write_firmware(self->hpe_plugin, dev, stream_fw, fu_progress_get_child(progress), FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_test_redfish_update_func(gconstpointer user_data) { FuDevice *dev; FuTest *self = (FuTest *)user_data; GPtrArray *devices; gboolean ret; g_autoptr(GError) error = NULL; g_autoptr(GBytes) blob_fw = NULL; g_autoptr(FuFirmware) firmware = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); /* progress */ fu_progress_add_flag(progress, FU_PROGRESS_FLAG_NO_PROFILE); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 50, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 50, NULL); devices = fu_plugin_get_devices(self->plugin); g_assert_nonnull(devices); if (devices->len == 0) { g_test_skip("no redfish support"); return; } g_assert_cmpint(devices->len, ==, 2); /* BMC */ dev = g_ptr_array_index(devices, 1); blob_fw = g_bytes_new_static("hello", 5); firmware = fu_firmware_new_from_bytes(blob_fw); ret = fu_plugin_runner_write_firmware(self->plugin, dev, firmware, fu_progress_get_child(progress), FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_true(fu_device_has_flag(dev, FWUPD_DEVICE_FLAG_NEEDS_REBOOT)); fu_progress_step_done(progress); /* try again */ ret = fu_plugin_runner_write_firmware(self->plugin, dev, firmware, fu_progress_get_child(progress), FWUPD_INSTALL_FLAG_NONE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE); g_assert_false(ret); } static void fu_test_redfish_smc_update_func(gconstpointer user_data) { FuDevice *dev; FuTest *self = (FuTest *)user_data; GPtrArray *devices; gboolean ret; g_autoptr(GBytes) blob_fw1 = NULL; g_autoptr(GBytes) blob_fw2 = NULL; g_autoptr(GError) error = NULL; g_autoptr(FuFirmware) firmware1 = NULL; g_autoptr(FuFirmware) firmware2 = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); /* progress */ fu_progress_add_flag(progress, FU_PROGRESS_FLAG_NO_PROFILE); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 50, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 50, NULL); devices = fu_plugin_get_devices(self->smc_plugin); g_assert_nonnull(devices); if (devices->len == 0) { g_test_skip("no redfish support"); return; } g_assert_cmpint(devices->len, ==, 2); /* BMC */ dev = g_ptr_array_index(devices, 1); blob_fw1 = g_bytes_new_static("hello", 5); firmware1 = fu_firmware_new_from_bytes(blob_fw1); ret = fu_plugin_runner_write_firmware(self->plugin, dev, firmware1, fu_progress_get_child(progress), FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); fu_progress_step_done(progress); /* stuck update */ blob_fw2 = g_bytes_new_static("stuck", 5); firmware2 = fu_firmware_new_from_bytes(blob_fw2); ret = fu_plugin_runner_write_firmware(self->plugin, dev, firmware2, fu_progress_get_child(progress), FWUPD_INSTALL_FLAG_NONE, &error); g_assert_false(ret); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_ALREADY_PENDING); g_assert_true(fu_device_has_inhibit( dev, fwupd_device_problem_to_string(FWUPD_DEVICE_PROBLEM_UPDATE_PENDING))); } static void fu_test_self_free(FuTest *self) { if (self->plugin != NULL) g_object_unref(self->plugin); if (self->smc_plugin != NULL) g_object_unref(self->smc_plugin); if (self->unlicensed_plugin != NULL) g_object_unref(self->unlicensed_plugin); if (self->hpe_plugin != NULL) g_object_unref(self->hpe_plugin); if (self->dell_plugin != NULL) g_object_unref(self->dell_plugin); g_free(self); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuTest, fu_test_self_free) #pragma clang diagnostic pop int main(int argc, char **argv) { g_autoptr(FuTest) self = g_new0(FuTest, 1); g_autofree gchar *smbios_data_fn = NULL; g_autofree gchar *testdatadir = NULL; (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); (void)g_setenv("FWUPD_REDFISH_VERBOSE", "1", TRUE); testdatadir = g_test_build_filename(G_TEST_DIST, "tests", NULL); smbios_data_fn = g_build_filename(testdatadir, "redfish-smbios.builder.xml", NULL); (void)g_setenv("FWUPD_REDFISH_SMBIOS_DATA", smbios_data_fn, TRUE); (void)g_setenv("FWUPD_SYSFSFWDIR", testdatadir, TRUE); (void)g_setenv("CONFIGURATION_DIRECTORY", testdatadir, TRUE); (void)g_setenv("FWUPD_SYSFSFWATTRIBDIR", testdatadir, TRUE); g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); fu_test_self_init(self); g_test_add_func("/redfish/ipmi", fu_test_redfish_ipmi_func); g_test_add_func("/redfish/common", fu_test_redfish_common_func); g_test_add_func("/redfish/common{version}", fu_test_redfish_common_version_func); g_test_add_func("/redfish/common{lenovo}", fu_test_redfish_common_lenovo_func); g_test_add_func("/redfish/network{mac_addr}", fu_test_redfish_network_mac_addr_func); g_test_add_func("/redfish/network{vid_pid}", fu_test_redfish_network_vid_pid_func); g_test_add_data_func("/redfish/unlicensed_plugin{devices}", self, fu_test_redfish_unlicensed_devices_func); g_test_add_data_func("/redfish/smc_plugin{devices}", self, fu_test_redfish_smc_devices_func); g_test_add_data_func("/redfish/smc_plugin{update}", self, fu_test_redfish_smc_update_func); g_test_add_data_func("/redfish/hpe_plugin{update}", self, fu_test_redfish_hpe_update_func); g_test_add_data_func("/redfish/plugin{devices}", self, fu_test_redfish_devices_func); g_test_add_data_func("/redfish/dell_plugin{devices}", self, fu_test_redfish_dell_devices_func); g_test_add_data_func("/redfish/plugin{update}", self, fu_test_redfish_update_func); return g_test_run(); } fwupd-2.0.10/plugins/redfish/meson.build000066400000000000000000000047341501337203100201710ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginRedfish"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('redfish.quirk') ipmi_src = [] if cc.has_header('linux/ipmi.h') ipmi_src += 'fu-ipmi-device.c' endif plugin_builtin_redfish = static_library('fu_plugin_redfish', rustgen.process( 'fu-redfish.rs', # fuzzing ), sources: [ 'fu-redfish-plugin.c', 'fu-redfish-backend.c', 'fu-redfish-common.c', # fuzzing 'fu-redfish-device.c', 'fu-redfish-smc-device.c', 'fu-redfish-legacy-device.c', 'fu-redfish-hpe-device.c', 'fu-redfish-multipart-device.c', 'fu-redfish-network.c', 'fu-redfish-network-device.c', 'fu-redfish-request.c', 'fu-redfish-smbios.c', # fuzzing ipmi_src, ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: [ plugin_deps, libcurl, ], ) plugin_builtins += plugin_builtin_redfish if get_option('tests') install_data(['tests/redfish-smbios.builder.xml'], install_dir: join_paths(installed_test_datadir, 'tests')) install_data(['tests/fwupd.conf'], install_dir: join_paths(installed_test_datadir, 'tests'), install_mode: 'rw-r-----', ) install_data(['tests/efi/efivars/RedfishIndications-16faa37e-4b6a-4891-9028-242de65a3b70'], install_dir: join_paths(installed_test_datadir, 'tests', 'efi', 'efivars')) install_data(['tests/efi/efivars/RedfishOSCredentials-16faa37e-4b6a-4891-9028-242de65a3b70'], install_dir: join_paths(installed_test_datadir, 'tests', 'efi', 'efivars')) env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) env.set('FWUPD_LOCALSTATEDIR', '/tmp/fwupd-self-test/var') env.set('LSAN_OPTIONS', 'suppressions=@0@'.format(join_paths(meson.project_source_root(), 'data', 'tests', 'lsan-suppressions.txt'))) e = executable( 'redfish-self-test', rustgen.process('fu-redfish.rs'), sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: [ plugin_deps, libcurl, ], link_with: [ fwupd, fwupdplugin, plugin_builtin_redfish, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ cargs, '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('redfish-self-test', e, env: env) # added to installed-tests endif endif fwupd-2.0.10/plugins/redfish/redfish.quirk000066400000000000000000000016171501337203100205250ustar00rootroot00000000000000# Lenovo ThinkSystem [42f00735-c9ab-5374-bd63-a5deee5881e0] Flags = wildcard-targets,reset-required,ipmi-create-user [REDFISH\VENDOR_Lenovo&ID_BMC-Backup] ParentGuid = REDFISH\VENDOR_Lenovo&ID_BMC-Primary Flags = dual-image,is-backup,no-probe [REDFISH\VENDOR_Lenovo&ID_BMC-Primary] Flags = dual-image,manager-reset RedfishResetPreDelay = 10000 RedfishResetPostDelay = 40000 [REDFISH\VENDOR_Lenovo&ID_LXPM] Flags = ~updatable [REDFISH\VENDOR_Lenovo&ID_LXPMLinuxDriver] Flags = ~updatable ParentGuid = REDFISH\VENDOR_Lenovo&ID_LXPM [REDFISH\VENDOR_Lenovo&ID_LXPMWindowsDriver] Flags = ~updatable ParentGuid = REDFISH\VENDOR_Lenovo&ID_LXPM [REDFISH\VENDOR_SMCI&ID_Backup_BIOS] Flags = is-backup,no-probe # Advantech [18789130-a714-53c0-b025-fa93801d3995] Flags = wildcard-targets,ipmi-create-user [REDFISH\VENDOR_Advantech&ID_BMC] Flags = manager-reset,no-manager-reset-request RedfishResetPreDelay = 100000 fwupd-2.0.10/plugins/redfish/tests/000077500000000000000000000000001501337203100171615ustar00rootroot00000000000000fwupd-2.0.10/plugins/redfish/tests/efi/000077500000000000000000000000001501337203100177245ustar00rootroot00000000000000fwupd-2.0.10/plugins/redfish/tests/efi/efivars/000077500000000000000000000000001501337203100213635ustar00rootroot00000000000000RedfishIndications-16faa37e-4b6a-4891-9028-242de65a3b70000066400000000000000000000000101501337203100314650ustar00rootroot00000000000000fwupd-2.0.10/plugins/redfish/tests/efi/efivarsRedfishOSCredentials-16faa37e-4b6a-4891-9028-242de65a3b70000066400000000000000000000000251501337203100317260ustar00rootroot00000000000000fwupd-2.0.10/plugins/redfish/tests/efi/efivarsusername:passwordfwupd-2.0.10/plugins/redfish/tests/fwupd.conf000066400000000000000000000001121501337203100211470ustar00rootroot00000000000000[redfish] Uri=http://localhost:4661 Username=username2 Password=password2 fwupd-2.0.10/plugins/redfish/tests/redfish-smbios.builder.xml000066400000000000000000000002511501337203100242440ustar00rootroot00000000000000 4661 localhost 0.0.0.0 0x9876 0x5678 fwupd-2.0.10/plugins/redfish/tests/redfish.py000077500000000000000000000451711501337203100211720ustar00rootroot00000000000000#!/usr/bin/env python3 # pylint: disable=invalid-name,missing-docstring # # Copyright 2021 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later import json from flask import Flask, Response, request app = Flask(__name__) HARDCODED_SMC_USERNAME = "smc_username" HARDCODED_UNL_USERNAME = "unlicensed_username" HARDCODED_HPE_USERNAME = "hpe_username" HARDCODED_DELL_USERNAME = "dell_username" HARDCODED_USERNAMES = { "username2", HARDCODED_SMC_USERNAME, HARDCODED_UNL_USERNAME, HARDCODED_HPE_USERNAME, HARDCODED_DELL_USERNAME, } HARDCODED_PASSWORD = "password2" app._percentage545: int = 0 app._percentage546: int = 0 app._hpeupdatestate: str = "Idle" def _failure(msg: str, status=400): res = { "error": {"message": msg}, } return Response( response=json.dumps(res), status=status, mimetype="application/json" ) @app.route("/redfish/v1/") def index(): # reset counter app._percentage545 = 0 app._percentage546 = 0 app._hpeupdatestate = "Idle" # check password from the config file try: if ( request.authorization["username"] not in HARDCODED_USERNAMES or request.authorization["password"] != HARDCODED_PASSWORD ): return _failure("unauthorised", status=401) except (KeyError, TypeError): return _failure("invalid") res = { "@odata.id": "/redfish/v1/", "@odata.etag": "653b835e9ee4af9ea7ea", "RedfishVersion": "1.6.0", "UUID": "92384634-2938-2342-8820-489239905423", "UpdateService": {"@odata.id": "/redfish/v1/UpdateService"}, } if request.authorization["username"] == HARDCODED_HPE_USERNAME: res["Vendor"] = "HPE" if request.authorization["username"] == HARDCODED_DELL_USERNAME: res["Vendor"] = "Dell" if request.authorization["username"] in ( HARDCODED_SMC_USERNAME, HARDCODED_UNL_USERNAME, ): res["Vendor"] = "SMCI" return Response(json.dumps(res), status=200, mimetype="application/json") @app.route("/redfish/v1/Systems") def systems(): res = { "@odata.id": "/redfish/v1/Systems", "@odata.type": "#ComputerSystemCollection.ComputerSystemCollection", "Members": [ {"@odata.id": "/redfish/v1/Systems/System.Embedded.1"}, ], "Members@odata.count": 1, } return Response(json.dumps(res), status=200, mimetype="application/json") @app.route("/redfish/v1/Systems/System.Embedded.1") def system(): res = { "@odata.id": "/redfish/v1/Systems/System.Embedded.1", "@odata.type": "#ComputerSystem.v1_20_1.ComputerSystem", } if request.authorization["username"] == HARDCODED_DELL_USERNAME: res["Oem"] = { "Dell": { "DellSystem": { "SystemID": 3168, } } } return Response(json.dumps(res), status=200, mimetype="application/json") @app.route("/redfish/v1/UpdateService") def update_service(): res = { "@odata.id": "/redfish/v1/UpdateService", "@odata.type": "#UpdateService.v1_8_0.UpdateService", "@odata.etag": "653b835e9ee4af9ea7ea", "FirmwareInventory": { "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory" }, "HttpPushUri": "/FWUpdate", "HttpPushUriOptions": { "HttpPushUriApplyTime": { "ApplyTime": "Immediate", } }, "HttpPushUriOptionsBusy": False, "ServiceEnabled": True, } if request.authorization["username"] == HARDCODED_UNL_USERNAME: res["MultipartHttpPushUri"] = "/FWUpdate-unlicensed" res["Actions"] = { "#UpdateService.StartUpdate": { "target": "/redfish/v1/UpdateService/Actions/UpdateService.StartUpdate" } } elif request.authorization["username"] == HARDCODED_SMC_USERNAME: res["MultipartHttpPushUri"] = "/FWUpdate-smc" res["Actions"] = { "#UpdateService.StartUpdate": { "target": "/redfish/v1/UpdateService/Actions/UpdateService.StartUpdate" } } elif request.authorization["username"] == HARDCODED_HPE_USERNAME: res["Oem"] = {"Hpe": {"State": app._hpeupdatestate}} res["HttpPushUri"] = "/FWUpdate-hpe" else: res["MultipartHttpPushUri"] = "/FWUpdate" return Response(json.dumps(res), status=200, mimetype="application/json") @app.route("/redfish/v1/UpdateService/FirmwareInventory") def firmware_inventory(): res = { "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory", "@odata.type": "#SoftwareInventoryCollection.SoftwareInventoryCollection", "@odata.etag": "653b835e9ee4af9ea7ea", "Members": [ {"@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/BMC"}, {"@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/BIOS"}, ], "Members@odata.count": 2, } return Response(json.dumps(res), status=200, mimetype="application/json") @app.route("/redfish/v1/UpdateService/FirmwareInventory/BMC") def firmware_inventory_bmc(): res = { "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/BMC", "@odata.type": "#SoftwareInventory.v1_2_3.SoftwareInventory", "@odata.etag": "653b835e9ee4af9ea7ea", "Id": "BMC", "LowestSupportedVersion": "11A-0.12", "Name": "Lenovo BMC Firmware", "Oem": { "Hpe": { "DeviceClass": "aa148d2e-6e09-453e-bc6f-63baa5f5ccc4", "Targets": [ "00000000-0000-0000-0000-000000000229", "00000000-0000-0000-0000-000001413436", ], } }, "RelatedItem": [{"@odata.id": "/redfish/v1/Managers/BMC"}], "SoftwareId": "UEFI-AFE1-6", "UefiDevicePaths": ["BMC(0x1,0x0ABCDEF)"], "Updateable": True, "Version": "11A-1.02", "ReleaseDate": "2019-03-15T00:00:00", } if request.authorization["username"] in { HARDCODED_UNL_USERNAME, HARDCODED_SMC_USERNAME, }: res["Manufacturer"] = "SMCI" else: res["Manufacturer"] = "Lenovo" if request.authorization["username"] == HARDCODED_DELL_USERNAME: res["Oem"] = {"Dell": {"DellSoftwareInventory": {"Status": "Installed"}}} return Response(json.dumps(res), status=200, mimetype="application/json") @app.route("/redfish/v1/Managers/BMC") def redfish_managers_bmc(): res = {} return Response(json.dumps(res), status=200, mimetype="application/json") @app.route("/redfish/v1/Chassis/1/PCIeDevices/slot_3/PCIeFunctions/slot_2.00") def firmware_chassis_pcie_function_slot2(): res = { "VendorId": "0x14e4", "FunctionId": 1, "SubsystemId": "0x4042", "DeviceClass": "NetworkController", "SubsystemVendorId": "0x17aa", "DeviceId": "0x165f", "RevisionId": "0x00", "ClassCode": "0x020000", } return Response(json.dumps(res), status=200, mimetype="application/json") @app.route("/redfish/v1/Chassis/1/PCIeDevices/slot_3/PCIeFunctions") def firmware_chassis_pcie_functions(): res = { "Members": [ { "@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/slot_3/PCIeFunctions/slot_2.00" } ], } return Response(json.dumps(res), status=200, mimetype="application/json") @app.route("/redfish/v1/Systems/437XR1138R2") def firmware_systems_slot7(): res = { "SerialNumber": "12345", "@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/slot_3", "@odata.etag": "653b835e9ee4af9ea7ea", "PCIeFunctions": { "@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/slot_3/PCIeFunctions" }, "DeviceType": "SingleFunction", } return Response(json.dumps(res), status=200, mimetype="application/json") @app.route("/redfish/v1/UpdateService/FirmwareInventory/BIOS") def firmware_inventory_bios(): res = { "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/BIOS", "@odata.type": "#SoftwareInventory.v1_2_3.SoftwareInventory", "@odata.etag": "653b835e9ee4af9ea7ea", "Id": "BIOS", "LowestSupportedVersion": "P79 v1.10", "Name": "Contoso BIOS Firmware", "RelatedItem": [{"@odata.id": "/redfish/v1/Systems/437XR1138R2"}], "SoftwareId": "FEE82A67-6CE2-4625-9F44-237AD2402C28", "Updateable": True, "Version": "P79 v1.45", "ReleaseDate": "2019-03-15T00:00:00Z", } if request.authorization["username"] in { HARDCODED_UNL_USERNAME, HARDCODED_SMC_USERNAME, }: res["Manufacturer"] = "SMCI" else: res["Manufacturer"] = "Contoso" return Response(json.dumps(res), status=200, mimetype="application/json") @app.route("/redfish/v1/SessionService/Sessions", methods=["POST"]) def session_service_sessions(): username = request.authorization["username"] if username not in HARDCODED_USERNAMES: return _failure("unauthorised") if request.authorization["password"] != HARDCODED_PASSWORD: return _failure("invalid password") res = { "@odata.id": "/redfish/v1/SessionService/Sessions/1", "@odata.type": "#Session.v1_0_0.Session", "@odata.etag": "653b835e9ee4af9ea7ea", "Id": "1", "Name": "Session 1", "UserName": username, } return Response( json.dumps(res), status=200, mimetype="application/json", headers={"X-Auth-Token": "1234eabcdeabcdeabcdeabcdeabc1234"}, ) @app.route("/redfish/v1/TaskService/999") def task_manager(): res = { "@odata.id": "/redfish/v1/TaskService/999", "@odata.type": "#Task.v1_4_3.Task", "@odata.etag": "653b835e9ee4af9ea7ea", "Id": "545", "Name": "Task 545", } return Response(json.dumps(res), status=200, mimetype="application/json") @app.route("/redfish/v1/TaskService/Tasks/545") def task_status_545(): res = { "@odata.id": "/redfish/v1/TaskService/Tasks/545", "@odata.type": "#Task.v1_4_3.Task", "@odata.etag": "653b835e9ee4af9ea7ea", "Id": "545", "Name": "Task 545", "PercentComplete": app._percentage545, } if app._percentage545 == 0: res["TaskState"] = "Running" elif app._percentage545 in [25, 50, 75]: res["TaskState"] = "Running" res["TaskStatus"] = "OK" res["Messages"] = [ { "Message": "Applying image", "MessageId": "Update.1.1.TransferringToComponent", } ] elif app._percentage545 == 100: res["TaskState"] = "Completed" res["TaskStatus"] = "OK" res["Messages"] = [ { "Message": "Applying image", "MessageId": "Update.1.1.TransferringToComponent", }, { "Message": "A reset is required", "MessageId": "Base.1.10.ResetRequired", }, { "Message": "Task completed OK", "MessageId": "TaskEvent.1.0.TaskCompletedOK", }, ] else: res["TaskState"] = "Exception" res["TaskStatus"] = "Warning" res["Messages"] = [ { "Message": "Error verifying image", "MessageId": "Update.1.0.ApplyFailed", "Severity": "Warning", } ] app._percentage545 += 25 return Response(response=json.dumps(res), status=200, mimetype="application/json") @app.route("/redfish/v1/TaskService/Tasks/546") def task_status_546(): res = { "@odata.type": "#Task.v1_4_3.Task", "@odata.id": "/redfish/v1/TaskService/Tasks/546", "@odata.etag": "653b835e9ee4af9ea7ea", "Id": "546", "Name": "BIOS Verify", "TaskState": "Running", "StartTime": "2022-09-29T14:50:54+00:00", "PercentComplete": app._percentage546, "HidePayload": True, "TaskMonitor": "/redfish/v1/TaskMonitor/gd5n5ffS4gi9r6YKVZmgIIaj8ECLfnc", "TaskStatus": "OK", "Messages": [ { "MessageId": "", "RelatedProperties": [""], "Message": "", "MessageArgs": [""], "Severity": "", } ], "Oem": {}, } if app._percentage546 == 0: res["TaskState"] = "Running" elif app._percentage546 in [25, 50, 75]: res["TaskState"] = "Running" elif app._percentage546 == 100: res["TaskState"] = "Completed" app._percentage546 += 25 return Response(response=json.dumps(res), status=200, mimetype="application/json") @app.route("/FWUpdate-unlicensed", methods=["GET"]) def fwupdate_unlicensed(): res = { "error": { "code": "Base.v1_4_0.GeneralError", "Message": "A general error has occurred. See ExtendedInfo for more information.", "@Message.ExtendedInfo": [ { "MessageId": "SMC.1.0.OemLicenseNotPassed", "Message": "Feature not available.", "MessageArgs": ["BIOS"], } ], } } return Response(json.dumps(res), status=405, mimetype="application/json") @app.route("/FWUpdate-smc", methods=["GET"]) def fwupdate_smc_query(): res = {"Accepted": {"code": "Base.v1_4_0.Accepted"}} return Response(json.dumps(res), status=200, mimetype="application/json") @app.route("/FWUpdate-smc", methods=["POST"]) def fwupdate_smc(): data = json.loads(request.form["UpdateParameters"]) if data["@Redfish.OperationApplyTime"] != "OnStartUpdateRequest": return _failure("apply invalid") if data["Targets"][0] != "/redfish/v1/Systems/1/Bios": return _failure("id invalid") fileitem = request.files["UpdateFile"] if not fileitem.filename.endswith(".bin"): return _failure("filename invalid") filecontents = fileitem.read().decode() if filecontents == "hello": app._percentage546 = 0 res = { "Accepted": { "code": "Base.v1_4_0.Accepted", "Message": "Successfully Accepted Request. Please see the location header and ExtendedInfo for more information.", "@Message.ExtendedInfo": [ { "MessageId": "SMC.1.0.OemSimpleupdateAcceptedMessage", "Severity": "Ok", "Resolution": "No resolution was required.", "Message": "Please also check Task Resource /redfish/v1/TaskService/Tasks/546 to see more information.", "MessageArgs": ["/redfish/v1/TaskService/Tasks/546"], "RelatedProperties": ["BiosVerifyAccepted"], } ], } } # Location set to the URI of a task monitor. return Response( json.dumps(res), status=202, mimetype="application/json", headers={"Location": "/redfish/v1/TaskService/Tasks/546"}, ) elif filecontents == "stuck": res = { "error": { "code": "Base.v1_4_0.GeneralError", "Message": "A general error has occurred. See ExtendedInfo for more information.", "@Message.ExtendedInfo": [ { "MessageId": "SMC.1.0.OemFirmwareAlreadyInUpdateMode", "Severity": "Warning", "Resolution": "Please check if there was the next step with respective API to execute.", "Message": "The BIOS firmware update was already in update mode.", "MessageArgs": ["BIOS"], "RelatedProperties": ["EnterUpdateMode_StatusCheck"], } ], } } return Response(json.dumps(res), status=405, mimetype="application/json") else: return _failure("data invalid") @app.route("/FWUpdate-hpe", methods=["POST"]) def fwupdate_hpe(): print(request.form) if not request.form["sessionKey"]: return _failure("no sessionKey", status=401) data = json.loads(request.form["parameters"]) if not data["UpdateTarget"]: return _failure("payload will not update the target") if data["UpdateRepository"]: return _failure("payload will update the repository") fileitem = request.files["files[]"] if not fileitem: return _failure("no file supplied") app._hpeupdatestate = "Complete" return Response(status=200) @app.route("/FWUpdate", methods=["POST"]) def fwupdate(): data = json.loads(request.form["UpdateParameters"]) if data["@Redfish.OperationApplyTime"] != "Immediate": return _failure("apply invalid") if data["Targets"][0] != "/redfish/v1/UpdateService/FirmwareInventory/BMC": return _failure("id invalid") fileitem = request.files["UpdateFile"] if not fileitem.filename.endswith(".bin"): return _failure("filename invalid") if fileitem.read().decode() != "hello": return _failure("data invalid") res = { "Version": "P79 v1.45", "@odata.id": "/redfish/v1/TaskService/Tasks/545", "@odata.etag": "653b835e9ee4af9ea7ea", "TaskMonitor": "/redfish/v1/TaskService/999", } # Location set to the URI of a task monitor. return Response( json.dumps(res), status=202, mimetype="application/json", headers={"Location": "/redfish/v1/TaskService/Tasks/545"}, ) @app.route( "/redfish/v1/UpdateService/Actions/UpdateService.StartUpdate", methods=["POST"] ) def startupdate(): res = { "Accepted": { "code": "Base.v1_4_0.Accepted", "Message": "Successfully Accepted Request. Please see the location header and ExtendedInfo for more information.", "@Message.ExtendedInfo": [ { "MessageId": "SMC.1.0.OemSimpleupdateAcceptedMessage", "Severity": "Ok", "Resolution": "No resolution was required.", "Message": "Please also check Task Resource /redfish/v1/TaskService/Tasks/546 to see more information.", "MessageArgs": ["/redfish/v1/TaskService/Tasks/546"], "RelatedProperties": ["BiosUpdateAccepted"], } ], } } app._percentage546 = 0 return Response( json.dumps(res), status=202, mimetype="application/json", headers={"Location": "/redfish/v1/TaskService/Tasks/546"}, ) if __name__ == "__main__": app.run(host="0.0.0.0", port=4661) fwupd-2.0.10/plugins/robots.json000066400000000000000000000004411501337203100165750ustar00rootroot00000000000000{ "name": "AI Robot Poison Pill", "steps": [ { "_comment": "DO NOT DOWNLOAD THE FILE BELOW OTHERWISE YOUR IP ADDRESS WILL BE BANNED FROM THE LVFS", "url": "https://fwupd.org/downloads/0c4b070f6e812788df987e40a2ed4f96f28263ad-hughski-colorhug-als-3.0.2.cab" } ] } fwupd-2.0.10/plugins/rp-pico/000077500000000000000000000000001501337203100157445ustar00rootroot00000000000000fwupd-2.0.10/plugins/rp-pico/README.md000066400000000000000000000025331501337203100172260ustar00rootroot00000000000000--- title: Plugin: Raspberry Pi Pico Microcontrollers --- ## Introduction Raspberry Pi Pico Microcontrollers can expose a so-called "Reset" USB interface. When this is exposed, they can be reset into the bootrom and then updated (using the UF2 plugin). ## GUID Generation Raspberry Pi offers vendors to use the Raspberry Pi USB PID space. However, each vendor can chose their own VID/PID instead. Devices from MNT Research use the standard USB DeviceInstanceId values, e.g. * `USB\VID_1209&PID_6D07` * `USB\VID_1209&PID_6D07&REV_0001` Note that the VID is the shared VID. ## Update Behavior The runtime interface should use a [DS-20 BOS descriptor](https://fwupd.github.io/libfwupdplugin/ds20.html) with something like these as the contents: Plugin=rp_pico Flags=internal Version=1.2.3 VersionFormat=triplet Icon=computer Devices are updated by triggering a reset, and then entering the Raspberry Pi Pico bootrom. The bootrom speaks the UF2 protocol. ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `2.0.2`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Lukas F. Hartmann: @mntmn fwupd-2.0.10/plugins/rp-pico/fu-rp-pico-device.c000066400000000000000000000075331501337203100213360ustar00rootroot00000000000000/* * Copyright 2024 Chris Hofstaedtler * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-rp-pico-device.h" #include "fu-rp-pico-struct.h" struct _FuRpPicoDevice { FuUsbDevice parent_instance; guint8 iface_reset; }; G_DEFINE_TYPE(FuRpPicoDevice, fu_rp_pico_device, FU_TYPE_USB_DEVICE) #define FU_RP_PICO_DEVICE_RESET_INTERFACE_SUBCLASS 0x00 #define FU_RP_PICO_DEVICE_RESET_INTERFACE_PROTOCOL 0x01 static void fu_rp_pico_device_to_string(FuDevice *device, guint idt, GString *str) { FuRpPicoDevice *self = FU_RP_PICO_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "IfaceReset", self->iface_reset); } static gboolean fu_rp_pico_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuRpPicoDevice *self = FU_RP_PICO_DEVICE(device); g_autoptr(GError) error_local = NULL; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_CLASS, FU_USB_RECIPIENT_INTERFACE, FU_RP_PICO_RESET_REQUEST_BOOTSEL, 0, self->iface_reset, NULL, 0, NULL, 2000, NULL, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_READ) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_debug("ignoring expected error %s", error_local->message); } else { g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to restart device: "); return FALSE; } } /* success */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_rp_pico_device_probe(FuDevice *device, GError **error) { FuRpPicoDevice *self = FU_RP_PICO_DEVICE(device); g_autoptr(FuUsbInterface) intf = NULL; intf = fu_usb_device_get_interface(FU_USB_DEVICE(self), FU_USB_CLASS_VENDOR_SPECIFIC, FU_RP_PICO_DEVICE_RESET_INTERFACE_SUBCLASS, FU_RP_PICO_DEVICE_RESET_INTERFACE_PROTOCOL, error); if (intf == NULL) return FALSE; self->iface_reset = fu_usb_interface_get_number(intf); fu_usb_device_add_interface(FU_USB_DEVICE(self), self->iface_reset); return TRUE; } static void fu_rp_pico_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 80, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 5, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 15, "reload"); } static void fu_rp_pico_device_init(FuRpPicoDevice *self) { fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_add_protocol(FU_DEVICE(self), "com.microsoft.uf2"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_COUNTERPART_GUIDS); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_RETRY_OPEN); /* revisions indicate incompatible hardware */ fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_INSTANCE_ID_REV); fu_device_retry_set_delay(FU_DEVICE(self), 100); } static void fu_rp_pico_device_class_init(FuRpPicoDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_rp_pico_device_to_string; device_class->probe = fu_rp_pico_device_probe; device_class->detach = fu_rp_pico_device_detach; device_class->set_progress = fu_rp_pico_device_set_progress; } fwupd-2.0.10/plugins/rp-pico/fu-rp-pico-device.h000066400000000000000000000004521501337203100213340ustar00rootroot00000000000000/* * Copyright 2024 Chris Hofstaedtler * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_RP_PICO_DEVICE (fu_rp_pico_device_get_type()) G_DECLARE_FINAL_TYPE(FuRpPicoDevice, fu_rp_pico_device, FU, RP_PICO_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/rp-pico/fu-rp-pico-plugin.c000066400000000000000000000013211501337203100213620ustar00rootroot00000000000000/* * Copyright 2024 Chris Hofstaedtler * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-rp-pico-device.h" #include "fu-rp-pico-plugin.h" struct _FuRpPicoPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuRpPicoPlugin, fu_rp_pico_plugin, FU_TYPE_PLUGIN) static void fu_rp_pico_plugin_init(FuRpPicoPlugin *self) { } static void fu_rp_pico_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_RP_PICO_DEVICE); } static void fu_rp_pico_plugin_class_init(FuRpPicoPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_rp_pico_plugin_constructed; } fwupd-2.0.10/plugins/rp-pico/fu-rp-pico-plugin.h000066400000000000000000000003511501337203100213710ustar00rootroot00000000000000/* * Copyright 2024 Chris Hofstaedtler * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuRpPicoPlugin, fu_rp_pico_plugin, FU, RP_PICO_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/rp-pico/fu-rp-pico.rs000066400000000000000000000003651501337203100202770ustar00rootroot00000000000000// Copyright 2024 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later // control requests: enum FuRpPicoResetRequest { Bootsel = 0x01, // to UF2 Reset = 0x02, // to application } fwupd-2.0.10/plugins/rp-pico/meson.build000066400000000000000000000010031501337203100201000ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginRpPico"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_builtins += static_library('fu_plugin_rp_pico', rustgen.process('fu-rp-pico.rs'), sources: [ 'fu-rp-pico-device.c', 'fu-rp-pico-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) enumeration_data += files('tests/mnt-pocket-reform-sysctl-setup.json') device_tests += files('tests/mnt-pocket-reform-sysctl.json') fwupd-2.0.10/plugins/rp-pico/tests/000077500000000000000000000000001501337203100171065ustar00rootroot00000000000000fwupd-2.0.10/plugins/rp-pico/tests/mnt-pocket-reform-sysctl-setup.json000066400000000000000000000070651501337203100260370ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUsbDevice", "PlatformId": "1-6", "Created": "2024-11-12T10:07:39.666088Z", "IdVendor": 4617, "IdProduct": 27911, "Device": 1, "USB": 528, "Manufacturer": 1, "DeviceClass": 239, "DeviceSubClass": 2, "DeviceProtocol": 1, "Product": 2, "SerialNumber": 3, "UsbBosDescriptors": [ { "DevCapabilityType": 33, "ExtraData": "AAE=" }, { "DevCapabilityType": 5, "ExtraData": "AGPsCgF09c1SndooUlUNlPAOCQEAiQBCAA==" } ], "UsbConfigDescriptors": [ { "ConfigurationValue": 1 } ], "UsbInterfaces": [ { "Length": 9, "DescriptorType": 4, "InterfaceClass": 2, "InterfaceSubClass": 2, "Interface": 4, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 129, "Interval": 16, "MaxPacketSize": 8 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 1, "InterfaceClass": 10, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 2, "MaxPacketSize": 64 }, { "DescriptorType": 5, "EndpointAddress": 130, "MaxPacketSize": 64 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 2, "InterfaceClass": 255, "InterfaceProtocol": 1, "Interface": 5 } ], "UsbEvents": [ { "Id": "#d5a801ad", "Data": "usb" }, { "Id": "#ad8c58d3", "Data": "usb" }, { "Id": "#1075ed5c", "Data": "usb_device" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=25\nDEVNAME=bus/usb/001/026\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=1209/6d07/1\nTYPE=239/2/1\nBUSNUM=001\nDEVNUM=026" }, { "Id": "#d432c663", "Data": "bus/usb/001/026" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c", "Data": "usb_device" }, { "Id": "#4693935e", "Data": "001" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=25\nDEVNAME=bus/usb/001/026\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=1209/6d07/1\nTYPE=239/2/1\nBUSNUM=001\nDEVNUM=026" }, { "Id": "#1ab3ae0a", "Data": "026" }, { "Id": "#60108bf3", "Data": "VmVuZG9yPU1OVCBSZXNlYXJjaApQbHVnaW49cnBfcGljbwpGbGFncz1pbnRlcm5hbApWZXJzaW9uRm9ybWF0PW51bWJlcgpWZXJzaW9uPTExMTEKSWNvbj1jb21wdXRlcgpDb3VudGVycGFydEd1aWQ9QkxPQ0tcVkVOXzJFOEEmREVWXzAwMDM=" }, { "Id": "#1fcf122d", "Data": "UG9ja2V0IFJlZm9ybSBTeXN0ZW0gQ29udHJvbGxlciAxLjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" }, { "Id": "#028c3a0e", "Data": "RTY2MEMwNjIxMzk2ODgzNgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" } ] } ] } fwupd-2.0.10/plugins/rp-pico/tests/mnt-pocket-reform-sysctl.json000066400000000000000000000007251501337203100246750ustar00rootroot00000000000000{ "name": "MNT Pocket Reform System Controller", "interactive": false, "steps": [ { "url": "607566d4cf837c8e6607339bf8e81d29f8cc6ad24d529eb93b66d6c34feb4a23-sysctl_3_.cab", "emulation-url": "e4f34b9dc6f546011f1db264d40a54db07aa542eb5d1a1715fbe5942d92d498e-sysctl_2_.zip", "components": [ { "version": "1112", "guids": [ "df0c9fc8-88d6-5100-ba66-0f5c436fc522" ] } ] } ] } fwupd-2.0.10/plugins/rts54hid/000077500000000000000000000000001501337203100160415ustar00rootroot00000000000000fwupd-2.0.10/plugins/rts54hid/README.md000066400000000000000000000032201501337203100173150ustar00rootroot00000000000000--- title: Plugin: RTS54HID --- ## Introduction This plugin allows the user to update any supported hub and attached downstream ICs using a custom HID-based flashing protocol. It does not support any RTS54xx device using the HUB update protocol. Other devices connected to the RTS54HIDxx using I2C will be supported soon. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in an unspecified binary file format. This plugin supports the following protocol ID: * `com.realtek.rts54` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_0BDA&PID_1100` Child I²C devices are created using the device number as a suffix, for instance: * `USB\VID_0BDA&PID_1100&I2C_01` ## Update Behavior The firmware is deployed when the device is in normal runtime mode, and the device will reset when the new firmware has been written. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x0BDA` ## Quirk Use This plugin uses the following plugin-specific quirks: ### Rts54TargetAddr The target address of a child module. Since: 1.1.3 ### Rts54I2cSpeed The I2C speed to operate at (0, 1, 2). Since: 1.1.3 ### Rts54RegisterAddrLen The I2C register address length of commands. Since: 1.1.3 ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.2.0`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Ricky Wu: @AnyProblem fwupd-2.0.10/plugins/rts54hid/fu-rts54hid-common.h000066400000000000000000000006221501337203100215560ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2018 Realtek Semiconductor Corporation * Copyright 2018 Dell Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_RTS54HID_TRANSFER_BLOCK_SIZE 0x80 #define FU_RTS54FU_HID_REPORT_LENGTH 0xc0 /* [vendor-cmd:64] [data-payload:128] */ #define FU_RTS54HID_CMD_BUFFER_OFFSET_DATA 0x40 fwupd-2.0.10/plugins/rts54hid/fu-rts54hid-device.c000066400000000000000000000253361501337203100215310ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-rts54hid-common.h" #include "fu-rts54hid-device.h" #include "fu-rts54hid-struct.h" struct _FuRts54HidDevice { FuHidDevice parent_instance; gboolean fw_auth; gboolean dual_bank; }; G_DEFINE_TYPE(FuRts54HidDevice, fu_rts54hid_device, FU_TYPE_HID_DEVICE) static void fu_rts54hid_device_to_string(FuDevice *device, guint idt, GString *str) { FuRts54HidDevice *self = FU_RTS54HID_DEVICE(device); fwupd_codec_string_append_bool(str, idt, "FwAuth", self->fw_auth); fwupd_codec_string_append_bool(str, idt, "DualBank", self->dual_bank); } static gboolean fu_rts54hid_device_set_clock_mode(FuRts54HidDevice *self, gboolean enable, GError **error) { g_autoptr(FuRts54HidCmdBuffer) st = fu_rts54_hid_cmd_buffer_new(); fu_rts54_hid_cmd_buffer_set_cmd(st, FU_RTS54HID_CMD_WRITE_DATA); fu_rts54_hid_cmd_buffer_set_ext(st, FU_RTS54HID_EXT_MCUMODIFYCLOCK); fu_rts54_hid_cmd_buffer_set_dwregaddr(st, (guint8)enable); fu_byte_array_set_size(st, FU_RTS54FU_HID_REPORT_LENGTH, 0x0); if (!fu_hid_device_set_report(FU_HID_DEVICE(self), 0x0, st->data, st->len, FU_RTS54HID_DEVICE_TIMEOUT * 2, FU_HID_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to set clock-mode=%i: ", enable); return FALSE; } return TRUE; } static gboolean fu_rts54hid_device_reset_to_flash(FuRts54HidDevice *self, GError **error) { g_autoptr(FuRts54HidCmdBuffer) st = fu_rts54_hid_cmd_buffer_new(); fu_rts54_hid_cmd_buffer_set_cmd(st, FU_RTS54HID_CMD_WRITE_DATA); fu_rts54_hid_cmd_buffer_set_ext(st, FU_RTS54HID_EXT_RESET2FLASH); fu_byte_array_set_size(st, FU_RTS54FU_HID_REPORT_LENGTH, 0x0); if (!fu_hid_device_set_report(FU_HID_DEVICE(self), 0x0, st->data, st->len, FU_RTS54HID_DEVICE_TIMEOUT * 2, FU_HID_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to soft reset: "); return FALSE; } return TRUE; } static gboolean fu_rts54hid_device_write_flash(FuRts54HidDevice *self, guint32 addr, const guint8 *data, guint16 data_sz, GError **error) { g_autoptr(FuRts54HidCmdBuffer) st = fu_rts54_hid_cmd_buffer_new(); g_return_val_if_fail(data_sz <= 128, FALSE); g_return_val_if_fail(data != NULL, FALSE); g_return_val_if_fail(data_sz != 0, FALSE); fu_rts54_hid_cmd_buffer_set_cmd(st, FU_RTS54HID_CMD_WRITE_DATA); fu_rts54_hid_cmd_buffer_set_ext(st, FU_RTS54HID_EXT_WRITEFLASH); fu_rts54_hid_cmd_buffer_set_dwregaddr(st, addr); fu_rts54_hid_cmd_buffer_set_bufferlen(st, data_sz); fu_byte_array_set_size(st, FU_RTS54FU_HID_REPORT_LENGTH, 0x0); if (!fu_memcpy_safe(st->data, st->len, FU_RTS54HID_CMD_BUFFER_OFFSET_DATA, /* dst */ data, data_sz, 0x0, /* src */ data_sz, error)) return FALSE; if (!fu_hid_device_set_report(FU_HID_DEVICE(self), 0x0, st->data, st->len, FU_RTS54HID_DEVICE_TIMEOUT * 2, FU_HID_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write flash @%08x: ", (guint)addr); return FALSE; } return TRUE; } static gboolean fu_rts54hid_device_verify_update_fw(FuRts54HidDevice *self, FuProgress *progress, GError **error) { g_autoptr(FuRts54HidCmdBuffer) st = fu_rts54_hid_cmd_buffer_new(); fu_rts54_hid_cmd_buffer_set_cmd(st, FU_RTS54HID_CMD_WRITE_DATA); fu_rts54_hid_cmd_buffer_set_ext(st, FU_RTS54HID_EXT_VERIFYUPDATE); fu_rts54_hid_cmd_buffer_set_dwregaddr(st, 1); fu_rts54_hid_cmd_buffer_set_bufferlen(st, 1); fu_byte_array_set_size(st, FU_RTS54FU_HID_REPORT_LENGTH, 0x0); /* set then get */ if (!fu_hid_device_set_report(FU_HID_DEVICE(self), 0x0, st->data, st->len, FU_RTS54HID_DEVICE_TIMEOUT * 2, FU_HID_DEVICE_FLAG_NONE, error)) return FALSE; fu_device_sleep_full(FU_DEVICE(self), 4000, progress); /* ms */ if (!fu_hid_device_get_report(FU_HID_DEVICE(self), 0x0, st->data, st->len, FU_RTS54HID_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_NONE, error)) return FALSE; /* check device status */ if (st->data[0] != 0x01) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "firmware flash failed"); return FALSE; } /* success */ return TRUE; } static gboolean fu_rts54hid_device_erase_spare_bank(FuRts54HidDevice *self, GError **error) { g_autoptr(FuRts54HidCmdBuffer) st = fu_rts54_hid_cmd_buffer_new(); fu_rts54_hid_cmd_buffer_set_cmd(st, FU_RTS54HID_CMD_WRITE_DATA); fu_rts54_hid_cmd_buffer_set_ext(st, FU_RTS54HID_EXT_ERASEBANK); fu_rts54_hid_cmd_buffer_set_dwregaddr(st, 0x100); fu_byte_array_set_size(st, FU_RTS54FU_HID_REPORT_LENGTH, 0x0); if (!fu_hid_device_set_report(FU_HID_DEVICE(self), 0x0, st->data, st->len, FU_RTS54HID_DEVICE_TIMEOUT * 2, FU_HID_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to erase spare bank: "); return FALSE; } return TRUE; } static gboolean fu_rts54hid_device_ensure_status(FuRts54HidDevice *self, GError **error) { g_autofree gchar *version = NULL; g_autoptr(FuRts54HidCmdBuffer) st = fu_rts54_hid_cmd_buffer_new(); fu_rts54_hid_cmd_buffer_set_cmd(st, FU_RTS54HID_CMD_READ_DATA); fu_rts54_hid_cmd_buffer_set_ext(st, FU_RTS54HID_EXT_READ_STATUS); fu_rts54_hid_cmd_buffer_set_bufferlen(st, 32); fu_byte_array_set_size(st, FU_RTS54FU_HID_REPORT_LENGTH, 0x0); /* set then get */ if (!fu_hid_device_set_report(FU_HID_DEVICE(self), 0x0, st->data, st->len, FU_RTS54HID_DEVICE_TIMEOUT * 2, FU_HID_DEVICE_FLAG_NONE, error)) return FALSE; if (!fu_hid_device_get_report(FU_HID_DEVICE(self), 0x0, st->data, st->len, FU_RTS54HID_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_NONE, error)) return FALSE; /* check the hardware capabilities */ self->dual_bank = (st->data[7] & 0xf0) == 0x80; self->fw_auth = (st->data[13] & 0x02) > 0; /* hub version is more accurate than bcdVersion */ version = g_strdup_printf("%x.%x", st->data[10], st->data[11]); fu_device_set_version(FU_DEVICE(self), version); return TRUE; } static gboolean fu_rts54hid_device_setup(FuDevice *device, GError **error) { FuRts54HidDevice *self = FU_RTS54HID_DEVICE(device); /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_rts54hid_device_parent_class)->setup(device, error)) return FALSE; /* check this device is correct */ if (!fu_rts54hid_device_ensure_status(self, error)) return FALSE; /* both conditions must be set */ if (!self->fw_auth) { fu_device_set_update_error(device, "device does not support authentication"); } else if (!self->dual_bank) { fu_device_set_update_error(device, "device does not support dual-bank updating"); } else { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); } /* success */ return TRUE; } static gboolean fu_rts54hid_device_close(FuDevice *device, GError **error) { FuRts54HidDevice *self = FU_RTS54HID_DEVICE(device); /* set MCU to normal clock rate */ if (!fu_rts54hid_device_set_clock_mode(self, FALSE, error)) return FALSE; /* FuHidDevice->close */ return FU_DEVICE_CLASS(fu_rts54hid_device_parent_class)->close(device, error); } static gboolean fu_rts54hid_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuRts54HidDevice *self = FU_RTS54HID_DEVICE(device); g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 46, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 52, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reset"); /* get default image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* set MCU to high clock rate for better ISP performance */ if (!fu_rts54hid_device_set_clock_mode(self, TRUE, error)) return FALSE; /* erase spare flash bank only if it is not empty */ if (!fu_rts54hid_device_erase_spare_bank(self, error)) return FALSE; fu_progress_step_done(progress); /* write each block */ chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_RTS54HID_TRANSFER_BLOCK_SIZE, error); if (chunks == NULL) return FALSE; for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; /* write chunk */ if (!fu_rts54hid_device_write_flash(self, fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; /* update progress */ fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)i + 1, (gsize)fu_chunk_array_length(chunks)); } fu_progress_step_done(progress); /* get device to authenticate the firmware */ if (!fu_rts54hid_device_verify_update_fw(self, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* send software reset to run available flash code */ if (!fu_rts54hid_device_reset_to_flash(self, error)) return FALSE; fu_progress_step_done(progress); /* success! */ return TRUE; } static void fu_rts54hid_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 62, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 38, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_rts54hid_device_init(FuRts54HidDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "com.realtek.rts54"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PAIR); } static void fu_rts54hid_device_class_init(FuRts54HidDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_rts54hid_device_write_firmware; device_class->to_string = fu_rts54hid_device_to_string; device_class->setup = fu_rts54hid_device_setup; device_class->close = fu_rts54hid_device_close; device_class->set_progress = fu_rts54hid_device_set_progress; } fwupd-2.0.10/plugins/rts54hid/fu-rts54hid-device.h000066400000000000000000000005471501337203100215330ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_RTS54HID_DEVICE (fu_rts54hid_device_get_type()) G_DECLARE_FINAL_TYPE(FuRts54HidDevice, fu_rts54hid_device, FU, RTS54HID_DEVICE, FuHidDevice) #define FU_RTS54HID_DEVICE_TIMEOUT 1000 /* ms */ fwupd-2.0.10/plugins/rts54hid/fu-rts54hid-module.c000066400000000000000000000164231501337203100215540ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-rts54hid-common.h" #include "fu-rts54hid-device.h" #include "fu-rts54hid-module.h" #include "fu-rts54hid-struct.h" struct _FuRts54HidModule { FuDevice parent_instance; guint8 target_addr; guint8 i2c_speed; guint8 register_addr_len; }; G_DEFINE_TYPE(FuRts54HidModule, fu_rts54hid_module, FU_TYPE_DEVICE) static void fu_rts54hid_module_to_string(FuDevice *module, guint idt, GString *str) { FuRts54HidModule *self = FU_RTS54HID_MODULE(module); fwupd_codec_string_append_hex(str, idt, "TargetAddr", self->target_addr); fwupd_codec_string_append_hex(str, idt, "I2cSpeed", self->i2c_speed); fwupd_codec_string_append_hex(str, idt, "RegisterAddrLen", self->register_addr_len); } static FuRts54HidDevice * fu_rts54hid_module_get_parent(FuRts54HidModule *self, GError **error) { FuDevice *parent = fu_device_get_parent(FU_DEVICE(self)); if (parent == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "no parent set"); return NULL; } return FU_RTS54HID_DEVICE(parent); } static gboolean fu_rts54hid_module_i2c_write(FuRts54HidModule *self, const guint8 *data, guint8 data_sz, GError **error) { FuRts54HidDevice *parent; g_autoptr(FuRts54HidCmdBuffer) st = fu_rts54_hid_cmd_buffer_new(); fu_rts54_hid_cmd_buffer_set_cmd(st, FU_RTS54HID_CMD_WRITE_DATA); fu_rts54_hid_cmd_buffer_set_ext(st, FU_RTS54HID_EXT_I2C_WRITE); fu_rts54_hid_cmd_buffer_set_bufferlen(st, data_sz); fu_rts54_hid_cmd_buffer_set_i2c_target_addr(st, self->target_addr); fu_rts54_hid_cmd_buffer_set_i2c_data_sz(st, self->register_addr_len); fu_rts54_hid_cmd_buffer_set_i2c_speed(st, self->i2c_speed | 0x80); fu_byte_array_set_size(st, FU_RTS54FU_HID_REPORT_LENGTH, 0x0); g_return_val_if_fail(data_sz <= 128, FALSE); g_return_val_if_fail(data != NULL, FALSE); g_return_val_if_fail(data_sz != 0, FALSE); /* get parent to issue command */ parent = fu_rts54hid_module_get_parent(self, error); if (parent == NULL) return FALSE; if (!fu_memcpy_safe(st->data, st->len, FU_RTS54HID_CMD_BUFFER_OFFSET_DATA, /* dst */ data, data_sz, 0x0, /* src */ data_sz, error)) return FALSE; if (!fu_hid_device_set_report(FU_HID_DEVICE(parent), 0x0, st->data, st->len, FU_RTS54HID_DEVICE_TIMEOUT * 2, FU_HID_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write i2c @%04x: ", self->target_addr); return FALSE; } return TRUE; } static gboolean fu_rts54hid_module_i2c_read(FuRts54HidModule *self, guint32 cmd, guint8 *data, guint8 data_sz, GError **error) { FuRts54HidDevice *parent; g_autoptr(FuRts54HidCmdBuffer) st = fu_rts54_hid_cmd_buffer_new(); fu_rts54_hid_cmd_buffer_set_cmd(st, FU_RTS54HID_CMD_WRITE_DATA); fu_rts54_hid_cmd_buffer_set_ext(st, FU_RTS54HID_EXT_I2C_READ); fu_rts54_hid_cmd_buffer_set_dwregaddr(st, cmd); fu_rts54_hid_cmd_buffer_set_bufferlen(st, data_sz); fu_rts54_hid_cmd_buffer_set_i2c_target_addr(st, self->target_addr); fu_rts54_hid_cmd_buffer_set_i2c_data_sz(st, self->register_addr_len); fu_rts54_hid_cmd_buffer_set_i2c_speed(st, self->i2c_speed | 0x80); fu_byte_array_set_size(st, FU_RTS54FU_HID_REPORT_LENGTH, 0x0); g_return_val_if_fail(data_sz <= 192, FALSE); g_return_val_if_fail(data != NULL, FALSE); g_return_val_if_fail(data_sz != 0, FALSE); /* get parent to issue command */ parent = fu_rts54hid_module_get_parent(self, error); if (parent == NULL) return FALSE; /* read from module */ if (!fu_hid_device_set_report(FU_HID_DEVICE(parent), 0x0, st->data, st->len, FU_RTS54HID_DEVICE_TIMEOUT * 2, FU_HID_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write i2c @%04x: ", self->target_addr); return FALSE; } if (!fu_hid_device_get_report(FU_HID_DEVICE(parent), 0x0, st->data, st->len, FU_RTS54HID_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_NONE, error)) return FALSE; return fu_memcpy_safe(data, data_sz, 0x0, st->data, st->len, FU_RTS54HID_CMD_BUFFER_OFFSET_DATA, data_sz, error); } static gboolean fu_rts54hid_module_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuRts54HidModule *self = FU_RTS54HID_MODULE(device); guint64 tmp = 0; /* load target address from quirks */ if (g_strcmp0(key, "Rts54TargetAddr") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->target_addr = tmp; return TRUE; } /* load i2c speed from quirks */ if (g_strcmp0(key, "Rts54I2cSpeed") == 0) { if (!fu_strtoull(value, &tmp, 0, FU_RTS54HID_I2C_SPEED_LAST - 1, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->i2c_speed = tmp; return TRUE; } /* load register address length from quirks */ if (g_strcmp0(key, "Rts54RegisterAddrLen") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->register_addr_len = tmp; return TRUE; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static gboolean fu_rts54hid_module_write_firmware(FuDevice *module, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuRts54HidModule *self = FU_RTS54HID_MODULE(module); g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* get default image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* build packets */ chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_RTS54HID_TRANSFER_BLOCK_SIZE, error); if (chunks == NULL) return FALSE; if (0) { if (!fu_rts54hid_module_i2c_read(self, 0x0000, NULL, 0, error)) return FALSE; if (!fu_rts54hid_module_i2c_write(self, NULL, 0, error)) return FALSE; } /* write each block */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; /* write chunk */ if (!fu_rts54hid_module_i2c_write(self, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; /* update progress */ fu_progress_set_percentage_full(progress, (gsize)i + 1, (gsize)fu_chunk_array_length(chunks)); } /* success! */ return TRUE; } static void fu_rts54hid_module_init(FuRts54HidModule *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_OPEN); } static void fu_rts54hid_module_class_init(FuRts54HidModuleClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_rts54hid_module_write_firmware; device_class->to_string = fu_rts54hid_module_to_string; device_class->set_quirk_kv = fu_rts54hid_module_set_quirk_kv; } fwupd-2.0.10/plugins/rts54hid/fu-rts54hid-module.h000066400000000000000000000004621501337203100215550ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_RTS54HID_MODULE (fu_rts54hid_module_get_type()) G_DECLARE_FINAL_TYPE(FuRts54HidModule, fu_rts54hid_module, FU, RTS54HID_MODULE, FuDevice) fwupd-2.0.10/plugins/rts54hid/fu-rts54hid-plugin.c000066400000000000000000000024341501337203100215620ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-rts54hid-device.h" #include "fu-rts54hid-module.h" #include "fu-rts54hid-plugin.h" struct _FuRts54HidPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuRts54HidPlugin, fu_rts54hid_plugin, FU_TYPE_PLUGIN) static void fu_rts54hid_plugin_init(FuRts54HidPlugin *self) { } static void fu_rts54hid_plugin_object_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_set_name(plugin, "rts54hid"); } static void fu_rts54hid_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "Rts54TargetAddr"); fu_context_add_quirk_key(ctx, "Rts54I2cSpeed"); fu_context_add_quirk_key(ctx, "Rts54RegisterAddrLen"); fu_plugin_add_device_gtype(plugin, FU_TYPE_RTS54HID_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_RTS54HID_MODULE); } static void fu_rts54hid_plugin_class_init(FuRts54HidPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->constructed = fu_rts54hid_plugin_object_constructed; plugin_class->constructed = fu_rts54hid_plugin_constructed; } fwupd-2.0.10/plugins/rts54hid/fu-rts54hid-plugin.h000066400000000000000000000003621501337203100215650ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuRts54HidPlugin, fu_rts54hid_plugin, FU, RTS54HID_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/rts54hid/fu-rts54hid.rs000066400000000000000000000014511501337203100204660ustar00rootroot00000000000000// Copyright 2024 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[repr(u8)] enum FuRts54hidI2cSpeed { 250K, 400K, 800K, } #[repr(u8)] enum FuRts54hidCmd { ReadData = 0xC0, WriteData = 0x40, } #[repr(u8)] enum FuRts54hidExt { Mcumodifyclock = 0x06, ReadStatus = 0x09, I2cWrite = 0xC6, Writeflash = 0xC8, I2cRead = 0xD6, Readflash = 0xD8, Verifyupdate = 0xD9, Erasebank = 0xE8, Reset2flash = 0xE9, } #[repr(C, packed)] #[derive(New)] struct FuRts54HidCmdBuffer { cmd: FuRts54hidCmd, ext: FuRts54hidExt, dwregaddr: u32le, bufferlen: u16le, i2c_target_addr: u8, i2c_data_sz: u8, i2c_speed: FuRts54hidI2cSpeed, reserved: u8, } fwupd-2.0.10/plugins/rts54hid/meson.build000066400000000000000000000007111501337203100202020ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginRts54Hid"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('rts54hid.quirk') plugin_builtins += static_library('fu_plugin_rts54hid', rustgen.process('fu-rts54hid.rs'), sources: [ 'fu-rts54hid-device.c', 'fu-rts54hid-module.c', 'fu-rts54hid-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) fwupd-2.0.10/plugins/rts54hid/rts54hid.quirk000066400000000000000000000007021501337203100205630ustar00rootroot00000000000000# Realtek USBHub (HID Device) [USB\VID_0BDA&PID_5A00] Plugin = rts54hid Flags = enforce-requires GType = FuRts54HidDevice FirmwareSizeMin = 0x10000 FirmwareSizeMax = 0x40000 Children = FuRts54HidModule|USB\VID_0BDA&PID_5A00&I2C_01 # this is a fictitious example... [USB\VID_0BDA&PID_5A00&I2C_01] Plugin = rts54hid Name = HDMI Converter Flags = updatable FirmwareSize = 0x20000 Rts54TargetAddr = 0x00 Rts54I2cSpeed = 0x00 Rts54RegisterAddrLen = 0x04 fwupd-2.0.10/plugins/rts54hub/000077500000000000000000000000001501337203100160535ustar00rootroot00000000000000fwupd-2.0.10/plugins/rts54hub/README.md000066400000000000000000000024041501337203100173320ustar00rootroot00000000000000--- title: Plugin: RTS54HUB --- ## Introduction This plugin allows the user to update any supported hub and attached downstream ICs using a custom HUB-based flashing protocol. It does not support any RTS54xx device using the HID update protocol. Other devices connected to the RTS54xx using I2C will be supported soon. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in an unspecified binary file format. This plugin supports the following protocol IDs: * `com.realtek.rts54` * `com.realtek.rts54.i2c` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_0BDA&PID_5423` ## Update Behavior The firmware is deployed when the device is in normal runtime mode, and the device will reset when the new firmware has been written. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x0BDA` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.2.0`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Ricky Wu: @AnyProblem fwupd-2.0.10/plugins/rts54hub/data/000077500000000000000000000000001501337203100167645ustar00rootroot00000000000000fwupd-2.0.10/plugins/rts54hub/data/lsusb.txt000066400000000000000000000111441501337203100206560ustar00rootroot00000000000000Bus 001 Device 038: ID 0bda:5423 Realtek Semiconductor Corp. Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.10 bDeviceClass 9 Hub bDeviceSubClass 0 bDeviceProtocol 2 TT per port bMaxPacketSize0 64 idVendor 0x0bda Realtek Semiconductor Corp. idProduct 0x5423 bcdDevice 1.19 iManufacturer 1 Generic iProduct 2 4-Port USB 2.0 Hub iSerial 0 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 41 bNumInterfaces 1 bConfigurationValue 1 iConfiguration 0 bmAttributes 0xe0 Self Powered Remote Wakeup MaxPower 0mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 1 bInterfaceClass 9 Hub bInterfaceSubClass 0 bInterfaceProtocol 1 Single TT iInterface 0 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0001 1x 1 bytes bInterval 12 Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 1 bNumEndpoints 1 bInterfaceClass 9 Hub bInterfaceSubClass 0 bInterfaceProtocol 2 TT per port iInterface 0 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0001 1x 1 bytes bInterval 12 Hub Descriptor: bLength 9 bDescriptorType 41 nNbrPorts 5 wHubCharacteristic 0x00a9 Per-port power switching Per-port overcurrent protection TT think time 16 FS bits Port indicators bPwrOn2PwrGood 0 * 2 milli seconds bHubContrCurrent 100 milli Ampere DeviceRemovable 0x00 PortPwrCtrlMask 0xff Hub Port Status: Port 1: 0000.0100 power Port 2: 0000.0100 power Port 3: 0000.0100 power Port 4: 0000.0100 power Port 5: 0000.0503 highspeed power enable connect Binary Object Store Descriptor: bLength 5 bDescriptorType 15 wTotalLength 73 bNumDeviceCaps 5 USB 2.0 Extension Device Capability: bLength 7 bDescriptorType 16 bDevCapabilityType 2 bmAttributes 0x0000f41e BESL Link Power Management (LPM) Supported BESL value 1024 us Deep BESL value 61440 us SuperSpeed USB Device Capability: bLength 10 bDescriptorType 16 bDevCapabilityType 3 bmAttributes 0x00 wSpeedsSupported 0x000e Device can operate at Full Speed (12Mbps) Device can operate at High Speed (480Mbps) Device can operate at SuperSpeed (5Gbps) bFunctionalitySupport 1 Lowest fully-functional device speed is Full Speed (12Mbps) bU1DevExitLat 10 micro seconds bU2DevExitLat 1023 micro seconds SuperSpeedPlus USB Device Capability: bLength 28 bDescriptorType 16 bDevCapabilityType 10 bmAttributes 0x00000023 Sublink Speed Attribute count 3 Sublink Speed ID count 1 wFunctionalitySupport 0x1100 bmSublinkSpeedAttr[0] 0x00050030 Speed Attribute ID: 0 5Gb/s Symmetric RX SuperSpeed bmSublinkSpeedAttr[1] 0x000500b0 Speed Attribute ID: 0 5Gb/s Symmetric TX SuperSpeed bmSublinkSpeedAttr[2] 0x000a4031 Speed Attribute ID: 1 10Gb/s Symmetric RX SuperSpeedPlus bmSublinkSpeedAttr[3] 0x000a40b1 Speed Attribute ID: 1 10Gb/s Symmetric TX SuperSpeedPlus Container ID Device Capability: bLength 20 bDescriptorType 16 bDevCapabilityType 4 bReserved 0 ContainerID {20b9cde5-7039-e011-a935-0002a5d5c51b} ** UNRECOGNIZED: 03 10 0b Device Status: 0x0001 Self Powered fwupd-2.0.10/plugins/rts54hub/fu-rts54hub-device.c000066400000000000000000000371451501337203100215560ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-rts54hub-device.h" struct _FuRts54HubDevice { FuUsbDevice parent_instance; gboolean fw_auth; gboolean dual_bank; gboolean running_on_flash; guint8 vendor_cmd; }; G_DEFINE_TYPE(FuRts54HubDevice, fu_rts54hub_device, FU_TYPE_USB_DEVICE) #define FU_RTS54HUB_DEVICE_TIMEOUT 1000 /* ms */ #define FU_RTS54HUB_DEVICE_TIMEOUT_RW 1000 /* ms */ #define FU_RTS54HUB_DEVICE_TIMEOUT_ERASE 5000 /* ms */ #define FU_RTS54HUB_DEVICE_TIMEOUT_AUTH 10000 /* ms */ #define FU_RTS54HUB_DEVICE_BLOCK_SIZE 4096 #define FU_RTS54HUB_DEVICE_STATUS_LEN 24 #define FU_RTS54HUB_I2C_CONFIG_REQUEST 0xF6 #define FU_RTS54HUB_I2C_WRITE_REQUEST 0xC6 #define FU_RTS54HUB_I2C_READ_REQUEST 0xD6 typedef enum { FU_RTS54HUB_VENDOR_CMD_NONE = 0x00, FU_RTS54HUB_VENDOR_CMD_STATUS = 1 << 0, FU_RTS54HUB_VENDOR_CMD_FLASH = 1 << 1, } FuRts54HubVendorCmd; static void fu_rts54hub_device_to_string(FuDevice *device, guint idt, GString *str) { FuRts54HubDevice *self = FU_RTS54HUB_DEVICE(device); fwupd_codec_string_append_bool(str, idt, "FwAuth", self->fw_auth); fwupd_codec_string_append_bool(str, idt, "DualBank", self->dual_bank); fwupd_codec_string_append_bool(str, idt, "RunningOnFlash", self->running_on_flash); } gboolean fu_rts54hub_device_i2c_config(FuRts54HubDevice *self, guint8 target_addr, guint8 sub_length, FuRts54HubI2cSpeed speed, GError **error) { guint16 value = 0; guint16 index = 0x8080; value = ((guint16)target_addr << 8) | sub_length; index += speed; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, FU_RTS54HUB_I2C_CONFIG_REQUEST, value, /* value */ index, /* idx */ NULL, 0, /* data */ NULL, /* actual */ FU_RTS54HUB_DEVICE_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to issue i2c Conf cmd 0x%02x: ", target_addr); return FALSE; } return TRUE; } gboolean fu_rts54hub_device_i2c_write(FuRts54HubDevice *self, guint32 sub_addr, const guint8 *data, gsize datasz, GError **error) { g_autofree guint8 *datarw = fu_memdup_safe(data, datasz, error); if (datarw == NULL) return FALSE; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, FU_RTS54HUB_I2C_WRITE_REQUEST, sub_addr, /* value */ 0x0000, /* idx */ datarw, datasz, /* data */ NULL, FU_RTS54HUB_DEVICE_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to write I2C: "); return FALSE; } return TRUE; } gboolean fu_rts54hub_device_i2c_read(FuRts54HubDevice *self, guint32 sub_addr, guint8 *data, gsize datasz, GError **error) { if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, FU_RTS54HUB_I2C_READ_REQUEST, 0x0000, sub_addr, data, datasz, NULL, FU_RTS54HUB_DEVICE_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to read I2C: "); return FALSE; } return TRUE; } static gboolean fu_rts54hub_device_highclockmode(FuRts54HubDevice *self, guint16 value, GError **error) { if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0x06, /* request */ value, /* value */ 0, /* idx */ NULL, 0, /* data */ NULL, /* actual */ FU_RTS54HUB_DEVICE_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to set highclockmode: "); return FALSE; } return TRUE; } static gboolean fu_rts54hub_device_reset_flash(FuRts54HubDevice *self, GError **error) { if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xC0 + 0x29, /* request */ 0x0, /* value */ 0x0, /* idx */ NULL, 0, /* data */ NULL, /* actual */ FU_RTS54HUB_DEVICE_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to reset flash: "); return FALSE; } return TRUE; } static gboolean fu_rts54hub_device_write_flash(FuRts54HubDevice *self, guint32 addr, const guint8 *data, gsize datasz, GError **error) { gsize actual_len = 0; g_autofree guint8 *datarw = NULL; /* make mutable */ datarw = fu_memdup_safe(data, datasz, error); if (datarw == NULL) return FALSE; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xC0 + 0x08, /* request */ addr % (1 << 16), /* value */ addr / (1 << 16), /* idx */ datarw, datasz, /* data */ &actual_len, FU_RTS54HUB_DEVICE_TIMEOUT_RW, NULL, error)) { g_prefix_error(error, "failed to write flash: "); return FALSE; } if (actual_len != datasz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "only wrote %" G_GSIZE_FORMAT "bytes", actual_len); return FALSE; } return TRUE; } #if 0 static gboolean fu_rts54hub_device_read_flash (FuRts54HubDevice *self, guint32 addr, guint8 *data, gsize datasz, GError **error) { gsize actual_len = 0; if (!fu_usb_device_control_transfer (FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xC0 + 0x18, /* request */ addr % (1 << 16), /* value */ addr / (1 << 16), /* idx */ data, datasz, /* data */ &actual_len, FU_RTS54HUB_DEVICE_TIMEOUT_RW, NULL, error)) { g_prefix_error (error, "failed to read flash: "); return FALSE; } if (actual_len != datasz) { g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "only read %" G_GSIZE_FORMAT "bytes", actual_len); return FALSE; } return TRUE; } #endif static gboolean fu_rts54hub_device_flash_authentication(FuRts54HubDevice *self, GError **error) { if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xC0 + 0x19, /* request */ 0x01, /* value */ 0x0, /* idx */ NULL, 0, /* data */ NULL, /* actual */ FU_RTS54HUB_DEVICE_TIMEOUT_AUTH, NULL, error)) { g_prefix_error(error, "failed to authenticate: "); return FALSE; } return TRUE; } static gboolean fu_rts54hub_device_erase_flash(FuRts54HubDevice *self, guint8 erase_type, GError **error) { if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xC0 + 0x28, /* request */ erase_type * 256, /* value */ 0x0, /* idx */ NULL, 0, /* data */ NULL, /* actual */ FU_RTS54HUB_DEVICE_TIMEOUT_ERASE, NULL, error)) { g_prefix_error(error, "failed to erase flash: "); return FALSE; } return TRUE; } gboolean fu_rts54hub_device_vendor_cmd(FuRts54HubDevice *self, guint8 value, GError **error) { /* don't set something that's already set */ if (self->vendor_cmd == value) { g_debug("skipping vendor command 0x%02x as already set", value); return TRUE; } if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0x02, /* request */ value, /* value */ 0x0bda, /* idx */ NULL, 0, /* data */ NULL, /* actual */ FU_RTS54HUB_DEVICE_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to issue vendor cmd 0x%02x: ", value); return FALSE; } self->vendor_cmd = value; return TRUE; } static gboolean fu_rts54hub_device_ensure_status(FuRts54HubDevice *self, GError **error) { guint8 data[FU_RTS54HUB_DEVICE_STATUS_LEN] = {0}; gsize actual_len = 0; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0x09, /* request */ 0x0, /* value */ 0x0, /* idx */ data, sizeof(data), &actual_len, /* actual */ FU_RTS54HUB_DEVICE_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to get status: "); return FALSE; } if (actual_len != FU_RTS54HUB_DEVICE_STATUS_LEN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "only read %" G_GSIZE_FORMAT "bytes", actual_len); return FALSE; } /* check the hardware capabilities */ self->dual_bank = (data[7] & 0x80) == 0x80; self->fw_auth = (data[13] & 0x02) > 0; self->running_on_flash = (data[15] & 0x02) > 0; return TRUE; } static gboolean fu_rts54hub_device_setup(FuDevice *device, GError **error) { FuRts54HubDevice *self = FU_RTS54HUB_DEVICE(device); /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_rts54hub_device_parent_class)->setup(device, error)) return FALSE; /* check this device is correct */ if (!fu_rts54hub_device_vendor_cmd(self, FU_RTS54HUB_VENDOR_CMD_STATUS, error)) { g_prefix_error(error, "failed to vendor enable: "); return FALSE; } if (!fu_rts54hub_device_ensure_status(self, error)) return FALSE; /* all three conditions must be set */ if (!self->running_on_flash) { fu_device_set_update_error(device, "device is abnormally running from ROM"); } else if (!self->fw_auth) { fu_device_set_update_error(device, "device does not support authentication"); } else if (!self->dual_bank) { fu_device_set_update_error(device, "device does not support dual-bank updating"); } else { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); } /* success */ return TRUE; } static gboolean fu_rts54hub_device_close(FuDevice *device, GError **error) { FuRts54HubDevice *self = FU_RTS54HUB_DEVICE(device); /* disable vendor commands */ if (self->vendor_cmd != FU_RTS54HUB_VENDOR_CMD_NONE) { if (!fu_rts54hub_device_vendor_cmd(self, FU_RTS54HUB_VENDOR_CMD_NONE, error)) { g_prefix_error(error, "failed to disable vendor command: "); return FALSE; } } /* FuUsbDevice->close */ return FU_DEVICE_CLASS(fu_rts54hub_device_parent_class)->close(device, error); } static gboolean fu_rts54hub_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuRts54HubDevice *self = FU_RTS54HUB_DEVICE(device); g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 46, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 52, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, NULL); /* get default image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* enable vendor commands */ if (!fu_rts54hub_device_vendor_cmd(self, FU_RTS54HUB_VENDOR_CMD_STATUS | FU_RTS54HUB_VENDOR_CMD_FLASH, error)) { g_prefix_error(error, "failed to cmd enable: "); return FALSE; } /* erase spare flash bank only if it is not empty */ if (!fu_rts54hub_device_erase_flash(self, 1, error)) return FALSE; fu_progress_step_done(progress); /* set MCU clock to high clock mode */ if (!fu_rts54hub_device_highclockmode(self, 0x0001, error)) { g_prefix_error(error, "failed to enable MCU clock: "); return FALSE; } /* set SPI controller clock to high clock mode */ if (!fu_rts54hub_device_highclockmode(self, 0x0101, error)) { g_prefix_error(error, "failed to enable SPI clock: "); return FALSE; } /* write each block */ chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_RTS54HUB_DEVICE_BLOCK_SIZE, error); if (chunks == NULL) return FALSE; for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; /* write chunk */ if (!fu_rts54hub_device_write_flash(self, fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; /* update progress */ fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)i + 1, (gsize)fu_chunk_array_length(chunks)); } fu_progress_step_done(progress); /* get device to authenticate the firmware */ if (!fu_rts54hub_device_flash_authentication(self, error)) return FALSE; fu_progress_step_done(progress); /* send software reset to run available flash code */ if (!fu_rts54hub_device_reset_flash(self, error)) return FALSE; fu_progress_step_done(progress); /* don't reset the vendor command enable, the device will be rebooted */ self->vendor_cmd = FU_RTS54HUB_VENDOR_CMD_NONE; /* success! */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static FuFirmware * fu_rts54hub_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { guint8 tmp = 0; g_autoptr(FuFirmware) firmware = fu_firmware_new(); if (!fu_input_stream_read_u8(stream, 0x7EF3, &tmp, error)) return NULL; if ((tmp & 0xf0) != 0x80) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware needs to be dual bank"); return NULL; } if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; return g_steal_pointer(&firmware); } static void fu_rts54hub_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 62, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 38, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_rts54hub_device_init(FuRts54HubDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "com.realtek.rts54"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); } static void fu_rts54hub_device_class_init(FuRts54HubDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_rts54hub_device_write_firmware; device_class->setup = fu_rts54hub_device_setup; device_class->to_string = fu_rts54hub_device_to_string; device_class->prepare_firmware = fu_rts54hub_device_prepare_firmware; device_class->close = fu_rts54hub_device_close; device_class->set_progress = fu_rts54hub_device_set_progress; } fwupd-2.0.10/plugins/rts54hub/fu-rts54hub-device.h000066400000000000000000000022571501337203100215570ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_RTS54HUB_DEVICE (fu_rts54hub_device_get_type()) typedef enum { FU_RTS54HUB_I2C_SPEED_100K, FU_RTS54HUB_I2C_SPEED_200K, FU_RTS54HUB_I2C_SPEED_300K, FU_RTS54HUB_I2C_SPEED_400K, FU_RTS54HUB_I2C_SPEED_500K, FU_RTS54HUB_I2C_SPEED_600K, FU_RTS54HUB_I2C_SPEED_700K, FU_RTS54HUB_I2C_SPEED_800K, FU_RTS54HUB_I2C_SPEED_LAST } FuRts54HubI2cSpeed; G_DECLARE_FINAL_TYPE(FuRts54HubDevice, fu_rts54hub_device, FU, RTS54HUB_DEVICE, FuUsbDevice) gboolean fu_rts54hub_device_vendor_cmd(FuRts54HubDevice *self, guint8 value, GError **error); gboolean fu_rts54hub_device_i2c_config(FuRts54HubDevice *self, guint8 target_addr, guint8 sub_length, FuRts54HubI2cSpeed speed, GError **error); gboolean fu_rts54hub_device_i2c_write(FuRts54HubDevice *self, guint32 sub_addr, const guint8 *data, gsize datasz, GError **error); gboolean fu_rts54hub_device_i2c_read(FuRts54HubDevice *self, guint32 sub_addr, guint8 *data, gsize datasz, GError **error); fwupd-2.0.10/plugins/rts54hub/fu-rts54hub-plugin.c000066400000000000000000000026521501337203100216100ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-rts54hub-device.h" #include "fu-rts54hub-plugin.h" #include "fu-rts54hub-rtd21xx-background.h" #include "fu-rts54hub-rtd21xx-foreground.h" struct _FuRts54HubPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuRts54HubPlugin, fu_rts54hub_plugin, FU_TYPE_PLUGIN) static void fu_rts54hub_plugin_init(FuRts54HubPlugin *self) { } static void fu_rts54hub_plugin_object_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_set_name(plugin, "rts54hub"); } static void fu_rts54hub_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "Rts54TargetAddr"); fu_context_add_quirk_key(ctx, "Rts54I2cSpeed"); fu_context_add_quirk_key(ctx, "Rts54RegisterAddrLen"); fu_plugin_add_device_gtype(plugin, FU_TYPE_RTS54HUB_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_RTS54HUB_RTD21XX_BACKGROUND); fu_plugin_add_device_gtype(plugin, FU_TYPE_RTS54HUB_RTD21XX_FOREGROUND); } static void fu_rts54hub_plugin_class_init(FuRts54HubPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->constructed = fu_rts54hub_plugin_object_constructed; plugin_class->constructed = fu_rts54hub_plugin_constructed; } fwupd-2.0.10/plugins/rts54hub/fu-rts54hub-plugin.h000066400000000000000000000003621501337203100216110ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuRts54HubPlugin, fu_rts54hub_plugin, FU, RTS54HUB_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/rts54hub/fu-rts54hub-rtd21xx-background.c000066400000000000000000000251251501337203100237430ustar00rootroot00000000000000/* * Copyright 2021 Realtek Corporation * Copyright 2021 Ricky Wu * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-rts54hub-device.h" #include "fu-rts54hub-rtd21xx-background.h" struct _FuRts54hubRtd21xxBackground { FuRts54hubRtd21xxDevice parent_instance; }; G_DEFINE_TYPE(FuRts54hubRtd21xxBackground, fu_rts54hub_rtd21xx_background, FU_TYPE_RTS54HUB_RTD21XX_DEVICE) #define ISP_DATA_BLOCKSIZE 32 #define ISP_PACKET_SIZE 257 #define FU_RTS54HUB_RTD21XX_BACKGROUND_DETACH_RETRY_COUNT 10 #define FU_RTS54HUB_RTD21XX_BACKGROUND_DETACH_RETRY_DELAY 300 /* ms */ typedef enum { ISP_CMD_FW_UPDATE_START = 0x01, ISP_CMD_FW_UPDATE_ISP_DONE = 0x02, ISP_CMD_GET_FW_INFO = 0x03, ISP_CMD_FW_UPDATE_EXIT = 0x04, ISP_CMD_GET_PROJECT_ID_ADDR = 0x05, ISP_CMD_SYNC_IDENTIFY_CODE = 0x06, } IspCmd; static gboolean fu_rts54hub_rtd21xx_background_ensure_version_unlocked(FuRts54hubRtd21xxBackground *self, GError **error) { guint8 buf_rep[7] = {0x00}; guint8 buf_req[] = {ISP_CMD_GET_FW_INFO}; g_autofree gchar *version = NULL; if (!fu_rts54hub_rtd21xx_device_i2c_write(FU_RTS54HUB_RTD21XX_DEVICE(self), UC_ISP_TARGET_ADDR, UC_BACKGROUND_OPCODE, buf_req, sizeof(buf_req), error)) { g_prefix_error(error, "failed to get version number: "); return FALSE; } if (!fu_rts54hub_rtd21xx_device_i2c_read(FU_RTS54HUB_RTD21XX_DEVICE(self), UC_ISP_TARGET_ADDR, 0x00, buf_rep, sizeof(buf_rep), error)) { g_prefix_error(error, "failed to get version number: "); return FALSE; } /* set version */ version = g_strdup_printf("%u.%u", buf_rep[1], buf_rep[2]); fu_device_set_version(FU_DEVICE(self), version); return TRUE; } static gboolean fu_rts54hub_rtd21xx_background_detach_raw(FuRts54hubRtd21xxBackground *self, GError **error) { guint8 buf[] = {ISP_CMD_FW_UPDATE_ISP_DONE}; if (!fu_rts54hub_rtd21xx_device_i2c_write(FU_RTS54HUB_RTD21XX_DEVICE(self), 0x6A, UC_BACKGROUND_OPCODE, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to detach: "); return FALSE; } return TRUE; } static gboolean fu_rts54hub_rtd21xx_background_detach_cb(FuDevice *device, gpointer user_data, GError **error) { FuRts54hubRtd21xxBackground *self = FU_RTS54HUB_RTD21XX_BACKGROUND(device); guint8 status = 0xfe; if (!fu_rts54hub_rtd21xx_background_detach_raw(self, error)) return FALSE; if (!fu_rts54hub_rtd21xx_device_read_status_raw(FU_RTS54HUB_RTD21XX_DEVICE(self), &status, error)) return FALSE; if (status != ISP_STATUS_IDLE_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "detach status was 0x%02x", status); return FALSE; } /* success */ return TRUE; } static gboolean fu_rts54hub_rtd21xx_background_detach(FuDevice *device, FuProgress *progress, GError **error) { FuRts54HubDevice *parent = FU_RTS54HUB_DEVICE(fu_device_get_parent(device)); g_autoptr(FuDeviceLocker) locker = NULL; /* open device */ locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; return fu_device_retry_full(device, fu_rts54hub_rtd21xx_background_detach_cb, FU_RTS54HUB_RTD21XX_BACKGROUND_DETACH_RETRY_COUNT, FU_RTS54HUB_RTD21XX_BACKGROUND_DETACH_RETRY_DELAY, NULL, error); } static gboolean fu_rts54hub_rtd21xx_background_attach(FuDevice *device, FuProgress *progress, GError **error) { FuRts54HubDevice *parent = FU_RTS54HUB_DEVICE(fu_device_get_parent(device)); FuRts54hubRtd21xxDevice *self = FU_RTS54HUB_RTD21XX_DEVICE(device); g_autoptr(FuDeviceLocker) locker = NULL; guint8 buf[] = {ISP_CMD_FW_UPDATE_EXIT}; /* open device */ locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; if (!fu_rts54hub_rtd21xx_device_i2c_write(self, UC_ISP_TARGET_ADDR, UC_BACKGROUND_OPCODE, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to attach: "); return FALSE; } fu_device_sleep_full(device, 1000, progress); /* ms */ /* success */ return TRUE; } static gboolean fu_rts54hub_rtd21xx_background_setup(FuDevice *device, GError **error) { FuRts54hubRtd21xxBackground *self = FU_RTS54HUB_RTD21XX_BACKGROUND(device); g_autoptr(FuDeviceLocker) locker = NULL; /* get version */ locker = fu_device_locker_new_full(device, (FuDeviceLockerFunc)fu_device_detach, (FuDeviceLockerFunc)fu_device_attach, error); if (locker == NULL) return FALSE; if (!fu_rts54hub_rtd21xx_background_ensure_version_unlocked(self, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_rts54hub_rtd21xx_background_reload(FuDevice *device, GError **error) { FuRts54HubDevice *parent = FU_RTS54HUB_DEVICE(fu_device_get_parent(device)); g_autoptr(FuDeviceLocker) locker = NULL; /* open parent device */ locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; return fu_rts54hub_rtd21xx_background_setup(device, error); } static gboolean fu_rts54hub_rtd21xx_background_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuRts54hubRtd21xxBackground *self = FU_RTS54HUB_RTD21XX_BACKGROUND(device); guint32 project_addr; guint8 project_id_count; guint8 read_buf[10] = {0x0}; guint8 write_buf[ISP_PACKET_SIZE] = {0x0}; g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 5, "setup"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 90, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 5, "exit"); /* open device */ locker = fu_device_locker_new(self, error); if (locker == NULL) return FALSE; /* simple image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* get project ID address */ write_buf[0] = ISP_CMD_GET_PROJECT_ID_ADDR; if (!fu_rts54hub_rtd21xx_device_i2c_write(FU_RTS54HUB_RTD21XX_DEVICE(self), UC_ISP_TARGET_ADDR, UC_BACKGROUND_OPCODE, write_buf, 1, error)) { g_prefix_error(error, "failed to get project ID address: "); return FALSE; } /* read back 6 bytes data */ fu_device_sleep(device, I2C_DELAY_AFTER_SEND * 40); if (!fu_rts54hub_rtd21xx_device_i2c_read(FU_RTS54HUB_RTD21XX_DEVICE(self), UC_ISP_TARGET_ADDR, 0x00, read_buf, 6, error)) { g_prefix_error(error, "failed to read project ID: "); return FALSE; } if (read_buf[0] != ISP_STATUS_IDLE_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed project ID with error 0x%02x: ", read_buf[0]); return FALSE; } /* verify project ID */ project_addr = fu_memread_uint32(read_buf + 1, G_BIG_ENDIAN); project_id_count = read_buf[5]; write_buf[0] = ISP_CMD_SYNC_IDENTIFY_CODE; if (!fu_input_stream_read_safe(stream, write_buf, sizeof(write_buf), 0x1, /* dst */ project_addr, /* src */ project_id_count, error)) { g_prefix_error(error, "failed to write project ID from 0x%04x: ", project_addr); return FALSE; } if (!fu_rts54hub_rtd21xx_device_i2c_write(FU_RTS54HUB_RTD21XX_DEVICE(self), UC_ISP_TARGET_ADDR, UC_BACKGROUND_OPCODE, write_buf, project_id_count + 1, error)) { g_prefix_error(error, "failed to send fw update start cmd: "); return FALSE; } if (!fu_rts54hub_rtd21xx_device_read_status(FU_RTS54HUB_RTD21XX_DEVICE(self), NULL, error)) return FALSE; /* background FW update start command */ write_buf[0] = ISP_CMD_FW_UPDATE_START; fu_memwrite_uint16(write_buf + 1, ISP_DATA_BLOCKSIZE, G_BIG_ENDIAN); if (!fu_rts54hub_rtd21xx_device_i2c_write(FU_RTS54HUB_RTD21XX_DEVICE(self), UC_ISP_TARGET_ADDR, UC_BACKGROUND_OPCODE, write_buf, 3, error)) { g_prefix_error(error, "failed to send fw update start cmd: "); return FALSE; } fu_progress_step_done(progress); /* send data */ chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, ISP_DATA_BLOCKSIZE, error); if (chunks == NULL) return FALSE; for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_rts54hub_rtd21xx_device_read_status(FU_RTS54HUB_RTD21XX_DEVICE(self), NULL, error)) return FALSE; if (!fu_rts54hub_rtd21xx_device_i2c_write(FU_RTS54HUB_RTD21XX_DEVICE(self), UC_ISP_TARGET_ADDR, UC_BACKGROUND_ISP_DATA_OPCODE, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "failed to write @0x%04x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } /* update progress */ fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)i + 1, (gsize)fu_chunk_array_length(chunks)); } fu_progress_step_done(progress); /* update finish command */ if (!fu_rts54hub_rtd21xx_device_read_status(FU_RTS54HUB_RTD21XX_DEVICE(self), NULL, error)) return FALSE; write_buf[0] = ISP_CMD_FW_UPDATE_ISP_DONE; if (!fu_rts54hub_rtd21xx_device_i2c_write(FU_RTS54HUB_RTD21XX_DEVICE(self), UC_ISP_TARGET_ADDR, UC_BACKGROUND_OPCODE, write_buf, 1, error)) { g_prefix_error(error, "failed update finish cmd: "); return FALSE; } /* exit fw mode */ if (!fu_rts54hub_rtd21xx_device_read_status(FU_RTS54HUB_RTD21XX_DEVICE(self), NULL, error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static void fu_rts54hub_rtd21xx_background_init(FuRts54hubRtd21xxBackground *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE); } static void fu_rts54hub_rtd21xx_background_class_init(FuRts54hubRtd21xxBackgroundClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_rts54hub_rtd21xx_background_setup; device_class->reload = fu_rts54hub_rtd21xx_background_reload; device_class->attach = fu_rts54hub_rtd21xx_background_attach; device_class->detach = fu_rts54hub_rtd21xx_background_detach; device_class->write_firmware = fu_rts54hub_rtd21xx_background_write_firmware; } fwupd-2.0.10/plugins/rts54hub/fu-rts54hub-rtd21xx-background.h000066400000000000000000000010121501337203100237350ustar00rootroot00000000000000/* * Copyright 2021 Ricky Wu * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-rts54hub-rtd21xx-device.h" #define FU_TYPE_RTS54HUB_RTD21XX_BACKGROUND (fu_rts54hub_rtd21xx_background_get_type()) G_DECLARE_FINAL_TYPE(FuRts54hubRtd21xxBackground, fu_rts54hub_rtd21xx_background, FU, RTS54HUB_RTD21XX_BACKGROUND, FuRts54hubRtd21xxDevice) fwupd-2.0.10/plugins/rts54hub/fu-rts54hub-rtd21xx-device.c000066400000000000000000000150311501337203100230560ustar00rootroot00000000000000/* * Copyright 2021 Realtek Corporation * Copyright 2021 Ricky Wu * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-rts54hub-device.h" #include "fu-rts54hub-rtd21xx-device.h" typedef struct { guint8 target_addr; guint8 i2c_speed; guint8 register_addr_len; } FuRts54hubRtd21xxDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuRts54hubRtd21xxDevice, fu_rts54hub_rtd21xx_device, FU_TYPE_DEVICE) #define GET_PRIVATE(o) (fu_rts54hub_rtd21xx_device_get_instance_private(o)) typedef enum { VENDOR_CMD_DISABLE = 0x00, VENDOR_CMD_ENABLE = 0x01, VENDOR_CMD_ACCESS_FLASH = 0x02, } VendorCmd; static void fu_rts54hub_rtd21xx_device_to_string(FuDevice *module, guint idt, GString *str) { FuRts54hubRtd21xxDevice *self = FU_RTS54HUB_RTD21XX_DEVICE(module); FuRts54hubRtd21xxDevicePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append_hex(str, idt, "TargetAddr", priv->target_addr); fwupd_codec_string_append_hex(str, idt, "I2cSpeed", priv->i2c_speed); fwupd_codec_string_append_hex(str, idt, "RegisterAddrLen", priv->register_addr_len); } static FuRts54HubDevice * fu_rts54hub_rtd21xx_device_get_parent(FuRts54hubRtd21xxDevice *self, GError **error) { FuDevice *parent = fu_device_get_parent(FU_DEVICE(self)); if (parent == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "no parent set"); return NULL; } return FU_RTS54HUB_DEVICE(parent); } static gboolean fu_rts54hub_rtd21xx_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuRts54hubRtd21xxDevice *self = FU_RTS54HUB_RTD21XX_DEVICE(device); FuRts54hubRtd21xxDevicePrivate *priv = GET_PRIVATE(self); guint64 tmp = 0; /* load target address from quirks */ if (g_strcmp0(key, "Rts54TargetAddr") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->target_addr = tmp; return TRUE; } /* load i2c speed from quirks */ if (g_strcmp0(key, "Rts54I2cSpeed") == 0) { if (!fu_strtoull(value, &tmp, 0, FU_RTS54HUB_I2C_SPEED_LAST - 1, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->i2c_speed = tmp; return TRUE; } /* load register address length from quirks */ if (g_strcmp0(key, "Rts54RegisterAddrLen") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->register_addr_len = tmp; return TRUE; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } gboolean fu_rts54hub_rtd21xx_device_i2c_write(FuRts54hubRtd21xxDevice *self, guint8 target_addr, guint8 sub_addr, const guint8 *data, gsize datasz, GError **error) { FuRts54HubDevice *parent; FuRts54hubRtd21xxDevicePrivate *priv = GET_PRIVATE(self); parent = fu_rts54hub_rtd21xx_device_get_parent(self, error); if (parent == NULL) return FALSE; if (!fu_rts54hub_device_vendor_cmd(parent, VENDOR_CMD_ENABLE, error)) return FALSE; if (target_addr != priv->target_addr) { if (!fu_rts54hub_device_i2c_config(parent, target_addr, 1, FU_RTS54HUB_I2C_SPEED_200K, error)) return FALSE; priv->target_addr = target_addr; } if (!fu_rts54hub_device_i2c_write(parent, sub_addr, data, datasz, error)) { g_prefix_error(error, "failed to write I2C @0x%02x:%02x: ", target_addr, sub_addr); return FALSE; } fu_device_sleep(FU_DEVICE(self), I2C_DELAY_AFTER_SEND); return TRUE; } gboolean fu_rts54hub_rtd21xx_device_i2c_read(FuRts54hubRtd21xxDevice *self, guint8 target_addr, guint8 sub_addr, guint8 *data, gsize datasz, GError **error) { FuRts54HubDevice *parent; FuRts54hubRtd21xxDevicePrivate *priv = GET_PRIVATE(self); parent = fu_rts54hub_rtd21xx_device_get_parent(self, error); if (parent == NULL) return FALSE; if (!fu_rts54hub_device_vendor_cmd(parent, VENDOR_CMD_ENABLE, error)) return FALSE; if (target_addr != priv->target_addr) { if (!fu_rts54hub_device_i2c_config(parent, target_addr, 1, FU_RTS54HUB_I2C_SPEED_200K, error)) return FALSE; priv->target_addr = target_addr; } if (!fu_rts54hub_device_i2c_read(parent, sub_addr, data, datasz, error)) { g_prefix_error(error, "failed to read I2C: "); return FALSE; } return TRUE; } gboolean fu_rts54hub_rtd21xx_device_read_status_raw(FuRts54hubRtd21xxDevice *self, guint8 *status, GError **error) { guint8 buf = 0x00; if (!fu_rts54hub_rtd21xx_device_i2c_read(self, UC_ISP_TARGET_ADDR, UC_FOREGROUND_STATUS, &buf, sizeof(buf), error)) return FALSE; if (status != NULL) *status = buf; return TRUE; } static gboolean fu_rts54hub_rtd21xx_device_read_status_cb(FuDevice *device, gpointer user_data, GError **error) { FuRts54hubRtd21xxDevice *self = FU_RTS54HUB_RTD21XX_DEVICE(device); guint8 status = 0xfd; if (!fu_rts54hub_rtd21xx_device_read_status_raw(self, &status, error)) return FALSE; if (status == ISP_STATUS_BUSY) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "status was 0x%02x", status); return FALSE; } return TRUE; } gboolean fu_rts54hub_rtd21xx_device_read_status(FuRts54hubRtd21xxDevice *self, guint8 *status, GError **error) { return fu_device_retry(FU_DEVICE(self), fu_rts54hub_rtd21xx_device_read_status_cb, 4200, status, error); } static void fu_rts54hub_rtd21xx_device_init(FuRts54hubRtd21xxDevice *self) { fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_VIDEO_DISPLAY); fu_device_add_protocol(FU_DEVICE(self), "com.realtek.rts54.i2c"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_OPEN); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PAIR); fu_device_set_install_duration(FU_DEVICE(self), 100); /* seconds */ fu_device_set_logical_id(FU_DEVICE(self), "I2C"); fu_device_retry_set_delay(FU_DEVICE(self), 30); /* ms */ } static void fu_rts54hub_rtd21xx_device_class_init(FuRts54hubRtd21xxDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_rts54hub_rtd21xx_device_to_string; device_class->set_quirk_kv = fu_rts54hub_rtd21xx_device_set_quirk_kv; } fwupd-2.0.10/plugins/rts54hub/fu-rts54hub-rtd21xx-device.h000066400000000000000000000032241501337203100230640ustar00rootroot00000000000000/* * Copyright 2021 Ricky Wu * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_RTS54HUB_RTD21XX_DEVICE (fu_rts54hub_rtd21xx_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuRts54hubRtd21xxDevice, fu_rts54hub_rtd21xx_device, FU, RTS54HUB_RTD21XX_DEVICE, FuDevice) struct _FuRts54hubRtd21xxDeviceClass { FuDeviceClass parent_class; }; #define I2C_DELAY_AFTER_SEND 5 /* ms */ #define UC_ISP_TARGET_ADDR 0x3A #define UC_FOREGROUND_STATUS 0x31 #define UC_FOREGROUND_OPCODE 0x33 #define UC_FOREGROUND_ISP_DATA_OPCODE 0x34 #define UC_BACKGROUND_OPCODE 0x31 #define UC_BACKGROUND_ISP_DATA_OPCODE 0x32 typedef enum { ISP_STATUS_BUSY = 0xBB, /* host must wait for device */ ISP_STATUS_IDLE_SUCCESS = 0x11, /* previous command was OK */ ISP_STATUS_IDLE_FAILURE = 0x12, /* previous command failed */ } IspStatus; gboolean fu_rts54hub_rtd21xx_device_read_status(FuRts54hubRtd21xxDevice *self, guint8 *status, GError **error); gboolean fu_rts54hub_rtd21xx_device_read_status_raw(FuRts54hubRtd21xxDevice *self, guint8 *status, GError **error); gboolean fu_rts54hub_rtd21xx_device_i2c_read(FuRts54hubRtd21xxDevice *self, guint8 target_addr, guint8 sub_addr, guint8 *data, gsize datasz, GError **error); gboolean fu_rts54hub_rtd21xx_device_i2c_write(FuRts54hubRtd21xxDevice *self, guint8 target_addr, guint8 sub_addr, const guint8 *data, gsize datasz, GError **error); fwupd-2.0.10/plugins/rts54hub/fu-rts54hub-rtd21xx-foreground.c000066400000000000000000000274321501337203100240010ustar00rootroot00000000000000/* * Copyright 2021 Realtek Corporation * Copyright 2021 Ricky Wu * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-rts54hub-device.h" #include "fu-rts54hub-rtd21xx-foreground.h" struct _FuRts54hubRtd21xxForeground { FuRts54hubRtd21xxDevice parent_instance; }; G_DEFINE_TYPE(FuRts54hubRtd21xxForeground, fu_rts54hub_rtd21xx_foreground, FU_TYPE_RTS54HUB_RTD21XX_DEVICE) #define ISP_DATA_BLOCKSIZE 256 #define ISP_PACKET_SIZE 257 typedef enum { ISP_CMD_ENTER_FW_UPDATE = 0x01, ISP_CMD_GET_PROJECT_ID_ADDR = 0x02, ISP_CMD_SYNC_IDENTIFY_CODE = 0x03, ISP_CMD_GET_FW_INFO = 0x04, ISP_CMD_FW_UPDATE_START = 0x05, ISP_CMD_FW_UPDATE_ISP_DONE = 0x06, ISP_CMD_FW_UPDATE_RESET = 0x07, ISP_CMD_FW_UPDATE_EXIT = 0x08, } IspCmd; static gboolean fu_rts54hub_rtd21xx_foreground_ensure_version_unlocked(FuRts54hubRtd21xxForeground *self, GError **error) { guint8 buf_rep[7] = {0x00}; guint8 buf_req[] = {ISP_CMD_GET_FW_INFO}; g_autofree gchar *version = NULL; if (!fu_rts54hub_rtd21xx_device_i2c_write(FU_RTS54HUB_RTD21XX_DEVICE(self), UC_ISP_TARGET_ADDR, UC_FOREGROUND_OPCODE, buf_req, sizeof(buf_req), error)) { g_prefix_error(error, "failed to get version number: "); return FALSE; } /* wait for device ready */ fu_device_sleep(FU_DEVICE(self), 300); /* ms */ if (!fu_rts54hub_rtd21xx_device_i2c_read(FU_RTS54HUB_RTD21XX_DEVICE(self), UC_ISP_TARGET_ADDR, 0x00, buf_rep, sizeof(buf_rep), error)) { g_prefix_error(error, "failed to get version number: "); return FALSE; } /* set version */ version = g_strdup_printf("%u.%u", buf_rep[1], buf_rep[2]); fu_device_set_version(FU_DEVICE(self), version); return TRUE; } static gboolean fu_rts54hub_rtd21xx_foreground_detach_raw(FuRts54hubRtd21xxForeground *self, GError **error) { guint8 buf = 0x03; if (!fu_rts54hub_rtd21xx_device_i2c_write(FU_RTS54HUB_RTD21XX_DEVICE(self), 0x6A, 0x31, &buf, sizeof(buf), error)) { g_prefix_error(error, "failed to detach: "); return FALSE; } /* wait for device ready */ fu_device_sleep(FU_DEVICE(self), 300); /* ms */ return TRUE; } static gboolean fu_rts54hub_rtd21xx_foreground_detach_cb(FuDevice *device, gpointer user_data, GError **error) { FuRts54hubRtd21xxForeground *self = FU_RTS54HUB_RTD21XX_FOREGROUND(device); guint8 status = 0xfe; if (!fu_rts54hub_rtd21xx_foreground_detach_raw(self, error)) return FALSE; if (!fu_rts54hub_rtd21xx_device_read_status_raw(FU_RTS54HUB_RTD21XX_DEVICE(self), &status, error)) return FALSE; if (status != ISP_STATUS_IDLE_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "detach status was 0x%02x", status); return FALSE; } /* success */ return TRUE; } static gboolean fu_rts54hub_rtd21xx_foreground_detach(FuDevice *device, FuProgress *progress, GError **error) { FuRts54HubDevice *parent = FU_RTS54HUB_DEVICE(fu_device_get_parent(device)); g_autoptr(FuDeviceLocker) locker = NULL; /* open device */ locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; return fu_device_retry(device, fu_rts54hub_rtd21xx_foreground_detach_cb, 100, NULL, error); } static gboolean fu_rts54hub_rtd21xx_foreground_attach(FuDevice *device, FuProgress *progress, GError **error) { FuRts54HubDevice *parent = FU_RTS54HUB_DEVICE(fu_device_get_parent(device)); FuRts54hubRtd21xxForeground *self = FU_RTS54HUB_RTD21XX_FOREGROUND(device); guint8 buf[] = {ISP_CMD_FW_UPDATE_RESET}; g_autoptr(FuDeviceLocker) locker = NULL; /* open device */ locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; /* exit fw mode */ if (!fu_rts54hub_rtd21xx_device_read_status(FU_RTS54HUB_RTD21XX_DEVICE(self), NULL, error)) return FALSE; if (!fu_rts54hub_rtd21xx_device_i2c_write(FU_RTS54HUB_RTD21XX_DEVICE(self), UC_ISP_TARGET_ADDR, UC_FOREGROUND_OPCODE, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to ISP_CMD_FW_UPDATE_RESET: "); return FALSE; } /* the device needs some time to restart with the new firmware before * it can be queried again */ fu_device_sleep_full(device, 60000, progress); /* ms */ /* success */ return TRUE; } static gboolean fu_rts54hub_rtd21xx_foreground_exit(FuDevice *device, GError **error) { FuRts54HubDevice *parent = FU_RTS54HUB_DEVICE(fu_device_get_parent(device)); FuRts54hubRtd21xxForeground *self = FU_RTS54HUB_RTD21XX_FOREGROUND(device); guint8 buf[] = {ISP_CMD_FW_UPDATE_EXIT}; g_autoptr(FuDeviceLocker) locker = NULL; /* open device */ locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; if (!fu_rts54hub_rtd21xx_device_i2c_write(FU_RTS54HUB_RTD21XX_DEVICE(self), UC_ISP_TARGET_ADDR, UC_FOREGROUND_OPCODE, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to ISP_CMD_FW_UPDATE_EXIT: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_rts54hub_rtd21xx_foreground_setup(FuDevice *device, GError **error) { FuRts54hubRtd21xxForeground *self = FU_RTS54HUB_RTD21XX_FOREGROUND(device); g_autoptr(FuDeviceLocker) locker = NULL; /* get version */ locker = fu_device_locker_new_full(device, (FuDeviceLockerFunc)fu_device_detach, (FuDeviceLockerFunc)fu_rts54hub_rtd21xx_foreground_exit, error); if (locker == NULL) return FALSE; if (!fu_rts54hub_rtd21xx_foreground_ensure_version_unlocked(self, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_rts54hub_rtd21xx_foreground_reload(FuDevice *device, GError **error) { FuRts54HubDevice *parent = FU_RTS54HUB_DEVICE(fu_device_get_parent(device)); g_autoptr(FuDeviceLocker) locker = NULL; /* open parent device */ locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; return fu_rts54hub_rtd21xx_foreground_setup(device, error); } static gboolean fu_rts54hub_rtd21xx_foreground_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuRts54hubRtd21xxForeground *self = FU_RTS54HUB_RTD21XX_FOREGROUND(device); guint32 project_addr; guint8 project_id_count; guint8 read_buf[10] = {0x0}; guint8 write_buf[ISP_PACKET_SIZE] = {0x0}; g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 5, "setup"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 90, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 5, "finish"); /* open device */ locker = fu_device_locker_new(self, error); if (locker == NULL) return FALSE; /* simple image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* enable ISP high priority */ write_buf[0] = ISP_CMD_ENTER_FW_UPDATE; write_buf[1] = 0x01; if (!fu_rts54hub_rtd21xx_device_i2c_write(FU_RTS54HUB_RTD21XX_DEVICE(self), UC_ISP_TARGET_ADDR, UC_FOREGROUND_OPCODE, write_buf, 2, error)) { g_prefix_error(error, "failed to enable ISP: "); return FALSE; } if (!fu_rts54hub_rtd21xx_device_read_status(FU_RTS54HUB_RTD21XX_DEVICE(self), NULL, error)) return FALSE; /* get project ID address */ write_buf[0] = ISP_CMD_GET_PROJECT_ID_ADDR; if (!fu_rts54hub_rtd21xx_device_i2c_write(FU_RTS54HUB_RTD21XX_DEVICE(self), UC_ISP_TARGET_ADDR, UC_FOREGROUND_OPCODE, write_buf, 1, error)) { g_prefix_error(error, "failed to get project ID address: "); return FALSE; } /* read back 6 bytes data */ fu_device_sleep(FU_DEVICE(self), I2C_DELAY_AFTER_SEND * 40); if (!fu_rts54hub_rtd21xx_device_i2c_read(FU_RTS54HUB_RTD21XX_DEVICE(self), UC_ISP_TARGET_ADDR, UC_FOREGROUND_STATUS, read_buf, 6, error)) { g_prefix_error(error, "failed to read project ID: "); return FALSE; } if (read_buf[0] != ISP_STATUS_IDLE_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed project ID with error 0x%02x: ", read_buf[0]); return FALSE; } /* verify project ID */ project_addr = fu_memread_uint32(read_buf + 1, G_BIG_ENDIAN); project_id_count = read_buf[5]; write_buf[0] = ISP_CMD_SYNC_IDENTIFY_CODE; if (!fu_input_stream_read_safe(stream, write_buf, sizeof(write_buf), 0x1, /* dst */ project_addr, /* src */ project_id_count, error)) { g_prefix_error(error, "failed to write project ID from 0x%04x: ", project_addr); return FALSE; } if (!fu_rts54hub_rtd21xx_device_i2c_write(FU_RTS54HUB_RTD21XX_DEVICE(self), UC_ISP_TARGET_ADDR, UC_FOREGROUND_OPCODE, write_buf, project_id_count + 1, error)) { g_prefix_error(error, "failed to send fw update start cmd: "); return FALSE; } if (!fu_rts54hub_rtd21xx_device_read_status(FU_RTS54HUB_RTD21XX_DEVICE(self), NULL, error)) return FALSE; /* foreground FW update start command */ write_buf[0] = ISP_CMD_FW_UPDATE_START; fu_memwrite_uint16(write_buf + 1, ISP_DATA_BLOCKSIZE, G_BIG_ENDIAN); if (!fu_rts54hub_rtd21xx_device_i2c_write(FU_RTS54HUB_RTD21XX_DEVICE(self), UC_ISP_TARGET_ADDR, UC_FOREGROUND_OPCODE, write_buf, 3, error)) { g_prefix_error(error, "failed to send fw update start cmd: "); return FALSE; } fu_progress_step_done(progress); /* send data */ chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, ISP_DATA_BLOCKSIZE, error); if (chunks == NULL) return FALSE; for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_rts54hub_rtd21xx_device_read_status(FU_RTS54HUB_RTD21XX_DEVICE(self), NULL, error)) return FALSE; if (!fu_rts54hub_rtd21xx_device_i2c_write(FU_RTS54HUB_RTD21XX_DEVICE(self), UC_ISP_TARGET_ADDR, UC_FOREGROUND_ISP_DATA_OPCODE, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "failed to write @0x%04x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } /* update progress */ fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)i + 1, (gsize)fu_chunk_array_length(chunks)); } fu_progress_step_done(progress); /* update finish command */ if (!fu_rts54hub_rtd21xx_device_read_status(FU_RTS54HUB_RTD21XX_DEVICE(self), NULL, error)) return FALSE; write_buf[0] = ISP_CMD_FW_UPDATE_ISP_DONE; if (!fu_rts54hub_rtd21xx_device_i2c_write(FU_RTS54HUB_RTD21XX_DEVICE(self), UC_ISP_TARGET_ADDR, UC_FOREGROUND_OPCODE, write_buf, 1, error)) { g_prefix_error(error, "failed update finish cmd: "); return FALSE; } fu_progress_step_done(progress); /* success */ return TRUE; } static void fu_rts54hub_rtd21xx_foreground_init(FuRts54hubRtd21xxForeground *self) { } static void fu_rts54hub_rtd21xx_foreground_class_init(FuRts54hubRtd21xxForegroundClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_rts54hub_rtd21xx_foreground_setup; device_class->reload = fu_rts54hub_rtd21xx_foreground_reload; device_class->attach = fu_rts54hub_rtd21xx_foreground_attach; device_class->detach = fu_rts54hub_rtd21xx_foreground_detach; device_class->write_firmware = fu_rts54hub_rtd21xx_foreground_write_firmware; } fwupd-2.0.10/plugins/rts54hub/fu-rts54hub-rtd21xx-foreground.h000066400000000000000000000010121501337203100237700ustar00rootroot00000000000000/* * Copyright 2021 Ricky Wu * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-rts54hub-rtd21xx-device.h" #define FU_TYPE_RTS54HUB_RTD21XX_FOREGROUND (fu_rts54hub_rtd21xx_foreground_get_type()) G_DECLARE_FINAL_TYPE(FuRts54hubRtd21xxForeground, fu_rts54hub_rtd21xx_foreground, FU, RTS54HUB_RTD21XX_FOREGROUND, FuRts54hubRtd21xxDevice) fwupd-2.0.10/plugins/rts54hub/meson.build000066400000000000000000000010661501337203100202200ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginRts54Hub"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('rts54hub.quirk') plugin_builtins += static_library('fu_plugin_rts54hub', sources: [ 'fu-rts54hub-device.c', 'fu-rts54hub-rtd21xx-device.c', 'fu-rts54hub-rtd21xx-background.c', 'fu-rts54hub-rtd21xx-foreground.c', 'fu-rts54hub-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files( 'tests/realtek-rts5423.json', ) fwupd-2.0.10/plugins/rts54hub/rts54hub.quirk000066400000000000000000000051101501337203100206050ustar00rootroot00000000000000# RTS5423 Development Board [USB\VID_0BDA&PID_5B00] Plugin = rts54hub Flags = enforce-requires GType = FuRts54HubDevice FirmwareSizeMin = 0x20000 FirmwareSizeMax = 0x40000 # Lenovo HotRod [USB\VID_17EF&PID_30BF] Plugin = rts54hub GType = FuRts54HubDevice Vendor = Lenovo FirmwareSizeMin = 0x20000 FirmwareSizeMax = 0x40000 Children = FuRts54hubRtd21xxForeground|USB\VID_17EF&PID_30BF&I2C_01 [USB\VID_17EF&PID_30BF&I2C_01] Plugin = rts54hub Name = HDMI Converter Flags = updatable FirmwareSize = 0x30000 Rts54TargetAddr = 0x20 Rts54I2cSpeed = 0x2 Rts54RegisterAddrLen = 0x04 # Acer D501 Dock [USB\VID_2BEF&PID_1009] Plugin = rts54hub GType = FuRts54HubDevice Name = Acer D501 Dock USB Hub FirmwareSizeMin = 0x10000 FirmwareSizeMax = 0x40000 Children = FuRts54hubRtd21xxBackground|USB\VID_2BEF&PID_1009&I2C_01 [USB\VID_2BEF&PID_1009&I2C_01] Plugin = rts54hub Vendor = Realtek Name = Acer D501 Dock HDMI Converter Flags = updatable FirmwareSizeMin = 0x10000 FirmwareSizeMax = 0x90000 Rts54TargetAddr = 0x20 Rts54I2cSpeed = 0x2 Rts54RegisterAddrLen = 0x04 #Acer T34 Dock gen1 [USB\VID_0502&PID_0702] Plugin = rts54hub Flags = updatable GType = FuRts54HubDevice FirmwareSizeMin = 0x10000 FirmwareSizeMax = 0x20000 #Acer T34 Dock gen2 [USB\VID_0502&PID_0701] Plugin = rts54hub Flags = updatable GType = FuRts54HubDevice FirmwareSizeMin = 0x10000 FirmwareSizeMax = 0x20000 #Acer U33 Dock gen2 [USB\VID_0502&PID_0801] Plugin = rts54hub Flags = updatable GType = FuRts54HubDevice FirmwareSizeMin = 0x10000 FirmwareSizeMax = 0x20000 #Acer U33 Dock gen1 [USB\VID_0502&PID_0802] Plugin = rts54hub Flags = updatable GType = FuRts54HubDevice FirmwareSizeMin = 0x10000 FirmwareSizeMax = 0x20000 #Acer U32 Dock level 1 [USB\VID_0502&PID_0804] Plugin = rts54hub Name = Acer Universal Dock U32 Lv1 Flags = updatable GType = FuRts54HubDevice FirmwareSizeMin = 0x10000 FirmwareSizeMax = 0x20000 #Acer U32 Dock level 2 [USB\VID_0502&PID_0806] Plugin = rts54hub Name = Acer Universal Dock U32 Lv2 Flags = updatable GType = FuRts54HubDevice FirmwareSizeMin = 0x10000 FirmwareSizeMax = 0x20000 #UCDDS Dock level 1 [USB\VID_3749&PID_050B] Plugin = rts54hub Name = UCDDS1080P Lv1 Flags = updatable GType = FuRts54HubDevice FirmwareSizeMin = 0x10000 FirmwareSizeMax = 0x20000 #UCDDS Dock level 2 [USB\VID_3749&PID_050D] Plugin = rts54hub Name = UCDDS1080P Lv2 Flags = updatable GType = FuRts54HubDevice FirmwareSizeMin = 0x10000 FirmwareSizeMax = 0x20000 #USB-C Dual Display Dock [USB\VID_065F&PID_2260] Plugin = rts54hub Name = USB-C Dual Display Dock Flags = updatable GType = FuRts54HubDevice FirmwareSizeMin = 0x10000 FirmwareSizeMax = 0x20000 fwupd-2.0.10/plugins/rts54hub/tests/000077500000000000000000000000001501337203100172155ustar00rootroot00000000000000fwupd-2.0.10/plugins/rts54hub/tests/realtek-rts5423.json000066400000000000000000000015541501337203100226700ustar00rootroot00000000000000{ "name": "Realtek 4-Port USB Hub", "interactive": false, "steps": [ { "url": "460a18d93f5d6118908d8437009ebbd6eceb3c6a0cdfbaf31f8a96df008564da-Realtek-RTS5423-1.56.cab", "emulation-url": "a203b727b0dae3d721605dbd1551e90c5c8d2fe5f8e7a7059300f1cc94658186-Realtek-RTS5423-1.56.zip", "components": [ { "version": "1.56", "guids": [ "b2d2fae3-1546-5d16-a9af-ac117a255a91" ] } ] }, { "url": "659e721b0efbe2dfc003d3d30ea5b771c00113cc9a162333ee5a8918c4515f69-Realtek-RTS5423-1.57.cab", "emulation-url": "85ca3b102ca29c42d2e14bbeaa355d0f26d3cf05f5db10716efd52e48bff1496-Realtek-RTS5423-1.57.zip", "components": [ { "version": "1.57", "guids": [ "b2d2fae3-1546-5d16-a9af-ac117a255a91" ] } ] } ] } fwupd-2.0.10/plugins/scsi/000077500000000000000000000000001501337203100153345ustar00rootroot00000000000000fwupd-2.0.10/plugins/scsi/README.md000066400000000000000000000022441501337203100166150ustar00rootroot00000000000000--- title: Plugin: SCSI --- ## Introduction This plugin adds support for SCSI storage hardware. Most SCSI devices are enumerated and some UFS devices may also be updatable. Firmware is sent in chunks of 4kB by default and activated on next reboot only. There is a quirk to change the chunk size for specific device. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in an unspecified binary file format. This plugin supports the following protocol ID: * `org.jedec.ufs` ## GUID Generation These device use the SCSI DeviceInstanceId values, e.g. * `SCSI\VEN_HP&DEV_EG0900JETKB&REV_HPD4` * `SCSI\VEN_HP&DEV_EG0900JETKB` ## Vendor ID Security The vendor ID is set from the vendor, for example set to `SCSI:HP` ## External Interface Access This plugin requires only reading from sysfs for enumeration, but requires using a `sg_io ioctl` for UFS updates. ## Version Considerations This plugin has been available since fwupd version `1.7.6`. ## Quirk Use This plugin uses the following plugin-specific quirks: ### ScsiWriteBufferSize The block size used for WRITE_BUFFER commands to update the firmware. Must be a multiple of 4k. fwupd-2.0.10/plugins/scsi/fu-scsi-device.c000066400000000000000000000333001501337203100203050ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fu-scsi-device.h" #include "fu-scsi-struct.h" struct _FuScsiDevice { FuUdevDevice parent_instance; guint64 ffu_timeout; guint32 write_buffer_size; }; G_DEFINE_TYPE(FuScsiDevice, fu_scsi_device, FU_TYPE_UDEV_DEVICE) #define INQUIRY_CMD 0x12 #define INQUIRY_CMDLEN 6 #define SCSI_INQ_BUFF_LEN 254 #define BUFFER_VENDOR_MODE 0x01 #define BUFFER_DUFS_MODE 0x02 #define BUFFER_FFU_MODE 0x0E #define BUFFER_EHS_MODE 0x1C #define SENSE_BUFF_LEN 18 #define WRITE_BUF_CMDLEN 10 #define READ_BUF_CMDLEN 10 #define WRITE_BUFFER_CMD 0x3B #define READ_BUFFER_CMD 0x3C #define FU_SCSI_DEVICE_IOCTL_TIMEOUT 5000 /* ms */ #define FU_SCSI_DEFAULT_WRITE_BUFFER_SIZE 4096 /* byte */ static void fu_scsi_device_to_string(FuDevice *device, guint idt, GString *str) { FuScsiDevice *self = FU_SCSI_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "FfuTimeout", self->ffu_timeout); fwupd_codec_string_append_hex(str, idt, "WriteBufferSize", self->write_buffer_size); } static gboolean fu_scsi_device_probe(FuDevice *device, GError **error) { FuScsiDevice *self = FU_SCSI_DEVICE(device); g_autofree gchar *attr_removable = NULL; g_autoptr(FuDevice) ufshci_parent = NULL; g_autoptr(FuDevice) device_target = NULL; g_autoptr(FuDevice) device_scsi = NULL; const gchar *subsystem_parents[] = {"pci", "platform", NULL}; /* the ufshci controller could really be on any bus... search in order of priority */ for (guint i = 0; subsystem_parents[i] != NULL && ufshci_parent == NULL; i++) { ufshci_parent = fu_device_get_backend_parent_with_subsystem(device, subsystem_parents[i], NULL); } if (ufshci_parent != NULL) { g_autofree gchar *attr_ufs_features = NULL; g_autofree gchar *attr_ffu_timeout = NULL; /* check if this is a UFS device */ g_info("found ufshci controller at %s", fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(ufshci_parent))); attr_ufs_features = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(ufshci_parent), "device_descriptor/ufs_features", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (attr_ufs_features != NULL) { guint64 ufs_features = 0; fu_device_set_summary(device, "UFS device"); /* least significant bit specifies FFU capability */ if (!fu_strtoull(attr_ufs_features, &ufs_features, 0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error)) return FALSE; if (ufs_features & 0x1) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_SIGNED); fu_device_add_protocol(device, "org.jedec.ufs"); } attr_ffu_timeout = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(ufshci_parent), "device_descriptor/ffu_timeout", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, error); if (attr_ffu_timeout == NULL) { g_prefix_error(error, "no ffu timeout specified: "); return FALSE; } if (!fu_strtoull(attr_ffu_timeout, &self->ffu_timeout, 0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error)) return FALSE; } } /* is internal? */ attr_removable = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(self), "removable", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (attr_removable != NULL) { guint64 removable = 0; if (!fu_strtoull(attr_removable, &removable, 0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error)) return FALSE; if (removable == 0x0) fu_device_add_flag(device, FWUPD_DEVICE_FLAG_INTERNAL); } /* scsi_target */ device_target = fu_device_get_backend_parent_with_subsystem(device, "scsi:scsi_target", NULL); if (device_target != NULL) { g_autofree gchar *devpath = fu_udev_device_get_devpath(FU_UDEV_DEVICE(device_target)); if (devpath != NULL) { g_autofree gchar *physical_id = g_strdup_printf("DEVPATH=%s", devpath); fu_device_set_physical_id(device, physical_id); } } /* scsi_device */ device_scsi = fu_device_get_backend_parent_with_subsystem(device, "scsi:scsi_device", NULL); if (device_scsi != NULL) { if (fu_device_get_vendor(device) == NULL) { g_autofree gchar *attr_vendor = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(device_scsi), "vendor", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (attr_vendor != NULL) fu_device_set_vendor(device, attr_vendor); } if (fu_device_get_name(device) == NULL) { g_autofree gchar *attr_model = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(device_scsi), "model", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (attr_model != NULL) fu_device_set_name(device, attr_model); } } /* fake something as we cannot use ioctls */ if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_IS_FAKE)) { fu_device_add_instance_str(device, "VEN", "fwupd"); fu_device_add_instance_str(device, "DEV", "DEVICE"); if (!fu_device_build_instance_id(device, error, "SCSI", "VEN", "DEV", NULL)) return FALSE; } /* success */ return TRUE; } static FuFirmware * fu_scsi_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { g_autoptr(FuFirmware) firmware = fu_firmware_new(); fu_firmware_set_alignment(firmware, FU_FIRMWARE_ALIGNMENT_4K); if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; return g_steal_pointer(&firmware); } static gboolean fu_scsi_device_ioctl_buf_cb(FuIoctl *self, gpointer ptr, guint8 *buf, gsize bufsz, GError **error) { struct sg_io_hdr *io_hdr = (struct sg_io_hdr *)ptr; io_hdr->dxferp = buf; io_hdr->dxfer_len = bufsz; return TRUE; } static gboolean fu_scsi_device_ioctl_cdb_cb(FuIoctl *self, gpointer ptr, guint8 *buf, gsize bufsz, GError **error) { struct sg_io_hdr *io_hdr = (struct sg_io_hdr *)ptr; io_hdr->cmdp = buf; io_hdr->cmd_len = bufsz; return TRUE; } static gboolean fu_scsi_device_ioctl_sense_cb(FuIoctl *self, gpointer ptr, guint8 *buf, gsize bufsz, GError **error) { struct sg_io_hdr *io_hdr = (struct sg_io_hdr *)ptr; io_hdr->sbp = buf; io_hdr->mx_sb_len = bufsz; return TRUE; } static gboolean fu_scsi_device_send_scsi_cmd_v3(FuScsiDevice *self, const guint8 *cdb, gsize cdbsz, const guint8 *buf, gsize bufsz, gint dir, GError **error) { guint8 sense_buffer[SENSE_BUFF_LEN] = {0}; struct sg_io_hdr io_hdr = { .interface_id = 'S', .dxfer_direction = dir, .timeout = 60000, /* ms */ }; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); g_debug("cmd=0x%x len=0x%x", cdb[0], (guint)bufsz); /* include these when generating the emulation event */ fu_ioctl_add_key_as_u16(ioctl, "Request", SG_IO); fu_ioctl_add_key_as_u8(ioctl, "DxferDirection", io_hdr.dxfer_direction); fu_ioctl_add_const_buffer(ioctl, NULL, buf, bufsz, fu_scsi_device_ioctl_buf_cb); fu_ioctl_add_const_buffer(ioctl, "Cdb", cdb, cdbsz, fu_scsi_device_ioctl_cdb_cb); fu_ioctl_add_mutable_buffer(ioctl, "Sense", sense_buffer, sizeof(sense_buffer), fu_scsi_device_ioctl_sense_cb); if (!fu_ioctl_execute(ioctl, SG_IO, (guint8 *)&io_hdr, sizeof(io_hdr), NULL, FU_SCSI_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_RETRY, error)) return FALSE; if (io_hdr.status) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "command fail with status %x, senseKey %s, asc 0x%02x, ascq 0x%02x", io_hdr.status, fu_scsi_sense_key_to_string(sense_buffer[2]), sense_buffer[12], sense_buffer[13]); return FALSE; } /* success */ return TRUE; } static gboolean fu_scsi_device_setup(FuDevice *device, GError **error) { FuScsiDevice *self = FU_SCSI_DEVICE(device); guint8 buf[SCSI_INQ_BUFF_LEN] = {0}; guint8 cdb[INQUIRY_CMDLEN] = { INQUIRY_CMD, 0, /* evpd */ 0, /* page */ 0, sizeof(buf), 0, }; g_autofree gchar *model = NULL; g_autofree gchar *revision = NULL; g_autofree gchar *vendor = NULL; g_autoptr(FuStructScsiInquiry) st = NULL; /* prepare chunk */ if (!fu_scsi_device_send_scsi_cmd_v3(self, cdb, sizeof(cdb), buf, sizeof(buf), SG_DXFER_FROM_DEV, error)) { g_prefix_error(error, "SG_IO INQUIRY_CMD data error: "); return FALSE; } /* parse */ st = fu_struct_scsi_inquiry_parse(buf, sizeof(buf), 0x0, error); if (st == NULL) return FALSE; vendor = fu_struct_scsi_inquiry_get_vendor_id(st); if (vendor != NULL) fu_device_set_vendor(device, vendor); model = fu_struct_scsi_inquiry_get_product_id(st); if (model != NULL) fu_device_set_name(device, model); revision = fu_struct_scsi_inquiry_get_product_rev(st); if (revision != NULL) fu_device_set_version(device, revision); /* add GUIDs */ fu_device_add_instance_str(device, "VEN", vendor); fu_device_add_instance_str(device, "DEV", model); fu_device_add_instance_str(device, "REV", revision); if (!fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, error, "SCSI", "VEN", NULL)) return FALSE; if (!fu_device_build_instance_id(device, error, "SCSI", "VEN", "DEV", NULL)) return FALSE; if (!fu_device_build_instance_id(device, error, "SCSI", "VEN", "DEV", "REV", NULL)) return FALSE; /* vendor sanity */ if (g_strcmp0(fu_device_get_vendor(device), "ATA") == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no assigned vendor"); return FALSE; } fu_device_build_vendor_id(device, "SCSI", fu_device_get_vendor(device)); /* success */ return TRUE; } static gboolean fu_scsi_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuScsiDevice *self = FU_SCSI_DEVICE(device); guint32 chunksz = self->write_buffer_size; guint32 offset = 0; g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* get default image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* prepare chunks */ chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, chunksz, error); if (chunks == NULL) return FALSE; fu_progress_set_id(progress, G_STRLOC); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); /* write each block */ for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; guint8 cdb[WRITE_BUF_CMDLEN] = {WRITE_BUFFER_CMD, BUFFER_FFU_MODE, 0x0 /* buf_id */}; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; fu_memwrite_uint24(cdb + 3, offset, G_BIG_ENDIAN); fu_memwrite_uint24(cdb + 6, fu_chunk_get_data_sz(chk), G_BIG_ENDIAN); if (!fu_scsi_device_send_scsi_cmd_v3(self, cdb, sizeof(cdb), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), SG_DXFER_TO_DEV, error)) { g_prefix_error(error, "SG_IO WRITE BUFFER data error for v3 chunk 0x%x: ", fu_chunk_get_idx(chk)); return FALSE; } /* chunk done */ fu_progress_step_done(progress); offset += fu_chunk_get_data_sz(chk); } /* success! */ fu_device_add_problem(device, FWUPD_DEVICE_PROBLEM_UPDATE_PENDING); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT); return TRUE; } static gboolean fu_scsi_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuScsiDevice *self = FU_SCSI_DEVICE(device); if (g_strcmp0(key, "ScsiWriteBufferSize") == 0) { guint64 tmp = 0; if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->write_buffer_size = tmp; return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_scsi_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 99, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reload"); } static void fu_scsi_device_init(FuScsiDevice *self) { fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_DRIVE_HARDDISK); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_summary(FU_DEVICE(self), "SCSI device"); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_INSTANCE_ID_REV); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_SYNC); self->write_buffer_size = FU_SCSI_DEFAULT_WRITE_BUFFER_SIZE; } static void fu_scsi_device_class_init(FuScsiDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_scsi_device_to_string; device_class->probe = fu_scsi_device_probe; device_class->setup = fu_scsi_device_setup; device_class->prepare_firmware = fu_scsi_device_prepare_firmware; device_class->write_firmware = fu_scsi_device_write_firmware; device_class->set_progress = fu_scsi_device_set_progress; device_class->set_quirk_kv = fu_scsi_device_set_quirk_kv; } fwupd-2.0.10/plugins/scsi/fu-scsi-device.h000066400000000000000000000004421501337203100203130ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_SCSI_DEVICE (fu_scsi_device_get_type()) G_DECLARE_FINAL_TYPE(FuScsiDevice, fu_scsi_device, FU, SCSI_DEVICE, FuUdevDevice) fwupd-2.0.10/plugins/scsi/fu-scsi-plugin.c000066400000000000000000000015331501337203100203470ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-scsi-device.h" #include "fu-scsi-plugin.h" struct _FuScsiPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuScsiPlugin, fu_scsi_plugin, FU_TYPE_PLUGIN) static void fu_scsi_plugin_init(FuScsiPlugin *self) { } static void fu_scsi_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "ScsiWriteBufferSize"); fu_plugin_add_device_udev_subsystem(plugin, "block:disk"); fu_plugin_add_device_gtype(plugin, FU_TYPE_SCSI_DEVICE); } static void fu_scsi_plugin_class_init(FuScsiPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_scsi_plugin_constructed; } fwupd-2.0.10/plugins/scsi/fu-scsi-plugin.h000066400000000000000000000003461501337203100203550ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuScsiPlugin, fu_scsi_plugin, FU, SCSI_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/scsi/fu-scsi.rs000066400000000000000000000014071501337203100172550ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] enum FuScsiSenseKey { NoSense = 0x00, RecoveredError = 0x01, NotReady = 0x02, MediumError = 0x03, HardwareError = 0x04, IllegalRequest = 0x05, UnitAttention = 0x06, DataProtect = 0x07, BlankCheck = 0x08, VendorSpecific = 0x09, CopyAborted = 0x0A, AbortedCommand = 0x0B, Equal = 0x0C, VolumeOverflow = 0x0D, Miscompare = 0x0E, } // see https://tldp.org/HOWTO/archived/SCSI-Programming-HOWTO/SCSI-Programming-HOWTO-9.html #[derive(Parse)] #[repr(C, packed)] struct FuStructScsiInquiry { reserved: [u8; 8], vendor_id: [char; 8], product_id: [char; 16], product_rev: [char; 4], } fwupd-2.0.10/plugins/scsi/meson.build000066400000000000000000000007711501337203100175030ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginScsi"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('scsi.quirk') plugin_builtins += static_library('fu_plugin_scsi', rustgen.process('fu-scsi.rs'), sources: [ 'fu-scsi-plugin.c', 'fu-scsi-device.c', ], include_directories: plugin_incdirs, c_args: [ cargs, '-DLOCALSTATEDIR="' + localstatedir + '"', ], link_with: plugin_libs, dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/scsi/scsi.quirk000066400000000000000000000014361501337203100173560ustar00rootroot00000000000000[SCSI\VEN_LSI&DEV_VirtualSES] Flags = no-probe [SCSI\VEN_Marvell&DEV_Console] Flags = no-probe [SCSI\VEN_HP&DEV_EK0800JVYPN&REV_HPD5] Issue = MTX-6f54e8621f4c490a918defdab6 [SCSI\VEN_HP&DEV_EK0800JVYPN&REV_HPD6] Issue = MTX-6f54e8621f4c490a918defdab6 [SCSI\VEN_HP&DEV_EO1600JVYPP&REV_HPD5] Issue = MTX-6f54e8621f4c490a918defdab6 [SCSI\VEN_HP&DEV_EO1600JVYPP&REV_HPD6] Issue = MTX-6f54e8621f4c490a918defdab6 [SCSI\VEN_HP&DEV_MK0800JVYPQ&REV_HPD5] Issue = MTX-6f54e8621f4c490a918defdab6 [SCSI\VEN_HP&DEV_MK0800JVYPQ&REV_HPD6] Issue = MTX-6f54e8621f4c490a918defdab6 [SCSI\VEN_HP&DEV_MO1600JVYPR&REV_HPD5] Issue = MTX-6f54e8621f4c490a918defdab6 [SCSI\VEN_HP&DEV_MO1600JVYPR&REV_HPD6] Issue = MTX-6f54e8621f4c490a918defdab6 #Samsung [SCSI\VEN_SAMSUNG&DEV_KLUDG4UHGC-B0E1] ScsiWriteBufferSize = 524288 fwupd-2.0.10/plugins/steelseries/000077500000000000000000000000001501337203100167225ustar00rootroot00000000000000fwupd-2.0.10/plugins/steelseries/README.md000066400000000000000000000033771501337203100202130ustar00rootroot00000000000000--- title: Plugin: SteelSeries --- ## Introduction This plugin allows to update firmware on SteelSeries gamepads: * Stratus Duo * Stratus Duo USB wireless adapter * Stratus+ * Aerox 3 Wireless * Rival 3 Wireless SteelSeries Rival 100 gaming mice support is limited by getting the correct version number. These mice have updatable firmware but so far no updates are available from the vendor. This plugin supports the following protocol IDs: * `com.steelseries.fizz` * `com.steelseries.gamepad` * `com.steelseries.sonic` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_1038&PID_1702` ## Quirk Use This plugin uses the following plugin-specific quirks: ### Flags:is-receiver The device is a USB receiver. Since 1.8.1 ## Update Behavior ### Gamepad Gamepad and/or its wireless adapter must be connected to host via USB cable to apply an update. The device is switched to bootloader mode to flash updates, and is reset automatically to new firmware after flashing. ### Mice The device is not upgradable and thus requires no vendor ID set. ### Wireless Mice ### Rival 3 Wireless The mouse switch button underneath must be set to 2.4G, and its 2.4G USB Wireless adapter must be connected to host. ### Aerox 3 Wireless The mouse switch button underneath must be set to 2.4G, and its 2.4G USB Wireless adapter must be connected to host; or the mouse must be connected to host via the USB-A to USB-C cable. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x1038` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available in `fwupd >= 0.8.0` but only learned how to update devices in `1.8.1`. fwupd-2.0.10/plugins/steelseries/fu-steelseries-device.c000066400000000000000000000121751501337203100232700ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-steelseries-device.h" typedef struct { gint iface_idx_offset; guint8 iface_idx; guint8 ep; gsize ep_in_size; } FuSteelseriesDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuSteelseriesDevice, fu_steelseries_device, FU_TYPE_USB_DEVICE) #define GET_PRIVATE(o) (fu_steelseries_device_get_instance_private(o)) /* @iface_idx_offset can be negative to specify from the end */ void fu_steelseries_device_set_iface_idx_offset(FuSteelseriesDevice *self, gint iface_idx_offset) { FuSteelseriesDevicePrivate *priv = GET_PRIVATE(self); priv->iface_idx_offset = iface_idx_offset; } gboolean fu_steelseries_device_request(FuSteelseriesDevice *self, const GByteArray *buf, GError **error) { FuSteelseriesDevicePrivate *priv = GET_PRIVATE(self); gsize actual_len = 0; g_autoptr(GByteArray) buf_padded = g_byte_array_new(); g_return_val_if_fail(buf != NULL, FALSE); /* pad out */ g_byte_array_append(buf_padded, buf->data, buf->len); fu_byte_array_set_size(buf_padded, FU_STEELSERIES_BUFFER_CONTROL_SIZE, 0x00); fu_dump_raw(G_LOG_DOMAIN, "Request", buf_padded->data, buf_padded->len); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_CLASS, FU_USB_RECIPIENT_INTERFACE, 0x09, 0x0200, priv->iface_idx, buf_padded->data, buf_padded->len, &actual_len, FU_STEELSERIES_TRANSACTION_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to do control transfer: "); return FALSE; } if (actual_len != buf_padded->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "only wrote %" G_GSIZE_FORMAT "bytes", actual_len); return FALSE; } /* success */ return TRUE; } GByteArray * fu_steelseries_device_response(FuSteelseriesDevice *self, GError **error) { FuSteelseriesDevicePrivate *priv = GET_PRIVATE(self); gsize actual_len = 0; g_autoptr(GByteArray) buf = g_byte_array_new(); fu_byte_array_set_size(buf, priv->ep_in_size, 0x00); if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), priv->ep, buf->data, buf->len, &actual_len, FU_STEELSERIES_TRANSACTION_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to do EP transfer: "); return NULL; } fu_dump_raw(G_LOG_DOMAIN, "Response", buf->data, actual_len); if (actual_len != priv->ep_in_size) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "only read %" G_GSIZE_FORMAT "bytes", actual_len); return NULL; } /* success */ return g_steal_pointer(&buf); } static gboolean fu_steelseries_device_probe(FuDevice *device, GError **error) { FuSteelseriesDevice *self = FU_STEELSERIES_DEVICE(device); FuSteelseriesDevicePrivate *priv = GET_PRIVATE(self); FuUsbInterface *iface = NULL; FuUsbEndpoint *ep = NULL; guint8 iface_idx; guint8 ep_id; guint16 packet_size; g_autoptr(GPtrArray) ifaces = NULL; g_autoptr(GPtrArray) endpoints = NULL; ifaces = fu_usb_device_get_interfaces(FU_USB_DEVICE(device), error); if (ifaces == NULL) return FALSE; /* use the correct interface for interrupt transfer, either specifying an absolute offset, * or a negative offset value for the "last" one */ if (priv->iface_idx_offset >= 0) { if ((guint)priv->iface_idx_offset > ifaces->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "update interface 0x%x not found", (guint)priv->iface_idx_offset); return FALSE; } iface_idx = priv->iface_idx_offset; } else { iface_idx = ifaces->len - 1; } iface = g_ptr_array_index(ifaces, iface_idx); priv->iface_idx = fu_usb_interface_get_number(iface); endpoints = fu_usb_interface_get_endpoints(iface); /* expecting to have only one endpoint for communication */ if (endpoints == NULL || endpoints->len != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "endpoint not found"); return FALSE; } ep = g_ptr_array_index(endpoints, 0); ep_id = fu_usb_endpoint_get_address(ep); packet_size = fu_usb_endpoint_get_maximum_packet_size(ep); priv->ep = ep_id; priv->ep_in_size = packet_size; fu_usb_device_add_interface(FU_USB_DEVICE(self), priv->iface_idx); /* success */ return TRUE; } static void fu_steelseries_device_to_string(FuDevice *device, guint idt, GString *str) { FuSteelseriesDevice *self = FU_STEELSERIES_DEVICE(device); FuSteelseriesDevicePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append_hex(str, idt, "Interface", priv->iface_idx); fwupd_codec_string_append_hex(str, idt, "Endpoint", priv->ep); } static void fu_steelseries_device_init(FuSteelseriesDevice *self) { fu_device_register_private_flag(FU_DEVICE(self), FU_STEELSERIES_DEVICE_FLAG_IS_RECEIVER); } static void fu_steelseries_device_class_init(FuSteelseriesDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_steelseries_device_to_string; device_class->probe = fu_steelseries_device_probe; } fwupd-2.0.10/plugins/steelseries/fu-steelseries-device.h000066400000000000000000000022051501337203100232660ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * Copyright 2021 Denis Pynkin * Copyright 2022 Gaël PORTAY * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_STEELSERIES_DEVICE (fu_steelseries_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuSteelseriesDevice, fu_steelseries_device, FU, STEELSERIES_DEVICE, FuUsbDevice) struct _FuSteelseriesDeviceClass { FuUsbDeviceClass parent_class; }; #define FU_STEELSERIES_FIZZ_CMD_TUNNEL_BIT 1 << 6 #define FU_STEELSERIES_BUFFER_CONTROL_SIZE 64 #define FU_STEELSERIES_TRANSACTION_TIMEOUT 7000 #define FU_STEELSERIES_DEVICE_FLAG_IS_RECEIVER "is-receiver" /* device needs bootloader mode for flashing */ #define FU_STEELSERIES_DEVICE_FLAG_DETACH_BOOTLOADER "detach-bootloader" void fu_steelseries_device_set_iface_idx_offset(FuSteelseriesDevice *self, gint iface_idx_offset); gboolean fu_steelseries_device_request(FuSteelseriesDevice *self, const GByteArray *buf, GError **error); GByteArray * fu_steelseries_device_response(FuSteelseriesDevice *self, GError **error); fwupd-2.0.10/plugins/steelseries/fu-steelseries-firmware.c000066400000000000000000000050531501337203100236420ustar00rootroot00000000000000/* * Copyright 2022 Gaël PORTAY * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-steelseries-firmware.h" struct _FuSteelseriesFirmware { FuFirmwareClass parent_instance; guint32 checksum; }; G_DEFINE_TYPE(FuSteelseriesFirmware, fu_steelseries_firmware, FU_TYPE_FIRMWARE) static gboolean fu_steelseries_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuSteelseriesFirmware *self = FU_STEELSERIES_FIRMWARE(firmware); guint32 checksum_tmp = 0xFFFFFFFF; guint32 checksum = 0; gsize streamsz = 0; g_autoptr(GInputStream) stream_tmp = NULL; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz < sizeof(checksum)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "image is too small"); return FALSE; } if (!fu_input_stream_read_u32(stream, streamsz - sizeof(checksum), &checksum, G_LITTLE_ENDIAN, error)) return FALSE; stream_tmp = fu_partial_input_stream_new(stream, 0, streamsz - sizeof(checksum_tmp), error); if (stream_tmp == NULL) return FALSE; if (!fu_input_stream_compute_crc32(stream_tmp, FU_CRC_KIND_B32_STANDARD, &checksum_tmp, error)) return FALSE; if (checksum_tmp != checksum) { if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "checksum mismatch, got 0x%08x, expected 0x%08x", checksum_tmp, checksum); return FALSE; } g_debug("ignoring checksum mismatch, got 0x%08x, expected 0x%08x", checksum_tmp, checksum); } self->checksum = checksum; fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); /* success */ return TRUE; } static void fu_steelseries_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuSteelseriesFirmware *self = FU_STEELSERIES_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "checksum", self->checksum); } static void fu_steelseries_firmware_init(FuSteelseriesFirmware *self) { } static void fu_steelseries_firmware_class_init(FuSteelseriesFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_steelseries_firmware_parse; firmware_class->export = fu_steelseries_firmware_export; } FuFirmware * fu_steelseries_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_STEELSERIES_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/steelseries/fu-steelseries-firmware.h000066400000000000000000000006361501337203100236510ustar00rootroot00000000000000/* * Copyright 2022 Gaël PORTAY * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_STEELSERIES_FIRMWARE (fu_steelseries_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuSteelseriesFirmware, fu_steelseries_firmware, FU, STEELSERIES_FIRMWARE, FuFirmware) FuFirmware * fu_steelseries_firmware_new(void); fwupd-2.0.10/plugins/steelseries/fu-steelseries-fizz-gen1.c000066400000000000000000000143601501337203100236410ustar00rootroot00000000000000/* * Copyright 2024 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-steelseries-fizz-gen1.h" #include "fu-steelseries-fizz-impl.h" struct _FuSteelseriesFizzGen1 { FuSteelseriesDevice parent_instance; }; static void fu_steelseries_fizz_gen1_impl_iface_init(FuSteelseriesFizzImplInterface *iface); G_DEFINE_TYPE_WITH_CODE(FuSteelseriesFizzGen1, fu_steelseries_fizz_gen1, FU_TYPE_STEELSERIES_DEVICE, G_IMPLEMENT_INTERFACE(FU_TYPE_STEELSERIES_FIZZ_IMPL, fu_steelseries_fizz_gen1_impl_iface_init)) static gboolean fu_steelseries_fizz_gen1_request(FuSteelseriesFizzImpl *self, const GByteArray *buf, GError **error) { return fu_steelseries_device_request(FU_STEELSERIES_DEVICE(self), buf, error); } static GByteArray * fu_steelseries_fizz_gen1_response(FuSteelseriesFizzImpl *self, GError **error) { return fu_steelseries_device_response(FU_STEELSERIES_DEVICE(self), error); } static gchar * fu_steelseries_fizz_gen1_get_version(FuSteelseriesFizzImpl *self, gboolean tunnel, GError **error) { guint8 cmd = FU_STEELSERIES_FIZZ_CMD_VERSION; g_autoptr(FuStructSteelseriesFizzVersionReq) st_req = NULL; g_autoptr(GByteArray) buf_res = NULL; if (tunnel) cmd |= FU_STEELSERIES_FIZZ_CMD_TUNNEL_BIT; st_req = fu_struct_steelseries_fizz_version_req_new(); fu_struct_steelseries_fizz_version_req_set_cmd(st_req, cmd); if (!fu_steelseries_fizz_gen1_request(self, st_req, error)) return NULL; buf_res = fu_steelseries_fizz_gen1_response(self, error); if (buf_res == NULL) return NULL; /* success */ fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); return fu_memstrsafe(buf_res->data, buf_res->len, 0x0, buf_res->len, error); } static guint8 fu_steelseries_fizz_gen1_get_fs_id(FuSteelseriesFizzImpl *self, gboolean is_receiver) { if (is_receiver) return FU_STEELSERIES_FIZZ_FILESYSTEM_RECEIVER; return FU_STEELSERIES_FIZZ_FILESYSTEM_MOUSE; } static guint8 fu_steelseries_fizz_gen1_get_file_id(FuSteelseriesFizzImpl *self, gboolean is_receiver) { if (is_receiver) return FU_STEELSERIES_FIZZ_RECEIVER_FILESYSTEM_ID_BACKUP_APP; return FU_STEELSERIES_FIZZ_MOUSE_FILESYSTEM_ID_BACKUP_APP; } static gboolean fu_steelseries_fizz_gen1_get_paired_status(FuSteelseriesFizzImpl *self, guint8 *status, GError **error) { g_autoptr(FuStructSteelseriesPairedStatusReq) st_req = fu_struct_steelseries_paired_status_req_new(); g_autoptr(FuStructSteelseriesPairedStatusRes) st_res = NULL; g_autoptr(GByteArray) buf_res = NULL; if (!fu_steelseries_fizz_gen1_request(self, st_req, error)) return FALSE; buf_res = fu_steelseries_fizz_gen1_response(self, error); if (buf_res == NULL) return FALSE; st_res = fu_struct_steelseries_paired_status_res_parse(buf_res->data, buf_res->len, 0x0, error); if (st_res == NULL) return FALSE; *status = fu_struct_steelseries_paired_status_res_get_status(st_res); return TRUE; } static gboolean fu_steelseries_fizz_gen1_get_connection_status(FuSteelseriesFizzImpl *self, FuSteelseriesFizzConnectionStatus *status, GError **error) { g_autoptr(FuStructSteelseriesConnectionStatusReq) st_req = fu_struct_steelseries_connection_status_req_new(); g_autoptr(FuStructSteelseriesConnectionStatusRes) st_res = NULL; g_autoptr(GByteArray) buf_res = NULL; if (!fu_steelseries_fizz_gen1_request(self, st_req, error)) return FALSE; buf_res = fu_steelseries_fizz_gen1_response(self, error); if (buf_res == NULL) return FALSE; st_res = fu_struct_steelseries_connection_status_res_parse(buf_res->data, buf_res->len, 0x0, error); if (st_res == NULL) return FALSE; *status = fu_struct_steelseries_connection_status_res_get_status(st_res); return TRUE; } static gboolean fu_steelseries_fizz_gen1_get_battery_level(FuSteelseriesFizzImpl *self, gboolean tunnel, guint8 *level, GError **error) { guint8 cmd = FU_STEELSERIES_FIZZ_CMD_BATTERY_LEVEL; g_autoptr(FuStructSteelseriesBatteryLevelReq) st_req = NULL; g_autoptr(FuStructSteelseriesBatteryLevelRes) st_res = NULL; g_autoptr(GByteArray) buf_res = NULL; if (tunnel) cmd |= FU_STEELSERIES_FIZZ_CMD_TUNNEL_BIT; st_req = fu_struct_steelseries_battery_level_req_new(); fu_struct_steelseries_battery_level_req_set_cmd(st_req, cmd); if (!fu_steelseries_fizz_gen1_request(self, st_req, error)) return FALSE; buf_res = fu_steelseries_fizz_gen1_response(self, error); if (buf_res == NULL) return FALSE; st_res = fu_struct_steelseries_battery_level_res_parse(buf_res->data, buf_res->len, 0x0, error); if (st_res == NULL) return FALSE; /* * CHARGING: Most significant bit. When bit is set to 1 it means battery is currently * charging/plugged in * * LEVEL: 7 least significant bit value of the battery. Values are between 2-21, to get % * you can do (LEVEL - 1) * 5 */ *level = ((fu_struct_steelseries_battery_level_res_get_level(st_res) & FU_STEELSERIES_FIZZ_BATTERY_LEVEL_STATUS_BITS) - 1U) * 5U; /* success */ return TRUE; } static gboolean fu_steelseries_fizz_gen1_setup(FuDevice *device, GError **error) { /* in bootloader mode */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) return TRUE; /* FuUsbDevice->setup */ return FU_DEVICE_CLASS(fu_steelseries_fizz_gen1_parent_class)->setup(device, error); } static void fu_steelseries_fizz_gen1_impl_iface_init(FuSteelseriesFizzImplInterface *iface) { iface->request = fu_steelseries_fizz_gen1_request; iface->response = fu_steelseries_fizz_gen1_response; iface->get_version = fu_steelseries_fizz_gen1_get_version; iface->get_fs_id = fu_steelseries_fizz_gen1_get_fs_id; iface->get_file_id = fu_steelseries_fizz_gen1_get_file_id; iface->get_paired_status = fu_steelseries_fizz_gen1_get_paired_status; iface->get_connection_status = fu_steelseries_fizz_gen1_get_connection_status; iface->get_battery_level = fu_steelseries_fizz_gen1_get_battery_level; } static void fu_steelseries_fizz_gen1_class_init(FuSteelseriesFizzGen1Class *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_steelseries_fizz_gen1_setup; } static void fu_steelseries_fizz_gen1_init(FuSteelseriesFizzGen1 *self) { fu_steelseries_device_set_iface_idx_offset(FU_STEELSERIES_DEVICE(self), 0x03); } fwupd-2.0.10/plugins/steelseries/fu-steelseries-fizz-gen1.h000066400000000000000000000006351501337203100236460ustar00rootroot00000000000000/* * Copyright 2024 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-steelseries-fizz.h" #define FU_TYPE_STEELSERIES_FIZZ_GEN1 (fu_steelseries_fizz_gen1_get_type()) G_DECLARE_FINAL_TYPE(FuSteelseriesFizzGen1, fu_steelseries_fizz_gen1, FU, STEELSERIES_FIZZ_GEN1, FuSteelseriesDevice) fwupd-2.0.10/plugins/steelseries/fu-steelseries-fizz-gen2.c000066400000000000000000000254701501337203100236460ustar00rootroot00000000000000/* * Copyright 2024 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-steelseries-fizz-gen2.h" #include "fu-steelseries-fizz-impl.h" #include "fu-steelseries-fizz-struct.h" #define FU_STEELSERIES_FIZZ_GEN2_FILESYSTEM_RECEIVER 0x01U #define FU_STEELSERIES_FIZZ_GEN2_FILESYSTEM_HEADSET 0x01U #define FU_STEELSERIES_FIZZ_GEN2_APP_ID 0x01U #define FU_STEELSERIES_FIZZ_GEN2_NOT_PAIRED 0x00; #define FU_STEELSERIES_FIZZ_GEN2_PAIRED 0x01; struct _FuSteelseriesFizzGen2 { FuSteelseriesDevice parent_instance; }; static void fu_steelseries_fizz_gen2_impl_iface_init(FuSteelseriesFizzImplInterface *iface); G_DEFINE_TYPE_WITH_CODE(FuSteelseriesFizzGen2, fu_steelseries_fizz_gen2, FU_TYPE_STEELSERIES_DEVICE, G_IMPLEMENT_INTERFACE(FU_TYPE_STEELSERIES_FIZZ_IMPL, fu_steelseries_fizz_gen2_impl_iface_init)) static gboolean fu_steelseries_fizz_gen2_request(FuSteelseriesFizzImpl *self, const GByteArray *buf, GError **error) { return fu_steelseries_device_request(FU_STEELSERIES_DEVICE(self), buf, error); } static GByteArray * fu_steelseries_fizz_gen2_response(FuSteelseriesFizzImpl *self, GError **error) { return fu_steelseries_device_response(FU_STEELSERIES_DEVICE(self), error); } static gchar * fu_steelseries_fizz_gen2_get_version(FuSteelseriesFizzImpl *self, gboolean tunnel, GError **error) { g_autofree gchar *version_raw = NULL; g_autoptr(FuStructSteelseriesFizzVersion2Req) st_req = fu_struct_steelseries_fizz_version2_req_new(); g_autoptr(FuStructSteelseriesVersion2Res) st_res = NULL; g_autoptr(GByteArray) buf_res = NULL; if (!fu_steelseries_device_request(FU_STEELSERIES_DEVICE(self), st_req, error)) return NULL; buf_res = fu_steelseries_device_response(FU_STEELSERIES_DEVICE(self), error); if (buf_res == NULL) return NULL; st_res = fu_struct_steelseries_version2_res_parse(buf_res->data, buf_res->len, 0x0, error); if (st_res == NULL) return NULL; if (tunnel) version_raw = fu_struct_steelseries_version2_res_get_version_device(st_res); else version_raw = fu_struct_steelseries_version2_res_get_version_receiver(st_res); if (version_raw == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "version number provided"); return NULL; } if (strlen(version_raw) != FU_STRUCT_STEELSERIES_VERSION2_RES_SIZE_VERSION_RECEIVER) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid version number: %s", version_raw); return NULL; } /* very interesting version format */ if (version_raw[1] == 0x2E && version_raw[4] == 0x2E && version_raw[8] == 0x2E) { /* format triple */ return g_strdup_printf( "%u.%u.%u", ((guint32)(version_raw[2] - 0x30) << 4) + (version_raw[3] - 0x30), ((guint32)(version_raw[6] - 0x30) << 4) + (version_raw[7] - 0x30), ((guint32)(version_raw[9] - 0x30) << 4) + (version_raw[10] - 0x30)); }; /* format dual */ return g_strdup_printf("%u.%u.0", ((guint32)(version_raw[7] - 0x30) << 4) + (version_raw[8] - 0x30), ((guint32)(version_raw[10] - 0x30) << 4) + (version_raw[11] - 0x30)); } static gboolean fu_steelseries_fizz_gen2_get_battery_level(FuSteelseriesFizzImpl *self, gboolean tunnel, guint8 *level, GError **error) { g_autoptr(FuStructSteelseriesBatteryLevel2Req) st_req = fu_struct_steelseries_battery_level2_req_new(); g_autoptr(FuStructSteelseriesBatteryLevel2Res) st_res = NULL; g_autoptr(GByteArray) buf_res = NULL; if (!fu_steelseries_fizz_gen2_request(self, st_req, error)) return FALSE; buf_res = fu_steelseries_fizz_gen2_response(self, error); if (buf_res == NULL) return FALSE; st_res = fu_struct_steelseries_battery_level2_res_parse(buf_res->data, buf_res->len, 0x0, error); if (st_res == NULL) return FALSE; *level = fu_struct_steelseries_battery_level2_res_get_level(st_res); /* success */ return TRUE; } static guint8 fu_steelseries_fizz_gen2_get_fs_id(FuSteelseriesFizzImpl *self, gboolean is_receiver) { if (is_receiver) return FU_STEELSERIES_FIZZ_GEN2_FILESYSTEM_RECEIVER; return FU_STEELSERIES_FIZZ_GEN2_FILESYSTEM_HEADSET; } static guint8 fu_steelseries_fizz_gen2_get_file_id(FuSteelseriesFizzImpl *self, gboolean is_receiver) { return FU_STEELSERIES_FIZZ_GEN2_APP_ID; } static gboolean fu_steelseries_fizz_gen2_get_paired_status(FuSteelseriesFizzImpl *self, guint8 *status, GError **error) { g_autoptr(FuStructSteelseriesConnectionStatus2Req) st_req = fu_struct_steelseries_connection_status2_req_new(); g_autoptr(FuStructSteelseriesConnectionStatus2Res) st_res = NULL; g_autoptr(GByteArray) buf_res = NULL; if (!fu_steelseries_fizz_gen2_request(self, st_req, error)) return FALSE; buf_res = fu_steelseries_fizz_gen2_response(self, error); if (buf_res == NULL) return FALSE; st_res = fu_struct_steelseries_connection_status2_res_parse(buf_res->data, buf_res->len, 0x0, error); if (st_res == NULL) return FALSE; /* treat CONNECTED and DISCONNECTED as paired */ switch (fu_struct_steelseries_connection_status2_res_get_status(st_res)) { case FU_STEELSERIES_FIZZ_CONNECTION_CONNECTED: case FU_STEELSERIES_FIZZ_CONNECTION_DISCONNECTED: *status = FU_STEELSERIES_FIZZ_GEN2_PAIRED; break; default: *status = FU_STEELSERIES_FIZZ_GEN2_NOT_PAIRED; } /* success */ return TRUE; } static gboolean fu_steelseries_fizz_gen2_get_connection_status(FuSteelseriesFizzImpl *self, FuSteelseriesFizzConnectionStatus *status, GError **error) { g_autoptr(FuStructSteelseriesConnectionStatus2Req) st_req = fu_struct_steelseries_connection_status2_req_new(); g_autoptr(FuStructSteelseriesConnectionStatus2Res) st_res = NULL; g_autoptr(GByteArray) buf_res = NULL; if (!fu_steelseries_fizz_gen2_request(self, st_req, error)) return FALSE; buf_res = fu_steelseries_fizz_gen2_response(self, error); if (buf_res == NULL) return FALSE; st_res = fu_struct_steelseries_connection_status2_res_parse(buf_res->data, buf_res->len, 0x0, error); if (st_res == NULL) return FALSE; if (fu_struct_steelseries_connection_status2_res_get_status(st_res) == FU_STEELSERIES_FIZZ_CONNECTION_CONNECTED) { *status = FU_STEELSERIES_FIZZ_CONNECTION_STATUS_CONNECTED; } else { *status = FU_STEELSERIES_FIZZ_CONNECTION_STATUS_NOT_CONNECTED; } /* success */ return TRUE; } static gchar * fu_steelseries_fizz_gen2_get_serial(FuSteelseriesFizzImpl *self, gboolean tunnel, GError **error) { g_autofree gchar *serial = NULL; g_autoptr(FuStructSteelseriesSerial2Req) st_req = fu_struct_steelseries_serial2_req_new(); g_autoptr(FuStructSteelseriesSerial2Res) st_res = NULL; g_autoptr(GByteArray) buf_res = NULL; if (!fu_steelseries_device_request(FU_STEELSERIES_DEVICE(self), st_req, error)) return NULL; buf_res = fu_steelseries_device_response(FU_STEELSERIES_DEVICE(self), error); if (buf_res == NULL) return NULL; st_res = fu_struct_steelseries_serial2_res_parse(buf_res->data, buf_res->len, 0x0, error); if (st_res == NULL) return NULL; serial = fu_struct_steelseries_serial2_res_get_serial(st_res); if (serial == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no serial number provided"); return NULL; } return g_steal_pointer(&serial); } static gboolean fu_steelseries_fizz_gen2_is_updatable(FuSteelseriesFizzImpl *self, FuDevice *device, GError **error) { g_autoptr(FwupdRequest) request = NULL; g_autofree gchar *msg = NULL; /* requires direct USB only */ if (g_strcmp0(fu_device_get_composite_id(device), fu_device_get_id(device)) == 0) return TRUE; fu_device_add_request_flag(device, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); /* the user has to do something */ msg = g_strdup_printf("%s needs to be connected via the USB cable, " "to start the update. " "Please plug the USB-C cable.", fu_device_get_name(device)); request = fwupd_request_new(); fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_IMMEDIATE); fwupd_request_set_id(request, FWUPD_REQUEST_ID_INSERT_USB_CABLE); fwupd_request_add_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); fwupd_request_set_message(request, msg); if (!fu_device_emit_request(device, request, NULL, error)) return FALSE; /* FIXME: return commented as soon as we have support of simultaneous connections */ // fu_device_set_remove_delay(device, FU_DEVICE_REMOVE_DELAY_USER_REPLUG); /* 40 sec */ // fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); // fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* success */ // return TRUE; g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "supported update via USB-C only"); return FALSE; } static gboolean fu_steelseries_fizz_gen2_probe(FuDevice *device, GError **error) { /* in bootloader mode */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) fu_steelseries_device_set_iface_idx_offset(FU_STEELSERIES_DEVICE(device), 0x00); /* FuUsbDevice->probe */ return FU_DEVICE_CLASS(fu_steelseries_fizz_gen2_parent_class)->probe(device, error); } static void fu_steelseries_fizz_gen2_impl_iface_init(FuSteelseriesFizzImplInterface *iface) { iface->request = fu_steelseries_fizz_gen2_request; iface->response = fu_steelseries_fizz_gen2_response; iface->get_version = fu_steelseries_fizz_gen2_get_version; iface->get_fs_id = fu_steelseries_fizz_gen2_get_fs_id; iface->get_file_id = fu_steelseries_fizz_gen2_get_file_id; iface->get_paired_status = fu_steelseries_fizz_gen2_get_paired_status; iface->get_connection_status = fu_steelseries_fizz_gen2_get_connection_status; iface->get_battery_level = fu_steelseries_fizz_gen2_get_battery_level; iface->is_updatable = fu_steelseries_fizz_gen2_is_updatable; iface->get_serial = fu_steelseries_fizz_gen2_get_serial; } static gboolean fu_steelseries_fizz_gen2_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuSteelseriesFizzGen2 *self = FU_STEELSERIES_FIZZ_GEN2(device); guint64 tmp = 0; if (g_strcmp0(key, "SteelSeriesFizzInterface") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_steelseries_device_set_iface_idx_offset(FU_STEELSERIES_DEVICE(self), tmp); return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported"); return FALSE; } static void fu_steelseries_fizz_gen2_class_init(FuSteelseriesFizzGen2Class *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_steelseries_fizz_gen2_probe; device_class->set_quirk_kv = fu_steelseries_fizz_gen2_set_quirk_kv; } static void fu_steelseries_fizz_gen2_init(FuSteelseriesFizzGen2 *self) { fu_steelseries_device_set_iface_idx_offset(FU_STEELSERIES_DEVICE(self), 0x05); } fwupd-2.0.10/plugins/steelseries/fu-steelseries-fizz-gen2.h000066400000000000000000000006351501337203100236470ustar00rootroot00000000000000/* * Copyright 2024 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-steelseries-fizz.h" #define FU_TYPE_STEELSERIES_FIZZ_GEN2 (fu_steelseries_fizz_gen2_get_type()) G_DECLARE_FINAL_TYPE(FuSteelseriesFizzGen2, fu_steelseries_fizz_gen2, FU, STEELSERIES_FIZZ_GEN2, FuSteelseriesDevice) fwupd-2.0.10/plugins/steelseries/fu-steelseries-fizz-hid.c000066400000000000000000000143611501337203100235540ustar00rootroot00000000000000/* * Copyright 2022 Gaël PORTAY * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-steelseries-fizz-hid.h" #include "fu-steelseries-fizz-struct.h" #define FU_STEELSERIES_BUFFER_REPORT_SIZE 64 + 1 #define FU_STEELSERIES_HID_MAX_RETRIES 20 #define FU_STEELSERIES_HID_CB_TIMEOUT 300 /* ms */ struct _FuSteelseriesFizzHid { FuUdevDevice parent_instance; }; G_DEFINE_TYPE(FuSteelseriesFizzHid, fu_steelseries_fizz_hid, FU_TYPE_UDEV_DEVICE) typedef struct { GByteArray *buf_in; GByteArray *buf_out; } FuSteelseriesFizzHidCommandHelper; static gboolean fu_steelseries_fizz_hid_command_cb(FuDevice *device, gpointer user_data, GError **error) { FuSteelseriesFizzHidCommandHelper *helper = (FuSteelseriesFizzHidCommandHelper *)user_data; gboolean ret; guint8 buf[FU_STEELSERIES_BUFFER_REPORT_SIZE] = {0}; g_autoptr(FuStructSteelseriesFizzHidResponse) st = NULL; g_autoptr(GError) error_local = NULL; /* force the request for each iteration to avoid a loop due the lost single packet -- * this is safe since the device doesn't support update over bluetooth */ if (!fu_memcpy_safe(buf, sizeof(buf), 0, /* dst */ helper->buf_in->data, helper->buf_in->len, 0, /* src */ helper->buf_in->len, error)) return FALSE; fu_dump_raw(G_LOG_DOMAIN, "write", buf, sizeof(buf)); if (!fu_udev_device_pwrite(FU_UDEV_DEVICE(device), 0, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to write report: "); return FALSE; } memset(buf, 0x0, sizeof(buf)); ret = fu_udev_device_pread(FU_UDEV_DEVICE(device), 0, buf, sizeof(buf), &error_local); if (!ret) { /* since fu_udev_device_pread() treats unexpected data size as error * we have to check the output additionally since the size of * unexpected data size from mouse input data is only 16b */ if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL) || buf[0] != 0x01) { g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to read report: "); return FALSE; } } fu_dump_raw(G_LOG_DOMAIN, "read", buf, sizeof(buf)); st = fu_struct_steelseries_fizz_hid_response_parse(buf, sizeof(buf), 0x0, error); if (st == NULL) return FALSE; if (fu_struct_steelseries_fizz_hid_response_get_report_id(st) != FU_STRUCT_STEELSERIES_FIZZ_HID_GET_VERSION_REQ_DEFAULT_REPORT_ID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "data with unexpected Report ID (%u)", fu_struct_steelseries_fizz_hid_response_get_report_id(st)); return FALSE; } g_byte_array_append(helper->buf_out, buf + 1, sizeof(buf) - 1); /* success */ return TRUE; } static GByteArray * fu_steelseries_fizz_hid_command(FuSteelseriesFizzHid *self, GByteArray *buf_in, GError **error) { g_autoptr(GByteArray) buf_out = g_byte_array_new(); FuSteelseriesFizzHidCommandHelper helper = { .buf_in = buf_in, .buf_out = buf_out, }; /* In BT mode the sync and data channels are sharing the device descriptor with the * management channel. * This is the reason why we receive "unexpected" packets with 0x01 or 0x05 Report IDs over * the same descriptor on mouse connecting, waking up or just moving the mouse -- hence * trying to repeat the query/response cycle lot of times */ if (!fu_device_retry_full(FU_DEVICE(self), fu_steelseries_fizz_hid_command_cb, FU_STEELSERIES_HID_MAX_RETRIES, FU_STEELSERIES_HID_CB_TIMEOUT, /* BT might be laggy */ &helper, error)) return NULL; return g_steal_pointer(&buf_out); } static gboolean fu_steelseries_fizz_hid_ensure_version(FuSteelseriesFizzHid *self, GError **error) { g_autofree gchar *version = NULL; g_autoptr(GByteArray) st_buf = NULL; g_autoptr(FuStructSteelseriesFizzHidGetVersionReq) st = fu_struct_steelseries_fizz_hid_get_version_req_new(); st_buf = fu_steelseries_fizz_hid_command(self, st, error); if (st_buf == NULL) return FALSE; version = fu_strsafe((const gchar *)st_buf->data, st_buf->len); if (version == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "unable to read version"); return FALSE; } fu_device_set_version(FU_DEVICE(self), version); return TRUE; } static gboolean fu_steelseries_fizz_hid_detach(FuDevice *device, FuProgress *progress, GError **error) { g_autoptr(FwupdRequest) request = NULL; g_autofree gchar *msg = NULL; /* the user has to do something */ msg = g_strdup_printf( "%s needs to be manually connected either via the USB cable, " "or via the 2.4G USB Wireless adapter to start the update. " "Please plug either the USB-C cable and put the switch button underneath to off, " "or the 2.4G USB Wireless adapter and put the switch button underneath to 2.4G.", fu_device_get_name(device)); request = fwupd_request_new(); fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_IMMEDIATE); fwupd_request_set_id(request, FWUPD_REQUEST_ID_PRESS_UNLOCK); fwupd_request_set_message(request, msg); if (!fu_device_emit_request(device, request, progress, error)) return FALSE; /* success */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_steelseries_fizz_hid_setup(FuDevice *device, GError **error) { FuSteelseriesFizzHid *self = FU_STEELSERIES_FIZZ_HID(device); if (!fu_steelseries_fizz_hid_ensure_version(self, error)) return FALSE; /* success */ return TRUE; } static void fu_steelseries_fizz_hid_class_init(FuSteelseriesFizzHidClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_steelseries_fizz_hid_setup; device_class->detach = fu_steelseries_fizz_hid_detach; } static void fu_steelseries_fizz_hid_init(FuSteelseriesFizzHid *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); fu_device_add_request_flag(FU_DEVICE(self), FWUPD_REQUEST_FLAG_NON_GENERIC_MESSAGE); fu_device_set_physical_id(FU_DEVICE(self), "hid"); fu_device_add_protocol(FU_DEVICE(self), "com.steelseries.fizz"); fu_device_set_remove_delay(FU_DEVICE(self), 300000); /* 5min */ } fwupd-2.0.10/plugins/steelseries/fu-steelseries-fizz-hid.h000066400000000000000000000005551501337203100235610ustar00rootroot00000000000000/* * Copyright 2022 Gaël PORTAY * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_STEELSERIES_FIZZ_HID (fu_steelseries_fizz_hid_get_type()) G_DECLARE_FINAL_TYPE(FuSteelseriesFizzHid, fu_steelseries_fizz_hid, FU, STEELSERIES_FIZZ_HID, FuHidDevice) fwupd-2.0.10/plugins/steelseries/fu-steelseries-fizz-impl.c000066400000000000000000000126461501337203100237550ustar00rootroot00000000000000/* * Copyright 2024 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-steelseries-fizz-impl.h" G_DEFINE_INTERFACE(FuSteelseriesFizzImpl, fu_steelseries_fizz_impl, G_TYPE_OBJECT) static void fu_steelseries_fizz_impl_default_init(FuSteelseriesFizzImplInterface *iface) { } gboolean fu_steelseries_fizz_impl_request(FuSteelseriesFizzImpl *self, GByteArray *buf, GError **error) { FuSteelseriesFizzImplInterface *iface; g_return_val_if_fail(FU_IS_STEELSERIES_FIZZ_IMPL(self), FALSE); iface = FU_STEELSERIES_FIZZ_IMPL_GET_IFACE(self); if (iface->request == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "iface->request not implemented"); return FALSE; } return (*iface->request)(self, buf, error); } GByteArray * fu_steelseries_fizz_impl_response(FuSteelseriesFizzImpl *self, GError **error) { FuSteelseriesFizzImplInterface *iface; g_return_val_if_fail(FU_IS_STEELSERIES_FIZZ_IMPL(self), NULL); iface = FU_STEELSERIES_FIZZ_IMPL_GET_IFACE(self); if (iface->response == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "iface->response not implemented"); return NULL; } return (*iface->response)(self, error); } gchar * fu_steelseries_fizz_impl_get_version(FuSteelseriesFizzImpl *self, gboolean tunnel, GError **error) { FuSteelseriesFizzImplInterface *iface; g_return_val_if_fail(FU_IS_STEELSERIES_FIZZ_IMPL(self), NULL); iface = FU_STEELSERIES_FIZZ_IMPL_GET_IFACE(self); if (iface->get_version == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "iface->get_version not implemented"); return NULL; } return (*iface->get_version)(self, tunnel, error); } gboolean fu_steelseries_fizz_impl_get_battery_level(FuSteelseriesFizzImpl *self, gboolean tunnel, guint8 *level, GError **error) { FuSteelseriesFizzImplInterface *iface; g_return_val_if_fail(FU_IS_STEELSERIES_FIZZ_IMPL(self), FALSE); iface = FU_STEELSERIES_FIZZ_IMPL_GET_IFACE(self); if (iface->get_battery_level == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "iface->get_battery_level not implemented"); return FALSE; } return (*iface->get_battery_level)(self, tunnel, level, error); } gboolean fu_steelseries_fizz_impl_get_fs_id(FuSteelseriesFizzImpl *self, gboolean is_receiver, guint8 *id, GError **error) { FuSteelseriesFizzImplInterface *iface; g_return_val_if_fail(FU_IS_STEELSERIES_FIZZ_IMPL(self), FALSE); iface = FU_STEELSERIES_FIZZ_IMPL_GET_IFACE(self); if (iface->get_fs_id == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "iface->get_fs_id not implemented"); return FALSE; } *id = (*iface->get_fs_id)(self, is_receiver); return TRUE; } gboolean fu_steelseries_fizz_impl_get_file_id(FuSteelseriesFizzImpl *self, gboolean is_receiver, guint8 *id, GError **error) { FuSteelseriesFizzImplInterface *iface; g_return_val_if_fail(FU_IS_STEELSERIES_FIZZ_IMPL(self), FALSE); iface = FU_STEELSERIES_FIZZ_IMPL_GET_IFACE(self); if (iface->get_file_id == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "iface->get_file_id not implemented"); return FALSE; } *id = (*iface->get_file_id)(self, is_receiver); return TRUE; } gboolean fu_steelseries_fizz_impl_get_paired_status(FuSteelseriesFizzImpl *self, guint8 *status, GError **error) { FuSteelseriesFizzImplInterface *iface; g_return_val_if_fail(FU_IS_STEELSERIES_FIZZ_IMPL(self), FALSE); iface = FU_STEELSERIES_FIZZ_IMPL_GET_IFACE(self); if (iface->get_paired_status == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "iface->get_paired_status not implemented"); return FALSE; } return (*iface->get_paired_status)(self, status, error); } gboolean fu_steelseries_fizz_impl_get_connection_status(FuSteelseriesFizzImpl *self, FuSteelseriesFizzConnectionStatus *status, GError **error) { FuSteelseriesFizzImplInterface *iface; g_return_val_if_fail(FU_IS_STEELSERIES_FIZZ_IMPL(self), FALSE); iface = FU_STEELSERIES_FIZZ_IMPL_GET_IFACE(self); if (iface->get_connection_status == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "iface->get_connection_status not implemented"); return FALSE; } return (*iface->get_connection_status)(self, status, error); } gboolean fu_steelseries_fizz_impl_is_updatable(FuSteelseriesFizzImpl *self, FuDevice *device, GError **error) { FuSteelseriesFizzImplInterface *iface; g_return_val_if_fail(FU_IS_STEELSERIES_FIZZ_IMPL(self), FALSE); iface = FU_STEELSERIES_FIZZ_IMPL_GET_IFACE(self); if (iface->is_updatable != NULL) return (*iface->is_updatable)(self, device, error); /* assume device is supported by default */ return TRUE; } gchar * fu_steelseries_fizz_impl_get_serial(FuSteelseriesFizzImpl *self, gboolean tunnel, GError **error) { FuSteelseriesFizzImplInterface *iface; g_return_val_if_fail(FU_IS_STEELSERIES_FIZZ_IMPL(self), NULL); iface = FU_STEELSERIES_FIZZ_IMPL_GET_IFACE(self); if (iface->get_serial == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "iface->get_serial not implemented"); return NULL; } return (*iface->get_serial)(self, tunnel, error); } fwupd-2.0.10/plugins/steelseries/fu-steelseries-fizz-impl.h000066400000000000000000000052361501337203100237570ustar00rootroot00000000000000/* * Copyright 2024 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-steelseries-fizz-struct.h" #define FU_TYPE_STEELSERIES_FIZZ_IMPL (fu_steelseries_fizz_impl_get_type()) G_DECLARE_INTERFACE(FuSteelseriesFizzImpl, fu_steelseries_fizz_impl, FU, STEELSERIES_FIZZ_IMPL, GObject) struct _FuSteelseriesFizzImplInterface { GTypeInterface g_iface; gboolean (*request)(FuSteelseriesFizzImpl *self, const GByteArray *buf, GError **error); GByteArray *(*response)(FuSteelseriesFizzImpl *self, GError **error); gchar *(*get_version)(FuSteelseriesFizzImpl *self, gboolean tunnel, GError **error); gboolean (*get_battery_level)(FuSteelseriesFizzImpl *self, gboolean tunnel, guint8 *level, GError **error); guint8 (*get_fs_id)(FuSteelseriesFizzImpl *self, gboolean is_receiver); guint8 (*get_file_id)(FuSteelseriesFizzImpl *self, gboolean is_receiver); gboolean (*get_paired_status)(FuSteelseriesFizzImpl *self, guint8 *status, GError **error); gboolean (*get_connection_status)(FuSteelseriesFizzImpl *self, FuSteelseriesFizzConnectionStatus *status, GError **error); gboolean (*is_updatable)(FuSteelseriesFizzImpl *self, FuDevice *device, GError **error); gchar *(*get_serial)(FuSteelseriesFizzImpl *self, gboolean tunnel, GError **error); }; gboolean fu_steelseries_fizz_impl_request(FuSteelseriesFizzImpl *self, GByteArray *buf, GError **error); GByteArray * fu_steelseries_fizz_impl_response(FuSteelseriesFizzImpl *self, GError **error); gchar * fu_steelseries_fizz_impl_get_version(FuSteelseriesFizzImpl *self, gboolean tunnel, GError **error); gboolean fu_steelseries_fizz_impl_get_battery_level(FuSteelseriesFizzImpl *self, gboolean tunnel, guint8 *level, GError **error); gboolean fu_steelseries_fizz_impl_get_fs_id(FuSteelseriesFizzImpl *self, gboolean is_receiver, guint8 *id, GError **error); gboolean fu_steelseries_fizz_impl_get_file_id(FuSteelseriesFizzImpl *self, gboolean is_receiver, guint8 *id, GError **error); gboolean fu_steelseries_fizz_impl_get_paired_status(FuSteelseriesFizzImpl *self, guint8 *status, GError **error); gboolean fu_steelseries_fizz_impl_get_connection_status(FuSteelseriesFizzImpl *self, FuSteelseriesFizzConnectionStatus *status, GError **error); gboolean fu_steelseries_fizz_impl_is_updatable(FuSteelseriesFizzImpl *self, FuDevice *device, GError **error); gchar * fu_steelseries_fizz_impl_get_serial(FuSteelseriesFizzImpl *self, gboolean tunnel, GError **error); fwupd-2.0.10/plugins/steelseries/fu-steelseries-fizz-tunnel.c000066400000000000000000000341021501337203100243100ustar00rootroot00000000000000/* * Copyright 2022 Gaël PORTAY * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-steelseries-firmware.h" #include "fu-steelseries-fizz-impl.h" #include "fu-steelseries-fizz-tunnel.h" #define FU_STEELSERIES_FIZZ_TUNNEL_POLL_INTERVAL 20000 /* ms*/ struct _FuSteelseriesFizzTunnel { FuDevice parent_instance; }; G_DEFINE_TYPE(FuSteelseriesFizzTunnel, fu_steelseries_fizz_tunnel, FU_TYPE_DEVICE) static gboolean fu_steelseries_fizz_tunnel_ping(FuDevice *device, gboolean *reached, GError **error) { FuDevice *proxy = fu_device_get_proxy(device); FuSteelseriesFizzConnectionStatus status = FU_STEELSERIES_FIZZ_CONNECTION_STATUS_CONNECTED; guint8 level; g_autoptr(GError) error_local = NULL; g_autofree gchar *version = NULL; if (proxy == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return FALSE; } if (!fu_steelseries_fizz_impl_get_connection_status(FU_STEELSERIES_FIZZ_IMPL(proxy), &status, error)) { g_prefix_error(error, "failed to get connection status: "); return FALSE; } g_debug("FuSteelseriesFizzConnection: %s", fu_steelseries_fizz_connection_status_to_string(status)); *reached = status != FU_STEELSERIES_FIZZ_CONNECTION_STATUS_NOT_CONNECTED; if (!*reached) { /* success */ return TRUE; } /* ping device anyway */ if (!fu_steelseries_fizz_impl_get_battery_level(FU_STEELSERIES_FIZZ_IMPL(proxy), TRUE, &level, &error_local)) { *reached = FALSE; if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT)) { /* failure */ *error = g_steal_pointer(&error_local); return FALSE; } /* success */ return TRUE; } g_debug("BatteryLevel: 0x%02x", level); fu_device_set_battery_level(device, level); /* re-read version after reconnect/update */ version = fu_steelseries_fizz_impl_get_version(FU_STEELSERIES_FIZZ_IMPL(proxy), TRUE, error); if (version == NULL) { *reached = FALSE; g_prefix_error(error, "unable to read version from device %s: ", fu_device_get_id(device)); return FALSE; } fu_device_set_version(device, version); /* nocheck:set-version */ /* success */ return TRUE; } static gboolean fu_steelseries_fizz_tunnel_wait_for_reconnect_cb(FuDevice *device, gpointer user_data, GError **error) { FuDevice *parent = fu_device_get_parent(device); FuSteelseriesFizzConnectionStatus status; gboolean reached; if (!fu_steelseries_fizz_get_connection_status(FU_STEELSERIES_FIZZ(parent), &status, error)) { g_prefix_error(error, "failed to get connection status: "); return FALSE; } g_debug("FuSteelseriesFizzConnection: %s", fu_steelseries_fizz_connection_status_to_string(status)); if (status == FU_STEELSERIES_FIZZ_CONNECTION_STATUS_NOT_CONNECTED) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "device is unreachable"); return FALSE; } /* ping */ if (!fu_steelseries_fizz_tunnel_ping(device, &reached, error)) { g_prefix_error(error, "failed to ping on reconnect: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_steelseries_fizz_tunnel_wait_for_reconnect(FuDevice *device, guint delay, GError **error) { return fu_device_retry_full(device, fu_steelseries_fizz_tunnel_wait_for_reconnect_cb, delay / 1000, 1000, NULL, error); } static gboolean fu_steelseries_fizz_tunnel_detach(FuDevice *device, FuProgress *progress, GError **error) { FuDevice *proxy = fu_device_get_proxy(device); if (proxy == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return FALSE; } if (!fu_steelseries_fizz_impl_is_updatable(FU_STEELSERIES_FIZZ_IMPL(proxy), device, error)) return FALSE; if (!fu_device_has_private_flag(device, FU_STEELSERIES_DEVICE_FLAG_DETACH_BOOTLOADER)) return TRUE; /* switch to bootloader mode only if device needs it */ if (!fu_steelseries_fizz_reset(FU_STEELSERIES_FIZZ(device), FALSE, FU_STEELSERIES_FIZZ_RESET_MODE_BOOTLOADER, error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* success */ return TRUE; } static gboolean fu_steelseries_fizz_tunnel_attach(FuDevice *device, FuProgress *progress, GError **error) { FuDevice *parent = fu_device_get_parent(device); guint remove_delay = fu_device_get_remove_delay(device); g_autoptr(GError) error_local = NULL; fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 67, "sleep"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 33, NULL); if (!fu_steelseries_fizz_reset(FU_STEELSERIES_FIZZ(parent), TRUE, FU_STEELSERIES_FIZZ_RESET_MODE_NORMAL, &error_local)) g_warning("failed to reset: %s", error_local->message); fu_progress_step_done(progress); /* wait for receiver to reset the connection status to 0 */ fu_device_sleep_full(device, 2000, fu_progress_get_child(progress)); /* ms */ remove_delay -= 2000; fu_progress_step_done(progress); if (!fu_steelseries_fizz_tunnel_wait_for_reconnect(device, remove_delay, error)) { g_prefix_error(error, "device %s did not come back: ", fu_device_get_id(device)); return FALSE; } fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_steelseries_fizz_tunnel_probe(FuDevice *device, GError **error) { FuDevice *proxy = fu_device_get_proxy(device); guint16 release; if (proxy == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return FALSE; } /* set the version if the release has been set */ release = fu_usb_device_get_release(FU_USB_DEVICE(proxy)); if (release != 0x0 && fu_device_get_version_format(device) == FWUPD_VERSION_FORMAT_UNKNOWN) { fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_BCD); fu_device_set_version_raw(device, release); } /* add GUIDs in order of priority */ fu_device_add_instance_str(device, "PROTOCOL", "FIZZ_TUNNEL"); fu_device_add_instance_u16(device, "VID", fu_device_get_vid(proxy)); fu_device_add_instance_u16(device, "PID", fu_device_get_pid(proxy)); fu_device_add_instance_u16(device, "REV", release); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "STEELSERIES", "VID", "PROTOCOL", NULL); fu_device_build_instance_id(device, NULL, "STEELSERIES", "VID", "PID", "PROTOCOL", NULL); if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_ADD_INSTANCE_ID_REV)) { fu_device_build_instance_id(device, NULL, "STEELSERIES", "VID", "PID", "REV", "PROTOCOL", NULL); } /* success */ return TRUE; } static gboolean fu_steelseries_fizz_tunnel_setup(FuDevice *device, GError **error) { FuDevice *proxy = fu_device_get_proxy(device); gboolean reached; g_autofree gchar *serial = NULL; g_autoptr(GError) error_local = NULL; if (proxy == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return FALSE; } /* ping */ if (!fu_steelseries_fizz_tunnel_ping(device, &reached, &error_local)) { g_debug("ignoring error for %s on ping: %s", fu_device_get_id(device), error_local->message); /* success */ return TRUE; } serial = fu_steelseries_fizz_impl_get_serial(FU_STEELSERIES_FIZZ_IMPL(proxy), FALSE, &error_local); if (serial != NULL) { fu_device_set_serial(device, serial); } else { g_debug("ignoring error: %s", error_local->message); } if (!reached) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNREACHABLE); /* success */ return TRUE; } /* success */ return TRUE; } static gboolean fu_steelseries_fizz_tunnel_poll(FuDevice *device, GError **error) { FuDevice *parent = fu_device_get_parent(device); FuDevice *proxy = fu_device_get_proxy(device); guint32 calculated_crc; guint32 stored_crc; gboolean reached; guint8 fs = 0; guint8 id = 0; g_autoptr(GError) error_local = NULL; g_autoptr(FuDeviceLocker) locker = NULL; if (!fu_steelseries_fizz_impl_get_fs_id(FU_STEELSERIES_FIZZ_IMPL(proxy), FALSE, &fs, error)) return FALSE; if (!fu_steelseries_fizz_impl_get_file_id(FU_STEELSERIES_FIZZ_IMPL(proxy), FALSE, &id, error)) return FALSE; /* open */ locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; if (!fu_steelseries_fizz_tunnel_ping(device, &reached, error)) { g_prefix_error(error, "failed to ping: "); return FALSE; } if (!reached) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNREACHABLE); /* success */ return TRUE; } /* set reachable here since we ignore crc error while polling device */ fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_UNREACHABLE); if (!fu_steelseries_fizz_get_crc32_fs(FU_STEELSERIES_FIZZ(parent), TRUE, fs, id, &calculated_crc, &stored_crc, &error_local)) { g_debug("ignoring error on get file CRC32 from FS 0x%02x ID 0x%02x: %s", fs, id, error_local->message); /* success */ return TRUE; } if (calculated_crc != stored_crc) g_warning("%s: checksum mismatch, got 0x%08x, expected 0x%08x", fu_device_get_name(device), calculated_crc, stored_crc); /* success */ return TRUE; } static gboolean fu_steelseries_fizz_tunnel_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDevice *parent = fu_device_get_parent(device); FuDevice *proxy = fu_device_get_proxy(device); guint8 fs = 0; guint8 id = 0; if (!fu_steelseries_fizz_impl_get_fs_id(FU_STEELSERIES_FIZZ_IMPL(proxy), FALSE, &fs, error)) return FALSE; if (!fu_steelseries_fizz_impl_get_file_id(FU_STEELSERIES_FIZZ_IMPL(proxy), FALSE, &id, error)) return FALSE; fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, NULL); if (!fu_steelseries_fizz_write_firmware_fs(FU_STEELSERIES_FIZZ(parent), TRUE, fs, id, firmware, fu_progress_get_child(progress), flags, error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static FuFirmware * fu_steelseries_fizz_tunnel_read_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuDevice *parent = fu_device_get_parent(device); FuDevice *proxy = fu_device_get_proxy(device); guint8 fs = 0; guint8 id = 0; g_autoptr(FuFirmware) firmware = NULL; if (!fu_steelseries_fizz_impl_get_fs_id(FU_STEELSERIES_FIZZ_IMPL(proxy), FALSE, &fs, error)) return NULL; if (!fu_steelseries_fizz_impl_get_file_id(FU_STEELSERIES_FIZZ_IMPL(proxy), FALSE, &id, error)) return NULL; fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 100, NULL); firmware = fu_steelseries_fizz_read_firmware_fs(FU_STEELSERIES_FIZZ(parent), TRUE, fs, id, fu_device_get_firmware_size_max(device), fu_progress_get_child(progress), error); if (firmware == NULL) return NULL; fu_progress_step_done(progress); /* success */ return g_steal_pointer(&firmware); } static void fu_steelseries_fizz_tunnel_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 6, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static gchar * fu_steelseries_fizz_tunnel_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint16(version_raw, fu_device_get_version_format(device)); } static void fu_steelseries_fizz_tunnel_class_init(FuSteelseriesFizzTunnelClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->detach = fu_steelseries_fizz_tunnel_detach; device_class->attach = fu_steelseries_fizz_tunnel_attach; device_class->probe = fu_steelseries_fizz_tunnel_probe; device_class->setup = fu_steelseries_fizz_tunnel_setup; device_class->poll = fu_steelseries_fizz_tunnel_poll; device_class->write_firmware = fu_steelseries_fizz_tunnel_write_firmware; device_class->read_firmware = fu_steelseries_fizz_tunnel_read_firmware; device_class->set_progress = fu_steelseries_fizz_tunnel_set_progress; device_class->convert_version = fu_steelseries_fizz_tunnel_convert_version; } static void fu_steelseries_fizz_tunnel_init(FuSteelseriesFizzTunnel *self) { fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_register_private_flag(FU_DEVICE(self), FU_STEELSERIES_DEVICE_FLAG_DETACH_BOOTLOADER); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PROXY_FOR_OPEN); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REFCOUNTED_PROXY); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_AUTO_PAUSE_POLLING); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_add_protocol(FU_DEVICE(self), "com.steelseries.fizz"); fu_device_set_logical_id(FU_DEVICE(self), "tunnel"); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); /* 10 s */ fu_device_set_poll_interval(FU_DEVICE(self), FU_STEELSERIES_FIZZ_TUNNEL_POLL_INTERVAL); fu_device_set_battery_threshold(FU_DEVICE(self), 20); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_STEELSERIES_FIRMWARE); } FuSteelseriesFizzTunnel * fu_steelseries_fizz_tunnel_new(FuSteelseriesFizz *parent) { return g_object_new(FU_TYPE_STEELSERIES_FIZZ_TUNNEL, "parent", FU_DEVICE(parent), NULL); } fwupd-2.0.10/plugins/steelseries/fu-steelseries-fizz-tunnel.h000066400000000000000000000007611501337203100243210ustar00rootroot00000000000000/* * Copyright 2022 Gaël PORTAY * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-steelseries-fizz.h" #define FU_TYPE_STEELSERIES_FIZZ_TUNNEL (fu_steelseries_fizz_tunnel_get_type()) G_DECLARE_FINAL_TYPE(FuSteelseriesFizzTunnel, fu_steelseries_fizz_tunnel, FU, STEELSERIES_FIZZ_TUNNEL, FuDevice) FuSteelseriesFizzTunnel * fu_steelseries_fizz_tunnel_new(FuSteelseriesFizz *parent); fwupd-2.0.10/plugins/steelseries/fu-steelseries-fizz.c000066400000000000000000000537271501337203100230230ustar00rootroot00000000000000/* * Copyright 2022 Gaël PORTAY * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-steelseries-device.h" #include "fu-steelseries-firmware.h" #include "fu-steelseries-fizz-impl.h" #include "fu-steelseries-fizz-tunnel.h" #include "fu-steelseries-fizz.h" #define FU_STEELSERIES_BUFFER_TRANSFER_SIZE 52 struct _FuSteelseriesFizz { FuUsbDevice parent_instance; }; G_DEFINE_TYPE(FuSteelseriesFizz, fu_steelseries_fizz, FU_TYPE_USB_DEVICE) static gboolean fu_steelseries_fizz_command_error_to_error(guint8 cmd, guint8 err, GError **error) { FwupdError err_code; switch (err) { /* success */ case FU_STEELSERIES_FIZZ_COMMAND_ERROR_SUCCESS: return TRUE; case FU_STEELSERIES_FIZZ_COMMAND_ERROR_FILE_NOT_FOUND: err_code = FWUPD_ERROR_NOT_FOUND; break; /* targeted offset is past the file end */ case FU_STEELSERIES_FIZZ_COMMAND_ERROR_FILE_TOO_SHORT: err_code = FWUPD_ERROR_INVALID_DATA; break; /* when internal flash returns error */ case FU_STEELSERIES_FIZZ_COMMAND_ERROR_FLASH_FAILED: err_code = FWUPD_ERROR_INTERNAL; break; /* USB API doesn't have permission to access this file */ case FU_STEELSERIES_FIZZ_COMMAND_ERROR_PERMISSION_DENIED: err_code = FWUPD_ERROR_PERMISSION_DENIED; break; /* USB API doesn't have permission to access this file */ case FU_STEELSERIES_FIZZ_COMMAND_ERROR_OPERATION_NO_SUPPORTED: err_code = FWUPD_ERROR_NOT_SUPPORTED; break; /* fallback */ default: err_code = FWUPD_ERROR_INTERNAL; break; } g_set_error(error, FWUPD_ERROR, err_code, "command 0x%02x returned error 0x%02x", cmd, err); return FALSE; } static gboolean fu_steelseries_fizz_request(FuSteelseriesFizz *self, GByteArray *buf, GError **error) { FuDevice *proxy = fu_device_get_proxy(FU_DEVICE(self)); if (proxy == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, fu_steelseries_fizz_cmd_to_string(buf->data[0]), buf->data, buf->len); return fu_steelseries_fizz_impl_request(FU_STEELSERIES_FIZZ_IMPL(proxy), buf, error); } static GByteArray * fu_steelseries_fizz_response(FuSteelseriesFizz *self, GError **error) { FuDevice *proxy = fu_device_get_proxy(FU_DEVICE(self)); if (proxy == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return NULL; } return fu_steelseries_fizz_impl_response(FU_STEELSERIES_FIZZ_IMPL(proxy), error); } static GByteArray * fu_steelseries_fizz_request_response(FuSteelseriesFizz *self, GByteArray *buf, GError **error) { const FuSteelseriesFizzCmd cmd = buf->data[0]; g_autoptr(FuStructSteelseriesFizzGenericRes) st_res = NULL; g_autoptr(GByteArray) buf_res = NULL; if (!fu_steelseries_fizz_request(self, buf, error)) return NULL; buf_res = fu_steelseries_fizz_response(self, error); if (buf_res == NULL) return NULL; st_res = fu_struct_steelseries_fizz_generic_res_parse(buf_res->data, buf_res->len, 0x0, error); if (st_res == NULL) return NULL; if (fu_struct_steelseries_fizz_generic_res_get_cmd(st_res) != cmd) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "command invalid, got 0x%02x, expected 0x%02x", fu_struct_steelseries_fizz_generic_res_get_cmd(st_res), cmd); return NULL; } if (!fu_steelseries_fizz_command_error_to_error( cmd, fu_struct_steelseries_fizz_generic_res_get_error(st_res), error)) return NULL; return g_steal_pointer(&buf_res); } static gboolean fu_steelseries_fizz_write_fs(FuSteelseriesFizz *self, gboolean tunnel, guint8 fs, guint8 id, GBytes *fw, FuProgress *progress, GError **error) { guint8 cmd = FU_STEELSERIES_FIZZ_CMD_WRITE_ACCESS_FILE; g_autoptr(FuChunkArray) chunks = NULL; if (tunnel) cmd |= FU_STEELSERIES_FIZZ_CMD_TUNNEL_BIT; chunks = fu_chunk_array_new_from_bytes(fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_STEELSERIES_BUFFER_TRANSFER_SIZE); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(FuStructSteelseriesFizzWriteAccessFileReq) st_req = fu_struct_steelseries_fizz_write_access_file_req_new(); g_autoptr(GByteArray) buf_res = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; fu_struct_steelseries_fizz_write_access_file_req_set_cmd(st_req, cmd); fu_struct_steelseries_fizz_write_access_file_req_set_filesystem(st_req, fs); fu_struct_steelseries_fizz_write_access_file_req_set_id(st_req, id); fu_struct_steelseries_fizz_write_access_file_req_set_size( st_req, fu_chunk_get_data_sz(chk)); fu_struct_steelseries_fizz_write_access_file_req_set_offset( st_req, fu_chunk_get_address(chk)); if (!fu_struct_steelseries_fizz_write_access_file_req_set_data( st_req, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; buf_res = fu_steelseries_fizz_request_response(self, st_req, error); if (buf_res == NULL) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_steelseries_fizz_erase_fs(FuSteelseriesFizz *self, gboolean tunnel, guint8 fs, guint8 id, GError **error) { guint8 cmd = FU_STEELSERIES_FIZZ_CMD_ERASE_FILE; g_autoptr(FuStructSteelseriesFizzEraseFileReq) st_req = NULL; g_autoptr(GByteArray) buf_res = NULL; if (tunnel) cmd |= FU_STEELSERIES_FIZZ_CMD_TUNNEL_BIT; st_req = fu_struct_steelseries_fizz_erase_file_req_new(); fu_struct_steelseries_fizz_erase_file_req_set_cmd(st_req, cmd); fu_struct_steelseries_fizz_erase_file_req_set_filesystem(st_req, fs); fu_struct_steelseries_fizz_erase_file_req_set_id(st_req, id); buf_res = fu_steelseries_fizz_request_response(self, st_req, error); return buf_res != NULL; } gboolean fu_steelseries_fizz_reset(FuSteelseriesFizz *self, gboolean tunnel, FuSteelseriesFizzResetMode mode, GError **error) { guint8 cmd = FU_STEELSERIES_FIZZ_CMD_RESET; g_autoptr(FuStructSteelseriesFizzResetReq) st_req = NULL; if (tunnel) cmd |= FU_STEELSERIES_FIZZ_CMD_TUNNEL_BIT; st_req = fu_struct_steelseries_fizz_reset_req_new(); fu_struct_steelseries_fizz_reset_req_set_cmd(st_req, cmd); fu_struct_steelseries_fizz_reset_req_set_mode(st_req, mode); return fu_steelseries_fizz_request(self, st_req, error); } gboolean fu_steelseries_fizz_get_crc32_fs(FuSteelseriesFizz *self, gboolean tunnel, guint8 fs, guint8 id, guint32 *calculated_crc, guint32 *stored_crc, GError **error) { guint8 cmd = FU_STEELSERIES_FIZZ_CMD_FILE_CRC32; g_autoptr(FuStructSteelseriesFizzFileCrc32Req) st_req = NULL; g_autoptr(FuStructSteelseriesFizzFileCrc32Res) st_res = NULL; g_autoptr(GByteArray) buf_res = NULL; if (tunnel) cmd |= FU_STEELSERIES_FIZZ_CMD_TUNNEL_BIT; st_req = fu_struct_steelseries_fizz_file_crc32_req_new(); fu_struct_steelseries_fizz_file_crc32_req_set_cmd(st_req, cmd); fu_struct_steelseries_fizz_file_crc32_req_set_filesystem(st_req, fs); fu_struct_steelseries_fizz_file_crc32_req_set_id(st_req, id); buf_res = fu_steelseries_fizz_request_response(self, st_req, error); if (buf_res == NULL) return FALSE; st_res = fu_struct_steelseries_fizz_file_crc32_res_parse(buf_res->data, buf_res->len, 0x0, error); if (st_res == NULL) return FALSE; *calculated_crc = fu_struct_steelseries_fizz_file_crc32_res_get_calculated(st_res); *stored_crc = fu_struct_steelseries_fizz_file_crc32_res_get_stored(st_res); /* success */ return TRUE; } static gboolean fu_steelseries_fizz_read_fs(FuSteelseriesFizz *self, gboolean tunnel, guint8 fs, guint8 id, guint8 *buf, gsize bufsz, FuProgress *progress, GError **error) { guint8 cmd = FU_STEELSERIES_FIZZ_CMD_READ_ACCESS_FILE; g_autoptr(GPtrArray) chunks = NULL; if (tunnel) cmd |= FU_STEELSERIES_FIZZ_CMD_TUNNEL_BIT; chunks = fu_chunk_array_mutable_new(buf, bufsz, 0x0, 0x0, FU_STEELSERIES_BUFFER_TRANSFER_SIZE); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); const guint8 *data; gsize datasz = 0; g_autoptr(FuStructSteelseriesFizzReadAccessFileReq) st_req = NULL; g_autoptr(FuStructSteelseriesFizzReadAccessFileRes) st_res = NULL; g_autoptr(GByteArray) buf_res = NULL; st_req = fu_struct_steelseries_fizz_read_access_file_req_new(); fu_struct_steelseries_fizz_read_access_file_req_set_cmd(st_req, cmd); fu_struct_steelseries_fizz_read_access_file_req_set_filesystem(st_req, fs); fu_struct_steelseries_fizz_read_access_file_req_set_id(st_req, id); fu_struct_steelseries_fizz_read_access_file_req_set_size(st_req, fu_chunk_get_data_sz(chk)); fu_struct_steelseries_fizz_read_access_file_req_set_offset( st_req, fu_chunk_get_address(chk)); buf_res = fu_steelseries_fizz_request_response(self, st_req, error); if (buf_res == NULL) return FALSE; st_res = fu_struct_steelseries_fizz_read_access_file_res_parse(buf_res->data, buf_res->len, 0x0, error); if (st_res == NULL) return FALSE; data = fu_struct_steelseries_fizz_read_access_file_res_get_data(st_res, &datasz); if (!fu_memcpy_safe(fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), 0x0, data, datasz, 0x0, fu_chunk_get_data_sz(chk), error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_steelseries_fizz_get_paired_status(FuSteelseriesFizz *self, guint8 *status, GError **error) { FuDevice *proxy = fu_device_get_proxy(FU_DEVICE(self)); if (proxy == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return FALSE; } return fu_steelseries_fizz_impl_get_paired_status(FU_STEELSERIES_FIZZ_IMPL(proxy), status, error); } gboolean fu_steelseries_fizz_get_connection_status(FuSteelseriesFizz *self, FuSteelseriesFizzConnectionStatus *status, GError **error) { FuDevice *proxy = fu_device_get_proxy(FU_DEVICE(self)); if (proxy == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return FALSE; } return fu_steelseries_fizz_impl_get_connection_status(FU_STEELSERIES_FIZZ_IMPL(proxy), status, error); } static gboolean fu_steelseries_fizz_ensure_children(FuSteelseriesFizz *self, GError **error) { guint8 status; FuDevice *proxy = fu_device_get_proxy(FU_DEVICE(self)); if (proxy == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return FALSE; } /* not a USB receiver */ if (!fu_device_has_private_flag(FU_DEVICE(self), FU_STEELSERIES_DEVICE_FLAG_IS_RECEIVER)) return TRUE; /* in bootloader mode */ if (fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) return TRUE; if (!fu_steelseries_fizz_get_paired_status(self, &status, error)) { g_prefix_error(error, "failed to get paired status: "); return FALSE; } if (status != 0) { g_autoptr(FuSteelseriesFizzTunnel) paired_device = fu_steelseries_fizz_tunnel_new(self); fu_device_set_proxy(FU_DEVICE(paired_device), FU_DEVICE(proxy)); fu_device_add_child(FU_DEVICE(self), FU_DEVICE(paired_device)); } /* success */ return TRUE; } static gboolean fu_steelseries_fizz_detach(FuDevice *device, FuProgress *progress, GError **error) { FuSteelseriesFizz *self = FU_STEELSERIES_FIZZ(device); g_autoptr(FuDeviceLocker) locker = NULL; locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER) || !fu_device_has_private_flag(device, FU_STEELSERIES_DEVICE_FLAG_DETACH_BOOTLOADER)) return TRUE; /* switch to bootloader mode only if device needs it */ if (!fu_steelseries_fizz_reset(self, FALSE, FU_STEELSERIES_FIZZ_RESET_MODE_BOOTLOADER, error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* success */ return TRUE; } static gboolean fu_steelseries_fizz_attach(FuDevice *device, FuProgress *progress, GError **error) { FuSteelseriesFizz *self = FU_STEELSERIES_FIZZ(device); g_autoptr(GError) error_local = NULL; g_autoptr(FuDeviceLocker) locker = NULL; locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; if (!fu_steelseries_fizz_reset(self, FALSE, FU_STEELSERIES_FIZZ_RESET_MODE_NORMAL, &error_local)) g_warning("failed to reset: %s", error_local->message); fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* success */ return TRUE; } static gboolean fu_steelseries_fizz_setup(FuDevice *device, GError **error) { FuSteelseriesFizz *self = FU_STEELSERIES_FIZZ(device); g_autofree gchar *version = NULL; g_autofree gchar *serial = NULL; g_autoptr(GError) error_local = NULL; FuDevice *proxy = fu_device_get_proxy(device); if (proxy == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return FALSE; } /* in bootloader mode */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) return TRUE; if (fu_device_has_private_flag(device, FU_STEELSERIES_DEVICE_FLAG_IS_RECEIVER)) { if (!fu_steelseries_fizz_ensure_children(self, error)) return FALSE; } version = fu_steelseries_fizz_impl_get_version(FU_STEELSERIES_FIZZ_IMPL(proxy), FALSE, error); if (version == NULL) { g_prefix_error(error, "failed to get version: "); /* nocheck:set-version */ return FALSE; } fu_device_set_version(device, version); if (!fu_device_has_private_flag(device, FU_STEELSERIES_DEVICE_FLAG_IS_RECEIVER)) { /* direct connection */ serial = fu_steelseries_fizz_impl_get_serial(FU_STEELSERIES_FIZZ_IMPL(proxy), FALSE, &error_local); if (serial != NULL) fu_device_set_serial(device, serial); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_ADD_COUNTERPART_GUIDS); } /* success */ return TRUE; } gboolean fu_steelseries_fizz_write_firmware_fs(FuSteelseriesFizz *self, gboolean tunnel, guint8 fs, guint8 id, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { guint32 calculated_crc; guint32 stored_crc; const guint8 *buf; gsize bufsz; g_autoptr(GBytes) blob = NULL; fu_progress_set_id(progress, G_STRLOC); if (tunnel) { fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 13, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 87, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 1, NULL); } else { fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 38, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 60, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 2, NULL); } blob = fu_firmware_get_bytes(firmware, error); if (blob == NULL) return FALSE; buf = fu_bytes_get_data_safe(blob, &bufsz, error); if (buf == NULL) return FALSE; if (!fu_steelseries_fizz_erase_fs(self, tunnel, fs, id, error)) { g_prefix_error(error, "failed to erase FS 0x%02x ID 0x%02x: ", fs, id); return FALSE; } fu_progress_step_done(progress); if (!fu_steelseries_fizz_write_fs(self, tunnel, fs, id, blob, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to write FS 0x%02x ID 0x%02x: ", fs, id); return FALSE; } fu_progress_step_done(progress); if (!fu_steelseries_fizz_get_crc32_fs(self, tunnel, fs, id, &calculated_crc, &stored_crc, error)) { g_prefix_error(error, "failed to get CRC32 FS 0x%02x ID 0x%02x: ", fs, id); return FALSE; } if (calculated_crc != stored_crc) { g_warning("%s: checksum mismatch, got 0x%08x, expected 0x%08x", fu_device_get_name(FU_DEVICE(self)), calculated_crc, stored_crc); } fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_steelseries_fizz_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuSteelseriesFizz *self = FU_STEELSERIES_FIZZ(device); guint8 fs; gboolean is_receiver = FALSE; FuDevice *proxy = fu_device_get_proxy(device); guint8 id = 0; g_autoptr(FuDeviceLocker) locker = NULL; locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; if (proxy == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return FALSE; } is_receiver = fu_device_has_private_flag(device, FU_STEELSERIES_DEVICE_FLAG_IS_RECEIVER); if (!fu_steelseries_fizz_impl_get_fs_id(FU_STEELSERIES_FIZZ_IMPL(proxy), is_receiver, &fs, error)) return FALSE; if (!fu_steelseries_fizz_impl_get_file_id(FU_STEELSERIES_FIZZ_IMPL(proxy), is_receiver, &id, error)) return FALSE; fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, 1); if (!fu_steelseries_fizz_write_firmware_fs(self, FALSE, fs, id, firmware, fu_progress_get_child(progress), flags, error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } FuFirmware * fu_steelseries_fizz_read_firmware_fs(FuSteelseriesFizz *self, gboolean tunnel, guint8 fs, guint8 id, gsize size, FuProgress *progress, GError **error) { g_autoptr(FuFirmware) firmware = fu_steelseries_firmware_new(); g_autoptr(GBytes) blob = NULL; g_autofree guint8 *buf = NULL; fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 100, NULL); buf = g_malloc0(size); if (!fu_steelseries_fizz_read_fs(self, tunnel, fs, id, buf, size, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to read FS 0x%02x ID 0x%02x: ", fs, id); return NULL; } fu_progress_step_done(progress); fu_dump_raw(G_LOG_DOMAIN, "Firmware", buf, size); blob = g_bytes_new_take(g_steal_pointer(&buf), size); if (!fu_firmware_parse_bytes(firmware, blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error)) return NULL; /* success */ return g_steal_pointer(&firmware); } static FuFirmware * fu_steelseries_fizz_read_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuSteelseriesFizz *self = FU_STEELSERIES_FIZZ(device); guint8 fs = 0; guint8 id = 0; gboolean is_receiver; FuDevice *proxy = fu_device_get_proxy(device); g_autoptr(FuFirmware) firmware = NULL; g_autoptr(FuDeviceLocker) locker = NULL; locker = fu_device_locker_new(device, error); if (locker == NULL) return NULL; if (proxy == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no proxy"); return NULL; } fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 100, NULL); is_receiver = fu_device_has_private_flag(device, FU_STEELSERIES_DEVICE_FLAG_IS_RECEIVER); if (!fu_steelseries_fizz_impl_get_fs_id(FU_STEELSERIES_FIZZ_IMPL(proxy), is_receiver, &fs, error)) return NULL; if (!fu_steelseries_fizz_impl_get_file_id(FU_STEELSERIES_FIZZ_IMPL(proxy), is_receiver, &id, error)) return NULL; firmware = fu_steelseries_fizz_read_firmware_fs(self, FALSE, fs, id, fu_device_get_firmware_size_max(device), fu_progress_get_child(progress), error); if (firmware == NULL) return NULL; fu_progress_step_done(progress); /* success */ return g_steal_pointer(&firmware); } static void fu_steelseries_fizz_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 80, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 18, "reload"); } static void fu_steelseries_fizz_class_init(FuSteelseriesFizzClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->detach = fu_steelseries_fizz_detach; device_class->attach = fu_steelseries_fizz_attach; device_class->setup = fu_steelseries_fizz_setup; device_class->write_firmware = fu_steelseries_fizz_write_firmware; device_class->read_firmware = fu_steelseries_fizz_read_firmware; device_class->set_progress = fu_steelseries_fizz_set_progress; } static void fu_steelseries_fizz_init(FuSteelseriesFizz *self) { fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_register_private_flag(FU_DEVICE(self), FU_STEELSERIES_DEVICE_FLAG_IS_RECEIVER); fu_device_register_private_flag(FU_DEVICE(self), FU_STEELSERIES_DEVICE_FLAG_DETACH_BOOTLOADER); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PROXY_FOR_OPEN); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REFCOUNTED_PROXY); fu_device_add_protocol(FU_DEVICE(self), "com.steelseries.fizz"); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_USER_REPLUG); /* 40 s */ fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_STEELSERIES_FIRMWARE); fu_device_set_priority(FU_DEVICE(self), 10); /* better than tunneled device */ } fwupd-2.0.10/plugins/steelseries/fu-steelseries-fizz.h000066400000000000000000000032541501337203100230160ustar00rootroot00000000000000/* * Copyright 2022 Gaël PORTAY * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-steelseries-device.h" #include "fu-steelseries-fizz-struct.h" #define FU_TYPE_STEELSERIES_FIZZ (fu_steelseries_fizz_get_type()) G_DECLARE_FINAL_TYPE(FuSteelseriesFizz, fu_steelseries_fizz, FU, STEELSERIES_FIZZ, FuUsbDevice) FuSteelseriesFizz * fu_steelseries_fizz_new(FuDevice *self); #define FU_STEELSERIES_FIZZ_BATTERY_LEVEL_CHARGING_BIT 0x80U #define FU_STEELSERIES_FIZZ_BATTERY_LEVEL_STATUS_BITS 0x7fU gboolean fu_steelseries_fizz_reset(FuSteelseriesFizz *self, gboolean tunnel, FuSteelseriesFizzResetMode mode, GError **error); gboolean fu_steelseries_fizz_get_crc32_fs(FuSteelseriesFizz *self, gboolean tunnel, guint8 fs, guint8 id, guint32 *calculated_crc, guint32 *stored_crc, GError **error); FuFirmware * fu_steelseries_fizz_read_firmware_fs(FuSteelseriesFizz *self, gboolean tunnel, guint8 fs, guint8 id, gsize size, FuProgress *progress, GError **error); gboolean fu_steelseries_fizz_write_firmware_fs(FuSteelseriesFizz *self, gboolean tunnel, guint8 fs, guint8 id, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error); gboolean fu_steelseries_fizz_get_battery_level(FuSteelseriesFizz *self, gboolean tunnel, guint8 *level, GError **error); gboolean fu_steelseries_fizz_get_connection_status(FuSteelseriesFizz *self, FuSteelseriesFizzConnectionStatus *status, GError **error); fwupd-2.0.10/plugins/steelseries/fu-steelseries-fizz.rs000066400000000000000000000134411501337203100232120ustar00rootroot00000000000000// Copyright 2024 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[repr(u8)] enum FuSteelseriesFizzConnection { Unexpected, Pairing, Disconnected, Connected, } #[derive(ToString)] #[repr(u8)] enum FuSteelseriesFizzConnectionStatus { NotConnected = 0x00, Connected = 0x01, } #[repr(u8)] enum FuSteelseriesFizzResetMode { Normal = 0x00, Bootloader = 0x01, } #[repr(u8)] enum FuSteelseriesFizzFilesystem { Receiver = 0x01, Mouse = 0x02, } #[repr(u8)] enum FuSteelseriesFizzReceiverFilesystemId { Unknown = 0x01, MainBoot = 0x01, FsdataFile = 0x02, FactorySettings = 0x03, MainApp = 0x04, BackupApp = 0x05, ProfilesMouse = 0x06, ProfilesLighting = 0x0F, ProfilesDevice = 0x10, ProfilesReserved = 0x11, Recovery = 0x0D, FreeSpace = 0xF1, } #[repr(u8)] enum FuSteelseriesFizzMouseFilesystemId { SoftDevice = 0x00, ProfilesMouse = 0x06, MainApp = 0x07, BackupApp = 0x08, MsbData = 0x09, FactorySettings = 0x0A, FsdataFile = 0x0B, MainBoot = 0x0C, Recovery = 0x0E, ProfilesLighting = 0x0F, ProfilesDevice = 0x10, FdsPages = 0x12, ProfilesBluetooth = 0x13, FreeSpace = 0xF0, } #[repr(u8)] enum FuSteelseriesFizzCommandError { Success, FileNotFound, FileTooShort, FlashFailed, PermissionDenied, OperationNoSupported, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructSteelseriesFizzHidGetVersionReq { report_id: u8 == 0x04, // GetReport command: u8 == 0x90, // GetVersion mode: u8 == 0x00, // string } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructSteelseriesFizzHidResponse { report_id: u8 = 0x04, // GetReport _data: [u8; 64], } #[derive(ToString)] #[repr(u8)] enum FuSteelseriesFizzCmd { Reset = 0x01, EraseFile = 0x02, WriteAccessFile = 0x03, Version2 = 0x10, Serial2 = 0x12, ReadAccessFile = 0x83, FileCrc32 = 0x84, Version = 0x90, BatteryLevel = 0x92, PairedStatus = 0xBB, ConnectionStatus2 = 0xB0, ConnectionStatus = 0xBC, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructSteelseriesFizzWriteAccessFileReq { cmd: FuSteelseriesFizzCmd = WriteAccessFile, filesystem: u8, id: u8, size: u16le, offset: u32le, data: [u8; 52], } #[derive(New, Default)] #[repr(C, packed)] struct FuStructSteelseriesFizzReadAccessFileReq { cmd: FuSteelseriesFizzCmd = ReadAccessFile, filesystem: u8, id: u8, size: u16le, offset: u32le, } #[derive(Parse)] #[repr(C, packed)] struct FuStructSteelseriesFizzReadAccessFileRes { reserved: [u8; 2], data: [u8; 52], } #[derive(New, Default)] #[repr(C, packed)] struct FuStructSteelseriesFizzEraseFileReq { cmd: FuSteelseriesFizzCmd = EraseFile, filesystem: u8, id: u8, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructSteelseriesFizzResetReq { cmd: FuSteelseriesFizzCmd = Reset, mode: FuSteelseriesFizzResetMode, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructSteelseriesFizzFileCrc32Req { cmd: FuSteelseriesFizzCmd = FileCrc32, filesystem: u8, id: u8, } #[derive(Parse)] #[repr(C, packed)] struct FuStructSteelseriesFizzFileCrc32Res { reserved: [u8; 2], calculated: u32le, stored: u32le, } #[derive(Parse)] #[repr(C, packed)] struct FuStructSteelseriesFizzGenericRes { cmd: FuSteelseriesFizzCmd, error: u8, } // gen1 only #[derive(New, Default)] #[repr(C, packed)] struct FuStructSteelseriesFizzVersionReq { cmd: FuSteelseriesFizzCmd = Version, mode: u8 == 0, // string } // gen1 only #[derive(New, Default)] #[repr(C, packed)] struct FuStructSteelseriesPairedStatusReq { cmd: FuSteelseriesFizzCmd == PairedStatus, } #[derive(Parse)] #[repr(C, packed)] struct FuStructSteelseriesPairedStatusRes { status: u8, } // gen1 only #[derive(New, Default)] #[repr(C, packed)] struct FuStructSteelseriesConnectionStatusReq { cmd: FuSteelseriesFizzCmd == ConnectionStatus, } #[derive(Parse)] #[repr(C, packed)] struct FuStructSteelseriesConnectionStatusRes { reserved: u8, status: u8, } // gen1 only #[derive(New, Default)] #[repr(C, packed)] struct FuStructSteelseriesBatteryLevelReq { cmd: FuSteelseriesFizzCmd = BatteryLevel, } #[derive(Parse)] #[repr(C, packed)] struct FuStructSteelseriesBatteryLevelRes { reserved: u8, level: u8, } // gen2 only #[derive(New, Default)] #[repr(C, packed)] struct FuStructSteelseriesFizzVersion2Req { cmd: FuSteelseriesFizzCmd == Version2, } #[derive(Parse)] #[repr(C, packed)] struct FuStructSteelseriesVersion2Res { reserved: u8, version_receiver: [char; 12], _version_unknown: [char; 12], version_device: [char; 12], } // gen2 only #[derive(New, Default)] #[repr(C, packed)] struct FuStructSteelseriesBatteryLevel2Req { cmd: FuSteelseriesFizzCmd == ConnectionStatus2, // FIXME weird, confirm! } #[derive(Parse)] #[repr(C, packed)] struct FuStructSteelseriesBatteryLevel2Res { reserved: [u8; 3], level: u8, } // gen2 only #[derive(New, Default)] #[repr(C, packed)] struct FuStructSteelseriesConnectionStatus2Req { cmd: FuSteelseriesFizzCmd == ConnectionStatus2, } #[derive(Parse)] #[repr(C, packed)] struct FuStructSteelseriesConnectionStatus2Res { reserved: u8, status: FuSteelseriesFizzConnection, } // gen2 only #[derive(New, Default)] #[repr(C, packed)] struct FuStructSteelseriesSerial2Req { cmd: FuSteelseriesFizzCmd == Serial2, } #[derive(Parse)] #[repr(C, packed)] struct FuStructSteelseriesSerial2Res { reserved: u8, serial: [char; 0x12], } fwupd-2.0.10/plugins/steelseries/fu-steelseries-gamepad.c000066400000000000000000000245001501337203100234220ustar00rootroot00000000000000/* * Copyright 2021 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-steelseries-gamepad-struct.h" #include "fu-steelseries-gamepad.h" struct _FuSteelseriesGamepad { FuSteelseriesDevice parent_instance; }; G_DEFINE_TYPE(FuSteelseriesGamepad, fu_steelseries_gamepad, FU_TYPE_STEELSERIES_DEVICE) static gboolean fu_steelseries_gamepad_cmd_erase(FuSteelseriesGamepad *self, GError **error) { g_autoptr(FuStructSteelseriesGamepadEraseReq) st_req = fu_struct_steelseries_gamepad_erase_req_new(); /* USB receiver for gamepad is using different options */ if (fu_device_has_private_flag(FU_DEVICE(self), FU_STEELSERIES_DEVICE_FLAG_IS_RECEIVER)) { /* USB receiver */ fu_struct_steelseries_gamepad_erase_req_set_length(st_req, 0x01D0); } else { /* gamepad */ fu_struct_steelseries_gamepad_erase_req_set_length(st_req, 0x0200); /* magic is needed for newer gamepad */ fu_struct_steelseries_gamepad_erase_req_set_magic3(st_req, 0x02); } if (!fu_steelseries_device_request(FU_STEELSERIES_DEVICE(self), st_req, error)) { g_prefix_error(error, "unable erase flash block: "); return FALSE; } /* timeout to give some time to erase */ fu_device_sleep(FU_DEVICE(self), 20); /* ms */ return TRUE; } static gboolean fu_steelseries_gamepad_setup(FuDevice *device, GError **error) { FuSteelseriesGamepad *self = FU_STEELSERIES_GAMEPAD(device); g_autofree gchar *bootloader_version = NULL; g_autoptr(FuStructSteelseriesGamepadGetVersionsReq) st_req = fu_struct_steelseries_gamepad_get_versions_req_new(); g_autoptr(FuStructSteelseriesGamepadGetVersionsRes) st_res = NULL; g_autoptr(GByteArray) buf_res = NULL; if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) return TRUE; /* get version of FW and bootloader */ if (!fu_steelseries_device_request(FU_STEELSERIES_DEVICE(self), st_req, error)) return FALSE; buf_res = fu_steelseries_device_response(FU_STEELSERIES_DEVICE(self), error); if (buf_res == NULL) return FALSE; st_res = fu_struct_steelseries_gamepad_get_versions_res_parse(buf_res->data, buf_res->len, 0x0, error); if (st_res == NULL) return FALSE; fu_device_set_version_raw( FU_DEVICE(device), fu_struct_steelseries_gamepad_get_versions_res_get_runtime_version(st_res)); bootloader_version = fu_version_from_uint16( fu_struct_steelseries_gamepad_get_versions_res_get_bootloader_version(st_res), FWUPD_VERSION_FORMAT_BCD); fu_device_set_version_bootloader(device, bootloader_version); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); /* success */ return TRUE; } static gboolean fu_steelseries_gamepad_attach(FuDevice *device, FuProgress *progress, GError **error) { FuSteelseriesGamepad *self = FU_STEELSERIES_GAMEPAD(device); g_autoptr(FuStructSteelseriesGamepadResetReq) st_req = fu_struct_steelseries_gamepad_reset_req_new(); g_autoptr(GError) error_local = NULL; if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) return TRUE; /* switch to runtime mode */ if (!fu_steelseries_device_request(FU_STEELSERIES_DEVICE(self), st_req, &error_local)) g_debug("ignoring error on reset: %s", error_local->message); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_steelseries_gamepad_detach(FuDevice *device, FuProgress *progress, GError **error) { FuSteelseriesGamepad *self = FU_STEELSERIES_GAMEPAD(device); g_autoptr(GError) error_local = NULL; g_autoptr(FuStructSteelseriesGamepadBootloaderModeReq) st_req = fu_struct_steelseries_gamepad_bootloader_mode_req_new(); if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) return TRUE; /* switch to bootloader mode */ if (!fu_steelseries_device_request(FU_STEELSERIES_DEVICE(self), st_req, &error_local)) g_debug("ignoring error on reset: %s", error_local->message); /* controller will be renumbered after switching to bootloader mode */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_steelseries_gamepad_write_firmware_chunk(FuSteelseriesGamepad *self, FuChunk *chunk, guint32 *checksum, GError **error) { guint16 chunk_checksum; g_autoptr(FuStructSteelseriesGamepadWriteChunkReq) st_req = fu_struct_steelseries_gamepad_write_chunk_req_new(); /* block ID, 32B of data then block checksum -- probably not necessary */ fu_struct_steelseries_gamepad_write_chunk_req_set_block_id(st_req, fu_chunk_get_idx(chunk)); if (!fu_struct_steelseries_gamepad_write_chunk_req_set_data(st_req, fu_chunk_get_data(chunk), fu_chunk_get_data_sz(chunk), error)) return FALSE; chunk_checksum = fu_sum16(st_req->data + FU_STRUCT_STEELSERIES_GAMEPAD_WRITE_CHUNK_REQ_OFFSET_DATA, FU_STRUCT_STEELSERIES_GAMEPAD_WRITE_CHUNK_REQ_SIZE_DATA); fu_struct_steelseries_gamepad_write_chunk_req_set_checksum(st_req, chunk_checksum); *checksum += (guint32)chunk_checksum; if (!fu_steelseries_device_request(FU_STEELSERIES_DEVICE(self), st_req, error)) { g_prefix_error(error, "unable to flash block %u: ", fu_chunk_get_idx(chunk)); return FALSE; } /* timeout to give some time to flash the block on device */ fu_device_sleep(FU_DEVICE(self), 10); /* ms */ return TRUE; } static gboolean fu_steelseries_gamepad_write_firmware_chunks(FuSteelseriesGamepad *self, FuChunkArray *chunks, FuProgress *progress, guint32 *checksum, GError **error) { fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chunk = NULL; chunk = fu_chunk_array_index(chunks, i, error); if (chunk == NULL) return FALSE; if (!fu_steelseries_gamepad_write_firmware_chunk(self, chunk, checksum, error)) return FALSE; fu_progress_step_done(progress); } return TRUE; } static gboolean fu_steelseries_gamepad_write_checksum(FuSteelseriesGamepad *self, guint32 checksum, GError **error) { g_autoptr(FuStructSteelseriesGamepadWriteChecksumReq) st_req = fu_struct_steelseries_gamepad_write_checksum_req_new(); g_autoptr(FuStructSteelseriesGamepadWriteChecksumRes) st_res = NULL; g_autoptr(GByteArray) buf_res = NULL; fu_struct_steelseries_gamepad_write_checksum_req_set_checksum(st_req, checksum); if (!fu_steelseries_device_request(FU_STEELSERIES_DEVICE(self), st_req, error)) { g_prefix_error(error, "unable to write checksum: "); return FALSE; } buf_res = fu_steelseries_device_response(FU_STEELSERIES_DEVICE(self), error); if (buf_res == NULL) return FALSE; /* validate checksum */ st_res = fu_struct_steelseries_gamepad_write_checksum_res_parse(buf_res->data, buf_res->len, 0x0, error); if (st_res == NULL) { g_prefix_error(error, "controller is unable to validate checksum: "); return FALSE; } return TRUE; } static gboolean fu_steelseries_gamepad_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuSteelseriesGamepad *self = FU_STEELSERIES_GAMEPAD(device); guint32 checksum = 0; g_autoptr(GBytes) blob = NULL; g_autoptr(FuChunkArray) chunks = NULL; blob = fu_firmware_get_bytes(firmware, error); if (blob == NULL) return FALSE; chunks = fu_chunk_array_new_from_bytes(blob, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_STRUCT_STEELSERIES_GAMEPAD_WRITE_CHUNK_REQ_SIZE_DATA); if (fu_chunk_array_length(chunks) > (G_MAXUINT16 + 1)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "too many firmware chunks for the device"); return FALSE; } fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 98, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 1, NULL); /* erase all first */ if (!fu_steelseries_gamepad_cmd_erase(self, error)) return FALSE; fu_progress_step_done(progress); if (!fu_steelseries_gamepad_write_firmware_chunks(self, chunks, fu_progress_get_child(progress), &checksum, error)) return FALSE; fu_progress_step_done(progress); if (!fu_steelseries_gamepad_write_checksum(self, checksum, error)) return FALSE; fu_progress_step_done(progress); return TRUE; } static void fu_steelseries_gamepad_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 93, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 5, "reload"); } static gchar * fu_steelseries_gamepad_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint16(version_raw, fu_device_get_version_format(device)); } static void fu_steelseries_gamepad_class_init(FuSteelseriesGamepadClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_steelseries_gamepad_setup; device_class->attach = fu_steelseries_gamepad_attach; device_class->detach = fu_steelseries_gamepad_detach; device_class->write_firmware = fu_steelseries_gamepad_write_firmware; device_class->set_progress = fu_steelseries_gamepad_set_progress; device_class->convert_version = fu_steelseries_gamepad_convert_version; } static void fu_steelseries_gamepad_init(FuSteelseriesGamepad *self) { fu_steelseries_device_set_iface_idx_offset(FU_STEELSERIES_DEVICE(self), -1); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_BCD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_COUNTERPART_GUIDS); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_add_protocol(FU_DEVICE(self), "com.steelseries.gamepad"); fu_device_set_firmware_size_max( FU_DEVICE(self), (G_MAXUINT16 + 1) * FU_STRUCT_STEELSERIES_GAMEPAD_WRITE_CHUNK_REQ_SIZE_DATA); } fwupd-2.0.10/plugins/steelseries/fu-steelseries-gamepad.h000066400000000000000000000006261501337203100234320ustar00rootroot00000000000000/* * Copyright 2021 Denis Pynkin * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-steelseries-device.h" #define FU_TYPE_STEELSERIES_GAMEPAD (fu_steelseries_gamepad_get_type()) G_DECLARE_FINAL_TYPE(FuSteelseriesGamepad, fu_steelseries_gamepad, FU, STEELSERIES_GAMEPAD, FuSteelseriesDevice) fwupd-2.0.10/plugins/steelseries/fu-steelseries-gamepad.rs000066400000000000000000000041771501337203100236340ustar00rootroot00000000000000// Copyright 2024 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[repr(u8)] enum FuSteelseriesGamepadCmd { Erase = 0xA1, GetVersions = 0x12, Reset = 0xA6, WorkMode = 0x02, WriteChunk = 0xA3, WriteChecksum = 0xA5, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructSteelseriesGamepadEraseReq { cmd: FuSteelseriesGamepadCmd == Erase, magic1: u8 == 0xAA, magic2: u8 == 0x55, reserved: [u8; 5], length: u16le, reserved: [u8; 3], magic3: u8, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructSteelseriesGamepadGetVersionsReq { cmd: FuSteelseriesGamepadCmd == GetVersions, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructSteelseriesGamepadGetVersionsRes { cmd: FuSteelseriesGamepadCmd == GetVersions, runtime_version: u16le, bootloader_version: u16le, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructSteelseriesGamepadResetReq { cmd: FuSteelseriesGamepadCmd == Reset, magic1: u8 == 0xAA, magic2: u8 == 0x55, } #[repr(u8)] enum FuSteelseriesGamepadMode { ControllerMode = 0x01, BootloaderMode = 0x08, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructSteelseriesGamepadBootloaderModeReq { cmd: FuSteelseriesGamepadCmd == WorkMode, mode: FuSteelseriesGamepadMode == BootloaderMode, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructSteelseriesGamepadWriteChunkReq { cmd: FuSteelseriesGamepadCmd == WriteChunk, block_id: u16le, data: [u8; 32], checksum: u16le, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructSteelseriesGamepadWriteChecksumReq { cmd: FuSteelseriesGamepadCmd == WriteChecksum, magic1: u8 == 0xAA, magic2: u8 == 0x55, checksum: u32le, } #[repr(u8)] enum FuSteelseriesGamepadChecksumStatus { Incorrect = 0x00, Correct = 0x01, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructSteelseriesGamepadWriteChecksumRes { cmd: FuSteelseriesGamepadCmd == WriteChecksum, magic1: u8 == 0xAA, magic2: u8 == 0x55, status: FuSteelseriesGamepadChecksumStatus == Correct, checksum: u32le, } fwupd-2.0.10/plugins/steelseries/fu-steelseries-mouse.c000066400000000000000000000045231501337203100231570ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-steelseries-mouse.h" #define FU_STEELSERIES_TRANSACTION_TIMEOUT 1000 /* ms */ struct _FuSteelseriesMouse { FuUsbDevice parent_instance; }; G_DEFINE_TYPE(FuSteelseriesMouse, fu_steelseries_mouse, FU_TYPE_USB_DEVICE) static gboolean fu_steelseries_mouse_setup(FuDevice *device, GError **error) { gsize actual_len = 0; guint8 data[32]; g_autofree gchar *version = NULL; /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_steelseries_mouse_parent_class)->setup(device, error)) return FALSE; memset(data, 0x00, sizeof(data)); data[0] = 0x16; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_CLASS, FU_USB_RECIPIENT_INTERFACE, 0x09, 0x0200, 0x0000, data, sizeof(data), &actual_len, FU_STEELSERIES_TRANSACTION_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to do control transfer: "); return FALSE; } if (actual_len != 32) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "only wrote %" G_GSIZE_FORMAT "bytes", actual_len); return FALSE; } if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(device), 0x81, /* EP1 IN */ data, sizeof(data), &actual_len, FU_STEELSERIES_TRANSACTION_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to do EP1 transfer: "); return FALSE; } if (actual_len != 32) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "only read %" G_GSIZE_FORMAT "bytes", actual_len); return FALSE; } version = g_strdup_printf("%i.%i.%i", data[0], data[1], data[2]); fu_device_set_version(FU_DEVICE(device), version); /* success */ return TRUE; } static void fu_steelseries_mouse_init(FuSteelseriesMouse *self) { fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_usb_device_add_interface(FU_USB_DEVICE(self), 0x00); } static void fu_steelseries_mouse_class_init(FuSteelseriesMouseClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_steelseries_mouse_setup; } fwupd-2.0.10/plugins/steelseries/fu-steelseries-mouse.h000066400000000000000000000004771501337203100231700ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_STEELSERIES_MOUSE (fu_steelseries_mouse_get_type()) G_DECLARE_FINAL_TYPE(FuSteelseriesMouse, fu_steelseries_mouse, FU, STEELSERIES_MOUSE, FuUsbDevice) fwupd-2.0.10/plugins/steelseries/fu-steelseries-plugin.c000066400000000000000000000056201501337203100233240ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * Copyright 2022 Gaël PORTAY * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-steelseries-fizz-gen1.h" #include "fu-steelseries-fizz-gen2.h" #include "fu-steelseries-fizz-hid.h" #include "fu-steelseries-fizz-tunnel.h" #include "fu-steelseries-fizz.h" #include "fu-steelseries-gamepad.h" #include "fu-steelseries-mouse.h" #include "fu-steelseries-plugin.h" #include "fu-steelseries-sonic.h" struct _FuSteelseriesPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuSteelseriesPlugin, fu_steelseries_plugin, FU_TYPE_PLUGIN) static void fu_steelseries_plugin_init(FuSteelseriesPlugin *self) { } static void fu_steelseries_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "SteelSeriesFizzInterface"); fu_plugin_add_device_gtype(plugin, FU_TYPE_STEELSERIES_FIZZ); fu_plugin_add_device_gtype(plugin, FU_TYPE_STEELSERIES_FIZZ_GEN1); fu_plugin_add_device_gtype(plugin, FU_TYPE_STEELSERIES_FIZZ_GEN2); fu_plugin_add_device_gtype(plugin, FU_TYPE_STEELSERIES_FIZZ_HID); fu_plugin_add_device_gtype(plugin, FU_TYPE_STEELSERIES_FIZZ_TUNNEL); fu_plugin_add_device_gtype(plugin, FU_TYPE_STEELSERIES_GAMEPAD); fu_plugin_add_device_gtype(plugin, FU_TYPE_STEELSERIES_MOUSE); fu_plugin_add_device_gtype(plugin, FU_TYPE_STEELSERIES_SONIC); fu_plugin_add_udev_subsystem(plugin, "hidraw"); } static FuDevice * fu_steelseries_plugin_find_device_by_serial(FuSteelseriesPlugin *self, const gchar *serial) { GPtrArray *devices = fu_plugin_get_devices(FU_PLUGIN(self)); for (guint i = 0; i < devices->len; i++) { FuDevice *device = g_ptr_array_index(devices, i); if (g_strcmp0(serial, fu_device_get_serial(device)) == 0) return device; } return NULL; } static void fu_steelseries_plugin_device_added_all(FuSteelseriesPlugin *self, FuDevice *device) { if (fu_device_get_serial(device) != NULL) { FuDevice *device2 = fu_steelseries_plugin_find_device_by_serial(self, fu_device_get_serial(device)); if (device2 != NULL && device != device2) fu_device_set_equivalent_id(device, fu_device_get_id(device2)); } } static void fu_steelseries_plugin_device_added(FuPlugin *plugin, FuDevice *device) { FuSteelseriesPlugin *self = FU_STEELSERIES_PLUGIN(plugin); GPtrArray *children = fu_device_get_children(device); /* parent then children */ fu_steelseries_plugin_device_added_all(self, device); for (guint i = 0; i < children->len; i++) { FuDevice *child = g_ptr_array_index(children, i); fu_steelseries_plugin_device_added_all(self, child); } } static void fu_steelseries_plugin_class_init(FuSteelseriesPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_steelseries_plugin_constructed; plugin_class->device_added = fu_steelseries_plugin_device_added; } fwupd-2.0.10/plugins/steelseries/fu-steelseries-plugin.h000066400000000000000000000003731501337203100233310ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuSteelseriesPlugin, fu_steelseries_plugin, FU, STEELSERIES_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/steelseries/fu-steelseries-sonic.c000066400000000000000000000732771501337203100231560ustar00rootroot00000000000000/* * Copyright 2022 Gaël PORTAY * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-steelseries-sonic-struct.h" #include "fu-steelseries-sonic.h" #define FU_STEELSERIES_BUFFER_FLASH_TRANSFER_SIZE 128 const guint16 FU_STEELSERIES_SONIC_READ_FROM_RAM_OPCODE[] = {0x00c3U, 0x00c3U, 0x0083U}; const guint16 FU_STEELSERIES_SONIC_READ_FROM_FLASH_OPCODE[] = {0x00c5U, 0x00c5U, 0x0085U}; const guint16 FU_STEELSERIES_SONIC_WRITE_TO_RAM_OPCODE[] = {0x0043U, 0x0043U, 0x0003U}; const guint16 FU_STEELSERIES_SONIC_WRITE_TO_FLASH_OPCODE[] = {0x0045U, 0x0045U, 0x0005U}; const guint16 FU_STEELSERIES_SONIC_ERASE_OPCODE[] = {0x0048U, 0x0048U, 0x0008U}; const guint16 FU_STEELSERIES_SONIC_RESTART_OPCODE[] = {0x0041U, 0x0041U, 0x0001U}; const guint16 FU_STEELSERIES_SONIC_CHIP[] = {0x0002U, 0x0003U, 0x0002U}; const guint32 FU_STEELSERIES_SONIC_FIRMWARE_SIZE[] = {0x9000U, 0x4000U, 0x12000U}; const gchar *FU_STEELSERIES_SONIC_FIRMWARE_ID[] = { "app-nordic.bin", "app-holtek.bin", "mouse-app.bin", }; const guint FU_STEELSERIES_SONIC_WRITE_PROGRESS_STEP_VALUE[][2] = {{5, 95}, {11, 89}, {3, 97}}; struct _FuSteelseriesSonic { FuSteelseriesDevice parent_instance; }; G_DEFINE_TYPE(FuSteelseriesSonic, fu_steelseries_sonic, FU_TYPE_STEELSERIES_DEVICE) static gboolean fu_steelseries_sonic_wireless_status(FuSteelseriesSonic *self, FuSteelseriesSonicWirelessStatus *status, GError **error) { g_autoptr(FuStructSteelseriesSonicWirelessStatusReq) st_req = fu_struct_steelseries_sonic_wireless_status_req_new(); g_autoptr(FuStructSteelseriesSonicWirelessStatusRes) st_res = NULL; g_autoptr(GByteArray) buf_res = NULL; if (!fu_steelseries_device_request(FU_STEELSERIES_DEVICE(self), st_req, error)) return FALSE; buf_res = fu_steelseries_device_response(FU_STEELSERIES_DEVICE(self), error); if (buf_res == NULL) return FALSE; st_res = fu_struct_steelseries_sonic_wireless_status_res_parse(buf_res->data, buf_res->len, 0x0, error); if (st_res == NULL) return FALSE; *status = fu_struct_steelseries_sonic_wireless_status_res_get_status(st_res); /* success */ return TRUE; } static gboolean fu_steelseries_sonic_ensure_battery_state(FuSteelseriesSonic *self, GError **error) { g_autoptr(FuStructSteelseriesSonicBatteryReq) st_req = fu_struct_steelseries_sonic_battery_req_new(); g_autoptr(FuStructSteelseriesSonicBatteryRes) st_res = NULL; g_autoptr(GByteArray) buf_res = NULL; if (!fu_steelseries_device_request(FU_STEELSERIES_DEVICE(self), st_req, error)) return FALSE; buf_res = fu_steelseries_device_response(FU_STEELSERIES_DEVICE(self), error); if (buf_res == NULL) return FALSE; st_res = fu_struct_steelseries_sonic_battery_res_parse(buf_res->data, buf_res->len, 0x0, error); if (st_res == NULL) return FALSE; fu_device_set_battery_level(FU_DEVICE(self), fu_struct_steelseries_sonic_battery_res_get_value(st_res)); /* success */ return TRUE; } static gboolean fu_steelseries_sonic_read_from_ram_chunk(FuSteelseriesSonic *self, FuSteelseriesSonicChip chip, FuChunk *chk, GError **error) { const guint8 *data; gsize datasz = 0; g_autoptr(FuStructSteelseriesSonicReadFromRamReq) st_req = fu_struct_steelseries_sonic_read_from_ram_req_new(); g_autoptr(FuStructSteelseriesSonicReadFromRamRes) st_res = NULL; g_autoptr(GByteArray) buf_res = NULL; fu_struct_steelseries_sonic_read_from_ram_req_set_opcode( st_req, FU_STEELSERIES_SONIC_READ_FROM_RAM_OPCODE[chip]); fu_struct_steelseries_sonic_read_from_ram_req_set_offset(st_req, fu_chunk_get_address(chk)); fu_struct_steelseries_sonic_read_from_ram_req_set_size(st_req, fu_chunk_get_data_sz(chk)); if (!fu_steelseries_device_request(FU_STEELSERIES_DEVICE(self), st_req, error)) return FALSE; buf_res = fu_steelseries_device_response(FU_STEELSERIES_DEVICE(self), error); st_res = fu_struct_steelseries_sonic_read_from_ram_res_parse(buf_res->data, buf_res->len, 0x0, error); if (st_res == NULL) return FALSE; data = fu_struct_steelseries_sonic_read_from_ram_res_get_data(st_res, &datasz); return fu_memcpy_safe(fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), 0x0, data, datasz, 0x0, fu_chunk_get_data_sz(chk), error); } static gboolean fu_steelseries_sonic_read_from_ram(FuSteelseriesSonic *self, FuSteelseriesSonicChip chip, guint32 address, guint8 *buf, guint16 bufsz, FuProgress *progress, GError **error) { g_autoptr(GPtrArray) chunks = NULL; chunks = fu_chunk_array_mutable_new(buf, bufsz, 0x0, 0x0, FU_STRUCT_STEELSERIES_SONIC_READ_FROM_RAM_RES_SIZE_DATA); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_steelseries_sonic_read_from_ram_chunk(self, chip, chk, error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_steelseries_sonic_read_from_flash_chunk(FuSteelseriesSonic *self, FuSteelseriesSonicChip chip, FuChunk *chk, FuProgress *progress, GError **error) { g_autoptr(FuStructSteelseriesSonicReadFromFlashReq) st_req = fu_struct_steelseries_sonic_read_from_flash_req_new(); fu_struct_steelseries_sonic_read_from_flash_req_set_opcode( st_req, FU_STEELSERIES_SONIC_READ_FROM_FLASH_OPCODE[chip]); fu_struct_steelseries_sonic_read_from_flash_req_set_chipid(st_req, FU_STEELSERIES_SONIC_CHIP[chip]); fu_struct_steelseries_sonic_read_from_flash_req_set_offset(st_req, fu_chunk_get_address(chk)); fu_struct_steelseries_sonic_read_from_flash_req_set_size(st_req, fu_chunk_get_data_sz(chk)); if (!fu_steelseries_device_request(FU_STEELSERIES_DEVICE(self), st_req, error)) return FALSE; /* timeout to give some time to read from flash to ram */ fu_device_sleep(FU_DEVICE(self), 15); /* ms */ return fu_steelseries_sonic_read_from_ram(self, chip, fu_chunk_get_address(chk), fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), progress, error); } static gboolean fu_steelseries_sonic_read_from_flash(FuSteelseriesSonic *self, FuSteelseriesSonicChip chip, guint32 address, guint8 *buf, gsize bufsz, FuProgress *progress, GError **error) { g_autoptr(GPtrArray) chunks = NULL; chunks = fu_chunk_array_mutable_new(buf, bufsz, address, 0x0, FU_STEELSERIES_BUFFER_FLASH_TRANSFER_SIZE); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_steelseries_sonic_read_from_flash_chunk(self, chip, chk, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_steelseries_sonic_write_to_ram_chunk(FuSteelseriesSonic *self, FuSteelseriesSonicChip chip, FuChunk *chk, GError **error) { g_autoptr(FuStructSteelseriesSonicWriteToRamReq) st_req = fu_struct_steelseries_sonic_write_to_ram_req_new(); fu_struct_steelseries_sonic_write_to_ram_req_set_opcode( st_req, FU_STEELSERIES_SONIC_WRITE_TO_RAM_OPCODE[chip]); fu_struct_steelseries_sonic_write_to_ram_req_set_offset(st_req, fu_chunk_get_address(chk)); fu_struct_steelseries_sonic_write_to_ram_req_set_size(st_req, fu_chunk_get_data_sz(chk)); if (!fu_struct_steelseries_sonic_write_to_ram_req_set_data(st_req, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; if (!fu_steelseries_device_request(FU_STEELSERIES_DEVICE(self), st_req, error)) return FALSE; /* timeout to give some time to write to ram */ fu_device_sleep(FU_DEVICE(self), 15); /* ms */ return TRUE; } static gboolean fu_steelseries_sonic_write_to_ram(FuSteelseriesSonic *self, FuSteelseriesSonicChip chip, guint16 address, GBytes *fw, FuProgress *progress, GError **error) { g_autoptr(FuChunkArray) chunks = NULL; chunks = fu_chunk_array_new_from_bytes(fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_STRUCT_STEELSERIES_SONIC_WRITE_TO_RAM_REQ_SIZE_DATA); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_steelseries_sonic_write_to_ram_chunk(self, chip, chk, error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_steelseries_sonic_write_to_flash_chunk(FuSteelseriesSonic *self, FuSteelseriesSonicChip chip, FuChunk *chk, FuProgress *progress, GError **error) { g_autoptr(GBytes) chk_blob = NULL; g_autoptr(FuStructSteelseriesSonicWriteToFlashReq) st_req = fu_struct_steelseries_sonic_write_to_flash_req_new(); chk_blob = fu_chunk_get_bytes(chk); if (!fu_steelseries_sonic_write_to_ram(self, chip, fu_chunk_get_address(chk), chk_blob, progress, error)) return FALSE; fu_struct_steelseries_sonic_write_to_flash_req_set_opcode( st_req, FU_STEELSERIES_SONIC_WRITE_TO_FLASH_OPCODE[chip]); fu_struct_steelseries_sonic_write_to_flash_req_set_chipid(st_req, FU_STEELSERIES_SONIC_CHIP[chip]); fu_struct_steelseries_sonic_write_to_flash_req_set_offset(st_req, fu_chunk_get_address(chk)); fu_struct_steelseries_sonic_write_to_flash_req_set_size(st_req, fu_chunk_get_data_sz(chk)); if (!fu_steelseries_device_request(FU_STEELSERIES_DEVICE(self), st_req, error)) return FALSE; /* timeout to give some time to write from ram to flash */ fu_device_sleep(FU_DEVICE(self), 15); /* ms */ return TRUE; } static gboolean fu_steelseries_sonic_write_to_flash(FuSteelseriesSonic *self, FuSteelseriesSonicChip chip, guint32 address, GBytes *fw, FuProgress *progress, GError **error) { g_autoptr(FuChunkArray) chunks = NULL; chunks = fu_chunk_array_new_from_bytes(fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_STEELSERIES_BUFFER_FLASH_TRANSFER_SIZE); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_steelseries_sonic_write_to_flash_chunk(self, chip, chk, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_steelseries_sonic_erase(FuSteelseriesSonic *self, FuSteelseriesSonicChip chip, FuProgress *progress, GError **error) { g_autoptr(FuStructSteelseriesSonicEraseReq) st_req = fu_struct_steelseries_sonic_erase_req_new(); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_ERASE); fu_progress_set_steps(progress, 1); fu_struct_steelseries_sonic_erase_req_set_opcode(st_req, FU_STEELSERIES_SONIC_ERASE_OPCODE[chip]); fu_struct_steelseries_sonic_erase_req_set_chipid(st_req, FU_STEELSERIES_SONIC_CHIP[chip]); if (!fu_steelseries_device_request(FU_STEELSERIES_DEVICE(self), st_req, error)) return FALSE; /* timeout to give some time to erase flash */ fu_device_sleep_full(FU_DEVICE(self), 1000, fu_progress_get_child(progress)); /* ms */ fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_steelseries_sonic_restart(FuSteelseriesSonic *self, FuSteelseriesSonicChip chip, FuProgress *progress, GError **error) { g_autoptr(FuStructSteelseriesSonicRestartReq) st_req = fu_struct_steelseries_sonic_restart_req_new(); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_RESTART); fu_progress_set_steps(progress, 1); fu_struct_steelseries_sonic_restart_req_set_opcode( st_req, FU_STEELSERIES_SONIC_RESTART_OPCODE[chip]); if (!fu_steelseries_device_request(FU_STEELSERIES_DEVICE(self), st_req, error)) return FALSE; /* timeout to give some time to restart chip */ fu_device_sleep_full(FU_DEVICE(self), 3000, progress); /* ms */ fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_steelseries_sonic_wait_for_connect_cb(FuDevice *device, gpointer user_data, GError **error) { FuSteelseriesSonic *self = FU_STEELSERIES_SONIC(device); FuSteelseriesSonicWirelessStatus *wl_status = (FuSteelseriesSonicWirelessStatus *)user_data; if (!fu_steelseries_sonic_wireless_status(self, wl_status, error)) { g_prefix_error(error, "failed to get wireless status: "); return FALSE; } g_debug("WirelessStatus: %s", fu_steelseries_sonic_wireless_status_to_string(*wl_status)); if (*wl_status != FU_STEELSERIES_SONIC_WIRELESS_STATUS_CONNECTED) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "device is unreachable"); return FALSE; } /* success */ return TRUE; } static gboolean fu_steelseries_sonic_wait_for_connect(FuSteelseriesSonic *self, guint delay, FuProgress *progress, GError **error) { FuSteelseriesSonicWirelessStatus wl_status; g_autoptr(FwupdRequest) request = NULL; g_autoptr(GError) error_local = NULL; g_autofree gchar *msg = NULL; if (!fu_steelseries_sonic_wireless_status(self, &wl_status, error)) { g_prefix_error(error, "failed to get wireless status: "); return FALSE; } g_debug("WirelessStatus: %s", fu_steelseries_sonic_wireless_status_to_string(wl_status)); if (wl_status == FU_STEELSERIES_SONIC_WIRELESS_STATUS_CONNECTED) { /* success */ return TRUE; } /* the user has to do something */ msg = g_strdup_printf("%s needs to be connected to start the update. " "Please put the switch button underneath to 2.4G, or " "click on any button to reconnect it.", fu_device_get_name(FU_DEVICE(self))); request = fwupd_request_new(); fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_IMMEDIATE); fwupd_request_set_id(request, FWUPD_REQUEST_ID_PRESS_UNLOCK); fwupd_request_set_message(request, msg); if (!fu_device_emit_request(FU_DEVICE(self), request, progress, error)) return FALSE; if (!fu_device_retry_full(FU_DEVICE(self), fu_steelseries_sonic_wait_for_connect_cb, delay / 1000, 1000, &wl_status, &error_local)) g_debug("%s", error_local->message); if (wl_status != FU_STEELSERIES_SONIC_WIRELESS_STATUS_CONNECTED) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NEEDS_USER_ACTION, msg); return FALSE; } /* success */ return TRUE; } static gboolean fu_steelseries_sonic_attach(FuDevice *device, FuProgress *progress, GError **error) { FuSteelseriesSonic *self = FU_STEELSERIES_SONIC(device); FuSteelseriesSonicChip chip; g_autoptr(FwupdRequest) request = NULL; g_autofree gchar *msg = NULL; fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 50, "mouse"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 50, "holtek"); /* mouse */ chip = FU_STEELSERIES_SONIC_CHIP_MOUSE; if (!fu_steelseries_sonic_restart(self, chip, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to restart chip %u: ", chip); return FALSE; } fu_progress_step_done(progress); /* USB receiver (nordic, holtek; same command) */ chip = FU_STEELSERIES_SONIC_CHIP_HOLTEK; if (!fu_steelseries_sonic_restart(self, chip, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to restart chip %u: ", chip); return FALSE; } fu_progress_step_done(progress); /* the user has to do something */ msg = g_strdup_printf("%s needs to be manually restarted to complete the update. " "Please unplug the 2.4G USB Wireless adapter and then re-plug it.", fu_device_get_name(device)); request = fwupd_request_new(); fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_IMMEDIATE); fwupd_request_set_id(request, FWUPD_REQUEST_ID_REMOVE_REPLUG); fwupd_request_add_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); fwupd_request_set_message(request, msg); if (!fu_device_emit_request(FU_DEVICE(self), request, progress, error)) return FALSE; /* success */ fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_steelseries_sonic_prepare(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuSteelseriesSonic *self = FU_STEELSERIES_SONIC(device); if (!fu_steelseries_sonic_wait_for_connect(self, fu_device_get_remove_delay(FU_DEVICE(self)), progress, error)) return FALSE; if (!fu_steelseries_sonic_ensure_battery_state(self, error)) { g_prefix_error(error, "failed to get battery state: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_steelseries_sonic_write_chip(FuSteelseriesSonic *self, FuSteelseriesSonicChip chip, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { const guint8 *buf; gsize bufsz; g_autoptr(FuFirmware) fw = NULL; g_autoptr(GBytes) blob = NULL; fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, FU_STEELSERIES_SONIC_WRITE_PROGRESS_STEP_VALUE[chip][0], NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, FU_STEELSERIES_SONIC_WRITE_PROGRESS_STEP_VALUE[chip][1], NULL); fw = fu_firmware_get_image_by_id(firmware, FU_STEELSERIES_SONIC_FIRMWARE_ID[chip], error); if (fw == NULL) return FALSE; blob = fu_firmware_get_bytes(fw, error); if (blob == NULL) return FALSE; buf = fu_bytes_get_data_safe(blob, &bufsz, error); if (buf == NULL) return FALSE; if (!fu_steelseries_sonic_erase(self, chip, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to erase chip %u: ", chip); return FALSE; } fu_progress_step_done(progress); if (!fu_steelseries_sonic_write_to_flash(self, chip, 0x0, blob, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to write to flash chip %u: ", chip); return FALSE; } fu_progress_step_done(progress); /* success */ return TRUE; } static FuFirmware * fu_steelseries_sonic_read_chip(FuSteelseriesSonic *self, FuSteelseriesSonicChip chip, FuProgress *progress, GError **error) { gsize bufsz; g_autoptr(GBytes) blob = NULL; g_autofree guint8 *buf = NULL; fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, 1); bufsz = FU_STEELSERIES_SONIC_FIRMWARE_SIZE[chip]; buf = g_malloc0(bufsz); if (!fu_steelseries_sonic_read_from_flash(self, chip, 0x0, buf, bufsz, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to read from flash chip %u: ", chip); return NULL; } fu_progress_step_done(progress); blob = g_bytes_new_take(g_steal_pointer(&buf), bufsz); return fu_firmware_new_from_bytes(blob); } static gboolean fu_steelseries_sonic_verify_chip(FuSteelseriesSonic *self, FuSteelseriesSonicChip chip, FuFirmware *firmware, FuProgress *progress, GError **error) { g_autoptr(FuFirmware) fw_tmp = NULL; g_autoptr(FuFirmware) fw = NULL; g_autoptr(GBytes) blob_tmp = NULL; g_autoptr(GBytes) blob = NULL; fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 100, NULL); fw = fu_firmware_get_image_by_id(firmware, FU_STEELSERIES_SONIC_FIRMWARE_ID[chip], error); if (fw == NULL) return FALSE; blob = fu_firmware_get_bytes(fw, error); if (blob == NULL) return FALSE; fw_tmp = fu_steelseries_sonic_read_chip(self, chip, fu_progress_get_child(progress), error); if (fw_tmp == NULL) { g_prefix_error(error, "failed to read from flash chip %u: ", chip); return FALSE; } blob_tmp = fu_firmware_get_bytes(fw_tmp, error); if (blob_tmp == NULL) return FALSE; if (!fu_bytes_compare(blob_tmp, blob, error)) { fu_dump_raw(G_LOG_DOMAIN, "Verify", g_bytes_get_data(blob_tmp, NULL), g_bytes_get_size(blob_tmp)); return FALSE; } fu_progress_step_done(progress); /* success */ return TRUE; } static FuFirmware * fu_steelseries_sonic_read_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuSteelseriesSonic *self = FU_STEELSERIES_SONIC(device); FuSteelseriesSonicChip chip; g_autoptr(FuFirmware) firmware = fu_archive_firmware_new(); g_autoptr(FuFirmware) firmware_nordic = NULL; g_autoptr(FuFirmware) firmware_holtek = NULL; g_autoptr(FuFirmware) firmware_mouse = NULL; if (!fu_steelseries_sonic_wait_for_connect(self, fu_device_get_remove_delay(device), progress, error)) return NULL; fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 18, "nordic"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 8, "holtek"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 73, "mouse"); fu_archive_firmware_set_format(FU_ARCHIVE_FIRMWARE(firmware), FU_ARCHIVE_FORMAT_ZIP); fu_archive_firmware_set_compression(FU_ARCHIVE_FIRMWARE(firmware), FU_ARCHIVE_COMPRESSION_NONE); /* nordic */ chip = FU_STEELSERIES_SONIC_CHIP_NORDIC; firmware_nordic = fu_steelseries_sonic_read_chip(self, chip, fu_progress_get_child(progress), error); if (firmware_nordic == NULL) return NULL; fu_firmware_set_id(firmware_nordic, FU_STEELSERIES_SONIC_FIRMWARE_ID[chip]); fu_firmware_add_image(firmware, firmware_nordic); fu_progress_step_done(progress); /* holtek */ chip = FU_STEELSERIES_SONIC_CHIP_HOLTEK; firmware_holtek = fu_steelseries_sonic_read_chip(self, chip, fu_progress_get_child(progress), error); if (firmware_holtek == NULL) return NULL; fu_firmware_set_id(firmware_holtek, FU_STEELSERIES_SONIC_FIRMWARE_ID[chip]); fu_firmware_add_image(firmware, firmware_holtek); fu_progress_step_done(progress); /* mouse */ chip = FU_STEELSERIES_SONIC_CHIP_MOUSE; firmware_mouse = fu_steelseries_sonic_read_chip(self, chip, fu_progress_get_child(progress), error); if (firmware_mouse == NULL) return NULL; fu_firmware_set_id(firmware_mouse, FU_STEELSERIES_SONIC_FIRMWARE_ID[chip]); fu_firmware_add_image(firmware, firmware_mouse); fu_progress_step_done(progress); /* success */ fu_firmware_set_id(firmware, FU_FIRMWARE_ID_PAYLOAD); return g_steal_pointer(&firmware); } static gboolean fu_steelseries_sonic_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuSteelseriesSonic *self = FU_STEELSERIES_SONIC(device); FuSteelseriesSonicChip chip; fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 34, "device-write-mouse"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 30, "device-verify-mouse"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 17, "device-write-nordic"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 7, "device-verify-nordic"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 8, "device-write-holtek"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 3, "device-verify-holtek"); /* mouse */ chip = FU_STEELSERIES_SONIC_CHIP_MOUSE; if (!fu_steelseries_sonic_write_chip(self, chip, firmware, fu_progress_get_child(progress), flags, error)) return FALSE; fu_progress_step_done(progress); if (!fu_steelseries_sonic_verify_chip(self, chip, firmware, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* nordic */ chip = FU_STEELSERIES_SONIC_CHIP_NORDIC; if (!fu_steelseries_sonic_write_chip(self, chip, firmware, fu_progress_get_child(progress), flags, error)) return FALSE; fu_progress_step_done(progress); if (!fu_steelseries_sonic_verify_chip(self, chip, firmware, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* holtek */ chip = FU_STEELSERIES_SONIC_CHIP_HOLTEK; if (!fu_steelseries_sonic_write_chip(self, FU_STEELSERIES_SONIC_CHIP_HOLTEK, firmware, fu_progress_get_child(progress), flags, error)) return FALSE; fu_progress_step_done(progress); if (!fu_steelseries_sonic_verify_chip(self, chip, firmware, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_steelseries_sonic_parse_firmware(FuFirmware *firmware, FuFirmwareParseFlags flags, GError **error) { guint32 checksum_tmp; guint32 checksum; g_autoptr(GBytes) blob = NULL; blob = fu_firmware_get_bytes(firmware, error); if (blob == NULL) return FALSE; if (!fu_memread_uint32_safe(g_bytes_get_data(blob, NULL), g_bytes_get_size(blob), g_bytes_get_size(blob) - sizeof(checksum), &checksum, G_LITTLE_ENDIAN, error)) return FALSE; checksum_tmp = fu_crc32(FU_CRC_KIND_B32_STANDARD, g_bytes_get_data(blob, NULL), g_bytes_get_size(blob) - sizeof(checksum_tmp)); checksum_tmp = ~checksum_tmp; if (checksum_tmp != checksum) { if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "checksum mismatch for %s, got 0x%08x, expected 0x%08x", fu_firmware_get_id(firmware), checksum_tmp, checksum); return FALSE; } g_debug("ignoring checksum mismatch, got 0x%08x, expected 0x%08x", checksum_tmp, checksum); } fu_firmware_add_flag(firmware, FU_FIRMWARE_FLAG_HAS_CHECKSUM); /* success */ return TRUE; } static FuFirmware * fu_steelseries_sonic_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { g_autoptr(FuFirmware) firmware_nordic = NULL; g_autoptr(FuFirmware) firmware_holtek = NULL; g_autoptr(FuFirmware) firmware_mouse = NULL; g_autoptr(FuFirmware) firmware = NULL; firmware = fu_archive_firmware_new(); if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; /* mouse */ firmware_mouse = fu_firmware_get_image_by_id( firmware, FU_STEELSERIES_SONIC_FIRMWARE_ID[FU_STEELSERIES_SONIC_CHIP_MOUSE], error); if (firmware_mouse == NULL) return NULL; if (!fu_steelseries_sonic_parse_firmware(firmware_mouse, flags, error)) return NULL; /* nordic */ firmware_nordic = fu_firmware_get_image_by_id( firmware, FU_STEELSERIES_SONIC_FIRMWARE_ID[FU_STEELSERIES_SONIC_CHIP_NORDIC], error); if (firmware_nordic == NULL) return NULL; if (!fu_steelseries_sonic_parse_firmware(firmware_nordic, flags, error)) return NULL; /* holtek */ firmware_holtek = fu_firmware_get_image_by_id( firmware, FU_STEELSERIES_SONIC_FIRMWARE_ID[FU_STEELSERIES_SONIC_CHIP_HOLTEK], error); if (firmware_holtek == NULL) return NULL; if (!fu_steelseries_sonic_parse_firmware(firmware_holtek, flags, error)) return NULL; /* success */ return g_steal_pointer(&firmware); } static void fu_steelseries_sonic_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 92, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 5, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 3, "reload"); } static void fu_steelseries_sonic_class_init(FuSteelseriesSonicClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->attach = fu_steelseries_sonic_attach; device_class->prepare = fu_steelseries_sonic_prepare; device_class->read_firmware = fu_steelseries_sonic_read_firmware; device_class->write_firmware = fu_steelseries_sonic_write_firmware; device_class->prepare_firmware = fu_steelseries_sonic_prepare_firmware; device_class->set_progress = fu_steelseries_sonic_set_progress; } static void fu_steelseries_sonic_init(FuSteelseriesSonic *self) { fu_steelseries_device_set_iface_idx_offset(FU_STEELSERIES_DEVICE(self), -1); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_BCD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_add_request_flag(FU_DEVICE(self), FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); fu_device_add_protocol(FU_DEVICE(self), "com.steelseries.sonic"); fu_device_set_install_duration(FU_DEVICE(self), 120); /* 2 min */ fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_USER_REPLUG); /* 40 s */ fu_device_set_battery_level(FU_DEVICE(self), FWUPD_BATTERY_LEVEL_INVALID); fu_device_set_battery_threshold(FU_DEVICE(self), 20); } fwupd-2.0.10/plugins/steelseries/fu-steelseries-sonic.h000066400000000000000000000006131501337203100231430ustar00rootroot00000000000000/* * Copyright 2022 Gaël PORTAY * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-steelseries-device.h" #define FU_TYPE_STEELSERIES_SONIC (fu_steelseries_sonic_get_type()) G_DECLARE_FINAL_TYPE(FuSteelseriesSonic, fu_steelseries_sonic, FU, STEELSERIES_SONIC, FuSteelseriesDevice) fwupd-2.0.10/plugins/steelseries/fu-steelseries-sonic.rs000066400000000000000000000044401501337203100233420ustar00rootroot00000000000000// Copyright 2024 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later enum FuSteelseriesSonicChip { Nordic, Holtek, Mouse, } #[derive(ToString)] #[repr(u8)] enum FuSteelseriesSonicWirelessStatus { Off, // WDS not initiated, radio is off Idle, // WDS initiated, USB receiver is transmitting beacon (mouse will not have this state) Search, // WDS initiated, mouse is trying to synchronize to receiver (receiver will not have this state) Locked, // USB receiver and mouse are synchronized, but not necessarily connected Connected, // USB receiver and mouse are connected Terminated, // Mouse has been disconnected from the USB receiver } #[repr(u8)] enum FuSteelseriesSonicOpcode8 { Battery = 0xAA, WirelessStatus = 0xE8, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructSteelseriesSonicWirelessStatusReq { opcode: FuSteelseriesSonicOpcode8 == WirelessStatus, } #[derive(Parse)] #[repr(C, packed)] struct FuStructSteelseriesSonicWirelessStatusRes { status: FuSteelseriesSonicWirelessStatus, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructSteelseriesSonicBatteryReq { opcode: FuSteelseriesSonicOpcode8 == Battery, bat_mode: u8 == 0x01, // percentage } #[derive(Parse)] #[repr(C, packed)] struct FuStructSteelseriesSonicBatteryRes { value: u16le, } #[derive(New)] #[repr(C, packed)] struct FuStructSteelseriesSonicRestartReq { opcode: u16le, } #[derive(New)] #[repr(C, packed)] struct FuStructSteelseriesSonicEraseReq { opcode: u16le, chipid: u16le, } #[derive(New)] #[repr(C, packed)] struct FuStructSteelseriesSonicReadFromRamReq { opcode: u16le, offset: u16le, size: u16le, } #[derive(Parse)] #[repr(C, packed)] struct FuStructSteelseriesSonicReadFromRamRes { data: [u8; 48], } #[derive(New)] #[repr(C, packed)] struct FuStructSteelseriesSonicReadFromFlashReq { opcode: u16le, chipid: u16le, offset: u32le, size: u16le, } #[derive(New)] #[repr(C, packed)] struct FuStructSteelseriesSonicWriteToRamReq { opcode: u16le, offset: u16le, size: u16le, data: [u8; 48], } #[derive(New)] #[repr(C, packed)] struct FuStructSteelseriesSonicWriteToFlashReq { opcode: u16le, chipid: u16le, offset: u32le, size: u16le, } fwupd-2.0.10/plugins/steelseries/meson.build000066400000000000000000000020751501337203100210700ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginSteelSeries"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('steelseries.quirk') plugin_builtins += static_library('fu_plugin_steelseries', rustgen.process([ 'fu-steelseries-fizz.rs', 'fu-steelseries-gamepad.rs', 'fu-steelseries-sonic.rs', ]), sources: [ 'fu-steelseries-plugin.c', 'fu-steelseries-device.c', 'fu-steelseries-firmware.c', 'fu-steelseries-fizz-hid.c', 'fu-steelseries-fizz-tunnel.c', 'fu-steelseries-fizz.c', 'fu-steelseries-fizz-gen1.c', 'fu-steelseries-fizz-gen2.c', 'fu-steelseries-fizz-impl.c', 'fu-steelseries-mouse.c', 'fu-steelseries-gamepad.c', 'fu-steelseries-sonic.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files( 'tests/steelseries-aerox-3-wireless.json', 'tests/steelseries-nova5.json', 'tests/steelseries-rival-3-wireless.json', 'tests/steelseries-stratus-duo.json', 'tests/steelseries-stratus-duo-rx.json', ) fwupd-2.0.10/plugins/steelseries/steelseries.quirk000066400000000000000000000105741501337203100223350ustar00rootroot00000000000000# Stratus Duo RX [USB\VID_1038&PID_1430] Plugin = steelseries GType = FuSteelseriesGamepad Name = Stratus Duo RX Icon = usb-receiver CounterpartGuid = USB\VID_1038&PID_1432 Flags = is-receiver [USB\VID_1038&PID_1432] Plugin = steelseries GType = FuSteelseriesGamepad Name = Stratus Duo RX bootloader Icon = usb-receiver CounterpartGuid = USB\VID_1038&PID_1430 Flags = is-receiver,is-bootloader # Stratus Duo [USB\VID_1038&PID_1431] Plugin = steelseries GType = FuSteelseriesGamepad Name = Stratus Duo Gamepad Icon = input-gaming CounterpartGuid = USB\VID_1038&PID_1433 [USB\VID_1038&PID_1433] Plugin = steelseries GType = FuSteelseriesGamepad Name = Stratus Duo bootloader CounterpartGuid = USB\VID_1038&PID_1431 Flags = is-bootloader # Stratus+ [USB\VID_1038&PID_1434] Plugin = steelseries GType = FuSteelseriesGamepad Name = Stratus+ gamepad Icon = input-gaming CounterpartGuid = USB\VID_1038&PID_1435 [USB\VID_1038&PID_1435] Plugin = steelseries GType = FuSteelseriesGamepad Name = Stratus+ bootloader CounterpartGuid = USB\VID_1038&PID_1434 Flags = is-bootloader # Rival 100 [USB\VID_1038&PID_1702] Plugin = steelseries GType = FuSteelseriesMouse Summary = An optical gaming mouse Icon = input-mouse # Rival 3 Wireless [USB\VID_1038&PID_1830] Plugin = steelseries GType = FuSteelseriesSonic Icon = input-mouse # Aerox 3 Wireless [HIDRAW\VEN_0111&DEV_183A] Plugin = steelseries GType = FuSteelseriesFizzHid Name = Aerox 3 Wireless Mouse via Bluetooth Icon = input-mouse [USB\VID_1038&PID_1838] Plugin = steelseries GType = FuSteelseriesFizz ProxyGType = FuSteelseriesFizzGen1 Name = Aerox 3 Wireless USB Receiver Icon = usb-receiver CounterpartGuid = USB\VID_1038&PID_1839 FirmwareSize = 0x23000 Flags = is-receiver InstallDuration = 13 [USB\VID_1038&PID_1839] Plugin = steelseries GType = FuSteelseriesFizz ProxyGType = FuSteelseriesFizzGen1 Name = Aerox 3 Wireless USB Receiver bootloader Icon = usb-receiver CounterpartGuid = USB\VID_1038&PID_1838 FirmwareSize = 0x23000 Flags = is-bootloader,is-receiver,~usable-during-update InstallDuration = 13 [STEELSERIES\VID_1038&PID_1838&PROTOCOL_FIZZ_TUNNEL] Plugin = steelseries GType = FuSteelseriesFizzTunnel Name = Aerox 3 Wireless Mouse via USB Receiver Icon = input-mouse FirmwareSize = 0x27000 InstallDuration = 37 [USB\VID_1038&PID_183A] Plugin = steelseries GType = FuSteelseriesFizz ProxyGType = FuSteelseriesFizzGen1 Name = Aerox 3 Wireless Mouse Icon = input-mouse CounterpartGuid = USB\VID_1038&PID_183B,HIDRAW\VEN_0111&DEV_183A FirmwareSize = 0x27000 InstallDuration = 13 [USB\VID_1038&PID_183B] Plugin = steelseries GType = FuSteelseriesFizz ProxyGType = FuSteelseriesFizzGen1 Name = Aerox 3 Wireless Mouse bootloader Icon = input-mouse CounterpartGuid = USB\VID_1038&PID_183A FirmwareSize = 0x27000 Flags = is-bootloader,~usable-during-update InstallDuration = 13 # Arctis Nova 5 [USB\VID_1038&PID_2230] Plugin = steelseries GType = FuSteelseriesFizz ProxyGType = FuSteelseriesFizzGen2 Name = Arctis Nova 5 Headset Icon = usb-headset FirmwareSize = 0x14B000 SteelSeriesFizzInterface = 0 Flags = ~usable-during-update,detach-bootloader InstallDuration = 87 CounterpartGuid = USB\VID_1038&PID_2231,STEELSERIES\VID_1038&PID_2232&PROTOCOL_FIZZ_TUNNEL [USB\VID_1038&PID_2231] Plugin = steelseries GType = FuSteelseriesFizz ProxyGType = FuSteelseriesFizzGen2 Name = Arctis Nova 5 Headset bootloader Icon = usb-headset FirmwareSize = 0x14B000 CounterpartGuid = USB\VID_1038&PID_2230,STEELSERIES\VID_1038&PID_2232&PROTOCOL_FIZZ_TUNNEL Flags = is-bootloader,~usable-during-update InstallDuration = 87 [STEELSERIES\VID_1038&PID_2232&PROTOCOL_FIZZ_TUNNEL] Plugin = steelseries GType = FuSteelseriesFizzTunnel Name = Arctis Nova 5 Headset via USB Receiver Icon = usb-headset Flags = ~usable-during-update,detach-bootloader FirmwareSize = 0x14B000 CounterpartGuid = USB\VID_1038&PID_2230,USB\VID_1038&PID_2231 [USB\VID_1038&PID_2232] Plugin = steelseries GType = FuSteelseriesFizz ProxyGType = FuSteelseriesFizzGen2 Name = Arctis Nova 5 USB Receiver Icon = usb-receiver CounterpartGuid = USB\VID_1038&PID_2233 FirmwareSize = 0x14B000 Flags = is-receiver,detach-bootloader InstallDuration = 165 [USB\VID_1038&PID_2233] Plugin = steelseries GType = FuSteelseriesFizz ProxyGType = FuSteelseriesFizzGen2 Name = Arctis Nova 5 USB Receiver bootloader Icon = usb-receiver FirmwareSize = 0x14B000 CounterpartGuid = USB\VID_1038&PID_2232 Flags = is-bootloader,is-receiver,~usable-during-update InstallDuration = 165 fwupd-2.0.10/plugins/steelseries/tests/000077500000000000000000000000001501337203100200645ustar00rootroot00000000000000fwupd-2.0.10/plugins/steelseries/tests/steelseries-aerox-3-wireless.json000066400000000000000000000034421501337203100264200ustar00rootroot00000000000000{ "name": "Steelseries Aerox 3 Wireless", "interactive": false, "steps": [ { "url": "41e25df1ee09f3c05a4799e4fc325115bd5ba9490309c76c545f3e3a88c1c9b9-aerox-3-wireless-dongle.cab", "components": [ { "version": "1.4.2", "guids": [ "291b0582-8d08-5714-88db-986911c744d9", "df0cb7f0-be88-532f-87f9-65662da376b9" ] } ] }, { "url": "de803999ea8f8c71268bb8ed771c230907b78e9ca8435212f977bfa859cc5f2b-aerox-3-wireless-mouse.cab", "components": [ { "version": "1.10.9", "guids": [ "541b2713-d367-5240-a443-bed07f09ffbf", "4f3fac7c-981b-58cc-bb58-8a3b90f31c9c", "ac7d7cbb-7ef5-5466-ab3d-e70ced97ee61" ] } ] }, { "url": "596d1032e7c916822ffae84a48d5f84d641808be761723536c0d82bb83ac077f-aerox-3-wireless-dongle.cab", "emulation-url": "ce35aed86ad2bab08931a07c29524a2f173f10ab7f1c244083a607d64331eefa-aerox-3-wireless-dongle_1.11.4_USB-C.emulation.zip", "components": [ { "version": "1.11.4", "guids": [ "291b0582-8d08-5714-88db-986911c744d9", "df0cb7f0-be88-532f-87f9-65662da376b9" ] } ] }, { "url": "f1453976fe7ea27522524872327ad5aab6fba0c962df719ac05253439d7d72a9-aerox-3-wireless-mouse.cab", "emulation-url": "01958299662917005d9b499074d9624207e8762d7ed1d826b72e962bf4d36895-aerox-3-wireless-mouse_1.11.4_USB-C.emulation.zip", "components": [ { "version": "1.11.4", "guids": [ "541b2713-d367-5240-a443-bed07f09ffbf", "4f3fac7c-981b-58cc-bb58-8a3b90f31c9c", "ac7d7cbb-7ef5-5466-ab3d-e70ced97ee61" ] } ] } ] } fwupd-2.0.10/plugins/steelseries/tests/steelseries-nova5.json000066400000000000000000000016471501337203100243440ustar00rootroot00000000000000{ "name": "SteelSeries Artix Nova5 Headset", "interactive": false, "steps": [ { "url": "fa73c8393027e7231776c917486b2e3f417e412e3b1c0cea0be90a57df600e08-SteelSeries_Nova_5_Headset_1.6.0.cab", "emulation-url": "272e4725290ff7d64419cfbe57e97021695c9014fb03e9bd6087db99a7e09707-SteelSeries_Nova_5_Headset_1.6.0.zip", "components": [ { "version": "1.6.0", "guids": [ "3722381d-29fb-572f-895b-8584d94a7a77" ] } ] }, { "url": "6317a70eef744dabe4d70931962da42fd419bdebe5436dcf939a889782fe33e1-SteelSeries_Nova_5_Headset_1.7.0.cab", "emulation-url": "46850811d0c25e739d8016da2a5a83fd64b57c6303c9cbbca25312a603ba1321-SteelSeries_Nova_5_Headset_1.7.0.zip", "components": [ { "version": "1.7.0", "guids": [ "3722381d-29fb-572f-895b-8584d94a7a77" ] } ] } ] } fwupd-2.0.10/plugins/steelseries/tests/steelseries-rival-3-wireless.json000066400000000000000000000010041501337203100264070ustar00rootroot00000000000000{ "name": "Steelseries Rival 3 Wireless", "interactive": true, "steps": [ { "url": "0e227c83714c7c37ac8053164d420d1ca2b85d68b65ba60d51aa0a6b03ee4087-SteelSeries_Rival_3_Wireless_1.4.cab", "emulation-url": "5a2f36255139825c58aca8f09e183e6239f227a81626596518c139b6551112b1-SteelSeries_Rival_3_Wireless_1.4.emulation.zip", "components": [ { "version": "1.4", "guids": [ "73e6daa7-d5a1-567d-a0ff-01514f8498b1" ] } ] } ] } fwupd-2.0.10/plugins/steelseries/tests/steelseries-stratus-duo-rx.json000066400000000000000000000010611501337203100262230ustar00rootroot00000000000000{ "name": "Steelseries Stratus Duo RX", "interactive": true, "steps": [ { "url": "10521cfeed9086a597892469fbff79ab5d9b83d3b062f606a8ab6bf34664f6dd-steelseries-stratusduorx-1.75.cab", "emulation-url": "bec561d97746ae0251fca3e26355423cd2771e4c3addecab04daee851d3d877d-steelseries-stratusduorx-1.75.emulation.zip", "components": [ { "version": "1.75", "guids": [ "4806f086-94a5-5347-96b6-3379532dfe17", "2a962d9a-89e2-5b8f-8cfb-cffca4876818" ] } ] } ] } fwupd-2.0.10/plugins/steelseries/tests/steelseries-stratus-duo.json000066400000000000000000000010621501337203100255750ustar00rootroot00000000000000{ "name": "Steelseries Stratus Duo Gamepad", "interactive": true, "steps": [ { "url": "ecc54ffe68eebb0e5d77604683df698e7c15911421eee960d2876c175a2aa164-steelseries-stratusduo-1.75.cab", "emulation-url": "4da8d30dfe2343273162121a99b5ce31039e0f9e0590e15582821d32f64cf7c5-steelseries-stratusduo-1.75.emulation.zip", "components": [ { "version": "1.75", "guids": [ "3c432d75-7e6f-5a7c-8fc1-4d1490fc4cde", "5bc1707d-5681-54f9-b4d7-8c4c53279db6" ] } ] } ] } fwupd-2.0.10/plugins/synaptics-cape/000077500000000000000000000000001501337203100173165ustar00rootroot00000000000000fwupd-2.0.10/plugins/synaptics-cape/README.md000066400000000000000000000025111501337203100205740ustar00rootroot00000000000000--- title: Plugin: Synaptics CAPE --- ## Introduction This plugin is used to update Synaptics CAPE based audio devices. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob. This plugin supports the following protocol ID: * `com.synaptics.cape` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_1395&PID_0293` These devices also use custom GUID values, e.g. * `SYNAPTICS_CAPE\CX31993` (only-quirk) ## Update Behavior The firmware is deployed when the device is in normal runtime mode, and the device will reset when the new firmware has been written. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x1395` ## Quirk Use This plugin uses the following plugin-specific quirks: ### `Flags=use-in-report-interrupt` Some devices will support IN_REPORT that allow host communicate with device over interrupt instead of control endpoint. Since: 1.7.0 ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.7.0`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Simon Ho: @CNXT-Simon fwupd-2.0.10/plugins/synaptics-cape/fu-synaptics-cape-device.c000066400000000000000000000621561501337203100242640ustar00rootroot00000000000000/* * Copyright 2021 Synaptics Incorporated * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-synaptics-cape-device.h" #include "fu-synaptics-cape-hid-firmware.h" #include "fu-synaptics-cape-struct.h" /* defines timings */ #define FU_SYNAPTICS_CAPE_DEVICE_USB_CMD_WRITE_TIMEOUT 20 /* ms */ #define FU_SYNAPTICS_CAPE_DEVICE_USB_CMD_READ_TIMEOUT 30 /* ms */ #define FU_SYNAPTICS_CAPE_DEVICE_USB_CMD_RETRY_INTERVAL 10 /* ms */ #define FU_SYNAPTICS_CAPE_DEVICE_USB_CMD_RETRY_TIMEOUT 300 /* ms */ #define FU_SYNAPTICS_CAPE_DEVICE_USB_CMD_INTR_TIMEOUT 5000 /* ms */ #define FU_SYNAPTICS_CAPE_DEVICE_USB_RESET_DELAY_MS 5000 /* ms */ #define FU_SYNAPTICS_CAPE_DEVICE_HDR_WRITE_DELAY_MS 150 /* ms */ /* defines CAPE command constant values and macro */ #define FU_SYNAPTICS_CAPE_CMD_WRITE_DATAL_LEN 8 /* number of guint32 */ #define FU_SYNAPTICS_CAPE_CMD_GET_FLAG 0x100 /* GET flag */ #define FU_SYNAPTICS_CAPE_CMD_IS_REPLY 0x8000 #define FU_SYNAPTICS_CAPE_FM3_HID_INTR_IN_EP 0x83 /* CAPE fwupd device structure */ struct _FuSynapticsCapeDevice { FuHidDevice parent_instance; FuSynapticsCapeFirmwarePartition active_partition; }; #define FU_SYNAPTICS_CAPE_DEVICE_FLAG_USE_IN_REPORT_INTERRUPT "use-in-report-interrupt" G_DEFINE_TYPE(FuSynapticsCapeDevice, fu_synaptics_cape_device, FU_TYPE_HID_DEVICE) /* sends SET_REPORT to device */ static gboolean fu_synaptics_cape_device_set_report(FuSynapticsCapeDevice *self, const FuSynapticsCapeCmdHidReport *data, GError **error) { g_return_val_if_fail(FU_IS_SYNAPTICS_CAPE_DEVICE(self), FALSE); g_return_val_if_fail(data != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); fu_dump_raw(G_LOG_DOMAIN, "SetReport", (guint8 *)data, sizeof(*data)); return fu_hid_device_set_report(FU_HID_DEVICE(self), FU_SYNAPTICS_CAPE_CMD_HID_REPORT_DEFAULT_REPORT_ID, (guint8 *)data, sizeof(*data), FU_SYNAPTICS_CAPE_DEVICE_USB_CMD_WRITE_TIMEOUT, FU_HID_DEVICE_FLAG_NONE, error); } /* gets HID report over control ep */ static gboolean fu_synaptics_cape_device_get_report(FuSynapticsCapeDevice *self, FuSynapticsCapeCmdHidReport *st_report, GError **error) { return fu_hid_device_get_report(FU_HID_DEVICE(self), FU_SYNAPTICS_CAPE_CMD_HID_REPORT_DEFAULT_REPORT_ID, st_report->data, st_report->len, FU_SYNAPTICS_CAPE_DEVICE_USB_CMD_READ_TIMEOUT, FU_HID_DEVICE_FLAG_NONE, error); } /* gets HID report over interrupt ep */ static gboolean fu_synaptics_cape_device_get_report_intr(FuSynapticsCapeDevice *self, FuSynapticsCapeCmdHidReport *data, GError **error) { gsize actual_len = 0; g_return_val_if_fail(FU_IS_SYNAPTICS_CAPE_DEVICE(self), FALSE); g_return_val_if_fail(data != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(self), FU_SYNAPTICS_CAPE_FM3_HID_INTR_IN_EP, (guint8 *)data, sizeof(*data), &actual_len, FU_SYNAPTICS_CAPE_DEVICE_USB_CMD_INTR_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to get report over interrupt ep: "); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "GetReport", (guint8 *)data, sizeof(*data)); /* success */ return TRUE; } /* dump CAPE command error if any */ static gboolean fu_synaptics_cape_device_rc_set_error(const FuSynapticsCapeMsg *rsp, GError **error) { gint16 value; g_return_val_if_fail(rsp != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); value = (gint16)fu_synaptics_cape_msg_get_data_len(rsp); if (value >= 0) return TRUE; switch (value) { case FU_SYNAPTICS_CAPE_ERROR_GENERIC_FAILURE: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "generic failure"); break; case FU_SYNAPTICS_CAPE_ERROR_ALREADY_EXISTS: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "already exists"); break; case FU_SYNAPTICS_CAPE_ERROR_NULL_APP_POINTER: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "null app pointer"); break; case FU_SYNAPTICS_CAPE_ERROR_NULL_MODULE_POINTER: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "null module pointer"); break; case FU_SYNAPTICS_CAPE_ERROR_NULL_POINTER: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "null pointer"); break; case FU_SYNAPTICS_CAPE_ERROR_BAD_APP_ID: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "bad app id"); break; case FU_SYNAPTICS_CAPE_ERROR_MODULE_TYPE_HAS_NO_API: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "has no api"); break; case FU_SYNAPTICS_CAPE_ERROR_BAD_MAGIC_NUMBER: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "bad magic number"); break; case FU_SYNAPTICS_CAPE_ERROR_CMD_MODE_UNSUPPORTED: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "mode unsupported"); break; case FU_SYNAPTICS_CAPE_ERROR_EAGAIN: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "query timeout"); break; case FU_SYNAPTICS_CAPE_ERROR_SFU_FAIL: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "command failed"); break; case FU_SYNAPTICS_CAPE_ERROR_SFU_WRITE_FAIL: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "writing to flash failed"); break; case FU_SYNAPTICS_CAPE_ERROR_SFU_READ_FAIL: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_READ, "reading from flash failed"); break; case FU_SYNAPTICS_CAPE_ERROR_SFU_CRC_ERROR: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "firmware data has been corrupted"); break; case FU_SYNAPTICS_CAPE_ERROR_SFU_USB_ID_NOT_MATCH: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "vendor and device IDs do not match"); break; case FU_SYNAPTICS_CAPE_ERROR_SFU_VERSION_DOWNGRADE: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_VERSION_NEWER, "update is older than current version"); break; case FU_SYNAPTICS_CAPE_ERROR_SFU_HEADER_CORRUPTION: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "firmware header data has been corrupted"); break; case FU_SYNAPTICS_CAPE_ERROR_SFU_IMAGE_CORRUPTION: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "firmware payload data has been corrupted"); break; case FU_SYNAPTICS_CAPE_ERROR_SFU_ALREADY_ACTIVE: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to active new firmward"); break; case FU_SYNAPTICS_CAPE_ERROR_SFU_NOT_READY: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "firmware update is not ready"); break; case FU_SYNAPTICS_CAPE_ERROR_SFU_SIGN_TRANSFER_CORRUPTION: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "signature data has been corrupted"); break; case FU_SYNAPTICS_CAPE_ERROR_SFU_DIGITAL_SIGNATURE_VERIFICATION_FAILED: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_AUTH_FAILED, "digital signature is invalid"); break; case FU_SYNAPTICS_CAPE_ERROR_SFU_TASK_NOT_RUNNING: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "firmware update task is not running"); break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unknown error %d", value); } /* success */ return FALSE; } /* sends a FuSynapticsCapeMsg structure command to device to get the response in the same structure */ static FuSynapticsCapeMsg * fu_synaptics_cape_device_sendcmd_ex(FuSynapticsCapeDevice *self, FuSynapticsCapeMsg *st_msg_req, guint delay_ms, GError **error) { guint elapsed_ms = 0; g_autoptr(FuSynapticsCapeCmdHidReport) st_report = fu_synaptics_cape_cmd_hid_report_new(); g_autoptr(FuSynapticsCapeMsg) st_msg_res = NULL; g_autoptr(GError) error_local = NULL; g_return_val_if_fail(st_msg_req != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* sets data length to MAX for any GET commands */ if (fu_synaptics_cape_msg_get_cmd_id(st_msg_req) & FU_SYNAPTICS_CAPE_CMD_GET_FLAG) { fu_synaptics_cape_msg_set_data_len(st_msg_req, FU_SYNAPTICS_CAPE_MSG_N_ELEMENTS_DATA); } /* first two bytes are report id */ if (!fu_synaptics_cape_cmd_hid_report_set_msg(st_report, st_msg_req, error)) return NULL; if (!fu_synaptics_cape_device_set_report(self, st_report, error)) { g_prefix_error(error, "failed to send: "); return NULL; } fu_device_sleep(FU_DEVICE(self), delay_ms); /* waits for the command to complete. There are two approaches to get status from device: * 1. gets IN_REPORT over interrupt endpoint. device won't reply until a command operation * has completed. This works only on devices support interrupt endpoint. * 2. polls GET_REPORT over control endpoint. device will return 'reply==0' before a * command operation has completed. */ if (fu_device_has_private_flag(FU_DEVICE(self), FU_SYNAPTICS_CAPE_DEVICE_FLAG_USE_IN_REPORT_INTERRUPT)) { if (!fu_synaptics_cape_device_get_report_intr(self, st_report, &error_local)) { /* ignoring io error for software reset command */ if ((fu_synaptics_cape_msg_get_cmd_id(st_msg_req) == FU_SYNAPTICS_CAPE_CMD_MCU_SOFT_RESET) && (fu_synaptics_cape_msg_get_module_id(st_msg_req) == FU_SYNAPTICS_CAPE_MODULE_ID_APP_CTRL) && (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL))) { g_debug("ignoring: %s", error_local->message); return g_byte_array_ref(st_msg_req); } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to get IN_REPORT: "); return NULL; } } else { for (; elapsed_ms < FU_SYNAPTICS_CAPE_DEVICE_USB_CMD_RETRY_TIMEOUT; elapsed_ms += FU_SYNAPTICS_CAPE_DEVICE_USB_CMD_RETRY_INTERVAL) { g_autoptr(FuSynapticsCapeMsg) st_msg2 = NULL; if (!fu_synaptics_cape_device_get_report(self, st_report, &error_local)) { /* ignoring io error for software reset command */ if ((fu_synaptics_cape_msg_get_cmd_id(st_msg_req) == FU_SYNAPTICS_CAPE_CMD_MCU_SOFT_RESET) && (fu_synaptics_cape_msg_get_module_id(st_msg_req) == FU_SYNAPTICS_CAPE_MODULE_ID_APP_CTRL) && (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL))) { g_debug("ignoring: %s", error_local->message); g_byte_array_ref(st_msg_req); } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to get GET_REPORT: "); return NULL; } st_msg2 = fu_synaptics_cape_cmd_hid_report_get_msg(st_report); if (fu_synaptics_cape_msg_get_cmd_id(st_msg2) & FU_SYNAPTICS_CAPE_CMD_IS_REPLY) break; fu_device_sleep(FU_DEVICE(self), FU_SYNAPTICS_CAPE_DEVICE_USB_CMD_RETRY_INTERVAL); } } st_msg_res = fu_synaptics_cape_cmd_hid_report_get_msg(st_report); if ((fu_synaptics_cape_msg_get_cmd_id(st_msg_res) & FU_SYNAPTICS_CAPE_CMD_IS_REPLY) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "firmware don't respond to command"); return NULL; } if (!fu_synaptics_cape_device_rc_set_error(st_msg_res, error)) return NULL; /* success */ return g_steal_pointer(&st_msg_res); } /* a simple version of sendcmd_ex without returned data */ static gboolean fu_synaptics_cape_device_sendcmd(FuSynapticsCapeDevice *self, FuSynapticsCapeModuleId module_id, FuSynapticsCapeCmd cmd_id, const guint32 *data, gsize data_len, guint delay_ms, GError **error) { g_autoptr(FuSynapticsCapeMsg) st_msg_req = fu_synaptics_cape_msg_new(); g_autoptr(FuSynapticsCapeMsg) st_msg_res = NULL; fu_synaptics_cape_msg_set_data_len(st_msg_req, data_len); fu_synaptics_cape_msg_set_cmd_id(st_msg_req, cmd_id); fu_synaptics_cape_msg_set_module_id(st_msg_req, module_id); for (guint i = 0; i < data_len; i++) fu_synaptics_cape_msg_set_data(st_msg_req, i, data[i]); st_msg_res = fu_synaptics_cape_device_sendcmd_ex(self, st_msg_req, delay_ms, error); return st_msg_res != NULL; } static void fu_synaptics_cape_device_to_string(FuDevice *device, guint idt, GString *str) { FuSynapticsCapeDevice *self = FU_SYNAPTICS_CAPE_DEVICE(device); g_return_if_fail(FU_IS_SYNAPTICS_CAPE_DEVICE(self)); fwupd_codec_string_append_int(str, idt, "ActivePartition", self->active_partition); } /* resets device */ static gboolean fu_synaptics_cape_device_reset(FuSynapticsCapeDevice *self, GError **error) { g_autoptr(GTimer) timer = g_timer_new(); g_return_val_if_fail(FU_IS_SYNAPTICS_CAPE_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!fu_synaptics_cape_device_sendcmd(self, FU_SYNAPTICS_CAPE_MODULE_ID_APP_CTRL, FU_SYNAPTICS_CAPE_CMD_MCU_SOFT_RESET, NULL, 0, 0, error)) { g_prefix_error(error, "reset command is not supported: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), FU_SYNAPTICS_CAPE_DEVICE_USB_RESET_DELAY_MS); g_debug("reset took %.2lfms", g_timer_elapsed(timer, NULL) * 1000); /* success */ return TRUE; } /** * fu_synaptics_cape_device_get_active_partition: * @self: a #FuSynapticsCapeDevice * @error: return location for an error * * updates active partition information to FuSynapticsCapeDevice::active_partition * * Returns: returns TRUE if operation is successful, otherwise, return FALSE if * unsuccessful. * **/ static gboolean fu_synaptics_cape_device_setup_active_partition(FuSynapticsCapeDevice *self, GError **error) { g_autoptr(FuSynapticsCapeMsg) st_msg_req = fu_synaptics_cape_msg_new(); g_autoptr(FuSynapticsCapeMsg) st_msg_res = NULL; g_return_val_if_fail(FU_IS_SYNAPTICS_CAPE_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); fu_synaptics_cape_msg_set_cmd_id(st_msg_req, FU_SYNAPTICS_CAPE_CMD_FW_GET_ACTIVE_PARTITION); st_msg_res = fu_synaptics_cape_device_sendcmd_ex(self, st_msg_req, 0, error); if (st_msg_res == NULL) return FALSE; self->active_partition = fu_synaptics_cape_msg_get_data(st_msg_res, 0); if (self->active_partition != FU_SYNAPTICS_CAPE_FIRMWARE_PARTITION_1 && self->active_partition != FU_SYNAPTICS_CAPE_FIRMWARE_PARTITION_2) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "partition number out of range, returned partition number is %u", self->active_partition); return FALSE; } /* success */ return TRUE; } /* gets version number from device and saves to FU_DEVICE */ static gboolean fu_synaptics_cape_device_setup_version(FuSynapticsCapeDevice *self, GError **error) { guint32 version_raw; g_autoptr(FuSynapticsCapeMsg) st_msg_req = fu_synaptics_cape_msg_new(); g_autoptr(FuSynapticsCapeMsg) st_msg_res = NULL; /* gets version number from device */ fu_synaptics_cape_msg_set_cmd_id(st_msg_req, FU_SYNAPTICS_CAPE_CMD_GET_VERSION); fu_synaptics_cape_msg_set_data_len(st_msg_req, 4); st_msg_res = fu_synaptics_cape_device_sendcmd_ex(self, st_msg_req, 0, error); if (st_msg_res == NULL) return FALSE; /* the version number are stored in lowest byte of a sequence of returned data */ version_raw = ((fu_synaptics_cape_msg_get_data(st_msg_res, 0) << 24) | ((fu_synaptics_cape_msg_get_data(st_msg_res, 1) & 0xFF) << 16) | ((fu_synaptics_cape_msg_get_data(st_msg_res, 2) & 0xFF) << 8) | (fu_synaptics_cape_msg_get_data(st_msg_res, 3) & 0xFF)); fu_device_set_version_raw(FU_DEVICE(self), version_raw); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); /* success */ return TRUE; } static gboolean fu_synaptics_cape_device_setup(FuDevice *device, GError **error) { FuSynapticsCapeDevice *self = FU_SYNAPTICS_CAPE_DEVICE(device); g_return_val_if_fail(FU_IS_SYNAPTICS_CAPE_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_synaptics_cape_device_parent_class)->setup(device, error)) return FALSE; if (!fu_synaptics_cape_device_setup_version(self, error)) { g_prefix_error(error, "failed to get firmware version info: "); return FALSE; } if (!fu_synaptics_cape_device_setup_active_partition(self, error)) { g_prefix_error(error, "failed to get active partition info: "); return FALSE; } /* success */ return TRUE; } static FuFirmware * fu_synaptics_cape_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuSynapticsCapeDevice *self = FU_SYNAPTICS_CAPE_DEVICE(device); gsize bufsz = 0; gsize offset = 0; g_autoptr(FuFirmware) firmware = fu_synaptics_cape_hid_firmware_new(); g_autoptr(GInputStream) stream_fw = NULL; /* a "fw" includes two firmware data for each partition, we need to divide a 'fw' into * two equal parts */ if (!fu_input_stream_size(stream, &bufsz, error)) return NULL; if ((guint32)bufsz % 4 != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "data not aligned to 32 bits"); return NULL; } /* uses second partition if active partition is 1 */ if (self->active_partition == FU_SYNAPTICS_CAPE_FIRMWARE_PARTITION_1) offset = bufsz / 2; stream_fw = fu_partial_input_stream_new(stream, offset, bufsz / 2, error); if (stream_fw == NULL) return NULL; if (!fu_firmware_parse_stream(firmware, stream_fw, 0x0, flags, error)) return NULL; /* verify if correct device */ if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_VID_PID) == 0) { const guint16 vid = fu_synaptics_cape_firmware_get_vid(FU_SYNAPTICS_CAPE_FIRMWARE(firmware)); const guint16 pid = fu_synaptics_cape_firmware_get_pid(FU_SYNAPTICS_CAPE_FIRMWARE(firmware)); if (vid != 0x0 && pid != 0x0 && (fu_device_get_vid(device) != vid || fu_device_get_pid(device) != pid)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "USB vendor or product incorrect, " "got: %04X:%04X expected %04X:%04X", vid, pid, fu_device_get_vid(device), fu_device_get_pid(device)); return NULL; } } /* success */ return g_steal_pointer(&firmware); } /* sends firmware header to device */ static gboolean fu_synaptics_cape_device_write_firmware_header(FuSynapticsCapeDevice *self, GBytes *fw, GError **error) { const guint8 *buf = NULL; gsize bufsz = 0; g_autofree guint32 *buf32 = NULL; g_return_val_if_fail(FU_IS_SYNAPTICS_CAPE_DEVICE(self), FALSE); g_return_val_if_fail(fw != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); buf = g_bytes_get_data(fw, &bufsz); /* checks size */ if (bufsz != 20) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware header is not 20 bytes"); return FALSE; } /* 32 bit align */ buf32 = g_new0(guint32, bufsz / sizeof(guint32)); if (!fu_memcpy_safe((guint8 *)buf32, bufsz, 0x0, /* dst */ buf, bufsz, 0x0, /* src */ bufsz, error)) return FALSE; return fu_synaptics_cape_device_sendcmd(self, FU_SYNAPTICS_CAPE_MODULE_ID_APP_CTRL, FU_SYNAPTICS_CAPE_CMD_FW_UPDATE_START, buf32, bufsz / sizeof(guint32), FU_SYNAPTICS_CAPE_DEVICE_HDR_WRITE_DELAY_MS, error); } /* sends firmware image to device */ static gboolean fu_synaptics_cape_device_write_firmware_image(FuSynapticsCapeDevice *self, GInputStream *stream, FuProgress *progress, GError **error) { g_autoptr(FuChunkArray) chunks = NULL; chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, sizeof(guint32) * FU_SYNAPTICS_CAPE_CMD_WRITE_DATAL_LEN, error); if (chunks == NULL) return FALSE; fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { gsize bufsz; g_autofree guint32 *buf32 = NULL; g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; /* 32 bit align */ bufsz = fu_chunk_get_data_sz(chk); buf32 = g_new0(guint32, bufsz / sizeof(guint32)); if (!fu_memcpy_safe((guint8 *)buf32, bufsz, 0x0, /* dst */ fu_chunk_get_data(chk), bufsz, 0x0, /* src */ bufsz, error)) return FALSE; if (!fu_synaptics_cape_device_sendcmd(self, FU_SYNAPTICS_CAPE_MODULE_ID_APP_CTRL, FU_SYNAPTICS_CAPE_CMD_FW_UPDATE_WRITE, buf32, bufsz / sizeof(guint32), 0, error)) { g_prefix_error(error, "failed send on chk %u: ", i); return FALSE; } fu_progress_step_done(progress); } /* success */ return TRUE; } /* performs firmware update */ static gboolean fu_synaptics_cape_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuSynapticsCapeDevice *self = FU_SYNAPTICS_CAPE_DEVICE(device); g_autoptr(GInputStream) stream = NULL; g_autoptr(GBytes) fw_header = NULL; g_return_val_if_fail(FU_IS_SYNAPTICS_CAPE_DEVICE(self), FALSE); g_return_val_if_fail(firmware != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 2, "device-write-hdr"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 69, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 29, NULL); fw_header = fu_firmware_get_image_by_id_bytes(firmware, FU_FIRMWARE_ID_HEADER, error); if (fw_header == NULL) return FALSE; if (!fu_synaptics_cape_device_write_firmware_header(self, fw_header, error)) { g_prefix_error(error, "update header failed: "); return FALSE; } fu_progress_step_done(progress); /* performs the actual write */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; if (!fu_synaptics_cape_device_write_firmware_image(self, stream, fu_progress_get_child(progress), error)) { g_prefix_error(error, "update image failed: "); return FALSE; } fu_progress_step_done(progress); /* verify the firmware image */ if (!fu_synaptics_cape_device_sendcmd(self, FU_SYNAPTICS_CAPE_MODULE_ID_APP_CTRL, FU_SYNAPTICS_CAPE_CMD_FW_UPDATE_END, NULL, 0, 0, error)) { g_prefix_error(error, "failed to verify firmware: "); return FALSE; } fu_progress_step_done(progress); /* sends software reset to boot into the newly flashed firmware */ if (!fu_synaptics_cape_device_reset(self, error)) return FALSE; fu_progress_step_done(progress); /* success */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static void fu_synaptics_cape_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static gchar * fu_synaptics_cape_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_device_get_version_format(device)); } static void fu_synaptics_cape_device_init(FuSynapticsCapeDevice *self) { fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_AUDIO_CARD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); fu_device_set_install_duration(FU_DEVICE(self), 3); /* seconds */ fu_device_add_protocol(FU_DEVICE(self), "com.synaptics.cape"); fu_device_retry_set_delay(FU_DEVICE(self), 100); /* ms */ fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_register_private_flag(FU_DEVICE(self), FU_SYNAPTICS_CAPE_DEVICE_FLAG_USE_IN_REPORT_INTERRUPT); } static void fu_synaptics_cape_device_class_init(FuSynapticsCapeDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_synaptics_cape_device_to_string; device_class->setup = fu_synaptics_cape_device_setup; device_class->write_firmware = fu_synaptics_cape_device_write_firmware; device_class->prepare_firmware = fu_synaptics_cape_device_prepare_firmware; device_class->set_progress = fu_synaptics_cape_device_set_progress; device_class->convert_version = fu_synaptics_cape_device_convert_version; } fwupd-2.0.10/plugins/synaptics-cape/fu-synaptics-cape-device.h000066400000000000000000000005711501337203100242620ustar00rootroot00000000000000/* * Copyright 2021 Synaptics Incorporated * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_SYNAPTICS_CAPE_DEVICE (fu_synaptics_cape_device_get_type()) G_DECLARE_FINAL_TYPE(FuSynapticsCapeDevice, fu_synaptics_cape_device, FU, SYNAPTICS_CAPE_DEVICE, FuHidDevice) fwupd-2.0.10/plugins/synaptics-cape/fu-synaptics-cape-firmware.c000066400000000000000000000052001501337203100246240ustar00rootroot00000000000000/* * Copyright 2021 Synaptics Incorporated * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-synaptics-cape-firmware.h" #include "fu-synaptics-cape-struct.h" typedef struct { guint16 vid; guint16 pid; } FuSynapticsCapeFirmwarePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuSynapticsCapeFirmware, fu_synaptics_cape_firmware, FU_TYPE_FIRMWARE) #define GET_PRIVATE(o) (fu_synaptics_cape_firmware_get_instance_private(o)) guint16 fu_synaptics_cape_firmware_get_vid(FuSynapticsCapeFirmware *self) { FuSynapticsCapeFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_SYNAPTICS_CAPE_FIRMWARE(self), 0); return priv->vid; } void fu_synaptics_cape_firmware_set_vid(FuSynapticsCapeFirmware *self, guint16 vid) { FuSynapticsCapeFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_SYNAPTICS_CAPE_FIRMWARE(self)); priv->vid = vid; } guint16 fu_synaptics_cape_firmware_get_pid(FuSynapticsCapeFirmware *self) { FuSynapticsCapeFirmwarePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_SYNAPTICS_CAPE_FIRMWARE(self), 0); return priv->pid; } void fu_synaptics_cape_firmware_set_pid(FuSynapticsCapeFirmware *self, guint16 pid) { FuSynapticsCapeFirmwarePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_SYNAPTICS_CAPE_FIRMWARE(self)); priv->pid = pid; } static void fu_synaptics_cape_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuSynapticsCapeFirmware *self = FU_SYNAPTICS_CAPE_FIRMWARE(firmware); FuSynapticsCapeFirmwarePrivate *priv = GET_PRIVATE(self); fu_xmlb_builder_insert_kx(bn, "vid", priv->vid); fu_xmlb_builder_insert_kx(bn, "pid", priv->pid); } static gboolean fu_synaptics_cape_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { FuSynapticsCapeFirmware *self = FU_SYNAPTICS_CAPE_FIRMWARE(firmware); FuSynapticsCapeFirmwarePrivate *priv = GET_PRIVATE(self); guint64 tmp; /* optional properties */ tmp = xb_node_query_text_as_uint(n, "vid", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT16) priv->vid = tmp; tmp = xb_node_query_text_as_uint(n, "pid", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT16) priv->pid = tmp; /* success */ return TRUE; } static void fu_synaptics_cape_firmware_init(FuSynapticsCapeFirmware *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_VID_PID); } static void fu_synaptics_cape_firmware_class_init(FuSynapticsCapeFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->export = fu_synaptics_cape_firmware_export; firmware_class->build = fu_synaptics_cape_firmware_build; } fwupd-2.0.10/plugins/synaptics-cape/fu-synaptics-cape-firmware.h000066400000000000000000000014061501337203100246350ustar00rootroot00000000000000/* * Copyright 2021 Synaptics Incorporated * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_SYNAPTICS_CAPE_FIRMWARE (fu_synaptics_cape_firmware_get_type()) G_DECLARE_DERIVABLE_TYPE(FuSynapticsCapeFirmware, fu_synaptics_cape_firmware, FU, SYNAPTICS_CAPE_FIRMWARE, FuFirmware) struct _FuSynapticsCapeFirmwareClass { FuFirmwareClass parent_class; }; guint16 fu_synaptics_cape_firmware_get_vid(FuSynapticsCapeFirmware *self); void fu_synaptics_cape_firmware_set_vid(FuSynapticsCapeFirmware *self, guint16 vid); guint16 fu_synaptics_cape_firmware_get_pid(FuSynapticsCapeFirmware *self); void fu_synaptics_cape_firmware_set_pid(FuSynapticsCapeFirmware *self, guint16 pid); fwupd-2.0.10/plugins/synaptics-cape/fu-synaptics-cape-hid-firmware.c000066400000000000000000000102421501337203100253700ustar00rootroot00000000000000/* * Copyright 2021 Synaptics Incorporated * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-synaptics-cape-hid-firmware.h" #include "fu-synaptics-cape-struct.h" struct _FuSynapticsCapeHidFirmware { FuSynapticsCapeFirmware parent_instance; }; G_DEFINE_TYPE(FuSynapticsCapeHidFirmware, fu_synaptics_cape_hid_firmware, FU_TYPE_SYNAPTICS_CAPE_FIRMWARE) static gboolean fu_synaptics_cape_hid_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuSynapticsCapeHidFirmware *self = FU_SYNAPTICS_CAPE_HID_FIRMWARE(firmware); gsize streamsz = 0; g_autofree gchar *version_str = NULL; g_autoptr(FuFirmware) img_hdr = fu_firmware_new(); g_autoptr(GByteArray) st = NULL; g_autoptr(GInputStream) stream_hdr = NULL; g_autoptr(GInputStream) stream_body = NULL; /* sanity check */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if ((guint32)streamsz % 4 != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "data not aligned to 32 bits"); return FALSE; } /* unpack */ st = fu_struct_synaptics_cape_hid_hdr_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; fu_synaptics_cape_firmware_set_vid(FU_SYNAPTICS_CAPE_FIRMWARE(self), fu_struct_synaptics_cape_hid_hdr_get_vid(st)); fu_synaptics_cape_firmware_set_pid(FU_SYNAPTICS_CAPE_FIRMWARE(self), fu_struct_synaptics_cape_hid_hdr_get_pid(st)); version_str = g_strdup_printf("%u.%u.%u.%u", fu_struct_synaptics_cape_hid_hdr_get_ver_z(st), fu_struct_synaptics_cape_hid_hdr_get_ver_y(st), fu_struct_synaptics_cape_hid_hdr_get_ver_x(st), fu_struct_synaptics_cape_hid_hdr_get_ver_w(st)); fu_firmware_set_version(FU_FIRMWARE(self), version_str); /* top-most part of header */ stream_hdr = fu_partial_input_stream_new(stream, 0, FU_STRUCT_SYNAPTICS_CAPE_HID_HDR_OFFSET_VER_W, error); if (stream_hdr == NULL) return FALSE; if (!fu_firmware_parse_stream(img_hdr, stream_hdr, 0x0, flags, error)) return FALSE; fu_firmware_set_id(img_hdr, FU_FIRMWARE_ID_HEADER); fu_firmware_add_image(firmware, img_hdr); /* body */ stream_body = fu_partial_input_stream_new(stream, st->len, streamsz - st->len, error); if (stream_body == NULL) return FALSE; if (!fu_firmware_set_stream(firmware, stream_body, error)) return FALSE; fu_firmware_set_id(firmware, FU_FIRMWARE_ID_PAYLOAD); return TRUE; } static GByteArray * fu_synaptics_cape_hid_firmware_write(FuFirmware *firmware, GError **error) { FuSynapticsCapeHidFirmware *self = FU_SYNAPTICS_CAPE_HID_FIRMWARE(firmware); guint64 ver = fu_firmware_get_version_raw(firmware); g_autoptr(GByteArray) buf = fu_struct_synaptics_cape_hid_hdr_new(); g_autoptr(GBytes) payload = NULL; /* pack */ fu_struct_synaptics_cape_hid_hdr_set_vid( buf, fu_synaptics_cape_firmware_get_vid(FU_SYNAPTICS_CAPE_FIRMWARE(self))); fu_struct_synaptics_cape_hid_hdr_set_pid( buf, fu_synaptics_cape_firmware_get_pid(FU_SYNAPTICS_CAPE_FIRMWARE(self))); fu_struct_synaptics_cape_hid_hdr_set_crc(buf, 0xFFFF); fu_struct_synaptics_cape_hid_hdr_set_ver_w(buf, ver >> 0); fu_struct_synaptics_cape_hid_hdr_set_ver_x(buf, ver >> 16); fu_struct_synaptics_cape_hid_hdr_set_ver_y(buf, ver >> 32); fu_struct_synaptics_cape_hid_hdr_set_ver_z(buf, ver >> 48); /* payload */ payload = fu_firmware_get_bytes_with_patches(firmware, error); if (payload == NULL) return NULL; fu_byte_array_append_bytes(buf, payload); fu_byte_array_align_up(buf, FU_FIRMWARE_ALIGNMENT_4, 0xFF); return g_steal_pointer(&buf); } static void fu_synaptics_cape_hid_firmware_init(FuSynapticsCapeHidFirmware *self) { } static void fu_synaptics_cape_hid_firmware_class_init(FuSynapticsCapeHidFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_synaptics_cape_hid_firmware_parse; firmware_class->write = fu_synaptics_cape_hid_firmware_write; } FuFirmware * fu_synaptics_cape_hid_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_SYNAPTICS_CAPE_HID_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/synaptics-cape/fu-synaptics-cape-hid-firmware.h000066400000000000000000000010041501337203100253710ustar00rootroot00000000000000/* * Copyright 2021 Synaptics Incorporated * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-synaptics-cape-firmware.h" #define FU_TYPE_SYNAPTICS_CAPE_HID_FIRMWARE (fu_synaptics_cape_hid_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuSynapticsCapeHidFirmware, fu_synaptics_cape_hid_firmware, FU, SYNAPTICS_CAPE_HID_FIRMWARE, FuSynapticsCapeFirmware) FuFirmware * fu_synaptics_cape_hid_firmware_new(void); fwupd-2.0.10/plugins/synaptics-cape/fu-synaptics-cape-plugin.c000066400000000000000000000020631501337203100243120ustar00rootroot00000000000000/* * Copyright 2021 Synaptics Incorporated * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-synaptics-cape-device.h" #include "fu-synaptics-cape-hid-firmware.h" #include "fu-synaptics-cape-plugin.h" #include "fu-synaptics-cape-sngl-firmware.h" struct _FuSynapticsCapePlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuSynapticsCapePlugin, fu_synaptics_cape_plugin, FU_TYPE_PLUGIN) static void fu_synaptics_cape_plugin_init(FuSynapticsCapePlugin *self) { } static void fu_synaptics_cape_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_SYNAPTICS_CAPE_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_SYNAPTICS_CAPE_HID_FIRMWARE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_SYNAPTICS_CAPE_SNGL_FIRMWARE); } static void fu_synaptics_cape_plugin_class_init(FuSynapticsCapePluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_synaptics_cape_plugin_constructed; } fwupd-2.0.10/plugins/synaptics-cape/fu-synaptics-cape-plugin.h000066400000000000000000000004371501337203100243220ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuSynapticsCapePlugin, fu_synaptics_cape_plugin, FU, SYNAPTICS_CAPE_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/synaptics-cape/fu-synaptics-cape-sngl-firmware.c000066400000000000000000000077231501337203100256010ustar00rootroot00000000000000/* * Copyright 2023 Synaptics Incorporated * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-synaptics-cape-sngl-firmware.h" #include "fu-synaptics-cape-struct.h" struct _FuSynapticsCapeSnglFirmware { FuSynapticsCapeFirmware parent_instance; }; G_DEFINE_TYPE(FuSynapticsCapeSnglFirmware, fu_synaptics_cape_sngl_firmware, FU_TYPE_SYNAPTICS_CAPE_FIRMWARE) static gboolean fu_synaptics_cape_sngl_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuSynapticsCapeSnglFirmware *self = FU_SYNAPTICS_CAPE_SNGL_FIRMWARE(firmware); gsize streamsz = 0; guint16 num_fw_file; g_autoptr(GByteArray) st = NULL; g_autofree gchar *version_str = NULL; /* sanity check */ if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if ((guint32)streamsz % 4 != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "data not aligned to 32 bits"); return FALSE; } if (streamsz < 8) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "image is too small"); return FALSE; } /* unpack */ st = fu_struct_synaptics_cape_sngl_hdr_parse_stream(stream, 0x0, error); if (st == NULL) return FALSE; if (fu_struct_synaptics_cape_sngl_hdr_get_file_size(st) != streamsz) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "file size is incorrect"); return FALSE; } /* check CRC */ if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { guint32 crc_calc = 0xFFFFFFFF; g_autoptr(GInputStream) stream_tmp = NULL; stream_tmp = fu_partial_input_stream_new(stream, 8, G_MAXSIZE, error); if (stream_tmp == NULL) return FALSE; if (!fu_input_stream_compute_crc32(stream_tmp, FU_CRC_KIND_B32_STANDARD, &crc_calc, error)) return FALSE; if (crc_calc != fu_struct_synaptics_cape_sngl_hdr_get_file_crc(st)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "CRC did not match, got 0x%x, expected 0x%x", fu_struct_synaptics_cape_sngl_hdr_get_file_crc(st), crc_calc); return FALSE; } } fu_synaptics_cape_firmware_set_vid(FU_SYNAPTICS_CAPE_FIRMWARE(self), fu_struct_synaptics_cape_sngl_hdr_get_vid(st)); fu_synaptics_cape_firmware_set_pid(FU_SYNAPTICS_CAPE_FIRMWARE(self), fu_struct_synaptics_cape_sngl_hdr_get_pid(st)); version_str = fu_version_from_uint32(fu_struct_synaptics_cape_sngl_hdr_get_fw_version(st), FWUPD_VERSION_FORMAT_QUAD); fu_firmware_set_version(FU_FIRMWARE(self), version_str); /* add each file */ num_fw_file = fu_struct_synaptics_cape_sngl_hdr_get_fw_file_num(st); if (num_fw_file == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no image files found"); return FALSE; } /* success */ return TRUE; } static GByteArray * fu_synaptics_cape_sngl_firmware_write(FuFirmware *firmware, GError **error) { FuSynapticsCapeSnglFirmware *self = FU_SYNAPTICS_CAPE_SNGL_FIRMWARE(firmware); g_autoptr(GByteArray) buf = fu_struct_synaptics_cape_sngl_hdr_new(); /* pack */ fu_struct_synaptics_cape_sngl_hdr_set_vid( buf, fu_synaptics_cape_firmware_get_vid(FU_SYNAPTICS_CAPE_FIRMWARE(self))); fu_struct_synaptics_cape_sngl_hdr_set_pid( buf, fu_synaptics_cape_firmware_get_pid(FU_SYNAPTICS_CAPE_FIRMWARE(self))); /* success */ return g_steal_pointer(&buf); } static void fu_synaptics_cape_sngl_firmware_init(FuSynapticsCapeSnglFirmware *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); } static void fu_synaptics_cape_sngl_firmware_class_init(FuSynapticsCapeSnglFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_synaptics_cape_sngl_firmware_parse; firmware_class->write = fu_synaptics_cape_sngl_firmware_write; } fwupd-2.0.10/plugins/synaptics-cape/fu-synaptics-cape-sngl-firmware.h000066400000000000000000000007211501337203100255750ustar00rootroot00000000000000/* * Copyright 2023 Synaptics Incorporated * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-synaptics-cape-firmware.h" #define FU_TYPE_SYNAPTICS_CAPE_SNGL_FIRMWARE (fu_synaptics_cape_sngl_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuSynapticsCapeSnglFirmware, fu_synaptics_cape_sngl_firmware, FU, SYNAPTICS_CAPE_SNGL_FIRMWARE, FuSynapticsCapeFirmware) fwupd-2.0.10/plugins/synaptics-cape/fu-synaptics-cape.rs000066400000000000000000000066701501337203100232300ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[repr(u16le)] enum FuSynapticsCapeCmd { FwUpdateStart = 0xC8, FwUpdateWrite = 0xC9, FwUpdateEnd = 0xCA, McuSoftReset = 0xAF, FwGetActivePartition = 0x1CF, GetVersion = 0x103, } enum FuSynapticsCapeError { Eagain = -11, SfuFail = -200, SfuWriteFail = -201, SfuReadFail = -202, SfuCrcError = -203, SfuUsbIdNotMatch = -204, SfuVersionDowngrade = -205, SfuHeaderCorruption = -206, SfuImageCorruption = -207, SfuAlreadyActive = -208, SfuNotReady = -209, SfuSignTransferCorruption = -210, SfuDigitalSignatureVerificationFailed = -211, SfuTaskNotRunning = -212, GenericFailure = -1025, AlreadyExists = -1026, NullAppPointer = -1027, NullModulePointer = -1028, NullStreamPointer = -1029, NullPointer = -1030, BadAppId = -1031, ModuleTypeHasNoApi = -1034, BadMagicNumber = -1052, CmdModeUnsupported = -1056, } #[repr(u32le)] enum FuSynapticsCapeModuleId { AppCtrl = 0xb32d2300, } #[derive(New, Getters, Default)] #[repr(C, packed)] struct FuSynapticsCapeMsg { data_len: u16le, // data length in DWORDs cmd_id: FuSynapticsCapeCmd, // bit 15 set when the host want a reply from device module_id: FuSynapticsCapeModuleId = AppCtrl, data: [u32le; 13], } #[derive(New, Getters, Default)] #[repr(C, packed)] struct FuSynapticsCapeCmdHidReport { report_id: u16le == 1, msg: FuSynapticsCapeMsg, } #[derive(New, ParseStream, Default)] #[repr(C, packed)] struct FuStructSynapticsCapeHidHdr { vid: u32le, pid: u32le, update_type: u32le, signature: u32le == 0x43534645, // "EFSC" crc: u32le, ver_w: u16le, ver_x: u16le, ver_y: u16le, ver_z: u16le, reserved: u32le, } #[derive(New, ParseStream, Default)] #[repr(C, packed)] struct FuStructSynapticsCapeSnglHdr { magic: u32le == 0x4C474E53, // "SNGL" file_crc: u32le, file_size: u32le, magic2: u32le, img_type: u32le, fw_version: u32le, vid: u16le, pid: u16le, fw_file_num: u32le, version: u32le, fw_crc: u32le, _reserved: [u8; 8], machine_name: [char; 16], time_stamp: [char; 16], } #[repr(C, packed)] struct FuStructSynapticsCapeSnglFile { Id: u32le, Crc: u32le, File: u32le, Size: u32le, } enum FuSynapticsCapeSnglImgTypeId { Hid0 = 0x30444948, // hid file for partition 0 Hid1 = 0x31444948, // hid file for partition 1 Hof0 = 0x30464F48, // hid + offer file for partition 0 Hof1 = 0x31464F48, // hid + offer file for partition 1 Sfsx = 0x58534653, // sfs file Sofx = 0x58464F53, // sfs + offer file Sign = 0x4e474953, // digital signature file } enum FuSynapticsCapeFirmwarePartition { Auto, 1, 2, } fwupd-2.0.10/plugins/synaptics-cape/meson.build000066400000000000000000000011761501337203100214650ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginSynapticsCape"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('synaptics-cape.quirk') plugin_builtins += static_library('fu_plugin_synaptics_cape', rustgen.process( 'fu-synaptics-cape.rs', # fuzzing ), sources: [ 'fu-synaptics-cape-plugin.c', 'fu-synaptics-cape-device.c', 'fu-synaptics-cape-firmware.c', # fuzzing 'fu-synaptics-cape-hid-firmware.c', # fuzzing 'fu-synaptics-cape-sngl-firmware.c', # fuzzing ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) fwupd-2.0.10/plugins/synaptics-cape/synaptics-cape.quirk000066400000000000000000000047741501337203100233320ustar00rootroot00000000000000# Framework Audio Card [USB\VID_32AC&PID_0010] Guid[quirk] = SYNAPTICS_CAPE\CX31993 # EPOS Raw Plus [USB\VID_1395&PID_0280] Guid[quirk] = SYNAPTICS_CAPE\CX31993 Flags = use-in-report-interrupt [USB\VID_1395&PID_0281] Guid[quirk] = SYNAPTICS_CAPE\CX31993 Flags = use-in-report-interrupt # RAW Teams [USB\VID_1395&PID_0294] Guid[quirk] = SYNAPTICS_CAPE\CX31993 Flags = use-in-report-interrupt [USB\VID_1395&PID_0295] Guid[quirk] = SYNAPTICS_CAPE\CX31993 Flags = use-in-report-interrupt [USB\VID_1395&PID_0296] Guid[quirk] = SYNAPTICS_CAPE\CX31993 Flags = use-in-report-interrupt [USB\VID_1395&PID_0297] Guid[quirk] = SYNAPTICS_CAPE\CX31993 Flags = use-in-report-interrupt # This is the outdated PID for 0x0298 [USB\VID_1395&PID_0286] Guid[quirk] = SYNAPTICS_CAPE\CX31993 Flags = add-counterpart-guids CounterpartGuid = USB\VID_1395&PID_0298 [USB\VID_1395&PID_0298] Guid[quirk] = SYNAPTICS_CAPE\CX31993 Flags = use-in-report-interrupt [USB\VID_1395&PID_0299] Guid[quirk] = SYNAPTICS_CAPE\CX31993 Flags = use-in-report-interrupt [USB\VID_1395&PID_0400] Guid[quirk] = SYNAPTICS_CAPE\CX31993 Flags = use-in-report-interrupt # EPOS Morgan-T [USB\VID_1395&PID_0200] Guid[quirk] = SYNAPTICS_CAPE\CX31993 Flags = use-in-report-interrupt [USB\VID_1395&PID_0288] Guid[quirk] = SYNAPTICS_CAPE\CX31993 Flags = use-in-report-interrupt [USB\VID_1395&PID_0289] Guid[quirk] = SYNAPTICS_CAPE\CX31993 Flags = use-in-report-interrupt [USB\VID_1395&PID_028A] Guid[quirk] = SYNAPTICS_CAPE\CX31993 Flags = use-in-report-interrupt [USB\VID_1395&PID_028B] Guid[quirk] = SYNAPTICS_CAPE\CX31993 Flags = use-in-report-interrupt [USB\VID_1395&PID_028C] Guid[quirk] = SYNAPTICS_CAPE\CX31993 Flags = use-in-report-interrupt [USB\VID_1395&PID_028D] Guid[quirk] = SYNAPTICS_CAPE\CX31993 Flags = use-in-report-interrupt [USB\VID_1395&PID_028E] Guid[quirk] = SYNAPTICS_CAPE\CX31993 Flags = use-in-report-interrupt [USB\VID_1395&PID_028F] Guid[quirk] = SYNAPTICS_CAPE\CX31993 Flags = use-in-report-interrupt # EPOS Morgan-V [USB\VID_1395&PID_0290] Guid[quirk] = SYNAPTICS_CAPE\CX31993 Flags = use-in-report-interrupt [USB\VID_1395&PID_0291] Guid[quirk] = SYNAPTICS_CAPE\CX31993 Flags = use-in-report-interrupt [USB\VID_1395&PID_0292] Guid[quirk] = SYNAPTICS_CAPE\CX31993 Flags = use-in-report-interrupt [USB\VID_1395&PID_0293] Guid[quirk] = SYNAPTICS_CAPE\CX31993 Flags = use-in-report-interrupt # USB audio codec USB receiver [SYNAPTICS_CAPE\CX31993] Plugin = synaptics_cape Icon = usb-receiver # USB audio codec Hifi [SYNAPTICS_CAPE\CX31988] Plugin = synaptics_cape fwupd-2.0.10/plugins/synaptics-cape/tests/000077500000000000000000000000001501337203100204605ustar00rootroot00000000000000fwupd-2.0.10/plugins/synaptics-cape/tests/synaptics-cape.builder.xml000066400000000000000000000003631501337203100255540ustar00rootroot00000000000000 has-vid-pid payload 8.41.24.0 EFSCh... header fwupd-2.0.10/plugins/synaptics-cxaudio/000077500000000000000000000000001501337203100200425ustar00rootroot00000000000000fwupd-2.0.10/plugins/synaptics-cxaudio/README.md000066400000000000000000000025471501337203100213310ustar00rootroot00000000000000--- title: Plugin: Synaptics CxAudio — Conexant Audio --- ## Introduction This plugin is used to update a small subset of Conexant (now owned by Synaptics) audio devices. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a modified SREC file format. This plugin supports the following protocol ID: * `com.synaptics.synaptics-cxaudio` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_17EF&PID_3083` These devices also use custom GUID values, e.g. * `SYNAPTICS_CXAUDIO\CX2198X` (only-quirk) ## Update Behavior The firmware is deployed when the device is in normal runtime mode, and the device will reset when the new firmware has been written. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x17EF` ## Quirk Use This plugin uses the following plugin-specific quirks: ### CxaudioChipIdBase Base integer for ChipID. Since: 1.3.2 ### CxaudioSoftwareReset If the chip supports self-reset. Since: 1.3.2 ### CxaudioPatch1ValidAddr Address of patch location #1. Since: 1.3.2 ### CxaudioPatch2ValidAddr Address of patch location #2. Since: 1.3.2 ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.3.2`. fwupd-2.0.10/plugins/synaptics-cxaudio/fu-synaptics-cxaudio-common.h000066400000000000000000000025331501337203100255630ustar00rootroot00000000000000/* * Copyright 2005 Synaptics Incorporated * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include /* usb */ #define FU_SYNAPTICS_CXAUDIO_INPUT_REPORT_SIZE 35 #define FU_SYNAPTICS_CXAUDIO_OUTPUT_REPORT_SIZE 39 #define FU_SYNAPTICS_CXAUDIO_USB_TIMEOUT 2000 /* ms */ /* commands */ #define FU_SYNAPTICS_CXAUDIO_MEM_WRITEID 0x4 #define FU_SYNAPTICS_CXAUDIO_MEM_READID 0x5 /* EEPROM */ #define FU_SYNAPTICS_CXAUDIO_EEPROM_VALIDITY_SIGNATURE_OFFSET 0x0000 #define FU_SYNAPTICS_CXAUDIO_EEPROM_PATCH_INFO_OFFSET 0x0014 #define FU_SYNAPTICS_CXAUDIO_EEPROM_CUSTOM_INFO_OFFSET 0x0020 #define FU_SYNAPTICS_CXAUDIO_EEPROM_CPX_PATCH_VERSION_ADDRESS 0x0022 #define FU_SYNAPTICS_CXAUDIO_EEPROM_CPX_PATCH2_VERSION_ADDRESS 0x0176 #define FU_SYNAPTICS_CXAUDIO_EEPROM_STORAGE_SIZE_ADDRESS 0x0005 #define FU_SYNAPTICS_CXAUDIO_EEPROM_STORAGE_PADDING_SIZE 0x4 /* bytes */ #define FU_SYNAPTICS_CXAUDIO_DEVICE_CAPABILITIES_STRIDX 50 #define FU_SYNAPTICS_CXAUDIO_SIGNATURE_BYTE 'S' #define FU_SYNAPTICS_CXAUDIO_SIGNATURE_PATCH_BYTE 'P' #define FU_SYNAPTICS_CXAUDIO_REG_FIRMWARE_PARK_ADDR 0x1000 #define FU_SYNAPTICS_CXAUDIO_REG_FIRMWARE_VERSION_ADDR 0x1001 #define FU_SYNAPTICS_CXAUDIO_REG_RESET_ADDR 0x0400 #define FU_SYNAPTICS_CXAUDIO_EEPROM_SHADOW_SIZE (8 * 1024) fwupd-2.0.10/plugins/synaptics-cxaudio/fu-synaptics-cxaudio-device.c000066400000000000000000000707251501337203100255350ustar00rootroot00000000000000/* * Copyright 2005 Synaptics Incorporated * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-synaptics-cxaudio-common.h" #include "fu-synaptics-cxaudio-device.h" #include "fu-synaptics-cxaudio-firmware.h" #include "fu-synaptics-cxaudio-struct.h" struct _FuSynapticsCxaudioDevice { FuHidDevice parent_instance; guint32 chip_id_base; guint32 chip_id; gboolean serial_number_set; gboolean sw_reset_supported; guint32 eeprom_layout_version; guint32 eeprom_patch2_valid_addr; guint32 eeprom_patch_valid_addr; guint32 eeprom_storage_address; guint32 eeprom_storage_sz; guint32 eeprom_sz; guint8 patch_level; }; G_DEFINE_TYPE(FuSynapticsCxaudioDevice, fu_synaptics_cxaudio_device, FU_TYPE_HID_DEVICE) static void fu_synaptics_cxaudio_device_to_string(FuDevice *device, guint idt, GString *str) { FuSynapticsCxaudioDevice *self = FU_SYNAPTICS_CXAUDIO_DEVICE(device); fwupd_codec_string_append_int(str, idt, "ChipIdBase", self->chip_id_base); fwupd_codec_string_append_int(str, idt, "ChipId", self->chip_id); fwupd_codec_string_append_hex(str, idt, "EepromLayoutVersion", self->eeprom_layout_version); fwupd_codec_string_append_hex(str, idt, "EepromStorageAddress", self->eeprom_storage_address); fwupd_codec_string_append_hex(str, idt, "EepromStorageSz", self->eeprom_storage_sz); fwupd_codec_string_append_hex(str, idt, "EepromSz", self->eeprom_sz); fwupd_codec_string_append_bool(str, idt, "SwResetSupported", self->sw_reset_supported); fwupd_codec_string_append_bool(str, idt, "SerialNumberSet", self->serial_number_set); } static gboolean fu_synaptics_cxaudio_device_output_report(FuSynapticsCxaudioDevice *self, guint8 *buf, guint16 bufsz, GError **error) { /* weird */ if (buf[0] == 0x0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "report 0 not supported"); return FALSE; } /* to device */ return fu_hid_device_set_report(FU_HID_DEVICE(self), buf[0], buf, bufsz, FU_SYNAPTICS_CXAUDIO_USB_TIMEOUT, FU_HID_DEVICE_FLAG_RETRY_FAILURE, error); } static gboolean fu_synaptics_cxaudio_device_input_report(FuSynapticsCxaudioDevice *self, guint8 ReportID, guint8 *buf, guint16 bufsz, GError **error) { return fu_hid_device_get_report(FU_HID_DEVICE(self), ReportID, buf, bufsz, FU_SYNAPTICS_CXAUDIO_USB_TIMEOUT, FU_HID_DEVICE_FLAG_RETRY_FAILURE, error); } typedef enum { FU_SYNAPTICS_CXAUDIO_OPERATION_READ, FU_SYNAPTICS_CXAUDIO_OPERATION_WRITE, FU_SYNAPTICS_CXAUDIO_OPERATION_LAST } FuSynapticsCxaudioOperation; typedef enum { FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_NONE = 0, FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_VERIFY = (1 << 4), } FuSynapticsCxaudioOperationFlags; static gboolean fu_synaptics_cxaudio_device_operation(FuSynapticsCxaudioDevice *self, FuSynapticsCxaudioOperation operation, FuSynapticsCxaudioMemKind mem_kind, guint32 addr, guint8 *buf, gsize bufsz, FuSynapticsCxaudioOperationFlags flags, GError **error) { const guint32 idx_read = 0x1; const guint32 idx_write = 0x5; const guint32 payload_max = 0x20; guint32 size = 0x02800; g_autoptr(GPtrArray) chunks = NULL; g_return_val_if_fail(bufsz > 0, FALSE); g_return_val_if_fail(buf != NULL, FALSE); /* check if memory operation is supported by device */ if (operation == FU_SYNAPTICS_CXAUDIO_OPERATION_WRITE && mem_kind == FU_SYNAPTICS_CXAUDIO_MEM_KIND_CPX_ROM) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "trying to write unwritable section %u", mem_kind); return FALSE; } /* check memory address - should be within valid range */ if (mem_kind == FU_SYNAPTICS_CXAUDIO_MEM_KIND_EEPROM) size = 0x20000; if (addr > size) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "address out of range 0x%x < 0x%x", addr, size); return FALSE; } /* send to hardware */ chunks = fu_chunk_array_mutable_new(buf, bufsz, addr, 0x0, payload_max); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); guint8 inbuf[FU_SYNAPTICS_CXAUDIO_INPUT_REPORT_SIZE] = {0}; guint8 outbuf[FU_SYNAPTICS_CXAUDIO_OUTPUT_REPORT_SIZE] = {0}; /* first byte is always report ID */ outbuf[0] = FU_SYNAPTICS_CXAUDIO_MEM_WRITEID; /* set memory address and payload length (if relevant) */ if (fu_chunk_get_address(chk) >= 64 * 1024) FU_BIT_SET(outbuf[1], 4); outbuf[2] = fu_chunk_get_data_sz(chk); fu_memwrite_uint16(outbuf + 3, fu_chunk_get_address(chk), G_BIG_ENDIAN); /* set memtype */ if (mem_kind == FU_SYNAPTICS_CXAUDIO_MEM_KIND_EEPROM) FU_BIT_SET(outbuf[1], 5); /* fill the report payload part */ if (operation == FU_SYNAPTICS_CXAUDIO_OPERATION_WRITE) { FU_BIT_SET(outbuf[1], 6); if (!fu_memcpy_safe(outbuf, sizeof(outbuf), idx_write, /* dst */ fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0x0, /* src */ fu_chunk_get_data_sz(chk), error)) return FALSE; } if (!fu_synaptics_cxaudio_device_output_report(self, outbuf, sizeof(outbuf), error)) return FALSE; /* issue additional write directive to read */ if (operation == FU_SYNAPTICS_CXAUDIO_OPERATION_WRITE && flags & FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_VERIFY) { FU_BIT_CLEAR(outbuf[1], 6); if (!fu_synaptics_cxaudio_device_output_report(self, outbuf, sizeof(outbuf), error)) return FALSE; } if (operation == FU_SYNAPTICS_CXAUDIO_OPERATION_READ || flags & FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_VERIFY) { if (!fu_synaptics_cxaudio_device_input_report( self, FU_SYNAPTICS_CXAUDIO_MEM_READID, inbuf, sizeof(inbuf), error)) return FALSE; } if (operation == FU_SYNAPTICS_CXAUDIO_OPERATION_WRITE && flags & FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_VERIFY) { if (!fu_memcmp_safe(outbuf, sizeof(outbuf), idx_write, inbuf, sizeof(inbuf), idx_read, payload_max, error)) { g_prefix_error(error, "failed to verify on packet %u @0x%x: ", fu_chunk_get_idx(chk), (guint)fu_chunk_get_address(chk)); return FALSE; } } if (operation == FU_SYNAPTICS_CXAUDIO_OPERATION_READ) { if (!fu_memcpy_safe(fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), 0x0, /* dst */ inbuf, sizeof(inbuf), idx_read, /* src */ fu_chunk_get_data_sz(chk), error)) return FALSE; } } /* success */ return TRUE; } static gboolean fu_synaptics_cxaudio_device_register_clear_bit(FuSynapticsCxaudioDevice *self, guint32 address, guint8 bit_position, GError **error) { guint8 tmp = 0x0; if (!fu_synaptics_cxaudio_device_operation(self, FU_SYNAPTICS_CXAUDIO_OPERATION_READ, FU_SYNAPTICS_CXAUDIO_MEM_KIND_CPX_RAM, address, &tmp, sizeof(tmp), FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_NONE, error)) return FALSE; FU_BIT_CLEAR(tmp, bit_position); return fu_synaptics_cxaudio_device_operation(self, FU_SYNAPTICS_CXAUDIO_OPERATION_WRITE, FU_SYNAPTICS_CXAUDIO_MEM_KIND_CPX_RAM, address, &tmp, sizeof(guint8), FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_NONE, error); } static gboolean fu_synaptics_cxaudio_device_register_set_bit(FuSynapticsCxaudioDevice *self, guint32 address, guint8 bit_position, GError **error) { guint8 tmp = 0x0; if (!fu_synaptics_cxaudio_device_operation(self, FU_SYNAPTICS_CXAUDIO_OPERATION_READ, FU_SYNAPTICS_CXAUDIO_MEM_KIND_CPX_RAM, address, &tmp, sizeof(tmp), FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_NONE, error)) return FALSE; FU_BIT_SET(tmp, bit_position); return fu_synaptics_cxaudio_device_operation(self, FU_SYNAPTICS_CXAUDIO_OPERATION_WRITE, FU_SYNAPTICS_CXAUDIO_MEM_KIND_CPX_RAM, address, &tmp, sizeof(tmp), FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_NONE, error); } static gchar * fu_synaptics_cxaudio_device_eeprom_read_string(FuSynapticsCxaudioDevice *self, guint32 address, GError **error) { guint8 buf[FU_STRUCT_SYNAPTICS_CXAUDIO_STRING_HEADER_SIZE] = {0}; guint8 header_length; g_autofree gchar *str = NULL; g_autoptr(GByteArray) st = NULL; /* read header */ if (!fu_synaptics_cxaudio_device_operation(self, FU_SYNAPTICS_CXAUDIO_OPERATION_READ, FU_SYNAPTICS_CXAUDIO_MEM_KIND_EEPROM, address, buf, sizeof(buf), FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_NONE, error)) { g_prefix_error(error, "failed to read EEPROM string header @0x%x: ", address); return NULL; } /* sanity check */ st = fu_struct_synaptics_cxaudio_string_header_parse(buf, sizeof(buf), 0x0, error); if (st == NULL) return NULL; header_length = fu_struct_synaptics_cxaudio_string_header_get_length(st); if (header_length < st->len) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "EEPROM string header length invalid"); return NULL; } /* allocate buffer + NUL terminator */ str = g_malloc0(header_length - st->len + 1); if (!fu_synaptics_cxaudio_device_operation(self, FU_SYNAPTICS_CXAUDIO_OPERATION_READ, FU_SYNAPTICS_CXAUDIO_MEM_KIND_EEPROM, address + sizeof(buf), (guint8 *)str, header_length - sizeof(buf), FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_NONE, error)) { g_prefix_error(error, "failed to read EEPROM string @0x%x: ", address); return NULL; } return g_steal_pointer(&str); } static gboolean fu_synaptics_cxaudio_device_ensure_patch_level(FuSynapticsCxaudioDevice *self, GError **error) { guint8 tmp = 0x0; if (!fu_synaptics_cxaudio_device_operation(self, FU_SYNAPTICS_CXAUDIO_OPERATION_READ, FU_SYNAPTICS_CXAUDIO_MEM_KIND_EEPROM, self->eeprom_patch_valid_addr, &tmp, sizeof(tmp), FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_NONE, error)) { g_prefix_error(error, "failed to read EEPROM patch validation byte: "); return FALSE; } if (tmp == FU_SYNAPTICS_CXAUDIO_SIGNATURE_PATCH_BYTE) { self->patch_level = 1; return TRUE; } if (!fu_synaptics_cxaudio_device_operation(self, FU_SYNAPTICS_CXAUDIO_OPERATION_READ, FU_SYNAPTICS_CXAUDIO_MEM_KIND_EEPROM, self->eeprom_patch2_valid_addr, &tmp, sizeof(tmp), FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_NONE, error)) { g_prefix_error(error, "failed to read EEPROM patch validation byte: "); return FALSE; } if (tmp == FU_SYNAPTICS_CXAUDIO_SIGNATURE_PATCH_BYTE) { self->patch_level = 2; return TRUE; } /* not sure what to do here */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "EEPROM patch version undiscoverable"); return FALSE; } static gboolean fu_synaptics_cxaudio_device_setup(FuDevice *device, GError **error) { FuSynapticsCxaudioDevice *self = FU_SYNAPTICS_CXAUDIO_DEVICE(device); guint32 addr = FU_SYNAPTICS_CXAUDIO_EEPROM_CPX_PATCH_VERSION_ADDRESS; guint8 chip_id_offset = 0x0; guint8 sigbuf[FU_STRUCT_SYNAPTICS_CXAUDIO_VALIDITY_SIGNATURE_SIZE] = {0x0}; guint8 verbuf_fw[4] = {0x0}; guint8 verbuf_patch[3] = {0x0}; g_autofree gchar *cap_str = NULL; g_autofree gchar *chip_id = NULL; g_autofree gchar *summary = NULL; g_autofree gchar *version_fw = NULL; g_autofree gchar *version_patch = NULL; g_autoptr(GByteArray) st_inf = NULL; g_autoptr(GByteArray) st_sig = NULL; guint8 cinfo[FU_STRUCT_SYNAPTICS_CXAUDIO_CUSTOM_INFO_SIZE] = {0x0}; /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_synaptics_cxaudio_device_parent_class)->setup(device, error)) return FALSE; /* get the ChipID */ if (!fu_synaptics_cxaudio_device_operation(self, FU_SYNAPTICS_CXAUDIO_OPERATION_READ, FU_SYNAPTICS_CXAUDIO_MEM_KIND_CPX_RAM, 0x1005, &chip_id_offset, sizeof(chip_id_offset), FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_NONE, error)) { g_prefix_error(error, "failed to read ChipID: "); return FALSE; } self->chip_id = self->chip_id_base + chip_id_offset; /* add instance ID */ chip_id = g_strdup_printf("CX%u", self->chip_id); fu_device_add_instance_str(device, "ID", chip_id); if (!fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, error, "SYNAPTICS_CXAUDIO", "ID", NULL)) return FALSE; /* set summary */ summary = g_strdup_printf("CX%u USB audio device", self->chip_id); fu_device_set_summary(device, summary); /* read the EEPROM validity signature */ if (!fu_synaptics_cxaudio_device_operation( self, FU_SYNAPTICS_CXAUDIO_OPERATION_READ, FU_SYNAPTICS_CXAUDIO_MEM_KIND_EEPROM, FU_SYNAPTICS_CXAUDIO_EEPROM_VALIDITY_SIGNATURE_OFFSET, sigbuf, sizeof(sigbuf), FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_NONE, error)) { g_prefix_error(error, "failed to read EEPROM signature bytes: "); return FALSE; } /* blank EEPROM */ if (sigbuf[0] == 0xff && sigbuf[1] == 0xff) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "EEPROM is missing or blank"); return FALSE; } /* is disabled on EVK board using jumper */ if ((sigbuf[0] == 0x00 && sigbuf[1] == 0x00) || (sigbuf[0] == 0xff && sigbuf[1] == 0x00)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "EEPROM has been disabled using a jumper"); return FALSE; } /* check magic byte */ st_sig = fu_struct_synaptics_cxaudio_validity_signature_parse(sigbuf, sizeof(sigbuf), 0x0, error); if (st_sig == NULL) return FALSE; if (fu_struct_synaptics_cxaudio_validity_signature_get_magic_byte(st_sig) != FU_STRUCT_SYNAPTICS_CXAUDIO_VALIDITY_SIGNATURE_DEFAULT_MAGIC_BYTE) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "EEPROM magic byte invalid, got 0x%02x expected 0x%02x", fu_struct_synaptics_cxaudio_validity_signature_get_magic_byte(st_sig), (guint)FU_STRUCT_SYNAPTICS_CXAUDIO_VALIDITY_SIGNATURE_DEFAULT_MAGIC_BYTE); return FALSE; } /* calculate EEPROM size */ self->eeprom_sz = (guint32)1 << (fu_struct_synaptics_cxaudio_validity_signature_get_eeprom_size_code(st_sig) + 8); if (!fu_synaptics_cxaudio_device_operation(self, FU_SYNAPTICS_CXAUDIO_OPERATION_READ, FU_SYNAPTICS_CXAUDIO_MEM_KIND_EEPROM, FU_SYNAPTICS_CXAUDIO_EEPROM_STORAGE_SIZE_ADDRESS, sigbuf, sizeof(sigbuf), FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_NONE, error)) { g_prefix_error(error, "failed to read EEPROM signature bytes: "); return FALSE; } self->eeprom_storage_sz = fu_memread_uint16(sigbuf, G_LITTLE_ENDIAN); if (self->eeprom_storage_sz < self->eeprom_sz - FU_SYNAPTICS_CXAUDIO_EEPROM_STORAGE_PADDING_SIZE) { self->eeprom_storage_address = self->eeprom_sz - self->eeprom_storage_sz - FU_SYNAPTICS_CXAUDIO_EEPROM_STORAGE_PADDING_SIZE; } /* get EEPROM custom info */ if (!fu_synaptics_cxaudio_device_operation(self, FU_SYNAPTICS_CXAUDIO_OPERATION_READ, FU_SYNAPTICS_CXAUDIO_MEM_KIND_EEPROM, FU_SYNAPTICS_CXAUDIO_EEPROM_CUSTOM_INFO_OFFSET, cinfo, sizeof(cinfo), FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_NONE, error)) { g_prefix_error(error, "failed to read EEPROM custom info: "); return FALSE; } /* parse */ st_inf = fu_struct_synaptics_cxaudio_custom_info_parse(cinfo, sizeof(cinfo), 0x0, error); if (st_inf == NULL) return FALSE; if (fu_struct_synaptics_cxaudio_custom_info_get_layout_signature(st_inf) == FU_SYNAPTICS_CXAUDIO_SIGNATURE_BYTE) self->eeprom_layout_version = fu_struct_synaptics_cxaudio_custom_info_get_layout_version(st_inf); /* serial number, which also allows us to recover it after write */ if (self->eeprom_layout_version >= 0x01) { guint16 serial_number_string_address = fu_struct_synaptics_cxaudio_custom_info_get_serial_number_string_address( st_inf); self->serial_number_set = serial_number_string_address != 0x0; if (self->serial_number_set) { g_autofree gchar *tmp = NULL; tmp = fu_synaptics_cxaudio_device_eeprom_read_string( self, serial_number_string_address, error); if (tmp == NULL) return FALSE; fu_device_set_serial(device, tmp); } } /* read fw version */ if (!fu_synaptics_cxaudio_device_operation(self, FU_SYNAPTICS_CXAUDIO_OPERATION_READ, FU_SYNAPTICS_CXAUDIO_MEM_KIND_CPX_RAM, FU_SYNAPTICS_CXAUDIO_REG_FIRMWARE_VERSION_ADDR, verbuf_fw, sizeof(verbuf_fw), FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_NONE, error)) { g_prefix_error(error, "failed to read EEPROM firmware version: "); return FALSE; } version_fw = g_strdup_printf("%02X.%02X.%02X.%02X", verbuf_fw[1], verbuf_fw[0], verbuf_fw[3], verbuf_fw[2]); fu_device_set_version_bootloader(device, version_fw); /* use a different address if a patch is in use */ if (self->eeprom_patch_valid_addr != 0x0) { if (!fu_synaptics_cxaudio_device_ensure_patch_level(self, error)) return FALSE; } if (self->patch_level == 2) addr = FU_SYNAPTICS_CXAUDIO_EEPROM_CPX_PATCH2_VERSION_ADDRESS; if (!fu_synaptics_cxaudio_device_operation(self, FU_SYNAPTICS_CXAUDIO_OPERATION_READ, FU_SYNAPTICS_CXAUDIO_MEM_KIND_EEPROM, addr, verbuf_patch, sizeof(verbuf_patch), FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_NONE, error)) { g_prefix_error(error, "failed to read EEPROM patch version: "); return FALSE; } version_patch = g_strdup_printf("%02X-%02X-%02X", verbuf_patch[0], verbuf_patch[1], verbuf_patch[2]); fu_device_set_version(device, version_patch); /* find out if patch supports additional capabilities (optional) */ cap_str = fu_usb_device_get_string_descriptor(FU_USB_DEVICE(device), FU_SYNAPTICS_CXAUDIO_DEVICE_CAPABILITIES_STRIDX, NULL); if (cap_str != NULL) { g_auto(GStrv) split = g_strsplit(cap_str, ";", -1); for (guint i = 0; split[i] != NULL; i++) { g_debug("capability: %s", split[i]); if (g_strcmp0(split[i], "RESET") == 0) self->sw_reset_supported = TRUE; } } /* success */ return TRUE; } static FuFirmware * fu_synaptics_cxaudio_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuSynapticsCxaudioDevice *self = FU_SYNAPTICS_CXAUDIO_DEVICE(device); guint32 chip_id_base; g_autoptr(FuFirmware) firmware = fu_synaptics_cxaudio_firmware_new(); if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; chip_id_base = fu_synaptics_cxaudio_firmware_get_devtype(FU_SYNAPTICS_CXAUDIO_FIRMWARE(firmware)); if (chip_id_base != self->chip_id_base) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "device 0x%04u is incompatible with firmware 0x%04u", self->chip_id_base, chip_id_base); return NULL; } return g_steal_pointer(&firmware); } static gboolean fu_synaptics_cxaudio_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuSynapticsCxaudioDevice *self = FU_SYNAPTICS_CXAUDIO_DEVICE(device); GPtrArray *records = fu_srec_firmware_get_records(FU_SREC_FIRMWARE(firmware)); FuSynapticsCxaudioFileKind file_kind; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 3, "park"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "init"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "invalidate"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "unpark"); /* check if a patch file fits completely into the EEPROM */ for (guint i = 0; i < records->len; i++) { FuSrecFirmwareRecord *rcd = g_ptr_array_index(records, i); if (rcd->kind == FU_FIRMWARE_SREC_RECORD_KIND_S9_TERMINATION_16) continue; if (rcd->kind == FU_FIRMWARE_SREC_RECORD_KIND_LAST) continue; if (rcd->addr > self->eeprom_sz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "EEPROM address 0x%02x is bigger than size 0x%02x", rcd->addr, self->eeprom_sz); return FALSE; } } /* park the FW: run only the basic functionality until the upgrade is over */ if (!fu_synaptics_cxaudio_device_register_set_bit( self, FU_SYNAPTICS_CXAUDIO_REG_FIRMWARE_PARK_ADDR, 7, error)) return FALSE; fu_device_sleep(device, 10); /* ms */ fu_progress_step_done(progress); /* initialize layout signature and version to 0 if transitioning from * EEPROM layout version 1 => 0 */ file_kind = fu_synaptics_cxaudio_firmware_get_file_type(FU_SYNAPTICS_CXAUDIO_FIRMWARE(firmware)); if (file_kind == FU_SYNAPTICS_CXAUDIO_FILE_KIND_CX2070X_FW && self->eeprom_layout_version >= 1 && fu_synaptics_cxaudio_firmware_get_layout_version( FU_SYNAPTICS_CXAUDIO_FIRMWARE(firmware)) == 0) { guint8 value = 0; if (!fu_synaptics_cxaudio_device_operation( self, FU_SYNAPTICS_CXAUDIO_OPERATION_WRITE, FU_SYNAPTICS_CXAUDIO_MEM_KIND_EEPROM, FU_SYNAPTICS_CXAUDIO_EEPROM_CUSTOM_INFO_OFFSET + FU_STRUCT_SYNAPTICS_CXAUDIO_CUSTOM_INFO_OFFSET_LAYOUT_SIGNATURE, &value, sizeof(value), FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_NONE, error)) { g_prefix_error(error, "failed to initialize layout signature: "); return FALSE; } if (!fu_synaptics_cxaudio_device_operation( self, FU_SYNAPTICS_CXAUDIO_OPERATION_WRITE, FU_SYNAPTICS_CXAUDIO_MEM_KIND_EEPROM, FU_SYNAPTICS_CXAUDIO_EEPROM_CUSTOM_INFO_OFFSET + FU_STRUCT_SYNAPTICS_CXAUDIO_CUSTOM_INFO_OFFSET_LAYOUT_VERSION, &value, sizeof(value), FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_NONE, error)) { g_prefix_error(error, "failed to initialize layout signature: "); return FALSE; } } fu_progress_step_done(progress); /* perform the actual write */ for (guint i = 0; i < records->len; i++) { FuSrecFirmwareRecord *rcd = g_ptr_array_index(records, i); if (rcd->kind != FU_FIRMWARE_SREC_RECORD_KIND_S3_DATA_32) continue; g_debug("writing @0x%04x len:0x%02x", rcd->addr, rcd->buf->len); if (!fu_synaptics_cxaudio_device_operation( self, FU_SYNAPTICS_CXAUDIO_OPERATION_WRITE, FU_SYNAPTICS_CXAUDIO_MEM_KIND_EEPROM, rcd->addr, rcd->buf->data, rcd->buf->len, FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_VERIFY, error)) { g_prefix_error(error, "failed to write @0x%04x len:0x%02x: ", rcd->addr, rcd->buf->len); return FALSE; } fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)i + 1, (gsize)records->len); } fu_progress_step_done(progress); /* in case of a full FW upgrade invalidate the old FW patch (if any) * as it may have not been done by the S37 file */ if (file_kind == FU_SYNAPTICS_CXAUDIO_FILE_KIND_CX2070X_FW) { guint8 buf[FU_STRUCT_SYNAPTICS_CXAUDIO_PATCH_INFO_SIZE] = {0}; g_autoptr(GByteArray) st_pat = NULL; if (!fu_synaptics_cxaudio_device_operation( self, FU_SYNAPTICS_CXAUDIO_OPERATION_READ, FU_SYNAPTICS_CXAUDIO_MEM_KIND_EEPROM, FU_SYNAPTICS_CXAUDIO_EEPROM_PATCH_INFO_OFFSET, buf, sizeof(buf), FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_NONE, error)) { g_prefix_error(error, "failed to read EEPROM patch info: "); return FALSE; } st_pat = fu_struct_synaptics_cxaudio_patch_info_parse(buf, sizeof(buf), 0x0, error); if (st_pat == NULL) return FALSE; if (fu_struct_synaptics_cxaudio_patch_info_get_patch_signature(st_pat) == FU_SYNAPTICS_CXAUDIO_SIGNATURE_PATCH_BYTE) { fu_struct_synaptics_cxaudio_patch_info_set_patch_signature(st_pat, 0x0); fu_struct_synaptics_cxaudio_patch_info_set_patch_address(st_pat, 0x0); if (!fu_synaptics_cxaudio_device_operation( self, FU_SYNAPTICS_CXAUDIO_OPERATION_WRITE, FU_SYNAPTICS_CXAUDIO_MEM_KIND_EEPROM, FU_SYNAPTICS_CXAUDIO_EEPROM_PATCH_INFO_OFFSET, st_pat->data, st_pat->len, FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_NONE, error)) { g_prefix_error(error, "failed to write empty EEPROM patch info: "); return FALSE; } g_debug("invalidated old FW patch for CX2070x (RAM) device"); } } fu_progress_step_done(progress); /* unpark the FW */ if (!fu_synaptics_cxaudio_device_register_clear_bit( self, FU_SYNAPTICS_CXAUDIO_REG_FIRMWARE_PARK_ADDR, 7, error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_synaptics_cxaudio_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuSynapticsCxaudioDevice *self = FU_SYNAPTICS_CXAUDIO_DEVICE(device); guint8 tmp = 1 << 6; g_autoptr(GError) error_local = NULL; /* is disabled on EVK board using jumper */ if (!self->sw_reset_supported) return TRUE; /* wait for re-enumeration */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* this fails on success */ if (!fu_synaptics_cxaudio_device_operation(self, FU_SYNAPTICS_CXAUDIO_OPERATION_WRITE, FU_SYNAPTICS_CXAUDIO_MEM_KIND_CPX_RAM, FU_SYNAPTICS_CXAUDIO_REG_RESET_ADDR, &tmp, sizeof(tmp), FU_SYNAPTICS_CXAUDIO_OPERATION_FLAG_NONE, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) return TRUE; g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } return TRUE; } static gboolean fu_synaptics_cxaudio_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuSynapticsCxaudioDevice *self = FU_SYNAPTICS_CXAUDIO_DEVICE(device); guint64 tmp = 0; if (g_strcmp0(key, "CxaudioChipIdBase") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->chip_id_base = tmp; return TRUE; } if (g_strcmp0(key, "CxaudioSoftwareReset") == 0) return fu_strtobool(value, &self->sw_reset_supported, error); if (g_strcmp0(key, "CxaudioPatch1ValidAddr") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->eeprom_patch_valid_addr = tmp; return TRUE; } if (g_strcmp0(key, "CxaudioPatch2ValidAddr") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->eeprom_patch2_valid_addr = tmp; return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_synaptics_cxaudio_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 3, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 37, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 60, "reload"); } static void fu_synaptics_cxaudio_device_init(FuSynapticsCxaudioDevice *self) { self->sw_reset_supported = TRUE; fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_AUDIO_CARD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_install_duration(FU_DEVICE(self), 3); /* seconds */ fu_device_add_protocol(FU_DEVICE(self), "com.synaptics.cxaudio"); fu_device_retry_set_delay(FU_DEVICE(self), 100); /* ms */ fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); } static void fu_synaptics_cxaudio_device_class_init(FuSynapticsCxaudioDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_synaptics_cxaudio_device_to_string; device_class->set_quirk_kv = fu_synaptics_cxaudio_device_set_quirk_kv; device_class->setup = fu_synaptics_cxaudio_device_setup; device_class->write_firmware = fu_synaptics_cxaudio_device_write_firmware; device_class->attach = fu_synaptics_cxaudio_device_attach; device_class->prepare_firmware = fu_synaptics_cxaudio_device_prepare_firmware; device_class->set_progress = fu_synaptics_cxaudio_device_set_progress; } fwupd-2.0.10/plugins/synaptics-cxaudio/fu-synaptics-cxaudio-device.h000066400000000000000000000005751501337203100255360ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_SYNAPTICS_CXAUDIO_DEVICE (fu_synaptics_cxaudio_device_get_type()) G_DECLARE_FINAL_TYPE(FuSynapticsCxaudioDevice, fu_synaptics_cxaudio_device, FU, SYNAPTICS_CXAUDIO_DEVICE, FuHidDevice) fwupd-2.0.10/plugins/synaptics-cxaudio/fu-synaptics-cxaudio-firmware.c000066400000000000000000000301221501337203100260750ustar00rootroot00000000000000/* * Copyright 2005 Synaptics Incorporated * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-synaptics-cxaudio-common.h" #include "fu-synaptics-cxaudio-firmware.h" #include "fu-synaptics-cxaudio-struct.h" struct _FuSynapticsCxaudioFirmware { FuSrecFirmwareClass parent_instance; FuSynapticsCxaudioFileKind file_kind; FuSynapticsCxaudioDeviceKind device_kind; guint8 layout_signature; guint8 layout_version; guint16 vendor_id; guint16 product_id; guint16 revision_id; }; G_DEFINE_TYPE(FuSynapticsCxaudioFirmware, fu_synaptics_cxaudio_firmware, FU_TYPE_SREC_FIRMWARE) FuSynapticsCxaudioFileKind fu_synaptics_cxaudio_firmware_get_file_type(FuSynapticsCxaudioFirmware *self) { g_return_val_if_fail(FU_IS_SYNAPTICS_CXAUDIO_FIRMWARE(self), 0); return self->file_kind; } FuSynapticsCxaudioDeviceKind fu_synaptics_cxaudio_firmware_get_devtype(FuSynapticsCxaudioFirmware *self) { g_return_val_if_fail(FU_IS_SYNAPTICS_CXAUDIO_FIRMWARE(self), 0); return self->device_kind; } guint8 fu_synaptics_cxaudio_firmware_get_layout_version(FuSynapticsCxaudioFirmware *self) { g_return_val_if_fail(FU_IS_SYNAPTICS_CXAUDIO_FIRMWARE(self), 0); return self->layout_version; } static void fu_synaptics_cxaudio_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuSynapticsCxaudioFirmware *self = FU_SYNAPTICS_CXAUDIO_FIRMWARE(firmware); fu_xmlb_builder_insert_kv(bn, "file_kind", fu_synaptics_cxaudio_file_kind_to_string(self->file_kind)); fu_xmlb_builder_insert_kv(bn, "device_kind", fu_synaptics_cxaudio_device_kind_to_string(self->device_kind)); fu_xmlb_builder_insert_kx(bn, "layout_signature", self->layout_signature); fu_xmlb_builder_insert_kx(bn, "layout_version", self->layout_version); if (self->layout_version >= 1) { fu_xmlb_builder_insert_kx(bn, "vid", self->vendor_id); fu_xmlb_builder_insert_kx(bn, "pid", self->product_id); fu_xmlb_builder_insert_kx(bn, "rev", self->revision_id); } } typedef struct { const gchar *str; guint32 addr; guint32 len; } FuSynapticsCxaudioFirmwareBadblock; static void fu_synaptics_cxaudio_firmware_badblock_add(GPtrArray *badblocks, const gchar *str, guint32 addr, guint32 len) { FuSynapticsCxaudioFirmwareBadblock *bb = g_new0(FuSynapticsCxaudioFirmwareBadblock, 1); g_debug("created reserved range @0x%04x len:0x%x: %s", addr, len, str); bb->str = str; bb->addr = addr; bb->len = len; g_ptr_array_add(badblocks, bb); } static gboolean fu_synaptics_cxaudio_firmware_is_addr_valid(GPtrArray *badblocks, guint32 addr, guint32 len) { for (guint j = 0; j < badblocks->len; j++) { FuSynapticsCxaudioFirmwareBadblock *bb = g_ptr_array_index(badblocks, j); if (addr <= bb->addr + bb->len - 1 && addr + len - 1 >= bb->addr) { g_debug("addr @0x%04x len:0x%x invalid " "as 0x%02x->0x%02x protected: %s", addr, len, bb->addr, bb->addr + bb->len - 1, bb->str); return FALSE; } } return TRUE; } static gboolean fu_synaptics_cxaudio_firmware_is_record_valid(GPtrArray *badblocks, FuSrecFirmwareRecord *rcd) { /* the entire record is not within an ignored range */ return fu_synaptics_cxaudio_firmware_is_addr_valid(badblocks, rcd->addr, rcd->buf->len); } static void fu_synaptics_cxaudio_firmware_avoid_badblocks(GPtrArray *badblocks, GPtrArray *records) { g_autoptr(GPtrArray) records_new = g_ptr_array_new(); /* find records that include addresses with blocks we want to avoid */ for (guint i = 0; i < records->len; i++) { FuSrecFirmwareRecord *rcd = g_ptr_array_index(records, i); FuSrecFirmwareRecord *rcd1; if (rcd->kind != FU_FIRMWARE_SREC_RECORD_KIND_S3_DATA_32) continue; if (fu_synaptics_cxaudio_firmware_is_record_valid(badblocks, rcd)) { rcd1 = fu_srec_firmware_record_new(rcd->ln, rcd->kind, rcd->addr); g_byte_array_append(rcd1->buf, rcd->buf->data, rcd->buf->len); g_ptr_array_add(records_new, rcd1); continue; } g_debug("splitting record @0x%04x len:0x%x as protected", rcd->addr, rcd->buf->len); for (guint j = 0; j < rcd->buf->len; j++) { if (!fu_synaptics_cxaudio_firmware_is_addr_valid(badblocks, rcd->addr + j, 0x1)) continue; rcd1 = fu_srec_firmware_record_new(rcd->ln, rcd->kind, rcd->addr + j); g_byte_array_append(rcd1->buf, rcd->buf->data + j, 0x1); g_ptr_array_add(records_new, rcd1); } } /* swap the old set of records with the new records */ g_ptr_array_set_size(records, 0); for (guint i = 0; i < records_new->len; i++) { FuSrecFirmwareRecord *rcd1 = g_ptr_array_index(records_new, i); g_ptr_array_add(records, rcd1); } } static gboolean fu_synaptics_cxaudio_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuSynapticsCxaudioFirmware *self = FU_SYNAPTICS_CXAUDIO_FIRMWARE(firmware); GPtrArray *records = fu_srec_firmware_get_records(FU_SREC_FIRMWARE(firmware)); guint8 dev_kind_candidate = G_MAXUINT8; g_autoptr(GByteArray) st = NULL; g_autoptr(GByteArray) st_sig = NULL; g_autoptr(GByteArray) st_pat = NULL; guint8 shadow[FU_SYNAPTICS_CXAUDIO_EEPROM_SHADOW_SIZE] = {0x0}; /* copy shadow EEPROM */ for (guint i = 0; i < records->len; i++) { FuSrecFirmwareRecord *rcd = g_ptr_array_index(records, i); if (rcd->kind != FU_FIRMWARE_SREC_RECORD_KIND_S3_DATA_32) continue; if (rcd->addr > FU_SYNAPTICS_CXAUDIO_EEPROM_SHADOW_SIZE) continue; if (rcd->buf->len == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "record 0x%x had zero size", i); return FALSE; } if (!fu_memcpy_safe(shadow, sizeof(shadow), rcd->addr, /* dst */ rcd->buf->data, rcd->buf->len, 0x0, /* src */ rcd->buf->len, error)) return FALSE; } /* parse EEPROM map */ st = fu_struct_synaptics_cxaudio_custom_info_parse( shadow, sizeof(shadow), FU_SYNAPTICS_CXAUDIO_EEPROM_CUSTOM_INFO_OFFSET, error); if (st == NULL) return FALSE; self->layout_signature = fu_struct_synaptics_cxaudio_custom_info_get_layout_signature(st); self->layout_version = fu_struct_synaptics_cxaudio_custom_info_get_layout_version(st); self->vendor_id = fu_struct_synaptics_cxaudio_custom_info_get_vendor_id(st); self->product_id = fu_struct_synaptics_cxaudio_custom_info_get_product_id(st); self->revision_id = fu_struct_synaptics_cxaudio_custom_info_get_revision_id(st); /* just layout version byte is not enough in case of old CX20562 patch * files that could have non-zero value of the Layout version */ st_sig = fu_struct_synaptics_cxaudio_validity_signature_parse( shadow, sizeof(shadow), FU_SYNAPTICS_CXAUDIO_EEPROM_VALIDITY_SIGNATURE_OFFSET, error); if (st_sig == NULL) return FALSE; st_pat = fu_struct_synaptics_cxaudio_patch_info_parse( shadow, sizeof(shadow), FU_SYNAPTICS_CXAUDIO_EEPROM_PATCH_INFO_OFFSET, error); if (st_pat == NULL) return FALSE; if (fu_struct_synaptics_cxaudio_validity_signature_get_magic_byte(st_sig) == FU_SYNAPTICS_CXAUDIO_SIGNATURE_BYTE) { self->device_kind = FU_SYNAPTICS_CXAUDIO_DEVICE_KIND_CX2070X; self->file_kind = FU_SYNAPTICS_CXAUDIO_FILE_KIND_CX2070X_FW; g_debug("FileKind: CX2070x (FW)"); } else if (fu_struct_synaptics_cxaudio_patch_info_get_patch_signature(st_pat) == FU_SYNAPTICS_CXAUDIO_SIGNATURE_PATCH_BYTE) { self->device_kind = FU_SYNAPTICS_CXAUDIO_DEVICE_KIND_CX2070X; self->file_kind = FU_SYNAPTICS_CXAUDIO_FILE_KIND_CX2070X_PATCH; g_debug("FileKind: CX2070x (Patch)"); } else { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "CX20562 is not supported"); return FALSE; } for (guint i = records->len - 3; i < records->len; i++) { FuSrecFirmwareRecord *rcd = g_ptr_array_index(records, i); if (rcd->kind == FU_FIRMWARE_SREC_RECORD_KIND_S9_TERMINATION_16) continue; if (rcd->buf->len < 2) continue; if (memcmp(rcd->buf->data, "CX", 2) == 0) { dev_kind_candidate = rcd->buf->data[2]; g_debug("DeviceKind signature suspected 0x%0x", dev_kind_candidate); break; } } /* check the signature character to see if it defines the device */ switch (dev_kind_candidate) { case '2': /* fallthrough */ /* CX2070x */ case '4': /* CX2070x-21Z */ case '6': /* CX2070x-21Z */ self->device_kind = FU_SYNAPTICS_CXAUDIO_DEVICE_KIND_CX2070X; self->file_kind = FU_SYNAPTICS_CXAUDIO_FILE_KIND_CX2070X_PATCH; g_debug("FileKind: CX2070x overwritten from signature"); break; case '3': self->device_kind = FU_SYNAPTICS_CXAUDIO_DEVICE_KIND_CX2077X; self->file_kind = FU_SYNAPTICS_CXAUDIO_FILE_KIND_CX2077X_PATCH; g_debug("FileKind: CX2077x overwritten from signature"); break; case '5': self->device_kind = FU_SYNAPTICS_CXAUDIO_DEVICE_KIND_CX2076X; self->file_kind = FU_SYNAPTICS_CXAUDIO_FILE_KIND_CX2076X_PATCH; g_debug("FileKind: CX2076x overwritten from signature"); break; case '7': self->device_kind = FU_SYNAPTICS_CXAUDIO_DEVICE_KIND_CX2085X; self->file_kind = FU_SYNAPTICS_CXAUDIO_FILE_KIND_CX2085X_PATCH; g_debug("FileKind: CX2085x overwritten from signature"); break; case '8': self->device_kind = FU_SYNAPTICS_CXAUDIO_DEVICE_KIND_CX2089X; self->file_kind = FU_SYNAPTICS_CXAUDIO_FILE_KIND_CX2089X_PATCH; g_debug("FileKind: CX2089x overwritten from signature"); break; case '9': self->device_kind = FU_SYNAPTICS_CXAUDIO_DEVICE_KIND_CX2098X; self->file_kind = FU_SYNAPTICS_CXAUDIO_FILE_KIND_CX2098X_PATCH; g_debug("FileKind: CX2098x overwritten from signature"); break; case 'A': self->device_kind = FU_SYNAPTICS_CXAUDIO_DEVICE_KIND_CX2198X; self->file_kind = FU_SYNAPTICS_CXAUDIO_FILE_KIND_CX2198X_PATCH; g_debug("FileKind: CX2198x overwritten from signature"); break; default: /* probably future devices */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "DeviceKind signature invalid 0x%x", dev_kind_candidate); return FALSE; } /* ignore records with protected content */ if (self->layout_version >= 1) { guint16 serial_number_string_address = fu_struct_synaptics_cxaudio_custom_info_get_serial_number_string_address(st); g_autoptr(GPtrArray) badblocks = g_ptr_array_new_with_free_func(g_free); /* add standard ranges to ignore */ fu_synaptics_cxaudio_firmware_badblock_add(badblocks, "test mark", 0x00BC, 0x02); fu_synaptics_cxaudio_firmware_badblock_add( badblocks, "application status", FU_SYNAPTICS_CXAUDIO_EEPROM_CUSTOM_INFO_OFFSET + FU_STRUCT_SYNAPTICS_CXAUDIO_CUSTOM_INFO_OFFSET_APPLICATION_STATUS, sizeof(guint8)); fu_synaptics_cxaudio_firmware_badblock_add( badblocks, "boot bytes", FU_SYNAPTICS_CXAUDIO_EEPROM_VALIDITY_SIGNATURE_OFFSET, FU_STRUCT_SYNAPTICS_CXAUDIO_VALIDITY_SIGNATURE_SIZE + 1); /* serial number address and also string pointer itself if set */ if (serial_number_string_address != 0x0) { guint16 addr_tmp; guint16 addr_str = 0; addr_tmp = FU_SYNAPTICS_CXAUDIO_EEPROM_CUSTOM_INFO_OFFSET + FU_STRUCT_SYNAPTICS_CXAUDIO_CUSTOM_INFO_OFFSET_SERIAL_NUMBER_STRING_ADDRESS; fu_synaptics_cxaudio_firmware_badblock_add(badblocks, "serial number", addr_tmp, sizeof(guint16)); if (!fu_memread_uint16_safe(shadow, sizeof(shadow), addr_tmp, &addr_str, G_LITTLE_ENDIAN, error)) return FALSE; fu_synaptics_cxaudio_firmware_badblock_add(badblocks, "serial number data", addr_str, shadow[addr_str]); } fu_synaptics_cxaudio_firmware_avoid_badblocks(badblocks, records); } /* success */ return TRUE; } static void fu_synaptics_cxaudio_firmware_init(FuSynapticsCxaudioFirmware *self) { } static void fu_synaptics_cxaudio_firmware_class_init(FuSynapticsCxaudioFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_synaptics_cxaudio_firmware_parse; firmware_class->export = fu_synaptics_cxaudio_firmware_export; } FuFirmware * fu_synaptics_cxaudio_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_SYNAPTICS_CXAUDIO_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/synaptics-cxaudio/fu-synaptics-cxaudio-firmware.h000066400000000000000000000015031501337203100261030ustar00rootroot00000000000000/* * Copyright 2005 Synaptics Incorporated * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-synaptics-cxaudio-struct.h" #define FU_TYPE_SYNAPTICS_CXAUDIO_FIRMWARE (fu_synaptics_cxaudio_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuSynapticsCxaudioFirmware, fu_synaptics_cxaudio_firmware, FU, SYNAPTICS_CXAUDIO_FIRMWARE, FuSrecFirmware) FuFirmware * fu_synaptics_cxaudio_firmware_new(void); FuSynapticsCxaudioFileKind fu_synaptics_cxaudio_firmware_get_file_type(FuSynapticsCxaudioFirmware *self); FuSynapticsCxaudioDeviceKind fu_synaptics_cxaudio_firmware_get_devtype(FuSynapticsCxaudioFirmware *self); guint8 fu_synaptics_cxaudio_firmware_get_layout_version(FuSynapticsCxaudioFirmware *self); fwupd-2.0.10/plugins/synaptics-cxaudio/fu-synaptics-cxaudio-plugin.c000066400000000000000000000023341501337203100255630ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-synaptics-cxaudio-device.h" #include "fu-synaptics-cxaudio-firmware.h" #include "fu-synaptics-cxaudio-plugin.h" struct _FuSynapticsCxaudioPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuSynapticsCxaudioPlugin, fu_synaptics_cxaudio_plugin, FU_TYPE_PLUGIN) static void fu_synaptics_cxaudio_plugin_init(FuSynapticsCxaudioPlugin *self) { } static void fu_synaptics_cxaudio_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "CxaudioChipIdBase"); fu_context_add_quirk_key(ctx, "CxaudioPatch1ValidAddr"); fu_context_add_quirk_key(ctx, "CxaudioPatch2ValidAddr"); fu_context_add_quirk_key(ctx, "CxaudioSoftwareReset"); fu_plugin_add_device_gtype(plugin, FU_TYPE_SYNAPTICS_CXAUDIO_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_SYNAPTICS_CXAUDIO_FIRMWARE); } static void fu_synaptics_cxaudio_plugin_class_init(FuSynapticsCxaudioPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_synaptics_cxaudio_plugin_constructed; } fwupd-2.0.10/plugins/synaptics-cxaudio/fu-synaptics-cxaudio-plugin.h000066400000000000000000000004501501337203100255650ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuSynapticsCxaudioPlugin, fu_synaptics_cxaudio_plugin, FU, SYNAPTICS_CXAUDIO_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/synaptics-cxaudio/fu-synaptics-cxaudio.rs000066400000000000000000000030131501337203100244640ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] enum FuSynapticsCxaudioDeviceKind { Unknown, Cx20562 = 20562, Cx2070x = 20700, Cx2077x = 20770, Cx2076x = 20760, Cx2085x = 20850, Cx2089x = 20890, Cx2098x = 20980, Cx2198x = 21980, } enum FuSynapticsCxaudioMemKind { Eeprom, CpxRam, CpxRom, } #[derive(ToString)] enum FuSynapticsCxaudioFileKind { Unknown, Cx2070xFw, Cx2070xPatch, Cx2077xPatch, Cx2076xPatch, Cx2085xPatch, Cx2089xPatch, Cx2098xPatch, Cx2198xPatch, } #[derive(Parse)] #[repr(C, packed)] struct FuStructSynapticsCxaudioCustomInfo { patch_version_string_address: u16le, cpx_patch_version: [u8; 3], spx_patch_version: [u8; 4], layout_signature: u8, layout_version: u8, application_status: u8, vendor_id: u16le, product_id: u16le, revision_id: u16le, language_string_address: u16le, manufacturer_string_address: u16le, product_string_address: u16le, serial_number_string_address: u16le, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructSynapticsCxaudioStringHeader { length: u8, type: u8 == 0x03, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructSynapticsCxaudioValiditySignature { magic_byte: u8 = 0x4C, // 'L' eeprom_size_code: u8, } #[derive(Parse, Setters)] #[repr(C, packed)] struct FuStructSynapticsCxaudioPatchInfo { patch_signature: u8, patch_address: u16le, } fwupd-2.0.10/plugins/synaptics-cxaudio/meson.build000066400000000000000000000011121501337203100221770ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginSynapticsCxaudio"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('synaptics-cxaudio.quirk') plugin_builtins += static_library('fu_plugin_synaptics_cxaudio', rustgen.process('fu-synaptics-cxaudio.rs'), sources: [ 'fu-synaptics-cxaudio-plugin.c', 'fu-synaptics-cxaudio-device.c', 'fu-synaptics-cxaudio-firmware.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files( 'tests/lenovo-03x7609-cxaudio.json', ) fwupd-2.0.10/plugins/synaptics-cxaudio/synaptics-cxaudio.quirk000066400000000000000000000024611501337203100245710ustar00rootroot00000000000000# ThinkPad TBT3-TR Gen 2 dock [USB\VID_17EF&PID_3083] Guid[quirk] = SYNAPTICS_CXAUDIO\ID_CX2098X ParentGuid = USB\VID_17EF&PID_307F&HUB_0006 # ThinkPad TBT3-TR Gen 2 dock UAC2.0 [USB\VID_17EF&PID_30CF] Guid[quirk] = SYNAPTICS_CXAUDIO\ID_CX2098X ParentGuid = USB\VID_17EF&PID_307F&HUB_0006 # ThinkPad TBT3-TR Gen 2 dock substitute [USB\VID_17EF&PID_30C9] Guid[quirk] = SYNAPTICS_CXAUDIO\ID_CX2198X ParentGuid = USB\VID_17EF&PID_307F&HUB_0006 # ThinkPad TBT3-MS Gen 2 dock [USB\VID_17EF&PID_3092] Guid[quirk] = SYNAPTICS_CXAUDIO\ID_CX2198X ParentGuid = USB\VID_17EF&PID_308F # ThinkPad USB-C Dock Gen2 Audio [USB\VID_17EF&PID_A396] Guid[quirk] = SYNAPTICS_CXAUDIO\ID_CX2198X ParentGuid = USB\VID_17EF&PID_A391 # ThinkPad USB-C Dock Gen2 Audio as updated in firmware version 49-0E-41 [USB\VID_17EF&PID_30D1] Guid[quirk] = SYNAPTICS_CXAUDIO\ID_CX2198X ParentGuid = USB\VID_17EF&PID_A391 # Google Pixel USB-C headphones [USB\VID_18D1&PID_5033] Guid[quirk] = SYNAPTICS_CXAUDIO\ID_CX2198X # Google Pixel USB-C <-> 3.5mm adapter [USB\VID_18D1&PID_5034] Guid[quirk] = SYNAPTICS_CXAUDIO\ID_CX2198X [SYNAPTICS_CXAUDIO\ID_CX2098X] Plugin = synaptics_cxaudio CxaudioChipIdBase = 20980 [SYNAPTICS_CXAUDIO\ID_CX2198X] Plugin = synaptics_cxaudio CxaudioChipIdBase = 21980 CxaudioPatch1ValidAddr = 0x0014 CxaudioPatch2ValidAddr = 0x0171 fwupd-2.0.10/plugins/synaptics-cxaudio/tests/000077500000000000000000000000001501337203100212045ustar00rootroot00000000000000fwupd-2.0.10/plugins/synaptics-cxaudio/tests/lenovo-03x7609-cxaudio.json000066400000000000000000000011141501337203100257660ustar00rootroot00000000000000{ "name": "Lenovo USB-C Dock Gen2 (CXAUDIO)", "interactive": false, "repeat": 2, "steps": [ { "url": "2e0bf8aaf9c63ca11cfe3444d032277c21ec0d678e5963123a8b33e5dcd37d99-Lenovo-ThinkPad-USBCGen2Dock-Firmware-49-0E-14.cab", "emulation-url": "9b6a1401bbd5ab3304a50a00dfc6d17d853bfbfd63f80c0360774d0e9e46e772-Lenovo-ThinkPad-USBCGen2Dock-Firmware-49-0E-14.zip", "components": [ { "name": "cxaudio", "version": "49-0E-14", "guids": [ "dbb8d54c-42e6-5215-b7ac-1df16872bb06" ] } ] } ] } fwupd-2.0.10/plugins/synaptics-mst/000077500000000000000000000000001501337203100172115ustar00rootroot00000000000000fwupd-2.0.10/plugins/synaptics-mst/README.md000066400000000000000000000071051501337203100204730ustar00rootroot00000000000000--- title: Plugin: Synaptics MST --- ## Introduction This plugin supports querying and flashing Synaptics MST hubs used in Dell systems and docks. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in an unspecified binary file format. This plugin supports the following protocol ID: * `com.synaptics.mst` ## GUID Generation These devices use the standard DPAUX GUID values, e.g. * `DPAUX\OUI_0090CC24` (only-quirk) * `DPAUX\OUI_0090CC24&HWREV_10` (only-quirk) * `DPAUX\OUI_0090CC24&HWREV_10&DEVID_SYNAB2` (only-quirk) * `DPAUX\OUI_0090CC24&DEVID_SYNAB2` (only-quirk) These devices also use custom GUID values, e.g. * `MST-$(board-ID)` * `MST-$(device_kind)-$(chip-ID)-$(board-ID)` * `MST-$(device_kind)-$(board-ID)` * `MST-$(device_kind)` Please refer to the plugin source for more details about how the GUID is constructed for specific hardware. ## Update Behavior The firmware is deployed when the device is in normal runtime mode, and the device will reset when the new firmware has been written. On some hardware the MST device may not enumerate if there is no monitor actually plugged in. ## Vendor ID Security The vendor ID is set from the PCI vendor, for example set to `DRM_DP_AUX_DEV:0x$(vid)` ## Quirk Use This plugin uses the following plugin-specific quirks: ### SynapticsMstDeviceKind The comma-seporated kind of device, e.g. `system` or `wd15,tb16,tb18` ### Flags:manual-restart-required The device must be restarted manually after the update has completed. ### Flags:ignore-board-id Ignore board ID firmware mismatch. ## Requirements ### (Kernel) DP Aux Interface Kernel 4.6 introduced an DRM DP Aux interface for manipulation of the registers needed to access an MST hub. This patch can be backported to earlier kernels: ## Usage Supported devices will be displayed in `# fwupdmgr get-devices` output. Here is an example output from a Dell WD15 dock: ```text Dell WD15/TB16 wired Dock Synaptics VMM3332 Guid: 653cd006-5433-57db-8632-0413af4d3fcc DeviceID: MST-1-1-0-0 Plugin: synaptics_mst Flags: allow-online Version: 3.10.002 Created: 2017-01-13 Modified: 2017-01-13 Trusted: none ``` Payloads can be flashed just like any other plugin from LVFS. ## Supported devices Not all Dell systems or accessories contain MST hubs. Here is a sample list of systems known to support them however: * Dell WD15 dock * Dell TB16 dock * Dell TB18DC * Latitude E5570 * Latitude E5470 * Latitude E5270 * Latitude E7470 * Latitude E7270 * Latitude E7450 * Latitude E7250 * Latitude E5550 * Latitude E5450 * Latitude E5250 * Latitude Rugged 5414 * Latitude Rugged 7214 * Latitude Rugged 7414 ## External Interface Access This plugin requires read/write access to `/dev/drm_dp_aux*`. ## Version Considerations This plugin has been available since fwupd version `1.3.6`. ## Data Flow ```mermaid flowchart LR subgraph MST Controller MST(Controller) SPI[(SPI)] end subgraph Kernel gpu(GPU\ndriver) end subgraph fwupd Process fwupdengine(FuEngine) mst_plugin(Synaptics MST\nPlugin) end fwupdengine-->mst_plugin mst_plugin<--/dev/drm_dp_aux-->gpu gpu<--DDC/I2C-->MST MST---SPI ``` ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Apollo Ling: @ApolloLing fwupd-2.0.10/plugins/synaptics-mst/fu-self-test.c000066400000000000000000000165251501337203100217040ustar00rootroot00000000000000/* * Copyright 2017 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fu-context-private.h" #include "fu-plugin-private.h" #include "fu-synaptics-mst-common.h" #include "fu-synaptics-mst-device.h" #include "fu-synaptics-mst-firmware.h" #include "fu-synaptics-mst-plugin.h" static void fu_test_plugin_device_added_cb(FuPlugin *plugin, FuDevice *device, gpointer user_data) { GPtrArray **devices = (GPtrArray **)user_data; g_ptr_array_add(*devices, g_object_ref(device)); } static void fu_test_add_fake_devices_from_dir(FuPlugin *plugin, const gchar *path) { const gchar *basename; gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(GDir) dir = g_dir_open(path, 0, &error); g_assert_no_error(error); g_assert_nonnull(dir); ret = fu_context_load_quirks(ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_context_load_hwinfo(ctx, progress, FU_CONTEXT_HWID_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); while ((basename = g_dir_read_name(dir)) != NULL) { g_autofree gchar *fn = g_build_filename(path, basename, NULL); g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(FuProgress) progress_local = fu_progress_new(G_STRLOC); g_autoptr(FuSynapticsMstDevice) dev = NULL; g_autoptr(GError) error_local = NULL; if (!g_str_has_prefix(basename, "drm_dp_aux")) continue; dev = g_object_new(FU_TYPE_SYNAPTICS_MST_DEVICE, "context", ctx, "physical-id", "PCI_SLOT_NAME=0000:3e:00.0", "logical-id", basename, "subsystem", "drm_dp_aux_dev", "device-file", fn, "dpcd-ieee-oui", SYNAPTICS_IEEE_OUI, NULL); fu_device_add_private_flag(FU_DEVICE(dev), FU_SYNAPTICS_MST_DEVICE_FLAG_IS_SOMEWHAT_EMULATED); g_debug("creating drm_dp_aux_dev object backed by %s", fn); locker = fu_device_locker_new(dev, &error_local); if (locker == NULL) { g_debug("%s", error_local->message); continue; } fu_plugin_device_add(plugin, FU_DEVICE(dev)); } } /* test with no Synaptics MST devices */ static void fu_plugin_synaptics_mst_none_func(void) { gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuPlugin) plugin = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_autofree gchar *filename = NULL; ret = fu_context_load_quirks(ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_context_load_hwinfo(ctx, progress, FU_CONTEXT_HWID_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); plugin = fu_plugin_new_from_gtype(fu_synaptics_mst_plugin_get_type(), ctx); g_signal_connect(FU_PLUGIN(plugin), "device-added", G_CALLBACK(fu_test_plugin_device_added_cb), &devices); ret = fu_plugin_runner_startup(plugin, progress, &error); if (!ret && g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_test_skip("Skipping tests due to unsupported configuration"); return; } g_assert_no_error(error); g_assert_true(ret); filename = g_test_build_filename(G_TEST_DIST, "tests", "no_devices", NULL); if (!g_file_test(filename, G_FILE_TEST_EXISTS)) { g_test_skip("Missing no_devices"); return; } fu_test_add_fake_devices_from_dir(plugin, filename); g_assert_cmpint(devices->len, ==, 0); } /* emulate adding/removing a Dell TB16 dock */ static void fu_plugin_synaptics_mst_tb16_func(void) { gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FuPlugin) plugin = NULL; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_autofree gchar *filename = NULL; ret = fu_context_load_quirks(ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_context_load_hwinfo(ctx, progress, FU_CONTEXT_HWID_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); plugin = fu_plugin_new_from_gtype(fu_synaptics_mst_plugin_get_type(), ctx); g_signal_connect(FU_PLUGIN(plugin), "device-added", G_CALLBACK(fu_test_plugin_device_added_cb), &devices); ret = fu_plugin_runner_startup(plugin, progress, &error); if (!ret && g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_test_skip("Skipping tests due to unsupported configuration"); return; } g_assert_no_error(error); g_assert_true(ret); filename = g_test_build_filename(G_TEST_DIST, "tests", "tb16_dock", NULL); if (!g_file_test(filename, G_FILE_TEST_EXISTS)) { g_test_skip("Missing tb16_dock"); return; } fu_test_add_fake_devices_from_dir(plugin, filename); for (guint i = 0; i < devices->len; i++) { FuDevice *device = g_ptr_array_index(devices, i); g_autofree gchar *tmp = fu_device_to_string(device); g_debug("%s", tmp); } g_assert_cmpint(devices->len, ==, 2); } static void fu_synaptics_mst_firmware_xml_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autofree gchar *csum1 = NULL; g_autofree gchar *csum2 = NULL; g_autofree gchar *xml_out = NULL; g_autofree gchar *xml_src = NULL; g_autoptr(FuFirmware) firmware1 = fu_synaptics_mst_firmware_new(); g_autoptr(FuFirmware) firmware2 = fu_synaptics_mst_firmware_new(); g_autoptr(GError) error = NULL; /* build and write */ filename = g_test_build_filename(G_TEST_DIST, "tests", "synaptics-mst.builder.xml", NULL); ret = g_file_get_contents(filename, &xml_src, NULL, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_firmware_build_from_xml(firmware1, xml_src, &error); g_assert_no_error(error); g_assert_true(ret); csum1 = fu_firmware_get_checksum(firmware1, G_CHECKSUM_SHA1, &error); g_assert_no_error(error); g_assert_cmpstr(csum1, ==, "bfcdf3e6ca6cef45543bfbb57509c92aec9a39fb"); /* ensure we can round-trip */ xml_out = fu_firmware_export_to_xml(firmware1, FU_FIRMWARE_EXPORT_FLAG_NONE, &error); g_assert_no_error(error); ret = fu_firmware_build_from_xml(firmware2, xml_out, &error); g_assert_no_error(error); g_assert_true(ret); csum2 = fu_firmware_get_checksum(firmware2, G_CHECKSUM_SHA1, &error); g_assert_cmpstr(csum1, ==, csum2); } int main(int argc, char **argv) { g_autofree gchar *testdatadir = NULL; (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); testdatadir = g_test_build_filename(G_TEST_DIST, "tests", NULL); (void)g_setenv("FWUPD_SYSFSFWDIR", testdatadir, TRUE); (void)g_setenv("FWUPD_SYSFSFWATTRIBDIR", testdatadir, TRUE); (void)g_setenv("CONFIGURATION_DIRECTORY", testdatadir, TRUE); g_assert_cmpint(g_mkdir_with_parents("/tmp/fwupd-self-test/var/lib/fwupd", 0755), ==, 0); /* tests go here */ g_test_add_func("/fwupd/plugin/synaptics_mst{none}", fu_plugin_synaptics_mst_none_func); g_test_add_func("/fwupd/plugin/synaptics_mst{tb16}", fu_plugin_synaptics_mst_tb16_func); g_test_add_func("/fwupd/plugin/synaptics_mst/firmware{xml}", fu_synaptics_mst_firmware_xml_func); return g_test_run(); } fwupd-2.0.10/plugins/synaptics-mst/fu-synaptics-mst-common.c000066400000000000000000000126021501337203100240720ustar00rootroot00000000000000/* * Copyright 2016 Mario Limonciello * Copyright 2019 Richard Hughes * Copyright 2021 Apollo Ling * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-synaptics-mst-common.h" FuSynapticsMstFamily fu_synaptics_mst_family_from_chip_id(guint16 chip_id) { if (chip_id >= 0x8000 && chip_id < 0xA000) return FU_SYNAPTICS_MST_FAMILY_CARRERA; if (chip_id >= 0x7000 && chip_id < 0x8000) return FU_SYNAPTICS_MST_FAMILY_SPYDER; if ((chip_id >= 0x6000 && chip_id < 0x7000) || (chip_id >= 0x8000 && chip_id < 0x9000)) return FU_SYNAPTICS_MST_FAMILY_CAYENNE; if (chip_id >= 0x5000 && chip_id < 0x6000) return FU_SYNAPTICS_MST_FAMILY_PANAMERA; if (chip_id >= 0x3000 && chip_id < 0x4000) return FU_SYNAPTICS_MST_FAMILY_LEAF; if (chip_id >= 0x2000 && chip_id < 0x3000) return FU_SYNAPTICS_MST_FAMILY_TESLA; return FU_SYNAPTICS_MST_FAMILY_UNKNOWN; } guint8 fu_synaptics_mst_calculate_crc8(guint8 crc, const guint8 *buf, gsize bufsz) { static const guint16 CRC8_table[] = { 0x00, 0xd5, 0x7f, 0xaa, 0xfe, 0x2b, 0x81, 0x54, 0x29, 0xfc, 0x56, 0x83, 0xd7, 0x02, 0xa8, 0x7d, 0x52, 0x87, 0x2d, 0xf8, 0xac, 0x79, 0xd3, 0x06, 0x7b, 0xae, 0x04, 0xd1, 0x85, 0x50, 0xfa, 0x2f, 0xa4, 0x71, 0xdb, 0x0e, 0x5a, 0x8f, 0x25, 0xf0, 0x8d, 0x58, 0xf2, 0x27, 0x73, 0xa6, 0x0c, 0xd9, 0xf6, 0x23, 0x89, 0x5c, 0x08, 0xdd, 0x77, 0xa2, 0xdf, 0x0a, 0xa0, 0x75, 0x21, 0xf4, 0x5e, 0x8b, 0x9d, 0x48, 0xe2, 0x37, 0x63, 0xb6, 0x1c, 0xc9, 0xb4, 0x61, 0xcb, 0x1e, 0x4a, 0x9f, 0x35, 0xe0, 0xcf, 0x1a, 0xb0, 0x65, 0x31, 0xe4, 0x4e, 0x9b, 0xe6, 0x33, 0x99, 0x4c, 0x18, 0xcd, 0x67, 0xb2, 0x39, 0xec, 0x46, 0x93, 0xc7, 0x12, 0xb8, 0x6d, 0x10, 0xc5, 0x6f, 0xba, 0xee, 0x3b, 0x91, 0x44, 0x6b, 0xbe, 0x14, 0xc1, 0x95, 0x40, 0xea, 0x3f, 0x42, 0x97, 0x3d, 0xe8, 0xbc, 0x69, 0xc3, 0x16, 0xef, 0x3a, 0x90, 0x45, 0x11, 0xc4, 0x6e, 0xbb, 0xc6, 0x13, 0xb9, 0x6c, 0x38, 0xed, 0x47, 0x92, 0xbd, 0x68, 0xc2, 0x17, 0x43, 0x96, 0x3c, 0xe9, 0x94, 0x41, 0xeb, 0x3e, 0x6a, 0xbf, 0x15, 0xc0, 0x4b, 0x9e, 0x34, 0xe1, 0xb5, 0x60, 0xca, 0x1f, 0x62, 0xb7, 0x1d, 0xc8, 0x9c, 0x49, 0xe3, 0x36, 0x19, 0xcc, 0x66, 0xb3, 0xe7, 0x32, 0x98, 0x4d, 0x30, 0xe5, 0x4f, 0x9a, 0xce, 0x1b, 0xb1, 0x64, 0x72, 0xa7, 0x0d, 0xd8, 0x8c, 0x59, 0xf3, 0x26, 0x5b, 0x8e, 0x24, 0xf1, 0xa5, 0x70, 0xda, 0x0f, 0x20, 0xf5, 0x5f, 0x8a, 0xde, 0x0b, 0xa1, 0x74, 0x09, 0xdc, 0x76, 0xa3, 0xf7, 0x22, 0x88, 0x5d, 0xd6, 0x03, 0xa9, 0x7c, 0x28, 0xfd, 0x57, 0x82, 0xff, 0x2a, 0x80, 0x55, 0x01, 0xd4, 0x7e, 0xab, 0x84, 0x51, 0xfb, 0x2e, 0x7a, 0xaf, 0x05, 0xd0, 0xad, 0x78, 0xd2, 0x07, 0x53, 0x86, 0x2c, 0xf9}; guint8 val; guint16 remainder = (guint16)crc; const guint8 *message = buf; for (guint32 byte = 0; byte < bufsz; ++byte) { val = (guint8)(message[byte] ^ remainder); remainder = CRC8_table[val]; } return (guint8)remainder; } guint16 fu_synaptics_mst_calculate_crc16(guint16 crc, const guint8 *buf, gsize bufsz) { static const guint16 CRC16_table[] = { 0x0000, 0x8005, 0x800f, 0x000a, 0x801b, 0x001e, 0x0014, 0x8011, 0x8033, 0x0036, 0x003c, 0x8039, 0x0028, 0x802d, 0x8027, 0x0022, 0x8063, 0x0066, 0x006c, 0x8069, 0x0078, 0x807d, 0x8077, 0x0072, 0x0050, 0x8055, 0x805f, 0x005a, 0x804b, 0x004e, 0x0044, 0x8041, 0x80c3, 0x00c6, 0x00cc, 0x80c9, 0x00d8, 0x80dd, 0x80d7, 0x00d2, 0x00f0, 0x80f5, 0x80ff, 0x00fa, 0x80eb, 0x00ee, 0x00e4, 0x80e1, 0x00a0, 0x80a5, 0x80af, 0x00aa, 0x80bb, 0x00be, 0x00b4, 0x80b1, 0x8093, 0x0096, 0x009c, 0x8099, 0x0088, 0x808d, 0x8087, 0x0082, 0x8183, 0x0186, 0x018c, 0x8189, 0x0198, 0x819d, 0x8197, 0x0192, 0x01b0, 0x81b5, 0x81bf, 0x01ba, 0x81ab, 0x01ae, 0x01a4, 0x81a1, 0x01e0, 0x81e5, 0x81ef, 0x01ea, 0x81fb, 0x01fe, 0x01f4, 0x81f1, 0x81d3, 0x01d6, 0x01dc, 0x81d9, 0x01c8, 0x81cd, 0x81c7, 0x01c2, 0x0140, 0x8145, 0x814f, 0x014a, 0x815b, 0x015e, 0x0154, 0x8151, 0x8173, 0x0176, 0x017c, 0x8179, 0x0168, 0x816d, 0x8167, 0x0162, 0x8123, 0x0126, 0x012c, 0x8129, 0x0138, 0x813d, 0x8137, 0x0132, 0x0110, 0x8115, 0x811f, 0x011a, 0x810b, 0x010e, 0x0104, 0x8101, 0x8303, 0x0306, 0x030c, 0x8309, 0x0318, 0x831d, 0x8317, 0x0312, 0x0330, 0x8335, 0x833f, 0x033a, 0x832b, 0x032e, 0x0324, 0x8321, 0x0360, 0x8365, 0x836f, 0x036a, 0x837b, 0x037e, 0x0374, 0x8371, 0x8353, 0x0356, 0x035c, 0x8359, 0x0348, 0x834d, 0x8347, 0x0342, 0x03c0, 0x83c5, 0x83cf, 0x03ca, 0x83db, 0x03de, 0x03d4, 0x83d1, 0x83f3, 0x03f6, 0x03fc, 0x83f9, 0x03e8, 0x83ed, 0x83e7, 0x03e2, 0x83a3, 0x03a6, 0x03ac, 0x83a9, 0x03b8, 0x83bd, 0x83b7, 0x03b2, 0x0390, 0x8395, 0x839f, 0x039a, 0x838b, 0x038e, 0x0384, 0x8381, 0x0280, 0x8285, 0x828f, 0x028a, 0x829b, 0x029e, 0x0294, 0x8291, 0x82b3, 0x02b6, 0x02bc, 0x82b9, 0x02a8, 0x82ad, 0x82a7, 0x02a2, 0x82e3, 0x02e6, 0x02ec, 0x82e9, 0x02f8, 0x82fd, 0x82f7, 0x02f2, 0x02d0, 0x82d5, 0x82df, 0x02da, 0x82cb, 0x02ce, 0x02c4, 0x82c1, 0x8243, 0x0246, 0x024c, 0x8249, 0x0258, 0x825d, 0x8257, 0x0252, 0x0270, 0x8275, 0x827f, 0x027a, 0x826b, 0x026e, 0x0264, 0x8261, 0x0220, 0x8225, 0x822f, 0x022a, 0x823b, 0x023e, 0x0234, 0x8231, 0x8213, 0x0216, 0x021c, 0x8219, 0x0208, 0x820d, 0x8207, 0x0202}; guint8 val; guint16 remainder = crc; const guint8 *message = buf; for (guint32 byte = 0; byte < bufsz; ++byte) { val = (guint8)(message[byte] ^ (remainder >> 8)); remainder = CRC16_table[val] ^ (remainder << 8); } return remainder; } fwupd-2.0.10/plugins/synaptics-mst/fu-synaptics-mst-common.h000066400000000000000000000013021501337203100240720ustar00rootroot00000000000000/* * Copyright 2016 Mario Limonciello * Copyright 2017 Peichen Huang * Copyright 2019 Richard Hughes * Copyright 2021 Apollo Ling * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-synaptics-mst-struct.h" #define SYNAPTICS_FLASH_MODE_DELAY 3 /* seconds */ #define SYNAPTICS_IEEE_OUI 0x90CC24 FuSynapticsMstFamily fu_synaptics_mst_family_from_chip_id(guint16 chip_id); guint8 fu_synaptics_mst_calculate_crc8(guint8 crc, const guint8 *buf, gsize bufsz); guint16 fu_synaptics_mst_calculate_crc16(guint16 crc, const guint8 *buf, gsize bufsz); fwupd-2.0.10/plugins/synaptics-mst/fu-synaptics-mst-device.c000066400000000000000000001520221501337203100240420ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * Copyright 2016 Mario Limonciello * Copyright 2017 Peichen Huang * Copyright 2018 Ryan Chang * Copyright 2021 Apollo Ling * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-synaptics-mst-common.h" #include "fu-synaptics-mst-device.h" #include "fu-synaptics-mst-firmware.h" #include "fu-synaptics-mst-struct.h" #define FU_SYNAPTICS_MST_ID_CTRL_SIZE 0x1000 #define SYNAPTICS_UPDATE_ENUMERATE_TRIES 3 #define BIT(n) (1 << (n)) #define FLASH_SECTOR_ERASE_4K 0x1000 #define FLASH_SECTOR_ERASE_32K 0x2000 #define FLASH_SECTOR_ERASE_64K 0x3000 #define EEPROM_TAG_OFFSET 0x1FFF0 #define EEPROM_BANK_OFFSET 0x20000 #define EEPROM_ESM_OFFSET 0x40000 #define ESM_CODE_SIZE 0x40000 #define PAYLOAD_SIZE_512K 0x80000 #define PAYLOAD_SIZE_64K 0x10000 #define MAX_RETRY_COUNTS 10 #define BLOCK_UNIT 64 #define BANKTAG_0 0 #define BANKTAG_1 1 #define REG_ESM_DISABLE 0x2000fc #define REG_QUAD_DISABLE 0x200fc0 #define REG_HDCP22_DISABLE 0x200f90 #define FLASH_SETTLE_TIME 5000 /* ms */ #define FU_SYNAPTICS_MST_DEVICE_READ_TIMEOUT 2000 /* ms */ #define CARRERA_FIRMWARE_SIZE 0x7F000 /* bytes */ #define CAYENNE_FIRMWARE_SIZE 0x50000 /* bytes */ #define PANAMERA_FIRMWARE_SIZE 0x1A000 /* bytes */ #define UNIT_SIZE 32 #define ADDR_MEMORY_CUSTOMER_ID_CAYENNE 0x9000024E #define ADDR_MEMORY_BOARD_ID_CAYENNE 0x9000024F #define ADDR_MEMORY_CUSTOMER_ID_SPYDER 0x9000020E #define ADDR_MEMORY_BOARD_ID_SPYDER 0x9000020F #define ADDR_MEMORY_CUSTOMER_ID_CARRERA 0x9000014E #define ADDR_MEMORY_BOARD_ID_CARRERA 0x9000014F #define ADDR_MEMORY_CUSTOMER_ID 0x170E #define ADDR_MEMORY_BOARD_ID 0x170F #define REG_CHIP_ID 0x507 #define REG_FIRMWARE_VERSION 0x50A #define FU_SYNAPTICS_MST_DEVICE_FLAG_IGNORE_BOARD_ID "ignore-board-id" #define FU_SYNAPTICS_MST_DEVICE_FLAG_MANUAL_RESTART_REQUIRED "manual-restart-required" struct _FuSynapticsMstDevice { FuDpauxDevice parent_instance; gchar *device_kind; guint64 write_block_size; FuSynapticsMstFamily family; guint8 active_bank; guint16 board_id; guint16 chip_id; }; G_DEFINE_TYPE(FuSynapticsMstDevice, fu_synaptics_mst_device, FU_TYPE_DPAUX_DEVICE) static void fu_synaptics_mst_device_finalize(GObject *object) { FuSynapticsMstDevice *self = FU_SYNAPTICS_MST_DEVICE(object); g_free(self->device_kind); G_OBJECT_CLASS(fu_synaptics_mst_device_parent_class)->finalize(object); } static void fu_synaptics_mst_device_udev_device_notify_cb(FuUdevDevice *udev_device, GParamSpec *pspec, gpointer user_data) { FuSynapticsMstDevice *self = FU_SYNAPTICS_MST_DEVICE(user_data); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); if (!fu_device_has_private_flag(FU_DEVICE(self), FU_SYNAPTICS_MST_DEVICE_FLAG_IS_SOMEWHAT_EMULATED)) { fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); } } static void fu_synaptics_mst_device_init(FuSynapticsMstDevice *self) { self->family = FU_SYNAPTICS_MST_FAMILY_UNKNOWN; fu_device_add_protocol(FU_DEVICE(self), "com.synaptics.mst"); fu_device_set_vendor(FU_DEVICE(self), "Synaptics"); fu_device_build_vendor_id_u16(FU_DEVICE(self), "DRM_DP_AUX_DEV", 0x06CB); fu_device_set_summary(FU_DEVICE(self), "Multi-stream transport device"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_VIDEO_DISPLAY); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_register_private_flag(FU_DEVICE(self), FU_SYNAPTICS_MST_DEVICE_FLAG_IGNORE_BOARD_ID); fu_device_register_private_flag(FU_DEVICE(self), FU_SYNAPTICS_MST_DEVICE_FLAG_MANUAL_RESTART_REQUIRED); fu_device_register_private_flag(FU_DEVICE(self), FU_SYNAPTICS_MST_DEVICE_FLAG_IS_SOMEWHAT_EMULATED); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_request_flag(FU_DEVICE(self), FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); /* this is set from ->incorporate() */ g_signal_connect(FU_UDEV_DEVICE(self), "notify::udev-device", G_CALLBACK(fu_synaptics_mst_device_udev_device_notify_cb), self); } static void fu_synaptics_mst_device_to_string(FuDevice *device, guint idt, GString *str) { FuSynapticsMstDevice *self = FU_SYNAPTICS_MST_DEVICE(device); fwupd_codec_string_append(str, idt, "DeviceKind", self->device_kind); fwupd_codec_string_append_hex(str, idt, "ActiveBank", self->active_bank); fwupd_codec_string_append_hex(str, idt, "BoardId", self->board_id); fwupd_codec_string_append_hex(str, idt, "ChipId", self->chip_id); } static gboolean fu_synaptics_mst_device_rc_to_error(FuSynapticsMstUpdcRc rc, GError **error) { gint code = FWUPD_ERROR_INTERNAL; /* yay */ if (rc == FU_SYNAPTICS_MST_UPDC_RC_SUCCESS) return TRUE; /* map */ switch (rc) { case FU_SYNAPTICS_MST_UPDC_RC_INVALID: code = FWUPD_ERROR_INVALID_DATA; break; case FU_SYNAPTICS_MST_UPDC_RC_UNSUPPORTED: code = FWUPD_ERROR_NOT_SUPPORTED; break; case FU_SYNAPTICS_MST_UPDC_RC_FAILED: code = FWUPD_ERROR_INTERNAL; break; case FU_SYNAPTICS_MST_UPDC_RC_DISABLED: code = FWUPD_ERROR_NOT_FOUND; break; case FU_SYNAPTICS_MST_UPDC_RC_CONFIGURE_SIGN_FAILED: case FU_SYNAPTICS_MST_UPDC_RC_FIRMWARE_SIGN_FAILED: case FU_SYNAPTICS_MST_UPDC_RC_ROLLBACK_FAILED: code = FWUPD_ERROR_INVALID_DATA; break; default: break; } g_set_error(error, FWUPD_ERROR, code, "%s [%u]", fu_synaptics_mst_updc_rc_to_string(rc), rc); return FALSE; } typedef struct { FuSynapticsMstUpdcRc rc; FuSynapticsMstUpdcCmd rc_cmd; } FuSynapticsMstUpdcRcHelper; static gboolean fu_synaptics_mst_device_rc_send_command_and_wait_cb(FuDevice *device, gpointer user_data, GError **error) { FuSynapticsMstDevice *self = FU_SYNAPTICS_MST_DEVICE(device); FuSynapticsMstUpdcRcHelper *helper = (FuSynapticsMstUpdcRcHelper *)user_data; guint8 buf[2] = {0}; if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), FU_SYNAPTICS_MST_REG_RC_CMD, buf, sizeof(buf), FU_SYNAPTICS_MST_DEVICE_READ_TIMEOUT, error)) { g_prefix_error(error, "failed to read command: "); return FALSE; } if (buf[0] & 0x80) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "command in active state"); return FALSE; } /* success */ helper->rc = buf[1]; return TRUE; } static gboolean fu_synaptics_mst_device_rc_send_command_and_wait(FuSynapticsMstDevice *self, FuSynapticsMstUpdcCmd rc_cmd, GError **error) { guint8 buf[1] = {rc_cmd | 0x80}; FuSynapticsMstUpdcRcHelper helper = { .rc = FU_SYNAPTICS_MST_UPDC_RC_SUCCESS, .rc_cmd = rc_cmd, }; if (!fu_dpaux_device_write(FU_DPAUX_DEVICE(self), FU_SYNAPTICS_MST_REG_RC_CMD, buf, sizeof(buf), FU_SYNAPTICS_MST_DEVICE_READ_TIMEOUT, error)) { g_prefix_error(error, "failed to write command: "); return FALSE; } /* wait command complete */ if (!fu_device_retry_full(FU_DEVICE(self), fu_synaptics_mst_device_rc_send_command_and_wait_cb, 30, 100, /* ms */ &helper, error)) { g_prefix_error(error, "remote command failed: "); return FALSE; } if (helper.rc != FU_SYNAPTICS_MST_UPDC_RC_SUCCESS) { if (!fu_synaptics_mst_device_rc_to_error(helper.rc, error)) { g_prefix_error(error, "remote command failed: "); return FALSE; } } return TRUE; } static gboolean fu_synaptics_mst_device_rc_set_command(FuSynapticsMstDevice *self, FuSynapticsMstUpdcCmd rc_cmd, guint32 offset, const guint8 *buf, gsize bufsz, GError **error) { g_autoptr(GPtrArray) chunks = fu_chunk_array_new(buf, bufsz, offset, 0x0, UNIT_SIZE); /* just sent command */ if (chunks->len == 0) { g_debug("no data, just sending command %s [0x%x]", fu_synaptics_mst_updc_cmd_to_string(rc_cmd), rc_cmd); return fu_synaptics_mst_device_rc_send_command_and_wait(self, rc_cmd, error); } /* read each chunk */ for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); guint8 buf2[4] = {0}; g_debug("writing chunk of 0x%x bytes at offset 0x%x", (guint)fu_chunk_get_data_sz(chk), (guint)fu_chunk_get_address(chk)); /* write data */ if (!fu_dpaux_device_write(FU_DPAUX_DEVICE(self), FU_SYNAPTICS_MST_REG_RC_DATA, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), FU_SYNAPTICS_MST_DEVICE_READ_TIMEOUT, error)) { g_prefix_error(error, "failure writing data register: "); return FALSE; } /* write offset */ fu_memwrite_uint32(buf2, fu_chunk_get_address(chk), G_LITTLE_ENDIAN); if (!fu_dpaux_device_write(FU_DPAUX_DEVICE(self), FU_SYNAPTICS_MST_REG_RC_OFFSET, buf2, sizeof(buf2), FU_SYNAPTICS_MST_DEVICE_READ_TIMEOUT, error)) { g_prefix_error(error, "failure writing offset register: "); return FALSE; } /* write length */ fu_memwrite_uint32(buf2, fu_chunk_get_data_sz(chk), G_LITTLE_ENDIAN); if (!fu_dpaux_device_write(FU_DPAUX_DEVICE(self), FU_SYNAPTICS_MST_REG_RC_LEN, buf2, sizeof(buf2), FU_SYNAPTICS_MST_DEVICE_READ_TIMEOUT, error)) { g_prefix_error(error, "failure writing length register: "); return FALSE; } /* send command */ g_debug("data, sending command %s [0x%x]", fu_synaptics_mst_updc_cmd_to_string(rc_cmd), rc_cmd); if (!fu_synaptics_mst_device_rc_send_command_and_wait(self, rc_cmd, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_synaptics_mst_device_rc_get_command(FuSynapticsMstDevice *self, FuSynapticsMstUpdcCmd rc_cmd, guint32 offset, guint8 *buf, gsize bufsz, GError **error) { g_autoptr(GPtrArray) chunks = fu_chunk_array_mutable_new(buf, bufsz, offset, 0x0, UNIT_SIZE); /* just sent command */ if (chunks->len == 0) { g_debug("no data, just sending command %s [0x%x]", fu_synaptics_mst_updc_cmd_to_string(rc_cmd), rc_cmd); return fu_synaptics_mst_device_rc_send_command_and_wait(self, rc_cmd, error); } /* read each chunk */ for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); guint8 buf2[4] = {0}; g_debug("reading chunk of 0x%x bytes at offset 0x%x", (guint)fu_chunk_get_data_sz(chk), (guint)fu_chunk_get_address(chk)); /* write offset */ fu_memwrite_uint32(buf2, fu_chunk_get_address(chk), G_LITTLE_ENDIAN); if (!fu_dpaux_device_write(FU_DPAUX_DEVICE(self), FU_SYNAPTICS_MST_REG_RC_OFFSET, buf2, sizeof(buf2), FU_SYNAPTICS_MST_DEVICE_READ_TIMEOUT, error)) { g_prefix_error(error, "failed to write offset: "); return FALSE; } /* write length */ fu_memwrite_uint32(buf2, fu_chunk_get_data_sz(chk), G_LITTLE_ENDIAN); if (!fu_dpaux_device_write(FU_DPAUX_DEVICE(self), FU_SYNAPTICS_MST_REG_RC_LEN, buf2, sizeof(buf2), FU_SYNAPTICS_MST_DEVICE_READ_TIMEOUT, error)) { g_prefix_error(error, "failed to write length: "); return FALSE; } /* send command */ g_debug("data, sending command %s [0x%x]", fu_synaptics_mst_updc_cmd_to_string(rc_cmd), rc_cmd); if (!fu_synaptics_mst_device_rc_send_command_and_wait(self, rc_cmd, error)) return FALSE; /* read data */ if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), FU_SYNAPTICS_MST_REG_RC_DATA, fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), FU_SYNAPTICS_MST_DEVICE_READ_TIMEOUT, error)) { g_prefix_error(error, "failed to read data: "); return FALSE; } } /* success */ return TRUE; } static gboolean fu_synaptics_mst_device_disable_rc(FuSynapticsMstDevice *self, GError **error) { g_autoptr(GError) error_local = NULL; /* in test mode */ if (fu_device_has_private_flag(FU_DEVICE(self), FU_SYNAPTICS_MST_DEVICE_FLAG_IS_SOMEWHAT_EMULATED)) return TRUE; if (!fu_synaptics_mst_device_rc_set_command(self, FU_SYNAPTICS_MST_UPDC_CMD_DISABLE_RC, 0, NULL, 0, &error_local)) { /* ignore disabled */ if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) return TRUE; g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to disable remote control: "); return FALSE; } return TRUE; } static gboolean fu_synaptics_mst_device_enable_rc(FuSynapticsMstDevice *self, GError **error) { const gchar *sc = "PRIUS"; /* in test mode */ if (fu_device_has_private_flag(FU_DEVICE(self), FU_SYNAPTICS_MST_DEVICE_FLAG_IS_SOMEWHAT_EMULATED)) return TRUE; if (!fu_synaptics_mst_device_disable_rc(self, error)) { g_prefix_error(error, "failed to disable-to-enable: "); return FALSE; } if (!fu_synaptics_mst_device_rc_set_command(self, FU_SYNAPTICS_MST_UPDC_CMD_ENABLE_RC, 0, (guint8 *)sc, strlen(sc), error)) { g_prefix_error(error, "failed to enable remote control: "); return FALSE; } return TRUE; } static gboolean fu_synaptics_mst_device_rc_special_get_command(FuSynapticsMstDevice *self, FuSynapticsMstUpdcCmd rc_cmd, guint32 cmd_offset, guint8 *cmd_data, gsize cmd_datasz, guint8 *buf, gsize bufsz, GError **error) { if (cmd_datasz > 0) { guint8 buf2[4] = {0}; /* write cmd data */ if (cmd_data != NULL) { if (!fu_dpaux_device_write(FU_DPAUX_DEVICE(self), FU_SYNAPTICS_MST_REG_RC_DATA, cmd_data, cmd_datasz, FU_SYNAPTICS_MST_DEVICE_READ_TIMEOUT, error)) { g_prefix_error(error, "Failed to write command data: "); return FALSE; } } /* write offset */ fu_memwrite_uint32(buf2, cmd_offset, G_LITTLE_ENDIAN); if (!fu_dpaux_device_write(FU_DPAUX_DEVICE(self), FU_SYNAPTICS_MST_REG_RC_OFFSET, buf2, sizeof(buf2), FU_SYNAPTICS_MST_DEVICE_READ_TIMEOUT, error)) { g_prefix_error(error, "failed to write offset: "); return FALSE; } /* write length */ fu_memwrite_uint32(buf2, cmd_datasz, G_LITTLE_ENDIAN); if (!fu_dpaux_device_write(FU_DPAUX_DEVICE(self), FU_SYNAPTICS_MST_REG_RC_LEN, buf2, sizeof(buf2), FU_SYNAPTICS_MST_DEVICE_READ_TIMEOUT, error)) { g_prefix_error(error, "failed to write length: "); return FALSE; } } /* send command */ g_debug("sending command 0x%x", rc_cmd); if (!fu_synaptics_mst_device_rc_send_command_and_wait(self, rc_cmd, error)) return FALSE; if (bufsz > 0) { if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), FU_SYNAPTICS_MST_REG_RC_DATA, buf, bufsz, FU_SYNAPTICS_MST_DEVICE_READ_TIMEOUT, error)) { g_prefix_error(error, "failed to read length: "); return FALSE; } } /* success */ return TRUE; } static gboolean fu_synaptics_mst_device_get_flash_checksum(FuSynapticsMstDevice *self, guint32 offset, guint32 length, guint32 *checksum, GError **error) { guint8 buf[4] = {0}; g_return_val_if_fail(length > 0, FALSE); if (!fu_synaptics_mst_device_rc_special_get_command( self, FU_SYNAPTICS_MST_UPDC_CMD_CAL_EEPROM_CHECKSUM, offset, NULL, length, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to get flash checksum: "); return FALSE; } *checksum = fu_memread_uint32(buf, G_LITTLE_ENDIAN); return TRUE; } static gboolean fu_synaptics_mst_device_set_flash_sector_erase(FuSynapticsMstDevice *self, guint16 rc_cmd, guint16 offset, GError **error) { guint8 buf[2] = {0}; /* Need to add Wp control ? */ fu_memwrite_uint16(buf, rc_cmd + offset, G_LITTLE_ENDIAN); if (!fu_synaptics_mst_device_rc_set_command(self, FU_SYNAPTICS_MST_UPDC_CMD_FLASH_ERASE, 0, buf, sizeof(buf), error)) { g_prefix_error(error, "can't sector erase flash at offset %x: ", offset); return FALSE; } return TRUE; } typedef struct { GBytes *fw; FuChunkArray *chunks; FuProgress *progress; guint8 bank_to_update; guint32 checksum; } FuSynapticsMstDeviceHelper; static void fu_synaptics_mst_device_helper_free(FuSynapticsMstDeviceHelper *helper) { if (helper->chunks != NULL) g_object_unref(helper->chunks); if (helper->fw != NULL) g_bytes_unref(helper->fw); if (helper->progress != NULL) g_object_unref(helper->progress); g_free(helper); } static FuSynapticsMstDeviceHelper * fu_synaptics_mst_device_helper_new(void) { FuSynapticsMstDeviceHelper *helper = g_new0(FuSynapticsMstDeviceHelper, 1); helper->bank_to_update = BANKTAG_1; return helper; } G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuSynapticsMstDeviceHelper, fu_synaptics_mst_device_helper_free) static gboolean fu_synaptics_mst_device_update_esm_cb(FuDevice *device, gpointer user_data, GError **error) { FuSynapticsMstDevice *self = FU_SYNAPTICS_MST_DEVICE(device); FuSynapticsMstDeviceHelper *helper = (FuSynapticsMstDeviceHelper *)user_data; guint32 flash_checksum = 0; /* erase ESM firmware; erase failure is fatal */ for (guint32 j = 0; j < 4; j++) { if (!fu_synaptics_mst_device_set_flash_sector_erase(self, FLASH_SECTOR_ERASE_64K, j + 4, error)) { g_prefix_error(error, "failed to erase sector %u: ", j); return FALSE; } } g_debug("waiting for flash clear to settle"); fu_device_sleep(FU_DEVICE(self), FLASH_SETTLE_TIME); /* write firmware */ fu_progress_set_id(helper->progress, G_STRLOC); fu_progress_set_steps(helper->progress, fu_chunk_array_length(helper->chunks)); for (guint i = 0; i < fu_chunk_array_length(helper->chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GError) error_local = NULL; /* prepare chunk */ chk = fu_chunk_array_index(helper->chunks, i, error); if (chk == NULL) return FALSE; if (!fu_synaptics_mst_device_rc_set_command( self, FU_SYNAPTICS_MST_UPDC_CMD_WRITE_TO_EEPROM, fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), &error_local)) { g_warning("failed to write ESM: %s", error_local->message); break; } fu_progress_step_done(helper->progress); } /* check ESM checksum */ if (!fu_synaptics_mst_device_get_flash_checksum(self, EEPROM_ESM_OFFSET, ESM_CODE_SIZE, &flash_checksum, error)) return FALSE; /* ESM update done */ if (helper->checksum != flash_checksum) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "checksum 0x%x mismatched 0x%x", flash_checksum, helper->checksum); return FALSE; } /* success */ return TRUE; } static gboolean fu_synaptics_mst_device_update_esm(FuSynapticsMstDevice *self, GBytes *fw, FuProgress *progress, GError **error) { guint32 flash_checksum = 0; g_autoptr(FuSynapticsMstDeviceHelper) helper = fu_synaptics_mst_device_helper_new(); /* ESM checksum same */ helper->fw = fu_bytes_new_offset(fw, EEPROM_ESM_OFFSET, ESM_CODE_SIZE, error); if (helper->fw == NULL) return FALSE; helper->checksum = fu_sum32_bytes(helper->fw); if (!fu_synaptics_mst_device_get_flash_checksum(self, EEPROM_ESM_OFFSET, ESM_CODE_SIZE, &flash_checksum, error)) { return FALSE; } if (helper->checksum == flash_checksum) { g_debug("ESM checksum already matches"); return TRUE; } g_debug("ESM checksum %x doesn't match expected %x", flash_checksum, helper->checksum); helper->progress = g_object_ref(progress); helper->chunks = fu_chunk_array_new_from_bytes(helper->fw, EEPROM_ESM_OFFSET, FU_CHUNK_PAGESZ_NONE, BLOCK_UNIT); return fu_device_retry(FU_DEVICE(self), fu_synaptics_mst_device_update_esm_cb, MAX_RETRY_COUNTS, helper, error); } static gboolean fu_synaptics_mst_device_update_tesla_leaf_firmware_cb(FuDevice *device, gpointer user_data, GError **error) { FuSynapticsMstDevice *self = FU_SYNAPTICS_MST_DEVICE(device); FuSynapticsMstDeviceHelper *helper = (FuSynapticsMstDeviceHelper *)user_data; guint32 flash_checksum = 0; if (!fu_synaptics_mst_device_set_flash_sector_erase(self, 0xffff, 0, error)) return FALSE; g_debug("waiting for flash clear to settle"); fu_device_sleep(FU_DEVICE(self), FLASH_SETTLE_TIME); fu_progress_set_steps(helper->progress, fu_chunk_array_length(helper->chunks)); for (guint i = 0; i < fu_chunk_array_length(helper->chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GError) error_local = NULL; /* prepare chunk */ chk = fu_chunk_array_index(helper->chunks, i, error); if (chk == NULL) return FALSE; if (!fu_synaptics_mst_device_rc_set_command( self, FU_SYNAPTICS_MST_UPDC_CMD_WRITE_TO_EEPROM, fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), &error_local)) { g_warning("failed to write flash offset 0x%04x: %s, retrying", (guint)fu_chunk_get_address(chk), error_local->message); /* repeat once */ if (!fu_synaptics_mst_device_rc_set_command( self, FU_SYNAPTICS_MST_UPDC_CMD_WRITE_TO_EEPROM, fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "can't write flash offset 0x%04x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } } fu_progress_step_done(helper->progress); } /* check data just written */ if (!fu_synaptics_mst_device_get_flash_checksum(self, 0, g_bytes_get_size(helper->fw), &flash_checksum, error)) return FALSE; if (helper->checksum != flash_checksum) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "checksum 0x%x mismatched 0x%x", flash_checksum, helper->checksum); return FALSE; } /* success */ return TRUE; } static gboolean fu_synaptics_mst_device_update_tesla_leaf_firmware(FuSynapticsMstDevice *self, GBytes *fw, FuProgress *progress, GError **error) { g_autoptr(FuSynapticsMstDeviceHelper) helper = fu_synaptics_mst_device_helper_new(); helper->fw = g_bytes_ref(fw); helper->checksum = fu_sum32_bytes(fw); helper->progress = g_object_ref(progress); helper->chunks = fu_chunk_array_new_from_bytes(fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, BLOCK_UNIT); return fu_device_retry(FU_DEVICE(self), fu_synaptics_mst_device_update_tesla_leaf_firmware_cb, MAX_RETRY_COUNTS, helper, error); } static gboolean fu_synaptics_mst_device_get_active_bank_panamera(FuSynapticsMstDevice *self, GError **error) { guint32 buf[16] = {0}; /* get used bank */ if (!fu_synaptics_mst_device_rc_get_command(self, FU_SYNAPTICS_MST_UPDC_CMD_READ_FROM_MEMORY, (gint)0x20010c, (guint8 *)buf, ((sizeof(buf) / sizeof(buf[0])) * 4), error)) { g_prefix_error(error, "get active bank failed: "); return FALSE; } if ((buf[0] & BIT(7)) || (buf[0] & BIT(30))) self->active_bank = BANKTAG_1; else self->active_bank = BANKTAG_0; return TRUE; } static gboolean fu_synaptics_mst_device_update_panamera_firmware_cb(FuDevice *device, gpointer user_data, GError **error) { FuSynapticsMstDevice *self = FU_SYNAPTICS_MST_DEVICE(device); FuSynapticsMstDeviceHelper *helper = (FuSynapticsMstDeviceHelper *)user_data; guint32 erase_offset = helper->bank_to_update * 2; guint32 flash_checksum; /* erase storage */ if (!fu_synaptics_mst_device_set_flash_sector_erase(self, FLASH_SECTOR_ERASE_64K, erase_offset++, error)) return FALSE; if (!fu_synaptics_mst_device_set_flash_sector_erase(self, FLASH_SECTOR_ERASE_64K, erase_offset, error)) return FALSE; g_debug("waiting for flash clear to settle"); fu_device_sleep(FU_DEVICE(self), FLASH_SETTLE_TIME); /* write */ fu_progress_set_steps(helper->progress, fu_chunk_array_length(helper->chunks)); for (guint i = 0; i < fu_chunk_array_length(helper->chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GError) error_local = NULL; /* prepare chunk */ chk = fu_chunk_array_index(helper->chunks, i, error); if (chk == NULL) return FALSE; if (!fu_synaptics_mst_device_rc_set_command( self, FU_SYNAPTICS_MST_UPDC_CMD_WRITE_TO_EEPROM, fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), &error_local)) { g_warning("write failed: %s, retrying", error_local->message); /* repeat once */ if (!fu_synaptics_mst_device_rc_set_command( self, FU_SYNAPTICS_MST_UPDC_CMD_WRITE_TO_EEPROM, fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "firmware write failed: "); return FALSE; } } fu_progress_step_done(helper->progress); } /* verify CRC */ for (guint32 i = 0; i < 4; i++) { guint8 buf[4] = {0}; fu_device_sleep(FU_DEVICE(self), 1); /* wait crc calculation */ if (!fu_synaptics_mst_device_rc_special_get_command( self, FU_SYNAPTICS_MST_UPDC_CMD_CAL_EEPROM_CHECK_CRC16, (EEPROM_BANK_OFFSET * helper->bank_to_update), NULL, g_bytes_get_size(helper->fw), buf, sizeof(buf), error)) { g_prefix_error(error, "failed to get flash checksum: "); return FALSE; } flash_checksum = fu_memread_uint32(buf, G_LITTLE_ENDIAN); } if (helper->checksum != flash_checksum) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "checksum 0x%x mismatched 0x%x", flash_checksum, helper->checksum); return FALSE; } /* success */ return TRUE; } static gboolean fu_synaptics_mst_device_update_panamera_set_new_valid_cb(FuDevice *device, gpointer user_data, GError **error) { FuSynapticsMstDevice *self = FU_SYNAPTICS_MST_DEVICE(device); FuSynapticsMstDeviceHelper *helper = (FuSynapticsMstDeviceHelper *)user_data; guint8 buf[16] = {0x0}; guint8 buf_verify[16] = {0x0}; g_autoptr(GDateTime) dt = g_date_time_new_now_utc(); buf[0] = helper->bank_to_update; buf[1] = g_date_time_get_month(dt); buf[2] = g_date_time_get_day_of_month(dt); buf[3] = g_date_time_get_year(dt) - 2000; buf[4] = (helper->checksum >> 8) & 0xff; buf[5] = helper->checksum & 0xff; buf[15] = fu_synaptics_mst_calculate_crc8(0, buf, sizeof(buf) - 1); g_debug("tag date %x %x %x crc %x %x %x %x", buf[1], buf[2], buf[3], buf[0], buf[4], buf[5], buf[15]); if (!fu_synaptics_mst_device_rc_set_command( self, FU_SYNAPTICS_MST_UPDC_CMD_WRITE_TO_EEPROM, (EEPROM_BANK_OFFSET * helper->bank_to_update + EEPROM_TAG_OFFSET), buf, sizeof(buf), error)) { g_prefix_error(error, "failed to write tag: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), 1); /* ms */ if (!fu_synaptics_mst_device_rc_get_command( self, FU_SYNAPTICS_MST_UPDC_CMD_READ_FROM_EEPROM, (EEPROM_BANK_OFFSET * helper->bank_to_update + EEPROM_TAG_OFFSET), buf_verify, sizeof(buf_verify), error)) { g_prefix_error(error, "failed to read tag: "); return FALSE; } if (memcmp(buf, buf_verify, sizeof(buf)) != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "set tag valid fail"); return FALSE; } /* success */ return TRUE; } static gboolean fu_synaptics_mst_device_update_panamera_set_old_invalid_cb(FuDevice *device, gpointer user_data, GError **error) { FuSynapticsMstDevice *self = FU_SYNAPTICS_MST_DEVICE(device); FuSynapticsMstDeviceHelper *helper = (FuSynapticsMstDeviceHelper *)user_data; guint8 checksum_tmp = 0; guint8 checksum_nul = 0; /* CRC8 is not 0xff, erase last 4k of bank# */ if (helper->checksum != 0xff) { guint32 erase_offset = (EEPROM_BANK_OFFSET * self->active_bank + EEPROM_BANK_OFFSET - 0x1000) / 0x1000; g_debug("erasing offset 0x%x", erase_offset); if (!fu_synaptics_mst_device_set_flash_sector_erase(self, FLASH_SECTOR_ERASE_4K, erase_offset, error)) return FALSE; } /* set CRC8 to 0x00 */ if (!fu_synaptics_mst_device_rc_set_command( self, FU_SYNAPTICS_MST_UPDC_CMD_WRITE_TO_EEPROM, (EEPROM_BANK_OFFSET * self->active_bank + EEPROM_TAG_OFFSET + 15), &checksum_nul, sizeof(checksum_nul), error)) { g_prefix_error(error, "failed to clear CRC: "); return FALSE; } if (!fu_synaptics_mst_device_rc_get_command( self, FU_SYNAPTICS_MST_UPDC_CMD_READ_FROM_EEPROM, (EEPROM_BANK_OFFSET * self->active_bank + EEPROM_TAG_OFFSET + 15), &checksum_tmp, sizeof(checksum_tmp), error)) { g_prefix_error(error, "failed to read CRC from flash: "); return FALSE; } if (checksum_tmp != checksum_nul) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "set tag invalid fail, got 0x%x and expected 0x%x", checksum_tmp, checksum_nul); return FALSE; } /* success */ return TRUE; } static gboolean fu_synaptics_mst_device_update_panamera_firmware(FuSynapticsMstDevice *self, GBytes *fw, FuProgress *progress, GError **error) { guint32 fw_size = 0; guint8 checksum8 = 0; g_autoptr(FuSynapticsMstDeviceHelper) helper = fu_synaptics_mst_device_helper_new(); /* get used bank */ if (!fu_synaptics_mst_device_get_active_bank_panamera(self, error)) return FALSE; if (self->active_bank == BANKTAG_1) helper->bank_to_update = BANKTAG_0; g_debug("bank to update:%x", helper->bank_to_update); /* get firmware size */ if (!fu_memread_uint32_safe(g_bytes_get_data(fw, NULL), g_bytes_get_size(fw), 0x400, &fw_size, G_BIG_ENDIAN, error)) return FALSE; fw_size += 0x410; if (fw_size > PANAMERA_FIRMWARE_SIZE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid firmware size 0x%x", fw_size); return FALSE; } /* current max firmware size is 104K */ if (fw_size < g_bytes_get_size(fw)) fw_size = PANAMERA_FIRMWARE_SIZE; g_debug("calculated fw size as %u", fw_size); helper->fw = fu_bytes_new_offset(fw, 0x0, fw_size, error); if (helper->fw == NULL) return FALSE; helper->checksum = fu_synaptics_mst_calculate_crc16(0, g_bytes_get_data(helper->fw, NULL), g_bytes_get_size(helper->fw)); helper->progress = g_object_ref(progress); helper->chunks = fu_chunk_array_new_from_bytes(helper->fw, EEPROM_BANK_OFFSET * helper->bank_to_update, FU_CHUNK_PAGESZ_NONE, BLOCK_UNIT); if (!fu_device_retry_full(FU_DEVICE(self), fu_synaptics_mst_device_update_panamera_firmware_cb, MAX_RETRY_COUNTS, 2, /* ms */ helper, error)) return FALSE; /* set bank_to_update tag valid */ if (!fu_device_retry(FU_DEVICE(self), fu_synaptics_mst_device_update_panamera_set_new_valid_cb, MAX_RETRY_COUNTS, helper, error)) return FALSE; /* set active_bank tag invalid */ if (!fu_synaptics_mst_device_rc_get_command( self, FU_SYNAPTICS_MST_UPDC_CMD_READ_FROM_EEPROM, (EEPROM_BANK_OFFSET * self->active_bank + EEPROM_TAG_OFFSET + 15), &checksum8, sizeof(checksum8), error)) { g_prefix_error(error, "failed to read tag from flash: "); return FALSE; } helper->checksum = checksum8; return fu_device_retry(FU_DEVICE(self), fu_synaptics_mst_device_update_panamera_set_old_invalid_cb, MAX_RETRY_COUNTS, helper, error); } static gboolean fu_synaptics_mst_device_panamera_prepare_write(FuSynapticsMstDevice *self, GError **error) { guint32 buf[4] = {0}; /* Need to detect flash mode and ESM first ? */ /* disable flash Quad mode and ESM/HDCP2.2*/ /* disable ESM first */ buf[0] = 0x21; if (!fu_synaptics_mst_device_rc_set_command(self, FU_SYNAPTICS_MST_UPDC_CMD_WRITE_TO_MEMORY, (gint)REG_ESM_DISABLE, (guint8 *)buf, 4, error)) { g_prefix_error(error, "ESM disable failed: "); return FALSE; } /* wait for ESM exit */ fu_device_sleep(FU_DEVICE(self), 1); /* ms */ /* disable QUAD mode */ if (!fu_synaptics_mst_device_rc_get_command(self, FU_SYNAPTICS_MST_UPDC_CMD_READ_FROM_MEMORY, (gint)REG_QUAD_DISABLE, (guint8 *)buf, ((sizeof(buf) / sizeof(buf[0])) * 4), error)) { g_prefix_error(error, "quad query failed: "); return FALSE; } buf[0] = 0x00; if (!fu_synaptics_mst_device_rc_set_command(self, FU_SYNAPTICS_MST_UPDC_CMD_WRITE_TO_MEMORY, (gint)REG_QUAD_DISABLE, (guint8 *)buf, 4, error)) { g_prefix_error(error, "quad disable failed: "); return FALSE; } /* disable HDCP2.2 */ if (!fu_synaptics_mst_device_rc_get_command(self, FU_SYNAPTICS_MST_UPDC_CMD_READ_FROM_MEMORY, (gint)REG_HDCP22_DISABLE, (guint8 *)buf, 4, error)) { g_prefix_error(error, "HDCP query failed: "); return FALSE; } buf[0] = buf[0] & (~BIT(2)); if (!fu_synaptics_mst_device_rc_set_command(self, FU_SYNAPTICS_MST_UPDC_CMD_WRITE_TO_MEMORY, (gint)REG_HDCP22_DISABLE, (guint8 *)buf, 4, error)) { g_prefix_error(error, "HDCP disable failed: "); return FALSE; } return TRUE; } static gboolean fu_synaptics_mst_device_update_firmware_cb(FuDevice *device, gpointer user_data, GError **error) { FuSynapticsMstDevice *self = FU_SYNAPTICS_MST_DEVICE(device); FuSynapticsMstDeviceHelper *helper = (FuSynapticsMstDeviceHelper *)user_data; guint32 flash_checksum; guint8 buf[4] = {0}; if (!fu_synaptics_mst_device_set_flash_sector_erase(self, 0xffff, 0, error)) return FALSE; g_debug("waiting for flash clear to settle"); fu_device_sleep(FU_DEVICE(self), FLASH_SETTLE_TIME); fu_progress_set_steps(helper->progress, fu_chunk_array_length(helper->chunks)); for (guint i = 0; i < fu_chunk_array_length(helper->chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GError) error_local = NULL; /* prepare chunk */ chk = fu_chunk_array_index(helper->chunks, i, error); if (chk == NULL) return FALSE; if (!fu_synaptics_mst_device_rc_set_command( self, FU_SYNAPTICS_MST_UPDC_CMD_WRITE_TO_EEPROM, fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), &error_local)) { g_warning("failed to write flash offset 0x%04x: %s, retrying", (guint)fu_chunk_get_address(chk), error_local->message); /* repeat once */ if (!fu_synaptics_mst_device_rc_set_command( self, FU_SYNAPTICS_MST_UPDC_CMD_WRITE_TO_EEPROM, fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "can't write flash offset 0x%04x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } } fu_progress_step_done(helper->progress); } /* no need to check CRC here, it will be done in Activate Firmware command */ if (self->family == FU_SYNAPTICS_MST_FAMILY_CARRERA) return TRUE; /* verify CRC */ if (!fu_synaptics_mst_device_rc_special_get_command( self, FU_SYNAPTICS_MST_UPDC_CMD_CAL_EEPROM_CHECK_CRC16, 0, NULL, g_bytes_get_size(helper->fw), buf, sizeof(buf), error)) { g_prefix_error(error, "Failed to get flash checksum: "); return FALSE; } flash_checksum = fu_memread_uint32(buf, G_LITTLE_ENDIAN); if (helper->checksum != flash_checksum) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "checksum 0x%x mismatched 0x%x", flash_checksum, helper->checksum); return FALSE; } /* success */ return TRUE; } static gboolean fu_synaptics_mst_device_update_firmware(FuSynapticsMstDevice *self, GBytes *fw, FuProgress *progress, GError **error) { g_autoptr(FuSynapticsMstDeviceHelper) helper = fu_synaptics_mst_device_helper_new(); guint32 fw_size = 0; switch (self->family) { case FU_SYNAPTICS_MST_FAMILY_TESLA: case FU_SYNAPTICS_MST_FAMILY_LEAF: case FU_SYNAPTICS_MST_FAMILY_PANAMERA: fw_size = PANAMERA_FIRMWARE_SIZE; break; case FU_SYNAPTICS_MST_FAMILY_SPYDER: case FU_SYNAPTICS_MST_FAMILY_CAYENNE: fw_size = CAYENNE_FIRMWARE_SIZE; break; case FU_SYNAPTICS_MST_FAMILY_CARRERA: fw_size = CARRERA_FIRMWARE_SIZE; break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Unsupported chip family"); return FALSE; } helper->fw = fu_bytes_new_offset(fw, 0x0, fw_size, error); if (helper->fw == NULL) return FALSE; helper->checksum = fu_synaptics_mst_calculate_crc16(0, g_bytes_get_data(helper->fw, NULL), g_bytes_get_size(helper->fw)); helper->progress = g_object_ref(progress); helper->chunks = fu_chunk_array_new_from_bytes(helper->fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, BLOCK_UNIT); if (!fu_device_retry(FU_DEVICE(self), fu_synaptics_mst_device_update_firmware_cb, MAX_RETRY_COUNTS, helper, error)) return FALSE; if (!fu_synaptics_mst_device_rc_set_command(self, FU_SYNAPTICS_MST_UPDC_CMD_ACTIVATE_FIRMWARE, 0, NULL, 0, error)) { g_prefix_error(error, "active firmware failed: "); return FALSE; } return TRUE; } static gboolean fu_synaptics_mst_device_restart(FuSynapticsMstDevice *self, GError **error) { guint8 buf[4] = {0xF5, 0, 0, 0}; gint offset; g_autoptr(GError) error_local = NULL; switch (self->family) { case FU_SYNAPTICS_MST_FAMILY_TESLA: case FU_SYNAPTICS_MST_FAMILY_LEAF: case FU_SYNAPTICS_MST_FAMILY_PANAMERA: offset = 0x2000FC; break; case FU_SYNAPTICS_MST_FAMILY_CAYENNE: case FU_SYNAPTICS_MST_FAMILY_SPYDER: offset = 0x2020021C; break; case FU_SYNAPTICS_MST_FAMILY_CARRERA: offset = 0x2020A024; break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Unsupported chip family"); return FALSE; } /* issue the reboot command, ignore return code (triggers before returning) */ if (!fu_synaptics_mst_device_rc_set_command(self, FU_SYNAPTICS_MST_UPDC_CMD_WRITE_TO_MEMORY, offset, buf, sizeof(buf), &error_local)) g_debug("failed to restart: %s", error_local->message); return TRUE; } static FuFirmware * fu_synaptics_mst_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuSynapticsMstDevice *self = FU_SYNAPTICS_MST_DEVICE(device); g_autoptr(FuFirmware) firmware = fu_synaptics_mst_firmware_new(); /* set chip family to use correct board ID offset */ fu_synaptics_mst_firmware_set_family(FU_SYNAPTICS_MST_FIRMWARE(firmware), self->family); /* check firmware and board ID match */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_VID_PID) == 0 && !fu_device_has_private_flag(device, FU_SYNAPTICS_MST_DEVICE_FLAG_IGNORE_BOARD_ID)) { guint16 board_id = fu_synaptics_mst_firmware_get_board_id(FU_SYNAPTICS_MST_FIRMWARE(firmware)); if (board_id != self->board_id) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "board ID mismatch, got 0x%04x, expected 0x%04x", board_id, self->board_id); return NULL; } } return g_steal_pointer(&firmware); } static gboolean fu_synaptics_mst_device_attach(FuDevice *device, FuProgress *progress, GError **error) { /* some devices do not use a GPIO to reset the chip */ if (fu_device_has_private_flag(device, FU_SYNAPTICS_MST_DEVICE_FLAG_MANUAL_RESTART_REQUIRED)) { g_autoptr(FwupdRequest) request = fwupd_request_new(); fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_IMMEDIATE); fwupd_request_set_id(request, FWUPD_REQUEST_ID_REPLUG_POWER); fwupd_request_add_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); if (!fu_device_emit_request(device, request, progress, error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); } /* success */ return TRUE; } static gboolean fu_synaptics_mst_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuSynapticsMstDevice *self = FU_SYNAPTICS_MST_DEVICE(device); g_autoptr(GBytes) fw = NULL; g_autoptr(FuDeviceLocker) locker = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 90, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 10, NULL); fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* enable remote control and disable on exit */ if (!fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_SKIPS_RESTART)) { locker = fu_device_locker_new_full(self, (FuDeviceLockerFunc)fu_synaptics_mst_device_enable_rc, (FuDeviceLockerFunc)fu_synaptics_mst_device_restart, error); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); fu_device_set_remove_delay(FU_DEVICE(self), 10000); /* a long time */ } else { locker = fu_device_locker_new_full( self, (FuDeviceLockerFunc)fu_synaptics_mst_device_enable_rc, (FuDeviceLockerFunc)fu_synaptics_mst_device_disable_rc, error); } if (locker == NULL) return FALSE; /* update firmware */ switch (self->family) { case FU_SYNAPTICS_MST_FAMILY_TESLA: case FU_SYNAPTICS_MST_FAMILY_LEAF: if (!fu_synaptics_mst_device_update_tesla_leaf_firmware(self, fw, progress, error)) { g_prefix_error(error, "Firmware update failed: "); return FALSE; } break; case FU_SYNAPTICS_MST_FAMILY_PANAMERA: if (!fu_synaptics_mst_device_panamera_prepare_write(self, error)) { g_prefix_error(error, "Failed to prepare for write: "); return FALSE; } if (!fu_synaptics_mst_device_update_esm(self, fw, progress, error)) { g_prefix_error(error, "ESM update failed: "); return FALSE; } if (!fu_synaptics_mst_device_update_panamera_firmware(self, fw, progress, error)) { g_prefix_error(error, "Firmware update failed: "); return FALSE; } break; case FU_SYNAPTICS_MST_FAMILY_CAYENNE: case FU_SYNAPTICS_MST_FAMILY_SPYDER: case FU_SYNAPTICS_MST_FAMILY_CARRERA: if (!fu_synaptics_mst_device_update_firmware(self, fw, progress, error)) { g_prefix_error(error, "Firmware update failed: "); return FALSE; } break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Unsupported chip family"); return FALSE; } fu_progress_step_done(progress); /* wait for flash clear to settle */ fu_device_sleep_full(device, 2000, fu_progress_get_child(progress)); /* ms */ fu_progress_step_done(progress); return TRUE; } static gboolean fu_synaptics_mst_device_ensure_board_id(FuSynapticsMstDevice *self, GError **error) { gint offset; guint8 buf[4] = {0x0}; /* in test mode we need to open a different file node instead */ if (fu_device_has_private_flag(FU_DEVICE(self), FU_SYNAPTICS_MST_DEVICE_FLAG_IS_SOMEWHAT_EMULATED)) { g_autofree gchar *filename = NULL; g_autofree gchar *dirname = NULL; gboolean exists_eeprom = FALSE; gint fd; dirname = g_path_get_dirname(fu_udev_device_get_device_file(FU_UDEV_DEVICE(self))); filename = g_strdup_printf("%s/remote/%s_eeprom", dirname, fu_device_get_logical_id(FU_DEVICE(self))); if (!fu_device_query_file_exists(FU_DEVICE(self), filename, &exists_eeprom, error)) return FALSE; if (!exists_eeprom) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no device exists %s", filename); return FALSE; } fd = open(filename, O_RDONLY); if (fd == -1) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_PERMISSION_DENIED, "cannot open device %s", filename); return FALSE; } if (read(fd, buf, 2) != 2) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "error reading EEPROM file %s", filename); close(fd); return FALSE; } self->board_id = fu_memread_uint16(buf, G_BIG_ENDIAN); close(fd); return TRUE; } if (self->family == FU_SYNAPTICS_MST_FAMILY_CARRERA) { /* get ID via RC command */ if (!fu_synaptics_mst_device_rc_get_command(self, FU_SYNAPTICS_MST_UPDC_CMD_GET_ID, 0, buf, sizeof(buf), error)) { g_prefix_error(error, "RC command failed: "); return FALSE; } if (!fu_memread_uint16_safe(buf, sizeof(buf), 0x2, &self->board_id, G_BIG_ENDIAN, error)) return FALSE; } else { /* older chip reads customer&board ID from memory */ switch (self->family) { case FU_SYNAPTICS_MST_FAMILY_TESLA: case FU_SYNAPTICS_MST_FAMILY_LEAF: case FU_SYNAPTICS_MST_FAMILY_PANAMERA: offset = (gint)ADDR_MEMORY_CUSTOMER_ID; break; case FU_SYNAPTICS_MST_FAMILY_CAYENNE: offset = (gint)ADDR_MEMORY_CUSTOMER_ID_CAYENNE; break; case FU_SYNAPTICS_MST_FAMILY_SPYDER: offset = (gint)ADDR_MEMORY_CUSTOMER_ID_SPYDER; break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Unsupported chip family"); return FALSE; } if (!fu_synaptics_mst_device_rc_get_command( self, FU_SYNAPTICS_MST_UPDC_CMD_READ_FROM_MEMORY, offset, buf, sizeof(buf), error)) { g_prefix_error(error, "memory query failed: "); return FALSE; } if (!fu_memread_uint16_safe(buf, sizeof(buf), 0x0, &self->board_id, G_BIG_ENDIAN, error)) return FALSE; } return TRUE; } static gboolean fu_synaptics_mst_device_setup(FuDevice *device, GError **error) { FuSynapticsMstDevice *self = FU_SYNAPTICS_MST_DEVICE(device); FuDevice *parent; const gchar *name_family; const gchar *name_parent = NULL; guint8 buf_ver[3] = {0x0}; guint8 buf_cid[2] = {0x0}; guint8 rc_cap = 0x0; g_autofree gchar *guid0 = NULL; g_autofree gchar *guid1 = NULL; g_autofree gchar *guid2 = NULL; g_autofree gchar *guid3 = NULL; g_autofree gchar *name = NULL; g_autofree gchar *version = NULL; g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GError) error_local = NULL; /* read support for remote control */ if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), FU_SYNAPTICS_MST_REG_RC_CAP, &rc_cap, sizeof(rc_cap), FU_SYNAPTICS_MST_DEVICE_READ_TIMEOUT, error)) { g_prefix_error(error, "failed to read remote control capabilities: "); return FALSE; } if ((rc_cap & 0x04) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no support for remote control"); return FALSE; } /* enable remote control and disable on exit */ locker = fu_device_locker_new_full(self, (FuDeviceLockerFunc)fu_synaptics_mst_device_enable_rc, (FuDeviceLockerFunc)fu_synaptics_mst_device_disable_rc, &error_local); if (locker == NULL) { if (g_strcmp0(fu_device_get_name(device), "DPMST") == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "downstream endpoint not supported"); } else { g_propagate_error(error, g_steal_pointer(&error_local)); } return FALSE; } /* read firmware version: the third byte is vendor-specific usage */ if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), REG_FIRMWARE_VERSION, buf_ver, sizeof(buf_ver), FU_SYNAPTICS_MST_DEVICE_READ_TIMEOUT, error)) { g_prefix_error(error, "failed to read firmware version: "); return FALSE; } version = g_strdup_printf("%1d.%02d.%02d", buf_ver[0], buf_ver[1], buf_ver[2]); fu_device_set_version(FU_DEVICE(self), version); /* read board chip_id */ if (!fu_dpaux_device_read(FU_DPAUX_DEVICE(self), REG_CHIP_ID, buf_cid, sizeof(buf_cid), FU_SYNAPTICS_MST_DEVICE_READ_TIMEOUT, error)) { g_prefix_error(error, "failed to read chip id: "); return FALSE; } self->chip_id = fu_memread_uint16(buf_cid, G_BIG_ENDIAN); if (self->chip_id == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid chip ID"); return FALSE; } self->family = fu_synaptics_mst_family_from_chip_id(self->chip_id); /* VMM >= 6 use RSA2048 */ switch (self->family) { case FU_SYNAPTICS_MST_FAMILY_TESLA: case FU_SYNAPTICS_MST_FAMILY_LEAF: case FU_SYNAPTICS_MST_FAMILY_PANAMERA: fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); break; case FU_SYNAPTICS_MST_FAMILY_CAYENNE: case FU_SYNAPTICS_MST_FAMILY_SPYDER: case FU_SYNAPTICS_MST_FAMILY_CARRERA: fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); break; default: g_warning("family 0x%02x does not indicate unsigned/signed payload", self->family); break; } /* check the active bank for debugging */ if (self->family == FU_SYNAPTICS_MST_FAMILY_PANAMERA) { if (!fu_synaptics_mst_device_get_active_bank_panamera(self, error)) return FALSE; } /* read board ID */ if (!fu_synaptics_mst_device_ensure_board_id(self, error)) return FALSE; parent = fu_device_get_parent(FU_DEVICE(self)); if (parent != NULL) name_parent = fu_device_get_name(parent); if (name_parent != NULL) { name = g_strdup_printf("VMM%04x inside %s", self->chip_id, name_parent); } else { name = g_strdup_printf("VMM%04x", self->chip_id); } fu_device_set_name(FU_DEVICE(self), name); /* set up the device name and kind via quirks */ guid0 = g_strdup_printf("MST-%u", self->board_id); fu_device_add_instance_id(FU_DEVICE(self), guid0); /* requires user to do something */ if (fu_device_has_private_flag(device, FU_SYNAPTICS_MST_DEVICE_FLAG_MANUAL_RESTART_REQUIRED)) { fu_device_set_remove_delay(device, FU_DEVICE_REMOVE_DELAY_USER_REPLUG); } /* this is a host system, use system ID */ name_family = fu_synaptics_mst_family_to_string(self->family); if (g_strcmp0(self->device_kind, "system") == 0) { FuContext *ctx = fu_device_get_context(device); const gchar *system_type = fu_context_get_hwid_value(ctx, FU_HWIDS_KEY_PRODUCT_SKU); g_autofree gchar *guid = g_strdup_printf("MST-%s-%s-%u", name_family, system_type, self->board_id); fu_device_add_instance_id(FU_DEVICE(self), guid); /* docks or something else */ } else if (self->device_kind != NULL) { g_auto(GStrv) templates = NULL; templates = g_strsplit(self->device_kind, ",", -1); for (guint i = 0; templates[i] != NULL; i++) { g_autofree gchar *dock_id1 = NULL; g_autofree gchar *dock_id2 = NULL; dock_id1 = g_strdup_printf("MST-%s-%u", templates[i], self->board_id); fu_device_add_instance_id(FU_DEVICE(self), dock_id1); dock_id2 = g_strdup_printf("MST-%s-vmm%04x-%u", templates[i], self->chip_id, self->board_id); fu_device_add_instance_id(FU_DEVICE(self), dock_id2); } } else { /* devices are explicit opt-in */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "ignoring %s device with no SynapticsMstDeviceKind quirk", guid0); return FALSE; } /* detect chip family */ switch (self->family) { case FU_SYNAPTICS_MST_FAMILY_PANAMERA: case FU_SYNAPTICS_MST_FAMILY_CAYENNE: case FU_SYNAPTICS_MST_FAMILY_SPYDER: case FU_SYNAPTICS_MST_FAMILY_CARRERA: fu_device_add_flag(device, FWUPD_DEVICE_FLAG_DUAL_IMAGE); default: break; } /* add non-standard GUIDs */ guid1 = g_strdup_printf("MST-%s-vmm%04x-%u", name_family, self->chip_id, self->board_id); fu_device_add_instance_id(FU_DEVICE(self), guid1); guid2 = g_strdup_printf("MST-%s-%u", name_family, self->board_id); fu_device_add_instance_id(FU_DEVICE(self), guid2); guid3 = g_strdup_printf("MST-%s", name_family); fu_device_add_instance_id_full(FU_DEVICE(self), guid3, FU_DEVICE_INSTANCE_FLAG_QUIRKS); /* whitebox customers */ if ((self->board_id >> 8) == 0x0) fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_ENFORCE_REQUIRES); return TRUE; } static gboolean fu_synaptics_mst_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuSynapticsMstDevice *self = FU_SYNAPTICS_MST_DEVICE(device); if (g_strcmp0(key, "SynapticsMstDeviceKind") == 0) { self->device_kind = g_strdup(value); return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_synaptics_mst_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 45, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 54, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "reload"); } static void fu_synaptics_mst_device_class_init(FuSynapticsMstDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->finalize = fu_synaptics_mst_device_finalize; device_class->to_string = fu_synaptics_mst_device_to_string; device_class->set_quirk_kv = fu_synaptics_mst_device_set_quirk_kv; device_class->setup = fu_synaptics_mst_device_setup; device_class->write_firmware = fu_synaptics_mst_device_write_firmware; device_class->attach = fu_synaptics_mst_device_attach; device_class->prepare_firmware = fu_synaptics_mst_device_prepare_firmware; device_class->set_progress = fu_synaptics_mst_device_set_progress; } fwupd-2.0.10/plugins/synaptics-mst/fu-synaptics-mst-device.h000066400000000000000000000007451501337203100240530ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_SYNAPTICS_MST_DEVICE (fu_synaptics_mst_device_get_type()) G_DECLARE_FINAL_TYPE(FuSynapticsMstDevice, fu_synaptics_mst_device, FU, SYNAPTICS_MST_DEVICE, FuDpauxDevice) /* FIXME remove when emulation works */ #define FU_SYNAPTICS_MST_DEVICE_FLAG_IS_SOMEWHAT_EMULATED "is-somewhat-emulated" fwupd-2.0.10/plugins/synaptics-mst/fu-synaptics-mst-firmware.c000066400000000000000000000132171501337203100244210ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-synaptics-mst-common.h" #include "fu-synaptics-mst-firmware.h" struct _FuSynapticsMstFirmware { FuFirmwareClass parent_instance; guint16 board_id; FuSynapticsMstFamily family; }; G_DEFINE_TYPE(FuSynapticsMstFirmware, fu_synaptics_mst_firmware, FU_TYPE_FIRMWARE) #define ADDR_CUSTOMER_ID_CARRERA 0x620E #define ADDR_CUSTOMER_ID_CAYENNE 0x20E #define ADDR_CUSTOMER_ID_TESLA 0x10E #define ADDR_CONFIG_CARRERA 0x6200 #define ADDR_CONFIG_CAYENNE 0x200 #define ADDR_CONFIG_TESLA 0x100 guint16 fu_synaptics_mst_firmware_get_board_id(FuSynapticsMstFirmware *self) { g_return_val_if_fail(FU_IS_SYNAPTICS_MST_FIRMWARE(self), 0); return self->board_id; } void fu_synaptics_mst_firmware_set_family(FuSynapticsMstFirmware *self, FuSynapticsMstFamily family) { g_return_if_fail(FU_IS_SYNAPTICS_MST_FIRMWARE(self)); self->family = family; } static void fu_synaptics_mst_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuSynapticsMstFirmware *self = FU_SYNAPTICS_MST_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "board_id", self->board_id); fu_xmlb_builder_insert_kv(bn, "family", fu_synaptics_mst_family_to_string(self->family)); } static gboolean fu_synaptics_mst_firmware_detect_family(FuSynapticsMstFirmware *self, GInputStream *stream, gsize offset, GError **error) { guint16 addrs[] = {ADDR_CONFIG_TESLA, ADDR_CONFIG_CAYENNE, ADDR_CONFIG_CARRERA}; for (guint i = 0; i < G_N_ELEMENTS(addrs); i++) { g_autoptr(GByteArray) st = NULL; st = fu_struct_synaptics_firmware_config_parse_stream(stream, offset + addrs[i], error); if (st == NULL) return FALSE; if ((fu_struct_synaptics_firmware_config_get_magic1(st) & 0x80) && (fu_struct_synaptics_firmware_config_get_magic2(st) & 0x80)) { self->family = fu_struct_synaptics_firmware_config_get_version(st) >> 4; return TRUE; } } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unable to autodetect chip family"); return FALSE; } static gboolean fu_synaptics_mst_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuSynapticsMstFirmware *self = FU_SYNAPTICS_MST_FIRMWARE(firmware); guint16 addr; /* if device family not specified by caller, try to get from firmware file */ if (self->family == FU_SYNAPTICS_MST_FAMILY_UNKNOWN) { if (!fu_synaptics_mst_firmware_detect_family(self, stream, 0x0, error)) return FALSE; } switch (self->family) { case FU_SYNAPTICS_MST_FAMILY_TESLA: case FU_SYNAPTICS_MST_FAMILY_LEAF: case FU_SYNAPTICS_MST_FAMILY_PANAMERA: addr = ADDR_CUSTOMER_ID_TESLA; break; case FU_SYNAPTICS_MST_FAMILY_CAYENNE: case FU_SYNAPTICS_MST_FAMILY_SPYDER: addr = ADDR_CUSTOMER_ID_CAYENNE; break; case FU_SYNAPTICS_MST_FAMILY_CARRERA: addr = ADDR_CUSTOMER_ID_CARRERA; break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported chip family %s", fu_synaptics_mst_family_to_string(self->family)); return FALSE; } if (!fu_input_stream_read_u16(stream, addr, &self->board_id, G_BIG_ENDIAN, error)) return FALSE; /* success */ return TRUE; } static GByteArray * fu_synaptics_mst_firmware_write(FuFirmware *firmware, GError **error) { FuSynapticsMstFirmware *self = FU_SYNAPTICS_MST_FIRMWARE(firmware); g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GBytes) blob = NULL; guint16 addr; switch (self->family) { case FU_SYNAPTICS_MST_FAMILY_TESLA: case FU_SYNAPTICS_MST_FAMILY_LEAF: case FU_SYNAPTICS_MST_FAMILY_PANAMERA: addr = ADDR_CUSTOMER_ID_TESLA; break; case FU_SYNAPTICS_MST_FAMILY_CAYENNE: case FU_SYNAPTICS_MST_FAMILY_SPYDER: addr = ADDR_CUSTOMER_ID_CAYENNE; break; case FU_SYNAPTICS_MST_FAMILY_CARRERA: addr = ADDR_CUSTOMER_ID_CARRERA; break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported chip family"); return NULL; } /* assumed header */ fu_byte_array_set_size(buf, addr + sizeof(guint16), 0x00); if (!fu_memwrite_uint16_safe(buf->data, buf->len, addr, fu_firmware_get_idx(firmware), G_BIG_ENDIAN, error)) return NULL; /* payload */ blob = fu_firmware_get_bytes_with_patches(firmware, error); if (blob == NULL) return NULL; fu_byte_array_append_bytes(buf, blob); /* success */ return g_steal_pointer(&buf); } static gboolean fu_synaptics_mst_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { FuSynapticsMstFirmware *self = FU_SYNAPTICS_MST_FIRMWARE(firmware); guint64 tmp; /* optional properties */ tmp = xb_node_query_text_as_uint(n, "board_id", NULL); if (tmp != G_MAXUINT64) self->board_id = tmp; tmp = xb_node_query_text_as_uint(n, "family", NULL); if (tmp != G_MAXUINT64) self->family = tmp; /* success */ return TRUE; } static void fu_synaptics_mst_firmware_init(FuSynapticsMstFirmware *self) { self->family = FU_SYNAPTICS_MST_FAMILY_UNKNOWN; fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_NO_AUTO_DETECTION); } static void fu_synaptics_mst_firmware_class_init(FuSynapticsMstFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_synaptics_mst_firmware_parse; firmware_class->export = fu_synaptics_mst_firmware_export; firmware_class->write = fu_synaptics_mst_firmware_write; firmware_class->build = fu_synaptics_mst_firmware_build; } FuFirmware * fu_synaptics_mst_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_SYNAPTICS_MST_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/synaptics-mst/fu-synaptics-mst-firmware.h000066400000000000000000000011771501337203100244300ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-synaptics-mst-struct.h" #define FU_TYPE_SYNAPTICS_MST_FIRMWARE (fu_synaptics_mst_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuSynapticsMstFirmware, fu_synaptics_mst_firmware, FU, SYNAPTICS_MST_FIRMWARE, FuFirmware) FuFirmware * fu_synaptics_mst_firmware_new(void); guint16 fu_synaptics_mst_firmware_get_board_id(FuSynapticsMstFirmware *self); void fu_synaptics_mst_firmware_set_family(FuSynapticsMstFirmware *self, FuSynapticsMstFamily family); fwupd-2.0.10/plugins/synaptics-mst/fu-synaptics-mst-plugin.c000066400000000000000000000036521501337203100241050ustar00rootroot00000000000000/* * Copyright 2017 Mario Limonciello * Copyright 2017 Peichen Huang * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-synaptics-mst-common.h" #include "fu-synaptics-mst-device.h" #include "fu-synaptics-mst-firmware.h" #include "fu-synaptics-mst-plugin.h" struct _FuSynapticsMstPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuSynapticsMstPlugin, fu_synaptics_mst_plugin, FU_TYPE_PLUGIN) static gboolean fu_synaptics_mst_plugin_write_firmware(FuPlugin *plugin, FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { g_autoptr(FuDeviceLocker) locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; if (!fu_device_write_firmware(device, firmware, progress, flags, error)) return FALSE; if (!fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_SKIPS_RESTART)) fu_plugin_device_remove(plugin, device); return TRUE; } static void fu_synaptics_mst_plugin_init(FuSynapticsMstPlugin *self) { } static void fu_synaptics_mst_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "SynapticsMstDeviceKind"); fu_plugin_add_udev_subsystem(plugin, "drm"); /* used for uevent only */ fu_plugin_add_udev_subsystem(plugin, "drm_dp_aux_dev"); fu_plugin_add_device_gtype(plugin, FU_TYPE_SYNAPTICS_MST_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_SYNAPTICS_MST_FIRMWARE); } static void fu_synaptics_mst_plugin_class_init(FuSynapticsMstPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_synaptics_mst_plugin_constructed; plugin_class->write_firmware = fu_synaptics_mst_plugin_write_firmware; } fwupd-2.0.10/plugins/synaptics-mst/fu-synaptics-mst-plugin.h000066400000000000000000000004341501337203100241050ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuSynapticsMstPlugin, fu_synaptics_mst_plugin, FU, SYNAPTICS_MST_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/synaptics-mst/fu-synaptics-mst.rs000066400000000000000000000026761501337203100230200ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] enum FuSynapticsMstFamily { Unknown = 0xFF, Tesla = 0, Leaf = 1, Panamera = 2, Cayenne = 3, Spyder = 4, Carrera = 5, } #[derive(ToString)] enum FuSynapticsMstUpdcRc { Success, Invalid, Unsupported, Failed, Disabled, ConfigureSignFailed, FirmwareSignFailed, RollbackFailed, } #[derive(ToString)] enum FuSynapticsMstUpdcCmd { EnableRc = 0x01, DisableRc = 0x02, GetId = 0x03, GetVersion = 0x04, FlashMapping = 0x07, EnableFlashChipErase = 0x08, CalEepromChecksum = 0x11, FlashErase = 0x14, CalEepromCheckCrc8 = 0x16, CalEepromCheckCrc16 = 0x17, ActivateFirmware = 0x18, WriteToEeprom = 0x20, WriteToMemory = 0x21, WriteToTxDpcd = 0x22, // TX0 WriteToTxDpcdTx1 = 0x23, WriteToTxDpcdTx2 = 0x24, WriteToTxDpcdTx3 = 0x25, ReadFromEeprom = 0x30, ReadFromMemory = 0x31, ReadFromTxDpcd = 0x32, // TX0 ReadFromTxDpcdTx1 = 0x33, ReadFromTxDpcdTx2 = 0x34, ReadFromTxDpcdTx3 = 0x35, } enum FuSynapticsMstRegRc { Cap = 0x4B0, State = 0x4B1, Cmd = 0x4B2, Result = 0x4B3, Len = 0x4B8, Offset = 0x4BC, Data = 0x4C0, } #[derive(ParseStream)] #[repr(C, packed)] struct FuStructSynapticsFirmwareConfig { version: u8, reserved: u8, magic1: u8, magic2: u8, } fwupd-2.0.10/plugins/synaptics-mst/meson.build000066400000000000000000000033511501337203100213550ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginSynapticsMST"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('synaptics-mst.quirk') plugin_builtin_synaptics_mst = static_library('fu_plugin_synaptics_mst', rustgen.process( 'fu-synaptics-mst.rs', # fuzzing ), sources: [ 'fu-synaptics-mst-plugin.c', 'fu-synaptics-mst-common.c', 'fu-synaptics-mst-device.c', 'fu-synaptics-mst-firmware.c', # fuzzing ], include_directories: plugin_incdirs, c_args: [ cargs, ], link_with: plugin_libs, dependencies: plugin_deps, ) plugin_builtins += plugin_builtin_synaptics_mst device_tests += files( 'tests/synaptics-panamera.json', ) enumeration_data += files( 'tests/synaptics-panamera-setup.json', ) if get_option('tests') install_data(['tests/synaptics-mst.builder.xml'], install_dir: join_paths(installed_test_datadir, 'tests')) env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) env.set('FWUPD_LOCALSTATEDIR', '/tmp/fwupd-self-test/var') env.set('FWUPD_DATADIR_QUIRKS', meson.current_source_dir()) e = executable( 'synaptics-mst-self-test', rustgen.process('fu-synaptics-mst.rs'), sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: [ plugin_deps, valgrind, ], link_with: [ fwupd, fwupdplugin, plugin_builtin_synaptics_mst, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ cargs, '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('synaptics-mst-self-test', e, env: env) endif endif fwupd-2.0.10/plugins/synaptics-mst/synaptics-mst-evb.quirk000066400000000000000000000013011501337203100236510ustar00rootroot00000000000000# Synaptics MST early validation board support # # This is not installed by default, but can be used to exercise new boards # that don't yet have customer ID or board ID bytes filled out. # # To use it, load the quirk file into the quirks directory for the fwupd installation # Usually this is /usr/share/fwupd/quirks.d # # Note: The flag "ignore-board-id" will be used to ignore the board ID checking in # during flashing. This shouldn't be used in practice for production boards. # [MST-4] Name = Synaptics Carrera EVB R2 SynapticsMstDeviceKind = carrera_evb_r2 [MST-2] Name = Synaptics EVB development board SynapticsMstDeviceKind = panamera_evb [MST-panamera_evb-vmm5331-2] Flags = ignore-board-id fwupd-2.0.10/plugins/synaptics-mst/synaptics-mst.quirk000066400000000000000000000033671501337203100231150ustar00rootroot00000000000000# GUID generation for Synaptics MST plugin # # SYNAPTICSMST\BID is the 16 bit board ID which contains: # * Customer ID in first byte # * Board ID in the second byte # # SynapticsMstDeviceKind = system # * Will map to a GUID containing HwID product SKU # * These GUIDs will look like MST-${PRODUCTSKU}-${BOARDID} # SynapticsMstDeviceKind != system # * Will map to a GUID containing each comma delimited substring # * These GUIDs will look like MST-${DEVICEKIND}-${CHIPID}-${BOARDID} # # By default the Synaptics MST device will restart after update # To override this behavior add FU_DEVICE_PRIVATE_FLAG_SKIPS_RESTART # [DPAUX\OUI_0090CC24] Plugin = synaptics_mst [MST-162] Name = Luxshare Quad USB4 Dock SynapticsMstDeviceKind = dock Flags = manual-restart-required [MST-163] Name = Luxshare 7-in-1 Travel Dock Video Hub SynapticsMstDeviceKind = dock Flags = install-parent-first ParentGuid = USB\VID_208E&PID_0822 [MST-272] Name = Dell X6 Platform SynapticsMstDeviceKind = system [MST-273] Name = Dell X7 Platform SynapticsMstDeviceKind = system [MST-274] Name = Dell WD15/TB16/TB18 wired Dock SynapticsMstDeviceKind = wd15,tb16,tb18 [MST-275] Name = Dell WLD15 Wireless Dock SynapticsMstDeviceKind = wld15 [MST-277] Name = Dell Rugged Platform SynapticsMstDeviceKind = system # ThinkPad Workstation Dock [MST-513] ParentGuid = USB\VID_17EF&PID_305A SynapticsMstDeviceKind = dock # ThinkPad Thunderbolt 3 Workstation Dock [MST-595] ParentGuid = TBT-01081720 SynapticsMstDeviceKind = dock [MST-596] Name = ThinkPad USB-C Dock Gen2 SynapticsMstDeviceKind = dock [MST-602] Name = ThinkPad Universal ThunderBolt 4 Dock SynapticsMstDeviceKind = dock [MST-515] Name = Lenovo USB-C Dual Display Travel Dock SynapticsMstDeviceKind = dock ParentGuid = USB\VID_17EF&PID_10CA fwupd-2.0.10/plugins/synaptics-mst/tests/000077500000000000000000000000001501337203100203535ustar00rootroot00000000000000fwupd-2.0.10/plugins/synaptics-mst/tests/.gitignore000066400000000000000000000000251501337203100223400ustar00rootroot00000000000000no_devices tb16_dock fwupd-2.0.10/plugins/synaptics-mst/tests/synaptics-mst.builder.xml000066400000000000000000000002251501337203100253370ustar00rootroot00000000000000 0x0 0x1234 0x42 ZGF2ZQ== fwupd-2.0.10/plugins/synaptics-mst/tests/synaptics-panamera-setup.json000066400000000000000000000135651501337203100262150ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUdevDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:08.1/0000:c5:00.0/drm/card1/card1-DP-3/drm_dp_aux3", "DeviceFile": "/dev/drm_dp_aux3", "Subsystem": "drm_dp_aux_dev", "Created": "2025-03-04T14:31:58.001138Z", "Events": [ { "Id": "#d5a801ad", "Data": "drm_dp_aux_dev" }, { "Id": "#ad8c58d3" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "MAJOR=236\nMINOR=3\nDEVNAME=drm_dp_aux3" }, { "Id": "#d432c663", "Data": "drm_dp_aux3" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c" }, { "Id": "#ad8c58d3" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "MAJOR=236\nMINOR=3\nDEVNAME=drm_dp_aux3" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c" }, { "Id": "#fbe018fe" }, { "Id": "#285d9a88", "GType": "FuPciDevice", "BackendId": "/sys/devices/pci0000:00/0000:00:08.1/0000:c5:00.0", "PhysicalId": "PCI_SLOT_NAME=0000:c5:00.0" }, { "Id": "#d5a801ad", "Data": "pci" }, { "Id": "#ad8c58d3", "Data": "amdgpu" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "DRIVER=amdgpu\nPCI_CLASS=30000\nPCI_ID=1002:15BF\nPCI_SUBSYS_ID=17AA:231C\nPCI_SLOT_NAME=0000:c5:00.0\nMODALIAS=pci:v00001002d000015BFsv000017AAsd0000231Cbc03sc00i00" }, { "Id": "#d432c663" }, { "Id": "#9b895db2", "Data": "0x1002" }, { "Id": "#66f3e150", "Data": "0x15bf" }, { "Id": "#d410b6c7", "Data": "0x030000" }, { "Id": "#1075ed5c" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "DRIVER=amdgpu\nPCI_CLASS=30000\nPCI_ID=1002:15BF\nPCI_SUBSYS_ID=17AA:231C\nPCI_SLOT_NAME=0000:c5:00.0\nMODALIAS=pci:v00001002d000015BFsv000017AAsd0000231Cbc03sc00i00" }, { "Id": "#d432c663" }, { "Id": "#9b895db2", "Data": "0x1002" }, { "Id": "#66f3e150", "Data": "0x15bf" }, { "Id": "#d410b6c7", "Data": "0x030000" }, { "Id": "#1075ed5c" }, { "Id": "#fbe018fe" }, { "Id": "#d410b6c7", "Data": "0x030000" }, { "Id": "#41bc32d8", "Data": "113-PHXGENERIC-001" }, { "Id": "#bf29d2f6", "Data": "0xd4" }, { "Id": "#269abd81", "Data": "0x17aa" }, { "Id": "#360cec38", "Data": "0x231c" }, { "Id": "#d2629d83", "Data": "0000:c5:00.0" }, { "Id": "#d2629d83", "Data": "0000:c5:00.0" }, { "Id": "#e371a663", "Data": "AMDGPU DM aux hw bus 4" }, { "Id": "#506aff56", "Data": "kMwkU1lOQVMyEAUHBQ==" }, { "Id": "#ad8c58d3" }, { "Id": "#1075ed5c" }, { "Id": "#bddbca22", "Data": "MAJOR=236\nMINOR=3\nDEVNAME=drm_dp_aux3" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c" }, { "Id": "#fbe018fe" }, { "Id": "#e371a663", "Data": "AMDGPU DM aux hw bus 4" }, { "Id": "#2f2bb69f", "Data": "lA==" }, { "Id": "#73c075ab" }, { "Id": "#292d9db7", "Data": "AgQ=" }, { "Id": "#b3afdf0d" }, { "Id": "#6ecc6fe7" }, { "Id": "#62e0f0ae" }, { "Id": "#5f872044" }, { "Id": "#292d9db7", "Data": "AQA=" }, { "Id": "#db76a894", "Data": "BQcF" }, { "Id": "#292d9db7", "Data": "UzI=" }, { "Id": "#2cc68095" }, { "Id": "#9af06bdb" }, { "Id": "#a6746544" }, { "Id": "#292d9db7", "Data": "MQA=" }, { "Id": "#271d05ea", "Data": "DwAAJQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" }, { "Id": "#a2224be6" }, { "Id": "#9af06bdb" }, { "Id": "#a6746544" }, { "Id": "#292d9db7", "Data": "MQA=" }, { "Id": "#271d05ea", "Data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" }, { "Id": "#d196e2e5" }, { "Id": "#b112b173" }, { "Id": "#a6746544" }, { "Id": "#292d9db7", "Data": "MQA=" }, { "Id": "#1adc030f", "Data": "AloF5A==" }, { "Id": "#73c075ab" }, { "Id": "#292d9db7", "Data": "AgA=" } ] } ] } fwupd-2.0.10/plugins/synaptics-mst/tests/synaptics-panamera.json000066400000000000000000000005711501337203100250500ustar00rootroot00000000000000{ "name": "Synaptics Panamera", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/synaptics-panamera-setup.json", "components": [ { "version": "5.07.05", "protocol": "com.synaptics.mst", "guids": [ "2a45c6cf-3641-50ec-9f69-4ee875d0d1ff" ] } ] } ] } fwupd-2.0.10/plugins/synaptics-prometheus/000077500000000000000000000000001501337203100206015ustar00rootroot00000000000000fwupd-2.0.10/plugins/synaptics-prometheus/README.md000066400000000000000000000024341501337203100220630ustar00rootroot00000000000000--- title: Plugin: Synaptics Prometheus --- ## Introduction This plugin can flash the firmware on the Synaptics Prometheus fingerprint readers. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in an unspecified binary file format. The binary file has a vendor-specific header that is used when flashing the image. This plugin supports the following protocol ID: * `com.synaptics.prometheus` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_06CB&PID_00A9` * `USB\VID_06CB&PID_00A9-cfg` * `USB\VID_06CB&PID_00A9&CFG1_3483&CFG2_500` ## Update Behavior The device usually presents in runtime mode, but on detach re-enumerates with the same USB PID in an unlocked mode. On attach the device again re-enumerates back to the runtime locked mode. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x06CB` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.2.9`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Vincent Huang: @synavincent fwupd-2.0.10/plugins/synaptics-prometheus/data/000077500000000000000000000000001501337203100215125ustar00rootroot00000000000000fwupd-2.0.10/plugins/synaptics-prometheus/data/lsusb.txt000066400000000000000000000044351501337203100234110ustar00rootroot00000000000000Bus 001 Device 043: ID 06cb:00a9 Synaptics, Inc. Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 255 Vendor Specific Class bDeviceSubClass 16 bDeviceProtocol 255 bMaxPacketSize0 8 idVendor 0x06cb Synaptics, Inc. idProduct 0x00a9 bcdDevice 0.00 iManufacturer 0 iProduct 0 iSerial 1 942cfe315551 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 0x0027 bNumInterfaces 1 bConfigurationValue 1 iConfiguration 0 bmAttributes 0xa0 (Bus Powered) Remote Wakeup MaxPower 100mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 3 bInterfaceClass 255 Vendor Specific Class bInterfaceSubClass 0 bInterfaceProtocol 0 iInterface 0 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x01 EP 1 OUT bmAttributes 2 Transfer Type Bulk Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 0 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 2 Transfer Type Bulk Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 0 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x83 EP 3 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0008 1x 8 bytes bInterval 4 Device Status: 0x0000 (Bus Powered) fwupd-2.0.10/plugins/synaptics-prometheus/fu-self-test.c000066400000000000000000000103271501337203100232660ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-plugin-private.h" #include "fu-synaprom-device.h" #include "fu-synaprom-firmware.h" static void fu_test_synaprom_firmware_func(void) { const guint8 *buf; gboolean ret; gsize sz = 0; g_autofree gchar *filename = NULL; g_autoptr(FuSynapromDevice) device = fu_synaprom_device_new(NULL); g_autoptr(GBytes) blob1 = NULL; g_autoptr(GBytes) blob2 = NULL; g_autoptr(GBytes) fw = NULL; g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(FuFirmware) firmware2 = NULL; g_autoptr(FuFirmware) firmware = fu_synaprom_firmware_new(); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); filename = g_test_build_filename(G_TEST_DIST, "tests", "test.pkg", NULL); if (!g_file_test(filename, G_FILE_TEST_EXISTS)) { g_test_skip("Missing test.pkg"); return; } fw = fu_bytes_get_contents(filename, &error); g_assert_no_error(error); g_assert_nonnull(fw); buf = g_bytes_get_data(fw, &sz); g_assert_cmpint(sz, ==, 294); g_assert_cmpint(buf[0], ==, 0x01); g_assert_cmpint(buf[1], ==, 0x00); ret = fu_firmware_parse_bytes(firmware, fw, 0x0, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH | FU_FIRMWARE_PARSE_FLAG_CACHE_STREAM, &error); g_assert_no_error(error); g_assert_true(ret); /* does not exist */ blob1 = fu_firmware_get_image_by_id_bytes(firmware, "NotGoingToExist", NULL); g_assert_null(blob1); blob1 = fu_firmware_get_image_by_id_bytes(firmware, "cfg-update-header", NULL); g_assert_null(blob1); /* header needs to exist */ blob1 = fu_firmware_get_image_by_id_bytes(firmware, "mfw-update-header", &error); g_assert_no_error(error); g_assert_nonnull(blob1); buf = g_bytes_get_data(blob1, &sz); g_assert_cmpint(sz, ==, 24); g_assert_cmpint(buf[0], ==, 0x41); g_assert_cmpint(buf[1], ==, 0x00); g_assert_cmpint(buf[2], ==, 0x00); g_assert_cmpint(buf[3], ==, 0x00); g_assert_cmpint(buf[4], ==, 0xff); /* payload needs to exist */ fu_synaprom_device_set_version(device, 10, 1, 1234); stream = g_memory_input_stream_new_from_bytes(fw); firmware2 = fu_synaprom_device_prepare_firmware(FU_DEVICE(device), stream, progress, FU_FIRMWARE_PARSE_FLAG_CACHE_STREAM, &error); g_assert_no_error(error); g_assert_nonnull(firmware2); blob2 = fu_firmware_get_image_by_id_bytes(firmware2, "mfw-update-payload", &error); g_assert_no_error(error); g_assert_nonnull(blob2); buf = g_bytes_get_data(blob2, &sz); g_assert_cmpint(sz, ==, 2); g_assert_cmpint(buf[0], ==, 'R'); g_assert_cmpint(buf[1], ==, 'H'); } static void fu_synaprom_firmware_xml_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autofree gchar *csum1 = NULL; g_autofree gchar *csum2 = NULL; g_autofree gchar *xml_out = NULL; g_autofree gchar *xml_src = NULL; g_autoptr(FuFirmware) firmware1 = fu_synaprom_firmware_new(); g_autoptr(FuFirmware) firmware2 = fu_synaprom_firmware_new(); g_autoptr(GError) error = NULL; /* build and write */ filename = g_test_build_filename(G_TEST_DIST, "tests", "synaptics-prometheus.builder.xml", NULL); ret = g_file_get_contents(filename, &xml_src, NULL, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_firmware_build_from_xml(firmware1, xml_src, &error); g_assert_no_error(error); g_assert_true(ret); csum1 = fu_firmware_get_checksum(firmware1, G_CHECKSUM_SHA1, &error); g_assert_no_error(error); g_assert_cmpstr(csum1, ==, "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"); /* ensure we can round-trip */ xml_out = fu_firmware_export_to_xml(firmware1, FU_FIRMWARE_EXPORT_FLAG_NONE, &error); g_assert_no_error(error); ret = fu_firmware_build_from_xml(firmware2, xml_out, &error); g_assert_no_error(error); g_assert_true(ret); csum2 = fu_firmware_get_checksum(firmware2, G_CHECKSUM_SHA1, &error); g_assert_cmpstr(csum1, ==, csum2); } int main(int argc, char **argv) { (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); g_test_add_func("/synaprom/firmware", fu_test_synaprom_firmware_func); g_test_add_func("/synaprom/firmware{xml}", fu_synaprom_firmware_xml_func); return g_test_run(); } fwupd-2.0.10/plugins/synaptics-prometheus/fu-synaprom-common.c000066400000000000000000000037021501337203100245150ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * Copyright 2019 Synaptics Inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-synaprom-common.h" #include "fu-synaprom-struct.h" GByteArray * fu_synaprom_reply_new(gsize cmdlen) { GByteArray *blob = g_byte_array_new(); fu_byte_array_set_size(blob, cmdlen, 0x00); return blob; } gboolean fu_synaprom_error_from_status(guint16 status, GError **error) { if (status == FU_SYNAPROM_RESULT_OK) return TRUE; switch (status) { case FU_SYNAPROM_RESULT_GEN_OPERATION_CANCELED: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "cancelled"); break; case FU_SYNAPROM_RESULT_GEN_BAD_PARAM: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "bad parameter"); break; case FU_SYNAPROM_RESULT_GEN_NULL_POINTER: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "NULL pointer"); break; case FU_SYNAPROM_RESULT_GEN_UNEXPECTED_FORMAT: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "unexpected format"); break; case FU_SYNAPROM_RESULT_GEN_TIMEOUT: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "timed out"); break; case FU_SYNAPROM_RESULT_GEN_OBJECT_DOESNT_EXIST: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "object does not exist"); break; case FU_SYNAPROM_RESULT_GEN_ERROR: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "generic error"); break; case FU_SYNAPROM_RESULT_SENSOR_MALFUNCTIONED: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "sensor malfunctioned"); break; case FU_SYNAPROM_RESULT_SYS_OUT_OF_MEMORY: g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "out of heap memory"); break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "error status: 0x%x", status); } return FALSE; } fwupd-2.0.10/plugins/synaptics-prometheus/fu-synaprom-common.h000066400000000000000000000004531501337203100245220ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * Copyright 2019 Synaptics Inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include GByteArray * fu_synaprom_reply_new(gsize cmdlen); gboolean fu_synaprom_error_from_status(guint16 status, GError **error); fwupd-2.0.10/plugins/synaptics-prometheus/fu-synaprom-config.c000066400000000000000000000226631501337203100245010ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * Copyright 2019 Synaptics Inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-synaprom-common.h" #include "fu-synaprom-config.h" #include "fu-synaprom-firmware.h" #include "fu-synaprom-struct.h" struct _FuSynapromConfig { FuDevice parent_instance; guint32 configid1; /* config ID1 */ guint32 configid2; /* config ID2 */ }; #define FU_SYNAPROM_CMD_IOTA_FIND_FLAGS_ALLIOTAS 0x0001 /* itype ignored*/ #define FU_SYNAPROM_CMD_IOTA_FIND_FLAGS_READMAX 0x0002 /* nbytes ignored */ #define FU_SYNAPROM_MAX_IOTA_READ_SIZE (64 * 1024) /* max size of iota data returned */ #define FU_SYNAPROM_IOTA_ITYPE_CONFIG_VERSION 0x0009 /* Configuration id and version */ G_DEFINE_TYPE(FuSynapromConfig, fu_synaprom_config, FU_TYPE_DEVICE) static gboolean fu_synaprom_config_setup(FuDevice *device, GError **error) { FuDevice *parent = fu_device_get_parent(device); FuSynapromConfig *self = FU_SYNAPROM_CONFIG(device); g_autofree gchar *configid1_str = NULL; g_autofree gchar *configid2_str = NULL; g_autofree gchar *version = NULL; g_autoptr(GByteArray) reply = NULL; g_autoptr(GByteArray) st_request = fu_struct_synaprom_request_new(); g_autoptr(GByteArray) st_cfg = NULL; g_autoptr(GByteArray) st_hdr = NULL; g_autoptr(GByteArray) st_cmd = fu_struct_synaprom_cmd_iota_find_new(); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); /* get IOTA */ fu_struct_synaprom_cmd_iota_find_set_itype(st_cmd, FU_SYNAPROM_IOTA_ITYPE_CONFIG_VERSION); fu_struct_synaprom_cmd_iota_find_set_flags(st_cmd, FU_SYNAPROM_CMD_IOTA_FIND_FLAGS_READMAX); fu_struct_synaprom_request_set_cmd(st_request, FU_SYNAPROM_CMD_IOTA_FIND); g_byte_array_append(st_request, st_cmd->data, st_cmd->len); reply = fu_synaprom_reply_new(FU_STRUCT_SYNAPROM_REPLY_IOTA_FIND_HDR_SIZE + FU_SYNAPROM_MAX_IOTA_READ_SIZE); if (!fu_synaprom_device_cmd_send(FU_SYNAPROM_DEVICE(parent), st_request, reply, progress, 5000, error)) return FALSE; if (reply->len < FU_STRUCT_SYNAPROM_REPLY_IOTA_FIND_HDR_SIZE + FU_STRUCT_SYNAPROM_IOTA_CONFIG_VERSION_SIZE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "CFG return data invalid size: 0x%04x", reply->len); return FALSE; } st_hdr = fu_struct_synaprom_reply_iota_find_hdr_parse(reply->data, reply->len, 0x0, error); if (st_hdr == NULL) return FALSE; if (fu_struct_synaprom_reply_iota_find_hdr_get_itype(st_hdr) != FU_SYNAPROM_IOTA_ITYPE_CONFIG_VERSION) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "CFG iota had invalid itype: 0x%04x", fu_struct_synaprom_reply_iota_find_hdr_get_itype(st_hdr)); return FALSE; } st_cfg = fu_struct_synaprom_iota_config_version_parse(reply->data, reply->len, st_hdr->len, error); if (st_cfg == NULL) return FALSE; self->configid1 = fu_struct_synaprom_iota_config_version_get_config_id1(st_cfg); self->configid2 = fu_struct_synaprom_iota_config_version_get_config_id2(st_cfg); /* we should have made these a %08% uint32_t... */ configid1_str = g_strdup_printf("%u", self->configid1); configid2_str = g_strdup_printf("%u", self->configid2); /* append the configid to the generated GUID */ fu_device_add_instance_str(device, "CFG1", configid1_str); fu_device_add_instance_str(device, "CFG2", configid2_str); if (!fu_device_build_instance_id(device, error, "USB", "VID", "PID", "CFG1", "CFG2", NULL)) return FALSE; /* no downgrades are allowed */ version = g_strdup_printf("%04u", fu_struct_synaprom_iota_config_version_get_version(st_cfg)); fu_device_set_version(FU_DEVICE(self), version); fu_device_set_version_lowest(FU_DEVICE(self), version); return TRUE; } static FuFirmware * fu_synaprom_config_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuSynapromConfig *self = FU_SYNAPROM_CONFIG(device); FuDevice *parent = fu_device_get_parent(device); g_autoptr(GByteArray) st_hdr = NULL; g_autoptr(GInputStream) stream_hdr = NULL; g_autoptr(FuFirmware) firmware = fu_synaprom_firmware_new(); g_autoptr(FuFirmware) img_hdr = NULL; if (fu_synaprom_device_get_product_type(FU_SYNAPROM_DEVICE(parent)) == FU_SYNAPROM_PRODUCT_TYPE_TRITON) { if (!fu_synaprom_firmware_set_signature_size(FU_SYNAPROM_FIRMWARE(firmware), FU_SYNAPROM_FIRMWARE_TRITON_SIGSIZE)) return NULL; } /* parse the firmware */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; /* check the update header product and version */ img_hdr = fu_firmware_get_image_by_id(firmware, "cfg-update-header", error); if (img_hdr == NULL) return NULL; stream_hdr = fu_firmware_get_stream(img_hdr, error); if (stream_hdr == NULL) return NULL; st_hdr = fu_struct_synaprom_cfg_hdr_parse_stream(stream_hdr, 0x0, error); if (st_hdr == NULL) { g_prefix_error(error, "CFG metadata is invalid: "); return NULL; } if (fu_struct_synaprom_cfg_hdr_get_product(st_hdr) != FU_SYNAPROM_PRODUCT_PROMETHEUS) { if (flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_VID_PID) { g_warning("CFG metadata not compatible, " "got 0x%02x expected 0x%02x", fu_struct_synaprom_cfg_hdr_get_product(st_hdr), (guint)FU_SYNAPROM_PRODUCT_PROMETHEUS); } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "CFG metadata not compatible, " "got 0x%02x expected 0x%02x", fu_struct_synaprom_cfg_hdr_get_product(st_hdr), (guint)FU_SYNAPROM_PRODUCT_PROMETHEUS); return NULL; } } if (fu_struct_synaprom_cfg_hdr_get_id1(st_hdr) != self->configid1 || fu_struct_synaprom_cfg_hdr_get_id2(st_hdr) != self->configid2) { if (flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_VID_PID) { g_warning("CFG version not compatible, " "got %u:%u expected %u:%u", fu_struct_synaprom_cfg_hdr_get_id1(st_hdr), fu_struct_synaprom_cfg_hdr_get_id2(st_hdr), self->configid1, self->configid2); } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "CFG version not compatible, " "got %u:%u expected %u:%u", fu_struct_synaprom_cfg_hdr_get_id1(st_hdr), fu_struct_synaprom_cfg_hdr_get_id2(st_hdr), self->configid1, self->configid2); return NULL; } } /* success */ return g_steal_pointer(&firmware); } static gboolean fu_synaprom_config_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDevice *parent = fu_device_get_parent(device); g_autoptr(GBytes) fw = NULL; /* get default image */ fw = fu_firmware_get_image_by_id_bytes(firmware, "cfg-update-payload", error); if (fw == NULL) return FALSE; /* I assume the CFG/MFW difference is detected in the device...*/ return fu_synaprom_device_write_fw(FU_SYNAPROM_DEVICE(parent), fw, progress, error); } static void fu_synaprom_config_init(FuSynapromConfig *self) { fu_device_add_protocol(FU_DEVICE(self), "com.synaptics.prometheus.config"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_ONLY_VERSION_UPGRADE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_OPEN); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_logical_id(FU_DEVICE(self), "cfg"); fu_device_set_name(FU_DEVICE(self), "Prometheus IOTA Config"); fu_device_set_summary(FU_DEVICE(self), "Fingerprint reader config"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_AUTH_FINGERPRINT); } static void fu_synaprom_config_constructed(GObject *obj) { FuSynapromConfig *self = FU_SYNAPROM_CONFIG(obj); FuDevice *parent = fu_device_get_parent(FU_DEVICE(self)); /* append the firmware kind to the generated GUID */ if (parent != NULL) { g_autofree gchar *devid = NULL; devid = g_strdup_printf("USB\\VID_%04X&PID_%04X-cfg", fu_device_get_vid(parent), fu_device_get_pid(parent)); fu_device_add_instance_id(FU_DEVICE(self), devid); } G_OBJECT_CLASS(fu_synaprom_config_parent_class)->constructed(obj); } static gboolean fu_synaprom_config_attach(FuDevice *device, FuProgress *progress, GError **error) { FuDevice *parent = fu_device_get_parent(device); return fu_device_attach_full(parent, progress, error); } static gboolean fu_synaprom_config_detach(FuDevice *device, FuProgress *progress, GError **error) { FuDevice *parent = fu_device_get_parent(device); return fu_device_detach_full(parent, progress, error); } static void fu_synaprom_config_class_init(FuSynapromConfigClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->constructed = fu_synaprom_config_constructed; device_class->write_firmware = fu_synaprom_config_write_firmware; device_class->prepare_firmware = fu_synaprom_config_prepare_firmware; device_class->setup = fu_synaprom_config_setup; device_class->reload = fu_synaprom_config_setup; device_class->attach = fu_synaprom_config_attach; device_class->detach = fu_synaprom_config_detach; } FuSynapromConfig * fu_synaprom_config_new(FuSynapromDevice *device) { FuSynapromConfig *self; self = g_object_new(FU_TYPE_SYNAPROM_CONFIG, "parent", device, NULL); return FU_SYNAPROM_CONFIG(self); } fwupd-2.0.10/plugins/synaptics-prometheus/fu-synaprom-config.h000066400000000000000000000006711501337203100245010ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * Copyright 2019 Synaptics Inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-synaprom-device.h" #define FU_TYPE_SYNAPROM_CONFIG (fu_synaprom_config_get_type()) G_DECLARE_FINAL_TYPE(FuSynapromConfig, fu_synaprom_config, FU, SYNAPROM_CONFIG, FuDevice) FuSynapromConfig * fu_synaprom_config_new(FuSynapromDevice *device); fwupd-2.0.10/plugins/synaptics-prometheus/fu-synaprom-device.c000066400000000000000000000374021501337203100244700ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * Copyright 2019 Synaptics Inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-synaprom-common.h" #include "fu-synaprom-config.h" #include "fu-synaprom-device.h" #include "fu-synaprom-firmware.h" #include "fu-synaprom-struct.h" struct _FuSynapromDevice { FuUsbDevice parent_instance; guint8 vmajor; guint8 vminor; guint32 product_type; }; /* vendor-specific USB control requests to write DFT word (Hayes) */ #define FU_SYNAPROM_USB_CTRLREQUEST_VENDOR_WRITEDFT 21 /* endpoint addresses for command and fingerprint data */ #define FU_SYNAPROM_USB_REQUEST_EP 0x01 #define FU_SYNAPROM_USB_REPLY_EP 0x81 #define FU_SYNAPROM_USB_FINGERPRINT_EP 0x82 #define FU_SYNAPROM_USB_INTERRUPT_EP 0x83 /* the following bits describe security options in ** FuStructSynapromReplyGetVersion::security[1] bit-field */ #define FU_SYNAPROM_SECURITY1_PROD_SENSOR (1 << 5) G_DEFINE_TYPE(FuSynapromDevice, fu_synaprom_device, FU_TYPE_USB_DEVICE) gboolean fu_synaprom_device_cmd_send(FuSynapromDevice *self, GByteArray *request, GByteArray *reply, FuProgress *progress, guint timeout_ms, GError **error) { gsize actual_len = 0; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_NO_PROFILE); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 25, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 75, NULL); fu_dump_full(G_LOG_DOMAIN, "REQST", request->data, request->len, 16, FU_DUMP_FLAGS_SHOW_ADDRESSES); if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), FU_SYNAPROM_USB_REQUEST_EP, request->data, request->len, &actual_len, timeout_ms, NULL, error)) { g_prefix_error(error, "failed to request: "); return FALSE; } if (actual_len < request->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "only sent 0x%04x of 0x%04x", (guint)actual_len, request->len); return FALSE; } fu_progress_step_done(progress); if (!fu_usb_device_bulk_transfer(FU_USB_DEVICE(self), FU_SYNAPROM_USB_REPLY_EP, reply->data, reply->len, NULL, /* allowed to return short read */ timeout_ms, NULL, error)) { g_prefix_error(error, "failed to reply: "); return FALSE; } fu_dump_full(G_LOG_DOMAIN, "REPLY", reply->data, actual_len, 16, FU_DUMP_FLAGS_SHOW_ADDRESSES); fu_progress_step_done(progress); /* parse as FuSynapromReplyGeneric */ if (reply->len >= FU_STRUCT_SYNAPROM_REPLY_GENERIC_SIZE) { g_autoptr(FuStructSynapromReplyGeneric) st_reply = NULL; st_reply = fu_struct_synaprom_reply_generic_parse(reply->data, reply->len, 0x0, error); if (st_reply == NULL) return FALSE; return fu_synaprom_error_from_status( fu_struct_synaprom_reply_generic_get_status(st_reply), error); } /* success */ return TRUE; } guint32 fu_synaprom_device_get_product_type(FuSynapromDevice *self) { return self->product_type; } void fu_synaprom_device_set_version(FuSynapromDevice *self, guint8 vmajor, guint8 vminor, guint32 buildnum) { g_autofree gchar *str = NULL; /* We decide to skip 10.02.xxxxxx firmware, so we force the minor version from 0x02 ** to 0x01 to make the devices with 0x02 minor version firmware allow to be updated ** back to minor version 0x01. */ if (vmajor == 0x0a && vminor == 0x02) { g_debug("quirking vminor from %02x to 01", vminor); vminor = 0x01; } /* set display version */ str = g_strdup_printf("%02u.%02u.%u", vmajor, vminor, buildnum); fu_device_set_version(FU_DEVICE(self), str); /* we need this for checking the firmware compatibility later */ self->vmajor = vmajor; self->vminor = vminor; } static void fu_synaprom_device_set_serial_number(FuSynapromDevice *self, guint64 serial_number) { g_autofree gchar *str = NULL; str = g_strdup_printf("%" G_GUINT64_FORMAT, serial_number); fu_device_set_serial(FU_DEVICE(self), str); } static gboolean fu_synaprom_device_setup(FuDevice *device, GError **error) { FuSynapromDevice *self = FU_SYNAPROM_DEVICE(device); guint32 product; guint64 serial_number = 0; g_autoptr(FuStructSynapromReplyGetVersion) st_reply = NULL; g_autoptr(FuStructSynapromRequest) st_request = fu_struct_synaprom_request_new(); g_autoptr(GByteArray) reply = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_synaprom_device_parent_class)->setup(device, error)) return FALSE; /* get version */ fu_struct_synaprom_request_set_cmd(st_request, FU_SYNAPROM_CMD_GET_VERSION); reply = fu_synaprom_reply_new(FU_STRUCT_SYNAPROM_REPLY_GET_VERSION_SIZE); if (!fu_synaprom_device_cmd_send(self, st_request, reply, progress, 250, error)) { g_prefix_error(error, "failed to get version: "); return FALSE; } st_reply = fu_struct_synaprom_reply_get_version_parse(reply->data, reply->len, 0x0, error); if (st_reply == NULL) return FALSE; fu_synaprom_device_set_version(self, fu_struct_synaprom_reply_get_version_get_vmajor(st_reply), fu_struct_synaprom_reply_get_version_get_vminor(st_reply), fu_struct_synaprom_reply_get_version_get_buildnum(st_reply)); /* get 48-bit serial number */ memcpy(&serial_number, /* nocheck:blocked */ fu_struct_synaprom_reply_get_version_get_serial_number(st_reply, NULL), FU_STRUCT_SYNAPROM_REPLY_GET_VERSION_SIZE_SERIAL_NUMBER); fu_synaprom_device_set_serial_number(self, serial_number); /* check device type */ product = fu_struct_synaprom_reply_get_version_get_product(st_reply); if (product == FU_SYNAPROM_PRODUCT_PROMETHEUS || product == FU_SYNAPROM_PRODUCT_TRITON) { fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); } else if (product == FU_SYNAPROM_PRODUCT_PROMETHEUSPBL || product == FU_SYNAPROM_PRODUCT_PROMETHEUSMSBL || product == FU_SYNAPROM_PRODUCT_TRITONPBL || product == FU_SYNAPROM_PRODUCT_TRITONMSBL) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device %u is not supported by this plugin", product); return FALSE; } if (product == FU_SYNAPROM_PRODUCT_TRITON || product == FU_SYNAPROM_PRODUCT_TRITONPBL || product == FU_SYNAPROM_PRODUCT_TRITONMSBL) self->product_type = FU_SYNAPROM_PRODUCT_TYPE_TRITON; else self->product_type = FU_SYNAPROM_PRODUCT_TYPE_PROMETHEUS; /* add updatable config child, if this is a production sensor */ if (fu_device_get_children(device)->len == 0 && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER) && fu_struct_synaprom_reply_get_version_get_security1(st_reply) & FU_SYNAPROM_SECURITY1_PROD_SENSOR) { g_autoptr(FuSynapromConfig) cfg = fu_synaprom_config_new(self); fu_device_add_child(FU_DEVICE(device), FU_DEVICE(cfg)); } /* success */ return TRUE; } FuFirmware * fu_synaprom_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { guint32 product_id; g_autoptr(FuFirmware) firmware = fu_synaprom_firmware_new(); FuSynapromDevice *self = FU_SYNAPROM_DEVICE(device); if (self->product_type == FU_SYNAPROM_PRODUCT_TYPE_TRITON) { if (!fu_synaprom_firmware_set_signature_size(FU_SYNAPROM_FIRMWARE(firmware), FU_SYNAPROM_FIRMWARE_TRITON_SIGSIZE)) return NULL; } /* check the update header product and version */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; product_id = fu_synaprom_firmware_get_product_id(FU_SYNAPROM_FIRMWARE(firmware)); if (product_id != FU_SYNAPROM_PRODUCT_PROMETHEUS && product_id != FU_SYNAPROM_PRODUCT_TRITON) { if (flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_VID_PID) { g_warning("MFW metadata not compatible, " "got 0x%02x expected 0x%02x or 0x%02x", product_id, (guint)FU_SYNAPROM_PRODUCT_PROMETHEUS, (guint)FU_SYNAPROM_PRODUCT_TRITON); } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "MFW metadata not compatible, " "got 0x%02x expected 0x%02x or 0x%02x", product_id, (guint)FU_SYNAPROM_PRODUCT_PROMETHEUS, (guint)FU_SYNAPROM_PRODUCT_TRITON); return NULL; } } /* success */ return g_steal_pointer(&firmware); } static gboolean fu_synaprom_device_write_chunks(FuSynapromDevice *self, GPtrArray *chunks, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { GByteArray *chunk = g_ptr_array_index(chunks, i); g_autoptr(FuStructSynapromRequest) st_request = fu_struct_synaprom_request_new(); g_autoptr(GByteArray) reply = NULL; /* patch */ fu_struct_synaprom_request_set_cmd(st_request, FU_SYNAPROM_CMD_BOOTLDR_PATCH); g_byte_array_append(st_request, chunk->data, chunk->len); reply = fu_synaprom_reply_new(FU_STRUCT_SYNAPROM_REPLY_GENERIC_SIZE); if (!fu_synaprom_device_cmd_send(self, st_request, reply, fu_progress_get_child(progress), 20000, error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } gboolean fu_synaprom_device_write_fw(FuSynapromDevice *self, GBytes *fw, FuProgress *progress, GError **error) { const guint8 *buf; gsize bufsz = 0; gsize offset = 0; g_autoptr(GPtrArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 99, NULL); /* collect chunks */ buf = g_bytes_get_data(fw, &bufsz); chunks = g_ptr_array_new_with_free_func((GDestroyNotify)g_byte_array_unref); while (offset != bufsz) { guint32 chunksz = 0; g_autofree guint8 *chunkbuf = NULL; g_autoptr(GByteArray) chunk = g_byte_array_new(); /* get chunk size */ if (!fu_memread_uint32_safe(buf, bufsz, offset, &chunksz, G_LITTLE_ENDIAN, error)) return FALSE; offset += sizeof(guint32); /* read out chunk */ chunkbuf = g_malloc0(chunksz); if (!fu_memcpy_safe(chunkbuf, chunksz, 0x0, /* dst */ buf, bufsz, offset, /* src */ chunksz, error)) return FALSE; offset += chunksz; /* add chunk */ g_byte_array_append(chunk, chunkbuf, chunksz); g_ptr_array_add(chunks, g_steal_pointer(&chunk)); } fu_progress_step_done(progress); /* write chunks */ if (!fu_synaprom_device_write_chunks(self, chunks, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* success! */ return TRUE; } static gboolean fu_synaprom_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuSynapromDevice *self = FU_SYNAPROM_DEVICE(device); g_autoptr(GBytes) fw = NULL; /* get default image */ fw = fu_firmware_get_image_by_id_bytes(firmware, "mfw-update-payload", error); if (fw == NULL) return FALSE; return fu_synaprom_device_write_fw(self, fw, progress, error); } static gboolean fu_synaprom_device_attach(FuDevice *device, FuProgress *progress, GError **error) { gboolean ret; gsize actual_len = 0; guint8 data[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; /* sanity check */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in runtime mode, skipping"); return TRUE; } ret = fu_usb_device_control_transfer(FU_USB_DEVICE(device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, FU_SYNAPROM_USB_CTRLREQUEST_VENDOR_WRITEDFT, 0x0000, 0x0000, data, sizeof(data), &actual_len, 2000, NULL, error); if (!ret) return FALSE; if (actual_len != sizeof(data)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "only sent 0x%04x of 0x%04x", (guint)actual_len, (guint)sizeof(data)); return FALSE; } if (!fu_usb_device_reset(FU_USB_DEVICE(device), error)) { g_prefix_error(error, "failed to force-reset device: "); return FALSE; } fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); return TRUE; } static gboolean fu_synaprom_device_detach(FuDevice *device, FuProgress *progress, GError **error) { gsize actual_len = 0; guint8 data[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00}; /* sanity check */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in bootloader mode, skipping"); return TRUE; } if (!fu_usb_device_control_transfer(FU_USB_DEVICE(device), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, FU_SYNAPROM_USB_CTRLREQUEST_VENDOR_WRITEDFT, 0x0000, 0x0000, data, sizeof(data), &actual_len, 2000, NULL, error)) return FALSE; if (actual_len != sizeof(data)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "only sent 0x%04x of 0x%04x", (guint)actual_len, (guint)sizeof(data)); return FALSE; } fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_RESTART); if (!fu_usb_device_reset(FU_USB_DEVICE(device), error)) { g_prefix_error(error, "failed to force-reset device: "); return FALSE; } fu_device_add_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); return TRUE; } static void fu_synaprom_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 96, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_synaprom_device_init(FuSynapromDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_RETRY_OPEN); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_add_protocol(FU_DEVICE(self), "com.synaptics.prometheus"); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_set_name(FU_DEVICE(self), "Prometheus"); fu_device_set_summary(FU_DEVICE(self), "Fingerprint reader"); fu_device_set_vendor(FU_DEVICE(self), "Synaptics"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_AUTH_FINGERPRINT); fu_usb_device_add_interface(FU_USB_DEVICE(self), 0x0); } static void fu_synaprom_device_class_init(FuSynapromDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_synaprom_device_write_firmware; device_class->prepare_firmware = fu_synaprom_device_prepare_firmware; device_class->setup = fu_synaprom_device_setup; device_class->reload = fu_synaprom_device_setup; device_class->attach = fu_synaprom_device_attach; device_class->detach = fu_synaprom_device_detach; device_class->set_progress = fu_synaprom_device_set_progress; } FuSynapromDevice * fu_synaprom_device_new(FuUsbDevice *device) { FuSynapromDevice *self; self = g_object_new(FU_TYPE_SYNAPROM_DEVICE, NULL); if (device != NULL) { fu_device_incorporate(FU_DEVICE(self), FU_DEVICE(device), FU_DEVICE_INCORPORATE_FLAG_ALL); } return FU_SYNAPROM_DEVICE(self); } fwupd-2.0.10/plugins/synaptics-prometheus/fu-synaprom-device.h000066400000000000000000000022011501337203100244620ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * Copyright 2019 Synaptics Inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_SYNAPROM_DEVICE (fu_synaprom_device_get_type()) G_DECLARE_FINAL_TYPE(FuSynapromDevice, fu_synaprom_device, FU, SYNAPROM_DEVICE, FuUsbDevice) FuSynapromDevice * fu_synaprom_device_new(FuUsbDevice *device); gboolean fu_synaprom_device_cmd_send(FuSynapromDevice *device, GByteArray *request, GByteArray *reply, FuProgress *progress, guint timeout_ms, GError **error); gboolean fu_synaprom_device_write_fw(FuSynapromDevice *self, GBytes *fw, FuProgress *progress, GError **error); /* for self tests */ void fu_synaprom_device_set_version(FuSynapromDevice *self, guint8 vmajor, guint8 vminor, guint32 buildnum); FuFirmware * fu_synaprom_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error); guint32 fu_synaprom_device_get_product_type(FuSynapromDevice *self); fwupd-2.0.10/plugins/synaptics-prometheus/fu-synaprom-firmware.c000066400000000000000000000143271501337203100250460ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * Copyright 2019 Synaptics Inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-synaprom-firmware.h" #include "fu-synaprom-struct.h" struct _FuSynapromFirmware { FuFirmware parent_instance; guint32 product_id; guint32 signature_size; }; G_DEFINE_TYPE(FuSynapromFirmware, fu_synaprom_firmware, FU_TYPE_FIRMWARE) /* use only first 12 bit of 16 bits as tag value */ #define FU_SYNAPROM_FIRMWARE_TAG_MAX 0xfff0 #define FU_SYNAPROM_FIRMWARE_COUNT_MAX 64 guint32 fu_synaprom_firmware_get_product_id(FuSynapromFirmware *self) { g_return_val_if_fail(FU_IS_SYNAPROM_FIRMWARE(self), 0x0); return self->product_id; } gboolean fu_synaprom_firmware_set_signature_size(FuSynapromFirmware *self, guint32 signature_size) { g_return_val_if_fail(FU_IS_SYNAPROM_FIRMWARE(self), FALSE); self->signature_size = signature_size; return TRUE; } static void fu_synaprom_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuSynapromFirmware *self = FU_SYNAPROM_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "product_id", self->product_id); } static gboolean fu_synaprom_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuSynapromFirmware *self = FU_SYNAPROM_FIRMWARE(firmware); gsize offset = 0; gsize streamsz = 0; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz < self->signature_size + FU_STRUCT_SYNAPROM_HDR_SIZE) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "blob is too small to be firmware"); return FALSE; } streamsz -= self->signature_size; /* parse each chunk */ while (offset < streamsz) { guint32 hdrsz; guint32 tag; g_autoptr(FuFirmware) img = fu_firmware_new(); g_autoptr(FuFirmware) img_old = NULL; g_autoptr(GByteArray) st_hdr = NULL; g_autoptr(GInputStream) partial_stream = NULL; /* verify item header */ st_hdr = fu_struct_synaprom_hdr_parse_stream(stream, offset, error); if (st_hdr == NULL) return FALSE; tag = fu_struct_synaprom_hdr_get_tag(st_hdr); if (tag >= FU_SYNAPROM_FIRMWARE_TAG_MAX) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "tag 0x%04x is too large", tag); return FALSE; } /* sanity check */ img_old = fu_firmware_get_image_by_idx(firmware, tag, NULL); if (img_old != NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "tag 0x%04x already present in image", tag); return FALSE; } hdrsz = fu_struct_synaprom_hdr_get_bufsz(st_hdr); if (hdrsz == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "empty header for tag 0x%04x", tag); return FALSE; } offset += st_hdr->len; partial_stream = fu_partial_input_stream_new(stream, offset, hdrsz, error); if (partial_stream == NULL) return FALSE; if (!fu_firmware_parse_stream(img, partial_stream, 0x0, flags, error)) return FALSE; g_debug("adding 0x%04x (%s) with size 0x%04x", tag, fu_synaprom_firmware_tag_to_string(tag), hdrsz); fu_firmware_set_idx(img, tag); fu_firmware_set_id(img, fu_synaprom_firmware_tag_to_string(tag)); if (!fu_firmware_add_image_full(firmware, img, error)) return FALSE; /* metadata */ if (tag == FU_SYNAPROM_FIRMWARE_TAG_MFW_UPDATE_HEADER) { g_autofree gchar *version = NULL; g_autoptr(GByteArray) st_mfw = NULL; st_mfw = fu_struct_synaprom_mfw_hdr_parse_stream(stream, offset, error); if (st_mfw == NULL) return FALSE; self->product_id = fu_struct_synaprom_mfw_hdr_get_product(st_mfw); version = g_strdup_printf("%u.%u", fu_struct_synaprom_mfw_hdr_get_vmajor(st_mfw), fu_struct_synaprom_mfw_hdr_get_vminor(st_mfw)); fu_firmware_set_version(firmware, version); } /* next item */ offset += hdrsz; } return TRUE; } static GByteArray * fu_synaprom_firmware_write(FuFirmware *firmware, GError **error) { FuSynapromFirmware *self = FU_SYNAPROM_FIRMWARE(firmware); g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GByteArray) st_hdr = fu_struct_synaprom_hdr_new(); g_autoptr(GByteArray) st_mfw = fu_struct_synaprom_mfw_hdr_new(); g_autoptr(GBytes) payload = NULL; /* add header */ fu_struct_synaprom_hdr_set_tag(st_hdr, FU_SYNAPROM_FIRMWARE_TAG_MFW_UPDATE_HEADER); fu_struct_synaprom_hdr_set_bufsz(st_hdr, st_mfw->len); g_byte_array_append(buf, st_hdr->data, st_hdr->len); fu_struct_synaprom_mfw_hdr_set_product(st_mfw, self->product_id); g_byte_array_append(buf, st_mfw->data, st_mfw->len); /* add payload */ payload = fu_firmware_get_bytes_with_patches(firmware, error); if (payload == NULL) return NULL; fu_struct_synaprom_hdr_set_tag(st_hdr, FU_SYNAPROM_FIRMWARE_TAG_MFW_UPDATE_PAYLOAD); fu_struct_synaprom_hdr_set_bufsz(st_hdr, g_bytes_get_size(payload)); g_byte_array_append(buf, st_hdr->data, st_hdr->len); fu_byte_array_append_bytes(buf, payload); /* add signature */ for (guint i = 0; i < self->signature_size; i++) fu_byte_array_append_uint8(buf, 0xff); return g_steal_pointer(&buf); } static gboolean fu_synaprom_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { FuSynapromFirmware *self = FU_SYNAPROM_FIRMWARE(firmware); guint64 tmp; /* simple properties */ tmp = xb_node_query_text_as_uint(n, "product_id", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT32) self->product_id = tmp; /* success */ return TRUE; } static void fu_synaprom_firmware_init(FuSynapromFirmware *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_VID_PID); fu_firmware_set_images_max(FU_FIRMWARE(self), FU_SYNAPROM_FIRMWARE_COUNT_MAX); self->signature_size = FU_SYNAPROM_FIRMWARE_PROMETHEUS_SIGSIZE; } static void fu_synaprom_firmware_class_init(FuSynapromFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_synaprom_firmware_parse; firmware_class->write = fu_synaprom_firmware_write; firmware_class->export = fu_synaprom_firmware_export; firmware_class->build = fu_synaprom_firmware_build; } FuFirmware * fu_synaprom_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_SYNAPROM_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/synaptics-prometheus/fu-synaprom-firmware.h000066400000000000000000000012411501337203100250420ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * Copyright 2019 Synaptics Inc * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_SYNAPROM_FIRMWARE (fu_synaprom_firmware_get_type()) #define FU_SYNAPROM_FIRMWARE_PROMETHEUS_SIGSIZE 0x100 #define FU_SYNAPROM_FIRMWARE_TRITON_SIGSIZE 0x180 G_DECLARE_FINAL_TYPE(FuSynapromFirmware, fu_synaprom_firmware, FU, SYNAPROM_FIRMWARE, FuFirmware) FuFirmware * fu_synaprom_firmware_new(void); guint32 fu_synaprom_firmware_get_product_id(FuSynapromFirmware *self); gboolean fu_synaprom_firmware_set_signature_size(FuSynapromFirmware *self, guint32 signature_size); fwupd-2.0.10/plugins/synaptics-prometheus/fu-synaprom-plugin.c000066400000000000000000000023401501337203100245200ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-synaprom-config.h" #include "fu-synaprom-device.h" #include "fu-synaprom-firmware.h" #include "fu-synaprom-plugin.h" struct _FuSynapromPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuSynapromPlugin, fu_synaprom_plugin, FU_TYPE_PLUGIN) static void fu_synaprom_plugin_init(FuSynapromPlugin *self) { } static void fu_synaprom_plugin_object_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_set_name(plugin, "synaptics_prometheus"); } static void fu_synaprom_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_set_device_gtype_default(plugin, FU_TYPE_SYNAPROM_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_SYNAPROM_CONFIG); /* for coverage */ fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_SYNAPROM_FIRMWARE); } static void fu_synaprom_plugin_class_init(FuSynapromPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->constructed = fu_synaprom_plugin_object_constructed; plugin_class->constructed = fu_synaprom_plugin_constructed; } fwupd-2.0.10/plugins/synaptics-prometheus/fu-synaprom-plugin.h000066400000000000000000000003621501337203100245270ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuSynapromPlugin, fu_synaprom_plugin, FU, SYNAPROM_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/synaptics-prometheus/fu-synaprom.rs000066400000000000000000000072671501337203100234430ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[repr(u8)] enum FuSynapromCmd { GetVersion = 0x01, BootldrPatch = 0x7d, IotaFind = 0x8e, } #[derive(New)] #[repr(C, packed)] struct FuStructSynapromRequest { cmd: FuSynapromCmd, } #[derive(Parse)] #[repr(C, packed)] struct FuStructSynapromReplyGeneric { status: u16le, } #[repr(u8)] enum FuSynapromProduct { Prometheus = 65, // b1422 Prometheuspbl = 66, Prometheusmsbl = 67, Triton = 69, Tritonpbl = 70, Tritonmsbl = 71, } #[derive(Parse)] #[repr(C, packed)] struct FuStructSynapromReplyGetVersion { status: u16le, buildtime: u32le, buildnum: u32le, vmajor: u8, vminor: u8, target: u8, // e.g. VCSFW_TARGET_ROM product: FuSynapromProduct, siliconrev: u8, formalrel: u8, // boolean: non-zero -> formal release platform: u8, // PCB revision patch: u8, serial_number: [u8; 6], // 48-bit */ security0: u8, // byte of OTP */ security1: u8, // byte of OTP */ patchsig: u32le, // opaque patch signature */ iface: u8, // interface type otpsig: [u8; 3], // OTP Patch Signature otpspare1: u16le, // OTP spare space reserved: u8, device_type: u8, } enum FuSynapromResult { Ok = 0, GenOperationCanceled = 103, GenInvalid = 110, GenBadParam = 111, GenNullPointer = 112, GenUnexpectedFormat = 114, GenTimeout = 117, GenObjectDoesntExist = 118, GenError = 119, SensorMalfunctioned = 202, SysOutOfMemory = 602, } enum FuSynapromProductType { Denali, Hayes, Shasta, Steller, Whitney, Prometheus, PacificPeak, Morgan, Ox6101, Triton, } #[derive(New, ParseStream, Default)] #[repr(C, packed)] struct FuStructSynapromMfwHdr { product: u32le, id: u32le = 0xFF, // MFW unique id used for compat verification buildtime: u32le = 0xFF, // unix-style buildnum: u32le = 0xFF, vmajor: u8 = 10, // major version vminor: u8 = 1, // minor version unused: [u8; 6], } #[derive(ToString)] #[repr(u16le)] enum FuSynapromFirmwareTag { MfwUpdateHeader = 0x0001, MfwUpdatePayload = 0x0002, CfgUpdateHeader = 0x0003, CfgUpdatePayload = 0x0004, } #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructSynapromHdr { tag: FuSynapromFirmwareTag, bufsz: u32le, } #[derive(ParseStream, Default)] #[repr(C, packed)] struct FuStructSynapromCfgHdr { product: u32le = 65, // Prometheus (b1422) id1: u32le, id2: u32le, version: u16le, _unused: [u8; 2], } #[derive(Parse)] #[repr(C, packed)] struct FuStructSynapromIotaConfigVersion { config_id1: u32le, // YYMMDD config_id2: u32le, // HHMMSS version: u16le, _unused: [u16; 3], } #[derive(Parse)] #[repr(C, packed)] struct FuStructSynapromReplyIotaFindHdr { status: u16le, fullsize: u32le, nbytes: u16le, itype: u16le, } // Iotas can exceed the size of available RAM in the part: to allow the host to read them the // IOTA_FIND command supports transferring iotas with multiple commands #[derive(New, Getters)] #[repr(C, packed)] struct FuStructSynapromCmdIotaFind { itype: u16le, // type of iotas to find flags: u16le, maxniotas: u8, // maximum number of iotas to return, 0 = unlimited firstidx: u8, // first index of iotas to return _dummy: [u8; 2], offset: u32le, // byte offset of data to return nbytes: u32le, // maximum number of bytes to return } fwupd-2.0.10/plugins/synaptics-prometheus/meson.build000066400000000000000000000027201501337203100227440ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginSynapticsPrometheus"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('synaptics-prometheus.quirk') plugin_builtin_synaprom = static_library('fu_plugin_synaprom', rustgen.process( 'fu-synaprom.rs', # fuzzing ), sources: [ 'fu-synaprom-plugin.c', 'fu-synaprom-common.c', 'fu-synaprom-config.c', 'fu-synaprom-device.c', 'fu-synaprom-firmware.c', # fuzzing ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) plugin_builtins += plugin_builtin_synaprom if get_option('tests') install_data(['tests/synaptics-prometheus.builder.xml'], install_dir: join_paths(installed_test_datadir, 'tests')) env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) e = executable( 'synaptics-prometheus-self-test', sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: plugin_deps, link_with: [ plugin_libs, plugin_builtin_synaprom, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ cargs, '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('synaptics-prometheus-self-test', e, env: env) # added to installed-tests endif device_tests += files('tests/synaptics-prometheus.json') fwupd-2.0.10/plugins/synaptics-prometheus/synaptics-prometheus.quirk000066400000000000000000000030471501337203100260700ustar00rootroot00000000000000[USB\VID_06CB&PID_00A9] Plugin = synaptics_prometheus InstallDuration = 2 [USB\VID_06CB&PID_00BD] Plugin = synaptics_prometheus InstallDuration = 2 [USB\VID_06CB&PID_00DF] Plugin = synaptics_prometheus InstallDuration = 2 [USB\VID_06CB&PID_00E9] Plugin = synaptics_prometheus InstallDuration = 2 [USB\VID_06CB&PID_00C2] Plugin = synaptics_prometheus InstallDuration = 2 [USB\VID_06CB&PID_00C6] Plugin = synaptics_prometheus InstallDuration = 2 [USB\VID_06CB&PID_00F9] Plugin = synaptics_prometheus InstallDuration = 2 [USB\VID_06CB&PID_00FC] Plugin = synaptics_prometheus InstallDuration = 2 [USB\VID_06CB&PID_00D8] Plugin = synaptics_prometheus InstallDuration = 2 [USB\VID_06CB&PID_00F0] Plugin = synaptics_prometheus InstallDuration = 2 [USB\VID_06CB&PID_0103] Plugin = synaptics_prometheus InstallDuration = 2 [USB\VID_06CB&PID_0123] Plugin = synaptics_prometheus InstallDuration = 2 [USB\VID_06CB&PID_0126] Plugin = synaptics_prometheus InstallDuration = 2 [USB\VID_06CB&PID_0129] Plugin = synaptics_prometheus InstallDuration = 2 [USB\VID_06CB&PID_0100] Plugin = synaptics_prometheus InstallDuration = 2 [USB\VID_06CB&PID_0168] Plugin = synaptics_prometheus InstallDuration = 2 [USB\VID_06CB&PID_015F] Plugin = synaptics_prometheus InstallDuration = 2 [USB\VID_06CB&PID_0104] Plugin = synaptics_prometheus InstallDuration = 2 [USB\VID_06CB&PID_0160] Plugin = synaptics_prometheus InstallDuration = 2 [USB\VID_06CB&PID_0106] Plugin = synaptics_prometheus InstallDuration = 2 [USB\VID_06CB&PID_0108] Plugin = synaptics_prometheus InstallDuration = 2 fwupd-2.0.10/plugins/synaptics-prometheus/tests/000077500000000000000000000000001501337203100217435ustar00rootroot00000000000000fwupd-2.0.10/plugins/synaptics-prometheus/tests/synaptics-prometheus.builder.xml000066400000000000000000000002331501337203100303160ustar00rootroot00000000000000 1.2 0x42 aGVsbG8gd29ybGQ= fwupd-2.0.10/plugins/synaptics-prometheus/tests/synaptics-prometheus.json000066400000000000000000000010121501337203100270360ustar00rootroot00000000000000{ "name": "Prometheus Fingerprint Reader", "interactive": false, "steps": [ { "url": "7c260a13ea6df444f7a1fa8fa2bf431876a8c7203b6aeac371564aad30756ff1-Synaptics-Prometheus-10.01.3121519.cab", "emulation-url": "73877ed7c8c15dac6adf04db32bd5e9f1e702229cfdb266fe98855ee53929cd1-Synaptics-Prometheus-10.01.3121519.zip", "components": [ { "version": "10.01.3121519", "guids": [ "8088f861-6318-5b1e-9ce4-fbddbedb09ac" ] } ] } ] } fwupd-2.0.10/plugins/synaptics-rmi/000077500000000000000000000000001501337203100171755ustar00rootroot00000000000000fwupd-2.0.10/plugins/synaptics-rmi/README.md000066400000000000000000000025061501337203100204570ustar00rootroot00000000000000--- title: Plugin: Synaptics RMI4 --- ## Introduction This plugin updates integrated Synaptics RMI4 devices, typically touchpads. ## GUID Generation The HID DeviceInstanceId values are used, e.g. `HIDRAW\VEN_06CB&DEV_4875`. These devices also use custom GUID values constructed using the board ID, e.g. * `SYNAPTICS_RMI\TM3038-002` * `SYNAPTICS_RMI\TM3038` ## Update Behavior The device usually presents in HID mode, and the firmware is written to the device by switching to a SERIO mode where the touchpad is nonfunctional. Once complete the device is reset to get out of SERIO mode and to load the new firmware version. ## Vendor ID Security The vendor ID is set from the udev vendor, in this instance set to `HIDRAW:0x06CB` ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a proprietary (but documented) file format. This plugin supports the following protocol ID: * `com.synaptics.rmi` ## External Interface Access This plugin requires ioctl access to `HIDIOCSFEATURE` and `HIDIOCGFEATURE`. ## Version Considerations This plugin has been available since fwupd version `1.3.3`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * David Chiu: @blueue * Vincent Huang: @vhuag fwupd-2.0.10/plugins/synaptics-rmi/fu-self-test.c000066400000000000000000000062051501337203100216620ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-synaptics-rmi-firmware.h" static void fu_synaptics_rmi_firmware_0x_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autofree gchar *csum1 = NULL; g_autofree gchar *csum2 = NULL; g_autofree gchar *xml_out = NULL; g_autofree gchar *xml_src = NULL; g_autoptr(FuFirmware) firmware1 = fu_synaptics_rmi_firmware_new(); g_autoptr(FuFirmware) firmware2 = fu_synaptics_rmi_firmware_new(); g_autoptr(GError) error = NULL; /* build and write */ filename = g_test_build_filename(G_TEST_DIST, "tests", "synaptics-rmi-0x.builder.xml", NULL); ret = g_file_get_contents(filename, &xml_src, NULL, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_firmware_build_from_xml(firmware1, xml_src, &error); g_assert_no_error(error); g_assert_true(ret); csum1 = fu_firmware_get_checksum(firmware1, G_CHECKSUM_SHA1, &error); g_assert_no_error(error); g_assert_cmpstr(csum1, ==, "8b097c034028a69e6416bcc39f312e2fa9247381"); /* ensure we can round-trip */ xml_out = fu_firmware_export_to_xml(firmware1, FU_FIRMWARE_EXPORT_FLAG_NONE, &error); g_assert_no_error(error); ret = fu_firmware_build_from_xml(firmware2, xml_out, &error); g_assert_no_error(error); g_assert_true(ret); csum2 = fu_firmware_get_checksum(firmware2, G_CHECKSUM_SHA1, &error); g_assert_cmpstr(csum1, ==, csum2); } static void fu_synaptics_rmi_firmware_10_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autofree gchar *csum1 = NULL; g_autofree gchar *csum2 = NULL; g_autofree gchar *xml_out = NULL; g_autofree gchar *xml_src = NULL; g_autoptr(FuFirmware) firmware1 = fu_synaptics_rmi_firmware_new(); g_autoptr(FuFirmware) firmware2 = fu_synaptics_rmi_firmware_new(); g_autoptr(GError) error = NULL; /* build and write */ filename = g_test_build_filename(G_TEST_DIST, "tests", "synaptics-rmi-10.builder.xml", NULL); ret = g_file_get_contents(filename, &xml_src, NULL, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_firmware_build_from_xml(firmware1, xml_src, &error); g_assert_no_error(error); g_assert_true(ret); csum1 = fu_firmware_get_checksum(firmware1, G_CHECKSUM_SHA1, &error); g_assert_no_error(error); g_assert_cmpstr(csum1, ==, "bd85539bb100e5bd6debb00b06b5a7e7fa9bd030"); /* ensure we can round-trip */ xml_out = fu_firmware_export_to_xml(firmware1, FU_FIRMWARE_EXPORT_FLAG_NONE, &error); g_assert_no_error(error); ret = fu_firmware_build_from_xml(firmware2, xml_out, &error); g_assert_no_error(error); g_assert_true(ret); csum2 = fu_firmware_get_checksum(firmware2, G_CHECKSUM_SHA1, &error); g_assert_cmpstr(csum1, ==, csum2); } int main(int argc, char **argv) { (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); /* tests go here */ g_test_add_func("/synaptics-rmi/firmware{0x}", fu_synaptics_rmi_firmware_0x_func); g_test_add_func("/synaptics-rmi/firmware{10}", fu_synaptics_rmi_firmware_10_func); return g_test_run(); } fwupd-2.0.10/plugins/synaptics-rmi/fu-synaptics-rmi-common.c000066400000000000000000000113621501337203100240440ustar00rootroot00000000000000/* * Copyright 2012 Andrew Duggan * Copyright 2012 Synaptics Inc. * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include #include #ifdef HAVE_GNUTLS #include #include #endif #include "fu-synaptics-rmi-common.h" #define RMI_FUNCTION_QUERY_OFFSET 0 #define RMI_FUNCTION_COMMAND_OFFSET 1 #define RMI_FUNCTION_CONTROL_OFFSET 2 #define RMI_FUNCTION_DATA_OFFSET 3 #define RMI_FUNCTION_INTERRUPT_SOURCES_OFFSET 4 #define RMI_FUNCTION_NUMBER 5 #define RMI_FUNCTION_VERSION_MASK 0x60 #define RMI_FUNCTION_INTERRUPT_SOURCES_MASK 0x7 #ifdef HAVE_GNUTLS typedef guchar gnutls_data_t; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTOPTR_CLEANUP_FUNC(gnutls_data_t, gnutls_free) G_DEFINE_AUTO_CLEANUP_FREE_FUNC(gnutls_pubkey_t, gnutls_pubkey_deinit, NULL) #pragma clang diagnostic pop #endif guint32 fu_synaptics_rmi_generate_checksum(const guint8 *data, gsize len) { guint32 lsw = 0xffff; guint32 msw = 0xffff; for (gsize i = 0; i < len / 2; i++) { lsw += fu_memread_uint16(&data[i * 2], G_LITTLE_ENDIAN); msw += lsw; lsw = (lsw & 0xffff) + (lsw >> 16); msw = (msw & 0xffff) + (msw >> 16); } return msw << 16 | lsw; } FuSynapticsRmiFunction * fu_synaptics_rmi_function_parse(GByteArray *buf, guint16 page_base, guint interrupt_count, GError **error) { FuSynapticsRmiFunction *func; guint8 interrupt_offset; const guint8 *data = buf->data; /* not expected */ if (buf->len != RMI_DEVICE_PDT_ENTRY_SIZE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "PDT entry buffer invalid size %u != %i", buf->len, RMI_DEVICE_PDT_ENTRY_SIZE); return NULL; } func = g_new0(FuSynapticsRmiFunction, 1); func->query_base = data[RMI_FUNCTION_QUERY_OFFSET] + page_base; func->command_base = data[RMI_FUNCTION_COMMAND_OFFSET] + page_base; func->control_base = data[RMI_FUNCTION_CONTROL_OFFSET] + page_base; func->data_base = data[RMI_FUNCTION_DATA_OFFSET] + page_base; func->interrupt_source_count = data[RMI_FUNCTION_INTERRUPT_SOURCES_OFFSET] & RMI_FUNCTION_INTERRUPT_SOURCES_MASK; func->function_number = data[RMI_FUNCTION_NUMBER]; func->function_version = (data[RMI_FUNCTION_INTERRUPT_SOURCES_OFFSET] & RMI_FUNCTION_VERSION_MASK) >> 5; if (func->interrupt_source_count > 0) { func->interrupt_reg_num = (interrupt_count + 8) / 8 - 1; /* set an enable bit for each data source */ interrupt_offset = interrupt_count % 8; func->interrupt_mask = 0; for (guint i = interrupt_offset; i < (func->interrupt_source_count + interrupt_offset); i++) FU_BIT_SET(func->interrupt_mask, i); } return func; } gboolean fu_synaptics_rmi_device_writeln(const gchar *fn, const gchar *buf, GError **error) { g_autoptr(FuIOChannel) io = NULL; io = fu_io_channel_new_file(fn, FU_IO_CHANNEL_OPEN_FLAG_WRITE, error); if (io == NULL) return FALSE; return fu_io_channel_write_raw(io, (const guint8 *)buf, strlen(buf), 1000, FU_IO_CHANNEL_FLAG_NONE, error); } gboolean fu_synaptics_rmi_verify_sha256_signature(GBytes *payload, GBytes *pubkey, GBytes *signature, GError **error) { #ifdef HAVE_GNUTLS gnutls_datum_t hash; gnutls_datum_t m; gnutls_datum_t e; gnutls_datum_t sig; gnutls_hash_hd_t sha2; g_auto(gnutls_pubkey_t) pub = NULL; gint ec; guint8 exponent[] = {1, 0, 1}; guint hash_length = gnutls_hash_get_len(GNUTLS_DIG_SHA256); g_autoptr(gnutls_data_t) hash_data = NULL; /* hash firmware data */ hash_data = gnutls_malloc(hash_length); gnutls_hash_init(&sha2, GNUTLS_DIG_SHA256); gnutls_hash(sha2, g_bytes_get_data(payload, NULL), g_bytes_get_size(payload)); gnutls_hash_deinit(sha2, hash_data); /* hash */ hash.size = hash_length; hash.data = hash_data; /* modulus */ m.size = g_bytes_get_size(pubkey); m.data = (guint8 *)g_bytes_get_data(pubkey, NULL); /* exponent */ e.size = sizeof(exponent); e.data = exponent; /* signature */ sig.size = g_bytes_get_size(signature); sig.data = (guint8 *)g_bytes_get_data(signature, NULL); gnutls_pubkey_init(&pub); ec = gnutls_pubkey_import_rsa_raw(pub, &m, &e); if (ec < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to import RSA key: %s", gnutls_strerror(ec)); return FALSE; } ec = gnutls_pubkey_verify_hash2(pub, GNUTLS_SIGN_RSA_SHA256, 0, &hash, &sig); if (ec < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to verify firmware: %s", gnutls_strerror(ec)); return FALSE; } #endif /* success */ return TRUE; } fwupd-2.0.10/plugins/synaptics-rmi/fu-synaptics-rmi-common.h000066400000000000000000000017541501337203100240550ustar00rootroot00000000000000/* * Copyright 2012 Andrew Duggan * Copyright 2012 Synaptics Inc. * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define RMI_PRODUCT_ID_LENGTH 10 #define RMI_DEVICE_PDT_ENTRY_SIZE 6 typedef struct { guint16 query_base; guint16 command_base; guint16 control_base; guint16 data_base; guint8 interrupt_source_count; guint8 function_number; guint8 function_version; guint8 interrupt_reg_num; guint8 interrupt_mask; } FuSynapticsRmiFunction; guint32 fu_synaptics_rmi_generate_checksum(const guint8 *data, gsize len); FuSynapticsRmiFunction * fu_synaptics_rmi_function_parse(GByteArray *buf, guint16 page_base, guint interrupt_count, GError **error); gboolean fu_synaptics_rmi_device_writeln(const gchar *fn, const gchar *buf, GError **error); gboolean fu_synaptics_rmi_verify_sha256_signature(GBytes *payload, GBytes *pubkey, GBytes *signature, GError **error); fwupd-2.0.10/plugins/synaptics-rmi/fu-synaptics-rmi-device.c000066400000000000000000000645371501337203100240270ustar00rootroot00000000000000/* * Copyright 2012 Andrew Duggan * Copyright 2012 Synaptics Inc. * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-synaptics-rmi-common.h" #include "fu-synaptics-rmi-firmware.h" #include "fu-synaptics-rmi-ps2-device.h" #include "fu-synaptics-rmi-struct.h" #include "fu-synaptics-rmi-v5-device.h" #include "fu-synaptics-rmi-v6-device.h" #include "fu-synaptics-rmi-v7-device.h" #define RMI_DEVICE_PAGE_SIZE 0x100 #define RMI_DEVICE_PAGE_SCAN_START 0x00e9 #define RMI_DEVICE_PAGE_SCAN_END 0x0005 #define RMI_DEVICE_F01_BASIC_QUERY_LEN 11 #define RMI_DEVICE_F01_LTS_RESERVED_SIZE 19 #define RMI_DEVICE_F01_QRY1_HAS_LTS (1 << 2) #define RMI_DEVICE_F01_QRY1_HAS_SENSOR_ID (1 << 3) #define RMI_DEVICE_F01_QRY1_HAS_PROPS_2 (1 << 7) #define RMI_DEVICE_F01_QRY42_DS4_QUERIES (1 << 0) #define RMI_DEVICE_F01_QRY43_01_PACKAGE_ID (1 << 0) #define RMI_DEVICE_F01_QRY43_01_BUILD_ID (1 << 1) #define RMI_F34_COMMAND_MASK 0x0f #define RMI_F34_STATUS_MASK 0x07 #define RMI_F34_STATUS_SHIFT 4 #define RMI_F34_ENABLED_MASK 0x80 #define RMI_F34_COMMAND_V1_MASK 0x3f #define RMI_F34_STATUS_V1_MASK 0x3f #define RMI_F34_ENABLED_V1_MASK 0x80 #define RMI_F01_CMD_DEVICE_RESET 1 #define RMI_F01_DEFAULT_RESET_DELAY_MS 100 typedef struct { FuSynapticsRmiFlash flash; GPtrArray *functions; FuSynapticsRmiFunction *f01; FuSynapticsRmiFunction *f34; guint8 current_page; guint16 sig_size; /* 0x0 for non-secure update */ guint8 max_page; gboolean in_iep_mode; } FuSynapticsRmiDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuSynapticsRmiDevice, fu_synaptics_rmi_device, FU_TYPE_UDEV_DEVICE) #define GET_PRIVATE(o) (fu_synaptics_rmi_device_get_instance_private(o)) FuSynapticsRmiFlash * fu_synaptics_rmi_device_get_flash(FuSynapticsRmiDevice *self) { FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self); return &priv->flash; } static void fu_synaptics_rmi_device_flash_to_string(FuSynapticsRmiFlash *flash, guint idt, GString *str) { if (flash->bootloader_id[0] != 0x0) { g_autofree gchar *tmp = g_strdup_printf("%02x.%02x", flash->bootloader_id[0], flash->bootloader_id[1]); fwupd_codec_string_append(str, idt, "BootloaderId", tmp); } fwupd_codec_string_append_hex(str, idt, "BlockSize", flash->block_size); fwupd_codec_string_append_hex(str, idt, "BlockCountFw", flash->block_count_fw); fwupd_codec_string_append_hex(str, idt, "BlockCountCfg", flash->block_count_cfg); fwupd_codec_string_append_hex(str, idt, "FlashConfigLength", flash->config_length); fwupd_codec_string_append_hex(str, idt, "PayloadLength", flash->payload_length); fwupd_codec_string_append_hex(str, idt, "BuildID", flash->build_id); } static void fu_synaptics_rmi_device_to_string(FuDevice *device, guint idt, GString *str) { FuSynapticsRmiDevice *self = FU_SYNAPTICS_RMI_DEVICE(device); FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append_hex(str, idt, "CurrentPage", priv->current_page); fwupd_codec_string_append_hex(str, idt, "InIepMode", priv->in_iep_mode); fwupd_codec_string_append_hex(str, idt, "MaxPage", priv->max_page); fwupd_codec_string_append_hex(str, idt, "SigSize", priv->sig_size); if (priv->f34 != NULL) { fwupd_codec_string_append_hex(str, idt, "BlVer", priv->f34->function_version + 0x5); } fu_synaptics_rmi_device_flash_to_string(&priv->flash, idt, str); } FuSynapticsRmiFunction * fu_synaptics_rmi_device_get_function(FuSynapticsRmiDevice *self, guint8 function_number, GError **error) { FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self); if (priv->functions->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "no RMI functions, perhaps read the PDT?"); return NULL; } for (guint i = 0; i < priv->functions->len; i++) { FuSynapticsRmiFunction *func = g_ptr_array_index(priv->functions, i); if (func->function_number == function_number) return func; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to get RMI function 0x%02x", function_number); return NULL; } GByteArray * fu_synaptics_rmi_device_read(FuSynapticsRmiDevice *self, guint16 addr, gsize req_sz, GError **error) { FuSynapticsRmiDeviceClass *rmi_class = FU_SYNAPTICS_RMI_DEVICE_GET_CLASS(self); return rmi_class->read(self, addr, req_sz, error); } GByteArray * fu_synaptics_rmi_device_read_packet_register(FuSynapticsRmiDevice *self, guint16 addr, gsize req_sz, GError **error) { FuSynapticsRmiDeviceClass *rmi_class = FU_SYNAPTICS_RMI_DEVICE_GET_CLASS(self); if (rmi_class->read_packet_register == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "packet register reads not supported"); return NULL; } return rmi_class->read_packet_register(self, addr, req_sz, error); } gboolean fu_synaptics_rmi_device_write(FuSynapticsRmiDevice *self, guint16 addr, GByteArray *req, FuSynapticsRmiDeviceFlags flags, GError **error) { FuSynapticsRmiDeviceClass *rmi_class = FU_SYNAPTICS_RMI_DEVICE_GET_CLASS(self); return rmi_class->write(self, addr, req, flags, error); } gboolean fu_synaptics_rmi_device_set_page(FuSynapticsRmiDevice *self, guint8 page, GError **error) { FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self); FuSynapticsRmiDeviceClass *rmi_class = FU_SYNAPTICS_RMI_DEVICE_GET_CLASS(self); if (priv->current_page == page) return TRUE; if (!rmi_class->set_page(self, page, error)) return FALSE; priv->current_page = page; return TRUE; } void fu_synaptics_rmi_device_set_iepmode(FuSynapticsRmiDevice *self, gboolean iepmode) { FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self); priv->in_iep_mode = iepmode; } gboolean fu_synaptics_rmi_device_write_bus_select(FuSynapticsRmiDevice *self, guint8 bus, GError **error) { FuSynapticsRmiDeviceClass *rmi_class = FU_SYNAPTICS_RMI_DEVICE_GET_CLASS(self); if (rmi_class->write_bus_select == NULL) return TRUE; return rmi_class->write_bus_select(self, bus, error); } gboolean fu_synaptics_rmi_device_reset(FuSynapticsRmiDevice *self, GError **error) { FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(GByteArray) req = g_byte_array_new(); fu_byte_array_append_uint8(req, RMI_F01_CMD_DEVICE_RESET); if (!fu_synaptics_rmi_device_write(self, priv->f01->command_base, req, FU_SYNAPTICS_RMI_DEVICE_FLAG_ALLOW_FAILURE, error)) return FALSE; fu_device_sleep(FU_DEVICE(self), RMI_F01_DEFAULT_RESET_DELAY_MS); return TRUE; } static gboolean fu_synaptics_rmi_device_scan_pdt(FuSynapticsRmiDevice *self, GError **error) { FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self); guint interrupt_count = 0; /* clear old list */ g_ptr_array_set_size(priv->functions, 0); /* scan pages */ for (guint page = 0; page < priv->max_page; page++) { gboolean found = FALSE; guint32 page_start = RMI_DEVICE_PAGE_SIZE * page; guint32 pdt_start = page_start + RMI_DEVICE_PAGE_SCAN_START; guint32 pdt_end = page_start + RMI_DEVICE_PAGE_SCAN_END; /* set page */ if (!fu_synaptics_rmi_device_set_page(self, page, error)) return FALSE; /* read out functions */ for (guint addr = pdt_start; addr >= pdt_end; addr -= RMI_DEVICE_PDT_ENTRY_SIZE) { g_autofree FuSynapticsRmiFunction *func = NULL; g_autoptr(GByteArray) res = NULL; res = fu_synaptics_rmi_device_read(self, addr, RMI_DEVICE_PDT_ENTRY_SIZE, error); if (res == NULL) { g_prefix_error(error, "failed to read page %u PDT entry @ 0x%04x: ", page, addr); return FALSE; } func = fu_synaptics_rmi_function_parse(res, page_start, interrupt_count, error); if (func == NULL) return FALSE; if (func->function_number == 0) break; interrupt_count += func->interrupt_source_count; g_ptr_array_add(priv->functions, g_steal_pointer(&func)); found = TRUE; } if (!found) break; } /* for debug */ for (guint i = 0; i < priv->functions->len; i++) { FuSynapticsRmiFunction *func = g_ptr_array_index(priv->functions, i); g_debug("PDT-%02u fn:0x%02x vr:%d sc:%d ms:0x%x " "db:0x%02x cb:0x%02x cm:0x%02x qb:0x%02x", i, func->function_number, func->function_version, func->interrupt_source_count, func->interrupt_mask, func->data_base, func->control_base, func->command_base, func->query_base); } /* success */ return TRUE; } void fu_synaptics_rmi_device_set_sig_size(FuSynapticsRmiDevice *self, guint16 sig_size) { FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self); priv->sig_size = sig_size; } guint16 fu_synaptics_rmi_device_get_sig_size(FuSynapticsRmiDevice *self) { FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self); return priv->sig_size; } void fu_synaptics_rmi_device_set_max_page(FuSynapticsRmiDevice *self, guint8 max_page) { FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self); priv->max_page = max_page; } static void fu_synaptics_rmi_device_set_product_id(FuSynapticsRmiDevice *self, const gchar *product_id) { g_autofree gchar *instance_id = NULL; g_auto(GStrv) product_id_split = g_strsplit(product_id, "-", 2); /* use the product ID as an instance ID */ instance_id = g_strdup_printf("SYNAPTICS_RMI\\%s", product_id); fu_device_add_instance_id(FU_DEVICE(self), instance_id); /* also add the product ID without the sub-number */ if (g_strv_length(product_id_split) == 2) { g_autofree gchar *instance_id_major = NULL; instance_id_major = g_strdup_printf("SYNAPTICS_RMI\\%s", product_id_split[0]); fu_device_add_instance_id(FU_DEVICE(self), instance_id_major); } } static gboolean fu_synaptics_rmi_device_query_status(FuSynapticsRmiDevice *self, GError **error) { FuSynapticsRmiDeviceClass *rmi_class = FU_SYNAPTICS_RMI_DEVICE_GET_CLASS(self); return rmi_class->query_status(self, error); } static gboolean fu_synaptics_rmi_device_query_build_id(FuSynapticsRmiDevice *self, guint32 *build_id, GError **error) { FuSynapticsRmiDeviceClass *rmi_class = FU_SYNAPTICS_RMI_DEVICE_GET_CLASS(self); if (rmi_class->query_build_id == NULL) return TRUE; return rmi_class->query_build_id(self, build_id, error); } static gboolean fu_synaptics_rmi_device_query_product_sub_id(FuSynapticsRmiDevice *self, guint8 *product_sub_id, GError **error) { FuSynapticsRmiDeviceClass *rmi_class = FU_SYNAPTICS_RMI_DEVICE_GET_CLASS(self); if (rmi_class->query_product_sub_id == NULL) return TRUE; return rmi_class->query_product_sub_id(self, product_sub_id, error); } static gboolean fu_synaptics_rmi_device_setup(FuDevice *device, GError **error) { FuSynapticsRmiDevice *self = FU_SYNAPTICS_RMI_DEVICE(device); FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self); guint16 addr; guint16 prod_info_addr; guint8 ds4_query_length = 0; guint8 product_sub_id = 0; gboolean has_build_id_query = FALSE; gboolean has_dds4_queries = FALSE; gboolean has_lts; gboolean has_package_id_query = FALSE; gboolean has_query42; gboolean has_sensor_id; g_autofree gchar *bl_ver = NULL; g_autofree gchar *fw_ver = NULL; g_autofree gchar *product_id = NULL; g_autoptr(GByteArray) f01_basic = NULL; g_autoptr(GByteArray) f01_product_id = NULL; g_autoptr(GByteArray) f01_ds4 = NULL; /* assume reset */ priv->in_iep_mode = FALSE; /* read PDT */ if (!fu_synaptics_rmi_device_scan_pdt(self, error)) return FALSE; priv->f01 = fu_synaptics_rmi_device_get_function(self, 0x01, error); if (priv->f01 == NULL) return FALSE; addr = priv->f01->query_base; /* set page */ if (!fu_synaptics_rmi_device_set_page(self, 0, error)) return FALSE; /* force entering iep mode again */ if (!fu_synaptics_rmi_device_enter_iep_mode(self, FU_SYNAPTICS_RMI_DEVICE_FLAG_FORCE, error)) return FALSE; f01_basic = fu_synaptics_rmi_device_read(self, addr, RMI_DEVICE_F01_BASIC_QUERY_LEN, error); if (f01_basic == NULL) { g_prefix_error(error, "failed to read the basic query: "); return FALSE; } has_lts = (f01_basic->data[1] & RMI_DEVICE_F01_QRY1_HAS_LTS) > 0; has_sensor_id = (f01_basic->data[1] & RMI_DEVICE_F01_QRY1_HAS_SENSOR_ID) > 0; has_query42 = (f01_basic->data[1] & RMI_DEVICE_F01_QRY1_HAS_PROPS_2) > 0; /* get the product ID */ addr += 11; f01_product_id = fu_synaptics_rmi_device_read(self, addr, RMI_PRODUCT_ID_LENGTH, error); if (f01_product_id == NULL) { g_prefix_error(error, "failed to read the product id: "); return FALSE; } if (!fu_synaptics_rmi_device_query_product_sub_id(self, &product_sub_id, error)) { g_prefix_error(error, "failed to query product sub id: "); return FALSE; } if (product_sub_id == 0) { /* HID */ product_id = g_strndup((const gchar *)f01_product_id->data, f01_product_id->len); } else { /* PS/2 */ g_autofree gchar *tmp = g_strndup((const gchar *)f01_product_id->data, 6); product_id = g_strdup_printf("%s-%03d", tmp, product_sub_id); } if (product_id != NULL) fu_synaptics_rmi_device_set_product_id(self, product_id); /* force entering iep mode again */ if (!fu_synaptics_rmi_device_enter_iep_mode(self, FU_SYNAPTICS_RMI_DEVICE_FLAG_FORCE, error)) return FALSE; /* skip */ prod_info_addr = addr + 6; addr += 10; if (has_lts) addr++; if (has_sensor_id) addr++; if (has_lts) addr += RMI_DEVICE_F01_LTS_RESERVED_SIZE; /* read package ids */ if (has_query42) { g_autoptr(GByteArray) f01_tmp = NULL; f01_tmp = fu_synaptics_rmi_device_read(self, addr++, 1, error); if (f01_tmp == NULL) { g_prefix_error(error, "failed to read query 42: "); return FALSE; } has_dds4_queries = (f01_tmp->data[0] & RMI_DEVICE_F01_QRY42_DS4_QUERIES) > 0; } if (has_dds4_queries) { g_autoptr(GByteArray) f01_tmp = NULL; f01_tmp = fu_synaptics_rmi_device_read(self, addr++, 1, error); if (f01_tmp == NULL) { g_prefix_error(error, "failed to read DS4 query length: "); return FALSE; } ds4_query_length = f01_tmp->data[0]; } f01_ds4 = fu_synaptics_rmi_device_read(self, addr, 0x1, error); if (f01_ds4 == NULL) { g_prefix_error(error, "failed to read F01 Query43: "); return FALSE; } has_package_id_query = (f01_ds4->data[0] & RMI_DEVICE_F01_QRY43_01_PACKAGE_ID) > 0; has_build_id_query = (f01_ds4->data[0] & RMI_DEVICE_F01_QRY43_01_BUILD_ID) > 0; addr += ds4_query_length; if (has_package_id_query) prod_info_addr++; if (has_build_id_query) { g_autoptr(GByteArray) f01_tmp = NULL; guint8 buf32[4] = {0x0}; f01_tmp = fu_synaptics_rmi_device_read(self, prod_info_addr, 0x3, error); if (f01_tmp == NULL) { g_prefix_error(error, "failed to read build ID bytes: "); return FALSE; } if (!fu_memcpy_safe(buf32, sizeof(buf32), 0x0, /* dst */ f01_tmp->data, f01_tmp->len, 0x0, /* src */ f01_tmp->len, error)) return FALSE; if (!fu_memread_uint32_safe(buf32, sizeof(buf32), 0x0, &priv->flash.build_id, G_LITTLE_ENDIAN, error)) return FALSE; } /* read build ID, typically only for PS/2 */ if (!fu_synaptics_rmi_device_query_build_id(self, &priv->flash.build_id, error)) { g_prefix_error(error, "failed to query build id: "); return FALSE; } /* get Function34_Query0,1 */ priv->f34 = fu_synaptics_rmi_device_get_function(self, 0x34, error); if (priv->f34 == NULL) return FALSE; if (priv->f34->function_version == 0x0) { if (!fu_synaptics_rmi_v5_device_setup(self, error)) { g_prefix_error(error, "failed to do v5 setup: "); return FALSE; } } else if (priv->f34->function_version == 0x1) { if (!fu_synaptics_rmi_v6_device_setup(self, error)) { g_prefix_error(error, "failed to do v6 setup: "); return FALSE; } } else if (priv->f34->function_version == 0x2) { if (!fu_synaptics_rmi_v7_device_setup(self, error)) { g_prefix_error(error, "failed to do v7 setup: "); return FALSE; } } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "f34 function version 0x%02x unsupported", priv->f34->function_version); return FALSE; } if (!fu_synaptics_rmi_device_query_status(self, error)) { g_prefix_error(error, "failed to read bootloader status: "); return FALSE; } /* set versions */ fw_ver = g_strdup_printf("%u.%u.%u", f01_basic->data[2], f01_basic->data[3], priv->flash.build_id); fu_device_set_version(device, fw_ver); if (priv->f34->function_version == 0x0 || priv->f34->function_version == 0x1) { bl_ver = g_strdup_printf("%c.0.0", priv->flash.bootloader_id[1]); } else { bl_ver = g_strdup_printf("%u.0.0", priv->flash.bootloader_id[1]); } fu_device_set_version_bootloader(device, bl_ver); /* success */ return TRUE; } static FuFirmware * fu_synaptics_rmi_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuSynapticsRmiDevice *self = FU_SYNAPTICS_RMI_DEVICE(device); FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(FuFirmware) firmware = fu_synaptics_rmi_firmware_new(); g_autoptr(GBytes) bytes_cfg = NULL; g_autoptr(GBytes) bytes_bin = NULL; gsize size_expected; if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; /* check sizes */ bytes_bin = fu_firmware_get_image_by_id_bytes(firmware, "ui", error); if (bytes_bin == NULL) return NULL; size_expected = ((gsize)priv->flash.block_count_fw * (gsize)priv->flash.block_size) + fu_synaptics_rmi_firmware_get_sig_size(FU_SYNAPTICS_RMI_FIRMWARE(firmware)); if (g_bytes_get_size(bytes_bin) != size_expected) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "file firmware invalid size 0x%04x, expected 0x%04x", (guint)g_bytes_get_size(bytes_bin), (guint)size_expected); return NULL; } bytes_cfg = fu_firmware_get_image_by_id_bytes(firmware, "config", error); if (bytes_cfg == NULL) return NULL; size_expected = (gsize)priv->flash.block_count_cfg * (gsize)priv->flash.block_size; if (g_bytes_get_size(bytes_cfg) != size_expected) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "file config invalid size 0x%04x, expected 0x%04x", (guint)g_bytes_get_size(bytes_cfg), (guint)size_expected); return NULL; } return g_steal_pointer(&firmware); } static gboolean fu_synaptics_rmi_device_poll(FuSynapticsRmiDevice *self, GError **error) { FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(GByteArray) f34_db = NULL; /* get if the last flash read completed successfully */ f34_db = fu_synaptics_rmi_device_read(self, priv->f34->data_base, 0x1, error); if (f34_db == NULL) { g_prefix_error(error, "failed to read f34_db: "); return FALSE; } if ((f34_db->data[0] & 0x1f) != 0x0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "flash status invalid: 0x%x", (guint)(f34_db->data[0] & 0x1f)); return FALSE; } /* success */ return TRUE; } gboolean fu_synaptics_rmi_device_poll_wait(FuSynapticsRmiDevice *self, GError **error) { g_autoptr(GError) error_local = NULL; /* try to poll every 20ms for up to 400ms */ for (guint i = 0; i < 20; i++) { fu_device_sleep(FU_DEVICE(self), 20); g_clear_error(&error_local); if (fu_synaptics_rmi_device_poll(self, &error_local)) return TRUE; g_debug("failed: %s", error_local->message); } /* proxy the last error */ g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } static gboolean fu_synaptics_rmi_device_wait_for_attr(FuSynapticsRmiDevice *self, guint8 source_mask, guint timeout_ms, GError **error) { FuSynapticsRmiDeviceClass *rmi_class = FU_SYNAPTICS_RMI_DEVICE_GET_CLASS(self); return rmi_class->wait_for_attr(self, source_mask, timeout_ms, error); } gboolean fu_synaptics_rmi_device_enter_iep_mode(FuSynapticsRmiDevice *self, FuSynapticsRmiDeviceFlags flags, GError **error) { FuSynapticsRmiDeviceClass *rmi_class = FU_SYNAPTICS_RMI_DEVICE_GET_CLASS(self); FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self); /* already set */ if ((flags & FU_SYNAPTICS_RMI_DEVICE_FLAG_FORCE) == 0 && priv->in_iep_mode) return TRUE; if (rmi_class->enter_iep_mode != NULL) { g_debug("enabling RMI iep_mode"); if (!rmi_class->enter_iep_mode(self, error)) { g_prefix_error(error, "failed to enable RMI iep_mode: "); return FALSE; } } priv->in_iep_mode = TRUE; return TRUE; } gboolean fu_synaptics_rmi_device_wait_for_idle(FuSynapticsRmiDevice *self, guint timeout_ms, RmiDeviceWaitForIdleFlags flags, GError **error) { FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self); guint8 f34_command; guint8 f34_enabled; guint8 f34_status; g_autoptr(GByteArray) res = NULL; g_autoptr(GError) error_local = NULL; /* try to get report without requesting */ if (timeout_ms > 0 && !fu_synaptics_rmi_device_wait_for_attr(self, priv->f34->interrupt_mask, timeout_ms, &error_local)) { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to wait for attr: "); return FALSE; } } else if ((flags & RMI_DEVICE_WAIT_FOR_IDLE_FLAG_REFRESH_F34) == 0) { /* device reported idle via an event */ return TRUE; } /* if for some reason we are not getting attention reports for HID devices * then we can still continue after the timeout and read F34 status * but if we have to wait for the timeout to elapse every time then this * will be slow */ if (priv->f34->function_version == 0x1) { res = fu_synaptics_rmi_device_read(self, priv->flash.status_addr, 0x2, error); if (res == NULL) return FALSE; f34_command = res->data[0] & RMI_F34_COMMAND_V1_MASK; f34_status = res->data[1] & RMI_F34_STATUS_V1_MASK; f34_enabled = !!(res->data[1] & RMI_F34_ENABLED_MASK); } else { res = fu_synaptics_rmi_device_read(self, priv->flash.status_addr, 0x1, error); if (res == NULL) return FALSE; f34_command = res->data[0] & RMI_F34_COMMAND_MASK; f34_status = (res->data[0] >> RMI_F34_STATUS_SHIFT) & RMI_F34_STATUS_MASK; f34_enabled = !!(res->data[0] & RMI_F34_ENABLED_MASK); } /* PS/2 */ if (FU_IS_SYNAPTICS_RMI_PS2_DEVICE(self)) { if (f34_command == 0) { g_debug("F34 zero as PS/2"); return TRUE; } } /* is idle */ if (f34_status == 0x0 && f34_command == 0x0) { if (f34_enabled == 0x0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "idle but enabled unset"); return FALSE; } return TRUE; } /* failed */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "timed out waiting for idle [cmd:0x%x, sta:0x%x, ena:0x%x]", f34_command, f34_status, f34_enabled); return FALSE; } gboolean fu_synaptics_rmi_device_disable_sleep(FuSynapticsRmiDevice *self, GError **error) { FuSynapticsRmiDeviceClass *rmi_class = FU_SYNAPTICS_RMI_DEVICE_GET_CLASS(self); if (rmi_class->disable_sleep == NULL) return TRUE; return rmi_class->disable_sleep(self, error); } gboolean fu_synaptics_rmi_device_write_bootloader_id(FuSynapticsRmiDevice *self, GError **error) { FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self); gint block_data_offset = RMI_F34_BLOCK_DATA_OFFSET; g_autoptr(GByteArray) bootloader_id_req = g_byte_array_new(); if (priv->f34->function_version == 0x1) block_data_offset = RMI_F34_BLOCK_DATA_V1_OFFSET; /* write bootloader_id into F34_Flash_Data0,1 */ g_byte_array_append(bootloader_id_req, priv->flash.bootloader_id, sizeof(priv->flash.bootloader_id)); if (!fu_synaptics_rmi_device_write(self, priv->f34->data_base + block_data_offset, bootloader_id_req, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write bootloader_id: "); return FALSE; } /* success */ return TRUE; } gboolean fu_synaptics_rmi_device_disable_irqs(FuSynapticsRmiDevice *self, GError **error) { FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(GByteArray) interrupt_disable_req = g_byte_array_new(); fu_byte_array_append_uint8(interrupt_disable_req, priv->f34->interrupt_mask | priv->f01->interrupt_mask); if (!fu_synaptics_rmi_device_write(self, priv->f01->control_base + 1, interrupt_disable_req, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to disable interrupts: "); return FALSE; } return TRUE; } static gboolean fu_synaptics_rmi_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuSynapticsRmiDevice *self = FU_SYNAPTICS_RMI_DEVICE(device); FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self); if (priv->f34->function_version == 0x0 || priv->f34->function_version == 0x1) { return fu_synaptics_rmi_v5_device_write_firmware(device, firmware, progress, flags, error); } if (priv->f34->function_version == 0x2) { return fu_synaptics_rmi_v7_device_write_firmware(device, firmware, progress, flags, error); } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "f34 function version 0x%02x unsupported", priv->f34->function_version); return FALSE; } static void fu_synaptics_rmi_device_init(FuSynapticsRmiDevice *self) { FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self); fu_device_add_protocol(FU_DEVICE(self), "com.synaptics.rmi"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); priv->current_page = 0xfe; priv->functions = g_ptr_array_new_with_free_func(g_free); } static void fu_synaptics_rmi_device_finalize(GObject *object) { FuSynapticsRmiDevice *self = FU_SYNAPTICS_RMI_DEVICE(object); FuSynapticsRmiDevicePrivate *priv = GET_PRIVATE(self); g_ptr_array_unref(priv->functions); G_OBJECT_CLASS(fu_synaptics_rmi_device_parent_class)->finalize(object); } static void fu_synaptics_rmi_device_class_init(FuSynapticsRmiDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->finalize = fu_synaptics_rmi_device_finalize; device_class->to_string = fu_synaptics_rmi_device_to_string; device_class->prepare_firmware = fu_synaptics_rmi_device_prepare_firmware; device_class->setup = fu_synaptics_rmi_device_setup; device_class->write_firmware = fu_synaptics_rmi_device_write_firmware; } fwupd-2.0.10/plugins/synaptics-rmi/fu-synaptics-rmi-device.h000066400000000000000000000110031501337203100240100ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-synaptics-rmi-common.h" #define FU_TYPE_SYNAPTICS_RMI_DEVICE (fu_synaptics_rmi_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuSynapticsRmiDevice, fu_synaptics_rmi_device, FU, SYNAPTICS_RMI_DEVICE, FuUdevDevice) typedef enum { FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE = 0, FU_SYNAPTICS_RMI_DEVICE_FLAG_ALLOW_FAILURE = 1 << 0, FU_SYNAPTICS_RMI_DEVICE_FLAG_FORCE = 1 << 1, } FuSynapticsRmiDeviceFlags; struct _FuSynapticsRmiDeviceClass { FuUdevDeviceClass parent_class; gboolean (*setup)(FuSynapticsRmiDevice *self, GError **error); gboolean (*query_status)(FuSynapticsRmiDevice *self, GError **error); gboolean (*write)(FuSynapticsRmiDevice *self, guint16 addr, GByteArray *req, FuSynapticsRmiDeviceFlags flags, GError **error); GByteArray *(*read)(FuSynapticsRmiDevice *self, guint16 addr, gsize req_sz, GError **error); GByteArray *(*read_packet_register)(FuSynapticsRmiDevice *self, guint16 addr, gsize req_sz, GError **error); gboolean (*wait_for_attr)(FuSynapticsRmiDevice *self, guint8 source_mask, guint timeout_ms, GError **error); gboolean (*set_page)(FuSynapticsRmiDevice *self, guint8 page, GError **error); gboolean (*disable_sleep)(FuSynapticsRmiDevice *self, GError **error); gboolean (*write_bus_select)(FuSynapticsRmiDevice *self, guint8 bus, GError **error); gboolean (*query_build_id)(FuSynapticsRmiDevice *self, guint32 *build_id, GError **error); gboolean (*query_product_sub_id)(FuSynapticsRmiDevice *self, guint8 *product_sub_id, GError **error); gboolean (*enter_iep_mode)(FuSynapticsRmiDevice *self, GError **error); }; typedef struct { guint16 block_count_cfg; guint16 block_count_fw; guint16 block_size; guint16 config_length; guint16 payload_length; guint32 build_id; guint8 bootloader_id[2]; guint8 status_addr; gboolean has_pubkey; } FuSynapticsRmiFlash; #define RMI_F34_HAS_NEW_REG_MAP (1 << 0) #define RMI_F34_HAS_CONFIG_ID (1 << 2) #define RMI_F34_BLOCK_DATA_OFFSET 2 #define RMI_F34_BLOCK_DATA_V1_OFFSET 1 #define RMI_F34_ENABLE_WAIT_MS 300 /* ms */ #define RMI_F34_IDLE_WAIT_MS 500 /* ms */ #define RMI_DEVICE_PAGE_SELECT_REGISTER 0xff #define RMI_DEVICE_BUS_SELECT_REGISTER 0xfe #define RMI_KEY_SIZE_2K 0x100 typedef enum { RMI_DEVICE_WAIT_FOR_IDLE_FLAG_NONE = 0, RMI_DEVICE_WAIT_FOR_IDLE_FLAG_REFRESH_F34 = (1 << 0), } RmiDeviceWaitForIdleFlags; void fu_synaptics_rmi_device_set_iepmode(FuSynapticsRmiDevice *self, gboolean iepmode); gboolean fu_synaptics_rmi_device_set_page(FuSynapticsRmiDevice *self, guint8 page, GError **error); gboolean fu_synaptics_rmi_device_write_bootloader_id(FuSynapticsRmiDevice *self, GError **error); gboolean fu_synaptics_rmi_device_disable_irqs(FuSynapticsRmiDevice *self, GError **error); GByteArray * fu_synaptics_rmi_device_read(FuSynapticsRmiDevice *self, guint16 addr, gsize req_sz, GError **error); GByteArray * fu_synaptics_rmi_device_read_packet_register(FuSynapticsRmiDevice *self, guint16 addr, gsize req_sz, GError **error); gboolean fu_synaptics_rmi_device_write(FuSynapticsRmiDevice *self, guint16 addr, GByteArray *req, FuSynapticsRmiDeviceFlags flags, GError **error); gboolean fu_synaptics_rmi_device_reset(FuSynapticsRmiDevice *self, GError **error); gboolean fu_synaptics_rmi_device_wait_for_idle(FuSynapticsRmiDevice *self, guint timeout_ms, RmiDeviceWaitForIdleFlags flags, GError **error); gboolean fu_synaptics_rmi_device_disable_sleep(FuSynapticsRmiDevice *self, GError **error); FuSynapticsRmiFlash * fu_synaptics_rmi_device_get_flash(FuSynapticsRmiDevice *self); FuSynapticsRmiFunction * fu_synaptics_rmi_device_get_function(FuSynapticsRmiDevice *self, guint8 function_number, GError **error); gboolean fu_synaptics_rmi_device_poll_wait(FuSynapticsRmiDevice *self, GError **error); void fu_synaptics_rmi_device_set_sig_size(FuSynapticsRmiDevice *self, guint16 sig_size); guint16 fu_synaptics_rmi_device_get_sig_size(FuSynapticsRmiDevice *self); void fu_synaptics_rmi_device_set_max_page(FuSynapticsRmiDevice *self, guint8 max_page); gboolean fu_synaptics_rmi_device_enter_iep_mode(FuSynapticsRmiDevice *self, FuSynapticsRmiDeviceFlags flags, GError **error); gboolean fu_synaptics_rmi_device_write_bus_select(FuSynapticsRmiDevice *self, guint8 bus, GError **error); fwupd-2.0.10/plugins/synaptics-rmi/fu-synaptics-rmi-firmware.c000066400000000000000000000526431501337203100243770ustar00rootroot00000000000000/* * Copyright 2012 Andrew Duggan * Copyright 2012 Synaptics Inc. * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-synaptics-rmi-common.h" #include "fu-synaptics-rmi-firmware.h" #include "fu-synaptics-rmi-struct.h" typedef enum { RMI_FIRMWARE_KIND_UNKNOWN = 0x00, RMI_FIRMWARE_KIND_0X = 0x01, RMI_FIRMWARE_KIND_10 = 0x10, RMI_FIRMWARE_KIND_LAST, } RmiFirmwareKind; struct _FuSynapticsRmiFirmware { FuFirmware parent_instance; RmiFirmwareKind kind; guint32 checksum; guint8 io; guint8 bootloader_version; guint32 build_id; guint32 package_id; guint16 product_info; gchar *product_id; guint32 sig_size; }; G_DEFINE_TYPE(FuSynapticsRmiFirmware, fu_synaptics_rmi_firmware, FU_TYPE_FIRMWARE) #define RMI_IMG_FW_OFFSET 0x100 #define RMI_IMG_V10_CNTR_ADDR_OFFSET 0x0c #define RMI_IMG_MAX_CONTAINERS 1024 static gboolean fu_synaptics_rmi_firmware_add_image(FuFirmware *firmware, const gchar *id, GInputStream *stream, gsize offset, gsize bufsz, FuFirmwareParseFlags flags, GError **error) { g_autoptr(FuFirmware) img = fu_firmware_new(); g_autoptr(GInputStream) partial_stream = NULL; partial_stream = fu_partial_input_stream_new(stream, offset, bufsz, error); if (partial_stream == NULL) return FALSE; if (!fu_firmware_parse_stream(img, partial_stream, 0x0, flags, error)) return FALSE; fu_firmware_set_id(img, id); return fu_firmware_add_image_full(firmware, img, error); } static gboolean fu_synaptics_rmi_firmware_add_image_v10(FuFirmware *firmware, const gchar *id, GInputStream *stream, gsize offset, gsize bufsz, gsize sig_sz, FuFirmwareParseFlags flags, GError **error) { if (!fu_synaptics_rmi_firmware_add_image(firmware, id, stream, offset, bufsz, flags, error)) return FALSE; if (sig_sz != 0) { g_autoptr(GInputStream) partial_stream = NULL; g_autoptr(FuFirmware) img = fu_firmware_new(); g_autofree gchar *sig_id = NULL; partial_stream = fu_partial_input_stream_new(stream, offset + bufsz, sig_sz, error); if (partial_stream == NULL) return FALSE; if (!fu_firmware_parse_stream(img, partial_stream, 0x0, flags, error)) return FALSE; sig_id = g_strdup_printf("%s-signature", id); fu_firmware_set_id(img, sig_id); fu_firmware_add_image(firmware, img); } return TRUE; } static void fu_synaptics_rmi_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuSynapticsRmiFirmware *self = FU_SYNAPTICS_RMI_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "kind", self->kind); fu_xmlb_builder_insert_kv(bn, "product_id", self->product_id); if (flags & FU_FIRMWARE_EXPORT_FLAG_INCLUDE_DEBUG) { fu_xmlb_builder_insert_kx(bn, "bootloader_version", self->bootloader_version); fu_xmlb_builder_insert_kx(bn, "io", self->io); fu_xmlb_builder_insert_kx(bn, "checksum", self->checksum); fu_xmlb_builder_insert_kx(bn, "build_id", self->build_id); fu_xmlb_builder_insert_kx(bn, "package_id", self->package_id); fu_xmlb_builder_insert_kx(bn, "product_info", self->product_info); fu_xmlb_builder_insert_kx(bn, "sig_size", self->sig_size); } } static gboolean fu_synaptics_rmi_firmware_parse_v10(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuSynapticsRmiFirmware *self = FU_SYNAPTICS_RMI_FIRMWARE(firmware); guint16 container_id; guint32 cntrs_len; guint32 offset; guint32 cntr_addr; guint8 product_id[RMI_PRODUCT_ID_LENGTH] = {0x0}; gsize bufsz = 0; const guint8 *buf; guint32 signature_size; g_autoptr(GByteArray) st_dsc = NULL; g_autoptr(GBytes) fw = NULL; /* maybe stream later */ fw = fu_input_stream_read_bytes(stream, 0, G_MAXSIZE, NULL, error); if (fw == NULL) return FALSE; buf = g_bytes_get_data(fw, &bufsz); if (!fu_memread_uint32_safe(buf, bufsz, RMI_IMG_V10_CNTR_ADDR_OFFSET, &cntr_addr, G_LITTLE_ENDIAN, error)) return FALSE; g_debug("v10 RmiContainerDescriptor at 0x%x", cntr_addr); st_dsc = fu_struct_rmi_container_descriptor_parse_stream(stream, cntr_addr, error); if (st_dsc == NULL) { g_prefix_error(error, "RmiContainerDescriptor invalid: "); return FALSE; } if (bufsz < sizeof(guint32) + st_dsc->len) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "stream was too small"); return FALSE; } container_id = fu_struct_rmi_container_descriptor_get_container_id(st_dsc); if (container_id != FU_RMI_CONTAINER_ID_TOP_LEVEL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "toplevel container_id invalid, got 0x%x expected 0x%x", (guint)container_id, (guint)FU_RMI_CONTAINER_ID_TOP_LEVEL); return FALSE; } offset = fu_struct_rmi_container_descriptor_get_content_address(st_dsc); if (offset > bufsz - sizeof(guint32) - st_dsc->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "image offset invalid, got 0x%x, size 0x%x", (guint)offset, (guint)bufsz); return FALSE; } cntrs_len = fu_struct_rmi_container_descriptor_get_content_length(st_dsc) / 4; if (cntrs_len > RMI_IMG_MAX_CONTAINERS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "too many containers in file [%u], maximum is %u", cntrs_len, (guint)RMI_IMG_MAX_CONTAINERS); return FALSE; } g_debug("offset=0x%x (cntrs_len=%u)", offset, cntrs_len); for (guint32 i = 0; i < cntrs_len; i++) { guint32 content_addr; guint32 addr; guint32 length; g_autoptr(GByteArray) st_dsc2 = NULL; if (!fu_memread_uint32_safe(buf, bufsz, offset, &addr, G_LITTLE_ENDIAN, error)) return FALSE; g_debug("parsing RmiContainerDescriptor at 0x%x", addr); st_dsc2 = fu_struct_rmi_container_descriptor_parse_stream(stream, addr, error); if (st_dsc2 == NULL) return FALSE; container_id = fu_struct_rmi_container_descriptor_get_container_id(st_dsc2); content_addr = fu_struct_rmi_container_descriptor_get_content_address(st_dsc2); length = fu_struct_rmi_container_descriptor_get_content_length(st_dsc2); signature_size = fu_struct_rmi_container_descriptor_get_signature_size(st_dsc2); g_debug("RmiContainerDescriptor 0x%02x @ 0x%x (len 0x%x) sig_size 0x%x", container_id, content_addr, length, signature_size); if (length == 0 || length > bufsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "length invalid, length 0x%x, size 0x%x", (guint)length, (guint)bufsz); return FALSE; } if (content_addr > bufsz - length) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "address invalid, got 0x%x (length 0x%x), size 0x%x", (guint)content_addr, (guint)length, (guint)bufsz); return FALSE; } switch (container_id) { case FU_RMI_CONTAINER_ID_BL: if (!fu_memread_uint8_safe(buf, bufsz, content_addr, &self->bootloader_version, error)) return FALSE; break; case FU_RMI_CONTAINER_ID_UI: case FU_RMI_CONTAINER_ID_CORE_CODE: if (!fu_synaptics_rmi_firmware_add_image_v10(firmware, "ui", stream, content_addr, length, signature_size, flags, error)) return FALSE; break; case FU_RMI_CONTAINER_ID_FLASH_CONFIG: if (!fu_synaptics_rmi_firmware_add_image_v10(firmware, "flash-config", stream, content_addr, length, signature_size, flags, error)) return FALSE; break; case FU_RMI_CONTAINER_ID_UI_CONFIG: case FU_RMI_CONTAINER_ID_CORE_CONFIG: if (!fu_synaptics_rmi_firmware_add_image_v10(firmware, "config", stream, content_addr, length, signature_size, flags, error)) return FALSE; break; case FU_RMI_CONTAINER_ID_FIXED_LOCATION_DATA: if (!fu_synaptics_rmi_firmware_add_image_v10(firmware, "fixed-location-data", stream, content_addr, length, signature_size, flags, error)) return FALSE; break; case FU_RMI_CONTAINER_ID_EXTERNAL_TOUCH_AFE_CONFIG: if (!fu_synaptics_rmi_firmware_add_image_v10(firmware, "afe-config", stream, content_addr, length, signature_size, flags, error)) return FALSE; break; case FU_RMI_CONTAINER_ID_DISPLAY_CONFIG: if (!fu_synaptics_rmi_firmware_add_image_v10(firmware, "display-config", stream, content_addr, length, signature_size, flags, error)) return FALSE; break; case FU_RMI_CONTAINER_ID_GENERAL_INFORMATION: if (length < 0x18 + RMI_PRODUCT_ID_LENGTH) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "content_addr invalid, got 0x%x (length 0x%x)", content_addr, (guint)length); return FALSE; } g_clear_pointer(&self->product_id, g_free); self->io = 1; if (!fu_memread_uint32_safe(buf, bufsz, content_addr, &self->package_id, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memread_uint32_safe(buf, bufsz, content_addr + 0x04, &self->build_id, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memcpy_safe(product_id, sizeof(product_id), 0x0, /* dst */ buf, bufsz, content_addr + 0x18, /* src */ sizeof(product_id), error)) return FALSE; break; default: g_debug("unsupported container %s [0x%02x]", fu_rmi_container_id_to_string(container_id), container_id); break; } offset += 4; } if (product_id[0] != '\0') { g_free(self->product_id); self->product_id = g_strndup((const gchar *)product_id, sizeof(product_id)); } return TRUE; } static gboolean fu_synaptics_rmi_firmware_parse_v0x(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuSynapticsRmiFirmware *self = FU_SYNAPTICS_RMI_FIRMWARE(firmware); guint32 cfg_sz; guint32 img_sz; g_autoptr(GByteArray) st_img = NULL; /* main firmware */ st_img = fu_struct_rmi_img_parse_stream(stream, 0x0, error); if (st_img == NULL) return FALSE; img_sz = fu_struct_rmi_img_get_image_size(st_img); if (img_sz > 0) { /* payload, then signature appended */ if (self->sig_size > 0) { guint32 sig_offset = img_sz - self->sig_size; if (!fu_synaptics_rmi_firmware_add_image(firmware, "sig", stream, RMI_IMG_FW_OFFSET + sig_offset, self->sig_size, flags, error)) return FALSE; } if (!fu_synaptics_rmi_firmware_add_image(firmware, "ui", stream, RMI_IMG_FW_OFFSET, img_sz, flags, error)) return FALSE; } /* config */ cfg_sz = fu_struct_rmi_img_get_config_size(st_img); if (cfg_sz > 0) { if (!fu_synaptics_rmi_firmware_add_image(firmware, "config", stream, RMI_IMG_FW_OFFSET + img_sz, cfg_sz, flags, error)) return FALSE; } return TRUE; } static gboolean fu_synaptics_rmi_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuSynapticsRmiFirmware *self = FU_SYNAPTICS_RMI_FIRMWARE(firmware); gsize bufsz = 0; const guint8 *buf; g_autoptr(GByteArray) st_img = NULL; g_autoptr(GBytes) fw = NULL; /* maybe stream later */ fw = fu_input_stream_read_bytes(stream, 0x0, G_MAXSIZE, NULL, error); if (fw == NULL) return FALSE; buf = g_bytes_get_data(fw, &bufsz); /* sanity check */ st_img = fu_struct_rmi_img_parse_stream(stream, 0x0, error); if (st_img == NULL) return FALSE; if (bufsz % 2 != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "data not aligned to 16 bits"); return FALSE; } if (bufsz < 4) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "stream was too small"); return FALSE; } /* verify checksum */ self->checksum = fu_struct_rmi_img_get_checksum(st_img); if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { guint32 checksum_calculated = fu_synaptics_rmi_generate_checksum(buf + 4, bufsz - 4); if (self->checksum != checksum_calculated) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "checksum verification failed, got 0x%08x, actual 0x%08x", (guint)self->checksum, (guint)checksum_calculated); return FALSE; } } /* parse legacy image */ g_clear_pointer(&self->product_id, g_free); self->io = fu_struct_rmi_img_get_io_offset(st_img); self->bootloader_version = fu_struct_rmi_img_get_bootloader_version(st_img); if (self->io == 1) { self->build_id = fu_struct_rmi_img_get_fw_build_id(st_img); self->package_id = fu_struct_rmi_img_get_package_id(st_img); } self->product_id = fu_struct_rmi_img_get_product_id(st_img); self->product_info = fu_struct_rmi_img_get_product_info(st_img); fu_firmware_set_size(firmware, fu_struct_rmi_img_get_image_size(st_img)); /* parse partitions, but ignore lockdown */ switch (self->bootloader_version) { case 2: case 3: case 4: case 5: case 6: if ((self->io & 0x10) >> 1) self->sig_size = fu_struct_rmi_img_get_signature_size(st_img); if (!fu_synaptics_rmi_firmware_parse_v0x(firmware, stream, flags, error)) return FALSE; self->kind = RMI_FIRMWARE_KIND_0X; break; case 16: case 17: if (!fu_synaptics_rmi_firmware_parse_v10(firmware, stream, flags, error)) return FALSE; self->kind = RMI_FIRMWARE_KIND_10; break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "unsupported image version 0x%02x", self->bootloader_version); return FALSE; } /* success */ return TRUE; } guint32 fu_synaptics_rmi_firmware_get_sig_size(FuSynapticsRmiFirmware *self) { return self->sig_size; } static GByteArray * fu_synaptics_rmi_firmware_write_v0x(FuFirmware *firmware, GError **error) { FuSynapticsRmiFirmware *self = FU_SYNAPTICS_RMI_FIRMWARE(firmware); gsize bufsz = 0; guint32 csum; g_autoptr(FuFirmware) img = NULL; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GByteArray) st_img = fu_struct_rmi_img_new(); g_autoptr(GBytes) buf_blob = NULL; /* default image */ img = fu_firmware_get_image_by_id(firmware, "ui", error); if (img == NULL) return NULL; buf_blob = fu_firmware_write(img, error); if (buf_blob == NULL) return NULL; bufsz = g_bytes_get_size(buf_blob); /* create empty block */ fu_struct_rmi_img_set_bootloader_version(st_img, 0x2); /* not hierarchical */ if (self->product_id != NULL) { if (!fu_struct_rmi_img_set_product_id(st_img, self->product_id, error)) return NULL; } fu_struct_rmi_img_set_product_info(st_img, 0x1234); fu_struct_rmi_img_set_image_size(st_img, bufsz); fu_struct_rmi_img_set_config_size(st_img, bufsz); g_byte_array_append(buf, st_img->data, st_img->len); fu_byte_array_set_size(buf, RMI_IMG_FW_OFFSET + 0x4 + bufsz, 0x00); fu_memwrite_uint32(buf->data + RMI_IMG_FW_OFFSET, 0xDEAD, G_LITTLE_ENDIAN); /* img */ fu_memwrite_uint32(buf->data + RMI_IMG_FW_OFFSET + bufsz, 0xBEEF, G_LITTLE_ENDIAN); /* config */ /* fixup checksum */ csum = fu_synaptics_rmi_generate_checksum(buf->data + 4, buf->len - 4); fu_memwrite_uint32(buf->data + FU_STRUCT_RMI_IMG_OFFSET_CHECKSUM, csum, G_LITTLE_ENDIAN); /* success */ return g_steal_pointer(&buf); } static GByteArray * fu_synaptics_rmi_firmware_write_v10(FuFirmware *firmware, GError **error) { FuSynapticsRmiFirmware *self = FU_SYNAPTICS_RMI_FIRMWARE(firmware); gsize bufsz; guint32 csum; g_autoptr(FuFirmware) img = NULL; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GByteArray) desc_hdr = fu_struct_rmi_container_descriptor_new(); g_autoptr(GByteArray) desc = fu_struct_rmi_container_descriptor_new(); g_autoptr(GBytes) buf_blob = NULL; /* header | desc_hdr | offset_table | desc | flash_config | * \0x0 \0x20 \0x24 \0x44 |0x48 */ guint32 offset_table[] = {/* offset to first descriptor */ GUINT32_TO_LE(RMI_IMG_FW_OFFSET + 0x24)}; /* nocheck:blocked */ fu_struct_rmi_container_descriptor_set_container_id(desc, FU_RMI_CONTAINER_ID_FLASH_CONFIG); fu_struct_rmi_container_descriptor_set_content_address(desc, RMI_IMG_FW_OFFSET + 0x44); /* default image */ img = fu_firmware_get_image_by_id(firmware, "ui", error); if (img == NULL) return NULL; buf_blob = fu_firmware_write(img, error); if (buf_blob == NULL) return NULL; bufsz = g_bytes_get_size(buf_blob); fu_struct_rmi_container_descriptor_set_content_length(desc, bufsz); /* create empty block */ fu_byte_array_set_size(buf, RMI_IMG_FW_OFFSET + 0x48, 0x00); buf->data[FU_STRUCT_RMI_IMG_OFFSET_IO_OFFSET] = 0x1; buf->data[FU_STRUCT_RMI_IMG_OFFSET_BOOTLOADER_VERSION] = 16; /* hierarchical */ if (self->product_id != NULL) { gsize product_id_sz = strlen(self->product_id); if (!fu_memcpy_safe(buf->data, buf->len, FU_STRUCT_RMI_IMG_OFFSET_PRODUCT_ID, /* dst */ (const guint8 *)self->product_id, product_id_sz, 0x0, /* src */ product_id_sz, error)) return NULL; } fu_memwrite_uint32(buf->data + FU_STRUCT_RMI_IMG_OFFSET_FW_BUILD_ID, 0x1234, G_LITTLE_ENDIAN); fu_memwrite_uint32(buf->data + FU_STRUCT_RMI_IMG_OFFSET_PACKAGE_ID, 0x4321, G_LITTLE_ENDIAN); fu_memwrite_uint16(buf->data + FU_STRUCT_RMI_IMG_OFFSET_PRODUCT_INFO, 0x3456, G_LITTLE_ENDIAN); fu_memwrite_uint32(buf->data + FU_STRUCT_RMI_IMG_OFFSET_IMAGE_SIZE, bufsz, G_LITTLE_ENDIAN); fu_memwrite_uint32(buf->data + FU_STRUCT_RMI_IMG_OFFSET_CONFIG_SIZE, bufsz, G_LITTLE_ENDIAN); fu_memwrite_uint32(buf->data + RMI_IMG_V10_CNTR_ADDR_OFFSET, RMI_IMG_FW_OFFSET, G_LITTLE_ENDIAN); /* hierarchical section */ fu_struct_rmi_container_descriptor_set_container_id(desc_hdr, FU_RMI_CONTAINER_ID_TOP_LEVEL); fu_struct_rmi_container_descriptor_set_content_length(desc_hdr, 0x1 * 4); /* bytes */ fu_struct_rmi_container_descriptor_set_content_address(desc_hdr, RMI_IMG_FW_OFFSET + 0x20); /* offset to table */ memcpy(buf->data + RMI_IMG_FW_OFFSET + 0x00, /* nocheck:blocked */ desc_hdr->data, desc_hdr->len); memcpy(buf->data + RMI_IMG_FW_OFFSET + 0x20, /* nocheck:blocked */ offset_table, sizeof(offset_table)); memcpy(buf->data + RMI_IMG_FW_OFFSET + 0x24, /* nocheck:blocked */ desc->data, desc->len); fu_memwrite_uint32(buf->data + RMI_IMG_FW_OFFSET + 0x44, 0xfeed, G_LITTLE_ENDIAN); /* flash_config */ /* fixup checksum */ csum = fu_synaptics_rmi_generate_checksum(buf->data + 4, buf->len - 4); fu_memwrite_uint32(buf->data + FU_STRUCT_RMI_IMG_OFFSET_CHECKSUM, csum, G_LITTLE_ENDIAN); /* success */ return g_steal_pointer(&buf); } static gboolean fu_synaptics_rmi_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { FuSynapticsRmiFirmware *self = FU_SYNAPTICS_RMI_FIRMWARE(firmware); const gchar *product_id; guint64 tmp; /* either 0x or 10 */ tmp = xb_node_query_text_as_uint(n, "kind", NULL); if (tmp != G_MAXUINT64) self->kind = tmp; /* any string */ product_id = xb_node_query_text(n, "product_id", NULL); if (product_id != NULL) { gsize product_id_sz = strlen(product_id); if (product_id_sz > RMI_PRODUCT_ID_LENGTH) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "product_id not supported, %u of %u bytes", (guint)product_id_sz, (guint)RMI_PRODUCT_ID_LENGTH); return FALSE; } g_free(self->product_id); self->product_id = g_strdup(product_id); } /* success */ return TRUE; } static GByteArray * fu_synaptics_rmi_firmware_write(FuFirmware *firmware, GError **error) { FuSynapticsRmiFirmware *self = FU_SYNAPTICS_RMI_FIRMWARE(firmware); /* two supported container formats */ if (self->kind == RMI_FIRMWARE_KIND_0X) return fu_synaptics_rmi_firmware_write_v0x(firmware, error); if (self->kind == RMI_FIRMWARE_KIND_10) return fu_synaptics_rmi_firmware_write_v10(firmware, error); /* not supported */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "kind not supported"); return NULL; } static void fu_synaptics_rmi_firmware_init(FuSynapticsRmiFirmware *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); fu_firmware_set_images_max(FU_FIRMWARE(self), RMI_IMG_MAX_CONTAINERS); } static void fu_synaptics_rmi_firmware_finalize(GObject *obj) { FuSynapticsRmiFirmware *self = FU_SYNAPTICS_RMI_FIRMWARE(obj); g_free(self->product_id); G_OBJECT_CLASS(fu_synaptics_rmi_firmware_parent_class)->finalize(obj); } static void fu_synaptics_rmi_firmware_class_init(FuSynapticsRmiFirmwareClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_synaptics_rmi_firmware_finalize; firmware_class->parse = fu_synaptics_rmi_firmware_parse; firmware_class->export = fu_synaptics_rmi_firmware_export; firmware_class->build = fu_synaptics_rmi_firmware_build; firmware_class->write = fu_synaptics_rmi_firmware_write; } FuFirmware * fu_synaptics_rmi_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_SYNAPTICS_RMI_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/synaptics-rmi/fu-synaptics-rmi-firmware.h000066400000000000000000000007631501337203100244000ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_SYNAPTICS_RMI_FIRMWARE (fu_synaptics_rmi_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuSynapticsRmiFirmware, fu_synaptics_rmi_firmware, FU, SYNAPTICS_RMI_FIRMWARE, FuFirmware) FuFirmware * fu_synaptics_rmi_firmware_new(void); guint32 fu_synaptics_rmi_firmware_get_sig_size(FuSynapticsRmiFirmware *self); fwupd-2.0.10/plugins/synaptics-rmi/fu-synaptics-rmi-hid-device.c000066400000000000000000000415011501337203100245530ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * Copyright 2020 Synaptics Incorporated. * Copyright 2012 Andrew Duggan * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fu-synaptics-rmi-hid-device.h" #include "fu-synaptics-rmi-v5-device.h" #include "fu-synaptics-rmi-v7-device.h" struct _FuSynapticsRmiHidDevice { FuSynapticsRmiDevice parent_instance; }; G_DEFINE_TYPE(FuSynapticsRmiHidDevice, fu_synaptics_rmi_hid_device, FU_TYPE_SYNAPTICS_RMI_DEVICE) #define RMI_WRITE_REPORT_ID 0x9 /* output report */ #define RMI_READ_ADDR_REPORT_ID 0xa /* output report */ #define RMI_READ_DATA_REPORT_ID 0xb /* input report */ #define RMI_ATTN_REPORT_ID 0xc /* input report */ #define RMI_SET_RMI_MODE_REPORT_ID 0xf /* feature report */ #define RMI_DEVICE_DEFAULT_TIMEOUT 2000 #define HID_RMI4_REPORT_ID 0 #define HID_RMI4_READ_INPUT_COUNT 1 #define HID_RMI4_READ_INPUT_DATA 2 #define HID_RMI4_READ_OUTPUT_ADDR 2 #define HID_RMI4_READ_OUTPUT_COUNT 4 #define HID_RMI4_WRITE_OUTPUT_COUNT 1 #define HID_RMI4_WRITE_OUTPUT_ADDR 2 #define HID_RMI4_WRITE_OUTPUT_DATA 4 #define HID_RMI4_FEATURE_MODE 1 #define HID_RMI4_ATTN_INTERRUPT_SOURCES 1 #define HID_RMI4_ATTN_DATA 2 /* * This bit disables whatever sleep mode may be selected by the sleep_mode * field and forces the device to run at full power without sleeping. */ #define RMI_F01_CRTL0_NOSLEEP_BIT (1 << 2) /* * msleep mode controls power management on the device and affects all * functions of the device. */ #define RMI_F01_CTRL0_SLEEP_MODE_MASK 0x03 #define RMI_SLEEP_MODE_NORMAL 0x00 #define RMI_SLEEP_MODE_SENSOR_SLEEP 0x01 #define FU_SYNAPTICS_RMI_HID_DEVICE_IOCTL_TIMEOUT 5000 /* ms */ static GByteArray * fu_synaptics_rmi_hid_device_read(FuSynapticsRmiDevice *rmi_device, guint16 addr, gsize req_sz, GError **error) { FuSynapticsRmiHidDevice *self = FU_SYNAPTICS_RMI_HID_DEVICE(rmi_device); FuIOChannel *io_channel = fu_udev_device_get_io_channel(FU_UDEV_DEVICE(self)); g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GByteArray) req = g_byte_array_new(); /* maximum size */ if (req_sz > 0xffff) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "data to read was too long"); return NULL; } /* report then old 1 byte read count */ fu_byte_array_append_uint8(req, RMI_READ_ADDR_REPORT_ID); fu_byte_array_append_uint8(req, 0x0); /* address */ fu_byte_array_append_uint16(req, addr, G_LITTLE_ENDIAN); /* read output count */ fu_byte_array_append_uint16(req, req_sz, G_LITTLE_ENDIAN); /* request */ for (guint j = req->len; j < 21; j++) fu_byte_array_append_uint8(req, 0x0); fu_dump_full(G_LOG_DOMAIN, "ReportWrite", req->data, req->len, 80, FU_DUMP_FLAGS_NONE); if (!fu_io_channel_write_byte_array(io_channel, req, RMI_DEVICE_DEFAULT_TIMEOUT, FU_IO_CHANNEL_FLAG_SINGLE_SHOT | FU_IO_CHANNEL_FLAG_USE_BLOCKING_IO, error)) return NULL; /* keep reading responses until we get enough data */ while (buf->len < req_sz) { guint8 input_count_sz = 0; g_autoptr(GByteArray) res = NULL; res = fu_io_channel_read_byte_array(io_channel, req_sz, RMI_DEVICE_DEFAULT_TIMEOUT, FU_IO_CHANNEL_FLAG_SINGLE_SHOT, error); if (res == NULL) return NULL; if (res->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "response zero sized"); return NULL; } fu_dump_full(G_LOG_DOMAIN, "ReportRead", res->data, res->len, 80, FU_DUMP_FLAGS_NONE); /* ignore non data report events */ if (res->data[HID_RMI4_REPORT_ID] != RMI_READ_DATA_REPORT_ID) { g_debug("ignoring report with ID 0x%02x", res->data[HID_RMI4_REPORT_ID]); continue; } if (res->len < HID_RMI4_READ_INPUT_DATA) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "response too small: 0x%02x", res->len); return NULL; } input_count_sz = res->data[HID_RMI4_READ_INPUT_COUNT]; if (input_count_sz == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "input count zero"); return NULL; } if (input_count_sz + (guint)HID_RMI4_READ_INPUT_DATA > res->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "underflow 0x%02x from expected 0x%02x", res->len, (guint)input_count_sz + HID_RMI4_READ_INPUT_DATA); return NULL; } g_byte_array_append(buf, res->data + HID_RMI4_READ_INPUT_DATA, input_count_sz); } fu_dump_full(G_LOG_DOMAIN, "DeviceRead", buf->data, buf->len, 80, FU_DUMP_FLAGS_NONE); return g_steal_pointer(&buf); } static GByteArray * fu_synaptics_rmi_hid_device_read_packet_register(FuSynapticsRmiDevice *rmi_device, guint16 addr, gsize req_sz, GError **error) { return fu_synaptics_rmi_hid_device_read(rmi_device, addr, req_sz, error); } static gboolean fu_synaptics_rmi_hid_device_write(FuSynapticsRmiDevice *rmi_device, guint16 addr, GByteArray *req, FuSynapticsRmiDeviceFlags flags, GError **error) { FuSynapticsRmiHidDevice *self = FU_SYNAPTICS_RMI_HID_DEVICE(rmi_device); FuIOChannel *io_channel = fu_udev_device_get_io_channel(FU_UDEV_DEVICE(self)); guint8 len = 0x0; g_autoptr(GByteArray) buf = g_byte_array_new(); /* check size */ if (req != NULL) { if (req->len > 0xff) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "data to write was too long"); return FALSE; } len = req->len; } /* report */ fu_byte_array_append_uint8(buf, RMI_WRITE_REPORT_ID); /* length */ fu_byte_array_append_uint8(buf, len); /* address */ fu_byte_array_append_uint16(buf, addr, G_LITTLE_ENDIAN); /* optional data */ if (req != NULL) g_byte_array_append(buf, req->data, req->len); /* pad out to 21 bytes for some reason */ for (guint i = buf->len; i < 21; i++) fu_byte_array_append_uint8(buf, 0x0); fu_dump_full(G_LOG_DOMAIN, "DeviceWrite", buf->data, buf->len, 80, FU_DUMP_FLAGS_NONE); return fu_io_channel_write_byte_array(io_channel, buf, RMI_DEVICE_DEFAULT_TIMEOUT, FU_IO_CHANNEL_FLAG_SINGLE_SHOT | FU_IO_CHANNEL_FLAG_USE_BLOCKING_IO, error); } static gboolean fu_synaptics_rmi_hid_device_wait_for_attr(FuSynapticsRmiDevice *rmi_device, guint8 source_mask, guint timeout_ms, GError **error) { FuSynapticsRmiHidDevice *self = FU_SYNAPTICS_RMI_HID_DEVICE(rmi_device); FuIOChannel *io_channel = fu_udev_device_get_io_channel(FU_UDEV_DEVICE(self)); g_autoptr(GTimer) timer = g_timer_new(); /* wait for event from hardware */ while (g_timer_elapsed(timer, NULL) * 1000.f < timeout_ms) { g_autoptr(GByteArray) res = NULL; g_autoptr(GError) error_local = NULL; /* read from fd */ res = fu_io_channel_read_byte_array(io_channel, HID_RMI4_ATTN_INTERRUPT_SOURCES + 1, timeout_ms, FU_IO_CHANNEL_FLAG_NONE, &error_local); if (res == NULL) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT)) break; g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } fu_dump_full(G_LOG_DOMAIN, "ReportRead", res->data, res->len, 80, FU_DUMP_FLAGS_NONE); if (res->len < HID_RMI4_ATTN_INTERRUPT_SOURCES + 1) { g_debug("attr: ignoring small read of %u", res->len); continue; } if (res->data[HID_RMI4_REPORT_ID] != RMI_ATTN_REPORT_ID) { g_debug("attr: ignoring invalid report ID 0x%x", res->data[HID_RMI4_REPORT_ID]); continue; } /* success */ if (source_mask & res->data[HID_RMI4_ATTN_INTERRUPT_SOURCES]) return TRUE; /* wrong mask */ g_debug("source mask did not match: 0x%x", res->data[HID_RMI4_ATTN_INTERRUPT_SOURCES]); } /* urgh */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no attr report, timed out"); return FALSE; } typedef enum { HID_RMI4_MODE_MOUSE = 0, HID_RMI4_MODE_ATTN_REPORTS = 1, HID_RMI4_MODE_NO_PACKED_ATTN_REPORTS = 2, } FuSynapticsRmiHidMode; static gboolean fu_synaptics_rmi_hid_device_set_mode(FuSynapticsRmiHidDevice *self, FuSynapticsRmiHidMode mode, GError **error) { const guint8 data[] = {0x0f, mode}; g_autoptr(FuIoctl) ioctl = fu_udev_device_ioctl_new(FU_UDEV_DEVICE(self)); fu_dump_raw(G_LOG_DOMAIN, "SetMode", data, sizeof(data)); return fu_ioctl_execute(ioctl, HIDIOCSFEATURE(sizeof(data)), /* nocheck:blocked */ (guint8 *)data, sizeof(data), NULL, FU_SYNAPTICS_RMI_HID_DEVICE_IOCTL_TIMEOUT, FU_IOCTL_FLAG_NONE, error); } static gboolean fu_synaptics_rmi_hid_device_open(FuDevice *device, GError **error) { FuSynapticsRmiHidDevice *self = FU_SYNAPTICS_RMI_HID_DEVICE(device); /* FuUdevDevice->open */ if (!FU_DEVICE_CLASS(fu_synaptics_rmi_hid_device_parent_class)->open(device, error)) return FALSE; /* set up touchpad so we can query it */ if (!fu_synaptics_rmi_hid_device_set_mode(self, HID_RMI4_MODE_ATTN_REPORTS, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_synaptics_rmi_hid_device_close(FuDevice *device, GError **error) { FuSynapticsRmiHidDevice *self = FU_SYNAPTICS_RMI_HID_DEVICE(device); g_autoptr(GError) error_local = NULL; /* turn it back to mouse mode */ if (!fu_synaptics_rmi_hid_device_set_mode(self, HID_RMI4_MODE_MOUSE, &error_local)) { /* if just detached for replug, swallow error */ if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_PERMISSION_DENIED)) { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } g_debug("ignoring: %s", error_local->message); } /* FuUdevDevice->close */ return FU_DEVICE_CLASS(fu_synaptics_rmi_hid_device_parent_class)->close(device, error); } static gboolean fu_synaptics_rmi_hid_device_rebind_driver(FuSynapticsRmiDevice *self, GError **error) { const gchar *hid_id; const gchar *driver; const gchar *subsystem; g_autofree gchar *fn_rebind = NULL; g_autofree gchar *fn_unbind = NULL; g_autoptr(FuDevice) parent_hid = NULL; g_autoptr(FuUdevDevice) parent_phys = NULL; g_auto(GStrv) hid_strs = NULL; /* get actual HID node */ parent_hid = fu_device_get_backend_parent_with_subsystem(FU_DEVICE(self), "hid", error); if (parent_hid == NULL) return FALSE; /* build paths */ parent_phys = FU_UDEV_DEVICE( fu_device_get_backend_parent_with_subsystem(FU_DEVICE(self), "i2c", NULL)); if (parent_phys == NULL) { parent_phys = FU_UDEV_DEVICE( fu_device_get_backend_parent_with_subsystem(FU_DEVICE(self), "usb", NULL)); } if (parent_phys == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no parent device for %s", fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(parent_hid))); return FALSE; } /* find the physical ID to use for the rebind */ hid_strs = g_strsplit(fu_udev_device_get_sysfs_path(parent_phys), "/", -1); hid_id = hid_strs[g_strv_length(hid_strs) - 1]; if (hid_id == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no HID_PHYS in %s", fu_udev_device_get_sysfs_path(parent_phys)); return FALSE; } g_debug("HID_PHYS: %s", hid_id); driver = fu_udev_device_get_driver(parent_phys); subsystem = fu_udev_device_get_subsystem(parent_phys); fn_rebind = g_build_filename("/sys/bus/", subsystem, "drivers", driver, "bind", NULL); fn_unbind = g_build_filename("/sys/bus/", subsystem, "drivers", driver, "unbind", NULL); /* unbind hidraw, then bind it again to get a replug */ fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); if (!fu_synaptics_rmi_device_writeln(fn_unbind, hid_id, error)) return FALSE; if (!fu_synaptics_rmi_device_writeln(fn_rebind, hid_id, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_synaptics_rmi_hid_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuSynapticsRmiDevice *self = FU_SYNAPTICS_RMI_DEVICE(device); FuSynapticsRmiFunction *f34; f34 = fu_synaptics_rmi_device_get_function(self, 0x34, error); if (f34 == NULL) return FALSE; if (f34->function_version == 0x0 || f34->function_version == 0x1) { if (!fu_synaptics_rmi_v5_device_detach(device, progress, error)) return FALSE; } else if (f34->function_version == 0x2) { if (!fu_synaptics_rmi_v7_device_detach(device, progress, error)) return FALSE; } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "f34 function version 0x%02x unsupported", f34->function_version); return FALSE; } return fu_synaptics_rmi_hid_device_rebind_driver(self, error); } static gboolean fu_synaptics_rmi_hid_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuSynapticsRmiDevice *self = FU_SYNAPTICS_RMI_DEVICE(device); /* sanity check */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in runtime mode, skipping"); return TRUE; } /* reset device */ if (!fu_synaptics_rmi_device_reset(self, error)) return FALSE; /* rebind to rescan PDT with new firmware running */ return fu_synaptics_rmi_hid_device_rebind_driver(self, error); } static gboolean fu_synaptics_rmi_hid_device_set_page(FuSynapticsRmiDevice *self, guint8 page, GError **error) { g_autoptr(GByteArray) req = g_byte_array_new(); fu_byte_array_append_uint8(req, page); if (!fu_synaptics_rmi_device_write(self, RMI_DEVICE_PAGE_SELECT_REGISTER, req, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to set RMA page 0x%x: ", page); return FALSE; } return TRUE; } static gboolean fu_synaptics_rmi_hid_device_disable_sleep(FuSynapticsRmiDevice *rmi_device, GError **error) { FuSynapticsRmiFunction *f01; g_autoptr(GByteArray) f01_control0 = NULL; f01 = fu_synaptics_rmi_device_get_function(rmi_device, 0x34, error); if (f01 == NULL) return FALSE; f01_control0 = fu_synaptics_rmi_device_read(rmi_device, f01->control_base, 0x1, error); if (f01_control0 == NULL) { g_prefix_error(error, "failed to write get f01_control0: "); return FALSE; } f01_control0->data[0] |= RMI_F01_CRTL0_NOSLEEP_BIT; f01_control0->data[0] = (f01_control0->data[0] & ~RMI_F01_CTRL0_SLEEP_MODE_MASK) | RMI_SLEEP_MODE_NORMAL; if (!fu_synaptics_rmi_device_write(rmi_device, f01->control_base, f01_control0, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write f01_control0: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_synaptics_rmi_hid_device_query_status(FuSynapticsRmiDevice *rmi_device, GError **error) { FuSynapticsRmiFunction *f34; f34 = fu_synaptics_rmi_device_get_function(rmi_device, 0x34, error); if (f34 == NULL) return FALSE; if (f34->function_version == 0x0 || f34->function_version == 0x1) { return fu_synaptics_rmi_v5_device_query_status(rmi_device, error); } if (f34->function_version == 0x2) { return fu_synaptics_rmi_v7_device_query_status(rmi_device, error); } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "f34 function version 0x%02x unsupported", f34->function_version); return FALSE; } static void fu_synaptics_rmi_hid_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 3, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 88, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 7, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static void fu_synaptics_rmi_hid_device_init(FuSynapticsRmiHidDevice *self) { fu_device_set_name(FU_DEVICE(self), "Touchpad"); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_synaptics_rmi_device_set_max_page(FU_SYNAPTICS_RMI_DEVICE(self), 0xff); } static void fu_synaptics_rmi_hid_device_class_init(FuSynapticsRmiHidDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); FuSynapticsRmiDeviceClass *rmi_class = FU_SYNAPTICS_RMI_DEVICE_CLASS(klass); device_class->attach = fu_synaptics_rmi_hid_device_attach; device_class->detach = fu_synaptics_rmi_hid_device_detach; device_class->open = fu_synaptics_rmi_hid_device_open; device_class->close = fu_synaptics_rmi_hid_device_close; device_class->set_progress = fu_synaptics_rmi_hid_device_set_progress; rmi_class->write = fu_synaptics_rmi_hid_device_write; rmi_class->read = fu_synaptics_rmi_hid_device_read; rmi_class->wait_for_attr = fu_synaptics_rmi_hid_device_wait_for_attr; rmi_class->set_page = fu_synaptics_rmi_hid_device_set_page; rmi_class->query_status = fu_synaptics_rmi_hid_device_query_status; rmi_class->read_packet_register = fu_synaptics_rmi_hid_device_read_packet_register; rmi_class->disable_sleep = fu_synaptics_rmi_hid_device_disable_sleep; } fwupd-2.0.10/plugins/synaptics-rmi/fu-synaptics-rmi-hid-device.h000066400000000000000000000006731501337203100245650ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * Copyright 2012 Synaptics Incorporated. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-synaptics-rmi-device.h" #define FU_TYPE_SYNAPTICS_RMI_HID_DEVICE (fu_synaptics_rmi_hid_device_get_type()) G_DECLARE_FINAL_TYPE(FuSynapticsRmiHidDevice, fu_synaptics_rmi_hid_device, FU, SYNAPTICS_RMI_HID_DEVICE, FuSynapticsRmiDevice) fwupd-2.0.10/plugins/synaptics-rmi/fu-synaptics-rmi-plugin.c000066400000000000000000000021531501337203100240500ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-synaptics-rmi-firmware.h" #include "fu-synaptics-rmi-hid-device.h" #include "fu-synaptics-rmi-plugin.h" #include "fu-synaptics-rmi-ps2-device.h" struct _FuSynapticsRmiPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuSynapticsRmiPlugin, fu_synaptics_rmi_plugin, FU_TYPE_PLUGIN) static void fu_synaptics_rmi_plugin_init(FuSynapticsRmiPlugin *self) { } static void fu_synaptics_rmi_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_udev_subsystem(plugin, "hidraw"); fu_plugin_add_udev_subsystem(plugin, "serio"); fu_plugin_add_device_gtype(plugin, FU_TYPE_SYNAPTICS_RMI_HID_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_SYNAPTICS_RMI_PS2_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_SYNAPTICS_RMI_FIRMWARE); } static void fu_synaptics_rmi_plugin_class_init(FuSynapticsRmiPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_synaptics_rmi_plugin_constructed; } fwupd-2.0.10/plugins/synaptics-rmi/fu-synaptics-rmi-plugin.h000066400000000000000000000004341501337203100240550ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuSynapticsRmiPlugin, fu_synaptics_rmi_plugin, FU, SYNAPTICS_RMI_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/synaptics-rmi/fu-synaptics-rmi-ps2-device.c000066400000000000000000000654631501337203100245300ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * Copyright 2020 Synaptics Incorporated. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-synaptics-rmi-ps2-device.h" #include "fu-synaptics-rmi-struct.h" #include "fu-synaptics-rmi-v5-device.h" #include "fu-synaptics-rmi-v7-device.h" struct _FuSynapticsRmiPs2Device { FuSynapticsRmiDevice parent_instance; }; G_DEFINE_TYPE(FuSynapticsRmiPs2Device, fu_synaptics_rmi_ps2_device, FU_TYPE_SYNAPTICS_RMI_DEVICE) #define FU_SYNAPTICS_RMI_DEVICE_BIND_TIMEOUT 1000 /* ms */ static gboolean fu_synaptics_rmi_ps2_device_read_ack(FuSynapticsRmiPs2Device *self, guint8 *pbuf, GError **error) { FuIOChannel *io_channel = fu_udev_device_get_io_channel(FU_UDEV_DEVICE(self)); for (guint i = 0; i < 60; i++) { g_autoptr(GError) error_local = NULL; if (!fu_io_channel_read_raw(io_channel, pbuf, 0x1, NULL, 10, FU_IO_CHANNEL_FLAG_USE_BLOCKING_IO, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT)) { g_warning("read timed out: %u", i); fu_device_sleep(FU_DEVICE(self), 1); /* ms */ continue; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } return TRUE; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "read timed out"); return FALSE; } /* read a single byte from the touchpad */ static gboolean fu_synaptics_rmi_ps2_device_read_byte(FuSynapticsRmiPs2Device *self, guint8 *pbuf, guint timeout, GError **error) { FuIOChannel *io_channel = fu_udev_device_get_io_channel(FU_UDEV_DEVICE(self)); g_return_val_if_fail(timeout > 0, FALSE); return fu_io_channel_read_raw(io_channel, pbuf, 0x1, NULL, timeout, FU_IO_CHANNEL_FLAG_NONE, error); } /* write a single byte to the touchpad and the read the acknowledge */ static gboolean fu_synaptics_rmi_ps2_device_write_byte(FuSynapticsRmiPs2Device *self, guint8 buf, guint timeout, FuSynapticsRmiDeviceFlags flags, GError **error) { FuIOChannel *io_channel = fu_udev_device_get_io_channel(FU_UDEV_DEVICE(self)); gboolean do_write = TRUE; g_return_val_if_fail(timeout > 0, FALSE); for (guint i = 0;; i++) { guint8 res = 0; g_autoptr(GError) error_local = NULL; if (do_write) { if (!fu_io_channel_write_raw(io_channel, &buf, sizeof(buf), timeout, FU_IO_CHANNEL_FLAG_FLUSH_INPUT | FU_IO_CHANNEL_FLAG_USE_BLOCKING_IO, error)) return FALSE; } do_write = FALSE; for (;;) { /* attempt to read acknowledge... */ if (!fu_synaptics_rmi_ps2_device_read_ack(self, &res, &error_local)) { if (i > 3) { g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "read ack failed: "); return FALSE; } g_warning("read ack failed: %s, retrying", error_local->message); break; } if (res == FU_RMI_DATA_PORT_STATUS_ACKNOWLEDGE) return TRUE; if (res == FU_RMI_DATA_PORT_STATUS_RESEND) { do_write = TRUE; g_debug("resend"); fu_device_sleep(FU_DEVICE(self), 1000); /* ms */ break; } if (res == FU_RMI_DATA_PORT_STATUS_ERROR) { do_write = TRUE; g_debug("error"); fu_device_sleep(FU_DEVICE(self), 10); /* ms */ break; } g_debug("other response: 0x%x", res); fu_device_sleep(FU_DEVICE(self), 10); /* ms */ } if (i >= 3) { if (flags & FU_SYNAPTICS_RMI_DEVICE_FLAG_ALLOW_FAILURE) { /* just break without error return because FW * will not return ACK for commands like RESET */ break; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot write byte after retries"); return FALSE; } } /* success */ return TRUE; } static gboolean fu_synaptics_rmi_ps2_device_set_resolution_sequence(FuSynapticsRmiPs2Device *self, guint8 arg, gboolean send_e6s, GError **error) { /* send set scaling twice if send_e6s */ for (gint i = send_e6s ? 2 : 1; i > 0; --i) { if (!fu_synaptics_rmi_ps2_device_write_byte(self, FU_RMI_EDP_COMMAND_AUX_SET_SCALING1_TO1, 50, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) return FALSE; } for (gint i = 3; i >= 0; --i) { guint8 ucTwoBitArg = (arg >> (i * 2)) & 0x3; if (!fu_synaptics_rmi_ps2_device_write_byte(self, FU_RMI_EDP_COMMAND_AUX_SET_RESOLUTION, 50, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { return FALSE; } if (!fu_synaptics_rmi_ps2_device_write_byte(self, ucTwoBitArg, 50, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_synaptics_rmi_ps2_device_status_request(FuSynapticsRmiPs2Device *self, FuRmiStatusRequest ucArgument, guint32 *buf, GError **error) { gboolean success = FALSE; /* allow 3 retries */ for (guint i = 0; i < 3; ++i) { g_autoptr(GError) error_local = NULL; if (!fu_synaptics_rmi_ps2_device_set_resolution_sequence(self, ucArgument, FALSE, &error_local)) { g_debug("failed set try #%u: %s", i, error_local->message); continue; } if (!fu_synaptics_rmi_ps2_device_write_byte(self, FU_RMI_EDP_COMMAND_AUX_STATUS_REQUEST, 10, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, &error_local)) { g_debug("failed write try #%u: %s", i, error_local->message); continue; } success = TRUE; break; } if (success == FALSE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed"); return FALSE; } /* read the response from the status request */ for (gint i = 0; i < 3; ++i) { guint8 tmp = 0x0; if (!fu_synaptics_rmi_ps2_device_read_byte(self, &tmp, 10, error)) { g_prefix_error(error, "failed to read byte: "); return FALSE; } *buf = ((*buf) << 8) | tmp; } return TRUE; } static gboolean fu_synaptics_rmi_ps2_device_sample_rate(FuSynapticsRmiPs2Device *self, FuRmiSetSampleRate param, FuRmiEdpCommand arg, gboolean send_e6s, GError **error) { /* allow 3 retries */ for (guint i = 0;; i++) { g_autoptr(GError) error_local = NULL; if (i > 0) { /* always send two E6s when retrying */ send_e6s = TRUE; } if (!fu_synaptics_rmi_ps2_device_set_resolution_sequence(self, arg, send_e6s, &error_local) || !fu_synaptics_rmi_ps2_device_write_byte(self, FU_RMI_EDP_COMMAND_AUX_SET_SAMPLE_RATE, 50, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, &error_local) || !fu_synaptics_rmi_ps2_device_write_byte(self, param, 50, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, &error_local)) { if (i > 3) { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } g_warning("failed, will retry: %s", error_local->message); continue; } break; } /* success */ return TRUE; } static gboolean fu_synaptics_rmi_ps2_device_detect_synaptics_styk(FuSynapticsRmiPs2Device *self, gboolean *result, GError **error) { guint8 buf; if (!fu_synaptics_rmi_ps2_device_write_byte(self, FU_RMI_EDP_COMMAND_AUX_IBM_READ_SECONDARY_ID, 10, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write IBMReadSecondaryID(0xE1): "); return FALSE; } if (!fu_synaptics_rmi_ps2_device_read_byte(self, &buf, 10, error)) { g_prefix_error(error, "failed to receive IBMReadSecondaryID: "); return FALSE; } if (buf == FU_RMI_STICK_DEVICE_TYPE_JYT_SYNA || buf == FU_RMI_STICK_DEVICE_TYPE_SYNAPTICS) *result = TRUE; return TRUE; } static gboolean fu_synaptics_rmi_ps2_device_query_build_id(FuSynapticsRmiDevice *rmi_device, guint32 *build_id, GError **error) { FuSynapticsRmiPs2Device *self = FU_SYNAPTICS_RMI_PS2_DEVICE(rmi_device); guint32 buf = 0; gboolean is_synaptics_styk = FALSE; FuRmiDeviceResponse esdr; if (!fu_synaptics_rmi_ps2_device_status_request(self, FU_RMI_STATUS_REQUEST_IDENTIFY_SYNAPTICS, &buf, error)) { g_prefix_error(error, "failed to request IdentifySynaptics: "); return FALSE; } g_debug("identify Synaptics response = 0x%x", buf); esdr = (buf & 0xFF00) >> 8; if (!fu_synaptics_rmi_ps2_device_detect_synaptics_styk(self, &is_synaptics_styk, error)) { g_prefix_error(error, "failed to detect Synaptics styk: "); return FALSE; } fu_synaptics_rmi_device_set_iepmode(rmi_device, FALSE); if (esdr == FU_RMI_DEVICE_RESPONSE_TOUCH_PAD || is_synaptics_styk) { /* Get the firmware id from the Extra Capabilities 2 Byte * The firmware id is located in bits 0 - 23 */ if (!fu_synaptics_rmi_ps2_device_status_request( self, FU_RMI_STATUS_REQUEST_READ_EXTRA_CAPABILITIES2, build_id, error)) { g_prefix_error(error, "failed to read extraCapabilities2: "); return FALSE; } } return TRUE; } static gboolean fu_synaptics_rmi_ps2_device_query_product_sub_id(FuSynapticsRmiDevice *rmi_device, guint8 *sub_id, GError **error) { FuSynapticsRmiPs2Device *self = FU_SYNAPTICS_RMI_PS2_DEVICE(rmi_device); guint32 buf = 0; if (!fu_synaptics_rmi_ps2_device_status_request(self, FU_RMI_STATUS_REQUEST_READ_CAPABILITIES, &buf, error)) { g_prefix_error(error, "failed to status_request_sequence read esrReadCapabilities: "); return FALSE; } *sub_id = (buf >> 8) & 0xFF; return TRUE; } static gboolean fu_synaptics_rmi_ps2_device_enter_iep_mode(FuSynapticsRmiDevice *rmi_device, GError **error) { FuSynapticsRmiPs2Device *self = FU_SYNAPTICS_RMI_PS2_DEVICE(rmi_device); /* disable stream */ if (!fu_synaptics_rmi_ps2_device_write_byte(self, FU_RMI_EDP_COMMAND_AUX_DISABLE, 50, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to disable stream mode: "); return FALSE; } /* enable RMI mode */ if (!fu_synaptics_rmi_ps2_device_sample_rate(self, FU_RMI_SET_SAMPLE_RATE_SET_MODE_BYTE2, FU_RMI_EDP_COMMAND_AUX_FULL_RMI_BACK_DOOR, FALSE, error)) { g_prefix_error(error, "failed to enter RMI mode: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_synaptics_rmi_ps2_device_write_rmi_register(FuSynapticsRmiPs2Device *self, guint8 addr, const guint8 *buf, guint8 buflen, guint timeout, FuSynapticsRmiDeviceFlags flags, GError **error) { g_return_val_if_fail(timeout > 0, FALSE); if (!fu_synaptics_rmi_device_enter_iep_mode(FU_SYNAPTICS_RMI_DEVICE(self), FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) return FALSE; if (!fu_synaptics_rmi_ps2_device_write_byte(self, FU_RMI_EDP_COMMAND_AUX_SET_SCALING2_TO1, timeout, flags, error)) { g_prefix_error(error, "failed to edpAuxSetScaling2To1: "); return FALSE; } if (!fu_synaptics_rmi_ps2_device_write_byte(self, FU_RMI_EDP_COMMAND_AUX_SET_SAMPLE_RATE, timeout, flags, error)) { g_prefix_error(error, "failed to edpAuxSetSampleRate: "); return FALSE; } if (!fu_synaptics_rmi_ps2_device_write_byte(self, addr, timeout, flags, error)) { g_prefix_error(error, "failed to write address: "); return FALSE; } for (guint8 i = 0; i < buflen; i++) { if (!fu_synaptics_rmi_ps2_device_write_byte(self, FU_RMI_EDP_COMMAND_AUX_SET_SAMPLE_RATE, timeout, flags, error)) { g_prefix_error(error, "failed to set byte %u: ", i); return FALSE; } if (!fu_synaptics_rmi_ps2_device_write_byte(self, buf[i], timeout, flags, error)) { g_prefix_error(error, "failed to write byte %u: ", i); return FALSE; } } /* success */ fu_device_sleep(FU_DEVICE(self), 20); /* ms */ return TRUE; } static gboolean fu_synaptics_rmi_ps2_device_read_rmi_register(FuSynapticsRmiPs2Device *self, guint8 addr, guint8 *buf, GError **error) { g_return_val_if_fail(buf != NULL, FALSE); if (!fu_synaptics_rmi_device_enter_iep_mode(FU_SYNAPTICS_RMI_DEVICE(self), FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) return FALSE; for (guint retries = 0;; retries++) { g_autoptr(GError) error_local = NULL; if (!fu_synaptics_rmi_ps2_device_write_byte(self, FU_RMI_EDP_COMMAND_AUX_SET_SCALING2_TO1, 50, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error) || !fu_synaptics_rmi_ps2_device_write_byte(self, FU_RMI_EDP_COMMAND_AUX_SET_SAMPLE_RATE, 50, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error) || !fu_synaptics_rmi_ps2_device_write_byte(self, addr, 50, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error) || !fu_synaptics_rmi_ps2_device_write_byte(self, FU_RMI_EDP_COMMAND_AUX_STATUS_REQUEST, 50, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write command in Read RMI register: "); return FALSE; } if (!fu_synaptics_rmi_ps2_device_read_byte(self, buf, 10, &error_local)) { if (retries++ > 2) { g_propagate_prefixed_error( error, g_steal_pointer(&error_local), "failed to read byte @0x%x after %u retries: ", addr, retries); return FALSE; } g_debug("failed to read byte @0x%x: %s", addr, error_local->message); continue; } /* success */ break; } /* success */ fu_device_sleep(FU_DEVICE(self), 20); /* ms */ return TRUE; } static GByteArray * fu_synaptics_rmi_ps2_device_read_rmi_packet_register(FuSynapticsRmiPs2Device *self, guint8 addr, guint req_sz, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); if (!fu_synaptics_rmi_device_enter_iep_mode(FU_SYNAPTICS_RMI_DEVICE(self), FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) return NULL; if (!fu_synaptics_rmi_ps2_device_write_byte(self, FU_RMI_EDP_COMMAND_AUX_SET_SCALING2_TO1, 50, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error) || !fu_synaptics_rmi_ps2_device_write_byte(self, FU_RMI_EDP_COMMAND_AUX_SET_SAMPLE_RATE, 50, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error) || !fu_synaptics_rmi_ps2_device_write_byte(self, addr, 50, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error) || !fu_synaptics_rmi_ps2_device_write_byte(self, FU_RMI_EDP_COMMAND_AUX_STATUS_REQUEST, 50, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write command in Read RMI Packet Register: "); return NULL; } for (guint i = 0; i < req_sz; ++i) { guint8 tmp = 0; if (!fu_synaptics_rmi_ps2_device_read_byte(self, &tmp, 10, error)) { g_prefix_error(error, "failed to read byte %u: ", i); return NULL; } fu_byte_array_append_uint8(buf, tmp); } fu_device_sleep(FU_DEVICE(self), 20); /* ms */ return g_steal_pointer(&buf); } static gboolean fu_synaptics_rmi_ps2_device_query_status(FuSynapticsRmiDevice *rmi_device, GError **error) { FuSynapticsRmiFunction *f34; g_debug("ps2 query status"); f34 = fu_synaptics_rmi_device_get_function(rmi_device, 0x34, error); if (f34 == NULL) return FALSE; if (f34->function_version == 0x0 || f34->function_version == 0x1) { return fu_synaptics_rmi_v5_device_query_status(rmi_device, error); } if (f34->function_version == 0x2) { return fu_synaptics_rmi_v7_device_query_status(rmi_device, error); } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "f34 function version 0x%02x unsupported", f34->function_version); return FALSE; } static gboolean fu_synaptics_rmi_ps2_device_set_page(FuSynapticsRmiDevice *rmi_device, guint8 page, GError **error) { FuSynapticsRmiPs2Device *self = FU_SYNAPTICS_RMI_PS2_DEVICE(rmi_device); if (!fu_synaptics_rmi_ps2_device_write_rmi_register(self, RMI_DEVICE_PAGE_SELECT_REGISTER, &page, 1, 20, FALSE, error)) { g_prefix_error(error, "failed to write page %u: ", page); return FALSE; } return TRUE; } static GByteArray * fu_synaptics_rmi_ps2_device_read(FuSynapticsRmiDevice *rmi_device, guint16 addr, gsize req_sz, GError **error) { FuSynapticsRmiPs2Device *self = FU_SYNAPTICS_RMI_PS2_DEVICE(rmi_device); g_autoptr(GByteArray) buf = NULL; g_autofree gchar *dump = NULL; if (!fu_synaptics_rmi_device_set_page(rmi_device, addr >> 8, error)) { g_prefix_error(error, "failed to set RMI page: "); return NULL; } for (guint retries = 0;; retries++) { buf = g_byte_array_new(); for (guint i = 0; i < req_sz; i++) { guint8 tmp = 0x0; if (!fu_synaptics_rmi_ps2_device_read_rmi_register( self, (guint8)((addr & 0x00FF) + i), &tmp, error)) { g_prefix_error(error, "failed register read 0x%x: ", addr + i); return NULL; } fu_byte_array_append_uint8(buf, tmp); } if (buf->len != req_sz) { g_debug("buf->len(%u) != req_sz(%u)", buf->len, (guint)req_sz); if (retries++ > 2) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "buffer length did not match: %u vs %u", buf->len, (guint)req_sz); return NULL; } continue; } break; } dump = g_strdup_printf("R %x", addr); fu_dump_full(G_LOG_DOMAIN, dump, buf->data, buf->len, 80, FU_DUMP_FLAGS_NONE); return g_steal_pointer(&buf); } static GByteArray * fu_synaptics_rmi_ps2_device_read_packet_register(FuSynapticsRmiDevice *rmi_device, guint16 addr, gsize req_sz, GError **error) { FuSynapticsRmiPs2Device *self = FU_SYNAPTICS_RMI_PS2_DEVICE(rmi_device); g_autoptr(GByteArray) buf = NULL; g_autofree gchar *dump = g_strdup_printf("R %x", addr); if (!fu_synaptics_rmi_device_set_page(rmi_device, addr >> 8, error)) { g_prefix_error(error, "failed to set RMI page: "); return NULL; } buf = fu_synaptics_rmi_ps2_device_read_rmi_packet_register(self, addr, req_sz, error); if (buf == NULL) { g_prefix_error(error, "failed packet register read %x: ", addr); return NULL; } fu_dump_full(G_LOG_DOMAIN, dump, buf->data, buf->len, 80, FU_DUMP_FLAGS_NONE); return g_steal_pointer(&buf); } static gboolean fu_synaptics_rmi_ps2_device_write(FuSynapticsRmiDevice *rmi_device, guint16 addr, GByteArray *req, FuSynapticsRmiDeviceFlags flags, GError **error) { FuSynapticsRmiPs2Device *self = FU_SYNAPTICS_RMI_PS2_DEVICE(rmi_device); g_autofree gchar *str = g_strdup_printf("W %x", addr); if (!fu_synaptics_rmi_device_set_page(rmi_device, addr >> 8, error)) { g_prefix_error(error, "failed to set RMI page: "); return FALSE; } if (!fu_synaptics_rmi_ps2_device_write_rmi_register(self, addr & 0x00FF, req->data, req->len, 1000, /* timeout */ flags, error)) { g_prefix_error(error, "failed to write register %x: ", addr); return FALSE; } fu_dump_full(G_LOG_DOMAIN, str, req->data, req->len, 80, FU_DUMP_FLAGS_NONE); return TRUE; } static gboolean fu_synaptics_rmi_ps2_device_write_bus_select(FuSynapticsRmiDevice *rmi_device, guint8 bus, GError **error) { g_autoptr(GByteArray) req = g_byte_array_new(); fu_byte_array_append_uint8(req, bus); if (!fu_synaptics_rmi_ps2_device_write(rmi_device, RMI_DEVICE_BUS_SELECT_REGISTER, req, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write rmi register %u: ", bus); return FALSE; } return TRUE; } static gboolean fu_synaptics_rmi_ps2_device_probe(FuDevice *device, GError **error) { /* psmouse is the usual mode, but serio is needed for update */ if (g_strcmp0(fu_udev_device_get_driver(FU_UDEV_DEVICE(device)), "serio_raw") == 0) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); } else { fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); } return TRUE; } static gboolean fu_synaptics_rmi_ps2_device_open(FuDevice *device, GError **error) { FuSynapticsRmiPs2Device *self = FU_SYNAPTICS_RMI_PS2_DEVICE(device); guint8 buf[2] = {0x0}; /* FuUdevDevice->open */ if (!FU_DEVICE_CLASS(fu_synaptics_rmi_ps2_device_parent_class)->open(device, error)) return FALSE; /* in serio_raw mode */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { /* clear out any data in the serio_raw queue */ for (guint i = 0; i < 0xffff; i++) { guint8 tmp = 0; if (!fu_synaptics_rmi_ps2_device_read_byte(self, &tmp, 20, NULL)) break; } /* send reset -- may take 300-500ms */ if (!fu_synaptics_rmi_ps2_device_write_byte(self, FU_RMI_EDP_COMMAND_AUX_RESET, 600, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to reset: "); return FALSE; } /* read the 0xAA 0x00 announcing the touchpad is ready */ if (!fu_synaptics_rmi_ps2_device_read_byte(self, &buf[0], 500, error) || !fu_synaptics_rmi_ps2_device_read_byte(self, &buf[1], 500, error)) { g_prefix_error(error, "failed to read 0xAA00: "); return FALSE; } if (buf[0] != 0xAA || buf[1] != 0x00) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to read 0xAA00, got 0x%02X%02X: ", buf[0], buf[1]); return FALSE; } /* disable the device so that it stops reporting finger data */ if (!fu_synaptics_rmi_ps2_device_write_byte(self, FU_RMI_EDP_COMMAND_AUX_DISABLE, 50, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to disable stream mode: "); return FALSE; } } /* success */ return TRUE; } static gboolean fu_synaptics_rmi_ps2_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuSynapticsRmiDevice *self = FU_SYNAPTICS_RMI_DEVICE(device); FuSynapticsRmiFunction *f34; /* sanity check */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in bootloader mode, skipping"); return TRUE; } /* put in serio_raw mode so that we can do register writes */ if (!fu_udev_device_write_sysfs(FU_UDEV_DEVICE(device), "drvctl", "serio_raw", FU_SYNAPTICS_RMI_DEVICE_BIND_TIMEOUT, error)) { g_prefix_error(error, "failed to write to drvctl: "); return FALSE; } /* rescan device */ if (!fu_device_close(device, error)) return FALSE; if (!fu_device_rescan(device, error)) return FALSE; if (!fu_device_open(device, error)) return FALSE; f34 = fu_synaptics_rmi_device_get_function(self, 0x34, error); if (f34 == NULL) return FALSE; if (f34->function_version == 0x0 || f34->function_version == 0x1) { if (!fu_synaptics_rmi_v5_device_detach(device, progress, error)) return FALSE; } else if (f34->function_version == 0x2) { if (!fu_synaptics_rmi_v7_device_detach(device, progress, error)) return FALSE; } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "f34 function version 0x%02x unsupported", f34->function_version); return FALSE; } /* set iepmode before querying device forcibly because of FW requirement */ if (!fu_synaptics_rmi_device_enter_iep_mode(self, FU_SYNAPTICS_RMI_DEVICE_FLAG_FORCE, error)) return FALSE; if (!fu_synaptics_rmi_ps2_device_query_status(self, error)) { g_prefix_error(error, "failed to query status after detach: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_synaptics_rmi_ps2_device_setup(FuDevice *device, GError **error) { /* we can only scan the PDT in serio_raw mode */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) return TRUE; return FU_DEVICE_CLASS(fu_synaptics_rmi_ps2_device_parent_class)->setup(device, error); } static gboolean fu_synaptics_rmi_ps2_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuSynapticsRmiDevice *rmi_device = FU_SYNAPTICS_RMI_DEVICE(device); /* sanity check */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in runtime mode, skipping"); return TRUE; } /* set iepmode before reset device forcibly because of FW requirement */ fu_synaptics_rmi_device_set_iepmode(rmi_device, FALSE); /* delay after writing */ fu_device_sleep_full(device, 2000, progress); /* ms */ /* reset device */ if (!fu_synaptics_rmi_device_enter_iep_mode(rmi_device, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) return FALSE; if (!fu_synaptics_rmi_device_reset(rmi_device, error)) { g_prefix_error(error, "failed to reset device: "); return FALSE; } fu_device_sleep_full(device, 5000, progress); /* ms */ /* back to psmouse */ if (!fu_udev_device_write_sysfs(FU_UDEV_DEVICE(device), "drvctl", "psmouse", FU_SYNAPTICS_RMI_DEVICE_BIND_TIMEOUT, error)) { g_prefix_error(error, "failed to write to drvctl: "); return FALSE; } /* rescan device */ return fu_device_rescan(device, error); } static void fu_synaptics_rmi_ps2_device_init(FuSynapticsRmiPs2Device *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_set_name(FU_DEVICE(self), "TouchStyk"); fu_device_set_vendor(FU_DEVICE(self), "Synaptics"); fu_device_build_vendor_id_u16(FU_DEVICE(self), "HIDRAW", 0x06CB); fu_synaptics_rmi_device_set_max_page(FU_SYNAPTICS_RMI_DEVICE(self), 0x1); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); } static gboolean fu_synaptics_rmi_ps2_device_wait_for_attr(FuSynapticsRmiDevice *rmi_device, guint8 source_mask, guint timeout_ms, GError **error) { fu_device_sleep(FU_DEVICE(rmi_device), timeout_ms); return TRUE; } static void fu_synaptics_rmi_ps2_device_class_init(FuSynapticsRmiPs2DeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); FuSynapticsRmiDeviceClass *rmi_class = FU_SYNAPTICS_RMI_DEVICE_CLASS(klass); device_class->attach = fu_synaptics_rmi_ps2_device_attach; device_class->detach = fu_synaptics_rmi_ps2_device_detach; device_class->setup = fu_synaptics_rmi_ps2_device_setup; device_class->probe = fu_synaptics_rmi_ps2_device_probe; device_class->open = fu_synaptics_rmi_ps2_device_open; rmi_class->read = fu_synaptics_rmi_ps2_device_read; rmi_class->write = fu_synaptics_rmi_ps2_device_write; rmi_class->set_page = fu_synaptics_rmi_ps2_device_set_page; rmi_class->query_status = fu_synaptics_rmi_ps2_device_query_status; rmi_class->query_build_id = fu_synaptics_rmi_ps2_device_query_build_id; rmi_class->query_product_sub_id = fu_synaptics_rmi_ps2_device_query_product_sub_id; rmi_class->wait_for_attr = fu_synaptics_rmi_ps2_device_wait_for_attr; rmi_class->enter_iep_mode = fu_synaptics_rmi_ps2_device_enter_iep_mode; rmi_class->write_bus_select = fu_synaptics_rmi_ps2_device_write_bus_select; rmi_class->read_packet_register = fu_synaptics_rmi_ps2_device_read_packet_register; } fwupd-2.0.10/plugins/synaptics-rmi/fu-synaptics-rmi-ps2-device.h000066400000000000000000000006731501337203100245250ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * Copyright 2020 Synaptics Incorporated. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-synaptics-rmi-device.h" #define FU_TYPE_SYNAPTICS_RMI_PS2_DEVICE (fu_synaptics_rmi_ps2_device_get_type()) G_DECLARE_FINAL_TYPE(FuSynapticsRmiPs2Device, fu_synaptics_rmi_ps2_device, FU, SYNAPTICS_RMI_PS2_DEVICE, FuSynapticsRmiDevice) fwupd-2.0.10/plugins/synaptics-rmi/fu-synaptics-rmi-v5-device.c000066400000000000000000000440051501337203100243430ustar00rootroot00000000000000/* * Copyright 2012 Andrew Duggan * Copyright 2012 Synaptics Inc. * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-synaptics-rmi-firmware.h" #include "fu-synaptics-rmi-v5-device.h" #define RMI_F34_WRITE_FW_BLOCK 0x02 #define RMI_F34_ERASE_ALL 0x03 #define RMI_F34_WRITE_LOCKDOWN_BLOCK 0x04 #define RMI_F34_WRITE_CONFIG_BLOCK 0x06 #define RMI_F34_WRITE_SIGNATURE 0x0b #define RMI_F34_ENABLE_FLASH_PROG 0x0f #define RMI_F34_BLOCK_SIZE_OFFSET 1 #define RMI_F34_FW_BLOCKS_OFFSET 3 #define RMI_F34_CONFIG_BLOCKS_OFFSET 5 #define RMI_F34_ERASE_WAIT_MS (5 * 1000) /* ms */ gboolean fu_synaptics_rmi_v5_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuSynapticsRmiDevice *self = FU_SYNAPTICS_RMI_DEVICE(device); FuSynapticsRmiFlash *flash = fu_synaptics_rmi_device_get_flash(self); g_autoptr(GByteArray) enable_req = g_byte_array_new(); /* sanity check */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in bootloader mode, skipping"); return TRUE; } /* disable interrupts */ if (!fu_synaptics_rmi_device_disable_irqs(self, error)) return FALSE; if (!fu_synaptics_rmi_device_write_bus_select(self, 0, error)) { g_prefix_error(error, "failed to write bus select: "); return FALSE; } /* unlock bootloader and rebind kernel driver */ if (!fu_synaptics_rmi_device_write_bootloader_id(self, error)) return FALSE; fu_byte_array_append_uint8(enable_req, RMI_F34_ENABLE_FLASH_PROG); if (!fu_synaptics_rmi_device_write(self, flash->status_addr, enable_req, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to enable programming: "); return FALSE; } fu_device_sleep(device, RMI_F34_ENABLE_WAIT_MS); return TRUE; } static gboolean fu_synaptics_rmi_v5_device_erase_all(FuSynapticsRmiDevice *self, GError **error) { FuSynapticsRmiFunction *f34; FuSynapticsRmiFlash *flash = fu_synaptics_rmi_device_get_flash(self); g_autoptr(GByteArray) erase_cmd = g_byte_array_new(); /* f34 */ f34 = fu_synaptics_rmi_device_get_function(self, 0x34, error); if (f34 == NULL) return FALSE; /* all other versions */ fu_byte_array_append_uint8(erase_cmd, RMI_F34_ERASE_ALL); if (!fu_synaptics_rmi_device_write(self, flash->status_addr, erase_cmd, FU_SYNAPTICS_RMI_DEVICE_FLAG_ALLOW_FAILURE, error)) { g_prefix_error(error, "failed to erase core config: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), RMI_F34_ERASE_WAIT_MS); fu_synaptics_rmi_device_set_iepmode(self, FALSE); if (!fu_synaptics_rmi_device_enter_iep_mode(self, FU_SYNAPTICS_RMI_DEVICE_FLAG_FORCE, error)) return FALSE; if (!fu_synaptics_rmi_device_wait_for_idle(self, RMI_F34_ERASE_WAIT_MS, RMI_DEVICE_WAIT_FOR_IDLE_FLAG_REFRESH_F34, error)) { g_prefix_error(error, "failed to wait for idle for erase: "); return FALSE; } return TRUE; } static gboolean fu_synaptics_rmi_v5_device_write_block(FuSynapticsRmiDevice *self, guint8 cmd, guint32 address, const guint8 *data, gsize datasz, GError **error) { g_autoptr(GByteArray) req = g_byte_array_new(); g_byte_array_append(req, data, datasz); fu_byte_array_append_uint8(req, cmd); if (!fu_synaptics_rmi_device_write(self, address, req, FU_SYNAPTICS_RMI_DEVICE_FLAG_ALLOW_FAILURE, error)) { g_prefix_error(error, "failed to write block @0x%x: ", address); return FALSE; } if (!fu_synaptics_rmi_device_wait_for_idle(self, RMI_F34_IDLE_WAIT_MS, RMI_DEVICE_WAIT_FOR_IDLE_FLAG_NONE, error)) { g_prefix_error(error, "failed to wait for idle @0x%x: ", address); return FALSE; } return TRUE; } static gboolean fu_synaptics_rmi_v5_device_secure_check(FuDevice *device, GBytes *payload, GBytes *signature, GError **error) { FuSynapticsRmiDevice *self = FU_SYNAPTICS_RMI_DEVICE(device); FuSynapticsRmiFunction *f34; guint16 rsa_pubkey_len = fu_synaptics_rmi_device_get_sig_size(self) / 8; guint16 rsa_block_cnt = rsa_pubkey_len / 3; guint16 rsa_block_remain = rsa_pubkey_len % 3; g_autoptr(GByteArray) pubkey_buf = g_byte_array_new(); g_autoptr(GBytes) pubkey = NULL; fu_dump_bytes(G_LOG_DOMAIN, "Signature", signature); f34 = fu_synaptics_rmi_device_get_function(self, 0x34, error); if (f34 == NULL) return FALSE; /* parse RSA public key modulus */ if (rsa_block_remain > 0) rsa_block_cnt += 1; for (guint retries = 0;; retries++) { /* need read another register to reset the offset of packet register */ if (!fu_synaptics_rmi_v5_device_query_status(self, error)) { g_prefix_error(error, "failed to read status: "); return FALSE; } if (!fu_synaptics_rmi_device_enter_iep_mode(self, FU_SYNAPTICS_RMI_DEVICE_FLAG_FORCE, error)) return FALSE; for (guint16 block_num = 0; block_num < rsa_block_cnt; block_num++) { g_autoptr(GByteArray) res = NULL; res = fu_synaptics_rmi_device_read_packet_register( self, f34->query_base + 14, /* addr of flash properties + 5 */ 0x3, error); if (res == NULL) return FALSE; if (res->len != 0x3) g_debug("read %u bytes in return", res->len); if (rsa_block_remain && block_num + 1 == rsa_block_cnt) { g_byte_array_remove_range(res, rsa_block_remain, res->len - rsa_block_remain); } for (guint i = 0; i < res->len / 2; i++) { guint8 tmp = res->data[i]; res->data[i] = res->data[res->len - i - 1]; res->data[res->len - i - 1] = tmp; } if (rsa_block_remain && block_num + 1 == rsa_block_cnt) { g_byte_array_prepend(pubkey_buf, res->data, rsa_block_remain); } else { g_byte_array_prepend(pubkey_buf, res->data, res->len); } } if (rsa_pubkey_len != pubkey_buf->len) { if (retries++ > 2) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "RSA public key length not matched %u: after %u retries: ", pubkey_buf->len, retries); return FALSE; } g_byte_array_set_size(pubkey_buf, 0); continue; } /* success */ break; } fu_dump_full(G_LOG_DOMAIN, "RSA public key", pubkey_buf->data, pubkey_buf->len, 16, FU_DUMP_FLAGS_NONE); /* sanity check size */ if (rsa_pubkey_len != pubkey_buf->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "RSA public key length did not match: %u != %u: ", rsa_pubkey_len, pubkey_buf->len); return FALSE; } pubkey = g_bytes_new(pubkey_buf->data, pubkey_buf->len); return fu_synaptics_rmi_verify_sha256_signature(payload, pubkey, signature, error); } gboolean fu_synaptics_rmi_v5_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuSynapticsRmiDevice *self = FU_SYNAPTICS_RMI_DEVICE(device); FuSynapticsRmiFlash *flash = fu_synaptics_rmi_device_get_flash(self); FuSynapticsRmiFunction *f34; FuSynapticsRmiFirmware *rmi_firmware = FU_SYNAPTICS_RMI_FIRMWARE(firmware); guint32 address; guint32 firmware_length = fu_firmware_get_size(firmware) - fu_synaptics_rmi_firmware_get_sig_size(rmi_firmware); g_autoptr(GBytes) bytes_bin = NULL; g_autoptr(GBytes) bytes_cfg = NULL; g_autoptr(GBytes) signature_bin = NULL; g_autoptr(GBytes) firmware_bin = NULL; g_autoptr(FuChunkArray) chunks_bin = NULL; g_autoptr(FuChunkArray) chunks_cfg = NULL; g_autoptr(GByteArray) req_addr = g_byte_array_new(); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 10, "enter-iep"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 10, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 90, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 5, "write-signature"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 10, "cfg-image"); /* we should be in bootloader mode now, but check anyway */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not bootloader, perhaps need detach?!"); return FALSE; } if (!fu_synaptics_rmi_device_enter_iep_mode(self, FU_SYNAPTICS_RMI_DEVICE_FLAG_FORCE, error)) return FALSE; /* check is idle */ if (!fu_synaptics_rmi_device_wait_for_idle(self, 0, RMI_DEVICE_WAIT_FOR_IDLE_FLAG_REFRESH_F34, error)) { g_prefix_error(error, "not idle: "); return FALSE; } if (fu_synaptics_rmi_firmware_get_sig_size(rmi_firmware) == 0 && fu_synaptics_rmi_device_get_sig_size(self) != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "device secure but firmware not secure"); return FALSE; } if (fu_synaptics_rmi_firmware_get_sig_size(rmi_firmware) != 0 && fu_synaptics_rmi_device_get_sig_size(self) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "device not secure but firmware secure"); return FALSE; } /* f34 */ f34 = fu_synaptics_rmi_device_get_function(self, 0x34, error); if (f34 == NULL) return FALSE; /* get both images */ bytes_bin = fu_firmware_get_image_by_id_bytes(firmware, "ui", error); if (bytes_bin == NULL) return FALSE; bytes_cfg = fu_firmware_get_image_by_id_bytes(firmware, "config", error); if (bytes_cfg == NULL) return FALSE; /* verify signature if set */ firmware_bin = fu_bytes_new_offset(bytes_bin, 0, firmware_length, error); if (firmware_bin == NULL) return FALSE; signature_bin = fu_firmware_get_image_by_id_bytes(firmware, "sig", NULL); if (signature_bin != NULL) { if (!fu_synaptics_rmi_v5_device_secure_check(device, firmware_bin, signature_bin, error)) { g_prefix_error(error, "secure check failed: "); return FALSE; } } /* disable powersaving */ if (!fu_synaptics_rmi_device_disable_sleep(self, error)) { g_prefix_error(error, "failed to disable sleep: "); return FALSE; } /* unlock again */ if (!fu_synaptics_rmi_device_write_bootloader_id(self, error)) { g_prefix_error(error, "failed to unlock again: "); return FALSE; } fu_progress_step_done(progress); /* erase all */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_ERASE); if (!fu_synaptics_rmi_v5_device_erase_all(self, error)) { g_prefix_error(error, "failed to erase all: "); return FALSE; } fu_progress_step_done(progress); /* write initial address */ fu_byte_array_append_uint16(req_addr, 0x0, G_LITTLE_ENDIAN); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); if (!fu_synaptics_rmi_device_write(self, f34->data_base, req_addr, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write 1st address zero: "); return FALSE; } /* write each block */ if (f34->function_version == 0x01) address = f34->data_base + RMI_F34_BLOCK_DATA_V1_OFFSET; else address = f34->data_base + RMI_F34_BLOCK_DATA_OFFSET; chunks_bin = fu_chunk_array_new_from_bytes(firmware_bin, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, flash->block_size); chunks_cfg = fu_chunk_array_new_from_bytes(bytes_cfg, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, flash->block_size); for (guint i = 0; i < fu_chunk_array_length(chunks_bin); i++) { g_autoptr(FuChunk) chk = fu_chunk_array_index(chunks_bin, i, error); if (chk == NULL) return FALSE; if (!fu_synaptics_rmi_v5_device_write_block(self, RMI_F34_WRITE_FW_BLOCK, address, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "failed to write bin block %u: ", fu_chunk_get_idx(chk)); return FALSE; } fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)i + 1, (gsize)fu_chunk_array_length(chunks_bin)); } fu_progress_step_done(progress); /* payload signature */ if (signature_bin != NULL && fu_synaptics_rmi_device_get_sig_size(self) != 0) { FuProgress *progress_child = fu_progress_get_child(progress); g_autoptr(FuChunkArray) chunks_sig = NULL; chunks_sig = fu_chunk_array_new_from_bytes(signature_bin, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, flash->block_size); if (!fu_synaptics_rmi_device_write(self, f34->data_base, req_addr, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write 1st address zero: "); return FALSE; } fu_progress_set_id(progress_child, G_STRLOC); fu_progress_set_steps(progress_child, fu_chunk_array_length(chunks_sig)); for (guint i = 0; i < fu_chunk_array_length(chunks_sig); i++) { g_autoptr(FuChunk) chk = fu_chunk_array_index(chunks_sig, i, error); if (chk == NULL) return FALSE; if (!fu_synaptics_rmi_v5_device_write_block(self, RMI_F34_WRITE_SIGNATURE, address, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "failed to write bin block %u: ", fu_chunk_get_idx(chk)); return FALSE; } fu_progress_step_done(progress_child); } fu_device_sleep(device, 1000); /* ms */ } fu_progress_step_done(progress); if (!fu_synaptics_rmi_device_enter_iep_mode(self, FU_SYNAPTICS_RMI_DEVICE_FLAG_FORCE, error)) return FALSE; /* program the configuration image */ if (!fu_synaptics_rmi_device_write(self, f34->data_base, req_addr, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to 2nd write address zero: "); return FALSE; } for (guint i = 0; i < fu_chunk_array_length(chunks_cfg); i++) { g_autoptr(FuChunk) chk = fu_chunk_array_index(chunks_cfg, i, error); if (chk == NULL) return FALSE; if (!fu_synaptics_rmi_v5_device_write_block(self, RMI_F34_WRITE_CONFIG_BLOCK, address, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "failed to write cfg block %u: ", fu_chunk_get_idx(chk)); return FALSE; } fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)i + 1, (gsize)fu_chunk_array_length(chunks_cfg)); } fu_progress_step_done(progress); /* success */ return TRUE; } gboolean fu_synaptics_rmi_v5_device_setup(FuSynapticsRmiDevice *self, GError **error) { FuSynapticsRmiFunction *f34; FuSynapticsRmiFlash *flash = fu_synaptics_rmi_device_get_flash(self); guint8 flash_properties2 = 0; g_autoptr(GByteArray) f34_data0 = NULL; g_autoptr(GByteArray) f34_data2 = NULL; g_autoptr(GByteArray) buf_flash_properties2 = NULL; /* f34 */ f34 = fu_synaptics_rmi_device_get_function(self, 0x34, error); if (f34 == NULL) return FALSE; /* get bootloader ID */ f34_data0 = fu_synaptics_rmi_device_read(self, f34->query_base, 0x2, error); if (f34_data0 == NULL) { g_prefix_error(error, "failed to read bootloader ID: "); return FALSE; } flash->bootloader_id[0] = f34_data0->data[0]; flash->bootloader_id[1] = f34_data0->data[1]; /* get flash properties1 */ f34_data2 = fu_synaptics_rmi_device_read(self, f34->query_base + 0x2, 0x7, error); if (f34_data2 == NULL) return FALSE; if (f34_data2->data[0] & 0x80) { /* get flash properties2 */ buf_flash_properties2 = fu_synaptics_rmi_device_read(self, f34->query_base + 0x9, 1, error); if (buf_flash_properties2 == NULL) { g_prefix_error(error, "failed to read Flash Properties 2: "); return FALSE; } if (!fu_memread_uint8_safe(buf_flash_properties2->data, buf_flash_properties2->len, 0x0, /* offset */ &flash_properties2, error)) { g_prefix_error(error, "failed to parse Flash Properties 2: "); return FALSE; } if (flash_properties2 & 0x01) { guint16 sig_size = 0; g_autoptr(GByteArray) buf_rsa_key = NULL; buf_rsa_key = fu_synaptics_rmi_device_read(self, f34->query_base + 0x9 + 0x1, 2, error); if (buf_rsa_key == NULL) { g_prefix_error(error, "failed to read RSA key length: "); return FALSE; } if (!fu_memread_uint16_safe(buf_rsa_key->data, buf_rsa_key->len, 0x0, /* offset */ &sig_size, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to parse RSA key length: "); return FALSE; } fu_synaptics_rmi_device_set_sig_size(self, sig_size); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); } else { fu_synaptics_rmi_device_set_sig_size(self, 0); } } if (!fu_memread_uint16_safe(f34_data2->data, f34_data2->len, RMI_F34_BLOCK_SIZE_OFFSET, &flash->block_size, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memread_uint16_safe(f34_data2->data, f34_data2->len, RMI_F34_FW_BLOCKS_OFFSET, &flash->block_count_fw, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memread_uint16_safe(f34_data2->data, f34_data2->len, RMI_F34_CONFIG_BLOCKS_OFFSET, &flash->block_count_cfg, G_LITTLE_ENDIAN, error)) return FALSE; flash->status_addr = f34->data_base + RMI_F34_BLOCK_DATA_OFFSET + flash->block_size; return TRUE; } gboolean fu_synaptics_rmi_v5_device_query_status(FuSynapticsRmiDevice *self, GError **error) { FuSynapticsRmiFunction *f01; g_autoptr(GByteArray) f01_db = NULL; /* f01 */ f01 = fu_synaptics_rmi_device_get_function(self, 0x01, error); if (f01 == NULL) return FALSE; f01_db = fu_synaptics_rmi_device_read(self, f01->data_base, 0x1, error); if (f01_db == NULL) { g_prefix_error(error, "failed to read the f01 data base: "); return FALSE; } if (f01_db->data[0] & 0x40) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); } else { fu_device_remove_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); } return TRUE; } fwupd-2.0.10/plugins/synaptics-rmi/fu-synaptics-rmi-v5-device.h000066400000000000000000000011641501337203100243470ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-synaptics-rmi-device.h" gboolean fu_synaptics_rmi_v5_device_detach(FuDevice *device, FuProgress *progress, GError **error); gboolean fu_synaptics_rmi_v5_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error); gboolean fu_synaptics_rmi_v5_device_setup(FuSynapticsRmiDevice *self, GError **error); gboolean fu_synaptics_rmi_v5_device_query_status(FuSynapticsRmiDevice *self, GError **error); fwupd-2.0.10/plugins/synaptics-rmi/fu-synaptics-rmi-v6-device.c000066400000000000000000000040251501337203100243420ustar00rootroot00000000000000/* * Copyright 2012 Andrew Duggan * Copyright 2012 Synaptics Inc. * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-synaptics-rmi-v6-device.h" #define RMI_F34_CONFIG_BLOCKS_OFFSET 2 gboolean fu_synaptics_rmi_v6_device_setup(FuSynapticsRmiDevice *self, GError **error) { FuSynapticsRmiFlash *flash = fu_synaptics_rmi_device_get_flash(self); FuSynapticsRmiFunction *f34; g_autoptr(GByteArray) f34_data0 = NULL; g_autoptr(GByteArray) f34_data2 = NULL; g_autoptr(GByteArray) f34_data3 = NULL; /* f34 */ f34 = fu_synaptics_rmi_device_get_function(self, 0x34, error); if (f34 == NULL) return FALSE; /* get bootloader ID */ f34_data0 = fu_synaptics_rmi_device_read(self, f34->query_base, 0x2, error); if (f34_data0 == NULL) { g_prefix_error(error, "failed to read bootloader ID: "); return FALSE; } if (!fu_memread_uint8_safe(f34_data0->data, f34_data0->len, 0x0, &flash->bootloader_id[0], error)) return FALSE; if (!fu_memread_uint8_safe(f34_data0->data, f34_data0->len, 0x1, &flash->bootloader_id[1], error)) return FALSE; /* get flash properties */ f34_data2 = fu_synaptics_rmi_device_read(self, f34->query_base + 0x02, 2, error); if (f34_data2 == NULL) return FALSE; if (!fu_memread_uint16_safe(f34_data2->data, f34_data2->len, 0x0, &flash->block_size, G_LITTLE_ENDIAN, error)) return FALSE; f34_data3 = fu_synaptics_rmi_device_read(self, f34->query_base + 0x03, 8, error); if (f34_data3 == NULL) return FALSE; if (!fu_memread_uint16_safe(f34_data3->data, f34_data3->len, 0x0, &flash->block_count_fw, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memread_uint16_safe(f34_data3->data, f34_data3->len, RMI_F34_CONFIG_BLOCKS_OFFSET, &flash->block_count_cfg, G_LITTLE_ENDIAN, error)) return FALSE; flash->status_addr = f34->data_base + 2; return TRUE; } fwupd-2.0.10/plugins/synaptics-rmi/fu-synaptics-rmi-v6-device.h000066400000000000000000000003731501337203100243510ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-synaptics-rmi-device.h" gboolean fu_synaptics_rmi_v6_device_setup(FuSynapticsRmiDevice *self, GError **error); fwupd-2.0.10/plugins/synaptics-rmi/fu-synaptics-rmi-v7-device.c000066400000000000000000000770271501337203100243570ustar00rootroot00000000000000/* * Copyright 2012 Andrew Duggan * Copyright 2012 Synaptics Inc. * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-synaptics-rmi-struct.h" #include "fu-synaptics-rmi-v7-device.h" #define RMI_F34_ERASE_WAIT_MS 10000 /* ms */ typedef enum { RMI_FLASH_CMD_IDLE = 0x00, RMI_FLASH_CMD_ENTER_BL, RMI_FLASH_CMD_READ, RMI_FLASH_CMD_WRITE, RMI_FLASH_CMD_ERASE, RMI_FLASH_CMD_ERASE_AP, RMI_FLASH_CMD_SENSOR_ID, RMI_FLASH_CMD_SIGNATURE, } RmiFlashCommand; gboolean fu_synaptics_rmi_v7_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuSynapticsRmiDevice *self = FU_SYNAPTICS_RMI_DEVICE(device); g_autoptr(GByteArray) enable_req = g_byte_array_new(); FuSynapticsRmiFlash *flash = fu_synaptics_rmi_device_get_flash(self); FuSynapticsRmiFunction *f34; /* f34 */ f34 = fu_synaptics_rmi_device_get_function(self, 0x34, error); if (f34 == NULL) return FALSE; /* disable interrupts */ if (!fu_synaptics_rmi_device_disable_irqs(self, error)) return FALSE; /* enter BL */ fu_byte_array_append_uint8(enable_req, FU_RMI_PARTITION_ID_BOOTLOADER); fu_byte_array_append_uint32(enable_req, 0x0, G_LITTLE_ENDIAN); fu_byte_array_append_uint8(enable_req, RMI_FLASH_CMD_ENTER_BL); fu_byte_array_append_uint8(enable_req, flash->bootloader_id[0]); fu_byte_array_append_uint8(enable_req, flash->bootloader_id[1]); if (!fu_synaptics_rmi_device_write(self, f34->data_base + 1, enable_req, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to enable programming: "); return FALSE; } /* wait for idle */ if (!fu_synaptics_rmi_device_wait_for_idle(self, RMI_F34_ENABLE_WAIT_MS, RMI_DEVICE_WAIT_FOR_IDLE_FLAG_NONE, error)) return FALSE; if (!fu_synaptics_rmi_device_poll_wait(self, error)) return FALSE; fu_device_sleep(device, RMI_F34_ENABLE_WAIT_MS); return TRUE; } static gboolean fu_synaptics_rmi_v7_device_erase_partition(FuSynapticsRmiDevice *self, guint8 partition_id, GError **error) { FuSynapticsRmiFunction *f34; FuSynapticsRmiFlash *flash = fu_synaptics_rmi_device_get_flash(self); g_autoptr(GByteArray) erase_cmd = g_byte_array_new(); /* f34 */ f34 = fu_synaptics_rmi_device_get_function(self, 0x34, error); if (f34 == NULL) return FALSE; fu_byte_array_append_uint8(erase_cmd, partition_id); fu_byte_array_append_uint32(erase_cmd, 0x0, G_LITTLE_ENDIAN); fu_byte_array_append_uint8(erase_cmd, RMI_FLASH_CMD_ERASE); fu_byte_array_append_uint8(erase_cmd, flash->bootloader_id[0]); fu_byte_array_append_uint8(erase_cmd, flash->bootloader_id[1]); fu_device_sleep(FU_DEVICE(self), 1000); /* ms */ if (!fu_synaptics_rmi_device_write(self, f34->data_base + 1, erase_cmd, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to unlock erasing: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), 100); /* ms */ /* wait for ATTN */ if (!fu_synaptics_rmi_device_wait_for_idle(self, RMI_F34_ERASE_WAIT_MS, RMI_DEVICE_WAIT_FOR_IDLE_FLAG_NONE, error)) { g_prefix_error(error, "failed to wait for idle: "); return FALSE; } if (!fu_synaptics_rmi_device_poll_wait(self, error)) { g_prefix_error(error, "failed to get flash success: "); return FALSE; } return TRUE; } static gboolean fu_synaptics_rmi_v7_device_erase_all(FuSynapticsRmiDevice *self, GError **error) { FuSynapticsRmiFunction *f34; FuSynapticsRmiFlash *flash = fu_synaptics_rmi_device_get_flash(self); g_autoptr(GByteArray) erase_cmd = g_byte_array_new(); /* f34 */ f34 = fu_synaptics_rmi_device_get_function(self, 0x34, error); if (f34 == NULL) return FALSE; fu_byte_array_append_uint8(erase_cmd, FU_RMI_PARTITION_ID_CORE_CODE); fu_byte_array_append_uint32(erase_cmd, 0x0, G_LITTLE_ENDIAN); if (flash->bootloader_id[1] >= 8) { /* For bootloader v8 */ fu_byte_array_append_uint8(erase_cmd, RMI_FLASH_CMD_ERASE_AP); } else { /* For bootloader v7 */ fu_byte_array_append_uint8(erase_cmd, RMI_FLASH_CMD_ERASE); } fu_byte_array_append_uint8(erase_cmd, flash->bootloader_id[0]); fu_byte_array_append_uint8(erase_cmd, flash->bootloader_id[1]); /* for BL8 device, we need hold 1 seconds after querying F34 status to * avoid not get attention by following giving erase command */ if (flash->bootloader_id[1] >= 8) fu_device_sleep(FU_DEVICE(self), 1000); /* ms */ if (!fu_synaptics_rmi_device_write(self, f34->data_base + 1, erase_cmd, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to unlock erasing: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), 100); /* ms */ if (flash->bootloader_id[1] >= 8) { /* wait for ATTN */ if (!fu_synaptics_rmi_device_wait_for_idle(self, RMI_F34_ERASE_WAIT_MS, RMI_DEVICE_WAIT_FOR_IDLE_FLAG_NONE, error)) { g_prefix_error(error, "failed to wait for idle: "); return FALSE; } } if (!fu_synaptics_rmi_device_poll_wait(self, error)) { g_prefix_error(error, "failed to get flash success: "); return FALSE; } /* for BL7, we need erase config partition */ if (flash->bootloader_id[1] == 7) { g_autoptr(GByteArray) erase_config_cmd = g_byte_array_new(); fu_byte_array_append_uint8(erase_config_cmd, FU_RMI_PARTITION_ID_CORE_CONFIG); fu_byte_array_append_uint32(erase_config_cmd, 0x0, G_LITTLE_ENDIAN); fu_byte_array_append_uint8(erase_config_cmd, RMI_FLASH_CMD_ERASE); fu_device_sleep(FU_DEVICE(self), 100); /* ms */ if (!fu_synaptics_rmi_device_write(self, f34->data_base + 1, erase_config_cmd, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to erase core config: "); return FALSE; } /* wait for ATTN */ fu_device_sleep(FU_DEVICE(self), 100); /* ms */ if (!fu_synaptics_rmi_device_wait_for_idle( self, RMI_F34_ERASE_WAIT_MS, RMI_DEVICE_WAIT_FOR_IDLE_FLAG_REFRESH_F34, error)) { g_prefix_error(error, "failed to wait for idle: "); return FALSE; } if (!fu_synaptics_rmi_device_poll_wait(self, error)) { g_prefix_error(error, "failed to get flash success: "); return FALSE; } } return TRUE; } static gboolean fu_synaptics_rmi_v7_device_write_blocks(FuSynapticsRmiDevice *self, guint32 address, GBytes *fw, GError **error) { FuSynapticsRmiFlash *flash = fu_synaptics_rmi_device_get_flash(self); g_autoptr(FuChunkArray) chunks = NULL; /* write FW blocks */ chunks = fu_chunk_array_new_from_bytes(fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, flash->block_size); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GByteArray) req = g_byte_array_new(); /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; g_byte_array_append(req, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); if (!fu_synaptics_rmi_device_write(self, address, req, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write block @0x%x:%x: ", address, (guint)fu_chunk_get_address(chk)); return FALSE; } } /* wait for idle */ if (!fu_synaptics_rmi_device_wait_for_idle(self, RMI_F34_IDLE_WAIT_MS, RMI_DEVICE_WAIT_FOR_IDLE_FLAG_NONE, error)) { g_prefix_error(error, "failed to wait for idle @0x%x: ", address); return FALSE; } /* success */ return TRUE; } static gboolean fu_synaptics_rmi_v7_device_write_partition_signature(FuSynapticsRmiDevice *self, FuFirmware *firmware, const gchar *id, FuRmiPartitionId partition_id, GError **error) { FuSynapticsRmiFunction *f34; FuSynapticsRmiFlash *flash = fu_synaptics_rmi_device_get_flash(self); g_autoptr(GByteArray) req_offset = g_byte_array_new(); g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(GBytes) bytes = NULL; /* f34 */ f34 = fu_synaptics_rmi_device_get_function(self, 0x34, error); if (f34 == NULL) return FALSE; /*check if signature exists */ bytes = fu_firmware_get_image_by_id_bytes(firmware, g_strdup_printf("%s-signature", id), NULL); if (bytes == NULL) { return TRUE; } /* write partition signature */ g_info("writing partition signature %s…", fu_rmi_partition_id_to_string(partition_id)); fu_byte_array_append_uint16(req_offset, 0x0, G_LITTLE_ENDIAN); if (!fu_synaptics_rmi_device_write(self, f34->data_base + 0x2, req_offset, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write offset: "); return FALSE; } chunks = fu_chunk_array_new_from_bytes(bytes, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, (gsize)flash->payload_length * (gsize)flash->block_size); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GByteArray) req_cmd = g_byte_array_new(); g_autoptr(GByteArray) req_trans_sz = g_byte_array_new(); g_autoptr(GBytes) chk_blob = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; fu_byte_array_append_uint16(req_trans_sz, fu_chunk_get_data_sz(chk) / flash->block_size, G_LITTLE_ENDIAN); if (!fu_synaptics_rmi_device_write(self, f34->data_base + 0x3, req_trans_sz, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write transfer length: "); return FALSE; } fu_byte_array_append_uint8(req_cmd, RMI_FLASH_CMD_SIGNATURE); if (!fu_synaptics_rmi_device_write(self, f34->data_base + 0x4, req_cmd, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write signature command: "); return FALSE; } chk_blob = fu_chunk_get_bytes(chk); if (!fu_synaptics_rmi_v7_device_write_blocks(self, f34->data_base + 0x5, chk_blob, error)) return FALSE; } return TRUE; } static gboolean fu_synaptics_rmi_v7_device_write_partition(FuSynapticsRmiDevice *self, FuFirmware *firmware, const gchar *id, FuRmiPartitionId partition_id, GBytes *bytes, FuProgress *progress, GError **error) { FuSynapticsRmiFunction *f34; FuSynapticsRmiFlash *flash = fu_synaptics_rmi_device_get_flash(self); g_autoptr(GByteArray) req_offset = g_byte_array_new(); g_autoptr(GByteArray) req_partition_id = g_byte_array_new(); g_autoptr(FuChunkArray) chunks = NULL; /* f34 */ f34 = fu_synaptics_rmi_device_get_function(self, 0x34, error); if (f34 == NULL) return FALSE; /* write partition id */ g_info("writing partition %s…", fu_rmi_partition_id_to_string(partition_id)); fu_byte_array_append_uint8(req_partition_id, partition_id); if (!fu_synaptics_rmi_device_write(self, f34->data_base + 0x1, req_partition_id, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write flash partition: "); return FALSE; } fu_byte_array_append_uint16(req_offset, 0x0, G_LITTLE_ENDIAN); if (!fu_synaptics_rmi_device_write(self, f34->data_base + 0x2, req_offset, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write offset: "); return FALSE; } /* write partition */ chunks = fu_chunk_array_new_from_bytes(bytes, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, (gsize)flash->payload_length * (gsize)flash->block_size); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks) + 1); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GBytes) chk_blob = NULL; g_autoptr(GByteArray) req_trans_sz = g_byte_array_new(); g_autoptr(GByteArray) req_cmd = g_byte_array_new(); /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; fu_byte_array_append_uint16(req_trans_sz, fu_chunk_get_data_sz(chk) / flash->block_size, G_LITTLE_ENDIAN); if (!fu_synaptics_rmi_device_write(self, f34->data_base + 0x3, req_trans_sz, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write transfer length: "); return FALSE; } fu_byte_array_append_uint8(req_cmd, RMI_FLASH_CMD_WRITE); if (!fu_synaptics_rmi_device_write(self, f34->data_base + 0x4, req_cmd, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to flash command: "); return FALSE; } chk_blob = fu_chunk_get_bytes(chk); if (!fu_synaptics_rmi_v7_device_write_blocks(self, f34->data_base + 0x5, chk_blob, error)) return FALSE; fu_progress_step_done(progress); } if (!fu_synaptics_rmi_v7_device_write_partition_signature(self, firmware, id, partition_id, error)) return FALSE; fu_progress_step_done(progress); return TRUE; } static GBytes * fu_synaptics_rmi_v7_device_get_pubkey(FuSynapticsRmiDevice *self, GError **error) { FuSynapticsRmiFlash *flash = fu_synaptics_rmi_device_get_flash(self); FuSynapticsRmiFunction *f34; const gsize key_size = RMI_KEY_SIZE_2K; g_autoptr(GByteArray) req_addr_zero = g_byte_array_new(); g_autoptr(GByteArray) req_cmd = g_byte_array_new(); g_autoptr(GByteArray) req_partition_id = g_byte_array_new(); g_autoptr(GByteArray) req_transfer_length = g_byte_array_new(); g_autoptr(GByteArray) res = NULL; g_autoptr(GByteArray) pubkey = g_byte_array_new(); /* f34 */ f34 = fu_synaptics_rmi_device_get_function(self, 0x34, error); if (f34 == NULL) return NULL; /* set partition id for bootloader 7 */ fu_byte_array_append_uint8(req_partition_id, FU_RMI_PARTITION_ID_PUBKEY); if (!fu_synaptics_rmi_device_write(self, f34->data_base + 0x1, req_partition_id, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write flash partition id: "); return NULL; } fu_byte_array_append_uint16(req_addr_zero, 0x0, G_LITTLE_ENDIAN); if (!fu_synaptics_rmi_device_write(self, f34->data_base + 0x2, req_addr_zero, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write flash config address: "); return NULL; } /* set transfer length */ fu_byte_array_append_uint16(req_transfer_length, key_size / flash->block_size, G_LITTLE_ENDIAN); if (!fu_synaptics_rmi_device_write(self, f34->data_base + 0x3, req_transfer_length, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to set transfer length: "); return NULL; } /* set command to read */ fu_byte_array_append_uint8(req_cmd, RMI_FLASH_CMD_READ); if (!fu_synaptics_rmi_device_write(self, f34->data_base + 0x4, req_cmd, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write command to read: "); return NULL; } if (!fu_synaptics_rmi_device_poll_wait(self, error)) { g_prefix_error(error, "failed to wait: "); return NULL; } /* read back entire buffer in blocks */ res = fu_synaptics_rmi_device_read(self, f34->data_base + 0x5, (guint32)key_size, error); if (res == NULL) { g_prefix_error(error, "failed to read: "); return NULL; } for (guint i = 0; i < res->len; i++) fu_byte_array_append_uint8(pubkey, res->data[res->len - i - 1]); /* success */ return g_bytes_new(pubkey->data, pubkey->len); } static gboolean fu_synaptics_rmi_v7_device_secure_check(FuSynapticsRmiDevice *self, FuFirmware *firmware, GError **error) { FuSynapticsRmiFlash *flash = fu_synaptics_rmi_device_get_flash(self); g_autoptr(GBytes) pubkey = NULL; g_autoptr(GPtrArray) imgs = NULL; if (flash->bootloader_id[1] >= 10 || flash->has_pubkey == FALSE) return TRUE; pubkey = fu_synaptics_rmi_v7_device_get_pubkey(self, error); if (pubkey == NULL) { g_prefix_error(error, "get pubkey failed: "); return FALSE; } imgs = fu_firmware_get_images(firmware); for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); const gchar *id = fu_firmware_get_id(img); g_autoptr(GBytes) byte_payload = NULL; g_autoptr(GBytes) byte_signature = NULL; g_autofree gchar *id_signature = NULL; if (g_str_has_suffix(id, "-signature")) continue; id_signature = g_strdup_printf("%s-signature", id); byte_signature = fu_firmware_get_image_by_id_bytes(firmware, id_signature, NULL); if (byte_signature == NULL) continue; byte_payload = fu_firmware_get_bytes(img, error); if (byte_payload == NULL) return FALSE; if (!fu_synaptics_rmi_verify_sha256_signature(byte_payload, pubkey, byte_signature, error)) { g_prefix_error(error, "%s secure check failed: ", id); return FALSE; } g_info("%s signature verified successfully", id); } return TRUE; } gboolean fu_synaptics_rmi_v7_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuSynapticsRmiDevice *self = FU_SYNAPTICS_RMI_DEVICE(device); FuSynapticsRmiFlash *flash = fu_synaptics_rmi_device_get_flash(self); FuSynapticsRmiFunction *f34; g_autoptr(GBytes) bytes_bin = NULL; g_autoptr(GBytes) bytes_cfg = NULL; g_autoptr(GBytes) bytes_flashcfg = NULL; g_autoptr(GBytes) bytes_fld = NULL; g_autoptr(GBytes) bytes_afe = NULL; g_autoptr(GBytes) bytes_displayconfig = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); if (flash->bootloader_id[1] > 8) { fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "disable-sleep"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 0, "verify-signature"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "fixed-location-data"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 8, "flash-config"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 9, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 81, "core-code"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "core-config"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 0, "external-touch-afe-config"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 0, "display-config"); } else if (flash->bootloader_id[1] == 8) { fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "disable-sleep"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 0, "verify-signature"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 0, "fixed-location-data"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 16, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 0, "flash-config"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 81, "core-code"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "core-config"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 0, "external-touch-afe-config"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 0, "display-config"); } else { fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "disable-sleep"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_READ, 2, "verify-signature"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 2, "fixed-location-data"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 3, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 89, "core-code"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 2, "core-config"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 2, "external-touch-afe-config"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 2, "display-config"); } /* we should be in bootloader mode now, but check anyway */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not bootloader, perhaps need detach?!"); return FALSE; } /* f34 */ f34 = fu_synaptics_rmi_device_get_function(self, 0x34, error); if (f34 == NULL) return FALSE; /* get both images */ bytes_bin = fu_firmware_get_image_by_id_bytes(firmware, "ui", error); if (bytes_bin == NULL) return FALSE; bytes_cfg = fu_firmware_get_image_by_id_bytes(firmware, "config", error); if (bytes_cfg == NULL) return FALSE; if (flash->bootloader_id[1] >= 8) { bytes_flashcfg = fu_firmware_get_image_by_id_bytes(firmware, "flash-config", error); if (bytes_flashcfg == NULL) return FALSE; } bytes_fld = fu_firmware_get_image_by_id_bytes(firmware, "fixed-location-data", NULL); bytes_afe = fu_firmware_get_image_by_id_bytes(firmware, "afe-config", NULL); bytes_displayconfig = fu_firmware_get_image_by_id_bytes(firmware, "display-config", NULL); /* disable powersaving */ if (!fu_synaptics_rmi_device_disable_sleep(self, error)) return FALSE; fu_progress_step_done(progress); /* verify signature */ if (!fu_synaptics_rmi_v7_device_secure_check(self, firmware, error)) return FALSE; fu_progress_step_done(progress); /* write fld before erase if exists */ if (bytes_fld != NULL) { if (!fu_synaptics_rmi_v7_device_write_partition( self, firmware, "fixed-location-data", FU_RMI_PARTITION_ID_FIXED_LOCATION_DATA, bytes_fld, fu_progress_get_child(progress), error)) return FALSE; } fu_progress_step_done(progress); /* write flash config for BL > v8 */ if (flash->bootloader_id[1] > 8) { if (!fu_synaptics_rmi_v7_device_erase_partition(self, FU_RMI_PARTITION_ID_FLASH_CONFIG, error)) return FALSE; if (!fu_synaptics_rmi_v7_device_write_partition(self, firmware, "flash-config", FU_RMI_PARTITION_ID_FLASH_CONFIG, bytes_flashcfg, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); } /* erase all */ if (!fu_synaptics_rmi_v7_device_erase_all(self, error)) { g_prefix_error(error, "failed to erase all: "); return FALSE; } fu_progress_step_done(progress); /* write flash config for v8 */ if (flash->bootloader_id[1] == 8) { if (!fu_synaptics_rmi_v7_device_write_partition(self, firmware, "flash-config", FU_RMI_PARTITION_ID_FLASH_CONFIG, bytes_flashcfg, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); } /* write core code */ if (!fu_synaptics_rmi_v7_device_write_partition(self, firmware, "ui", FU_RMI_PARTITION_ID_CORE_CODE, bytes_bin, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* write core config */ if (!fu_synaptics_rmi_v7_device_write_partition(self, firmware, "config", FU_RMI_PARTITION_ID_CORE_CONFIG, bytes_cfg, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* write afe-config if exists */ if (bytes_afe != NULL) { if (!fu_synaptics_rmi_v7_device_write_partition( self, firmware, "afe-config", FU_RMI_PARTITION_ID_EXTERNAL_TOUCH_AFE_CONFIG, bytes_afe, fu_progress_get_child(progress), error)) return FALSE; } fu_progress_step_done(progress); /* write display config if exists */ if (bytes_displayconfig != NULL) { if (!fu_synaptics_rmi_v7_device_write_partition(self, firmware, "display-config", FU_RMI_PARTITION_ID_DISPLAY_CONFIG, bytes_displayconfig, fu_progress_get_child(progress), error)) return FALSE; } fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_synaptics_rmi_v7_device_read_flash_config(FuSynapticsRmiDevice *self, GError **error) { FuSynapticsRmiFlash *flash = fu_synaptics_rmi_device_get_flash(self); FuSynapticsRmiFunction *f34; g_autoptr(GByteArray) req_addr_zero = g_byte_array_new(); g_autoptr(GByteArray) req_cmd = g_byte_array_new(); g_autoptr(GByteArray) req_partition_id = g_byte_array_new(); g_autoptr(GByteArray) req_transfer_length = g_byte_array_new(); g_autoptr(GByteArray) res = NULL; gsize partition_size = FU_STRUCT_RMI_PARTITION_TBL_SIZE; /* f34 */ f34 = fu_synaptics_rmi_device_get_function(self, 0x34, error); if (f34 == NULL) return FALSE; /* set partition id for bootloader 7 */ fu_byte_array_append_uint8(req_partition_id, FU_RMI_PARTITION_ID_FLASH_CONFIG); if (!fu_synaptics_rmi_device_write(self, f34->data_base + 0x1, req_partition_id, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write flash partition id: "); return FALSE; } fu_byte_array_append_uint16(req_addr_zero, 0x0, G_LITTLE_ENDIAN); if (!fu_synaptics_rmi_device_write(self, f34->data_base + 0x2, req_addr_zero, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write flash config address: "); return FALSE; } /* set transfer length */ fu_byte_array_append_uint16(req_transfer_length, flash->config_length, G_LITTLE_ENDIAN); if (!fu_synaptics_rmi_device_write(self, f34->data_base + 0x3, req_transfer_length, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to set transfer length: "); return FALSE; } /* set command to read */ fu_byte_array_append_uint8(req_cmd, RMI_FLASH_CMD_READ); if (!fu_synaptics_rmi_device_write(self, f34->data_base + 0x4, req_cmd, FU_SYNAPTICS_RMI_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to write command to read: "); return FALSE; } if (!fu_synaptics_rmi_device_poll_wait(self, error)) { g_prefix_error(error, "failed to wait: "); return FALSE; } /* read back entire buffer in blocks */ res = fu_synaptics_rmi_device_read(self, f34->data_base + 0x5, (guint32)flash->block_size * (guint32)flash->config_length, error); if (res == NULL) { g_prefix_error(error, "failed to read: "); return FALSE; } /* debugging */ fu_dump_full(G_LOG_DOMAIN, "FlashConfig", res->data, res->len, 80, FU_DUMP_FLAGS_NONE); if ((res->data[0] & 0x0f) == 1) partition_size += 0x2; /* parse the config length */ for (guint i = 0x2; i < res->len; i += partition_size) { guint16 partition_id; g_autoptr(GByteArray) st_prt = NULL; st_prt = fu_struct_rmi_partition_tbl_parse(res->data, res->len, i, error); if (st_prt == NULL) return FALSE; partition_id = fu_struct_rmi_partition_tbl_get_partition_id(st_prt); g_debug("found partition %s (0x%02x)", fu_rmi_partition_id_to_string(partition_id), partition_id); if (partition_id == FU_RMI_PARTITION_ID_CORE_CONFIG) { flash->block_count_cfg = fu_struct_rmi_partition_tbl_get_partition_len(st_prt); continue; } if (partition_id == FU_RMI_PARTITION_ID_CORE_CODE) { flash->block_count_fw = fu_struct_rmi_partition_tbl_get_partition_len(st_prt); continue; } if (partition_id == FU_RMI_PARTITION_ID_PUBKEY) { flash->has_pubkey = TRUE; continue; } if (partition_id == FU_RMI_PARTITION_ID_NONE) break; } /* success */ return TRUE; } gboolean fu_synaptics_rmi_v7_device_setup(FuSynapticsRmiDevice *self, GError **error) { FuSynapticsRmiFlash *flash = fu_synaptics_rmi_device_get_flash(self); FuSynapticsRmiFunction *f34; guint8 offset; g_autoptr(GByteArray) f34_data0 = NULL; g_autoptr(GByteArray) f34_dataX = NULL; /* f34 */ f34 = fu_synaptics_rmi_device_get_function(self, 0x34, error); if (f34 == NULL) return FALSE; f34_data0 = fu_synaptics_rmi_device_read(self, f34->query_base, 1, error); if (f34_data0 == NULL) { g_prefix_error(error, "failed to read bootloader ID: "); return FALSE; } offset = (f34_data0->data[0] & 0b00000111) + 1; f34_dataX = fu_synaptics_rmi_device_read(self, f34->query_base + offset, 21, error); if (f34_dataX == NULL) return FALSE; if (!fu_memread_uint8_safe(f34_dataX->data, f34_dataX->len, 0x0, &flash->bootloader_id[0], error)) return FALSE; if (!fu_memread_uint8_safe(f34_dataX->data, f34_dataX->len, 0x1, &flash->bootloader_id[1], error)) return FALSE; if (!fu_memread_uint16_safe(f34_dataX->data, f34_dataX->len, 0x07, &flash->block_size, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memread_uint16_safe(f34_dataX->data, f34_dataX->len, 0x0d, &flash->config_length, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memread_uint16_safe(f34_dataX->data, f34_dataX->len, 0x0f, &flash->payload_length, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memread_uint32_safe(f34_dataX->data, f34_dataX->len, 0x02, &flash->build_id, G_LITTLE_ENDIAN, error)) return FALSE; /* sanity check */ if ((guint32)flash->block_size * (guint32)flash->config_length > G_MAXUINT16) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "block size 0x%x or config length 0x%x invalid", flash->block_size, flash->config_length); return FALSE; } /* read flash config */ return fu_synaptics_rmi_v7_device_read_flash_config(self, error); } gboolean fu_synaptics_rmi_v7_device_query_status(FuSynapticsRmiDevice *self, GError **error) { FuSynapticsRmiFunction *f34; guint8 status; g_autoptr(GByteArray) f34_data = NULL; /* f34 */ f34 = fu_synaptics_rmi_device_get_function(self, 0x34, error); if (f34 == NULL) return FALSE; f34_data = fu_synaptics_rmi_device_read(self, f34->data_base, 0x1, error); if (f34_data == NULL) { g_prefix_error(error, "failed to read the f01 data base: "); return FALSE; } status = f34_data->data[0]; if (status & 0x80) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); } else { fu_device_remove_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); } if (status == 0x01) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "operation only supported in bootloader mode"); return FALSE; } if (status == 0x02) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "partition ID is not supported by the bootloader"); return FALSE; } if (status == 0x03) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "partition supported, but command not supported"); return FALSE; } if (status == 0x04) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid block offset"); return FALSE; } if (status == 0x05) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid transfer"); return FALSE; } if (status == 0x06) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "partition has not been erased"); return FALSE; } if (status == 0x07) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_SIGNATURE_INVALID, "flash programming key incorrect"); return FALSE; } if (status == 0x08) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "bad partition table"); return FALSE; } if (status == 0x09) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "transfer checksum failed"); return FALSE; } if (status == 0x1f) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "flash hardware failure"); return FALSE; } return TRUE; } fwupd-2.0.10/plugins/synaptics-rmi/fu-synaptics-rmi-v7-device.h000066400000000000000000000011641501337203100243510ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-synaptics-rmi-device.h" gboolean fu_synaptics_rmi_v7_device_detach(FuDevice *device, FuProgress *progress, GError **error); gboolean fu_synaptics_rmi_v7_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error); gboolean fu_synaptics_rmi_v7_device_setup(FuSynapticsRmiDevice *self, GError **error); gboolean fu_synaptics_rmi_v7_device_query_status(FuSynapticsRmiDevice *self, GError **error); fwupd-2.0.10/plugins/synaptics-rmi/fu-synaptics-rmi.rs000066400000000000000000000072751501337203100227700ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] #[repr(u16le)] enum FuRmiPartitionId { None = 0x00, Bootloader = 0x01, DeviceConfig, FlashConfig, ManufacturingBlock, GuestSerialization, GlobalParameters, CoreCode, CoreConfig, GuestCode, DisplayConfig, ExternalTouchAfeConfig, UtilityParameter, Pubkey, FixedLocationData = 0x0E, } #[derive(Parse)] #[repr(C, packed)] struct FuStructRmiPartitionTbl { partition_id: FuRmiPartitionId, partition_len: u16le, partition_addr: u16le, partition_prop: u16le, } #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructRmiImg { checksum: u32le, _reserved1: [u8; 2], io_offset: u8, bootloader_version: u8, image_size: u32le, config_size: u32le, product_id: [char; 10], package_id: u32le, product_info: u32le, _reserved3: [u8; 46], fw_build_id: u32le, signature_size: u32le, } #[derive(ToString)] #[repr(u16le)] enum FuRmiContainerId { TopLevel, Ui, UiConfig, Bl, BlImage, BlConfig, BlLockdownInfo, PermanentConfig, GuestCode, BlProtocolDescriptor, UiProtocolDescriptor, RmiSelfDiscovery, RmiPageContent, GeneralInformation, DeviceConfig, FlashConfig, GuestSerialization, GlobalParameters, CoreCode, CoreConfig, DisplayConfig, ExternalTouchAfeConfig, Utility, UtilityParameter, FixedLocationData = 27, } #[derive(New, ParseStream)] #[repr(C, packed)] struct FuStructRmiContainerDescriptor { content_checksum: u32le, container_id: FuRmiContainerId, minor_version: u8, major_version: u8, signature_size: u32le, container_option_flags: u32le, content_options_length: u32le, content_options_address: u32le, content_length: u32le, content_address: u32le, } // PS2 Data Port Command enum FuRmiEdpCommand { AuxFullRmiBackDoor = 0x7F, AuxAccessModeByte1 = 0xE0, AuxAccessModeByte2 = 0xE1, AuxIbmReadSecondaryId = 0xE1, AuxSetScaling1To1 = 0xE6, AuxSetScaling2To1 = 0xE7, AuxSetResolution = 0xE8, AuxStatusRequest = 0xE9, AuxSetStreamMode = 0xEA, AuxReadData = 0xEB, AuxResetWrapMode = 0xEC, AuxSetWrapMode = 0xEE, AuxSetRemoteMode = 0xF0, AuxReadDeviceType = 0xF2, AuxSetSampleRate = 0xF3, AuxEnable = 0xF4, AuxDisable = 0xF5, AuxSetDefault = 0xF6, AuxResend = 0xFE, AuxReset = 0xFF, } enum FuRmiDeviceResponse { TouchPad = 0x47, Styk = 0x46, ControlBar = 0x44, RgbControlBar = 0x43, } enum FuRmiStatusRequest { IdentifySynaptics = 0x00, ReadTouchPadModes = 0x01, ReadModeByte = 0x01, ReadEdgeMargins = 0x02, ReadCapabilities = 0x02, ReadModelID = 0x03, ReadCompilationDate = 0x04, ReadSerialNumberPrefix = 0x06, ReadSerialNumberSuffix = 0x07, ReadResolutions = 0x08, ReadExtraCapabilities1 = 0x09, ReadExtraCapabilities2 = 0x0A, ReadExtraCapabilities3 = 0x0B, ReadExtraCapabilities4 = 0x0C, ReadExtraCapabilities5 = 0x0D, ReadCoordinates = 0x0D, ReadExtraCapabilities6 = 0x0E, ReadExtraCapabilities7 = 0x0F, } enum FuRmiDataPortStatus { Acknowledge = 0xFA, Error = 0xFC, Resend = 0xFE, TimeOut = 0x100, } enum FuRmiSetSampleRate { SetModeByte1 = 0x0A, SetModeByte2 = 0x14, SetModeByte3 = 0x28, SetModeByte4 = 0x3C, SetDeluxeModeByte1 = 0x0A, SetDeluxeModeByte2 = 0x3C, SetDeluxeModeByte3 = 0xC8, FastRecalibrate = 0x50, PassThroughCommandTunnel = 0x28, } enum FuRmiStickDeviceType { None = 0, Ibm, JytSyna = 5, Synaptics = 6, Unknown = 0xFFFFFFFF, } fwupd-2.0.10/plugins/synaptics-rmi/meson.build000066400000000000000000000031771501337203100213470ustar00rootroot00000000000000if gnutls.found() and host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginSynapticsRmi"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('synaptics-rmi.quirk') plugin_builtin_synaptics_rmi = static_library('fu_plugin_synaptics_rmi', rustgen.process( 'fu-synaptics-rmi.rs', # fuzzing ), sources: [ 'fu-synaptics-rmi-plugin.c', 'fu-synaptics-rmi-common.c', # fuzzing 'fu-synaptics-rmi-device.c', 'fu-synaptics-rmi-hid-device.c', 'fu-synaptics-rmi-ps2-device.c', 'fu-synaptics-rmi-v5-device.c', 'fu-synaptics-rmi-v6-device.c', 'fu-synaptics-rmi-v7-device.c', 'fu-synaptics-rmi-firmware.c', # fuzzing ], include_directories: plugin_incdirs, c_args: cargs, dependencies: plugin_deps, link_with: plugin_libs, ) plugin_builtins += plugin_builtin_synaptics_rmi if get_option('tests') install_data(['tests/synaptics-rmi-0x.builder.xml','tests/synaptics-rmi-10.builder.xml'], install_dir: join_paths(installed_test_datadir, 'tests')) env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) e = executable( 'synaptics-rmi-self-test', sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: plugin_deps, link_with: [ plugin_libs, plugin_builtin_synaptics_rmi, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('synaptics-rmi-self-test', e, env: env) endif endif fwupd-2.0.10/plugins/synaptics-rmi/synaptics-rmi.quirk000066400000000000000000000005661501337203100230630ustar00rootroot00000000000000# Dell K12A kbd-dock [HIDRAW\VEN_06CB&DEV_2819] Plugin = synaptics_rmi GType = FuSynapticsRmiHidDevice Vendor = Synaptics Flags = only-supported # LENOVO X1 Nano [SERIO\FWID_LEN0305-PNP0F13] Plugin = synaptics_rmi GType = FuSynapticsRmiPs2Device # LENOVO X1 Panther [SERIO\FWID_LEN0306-PNP0F13] Plugin = synaptics_rmi GType = FuSynapticsRmiPs2Device InstallDuration = 236 fwupd-2.0.10/plugins/synaptics-rmi/tests/000077500000000000000000000000001501337203100203375ustar00rootroot00000000000000fwupd-2.0.10/plugins/synaptics-rmi/tests/synaptics-rmi-0x.builder.xml000066400000000000000000000003361501337203100256370ustar00rootroot00000000000000 0x01 Example ui ZGF2ZQ== fwupd-2.0.10/plugins/synaptics-rmi/tests/synaptics-rmi-10.builder.xml000066400000000000000000000003361501337203100255300ustar00rootroot00000000000000 0x10 Example ui ZGF2ZQ== fwupd-2.0.10/plugins/synaptics-vmm9/000077500000000000000000000000001501337203100172765ustar00rootroot00000000000000fwupd-2.0.10/plugins/synaptics-vmm9/README.md000066400000000000000000000030621501337203100205560ustar00rootroot00000000000000--- title: Plugin: Synaptics VMM9xxxx --- ## Introduction This plugin updates the VMM9xxxx MST devices from Synaptics. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a packed binary file format. This plugin supports the following protocol ID: * `com.synaptics.mst-hid` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_06CB&PID_9430` These devices also use custom GUID values using the board ID and customer ID values which are different for each customer and hardware design, e.g. * `USB\VID_06CB&PID_9430&BID_56` * `USB\VID_06CB&PID_9430&BID_56&CID_78` ## Update Behavior The device storage banks are erased, and each block from the firmware is written and then finally activated. Calculating the CRC manually is no longer required. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x06CB` and also from the customer ID (if set), e.g. `SYNA:0x12` ## Quirk Use This plugin uses the following plugin-specific quirks: ### Flags=manual-restart-required The device is not capable of self-rebooting, and we have to ask the user to replug the power cable. Since: 1.9.20 ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.9.20`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Apollo Ling: @ApolloLing fwupd-2.0.10/plugins/synaptics-vmm9/fu-synaptics-vmm9-device.c000066400000000000000000000540341501337203100242200ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-synaptics-vmm9-device.h" #include "fu-synaptics-vmm9-firmware.h" #include "fu-synaptics-vmm9-struct.h" #define FU_SYNAPTICS_VMM9_DEVICE_FLAG_MANUAL_RESTART_REQUIRED "manual-restart-required" struct _FuSynapticsVmm9Device { FuHidDevice parent_instance; guint8 board_id; guint8 customer_id; guint8 active_bank; }; G_DEFINE_TYPE(FuSynapticsVmm9Device, fu_synaptics_vmm9_device, FU_TYPE_HID_DEVICE) #define FU_SYNAPTICS_VMM9_DEVICE_REPORT_SIZE 62 #define FU_SYNAPTICS_VMM9_DEVICE_TIMEOUT 5000 /* ms */ #define FU_SYNAPTICS_VMM9_CTRL_BUSY_MASK 0x80 #define FU_SYNAPTICS_VMM9_BUSY_POLL 10 /* ms */ #define FU_SYNAPTICS_VMM9_MEM_OFFSET_CHIP_SERIAL 0x20200D3C /* 0x4 bytes, %02x */ #define FU_SYNAPTICS_VMM9_MEM_OFFSET_RC_TRIGGER 0x2020A024 /* write 0xF5000000 to reset */ #define FU_SYNAPTICS_VMM9_MEM_OFFSET_MCU_BOOTLOADER_STS 0x2020A030 /* bootloader status */ #define FU_SYNAPTICS_VMM9_MEM_OFFSET_MCU_FW_VERSION 0x2020A038 /* 0x4 bytes, maj.min.mic.? */ #define FU_SYNAPTICS_VMM9_MEM_OFFSET_FIRMWARE_BUILD 0x2020A084 /* 0x4 bytes, be */ #define FU_SYNAPTICS_VMM9_MEM_OFFSET_RC_COMMAND 0x2020B000 #define FU_SYNAPTICS_VMM9_MEM_OFFSET_RC_OFFSET 0x2020B004 #define FU_SYNAPTICS_VMM9_MEM_OFFSET_RC_LENGTH 0x2020B008 #define FU_SYNAPTICS_VMM9_MEM_OFFSET_RC_DATA 0x2020B010 /* until 0x2020B02C */ #define FU_SYNAPTICS_VMM9_MEM_OFFSET_FIRMWARE_NAME 0x90000230 /* 0xF bytes, ASCII */ #define FU_SYNAPTICS_VMM9_MEM_OFFSET_BOARD_ID 0x9000014E /* 0x2 bytes, customer.hardware */ static void fu_synaptics_vmm9_device_to_string(FuDevice *device, guint idt, GString *str) { FuSynapticsVmm9Device *self = FU_SYNAPTICS_VMM9_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "BoardId", self->board_id); fwupd_codec_string_append_hex(str, idt, "CustomerId", self->customer_id); fwupd_codec_string_append_hex(str, idt, "ActiveBank", self->active_bank); } typedef enum { FU_SYNAPTICS_VMM9_COMMAND_FLAG_NONE = 0, FU_SYNAPTICS_VMM9_COMMAND_FLAG_FULL_BUFFER = 1 << 0, FU_SYNAPTICS_VMM9_COMMAND_FLAG_NO_REPLY = 1 << 1, FU_SYNAPTICS_VMM9_COMMAND_FLAG_IGNORE_REPLY = 1 << 2, } FuSynapticsVmm9DeviceCommandFlags; typedef struct { guint8 *buf; gsize bufsz; } FuSynapticsVmm9DeviceCommandHelper; static gboolean fu_synaptics_vmm9_device_command_cb(FuDevice *self, gpointer user_data, GError **error) { FuSynapticsVmm9DeviceCommandHelper *helper = (FuSynapticsVmm9DeviceCommandHelper *)user_data; guint8 buf[FU_SYNAPTICS_VMM9_DEVICE_REPORT_SIZE] = {0}; g_autoptr(FuStructHidGetCommand) st = NULL; g_autoptr(FuStructHidPayload) st_payload = NULL; /* get, and parse */ if (!fu_hid_device_get_report(FU_HID_DEVICE(self), FU_STRUCT_HID_GET_COMMAND_DEFAULT_ID, buf, sizeof(buf), FU_SYNAPTICS_VMM9_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to send packet: "); return FALSE; } st = fu_struct_hid_get_command_parse(buf, sizeof(buf), 0x0, error); if (st == NULL) return FALSE; /* sanity check */ st_payload = fu_struct_hid_get_command_get_payload(st); if (fu_struct_hid_payload_get_sts(st_payload) != FU_SYNAPTICS_VMM9_RC_STS_SUCCESS) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "sts is %s [0x%x]", fu_synaptics_vmm9_rc_sts_to_string(fu_struct_hid_payload_get_sts(st_payload)), fu_struct_hid_payload_get_sts(st_payload)); return FALSE; } /* check the busy status */ if (fu_struct_hid_payload_get_ctrl(st_payload) & FU_SYNAPTICS_VMM9_CTRL_BUSY_MASK) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "is busy"); return FALSE; } /* payload is optional */ if (helper->buf != NULL) { gsize fifosz = 0; const guint8 *fifo = fu_struct_hid_payload_get_fifo(st_payload, &fifosz); if (!fu_memcpy_safe(helper->buf, helper->bufsz, 0x0, /* dst */ fifo, fifosz, 0x0, /*src */ helper->bufsz, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_synaptics_vmm9_device_command(FuSynapticsVmm9Device *self, FuSynapticsVmm9RcCtrl ctrl, guint32 offset, const guint8 *src_buf, gsize src_bufsz, guint8 *dst_buf, gsize dst_bufsz, FuSynapticsVmm9DeviceCommandFlags flags, GError **error) { FuSynapticsVmm9DeviceCommandHelper helper = {.buf = dst_buf, .bufsz = dst_bufsz}; guint8 checksum; g_autofree gchar *str = NULL; g_autoptr(FuStructHidPayload) st_payload = fu_struct_hid_payload_new(); g_autoptr(FuStructHidSetCommand) st = fu_struct_hid_set_command_new(); g_autoptr(GError) error_local = NULL; /* payload */ fu_struct_hid_payload_set_ctrl(st_payload, ctrl | FU_SYNAPTICS_VMM9_CTRL_BUSY_MASK); fu_struct_hid_payload_set_offset(st_payload, offset); fu_struct_hid_payload_set_length(st_payload, src_bufsz); if (src_buf != NULL) { if (!fu_struct_hid_payload_set_fifo(st_payload, src_buf, src_bufsz, error)) return FALSE; } /* request */ fu_struct_hid_set_command_set_size(st, FU_STRUCT_HID_PAYLOAD_OFFSET_FIFO + src_bufsz); if (!fu_struct_hid_set_command_set_payload(st, st_payload, error)) return FALSE; checksum = 0x100 - fu_sum8(st->data + 1, st->len - 1); if (flags & FU_SYNAPTICS_VMM9_COMMAND_FLAG_FULL_BUFFER) { fu_struct_hid_set_command_set_checksum(st, checksum); } else { goffset offset_checksum = FU_STRUCT_HID_SET_COMMAND_OFFSET_PAYLOAD + FU_STRUCT_HID_PAYLOAD_OFFSET_FIFO + src_bufsz; if (!fu_memwrite_uint8_safe(st->data, st->len, offset_checksum, checksum, error)) return FALSE; } fu_byte_array_set_size(st, FU_SYNAPTICS_VMM9_DEVICE_REPORT_SIZE, 0x0); /* set */ str = fu_struct_hid_set_command_to_string(st); g_debug("%s", str); if (!fu_hid_device_set_report(FU_HID_DEVICE(self), FU_STRUCT_HID_SET_COMMAND_DEFAULT_ID, st->data, st->len, FU_SYNAPTICS_VMM9_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to send packet: "); return FALSE; } /* disregard */ if (flags & FU_SYNAPTICS_VMM9_COMMAND_FLAG_NO_REPLY) return TRUE; /* need time to complete, no need to poll frequently */ if (ctrl == FU_SYNAPTICS_VMM9_RC_CTRL_ERASE_FLASH) fu_device_sleep(FU_DEVICE(self), 100); /* poll for success */ if (!fu_device_retry_full(FU_DEVICE(self), fu_synaptics_vmm9_device_command_cb, FU_SYNAPTICS_VMM9_DEVICE_TIMEOUT / FU_SYNAPTICS_VMM9_BUSY_POLL, FU_SYNAPTICS_VMM9_BUSY_POLL, /* ms */ &helper, &error_local)) { if (flags & FU_SYNAPTICS_VMM9_COMMAND_FLAG_IGNORE_REPLY) { g_debug("ignoring: %s", error_local->message); return TRUE; } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to poll for success: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_synaptics_vmm9_device_setup(FuDevice *device, GError **error) { FuSynapticsVmm9Device *self = FU_SYNAPTICS_VMM9_DEVICE(device); guint32 mcu_status; guint8 buf[4] = {0x0}; g_autofree gchar *serial = NULL; g_autofree gchar *bootloader_version = NULL; g_autoptr(FuStructSynapticsUpdGetId) st_getid = NULL; /* read chip serial number */ if (!fu_synaptics_vmm9_device_command(self, FU_SYNAPTICS_VMM9_RC_CTRL_MEMORY_READ, FU_SYNAPTICS_VMM9_MEM_OFFSET_CHIP_SERIAL, NULL, sizeof(buf), buf, sizeof(buf), FU_SYNAPTICS_VMM9_COMMAND_FLAG_FULL_BUFFER, error)) return FALSE; serial = g_strdup_printf("%02x%02x%02x%02x", buf[0], buf[1], buf[2], buf[3]); fu_device_set_serial(device, serial); /* read board and customer IDs */ if (!fu_synaptics_vmm9_device_command(self, FU_SYNAPTICS_VMM9_RC_CTRL_GET_ID, 0x0, NULL, sizeof(buf), buf, sizeof(buf), FU_SYNAPTICS_VMM9_COMMAND_FLAG_FULL_BUFFER, error)) return FALSE; st_getid = fu_struct_synaptics_upd_get_id_parse(buf, sizeof(buf), 0x0, error); if (st_getid == NULL) return FALSE; self->board_id = fu_struct_synaptics_upd_get_id_get_bid(st_getid); fu_device_add_instance_u8(device, "BID", self->board_id); self->customer_id = fu_struct_synaptics_upd_get_id_get_cid(st_getid); fu_device_add_instance_u8(device, "CID", self->customer_id); fu_device_build_instance_id(device, NULL, "USB", "VID", "PID", "BID", NULL); fu_device_build_instance_id(device, NULL, "USB", "VID", "PID", "BID", "CID", NULL); /* whitebox customers */ if (self->customer_id == 0x0) { fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_ENFORCE_REQUIRES); } else { g_autofree gchar *vendor_id = g_strdup_printf("0x%02X", self->customer_id); fu_device_build_vendor_id(device, "SYNA", vendor_id); } /* read version */ if (!fu_synaptics_vmm9_device_command(self, FU_SYNAPTICS_VMM9_RC_CTRL_MEMORY_READ, FU_SYNAPTICS_VMM9_MEM_OFFSET_MCU_FW_VERSION, NULL, sizeof(buf), buf, sizeof(buf), FU_SYNAPTICS_VMM9_COMMAND_FLAG_FULL_BUFFER, error)) return FALSE; fu_device_set_version_raw(device, fu_memread_uint32(buf, G_BIG_ENDIAN)); /* read bootloader status */ if (!fu_synaptics_vmm9_device_command(self, FU_SYNAPTICS_VMM9_RC_CTRL_MEMORY_READ, FU_SYNAPTICS_VMM9_MEM_OFFSET_MCU_BOOTLOADER_STS, NULL, sizeof(buf), buf, sizeof(buf), FU_SYNAPTICS_VMM9_COMMAND_FLAG_FULL_BUFFER, error)) return FALSE; mcu_status = fu_memread_uint32(buf, G_BIG_ENDIAN); if (mcu_status & 1 << 7) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); } else { fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); } self->active_bank = (mcu_status >> 28) & 0b1; bootloader_version = g_strdup_printf("0.0.%03u", (guint)(24 >> 28) & 0b1111); fu_device_set_version_bootloader(device, bootloader_version); /* manual replug required */ if (fu_device_has_private_flag(device, FU_SYNAPTICS_VMM9_DEVICE_FLAG_MANUAL_RESTART_REQUIRED)) { fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_USER_REPLUG); fu_device_add_request_flag(device, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); } else { fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); } /* success */ return TRUE; } static gboolean fu_synaptics_vmm9_device_open(FuDevice *device, GError **error) { FuSynapticsVmm9Device *self = FU_SYNAPTICS_VMM9_DEVICE(device); guint8 payload[] = {'P', 'R', 'I', 'U', 'S'}; /* HidDevice->open */ if (!FU_DEVICE_CLASS(fu_synaptics_vmm9_device_parent_class)->open(device, error)) return FALSE; /* unconditionally disable, then enable RC with the magic token */ if (!fu_synaptics_vmm9_device_command(self, FU_SYNAPTICS_VMM9_RC_CTRL_DISABLE_RC, 0x0, /* offset */ NULL, 0, NULL, 0, FU_SYNAPTICS_VMM9_COMMAND_FLAG_NO_REPLY, error)) { g_prefix_error(error, "failed to DISABLE_RC before ENABLE_RC: "); return FALSE; } if (!fu_synaptics_vmm9_device_command(self, FU_SYNAPTICS_VMM9_RC_CTRL_ENABLE_RC, 0x0, /* offset */ payload, sizeof(payload), NULL, 0, FU_SYNAPTICS_VMM9_COMMAND_FLAG_FULL_BUFFER, error)) { g_prefix_error(error, "failed to ENABLE_RC: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_synaptics_vmm9_device_close(FuDevice *device, GError **error) { FuSynapticsVmm9Device *self = FU_SYNAPTICS_VMM9_DEVICE(device); /* no magic token required */ if (!fu_synaptics_vmm9_device_command(self, FU_SYNAPTICS_VMM9_RC_CTRL_DISABLE_RC, 0, /* offset */ NULL, 0x0, NULL, 0x0, FU_SYNAPTICS_VMM9_COMMAND_FLAG_NONE, error)) { g_prefix_error(error, "failed to DISABLE_RC: "); return FALSE; } /* HidDevice->close */ if (!FU_DEVICE_CLASS(fu_synaptics_vmm9_device_parent_class)->close(device, error)) return FALSE; /* success */ return TRUE; } static FuFirmware * fu_synaptics_vmm9_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuSynapticsVmm9Device *self = FU_SYNAPTICS_VMM9_DEVICE(device); g_autoptr(FuFirmware) firmware = fu_synaptics_vmm9_firmware_new(); g_autoptr(GInputStream) stream_partial = NULL; /* parse */ stream_partial = fu_partial_input_stream_new(stream, 0x0, fu_device_get_firmware_size_min(device), error); if (stream_partial == NULL) return NULL; if (!fu_firmware_parse_stream(firmware, stream_partial, 0x0, flags, error)) return NULL; /* verify this firmware is for this hardware */ if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_VID_PID) == 0) { if (self->board_id != fu_synaptics_vmm9_firmware_get_board_id(FU_SYNAPTICS_VMM9_FIRMWARE(firmware))) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "board ID mismatch, got 0x%02x, expected 0x%02x", fu_synaptics_vmm9_firmware_get_board_id( FU_SYNAPTICS_VMM9_FIRMWARE(firmware)), self->board_id); return NULL; } if (self->customer_id != fu_synaptics_vmm9_firmware_get_customer_id( FU_SYNAPTICS_VMM9_FIRMWARE(firmware))) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "customer ID mismatch, got 0x%02x, expected 0x%02x", fu_synaptics_vmm9_firmware_get_customer_id( FU_SYNAPTICS_VMM9_FIRMWARE(firmware)), self->customer_id); return NULL; } } /* success */ return g_steal_pointer(&firmware); } static gboolean fu_synaptics_vmm9_device_write_blocks(FuSynapticsVmm9Device *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_synaptics_vmm9_device_command(self, FU_SYNAPTICS_VMM9_RC_CTRL_WRITE_FLASH_DATA, fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), NULL, 0, FU_SYNAPTICS_VMM9_COMMAND_FLAG_NONE, error)) { g_prefix_error(error, "failed at page %u, @0x%x", fu_chunk_get_idx(chk), (guint)fu_chunk_get_address(chk)); return FALSE; } /* update progress */ fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_synaptics_vmm9_device_erase(FuSynapticsVmm9Device *self, FuProgress *progress, GError **error) { guint8 buf[2] = {0xFF, 0xFF}; if (!fu_synaptics_vmm9_device_command(self, FU_SYNAPTICS_VMM9_RC_CTRL_ERASE_FLASH, 0x0, /* offset */ buf, 2, NULL, 0, FU_SYNAPTICS_VMM9_COMMAND_FLAG_NONE, error)) { g_prefix_error(error, "failed to erase: "); return FALSE; } return TRUE; } static FuFirmware * fu_synaptics_vmm9_device_read_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuSynapticsVmm9Device *self = FU_SYNAPTICS_VMM9_DEVICE(device); gsize bufsz = fu_device_get_firmware_size_min(FU_DEVICE(self)); g_autofree guint8 *buf = g_malloc0(bufsz); g_autoptr(FuFirmware) firmware = fu_firmware_new(); g_autoptr(GBytes) fw = NULL; g_autoptr(GPtrArray) chunks = NULL; chunks = fu_chunk_array_mutable_new(buf, bufsz, 0, 0x0, FU_STRUCT_HID_PAYLOAD_SIZE_FIFO); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_synaptics_vmm9_device_command(self, FU_SYNAPTICS_VMM9_RC_CTRL_READ_FLASH_DATA, fu_chunk_get_address(chk), NULL, fu_chunk_get_data_sz(chk), fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), FU_SYNAPTICS_VMM9_COMMAND_FLAG_NONE, error)) { g_prefix_error(error, "failed at chunk %u, @0x%x", fu_chunk_get_idx(chk), (guint)fu_chunk_get_address(chk)); return NULL; } /* update progress */ fu_progress_step_done(progress); } /* parse */ fw = g_bytes_new_take(g_steal_pointer(&buf), bufsz); if (!fu_firmware_parse_bytes(firmware, fw, 0x0, FU_FIRMWARE_PARSE_FLAG_CACHE_STREAM, error)) return NULL; /* success */ return g_steal_pointer(&firmware); } static gboolean fu_synaptics_vmm9_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuSynapticsVmm9Device *self = FU_SYNAPTICS_VMM9_DEVICE(device); g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 3, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 1, NULL); /* erase the storage bank */ if (!fu_synaptics_vmm9_device_erase(self, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to erase: "); return FALSE; } fu_progress_step_done(progress); /* ensure the SPI flash is ready to access the write command */ fu_device_sleep_full(device, 3000, fu_progress_get_child(progress)); fu_progress_step_done(progress); /* write each block */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_STRUCT_HID_PAYLOAD_SIZE_FIFO, error); if (chunks == NULL) return FALSE; if (!fu_synaptics_vmm9_device_write_blocks(self, chunks, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to write: "); return FALSE; } fu_device_sleep(device, 10); fu_progress_step_done(progress); /* activate the firmware */ if (!fu_synaptics_vmm9_device_command(self, FU_SYNAPTICS_VMM9_RC_CTRL_ACTIVATE_FIRMWARE, 0x0, /* offset */ NULL, 0, NULL, 0, FU_SYNAPTICS_VMM9_COMMAND_FLAG_NONE, error)) { g_prefix_error(error, "failed to activate: "); return FALSE; } fu_progress_step_done(progress); /* if device reboot is not required */ if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_SKIPS_RESTART)) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_INSTALL_SKIP_VERSION_CHECK); g_debug("skipped device reboot intentionally."); return TRUE; } /* generic request */ if (fu_device_has_private_flag(device, FU_SYNAPTICS_VMM9_DEVICE_FLAG_MANUAL_RESTART_REQUIRED)) { g_autoptr(FwupdRequest) request = fwupd_request_new(); fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_IMMEDIATE); fwupd_request_set_id(request, FWUPD_REQUEST_ID_REPLUG_POWER); fwupd_request_add_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); if (!fu_device_emit_request(device, request, progress, error)) return FALSE; } else { guint8 buf[] = {0xF5, 0x00, 0x00, 0x00}; /* one register write to exactly the right place :) */ if (!fu_synaptics_vmm9_device_command( self, FU_SYNAPTICS_VMM9_RC_CTRL_MEMORY_WRITE, FU_SYNAPTICS_VMM9_MEM_OFFSET_RC_TRIGGER, buf, sizeof(buf), NULL, 0, FU_SYNAPTICS_VMM9_COMMAND_FLAG_FULL_BUFFER | FU_SYNAPTICS_VMM9_COMMAND_FLAG_IGNORE_REPLY, error)) { g_prefix_error(error, "failed to reboot: "); return FALSE; } } /* success! */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static void fu_synaptics_vmm9_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 4, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static gchar * fu_synaptics_vmm9_device_convert_version(FuDevice *device, guint64 version_raw) { return g_strdup_printf("%u.%02u.%03u", (guint)(version_raw >> 16) & 0xFF, (guint)(version_raw >> 24) & 0xFF, (guint)(version_raw >> 8) & 0xFF); } static void fu_synaptics_vmm9_device_init(FuSynapticsVmm9Device *self) { fu_device_set_firmware_size_min(FU_DEVICE(self), 0x7F000); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_install_duration(FU_DEVICE(self), 40); fu_device_add_protocol(FU_DEVICE(self), "com.synaptics.mst-hid"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_register_private_flag(FU_DEVICE(self), FU_SYNAPTICS_VMM9_DEVICE_FLAG_MANUAL_RESTART_REQUIRED); } static void fu_synaptics_vmm9_device_class_init(FuSynapticsVmm9DeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_synaptics_vmm9_device_to_string; device_class->setup = fu_synaptics_vmm9_device_setup; device_class->open = fu_synaptics_vmm9_device_open; device_class->close = fu_synaptics_vmm9_device_close; device_class->prepare_firmware = fu_synaptics_vmm9_device_prepare_firmware; device_class->write_firmware = fu_synaptics_vmm9_device_write_firmware; device_class->read_firmware = fu_synaptics_vmm9_device_read_firmware; device_class->set_progress = fu_synaptics_vmm9_device_set_progress; device_class->convert_version = fu_synaptics_vmm9_device_convert_version; } fwupd-2.0.10/plugins/synaptics-vmm9/fu-synaptics-vmm9-device.h000066400000000000000000000005561501337203100242250ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_SYNAPTICS_VMM9_DEVICE (fu_synaptics_vmm9_device_get_type()) G_DECLARE_FINAL_TYPE(FuSynapticsVmm9Device, fu_synaptics_vmm9_device, FU, SYNAPTICS_VMM9_DEVICE, FuHidDevice) fwupd-2.0.10/plugins/synaptics-vmm9/fu-synaptics-vmm9-firmware.c000066400000000000000000000071541501337203100245760ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-synaptics-vmm9-firmware.h" #include "fu-synaptics-vmm9-struct.h" struct _FuSynapticsVmm9Firmware { FuFirmware parent_instance; guint8 board_id; guint8 customer_id; }; G_DEFINE_TYPE(FuSynapticsVmm9Firmware, fu_synaptics_vmm9_firmware, FU_TYPE_FIRMWARE) #define FU_SYNAPTICS_VMM9_FIRMWARE_OFFSET_CUSTOMER_ID 0x0000620E #define FU_SYNAPTICS_VMM9_FIRMWARE_OFFSET_BOARD_ID 0x0000620F #define FU_SYNAPTICS_VMM9_FIRMWARE_OFFSET_VERSION 0x00015000 static void fu_synaptics_vmm9_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuSynapticsVmm9Firmware *self = FU_SYNAPTICS_VMM9_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "board_id", self->board_id); fu_xmlb_builder_insert_kx(bn, "customer_id", self->customer_id); } static gboolean fu_synaptics_vmm9_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_synaptics_vmm9_validate_stream(stream, offset, error); } static gboolean fu_synaptics_vmm9_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuSynapticsVmm9Firmware *self = FU_SYNAPTICS_VMM9_FIRMWARE(firmware); g_autoptr(GByteArray) st_hdr = NULL; guint8 version_major = 0; guint8 version_minor = 0; guint16 version_micro = 0; g_autofree gchar *version = NULL; /* verify header */ st_hdr = fu_struct_synaptics_vmm9_parse_stream(stream, 0x0, error); if (st_hdr == NULL) return FALSE; /* read version */ if (!fu_input_stream_read_u8(stream, FU_SYNAPTICS_VMM9_FIRMWARE_OFFSET_VERSION + 0x0, &version_major, error)) return FALSE; if (!fu_input_stream_read_u8(stream, FU_SYNAPTICS_VMM9_FIRMWARE_OFFSET_VERSION + 0x1, &version_minor, error)) return FALSE; if (!fu_input_stream_read_u16(stream, FU_SYNAPTICS_VMM9_FIRMWARE_OFFSET_VERSION + 0x2, &version_micro, G_LITTLE_ENDIAN, error)) return FALSE; version = g_strdup_printf("%u.%02u.%03u", version_major, version_minor, version_micro); fu_firmware_set_version(firmware, version); /* board and customer IDs */ if (!fu_input_stream_read_u8(stream, FU_SYNAPTICS_VMM9_FIRMWARE_OFFSET_BOARD_ID, &self->board_id, error)) return FALSE; if (!fu_input_stream_read_u8(stream, FU_SYNAPTICS_VMM9_FIRMWARE_OFFSET_CUSTOMER_ID, &self->customer_id, error)) return FALSE; /* success */ return TRUE; } guint8 fu_synaptics_vmm9_firmware_get_board_id(FuSynapticsVmm9Firmware *self) { g_return_val_if_fail(FU_IS_SYNAPTICS_VMM9_FIRMWARE(self), G_MAXUINT8); return self->board_id; } guint8 fu_synaptics_vmm9_firmware_get_customer_id(FuSynapticsVmm9Firmware *self) { g_return_val_if_fail(FU_IS_SYNAPTICS_VMM9_FIRMWARE(self), G_MAXUINT8); return self->customer_id; } static void fu_synaptics_vmm9_firmware_init(FuSynapticsVmm9Firmware *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_VID_PID); } static void fu_synaptics_vmm9_firmware_class_init(FuSynapticsVmm9FirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_synaptics_vmm9_firmware_validate; firmware_class->parse = fu_synaptics_vmm9_firmware_parse; firmware_class->export = fu_synaptics_vmm9_firmware_export; } FuFirmware * fu_synaptics_vmm9_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_SYNAPTICS_VMM9_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/synaptics-vmm9/fu-synaptics-vmm9-firmware.h000066400000000000000000000011141501337203100245710ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_SYNAPTICS_VMM9_FIRMWARE (fu_synaptics_vmm9_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuSynapticsVmm9Firmware, fu_synaptics_vmm9_firmware, FU, SYNAPTICS_VMM9_FIRMWARE, FuFirmware) FuFirmware * fu_synaptics_vmm9_firmware_new(void); guint8 fu_synaptics_vmm9_firmware_get_board_id(FuSynapticsVmm9Firmware *self); guint8 fu_synaptics_vmm9_firmware_get_customer_id(FuSynapticsVmm9Firmware *self); fwupd-2.0.10/plugins/synaptics-vmm9/fu-synaptics-vmm9-plugin.c000066400000000000000000000016401501337203100242520ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-synaptics-vmm9-device.h" #include "fu-synaptics-vmm9-firmware.h" #include "fu-synaptics-vmm9-plugin.h" struct _FuSynapticsVmm9Plugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuSynapticsVmm9Plugin, fu_synaptics_vmm9_plugin, FU_TYPE_PLUGIN) static void fu_synaptics_vmm9_plugin_init(FuSynapticsVmm9Plugin *self) { } static void fu_synaptics_vmm9_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_SYNAPTICS_VMM9_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_SYNAPTICS_VMM9_FIRMWARE); } static void fu_synaptics_vmm9_plugin_class_init(FuSynapticsVmm9PluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_synaptics_vmm9_plugin_constructed; } fwupd-2.0.10/plugins/synaptics-vmm9/fu-synaptics-vmm9-plugin.h000066400000000000000000000004371501337203100242620ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuSynapticsVmm9Plugin, fu_synaptics_vmm9_plugin, FU, SYNAPTICS_VMM9_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/synaptics-vmm9/fu-synaptics-vmm9.rs000066400000000000000000000037021501337203100231610ustar00rootroot00000000000000// Copyright 2024 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ValidateStream, ParseStream, Default)] #[repr(C, packed)] struct FuStructSynapticsVmm9 { signature: [char; 7] == "CARRERA", } #[repr(u8)] enum FuSynapticsVmm9RcCtrl { EnableRc = 0x01, DisableRc = 0x02, GetId = 0x03, EraseFlash = 0x14, ActivateFirmware = 0x18, WriteFlashData = 0x20, MemoryWrite = 0x21, TxDpcdRegisterWrite = 0x22, // TX0 to TX3 ReadFlashData = 0x30, MemoryRead = 0x31, TxDpcdRegisterRead = 0x32, // TX0 to TX3 // these are fake, but useful for debugging EnableRcBusy = 0x80|0x01, DisableRcBusy = 0x80|0x02, GetIdBusy = 0x80|0x03, EraseFlashBusy = 0x80|0x14, ActivateFirmwareBusy = 0x80|0x18, WriteFlashDataBusy = 0x80|0x20, MemoryWriteBusy = 0x80|0x21, ReadFlashDataBusy = 0x80|0x30, MemoryReadBusy = 0x80|0x31, } #[derive(ToString)] #[repr(u8)] enum FuSynapticsVmm9RcSts { Success, Invalid, Unsupported, Failed, Disabled, ConfigureSignFailed, FirmwareSignFailed, RollbackFailed, } #[derive(New, Getters)] #[repr(C, packed)] struct FuStructHidPayload { cap: u8, state: u8, ctrl: FuSynapticsVmm9RcCtrl, sts: FuSynapticsVmm9RcSts, offset: u32le, length: u32le, fifo: [u8; 32], } #[derive(New, ToString, Getters, Default)] #[repr(C, packed)] struct FuStructHidSetCommand { id: u8 == 0x1, type: u8 == 0x0, // packet write size: u8, payload: FuStructHidPayload, checksum: u8, // this is actually lower if @rc_fifo is less than 32 bytes } #[derive(New, Parse, Default)] #[repr(C, packed)] struct FuStructHidGetCommand { id: u8 == 0x1, type: u8 == 0x0, // packet reply size: u8, payload: FuStructHidPayload, checksum: u8, // payload is always 32 bytes } #[derive(Parse)] #[repr(C, packed)] struct FuStructSynapticsUpdGetId { _pid: u16le, cid: u8, bid: u8, } fwupd-2.0.10/plugins/synaptics-vmm9/meson.build000066400000000000000000000010561501337203100214420ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginSynapticsVmm9"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('synaptics-vmm9.quirk') plugin_builtins += static_library('fu_plugin_synaptics_vmm9', rustgen.process('fu-synaptics-vmm9.rs'), sources: [ 'fu-synaptics-vmm9-device.c', 'fu-synaptics-vmm9-firmware.c', 'fu-synaptics-vmm9-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files('tests/synaptics-vmm9430evb.json') fwupd-2.0.10/plugins/synaptics-vmm9/parse-pcap.py000077500000000000000000000025201501337203100217050ustar00rootroot00000000000000#!/usr/bin/env python3 # pylint: disable=invalid-name,missing-docstring # # Copyright 2024 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later import struct import sys import binascii from pcapng import FileScanner def parse_pkt(block, title: str, offset: int): ( st_id, st_type, st_size, rc_ctrl, rc_sts, rc_offset, rc_length, ) = struct.unpack_from(" * Copyright 2021 Jeremy Soller * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-system76-launch-device.h" #include "fu-system76-launch-struct.h" #define SYSTEM76_LAUNCH_CMD_VERSION 3 #define SYSTEM76_LAUNCH_CMD_RESET 6 #define SYSTEM76_LAUNCH_CMD_SECURITY_SET 21 #define SYSTEM76_LAUNCH_TIMEOUT 1000 struct _FuSystem76LaunchDevice { FuUsbDevice parent_instance; }; G_DEFINE_TYPE(FuSystem76LaunchDevice, fu_system76_launch_device, FU_TYPE_USB_DEVICE) typedef struct { guint8 *data; gsize len; } FuSystem76LaunchDeviceHelper; static gboolean fu_system76_launch_device_response_cb(FuDevice *device, gpointer user_data, GError **error) { const guint8 ep_in = 0x82; gsize actual_len = 0; FuSystem76LaunchDeviceHelper *helper = (FuSystem76LaunchDeviceHelper *)user_data; /* receive response */ if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(device), ep_in, helper->data, helper->len, &actual_len, SYSTEM76_LAUNCH_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to read response: "); return FALSE; } if (actual_len < helper->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "response truncated: received %" G_GSIZE_FORMAT " bytes", actual_len); return FALSE; } /* success */ return TRUE; } static gboolean fu_system76_launch_device_command(FuDevice *device, guint8 *data, gsize len, GError **error) { const guint8 ep_out = 0x03; gsize actual_len = 0; FuSystem76LaunchDeviceHelper helper = {.data = data, .len = len}; /* send command */ if (!fu_usb_device_interrupt_transfer(FU_USB_DEVICE(device), ep_out, data, len, &actual_len, SYSTEM76_LAUNCH_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to send command: "); return FALSE; } if (actual_len < len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "command truncated: sent %" G_GSIZE_FORMAT " bytes", actual_len); return FALSE; } return fu_device_retry(device, fu_system76_launch_device_response_cb, 5, &helper, error); } static gboolean fu_system76_launch_device_version_cb(FuDevice *device, gpointer user_data, GError **error) { guint8 data[32] = {SYSTEM76_LAUNCH_CMD_VERSION, 0}; g_autofree gchar *version = NULL; /* execute version command */ if (!fu_system76_launch_device_command(device, data, sizeof(data), error)) { g_prefix_error(error, "failed to execute version command: "); return FALSE; } version = g_strdup_printf("%s", &data[2]); fu_device_set_version(device, version); return TRUE; } static gboolean fu_system76_launch_device_setup(FuDevice *device, GError **error) { /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_system76_launch_device_parent_class)->setup(device, error)) return FALSE; /* set version */ return fu_device_retry_full(device, fu_system76_launch_device_version_cb, 5, 500, NULL, error); } static gboolean fu_system76_launch_device_reset(FuDevice *device, guint8 *rc, GError **error) { guint8 data[32] = {SYSTEM76_LAUNCH_CMD_RESET, 0}; /* execute reset command */ if (!fu_system76_launch_device_command(device, data, sizeof(data), error)) { g_prefix_error(error, "failed to execute reset command: "); return FALSE; } *rc = data[1]; return TRUE; } static gboolean fu_system76_launch_device_security_set(FuDevice *device, FuSystem76LaunchSecurityState state, guint8 *rc, GError **error) { guint8 data[32] = {SYSTEM76_LAUNCH_CMD_SECURITY_SET, 0, state, 0}; /* execute security set command */ if (!fu_system76_launch_device_command(device, data, sizeof(data), error)) { g_prefix_error(error, "failed to execute security set command: "); return FALSE; } *rc = data[1]; return TRUE; } static gboolean fu_system76_launch_device_detach(FuDevice *device, FuProgress *progress, GError **error) { guint8 rc = 0x0; g_autoptr(FwupdRequest) request = fwupd_request_new(); g_autoptr(GTimer) timer = g_timer_new(); /* prompt for unlock if reset was blocked */ if (!fu_system76_launch_device_reset(device, &rc, error)) return FALSE; /* unlikely, but already unlocked */ if (rc == 0) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } /* notify device of desire to unlock */ if (!fu_system76_launch_device_security_set( device, FU_SYSTEM76_LAUNCH_SECURITY_STATE_PREPARE_UNLOCK, &rc, error)) return FALSE; /* generate a message if not already set */ if (fu_device_get_update_message(device) == NULL) { g_autofree gchar *msg = NULL; const gchar *unlock_keys; switch (fu_device_get_pid(device)) { case 0x0001: /* launch_1 */ unlock_keys = "Fn+Esc"; break; case 0x000B: /* thelio_io_2 */ unlock_keys = "the Power button"; break; default: unlock_keys = "Left Ctrl+Right Ctrl+Esc"; break; } msg = g_strdup_printf( "To ensure you have physical access, %s needs to be manually unlocked. " "Please press %s to unlock and re-run the update.", fu_device_get_name(device), unlock_keys); fu_device_set_update_message(device, msg); } /* the user has to do something */ fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_IMMEDIATE); fwupd_request_set_id(request, FWUPD_REQUEST_ID_PRESS_UNLOCK); fwupd_request_set_message(request, fu_device_get_update_message(device)); if (!fu_device_emit_request(device, request, progress, error)) return FALSE; /* poll for the user-unlock */ do { fu_device_sleep(device, 1000); /* ms */ if (!fu_system76_launch_device_reset(device, &rc, error)) return FALSE; } while (rc != 0 && g_timer_elapsed(timer, NULL) * 1000.f < FU_DEVICE_REMOVE_DELAY_USER_REPLUG); if (rc != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NEEDS_USER_ACTION, fu_device_get_update_message(device)); return FALSE; } /* success */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static void fu_system76_launch_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 30, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 40, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 5, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 25, "reload"); } static void fu_system76_launch_device_init(FuSystem76LaunchDevice *self) { fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_COUNTERPART_GUIDS); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_INSTANCE_ID_REV); fu_device_add_request_flag(FU_DEVICE(self), FWUPD_REQUEST_FLAG_NON_GENERIC_MESSAGE); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); fu_device_add_protocol(FU_DEVICE(self), "com.microsoft.uf2"); fu_device_add_protocol(FU_DEVICE(self), "org.usb.dfu"); fu_device_retry_set_delay(FU_DEVICE(self), 100); fu_usb_device_add_interface(FU_USB_DEVICE(self), 0x01); } static void fu_system76_launch_device_class_init(FuSystem76LaunchDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_system76_launch_device_setup; device_class->detach = fu_system76_launch_device_detach; device_class->set_progress = fu_system76_launch_device_set_progress; } fwupd-2.0.10/plugins/system76-launch/fu-system76-launch-device.h000066400000000000000000000006511501337203100243350ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * Copyright 2021 Jeremy Soller * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_SYSTEM76_LAUNCH_DEVICE (fu_system76_launch_device_get_type()) G_DECLARE_FINAL_TYPE(FuSystem76LaunchDevice, fu_system76_launch_device, FU, SYSTEM76_LAUNCH_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/system76-launch/fu-system76-launch-plugin.c000066400000000000000000000015541501337203100243720ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * Copyright 2021 Jeremy Soller * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-system76-launch-device.h" #include "fu-system76-launch-plugin.h" struct _FuSystem76LaunchPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuSystem76LaunchPlugin, fu_system76_launch_plugin, FU_TYPE_PLUGIN) static void fu_system76_launch_plugin_init(FuSystem76LaunchPlugin *self) { } static void fu_system76_launch_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_SYSTEM76_LAUNCH_DEVICE); } static void fu_system76_launch_plugin_class_init(FuSystem76LaunchPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_system76_launch_plugin_constructed; } fwupd-2.0.10/plugins/system76-launch/fu-system76-launch-plugin.h000066400000000000000000000004421501337203100243720ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuSystem76LaunchPlugin, fu_system76_launch_plugin, FU, SYSTEM76_LAUNCH_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/system76-launch/fu-system76-launch.rs000066400000000000000000000006671501337203100233040ustar00rootroot00000000000000// Copyright 2025 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later enum FuSystem76LaunchSecurityState { Lock, // flashing is prevented, cannot be set with CMD_SECURITY_SET Unlock, // flashing is allowed, cannot be set with CMD_SECURITY_SET PrepareLock, // flashing will be prevented on the next reboot PrepareUnlock, // flashing will be allowed on the next reboot } fwupd-2.0.10/plugins/system76-launch/meson.build000066400000000000000000000007261501337203100215130ustar00rootroot00000000000000plugins += {meson.current_source_dir().split('/')[-1]: true} cargs = ['-DG_LOG_DOMAIN="FuPluginSystem76Launch"'] plugin_quirks += files('system76-launch.quirk') plugin_builtins += static_library('fu_plugin_system76_launch', rustgen.process('fu-system76-launch.rs'), sources: [ 'fu-system76-launch-plugin.c', 'fu-system76-launch-device.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) fwupd-2.0.10/plugins/system76-launch/system76-launch.quirk000066400000000000000000000014331501337203100233730ustar00rootroot00000000000000# System76 launch_1 [USB\VID_3384&PID_0001] Plugin = system76_launch CounterpartGuid = USB\VID_03EB&PID_2FF4 # System76 launch_lite_1 [USB\VID_3384&PID_0005] Plugin = system76_launch CounterpartGuid = USB\VID_03EB&PID_2FF9 # System76 launch_2 [USB\VID_3384&PID_0006] Plugin = system76_launch CounterpartGuid = USB\VID_03EB&PID_2FF9 # System76 launch_heavy_1 [USB\VID_3384&PID_0007] Plugin = system76_launch CounterpartGuid = USB\VID_03EB&PID_2FF9 # System76 launch_3 [USB\VID_3384&PID_0009] Plugin = system76_launch CounterpartGuid = BLOCK\VEN_2E8A&DEV_0003 # System76 launch_heavy_3 [USB\VID_3384&PID_000A] Plugin = system76_launch CounterpartGuid = BLOCK\VEN_2E8A&DEV_0003 # System76 thelio_io_2 [USB\VID_3384&PID_000B] Plugin = system76_launch CounterpartGuid = BLOCK\VEN_2E8A&DEV_0003 fwupd-2.0.10/plugins/telink-dfu/000077500000000000000000000000001501337203100164355ustar00rootroot00000000000000fwupd-2.0.10/plugins/telink-dfu/README.md000066400000000000000000000027711501337203100177230ustar00rootroot00000000000000--- title: Plugin: Telink DFU --- ## Introduction This plugin allows upgrade of Telink OTA-compliant devices. Supported devices: * 8208 Dual Keyboard ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a packed binary file format. The cabinet file prepared by Telink Semiconductor includes a validated firmware image `firmware.bin`, an accompanying `manifest.json` of firmware description. At the moment only the version field takes effect. This plugin supports the following protocol ID: * `com.telink.dfu` ## GUID Generation These devices use the standard BLUETOOTH DeviceInstanceId values, e.g. * `BLUETOOTH\VID_248A&PID_8208` HIDRAW is expected to be supported in the future. ## Update Behavior The upgrade mechanism resembles the way used in Telink OTA App. Read the [handbook on Telink Wiki site](https://wiki.telink-semi.cn/wiki/index.html) for details. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x248A`. ## Quirk Use This plugin uses the following plugin-specific quirks: ### TelinkHidToolVer The BCD tool version. Since: 2.0.2 ## External Interface Access This plugin requires access to bluetooth GATT characteristics. ## Version Considerations This plugin has been available since fwupd version `2.0.0`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Liang-Jiazhi: @liangjiazhi-telink fwupd-2.0.10/plugins/telink-dfu/fu-telink-dfu-archive.c000066400000000000000000000143731501337203100227020ustar00rootroot00000000000000/* * Copyright 2024 Mike Chang * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-telink-dfu-archive.h" #include "fu-telink-dfu-common.h" struct _FuTelinkDfuArchive { FuFirmware parent_instance; }; G_DEFINE_TYPE(FuTelinkDfuArchive, fu_telink_dfu_archive, FU_TYPE_FIRMWARE) #define FU_TELINK_DFU_FIRMWARE_JSON_FORMAT_VERSION_MAX 0 static gboolean fu_telink_dfu_archive_load_file(FuTelinkDfuArchive *self, FuArchive *archive, JsonObject *obj, guint i, FuFirmwareParseFlags flags, GError **error) { struct { const gchar *name; FwupdVersionFormat ver_format; } bl_type_keys[] = { {"beta", FWUPD_VERSION_FORMAT_TRIPLET}, {"ota-v1", FWUPD_VERSION_FORMAT_TRIPLET}, {"usb-dongle-simple", FWUPD_VERSION_FORMAT_PAIR}, }; const gchar *board_name; const gchar *bootloader_name; const gchar *filename; const gchar *version; gboolean supported_bootloader = FALSE; g_autofree gchar *image_id = NULL; g_autoptr(FuFirmware) image = fu_firmware_new(); g_autoptr(GBytes) blob = NULL; guint bl_type_idx; if (!json_object_has_member(obj, "file")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "manifest invalid as has no file name for the image"); return FALSE; } filename = json_object_get_string_member(obj, "file"); blob = fu_archive_lookup_by_fn(archive, filename, error); if (blob == NULL) return FALSE; if (!json_object_has_member(obj, "bootloader_type")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "missing param: bootloader_type"); return FALSE; } bootloader_name = json_object_get_string_member(obj, "bootloader_type"); for (bl_type_idx = 0; bl_type_idx < sizeof(bl_type_keys) / sizeof(bl_type_keys[0]); bl_type_idx++) { if (g_strcmp0(bootloader_name, bl_type_keys[bl_type_idx].name) == 0) { supported_bootloader = TRUE; break; } } if (!supported_bootloader) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "bad param: bootloader_type"); return FALSE; } if (!json_object_has_member(obj, "board")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "missing param: board"); return FALSE; } board_name = json_object_get_string_member(obj, "board"); /* string format: __N * will compare 'image_id' in fu_firmware_get_image_by_id() * e.g. 8278_otav1_bank0 */ image_id = g_strdup_printf("%s_%s_bank%01u", board_name, bootloader_name, i); if (!fu_firmware_parse_bytes(image, blob, 0x0, flags, error)) return FALSE; g_debug("image_id=%s", image_id); fu_firmware_set_id(image, image_id); fu_firmware_set_idx(image, i); if (json_object_has_member(obj, "load_address")) { guint image_addr = json_object_get_int_member(obj, "load_address"); fu_firmware_set_addr(image, image_addr); } fu_firmware_add_image(FU_FIRMWARE(self), image); if (!json_object_has_member(obj, "image_version")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "missing param: image_version"); return FALSE; } version = json_object_get_string_member(obj, "image_version"); fu_firmware_set_version_raw( FU_FIRMWARE(self), fu_telink_dfu_parse_image_version(version, bl_type_keys[bl_type_idx].ver_format)); fu_firmware_set_version(FU_FIRMWARE(self), version); /* nocheck:set-version */ /* success */ return TRUE; } static gboolean fu_telink_dfu_archive_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuTelinkDfuArchive *self = FU_TELINK_DFU_ARCHIVE(firmware); JsonArray *json_files; JsonNode *json_root_node; JsonObject *json_obj; guint files_cnt = 0; guint manifest_ver; g_autoptr(FuArchive) archive = NULL; g_autoptr(GBytes) manifest = NULL; g_autoptr(JsonParser) parser = json_parser_new(); /* load archive */ archive = fu_archive_new_stream(stream, FU_ARCHIVE_FLAG_IGNORE_PATH, error); if (archive == NULL) return FALSE; /* parse manifest.json */ manifest = fu_archive_lookup_by_fn(archive, "manifest.json", error); if (manifest == NULL) return FALSE; if (!json_parser_load_from_data(parser, g_bytes_get_data(manifest, NULL), g_bytes_get_size(manifest), error)) { g_prefix_error(error, "manifest not in JSON format: "); return FALSE; } json_root_node = json_parser_get_root(parser); if (json_root_node == NULL || !JSON_NODE_HOLDS_OBJECT(json_root_node)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "manifest invalid as has no root"); return FALSE; } json_obj = json_node_get_object(json_root_node); if (!json_object_has_member(json_obj, "format-version")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "manifest has invalid format"); return FALSE; } /* maximum-allowed format version(backward compatibility) */ manifest_ver = json_object_get_int_member(json_obj, "format-version"); if (manifest_ver > FU_TELINK_DFU_FIRMWARE_JSON_FORMAT_VERSION_MAX) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "unsupported manifest version"); return FALSE; } g_debug("manifest_ver=0x%u", manifest_ver); /* get files */ json_files = json_object_get_array_member(json_obj, "files"); if (json_files == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "manifest invalid as has no 'files' array"); return FALSE; } files_cnt = json_array_get_length(json_files); if (files_cnt == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "manifest invalid as contains no update images"); return FALSE; } for (guint i = 0; i < files_cnt; i++) { JsonObject *obj = json_array_get_object_element(json_files, i); if (!fu_telink_dfu_archive_load_file(self, archive, obj, i, flags, error)) return FALSE; } /* success */ return TRUE; } static void fu_telink_dfu_archive_init(FuTelinkDfuArchive *self) { } static void fu_telink_dfu_archive_class_init(FuTelinkDfuArchiveClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_telink_dfu_archive_parse; } fwupd-2.0.10/plugins/telink-dfu/fu-telink-dfu-archive.h000066400000000000000000000005051501337203100226770ustar00rootroot00000000000000/* * Copyright 2024 Mike Chang * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_TELINK_DFU_ARCHIVE (fu_telink_dfu_archive_get_type()) G_DECLARE_FINAL_TYPE(FuTelinkDfuArchive, fu_telink_dfu_archive, FU, TELINK_DFU_ARCHIVE, FuFirmware) fwupd-2.0.10/plugins/telink-dfu/fu-telink-dfu-ble-device.c000066400000000000000000000164361501337203100232620ustar00rootroot00000000000000/* * Copyright 2024 Mike Chang * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-telink-dfu-archive.h" #include "fu-telink-dfu-ble-device.h" #include "fu-telink-dfu-common.h" #include "fu-telink-dfu-struct.h" struct _FuTelinkDfuBleDevice { FuBluezDevice parent_instance; }; G_DEFINE_TYPE(FuTelinkDfuBleDevice, fu_telink_dfu_ble_device, FU_TYPE_BLUEZ_DEVICE) #define FU_TELINK_DFU_HID_DEVICE_START_ADDR 0x5000 #define FU_TELINK_DFU_BLE_DEVICE_UUID_OTA "00010203-0405-0607-0809-0a0b0c0d2b12" static FuStructTelinkDfuBlePkt * fu_telink_dfu_ble_device_create_packet(guint16 preamble, const guint8 *buf, gsize bufsz, GError **error) { g_autoptr(FuStructTelinkDfuBlePkt) st_pkt = fu_struct_telink_dfu_ble_pkt_new(); fu_struct_telink_dfu_ble_pkt_set_preamble(st_pkt, preamble); if (buf != NULL) { if (!fu_struct_telink_dfu_ble_pkt_set_payload(st_pkt, buf, bufsz, error)) return NULL; } fu_struct_telink_dfu_ble_pkt_set_crc( st_pkt, ~fu_crc16(FU_CRC_KIND_B16_USB, st_pkt->data, st_pkt->len - 2)); return g_steal_pointer(&st_pkt); } static gboolean fu_telink_dfu_ble_device_write_blocks(FuTelinkDfuBleDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(FuStructTelinkDfuBlePkt) st_pkt = NULL; /* send chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; st_pkt = fu_telink_dfu_ble_device_create_packet((guint16)i, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error); if (st_pkt == NULL) return FALSE; if (!fu_bluez_device_write(FU_BLUEZ_DEVICE(self), FU_TELINK_DFU_BLE_DEVICE_UUID_OTA, st_pkt, error)) return FALSE; fu_device_sleep(FU_DEVICE(self), 5); /* update progress */ fu_progress_step_done(progress); } /* success */ fu_device_sleep(FU_DEVICE(self), 5); return TRUE; } static gboolean fu_telink_dfu_ble_device_ota_start(FuTelinkDfuBleDevice *self, GError **error) { g_autoptr(FuStructTelinkDfuBlePkt) st_pkt = NULL; st_pkt = fu_telink_dfu_ble_device_create_packet(FU_TELINK_DFU_CMD_OTA_START, NULL, 0, error); if (st_pkt == NULL) return FALSE; if (!fu_bluez_device_write(FU_BLUEZ_DEVICE(self), FU_TELINK_DFU_BLE_DEVICE_UUID_OTA, st_pkt, error)) return FALSE; /* success */ fu_device_sleep(FU_DEVICE(self), 5); return TRUE; } static gboolean fu_telink_dfu_ble_device_ota_stop(FuTelinkDfuBleDevice *self, guint number_chunks, GError **error) { g_autoptr(FuStructTelinkDfuEndCheck) st_end_check = fu_struct_telink_dfu_end_check_new(); g_autoptr(FuStructTelinkDfuBlePkt) st_pkt = NULL; guint16 pkt_index = (guint16)(number_chunks >> 4) - 1; /* last data packet index */ fu_struct_telink_dfu_end_check_set_pkt_index(st_end_check, pkt_index); fu_struct_telink_dfu_end_check_set_inverted_pkt_index(st_end_check, ~pkt_index); st_pkt = fu_telink_dfu_ble_device_create_packet(FU_TELINK_DFU_CMD_OTA_END, st_end_check->data, st_end_check->len, error); if (st_pkt == NULL) return FALSE; if (!fu_bluez_device_write(FU_BLUEZ_DEVICE(self), FU_TELINK_DFU_BLE_DEVICE_UUID_OTA, st_pkt, error)) return FALSE; /* success */ fu_device_sleep(FU_DEVICE(self), 20000); return TRUE; } static gboolean fu_telink_dfu_ble_device_write_blob(FuTelinkDfuBleDevice *self, GBytes *blob, FuProgress *progress, GError **error) { g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(FuStructTelinkDfuBlePkt) st_pkt = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "ota-start"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 70, "ota-data"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 29, "ota-stop"); /* ensure we can get the current version */ st_pkt = fu_telink_dfu_ble_device_create_packet(FU_TELINK_DFU_CMD_OTA_FW_VERSION, NULL, 0, error); if (st_pkt == NULL) return FALSE; if (!fu_bluez_device_write(FU_BLUEZ_DEVICE(self), FU_TELINK_DFU_BLE_DEVICE_UUID_OTA, st_pkt, error)) return FALSE; fu_device_sleep(FU_DEVICE(self), 5); /* OTA start command */ if (!fu_telink_dfu_ble_device_ota_start(self, error)) return FALSE; fu_progress_step_done(progress); /* OTA firmware data */ chunks = fu_chunk_array_new_from_bytes(blob, FU_TELINK_DFU_HID_DEVICE_START_ADDR, FU_CHUNK_PAGESZ_NONE, FU_STRUCT_TELINK_DFU_BLE_PKT_SIZE_PAYLOAD); if (!fu_telink_dfu_ble_device_write_blocks(self, chunks, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* OTA stop command */ if (!fu_telink_dfu_ble_device_ota_stop(self, fu_chunk_array_length(chunks), error)) return FALSE; fu_progress_step_done(progress); /* success */ fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_telink_dfu_ble_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuTelinkDfuBleDevice *self = FU_TELINK_DFU_BLE_DEVICE(device); g_autoptr(FuArchive) archive = NULL; g_autoptr(GBytes) blob = NULL; g_autoptr(GInputStream) stream = NULL; /* get default image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; archive = fu_archive_new_stream(stream, FU_ARCHIVE_FLAG_IGNORE_PATH, error); if (archive == NULL) return FALSE; blob = fu_archive_lookup_by_fn(archive, "firmware.bin", error); if (blob == NULL) return FALSE; return fu_telink_dfu_ble_device_write_blob(self, blob, progress, error); } static void fu_telink_dfu_ble_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_telink_dfu_ble_device_init(FuTelinkDfuBleDevice *self) { fu_device_set_vendor(FU_DEVICE(self), "Telink"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_remove_delay(FU_DEVICE(self), 10000); /* ms */ fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_TELINK_DFU_ARCHIVE); fu_device_add_protocol(FU_DEVICE(self), "com.telink.dfu"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); } static void fu_telink_dfu_ble_device_class_init(FuTelinkDfuBleDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_telink_dfu_ble_device_write_firmware; device_class->set_progress = fu_telink_dfu_ble_device_set_progress; } fwupd-2.0.10/plugins/telink-dfu/fu-telink-dfu-ble-device.h000066400000000000000000000005621501337203100232600ustar00rootroot00000000000000/* * Copyright 2024 Mike Chang * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_TELINK_DFU_BLE_DEVICE (fu_telink_dfu_ble_device_get_type()) G_DECLARE_FINAL_TYPE(FuTelinkDfuBleDevice, fu_telink_dfu_ble_device, FU, TELINK_DFU_BLE_DEVICE, FuBluezDevice) fwupd-2.0.10/plugins/telink-dfu/fu-telink-dfu-common.c000066400000000000000000000024551501337203100225470ustar00rootroot00000000000000/* * Copyright 2024 Mike Chang * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-telink-dfu-common.h" guint32 fu_telink_dfu_parse_image_version(const gchar *version, FwupdVersionFormat ver_format) { gint rc; guint32 v_major = 0; guint32 v_minor = 0; guint32 v_patch = 0; guint32 ver_hex = 0; /* revision not available; forced update */ if (version == NULL) return 0; if (ver_format == FWUPD_VERSION_FORMAT_TRIPLET) { /* version format: aa.bb.cc */ rc = sscanf(version, "%u.%u.%u", &v_major, &v_minor, &v_patch); if (rc != 3 || v_major > 999 || v_minor > 999 || v_patch > 999) { /* invalid version format; forced update */ g_warning("invalid version string(FORMAT_TRIPLET): %s", version); } else { ver_hex = (v_major << 24) | (v_minor << 16) | v_patch; } } else if (ver_format == FWUPD_VERSION_FORMAT_PAIR) { /* version format: aaaa.bbbb */ rc = sscanf(version, "%u.%u", &v_major, &v_minor); if (rc != 2 || v_major > 99 || v_minor > 99) { /* invalid version format; forced update */ g_warning("invalid version string(FORMAT_PAIR): %s", version); } else { ver_hex = (v_major << 16) | v_minor; } } else { g_warning("unsupported version format: %u", ver_format); } return ver_hex; } fwupd-2.0.10/plugins/telink-dfu/fu-telink-dfu-common.h000066400000000000000000000003731501337203100225510ustar00rootroot00000000000000/* * Copyright 2024 Mike Chang * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include guint32 fu_telink_dfu_parse_image_version(const gchar *version, FwupdVersionFormat ver_format); fwupd-2.0.10/plugins/telink-dfu/fu-telink-dfu-hid-device.c000066400000000000000000000344161501337203100232620ustar00rootroot00000000000000/* * Copyright 2024 Mike Chang * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-telink-dfu-archive.h" #include "fu-telink-dfu-hid-device.h" #include "fu-telink-dfu-struct.h" struct _FuTelinkDfuHidDevice { FuHidDevice parent_instance; guint16 windows_hid_tool_ver; }; G_DEFINE_TYPE(FuTelinkDfuHidDevice, fu_telink_dfu_hid_device, FU_TYPE_HID_DEVICE) #define FU_TELINK_DFU_HID_DEVICE_START_ADDR 0x0000 #define FU_TELINK_DFU_HID_DEVICE_REPORT_TIMEOUT 500 /* ms */ #define FU_TELINK_DFU_HID_DEVICE_OTA_LENGTH 65 #define FU_TELINK_DFU_HID_DEVICE_OTA_START_LEN 2 #define FU_TELINK_DFU_HID_DEVICE_OTA_END_LEN 6 #define FU_TELINK_DFU_HID_DEVICE_OTA_DATA_LEN 20 #define FU_TELINK_DFU_HID_DEVICE_REPORT_ID 6 #define FU_TELINK_DFU_HID_EP_IN (0x80 | 4) #define FU_TELINK_DFU_HID_EP_OUT (0x00 | 5) #define FU_TELINK_DEVICE_WINDOWS_TOOL_VERSION(a, b) ((a) * 100 + (b)) static void fu_telink_dfu_hid_device_to_string(FuDevice *device, guint idt, GString *str) { FuTelinkDfuHidDevice *self = FU_TELINK_DFU_HID_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "WindowsHidToolVer", self->windows_hid_tool_ver); } static FuStructTelinkDfuHidPkt * fu_telink_dfu_hid_device_create_packet(FuTelinkDfuCmd cmd, const guint8 *buf, gsize bufsz, GError **error) { guint16 ota_data_len; g_autoptr(FuStructTelinkDfuHidPkt) st_pkt = fu_struct_telink_dfu_hid_pkt_new(); g_autoptr(FuStructTelinkDfuHidPkt) st_payload = fu_struct_telink_dfu_hid_pkt_payload_new(); switch (cmd) { case FU_TELINK_DFU_CMD_OTA_FW_VERSION: ota_data_len = 0; break; case FU_TELINK_DFU_CMD_OTA_START: ota_data_len = FU_TELINK_DFU_HID_DEVICE_OTA_START_LEN; break; case FU_TELINK_DFU_CMD_OTA_END: ota_data_len = FU_TELINK_DFU_HID_DEVICE_OTA_END_LEN; break; default: /* OTA data */ ota_data_len = FU_TELINK_DFU_HID_DEVICE_OTA_DATA_LEN; break; } fu_struct_telink_dfu_hid_pkt_payload_set_ota_cmd(st_payload, cmd); if (buf != NULL) { if (!fu_struct_telink_dfu_hid_pkt_payload_set_ota_data(st_payload, buf, bufsz, error)) { return NULL; } } /* exclude the ota_cmd field */ fu_struct_telink_dfu_hid_pkt_payload_set_crc( st_payload, ~fu_crc16(FU_CRC_KIND_B16_USB, st_payload->data, st_payload->len - 2)); fu_struct_telink_dfu_hid_pkt_set_ota_data_len(st_pkt, ota_data_len); if (!fu_struct_telink_dfu_hid_pkt_set_payload(st_pkt, st_payload, error)) return NULL; return g_steal_pointer(&st_pkt); } static gboolean fu_telink_dfu_hid_device_write(FuTelinkDfuHidDevice *self, const guint8 *buf, gsize bufsz, GError **error) { FuHidDeviceFlags set_report_flag = FU_HID_DEVICE_FLAG_NONE; guint8 buf_mut[FU_TELINK_DFU_HID_DEVICE_OTA_LENGTH] = {0}; if (self->windows_hid_tool_ver >= FU_TELINK_DEVICE_WINDOWS_TOOL_VERSION(5, 2)) set_report_flag = FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER; if (!fu_memcpy_safe(buf_mut, sizeof(buf_mut), 0x0, buf, bufsz, 0x0, bufsz, error)) return FALSE; return fu_hid_device_set_report(FU_HID_DEVICE(self), FU_TELINK_DFU_HID_DEVICE_REPORT_ID, buf_mut, sizeof(buf_mut), FU_TELINK_DFU_HID_DEVICE_REPORT_TIMEOUT, set_report_flag, error); } static gboolean fu_telink_dfu_hid_device_write_blocks(FuTelinkDfuHidDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { guint payload_index = 0; g_autoptr(FuStructTelinkDfuHidLongPkt) st_long_pkt = fu_struct_telink_dfu_hid_long_pkt_new(); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(FuStructTelinkDfuHidPkt) st_pkt = NULL; /* send chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; st_pkt = fu_telink_dfu_hid_device_create_packet((guint16)i, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error); if (st_pkt == NULL) return FALSE; if (self->windows_hid_tool_ver >= FU_TELINK_DEVICE_WINDOWS_TOOL_VERSION(5, 2)) { g_autoptr(FuStructTelinkDfuHidPktPayload) st_payload = fu_struct_telink_dfu_hid_pkt_get_payload(st_pkt); /* TODO: find a better method to declare the structure array */ payload_index = i % 3; fu_struct_telink_dfu_hid_long_pkt_set_ota_data_len( st_long_pkt, FU_TELINK_DFU_HID_DEVICE_OTA_DATA_LEN * (payload_index + 1)); if (payload_index == 0) { g_autoptr(FuStructTelinkDfuHidPktPayload) st_default_payload = fu_struct_telink_dfu_hid_pkt_payload_new(); if (!fu_struct_telink_dfu_hid_long_pkt_set_payload_1(st_long_pkt, st_payload, error)) return FALSE; if (!fu_struct_telink_dfu_hid_long_pkt_set_payload_2( st_long_pkt, st_default_payload, error)) return FALSE; if (!fu_struct_telink_dfu_hid_long_pkt_set_payload_3( st_long_pkt, st_default_payload, error)) return FALSE; } else if (payload_index == 1) { if (!fu_struct_telink_dfu_hid_long_pkt_set_payload_2(st_long_pkt, st_payload, error)) return FALSE; } else if (payload_index == 2) { if (!fu_struct_telink_dfu_hid_long_pkt_set_payload_3(st_long_pkt, st_payload, error)) return FALSE; if (!fu_telink_dfu_hid_device_write(self, st_long_pkt->data, st_long_pkt->len, error)) return FALSE; } else { /* should not reach here */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "wrong payload index"); return FALSE; } } else { if (!fu_telink_dfu_hid_device_write(self, st_pkt->data, st_pkt->len, error)) return FALSE; fu_device_sleep(FU_DEVICE(self), 20); } /* update progress */ fu_progress_step_done(progress); } if (self->windows_hid_tool_ver >= FU_TELINK_DEVICE_WINDOWS_TOOL_VERSION(5, 2) && payload_index != 2) { if (!fu_telink_dfu_hid_device_write(self, st_long_pkt->data, st_long_pkt->len, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_telink_dfu_hid_device_ota_start(FuTelinkDfuHidDevice *self, GError **error) { g_autoptr(FuStructTelinkDfuHidPkt) st_pkt = NULL; st_pkt = fu_telink_dfu_hid_device_create_packet(FU_TELINK_DFU_CMD_OTA_START, NULL, 0, error); if (st_pkt == NULL) return FALSE; if (self->windows_hid_tool_ver >= FU_TELINK_DEVICE_WINDOWS_TOOL_VERSION(5, 2)) { g_autoptr(FuStructTelinkDfuHidLongPkt) st_long_pkt = fu_struct_telink_dfu_hid_long_pkt_new(); g_autoptr(FuStructTelinkDfuHidPktPayload) st_payload = fu_struct_telink_dfu_hid_pkt_get_payload(st_pkt); fu_struct_telink_dfu_hid_long_pkt_set_ota_data_len( st_long_pkt, fu_struct_telink_dfu_hid_pkt_get_ota_data_len(st_pkt)); if (!fu_struct_telink_dfu_hid_long_pkt_set_payload_1(st_long_pkt, st_payload, error)) return FALSE; if (!fu_telink_dfu_hid_device_write(self, st_long_pkt->data, st_long_pkt->len, error)) return FALSE; } else { if (!fu_telink_dfu_hid_device_write(self, st_pkt->data, st_pkt->len, error)) return FALSE; } /* success */ fu_device_sleep(FU_DEVICE(self), 20); return TRUE; } static gboolean fu_telink_dfu_hid_device_ota_stop(FuTelinkDfuHidDevice *self, guint number_chunks, GError **error) { guint16 pkt_index = (guint16)(number_chunks)-1; g_autoptr(FuStructTelinkDfuEndCheck) st_end_check = fu_struct_telink_dfu_end_check_new(); g_autoptr(FuStructTelinkDfuHidPkt) st_pkt = NULL; /* last data packet index */ fu_struct_telink_dfu_end_check_set_pkt_index(st_end_check, pkt_index); if (self->windows_hid_tool_ver >= FU_TELINK_DEVICE_WINDOWS_TOOL_VERSION(5, 2)) fu_struct_telink_dfu_end_check_set_inverted_pkt_index(st_end_check, ~pkt_index + 1); else fu_struct_telink_dfu_end_check_set_inverted_pkt_index(st_end_check, ~pkt_index); st_pkt = fu_telink_dfu_hid_device_create_packet(FU_TELINK_DFU_CMD_OTA_END, st_end_check->data, st_end_check->len, error); if (st_pkt == NULL) return FALSE; if (self->windows_hid_tool_ver >= FU_TELINK_DEVICE_WINDOWS_TOOL_VERSION(5, 2)) { g_autoptr(FuStructTelinkDfuHidLongPkt) st_long_pkt = fu_struct_telink_dfu_hid_long_pkt_new(); g_autoptr(FuStructTelinkDfuHidPktPayload) st_payload = fu_struct_telink_dfu_hid_pkt_get_payload(st_pkt); fu_struct_telink_dfu_hid_pkt_payload_set_crc(st_payload, 0xFFFF); fu_struct_telink_dfu_hid_long_pkt_set_ota_data_len(st_long_pkt, 6); if (!fu_struct_telink_dfu_hid_long_pkt_set_payload_1(st_long_pkt, st_payload, error)) return FALSE; if (!fu_telink_dfu_hid_device_write(self, st_long_pkt->data, st_long_pkt->len, error)) return FALSE; } else { if (!fu_telink_dfu_hid_device_write(self, st_pkt->data, st_pkt->len, error)) return FALSE; } /* success */ fu_device_sleep(FU_DEVICE(self), 10000); return TRUE; } static gboolean fu_telink_dfu_hid_device_write_blob(FuTelinkDfuHidDevice *self, GBytes *blob, FuProgress *progress, GError **error) { g_autoptr(FuChunkArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "ota-start"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 70, "ota-data"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 29, "ota-stop"); /* OTA start command */ if (!fu_telink_dfu_hid_device_ota_start(self, error)) return FALSE; fu_progress_step_done(progress); /* OTA firmware data */ chunks = fu_chunk_array_new_from_bytes(blob, FU_TELINK_DFU_HID_DEVICE_START_ADDR, FU_CHUNK_PAGESZ_NONE, FU_STRUCT_TELINK_DFU_HID_PKT_PAYLOAD_SIZE_OTA_DATA); if (!fu_telink_dfu_hid_device_write_blocks(self, chunks, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* OTA stop command */ if (!fu_telink_dfu_hid_device_ota_stop(self, fu_chunk_array_length(chunks), error)) return FALSE; fu_progress_step_done(progress); /* success */ fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_telink_dfu_hid_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuTelinkDfuHidDevice *self = FU_TELINK_DFU_HID_DEVICE(device); g_autoptr(FuArchive) archive = NULL; g_autoptr(GBytes) blob = NULL; g_autoptr(GInputStream) stream = NULL; /* get default image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; archive = fu_archive_new_stream(stream, FU_ARCHIVE_FLAG_IGNORE_PATH, error); if (archive == NULL) return FALSE; blob = fu_archive_lookup_by_fn(archive, "firmware.bin", error); if (blob == NULL) return FALSE; return fu_telink_dfu_hid_device_write_blob(self, blob, progress, error); } static void fu_telink_dfu_hid_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static gboolean fu_telink_dfu_hid_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuTelinkDfuHidDevice *self = FU_TELINK_DFU_HID_DEVICE(device); g_auto(GStrv) ver_split = NULL; guint64 tmp = 0; /* version of supported Telink usb ota tool */ self->windows_hid_tool_ver = 0; if (g_strcmp0(key, "TelinkHidToolVer") == 0) { ver_split = g_strsplit(value, ".", 2); if (!fu_strtoull(ver_split[0], &tmp, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->windows_hid_tool_ver += (guint16)tmp * 100; if (!fu_strtoull(ver_split[1], &tmp, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->windows_hid_tool_ver += (guint16)tmp; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_telink_dfu_hid_device_init(FuTelinkDfuHidDevice *self) { fu_device_set_vendor(FU_DEVICE(self), "Telink"); /* read the ReleaseNumber field of USB descriptor */ fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PAIR); fu_device_set_remove_delay(FU_DEVICE(self), 10000); /* ms */ fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_TELINK_DFU_ARCHIVE); fu_device_add_protocol(FU_DEVICE(self), "com.telink.dfu"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); } static gboolean fu_telink_dfu_hid_device_probe(FuDevice *device, GError **error) { FuTelinkDfuHidDevice *self = FU_TELINK_DFU_HID_DEVICE(device); FuUsbDevice *usb_dev = FU_USB_DEVICE(FU_DEVICE(self)); FuHidDevice *hid_dev = FU_HID_DEVICE(self); g_autoptr(GPtrArray) ifaces = NULL; ifaces = fu_usb_device_get_interfaces(usb_dev, error); if (ifaces == NULL) return FALSE; /* the last interface would always be reserved for OTA upgrade */ fu_hid_device_set_interface(hid_dev, ifaces->len - 1); fu_hid_device_set_ep_addr_in(hid_dev, FU_TELINK_DFU_HID_EP_IN); fu_hid_device_set_ep_addr_out(hid_dev, FU_TELINK_DFU_HID_EP_OUT); /* FuHidDevice->probe */ if (!FU_DEVICE_CLASS(fu_telink_dfu_hid_device_parent_class)->probe(device, error)) return FALSE; return TRUE; } static void fu_telink_dfu_hid_device_class_init(FuTelinkDfuHidDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_telink_dfu_hid_device_probe; device_class->write_firmware = fu_telink_dfu_hid_device_write_firmware; device_class->set_progress = fu_telink_dfu_hid_device_set_progress; device_class->set_quirk_kv = fu_telink_dfu_hid_device_set_quirk_kv; device_class->to_string = fu_telink_dfu_hid_device_to_string; } fwupd-2.0.10/plugins/telink-dfu/fu-telink-dfu-hid-device.h000066400000000000000000000005601501337203100232600ustar00rootroot00000000000000/* * Copyright 2024 Mike Chang * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_TELINK_DFU_HID_DEVICE (fu_telink_dfu_hid_device_get_type()) G_DECLARE_FINAL_TYPE(FuTelinkDfuHidDevice, fu_telink_dfu_hid_device, FU, TELINK_DFU_HID_DEVICE, FuHidDevice) fwupd-2.0.10/plugins/telink-dfu/fu-telink-dfu-plugin.c000066400000000000000000000021611501337203100225470ustar00rootroot00000000000000/* * Copyright 2024 Mike Chang * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-telink-dfu-archive.h" #include "fu-telink-dfu-ble-device.h" #include "fu-telink-dfu-hid-device.h" #include "fu-telink-dfu-plugin.h" struct _FuTelinkDfuPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuTelinkDfuPlugin, fu_telink_dfu_plugin, FU_TYPE_PLUGIN) static void fu_telink_dfu_plugin_init(FuTelinkDfuPlugin *self) { } static void fu_telink_dfu_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "TelinkHidToolVer"); fu_plugin_add_device_gtype(plugin, FU_TYPE_TELINK_DFU_HID_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_TELINK_DFU_BLE_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_TELINK_DFU_ARCHIVE); fu_plugin_add_udev_subsystem(plugin, "hidraw"); } static void fu_telink_dfu_plugin_class_init(FuTelinkDfuPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_telink_dfu_plugin_constructed; } fwupd-2.0.10/plugins/telink-dfu/fu-telink-dfu-plugin.h000066400000000000000000000003721501337203100225560ustar00rootroot00000000000000/* * Copyright 2024 Mike Chang * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuTelinkDfuPlugin, fu_telink_dfu_plugin, FU, TELINK_DFU_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/telink-dfu/fu-telink-dfu.rs000066400000000000000000000024371501337203100214630ustar00rootroot00000000000000// Copyright 2024 Mike Chang // SPDX-License-Identifier: LGPL-2.1-or-later enum FuTelinkDfuCmd { OtaFwVersion = 0xff00, OtaStart = 0xff01, OtaEnd = 0xff02, OtaStartReq = 0xff03, OtaStartRsp = 0xff04, OtaTest = 0xff05, OtaTestRsp = 0xff06, OtaError = 0xff07, } #[derive(New, Getters, Default)] #[repr(C, packed)] struct FuStructTelinkDfuEndCheck { pkt_index: u16le, inverted_pkt_index: u16le, } #[derive(New, Getters, Default)] #[repr(C, packed)] struct FuStructTelinkDfuBlePkt { preamble: u16le, payload: [u8; 16] = 0xFF, crc: u16le, } #[derive(New, Getters, Default)] #[repr(C, packed)] struct FuStructTelinkDfuHidPktPayload { ota_cmd: u16le = 0xFFFF, ota_data: [u8; 16] = 0xFF, crc: u16le = 0xFFFF, } #[derive(New, Getters, Default)] #[repr(C, packed)] struct FuStructTelinkDfuHidPkt { op_code: u8 = 0x02, ota_data_len: u16le, payload: FuStructTelinkDfuHidPktPayload, } #[derive(New, Getters, Default)] #[repr(C, packed)] struct FuStructTelinkDfuHidLongPkt { op_code: u8 = 0x02, ota_data_len: u16le, payload_1: FuStructTelinkDfuHidPktPayload, payload_2: FuStructTelinkDfuHidPktPayload, payload_3: FuStructTelinkDfuHidPktPayload, } fwupd-2.0.10/plugins/telink-dfu/meson.build000066400000000000000000000011321501337203100205740ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginTelinkDfu"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('telink-dfu.quirk') plugin_builtins += static_library('fu_plugin_telink_dfu', rustgen.process('fu-telink-dfu.rs'), sources: [ 'fu-telink-dfu-archive.c', 'fu-telink-dfu-common.c', 'fu-telink-dfu-ble-device.c', 'fu-telink-dfu-hid-device.c', 'fu-telink-dfu-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files( 'tests/telink-ryder-dongle.json', ) fwupd-2.0.10/plugins/telink-dfu/telink-dfu.quirk000066400000000000000000000010231501337203100215500ustar00rootroot00000000000000# 8266 mouse beta test [BLUETOOTH\VID_248A&PID_8266] Plugin = telink_dfu GType = FuTelinkDfuBleDevice Name = BT Mouse Prototype # 8208 Dual Keyboard [BLUETOOTH\VID_248A&PID_8208] Plugin = telink_dfu GType = FuTelinkDfuBleDevice Name = BT Keyboard Prototype # 8272 2.4g Dongle [USB\VID_248A&PID_881C] Plugin = telink_dfu GType = FuTelinkDfuHidDevice Name = Dongle Prototype TelinkHidToolVer = 1.0 # Ryder dongle [USB\VID_0461&PID_4F27] Plugin = telink_dfu GType = FuTelinkDfuHidDevice Name = Ryder dongle TelinkHidToolVer = 5.02 fwupd-2.0.10/plugins/telink-dfu/tests/000077500000000000000000000000001501337203100175775ustar00rootroot00000000000000fwupd-2.0.10/plugins/telink-dfu/tests/telink-ryder-dongle.json000066400000000000000000000007501501337203100243530ustar00rootroot00000000000000{ "name": "Telink Ryder Dongle", "interactive": false, "steps": [ { "url": "35ee97796aae0990cb403ffb0d6bf123e75c67ed9a3d096045580d1f69197271-primax-ryder_dongle-v1.25.cab", "emulation-url": "5e9f7e8686c1a9bd7735745daf2c9d0fd1413cd1e7d3ccb651487218be77cf18-ryder-dongle-v1.25-emulation.zip", "components": [ { "version": "1.25", "guids": [ "3db7a7d4-b24f-52e0-ab17-a5b56d5afc7f" ] } ] } ] } fwupd-2.0.10/plugins/test/000077500000000000000000000000001501337203100153525ustar00rootroot00000000000000fwupd-2.0.10/plugins/test/README.md000066400000000000000000000011501501337203100166260ustar00rootroot00000000000000--- title: Plugin: Test --- ## Introduction This plugin is used when running the self tests in the fwupd project. ## GUID Generation The devices created by this plugin use hardcoded GUIDs that do not correspond to any kind of DeviceInstanceId values. In other cases devices use the standard BLE DeviceInstanceId values, e.g. * `USB\VID_2DC8&PID_AB11` ## Vendor ID Security The fake device is only for local testing and thus requires no vendor ID set. ## External Interface Access This plugin requires no extra access. ## Version Considerations This plugin has been available since fwupd version `0.8.0`. fwupd-2.0.10/plugins/test/fu-test-ble-device.c000066400000000000000000000013361501337203100211050ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-test-ble-device.h" struct _FuTestBleDevice { FuBluezDevice parent_instance; }; G_DEFINE_TYPE(FuTestBleDevice, fu_test_ble_device, FU_TYPE_BLUEZ_DEVICE) static void fu_test_ble_device_init(FuTestBleDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "org.test.testble"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_INSTANCE_ID_REV); } static void fu_test_ble_device_class_init(FuTestBleDeviceClass *klass) { } fwupd-2.0.10/plugins/test/fu-test-ble-device.h000066400000000000000000000004661501337203100211150ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_TEST_BLE_DEVICE (fu_test_ble_device_get_type()) G_DECLARE_FINAL_TYPE(FuTestBleDevice, fu_test_ble_device, FU, TEST_BLE_DEVICE, FuBluezDevice) fwupd-2.0.10/plugins/test/fu-test-ble-plugin.c000066400000000000000000000014451501337203100211450ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-test-ble-device.h" #include "fu-test-ble-plugin.h" struct _FuTestBlePlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuTestBlePlugin, fu_test_ble_plugin, FU_TYPE_PLUGIN) static void fu_test_ble_plugin_init(FuTestBlePlugin *self) { fu_plugin_add_flag(FU_PLUGIN(self), FWUPD_PLUGIN_FLAG_TEST_ONLY); } static void fu_test_ble_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_TEST_BLE_DEVICE); } static void fu_test_ble_plugin_class_init(FuTestBlePluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_test_ble_plugin_constructed; } fwupd-2.0.10/plugins/test/fu-test-ble-plugin.h000066400000000000000000000003611501337203100211460ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuTestBlePlugin, fu_test_ble_plugin, FU, TEST_BLE_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/test/fu-test-plugin.c000066400000000000000000000352241501337203100204070ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-test-plugin.h" struct _FuTestPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuTestPlugin, fu_test_plugin, FU_TYPE_PLUGIN) static gboolean fu_test_plugin_coldplug(FuPlugin *plugin, FuProgress *progress, GError **error) { FuContext *ctx = fu_plugin_get_context(plugin); g_autoptr(FuDevice) device = NULL; device = fu_device_new(ctx); fu_device_set_id(device, "FakeDevice"); fu_device_add_instance_id(device, "b585990a-003e-5270-89d5-3705a17f9a43"); fu_device_set_name(device, "Integrated_Webcam(TM)"); fu_device_add_icon(device, FU_DEVICE_ICON_WEB_CAMERA); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_CAN_EMULATION_TAG); fu_device_add_request_flag(device, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); fu_device_add_protocol(device, "com.acme.test"); fu_device_set_summary(device, "Fake webcam"); fu_device_set_vendor(device, "ACME Corp."); fu_device_build_vendor_id_u16(device, "USB", 0x046D); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version_bootloader(device, "0.1.2"); fu_device_set_version(device, "1.2.2"); fu_device_set_version_lowest(device, "1.2.0"); if (fu_plugin_get_config_value_boolean(plugin, "RegistrationSupported")) { fu_plugin_device_register(plugin, device); if (fu_device_get_metadata(device, "BestDevice") == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "Device not set by another plugin"); return FALSE; } } fu_plugin_device_add(plugin, device); if (fu_plugin_get_config_value_boolean(plugin, "CompositeChild")) { g_autoptr(FuDevice) child1 = NULL; g_autoptr(FuDevice) child2 = NULL; child1 = fu_device_new(ctx); fu_device_build_vendor_id_u16(child1, "USB", 0xFFFF); fu_device_add_protocol(child1, "com.acme"); fu_device_set_physical_id(child1, "fake"); fu_device_set_logical_id(child1, "child1"); fu_device_add_instance_id(child1, "7fddead7-12b5-4fb9-9fa0-6d30305df755"); fu_device_set_name(child1, "Module1"); fu_device_set_version_format(child1, FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_version(child1, "1"); fu_device_add_parent_guid(child1, "b585990a-003e-5270-89d5-3705a17f9a43"); fu_device_add_flag(child1, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(child1, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(child1, FU_DEVICE_PRIVATE_FLAG_INSTALL_PARENT_FIRST); fu_plugin_device_add(plugin, child1); child2 = fu_device_new(ctx); fu_device_build_vendor_id_u16(child2, "USB", 0xFFFF); fu_device_add_protocol(child2, "com.acme"); fu_device_set_physical_id(child2, "fake"); fu_device_set_logical_id(child2, "child2"); fu_device_add_instance_id(child2, "b8fe6b45-8702-4bcd-8120-ef236caac76f"); fu_device_set_name(child2, "Module2"); fu_device_set_version_format(child2, FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_version(child2, "10"); fu_device_add_parent_guid(child2, "b585990a-003e-5270-89d5-3705a17f9a43"); fu_device_add_flag(child2, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(child2, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(child2, FU_DEVICE_PRIVATE_FLAG_INSTALL_PARENT_FIRST); fu_plugin_device_add(plugin, child2); } return TRUE; } static gboolean fu_test_plugin_modify_config(FuPlugin *plugin, const gchar *key, const gchar *value, GError **error) { const gchar *keys[] = {"AnotherWriteRequired", "CompositeChild", "DecompressDelay", "NeedsActivation", "NeedsReboot", "RegistrationSupported", "RequestDelay", "RequestSupported", "VerifyDelay", "WriteDelay", "WriteSupported", NULL}; if (!g_strv_contains(keys, key)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "config key %s not supported", key); return FALSE; } return fu_plugin_set_config_value(plugin, key, value, error); } static void fu_test_plugin_device_registered(FuPlugin *plugin, FuDevice *device) { fu_device_set_metadata(device, "BestDevice", "/dev/urandom"); } static gboolean fu_test_plugin_verify(FuPlugin *plugin, FuDevice *device, FuProgress *progress, FuPluginVerifyFlags flags, GError **error) { if (g_strcmp0(fu_device_get_version(device), "1.2.2") == 0) { fu_device_add_checksum(device, "90d0ad436d21e0687998cd2127b2411135e1f730"); fu_device_add_checksum( device, "921631916a60b295605dbae6a0309f9b64e2401b3de8e8506e109fc82c586e3a"); return TRUE; } if (g_strcmp0(fu_device_get_version(device), "1.2.3") == 0) { fu_device_add_checksum(device, "7998cd212721e068b2411135e1f90d0ad436d730"); fu_device_add_checksum( device, "dbae6a0309b3de8e850921631916a60b2956056e109fc82c586e3f9b64e2401a"); return TRUE; } if (g_strcmp0(fu_device_get_version(device), "1.2.4") == 0) { fu_device_add_checksum(device, "2b8546ba805ad10bf8a2e5ad539d53f303812ba5"); fu_device_add_checksum( device, "b546c241029ce4e16c99eb6bfd77b86e4490aa3826ba71b8a4114e96a2d69bcd"); return TRUE; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no checksum for %s", fu_device_get_version(device)); return FALSE; } static gchar * fu_test_plugin_get_version(GBytes *blob_fw) { gsize bufsz = 0; const gchar *buf = g_bytes_get_data(blob_fw, &bufsz); guint64 val = 0; g_autoptr(GError) error_local = NULL; g_autofree gchar *str_safe = fu_strsafe(buf, bufsz); if (str_safe == NULL) return NULL; if (!fu_strtoull(str_safe, &val, 0, G_MAXSIZE, FU_INTEGER_BASE_AUTO, &error_local)) { g_debug("invalid version specified: %s", error_local->message); return NULL; } if (val == 0x0) return NULL; return fu_version_from_uint32(val, FWUPD_VERSION_FORMAT_TRIPLET); } static gboolean fu_test_plugin_write_firmware(FuPlugin *plugin, FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { g_autofree gchar *decompress_delay_str = NULL; g_autofree gchar *write_delay_str = NULL; g_autofree gchar *verify_delay_str = NULL; guint64 delay_decompress_ms = 0; guint64 delay_write_ms = 0; guint64 delay_verify_ms = 0; guint64 delay_request_ms = 0; if (!fu_plugin_get_config_value_boolean(plugin, "WriteSupported")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device was not in supported mode"); return FALSE; } fu_progress_set_status(progress, FWUPD_STATUS_DECOMPRESSING); decompress_delay_str = fu_plugin_get_config_value(plugin, "DecompressDelay"); if (decompress_delay_str != NULL) { if (!fu_strtoull(decompress_delay_str, &delay_decompress_ms, 0, 10000, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "failed to parse DecompressDelay: "); return FALSE; } } for (guint i = 0; i <= delay_decompress_ms; i++) { fu_device_sleep(device, 1); fu_progress_set_percentage_full(progress, i, delay_decompress_ms); } /* send an interactive request, and wait some time */ if (fu_plugin_get_config_value_boolean(plugin, "RequestSupported")) { g_autofree gchar *request_delay_str = NULL; g_autoptr(FwupdRequest) request = fwupd_request_new(); fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_IMMEDIATE); fwupd_request_set_id(request, FWUPD_REQUEST_ID_REMOVE_REPLUG); fwupd_request_add_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); fwupd_request_set_message(request, "Please pretend to remove the device you cannot see or " "touch and please re-insert it."); if (!fu_device_emit_request(device, request, progress, error)) return FALSE; request_delay_str = fu_plugin_get_config_value(plugin, "RequestDelay"); if (request_delay_str != NULL) { if (!fu_strtoull(request_delay_str, &delay_request_ms, 0, 10000, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "failed to parse RequestDelay: "); return FALSE; } } fu_device_sleep(device, delay_request_ms); } fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); write_delay_str = fu_plugin_get_config_value(plugin, "WriteDelay"); if (write_delay_str != NULL) { if (!fu_strtoull(write_delay_str, &delay_write_ms, 0, 10000, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "failed to parse WriteDelay: "); return FALSE; } } for (guint i = 0; i <= delay_write_ms; i++) { fu_device_sleep(device, 1); fu_progress_set_percentage_full(progress, i, delay_write_ms); } fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_VERIFY); verify_delay_str = fu_plugin_get_config_value(plugin, "VerifyDelay"); if (verify_delay_str != NULL) { if (!fu_strtoull(verify_delay_str, &delay_verify_ms, 0, 10000, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "failed to parse VerifyDelay: "); return FALSE; } } for (guint i = 0; i <= delay_verify_ms; i++) { fu_device_sleep(device, 1); fu_progress_set_percentage_full(progress, i, delay_verify_ms); } /* composite test, upgrade composite devices */ if (fu_plugin_get_config_value_boolean(plugin, "CompositeChild")) { fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_PLAIN); if (g_strcmp0(fu_device_get_logical_id(device), "child1") == 0) { fu_device_set_version(device, "2"); return TRUE; } if (g_strcmp0(fu_device_get_logical_id(device), "child2") == 0) { fu_device_set_version(device, "11"); return TRUE; } } /* upgrade, or downgrade */ if (fu_plugin_get_config_value_boolean(plugin, "NeedsActivation")) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION); } else if (fu_plugin_get_config_value_boolean(plugin, "NeedsReboot")) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT); } else { g_autofree gchar *ver = NULL; g_autoptr(GBytes) blob_fw = NULL; blob_fw = fu_firmware_get_bytes(firmware, error); if (blob_fw == NULL) return FALSE; ver = fu_test_plugin_get_version(blob_fw); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); if (ver != NULL) { fu_device_set_version(device, ver); } else { if (flags & FWUPD_INSTALL_FLAG_ALLOW_OLDER) { fu_device_set_version(device, "1.2.2"); } else { fu_device_set_version(device, "1.2.3"); } } } /* do this all over again */ if (fu_plugin_get_config_value_boolean(plugin, "AnotherWriteRequired") && !fu_device_get_metadata_boolean(device, "DoneAnotherWriteRequired")) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED); fu_device_set_metadata_boolean(device, "DoneAnotherWriteRequired", TRUE); } /* do this all over again */ if (fu_plugin_get_config_value_boolean(plugin, "InstallLoopRestart") && !fu_device_get_metadata_boolean(device, "DoneInstallLoopRestart")) { fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_INSTALL_LOOP_RESTART); fu_device_set_metadata_boolean(device, "DoneInstallLoopRestart", TRUE); } /* for the self tests only */ fu_device_set_metadata_integer(device, "nr-update", fu_device_get_metadata_integer(device, "nr-update") + 1); return TRUE; } static gboolean fu_test_plugin_activate(FuPlugin *plugin, FuDevice *device, FuProgress *process, GError **error) { fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "1.2.3"); return TRUE; } static gboolean fu_test_plugin_get_results(FuPlugin *plugin, FuDevice *device, GError **error) { fu_device_set_update_state(device, FWUPD_UPDATE_STATE_SUCCESS); return TRUE; } static gboolean fu_test_plugin_composite_prepare(FuPlugin *plugin, GPtrArray *devices, GError **error) { if (fu_plugin_get_config_value_boolean(plugin, "CompositeChild")) { for (guint i = 0; i < devices->len; i++) { FuDevice *device = g_ptr_array_index(devices, i); fu_device_set_metadata(device, "frimbulator", "1"); } } return TRUE; } static gboolean fu_test_plugin_composite_cleanup(FuPlugin *plugin, GPtrArray *devices, GError **error) { if (fu_plugin_get_config_value_boolean(plugin, "CompositeChild")) { for (guint i = 0; i < devices->len; i++) { FuDevice *device = g_ptr_array_index(devices, i); fu_device_set_metadata(device, "frombulator", "1"); } } return TRUE; } static gboolean fu_test_plugin_attach(FuPlugin *plugin, FuDevice *device, FuProgress *progress, GError **error) { fu_device_set_metadata_integer(device, "nr-attach", fu_device_get_metadata_integer(device, "nr-attach") + 1); return TRUE; } static void fu_test_plugin_init(FuTestPlugin *self) { fu_plugin_add_flag(FU_PLUGIN(self), FWUPD_PLUGIN_FLAG_TEST_ONLY); } static void fu_test_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_set_config_default(plugin, "AnotherWriteRequired", "false"); fu_plugin_set_config_default(plugin, "InstallLoopRestart", "false"); fu_plugin_set_config_default(plugin, "CompositeChild", "false"); fu_plugin_set_config_default(plugin, "DecompressDelay", "0"); fu_plugin_set_config_default(plugin, "NeedsActivation", "false"); fu_plugin_set_config_default(plugin, "NeedsReboot", "false"); fu_plugin_set_config_default(plugin, "RegistrationSupported", "false"); fu_plugin_set_config_default(plugin, "RequestDelay", "10"); /* ms */ fu_plugin_set_config_default(plugin, "RequestSupported", "false"); fu_plugin_set_config_default(plugin, "VerifyDelay", "0"); fu_plugin_set_config_default(plugin, "WriteDelay", "0"); fu_plugin_set_config_default(plugin, "WriteSupported", "true"); } static void fu_test_plugin_finalize(GObject *obj) { g_debug("destroy"); G_OBJECT_CLASS(fu_test_plugin_parent_class)->finalize(obj); } static void fu_test_plugin_class_init(FuTestPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_test_plugin_finalize; plugin_class->constructed = fu_test_plugin_constructed; plugin_class->composite_cleanup = fu_test_plugin_composite_cleanup; plugin_class->composite_prepare = fu_test_plugin_composite_prepare; plugin_class->get_results = fu_test_plugin_get_results; plugin_class->activate = fu_test_plugin_activate; plugin_class->write_firmware = fu_test_plugin_write_firmware; plugin_class->verify = fu_test_plugin_verify; plugin_class->attach = fu_test_plugin_attach; plugin_class->coldplug = fu_test_plugin_coldplug; plugin_class->device_registered = fu_test_plugin_device_registered; plugin_class->modify_config = fu_test_plugin_modify_config; } fwupd-2.0.10/plugins/test/fu-test-plugin.h000066400000000000000000000003461501337203100204110ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuTestPlugin, fu_test_plugin, FU, TEST_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/test/meson.build000066400000000000000000000012111501337203100175070ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginTest"'] plugins += {meson.current_source_dir().split('/')[-1]: true} if bluez.allowed() plugin_quirks += files('test-ble.quirk') endif plugin_builtins += static_library('fu_plugin_test', sources: [ 'fu-test-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) if bluez.allowed() plugin_builtins += static_library('fu_plugin_test_ble', sources: [ 'fu-test-ble-plugin.c', 'fu-test-ble-device.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/test/test-ble.quirk000066400000000000000000000001621501337203100201450ustar00rootroot00000000000000[BLUETOOTH\VID_0461&PID_4EEE&REV_0001] Plugin = test_ble [BLUETOOTH\VID_0461&PID_4EEF&REV_0001] Plugin = test_ble fwupd-2.0.10/plugins/thelio-io/000077500000000000000000000000001501337203100162645ustar00rootroot00000000000000fwupd-2.0.10/plugins/thelio-io/README.md000066400000000000000000000022421501337203100175430ustar00rootroot00000000000000--- title: Plugin: Thelio IO --- ## Introduction This plugin is used to detach the Thelio IO device to DFU mode. To switch to this mode `1` has to be written to the `bootloader` file in sysfs. ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_1209&PID_1776&REV_0001` ## Update Behavior The device usually presents in runtime mode, but on detach re-enumerates with a different USB VID and PID in DFU mode. The device is then handled by the `dfu` plugin. On DFU attach the device again re-enumerates back to the runtime mode. For this reason the `REPLUG_MATCH_GUID` internal device flag is used so that the bootloader and runtime modes are treated as the same device. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x1209` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.3.2`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Jeremy Soller: @jackpot51 fwupd-2.0.10/plugins/thelio-io/fu-thelio-io-device.c000066400000000000000000000075611501337203100221770ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * Copyright 2019 Jeremy Soller * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-thelio-io-device.h" struct _FuThelioIoDevice { FuUsbDevice parent_instance; }; G_DEFINE_TYPE(FuThelioIoDevice, fu_thelio_io_device, FU_TYPE_USB_DEVICE) static gboolean fu_thelio_io_device_probe(FuDevice *device, GError **error) { const gchar *devpath; gsize bufsz = 0; g_autofree gchar *fn = NULL; g_autofree gchar *buf = NULL; g_autoptr(GError) error_local = NULL; /* this is the atmel bootloader */ fu_device_add_instance_id_full(device, "USB\\VID_03EB&PID_2FF4", FU_DEVICE_INSTANCE_FLAG_COUNTERPART); devpath = fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device)); if (G_UNLIKELY(devpath == NULL)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Could not determine sysfs path for device"); return FALSE; } /* pre-1.0.0 firmware versions do not implement this */ fn = g_build_filename(devpath, "revision", NULL); if (!g_file_get_contents(fn, &buf, &bufsz, &error_local)) { if (g_error_matches(error_local, G_FILE_ERROR, G_FILE_ERROR_FAILED)) { g_debug("FW revision unimplemented: %s", error_local->message); fu_device_set_version(device, "0.0.0"); } else { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } else { g_autofree gchar *version = fu_strsafe((const gchar *)buf, bufsz); fu_device_set_version(device, version); } return TRUE; } static gboolean fu_thelio_io_device_detach(FuDevice *device, FuProgress *progress, GError **error) { const gchar *devpath; g_autofree gchar *fn = NULL; g_autoptr(FuIOChannel) io_channel = NULL; const guint8 buf[] = {'1', '\n'}; devpath = fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device)); if (G_UNLIKELY(devpath == NULL)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Could not determine sysfs path for device"); return FALSE; } fn = g_build_filename(devpath, "bootloader", NULL); io_channel = fu_io_channel_new_file(fn, FU_IO_CHANNEL_OPEN_FLAG_WRITE, error); if (io_channel == NULL) return FALSE; if (!fu_io_channel_write_raw(io_channel, buf, sizeof(buf), 500, FU_IO_CHANNEL_FLAG_SINGLE_SHOT, error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static void fu_thelio_io_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static void fu_thelio_io_device_init(FuThelioIoDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_INSTANCE_ID_REV); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_add_protocol(FU_DEVICE(self), "org.usb.dfu"); } static void fu_thelio_io_device_class_init(FuThelioIoDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_thelio_io_device_probe; device_class->detach = fu_thelio_io_device_detach; device_class->set_progress = fu_thelio_io_device_set_progress; } fwupd-2.0.10/plugins/thelio-io/fu-thelio-io-device.h000066400000000000000000000005571501337203100222020ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * Copyright 2019 Jeremy Soller * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_THELIO_IO_DEVICE (fu_thelio_io_device_get_type()) G_DECLARE_FINAL_TYPE(FuThelioIoDevice, fu_thelio_io_device, FU, THELIO_IO_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/thelio-io/fu-thelio-io-plugin.c000066400000000000000000000014441501337203100222300ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * Copyright 2019 Jeremy Soller * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-thelio-io-device.h" #include "fu-thelio-io-plugin.h" struct _FuThelioIoPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuThelioIoPlugin, fu_thelio_io_plugin, FU_TYPE_PLUGIN) static void fu_thelio_io_plugin_init(FuThelioIoPlugin *self) { } static void fu_thelio_io_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_THELIO_IO_DEVICE); } static void fu_thelio_io_plugin_class_init(FuThelioIoPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_thelio_io_plugin_constructed; } fwupd-2.0.10/plugins/thelio-io/fu-thelio-io-plugin.h000066400000000000000000000003641501337203100222350ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuThelioIoPlugin, fu_thelio_io_plugin, FU, THELIO_IO_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/thelio-io/meson.build000066400000000000000000000006661501337203100204360ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginThelioIo"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('thelio-io.quirk') plugin_builtins += static_library('fu_plugin_thelio_io', sources: [ 'fu-thelio-io-plugin.c', 'fu-thelio-io-device.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/thelio-io/thelio-io.quirk000066400000000000000000000001111501337203100212230ustar00rootroot00000000000000# System76 Thelio IO [USB\VID_1209&PID_1776&REV_0001] Plugin = thelio_io fwupd-2.0.10/plugins/thunderbolt/000077500000000000000000000000001501337203100167255ustar00rootroot00000000000000fwupd-2.0.10/plugins/thunderbolt/README.md000066400000000000000000000135351501337203100202130ustar00rootroot00000000000000--- title: Plugin: Thunderbolt --- ## Introduction Thunderboltâ„¢ is the brand name of a hardware interface developed by Intel that allows the connection of external peripherals to a computer. Versions 1 and 2 use the same connector as Mini DisplayPort (MDP), whereas version 3 uses USB Type-C. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in an unspecified binary file format, with vendor specific header. This plugin supports the following protocol ID: * `com.intel.thunderbolt` ## GUID Generation These devices use a custom GUID generation scheme. When the device is in "safe mode" the GUID is hardcoded using: * `TBT-safemode` ... and when in runtime mode the GUID is: * `TBT-$(vid)$(pid)-native` when native, and `TBT-$(vid)$(pid)` otherwise. Additionally for host system thunderbolt controllers another GUID is added containing the domain designation of the controller. This is intended to be used for systems with multiple host controllers to disambiguate between controllers. * `TBT-$(vid)$(pid)-native-controller$(num)` For retimers the only GUID created is as follows: * `TBT-$(vid)$(pid)-retimer$index` The retimer index is oriented around the physical connection within the machine. It is important as multiple controllers may otherwise identify identically. ## Update Behavior For most devices the firmware is written to the device at runtime and the update is applied immediately. Once complete the controller may reboot which may cause all connected USB and TBT devices to be reenumerated. For some devices and circumstances (such as the Dell WD19 with a new enough kernel) the device will remain functional for the duration of the update. The update will complete on unplug. If a user sets `DelayedActivation` configuration option then the update will be staged but not completed until `activate` is separately called such as at logout or shutdown. ### USB4 Retimer To update the retimer userspace need to set the port offline (`echo 1 > offline`), and then rescan it (`echo 1 > rescan`). This will cause the retimer to be visible to the OS, it will be added to sysfs and thus a fwupd `FuThunderboltRetimer` device will be created. If the system is on AC power, and the controller is offlined, the system will think that is has been unplugged and so the fwupd plugin should inhibit the system power change until the controller is onlined with `echo 0 > offline`. When the controller is offlined (perhaps after enumeration), the retimer device will disappear from sysfs which is why the device flag `no-auto-remove` is required. A further complication is that userspace needs to re-start the retimer to get the new version number, for example: * `1->offline` * `->write_firmware()` * `0->offline ()` * Retimer gets removed, which fwupd should ignore, due to no-auto-remove * `1->offline` * `1->rescan` * Retimer gets added * `1->online` * Retimer gets removed, which fwupd should ignore again ## Vendor ID Security The vendor ID is set from the udev vendor, for example set to `TBT:0x$(vid)` ## Runtime Power Management Thunderbolt controllers are slightly unusual in that they power down completely when no thunderbolt devices are detected. This poses a problem for fwupd as it can't coldplug devices to see if there are firmware updates available, and also can't ensure the controller stays awake during a firmware upgrade. On Dell hardware the `Thunderbolt::CanForcePower` metadata value is set as the system can force the thunderbolt controller on during coldplug or during the firmware update process. This is typically done calling a SMI or ACPI method which asserts the GPIO for the duration of the request. On non-Dell hardware you will have to insert a Thunderbolt device (e.g. a dock) into the laptop to be able to update the controller itself. ## Safe Mode Thunderbolt hardware is also slightly unusual in that it goes into "safe mode" whenever it encounters a critical firmware error, for instance if an update failed to be completed. In this safe mode you cannot query the controller vendor or model and therefore the thunderbolt plugin cannot add the correct GUID used to match it to the correct firmware. In this case the metadata value `Thunderbolt::IsSafeMode` is set which would allow a different plugin to add the correct GUID based on some out-of-band device discovery. At the moment this only happens on Dell hardware. ## GUID generation for LVFS The GUID for the controller, which must appear in the metadata when uploading an NVM to LVFS, can be generated by a tool like `appstream-util` (with `generate-guid` command) or by Python (with `uuid.uuid5(uuid.NAMESPACE_DNS, 'string')`). The format of the string used as input is "TBT-vvvvdddd", where vvvvv is the vendor ID and dddd is the device ID, both in hex, as appear in the controller's DROM and exposed in the relevant sysfs attributes. If the controller is in native enumeration mode, the string "-native" is added at the end so the format is "TBT-vvvvdddd-native". ## Quirk Use This plugin uses the following plugin-specific quirks: ### `Flags=retimer-offline-mode` Use offline mode interface to update retimers. Since: 1.9.1 ### `Flags=force-enumeration` Forces composite device components to be enumerated. ## External Interface Access This plugin requires read/write access to `/sys/bus/thunderbolt`. ## Version Considerations This plugin has been available since fwupd version `0.8.0`. ## Data Flow ```mermaid flowchart LR subgraph Thunderbolt Controller controller(TBT/USB4) TBT_SPI[(SPI)] end subgraph Kernel thunderbolt(Thunderbolt\ndriver) end subgraph fwupd Process fwupdengine(FuEngine) tbt_plugin(Thunderbolt\nPlugin) end fwupdengine-->tbt_plugin tbt_plugin<--sysfs-->thunderbolt thunderbolt<--mailbox-->controller controller---TBT_SPI thunderbolt~~~controller ``` fwupd-2.0.10/plugins/thunderbolt/fu-self-test.c000066400000000000000000001033231501337203100214110ustar00rootroot00000000000000/* * Copyright 2017 Christian J. Kellner * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include "fu-context-private.h" #include "fu-plugin-private.h" #include "fu-thunderbolt-plugin.h" #include "fu-udev-device-private.h" static gchar * udev_mock_add_nvmem(UMockdevTestbed *bed, gboolean active, const char *parent, int id) { g_autofree gchar *name = NULL; gchar *path; name = g_strdup_printf("%s%d", active ? "nvm_active" : "nvm_non_active", id); path = umockdev_testbed_add_device(bed, "nvmem", name, parent, "nvmem", "", NULL, NULL); g_assert_nonnull(path); return path; } static gchar * udev_mock_add_usb4_port(UMockdevTestbed *bed, int id) { g_autofree gchar *name = g_strdup_printf("usb4_port%d", id); gchar *path = umockdev_testbed_add_device(bed, "thunderbolt", name, NULL, "security", "secure", NULL, "DEVTYPE", "thunderbolt_usb4_port", NULL); g_assert_nonnull(path); return path; } typedef struct MockDevice MockDevice; struct MockDevice { const char *name; /* sysfs: device_name */ const char *id; /* sysfs: device */ const char *nvm_version; const char *nvm_parsed_version; int delay_ms; int domain_id; struct MockDevice *children; /* optionally filled out */ const char *uuid; }; typedef struct MockTree MockTree; struct MockTree { const MockDevice *device; MockTree *parent; GPtrArray *children; gchar *sysfs_parent; int sysfs_id; int sysfs_nvm_id; gchar *uuid; UMockdevTestbed *bed; gchar *path; gchar *nvm_non_active; gchar *nvm_active; guint nvm_authenticate; gchar *nvm_version; FuDevice *fu_device; }; static MockTree * mock_tree_new(MockTree *parent, MockDevice *device, int *id) { MockTree *node = g_slice_new0(MockTree); int current_id = (*id)++; node->device = device; node->sysfs_id = current_id; node->sysfs_nvm_id = current_id; node->parent = parent; if (device->uuid) node->uuid = g_strdup(device->uuid); else node->uuid = g_uuid_string_random(); node->nvm_version = g_strdup(device->nvm_version); return node; } static void mock_tree_free(MockTree *tree) { for (guint i = 0; i < tree->children->len; i++) { MockTree *child = g_ptr_array_index(tree->children, i); mock_tree_free(child); } g_ptr_array_free(tree->children, TRUE); if (tree->fu_device) g_object_unref(tree->fu_device); g_free(tree->uuid); if (tree->bed != NULL) { if (tree->nvm_active) { umockdev_testbed_uevent(tree->bed, tree->nvm_active, "remove"); umockdev_testbed_remove_device(tree->bed, tree->nvm_active); } if (tree->nvm_non_active) { umockdev_testbed_uevent(tree->bed, tree->nvm_non_active, "remove"); umockdev_testbed_remove_device(tree->bed, tree->nvm_non_active); } if (tree->path) { umockdev_testbed_uevent(tree->bed, tree->path, "remove"); umockdev_testbed_remove_device(tree->bed, tree->path); } g_object_unref(tree->bed); } g_free(tree->nvm_version); g_free(tree->nvm_active); g_free(tree->nvm_non_active); g_free(tree->path); g_free(tree->sysfs_parent); g_slice_free(MockTree, tree); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTOPTR_CLEANUP_FUNC(MockTree, mock_tree_free); #pragma clang diagnostic pop static GPtrArray * mock_tree_init_children(MockTree *node, int *id) { GPtrArray *children = g_ptr_array_new(); MockDevice *iter; for (iter = node->device->children; iter && iter->name; iter++) { MockTree *child = mock_tree_new(node, iter, id); g_ptr_array_add(children, child); child->children = mock_tree_init_children(child, id); } return children; } static MockTree * mock_tree_init(MockDevice *device) { MockTree *tree; int devices = 0; tree = mock_tree_new(NULL, device, &devices); tree->children = mock_tree_init_children(tree, &devices); return tree; } static void mock_tree_dump(const MockTree *node, int level) { if (node->path) { g_debug("%*s * %s [%s] at %s", level, " ", node->device->name, node->uuid, node->path); g_debug("%*s non-active nvmem at %s", level, " ", node->nvm_non_active); g_debug("%*s active nvmem at %s", level, " ", node->nvm_active); } else { g_debug("%*s * %s [%s] %d", level, " ", node->device->name, node->uuid, node->sysfs_id); } for (guint i = 0; i < node->children->len; i++) { const MockTree *child = g_ptr_array_index(node->children, i); mock_tree_dump(child, level + 2); } } static void mock_tree_firmware_verify(const MockTree *node, GBytes *data) { g_autoptr(GFile) nvm_device = NULL; g_autoptr(GFile) nvm = NULL; g_autoptr(GInputStream) is = NULL; g_autoptr(GChecksum) chk = NULL; g_autoptr(GError) error = NULL; g_autofree gchar *sum_data = NULL; const gchar *sum_disk = NULL; gsize s; sum_data = g_compute_checksum_for_bytes(G_CHECKSUM_SHA1, data); chk = g_checksum_new(G_CHECKSUM_SHA1); g_assert_nonnull(node); g_assert_nonnull(node->nvm_non_active); nvm_device = g_file_new_for_path(node->nvm_non_active); nvm = g_file_get_child(nvm_device, "nvmem"); is = (GInputStream *)g_file_read(nvm, NULL, &error); g_assert_no_error(error); g_assert_nonnull(is); do { g_autoptr(GBytes) b = NULL; const guchar *d; b = g_input_stream_read_bytes(is, 4096, NULL, &error); g_assert_no_error(error); g_assert_nonnull(is); d = g_bytes_get_data(b, &s); if (s > 0) g_checksum_update(chk, d, (gssize)s); } while (s > 0); sum_disk = g_checksum_get_string(chk); g_assert_cmpstr(sum_data, ==, sum_disk); } typedef gboolean (*MockTreePredicate)(const MockTree *node, gpointer data); static const MockTree * mock_tree_contains(const MockTree *node, MockTreePredicate predicate, gpointer data) { if (predicate(node, data)) return node; for (guint i = 0; i < node->children->len; i++) { const MockTree *child; const MockTree *match; child = g_ptr_array_index(node->children, i); match = mock_tree_contains(child, predicate, data); if (match != NULL) return match; } return NULL; } static gboolean mock_tree_all(const MockTree *node, MockTreePredicate predicate, gpointer data) { if (!predicate(node, data)) return FALSE; for (guint i = 0; i < node->children->len; i++) { const MockTree *child; child = g_ptr_array_index(node->children, i); if (!mock_tree_all(child, predicate, data)) return FALSE; } return TRUE; } static gboolean mock_tree_compare_uuid(const MockTree *node, gpointer data) { const gchar *uuid = (const gchar *)data; return g_str_equal(node->uuid, uuid); } static const MockTree * mock_tree_find_uuid(const MockTree *root, const char *uuid) { return mock_tree_contains(root, mock_tree_compare_uuid, (gpointer)uuid); } static gboolean mock_tree_node_have_fu_device(const MockTree *node, gpointer data) { return node->fu_device != NULL; } static void write_controller_fw(const gchar *nvm) { gboolean ret; g_autoptr(GFile) nvm_device = NULL; g_autoptr(GFile) nvmem = NULL; g_autofree gchar *fw_path = NULL; g_autoptr(GBytes) fw_blob = NULL; g_autoptr(GInputStream) is = NULL; g_autoptr(GOutputStream) os = NULL; g_autoptr(GError) error = NULL; g_autoptr(FuFirmware) firmware_ctl = fu_intel_thunderbolt_nvm_new(); gssize n; fw_path = g_test_build_filename(G_TEST_DIST, "tests", "minimal-fw-controller.builder.xml", NULL); ret = fu_firmware_build_from_filename(firmware_ctl, fw_path, &error); g_assert_no_error(error); g_assert_true(ret); fw_blob = fu_firmware_write(firmware_ctl, &error); g_assert_no_error(error); g_assert_nonnull(fw_blob); nvm_device = g_file_new_for_path(nvm); g_assert_nonnull(nvm_device); nvmem = g_file_get_child(nvm_device, "nvmem"); g_assert_nonnull(nvmem); os = (GOutputStream *)g_file_append_to(nvmem, G_FILE_CREATE_NONE, NULL, &error); g_assert_no_error(error); g_assert_nonnull(os); is = g_memory_input_stream_new_from_bytes(fw_blob); n = g_output_stream_splice(os, is, G_OUTPUT_STREAM_SPLICE_NONE, NULL, &error); g_assert_no_error(error); g_assert_cmpuint(n, >, 0); } static gboolean mock_tree_attach_device(gpointer user_data) { MockTree *tree = (MockTree *)user_data; const MockDevice *dev = tree->device; g_autofree gchar *idstr = NULL; g_autofree gchar *authenticate = NULL; g_assert_nonnull(tree); g_assert_nonnull(tree->sysfs_parent); g_assert_nonnull(dev); idstr = g_strdup_printf("%d-%d", dev->domain_id, tree->sysfs_id); authenticate = g_strdup_printf("0x%x", tree->nvm_authenticate); tree->path = umockdev_testbed_add_device(tree->bed, "thunderbolt", idstr, tree->sysfs_parent, "device_name", dev->name, "device", dev->id, "vendor", "042", "vendor_name", "GNOME.org", "authorized", "1", "nvm_authenticate", authenticate, "nvm_version", tree->nvm_version, "unique_id", tree->uuid, NULL, "DEVTYPE", "thunderbolt_device", NULL); tree->nvm_non_active = udev_mock_add_nvmem(tree->bed, FALSE, tree->path, tree->sysfs_id); tree->nvm_active = udev_mock_add_nvmem(tree->bed, TRUE, tree->path, tree->sysfs_id); g_assert_nonnull(tree->path); g_assert_nonnull(tree->nvm_non_active); g_assert_nonnull(tree->nvm_active); write_controller_fw(tree->nvm_active); for (guint i = 0; i < tree->children->len; i++) { MockTree *child; child = g_ptr_array_index(tree->children, i); child->bed = g_object_ref(tree->bed); child->sysfs_parent = g_strdup(tree->path); g_timeout_add(child->device->delay_ms, mock_tree_attach_device, child); } return FALSE; } typedef struct SyncContext { MockTree *tree; GMainLoop *loop; } SyncContext; static gboolean on_sync_timeout(gpointer user_data) { SyncContext *ctx = (SyncContext *)user_data; g_main_loop_quit(ctx->loop); return FALSE; } static void sync_device_added(FuPlugin *plugin, FuDevice *device, gpointer user_data) { SyncContext *ctx = (SyncContext *)user_data; MockTree *tree = ctx->tree; const gchar *uuid = fu_device_get_physical_id(device); MockTree *target; target = (MockTree *)mock_tree_find_uuid(tree, uuid); if (target == NULL) { g_critical("Got device that could not be matched: %s", uuid); return; } if (target->fu_device != NULL) g_object_unref(target->fu_device); target->fu_device = g_object_ref(device); } static void sync_device_removed(FuPlugin *plugin, FuDevice *device, gpointer user_data) { SyncContext *ctx = (SyncContext *)user_data; MockTree *tree = ctx->tree; const gchar *uuid = fu_device_get_physical_id(device); MockTree *target; target = (MockTree *)mock_tree_find_uuid(tree, uuid); if (target == NULL) { g_warning("Got device that could not be matched: %s", uuid); return; } if (target->fu_device == NULL) { g_warning("Got remove event for out-of-tree device %s", uuid); return; } g_object_unref(target->fu_device); target->fu_device = NULL; } static void mock_tree_sync(MockTree *root, FuPlugin *plugin, int timeout_ms) { g_autoptr(GMainLoop) mainloop = g_main_loop_new(NULL, FALSE); gulong id_add; gulong id_del; SyncContext ctx = { .tree = root, .loop = mainloop, }; id_add = g_signal_connect(FU_PLUGIN(plugin), "device-added", G_CALLBACK(sync_device_added), &ctx); id_del = g_signal_connect(FU_PLUGIN(plugin), "device-removed", G_CALLBACK(sync_device_removed), &ctx); if (timeout_ms > 0) g_timeout_add(timeout_ms, on_sync_timeout, &ctx); g_main_loop_run(mainloop); g_signal_handler_disconnect(plugin, id_add); g_signal_handler_disconnect(plugin, id_del); } typedef struct AttachContext { /* in */ MockTree *tree; GMainLoop *loop; /* out */ gboolean complete; } AttachContext; static void mock_tree_plugin_device_added(FuPlugin *plugin, FuDevice *device, gpointer user_data) { AttachContext *ctx = (AttachContext *)user_data; MockTree *tree = ctx->tree; const gchar *uuid = fu_device_get_physical_id(device); MockTree *target; target = (MockTree *)mock_tree_find_uuid(tree, uuid); if (target == NULL) { g_warning("Got device that could not be matched: %s", uuid); return; } g_set_object(&target->fu_device, device); if (mock_tree_all(tree, mock_tree_node_have_fu_device, NULL)) { ctx->complete = TRUE; g_main_loop_quit(ctx->loop); } } static gboolean mock_tree_settle(MockTree *root, FuPlugin *plugin) { g_autoptr(GMainLoop) mainloop = g_main_loop_new(NULL, FALSE); gulong id; AttachContext ctx = { .tree = root, .loop = mainloop, }; id = g_signal_connect(FU_PLUGIN(plugin), "device-added", G_CALLBACK(mock_tree_plugin_device_added), &ctx); g_main_loop_run(mainloop); g_signal_handler_disconnect(plugin, id); return ctx.complete; } static gboolean mock_tree_attach(MockTree *root, UMockdevTestbed *bed, FuPlugin *plugin) { root->bed = g_object_ref(bed); root->sysfs_parent = udev_mock_add_usb4_port(bed, 1); g_assert_nonnull(root->sysfs_parent); g_timeout_add(root->device->delay_ms, mock_tree_attach_device, root); return mock_tree_settle(root, plugin); } /* the unused parameter makes the function signature compatible * with 'MockTreePredicate' */ static gboolean mock_tree_node_is_detached(const MockTree *node, gpointer unused) { gboolean ret = node->path == NULL; /* consistency checks: if ret, make sure we are * fully detached */ if (ret) { g_assert_null(node->nvm_active); g_assert_null(node->nvm_non_active); g_assert_null(node->bed); } else { g_assert_nonnull(node->nvm_active); g_assert_nonnull(node->nvm_non_active); g_assert_nonnull(node->bed); } return ret; } static void mock_tree_detach(MockTree *node) { UMockdevTestbed *bed; if (mock_tree_node_is_detached(node, NULL)) return; for (guint i = 0; i < node->children->len; i++) { MockTree *child = g_ptr_array_index(node->children, i); mock_tree_detach(child); g_free(child->sysfs_parent); child->sysfs_parent = NULL; } bed = node->bed; umockdev_testbed_uevent(bed, node->nvm_active, "remove"); umockdev_testbed_remove_device(bed, node->nvm_active); umockdev_testbed_uevent(bed, node->nvm_non_active, "remove"); umockdev_testbed_remove_device(bed, node->nvm_non_active); umockdev_testbed_uevent(bed, node->path, "remove"); umockdev_testbed_remove_device(bed, node->path); g_free(node->path); g_free(node->nvm_non_active); g_free(node->nvm_active); node->path = NULL; node->nvm_non_active = NULL; node->nvm_active = NULL; g_object_unref(bed); node->bed = NULL; } typedef enum FuThunderboltTestUpdateResult { UPDATE_SUCCESS = 0, /* nvm_authenticate will report error condition */ UPDATE_FAIL_DEVICE_INTERNAL = 1, /* device to be updated will NOT re-appear */ UPDATE_FAIL_DEVICE_NOSHOW = 2 } FuThunderboltTestUpdateResult; typedef struct UpdateContext { GFileMonitor *monitor; FuThunderboltTestUpdateResult result; guint timeout; GBytes *data; UMockdevTestbed *bed; FuPlugin *plugin; MockTree *node; gchar *version; } UpdateContext; static void update_context_free(UpdateContext *ctx) { if (ctx == NULL) return; g_file_monitor_cancel(ctx->monitor); g_object_unref(ctx->bed); g_object_unref(ctx->plugin); g_object_unref(ctx->monitor); g_bytes_unref(ctx->data); g_free(ctx->version); g_free(ctx); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTOPTR_CLEANUP_FUNC(UpdateContext, update_context_free); #pragma clang diagnostic pop static gboolean reattach_tree(gpointer user_data) { UpdateContext *ctx = (UpdateContext *)user_data; MockTree *node = ctx->node; g_debug("Mock update done, reattaching tree..."); node->bed = g_object_ref(ctx->bed); g_timeout_add(node->device->delay_ms, mock_tree_attach_device, node); return FALSE; } static void udev_file_changed_cb(GFileMonitor *monitor, GFile *file, GFile *other_file, GFileMonitorEvent event_type, gpointer user_data) { UpdateContext *ctx = (UpdateContext *)user_data; gboolean ok; gsize len; g_autofree gchar *data = NULL; g_autoptr(GError) error = NULL; g_debug("Got update trigger"); ok = g_file_monitor_cancel(monitor); g_assert_true(ok); ok = g_file_load_contents(file, NULL, &data, &len, NULL, &error); g_assert_no_error(error); g_assert_true(ok); if (!g_str_has_prefix(data, "1")) return; /* verify the firmware is correct */ mock_tree_firmware_verify(ctx->node, ctx->data); g_debug("Removing tree below and including: %s", ctx->node->path); mock_tree_detach(ctx->node); ctx->node->nvm_authenticate = (guint)ctx->result; /* update the version only on "success" simulations */ if (ctx->result == UPDATE_SUCCESS) { g_free(ctx->node->nvm_version); ctx->node->nvm_version = g_strdup(ctx->version); } g_debug("Simulating update to '%s' with result: 0x%x", ctx->version, ctx->node->nvm_authenticate); if (ctx->result == UPDATE_FAIL_DEVICE_NOSHOW) { g_debug("Simulating no-show fail:" " device tree will not reappear"); return; } g_debug("Device tree reattachment in %3.2f seconds", ctx->timeout / 1000.0); g_timeout_add(ctx->timeout, reattach_tree, ctx); } static UpdateContext * mock_tree_prepare_for_update(MockTree *node, FuPlugin *plugin, const char *version, GBytes *fw_data, guint timeout_ms) { UpdateContext *ctx; g_autoptr(GFile) dir = NULL; g_autoptr(GFile) f = NULL; g_autoptr(GError) error = NULL; GFileMonitor *monitor; ctx = g_new0(UpdateContext, 1); dir = g_file_new_for_path(node->path); f = g_file_get_child(dir, "nvm_authenticate"); monitor = g_file_monitor_file(f, G_FILE_MONITOR_NONE, NULL, &error); g_assert_no_error(error); g_assert_nonnull(monitor); ctx->node = node; ctx->plugin = g_object_ref(plugin); ctx->bed = g_object_ref(node->bed); ctx->timeout = timeout_ms; ctx->monitor = monitor; ctx->version = g_strdup(version); ctx->data = g_bytes_ref(fw_data); g_signal_connect(G_FILE_MONITOR(monitor), "changed", G_CALLBACK(udev_file_changed_cb), ctx); return ctx; } static MockDevice root_one = { .name = "Laptop", .id = "0x23", .nvm_version = "20.2", .nvm_parsed_version = "20.02", .children = (MockDevice[]){ { .name = "Thunderbolt Cable", .id = "0x24", .nvm_version = "20.0", .nvm_parsed_version = "20.00", .children = (MockDevice[]){{ .name = "Thunderbolt Dock", .id = "0x25", .nvm_version = "10.0", .nvm_parsed_version = "10.00", }, { NULL, } }, }, { .name = "Thunderbolt Cable", .id = "0x24", .nvm_version = "23.0", .nvm_parsed_version = "23.00", .children = (MockDevice[]){{ .name = "Thunderbolt SSD", .id = "0x26", .nvm_version = "5.0", .nvm_parsed_version = "05.00", }, { NULL, }}, }, { NULL, }, }, }; typedef struct TestParam { gboolean initialize_tree; gboolean attach_and_coldplug; const char *firmware_file; } TestParam; typedef enum FuThunderboltTestFlags { TEST_INITIALIZE_TREE = 1 << 0, TEST_ATTACH = 1 << 1, TEST_PREPARE_FIRMWARE = 1 << 2, TEST_PREPARE_ALL = TEST_INITIALIZE_TREE | TEST_ATTACH | TEST_PREPARE_FIRMWARE } FuThunderboltTestFlags; #define TEST_INIT_FULL (GUINT_TO_POINTER(TEST_PREPARE_ALL)) #define TEST_INIT_NONE (GUINT_TO_POINTER(0)) typedef struct ThunderboltTest { UMockdevTestbed *bed; FuPlugin *plugin; FuContext *ctx; GUdevClient *udev_client; /* if TestParam::initialize_tree */ MockTree *tree; /* if TestParam::firmware_file is nonnull */ GBytes *fw_data; GInputStream *fw_stream; } ThunderboltTest; static void fu_thunderbolt_gudev_uevent_cb(GUdevClient *gudev_client, const gchar *action, GUdevDevice *udev_device, ThunderboltTest *tt) { if (g_strcmp0(action, "add") == 0) { g_autoptr(FuUdevDevice) device = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); device = fu_udev_device_new(tt->ctx, g_udev_device_get_sysfs_path(udev_device)); if (!fu_device_probe(FU_DEVICE(device), &error_local)) { g_warning("failed to probe: %s", error_local->message); return; } if (!fu_plugin_runner_backend_device_added(tt->plugin, FU_DEVICE(device), progress, &error_local)) g_debug("failed to add: %s", error_local->message); return; } if (g_strcmp0(action, "remove") == 0) { if (tt->tree->fu_device != NULL) fu_plugin_device_remove(tt->plugin, tt->tree->fu_device); return; } if (g_strcmp0(action, "change") == 0) { const gchar *uuid = g_udev_device_get_sysfs_attr(udev_device, "unique_id"); /* nocheck:blocked */ MockTree *target = (MockTree *)mock_tree_find_uuid(tt->tree, uuid); g_assert_nonnull(target); fu_udev_device_emit_changed(FU_UDEV_DEVICE(target->fu_device)); return; } } static void test_set_up(ThunderboltTest *tt, gconstpointer params) { FuThunderboltTestFlags flags = GPOINTER_TO_UINT(params); gboolean ret; g_autofree gchar *sysfs = NULL; g_autoptr(GError) error = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); const gchar *udev_subsystems[] = {"thunderbolt", NULL}; tt->ctx = fu_context_new(); ret = fu_context_load_quirks(tt->ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE | FU_QUIRKS_LOAD_FLAG_NO_VERIFY, &error); g_assert_no_error(error); g_assert_true(ret); tt->bed = umockdev_testbed_new(); g_assert_nonnull(tt->bed); sysfs = umockdev_testbed_get_sys_dir(tt->bed); g_debug("mock sysfs at %s", sysfs); tt->plugin = fu_plugin_new_from_gtype(fu_thunderbolt_plugin_get_type(), tt->ctx); g_assert_nonnull(tt->plugin); ret = fu_plugin_runner_startup(tt->plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); if (flags & TEST_INITIALIZE_TREE) { tt->tree = mock_tree_init(&root_one); g_assert_nonnull(tt->tree); } if (!umockdev_in_mock_environment()) { g_warning("Need to run with umockdev-wrapper"); return; } tt->udev_client = g_udev_client_new(udev_subsystems); /* nocheck:blocked */ g_assert_nonnull(tt->udev_client); g_signal_connect(G_UDEV_CLIENT(tt->udev_client), "uevent", G_CALLBACK(fu_thunderbolt_gudev_uevent_cb), tt); if (flags & TEST_ATTACH) { g_assert_true(flags & TEST_INITIALIZE_TREE); ret = mock_tree_attach(tt->tree, tt->bed, tt->plugin); g_assert_true(ret); } if (flags & TEST_PREPARE_FIRMWARE) { g_autofree gchar *fw_path = NULL; g_autoptr(GBytes) fw_blob = NULL; g_autoptr(FuFirmware) firmware = fu_intel_thunderbolt_firmware_new(); fw_path = g_test_build_filename(G_TEST_DIST, "tests", "minimal-fw.builder.xml", NULL); ret = fu_firmware_build_from_filename(firmware, fw_path, &error); g_assert_no_error(error); g_assert_true(ret); tt->fw_data = fu_firmware_write(firmware, &error); g_assert_no_error(error); g_assert_nonnull(tt->fw_data); tt->fw_stream = g_memory_input_stream_new_from_bytes(tt->fw_data); g_assert_nonnull(tt->fw_stream); } } static void test_tear_down(ThunderboltTest *tt, gconstpointer user_data) { g_object_unref(tt->plugin); g_object_unref(tt->ctx); g_object_unref(tt->bed); g_object_unref(tt->udev_client); if (tt->tree) mock_tree_free(tt->tree); if (tt->fw_data) g_bytes_unref(tt->fw_data); if (tt->fw_stream) g_object_unref(tt->fw_stream); } static gboolean test_tree_uuids(const MockTree *node, gpointer data) { const MockTree *root = (MockTree *)data; const gchar *uuid = node->uuid; const MockTree *found; g_assert_nonnull(uuid); g_debug("Looking for %s", uuid); found = mock_tree_find_uuid(root, uuid); g_assert_nonnull(node); g_assert_nonnull(found); g_assert_cmpstr(node->uuid, ==, found->uuid); /* return false so we traverse the whole tree */ return FALSE; } static void test_tree(ThunderboltTest *tt, gconstpointer user_data) { const MockTree *found; gboolean ret; g_autoptr(MockTree) tree = NULL; tree = mock_tree_init(&root_one); g_assert_nonnull(tree); mock_tree_dump(tree, 0); (void)mock_tree_contains(tree, test_tree_uuids, tree); found = mock_tree_find_uuid(tree, "nonexistentuuid"); g_assert_null(found); ret = mock_tree_attach(tree, tt->bed, tt->plugin); g_assert_true(ret); mock_tree_detach(tree); ret = mock_tree_all(tree, mock_tree_node_is_detached, NULL); g_assert_true(ret); } static void test_image_validation(ThunderboltTest *tt, gconstpointer user_data) { gboolean ret; g_autofree gchar *ctl_path = NULL; g_autofree gchar *fwi_path = NULL; g_autofree gchar *bad_path = NULL; g_autoptr(GMappedFile) bad_file = NULL; g_autoptr(GBytes) bad_data = NULL; g_autoptr(GError) error = NULL; g_autoptr(FuFirmware) firmware_fwi = fu_intel_thunderbolt_firmware_new(); g_autoptr(FuFirmware) firmware_ctl = fu_intel_thunderbolt_nvm_new(); g_autoptr(FuFirmware) firmware_bad = fu_intel_thunderbolt_nvm_new(); /* image as if read from the controller (i.e. no headers) */ ctl_path = g_test_build_filename(G_TEST_DIST, "tests", "minimal-fw-controller.builder.xml", NULL); ret = fu_firmware_build_from_filename(firmware_ctl, ctl_path, &error); g_assert_no_error(error); g_assert_true(ret); /* valid firmware update image */ fwi_path = g_test_build_filename(G_TEST_DIST, "tests", "minimal-fw.builder.xml", NULL); ret = fu_firmware_build_from_filename(firmware_fwi, fwi_path, &error); g_assert_no_error(error); g_assert_true(ret); /* a wrong/bad firmware update image */ bad_path = g_test_build_filename(G_TEST_DIST, "tests", "colorhug.txt", NULL); bad_file = g_mapped_file_new(bad_path, FALSE, &error); g_assert_no_error(error); g_assert_nonnull(bad_file); bad_data = g_mapped_file_get_bytes(bad_file); g_assert_nonnull(bad_data); /* parse; should fail, bad image */ ret = fu_firmware_parse_bytes(firmware_bad, bad_data, 0x0, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, &error); g_assert_false(ret); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_READ); g_debug("expected image validation error [ctl]: %s", error->message); g_clear_error(&error); /* now for some testing ... this should work */ ret = fu_firmware_check_compatible(firmware_ctl, firmware_fwi, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); } static void test_change_uevent(ThunderboltTest *tt, gconstpointer user_data) { FuPlugin *plugin = tt->plugin; MockTree *tree = tt->tree; gboolean ret; const gchar *version_after; /* test sanity check */ g_assert_nonnull(tree); /* simulate change of version via a change even, i.e. * without add, remove. */ umockdev_testbed_set_attribute(tt->bed, tree->path, "nvm_version", "42.23"); umockdev_testbed_uevent(tt->bed, tree->path, "change"); /* we just "wait" for 500ms, should be enough */ mock_tree_sync(tree, plugin, 500); /* the tree should not have changed */ ret = mock_tree_all(tree, mock_tree_node_have_fu_device, NULL); g_assert_true(ret); /* we should have the version change in the FuDevice */ version_after = fu_device_get_version(tree->fu_device); g_assert_cmpstr(version_after, ==, "42.23"); } static void test_update_working(ThunderboltTest *tt, gconstpointer user_data) { FuPlugin *plugin = tt->plugin; MockTree *tree = tt->tree; GBytes *fw_data = tt->fw_data; gboolean ret; const gchar *version_after; g_autoptr(GError) error = NULL; g_autoptr(UpdateContext) up_ctx = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); /* test sanity check */ g_assert_nonnull(tree); g_assert_nonnull(fw_data); /* simulate an update, where the device goes away and comes back * after the time in the last parameter (given in ms) */ up_ctx = mock_tree_prepare_for_update(tree, plugin, "42.23", fw_data, 1000); g_assert_nonnull(up_ctx); ret = fu_plugin_runner_write_firmware(plugin, tree->fu_device, tt->fw_stream, progress, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, &error); g_assert_no_error(error); g_assert_true(ret); /* we wait until the plugin has picked up all the * subtree changes */ ret = mock_tree_settle(tree, plugin); g_assert_true(ret); ret = fu_plugin_runner_attach(plugin, tree->fu_device, progress, &error); g_assert_no_error(error); g_assert_true(ret); version_after = fu_device_get_version(tree->fu_device); g_debug("version after update: %s", version_after); g_assert_cmpstr(version_after, ==, "42.23"); /* make sure all pending events have happened */ ret = mock_tree_settle(tree, plugin); g_assert_true(ret); /* now we check if the every tree node has a corresponding FuDevice, * this implicitly checks that we are handling uevents correctly * after the event, and that we are in sync with the udev tree */ ret = mock_tree_all(tree, mock_tree_node_have_fu_device, NULL); g_assert_true(ret); } static void test_update_wd19(ThunderboltTest *tt, gconstpointer user_data) { FuPlugin *plugin = tt->plugin; MockTree *tree = tt->tree; GBytes *fw_data = tt->fw_data; gboolean ret; const gchar *version_before; const gchar *version_after; g_autoptr(GError) error = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); /* test sanity check */ g_assert_nonnull(tree); g_assert_nonnull(fw_data); /* simulate a wd19 update which will not disappear / re-appear */ fu_device_add_private_flag(tree->fu_device, FU_DEVICE_PRIVATE_FLAG_SKIPS_RESTART); fu_device_add_flag(tree->fu_device, FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE); version_before = fu_device_get_version(tree->fu_device); ret = fu_plugin_runner_write_firmware(plugin, tree->fu_device, tt->fw_stream, progress, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_device_has_flag(tree->fu_device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION); g_assert_true(ret); version_after = fu_device_get_version(tree->fu_device); g_assert_cmpstr(version_after, ==, version_before); } static void test_update_fail(ThunderboltTest *tt, gconstpointer user_data) { FuPlugin *plugin = tt->plugin; MockTree *tree = tt->tree; GBytes *fw_data = tt->fw_data; gboolean ret; const gchar *version_after; g_autoptr(GError) error = NULL; g_autoptr(UpdateContext) up_ctx = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); /* test sanity check */ g_assert_nonnull(tree); g_assert_nonnull(fw_data); /* simulate an update, as in test_update_working, * but simulate an error indicated by the device */ up_ctx = mock_tree_prepare_for_update(tree, plugin, "42.23", fw_data, 1000); up_ctx->result = UPDATE_FAIL_DEVICE_INTERNAL; ret = fu_plugin_runner_write_firmware(plugin, tree->fu_device, tt->fw_stream, progress, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, &error); g_assert_no_error(error); g_assert_true(ret); /* we wait until the plugin has picked up all the * subtree changes, and make sure we still receive * udev updates correctly and are in sync */ ret = mock_tree_settle(tree, plugin); g_assert_true(ret); ret = fu_plugin_runner_attach(plugin, tree->fu_device, progress, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL); g_assert_false(ret); /* make sure all pending events have happened */ ret = mock_tree_settle(tree, plugin); g_assert_true(ret); /* version should *not* have changed (but we get parsed version) */ version_after = fu_device_get_version(tree->fu_device); g_debug("version after update: %s", version_after); g_assert_cmpstr(version_after, ==, tree->device->nvm_parsed_version); ret = mock_tree_all(tree, mock_tree_node_have_fu_device, NULL); g_assert_true(ret); } static void test_update_fail_nowshow(ThunderboltTest *tt, gconstpointer user_data) { FuPlugin *plugin = tt->plugin; MockTree *tree = tt->tree; GBytes *fw_data = tt->fw_data; gboolean ret; g_autoptr(GError) error = NULL; g_autoptr(UpdateContext) up_ctx = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); /* test sanity check */ g_assert_nonnull(tree); g_assert_nonnull(fw_data); /* simulate an update, as in test_update_working, * but simulate an error indicated by the device */ up_ctx = mock_tree_prepare_for_update(tree, plugin, "42.23", fw_data, 1000); up_ctx->result = UPDATE_FAIL_DEVICE_NOSHOW; ret = fu_plugin_runner_write_firmware(plugin, tree->fu_device, tt->fw_stream, progress, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, &error); g_assert_no_error(error); g_assert_true(ret); mock_tree_sync(tree, plugin, 500); ret = mock_tree_all(tree, mock_tree_node_have_fu_device, NULL); g_assert_false(ret); } int main(int argc, char **argv) { g_autofree gchar *testdatadir = NULL; (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); testdatadir = g_test_build_filename(G_TEST_DIST, "tests", NULL); (void)g_setenv("FWUPD_SYSFSFWATTRIBDIR", testdatadir, TRUE); g_test_add("/thunderbolt/basic", ThunderboltTest, NULL, test_set_up, test_tree, test_tear_down); g_test_add("/thunderbolt/image-validation", ThunderboltTest, TEST_INIT_NONE, test_set_up, test_image_validation, test_tear_down); g_test_add("/thunderbolt/change-uevent", ThunderboltTest, GUINT_TO_POINTER(TEST_INITIALIZE_TREE | TEST_ATTACH), test_set_up, test_change_uevent, test_tear_down); g_test_add("/thunderbolt/update{working}", ThunderboltTest, TEST_INIT_FULL, test_set_up, test_update_working, test_tear_down); g_test_add("/thunderbolt/update{failing}", ThunderboltTest, TEST_INIT_FULL, test_set_up, test_update_fail, test_tear_down); g_test_add("/thunderbolt/update{failing-noshow}", ThunderboltTest, TEST_INIT_FULL, test_set_up, test_update_fail_nowshow, test_tear_down); g_test_add("/thunderbolt/update{delayed_activation}", ThunderboltTest, TEST_INIT_FULL, test_set_up, test_update_wd19, test_tear_down); return g_test_run(); } fwupd-2.0.10/plugins/thunderbolt/fu-thunderbolt-common.c000066400000000000000000000060211501337203100233200ustar00rootroot00000000000000/* * Copyright 2017 Christian J. Kellner * Copyright 2020 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-thunderbolt-common.h" static gchar * fu_thunderbolt_device_find_usb4_port_path(FuUdevDevice *device, const gchar *attribute, GError **error) { const gchar *sysfs_path = fu_udev_device_get_sysfs_path(device); for (guint i = 0; i < 9; i++) { g_autofree gchar *path = g_strdup_printf("usb4_port%u/%s", i, attribute); g_autofree gchar *fn = g_build_filename(sysfs_path, path, NULL); g_autoptr(GFile) file = g_file_new_for_path(fn); if (g_file_query_exists(file, NULL)) return g_steal_pointer(&path); } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "failed to find usb4_port?/%s", attribute); return NULL; } gboolean fu_thunderbolt_udev_set_port_offline(FuUdevDevice *device, GError **error) { g_autofree gchar *attribute = NULL; g_autoptr(GError) error_local = NULL; attribute = fu_thunderbolt_device_find_usb4_port_path(device, "offline", &error_local); if (attribute == NULL) { g_debug("failed to check usb4 offline path: %s", error_local->message); return TRUE; } if (!fu_udev_device_write_sysfs(device, attribute, "1", FU_THUNDERBOLT_DEVICE_WRITE_TIMEOUT, error)) { g_prefix_error(error, "setting usb4 port offline failed: "); return FALSE; } return TRUE; } gboolean fu_thunderbolt_udev_rescan_port(FuUdevDevice *device, GError **error) { g_autofree gchar *attribute = NULL; g_autoptr(GError) error_local = NULL; attribute = fu_thunderbolt_device_find_usb4_port_path(device, "rescan", &error_local); if (attribute == NULL) { g_debug("failed to check usb4 rescan path: %s", error_local->message); return TRUE; } if (!fu_udev_device_write_sysfs(device, attribute, "1", FU_THUNDERBOLT_DEVICE_WRITE_TIMEOUT, error)) { g_prefix_error(error, "rescan on port failed: "); return FALSE; } return TRUE; } gboolean fu_thunderbolt_udev_set_port_online(FuUdevDevice *device, GError **error) { FuUdevDevice *udev = FU_UDEV_DEVICE(device); g_autofree gchar *attribute = NULL; g_autoptr(GError) error_local = NULL; attribute = fu_thunderbolt_device_find_usb4_port_path(device, "offline", &error_local); if (attribute == NULL) { g_debug("failed to check usb4 port path: %s", error_local->message); return TRUE; } if (!fu_udev_device_write_sysfs(udev, attribute, "0", FU_THUNDERBOLT_DEVICE_WRITE_TIMEOUT, error)) { g_prefix_error(error, "setting port online failed: "); return FALSE; } return TRUE; } guint16 fu_thunderbolt_udev_get_attr_uint16(FuUdevDevice *device, const gchar *name, GError **error) { guint64 val = 0; g_autofree gchar *str = NULL; str = fu_udev_device_read_sysfs(device, name, FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, error); if (str == NULL) return 0x0; if (!fu_strtoull(str, &val, 0, G_MAXUINT16, FU_INTEGER_BASE_16, error)) return 0x0; return (guint16)val; } fwupd-2.0.10/plugins/thunderbolt/fu-thunderbolt-common.h000066400000000000000000000012751501337203100233330ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * Copyright 2020 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_THUNDERBOLT_DEVICE_FLAG_FORCE_ENUMERATION "force-enumeration" #define FU_THUNDERBOLT_DEVICE_WRITE_TIMEOUT 1500 /* ms */ gboolean fu_thunderbolt_udev_set_port_online(FuUdevDevice *device, GError **error); gboolean fu_thunderbolt_udev_set_port_offline(FuUdevDevice *device, GError **error); gboolean fu_thunderbolt_udev_rescan_port(FuUdevDevice *device, GError **error); guint16 fu_thunderbolt_udev_get_attr_uint16(FuUdevDevice *device, const gchar *name, GError **error); fwupd-2.0.10/plugins/thunderbolt/fu-thunderbolt-controller.c000066400000000000000000000324541501337203100242240ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * Copyright 2017 Christian J. Kellner * Copyright 2020 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-thunderbolt-common.h" #include "fu-thunderbolt-controller.h" #include "fu-thunderbolt-struct.h" struct _FuThunderboltController { FuThunderboltDevice parent_instance; FuThunderboltControllerKind controller_kind; gboolean safe_mode; gboolean is_native; guint16 gen; guint host_online_timer_id; }; G_DEFINE_TYPE(FuThunderboltController, fu_thunderbolt_controller, FU_TYPE_THUNDERBOLT_DEVICE) /* byte offsets in firmware image */ #define FU_TBT_OFFSET_NATIVE 0x7B #define FU_TBT_CHUNK_SZ 0x40 static void fu_thunderbolt_controller_check_safe_mode(FuThunderboltController *self) { const gchar *devpath = fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(self)); /* failed to read, for host check for safe mode */ if (self->controller_kind != FU_THUNDERBOLT_CONTROLLER_KIND_DEVICE) return; g_warning("%s is in safe mode -- VID/DID will " "need to be set by another plugin", devpath); self->safe_mode = TRUE; fu_device_set_version(FU_DEVICE(self), "00.00"); fu_device_add_instance_id(FU_DEVICE(self), "TBT-safemode"); fu_device_set_metadata_boolean(FU_DEVICE(self), FU_DEVICE_METADATA_TBT_IS_SAFE_MODE, TRUE); } static void fu_thunderbolt_controller_ensure_fallback_name(FuThunderboltController *self) { if (self->controller_kind == FU_THUNDERBOLT_CONTROLLER_KIND_HOST) { if (self->gen >= 4) { fu_device_set_name(FU_DEVICE(self), "USB4 host controller"); return; } fu_device_set_name(FU_DEVICE(self), "Thunderbolt host controller"); } if (self->controller_kind == FU_THUNDERBOLT_CONTROLLER_KIND_DEVICE) { if (self->gen >= 4) { fu_device_set_name(FU_DEVICE(self), "USB4 device controller"); return; } fu_device_set_name(FU_DEVICE(self), "Thunderbolt device controller"); } } static void fu_thunderbolt_controller_to_string(FuDevice *device, guint idt, GString *str) { FuThunderboltController *self = FU_THUNDERBOLT_CONTROLLER(device); fwupd_codec_string_append(str, idt, "DeviceType", fu_thunderbolt_controller_kind_to_string(self->controller_kind)); fwupd_codec_string_append_bool(str, idt, "SafeMode", self->safe_mode); fwupd_codec_string_append_bool(str, idt, "NativeMode", self->is_native); fwupd_codec_string_append_int(str, idt, "Generation", self->gen); } static gboolean fu_thunderbolt_controller_probe(FuDevice *device, GError **error) { FuThunderboltController *self = FU_THUNDERBOLT_CONTROLLER(device); g_autofree gchar *attr_unique_id = NULL; g_autofree gchar *prop_type = NULL; /* FuUdevDevice->probe */ if (!FU_DEVICE_CLASS(fu_thunderbolt_controller_parent_class)->probe(device, error)) return FALSE; /* determine if host controller or not */ prop_type = fu_udev_device_read_property(FU_UDEV_DEVICE(self), "USB4_TYPE", NULL); if (prop_type != NULL) { self->controller_kind = fu_thunderbolt_controller_kind_from_string(prop_type); } else { g_autoptr(FuDevice) device_parent = fu_device_get_backend_parent_with_subsystem(FU_DEVICE(self), "thunderbolt:thunderbolt_domain", NULL); if (device_parent != NULL) { g_autofree gchar *parent_name = g_path_get_basename( fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device_parent))); if (g_str_has_prefix(parent_name, "domain")) self->controller_kind = FU_THUNDERBOLT_CONTROLLER_KIND_HOST; else self->controller_kind = FU_THUNDERBOLT_CONTROLLER_KIND_DEVICE; } } attr_unique_id = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(device), "unique_id", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (attr_unique_id != NULL) fu_device_set_physical_id(device, attr_unique_id); /* success */ return TRUE; } static gboolean fu_thunderbolt_controller_read_status_block(FuThunderboltController *self, GError **error) { gsize nr_chunks; g_autoptr(GBytes) blob = NULL; g_autoptr(GFile) nvmem = NULL; g_autoptr(GInputStream) istr = NULL; g_autoptr(GInputStream) istr_partial = NULL; g_autoptr(FuFirmware) firmware = NULL; nvmem = fu_thunderbolt_device_find_nvmem(FU_THUNDERBOLT_DEVICE(self), TRUE, error); if (nvmem == NULL) return FALSE; /* read just enough bytes to read the status byte */ nr_chunks = (FU_TBT_OFFSET_NATIVE + FU_TBT_CHUNK_SZ - 1) / FU_TBT_CHUNK_SZ; istr = G_INPUT_STREAM(g_file_read(nvmem, NULL, error)); if (istr == NULL) return FALSE; blob = fu_input_stream_read_bytes(istr, 0x0, nr_chunks * FU_TBT_CHUNK_SZ, NULL, error); if (blob == NULL) return FALSE; istr_partial = g_memory_input_stream_new_from_bytes(blob); firmware = fu_firmware_new_from_gtypes(istr_partial, 0x0, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error, FU_TYPE_INTEL_THUNDERBOLT_NVM, FU_TYPE_FIRMWARE, G_TYPE_INVALID); if (firmware == NULL) return FALSE; if (FU_IS_INTEL_THUNDERBOLT_NVM(firmware)) { self->is_native = fu_intel_thunderbolt_nvm_is_native(FU_INTEL_THUNDERBOLT_NVM(firmware)); } return TRUE; } static gboolean fu_thunderbolt_controller_can_update(FuThunderboltController *self) { g_autoptr(GError) nvmem_error = NULL; g_autoptr(GFile) non_active_nvmem = NULL; non_active_nvmem = fu_thunderbolt_device_find_nvmem(FU_THUNDERBOLT_DEVICE(self), FALSE, &nvmem_error); if (non_active_nvmem == NULL) { g_debug("%s", nvmem_error->message); return FALSE; } return TRUE; } static gboolean fu_thunderbolt_controller_set_port_online_cb(gpointer user_data) { FuThunderboltController *self = FU_THUNDERBOLT_CONTROLLER(user_data); g_autoptr(GError) error_local = NULL; if (!fu_thunderbolt_udev_set_port_online(FU_UDEV_DEVICE(self), &error_local)) g_warning("failed to set online after initial delay: %s", error_local->message); /* no longer valid */ self->host_online_timer_id = 0; return G_SOURCE_REMOVE; } static gboolean fu_thunderbolt_controller_setup_usb4(FuThunderboltController *self, GError **error) { if (!fu_thunderbolt_udev_set_port_offline(FU_UDEV_DEVICE(self), error)) return FALSE; if (!fu_thunderbolt_udev_rescan_port(FU_UDEV_DEVICE(self), error)) return FALSE; if (self->host_online_timer_id > 0) g_source_remove(self->host_online_timer_id); self->host_online_timer_id = g_timeout_add_seconds(5, fu_thunderbolt_controller_set_port_online_cb, self); return TRUE; } static gboolean fu_thunderbolt_controller_setup(FuDevice *device, GError **error) { FuThunderboltController *self = FU_THUNDERBOLT_CONTROLLER(device); guint16 did; guint16 vid; g_autofree gchar *attr_nvm_authenticate_on_disconnect = NULL; g_autofree gchar *attr_vendor_name = NULL; g_autoptr(GError) error_gen = NULL; g_autoptr(GError) error_version = NULL; /* requires kernel 5.5 or later, non-fatal if not available */ self->gen = fu_thunderbolt_udev_get_attr_uint16(FU_UDEV_DEVICE(self), "generation", &error_gen); if (self->gen == 0) g_debug("unable to read generation: %s", error_gen->message); if (self->gen >= 4) fu_thunderbolt_device_set_retries(FU_THUNDERBOLT_DEVICE(self), 1); /* discover retimers */ if (self->controller_kind == FU_THUNDERBOLT_CONTROLLER_KIND_HOST && fu_device_has_private_flag(FU_DEVICE(self), FU_THUNDERBOLT_DEVICE_FLAG_FORCE_ENUMERATION)) { g_autoptr(GError) error_local = NULL; if (!fu_thunderbolt_controller_setup_usb4(self, &error_local)) g_warning("failed to setup host: %s", error_local->message); } /* try to read the version */ if (!fu_thunderbolt_device_get_version(FU_THUNDERBOLT_DEVICE(self), &error_version)) { if (self->controller_kind != FU_THUNDERBOLT_CONTROLLER_KIND_HOST && g_error_matches(error_version, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_propagate_error(error, g_steal_pointer(&error_version)); return FALSE; } g_debug("%s", error_version->message); } /* these may be missing on ICL or later */ vid = fu_device_get_vid(device); if (vid == 0x0) g_debug("failed to get Vendor ID"); did = fu_device_get_pid(device); if (did == 0x0) g_debug("failed to get Device ID"); if (self->controller_kind == FU_THUNDERBOLT_CONTROLLER_KIND_HOST) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_INTERNAL); fu_device_set_summary(device, "Unmatched performance for high-speed I/O"); } else { g_autofree gchar *attr_device_name = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(self), "device_name", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); fu_device_set_name(device, attr_device_name); } /* set the controller name */ if (fu_device_get_name(device) == NULL) fu_thunderbolt_controller_ensure_fallback_name(self); /* set vendor string */ attr_vendor_name = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(self), "vendor_name", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (attr_vendor_name != NULL) fu_device_set_vendor(device, attr_vendor_name); if (fu_device_get_version(device) == NULL) fu_thunderbolt_controller_check_safe_mode(self); if (self->safe_mode) { fu_device_set_update_error(device, "Device is in safe mode"); } else { g_autofree gchar *device_id = NULL; g_autofree gchar *domain_id = NULL; if (fu_thunderbolt_controller_can_update(self)) { const gchar *devpath = fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(self)); g_autofree gchar *domain = g_path_get_basename(devpath); /* USB4 controllers don't have a concept of legacy vs native * so don't try to read a native attribute from their NVM */ if (self->controller_kind == FU_THUNDERBOLT_CONTROLLER_KIND_HOST && self->gen < 4) { /* read first block of firmware to get the is-native attribute */ if (!fu_thunderbolt_controller_read_status_block(self, error)) return FALSE; } else { self->is_native = FALSE; } domain_id = g_strdup_printf("TBT-%04x%04x%s-controller%s", (guint)vid, (guint)did, self->is_native ? "-native" : "", domain); fu_device_build_vendor_id_u16(device, "TBT", vid); device_id = g_strdup_printf("TBT-%04x%04x%s", (guint)vid, (guint)did, self->is_native ? "-native" : ""); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); /* check if device is authorized */ if (!fu_thunderbolt_device_check_authorized(FU_THUNDERBOLT_DEVICE(self), error)) return FALSE; } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "updates are distributed as part of the platform"); return FALSE; } fu_device_add_instance_id(device, device_id); if (domain_id != NULL) fu_device_add_instance_id(device, domain_id); } /* determine if we can update on unplug */ attr_nvm_authenticate_on_disconnect = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(device), "nvm_authenticate_on_disconnect", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, NULL); if (attr_nvm_authenticate_on_disconnect != NULL) { fu_thunderbolt_device_set_auth_method(FU_THUNDERBOLT_DEVICE(self), "nvm_authenticate_on_disconnect"); /* flushes image */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE); /* forces the device to write to authenticate on disconnect attribute */ fu_device_remove_private_flag(device, FU_DEVICE_PRIVATE_FLAG_SKIPS_RESTART); /* control the order of activation (less relevant; install too though) */ fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_INSTALL_PARENT_FIRST); } else { fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); } /* set up signed payload attribute */ if (self->controller_kind == FU_THUNDERBOLT_CONTROLLER_KIND_HOST && self->gen >= 3) fu_device_add_flag(device, FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); /* success */ return TRUE; } static gboolean fu_thunderbolt_controller_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { /* FuThunderboltDevice->write_firmware */ if (!FU_DEVICE_CLASS(fu_thunderbolt_controller_parent_class) ->write_firmware(device, firmware, progress, flags, error)) return FALSE; if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE)) fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static void fu_thunderbolt_controller_init(FuThunderboltController *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_register_private_flag(FU_DEVICE(self), FU_THUNDERBOLT_DEVICE_FLAG_FORCE_ENUMERATION); } static void fu_thunderbolt_controller_finalize(GObject *object) { FuThunderboltController *self = FU_THUNDERBOLT_CONTROLLER(object); if (self->host_online_timer_id != 0) g_source_remove(self->host_online_timer_id); G_OBJECT_CLASS(fu_thunderbolt_controller_parent_class)->finalize(object); } static void fu_thunderbolt_controller_class_init(FuThunderboltControllerClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->finalize = fu_thunderbolt_controller_finalize; device_class->setup = fu_thunderbolt_controller_setup; device_class->probe = fu_thunderbolt_controller_probe; device_class->to_string = fu_thunderbolt_controller_to_string; device_class->write_firmware = fu_thunderbolt_controller_write_firmware; } fwupd-2.0.10/plugins/thunderbolt/fu-thunderbolt-controller.h000066400000000000000000000006401501337203100242210ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-thunderbolt-device.h" #define FU_TYPE_THUNDERBOLT_CONTROLLER (fu_thunderbolt_controller_get_type()) G_DECLARE_FINAL_TYPE(FuThunderboltController, fu_thunderbolt_controller, FU, THUNDERBOLT_CONTROLLER, FuThunderboltDevice) fwupd-2.0.10/plugins/thunderbolt/fu-thunderbolt-device.c000066400000000000000000000342241501337203100232750ustar00rootroot00000000000000/* * Copyright 2017 Christian J. Kellner * Copyright 2020 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include #include #include #include #include "fu-thunderbolt-common.h" #include "fu-thunderbolt-controller.h" #include "fu-thunderbolt-device.h" typedef struct { const gchar *auth_method; guint retries; } FuThunderboltDevicePrivate; #define TBT_NVM_RETRY_TIMEOUT 200 /* ms */ #define FU_PLUGIN_THUNDERBOLT_UPDATE_TIMEOUT 60000 /* ms */ G_DEFINE_TYPE_WITH_PRIVATE(FuThunderboltDevice, fu_thunderbolt_device, FU_TYPE_UDEV_DEVICE) #define GET_PRIVATE(o) (fu_thunderbolt_device_get_instance_private(o)) void fu_thunderbolt_device_set_retries(FuThunderboltDevice *self, guint retries) { FuThunderboltDevicePrivate *priv = GET_PRIVATE(self); priv->retries = retries; } GFile * fu_thunderbolt_device_find_nvmem(FuThunderboltDevice *self, gboolean active, GError **error) { const gchar *nvmem_dir = active ? "nvm_active" : "nvm_non_active"; const gchar *name; const gchar *devpath = fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(self)); g_autoptr(GDir) d = NULL; if (G_UNLIKELY(devpath == NULL)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Could not determine sysfs path for device"); return NULL; } d = g_dir_open(devpath, 0, error); if (d == NULL) return NULL; while ((name = g_dir_read_name(d)) != NULL) { if (g_str_has_prefix(name, nvmem_dir)) { g_autoptr(GFile) parent = g_file_new_for_path(devpath); g_autoptr(GFile) nvm_dir = g_file_get_child(parent, name); return g_file_get_child(nvm_dir, "nvmem"); } } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Could not find non-volatile memory location"); return NULL; } gboolean fu_thunderbolt_device_check_authorized(FuThunderboltDevice *self, GError **error) { gboolean exists_authorized = FALSE; guint64 status = 0; const gchar *devpath = fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(self)); g_autofree gchar *attribute = NULL; g_autofree gchar *safe_path = g_build_path("/", devpath, "authorized", NULL); /* read directly from file to prevent udev caching */ if (!fu_device_query_file_exists(FU_DEVICE(self), safe_path, &exists_authorized, error)) return FALSE; if (!exists_authorized) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "missing authorized attribute"); return FALSE; } if (!g_file_get_contents(safe_path, &attribute, NULL, error)) return FALSE; if (!fu_strtoull(attribute, &status, 0, G_MAXUINT64, FU_INTEGER_BASE_16, error)) { g_prefix_error(error, "failed to read authorized: "); return FALSE; } if (status == 1 || status == 2) fu_device_uninhibit(FU_DEVICE(self), "not-authorized"); else fu_device_inhibit(FU_DEVICE(self), "not-authorized", "Not authorized"); return TRUE; } gboolean fu_thunderbolt_device_get_version(FuThunderboltDevice *self, GError **error) { FuThunderboltDevicePrivate *priv = GET_PRIVATE(self); const gchar *devpath = fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(self)); guint64 version_major = 0; guint64 version_minor = 0; g_auto(GStrv) split = NULL; g_autofree gchar *version_raw = NULL; g_autofree gchar *version = NULL; /* read directly from file to prevent udev caching */ g_autofree gchar *safe_path = g_build_path("/", devpath, "nvm_version", NULL); if (!g_file_test(safe_path, G_FILE_TEST_EXISTS)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "missing nvm_version attribute"); return FALSE; } for (guint i = 0; i < priv->retries; i++) { g_autoptr(GError) error_local = NULL; /* glib can't return a properly mapped -ENODATA but the * kernel only returns -ENODATA or -EAGAIN */ if (g_file_get_contents(safe_path, &version_raw, NULL, &error_local)) break; g_debug("attempt %u: failed to read NVM version", i); fu_device_sleep(FU_DEVICE(self), TBT_NVM_RETRY_TIMEOUT); /* safe mode probably */ if (g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) break; } if (version_raw == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to read NVM"); return FALSE; } split = g_strsplit(version_raw, ".", -1); if (g_strv_length(split) != 2) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid nvm_version format: %s", version_raw); return FALSE; } if (!fu_strtoull(split[0], &version_major, 0, G_MAXUINT64, FU_INTEGER_BASE_16, error)) return FALSE; if (!fu_strtoull(split[1], &version_minor, 0, G_MAXUINT64, FU_INTEGER_BASE_16, error)) return FALSE; version = g_strdup_printf("%02x.%02x", (guint)version_major, (guint)version_minor); fu_device_set_version(FU_DEVICE(self), version); return TRUE; } static void fu_thunderbolt_device_to_string(FuDevice *device, guint idt, GString *str) { FuThunderboltDevice *self = FU_THUNDERBOLT_DEVICE(device); FuThunderboltDevicePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append(str, idt, "AuthMethod", priv->auth_method); } void fu_thunderbolt_device_set_auth_method(FuThunderboltDevice *self, const gchar *auth_method) { FuThunderboltDevicePrivate *priv = GET_PRIVATE(self); priv->auth_method = auth_method; } static gboolean fu_thunderbolt_device_activate(FuDevice *device, FuProgress *progress, GError **error) { FuUdevDevice *udev = FU_UDEV_DEVICE(device); return fu_udev_device_write_sysfs(udev, "nvm_authenticate", "1", FU_THUNDERBOLT_DEVICE_WRITE_TIMEOUT, error); } static gboolean fu_thunderbolt_device_authenticate(FuDevice *device, GError **error) { FuThunderboltDevice *self = FU_THUNDERBOLT_DEVICE(device); FuThunderboltDevicePrivate *priv = GET_PRIVATE(self); FuUdevDevice *udev = FU_UDEV_DEVICE(device); return fu_udev_device_write_sysfs(udev, priv->auth_method, "1", FU_THUNDERBOLT_DEVICE_WRITE_TIMEOUT, error); } static gboolean fu_thunderbolt_device_flush_update(FuDevice *device, GError **error) { FuThunderboltDevice *self = FU_THUNDERBOLT_DEVICE(device); FuThunderboltDevicePrivate *priv = GET_PRIVATE(self); FuUdevDevice *udev = FU_UDEV_DEVICE(device); return fu_udev_device_write_sysfs(udev, priv->auth_method, "2", FU_THUNDERBOLT_DEVICE_WRITE_TIMEOUT, error); } static gboolean fu_thunderbolt_device_attach(FuDevice *device, FuProgress *progress, GError **error) { g_autofree gchar *attr_nvm_authenticate = NULL; guint64 status = 0; /* now check if the update actually worked */ attr_nvm_authenticate = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(device), "nvm_authenticate", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, error); if (attr_nvm_authenticate == NULL) return FALSE; if (!fu_strtoull(attr_nvm_authenticate, &status, 0, G_MAXUINT64, FU_INTEGER_BASE_16, error)) { g_prefix_error(error, "failed to read nvm_authenticate: "); return FALSE; } /* anything else then 0x0 means we got an error */ if (status != 0x0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "update failed (status %" G_GINT64_MODIFIER "x)", status); return FALSE; } return TRUE; } static gboolean fu_thunderbolt_device_rescan(FuDevice *device, GError **error) { FuThunderboltDevice *self = FU_THUNDERBOLT_DEVICE(device); /* refresh updatability */ if (!fu_thunderbolt_device_check_authorized(self, error)) return FALSE; /* refresh the version */ return fu_thunderbolt_device_get_version(self, error); } static gboolean fu_thunderbolt_device_write_stream(GOutputStream *ostream, GBytes *bytes, FuProgress *progress, GError **error) { gsize bufsz = g_bytes_get_size(bytes); gsize total_written = 0; do { gssize wrote; g_autoptr(GBytes) fw_data = NULL; fw_data = fu_bytes_new_offset(bytes, total_written, bufsz - total_written, error); if (fw_data == NULL) return FALSE; wrote = g_output_stream_write_bytes(ostream, fw_data, NULL, error); if (wrote < 0) return FALSE; total_written += wrote; fu_progress_set_percentage_full(progress, total_written, bufsz); } while (total_written < bufsz); if (total_written != bufsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "only wrote 0x%x of 0x%x", (guint)total_written, (guint)bufsz); return FALSE; } /* success */ return TRUE; } static gboolean fu_thunderbolt_device_write_data(FuThunderboltDevice *self, GBytes *blob_fw, FuProgress *progress, GError **error) { g_autoptr(GFile) nvmem = NULL; g_autoptr(GOutputStream) ostream = NULL; nvmem = fu_thunderbolt_device_find_nvmem(self, FALSE, error); if (nvmem == NULL) return FALSE; ostream = (GOutputStream *)g_file_append_to(nvmem, G_FILE_CREATE_NONE, NULL, error); if (ostream == NULL) return FALSE; if (!fu_thunderbolt_device_write_stream(ostream, blob_fw, progress, error)) return FALSE; return g_output_stream_close(ostream, NULL, error); } static FuFirmware * fu_thunderbolt_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuThunderboltDevice *self = FU_THUNDERBOLT_DEVICE(device); g_autoptr(FuFirmware) firmware = NULL; /* parse */ firmware = fu_firmware_new_from_gtypes(stream, 0x0, flags, error, FU_TYPE_INTEL_THUNDERBOLT_FIRMWARE, FU_TYPE_FIRMWARE, G_TYPE_INVALID); if (firmware == NULL) return NULL; /* get current NVMEM */ if (fu_firmware_has_flag(firmware, FU_FIRMWARE_FLAG_HAS_CHECK_COMPATIBLE)) { g_autoptr(FuFirmware) firmware_old = NULL; g_autoptr(GFile) nvmem = NULL; g_autoptr(GInputStream) controller_fw = NULL; fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ); nvmem = fu_thunderbolt_device_find_nvmem(self, TRUE, error); if (nvmem == NULL) return NULL; controller_fw = G_INPUT_STREAM(g_file_read(nvmem, NULL, error)); if (controller_fw == NULL) return NULL; firmware_old = fu_firmware_new_from_gtypes(controller_fw, 0x0, flags, error, FU_TYPE_INTEL_THUNDERBOLT_NVM, FU_TYPE_FIRMWARE, G_TYPE_INVALID); if (firmware_old == NULL) return NULL; if (!fu_firmware_check_compatible(firmware_old, firmware, flags, error)) return NULL; } /* success */ return g_steal_pointer(&firmware); } static gboolean fu_thunderbolt_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuThunderboltDevice *self = FU_THUNDERBOLT_DEVICE(device); g_autoptr(GBytes) blob_fw = NULL; /* get default image */ blob_fw = fu_firmware_get_bytes(firmware, error); if (blob_fw == NULL) return FALSE; fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); if (!fu_thunderbolt_device_write_data(self, blob_fw, progress, error)) { g_prefix_error(error, "could not write firmware to thunderbolt device at %s: ", fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(self))); return FALSE; } /* flush the image if supported by kernel and/or device */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE)) { if (!fu_thunderbolt_device_flush_update(device, error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION); } /* using an active delayed activation flow later (either shutdown or another plugin) */ if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_SKIPS_RESTART)) { g_debug("skipping Thunderbolt reset per quirk request"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION); return TRUE; } /* authenticate (possibly on unplug if device supports it) */ if (!fu_thunderbolt_device_authenticate(FU_DEVICE(self), error)) { g_prefix_error(error, "could not start thunderbolt device upgrade: "); return FALSE; } /* whether to wait for a device replug or not */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE)) { fu_device_set_remove_delay(device, FU_PLUGIN_THUNDERBOLT_UPDATE_TIMEOUT); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_RESTART); } return TRUE; } static gboolean fu_thunderbolt_device_probe(FuDevice *device, GError **error) { g_autoptr(FuDevice) udev_parent = NULL; /* if the PCI ID is Intel then it's signed, no idea otherwise */ udev_parent = fu_device_get_backend_parent_with_subsystem(device, "pci", NULL); if (udev_parent != NULL) { if (!fu_device_probe(udev_parent, error)) return FALSE; if (fu_device_get_vid(udev_parent) == 0x8086) fu_device_add_flag(device, FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); } /* success */ return TRUE; } static void fu_thunderbolt_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_thunderbolt_device_init(FuThunderboltDevice *self) { FuThunderboltDevicePrivate *priv = GET_PRIVATE(self); priv->auth_method = "nvm_authenticate"; priv->retries = 50; fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_THUNDERBOLT); fu_device_add_protocol(FU_DEVICE(self), "com.intel.thunderbolt"); } static void fu_thunderbolt_device_class_init(FuThunderboltDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->activate = fu_thunderbolt_device_activate; device_class->to_string = fu_thunderbolt_device_to_string; device_class->probe = fu_thunderbolt_device_probe; device_class->prepare_firmware = fu_thunderbolt_device_prepare_firmware; device_class->write_firmware = fu_thunderbolt_device_write_firmware; device_class->attach = fu_thunderbolt_device_attach; device_class->rescan = fu_thunderbolt_device_rescan; device_class->set_progress = fu_thunderbolt_device_set_progress; } fwupd-2.0.10/plugins/thunderbolt/fu-thunderbolt-device.h000066400000000000000000000016571501337203100233060ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2020 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_THUNDERBOLT_DEVICE (fu_thunderbolt_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuThunderboltDevice, fu_thunderbolt_device, FU, THUNDERBOLT_DEVICE, FuUdevDevice) struct _FuThunderboltDeviceClass { FuUdevDeviceClass parent_class; }; gboolean fu_thunderbolt_device_get_version(FuThunderboltDevice *self, GError **error); GFile * fu_thunderbolt_device_find_nvmem(FuThunderboltDevice *self, gboolean active, GError **error); gboolean fu_thunderbolt_device_check_authorized(FuThunderboltDevice *self, GError **error); void fu_thunderbolt_device_set_auth_method(FuThunderboltDevice *self, const gchar *auth_method); void fu_thunderbolt_device_set_retries(FuThunderboltDevice *self, guint retries); fwupd-2.0.10/plugins/thunderbolt/fu-thunderbolt-plugin.c000066400000000000000000000111631501337203100233310ustar00rootroot00000000000000/* * Copyright 2017 Christian J. Kellner * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-thunderbolt-common.h" #include "fu-thunderbolt-controller.h" #include "fu-thunderbolt-plugin.h" #include "fu-thunderbolt-retimer.h" struct _FuThunderboltPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuThunderboltPlugin, fu_thunderbolt_plugin, FU_TYPE_PLUGIN) static gboolean fu_thunderbolt_plugin_safe_kernel(FuPlugin *plugin, GError **error) { g_autofree gchar *min = fu_plugin_get_config_value(plugin, "MinimumKernelVersion"); return fu_kernel_check_version(min, error); } static gboolean fu_thunderbolt_plugin_device_created(FuPlugin *plugin, FuDevice *dev, GError **error) { FuContext *ctx = fu_plugin_get_context(plugin); fu_plugin_add_rule(plugin, FU_PLUGIN_RULE_INHIBITS_IDLE, "thunderbolt requires device wakeup"); if (fu_context_has_hwid_flag(ctx, "retimer-offline-mode")) fu_device_add_private_flag(dev, FU_THUNDERBOLT_DEVICE_FLAG_FORCE_ENUMERATION); return TRUE; } static void fu_thunderbolt_plugin_device_registered(FuPlugin *plugin, FuDevice *device) { if (g_strcmp0(fu_device_get_plugin(device), "thunderbolt") != 0) return; /* Operating system will handle finishing updates later */ if (fu_plugin_get_config_value_boolean(plugin, "DelayedActivation") && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE)) { g_info("turning on delayed activation for %s", fu_device_get_name(device)); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_SKIPS_RESTART); fu_device_remove_private_flag(device, FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); } } static gboolean fu_thunderbolt_plugin_startup(FuPlugin *plugin, FuProgress *progress, GError **error) { return fu_thunderbolt_plugin_safe_kernel(plugin, error); } static gboolean fu_thunderbolt_plugin_composite_prepare(FuPlugin *plugin, GPtrArray *devices, GError **error) { for (guint i = 0; i < devices->len; i++) { FuDevice *dev = g_ptr_array_index(devices, i); if ((g_strcmp0(fu_device_get_plugin(dev), "thunderbolt") == 0) && fu_device_has_private_flag(dev, FU_THUNDERBOLT_DEVICE_FLAG_FORCE_ENUMERATION) && fu_device_has_private_flag(dev, FU_DEVICE_PRIVATE_FLAG_NO_AUTO_REMOVE)) { return fu_thunderbolt_retimer_set_parent_port_offline(dev, error); } } return TRUE; } static gboolean fu_thunderbolt_plugin_composite_cleanup(FuPlugin *plugin, GPtrArray *devices, GError **error) { for (guint i = 0; i < devices->len; i++) { FuDevice *dev = g_ptr_array_index(devices, i); if ((g_strcmp0(fu_device_get_plugin(dev), "thunderbolt") == 0) && fu_device_has_private_flag(dev, FU_THUNDERBOLT_DEVICE_FLAG_FORCE_ENUMERATION) && fu_device_has_private_flag(dev, FU_DEVICE_PRIVATE_FLAG_NO_AUTO_REMOVE)) { fu_device_sleep(dev, FU_THUNDERBOLT_RETIMER_CLEANUP_DELAY); return fu_thunderbolt_retimer_set_parent_port_online(dev, error); } } return TRUE; } static gboolean fu_thunderbolt_plugin_modify_config(FuPlugin *plugin, const gchar *key, const gchar *value, GError **error) { const gchar *keys[] = {"DelayedActivation", "MinimumKernelVersion", NULL}; if (!g_strv_contains(keys, key)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "config key %s not supported", key); return FALSE; } return fu_plugin_set_config_value(plugin, key, value, error); } static void fu_thunderbolt_plugin_init(FuThunderboltPlugin *self) { } static void fu_thunderbolt_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_udev_subsystem(plugin, "thunderbolt"); fu_plugin_add_device_gtype(plugin, FU_TYPE_THUNDERBOLT_CONTROLLER); fu_plugin_add_device_gtype(plugin, FU_TYPE_THUNDERBOLT_RETIMER); /* defaults changed here will also be reflected in the fwupd.conf man page */ fu_plugin_set_config_default(plugin, "DelayedActivation", "false"); fu_plugin_set_config_default(plugin, "MinimumKernelVersion", "4.13.0"); } static void fu_thunderbolt_plugin_class_init(FuThunderboltPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_thunderbolt_plugin_constructed; plugin_class->startup = fu_thunderbolt_plugin_startup; plugin_class->device_registered = fu_thunderbolt_plugin_device_registered; plugin_class->device_created = fu_thunderbolt_plugin_device_created; plugin_class->composite_prepare = fu_thunderbolt_plugin_composite_prepare; plugin_class->composite_cleanup = fu_thunderbolt_plugin_composite_cleanup; plugin_class->modify_config = fu_thunderbolt_plugin_modify_config; } fwupd-2.0.10/plugins/thunderbolt/fu-thunderbolt-plugin.h000066400000000000000000000003731501337203100233370ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuThunderboltPlugin, fu_thunderbolt_plugin, FU, THUNDERBOLT_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/thunderbolt/fu-thunderbolt-retimer.c000066400000000000000000000112371501337203100235040ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * Copyright 2017 Christian J. Kellner * Copyright 2020 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-thunderbolt-common.h" #include "fu-thunderbolt-retimer.h" struct _FuThunderboltRetimer { FuThunderboltDevice parent_instance; }; G_DEFINE_TYPE(FuThunderboltRetimer, fu_thunderbolt_retimer, FU_TYPE_THUNDERBOLT_DEVICE) gboolean fu_thunderbolt_retimer_set_parent_port_offline(FuDevice *device, GError **error) { g_autoptr(FuDevice) parent = fu_device_get_backend_parent_with_subsystem(device, "thunderbolt:thunderbolt_domain", error); if (parent == NULL) return FALSE; if (!fu_thunderbolt_udev_set_port_offline(FU_UDEV_DEVICE(parent), error)) return FALSE; return fu_thunderbolt_udev_rescan_port(FU_UDEV_DEVICE(parent), error); } gboolean fu_thunderbolt_retimer_set_parent_port_online(FuDevice *device, GError **error) { g_autoptr(FuDevice) parent = fu_device_get_backend_parent_with_subsystem(device, "thunderbolt:thunderbolt_domain", error); if (parent == NULL) return FALSE; return fu_thunderbolt_udev_set_port_online(FU_UDEV_DEVICE(parent), error); } static gboolean fu_thunderbolt_retimer_probe(FuDevice *device, GError **error) { const gchar *devpath = fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device)); g_autofree gchar *physical_id = g_path_get_basename(devpath); /* device */ if (physical_id != NULL) fu_device_set_physical_id(device, physical_id); return TRUE; } static gboolean fu_thunderbolt_retimer_reload(FuDevice *device, GError **error) { /* get version */ if (!fu_thunderbolt_udev_rescan_port(FU_UDEV_DEVICE(device), error)) return FALSE; if (!fu_thunderbolt_device_get_version(FU_THUNDERBOLT_DEVICE(device), error)) return FALSE; /* success */ return TRUE; } static gboolean fu_thunderbolt_retimer_attach(FuDevice *device, FuProgress *progress, GError **error) { /* FuThunderboltDevice->attach for nvm_authenticate */ if (!FU_DEVICE_CLASS(fu_thunderbolt_retimer_parent_class)->attach(device, progress, error)) return FALSE; /* online */ fu_device_sleep(device, FU_THUNDERBOLT_RETIMER_CLEANUP_DELAY); if (!fu_thunderbolt_retimer_set_parent_port_online(device, error)) return FALSE; /* retimer gets removed, which we ignore, due to no-auto-remove */ fu_device_sleep(device, 1000); /* get the new retimer firmware version by rescanning */ if (!fu_thunderbolt_retimer_set_parent_port_offline(device, error)) return FALSE; /* wait for it to re-appear */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_thunderbolt_retimer_setup(FuDevice *device, GError **error) { FuThunderboltRetimer *self = FU_THUNDERBOLT_RETIMER(device); guint16 did; guint16 vid; g_autofree gchar *instance = NULL; /* get version */ if (!fu_thunderbolt_device_get_version(FU_THUNDERBOLT_DEVICE(self), error)) return FALSE; /* as defined in PCIe 4.0 spec */ vid = fu_device_get_vid(FU_DEVICE(self)); if (vid == 0x0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "missing vendor id"); return FALSE; } did = fu_device_get_pid(device); if (did == 0x0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "missing device id"); return FALSE; } instance = g_strdup_printf("TBT-%04x%04x-retimer%s", (guint)vid, (guint)did, fu_device_get_physical_id(device)); fu_device_add_instance_id(device, instance); /* success */ return TRUE; } static void fu_thunderbolt_retimer_init(FuThunderboltRetimer *self) { fu_device_set_name(FU_DEVICE(self), "USB4 Retimer"); fu_device_set_summary( FU_DEVICE(self), "A physical layer protocol-aware, software-transparent extension device " "that forms two separate electrical link segments"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_NO_AUTO_REMOVE); fu_thunderbolt_device_set_retries(FU_THUNDERBOLT_DEVICE(self), 1); } static void fu_thunderbolt_retimer_class_init(FuThunderboltRetimerClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->attach = fu_thunderbolt_retimer_attach; device_class->setup = fu_thunderbolt_retimer_setup; device_class->reload = fu_thunderbolt_retimer_reload; device_class->probe = fu_thunderbolt_retimer_probe; } fwupd-2.0.10/plugins/thunderbolt/fu-thunderbolt-retimer.h000066400000000000000000000014071501337203100235070ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * Copyright 2020 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-thunderbolt-device.h" #define FU_TYPE_THUNDERBOLT_RETIMER (fu_thunderbolt_retimer_get_type()) G_DECLARE_FINAL_TYPE(FuThunderboltRetimer, fu_thunderbolt_retimer, FU, THUNDERBOLT_RETIMER, FuThunderboltDevice) /* 5 seconds sleep until retimer is available after nvm update */ #define FU_THUNDERBOLT_RETIMER_CLEANUP_DELAY 5000 /* ms */ gboolean fu_thunderbolt_retimer_set_parent_port_offline(FuDevice *device, GError **error); gboolean fu_thunderbolt_retimer_set_parent_port_online(FuDevice *device, GError **error); fwupd-2.0.10/plugins/thunderbolt/fu-thunderbolt.rs000066400000000000000000000003271501337203100222370ustar00rootroot00000000000000// Copyright 2024 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString, FromString)] enum FuThunderboltControllerKind { Unknown, Host, Device, Hub, } fwupd-2.0.10/plugins/thunderbolt/meson.build000066400000000000000000000013571501337203100210750ustar00rootroot00000000000000if host_machine.system() == 'linux' and (host_cpu == 'x86' or host_cpu == 'x86_64') cargs = ['-DG_LOG_DOMAIN="FuPluginThunderbolt"'] plugin_quirks += files('thunderbolt.quirk') plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_builtin_thunderbolt = static_library('fu_plugin_thunderbolt', rustgen.process('fu-thunderbolt.rs'), sources: [ 'fu-thunderbolt-plugin.c', 'fu-thunderbolt-common.c', 'fu-thunderbolt-device.c', 'fu-thunderbolt-retimer.c', 'fu-thunderbolt-controller.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) plugin_builtins += plugin_builtin_thunderbolt device_tests += files( 'tests/caldigit-ts4-tbt.json', ) endif fwupd-2.0.10/plugins/thunderbolt/tests/000077500000000000000000000000001501337203100200675ustar00rootroot00000000000000fwupd-2.0.10/plugins/thunderbolt/tests/COPYING000066400000000000000000000027321501337203100211260ustar00rootroot00000000000000Copyright(c) 2017 Intel Corporation Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Intel Corporation nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. fwupd-2.0.10/plugins/thunderbolt/tests/caldigit-ts4-tbt.json000066400000000000000000000005471501337203100240470ustar00rootroot00000000000000{ "name": "CalDigit Element Hub", "interactive": false, "steps": [ { "url": "78f0eb25da781a88a2a9a1191a32b6c7f8c8a61664a42dcb6b156a1b87d525e4-CalDigit-TS4_TBT39.cab", "components": [ { "version": "39.82", "guids": [ "6c67279d-c348-5610-a7ee-0f97836a898d" ] } ] } ] } fwupd-2.0.10/plugins/thunderbolt/tests/colorhug.txt000077700000000000000000000000001501337203100337022../../../libfwupdplugin/tests/colorhug/firmware.txtustar00rootroot00000000000000fwupd-2.0.10/plugins/thunderbolt/tests/minimal-fw-controller.builder.xml000066400000000000000000000004731501337203100264630ustar00rootroot00000000000000 0x8086 0x15d3 0x1234 alpine-ridge-c true false 0x3 0x2 false fwupd-2.0.10/plugins/thunderbolt/tests/minimal-fw.builder.xml000066400000000000000000000005261501337203100243010ustar00rootroot00000000000000 0x8 0x8086 0x15d3 0x1234 alpine-ridge-c true false 0x3 0x2 true fwupd-2.0.10/plugins/thunderbolt/thunderbolt.quirk000066400000000000000000000006731501337203100223420ustar00rootroot00000000000000[THUNDERBOLT\TYPE_THUNDERBOLT_DEVICE] Plugin = thunderbolt GType = FuThunderboltController [THUNDERBOLT\TYPE_THUNDERBOLT_RETIMER] Plugin = thunderbolt GType = FuThunderboltRetimer # Google Redrix [​af1d04d1-fbe9-5197-973d-fdc9310a33bd] Flags = retimer-offline-mode # Google Rex [​​fe682559-28dc-58b3-bf61-aa8414d9f363] Flags = retimer-offline-mode # Google Screebo [967000fc-42df-5a17-a0a2-70a43738e78b] Flags = retimer-offline-mode fwupd-2.0.10/plugins/ti-tps6598x/000077500000000000000000000000001501337203100163375ustar00rootroot00000000000000fwupd-2.0.10/plugins/ti-tps6598x/README.md000066400000000000000000000022421501337203100176160ustar00rootroot00000000000000--- title: Plugin: TI TPS6598x --- ## Introduction The TPS65982DMC is a dock management controller for docks, hubs, and monitors implementing TI PD controllers. Suitable Power Delivery (PD) Controller devices include TPS6598x which are updated as part of the DMC firmware. There may be multiple PD devices attached to each DMC device. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob as a SHA-256+RSA-3072 signed binary file. This plugin supports the following protocol ID: * `com.ti.tps6598x` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_0451&PID_ACE1` (enforce-requires) Child devices also have an additional instance IDs which corresponds to the index, e.g. * `USB\VID_2188&PID_5988&REV_0714&PD_00` * `USB\VID_2188&PID_5988&PD_00` ## Update Behavior The device usually presents in runtime mode. ## Vendor ID Security The vendor ID is set from the USB vendor, in this instance set to `USB:0x0451` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.8.9`. fwupd-2.0.10/plugins/ti-tps6598x/fu-ti-tps6598x-common.c000066400000000000000000000006551501337203100223530ustar00rootroot00000000000000/* * Copyright 2021 Texas Instruments Incorporated * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-ti-tps6598x-common.h" gboolean fu_ti_tps6598x_byte_array_is_nonzero(GByteArray *buf) { if (buf->len == 0) return FALSE; for (guint j = 1; j < buf->len; j++) { if (buf->data[j] != 0x0) return TRUE; } return FALSE; } fwupd-2.0.10/plugins/ti-tps6598x/fu-ti-tps6598x-common.h000066400000000000000000000030131501337203100223470ustar00rootroot00000000000000/* * Copyright 2021 Texas Instruments Incorporated * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include /* registers */ #define TI_TPS6598X_REGISTER_TBT_VID 0x00 /* ro, 4 bytes -- Intel assigned */ #define TI_TPS6598X_REGISTER_TBT_DID 0x01 /* ro, 4 bytes -- Intel assigned */ #define TI_TPS6598X_REGISTER_PROTO_VER 0x02 /* ro, 4 bytes */ #define TI_TPS6598X_REGISTER_MODE 0x03 /* ro, 4 bytes */ #define TI_TPS6598X_REGISTER_TYPE 0x04 /* ro, 4 bytes */ #define TI_TPS6598X_REGISTER_UID 0x05 /* ro, 16 bytes */ #define TI_TPS6598X_REGISTER_OUID 0x06 /* ro, 8 bytes */ #define TI_TPS6598X_REGISTER_CMD1 0x08 /* ro, 4CC */ #define TI_TPS6598X_REGISTER_DATA1 0x09 /* rw, 64 bytes */ #define TI_TPS6598X_REGISTER_VERSION 0x0F /* rw, 4 bytes */ #define TI_TPS6598X_REGISTER_CMD2 0x10 /* ro, 4CC */ #define TI_TPS6598X_REGISTER_DATA2 0x11 /* rw, 64 bytes */ #define TI_TPS6598X_REGISTER_CMD3 0x1E /* ro, variable */ #define TI_TPS6598X_REGISTER_DATA3 0x1F /* ro, variable */ #define TI_TPS6598X_REGISTER_OTP_CONFIG 0x2D /* ro, 12 bytes */ #define TI_TPS6598X_REGISTER_BUILD_IDENTIFIER 0x2E /* ro, 64 bytes */ #define TI_TPS6598X_REGISTER_DEVICE_INFO 0x2F /* ro, 47 bytes */ #define TI_TPS6598X_REGISTER_TX_IDENTITY 0x47 /* rw, 49 bytes */ #define FU_TI_TPS6598X_PD_MAX 2 /* devices */ gboolean fu_ti_tps6598x_byte_array_is_nonzero(GByteArray *buf); fwupd-2.0.10/plugins/ti-tps6598x/fu-ti-tps6598x-device.c000066400000000000000000000555171501337203100223310ustar00rootroot00000000000000/* * Copyright 2021 Texas Instruments Incorporated * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-ti-tps6598x-common.h" #include "fu-ti-tps6598x-device.h" #include "fu-ti-tps6598x-firmware.h" #include "fu-ti-tps6598x-pd-device.h" #include "fu-ti-tps6598x-struct.h" struct _FuTiTps6598xDevice { FuUsbDevice parent_instance; gchar *uid; gchar *ouid; }; G_DEFINE_TYPE(FuTiTps6598xDevice, fu_ti_tps6598x_device, FU_TYPE_USB_DEVICE) #define TI_TPS6598X_DEVICE_USB_TIMEOUT 2000 /* ms */ /* command types in USB messages from PC to device */ #define TI_TPS6598X_USB_REQUEST_WRITE 0xFD #define TI_TPS6598X_USB_REQUEST_READ 0xFE #define TI_TPS6598X_USB_BUFFER_SIZE 8 /* bytes */ static void fu_ti_tps6598x_device_to_string(FuDevice *device, guint idt, GString *str) { FuTiTps6598xDevice *self = FU_TI_TPS6598X_DEVICE(device); fwupd_codec_string_append(str, idt, "UID", self->uid); fwupd_codec_string_append(str, idt, "oUID", self->ouid); } /* read @length bytes from address @addr */ static GByteArray * fu_ti_tps6598x_device_usbep_read_raw(FuTiTps6598xDevice *self, guint16 addr, guint8 length, GError **error) { gsize actual_length = 0; g_autofree gchar *title = g_strdup_printf("read@0x%x", addr); g_autoptr(GByteArray) buf = g_byte_array_new(); /* first byte is length */ fu_byte_array_set_size(buf, length + 1, 0x0); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, TI_TPS6598X_USB_REQUEST_READ, addr, 0x0, /* idx */ buf->data, buf->len, &actual_length, TI_TPS6598X_DEVICE_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to contact device: "); return NULL; } fu_dump_raw(G_LOG_DOMAIN, title, buf->data, buf->len); if (actual_length != buf->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "got 0x%x but requested 0x%x", (guint)actual_length, (guint)buf->len); return NULL; } /* success */ return g_steal_pointer(&buf); } /* read @length bytes from address @addr */ static GByteArray * fu_ti_tps6598x_device_usbep_read(FuTiTps6598xDevice *self, guint16 addr, guint8 length, GError **error) { g_autoptr(GByteArray) buf = NULL; /* first byte is length */ buf = fu_ti_tps6598x_device_usbep_read_raw(self, addr, length, error); if (buf == NULL) return NULL; /* check then remove size */ if (buf->data[0] < length) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "response 0x%x but requested 0x%x", (guint)buf->data[0], (guint)length); return NULL; } g_byte_array_remove_index(buf, 0); /* success */ return g_steal_pointer(&buf); } static gboolean fu_ti_tps6598x_device_usbep_write(FuTiTps6598xDevice *self, guint16 addr, GByteArray *buf, GError **error) { g_autoptr(GPtrArray) chunks = NULL; g_autofree gchar *title = g_strdup_printf("write@0x%x", addr); fu_dump_raw(G_LOG_DOMAIN, title, buf->data, buf->len); chunks = fu_chunk_array_mutable_new(buf->data, buf->len, 0x0, 0x0, TI_TPS6598X_USB_BUFFER_SIZE); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); guint16 idx = 0; gsize actual_length = 0; /* for the first chunk use the total data length */ if (i == 0) idx = buf->len; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, TI_TPS6598X_USB_REQUEST_WRITE, addr, idx, /* idx */ fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), &actual_length, TI_TPS6598X_DEVICE_USB_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to contact device: "); return FALSE; } if (actual_length != fu_chunk_get_data_sz(chk)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "wrote 0x%x but expected 0x%x", (guint)actual_length, (guint)fu_chunk_get_data_sz(chk)); return FALSE; } } /* success */ return TRUE; } /* read the specified DATA register */ static GByteArray * fu_ti_tps6598x_device_read_data(FuTiTps6598xDevice *self, gsize bufsz, GError **error) { g_autoptr(GByteArray) buf = fu_ti_tps6598x_device_usbep_read(self, TI_TPS6598X_REGISTER_DATA3, bufsz, error); if (buf == NULL) { g_prefix_error(error, "failed to read data at 0x%x: ", (guint)TI_TPS6598X_REGISTER_DATA3); return NULL; } return g_steal_pointer(&buf); } /* write to the DATA register */ static gboolean fu_ti_tps6598x_device_write_data(FuTiTps6598xDevice *self, GByteArray *buf, GError **error) { if (!fu_ti_tps6598x_device_usbep_write(self, TI_TPS6598X_REGISTER_DATA3, buf, error)) { g_prefix_error(error, "failed to write data at 0x%x: ", (guint)TI_TPS6598X_REGISTER_DATA3); return FALSE; } return TRUE; } static gboolean fu_ti_tps6598x_device_write_4cc(FuTiTps6598xDevice *self, const gchar *cmd, GByteArray *data, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); /* sanity check */ if (strlen(cmd) != 4) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "expected 4-char cmd"); return FALSE; } if (data != NULL) { if (!fu_ti_tps6598x_device_write_data(self, data, error)) return FALSE; } for (guint i = 0; i < 4; i++) fu_byte_array_append_uint8(buf, cmd[i]); return fu_ti_tps6598x_device_usbep_write(self, TI_TPS6598X_REGISTER_CMD3, buf, error); } static gboolean fu_ti_tps6598x_device_reset_hard(FuTiTps6598xDevice *self, GError **error) { return fu_ti_tps6598x_device_write_4cc(self, "GAID", NULL, error); } static gboolean fu_ti_tps6598x_device_wait_for_command_cb(FuDevice *device, gpointer user_data, GError **error) { FuTiTps6598xDevice *self = FU_TI_TPS6598X_DEVICE(device); g_autoptr(GByteArray) buf = NULL; /* 4 bytes of data and the first byte is length */ buf = fu_ti_tps6598x_device_usbep_read(self, TI_TPS6598X_REGISTER_CMD3, 4, error); if (buf == NULL) return FALSE; /* check the value of the cmd register */ if (buf->data[0] != 0 || buf->data[1] != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid status register, got 0x%02x:0x%02x", buf->data[1], buf->data[2]); return FALSE; } /* success */ return TRUE; } /* wait for a 4CC command to complete, defaults for count is 15ms, delay 100ms */ static gboolean fu_ti_tps6598x_device_wait_for_command(FuTiTps6598xDevice *self, guint count, guint delay, GError **error) { return fu_device_retry_full(FU_DEVICE(self), fu_ti_tps6598x_device_wait_for_command_cb, count, delay, NULL, error); } static gboolean fu_ti_tps6598x_device_target_reboot(FuTiTps6598xDevice *self, guint8 slaveNum, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); fu_byte_array_append_uint8(buf, slaveNum); fu_byte_array_append_uint8(buf, 0); if (!fu_ti_tps6598x_device_write_4cc(self, "DSRT", buf, error)) return FALSE; return fu_ti_tps6598x_device_wait_for_command(self, 15, 100, error); } static gboolean fu_ti_tps6598x_device_maybe_reboot(FuTiTps6598xDevice *self, GError **error) { /* reset the targets first */ if (!fu_ti_tps6598x_device_target_reboot(self, 0, error)) return FALSE; if (!fu_ti_tps6598x_device_target_reboot(self, 1, error)) return FALSE; return fu_ti_tps6598x_device_reset_hard(self, error); } /* prepare device to receive the upcoming data transactions */ static gboolean fu_ti_tps6598x_device_sfwi(FuTiTps6598xDevice *self, GError **error) { guint8 res; g_autoptr(GByteArray) buf = NULL; if (!fu_ti_tps6598x_device_write_4cc(self, "SFWi", NULL, error)) return FALSE; if (!fu_ti_tps6598x_device_wait_for_command(self, 15, 100, error)) return FALSE; buf = fu_ti_tps6598x_device_read_data(self, 6, error); if (buf == NULL) return FALSE; res = buf->data[0] & 0b1111; if (res != FU_TI_TPS6598X_SFWI_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "SFWi failed, got %s [0x%02x]", fu_ti_tps6598x_sfwi_to_string(res), res); return FALSE; } /* success */ g_debug("prod-key-present: %u", (guint)(buf->data[2] & 0b00010) >> 1); g_debug("engr-key-present: %u", (guint)(buf->data[2] & 0b00100) >> 2); g_debug("new-flash-region: %u", (guint)(buf->data[2] & 0b11000) >> 3); return TRUE; } /* provide device with the next 64 bytes to be flashed into the SPI */ static gboolean fu_ti_tps6598x_device_sfwd(FuTiTps6598xDevice *self, GByteArray *data, GError **error) { guint8 res; g_autoptr(GByteArray) buf = NULL; if (!fu_ti_tps6598x_device_write_4cc(self, "SFWd", data, error)) return FALSE; if (!fu_ti_tps6598x_device_wait_for_command(self, 15, 100, error)) return FALSE; buf = fu_ti_tps6598x_device_read_data(self, 1, error); if (buf == NULL) return FALSE; res = buf->data[0] & 0b1111; if (res != FU_TI_TPS6598X_SFWD_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "SFWd failed, got %s [0x%02x]", fu_ti_tps6598x_sfwd_to_string(res), res); return FALSE; } /* success */ g_debug("more-data-expected: %i", (buf->data[0] & 0x80) > 0); return TRUE; } /* pass the image signature information to device for verification of the data */ static gboolean fu_ti_tps6598x_device_sfws(FuTiTps6598xDevice *self, GByteArray *data, GError **error) { guint8 res; g_autoptr(GByteArray) buf = NULL; if (!fu_ti_tps6598x_device_write_4cc(self, "SFWs", data, error)) return FALSE; if (!fu_ti_tps6598x_device_wait_for_command(self, 300, 1000, error)) return FALSE; buf = fu_ti_tps6598x_device_read_data(self, 10, error); if (buf == NULL) return FALSE; res = buf->data[0] & 0b1111; if (res != FU_TI_TPS6598X_SFWS_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "SFWs failed, got %s [0x%02x]", fu_ti_tps6598x_sfws_to_string(res), res); return FALSE; } /* success */ g_debug("more-data-expected: %i", (buf->data[0] & 0x80) > 0); g_debug("signature-data-block: %u", (guint)buf->data[1]); g_debug("prod-key-present: %u", (guint)(buf->data[2] & 0b00010) >> 1); g_debug("engr-key-present: %u", (guint)(buf->data[2] & 0b00100) >> 2); g_debug("new-flash-region: %u", (guint)(buf->data[2] & 0b11000) >> 3); g_debug("hash-match: %u", (guint)(buf->data[2] & 0b1100000) >> 5); return TRUE; } GByteArray * fu_ti_tps6598x_device_read_target_register(FuTiTps6598xDevice *self, guint8 target, guint8 addr, guint8 length, GError **error) { g_autoptr(GByteArray) buf = NULL; g_autoptr(GByteArray) data = g_byte_array_new(); fu_byte_array_append_uint8(data, target); fu_byte_array_append_uint8(data, addr); fu_byte_array_append_uint8(data, length); if (!fu_ti_tps6598x_device_write_4cc(self, "DSRD", data, error)) return NULL; if (!fu_ti_tps6598x_device_wait_for_command(self, 300, 1000, error)) return NULL; buf = fu_ti_tps6598x_device_read_data(self, length + 1, error); if (buf == NULL) return NULL; /* check then remove response code */ if (buf->data[0] != 0x00) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "response code 0x%02x", (guint)buf->data[0]); return NULL; } g_byte_array_remove_index(buf, 0); /* success */ return g_steal_pointer(&buf); } static gboolean fu_ti_tps6598x_device_ensure_version(FuTiTps6598xDevice *self, GError **error) { g_autofree gchar *str = NULL; g_autoptr(GByteArray) buf = NULL; /* get bcdVersion */ buf = fu_ti_tps6598x_device_usbep_read(self, TI_TPS6598X_REGISTER_VERSION, 4, error); if (buf == NULL) return FALSE; str = g_strdup_printf("%X.%X.%X", buf->data[2], buf->data[1], buf->data[0]); fu_device_set_version(FU_DEVICE(self), str); return TRUE; } static gboolean fu_ti_tps6598x_device_ensure_mode(FuTiTps6598xDevice *self, GError **error) { g_autofree gchar *str = NULL; g_autoptr(GByteArray) buf = NULL; buf = fu_ti_tps6598x_device_usbep_read(self, TI_TPS6598X_REGISTER_MODE, 4, error); if (buf == NULL) return FALSE; /* ensure in recognized mode */ str = fu_memstrsafe(buf->data, buf->len, 0x0, 4, error); if (str == NULL) return FALSE; if (g_strcmp0(str, "APP ") == 0) { fu_device_remove_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); return TRUE; } if (g_strcmp0(str, "BOOT") == 0) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); return TRUE; } /* unhandled */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device in unknown mode: %s", str); return FALSE; } static gboolean fu_ti_tps6598x_device_ensure_uid(FuTiTps6598xDevice *self, GError **error) { g_autoptr(GByteArray) buf = fu_ti_tps6598x_device_usbep_read(self, TI_TPS6598X_REGISTER_UID, 16, error); if (buf == NULL) return FALSE; g_free(self->uid); self->uid = fu_byte_array_to_string(buf); return TRUE; } static gboolean fu_ti_tps6598x_device_ensure_ouid(FuTiTps6598xDevice *self, GError **error) { g_autoptr(GByteArray) buf = fu_ti_tps6598x_device_usbep_read(self, TI_TPS6598X_REGISTER_OUID, 8, error); if (buf == NULL) return FALSE; g_free(self->ouid); self->ouid = fu_byte_array_to_string(buf); return TRUE; } static gboolean fu_ti_tps6598x_device_setup(FuDevice *device, GError **error) { FuTiTps6598xDevice *self = FU_TI_TPS6598X_DEVICE(device); /* there are two devices with the same VID:PID -- ignore the non-vendor one */ if (fu_usb_device_get_class(FU_USB_DEVICE(self)) != FU_USB_CLASS_VENDOR_SPECIFIC) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "non-vendor specific interface ignored"); return FALSE; } /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_ti_tps6598x_device_parent_class)->setup(device, error)) return FALSE; /* get hardware details */ if (!fu_ti_tps6598x_device_ensure_version(self, error)) { g_prefix_error(error, "failed to read version: "); return FALSE; } if (!fu_ti_tps6598x_device_ensure_mode(self, error)) { g_prefix_error(error, "failed to read mode: "); return FALSE; } if (!fu_ti_tps6598x_device_ensure_uid(self, error)) { g_prefix_error(error, "failed to read UID: "); return FALSE; } if (!fu_ti_tps6598x_device_ensure_ouid(self, error)) { g_prefix_error(error, "failed to read oUID: "); return FALSE; } /* create child PD devices */ for (guint i = 0; i < FU_TI_TPS6598X_PD_MAX; i++) { g_autoptr(FuDevice) device_pd = fu_ti_tps6598x_pd_device_new(device, i); fu_device_add_child(device, device_pd); } /* success */ return TRUE; } static void fu_ti_tps6598x_device_report_metadata_pre(FuDevice *device, GHashTable *metadata) { FuTiTps6598xDevice *self = FU_TI_TPS6598X_DEVICE(device); for (guint i = 0; i < 0xFF; i++) { g_autoptr(GByteArray) buf = NULL; g_autoptr(GError) error_local = NULL; buf = fu_ti_tps6598x_device_usbep_read_raw(self, i, 62, &error_local); if (buf == NULL) { g_debug("failed to get DMC register 0x%02x: %s", i, error_local->message); continue; } if (!fu_ti_tps6598x_byte_array_is_nonzero(buf)) continue; g_hash_table_insert(metadata, g_strdup_printf("Tps6598xDmcRegister@0x%02x", i), fu_byte_array_to_string(buf)); } } static gboolean fu_ti_tps6598x_device_write_chunks(FuTiTps6598xDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GByteArray) buf = g_byte_array_new(); /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; /* align */ g_byte_array_append(buf, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); fu_byte_array_align_up(buf, FU_FIRMWARE_ALIGNMENT_64, 0xFF); if (!fu_ti_tps6598x_device_sfwd(self, buf, error)) { g_prefix_error(error, "failed to write chunk %u: ", i); return FALSE; } /* update progress */ fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_ti_tps6598x_device_write_sfws_chunks(FuTiTps6598xDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GByteArray) buf = g_byte_array_new(); /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; /* align and pad low before sending */ g_byte_array_append(buf, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); fu_byte_array_align_up(buf, FU_FIRMWARE_ALIGNMENT_64, 0x0); if (!fu_ti_tps6598x_device_sfws(self, buf, error)) { g_prefix_error(error, "failed to write chunk %u: ", i); return FALSE; } /* update progress */ fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_ti_tps6598x_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuTiTps6598xDevice *self = FU_TI_TPS6598X_DEVICE(device); g_autoptr(GError) error_local = NULL; /* hopefully this fails because the hardware rebooted */ if (!fu_ti_tps6598x_device_maybe_reboot(self, &error_local)) { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } g_debug("ignoring expected failure: %s", error_local->message); } /* success! */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } gboolean fu_ti_tps6598x_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuTiTps6598xDevice *self = FU_TI_TPS6598X_DEVICE(device); g_autoptr(GInputStream) stream_sig = NULL; g_autoptr(GInputStream) stream_pubkey = NULL; g_autoptr(GInputStream) stream_payload = NULL; g_autoptr(FuChunkArray) chunks_pubkey = NULL; g_autoptr(FuChunkArray) chunks_sig = NULL; g_autoptr(FuChunkArray) chunks_payload = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 91, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 7, NULL); /* get payload image */ stream_payload = fu_firmware_get_image_by_id_stream(firmware, FU_FIRMWARE_ID_PAYLOAD, error); if (stream_payload == NULL) return FALSE; /* SFWi */ if (!fu_ti_tps6598x_device_sfwi(self, error)) return FALSE; fu_progress_step_done(progress); /* write each SFWd block */ chunks_payload = fu_chunk_array_new_from_stream(stream_payload, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 64, error); if (chunks_payload == NULL) return FALSE; if (!fu_ti_tps6598x_device_write_chunks(self, chunks_payload, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to write SFWd: "); return FALSE; } fu_progress_step_done(progress); /* SFWs with signature */ stream_sig = fu_firmware_get_image_by_id_stream(firmware, FU_FIRMWARE_ID_SIGNATURE, error); if (stream_sig == NULL) return FALSE; chunks_sig = fu_chunk_array_new_from_stream(stream_sig, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 64, error); if (chunks_sig == NULL) return FALSE; if (!fu_ti_tps6598x_device_write_sfws_chunks(self, chunks_sig, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to write SFWs with signature: "); return FALSE; } fu_progress_step_done(progress); /* SFWs with pubkey */ stream_pubkey = fu_firmware_get_image_by_id_stream(firmware, "pubkey", error); if (stream_pubkey == NULL) return FALSE; chunks_pubkey = fu_chunk_array_new_from_stream(stream_pubkey, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 64, error); if (chunks_pubkey == NULL) return FALSE; if (!fu_ti_tps6598x_device_write_sfws_chunks(self, chunks_pubkey, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to write SFWs with pubkey: "); return FALSE; } fu_progress_step_done(progress); /* success! */ return TRUE; } static void fu_ti_tps6598x_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 91, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 9, "reload"); } static void fu_ti_tps6598x_device_init(FuTiTps6598xDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "com.ti.tps6598x"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_ONLY_VERSION_UPGRADE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SELF_RECOVERY); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_VENDOR); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ENFORCE_REQUIRES); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_TI_TPS6598X_FIRMWARE); fu_device_set_remove_delay(FU_DEVICE(self), 30000); fu_usb_device_add_interface(FU_USB_DEVICE(self), 0x0); } static void fu_ti_tps6598x_device_finalize(GObject *object) { FuTiTps6598xDevice *self = FU_TI_TPS6598X_DEVICE(object); g_free(self->uid); g_free(self->ouid); G_OBJECT_CLASS(fu_ti_tps6598x_device_parent_class)->finalize(object); } static void fu_ti_tps6598x_device_class_init(FuTiTps6598xDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_ti_tps6598x_device_finalize; device_class->to_string = fu_ti_tps6598x_device_to_string; device_class->write_firmware = fu_ti_tps6598x_device_write_firmware; device_class->attach = fu_ti_tps6598x_device_attach; device_class->setup = fu_ti_tps6598x_device_setup; device_class->report_metadata_pre = fu_ti_tps6598x_device_report_metadata_pre; device_class->set_progress = fu_ti_tps6598x_device_set_progress; } fwupd-2.0.10/plugins/ti-tps6598x/fu-ti-tps6598x-device.h000066400000000000000000000013441501337203100223230ustar00rootroot00000000000000/* * Copyright 2021 Texas Instruments Incorporated * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_TI_TPS6598X_DEVICE (fu_ti_tps6598x_device_get_type()) G_DECLARE_FINAL_TYPE(FuTiTps6598xDevice, fu_ti_tps6598x_device, FU, TI_TPS6598X_DEVICE, FuUsbDevice) GByteArray * fu_ti_tps6598x_device_read_target_register(FuTiTps6598xDevice *self, guint8 target, guint8 addr, guint8 length, GError **error); gboolean fu_ti_tps6598x_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error); fwupd-2.0.10/plugins/ti-tps6598x/fu-ti-tps6598x-firmware.c000066400000000000000000000103731501337203100226750ustar00rootroot00000000000000/* * Copyright 2021 Texas Instruments Incorporated * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-ti-tps6598x-firmware.h" #include "fu-ti-tps6598x-struct.h" struct _FuTiTps6598xFirmware { FuFirmware parent_instance; }; G_DEFINE_TYPE(FuTiTps6598xFirmware, fu_ti_tps6598x_firmware, FU_TYPE_FIRMWARE) #define FU_TI_TPS6598X_FIRMWARE_PUBKEY_SIZE 0x180 /* bytes */ static gboolean fu_ti_tps6598x_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_ti_tps6598x_firmware_hdr_validate_stream(stream, offset, error); } static gboolean fu_ti_tps6598x_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { guint8 verbuf[3] = {0x0}; gsize offset = 0; gsize streamsz = 0; g_autofree gchar *version_str = NULL; g_autoptr(FuFirmware) img_payload = fu_firmware_new(); g_autoptr(FuFirmware) img_pubkey = fu_firmware_new(); g_autoptr(FuFirmware) img_sig = fu_firmware_new(); g_autoptr(GInputStream) stream_payload = NULL; g_autoptr(GInputStream) stream_pubkey = NULL; g_autoptr(GInputStream) stream_sig = NULL; /* skip magic */ offset += 0x4; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; /* pubkey */ stream_pubkey = fu_partial_input_stream_new(stream, offset, FU_TI_TPS6598X_FIRMWARE_PUBKEY_SIZE, error); if (stream_pubkey == NULL) return FALSE; if (!fu_firmware_parse_stream(img_pubkey, stream_pubkey, 0x0, flags, error)) return FALSE; fu_firmware_set_id(img_pubkey, "pubkey"); fu_firmware_add_image(firmware, img_pubkey); offset += FU_TI_TPS6598X_FIRMWARE_PUBKEY_SIZE; /* RSA signature */ stream_sig = fu_partial_input_stream_new(stream, offset, FU_TI_TPS6598X_FIRMWARE_PUBKEY_SIZE, error); if (stream_sig == NULL) return FALSE; if (!fu_firmware_parse_stream(img_sig, stream_sig, 0x0, flags, error)) return FALSE; fu_firmware_set_id(img_sig, FU_FIRMWARE_ID_SIGNATURE); fu_firmware_add_image(firmware, img_sig); offset += FU_TI_TPS6598X_FIRMWARE_PUBKEY_SIZE; /* payload */ stream_payload = fu_partial_input_stream_new(stream, offset, streamsz - offset, error); if (stream_payload == NULL) return FALSE; if (!fu_firmware_parse_stream(img_payload, stream_payload, 0x0, flags, error)) return FALSE; if (!fu_input_stream_read_safe(stream, verbuf, sizeof(verbuf), 0x0, 0x34, /* offset */ sizeof(verbuf), error)) return FALSE; version_str = g_strdup_printf("%X.%X.%X", verbuf[2], verbuf[1], verbuf[0]); fu_firmware_set_version(img_payload, version_str); fu_firmware_set_id(img_payload, FU_FIRMWARE_ID_PAYLOAD); fu_firmware_add_image(firmware, img_payload); /* success */ return TRUE; } static GByteArray * fu_ti_tps6598x_firmware_write(FuFirmware *firmware, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GBytes) blob_payload = NULL; g_autoptr(GBytes) blob_pubkey = NULL; g_autoptr(GBytes) blob_sig = NULL; /* magic */ fu_byte_array_append_uint32(buf, FU_STRUCT_TI_TPS6598X_FIRMWARE_HDR_DEFAULT_MAGIC, G_LITTLE_ENDIAN); /* pubkey */ blob_pubkey = fu_firmware_get_image_by_id_bytes(firmware, "pubkey", error); if (blob_pubkey == NULL) return NULL; fu_byte_array_append_bytes(buf, blob_pubkey); /* sig */ blob_sig = fu_firmware_get_image_by_id_bytes(firmware, FU_FIRMWARE_ID_SIGNATURE, error); if (blob_sig == NULL) return NULL; fu_byte_array_append_bytes(buf, blob_sig); /* payload */ blob_payload = fu_firmware_get_image_by_id_bytes(firmware, FU_FIRMWARE_ID_PAYLOAD, error); if (blob_payload == NULL) return NULL; fu_byte_array_append_bytes(buf, blob_payload); /* add EOF */ return g_steal_pointer(&buf); } static void fu_ti_tps6598x_firmware_init(FuTiTps6598xFirmware *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_DEDUPE_ID); } static void fu_ti_tps6598x_firmware_class_init(FuTiTps6598xFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_ti_tps6598x_firmware_validate; firmware_class->parse = fu_ti_tps6598x_firmware_parse; firmware_class->write = fu_ti_tps6598x_firmware_write; } fwupd-2.0.10/plugins/ti-tps6598x/fu-ti-tps6598x-firmware.h000066400000000000000000000006401501337203100226760ustar00rootroot00000000000000/* * Copyright 2021 Texas Instruments Incorporated * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_TI_TPS6598X_FIRMWARE (fu_ti_tps6598x_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuTiTps6598xFirmware, fu_ti_tps6598x_firmware, FU, TI_TPS6598X_FIRMWARE, FuFirmware) fwupd-2.0.10/plugins/ti-tps6598x/fu-ti-tps6598x-pd-device.c000066400000000000000000000153171501337203100227240ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-ti-tps6598x-common.h" #include "fu-ti-tps6598x-device.h" #include "fu-ti-tps6598x-firmware.h" #include "fu-ti-tps6598x-pd-device.h" struct _FuTiTps6598xPdDevice { FuDevice parent_instance; guint8 target; }; G_DEFINE_TYPE(FuTiTps6598xPdDevice, fu_ti_tps6598x_pd_device, FU_TYPE_DEVICE) static gboolean fu_ti_tps6598x_pd_device_probe(FuDevice *device, GError **error) { FuTiTps6598xPdDevice *self = FU_TI_TPS6598X_PD_DEVICE(device); g_autofree gchar *name = g_strdup_printf("TPS6598X PD#%u", self->target); g_autofree gchar *logical_id = g_strdup_printf("PD%u", self->target); fu_device_set_name(device, name); fu_device_set_logical_id(device, logical_id); fu_device_add_instance_u8(device, "PD", self->target); return TRUE; } static gboolean fu_ti_tps6598x_pd_device_ensure_version(FuTiTps6598xPdDevice *self, GError **error) { FuTiTps6598xDevice *proxy = FU_TI_TPS6598X_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); g_autoptr(GByteArray) buf = NULL; g_autofree gchar *str = NULL; buf = fu_ti_tps6598x_device_read_target_register(proxy, self->target, TI_TPS6598X_REGISTER_VERSION, 4, error); if (buf == NULL) return FALSE; str = g_strdup_printf("%02X%02X.%02X.%02X", buf->data[3], buf->data[2], buf->data[1], buf->data[0]); fu_device_set_version(FU_DEVICE(self), str); return TRUE; } static gboolean fu_ti_tps6598x_pd_device_ensure_tx_identity(FuTiTps6598xPdDevice *self, GError **error) { FuTiTps6598xDevice *proxy = FU_TI_TPS6598X_DEVICE(fu_device_get_proxy(FU_DEVICE(self))); guint16 val = 0; g_autoptr(GByteArray) buf = NULL; buf = fu_ti_tps6598x_device_read_target_register(proxy, self->target, TI_TPS6598X_REGISTER_TX_IDENTITY, 47, error); if (buf == NULL) return FALSE; if (!fu_memread_uint16_safe(buf->data, buf->len, 0x01, &val, G_LITTLE_ENDIAN, error)) return FALSE; if (val != 0x0 && val != 0xFF) fu_device_add_instance_u16(FU_DEVICE(self), "VID", val); if (!fu_memread_uint16_safe(buf->data, buf->len, 0x0B, &val, G_LITTLE_ENDIAN, error)) return FALSE; if (val != 0x0 && val != 0xFF) fu_device_add_instance_u16(FU_DEVICE(self), "PID", val); if (!fu_memread_uint16_safe(buf->data, buf->len, 0x09, &val, G_LITTLE_ENDIAN, error)) return FALSE; if (val != 0x0 && val != 0xFF) fu_device_add_instance_u16(FU_DEVICE(self), "REV", val); /* success */ return TRUE; } static gboolean fu_ti_tps6598x_pd_device_setup(FuDevice *device, GError **error) { FuTiTps6598xPdDevice *self = FU_TI_TPS6598X_PD_DEVICE(device); /* register reads are slow, so do as few as possible */ if (!fu_ti_tps6598x_pd_device_ensure_version(self, error)) return FALSE; if (!fu_ti_tps6598x_pd_device_ensure_tx_identity(self, error)) return FALSE; /* add new instance IDs */ if (!fu_device_build_instance_id(FU_DEVICE(self), error, "USB", "VID", "PID", "PD", NULL)) return FALSE; return fu_device_build_instance_id(FU_DEVICE(self), error, "USB", "VID", "PID", "REV", "PD", NULL); } static void fu_ti_tps6598x_pd_device_report_metadata_pre(FuDevice *device, GHashTable *metadata) { FuTiTps6598xPdDevice *self = FU_TI_TPS6598X_PD_DEVICE(device); FuTiTps6598xDevice *proxy = FU_TI_TPS6598X_DEVICE(fu_device_get_proxy(device)); /* this is too slow to do for each update... */ if (g_getenv("FWUPD_TI_TPS6598X_VERBOSE") == NULL) return; for (guint i = 0; i < 0x80; i++) { g_autoptr(GByteArray) buf = NULL; g_autoptr(GError) error_local = NULL; buf = fu_ti_tps6598x_device_read_target_register(proxy, self->target, i, 63, &error_local); if (buf == NULL) { g_debug("failed to get target 0x%02x register 0x%02x: %s", self->target, i, error_local->message); continue; } if (!fu_ti_tps6598x_byte_array_is_nonzero(buf)) continue; g_hash_table_insert( metadata, g_strdup_printf("Tps6598xPd%02xRegister@0x%02x", self->target, i), fu_byte_array_to_string(buf)); } } static gboolean fu_ti_tps6598x_pd_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuTiTps6598xDevice *proxy = FU_TI_TPS6598X_DEVICE(fu_device_get_proxy(device)); return fu_device_attach_full(FU_DEVICE(proxy), progress, error); } static gboolean fu_ti_tps6598x_pd_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuTiTps6598xDevice *proxy = FU_TI_TPS6598X_DEVICE(fu_device_get_proxy(device)); return fu_ti_tps6598x_device_write_firmware(FU_DEVICE(proxy), firmware, progress, flags, error); } static void fu_ti_tps6598x_pd_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 91, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 9, "reload"); } static void fu_ti_tps6598x_pd_device_init(FuTiTps6598xPdDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "com.ti.tps6598x"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_VENDOR); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_OPEN); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_TI_TPS6598X_FIRMWARE); fu_device_set_remove_delay(FU_DEVICE(self), 30000); } static void fu_ti_tps6598x_pd_device_class_init(FuTiTps6598xPdDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_ti_tps6598x_pd_device_write_firmware; device_class->attach = fu_ti_tps6598x_pd_device_attach; device_class->setup = fu_ti_tps6598x_pd_device_setup; device_class->probe = fu_ti_tps6598x_pd_device_probe; device_class->report_metadata_pre = fu_ti_tps6598x_pd_device_report_metadata_pre; device_class->set_progress = fu_ti_tps6598x_pd_device_set_progress; } FuDevice * fu_ti_tps6598x_pd_device_new(FuDevice *proxy, guint8 target) { FuTiTps6598xPdDevice *self = g_object_new(FU_TYPE_TI_TPS6598X_PD_DEVICE, "context", fu_device_get_context(proxy), "proxy", proxy, NULL); self->target = target; return FU_DEVICE(self); } fwupd-2.0.10/plugins/ti-tps6598x/fu-ti-tps6598x-pd-device.h000066400000000000000000000006731501337203100227300ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include #define FU_TYPE_TI_TPS6598X_PD_DEVICE (fu_ti_tps6598x_pd_device_get_type()) G_DECLARE_FINAL_TYPE(FuTiTps6598xPdDevice, fu_ti_tps6598x_pd_device, FU, TI_TPS6598X_PD_DEVICE, FuDevice) FuDevice * fu_ti_tps6598x_pd_device_new(FuDevice *proxy, guint8 target); fwupd-2.0.10/plugins/ti-tps6598x/fu-ti-tps6598x-plugin.c000066400000000000000000000020701501337203100223520ustar00rootroot00000000000000/* * Copyright 2021 Texas Instruments Incorporated * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #include "config.h" #include "fu-ti-tps6598x-device.h" #include "fu-ti-tps6598x-firmware.h" #include "fu-ti-tps6598x-pd-device.h" #include "fu-ti-tps6598x-plugin.h" struct _FuTiTps6598xPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuTiTps6598xPlugin, fu_ti_tps6598x_plugin, FU_TYPE_PLUGIN) static void fu_ti_tps6598x_plugin_init(FuTiTps6598xPlugin *self) { } static void fu_ti_tps6598x_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_set_device_gtype_default(plugin, FU_TYPE_TI_TPS6598X_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_TI_TPS6598X_PD_DEVICE); /* coverage */ fu_plugin_add_firmware_gtype(plugin, "ti-tps6598x", FU_TYPE_TI_TPS6598X_FIRMWARE); } static void fu_ti_tps6598x_plugin_class_init(FuTiTps6598xPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_ti_tps6598x_plugin_constructed; } fwupd-2.0.10/plugins/ti-tps6598x/fu-ti-tps6598x-plugin.h000066400000000000000000000004621501337203100223620ustar00rootroot00000000000000/* * Copyright 2021 Texas Instruments Incorporated * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */ #pragma once #include G_DECLARE_FINAL_TYPE(FuTiTps6598xPlugin, fu_ti_tps6598x_plugin, FU, TI_TPS6598X_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/ti-tps6598x/fu-ti-tps6598x.rs000066400000000000000000000023201501337203100212560ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ValidateStream, Default)] #[repr(C, packed)] struct FuStructTiTps6598xFirmwareHdr { magic: u32le == 0xACEF0001, } #[derive(ToString)] enum FuTiTps6598xSfwi { Success = 0x0, FailFlashErrorOrBusy = 0x4, FailFlashInvalidAddress = 0x5, FailLastBootWasUart = 0x6, FailSfwiAfterComplete = 0x7, FailNoValidFlashRegion = 0x8, FailUnknownError = 0xF, } #[derive(ToString)] enum FuTiTps6598xSfwd { Success = 0x0, FailFlashEraseWriteError = 0x4, FailSfwiNotRunFirst = 0x6, FailTooMuchData = 0x7, FailIdNotInHeader = 0x8, FailBinaryTooLarge = 0x9, FailDeviceIdMismatch = 0xA, FailFlashErrorReadOnly = 0xD, FailUnknownError = 0xF, } #[derive(ToString)] enum FuTiTps6598xSfws { Success = 0x0, FailFlashEraseWriteError = 0x4, FailSfwdNotRunOrNoKeyExists = 0x6, FailTooMuchData = 0x7, FailCrcFail = 0x8, FailDidCheckFail = 0x9, FailVersionCheckFail = 0xA, FailNoHashMatchRuleSatisfied = 0xB, FailEngrFwUpdateAttemptWhileRunningProd = 0xC, FailIncompatibleRomVersion = 0xD, FailCrcBusy = 0xE, FailUnknownError = 0xF, } fwupd-2.0.10/plugins/ti-tps6598x/meson.build000066400000000000000000000011271501337203100205020ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginTiTps6598x"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('ti-tps6598x.quirk') plugin_builtins += static_library('fu_plugin_ti_tps6598x', rustgen.process('fu-ti-tps6598x.rs'), sources: [ 'fu-ti-tps6598x-common.c', 'fu-ti-tps6598x-device.c', 'fu-ti-tps6598x-firmware.c', 'fu-ti-tps6598x-pd-device.c', 'fu-ti-tps6598x-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files( 'tests/caldigit-ts4.json', ) fwupd-2.0.10/plugins/ti-tps6598x/tests/000077500000000000000000000000001501337203100175015ustar00rootroot00000000000000fwupd-2.0.10/plugins/ti-tps6598x/tests/caldigit-ts4.json000066400000000000000000000017071501337203100226710ustar00rootroot00000000000000{ "name": "CalDigit TS4 Dock", "interactive": false, "steps": [ { "url": "e07ae12eefb6dc1b9985b5759a017a74160a587361db2e4e63c4032d1af5d7d2-CalDigit-TS4-combind-050322-secure-Cus-22.cab", "emulation-url": "e755191ff3c7d26777273262eff0be266758ebe5b79685cd44d14d491ab6107a-CalDigit-TS4-combind-050322-secure-Cus-22.zip", "components": [ { "version": "F907.14.10", "guids": [ "707d7941-3129-53d9-a205-47a659d94fe3" ] } ] }, { "url": "d685ab1fa73a63a797172d498370d8717a6d80c70fff527be051baeaf8ac2bb8-CalDigit-TS4-combind-121022-secure-Cus-37.cab", "emulation-url": "21c81548a6126f806966497613bc6e33ddd5b3dc045e7cc83b27abd64ba6afd8-CalDigit-TS4-combind-121022-secure-Cus-37.zip", "components": [ { "version": "F907.14.13", "guids": [ "707d7941-3129-53d9-a205-47a659d94fe3" ] } ] } ] } fwupd-2.0.10/plugins/ti-tps6598x/ti-tps6598x.quirk000066400000000000000000000000551501337203100213600ustar00rootroot00000000000000[USB\VID_0451&PID_ACE1] Plugin = ti_tps6598x fwupd-2.0.10/plugins/tpm/000077500000000000000000000000001501337203100151735ustar00rootroot00000000000000fwupd-2.0.10/plugins/tpm/.gitignore000066400000000000000000000000061501337203100171570ustar00rootroot00000000000000tests fwupd-2.0.10/plugins/tpm/README.md000066400000000000000000000044201501337203100164520ustar00rootroot00000000000000--- title: Plugin: TPM --- ## Introduction This allows enumerating Trusted Platform Modules, also known as "TPM" devices, although it does not allow the user to update the firmware on them. The TPM Event Log records which events are registered for the PCR0 hash, which may help in explaining why PCR0 values are differing for some firmware. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a packed binary file format. This plugin supports the following protocol ID: * `org.trustedcomputinggroup.tpm2` ## GUID Generation These devices use custom GUIDs: * `TPM\VEN_$(manufacturer)&DEV_$(type)` * `TPM\VEN_$(manufacturer)&MOD_$(vendor-string)` * `TPM\VEN_$(manufacturer)&DEV_$(type)_VER_$(family)`, * `TPM\VEN_$(manufacturer)&MOD_$(vendor-string)_VER_$(family)` ...where `family` is either `2.0` or `1.2` Example GUIDs from a real system containing a TPM from Intel: ```text Guid: 34801700-3a50-5b05-820c-fe14580e4c2d <- TPM\VEN_INTC&DEV_0000 Guid: 03f304f4-223e-54f4-b2c1-c3cf3b5817c6 <- TPM\VEN_INTC&DEV_0000&VER_2.0 ``` ## Update Behavior The plugin detects if the TPM device is updatable (if the commands `FieldUpgradeStart` and `FieldUpgradeData` are implemented) and provides stub functionality to read and write the firmware. Nearly all TPMs in the wild have this behavior disabled, and are instead updated using vendor-specific commands used from a UEFI UpdateCapsule and with an OEM-provided ESRT entry. If a vendor wanted to use the plugin code provided here they would still need to tell us how to set up the correct `keyHandle` for `FieldUpgradeStart` and how to handle authorization. We do not know of any vendor that does TPM updates using the standardized API, and so the code provided here is more of a *this is how it should be implemented* rather than with any expectation it is actually going to just work. ## Vendor ID Security The vendor ID is set from the TPM vendor, e.g. `TPM:STM` ## External Interface Access This plugin uses the tpm2-tss library to access the TPM. It requires access to `/sys/class/tpm` and optionally requires read only access to `/sys/kernel/security/tpm0/binary_bios_measurements`. ## Version Considerations This plugin has been available since fwupd version `1.3.6`. fwupd-2.0.10/plugins/tpm/fu-self-test.c000066400000000000000000000203571501337203100176640ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-context-private.h" #include "fu-plugin-private.h" #include "fu-security-attrs-private.h" #include "fu-tpm-eventlog-common.h" #include "fu-tpm-eventlog-parser.h" #include "fu-tpm-plugin.h" #include "fu-tpm-v1-device.h" #include "fu-tpm-v2-device.h" static void fu_tpm_device_1_2_func(void) { FuTpmDevice *device; GPtrArray *devices; gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuPlugin) plugin = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FuSecurityAttrs) attrs = fu_security_attrs_new(); g_autoptr(FwupdSecurityAttr) attr0 = NULL; g_autoptr(FwupdSecurityAttr) attr1 = NULL; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) pcr0s = NULL; g_autoptr(GPtrArray) pcrXs = NULL; /* do not save silo */ ret = fu_context_load_quirks(ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE, &error); g_assert_no_error(error); g_assert_true(ret); /* load the plugin */ plugin = fu_plugin_new_from_gtype(fu_tpm_plugin_get_type(), ctx); ret = fu_plugin_runner_startup(plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* get the v1.2 device */ devices = fu_plugin_get_devices(plugin); g_assert_cmpint(devices->len, ==, 1); device = g_ptr_array_index(devices, 0); g_assert_true(FU_IS_TPM_DEVICE(device)); /* verify checksums set correctly */ pcr0s = fu_tpm_device_get_checksums(device, 0); g_assert_nonnull(pcr0s); g_assert_cmpint(pcr0s->len, ==, 1); pcrXs = fu_tpm_device_get_checksums(device, 999); g_assert_nonnull(pcrXs); g_assert_cmpint(pcrXs->len, ==, 0); /* verify HSI attributes */ fu_plugin_runner_add_security_attrs(plugin, attrs); attr0 = fu_security_attrs_get_by_appstream_id(attrs, FWUPD_SECURITY_ATTR_ID_TPM_VERSION_20, &error); g_assert_no_error(error); g_assert_nonnull(attr0); g_assert_cmpint(fwupd_security_attr_get_result(attr0), ==, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); attr1 = fu_security_attrs_get_by_appstream_id(attrs, FWUPD_SECURITY_ATTR_ID_TPM_EMPTY_PCR, &error); g_assert_no_error(error); g_assert_nonnull(attr1); /* Some PCRs are empty, but PCRs 0-7 are set (tests/tpm0/pcrs) */ g_assert_cmpint(fwupd_security_attr_get_result(attr1), ==, FWUPD_SECURITY_ATTR_RESULT_VALID); } static void fu_tpm_device_2_0_func(void) { gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(FuTpmDevice) device = fu_tpm_v2_device_new(ctx); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) pcr0s = NULL; g_autoptr(GPtrArray) pcrXs = NULL; const gchar *tpm_server_running = g_getenv("TPM_SERVER_RUNNING"); (void)g_setenv("FWUPD_FORCE_TPM2", "1", TRUE); #ifdef HAVE_GETUID if (tpm_server_running == NULL && (getuid() != 0 || geteuid() != 0)) { g_test_skip("TPM2.0 tests require simulated TPM2.0 running or need root access " "with physical TPM"); g_unsetenv("FWUPD_FORCE_TPM2"); return; } #endif fu_device_set_physical_id(FU_DEVICE(device), "dummy"); locker = fu_device_locker_new(FU_DEVICE(device), &error); if (locker == NULL && tpm_server_running == NULL && g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_test_skip("no physical or simulated TPM 2.0 device available"); g_unsetenv("FWUPD_FORCE_TPM2"); return; } g_assert_no_error(error); g_assert_nonnull(locker); ret = fu_device_setup(FU_DEVICE(device), &error); g_assert_no_error(error); g_assert_true(ret); pcr0s = fu_tpm_device_get_checksums(device, 0); g_assert_nonnull(pcr0s); g_assert_cmpint(pcr0s->len, >=, 1); pcrXs = fu_tpm_device_get_checksums(device, 999); g_assert_nonnull(pcrXs); g_assert_cmpint(pcrXs->len, ==, 0); g_unsetenv("FWUPD_FORCE_TPM2"); } static void fu_tpm_eventlog_parse_v1_func(void) { const gchar *tmp; gboolean ret; gsize bufsz = 0; g_autofree gchar *fn = NULL; g_autofree guint8 *buf = NULL; g_autoptr(GPtrArray) items = NULL; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) pcr0s = NULL; fn = g_test_build_filename(G_TEST_DIST, "tests", "binary_bios_measurements-v1", NULL); if (!g_file_test(fn, G_FILE_TEST_EXISTS)) { g_test_skip("Missing binary_bios_measurements-v1"); return; } ret = g_file_get_contents(fn, (gchar **)&buf, &bufsz, &error); g_assert_no_error(error); g_assert_true(ret); items = fu_tpm_eventlog_parser_new(buf, bufsz, FU_TPM_EVENTLOG_PARSER_FLAG_NONE, &error); g_assert_no_error(error); g_assert_nonnull(items); pcr0s = fu_tpm_eventlog_calc_checksums(items, 0, &error); g_assert_no_error(error); g_assert_nonnull(pcr0s); g_assert_cmpint(pcr0s->len, ==, 1); tmp = g_ptr_array_index(pcr0s, 0); g_assert_cmpstr(tmp, ==, "543ae96e57b6fc4003531cd0dab1d9ba7f8166e0"); } static void fu_tpm_eventlog_parse_v2_func(void) { const gchar *tmp; gboolean ret; gsize bufsz = 0; g_autofree gchar *fn = NULL; g_autofree guint8 *buf = NULL; g_autoptr(GPtrArray) items = NULL; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) pcr0s = NULL; fn = g_test_build_filename(G_TEST_DIST, "tests", "binary_bios_measurements-v2", NULL); if (!g_file_test(fn, G_FILE_TEST_EXISTS)) { g_test_skip("Missing binary_bios_measurements-v2"); return; } ret = g_file_get_contents(fn, (gchar **)&buf, &bufsz, &error); g_assert_no_error(error); g_assert_true(ret); items = fu_tpm_eventlog_parser_new(buf, bufsz, FU_TPM_EVENTLOG_PARSER_FLAG_NONE, &error); g_assert_no_error(error); g_assert_nonnull(items); pcr0s = fu_tpm_eventlog_calc_checksums(items, 0, &error); g_assert_no_error(error); g_assert_nonnull(pcr0s); g_assert_cmpint(pcr0s->len, ==, 2); tmp = g_ptr_array_index(pcr0s, 0); g_assert_cmpstr(tmp, ==, "ebead4b31c7c49e193c440cd6ee90bc1b61a3ca6"); tmp = g_ptr_array_index(pcr0s, 1); g_assert_cmpstr(tmp, ==, "6d9fed68092cfb91c9552bcb7879e75e1df36efd407af67690dc3389a5722fab"); } static void fu_tpm_empty_pcr_func(void) { gboolean ret; g_autofree gchar *testdatadir = NULL; g_auto(GStrv) environ_old = NULL; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuPlugin) plugin = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FuSecurityAttrs) attrs = fu_security_attrs_new(); g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(GError) error = NULL; /* do not save silo */ ret = fu_context_load_quirks(ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE, &error); g_assert_no_error(error); g_assert_true(ret); /* save environment and set broken PCR data */ environ_old = g_get_environ(); testdatadir = g_test_build_filename(G_TEST_DIST, "tests", "empty_pcr", NULL); (void)g_setenv("FWUPD_SYSFSTPMDIR", testdatadir, TRUE); /* load the plugin */ plugin = fu_plugin_new_from_gtype(fu_tpm_plugin_get_type(), ctx); ret = fu_plugin_runner_startup(plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* verify HSI attr */ fu_plugin_runner_add_security_attrs(plugin, attrs); attr = fu_security_attrs_get_by_appstream_id(attrs, FWUPD_SECURITY_ATTR_ID_TPM_EMPTY_PCR, &error); g_assert_no_error(error); g_assert_nonnull(attr); /* PCR 6 is empty (tests/empty_pcr/tpm0/pcrs) */ g_assert_cmpint(fwupd_security_attr_get_result(attr), ==, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); /* restore default environment */ (void)g_setenv("FWUPD_SYSFSTPMDIR", g_environ_getenv(environ_old, "FWUPD_SYSFSTPMDIR"), TRUE); } int main(int argc, char **argv) { g_autofree gchar *testdatadir = NULL; (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); testdatadir = g_test_build_filename(G_TEST_DIST, "tests", NULL); (void)g_setenv("FWUPD_SYSFSTPMDIR", testdatadir, TRUE); (void)g_setenv("FWUPD_UEFI_TEST", "1", TRUE); (void)g_setenv("FWUPD_SYSFSFWATTRIBDIR", testdatadir, TRUE); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); (void)g_setenv("G_MESSAGES_DEBUG", "all", TRUE); /* tests go here */ g_test_add_func("/tpm/pcrs1.2", fu_tpm_device_1_2_func); g_test_add_func("/tpm/pcrs2.0", fu_tpm_device_2_0_func); g_test_add_func("/tpm/empty-pcr", fu_tpm_empty_pcr_func); g_test_add_func("/tpm/eventlog-parse{v1}", fu_tpm_eventlog_parse_v1_func); g_test_add_func("/tpm/eventlog-parse{v2}", fu_tpm_eventlog_parse_v2_func); return g_test_run(); } fwupd-2.0.10/plugins/tpm/fu-tpm-device.c000066400000000000000000000060301501337203100200030ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-tpm-device.h" typedef struct { guint idx; gchar *checksum; } FuTpmDevicePcrItem; typedef struct { gchar *family; GPtrArray *items; /* of FuTpmDevicePcrItem */ } FuTpmDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuTpmDevice, fu_tpm_device, FU_TYPE_UDEV_DEVICE) #define GET_PRIVATE(o) (fu_tpm_device_get_instance_private(o)) void fu_tpm_device_set_family(FuTpmDevice *self, const gchar *family) { FuTpmDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_TPM_DEVICE(self)); priv->family = g_strdup(family); } const gchar * fu_tpm_device_get_family(FuTpmDevice *self) { FuTpmDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_TPM_DEVICE(self), NULL); return priv->family; } void fu_tpm_device_add_checksum(FuTpmDevice *self, guint idx, const gchar *checksum) { FuTpmDevicePrivate *priv = GET_PRIVATE(self); FuTpmDevicePcrItem *item; g_return_if_fail(FU_IS_TPM_DEVICE(self)); g_return_if_fail(checksum != NULL); item = g_new0(FuTpmDevicePcrItem, 1); item->idx = idx; item->checksum = g_strdup(checksum); g_debug("added PCR-%02u=%s", item->idx, item->checksum); g_ptr_array_add(priv->items, item); } GPtrArray * fu_tpm_device_get_checksums(FuTpmDevice *self, guint idx) { FuTpmDevicePrivate *priv = GET_PRIVATE(self); g_autoptr(GPtrArray) array = g_ptr_array_new_with_free_func(g_free); g_return_val_if_fail(FU_IS_TPM_DEVICE(self), NULL); for (guint i = 0; i < priv->items->len; i++) { FuTpmDevicePcrItem *item = g_ptr_array_index(priv->items, i); if (item->idx == idx) g_ptr_array_add(array, g_strdup(item->checksum)); } return g_steal_pointer(&array); } static void fu_tpm_device_to_string(FuDevice *device, guint idt, GString *str) { FuTpmDevice *self = FU_TPM_DEVICE(device); FuTpmDevicePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append(str, idt, "Family", priv->family); } static void fu_tpm_device_item_free(FuTpmDevicePcrItem *item) { g_free(item->checksum); g_free(item); } static void fu_tpm_device_finalize(GObject *object) { FuTpmDevice *self = FU_TPM_DEVICE(object); FuTpmDevicePrivate *priv = GET_PRIVATE(self); g_free(priv->family); g_ptr_array_unref(priv->items); G_OBJECT_CLASS(fu_tpm_device_parent_class)->finalize(object); } static void fu_tpm_device_init(FuTpmDevice *self) { FuTpmDevicePrivate *priv = GET_PRIVATE(self); priv->items = g_ptr_array_new_with_free_func((GDestroyNotify)fu_tpm_device_item_free); fu_device_set_name(FU_DEVICE(self), "TPM"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_COMPUTER); } static void fu_tpm_device_class_init(FuTpmDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->finalize = fu_tpm_device_finalize; device_class->to_string = fu_tpm_device_to_string; } fwupd-2.0.10/plugins/tpm/fu-tpm-device.h000066400000000000000000000012011501337203100200030ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_TPM_DEVICE (fu_tpm_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuTpmDevice, fu_tpm_device, FU, TPM_DEVICE, FuUdevDevice) struct _FuTpmDeviceClass { FuUdevDeviceClass parent_class; }; void fu_tpm_device_set_family(FuTpmDevice *self, const gchar *family); const gchar * fu_tpm_device_get_family(FuTpmDevice *self); void fu_tpm_device_add_checksum(FuTpmDevice *self, guint idx, const gchar *checksum); GPtrArray * fu_tpm_device_get_checksums(FuTpmDevice *self, guint idx); fwupd-2.0.10/plugins/tpm/fu-tpm-eventlog-common.c000066400000000000000000000124321501337203100216600ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-tpm-eventlog-common.h" const gchar * fu_tpm_eventlog_pcr_to_string(gint pcr) { if (pcr == 0) return "BIOS"; if (pcr == 1) return "BIOS Configuration"; if (pcr == 2) return "Option ROMs"; if (pcr == 3) return "Option ROM configuration"; if (pcr == 4) return "Initial program loader code"; if (pcr == 5) return "Initial program loader code configuration"; if (pcr == 6) return "State transitions and wake events"; if (pcr == 7) return "Platform manufacturer specific measurements"; if (pcr >= 8 && pcr <= 15) return "Static operating system"; if (pcr == 16) return "Debug"; if (pcr == 17) return "Dynamic root of trust measurement and launch control policy"; if (pcr >= 18 && pcr <= 22) return "Trusted OS"; if (pcr == 23) return "Application support"; return "Undefined"; } guint32 fu_tpm_eventlog_hash_get_size(TPM2_ALG_ID hash_kind) { if (hash_kind == TPM2_ALG_SHA1) return TPM2_SHA1_DIGEST_SIZE; if (hash_kind == TPM2_ALG_SHA256) return TPM2_SHA256_DIGEST_SIZE; if (hash_kind == TPM2_ALG_SHA384) return TPM2_SHA384_DIGEST_SIZE; if (hash_kind == TPM2_ALG_SHA512) return TPM2_SHA512_DIGEST_SIZE; if (hash_kind == TPM2_ALG_SM3_256) return TPM2_SM3_256_DIGEST_SIZE; return 0; } gchar * fu_tpm_eventlog_blobstr(GBytes *blob) { g_return_val_if_fail(blob != NULL, NULL); return g_base64_encode((const guchar *)g_bytes_get_data(blob, NULL), g_bytes_get_size(blob)); } GPtrArray * fu_tpm_eventlog_calc_checksums(GPtrArray *items, guint8 pcr, GError **error) { guint cnt_sha1 = 0; guint cnt_sha256 = 0; guint cnt_sha384 = 0; guint8 digest_sha1[TPM2_SHA1_DIGEST_SIZE] = {0x0}; guint8 digest_sha256[TPM2_SHA256_DIGEST_SIZE] = {0x0}; guint8 digest_sha384[TPM2_SHA384_DIGEST_SIZE] = {0x0}; gsize digest_sha1_len = sizeof(digest_sha1); gsize digest_sha256_len = sizeof(digest_sha256); gsize digest_sha384_len = sizeof(digest_sha384); g_autoptr(GPtrArray) csums = g_ptr_array_new_with_free_func(g_free); /* sanity check */ if (items->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no event log data"); return NULL; } /* take existing PCR hash, append new measurement to that, * hash that with the same algorithm */ for (guint i = 0; i < items->len; i++) { FuTpmEventlogItem *item = g_ptr_array_index(items, i); if (item->pcr != pcr) continue; /* if TXT is enabled then the first event for PCR0 should be a StartupLocality */ if (item->kind == FU_TPM_EVENTLOG_ITEM_KIND_EV_NO_ACTION && item->pcr == 0 && item->blob != NULL && i == 0) { g_autoptr(GByteArray) st_loc = NULL; st_loc = fu_struct_tpm_efi_startup_locality_event_parse_bytes(item->blob, 0x0, NULL); if (st_loc != NULL) { guint8 locality = fu_struct_tpm_efi_startup_locality_event_get_locality(st_loc); digest_sha384[TPM2_SHA384_DIGEST_SIZE - 1] = locality; digest_sha256[TPM2_SHA256_DIGEST_SIZE - 1] = locality; digest_sha1[TPM2_SHA1_DIGEST_SIZE - 1] = locality; continue; } } if (item->checksum_sha1 != NULL) { g_autoptr(GChecksum) csum_sha1 = g_checksum_new(G_CHECKSUM_SHA1); g_checksum_update(csum_sha1, (const guchar *)digest_sha1, digest_sha1_len); g_checksum_update( csum_sha1, (const guchar *)g_bytes_get_data(item->checksum_sha1, NULL), g_bytes_get_size(item->checksum_sha1)); g_checksum_get_digest(csum_sha1, digest_sha1, &digest_sha1_len); cnt_sha1++; } if (item->checksum_sha256 != NULL) { g_autoptr(GChecksum) csum_sha256 = g_checksum_new(G_CHECKSUM_SHA256); g_checksum_update(csum_sha256, (const guchar *)digest_sha256, digest_sha256_len); g_checksum_update( csum_sha256, (const guchar *)g_bytes_get_data(item->checksum_sha256, NULL), g_bytes_get_size(item->checksum_sha256)); g_checksum_get_digest(csum_sha256, digest_sha256, &digest_sha256_len); cnt_sha256++; } if (item->checksum_sha384 != NULL) { g_autoptr(GChecksum) csum_sha384 = g_checksum_new(G_CHECKSUM_SHA384); g_checksum_update(csum_sha384, (const guchar *)digest_sha384, digest_sha384_len); g_checksum_update( csum_sha384, (const guchar *)g_bytes_get_data(item->checksum_sha384, NULL), g_bytes_get_size(item->checksum_sha384)); g_checksum_get_digest(csum_sha384, digest_sha384, &digest_sha384_len); cnt_sha384++; } } if (cnt_sha1 == 0 && cnt_sha256 == 0 && cnt_sha384 == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no SHA1, SHA256, or SHA384 data"); return NULL; } if (cnt_sha1 > 0) { g_autoptr(GBytes) blob_sha1 = NULL; blob_sha1 = g_bytes_new_static(digest_sha1, sizeof(digest_sha1)); g_ptr_array_add(csums, fu_bytes_to_string(blob_sha1)); } if (cnt_sha256 > 0) { g_autoptr(GBytes) blob_sha256 = NULL; blob_sha256 = g_bytes_new_static(digest_sha256, sizeof(digest_sha256)); g_ptr_array_add(csums, fu_bytes_to_string(blob_sha256)); } if (cnt_sha384 > 0) { g_autoptr(GBytes) blob_sha384 = NULL; blob_sha384 = g_bytes_new_static(digest_sha384, sizeof(digest_sha384)); g_ptr_array_add(csums, fu_bytes_to_string(blob_sha384)); } return g_steal_pointer(&csums); } fwupd-2.0.10/plugins/tpm/fu-tpm-eventlog-common.h000066400000000000000000000012011501337203100216550ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include #include "fu-tpm-struct.h" typedef struct { guint8 pcr; FuTpmEventlogItemKind kind; GBytes *checksum_sha1; GBytes *checksum_sha256; GBytes *checksum_sha384; GBytes *blob; } FuTpmEventlogItem; const gchar * fu_tpm_eventlog_pcr_to_string(gint pcr); guint32 fu_tpm_eventlog_hash_get_size(TPM2_ALG_ID hash_kind); gchar * fu_tpm_eventlog_blobstr(GBytes *blob); GPtrArray * fu_tpm_eventlog_calc_checksums(GPtrArray *items, guint8 pcr, GError **error); fwupd-2.0.10/plugins/tpm/fu-tpm-eventlog-parser.c000066400000000000000000000204371501337203100216700ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-tpm-eventlog-parser.h" #include "fu-tpm-struct.h" #define FU_TPM_EVENTLOG_V1_IDX_PCR 0x00 #define FU_TPM_EVENTLOG_V1_IDX_TYPE 0x04 #define FU_TPM_EVENTLOG_V1_IDX_DIGEST 0x08 #define FU_TPM_EVENTLOG_V1_IDX_EVENT_SIZE 0x1c #define FU_TPM_EVENTLOG_V1_SIZE 0x20 #define FU_TPM_EVENTLOG_V2_HDR_SIGNATURE "Spec ID Event03" static void fu_tpm_eventlog_parser_item_free(FuTpmEventlogItem *item) { if (item->blob != NULL) g_bytes_unref(item->blob); if (item->checksum_sha1 != NULL) g_bytes_unref(item->checksum_sha1); if (item->checksum_sha256 != NULL) g_bytes_unref(item->checksum_sha256); if (item->checksum_sha384 != NULL) g_bytes_unref(item->checksum_sha384); g_free(item); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuTpmEventlogItem, fu_tpm_eventlog_parser_item_free); void fu_tpm_eventlog_item_to_string(FuTpmEventlogItem *item, guint idt, GString *str) /* nocheck:name */ { const gchar *tmp; g_autofree gchar *pcrstr = g_strdup_printf("%s (%u)", fu_tpm_eventlog_pcr_to_string(item->pcr), item->pcr); fwupd_codec_string_append(str, idt, "PCR", pcrstr); fwupd_codec_string_append_hex(str, idt, "Type", item->kind); tmp = fu_tpm_eventlog_item_kind_to_string(item->kind); fwupd_codec_string_append(str, idt, "Description", tmp); if (item->checksum_sha1 != NULL) { g_autofree gchar *csum = fu_bytes_to_string(item->checksum_sha1); fwupd_codec_string_append(str, idt, "ChecksumSha1", csum); } if (item->checksum_sha256 != NULL) { g_autofree gchar *csum = fu_bytes_to_string(item->checksum_sha256); fwupd_codec_string_append(str, idt, "ChecksumSha256", csum); } if (item->checksum_sha384 != NULL) { g_autofree gchar *csum = fu_bytes_to_string(item->checksum_sha384); fwupd_codec_string_append(str, idt, "ChecksumSha384", csum); } if (item->blob != NULL) { g_autofree gchar *blobstr = fu_tpm_eventlog_blobstr(item->blob); if (blobstr != NULL) fwupd_codec_string_append(str, idt, "BlobStr", blobstr); } } static GPtrArray * fu_tpm_eventlog_parser_parse_blob_v2(const guint8 *buf, gsize bufsz, FuTpmEventlogParserFlags flags, GError **error) { guint32 hdrsz = 0x0; g_autoptr(GPtrArray) items = NULL; /* advance over the header block */ if (!fu_memread_uint32_safe(buf, bufsz, FU_TPM_EVENTLOG_V1_IDX_EVENT_SIZE, &hdrsz, G_LITTLE_ENDIAN, error)) return NULL; items = g_ptr_array_new_with_free_func((GDestroyNotify)fu_tpm_eventlog_parser_item_free); for (gsize idx = FU_TPM_EVENTLOG_V1_SIZE + hdrsz; idx < bufsz;) { guint32 pcr; guint32 digestcnt; guint32 datasz = 0; g_autoptr(GBytes) checksum_sha1 = NULL; g_autoptr(GBytes) checksum_sha256 = NULL; g_autoptr(GBytes) checksum_sha384 = NULL; g_autoptr(GByteArray) st = NULL; /* read checksum block */ st = fu_struct_tpm_event_log2_parse(buf, bufsz, idx, error); if (st == NULL) return NULL; idx += st->len; digestcnt = fu_struct_tpm_event_log2_get_digest_count(st); for (guint i = 0; i < digestcnt; i++) { guint16 alg_type = 0; guint32 alg_size = 0; g_autofree guint8 *digest = NULL; /* get checksum type */ if (!fu_memread_uint16_safe(buf, bufsz, idx, &alg_type, G_LITTLE_ENDIAN, error)) return NULL; alg_size = fu_tpm_eventlog_hash_get_size(alg_type); if (alg_size == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "hash algorithm 0x%x size not known", alg_type); return NULL; } /* build checksum */ idx += sizeof(alg_type); /* copy hash */ digest = g_malloc0(alg_size); if (!fu_memcpy_safe(digest, alg_size, 0x0, /* dst */ buf, bufsz, idx, /* src */ alg_size, error)) return NULL; /* save this for analysis */ if (alg_type == TPM2_ALG_SHA1) checksum_sha1 = g_bytes_new_take(g_steal_pointer(&digest), alg_size); else if (alg_type == TPM2_ALG_SHA256) checksum_sha256 = g_bytes_new_take(g_steal_pointer(&digest), alg_size); else if (alg_type == TPM2_ALG_SHA384) checksum_sha384 = g_bytes_new_take(g_steal_pointer(&digest), alg_size); /* next block */ idx += alg_size; } /* read data block */ if (!fu_memread_uint32_safe(buf, bufsz, idx, &datasz, G_LITTLE_ENDIAN, error)) return NULL; if (datasz > 1024 * 1024) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "event log item too large"); return NULL; } /* save blob if PCR=0 */ idx += sizeof(datasz); pcr = fu_struct_tpm_event_log2_get_pcr(st); if (pcr == ESYS_TR_PCR0 || flags & FU_TPM_EVENTLOG_PARSER_FLAG_ALL_PCRS) { g_autoptr(FuTpmEventlogItem) item = NULL; /* build item */ item = g_new0(FuTpmEventlogItem, 1); item->pcr = pcr; item->kind = fu_struct_tpm_event_log2_get_type(st); item->checksum_sha1 = g_steal_pointer(&checksum_sha1); item->checksum_sha256 = g_steal_pointer(&checksum_sha256); item->checksum_sha384 = g_steal_pointer(&checksum_sha384); if (datasz > 0) { g_autofree guint8 *data = g_malloc0(datasz); if (!fu_memcpy_safe(data, datasz, 0x0, /* dst */ buf, bufsz, idx, datasz, /* src */ error)) return NULL; item->blob = g_bytes_new_take(g_steal_pointer(&data), datasz); fu_dump_bytes(G_LOG_DOMAIN, "TpmEvent", item->blob); } g_ptr_array_add(items, g_steal_pointer(&item)); } /* next entry */ idx += datasz; } /* success */ return g_steal_pointer(&items); } GPtrArray * fu_tpm_eventlog_parser_new(const guint8 *buf, gsize bufsz, FuTpmEventlogParserFlags flags, GError **error) { gchar sig[] = FU_TPM_EVENTLOG_V2_HDR_SIGNATURE; g_autoptr(GPtrArray) items = NULL; g_return_val_if_fail(buf != NULL, NULL); /* look for TCG v2 signature */ if (!fu_memcpy_safe((guint8 *)sig, sizeof(sig), 0x0, /* dst */ buf, bufsz, FU_TPM_EVENTLOG_V1_SIZE, /* src */ sizeof(sig), error)) return NULL; if (g_strcmp0(sig, FU_TPM_EVENTLOG_V2_HDR_SIGNATURE) == 0) return fu_tpm_eventlog_parser_parse_blob_v2(buf, bufsz, flags, error); /* assume v1 structure */ items = g_ptr_array_new_with_free_func((GDestroyNotify)fu_tpm_eventlog_parser_item_free); for (gsize idx = 0; idx < bufsz; idx += FU_TPM_EVENTLOG_V1_SIZE) { guint32 datasz = 0; guint32 pcr = 0; guint32 event_type = 0; if (!fu_memread_uint32_safe(buf, bufsz, idx + FU_TPM_EVENTLOG_V1_IDX_PCR, &pcr, G_LITTLE_ENDIAN, error)) return NULL; if (!fu_memread_uint32_safe(buf, bufsz, idx + FU_TPM_EVENTLOG_V1_IDX_TYPE, &event_type, G_LITTLE_ENDIAN, error)) return NULL; if (!fu_memread_uint32_safe(buf, bufsz, idx + FU_TPM_EVENTLOG_V1_IDX_EVENT_SIZE, &datasz, G_LITTLE_ENDIAN, error)) return NULL; if (datasz > 1024 * 1024) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "event log item too large"); return NULL; } if (pcr == ESYS_TR_PCR0 || flags & FU_TPM_EVENTLOG_PARSER_FLAG_ALL_PCRS) { g_autoptr(FuTpmEventlogItem) item = NULL; guint8 digest[TPM2_SHA1_DIGEST_SIZE] = {0x0}; /* copy hash */ if (!fu_memcpy_safe(digest, sizeof(digest), 0x0, /* dst */ buf, bufsz, idx + FU_TPM_EVENTLOG_V1_IDX_DIGEST, /* src */ sizeof(digest), error)) return NULL; /* build item */ item = g_new0(FuTpmEventlogItem, 1); item->pcr = pcr; item->kind = event_type; item->checksum_sha1 = g_bytes_new(digest, sizeof(digest)); if (datasz > 0) { g_autofree guint8 *data = g_malloc0(datasz); if (!fu_memcpy_safe(data, datasz, 0x0, /* dst */ buf, bufsz, idx + FU_TPM_EVENTLOG_V1_SIZE, /* src */ datasz, error)) return NULL; item->blob = g_bytes_new_take(g_steal_pointer(&data), datasz); fu_dump_bytes(G_LOG_DOMAIN, "TpmEvent", item->blob); } g_ptr_array_add(items, g_steal_pointer(&item)); } idx += datasz; } return g_steal_pointer(&items); } fwupd-2.0.10/plugins/tpm/fu-tpm-eventlog-parser.h000066400000000000000000000011031501337203100216620ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-tpm-eventlog-common.h" typedef enum { FU_TPM_EVENTLOG_PARSER_FLAG_NONE = 0, FU_TPM_EVENTLOG_PARSER_FLAG_ALL_PCRS = 1 << 0, FU_TPM_EVENTLOG_PARSER_FLAG_LAST } FuTpmEventlogParserFlags; GPtrArray * fu_tpm_eventlog_parser_new(const guint8 *buf, gsize bufsz, FuTpmEventlogParserFlags flags, GError **error); void fu_tpm_eventlog_item_to_string(FuTpmEventlogItem *item, guint idt, GString *str); fwupd-2.0.10/plugins/tpm/fu-tpm-eventlog.c000066400000000000000000000103051501337203100203670ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuTpmEventlog" #include "config.h" #include #include #include #include #include #include "fu-tpm-eventlog-parser.h" static gint fu_tpm_eventlog_sort_cb(gconstpointer a, gconstpointer b) { FuTpmEventlogItem *item_a = *((FuTpmEventlogItem **)a); FuTpmEventlogItem *item_b = *((FuTpmEventlogItem **)b); if (item_a->pcr > item_b->pcr) return 1; if (item_a->pcr < item_b->pcr) return -1; return 0; } static gboolean fu_tpm_eventlog_process(const gchar *fn, gint pcr, GError **error) { gsize bufsz = 0; g_autofree guint8 *buf = NULL; g_autoptr(GPtrArray) items = NULL; g_autoptr(GString) str = g_string_new(NULL); gint max_pcr = 0; /* parse this */ if (!g_file_get_contents(fn, (gchar **)&buf, &bufsz, error)) return FALSE; items = fu_tpm_eventlog_parser_new(buf, bufsz, FU_TPM_EVENTLOG_PARSER_FLAG_ALL_PCRS, error); if (items == NULL) return FALSE; g_ptr_array_sort(items, fu_tpm_eventlog_sort_cb); for (guint i = 0; i < items->len; i++) { FuTpmEventlogItem *item = g_ptr_array_index(items, i); if (item->pcr > max_pcr) max_pcr = item->pcr; if (pcr >= 0 && item->pcr != pcr) continue; fu_tpm_eventlog_item_to_string(item, 0, str); g_string_append(str, "\n"); } if (pcr > max_pcr) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid PCR specified: %d", pcr); return FALSE; } fwupd_codec_string_append(str, 0, "Reconstructed PCRs", ""); for (guint8 i = 0; i <= max_pcr; i++) { g_autoptr(GPtrArray) pcrs = fu_tpm_eventlog_calc_checksums(items, i, NULL); if (pcrs == NULL) continue; for (guint j = 0; j < pcrs->len; j++) { const gchar *csum = g_ptr_array_index(pcrs, j); g_autofree gchar *title = NULL; g_autofree gchar *pretty = NULL; if (pcr >= 0 && i != (guint)pcr) continue; title = g_strdup_printf("PCR %x", i); pretty = fwupd_checksum_format_for_display(csum); fwupd_codec_string_append(str, 1, title, pretty); } } /* success */ g_print("%s", str->str); return TRUE; } int main(int argc, char *argv[]) { const gchar *fn; gboolean verbose = FALSE; gboolean interactive = isatty(fileno(stdout)) != 0; gint pcr = -1; g_autoptr(GError) error = NULL; g_autoptr(GOptionContext) context = g_option_context_new(NULL); const GOptionEntry options[] = {{"verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose, /* TRANSLATORS: command line option */ N_("Show extra debugging information"), NULL}, {"pcr", 'p', 0, G_OPTION_ARG_INT, &pcr, /* TRANSLATORS: command line option */ N_("Only show single PCR value"), NULL}, {NULL}}; setlocale(LC_ALL, ""); bindtextdomain(GETTEXT_PACKAGE, FWUPD_LOCALEDIR); bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8"); textdomain(GETTEXT_PACKAGE); #ifdef HAVE_GETUID /* ensure root user */ if (argc < 2 && interactive && (getuid() != 0 || geteuid() != 0)) /* TRANSLATORS: we're poking around as a power user */ g_printerr("%s\n", _("This program may only work correctly as root")); #endif /* TRANSLATORS: program name */ g_set_application_name(_("fwupd TPM event log utility")); g_option_context_add_main_entries(context, options, NULL); g_option_context_set_description(context, /* TRANSLATORS: CLI description */ _("This tool will read and parse the TPM event log " "from the system firmware.")); if (!g_option_context_parse(context, &argc, &argv, &error)) { /* TRANSLATORS: the user didn't read the man page */ g_print("%s: %s\n", _("Failed to parse arguments"), error->message); return EXIT_FAILURE; } /* set verbose? */ if (verbose) { (void)g_setenv("G_MESSAGES_DEBUG", "all", FALSE); (void)g_setenv("FWUPD_TPM_EVENTLOG_VERBOSE", "1", FALSE); } /* allow user to chose a local file */ fn = argc <= 1 ? "/sys/kernel/security/tpm0/binary_bios_measurements" : argv[1]; if (!fu_tpm_eventlog_process(fn, pcr, &error)) { /* TRANSLATORS: failed to read measurements file */ g_printerr("%s: %s\n", _("Failed to parse file"), error->message); return EXIT_FAILURE; } return EXIT_SUCCESS; } fwupd-2.0.10/plugins/tpm/fu-tpm-plugin.c000066400000000000000000000305551501337203100200530ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-tpm-eventlog-parser.h" #include "fu-tpm-plugin.h" #include "fu-tpm-v1-device.h" #include "fu-tpm-v2-device.h" struct _FuTpmPlugin { FuPlugin parent_instance; FuTpmDevice *tpm_device; FuDevice *bios_device; GPtrArray *ev_items; /* of FuTpmEventlogItem */ }; G_DEFINE_TYPE(FuTpmPlugin, fu_tpm_plugin, FU_TYPE_PLUGIN) static void fu_tpm_plugin_to_string(FuPlugin *plugin, guint idt, GString *str) { FuTpmPlugin *self = FU_TPM_PLUGIN(plugin); if (self->tpm_device != NULL) fwupd_codec_string_append(str, idt, "TpmDevice", fu_device_get_id(self->tpm_device)); if (self->bios_device != NULL) fwupd_codec_string_append(str, idt, "BiosDevice", fu_device_get_id(self->bios_device)); } static void fu_tpm_plugin_set_bios_pcr0s(FuPlugin *plugin) { FuTpmPlugin *self = FU_TPM_PLUGIN(plugin); g_autoptr(GPtrArray) pcr0s = NULL; if (self->tpm_device == NULL) return; if (self->bios_device == NULL) return; /* add all the PCR0s */ pcr0s = fu_tpm_device_get_checksums(self->tpm_device, 0); if (pcr0s->len == 0) return; for (guint i = 0; i < pcr0s->len; i++) { const gchar *checksum = g_ptr_array_index(pcr0s, i); fu_device_add_checksum(self->bios_device, checksum); } fu_device_add_flag(self->bios_device, FWUPD_DEVICE_FLAG_CAN_VERIFY); } /* set the PCR0 as the device checksum */ static void fu_tpm_plugin_device_registered(FuPlugin *plugin, FuDevice *device) { FuTpmPlugin *self = FU_TPM_PLUGIN(plugin); if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE)) { g_set_object(&self->bios_device, device); fu_tpm_plugin_set_bios_pcr0s(plugin); } } static void fu_tpm_plugin_device_added(FuPlugin *plugin, FuDevice *dev) { FuTpmPlugin *self = FU_TPM_PLUGIN(plugin); g_autoptr(GPtrArray) pcr0s = NULL; const gchar *family = fu_tpm_device_get_family(FU_TPM_DEVICE(dev)); g_set_object(&self->tpm_device, FU_TPM_DEVICE(dev)); if (family != NULL) fu_plugin_add_report_metadata(plugin, "TpmFamily", family); /* ensure */ fu_tpm_plugin_set_bios_pcr0s(plugin); /* add extra plugin metadata */ pcr0s = fu_tpm_device_get_checksums(self->tpm_device, 0); for (guint i = 0; i < pcr0s->len; i++) { const gchar *csum = g_ptr_array_index(pcr0s, i); GChecksumType csum_type = fwupd_checksum_guess_kind(csum); if (csum_type == G_CHECKSUM_SHA1) { fu_plugin_add_report_metadata(plugin, "Pcr0_SHA1", csum); continue; } if (csum_type == G_CHECKSUM_SHA256) { fu_plugin_add_report_metadata(plugin, "Pcr0_SHA256", csum); continue; } if (csum_type == G_CHECKSUM_SHA384) { fu_plugin_add_report_metadata(plugin, "Pcr0_SHA384", csum); continue; } } } static void fu_tpm_plugin_add_security_attr_version(FuPlugin *plugin, FuSecurityAttrs *attrs) { FuTpmPlugin *self = FU_TPM_PLUGIN(plugin); g_autoptr(FwupdSecurityAttr) attr = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_TPM_VERSION_20); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_FOUND); fu_security_attrs_append(attrs, attr); /* check exists, and in v2.0 mode */ if (self->tpm_device == NULL) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND); return; } if (g_strcmp0(fu_tpm_device_get_family(self->tpm_device), "2.0") != 0) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW); return; } /* success */ fwupd_security_attr_add_guids(attr, fu_device_get_guids(FU_DEVICE(self->tpm_device))); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_tpm_plugin_add_security_attr_eventlog(FuPlugin *plugin, FuSecurityAttrs *attrs) { FuTpmPlugin *self = FU_TPM_PLUGIN(plugin); gboolean reconstructed = TRUE; g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) pcr0s_calc = NULL; g_autoptr(GPtrArray) pcr0s_real = NULL; /* no TPM device */ if (self->tpm_device == NULL) return; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_TPM_RECONSTRUCTION_PCR0); fwupd_security_attr_add_guids(attr, fu_device_get_guids(self->tpm_device)); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_VALID); fu_security_attrs_append(attrs, attr); /* check reconstructed to PCR0 */ if (self->ev_items == NULL) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND); return; } /* calculate from the eventlog */ pcr0s_calc = fu_tpm_eventlog_calc_checksums(self->ev_items, 0, &error); if (pcr0s_calc == NULL) { g_warning("failed to get eventlog reconstruction: %s", error->message); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* compare against the real PCR0s */ pcr0s_real = fu_tpm_device_get_checksums(self->tpm_device, 0); for (guint i = 0; i < pcr0s_calc->len; i++) { const gchar *checksum = g_ptr_array_index(pcr0s_calc, i); reconstructed = FALSE; for (guint j = 0; j < pcr0s_real->len; j++) { const gchar *checksum_tmp = g_ptr_array_index(pcr0s_real, j); /* skip unless same algorithm */ if (strlen(checksum) != strlen(checksum_tmp)) continue; g_debug("comparing TPM %s and EVT %s", checksum, checksum_tmp); if (g_strcmp0(checksum, checksum_tmp) == 0) { reconstructed = TRUE; break; } } /* all algorithms must match */ if (!reconstructed) break; } if (!reconstructed) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_tpm_plugin_add_security_attr_empty(FuPlugin *plugin, FuSecurityAttrs *attrs) { FuTpmPlugin *self = FU_TPM_PLUGIN(plugin); g_autoptr(FwupdSecurityAttr) attr = NULL; /* no TPM device */ if (self->tpm_device == NULL) return; /* add attributes */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_TPM_EMPTY_PCR); fwupd_security_attr_add_guids(attr, fu_device_get_guids(self->tpm_device)); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_VALID); fu_security_attrs_append(attrs, attr); /* check PCRs 0 through 7 for empty checksums */ for (guint pcr = 0; pcr <= 7; pcr++) { g_autoptr(GPtrArray) checksums = fu_tpm_device_get_checksums(self->tpm_device, pcr); for (guint i = 0; i < checksums->len; i++) { const gchar *checksum = g_ptr_array_index(checksums, i); gboolean empty = TRUE; /* empty checksum is zero, so made entirely of zeroes */ for (guint j = 0; checksum[j] != '\0'; j++) { if (checksum[j] != '0') { empty = FALSE; break; } } if (empty) { fwupd_security_attr_set_result( attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); return; } } } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_tpm_plugin_add_security_attrs(FuPlugin *plugin, FuSecurityAttrs *attrs) { if (fu_plugin_has_flag(plugin, FWUPD_PLUGIN_FLAG_DISABLED)) return; fu_tpm_plugin_add_security_attr_version(plugin, attrs); fu_tpm_plugin_add_security_attr_eventlog(plugin, attrs); fu_tpm_plugin_add_security_attr_empty(plugin, attrs); } static gchar * fu_tpm_plugin_eventlog_report_metadata(FuPlugin *plugin) { FuTpmPlugin *self = FU_TPM_PLUGIN(plugin); GString *str = g_string_new(""); g_autoptr(GPtrArray) pcrs = NULL; for (guint i = 0; i < self->ev_items->len; i++) { FuTpmEventlogItem *item = g_ptr_array_index(self->ev_items, i); g_autofree gchar *blobstr = NULL; g_autofree gchar *checksum = NULL; if (item->blob == NULL) continue; if (item->checksum_sha1 != NULL) checksum = fu_bytes_to_string(item->checksum_sha1); else if (item->checksum_sha256 != NULL) checksum = fu_bytes_to_string(item->checksum_sha256); else if (item->checksum_sha384 != NULL) checksum = fu_bytes_to_string(item->checksum_sha384); else continue; g_string_append_printf(str, "0x%08x %s", item->kind, checksum); blobstr = fu_tpm_eventlog_blobstr(item->blob); if (blobstr != NULL) g_string_append_printf(str, " [%s]", blobstr); g_string_append(str, "\n"); } pcrs = fu_tpm_eventlog_calc_checksums(self->ev_items, 0, NULL); if (pcrs != NULL) { for (guint j = 0; j < pcrs->len; j++) { const gchar *csum = g_ptr_array_index(pcrs, j); g_string_append_printf(str, "PCR0: %s\n", csum); } } if (str->len > 0) g_string_truncate(str, str->len - 1); return g_string_free(str, FALSE); } static gboolean fu_tpm_plugin_coldplug_eventlog(FuPlugin *plugin, GError **error) { FuTpmPlugin *self = FU_TPM_PLUGIN(plugin); gsize bufsz = 0; g_autofree gchar *fn = NULL; g_autofree gchar *str = NULL; g_autofree gchar *sysfsdir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR); g_autofree guint8 *buf = NULL; /* do not show a warning if no TPM exists, or the kernel is too old */ fn = g_build_filename(sysfsdir, "kernel", "security", "tpm0", "binary_bios_measurements", NULL); if (!g_file_test(fn, G_FILE_TEST_EXISTS)) { g_debug("no %s, so skipping", fn); return TRUE; } if (!g_file_get_contents(fn, (gchar **)&buf, &bufsz, error)) return FALSE; if (bufsz == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to read data from %s", fn); return FALSE; } self->ev_items = fu_tpm_eventlog_parser_new(buf, bufsz, FU_TPM_EVENTLOG_PARSER_FLAG_NONE, error); if (self->ev_items == NULL) return FALSE; /* add optional report metadata */ str = fu_tpm_plugin_eventlog_report_metadata(plugin); fu_plugin_add_report_metadata(plugin, "TpmEventLog", str); return TRUE; } static gboolean fu_tpm_plugin_coldplug(FuPlugin *plugin, FuProgress *progress, GError **error) { g_autoptr(GError) error_local = NULL; /* best effort */ if (!fu_tpm_plugin_coldplug_eventlog(plugin, &error_local)) g_warning("failed to load eventlog: %s", error_local->message); /* success */ return TRUE; } static gboolean fu_tpm_plugin_startup(FuPlugin *plugin, FuProgress *progress, GError **error) { FuTpmPlugin *self = FU_TPM_PLUGIN(plugin); g_autofree gchar *sysfstpmdir = NULL; g_autofree gchar *fn_pcrs = NULL; /* look for TPM v1.2 */ sysfstpmdir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR_TPM); fn_pcrs = g_build_filename(sysfstpmdir, "tpm0", "pcrs", NULL); if (g_file_test(fn_pcrs, G_FILE_TEST_EXISTS) && g_getenv("FWUPD_FORCE_TPM2") == NULL) { self->tpm_device = fu_tpm_v1_device_new(fu_plugin_get_context(plugin)); g_object_set(self->tpm_device, "device-file", fn_pcrs, NULL); fu_device_set_physical_id(FU_DEVICE(self->tpm_device), "tpm"); if (!fu_device_probe(FU_DEVICE(self->tpm_device), error)) return FALSE; fu_plugin_device_add(plugin, FU_DEVICE(self->tpm_device)); } /* success */ return TRUE; } static void fu_tpm_plugin_init(FuTpmPlugin *self) { } static void fu_tpm_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); /* old name */ fu_plugin_add_rule(plugin, FU_PLUGIN_RULE_CONFLICTS, "tpm_eventlog"); fu_plugin_add_device_udev_subsystem(plugin, "tpm"); fu_plugin_set_device_gtype_default(plugin, FU_TYPE_TPM_V2_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_TPM_V1_DEVICE); /* coverage */ } static void fu_tpm_plugin_finalize(GObject *obj) { FuTpmPlugin *self = FU_TPM_PLUGIN(obj); if (self->tpm_device != NULL) g_object_unref(self->tpm_device); if (self->bios_device != NULL) g_object_unref(self->bios_device); if (self->ev_items != NULL) g_ptr_array_unref(self->ev_items); G_OBJECT_CLASS(fu_tpm_plugin_parent_class)->finalize(obj); } static void fu_tpm_plugin_class_init(FuTpmPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_tpm_plugin_finalize; plugin_class->constructed = fu_tpm_plugin_constructed; plugin_class->to_string = fu_tpm_plugin_to_string; plugin_class->startup = fu_tpm_plugin_startup; plugin_class->coldplug = fu_tpm_plugin_coldplug; plugin_class->device_added = fu_tpm_plugin_device_added; plugin_class->device_registered = fu_tpm_plugin_device_registered; plugin_class->add_security_attrs = fu_tpm_plugin_add_security_attrs; } fwupd-2.0.10/plugins/tpm/fu-tpm-plugin.h000066400000000000000000000003431501337203100200500ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuTpmPlugin, fu_tpm_plugin, FU, TPM_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/tpm/fu-tpm-v1-device.c000066400000000000000000000051251501337203100203330ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-tpm-v1-device.h" struct _FuTpmV1Device { FuTpmDevice parent_instance; }; G_DEFINE_TYPE(FuTpmV1Device, fu_tpm_v1_device, FU_TYPE_TPM_DEVICE) static gboolean _g_string_isxdigit(GString *str) { for (gsize i = 0; i < str->len; i++) { if (!g_ascii_isxdigit(str->str[i])) return FALSE; } return TRUE; } static void fu_tpm_v1_device_parse_line(const gchar *line, gpointer user_data) { FuTpmDevice *self = FU_TPM_DEVICE(user_data); guint64 idx; g_autofree gchar *idxstr = NULL; g_auto(GStrv) split = NULL; g_autoptr(GString) str = NULL; g_autoptr(GError) error_local = NULL; /* split into index:hash */ if (line == NULL || line[0] == '\0') return; split = g_strsplit(line, ":", -1); if (g_strv_length(split) != 2) { g_debug("unexpected format, skipping: %s", line); return; } /* get index */ idxstr = fu_strstrip(split[0]); if (!fu_strtoull(idxstr, &idx, 0, 64, FU_INTEGER_BASE_AUTO, &error_local)) { g_debug("unexpected index %s, skipping: %s", idxstr, error_local->message); return; } /* parse hash */ str = g_string_new(split[1]); g_string_replace(str, " ", "", 0); if ((str->len != 40 && str->len != 64) || !_g_string_isxdigit(str)) { g_debug("not SHA-1 or SHA-256, skipping: %s", split[1]); return; } g_string_ascii_down(str); fu_tpm_device_add_checksum(self, idx, str->str); } static gboolean fu_tpm_v1_device_probe(FuDevice *device, GError **error) { FuTpmV1Device *self = FU_TPM_V1_DEVICE(device); g_auto(GStrv) lines = NULL; g_autofree gchar *buf_pcrs = NULL; /* sanity check */ if (fu_udev_device_get_device_file(FU_UDEV_DEVICE(device)) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no device file"); return FALSE; } /* get entire contents */ if (!g_file_get_contents(fu_udev_device_get_device_file(FU_UDEV_DEVICE(device)), &buf_pcrs, NULL, error)) return FALSE; /* find PCR lines */ lines = g_strsplit(buf_pcrs, "\n", -1); for (guint i = 0; lines[i] != NULL; i++) { if (g_str_has_prefix(lines[i], "PCR-")) fu_tpm_v1_device_parse_line(lines[i] + 4, self); } return TRUE; } static void fu_tpm_v1_device_init(FuTpmV1Device *self) { } static void fu_tpm_v1_device_class_init(FuTpmV1DeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_tpm_v1_device_probe; } FuTpmDevice * fu_tpm_v1_device_new(FuContext *ctx) { return FU_TPM_DEVICE(g_object_new(FU_TYPE_TPM_V1_DEVICE, "context", ctx, NULL)); } fwupd-2.0.10/plugins/tpm/fu-tpm-v1-device.h000066400000000000000000000005731501337203100203420ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-tpm-device.h" #define FU_TYPE_TPM_V1_DEVICE (fu_tpm_v1_device_get_type()) G_DECLARE_FINAL_TYPE(FuTpmV1Device, fu_tpm_v1_device, FU, TPM_V1_DEVICE, FuTpmDevice) FuTpmDevice * fu_tpm_v1_device_new(FuContext *ctx); fwupd-2.0.10/plugins/tpm/fu-tpm-v2-device.c000066400000000000000000000432121501337203100203330ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-tpm-v2-device.h" struct _FuTpmV2Device { FuTpmDevice parent_instance; ESYS_CONTEXT *esys_context; }; G_DEFINE_TYPE(FuTpmV2Device, fu_tpm_v2_device, FU_TYPE_TPM_DEVICE) static gboolean fu_tpm_v2_device_probe(FuDevice *device, GError **error) { g_autofree gchar *physical_id = NULL; g_autofree gchar *prop_devname = NULL; /* we don't have anything better */ prop_devname = fu_udev_device_read_property(FU_UDEV_DEVICE(device), "DEVNAME", error); if (prop_devname == NULL) return FALSE; physical_id = g_strdup_printf("DEVNAME=%s", prop_devname); fu_device_set_physical_id(device, physical_id); /* fake something as we cannot talk to tpmd */ if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_IS_FAKE)) { fu_device_add_instance_str(device, "VEN", "fwupd"); fu_device_add_instance_str(device, "DEV", "TPM"); return fu_device_build_instance_id(device, error, "TPM", "VEN", "DEV", NULL); } /* success */ return TRUE; } static gboolean fu_tpm_v2_device_get_uint32(FuTpmV2Device *self, guint32 query, guint32 *val, GError **error) { TSS2_RC rc; g_autofree TPMS_CAPABILITY_DATA *capability = NULL; g_return_val_if_fail(val != NULL, FALSE); rc = Esys_GetCapability(self->esys_context, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, TPM2_CAP_TPM_PROPERTIES, query, 1, NULL, &capability); if (rc != TSS2_RC_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "capability request failed for query %x", query); return FALSE; } if (capability->data.tpmProperties.count == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no properties returned for query %x", query); return FALSE; } if (capability->data.tpmProperties.tpmProperty[0].property != query) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "wrong query returned (got %x expected %x)", capability->data.tpmProperties.tpmProperty[0].property, query); return FALSE; } *val = capability->data.tpmProperties.tpmProperty[0].value; return TRUE; } static gchar * fu_tpm_v2_device_get_string(FuTpmV2Device *self, guint32 query, GError **error) { guint32 val_be = 0; guint32 val; gchar result[5] = {'\0'}; /* return four bytes */ if (!fu_tpm_v2_device_get_uint32(self, query, &val_be, error)) return NULL; val = GUINT32_FROM_BE(val_be); /* nocheck:blocked */ memcpy(result, (gchar *)&val, 4); /* nocheck:blocked */ /* convert non-ASCII into spaces */ for (guint i = 0; i < 4; i++) { if (!g_ascii_isgraph(result[i])) result[i] = 0x20; } return fu_strstrip(result); } /* taken from TCG-TPM-Vendor-ID-Registry-Version-1.01-Revision-1.00.pdf */ static const gchar * fu_tpm_v2_device_convert_manufacturer(const gchar *manufacturer) { if (g_strcmp0(manufacturer, "AMD") == 0) return "Advanced Micro Devices, Inc."; if (g_strcmp0(manufacturer, "ATML") == 0) return "Atmel"; if (g_strcmp0(manufacturer, "BRCM") == 0) return "Broadcom"; if (g_strcmp0(manufacturer, "HPE") == 0) return "HPE"; if (g_strcmp0(manufacturer, "IBM") == 0) return "IBM"; if (g_strcmp0(manufacturer, "IFX") == 0) return "Infineon"; if (g_strcmp0(manufacturer, "INTC") == 0) return "Intel"; if (g_strcmp0(manufacturer, "LEN") == 0) return "Lenovo"; if (g_strcmp0(manufacturer, "MSFT") == 0) return "Microsoft"; if (g_strcmp0(manufacturer, "NSM") == 0) return "National Semiconductor"; if (g_strcmp0(manufacturer, "NTZ") == 0) return "Nationz"; if (g_strcmp0(manufacturer, "NTC") == 0) return "Nuvoton Technology"; if (g_strcmp0(manufacturer, "QCOM") == 0) return "Qualcomm"; if (g_strcmp0(manufacturer, "SMSC") == 0) return "SMSC"; if (g_strcmp0(manufacturer, "STM") == 0) return "ST Microelectronics"; if (g_strcmp0(manufacturer, "SMSN") == 0) return "Samsung"; if (g_strcmp0(manufacturer, "SNS") == 0) return "Sinosun"; if (g_strcmp0(manufacturer, "TXN") == 0) return "Texas Instruments"; if (g_strcmp0(manufacturer, "WEC") == 0) return "Winbond"; if (g_strcmp0(manufacturer, "ROCC") == 0) return "Fuzhou Rockchip"; if (g_strcmp0(manufacturer, "GOOG") == 0) return "Google"; return NULL; } static gboolean fu_tpm_v2_device_setup_pcrs(FuTpmV2Device *self, GError **error) { TSS2_RC rc; g_autofree TPMS_CAPABILITY_DATA *capability_data = NULL; TPML_PCR_SELECTION pcr_selection_in = { 0, }; g_autofree TPML_DIGEST *pcr_values = NULL; /* get hash algorithms supported by the TPM */ rc = Esys_GetCapability(self->esys_context, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, TPM2_CAP_PCRS, 0, 1, NULL, &capability_data); if (rc != TSS2_RC_SUCCESS) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to get hash algorithms supported by TPM"); return FALSE; } /* fetch PCR 0 for every supported hash algorithm */ pcr_selection_in.count = capability_data->data.assignedPCR.count; for (guint i = 0; i < pcr_selection_in.count; i++) { pcr_selection_in.pcrSelections[i].hash = capability_data->data.assignedPCR.pcrSelections[i].hash; pcr_selection_in.pcrSelections[i].sizeofSelect = capability_data->data.assignedPCR.pcrSelections[i].sizeofSelect; pcr_selection_in.pcrSelections[i].pcrSelect[0] = 0b00000001; } rc = Esys_PCR_Read(self->esys_context, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, &pcr_selection_in, NULL, NULL, &pcr_values); if (rc != TSS2_RC_SUCCESS) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to read PCR values from TPM"); return FALSE; } for (guint i = 0; i < pcr_values->count; i++) { g_autoptr(GString) str = NULL; gboolean valid = FALSE; str = g_string_new(NULL); for (guint j = 0; j < pcr_values->digests[i].size; j++) { gint64 val = pcr_values->digests[i].buffer[j]; if (val > 0) valid = TRUE; g_string_append_printf(str, "%02x", pcr_values->digests[i].buffer[j]); } if (valid) { /* constant PCR index 0, since we only read this single PCR */ fu_tpm_device_add_checksum(FU_TPM_DEVICE(self), 0, str->str); } } /* success */ return TRUE; } static gboolean fu_tpm_v2_device_ensure_commands(FuTpmV2Device *self, GError **error) { gboolean seen_upgrade_data = FALSE; gboolean seen_upgrade_start = FALSE; TSS2_RC rc; g_autofree TPMS_CAPABILITY_DATA *capability = NULL; g_autoptr(GString) str = g_string_new(NULL); rc = Esys_GetCapability(self->esys_context, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, TPM2_CAP_COMMANDS, TPM2_CC_FIRST, TPM2_MAX_CAP_CC, NULL, &capability); if (rc != TSS2_RC_SUCCESS) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "capability request failed for TPM2_CAP_COMMANDS"); return FALSE; } for (guint i = 0; i < capability->data.ppCommands.count; i++) { guint cap_cmd = capability->data.ppCommands.commandCodes[i] & 0xFFFF; if (str->len > 0) g_string_append(str, ", "); g_string_append_printf(str, "0x%04x", cap_cmd); /* ones we care about */ if (cap_cmd == TPM2_CC_FieldUpgradeStart) { seen_upgrade_start = TRUE; } else if (cap_cmd == TPM2_CC_FieldUpgradeData) { seen_upgrade_data = TRUE; } else if (cap_cmd == TPM2_CC_FirmwareRead) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); } } g_debug("CAP_COMMANDS: %s", str->str); /* both available */ if (seen_upgrade_start && seen_upgrade_data) fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); /* success */ return TRUE; } static gboolean fu_tpm_v2_device_setup(FuDevice *device, GError **error) { FuTpmV2Device *self = FU_TPM_V2_DEVICE(device); TSS2_RC rc; const gchar *tmp; guint32 tpm_type = 0; guint32 version1 = 0; guint32 version2 = 0; guint64 version_raw; g_autofree gchar *manufacturer = NULL; g_autofree gchar *model1 = NULL; g_autofree gchar *model2 = NULL; g_autofree gchar *model3 = NULL; g_autofree gchar *model4 = NULL; g_autofree gchar *model = NULL; g_autofree gchar *family = NULL; /* suppress warning messages about missing TCTI libraries for tpm2-tss <2.3 */ if (g_getenv("FWUPD_UEFI_VERBOSE") == NULL) (void)g_setenv("TSS2_LOG", "esys+none,tcti+none", FALSE); rc = Esys_Startup(self->esys_context, TPM2_SU_CLEAR); if (rc != TSS2_RC_SUCCESS) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to initialize TPM"); return FALSE; } /* lookup guaranteed details from TPM */ family = fu_tpm_v2_device_get_string(self, TPM2_PT_FAMILY_INDICATOR, error); if (family == NULL) { g_prefix_error(error, "failed to read TPM family: "); return FALSE; } fu_tpm_device_set_family(FU_TPM_DEVICE(self), family); manufacturer = fu_tpm_v2_device_get_string(self, TPM2_PT_MANUFACTURER, error); if (manufacturer == NULL) { g_prefix_error(error, "failed to read TPM manufacturer: "); return FALSE; } model1 = fu_tpm_v2_device_get_string(self, TPM2_PT_VENDOR_STRING_1, error); if (model1 == NULL) { g_prefix_error(error, "failed to read TPM vendor string: "); return FALSE; } if (!fu_tpm_v2_device_get_uint32(self, TPM2_PT_VENDOR_TPM_TYPE, &tpm_type, error)) { g_prefix_error(error, "failed to read TPM type: "); return FALSE; } /* these are not guaranteed by spec and may be NULL */ model2 = fu_tpm_v2_device_get_string(self, TPM2_PT_VENDOR_STRING_2, error); model3 = fu_tpm_v2_device_get_string(self, TPM2_PT_VENDOR_STRING_3, error); model4 = fu_tpm_v2_device_get_string(self, TPM2_PT_VENDOR_STRING_4, error); model = g_strjoin("", model1, model2, model3, model4, NULL); /* add GUIDs to daemon */ fu_device_add_instance_str(device, "VEN", manufacturer); fu_device_add_instance_u16(device, "DEV", tpm_type); fu_device_add_instance_str(device, "MOD", model); fu_device_add_instance_str(device, "VER", family); fu_device_build_instance_id(device, NULL, "TPM", "VEN", "DEV", NULL); fu_device_build_instance_id(device, NULL, "TPM", "VEN", "MOD", NULL); fu_device_build_instance_id(device, NULL, "TPM", "VEN", "DEV", "VER", NULL); fu_device_build_instance_id(device, NULL, "TPM", "VEN", "MOD", "VER", NULL); /* enforce vendors can only ship updates for their own hardware */ fu_device_build_vendor_id(device, "TPM", manufacturer); tmp = fu_tpm_v2_device_convert_manufacturer(manufacturer); fu_device_set_vendor(device, tmp != NULL ? tmp : manufacturer); /* get version */ if (!fu_tpm_v2_device_get_uint32(self, TPM2_PT_FIRMWARE_VERSION_1, &version1, error)) return FALSE; if (!fu_tpm_v2_device_get_uint32(self, TPM2_PT_FIRMWARE_VERSION_2, &version2, error)) return FALSE; version_raw = ((guint64)version1) << 32 | ((guint64)version2); fu_device_set_version_raw(device, version_raw); /* get capabilities */ if (!fu_tpm_v2_device_ensure_commands(self, error)) return FALSE; /* get PCRs */ return fu_tpm_v2_device_setup_pcrs(self, error); } static gboolean fu_tpm_v2_device_upgrade_data(FuTpmV2Device *self, GInputStream *stream, FuProgress *progress, GError **error) { TPMT_HA *first_digest; TPMT_HA *next_digest; TSS2_RC rc; gsize streamsz = 0; g_autoptr(FuChunkArray) chunks = NULL; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, TPM2_MAX_DIGEST_BUFFER, error); if (chunks == NULL) return FALSE; fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { TPM2B_MAX_BUFFER data = {.size = streamsz}; g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_memcpy_safe((guint8 *)data.buffer, sizeof(data.buffer), 0x0, /* dst */ fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0x0, /* src */ fu_chunk_get_data_sz(chk), error)) return FALSE; rc = Esys_FieldUpgradeData(self->esys_context, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, &data, &next_digest, &first_digest); if (rc == TPM2_RC_COMMAND_CODE || (rc == (TPM2_RC_COMMAND_CODE | TSS2_RESMGR_RC_LAYER)) || (rc == (TPM2_RC_COMMAND_CODE | TSS2_RESMGR_TPM_RC_LAYER))) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "TPM2_FieldUpgradeData not supported: 0x%x", rc); return FALSE; } /* update progress */ fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_tpm_v2_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuTpmV2Device *self = FU_TPM_V2_DEVICE(device); TPM2B_DIGEST digest = {0x0}; TSS2_RC rc; g_autoptr(GInputStream) stream = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 99, NULL); /* validate the signature and that the authorization is valid */ rc = Esys_FieldUpgradeStart(self->esys_context, ESYS_TR_NONE, /* TODO: authorization */ ESYS_TR_NONE, /* TODO: keyHandle */ ESYS_TR_PASSWORD, ESYS_TR_NONE, ESYS_TR_NONE, &digest, NULL); if (rc == TPM2_RC_SIGNATURE) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "the signature check failed"); return FALSE; } if (rc == TPM2_RC_COMMAND_CODE || (rc == (TPM2_RC_COMMAND_CODE | TSS2_RESMGR_RC_LAYER)) || (rc == (TPM2_RC_COMMAND_CODE | TSS2_RESMGR_TPM_RC_LAYER))) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "TPM2_FieldUpgradeStart not supported: 0x%x", rc); return FALSE; } /* deploy data to device */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; if (!fu_tpm_v2_device_upgrade_data(self, stream, fu_progress_get_child(progress), error)) return FALSE; /* success! */ return TRUE; } static GBytes * fu_tpm_v2_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuTpmV2Device *self = FU_TPM_V2_DEVICE(device); TSS2_RC rc; guint chunks_max = fu_device_get_firmware_size_max(device) / TPM2_MAX_DIGEST_BUFFER; g_autoptr(GByteArray) buf = g_byte_array_new(); /* keep reading chunks until we get a zero sized response */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ); for (guint i = 0; i < chunks_max; i++) { g_autofree TPM2B_MAX_BUFFER **data = g_new0(TPM2B_MAX_BUFFER *, 1); g_debug("getting firmware chunk 0x%x", i); rc = Esys_FirmwareRead(self->esys_context, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, i /* seqnum */, (TPM2B_MAX_BUFFER **)&data); if (rc == TPM2_RC_COMMAND_CODE || (rc == (TPM2_RC_COMMAND_CODE | TSS2_RESMGR_RC_LAYER)) || (rc == (TPM2_RC_COMMAND_CODE | TSS2_RESMGR_TPM_RC_LAYER))) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "TPM2_FirmwareRead not supported: 0x%x", rc); return NULL; } if (data[0] == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "no data returned"); return NULL; } if (data[0]->size == 0) break; /* yes, the blocks are returned in reverse order */ g_byte_array_prepend(buf, data[0]->buffer, data[0]->size); Esys_Free(data[0]); } /* success */ return g_bytes_new(buf->data, buf->len); } static gboolean fu_tpm_v2_device_open(FuDevice *device, GError **error) { FuTpmV2Device *self = FU_TPM_V2_DEVICE(device); TSS2_RC rc; /* setup TSS */ rc = Esys_Initialize(&self->esys_context, NULL, NULL); if (rc != TSS2_RC_SUCCESS) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "failed to initialize TPM library"); return FALSE; } /* success */ return TRUE; } static gboolean fu_tpm_v2_device_close(FuDevice *device, GError **error) { FuTpmV2Device *self = FU_TPM_V2_DEVICE(device); Esys_Finalize(&self->esys_context); return TRUE; } static gchar * fu_tpm_v2_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint64(version_raw, fu_device_get_version_format(device)); } static void fu_tpm_v2_device_init(FuTpmV2Device *self) { fu_device_add_protocol(FU_DEVICE(self), "org.trustedcomputinggroup.tpm2"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_REBOOT); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SELF_RECOVERY); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_AFFECTS_FDE); fu_device_set_firmware_size_max(FU_DEVICE(self), 32 * 1024 * 1024); } static void fu_tpm_v2_device_class_init(FuTpmV2DeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->setup = fu_tpm_v2_device_setup; device_class->probe = fu_tpm_v2_device_probe; device_class->open = fu_tpm_v2_device_open; device_class->close = fu_tpm_v2_device_close; device_class->write_firmware = fu_tpm_v2_device_write_firmware; device_class->dump_firmware = fu_tpm_v2_device_dump_firmware; device_class->convert_version = fu_tpm_v2_device_convert_version; } FuTpmDevice * fu_tpm_v2_device_new(FuContext *ctx) { FuTpmV2Device *self; self = g_object_new(FU_TYPE_TPM_V2_DEVICE, "context", ctx, NULL); return FU_TPM_DEVICE(self); } fwupd-2.0.10/plugins/tpm/fu-tpm-v2-device.h000066400000000000000000000005731501337203100203430ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-tpm-device.h" #define FU_TYPE_TPM_V2_DEVICE (fu_tpm_v2_device_get_type()) G_DECLARE_FINAL_TYPE(FuTpmV2Device, fu_tpm_v2_device, FU, TPM_V2_DEVICE, FuTpmDevice) FuTpmDevice * fu_tpm_v2_device_new(FuContext *ctx); fwupd-2.0.10/plugins/tpm/fu-tpm.rs000066400000000000000000000031221501337203100167470ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] #[repr(u32le)] enum FuTpmEventlogItemKind { EV_PREBOOT_CERT = 0x00000000, EV_POST_CODE = 0x00000001, EV_NO_ACTION = 0x00000003, EV_SEPARATOR = 0x00000004, EV_ACTION = 0x00000005, EV_EVENT_TAG = 0x00000006, EV_S_CRTM_CONTENTS = 0x00000007, EV_S_CRTM_VERSION = 0x00000008, EV_CPU_MICROCODE = 0x00000009, EV_PLATFORM_CONFIG_FLAGS = 0x0000000a, EV_TABLE_OF_DEVICES = 0x0000000b, EV_COMPACT_HASH = 0x0000000c, EV_NONHOST_CODE = 0x0000000f, EV_NONHOST_CONFIG = 0x00000010, EV_NONHOST_INFO = 0x00000011, EV_OMIT_BOOT_DEVICE_EVENTS = 0x00000012, EV_EFI_EVENT_BASE = 0x80000000, EV_EFI_VARIABLE_DRIVER_CONFIG = 0x80000001, EV_EFI_VARIABLE_BOOT = 0x80000002, EV_EFI_BOOT_SERVICES_APPLICATION = 0x80000003, EV_EFI_BOOT_SERVICES_DRIVER = 0x80000004, EV_EFI_RUNTIME_SERVICES_DRIVER = 0x80000005, EV_EFI_GPT_EVENT = 0x80000006, EV_EFI_ACTION = 0x80000007, EV_EFI_PLATFORM_FIRMWARE_BLOB = 0x80000008, EV_EFI_HANDOFF_TABLES = 0x80000009, EV_EFI_HCRTM_EVENT = 0x80000010, EV_EFI_VARIABLE_AUTHORITY = 0x800000e0, } #[derive(Parse)] #[repr(C, packed)] struct FuStructTpmEventLog2 { pcr: u32le, type: FuTpmEventlogItemKind, digest_count: u32le, } #[derive(ParseBytes, Default)] #[repr(C, packed)] struct FuStructTpmEfiStartupLocalityEvent { signature: [char; 16] == "StartupLocality", locality: u8, // from which TPM2_Startup() was issued -- which is the initial value of PCR0 } fwupd-2.0.10/plugins/tpm/meson.build000066400000000000000000000046531501337203100173450ustar00rootroot00000000000000tpm2tss_tpm = dependency('tss2-esys', version: '>= 2.0', required: false) if hsi and tpm2tss_tpm.found() and host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginTpm"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('tpm.quirk') plugin_builtin_tpm = static_library('fu_plugin_tpm', rustgen.process('fu-tpm.rs'), sources: [ 'fu-tpm-plugin.c', 'fu-tpm-device.c', 'fu-tpm-v1-device.c', 'fu-tpm-v2-device.c', 'fu-tpm-eventlog-common.c', 'fu-tpm-eventlog-parser.c', ], include_directories: plugin_incdirs, link_with: [ fwupdplugin, fwupd, ], c_args: cargs, dependencies: [ plugin_deps, tpm2tss_tpm, ], ) plugin_builtins += plugin_builtin_tpm if get_option('tests') env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) env.set('FWUPD_LOCALSTATEDIR', '/tmp/fwupd-self-test/var') env.set('G_DEBUG', 'fatal-criticals') env.set('LSAN_OPTIONS', 'suppressions=@0@'.format(join_paths(meson.project_source_root(), 'data', 'tests', 'lsan-suppressions.txt'))) e = executable( 'tpm-self-test', rustgen.process('fu-tpm.rs'), sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: [ plugin_deps, tpm2tss_tpm, ], link_with: [ fwupd, fwupdplugin, plugin_builtin_tpm, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ cargs, '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('tpm-self-test', e, env: env) install_data([ 'tests/tpm0/active', 'tests/tpm0/caps', 'tests/tpm0/enabled', 'tests/tpm0/owned', 'tests/tpm0/pcrs', ], install_dir: join_paths(installed_test_datadir, 'tests', 'tpm0'), ) install_data([ 'tests/empty_pcr/tpm0/active', 'tests/empty_pcr/tpm0/caps', 'tests/empty_pcr/tpm0/enabled', 'tests/empty_pcr/tpm0/owned', 'tests/empty_pcr/tpm0/pcrs', ], install_dir: join_paths(installed_test_datadir, 'tests', 'empty_pcr', 'tpm0'), ) endif executable( 'fwupdtpmevlog', rustgen.process('fu-tpm.rs'), sources: [ 'fu-tpm-eventlog.c', ], include_directories: plugin_incdirs, dependencies: [ plugin_deps, tpm2tss_tpm, ], link_with: [ plugin_libs, plugin_builtin_tpm, ], ) endif fwupd-2.0.10/plugins/tpm/tests/000077500000000000000000000000001501337203100163355ustar00rootroot00000000000000fwupd-2.0.10/plugins/tpm/tests/empty_pcr/000077500000000000000000000000001501337203100203375ustar00rootroot00000000000000fwupd-2.0.10/plugins/tpm/tests/empty_pcr/tpm0/000077500000000000000000000000001501337203100212175ustar00rootroot00000000000000fwupd-2.0.10/plugins/tpm/tests/empty_pcr/tpm0/active000066400000000000000000000000021501337203100224050ustar00rootroot000000000000001 fwupd-2.0.10/plugins/tpm/tests/empty_pcr/tpm0/caps000066400000000000000000000001011501337203100220600ustar00rootroot00000000000000Manufacturer: 0x49465800 TCG version: 1.2 Firmware version: 6.40 fwupd-2.0.10/plugins/tpm/tests/empty_pcr/tpm0/enabled000066400000000000000000000000021501337203100225240ustar00rootroot000000000000001 fwupd-2.0.10/plugins/tpm/tests/empty_pcr/tpm0/owned000066400000000000000000000000021501337203100222460ustar00rootroot000000000000001 fwupd-2.0.10/plugins/tpm/tests/empty_pcr/tpm0/pcrs000066400000000000000000000031701501337203100221120ustar00rootroot00000000000000PCR-00: 3C 97 99 20 C9 00 99 60 09 27 D5 DA B3 81 EB 95 1E 7F C8 68 PCR-01: CE 9F A4 B2 01 09 D8 81 14 EA 1A 6D 13 94 CD 45 5F 52 69 23 PCR-02: 47 09 7A 9A AD C3 26 A4 93 91 26 63 A1 6F DF 53 D7 88 96 8E PCR-03: B2 A8 3B 0E BF 2F 83 74 29 9A 5B 2B DF C3 1E A9 55 AD 72 36 PCR-04: A4 A5 87 4C 59 94 8D 9B 93 66 0A F4 19 D8 6F F8 94 36 20 CC PCR-05: 00 0B 58 00 89 72 EF 6C 2A AC 79 33 C4 AE 67 6B A6 EF CF 6A PCR-06: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 PCR-07: 0A 2A 68 15 85 0D AC B2 D1 F4 E0 C1 F4 56 D5 E2 81 08 6D EA PCR-08: DB A7 29 4E 49 BA D7 9E 53 99 0A 6E 3A CB 52 97 B9 08 3A 66 PCR-09: 19 F9 6F 10 83 F5 5B 50 98 26 C3 14 73 43 35 21 1F E6 39 E9 PCR-10: 37 3D 89 9E 10 0D DD 2D 21 B5 F4 96 8D 4F DC A7 6D 1A C7 BD PCR-11: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 PCR-12: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 PCR-13: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 PCR-14: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 PCR-15: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 PCR-16: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 PCR-17: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF PCR-18: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF PCR-19: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF PCR-20: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF PCR-21: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF PCR-22: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF PCR-23: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 fwupd-2.0.10/plugins/tpm/tests/tpm0/000077500000000000000000000000001501337203100172155ustar00rootroot00000000000000fwupd-2.0.10/plugins/tpm/tests/tpm0/active000066400000000000000000000000021501337203100204030ustar00rootroot000000000000001 fwupd-2.0.10/plugins/tpm/tests/tpm0/caps000066400000000000000000000001011501337203100200560ustar00rootroot00000000000000Manufacturer: 0x49465800 TCG version: 1.2 Firmware version: 6.40 fwupd-2.0.10/plugins/tpm/tests/tpm0/enabled000066400000000000000000000000021501337203100205220ustar00rootroot000000000000001 fwupd-2.0.10/plugins/tpm/tests/tpm0/owned000066400000000000000000000000021501337203100202440ustar00rootroot000000000000001 fwupd-2.0.10/plugins/tpm/tests/tpm0/pcrs000066400000000000000000000031701501337203100201100ustar00rootroot00000000000000PCR-00: 3C 97 99 20 C9 00 99 60 09 27 D5 DA B3 81 EB 95 1E 7F C8 68 PCR-01: CE 9F A4 B2 01 09 D8 81 14 EA 1A 6D 13 94 CD 45 5F 52 69 23 PCR-02: 47 09 7A 9A AD C3 26 A4 93 91 26 63 A1 6F DF 53 D7 88 96 8E PCR-03: B2 A8 3B 0E BF 2F 83 74 29 9A 5B 2B DF C3 1E A9 55 AD 72 36 PCR-04: A4 A5 87 4C 59 94 8D 9B 93 66 0A F4 19 D8 6F F8 94 36 20 CC PCR-05: 00 0B 58 00 89 72 EF 6C 2A AC 79 33 C4 AE 67 6B A6 EF CF 6A PCR-06: B2 A8 3B 0E BF 2F 83 74 29 9A 5B 2B DF C3 1E A9 55 AD 72 36 PCR-07: 0A 2A 68 15 85 0D AC B2 D1 F4 E0 C1 F4 56 D5 E2 81 08 6D EA PCR-08: DB A7 29 4E 49 BA D7 9E 53 99 0A 6E 3A CB 52 97 B9 08 3A 66 PCR-09: 19 F9 6F 10 83 F5 5B 50 98 26 C3 14 73 43 35 21 1F E6 39 E9 PCR-10: 37 3D 89 9E 10 0D DD 2D 21 B5 F4 96 8D 4F DC A7 6D 1A C7 BD PCR-11: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 PCR-12: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 PCR-13: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 PCR-14: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 PCR-15: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 PCR-16: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 PCR-17: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF PCR-18: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF PCR-19: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF PCR-20: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF PCR-21: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF PCR-22: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF PCR-23: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 fwupd-2.0.10/plugins/tpm/tpm.quirk000066400000000000000000000000671501337203100170530ustar00rootroot00000000000000[TPM\VEN_MSFT&MOD_Pluton.TPM.A] Flags = host-cpu-child fwupd-2.0.10/plugins/uefi-capsule/000077500000000000000000000000001501337203100167555ustar00rootroot00000000000000fwupd-2.0.10/plugins/uefi-capsule/README.md000066400000000000000000000166641501337203100202510ustar00rootroot00000000000000--- title: Plugin: UEFI Capsule --- ## Introduction The Unified Extensible Firmware Interface (UEFI) is a specification that defines the software interface between an OS and platform firmware. With the UpdateCapsule boot service it can be used to update system firmware. When this plugin is enabled, the companion UEFI binary may also be built from the [fwupd-efi](https://github.com/fwupd/fwupd-efi) project if not already present on the filesystem by using the meson option `-Defi_binary=true`. For this companion binary to work with secure boot, it will need to be signed by an authority trusted with shim and/or the host environment. ## Lenovo Specific Behavior On Lenovo hardware only the boot label is set to `Linux-Firmware-Updater` rather than "Linux Firmware Updater" (with spaces) due to long-fixed EFI boot manager bugs. Many users will have these old BIOS versions installed and so we use the `use-legacy-bootmgr-desc` quirk to use the safe name. On some Lenovo hardware only one capsule is installable due to possible problems with the UpdateCapsule coalesce operation. As soon as one UEFI device has been scheduled for update the other UEFI devices found in the ESRT will be marked as `updatable-hidden` rather than `updatable`. Rebooting will restore them so they can be updated on next OS boot. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in EFI capsule file format. See the [UEFI specification](https://www.uefi.org/sites/default/files/resources/UEFI%20Spec%202_6.pdf) for details. This plugin supports the following protocol ID: * `org.uefi.capsule` ## Version Format The ESRT table is the way the firmware tells fwupd about what devices are updatable. Unfortunately the information it contains is minimal: * GUID of the sub-component * Enumerated result of the last update * Firmware version as a **32 bit unsigned number** By default, we show the ESRT devices as decimal or hexadecimal numbers as different vendors format the number in different ways. When fwupd gets information about how to format the *raw* version is converts the number into a more familiar form. When the hardware GUID is static, a quirk may be appropriate, for example: [28108d08-5027-42c2-a5b8-92d6ede9b97b] VersionFormat = quad As the GUID may be model specific, the daemon also reads the metadata and copies the version format from that. This means that `fwupdmgr get-devices` may return the UEFI device as a number initially, then once `fwupdmgr refresh` has completed it may start showing the exact same device as `A.B.C.D`, aka `quad` format. The main formats used by vendors are `triplet`, `quad`, `dell-bios` and `dell-bios-msb`. 0xAABBCCDD -> 0xAA.0xBB.0xCCCC is `triplet`, used for Lenovo 0xAABBCCDD -> 0xAA.0xBB.0xCC.0xDD is `quad`, used for HP 0xAABBCCDD -> 0xBB.0xCC.0xDD is `dell-bios`, used for Dell 0xAABBCCDD -> 0xAA.0xBB.0xCC is `dell-bios-msb`, used for Dell since CY24 There are more details about firmware version formats and a full list of all the different allowed values on the [LVFS](https://lvfs.readthedocs.io/en/latest/metainfo.html#version-format). NOTE: Firmware can require either the `quad` or `triplet` string version format, but it may be more portable to depend on the number -- which will also work if the metadata has not been refreshed yet. ## Update Behavior ### Capsule update on-disk Described in [UEFI specification](https://www.uefi.org/sites/default/files/resources/UEFI%20Spec%202_6.pdf) § 8.5.5 - Delivery of Capsules via file on Mass Storage device. If the firmware supports this, it will be the preferred method of updating when supported. You can explicitly disable it by by modifying *DisableCapsuleUpdateOnDisk* in the `uefi_capsule` section of `/etc/fwupd/fwupd.conf`. The spec expects runtime *SetVariable* to be available in order to enable this feature, we need to set `EFI_OS_INDICATIONS_FILE_CAPSULE_DELIVERY_SUPPORTED` in *OsIndications* variable to trigger processing of submitted capsule on next reboot. However some firmware implementations (e.g U-Boot), can't set the variable at runtime, but ignore the variable in next reboot and apply the capsule anyway. The directory \EFI\UpdateCapsule is checked for capsules only within the EFI system partition on the device specified in the active boot option determine by reference to *BootNext* variable or *BootOrder* variable processing. Since setting *BootNext*, for capsule update on-disk, is not yet implemented, the only available option is place the \EFI\UpdateCapsule within the ESP partition indicated by the current *BootOrder*. Note that this will be always needed if your firmware doesn't support *SetVariable* at runtime (even if *BootNext* functionality is added). ### Runtime capsule updates The firmware is deployed when the OS is running, but it is only written when the system has been restarted and the `fwupd*.efi` binary has been run. To achieve this fwupd sets up the EFI `BootNext` variable, creating the new boot entry if required. ## GUID Generation These devices use the UEFI GUID as provided in the ESRT. For compatibility with Windows 10, the plugin also adds GUIDs of the form `UEFI\RES_{$(esrt)}`. ## Vendor ID Security The vendor ID is set from the BIOS vendor, for example `DMI:LENOVO` for all devices that are not marked as supporting Firmware Management Protocol. For FMP device no vendor ID is set. ## Custom EFI System Partition (ESP) Since version 1.1.0 fwupd will autodetect the ESP if it is mounted on `/boot/efi`, `/boot`, or `/efi`, and UDisks is available on the system. In other cases the mount point of the ESP needs to be manually specified using the option *EspLocation* in `/etc/fwupd/fwupd.conf`. Setting an invalid directory will disable the fwupd plugin. ## Quirk Use This plugin uses the following plugin-specific quirks: ### `Flags=cod-indexed-filename` Use a Capsule-on-Disk filename of `CapsuleUpdateFileXXXX.bin` rather than including the ESRT GUID. This alternative format may be needed for some early InsydeH2O firmwares. ### `Flags=no-ux-capsule` Do not use the additional UX capsule. ### `Flags=use-shim-unique` Use a unique shim filename to work around a common BIOS bug. ### `Flags=use-legacy-bootmgr-desc` Use the legacy boot manager description to work around a Lenovo BIOS bug. ### `Flags=supports-boot-order-lock` The BIOS might have Boot Order Lock enabled which can cause failures when not using grub chainloading or capsule-on-disk. ### `Flags=use-shim-for-sb` Use shim to load fwupdx64.efi when SecureBoot is turned on. ### `Flags=no-rt-set-variable` Do not use RT->SetVariable. ### `Flags=no-capsule-header-fixup` Do not prepend a plausible missing capsule header. ### `Flags=enable-debugging` Enable debugging the EFI binary. ### `Flags=modify-bootorder` Modify `BootOrder` as well as `BootNext` to work around BIOS bugs. ### `Flags=cod-dell-recovery` Use Dell customized file location for the capsule on disk. ### `Flags=no-esp-backup` The UEFI ESRT entry does not need extra ESP space to write a backup ROM. If this flag is set then the `RequireESPFreeSpace` config file option is ignored. Since: 2.0.7 ## External Interface Access This plugin requires: * read/write access to the EFI system partition. * read access to `/sys/firmware/efi/esrt/` * read access to `/sys/firmware/efi/fw_platform_size` * read/write access to `/sys/firmware/efi/efivars` ## Version Considerations This plugin has been available since fwupd version `0.8.0` but was renamed to the current name in `1.5.5`. fwupd-2.0.10/plugins/uefi-capsule/fu-acpi-uefi.c000066400000000000000000000103271501337203100213760ustar00rootroot00000000000000/* * Copyright 2023 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-acpi-uefi.h" #include "fu-uefi-struct.h" #define INSYDE_QUIRK_COD_WORKING 0x1 struct _FuAcpiUefi { FuAcpiTable parent_instance; guint32 insyde_cod_status; gboolean is_insyde; gchar *guid; }; G_DEFINE_TYPE(FuAcpiUefi, fu_acpi_uefi, FU_TYPE_ACPI_TABLE) static void fu_acpi_uefi_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuAcpiUefi *self = FU_ACPI_UEFI(firmware); /* FuAcpiTable->export */ FU_FIRMWARE_CLASS(fu_acpi_uefi_parent_class)->export(firmware, flags, bn); fu_xmlb_builder_insert_kb(bn, "is_insyde", self->is_insyde); fu_xmlb_builder_insert_kx(bn, "insyde_cod_status", self->insyde_cod_status); fu_xmlb_builder_insert_kv(bn, "guid", self->guid); } static gboolean fu_acpi_uefi_parse_insyde(FuAcpiUefi *self, GInputStream *stream, GError **error) { const gchar *needle = "$QUIRK"; gsize data_offset = 0; g_autoptr(GByteArray) st_qrk = NULL; g_autoptr(GBytes) fw = NULL; fw = fu_input_stream_read_bytes(stream, 0x0, G_MAXSIZE, NULL, error); if (fw == NULL) return FALSE; if (!fu_memmem_safe(g_bytes_get_data(fw, NULL), g_bytes_get_size(fw), (const guint8 *)needle, strlen(needle), &data_offset, error)) { g_prefix_error(error, "$QUIRK not found"); return FALSE; } /* parse */ st_qrk = fu_struct_acpi_insyde_quirk_parse_stream(stream, data_offset, error); if (st_qrk == NULL) return FALSE; if (fu_struct_acpi_insyde_quirk_get_size(st_qrk) < st_qrk->len) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_READ, "$QUIRK structure is too small"); return FALSE; } self->insyde_cod_status = fu_struct_acpi_insyde_quirk_get_flags(st_qrk) & INSYDE_QUIRK_COD_WORKING; return TRUE; } static gboolean fu_acpi_uefi_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuAcpiUefi *self = FU_ACPI_UEFI(firmware); fwupd_guid_t guid = {0x0}; /* FuAcpiTable->parse */ if (!FU_FIRMWARE_CLASS(fu_acpi_uefi_parent_class) ->parse(FU_FIRMWARE(self), stream, flags, error)) return FALSE; /* check signature and read flags */ if (g_strcmp0(fu_firmware_get_id(FU_FIRMWARE(self)), "UEFI") != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Not a UEFI table, got %s", fu_firmware_get_id(FU_FIRMWARE(self))); return FALSE; } /* GUID for the table */ if (!fu_input_stream_read_safe(stream, (guint8 *)guid, sizeof(guid), 0x0, /* dst */ 0x24, /* src */ sizeof(guid), error)) return FALSE; self->guid = fwupd_guid_to_string(&guid, FWUPD_GUID_FLAG_MIXED_ENDIAN); /* parse Insyde-specific data */ self->is_insyde = (g_strcmp0(self->guid, FU_EFI_INSYDE_GUID) == 0); if (self->is_insyde) { g_autoptr(GError) error_local = NULL; if (!fu_acpi_uefi_parse_insyde(self, stream, &error_local)) g_debug("%s", error_local->message); } /* success */ return TRUE; } static void fu_acpi_uefi_init(FuAcpiUefi *self) { } static void fu_acpi_uefi_finalize(GObject *object) { FuAcpiUefi *self = FU_ACPI_UEFI(object); g_free(self->guid); G_OBJECT_CLASS(fu_acpi_uefi_parent_class)->finalize(object); } static void fu_acpi_uefi_class_init(FuAcpiUefiClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_acpi_uefi_finalize; firmware_class->parse = fu_acpi_uefi_parse; firmware_class->export = fu_acpi_uefi_export; } FuFirmware * fu_acpi_uefi_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_ACPI_UEFI, NULL)); } gboolean fu_acpi_uefi_cod_functional(FuAcpiUefi *self, GError **error) { g_return_val_if_fail(FU_IS_ACPI_UEFI(self), FALSE); if (!self->is_insyde || self->insyde_cod_status > 0) return TRUE; g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Capsule-on-Disk may have a firmware bug"); return FALSE; } gboolean fu_acpi_uefi_cod_indexed_filename(FuAcpiUefi *self) { g_return_val_if_fail(FU_IS_ACPI_UEFI(self), FALSE); if (self->is_insyde) return TRUE; return FALSE; } fwupd-2.0.10/plugins/uefi-capsule/fu-acpi-uefi.h000066400000000000000000000010171501337203100213770ustar00rootroot00000000000000/* * Copyright 2023 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_ACPI_UEFI (fu_acpi_uefi_get_type()) G_DECLARE_FINAL_TYPE(FuAcpiUefi, fu_acpi_uefi, FU, ACPI_UEFI, FuAcpiTable) #define FU_EFI_INSYDE_GUID "9d4bf935-a674-4710-ba02-bf0aa1758c7b" FuFirmware * fu_acpi_uefi_new(void); gboolean fu_acpi_uefi_cod_functional(FuAcpiUefi *self, GError **error); gboolean fu_acpi_uefi_cod_indexed_filename(FuAcpiUefi *self); fwupd-2.0.10/plugins/uefi-capsule/fu-bitmap-image.c000066400000000000000000000043611501337203100220710ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-bitmap-image.h" #include "fu-uefi-struct.h" struct _FuBitmapImage { FuFirmware parent_instance; guint32 width; guint32 height; }; G_DEFINE_TYPE(FuBitmapImage, fu_bitmap_image, FU_TYPE_FIRMWARE) static void fu_bitmap_image_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuBitmapImage *self = FU_BITMAP_IMAGE(firmware); fu_xmlb_builder_insert_kx(bn, "width", self->width); fu_xmlb_builder_insert_kx(bn, "height", self->height); } static gboolean fu_bitmap_image_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuBitmapImage *self = FU_BITMAP_IMAGE(firmware); g_autoptr(FuStructBitmapFileHeader) st_file = NULL; g_autoptr(FuStructBitmapInfoHeader) st_info = NULL; st_file = fu_struct_bitmap_file_header_parse_stream(stream, 0x0, error); if (st_file == NULL) { g_prefix_error(error, "image is corrupt: "); return FALSE; } fu_firmware_set_size(firmware, fu_struct_bitmap_file_header_get_size(st_file)); st_info = fu_struct_bitmap_info_header_parse_stream(stream, st_file->len, error); if (st_info == NULL) { g_prefix_error(error, "header is corrupt: "); return FALSE; } self->width = fu_struct_bitmap_info_header_get_width(st_info); self->height = fu_struct_bitmap_info_header_get_height(st_info); /* success */ return TRUE; } guint32 fu_bitmap_image_get_width(FuBitmapImage *self) { g_return_val_if_fail(FU_IS_BITMAP_IMAGE(self), 0); return self->width; } guint32 fu_bitmap_image_get_height(FuBitmapImage *self) { g_return_val_if_fail(FU_IS_BITMAP_IMAGE(self), 0); return self->height; } static void fu_bitmap_image_class_init(FuBitmapImageClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_bitmap_image_parse; firmware_class->export = fu_bitmap_image_export; } static void fu_bitmap_image_init(FuBitmapImage *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_STORED_SIZE); } FuBitmapImage * fu_bitmap_image_new(void) { FuBitmapImage *self; self = g_object_new(FU_TYPE_BITMAP_IMAGE, NULL); return FU_BITMAP_IMAGE(self); } fwupd-2.0.10/plugins/uefi-capsule/fu-bitmap-image.h000066400000000000000000000007021501337203100220710ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_BITMAP_IMAGE (fu_bitmap_image_get_type()) G_DECLARE_FINAL_TYPE(FuBitmapImage, fu_bitmap_image, FU, BITMAP_IMAGE, FuFirmware) FuBitmapImage * fu_bitmap_image_new(void); guint32 fu_bitmap_image_get_width(FuBitmapImage *self); guint32 fu_bitmap_image_get_height(FuBitmapImage *self); fwupd-2.0.10/plugins/uefi-capsule/fu-self-test.c000066400000000000000000000771451501337203100214550ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-bitmap-image.h" #include "fu-context-private.h" #include "fu-efivars-private.h" #include "fu-plugin-private.h" #include "fu-uefi-bgrt.h" #include "fu-uefi-capsule-backend.h" #include "fu-uefi-capsule-plugin.h" #include "fu-uefi-cod-device.h" #include "fu-uefi-common.h" #include "fu-uefi-grub-device.h" #include "fu-uefi-nvram-device.h" #include "fu-volume-private.h" static void fu_uefi_update_esp_valid_func(void) { g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuDevice) device = NULL; g_autoptr(FuFirmware) firmware = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FuVolume) volume_esp = fu_volume_new_from_mount_path("/tmp"); g_autoptr(GBytes) blob = g_bytes_new_static((const guint8 *)"BOB", 3); g_autoptr(GBytes) blob_padded = fu_bytes_pad(blob, 4 * 1024 * 1024, 0xFF); g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = g_memory_input_stream_new_from_bytes(blob_padded); /* enough to fit the firmware */ fu_volume_set_filesystem_free(volume_esp, 10 * 1024 * 1024); device = g_object_new(FU_TYPE_UEFI_CAPSULE_DEVICE, "context", ctx, NULL); fu_uefi_capsule_device_set_esp(FU_UEFI_CAPSULE_DEVICE(device), volume_esp); firmware = fu_device_prepare_firmware(device, stream, progress, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_nonnull(firmware); } static void fu_uefi_update_esp_invalid_func(void) { g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuDevice) device = NULL; g_autoptr(FuFirmware) firmware = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FuVolume) volume_esp = fu_volume_new_from_mount_path("/tmp"); g_autoptr(GBytes) blob = g_bytes_new_static((const guint8 *)"BOB", 3); g_autoptr(GBytes) blob_padded = fu_bytes_pad(blob, 4 * 1024 * 1024, 0xFF); g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = g_memory_input_stream_new_from_bytes(blob_padded); /* enough to fit the firmware */ fu_volume_set_filesystem_free(volume_esp, 1024 * 1024); device = g_object_new(FU_TYPE_UEFI_CAPSULE_DEVICE, "context", ctx, NULL); fu_uefi_capsule_device_set_esp(FU_UEFI_CAPSULE_DEVICE(device), volume_esp); firmware = fu_device_prepare_firmware(device, stream, progress, FU_FIRMWARE_PARSE_FLAG_CACHE_STREAM, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_null(firmware); } static void fu_uefi_update_esp_no_backup_func(void) { g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuDevice) device = NULL; g_autoptr(FuFirmware) firmware = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FuVolume) volume_esp = fu_volume_new_from_mount_path("/tmp"); g_autoptr(GBytes) blob = g_bytes_new_static((const guint8 *)"BOB", 3); g_autoptr(GBytes) blob_padded = fu_bytes_pad(blob, 4 * 1024 * 1024, 0xFF); g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = g_memory_input_stream_new_from_bytes(blob_padded); /* enough to fit the firmware */ fu_volume_set_filesystem_free(volume_esp, 6 * 1024 * 1024); device = g_object_new(FU_TYPE_UEFI_CAPSULE_DEVICE, "context", ctx, NULL); fu_device_add_private_flag(device, "no-esp-backup"); fu_uefi_capsule_device_set_esp(FU_UEFI_CAPSULE_DEVICE(device), volume_esp); firmware = fu_device_prepare_firmware(device, stream, progress, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_nonnull(firmware); } static void fu_uefi_bgrt_func(void) { gboolean ret; g_autoptr(GError) error = NULL; g_autoptr(FuUefiBgrt) bgrt = fu_uefi_bgrt_new(); ret = fu_uefi_bgrt_setup(bgrt, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_true(fu_uefi_bgrt_get_supported(bgrt)); g_assert_cmpint(fu_uefi_bgrt_get_xoffset(bgrt), ==, 123); g_assert_cmpint(fu_uefi_bgrt_get_yoffset(bgrt), ==, 456); g_assert_cmpint(fu_uefi_bgrt_get_width(bgrt), ==, 54); g_assert_cmpint(fu_uefi_bgrt_get_height(bgrt), ==, 24); } static void fu_uefi_framebuffer_func(void) { gboolean ret; guint32 height = 0; guint32 width = 0; g_autoptr(GError) error = NULL; ret = fu_uefi_get_framebuffer_size(&width, &height, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(width, ==, 800); g_assert_cmpint(height, ==, 600); } static void fu_uefi_bitmap_func(void) { gboolean ret; g_autofree gchar *fn = NULL; g_autoptr(FuBitmapImage) bmp_image = fu_bitmap_image_new(); g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = NULL; fn = g_test_build_filename(G_TEST_DIST, "tests", "test.bmp", NULL); stream = fu_input_stream_from_path(fn, &error); g_assert_no_error(error); g_assert_nonnull(stream); ret = fu_firmware_parse_stream(FU_FIRMWARE(bmp_image), stream, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(fu_bitmap_image_get_width(bmp_image), ==, 54); g_assert_cmpint(fu_bitmap_image_get_height(bmp_image), ==, 24); } static GBytes * fu_uefi_cod_device_build_efi_result(const gchar *guidstr) { fwupd_guid_t guid = {0x0}; gboolean ret; guint8 timestamp[16] = {0x0}; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GError) error = NULL; fu_byte_array_append_uint32(buf, 0x3A, G_LITTLE_ENDIAN); /* VariableTotalSize */ fu_byte_array_append_uint32(buf, 0xFF, G_LITTLE_ENDIAN); /* Reserved */ ret = fwupd_guid_from_string(guidstr, &guid, FWUPD_GUID_FLAG_MIXED_ENDIAN, &error); g_assert_no_error(error); g_assert_true(ret); g_byte_array_append(buf, guid, sizeof(guid)); /* CapsuleGuid */ g_byte_array_append(buf, timestamp, sizeof(timestamp)); /* CapsuleProcessed */ fu_byte_array_append_uint32(buf, FU_UEFI_CAPSULE_DEVICE_STATUS_ERROR_PWR_EVT_BATT, G_LITTLE_ENDIAN); /* Status */ return g_bytes_new(buf->data, buf->len); } static void fu_uefi_cod_device_func(void) { gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuDevice) dev = NULL; g_autoptr(GError) error = NULL; g_autofree gchar *str = NULL; FuEfivars *efivars = fu_context_get_efivars(ctx); g_autoptr(GBytes) cap0 = NULL; g_autoptr(GBytes) cap1 = NULL; g_autoptr(GBytes) last = NULL; g_autoptr(GBytes) max = NULL; last = fu_utf8_to_utf16_bytes("Capsule0001", G_LITTLE_ENDIAN, FU_UTF_CONVERT_FLAG_NONE, &error); g_assert_no_error(error); g_assert_nonnull(last); ret = fu_efivars_set_data_bytes(efivars, FU_EFIVARS_GUID_EFI_CAPSULE_REPORT, "CapsuleLast", last, 0, &error); g_assert_no_error(error); g_assert_true(ret); max = fu_utf8_to_utf16_bytes("Capsule9999", G_LITTLE_ENDIAN, FU_UTF_CONVERT_FLAG_NONE, &error); g_assert_no_error(error); g_assert_nonnull(last); ret = fu_efivars_set_data_bytes(efivars, FU_EFIVARS_GUID_EFI_CAPSULE_REPORT, "CapsuleMax", max, 0, &error); g_assert_no_error(error); g_assert_true(ret); cap0 = fu_uefi_cod_device_build_efi_result("99999999-bf9d-540b-b92b-172ce31013c1"); ret = fu_efivars_set_data_bytes(efivars, FU_EFIVARS_GUID_EFI_CAPSULE_REPORT, "Capsule0000", cap0, 0, &error); g_assert_no_error(error); g_assert_true(ret); cap1 = fu_uefi_cod_device_build_efi_result("cc4cbfa9-bf9d-540b-b92b-172ce31013c1"); ret = fu_efivars_set_data_bytes(efivars, FU_EFIVARS_GUID_EFI_CAPSULE_REPORT, "Capsule0001", cap1, 0, &error); g_assert_no_error(error); g_assert_true(ret); /* create device */ dev = g_object_new(FU_TYPE_UEFI_COD_DEVICE, "context", ctx, "fw-class", "cc4cbfa9-bf9d-540b-b92b-172ce31013c1", NULL); ret = fu_device_get_results(dev, &error); g_assert_no_error(error); g_assert_true(ret); /* debug */ str = fu_device_to_string(dev); g_debug("%s", str); g_assert_cmpint(fu_device_get_update_state(dev), ==, FWUPD_UPDATE_STATE_FAILED_TRANSIENT); g_assert_cmpstr(fu_device_get_update_error(dev), ==, "failed to update to 0: error-pwr-evt-batt"); g_assert_cmpint(fu_uefi_capsule_device_get_status(FU_UEFI_CAPSULE_DEVICE(dev)), ==, FU_UEFI_CAPSULE_DEVICE_STATUS_ERROR_PWR_EVT_BATT); } static FuVolume * fu_uefi_plugin_fake_esp_new(void) { g_autofree gchar *tmpdir_efi = NULL; g_autofree gchar *tmpdir = NULL; g_autoptr(FuVolume) esp = NULL; g_autoptr(GError) error = NULL; /* enough to fit the firmware */ tmpdir = g_dir_make_tmp("fwupd-esp-XXXXXX", &error); g_assert_no_error(error); g_assert_nonnull(tmpdir); esp = fu_volume_new_from_mount_path(tmpdir); fu_volume_set_filesystem_free(esp, 10 * 1024 * 1024); fu_volume_set_partition_kind(esp, FU_VOLUME_KIND_ESP); fu_volume_set_partition_uuid(esp, "00000000-0000-0000-0000-000000000000"); /* make fu_uefi_get_esp_path_for_os() distro-neutral */ tmpdir_efi = g_build_filename(tmpdir, "EFI", "systemd", NULL); g_assert_cmpint(g_mkdir_with_parents(tmpdir_efi, 0700), ==, 0); /* success */ return g_steal_pointer(&esp); } static void fu_uefi_plugin_no_coalesce_func(void) { FuUefiCapsuleDevice *dev1; FuUefiCapsuleDevice *dev2; GPtrArray *devices; gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuPlugin) plugin = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FuVolume) esp = fu_uefi_plugin_fake_esp_new(); g_autoptr(GError) error = NULL; #ifndef __linux__ g_test_skip("ESRT data is mocked only on Linux"); return; #endif /* override ESP */ fu_context_add_esp_volume(ctx, esp); /* set up at least one HWID */ fu_config_set_default(fu_context_get_config(ctx), "fwupd", "Manufacturer", "fwupd"); /* load dummy hwids */ ret = fu_context_load_hwinfo(ctx, progress, FU_CONTEXT_HWID_FLAG_LOAD_CONFIG, &error); g_assert_no_error(error); g_assert_true(ret); /* create plugin, and ->startup then ->coldplug */ plugin = g_object_new(FU_TYPE_UEFI_CAPSULE_PLUGIN, "context", ctx, "name", "uefi_capsule", NULL); ret = fu_plugin_runner_startup(plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_plugin_runner_coldplug(plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* do not save silo */ ret = fu_context_load_quirks(ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE, &error); g_assert_no_error(error); g_assert_true(ret); /* check each device */ devices = fu_plugin_get_devices(plugin); g_assert_cmpint(devices->len, ==, 2); /* system firmware */ dev1 = g_ptr_array_index(devices, 0); g_assert_cmpint(fu_uefi_capsule_device_get_kind(dev1), ==, FU_UEFI_CAPSULE_DEVICE_KIND_SYSTEM_FIRMWARE); g_assert_cmpstr(fu_uefi_capsule_device_get_guid(dev1), ==, "ddc0ee61-e7f0-4e7d-acc5-c070a398838e"); g_assert_cmpint(fu_uefi_capsule_device_get_hardware_instance(dev1), ==, 0x0); g_assert_cmpint(fu_uefi_capsule_device_get_version(dev1), ==, 65586); g_assert_cmpint(fu_uefi_capsule_device_get_version_lowest(dev1), ==, 65582); g_assert_cmpint(fu_uefi_capsule_device_get_version_error(dev1), ==, 18472960); g_assert_cmpint(fu_uefi_capsule_device_get_capsule_flags(dev1), ==, 0xfe); g_assert_cmpint(fu_uefi_capsule_device_get_status(dev1), ==, FU_UEFI_CAPSULE_DEVICE_STATUS_ERROR_UNSUCCESSFUL); g_assert_true(fu_device_has_flag(FU_DEVICE(dev1), FWUPD_DEVICE_FLAG_UPDATABLE)); /* system firmware */ dev2 = g_ptr_array_index(devices, 1); g_assert_cmpint(fu_uefi_capsule_device_get_kind(dev2), ==, FU_UEFI_CAPSULE_DEVICE_KIND_DEVICE_FIRMWARE); g_assert_cmpstr(fu_uefi_capsule_device_get_guid(dev2), ==, "671d19d0-d43c-4852-98d9-1ce16f9967e4"); g_assert_cmpint(fu_uefi_capsule_device_get_version(dev2), ==, 3090287969); g_assert_cmpint(fu_uefi_capsule_device_get_version_lowest(dev2), ==, 1); g_assert_cmpint(fu_uefi_capsule_device_get_version_error(dev2), ==, 0); g_assert_cmpint(fu_uefi_capsule_device_get_capsule_flags(dev2), ==, 32784); g_assert_cmpint(fu_uefi_capsule_device_get_status(dev2), ==, FU_UEFI_CAPSULE_DEVICE_STATUS_SUCCESS); g_assert_true(fu_device_has_flag(FU_DEVICE(dev2), FWUPD_DEVICE_FLAG_UPDATABLE)); /* ensure the other device is not updatable when the first is updated */ fu_device_set_update_state(FU_DEVICE(dev2), FWUPD_UPDATE_STATE_NEEDS_REBOOT); g_assert_false(fu_device_has_flag(FU_DEVICE(dev1), FWUPD_DEVICE_FLAG_UPDATABLE)); } static void fu_uefi_plugin_no_flashes_func(void) { gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuDevice) device = NULL; g_autoptr(FuFirmware) firmware = fu_firmware_new(); g_autoptr(FuPlugin) plugin = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FuVolume) esp = fu_uefi_plugin_fake_esp_new(); g_autoptr(GBytes) blob = g_bytes_new_static("GUIDGUIDGUIDGUID", 16); g_autoptr(GError) error = NULL; /* override ESP */ fu_context_add_esp_volume(ctx, esp); /* load dummy hwids */ ret = fu_context_load_hwinfo(ctx, progress, FU_CONTEXT_HWID_FLAG_LOAD_CONFIG, &error); g_assert_no_error(error); g_assert_true(ret); /* create plugin, and ->startup then ->coldplug */ plugin = g_object_new(FU_TYPE_UEFI_CAPSULE_PLUGIN, "context", ctx, "name", "uefi_capsule", NULL); ret = fu_plugin_runner_startup(plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_plugin_runner_coldplug(plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* test with almost no flashes left */ device = g_object_new(FU_TYPE_UEFI_NVRAM_DEVICE, "context", ctx, "fw-class", "cc4cbfa9-bf9d-540b-b92b-172ce31013c1", NULL); fu_device_add_private_flag(device, FU_UEFI_CAPSULE_DEVICE_FLAG_NO_UX_CAPSULE); fu_device_set_flashes_left(device, 2); ret = fu_plugin_runner_write_firmware(plugin, device, firmware, progress, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_false(ret); } static gboolean fu_uefi_plugin_esp_file_exists(FuVolume *esp, const gchar *filename) { g_autofree gchar *mount_point = fu_volume_get_mount_point(esp); g_autofree gchar *fn = g_build_filename(mount_point, filename, NULL); return g_file_test(fn, G_FILE_TEST_EXISTS); } static void fu_uefi_plugin_esp_rmtree(FuVolume *esp) { gboolean ret; g_autofree gchar *mount_point = fu_volume_get_mount_point(esp); g_autoptr(GError) error = NULL; ret = fu_path_rmtree(mount_point, &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_uefi_plugin_nvram_func(void) { gboolean ret; guint16 bootnext = 0; guint16 idx; g_autoptr(GBytes) blob = g_bytes_new_static("GUIDGUIDGUIDGUID", 16); g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuDevice) device = NULL; g_autoptr(FuEfiLoadOption) loadopt = fu_efi_load_option_new(); g_autoptr(FuFirmware) firmware = fu_firmware_new_from_bytes(blob); g_autoptr(FuPlugin) plugin = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FuVolume) esp = fu_uefi_plugin_fake_esp_new(); g_autoptr(GArray) bootorder = NULL; g_autoptr(GError) error = NULL; #ifndef __x86_64__ g_test_skip("NVRAM binary is mocked only for x86_64"); return; #endif /* override ESP */ fu_context_add_esp_volume(ctx, esp); /* set up system so that secure boot is on */ ret = fu_efivars_set_secure_boot(fu_context_get_efivars(ctx), TRUE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_efivars_create_boot_entry_for_volume(fu_context_get_efivars(ctx), 0x0000, esp, "Fedora", "grubx64.efi", &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_efivars_set_boot_current(fu_context_get_efivars(ctx), 0x0000, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_efivars_build_boot_order(fu_context_get_efivars(ctx), &error, 0x0000, G_MAXUINT16); g_assert_no_error(error); g_assert_true(ret); /* load dummy hwids */ ret = fu_context_load_hwinfo(ctx, progress, FU_CONTEXT_HWID_FLAG_LOAD_CONFIG, &error); g_assert_no_error(error); g_assert_true(ret); /* create plugin, and ->startup then ->coldplug */ plugin = g_object_new(FU_TYPE_UEFI_CAPSULE_PLUGIN, "context", ctx, "name", "uefi_capsule", NULL); fu_plugin_set_config_default(plugin, "ScreenWidth", "800"); fu_plugin_set_config_default(plugin, "ScreenHeight", "600"); ret = fu_plugin_runner_startup(plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_plugin_runner_coldplug(plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* test with a dummy device that just writes the splash */ device = g_object_new(FU_TYPE_UEFI_NVRAM_DEVICE, "context", ctx, "fw-class", "cc4cbfa9-bf9d-540b-b92b-172ce31013c1", NULL); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_private_flag(device, FU_UEFI_CAPSULE_DEVICE_FLAG_USE_FWUPD_EFI); fu_device_add_private_flag(device, FU_UEFI_CAPSULE_DEVICE_FLAG_USE_LEGACY_BOOTMGR_DESC); fu_device_add_private_flag(device, FU_UEFI_CAPSULE_DEVICE_FLAG_MODIFY_BOOTORDER); fu_uefi_capsule_device_set_esp(FU_UEFI_CAPSULE_DEVICE(device), esp); ret = fu_device_prepare(device, progress, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_plugin_runner_write_firmware(plugin, device, firmware, progress, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); /* check UX splash was created */ g_assert_true(fu_uefi_plugin_esp_file_exists( esp, "EFI/systemd/fw/fwupd-3b8c8162-188c-46a4-aec9-be43f1d65697.cap")); g_assert_true(fu_efivars_exists(fu_context_get_efivars(ctx), FU_EFIVARS_GUID_FWUPDATE, "fwupd-ux-capsule")); /* check FW was created */ g_assert_true(fu_uefi_plugin_esp_file_exists( esp, "EFI/systemd/fw/fwupd-cc4cbfa9-bf9d-540b-b92b-172ce31013c1.cap")); g_assert_true(fu_efivars_exists(fu_context_get_efivars(ctx), FU_EFIVARS_GUID_FWUPDATE, "fwupd-ux-capsule")); g_assert_true(fu_efivars_exists(fu_context_get_efivars(ctx), FU_EFIVARS_GUID_FWUPDATE, "fwupd-cc4cbfa9-bf9d-540b-b92b-172ce31013c1-0")); /* we skipped this, so emulate something */ g_assert_no_error(error); g_assert_true(ret); /* verify BootOrder */ bootorder = fu_efivars_get_boot_order(fu_context_get_efivars(ctx), &error); g_assert_no_error(error); g_assert_nonnull(bootorder); g_assert_cmpint(bootorder->len, ==, 2); idx = g_array_index(bootorder, guint16, 0); g_assert_cmpint(idx, ==, 0x0000); idx = g_array_index(bootorder, guint16, 1); g_assert_cmpint(idx, ==, 0x0001); /* verify BootNext */ ret = fu_efivars_get_boot_next(fu_context_get_efivars(ctx), &bootnext, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(bootnext, ==, 0x0001); /* clear results */ ret = fu_plugin_runner_clear_results(plugin, device, &error); g_assert_no_error(error); g_assert_true(ret); /* cleanup */ ret = fu_plugin_runner_reboot_cleanup(plugin, device, &error); g_assert_no_error(error); g_assert_true(ret); /* check both files and variables no longer exist */ g_assert_false(fu_uefi_plugin_esp_file_exists( esp, "EFI/systemd/fw/fwupd-3b8c8162-188c-46a4-aec9-be43f1d65697.cap")); g_assert_false(fu_efivars_exists(fu_context_get_efivars(ctx), FU_EFIVARS_GUID_FWUPDATE, "fwupd-ux-capsule")); g_assert_false(fu_uefi_plugin_esp_file_exists( esp, "EFI/systemd/fw/fwupd-cc4cbfa9-bf9d-540b-b92b-172ce31013c1.cap")); g_assert_false(fu_efivars_exists(fu_context_get_efivars(ctx), FU_EFIVARS_GUID_FWUPDATE, "fwupd-cc4cbfa9-bf9d-540b-b92b-172ce31013c1-0")); /* check BootNext was removed */ g_assert_false( fu_efivars_exists(fu_context_get_efivars(ctx), FU_EFIVARS_GUID_EFI_GLOBAL, "BootNext")); /* get results */ ret = fu_device_get_results(device, &error); g_assert_no_error(error); g_assert_true(ret); /* cleanup */ fu_uefi_plugin_esp_rmtree(esp); } static void fu_uefi_plugin_cod_func(void) { gboolean ret; g_autoptr(GBytes) blob = g_bytes_new_static("GUIDGUIDGUIDGUID", 16); g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuDevice) device = NULL; g_autoptr(FuEfiLoadOption) loadopt = fu_efi_load_option_new(); g_autoptr(FuFirmware) firmware = fu_firmware_new_from_bytes(blob); g_autoptr(FuPlugin) plugin = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FuVolume) esp = fu_uefi_plugin_fake_esp_new(); g_autoptr(GByteArray) buf_last = NULL; g_autoptr(GError) error = NULL; /* override ESP */ fu_context_add_esp_volume(ctx, esp); /* set up system */ buf_last = fu_utf8_to_utf16_byte_array("Capsule0001", G_LITTLE_ENDIAN, FU_UTF_CONVERT_FLAG_NONE, &error); g_assert_no_error(error); g_assert_nonnull(buf_last); ret = fu_efivars_set_data(fu_context_get_efivars(ctx), FU_EFIVARS_GUID_EFI_CAPSULE_REPORT, "CapsuleLast", buf_last->data, buf_last->len, 0x0, &error); g_assert_no_error(error); g_assert_true(ret); /* load dummy hwids */ ret = fu_context_load_hwinfo(ctx, progress, FU_CONTEXT_HWID_FLAG_LOAD_CONFIG, &error); g_assert_no_error(error); g_assert_true(ret); /* create plugin, and ->startup then ->coldplug */ plugin = g_object_new(FU_TYPE_UEFI_CAPSULE_PLUGIN, "context", ctx, "name", "uefi_capsule", NULL); ret = fu_plugin_runner_startup(plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_plugin_runner_coldplug(plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* test with a dummy device that just writes the splash */ device = g_object_new(FU_TYPE_UEFI_COD_DEVICE, "context", ctx, "fw-class", "cc4cbfa9-bf9d-540b-b92b-172ce31013c1", NULL); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_private_flag(device, FU_UEFI_CAPSULE_DEVICE_FLAG_NO_UX_CAPSULE); fu_uefi_capsule_device_set_esp(FU_UEFI_CAPSULE_DEVICE(device), esp); /* write default capsule */ ret = fu_plugin_runner_write_firmware(plugin, device, firmware, progress, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_true(fu_uefi_plugin_esp_file_exists( esp, "EFI/UpdateCapsule/fwupd-cc4cbfa9-bf9d-540b-b92b-172ce31013c1.cap")); /* try again with a different filename */ fu_device_add_private_flag(device, FU_UEFI_CAPSULE_DEVICE_FLAG_COD_DELL_RECOVERY); ret = fu_plugin_runner_write_firmware(plugin, device, firmware, progress, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_true(fu_uefi_plugin_esp_file_exists(esp, "EFI/dell/bios/recovery/BIOS_TRS.rcv")); /* try again with a different filename */ fu_device_add_private_flag(device, FU_UEFI_CAPSULE_DEVICE_FLAG_COD_INDEXED_FILENAME); ret = fu_plugin_runner_write_firmware(plugin, device, firmware, progress, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_true( fu_uefi_plugin_esp_file_exists(esp, "EFI/UpdateCapsule/CapsuleUpdateFile0000.bin")); /* get results */ ret = fu_device_get_results(device, &error); g_assert_no_error(error); g_assert_true(ret); /* cleanup */ fu_uefi_plugin_esp_rmtree(esp); } static void fu_uefi_plugin_grub_func(void) { gboolean ret; g_autoptr(GBytes) blob = g_bytes_new_static("GUIDGUIDGUIDGUID", 16); g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuDevice) device = NULL; g_autoptr(FuEfiLoadOption) loadopt = fu_efi_load_option_new(); g_autoptr(FuFirmware) firmware = fu_firmware_new_from_bytes(blob); g_autoptr(FuPlugin) plugin = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FuVolume) esp = fu_uefi_plugin_fake_esp_new(); g_autoptr(GByteArray) buf_last = NULL; g_autoptr(GError) error = NULL; #ifndef __x86_64__ g_test_skip("ESRT is mocked only for x86_64"); return; #endif /* set up system so that secure boot is on */ ret = fu_efivars_set_secure_boot(fu_context_get_efivars(ctx), TRUE, &error); g_assert_no_error(error); g_assert_true(ret); /* load dummy hwids */ ret = fu_context_load_hwinfo(ctx, progress, FU_CONTEXT_HWID_FLAG_LOAD_CONFIG, &error); g_assert_no_error(error); g_assert_true(ret); /* override ESP */ fu_context_add_esp_volume(ctx, esp); /* create plugin, and ->startup then ->coldplug */ plugin = g_object_new(FU_TYPE_UEFI_CAPSULE_PLUGIN, "context", ctx, "name", "uefi_capsule", NULL); ret = fu_plugin_runner_startup(plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_plugin_runner_coldplug(plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* test with a dummy device */ device = g_object_new(FU_TYPE_UEFI_GRUB_DEVICE, "context", ctx, "fw-class", "cc4cbfa9-bf9d-540b-b92b-172ce31013c1", NULL); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_private_flag(device, FU_UEFI_CAPSULE_DEVICE_FLAG_NO_UX_CAPSULE); fu_uefi_capsule_device_set_esp(FU_UEFI_CAPSULE_DEVICE(device), esp); /* write */ ret = fu_device_prepare(device, progress, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_plugin_runner_write_firmware(plugin, device, firmware, progress, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_true(fu_uefi_plugin_esp_file_exists( esp, "EFI/systemd/fw/fwupd-cc4cbfa9-bf9d-540b-b92b-172ce31013c1.cap")); /* cleanup */ fu_uefi_plugin_esp_rmtree(esp); } static void fu_uefi_update_info_xml_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autofree gchar *csum1 = NULL; g_autofree gchar *csum2 = NULL; g_autofree gchar *xml_out = NULL; g_autofree gchar *xml_src = NULL; g_autoptr(FuFirmware) firmware1 = FU_FIRMWARE(fu_uefi_update_info_new()); g_autoptr(FuFirmware) firmware2 = FU_FIRMWARE(fu_uefi_update_info_new()); g_autoptr(FuFirmware) firmware3 = FU_FIRMWARE(fu_uefi_update_info_new()); g_autoptr(GBytes) fw = NULL; g_autoptr(GError) error = NULL; /* build and write */ filename = g_test_build_filename(G_TEST_DIST, "tests", "uefi-update-info.builder.xml", NULL); ret = g_file_get_contents(filename, &xml_src, NULL, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_firmware_build_from_xml(firmware1, xml_src, &error); g_assert_no_error(error); g_assert_true(ret); fw = fu_firmware_write(firmware1, &error); g_assert_no_error(error); g_assert_nonnull(fw); csum1 = fu_firmware_get_checksum(firmware1, G_CHECKSUM_SHA1, &error); g_assert_no_error(error); g_assert_cmpstr(csum1, ==, "18e8c43a912d3918498723340ae80a57d8b0657c"); /* ensure we can parse */ ret = fu_firmware_parse_bytes(firmware3, fw, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); /* ensure we can round-trip */ xml_out = fu_firmware_export_to_xml(firmware1, FU_FIRMWARE_EXPORT_FLAG_NONE, &error); g_assert_no_error(error); ret = fu_firmware_build_from_xml(firmware2, xml_out, &error); g_assert_no_error(error); g_assert_true(ret); csum2 = fu_firmware_get_checksum(firmware2, G_CHECKSUM_SHA1, &error); g_assert_cmpstr(csum1, ==, csum2); } static void fu_uefi_update_info_func(void) { FuUefiCapsuleDevice *dev; gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuBackend) backend = fu_uefi_capsule_backend_new(ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FuUefiUpdateInfo) info2 = fu_uefi_update_info_new(); g_autoptr(FuUefiUpdateInfo) info = NULL; g_autoptr(GBytes) info2_blob = NULL; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) devices = NULL; FuEfivars *efivars = fu_context_get_efivars(ctx); /* create some fake data */ fu_uefi_update_info_set_guid(info2, "697bd920-12cf-4da9-8385-996909bc6559"); fu_uefi_update_info_set_capsule_fn( info2, "/EFI/fedora/fw/fwupd-697bd920-12cf-4da9-8385-996909bc6559.cap"); fu_uefi_update_info_set_hw_inst(info2, 0); fu_uefi_update_info_set_capsule_flags(info2, 0x50000); fu_uefi_update_info_set_status(info2, FU_UEFI_UPDATE_INFO_STATUS_ATTEMPT_UPDATE); info2_blob = fu_firmware_write(FU_FIRMWARE(info2), &error); g_assert_no_error(error); g_assert_nonnull(info2_blob); ret = fu_efivars_set_data_bytes(efivars, FU_EFIVARS_GUID_FWUPDATE, "fwupd-ddc0ee61-e7f0-4e7d-acc5-c070a398838e-0", info2_blob, 0, &error); g_assert_no_error(error); g_assert_true(ret); /* add each device */ ret = fu_backend_coldplug(backend, progress, &error); g_assert_no_error(error); g_assert_true(ret); devices = fu_backend_get_devices(backend); g_assert_cmpint(devices->len, ==, 3); dev = g_ptr_array_index(devices, 0); g_assert_cmpint(fu_uefi_capsule_device_get_kind(dev), ==, FU_UEFI_CAPSULE_DEVICE_KIND_SYSTEM_FIRMWARE); g_assert_cmpstr(fu_uefi_capsule_device_get_guid(dev), ==, "ddc0ee61-e7f0-4e7d-acc5-c070a398838e"); info = fu_uefi_capsule_device_load_update_info(dev, &error); g_assert_no_error(error); g_assert_nonnull(info); g_assert_cmpint(fu_firmware_get_version_raw(FU_FIRMWARE(info)), ==, 0x7); g_assert_cmpstr(fu_uefi_update_info_get_guid(info), ==, "697bd920-12cf-4da9-8385-996909bc6559"); g_assert_cmpint(fu_uefi_update_info_get_capsule_flags(info), ==, 0x50000); g_assert_cmpint(fu_uefi_update_info_get_hw_inst(info), ==, 0x0); g_assert_cmpint(fu_uefi_update_info_get_status(info), ==, FU_UEFI_UPDATE_INFO_STATUS_ATTEMPT_UPDATE); g_assert_cmpstr(fu_uefi_update_info_get_capsule_fn(info), ==, "/EFI/fedora/fw/fwupd-697bd920-12cf-4da9-8385-996909bc6559.cap"); } int main(int argc, char **argv) { g_autofree gchar *testdatadir = NULL; g_autofree gchar *testdatadir_mut = NULL; (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); testdatadir = g_test_build_filename(G_TEST_DIST, "tests", NULL); testdatadir_mut = g_test_build_filename(G_TEST_BUILT, "tests", NULL); (void)g_setenv("FWUPD_SYSFSFWDIR", testdatadir, TRUE); (void)g_setenv("FWUPD_EFIVARS", "dummy", TRUE); (void)g_setenv("FWUPD_SYSFSDRIVERDIR", testdatadir, TRUE); (void)g_setenv("FWUPD_SYSFSFWATTRIBDIR", testdatadir, TRUE); (void)g_setenv("FWUPD_DATADIR_QUIRKS", testdatadir, TRUE); (void)g_setenv("FWUPD_HOSTFS_BOOT", testdatadir, TRUE); (void)g_setenv("FWUPD_EFIAPPDIR", testdatadir_mut, TRUE); (void)g_setenv("FWUPD_ACPITABLESDIR", testdatadir_mut, TRUE); (void)g_setenv("FWUPD_DATADIR", g_test_get_dir(G_TEST_BUILT), TRUE); (void)g_setenv("FWUPD_UEFI_TEST", "1", TRUE); (void)g_setenv("LANGUAGE", "en", TRUE); (void)g_setenv("PATH", testdatadir, TRUE); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); (void)g_setenv("G_MESSAGES_DEBUG", "all", TRUE); /* tests go here */ g_test_add_func("/uefi/update-esp-valid", fu_uefi_update_esp_valid_func); g_test_add_func("/uefi/update-esp-invalid", fu_uefi_update_esp_invalid_func); g_test_add_func("/uefi/update-esp-no-backup", fu_uefi_update_esp_no_backup_func); g_test_add_func("/uefi/bgrt", fu_uefi_bgrt_func); g_test_add_func("/uefi/framebuffer", fu_uefi_framebuffer_func); g_test_add_func("/uefi/bitmap", fu_uefi_bitmap_func); g_test_add_func("/uefi/cod-device", fu_uefi_cod_device_func); g_test_add_func("/uefi/update-info", fu_uefi_update_info_func); g_test_add_func("/uefi/update-info{xml}", fu_uefi_update_info_xml_func); g_test_add_func("/uefi/plugin{no-coalesce}", fu_uefi_plugin_no_coalesce_func); g_test_add_func("/uefi/plugin{no-flashes-left}", fu_uefi_plugin_no_flashes_func); g_test_add_func("/uefi/plugin{nvram}", fu_uefi_plugin_nvram_func); g_test_add_func("/uefi/plugin{cod}", fu_uefi_plugin_cod_func); g_test_add_func("/uefi/plugin{grub}", fu_uefi_plugin_grub_func); return g_test_run(); } fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-bgrt.c000066400000000000000000000057561501337203100214320ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-bitmap-image.h" #include "fu-uefi-bgrt.h" #include "fu-uefi-common.h" struct _FuUefiBgrt { GObject parent_instance; guint32 xoffset; guint32 yoffset; guint32 width; guint32 height; }; G_DEFINE_TYPE(FuUefiBgrt, fu_uefi_bgrt, G_TYPE_OBJECT) gboolean fu_uefi_bgrt_setup(FuUefiBgrt *self, GError **error) { guint64 type; guint64 version; g_autofree gchar *bgrtdir = NULL; g_autofree gchar *imagefn = NULL; g_autofree gchar *sysfsfwdir = NULL; g_autoptr(FuBitmapImage) bmp_image = fu_bitmap_image_new(); g_autoptr(GFile) file = NULL; g_return_val_if_fail(FU_IS_UEFI_BGRT(self), FALSE); sysfsfwdir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR_FW); bgrtdir = g_build_filename(sysfsfwdir, "acpi", "bgrt", NULL); if (!g_file_test(bgrtdir, G_FILE_TEST_EXISTS)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "BGRT is not supported"); return FALSE; } type = fu_uefi_read_file_as_uint64(bgrtdir, "type"); if (type != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "BGRT type was %" G_GUINT64_FORMAT, type); return FALSE; } version = fu_uefi_read_file_as_uint64(bgrtdir, "version"); if (version != 1) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "BGRT version was %" G_GUINT64_FORMAT, version); return FALSE; } /* load image */ self->xoffset = fu_uefi_read_file_as_uint64(bgrtdir, "xoffset"); self->yoffset = fu_uefi_read_file_as_uint64(bgrtdir, "yoffset"); imagefn = g_build_filename(bgrtdir, "image", NULL); file = g_file_new_build_filename(bgrtdir, "image", NULL); if (!fu_firmware_parse_file(FU_FIRMWARE(bmp_image), file, FU_FIRMWARE_PARSE_FLAG_NONE, error)) { g_prefix_error(error, "BGRT image invalid: "); return FALSE; } self->width = fu_bitmap_image_get_width(bmp_image); self->height = fu_bitmap_image_get_height(bmp_image); /* success */ return TRUE; } gboolean fu_uefi_bgrt_get_supported(FuUefiBgrt *self) { g_return_val_if_fail(FU_IS_UEFI_BGRT(self), FALSE); if (self->width == 0 || self->height == 0) return FALSE; return TRUE; } guint32 fu_uefi_bgrt_get_xoffset(FuUefiBgrt *self) { g_return_val_if_fail(FU_IS_UEFI_BGRT(self), 0); return self->xoffset; } guint32 fu_uefi_bgrt_get_yoffset(FuUefiBgrt *self) { g_return_val_if_fail(FU_IS_UEFI_BGRT(self), 0); return self->yoffset; } guint32 fu_uefi_bgrt_get_width(FuUefiBgrt *self) { g_return_val_if_fail(FU_IS_UEFI_BGRT(self), 0); return self->width; } guint32 fu_uefi_bgrt_get_height(FuUefiBgrt *self) { g_return_val_if_fail(FU_IS_UEFI_BGRT(self), 0); return self->height; } static void fu_uefi_bgrt_class_init(FuUefiBgrtClass *klass) { } static void fu_uefi_bgrt_init(FuUefiBgrt *self) { } FuUefiBgrt * fu_uefi_bgrt_new(void) { FuUefiBgrt *self; self = g_object_new(FU_TYPE_UEFI_BGRT, NULL); return FU_UEFI_BGRT(self); } fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-bgrt.h000066400000000000000000000011741501337203100214250ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_UEFI_BGRT (fu_uefi_bgrt_get_type()) G_DECLARE_FINAL_TYPE(FuUefiBgrt, fu_uefi_bgrt, FU, UEFI_BGRT, GObject) FuUefiBgrt * fu_uefi_bgrt_new(void); gboolean fu_uefi_bgrt_setup(FuUefiBgrt *self, GError **error); gboolean fu_uefi_bgrt_get_supported(FuUefiBgrt *self); guint32 fu_uefi_bgrt_get_xoffset(FuUefiBgrt *self); guint32 fu_uefi_bgrt_get_yoffset(FuUefiBgrt *self); guint32 fu_uefi_bgrt_get_width(FuUefiBgrt *self); guint32 fu_uefi_bgrt_get_height(FuUefiBgrt *self); fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-bootmgr.c000066400000000000000000000322711501337203100221350ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-uefi-bootmgr.h" #include "fu-uefi-capsule-device.h" #include "fu-uefi-common.h" static gboolean fu_uefi_bootmgr_add_to_boot_order(FuEfivars *efivars, guint16 boot_entry, GError **error) { g_autoptr(GArray) order = NULL; /* get the current boot order */ order = fu_efivars_get_boot_order(efivars, error); if (order == NULL) return FALSE; /* already set */ for (guint i = 0; i < order->len; i++) { guint16 val = g_array_index(order, guint16, i); if (val == boot_entry) return TRUE; } /* add the new boot index to the end of the list */ g_array_append_val(order, boot_entry); if (!fu_efivars_set_boot_order(efivars, order, error)) { g_prefix_error(error, "could not set BootOrder(%u): ", boot_entry); return FALSE; } /* success */ return TRUE; } static guint16 fu_uefi_bootmgr_parse_name(const gchar *name) { gint rc; gint scanned = 0; guint16 entry = 0; /* BootXXXX */ rc = sscanf(name, "Boot%hX%n", &entry, &scanned); if (rc != 1 || scanned != 8) return G_MAXUINT16; return entry; } gboolean fu_uefi_bootmgr_verify_fwupd(FuEfivars *efivars, GError **error) { g_autoptr(GPtrArray) names = NULL; names = fu_efivars_get_names(efivars, FU_EFIVARS_GUID_EFI_GLOBAL, error); if (names == NULL) return FALSE; for (guint i = 0; i < names->len; i++) { const gchar *desc; const gchar *name = g_ptr_array_index(names, i); guint16 entry; g_autoptr(FuEfiLoadOption) loadopt = NULL; g_autoptr(GError) error_local = NULL; /* not BootXXXX */ entry = fu_uefi_bootmgr_parse_name(name); if (entry == G_MAXUINT16) continue; /* parse key */ loadopt = fu_efivars_get_boot_entry(efivars, entry, &error_local); if (loadopt == NULL) { g_debug("%s -> load option was invalid: %s", name, error_local->message); continue; } desc = fu_firmware_get_id(FU_FIRMWARE(loadopt)); if (g_strcmp0(desc, "Linux Firmware Updater") == 0 || g_strcmp0(desc, "Linux-Firmware-Updater") == 0) { g_debug("found %s at Boot%04X", desc, entry); return TRUE; } } /* did not find */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no 'Linux Firmware Updater' entry found"); return FALSE; } static gboolean fu_uefi_bootmgr_setup_bootnext_with_loadopt(FuEfivars *efivars, FuEfiLoadOption *loadopt, FuUefiBootmgrFlags flags, GError **error) { const gchar *name = NULL; guint16 boot_next = G_MAXUINT16; g_autofree guint8 *set_entries = g_malloc0(G_MAXUINT16); g_autoptr(GBytes) loadopt_blob = NULL; g_autoptr(GBytes) loadopt_blob_old = NULL; g_autoptr(GPtrArray) names = NULL; /* write */ loadopt_blob = fu_firmware_write(FU_FIRMWARE(loadopt), error); if (loadopt_blob == NULL) return FALSE; /* find existing BootXXXX entry for fwupd */ names = fu_efivars_get_names(efivars, FU_EFIVARS_GUID_EFI_GLOBAL, error); if (names == NULL) return FALSE; for (guint i = 0; i < names->len; i++) { const gchar *desc; guint16 entry = 0; g_autoptr(GBytes) loadopt_blob_tmp = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(FuEfiLoadOption) loadopt_tmp = fu_efi_load_option_new(); /* not BootXXXX */ name = g_ptr_array_index(names, i); entry = fu_uefi_bootmgr_parse_name(name); if (entry == G_MAXUINT16) continue; /* mark this as used */ set_entries[entry] = 1; loadopt_blob_tmp = fu_efivars_get_boot_data(efivars, entry, &error_local); if (loadopt_blob_tmp == NULL) { g_debug("failed to get data for name %s: %s", name, error_local->message); continue; } if (!fu_firmware_parse_bytes(FU_FIRMWARE(loadopt_tmp), loadopt_blob_tmp, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error_local)) { g_debug("%s -> load option was invalid: %s", name, error_local->message); continue; } desc = fu_firmware_get_id(FU_FIRMWARE(loadopt_tmp)); if (g_strcmp0(desc, "Linux Firmware Updater") != 0 && g_strcmp0(desc, "Linux-Firmware-Updater") != 0) { g_debug("%s -> '%s' : does not match", name, desc); continue; } loadopt_blob_old = g_steal_pointer(&loadopt_blob_tmp); boot_next = entry; break; } /* already exists */ if (loadopt_blob_old != NULL) { /* is different than before */ if (!fu_bytes_compare(loadopt_blob, loadopt_blob_old, NULL)) { g_debug("%s: updating existing boot entry", name); if (!fu_efivars_set_boot_data(efivars, boot_next, loadopt_blob, error)) { g_prefix_error(error, "could not set boot variable active: "); return FALSE; } } else { g_debug("%s: re-using existing boot entry", name); } /* create a new one */ } else { g_autofree gchar *boot_next_name = NULL; for (guint16 value = 0; value < G_MAXUINT16; value++) { if (set_entries[value]) continue; boot_next = value; break; } if (boot_next == G_MAXUINT16) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no free boot variables (tried %x)", boot_next); return FALSE; } boot_next_name = g_strdup_printf("Boot%04X", (guint)boot_next); g_debug("%s -> creating new entry", boot_next_name); if (!fu_efivars_set_data_bytes(efivars, FU_EFIVARS_GUID_EFI_GLOBAL, boot_next_name, loadopt_blob, FU_EFIVARS_ATTR_NON_VOLATILE | FU_EFIVARS_ATTR_BOOTSERVICE_ACCESS | FU_EFIVARS_ATTR_RUNTIME_ACCESS, error)) { g_prefix_error(error, "could not set boot variable %s: ", boot_next_name); return FALSE; } } /* TODO: conditionalize this on the UEFI version? */ if (flags & FU_UEFI_BOOTMGR_FLAG_MODIFY_BOOTORDER) { if (!fu_uefi_bootmgr_add_to_boot_order(efivars, boot_next, error)) return FALSE; } /* set the boot next */ if (!fu_efivars_set_boot_next(efivars, boot_next, error)) { g_prefix_error(error, "could not set BootNext(%u): ", boot_next); return FALSE; } return TRUE; } static gboolean fu_uefi_bootmgr_shim_is_safe(FuEfivars *efivars, const gchar *source_shim, GError **error) { g_autoptr(GBytes) current_sbatlevel_bytes = NULL; g_autoptr(FuFirmware) shim = fu_pefile_firmware_new(); g_autoptr(FuFirmware) sbatlevel_section = NULL; g_autoptr(FuFirmware) previous_sbatlevel = NULL; g_autoptr(FuFirmware) current_sbatlevel = fu_csv_firmware_new(); g_autoptr(GError) error_local = NULL; g_autoptr(GFile) file = NULL; g_autoptr(GPtrArray) shim_entries = NULL; file = g_file_new_for_path(source_shim); if (!fu_firmware_parse_file(shim, file, FU_FIRMWARE_PARSE_FLAG_NONE, error)) { g_prefix_error(error, "failed to load %s: ", source_shim); return FALSE; } sbatlevel_section = fu_firmware_get_image_by_id(shim, ".sbatlevel", &error_local); if (sbatlevel_section == NULL) { g_debug("no sbatlevel section was found"); /* if there is no .sbatlevel section, then it will not update, it should be safe */ if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) return TRUE; g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } /* not safe if variable is not set but new shim would set it */ current_sbatlevel_bytes = fu_efivars_get_data_bytes(efivars, FU_EFIVARS_GUID_SHIM, "SbatLevelRT", NULL, error); if (current_sbatlevel_bytes == NULL) return FALSE; fu_csv_firmware_add_column_id(FU_CSV_FIRMWARE(current_sbatlevel), "$id"); fu_csv_firmware_add_column_id(FU_CSV_FIRMWARE(current_sbatlevel), "component_generation"); fu_csv_firmware_add_column_id(FU_CSV_FIRMWARE(current_sbatlevel), "date_stamp"); if (!fu_firmware_parse_bytes(current_sbatlevel, current_sbatlevel_bytes, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, error)) { g_prefix_error(error, "failed to load SbatLevelRT: "); return FALSE; } /* * For every new shim entry, we need a matching entry in the * current sbatlevel. That is the entry of the shim is not * newer than current sbatlevel. * * The opposite way might work (for example shim's latest * sbatlevel matches) or not (shim is too old), but it will * not brick the current OS. */ previous_sbatlevel = fu_firmware_get_image_by_id(sbatlevel_section, "previous", error); if (previous_sbatlevel == NULL) return FALSE; shim_entries = fu_firmware_get_images(previous_sbatlevel); for (guint idx = 0; idx < shim_entries->len; idx++) { FuCsvEntry *current_entry = NULL; FuCsvEntry *shim_entry = g_ptr_array_index(shim_entries, idx); const gchar *entry_id = fu_firmware_get_id(FU_FIRMWARE(shim_entry)); guint64 current_generation = 0; guint64 shim_generation = 0; current_entry = FU_CSV_ENTRY(fu_firmware_get_image_by_id(FU_FIRMWARE(current_sbatlevel), entry_id, &error_local)); if (current_entry == NULL) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "shim sbatlevel for %s has a bricking update for entry %s " "(missing entry in current UEFI variable)", source_shim, entry_id); } else { g_prefix_error(&error_local, "while looking for entry in current sbatlevel: "); g_propagate_error(error, g_steal_pointer(&error_local)); } return FALSE; } if (!fu_csv_entry_get_value_by_column_id_uint64(shim_entry, "component_generation", &shim_generation, error)) { g_prefix_error(error, "sbatlevel entry %s for shim %s: ", entry_id, source_shim); return FALSE; } if (!fu_csv_entry_get_value_by_column_id_uint64(current_entry, "component_generation", ¤t_generation, error)) { g_prefix_error(error, "entry %s from current sbatlevel: ", entry_id); return FALSE; } if (current_generation < shim_generation) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "sbatlevel for shim %s has a bricking update for entry " "%s (newer generation)", source_shim, entry_id); return FALSE; } } /* success */ return TRUE; } gboolean fu_uefi_bootmgr_bootnext(FuEfivars *efivars, FuVolume *esp, const gchar *description, FuUefiBootmgrFlags flags, GError **error) { const gchar *filepath = NULL; gboolean use_fwup_path = TRUE; gboolean secureboot_enabled = FALSE; g_autofree gchar *shim_app = NULL; g_autofree gchar *shim_cpy = NULL; g_autofree gchar *source_app = NULL; g_autofree gchar *source_shim = NULL; g_autofree gchar *target_app = NULL; g_autofree gchar *esp_path = fu_volume_get_mount_point(esp); g_autoptr(FuEfiDevicePathList) dp_buf = NULL; g_autoptr(FuEfiLoadOption) loadopt = fu_efi_load_option_new(); /* if secure boot was turned on this might need to be installed separately */ source_app = fu_uefi_get_built_app_path(efivars, "fwupd", error); if (source_app == NULL) return FALSE; /* test if we should use shim */ if (!fu_efivars_get_secure_boot(efivars, &secureboot_enabled, error)) return FALSE; if (secureboot_enabled) { shim_app = fu_uefi_get_esp_app_path(esp_path, "shim", error); if (shim_app == NULL) return FALSE; /* copy in an updated shim if we have one */ source_shim = fu_uefi_get_built_app_path(efivars, "shim", NULL); if (source_shim != NULL) { if (!fu_uefi_esp_target_verify(source_shim, esp, shim_app)) { if (!fu_uefi_bootmgr_shim_is_safe(efivars, source_shim, error)) return FALSE; if (!fu_uefi_esp_target_copy(source_shim, esp, shim_app, error)) return FALSE; } } if (fu_uefi_esp_target_exists(esp, shim_app)) { /* use a custom copy of shim for firmware updates */ if (flags & FU_UEFI_BOOTMGR_FLAG_USE_SHIM_UNIQUE) { shim_cpy = fu_uefi_get_esp_app_path(esp_path, "shimfwupd", error); if (shim_cpy == NULL) return FALSE; if (!fu_uefi_esp_target_verify(shim_app, esp, shim_cpy)) { if (!fu_uefi_esp_target_copy(shim_app, esp, shim_cpy, error)) return FALSE; } filepath = shim_cpy; } else { filepath = shim_app; } use_fwup_path = FALSE; } else if ((flags & FU_UEFI_BOOTMGR_FLAG_USE_SHIM_FOR_SB) > 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_BROKEN_SYSTEM, "Secure boot is enabled, but shim isn't installed to " "%s", shim_app); return FALSE; } } /* test if correct asset in place */ target_app = fu_uefi_get_esp_app_path(esp_path, "fwupd", error); if (target_app == NULL) return FALSE; if (!fu_uefi_esp_target_verify(source_app, esp, target_app)) { if (!fu_uefi_esp_target_copy(source_app, esp, target_app, error)) return FALSE; } /* no shim, so use this directly */ if (use_fwup_path) filepath = target_app; /* add the fwupdx64.efi ESP path as the shim loadopt data */ if (!use_fwup_path) { g_autofree gchar *fwup_fs_basename = g_path_get_basename(target_app); fu_efi_load_option_set_metadata(loadopt, FU_EFI_LOAD_OPTION_METADATA_PATH, fwup_fs_basename); } /* add DEVICE_PATH */ dp_buf = fu_uefi_capsule_device_build_dp_buf(esp, filepath, error); if (dp_buf == NULL) return FALSE; fu_firmware_add_image(FU_FIRMWARE(loadopt), FU_FIRMWARE(dp_buf)); fu_firmware_set_id(FU_FIRMWARE(loadopt), description); /* save as BootNext */ return fu_uefi_bootmgr_setup_bootnext_with_loadopt(efivars, loadopt, flags, error); } fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-bootmgr.h000066400000000000000000000012411501337203100221330ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2015 Peter Jones * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include typedef enum { FU_UEFI_BOOTMGR_FLAG_NONE = 0, FU_UEFI_BOOTMGR_FLAG_USE_SHIM_FOR_SB = 1 << 0, FU_UEFI_BOOTMGR_FLAG_USE_SHIM_UNIQUE = 1 << 1, FU_UEFI_BOOTMGR_FLAG_MODIFY_BOOTORDER = 1 << 2, FU_UEFI_BOOTMGR_FLAG_LAST } FuUefiBootmgrFlags; gboolean fu_uefi_bootmgr_verify_fwupd(FuEfivars *efivars, GError **error); gboolean fu_uefi_bootmgr_bootnext(FuEfivars *efivars, FuVolume *esp, const gchar *description, FuUefiBootmgrFlags flags, GError **error); fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-capsule-backend-freebsd.c000066400000000000000000000121631501337203100251130ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * Copyright 2021 3mdeb Embedded Systems Consulting * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include #include #ifdef HAVE_FREEBSD_ESRT #include #include #endif #include #include "fu-uefi-capsule-backend-freebsd.h" #include "fu-uefi-capsule-device.h" #include "fu-uefi-common.h" struct _FuUefiCapsuleBackendFreebsd { FuUefiCapsuleBackend parent_instance; }; G_DEFINE_TYPE(FuUefiCapsuleBackendFreebsd, fu_uefi_capsule_backend_freebsd, FU_TYPE_UEFI_CAPSULE_BACKEND) #ifdef HAVE_FREEBSD_ESRT static FuUefiCapsuleDevice * fu_uefi_capsule_backend_device_new(FuUefiCapsuleBackend *self, const gchar *physical_id, struct efi_esrt_entry_v1 *entry, guint64 idx, GError **error) { g_autoptr(FuUefiCapsuleDevice) dev = NULL; g_autofree gchar *backend_id = NULL; g_autofree gchar *fw_class = NULL; uint32_t status; uuid_to_string(&entry->fw_class, &fw_class, &status); if (status != uuid_s_ok) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "uuid_to_string error"); return NULL; } /* create object */ dev = g_object_new(fu_uefi_capsule_backend_get_device_gtype(self), "fw-class", fw_class, "capsule-flags", entry->capsule_flags, "kind", entry->fw_type, "fw-version", entry->fw_version, "last-attempt-status", entry->last_attempt_status, "last-attempt-version", entry->last_attempt_version, "fw-version-lowest", entry->lowest_supported_fw_version, "fmp-hardware-instance", (guint64)0x0, "version-format", FWUPD_VERSION_FORMAT_NUMBER, NULL); /* set ID */ backend_id = g_strdup_printf("ESRT/%u", (guint)idx); fu_device_set_backend_id(FU_DEVICE(dev), backend_id); fu_device_set_physical_id(FU_DEVICE(dev), physical_id); fu_device_set_logical_id(FU_DEVICE(dev), fw_class); return g_steal_pointer(&dev); } #endif static gboolean fu_uefi_capsule_backend_freebsd_setup(FuBackend *backend, FuBackendSetupFlags flags, GError **error) { g_autofree gchar *efi_ver = fu_kenv_get_string("efi-version", error); if (efi_ver == NULL) { g_prefix_error(error, "System does not support UEFI mode, no efi-version kenv: "); return FALSE; } if (fu_version_compare(efi_ver, "2.0.0.0", FWUPD_VERSION_FORMAT_QUAD) < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "System does not support UEFI mode, got efi-version of %s", efi_ver); return FALSE; } return TRUE; } static gboolean fu_uefi_capsule_backend_freebsd_coldplug(FuBackend *backend, GError **error) { #ifdef HAVE_FREEBSD_ESRT FuUefiCapsuleBackend *self = FU_UEFI_CAPSULE_BACKEND(backend); const gchar *esrt_dev = "/dev/efi"; struct efi_get_table_ioc table = {.uuid = EFI_TABLE_ESRT}; gint efi_fd; struct efi_esrt_entry_v1 *entries; g_autofree struct efi_esrt_table *esrt = NULL; efi_fd = g_open(esrt_dev, O_RDONLY, 0); if (efi_fd < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot open %s", esrt_dev); return FALSE; } if (ioctl(efi_fd, EFIIOC_GET_TABLE, &table) == -1) { g_close(efi_fd, NULL); g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Cannot determine size of ESRT table"); return FALSE; } esrt = g_malloc(table.table_len); if (esrt == NULL) { g_close(efi_fd, NULL); g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Cannot allocate memory for ESRT table"); return FALSE; } table.buf = esrt; table.buf_len = table.table_len; if (ioctl(efi_fd, EFIIOC_GET_TABLE, &table) == -1) { g_close(efi_fd, NULL); g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Cannot fill ESRT table"); return FALSE; } entries = (struct efi_esrt_entry_v1 *)esrt->entries; for (guint i = 0; i < esrt->fw_resource_count; i++) { g_autoptr(FuUefiCapsuleDevice) dev = NULL; dev = fu_uefi_capsule_backend_device_new(self, esrt_dev, &entries[i], i, error); if (dev == NULL) return FALSE; fu_backend_device_added(backend, FU_DEVICE(dev)); } /* success */ return TRUE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "ESRT access API is missing from the kernel"); return FALSE; #endif } void fu_uefi_capsule_backend_freebsd_set_device_gtype(FuBackend *backend, GType device_gtype) { } static void fu_uefi_capsule_backend_freebsd_init(FuUefiCapsuleBackendFreebsd *self) { } static void fu_uefi_capsule_backend_freebsd_class_init(FuUefiCapsuleBackendFreebsdClass *klass) { FuBackendClass *backend_class = FU_BACKEND_CLASS(klass); backend_class->setup = fu_uefi_capsule_backend_freebsd_setup; backend_class->coldplug = fu_uefi_capsule_backend_freebsd_coldplug; } FuBackend * fu_uefi_capsule_backend_new(FuContext *ctx) { return g_object_new(FU_TYPE_UEFI_CAPSULE_BACKEND_FREEBSD, "name", "uefi", "context", ctx, NULL); } fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-capsule-backend-freebsd.h000066400000000000000000000006771501337203100251270ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-uefi-capsule-backend.h" #define FU_TYPE_UEFI_CAPSULE_BACKEND_FREEBSD (fu_uefi_capsule_backend_freebsd_get_type()) G_DECLARE_FINAL_TYPE(FuUefiCapsuleBackendFreebsd, fu_uefi_capsule_backend_freebsd, FU, UEFI_CAPSULE_BACKEND_FREEBSD, FuUefiCapsuleBackend) fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-capsule-backend-linux.c000066400000000000000000000172041501337203100246410ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-uefi-capsule-backend-linux.h" #include "fu-uefi-cod-device.h" #include "fu-uefi-common.h" #include "fu-uefi-nvram-device.h" struct _FuUefiCapsuleBackendLinux { FuUefiCapsuleBackend parent_instance; gboolean use_rt_set_variable; }; G_DEFINE_TYPE(FuUefiCapsuleBackendLinux, fu_uefi_capsule_backend_linux, FU_TYPE_UEFI_CAPSULE_BACKEND) /* yes, unsized uint_t */ static guint fu_uefi_capsule_backend_linux_read(const gchar *path, const gchar *filename) { return fu_uefi_read_file_as_uint64(path, filename); } static FuUefiCapsuleDevice * fu_uefi_capsule_backend_linux_device_new(FuUefiCapsuleBackendLinux *self, const gchar *physical_id, const gchar *path) { g_autoptr(FuUefiCapsuleDevice) dev = NULL; g_autofree gchar *fw_class = NULL; g_autofree gchar *fw_class_fn = NULL; g_return_val_if_fail(path != NULL, NULL); /* read values from sysfs */ fw_class_fn = g_build_filename(path, "fw_class", NULL); if (g_file_get_contents(fw_class_fn, &fw_class, NULL, NULL)) g_strdelimit(fw_class, "\n", '\0'); /* Create object, assuming a verfmt of NUMBER unless told otherwise by * a quirk entry or metadata. * * The hardware instance is not in the ESRT table and we should really * write the EFI stub to query with FMP -- but we still have not ever * seen a PCIe device with FMP support... */ dev = g_object_new(fu_uefi_capsule_backend_get_device_gtype(FU_UEFI_CAPSULE_BACKEND(self)), "fw-class", fw_class, "capsule-flags", fu_uefi_capsule_backend_linux_read(path, "capsule_flags"), "kind", fu_uefi_capsule_backend_linux_read(path, "fw_type"), "fw-version", fu_uefi_capsule_backend_linux_read(path, "fw_version"), "last-attempt-status", fu_uefi_capsule_backend_linux_read(path, "last_attempt_status"), "last-attempt-version", fu_uefi_capsule_backend_linux_read(path, "last_attempt_version"), "fw-version-lowest", fu_uefi_capsule_backend_linux_read(path, "lowest_supported_fw_version"), "fmp-hardware-instance", (guint64)0x0, "version-format", FWUPD_VERSION_FORMAT_NUMBER, NULL); /* u-boot for instance */ if (!self->use_rt_set_variable) fu_device_add_private_flag(FU_DEVICE(dev), FU_UEFI_CAPSULE_DEVICE_FLAG_NO_RT_SET_VARIABLE); /* set ID */ fu_device_set_backend_id(FU_DEVICE(dev), path); fu_device_set_physical_id(FU_DEVICE(dev), physical_id); fu_device_set_logical_id(FU_DEVICE(dev), fw_class); return g_steal_pointer(&dev); } static gboolean fu_uefi_capsule_backend_linux_check_efivarfs(FuUefiCapsuleBackendLinux *self, GError **error) { gboolean is_readonly; g_autofree gchar *sysfsfwdir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR_FW); g_autofree gchar *sysfsefivardir = g_build_filename(sysfsfwdir, "efi", "efivars", NULL); g_autoptr(GUnixMountEntry) mount = NULL; /* in the self tests */ if (g_getenv("FWUPD_UEFI_TEST") != NULL) return TRUE; #if GLIB_CHECK_VERSION(2, 83, 1) mount = g_unix_mount_entry_at(sysfsefivardir, NULL); #else mount = g_unix_mount_at(sysfsefivardir, NULL); #endif if (mount == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "%s was not mounted", sysfsefivardir); return FALSE; } #if GLIB_CHECK_VERSION(2, 83, 1) is_readonly = g_unix_mount_entry_is_readonly(mount); #else is_readonly = g_unix_mount_is_readonly(mount); #endif if (is_readonly) { GType gtype = fu_uefi_capsule_backend_get_device_gtype(FU_UEFI_CAPSULE_BACKEND(self)); if (gtype != FU_TYPE_UEFI_COD_DEVICE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "%s is read only and no CoD", sysfsefivardir); return FALSE; } /* this is fine! just do not use SetVariable... */ self->use_rt_set_variable = FALSE; } return TRUE; } static gboolean fu_uefi_capsule_backend_linux_coldplug(FuBackend *backend, FuProgress *progress, GError **error) { FuUefiCapsuleBackendLinux *self = FU_UEFI_CAPSULE_BACKEND_LINUX(backend); const gchar *fn; g_autofree gchar *esrt_entries = NULL; g_autofree gchar *esrt_path = NULL; g_autofree gchar *sysfsfwdir = NULL; g_autoptr(GDir) dir = NULL; /* make sure that efivarfs is suitable */ if (!fu_uefi_capsule_backend_linux_check_efivarfs(self, error)) return FALSE; /* get the directory of ESRT entries */ sysfsfwdir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR_FW); esrt_path = g_build_filename(sysfsfwdir, "efi", "esrt", NULL); esrt_entries = g_build_filename(esrt_path, "entries", NULL); dir = g_dir_open(esrt_entries, 0, error); if (dir == NULL) return FALSE; /* add each device */ while ((fn = g_dir_read_name(dir)) != NULL) { g_autofree gchar *path = g_build_filename(esrt_entries, fn, NULL); g_autoptr(FuUefiCapsuleDevice) dev = fu_uefi_capsule_backend_linux_device_new(self, esrt_path, path); fu_backend_device_added(backend, FU_DEVICE(dev)); } /* success */ return TRUE; } static gboolean fu_uefi_capsule_backend_linux_check_smbios_enabled(FuContext *ctx, GError **error) { GBytes *bios_blob; const guint8 *data; gsize sz; g_autoptr(GPtrArray) bios_tables = NULL; bios_tables = fu_context_get_smbios_data(ctx, 0, FU_SMBIOS_STRUCTURE_LENGTH_ANY, NULL); if (bios_tables == NULL) { const gchar *tmp = g_getenv("FWUPD_DELL_FAKE_SMBIOS"); if (tmp != NULL) return TRUE; g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "SMBIOS not supported"); return FALSE; } bios_blob = g_ptr_array_index(bios_tables, 0); data = g_bytes_get_data(bios_blob, &sz); if (sz < 0x14) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "offset bigger than size %" G_GSIZE_FORMAT, sz); return FALSE; } if (data[1] < 0x14) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "SMBIOS 2.3 not supported"); return FALSE; } if (!(data[0x13] & (1 << 3))) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "System does not support UEFI mode"); return FALSE; } return TRUE; } static gboolean fu_uefi_capsule_backend_linux_setup(FuBackend *backend, FuBackendSetupFlags flags, FuProgress *progress, GError **error) { g_autoptr(GError) error_local = NULL; /* using a pre-cooked SMBIOS */ if (g_getenv("FWUPD_SYSFSFWDIR") != NULL) return TRUE; /* check SMBIOS for 'UEFI Specification is supported' */ if (!fu_uefi_capsule_backend_linux_check_smbios_enabled(fu_backend_get_context(backend), &error_local)) { g_autofree gchar *fw = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR_FW); g_autofree gchar *fn = g_build_filename(fw, "efi", NULL); if (g_file_test(fn, G_FILE_TEST_EXISTS)) { g_warning("SMBIOS BIOS Characteristics Extension Byte 2 is invalid -- " "UEFI Specification is unsupported, but %s exists: %s", fn, error_local->message); return TRUE; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } return TRUE; } static void fu_uefi_capsule_backend_linux_init(FuUefiCapsuleBackendLinux *self) { self->use_rt_set_variable = TRUE; } static void fu_uefi_capsule_backend_linux_class_init(FuUefiCapsuleBackendLinuxClass *klass) { FuBackendClass *backend_class = FU_BACKEND_CLASS(klass); backend_class->coldplug = fu_uefi_capsule_backend_linux_coldplug; backend_class->setup = fu_uefi_capsule_backend_linux_setup; } FuBackend * fu_uefi_capsule_backend_new(FuContext *ctx) { return g_object_new(FU_TYPE_UEFI_CAPSULE_BACKEND_LINUX, "name", "uefi", "context", ctx, NULL); } fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-capsule-backend-linux.h000066400000000000000000000006651501337203100246510ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-uefi-capsule-backend.h" #define FU_TYPE_UEFI_CAPSULE_BACKEND_LINUX (fu_uefi_capsule_backend_linux_get_type()) G_DECLARE_FINAL_TYPE(FuUefiCapsuleBackendLinux, fu_uefi_capsule_backend_linux, FU, UEFI_CAPSULE_BACKEND_LINUX, FuUefiCapsuleBackend) fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-capsule-backend.c000066400000000000000000000045011501337203100235000ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uefi-capsule-backend.h" #include "fu-uefi-nvram-device.h" typedef struct { GType device_gtype; } FuUefiCapsuleBackendPrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuUefiCapsuleBackend, fu_uefi_capsule_backend, FU_TYPE_BACKEND) #define GET_PRIVATE(o) (fu_uefi_capsule_backend_get_instance_private(o)) void fu_uefi_capsule_backend_set_device_gtype(FuUefiCapsuleBackend *self, GType device_gtype) { FuUefiCapsuleBackendPrivate *priv = GET_PRIVATE(self); priv->device_gtype = device_gtype; } GType fu_uefi_capsule_backend_get_device_gtype(FuUefiCapsuleBackend *self) { FuUefiCapsuleBackendPrivate *priv = GET_PRIVATE(self); return priv->device_gtype; } static void fu_uefi_capsule_backend_to_string(FuBackend *backend, guint idt, GString *str) { FuUefiCapsuleBackend *self = FU_UEFI_CAPSULE_BACKEND(backend); FuUefiCapsuleBackendPrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append(str, idt, "DeviceGType", g_type_name(priv->device_gtype)); } /* create virtual object not backed by an ESRT entry */ FuUefiCapsuleDevice * fu_uefi_capsule_backend_device_new_from_dev(FuUefiCapsuleBackend *self, FuDevice *dev) { FuUefiCapsuleDevice *device; FuUefiCapsuleBackendPrivate *priv = GET_PRIVATE(self); const gchar *tmp; g_return_val_if_fail(fu_device_get_guid_default(dev) != NULL, NULL); tmp = fu_device_get_metadata(dev, FU_DEVICE_METADATA_UEFI_DEVICE_KIND); device = g_object_new(priv->device_gtype, "fw-class", fu_device_get_guid_default(dev), "kind", fu_uefi_capsule_device_kind_from_string(tmp), "capsule-flags", fu_device_get_metadata_integer(dev, FU_DEVICE_METADATA_UEFI_CAPSULE_FLAGS), "fw-version", fu_device_get_metadata_integer(dev, FU_DEVICE_METADATA_UEFI_FW_VERSION), NULL); fu_device_incorporate(FU_DEVICE(device), dev, FU_DEVICE_INCORPORATE_FLAG_ALL); return device; } static void fu_uefi_capsule_backend_init(FuUefiCapsuleBackend *self) { FuUefiCapsuleBackendPrivate *priv = GET_PRIVATE(self); priv->device_gtype = FU_TYPE_UEFI_NVRAM_DEVICE; } static void fu_uefi_capsule_backend_class_init(FuUefiCapsuleBackendClass *klass) { FuBackendClass *backend_class = FU_BACKEND_CLASS(klass); backend_class->to_string = fu_uefi_capsule_backend_to_string; } fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-capsule-backend.h000066400000000000000000000014361501337203100235110ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-uefi-capsule-device.h" #define FU_TYPE_UEFI_CAPSULE_BACKEND (fu_uefi_capsule_backend_get_type()) G_DECLARE_DERIVABLE_TYPE(FuUefiCapsuleBackend, fu_uefi_capsule_backend, FU, UEFI_CAPSULE_BACKEND, FuBackend) struct _FuUefiCapsuleBackendClass { FuBackendClass parent_class; }; FuBackend * fu_uefi_capsule_backend_new(FuContext *ctx); void fu_uefi_capsule_backend_set_device_gtype(FuUefiCapsuleBackend *self, GType device_gtype); GType fu_uefi_capsule_backend_get_device_gtype(FuUefiCapsuleBackend *self); FuUefiCapsuleDevice * fu_uefi_capsule_backend_device_new_from_dev(FuUefiCapsuleBackend *self, FuDevice *dev); fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-capsule-device.c000066400000000000000000000732241501337203100233600ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2015 Peter Jones * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-uefi-capsule-device.h" #include "fu-uefi-common.h" #include "fu-uefi-struct.h" typedef struct { FuVolume *esp; FuDeviceLocker *esp_locker; gchar *fw_class; FuUefiCapsuleDeviceKind kind; guint32 capsule_flags; guint32 fw_version; guint32 fw_version_lowest; FuUefiCapsuleDeviceStatus last_attempt_status; guint32 last_attempt_version; guint64 fmp_hardware_instance; gboolean missing_header; gboolean automounted_esp; gsize require_esp_free_space; } FuUefiCapsuleDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuUefiCapsuleDevice, fu_uefi_capsule_device, FU_TYPE_DEVICE) #define GET_PRIVATE(o) (fu_uefi_capsule_device_get_instance_private(o)) #define FU_EFI_FMP_CAPSULE_GUID "6dcbd5ed-e82d-4c44-bda1-7194199ad92a" /* the size of the fwupd*.efi binary plus any logs the firmware updater might generate */ #define FU_UEFI_CAPSULE_EXTRA_SIZE_REQUIRED (1024 * 1024) /* bytes */ enum { PROP_0, PROP_FW_CLASS, PROP_KIND, PROP_CAPSULE_FLAGS, PROP_FW_VERSION, PROP_FW_VERSION_LOWEST, PROP_LAST_ATTEMPT_STATUS, PROP_LAST_ATTEMPT_VERSION, PROP_FMP_HARDWARE_INSTANCE, PROP_LAST }; void fu_uefi_capsule_device_set_esp(FuUefiCapsuleDevice *self, FuVolume *esp) { FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_UEFI_CAPSULE_DEVICE(self)); g_return_if_fail(FU_IS_VOLUME(esp)); g_set_object(&priv->esp, esp); } static void fu_uefi_capsule_device_to_string(FuDevice *device, guint idt, GString *str) { FuUefiCapsuleDevice *self = FU_UEFI_CAPSULE_DEVICE(device); FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append(str, idt, "Kind", fu_uefi_capsule_device_kind_to_string(priv->kind)); fwupd_codec_string_append(str, idt, "FwClass", priv->fw_class); fwupd_codec_string_append_hex(str, idt, "CapsuleFlags", priv->capsule_flags); fwupd_codec_string_append_hex(str, idt, "FwVersion", priv->fw_version); fwupd_codec_string_append_hex(str, idt, "FwVersionLowest", priv->fw_version_lowest); fwupd_codec_string_append( str, idt, "LastAttemptStatus", fu_uefi_capsule_device_status_to_string(priv->last_attempt_status)); fwupd_codec_string_append_hex(str, idt, "LastAttemptVersion", priv->last_attempt_version); if (priv->esp != NULL) { g_autofree gchar *kind = fu_volume_get_partition_kind(priv->esp); g_autofree gchar *mount_point = fu_volume_get_mount_point(priv->esp); fwupd_codec_string_append(str, idt, "EspId", fu_volume_get_id(priv->esp)); if (mount_point != NULL) fwupd_codec_string_append(str, idt, "EspPath", mount_point); if (kind != NULL) { const gchar *guid = fu_volume_kind_convert_to_gpt(kind); fwupd_codec_string_append(str, idt, "EspKind", kind); if (g_strcmp0(kind, guid) != 0) fwupd_codec_string_append(str, idt, "EspGuid", guid); } } fwupd_codec_string_append_int(str, idt, "RequireESPFreeSpace", priv->require_esp_free_space); } static void fu_uefi_capsule_device_report_metadata_pre(FuDevice *device, GHashTable *metadata) { FuUefiCapsuleDevice *self = FU_UEFI_CAPSULE_DEVICE(device); FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); /* record if we had an invalid header during update */ g_hash_table_insert(metadata, g_strdup("MissingCapsuleHeader"), g_strdup(priv->missing_header ? "True" : "False")); /* where and how the ESP was mounted during installation */ if (priv->esp != NULL) { g_autofree gchar *kind = fu_volume_get_partition_kind(priv->esp); g_autofree gchar *mount_point = fu_volume_get_mount_point(priv->esp); if (mount_point != NULL) { g_hash_table_insert(metadata, g_strdup("EspPath"), g_steal_pointer(&mount_point)); } if (kind != NULL) g_hash_table_insert(metadata, g_strdup("EspKind"), g_steal_pointer(&kind)); } } static void fu_uefi_capsule_device_report_metadata_post(FuDevice *device, GHashTable *metadata) { FuUefiCapsuleDevice *self = FU_UEFI_CAPSULE_DEVICE(device); FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); /* the actual last_attempt values */ g_hash_table_insert(metadata, g_strdup("LastAttemptStatus"), g_strdup_printf("0x%x", priv->last_attempt_status)); g_hash_table_insert(metadata, g_strdup("LastAttemptVersion"), g_strdup_printf("0x%x", priv->last_attempt_version)); } FuUefiCapsuleDeviceKind fu_uefi_capsule_device_get_kind(FuUefiCapsuleDevice *self) { FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UEFI_CAPSULE_DEVICE(self), 0); return priv->kind; } guint32 fu_uefi_capsule_device_get_version(FuUefiCapsuleDevice *self) { FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UEFI_CAPSULE_DEVICE(self), 0x0); return priv->fw_version; } guint32 fu_uefi_capsule_device_get_version_lowest(FuUefiCapsuleDevice *self) { FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UEFI_CAPSULE_DEVICE(self), 0x0); return priv->fw_version_lowest; } guint32 fu_uefi_capsule_device_get_version_error(FuUefiCapsuleDevice *self) { FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UEFI_CAPSULE_DEVICE(self), 0x0); return priv->last_attempt_version; } guint64 fu_uefi_capsule_device_get_hardware_instance(FuUefiCapsuleDevice *self) { FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UEFI_CAPSULE_DEVICE(self), 0x0); return priv->fmp_hardware_instance; } FuUefiCapsuleDeviceStatus fu_uefi_capsule_device_get_status(FuUefiCapsuleDevice *self) { FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UEFI_CAPSULE_DEVICE(self), 0); return priv->last_attempt_status; } void fu_uefi_capsule_device_set_status(FuUefiCapsuleDevice *self, FuUefiCapsuleDeviceStatus status) { FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); const gchar *tmp; g_autofree gchar *err_msg = NULL; g_autofree gchar *version_str = NULL; g_return_if_fail(FU_IS_UEFI_CAPSULE_DEVICE(self)); /* cache for later */ priv->last_attempt_status = status; /* all good */ if (status == FU_UEFI_CAPSULE_DEVICE_STATUS_SUCCESS) { fu_device_set_update_state(FU_DEVICE(self), FWUPD_UPDATE_STATE_SUCCESS); return; } /* something went wrong */ if (status == FU_UEFI_CAPSULE_DEVICE_STATUS_ERROR_PWR_EVT_AC || status == FU_UEFI_CAPSULE_DEVICE_STATUS_ERROR_PWR_EVT_BATT) { fu_device_set_update_state(FU_DEVICE(self), FWUPD_UPDATE_STATE_FAILED_TRANSIENT); } else { fu_device_set_update_state(FU_DEVICE(self), FWUPD_UPDATE_STATE_FAILED); } version_str = g_strdup_printf("%u", priv->last_attempt_version); tmp = fu_uefi_capsule_device_status_to_string(status); if (tmp == NULL) { err_msg = g_strdup_printf("failed to update to %s", version_str); } else { err_msg = g_strdup_printf("failed to update to %s: %s", version_str, tmp); } fu_device_set_update_error(FU_DEVICE(self), err_msg); } void fu_uefi_capsule_device_set_require_esp_free_space(FuUefiCapsuleDevice *self, gsize require_esp_free_space) { FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_UEFI_CAPSULE_DEVICE(self)); priv->require_esp_free_space = require_esp_free_space; } guint32 fu_uefi_capsule_device_get_capsule_flags(FuUefiCapsuleDevice *self) { FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UEFI_CAPSULE_DEVICE(self), 0x0); return priv->capsule_flags; } const gchar * fu_uefi_capsule_device_get_guid(FuUefiCapsuleDevice *self) { FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_UEFI_CAPSULE_DEVICE(self), NULL); return priv->fw_class; } gchar * fu_uefi_capsule_device_build_varname(FuUefiCapsuleDevice *self) { FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); return g_strdup_printf("fwupd-%s-%" G_GUINT64_FORMAT, priv->fw_class, priv->fmp_hardware_instance); } FuUefiUpdateInfo * fu_uefi_capsule_device_load_update_info(FuUefiCapsuleDevice *self, GError **error) { FuContext *ctx = fu_device_get_context(FU_DEVICE(self)); FuEfivars *efivars = fu_context_get_efivars(ctx); g_autofree gchar *varname = fu_uefi_capsule_device_build_varname(self); g_autoptr(FuUefiUpdateInfo) info = fu_uefi_update_info_new(); g_autoptr(GBytes) fw = NULL; g_return_val_if_fail(FU_IS_UEFI_CAPSULE_DEVICE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* get the existing status */ fw = fu_efivars_get_data_bytes(efivars, FU_EFIVARS_GUID_FWUPDATE, varname, NULL, error); if (fw == NULL) return NULL; if (!fu_firmware_parse_bytes(FU_FIRMWARE(info), fw, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return NULL; return g_steal_pointer(&info); } gboolean fu_uefi_capsule_device_clear_status(FuUefiCapsuleDevice *self, GError **error) { FuContext *ctx = fu_device_get_context(FU_DEVICE(self)); FuEfivars *efivars = fu_context_get_efivars(ctx); gsize datasz = 0; g_autofree gchar *varname = fu_uefi_capsule_device_build_varname(self); g_autofree guint8 *data = NULL; g_autoptr(GByteArray) st_inf = NULL; g_return_val_if_fail(FU_IS_UEFI_CAPSULE_DEVICE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* get the existing status */ if (!fu_efivars_get_data(efivars, FU_EFIVARS_GUID_FWUPDATE, varname, &data, &datasz, NULL, error)) return FALSE; st_inf = fu_struct_efi_update_info_parse(data, datasz, 0x0, error); if (st_inf == NULL) { g_prefix_error(error, "EFI variable is corrupt: "); return FALSE; } /* just copy the new EfiUpdateInfo and save it back */ fu_struct_efi_update_info_set_status(st_inf, FU_UEFI_UPDATE_INFO_STATUS_UNKNOWN); memcpy(data, st_inf->data, st_inf->len); /* nocheck:blocked */ if (!fu_efivars_set_data(efivars, FU_EFIVARS_GUID_FWUPDATE, varname, data, datasz, FU_EFIVARS_ATTR_NON_VOLATILE | FU_EFIVARS_ATTR_BOOTSERVICE_ACCESS | FU_EFIVARS_ATTR_RUNTIME_ACCESS, error)) { g_prefix_error(error, "could not set EfiUpdateInfo: "); return FALSE; } /* success */ return TRUE; } FuEfiDevicePathList * fu_uefi_capsule_device_build_dp_buf(FuVolume *esp, const gchar *capsule_path, GError **error) { g_autoptr(FuEfiDevicePathList) dp_buf = fu_efi_device_path_list_new(); g_autoptr(FuEfiFilePathDevicePath) dp_file = fu_efi_file_path_device_path_new(); g_autoptr(FuEfiHardDriveDevicePath) dp_hd = NULL; g_autofree gchar *name_with_root = NULL; dp_hd = fu_efi_hard_drive_device_path_new_from_volume(esp, error); if (dp_hd == NULL) return NULL; name_with_root = g_strdup_printf("/%s", capsule_path); if (!fu_efi_file_path_device_path_set_name(dp_file, name_with_root, error)) return NULL; fu_firmware_add_image(FU_FIRMWARE(dp_buf), FU_FIRMWARE(dp_hd)); fu_firmware_add_image(FU_FIRMWARE(dp_buf), FU_FIRMWARE(dp_file)); return g_steal_pointer(&dp_buf); } GBytes * fu_uefi_capsule_device_fixup_firmware(FuUefiCapsuleDevice *self, GBytes *fw, GError **error) { FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); fwupd_guid_t esrt_guid = {0x0}; guint hdrsize = getpagesize(); gsize bufsz; const guint8 *buf = g_bytes_get_data(fw, &bufsz); g_autofree gchar *guid_new = NULL; g_autoptr(GByteArray) st_cap = fu_struct_efi_capsule_header_new(); g_return_val_if_fail(FU_IS_UEFI_CAPSULE_DEVICE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); priv->missing_header = FALSE; /* GUID is the first 16 bytes */ if (bufsz < sizeof(fwupd_guid_t)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "Invalid payload"); return NULL; } guid_new = fwupd_guid_to_string((fwupd_guid_t *)buf, FWUPD_GUID_FLAG_MIXED_ENDIAN); /* ESRT header matches payload */ if (g_strcmp0(fu_uefi_capsule_device_get_guid(self), guid_new) == 0) { g_debug("ESRT matches payload GUID"); return g_bytes_ref(fw); } if (g_strcmp0(guid_new, FU_EFI_FMP_CAPSULE_GUID) == 0 || fu_device_has_private_flag(FU_DEVICE(self), FU_UEFI_CAPSULE_DEVICE_FLAG_NO_CAPSULE_HEADER_FIXUP)) { return g_bytes_ref(fw); } /* create a fake header with plausible contents */ g_info("missing or invalid embedded capsule header"); priv->missing_header = TRUE; fu_struct_efi_capsule_header_set_flags(st_cap, priv->capsule_flags); fu_struct_efi_capsule_header_set_header_size(st_cap, hdrsize); fu_struct_efi_capsule_header_set_image_size(st_cap, bufsz + hdrsize); if (fu_uefi_capsule_device_get_guid(self) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "no GUID set"); return NULL; } if (!fwupd_guid_from_string(fu_uefi_capsule_device_get_guid(self), &esrt_guid, FWUPD_GUID_FLAG_MIXED_ENDIAN, error)) { g_prefix_error(error, "Invalid ESRT GUID: "); return NULL; } fu_struct_efi_capsule_header_set_guid(st_cap, &esrt_guid); /* pad to the headersize then add the payload */ fu_byte_array_set_size(st_cap, hdrsize, 0x00); g_byte_array_append(st_cap, buf, bufsz); return g_bytes_new(st_cap->data, st_cap->len); } gboolean fu_uefi_capsule_device_write_update_info(FuUefiCapsuleDevice *self, const gchar *capsule_path, const gchar *varname, const gchar *guid_str, GError **error) { FuContext *ctx = fu_device_get_context(FU_DEVICE(self)); FuEfivars *efivars = fu_context_get_efivars(ctx); FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); fwupd_guid_t guid = {0x0}; g_autoptr(FuEfiDevicePathList) dp_buf = NULL; g_autoptr(GBytes) dp_blob = NULL; g_autoptr(GByteArray) st_inf = fu_struct_efi_update_info_new(); /* convert to EFI device path */ dp_buf = fu_uefi_capsule_device_build_dp_buf(priv->esp, capsule_path, error); if (dp_buf == NULL) return FALSE; dp_blob = fu_firmware_write(FU_FIRMWARE(dp_buf), error); if (dp_blob == NULL) return FALSE; /* save this header and body to the hardware */ if (!fwupd_guid_from_string(guid_str, &guid, FWUPD_GUID_FLAG_MIXED_ENDIAN, error)) return FALSE; fu_struct_efi_update_info_set_flags(st_inf, priv->capsule_flags); fu_struct_efi_update_info_set_hw_inst(st_inf, priv->fmp_hardware_instance); fu_struct_efi_update_info_set_status(st_inf, FU_UEFI_UPDATE_INFO_STATUS_ATTEMPT_UPDATE); fu_struct_efi_update_info_set_guid(st_inf, &guid); fu_byte_array_append_bytes(st_inf, dp_blob); if (!fu_efivars_set_data(efivars, FU_EFIVARS_GUID_FWUPDATE, varname, st_inf->data, st_inf->len, FU_EFIVARS_ATTR_NON_VOLATILE | FU_EFIVARS_ATTR_BOOTSERVICE_ACCESS | FU_EFIVARS_ATTR_RUNTIME_ACCESS, error)) { g_prefix_error(error, "could not set DP_BUF with %s: ", capsule_path); return FALSE; } /* success */ return TRUE; } gboolean fu_uefi_capsule_device_check_asset(FuUefiCapsuleDevice *self, GError **error) { FuContext *ctx = fu_device_get_context(FU_DEVICE(self)); FuEfivars *efivars = fu_context_get_efivars(ctx); gboolean secureboot_enabled = FALSE; g_autofree gchar *source_app = NULL; if (!fu_efivars_get_secure_boot(efivars, &secureboot_enabled, error)) return FALSE; /* if fwupd-efi isn't in use, skip checks for the signed binary */ if (!fu_device_has_private_flag(FU_DEVICE(self), FU_UEFI_CAPSULE_DEVICE_FLAG_USE_FWUPD_EFI)) return TRUE; source_app = fu_uefi_get_built_app_path(efivars, "fwupd", error); if (source_app == NULL && secureboot_enabled) { g_prefix_error(error, "missing signed bootloader for secure boot: "); return FALSE; } return TRUE; } static gboolean fu_uefi_capsule_device_prepare(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuUefiCapsuleDevice *self = FU_UEFI_CAPSULE_DEVICE(device); FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); /* mount if required */ priv->esp_locker = fu_volume_locker(priv->esp, error); if (priv->esp_locker == NULL) return FALSE; return TRUE; } static gboolean fu_uefi_capsule_device_cleanup(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuUefiCapsuleDevice *self = FU_UEFI_CAPSULE_DEVICE(device); FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); /* unmount ESP if we opened it */ if (!fu_device_locker_close(priv->esp_locker, error)) return FALSE; g_clear_object(&priv->esp_locker); return TRUE; } static gboolean fu_uefi_capsule_device_probe(FuDevice *device, GError **error) { FuUefiCapsuleDevice *self = FU_UEFI_CAPSULE_DEVICE(device); FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); /* broken sysfs? */ if (priv->fw_class == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to read fw_class"); return FALSE; } /* this is invalid */ if (!fwupd_guid_is_valid(priv->fw_class)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "ESRT GUID '%s' was not valid", priv->fw_class); return FALSE; } /* add GUID first, as quirks may set the version format */ fu_device_add_instance_id(device, priv->fw_class); /* set versions */ fu_device_set_version_raw(device, priv->fw_version); if (priv->fw_version_lowest != 0) { g_autofree gchar *version_lowest = fu_version_from_uint32(priv->fw_version_lowest, fu_device_get_version_format(self)); fu_device_set_version_lowest_raw(device, priv->fw_version_lowest); fu_device_set_version_lowest(device, version_lowest); } /* set flags */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_MD_SET_VERFMT); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_MD_SET_ICON); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_MD_SET_VENDOR); /* add icons */ if (priv->kind == FU_UEFI_CAPSULE_DEVICE_KIND_SYSTEM_FIRMWARE) { fu_device_add_icon(device, FU_DEVICE_ICON_COMPUTER); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE); } /* whether to create a missing header */ if (priv->kind == FU_UEFI_CAPSULE_DEVICE_KIND_FMP || priv->kind == FU_UEFI_CAPSULE_DEVICE_KIND_DELL_TPM_FIRMWARE) fu_device_add_private_flag(device, FU_UEFI_CAPSULE_DEVICE_FLAG_NO_CAPSULE_HEADER_FIXUP); /* success */ return TRUE; } static void fu_uefi_capsule_device_capture_efi_debugging(FuDevice *device) { FuContext *ctx = fu_device_get_context(device); FuEfivars *efivars = fu_context_get_efivars(ctx); g_autofree gchar *str = NULL; g_autoptr(GBytes) buf = NULL; g_autoptr(GError) error_local = NULL; /* get the EFI variable contents */ buf = fu_efivars_get_data_bytes(efivars, FU_EFIVARS_GUID_FWUPDATE, "FWUPDATE_DEBUG_LOG", NULL, &error_local); if (buf == NULL) { g_warning("failed to capture EFI debugging: %s", error_local->message); return; } /* convert from UCS-2 to UTF-8 */ str = fu_utf16_to_utf8_bytes(buf, G_LITTLE_ENDIAN, &error_local); if (str == NULL) { g_warning("failed to capture EFI debugging: %s", error_local->message); return; } /* success, dump into journal */ g_info("EFI debugging: %s", str); } gboolean fu_uefi_capsule_device_perhaps_enable_debugging(FuUefiCapsuleDevice *self, GError **error) { FuContext *ctx = fu_device_get_context(FU_DEVICE(self)); FuEfivars *efivars = fu_context_get_efivars(ctx); if (fu_device_has_private_flag(FU_DEVICE(self), FU_UEFI_CAPSULE_DEVICE_FLAG_ENABLE_DEBUGGING)) { const guint8 data = 1; if (!fu_efivars_set_data(efivars, FU_EFIVARS_GUID_FWUPDATE, "FWUPDATE_VERBOSE", &data, sizeof(data), FU_EFIVARS_ATTR_NON_VOLATILE | FU_EFIVARS_ATTR_BOOTSERVICE_ACCESS | FU_EFIVARS_ATTR_RUNTIME_ACCESS, error)) { g_prefix_error(error, "failed to enable debugging: "); return FALSE; } return TRUE; } /* unset this */ if (fu_efivars_exists(efivars, FU_EFIVARS_GUID_FWUPDATE, "FWUPDATE_VERBOSE")) { if (!fu_efivars_delete(efivars, FU_EFIVARS_GUID_FWUPDATE, "FWUPDATE_VERBOSE", error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_uefi_capsule_device_get_results(FuDevice *device, GError **error) { FuUefiCapsuleDevice *self = FU_UEFI_CAPSULE_DEVICE(device); FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); /* capture EFI binary debug output */ if (fu_device_has_private_flag(device, FU_UEFI_CAPSULE_DEVICE_FLAG_ENABLE_DEBUGGING)) fu_uefi_capsule_device_capture_efi_debugging(device); /* just set the update error */ fu_uefi_capsule_device_set_status(self, priv->last_attempt_status); return TRUE; } FuVolume * fu_uefi_capsule_device_get_esp(FuUefiCapsuleDevice *self) { FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); return priv->esp; } static FuFirmware * fu_uefi_capsule_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuUefiCapsuleDevice *self = FU_UEFI_CAPSULE_DEVICE(device); FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); gsize sz_reqd = priv->require_esp_free_space; g_autoptr(FuFirmware) firmware = fu_firmware_new(); /* sanity check */ if (priv->esp == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no ESP set"); return NULL; } /* check there is enough space in the ESP */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; if (sz_reqd == 0) { if (fu_device_has_private_flag(FU_DEVICE(self), FU_UEFI_CAPSULE_DEVICE_FLAG_NO_ESP_BACKUP)) { g_info("minimal additional ESP free space required, using %uMB + %uMB", (guint)fu_firmware_get_size(firmware) / (1024 * 1024), (guint)FU_UEFI_CAPSULE_EXTRA_SIZE_REQUIRED / ((1024 * 1024))); sz_reqd = fu_firmware_get_size(firmware) + FU_UEFI_CAPSULE_EXTRA_SIZE_REQUIRED; } else { g_info("required ESP free space is not configured, using (2 x %uMB) + %uMB", (guint)fu_firmware_get_size(firmware) / (1024 * 1024), (guint)FU_UEFI_CAPSULE_EXTRA_SIZE_REQUIRED / ((1024 * 1024))); sz_reqd = fu_firmware_get_size(firmware) * 2 + FU_UEFI_CAPSULE_EXTRA_SIZE_REQUIRED; } } if (!fu_volume_check_free_space(priv->esp, sz_reqd, error)) return NULL; /* success */ return g_steal_pointer(&firmware); } static void fu_uefi_capsule_device_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuUefiCapsuleDevice *self = FU_UEFI_CAPSULE_DEVICE(object); FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_FW_CLASS: priv->fw_class = g_value_dup_string(value); break; case PROP_KIND: priv->kind = g_value_get_uint(value); break; case PROP_CAPSULE_FLAGS: priv->capsule_flags = g_value_get_uint(value); break; case PROP_FW_VERSION: priv->fw_version = g_value_get_uint(value); break; case PROP_FW_VERSION_LOWEST: priv->fw_version_lowest = g_value_get_uint(value); break; case PROP_LAST_ATTEMPT_STATUS: fu_uefi_capsule_device_set_status(self, g_value_get_uint(value)); break; case PROP_LAST_ATTEMPT_VERSION: priv->last_attempt_version = g_value_get_uint(value); break; case PROP_FMP_HARDWARE_INSTANCE: priv->fmp_hardware_instance = g_value_get_uint64(value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_uefi_capsule_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_uefi_capsule_device_init(FuUefiCapsuleDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "org.uefi.capsule"); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_SIGNED); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_FLAGS); fu_device_register_private_flag(FU_DEVICE(self), FU_UEFI_CAPSULE_DEVICE_FLAG_NO_UX_CAPSULE); fu_device_register_private_flag(FU_DEVICE(self), FU_UEFI_CAPSULE_DEVICE_FLAG_USE_SHIM_UNIQUE); fu_device_register_private_flag(FU_DEVICE(self), FU_UEFI_CAPSULE_DEVICE_FLAG_USE_LEGACY_BOOTMGR_DESC); fu_device_register_private_flag(FU_DEVICE(self), FU_UEFI_CAPSULE_DEVICE_FLAG_SUPPORTS_BOOT_ORDER_LOCK); fu_device_register_private_flag(FU_DEVICE(self), FU_UEFI_CAPSULE_DEVICE_FLAG_USE_SHIM_FOR_SB); fu_device_register_private_flag(FU_DEVICE(self), FU_UEFI_CAPSULE_DEVICE_FLAG_NO_RT_SET_VARIABLE); fu_device_register_private_flag(FU_DEVICE(self), FU_UEFI_CAPSULE_DEVICE_FLAG_NO_CAPSULE_HEADER_FIXUP); fu_device_register_private_flag(FU_DEVICE(self), FU_UEFI_CAPSULE_DEVICE_FLAG_ENABLE_DEBUGGING); fu_device_register_private_flag(FU_DEVICE(self), FU_UEFI_CAPSULE_DEVICE_FLAG_COD_INDEXED_FILENAME); fu_device_register_private_flag(FU_DEVICE(self), FU_UEFI_CAPSULE_DEVICE_FLAG_MODIFY_BOOTORDER); fu_device_register_private_flag(FU_DEVICE(self), FU_UEFI_CAPSULE_DEVICE_FLAG_COD_DELL_RECOVERY); fu_device_register_private_flag(FU_DEVICE(self), FU_UEFI_CAPSULE_DEVICE_FLAG_NO_ESP_BACKUP); fu_device_register_private_flag(FU_DEVICE(self), FU_UEFI_CAPSULE_DEVICE_FLAG_USE_FWUPD_EFI); } static void fu_uefi_capsule_device_finalize(GObject *object) { FuUefiCapsuleDevice *self = FU_UEFI_CAPSULE_DEVICE(object); FuUefiCapsuleDevicePrivate *priv = GET_PRIVATE(self); g_free(priv->fw_class); if (priv->esp != NULL) g_object_unref(priv->esp); if (priv->esp_locker != NULL) g_object_unref(priv->esp_locker); G_OBJECT_CLASS(fu_uefi_capsule_device_parent_class)->finalize(object); } static gchar * fu_uefi_capsule_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_device_get_version_format(device)); } static void fu_uefi_capsule_device_class_init(FuUefiCapsuleDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); GParamSpec *pspec; FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->set_property = fu_uefi_capsule_device_set_property; object_class->finalize = fu_uefi_capsule_device_finalize; device_class->to_string = fu_uefi_capsule_device_to_string; device_class->probe = fu_uefi_capsule_device_probe; device_class->prepare_firmware = fu_uefi_capsule_device_prepare_firmware; device_class->prepare = fu_uefi_capsule_device_prepare; device_class->cleanup = fu_uefi_capsule_device_cleanup; device_class->report_metadata_pre = fu_uefi_capsule_device_report_metadata_pre; device_class->report_metadata_post = fu_uefi_capsule_device_report_metadata_post; device_class->get_results = fu_uefi_capsule_device_get_results; device_class->set_progress = fu_uefi_capsule_device_set_progress; device_class->convert_version = fu_uefi_capsule_device_convert_version; /** * FuUefiCapsuleDevice:fw-class: * * The firmware class, i.e. the ESRT GUID. */ pspec = g_param_spec_string("fw-class", NULL, NULL, NULL, G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_FW_CLASS, pspec); /** * FuUefiCapsuleDevice:kind: * * The device kind. */ pspec = g_param_spec_uint("kind", NULL, NULL, FU_UEFI_CAPSULE_DEVICE_KIND_UNKNOWN, FU_UEFI_CAPSULE_DEVICE_KIND_LAST - 1, FU_UEFI_CAPSULE_DEVICE_KIND_UNKNOWN, G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_KIND, pspec); /** * FuUefiCapsuleDevice:capsule-flags: * * The capsule flags to use for the update. */ pspec = g_param_spec_uint("capsule-flags", NULL, NULL, 0, G_MAXUINT32, 0, G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_CAPSULE_FLAGS, pspec); /** * FuUefiCapsuleDevice:fw-version: * * The current firmware version. */ pspec = g_param_spec_uint("fw-version", NULL, NULL, 0, G_MAXUINT32, 0, G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_FW_VERSION, pspec); /** * FuUefiCapsuleDevice:fw-version-lowest: * * The lowest possible installable version. */ pspec = g_param_spec_uint("fw-version-lowest", NULL, NULL, 0, G_MAXUINT32, 0, G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_FW_VERSION_LOWEST, pspec); /** * FuUefiCapsuleDevice:last-attempt-status: * * The last attempt status value. */ pspec = g_param_spec_uint("last-attempt-status", NULL, NULL, FU_UEFI_CAPSULE_DEVICE_STATUS_SUCCESS, FU_UEFI_CAPSULE_DEVICE_STATUS_LAST - 1, FU_UEFI_CAPSULE_DEVICE_STATUS_SUCCESS, G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_LAST_ATTEMPT_STATUS, pspec); /** * FuUefiCapsuleDevice:last-attempt-version: * * The last attempt firmware version. */ pspec = g_param_spec_uint("last-attempt-version", NULL, NULL, 0, G_MAXUINT32, 0, G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_LAST_ATTEMPT_VERSION, pspec); /** * FuUefiCapsuleDevice:fmp-hardware-instance: * * The FMP hardware instance. */ pspec = g_param_spec_uint64("fmp-hardware-instance", NULL, NULL, 0, G_MAXUINT64, 0, G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_FMP_HARDWARE_INSTANCE, pspec); } fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-capsule-device.h000066400000000000000000000065331501337203100233640ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2015 Peter Jones * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-uefi-capsule-device.h" #include "fu-uefi-update-info.h" #define FU_TYPE_UEFI_CAPSULE_DEVICE (fu_uefi_capsule_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuUefiCapsuleDevice, fu_uefi_capsule_device, FU, UEFI_CAPSULE_DEVICE, FuDevice) struct _FuUefiCapsuleDeviceClass { FuDeviceClass parent_class; }; #define FU_UEFI_CAPSULE_DEVICE_FLAG_NO_UX_CAPSULE "no-ux-capsule" #define FU_UEFI_CAPSULE_DEVICE_FLAG_USE_SHIM_UNIQUE "use-shim-unique" #define FU_UEFI_CAPSULE_DEVICE_FLAG_USE_LEGACY_BOOTMGR_DESC "use-legacy-bootmgr-desc" #define FU_UEFI_CAPSULE_DEVICE_FLAG_SUPPORTS_BOOT_ORDER_LOCK "supports-boot-order-lock" #define FU_UEFI_CAPSULE_DEVICE_FLAG_USE_SHIM_FOR_SB "use-shim-for-sb" #define FU_UEFI_CAPSULE_DEVICE_FLAG_NO_RT_SET_VARIABLE "no-rt-set-variable" #define FU_UEFI_CAPSULE_DEVICE_FLAG_NO_CAPSULE_HEADER_FIXUP "no-capsule-header-fixup" #define FU_UEFI_CAPSULE_DEVICE_FLAG_ENABLE_DEBUGGING "enable-debugging" #define FU_UEFI_CAPSULE_DEVICE_FLAG_COD_INDEXED_FILENAME "cod-indexed-filename" #define FU_UEFI_CAPSULE_DEVICE_FLAG_MODIFY_BOOTORDER "modify-bootorder" #define FU_UEFI_CAPSULE_DEVICE_FLAG_COD_DELL_RECOVERY "cod-dell-recovery" #define FU_UEFI_CAPSULE_DEVICE_FLAG_NO_ESP_BACKUP "no-esp-backup" #define FU_UEFI_CAPSULE_DEVICE_FLAG_USE_FWUPD_EFI "use-fwupd-efi" void fu_uefi_capsule_device_set_esp(FuUefiCapsuleDevice *self, FuVolume *esp); gboolean fu_uefi_capsule_device_clear_status(FuUefiCapsuleDevice *self, GError **error); FuUefiCapsuleDeviceKind fu_uefi_capsule_device_get_kind(FuUefiCapsuleDevice *self); const gchar * fu_uefi_capsule_device_get_guid(FuUefiCapsuleDevice *self); FuVolume * fu_uefi_capsule_device_get_esp(FuUefiCapsuleDevice *self); gchar * fu_uefi_capsule_device_build_varname(FuUefiCapsuleDevice *self); guint32 fu_uefi_capsule_device_get_version(FuUefiCapsuleDevice *self); guint32 fu_uefi_capsule_device_get_version_lowest(FuUefiCapsuleDevice *self); guint32 fu_uefi_capsule_device_get_version_error(FuUefiCapsuleDevice *self); guint32 fu_uefi_capsule_device_get_capsule_flags(FuUefiCapsuleDevice *self); guint64 fu_uefi_capsule_device_get_hardware_instance(FuUefiCapsuleDevice *self); FuUefiCapsuleDeviceStatus fu_uefi_capsule_device_get_status(FuUefiCapsuleDevice *self); FuUefiUpdateInfo * fu_uefi_capsule_device_load_update_info(FuUefiCapsuleDevice *self, GError **error); gboolean fu_uefi_capsule_device_write_update_info(FuUefiCapsuleDevice *self, const gchar *capsule_path, const gchar *varname, const gchar *guid, GError **error); GBytes * fu_uefi_capsule_device_fixup_firmware(FuUefiCapsuleDevice *self, GBytes *fw, GError **error); void fu_uefi_capsule_device_set_status(FuUefiCapsuleDevice *self, FuUefiCapsuleDeviceStatus status); void fu_uefi_capsule_device_set_require_esp_free_space(FuUefiCapsuleDevice *self, gsize require_esp_free_space); gboolean fu_uefi_capsule_device_perhaps_enable_debugging(FuUefiCapsuleDevice *self, GError **error); FuEfiDevicePathList * fu_uefi_capsule_device_build_dp_buf(FuVolume *esp, const gchar *capsule_path, GError **error); gboolean fu_uefi_capsule_device_check_asset(FuUefiCapsuleDevice *self, GError **error); fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-capsule-plugin.c000066400000000000000000001313071501337203100234140ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * Copyright 2015 Peter Jones * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-acpi-uefi.h" #include "fu-bitmap-image.h" #include "fu-uefi-bgrt.h" #include "fu-uefi-bootmgr.h" #include "fu-uefi-capsule-backend.h" #include "fu-uefi-capsule-plugin.h" #include "fu-uefi-cod-device.h" #include "fu-uefi-common.h" #include "fu-uefi-grub-device.h" #include "fu-uefi-nvram-device.h" #include "fu-uefi-struct.h" #include "fu-uefi-update-info.h" struct _FuUefiCapsulePlugin { FuPlugin parent_instance; FuUefiBgrt *bgrt; FuFirmware *acpi_uefi; /* optional */ FuVolume *esp; FuBackend *backend; guint32 screen_width; guint32 screen_height; GFile *fwupd_efi_file; GFileMonitor *fwupd_efi_monitor; }; G_DEFINE_TYPE(FuUefiCapsulePlugin, fu_uefi_capsule_plugin, FU_TYPE_PLUGIN) static void fu_uefi_capsule_plugin_to_string(FuPlugin *plugin, guint idt, GString *str) { FuUefiCapsulePlugin *self = FU_UEFI_CAPSULE_PLUGIN(plugin); if (self->backend != NULL) fu_backend_add_string(self->backend, idt, str); fwupd_codec_string_append_int(str, idt, "ScreenWidth", self->screen_width); fwupd_codec_string_append_int(str, idt, "ScreenHeight", self->screen_height); if (self->bgrt != NULL) { fwupd_codec_string_append_bool(str, idt, "BgrtSupported", fu_uefi_bgrt_get_supported(self->bgrt)); } } static gboolean fu_uefi_capsule_plugin_fwupd_efi_parse(FuUefiCapsulePlugin *self, GError **error) { FuContext *ctx = fu_plugin_get_context(FU_PLUGIN(self)); const guint8 needle[] = "f\0w\0u\0p\0d\0-\0e\0f\0i\0 \0v\0e\0r\0s\0i\0o\0n\0 "; gsize offset = 0; g_autofree gchar *fn = g_file_get_path(self->fwupd_efi_file); g_autofree gchar *version = NULL; g_autofree gchar *version_safe = NULL; g_autoptr(GBytes) buf = NULL; g_autoptr(GBytes) ubuf = NULL; /* find the UTF-16 version string */ buf = fu_bytes_get_contents(fn, error); if (buf == NULL) return FALSE; if (!fu_memmem_safe(g_bytes_get_data(buf, NULL), g_bytes_get_size(buf), needle, sizeof(needle), &offset, error)) { g_prefix_error(error, "searching %s: ", fn); return FALSE; } ubuf = fu_bytes_new_offset(buf, offset + sizeof(needle), 30, error); if (ubuf == NULL) return FALSE; /* convert to UTF-8 */ version = fu_utf16_to_utf8_bytes(ubuf, G_LITTLE_ENDIAN, error); if (version == NULL) { g_prefix_error(error, "converting %s: ", fn); return FALSE; } version_safe = fu_version_ensure_semver(version, FWUPD_VERSION_FORMAT_PAIR); /* success */ fu_context_add_runtime_version(ctx, "org.freedesktop.fwupd-efi", version_safe); return TRUE; } static void fu_uefi_capsule_plugin_fwupd_efi_changed_cb(GFileMonitor *monitor, GFile *file, GFile *other_file, GFileMonitorEvent event_type, gpointer user_data) { FuUefiCapsulePlugin *self = FU_UEFI_CAPSULE_PLUGIN(user_data); FuContext *ctx = fu_plugin_get_context(FU_PLUGIN(self)); g_autoptr(GError) error_local = NULL; if (!fu_uefi_capsule_plugin_fwupd_efi_parse(self, &error_local)) { fu_context_add_runtime_version(ctx, "org.freedesktop.fwupd-efi", "1.0"); g_warning("failed to get new fwupd efi runtime version: %s", error_local->message); return; } } static gboolean fu_uefi_capsule_plugin_fwupd_efi_probe(FuUefiCapsulePlugin *self, GError **error) { FuContext *ctx = fu_plugin_get_context(FU_PLUGIN(self)); FuEfivars *efivars = fu_context_get_efivars(ctx); g_autofree gchar *fn = NULL; /* find the app binary */ fn = fu_uefi_get_built_app_path(efivars, "fwupd", error); if (fn == NULL) return FALSE; self->fwupd_efi_file = g_file_new_for_path(fn); self->fwupd_efi_monitor = g_file_monitor_file(self->fwupd_efi_file, G_FILE_MONITOR_NONE, NULL, error); if (self->fwupd_efi_monitor == NULL) return FALSE; g_signal_connect(G_FILE_MONITOR(self->fwupd_efi_monitor), "changed", G_CALLBACK(fu_uefi_capsule_plugin_fwupd_efi_changed_cb), self); if (!fu_uefi_capsule_plugin_fwupd_efi_parse(self, error)) { fu_context_add_runtime_version(ctx, "org.freedesktop.fwupd-efi", "1.0"); return FALSE; } return TRUE; } static gboolean fu_uefi_capsule_plugin_clear_results(FuPlugin *plugin, FuDevice *device, GError **error) { FuUefiCapsuleDevice *device_uefi = FU_UEFI_CAPSULE_DEVICE(device); return fu_uefi_capsule_device_clear_status(device_uefi, error); } static gchar * fu_uefi_capsule_plugin_efivars_attrs_to_string(guint32 attrs) { const gchar *data[7] = {0}; guint idx = 0; if (attrs & FU_EFIVARS_ATTR_NON_VOLATILE) data[idx++] = "non-volatile"; if (attrs & FU_EFIVARS_ATTR_BOOTSERVICE_ACCESS) data[idx++] = "bootservice-access"; if (attrs & FU_EFIVARS_ATTR_RUNTIME_ACCESS) data[idx++] = "runtime-access"; if (attrs & FU_EFIVARS_ATTR_HARDWARE_ERROR_RECORD) data[idx++] = "hardware-error-record"; if (attrs & FU_EFIVARS_ATTR_AUTHENTICATED_WRITE_ACCESS) data[idx++] = "authenticated-write-access"; if (attrs & FU_EFIVARS_ATTR_TIME_BASED_AUTHENTICATED_WRITE_ACCESS) data[idx++] = "time-based-authenticated-write-access"; if (attrs & FU_EFIVARS_ATTR_APPEND_WRITE) data[idx++] = "append-write"; return g_strjoinv(",", (gchar **)data); } static void fu_uefi_capsule_plugin_add_security_attrs_secureboot(FuPlugin *plugin, FuSecurityAttrs *attrs) { FuEfivars *efivars = fu_context_get_efivars(fu_plugin_get_context(plugin)); gboolean secureboot_enabled = FALSE; g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(GError) error = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_UEFI_SECUREBOOT); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_ENABLED); fu_security_attrs_append(attrs, attr); /* SB not available or disabled */ if (!fu_efivars_get_secure_boot(efivars, &secureboot_enabled, NULL)) fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA); if (!secureboot_enabled) { if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND); return; } fu_security_attr_add_bios_target_value(attr, "SecureBoot", "enable"); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_uefi_capsule_plugin_add_security_attrs_bootservices(FuPlugin *plugin, FuSecurityAttrs *attrs) { FuEfivars *efivars = fu_context_get_efivars(fu_plugin_get_context(plugin)); g_autoptr(FwupdSecurityAttr) attr = NULL; const gchar *guids[] = {FU_EFIVARS_GUID_SECURITY_DATABASE, FU_EFIVARS_GUID_EFI_GLOBAL}; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_UEFI_BOOTSERVICE_VARS); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_LOCKED); fu_security_attrs_append(attrs, attr); for (guint j = 0; j < G_N_ELEMENTS(guids); j++) { g_autoptr(GPtrArray) names = fu_efivars_get_names(efivars, guids[j], NULL); /* sanity check */ if (names == NULL) continue; for (guint i = 0; i < names->len; i++) { const gchar *name = g_ptr_array_index(names, i); g_autoptr(GError) error_local = NULL; gsize data_sz = 0; guint32 data_attr = 0; if (!fu_efivars_get_data(efivars, guids[j], name, NULL, &data_sz, &data_attr, &error_local)) { g_warning("failed to read %s-%s: %s", name, guids[j], error_local->message); continue; } if ((data_attr & FU_EFIVARS_ATTR_BOOTSERVICE_ACCESS) > 0 && (data_attr & FU_EFIVARS_ATTR_RUNTIME_ACCESS) == 0) { g_autofree gchar *flags = fu_uefi_capsule_plugin_efivars_attrs_to_string(data_attr); g_debug("%s-%s attr of size 0x%x had flags %s", name, guids[j], (guint)data_sz, flags); fwupd_security_attr_add_metadata(attr, "guid", guids[j]); fwupd_security_attr_add_metadata(attr, "name", name); fwupd_security_attr_add_flag( attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); fwupd_security_attr_set_result( attr, FWUPD_SECURITY_ATTR_RESULT_NOT_LOCKED); return; } } } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static void fu_uefi_capsule_plugin_add_security_attrs(FuPlugin *plugin, FuSecurityAttrs *attrs) { fu_uefi_capsule_plugin_add_security_attrs_secureboot(plugin, attrs); fu_uefi_capsule_plugin_add_security_attrs_bootservices(plugin, attrs); } static GBytes * fu_uefi_capsule_plugin_get_splash_data(guint width, guint height, GError **error) { const gchar *const *langs = g_get_language_names(); g_autofree gchar *datadir_pkg = NULL; g_autofree gchar *filename_archive = NULL; g_autofree gchar *langs_str = NULL; g_autoptr(FuArchive) archive = NULL; g_autoptr(GInputStream) stream_archive = NULL; /* load archive */ datadir_pkg = fu_path_from_kind(FU_PATH_KIND_DATADIR_PKG); filename_archive = g_build_filename(datadir_pkg, "uefi-capsule-ux.tar.xz", NULL); stream_archive = fu_input_stream_from_path(filename_archive, error); if (stream_archive == NULL) return NULL; archive = fu_archive_new_stream(stream_archive, FU_ARCHIVE_FLAG_NONE, error); if (archive == NULL) return NULL; /* find the closest locale match, falling back to `en` and `C` */ for (guint i = 0; langs[i] != NULL; i++) { g_autofree gchar *fn = NULL; g_autoptr(GBytes) blob_tmp = NULL; if (g_str_has_suffix(langs[i], ".UTF-8")) continue; fn = g_strdup_printf("fwupd-%s-%u-%u.bmp", langs[i], width, height); blob_tmp = fu_archive_lookup_by_fn(archive, fn, NULL); if (blob_tmp != NULL) { g_debug("using UX image %s", fn); return g_bytes_ref(blob_tmp); } g_debug("no %s found", fn); } /* we found nothing */ langs_str = g_strjoinv(",", (gchar **)langs); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to get splash file for %s in %s", langs_str, datadir_pkg); return NULL; } static gboolean fu_uefi_capsule_plugin_write_splash_data(FuUefiCapsulePlugin *self, FuDevice *device, GBytes *blob, GError **error) { gssize size; guint32 width; guint8 csum = 0; fwupd_guid_t guid = {0x0}; g_autofree gchar *capsule_path = NULL; g_autofree gchar *esp_path = NULL; g_autofree gchar *fn = NULL; g_autofree gchar *basename = NULL; g_autofree gchar *directory = NULL; g_autoptr(FuBitmapImage) bmp_image = fu_bitmap_image_new(); g_autoptr(GByteArray) st_cap = fu_struct_efi_capsule_header_new(); g_autoptr(GByteArray) st_uxh = fu_struct_efi_ux_capsule_header_new(); g_autoptr(GFile) ofile = NULL; g_autoptr(GOutputStream) ostream = NULL; /* get screen dimensions */ if (!fu_firmware_parse_bytes(FU_FIRMWARE(bmp_image), blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, error)) { g_prefix_error(error, "splash invalid: "); return FALSE; } width = fu_bitmap_image_get_width(bmp_image); /* save to a predictable filename */ esp_path = fu_volume_get_mount_point(self->esp); directory = fu_uefi_get_esp_path_for_os(esp_path); basename = g_strdup_printf("fwupd-%s.cap", FU_EFIVARS_GUID_UX_CAPSULE); capsule_path = g_build_filename(directory, "fw", basename, NULL); fn = g_build_filename(esp_path, capsule_path, NULL); if (!fu_path_mkdir_parent(fn, error)) return FALSE; ofile = g_file_new_for_path(fn); ostream = G_OUTPUT_STREAM(g_file_replace(ofile, NULL, FALSE, G_FILE_CREATE_NONE, NULL, error)); if (ostream == NULL) return FALSE; fu_struct_efi_capsule_header_set_flags(st_cap, EFI_CAPSULE_HEADER_FLAGS_PERSIST_ACROSS_RESET); if (!fwupd_guid_from_string(FU_EFIVARS_GUID_UX_CAPSULE, &guid, FWUPD_GUID_FLAG_MIXED_ENDIAN, error)) return FALSE; fu_struct_efi_capsule_header_set_guid(st_cap, &guid); fu_struct_efi_capsule_header_set_image_size(st_cap, g_bytes_get_size(blob) + FU_STRUCT_EFI_CAPSULE_HEADER_SIZE + FU_STRUCT_EFI_UX_CAPSULE_HEADER_SIZE); fu_struct_efi_ux_capsule_header_set_x_offset(st_uxh, (self->screen_width / 2) - (width / 2)); if (self->screen_height == fu_uefi_bgrt_get_height(self->bgrt)) { fu_struct_efi_ux_capsule_header_set_y_offset(st_uxh, (gdouble)self->screen_height * 0.8f); } else { fu_struct_efi_ux_capsule_header_set_y_offset( st_uxh, fu_uefi_bgrt_get_yoffset(self->bgrt) + fu_uefi_bgrt_get_height(self->bgrt)); }; /* header, payload and image has to add to zero */ csum += fu_sum8(st_cap->data, st_cap->len); csum += fu_sum8(st_uxh->data, st_uxh->len); csum += fu_sum8_bytes(blob); fu_struct_efi_ux_capsule_header_set_checksum(st_uxh, 0x100 - csum); /* write capsule file */ size = g_output_stream_write(ostream, st_cap->data, st_cap->len, NULL, error); if (size < 0) return FALSE; size = g_output_stream_write(ostream, st_uxh->data, st_uxh->len, NULL, error); if (size < 0) return FALSE; size = g_output_stream_write_bytes(ostream, blob, NULL, error); if (size < 0) return FALSE; /* write display capsule location as UPDATE_INFO */ return fu_uefi_capsule_device_write_update_info(FU_UEFI_CAPSULE_DEVICE(device), capsule_path, "fwupd-ux-capsule", FU_EFIVARS_GUID_UX_CAPSULE, error); } static gboolean fu_uefi_capsule_plugin_update_splash(FuPlugin *plugin, FuDevice *device, GError **error) { FuEfivars *efivars = fu_context_get_efivars(fu_plugin_get_context(plugin)); FuUefiCapsulePlugin *self = FU_UEFI_CAPSULE_PLUGIN(plugin); guint best_idx = G_MAXUINT; guint32 lowest_border_pixels = G_MAXUINT; g_autoptr(GBytes) image_bmp = NULL; struct { guint32 width; guint32 height; } sizes[] = {{640, 480}, /* matching the sizes in po/make-images */ {800, 600}, {1024, 768}, {1920, 1080}, {3840, 2160}, {5120, 2880}, {5688, 3200}, {7680, 4320}, {0, 0}}; /* no UX capsule support, so deleting var if it exists */ if (fu_device_has_private_flag(device, FU_UEFI_CAPSULE_DEVICE_FLAG_NO_UX_CAPSULE) && fu_efivars_exists(efivars, FU_EFIVARS_GUID_FWUPDATE, "fwupd-ux-capsule")) { g_info("not providing UX capsule"); return fu_efivars_delete(efivars, FU_EFIVARS_GUID_FWUPDATE, "fwupd-ux-capsule", error); } /* get the boot graphics resource table data */ if (!fu_uefi_bgrt_get_supported(self->bgrt)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "BGRT is not supported"); return FALSE; } g_debug("framebuffer size %" G_GUINT32_FORMAT " x%" G_GUINT32_FORMAT, self->screen_width, self->screen_height); /* find the 'best sized' pre-generated image */ for (guint i = 0; sizes[i].width != 0; i++) { guint32 border_pixels; /* disregard any images that are bigger than the screen */ if (sizes[i].width > self->screen_width) continue; if (sizes[i].height > self->screen_height) continue; /* is this the best fit for the display */ border_pixels = (self->screen_width * self->screen_height) - (sizes[i].width * sizes[i].height); if (border_pixels < lowest_border_pixels) { lowest_border_pixels = border_pixels; best_idx = i; } } if (best_idx == G_MAXUINT) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to find a suitable image to use"); return FALSE; } /* get the raw data */ image_bmp = fu_uefi_capsule_plugin_get_splash_data(sizes[best_idx].width, sizes[best_idx].height, error); if (image_bmp == NULL) return FALSE; /* perform the upload */ return fu_uefi_capsule_plugin_write_splash_data(self, device, image_bmp, error); } static gboolean fu_uefi_capsule_plugin_write_firmware(FuPlugin *plugin, FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { const gchar *str; guint32 flashes_left; g_autoptr(GError) error_splash = NULL; /* test the flash counter */ flashes_left = fu_device_get_flashes_left(device); if (flashes_left > 0) { g_debug("%s has %" G_GUINT32_FORMAT " flashes left", fu_device_get_name(device), flashes_left); if ((flags & FWUPD_INSTALL_FLAG_FORCE) == 0 && flashes_left <= 2) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "%s only has %" G_GUINT32_FORMAT " flashes left -- " "see https://github.com/fwupd/fwupd/wiki/Dell-TPM:-flashes-left for " "more information.", fu_device_get_name(device), flashes_left); return FALSE; } } /* TRANSLATORS: this is shown when updating the firmware after the reboot */ str = _("Installing firmware update…"); g_return_val_if_fail(str != NULL, FALSE); /* perform the update */ fu_progress_set_status(progress, FWUPD_STATUS_SCHEDULING); if (!fu_uefi_capsule_plugin_update_splash(plugin, device, &error_splash)) g_info("failed to upload UEFI UX capsule text: %s", error_splash->message); return fu_device_write_firmware(device, firmware, progress, flags, error); } static void fu_uefi_capsule_plugin_load_config(FuPlugin *plugin, FuDevice *device) { guint64 sz_reqd = 0; g_autofree gchar *require_esp_free_space = NULL; g_autoptr(GError) error_local = NULL; /* parse free space needed for ESP */ require_esp_free_space = fu_plugin_get_config_value(plugin, "RequireESPFreeSpace"); if (!fu_strtoull(require_esp_free_space, &sz_reqd, 0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, &error_local)) { g_warning("invalid ESP free space specified: %s", error_local->message); } fu_uefi_capsule_device_set_require_esp_free_space(FU_UEFI_CAPSULE_DEVICE(device), sz_reqd); /* shim used for SB or not? */ if (!fu_plugin_get_config_value_boolean(plugin, "DisableShimForSecureBoot")) fu_device_add_private_flag(device, FU_UEFI_CAPSULE_DEVICE_FLAG_USE_SHIM_FOR_SB); /* enable the fwupd.efi debug log? */ if (fu_plugin_get_config_value_boolean(plugin, "EnableEfiDebugging")) fu_device_add_private_flag(device, FU_UEFI_CAPSULE_DEVICE_FLAG_ENABLE_DEBUGGING); } static void fu_uefi_capsule_plugin_validate_esp(FuUefiCapsulePlugin *self) { g_autofree gchar *kind = NULL; /* nothing found */ if (self->esp == NULL) { fu_plugin_add_flag(FU_PLUGIN(self), FWUPD_PLUGIN_FLAG_ESP_NOT_FOUND); fu_plugin_add_flag(FU_PLUGIN(self), FWUPD_PLUGIN_FLAG_CLEAR_UPDATABLE); fu_plugin_add_flag(FU_PLUGIN(self), FWUPD_PLUGIN_FLAG_USER_WARNING); return; } /* found *something* but it wasn't quite an ESP... */ kind = fu_volume_get_partition_kind(self->esp); if (kind != NULL && g_strcmp0(fu_volume_kind_convert_to_gpt(kind), FU_VOLUME_KIND_ESP) != 0) { fu_plugin_add_flag(FU_PLUGIN(self), FWUPD_PLUGIN_FLAG_ESP_NOT_VALID); fu_plugin_add_flag(FU_PLUGIN(self), FWUPD_PLUGIN_FLAG_USER_WARNING); } } static void fu_uefi_capsule_plugin_register_proxy_device(FuPlugin *plugin, FuDevice *device) { FuUefiCapsulePlugin *self = FU_UEFI_CAPSULE_PLUGIN(plugin); g_autoptr(FuUefiCapsuleDevice) dev = NULL; g_autoptr(GError) error_local = NULL; /* load all configuration variables */ dev = fu_uefi_capsule_backend_device_new_from_dev(FU_UEFI_CAPSULE_BACKEND(self->backend), device); fu_uefi_capsule_plugin_load_config(plugin, FU_DEVICE(dev)); if (self->esp == NULL) { self->esp = fu_context_get_default_esp(fu_plugin_get_context(plugin), &error_local); if (self->esp == NULL) g_warning("cannot find default ESP: %s", error_local->message); fu_uefi_capsule_plugin_validate_esp(self); } if (self->esp == NULL) { fu_device_inhibit(device, "no-esp", error_local->message); } else { fu_uefi_capsule_device_set_esp(dev, self->esp); fu_device_uninhibit(device, "no-esp"); } fu_plugin_device_add(plugin, FU_DEVICE(dev)); } static void fu_uefi_capsule_plugin_device_registered(FuPlugin *plugin, FuDevice *device) { if (fu_device_get_metadata(device, FU_DEVICE_METADATA_UEFI_DEVICE_KIND) != NULL) { if (fu_device_get_guid_default(device) == NULL) { g_autofree gchar *dbg = fu_device_to_string(device); g_warning("cannot create proxy device as no GUID: %s", dbg); return; } fu_uefi_capsule_plugin_register_proxy_device(plugin, device); } } static const gchar * fu_uefi_capsule_plugin_uefi_type_to_string(FuUefiCapsuleDeviceKind device_kind) { if (device_kind == FU_UEFI_CAPSULE_DEVICE_KIND_UNKNOWN) return "Unknown Firmware"; if (device_kind == FU_UEFI_CAPSULE_DEVICE_KIND_SYSTEM_FIRMWARE) return "System Firmware"; if (device_kind == FU_UEFI_CAPSULE_DEVICE_KIND_DEVICE_FIRMWARE) return "Device Firmware"; if (device_kind == FU_UEFI_CAPSULE_DEVICE_KIND_UEFI_DRIVER) return "UEFI Driver"; if (device_kind == FU_UEFI_CAPSULE_DEVICE_KIND_FMP) return "Firmware Management Protocol"; return NULL; } static gchar * fu_uefi_capsule_plugin_get_name_for_type(FuPlugin *plugin, FuUefiCapsuleDeviceKind device_kind) { GString *display_name; /* set Display Name prefix for capsules that are not PCI cards */ display_name = g_string_new(fu_uefi_capsule_plugin_uefi_type_to_string(device_kind)); if (device_kind == FU_UEFI_CAPSULE_DEVICE_KIND_DEVICE_FIRMWARE) g_string_prepend(display_name, "UEFI "); return g_string_free(display_name, FALSE); } static gboolean fu_uefi_capsule_plugin_coldplug_device(FuPlugin *plugin, FuUefiCapsuleDevice *dev, GError **error) { FuUefiCapsulePlugin *self = FU_UEFI_CAPSULE_PLUGIN(plugin); FuContext *ctx = fu_plugin_get_context(plugin); FuUefiCapsuleDeviceKind device_kind; g_autoptr(GError) error_udisks2 = NULL; /* probe to get add GUIDs (and hence any quirk fixups) */ if (!fu_device_probe(FU_DEVICE(dev), error)) return FALSE; if (!fu_device_setup(FU_DEVICE(dev), error)) return FALSE; /* if not already set by quirks */ if (fu_context_has_hwid_flag(ctx, "use-legacy-bootmgr-desc")) { fu_device_add_private_flag(FU_DEVICE(dev), FU_UEFI_CAPSULE_DEVICE_FLAG_USE_LEGACY_BOOTMGR_DESC); } if (fu_context_has_hwid_flag(ctx, "supports-boot-order-lock")) { fu_device_add_private_flag(FU_DEVICE(dev), FU_UEFI_CAPSULE_DEVICE_FLAG_SUPPORTS_BOOT_ORDER_LOCK); } if (fu_context_has_hwid_flag(ctx, "no-ux-capsule")) fu_device_add_private_flag(FU_DEVICE(dev), FU_UEFI_CAPSULE_DEVICE_FLAG_NO_UX_CAPSULE); if (fu_context_has_hwid_flag(ctx, "no-lid-closed")) fu_device_add_private_flag(FU_DEVICE(dev), FU_DEVICE_PRIVATE_FLAG_NO_LID_CLOSED); if (fu_context_has_hwid_flag(ctx, "display-required")) { fu_device_add_private_flag(FU_DEVICE(dev), FU_DEVICE_PRIVATE_FLAG_DISPLAY_REQUIRED); } if (fu_context_has_hwid_flag(ctx, "modify-bootorder")) fu_device_add_private_flag(FU_DEVICE(dev), FU_UEFI_CAPSULE_DEVICE_FLAG_MODIFY_BOOTORDER); if (fu_context_has_hwid_flag(ctx, "cod-dell-recovery")) fu_device_add_private_flag(FU_DEVICE(dev), FU_UEFI_CAPSULE_DEVICE_FLAG_COD_DELL_RECOVERY); /* detected InsydeH2O */ if (self->acpi_uefi != NULL && fu_acpi_uefi_cod_indexed_filename(FU_ACPI_UEFI(self->acpi_uefi))) { fu_device_add_private_flag(FU_DEVICE(dev), FU_UEFI_CAPSULE_DEVICE_FLAG_COD_INDEXED_FILENAME); } /* set fallback name if nothing else is set */ device_kind = fu_uefi_capsule_device_get_kind(dev); if (fu_device_get_name(FU_DEVICE(dev)) == NULL) { g_autofree gchar *name = NULL; name = fu_uefi_capsule_plugin_get_name_for_type(plugin, device_kind); if (name != NULL) fu_device_set_name(FU_DEVICE(dev), name); if (device_kind != FU_UEFI_CAPSULE_DEVICE_KIND_SYSTEM_FIRMWARE) { fu_device_add_private_flag(FU_DEVICE(dev), FU_DEVICE_PRIVATE_FLAG_MD_SET_NAME_CATEGORY); } } /* set fallback vendor if nothing else is set */ if (fu_device_get_vendor(FU_DEVICE(dev)) == NULL && device_kind == FU_UEFI_CAPSULE_DEVICE_KIND_SYSTEM_FIRMWARE) { const gchar *vendor = fu_context_get_hwid_value(ctx, FU_HWIDS_KEY_MANUFACTURER); if (vendor != NULL) fu_device_set_vendor(FU_DEVICE(dev), vendor); } /* set vendor ID as the BIOS vendor */ if (device_kind != FU_UEFI_CAPSULE_DEVICE_KIND_FMP) { fu_device_build_vendor_id(FU_DEVICE(dev), "DMI", fu_context_get_hwid_value(ctx, FU_HWIDS_KEY_BIOS_VENDOR)); } fu_device_add_request_flag(FU_DEVICE(dev), FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); /* find and set ESP */ if (self->esp == NULL) { self->esp = fu_context_get_default_esp(fu_plugin_get_context(plugin), &error_udisks2); if (self->esp == NULL) g_warning("cannot find default ESP: %s", error_udisks2->message); fu_uefi_capsule_plugin_validate_esp(self); } if (self->esp != NULL) fu_uefi_capsule_device_set_esp(dev, self->esp); /* success */ return TRUE; } static void fu_uefi_capsule_plugin_test_secure_boot(FuPlugin *plugin) { FuEfivars *efivars = fu_context_get_efivars(fu_plugin_get_context(plugin)); gboolean secureboot_enabled = FALSE; g_autoptr(GError) error_local = NULL; if (!fu_efivars_get_secure_boot(efivars, &secureboot_enabled, &error_local)) { fu_plugin_add_report_metadata(plugin, "SecureBoot", error_local->message); return; } fu_plugin_add_report_metadata(plugin, "SecureBoot", secureboot_enabled ? "Enabled" : "Disabled"); } static FuFirmware * fu_uefi_capsule_plugin_parse_acpi_uefi(FuUefiCapsulePlugin *self, GError **error) { g_autofree gchar *fn = NULL; g_autofree gchar *path = NULL; g_autoptr(FuFirmware) firmware = fu_acpi_uefi_new(); g_autoptr(GFile) file = NULL; /* if we have a table, parse it and validate it */ path = fu_path_from_kind(FU_PATH_KIND_ACPI_TABLES); fn = g_build_filename(path, "UEFI", NULL); file = g_file_new_for_path(fn); if (!fu_firmware_parse_file(firmware, file, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return NULL; return g_steal_pointer(&firmware); } static gboolean fu_uefi_capsule_plugin_ensure_screen_size_config(FuUefiCapsulePlugin *self, GError **error) { g_autofree gchar *screen_width = NULL; g_autofree gchar *screen_height = NULL; /* is overridden? */ screen_width = fu_plugin_get_config_value(FU_PLUGIN(self), "ScreenWidth"); if (screen_width != NULL) { guint64 tmp64 = 0; if (!fu_strtoull(screen_width, &tmp64, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "failed to parse ScreenWidth: "); return FALSE; } self->screen_width = (guint32)tmp64; } screen_height = fu_plugin_get_config_value(FU_PLUGIN(self), "ScreenHeight"); if (screen_height != NULL) { guint64 tmp64 = 0; if (!fu_strtoull(screen_height, &tmp64, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "failed to parse ScreenHeight: "); return FALSE; } self->screen_height = (guint32)tmp64; } /* success */ return TRUE; } static gboolean fu_uefi_capsule_plugin_ensure_screen_size_drm(FuUefiCapsulePlugin *self, FuDrmDevice *drm_device, GError **error) { g_autoptr(FuDeviceLocker) locker = NULL; /* open the fd */ locker = fu_device_locker_new(FU_DEVICE(drm_device), error); if (locker == NULL) return FALSE; if (fu_drm_device_get_crtc_x(drm_device) == 0 && fu_drm_device_get_crtc_y(drm_device) == 0 && fu_drm_device_get_crtc_width(drm_device) > 0 && fu_drm_device_get_crtc_height(drm_device) > 0) { self->screen_width = fu_drm_device_get_crtc_width(drm_device); self->screen_height = fu_drm_device_get_crtc_height(drm_device); } /* success */ return TRUE; } static gboolean fu_uefi_capsule_plugin_startup(FuPlugin *plugin, FuProgress *progress, GError **error) { FuUefiCapsulePlugin *self = FU_UEFI_CAPSULE_PLUGIN(plugin); FuContext *ctx = fu_plugin_get_context(plugin); FuEfivars *efivars = fu_context_get_efivars(ctx); guint64 nvram_total; g_autofree gchar *nvram_total_str = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(GError) error_acpi_uefi = NULL; /* for the uploaded report */ if (fu_context_has_hwid_flag(ctx, "use-legacy-bootmgr-desc")) fu_plugin_add_report_metadata(plugin, "BootMgrDesc", "legacy"); /* some platforms have broken SMBIOS data */ if (fu_context_has_hwid_flag(ctx, "uefi-force-enable")) return TRUE; /* use GRUB to load updates */ if (fu_plugin_get_config_value_boolean(plugin, "EnableGrubChainLoad")) { fu_uefi_capsule_backend_set_device_gtype(FU_UEFI_CAPSULE_BACKEND(self->backend), FU_TYPE_UEFI_GRUB_DEVICE); } /* check we can use this backend */ if (!fu_backend_setup(self->backend, FU_BACKEND_SETUP_FLAG_NONE, progress, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_WRITE)) { fu_plugin_add_flag(plugin, FWUPD_PLUGIN_FLAG_EFIVAR_NOT_MOUNTED); fu_plugin_add_flag(plugin, FWUPD_PLUGIN_FLAG_CLEAR_UPDATABLE); fu_plugin_add_flag(plugin, FWUPD_PLUGIN_FLAG_USER_WARNING); } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } /* get the fallback screen size for the UX capsule from fwupd.conf */ if (!fu_uefi_capsule_plugin_ensure_screen_size_config(self, error)) return FALSE; /* now try the now-removed upstream efi-framebuffer */ if (self->screen_height == 0 || self->screen_width == 0) { g_autoptr(GError) error_efifb = NULL; if (!fu_uefi_get_framebuffer_size(&self->screen_width, &self->screen_height, &error_efifb)) g_debug("EFIFB data was not readable: %s", error_efifb->message); } /* are the EFI dirs set up so we can update each device */ if (!fu_efivars_supported(efivars, error)) return FALSE; nvram_total = fu_efivars_space_used(efivars, error); if (nvram_total == G_MAXUINT64) return FALSE; nvram_total_str = g_strdup_printf("%" G_GUINT64_FORMAT, nvram_total); fu_plugin_add_report_metadata(plugin, "EfivarsNvramUsed", nvram_total_str); /* we use this both for quirking the CoD implementation sanity and the CoD filename */ self->acpi_uefi = fu_uefi_capsule_plugin_parse_acpi_uefi(self, &error_acpi_uefi); if (self->acpi_uefi == NULL) g_debug("failed to load ACPI UEFI table: %s", error_acpi_uefi->message); /* test for invalid ESP in coldplug, and set the update-error rather * than showing no output if the plugin had self-disabled here */ return TRUE; } static gboolean fu_uefi_capsule_plugin_unlock(FuPlugin *plugin, FuDevice *device, GError **error) { /* not us */ if (!FU_IS_UEFI_CAPSULE_DEVICE(device)) return TRUE; if (fu_uefi_capsule_device_get_kind(FU_UEFI_CAPSULE_DEVICE(device)) != FU_UEFI_CAPSULE_DEVICE_KIND_DELL_TPM_FIRMWARE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Unable to unlock %s", fu_device_get_name(device)); return FALSE; } /* make sure that this unlocked device can be updated */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_QUAD); fu_device_set_version(device, "0.0.0.0"); return TRUE; } static void fu_uefi_capsule_plugin_update_state_notify_cb(GObject *object, GParamSpec *pspec, FuPlugin *plugin) { FuDevice *device = FU_DEVICE(object); GPtrArray *devices; g_autofree gchar *msg = NULL; /* device is not in needs-reboot state */ if (fu_device_get_update_state(device) != FWUPD_UPDATE_STATE_NEEDS_REBOOT) return; /* only do this on hardware that cannot coalesce multiple capsules */ if (!fu_context_has_hwid_flag(fu_plugin_get_context(plugin), "no-coalesce")) return; /* mark every other device for this plugin as non-updatable */ msg = g_strdup_printf("Cannot update as %s [%s] needs reboot", fu_device_get_name(device), fu_device_get_id(device)); devices = fu_plugin_get_devices(plugin); for (guint i = 0; i < devices->len; i++) { FuDevice *device_tmp = g_ptr_array_index(devices, i); if (device_tmp == device) continue; fu_device_inhibit(device_tmp, "no-coalesce", msg); } } static gboolean fu_uefi_capsule_plugin_check_cod_support(FuUefiCapsulePlugin *self, GError **error) { FuContext *ctx = fu_plugin_get_context(FU_PLUGIN(self)); FuEfivars *efivars = fu_context_get_efivars(ctx); gsize bufsz = 0; guint64 value = 0; g_autofree guint8 *buf = NULL; if (!fu_efivars_get_data(efivars, FU_EFIVARS_GUID_EFI_GLOBAL, "OsIndicationsSupported", &buf, &bufsz, NULL, error)) { g_prefix_error(error, "failed to read EFI variable: "); return FALSE; } if (!fu_memread_uint64_safe(buf, bufsz, 0x0, &value, G_LITTLE_ENDIAN, error)) return FALSE; if ((value & EFI_OS_INDICATIONS_FILE_CAPSULE_DELIVERY_SUPPORTED) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Capsule-on-Disk is not supported"); return FALSE; } /* no table, nothing to check */ if (self->acpi_uefi == NULL) return TRUE; return fu_acpi_uefi_cod_functional(FU_ACPI_UEFI(self->acpi_uefi), error); } static gboolean fu_uefi_capsule_plugin_bootloader_supports_fwupd(FuContext *ctx) { FuEfivars *efivars = fu_context_get_efivars(ctx); gsize data_sz = 0; g_autofree guint8 *data = NULL; /* if the bootloader supports installing the capsule updates provided by fwupd, it sets this * runtime-accessible UEFI variable with a one-byte value of 1 */ if (!fu_efivars_get_data(efivars, FU_EFIVARS_GUID_FWUPDATE, "BootloaderSupportsFwupd", &data, &data_sz, NULL, NULL)) { return FALSE; } return data_sz == 1 && data[0] == 1; } static gboolean fu_uefi_capsule_plugin_coldplug(FuPlugin *plugin, FuProgress *progress, GError **error) { FuUefiCapsulePlugin *self = FU_UEFI_CAPSULE_PLUGIN(plugin); FuContext *ctx = fu_plugin_get_context(plugin); const gchar *str; gboolean has_fde = FALSE; gboolean bootloader_supports_fwupd = fu_uefi_capsule_plugin_bootloader_supports_fwupd(ctx); g_autoptr(GError) error_fde = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(GPtrArray) devices = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "check-cod"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 8, "check-bitlocker"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 64, "coldplug"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 26, "add-devices"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "setup-bgrt"); /* firmware may lie */ if (!fu_plugin_get_config_value_boolean(plugin, "DisableCapsuleUpdateOnDisk")) { g_autoptr(GError) error_cod = NULL; if (!fu_uefi_capsule_plugin_check_cod_support(self, &error_cod)) { g_debug("not using CapsuleOnDisk support: %s", error_cod->message); } else { fu_uefi_capsule_backend_set_device_gtype( FU_UEFI_CAPSULE_BACKEND(self->backend), FU_TYPE_UEFI_COD_DEVICE); } } fu_progress_step_done(progress); /* warn the user that BitLocker might ask for recovery key after fw update */ if (fu_context_has_flag(ctx, FU_CONTEXT_FLAG_FDE_BITLOCKER)) has_fde = TRUE; fu_progress_step_done(progress); /* add each device */ if (!fu_backend_coldplug(self->backend, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); devices = fu_backend_get_devices(self->backend); for (guint i = 0; i < devices->len; i++) { FuUefiCapsuleDevice *dev = g_ptr_array_index(devices, i); g_autoptr(GError) error_device = NULL; if (!fu_uefi_capsule_plugin_coldplug_device(plugin, dev, &error_device)) { if (g_error_matches(error_device, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_debug("skipping device that failed coldplug: %s", error_device->message); continue; } g_propagate_error(error, g_steal_pointer(&error_device)); return FALSE; } fu_device_add_flag(FU_DEVICE(dev), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(dev), FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE); /* if the bootloader doesn't know how to install capsules provided by fwupd, * enable fwupd-efi */ if (!bootloader_supports_fwupd) { fu_device_add_private_flag(FU_DEVICE(dev), FU_UEFI_CAPSULE_DEVICE_FLAG_USE_FWUPD_EFI); } /* system firmware "BIOS" can change the PCRx registers */ if (fu_uefi_capsule_device_get_kind(dev) == FU_UEFI_CAPSULE_DEVICE_KIND_SYSTEM_FIRMWARE && has_fde) fu_device_add_flag(FU_DEVICE(dev), FWUPD_DEVICE_FLAG_AFFECTS_FDE); /* load all configuration variables */ fu_uefi_capsule_plugin_load_config(plugin, FU_DEVICE(dev)); /* watch in case we set needs-reboot in the engine */ g_signal_connect(FU_DEVICE(dev), "notify::update-state", G_CALLBACK(fu_uefi_capsule_plugin_update_state_notify_cb), plugin); fu_plugin_device_add(plugin, FU_DEVICE(dev)); } fu_progress_step_done(progress); /* for debugging problems later */ fu_uefi_capsule_plugin_test_secure_boot(plugin); if (!fu_uefi_bgrt_setup(self->bgrt, &error_local)) g_debug("BGRT setup failed: %s", error_local->message); str = fu_uefi_bgrt_get_supported(self->bgrt) ? "Enabled" : "Disabled"; g_info("UX capsule support : %s", str); fu_plugin_add_report_metadata(plugin, "UEFIUXCapsule", str); fu_progress_step_done(progress); return TRUE; } static gboolean fu_uefi_capsule_plugin_cleanup_esp(FuUefiCapsulePlugin *self, GError **error) { g_autofree gchar *esp_path = NULL; g_autofree gchar *pattern = NULL; g_autoptr(FuDeviceLocker) esp_locker = NULL; g_autoptr(GPtrArray) files = NULL; if (self->esp == NULL) return TRUE; /* delete any files matching the glob in the ESP */ esp_locker = fu_volume_locker(self->esp, error); if (esp_locker == NULL) return FALSE; esp_path = fu_volume_get_mount_point(self->esp); if (esp_path == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no mountpoint for ESP"); return FALSE; } files = fu_path_get_files(esp_path, error); if (files == NULL) return FALSE; pattern = g_build_filename("*", "fw", "fwupd*.cap", NULL); for (guint i = 0; i < files->len; i++) { const gchar *fn = g_ptr_array_index(files, i); if (g_pattern_match_simple(pattern, fn)) { g_autoptr(GFile) file = g_file_new_for_path(fn); g_debug("deleting %s", fn); if (!g_file_delete(file, NULL, error)) return FALSE; } } /* success */ return TRUE; } static gboolean fu_uefi_capsule_plugin_cleanup_bootnext(FuUefiCapsulePlugin *self, GError **error) { FuContext *ctx = fu_plugin_get_context(FU_PLUGIN(self)); FuEfivars *efivars = fu_context_get_efivars(ctx); guint16 boot_next = 0; g_autofree gchar *loadoptstr = NULL; g_autoptr(FuEfiLoadOption) loadopt = NULL; /* unset */ if (!fu_efivars_exists(efivars, FU_EFIVARS_GUID_EFI_GLOBAL, "BootNext")) return TRUE; /* get the BootXXXX entry for BootNext */ if (!fu_efivars_get_boot_next(efivars, &boot_next, error)) return FALSE; loadopt = fu_efivars_get_boot_entry(efivars, boot_next, error); if (loadopt == NULL) return FALSE; /* is this us? */ loadoptstr = fu_firmware_to_string(FU_FIRMWARE(loadopt)); g_debug("EFI LoadOption: %s", loadoptstr); if (g_strcmp0(fu_firmware_get_id(FU_FIRMWARE(loadopt)), "Linux Firmware Updater") == 0 || g_strcmp0(fu_firmware_get_id(FU_FIRMWARE(loadopt)), "Linux-Firmware-Updater") == 0) { g_debug("BootNext was not deleted automatically, so removing: " "this normally indicates a BIOS bug"); if (!fu_efivars_delete(efivars, FU_EFIVARS_GUID_EFI_GLOBAL, "BootNext", error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_uefi_capsule_plugin_reboot_cleanup(FuPlugin *plugin, FuDevice *device, GError **error) { FuContext *ctx = fu_plugin_get_context(plugin); FuEfivars *efivars = fu_context_get_efivars(ctx); FuUefiCapsulePlugin *self = FU_UEFI_CAPSULE_PLUGIN(plugin); /* provide an escape hatch for debugging */ if (!fu_plugin_get_config_value_boolean(plugin, "RebootCleanup")) return TRUE; /* delete capsules */ if (!fu_uefi_capsule_plugin_cleanup_esp(self, error)) return FALSE; /* delete any old variables */ if (!fu_efivars_delete_with_glob(efivars, FU_EFIVARS_GUID_FWUPDATE, "fwupd*-*", error)) return FALSE; /* this should not be required, but, hey -- here were are */ if (!fu_uefi_capsule_plugin_cleanup_bootnext(self, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_uefi_capsule_plugin_modify_config(FuPlugin *plugin, const gchar *key, const gchar *value, GError **error) { const gchar *keys[] = {"DisableCapsuleUpdateOnDisk", "DisableShimForSecureBoot", "EnableEfiDebugging", "EnableGrubChainLoad", "OverrideESPMountPoint", "RebootCleanup", "RequireESPFreeSpace", "ScreenWidth", "ScreenHeight", NULL}; if (!g_strv_contains(keys, key)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "config key %s not supported", key); return FALSE; } return fu_plugin_set_config_value(plugin, key, value, error); } static gboolean fu_uefi_capsule_plugin_backend_device_added(FuPlugin *plugin, FuDevice *device, FuProgress *progress, GError **error) { FuUefiCapsulePlugin *self = FU_UEFI_CAPSULE_PLUGIN(plugin); /* use this DRM device to get the panel resolution */ if (FU_IS_DRM_DEVICE(device) && self->screen_height == 0 && self->screen_width == 0) { g_autoptr(GError) error_local = NULL; if (!fu_uefi_capsule_plugin_ensure_screen_size_drm(self, FU_DRM_DEVICE(device), &error_local)) g_warning("failed to get screen size: %s", error_local->message); } /* success */ return TRUE; } static void fu_uefi_capsule_plugin_init(FuUefiCapsulePlugin *self) { self->bgrt = fu_uefi_bgrt_new(); fu_plugin_add_flag(FU_PLUGIN(self), FWUPD_PLUGIN_FLAG_MEASURE_SYSTEM_INTEGRITY); } static void fu_uefi_capsule_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); FuUefiCapsulePlugin *self = FU_UEFI_CAPSULE_PLUGIN(plugin); g_autoptr(GError) error_local = NULL; self->backend = fu_uefi_capsule_backend_new(ctx); fu_plugin_add_rule(plugin, FU_PLUGIN_RULE_RUN_AFTER, "upower"); fu_plugin_add_rule(plugin, FU_PLUGIN_RULE_METADATA_SOURCE, "tpm"); fu_plugin_add_rule(plugin, FU_PLUGIN_RULE_METADATA_SOURCE, "dell"); fu_plugin_add_rule(plugin, FU_PLUGIN_RULE_METADATA_SOURCE, "linux_lockdown"); fu_plugin_add_rule(plugin, FU_PLUGIN_RULE_METADATA_SOURCE, "acpi_phat"); fu_plugin_add_rule(plugin, FU_PLUGIN_RULE_CONFLICTS, "uefi"); /* old name */ fu_plugin_add_firmware_gtype(FU_PLUGIN(self), NULL, FU_TYPE_ACPI_UEFI); fu_plugin_add_firmware_gtype(FU_PLUGIN(self), NULL, FU_TYPE_UEFI_UPDATE_INFO); fu_plugin_add_firmware_gtype(FU_PLUGIN(self), NULL, FU_TYPE_BITMAP_IMAGE); fu_plugin_add_device_gtype(FU_PLUGIN(self), FU_TYPE_UEFI_COD_DEVICE); /* coverage */ fu_plugin_add_device_gtype(FU_PLUGIN(self), FU_TYPE_UEFI_GRUB_DEVICE); /* coverage */ fu_plugin_add_device_gtype(FU_PLUGIN(self), FU_TYPE_UEFI_NVRAM_DEVICE); /* coverage */ fu_plugin_add_udev_subsystem(plugin, "drm"); /* defaults changed here will also be reflected in the fwupd.conf man page */ fu_plugin_set_config_default(plugin, "DisableCapsuleUpdateOnDisk", "false"); fu_plugin_set_config_default(plugin, "DisableShimForSecureBoot", "false"); fu_plugin_set_config_default(plugin, "EnableEfiDebugging", "false"); fu_plugin_set_config_default(plugin, "EnableGrubChainLoad", "false"); fu_plugin_set_config_default(plugin, "OverrideESPMountPoint", NULL); fu_plugin_set_config_default(plugin, "RebootCleanup", "true"); fu_plugin_set_config_default(plugin, "RequireESPFreeSpace", "0"); /* in MB */ fu_plugin_set_config_default(plugin, "ScreenWidth", "0"); /* in pixels */ fu_plugin_set_config_default(plugin, "ScreenHeight", "0"); /* in pixels */ /* add a requirement on the fwupd-efi version -- which can change */ if (!fu_uefi_capsule_plugin_fwupd_efi_probe(self, &error_local)) g_debug("failed to get fwupd-efi runtime version: %s", error_local->message); } static void fu_uefi_capsule_plugin_finalize(GObject *obj) { FuUefiCapsulePlugin *self = FU_UEFI_CAPSULE_PLUGIN(obj); if (self->esp != NULL) g_object_unref(self->esp); if (self->fwupd_efi_file != NULL) g_object_unref(self->fwupd_efi_file); if (self->acpi_uefi != NULL) g_object_unref(self->acpi_uefi); if (self->fwupd_efi_monitor != NULL) { g_file_monitor_cancel(self->fwupd_efi_monitor); g_object_unref(self->fwupd_efi_monitor); } if (self->backend != NULL) g_object_unref(self->backend); if (self->bgrt != NULL) g_object_unref(self->bgrt); G_OBJECT_CLASS(fu_uefi_capsule_plugin_parent_class)->finalize(obj); } static void fu_uefi_capsule_plugin_class_init(FuUefiCapsulePluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_uefi_capsule_plugin_finalize; plugin_class->constructed = fu_uefi_capsule_plugin_constructed; plugin_class->to_string = fu_uefi_capsule_plugin_to_string; plugin_class->clear_results = fu_uefi_capsule_plugin_clear_results; plugin_class->add_security_attrs = fu_uefi_capsule_plugin_add_security_attrs; plugin_class->device_registered = fu_uefi_capsule_plugin_device_registered; plugin_class->startup = fu_uefi_capsule_plugin_startup; plugin_class->unlock = fu_uefi_capsule_plugin_unlock; plugin_class->coldplug = fu_uefi_capsule_plugin_coldplug; plugin_class->write_firmware = fu_uefi_capsule_plugin_write_firmware; plugin_class->reboot_cleanup = fu_uefi_capsule_plugin_reboot_cleanup; plugin_class->modify_config = fu_uefi_capsule_plugin_modify_config; plugin_class->backend_device_added = fu_uefi_capsule_plugin_backend_device_added; } fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-capsule-plugin.h000066400000000000000000000005051501337203100234140ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_UEFI_CAPSULE_PLUGIN (fu_uefi_capsule_plugin_get_type()) G_DECLARE_FINAL_TYPE(FuUefiCapsulePlugin, fu_uefi_capsule_plugin, FU, UEFI_CAPSULE_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-cod-device.c000066400000000000000000000217611501337203100224700ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-uefi-cod-device.h" #include "fu-uefi-common.h" struct _FuUefiCodDevice { FuUefiCapsuleDevice parent_instance; }; G_DEFINE_TYPE(FuUefiCodDevice, fu_uefi_cod_device, FU_TYPE_UEFI_CAPSULE_DEVICE) static gboolean fu_uefi_cod_device_get_results_for_idx(FuDevice *device, guint idx, GError **error) { FuContext *ctx = fu_device_get_context(device); FuEfivars *efivars = fu_context_get_efivars(ctx); FuUefiCapsuleDevice *device_uefi = FU_UEFI_CAPSULE_DEVICE(device); fwupd_guid_t guid = {0x0}; gsize bufsz = 0; guint32 status = 0; guint32 total_size = 0; g_autofree gchar *guidstr = NULL; g_autofree gchar *name = NULL; g_autofree guint8 *buf = NULL; /* read out result */ name = g_strdup_printf("Capsule%04u", idx); if (!fu_efivars_get_data(efivars, FU_EFIVARS_GUID_EFI_CAPSULE_REPORT, name, &buf, &bufsz, NULL, error)) { g_prefix_error(error, "failed to read %s: ", name); return FALSE; } /* sanity check */ if (!fu_memread_uint32_safe(buf, bufsz, 0x00, &total_size, G_LITTLE_ENDIAN, error)) return FALSE; if (total_size < 0x3A) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "EFI_CAPSULE_RESULT_VARIABLE_HEADER too small"); return FALSE; } /* verify guid */ if (!fu_memcpy_safe(guid, sizeof(guid), 0x0, /* dst */ buf, bufsz, 0x08, /* src */ sizeof(guid), error)) return FALSE; guidstr = fwupd_guid_to_string(&guid, FWUPD_GUID_FLAG_MIXED_ENDIAN); if (g_strcmp0(guidstr, fu_uefi_capsule_device_get_guid(device_uefi)) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "wrong GUID, expected %s, got %s", fu_uefi_capsule_device_get_guid(device_uefi), guidstr); return FALSE; } /* get status */ if (!fu_memread_uint32_safe(buf, bufsz, 0x28, &status, G_LITTLE_ENDIAN, error)) return FALSE; fu_uefi_capsule_device_set_status(device_uefi, status); return TRUE; } #define VARIABLE_IDX_SIZE 11 /* of CHAR16 */ static gboolean fu_uefi_cod_device_get_variable_idx(FuUefiCodDevice *self, const gchar *name, guint *value, GError **error) { FuContext *ctx = fu_device_get_context(FU_DEVICE(self)); FuEfivars *efivars = fu_context_get_efivars(ctx); guint64 tmp = 0; g_autofree gchar *str = NULL; g_autoptr(GBytes) buf = NULL; /* parse the value */ buf = fu_efivars_get_data_bytes(efivars, FU_EFIVARS_GUID_EFI_CAPSULE_REPORT, name, NULL, error); if (buf == NULL) return FALSE; str = fu_utf16_to_utf8_bytes(buf, G_LITTLE_ENDIAN, error); if (str == NULL) return FALSE; if (!g_str_has_prefix(str, "Capsule")) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "wrong contents, got '%s' for %s", str, name); return FALSE; } if (!fu_strtoull(str + strlen("Capsule"), &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; if (value != NULL) *value = tmp; return TRUE; } static gboolean fu_uefi_cod_device_get_results(FuDevice *device, GError **error) { FuUefiCodDevice *self = FU_UEFI_COD_DEVICE(device); guint capsule_last = 1024; /* tell us where to stop */ if (!fu_uefi_cod_device_get_variable_idx(self, "CapsuleLast", &capsule_last, error)) return FALSE; for (guint i = 0; i <= capsule_last; i++) { g_autoptr(GError) error_local = NULL; if (fu_uefi_cod_device_get_results_for_idx(device, i, &error_local)) return TRUE; if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } /* nothing found */ return TRUE; } static gchar * fu_uefi_cod_device_get_indexed_filename(FuUefiCapsuleDevice *self, GError **error) { g_autofree gchar *esp_path = fu_volume_get_mount_point(fu_uefi_capsule_device_get_esp(self)); for (guint i = 0; i < 0xFFFF; i++) { gboolean exists_cod_path = FALSE; g_autofree gchar *basename = g_strdup_printf("CapsuleUpdateFile%04X.bin", i); g_autofree gchar *cod_path = g_build_filename(esp_path, "EFI", "UpdateCapsule", basename, NULL); if (!fu_device_query_file_exists(FU_DEVICE(self), cod_path, &exists_cod_path, error)) return NULL; if (!exists_cod_path) return g_steal_pointer(&cod_path); } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "all potential CapsuleUpdateFile file names are taken"); return NULL; } static gchar * fu_uefi_cod_device_get_filename(FuUefiCapsuleDevice *self, GError **error) { FuVolume *esp = fu_uefi_capsule_device_get_esp(self); g_autofree gchar *esp_path = NULL; g_autofree gchar *basename = NULL; if (esp == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "no ESP set on device"); return NULL; } esp_path = fu_volume_get_mount_point(esp); /* InsydeH2O */ if (fu_device_has_private_flag(FU_DEVICE(self), FU_UEFI_CAPSULE_DEVICE_FLAG_COD_INDEXED_FILENAME)) return fu_uefi_cod_device_get_indexed_filename(self, error); /* Dell Inc. */ if (fu_device_has_private_flag(FU_DEVICE(self), FU_UEFI_CAPSULE_DEVICE_FLAG_COD_DELL_RECOVERY)) { return g_build_filename(esp_path, "EFI", "dell", "bios", "recovery", "BIOS_TRS.rcv", NULL); } /* fallback */ basename = g_strdup_printf("fwupd-%s.cap", fu_uefi_capsule_device_get_guid(self)); return g_build_filename(esp_path, "EFI", "UpdateCapsule", basename, NULL); } static gboolean fu_uefi_cod_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuContext *ctx = fu_device_get_context(device); FuEfivars *efivars = fu_context_get_efivars(ctx); FuUefiCapsuleDevice *self = FU_UEFI_CAPSULE_DEVICE(device); g_autofree gchar *cod_path = NULL; g_autoptr(GBytes) fixed_fw = NULL; g_autoptr(GBytes) fw = NULL; /* ensure we have the existing state */ if (fu_uefi_capsule_device_get_guid(self) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "cannot update device info with no GUID"); return FALSE; } /* copy the capsule */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; fixed_fw = fu_uefi_capsule_device_fixup_firmware(self, fw, error); if (fixed_fw == NULL) return FALSE; cod_path = fu_uefi_cod_device_get_filename(self, error); if (cod_path == NULL) return FALSE; g_info("using %s", cod_path); if (!fu_path_mkdir_parent(cod_path, error)) return FALSE; if (!fu_bytes_set_contents(cod_path, fixed_fw, error)) return FALSE; /* * NOTE: The EFI spec requires setting OsIndications! * RT->SetVariable is not supported for all hardware, and so when using * U-Boot, it applies the capsule even if OsIndications isn't set. * The capsule is then deleted by U-Boot after it has been deployed. */ if (!fu_device_has_private_flag(device, FU_UEFI_CAPSULE_DEVICE_FLAG_NO_RT_SET_VARIABLE)) { gsize bufsz = 0; guint64 os_indications = 0; g_autofree guint8 *buf = NULL; g_autoptr(GError) error_local = NULL; /* the firmware does not normally populate OsIndications by default */ if (!fu_efivars_get_data(efivars, FU_EFIVARS_GUID_EFI_GLOBAL, "OsIndications", &buf, &bufsz, NULL, &error_local)) { g_debug("failed to read EFI variable: %s", error_local->message); } else { if (!fu_memread_uint64_safe(buf, bufsz, 0x0, &os_indications, G_LITTLE_ENDIAN, error)) return FALSE; } os_indications |= EFI_OS_INDICATIONS_FILE_CAPSULE_DELIVERY_SUPPORTED; if (!fu_efivars_set_data(efivars, FU_EFIVARS_GUID_EFI_GLOBAL, "OsIndications", (guint8 *)&os_indications, sizeof(os_indications), FU_EFIVARS_ATTR_NON_VOLATILE | FU_EFIVARS_ATTR_BOOTSERVICE_ACCESS | FU_EFIVARS_ATTR_RUNTIME_ACCESS, error)) { g_prefix_error(error, "Could not set OsIndications: "); return FALSE; } } /* success */ return TRUE; } static void fu_uefi_cod_device_report_metadata_pre(FuDevice *device, GHashTable *metadata) { /* FuUefiCapsuleDevice */ FU_DEVICE_CLASS(fu_uefi_cod_device_parent_class)->report_metadata_pre(device, metadata); g_hash_table_insert(metadata, g_strdup("CapsuleApplyMethod"), g_strdup("cod")); } static void fu_uefi_cod_device_init(FuUefiCodDevice *self) { fu_device_add_private_flag(FU_DEVICE(self), FU_UEFI_CAPSULE_DEVICE_FLAG_NO_UX_CAPSULE); fu_device_set_summary(FU_DEVICE(self), "UEFI System Resource Table device (Updated via capsule-on-disk)"); } static void fu_uefi_cod_device_class_init(FuUefiCodDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_uefi_cod_device_write_firmware; device_class->get_results = fu_uefi_cod_device_get_results; device_class->report_metadata_pre = fu_uefi_cod_device_report_metadata_pre; } fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-cod-device.h000066400000000000000000000005411501337203100224660ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-uefi-capsule-device.h" #define FU_TYPE_UEFI_COD_DEVICE (fu_uefi_cod_device_get_type()) G_DECLARE_FINAL_TYPE(FuUefiCodDevice, fu_uefi_cod_device, FU, UEFI_COD_DEVICE, FuUefiCapsuleDevice) fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-common.c000066400000000000000000000222161501337203100217520ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2015 Peter Jones * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uefi-capsule-device.h" #include "fu-uefi-common.h" static const gchar * fu_uefi_bootmgr_get_suffix(GError **error) { guint64 firmware_bits; struct { guint64 bits; const gchar *arch; } suffixes[] = { #if defined(__x86_64__) {64, "x64"}, #elif defined(__aarch64__) {64, "aa64"}, #elif defined(__loongarch_lp64) {64, "loongarch64"}, #elif (defined(__riscv) && __riscv_xlen == 64) {64, "riscv64"}, #endif #if defined(__i386__) || defined(__i686__) {32, "ia32"}, #elif defined(__arm__) {32, "arm"}, #endif {0, NULL}}; g_autofree gchar *sysfsfwdir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR_FW); g_autofree gchar *sysfsefidir = g_build_filename(sysfsfwdir, "efi", NULL); firmware_bits = fu_uefi_read_file_as_uint64(sysfsefidir, "fw_platform_size"); if (firmware_bits == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "%s/fw_platform_size cannot be found", sysfsefidir); return NULL; } for (guint i = 0; suffixes[i].arch != NULL; i++) { if (firmware_bits != suffixes[i].bits) continue; return suffixes[i].arch; } /* this should exist */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "%s/fw_platform_size has unknown value %" G_GUINT64_FORMAT, sysfsefidir, firmware_bits); return NULL; } /* return without the ESP dir prepended */ gchar * fu_uefi_get_esp_app_path(const gchar *esp_path, const gchar *cmd, GError **error) { const gchar *suffix = fu_uefi_bootmgr_get_suffix(error); g_autofree gchar *base = NULL; if (suffix == NULL) return NULL; base = fu_uefi_get_esp_path_for_os(esp_path); return g_strdup_printf("%s/%s%s.efi", base, cmd, suffix); } /** * fu_uefi_get_built_app_path: * @basename: the prefix for the binary * @error: (nullable): optional return location for an error * * Gets the path intended to be used for an EFI binary on the local system. * The binary is matched against the correct architecture and if secure * boot is enabled. * * Returns: The full path to the binary, or %NULL if not found * * Since: 1.8.1 **/ gchar * fu_uefi_get_built_app_path(FuEfivars *efivars, const gchar *binary, GError **error) { const gchar *suffix; g_autofree gchar *prefix = NULL; g_autofree gchar *source_path = NULL; g_autofree gchar *source_path_signed = NULL; gboolean secureboot_enabled = FALSE; gboolean source_path_exists = FALSE; gboolean source_path_signed_exists = FALSE; suffix = fu_uefi_bootmgr_get_suffix(error); if (suffix == NULL) return NULL; prefix = fu_path_from_kind(FU_PATH_KIND_EFIAPPDIR); source_path = g_strdup_printf("%s/%s%s.efi", prefix, binary, suffix); source_path_signed = g_strdup_printf("%s.signed", source_path); source_path_exists = g_file_test(source_path, G_FILE_TEST_EXISTS); source_path_signed_exists = g_file_test(source_path_signed, G_FILE_TEST_EXISTS); if (!fu_efivars_get_secure_boot(efivars, &secureboot_enabled, error)) return NULL; if (secureboot_enabled) { if (!source_path_signed_exists) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "%s cannot be found", source_path_signed); return NULL; } return g_steal_pointer(&source_path_signed); } if (source_path_exists) return g_steal_pointer(&source_path); if (source_path_signed_exists) return g_steal_pointer(&source_path_signed); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "%s and %s cannot be found", source_path, source_path_signed); return NULL; } gboolean fu_uefi_get_framebuffer_size(guint32 *width, guint32 *height, GError **error) { guint32 height_tmp; guint32 width_tmp; g_autofree gchar *sysfsdriverdir = NULL; g_autofree gchar *fbdir = NULL; sysfsdriverdir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR_DRIVERS); fbdir = g_build_filename(sysfsdriverdir, "efi-framebuffer", "efi-framebuffer.0", NULL); if (!g_file_test(fbdir, G_FILE_TEST_EXISTS)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "EFI framebuffer not found"); return FALSE; } height_tmp = fu_uefi_read_file_as_uint64(fbdir, "height"); width_tmp = fu_uefi_read_file_as_uint64(fbdir, "width"); if (width_tmp == 0 || height_tmp == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "EFI framebuffer has invalid size " "%" G_GUINT32_FORMAT "x%" G_GUINT32_FORMAT, width_tmp, height_tmp); return FALSE; } if (width != NULL) *width = width_tmp; if (height != NULL) *height = height_tmp; return TRUE; } /** * fu_uefi_get_esp_path_for_os: * @esp_base: The base path of the EFI System Partition (ESP). * * Retrieves the directory structure of the EFI System Partition (ESP) for * the operating system. * * This function constructs and returns the path of the directory to use * within the ESP based on the provided base path. * * Returns: (transfer full): A newly allocated string containing the directory * structure within the ESP for the operating system. The caller is * responsible for freeing the returned string. */ gchar * fu_uefi_get_esp_path_for_os(const gchar *esp_base) { #ifndef EFI_OS_DIR g_autofree gchar *os_release_id = NULL; g_autofree gchar *id_like = NULL; g_autofree gchar *esp_path = NULL; g_autofree gchar *full_path = NULL; g_autofree gchar *systemd_path = NULL; g_autofree gchar *full_systemd_path = NULL; /* distro (or user) is using systemd-boot */ systemd_path = g_build_filename("EFI", "systemd", NULL); full_systemd_path = g_build_filename(esp_base, systemd_path, NULL); if (g_file_test(full_systemd_path, G_FILE_TEST_IS_DIR)) return g_steal_pointer(&systemd_path); /* try to lookup /etc/os-release ID key */ os_release_id = g_get_os_info(G_OS_INFO_KEY_ID); if (os_release_id == NULL) os_release_id = g_strdup("unknown"); /* if ID key points at something existing return it */ esp_path = g_build_filename("EFI", os_release_id, NULL); full_path = g_build_filename(esp_base, esp_path, NULL); if (g_file_test(full_path, G_FILE_TEST_IS_DIR)) return g_steal_pointer(&esp_path); /* if ID key doesn't exist, try ID_LIKE */ id_like = g_get_os_info("ID_LIKE"); if (id_like != NULL) { g_auto(GStrv) split = g_strsplit(id_like, " ", -1); for (guint i = 0; split[i] != NULL; i++) { g_autofree gchar *id_like_path = g_build_filename("EFI", split[i], NULL); g_autofree gchar *id_like_full_path = g_build_filename(esp_base, id_like_path, NULL); if (!g_file_test(id_like_full_path, G_FILE_TEST_IS_DIR)) continue; g_debug("using ID_LIKE key from os-release"); return g_steal_pointer(&id_like_path); } } return g_steal_pointer(&esp_path); #else return g_build_filename("EFI", EFI_OS_DIR, NULL); #endif } guint64 fu_uefi_read_file_as_uint64(const gchar *path, const gchar *attr_name) { guint64 tmp = 0; g_autofree gchar *data = NULL; g_autofree gchar *fn = g_build_filename(path, attr_name, NULL); g_autoptr(GError) error_local = NULL; if (!g_file_get_contents(fn, &data, NULL, NULL)) return 0x0; if (!fu_strtoull(data, &tmp, 0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, &error_local)) { g_warning("invalid string specified: %s", error_local->message); return G_MAXUINT64; } return tmp; } gboolean fu_uefi_esp_target_verify(const gchar *source_fn, FuVolume *esp, const gchar *target_no_mountpoint) { gsize len = 0; g_autofree gchar *source_csum = NULL; g_autofree gchar *source_data = NULL; g_autofree gchar *target_csum = NULL; g_autofree gchar *target_data = NULL; g_autofree gchar *esp_path = fu_volume_get_mount_point(esp); g_autofree gchar *target_fn = g_build_filename(esp_path, target_no_mountpoint, NULL); /* nothing in target yet */ if (!g_file_test(target_fn, G_FILE_TEST_EXISTS)) return FALSE; /* test if the file needs to be updated */ if (!g_file_get_contents(source_fn, &source_data, &len, NULL)) return FALSE; source_csum = g_compute_checksum_for_data(G_CHECKSUM_SHA256, (guchar *)source_data, len); if (!g_file_get_contents(target_fn, &target_data, &len, NULL)) return FALSE; target_csum = g_compute_checksum_for_data(G_CHECKSUM_SHA256, (guchar *)target_data, len); return g_strcmp0(target_csum, source_csum) == 0; } gboolean fu_uefi_esp_target_exists(FuVolume *esp, const gchar *target_no_mountpoint) { g_autofree gchar *esp_path = fu_volume_get_mount_point(esp); g_autofree gchar *target_fn = g_build_filename(esp_path, target_no_mountpoint, NULL); return g_file_test(target_fn, G_FILE_TEST_EXISTS); } gboolean fu_uefi_esp_target_copy(const gchar *source_fn, FuVolume *esp, const gchar *target_no_mountpoint, GError **error) { g_autofree gchar *esp_path = fu_volume_get_mount_point(esp); g_autofree gchar *target_fn = g_build_filename(esp_path, target_no_mountpoint, NULL); g_autoptr(GFile) source_file = g_file_new_for_path(source_fn); g_autoptr(GFile) target_file = g_file_new_for_path(target_fn); if (!g_file_copy(source_file, target_file, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, error)) { g_prefix_error(error, "Failed to copy %s to %s: ", source_fn, target_fn); return FALSE; } return TRUE; } fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-common.h000066400000000000000000000023721501337203100217600ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2015 Peter Jones * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #ifdef HAVE_EFI_TIME_T #include #endif #define EFI_CAPSULE_HEADER_FLAGS_PERSIST_ACROSS_RESET 0x00010000 #define EFI_CAPSULE_HEADER_FLAGS_POPULATE_SYSTEM_TABLE 0x00020000 #define EFI_CAPSULE_HEADER_FLAGS_INITIATE_RESET 0x00040000 #define EFI_OS_INDICATIONS_FILE_CAPSULE_DELIVERY_SUPPORTED 0x0000000000000004ULL gchar * fu_uefi_get_esp_app_path(const gchar *base, const gchar *cmd, GError **error); gchar * fu_uefi_get_built_app_path(FuEfivars *efivars, const gchar *binary, GError **error); gboolean fu_uefi_get_framebuffer_size(guint32 *width, guint32 *height, GError **error); gchar * fu_uefi_get_esp_path_for_os(const gchar *base); guint64 fu_uefi_read_file_as_uint64(const gchar *path, const gchar *attr_name); gboolean fu_uefi_esp_target_exists(FuVolume *esp, const gchar *target_no_mountpoint); gboolean fu_uefi_esp_target_verify(const gchar *source_fn, FuVolume *esp, const gchar *target_no_mountpoint); gboolean fu_uefi_esp_target_copy(const gchar *source_fn, FuVolume *esp, const gchar *target_no_mountpoint, GError **error); fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-grub-device.c000066400000000000000000000164361501337203100226650ustar00rootroot00000000000000/* * Copyright 2021 Mario Limonciello * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uefi-common.h" #include "fu-uefi-grub-device.h" struct _FuUefiGrubDevice { FuUefiCapsuleDevice parent_instance; }; G_DEFINE_TYPE(FuUefiGrubDevice, fu_uefi_grub_device, FU_TYPE_UEFI_CAPSULE_DEVICE) static gboolean fu_uefi_grub_device_mkconfig(FuDevice *device, const gchar *esp_path, const gchar *target_app, GError **error) { g_autofree gchar *bootdir = fu_path_from_kind(FU_PATH_KIND_HOSTFS_BOOT); g_autofree gchar *fn_grub_cfg = NULL; const gchar *argv_mkconfig[] = {"", "-o", "grub.cfg", NULL}; const gchar *argv_reboot[] = {"", "fwupd", NULL}; gboolean exists_mkconfig = FALSE; g_autofree gchar *grub_mkconfig = NULL; g_autofree gchar *grub_reboot = NULL; g_autofree gchar *grub_target = NULL; g_autofree gchar *localstatedir = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR_PKG); g_autofree gchar *output = NULL; g_autoptr(GString) str = g_string_new(NULL); /* find grub.conf */ fn_grub_cfg = g_build_filename(bootdir, "grub", "grub.cfg", NULL); if (!fu_device_query_file_exists(FU_DEVICE(device), fn_grub_cfg, &exists_mkconfig, error)) return FALSE; /* try harder */ if (!exists_mkconfig) { g_free(fn_grub_cfg); fn_grub_cfg = g_build_filename(bootdir, "grub2", "grub.cfg", NULL); } if (!fu_device_query_file_exists(device, fn_grub_cfg, &exists_mkconfig, error)) return FALSE; if (!exists_mkconfig) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "could not find grub.conf"); return FALSE; } /* find grub-mkconfig */ grub_mkconfig = fu_path_find_program("grub-mkconfig", NULL); if (grub_mkconfig == NULL) grub_mkconfig = fu_path_find_program("grub2-mkconfig", NULL); if (grub_mkconfig == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "could not find grub-mkconfig"); return FALSE; } /* find grub-reboot */ grub_reboot = fu_path_find_program("grub-reboot", NULL); if (grub_reboot == NULL) grub_reboot = fu_path_find_program("grub2-reboot", NULL); if (grub_reboot == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "could not find grub-reboot"); return FALSE; } /* replace ESP info in conf with what we detected */ g_string_append_printf(str, "EFI_PATH=%s\n", target_app); g_string_replace(str, esp_path, "", 0); g_string_append_printf(str, "ESP=%s\n", esp_path); grub_target = g_build_filename(localstatedir, "uefi_capsule.conf", NULL); if (!g_file_set_contents(grub_target, str->str, -1, error)) return FALSE; /* refresh GRUB configuration */ argv_mkconfig[0] = grub_mkconfig; argv_mkconfig[2] = fn_grub_cfg; if (!g_spawn_sync(NULL, (gchar **)argv_mkconfig, NULL, G_SPAWN_DEFAULT, NULL, NULL, &output, NULL, NULL, error)) return FALSE; g_debug("%s", output); /* skip for self tests */ if (g_getenv("FWUPD_UEFI_TEST") != NULL) return TRUE; /* make fwupd default */ argv_reboot[0] = grub_reboot; return g_spawn_sync(NULL, (gchar **)argv_reboot, NULL, G_SPAWN_DEFAULT, NULL, NULL, NULL, NULL, NULL, error); } static gboolean fu_uefi_grub_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuContext *ctx = fu_device_get_context(device); FuEfivars *efivars = fu_context_get_efivars(ctx); FuUefiCapsuleDevice *self = FU_UEFI_CAPSULE_DEVICE(device); FuVolume *esp = fu_uefi_capsule_device_get_esp(self); const gchar *fw_class = fu_uefi_capsule_device_get_guid(self); g_autofree gchar *basename = NULL; g_autofree gchar *capsule_path = NULL; g_autofree gchar *directory = NULL; g_autofree gchar *esp_path = fu_volume_get_mount_point(esp); g_autofree gchar *fn = NULL; g_autofree gchar *source_app = NULL; g_autofree gchar *target_app = NULL; g_autofree gchar *varname = fu_uefi_capsule_device_build_varname(self); g_autoptr(GBytes) fixed_fw = NULL; g_autoptr(GBytes) fw = NULL; /* ensure we have the existing state */ if (fw_class == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "cannot update device info with no GUID"); return FALSE; } /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* save the blob to the ESP */ directory = fu_uefi_get_esp_path_for_os(esp_path); basename = g_strdup_printf("fwupd-%s.cap", fw_class); capsule_path = g_build_filename(directory, "fw", basename, NULL); fn = g_build_filename(esp_path, capsule_path, NULL); if (!fu_path_mkdir_parent(fn, error)) return FALSE; fixed_fw = fu_uefi_capsule_device_fixup_firmware(self, fw, error); if (fixed_fw == NULL) return FALSE; if (!fu_bytes_set_contents(fn, fixed_fw, error)) return FALSE; /* enable debugging in the EFI binary */ if (!fu_uefi_capsule_device_perhaps_enable_debugging(self, error)) return FALSE; /* delete the old log to save space */ if (fu_efivars_exists(efivars, FU_EFIVARS_GUID_FWUPDATE, "FWUPDATE_DEBUG_LOG")) { if (!fu_efivars_delete(efivars, FU_EFIVARS_GUID_FWUPDATE, "FWUPDATE_DEBUG_LOG", error)) return FALSE; } /* set the blob header shared with fwupd.efi */ if (!fu_uefi_capsule_device_write_update_info(self, capsule_path, varname, fw_class, error)) return FALSE; /* if secure boot was turned on this might need to be installed separately */ source_app = fu_uefi_get_built_app_path(efivars, "fwupd", error); if (source_app == NULL) return FALSE; /* test if correct asset in place */ target_app = fu_uefi_get_esp_app_path(esp_path, "fwupd", error); if (target_app == NULL) return FALSE; if (!fu_uefi_esp_target_verify(source_app, esp, target_app)) { if (!fu_uefi_esp_target_copy(source_app, esp, target_app, error)) return FALSE; } /* we are using GRUB instead of NVRAM variables */ return fu_uefi_grub_device_mkconfig(device, esp_path, target_app, error); } static void fu_uefi_grub_device_report_metadata_pre(FuDevice *device, GHashTable *metadata) { /* FuUefiCapsuleDevice */ FU_DEVICE_CLASS(fu_uefi_grub_device_parent_class)->report_metadata_pre(device, metadata); g_hash_table_insert(metadata, g_strdup("CapsuleApplyMethod"), g_strdup("grub")); } static gboolean fu_uefi_grub_device_prepare(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { /* FuUefiCapsuleDevice->prepare */ if (!FU_DEVICE_CLASS(fu_uefi_grub_device_parent_class) ->prepare(device, progress, flags, error)) return FALSE; /* sanity checks */ return fu_uefi_capsule_device_check_asset(FU_UEFI_CAPSULE_DEVICE(device), error); } static void fu_uefi_grub_device_init(FuUefiGrubDevice *self) { fu_device_set_summary(FU_DEVICE(self), "UEFI System Resource Table device (updated via grub)"); } static void fu_uefi_grub_device_class_init(FuUefiGrubDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_uefi_grub_device_write_firmware; device_class->report_metadata_pre = fu_uefi_grub_device_report_metadata_pre; device_class->prepare = fu_uefi_grub_device_prepare; } fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-grub-device.h000066400000000000000000000006021501337203100226560ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-uefi-capsule-device.h" #define FU_TYPE_UEFI_GRUB_DEVICE (fu_uefi_grub_device_get_type()) G_DECLARE_FINAL_TYPE(FuUefiGrubDevice, fu_uefi_grub_device, FU, UEFI_GRUB_DEVICE, FuUefiCapsuleDevice) fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-nvram-device.c000066400000000000000000000137401501337203100230440ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2018 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uefi-bootmgr.h" #include "fu-uefi-common.h" #include "fu-uefi-nvram-device.h" struct _FuUefiNvramDevice { FuUefiCapsuleDevice parent_instance; }; G_DEFINE_TYPE(FuUefiNvramDevice, fu_uefi_nvram_device, FU_TYPE_UEFI_CAPSULE_DEVICE) static gboolean fu_uefi_nvram_device_get_results(FuDevice *device, GError **error) { FuContext *ctx = fu_device_get_context(device); FuEfivars *efivars = fu_context_get_efivars(ctx); g_autoptr(GError) error_local = NULL; /* check if something rudely removed our BOOTXXXX entry */ if (fu_device_has_private_flag(device, FU_UEFI_CAPSULE_DEVICE_FLAG_USE_FWUPD_EFI) && !fu_uefi_bootmgr_verify_fwupd(efivars, &error_local)) { if (fu_device_has_private_flag( device, FU_UEFI_CAPSULE_DEVICE_FLAG_SUPPORTS_BOOT_ORDER_LOCK)) { g_prefix_error(&error_local, "boot entry missing; " "perhaps 'Boot Order Lock' enabled in the BIOS: "); fu_device_set_update_state(device, FWUPD_UPDATE_STATE_FAILED_TRANSIENT); } else { g_prefix_error(&error_local, "boot entry missing: "); fu_device_set_update_state(device, FWUPD_UPDATE_STATE_FAILED); } fu_device_set_update_error(device, error_local->message); return TRUE; } /* parent */ return FU_DEVICE_CLASS(fu_uefi_nvram_device_parent_class)->get_results(device, error); } static gboolean fu_uefi_nvram_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuContext *ctx = fu_device_get_context(device); FuEfivars *efivars = fu_context_get_efivars(ctx); FuUefiCapsuleDevice *self = FU_UEFI_CAPSULE_DEVICE(device); FuUefiBootmgrFlags bootmgr_flags = FU_UEFI_BOOTMGR_FLAG_NONE; const gchar *bootmgr_desc = "Linux Firmware Updater"; const gchar *fw_class = fu_uefi_capsule_device_get_guid(self); FuVolume *esp = fu_uefi_capsule_device_get_esp(self); g_autofree gchar *esp_path = fu_volume_get_mount_point(esp); g_autoptr(GBytes) fixed_fw = NULL; g_autoptr(GBytes) fw = NULL; g_autofree gchar *capsule_path = NULL; g_autofree gchar *basename = NULL; g_autofree gchar *directory = NULL; g_autofree gchar *fn = NULL; g_autofree gchar *varname = fu_uefi_capsule_device_build_varname(self); /* ensure we have the existing state */ if (fw_class == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "cannot update device info with no GUID"); return FALSE; } /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* save the blob to the ESP */ directory = fu_uefi_get_esp_path_for_os(esp_path); basename = g_strdup_printf("fwupd-%s.cap", fw_class); capsule_path = g_build_filename(directory, "fw", basename, NULL); fn = g_build_filename(esp_path, capsule_path, NULL); if (!fu_path_mkdir_parent(fn, error)) return FALSE; fixed_fw = fu_uefi_capsule_device_fixup_firmware(self, fw, error); if (fixed_fw == NULL) return FALSE; if (!fu_bytes_set_contents(fn, fixed_fw, error)) return FALSE; /* enable debugging in the EFI binary */ if (!fu_uefi_capsule_device_perhaps_enable_debugging(self, error)) return FALSE; /* delete the old log to save space */ if (fu_efivars_exists(efivars, FU_EFIVARS_GUID_FWUPDATE, "FWUPDATE_DEBUG_LOG")) { if (!fu_efivars_delete(efivars, FU_EFIVARS_GUID_FWUPDATE, "FWUPDATE_DEBUG_LOG", error)) return FALSE; } /* set the blob header shared with fwupd.efi */ if (!fu_uefi_capsule_device_write_update_info(self, capsule_path, varname, fw_class, error)) return FALSE; /* if fwupd-efi is not in use, no need to change boot order or copy bootloader files */ if (!fu_device_has_private_flag(device, FU_UEFI_CAPSULE_DEVICE_FLAG_USE_FWUPD_EFI)) return TRUE; /* update the firmware before the bootloader runs */ if (fu_device_has_private_flag(device, FU_UEFI_CAPSULE_DEVICE_FLAG_USE_SHIM_FOR_SB)) bootmgr_flags |= FU_UEFI_BOOTMGR_FLAG_USE_SHIM_FOR_SB; if (fu_device_has_private_flag(device, FU_UEFI_CAPSULE_DEVICE_FLAG_USE_SHIM_UNIQUE)) bootmgr_flags |= FU_UEFI_BOOTMGR_FLAG_USE_SHIM_UNIQUE; if (fu_device_has_private_flag(device, FU_UEFI_CAPSULE_DEVICE_FLAG_MODIFY_BOOTORDER)) bootmgr_flags |= FU_UEFI_BOOTMGR_FLAG_MODIFY_BOOTORDER; /* some legacy devices use the old name to deduplicate boot entries */ if (fu_device_has_private_flag(device, FU_UEFI_CAPSULE_DEVICE_FLAG_USE_LEGACY_BOOTMGR_DESC)) bootmgr_desc = "Linux-Firmware-Updater"; if (!fu_uefi_bootmgr_bootnext(efivars, esp, bootmgr_desc, bootmgr_flags, error)) return FALSE; /* success! */ return TRUE; } static void fu_uefi_nvram_device_report_metadata_pre(FuDevice *device, GHashTable *metadata) { /* FuUefiCapsuleDevice */ FU_DEVICE_CLASS(fu_uefi_nvram_device_parent_class)->report_metadata_pre(device, metadata); g_hash_table_insert(metadata, g_strdup("CapsuleApplyMethod"), g_strdup("nvram")); } static gboolean fu_uefi_nvram_device_prepare(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { /* FuUefiCapsuleDevice->prepare */ if (!FU_DEVICE_CLASS(fu_uefi_nvram_device_parent_class) ->prepare(device, progress, flags, error)) return FALSE; /* sanity checks */ return fu_uefi_capsule_device_check_asset(FU_UEFI_CAPSULE_DEVICE(device), error); } static void fu_uefi_nvram_device_init(FuUefiNvramDevice *self) { fu_device_set_summary(FU_DEVICE(self), "UEFI System Resource Table device (updated via NVRAM)"); } static void fu_uefi_nvram_device_class_init(FuUefiNvramDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->get_results = fu_uefi_nvram_device_get_results; device_class->write_firmware = fu_uefi_nvram_device_write_firmware; device_class->report_metadata_pre = fu_uefi_nvram_device_report_metadata_pre; device_class->prepare = fu_uefi_nvram_device_prepare; } fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-nvram-device.h000066400000000000000000000006071501337203100230470ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-uefi-capsule-device.h" #define FU_TYPE_UEFI_NVRAM_DEVICE (fu_uefi_nvram_device_get_type()) G_DECLARE_FINAL_TYPE(FuUefiNvramDevice, fu_uefi_nvram_device, FU, UEFI_NVRAM_DEVICE, FuUefiCapsuleDevice) fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-update-info.c000066400000000000000000000164211501337203100226760ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uefi-common.h" #include "fu-uefi-update-info.h" #define EFIDP_MEDIA_TYPE 0x04 #define EFIDP_MEDIA_FILE 0x4 struct _FuUefiUpdateInfo { FuFirmware parent_instance; gchar *guid; gchar *capsule_fn; guint32 capsule_flags; guint64 hw_inst; FuUefiUpdateInfoStatus status; }; G_DEFINE_TYPE(FuUefiUpdateInfo, fu_uefi_update_info, FU_TYPE_FIRMWARE) static void fu_uefi_update_info_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuUefiUpdateInfo *self = FU_UEFI_UPDATE_INFO(firmware); fu_xmlb_builder_insert_kv(bn, "guid", self->guid); fu_xmlb_builder_insert_kv(bn, "capsule_fn", self->capsule_fn); fu_xmlb_builder_insert_kx(bn, "capsule_flags", self->capsule_flags); fu_xmlb_builder_insert_kx(bn, "hw_inst", self->hw_inst); fu_xmlb_builder_insert_kv(bn, "status", fu_uefi_update_info_status_to_string(self->status)); } void fu_uefi_update_info_set_guid(FuUefiUpdateInfo *self, const gchar *guid) { g_free(self->guid); self->guid = g_strdup(guid); } void fu_uefi_update_info_set_capsule_fn(FuUefiUpdateInfo *self, const gchar *capsule_fn) { g_free(self->capsule_fn); self->capsule_fn = g_strdup(capsule_fn); } void fu_uefi_update_info_set_capsule_flags(FuUefiUpdateInfo *self, guint32 capsule_flags) { self->capsule_flags = capsule_flags; } void fu_uefi_update_info_set_hw_inst(FuUefiUpdateInfo *self, guint64 hw_inst) { self->hw_inst = hw_inst; } void fu_uefi_update_info_set_status(FuUefiUpdateInfo *self, FuUefiUpdateInfoStatus status) { self->status = status; } static gboolean fu_uefi_update_info_build(FuFirmware *firmware, XbNode *n, GError **error) { FuUefiUpdateInfo *self = FU_UEFI_UPDATE_INFO(firmware); const gchar *tmp; tmp = xb_node_query_text(n, "guid", NULL); if (tmp != NULL) fu_uefi_update_info_set_guid(self, tmp); tmp = xb_node_query_text(n, "capsule_fn", NULL); if (tmp != NULL) fu_uefi_update_info_set_capsule_fn(self, tmp); tmp = xb_node_query_text(n, "capsule_flags", NULL); if (tmp != NULL) { guint64 tmp64 = 0; if (!fu_strtoull(tmp, &tmp64, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_uefi_update_info_set_capsule_flags(self, tmp64); } tmp = xb_node_query_text(n, "hw_inst", NULL); if (tmp != NULL) { guint64 tmp64 = 0; if (!fu_strtoull(tmp, &tmp64, 0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, error)) return FALSE; fu_uefi_update_info_set_hw_inst(self, tmp64); } tmp = xb_node_query_text(n, "status", NULL); if (tmp != NULL) { self->status = fu_uefi_update_info_status_from_string(tmp); if (self->status == FU_UEFI_UPDATE_INFO_STATUS_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "status %s not supported", tmp); return FALSE; } } /* success */ return TRUE; } static GByteArray * fu_uefi_update_info_write(FuFirmware *firmware, GError **error) { FuUefiUpdateInfo *self = FU_UEFI_UPDATE_INFO(firmware); fwupd_guid_t guid = {0}; g_autoptr(FuStructEfiUpdateInfo) st = fu_struct_efi_update_info_new(); if (self->guid != NULL) { if (!fwupd_guid_from_string(self->guid, &guid, FWUPD_GUID_FLAG_MIXED_ENDIAN, error)) return NULL; } fu_struct_efi_update_info_set_guid(st, &guid); fu_struct_efi_update_info_set_flags(st, self->capsule_flags); fu_struct_efi_update_info_set_hw_inst(st, self->hw_inst); fu_struct_efi_update_info_set_status(st, self->status); if (self->capsule_fn != NULL) { g_autoptr(GBytes) dpbuf = NULL; g_autoptr(FuEfiDevicePathList) dp_list = fu_efi_device_path_list_new(); g_autoptr(FuEfiFilePathDevicePath) dp_fp = fu_efi_file_path_device_path_new(); if (!fu_efi_file_path_device_path_set_name(dp_fp, self->capsule_fn, error)) return NULL; fu_firmware_add_image(FU_FIRMWARE(dp_list), FU_FIRMWARE(dp_fp)); dpbuf = fu_firmware_write(FU_FIRMWARE(dp_list), error); if (dpbuf == NULL) return NULL; fu_byte_array_append_bytes(st, dpbuf); } /* success */ return g_steal_pointer(&st); } static gboolean fu_uefi_update_info_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuUefiUpdateInfo *self = FU_UEFI_UPDATE_INFO(firmware); gsize streamsz = 0; g_autoptr(GByteArray) st_inf = NULL; g_return_val_if_fail((self), FALSE); st_inf = fu_struct_efi_update_info_parse_stream(stream, 0x0, error); if (st_inf == NULL) { g_prefix_error(error, "EFI variable is corrupt: "); return FALSE; } fu_firmware_set_version_raw(firmware, fu_struct_efi_update_info_get_version(st_inf)); self->capsule_flags = fu_struct_efi_update_info_get_flags(st_inf); self->hw_inst = fu_struct_efi_update_info_get_hw_inst(st_inf); self->status = fu_struct_efi_update_info_get_status(st_inf); self->guid = fwupd_guid_to_string(fu_struct_efi_update_info_get_guid(st_inf), FWUPD_GUID_FLAG_MIXED_ENDIAN); if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; if (streamsz > FU_STRUCT_EFI_UPDATE_INFO_SIZE) { g_autoptr(FuFirmware) dp = NULL; g_autoptr(FuEfiDevicePathList) dpbuf = fu_efi_device_path_list_new(); if (!fu_firmware_parse_stream(FU_FIRMWARE(dpbuf), stream, FU_STRUCT_EFI_UPDATE_INFO_SIZE, flags, error)) { g_prefix_error(error, "failed to parse dpbuf: "); return FALSE; } dp = fu_firmware_get_image_by_gtype(FU_FIRMWARE(dpbuf), FU_TYPE_EFI_FILE_PATH_DEVICE_PATH, error); if (dp == NULL) return FALSE; self->capsule_fn = fu_efi_file_path_device_path_get_name(FU_EFI_FILE_PATH_DEVICE_PATH(dp), error); if (self->capsule_fn == NULL) return FALSE; } return TRUE; } const gchar * fu_uefi_update_info_get_guid(FuUefiUpdateInfo *self) { g_return_val_if_fail(FU_IS_UEFI_UPDATE_INFO(self), NULL); return self->guid; } const gchar * fu_uefi_update_info_get_capsule_fn(FuUefiUpdateInfo *self) { g_return_val_if_fail(FU_IS_UEFI_UPDATE_INFO(self), NULL); return self->capsule_fn; } guint32 fu_uefi_update_info_get_capsule_flags(FuUefiUpdateInfo *self) { g_return_val_if_fail(FU_IS_UEFI_UPDATE_INFO(self), 0); return self->capsule_flags; } guint64 fu_uefi_update_info_get_hw_inst(FuUefiUpdateInfo *self) { g_return_val_if_fail(FU_IS_UEFI_UPDATE_INFO(self), 0); return self->hw_inst; } FuUefiUpdateInfoStatus fu_uefi_update_info_get_status(FuUefiUpdateInfo *self) { g_return_val_if_fail(FU_IS_UEFI_UPDATE_INFO(self), 0); return self->status; } static void fu_uefi_update_info_finalize(GObject *object) { FuUefiUpdateInfo *self = FU_UEFI_UPDATE_INFO(object); g_free(self->guid); g_free(self->capsule_fn); G_OBJECT_CLASS(fu_uefi_update_info_parent_class)->finalize(object); } static void fu_uefi_update_info_class_init(FuUefiUpdateInfoClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); firmware_class->parse = fu_uefi_update_info_parse; firmware_class->export = fu_uefi_update_info_export; firmware_class->build = fu_uefi_update_info_build; firmware_class->write = fu_uefi_update_info_write; object_class->finalize = fu_uefi_update_info_finalize; } static void fu_uefi_update_info_init(FuUefiUpdateInfo *self) { } FuUefiUpdateInfo * fu_uefi_update_info_new(void) { FuUefiUpdateInfo *self; self = g_object_new(FU_TYPE_UEFI_UPDATE_INFO, NULL); return FU_UEFI_UPDATE_INFO(self); } fwupd-2.0.10/plugins/uefi-capsule/fu-uefi-update-info.h000066400000000000000000000022331501337203100226770ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-uefi-struct.h" #define FU_TYPE_UEFI_UPDATE_INFO (fu_uefi_update_info_get_type()) G_DECLARE_FINAL_TYPE(FuUefiUpdateInfo, fu_uefi_update_info, FU, UEFI_UPDATE_INFO, FuFirmware) FuUefiUpdateInfo * fu_uefi_update_info_new(void); const gchar * fu_uefi_update_info_get_guid(FuUefiUpdateInfo *self); void fu_uefi_update_info_set_guid(FuUefiUpdateInfo *self, const gchar *guid); const gchar * fu_uefi_update_info_get_capsule_fn(FuUefiUpdateInfo *self); void fu_uefi_update_info_set_capsule_fn(FuUefiUpdateInfo *self, const gchar *capsule_fn); void fu_uefi_update_info_set_capsule_flags(FuUefiUpdateInfo *self, guint32 capsule_flags); guint32 fu_uefi_update_info_get_capsule_flags(FuUefiUpdateInfo *self); guint64 fu_uefi_update_info_get_hw_inst(FuUefiUpdateInfo *self); void fu_uefi_update_info_set_hw_inst(FuUefiUpdateInfo *self, guint64 hw_inst); FuUefiUpdateInfoStatus fu_uefi_update_info_get_status(FuUefiUpdateInfo *self); void fu_uefi_update_info_set_status(FuUefiUpdateInfo *self, FuUefiUpdateInfoStatus status); fwupd-2.0.10/plugins/uefi-capsule/fu-uefi.rs000066400000000000000000000034441501337203100206700ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(New, Getters, Default)] #[repr(C, packed)] struct FuStructEfiUxCapsuleHeader { version: u8 == 0x01, checksum: u8, image_type: u8, _reserved: u8, mode: u32le, x_offset: u32le, y_offset: u32le, } #[derive(New, Getters, Default)] #[repr(C, packed)] struct FuStructEfiCapsuleHeader { guid: Guid, header_size: u32le = $struct_size, flags: u32le, image_size: u32le, } #[derive(ToString, FromString)] #[repr(u32le)] enum FuUefiUpdateInfoStatus { Unknown, AttemptUpdate, Attempted, } #[derive(New, Parse, ParseStream, Default)] #[repr(C, packed)] struct FuStructEfiUpdateInfo { version: u32le = 0x7, guid: Guid, flags: u32le, hw_inst: u64le, time_attempted: [u8; 16], // a EFI_TIME_T status: FuUefiUpdateInfoStatus, // EFI_DEVICE_PATH goes here } #[derive(ParseStream)] #[repr(C, packed)] struct FuStructAcpiInsydeQuirk { signature: [char; 6], size: u32le, flags: u32le, } #[derive(ToString, FromString)] enum FuUefiCapsuleDeviceKind { Unknown, SystemFirmware, DeviceFirmware, UefiDriver, Fmp, DellTpmFirmware, } #[derive(ToString)] enum FuUefiCapsuleDeviceStatus { Success, ErrorUnsuccessful, ErrorInsufficientResources, ErrorIncorrectVersion, ErrorInvalidFormat, ErrorAuthError, ErrorPwrEvtAc, ErrorPwrEvtBatt, } #[derive(ParseStream, Default)] #[repr(C, packed)] struct FuStructBitmapFileHeader { signature: [char; 2] == "BM", size: u32le, _reserved1: u16le, _reserved2: u16le, _image_offset: u32le, } #[derive(ParseStream)] #[repr(C, packed)] struct FuStructBitmapInfoHeader { _header_size: u32le, width: u32le, height: u32le, } fwupd-2.0.10/plugins/uefi-capsule/fwupd.grub.conf.in000077500000000000000000000013601501337203100223170ustar00rootroot00000000000000#! /bin/sh # SPDX-License-Identifier: LGPL-2.1-or-later set -e [ -d ${pkgdatadir:?} ] # shellcheck source=/dev/null . "$pkgdatadir/grub-mkconfig_lib" if [ -f @localstatedir@/lib/fwupd/uefi_capsule.conf ] && ls /sys/firmware/efi/efivars/fwupd-*-0abba7dc-e516-4167-bbf5-4d9d1c739416 1>/dev/null 2>&1; then . @localstatedir@/lib/fwupd/uefi_capsule.conf if [ "${EFI_PATH}" != "" ] && [ "${ESP}" != "" ]; then echo "Adding Linux Firmware Updater entry" >&2 cat << EOF menuentry 'Linux Firmware Updater' \$menuentry_id_option 'fwupd' { EOF ${grub_probe:?} --version > /dev/null prepare_grub_to_access_device "$(${grub_probe} --target=device ${ESP})" | sed -e "s/^/\t/" cat << EOF chainloader ${EFI_PATH} } EOF fi fi fwupd-2.0.10/plugins/uefi-capsule/make-images.py000077500000000000000000000173261501337203100215230ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2017 Peter Jones # Copyright 2020 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later # # pylint: disable=wrong-import-position,too-many-locals,unused-argument,too-many-statements # pylint: disable=invalid-name,too-many-instance-attributes,missing-module-docstring # pylint: disable=missing-function-docstring,missing-class-docstring,too-few-public-methods import os import sys import argparse import tarfile import math import io import struct from typing import Dict, Optional, Any import cairo import gi gi.require_version("Pango", "1.0") gi.require_version("PangoCairo", "1.0") from gi.repository import Pango, PangoCairo def languages(podir: str): for x in open(os.path.join(podir, "LINGUAS")).readlines(): yield x.strip() yield "en" class PotFile: def __init__(self, fn=None): self.msgs: Dict[str, str] = {} if fn: self.parse(fn) def parse(self, fn: str) -> None: with open(fn) as f: lang_en: Optional[str] = None for line in f.read().split("\n"): if not line: continue if line[0] == "#": continue try: key, value = line.split(" ", maxsplit=1) except ValueError: continue if key == "msgid": lang_en = value[1:-1] continue if key == "msgstr" and lang_en: self.msgs[lang_en] = value[1:-1] lang_en = None continue def _cairo_surface_write_to_bmp(img: cairo.ImageSurface) -> bytes: data = bytes(img.get_data()) return ( b"BM" + struct.pack( " int: # open output archive with tarfile.open(args.out, "w:xz", preset=8) as tar: for lang in languages(args.podir): # these are the 1.6:1 of some common(ish) screen widths if lang == "en": label_translated: str = args.label else: potfile = PotFile(os.path.join(args.podir, f"{lang}.po")) try: label_translated = potfile.msgs[args.label] except KeyError: continue if label_translated == args.label: continue for width, height in ( (640, 480), (800, 600), (1024, 768), (1280, 720), (1280, 800), (1366, 768), (1536, 864), (1600, 900), (1920, 1080), (1920, 1200), (2160, 1350), (2560, 1440), (3840, 2160), (5120, 2880), (5688, 3200), (7680, 4320), ): # generate PangoLanguage font_desc = f"Sans {height / 32:.2f}px" fd = Pango.FontDescription(font_desc) font_option = cairo.FontOptions() font_option.set_antialias(cairo.ANTIALIAS_SUBPIXEL) l = Pango.Language.from_string(lang) # create surface img = cairo.ImageSurface(cairo.FORMAT_RGB24, 1, 1) cctx = cairo.Context(img) layout = PangoCairo.create_layout(cctx) pctx = layout.get_context() pctx.set_font_description(fd) pctx.set_language(l) fs = pctx.load_fontset(fd, l) PangoCairo.context_set_font_options(pctx, font_option) attrs = Pango.AttrList() length = len(bytes(label_translated, "utf8")) items = Pango.itemize(pctx, label_translated, 0, length, attrs, None) if not items: continue try: # urgh, https://gitlab.gnome.org/GNOME/pango/-/merge_requests/829 # -- if we depend on Pango >= 1.56.2 we can drop the fallback # # ...or if we depend on python3-gobject >= 3.51.0 we can use the nicer: # len(inspect.signature(Pango.shape).parameters) gs = Pango.shape(label_translated, length, items[0].analysis) except TypeError: gs = Pango.GlyphString() Pango.shape(label_translated, length, items[0].analysis, gs) del img, cctx, pctx, layout def find_size(fs, f, data): """find our size, I hope...""" (ink, log) = gs.extents(f) if ink.height == 0 or ink.width == 0: return False data.update({"log": log, "ink": ink}) return True data: Dict[str, Any] = {} fs.foreach(find_size, data) if len(data) == 0: print("Missing sans fonts") return 2 log = data["log"] ink = data["ink"] surface_height = math.ceil(max(ink.height, log.height) / Pango.SCALE) surface_width = math.ceil(max(ink.width, log.width) / Pango.SCALE) x = -math.ceil(log.x / Pango.SCALE) y = -math.ceil(log.y / Pango.SCALE) img = cairo.ImageSurface( cairo.FORMAT_RGB24, surface_width, surface_height ) cctx = cairo.Context(img) layout = PangoCairo.create_layout(cctx) pctx = layout.get_context() PangoCairo.context_set_font_options(pctx, font_option) cctx.set_source_rgb(1, 1, 1) cctx.move_to(x, y - surface_height / 2) def do_write(fs, f, data): """write out glyphs""" ink = gs.extents(f)[0] if ink.height == 0 or ink.width == 0: return False PangoCairo.show_glyph_string(cctx, f, gs) return True # flip the image to write the bitmap upside-down mat = cairo.Matrix() mat.scale(1, -1) cctx.transform(mat) fs.foreach(do_write, None) img.flush() # convert to BMP and add to archive with io.BytesIO() as io_bmp: io_bmp.write(_cairo_surface_write_to_bmp(img)) filename = f"fwupd-{lang}-{width}-{height}.bmp" tarinfo = tarfile.TarInfo(filename) tarinfo.size = io_bmp.tell() io_bmp.seek(0) tar.addfile(tarinfo, fileobj=io_bmp) # success return 0 if __name__ == "__main__": parser = argparse.ArgumentParser(description="Make UX images") parser.add_argument("--label", help="Update text", required=True) parser.add_argument("--podir", help="Po location", required=True) parser.add_argument("--out", help="Output archive", required=True) sys.exit(main(parser.parse_args())) fwupd-2.0.10/plugins/uefi-capsule/meson.build000066400000000000000000000104321501337203100211170ustar00rootroot00000000000000if allow_uefi_capsule subdir('tests') cargs = ['-DG_LOG_DOMAIN="FuPluginUefiCapsule"'] plugins += {meson.current_source_dir().split('/')[-1]: true} efi_os_dir = get_option('efi_os_dir') if efi_os_dir != '' cargs += '-DEFI_OS_DIR="' + efi_os_dir + '"' endif plugin_quirks += files('uefi-capsule.quirk') backend_srcs = ['fu-uefi-capsule-backend.c'] if host_machine.system() == 'linux' backend_srcs += 'fu-uefi-capsule-backend-linux.c' # replace @localstatedir@ con2 = configuration_data() con2.set('localstatedir', localstatedir) configure_file( input: 'fwupd.grub.conf.in', output: '35_fwupd', configuration: con2, install: true, install_dir: join_paths(sysconfdir, 'grub.d') ) elif host_machine.system() == 'freebsd' backend_srcs += 'fu-uefi-capsule-backend-freebsd.c' else error('no ESRT support for @0@'.format(host_machine.system())) endif plugin_builtin_uefi_capsule = static_library('fu_plugin_uefi_capsule', rustgen.process('fu-uefi.rs'), sources: [ 'fu-uefi-capsule-plugin.c', 'fu-uefi-bgrt.c', 'fu-uefi-bootmgr.c', 'fu-uefi-common.c', 'fu-uefi-cod-device.c', 'fu-uefi-nvram-device.c', 'fu-uefi-grub-device.c', 'fu-uefi-capsule-device.c', 'fu-uefi-update-info.c', 'fu-acpi-uefi.c', 'fu-bitmap-image.c', backend_srcs, ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: [ plugin_deps, platform_deps, ], ) plugin_builtins += plugin_builtin_uefi_capsule # add all the .po files as inputs to watch ux_linguas = run_command( ['cat', files(join_paths(meson.project_source_root(), 'po', 'LINGUAS'))], check: true, ).stdout().strip().split('\n') ux_capsule_pofiles = [] foreach ux_lingua: ux_linguas ux_capsule_pofiles += join_paths(meson.project_source_root(), 'po', '@0@.po'.format(ux_lingua)) endforeach if get_option('efi_binary') efi_binary = dependency('fwupd-efi', version: '>= 1.6', fallback: ['fwupd-efi', 'fwupd_efi_dep']) endif if get_option('plugin_uefi_capsule_splash') # add the archive of pregenerated images splash_deps = run_command([ python3, join_paths(meson.project_source_root(), 'po', 'test-deps'), join_paths(meson.project_source_root(), 'po', 'LINGUAS'), ], check: false) if splash_deps.returncode() != 0 error(splash_deps.stderr().strip()) endif custom_target('ux-capsule-tar', input: [ join_paths(meson.project_source_root(), 'po', 'LINGUAS'), files('make-images.py'), ux_capsule_pofiles, ], output: 'uefi-capsule-ux.tar.xz', command: [ python3.full_path(), files('make-images.py'), '--podir', join_paths(meson.project_source_root(), 'po'), '--label', 'Installing firmware update…', '--out', '@OUTPUT@', ], install: true, install_dir: join_paths(datadir, 'fwupd'), ) endif if get_option('tests') env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) env.set('FWUPD_LOCALSTATEDIR', '/tmp/fwupd-self-test/var') e = executable( 'uefi-self-test', rustgen.process('fu-uefi.rs'), uefi_insyde_blob, fwupdx64_efi_signed, sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: [ plugin_deps, platform_deps, ], link_with: [ fwupd, fwupdplugin, plugin_builtin_uefi_capsule, ], c_args: [ cargs, '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('uefi-self-test', e, env: env) # to use these do `sudo systemctl edit fwupd.service` and set # Environment="FWUPD_SYSFSFWDIR=/usr/share/installed-tests/fwupd" install_data([ 'tests/efi/esrt/entries/entry0/capsule_flags', 'tests/efi/esrt/entries/entry0/fw_class', 'tests/efi/esrt/entries/entry0/fw_type', 'tests/efi/esrt/entries/entry0/fw_version', 'tests/efi/esrt/entries/entry0/last_attempt_status', 'tests/efi/esrt/entries/entry0/last_attempt_version', 'tests/efi/esrt/entries/entry0/lowest_supported_fw_version', ], install_dir: join_paths(installed_test_datadir, 'efi/esrt/entries/entry0'), ) endif summary({ 'efi_os_dir': efi_os_dir, 'efi binary': get_option('efi_binary'), 'capsule splash': get_option('plugin_uefi_capsule_splash'), }, section:'uefi capsule options') endif fwupd-2.0.10/plugins/uefi-capsule/tests/000077500000000000000000000000001501337203100201175ustar00rootroot00000000000000fwupd-2.0.10/plugins/uefi-capsule/tests/.gitignore000066400000000000000000000001421501337203100221040ustar00rootroot00000000000000EFI efi/efivars/fwupd-c34cb672-a81e-5d32-9d89-cbcabe8ec37b-0-0abba7dc-e516-4167-bbf5-4d9d1c739416 fwupd-2.0.10/plugins/uefi-capsule/tests/acpi/000077500000000000000000000000001501337203100210335ustar00rootroot00000000000000fwupd-2.0.10/plugins/uefi-capsule/tests/acpi/bgrt/000077500000000000000000000000001501337203100217715ustar00rootroot00000000000000fwupd-2.0.10/plugins/uefi-capsule/tests/acpi/bgrt/image000077700000000000000000000000001501337203100250762../../test.bmpustar00rootroot00000000000000fwupd-2.0.10/plugins/uefi-capsule/tests/acpi/bgrt/status000066400000000000000000000000021501337203100232270ustar00rootroot000000000000001 fwupd-2.0.10/plugins/uefi-capsule/tests/acpi/bgrt/type000066400000000000000000000000021501337203100226650ustar00rootroot000000000000000 fwupd-2.0.10/plugins/uefi-capsule/tests/acpi/bgrt/version000066400000000000000000000000021501337203100233710ustar00rootroot000000000000001 fwupd-2.0.10/plugins/uefi-capsule/tests/acpi/bgrt/xoffset000066400000000000000000000000041501337203100233640ustar00rootroot00000000000000123 fwupd-2.0.10/plugins/uefi-capsule/tests/acpi/bgrt/yoffset000066400000000000000000000000041501337203100233650ustar00rootroot00000000000000456 fwupd-2.0.10/plugins/uefi-capsule/tests/build-fwupdx64-efi-signed.py000077500000000000000000000004601501337203100252700ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright 2025 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later # # pylint: disable=invalid-name,missing-module-docstring import sys with open(sys.argv[1], "wb") as f: f.write("fwupd-efi version 1.2\0".encode("utf-16") + b"PADDING" * 10) fwupd-2.0.10/plugins/uefi-capsule/tests/build-uefi-insyde.py000077500000000000000000000017661501337203100240240ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright 2025 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later # # pylint: disable=invalid-name,missing-module-docstring import sys import struct import uuid buf = b"" buf += struct.pack("<4s", b"UEFI") # signature buf += struct.pack(" firmware.bin fwupdtool build-cabinet firmware.cab firmware.bin firmware.metainfo.xml fwupd-2.0.10/plugins/uefi-capsule/tests/example/firmware.metainfo.xml000066400000000000000000000020251501337203100257100ustar00rootroot00000000000000 example.firmware UEFI Firmware

    Example UEFI firmware for fwupd

    The test device can be updated using the UEFI capsule plugin.

    ddc0ee61-e7f0-4e7d-acc5-c070a398838e https://fwupd.org/ CC0-1.0 CC0-1.0 Richard Hughes

    This release updates a frobnicator to frob faster.

    org.freedesktop.fwupd fwupd-2.0.10/plugins/uefi-capsule/tests/grub2-mkconfig000077500000000000000000000000271501337203100226600ustar00rootroot00000000000000#!/usr/bin/env python3 fwupd-2.0.10/plugins/uefi-capsule/tests/grub2-reboot000077500000000000000000000000271501337203100223550ustar00rootroot00000000000000#!/usr/bin/env python3 fwupd-2.0.10/plugins/uefi-capsule/tests/grub2/000077500000000000000000000000001501337203100211405ustar00rootroot00000000000000fwupd-2.0.10/plugins/uefi-capsule/tests/grub2/grub.cfg000066400000000000000000000000161501337203100225550ustar00rootroot00000000000000# hello world fwupd-2.0.10/plugins/uefi-capsule/tests/meson.build000066400000000000000000000012621501337203100222620ustar00rootroot00000000000000if get_option('tests') uefi_insyde_blob = custom_target('UEFI-insyde', output: 'UEFI', command: [ python3.full_path(), files('build-uefi-insyde.py'), '@OUTPUT@', ], install: true, install_dir: installed_test_datadir, ) fwupdx64_efi_signed = custom_target('fwupdx64.efi.signed', output: 'fwupdx64.efi.signed', command: [ python3.full_path(), files('build-fwupdx64-efi-signed.py'), '@OUTPUT@', ], install: true, install_dir: installed_test_datadir, ) install_data([ 'grub2/grub.cfg', 'test.quirk', 'uefi-update-info.builder.xml', ], install_dir: installed_test_datadir, ) endif fwupd-2.0.10/plugins/uefi-capsule/tests/test.bmp000066400000000000000000000046721501337203100216070ustar00rootroot00000000000000BMº zl6@  BGRs  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~€€€‚‚‚ƒƒƒ„„„………†††‡‡‡ˆˆˆ‰‰‰ŠŠŠ‹‹‹ŒŒŒŽŽŽ‘‘‘’’’“““”””•••–––———˜˜˜™™™ššš›››œœœžžžŸŸŸ   ¡¡¡¢¢¢£££¤¤¤¥¥¥¦¦¦§§§¨¨¨©©©ªªª«««¬¬¬­­­®®®¯¯¯°°°±±±²²²³³³´´´µµµ¶¶¶···¸¸¸¹¹¹ººº»»»¼¼¼½½½¾¾¾¿¿¿ÀÀÀÁÁÁÂÂÂÃÃÃÄÄÄÅÅÅÆÆÆÇÇÇÈÈÈÉÉÉÊÊÊËËËÌÌÌÍÍÍÎÎÎÏÏÏÐÐÐÑÑÑÒÒÒÓÓÓÔÔÔÕÕÕÖÖÖ×××ØØØÙÙÙÚÚÚÛÛÛÜÜÜÝÝÝÞÞÞßßßàààáááâââãããäääåååæææçççèèèéééêêêëëëìììíííîîîïïïðððñññòòòóóóôôôõõõööö÷÷÷øøøùùùúúúûûûüüüýýýþþþÿÿÿÈÊÅÎÄžwSIGIFCGEHVt¦ÄËÌÇÇÊÉÈÌÄÌÉÇÌȸ—mODCGGFFEFVnœÂÂûÃÉÌȱ|E  #;p¨ÊÇÌÊÇËÇÇÌÅÌØg:  "I|¹ÃÃÁÊǹo* )BGEBCDE<-TšÈÈÉËÉÉÌÆÄÆ‹M3ACEEDFH9 0u¼À¿É¾v, >wž¡¥£§¢¦ „Z0 Bœ¿ËÌÆÊÈÆÊ‡5 5f‹š¤¢¢œ”h1 #†¾ÀÈ“0B›ÈÊÇÊËÍÈÉËÍ¿‚5 ?ªÅÊÈÉÄÅŸ. EºËÅÄÅÆÉÅÁų77«º¾M 1ÆÉÊÌÌþÄÇËÊÉ™+q¶ÌÌÆÌ¶R>šÅÇËÇÇ¿¹ÁÅÁÅÆ¹…%^¸n¿ÆÊÊű ˆ“®¾ÌÈÊÅ‘1–ÈÉÉÇ—#ÅÇÈž­‘€™¬ÄÁ¿Ã­iŸL I£ÆÊÈîD/|¼ÌÌÊÂH {ÀÉʶoi°ÈÈȃ)>”ÃÂÅÅ+i! xÂÈÍʤ4 |ÅÎÑÐql¼È̱UÄÈÊÆ|4”ÁÄİb5#‘ÊËǵX:šÊÌʃk¹ÈͶSÃËÈ—;Y©ÅÀ¿‰4šÊÌǘ(eÍÉÏ„h¶Í˳R¨ÁÌÃk.„ÂÂÆ›4šÊÇÅ AÅÐÌ„i·ÌȰU¨ÉDZUo»Âà 5œÎÉ·d0©ÎÊŠl¸ÊɲX §ÉÈš<\°ÆÄ 8•Éά[ 697bd920-12cf-4da9-8385-996909bc6559 foo.cap 0x1 0x2 attempt-update
    fwupd-2.0.10/plugins/uefi-capsule/uefi-capsule.quirk000066400000000000000000000027411501337203100224200ustar00rootroot00000000000000# not an updatable device per-se, but used to get the DRI screen size for the UX capsule [DRM\TYPE_DRM_MINOR] Plugin = uefi_capsule # StarLite Mk II [797f8bae-0ea2-4c0f-8a30-7d10ccfacbc0] Flags = no-ux-capsule # StarLite Mk III [d9d7b13b-e4db-4f91-8bf6-8952a9caa82a] Flags = no-ux-capsule # Silicom Minnowboard Turbot D0/D1 PLATFORM [386c2f52-44ec-55d3-b802-47631bfb8451] Flags = uefi-force-enable # LENOVO [6de5d951-d755-576b-bd09-c5cf66b27234] Flags = supports-boot-order-lock,use-legacy-bootmgr-desc,no-ux-capsule,no-lid-closed,modify-bootorder # Dell Inc. [85d38fda-fc0e-5c6f-808f-076984ae7978] Flags = modify-bootorder,cod-dell-recovery # ASUSTeK COMPUTER INC. [65605ed1-09e2-57aa-bd0f-a26c1d35433f] Flags = modify-bootorder # Framework [2185e781-ed4b-5fc2-8839-b5ddb1e7d649] Flags = modify-bootorder # HP [93f84748-c854-5d6b-b78a-13c2361e0758] Flags = modify-bootorder # Lenovo ThinkPad X1 Yoga 4th [1138930e-8df1-5ae0-b946-7b0d9c9b4a79] Flags = no-coalesce # Lenovo ThinkPad X1 Carbon 5th [595a0c1a-9819-52f6-8ea7-e0924c073f64] Flags = no-coalesce # Lenovo ThinkPad X1 Carbon 7th [30ddbc9b-8200-5581-8dfd-6bb26e2e42d7] Flags = no-coalesce # Lenovo ThinkPad T460s [90706264-e399-575b-a9fb-077ead03b9b4] Flags = no-coalesce # Lenovo T14 Gen 2 Battery [f7576b01-100e-4ec1-96cf-ccb69f4ce87e] Flags = ignore-system-power # Dynabook (né Toshiba) X30, X40 [28108d08-5027-42c2-a5b8-92d6ede9b97b] VersionFormat = bcd # HP Dev One [9f7962c7-7d0e-473d-8a01-c168912f1f14] Flags = no-ux-capsule fwupd-2.0.10/plugins/uefi-db/000077500000000000000000000000001501337203100157065ustar00rootroot00000000000000fwupd-2.0.10/plugins/uefi-db/README.md000066400000000000000000000027311501337203100171700ustar00rootroot00000000000000--- title: Plugin: UEFI db --- ## Introduction The EFI Signature Database (also known as `db`) contains multiple certificates, hashes and signatures, all signed by the KEK. It is used to allow signed binaries to be run with SecureBoot enabled. This plugin allows updating the `db` with new certificates issued by Microsoft or the OEM vendor. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in `EFI_SIGNATURE_LIST` format. See for details. This plugin supports the following protocol ID: * `org.uefi.dbx2` ## GUID Generation These devices use the GUID constructed of the uppercase SHA256 of the X.509 certificate. e.g. * `UEFI\CRT_{sha256}` Additionally, the subject vendor and name are used if provided. * `UEFI\VENDOR_{vendor}&NAME_{name}` ## Update Behavior The firmware is deployed when the machine is in normal runtime mode, but it is only activated when the system is restarted. ## Vendor ID Security The vendor ID is set from the certificate vendor, e.g. `UEFI:Microsoft`. ## External Interface Access This plugin requires: * Read and write access to `/sys/firmware/efi/efivars` ## Version Considerations This plugin has been available since fwupd version `2.0.8`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Richard Hughes: @hughsie fwupd-2.0.10/plugins/uefi-db/fu-uefi-db-device.c000066400000000000000000000124771501337203100212450ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uefi-db-device.h" struct _FuUefiDbDevice { FuUefiDevice parent_instance; }; G_DEFINE_TYPE(FuUefiDbDevice, fu_uefi_db_device, FU_TYPE_UEFI_DEVICE) static gboolean fu_uefi_db_device_probe(FuDevice *device, GError **error) { FuContext *ctx = fu_device_get_context(device); g_autoptr(FuFirmware) siglist = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GPtrArray) sigs = NULL; /* FuUefiDevice->probe */ if (!FU_DEVICE_CLASS(fu_uefi_db_device_parent_class)->probe(device, error)) return FALSE; /* add each subdevice */ siglist = fu_device_read_firmware(device, progress, error); if (siglist == NULL) { g_prefix_error(error, "failed to parse db: "); return FALSE; } sigs = fu_efi_signature_list_get_newest(FU_EFI_SIGNATURE_LIST(siglist)); for (guint i = 0; i < sigs->len; i++) { FuEfiSignature *sig = g_ptr_array_index(sigs, i); g_autoptr(FuEfiX509Device) x509_device = NULL; if (fu_efi_signature_get_kind(sig) != FU_EFI_SIGNATURE_KIND_X509) continue; x509_device = fu_efi_x509_device_new(ctx, FU_EFI_X509_SIGNATURE(sig)); fu_device_set_physical_id(FU_DEVICE(x509_device), "db"); fu_device_set_proxy(FU_DEVICE(x509_device), device); fu_device_add_child(device, FU_DEVICE(x509_device)); } /* set in the subdevice */ fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_CAN_EMULATION_TAG); fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_CAN_VERIFY); /* success */ return TRUE; } static gboolean fu_uefi_db_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { g_autoptr(GBytes) fw = NULL; /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* write entire chunk to efivarsfs */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); if (!fu_uefi_device_set_efivar_bytes( FU_UEFI_DEVICE(device), FU_EFIVARS_GUID_SECURITY_DATABASE, fu_device_get_physical_id(device), fw, FU_EFIVARS_ATTR_APPEND_WRITE | FU_EFIVARS_ATTR_TIME_BASED_AUTHENTICATED_WRITE_ACCESS | FU_EFIVARS_ATTR_RUNTIME_ACCESS | FU_EFIVARS_ATTR_BOOTSERVICE_ACCESS | FU_EFIVARS_ATTR_NON_VOLATILE, error)) { return FALSE; } /* success! */ return TRUE; } static void fu_uefi_db_device_add_security_attrs(FuDevice *device, FuSecurityAttrs *attrs) { GPtrArray *children = fu_device_get_children(device); gboolean seen_old = FALSE; gboolean seen_new = FALSE; g_autoptr(FwupdSecurityAttr) attr = NULL; /* create attr */ attr = fu_device_security_attr_new(device, FWUPD_SECURITY_ATTR_ID_UEFI_DB); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_VALID); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW); fu_security_attrs_append(attrs, attr); /* look for both versions of the Microsoft UEFI CA */ for (guint i = 0; i < children->len; i++) { FuDevice *child = g_ptr_array_index(children, i); if (fu_device_has_instance_id(child, "UEFI\\CRT_7CD7437C555F89E7C2B50E21937E420C4E583E80", FU_DEVICE_INSTANCE_FLAG_VISIBLE)) { seen_new = TRUE; break; } if (fu_device_has_instance_id(child, "UEFI\\CRT_E30CF09DABEAB32A6E3B07A7135245DE05FFB658", FU_DEVICE_INSTANCE_FLAG_VISIBLE)) { seen_old = TRUE; break; } } if (!seen_new && !seen_old) { /* user is using a custom UEFI db, just ignore this HSI attribute */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND); } else if (seen_new) { fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } else { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); } } static void fu_uefi_db_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_uefi_db_device_init(FuUefiDbDevice *self) { fu_device_set_physical_id(FU_DEVICE(self), "db"); fu_device_set_name(FU_DEVICE(self), "UEFI Signature Database"); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE_CHILD); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_EFI_SIGNATURE_LIST); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_APPLICATION_CERTIFICATE); } static void fu_uefi_db_device_class_init(FuUefiDbDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_uefi_db_device_probe; device_class->write_firmware = fu_uefi_db_device_write_firmware; device_class->add_security_attrs = fu_uefi_db_device_add_security_attrs; device_class->set_progress = fu_uefi_db_device_set_progress; } fwupd-2.0.10/plugins/uefi-db/fu-uefi-db-device.h000066400000000000000000000004601501337203100212370ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_UEFI_DB_DEVICE (fu_uefi_db_device_get_type()) G_DECLARE_FINAL_TYPE(FuUefiDbDevice, fu_uefi_db_device, FU, UEFI_DB_DEVICE, FuUefiDevice) fwupd-2.0.10/plugins/uefi-db/fu-uefi-db-plugin.c000066400000000000000000000013261501337203100212730ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uefi-db-device.h" #include "fu-uefi-db-plugin.h" struct _FuUefiDbPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuUefiDbPlugin, fu_uefi_db_plugin, FU_TYPE_PLUGIN) static void fu_uefi_db_plugin_init(FuUefiDbPlugin *self) { } static void fu_uefi_db_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_UEFI_DB_DEVICE); } static void fu_uefi_db_plugin_class_init(FuUefiDbPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_uefi_db_plugin_constructed; } fwupd-2.0.10/plugins/uefi-db/fu-uefi-db-plugin.h000066400000000000000000000003561501337203100213020ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuUefiDbPlugin, fu_uefi_db_plugin, FU, UEFI_DB_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/uefi-db/meson.build000066400000000000000000000006021501337203100200460ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginUefiDb"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('uefi-db.quirk') plugin_builtins += static_library('fu_plugin_uefi_db', sources: [ 'fu-uefi-db-device.c', 'fu-uefi-db-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) fwupd-2.0.10/plugins/uefi-db/uefi-db.quirk000066400000000000000000000001121501337203100202700ustar00rootroot00000000000000[UEFI\GUID_d719b2cb-3d3a-4596-a3bc-dad00e67656f&NAME_db] Plugin = uefi_db fwupd-2.0.10/plugins/uefi-dbx/000077500000000000000000000000001501337203100160765ustar00rootroot00000000000000fwupd-2.0.10/plugins/uefi-dbx/README.md000066400000000000000000000071401501337203100173570ustar00rootroot00000000000000--- title: Plugin: UEFI dbx --- ## Introduction Updating the UEFI revocation database prevents starting EFI binaries with known security issues, and is typically no longer done from a firmware update due to the risk of the machine being "bricked" if the bootloader is not updated first. This plugin also checks if the UEFI dbx contains all the most recent revoked checksums. The result will be stored in an security attribute for HSI. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in EFI_SIGNATURE_LIST format. See for details. This plugin supports the following protocol ID: * `org.uefi.dbx2` ## Comparing Versions Earlier versions of fwupd used the `org.uefi.dbx` protocol which counted the number of Microsoft hashes in the local `dbx` file and in the firmware payload. The equivalent versions are given below: ### x64 | Old Version | New Version | |-------------|-------------| | `9` | `20100307` | | `13` | `20140413` | | `77` | `20160809` | | `190` | `20200701` | | `211` | `20210401` | | `217` | `20220801` | | `220` | `20230301` | | `371` | `20230501` | ### aarch64 | Old Version | New Version | |-------------|-------------| | `19` | `20200729` | | `21` | `20210401` | | `22` | `20220801` | | `26` | `20230501` | ### i386 | Old Version | New Version | |-------------|-------------| | `41` | `20200701` | | `55` | `20210401` | | `57` | `20230301` | | `89` | `20230501` | ## GUID Generation These devices use the GUID constructed of the uppercase SHA256 of the X509 certificates found in the system KEK and optionally the EFI architecture. e.g. * `UEFI\CRT_{sha256}` (quirk-only) * `UEFI\CRT_{sha256}&ARCH_{arch}` ...where `arch` is typically one of `IA32`, `X64`, `ARM` or `AA64` Additionally, the last listed dbx SHA256 checksum is added as a quirk-only GUID so that the version can be corrected even when fwupd is operating 100% offline. * `UEFI\CSUM_{sha256}` ## Metadata Microsoft actually removes checksums in some UEFI dbx updates, which is probably a result of OEM pressure about SPI usage -- but local dbx updates are append-only. This means that if you remove hashes then you can have a different number of dbx checksums on your machine depending on whether you went `A→B→C→D` or `A→D`... In these cases we look at the *last-entry* dbx checksum and compare to the set we know, either from the quirk files, local metadata, or remote metadata from the LVFS. The `org.linuxfoundation.dbx.*.firmware` components will match against a hash of the system PK. The latest cabinet archive can also be installed into the `vendor-firmware` remote found in `/usr/share/fwupd/remotes.d/vendor/firmware/` which allows the version-fixup to work even when offline -- although using the LVFS source is recommended for most users. The *last-entry checksum* can be found from the `fwupdtool firmware-parse DBXUpdate-$VERSION$.x64.bin efi-signature-list` command. ## Update Behavior The firmware is deployed when the machine is in normal runtime mode, but it is only activated when the system is restarted. ## Vendor ID Security The vendor ID is hardcoded to `UEFI:Microsoft` for all devices. ## External Interface Access This plugin requires: * read/write access to `/sys/firmware/efi/efivars` ## Version Considerations This plugin has been available since fwupd version `1.5.0` with `org.uefi.dbx2` being available in fwupd 1.9.27 (from the 1.9.x series) and 2.0.4 (from the 2.0.x series). fwupd-2.0.10/plugins/uefi-dbx/dbxtool.md000066400000000000000000000012221501337203100200700ustar00rootroot00000000000000--- title: dbxtool command line utility --- % dbxtool(1) {{PACKAGE_VERSION}} | dbxtool man page ## NAME **dbxtool** — modify the dbx revocation list ## SYNOPSIS | **dbxtool** [CMD] ## DESCRIPTION This manual page documents briefly the **dbxtool** command. **dbxtool** allows a user to operate on the UEFI dbx revocation list. This tool can be used to list the current dbx contents or update it to a newer version. ## OPTIONS The dbxtool command takes various options depending on the action. Run **dbxtool --help** for the full list. ## BUGS See GitHub Issues: ## SEE ALSO fwupd-2.0.10/plugins/uefi-dbx/fu-dbxtool.c000066400000000000000000000236071501337203100203350ustar00rootroot00000000000000/* * Copyright 2015 Peter Jones * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include #include #include "fu-context-private.h" #include "fu-uefi-dbx-common.h" /* custom return code */ #define EXIT_NOTHING_TO_DO 2 static void fu_util_ignore_cb(const gchar *log_domain, GLogLevelFlags log_level, const gchar *message, gpointer user_data) { } static FuFirmware * fu_dbxtool_get_siglist_system(FuContext *ctx, GError **error) { FuEfivars *efivars = fu_context_get_efivars(ctx); g_autoptr(GBytes) blob = NULL; g_autoptr(FuFirmware) dbx = fu_efi_signature_list_new(); blob = fu_efivars_get_data_bytes(efivars, FU_EFIVARS_GUID_SECURITY_DATABASE, "dbx", NULL, error); if (blob == NULL) return NULL; if (!fu_firmware_parse_bytes(dbx, blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error)) return NULL; return g_steal_pointer(&dbx); } static FuFirmware * fu_dbxtool_get_siglist_local(const gchar *filename, GError **error) { g_autoptr(GFile) file = NULL; g_autoptr(FuFirmware) siglist = fu_efi_signature_list_new(); file = g_file_new_for_path(filename); if (!fu_firmware_parse_file(siglist, file, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return NULL; return g_steal_pointer(&siglist); } static gboolean fu_dbxtool_siglist_inclusive(FuFirmware *outer, FuFirmware *inner) { g_autoptr(GPtrArray) sigs = fu_firmware_get_images(inner); for (guint i = 0; i < sigs->len; i++) { FuEfiSignature *sig = g_ptr_array_index(sigs, i); g_autofree gchar *checksum = NULL; g_autoptr(FuFirmware) img = NULL; checksum = fu_firmware_get_checksum(FU_FIRMWARE(sig), G_CHECKSUM_SHA256, NULL); if (checksum == NULL) continue; img = fu_firmware_get_image_by_checksum(outer, checksum, NULL); if (img == NULL) return FALSE; } return TRUE; } static const gchar * fu_dbxtool_guid_to_string(const gchar *guid) { if (g_strcmp0(guid, FU_EFI_SIGNATURE_GUID_ZERO) == 0) return "zero"; if (g_strcmp0(guid, FU_EFI_SIGNATURE_GUID_MICROSOFT) == 0) return "microsoft"; if (g_strcmp0(guid, FU_EFI_SIGNATURE_GUID_OVMF) == 0 || g_strcmp0(guid, FU_EFI_SIGNATURE_GUID_OVMF_LEGACY) == 0) return "ovmf"; return guid; } int main(int argc, char *argv[]) { gboolean action_apply = FALSE; gboolean action_list = FALSE; gboolean action_version = FALSE; gboolean force = FALSE; gboolean verbose = FALSE; g_autofree gchar *dbxfile = NULL; g_autoptr(GError) error = NULL; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(GOptionContext) context = NULL; g_autofree gchar *tmp = NULL; g_autofree gchar *esp_path = NULL; const GOptionEntry options[] = { {"verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose, /* TRANSLATORS: command line option */ N_("Show extra debugging information"), NULL}, {"version", '\0', 0, G_OPTION_ARG_NONE, &action_version, /* TRANSLATORS: command line option */ N_("Show the calculated version of the dbx"), NULL}, {"list", 'l', 0, G_OPTION_ARG_NONE, &action_list, /* TRANSLATORS: command line option */ N_("List entries in dbx"), NULL}, {"apply", 'a', 0, G_OPTION_ARG_NONE, &action_apply, /* TRANSLATORS: command line option */ N_("Apply update files"), NULL}, {"dbx", 'd', 0, G_OPTION_ARG_STRING, &dbxfile, /* TRANSLATORS: command line option */ N_("Specify the dbx database file"), /* TRANSLATORS: command argument: uppercase, spaces->dashes */ N_("FILENAME")}, {"esp-path", 'p', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &esp_path, /* TRANSLATORS: command line option */ N_("Override the default ESP path"), /* TRANSLATORS: command argument: uppercase, spaces->dashes */ N_("PATH")}, {"force", 'f', 0, G_OPTION_ARG_NONE, &force, /* TRANSLATORS: command line option */ N_("Apply update even when not advised"), NULL}, {NULL}}; setlocale(LC_ALL, ""); bindtextdomain(GETTEXT_PACKAGE, FWUPD_LOCALEDIR); bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8"); textdomain(GETTEXT_PACKAGE); /* get a action_list of the commands */ context = g_option_context_new(NULL); g_option_context_set_description( context, /* TRANSLATORS: description of dbxtool */ _("This tool allows an administrator to apply UEFI dbx updates.")); /* TRANSLATORS: program name */ g_set_application_name(_("UEFI dbx Utility")); g_option_context_add_main_entries(context, options, NULL); if (!g_option_context_parse(context, &argc, &argv, &error)) { /* TRANSLATORS: the user didn't read the man page */ g_print("%s: %s\n", _("Failed to parse arguments"), error->message); return EXIT_FAILURE; } /* set verbose? */ if (verbose) { (void)g_setenv("G_MESSAGES_DEBUG", "all", FALSE); } else { g_log_set_handler(G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, fu_util_ignore_cb, NULL); } /* override the default ESP path */ if (esp_path != NULL) fu_context_set_esp_location(ctx, esp_path); /* list contents, either of the existing system, or an update */ if (action_list || action_version) { guint cnt = 1; g_autoptr(FuFirmware) dbx = NULL; g_autoptr(GPtrArray) sigs = NULL; if (dbxfile != NULL) { dbx = fu_dbxtool_get_siglist_local(dbxfile, &error); if (dbx == NULL) { g_printerr("%s: %s\n", /* TRANSLATORS: could not read existing system data */ _("Failed to load local dbx"), error->message); return EXIT_FAILURE; } } else { dbx = fu_dbxtool_get_siglist_system(ctx, &error); if (dbx == NULL) { g_printerr("%s: %s\n", /* TRANSLATORS: could not read existing system data */ _("Failed to load system dbx"), error->message); return EXIT_FAILURE; } } if (action_version) { /* TRANSLATORS: the detected version number of the dbx */ g_print("%s: %s\n", _("Version"), fu_firmware_get_version(dbx)); return EXIT_SUCCESS; } sigs = fu_firmware_get_images(FU_FIRMWARE(dbx)); for (guint i = 0; i < sigs->len; i++) { FuEfiSignature *sig = g_ptr_array_index(sigs, i); g_autofree gchar *checksum = NULL; checksum = fu_firmware_get_checksum(FU_FIRMWARE(sig), G_CHECKSUM_SHA256, NULL); g_print("%4u: {%s} {%s} %s\n", cnt++, fu_dbxtool_guid_to_string(fu_efi_signature_get_owner(sig)), fu_efi_signature_kind_to_string(fu_efi_signature_get_kind(sig)), checksum); } g_info("version: %s", fu_firmware_get_version(FU_FIRMWARE(dbx))); return EXIT_SUCCESS; } #ifdef HAVE_GETUID /* ensure root user */ if (getuid() != 0 || geteuid() != 0) { /* TRANSLATORS: we're poking around as a power user */ g_printerr("%s\n", _("This program may only work correctly as root")); } #endif /* apply update */ if (action_apply) { FuEfivars *efivars = fu_context_get_efivars(ctx); g_autoptr(FuFirmware) dbx_system = NULL; g_autoptr(FuFirmware) dbx_update = fu_efi_signature_list_new(); g_autoptr(GBytes) blob = NULL; if (dbxfile == NULL) { /* TRANSLATORS: user did not include a filename parameter */ g_printerr("%s\n", _("Filename required")); return EXIT_FAILURE; } /* TRANSLATORS: reading existing dbx from the system */ g_print("%s\n", _("Parsing system dbx…")); dbx_system = fu_dbxtool_get_siglist_system(ctx, &error); if (dbx_system == NULL) { /* TRANSLATORS: could not read existing system data */ g_printerr("%s: %s\n", _("Failed to load system dbx"), error->message); return EXIT_FAILURE; } /* TRANSLATORS: reading new dbx from the update */ g_print("%s\n", _("Parsing dbx update…")); blob = fu_bytes_get_contents(dbxfile, &error); if (blob == NULL) { /* TRANSLATORS: could not read file */ g_printerr("%s: %s\n", _("Failed to load local dbx"), error->message); return EXIT_FAILURE; } if (!fu_firmware_parse_bytes(dbx_update, blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error)) { /* TRANSLATORS: could not parse file */ g_printerr("%s: %s\n", _("Failed to parse local dbx"), error->message); return EXIT_FAILURE; } /* check this is a newer dbx update */ if (!force && fu_dbxtool_siglist_inclusive(dbx_system, dbx_update)) { g_printerr("%s\n", /* TRANSLATORS: same or newer update already applied */ _("Cannot apply as dbx update has already been applied.")); return EXIT_FAILURE; } /* validate this is safe to apply */ if (!force) { /* TRANSLATORS: ESP refers to the EFI System Partition */ g_print("%s\n", _("Validating ESP contents…")); if (!fu_uefi_dbx_signature_list_validate(ctx, FU_EFI_SIGNATURE_LIST(dbx_update), FU_FIRMWARE_PARSE_FLAG_NONE, &error)) { g_printerr("%s: %s\n", /* TRANSLATORS: something with a blocked hash exists * in the users ESP -- which would be bad! */ _("Failed to validate ESP contents"), error->message); return EXIT_FAILURE; } } /* TRANSLATORS: actually sending the update to the hardware */ g_print("%s\n", _("Applying update…")); if (!fu_efivars_set_data_bytes( efivars, FU_EFIVARS_GUID_SECURITY_DATABASE, "dbx", blob, FU_EFIVARS_ATTR_APPEND_WRITE | FU_EFIVARS_ATTR_TIME_BASED_AUTHENTICATED_WRITE_ACCESS | FU_EFIVARS_ATTR_RUNTIME_ACCESS | FU_EFIVARS_ATTR_BOOTSERVICE_ACCESS | FU_EFIVARS_ATTR_NON_VOLATILE, &error)) { /* TRANSLATORS: dbx file failed to be applied as an update */ g_printerr("%s: %s\n", _("Failed to apply update"), error->message); return EXIT_FAILURE; } /* TRANSLATORS: success */ g_print("%s\n", _("Done!")); return EXIT_SUCCESS; } /* nothing specified */ tmp = g_option_context_get_help(context, TRUE, NULL); /* TRANSLATORS: user did not tell the tool what to do */ g_printerr("%s\n\n%s", _("No action specified!"), tmp); return EXIT_FAILURE; } fwupd-2.0.10/plugins/uefi-dbx/fu-self-test.c000066400000000000000000000603701501337203100205660ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fwupd-error.h" #include "fu-context-private.h" #include "fu-device-private.h" #include "fu-plugin-private.h" #include "fu-uefi-dbx-device.h" #include "fu-uefi-dbx-plugin.h" #include "fu-uefi-device-private.h" static void fu_efi_image_func(void) { struct { const gchar *basename; const gchar *checksum; } map[] = { {"bootmgr.efi", "fd26aad248cc1e21e0c6b453212b2b309f7e221047bf22500ed0f8ce30bd1610"}, {"fwupdx64-2.efi", "6e0f01e7018c90a1e3d24908956fbeffd29a620c6c5f3ffa3feb2f2802ed4448"}, }; for (guint i = 0; i < G_N_ELEMENTS(map); i++) { gboolean ret; g_autofree gchar *csum = NULL; g_autofree gchar *fn = NULL; g_autoptr(FuFirmware) firmware = fu_pefile_firmware_new(); g_autoptr(GError) error = NULL; g_autoptr(GFile) file = NULL; fn = g_test_build_filename(G_TEST_DIST, "tests", map[i].basename, NULL); file = g_file_new_for_path(fn); if (!g_file_query_exists(file, NULL)) { g_autofree gchar *msg = g_strdup_printf("failed to find file %s", map[i].basename); g_test_skip(msg); return; } ret = fu_firmware_parse_file(firmware, file, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); csum = fu_firmware_get_checksum(firmware, G_CHECKSUM_SHA256, &error); g_assert_no_error(error); g_assert_nonnull(csum); g_assert_cmpstr(csum, ==, map[i].checksum); } } typedef struct { gboolean running_in_snap; gboolean snapd_fde_detected; gboolean snapd_supported; const gchar *mock_snapd_scenario; } FuTestCase; typedef struct { FuContext *ctx; gboolean mock_snapd_available; CURL *mock_snapd_curl; struct curl_slist *mock_curl_hdrs; } FuTestFixture; static gboolean fu_self_test_mock_snapd_easy_post_request(FuTestFixture *fixture, const gchar *endpoint, const gchar *data, gsize len) { CURL *curl = curl_easy_duphandle(fixture->mock_snapd_curl); CURLcode res = -1; glong status_code = 0; g_autofree gchar *endpoint_str = g_strdup_printf("http://localhost%s", endpoint); g_debug("mock snapd request to %s with data: '%s'", endpoint_str, data); /* use snap dedicated socket when running inside a snap */ (void)curl_easy_setopt(curl, CURLOPT_URL, endpoint_str); (void)curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, len); (void)curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data); res = curl_easy_perform(curl); g_debug("curl error: %u %s", res, curl_easy_strerror(res)); curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status_code); curl_easy_cleanup(curl); return res == CURLE_OK && status_code == 200; } static size_t fu_self_test_curl_write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) { GByteArray *bufarr = (GByteArray *)userdata; gsize sz = size * nmemb; g_byte_array_append(bufarr, (const guint8 *)ptr, sz); return sz; } static GBytes * fu_self_test_mock_snapd_easy_get_request(FuTestFixture *fixture, const gchar *endpoint) { CURL *curl = curl_easy_duphandle(fixture->mock_snapd_curl); CURLcode res = -1; glong status_code = 0; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autofree gchar *endpoint_str = g_strdup_printf("http://localhost%s", endpoint); /* use snap dedicated socket when running inside a snap */ (void)curl_easy_setopt(curl, CURLOPT_URL, endpoint_str); (void)curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fu_self_test_curl_write_callback); (void)curl_easy_setopt(curl, CURLOPT_WRITEDATA, buf); res = curl_easy_perform(curl); g_debug("curl error: %u %s", res, curl_easy_strerror(res)); curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status_code); curl_easy_cleanup(curl); g_assert_true(res == CURLE_OK); g_assert_true(status_code == 200); g_debug("rsp:\n%s", buf->data); return g_bytes_new(buf->data, buf->len); } typedef struct { guint startup; guint prepare; guint cleanup; } FuTestSnapdCalls; static void fu_self_test_mock_snapd_assert_calls(FuTestFixture *fixture, FuTestSnapdCalls calls) { guint64 val = 0xffffff; g_autoptr(GBytes) rsp = NULL; g_autoptr(GKeyFile) kf = g_key_file_new(); g_autoptr(GError) error = NULL; rsp = fu_self_test_mock_snapd_easy_get_request(fixture, "/test/stats"); g_key_file_load_from_bytes(kf, rsp, 0, &error); g_assert_no_error(error); val = g_key_file_get_uint64(kf, "stats", "efi-secureboot-update-startup", &error); g_assert_no_error(error); g_assert_cmpuint(val, ==, calls.startup); val = g_key_file_get_uint64(kf, "stats", "efi-secureboot-update-db-prepare", &error); g_assert_no_error(error); g_assert_cmpuint(val, ==, calls.prepare); val = g_key_file_get_uint64(kf, "stats", "efi-secureboot-update-db-cleanup", &error); g_assert_no_error(error); g_assert_cmpuint(val, ==, calls.cleanup); } static gboolean fu_self_test_mock_snapd_reset(FuTestFixture *fixture) { return fu_self_test_mock_snapd_easy_post_request(fixture, "/test/reset", NULL, 0); } static gboolean fu_self_test_mock_snapd_setup_scenario(FuTestFixture *fixture, const gchar *scenario) { g_autofree gchar *scenario_request = g_strdup_printf("{\"scenario\":\"%s\"}", scenario); return fu_self_test_mock_snapd_easy_post_request(fixture, "/test/setup", scenario_request, strlen(scenario_request)); } static gboolean fu_self_test_mock_snapd_init(FuTestFixture *fixture) { CURL *curl = curl_easy_init(); struct curl_slist *req_hdrs = NULL; const char *mock_snapd_snap_socket = g_getenv("FWUPD_SNAPD_SNAP_SOCKET"); g_assert_nonnull(mock_snapd_snap_socket); /* use snap dedicated socket when running inside a snap */ (void)curl_easy_setopt(curl, CURLOPT_UNIX_SOCKET_PATH, mock_snapd_snap_socket); (void)curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); req_hdrs = curl_slist_append(req_hdrs, "Content-Type: application/json"); (void)curl_easy_setopt(curl, CURLOPT_HTTPHEADER, req_hdrs); fixture->mock_snapd_curl = curl; fixture->mock_curl_hdrs = req_hdrs; return fu_self_test_mock_snapd_reset(fixture); } static void fu_self_test_set_up(FuTestFixture *fixture, gconstpointer user_data) { FuTestCase *tc = (FuTestCase *)user_data; gboolean ret; g_autoptr(GError) error = NULL; fixture->ctx = fu_context_new(); if ((tc->running_in_snap || tc->snapd_fde_detected) && fu_self_test_mock_snapd_init(fixture)) { fixture->mock_snapd_available = TRUE; if (tc->running_in_snap) (void)g_setenv("SNAP", "fwupd", TRUE); if (tc->snapd_fde_detected) fu_context_add_flag(fixture->ctx, FU_CONTEXT_FLAG_FDE_SNAPD); fu_self_test_mock_snapd_setup_scenario(fixture, tc->mock_snapd_scenario); } fu_context_add_flag(fixture->ctx, FU_CONTEXT_FLAG_INHIBIT_VOLUME_MOUNT); ret = fu_context_load_quirks(fixture->ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE | FU_QUIRKS_LOAD_FLAG_NO_VERIFY, &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_self_test_tear_down(FuTestFixture *fixture, gconstpointer user_data) { FuTestCase *tc = (FuTestCase *)user_data; if (tc->running_in_snap) g_unsetenv("SNAP"); if (tc->running_in_snap || tc->snapd_fde_detected) { if (fixture->mock_snapd_available) fu_self_test_mock_snapd_reset(fixture); if (fixture->mock_snapd_curl) curl_easy_cleanup(fixture->mock_snapd_curl); if (fixture->mock_curl_hdrs) curl_slist_free_all(fixture->mock_curl_hdrs); } g_object_unref(fixture->ctx); } static gboolean fu_test_mock_efivar_content(FuEfivars *efivars, const gchar *guid, const gchar *name, const gchar *path, GError **error) { gchar *mock_blob = NULL; gsize mock_blob_size = 0; g_autoptr(GBytes) mock_bytes = NULL; if (!g_file_get_contents(path, &mock_blob, &mock_blob_size, error)) return FALSE; mock_bytes = g_bytes_new_take(mock_blob, mock_blob_size); return fu_efivars_set_data_bytes(efivars, guid, name, mock_bytes, 0, error); } static gboolean fu_test_mock_dbx_efivars(FuEfivars *efivars, GError **error) { g_autofree gchar *mock_kek_path = g_test_build_filename(G_TEST_DIST, "tests/KEK-8be4df61-93ca-11d2-aa0d-00e098032b8c", NULL); g_autofree gchar *mock_dbx_path = g_test_build_filename(G_TEST_DIST, "tests/dbx-d719b2cb-3d3a-4596-a3bc-dad00e67656f", NULL); if (!fu_test_mock_efivar_content(efivars, FU_EFIVARS_GUID_EFI_GLOBAL, "KEK", mock_kek_path, error)) return FALSE; return fu_test_mock_efivar_content(efivars, FU_EFIVARS_GUID_SECURITY_DATABASE, "dbx", mock_dbx_path, error); } static FuFirmware * fu_test_mock_dbx_update_firmware(void) { gboolean ret; gchar *mock_blob = NULL; gsize mock_blob_size = 0; g_autoptr(GError) error = NULL; g_autoptr(GBytes) mock_bytes = NULL; g_autofree gchar *mock_dbx_update_path = g_test_build_filename(G_TEST_DIST, "tests/dbx-update.auth", NULL); ret = g_file_get_contents(mock_dbx_update_path, &mock_blob, &mock_blob_size, &error); g_assert_no_error(error); g_assert_true(ret); mock_bytes = g_bytes_new_take(mock_blob, mock_blob_size); return fu_firmware_new_from_bytes(mock_bytes); } static void fu_uefi_dbx_test_plugin_update(FuTestFixture *fixture, gconstpointer user_data) { /* run though an update */ FuTestCase *tc = (FuTestCase *)user_data; gboolean ret; FuContext *ctx = fixture->ctx; FuEfivars *efivars = fu_context_get_efivars(ctx); g_autoptr(GError) error = NULL; g_autoptr(FuFirmware) firmware = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FuPlugin) plugin = fu_plugin_new_from_gtype(fu_uefi_dbx_plugin_get_type(), ctx); g_autoptr(FuUefiDbxDevice) uefi_device = NULL; gboolean expect_snapd_calls = tc->running_in_snap || tc->snapd_fde_detected; /* progress */ fu_progress_add_flag(progress, FU_PROGRESS_FLAG_NO_PROFILE); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 33, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 33, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 33, NULL); if (expect_snapd_calls && !fixture->mock_snapd_available) { g_test_skip("mock snapd not available"); return; } if (!fu_test_mock_dbx_efivars(efivars, &error) && g_error_matches(error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) { g_test_skip("test assets unavailable"); return; } g_assert_no_error(error); ret = fu_plugin_runner_startup(plugin, fu_progress_get_child(progress), &error); g_assert_no_error(error); g_assert_true(ret); fu_progress_step_done(progress); uefi_device = g_object_new(FU_TYPE_UEFI_DBX_DEVICE, "context", ctx, NULL); fu_uefi_device_set_guid(FU_UEFI_DEVICE(uefi_device), FU_EFIVARS_GUID_EFI_GLOBAL); fu_uefi_device_set_name(FU_UEFI_DEVICE(uefi_device), "KEK"); ret = fu_plugin_runner_device_created(plugin, FU_DEVICE(uefi_device), &error); g_assert_no_error(error); g_assert_true(ret); firmware = fu_test_mock_dbx_update_firmware(); ret = fu_plugin_runner_write_firmware(plugin, FU_DEVICE(uefi_device), firmware, fu_progress_get_child(progress), /* skip verification of ESP binaries*/ FWUPD_INSTALL_FLAG_FORCE, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_true(fu_device_has_flag(FU_DEVICE(uefi_device), FWUPD_DEVICE_FLAG_NEEDS_REBOOT)); fu_progress_step_done(progress); /* this is normally invoked by FuEngine */ ret = fu_device_cleanup(FU_DEVICE(uefi_device), fu_progress_get_child(progress), 0, &error); g_assert_true(ret); g_assert_no_error(error); fu_progress_step_done(progress); if (expect_snapd_calls) { fu_self_test_mock_snapd_assert_calls(fixture, (FuTestSnapdCalls){ .startup = 1, .prepare = 1, .cleanup = 1, }); } } static void fu_uefi_dbx_test_plugin_failed_update(FuTestFixture *fixture, gconstpointer user_data) { /* update which fails at either write or cleanup steps, this test can only * properly mock the environment when using snapd integration */ FuTestCase *tc = (FuTestCase *)user_data; gboolean ret; FuContext *ctx = fixture->ctx; FuEfivars *efivars = fu_context_get_efivars(ctx); g_autoptr(GError) error = NULL; g_autoptr(FuFirmware) firmware = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FuProgress) progress_write = fu_progress_new(G_STRLOC); g_autoptr(FuPlugin) plugin = fu_plugin_new_from_gtype(fu_uefi_dbx_plugin_get_type(), ctx); g_autoptr(FuUefiDbxDevice) uefi_device = NULL; if (!tc->running_in_snap && !tc->snapd_fde_detected) { g_test_skip("only supports snapd integration variant"); return; } if (!fixture->mock_snapd_available) { g_test_skip("mock snapd not available"); return; } if (!fu_test_mock_dbx_efivars(efivars, &error) && g_error_matches(error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) { g_test_skip("test assets unavailable"); return; } g_assert_no_error(error); ret = fu_plugin_runner_startup(plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); uefi_device = g_object_new(FU_TYPE_UEFI_DBX_DEVICE, "context", ctx, NULL); fu_uefi_device_set_guid(FU_UEFI_DEVICE(uefi_device), FU_EFIVARS_GUID_EFI_GLOBAL); fu_uefi_device_set_name(FU_UEFI_DEVICE(uefi_device), "KEK"); ret = fu_plugin_runner_device_created(plugin, FU_DEVICE(uefi_device), &error); g_assert_no_error(error); g_assert_true(ret); firmware = fu_test_mock_dbx_update_firmware(); ret = fu_plugin_runner_write_firmware(plugin, FU_DEVICE(uefi_device), firmware, progress_write, /* skip verification of ESP binaries*/ FWUPD_INSTALL_FLAG_FORCE, &error); if (g_str_equal(tc->mock_snapd_scenario, "failed-prepare")) { g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL); g_clear_error(&error); } else { g_assert_no_error(error); g_assert_true(ret); } /* engine invokes cleanup */ ret = fu_device_cleanup(FU_DEVICE(uefi_device), progress, 0, &error); if (g_str_equal(tc->mock_snapd_scenario, "failed-cleanup")) { g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL); } else { g_assert_no_error(error); g_assert_true(ret); } fu_self_test_mock_snapd_assert_calls(fixture, (FuTestSnapdCalls){ .startup = 1, .prepare = 1, .cleanup = 1, }); } static void fu_uefi_dbx_test_plugin_coldplug_probed_device(FuTestFixture *fixture, gconstpointer user_data) { FuTestCase *tc = (FuTestCase *)user_data; gboolean ret; FuContext *ctx = fixture->ctx; FuEfivars *efivars = fu_context_get_efivars(ctx); g_autoptr(GError) error = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FuPlugin) plugin = fu_plugin_new_from_gtype(fu_uefi_dbx_plugin_get_type(), ctx); g_autoptr(FuUefiDbxDevice) uefi_device = NULL; gboolean expect_snapd_calls = tc->running_in_snap || tc->snapd_fde_detected; if (expect_snapd_calls && !fixture->mock_snapd_available) { g_test_skip("mock snapd not available"); return; } if (!fu_test_mock_dbx_efivars(efivars, &error) && g_error_matches(error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) { g_test_skip("test assets unavailable"); return; } g_assert_no_error(error); ret = fu_plugin_runner_startup(plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); uefi_device = g_object_new(FU_TYPE_UEFI_DBX_DEVICE, "context", ctx, NULL); fu_uefi_device_set_guid(FU_UEFI_DEVICE(uefi_device), FU_EFIVARS_GUID_EFI_GLOBAL); fu_uefi_device_set_name(FU_UEFI_DEVICE(uefi_device), "KEK"); ret = fu_plugin_runner_device_created(plugin, FU_DEVICE(uefi_device), &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_device_has_inhibit(FU_DEVICE(uefi_device), "no-snapd-dbx"); if (expect_snapd_calls && tc->snapd_supported && g_str_equal(tc->mock_snapd_scenario, "failed-startup")) { /* startup failed for whatever reason, device updates are inhibited */ g_assert_true(ret); } else g_assert_false(ret); if (expect_snapd_calls) { fu_self_test_mock_snapd_assert_calls(fixture, (FuTestSnapdCalls){ .startup = 1, }); } } static void fu_uefi_dbx_test_plugin_startup(FuTestFixture *fixture, gconstpointer user_data) { FuTestCase *tc = (FuTestCase *)user_data; gboolean ret; FuContext *ctx = fixture->ctx; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(FuPlugin) plugin = fu_plugin_new_from_gtype(fu_uefi_dbx_plugin_get_type(), ctx); if (tc->running_in_snap && !fixture->mock_snapd_available) { g_test_skip("mock snapd not available"); return; } ret = fu_context_load_quirks(ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE | FU_QUIRKS_LOAD_FLAG_NO_VERIFY, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_plugin_runner_startup(plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); if (tc->running_in_snap) { fu_self_test_mock_snapd_assert_calls(fixture, (FuTestSnapdCalls){ .startup = 1, }); } } int main(int argc, char **argv) { FuTestCase simple = { .running_in_snap = FALSE, }; /* test variants with snapd, see tests/snapd.py for what a specific scenario * supports */ FuTestCase running_in_snap = { .running_in_snap = TRUE, .snapd_supported = TRUE, .mock_snapd_scenario = "happy-startup", }; FuTestCase snapd_fde_detected = { .snapd_fde_detected = TRUE, .snapd_supported = TRUE, .mock_snapd_scenario = "happy-startup", }; FuTestCase running_in_snap_snapd_fde_detected = { .snapd_fde_detected = TRUE, .running_in_snap = TRUE, .snapd_supported = TRUE, .mock_snapd_scenario = "happy-startup", }; FuTestCase running_in_snap_no_support = { .running_in_snap = TRUE, .snapd_supported = FALSE, .mock_snapd_scenario = "not-supported", }; FuTestCase snapd_fde_detected_no_support = { .snapd_fde_detected = TRUE, .snapd_supported = FALSE, .mock_snapd_scenario = "not-supported", }; FuTestCase running_in_snap_bad_startup = { .running_in_snap = TRUE, .snapd_supported = TRUE, .mock_snapd_scenario = "failed-startup", }; FuTestCase snapd_fde_detected_bad_startup = { .snapd_fde_detected = TRUE, .snapd_supported = TRUE, .mock_snapd_scenario = "failed-startup", }; FuTestCase running_in_snap_update = { .running_in_snap = TRUE, .snapd_supported = TRUE, .mock_snapd_scenario = "happy-update", }; FuTestCase snapd_fde_detected_update = { .snapd_fde_detected = TRUE, .snapd_supported = TRUE, .mock_snapd_scenario = "happy-update", }; FuTestCase running_in_snap_update_failed_prepare = { .running_in_snap = TRUE, .snapd_supported = TRUE, .mock_snapd_scenario = "failed-prepare", }; FuTestCase snapd_fde_detected_update_failed_prepare = { .snapd_fde_detected = TRUE, .snapd_supported = TRUE, .mock_snapd_scenario = "failed-prepare", }; FuTestCase running_in_snap_update_failed_cleanup = { .running_in_snap = TRUE, .snapd_supported = TRUE, .mock_snapd_scenario = "failed-cleanup", }; FuTestCase snapd_fde_detected_update_failed_cleanup = { .snapd_fde_detected = TRUE, .snapd_supported = TRUE, .mock_snapd_scenario = "failed-cleanup", }; g_autofree gchar *testdatadir = NULL; (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); (void)g_setenv("G_MESSAGES_DEBUG", "all", TRUE); testdatadir = g_test_build_filename(G_TEST_DIST, "tests", NULL); (void)g_setenv("FWUPD_SYSFSFWDIR", testdatadir, TRUE); (void)g_setenv("FWUPD_EFIVARS", "dummy", TRUE); (void)g_setenv("FWUPD_SYSFSDRIVERDIR", testdatadir, TRUE); (void)g_setenv("FWUPD_SYSFSFWATTRIBDIR", testdatadir, TRUE); (void)g_setenv("FWUPD_SNAPD_SNAP_SOCKET", "/tmp/mock-snapd-test.sock", TRUE); /* tests go here */ g_test_add_func("/uefi-dbx/image", fu_efi_image_func); g_test_add("/uefi-dbx/startup", FuTestFixture, &simple, fu_self_test_set_up, fu_uefi_dbx_test_plugin_startup, fu_self_test_tear_down); g_test_add("/uefi-dbx/update", FuTestFixture, &simple, fu_self_test_set_up, fu_uefi_dbx_test_plugin_update, fu_self_test_tear_down); g_test_add("/uefi-dbx/startup/snapd/running-in-snap/supported", FuTestFixture, &running_in_snap, fu_self_test_set_up, fu_uefi_dbx_test_plugin_startup, fu_self_test_tear_down); g_test_add("/uefi-dbx/startup/snapd/fde-detected/supported", FuTestFixture, &snapd_fde_detected, fu_self_test_set_up, fu_uefi_dbx_test_plugin_startup, fu_self_test_tear_down); g_test_add("/uefi-dbx/startup/snapd/running-in-snap-and-fde-detected/supported", FuTestFixture, &running_in_snap_snapd_fde_detected, fu_self_test_set_up, fu_uefi_dbx_test_plugin_startup, fu_self_test_tear_down); g_test_add("/uefi-dbx/startup/snapd/running-in-snap/not-supported", FuTestFixture, &running_in_snap_no_support, fu_self_test_set_up, fu_uefi_dbx_test_plugin_startup, fu_self_test_tear_down); g_test_add("/uefi-dbx/startup/snapd/fde-detected/not-supported", FuTestFixture, &snapd_fde_detected_no_support, fu_self_test_set_up, fu_uefi_dbx_test_plugin_startup, fu_self_test_tear_down); g_test_add("/uefi-dbx/startup/snapd/running-in-snap/supported-failure", FuTestFixture, &running_in_snap_bad_startup, fu_self_test_set_up, fu_uefi_dbx_test_plugin_startup, fu_self_test_tear_down); g_test_add("/uefi-dbx/startup/snapd/fde-detected/supported-failure", FuTestFixture, &snapd_fde_detected_bad_startup, fu_self_test_set_up, fu_uefi_dbx_test_plugin_startup, fu_self_test_tear_down); g_test_add("/uefi-dbx/coldplug/with-device", FuTestFixture, &simple, fu_self_test_set_up, fu_uefi_dbx_test_plugin_coldplug_probed_device, fu_self_test_tear_down); g_test_add("/uefi-dbx/coldplug/snapd/running-in-snap/supported", FuTestFixture, &running_in_snap, fu_self_test_set_up, fu_uefi_dbx_test_plugin_coldplug_probed_device, fu_self_test_tear_down); g_test_add("/uefi-dbx/coldplug/snapd/fde-detected/supported", FuTestFixture, &snapd_fde_detected, fu_self_test_set_up, fu_uefi_dbx_test_plugin_coldplug_probed_device, fu_self_test_tear_down); g_test_add("/uefi-dbx/coldplug/snapd/running-in-snap/not-supported", FuTestFixture, &running_in_snap_no_support, fu_self_test_set_up, fu_uefi_dbx_test_plugin_coldplug_probed_device, fu_self_test_tear_down); g_test_add("/uefi-dbx/coldplug/snapd/fde-detected/not-supported", FuTestFixture, &snapd_fde_detected_no_support, fu_self_test_set_up, fu_uefi_dbx_test_plugin_coldplug_probed_device, fu_self_test_tear_down); g_test_add("/uefi-dbx/coldplug/snapd/running-in-snap/supported-failure", FuTestFixture, &running_in_snap_bad_startup, fu_self_test_set_up, fu_uefi_dbx_test_plugin_coldplug_probed_device, fu_self_test_tear_down); g_test_add("/uefi-dbx/coldplug/snapd/fde-detected/supported-failure", FuTestFixture, &snapd_fde_detected_bad_startup, fu_self_test_set_up, fu_uefi_dbx_test_plugin_coldplug_probed_device, fu_self_test_tear_down); g_test_add("/uefi-dbx/update/snapd/running-in-snap/success", FuTestFixture, &running_in_snap_update, fu_self_test_set_up, fu_uefi_dbx_test_plugin_update, fu_self_test_tear_down); g_test_add("/uefi-dbx/update/snapd/fde-detected/success", FuTestFixture, &snapd_fde_detected_update, fu_self_test_set_up, fu_uefi_dbx_test_plugin_update, fu_self_test_tear_down); g_test_add("/uefi-dbx/update/snapd/running-in-snap/failed-prepare", FuTestFixture, &running_in_snap_update_failed_prepare, fu_self_test_set_up, fu_uefi_dbx_test_plugin_failed_update, fu_self_test_tear_down); g_test_add("/uefi-dbx/update/snapd/fde-detected/failed-prepare", FuTestFixture, &snapd_fde_detected_update_failed_prepare, fu_self_test_set_up, fu_uefi_dbx_test_plugin_failed_update, fu_self_test_tear_down); g_test_add("/uefi-dbx/update/snapd/running-in-snap/failed-cleanup", FuTestFixture, &running_in_snap_update_failed_cleanup, fu_self_test_set_up, fu_uefi_dbx_test_plugin_failed_update, fu_self_test_tear_down); g_test_add("/uefi-dbx/update/snapd/fde-detected/failed-cleanup", FuTestFixture, &snapd_fde_detected_update_failed_cleanup, fu_self_test_set_up, fu_uefi_dbx_test_plugin_failed_update, fu_self_test_tear_down); return g_test_run(); } fwupd-2.0.10/plugins/uefi-dbx/fu-uefi-dbx-common.c000066400000000000000000000056651501337203100216570ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #ifdef HAVE_UTSNAME_H #include #endif #include "fu-uefi-dbx-common.h" const gchar * fu_uefi_dbx_get_efi_arch(void) { #ifdef HAVE_UTSNAME_H struct utsname name_tmp; struct { const gchar *arch; const gchar *arch_efi; } map[] = { {"x86", "ia32"}, {"x86_64", "x64"}, {"arm", "arm"}, {"aarch64", "aa64"}, {"loongarch64", "loongarch64"}, {"riscv64", "riscv64"}, }; memset(&name_tmp, 0, sizeof(struct utsname)); if (uname(&name_tmp) < 0) return NULL; for (guint i = 0; i < G_N_ELEMENTS(map); i++) { if (g_strcmp0(name_tmp.machine, map[i].arch) == 0) return map[i].arch_efi; } #endif return NULL; } static gchar * fu_uefi_dbx_get_authenticode_hash(const gchar *fn, GError **error) { g_autoptr(FuFirmware) firmware = fu_pefile_firmware_new(); g_autoptr(GFile) file = g_file_new_for_path(fn); if (!fu_firmware_parse_file(firmware, file, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return NULL; return fu_firmware_get_checksum(firmware, G_CHECKSUM_SHA256, error); } static gboolean fu_uefi_dbx_signature_list_validate_filename(FuContext *ctx, FuEfiSignatureList *siglist, const gchar *fn, FuFirmwareParseFlags flags, GError **error) { g_autofree gchar *checksum = NULL; g_autoptr(FuFirmware) img = NULL; g_autoptr(GError) error_local = NULL; /* get checksum of file */ checksum = fu_uefi_dbx_get_authenticode_hash(fn, &error_local); if (checksum == NULL) { g_debug("failed to get checksum for %s: %s", fn, error_local->message); return TRUE; } /* Authenticode signature is present in dbx! */ g_debug("fn=%s, checksum=%s", fn, checksum); img = fu_firmware_get_image_by_checksum(FU_FIRMWARE(siglist), checksum, NULL); if (img != NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NEEDS_USER_ACTION, "%s Authenticode checksum [%s] is present in dbx", fn, checksum); return FALSE; } /* success */ return TRUE; } gboolean fu_uefi_dbx_signature_list_validate(FuContext *ctx, FuEfiSignatureList *siglist, FuFirmwareParseFlags flags, GError **error) { g_autoptr(GPtrArray) files = NULL; g_autoptr(GError) error_local = NULL; files = fu_context_get_esp_files(ctx, FU_CONTEXT_ESP_FILE_FLAG_INCLUDE_FIRST_STAGE | FU_CONTEXT_ESP_FILE_FLAG_INCLUDE_SECOND_STAGE, &error_local); if (files == NULL) { /* there is no BootOrder in CI */ if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) return TRUE; g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } for (guint i = 0; i < files->len; i++) { FuFirmware *firmware = g_ptr_array_index(files, i); if (!fu_uefi_dbx_signature_list_validate_filename( ctx, siglist, fu_firmware_get_filename(firmware), flags, error)) return FALSE; } return TRUE; } fwupd-2.0.10/plugins/uefi-dbx/fu-uefi-dbx-common.h000066400000000000000000000005451501337203100216540ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include const gchar * fu_uefi_dbx_get_efi_arch(void); gboolean fu_uefi_dbx_signature_list_validate(FuContext *ctx, FuEfiSignatureList *siglist, FuFirmwareParseFlags flags, GError **error); fwupd-2.0.10/plugins/uefi-dbx/fu-uefi-dbx-device.c000066400000000000000000000242161501337203100216170ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uefi-dbx-common.h" #include "fu-uefi-dbx-device.h" #include "fu-uefi-dbx-snapd-notifier.h" struct _FuUefiDbxDevice { FuUefiDevice parent_instance; FuUefiDbxSnapdNotifier *snapd_notifier; }; G_DEFINE_TYPE(FuUefiDbxDevice, fu_uefi_dbx_device, FU_TYPE_UEFI_DEVICE) void fu_uefi_dbx_device_set_snapd_notifier(FuUefiDbxDevice *self, FuUefiDbxSnapdNotifier *obs) { g_set_object(&self->snapd_notifier, obs); } static gboolean fu_uefi_dbx_device_maybe_notify_snapd_prepare(FuUefiDbxDevice *self, GBytes *data, GError **error) { if (self->snapd_notifier == NULL) return TRUE; return fu_uefi_dbx_snapd_notifier_dbx_update_prepare(self->snapd_notifier, data, error); } static gboolean fu_uefi_dbx_device_maybe_notify_snapd_cleanup(FuUefiDbxDevice *self, GError **error) { if (self->snapd_notifier == NULL) return TRUE; return fu_uefi_dbx_snapd_notifier_dbx_update_cleanup(self->snapd_notifier, error); } static gboolean fu_uefi_dbx_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags install_flags, GError **error) { g_autoptr(GBytes) fw = NULL; /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; if (!fu_uefi_dbx_device_maybe_notify_snapd_prepare(FU_UEFI_DBX_DEVICE(device), fw, error)) return FALSE; /* write entire chunk to efivarsfs */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); if (!fu_uefi_device_set_efivar_bytes( FU_UEFI_DEVICE(device), FU_EFIVARS_GUID_SECURITY_DATABASE, "dbx", fw, FU_EFIVARS_ATTR_APPEND_WRITE | FU_EFIVARS_ATTR_TIME_BASED_AUTHENTICATED_WRITE_ACCESS | FU_EFIVARS_ATTR_RUNTIME_ACCESS | FU_EFIVARS_ATTR_BOOTSERVICE_ACCESS | FU_EFIVARS_ATTR_NON_VOLATILE, error)) { return FALSE; } /* success! */ return TRUE; } static gboolean fu_uefi_dbx_device_set_checksum(FuUefiDbxDevice *self, const gchar *csum, GError **error) { /* used for md-set-version, but only when the device is added to the daemon */ fu_device_add_checksum(FU_DEVICE(self), csum); /* for operating offline, with no /usr/share/fwupd/remotes.d archives */ fu_device_add_instance_strup(FU_DEVICE(self), "CSUM", csum); if (!fu_device_build_instance_id_full(FU_DEVICE(self), FU_DEVICE_INSTANCE_FLAG_QUIRKS, error, "UEFI", "CSUM", NULL)) return FALSE; /* this makes debugging easier */ if (fu_device_get_version(FU_DEVICE(self)) == NULL) { g_autofree gchar *csum_trunc = g_strndup(csum, 8); g_autofree gchar *summary = g_strdup_printf("UEFI revocation database %s", csum_trunc); fu_device_set_summary(FU_DEVICE(self), summary); } /* success */ return TRUE; } static gboolean fu_uefi_dbx_device_ensure_checksum(FuUefiDbxDevice *self, GError **error) { g_autoptr(GBytes) dbx_blob = NULL; g_autoptr(FuFirmware) dbx = fu_efi_signature_list_new(); g_autoptr(GPtrArray) sigs = NULL; /* use the number of checksums in the dbx as a version number, ignoring * some owners that do not make sense */ dbx_blob = fu_uefi_device_get_efivar_bytes(FU_UEFI_DEVICE(self), FU_EFIVARS_GUID_SECURITY_DATABASE, "dbx", NULL, error); if (dbx_blob == NULL) return FALSE; if (!fu_firmware_parse_bytes(dbx, dbx_blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error)) return FALSE; /* add the last checksum to the device */ sigs = fu_firmware_get_images(dbx); if (sigs->len > 0) { FuEfiSignature *sig = g_ptr_array_index(sigs, sigs->len - 1); g_autofree gchar *csum = fu_firmware_get_checksum(FU_FIRMWARE(sig), G_CHECKSUM_SHA256, NULL); if (csum != NULL) { if (!fu_uefi_dbx_device_set_checksum(self, csum, error)) return FALSE; } } /* success */ return TRUE; } static gboolean fu_uefi_dbx_device_reload(FuDevice *device, GError **error) { FuUefiDbxDevice *self = FU_UEFI_DBX_DEVICE(device); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_VERSION); return fu_uefi_dbx_device_ensure_checksum(self, error); } static void fu_uefi_dbx_device_version_notify_cb(FuDevice *device, GParamSpec *pspec, gpointer user_data) { fu_device_set_version_lowest(device, fu_device_get_version(device)); } static FuFirmware * fu_uefi_dbx_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuContext *ctx = fu_device_get_context(device); g_autoptr(FuFirmware) firmware = fu_firmware_new(); g_autoptr(FuFirmware) siglist = fu_efi_signature_list_new(); /* parse dbx */ if (!fu_firmware_parse_stream(siglist, stream, 0x0, flags, error)) { g_prefix_error(error, "cannot parse DBX update: "); return NULL; } /* validate this is safe to apply */ if ((flags & FWUPD_INSTALL_FLAG_FORCE) == 0) { fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_VERIFY); if (!fu_uefi_dbx_signature_list_validate(ctx, FU_EFI_SIGNATURE_LIST(siglist), flags, error)) { g_prefix_error(error, "Blocked executable in the ESP, " "ensure grub and shim are up to date: "); return NULL; } } /* default blob */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; return g_steal_pointer(&firmware); } static gboolean fu_uefi_dbx_device_probe(FuDevice *device, GError **error) { FuUefiDbxDevice *self = FU_UEFI_DBX_DEVICE(device); FuContext *ctx = fu_device_get_context(device); g_autoptr(FuFirmware) kek = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GPtrArray) sigs = NULL; /* use each of the certificates in the KEK to generate the GUIDs */ kek = fu_device_read_firmware(device, progress, error); if (kek == NULL) { g_prefix_error(error, "failed to parse KEK: "); return FALSE; } fu_device_add_instance_strup(device, "ARCH", fu_uefi_dbx_get_efi_arch()); sigs = fu_firmware_get_images(kek); for (guint j = 0; j < sigs->len; j++) { FuEfiSignature *sig = g_ptr_array_index(sigs, j); g_autofree gchar *checksum = NULL; checksum = fu_firmware_get_checksum(FU_FIRMWARE(sig), G_CHECKSUM_SHA256, error); if (checksum == NULL) return FALSE; fu_device_add_instance_strup(device, "CRT", checksum); fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "UEFI", "CRT", NULL); fu_device_build_instance_id(device, NULL, "UEFI", "CRT", "ARCH", NULL); } /* dbx changes are expected to change PCR7, warn the user that BitLocker might ask for recovery key after fw update */ if (fu_context_has_flag(ctx, FU_CONTEXT_FLAG_FDE_BITLOCKER)) fu_device_add_flag(device, FWUPD_DEVICE_FLAG_AFFECTS_FDE); return fu_uefi_dbx_device_ensure_checksum(self, error); } static void fu_uefi_dbx_device_report_metadata_pre(FuDevice *device, GHashTable *metadata) { FuContext *ctx = fu_device_get_context(device); FuEfivars *efivars = fu_context_get_efivars(ctx); guint64 nvram_total = fu_efivars_space_used(efivars, NULL); if (nvram_total != G_MAXUINT64) { g_hash_table_insert(metadata, g_strdup("EfivarsNvramUsed"), g_strdup_printf("%" G_GUINT64_FORMAT, nvram_total)); } } static void fu_uefi_dbx_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static gboolean fu_uefi_dbx_device_cleanup(FuDevice *self, FuProgress *progress, FwupdInstallFlags flags, GError **error) { if (!fu_uefi_dbx_device_maybe_notify_snapd_cleanup(FU_UEFI_DBX_DEVICE(self), error)) return FALSE; return TRUE; } static void fu_uefi_dbx_device_init(FuUefiDbxDevice *self) { fu_device_set_physical_id(FU_DEVICE(self), "dbx"); fu_device_set_name(FU_DEVICE(self), "UEFI dbx"); fu_device_set_summary(FU_DEVICE(self), "UEFI revocation database"); fu_device_add_protocol(FU_DEVICE(self), "org.uefi.dbx2"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_NUMBER); fu_device_set_install_duration(FU_DEVICE(self), 1); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_EFI_SIGNATURE_LIST); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_APPLICATION_CERTIFICATE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_REBOOT); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_ONLY_VERSION_UPGRADE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_ONLY_CHECKSUM); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_VERSION); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE_CHILD); g_signal_connect(FWUPD_DEVICE(self), "notify::version", G_CALLBACK(fu_uefi_dbx_device_version_notify_cb), NULL); } static void fu_uefi_dbx_device_finalize(GObject *object) { FuUefiDbxDevice *self = FU_UEFI_DBX_DEVICE(object); if (self->snapd_notifier != NULL) g_object_unref(self->snapd_notifier); G_OBJECT_CLASS(fu_uefi_dbx_device_parent_class)->finalize(object); } static void fu_uefi_dbx_device_class_init(FuUefiDbxDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_uefi_dbx_device_probe; device_class->reload = fu_uefi_dbx_device_reload; device_class->write_firmware = fu_uefi_dbx_device_write_firmware; device_class->prepare_firmware = fu_uefi_dbx_device_prepare_firmware; device_class->set_progress = fu_uefi_dbx_device_set_progress; device_class->report_metadata_pre = fu_uefi_dbx_device_report_metadata_pre; device_class->cleanup = fu_uefi_dbx_device_cleanup; object_class->finalize = fu_uefi_dbx_device_finalize; } fwupd-2.0.10/plugins/uefi-dbx/fu-uefi-dbx-device.h000066400000000000000000000006771501337203100216310ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-uefi-dbx-snapd-notifier.h" #define FU_TYPE_UEFI_DBX_DEVICE (fu_uefi_dbx_device_get_type()) G_DECLARE_FINAL_TYPE(FuUefiDbxDevice, fu_uefi_dbx_device, FU, UEFI_DBX_DEVICE, FuUefiDevice) void fu_uefi_dbx_device_set_snapd_notifier(FuUefiDbxDevice *self, FuUefiDbxSnapdNotifier *obs); fwupd-2.0.10/plugins/uefi-dbx/fu-uefi-dbx-plugin.c000066400000000000000000000100411501337203100216450ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uefi-dbx-device.h" #include "fu-uefi-dbx-plugin.h" #include "fu-uefi-dbx-snapd-notifier.h" struct _FuUefiDbxPlugin { FuPlugin parent_instance; FuUefiDbxSnapdNotifier *snapd_notifier; gboolean snapd_integration_supported; }; G_DEFINE_TYPE(FuUefiDbxPlugin, fu_uefi_dbx_plugin, FU_TYPE_PLUGIN) static gboolean fu_uefi_dbx_plugin_device_created(FuPlugin *plugin, FuDevice *device, GError **error) { FuUefiDbxPlugin *self = FU_UEFI_DBX_PLUGIN(plugin); gboolean inhibited = FALSE; if (fu_context_has_hwid_flag(fu_plugin_get_context(plugin), "no-dbx-updates")) { fu_device_inhibit(FU_DEVICE(device), "no-dbx", "System firmware cannot accept DBX updates"); inhibited = TRUE; } if (self->snapd_notifier != NULL) { fu_uefi_dbx_device_set_snapd_notifier(FU_UEFI_DBX_DEVICE(device), self->snapd_notifier); } else if (!inhibited && self->snapd_integration_supported) { /* if snapd integration is supported, but we are unable to use the snapd notifier, then we should inhibit the update if it isn't already inhibited */ fu_device_inhibit(FU_DEVICE(device), "no-snapd-dbx", "Snapd integration for DBX update is not available"); } /* success */ return TRUE; } static void fu_uefi_dbx_plugin_init(FuUefiDbxPlugin *self) { } static void fu_uefi_dbx_plugin_finalize(GObject *object) { FuUefiDbxPlugin *self = FU_UEFI_DBX_PLUGIN(object); if (self->snapd_notifier != NULL) { g_object_unref(self->snapd_notifier); self->snapd_notifier = NULL; } G_OBJECT_CLASS(fu_uefi_dbx_plugin_parent_class)->finalize(object); } static gboolean fu_uefi_dbx_plugin_snapd_notify_init(FuUefiDbxPlugin *self, GError **error) { g_autoptr(FuUefiDbxSnapdNotifier) obs = fu_uefi_dbx_snapd_notifier_new(); if (!fu_uefi_dbx_snapd_notifier_dbx_manager_startup(obs, error)) return FALSE; g_set_object(&self->snapd_notifier, obs); return TRUE; } static void fu_uefi_dbx_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); g_autoptr(FuVolume) esp = NULL; g_autoptr(GError) error_udisks2 = NULL; FuUefiDbxPlugin *self = FU_UEFI_DBX_PLUGIN(plugin); FuContext *ctx = fu_plugin_get_context(plugin); fu_plugin_add_rule(plugin, FU_PLUGIN_RULE_METADATA_SOURCE, "uefi_capsule"); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_EFI_SIGNATURE_LIST); fu_plugin_add_device_gtype(plugin, FU_TYPE_UEFI_DBX_DEVICE); /* only enable snapd integration if either running inside a snap or we detect that this is a snapd FDE setup. either of these cases makes snapd integration mandatory */ if (fu_snap_is_in_snap() || fu_context_has_flag(ctx, FU_CONTEXT_FLAG_FDE_SNAPD)) { g_autoptr(GError) error_local = NULL; if (!fu_uefi_dbx_plugin_snapd_notify_init(FU_UEFI_DBX_PLUGIN(obj), &error_local)) { /* unless we got specific error code indicating lack of relevant APIs, snapd integration is considered to be supported, even if snapd itself cannot be reached */ self->snapd_integration_supported = !g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_info("snapd integration non-functional: %s", error_local->message); } else { g_info("snapd integration enabled"); self->snapd_integration_supported = TRUE; } } /* ensure that an ESP was found */ esp = fu_context_get_default_esp(fu_plugin_get_context(plugin), &error_udisks2); if (esp == NULL) { g_info("cannot find default ESP: %s", error_udisks2->message); fu_plugin_add_flag(plugin, FWUPD_PLUGIN_FLAG_ESP_NOT_FOUND); fu_plugin_add_flag(plugin, FWUPD_PLUGIN_FLAG_CLEAR_UPDATABLE); fu_plugin_add_flag(plugin, FWUPD_PLUGIN_FLAG_USER_WARNING); } } static void fu_uefi_dbx_plugin_class_init(FuUefiDbxPluginClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_uefi_dbx_plugin_constructed; plugin_class->device_created = fu_uefi_dbx_plugin_device_created; object_class->finalize = fu_uefi_dbx_plugin_finalize; } fwupd-2.0.10/plugins/uefi-dbx/fu-uefi-dbx-plugin.h000066400000000000000000000003611501337203100216560ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuUefiDbxPlugin, fu_uefi_dbx_plugin, FU, UEFI_DBX_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/uefi-dbx/fu-uefi-dbx-snapd-notifier.c000066400000000000000000000152071501337203100233020ustar00rootroot00000000000000/* * Copyright 2024 Maciej Borzecki * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fu-uefi-dbx-snapd-notifier.h" #include "glib.h" struct _FuUefiDbxSnapdNotifier { GObject parent_instance; CURL *curl_template; struct curl_slist *req_hdrs; }; G_DEFINE_TYPE(FuUefiDbxSnapdNotifier, fu_uefi_dbx_snapd_notifier, G_TYPE_OBJECT) G_DEFINE_AUTOPTR_CLEANUP_FUNC(CURL, curl_easy_cleanup); FuUefiDbxSnapdNotifier * fu_uefi_dbx_snapd_notifier_new(void) { return g_object_new(FU_TYPE_UEFI_DBX_SNAPD_NOTIFIER, NULL); } static void fu_uefi_dbx_snapd_notifier_init(FuUefiDbxSnapdNotifier *self) { /* default path is different inside the snap sandbox vs out */ const char *snapd_snap_socket = fu_snap_is_in_snap() ? "/run/snapd-snap.socket" : "/run/snapd.socket"; const char *snapd_snap_socket_override = g_getenv("FWUPD_SNAPD_SNAP_SOCKET"); self->curl_template = curl_easy_init(); if (snapd_snap_socket_override != NULL) snapd_snap_socket = snapd_snap_socket_override; /* use snap dedicated socket when running inside a snap */ (void)curl_easy_setopt(self->curl_template, CURLOPT_UNIX_SOCKET_PATH, snapd_snap_socket); self->req_hdrs = curl_slist_append(self->req_hdrs, "Content-Type: application/json"); (void)curl_easy_setopt(self->curl_template, CURLOPT_HTTPHEADER, self->req_hdrs); } static void fu_uefi_dbx_snapd_notifier_finalize(GObject *object) { FuUefiDbxSnapdNotifier *self = FU_UEFI_DBX_SNAPD_NOTIFIER(object); curl_slist_free_all(self->req_hdrs); curl_easy_cleanup(self->curl_template); G_OBJECT_CLASS(fu_uefi_dbx_snapd_notifier_parent_class)->finalize(object); } static void fu_uefi_dbx_snapd_notifier_class_init(FuUefiDbxSnapdNotifierClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_uefi_dbx_snapd_notifier_finalize; } /* see CURLOPT_WRITEFUNCTION(3) */ static size_t fu_uefi_dbx_snapd_notifier_rsp_cb(char *ptr, size_t size, size_t nmemb, void *userdata) { GByteArray *bufarr = (GByteArray *)userdata; gsize sz = size * nmemb; g_byte_array_append(bufarr, (const guint8 *)ptr, sz); return sz; } static gboolean fu_uefi_dbx_snapd_notifier_simple_req(FuUefiDbxSnapdNotifier *self, const char *endpoint, const char *data, gsize len, GError **error) { CURLcode res = -1; glong status_code = 0; g_autoptr(CURL) curl = NULL; g_autofree gchar *endpoint_str = NULL; g_autoptr(GByteArray) rsp_buf = g_byte_array_new(); /* duplicate a preconfigured curl handle */ curl = curl_easy_duphandle(self->curl_template); endpoint_str = g_strdup_printf("http://localhost%s", endpoint); (void)curl_easy_setopt(curl, CURLOPT_URL, endpoint_str); (void)curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, len); (void)curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data); /* collect response for debugging */ (void)curl_easy_setopt(curl, CURLOPT_WRITEDATA, rsp_buf); (void)curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fu_uefi_dbx_snapd_notifier_rsp_cb); res = curl_easy_perform(curl); if (res != CURLE_OK) { /* TODO inspect the error */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to communicate with snapd: %s", curl_easy_strerror(res)); return FALSE; } curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status_code); if (status_code == 404) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "snapd notification endpoint not supported by snapd API"); return FALSE; } if (status_code != 200) { g_autofree gchar *rsp = NULL; if (rsp_buf->len > 0) { /* make sure the response is printable */ rsp = fu_strsafe((const char *)rsp_buf->data, rsp_buf->len + 1); } /* TODO check whether the response is even printable? */ g_info("snapd request failed with status %ld, response: %s", (glong)status_code, rsp != NULL ? rsp : ""); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "snapd request failed with status %ld", (glong)status_code); return FALSE; } return TRUE; } /** * fu_uefi_dbx_snapd_notifier_dbx_manager_startup: * @self: a #FuUefiDbxSnapdNotifier * @error: (nullable): optional return location for an error * * Notify snapd of that the DBX manager has started. * * Returns: #TRUE if the notification was successful. **/ gboolean fu_uefi_dbx_snapd_notifier_dbx_manager_startup(FuUefiDbxSnapdNotifier *self, GError **error) { const char *startup_msg = "{\"action\":\"efi-secureboot-update-startup\"}"; if (!fu_uefi_dbx_snapd_notifier_simple_req(self, "/v2/system-secureboot", startup_msg, strlen(startup_msg), error)) { g_prefix_error(error, "failed to notify snapd of startup: "); return FALSE; } return TRUE; } /** * fu_uefi_dbx_snapd_notifier_dbx_update_prepare: * @self: a #FuUefiDbxSnapdNotifier * @fw_payload: payload used for the update * @error: (nullable): optional return location for an error * * Notify of an upcoming update to the DBX. A successful call shall initiate a * change tracking an update to the DBX on the snapd side. * * Returns: #TRUE if the notification was successful. **/ gboolean fu_uefi_dbx_snapd_notifier_dbx_update_prepare(FuUefiDbxSnapdNotifier *self, GBytes *fw_payload, GError **error) { gsize bufsz = 0; const guint8 *buf = g_bytes_get_data(fw_payload, &bufsz); g_autofree gchar *b64data = g_base64_encode(buf, bufsz); g_autofree gchar *msg = g_strdup_printf("{" "\"action\":\"efi-secureboot-update-db-prepare\"," "\"key-database\":\"DBX\"," "\"payload\":\"%s\"" "}", b64data); if (!fu_uefi_dbx_snapd_notifier_simple_req(self, "/v2/system-secureboot", msg, strlen(msg), error)) { g_prefix_error(error, "failed to notify snapd of prepare: "); return FALSE; } return TRUE; } /** * fu_uefi_dbx_snapd_notifier_dbx_update_cleanup: * @self: a #FuUefiDbxSnapdNotifier * @error: (nullable): optional return location for an error * * Notify of an completed update to one of secureboot key databases. A * successful call shall result in completion of a corresponding change on the * snapd side. * * Returns: #TRUE if the notification was successful. **/ gboolean fu_uefi_dbx_snapd_notifier_dbx_update_cleanup(FuUefiDbxSnapdNotifier *self, GError **error) { const char *msg = "{\"action\":\"efi-secureboot-update-db-cleanup\"}"; if (!fu_uefi_dbx_snapd_notifier_simple_req(self, "/v2/system-secureboot", msg, strlen(msg), error)) { g_prefix_error(error, "failed to notify snapd of cleanup: "); return FALSE; } return TRUE; } fwupd-2.0.10/plugins/uefi-dbx/fu-uefi-dbx-snapd-notifier.h000066400000000000000000000014361501337203100233060ustar00rootroot00000000000000/* * Copyright 2024 Maciej Borzecki * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_UEFI_DBX_SNAPD_NOTIFIER (fu_uefi_dbx_snapd_notifier_get_type()) G_DECLARE_FINAL_TYPE(FuUefiDbxSnapdNotifier, fu_uefi_dbx_snapd_notifier, FU, UEFI_DBX_SNAPD_NOTIFIER, GObject) FuUefiDbxSnapdNotifier * fu_uefi_dbx_snapd_notifier_new(void); gboolean fu_uefi_dbx_snapd_notifier_dbx_manager_startup(FuUefiDbxSnapdNotifier *self, GError **error); gboolean fu_uefi_dbx_snapd_notifier_dbx_update_prepare(FuUefiDbxSnapdNotifier *self, GBytes *fw_payload, GError **error); gboolean fu_uefi_dbx_snapd_notifier_dbx_update_cleanup(FuUefiDbxSnapdNotifier *self, GError **error); fwupd-2.0.10/plugins/uefi-dbx/meson.build000066400000000000000000000043431501337203100202440ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginUefiDbx"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('uefi-dbx.quirk') plugin_builtin_uefi_dbx = static_library('fu_plugin_uefi_dbx', sources: [ 'fu-uefi-dbx-plugin.c', 'fu-uefi-dbx-common.c', 'fu-uefi-dbx-device.c', 'fu-uefi-dbx-snapd-notifier.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: [ plugin_deps, libcurl, ], ) plugin_builtins += plugin_builtin_uefi_dbx device_tests += files('tests/uefi-dbx.json') if get_option('tests') install_data(['tests/snapd.py'], install_dir: join_paths(installed_test_datadir, 'tests')) env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) env.set('G_DEBUG', 'fatal-criticals') e = executable( 'uefi-dbx-self-test', sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: [ plugin_deps, libcurl, ], link_with: [ plugin_libs, plugin_builtin_uefi_dbx, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ cargs, '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('uefi-dbx-self-test', e, env: env) # added to installed-tests endif dbxtool = executable( 'dbxtool', sources: [ 'fu-dbxtool.c', ], include_directories: plugin_incdirs, dependencies: plugin_deps, link_with: [ plugin_libs, plugin_builtin_uefi_dbx, ], install: true, install_dir: bindir, install_rpath: libdir_pkg, c_args: cargs, ) if get_option('man') custom_target('dbxtool.1', input: 'dbxtool.md', output: 'dbxtool.1', command: [ generate_man, '@INPUT@', '-o', '@OUTPUT@', '--replace', 'PACKAGE_VERSION', fwupd_version, ], install: true, install_dir: join_paths(mandir, 'man1'), ) endif if build_docs md_targets += custom_target('dbxtool.md', input: 'dbxtool.md', output: 'dbxtool.md', command: [ generate_man, '@INPUT@', '-o', '@OUTPUT@', '--replace', 'PACKAGE_VERSION', fwupd_version, '--md', ], ) man_md += ['"dbxtool.md"'] endif fwupd-2.0.10/plugins/uefi-dbx/tests/000077500000000000000000000000001501337203100172405ustar00rootroot00000000000000fwupd-2.0.10/plugins/uefi-dbx/tests/snapd.py000077500000000000000000000177251501337203100207360ustar00rootroot00000000000000#!/usr/bin/env python3 # # Copyright 2024 Maciej Borzecki # # SPDX-License-Identifier: LGPL-2.1-or-later """The app provides a mock snapd API for use with fwupd unit tests. The /v2/system-secureboot endpoint mimics the behavior of snapd wrt. DBX updates. The app exposes a couple of test specific endpoinds, /test/scenario, which picks one of the scenarios (expected to be called in when setting a test fixture), /test/reset, for resetting the state (usually called in tear down), and /test/stats which provides the count of actions invoked since last reset. """ import argparse import logging import base64 import json import os.path from io import StringIO from dataclasses import dataclass from collections.abc import Callable from typing import Any, Optional from flask import Flask, Response, request, current_app @dataclass class Scenario: handler: Callable[[dict[str, Any]], Response] @dataclass class State: scenario: Optional[str] stats: dict[str, int] def __init__(self): self.scenario = None self.stats = {k: 0 for k in supported_actions} app = Flask(__name__) supported_actions = [ "efi-secureboot-update-startup", "efi-secureboot-update-db-cleanup", "efi-secureboot-update-db-prepare", ] def assert_no_extra_keys(d: dict[str, Any], allowed: list[str]): unexpected = [a for a in d.keys() if a not in allowed] assert len(unexpected) == 0, f"unexpected keys in request data: {unexpected}" def assert_prepare_req(req: dict[str, Any]): assert req.get("action") == "efi-secureboot-update-db-prepare", "unexpected action" assert "payload" in req, "missing payload field" assert "key-database" in req, "missing key-database field" assert req.get("key-database") == "DBX", "unexpected key database" payload = req.get("payload") assert_no_extra_keys(req, ["action", "payload", "key-database"]) assert payload is not None, "no update payload" # payload must be valid base64 try: raw_payload = base64.urlsafe_b64decode(payload) with open(os.path.join(current_app.datadir, "dbx-update.auth"), "rb") as inf: reference_payload = inf.read() assert raw_payload == reference_payload, "unexpected payload content" except Exception as err: raise AssertionError("invalid base64 data in request") from err def assert_startup_req(req: dict[str, Any]): assert req.get("action") == "efi-secureboot-update-startup", "unexpected action" assert_no_extra_keys(req, ["action"]) def assert_cleanup_req(req: dict[str, Any]): assert req.get("action") == "efi-secureboot-update-db-cleanup", "unexpected action" assert_no_extra_keys(req, ["action"]) def happy_startup(req: dict[str, Any]) -> Response: assert_startup_req(req) return Response(None, status=200) def not_supported(req: dict[str, Any]) -> Response: assert_startup_req(req) # pretend relevant APIs are missing, hence 404 return Response("", status=404) def failed_startup(req: dict[str, Any]) -> Response: assert_startup_req(req) return Response(None, status=400) def happy_prepare(req: dict[str, Any]) -> Response: assert_prepare_req(req) return Response("", status=200) def failed_prepare(req: dict[str, Any]) -> Response: action = req.get("action") if action == "efi-secureboot-update-db-cleanup": return happy_cleanup(req) if action == "efi-secureboot-update-db-prepare": assert_prepare_req(req) return Response( json.dumps( { "status": "500", "error": { "kind": "internal-error", "message": "cannot reseal keys in prepare", }, } ), status=500, ) if action == "efi-secureboot-update-startup": return happy_startup(req) raise AssertionError(f"unexpected action {action}") def failed_cleanup(req: dict[str, Any]) -> Response: action = req.get("action") if action == "efi-secureboot-update-db-cleanup": assert_cleanup_req(req) return Response( json.dumps( { "status": "500", "error": { "kind": "internal-error", "message": "cannot reseal keys in cleanup", }, } ), status=500, ) if action == "efi-secureboot-update-db-prepare": return happy_prepare(req) if action == "efi-secureboot-update-startup": return happy_startup(req) raise AssertionError(f"unexpected action {action}") def happy_cleanup(req: dict[str, Any]) -> Response: assert_cleanup_req(req) return Response("", status=200) def happy_update(req: dict[str, Any]) -> Response: action = req.get("action") if action == "efi-secureboot-update-db-cleanup": return happy_cleanup(req) if action == "efi-secureboot-update-db-prepare": return happy_prepare(req) if action == "efi-secureboot-update-startup": return happy_startup(req) raise AssertionError(f"unexpected action {action}") playbook: dict[str, Scenario] = { # successful startup "happy-startup": Scenario(handler=happy_startup), # startup with mock failure "failed-startup": Scenario(handler=failed_startup), # 404 on the API endpoint, indicating lack of support on the snapd side "not-supported": Scenario(handler=not_supported), # prepare step fails "failed-prepare": Scenario(handler=failed_prepare), # prepare is successful, but cleanup fails "failed-cleanup": Scenario(handler=failed_cleanup), # successful update cycle "happy-update": Scenario(handler=happy_update), } def assert_scenario(f): def do(): with app.app_context(): assert current_app.state.scenario is not None, "test scenario is not set" return f() return do def app_init_state(): with app.app_context(): current_app.state = State() @app.route("/v2/system-secureboot", methods=["POST"]) @assert_scenario def system_secureboot(): assert len(request.view_args) == 0 assert request.headers.get("Content-Type") == "application/json" req = request.get_json() logging.debug("req: %r", req) action = req.get("action") assert action in supported_actions, f"unknown action {action}" current_app.state.stats[action] += 1 return playbook[current_app.state.scenario].handler(req) @app.route("/test/setup", methods=["POST"]) def test_setup(): req = request.get_json() logging.debug("req: %r", req) scenario = req.get("scenario") assert scenario in playbook, f"unknown scenario {scenario}" logging.debug("setting scenario to '%s'", scenario) current_app.state.scenario = scenario return Response("", status=200) @app.route("/test/reset", methods=["POST"]) def test_reset(): app_init_state() return Response("", status=200) @app.route("/test/stats") def test_stats(): out = StringIO() # use a key-file format so that glib side parsing is easy out.write("[stats]\n") for action in sorted(current_app.state.stats.keys()): out.write(f"{action}={current_app.state.stats[action]}\n") return Response(out.getvalue(), status=200) def parse_arguments(): parser = argparse.ArgumentParser(description="mock snapd APIs") parser.add_argument( "--socket", help="socket path", default="/tmp/mock-snapd-test.sock" ) parser.add_argument( "--datadir", default=os.path.dirname(__file__), help="path to directory with test data files", ) return parser.parse_args() if __name__ == "__main__": opts = parse_arguments() logging.basicConfig(level=logging.DEBUG) logging.debug("socket path: %s", opts.socket) logging.debug("data dir: %s", opts.datadir) with app.app_context(): current_app.datadir = opts.datadir app_init_state() app.run(host="unix://" + opts.socket) fwupd-2.0.10/plugins/uefi-dbx/tests/uefi-dbx.json000066400000000000000000000007661501337203100216470ustar00rootroot00000000000000{ "name": "UEFI dbx", "interactive": false, "cpu-architectures": [ "x86_64" ], "steps": [ { "url": "d661d4a0aaca09dfa9e56967ca2467b0575fc07cb704d182fa8c68225452957f-DBXUpdate-20241101-x64.cab", "emulation-url": "d6e6c3566479b4724d117107ef0d34012b96c8088f2bf9498194fdf1dc32c256-emulation.zip", "components": [ { "version": "20241101", "guids": [ "f8ba2887-9411-5c36-9cee-88995bb39731" ] } ] } ] } fwupd-2.0.10/plugins/uefi-dbx/uefi-dbx.quirk000066400000000000000000000144141501337203100206620ustar00rootroot00000000000000[UEFI\GUID_8be4df61-93ca-11d2-aa0d-00e098032b8c&NAME_KEK] Plugin = uefi_dbx # x64 [UEFI\CSUM_5391C3A2FB112102A6AA1EDC25AE77E19F5D6F09CD09EEB2509922BFCD5992EA] Version = 20100307 [UEFI\CSUM_10D45FCBA396AEF3153EE8F6ECAE58AFE8476A280A2026FC71F6217DCF49BA2F] Version = 20100507 [UEFI\CSUM_90FBE70E69D633408D3E170C6832DBB2D209E0272527DFB63D49D29572A6F44C] Version = 20140413 [UEFI\CSUM_45C7C8AE750ACFBB48FC37527D6412DD644DAED8913CCD8A24C94D856967DF8E] Version = 20160809 [UEFI\CSUM_540801DD345DC1C33EF431B35BF4C0E68BD319B577B9ABE1A9CFF1CBC39F548F] Version = 20200701 [UEFI\CSUM_AF79B14064601BC0987D4747AF1E914A228C05D622CEDA03B7A4F67014FEE767] Version = 20210401 [UEFI\CSUM_90AEC5C4995674A849C1D1384463F3B02B5AA625A5C320FC4FE7D9BB58A62398] Version = 20220801 [UEFI\CSUM_6A0E824654B7479152058CF738A378E629483874B6DBD67E0D8C3327B2FCAC64] Version = 20230301 [UEFI\CSUM_13A1F37BEDFB5417B6B737E2A3816C8FD587D74D836914B2B2EDC9FD6CA30E58] Version = 20230501 [UEFI\CSUM_E427068FBA7A44C96515C47E9871798284A06397D4C8687DFDAFD0738026DDBB] Version = 20240301 [UEFI\CSUM_CDB7C90D3AB8833D5324F5D8516D41FA990B9CA721FE643FFFAEF9057D9F9E48] Version = 20241101 # aa64 [UEFI\CSUM_8C8183AD9B96FE1F3C74DEDB8087469227B642AFE2E80F8FD22E0137C11C7D90] Version = 20200729 [UEFI\CSUM_B133DE42A37376F5D91439AF3D61D38201F10377C36DACD9C2610F52AA124A91] Version = 20210401 [UEFI\CSUM_F4DC5A40D2A9DBDAB210BAE0C508E053AE986C4DA42D68760A1655D6FBAEC051] Version = 20220801 [UEFI\CSUM_AB311E737112E4D34ABF545836BC671637663E93738CEFA37405214CE8C92A58] Version = 20230501 # ia32 [UEFI\CSUM_A7DFCC3A8D6AB30F93F31748DBC8EA38415CF52BB9AD8085672CD9AB8938D5DE] Version = 20200701 [UEFI\CSUM_A8A3300E33A0A2692839CCBA84803C5E742D12501B6D58C46EB87F32017F2CFF] Version = 20210401 [UEFI\CSUM_C4EBDC43048C43F5F11C59EAD051A3585A07FAFCE985CFED8B27B73A5492F9B2] Version = 20230301 [UEFI\CSUM_490C927242CC6227CA439A7E9AA9D771AD4D1686EEDE1F331CBB6C69E9BE746E] Version = 20230501 [UEFI\CSUM_E9E4B5A51F6A5575B9F5BFAB1852B0CB2795C66FF4B28135097CBA671A5491B9] Version = 20241101 # Microsoft Corporation KEK CA 2011 [UEFI\CRT_A1117F516A32CEFCBA3F2D1ACE10A87972FD6BBE8FE0D0B996E09E65D802A503] VendorId = UEFI:Microsoft # Microsoft Windows IoT KEK CA 2015 [UEFI\CRT_CF74CD44C2BB5EF55836290AE926C660F884BF11C342E0934226463380BF2468] VendorId = UEFI:Microsoft # Microsoft Corporation KEK 2K CA 2023 [UEFI\CRT_3CD3F0309EDAE228767A976DD40D9F4AFFC4FBD5218F2E8CC3C9DD97E8AC6F9D] VendorId = UEFI:Microsoft # Microsoft Corporation KEK 3K CA 2023 [UEFI\CRT_56A752AD97ACE87A4BDC375591166C81265D58EE0D731D07D767436505C37BD0] VendorId = UEFI:Microsoft # Microsoft Corporation KEK 4K CA 2023 [UEFI\CRT_2128ED80A17DA1DE2A9C8CC7AADE9D44A80182B15B57A08681DC7E2EFBEC848D] VendorId = UEFI:Microsoft # Manufacturer=Apple Inc. [80f95c96-a739-5ef5-8482-3d65cb39ff55] Flags = no-dbx-updates # Manufacturer=FUJITSU # BaseboardManufacturer=FUJITSU # BaseboardProduct=FJNBB38 [71c3a6cd-3e9a-5b49-a4eb-0e2a57dd265b] Flags = no-dbx-updates # Manufacturer=HP # BaseboardManufacturer=HP # BaseboardProduct=83D5 [35ed19f7-015a-5da8-adf7-b31dc515c23a] Flags = no-dbx-updates # Manufacturer=HP # BaseboardManufacturer=HP # BaseboardProduct=83DA [a8cd7a16-e01e-5cdd-a098-cd5a29868d4c] Flags = no-dbx-updates # Manufacturer=HP # BaseboardManufacturer=HP # BaseboardProduct=83DD [580e5a81-1979-5e29-8d16-bb1ddcc43e87] Flags = no-dbx-updates # Manufacturer=HP # BaseboardManufacturer=HP # BaseboardProduct=83E7 [c7211192-6168-530e-b42d-650b9f091c7a] Flags = no-dbx-updates # Manufacturer=HP # BaseboardManufacturer=HP # BaseboardProduct=83E8 [624f1327-d66b-5f20-865f-9a978dc940ac] Flags = no-dbx-updates # Manufacturer=HP # BaseboardManufacturer=HP # BaseboardProduct=83E9 [af7be2cf-a1c2-50ce-b880-c090ee252249] Flags = no-dbx-updates # Manufacturer=HP # BaseboardManufacturer=HP # BaseboardProduct=8401 [48bd5791-ceaa-5f79-b8e6-c45bdf956c1e] Flags = no-dbx-updates # Manufacturer=HP # BaseboardManufacturer=HP # BaseboardProduct=8460 [7bf700eb-c41b-5898-9e09-5a26ca10c14e] Flags = no-dbx-updates # Manufacturer=HP # BaseboardManufacturer=HP # BaseboardProduct=8461 [c9f16517-3849-50cf-8b8a-48773695bfe4] Flags = no-dbx-updates # Manufacturer=HP # BaseboardManufacturer=HP # BaseboardProduct=8462 [d605b1fa-0eb3-5bd4-9ab4-2c65d7fc2a8b] Flags = no-dbx-updates # Manufacturer=HP # BaseboardManufacturer=HP # BaseboardProduct=8463 [7e2cbc58-92cf-5504-a1d4-b4f85de526a1] Flags = no-dbx-updates # Manufacturer=HP # BaseboardManufacturer=HP # BaseboardProduct=8464 [35ccf8b1-7f97-5f84-b3cb-8d343d2b595c] Flags = no-dbx-updates # Manufacturer=HP # BaseboardManufacturer=HP # BaseboardProduct=8584 [49549ee6-fa8e-5bb7-a346-9e93d2652b5d] Flags = no-dbx-updates # Manufacturer=HP # BaseboardManufacturer=HP # BaseboardProduct=8589 [23bfe0fb-be17-55a2-86e0-7970254b357c] Flags = no-dbx-updates # Manufacturer=HP # BaseboardManufacturer=HP # BaseboardProduct=8617 [cb686451-b325-57e4-a4fc-84bddb2ee470] Flags = no-dbx-updates # Manufacturer=HP # BaseboardManufacturer=HP # BaseboardProduct=8618 [2edc773c-03f9-5d72-966d-5f612f4cf127] Flags = no-dbx-updates # Manufacturer=HP # BaseboardManufacturer=HP # BaseboardProduct=8619 [2cc829ba-013c-5843-9b60-406e1ac6e106] Flags = no-dbx-updates # Manufacturer=HP # BaseboardManufacturer=HP # BaseboardProduct=8620 [3f4f41b0-f419-5715-879e-73500cdb3c5d] Flags = no-dbx-updates # Manufacturer=HP # BaseboardManufacturer=HP # BaseboardProduct=878C [4fc65dfe-4dd8-5d24-86f9-3d05e926f379] Flags = no-dbx-updates # Manufacturer=HP # BaseboardManufacturer=HP # BaseboardProduct=88F9 [b6eb0c7f-0bfc-5a3f-b2d7-cd2a38aa6d17] Flags = no-dbx-updates # Manufacturer=HP # BaseboardManufacturer=HP # BaseboardProduct=8C6A [baddb32a-7407-56a8-b1ff-cadefcf2fce7] Flags = no-dbx-updates # Manufacturer=HP # BaseboardManufacturer=HP # BaseboardProduct=8B6E [7121aa03-6ee3-5987-b04a-84aa6866a580] Flags = no-dbx-updates # Manufacturer=LENOVO # Family=ideacentre 300-20ISH [fbc17b82-0f29-561d-be80-578285ec4477] Flags = no-dbx-updates # Manufacturer=SAMSUNG ELECTRONICS CO., LTD. # Family=Galaxy Book2 360 [c4cd9095-8757-593c-8cbd-c7c281c699a7] Flags = no-dbx-updates # Manufacturer=CHUWI Innovation And Technology(ShenZhen)co.,Ltd # ProductName=MiniBook X [52e98239-4cfc-5a22-bf17-98539e439dd9] Flags = no-dbx-updates # Manufacturer=HUAWEI # BaseboardManufacturer=HUAWEI # BaseboardProduct=BOHK-WAX9X-PCB [a98d46c9-ed25-54da-8510-ef9035cecdd1] Flags = no-dbx-updates fwupd-2.0.10/plugins/uefi-esrt/000077500000000000000000000000001501337203100162765ustar00rootroot00000000000000fwupd-2.0.10/plugins/uefi-esrt/README.md000066400000000000000000000005451501337203100175610ustar00rootroot00000000000000--- title: Plugin: UEFI ESRT --- ## Introduction This allows enabling the BIOS setup option for UEFI capsule updates without manually going into BIOS setup. ## External Interface Access This plugin requires: * read/write access to `/sys/class/firmware-attributes` ## Version Considerations This plugin has been available since fwupd version `1.9.6`. fwupd-2.0.10/plugins/uefi-esrt/fu-uefi-esrt-plugin.c000066400000000000000000000037241501337203100222570ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2017 Dell, Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uefi-esrt-plugin.h" struct _FuUefiEsrtPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuUefiEsrtPlugin, fu_uefi_esrt_plugin, FU_TYPE_PLUGIN) #define LENOVO_CAPSULE_SETTING "com.thinklmi.WindowsUEFIFirmwareUpdate" #define DELL_CAPSULE_SETTING "com.dell.CapsuleFirmwareUpdate" static gboolean fu_uefi_esrt_plugin_check_esrt(void) { g_autofree gchar *sysfsfwdir = NULL; g_autofree gchar *esrtdir = NULL; /* already exists */ sysfsfwdir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR_FW); esrtdir = g_build_filename(sysfsfwdir, "efi", "esrt", NULL); return g_file_test(esrtdir, G_FILE_TEST_EXISTS); } static void fu_uefi_esrt_plugin_add_security_attrs(FuPlugin *plugin, FuSecurityAttrs *attrs) { FuContext *ctx = fu_plugin_get_context(plugin); FuEfivars *efivars = fu_context_get_efivars(ctx); g_autoptr(FwupdSecurityAttr) attr = NULL; if (!fu_efivars_supported(efivars, NULL)) return; attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_BIOS_CAPSULE_UPDATES); fu_security_attr_add_bios_target_value(attr, LENOVO_CAPSULE_SETTING, "enable"); fu_security_attr_add_bios_target_value(attr, DELL_CAPSULE_SETTING, "enabled"); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_ENABLED); if (fu_uefi_esrt_plugin_check_esrt()) fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); else fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); fu_security_attrs_append(attrs, attr); } static void fu_uefi_esrt_plugin_init(FuUefiEsrtPlugin *self) { fu_plugin_add_rule(FU_PLUGIN(self), FU_PLUGIN_RULE_BETTER_THAN, "bios"); } static void fu_uefi_esrt_plugin_class_init(FuUefiEsrtPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->add_security_attrs = fu_uefi_esrt_plugin_add_security_attrs; } fwupd-2.0.10/plugins/uefi-esrt/fu-uefi-esrt-plugin.h000066400000000000000000000003641501337203100222610ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuUefiEsrtPlugin, fu_uefi_esrt_plugin, FU, UEFI_ESRT_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/uefi-esrt/meson.build000066400000000000000000000005701501337203100204420ustar00rootroot00000000000000if allow_uefi_capsule cargs = ['-DG_LOG_DOMAIN="FuPluginUefiEsrt"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_builtins += static_library('fu_plugin_uefi_esrt', sources: [ 'fu-uefi-esrt-plugin.c', ], include_directories: plugin_incdirs, c_args: [ cargs, ], link_with: plugin_libs, dependencies: [ plugin_deps, ], ) endif fwupd-2.0.10/plugins/uefi-kek/000077500000000000000000000000001501337203100160735ustar00rootroot00000000000000fwupd-2.0.10/plugins/uefi-kek/README.md000066400000000000000000000026101501337203100173510ustar00rootroot00000000000000--- title: Plugin: UEFI KEK --- ## Introduction The EFI Key Exchange Key (also known as `KEK`) contains multiple certificates used to update the `db`, all signed by the `PK`. This plugin allows updating the `KEK` with new certificates signed by the OEM vendor. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in `EFI_SIGNATURE_LIST` format. See for details. This plugin supports the following protocol ID: * `org.uefi.dbx2` ## GUID Generation These devices use the GUID constructed of the uppercase SHA256 of the X.509 certificate. e.g. * `UEFI\CRT_{sha256}` Additionally, the subject vendor and name are used if provided. * `UEFI\VENDOR_{vendor}&NAME_{name}` ## Update Behavior The firmware is deployed when the machine is in normal runtime mode, but it is only activated when the system is restarted. ## Vendor ID Security The vendor ID is set from the certificate vendor, e.g. `UEFI:Microsoft`. ## External Interface Access This plugin requires: * Read and write access to `/sys/firmware/efi/efivars` ## Version Considerations This plugin has been available since fwupd version `2.0.8`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Richard Hughes: @hughsie fwupd-2.0.10/plugins/uefi-kek/fu-uefi-kek-device.c000066400000000000000000000073131501337203100216100ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uefi-kek-device.h" struct _FuUefiKekDevice { FuUefiDevice parent_instance; }; G_DEFINE_TYPE(FuUefiKekDevice, fu_uefi_kek_device, FU_TYPE_UEFI_DEVICE) static gboolean fu_uefi_kek_device_probe(FuDevice *device, GError **error) { FuContext *ctx = fu_device_get_context(device); g_autoptr(FuFirmware) siglist = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GPtrArray) sigs = NULL; /* FuUefiDevice->probe */ if (!FU_DEVICE_CLASS(fu_uefi_kek_device_parent_class)->probe(device, error)) return FALSE; /* add each subdevice */ siglist = fu_device_read_firmware(device, progress, error); if (siglist == NULL) { g_prefix_error(error, "failed to parse kek: "); return FALSE; } sigs = fu_efi_signature_list_get_newest(FU_EFI_SIGNATURE_LIST(siglist)); for (guint i = 0; i < sigs->len; i++) { FuEfiSignature *sig = g_ptr_array_index(sigs, i); g_autoptr(FuEfiX509Device) x509_device = NULL; if (fu_efi_signature_get_kind(sig) != FU_EFI_SIGNATURE_KIND_X509) continue; x509_device = fu_efi_x509_device_new(ctx, FU_EFI_X509_SIGNATURE(sig)); fu_device_set_physical_id(FU_DEVICE(x509_device), "kek"); fu_device_set_proxy(FU_DEVICE(x509_device), device); fu_device_add_child(device, FU_DEVICE(x509_device)); } /* set in the subdevice */ fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_CAN_EMULATION_TAG); fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_CAN_VERIFY); /* success */ return TRUE; } static gboolean fu_uefi_kek_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { g_autoptr(GBytes) fw = NULL; /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* write entire chunk to efivarsfs */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); if (!fu_uefi_device_set_efivar_bytes( FU_UEFI_DEVICE(device), FU_EFIVARS_GUID_EFI_GLOBAL, fu_device_get_physical_id(device), fw, FU_EFIVARS_ATTR_APPEND_WRITE | FU_EFIVARS_ATTR_TIME_BASED_AUTHENTICATED_WRITE_ACCESS | FU_EFIVARS_ATTR_RUNTIME_ACCESS | FU_EFIVARS_ATTR_BOOTSERVICE_ACCESS | FU_EFIVARS_ATTR_NON_VOLATILE, error)) { return FALSE; } /* success! */ return TRUE; } static void fu_uefi_kek_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_uefi_kek_device_init(FuUefiKekDevice *self) { fu_device_set_physical_id(FU_DEVICE(self), "KEK"); fu_device_set_name(FU_DEVICE(self), "UEFI Key Exchange Key"); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE_CHILD); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_EFI_SIGNATURE_LIST); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_APPLICATION_CERTIFICATE); } static void fu_uefi_kek_device_class_init(FuUefiKekDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_uefi_kek_device_probe; device_class->write_firmware = fu_uefi_kek_device_write_firmware; device_class->set_progress = fu_uefi_kek_device_set_progress; } fwupd-2.0.10/plugins/uefi-kek/fu-uefi-kek-device.h000066400000000000000000000004651501337203100216160ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_UEFI_KEK_DEVICE (fu_uefi_kek_device_get_type()) G_DECLARE_FINAL_TYPE(FuUefiKekDevice, fu_uefi_kek_device, FU, UEFI_KEK_DEVICE, FuUefiDevice) fwupd-2.0.10/plugins/uefi-kek/fu-uefi-kek-plugin.c000066400000000000000000000013421501337203100216430ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uefi-kek-device.h" #include "fu-uefi-kek-plugin.h" struct _FuUefiKekPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuUefiKekPlugin, fu_uefi_kek_plugin, FU_TYPE_PLUGIN) static void fu_uefi_kek_plugin_init(FuUefiKekPlugin *self) { } static void fu_uefi_kek_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_UEFI_KEK_DEVICE); } static void fu_uefi_kek_plugin_class_init(FuUefiKekPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_uefi_kek_plugin_constructed; } fwupd-2.0.10/plugins/uefi-kek/fu-uefi-kek-plugin.h000066400000000000000000000003611501337203100216500ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuUefiKekPlugin, fu_uefi_kek_plugin, FU, UEFI_KEK_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/uefi-kek/meson.build000066400000000000000000000006071501337203100202400ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginUefiKek"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('uefi-kek.quirk') plugin_builtins += static_library('fu_plugin_uefi_kek', sources: [ 'fu-uefi-kek-device.c', 'fu-uefi-kek-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) fwupd-2.0.10/plugins/uefi-kek/uefi-kek.quirk000066400000000000000000000001141501337203100206440ustar00rootroot00000000000000[UEFI\GUID_8be4df61-93ca-11d2-aa0d-00e098032b8c&NAME_KEK] Plugin = uefi_kek fwupd-2.0.10/plugins/uefi-mok/000077500000000000000000000000001501337203100161075ustar00rootroot00000000000000fwupd-2.0.10/plugins/uefi-mok/README.md000066400000000000000000000033341501337203100173710ustar00rootroot00000000000000--- title: Plugin: UEFI MOK --- ## Introduction Shim 16.0 and newer export a `/sys/firmware/efi/mok-variables/HSIStatus` file that contains some BootService-only attributes in an easy-to-digest format. We can use these attributes to populate the `org.fwupd.hsi.UefiMemoryProtections` Host Security ID attribute. The attributes are x64, aarch64 and riscv specific: ### shim-has-nx-compat-set This attribute will be `1` if shim has the NX-compatible bit set in the COFF header, and `0` otherwise. ### heap-is-executable This attribute will be `1` if heap is executable, and `0` otherwise. ### stack-is-executable This attribute will be `1` if the stack is executable, and `0` otherwise. ### ro-sections-are-writable This attribute will be `1` if read-only sections are actually writable, and`0` otherwise. ### has-memory-attribute-protocol This attribute will be `1` if the memory attribute protocol is supported by the firmware, and `0` otherwise. ### has-dxe-services-table This attribute will be `1` if the firmware provides a DXE services table, and `0` otherwise. ### has-get-memory-space-descriptor This attribute will be `1` if `DxeServicesTable` has `GetMemorySpaceDescriptor()` populated, and `0` otherwise. ### has-set-memory-space-attributes This attribute will be `1` if `DxeServicesTable` has `SetMemorySpaceAttributes()` populated, and `0` otherwise. ## External Interface Access This plugin requires read access to `/sys/firmware/efi/mok-variables`. ## Version Considerations This plugin has been available since fwupd version `2.0.7`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Richard Hughes: @hughsie fwupd-2.0.10/plugins/uefi-mok/fu-self-test.c000066400000000000000000000111321501337203100205670ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-context-private.h" #include "fu-plugin-private.h" #include "fu-uefi-mok-common.h" static void fu_uefi_mok_nx_disabled_func(void) { g_autofree gchar *str = NULL; g_autofree gchar *fn = NULL; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuPlugin) plugin = fu_plugin_new(ctx); g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(GError) error = NULL; fn = g_test_build_filename(G_TEST_DIST, "tests", "HSIStatus-nx-disabled", NULL); attr = fu_uefi_mok_attr_new(plugin, fn, &error); g_assert_no_error(error); g_assert_nonnull(attr); g_assert_cmpint(fwupd_security_attr_get_result(attr), ==, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); g_assert_false(fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)); fwupd_security_attr_set_created(attr, 0); str = fwupd_codec_to_string(FWUPD_CODEC(attr)); g_assert_cmpstr(str, ==, "FuSecurityAttr:\n" " AppstreamId: org.fwupd.hsi.Uefi.MemoryProtection\n" " HsiResult: not-enabled\n" " HsiResultSuccess: locked\n" " Flags: action-config-os\n" " Plugin: uefi_mok\n" " has-dxe-services-table: 0\n" " has-get-memory-space-descriptor: 0\n" " has-memory-attribute-protocol: 0\n" " has-set-memory-space-attributes: 0\n" " heap-is-executable: 0\n" " ro-sections-are-writable: 0\n" " shim-has-nx-compat-set: 0\n" " stack-is-executable: 0\n"); } static void fu_uefi_mok_nx_invalid_func(void) { g_autofree gchar *str = NULL; g_autofree gchar *fn = NULL; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuPlugin) plugin = fu_plugin_new(ctx); g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(GError) error = NULL; fn = g_test_build_filename(G_TEST_DIST, "tests", "HSIStatus-nx-invalid", NULL); attr = fu_uefi_mok_attr_new(plugin, fn, &error); g_assert_no_error(error); g_assert_nonnull(attr); g_assert_cmpint(fwupd_security_attr_get_result(attr), ==, FWUPD_SECURITY_ATTR_RESULT_NOT_LOCKED); g_assert_false(fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)); fwupd_security_attr_set_created(attr, 0); str = fwupd_codec_to_string(FWUPD_CODEC(attr)); g_assert_cmpstr(str, ==, "FuSecurityAttr:\n" " AppstreamId: org.fwupd.hsi.Uefi.MemoryProtection\n" " HsiResult: not-locked\n" " HsiResultSuccess: locked\n" " Flags: action-contact-oem\n" " Plugin: uefi_mok\n" " has-dxe-services-table: 1\n" " has-get-memory-space-descriptor: 0\n" " has-memory-attribute-protocol: 0\n" " has-set-memory-space-attributes: 0\n" " heap-is-executable: 1\n" " ro-sections-are-writable: 1\n" " shim-has-nx-compat-set: 1\n" " stack-is-executable: 1\n" " this-property-does-not-exist: 1\n"); } static void fu_uefi_mok_nx_valid_func(void) { g_autofree gchar *str = NULL; g_autofree gchar *fn = NULL; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(FuPlugin) plugin = fu_plugin_new(ctx); g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(GError) error = NULL; fn = g_test_build_filename(G_TEST_DIST, "tests", "HSIStatus-nx-valid", NULL); attr = fu_uefi_mok_attr_new(plugin, fn, &error); g_assert_no_error(error); g_assert_nonnull(attr); g_assert_cmpint(fwupd_security_attr_get_result(attr), ==, FWUPD_SECURITY_ATTR_RESULT_LOCKED); g_assert_true(fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)); fwupd_security_attr_set_created(attr, 0); str = fwupd_codec_to_string(FWUPD_CODEC(attr)); g_assert_cmpstr(str, ==, "FuSecurityAttr:\n" " AppstreamId: org.fwupd.hsi.Uefi.MemoryProtection\n" " HsiResult: locked\n" " HsiResultSuccess: locked\n" " Flags: success\n" " Plugin: uefi_mok\n" " has-dxe-services-table: 1\n" " has-get-memory-space-descriptor: 1\n" " has-memory-attribute-protocol: 1\n" " has-set-memory-space-attributes: 1\n" " heap-is-executable: 0\n" " ro-sections-are-writable: 0\n" " shim-has-nx-compat-set: 1\n" " stack-is-executable: 0\n"); } int main(int argc, char **argv) { (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); (void)g_setenv("G_MESSAGES_DEBUG", "all", TRUE); g_test_init(&argc, &argv, NULL); g_test_add_func("/uefi/mok{nx-disabled}", fu_uefi_mok_nx_disabled_func); g_test_add_func("/uefi/mok{nx-invalid}", fu_uefi_mok_nx_invalid_func); g_test_add_func("/uefi/mok{nx-valid}", fu_uefi_mok_nx_valid_func); return g_test_run(); } fwupd-2.0.10/plugins/uefi-mok/fu-uefi-mok-common.c000066400000000000000000000040601501337203100216650ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uefi-mok-common.h" #include "fu-uefi-mok-struct.h" FwupdSecurityAttr * fu_uefi_mok_attr_new(FuPlugin *plugin, const gchar *filename, GError **error) { FuUefiMokHsiKey key_all = FU_UEFI_MOK_HSI_KEY_NONE; g_auto(GStrv) lines = NULL; g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(GBytes) blob = NULL; /* create attr */ attr = fu_plugin_security_attr_new(plugin, FWUPD_SECURITY_ATTR_ID_UEFI_MEMORY_PROTECTION); fwupd_security_attr_set_plugin(attr, "uefi_mok"); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_LOCKED); /* split into lines */ blob = fu_bytes_get_contents(filename, error); if (blob == NULL) return NULL; lines = fu_strsplit_bytes(blob, "\n", -1); for (guint i = 0; lines[i] != NULL; i++) { g_auto(GStrv) kv = NULL; if (lines[i][0] == '\0') continue; kv = g_strsplit(lines[i], ": ", -1); if (g_strv_length(kv) != 2) continue; if (g_strcmp0(kv[1], "1") == 0) key_all |= fu_uefi_mok_hsi_key_from_string(kv[0]); fwupd_security_attr_add_metadata(attr, kv[0], kv[1]); } /* is this valid? */ if ((key_all & FU_UEFI_MOK_HSI_KEY_SHIM_HAS_NX_COMPAT_SET) == 0) { /* the bootloader is not marked as NX compatible and the firmware may be operating * in a compatibility mode */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_OS); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED); } else if ((key_all & FU_UEFI_MOK_HSI_KEY_HEAP_IS_EXECUTABLE) > 0 || (key_all & FU_UEFI_MOK_HSI_KEY_STACK_IS_EXECUTABLE) > 0 || (key_all & FU_UEFI_MOK_HSI_KEY_RO_SECTIONS_ARE_WRITABLE) > 0) { /* the firmware is being dumb */ fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_LOCKED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); } else { fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } /* success */ return g_steal_pointer(&attr); } fwupd-2.0.10/plugins/uefi-mok/fu-uefi-mok-common.h000066400000000000000000000003731501337203100216750ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include FwupdSecurityAttr * fu_uefi_mok_attr_new(FuPlugin *plugin, const gchar *filename, GError **error); fwupd-2.0.10/plugins/uefi-mok/fu-uefi-mok-plugin.c000066400000000000000000000033041501337203100216730ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uefi-mok-common.h" #include "fu-uefi-mok-plugin.h" struct _FuUefiMokPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuUefiMokPlugin, fu_uefi_mok_plugin, FU_TYPE_PLUGIN) static gchar * fu_uefi_mok_plugin_get_filename(void) { g_autofree gchar *sysfsdir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR_FW); return g_build_filename(sysfsdir, "efi", "mok-variables", "HSIStatus", NULL); } static gboolean fu_uefi_mok_plugin_startup(FuPlugin *plugin, FuProgress *progress, GError **error) { g_autofree gchar *fn = fu_uefi_mok_plugin_get_filename(); /* sanity check */ if (!g_file_test(fn, G_FILE_TEST_EXISTS)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "%s does not exist", fn); return FALSE; } /* success */ return TRUE; } static void fu_uefi_mok_plugin_add_security_attrs(FuPlugin *plugin, FuSecurityAttrs *attrs) { g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(GError) error_local = NULL; g_autofree gchar *fn = fu_uefi_mok_plugin_get_filename(); attr = fu_uefi_mok_attr_new(plugin, fn, &error_local); if (attr == NULL) { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE)) g_warning("failed to load %s: %s", fn, error_local->message); return; } fu_security_attrs_append(attrs, attr); } static void fu_uefi_mok_plugin_init(FuUefiMokPlugin *self) { } static void fu_uefi_mok_plugin_class_init(FuUefiMokPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->startup = fu_uefi_mok_plugin_startup; plugin_class->add_security_attrs = fu_uefi_mok_plugin_add_security_attrs; } fwupd-2.0.10/plugins/uefi-mok/fu-uefi-mok-plugin.h000066400000000000000000000003611501337203100217000ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuUefiMokPlugin, fu_uefi_mok_plugin, FU, UEFI_MOK_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/uefi-mok/fu-uefi-mok.rs000066400000000000000000000010561501337203100206030ustar00rootroot00000000000000// Copyright 2025 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(FromString)] enum FuUefiMokHsiKey { None = 0, ShimHasNxCompatSet = 1 << 0, HeapIsExecutable = 1 << 1, StackIsExecutable = 1 << 2, RoSectionsAreWritable = 1 << 3, HasMemoryAttributeProtocol = 1 << 4, HasDxeServicesTable = 1 << 5, HasGetMemorySpaceDescriptor = 1 << 6, HasSetMemorySpaceAttributes = 1 << 7, } fwupd-2.0.10/plugins/uefi-mok/meson.build000066400000000000000000000025071501337203100202550ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginUefiMok"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_builtin_uefi_mok = static_library('fu_plugin_uefi_mok', rustgen.process('fu-uefi-mok.rs'), sources: [ 'fu-uefi-mok-common.c', 'fu-uefi-mok-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) plugin_builtins += plugin_builtin_uefi_mok if get_option('tests') install_data( [ 'tests/HSIStatus-nx-disabled', 'tests/HSIStatus-nx-invalid', 'tests/HSIStatus-nx-valid', ], install_dir: join_paths(installed_test_datadir, 'tests') ) env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) e = executable( 'uefi-mok-self-test', rustgen.process('fu-uefi-mok.rs'), sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: [ plugin_deps, platform_deps, ], link_with: [ fwupd, fwupdplugin, plugin_builtin_uefi_mok, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ cargs, '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('uefi-mok-self-test', e, env: env) endif fwupd-2.0.10/plugins/uefi-mok/tests/000077500000000000000000000000001501337203100172515ustar00rootroot00000000000000fwupd-2.0.10/plugins/uefi-mok/tests/HSIStatus-nx-disabled000066400000000000000000000003441501337203100232140ustar00rootroot00000000000000shim-has-nx-compat-set: 0 heap-is-executable: 0 stack-is-executable: 0 ro-sections-are-writable: 0 has-memory-attribute-protocol: 0 has-dxe-services-table: 0 has-get-memory-space-descriptor: 0 has-set-memory-space-attributes: 0 fwupd-2.0.10/plugins/uefi-mok/tests/HSIStatus-nx-invalid000066400000000000000000000004301501337203100230670ustar00rootroot00000000000000# this is a comment this-property-does-not-exist: 1 shim-has-nx-compat-set: 1 heap-is-executable: 1 stack-is-executable: 1 ro-sections-are-writable: 1 has-memory-attribute-protocol: 0 has-dxe-services-table: 1 has-get-memory-space-descriptor: 0 has-set-memory-space-attributes: 0 fwupd-2.0.10/plugins/uefi-mok/tests/HSIStatus-nx-valid000066400000000000000000000003441501337203100225440ustar00rootroot00000000000000shim-has-nx-compat-set: 1 heap-is-executable: 0 stack-is-executable: 0 ro-sections-are-writable: 0 has-memory-attribute-protocol: 1 has-dxe-services-table: 1 has-get-memory-space-descriptor: 1 has-set-memory-space-attributes: 1 fwupd-2.0.10/plugins/uefi-pk/000077500000000000000000000000001501337203100157335ustar00rootroot00000000000000fwupd-2.0.10/plugins/uefi-pk/README.md000066400000000000000000000015121501337203100172110ustar00rootroot00000000000000--- title: Plugin: UEFI PK --- ## Introduction The platform key (PK) specifies the machine owner, typically the OEM that created the laptop or desktop. Several device manufacturers decide to ship the default "AMI Test PK" platform key instead of a Device Manufacturer specific one. This will cause an HSI-1 failure. ## GUID Generation These devices use the GUID constructed of the uppercase SHA256 of the X.509 certificate. e.g. * `UEFI\CRT_{sha256}` Additionally, the subject vendor and name are used if provided. * `UEFI\VENDOR_{vendor}&NAME_{name}` These GUIDs can be used to fulfill "other device" requirements when installing `KEK` updates. ## External Interface Access This plugin requires: * read access to `/sys/firmware/efi/efivars` ## Version Considerations This plugin has been available since fwupd version `1.5.5`. fwupd-2.0.10/plugins/uefi-pk/fu-uefi-pk-device.c000066400000000000000000000134731501337203100213140ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uefi-pk-device.h" struct _FuUefiPkDevice { FuUefiDevice parent_instance; gboolean has_pk_test_key; gchar *key_id; }; G_DEFINE_TYPE(FuUefiPkDevice, fu_uefi_pk_device, FU_TYPE_UEFI_DEVICE) static void fu_uefi_pk_device_to_string(FuDevice *device, guint idt, GString *str) { FuUefiPkDevice *self = FU_UEFI_PK_DEVICE(device); fwupd_codec_string_append_bool(str, idt, "HasPkTestKey", self->has_pk_test_key); } #define FU_UEFI_PK_CHECKSUM_AMI_TEST_KEY "a773113bafaf5129aa83fd0912e95da4fa555f91" static gboolean fu_uefi_pk_device_check(FuUefiPkDevice *self, const gchar *str, GError **error) { const gchar *needles[] = { "DO NOT TRUST", "DO NOT SHIP", }; /* look for things that should not exist */ for (guint i = 0; i < G_N_ELEMENTS(needles); i++) { if (g_strstr_len(str, -1, needles[i]) != NULL) { g_info("got %s, marking unsafe", str); self->has_pk_test_key = TRUE; break; } } /* success */ return TRUE; } const gchar * fu_uefi_pk_device_get_key_id(FuUefiPkDevice *self) { g_return_val_if_fail(FU_IS_UEFI_PK_DEVICE(self), NULL); return self->key_id; } static void fu_uefi_pk_device_set_key_id(FuUefiPkDevice *self, const gchar *key_id) { g_free(self->key_id); self->key_id = g_strdup(key_id); } static gboolean fu_uefi_pk_device_parse_certificate(FuUefiPkDevice *self, FuEfiX509Signature *sig, GError **error) { const gchar *subject_name = fu_efi_x509_signature_get_subject_name(sig); const gchar *subject_vendor = fu_efi_x509_signature_get_subject_vendor(sig); /* look in issuer and subject */ if (fu_efi_x509_signature_get_issuer(sig) != NULL) { if (!fu_uefi_pk_device_check(self, fu_efi_x509_signature_get_issuer(sig), error)) return FALSE; } if (fu_efi_x509_signature_get_subject(sig) != NULL) { if (!fu_uefi_pk_device_check(self, fu_efi_x509_signature_get_subject(sig), error)) return FALSE; } /* the O= key may not exist */ fu_device_add_instance_strsafe(FU_DEVICE(self), "VENDOR", subject_vendor); fu_device_add_instance_strsafe(FU_DEVICE(self), "NAME", subject_name); fu_device_build_instance_id(FU_DEVICE(self), NULL, "UEFI", "VENDOR", "NAME", NULL); fu_device_set_name(FU_DEVICE(self), subject_name != NULL ? subject_name : "Unknown"); fu_device_set_vendor(FU_DEVICE(self), subject_vendor != NULL ? subject_vendor : "Unknown"); fu_device_set_version_raw(FU_DEVICE(self), fu_firmware_get_version_raw(FU_FIRMWARE(sig))); fu_uefi_pk_device_set_key_id(self, fu_firmware_get_id(FU_FIRMWARE(sig))); /* success, certificate was parsed correctly */ fu_device_add_instance_strup(FU_DEVICE(self), "CRT", self->key_id); return fu_device_build_instance_id(FU_DEVICE(self), error, "UEFI", "CRT", NULL); } static gboolean fu_uefi_pk_device_probe(FuDevice *device, GError **error) { FuUefiPkDevice *self = FU_UEFI_PK_DEVICE(device); g_autoptr(FuFirmware) img = NULL; g_autoptr(FuFirmware) pk = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GPtrArray) sigs = NULL; pk = fu_device_read_firmware(device, progress, error); if (pk == NULL) { g_prefix_error(error, "failed to parse PK: "); return FALSE; } /* by checksum */ img = fu_firmware_get_image_by_checksum(pk, FU_UEFI_PK_CHECKSUM_AMI_TEST_KEY, NULL); if (img != NULL) self->has_pk_test_key = TRUE; /* by text */ sigs = fu_firmware_get_images(pk); for (guint i = 0; i < sigs->len; i++) { FuEfiSignature *sig = g_ptr_array_index(sigs, i); if (fu_efi_signature_get_kind(sig) != FU_EFI_SIGNATURE_KIND_X509) continue; if (!fu_uefi_pk_device_parse_certificate(self, FU_EFI_X509_SIGNATURE(sig), error)) return FALSE; } /* success */ return TRUE; } static void fu_uefi_pk_device_add_security_attrs(FuDevice *device, FuSecurityAttrs *attrs) { FuUefiPkDevice *self = FU_UEFI_PK_DEVICE(device); g_autoptr(FwupdSecurityAttr) attr = NULL; /* create attr */ attr = fu_device_security_attr_new(device, FWUPD_SECURITY_ATTR_ID_UEFI_PK); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_VALID); fu_security_attrs_append(attrs, attr); /* test key is not secure */ if (self->has_pk_test_key) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_FW); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } static gchar * fu_uefi_pk_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint64(version_raw, fu_device_get_version_format(device)); } static void fu_uefi_pk_device_init(FuUefiPkDevice *self) { fu_device_set_physical_id(FU_DEVICE(self), "pk"); fu_device_set_summary(FU_DEVICE(self), "UEFI Platform Key"); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE_CHILD); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_APPLICATION_CERTIFICATE); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_EFI_SIGNATURE_LIST); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_NUMBER); } static void fu_uefi_pk_device_finalize(GObject *object) { FuUefiPkDevice *self = FU_UEFI_PK_DEVICE(object); g_free(self->key_id); G_OBJECT_CLASS(fu_uefi_pk_device_parent_class)->finalize(object); } static void fu_uefi_pk_device_class_init(FuUefiPkDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_uefi_pk_device_finalize; device_class->to_string = fu_uefi_pk_device_to_string; device_class->add_security_attrs = fu_uefi_pk_device_add_security_attrs; device_class->probe = fu_uefi_pk_device_probe; device_class->convert_version = fu_uefi_pk_device_convert_version; } fwupd-2.0.10/plugins/uefi-pk/fu-uefi-pk-device.h000066400000000000000000000006061501337203100213130ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_UEFI_PK_DEVICE (fu_uefi_pk_device_get_type()) G_DECLARE_FINAL_TYPE(FuUefiPkDevice, fu_uefi_pk_device, FU, UEFI_PK_DEVICE, FuUefiDevice) const gchar * fu_uefi_pk_device_get_key_id(FuUefiPkDevice *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/plugins/uefi-pk/fu-uefi-pk-plugin.c000066400000000000000000000021221501337203100213400ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uefi-pk-device.h" #include "fu-uefi-pk-plugin.h" struct _FuUefiPkPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuUefiPkPlugin, fu_uefi_pk_plugin, FU_TYPE_PLUGIN) static void fu_uefi_pk_plugin_init(FuUefiPkPlugin *self) { } static void fu_uefi_pk_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_set_device_gtype_default(plugin, FU_TYPE_UEFI_PK_DEVICE); } static gboolean fu_uefi_pk_plugin_device_created(FuPlugin *plugin, FuDevice *device, GError **error) { if (!fu_device_probe(device, error)) return FALSE; fu_plugin_add_report_metadata(plugin, "UefiPkKeyId", fu_uefi_pk_device_get_key_id(FU_UEFI_PK_DEVICE(device))); return TRUE; } static void fu_uefi_pk_plugin_class_init(FuUefiPkPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_uefi_pk_plugin_constructed; plugin_class->device_created = fu_uefi_pk_plugin_device_created; } fwupd-2.0.10/plugins/uefi-pk/fu-uefi-pk-plugin.h000066400000000000000000000003561501337203100213540ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuUefiPkPlugin, fu_uefi_pk_plugin, FU, UEFI_PK_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/uefi-pk/meson.build000066400000000000000000000007441501337203100201020ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginUefiPk"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('uefi-pk.quirk') plugin_builtins += static_library('fu_plugin_uefi_pk', sources: [ 'fu-uefi-pk-device.c', 'fu-uefi-pk-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) enumeration_data += files('tests/uefi-pk-setup.json') device_tests += files('tests/uefi-pk.json') fwupd-2.0.10/plugins/uefi-pk/tests/000077500000000000000000000000001501337203100170755ustar00rootroot00000000000000fwupd-2.0.10/plugins/uefi-pk/tests/uefi-pk-setup.json000066400000000000000000000032141501337203100224660ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUefiDevice", "BackendId": "8be4df61-93ca-11d2-aa0d-00e098032b8c-PK", "Guid": "8be4df61-93ca-11d2-aa0d-00e098032b8c", "Created": "2025-02-13T00:00:00Z", "Name": "PK", "Events": [ { "Id": "#b2b5cb40", "Data": "oVnApeSUp0qHtasVXCvwctcDAAAAAAAAuwMAAJZOwjzHIthBiGOOOdzcws8wggOnMIICj6ADAgECAgkA67UT1Gux3G4wDQYJKoZIhvcNAQELBQAwajELMAkGA1UEBhMCSlAxETAPBgNVBAgMCEthbmFnYXdhMREwDwYDVQQHDAhZb2tvaGFtYTEUMBIGA1UECgwLTGVub3ZvIEx0ZC4xHzAdBgNVBAMMFkxlbm92byBMdGQuIFBLIENBIDIwMTIwHhcNMTIwNjI5MTAzNDM2WhcNMzIwNjI0MTAzNDM2WjBqMQswCQYDVQQGEwJKUDERMA8GA1UECAwIS2FuYWdhd2ExETAPBgNVBAcMCFlva29oYW1hMRQwEgYDVQQKDAtMZW5vdm8gTHRkLjEfMB0GA1UEAwwWTGVub3ZvIEx0ZC4gUEsgQ0EgMjAxMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALacXWJj09F3UmaZX9giEoZxGx6uFDoYS/8MVP378r5aSdShp1Ief2xLx2AKzsK8fasTtmbpEsN8dfPewPMyGbhe8pzLWJhm2XMU6JtvKrJkNBY/B5u4GfosydYGVU53tSHlc2wCpUCx9LIxh9NTJPgsqm1Cqly0u6XszgUpxUKTWhzU56vfXoNwh3eoWXgz1MrxRmrAnpwEPwOeE1IPChM825RrXUwUCXMXGgs65uyhRR06paqa9N60sxXxB8jW+uCoMJm3jqPQ/zvyyfmIizG2ov0KTfT/KK7Ftdo+QpMmmpq9qg5UWP6HryCgd8w94ZZS8phOMhQusujtOwF62JMCAwEAAaNQME4wHQYDVR0OBBYEFP9330sXTHGLdPkXnew0PY0ZDpRIMB8GA1UdIwQYMBaAFP9330sXTHGLdPkXnew0PY0ZDpRIMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAHBlr6+QJa1V1pGl5t+RoInvjzJLte7G1Y67E45DXT1yTjpPJqZnsChWz2oc6jDuCGEtfUKM+t7W7tU7lAwoYd9PT/D+IdumzXiKDwcoX9XdtdeTcpjJbmWI8qW3qcN1DmUSvNUy1F3OwtFlXrlsTqgAB7ooeDCKDHC1VFhQtSIjPt9hT+CR7mAbR4Ry9+pppSjKT/U6txgOPr+HMYcKENnFNKsAfRAH6brC7UGpQcC/6p6C+1SdhbmBNl8B8ZwaubhcsRbJ6UyAEnhBeQfo+W0R7SyIjj0NvWwjF2Ac1kcb9itRtLaCUXzJXtJgJVk8eWUzHzqQgCPKwZgubhSOkXY=", "Attr": 39 } ] } ] } fwupd-2.0.10/plugins/uefi-pk/tests/uefi-pk.json000066400000000000000000000004301501337203100213250ustar00rootroot00000000000000{ "name": "UEFI PK", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/uefi-pk-setup.json", "components": [ { "guids": [ "6c200e38-b742-59d4-8928-51274abd4b8c" ] } ] } ] } fwupd-2.0.10/plugins/uefi-pk/uefi-pk.quirk000066400000000000000000000001121501337203100203420ustar00rootroot00000000000000[UEFI\GUID_8be4df61-93ca-11d2-aa0d-00e098032b8c&NAME_PK] Plugin = uefi_pk fwupd-2.0.10/plugins/uefi-recovery/000077500000000000000000000000001501337203100171575ustar00rootroot00000000000000fwupd-2.0.10/plugins/uefi-recovery/README.md000066400000000000000000000013231501337203100204350ustar00rootroot00000000000000--- title: Plugin: UEFI Recovery --- ## Introduction Some devices have firmware bugs which mean they do not include a valid ESRT table in old firmware versions. Create a 'fake' UEFI device with the lowest possible version so that it can be updated to a version of firmware which does have an ESRT table. ## GUID Generation All the HwId GUIDs are used for the fake UEFI device, and so should be used in the firmware metadata for releases that should recover the system. ## Vendor ID Security The vendor ID is set from the BIOS vendor, for example `DMI:LENOVO` ## External Interface Access This plugin requires no extra access. ## Version Considerations This plugin has been available since fwupd version `1.3.1`. fwupd-2.0.10/plugins/uefi-recovery/fu-uefi-recovery-plugin.c000066400000000000000000000047731501337203100240260ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uefi-recovery-plugin.h" struct _FuUefiRecoveryPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuUefiRecoveryPlugin, fu_uefi_recovery_plugin, FU_TYPE_PLUGIN) static gboolean fu_uefi_recovery_plugin_startup(FuPlugin *plugin, FuProgress *progress, GError **error) { FuContext *ctx = fu_plugin_get_context(plugin); FuEfivars *efivars = fu_context_get_efivars(ctx); /* are the EFI dirs set up so we can update each device */ return fu_efivars_supported(efivars, error); } static gboolean fu_uefi_recovery_plugin_coldplug(FuPlugin *plugin, FuProgress *progress, GError **error) { FuContext *ctx = fu_plugin_get_context(plugin); GPtrArray *hwids = fu_context_get_hwid_guids(ctx); g_autoptr(FuDevice) device = fu_device_new(fu_plugin_get_context(plugin)); fu_device_set_id(device, "uefi-recovery"); fu_device_set_name(device, "System Firmware ESRT Recovery"); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "0.0.0"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT); fu_device_set_metadata(device, FU_DEVICE_METADATA_UEFI_DEVICE_KIND, "system-firmware"); fu_device_add_icon(device, FU_DEVICE_ICON_COMPUTER); for (guint i = 0; i < hwids->len; i++) { const gchar *hwid = g_ptr_array_index(hwids, i); fu_device_add_instance_id(device, hwid); } /* set vendor ID as the BIOS vendor */ fu_device_build_vendor_id(device, "DMI", fu_context_get_hwid_value(ctx, FU_HWIDS_KEY_BIOS_VENDOR)); fu_plugin_device_register(plugin, device); return TRUE; } static void fu_uefi_recovery_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); /* make sure that UEFI plugin is ready to receive devices */ fu_plugin_add_rule(plugin, FU_PLUGIN_RULE_RUN_AFTER, "uefi_capsule"); fu_plugin_add_flag(plugin, FWUPD_PLUGIN_FLAG_REQUIRE_HWID); } static void fu_uefi_recovery_plugin_init(FuUefiRecoveryPlugin *self) { } static void fu_uefi_recovery_plugin_class_init(FuUefiRecoveryPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_uefi_recovery_plugin_constructed; plugin_class->coldplug = fu_uefi_recovery_plugin_coldplug; plugin_class->startup = fu_uefi_recovery_plugin_startup; } fwupd-2.0.10/plugins/uefi-recovery/fu-uefi-recovery-plugin.h000066400000000000000000000004341501337203100240210ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuUefiRecoveryPlugin, fu_uefi_recovery_plugin, FU, UEFI_RECOVERY_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/uefi-recovery/meson.build000066400000000000000000000006461501337203100213270ustar00rootroot00000000000000if allow_uefi_capsule cargs = ['-DG_LOG_DOMAIN="FuPluginUefiRecovery"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('uefi-recovery.quirk') plugin_builtins += static_library('fu_plugin_uefi_recovery', sources: [ 'fu-uefi-recovery-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: [ cargs, ], dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/uefi-recovery/uefi-recovery.quirk000066400000000000000000000001741501337203100230220ustar00rootroot00000000000000# Silicom Minnowboard Turbot MNW2MAX1.X64.0100.R01.1811141729 [ea358e00-39f1-55b6-97be-a39225a585e1] Plugin = uefi_recovery fwupd-2.0.10/plugins/uefi-sbat/000077500000000000000000000000001501337203100162525ustar00rootroot00000000000000fwupd-2.0.10/plugins/uefi-sbat/README.md000066400000000000000000000111501501337203100175270ustar00rootroot00000000000000--- title: Plugin: UEFI SBAT --- ## Introduction SBAT is a generation number based revocation mechanism that is used as a successsor to dbx checksum blocklisting. See [https://github.com/rhboot/shim/blob/main/SBAT.md](the specification) for more details. ## Firmware Format The daemon will decompress the cabinet archive and extract a signed PE/COFF firmware blob with a `.sbata` section containing the revocation CSV data. This plugin supports the following protocol ID: * `org.uefi.sbat` ## GUID Generation These devices use the distribution-specific DeviceInstanceId values, e.g. * `UEFI\OS_fedora` (only quirk) * `UEFI\OS_fedora&VAR_SbatLevelRT` ## Update Behavior The device version is calculated using the same heuristics as for the revocation PE file, which is complicated slightly by there being potentially multiple components (each with a version) in the payload, for instance, `shim`, `grub` and `sd-boot`. These revocation components may or may not exist in the ESP, and may not exist in the existing platform UEFI `SbatLevelRT` variable, and so it is impossible to create a set of child-devices for each item like we might do for a USB device for example. Additionally, assigning *a single version* is complicated further because per-vendor overrides may be present in the SBAT entries in either the `SbatLevelRT` key or the signed revocation file. To overcome these limitations (and to force a round peg in a square hole) we sum up each generation number contained in the SBAT sections, **including** the per-vendor value where provided. The semver *major* value is the SBAT revision, the *minor* value is the sum of all the upstream component generation numbers, and the *micro* value is the sum of all the downstream component generation numbers. For example: A single component: sbat,1 -> "version 1.0.0" Multiple components: sbat,1 grub,4 -> "version 1.4.0" Multiple components, with a distro specific override: sbat,1 grub,4 sd-boot,2 grub.fedora,2 grub.ubuntu,2 -> "version 1.7.4" This ensures that we can have a single version number representing the device current state, and also a single version number that represents the most up to date **per-distro** revocation data. For this reason different distributions should have a different AppStream namespace for the revocation data, for instance `org.fedoraproject.sbat.firmware` -- which makes sense as each vendor will be signing the revocation binary with its own key anyway. ## Deployment Concerns Before deploying the contents of the `.sbata` COFF section to the `SbatLevelRT` UEFI variable we should check that we wouldn't be *soft bricking* the users PC by deploying a SBAT policy that cannot allow the current shim and second stage bootloader to run on next reboot. Although the `SbatLevelRT` UEFI variable is deleted when SecureBoot is turned off, this is not something we should be asking end users to do. By locating each `BootXXXX` file path on the ESP we can load the `.sbat` section and only deploy the revocation if the generation number of the EFI binary is greater than or equal to the specific generation number specified in the revocation. For instance, if there is a boot entry `Boot0001 -> shim.efi` we would: * Read `ESP/shim.efi` COFF section `.sbat`: sbat,1 shim,4 shim.rh,3 shim.fedora,3 * Read `ESP/grubx64.efi` (this filename is hardcoded into `ESP/shim.efi`) COFF section `.sbat` sbat,1 grub,3 grub.rh,2 * Verify that the `.sbata` section in the `revocations.efi` payload is not newer that each of the file-defined SBAT values: sbat,1 shim,2 grub,3 grub.debian,4 ## Vendor ID Security The vendor ID is set from the *operating system* vendor as discovered from `os-release`, in this instance set to `OS:fedora` ## External Interface Access This plugin requires read access to `/sys/firmware/efi/efivars` and read/write access to the ESP. ## Version Considerations This plugin has been available since fwupd version `2.0.0`. ## Testing $ sudo fwupdtool --plugins uefi-sbat -vv install-blob revocations.efi.signed $ sudo fwupdtool --plugins uefi-sbat -vv reboot-cleanup $ sudo fwupdtool firmware-build ../plugins/uefi-sbat/revocation.builder.xml revocation.efi Decompressing… [************ ] SBAT level is too old on /boot/efi/EFI/fedora/grubx64.efi: ESP file /boot/efi/EFI/fedora/shimx64.efi has SBAT entry sbat v1, but revocation has v2 ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Peter Jones: @vathpela fwupd-2.0.10/plugins/uefi-sbat/fu-uefi-sbat-device.c000066400000000000000000000173241501337203100221510ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uefi-sbat-device.h" #include "fu-uefi-sbat-firmware.h" struct _FuUefiSbatDevice { FuDevice parent_instance; }; G_DEFINE_TYPE(FuUefiSbatDevice, fu_uefi_sbat_device, FU_TYPE_DEVICE) static gboolean fu_uefi_sbat_device_probe(FuDevice *device, GError **error) { g_autofree gchar *distro_id = NULL; distro_id = g_get_os_info(G_OS_INFO_KEY_ID); if (distro_id == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no os-release ID"); return FALSE; } fu_device_build_vendor_id(device, "OS", distro_id); /* try to lookup /etc/os-release ID key */ fu_device_add_instance_str(device, "OS", distro_id); fu_device_add_instance_str(device, "VAR", "SbatLevelRT"); if (!fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, error, "UEFI", "OS", NULL)) return FALSE; if (!fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_VISIBLE, error, "UEFI", "OS", "VAR", NULL)) return FALSE; /* success */ return TRUE; } static FuFirmware * fu_uefi_sbat_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuContext *ctx = fu_device_get_context(device); g_autoptr(FuFirmware) firmware_pefile = fu_pefile_firmware_new(); g_autoptr(FuFirmware) firmware_sbat = fu_uefi_sbat_firmware_new(); g_autoptr(GInputStream) stream_sbata = NULL; g_autoptr(GPtrArray) esp_files = NULL; if (!fu_firmware_parse_stream(firmware_pefile, stream, 0x0, flags, error)) return NULL; /* grab .sbata and parse */ stream_sbata = fu_firmware_get_image_by_id_stream(firmware_pefile, ".sbata", error); if (stream_sbata == NULL) return NULL; if (!fu_firmware_parse_stream(firmware_sbat, stream_sbata, 0x0, flags, error)) return NULL; /* verify there is nothing in the ESP with a lower version */ esp_files = fu_context_get_esp_files(ctx, FU_CONTEXT_ESP_FILE_FLAG_INCLUDE_FIRST_STAGE | FU_CONTEXT_ESP_FILE_FLAG_INCLUDE_SECOND_STAGE, error); if (esp_files == NULL) { g_prefix_error(error, "failed to get files on ESP: "); return NULL; } for (guint i = 0; i < esp_files->len; i++) { FuFirmware *esp_file = g_ptr_array_index(esp_files, i); if (!fu_firmware_check_compatible(firmware_sbat, esp_file, flags, error)) { g_prefix_error(error, "SBAT level is too old on %s: ", fu_firmware_get_filename(esp_file)); return NULL; } } /* success */ return g_steal_pointer(&firmware_pefile); } static gboolean fu_uefi_sbat_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuContext *ctx = fu_device_get_context(device); FuEfivars *efivars = fu_context_get_efivars(ctx); guint16 idx = 0; g_autofree gchar *dirname = NULL; g_autofree gchar *filename_shim = NULL; g_autofree gchar *filename_revocation = NULL; g_autofree gchar *fp_name = NULL; g_autofree gchar *mount_point = NULL; g_autoptr(FuDeviceLocker) volume_locker = NULL; g_autoptr(FuEfiLoadOption) entry = NULL; g_autoptr(FuFirmware) dp_fp = NULL; g_autoptr(FuFirmware) dp_hdd = NULL; g_autoptr(FuFirmware) dp_list = NULL; g_autoptr(FuVolume) volume = NULL; g_autoptr(GBytes) fw = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 86, "mount ESP"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 14, NULL); /* get the mountpoint of the currently used ESP */ if (!fu_efivars_get_boot_current(efivars, &idx, error)) return FALSE; entry = fu_efivars_get_boot_entry(efivars, idx, error); if (entry == NULL) return FALSE; dp_list = fu_firmware_get_image_by_gtype(FU_FIRMWARE(entry), FU_TYPE_EFI_DEVICE_PATH_LIST, error); if (dp_list == NULL) return FALSE; dp_hdd = fu_firmware_get_image_by_gtype(dp_list, FU_TYPE_EFI_HARD_DRIVE_DEVICE_PATH, error); if (dp_hdd == NULL) return FALSE; volume = fu_context_get_esp_volume_by_hard_drive_device_path( ctx, FU_EFI_HARD_DRIVE_DEVICE_PATH(dp_hdd), error); if (volume == NULL) return FALSE; volume_locker = fu_volume_locker(volume, error); if (volume_locker == NULL) return FALSE; mount_point = fu_volume_get_mount_point(volume); if (mount_point == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no mountpoint for %s", fu_volume_get_id(volume)); return FALSE; } fu_progress_step_done(progress); /* get the location of the CurrentBoot ESP file */ dp_fp = fu_firmware_get_image_by_gtype(dp_list, FU_TYPE_EFI_FILE_PATH_DEVICE_PATH, error); if (dp_fp == NULL) return FALSE; fp_name = fu_efi_file_path_device_path_get_name(FU_EFI_FILE_PATH_DEVICE_PATH(dp_fp), error); if (fp_name == NULL) return FALSE; filename_shim = g_build_filename(mount_point, fp_name, NULL); dirname = g_path_get_dirname(filename_shim); filename_revocation = g_build_filename(dirname, "revocations.efi", NULL); /* write image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; if (!fu_bytes_set_contents(filename_revocation, fw, error)) return FALSE; g_debug("wrote %s", filename_revocation); fu_progress_step_done(progress); /* success! */ return TRUE; } static void fu_uefi_sbat_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_uefi_sbat_device_init(FuUefiSbatDevice *self) { fu_device_set_name(FU_DEVICE(self), "SBAT"); fu_device_set_summary(FU_DEVICE(self), "Generation number based revocation mechanism"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_add_protocol(FU_DEVICE(self), "com.uefi.sbat"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_APPLICATION_CERTIFICATE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_REBOOT); fu_device_set_physical_id(FU_DEVICE(self), "UEFI"); fu_device_set_logical_id(FU_DEVICE(self), "SBAT"); } static void fu_uefi_sbat_device_class_init(FuUefiSbatDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_uefi_sbat_device_probe; device_class->prepare_firmware = fu_uefi_sbat_device_prepare_firmware; device_class->write_firmware = fu_uefi_sbat_device_write_firmware; device_class->set_progress = fu_uefi_sbat_device_set_progress; } FuUefiSbatDevice * fu_uefi_sbat_device_new(FuContext *ctx, GBytes *blob, GError **error) { g_autoptr(FuFirmware) firmware = fu_uefi_sbat_firmware_new(); g_autoptr(FuUefiSbatDevice) self = NULL; g_return_val_if_fail(FU_IS_CONTEXT(ctx), NULL); g_return_val_if_fail(blob != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* copy the version across */ if (!fu_firmware_parse_bytes(firmware, blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return NULL; self = g_object_new(FU_TYPE_UEFI_SBAT_DEVICE, "context", ctx, NULL); fu_device_set_version(FU_DEVICE(self), fu_firmware_get_version(firmware)); /* success */ return g_steal_pointer(&self); } fwupd-2.0.10/plugins/uefi-sbat/fu-uefi-sbat-device.h000066400000000000000000000006211501337203100221460ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_UEFI_SBAT_DEVICE (fu_uefi_sbat_device_get_type()) G_DECLARE_FINAL_TYPE(FuUefiSbatDevice, fu_uefi_sbat_device, FU, UEFI_SBAT_DEVICE, FuDevice) FuUefiSbatDevice * fu_uefi_sbat_device_new(FuContext *ctx, GBytes *blob, GError **error); fwupd-2.0.10/plugins/uefi-sbat/fu-uefi-sbat-firmware.c000066400000000000000000000106101501337203100225150ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uefi-sbat-firmware.h" struct _FuUefiSbatFirmware { FuCsvFirmware parent_instance; }; G_DEFINE_TYPE(FuUefiSbatFirmware, fu_uefi_sbat_firmware, FU_TYPE_CSV_FIRMWARE) static gboolean fu_uefi_sbat_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { guint semver[] = {0, 0, 0}; g_autofree gchar *debug_str = NULL; g_autofree gchar *version = NULL; g_autoptr(GPtrArray) debug = g_ptr_array_new_with_free_func(g_free); g_autoptr(GPtrArray) imgs = NULL; /* FuCsvFirmware->parse */ if (!FU_FIRMWARE_CLASS(fu_uefi_sbat_firmware_parent_class) ->parse(firmware, stream, flags, error)) return FALSE; imgs = fu_firmware_get_images(firmware); for (guint i = 0; i < imgs->len; i++) { FuCsvEntry *entry = g_ptr_array_index(imgs, i); const gchar *name = fu_firmware_get_id(FU_FIRMWARE(entry)); guint64 component_version = fu_firmware_get_version_raw(FU_FIRMWARE(entry)); guint semver_index = 2; /* sanity check */ if (name == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "entry has no name"); return FALSE; } /* parse generation */ g_ptr_array_add(debug, g_strdup_printf("%s:%u", name, (guint)component_version)); if (g_strcmp0(name, "sbat") == 0) { semver_index = 0; } else if (g_strstr_len(name, -1, ".") == NULL) { semver_index = 1; } semver[semver_index] += component_version; } /* success */ version = g_strdup_printf("%u.%u.%u", semver[0], semver[1], semver[2]); fu_firmware_set_version(firmware, version); /* for debugging */ debug_str = fu_strjoin(", ", debug); g_debug("%s -> %s", debug_str, version); return TRUE; } static gboolean fu_uefi_sbat_firmware_check_compatible(FuFirmware *firmware, FuFirmware *other, FuFirmwareParseFlags flags, GError **error) { g_autoptr(FuFirmware) esp_sbat = NULL; g_autoptr(GPtrArray) revocation_entries = NULL; g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_UEFI_SBAT_FIRMWARE(firmware), FALSE); g_return_val_if_fail(FU_IS_PEFILE_FIRMWARE(other), FALSE); /* find the .sbat section in the PE file */ esp_sbat = fu_firmware_get_image_by_id(other, ".sbat", &error_local); if (esp_sbat == NULL) { g_debug("%s was ignored: %s", fu_firmware_get_filename(other), error_local->message); return TRUE; } /* the revocation data */ revocation_entries = fu_firmware_get_images(firmware); for (guint i = 0; i < revocation_entries->len; i++) { FuFirmware *revocation_entry = g_ptr_array_index(revocation_entries, i); g_autoptr(FuFirmware) esp_entry = NULL; esp_entry = fu_firmware_get_image_by_id(esp_sbat, fu_firmware_get_id(revocation_entry), NULL); if (esp_entry == NULL) { g_debug("no %s SBAT entry in %s", fu_firmware_get_id(revocation_entry), fu_firmware_get_filename(other)); continue; } g_debug("%s has SBAT entry %s v%u, revocation has v%u", fu_firmware_get_filename(other), fu_firmware_get_id(revocation_entry), (guint)fu_firmware_get_version_raw(esp_entry), (guint)fu_firmware_get_version_raw(revocation_entry)); if (fu_firmware_get_version_raw(revocation_entry) > fu_firmware_get_version_raw(esp_entry)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_SIGNATURE_INVALID, "ESP file %s has SBAT entry %s v%u, but revocation has v%u", fu_firmware_get_filename(other), fu_firmware_get_id(revocation_entry), (guint)fu_firmware_get_version_raw(esp_entry), (guint)fu_firmware_get_version_raw(revocation_entry)); return FALSE; } } /* success */ return TRUE; } static void fu_uefi_sbat_firmware_init(FuUefiSbatFirmware *self) { fu_csv_firmware_add_column_id(FU_CSV_FIRMWARE(self), "$id"); fu_csv_firmware_add_column_id(FU_CSV_FIRMWARE(self), "$version_raw"); fu_csv_firmware_add_column_id(FU_CSV_FIRMWARE(self), "timestamp"); } static void fu_uefi_sbat_firmware_class_init(FuUefiSbatFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_uefi_sbat_firmware_parse; firmware_class->check_compatible = fu_uefi_sbat_firmware_check_compatible; } FuFirmware * fu_uefi_sbat_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_UEFI_SBAT_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/uefi-sbat/fu-uefi-sbat-firmware.h000066400000000000000000000006201501337203100225220ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_UEFI_SBAT_FIRMWARE (fu_uefi_sbat_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuUefiSbatFirmware, fu_uefi_sbat_firmware, FU, UEFI_SBAT_FIRMWARE, FuCsvFirmware) FuFirmware * fu_uefi_sbat_firmware_new(void); fwupd-2.0.10/plugins/uefi-sbat/fu-uefi-sbat-plugin.c000066400000000000000000000057661501337203100222170ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uefi-sbat-device.h" #include "fu-uefi-sbat-firmware.h" #include "fu-uefi-sbat-plugin.h" struct _FuUefiSbatPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuUefiSbatPlugin, fu_uefi_sbat_plugin, FU_TYPE_PLUGIN) static void fu_uefi_sbat_plugin_init(FuUefiSbatPlugin *self) { } static gboolean fu_uefi_sbat_plugin_startup(FuPlugin *plugin, FuProgress *progress, GError **error) { FuContext *ctx = fu_plugin_get_context(plugin); FuEfivars *efivars = fu_context_get_efivars(ctx); gboolean secureboot_enabled = FALSE; if (!fu_efivars_get_secure_boot(efivars, &secureboot_enabled, error)) return FALSE; if (!secureboot_enabled) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "SecureBoot is not enabled"); return FALSE; } /* success */ return TRUE; } static gboolean fu_uefi_sbat_plugin_coldplug(FuPlugin *plugin, FuProgress *progress, GError **error) { FuContext *ctx = fu_plugin_get_context(plugin); FuEfivars *efivars = fu_context_get_efivars(ctx); g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(FuUefiSbatDevice) device = NULL; g_autoptr(GBytes) blob = NULL; blob = fu_efivars_get_data_bytes(efivars, FU_EFIVARS_GUID_SHIM, "SbatLevelRT", NULL, error); if (blob == NULL) return FALSE; device = fu_uefi_sbat_device_new(ctx, blob, error); if (device == NULL) return FALSE; locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; fu_plugin_device_add(plugin, FU_DEVICE(device)); /* success */ return TRUE; } static gboolean fu_uefi_sbat_plugin_reboot_cleanup(FuPlugin *plugin, FuDevice *device, GError **error) { FuContext *ctx = fu_plugin_get_context(plugin); g_autoptr(GPtrArray) esp_files = NULL; /* delete any revocations that have been processed */ esp_files = fu_context_get_esp_files(ctx, FU_CONTEXT_ESP_FILE_FLAG_INCLUDE_REVOCATIONS, error); if (esp_files == NULL) return FALSE; for (guint i = 0; i < esp_files->len; i++) { FuFirmware *firmware = g_ptr_array_index(esp_files, i); g_autoptr(GFile) file = g_file_new_for_path(fu_firmware_get_filename(firmware)); if (g_file_query_exists(file, NULL)) { g_debug("deleting %s", fu_firmware_get_filename(firmware)); if (!g_file_delete(file, NULL, error)) return FALSE; } } /* success */ return TRUE; } static void fu_uefi_sbat_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_set_device_gtype_default(plugin, FU_TYPE_UEFI_SBAT_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_UEFI_SBAT_FIRMWARE); } static void fu_uefi_sbat_plugin_class_init(FuUefiSbatPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_uefi_sbat_plugin_constructed; plugin_class->startup = fu_uefi_sbat_plugin_startup; plugin_class->coldplug = fu_uefi_sbat_plugin_coldplug; plugin_class->reboot_cleanup = fu_uefi_sbat_plugin_reboot_cleanup; } fwupd-2.0.10/plugins/uefi-sbat/fu-uefi-sbat-plugin.h000066400000000000000000000003641501337203100222110ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuUefiSbatPlugin, fu_uefi_sbat_plugin, FU, UEFI_SBAT_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/uefi-sbat/meson.build000066400000000000000000000006011501337203100204110ustar00rootroot00000000000000cargs = ['-DG_LOG_DOMAIN="FuPluginUefiSbat"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_builtins += static_library('fu_plugin_uefi_sbat', sources: [ 'fu-uefi-sbat-device.c', 'fu-uefi-sbat-firmware.c', 'fu-uefi-sbat-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) fwupd-2.0.10/plugins/uefi-sbat/revocation.builder.xml000066400000000000000000000006751501337203100226020ustar00rootroot00000000000000 .text aGVsbG8gd29ybGQ= .sbata false sbat 2 fwupd-2.0.10/plugins/uf2/000077500000000000000000000000001501337203100150675ustar00rootroot00000000000000fwupd-2.0.10/plugins/uf2/README.md000066400000000000000000000042131501337203100163460ustar00rootroot00000000000000--- title: Plugin: UF2 --- ## Introduction This plugin allows the user to update any supported UF2 Device by writing firmware onto a mass storage device. A UF2 device exposes a VFAT block device which has a virtual file `INFO_UF2.TXT` where metadata can be read from. It may also have a the current firmware exported as a file `CURRENT.UF2` which is in a 512 byte-block UF2 format. Writing any file to the MSD will cause the firmware to be written. Sometimes the device will restart and the volume will be unmounted and then mounted again. In some cases the volume may not “come back†until the user manually puts the device back in programming mode. Match the block devices using the VID, PID and UUID, and then create a UF2 device which can be used to flash firmware. Note: We only read metadata from allow-listed IDs to avoid causing regressions on non-UF2 volumes. To get the UUID you can use commands like: udisksctl info -b /dev/sda1 The UF2 format is specified [here](https://github.com/Microsoft/uf2>). ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in the UF2 file format. This plugin supports the following protocol ID: * `com.microsoft.uf2` ## GUID Generation These devices use standard USB DeviceInstanceId values, e.g. * `USB\VID_1234&PID_5678` * `USB\VID_1234&PID_5678&UUID_E478-FA50` Additionally, the UF2 Board-ID and Family-ID may be added: * `UF2\BOARD_{Board-ID}` * `UF2\FAMILY_{Family-ID}` ## Update Behavior The firmware is deployed when the device is inserted, and the firmware will typically be written as the file is copied. ## Vendor ID Security The vendor ID is set from the USB vendor. ## Quirk Use This plugin uses the following plugin-specific quirks: ### `Flags=has-runtime` This is automatically set if the device is expected to go back to a runtime component, and it can also be set in the quirk file if the bootloader has a non-generic VID and PID. Since: 2.0.2 ## External Interface Access This plugin requires permission to mount, write a file and unmount the mass storage device. ## Version Considerations This plugin has been available since fwupd version `1.7.4`. fwupd-2.0.10/plugins/uf2/fu-self-test.c000066400000000000000000000033301501337203100175500ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uf2-firmware.h" static void fu_uf2_firmware_xml_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autofree gchar *csum1 = NULL; g_autofree gchar *csum2 = NULL; g_autofree gchar *xml_out = NULL; g_autofree gchar *xml_src = NULL; g_autoptr(FuFirmware) firmware1 = fu_uf2_firmware_new(); g_autoptr(FuFirmware) firmware2 = fu_uf2_firmware_new(); g_autoptr(GError) error = NULL; /* build and write */ filename = g_test_build_filename(G_TEST_DIST, "tests", "uf2.builder.xml", NULL); ret = g_file_get_contents(filename, &xml_src, NULL, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_firmware_build_from_xml(firmware1, xml_src, &error); g_assert_no_error(error); g_assert_true(ret); csum1 = fu_firmware_get_checksum(firmware1, G_CHECKSUM_SHA1, &error); g_assert_no_error(error); g_assert_cmpstr(csum1, ==, "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"); /* ensure we can round-trip */ xml_out = fu_firmware_export_to_xml(firmware1, FU_FIRMWARE_EXPORT_FLAG_NONE, &error); g_assert_no_error(error); ret = fu_firmware_build_from_xml(firmware2, xml_out, &error); g_assert_no_error(error); g_assert_true(ret); csum2 = fu_firmware_get_checksum(firmware2, G_CHECKSUM_SHA1, &error); g_assert_cmpstr(csum1, ==, csum2); } int main(int argc, char **argv) { (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); /* tests go here */ g_test_add_func("/uf2/firmware{xml}", fu_uf2_firmware_xml_func); return g_test_run(); } fwupd-2.0.10/plugins/uf2/fu-uf2-device.c000066400000000000000000000330201501337203100175720ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uf2-device.h" #include "fu-uf2-firmware.h" struct _FuUf2Device { FuBlockPartition parent_instance; guint64 family_id; FuVolume *volume; /* non-null when fwupd has mounted it privately */ }; G_DEFINE_TYPE(FuUf2Device, fu_uf2_device, FU_TYPE_BLOCK_PARTITION) #define FU_UF2_DEVICE_FLAG_HAS_RUNTIME "has-runtime" static FuFirmware * fu_uf2_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuUf2Device *self = FU_UF2_DEVICE(device); g_autoptr(FuFirmware) firmware_raw = fu_firmware_new(); g_autoptr(FuFirmware) firmware = fu_uf2_firmware_new(); if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; /* check the family_id matches if we can read the old firmware */ if (self->family_id > 0 && fu_firmware_get_idx(firmware) > 0 && self->family_id != fu_firmware_get_idx(firmware)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "family ID was different, expected 0x%08x and got 0x%08x", (guint)self->family_id, (guint)fu_firmware_get_idx(firmware)); return NULL; } /* success: but return the raw data */ if (!fu_firmware_parse_stream(firmware_raw, stream, 0x0, flags, error)) return NULL; return g_steal_pointer(&firmware_raw); } static gboolean fu_uf2_device_probe_current_fw(FuDevice *device, GBytes *fw, GError **error) { g_autofree gchar *csum_sha256 = NULL; g_autoptr(FuFirmware) firmware = fu_uf2_firmware_new(); g_autoptr(GBytes) fw_raw = NULL; /* parse to get version */ if (!fu_firmware_parse_bytes(firmware, fw, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return FALSE; if (fu_firmware_get_version(firmware) != NULL) fu_device_set_version(device, fu_firmware_get_version(firmware)); /* add instance ID for quirks */ if (fu_firmware_get_idx(firmware) != 0x0) { fu_device_add_instance_u32(device, "FAMILY", (guint32)fu_firmware_get_idx(firmware)); } (void)fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, NULL, "UF2", "FAMILY", NULL); /* add device checksum */ fw_raw = fu_firmware_get_bytes(firmware, error); if (fw_raw == NULL) return FALSE; csum_sha256 = g_compute_checksum_for_bytes(G_CHECKSUM_SHA256, fw_raw); fu_device_add_checksum(device, csum_sha256); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); /* success */ return TRUE; } static gchar * fu_uf2_device_get_full_path(FuUf2Device *self, const gchar *filename, GError **error) { g_autofree gchar *mount_point = NULL; mount_point = fu_block_partition_get_mount_point(FU_BLOCK_PARTITION(self), error); if (mount_point == NULL) return NULL; return g_build_filename(mount_point, filename, NULL); } static gboolean fu_uf2_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuUf2Device *self = FU_UF2_DEVICE(device); g_autofree gchar *fn = NULL; g_autoptr(GInputStream) stream = NULL; /* get blob */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* open file for writing; no cleverness */ fn = fu_uf2_device_get_full_path(self, "FIRMWARE.UF2", error); if (fn == NULL) return FALSE; /* success */ if (!fu_device_set_contents(device, fn, stream, progress, error)) return FALSE; /* we saw the device *come* from a runtime VID/PID, lets assume it's going back there */ if (fu_device_has_private_flag(device, FU_UF2_DEVICE_FLAG_HAS_RUNTIME)) { g_debug("expecting runtime"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); } /* success */ return TRUE; } static GBytes * fu_uf2_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuUf2Device *self = FU_UF2_DEVICE(device); g_autofree gchar *fn = NULL; /* open for reading */ fn = fu_uf2_device_get_full_path(self, "CURRENT.UF2", error); if (fn == NULL) return NULL; return fu_device_get_contents_bytes(device, fn, progress, error); } static FuFirmware * fu_uf2_device_read_firmware(FuDevice *device, FuProgress *progress, GError **error) { g_autoptr(FuFirmware) firmware = fu_uf2_firmware_new(); g_autoptr(GBytes) fw = NULL; fw = fu_device_dump_firmware(device, progress, error); if (fw == NULL) return NULL; if (!fu_firmware_parse_bytes(firmware, fw, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return NULL; return g_steal_pointer(&firmware); } static gboolean fu_uf2_device_volume_mount(FuUf2Device *self, GError **error) { const gchar *devfile = fu_udev_device_get_device_file(FU_UDEV_DEVICE(self)); /* mount volume if required */ self->volume = fu_volume_new_by_device(devfile, error); if (self->volume == NULL) return FALSE; return fu_volume_mount(self->volume, error); } static gboolean fu_uf2_device_check_volume_mounted_cb(FuDevice *device, gpointer user_data, GError **error) { const gchar *devfile = fu_udev_device_get_device_file(FU_UDEV_DEVICE(device)); g_autoptr(FuVolume) volume = NULL; if (devfile == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "invalid path: no devfile"); return FALSE; } /* mount volume if required */ volume = fu_volume_new_by_device(devfile, error); if (volume == NULL) return FALSE; if (!fu_volume_is_mounted(volume)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "is not mounted"); return FALSE; } /* success */ return TRUE; } static gboolean fu_uf2_device_open(FuDevice *device, GError **error) { FuUf2Device *self = FU_UF2_DEVICE(device); g_autoptr(GError) error_local = NULL; /* FuUdevDevice->open() */ if (!FU_DEVICE_CLASS(fu_uf2_device_parent_class)->open(device, error)) return FALSE; /* skip */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) return TRUE; /* wait for the user session to auto-mount the volume -- ideally we want to avoid using * fu_volume_mount() which would make the volume only accessible by the fwupd user */ if (!fu_device_retry_full(device, fu_uf2_device_check_volume_mounted_cb, 20, /* count */ 50, /* ms */ NULL, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { /* maybe no session running? */ if (!fu_uf2_device_volume_mount(self, error)) return FALSE; } else { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } /* success */ return TRUE; } static gboolean fu_uf2_device_close(FuDevice *device, GError **error) { FuUf2Device *self = FU_UF2_DEVICE(device); /* FuUdevDevice->close() */ if (!FU_DEVICE_CLASS(fu_uf2_device_parent_class)->close(device, error)) return FALSE; /* we only do this when mounting for the fwupd user */ if (self->volume != NULL) { if (!fu_volume_unmount(self->volume, error)) return FALSE; g_clear_object(&self->volume); } /* success */ return TRUE; } static gboolean fu_uf2_device_setup(FuDevice *device, GError **error) { FuUf2Device *self = FU_UF2_DEVICE(device); g_autofree gchar *fn1 = NULL; g_autofree gchar *fn2 = NULL; g_auto(GStrv) lines = NULL; g_autoptr(GBytes) blob_txt = NULL; g_autoptr(GBytes) fw = NULL; /* FuUdevDevice->setup() */ if (!FU_DEVICE_CLASS(fu_uf2_device_parent_class)->setup(device, error)) return FALSE; /* sanity check filesystem type */ if (g_strcmp0(fu_block_partition_get_fs_type(FU_BLOCK_PARTITION(self)), "vfat") != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "filesystem type of %s unsupported, expected vfat", fu_block_partition_get_fs_type(FU_BLOCK_PARTITION(self))); return FALSE; } /* only add UUID if it is set */ if (fu_block_partition_get_fs_uuid(FU_BLOCK_PARTITION(self)) != NULL) { fu_device_add_instance_str( device, "UUID", fu_block_partition_get_fs_uuid(FU_BLOCK_PARTITION(self))); if (!fu_device_build_instance_id(device, error, "USB", "VID", "PID", "UUID", NULL)) return FALSE; } /* this has to exist */ fn1 = fu_uf2_device_get_full_path(self, "INFO_UF2.TXT", error); if (fn1 == NULL) return FALSE; blob_txt = fu_device_get_contents_bytes(device, fn1, NULL, error); lines = fu_strsplit_bytes(blob_txt, "\n", -1); for (guint i = 0; lines[i] != NULL; i++) { if (g_str_has_prefix(lines[i], "Model: ")) { fu_device_set_name(device, lines[i] + 7); } else if (g_str_has_prefix(lines[i], "Board-ID: ")) { fu_device_add_instance_strsafe(device, "BOARD", lines[i] + 10); } } fu_device_build_instance_id(device, NULL, "UF2", "BOARD", NULL); /* this might exist */ fn2 = fu_uf2_device_get_full_path(self, "CURRENT.UF2", error); if (fn2 == NULL) return FALSE; fw = fu_device_get_contents_bytes(device, fn2, NULL, NULL); if (fw != NULL) { if (!fu_uf2_device_probe_current_fw(device, fw, error)) return FALSE; } else { fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_NUMBER); } /* success */ return TRUE; } static gboolean fu_uf2_device_usb_probe(FuUf2Device *self, FuDevice *usb_device, GError **error) { /* copy the VID and PID */ if (!fu_device_probe(usb_device, error)) return FALSE; fu_device_incorporate(FU_DEVICE(self), usb_device, FU_DEVICE_INCORPORATE_FLAG_VENDOR_IDS | FU_DEVICE_INCORPORATE_FLAG_VID | FU_DEVICE_INCORPORATE_FLAG_PID); if (!fu_device_build_instance_id_full(FU_DEVICE(self), FU_DEVICE_INSTANCE_FLAG_QUIRKS, error, "USB", "VID", NULL)) return FALSE; if (!fu_device_build_instance_id(FU_DEVICE(self), error, "USB", "VID", "PID", NULL)) return FALSE; /* success */ return TRUE; } static gboolean fu_uf2_device_probe(FuDevice *device, GError **error) { FuUf2Device *self = FU_UF2_DEVICE(device); g_autoptr(FuDevice) usb_device = NULL; /* get USB properties */ usb_device = fu_device_get_backend_parent_with_subsystem(device, "usb:usb_device", error); if (usb_device == NULL) return FALSE; if (!fu_uf2_device_usb_probe(self, usb_device, error)) return FALSE; /* check the quirk matched to avoid mounting *all* vfat devices */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not marked as updatable in uf2.quirk"); return FALSE; } /* success */ return TRUE; } static void fu_uf2_device_replace(FuDevice *device, FuDevice *donor) { /* we saw the same device come from a different VID/PID */ if (fu_device_get_vid(device) != fu_device_get_vid(donor) || fu_device_get_pid(device) != fu_device_get_pid(donor)) fu_device_add_private_flag(device, FU_UF2_DEVICE_FLAG_HAS_RUNTIME); } static void fu_uf2_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 98, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static void fu_uf2_device_to_string(FuDevice *device, guint idt, GString *str) { FuUf2Device *self = FU_UF2_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "FamilyId", self->family_id); } static void fu_uf2_device_vid_notify_cb(FuDevice *device, GParamSpec *pspec, gpointer user_data) { fu_device_add_instance_u16(device, "VID", fu_device_get_vid(device)); } static void fu_uf2_device_pid_notify_cb(FuDevice *device, GParamSpec *pspec, gpointer user_data) { fu_device_add_instance_u16(device, "PID", fu_device_get_pid(device)); } static void fu_uf2_device_init(FuUf2Device *self) { fu_device_add_protocol(FU_DEVICE(self), "com.microsoft.uf2"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_register_private_flag(FU_DEVICE(self), FU_UF2_DEVICE_FLAG_HAS_RUNTIME); g_signal_connect(FU_DEVICE(self), "notify::vid", G_CALLBACK(fu_uf2_device_vid_notify_cb), NULL); g_signal_connect(FU_DEVICE(self), "notify::pid", G_CALLBACK(fu_uf2_device_pid_notify_cb), NULL); } static void fu_uf2_device_finalize(GObject *obj) { FuUf2Device *self = FU_UF2_DEVICE(obj); /* should be done by ->close(), but check to be sure */ if (self->volume != NULL) g_object_unref(self->volume); G_OBJECT_CLASS(fu_uf2_device_parent_class)->finalize(obj); } static void fu_uf2_device_class_init(FuUf2DeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_uf2_device_finalize; device_class->to_string = fu_uf2_device_to_string; device_class->probe = fu_uf2_device_probe; device_class->setup = fu_uf2_device_setup; device_class->open = fu_uf2_device_open; device_class->close = fu_uf2_device_close; device_class->prepare_firmware = fu_uf2_device_prepare_firmware; device_class->set_progress = fu_uf2_device_set_progress; device_class->read_firmware = fu_uf2_device_read_firmware; device_class->write_firmware = fu_uf2_device_write_firmware; device_class->replace = fu_uf2_device_replace; device_class->dump_firmware = fu_uf2_device_dump_firmware; } fwupd-2.0.10/plugins/uf2/fu-uf2-device.h000066400000000000000000000004421501337203100176010ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_UF2_DEVICE (fu_uf2_device_get_type()) G_DECLARE_FINAL_TYPE(FuUf2Device, fu_uf2_device, FU, UF2_DEVICE, FuBlockPartition) fwupd-2.0.10/plugins/uf2/fu-uf2-firmware.c000066400000000000000000000177731501337203100201700ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uf2-firmware.h" #include "fu-uf2-struct.h" struct _FuUf2Firmware { FuFirmware parent_instance; }; G_DEFINE_TYPE(FuUf2Firmware, fu_uf2_firmware, FU_TYPE_FIRMWARE) #define FU_UF2_FIRMWARE_BLOCK_FLAG_NONE 0x00000000 #define FU_UF2_FIRMWARE_BLOCK_FLAG_NOFLASH 0x00000001 #define FU_UF2_FIRMWARE_BLOCK_FLAG_IS_CONTAINER 0x00001000 #define FU_UF2_FIRMWARE_BLOCK_FLAG_HAS_FAMILY 0x00002000 #define FU_UF2_FIRMWARE_BLOCK_FLAG_HAS_MD5 0x00004000 #define FU_UF2_FIRMWARE_BLOCK_FLAG_HAS_EXTENSION_TAG 0x00008000 #define FU_U2F_FIRMWARE_TAG_VERSION 0x9fc7bc /* semver of firmware file (UTF-8) */ #define FU_U2F_FIRMWARE_TAG_DESCRIPTION 0x650d9d /* description of device (UTF-8) */ #define FU_U2F_FIRMWARE_TAG_PAGE_SZ 0x0be9f7 /* page size of target device (uint32_t) */ #define FU_U2F_FIRMWARE_TAG_SHA1 0xb46db0 /* SHA-2 checksum of firmware */ #define FU_U2F_FIRMWARE_TAG_DEVICE_ID 0xc8a729 /* device type identifier (uint32_t or uint64_t) */ static gboolean fu_uf2_firmware_parse_chunk(FuUf2Firmware *self, FuChunk *chk, GByteArray *tmp, GError **error) { gsize bufsz = fu_chunk_get_data_sz(chk); const guint8 *buf = fu_chunk_get_data(chk); guint32 flags = 0; guint32 datasz = 0; g_autoptr(GByteArray) st = NULL; /* parse */ st = fu_struct_uf2_parse(fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0, /* offset */ error); if (st == NULL) return FALSE; flags = fu_struct_uf2_get_flags(st); if (flags & FU_UF2_FIRMWARE_BLOCK_FLAG_IS_CONTAINER) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "container U2F firmware not supported"); return FALSE; } datasz = fu_struct_uf2_get_payload_size(st); if (datasz > 476) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "data size impossible got 0x%08x", datasz); return FALSE; } if (fu_struct_uf2_get_block_no(st) != fu_chunk_get_idx(chk)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "block count invalid, expected 0x%04x and got 0x%04x", fu_chunk_get_idx(chk), fu_struct_uf2_get_block_no(st)); return FALSE; } if (fu_struct_uf2_get_num_blocks(st) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "block count invalid, expected > 0"); return FALSE; } if (flags & FU_UF2_FIRMWARE_BLOCK_FLAG_HAS_FAMILY) { if (fu_struct_uf2_get_family_id(st) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "family_id required but not supplied"); return FALSE; } } /* assume first chunk is representative of firmware */ if (fu_chunk_get_idx(chk) == 0) { fu_firmware_set_addr(FU_FIRMWARE(self), fu_struct_uf2_get_target_addr(st)); fu_firmware_set_idx(FU_FIRMWARE(self), fu_struct_uf2_get_family_id(st)); } /* just append raw data */ g_byte_array_append(tmp, fu_struct_uf2_get_data(st, NULL), datasz); if (flags & FU_UF2_FIRMWARE_BLOCK_FLAG_HAS_MD5) { if (datasz < 24) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not enough space for MD5 checksum"); return FALSE; } } if (flags & FU_UF2_FIRMWARE_BLOCK_FLAG_HAS_EXTENSION_TAG) { gsize offset = FU_STRUCT_UF2_OFFSET_DATA; while (offset < bufsz) { guint8 sz = 0; guint32 tag = 0; /* [SZ][TAG][TAG][TAG][TAG][DATA....] */ if (!fu_memread_uint8_safe(buf, bufsz, offset, &sz, error)) return FALSE; if (sz < 4) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid extension tag size"); return FALSE; } if (!fu_memread_uint32_safe(buf, bufsz, offset, &tag, G_LITTLE_ENDIAN, error)) return FALSE; tag &= 0xFFFFFF; if (tag == FU_U2F_FIRMWARE_TAG_VERSION) { g_autofree gchar *utf8buf = g_malloc0(sz); if (!fu_memcpy_safe((guint8 *)utf8buf, sz, 0x0, /* dst */ buf, bufsz, offset + 0x4, /* src */ sz - 4, error)) return FALSE; fu_firmware_set_version(FU_FIRMWARE(self), utf8buf); } else if (tag == FU_U2F_FIRMWARE_TAG_DESCRIPTION) { g_autofree gchar *utf8buf = g_malloc0(sz); if (!fu_memcpy_safe((guint8 *)utf8buf, sz, 0x0, /* dst */ buf, bufsz, offset + 0x4, /* src */ sz - 4, error)) return FALSE; fu_firmware_set_id(FU_FIRMWARE(self), utf8buf); } else { if (g_getenv("FWUPD_FUZZER_RUNNING") == NULL) g_warning("unknown tag 0x%06x", tag); } /* next! */ offset += sz; } } /* success */ return TRUE; } static gboolean fu_uf2_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuUf2Firmware *self = FU_UF2_FIRMWARE(firmware); g_autoptr(GByteArray) tmp = g_byte_array_new(); g_autoptr(GBytes) blob = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* read in fixed sized chunks */ chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 512, error); if (chunks == NULL) return FALSE; for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_uf2_firmware_parse_chunk(self, chk, tmp, error)) return FALSE; } /* success */ blob = g_bytes_new(tmp->data, tmp->len); fu_firmware_set_bytes(firmware, blob); return TRUE; } static GByteArray * fu_uf2_firmware_write_chunk(FuUf2Firmware *self, FuChunk *chk, guint chk_len, GError **error) { guint32 addr = fu_firmware_get_addr(FU_FIRMWARE(self)); guint32 flags = FU_UF2_FIRMWARE_BLOCK_FLAG_NONE; g_autoptr(GByteArray) st = fu_struct_uf2_new(); /* optional */ if (fu_firmware_get_idx(FU_FIRMWARE(self)) > 0) flags |= FU_UF2_FIRMWARE_BLOCK_FLAG_HAS_FAMILY; /* offset from base address */ addr += fu_chunk_get_idx(chk) * fu_chunk_get_data_sz(chk); /* build UF2 packet */ fu_struct_uf2_set_flags(st, flags); fu_struct_uf2_set_target_addr(st, addr); fu_struct_uf2_set_payload_size(st, fu_chunk_get_data_sz(chk)); fu_struct_uf2_set_block_no(st, fu_chunk_get_idx(chk)); fu_struct_uf2_set_num_blocks(st, chk_len); fu_struct_uf2_set_family_id(st, fu_firmware_get_idx(FU_FIRMWARE(self))); if (!fu_struct_uf2_set_data(st, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) return NULL; /* success */ return g_steal_pointer(&st); } static GByteArray * fu_uf2_firmware_write(FuFirmware *firmware, GError **error) { FuUf2Firmware *self = FU_UF2_FIRMWARE(firmware); g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* data first */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return NULL; /* write in chunks */ chunks = fu_chunk_array_new_from_stream(stream, fu_firmware_get_addr(firmware), FU_CHUNK_PAGESZ_NONE, 256, error); if (chunks == NULL) return NULL; for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GByteArray) tmp = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return NULL; tmp = fu_uf2_firmware_write_chunk(self, chk, fu_chunk_array_length(chunks), error); if (tmp == NULL) return NULL; g_byte_array_append(buf, tmp->data, tmp->len); } /* success */ return g_steal_pointer(&buf); } static void fu_uf2_firmware_init(FuUf2Firmware *self) { } static void fu_uf2_firmware_class_init(FuUf2FirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->parse = fu_uf2_firmware_parse; firmware_class->write = fu_uf2_firmware_write; } FuFirmware * fu_uf2_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_UF2_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/uf2/fu-uf2-firmware.h000066400000000000000000000005161501337203100201600ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_UF2_FIRMWARE (fu_uf2_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuUf2Firmware, fu_uf2_firmware, FU, UF2_FIRMWARE, FuFirmware) FuFirmware * fu_uf2_firmware_new(void); fwupd-2.0.10/plugins/uf2/fu-uf2-plugin.c000066400000000000000000000015141501337203100176340ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-uf2-device.h" #include "fu-uf2-firmware.h" #include "fu-uf2-plugin.h" struct _FuUf2Plugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuUf2Plugin, fu_uf2_plugin, FU_TYPE_PLUGIN) static void fu_uf2_plugin_init(FuUf2Plugin *self) { } static void fu_uf2_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_UF2_DEVICE); fu_plugin_add_firmware_gtype(plugin, "uf2", FU_TYPE_UF2_FIRMWARE); fu_plugin_add_device_udev_subsystem(plugin, "block:partition"); } static void fu_uf2_plugin_class_init(FuUf2PluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_uf2_plugin_constructed; } fwupd-2.0.10/plugins/uf2/fu-uf2-plugin.h000066400000000000000000000003431501337203100176400ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuUf2Plugin, fu_uf2_plugin, FU, UF2_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/uf2/fu-uf2.rs000066400000000000000000000006571501337203100165510ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(New, Parse, Default)] #[repr(C, packed)] struct FuStructUf2 { magic0: u32le == 0x0A324655, magic1: u32le == 0x9E5D5157, flags: u32le, target_addr: u32le, payload_size: u32le, block_no: u32le, num_blocks: u32le, family_id: u32le, data: [u8; 476], magic_end: u32le == 0x0AB16F30, } fwupd-2.0.10/plugins/uf2/meson.build000066400000000000000000000023411501337203100172310ustar00rootroot00000000000000if host_machine.system() == 'linux' cargs = ['-DG_LOG_DOMAIN="FuPluginUf2"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('uf2.quirk') plugin_builtin_uf2 = static_library('fu_plugin_uf2', rustgen.process( 'fu-uf2.rs', # fuzzing ), sources: [ 'fu-uf2-plugin.c', 'fu-uf2-device.c', 'fu-uf2-firmware.c', # fuzzing ], include_directories: plugin_incdirs, c_args: cargs, dependencies: plugin_deps, link_with: plugin_libs, ) plugin_builtins += plugin_builtin_uf2 if get_option('tests') install_data(['tests/uf2.builder.xml'], install_dir: join_paths(installed_test_datadir, 'tests')) env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) e = executable( 'uf2-self-test', sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: plugin_deps, link_with: [ plugin_libs, plugin_builtin_uf2, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('uf2-self-test', e, env: env) endif endif fwupd-2.0.10/plugins/uf2/tests/000077500000000000000000000000001501337203100162315ustar00rootroot00000000000000fwupd-2.0.10/plugins/uf2/tests/uf2.builder.xml000066400000000000000000000001631501337203100210740ustar00rootroot00000000000000 0x0100 aGVsbG8gd29ybGQ= fwupd-2.0.10/plugins/uf2/uf2.quirk000066400000000000000000000056161501337203100166500ustar00rootroot00000000000000# Raspberry Pi RP2 [no CURRENT.UF2] [USB\VID_2E8A&PID_0003] Guid = UF2\FAMILY_E48BFF56 Flags = updatable # Adafruit Trinket [USB\VID_1781&PID_0C9F] Flags = updatable # Adafruit Trinket M0 [USB\VID_239A&PID_001E] Flags = updatable # Adafruit Feather M0 Express [USB\VID_239A&PID_001B] Flags = updatable # nRF52840 MDK [USB\VID_239A&PID_0029] Flags = updatable # the following values are from https://github.com/microsoft/uf2/blob/master/utils/uf2families.json # although are lightly edited to split up Name and Vendor in the correct way. [UF2\FAMILY_16573617] Name = ATMEGA32 Vendor = Microchip [UF2\FAMILY_1851780A] Name = SAML21 Vendor = Microchip [UF2\FAMILY_1B57745F] Name = NRF52 Vendor = Nordic [UF2\FAMILY_1C5F21B0] Name = ESP32 Vendor = ESP32 [UF2\FAMILY_1E1F432D] Name = STM32L1 Vendor = ST [UF2\FAMILY_202E3A91] Name = STM32L0 Vendor = ST [UF2\FAMILY_21460FF0] Name = STM32WL Vendor = ST [UF2\FAMILY_2ABC77EC] Name = LPC55 Vendor = NXP [UF2\FAMILY_300F5633] Name = STM32G0 Vendor = ST [UF2\FAMILY_31D228C6] Name = GD32F350 Vendor = GD [UF2\FAMILY_04240BDF] Name = STM32L5 Vendor = ST [UF2\FAMILY_4C71240A] Name = STM32G4 Vendor = ST [UF2\FAMILY_4FB2D5BD] Name = MIMXRT10XX Vendor = NXP [UF2\FAMILY_53B80F00] Name = STM32F7 Vendor = ST [UF2\FAMILY_55114460] Name = SAMD51 Vendor = Microchip [UF2\FAMILY_57755A57] Name = STM32F4 Vendor = ST [UF2\FAMILY_5A18069B] Name = FX2 Vendor = Cypress [UF2\FAMILY_5D1A0A2E] Name = STM32F2 Vendor = ST [UF2\FAMILY_5EE21072] Name = STM32F1 Vendor = ST [UF2\FAMILY_647824B6] Name = STM32F0 Vendor = ST [UF2\FAMILY_68ED2B88] Name = SAMD21 Vendor = Microchip [UF2\FAMILY_6B846188] Name = STM32F3 Vendor = ST [UF2\FAMILY_6D0922FA] Name = STM32F407 Vendor = ST [UF2\FAMILY_6DB66082] Name = STM32H7 Vendor = ST [UF2\FAMILY_70D16653] Name = STM32WB Vendor = ST [UF2\FAMILY_7EAB61ED] Name = ESP8266 [UF2\FAMILY_7F83E793] Name = KL32L2 Vendor = NXP [UF2\FAMILY_8FB060FE] Name = STM32F407VG Vendor = ST [UF2\FAMILY_ADA52840] Name = NRF52840 Vendor = Nordic [UF2\FAMILY_BFDD4EEE] Name = ESP32S2 [UF2\FAMILY_C47E5767] Name = ESP32S3 [UF2\FAMILY_D42BA06C] Name = ESP32C3 [UF2\FAMILY_E48BFF56] Name = RP2040 Vendor = Raspberry Pi [UF2\FAMILY_00FF6919] Name = STM32L4 Vendor = ST [UF2\FAMILY_9AF03E33] Name = GD32VF103 Vendor = GD [UF2\FAMILY_4F6ACE52] Name = CSK4 Vendor = LISTENAI [UF2\FAMILY_6E7348A8] Name = CSK6 Vendor = LISTENAI [UF2\FAMILY_11DE784A] Name = M0SENSE Vendor = Sipeed [UF2\FAMILY_4B684D71] Name = MaixPlay-U4 Vendor = Sipeed [UF2\FAMILY_9517422F] Name = RZA1LU Vendor = Renesas [UF2\FAMILY_2DC309C5] Name = STM32F411xE Vendor = ST [UF2\FAMILY_06D1097B] Name = STM32F411xC Vendor = ST [UF2\FAMILY_72721D4E] Name = NRF52832xxAA Vendor = Nordic [UF2\FAMILY_6F752678] Name = NRF52832xxAB Vendor = Nordic [UF2\FAMILY_A0C97B8E] Name = AT32F415 Vendor = ArteryTek [UF2\FAMILY_699B62EC] Name = CH32V Vendor = WCH [UF2\FAMILY_7BE8976D] Name = RA4M1 Vendor = Renesas fwupd-2.0.10/plugins/upower/000077500000000000000000000000001501337203100157145ustar00rootroot00000000000000fwupd-2.0.10/plugins/upower/README.md000066400000000000000000000006451501337203100172000ustar00rootroot00000000000000--- title: Plugin: UPower --- ## Introduction This plugin is used to ensure that some updates are not done on battery power. ## Vendor ID Security This protocol does not create a device and thus requires no vendor ID set. ## External Interface Access This plugin requires access to the dbus interface `org.freedesktop.UPower`. ## Version Considerations This plugin has been available since fwupd version `0.8.0`. fwupd-2.0.10/plugins/upower/fu-upower-plugin.c000066400000000000000000000133571501337203100213160ustar00rootroot00000000000000/* * Copyright 2016 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-upower-plugin.h" struct _FuUpowerPlugin { FuPlugin parent_instance; GDBusProxy *proxy; /* nullable */ GDBusProxy *proxy_manager; /* nullable */ }; typedef enum { UP_DEVICE_STATE_UNKNOWN, UP_DEVICE_STATE_CHARGING, UP_DEVICE_STATE_DISCHARGING, UP_DEVICE_STATE_EMPTY, UP_DEVICE_STATE_FULLY_CHARGED, UP_DEVICE_STATE_PENDING_CHARGE, UP_DEVICE_STATE_PENDING_DISCHARGE, UP_DEVICE_STATE_LAST } UpDeviceState; G_DEFINE_TYPE(FuUpowerPlugin, fu_upower_plugin, FU_TYPE_PLUGIN) static void fu_upower_plugin_rescan_devices(FuPlugin *plugin) { FuUpowerPlugin *self = FU_UPOWER_PLUGIN(plugin); FuContext *ctx = fu_plugin_get_context(plugin); g_autoptr(GVariant) percentage_val = NULL; g_autoptr(GVariant) type_val = NULL; g_autoptr(GVariant) state_val = NULL; /* check that we "have" a battery */ type_val = g_dbus_proxy_get_cached_property(self->proxy, "Type"); if (type_val == NULL || g_variant_get_uint32(type_val) == 0) { fu_context_set_battery_level(ctx, FWUPD_BATTERY_LEVEL_INVALID); return; } /* get percentage */ percentage_val = g_dbus_proxy_get_cached_property(self->proxy, "Percentage"); if (percentage_val == NULL) { g_warning("failed to query power percentage level"); fu_context_set_battery_level(ctx, FWUPD_BATTERY_LEVEL_INVALID); return; } fu_context_set_battery_level(ctx, g_variant_get_double(percentage_val)); /* get state */ state_val = g_dbus_proxy_get_cached_property(self->proxy, "State"); if (state_val == NULL || g_variant_get_uint32(state_val) == 0) { g_warning("failed to query power state"); fu_context_set_battery_level(ctx, FWUPD_BATTERY_LEVEL_INVALID); } } static void fu_upower_plugin_update_lid(FuPlugin *plugin) { FuUpowerPlugin *self = FU_UPOWER_PLUGIN(plugin); FuContext *ctx = fu_plugin_get_context(plugin); g_autoptr(GVariant) lid_is_closed = NULL; g_autoptr(GVariant) lid_is_present = NULL; /* check that we "have" a lid */ lid_is_present = g_dbus_proxy_get_cached_property(self->proxy_manager, "LidIsPresent"); lid_is_closed = g_dbus_proxy_get_cached_property(self->proxy_manager, "LidIsClosed"); if (lid_is_present == NULL || lid_is_closed == NULL) { g_warning("failed to query lid state"); fu_context_set_lid_state(ctx, FU_LID_STATE_UNKNOWN); return; } if (!g_variant_get_boolean(lid_is_present)) { fu_context_set_lid_state(ctx, FU_LID_STATE_UNKNOWN); return; } if (g_variant_get_boolean(lid_is_closed)) { fu_context_set_lid_state(ctx, FU_LID_STATE_CLOSED); return; } fu_context_set_lid_state(ctx, FU_LID_STATE_OPEN); } static void fu_upower_plugin_update_battery(FuPlugin *plugin) { FuUpowerPlugin *self = FU_UPOWER_PLUGIN(plugin); FuContext *ctx = fu_plugin_get_context(plugin); g_autoptr(GVariant) on_battery = NULL; on_battery = g_dbus_proxy_get_cached_property(self->proxy_manager, "OnBattery"); if (on_battery == NULL) { fu_context_set_power_state(ctx, FU_POWER_STATE_AC); return; } if (g_variant_get_boolean(on_battery)) fu_context_set_power_state(ctx, FU_POWER_STATE_BATTERY); else fu_context_set_power_state(ctx, FU_POWER_STATE_AC); } static void fu_upower_plugin_rescan_manager(FuPlugin *plugin) { fu_upower_plugin_update_lid(plugin); fu_upower_plugin_update_battery(plugin); } static void fu_upower_plugin_proxy_changed_cb(GDBusProxy *proxy, GVariant *changed_properties, GStrv invalidated_properties, FuPlugin *plugin) { fu_upower_plugin_rescan_manager(plugin); fu_upower_plugin_rescan_devices(plugin); } static gboolean fu_upower_plugin_startup(FuPlugin *plugin, FuProgress *progress, GError **error) { FuUpowerPlugin *self = FU_UPOWER_PLUGIN(plugin); g_autofree gchar *name_owner = NULL; self->proxy_manager = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, NULL, "org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.UPower", NULL, error); if (self->proxy_manager == NULL) { g_prefix_error(error, "failed to connect to upower: "); return FALSE; } self->proxy = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, NULL, "org.freedesktop.UPower", "/org/freedesktop/UPower/devices/DisplayDevice", "org.freedesktop.UPower.Device", NULL, error); if (self->proxy == NULL) { g_prefix_error(error, "failed to connect to upower: "); return FALSE; } name_owner = g_dbus_proxy_get_name_owner(self->proxy); if (name_owner == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no owner for %s", g_dbus_proxy_get_name(self->proxy)); return FALSE; } g_signal_connect(G_DBUS_PROXY(self->proxy), "g-properties-changed", G_CALLBACK(fu_upower_plugin_proxy_changed_cb), plugin); g_signal_connect(G_DBUS_PROXY(self->proxy_manager), "g-properties-changed", G_CALLBACK(fu_upower_plugin_proxy_changed_cb), plugin); fu_upower_plugin_rescan_devices(plugin); fu_upower_plugin_rescan_manager(plugin); /* success */ return TRUE; } static void fu_upower_plugin_init(FuUpowerPlugin *self) { } static void fu_upower_plugin_finalize(GObject *obj) { FuUpowerPlugin *self = FU_UPOWER_PLUGIN(obj); if (self->proxy != NULL) g_object_unref(self->proxy); if (self->proxy_manager != NULL) g_object_unref(self->proxy_manager); G_OBJECT_CLASS(fu_upower_plugin_parent_class)->finalize(obj); } static void fu_upower_plugin_class_init(FuUpowerPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_upower_plugin_finalize; plugin_class->startup = fu_upower_plugin_startup; } fwupd-2.0.10/plugins/upower/fu-upower-plugin.h000066400000000000000000000003541501337203100213140ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuUpowerPlugin, fu_upower_plugin, FU, UPOWER_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/upower/meson.build000066400000000000000000000005501501337203100200560ustar00rootroot00000000000000if host_machine.system() == 'linux' plugins += {meson.current_source_dir().split('/')[-1]: true} cargs = ['-DG_LOG_DOMAIN="FuPluginUpower"'] plugin_builtins += static_library('fu_plugin_upower', sources: [ 'fu-upower-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) endif fwupd-2.0.10/plugins/usi-dock/000077500000000000000000000000001501337203100161115ustar00rootroot00000000000000fwupd-2.0.10/plugins/usi-dock/README.md000066400000000000000000000027761501337203100174040ustar00rootroot00000000000000--- title: Plugin: USI Dock --- ## Introduction This plugin uses the MCU to write all the dock firmware components. The MCU version is provided by the DMC bcdDevice. This plugin supports the following protocol ID: * `com.usi.dock` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_17EF&PID_7226` Additionally, some extra "component ID" instance IDs are added. * `USB\VID_17EF&PID_7226&CID_USB3` * `USB\VID_17EF&PID_7226&CID_40B0&DMCVER_10.10` Additionally, some extra "REV version" instance values are added. * `USB\VID_17EF&PID_7226&CID_40B0&REV_0040` ## Update Behavior The device usually presents in runtime mode, but on detach re-enumerates with the same USB PID in an unlocked mode. On attach the device again re-enumerates back to the runtime locked mode. ## Vendor ID Security The vendor ID is set from the USB vendor. ## Quirk Use This plugin uses the following plugin-specific quirks: ### `Flags=verfmt-hp` Use the HP-style `quad` version format. Since: 1.7.4 ### `Flags=set-chip-type` Workaround a provisioning problem by setting the chip type when the new update has completed. Since: 1.9.2 ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.7.4`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Victor Cheng: @victor-cheng fwupd-2.0.10/plugins/usi-dock/data/000077500000000000000000000000001501337203100170225ustar00rootroot00000000000000fwupd-2.0.10/plugins/usi-dock/data/lsusb.txt000066400000000000000000000044551501337203100207230ustar00rootroot00000000000000Bus 001 Device 024: ID 17ef:30b4 Lenovo ThinkPad Thunderbolt 4 Dock MCU Controller Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 0 bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 64 idVendor 0x17ef Lenovo idProduct 0x30b4 bcdDevice 1.00 iManufacturer 1 Lenovo iProduct 2 ThinkPad Thunderbolt 4 Dock MCU Controller iSerial 3 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 0x0029 bNumInterfaces 1 bConfigurationValue 1 iConfiguration 0 bmAttributes 0x80 (Bus Powered) MaxPower 100mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 2 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 0 bInterfaceProtocol 0 iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.11 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 39 Report Descriptors: ** UNAVAILABLE ** Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 1 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x02 EP 2 OUT bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 1 Device Status: 0x0000 (Bus Powered) fwupd-2.0.10/plugins/usi-dock/fu-usi-dock-child-device.c000066400000000000000000000052371501337203100227300ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-usi-dock-child-device.h" #include "fu-usi-dock-mcu-device.h" struct _FuUsiDockChildDevice { FuDevice parent_instance; FuUsiDockFirmwareIdx chip_idx; }; G_DEFINE_TYPE(FuUsiDockChildDevice, fu_usi_dock_child_device, FU_TYPE_DEVICE) FuUsiDockFirmwareIdx fu_usi_dock_child_device_get_chip_idx(FuUsiDockChildDevice *self) { return self->chip_idx; } void fu_usi_dock_child_device_set_chip_idx(FuUsiDockChildDevice *self, FuUsiDockFirmwareIdx chip_idx) { self->chip_idx = chip_idx; } static void fu_usi_dock_child_device_to_string(FuDevice *device, guint idt, GString *str) { FuUsiDockChildDevice *self = FU_USI_DOCK_CHILD_DEVICE(device); fwupd_codec_string_append(str, idt, "ChipIdx", fu_usi_dock_firmware_idx_to_string(self->chip_idx)); } /* use the parents parser */ static FuFirmware * fu_usi_dock_child_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuDevice *parent = fu_device_get_parent(device); if (parent == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no parent"); return NULL; } return fu_device_prepare_firmware(parent, stream, progress, flags, error); } /* only update this specific child component */ static gboolean fu_usi_dock_child_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuUsiDockChildDevice *self = FU_USI_DOCK_CHILD_DEVICE(device); FuDevice *parent = fu_device_get_parent(device); if (parent == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no parent"); return FALSE; } return fu_usi_dock_mcu_device_write_firmware_with_idx(FU_USI_DOCK_MCU_DEVICE(parent), firmware, self->chip_idx, progress, flags, error); } static void fu_usi_dock_child_device_init(FuUsiDockChildDevice *self) { fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PARENT_FOR_OPEN); } static void fu_usi_dock_child_device_class_init(FuUsiDockChildDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_usi_dock_child_device_to_string; device_class->prepare_firmware = fu_usi_dock_child_device_prepare_firmware; device_class->write_firmware = fu_usi_dock_child_device_write_firmware; } FuDevice * fu_usi_dock_child_device_new(FuContext *ctx) { return g_object_new(FU_TYPE_USI_DOCK_CHILD_DEVICE, "context", ctx, NULL); } fwupd-2.0.10/plugins/usi-dock/fu-usi-dock-child-device.h000066400000000000000000000013011501337203100227210ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-usi-dock-struct.h" #define FU_TYPE_USI_DOCK_CHILD_DEVICE (fu_usi_dock_child_device_get_type()) G_DECLARE_FINAL_TYPE(FuUsiDockChildDevice, fu_usi_dock_child_device, FU, USI_DOCK_CHILD_DEVICE, FuDevice) FuDevice * fu_usi_dock_child_device_new(FuContext *ctx) G_GNUC_NON_NULL(1); FuUsiDockFirmwareIdx fu_usi_dock_child_device_get_chip_idx(FuUsiDockChildDevice *self) G_GNUC_NON_NULL(1); void fu_usi_dock_child_device_set_chip_idx(FuUsiDockChildDevice *self, FuUsiDockFirmwareIdx chip_idx) G_GNUC_NON_NULL(1); fwupd-2.0.10/plugins/usi-dock/fu-usi-dock-dmc-device.c000066400000000000000000000050661501337203100224100ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-usi-dock-dmc-device.h" struct _FuUsiDockDmcDevice { FuUsbDevice parent_instance; }; G_DEFINE_TYPE(FuUsiDockDmcDevice, fu_usi_dock_dmc_device, FU_TYPE_USB_DEVICE) static void fu_usi_dock_dmc_device_parent_notify_cb(FuDevice *device, GParamSpec *pspec, gpointer user_data) { FuDevice *parent = fu_device_get_parent(device); if (parent != NULL) { g_autoptr(GError) error = NULL; const gchar *serialnum; /* slightly odd: the MCU device uses the DMC version number */ g_info("absorbing DMC version into MCU"); fu_device_set_version_format(parent, fu_device_get_version_format(device)); fu_device_set_version(parent, fu_device_get_version(device)); fu_device_set_serial(parent, fu_device_get_serial(device)); /* allow matching firmware */ fu_device_add_instance_str(parent, "CID", fu_device_get_name(device)); if (!fu_device_build_instance_id(parent, &error, "USB", "VID", "PID", "CID", NULL)) { g_warning("failed to build ID: %s", error->message); return; } /* this might match Flags=set-chip-type */ fu_device_add_instance_str(parent, "DMCVER", fu_device_get_version(device)); if (!fu_device_build_instance_id_full(parent, FU_DEVICE_INSTANCE_FLAG_QUIRKS, &error, "USB", "VID", "PID", "CID", "DMCVER", NULL)) { g_warning("failed to build MCU DMC Instance ID: %s", error->message); return; } /* allow matching PCB version */ serialnum = fu_device_get_serial(device); if (serialnum != NULL && strlen(serialnum) >= 10) { if (serialnum[6] == 'Z' && serialnum[7] == 'D') { if (serialnum[9] == 'A' || serialnum[9] == 'B') { fu_device_add_instance_u16(parent, "REV", 0x40); } else { fu_device_add_instance_u16(parent, "REV", 0x42); } } if (!fu_device_build_instance_id(parent, &error, "USB", "VID", "PID", "CID", "REV", NULL)) { g_warning("failed to build ID: %s", error->message); return; } } /* use a better device name */ fu_device_set_name(device, "Dock Management Controller Information"); } } static void fu_usi_dock_dmc_device_init(FuUsiDockDmcDevice *self) { g_signal_connect(FU_DEVICE(self), "notify::parent", G_CALLBACK(fu_usi_dock_dmc_device_parent_notify_cb), NULL); } static void fu_usi_dock_dmc_device_class_init(FuUsiDockDmcDeviceClass *klass) { } fwupd-2.0.10/plugins/usi-dock/fu-usi-dock-dmc-device.h000066400000000000000000000005431501337203100224100ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_USI_DOCK_DMC_DEVICE (fu_usi_dock_dmc_device_get_type()) G_DECLARE_FINAL_TYPE(FuUsiDockDmcDevice, fu_usi_dock_dmc_device, FU, USI_DOCK_DMC_DEVICE, FuUsbDevice) fwupd-2.0.10/plugins/usi-dock/fu-usi-dock-mcu-device.c000066400000000000000000000650761501337203100224400ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * Copyright 2021 Victor Cheng * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-usi-dock-child-device.h" #include "fu-usi-dock-dmc-device.h" #include "fu-usi-dock-mcu-device.h" #include "fu-usi-dock-struct.h" struct _FuUsiDockMcuDevice { FuHidDevice parent_instance; }; G_DEFINE_TYPE(FuUsiDockMcuDevice, fu_usi_dock_mcu_device, FU_TYPE_HID_DEVICE) #define FU_USI_DOCK_MCU_DEVICE_TIMEOUT 5000 /* ms */ #define USB_HID_REPORT_ID1 1u #define USB_HID_REPORT_ID2 2u #define DP_VERSION_FROM_MCU 0x01 /* if in use */ #define NIC_VERSION_FROM_MCU 0x2 /* if in use */ #define W25Q16DV_PAGE_SIZE 256 #define FU_USI_DOCK_DEVICE_FLAG_VERFMT_HP "verfmt-hp" #define FU_USI_DOCK_DEVICE_FLAG_SET_CHIP_TYPE "set-chip-type" #define FU_USI_DOCK_DEVICE_FLAG_WAITING_FOR_UNPLUG "waiting-for-unplug" static gboolean fu_usi_dock_mcu_device_tx(FuUsiDockMcuDevice *self, FuUsiDockTag2 tag2, const guint8 *buf, gsize bufsz, GError **error) { g_autoptr(GByteArray) st = fu_struct_usi_dock_mcu_cmd_req_new(); fu_struct_usi_dock_mcu_cmd_req_set_length(st, 0x3 + bufsz); fu_struct_usi_dock_mcu_cmd_req_set_tag3(st, tag2); if (buf != NULL) { if (!fu_struct_usi_dock_mcu_cmd_req_set_buf(st, buf, bufsz, error)) return FALSE; } /* special cases */ if (st->data[FU_STRUCT_USI_DOCK_MCU_CMD_REQ_OFFSET_BUF + 0] == FU_USI_DOCK_MCU_CMD_FW_UPDATE) st->data[FU_STRUCT_USI_DOCK_MCU_CMD_REQ_OFFSET_BUF + 1] = 0xFF; return fu_hid_device_set_report(FU_HID_DEVICE(self), USB_HID_REPORT_ID2, st->data, st->len, FU_USI_DOCK_MCU_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error); } static gboolean fu_usi_dock_mcu_device_rx(FuUsiDockMcuDevice *self, guint8 cmd, guint8 *outbuf, gsize outbufsz, GError **error) { guint8 buf[64] = {0}; g_autoptr(GByteArray) st_rsp = NULL; if (!fu_hid_device_get_report(FU_HID_DEVICE(self), USB_HID_REPORT_ID2, buf, sizeof(buf), FU_USI_DOCK_MCU_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER | FU_HID_DEVICE_FLAG_RETRY_FAILURE, error)) { return FALSE; } st_rsp = fu_struct_usi_dock_mcu_cmd_res_parse(buf, sizeof(buf), 0x0, error); if (st_rsp == NULL) return FALSE; if (outbuf != NULL) { if (!fu_memcpy_safe(outbuf, outbufsz, 0x0, /* dst */ buf, sizeof(buf), FU_STRUCT_USI_DOCK_MCU_CMD_RES_OFFSET_BUF, /* src */ outbufsz, error)) return FALSE; } return TRUE; } static gboolean fu_usi_dock_mcu_device_txrx(FuUsiDockMcuDevice *self, FuUsiDockTag2 tag2, const guint8 *inbuf, gsize inbufsz, guint8 *outbuf, gsize outbufsz, GError **error) { if (!fu_usi_dock_mcu_device_tx(self, tag2, inbuf, inbufsz, error)) { g_prefix_error(error, "failed to transmit: "); return FALSE; } if (!fu_usi_dock_mcu_device_rx(self, FU_USI_DOCK_MCU_CMD_ALL, outbuf, outbufsz, error)) { g_prefix_error(error, "failed to receive: "); return FALSE; } return TRUE; } static gboolean fu_usi_dock_mcu_device_get_status(FuUsiDockMcuDevice *self, GError **error) { guint8 cmd = FU_USI_DOCK_MCU_CMD_MCU_STATUS; guint8 response = 0; if (!fu_usi_dock_mcu_device_txrx(self, FU_USI_DOCK_TAG2_CMD_MCU, &cmd, sizeof(cmd), &response, sizeof(response), error)) { g_prefix_error(error, "failed to send CMD MCU: "); return FALSE; } if (response == 0x1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "device is busy"); return FALSE; } if (response == 0xFF) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "device timed out"); return FALSE; } /* success */ return TRUE; } static gboolean fu_usi_dock_mcu_device_enumerate_children(FuUsiDockMcuDevice *self, GError **error) { guint8 inbuf[] = {FU_USI_DOCK_MCU_CMD_READ_MCU_VERSIONPAGE, DP_VERSION_FROM_MCU | NIC_VERSION_FROM_MCU}; guint8 outbuf[49] = {0x0}; struct { const gchar *name; guint8 chip_idx; gsize offset; } components[] = { {"DMC", FU_USI_DOCK_FIRMWARE_IDX_DMC_PD, FU_STRUCT_USI_DOCK_ISP_VERSION_OFFSET_DMC}, {"PD", FU_USI_DOCK_FIRMWARE_IDX_DP, FU_STRUCT_USI_DOCK_ISP_VERSION_OFFSET_PD}, {"DP5x", FU_USI_DOCK_FIRMWARE_IDX_NONE, FU_STRUCT_USI_DOCK_ISP_VERSION_OFFSET_DP5X}, {"DP6x", FU_USI_DOCK_FIRMWARE_IDX_NONE, FU_STRUCT_USI_DOCK_ISP_VERSION_OFFSET_DP6X}, {"TBT4", FU_USI_DOCK_FIRMWARE_IDX_TBT4, FU_STRUCT_USI_DOCK_ISP_VERSION_OFFSET_TBT4}, {"USB3", FU_USI_DOCK_FIRMWARE_IDX_USB3, FU_STRUCT_USI_DOCK_ISP_VERSION_OFFSET_USB3}, {"USB2", FU_USI_DOCK_FIRMWARE_IDX_USB2, FU_STRUCT_USI_DOCK_ISP_VERSION_OFFSET_USB2}, {"AUDIO", FU_USI_DOCK_FIRMWARE_IDX_AUDIO, FU_STRUCT_USI_DOCK_ISP_VERSION_OFFSET_AUDIO}, {"I255", FU_USI_DOCK_FIRMWARE_IDX_I225, FU_STRUCT_USI_DOCK_ISP_VERSION_OFFSET_I255}, {"MCU", FU_USI_DOCK_FIRMWARE_IDX_MCU, FU_STRUCT_USI_DOCK_ISP_VERSION_OFFSET_MCU}, {"bcdVersion", FU_USI_DOCK_FIRMWARE_IDX_NONE, FU_STRUCT_USI_DOCK_ISP_VERSION_OFFSET_BCDVERSION}, {NULL, 0, 0}}; /* assume DP and NIC in-use */ if (!fu_usi_dock_mcu_device_txrx(self, FU_USI_DOCK_TAG2_CMD_MCU, inbuf, sizeof(inbuf), outbuf, sizeof(outbuf), error)) return FALSE; for (guint i = 0; components[i].name != NULL; i++) { const guint8 *val = outbuf + components[i].offset; g_autofree gchar *version = NULL; g_autoptr(FuDevice) child = NULL; child = fu_usi_dock_child_device_new(fu_device_get_context(FU_DEVICE(self))); if (g_strcmp0(components[i].name, "bcdVersion") == 0) { if ((val[0] == 0x00 && val[1] == 0x00) || (val[0] == 0xFF && val[1] == 0xFF)) { g_debug("ignoring %s", components[i].name); continue; } if (fu_device_has_private_flag(FU_DEVICE(self), FU_USI_DOCK_DEVICE_FLAG_VERFMT_HP)) { version = g_strdup_printf("%x.%x.%x.%x", (guint)(val[0] >> 4), val[0] & 0xFu, (guint)(val[1] >> 4), val[1] & 0xFu); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); fu_device_set_version(FU_DEVICE(self), version); } else { version = g_strdup_printf("%x.%x.%02x", val[0] & 0xFu, (guint)(val[0] >> 4), val[1]); g_debug("ignoring %s --> %s", components[i].name, version); } continue; } if (g_strcmp0(components[i].name, "DMC") == 0) { if ((val[2] == 0x00 && val[3] == 0x00 && val[4] == 0x00) || (val[2] == 0xFF && val[3] == 0xFF && val[4] == 0xFF)) { g_debug("ignoring %s", components[i].name); continue; } version = g_strdup_printf("%d.%d.%d", val[2], val[3], val[4]); fu_device_set_version_format(child, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(child, version); fu_device_set_name(child, "Dock Management Controller"); } else if (g_strcmp0(components[i].name, "PD") == 0) { if ((val[1] == 0x00 && val[2] == 0x00 && val[3] == 0x00 && val[4] == 0x00) || (val[1] == 0xFF && val[2] == 0xFF && val[3] == 0xFF && val[4] == 0xFF)) { g_debug("ignoring %s", components[i].name); continue; } if (fu_device_has_private_flag(FU_DEVICE(self), FU_USI_DOCK_DEVICE_FLAG_VERFMT_HP)) { version = g_strdup_printf("%d.%d.%d.%d", val[3], val[4], val[1], val[2]); fu_device_set_version_format(child, FWUPD_VERSION_FORMAT_QUAD); } else { version = g_strdup_printf("%d.%d.%d", val[2], val[3], val[4]); fu_device_set_version_format(child, FWUPD_VERSION_FORMAT_TRIPLET); } fu_device_set_version(child, version); fu_device_set_name(child, "Power Delivery"); } else if (g_strcmp0(components[i].name, "TBT4") == 0) { if ((val[1] == 0x00 && val[2] == 0x00 && val[3] == 0x00) || (val[1] == 0xFF && val[2] == 0xFF && val[3] == 0xFF)) { g_debug("ignoring %s", components[i].name); continue; } version = g_strdup_printf("%02x.%02x.%02x", val[1], val[2], val[3]); fu_device_set_version_format(child, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(child, version); fu_device_add_icon(child, FU_DEVICE_ICON_THUNDERBOLT); fu_device_set_name(child, "Thunderbolt 4 Controller"); } else if (g_strcmp0(components[i].name, "DP5x") == 0) { if ((val[2] == 0x00 && val[3] == 0x00 && val[4] == 0x00) || (val[2] == 0xFF && val[3] == 0xFF && val[4] == 0xFF)) { g_debug("ignoring %s", components[i].name); continue; } version = g_strdup_printf("%d.%02d.%03d", val[2], val[3], val[4]); fu_device_set_version_format(child, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(child, version); fu_device_add_icon(child, FU_DEVICE_ICON_VIDEO_DISPLAY); fu_device_set_name(child, "Display Port 5"); } else if (g_strcmp0(components[i].name, "DP6x") == 0) { if ((val[2] == 0x00 && val[3] == 0x00 && val[4] == 0x00) || (val[2] == 0xFF && val[3] == 0xFF && val[4] == 0xFF)) { g_debug("ignoring %s", components[i].name); continue; } if (fu_device_has_private_flag(FU_DEVICE(self), FU_USI_DOCK_DEVICE_FLAG_VERFMT_HP)) { version = g_strdup_printf("%x.%x.%x.%x", val[3], val[4], val[2], val[1]); fu_device_set_version_format(child, FWUPD_VERSION_FORMAT_QUAD); fu_device_set_name(child, "USB/PD HUB"); } else { version = g_strdup_printf("%d.%02d.%03d", val[2], val[3], val[4]); fu_device_set_version_format(child, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_name(child, "Display Port 6"); } fu_device_set_version(child, version); fu_device_add_icon(child, FU_DEVICE_ICON_VIDEO_DISPLAY); } else if (g_strcmp0(components[i].name, "USB3") == 0) { if ((val[3] == 0x00 && val[4] == 0x00) || (val[3] == 0xFF && val[4] == 0xFF)) { g_debug("ignoring %s", components[i].name); continue; } version = g_strdup_printf("%02X%02X", val[3], val[4]); fu_device_set_version_format(child, FWUPD_VERSION_FORMAT_NUMBER); fu_device_set_version(child, version); fu_device_set_name(child, "USB 3 Hub"); } else if (g_strcmp0(components[i].name, "USB2") == 0) { if ((val[0] == 0x00 && val[1] == 0x00 && val[2] == 0x00 && val[3] == 0x00 && val[4] == 0x00) || (val[0] == 0xFF && val[1] == 0xFF && val[2] == 0xFF && val[3] == 0xFF && val[4] == 0xFF)) { g_debug("ignoring %s", components[i].name); continue; } version = g_strdup_printf("%c%c%c%c%c", val[0], val[1], val[2], val[3], val[4]); fu_device_set_version_format(child, FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_version(child, version); fu_device_set_name(child, "USB 2 Hub"); } else if (g_strcmp0(components[i].name, "AUDIO") == 0) { if ((val[2] == 0x00 && val[3] == 0x00 && val[4] == 0x00) || (val[2] == 0xFF && val[3] == 0xFF && val[4] == 0xFF)) { g_debug("ignoring %s", components[i].name); continue; } version = g_strdup_printf("%02X-%02X-%02X", val[2], val[3], val[4]); fu_device_set_version_format(child, FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_version(child, version); fu_device_set_name(child, "Audio Controller"); } else if (g_strcmp0(components[i].name, "I255") == 0) { if ((val[2] == 0x00 && val[3] == 0x00 && val[4] == 0x00) || (val[2] == 0xFF && val[3] == 0xFF && val[4] == 0xFF)) { g_debug("ignoring %s", components[i].name); continue; } version = g_strdup_printf("%u.%u.%u", (guint)(val[2] >> 4), val[3], val[4]); fu_device_set_version_format(child, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(child, version); fu_device_add_icon(child, FU_DEVICE_ICON_NETWORK_WIRED); fu_device_set_name(child, "Ethernet Adapter"); } else if (g_strcmp0(components[i].name, "MCU") == 0) { if ((val[0] == 0x00 && val[1] == 0x00) || (val[0] == 0xFF && val[1] == 0xFF)) { g_debug("ignoring %s", components[i].name); continue; } if (fu_device_has_private_flag(FU_DEVICE(self), FU_USI_DOCK_DEVICE_FLAG_VERFMT_HP)) { version = g_strdup_printf("%x.%x.%x.%x", (guint)(val[0] >> 4), val[0] & 0xFu, (guint)(val[1] >> 4), val[1] & 0xFu); fu_device_set_version_format(child, FWUPD_VERSION_FORMAT_QUAD); } else { version = g_strdup_printf("%X.%X", val[0], val[1]); fu_device_set_version_format(child, FWUPD_VERSION_FORMAT_PLAIN); } fu_device_set_version(child, version); fu_device_set_name(child, "Dock Management Controller"); } else { g_warning("unhandled %s", components[i].name); } /* add virtual device */ fu_device_add_instance_u16(child, "VID", fu_device_get_vid(FU_DEVICE(self))); fu_device_add_instance_u16(child, "PID", fu_device_get_pid(FU_DEVICE(self))); fu_device_add_instance_str(child, "CID", components[i].name); if (!fu_device_build_instance_id(child, error, "USB", "VID", "PID", "CID", NULL)) return FALSE; if (fu_device_get_name(child) == NULL) fu_device_set_name(child, components[i].name); fu_device_set_logical_id(child, components[i].name); fu_usi_dock_child_device_set_chip_idx(FU_USI_DOCK_CHILD_DEVICE(child), components[i].chip_idx); fu_device_add_child(FU_DEVICE(self), child); } /* success */ return TRUE; } static gboolean fu_usi_dock_mcu_device_setup(FuDevice *device, GError **error) { FuUsiDockMcuDevice *self = FU_USI_DOCK_MCU_DEVICE(device); /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_usi_dock_mcu_device_parent_class)->setup(device, error)) return FALSE; /* get status and component versions */ if (!fu_usi_dock_mcu_device_get_status(self, error)) { g_prefix_error(error, "failed to get status: "); return FALSE; } if (!fu_usi_dock_mcu_device_enumerate_children(self, error)) { g_prefix_error(error, "failed to enumerate children: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_usi_dock_mcu_device_write_chunk(FuUsiDockMcuDevice *self, FuChunk *chk, GError **error) { g_autoptr(GByteArray) st_req = fu_struct_usi_dock_hid_req_new(); fu_struct_usi_dock_hid_req_set_length(st_req, fu_chunk_get_data_sz(chk)); fu_struct_usi_dock_hid_req_set_tag3(st_req, FU_USI_DOCK_TAG2_MASS_DATA_SPI); if (!fu_memcpy_safe(st_req->data, st_req->len, FU_STRUCT_USI_DOCK_HID_REQ_OFFSET_BUF, /* dst */ fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0x0, /* src */ fu_chunk_get_data_sz(chk), error)) return FALSE; if (!fu_hid_device_set_report(FU_HID_DEVICE(self), USB_HID_REPORT_ID2, st_req->data, st_req->len, FU_USI_DOCK_MCU_DEVICE_TIMEOUT, FU_HID_DEVICE_FLAG_USE_INTERRUPT_TRANSFER, error)) return FALSE; return fu_usi_dock_mcu_device_rx(self, FU_USI_DOCK_MCU_CMD_ALL, NULL, 0x0, error); } static gboolean fu_usi_dock_mcu_device_write_page(FuUsiDockMcuDevice *self, FuChunk *chk_page, GError **error) { g_autoptr(FuChunkArray) chunks = NULL; g_autoptr(GBytes) chk_blob = fu_chunk_get_bytes(chk_page); chunks = fu_chunk_array_new_from_bytes(chk_blob, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_STRUCT_USI_DOCK_HID_REQ_SIZE_BUF); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_usi_dock_mcu_device_write_chunk(self, chk, error)) return FALSE; } return TRUE; } static gboolean fu_usi_dock_mcu_device_write_pages(FuUsiDockMcuDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_usi_dock_mcu_device_write_page(self, chk, error)) { g_prefix_error(error, "failed to write chunk 0x%x: ", i); return FALSE; } fu_progress_step_done(progress); } return TRUE; } static gboolean fu_usi_dock_mcu_device_wait_for_spi_ready_cb(FuDevice *device, gpointer user_data, GError **error) { FuUsiDockMcuDevice *self = FU_USI_DOCK_MCU_DEVICE(device); guint8 buf[] = {FU_USI_DOCK_SPI_CMD_READ_STATUS}; guint8 val = 0; if (!fu_usi_dock_mcu_device_txrx(self, FU_USI_DOCK_TAG2_CMD_SPI, buf, sizeof(buf), &val, sizeof(val), error)) return FALSE; if (val != FU_USI_DOCK_SPI_STATE_READY) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "SPI state is %s [0x%02x]", fu_usi_dock_spi_state_to_string(val), val); return FALSE; } /* success */ return TRUE; } static gboolean fu_usi_dock_mcu_device_wait_for_spi_initial_ready_cb(FuDevice *device, gpointer user_data, GError **error) { FuUsiDockMcuDevice *self = FU_USI_DOCK_MCU_DEVICE(device); guint8 buf[] = {FU_USI_DOCK_SPI_CMD_INITIAL}; guint8 val = 0; if (!fu_usi_dock_mcu_device_txrx(self, FU_USI_DOCK_TAG2_CMD_SPI, buf, sizeof(buf), &val, sizeof(val), error)) return FALSE; if (val != FU_USI_DOCK_SPI_STATE_READY) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "SPI state is %s [0x%02x]", fu_usi_dock_spi_state_to_string(val), val); return FALSE; } /* success */ return TRUE; } static gboolean fu_usi_dock_mcu_device_wait_for_checksum_cb(FuDevice *device, gpointer user_data, GError **error) { FuUsiDockMcuDevice *self = FU_USI_DOCK_MCU_DEVICE(device); if (!fu_usi_dock_mcu_device_rx(self, FU_USI_DOCK_MCU_CMD_ALL, (guint8 *)user_data, sizeof(guint8), error)) return FALSE; /* success */ return TRUE; } gboolean fu_usi_dock_mcu_device_write_firmware_with_idx(FuUsiDockMcuDevice *self, FuFirmware *firmware, guint8 chip_idx, FuProgress *progress, FwupdInstallFlags flags, GError **error) { guint8 cmd; g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; guint8 checksum = 0xFF; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 5, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 69, "write-external"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 25, "wait-for-checksum"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 0, "internal-flash"); /* initial external flash */ if (!fu_device_retry(FU_DEVICE(self), fu_usi_dock_mcu_device_wait_for_spi_initial_ready_cb, 30, NULL, error)) { g_prefix_error(error, "failed to wait for initial: "); return FALSE; } fu_progress_step_done(progress); /* erase external flash */ cmd = FU_USI_DOCK_SPI_CMD_ERASE_FLASH; if (!fu_usi_dock_mcu_device_txrx(self, FU_USI_DOCK_TAG2_CMD_SPI, &cmd, sizeof(cmd), NULL, 0x0, error)) return FALSE; if (!fu_device_retry(FU_DEVICE(self), fu_usi_dock_mcu_device_wait_for_spi_ready_cb, 30, NULL, error)) { g_prefix_error(error, "failed to wait for erase: "); return FALSE; } fu_progress_step_done(progress); /* write external flash */ cmd = FU_USI_DOCK_SPI_CMD_PROGRAM; if (!fu_usi_dock_mcu_device_txrx(self, FU_USI_DOCK_TAG2_CMD_SPI, &cmd, sizeof(cmd), NULL, 0x0, error)) return FALSE; stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, W25Q16DV_PAGE_SIZE, error); if (chunks == NULL) return FALSE; if (!fu_usi_dock_mcu_device_write_pages(self, chunks, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* file transfer – finished */ cmd = FU_USI_DOCK_SPI_CMD_TRANSFER_FINISH; if (!fu_usi_dock_mcu_device_txrx(self, FU_USI_DOCK_TAG2_CMD_SPI, &cmd, sizeof(cmd), NULL, 0x0, error)) return FALSE; /* MCU checksum */ if (!fu_device_retry(FU_DEVICE(self), fu_usi_dock_mcu_device_wait_for_checksum_cb, 300, &checksum, error)) { g_prefix_error(error, "failed to wait for checksum: "); return FALSE; } if (checksum != 0x0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid checksum result for CMD_FWBUFER_CHECKSUM, got 0x%02x", checksum); return FALSE; } fu_progress_step_done(progress); /* internal flash */ cmd = FU_USI_DOCK_MCU_CMD_FW_UPDATE; if (!fu_usi_dock_mcu_device_txrx(self, FU_USI_DOCK_TAG2_CMD_MCU, &cmd, sizeof(cmd), NULL, 0x0, error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_usi_dock_mcu_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { return fu_usi_dock_mcu_device_write_firmware_with_idx(FU_USI_DOCK_MCU_DEVICE(device), firmware, 0xFF, /* all */ progress, flags, error); } static gboolean fu_usi_dock_mcu_device_reload(FuDevice *device, GError **error) { FuUsiDockMcuDevice *self = FU_USI_DOCK_MCU_DEVICE(device); guint8 inbuf[] = {FU_USI_DOCK_MCU_CMD_SET_CHIP_TYPE, 1, 1}; if (fu_device_has_private_flag(device, FU_USI_DOCK_DEVICE_FLAG_SET_CHIP_TYPE)) { g_info("repairing device with CMD_SET_CHIP_TYPE"); if (!fu_usi_dock_mcu_device_txrx(self, FU_USI_DOCK_TAG2_CMD_MCU, inbuf, sizeof(inbuf), NULL, 0x0, error)) return FALSE; } return TRUE; } static gboolean fu_usi_dock_mcu_device_attach(FuDevice *device, FuProgress *progress, GError **error) { fu_device_set_remove_delay(device, 900000); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* success */ return TRUE; } static gboolean fu_usi_dock_mcu_device_insert_cb(gpointer user_data) { FuDevice *device = FU_DEVICE(user_data); g_autoptr(FwupdRequest) request = fwupd_request_new(); g_autoptr(GError) error_local = NULL; /* interactive request to start the SPI write */ fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_IMMEDIATE); fwupd_request_set_id(request, FWUPD_REQUEST_ID_INSERT_USB_CABLE); fwupd_request_add_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); if (!fu_device_emit_request(device, request, NULL, &error_local)) g_critical("%s", error_local->message); /* success */ return G_SOURCE_REMOVE; } static void fu_usi_dock_mcu_device_internal_flags_notify_cb(FuDevice *device, GParamSpec *pspec, gpointer user_data) { if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_UNCONNECTED) && fu_device_has_private_flag(device, FU_USI_DOCK_DEVICE_FLAG_WAITING_FOR_UNPLUG)) { g_debug("starting 40s countdown"); g_timeout_add_seconds_full(G_PRIORITY_DEFAULT, 40, /* seconds */ fu_usi_dock_mcu_device_insert_cb, g_object_ref(device), g_object_unref); fu_device_remove_private_flag(device, FU_USI_DOCK_DEVICE_FLAG_WAITING_FOR_UNPLUG); } } static gboolean fu_usi_dock_mcu_device_cleanup(FuDevice *device, FuProgress *progress, FwupdInstallFlags install_flags, GError **error) { g_autoptr(FwupdRequest) request = fwupd_request_new(); /* wait for the user to unplug then start the 40 second timer */ fu_device_add_private_flag(device, FU_USI_DOCK_DEVICE_FLAG_WAITING_FOR_UNPLUG); fu_device_set_remove_delay(device, 900000); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_BUSY); /* interactive request to start the SPI write */ fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_IMMEDIATE); fwupd_request_set_id(request, FWUPD_REQUEST_ID_REMOVE_USB_CABLE); fwupd_request_add_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); return fu_device_emit_request(device, request, progress, error); } static void fu_usi_dock_mcu_device_replace(FuDevice *device, FuDevice *donor) { if (fu_device_has_private_flag(donor, FU_USI_DOCK_DEVICE_FLAG_SET_CHIP_TYPE)) fu_device_add_private_flag(device, FU_USI_DOCK_DEVICE_FLAG_SET_CHIP_TYPE); } static void fu_usi_dock_mcu_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 48, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 52, "reload"); } static void fu_usi_dock_mcu_device_init(FuUsiDockMcuDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_NO_SERIAL_NUMBER); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_INHIBIT_CHILDREN); fu_device_add_request_flag(FU_DEVICE(self), FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); g_signal_connect(FWUPD_DEVICE(self), "notify::private-flags", G_CALLBACK(fu_usi_dock_mcu_device_internal_flags_notify_cb), NULL); fu_device_register_private_flag(FU_DEVICE(self), FU_USI_DOCK_DEVICE_FLAG_VERFMT_HP); fu_device_register_private_flag(FU_DEVICE(self), FU_USI_DOCK_DEVICE_FLAG_SET_CHIP_TYPE); fu_device_register_private_flag(FU_DEVICE(self), FU_USI_DOCK_DEVICE_FLAG_WAITING_FOR_UNPLUG); fu_hid_device_add_flag(FU_HID_DEVICE(self), FU_HID_DEVICE_FLAG_AUTODETECT_EPS); fu_device_add_protocol(FU_DEVICE(self), "com.usi.dock"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_NUMBER); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_retry_set_delay(FU_DEVICE(self), 1000); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_DOCK); } static void fu_usi_dock_mcu_device_class_init(FuUsiDockMcuDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_usi_dock_mcu_device_write_firmware; device_class->attach = fu_usi_dock_mcu_device_attach; device_class->setup = fu_usi_dock_mcu_device_setup; device_class->set_progress = fu_usi_dock_mcu_device_set_progress; device_class->cleanup = fu_usi_dock_mcu_device_cleanup; device_class->reload = fu_usi_dock_mcu_device_reload; device_class->replace = fu_usi_dock_mcu_device_replace; } fwupd-2.0.10/plugins/usi-dock/fu-usi-dock-mcu-device.h000066400000000000000000000011301501337203100224220ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_USI_DOCK_MCU_DEVICE (fu_usi_dock_mcu_device_get_type()) G_DECLARE_FINAL_TYPE(FuUsiDockMcuDevice, fu_usi_dock_mcu_device, FU, USI_DOCK_MCU_DEVICE, FuHidDevice) gboolean fu_usi_dock_mcu_device_write_firmware_with_idx(FuUsiDockMcuDevice *self, FuFirmware *firmware, guint8 chip_idx, FuProgress *progress, FwupdInstallFlags flags, GError **error); fwupd-2.0.10/plugins/usi-dock/fu-usi-dock-plugin.c000066400000000000000000000065731501337203100217120ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * Copyright 2021 Victor Cheng * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-usi-dock-child-device.h" #include "fu-usi-dock-dmc-device.h" #include "fu-usi-dock-mcu-device.h" #include "fu-usi-dock-plugin.h" #define USI_DOCK_TBT_INSTANCE_ID "THUNDERBOLT\\VEN_0108&DEV_2031" struct _FuUsiDockPlugin { FuPlugin parent_instance; FuDevice *device_tbt; }; G_DEFINE_TYPE(FuUsiDockPlugin, fu_usi_dock_plugin, FU_TYPE_PLUGIN) static FuDevice * fu_usi_dock_plugin_find_tbt_device(FuPlugin *plugin, FuDevice *device) { GPtrArray *children = fu_device_get_children(device); for (guint i = 0; i < children->len; i++) { FuDevice *device_tmp = g_ptr_array_index(children, i); if (fu_usi_dock_child_device_get_chip_idx(FU_USI_DOCK_CHILD_DEVICE(device_tmp)) == FU_USI_DOCK_FIRMWARE_IDX_TBT4) { return g_object_ref(device_tmp); } } return NULL; } static FuDevice * fu_usi_dock_plugin_find_mcu_device(FuPlugin *plugin) { GPtrArray *devices = fu_plugin_get_devices(plugin); for (guint i = 0; i < devices->len; i++) { FuDevice *device_tmp = g_ptr_array_index(devices, i); if (FU_IS_USI_DOCK_MCU_DEVICE(device_tmp)) return fu_usi_dock_plugin_find_tbt_device(plugin, device_tmp); } return NULL; } static void fu_usi_dock_plugin_ensure_equivalent_id(FuPlugin *plugin) { FuUsiDockPlugin *self = FU_USI_DOCK_PLUGIN(plugin); g_autoptr(FuDevice) device_usi = NULL; if (self->device_tbt == NULL) return; device_usi = fu_usi_dock_plugin_find_mcu_device(plugin); if (device_usi == NULL) return; fu_device_set_priority(device_usi, fu_device_get_priority(self->device_tbt) + 1); fu_device_set_equivalent_id(device_usi, fu_device_get_id(self->device_tbt)); fu_device_set_equivalent_id(self->device_tbt, fu_device_get_id(device_usi)); } static void fu_usi_dock_plugin_device_added(FuPlugin *plugin, FuDevice *device) { fu_usi_dock_plugin_ensure_equivalent_id(plugin); } static void fu_usi_dock_plugin_device_registered(FuPlugin *plugin, FuDevice *device) { FuUsiDockPlugin *self = FU_USI_DOCK_PLUGIN(plugin); /* usb device from thunderbolt plugin */ if (g_strcmp0(fu_device_get_plugin(device), "thunderbolt") == 0 && fu_device_has_guid(device, USI_DOCK_TBT_INSTANCE_ID)) { g_set_object(&self->device_tbt, device); fu_usi_dock_plugin_ensure_equivalent_id(plugin); } } static void fu_usi_dock_plugin_init(FuUsiDockPlugin *self) { } static void fu_usi_dock_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_USI_DOCK_MCU_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_USI_DOCK_DMC_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_USI_DOCK_CHILD_DEVICE); /* coverage */ } static void fu_usi_dock_plugin_finalize(GObject *obj) { FuUsiDockPlugin *self = FU_USI_DOCK_PLUGIN(obj); g_clear_object(&self->device_tbt); G_OBJECT_CLASS(fu_usi_dock_plugin_parent_class)->finalize(obj); } static void fu_usi_dock_plugin_class_init(FuUsiDockPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_usi_dock_plugin_finalize; plugin_class->constructed = fu_usi_dock_plugin_constructed; plugin_class->device_added = fu_usi_dock_plugin_device_added; plugin_class->device_registered = fu_usi_dock_plugin_device_registered; } fwupd-2.0.10/plugins/usi-dock/fu-usi-dock-plugin.h000066400000000000000000000003611501337203100217040ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuUsiDockPlugin, fu_usi_dock_plugin, FU, USI_DOCK_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/usi-dock/fu-usi-dock.rs000066400000000000000000000047571501337203100206220ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] enum FuUsiDockSpiState { None, SwitchSuccess, SwitchFail, CmdSuccess, CmdFail, RwSuccess, RwFail, Ready, Busy, Timeout, FlashFound, FlashNotFound, } #[derive(ToString)] enum FuUsiDockFirmwareIdx { None = 0x00, DmcPd = 0x01, Dp = 0x02, Tbt4 = 0x04, Usb3 = 0x08, Usb2 = 0x10, Audio = 0x20, I225 = 0x40, Mcu = 0x80, } #[repr(u8)] enum FuUsiDockTag2 { IspBoot = 0, // before Common CMD for bootload, with TAG0, TAG1, CMD Isp = 0x5A, // before Common, with TAG0, TAG1, CMD CmdMcu = 0x6A, // USB->MCU(Common-cmd mode), with TAG0, TAG1, CMD CmdSpi = 0x7A, // USB->MCU->SPI(Common-cmd mode), with TAG0, TAG1, CMD CmdI2c = 0x8A, // USB->MCU->I2C(Mass data transmission) MassDataMcu = 0x6B, // MASS data transfer for MCU 0xA0 MassDataSpi = 0x7B, // MASS data transfer for External flash 0xA1 MassDataI2c = 0x8B, // MASS data transfer for TBT flash } #[repr(u8)] enum FuUsiDockMcuCmd { McuNone = 0x0, McuStatus = 0x1, McuJump2boot = 0x2, ReadMcuVersionpage = 0x3, SetI225Pwr = 0x4, DockReset = 0x5, VersionWriteback = 0x6, SetChipType = 0x9, FwInitial = 0x0A, FwUpdate = 0x0B, FwTargetChecksum = 0x0C, FwIspEnd = 0x0D, All = 0xFF, } enum FuUsiDockSpiCmd { Initial = 0x01, EraseFlash = 0x02, Program = 0x03, WriteResponse = 0x04, ReadStatus = 0x05, Checksum = 0x06, End = 0x07, TransferFinish = 0x08, ErrorEnd = 0x09, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructUsiDockHidReq { id: u8 == 2, length: u8, buf: [u8; 61], tag3: FuUsiDockTag2, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructUsiDockMcuCmdReq { id: u8 == 2, length: u8, tag1: u8 == 0xFE, tag2: u8 == 0xFF, buf: [u8; 59], tag3: FuUsiDockTag2, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructUsiDockMcuCmdRes { id: u8 == 2, cmd1: FuUsiDockMcuCmd, tag1: u8 == 0xFE, tag2: u8 == 0xFF, cmd2: FuUsiDockMcuCmd, buf: [u8; 58], tag3: FuUsiDockTag2, } #[repr(C, packed)] struct FuStructUsiDockIspVersion { dmc: [u8; 5], pd: [u8; 5], dp5x: [u8; 5], dp6x: [u8; 5], tbt4: [u8; 5], usb3: [u8; 5], usb2: [u8; 5], audio: [u8; 5], i255: [u8; 5], mcu: [u8; 2], bcdversion: [u8; 2], } fwupd-2.0.10/plugins/usi-dock/meson.build000066400000000000000000000007621501337203100202600ustar00rootroot00000000000000plugins += {meson.current_source_dir().split('/')[-1]: true} cargs = ['-DG_LOG_DOMAIN="FuPluginUsiDock"'] plugin_quirks += files('usi-dock.quirk') plugin_builtins += static_library('fu_plugin_usi_dock', rustgen.process('fu-usi-dock.rs'), sources: [ 'fu-usi-dock-child-device.c', 'fu-usi-dock-dmc-device.c', 'fu-usi-dock-mcu-device.c', 'fu-usi-dock-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) fwupd-2.0.10/plugins/usi-dock/usi-dock.quirk000066400000000000000000000013201501337203100207000ustar00rootroot00000000000000[USB\VID_17EF&PID_30B4&CID_40B0] Plugin = usi_dock GType = FuUsiDockMcuDevice Name = ThinkPad Thunderbolt 4 Dock [USB\VID_17EF&PID_30B4&CID_40B1] Plugin = usi_dock GType = FuUsiDockMcuDevice Name = ThinkPad Universal Thunderbolt 4 Smart Dock [USB\VID_17EF&PID_30B4] Plugin = usi_dock GType = FuUsiDockMcuDevice Name = ThinkPad Thunderbolt 4 Dock # fix up a device-provisioning problem for this specific version [USB\VID_17EF&PID_30B4&CID_40B0&DMCVER_10.10] Flags = set-chip-type [USB\VID_17EF&PID_30B5] Plugin = usi_dock GType = FuUsiDockDmcDevice ParentGuid = USB\VID_17EF&PID_30B4 [USB\VID_03F0&PID_0505] Plugin = usi_dock GType = FuUsiDockMcuDevice Name = USB-C G2 Multiport Hub MCU Controller Flags = verfmt-hp fwupd-2.0.10/plugins/vbe/000077500000000000000000000000001501337203100151475ustar00rootroot00000000000000fwupd-2.0.10/plugins/vbe/README.md000066400000000000000000000237451501337203100164410ustar00rootroot00000000000000--- title: Plugin: VBE — Verified Boot for Embedded --- ## Introduction This plugin is used for boards which use the VBE system. This allows the platform to be updated from user space. ## Firmware Format Firmware updates are held within a CAB file, a Windows format which allows files to be collected and compressed, similar to a tar file. The CAB file can be created using the `fwupdtool` tool, as shown in the VBE `example` directory. Inside the CAB file is the firmware itself, in Flat Image Tree (FIT) format. This is typically called `firmware.fit` and can be created by the `mkimage` tool, or using device tree tools such as `dtc` and `fdtput`. The FIT supports multiple configuration, each of which is intended to update a particular board type. This allows the same firmware update to be used for a number of related boards, including sharing certain images, at the expense of increasing the update size. A typical FIT file looks like this: ```fdt / { timestamp = <0x62a74c6c>; description = "Firmware image with one or more FDT blobs"; creator = "U-Boot mkimage 2021.01+dfsg-3ubuntu0~20.04.4"; #address-cells = <0x00000001>; images { firmware-1 { description = "v1.2.4"; type = "firmware"; arch = "arm64"; os = "u-boot"; compression = "none"; store-offset = <0x10000>; data = <...>; hash-1 { value = <0xa738ea1c>; algo = "crc32"; }; }; }; configurations { default = "conf-1"; conf-1 { version = "1.2.4"; compatible = "pine64,rockpro64-v2.1", "pine64,rockpro64"; firmware = "firmware-1"; }; }; }; ``` Each configuration includes the version of the update, the board(s) it is compatible with and a list of firmware 'images' in the `firmware` property. The `compatible` property is optional in the case where there is only one configuration that applies to all boards that receive this update. The images themselves are in a separate `images` node. Each image includes various fields to indicate its type as well as option hashes. The `data` property contains the actual firmware data. Multiple images can be including in each configuration, but each must have a different `store-offset` property, indicating the offset from the start of the firmware region where this particular image is kept. The default store offset is zero, which is typically suitable if there is only one image. It is common to use an 'external' FIT, meaning that the `data` property is omitted and the data is placed in the same file after the FIT itself. In this case `data-offset` and `data-size` allow the data to be relocated. The data is stored started at the next 32-bit aligned file position after the FIT. The `mkimage` tool allows converting a FIT to an external FIT, with the `-E` flag. See the `plugins/vbe/example` directory for an example of building a firmware update. ## Operation The daemon will decompress the cabinet archive and extract the firmware blob, then write it to storage. The firmware will be used on the next reboot, so no changes to firmware happen until there is a reboot. This plugin supports the following protocol ID: * `org.vbe` ## Board information VBE requires the board firmware to provide information about the firmware within the device tree passed to the Operating System. For systems without device tree, currently the only option is to install a file for use by fwupd, typically in `/var/lib/fwupd/vbe/system.dtb`. In either case, there must be one or more nodes with the required information. The format depends on which VBE method is used, but the information must be in a subnode of `chosen/fwupd`. Here is an example: ```fdt / { compatible = "pine64,rockpro64-v2.1"; chosen { fwupd { firmware { compatible = "fwupd,vbe-simple"; cur-version = "1.2.3"; storage = "/tmp/testfw"; area-start = <0x100000>; area-size = <0x100000>; skip-offset = <0x8000>; part-uuid = "62db0ccf-03"; part-id = "3"; }; }; }; }; ``` The first compatible string indicates the board type. Typically it is a single string, but a list is also supported. VBE matches the string(s) here against those in the update itself, using the configuration which has the earliest match (within the list) to the board's compatible string(s). Since more specific boards are at the start of the compatible string, this allows for one configuration to have an update for a specific board, while another provides an update for all other boards. The compatible string of the firmware update indicates the VBE method that is being used. It must have `fwupd` as the manufacturer and the model must start with `vbe-`. Other properties within the node are determined by that method, but some are common to all: * `compatible` - indicates the VBE method to use, in the form `fwupd,`. * `cur-version` - indicates the version that is currently installed, if this is known by the firmware. Note that fwupd keeps its own information about the installed version, so this is not needed by fwupd itself. But it can be useful for other utilities. ## Important files * `/var/lib/fwupd/vbe/system.dtb` - this file holds the system information. It overrides the system device tree if any, meaning that the system device tree is ignored if this file is preset. For systems that don't support device tree (e.g ACPI systems), this file is needed for VBE to work ## Available VBE methods Note that all VBE methods must subclass `FuVbeDevice` since it provides access to the device tree and the VBE directory, among other things. At present only one method is available: * `fwupd,vbe-simple` - writes a single copy of the firmware to media ### vbe-simple With this, only a single copy of the firmware is available so if the write fails, the board may not boot. This is implemented in the `vbe-simple.c` file and has the device GUID `ea1b96eb-a430-4033-8708-498b6d98178b` within fwupd. Properties for this method are: * `compatible` - must be `fwupd,vbe-simple` * `storage` - device to store firmware in. Two options are supported: a full path such as `/dev/mmcblk1` or a device number, like `mmc1`. Note that only mmc is currently supported. * `area-start` - start offset in bytes of the firmware area within the storage * `area-size` - size in bytes of the firmware area within the storage * `skip-offset` - offset to preserve at the start of the firmware area. This means that the first part of the image is ignored, with just the latter part being written. For example, if this is 0x200 then the first 512 bytes of the image (which must be present in the image) are skipped and the bytes after that are written to the store offset. * `part-uuid` - the UUID of the partition containing the fwupd state. This is not used by fwupd at present but may allow the bootloader to check the fwupd state on boot * `part-id` - the partition number containing the fwupd state. This is not used by fwupd at present but may allow the bootloader to check the fwupd state on boot ## Finding out more You can use the U-Boot mailing list or `u-boot` IRC on `libera.chat` to ask questions specific to VBE and firmware. ## Sending patches Use the [fwupd developer site](https://fwupd.org/lvfs/docs/developers) to find information about downloading the code, submitting bugs/feature requests and sending patches. ## Vendor ID Security This does not update USB devices and thus requires no vendor ID set. ## External Interface Access This plugin requires access to system firmware, e.g. via a file or an eMMC device. ## Documentation The following documents may help in understanding VBE: * [VBE](https://docs.google.com/document/d/e/2PACX-1vQjXLPWMIyVktaTMf8edHZYDrEvMYD_iNzIj1FgPmKF37fpglAC47Tt5cvPBC5fvTdoK-GA5Zv1wifo/pub) * [VBE Bootflows](https://docs.google.com/document/d/e/2PACX-1vR0OzhuyRJQ8kdeOibS3xB1rVFy3J4M_QKTM5-3vPIBNcdvR0W8EXu9ymG-yWfqthzWoM4JUNhqwydN/pub) * [VBE Firmware update](https://docs.google.com/document/d/e/2PACX-1vTnlIL17vVbl6TVoTHWYMED0bme7oHHNk-g5VGxblbPiKIdGDALE1HKId8Go5f0g1eziLsv4h9bocbk/pub) * [FIT](https://github.com/u-boot/u-boot/blob/master/doc/uImage.FIT/source_file_format.txt) * [U-Boot Standard boot](https://u-boot.readthedocs.io/en/latest/develop/bootstd.html) ## Useful information For development you may find the following useful. To set up the vbe directory: ```bash mkdir /var/lib/fwupd/vbe chmod a+rwx /var/lib/fwupd/vbe dtc /path/to/fwupd/plugins/vbe/example/test.dts -o \ /var/lib/fwupd/vbe/system.dtb ``` To build the example: ```bash sudo apt install appstream-util u-boot-tools cd /path/to/fwupd/plugins/vbe/example dd if=/dev/zero of=update.bin bs=1M count=1 ./build.sh ``` To install the cab on your development computer: ```bash # Set up the test file dd if=/dev/zero of=/tmp/testfw bs=1M count=3 sudo build/src/fwupdtool install plugins/vbe/example/Vbe-Board-1.2.4.cab \ bb3b05a8-ebef-11ec-be98-d3a15278be95 ``` To dump out the firmware: ```bash sudo rm fw.bin; sudo build/src/fwupdtool firmware-dump fw.bin \ bb3b05a8-ebef-11ec-be98-d3a15278be95 ``` ## To do Add an update method that actually supports verified boot, including maintaining update state. The update happens in two passes, the first installing firmware in the 'B' slot and the second writing it to the 'A' slot, to avoid bricking the device in the event of a write failure or non-functional firmware. Figure out how to select the right update for a board out of many that might be on LVFS. At present the selection mechanism only works within the FIT configuration. This probably needs to use the `` piece of the xml file to specify an identifier for the family of boards supported by the update. ## Version Considerations This plugin has been available since fwupd version `1.8.2`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Simon Glass: @sjg20 fwupd-2.0.10/plugins/vbe/example/000077500000000000000000000000001501337203100166025ustar00rootroot00000000000000fwupd-2.0.10/plugins/vbe/example/.gitignore000066400000000000000000000000461501337203100205720ustar00rootroot00000000000000/.gitignore /firmware.fit /update.bin fwupd-2.0.10/plugins/vbe/example/build.sh000077500000000000000000000007411501337203100202420ustar00rootroot00000000000000#!/bin/sh set -e appstream-util validate-relax com.Vbe.Board.metainfo.xml #dd if=/dev/zero of=update.bin bs=1M count=1 mkimage -D "-p 0x100" -n "v1.2.4" -O U-Boot -A arm64 -C none -T firmware -f auto -d update.bin firmware.fit fdtput firmware.fit -t s /configurations/conf-1 version "1.2.4" # Make the data external, now that we have finished fiddling with the FDT mkimage -E -F firmware.fit fwupdtool build-cabinet Vbe-Board-1.2.4.cab firmware.fit com.Vbe.Board.metainfo.xml fwupd-2.0.10/plugins/vbe/example/com.Vbe.Board.metainfo.xml000066400000000000000000000024041501337203100234440ustar00rootroot00000000000000 com.Vbe.Laptop.firmware VBE BoardFirmware System firmware for a board, for use with VBE

    The board can be updated using Verified Boot for Embedded (VBE).

    bb3b05a8-ebef-11ec-be98-d3a15278be95 http://www.TBD.com/ CC0-1.0 Proprietary Vbe

    This release updates firmware to version 1.2.3 which includes support for a mythical new feature.

    org.freedesktop.fwupd
    fwupd-2.0.10/plugins/vbe/example/my-file000066400000000000000000000000171501337203100200650ustar00rootroot00000000000000this is a test fwupd-2.0.10/plugins/vbe/example/rockpro64.dts000066400000000000000000000006571501337203100211570ustar00rootroot00000000000000/dts-v1/; /* * This file can be used to try our a VBE simple update on ROCKPro64 using * Debian or some other distribution. */ / { compatible = "pine64,rockpro64-v2.1"; chosen { fwupd { firmware { compatible = "fwupd,vbe-simple"; cur-version = "1.2.3"; bootloader-version = "2022.01"; storage = "mmc1"; area-start = <0x0>; area-size = <0x1000000>; skip-offset = <0x8000>; }; }; }; }; fwupd-2.0.10/plugins/vbe/example/test.dts000066400000000000000000000007401501337203100202760ustar00rootroot00000000000000/dts-v1/; /* * This file is suitable for testing on the host device as it uses a local * file. To set it up: * */ / { compatible = "pine64,rockpro64-v2.1"; chosen { fwupd { firmware { compatible = "fwupd,vbe-simple"; cur-version = "1.2.3"; bootloader-version = "2022.01"; storage = "/tmp/testfw"; area-start = <0x100000>; area-size = <0x100000>; skip-offset = <0x8000>; part-uuid = "62db0ccf-03"; part-id = "3"; }; }; }; }; fwupd-2.0.10/plugins/vbe/fu-vbe-device.c000066400000000000000000000126031501337203100177360ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * Copyright 2022 Google LLC * Written by Simon Glass * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-vbe-device.h" enum { PROP_0, PROP_VBE_METHOD, PROP_FDT_ROOT, PROP_FDT_NODE, PROP_LAST }; typedef struct { FuFdtImage *fdt_root; FuFdtImage *fdt_node; gchar **compatible; } FuVbeDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuVbeDevice, fu_vbe_device, FU_TYPE_DEVICE) #define GET_PRIVATE(o) (fu_vbe_device_get_instance_private(o)) static void fu_vbe_device_to_string(FuDevice *device, guint idt, GString *str) { FuVbeDevice *self = FU_VBE_DEVICE(device); FuVbeDevicePrivate *priv = GET_PRIVATE(self); if (priv->compatible != NULL) { g_autofree gchar *tmp = g_strjoinv(":", priv->compatible); fwupd_codec_string_append(str, idt, "Compatible", tmp); } } FuFdtImage * fu_vbe_device_get_fdt_node(FuVbeDevice *self) { FuVbeDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_VBE_DEVICE(self), NULL); return priv->fdt_node; } gchar ** fu_vbe_device_get_compatible(FuVbeDevice *self) { FuVbeDevicePrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_VBE_DEVICE(self), NULL); return priv->compatible; } static void fu_vbe_device_init(FuVbeDevice *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_REBOOT); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_add_protocol(FU_DEVICE(self), "org.vbe"); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ENSURE_SEMVER); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_SIGNED); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE); fu_device_set_physical_id(FU_DEVICE(self), "vbe"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PAIR); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_COMPUTER); } static gboolean fu_vbe_device_probe(FuDevice *device, GError **error) { FuVbeDevice *self = FU_VBE_DEVICE(device); FuVbeDevicePrivate *priv = GET_PRIVATE(self); g_autofree gchar *version = NULL; g_autofree gchar *version_bootloader = NULL; g_return_val_if_fail(FU_IS_VBE_DEVICE(device), FALSE); /* sanity check */ if (priv->fdt_root == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no FDT root"); return FALSE; } /* get a list of compatible strings */ if (!fu_fdt_image_get_attr_strlist(priv->fdt_root, FU_FIT_FIRMWARE_ATTR_COMPATIBLE, &priv->compatible, error)) return FALSE; /* get baseclass shared attributes */ fu_fdt_image_get_attr_str(priv->fdt_node, "cur-version", &version, NULL); if (version != NULL) fu_device_set_version(device, version); fu_fdt_image_get_attr_str(priv->fdt_node, "bootloader-version", &version_bootloader, NULL); if (version_bootloader != NULL) fu_device_set_version_bootloader(device, version_bootloader); /* success */ return TRUE; } static void fu_vbe_device_get_property(GObject *obj, guint prop_id, GValue *value, GParamSpec *pspec) { FuVbeDevice *self = FU_VBE_DEVICE(obj); FuVbeDevicePrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_FDT_ROOT: g_value_set_object(value, priv->fdt_root); break; case PROP_FDT_NODE: g_value_set_object(value, priv->fdt_node); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec); break; } } static void fu_vbe_device_set_property(GObject *obj, guint prop_id, const GValue *value, GParamSpec *pspec) { FuVbeDevice *self = FU_VBE_DEVICE(obj); FuVbeDevicePrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_FDT_ROOT: g_set_object(&priv->fdt_root, g_value_get_object(value)); break; case PROP_FDT_NODE: g_set_object(&priv->fdt_node, g_value_get_object(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec); break; } } static void fu_vbe_device_finalize(GObject *obj) { FuVbeDevice *self = FU_VBE_DEVICE(obj); FuVbeDevicePrivate *priv = GET_PRIVATE(self); g_strfreev(priv->compatible); if (priv->fdt_root != NULL) g_object_unref(priv->fdt_root); if (priv->fdt_node != NULL) g_object_unref(priv->fdt_node); G_OBJECT_CLASS(fu_vbe_device_parent_class)->finalize(obj); } static void fu_vbe_device_class_init(FuVbeDeviceClass *klass) { GParamSpec *pspec; GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->get_property = fu_vbe_device_get_property; object_class->set_property = fu_vbe_device_set_property; pspec = g_param_spec_object("fdt-root", NULL, "FDT root containing method parameters", FU_TYPE_FDT_IMAGE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_FDT_ROOT, pspec); pspec = g_param_spec_object("fdt-node", NULL, "FDT image within the device tree containing method parameters'", FU_TYPE_FDT_IMAGE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_FDT_NODE, pspec); object_class->finalize = fu_vbe_device_finalize; device_class->to_string = fu_vbe_device_to_string; device_class->probe = fu_vbe_device_probe; } fwupd-2.0.10/plugins/vbe/fu-vbe-device.h000066400000000000000000000006661501337203100177510ustar00rootroot00000000000000/* * Copyright 2022 Google LLC * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_VBE_DEVICE (fu_vbe_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuVbeDevice, fu_vbe_device, FU, VBE_DEVICE, FuDevice) struct _FuVbeDeviceClass { FuDeviceClass parent_class; }; FuFdtImage * fu_vbe_device_get_fdt_node(FuVbeDevice *self); gchar ** fu_vbe_device_get_compatible(FuVbeDevice *self); fwupd-2.0.10/plugins/vbe/fu-vbe-plugin.c000066400000000000000000000070141501337203100177750ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * Copyright 2022 Google LLC * Written by Simon Glass * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-vbe-plugin.h" #include "fu-vbe-simple-device.h" struct _FuVbePlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuVbePlugin, fu_vbe_plugin, FU_TYPE_PLUGIN) static gboolean fu_vbe_plugin_coldplug_img(FuPlugin *plugin, FuFdtImage *fdt_root, FuFdtImage *fdt_node, GError **error) { GType device_gtype = G_TYPE_INVALID; g_autofree gchar *compatible = NULL; g_auto(GStrv) split = NULL; g_autoptr(FuDevice) dev = NULL; /* we expect 'fwupd,vbe-' */ if (!fu_fdt_image_get_attr_str(fdt_node, FU_FIT_FIRMWARE_ATTR_COMPATIBLE, &compatible, error)) { g_prefix_error(error, "missing update mechanism: "); return FALSE; } split = g_strsplit(compatible, ",", 2); if (g_strv_length(split) != 2) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "update mechanism is invalid: %s", compatible); return FALSE; } if (g_strcmp0(split[0], "fwupd") != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "update mechanism should have manufacturer of fwupd: %s", split[0]); return FALSE; } /* skip past 'vbe-' */ if (!g_str_has_prefix(split[1], "vbe-")) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "update mechanism is missing vbe prefix: %s", split[1]); return FALSE; } if (g_strcmp0(split[1], "vbe-simple") == 0) { device_gtype = FU_TYPE_VBE_SIMPLE_DEVICE; } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no driver for VBE method '%s'", split[1]); return FALSE; } /* success */ dev = g_object_new(device_gtype, "context", fu_plugin_get_context(plugin), "fdt-root", fdt_root, "fdt-node", fdt_node, NULL); fu_plugin_device_add(plugin, dev); return TRUE; } static gboolean fu_vbe_plugin_coldplug(FuPlugin *plugin, FuProgress *progress, GError **error) { g_autoptr(FuFirmware) fdt = NULL; g_autoptr(FuFdtImage) fdt_root = NULL; g_autoptr(GPtrArray) fdt_imgs = NULL; /* get compatible from root node */ fdt = fu_context_get_fdt(fu_plugin_get_context(plugin), error); if (fdt == NULL) return FALSE; fdt_root = fu_fdt_firmware_get_image_by_path(FU_FDT_FIRMWARE(fdt), "/chosen/fwupd", error); if (fdt_root == NULL) return FALSE; fdt_imgs = fu_firmware_get_images(FU_FIRMWARE(fdt_root)); for (guint i = 0; i < fdt_imgs->len; i++) { FuFdtImage *fdt_node = g_ptr_array_index(fdt_imgs, i); g_autoptr(GError) error_local = NULL; if (!fu_vbe_plugin_coldplug_img(plugin, fdt_root, fdt_node, &error_local)) { g_warning("%s", error_local->message); continue; } } /* nothing found? */ if (fu_plugin_get_devices(plugin)->len == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no valid VBE update mechanism found"); return FALSE; } /* success */ return TRUE; } static void fu_vbe_plugin_init(FuVbePlugin *self) { } static void fu_vbe_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_VBE_SIMPLE_DEVICE); /* coverage */ } static void fu_vbe_plugin_class_init(FuVbePluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_vbe_plugin_constructed; plugin_class->coldplug = fu_vbe_plugin_coldplug; } fwupd-2.0.10/plugins/vbe/fu-vbe-plugin.h000066400000000000000000000003431501337203100200000ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuVbePlugin, fu_vbe_plugin, FU, VBE_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/vbe/fu-vbe-simple-device.c000066400000000000000000000340371501337203100212320ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * Copyright 2022 Google LLC * Written by Simon Glass * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #ifdef HAVE_ERRNO_H #include #endif #include #include #include "fu-vbe-simple-device.h" /** * @skip_offset: This allows an initial part of the image to be skipped when * writing. This means that the first part of the image is ignored, with just * the latter part being written. For example, if this is 0x200 then the first * 512 bytes of the image (which must be present in the image) are skipped and * the bytes after that are written to the store offset. */ struct _FuVbeSimpleDevice { FuVbeDevice parent_instance; gchar *storage; /* e.g. "mmc1" */ gchar *devname; /* e.g. /dev/mmcblk1 */ guint32 area_start; guint32 area_size; guint32 skip_offset; gint fd; }; G_DEFINE_TYPE(FuVbeSimpleDevice, fu_vbe_simple_device, FU_TYPE_VBE_DEVICE) static void fu_vbe_simple_device_to_string(FuDevice *device, guint idt, GString *str) { FuVbeSimpleDevice *self = FU_VBE_SIMPLE_DEVICE(device); fwupd_codec_string_append(str, idt, "Storage", self->storage); fwupd_codec_string_append(str, idt, "Devname", self->devname); fwupd_codec_string_append_hex(str, idt, "AreaStart", self->area_start); fwupd_codec_string_append_hex(str, idt, "AreaSize", self->area_size); fwupd_codec_string_append_hex(str, idt, "SkipOffset", self->skip_offset); } static gboolean fu_vbe_simple_device_parse_devnum(const gchar *str, guint *value, GError **error) { guint64 val64 = 0; /* skip non-numeric part */ while (*str != '\0' && g_ascii_isdigit(*str)) str++; /* convert to uint */ if (!fu_strtoull(str, &val64, 0x0, G_MAXUINT, FU_INTEGER_BASE_AUTO, error)) return FALSE; if (value != NULL) *value = val64; return TRUE; } static gboolean fu_vbe_simple_device_probe(FuDevice *device, GError **error) { FuVbeSimpleDevice *self = FU_VBE_SIMPLE_DEVICE(device); FuFdtImage *fdt_node; g_return_val_if_fail(FU_IS_VBE_DEVICE(self), FALSE); /* FuVbeDevice->probe */ if (!FU_DEVICE_CLASS(fu_vbe_simple_device_parent_class)->probe(device, error)) return FALSE; fdt_node = fu_vbe_device_get_fdt_node(FU_VBE_DEVICE(self)); if (!fu_fdt_image_get_attr_str(fdt_node, "storage", &self->storage, error)) return FALSE; /* if this is an absolute path, use it */ if (g_str_has_prefix(self->storage, "/")) { self->devname = g_strdup(self->storage); } else { guint devnum = 0; /* obtain the 1 from "mmc1" */ if (!fu_vbe_simple_device_parse_devnum(self->storage, &devnum, error)) { g_prefix_error(error, "cannot parse storage property %s: ", self->storage); return FALSE; } if (g_str_has_prefix(self->storage, "mmc")) { self->devname = g_strdup_printf("/dev/mmcblk%u", devnum); } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported 'storage' media '%s'", self->storage); return FALSE; } } /* get area */ if (!fu_fdt_image_get_attr_u32(fdt_node, "area-start", &self->area_start, error)) return FALSE; if (!fu_fdt_image_get_attr_u32(fdt_node, "area-size", &self->area_size, error)) return FALSE; /* an optional skip offset to skip everything, which could be useful for testing */ fu_fdt_image_get_attr_u32(fdt_node, "skip-offset", &self->skip_offset, NULL); if (self->skip_offset > self->area_size) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "store offset 0x%x is larger than size 0x%x", (guint)self->skip_offset, (guint)self->area_size); return FALSE; } /* success */ return TRUE; } static gboolean fu_vbe_simple_device_open(FuDevice *device, GError **error) { FuVbeSimpleDevice *self = FU_VBE_SIMPLE_DEVICE(device); /* open device */ self->fd = open(self->devname, O_RDWR); if (self->fd == -1) { #ifdef HAVE_ERRNO_H g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot open %s [%s]", self->devname, g_strerror(errno)); #else g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot open %s", self->devname); return FALSE; #endif } /* success */ return TRUE; } static gboolean fu_vbe_simple_device_close(FuDevice *device, GError **error) { FuVbeSimpleDevice *self = FU_VBE_SIMPLE_DEVICE(device); /* close device */ close(self->fd); self->fd = -1; /* success */ return TRUE; } static FuFdtImage * fu_vbe_simple_device_get_cfg_compatible(FuVbeSimpleDevice *self, FuFirmware *firmware, GError **error) { gchar **device_compatible; g_autoptr(FuFdtImage) fdt_configurations = NULL; g_autoptr(GPtrArray) img_configurations = NULL; g_autofree gchar *str = NULL; /* get all configurations */ fdt_configurations = fu_fdt_firmware_get_image_by_path(FU_FDT_FIRMWARE(firmware), "/" FU_FIT_FIRMWARE_ID_CONFIGURATIONS, error); if (fdt_configurations == NULL) return NULL; img_configurations = fu_firmware_get_images(firmware); /* look for a configuration with the device compatible strings in priority order */ device_compatible = fu_vbe_device_get_compatible(FU_VBE_DEVICE(self)); for (guint j = 0; device_compatible[j] != NULL; j++) { for (guint i = 0; i < img_configurations->len; i++) { FuFdtImage *img = g_ptr_array_index(img_configurations, i); g_auto(GStrv) img_compatible = NULL; if (!fu_fdt_image_get_attr_strlist(img, FU_FIT_FIRMWARE_ATTR_COMPATIBLE, &img_compatible, error)) return NULL; if (g_strv_contains((const gchar *const *)img_compatible, device_compatible[j])) return g_object_ref(img); } } /* failure */ str = g_strjoinv(", ", device_compatible); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no images found that match %s", str); return NULL; } static FuFirmware * fu_vbe_simple_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuVbeSimpleDevice *self = FU_VBE_SIMPLE_DEVICE(device); g_autofree gchar *version = NULL; g_auto(GStrv) firmware_ids = NULL; g_autoptr(FuFdtImage) img_cfg = NULL; g_autoptr(FuFirmware) firmware = fu_fit_firmware_new(); g_autoptr(FuFirmware) firmware_container = fu_firmware_new(); /* parse all images */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; /* look for a compatible configuration */ img_cfg = fu_vbe_simple_device_get_cfg_compatible(self, firmware, error); if (img_cfg == NULL) return NULL; if (!fu_fdt_image_get_attr_str(img_cfg, FU_FIT_FIRMWARE_ATTR_VERSION, &version, error)) return NULL; /* check the firmware images exists */ if (!fu_fdt_image_get_attr_strlist(img_cfg, "firmware", &firmware_ids, error)) return NULL; for (guint i = 0; firmware_ids[i] != NULL; i++) { g_autofree gchar *path = NULL; g_autoptr(FuFdtImage) img_firmware = NULL; path = g_strdup_printf("/%s/%s", FU_FIT_FIRMWARE_ID_IMAGES, firmware_ids[i]); img_firmware = fu_fdt_firmware_get_image_by_path(FU_FDT_FIRMWARE(firmware), path, error); if (img_firmware == NULL) return NULL; fu_firmware_add_image(firmware_container, FU_FIRMWARE(img_firmware)); } /* success: return the container */ return g_steal_pointer(&firmware_container); } static gboolean fu_vbe_simple_device_write_firmware_img(FuVbeSimpleDevice *self, FuFdtImage *img, FuProgress *progress, GError **error) { const guint8 *buf; gssize rc; gsize bufsz = 0; gsize seek_to; guint32 store_offset = 0; g_autoptr(GBytes) blob = NULL; /* get data */ blob = fu_fdt_image_get_attr(img, FU_FIT_FIRMWARE_ATTR_DATA, error); if (blob == NULL) return FALSE; buf = g_bytes_get_data(blob, &bufsz); fu_fdt_image_get_attr_u32(img, "store-offset", &store_offset, NULL); /* sanity check */ if (store_offset + bufsz > self->area_size) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "image '%s' store_offset=0x%x, bufsz=0x%x, area_size=0x%x", fu_firmware_get_id(FU_FIRMWARE(img)), (guint)store_offset, (guint)bufsz, (guint)self->area_size); return FALSE; } if (self->skip_offset >= bufsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "image '%s' skip_offset=0x%x, bufsz=0x%x, area_size=0x%x", fu_firmware_get_id(FU_FIRMWARE(img)), (guint)store_offset, (guint)bufsz, (guint)self->area_size); return FALSE; } /* seek to correct address */ seek_to = self->area_start + store_offset + self->skip_offset; g_debug("writing image '%s' bufsz 0x%x (skipping 0x%x) to store_offset 0x%x, seek 0x%x\n", fu_firmware_get_id(FU_FIRMWARE(img)), (guint)bufsz, (guint)self->skip_offset, (guint)store_offset, (guint)seek_to); rc = lseek(self->fd, seek_to, SEEK_SET); if (rc < 0) { #ifdef HAVE_ERRNO_H g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "cannot seek file '%s' to 0x%x [%s]", self->devname, (guint)seek_to, g_strerror(errno)); #else g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "cannot seek file '%s' to 0x%x", self->devname, (guint)seek_to); #endif return FALSE; } /* write buffer */ rc = write(self->fd, buf + self->skip_offset, bufsz - self->skip_offset); if (rc < 0) { #ifdef HAVE_ERRNO_H g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "cannot write file '%s' [%s]", self->devname, g_strerror(errno)); #else g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "cannot write file '%s'", self->devname); #endif return FALSE; } /* success */ return TRUE; } static gboolean fu_vbe_simple_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuVbeSimpleDevice *self = FU_VBE_SIMPLE_DEVICE(device); g_autoptr(GPtrArray) imgs = fu_firmware_get_images(firmware); g_return_val_if_fail(FU_IS_VBE_DEVICE(self), FALSE); /* write each firmware image */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, imgs->len); for (guint i = 0; i < imgs->len; i++) { FuFdtImage *img = g_ptr_array_index(imgs, i); if (!fu_vbe_simple_device_write_firmware_img(self, img, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static GBytes * fu_vbe_simple_device_upload(FuDevice *device, FuProgress *progress, GError **error) { FuVbeSimpleDevice *self = FU_VBE_SIMPLE_DEVICE(device); gssize rc; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GPtrArray) chunks = NULL; /* notify UI */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ); /* seek to start */ rc = lseek(self->fd, self->area_start, SEEK_SET); if (rc < 0) { #ifdef HAVE_ERRNO_H g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "cannot seek file %s to 0x%x [%s]", self->devname, (guint)self->area_start, g_strerror(errno)); #else g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "cannot seek file %s to 0x%x", self->devname, (guint)self->area_start); #endif return NULL; } /* process in chunks */ chunks = fu_chunk_array_new(NULL, self->area_size - self->area_start, 0x0, 0x0, 0x100000); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); g_autofree guint8 *tmpbuf = g_malloc0(fu_chunk_get_data_sz(chk)); rc = read(self->fd, tmpbuf, fu_chunk_get_data_sz(chk)); if (rc != (gssize)fu_chunk_get_data_sz(chk)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "incomplete read of %s @0x%x", self->devname, (guint)fu_chunk_get_address(chk)); return NULL; } g_byte_array_append(buf, tmpbuf, fu_chunk_get_data_sz(chk)); fu_progress_step_done(progress); } /* success */ return g_bytes_new(buf->data, buf->len); } static void fu_vbe_simple_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_vbe_simple_device_init(FuVbeSimpleDevice *self) { fu_device_set_name(FU_DEVICE(self), "simple"); fu_device_set_vendor(FU_DEVICE(self), "U-Boot"); fu_device_build_vendor_id(FU_DEVICE(self), "VBE", "U-Boot"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version_lowest(FU_DEVICE(self), "0.0.1"); } static void fu_vbe_simple_device_constructed(GObject *obj) { FuVbeSimpleDevice *self = FU_VBE_SIMPLE_DEVICE(obj); fu_device_add_instance_id(FU_DEVICE(self), "bb3b05a8-ebef-11ec-be98-d3a15278be95"); } static void fu_vbe_simple_device_finalize(GObject *obj) { FuVbeSimpleDevice *self = FU_VBE_SIMPLE_DEVICE(obj); g_free(self->devname); g_free(self->storage); G_OBJECT_CLASS(fu_vbe_simple_device_parent_class)->finalize(obj); } static void fu_vbe_simple_device_class_init(FuVbeSimpleDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->constructed = fu_vbe_simple_device_constructed; object_class->finalize = fu_vbe_simple_device_finalize; device_class->to_string = fu_vbe_simple_device_to_string; device_class->probe = fu_vbe_simple_device_probe; device_class->open = fu_vbe_simple_device_open; device_class->close = fu_vbe_simple_device_close; device_class->set_progress = fu_vbe_simple_device_set_progress; device_class->prepare_firmware = fu_vbe_simple_device_prepare_firmware; device_class->write_firmware = fu_vbe_simple_device_write_firmware; device_class->dump_firmware = fu_vbe_simple_device_upload; } fwupd-2.0.10/plugins/vbe/fu-vbe-simple-device.h000066400000000000000000000004461501337203100212340ustar00rootroot00000000000000/* * Copyright 2022 Google LLC * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-vbe-device.h" #define FU_TYPE_VBE_SIMPLE_DEVICE (fu_vbe_simple_device_get_type()) G_DECLARE_FINAL_TYPE(FuVbeSimpleDevice, fu_vbe_simple_device, FU, VBE_SIMPLE_DEVICE, FuVbeDevice) fwupd-2.0.10/plugins/vbe/meson.build000066400000000000000000000007731501337203100173200ustar00rootroot00000000000000plugins += {meson.current_source_dir().split('/')[-1]: true} cargs = ['-DG_LOG_DOMAIN="FuPluginVbe"'] plugin_builtins += static_library('fu_plugin_vbe', sources : [ 'fu-vbe-plugin.c', 'fu-vbe-device.c', 'fu-vbe-simple-device.c', ], include_directories : [ root_incdir, fwupd_incdir, fwupdplugin_incdir, ], link_with : [ fwupd, fwupdplugin, ], c_args : [ cargs, '-DLOCALSTATEDIR="' + localstatedir + '"', ], dependencies : [ plugin_deps, ], ) fwupd-2.0.10/plugins/vendor-example/000077500000000000000000000000001501337203100173215ustar00rootroot00000000000000fwupd-2.0.10/plugins/vendor-example/.gitignore000066400000000000000000000004071501337203100213120ustar00rootroot00000000000000README.md fu-vendor-example-common.c fu-vendor-example-common.h fu-vendor-example-device.c fu-vendor-example-device.h fu-vendor-example-firmware.c fu-vendor-example-firmware.h fu-vendor-example-plugin.c fu-vendor-example-plugin.h meson.build vendor-example.quirk fwupd-2.0.10/plugins/vendor-example/README.md.in000066400000000000000000000027331501337203100212120ustar00rootroot00000000000000--- title: Plugin: {{Vendor}} {{Example}} --- ## Introduction The {{Example}} is a bla bla bla. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a packed binary file format. This plugin supports the following protocol ID: * `com.{{vendor}}.{{example}}` ## GUID Generation {% if Parent == 'Usb' -%} These devices use the standard USB DeviceInstanceId values, e.g. * `USB\ID_XXX` * `USB\VID_273F&PID_1001` {%- else -%} These devices use the standard TODO DeviceInstanceId values, e.g. * `TODO\VID_XXX` {%- endif %} ## Update Behavior The device is updated by bla bla bla. ## Vendor ID Security {% if Parent in ['Usb', 'Hid'] -%} The vendor ID is set from the USB vendor, in this instance set to `USB:0x273F` {%- else -%} The vendor ID is set from the TODO vendor, in this instance set to `TODO:0x273F` {%- endif %} ## Quirk Use This plugin uses the following plugin-specific quirks: ### {{VendorExample}}StartAddr The bla bla bla. Since: 1.8.TODO ## External Interface Access {% if Parent in ['Usb', 'Hid'] -%} This plugin requires read/write access to `/dev/bus/usb`. {%- else -%} This plugin requires read/write access to `TODO`. {%- endif %} ## Version Considerations This plugin has been available since fwupd version `SET_VERSION_HERE`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * {{Vendor}}: @github-username fwupd-2.0.10/plugins/vendor-example/fu-vendor-example-common.c.in000066400000000000000000000004331501337203100247160ustar00rootroot00000000000000/* * Copyright {{Year}} {{Author}} <{{Email}}> * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-{{vendor_dash_example}}-common.h" const gchar * fu_{{vendor_example}}_strerror(guint8 code) { if (code == 0) return "success"; return NULL; } fwupd-2.0.10/plugins/vendor-example/fu-vendor-example-common.h.in000066400000000000000000000003111501337203100247160ustar00rootroot00000000000000/* * Copyright {{Year}} {{Author}} <{{Email}}> * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include const gchar * fu_{{vendor_example}}_strerror(guint8 code); fwupd-2.0.10/plugins/vendor-example/fu-vendor-example-device.c.in000066400000000000000000000261111501337203100246660ustar00rootroot00000000000000/* * Copyright {{Year}} {{Author}} <{{Email}}> * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-{{vendor_dash_example}}-common.h" #include "fu-{{vendor_dash_example}}-device.h" #include "fu-{{vendor_dash_example}}-firmware.h" #include "fu-{{vendor_dash_example}}-struct.h" /* this can be set using Flags=example in the quirk file */ #define FU_{{VENDOR_EXAMPLE}}_DEVICE_FLAG_EXAMPLE "example" struct _Fu{{VendorExample}}Device { Fu{{Parent}}Device parent_instance; guint16 start_addr; }; G_DEFINE_TYPE(Fu{{VendorExample}}Device, fu_{{vendor_example}}_device, FU_TYPE_{{PARENT}}_DEVICE) static void fu_{{vendor_example}}_device_to_string(FuDevice *device, guint idt, GString *str) { Fu{{VendorExample}}Device *self = FU_{{VENDOR_EXAMPLE}}_DEVICE(device); fwupd_codec_string_append_hex(str, idt, "StartAddr", self->start_addr); } /* TODO: this is only required if the device instance state is required elsewhere */ guint16 fu_{{vendor_example}}_device_get_start_addr(Fu{{VendorExample}}Device *self) { g_return_val_if_fail(FU_IS_{{VENDOR_EXAMPLE}}_DEVICE(self), G_MAXUINT16); return self->start_addr; } static gboolean fu_{{vendor_example}}_device_detach(FuDevice *device, FuProgress *progress, GError **error) { Fu{{VendorExample}}Device *self = FU_{{VENDOR_EXAMPLE}}_DEVICE(device); /* sanity check */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in bootloader mode, skipping"); return TRUE; } /* TODO: switch the device into bootloader mode */ g_assert(self != NULL); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_{{vendor_example}}_device_attach(FuDevice *device, FuProgress *progress, GError **error) { Fu{{VendorExample}}Device *self = FU_{{VENDOR_EXAMPLE}}_DEVICE(device); /* sanity check */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in runtime mode, skipping"); return TRUE; } /* TODO: switch the device into runtime mode */ g_assert(self != NULL); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_{{vendor_example}}_device_reload(FuDevice *device, GError **error) { Fu{{VendorExample}}Device *self = FU_{{VENDOR_EXAMPLE}}_DEVICE(device); /* TODO: reprobe the hardware, or delete this vfunc to use ->setup() as a fallback */ g_assert(self != NULL); return TRUE; } static gboolean fu_{{vendor_example}}_device_probe(FuDevice *device, GError **error) { Fu{{VendorExample}}Device *self = FU_{{VENDOR_EXAMPLE}}_DEVICE(device); /* Fu{{Parent}}Device->probe */ if (!FU_DEVICE_CLASS(fu_{{vendor_example}}_device_parent_class)->probe(device, error)) return FALSE; /* TODO: probe the device for properties available before it is opened */ if (fu_device_has_private_flag(device, FU_{{VENDOR_EXAMPLE}}_DEVICE_FLAG_EXAMPLE)) self->start_addr = 0x100; {%- if Parent == 'Udev' -%} /* get from sysfs */ return fu_udev_device_set_physical_id(FU_UDEV_DEVICE(device), "pci", error); {%- else %} /* success */ return TRUE; {%- endif %} } static gboolean fu_{{vendor_example}}_device_setup(FuDevice *device, GError **error) { Fu{{VendorExample}}Device *self = FU_{{VENDOR_EXAMPLE}}_DEVICE(device); /* {{Parent}}Device->setup */ if (!FU_DEVICE_CLASS(fu_{{vendor_example}}_device_parent_class)->setup(device, error)) return FALSE; /* TODO: get the version and other properties from the hardware while open */ g_assert(self != NULL); fu_device_set_version(device, "1.2.3"); /* success */ return TRUE; } static gboolean fu_{{vendor_example}}_device_prepare(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { Fu{{VendorExample}}Device *self = FU_{{VENDOR_EXAMPLE}}_DEVICE(device); /* TODO: anything the device has to do before the update starts */ g_assert(self != NULL); return TRUE; } static gboolean fu_{{vendor_example}}_device_cleanup(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { Fu{{VendorExample}}Device *self = FU_{{VENDOR_EXAMPLE}}_DEVICE(device); /* TODO: anything the device has to do when the update has completed */ g_assert(self != NULL); return TRUE; } static FuFirmware * fu_{{vendor_example}}_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FwupdInstallFlags flags, GError **error) { Fu{{VendorExample}}Device *self = FU_{{VENDOR_EXAMPLE}}_DEVICE(device); g_autoptr(FuFirmware) firmware = fu_{{vendor_example}}_firmware_new(); /* TODO: you do not need to use this vfunc if not checking attributes */ if (self->start_addr != fu_{{vendor_example}}_firmware_get_start_addr(FU_{{VENDOR_EXAMPLE}}_FIRMWARE(firmware))) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "start address mismatch, got 0x%04x, expected 0x%04x", fu_{{vendor_example}}_firmware_get_start_addr(FU_{{VENDOR_EXAMPLE}}_FIRMWARE(firmware)), self->start_addr); return NULL; } if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; return g_steal_pointer(&firmware); } static gboolean fu_{{vendor_example}}_device_write_blocks(Fu{{VendorExample}}Device *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; /* TODO: send to hardware */ {%- if Parent == 'Hid' %} guint8 buf[64] = { 0x12, 0x24, 0x0 }; /* TODO: this is the preamble */ /* TODO: copy in payload */ if (!fu_memcpy_safe(buf, sizeof(buf), 0x2, /* TODO: copy to dst at offset */ fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0x0, /* src */ fu_chunk_get_data_sz(chk), error)) return FALSE; if (!fu_hid_device_set_report(FU_HID_DEVICE(self), 0x01, buf, sizeof(buf), 5000, /* ms */ FU_HID_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to send packet: "); return FALSE; } if (!fu_hid_device_get_report(FU_HID_DEVICE(self), 0x01, buf, sizeof(buf), 5000, /* ms */ FU_HID_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to receive packet: "); return FALSE; } {%- endif %} /* update progress */ fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_{{vendor_example}}_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { Fu{{VendorExample}}Device *self = FU_{{VENDOR_EXAMPLE}}_DEVICE(device); g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 44, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 35, NULL); /* get default image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* write each block */ chunks = fu_chunk_array_new_from_stream(stream, self->start_addr, FU_CHUNK_PAGESZ_NONE, 64 /* block_size */, error); if (chunks == NULL) return FALSE; if (!fu_{{vendor_example}}_device_write_blocks(self, chunks, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* TODO: verify each block */ fu_progress_step_done(progress); /* success! */ return TRUE; } static gboolean fu_{{vendor_example}}_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { Fu{{VendorExample}}Device *self = FU_{{VENDOR_EXAMPLE}}_DEVICE(device); /* TODO: parse value from quirk file */ if (g_strcmp0(key, "{{VendorExample}}StartAddr") == 0) { guint64 tmp = 0; if (!fu_strtoull(value, &tmp, 0, G_MAXUINT16, FU_INTEGER_BASE_AUTO, error)) return FALSE; self->start_addr = tmp; return TRUE; } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_{{vendor_example}}_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 57, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 43, "reload"); } static void fu_{{vendor_example}}_device_init(Fu{{VendorExample}}Device *self) { self->start_addr = 0x5000; fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_add_protocol(FU_DEVICE(self), "com.{{vendor}}.{{example}}"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_icon(FU_DEVICE(self), "icon-name"); fu_device_register_private_flag(FU_DEVICE(self), FU_{{VENDOR_EXAMPLE}}_DEVICE_FLAG_EXAMPLE); {%- if Parent == 'Usb' %} fu_usb_device_add_interface(FU_USB_DEVICE(self), 0x01); {%- endif %} {%- if Parent == 'Hid' %} fu_hid_device_add_flag(FU_HID_DEVICE(self), FU_HID_DEVICE_FLAG_RETRY_FAILURE); {%- endif %} } static void fu_{{vendor_example}}_device_finalize(GObject *object) { Fu{{VendorExample}}Device *self = FU_{{VENDOR_EXAMPLE}}_DEVICE(object); /* TODO: free any allocated instance state here */ g_assert(self != NULL); G_OBJECT_CLASS(fu_{{vendor_example}}_device_parent_class)->finalize(object); } static void fu_{{vendor_example}}_device_class_init(Fu{{VendorExample}}DeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->finalize = fu_{{vendor_example}}_device_finalize; device_class->to_string = fu_{{vendor_example}}_device_to_string; device_class->probe = fu_{{vendor_example}}_device_probe; device_class->setup = fu_{{vendor_example}}_device_setup; device_class->reload = fu_{{vendor_example}}_device_reload; device_class->prepare = fu_{{vendor_example}}_device_prepare; device_class->cleanup = fu_{{vendor_example}}_device_cleanup; device_class->attach = fu_{{vendor_example}}_device_attach; device_class->detach = fu_{{vendor_example}}_device_detach; device_class->prepare_firmware = fu_{{vendor_example}}_device_prepare_firmware; device_class->write_firmware = fu_{{vendor_example}}_device_write_firmware; device_class->set_quirk_kv = fu_{{vendor_example}}_device_set_quirk_kv; device_class->set_progress = fu_{{vendor_example}}_device_set_progress; } fwupd-2.0.10/plugins/vendor-example/fu-vendor-example-device.h.in000066400000000000000000000007261501337203100246770ustar00rootroot00000000000000/* * Copyright {{Year}} {{Author}} <{{Email}}> * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_{{VENDOR_EXAMPLE}}_DEVICE (fu_{{vendor_example}}_device_get_type()) G_DECLARE_FINAL_TYPE(Fu{{VendorExample}}Device, fu_{{vendor_example}}_device, FU, {{VENDOR_EXAMPLE}}_DEVICE, Fu{{Parent}}Device) guint16 fu_{{vendor_example}}_device_get_start_addr(Fu{{VendorExample}}Device *self); fwupd-2.0.10/plugins/vendor-example/fu-vendor-example-firmware.c.in000066400000000000000000000067771501337203100252630ustar00rootroot00000000000000/* * Copyright {{Year}} {{Author}} <{{Email}}> * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-{{vendor_dash_example}}-firmware.h" #include "fu-{{vendor_dash_example}}-struct.h" struct _Fu{{VendorExample}}Firmware { FuFirmware parent_instance; guint16 start_addr; }; G_DEFINE_TYPE(Fu{{VendorExample}}Firmware, fu_{{vendor_example}}_firmware, FU_TYPE_FIRMWARE) static void fu_{{vendor_example}}_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { Fu{{VendorExample}}Firmware *self = FU_{{VENDOR_EXAMPLE}}_FIRMWARE(firmware); fu_xmlb_builder_insert_kx(bn, "start_addr", self->start_addr); } static gboolean fu_{{vendor_example}}_firmware_build(FuFirmware *firmware, XbNode *n, GError **error) { Fu{{VendorExample}}Firmware *self = FU_{{VENDOR_EXAMPLE}}_FIRMWARE(firmware); guint64 tmp; /* TODO: load from .builder.xml */ tmp = xb_node_query_text_as_uint(n, "start_addr", NULL); if (tmp != G_MAXUINT64 && tmp <= G_MAXUINT16) self->start_addr = tmp; /* success */ return TRUE; } static gboolean fu_{{vendor_example}}_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_{{vendor_example}}_header_validate_stream(stream, offset, error); } static gboolean fu_{{vendor_example}}_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { Fu{{VendorExample}}Firmware *self = FU_{{VENDOR_EXAMPLE}}_FIRMWARE(firmware); g_autoptr(GByteArray) st_hdr = NULL; /* TODO: parse firmware into images */ st_hdr = fu_struct_{{vendor_example}}_hdr_parse_stream(stream, 0x0, error); if (st_hdr == NULL) return FALSE; self->start_addr = 0x1234; fu_firmware_set_version(firmware, "1.2.3"); fu_firmware_set_bytes(firmware, fw); return TRUE; } static GByteArray * fu_{{vendor_example}}_firmware_write(FuFirmware *firmware, GError **error) { Fu{{VendorExample}}Firmware *self = FU_{{VENDOR_EXAMPLE}}_FIRMWARE(firmware); g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GBytes) fw = NULL; /* data first */ fw = fu_firmware_get_bytes_with_patches(firmware, error); if (fw == NULL) return NULL; /* TODO: write to the buffer with the correct format */ g_assert(self != NULL); fu_byte_array_append_bytes(buf, fw); /* success */ return g_steal_pointer(&buf); } guint16 fu_{{vendor_example}}_firmware_get_start_addr(Fu{{VendorExample}}Firmware *self) { g_return_val_if_fail(FU_IS_{{VENDOR_EXAMPLE}}_FIRMWARE(self), G_MAXUINT16); return self->start_addr; } static void fu_{{vendor_example}}_firmware_init(Fu{{VendorExample}}Firmware *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_STORED_SIZE); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_VID_PID); } static void fu_{{vendor_example}}_firmware_class_init(Fu{{VendorExample}}FirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_{{vendor_example}}_firmware_validate; firmware_class->parse = fu_{{vendor_example}}_firmware_parse; firmware_class->write = fu_{{vendor_example}}_firmware_write; firmware_class->build = fu_{{vendor_example}}_firmware_build; firmware_class->export = fu_{{vendor_example}}_firmware_export; } FuFirmware * fu_{{vendor_example}}_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_{{VENDOR_EXAMPLE}}_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/vendor-example/fu-vendor-example-firmware.h.in000066400000000000000000000010231501337203100252430ustar00rootroot00000000000000/* * Copyright {{Year}} {{Author}} <{{Email}}> * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_{{VENDOR_EXAMPLE}}_FIRMWARE (fu_{{vendor_example}}_firmware_get_type()) G_DECLARE_FINAL_TYPE(Fu{{VendorExample}}Firmware, fu_{{vendor_example}}_firmware, FU, {{VENDOR_EXAMPLE}}_FIRMWARE, FuFirmware) FuFirmware * fu_{{vendor_example}}_firmware_new(void); guint16 fu_{{vendor_example}}_firmware_get_start_addr(Fu{{VendorExample}}Firmware *self); fwupd-2.0.10/plugins/vendor-example/fu-vendor-example-plugin.c.in000066400000000000000000000021141501337203100247220ustar00rootroot00000000000000/* * Copyright {{Year}} {{Author}} <{{Email}}> * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-{{vendor_dash_example}}-device.h" #include "fu-{{vendor_dash_example}}-firmware.h" #include "fu-{{vendor_dash_example}}-plugin.h" struct _Fu{{VendorExample}}Plugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(Fu{{VendorExample}}Plugin, fu_{{vendor_example}}_plugin, FU_TYPE_PLUGIN) static void fu_{{vendor_example}}_plugin_init(Fu{{VendorExample}}Plugin *self) { } static void fu_{{vendor_example}}_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "{{VendorExample}}StartAddr"); fu_plugin_add_device_gtype(plugin, FU_TYPE_{{VENDOR_EXAMPLE}}_DEVICE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_{{VENDOR_EXAMPLE}}_FIRMWARE); } static void fu_{{vendor_example}}_plugin_class_init(Fu{{VendorExample}}PluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_{{vendor_example}}_plugin_constructed; } fwupd-2.0.10/plugins/vendor-example/fu-vendor-example-plugin.h.in000066400000000000000000000004411501337203100247300ustar00rootroot00000000000000/* * Copyright {{Year}} {{Author}} <{{Email}}> * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(Fu{{VendorExample}}Plugin, fu_{{vendor_example}}_plugin, FU, {{VENDOR_EXAMPLE}}_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/vendor-example/fu-vendor-example.rs000066400000000000000000000005111501337203100232220ustar00rootroot00000000000000// Copyright {{Year}} {{Author}} <{{Email}}> // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(New, ValidateStream, ParseStream, Default)] #[repr(C, packed)] struct FuStruct{{VendorExample}} { signature: u8 == 0xDE, address: u16le, } #[derive(ToString)] enum Fu{{VendorExample}}Status { Unknown, Failed, } fwupd-2.0.10/plugins/vendor-example/meson.build.in000066400000000000000000000014551501337203100220750ustar00rootroot00000000000000{%- if Parent == 'Udev' -%} if host_machine.system() == 'linux' {%- endif %} cargs = ['-DG_LOG_DOMAIN="FuPlugin{{VendorExample}}"'] plugins += {meson.current_source_dir().split('/')[-1]: true} plugin_quirks += files('{{vendor_dash_example}}.quirk') plugin_builtins += static_library('fu_plugin_{{vendor_example}}', rustgen.process('fu-{{vendor_dash_example}}.rs'), sources: [ 'fu-{{vendor_dash_example}}-common.c', 'fu-{{vendor_dash_example}}-device.c', 'fu-{{vendor_dash_example}}-firmware.c', 'fu-{{vendor_dash_example}}-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files( 'tests/vendor-example.json', 'tests/vendor-example-setup.json', ) {%- if Parent == 'Udev' %} endif {%- endif %} fwupd-2.0.10/plugins/vendor-example/tests/000077500000000000000000000000001501337203100204635ustar00rootroot00000000000000fwupd-2.0.10/plugins/vendor-example/tests/vendor-example-setup.json000066400000000000000000000005641501337203100254470ustar00rootroot00000000000000{ "UsbDevices": [ { "NOTE1": "This file is created using the instructions in https://fwupd.github.io/libfwupdplugin/device-emulation.html", "NOTE2": "Then do 'unzip emulation.zip' and rename setup.json to this filename", "NOTE3": "You can delete this file if you have a emulation-url specified in the {{vendor}}-{{example}}.json file!" } ] } fwupd-2.0.10/plugins/vendor-example/tests/vendor-example.json000066400000000000000000000013351501337203100243060ustar00rootroot00000000000000{ "name": "{{Vendor}} {{Example}}", "interactive": false, "steps": [ { "url": "{{vendor}}-{{example}}-1.2.3.cab", "emulation-file": "@enumeration_datadir@/{{vendor}}-{{example}}-setup.json", "emulation-url": "{{vendor}}-{{example}}-1.2.3.zip", "components": [ { "version": "1.2.3", "guids": [ "00000000-0000-0000-0000-000000000000" ] } ] }, { "url": "{{vendor}}-{{example}}-1.2.4.cab", "emulation-url": "{{vendor}}-{{example}}-1.2.4.zip", "components": [ { "version": "1.2.4", "guids": [ "00000000-0000-0000-0000-000000000000" ] } ] } ] } fwupd-2.0.10/plugins/vendor-example/vendor-example.quirk.in000066400000000000000000000002221501337203100237250ustar00rootroot00000000000000# {{Example}} {%- if Parent in ['Usb', 'Hid'] %} [USB\VID_TODO&PID_TODO] {%- else %} [{{PARENT}}\ID_XXX] {%- endif %} Plugin = {{vendor_example}} fwupd-2.0.10/plugins/vli/000077500000000000000000000000001501337203100151655ustar00rootroot00000000000000fwupd-2.0.10/plugins/vli/README.md000066400000000000000000000060541501337203100164510ustar00rootroot00000000000000--- title: Plugin: VIA --- ## Introduction This plugin is used to update USB hubs from VIA. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in an undisclosed binary file format. This plugin supports the following protocol ID: * `com.vli.i2c` * `com.vli.pd` * `com.vli.usbhub` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_17EF&PID_3083&REV_0001` * `USB\VID_17EF&PID_3083` All VLI devices also use custom GUID values for the device type, e.g. * `USB\VID_17EF&PID_3083&DEV_VL812B3` These devices also use custom GUID values for the SPI flash configuration, e.g. * `CFI\FLASHID_37303840` * `CFI\FLASHID_3730` * `CFI\FLASHID_37` Optional PD child devices sharing the SPI flash use two extra GUIDs, e.g. * `USB\VID_17EF&PID_3083&DEV_VL102` * `USB\VID_17EF&PID_3083&APP_26` Optional I²C child devices use just one extra GUID, e.g. * `USB\VID_17EF&PID_3083&I2C_MSP430` * `USB\VID_17EF&PID_3083&I2C_PS186` ## Vendor ID Security The vendor ID is set from the USB vendor, for example set to `USB:0x2109` ## Quirk Use This plugin uses the following plugin-specific quirks: ### VliDeviceKind Device kind, e.g. `VL102`. Since: 1.3.7 ### VliSpiAutoDetect SPI autodetect (default 0x1). Since: 1.3.7 ### CfiDeviceCmdReadId Flash command to read the ID. Since: 1.3.3 ### CfiDeviceCmdReadIdSz Size of the ReadId response. The `CfiDeviceCmdReadId` and `CfiDeviceCmdReadIdSz` quirks have to be assigned to the device instance attribute, rather then the flash part as the ID is required to query the other flash chip parameters. For example: [USB\VID_2109&PID_0210] Plugin = vli GType = FuVliUsbhubDevice CfiDeviceCmdReadId = 0xf8 CfiDeviceCmdReadIdSz = 4 # W3IRDFLASHxxx [CFI\\FLASHID_37303840] CfiDeviceCmdChipErase = 0xc7 CfiDeviceCmdSectorErase = 0x20 ### Flags:attach-with-gpiob This flag is used if device needs GPIO-B to reset the device. ### Flags:unlock-legacy813 This flag is used for unlocking VL813 with a custom VDR request. ### Flags:has-shared-spi-pd This flag is used for devices that share SPI with the PD device. ### Flags:has-msp430 This flag is used if device has a MSP430 attached via I²C. ### Flags:has-rtd21xx This flag is used if device has a RTD21XX attached via I²C. ### Flags:has-i2c-ps186 This flag is used if device has a PS186 attached via I²C. ### Flags:skips-rom This flag handles cases to update in firmware mode, skips ROM mode entirely. ### Flags:attach-with-usb This flag is used if device needs unplug & re-plug usb cable to reset the device. ### Flags:attach-with-power This flag is used if device needs unplug & re-plug power cord to reset the device. ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.3.3`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Emily Miller: @memily fwupd-2.0.10/plugins/vli/fu-self-test.c000066400000000000000000000032531501337203100176520ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-vli-pd-common.h" static void fu_test_vli_pd_common_func(void) { struct { guint32 fwver; FuVliDeviceKind device_kind; } map[] = {{0x00, FU_VLI_DEVICE_KIND_UNKNOWN}, {0x01, FU_VLI_DEVICE_KIND_VL100}, {0x02, FU_VLI_DEVICE_KIND_VL100}, {0x03, FU_VLI_DEVICE_KIND_VL100}, {0x04, FU_VLI_DEVICE_KIND_VL101}, {0x05, FU_VLI_DEVICE_KIND_VL101}, {0x06, FU_VLI_DEVICE_KIND_VL101}, {0x07, FU_VLI_DEVICE_KIND_VL102}, {0x08, FU_VLI_DEVICE_KIND_VL102}, {0x09, FU_VLI_DEVICE_KIND_VL103}, {0x0A, FU_VLI_DEVICE_KIND_VL103}, {0x0B, FU_VLI_DEVICE_KIND_VL104}, {0x0C, FU_VLI_DEVICE_KIND_VL105}, {0x0D, FU_VLI_DEVICE_KIND_VL106}, {0x0E, FU_VLI_DEVICE_KIND_VL107}, {0x0F, FU_VLI_DEVICE_KIND_UNKNOWN}, {0xA0, FU_VLI_DEVICE_KIND_UNKNOWN}, {0xA1, FU_VLI_DEVICE_KIND_VL108}, {0xA2, FU_VLI_DEVICE_KIND_VL109}, {0xA3, FU_VLI_DEVICE_KIND_UNKNOWN}, {0xA4, FU_VLI_DEVICE_KIND_UNKNOWN}, {0xB1, FU_VLI_DEVICE_KIND_VL108}, {0xB2, FU_VLI_DEVICE_KIND_VL109}, {0xB3, FU_VLI_DEVICE_KIND_UNKNOWN}, {0xB4, FU_VLI_DEVICE_KIND_UNKNOWN}, {0xFF, FU_VLI_DEVICE_KIND_UNKNOWN}}; for (guint i = 0; map[i].fwver != 0xFF; i++) { g_debug("checking fwver 0x%02X", map[i].fwver); g_assert_cmpint(fu_vli_pd_common_guess_device_kind((guint32)map[i].fwver << 24), ==, map[i].device_kind); } } int main(int argc, char **argv) { g_test_init(&argc, &argv, NULL); g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); g_test_add_func("/vli/pd-common", fu_test_vli_pd_common_func); return g_test_run(); } fwupd-2.0.10/plugins/vli/fu-vli-common.c000066400000000000000000000071571501337203100200330ustar00rootroot00000000000000/* * Copyright 2017 VIA Corporation * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-vli-common.h" guint32 fu_vli_common_device_kind_get_size(FuVliDeviceKind device_kind) { if (device_kind == FU_VLI_DEVICE_KIND_VL100) return 0x8000; /* 32KB */ if (device_kind == FU_VLI_DEVICE_KIND_VL101) return 0xc000; /* 48KB */ if (device_kind == FU_VLI_DEVICE_KIND_VL102) return 0x8000; /* 32KB */ if (device_kind == FU_VLI_DEVICE_KIND_VL103) return 0x8000; /* 32KB */ if (device_kind == FU_VLI_DEVICE_KIND_VL104) return 0xc000; /* 48KB */ if (device_kind == FU_VLI_DEVICE_KIND_VL105) return 0xc000; /* 48KB */ if (device_kind == FU_VLI_DEVICE_KIND_VL106) return 0x8000; /* 32KB */ if (device_kind == FU_VLI_DEVICE_KIND_VL107) return 0xC800; /* 50KB */ if (device_kind == FU_VLI_DEVICE_KIND_VL108) return 0x10000; if (device_kind == FU_VLI_DEVICE_KIND_VL109) return 0x10000; if (device_kind == FU_VLI_DEVICE_KIND_VL122) return 0x80000; if (device_kind == FU_VLI_DEVICE_KIND_VL210) return 0x20000 * 2; if (device_kind == FU_VLI_DEVICE_KIND_VL211) return 0x20000 * 2; if (device_kind == FU_VLI_DEVICE_KIND_VL212) return 0x20000 * 2; if (device_kind == FU_VLI_DEVICE_KIND_VL810) return 0x8000; if (device_kind == FU_VLI_DEVICE_KIND_VL811) return 0x8000; if (device_kind == FU_VLI_DEVICE_KIND_VL811PB0) return 0x8000; if (device_kind == FU_VLI_DEVICE_KIND_VL811PB3) return 0x8000; if (device_kind == FU_VLI_DEVICE_KIND_VL812B0) return 0x8000; if (device_kind == FU_VLI_DEVICE_KIND_VL812B3) return 0x8000; if (device_kind == FU_VLI_DEVICE_KIND_VL812Q4S) return 0x8000; if (device_kind == FU_VLI_DEVICE_KIND_VL813) return 0x8000; if (device_kind == FU_VLI_DEVICE_KIND_VL815) return 0x10000; if (device_kind == FU_VLI_DEVICE_KIND_VL817) return 0x10000; if (device_kind == FU_VLI_DEVICE_KIND_VL817S) return 0x10000; if (device_kind == FU_VLI_DEVICE_KIND_VL819Q7) return 0x10000; if (device_kind == FU_VLI_DEVICE_KIND_VL819Q8) return 0x10000; if (device_kind == FU_VLI_DEVICE_KIND_VL820Q7) return 0x10000; if (device_kind == FU_VLI_DEVICE_KIND_VL820Q8) return 0x10000; if (device_kind == FU_VLI_DEVICE_KIND_VL821Q7) return 0x10000; if (device_kind == FU_VLI_DEVICE_KIND_VL821Q8) return 0x10000; if (device_kind == FU_VLI_DEVICE_KIND_VL822T) return 0x10000; if (device_kind == FU_VLI_DEVICE_KIND_VL822Q5) return 0x10000; if (device_kind == FU_VLI_DEVICE_KIND_VL822Q7) return 0x10000; if (device_kind == FU_VLI_DEVICE_KIND_VL822Q8) return 0x10000; if (device_kind == FU_VLI_DEVICE_KIND_VL822C0) return 0x10000; if (device_kind == FU_VLI_DEVICE_KIND_PS186) return 0x40000; if (device_kind == FU_VLI_DEVICE_KIND_VL650) return 0x40000; if (device_kind == FU_VLI_DEVICE_KIND_VL830) return 0x20000; if (device_kind == FU_VLI_DEVICE_KIND_VL832) return 0x28000; return 0x0; } guint32 fu_vli_common_device_kind_get_offset(FuVliDeviceKind device_kind) { if (device_kind == FU_VLI_DEVICE_KIND_VL100) return 0x10000; if (device_kind == FU_VLI_DEVICE_KIND_VL101) return 0x10000; if (device_kind == FU_VLI_DEVICE_KIND_VL102) return 0x20000; if (device_kind == FU_VLI_DEVICE_KIND_VL103) return 0x20000; if (device_kind == FU_VLI_DEVICE_KIND_VL104) return 0x20000; if (device_kind == FU_VLI_DEVICE_KIND_VL105) return 0x20000; if (device_kind == FU_VLI_DEVICE_KIND_VL106) return 0x20000; if (device_kind == FU_VLI_DEVICE_KIND_VL107) return 0x20000; if (device_kind == FU_VLI_DEVICE_KIND_VL108) return 0x20000; if (device_kind == FU_VLI_DEVICE_KIND_VL109) return 0x20000; return 0x0; } fwupd-2.0.10/plugins/vli/fu-vli-common.h000066400000000000000000000005521501337203100200300ustar00rootroot00000000000000/* * Copyright 2017 VIA Corporation * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-vli-struct.h" guint32 fu_vli_common_device_kind_get_size(FuVliDeviceKind device_kind); guint32 fu_vli_common_device_kind_get_offset(FuVliDeviceKind device_kind); fwupd-2.0.10/plugins/vli/fu-vli-device.c000066400000000000000000000522621501337203100177770ustar00rootroot00000000000000/* * Copyright 2017 VIA Corporation * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-vli-device.h" #include "fu-vli-struct.h" typedef struct { FuVliDeviceKind kind; FuCfiDevice *cfi_device; gboolean spi_auto_detect; guint8 spi_cmd_read_id_sz; guint32 flash_id; } FuVliDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuVliDevice, fu_vli_device, FU_TYPE_USB_DEVICE) #define GET_PRIVATE(o) (fu_vli_device_get_instance_private(o)) enum { PROP_0, PROP_KIND, PROP_LAST }; FuCfiDevice * fu_vli_device_get_cfi_device(FuVliDevice *self) { FuVliDevicePrivate *priv = GET_PRIVATE(self); return priv->cfi_device; } static gboolean fu_vli_device_spi_write_enable(FuVliDevice *self, GError **error) { FuVliDeviceClass *klass = FU_VLI_DEVICE_GET_CLASS(self); if (klass->spi_write_enable != NULL) { if (!klass->spi_write_enable(self, error)) { g_prefix_error(error, "failed to write enable SPI: "); return FALSE; } } return TRUE; } static gboolean fu_vli_device_spi_chip_erase(FuVliDevice *self, GError **error) { FuVliDeviceClass *klass = FU_VLI_DEVICE_GET_CLASS(self); if (klass->spi_chip_erase != NULL) { if (!klass->spi_chip_erase(self, error)) { g_prefix_error(error, "failed to erase SPI data: "); return FALSE; } } return TRUE; } static gboolean fu_vli_device_spi_write_status(FuVliDevice *self, guint8 status, GError **error) { FuVliDeviceClass *klass = FU_VLI_DEVICE_GET_CLASS(self); if (klass->spi_write_status != NULL) { if (!klass->spi_write_status(self, status, error)) { g_prefix_error(error, "failed to write SPI status 0x%x: ", status); return FALSE; } } return TRUE; } static gboolean fu_vli_device_spi_read_status(FuVliDevice *self, guint8 *status, GError **error) { FuVliDeviceClass *klass = FU_VLI_DEVICE_GET_CLASS(self); if (klass->spi_read_status != NULL) { if (!klass->spi_read_status(self, status, error)) { g_prefix_error(error, "failed to read status: "); return FALSE; } } return TRUE; } static gboolean fu_vli_device_spi_sector_erase(FuVliDevice *self, guint32 addr, GError **error) { FuVliDeviceClass *klass = FU_VLI_DEVICE_GET_CLASS(self); if (klass->spi_sector_erase != NULL) { if (!klass->spi_sector_erase(self, addr, error)) { g_prefix_error(error, "failed to erase SPI data @0x%x: ", addr); return FALSE; } } return TRUE; } gboolean fu_vli_device_spi_read_block(FuVliDevice *self, guint32 addr, guint8 *buf, gsize bufsz, GError **error) { FuVliDeviceClass *klass = FU_VLI_DEVICE_GET_CLASS(self); if (klass->spi_read_data != NULL) { if (!klass->spi_read_data(self, addr, buf, bufsz, error)) { g_prefix_error(error, "failed to read SPI data @0x%x: ", addr); return FALSE; } } return TRUE; } static gboolean fu_vli_device_spi_write_data(FuVliDevice *self, guint32 addr, const guint8 *buf, gsize bufsz, GError **error) { FuVliDeviceClass *klass = FU_VLI_DEVICE_GET_CLASS(self); if (klass->spi_write_data != NULL) { if (!klass->spi_write_data(self, addr, buf, bufsz, error)) { g_prefix_error(error, "failed to write SPI data @0x%x: ", addr); return FALSE; } } return TRUE; } static gboolean fu_vli_device_spi_wait_finish(FuVliDevice *self, GError **error) { const guint32 rdy_cnt = 2; guint32 cnt = 0; for (guint32 idx = 0; idx < 1000; idx++) { guint8 status = 0x7f; /* must get bit[1:0] == 0 twice in a row for success */ if (!fu_vli_device_spi_read_status(self, &status, error)) return FALSE; if ((status & 0x03) == 0x00) { if (cnt++ >= rdy_cnt) return TRUE; } else { cnt = 0; } fu_device_sleep(FU_DEVICE(self), 500); /* ms */ } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "failed to wait for SPI"); return FALSE; } gboolean fu_vli_device_spi_erase_sector(FuVliDevice *self, guint32 addr, GError **error) { const guint32 bufsz = 0x1000; /* erase sector */ if (!fu_vli_device_spi_write_enable(self, error)) { g_prefix_error(error, "->spi_write_enable failed: "); return FALSE; } if (!fu_vli_device_spi_write_status(self, 0x00, error)) { g_prefix_error(error, "->spi_write_status failed: "); return FALSE; } if (!fu_vli_device_spi_write_enable(self, error)) { g_prefix_error(error, "->spi_write_enable failed: "); return FALSE; } if (!fu_vli_device_spi_sector_erase(self, addr, error)) { g_prefix_error(error, "->spi_sector_erase failed: "); return FALSE; } if (!fu_vli_device_spi_wait_finish(self, error)) { g_prefix_error(error, "->spi_wait_finish failed: "); return FALSE; } /* verify it really was blanked */ for (guint32 offset = 0; offset < bufsz; offset += FU_VLI_DEVICE_TXSIZE) { guint8 buf[FU_VLI_DEVICE_TXSIZE] = {0x0}; if (!fu_vli_device_spi_read_block(self, addr + offset, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read back empty: "); return FALSE; } for (guint i = 0; i < sizeof(buf); i++) { if (buf[i] != 0xff) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "failed to check blank @0x%x", addr + offset + i); return FALSE; } } } /* success */ return TRUE; } GBytes * fu_vli_device_spi_read(FuVliDevice *self, guint32 address, gsize bufsz, FuProgress *progress, GError **error) { g_autofree guint8 *buf = g_malloc0(bufsz); g_autoptr(GPtrArray) chunks = NULL; /* get data from hardware */ chunks = fu_chunk_array_mutable_new(buf, bufsz, address, 0x0, FU_VLI_DEVICE_TXSIZE); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); if (!fu_vli_device_spi_read_block(self, fu_chunk_get_address(chk), fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "SPI data read failed @0x%x: ", (guint)fu_chunk_get_address(chk)); return NULL; } fu_progress_step_done(progress); } return g_bytes_new_take(g_steal_pointer(&buf), bufsz); } gboolean fu_vli_device_spi_write_block(FuVliDevice *self, guint32 address, const guint8 *buf, gsize bufsz, FuProgress *progress, GError **error) { g_autofree guint8 *buf_tmp = g_malloc0(bufsz); /* sanity check */ if (bufsz > FU_VLI_DEVICE_TXSIZE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot write 0x%x in one block", (guint)bufsz); return FALSE; } /* write */ g_debug("writing 0x%x block @0x%x", (guint)bufsz, address); if (!fu_vli_device_spi_write_enable(self, error)) { g_prefix_error(error, "enabling SPI write failed: "); return FALSE; } if (!fu_vli_device_spi_write_data(self, address, buf, bufsz, error)) { g_prefix_error(error, "SPI data write failed: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), 1); /* ms */ /* verify */ if (!fu_vli_device_spi_read_block(self, address, buf_tmp, bufsz, error)) { g_prefix_error(error, "SPI data read failed: "); return FALSE; } return fu_memcmp_safe(buf, bufsz, 0, buf_tmp, bufsz, 0, bufsz, error); } gboolean fu_vli_device_spi_write(FuVliDevice *self, guint32 address, const guint8 *buf, gsize bufsz, FuProgress *progress, GError **error) { FuChunk *chk; g_autoptr(GPtrArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 99, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "device-write-chk0"); /* write SPI data, then CRC bytes last */ g_debug("writing 0x%x bytes @0x%x", (guint)bufsz, address); chunks = fu_chunk_array_new(buf, bufsz, 0x0, 0x0, FU_VLI_DEVICE_TXSIZE); if (chunks->len > 1) { FuProgress *progress_local = fu_progress_get_child(progress); fu_progress_set_id(progress_local, G_STRLOC); fu_progress_set_steps(progress_local, chunks->len - 1); for (guint i = 1; i < chunks->len; i++) { chk = g_ptr_array_index(chunks, i); if (!fu_vli_device_spi_write_block(self, fu_chunk_get_address(chk) + address, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), fu_progress_get_child(progress_local), error)) { g_prefix_error(error, "failed to write block 0x%x: ", fu_chunk_get_idx(chk)); return FALSE; } fu_progress_step_done(progress_local); } } fu_progress_step_done(progress); /* chk0 */ chk = g_ptr_array_index(chunks, 0); if (!fu_vli_device_spi_write_block(self, fu_chunk_get_address(chk) + address, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to write CRC block: "); return FALSE; } fu_progress_step_done(progress); return TRUE; } gboolean fu_vli_device_spi_erase_all(FuVliDevice *self, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 99, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 1, NULL); if (!fu_vli_device_spi_write_enable(self, error)) return FALSE; if (!fu_vli_device_spi_write_status(self, 0x00, error)) return FALSE; if (!fu_vli_device_spi_write_enable(self, error)) return FALSE; if (!fu_vli_device_spi_chip_erase(self, error)) return FALSE; fu_device_sleep_full(FU_DEVICE(self), 4000, fu_progress_get_child(progress)); /* ms */ fu_progress_step_done(progress); /* verify chip was erased */ for (guint addr = 0; addr < 0x10000; addr += 0x1000) { guint8 buf[FU_VLI_DEVICE_TXSIZE] = {0x0}; if (!fu_vli_device_spi_read_block(self, addr, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read @0x%x: ", addr); return FALSE; } for (guint i = 0; i < sizeof(buf); i++) { if (buf[i] != 0xff) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "failed to verify erase @0x%x: ", addr); return FALSE; } } fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)addr + 0x1000, (gsize)0x10000); } fu_progress_step_done(progress); return TRUE; } gboolean fu_vli_device_spi_erase(FuVliDevice *self, guint32 addr, gsize sz, FuProgress *progress, GError **error) { g_autoptr(GPtrArray) chunks = fu_chunk_array_new(NULL, sz, addr, 0x0, 0x1000); g_debug("erasing 0x%x bytes @0x%x", (guint)sz, addr); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, chunks->len); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); g_debug("erasing @0x%x", (guint)fu_chunk_get_address(chk)); if (!fu_vli_device_spi_erase_sector(FU_VLI_DEVICE(self), fu_chunk_get_address(chk), error)) { g_prefix_error(error, "failed to erase FW sector @0x%x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } fu_progress_step_done(progress); } return TRUE; } static gchar * fu_vli_device_get_flash_id_str(FuVliDevice *self) { FuVliDevicePrivate *priv = GET_PRIVATE(self); if (priv->spi_cmd_read_id_sz == 4) return g_strdup_printf("%08X", priv->flash_id); if (priv->spi_cmd_read_id_sz == 2) return g_strdup_printf("%04X", priv->flash_id); if (priv->spi_cmd_read_id_sz == 1) return g_strdup_printf("%02X", priv->flash_id); return g_strdup_printf("%X", priv->flash_id); } void fu_vli_device_set_kind(FuVliDevice *self, FuVliDeviceKind device_kind) { FuVliDevicePrivate *priv = GET_PRIVATE(self); guint32 sz; /* set and notify if different */ if (priv->kind != device_kind) { priv->kind = device_kind; g_object_notify(G_OBJECT(self), "kind"); } /* newer chips use SHA-256 and ECDSA-256 */ switch (device_kind) { case FU_VLI_DEVICE_KIND_MSP430: case FU_VLI_DEVICE_KIND_PS186: case FU_VLI_DEVICE_KIND_RTD21XX: case FU_VLI_DEVICE_KIND_VL100: case FU_VLI_DEVICE_KIND_VL101: case FU_VLI_DEVICE_KIND_VL102: case FU_VLI_DEVICE_KIND_VL103: case FU_VLI_DEVICE_KIND_VL104: case FU_VLI_DEVICE_KIND_VL105: case FU_VLI_DEVICE_KIND_VL106: case FU_VLI_DEVICE_KIND_VL107: case FU_VLI_DEVICE_KIND_VL108: case FU_VLI_DEVICE_KIND_VL109: case FU_VLI_DEVICE_KIND_VL120: case FU_VLI_DEVICE_KIND_VL122: case FU_VLI_DEVICE_KIND_VL210: case FU_VLI_DEVICE_KIND_VL211: case FU_VLI_DEVICE_KIND_VL212: case FU_VLI_DEVICE_KIND_VL810: case FU_VLI_DEVICE_KIND_VL811: case FU_VLI_DEVICE_KIND_VL811PB0: case FU_VLI_DEVICE_KIND_VL811PB3: case FU_VLI_DEVICE_KIND_VL812B0: case FU_VLI_DEVICE_KIND_VL812B3: case FU_VLI_DEVICE_KIND_VL812Q4S: case FU_VLI_DEVICE_KIND_VL813: case FU_VLI_DEVICE_KIND_VL815: case FU_VLI_DEVICE_KIND_VL817: case FU_VLI_DEVICE_KIND_VL817S: case FU_VLI_DEVICE_KIND_VL819Q7: case FU_VLI_DEVICE_KIND_VL819Q8: case FU_VLI_DEVICE_KIND_VL820Q7: case FU_VLI_DEVICE_KIND_VL820Q8: case FU_VLI_DEVICE_KIND_VL821Q7: case FU_VLI_DEVICE_KIND_VL821Q8: case FU_VLI_DEVICE_KIND_VL822T: case FU_VLI_DEVICE_KIND_VL822Q5: case FU_VLI_DEVICE_KIND_VL822Q7: case FU_VLI_DEVICE_KIND_VL822Q8: case FU_VLI_DEVICE_KIND_VL822C0: case FU_VLI_DEVICE_KIND_VL830: case FU_VLI_DEVICE_KIND_VL832: fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); break; case FU_VLI_DEVICE_KIND_VL650: fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); break; default: g_warning("device kind %s [0x%02x] does not indicate unsigned/signed payload", fu_vli_device_kind_to_string(device_kind), device_kind); break; } /* set maximum firmware size */ sz = fu_vli_common_device_kind_get_size(device_kind); if (sz > 0x0) fu_device_set_firmware_size_max(FU_DEVICE(self), sz); /* add extra DEV GUID too */ fu_device_add_instance_str(FU_DEVICE(self), "DEV", fu_vli_device_kind_to_string(priv->kind)); fu_device_build_instance_id(FU_DEVICE(self), NULL, "USB", "VID", "PID", "DEV", NULL); } void fu_vli_device_set_spi_auto_detect(FuVliDevice *self, gboolean spi_auto_detect) { FuVliDevicePrivate *priv = GET_PRIVATE(self); priv->spi_auto_detect = spi_auto_detect; } FuVliDeviceKind fu_vli_device_get_kind(FuVliDevice *self) { FuVliDevicePrivate *priv = GET_PRIVATE(self); return priv->kind; } guint32 fu_vli_device_get_offset(FuVliDevice *self) { FuVliDevicePrivate *priv = GET_PRIVATE(self); return fu_vli_common_device_kind_get_offset(priv->kind); } static void fu_vli_device_to_string(FuDevice *device, guint idt, GString *str) { FuVliDevice *self = FU_VLI_DEVICE(device); FuVliDevicePrivate *priv = GET_PRIVATE(self); if (priv->kind != FU_VLI_DEVICE_KIND_UNKNOWN) fwupd_codec_string_append(str, idt, "DeviceKind", fu_vli_device_kind_to_string(priv->kind)); fwupd_codec_string_append_bool(str, idt, "SpiAutoDetect", priv->spi_auto_detect); if (priv->flash_id != 0x0) { g_autofree gchar *tmp = fu_vli_device_get_flash_id_str(self); fwupd_codec_string_append(str, idt, "FlashId", tmp); } fu_device_add_string(FU_DEVICE(priv->cfi_device), idt + 1, str); } static gboolean fu_vli_device_spi_read_flash_id(FuVliDevice *self, GError **error) { FuVliDevicePrivate *priv = GET_PRIVATE(self); guint8 buf[4] = {0x0}; guint8 spi_cmd = 0x0; if (!fu_cfi_device_get_cmd(priv->cfi_device, FU_CFI_DEVICE_CMD_READ_ID, &spi_cmd, error)) return FALSE; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xc0 | (priv->spi_cmd_read_id_sz * 2), spi_cmd, 0x0000, buf, sizeof(buf), NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to read chip ID: "); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "SpiCmdReadId", buf, sizeof(buf)); if (priv->spi_cmd_read_id_sz == 4) { if (!fu_memread_uint32_safe(buf, sizeof(buf), 0x0, &priv->flash_id, G_BIG_ENDIAN, error)) return FALSE; } else if (priv->spi_cmd_read_id_sz == 2) { guint16 tmp = 0; if (!fu_memread_uint16_safe(buf, sizeof(buf), 0x0, &tmp, G_BIG_ENDIAN, error)) return FALSE; priv->flash_id = tmp; } else if (priv->spi_cmd_read_id_sz == 1) { guint8 tmp = 0; if (!fu_memread_uint8_safe(buf, sizeof(buf), 0x0, &tmp, error)) return FALSE; priv->flash_id = tmp; } return TRUE; } static gboolean fu_vli_device_setup(FuDevice *device, GError **error) { FuVliDevice *self = FU_VLI_DEVICE(device); FuVliDevicePrivate *priv = GET_PRIVATE(self); /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_vli_device_parent_class)->setup(device, error)) return FALSE; /* get the flash chip attached */ if (priv->spi_auto_detect) { if (!fu_vli_device_spi_read_flash_id(self, error)) { g_prefix_error(error, "failed to read SPI chip ID: "); return FALSE; } if (priv->flash_id != 0x0) { g_autofree gchar *flash_id = fu_vli_device_get_flash_id_str(self); /* use the correct flash device */ fu_cfi_device_set_flash_id(priv->cfi_device, flash_id); if (!fu_device_setup(FU_DEVICE(priv->cfi_device), error)) return FALSE; /* add extra instance IDs to include the SPI variant */ fu_device_add_instance_str(device, "SPI", flash_id); if (!fu_device_build_instance_id(device, error, "USB", "VID", "PID", "SPI", NULL)) return FALSE; fu_device_build_instance_id(device, NULL, "USB", "VID", "PID", "SPI", "REV", NULL); } } /* success */ return TRUE; } static gboolean fu_vli_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuVliDevice *self = FU_VLI_DEVICE(device); FuVliDevicePrivate *priv = GET_PRIVATE(self); guint64 tmp = 0; if (g_strcmp0(key, "CfiDeviceCmdReadIdSz") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->spi_cmd_read_id_sz = tmp; return TRUE; } if (g_strcmp0(key, "VliSpiAutoDetect") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT8, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->spi_auto_detect = tmp > 0; return TRUE; } if (g_strcmp0(key, "VliDeviceKind") == 0) { FuVliDeviceKind device_kind; device_kind = fu_vli_device_kind_from_string(value); if (device_kind == FU_VLI_DEVICE_KIND_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "VliDeviceKind %s is not supported", value); return FALSE; } fu_vli_device_set_kind(self, device_kind); return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_vli_device_report_metadata_pre(FuDevice *device, GHashTable *metadata) { FuVliDevice *self = FU_VLI_DEVICE(device); g_hash_table_insert(metadata, g_strdup("GType"), g_strdup(G_OBJECT_TYPE_NAME(self))); } static void fu_vli_device_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuVliDevice *self = FU_VLI_DEVICE(object); FuVliDevicePrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_KIND: g_value_set_uint(value, priv->kind); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_vli_device_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuVliDevice *self = FU_VLI_DEVICE(object); switch (prop_id) { case PROP_KIND: fu_vli_device_set_kind(self, g_value_get_uint(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_vli_device_constructed(GObject *obj) { FuVliDevice *self = FU_VLI_DEVICE(obj); FuVliDevicePrivate *priv = GET_PRIVATE(self); priv->cfi_device = fu_cfi_device_new(fu_device_get_context(FU_DEVICE(self)), NULL); } static void fu_vli_device_init(FuVliDevice *self) { FuVliDevicePrivate *priv = GET_PRIVATE(self); priv->spi_cmd_read_id_sz = 2; priv->spi_auto_detect = TRUE; fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_COUNTERPART_GUIDS); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_NO_SERIAL_NUMBER); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_ADD_INSTANCE_ID_REV); } static void fu_vli_device_finalize(GObject *obj) { FuVliDevice *self = FU_VLI_DEVICE(obj); FuVliDevicePrivate *priv = GET_PRIVATE(self); g_object_unref(priv->cfi_device); G_OBJECT_CLASS(fu_vli_device_parent_class)->finalize(obj); } static void fu_vli_device_class_init(FuVliDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); GParamSpec *pspec; /* properties */ object_class->get_property = fu_vli_device_get_property; object_class->set_property = fu_vli_device_set_property; object_class->constructed = fu_vli_device_constructed; object_class->finalize = fu_vli_device_finalize; /** * FuVliDevice:kind: * * The kind of VLI device. */ pspec = g_param_spec_uint("kind", NULL, NULL, 0, G_MAXUINT, 0, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_KIND, pspec); device_class->to_string = fu_vli_device_to_string; device_class->set_quirk_kv = fu_vli_device_set_quirk_kv; device_class->setup = fu_vli_device_setup; device_class->report_metadata_pre = fu_vli_device_report_metadata_pre; } fwupd-2.0.10/plugins/vli/fu-vli-device.h000066400000000000000000000045461501337203100200060ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-vli-common.h" #define FU_TYPE_VLI_DEVICE (fu_vli_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuVliDevice, fu_vli_device, FU, VLI_DEVICE, FuUsbDevice) struct _FuVliDeviceClass { FuUsbDeviceClass parent_class; gboolean (*spi_chip_erase)(FuVliDevice *self, GError **error); gboolean (*spi_sector_erase)(FuVliDevice *self, guint32 addr, GError **error); gboolean (*spi_read_data)(FuVliDevice *self, guint32 addr, guint8 *buf, gsize bufsz, GError **error); gboolean (*spi_read_status)(FuVliDevice *self, guint8 *status, GError **error); gboolean (*spi_write_enable)(FuVliDevice *self, GError **error); gboolean (*spi_write_data)(FuVliDevice *self, guint32 addr, const guint8 *buf, gsize bufsz, GError **error); gboolean (*spi_write_status)(FuVliDevice *self, guint8 status, GError **error); }; #define FU_VLI_DEVICE_TIMEOUT 3000 /* ms */ #define FU_VLI_DEVICE_TXSIZE 0x20 /* bytes */ void fu_vli_device_set_kind(FuVliDevice *self, FuVliDeviceKind device_kind); void fu_vli_device_set_spi_auto_detect(FuVliDevice *self, gboolean spi_auto_detect); FuVliDeviceKind fu_vli_device_get_kind(FuVliDevice *self); guint32 fu_vli_device_get_offset(FuVliDevice *self); FuCfiDevice * fu_vli_device_get_cfi_device(FuVliDevice *self); gboolean fu_vli_device_spi_erase_sector(FuVliDevice *self, guint32 addr, GError **error); gboolean fu_vli_device_spi_erase_all(FuVliDevice *self, FuProgress *progress, GError **error); gboolean fu_vli_device_spi_erase(FuVliDevice *self, guint32 addr, gsize sz, FuProgress *progress, GError **error); gboolean fu_vli_device_spi_read_block(FuVliDevice *self, guint32 addr, guint8 *buf, gsize bufsz, GError **error); GBytes * fu_vli_device_spi_read(FuVliDevice *self, guint32 address, gsize bufsz, FuProgress *progress, GError **error); gboolean fu_vli_device_spi_write_block(FuVliDevice *self, guint32 address, const guint8 *buf, gsize bufsz, FuProgress *progress, GError **error); gboolean fu_vli_device_spi_write(FuVliDevice *self, guint32 address, const guint8 *buf, gsize bufsz, FuProgress *progress, GError **error); fwupd-2.0.10/plugins/vli/fu-vli-pd-common.c000066400000000000000000000027111501337203100204230ustar00rootroot00000000000000/* * Copyright 2017 VIA Corporation * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-vli-pd-common.h" #include "fu-vli-struct.h" FuVliDeviceKind fu_vli_pd_common_guess_device_kind(guint32 fwver) { guint32 tmp = (fwver & 0xFF000000) >> 24; if (tmp < 0xA0) tmp = tmp & 0x0F; if (tmp == FU_VLI_DEVICE_FW_TAG_VL100A || tmp == FU_VLI_DEVICE_FW_TAG_VL100B || tmp == FU_VLI_DEVICE_FW_TAG_VL100C) return FU_VLI_DEVICE_KIND_VL100; if (tmp == FU_VLI_DEVICE_FW_TAG_VL101A || tmp == FU_VLI_DEVICE_FW_TAG_VL101B || tmp == FU_VLI_DEVICE_FW_TAG_VL101C) return FU_VLI_DEVICE_KIND_VL101; if (tmp == FU_VLI_DEVICE_FW_TAG_VL102A || tmp == FU_VLI_DEVICE_FW_TAG_VL102B) return FU_VLI_DEVICE_KIND_VL102; if (tmp == FU_VLI_DEVICE_FW_TAG_VL103A || tmp == FU_VLI_DEVICE_FW_TAG_VL103B) return FU_VLI_DEVICE_KIND_VL103; if (tmp == FU_VLI_DEVICE_FW_TAG_VL104) return FU_VLI_DEVICE_KIND_VL104; if (tmp == FU_VLI_DEVICE_FW_TAG_VL105) return FU_VLI_DEVICE_KIND_VL105; if (tmp == FU_VLI_DEVICE_FW_TAG_VL106) return FU_VLI_DEVICE_KIND_VL106; if (tmp == FU_VLI_DEVICE_FW_TAG_VL107) return FU_VLI_DEVICE_KIND_VL107; if (tmp == FU_VLI_DEVICE_FW_TAG_VL108A || tmp == FU_VLI_DEVICE_FW_TAG_VL108B) return FU_VLI_DEVICE_KIND_VL108; if (tmp == FU_VLI_DEVICE_FW_TAG_VL109A || tmp == FU_VLI_DEVICE_FW_TAG_VL109B) return FU_VLI_DEVICE_KIND_VL109; return FU_VLI_DEVICE_KIND_UNKNOWN; } fwupd-2.0.10/plugins/vli/fu-vli-pd-common.h000066400000000000000000000021351501337203100204300ustar00rootroot00000000000000/* * Copyright 2017 VIA Corporation * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-vli-common.h" #define VLI_USBHUB_PD_FLASHMAP_ADDR_LEGACY 0x4000 #define VLI_USBHUB_PD_FLASHMAP_ADDR 0x1003 #define FU_VLI_DEVICE_FW_TAG_VL100A 0x01 #define FU_VLI_DEVICE_FW_TAG_VL100B 0x02 #define FU_VLI_DEVICE_FW_TAG_VL100C 0x03 #define FU_VLI_DEVICE_FW_TAG_VL101A 0x04 #define FU_VLI_DEVICE_FW_TAG_VL101B 0x05 #define FU_VLI_DEVICE_FW_TAG_VL101C 0x06 #define FU_VLI_DEVICE_FW_TAG_VL102A 0x07 #define FU_VLI_DEVICE_FW_TAG_VL102B 0x08 #define FU_VLI_DEVICE_FW_TAG_VL103A 0x09 #define FU_VLI_DEVICE_FW_TAG_VL103B 0x0A #define FU_VLI_DEVICE_FW_TAG_VL104 0x0B #define FU_VLI_DEVICE_FW_TAG_VL105 0x0C #define FU_VLI_DEVICE_FW_TAG_VL106 0x0D #define FU_VLI_DEVICE_FW_TAG_VL107 0x0E #define FU_VLI_DEVICE_FW_TAG_VL108A 0xA1 #define FU_VLI_DEVICE_FW_TAG_VL108B 0xB1 #define FU_VLI_DEVICE_FW_TAG_VL109A 0xA2 #define FU_VLI_DEVICE_FW_TAG_VL109B 0xB2 FuVliDeviceKind fu_vli_pd_common_guess_device_kind(guint32 fwver); fwupd-2.0.10/plugins/vli/fu-vli-pd-device.c000066400000000000000000000656331501337203100204060ustar00rootroot00000000000000/* * Copyright 2015 VIA Corporation * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-vli-pd-device.h" #include "fu-vli-pd-firmware.h" #include "fu-vli-pd-parade-device.h" #include "fu-vli-struct.h" struct _FuVliPdDevice { FuVliDevice parent_instance; }; #define FU_VLI_PD_DEVICE_FLAG_HAS_I2C_PS186 "has-i2c-ps186" #define FU_VLI_PD_DEVICE_FLAG_SKIPS_ROM "skips-rom" G_DEFINE_TYPE(FuVliPdDevice, fu_vli_pd_device, FU_TYPE_VLI_DEVICE) static gboolean fu_vli_pd_device_read_regs(FuVliPdDevice *self, guint16 addr, guint8 *buf, gsize bufsz, GError **error) { g_autofree gchar *title = g_strdup_printf("ReadRegs@0x%x", addr); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xe0, ((addr & 0xff) << 8) | 0x01, addr >> 8, buf, bufsz, NULL, 1000, NULL, error)) { g_prefix_error(error, "failed to write register @0x%x: ", addr); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, title, buf, bufsz); return TRUE; } static gboolean fu_vli_pd_device_read_reg(FuVliPdDevice *self, guint16 addr, guint8 *value, GError **error) { return fu_vli_pd_device_read_regs(self, addr, value, 0x1, error); } static gboolean fu_vli_pd_device_write_reg(FuVliPdDevice *self, guint16 addr, guint8 value, GError **error) { g_autofree gchar *title = g_strdup_printf("WriteReg@0x%x", addr); fu_dump_raw(G_LOG_DOMAIN, title, &value, sizeof(value)); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xe0, ((addr & 0xff) << 8) | 0x02, addr >> 8, &value, sizeof(value), NULL, 1000, NULL, error)) { g_prefix_error(error, "failed to write register @0x%x: ", addr); return FALSE; } return TRUE; } static gboolean fu_vli_pd_device_spi_read_status(FuVliDevice *self, guint8 *status, GError **error) { guint8 spi_cmd = 0x0; if (!fu_cfi_device_get_cmd(fu_vli_device_get_cfi_device(self), FU_CFI_DEVICE_CMD_READ_STATUS, &spi_cmd, error)) return FALSE; return fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xc5, spi_cmd, 0x0000, status, 0x1, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error); } static gboolean fu_vli_pd_device_spi_read_data(FuVliDevice *self, guint32 addr, guint8 *buf, gsize bufsz, GError **error) { guint8 spi_cmd = 0x0; guint16 value; guint16 index; if (!fu_cfi_device_get_cmd(fu_vli_device_get_cfi_device(self), FU_CFI_DEVICE_CMD_READ_DATA, &spi_cmd, error)) return FALSE; value = ((addr << 8) & 0xff00) | spi_cmd; index = addr >> 8; return fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xc4, value, index, buf, bufsz, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error); } static gboolean fu_vli_pd_device_spi_write_status(FuVliDevice *self, guint8 status, GError **error) { guint8 spi_cmd = 0x0; guint16 value; if (!fu_cfi_device_get_cmd(fu_vli_device_get_cfi_device(self), FU_CFI_DEVICE_CMD_WRITE_STATUS, &spi_cmd, error)) return FALSE; value = ((guint16)status << 8) | spi_cmd; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xd8, value, 0x0, NULL, 0, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error)) { return FALSE; } /* Fix_For_GD_&_EN_SPI_Flash */ fu_device_sleep(FU_DEVICE(self), 100); /* ms */ return TRUE; } static gboolean fu_vli_pd_device_spi_write_enable(FuVliDevice *self, GError **error) { guint8 spi_cmd = 0x0; if (!fu_cfi_device_get_cmd(fu_vli_device_get_cfi_device(self), FU_CFI_DEVICE_CMD_WRITE_EN, &spi_cmd, error)) return FALSE; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xd4, spi_cmd, 0x0000, NULL, 0x0, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error)) { return FALSE; } return TRUE; } static gboolean fu_vli_pd_device_spi_chip_erase(FuVliDevice *self, GError **error) { guint8 spi_cmd = 0x0; if (!fu_cfi_device_get_cmd(fu_vli_device_get_cfi_device(self), FU_CFI_DEVICE_CMD_CHIP_ERASE, &spi_cmd, error)) return FALSE; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xd1, spi_cmd, 0x0000, NULL, 0x0, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error)) { return FALSE; } return TRUE; } static gboolean fu_vli_pd_device_spi_sector_erase(FuVliDevice *self, guint32 addr, GError **error) { guint8 spi_cmd = 0x0; guint16 value; guint16 index; if (!fu_cfi_device_get_cmd(fu_vli_device_get_cfi_device(self), FU_CFI_DEVICE_CMD_SECTOR_ERASE, &spi_cmd, error)) return FALSE; value = ((addr << 8) & 0xff00) | spi_cmd; index = addr >> 8; return fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xd2, value, index, NULL, 0x0, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error); } static gboolean fu_vli_pd_device_spi_write_data(FuVliDevice *self, guint32 addr, const guint8 *buf, gsize bufsz, GError **error) { guint8 spi_cmd = 0x0; guint16 value; guint16 index; g_autofree guint8 *buf_mut = NULL; if (!fu_cfi_device_get_cmd(fu_vli_device_get_cfi_device(self), FU_CFI_DEVICE_CMD_PAGE_PROG, &spi_cmd, error)) return FALSE; value = ((addr << 8) & 0xff00) | spi_cmd; index = addr >> 8; buf_mut = fu_memdup_safe(buf, bufsz, error); if (buf_mut == NULL) return FALSE; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xdc, value, index, buf_mut, bufsz, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error)) { return FALSE; } return TRUE; } static gboolean fu_vli_pd_device_parade_setup(FuVliPdDevice *self, GError **error) { g_autoptr(FuDevice) dev = NULL; g_autoptr(GError) error_local = NULL; /* add child */ dev = fu_vli_pd_parade_device_new(FU_VLI_DEVICE(self)); if (!fu_device_probe(dev, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_debug("%s", error_local->message); } else { g_warning("cannot create I²C parade device: %s", error_local->message); } return TRUE; } if (!fu_device_setup(dev, error)) { g_prefix_error(error, "failed to set up parade device: "); return FALSE; } fu_device_add_child(FU_DEVICE(self), dev); return TRUE; } static gboolean fu_vli_pd_device_setup(FuDevice *device, GError **error) { FuVliPdDevice *self = FU_VLI_PD_DEVICE(device); guint32 version_raw; guint8 verbuf[4] = {0x0}; guint8 value = 0; guint8 cmp = 0; /* FuVliDevice->setup */ if (!FU_DEVICE_CLASS(fu_vli_pd_device_parent_class)->setup(device, error)) return FALSE; /* get version */ if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xe2, 0x0001, 0x0000, verbuf, sizeof(verbuf), NULL, 1000, NULL, error)) { g_prefix_error(error, "failed to get version: "); return FALSE; } if (!fu_memread_uint32_safe(verbuf, sizeof(verbuf), 0x0, &version_raw, G_BIG_ENDIAN, error)) return FALSE; fu_device_set_version_raw(FU_DEVICE(self), version_raw); /* get device kind if not already in ROM mode */ if (fu_vli_device_get_kind(FU_VLI_DEVICE(self)) == FU_VLI_DEVICE_KIND_UNKNOWN) { if (!fu_vli_pd_device_read_reg(self, FU_VLI_PD_REGISTER_ADDRESS_PROJ_LEGACY, &value, error)) return FALSE; if (value != 0xFF) { switch (value & 0xF0) { case 0x00: fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL100); break; case 0x10: /* this is also the code for VL101, but VL102 is more likely */ fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL102); break; case 0x80: fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL103); break; case 0x90: fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL104); break; case 0xA0: fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL105); break; case 0xB0: fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL106); break; case 0xC0: fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL107); break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "unable to map 0x0018=0x%02X to device kind", value); return FALSE; } } else { if (!fu_vli_pd_device_read_reg(self, FU_VLI_PD_REGISTER_ADDRESS_PROJ_ID_HIGH, &value, error)) return FALSE; if (!fu_vli_pd_device_read_reg(self, FU_VLI_PD_REGISTER_ADDRESS_PROJ_ID_LOW, &cmp, error)) return FALSE; if ((value == 0x35) && (cmp == 0x96)) fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL108); else if ((value == 0x36) && (cmp == 0x01)) fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL109); else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "unable to map 0x9C9D=0x%02X%02X to device kind", value, cmp); return FALSE; } } } /* get bootloader mode */ if (!fu_vli_pd_device_read_reg(self, 0x00F7, &value, error)) return FALSE; if ((value & 0x80) == 0x00) fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); else fu_device_remove_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); /* detect any I²C child, e.g. parade device */ if (fu_device_has_private_flag(device, FU_VLI_PD_DEVICE_FLAG_HAS_I2C_PS186)) { if (!fu_vli_pd_device_parade_setup(self, error)) return FALSE; } /* success */ return TRUE; } static FuFirmware * fu_vli_pd_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuVliPdDevice *self = FU_VLI_PD_DEVICE(device); FuVliDeviceKind device_kind; g_autoptr(FuFirmware) firmware = fu_vli_pd_firmware_new(); /* check size */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; if (fu_firmware_get_size(firmware) > fu_device_get_firmware_size_max(device)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware too large, got 0x%x, expected <= 0x%x", (guint)fu_firmware_get_size(firmware), (guint)fu_device_get_firmware_size_max(device)); return NULL; } /* check is compatible with firmware */ device_kind = fu_vli_pd_firmware_get_kind(FU_VLI_PD_FIRMWARE(firmware)); if (fu_vli_device_get_kind(FU_VLI_DEVICE(self)) != device_kind) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware incompatible, got %s, expected %s", fu_vli_device_kind_to_string(device_kind), fu_vli_device_kind_to_string(fu_vli_device_get_kind(FU_VLI_DEVICE(self)))); return NULL; } /* we could check this against flags */ g_debug("parsed version: %s", fu_firmware_get_version(firmware)); return g_steal_pointer(&firmware); } static GBytes * fu_vli_pd_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuVliPdDevice *self = FU_VLI_PD_DEVICE(device); g_autoptr(FuDeviceLocker) locker = NULL; /* require detach -> attach */ locker = fu_device_locker_new_full(device, (FuDeviceLockerFunc)fu_device_detach, (FuDeviceLockerFunc)fu_device_attach, error); if (locker == NULL) return NULL; fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ); return fu_vli_device_spi_read(FU_VLI_DEVICE(self), 0x0, fu_device_get_firmware_size_max(device), progress, error); } static gboolean fu_vli_pd_device_write_gpios(FuVliPdDevice *self, GError **error) { /* disable UART-Rx mode */ if (!fu_vli_pd_device_write_reg(self, 0x0015, 0x7F, error)) return FALSE; /* disable 'Watch Mode', chip is not in debug mode */ if (!fu_vli_pd_device_write_reg(self, 0x0019, 0x00, error)) return FALSE; /* GPIO3 output enable, switch/CMOS/Boost control pin */ if (!fu_vli_pd_device_write_reg(self, 0x001C, 0x02, error)) return FALSE; return TRUE; } static gboolean fu_vli_pd_device_write_dual_firmware(FuVliPdDevice *self, GBytes *fw, FuProgress *progress, GError **error) { const guint8 *buf = NULL; const guint8 *sbuf = NULL; gsize bufsz = 0; gsize sbufsz = 0; guint16 crc_actual; guint16 crc_file = 0x0; guint32 sec_addr = 0x28000; g_autoptr(GBytes) spi_fw = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 10, "crc"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 45, "backup"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 45, "primary"); /* check spi fw1 crc16 */ spi_fw = fu_vli_device_spi_read(FU_VLI_DEVICE(self), fu_vli_device_get_offset(FU_VLI_DEVICE(self)), fu_device_get_firmware_size_max(FU_DEVICE(self)), fu_progress_get_child(progress), error); if (spi_fw == NULL) return FALSE; sbuf = g_bytes_get_data(spi_fw, &sbufsz); if (sbufsz != 0x8000) sec_addr = 0x30000; if (sbufsz < 2) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "buffer was too small"); return FALSE; } if (!fu_memread_uint16_safe(sbuf, sbufsz, sbufsz - 2, &crc_file, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to read file CRC: "); return FALSE; } crc_actual = fu_crc16(FU_CRC_KIND_B16_USB, sbuf, sbufsz - 2); fu_progress_step_done(progress); /* update fw2 first if fw1 correct */ buf = g_bytes_get_data(fw, &bufsz); if (crc_actual == crc_file) { if (!fu_vli_device_spi_write(FU_VLI_DEVICE(self), sec_addr, buf, bufsz, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); if (!fu_vli_device_spi_write(FU_VLI_DEVICE(self), fu_vli_device_get_offset(FU_VLI_DEVICE(self)), buf, bufsz, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* else update fw1 first */ } else { if (!fu_vli_device_spi_write(FU_VLI_DEVICE(self), fu_vli_device_get_offset(FU_VLI_DEVICE(self)), buf, bufsz, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); if (!fu_vli_device_spi_write(FU_VLI_DEVICE(self), sec_addr, buf, bufsz, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_vli_pd_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuVliPdDevice *self = FU_VLI_PD_DEVICE(device); gsize bufsz = 0; guint8 tmp = 0; const guint8 *buf = NULL; g_autoptr(GBytes) fw = NULL; /* binary blob */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* write GPIOs in new mode */ if (!fu_vli_pd_device_write_gpios(self, error)) return FALSE; /* disable write protect in GPIO_3 */ if (!fu_vli_pd_device_read_reg(self, FU_VLI_PD_REGISTER_ADDRESS_GPIO_CONTROL_A, &tmp, error)) return FALSE; if (!fu_vli_pd_device_write_reg(self, FU_VLI_PD_REGISTER_ADDRESS_GPIO_CONTROL_A, tmp | 0x44, error)) return FALSE; /* dual image on VL103 */ if (fu_vli_device_get_kind(FU_VLI_DEVICE(device)) == FU_VLI_DEVICE_KIND_VL103 && fu_device_has_flag(device, FWUPD_DEVICE_FLAG_DUAL_IMAGE)) return fu_vli_pd_device_write_dual_firmware(self, fw, progress, error); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 63, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 37, NULL); /* erase */ if (!fu_vli_device_spi_erase_all(FU_VLI_DEVICE(self), fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to erase all: "); return FALSE; } fu_progress_step_done(progress); /* write in chunks */ buf = g_bytes_get_data(fw, &bufsz); if (!fu_vli_device_spi_write(FU_VLI_DEVICE(self), fu_vli_device_get_offset(FU_VLI_DEVICE(self)), buf, bufsz, fu_progress_get_child(progress), error)) return FALSE; /* success */ fu_progress_step_done(progress); return TRUE; } static gboolean fu_vli_pd_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuVliPdDevice *self = FU_VLI_PD_DEVICE(device); g_autoptr(GError) error_local = NULL; guint8 gpio_control_a = 0; /* sanity check */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in bootloader mode, skipping"); return TRUE; } /* VL103 only updates in ROM mode, check other devices for skips-rom flag */ if (fu_vli_device_get_kind(FU_VLI_DEVICE(device)) != FU_VLI_DEVICE_KIND_VL103 && fu_device_has_private_flag(device, FU_VLI_PD_DEVICE_FLAG_SKIPS_ROM)) { return TRUE; } /* write GPIOs */ if (!fu_vli_pd_device_write_gpios(self, error)) return FALSE; /* disable charging */ if (!fu_vli_pd_device_read_reg(self, FU_VLI_PD_REGISTER_ADDRESS_GPIO_CONTROL_A, &gpio_control_a, error)) return FALSE; gpio_control_a = (gpio_control_a | 0x10) & 0xFE; if (!fu_vli_pd_device_write_reg(self, FU_VLI_PD_REGISTER_ADDRESS_GPIO_CONTROL_A, gpio_control_a, error)) return FALSE; /* VL103 set ROM sig does not work, so use alternate function */ if ((fu_vli_device_get_kind(FU_VLI_DEVICE(device)) != FU_VLI_DEVICE_KIND_VL100) && (fu_vli_device_get_kind(FU_VLI_DEVICE(device)) != FU_VLI_DEVICE_KIND_VL102)) { if (!fu_usb_device_control_transfer(FU_USB_DEVICE(FU_USB_DEVICE(device)), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xc0, 0x0000, 0x0000, NULL, 0x0, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_debug("ignoring %s", error_local->message); } else { g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to restart device: "); return FALSE; } } fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } /* patch APP5 FW bug (2AF2 -> 2AE2) on VL100-App5 and VL102 */ if (fu_vli_device_get_kind(FU_VLI_DEVICE(device)) == FU_VLI_DEVICE_KIND_VL100 || fu_vli_device_get_kind(FU_VLI_DEVICE(device)) == FU_VLI_DEVICE_KIND_VL102) { guint8 proj_legacy = 0; if (!fu_vli_pd_device_read_reg(self, FU_VLI_PD_REGISTER_ADDRESS_PROJ_LEGACY, &proj_legacy, error)) return FALSE; if (proj_legacy != 0x80) { if (!fu_vli_pd_device_write_reg(self, 0x2AE2, 0x1E, error)) return FALSE; if (!fu_vli_pd_device_write_reg(self, 0x2AE3, 0xC3, error)) return FALSE; if (!fu_vli_pd_device_write_reg(self, 0x2AE4, 0x5A, error)) return FALSE; if (!fu_vli_pd_device_write_reg(self, 0x2AE5, 0x87, error)) return FALSE; } } /* set ROM sig */ if (!fu_usb_device_control_transfer(FU_USB_DEVICE(FU_USB_DEVICE(device)), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xa0, 0x0000, 0x0000, NULL, 0x0, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error)) return FALSE; /* reset from SPI_Code into ROM_Code */ if (!fu_usb_device_control_transfer(FU_USB_DEVICE(FU_USB_DEVICE(device)), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xb0, 0x0000, 0x0000, NULL, 0x0, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_debug("ignoring %s", error_local->message); } else { g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to restart device: "); return FALSE; } } fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static gboolean fu_vli_pd_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuVliPdDevice *self = FU_VLI_PD_DEVICE(device); g_autoptr(GError) error_local = NULL; /* Work around a silicon bug: Once the CC-resistor is removed, the * CC-host thinks the device is un-plugged and turn off VBUS (power). * When VL103 is powered-off, VL103 puts a resistor at CC-pin. * The CC-host will think the device is re-plugged and provides VBUS * again. Then, VL103 will be powered on and runs new FW. */ if (fu_vli_device_get_kind(FU_VLI_DEVICE(device)) == FU_VLI_DEVICE_KIND_VL103) { if (!fu_vli_pd_device_write_reg(self, 0x1201, 0xf6, error)) return FALSE; if (!fu_vli_pd_device_write_reg(self, 0x1001, 0xf6, error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } /* sanity check */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in runtime mode, skipping"); return TRUE; } /* chip reset command works only for non-VL103 */ if (!fu_usb_device_control_transfer(FU_USB_DEVICE(FU_USB_DEVICE(device)), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xb0, 0x0000, 0x0000, NULL, 0x0, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL)) { g_debug("ignoring %s", error_local->message); } else { g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to restart device: "); return FALSE; } } /* replug */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } static void fu_vli_pd_device_kind_changed_cb(FuVliDevice *device, GParamSpec *pspec, gpointer user_data) { if (fu_vli_device_get_kind(device) == FU_VLI_DEVICE_KIND_VL103) { /* wait for USB-C timeout */ fu_device_set_remove_delay(FU_DEVICE(device), 10000); } } static void fu_vli_pd_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 72, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 28, "reload"); } static gchar * fu_vli_pd_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_device_get_version_format(device)); } static void fu_vli_pd_device_init(FuVliPdDevice *self) { fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_USB_HUB); fu_device_add_protocol(FU_DEVICE(self), "com.vli.pd"); fu_device_set_summary(FU_DEVICE(self), "USB power distribution device"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); fu_vli_device_set_spi_auto_detect(FU_VLI_DEVICE(self), FALSE); fu_device_register_private_flag(FU_DEVICE(self), FU_VLI_PD_DEVICE_FLAG_HAS_I2C_PS186); fu_device_register_private_flag(FU_DEVICE(self), FU_VLI_PD_DEVICE_FLAG_SKIPS_ROM); /* connect up attach or detach vfuncs when kind is known */ g_signal_connect(FU_VLI_DEVICE(self), "notify::kind", G_CALLBACK(fu_vli_pd_device_kind_changed_cb), NULL); } static void fu_vli_pd_device_class_init(FuVliPdDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); FuVliDeviceClass *vli_device_class = FU_VLI_DEVICE_CLASS(klass); device_class->dump_firmware = fu_vli_pd_device_dump_firmware; device_class->write_firmware = fu_vli_pd_device_write_firmware; device_class->prepare_firmware = fu_vli_pd_device_prepare_firmware; device_class->attach = fu_vli_pd_device_attach; device_class->detach = fu_vli_pd_device_detach; device_class->setup = fu_vli_pd_device_setup; device_class->set_progress = fu_vli_pd_device_set_progress; device_class->convert_version = fu_vli_pd_device_convert_version; vli_device_class->spi_chip_erase = fu_vli_pd_device_spi_chip_erase; vli_device_class->spi_sector_erase = fu_vli_pd_device_spi_sector_erase; vli_device_class->spi_read_data = fu_vli_pd_device_spi_read_data; vli_device_class->spi_read_status = fu_vli_pd_device_spi_read_status; vli_device_class->spi_write_data = fu_vli_pd_device_spi_write_data; vli_device_class->spi_write_enable = fu_vli_pd_device_spi_write_enable; vli_device_class->spi_write_status = fu_vli_pd_device_spi_write_status; } fwupd-2.0.10/plugins/vli/fu-vli-pd-device.h000066400000000000000000000010511501337203100203730ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-vli-device.h" #define FU_TYPE_VLI_PD_DEVICE (fu_vli_pd_device_get_type()) G_DECLARE_FINAL_TYPE(FuVliPdDevice, fu_vli_pd_device, FU, VLI_PD_DEVICE, FuVliDevice) #define FU_VLI_PD_REGISTER_ADDRESS_PROJ_ID_HIGH 0X009C #define FU_VLI_PD_REGISTER_ADDRESS_PROJ_ID_LOW 0X009D #define FU_VLI_PD_REGISTER_ADDRESS_PROJ_LEGACY 0X0018 #define FU_VLI_PD_REGISTER_ADDRESS_GPIO_CONTROL_A 0X0003 fwupd-2.0.10/plugins/vli/fu-vli-pd-firmware.c000066400000000000000000000076201501337203100207530ustar00rootroot00000000000000/* * Copyright 2017 VIA Corporation * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-vli-pd-common.h" #include "fu-vli-pd-firmware.h" #include "fu-vli-struct.h" struct _FuVliPdFirmware { FuFirmwareClass parent_instance; FuVliDeviceKind device_kind; }; G_DEFINE_TYPE(FuVliPdFirmware, fu_vli_pd_firmware, FU_TYPE_FIRMWARE) FuVliDeviceKind fu_vli_pd_firmware_get_kind(FuVliPdFirmware *self) { g_return_val_if_fail(FU_IS_VLI_PD_FIRMWARE(self), 0); return self->device_kind; } static void fu_vli_pd_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuVliPdFirmware *self = FU_VLI_PD_FIRMWARE(firmware); fu_xmlb_builder_insert_kv(bn, "device_kind", fu_vli_device_kind_to_string(self->device_kind)); } static gboolean fu_vli_pd_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuVliPdFirmware *self = FU_VLI_PD_FIRMWARE(firmware); gsize streamsz = 0; guint32 fwver; g_autoptr(GByteArray) st = NULL; /* parse */ st = fu_struct_vli_pd_hdr_parse_stream(stream, VLI_USBHUB_PD_FLASHMAP_ADDR, error); if (st == NULL) { g_prefix_error(error, "failed to read header: "); return FALSE; } if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; /* guess device kind from fwver */ fwver = fu_struct_vli_pd_hdr_get_fwver(st); self->device_kind = fu_vli_pd_common_guess_device_kind(fwver); if (self->device_kind == FU_VLI_DEVICE_KIND_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "version invalid, using 0x%x", fwver); return FALSE; } fu_firmware_set_version_raw(firmware, fwver); /* check size */ if (streamsz != fu_vli_common_device_kind_get_size(self->device_kind)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "size invalid, got 0x%x expected 0x%x", (guint)streamsz, fu_vli_common_device_kind_get_size(self->device_kind)); return FALSE; } /* check CRC */ if ((flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM) == 0) { guint16 crc_actual = 0xFFFF; guint16 crc_file = 0x0; g_autoptr(GInputStream) stream_tmp = NULL; if (streamsz < 2) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "stream was too small"); return FALSE; } if (!fu_input_stream_read_u16(stream, streamsz - 2, &crc_file, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to read file CRC: "); return FALSE; } stream_tmp = fu_partial_input_stream_new(stream, 0, streamsz - 2, error); if (stream_tmp == NULL) return FALSE; if (!fu_input_stream_compute_crc16(stream_tmp, FU_CRC_KIND_B16_USB, &crc_actual, error)) return FALSE; if (crc_actual != crc_file) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "CRC invalid, got 0x%x expected 0x%x", crc_file, crc_actual); return FALSE; } } /* success */ return TRUE; } static gchar * fu_vli_pd_firmware_convert_version(FuFirmware *firmware, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_firmware_get_version_format(firmware)); } static void fu_vli_pd_firmware_init(FuVliPdFirmware *self) { fu_firmware_add_flag(FU_FIRMWARE(self), FU_FIRMWARE_FLAG_HAS_CHECKSUM); fu_firmware_set_version_format(FU_FIRMWARE(self), FWUPD_VERSION_FORMAT_QUAD); } static void fu_vli_pd_firmware_class_init(FuVliPdFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->convert_version = fu_vli_pd_firmware_convert_version; firmware_class->parse = fu_vli_pd_firmware_parse; firmware_class->export = fu_vli_pd_firmware_export; } FuFirmware * fu_vli_pd_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_VLI_PD_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/vli/fu-vli-pd-firmware.h000066400000000000000000000007411501337203100207550ustar00rootroot00000000000000/* * Copyright 2017 VIA Corporation * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-vli-common.h" #define FU_TYPE_VLI_PD_FIRMWARE (fu_vli_pd_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuVliPdFirmware, fu_vli_pd_firmware, FU, VLI_PD_FIRMWARE, FuFirmware) FuFirmware * fu_vli_pd_firmware_new(void); FuVliDeviceKind fu_vli_pd_firmware_get_kind(FuVliPdFirmware *self); fwupd-2.0.10/plugins/vli/fu-vli-pd-parade-device.c000066400000000000000000000543261501337203100216350ustar00rootroot00000000000000/* * Copyright 2015 VIA Corporation * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-vli-pd-device.h" #include "fu-vli-pd-parade-device.h" #include "fu-vli-struct.h" struct _FuVliPdParadeDevice { FuUsbDevice parent_instance; FuVliDeviceKind device_kind; guint8 page2; /* base address */ guint8 page7; /* base address */ }; G_DEFINE_TYPE(FuVliPdParadeDevice, fu_vli_pd_parade_device, FU_TYPE_USB_DEVICE) #define FU_VLI_PD_PARADE_I2C_CMD_WRITE 0xa6 #define FU_VLI_PD_PARADE_I2C_CMD_READ 0xa5 static void fu_vli_pd_parade_device_to_string(FuDevice *device, guint idt, GString *str) { FuVliPdParadeDevice *self = FU_VLI_PD_PARADE_DEVICE(device); fwupd_codec_string_append(str, idt, "DeviceKind", fu_vli_device_kind_to_string(self->device_kind)); fwupd_codec_string_append_hex(str, idt, "Page2", self->page2); fwupd_codec_string_append_hex(str, idt, "Page7", self->page7); } static gboolean fu_vli_pd_parade_device_i2c_read(FuVliPdParadeDevice *self, guint8 page2, guint8 reg_offset, /* customers addr offset */ guint8 *buf, gsize bufsz, GError **error) { guint16 value; /* sanity check */ if (bufsz > 0x40) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "request too large"); return FALSE; } /* VL103 FW only Use bits[7:1], so divide by 2 */ value = ((guint16)reg_offset << 8) | (page2 >> 1); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, FU_VLI_PD_PARADE_I2C_CMD_READ, value, 0x0, buf, bufsz, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to read 0x%x:0x%x: ", page2, reg_offset); return FALSE; } return TRUE; } static gboolean fu_vli_pd_parade_device_i2c_write(FuVliPdParadeDevice *self, guint8 page2, guint8 reg_offset, /* customers addr offset */ guint8 val, /* only one byte supported */ GError **error) { guint16 value; guint16 index; guint8 buf[2] = {0x0}; /* apparently unused... */ /* VL103 FW only Use bits[7:1], so divide by 2 */ value = ((guint16)reg_offset << 8) | (page2 >> 1); index = (guint16)val << 8; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, FU_VLI_PD_PARADE_I2C_CMD_WRITE, value, index, buf, 0x0, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to write 0x%x:0x%x: ", page2, reg_offset); return FALSE; } return TRUE; } static gboolean fu_vli_pd_parade_device_start_mcu(FuVliPdParadeDevice *self, GError **error) { if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0xBC, 0x00, error)) { g_prefix_error(error, "failed to start MCU: "); return FALSE; } return TRUE; } static gboolean fu_vli_pd_parade_device_stop_mcu(FuVliPdParadeDevice *self, GError **error) { if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0xBC, 0xC0, error)) { g_prefix_error(error, "failed to stop MCU: "); return FALSE; } if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0xBC, 0x40, error)) { g_prefix_error(error, "failed to stop MCU 2nd: "); return FALSE; } return TRUE; } static gboolean fu_vli_pd_parade_device_set_offset(FuVliPdParadeDevice *self, guint16 addr, GError **error) { if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0x8E, addr >> 8, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0x8F, addr & 0xff, error)) return FALSE; return TRUE; } static gboolean fu_vli_pd_parade_device_read_fw_ver(FuVliPdParadeDevice *self, GError **error) { guint8 buf[0x20] = {0x0}; g_autofree gchar *version_str = NULL; /* stop MCU */ if (!fu_vli_pd_parade_device_stop_mcu(self, error)) return FALSE; if (!fu_vli_pd_parade_device_set_offset(self, 0x0, error)) return FALSE; fu_device_sleep(FU_DEVICE(self), 10); /* ms */ if (!fu_vli_pd_parade_device_i2c_read(self, self->page7, 0x02, buf, 0x1, error)) return FALSE; if (buf[0] != 0x01 && buf[0] != 0x02) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported on this device: buffer was 0x%02x", buf[0]); return FALSE; } g_debug("getting FW%X version", buf[0]); if (!fu_vli_pd_parade_device_set_offset(self, 0x5000 | buf[0], error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_read(self, self->page7, 0x00, buf, sizeof(buf), error)) return FALSE; /* start MCU */ if (!fu_vli_pd_parade_device_start_mcu(self, error)) return FALSE; /* format version triplet */ version_str = g_strdup_printf("%u.%u.%u", buf[0], buf[1], buf[2]); fu_device_set_version(FU_DEVICE(self), version_str); return TRUE; } static gboolean fu_vli_pd_parade_device_set_wp(FuVliPdParadeDevice *self, gboolean val, GError **error) { return fu_vli_pd_parade_device_i2c_write(self, self->page2, 0xB3, val ? 0x10 : 0x00, error); } static gboolean fu_vli_pd_parade_device_write_enable(FuVliPdParadeDevice *self, GError **error) { /* Set_WP_High, SPI_WEN_06, Len_00, Trigger_Write, Set_WP_Low */ if (!fu_vli_pd_parade_device_set_wp(self, TRUE, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0x90, 0x06, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0x92, 0x00, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0x93, 0x05, error)) return FALSE; if (!fu_vli_pd_parade_device_set_wp(self, FALSE, error)) return FALSE; return TRUE; } static gboolean fu_vli_pd_parade_device_write_disable(FuVliPdParadeDevice *self, GError **error) { if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0xDA, 0x00, error)) return FALSE; return TRUE; } static gboolean fu_vli_pd_parade_device_write_status(FuVliPdParadeDevice *self, guint8 target_status, GError **error) { /* Set_WP_High, SPI_WSTS_01, Target_Status, Len_01, Trigger_Write, Set_WP_Low */ if (!fu_vli_pd_parade_device_set_wp(self, TRUE, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0x90, 0x01, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0x90, target_status, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0x92, 0x01, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0x93, 0x05, error)) return FALSE; if (!fu_vli_pd_parade_device_set_wp(self, FALSE, error)) return FALSE; return TRUE; } static gboolean fu_vli_pd_parade_device_wait_ready(FuVliPdParadeDevice *self, GError **error) { gboolean ret = FALSE; guint limit = 100; guint8 buf = 0x0; /* wait for SPI ROM */ for (guint wait_cnt1 = 0; wait_cnt1 < limit; wait_cnt1++) { buf = 0xFF; if (!fu_vli_pd_parade_device_i2c_read(self, self->page2, 0x9E, &buf, sizeof(buf), error)) return FALSE; /* busy status: * bit[1,0]:Byte_Program * bit[3,2]:Sector Erase * bit[5,4]:Chip Erase */ if ((buf & 0x0C) == 0) { ret = TRUE; break; } } if (!ret) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to wait for SPI not BUSY"); return FALSE; } /* wait for SPI ROM status clear */ ret = FALSE; for (guint wait_cnt1 = 0; wait_cnt1 < limit; wait_cnt1++) { gboolean ret2 = FALSE; /* SPI_RSTS_05, Len_01, Trigger_Read */ if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0x90, 0x05, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0x92, 0x00, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0x93, 0x01, error)) return FALSE; /* wait for cmd done */ for (guint wait_cnt2 = 0; wait_cnt2 < limit; wait_cnt2++) { buf = 0xFF; if (!fu_vli_pd_parade_device_i2c_read(self, self->page2, 0x93, &buf, sizeof(buf), error)) return FALSE; if ((buf & 0x01) == 0) { ret2 = TRUE; break; } } if (!ret2) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to wait for SPI CMD done"); return FALSE; } /* Wait_SPI_STS_00 */ buf = 0xFF; if (!fu_vli_pd_parade_device_i2c_read(self, self->page2, 0x91, &buf, sizeof(buf), error)) return FALSE; if ((buf & 0x01) == 0) { ret = TRUE; break; } } if (!ret) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to wait for SPI status clear"); return FALSE; } /* success */ return TRUE; } static gboolean fu_vli_pd_parade_device_sector_erase(FuVliPdParadeDevice *self, guint16 addr, GError **error) { /* SPI_SE_20, SPI_Adr_H, SPI_Adr_M, SPI_Adr_L, Len_03, Trigger_Write */ if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0x90, 0x20, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0x90, addr >> 8, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0x90, addr & 0xff, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0x90, 0x00, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0x92, 0x03, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0x93, 0x05, error)) return FALSE; return TRUE; } static gboolean fu_vli_pd_parade_device_enable_mapping(FuVliPdParadeDevice *self, GError **error) { if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0xDA, 0xAA, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0xDA, 0x55, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0xDA, 0x50, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0xDA, 0x41, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0xDA, 0x52, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0xDA, 0x44, error)) return FALSE; return TRUE; } static gboolean fu_vli_pd_parade_device_block_erase(FuVliPdParadeDevice *self, guint8 block_idx, GError **error) { /* erase */ for (guint idx = 0x00; idx < 0x100; idx += 0x10) { if (!fu_vli_pd_parade_device_write_enable(self, error)) return FALSE; if (!fu_vli_pd_parade_device_set_wp(self, TRUE, error)) return FALSE; if (!fu_vli_pd_parade_device_sector_erase(self, ((guint16)block_idx << 8) | idx, error)) return FALSE; if (!fu_vli_pd_parade_device_wait_ready(self, error)) return FALSE; if (!fu_vli_pd_parade_device_set_wp(self, FALSE, error)) return FALSE; } /* verify */ for (guint idx = 0; idx < 0x100; idx += 0x10) { guint8 buf[0x20] = {0xff}; if (!fu_vli_pd_parade_device_set_offset(self, (block_idx << 8) | idx, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_read(self, self->page7, 0, buf, 0x20, error)) return FALSE; for (guint idx2 = 0; idx2 < 0x20; idx2++) { if (buf[idx2] != 0xFF) { guint32 addr = (block_idx << 16) + (idx << 8); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Erase failed @0x%x", addr); return FALSE; } } } /* success */ return TRUE; } static gboolean fu_vli_pd_parade_device_block_write(FuVliPdParadeDevice *self, guint8 block_idx, const guint8 *txbuf, GError **error) { for (guint idx = 0; idx < 0x100; idx++) { if (!fu_vli_pd_parade_device_set_offset(self, (block_idx << 8) | idx, error)) return FALSE; for (guint idx2 = 0; idx2 < 0x100; idx2++) { guint32 buf_offset = (idx << 8) + idx2; if (!fu_vli_pd_parade_device_i2c_write(self, self->page7, (guint8)idx2, txbuf[buf_offset], error)) return FALSE; } } /* success */ return TRUE; } static gboolean fu_vli_pd_parade_device_block_read(FuVliPdParadeDevice *self, guint8 block_idx, guint8 *buf, gsize bufsz, GError **error) { for (guint idx = 0; idx < 0x100; idx++) { if (!fu_vli_pd_parade_device_set_offset(self, (block_idx << 8) | idx, error)) return FALSE; for (guint idx2 = 0; idx2 < 0x100; idx2 += 0x20) { guint buf_offset = (idx << 8) + idx2; if (!fu_vli_pd_parade_device_i2c_read(self, self->page7, idx2, buf + buf_offset, 0x20, error)) return FALSE; } } return TRUE; } static gboolean fu_vli_pd_parade_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuVliPdParadeDevice *self = FU_VLI_PD_PARADE_DEVICE(device); FuVliPdDevice *parent = FU_VLI_PD_DEVICE(fu_device_get_parent(device)); guint8 buf[0x20]; guint block_idx_tmp; g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GByteArray) buf_verify = NULL; g_autoptr(GBytes) fw = NULL; g_autoptr(GBytes) fw_verify = NULL; g_autoptr(FuChunk) chk0 = NULL; g_autoptr(FuChunkArray) blocks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 19, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 45, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 36, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, NULL); /* simple image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* open device */ locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; /* stop MPU and reset SPI */ if (!fu_vli_pd_parade_device_stop_mcu(self, error)) return FALSE; /* 64K block erase */ if (!fu_vli_pd_parade_device_write_enable(self, error)) return FALSE; if (!fu_vli_pd_parade_device_write_status(self, 0x00, error)) return FALSE; if (!fu_vli_pd_parade_device_wait_ready(self, error)) return FALSE; blocks = fu_chunk_array_new_from_bytes(fw, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, 0x10000); for (guint i = 1; i < fu_chunk_array_length(blocks); i++) { g_autoptr(FuChunk) chk = fu_chunk_array_index(blocks, i, error); if (chk == NULL) return FALSE; if (!fu_vli_pd_parade_device_block_erase(self, fu_chunk_get_idx(chk), error)) return FALSE; fu_progress_set_percentage_full(fu_progress_get_child(progress), i + 1, fu_chunk_array_length(blocks)); } fu_progress_step_done(progress); /* load F/W to SPI ROM */ if (!fu_vli_pd_parade_device_enable_mapping(self, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0x82, 0x20, error)) return FALSE; /* Reset_CLT2SPI_Interface */ fu_device_sleep(device, 100); /* ms */ if (!fu_vli_pd_parade_device_i2c_write(self, self->page2, 0x82, 0x00, error)) return FALSE; /* write blocks */ for (guint i = 1; i < fu_chunk_array_length(blocks); i++) { g_autoptr(FuChunk) chk = fu_chunk_array_index(blocks, i, error); if (chk == NULL) return FALSE; if (!fu_vli_pd_parade_device_block_write(self, fu_chunk_get_idx(chk), fu_chunk_get_data(chk), error)) return FALSE; fu_progress_set_percentage_full(fu_progress_get_child(progress), i + 1, fu_chunk_array_length(blocks)); } if (!fu_vli_pd_parade_device_write_disable(self, error)) return FALSE; fu_progress_step_done(progress); /* add the new boot config into the verify buffer */ buf_verify = g_byte_array_sized_new(g_bytes_get_size(fw)); chk0 = fu_chunk_array_index(blocks, 0, error); if (chk0 == NULL) return FALSE; g_byte_array_append(buf_verify, fu_chunk_get_data(chk0), fu_chunk_get_data_sz(chk0)); /* verify SPI ROM, ignoring the boot config */ for (guint i = 1; i < fu_chunk_array_length(blocks); i++) { g_autofree guint8 *vbuf = NULL; g_autoptr(FuChunk) chk = NULL; chk = fu_chunk_array_index(blocks, i, error); if (chk == NULL) return FALSE; vbuf = g_malloc0(fu_chunk_get_data_sz(chk)); if (!fu_vli_pd_parade_device_block_read(self, fu_chunk_get_idx(chk), vbuf, fu_chunk_get_data_sz(chk), error)) return FALSE; g_byte_array_append(buf_verify, vbuf, fu_chunk_get_data_sz(chk)); fu_progress_set_percentage_full(fu_progress_get_child(progress), i + 1, fu_chunk_array_length(blocks)); } fw_verify = g_bytes_new(buf_verify->data, buf_verify->len); if (!fu_bytes_compare(fw, fw_verify, error)) return FALSE; fu_progress_step_done(progress); /* save boot config into Block_0 */ if (!fu_vli_pd_parade_device_write_enable(self, error)) return FALSE; if (!fu_vli_pd_parade_device_set_wp(self, TRUE, error)) return FALSE; if (!fu_vli_pd_parade_device_sector_erase(self, 0x0, error)) return FALSE; if (!fu_vli_pd_parade_device_wait_ready(self, error)) return FALSE; if (!fu_vli_pd_parade_device_set_wp(self, FALSE, error)) return FALSE; /* Page_HW_Write_Disable */ if (!fu_vli_pd_parade_device_enable_mapping(self, error)) return FALSE; block_idx_tmp = 1; if (!fu_vli_pd_parade_device_set_offset(self, 0x0, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page7, 0x00, 0x55, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page7, 0x01, 0xAA, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page7, 0x02, (guint8)block_idx_tmp, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_write(self, self->page7, 0x03, (guint8)(0x01 - block_idx_tmp), error)) return FALSE; if (!fu_vli_pd_parade_device_write_disable(self, error)) return FALSE; /* check boot config data */ if (!fu_vli_pd_parade_device_set_offset(self, 0x0, error)) return FALSE; if (!fu_vli_pd_parade_device_i2c_read(self, self->page7, 0, buf, sizeof(buf), error)) return FALSE; if (buf[0] != 0x55 || buf[1] != 0xAA || buf[2] != block_idx_tmp || buf[3] != 0x01 - block_idx_tmp) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "boot config data error"); return FALSE; } /* enable write protection */ if (!fu_vli_pd_parade_device_write_enable(self, error)) return FALSE; if (!fu_vli_pd_parade_device_write_status(self, 0x8C, error)) return FALSE; if (!fu_vli_pd_parade_device_wait_ready(self, error)) return FALSE; if (!fu_vli_pd_parade_device_write_disable(self, error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static GBytes * fu_vli_pd_parade_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuVliPdDevice *parent = FU_VLI_PD_DEVICE(fu_device_get_parent(device)); FuVliPdParadeDevice *self = FU_VLI_PD_PARADE_DEVICE(device); g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GByteArray) fw = g_byte_array_new(); g_autoptr(GPtrArray) blocks = NULL; /* open device */ locker = fu_device_locker_new(parent, error); if (locker == NULL) return NULL; /* stop MPU and reset SPI */ if (!fu_vli_pd_parade_device_stop_mcu(self, error)) return NULL; /* read */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_VERIFY); fu_byte_array_set_size(fw, fu_device_get_firmware_size_max(device), 0x00); blocks = fu_chunk_array_mutable_new(fw->data, fw->len, 0x0, 0x0, 0x10000); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, blocks->len); for (guint i = 0; i < blocks->len; i++) { FuChunk *chk = g_ptr_array_index(blocks, i); if (!fu_vli_pd_parade_device_block_read(self, fu_chunk_get_idx(chk), fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), error)) return NULL; fu_progress_step_done(progress); } return g_bytes_new(fw->data, fw->len); } static gboolean fu_vli_pd_parade_device_probe(FuDevice *device, GError **error) { FuVliPdParadeDevice *self = FU_VLI_PD_PARADE_DEVICE(device); /* get version */ if (!fu_vli_pd_parade_device_read_fw_ver(self, error)) return FALSE; /* use header to populate device info */ fu_device_add_instance_str(device, "I2C", fu_vli_device_kind_to_string(self->device_kind)); return fu_device_build_instance_id(device, error, "USB", "VID", "PID", "I2C", NULL); } static void fu_vli_pd_parade_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 98, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static void fu_vli_pd_parade_device_init(FuVliPdParadeDevice *self) { self->device_kind = FU_VLI_DEVICE_KIND_PS186; self->page2 = 0x14; self->page7 = 0x1E; fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_VIDEO_DISPLAY); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_add_protocol(FU_DEVICE(self), "com.vli.i2c"); fu_device_set_install_duration(FU_DEVICE(self), 15); /* seconds */ fu_device_set_logical_id(FU_DEVICE(self), "PS186"); fu_device_set_summary(FU_DEVICE(self), "DisplayPort 1.4a to HDMI 2.0b protocol converter"); fu_device_set_firmware_size(FU_DEVICE(self), 0x40000); } static void fu_vli_pd_parade_device_class_init(FuVliPdParadeDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_vli_pd_parade_device_to_string; device_class->probe = fu_vli_pd_parade_device_probe; device_class->dump_firmware = fu_vli_pd_parade_device_dump_firmware; device_class->write_firmware = fu_vli_pd_parade_device_write_firmware; device_class->set_progress = fu_vli_pd_parade_device_set_progress; } FuDevice * fu_vli_pd_parade_device_new(FuVliDevice *parent) { FuVliPdParadeDevice *self = g_object_new(FU_TYPE_VLI_PD_PARADE_DEVICE, "parent", parent, NULL); return FU_DEVICE(self); } fwupd-2.0.10/plugins/vli/fu-vli-pd-parade-device.h000066400000000000000000000007401501337203100216310ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-vli-device.h" #include "fu-vli-pd-common.h" #define FU_TYPE_VLI_PD_PARADE_DEVICE (fu_vli_pd_parade_device_get_type()) G_DECLARE_FINAL_TYPE(FuVliPdParadeDevice, fu_vli_pd_parade_device, FU, VLI_PD_PARADE_DEVICE, FuUsbDevice) FuDevice * fu_vli_pd_parade_device_new(FuVliDevice *parent); fwupd-2.0.10/plugins/vli/fu-vli-plugin.c000066400000000000000000000031651501337203100200340ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-vli-pd-device.h" #include "fu-vli-pd-firmware.h" #include "fu-vli-pd-parade-device.h" #include "fu-vli-plugin.h" #include "fu-vli-usbhub-device.h" #include "fu-vli-usbhub-firmware.h" #include "fu-vli-usbhub-msp430-device.h" #include "fu-vli-usbhub-pd-device.h" #include "fu-vli-usbhub-rtd21xx-device.h" struct _FuVliPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuVliPlugin, fu_vli_plugin, FU_TYPE_PLUGIN) static void fu_vli_plugin_init(FuVliPlugin *self) { } static void fu_vli_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "VliDeviceKind"); fu_context_add_quirk_key(ctx, "VliSpiAutoDetect"); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_VLI_USBHUB_FIRMWARE); fu_plugin_add_firmware_gtype(plugin, NULL, FU_TYPE_VLI_PD_FIRMWARE); fu_plugin_add_device_gtype(plugin, FU_TYPE_VLI_USBHUB_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_VLI_PD_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_VLI_PD_PARADE_DEVICE); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_VLI_USBHUB_MSP430_DEVICE); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_VLI_USBHUB_PD_DEVICE); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_VLI_USBHUB_RTD21XX_DEVICE); /* coverage */ } static void fu_vli_plugin_class_init(FuVliPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_vli_plugin_constructed; } fwupd-2.0.10/plugins/vli/fu-vli-plugin.h000066400000000000000000000003431501337203100200340ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuVliPlugin, fu_vli_plugin, FU, VLI_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/vli/fu-vli-usbhub-common.h000066400000000000000000000026051501337203100213170ustar00rootroot00000000000000/* * Copyright 2017 VIA Corporation * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-vli-common.h" #define FU_VLI_USBHUB_HEADER_STRAPPING1_SELFW1 (1 << 1) #define FU_VLI_USBHUB_HEADER_STRAPPING1_76PIN (1 << 2) #define FU_VLI_USBHUB_HEADER_STRAPPING1_B3UP (1 << 3) #define FU_VLI_USBHUB_HEADER_STRAPPING1_LPC (1 << 4) #define FU_VLI_USBHUB_HEADER_STRAPPING1_U1U2 (1 << 5) #define FU_VLI_USBHUB_HEADER_STRAPPING1_BC (1 << 6) #define FU_VLI_USBHUB_HEADER_STRAPPING1_Q4S (1 << 7) #define FU_VLI_USBHUB_HEADER_STRAPPING2_IDXEN (1 << 0) #define FU_VLI_USBHUB_HEADER_STRAPPING2_FWRTY (1 << 1) #define FU_VLI_USBHUB_HEADER_STRAPPING2_SELFW2 (1 << 7) #define VLI_USBHUB_FLASHMAP_ADDR_TO_IDX(addr) (addr / 0x20) #define VLI_USBHUB_FLASHMAP_IDX_TO_ADDR(addr) (addr * 0x20) #define VLI_USBHUB_FLASHMAP_IDX_HD1 0x00 /* factory firmware */ #define VLI_USBHUB_FLASHMAP_IDX_HD2 0x80 /* update firmware */ #define VLI_USBHUB_FLASHMAP_IDX_INVALID 0xff #define VLI_USBHUB_FLASHMAP_ADDR_HD1 0x0 #define VLI_USBHUB_FLASHMAP_ADDR_HD1_BACKUP 0x1800 #define VLI_USBHUB_FLASHMAP_ADDR_HD2 0x1000 #define VLI_USBHUB_FLASHMAP_ADDR_FW 0x2000 #define VLI_USBHUB_FLASHMAP_ADDR_PD_LEGACY 0x10000 #define VLI_USBHUB_FLASHMAP_ADDR_PD 0x20000 #define VLI_USBHUB_FLASHMAP_ADDR_PD_BACKUP 0x30000 fwupd-2.0.10/plugins/vli/fu-vli-usbhub-device.c000066400000000000000000001333331501337203100212640ustar00rootroot00000000000000/* * Copyright 2017 VIA Corporation * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-vli-struct.h" #include "fu-vli-usbhub-common.h" #include "fu-vli-usbhub-device.h" #include "fu-vli-usbhub-firmware.h" #include "fu-vli-usbhub-msp430-device.h" #include "fu-vli-usbhub-pd-device.h" #include "fu-vli-usbhub-rtd21xx-device.h" struct _FuVliUsbhubDevice { FuVliDevice parent_instance; gboolean disable_powersave; guint8 update_protocol; GByteArray *st_hd1; /* factory */ GByteArray *st_hd2; /* update */ }; G_DEFINE_TYPE(FuVliUsbhubDevice, fu_vli_usbhub_device, FU_TYPE_VLI_DEVICE) #define FU_VLI_USBHUB_DEVICE_FLAG_ATTACH_WITH_GPIOB "attach-with-gpiob" #define FU_VLI_USBHUB_DEVICE_FLAG_USB2 "usb2" #define FU_VLI_USBHUB_DEVICE_FLAG_USB3 "usb3" #define FU_VLI_USBHUB_DEVICE_FLAG_UNLOCK_LEGACY813 "unlock-legacy813" #define FU_VLI_USBHUB_DEVICE_FLAG_HAS_SHARED_SPI_PD "has-shared-spi-pd" #define FU_VLI_USBHUB_DEVICE_FLAG_HAS_MSP430 "has-msp430" #define FU_VLI_USBHUB_DEVICE_FLAG_HAS_RTD21XX "has-rtd21xx" #define FU_VLI_USBHUB_DEVICE_FLAG_ATTACH_WITH_USB_CABLE "attach-with-usb" #define FU_VLI_USBHUB_DEVICE_FLAG_ATTACH_WITH_POWER_CORD "attach-with-power" static void fu_vli_usbhub_device_to_string(FuDevice *device, guint idt, GString *str) { FuVliUsbhubDevice *self = FU_VLI_USBHUB_DEVICE(device); fwupd_codec_string_append_bool(str, idt, "DisablePowersave", self->disable_powersave); fwupd_codec_string_append_hex(str, idt, "UpdateProtocol", self->update_protocol); if (self->update_protocol >= 0x2) { g_autofree gchar *st_hd1str = fu_struct_vli_usbhub_hdr_to_string(self->st_hd1); fwupd_codec_string_append(str, idt, "H1Hdr@0x0", st_hd1str); if (fu_struct_vli_usbhub_hdr_get_dev_id(self->st_hd2) != 0xFFFF) { g_autofree gchar *st_hd2str = fu_struct_vli_usbhub_hdr_to_string(self->st_hd2); fwupd_codec_string_append(str, idt, "H2Hdr@0x1000", st_hd2str); } } } static guint8 fu_vli_usbhub_device_header_crc8(GByteArray *hdr) { return fu_crc8(FU_CRC_KIND_B8_STANDARD, hdr->data, hdr->len - 1); } static gboolean fu_vli_usbhub_device_vdr_unlock_813(FuVliUsbhubDevice *self, GError **error) { if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0x85, 0x8786, 0x8988, NULL, 0x0, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to UnLock_VL813: "); return FALSE; } return TRUE; } static gboolean fu_vli_usbhub_device_read_reg(FuVliUsbhubDevice *self, guint16 addr, guint8 *buf, GError **error) { if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, addr >> 8, addr & 0xff, 0x0, buf, 0x1, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to read register 0x%x: ", addr); return FALSE; } return TRUE; } static gboolean fu_vli_usbhub_device_write_reg(FuVliUsbhubDevice *self, guint16 addr, guint8 value, GError **error) { if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, addr >> 8, addr & 0xff, (guint16)value, NULL, 0x0, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to write register 0x%x: ", addr); return FALSE; } return TRUE; } static gboolean fu_vli_usbhub_device_spi_read_status(FuVliDevice *self, guint8 *status, GError **error) { guint8 spi_cmd = 0x0; if (!fu_cfi_device_get_cmd(fu_vli_device_get_cfi_device(self), FU_CFI_DEVICE_CMD_READ_STATUS, &spi_cmd, error)) return FALSE; return fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xc1, spi_cmd, 0x0000, status, 0x1, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error); } static gboolean fu_vli_usbhub_device_spi_read_data(FuVliDevice *self, guint32 addr, guint8 *buf, gsize bufsz, GError **error) { guint8 spi_cmd = 0x0; guint16 value; guint16 index; if (!fu_cfi_device_get_cmd(fu_vli_device_get_cfi_device(self), FU_CFI_DEVICE_CMD_READ_DATA, &spi_cmd, error)) return FALSE; value = ((addr >> 8) & 0xff00) | spi_cmd; index = ((addr << 8) & 0xff00) | ((addr >> 8) & 0x00ff); return fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xc4, value, index, buf, bufsz, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error); } static gboolean fu_vli_usbhub_device_spi_write_status(FuVliDevice *self, guint8 status, GError **error) { guint8 spi_cmd = 0x0; if (!fu_cfi_device_get_cmd(fu_vli_device_get_cfi_device(self), FU_CFI_DEVICE_CMD_WRITE_STATUS, &spi_cmd, error)) return FALSE; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xd1, spi_cmd, 0x0000, &status, 0x1, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error)) { return FALSE; } /* Fix_For_GD_&_EN_SPI_Flash */ fu_device_sleep(FU_DEVICE(self), 100); /* ms */ return TRUE; } static gboolean fu_vli_usbhub_device_spi_write_enable(FuVliDevice *self, GError **error) { guint8 spi_cmd = 0x0; if (!fu_cfi_device_get_cmd(fu_vli_device_get_cfi_device(self), FU_CFI_DEVICE_CMD_WRITE_EN, &spi_cmd, error)) return FALSE; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xd1, spi_cmd, 0x0000, NULL, 0x0, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to write enable SPI: "); return FALSE; } return TRUE; } static gboolean fu_vli_usbhub_device_spi_chip_erase(FuVliDevice *self, GError **error) { guint8 spi_cmd = 0x0; if (!fu_cfi_device_get_cmd(fu_vli_device_get_cfi_device(self), FU_CFI_DEVICE_CMD_CHIP_ERASE, &spi_cmd, error)) return FALSE; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xd1, spi_cmd, 0x0000, NULL, 0x0, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error)) { return FALSE; } return TRUE; } static gboolean fu_vli_usbhub_device_spi_sector_erase(FuVliDevice *self, guint32 addr, GError **error) { guint8 spi_cmd = 0x0; guint16 value; guint16 index; if (!fu_cfi_device_get_cmd(fu_vli_device_get_cfi_device(self), FU_CFI_DEVICE_CMD_SECTOR_ERASE, &spi_cmd, error)) return FALSE; value = ((addr >> 8) & 0xff00) | spi_cmd; index = ((addr << 8) & 0xff00) | ((addr >> 8) & 0x00ff); return fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xd4, value, index, NULL, 0x0, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error); } static gboolean fu_vli_usbhub_device_spi_write_data(FuVliDevice *self, guint32 addr, const guint8 *buf, gsize bufsz, GError **error) { guint8 spi_cmd = 0x0; guint16 value; guint16 index; g_autofree guint8 *buf_mut = NULL; if (!fu_cfi_device_get_cmd(fu_vli_device_get_cfi_device(self), FU_CFI_DEVICE_CMD_PAGE_PROG, &spi_cmd, error)) return FALSE; value = ((addr >> 8) & 0xff00) | spi_cmd; index = ((addr << 8) & 0xff00) | ((addr >> 8) & 0x00ff); buf_mut = fu_memdup_safe(buf, bufsz, error); if (buf_mut == NULL) return FALSE; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xd4, value, index, buf_mut, bufsz, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error)) { return FALSE; } /* patch for PUYA flash write data command */ fu_device_sleep(FU_DEVICE(self), 1); /* ms */ return TRUE; } #define VL817_ADDR_GPIO_OUTPUT_ENABLE 0xF6A0 /* 0=input, 1=output */ #define VL817_ADDR_GPIO_SET_OUTPUT_DATA 0xF6A1 /* 0=low, 1=high */ #define VL817_ADDR_GPIO_GET_INPUT_DATA 0xF6A2 /* 0=low, 1=high */ static gboolean fu_vli_usbhub_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuDevice *proxy = fu_device_get_proxy_with_fallback(device); g_autoptr(GError) error_local = NULL; /* the user has to do something */ if (fu_device_has_private_flag(device, FU_VLI_USBHUB_DEVICE_FLAG_ATTACH_WITH_USB_CABLE)) { g_autoptr(FwupdRequest) request = fwupd_request_new(); fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_IMMEDIATE); fwupd_request_set_id(request, FWUPD_REQUEST_ID_REMOVE_REPLUG); fwupd_request_add_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); if (!fu_device_emit_request(device, request, progress, error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } if (fu_device_has_private_flag(device, FU_VLI_USBHUB_DEVICE_FLAG_ATTACH_WITH_POWER_CORD)) { g_autoptr(FwupdRequest) request = fwupd_request_new(); fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_IMMEDIATE); fwupd_request_set_id(request, FWUPD_REQUEST_ID_REPLUG_POWER); fwupd_request_add_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); if (!fu_device_emit_request(device, request, progress, error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } /* some hardware has to toggle a GPIO to reset the entire PCB */ if (fu_vli_device_get_kind(FU_VLI_DEVICE(proxy)) == FU_VLI_DEVICE_KIND_VL817 && fu_device_has_private_flag(device, FU_VLI_USBHUB_DEVICE_FLAG_ATTACH_WITH_GPIOB)) { guint8 tmp = 0x0; /* set GPIOB output enable */ g_info("using GPIO reset for %s", fu_device_get_id(device)); if (!fu_vli_usbhub_device_read_reg(FU_VLI_USBHUB_DEVICE(proxy), VL817_ADDR_GPIO_OUTPUT_ENABLE, &tmp, error)) return FALSE; if (!fu_vli_usbhub_device_write_reg(FU_VLI_USBHUB_DEVICE(proxy), VL817_ADDR_GPIO_OUTPUT_ENABLE, tmp | (1 << 1), error)) return FALSE; /* toggle GPIOB to trigger reset */ if (!fu_vli_usbhub_device_read_reg(FU_VLI_USBHUB_DEVICE(proxy), VL817_ADDR_GPIO_SET_OUTPUT_DATA, &tmp, error)) return FALSE; if (!fu_vli_usbhub_device_write_reg(FU_VLI_USBHUB_DEVICE(proxy), VL817_ADDR_GPIO_SET_OUTPUT_DATA, tmp ^ (1 << 1), error)) return FALSE; } else { /* replug, and ignore the device going away */ if (!fu_usb_device_control_transfer(FU_USB_DEVICE(FU_USB_DEVICE(proxy)), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, 0xf6, 0x0040, 0x0002, NULL, 0x0, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_READ) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL)) { g_debug("ignoring %s", error_local->message); } else { g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to restart device: "); return FALSE; } } } /* success */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); return TRUE; } /* disable hub sleep states -- not really required by 815~ hubs */ static gboolean fu_vli_usbhub_device_disable_u1u2(FuVliUsbhubDevice *self, GError **error) { guint8 buf = 0x0; /* clear Reg[0xF8A2] bit_3 & bit_7 -- also * clear Total Switch / Flag To Disable FW Auto-Reload Function */ if (!fu_vli_usbhub_device_read_reg(self, 0xf8a2, &buf, error)) return FALSE; buf &= 0x77; if (!fu_vli_usbhub_device_write_reg(self, 0xf8a2, buf, error)) return FALSE; /* clear Reg[0xF832] bit_0 & bit_1 */ if (!fu_vli_usbhub_device_read_reg(self, 0xf832, &buf, error)) return FALSE; buf &= 0xfc; if (!fu_vli_usbhub_device_write_reg(self, 0xf832, buf, error)) return FALSE; /* clear Reg[0xF920] bit_1 & bit_2 */ if (!fu_vli_usbhub_device_read_reg(self, 0xf920, &buf, error)) return FALSE; buf &= 0xf9; if (!fu_vli_usbhub_device_write_reg(self, 0xf920, buf, error)) return FALSE; /* set Reg[0xF836] bit_3 */ if (!fu_vli_usbhub_device_read_reg(self, 0xf836, &buf, error)) return FALSE; buf |= 0x08; if (!fu_vli_usbhub_device_write_reg(self, 0xf836, buf, error)) return FALSE; return TRUE; } static gboolean fu_vli_usbhub_device_guess_kind(FuVliUsbhubDevice *self, GError **error) { guint8 b811P812 = 0x0; guint8 pkgtype = 0x0; guint8 chipid1 = 0x0; guint8 chipid2 = 0x0; guint8 chipid12 = 0x0; guint8 chipid22 = 0x0; guint8 chipver = 0x0; guint8 chipver2 = 0x0; gint tPid = fu_device_get_pid(FU_DEVICE(self)) & 0x0fff; if (!fu_vli_usbhub_device_read_reg(self, 0xf88c, &chipver, error)) { g_prefix_error(error, "Read_ChipVer failed: "); return FALSE; } g_debug("chipver = 0x%02x", chipver); if (!fu_vli_usbhub_device_read_reg(self, 0xf63f, &chipver2, error)) { g_prefix_error(error, "Read_ChipVer2 failed: "); return FALSE; } g_debug("chipver2 = 0x%02x", chipver2); if (!fu_vli_usbhub_device_read_reg(self, 0xf800, &b811P812, error)) { g_prefix_error(error, "Read_811P812 failed: "); return FALSE; } g_debug("b811P812 = 0x%02x", b811P812); if (!fu_vli_usbhub_device_read_reg(self, 0xf88e, &chipid1, error)) { g_prefix_error(error, "Read_ChipID1 failed: "); return FALSE; } g_debug("chipid1 = 0x%02x", chipid1); if (!fu_vli_usbhub_device_read_reg(self, 0xf88f, &chipid2, error)) { g_prefix_error(error, "Read_ChipID2 failed: "); return FALSE; } g_debug("chipid2 = 0x%02x", chipid2); if (!fu_vli_usbhub_device_read_reg(self, 0xf64e, &chipid12, error)) { g_prefix_error(error, "Read_ChipID12 failed: "); return FALSE; } g_debug("chipid12 = 0x%02x", chipid12); if (!fu_vli_usbhub_device_read_reg(self, 0xf64f, &chipid22, error)) { g_prefix_error(error, "Read_ChipID22 failed: "); return FALSE; } g_debug("chipid22 = 0x%02x", chipid22); if (!fu_vli_usbhub_device_read_reg(self, 0xf651, &pkgtype, error)) { g_prefix_error(error, "Read_820Q7Q8 failed: "); return FALSE; } g_debug("pkgtype = 0x%02x", pkgtype); if (chipid2 == 0x35 && chipid1 == 0x07) { fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL210); } else if (chipid2 == 0x35 && chipid1 == 0x18) { if (chipver == 0xF0) { /* packet type determines device kind for VL819-VL822, minus VL820 */ switch ((pkgtype >> 1) & 0x07) { /* VL822Q7 */ case 0x00: fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL822Q7); break; /* VL822Q5 */ case 0x01: fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL822Q5); break; /* VL822Q8 */ case 0x02: fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL822Q8); break; /* VL821Q7 */ case 0x04: fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL821Q7); break; /* VL819Q7 */ case 0x05: fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL819Q7); break; /* VL821Q8 */ case 0x06: fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL821Q8); break; /* VL819Q8 */ case 0x07: fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL819Q8); break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "packet Type match failed: "); return FALSE; } } else { if (pkgtype & (1 << 2)) fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL820Q8); else fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL820Q7); } } else if (chipid2 == 0x35 && chipid1 == 0x31) { fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL815); } else if (chipid2 == 0x35 && chipid1 == 0x38) { fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL817); } else if (chipid2 == 0x35 && chipid1 == 0x90) { fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL817S); } else if (chipid2 == 0x35 && chipid1 == 0x95) { fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL822T); } else if (chipid2 == 0x35 && chipid1 == 0x99) { if (chipver == 0xC0 || chipver == 0xC1) fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL822C0); else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not supported 99 type"); return FALSE; } } else if (chipid2 == 0x35 && chipid1 == 0x66) { if (chipver <= 0xC0) fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL830); else fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL832); } else if (chipid2 == 0x35 && chipid1 == 0x45) { fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL211); } else if (chipid22 == 0x35 && chipid12 == 0x53) { fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL120); } else if (chipid22 == 0x35 && chipid12 == 0x92) { fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL122); } else if (tPid == 0x810) { fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL810); } else if (tPid == 0x811) { fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL811); } else if ((b811P812 & ((1 << 5) | (1 << 4))) == 0) { if (chipver == 0x10) fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL811PB0); else fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL811PB3); } else if ((b811P812 & ((1 << 5) | (1 << 4))) == (1 << 4)) { fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL812Q4S); } else if ((b811P812 & ((1 << 5) | (1 << 4))) == ((1 << 5) | (1 << 4))) { if (chipver == 0x10) fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL812B0); else fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL812B3); } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "hardware is not supported"); return FALSE; } /* success */ return TRUE; } static gboolean fu_vli_usbhub_device_probe(FuDevice *device, GError **error) { FuVliUsbhubDevice *self = FU_VLI_USBHUB_DEVICE(device); guint16 usbver = fu_usb_device_get_spec(FU_USB_DEVICE(device)); /* quirks now applied... */ if (usbver > 0x0300 || fu_device_has_private_flag(device, FU_VLI_USBHUB_DEVICE_FLAG_USB3)) { fu_device_set_summary(device, "USB 3.x hub"); /* prefer to show the USB 3 device and only fall back to the * USB 2 version as a recovery */ fu_device_set_priority(device, 1); } else if (usbver > 0x0200 || fu_device_has_private_flag(device, FU_VLI_USBHUB_DEVICE_FLAG_USB2)) { fu_device_set_summary(device, "USB 2.x hub"); } else { fu_device_set_summary(device, "USB hub"); } /* only some required */ if (fu_device_has_private_flag(device, FU_VLI_USBHUB_DEVICE_FLAG_ATTACH_WITH_USB_CABLE) || fu_device_has_private_flag(device, FU_VLI_USBHUB_DEVICE_FLAG_ATTACH_WITH_POWER_CORD)) { fu_device_add_request_flag(FU_DEVICE(self), FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); } return TRUE; } static gboolean fu_vli_usbhub_device_pd_setup(FuVliUsbhubDevice *self, GError **error) { g_autoptr(FuDevice) dev = NULL; g_autoptr(GError) error_local = NULL; /* add child */ dev = fu_vli_usbhub_pd_device_new(self); if (!fu_device_probe(dev, error)) return FALSE; if (!fu_device_setup(dev, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_debug("%s", error_local->message); } else { g_warning("cannot create PD device: %s", error_local->message); } return TRUE; } fu_device_add_child(FU_DEVICE(self), dev); return TRUE; } static gboolean fu_vli_usbhub_device_msp430_setup(FuVliUsbhubDevice *self, GError **error) { g_autoptr(FuDevice) dev = NULL; g_autoptr(GError) error_local = NULL; /* add child */ dev = fu_vli_usbhub_msp430_device_new(self); if (!fu_device_probe(dev, error)) return FALSE; if (!fu_device_setup(dev, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_debug("%s", error_local->message); } else { g_warning("cannot create MSP430 I²C device: %s", error_local->message); } return TRUE; } fu_device_add_child(FU_DEVICE(self), dev); return TRUE; } static gboolean fu_vli_usbhub_device_rtd21xx_setup(FuVliUsbhubDevice *self, GError **error) { g_autoptr(FuDevice) dev = NULL; g_autoptr(GError) error_local = NULL; /* add child */ dev = fu_vli_usbhub_rtd21xx_device_new(self); if (!fu_device_probe(dev, error)) return FALSE; if (!fu_device_setup(dev, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_debug("%s", error_local->message); } else { g_warning("cannot create RTD21XX I²C device: %s", error_local->message); } return TRUE; } fu_device_add_child(FU_DEVICE(self), dev); return TRUE; } static gboolean fu_vli_usbhub_device_ready(FuDevice *device, GError **error) { FuVliUsbhubDevice *self = FU_VLI_USBHUB_DEVICE(device); g_autoptr(GError) error_tmp = NULL; /* FuUsbDevice->ready */ if (!FU_DEVICE_CLASS(fu_vli_usbhub_device_parent_class)->ready(device, error)) return FALSE; /* to expose U3 hub, wait until fw is stable before sending VDR */ fu_device_sleep(FU_DEVICE(self), 100); /* ms */ /* try to read a block of data which will fail for 813-type devices */ if (fu_device_has_private_flag(device, FU_VLI_USBHUB_DEVICE_FLAG_UNLOCK_LEGACY813) && !fu_vli_device_spi_read_block(FU_VLI_DEVICE(self), 0x0, self->st_hd1->data, self->st_hd1->len, &error_tmp)) { g_warning("failed to read, trying to unlock 813: %s", error_tmp->message); if (!fu_vli_usbhub_device_vdr_unlock_813(self, error)) return FALSE; if (!fu_vli_device_spi_read_block(FU_VLI_DEVICE(self), 0x0, self->st_hd1->data, self->st_hd1->len, error)) { g_prefix_error(error, "813 unlock fail: "); return FALSE; } g_debug("813 unlock OK"); /* VL813 & VL210 have same PID (0x0813), and only VL813 can reply */ fu_vli_device_set_kind(FU_VLI_DEVICE(self), FU_VLI_DEVICE_KIND_VL813); } else { if (!fu_vli_usbhub_device_guess_kind(self, error)) return FALSE; } /* read HD1 (factory) header */ if (!fu_vli_device_spi_read_block(FU_VLI_DEVICE(self), VLI_USBHUB_FLASHMAP_ADDR_HD1, self->st_hd1->data, self->st_hd1->len, error)) { g_prefix_error(error, "failed to read HD1 header: "); return FALSE; } /* detect update protocol from the device ID */ switch (fu_struct_vli_usbhub_hdr_get_dev_id(self->st_hd1)) { /* VL810~VL813 */ case 0x0d12: self->update_protocol = 0x1; self->disable_powersave = TRUE; fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_set_install_duration(FU_DEVICE(self), 10); /* seconds */ break; /* VL817~ */ case 0x0507: case 0x0518: case 0x0538: case 0x0545: case 0x0553: case 0x0590: case 0x0592: case 0x0595: self->update_protocol = 0x2; fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SELF_RECOVERY); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_set_install_duration(FU_DEVICE(self), 15); /* seconds */ break; case 0x0566: self->update_protocol = 0x3; fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SELF_RECOVERY); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_set_install_duration(FU_DEVICE(self), 30); /* seconds */ break; default: g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "hardware is not supported, dev_id=0x%x", (guint)fu_struct_vli_usbhub_hdr_get_dev_id(self->st_hd1)); return FALSE; } /* read HD2 (update) header */ if (self->update_protocol >= 0x2) { if (!fu_vli_device_spi_read_block(FU_VLI_DEVICE(self), VLI_USBHUB_FLASHMAP_ADDR_HD2, self->st_hd2->data, self->st_hd2->len, error)) { g_prefix_error(error, "failed to read HD2 header: "); return FALSE; } } /* detect the PD child */ if (fu_device_has_private_flag(device, FU_VLI_USBHUB_DEVICE_FLAG_HAS_SHARED_SPI_PD)) { if (!fu_vli_usbhub_device_pd_setup(self, error)) return FALSE; } /* detect the I²C child */ if (fu_usb_device_get_spec(FU_USB_DEVICE(self)) >= 0x0300 && fu_device_has_private_flag(device, FU_VLI_USBHUB_DEVICE_FLAG_HAS_MSP430)) { if (!fu_vli_usbhub_device_msp430_setup(self, error)) return FALSE; } if (fu_device_has_private_flag(device, FU_VLI_USBHUB_DEVICE_FLAG_HAS_RTD21XX)) { if (!fu_vli_usbhub_device_rtd21xx_setup(self, error)) return FALSE; } /* success */ return TRUE; } static FuFirmware * fu_vli_usbhub_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuVliUsbhubDevice *self = FU_VLI_USBHUB_DEVICE(device); FuVliDeviceKind device_kind; g_autoptr(FuFirmware) firmware = fu_vli_usbhub_firmware_new(); /* check is compatible with firmware */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; device_kind = fu_vli_usbhub_firmware_get_device_kind(FU_VLI_USBHUB_FIRMWARE(firmware)); if (fu_vli_device_get_kind(FU_VLI_DEVICE(self)) != device_kind) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware incompatible, got %s, expected %s", fu_vli_device_kind_to_string(device_kind), fu_vli_device_kind_to_string(fu_vli_device_get_kind(FU_VLI_DEVICE(self)))); return NULL; } if (fu_struct_vli_usbhub_hdr_get_dev_id(self->st_hd1) != fu_vli_usbhub_firmware_get_device_id(FU_VLI_USBHUB_FIRMWARE(firmware))) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware incompatible, got 0x%04x, expected 0x%04x", fu_vli_usbhub_firmware_get_device_id(FU_VLI_USBHUB_FIRMWARE(firmware)), (guint)fu_struct_vli_usbhub_hdr_get_dev_id(self->st_hd1)); return NULL; } /* we could check this against flags */ g_info("parsed version: %s", fu_firmware_get_version(firmware)); return g_steal_pointer(&firmware); } static gboolean fu_vli_usbhub_device_update_v1(FuVliUsbhubDevice *self, FuFirmware *firmware, FuProgress *progress, GError **error) { gsize bufsz = 0; const guint8 *buf; g_autoptr(GBytes) fw = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 20, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 80, NULL); /* simple image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* erase */ if (!fu_vli_device_spi_erase_all(FU_VLI_DEVICE(self), fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to erase chip: "); return FALSE; } fu_progress_step_done(progress); /* write in chunks */ buf = g_bytes_get_data(fw, &bufsz); if (!fu_vli_device_spi_write(FU_VLI_DEVICE(self), 0x0, buf, bufsz, fu_progress_get_child(progress), error)) return FALSE; /* success */ fu_progress_step_done(progress); return TRUE; } /* if no header1 or ROM code update, write data directly */ static gboolean fu_vli_usbhub_device_update_v2_recovery(FuVliUsbhubDevice *self, GBytes *fw, FuProgress *progress, GError **error) { gsize bufsz = 0; const guint8 *buf = g_bytes_get_data(fw, &bufsz); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 20, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 80, NULL); /* erase */ for (guint32 addr = 0; addr < bufsz; addr += 0x1000) { if (!fu_vli_device_spi_erase_sector(FU_VLI_DEVICE(self), addr, error)) { g_prefix_error(error, "failed to erase sector @0x%x: ", addr); return FALSE; } fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)addr, bufsz); } fu_progress_step_done(progress); /* write in chunks */ if (!fu_vli_device_spi_write(FU_VLI_DEVICE(self), VLI_USBHUB_FLASHMAP_ADDR_HD1, buf, bufsz, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_vli_usbhub_device_hd1_is_valid(GByteArray *hdr) { if (fu_struct_vli_usbhub_hdr_get_prev_ptr(hdr) != VLI_USBHUB_FLASHMAP_IDX_INVALID) return FALSE; if (fu_struct_vli_usbhub_hdr_get_checksum(hdr) != fu_vli_usbhub_device_header_crc8(hdr)) return FALSE; return TRUE; } static gboolean fu_vli_usbhub_device_hd1_recover(FuVliUsbhubDevice *self, GByteArray *hdr, FuProgress *progress, GError **error) { /* point to HD2, i.e. updated firmware */ if (fu_struct_vli_usbhub_hdr_get_next_ptr(hdr) != VLI_USBHUB_FLASHMAP_IDX_HD2) { fu_struct_vli_usbhub_hdr_set_next_ptr(hdr, VLI_USBHUB_FLASHMAP_IDX_HD2); fu_struct_vli_usbhub_hdr_set_checksum(hdr, fu_vli_usbhub_device_header_crc8(hdr)); } /* write new header block */ if (!fu_vli_device_spi_erase_sector(FU_VLI_DEVICE(self), VLI_USBHUB_FLASHMAP_ADDR_HD1, error)) { g_prefix_error(error, "failed to erase header1 sector at 0x%x: ", (guint)VLI_USBHUB_FLASHMAP_ADDR_HD1); return FALSE; } if (!fu_vli_device_spi_write_block(FU_VLI_DEVICE(self), VLI_USBHUB_FLASHMAP_ADDR_HD1, hdr->data, hdr->len, progress, error)) { g_prefix_error(error, "failed to write header1 block at 0x%x: ", (guint)VLI_USBHUB_FLASHMAP_ADDR_HD1); return FALSE; } /* update the cached copy */ g_byte_array_unref(self->st_hd1); self->st_hd1 = g_byte_array_ref(hdr); return TRUE; } static gboolean fu_vli_usbhub_device_update_v2(FuVliUsbhubDevice *self, FuFirmware *firmware, FuProgress *progress, GError **error) { gsize buf_fwsz = 0; guint32 hd1_fw_sz; guint32 hd2_fw_sz; guint32 hd2_fw_addr; guint32 hd2_fw_offset; const guint8 *buf_fw; g_autoptr(GByteArray) st_hd = NULL; g_autoptr(GBytes) fw = NULL; /* simple image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* root header is valid */ if (fu_vli_usbhub_device_hd1_is_valid(self->st_hd1)) { /* no update has ever been done */ if (fu_struct_vli_usbhub_hdr_get_next_ptr(self->st_hd1) != VLI_USBHUB_FLASHMAP_IDX_HD2) { /* backup HD1 before recovering */ if (!fu_vli_device_spi_erase_sector(FU_VLI_DEVICE(self), VLI_USBHUB_FLASHMAP_ADDR_HD2, error)) { g_prefix_error(error, "failed to erase sector at header 1: "); return FALSE; } if (!fu_vli_device_spi_write_block(FU_VLI_DEVICE(self), VLI_USBHUB_FLASHMAP_ADDR_HD1_BACKUP, self->st_hd1->data, self->st_hd1->len, progress, error)) { g_prefix_error(error, "failed to write block at header 1: "); return FALSE; } if (!fu_vli_usbhub_device_hd1_recover(self, self->st_hd1, progress, error)) { g_prefix_error(error, "failed to write header: "); return FALSE; } } } else { /* copy the header from the backup zone */ g_info("HD1 was invalid, reading backup"); if (!fu_vli_device_spi_read_block(FU_VLI_DEVICE(self), VLI_USBHUB_FLASHMAP_ADDR_HD1_BACKUP, self->st_hd1->data, self->st_hd1->len, error)) { g_prefix_error(error, "failed to read root header from 0x%x: ", (guint)VLI_USBHUB_FLASHMAP_ADDR_HD1_BACKUP); return FALSE; } if (!fu_vli_usbhub_device_hd1_is_valid(self->st_hd1)) { g_info("backup header is also invalid, starting recovery"); return fu_vli_usbhub_device_update_v2_recovery(self, fw, progress, error); } if (!fu_vli_usbhub_device_hd1_recover(self, self->st_hd1, progress, error)) { g_prefix_error(error, "failed to get root header in backup zone: "); return FALSE; } } /* align the update fw address to the sector after the factory size */ hd1_fw_sz = fu_struct_vli_usbhub_hdr_get_usb3_fw_sz(self->st_hd1); if (hd1_fw_sz > 0xF000) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "FW1 size abnormal 0x%x", (guint)hd1_fw_sz); return FALSE; } hd2_fw_addr = (hd1_fw_sz + 0xfff) & 0xf000; hd2_fw_addr += VLI_USBHUB_FLASHMAP_ADDR_FW; /* get the size and offset of the update firmware */ buf_fw = g_bytes_get_data(fw, &buf_fwsz); st_hd = fu_struct_vli_usbhub_hdr_parse(buf_fw, buf_fwsz, 0x0, error); if (st_hd == NULL) return FALSE; hd2_fw_sz = fu_struct_vli_usbhub_hdr_get_usb3_fw_sz(st_hd); if (hd2_fw_sz == 0 || hd2_fw_sz > 0xF000) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "FW2 size abnormal 0x%x", (guint)hd2_fw_sz); return FALSE; } hd2_fw_offset = fu_struct_vli_usbhub_hdr_get_usb3_fw_addr(st_hd); g_debug("FW2 @0x%x (length 0x%x, offset 0x%x)", hd2_fw_addr, hd2_fw_sz, hd2_fw_offset); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 72, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 20, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 8, "hd2"); /* make space */ if (!fu_vli_device_spi_erase(FU_VLI_DEVICE(self), hd2_fw_addr, hd2_fw_sz, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* perform the actual write */ if (!fu_vli_device_spi_write(FU_VLI_DEVICE(self), hd2_fw_addr, buf_fw + hd2_fw_offset, hd2_fw_sz, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to write payload: "); return FALSE; } fu_progress_step_done(progress); /* write new HD2 */ fu_struct_vli_usbhub_hdr_set_usb3_fw_addr(st_hd, hd2_fw_addr & 0xFFFF); fu_struct_vli_usbhub_hdr_set_usb3_fw_addr_high(st_hd, hd2_fw_addr >> 16); fu_struct_vli_usbhub_hdr_set_prev_ptr(st_hd, VLI_USBHUB_FLASHMAP_IDX_HD1); fu_struct_vli_usbhub_hdr_set_next_ptr(st_hd, VLI_USBHUB_FLASHMAP_IDX_INVALID); fu_struct_vli_usbhub_hdr_set_checksum(st_hd, fu_vli_usbhub_device_header_crc8(st_hd)); if (!fu_vli_device_spi_erase_sector(FU_VLI_DEVICE(self), VLI_USBHUB_FLASHMAP_ADDR_HD2, error)) { g_prefix_error(error, "failed to erase sectors for HD2: "); return FALSE; } if (!fu_vli_device_spi_write_block(FU_VLI_DEVICE(self), VLI_USBHUB_FLASHMAP_ADDR_HD2, st_hd->data, st_hd->len, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to write HD2: "); return FALSE; } fu_progress_step_done(progress); /* success */ g_byte_array_unref(self->st_hd2); self->st_hd2 = g_byte_array_ref(st_hd); return TRUE; } static gboolean fu_vli_usbhub_device_update_v3(FuVliUsbhubDevice *self, FuFirmware *firmware, FuProgress *progress, GError **error) { gsize buf_fwsz = 0; guint32 hd2_fw_sz; guint32 hd2_fw_addr; guint32 hd2_fw_offset; const guint8 *buf_fw; g_autoptr(GByteArray) st_hd = NULL; g_autoptr(GBytes) fw = NULL; /* simple image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* root header is valid */ if (fu_vli_usbhub_device_hd1_is_valid(self->st_hd1)) { /* no update has ever been done */ if (fu_struct_vli_usbhub_hdr_get_next_ptr(self->st_hd1) != VLI_USBHUB_FLASHMAP_IDX_HD2) { /* backup HD1 before recovering */ if (!fu_vli_device_spi_erase_sector(FU_VLI_DEVICE(self), VLI_USBHUB_FLASHMAP_ADDR_HD2, error)) { g_prefix_error(error, "failed to erase sector at header 1: "); return FALSE; } if (!fu_vli_device_spi_write_block(FU_VLI_DEVICE(self), VLI_USBHUB_FLASHMAP_ADDR_HD1_BACKUP, self->st_hd1->data, self->st_hd1->len, progress, error)) { g_prefix_error(error, "failed to write block at header 1: "); return FALSE; } if (!fu_vli_usbhub_device_hd1_recover(self, self->st_hd1, progress, error)) { g_prefix_error(error, "failed to write header: "); return FALSE; } } } else { /* copy the header from the backup zone */ g_info("HD1 was invalid, reading backup"); if (!fu_vli_device_spi_read_block(FU_VLI_DEVICE(self), VLI_USBHUB_FLASHMAP_ADDR_HD1_BACKUP, self->st_hd1->data, self->st_hd1->len, error)) { g_prefix_error(error, "failed to read root header from 0x%x: ", (guint)VLI_USBHUB_FLASHMAP_ADDR_HD1_BACKUP); return FALSE; } if (!fu_vli_usbhub_device_hd1_is_valid(self->st_hd1)) { g_info("backup header is also invalid, starting recovery"); return fu_vli_usbhub_device_update_v2_recovery(self, fw, progress, error); } if (!fu_vli_usbhub_device_hd1_recover(self, self->st_hd1, progress, error)) { g_prefix_error(error, "failed to get root header in backup zone: "); return FALSE; } } /* use fixed address for update fw */ if (fu_vli_device_get_kind(FU_VLI_DEVICE(self)) == FU_VLI_DEVICE_KIND_VL830) hd2_fw_addr = 0x60000; else hd2_fw_addr = 0x80000; /* get the size and offset of the update firmware */ buf_fw = g_bytes_get_data(fw, &buf_fwsz); st_hd = fu_struct_vli_usbhub_hdr_parse(buf_fw, buf_fwsz, 0x0, error); if (st_hd == NULL) return FALSE; hd2_fw_sz = (fu_struct_vli_usbhub_hdr_get_usb3_fw_sz_high(st_hd) << 16); hd2_fw_sz += fu_struct_vli_usbhub_hdr_get_usb3_fw_sz(st_hd); if (hd2_fw_sz == 0 || hd2_fw_sz > 0x40000) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "FW2 size abnormal 0x%x", (guint)hd2_fw_sz); return FALSE; } hd2_fw_offset = (fu_struct_vli_usbhub_hdr_get_usb3_fw_addr_high(st_hd) << 16); hd2_fw_offset += fu_struct_vli_usbhub_hdr_get_usb3_fw_addr(st_hd); g_debug("FW2 @0x%x (length 0x%x, offset 0x%x)", hd2_fw_addr, hd2_fw_sz, hd2_fw_offset); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 72, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 20, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 8, "hd2"); /* make space */ if (!fu_vli_device_spi_erase(FU_VLI_DEVICE(self), hd2_fw_addr, hd2_fw_sz, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* perform the actual write */ if (!fu_vli_device_spi_write(FU_VLI_DEVICE(self), hd2_fw_addr, buf_fw + hd2_fw_offset, hd2_fw_sz, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to write payload: "); return FALSE; } fu_progress_step_done(progress); /* write new HD2 */ fu_struct_vli_usbhub_hdr_set_usb3_fw_addr(st_hd, hd2_fw_addr & 0xFFFF); fu_struct_vli_usbhub_hdr_set_usb3_fw_addr_high(st_hd, hd2_fw_addr >> 16); fu_struct_vli_usbhub_hdr_set_prev_ptr(st_hd, VLI_USBHUB_FLASHMAP_IDX_HD1); fu_struct_vli_usbhub_hdr_set_next_ptr(st_hd, VLI_USBHUB_FLASHMAP_IDX_INVALID); fu_struct_vli_usbhub_hdr_set_checksum(st_hd, fu_vli_usbhub_device_header_crc8(st_hd)); if (!fu_vli_device_spi_erase_sector(FU_VLI_DEVICE(self), VLI_USBHUB_FLASHMAP_ADDR_HD2, error)) { g_prefix_error(error, "failed to erase sectors for HD2: "); return FALSE; } if (!fu_vli_device_spi_write_block(FU_VLI_DEVICE(self), VLI_USBHUB_FLASHMAP_ADDR_HD2, st_hd->data, st_hd->len, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to write HD2: "); return FALSE; } fu_progress_step_done(progress); /* success */ g_byte_array_unref(self->st_hd2); self->st_hd2 = g_byte_array_ref(st_hd); return TRUE; } static GBytes * fu_vli_usbhub_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuVliUsbhubDevice *self = FU_VLI_USBHUB_DEVICE(device); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ); return fu_vli_device_spi_read(FU_VLI_DEVICE(self), 0x0, fu_device_get_firmware_size_max(device), progress, error); } static gboolean fu_vli_usbhub_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuVliUsbhubDevice *self = FU_VLI_USBHUB_DEVICE(device); /* disable powersaving if required */ if (self->disable_powersave) { if (!fu_vli_usbhub_device_disable_u1u2(self, error)) { g_prefix_error(error, "disabling powersave failed: "); return FALSE; } } /* use correct method */ if (self->update_protocol == 0x1) return fu_vli_usbhub_device_update_v1(self, firmware, progress, error); if (self->update_protocol == 0x2) return fu_vli_usbhub_device_update_v2(self, firmware, progress, error); if (self->update_protocol == 0x3) return fu_vli_usbhub_device_update_v3(self, firmware, progress, error); /* not sure what to do */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "update protocol 0x%x not supported", self->update_protocol); return FALSE; } static void fu_vli_usbhub_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 92, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 7, "reload"); } static void fu_vli_usbhub_device_init(FuVliUsbhubDevice *self) { self->st_hd1 = fu_struct_vli_usbhub_hdr_new(); self->st_hd2 = fu_struct_vli_usbhub_hdr_new(); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_USB_HUB); fu_device_add_protocol(FU_DEVICE(self), "com.vli.usbhub"); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_USE_PROXY_FALLBACK); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_AUTO_PARENT_CHILDREN); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_register_private_flag(FU_DEVICE(self), FU_VLI_USBHUB_DEVICE_FLAG_ATTACH_WITH_GPIOB); fu_device_register_private_flag(FU_DEVICE(self), FU_VLI_USBHUB_DEVICE_FLAG_USB2); fu_device_register_private_flag(FU_DEVICE(self), FU_VLI_USBHUB_DEVICE_FLAG_USB3); fu_device_register_private_flag(FU_DEVICE(self), FU_VLI_USBHUB_DEVICE_FLAG_UNLOCK_LEGACY813); fu_device_register_private_flag(FU_DEVICE(self), FU_VLI_USBHUB_DEVICE_FLAG_HAS_SHARED_SPI_PD); fu_device_register_private_flag(FU_DEVICE(self), FU_VLI_USBHUB_DEVICE_FLAG_HAS_MSP430); fu_device_register_private_flag(FU_DEVICE(self), FU_VLI_USBHUB_DEVICE_FLAG_HAS_RTD21XX); fu_device_register_private_flag(FU_DEVICE(self), FU_VLI_USBHUB_DEVICE_FLAG_ATTACH_WITH_USB_CABLE); fu_device_register_private_flag(FU_DEVICE(self), FU_VLI_USBHUB_DEVICE_FLAG_ATTACH_WITH_POWER_CORD); } static void fu_vli_usbhub_device_finalize(GObject *obj) { FuVliUsbhubDevice *self = FU_VLI_USBHUB_DEVICE(obj); g_byte_array_unref(self->st_hd1); g_byte_array_unref(self->st_hd2); G_OBJECT_CLASS(fu_vli_usbhub_device_parent_class)->finalize(obj); } static void fu_vli_usbhub_device_class_init(FuVliUsbhubDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); FuVliDeviceClass *vli_device_class = FU_VLI_DEVICE_CLASS(klass); object_class->finalize = fu_vli_usbhub_device_finalize; device_class->probe = fu_vli_usbhub_device_probe; device_class->dump_firmware = fu_vli_usbhub_device_dump_firmware; device_class->write_firmware = fu_vli_usbhub_device_write_firmware; device_class->prepare_firmware = fu_vli_usbhub_device_prepare_firmware; device_class->attach = fu_vli_usbhub_device_attach; device_class->to_string = fu_vli_usbhub_device_to_string; device_class->ready = fu_vli_usbhub_device_ready; device_class->set_progress = fu_vli_usbhub_device_set_progress; vli_device_class->spi_chip_erase = fu_vli_usbhub_device_spi_chip_erase; vli_device_class->spi_sector_erase = fu_vli_usbhub_device_spi_sector_erase; vli_device_class->spi_read_data = fu_vli_usbhub_device_spi_read_data; vli_device_class->spi_read_status = fu_vli_usbhub_device_spi_read_status; vli_device_class->spi_write_data = fu_vli_usbhub_device_spi_write_data; vli_device_class->spi_write_enable = fu_vli_usbhub_device_spi_write_enable; vli_device_class->spi_write_status = fu_vli_usbhub_device_spi_write_status; } fwupd-2.0.10/plugins/vli/fu-vli-usbhub-device.h000066400000000000000000000005321501337203100212630ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-vli-device.h" #define FU_TYPE_VLI_USBHUB_DEVICE (fu_vli_usbhub_device_get_type()) G_DECLARE_FINAL_TYPE(FuVliUsbhubDevice, fu_vli_usbhub_device, FU, VLI_USBHUB_DEVICE, FuVliDevice) fwupd-2.0.10/plugins/vli/fu-vli-usbhub-firmware.c000066400000000000000000000236631501337203100216450ustar00rootroot00000000000000/* * Copyright 2017 VIA Corporation * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-vli-struct.h" #include "fu-vli-usbhub-firmware.h" struct _FuVliUsbhubFirmware { FuFirmwareClass parent_instance; FuVliDeviceKind device_kind; guint16 dev_id; }; G_DEFINE_TYPE(FuVliUsbhubFirmware, fu_vli_usbhub_firmware, FU_TYPE_FIRMWARE) FuVliDeviceKind fu_vli_usbhub_firmware_get_device_kind(FuVliUsbhubFirmware *self) { g_return_val_if_fail(FU_IS_VLI_USBHUB_FIRMWARE(self), 0); return self->device_kind; } guint16 fu_vli_usbhub_firmware_get_device_id(FuVliUsbhubFirmware *self) { g_return_val_if_fail(FU_IS_VLI_USBHUB_FIRMWARE(self), 0); return self->dev_id; } static void fu_vli_usbhub_firmware_export(FuFirmware *firmware, FuFirmwareExportFlags flags, XbBuilderNode *bn) { FuVliUsbhubFirmware *self = FU_VLI_USBHUB_FIRMWARE(firmware); fu_xmlb_builder_insert_kv(bn, "device_kind", fu_vli_device_kind_to_string(self->device_kind)); } static gboolean fu_vli_usbhub_firmware_parse_version(FuVliUsbhubFirmware *self, GInputStream *stream, guint8 strapping1, GError **error) { guint16 adr_ofs = 0; guint16 version = 0x0; guint32 adr_ofs32 = 0; guint8 tmp = 0x0; switch (self->dev_id) { case 0x0d12: /* VL81x */ if (!fu_input_stream_read_u16(stream, 0x1f4c, &version, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to get version: "); return FALSE; } version |= (strapping1 >> 4) & 0x07; if ((version & 0x0f) == 0x04) { if (!fu_input_stream_read_u8(stream, 0x700d, &tmp, error)) { g_prefix_error(error, "failed to get version increment: "); return FALSE; } if (tmp & 0x40) version += 1; } break; case 0x0507: /* VL210 */ if (!fu_input_stream_read_u16(stream, 0x8f0c, &version, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to get version: "); return FALSE; } version |= (strapping1 >> 4) & 0x07; if ((version & 0x0f) == 0x04) version += 1; break; case 0x0566: /* U4ID_Address_In_FW_Zone */ if (!fu_input_stream_read_u24(stream, 0x3F80, &adr_ofs32, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to get offset addr: "); return FALSE; } if (adr_ofs32 < 0x20000 + 0x2000 + 4) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid U4ID_Address_In_FW_Zone=0x%x", adr_ofs32); return FALSE; } if (!fu_input_stream_read_u16(stream, adr_ofs32 - 0x20000 + 0x2000 + 4, &version, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to get offset version: "); return FALSE; } version |= (strapping1 >> 4) & 0x07; break; default: /* U3ID_Address_In_FW_Zone */ if (!fu_input_stream_read_u16(stream, 0x8000, &adr_ofs, G_BIG_ENDIAN, error)) { g_prefix_error(error, "failed to get offset addr: "); return FALSE; } if (!fu_input_stream_read_u16(stream, adr_ofs + 0x2000 + 0x04, /* U3-M? */ &version, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to get offset version: "); return FALSE; } version |= (strapping1 >> 4) & 0x07; } /* version is set */ if (version != 0x0) fu_firmware_set_version_raw(FU_FIRMWARE(self), version); return TRUE; } static gboolean fu_vli_usbhub_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuVliUsbhubFirmware *self = FU_VLI_USBHUB_FIRMWARE(firmware); guint16 adr_ofs = 0; guint8 tmp = 0x0; guint8 fwtype = 0x0; guint8 strapping1; g_autoptr(GByteArray) st = NULL; /* map into header */ st = fu_struct_vli_usbhub_hdr_parse_stream(stream, 0x0, error); if (st == NULL) { g_prefix_error(error, "failed to read header: "); return FALSE; } self->dev_id = fu_struct_vli_usbhub_hdr_get_dev_id(st); strapping1 = fu_struct_vli_usbhub_hdr_get_strapping1(st); /* get firmware versions */ if (!fu_vli_usbhub_firmware_parse_version(self, stream, strapping1, error)) return FALSE; /* get device type from firmware image */ switch (self->dev_id) { case 0x0d12: { guint16 binver1 = 0x0; guint16 binver2 = 0x0; guint16 usb2_fw_addr = fu_struct_vli_usbhub_hdr_get_usb2_fw_addr(st) + 0x1ff1; guint16 usb3_fw_addr = fu_struct_vli_usbhub_hdr_get_usb3_fw_addr(st) + 0x1ffa; if (!fu_input_stream_read_u16(stream, usb2_fw_addr, &binver1, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to get binver1: "); return FALSE; } if (!fu_input_stream_read_u16(stream, usb3_fw_addr, &binver2, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to get binver2: "); return FALSE; } /* VL813 == VT3470 */ if ((binver1 == 0xb770 && binver2 == 0xb770) || (binver1 == 0xb870 && binver2 == 0xb870)) { self->device_kind = FU_VLI_DEVICE_KIND_VL813; /* VLQ4S == VT3470 (Q4S) */ } else if (strapping1 & FU_VLI_USBHUB_HEADER_STRAPPING1_Q4S) { self->device_kind = FU_VLI_DEVICE_KIND_VL812Q4S; /* VL812 == VT3470 (812/813) */ } else if (strapping1 & FU_VLI_USBHUB_HEADER_STRAPPING1_76PIN) { /* is B3 */ if (strapping1 & FU_VLI_USBHUB_HEADER_STRAPPING1_B3UP) self->device_kind = FU_VLI_DEVICE_KIND_VL812B3; else self->device_kind = FU_VLI_DEVICE_KIND_VL812B0; /* VL811P == VT3470 */ } else { /* is B3 */ if (strapping1 & FU_VLI_USBHUB_HEADER_STRAPPING1_B3UP) self->device_kind = FU_VLI_DEVICE_KIND_VL811PB3; else self->device_kind = FU_VLI_DEVICE_KIND_VL811PB0; } break; } case 0x0507: /* VL210 == VT3507 */ self->device_kind = FU_VLI_DEVICE_KIND_VL210; break; case 0x0545: /* VL211 == VT3545 */ self->device_kind = FU_VLI_DEVICE_KIND_VL211; break; case 0x0518: /* VL819~VL822 == VT3518 */ if (!fu_input_stream_read_u8(stream, 0x8021, &tmp, error)) { g_prefix_error(error, "failed to get 820/822 byte: "); return FALSE; } /* Q5/Q7/Q8 requires searching two addresses for offset value */ if (!fu_input_stream_read_u16(stream, 0x8018, &adr_ofs, G_BIG_ENDIAN, error)) { g_prefix_error(error, "failed to get Q7/Q8 offset mapping: "); return FALSE; } /* VL819, VL821, VL822 */ if (tmp == 0xF0) { if (!fu_input_stream_read_u8(stream, adr_ofs + 0x2000, &tmp, error)) { g_prefix_error(error, "failed to get offset version: "); return FALSE; } /* VL819 */ if ((tmp == 0x05) || (tmp == 0x07)) fwtype = tmp & 0x7; else fwtype = (tmp & 0x1) << 1 | (tmp & 0x2) << 1 | (tmp & 0x4) >> 2; /* matching Q5/Q7/Q8 */ switch (fwtype) { case 0x00: self->device_kind = FU_VLI_DEVICE_KIND_VL822Q7; break; case 0x01: self->device_kind = FU_VLI_DEVICE_KIND_VL822Q5; break; case 0x02: self->device_kind = FU_VLI_DEVICE_KIND_VL822Q8; break; case 0x04: self->device_kind = FU_VLI_DEVICE_KIND_VL821Q7; break; case 0x05: self->device_kind = FU_VLI_DEVICE_KIND_VL819Q7; break; case 0x06: self->device_kind = FU_VLI_DEVICE_KIND_VL821Q8; break; case 0x07: self->device_kind = FU_VLI_DEVICE_KIND_VL819Q8; break; default: g_prefix_error(error, "failed to match Q5/Q7/Q8 fw type: "); return FALSE; } /* VL820 */ } else if (tmp == 0xC0 || tmp == 0xC1) { self->device_kind = FU_VLI_DEVICE_KIND_VL822C0; } else { if (!fu_input_stream_read_u8(stream, 0xf000, &tmp, error)) { g_prefix_error(error, "failed to get Q7/Q8 difference: "); return FALSE; } if (tmp & (1 << 0)) self->device_kind = FU_VLI_DEVICE_KIND_VL820Q8; else self->device_kind = FU_VLI_DEVICE_KIND_VL820Q7; } break; case 0x0595: /* VL822T == VT3595 */ self->device_kind = FU_VLI_DEVICE_KIND_VL822T; break; case 0x0538: /* VL817 == VT3538 */ self->device_kind = FU_VLI_DEVICE_KIND_VL817; break; case 0x0590: /* VL817S == VT3590 */ self->device_kind = FU_VLI_DEVICE_KIND_VL817S; break; case 0x0553: /* VL120 == VT3553 */ self->device_kind = FU_VLI_DEVICE_KIND_VL120; break; case 0x0592: /* VL122 == VT3592 */ self->device_kind = FU_VLI_DEVICE_KIND_VL122; break; case 0x0566: { /* VL830 VL832 = VT3566 */ guint32 binveraddr = 0; guint8 binver = 0; if (!fu_input_stream_read_u24(stream, 0x3FBC, &binveraddr, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to get binveraddr: "); return FALSE; } if (binveraddr < 0x20000 + 0x2000) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "stream was too small"); return FALSE; } if (!fu_input_stream_read_u8(stream, binveraddr - 0x20000 + 0x2000, &binver, error)) { g_prefix_error(error, "failed to get binver2: "); return FALSE; } /* VL813 == VT3470 */ if (binver <= 0xC0) self->device_kind = FU_VLI_DEVICE_KIND_VL830; else self->device_kind = FU_VLI_DEVICE_KIND_VL832; break; } default: break; } /* device not supported */ if (self->device_kind == FU_VLI_DEVICE_KIND_UNKNOWN) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device kind unknown"); return FALSE; } /* success */ return TRUE; } static gchar * fu_vli_usbhub_firmware_convert_version(FuFirmware *firmware, guint64 version_raw) { return fu_version_from_uint16(version_raw, fu_firmware_get_version_format(firmware)); } static void fu_vli_usbhub_firmware_init(FuVliUsbhubFirmware *self) { fu_firmware_set_version_format(FU_FIRMWARE(self), FWUPD_VERSION_FORMAT_BCD); } static void fu_vli_usbhub_firmware_class_init(FuVliUsbhubFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->convert_version = fu_vli_usbhub_firmware_convert_version; firmware_class->parse = fu_vli_usbhub_firmware_parse; firmware_class->export = fu_vli_usbhub_firmware_export; } FuFirmware * fu_vli_usbhub_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_VLI_USBHUB_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/vli/fu-vli-usbhub-firmware.h000066400000000000000000000011641501337203100216420ustar00rootroot00000000000000/* * Copyright 2017 VIA Corporation * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-vli-usbhub-common.h" #define FU_TYPE_VLI_USBHUB_FIRMWARE (fu_vli_usbhub_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuVliUsbhubFirmware, fu_vli_usbhub_firmware, FU, VLI_USBHUB_FIRMWARE, FuFirmware) FuFirmware * fu_vli_usbhub_firmware_new(void); FuVliDeviceKind fu_vli_usbhub_firmware_get_device_kind(FuVliUsbhubFirmware *self); guint16 fu_vli_usbhub_firmware_get_device_id(FuVliUsbhubFirmware *self); fwupd-2.0.10/plugins/vli/fu-vli-usbhub-i2c-common.c000066400000000000000000000026041501337203100217640ustar00rootroot00000000000000/* * Copyright 2017 VIA Corporation * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-vli-usbhub-i2c-common.h" gboolean fu_vli_usbhub_i2c_check_status(FuVliUsbhubI2cStatus status, GError **error) { if (status == FU_VLI_USBHUB_I2C_STATUS_OK) return TRUE; if (status == FU_VLI_USBHUB_I2C_STATUS_HEADER) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Incorrect header value of data frame"); return FALSE; } if (status == FU_VLI_USBHUB_I2C_STATUS_COMMAND) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Invalid command data"); return FALSE; } if (status == FU_VLI_USBHUB_I2C_STATUS_ADDRESS) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Invalid address range"); return FALSE; } if (status == FU_VLI_USBHUB_I2C_STATUS_PACKETSIZE) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Incorrect payload data length"); return FALSE; } if (status == FU_VLI_USBHUB_I2C_STATUS_CHECKSUM) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Incorrect frame data checksum"); return FALSE; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Unknown error [0x%02x]", status); return FALSE; } fwupd-2.0.10/plugins/vli/fu-vli-usbhub-i2c-common.h000066400000000000000000000010621501337203100217660ustar00rootroot00000000000000/* * Copyright 2017 VIA Corporation * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include typedef enum { FU_VLI_USBHUB_I2C_STATUS_OK = 0x00, FU_VLI_USBHUB_I2C_STATUS_HEADER = 0x51, FU_VLI_USBHUB_I2C_STATUS_COMMAND = 0x52, FU_VLI_USBHUB_I2C_STATUS_ADDRESS = 0x53, FU_VLI_USBHUB_I2C_STATUS_PACKETSIZE = 0x54, FU_VLI_USBHUB_I2C_STATUS_CHECKSUM = 0x55, } FuVliUsbhubI2cStatus; gboolean fu_vli_usbhub_i2c_check_status(FuVliUsbhubI2cStatus status, GError **error); fwupd-2.0.10/plugins/vli/fu-vli-usbhub-msp430-device.c000066400000000000000000000252351501337203100223110ustar00rootroot00000000000000/* * Copyright 2017 VIA Corporation * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-vli-struct.h" #include "fu-vli-usbhub-common.h" #include "fu-vli-usbhub-i2c-common.h" #include "fu-vli-usbhub-msp430-device.h" struct _FuVliUsbhubMsp430Device { FuDevice parent_instance; }; G_DEFINE_TYPE(FuVliUsbhubMsp430Device, fu_vli_usbhub_msp430_device, FU_TYPE_DEVICE) /* Texas Instruments BSL */ #define I2C_ADDR_WRITE 0x18 #define I2C_ADDR_READ 0x19 #define I2C_CMD_WRITE 0x32 #define I2C_CMD_READ_STATUS 0x33 #define I2C_CMD_UPGRADE 0x34 #define I2C_CMD_READ_VERSIONS 0x40 #define I2C_R_VDR 0xa0 /* read vendor command */ #define I2C_W_VDR 0xb0 /* write vendor command */ static gboolean fu_vli_usbhub_msp430_device_i2c_read(FuVliUsbhubDevice *self, guint8 cmd, guint8 *buf, gsize bufsz, GError **error) { guint16 value = ((guint16)I2C_ADDR_WRITE << 8) | cmd; guint16 index = (guint16)I2C_ADDR_READ << 8; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, I2C_R_VDR, value, index, buf, bufsz, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to read I2C: "); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "I2cReadData", buf, bufsz); return TRUE; } static gboolean fu_vli_usbhub_msp430_device_i2c_read_status(FuVliUsbhubDevice *self, FuVliUsbhubI2cStatus *status, GError **error) { guint8 buf[1] = {0xff}; if (!fu_vli_usbhub_msp430_device_i2c_read(self, I2C_CMD_READ_STATUS, buf, sizeof(buf), error)) return FALSE; if (status != NULL) *status = buf[0]; return TRUE; } static gboolean fu_vli_usbhub_msp430_device_i2c_write_data(FuVliUsbhubDevice *self, guint8 disable_start_bit, guint8 disable_end_bit, const guint8 *buf, gsize bufsz, GError **error) { guint16 value = (((guint16)disable_start_bit) << 8) | disable_end_bit; g_autofree guint8 *buf_mut = NULL; fu_dump_raw(G_LOG_DOMAIN, "I2cWriteData", buf, bufsz); buf_mut = fu_memdup_safe(buf, bufsz, error); if (buf_mut == NULL) return FALSE; if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, I2C_W_VDR, value, 0x0, buf_mut, bufsz, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to write I2C @0x%x: ", value); return FALSE; } return TRUE; } static gboolean fu_vli_usbhub_msp430_device_setup(FuDevice *device, GError **error) { FuVliUsbhubDevice *parent = FU_VLI_USBHUB_DEVICE(fu_device_get_parent(device)); guint8 buf[11] = {0x0}; g_autofree gchar *version = NULL; /* get versions */ if (!fu_vli_usbhub_msp430_device_i2c_read(parent, I2C_CMD_READ_VERSIONS, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to read versions: "); return FALSE; } if ((buf[0] == 0x00 && buf[1] == 0x00 && buf[2] == 0x00) || (buf[0] == 0xff && buf[1] == 0xff && buf[2] == 0xff)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no MSP430 device detected"); return FALSE; } /* set version */ version = g_strdup_printf("%x.%x", buf[0], buf[1]); fu_device_set_version(device, version); return TRUE; } static gboolean fu_vli_usbhub_msp430_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuVliUsbhubDevice *parent = FU_VLI_USBHUB_DEVICE(fu_device_get_parent(device)); FuVliUsbhubI2cStatus status = 0xff; g_autoptr(FuDeviceLocker) locker = NULL; const guint8 buf[] = { I2C_ADDR_WRITE, I2C_CMD_UPGRADE, }; /* open device */ locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; if (!fu_vli_usbhub_msp430_device_i2c_write_data(parent, 0, 0, buf, sizeof(buf), error)) return FALSE; /* avoid power instability by waiting T1 */ fu_device_sleep_full(device, 1000, progress); /* ms */ /* check the device came back */ if (!fu_vli_usbhub_msp430_device_i2c_read_status(parent, &status, error)) { g_prefix_error(error, "device did not come back after detach: "); return FALSE; } return fu_vli_usbhub_i2c_check_status(status, error); } typedef struct { guint8 command; guint8 buf[0x40]; gsize bufsz; guint8 len; } FuVliUsbhubDeviceRequest; static gboolean fu_vli_usbhub_msp430_device_write_firmware_cb(FuDevice *device, gpointer user_data, GError **error) { FuVliUsbhubDeviceRequest *req = (FuVliUsbhubDeviceRequest *)user_data; FuVliUsbhubDevice *parent = FU_VLI_USBHUB_DEVICE(fu_device_get_parent(device)); FuVliUsbhubI2cStatus status = 0xff; fu_device_sleep(device, 5); /* ms */ if (fu_usb_device_get_spec(FU_USB_DEVICE(parent)) >= 0x0300 || req->bufsz <= 32) { if (!fu_vli_usbhub_msp430_device_i2c_write_data(parent, 0, 0, req->buf, req->bufsz, error)) return FALSE; } else { /* for U2, hub data buffer <= 32 bytes */ if (!fu_vli_usbhub_msp430_device_i2c_write_data(parent, 0, 1, req->buf, 32, error)) return FALSE; if (!fu_vli_usbhub_msp430_device_i2c_write_data(parent, 1, 0, req->buf + 32, req->bufsz - 32, error)) return FALSE; } /* end of file, no need to check status */ if (req->len == 0 && req->buf[6] == 0x01 && req->buf[7] == 0xFF) return TRUE; /* read data to check status */ fu_device_sleep(device, 5); /* ms */ if (!fu_vli_usbhub_msp430_device_i2c_read_status(parent, &status, error)) return FALSE; return fu_vli_usbhub_i2c_check_status(status, error); } static gboolean fu_vli_usbhub_msp430_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuVliUsbhubDevice *parent = FU_VLI_USBHUB_DEVICE(fu_device_get_parent(device)); GPtrArray *records = fu_ihex_firmware_get_records(FU_IHEX_FIRMWARE(firmware)); g_autoptr(FuDeviceLocker) locker = NULL; /* open device */ locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; /* transfer by I²C write, and check status by I²C read */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); fu_progress_set_steps(progress, records->len); for (guint j = 0; j < records->len; j++) { FuIhexFirmwareRecord *rcd = g_ptr_array_index(records, j); FuVliUsbhubDeviceRequest req = {0x0}; const gchar *line = rcd->buf->str; /* length, 16-bit address, type */ req.len = rcd->byte_cnt; if (req.len >= sizeof(req.buf) - 7) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "line too long; buffer size is 0x%x bytes", (guint)sizeof(req.buf)); return FALSE; } /* write each record directly to the hardware */ req.buf[0] = I2C_ADDR_WRITE; req.buf[1] = I2C_CMD_WRITE; req.buf[2] = 0x3a; /* ':' */ req.buf[3] = req.len; if (!fu_firmware_strparse_uint8_safe(line, rcd->buf->len, 3, &req.buf[4], error)) return FALSE; if (!fu_firmware_strparse_uint8_safe(line, rcd->buf->len, 5, &req.buf[5], error)) return FALSE; if (!fu_firmware_strparse_uint8_safe(line, rcd->buf->len, 7, &req.buf[6], error)) return FALSE; for (guint8 i = 0; i < req.len; i++) { if (!fu_firmware_strparse_uint8_safe(line, rcd->buf->len, 9 + (i * 2), &req.buf[7 + i], error)) return FALSE; } if (!fu_firmware_strparse_uint8_safe(line, rcd->buf->len, 9 + (req.len * 2), &req.buf[7 + req.len], error)) return FALSE; req.bufsz = req.len + 8; /* retry this if it fails */ if (!fu_device_retry(device, fu_vli_usbhub_msp430_device_write_firmware_cb, 5, &req, error)) return FALSE; fu_progress_step_done(progress); } /* the device automatically reboots */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* success */ return TRUE; } static gboolean fu_vli_usbhub_msp430_device_probe(FuDevice *device, GError **error) { FuVliDeviceKind device_kind = FU_VLI_DEVICE_KIND_MSP430; FuVliUsbhubDevice *parent = FU_VLI_USBHUB_DEVICE(fu_device_get_parent(device)); fu_device_set_name(device, fu_vli_device_kind_to_string(device_kind)); if (parent != NULL) { fu_device_incorporate(device, FU_DEVICE(parent), FU_DEVICE_INCORPORATE_FLAG_PHYSICAL_ID); } /* add instance ID */ fu_device_add_instance_str(device, "I2C", fu_vli_device_kind_to_string(device_kind)); return fu_device_build_instance_id(device, error, "USB", "VID", "PID", NULL); } static void fu_vli_usbhub_msp430_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 13, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 85, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_vli_usbhub_msp430_device_init(FuVliUsbhubMsp430Device *self) { fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_USB_HUB); fu_device_add_protocol(FU_DEVICE(self), "com.vli.i2c"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PAIR); fu_device_set_logical_id(FU_DEVICE(self), "I2C"); fu_device_set_summary(FU_DEVICE(self), "I²C dock management device"); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_IHEX_FIRMWARE); /* the MSP device reboot takes down the entire hub for ~60 seconds */ fu_device_set_remove_delay(FU_DEVICE(self), 120 * 1000); } static void fu_vli_usbhub_msp430_device_class_init(FuVliUsbhubMsp430DeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_vli_usbhub_msp430_device_probe; device_class->setup = fu_vli_usbhub_msp430_device_setup; device_class->detach = fu_vli_usbhub_msp430_device_detach; device_class->write_firmware = fu_vli_usbhub_msp430_device_write_firmware; device_class->set_progress = fu_vli_usbhub_msp430_device_set_progress; } FuDevice * fu_vli_usbhub_msp430_device_new(FuVliUsbhubDevice *parent) { FuVliUsbhubMsp430Device *self = g_object_new(FU_TYPE_VLI_USBHUB_MSP430_DEVICE, "parent", parent, NULL); return FU_DEVICE(self); } fwupd-2.0.10/plugins/vli/fu-vli-usbhub-msp430-device.h000066400000000000000000000007441501337203100223140ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-vli-usbhub-device.h" #define FU_TYPE_VLI_USBHUB_MSP430_DEVICE (fu_vli_usbhub_msp430_device_get_type()) G_DECLARE_FINAL_TYPE(FuVliUsbhubMsp430Device, fu_vli_usbhub_msp430_device, FU, VLI_USBHUB_MSP430_DEVICE, FuDevice) FuDevice * fu_vli_usbhub_msp430_device_new(FuVliUsbhubDevice *parent); fwupd-2.0.10/plugins/vli/fu-vli-usbhub-pd-device.c000066400000000000000000000250241501337203100216620ustar00rootroot00000000000000/* * Copyright 2017 VIA Corporation * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-vli-pd-common.h" #include "fu-vli-pd-firmware.h" #include "fu-vli-struct.h" #include "fu-vli-usbhub-common.h" #include "fu-vli-usbhub-pd-device.h" struct _FuVliUsbhubPdDevice { FuDevice parent_instance; FuVliDeviceKind device_kind; }; G_DEFINE_TYPE(FuVliUsbhubPdDevice, fu_vli_usbhub_pd_device, FU_TYPE_DEVICE) static void fu_vli_usbhub_pd_device_to_string(FuDevice *device, guint idt, GString *str) { FuVliUsbhubPdDevice *self = FU_VLI_USBHUB_PD_DEVICE(device); fwupd_codec_string_append(str, idt, "DeviceKind", fu_vli_device_kind_to_string(self->device_kind)); fwupd_codec_string_append_hex(str, idt, "FwOffset", fu_vli_common_device_kind_get_offset(self->device_kind)); fwupd_codec_string_append_hex(str, idt, "FwSize", fu_vli_common_device_kind_get_size(self->device_kind)); } static gboolean fu_vli_usbhub_pd_device_setup(FuDevice *device, GError **error) { FuVliUsbhubPdDevice *self = FU_VLI_USBHUB_PD_DEVICE(device); FuVliUsbhubDevice *parent = FU_VLI_USBHUB_DEVICE(fu_device_get_parent(device)); const gchar *name; guint32 fwver; gsize bufsz = FU_STRUCT_VLI_PD_HDR_SIZE; g_autofree guint8 *buf = g_malloc0(bufsz); g_autoptr(GByteArray) st = NULL; /* sanity check */ if (parent == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no parent"); return FALSE; } /* legacy location */ if (!fu_vli_device_spi_read_block(FU_VLI_DEVICE(parent), VLI_USBHUB_FLASHMAP_ADDR_PD_LEGACY + VLI_USBHUB_PD_FLASHMAP_ADDR_LEGACY, buf, bufsz, error)) { g_prefix_error(error, "failed to read legacy PD header: "); return FALSE; } st = fu_struct_vli_pd_hdr_parse(buf, bufsz, 0x0, error); if (st == NULL) return FALSE; /* new location */ if (fu_struct_vli_pd_hdr_get_vid(st) != 0x2109) { g_debug("PD VID was 0x%04x trying new location", fu_struct_vli_pd_hdr_get_vid(st)); if (!fu_vli_device_spi_read_block(FU_VLI_DEVICE(parent), VLI_USBHUB_FLASHMAP_ADDR_PD + VLI_USBHUB_PD_FLASHMAP_ADDR, buf, bufsz, error)) { g_prefix_error(error, "failed to read PD header: "); return FALSE; } fu_struct_vli_pd_hdr_unref(st); st = fu_struct_vli_pd_hdr_parse(buf, bufsz, 0x0, error); if (st == NULL) return FALSE; } /* just empty space */ fwver = fu_struct_vli_pd_hdr_get_fwver(st); if (fwver == G_MAXUINT32) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no PD device header found"); return FALSE; } /* get version */ self->device_kind = fu_vli_pd_common_guess_device_kind(fwver); if (self->device_kind == FU_VLI_DEVICE_KIND_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "PD version invalid [0x%x]", fwver); return FALSE; } name = fu_vli_device_kind_to_string(self->device_kind); fu_device_set_name(device, name); /* use header to populate device info */ fu_device_set_version_raw(device, fwver); /* add standard GUIDs in order of priority */ fu_device_add_instance_u16(device, "VID", fu_struct_vli_pd_hdr_get_vid(st)); fu_device_add_instance_u16(device, "PID", fu_struct_vli_pd_hdr_get_pid(st)); fu_device_add_instance_u8(device, "APP", fwver & 0xff); fu_device_add_instance_str(device, "DEV", name); if (!fu_device_build_instance_id_full(device, FU_DEVICE_INSTANCE_FLAG_QUIRKS, error, "USB", "VID", NULL)) return FALSE; if (!fu_device_build_instance_id(device, error, "USB", "VID", "PID", NULL)) return FALSE; if (!fu_device_build_instance_id(device, error, "USB", "VID", "PID", "DEV", NULL)) return FALSE; if (!fu_device_build_instance_id(device, error, "USB", "VID", "PID", "APP", NULL)) return FALSE; /* these have a backup section */ if (fu_vli_common_device_kind_get_offset(self->device_kind) == VLI_USBHUB_FLASHMAP_ADDR_PD) fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SELF_RECOVERY); /* success */ return TRUE; } static gboolean fu_vli_usbhub_pd_device_reload(FuDevice *device, GError **error) { FuVliUsbhubDevice *parent = FU_VLI_USBHUB_DEVICE(fu_device_get_parent(device)); g_autoptr(FuDeviceLocker) locker = NULL; /* open parent device */ locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; return fu_vli_usbhub_pd_device_setup(device, error); } static FuFirmware * fu_vli_usbhub_pd_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuVliUsbhubPdDevice *self = FU_VLI_USBHUB_PD_DEVICE(device); FuVliDeviceKind device_kind; g_autoptr(FuFirmware) firmware = fu_vli_pd_firmware_new(); /* check is compatible with firmware */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; device_kind = fu_vli_pd_firmware_get_kind(FU_VLI_PD_FIRMWARE(firmware)); if (self->device_kind != device_kind) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware incompatible, got %s, expected %s", fu_vli_device_kind_to_string(device_kind), fu_vli_device_kind_to_string(self->device_kind)); return NULL; } /* we could check this against flags */ g_info("parsed version: %s", fu_firmware_get_version(firmware)); return g_steal_pointer(&firmware); } static GBytes * fu_vli_usbhub_pd_device_dump_firmware(FuDevice *device, FuProgress *progress, GError **error) { FuVliUsbhubDevice *parent = FU_VLI_USBHUB_DEVICE(fu_device_get_parent(device)); FuVliUsbhubPdDevice *self = FU_VLI_USBHUB_PD_DEVICE(device); g_autoptr(FuDeviceLocker) locker = NULL; /* open device */ locker = fu_device_locker_new(parent, error); if (locker == NULL) return NULL; /* read */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_READ); return fu_vli_device_spi_read(FU_VLI_DEVICE(parent), fu_vli_common_device_kind_get_offset(self->device_kind), fu_device_get_firmware_size_max(device), progress, error); } static gboolean fu_vli_usbhub_pd_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuVliUsbhubPdDevice *self = FU_VLI_USBHUB_PD_DEVICE(device); FuVliUsbhubDevice *parent = FU_VLI_USBHUB_DEVICE(fu_device_get_parent(device)); gsize bufsz = 0; const guint8 *buf; g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GBytes) fw = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 78, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 22, NULL); /* simple image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* open device */ locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; /* erase */ buf = g_bytes_get_data(fw, &bufsz); if (!fu_vli_device_spi_erase(FU_VLI_DEVICE(parent), fu_vli_common_device_kind_get_offset(self->device_kind), bufsz, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* write */ if (!fu_vli_device_spi_write(FU_VLI_DEVICE(parent), fu_vli_common_device_kind_get_offset(self->device_kind), buf, bufsz, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } /* reboot the parent FuVliUsbhubDevice if we update the FuVliUsbhubPdDevice */ static gboolean fu_vli_usbhub_pd_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuDevice *parent = fu_device_get_parent(device); g_autoptr(FuDeviceLocker) locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; return fu_device_attach_full(parent, progress, error); } static gboolean fu_vli_usbhub_pd_device_probe(FuDevice *device, GError **error) { FuVliUsbhubDevice *parent = FU_VLI_USBHUB_DEVICE(fu_device_get_parent(device)); if (parent != NULL) { fu_device_incorporate(device, FU_DEVICE(parent), FU_DEVICE_INCORPORATE_FLAG_PHYSICAL_ID); } return TRUE; } static void fu_vli_usbhub_pd_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static gchar * fu_vli_usbhub_pd_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_device_get_version_format(device)); } static void fu_vli_usbhub_pd_device_init(FuVliUsbhubPdDevice *self) { fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_USB_HUB); fu_device_add_protocol(FU_DEVICE(self), "com.vli.usbhub"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); fu_device_set_install_duration(FU_DEVICE(self), 15); /* seconds */ fu_device_set_logical_id(FU_DEVICE(self), "PD"); fu_device_set_summary(FU_DEVICE(self), "USB-C power delivery device"); } static void fu_vli_usbhub_pd_device_class_init(FuVliUsbhubPdDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_vli_usbhub_pd_device_to_string; device_class->probe = fu_vli_usbhub_pd_device_probe; device_class->setup = fu_vli_usbhub_pd_device_setup; device_class->reload = fu_vli_usbhub_pd_device_reload; device_class->attach = fu_vli_usbhub_pd_device_attach; device_class->dump_firmware = fu_vli_usbhub_pd_device_dump_firmware; device_class->write_firmware = fu_vli_usbhub_pd_device_write_firmware; device_class->prepare_firmware = fu_vli_usbhub_pd_device_prepare_firmware; device_class->convert_version = fu_vli_usbhub_pd_device_convert_version; device_class->set_progress = fu_vli_usbhub_pd_device_set_progress; } FuDevice * fu_vli_usbhub_pd_device_new(FuVliUsbhubDevice *parent) { FuVliUsbhubPdDevice *self = g_object_new(FU_TYPE_VLI_USBHUB_PD_DEVICE, "parent", parent, NULL); return FU_DEVICE(self); } fwupd-2.0.10/plugins/vli/fu-vli-usbhub-pd-device.h000066400000000000000000000007141501337203100216660ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-vli-usbhub-device.h" #define FU_TYPE_VLI_USBHUB_PD_DEVICE (fu_vli_usbhub_pd_device_get_type()) G_DECLARE_FINAL_TYPE(FuVliUsbhubPdDevice, fu_vli_usbhub_pd_device, FU, VLI_USBHUB_PD_DEVICE, FuDevice) FuDevice * fu_vli_usbhub_pd_device_new(FuVliUsbhubDevice *parent); fwupd-2.0.10/plugins/vli/fu-vli-usbhub-rtd21xx-device.c000066400000000000000000000411021501337203100225660ustar00rootroot00000000000000/* * Copyright 2017 VIA Corporation * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-vli-struct.h" #include "fu-vli-usbhub-rtd21xx-device.h" struct _FuVliUsbhubRtd21xxDevice { FuDevice parent_instance; }; G_DEFINE_TYPE(FuVliUsbhubRtd21xxDevice, fu_vli_usbhub_rtd21xx_device, FU_TYPE_DEVICE) #define I2C_WRITE_REQUEST 0xB2 #define I2C_READ_REQUEST 0xA5 #define I2C_DELAY_AFTER_SEND 5 /* ms */ #define UC_FOREGROUND_TARGET_ADDR 0x3A #define UC_FOREGROUND_STATUS 0x31 #define UC_FOREGROUND_OPCODE 0x33 #define UC_FOREGROUND_ISP_DATA_OPCODE 0x34 #define ISP_DATA_BLOCKSIZE 30 #define ISP_PACKET_SIZE 32 typedef enum { ISP_STATUS_BUSY = 0xBB, /* host must wait for device */ ISP_STATUS_IDLE_SUCCESS = 0x11, /* previous command was OK */ ISP_STATUS_IDLE_FAILURE = 0x12, /* previous command failed */ } IspStatus; typedef enum { ISP_CMD_ENTER_FW_UPDATE = 0x01, ISP_CMD_GET_PROJECT_ID_ADDR = 0x02, ISP_CMD_SYNC_IDENTIFY_CODE = 0x03, ISP_CMD_GET_FW_INFO = 0x04, ISP_CMD_FW_UPDATE_START = 0x05, ISP_CMD_FW_UPDATE_ISP_DONE = 0x06, ISP_CMD_FW_UPDATE_EXIT = 0x07, ISP_CMD_FW_UPDATE_RESET = 0x08, } IspCmd; static gboolean fu_vli_usbhub_rtd21xx_device_i2c_write(FuVliUsbhubDevice *self, guint8 target_addr, guint8 sub_addr, guint8 *data, gsize datasz, GError **error) { gsize bufsz = datasz + 2; g_autofree guint8 *buf = g_malloc0(bufsz); buf[0] = target_addr; buf[1] = sub_addr; if (!fu_memcpy_safe(buf, bufsz, 0x2, /* dst */ data, datasz, 0x0, /* src */ datasz, error)) return FALSE; fu_dump_raw(G_LOG_DOMAIN, "I2cWriteData", buf, datasz + 2); if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_HOST_TO_DEVICE, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, I2C_WRITE_REQUEST, 0x0000, 0x0000, buf, datasz + 2, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to write I2C @0x%02x:%02x: ", target_addr, sub_addr); return FALSE; } fu_device_sleep(FU_DEVICE(self), I2C_DELAY_AFTER_SEND); return TRUE; } static gboolean fu_vli_usbhub_rtd21xx_device_i2c_read(FuVliUsbhubDevice *self, guint8 target_addr, guint8 sub_addr, guint8 *data, gsize datasz, GError **error) { if (!fu_usb_device_control_transfer(FU_USB_DEVICE(self), FU_USB_DIRECTION_DEVICE_TO_HOST, FU_USB_REQUEST_TYPE_VENDOR, FU_USB_RECIPIENT_DEVICE, I2C_READ_REQUEST, 0x0000, ((guint16)sub_addr << 8) + target_addr, data, datasz, NULL, FU_VLI_DEVICE_TIMEOUT, NULL, error)) { g_prefix_error(error, "failed to read I2C: "); return FALSE; } fu_dump_raw(G_LOG_DOMAIN, "I2cReadData", data, datasz); return TRUE; } static gboolean fu_vli_usbhub_rtd21xx_device_read_status_raw(FuVliUsbhubRtd21xxDevice *self, guint8 *status, GError **error) { FuVliUsbhubDevice *parent = FU_VLI_USBHUB_DEVICE(fu_device_get_parent(FU_DEVICE(self))); guint8 buf[] = {0x00}; if (!fu_vli_usbhub_rtd21xx_device_i2c_read(parent, UC_FOREGROUND_TARGET_ADDR, UC_FOREGROUND_STATUS, buf, sizeof(buf), error)) return FALSE; if (status != NULL) *status = buf[0]; return TRUE; } static gboolean fu_vli_usbhub_rtd21xx_device_read_status_cb(FuDevice *device, gpointer user_data, GError **error) { FuVliUsbhubRtd21xxDevice *self = FU_VLI_USBHUB_RTD21XX_DEVICE(device); guint8 status = 0xfd; if (!fu_vli_usbhub_rtd21xx_device_read_status_raw(self, &status, error)) return FALSE; if (status == ISP_STATUS_BUSY) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "status was 0x%02x", status); return FALSE; } return TRUE; } static gboolean fu_vli_usbhub_rtd21xx_device_read_status(FuVliUsbhubRtd21xxDevice *self, guint8 *status, GError **error) { return fu_device_retry(FU_DEVICE(self), fu_vli_usbhub_rtd21xx_device_read_status_cb, 4200, status, error); } static gboolean fu_vli_usbhub_rtd21xx_device_ensure_version_unlocked(FuVliUsbhubRtd21xxDevice *self, GError **error) { FuVliUsbhubDevice *parent = FU_VLI_USBHUB_DEVICE(fu_device_get_parent(FU_DEVICE(self))); guint8 buf_rep[7] = {0x00}; guint8 buf_req[] = {ISP_CMD_GET_FW_INFO}; g_autofree gchar *version = NULL; if (!fu_vli_usbhub_rtd21xx_device_i2c_write(parent, UC_FOREGROUND_TARGET_ADDR, UC_FOREGROUND_OPCODE, buf_req, sizeof(buf_req), error)) { g_prefix_error(error, "failed to get version number: "); return FALSE; } /* wait for device ready */ fu_device_sleep(FU_DEVICE(self), 300); if (!fu_vli_usbhub_rtd21xx_device_i2c_read(parent, UC_FOREGROUND_TARGET_ADDR, 0x00, buf_rep, sizeof(buf_rep), error)) { g_prefix_error(error, "failed to get version number: "); return FALSE; } /* set version */ version = g_strdup_printf("%u.%u", buf_rep[1], buf_rep[2]); fu_device_set_version(FU_DEVICE(self), version); return TRUE; } static gboolean fu_vli_usbhub_rtd21xx_device_setup(FuDevice *device, GError **error) { FuVliUsbhubRtd21xxDevice *self = FU_VLI_USBHUB_RTD21XX_DEVICE(device); g_autoptr(FuDeviceLocker) locker = NULL; /* get version */ locker = fu_device_locker_new_full(device, (FuDeviceLockerFunc)fu_device_detach, (FuDeviceLockerFunc)fu_device_attach, error); if (locker == NULL) return FALSE; if (!fu_vli_usbhub_rtd21xx_device_ensure_version_unlocked(self, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_vli_usbhub_rtd21xx_device_detach_raw(FuVliUsbhubRtd21xxDevice *self, GError **error) { FuVliUsbhubDevice *parent = FU_VLI_USBHUB_DEVICE(fu_device_get_parent(FU_DEVICE(self))); guint8 buf[] = {0x03}; if (!fu_vli_usbhub_rtd21xx_device_i2c_write(parent, 0x6A, 0x31, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to detach: "); return FALSE; } return TRUE; } static gboolean fu_vli_usbhub_rtd21xx_device_detach_cb(FuDevice *device, gpointer user_data, GError **error) { FuVliUsbhubRtd21xxDevice *self = FU_VLI_USBHUB_RTD21XX_DEVICE(device); guint8 status = 0xfe; if (!fu_vli_usbhub_rtd21xx_device_detach_raw(self, error)) return FALSE; if (!fu_vli_usbhub_rtd21xx_device_read_status_raw(self, &status, error)) return FALSE; if (status != ISP_STATUS_IDLE_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "detach status was 0x%02x", status); return FALSE; } return TRUE; } static gboolean fu_vli_usbhub_rtd21xx_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuVliUsbhubDevice *parent = FU_VLI_USBHUB_DEVICE(fu_device_get_parent(device)); g_autoptr(FuDeviceLocker) locker = NULL; /* open device */ locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; if (!fu_device_retry(device, fu_vli_usbhub_rtd21xx_device_detach_cb, 100, NULL, error)) return FALSE; /* success */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); return TRUE; } static gboolean fu_vli_usbhub_rtd21xx_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuVliUsbhubDevice *parent = FU_VLI_USBHUB_DEVICE(fu_device_get_parent(device)); guint8 buf[] = {ISP_CMD_FW_UPDATE_RESET}; g_autoptr(FuDeviceLocker) locker = NULL; /* open device */ locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; if (!fu_vli_usbhub_rtd21xx_device_i2c_write(parent, UC_FOREGROUND_TARGET_ADDR, UC_FOREGROUND_OPCODE, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to attach: "); return FALSE; } /* success */ fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); return TRUE; } static gboolean fu_vli_usbhub_rtd21xx_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuVliUsbhubDevice *parent = FU_VLI_USBHUB_DEVICE(fu_device_get_parent(device)); FuVliUsbhubRtd21xxDevice *self = FU_VLI_USBHUB_RTD21XX_DEVICE(device); guint32 project_addr; guint8 project_id_count; guint8 read_buf[10] = {0}; guint8 write_buf[ISP_PACKET_SIZE] = {0}; g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 10, "enable-isp"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 50, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 10, NULL); /* open device */ locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; /* simple image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) return FALSE; /* enable ISP high priority */ write_buf[0] = ISP_CMD_ENTER_FW_UPDATE; write_buf[1] = 0x01; if (!fu_vli_usbhub_rtd21xx_device_i2c_write(parent, UC_FOREGROUND_TARGET_ADDR, UC_FOREGROUND_OPCODE, write_buf, 2, error)) { g_prefix_error(error, "failed to enable ISP: "); return FALSE; } if (!fu_vli_usbhub_rtd21xx_device_read_status(self, NULL, error)) return FALSE; /* get project ID address */ write_buf[0] = ISP_CMD_GET_PROJECT_ID_ADDR; if (!fu_vli_usbhub_rtd21xx_device_i2c_write(parent, UC_FOREGROUND_TARGET_ADDR, UC_FOREGROUND_OPCODE, write_buf, 1, error)) { g_prefix_error(error, "failed to get project ID address: "); return FALSE; } /* read back 6 bytes data */ fu_device_sleep(device, I2C_DELAY_AFTER_SEND * 40); if (!fu_vli_usbhub_rtd21xx_device_i2c_read(parent, UC_FOREGROUND_TARGET_ADDR, UC_FOREGROUND_STATUS, read_buf, 6, error)) { g_prefix_error(error, "failed to read project ID: "); return FALSE; } if (read_buf[0] != ISP_STATUS_IDLE_SUCCESS) { g_prefix_error(error, "failed project ID with error 0x%02x: ", read_buf[0]); return FALSE; } /* verify project ID */ if (!fu_memread_uint32_safe(read_buf, sizeof(read_buf), 0x1, &project_addr, G_BIG_ENDIAN, error)) return FALSE; project_id_count = read_buf[5]; write_buf[0] = ISP_CMD_SYNC_IDENTIFY_CODE; if (!fu_input_stream_read_safe(stream, write_buf, sizeof(write_buf), 0x1, /* dst */ project_addr, /* src */ project_id_count, error)) { g_prefix_error(error, "failed to write project ID from 0x%04x: ", project_addr); return FALSE; } if (!fu_vli_usbhub_rtd21xx_device_i2c_write(parent, UC_FOREGROUND_TARGET_ADDR, UC_FOREGROUND_OPCODE, write_buf, project_id_count + 1, error)) { g_prefix_error(error, "failed to send fw update start cmd: "); return FALSE; } if (!fu_vli_usbhub_rtd21xx_device_read_status(self, NULL, error)) return FALSE; /* background FW update start command */ write_buf[0] = ISP_CMD_FW_UPDATE_START; fu_memwrite_uint16(write_buf + 1, ISP_DATA_BLOCKSIZE, G_BIG_ENDIAN); if (!fu_vli_usbhub_rtd21xx_device_i2c_write(parent, UC_FOREGROUND_TARGET_ADDR, UC_FOREGROUND_OPCODE, write_buf, 3, error)) { g_prefix_error(error, "failed to send fw update start cmd: "); return FALSE; } fu_progress_step_done(progress); /* send data */ chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, ISP_DATA_BLOCKSIZE, error); if (chunks == NULL) return FALSE; for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_vli_usbhub_rtd21xx_device_read_status(self, NULL, error)) return FALSE; if (!fu_vli_usbhub_rtd21xx_device_i2c_write(parent, UC_FOREGROUND_TARGET_ADDR, UC_FOREGROUND_ISP_DATA_OPCODE, fu_chunk_get_data_out(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "failed to write @0x%04x: ", (guint)fu_chunk_get_address(chk)); return FALSE; } /* update progress */ fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)i + 1, (gsize)fu_chunk_array_length(chunks)); } fu_progress_step_done(progress); /* update finish command */ if (!fu_vli_usbhub_rtd21xx_device_read_status(self, NULL, error)) return FALSE; write_buf[0] = ISP_CMD_FW_UPDATE_ISP_DONE; if (!fu_vli_usbhub_rtd21xx_device_i2c_write(parent, UC_FOREGROUND_TARGET_ADDR, UC_FOREGROUND_OPCODE, write_buf, 1, error)) { g_prefix_error(error, "failed update finish cmd: "); return FALSE; } fu_progress_step_done(progress); /* exit background-fw mode */ if (!fu_vli_usbhub_rtd21xx_device_read_status(self, NULL, error)) return FALSE; write_buf[0] = ISP_CMD_FW_UPDATE_EXIT; if (!fu_vli_usbhub_rtd21xx_device_i2c_write(parent, UC_FOREGROUND_TARGET_ADDR, UC_FOREGROUND_OPCODE, write_buf, 1, error)) { g_prefix_error(error, "FwUpdate exit: "); return FALSE; } /* the device needs some time to restart with the new firmware before * it can be queried again */ fu_device_sleep_full(device, 20000, progress); /* ms */ /* success */ fu_progress_step_done(progress); return TRUE; } static gboolean fu_vli_usbhub_rtd21xx_device_reload(FuDevice *device, GError **error) { FuVliUsbhubDevice *parent = FU_VLI_USBHUB_DEVICE(fu_device_get_parent(device)); g_autoptr(FuDeviceLocker) locker = NULL; /* open parent device */ locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; return fu_vli_usbhub_rtd21xx_device_setup(device, error); } static gboolean fu_vli_usbhub_rtd21xx_device_probe(FuDevice *device, GError **error) { FuVliDeviceKind device_kind = FU_VLI_DEVICE_KIND_RTD21XX; FuVliUsbhubDevice *parent = FU_VLI_USBHUB_DEVICE(fu_device_get_parent(device)); fu_device_set_name(device, fu_vli_device_kind_to_string(device_kind)); if (parent != NULL) { fu_device_incorporate(device, FU_DEVICE(parent), FU_DEVICE_INCORPORATE_FLAG_PHYSICAL_ID); } /* add instance ID */ fu_device_add_instance_str(device, "I2C", fu_vli_device_kind_to_string(device_kind)); return fu_device_build_instance_id(device, error, "USB", "VID", "PID", "I2C", NULL); } static void fu_vli_usbhub_rtd21xx_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static void fu_vli_usbhub_rtd21xx_device_init(FuVliUsbhubRtd21xxDevice *self) { fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_VIDEO_DISPLAY); fu_device_add_protocol(FU_DEVICE(self), "com.vli.i2c"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_DUAL_IMAGE); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PAIR); fu_device_set_install_duration(FU_DEVICE(self), 100); /* seconds */ fu_device_set_logical_id(FU_DEVICE(self), "I2C"); fu_device_retry_set_delay(FU_DEVICE(self), 30); /* ms */ } static void fu_vli_usbhub_rtd21xx_device_class_init(FuVliUsbhubRtd21xxDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->probe = fu_vli_usbhub_rtd21xx_device_probe; device_class->setup = fu_vli_usbhub_rtd21xx_device_setup; device_class->reload = fu_vli_usbhub_rtd21xx_device_reload; device_class->attach = fu_vli_usbhub_rtd21xx_device_attach; device_class->detach = fu_vli_usbhub_rtd21xx_device_detach; device_class->write_firmware = fu_vli_usbhub_rtd21xx_device_write_firmware; device_class->set_progress = fu_vli_usbhub_rtd21xx_device_set_progress; } FuDevice * fu_vli_usbhub_rtd21xx_device_new(FuVliUsbhubDevice *parent) { FuVliUsbhubRtd21xxDevice *self = g_object_new(FU_TYPE_VLI_USBHUB_RTD21XX_DEVICE, "parent", parent, NULL); return FU_DEVICE(self); } fwupd-2.0.10/plugins/vli/fu-vli-usbhub-rtd21xx-device.h000066400000000000000000000007521501337203100226010ustar00rootroot00000000000000/* * Copyright 2019 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-vli-usbhub-device.h" #define FU_TYPE_VLI_USBHUB_RTD21XX_DEVICE (fu_vli_usbhub_rtd21xx_device_get_type()) G_DECLARE_FINAL_TYPE(FuVliUsbhubRtd21xxDevice, fu_vli_usbhub_rtd21xx_device, FU, VLI_USBHUB_RTD21XX_DEVICE, FuDevice) FuDevice * fu_vli_usbhub_rtd21xx_device_new(FuVliUsbhubDevice *parent); fwupd-2.0.10/plugins/vli/fu-vli.rs000066400000000000000000000034711501337203100167420ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(Parse, ParseStream)] #[repr(C, packed)] struct FuStructVliPdHdr { fwver: u32be, vid: u16le, pid: u16le, } #[derive(New, Parse, ParseStream, ToString)] #[repr(C, packed)] struct FuStructVliUsbhubHdr { dev_id: u16be, strapping1: u8, strapping2: u8, usb3_fw_addr: u16be, usb3_fw_sz: u16be, usb2_fw_addr: u16be, usb2_fw_sz: u16be, usb3_fw_addr_high: u8, usb3_fw_sz_high: u8, _unknown_0e: [u8; 2], usb2_fw_addr_high: u8, _unknown_11: [u8; 10], inverse_pe41: u8, prev_ptr: u8, // addr / 0x20 next_ptr: u8, // addr / 0x20 variant: u8, checksum: u8, } #[derive(ToString, FromString)] enum FuVliDeviceKind { Unknown = 0x0, Vl100 = 0x0100, Vl101 = 0x0101, Vl102 = 0x0102, Vl103 = 0x0103, Vl104 = 0x0104, Vl105 = 0x0105, Vl106 = 0x0106, Vl107 = 0x0107, Vl108 = 0x0108, Vl109 = 0x0109, Vl120 = 0x0120, Vl122 = 0x0122, Vl210 = 0x0210, Vl211 = 0x0211, Vl212 = 0x0212, Vl650 = 0x0650, Vl810 = 0x0810, Vl811 = 0x0811, Vl811pb0 = 0x8110, Vl811pb3 = 0x8113, Vl812b0 = 0xa812, Vl812b3 = 0xb812, Vl812q4s = 0xc812, Vl813 = 0x0813, Vl815 = 0x0815, Vl817 = 0x0817, Vl817s = 0xa817, Vl819q7 = 0xa819, // guessed Vl819q8 = 0xb819, // guessed Vl820q7 = 0xa820, Vl820q8 = 0xb820, Vl821q7 = 0xa821, // guessed Vl821q8 = 0xb821, // guessed Vl822q5 = 0x0822, // guessed Vl822q7 = 0xa822, // guessed Vl822q8 = 0xb822, // guessed Vl822t = 0xc822, // guessed Vl822c0 = 0xd822, Vl830 = 0x0830, Vl832 = 0x0832, Msp430 = 0xf430, // guessed Ps186 = 0xf186, // guessed Rtd21xx = 0xff00, // guessed } fwupd-2.0.10/plugins/vli/meson.build000066400000000000000000000032671501337203100173370ustar00rootroot00000000000000plugins += {meson.current_source_dir().split('/')[-1]: true} cargs = ['-DG_LOG_DOMAIN="FuPluginVliUsbhub"'] plugin_quirks += files([ 'vli-bizlink.quirk', 'vli-cablematters.quirk', 'vli-dell.quirk', 'vli-fujitsu.quirk', 'vli-goodway.quirk', 'vli-hyper.quirk', 'vli-lenovo.quirk', 'vli-luxshare.quirk', 'vli-samsung.quirk', ]) plugin_builtin_vli = static_library('fu_plugin_vli', rustgen.process('fu-vli.rs'), sources: [ 'fu-vli-plugin.c', 'fu-vli-common.c', 'fu-vli-device.c', 'fu-vli-pd-common.c', 'fu-vli-pd-device.c', 'fu-vli-pd-firmware.c', 'fu-vli-pd-parade-device.c', 'fu-vli-usbhub-device.c', 'fu-vli-usbhub-firmware.c', 'fu-vli-usbhub-i2c-common.c', 'fu-vli-usbhub-msp430-device.c', 'fu-vli-usbhub-pd-device.c', 'fu-vli-usbhub-rtd21xx-device.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) plugin_builtins += plugin_builtin_vli device_tests += files( 'tests/bizlink-no-sku-vli.json', 'tests/hyper-no-sku-vli.json', 'tests/lenovo-03x7168.json', 'tests/lenovo-03x7605.json', 'tests/lenovo-03x7608-vli.json', 'tests/lenovo-40au0065-vli.json', 'tests/lenovo-GX90T33021-vli.json', ) if get_option('tests') e = executable( 'vli-self-test', rustgen.process('fu-vli.rs'), sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: plugin_deps, link_with: [ plugin_libs, plugin_builtin_vli, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: cargs, ) test('vli-self-test', e, env: env) # added to installed-tests endif fwupd-2.0.10/plugins/vli/tests/000077500000000000000000000000001501337203100163275ustar00rootroot00000000000000fwupd-2.0.10/plugins/vli/tests/bizlink-no-sku-vli.json000066400000000000000000000020241501337203100226640ustar00rootroot00000000000000{ "name": "BizLink Cayenne Hub [VL822+VL103]", "interactive": false, "steps": [ { "url": "e0d6f62140663744f114d59b1ec48dd3e4743006bb06d0643efe178b8bdb2a04-BizLink-Cayenne_New.cab", "components": [ { "name": "tier1", "version": "106.83", "guids": [ "a0eff862-6b0b-5571-91ec-a0c4bb25b539" ] }, { "name": "tier3", "version": "138.2.5.18", "guids": [ "a7927038-e2df-5209-88db-2e28bcd3cf8e" ] } ] }, { "url": "baf2f1a78334b7722d913ffa468240f728a09d91d0d2f755ff5515d4fed111c1-BizLink-Cayenne_Old.cab", "components": [ { "name": "tier1", "version": "06.83", "guids": [ "a0eff862-6b0b-5571-91ec-a0c4bb25b539" ] }, { "name": "tier3", "version": "122.2.5.18", "guids": [ "a7927038-e2df-5209-88db-2e28bcd3cf8e" ] } ] } ] } fwupd-2.0.10/plugins/vli/tests/hyper-no-sku-vli.json000066400000000000000000000020151501337203100223510ustar00rootroot00000000000000{ "name": "Hyper USB-C Hub [VL817+VL103]", "interactive": false, "steps": [ { "url": "ecfebc47d63a5319e35da39bd6124b80e90138a208bc9134c9d1b256b232a704-Hyper-USB-C_Hub.cab", "components": [ { "name": "tier1", "version": "90.83", "guids": [ "a476b1bb-9f8e-5ad4-a0ed-afadb52334fc" ] }, { "name": "tier3", "version": "154.36.9.55", "guids": [ "0a120f99-b0f4-52af-9af9-e13155634370" ] } ] }, { "url": "1694cceda16068b24d7627caace4bd373ecae73aca891f610dec0fe5a4d1207c-Hyper-USB-C_Hub_Old.cab", "components": [ { "name": "tier1", "version": "80.83", "guids": [ "a476b1bb-9f8e-5ad4-a0ed-afadb52334fc" ] }, { "name": "tier3", "version": "138.36.9.55", "guids": [ "0a120f99-b0f4-52af-9af9-e13155634370" ] } ] } ] } fwupd-2.0.10/plugins/vli/tests/lenovo-03x7168.json000066400000000000000000000012431501337203100214620ustar00rootroot00000000000000{ "name": "Lenovo USB-C to HDMI (with power) [VL100]", "interactive": false, "steps": [ { "url": "9ef0bb237baf8043f3ff16db5a8dd23bfd63fc24ea0eca2a6af3285792aa283b-Lenovo-USB-C_to_HDMI.cab", "components": [ { "version": "130.4.22.1", "guids": [ "eca16353-163f-570b-9e0a-8329045fdcff" ] } ] }, { "url": "5b06a36aa0f2b99fc33f43a26a553d95c2d8ac46a7f5a2e45a840570456fe29b-Lenovo-USB-C_to_HDMI.cab", "components": [ { "version": "130.4.23.1", "guids": [ "eca16353-163f-570b-9e0a-8329045fdcff" ] } ] } ] } fwupd-2.0.10/plugins/vli/tests/lenovo-03x7605.json000066400000000000000000000012411501337203100214540ustar00rootroot00000000000000{ "name": "Lenovo USB-C to HDMI (no power) [VL103]", "interactive": false, "steps": [ { "url": "61b393d8e27503746bae9a16dccf54929b9f1a0d1b5106334933a7313596c00c-Lenovo-USB-C_to_HDMI.cab", "components": [ { "version": "153.84.6.1", "guids": [ "d0909dd3-d140-5da8-85fd-4aa5b2dcee5d" ] } ] }, { "url": "8be7eea7db239766cf92adf2145138c9929fd953d6d36e39d60c9dd6425638de-Lenovo-USB-C_to_HDMI.cab", "components": [ { "version": "153.84.7.1", "guids": [ "d0909dd3-d140-5da8-85fd-4aa5b2dcee5d" ] } ] } ] } fwupd-2.0.10/plugins/vli/tests/lenovo-03x7608-vli.json000066400000000000000000000011041501337203100222450ustar00rootroot00000000000000{ "name": "Lenovo Travel Hub Gen2 [VL817]", "interactive": false, "steps": [ { "url": "ebfda3c96543d6d7ad97a972344f4d34a14549c535095bfbb1860115a88e6ff6-Lenovo-TravalHub-2020-12-11-153442.cab", "components": [ { "name": "tier1", "version": "4.74", "guids": [ "3fc55e47-f57a-55bd-9f41-ac60280bd689" ] }, { "name": "tier3", "version": "138.04.72.18", "guids": [ "3ae6610b-5c33-5714-96e3-05735eb9b2a5" ] } ] } ] } fwupd-2.0.10/plugins/vli/tests/lenovo-40au0065-vli.json000066400000000000000000000025151501337203100224010ustar00rootroot00000000000000{ "name": "Lenovo USB-C Mini Dock", "interactive": false, "steps": [ { "url": "3b183e21869e4fce8bc81b3357de2fd1ee47b35e272e26169ebaee88faa39e03-Lenovo-Mini_Dock_New.cab", "components": [ { "name": "tier1", "version": "4.154", "guids": [ "f281c1df-c3d5-5f8a-984d-e9548ffc95fe" ] }, { "name": "tier2", "version": "4.43", "guids": [ "d636c717-44c4-5fcf-9d7f-b96f9c5f6608" ] }, { "name": "tier3", "version": "138.4.24.38", "guids": [ "3ae6610b-5c33-5714-96e3-05735eb9b2a5" ] } ] }, { "url": "f55a307af1dc66d46bc12460ced828b31b2ee2a78c1d58d4f474958c2c6d134e-Lenovo-Mini_Dock_Old.cab", "components": [ { "name": "tier1", "version": "4.94", "guids": [ "f281c1df-c3d5-5f8a-984d-e9548ffc95fe" ] }, { "name": "tier2", "version": "4.33", "guids": [ "d636c717-44c4-5fcf-9d7f-b96f9c5f6608" ] }, { "name": "tier3", "version": "138.4.23.38", "guids": [ "3ae6610b-5c33-5714-96e3-05735eb9b2a5" ] } ] } ] } fwupd-2.0.10/plugins/vli/tests/lenovo-GX90T33021-vli.json000066400000000000000000000017031501337203100225170ustar00rootroot00000000000000{ "name": "Lenovo Travel Hub 1in3 [VL211]", "interactive": false, "steps": [ { "url": "2004603b40bd529d85c2fcfcf9d50d77b47229e6564ef0ea9c03633bdccff94d-Lenovo-Travel_Hub_1in3_New.cab", "emulation-url": "de48f2281354c4479b8b2302cf85b9643b0ca248fcf4beb19a71f5aba7927278-Lenovo-Travel_Hub_1in3_New.zip", "components": [ { "name": "tier1", "version": "44.33", "guids": [ "7636b85e-d79f-5d30-a329-458957958b88" ] } ] }, { "url": "5fb4f4dce233626558806c2b7474d3b5ed4f500f6013aa3b376a54322642d449-Lenovo-Travel_Hub_1in3_Old.cab", "emulation-url": "58221c63de8cdec9b4db3cc72b1e0303b6cfee80b7e7965edad9816c124b0cd7-Lenovo-Travel_Hub_1in3_Old.zip", "components": [ { "name": "tier1", "version": "4.33", "guids": [ "7636b85e-d79f-5d30-a329-458957958b88" ] } ] } ] } fwupd-2.0.10/plugins/vli/vli-bizlink.quirk000066400000000000000000000005741501337203100205020ustar00rootroot00000000000000# BizLink USB-C Cayenne [USB\VID_06C4&PID_C304] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3,has-shared-spi-pd CounterpartGuid = USB\VID_06C4&PID_C303 [USB\VID_06C4&PID_C303] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 ParentGuid = USB\VID_06C4&PID_C304 [USB\VID_06C4&PID_C305] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 ParentGuid = USB\VID_06C4&PID_C304 fwupd-2.0.10/plugins/vli/vli-cablematters.quirk000066400000000000000000000001131501337203100214730ustar00rootroot00000000000000# Cable Matters [USB\VID_2BD5&PID_5240] Plugin = vli GType = FuVliPdDevice fwupd-2.0.10/plugins/vli/vli-dell.quirk000066400000000000000000000001611501337203100177500ustar00rootroot00000000000000# Dell DA300 [USB\VID_2109&PID_0820&REV_3003] Flags = no-probe [USB\VID_2109&PID_2820&REV_3003] Flags = no-probe fwupd-2.0.10/plugins/vli/vli-fujitsu.quirk000066400000000000000000000015231501337203100205240ustar00rootroot00000000000000# Fujitsu B2711 Hub [USB\VID_0BF8&PID_105D] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3 CounterpartGuid = USB\VID_0BF8&PID_105E [USB\VID_0BF8&PID_105E] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 ParentGuid = USB\VID_0BF8&PID_105D # Fujitsu P2711 Multi-Hub [USB\VID_0BF8&PID_1051] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3,has-shared-spi-pd CounterpartGuid = USB\VID_0BF8&PID_1052 [USB\VID_0BF8&PID_1052] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 ParentGuid = USB\VID_0BF8&PID_1051 [USB\VID_0BF8&PID_1054] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3 ParentGuid = USB\VID_0BF8&PID_1051 CounterpartGuid = USB\VID_0BF8&PID_1055 [USB\VID_0BF8&PID_1055] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 ParentGuid = USB\VID_0BF8&PID_1051 # Fujitsu [USB\VID_0BF8&PID_103C] Plugin = vli GType = FuVliPdDevice fwupd-2.0.10/plugins/vli/vli-goodway.quirk000066400000000000000000000011741501337203100205060ustar00rootroot00000000000000# Goodway Minibons [USB\VID_065F&PID_F817] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3 CounterpartGuid = USB\VID_065F&PID_F816 [USB\VID_065F&PID_F816] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 ParentGuid = USB\VID_065F&PID_F817 # Goodway ASUS DC201 [USB\VID_0B05&PID_1BA2] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3,has-shared-spi-pd CounterpartGuid = USB\VID_0B05&PID_1BA8 [USB\VID_0B05&PID_1BA8] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 ParentGuid = USB\VID_0B05&PID_1BA2 # VL105 PD Controller [USB\VID_0B05&PID_1BA9] Plugin = vli GType = FuVliPdDevice Flags = skips-rom VliDeviceKind = vl105 fwupd-2.0.10/plugins/vli/vli-hyper.quirk000066400000000000000000000004071501337203100201620ustar00rootroot00000000000000# Hyper USB-C Hub [USB\VID_2D01&PID_0385] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3,has-shared-spi-pd CounterpartGuid = USB\VID_2D01&PID_0382 [USB\VID_2D01&PID_0382] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 ParentGuid = USB\VID_2D01&PID_0385 fwupd-2.0.10/plugins/vli/vli-lenovo.quirk000066400000000000000000000155101501337203100203360ustar00rootroot00000000000000# Lenovo CS18 Ultra Dock [USB\VID_17EF&PID_3070] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3 CounterpartGuid = USB\VID_17EF&PID_3071 [USB\VID_17EF&PID_3072&HUB_0006] ParentGuid = USB\VID_17EF&PID_3072&HUB_0002 [USB\VID_17EF&PID_3071] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 [USB\VID_17EF&PID_3071&HUB_0002] ParentGuid = USB\VID_17EF&PID_3071&HUB_0006 # Lenovo CS18 Pro and Basic Dock [USB\VID_17EF&PID_3072] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3 CounterpartGuid = USB\VID_17EF&PID_3073 [USB\VID_17EF&PID_3073] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 [USB\VID_17EF&PID_3073&HUB_0006] ParentGuid = USB\VID_17EF&PID_3073&HUB_0002 # Lenovo TR Dock [USB\VID_17EF&PID_307F&HUB_0002] ParentGuid = TBT-01081720 [USB\VID_17EF&PID_307F] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3 CounterpartGuid = USB\VID_17EF&PID_3080 [USB\VID_17EF&PID_307F&HUB_0002] Flags = usb3,has-msp430 [USB\VID_17EF&PID_307F&HUB_0006] ParentGuid = USB\VID_17EF&PID_307F&HUB_0002 [USB\VID_17EF&PID_3080] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 [USB\VID_17EF&PID_3080&HUB_06] ParentGuid = USB\VID_17EF&PID_3080&HUB_20 [USB\VID_17EF&PID_3080&HUB_20] ParentGuid = TBT-01081720 # Lenovo CS13 KG Dock [USB\VID_17EF&PID_1010] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2,usb3 # Lenovo CS13 GD Dock [USB\VID_17EF&PID_1012] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2,usb3 # Lenovo CS13 MO Dock [USB\VID_17EF&PID_1013] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2,usb3 # Lenovo USB3 Ultra Dock [USB\VID_17EF&PID_1014] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2,usb3 [USB\VID_17EF&PID_1015] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2,usb3 ParentGuid = USB\VID_17EF&PID_1014 # Lenovo USB3 Pro Dock [USB\VID_17EF&PID_1016] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2,usb3 [USB\VID_17EF&PID_1018] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2,usb3 ParentGuid = USB\VID_17EF&PID_1016 # Lenovo Workstation D40 [USB\VID_17EF&PID_1033] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2,usb3 # Lenovo Workstation S40 [USB\VID_17EF&PID_1034] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2,usb3 # Lenovo Workstation v40 [USB\VID_17EF&PID_1035] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2,usb3 # Lenovo One Link Plus [USB\VID_17EF&PID_1018] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2,usb3 [USB\VID_17EF&PID_1019] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2,usb3 ParentGuid = USB\VID_17EF&PID_1018 # Lenovo Hybrid dock [USB\VID_17EF&PID_A356] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3 CounterpartGuid = USB\VID_17EF&PID_1028 [USB\VID_17EF&PID_1028] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 [USB\VID_17EF&PID_A357] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3 ParentGuid = USB\VID_17EF&PID_A356 CounterpartGuid = USB\VID_17EF&PID_1029 [USB\VID_17EF&PID_1029] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 ParentGuid = USB\VID_17EF&PID_1028 # Lenovo Travel hub [USB\VID_17EF&PID_7216] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3 CounterpartGuid = USB\VID_17EF&PID_7224 [USB\VID_17EF&PID_7224] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 ParentGuid = USB\VID_17EF&PID_7216 # Lenovo Travel hub Gen2 [USB\VID_17EF&PID_721D] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3,has-shared-spi-pd CounterpartGuid = USB\VID_17EF&PID_7225 [USB\VID_17EF&PID_7225] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 ParentGuid = USB\VID_17EF&PID_721D [USB\VID_17EF&PID_721C] Plugin = vli GType = FuVliUsbhubDevice Flags = has-rtd21xx ParentGuid = USB\VID_17EF&PID_721D [USB\VID_17EF&PID_721C&I2C_REALTEK] Name = RTD2181S # Lenovo USB-C Mini dock [USB\VID_17EF&PID_3094] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3,has-shared-spi-pd,attach-with-gpiob CounterpartGuid = USB\VID_17EF&PID_3095 [USB\VID_17EF&PID_3095] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2,attach-with-gpiob ParentGuid = USB\VID_17EF&PID_3094 [USB\VID_17EF&PID_3097] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 ParentGuid = USB\VID_17EF&PID_3094 [USB\VID_17EF&PID_3093] Plugin = vli GType = FuVliUsbhubDevice Flags = has-rtd21xx ParentGuid = USB\VID_17EF&PID_3094 [USB\VID_17EF&PID_3093&I2C_REALTEK] Name = RTD2181S [USB\VID_17EF&PID_721C&APP_26] ProxyGuid = USB\VID_17EF&PID_3094 FirmwareSize = 0x8000 # Lenovo Travel Hub 1in3 [USB\VID_17EF&PID_7228] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3 CounterpartGuid = USB\VID_17EF&PID_7236 [USB\VID_17EF&PID_7236] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 ParentGuid = USB\VID_17EF&PID_7228 # Lenovo USB-C 7-in-1 Hub [USB\VID_17EF&PID_722A] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3,has-shared-spi-pd CounterpartGuid = USB\VID_17EF&PID_7229 [USB\VID_17EF&PID_7229] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 ParentGuid = USB\VID_17EF&PID_722A # Lenovo USB-C 7-in-1 WWCB Dock [USB\VID_17EF&PID_10CA] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3,has-shared-spi-pd CounterpartGuid = USB\VID_17EF&PID_10CB [USB\VID_17EF&PID_10CB] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 ParentGuid = USB\VID_17EF&PID_10CA # Lenovo USB-C to 4 USB-A Hub [USB\VID_17EF&PID_1039] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3,has-shared-spi-pd,attach-with-gpiob CounterpartGuid = USB\VID_17EF&PID_103A [USB\VID_17EF&PID_103A] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 ParentGuid = USB\VID_17EF&PID_1039 # Lenovo Gen2 dock [USB\VID_17EF&PID_A391] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3 CounterpartGuid = USB\VID_17EF&PID_A392 [USB\VID_17EF&PID_A392] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 ParentGuid = USB\VID_17EF&PID_A391 [USB\VID_17EF&PID_A393] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3 ParentGuid = USB\VID_17EF&PID_A391 CounterpartGuid = USB\VID_17EF&PID_A394 [USB\VID_17EF&PID_A394] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 ParentGuid = USB\VID_17EF&PID_A392 [USB\VID_17EF&PID_A395] Plugin = vli GType = FuVliUsbhubDevice # Lenovo Hybrid Dock Version 2 [USB\VID_17EF&PID_30D2] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3,has-shared-spi-pd CounterpartGuid = USB\VID_17EF&PID_30D3 [USB\VID_17EF&PID_30D3] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 ParentGuid = USB\VID_17EF&PID_30D2 [USB\VID_17EF&PID_30D4] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3 ParentGuid = USB\VID_17EF&PID_30D2 CounterpartGuid = USB\VID_17EF&PID_30D5 [USB\VID_17EF&PID_30D5] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 ParentGuid = USB\VID_17EF&PID_30D2 # Lenovo VGA [USB\VID_17EF&PID_7211] Plugin = vli GType = FuVliPdDevice # Lenovo HDMI [USB\VID_17EF&PID_7212] Plugin = vli GType = FuVliPdDevice Flags = dual-image # Lenovo [USB\VID_17EF&PID_7215] Plugin = vli GType = FuVliPdDevice [USB\VID_17EF&PID_7217] Plugin = vli GType = FuVliPdDevice [USB\VID_17EF&PID_7223] Plugin = vli GType = FuVliPdDevice Flags = dual-image [USB\VID_17EF&PID_7247] Plugin = vli GType = FuVliPdDevice fwupd-2.0.10/plugins/vli/vli-luxshare.quirk000066400000000000000000000017541501337203100206740ustar00rootroot00000000000000# Luxshare Quad USB4 Dock [USB\VID_208E&PID_0830] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3,attach-with-power CounterpartGuid = USB\VID_208E&PID_2830 [USB\VID_208E&PID_2830] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 ParentGuid = USB\VID_208E&PID_0830 [USB\VID_208E&PID_0824] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3 ParentGuid = USB\VID_208E&PID_0830 CounterpartGuid = USB\VID_208E&PID_2824 [USB\VID_208E&PID_2824] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 ParentGuid = USB\VID_208E&PID_2830 # Luxshare 7-in-1 Hub [USB\VID_208E&PID_0822] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3,has-shared-spi-pd CounterpartGuid = USB\VID_208E&PID_2822 [USB\VID_208E&PID_2822] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 ParentGuid = USB\VID_208E&PID_0822 # VL105 PD Controller [USB\VID_208E&PID_0105] Plugin = vli GType = FuVliPdDevice Flags = skips-rom VliDeviceKind = vl105 [USB\VID_208E&PID_1105] Plugin = vli GType = FuVliPdDevice VliDeviceKind = vl105 fwupd-2.0.10/plugins/vli/vli-noinst.quirk000066400000000000000000000054561501337203100203560ustar00rootroot00000000000000# 3470_Class [USB\VID_2109&PID_0810] Plugin = vli GType = FuVliUsbhubDevice [USB\VID_2109&PID_0811] Plugin = vli GType = FuVliUsbhubDevice [USB\VID_2109&PID_0812] Plugin = vli GType = FuVliUsbhubDevice [USB\VID_2109&PID_0813] Plugin = vli GType = FuVliUsbhubDevice Flags = needs-unlock-legacy813 [USB\VID_2109&PID_8110] Plugin = vli GType = FuVliUsbhubDevice [USB\VID_2109&PID_8113] Plugin = vli GType = FuVliUsbhubDevice # 3507_Class [USB\VID_2109&PID_0210] Plugin = vli GType = FuVliUsbhubDevice # 3545_Class [USB\VID_2109&PID_0211] Plugin = vli GType = FuVliUsbhubDevice [USB\VID_2109&PID_2211] Plugin = vli GType = FuVliUsbhubDevice # VL817 [USB\VID_2109&PID_0817] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3 [USB\VID_2109&PID_2817] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 # VL819 [USB\VID_2109&PID_0819] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3 [USB\VID_2109&PID_2819] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 # VL820 [USB\VID_2109&PID_0820] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3 [USB\VID_2109&PID_2820] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 # VL822 [USB\VID_2109&PID_0822] Plugin = vli GType = FuVliUsbhubDevice Flags = usb3 [USB\VID_2109&PID_2822] Plugin = vli GType = FuVliUsbhubDevice Flags = usb2 # Generic PD VliDeviceKind = vl100 [USB\VID_2109&PID_0100] Plugin = vli GType = FuVliPdDevice VliDeviceKind = vl100 [USB\VID_2109&PID_0101] Plugin = vli GType = FuVliPdDevice VliDeviceKind = vl101 [USB\VID_2109&PID_0102] Plugin = vli GType = FuVliPdDevice VliDeviceKind = vl102 [USB\VID_2109&PID_0103] Plugin = vli GType = FuVliPdDevice Flags = dual-image VliDeviceKind = vl103 [USB\VID_2109&PID_0104] Plugin = vli GType = FuVliPdDevice VliDeviceKind = vl104 [USB\VID_2109&PID_0105] Plugin = vli GType = FuVliPdDevice VliDeviceKind = vl105 [USB\VID_2109&PID_0106] Plugin = vli GType = FuVliPdDevice VliDeviceKind = vl106 [USB\VID_2109&PID_0107] Plugin = vli GType = FuVliPdDevice VliDeviceKind = vl107 # Generic devices [USB\VID_2109&PID_D101] Plugin = vli GType = FuVliPdDevice [USB\VID_2109&PID_D102] Plugin = vli GType = FuVliPdDevice [USB\VID_2109&PID_8880] Plugin = vli GType = FuVliPdDevice # VL671,U3TT, VT3547 [USB\VID_2109&PID_8881] Plugin = vli GType = FuVliPdDevice [USB\VID_2109&PID_8882] Plugin = vli GType = FuVliPdDevice [USB\VID_2109&PID_8883] Plugin = vli GType = FuVliPdDevice # Realtek [USB\VID_2109&PID_8884] Plugin = vli GType = FuVliPdDevice # VL650, VT3555 [USB\VID_2109&PID_8885] Plugin = vli GType = FuVliPdDevice # MStar, VT3518 [USB\VID_2109&PID_8886] Plugin = vli GType = FuVliPdDevice [USB\VID_2109&PID_8887] Plugin = vli GType = FuVliPdDevice [USB\VID_2109&PID_8889] Plugin = vli GType = FuVliPdDevice # Novatek, VT3538 [USB\VID_2109&PID_888A] Plugin = vli GType = FuVliPdDevice [USB\VID_2109&PID_888B] Plugin = vli GType = FuVliPdDevice fwupd-2.0.10/plugins/vli/vli-samsung.quirk000066400000000000000000000002001501337203100204770ustar00rootroot00000000000000# Samsung [USB\VID_04E8&PID_A025] Plugin = vli GType = FuVliPdDevice [USB\VID_04E8&PID_A048] Plugin = vli GType = FuVliPdDevice fwupd-2.0.10/plugins/wacom-raw/000077500000000000000000000000001501337203100162705ustar00rootroot00000000000000fwupd-2.0.10/plugins/wacom-raw/README.md000066400000000000000000000033441501337203100175530ustar00rootroot00000000000000--- title: Plugin: Wacom RAW --- ## Introduction This plugin updates integrated Wacom AES and EMR devices. They are typically connected using I²C and not USB. ## GUID Generation The HID DeviceInstanceId values are used, e.g. `HIDRAW\VEN_056A&DEV_4875`. To recover panels that have been flashed with the wrong firmware version, the panel may have a parent device with GUID constructed from the EDID, e.g. `DRM\VEN_BOE&DEV_086E`. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in Intel HEX file format. This plugin supports the following protocol ID: * `com.wacom.raw` ## Quirk Use This plugin uses the following plugin-specific quirks: ### WacomI2cFlashBlockSize Block size to transfer firmware. Since: 1.2.4 ### WacomI2cFlashBaseAddr Base address for firmware. Since: 1.2.4 ### Flags:requires-wait-for-replug The device needs to replug into a bootloader mode. Since: 1.9.14 ## Update Behavior The device usually presents in runtime mode, but on detach re-enumerates with a different HIDRAW PID in a bootloader mode. On attach the device re-enumerates back to the runtime mode. For this reason the `REPLUG_MATCH_GUID` internal device flag is used so that the bootloader and runtime modes are treated as the same device. ## Vendor ID Security The vendor ID is set from the udev vendor, in this instance set to `HIDRAW:0x056A` ## External Interface Access This plugin requires ioctl `HIDIOCSFEATURE` access. ## Version Considerations This plugin has been available since fwupd version `1.2.4`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Tatsunosuke Tobita: @flying-elephant fwupd-2.0.10/plugins/wacom-raw/data/000077500000000000000000000000001501337203100172015ustar00rootroot00000000000000fwupd-2.0.10/plugins/wacom-raw/data/hid-recorder.txt000066400000000000000000001054561501337203100223240ustar00rootroot00000000000000# WCOM4875:00 056A:4875 # 0x05, 0x0d, // Usage Page (Digitizers) 0 # 0x09, 0x04, // Usage (Touch Screen) 2 # 0xa1, 0x01, // Collection (Application) 4 # 0x85, 0x0c, // Report ID (12) 6 # 0x95, 0x01, // Report Count (1) 8 # 0x75, 0x08, // Report Size (8) 10 # 0x26, 0xff, 0x00, // Logical Maximum (255) 12 # 0x15, 0x00, // Logical Minimum (0) 15 # 0x81, 0x03, // Input (Cnst,Var,Abs) 17 # 0x09, 0x54, // Usage (Contact Count) 19 # 0x81, 0x02, // Input (Data,Var,Abs) 21 # 0x05, 0x0d, // Usage Page (Digitizers) 23 # 0x09, 0x22, // Usage (Finger) 25 # 0xa1, 0x02, // Collection (Logical) 27 # 0x09, 0x42, // Usage (Tip Switch) 29 # 0x15, 0x00, // Logical Minimum (0) 31 # 0x25, 0x01, // Logical Maximum (1) 33 # 0x75, 0x01, // Report Size (1) 35 # 0x95, 0x01, // Report Count (1) 37 # 0x81, 0x02, // Input (Data,Var,Abs) 39 # 0x81, 0x03, // Input (Cnst,Var,Abs) 41 # 0x09, 0x47, // Usage (Confidence) 43 # 0x81, 0x02, // Input (Data,Var,Abs) 45 # 0x95, 0x05, // Report Count (5) 47 # 0x81, 0x03, // Input (Cnst,Var,Abs) 49 # 0x75, 0x10, // Report Size (16) 51 # 0x09, 0x51, // Usage (Contact Id) 53 # 0x95, 0x01, // Report Count (1) 55 # 0x81, 0x02, // Input (Data,Var,Abs) 57 # 0x05, 0x01, // Usage Page (Generic Desktop) 59 # 0x75, 0x10, // Report Size (16) 61 # 0x95, 0x01, // Report Count (1) 63 # 0x55, 0x0e, // Unit Exponent (-2) 65 # 0x65, 0x11, // Unit (Centimeter,SILinear) 67 # 0x09, 0x30, // Usage (X) 69 # 0x26, 0xc8, 0x35, // Logical Maximum (13768) 71 # 0x35, 0x00, // Physical Minimum (0) 74 # 0x46, 0x72, 0x0d, // Physical Maximum (3442) 76 # 0x81, 0x02, // Input (Data,Var,Abs) 79 # 0x46, 0x90, 0x07, // Physical Maximum (1936) 81 # 0x09, 0x31, // Usage (Y) 84 # 0x26, 0x40, 0x1e, // Logical Maximum (7744) 86 # 0x81, 0x02, // Input (Data,Var,Abs) 89 # 0xc0, // End Collection 91 # 0x05, 0x0d, // Usage Page (Digitizers) 92 # 0x09, 0x22, // Usage (Finger) 94 # 0xa1, 0x02, // Collection (Logical) 96 # 0x09, 0x42, // Usage (Tip Switch) 98 # 0x15, 0x00, // Logical Minimum (0) 100 # 0x25, 0x01, // Logical Maximum (1) 102 # 0x75, 0x01, // Report Size (1) 104 # 0x95, 0x01, // Report Count (1) 106 # 0x81, 0x02, // Input (Data,Var,Abs) 108 # 0x81, 0x03, // Input (Cnst,Var,Abs) 110 # 0x09, 0x47, // Usage (Confidence) 112 # 0x81, 0x02, // Input (Data,Var,Abs) 114 # 0x95, 0x05, // Report Count (5) 116 # 0x81, 0x03, // Input (Cnst,Var,Abs) 118 # 0x75, 0x10, // Report Size (16) 120 # 0x09, 0x51, // Usage (Contact Id) 122 # 0x95, 0x01, // Report Count (1) 124 # 0x81, 0x02, // Input (Data,Var,Abs) 126 # 0x05, 0x01, // Usage Page (Generic Desktop) 128 # 0x75, 0x10, // Report Size (16) 130 # 0x95, 0x01, // Report Count (1) 132 # 0x55, 0x0e, // Unit Exponent (-2) 134 # 0x65, 0x11, // Unit (Centimeter,SILinear) 136 # 0x09, 0x30, // Usage (X) 138 # 0x26, 0xc8, 0x35, // Logical Maximum (13768) 140 # 0x35, 0x00, // Physical Minimum (0) 143 # 0x46, 0x72, 0x0d, // Physical Maximum (3442) 145 # 0x81, 0x02, // Input (Data,Var,Abs) 148 # 0x46, 0x90, 0x07, // Physical Maximum (1936) 150 # 0x09, 0x31, // Usage (Y) 153 # 0x26, 0x40, 0x1e, // Logical Maximum (7744) 155 # 0x81, 0x02, // Input (Data,Var,Abs) 158 # 0xc0, // End Collection 160 # 0x05, 0x0d, // Usage Page (Digitizers) 161 # 0x09, 0x22, // Usage (Finger) 163 # 0xa1, 0x02, // Collection (Logical) 165 # 0x09, 0x42, // Usage (Tip Switch) 167 # 0x15, 0x00, // Logical Minimum (0) 169 # 0x25, 0x01, // Logical Maximum (1) 171 # 0x75, 0x01, // Report Size (1) 173 # 0x95, 0x01, // Report Count (1) 175 # 0x81, 0x02, // Input (Data,Var,Abs) 177 # 0x81, 0x03, // Input (Cnst,Var,Abs) 179 # 0x09, 0x47, // Usage (Confidence) 181 # 0x81, 0x02, // Input (Data,Var,Abs) 183 # 0x95, 0x05, // Report Count (5) 185 # 0x81, 0x03, // Input (Cnst,Var,Abs) 187 # 0x75, 0x10, // Report Size (16) 189 # 0x09, 0x51, // Usage (Contact Id) 191 # 0x95, 0x01, // Report Count (1) 193 # 0x81, 0x02, // Input (Data,Var,Abs) 195 # 0x05, 0x01, // Usage Page (Generic Desktop) 197 # 0x75, 0x10, // Report Size (16) 199 # 0x95, 0x01, // Report Count (1) 201 # 0x55, 0x0e, // Unit Exponent (-2) 203 # 0x65, 0x11, // Unit (Centimeter,SILinear) 205 # 0x09, 0x30, // Usage (X) 207 # 0x26, 0xc8, 0x35, // Logical Maximum (13768) 209 # 0x35, 0x00, // Physical Minimum (0) 212 # 0x46, 0x72, 0x0d, // Physical Maximum (3442) 214 # 0x81, 0x02, // Input (Data,Var,Abs) 217 # 0x46, 0x90, 0x07, // Physical Maximum (1936) 219 # 0x09, 0x31, // Usage (Y) 222 # 0x26, 0x40, 0x1e, // Logical Maximum (7744) 224 # 0x81, 0x02, // Input (Data,Var,Abs) 227 # 0xc0, // End Collection 229 # 0x05, 0x0d, // Usage Page (Digitizers) 230 # 0x09, 0x22, // Usage (Finger) 232 # 0xa1, 0x02, // Collection (Logical) 234 # 0x09, 0x42, // Usage (Tip Switch) 236 # 0x15, 0x00, // Logical Minimum (0) 238 # 0x25, 0x01, // Logical Maximum (1) 240 # 0x75, 0x01, // Report Size (1) 242 # 0x95, 0x01, // Report Count (1) 244 # 0x81, 0x02, // Input (Data,Var,Abs) 246 # 0x81, 0x03, // Input (Cnst,Var,Abs) 248 # 0x09, 0x47, // Usage (Confidence) 250 # 0x81, 0x02, // Input (Data,Var,Abs) 252 # 0x95, 0x05, // Report Count (5) 254 # 0x81, 0x03, // Input (Cnst,Var,Abs) 256 # 0x75, 0x10, // Report Size (16) 258 # 0x09, 0x51, // Usage (Contact Id) 260 # 0x95, 0x01, // Report Count (1) 262 # 0x81, 0x02, // Input (Data,Var,Abs) 264 # 0x05, 0x01, // Usage Page (Generic Desktop) 266 # 0x75, 0x10, // Report Size (16) 268 # 0x95, 0x01, // Report Count (1) 270 # 0x55, 0x0e, // Unit Exponent (-2) 272 # 0x65, 0x11, // Unit (Centimeter,SILinear) 274 # 0x09, 0x30, // Usage (X) 276 # 0x26, 0xc8, 0x35, // Logical Maximum (13768) 278 # 0x35, 0x00, // Physical Minimum (0) 281 # 0x46, 0x72, 0x0d, // Physical Maximum (3442) 283 # 0x81, 0x02, // Input (Data,Var,Abs) 286 # 0x46, 0x90, 0x07, // Physical Maximum (1936) 288 # 0x09, 0x31, // Usage (Y) 291 # 0x26, 0x40, 0x1e, // Logical Maximum (7744) 293 # 0x81, 0x02, // Input (Data,Var,Abs) 296 # 0xc0, // End Collection 298 # 0x05, 0x0d, // Usage Page (Digitizers) 299 # 0x09, 0x22, // Usage (Finger) 301 # 0xa1, 0x02, // Collection (Logical) 303 # 0x09, 0x42, // Usage (Tip Switch) 305 # 0x15, 0x00, // Logical Minimum (0) 307 # 0x25, 0x01, // Logical Maximum (1) 309 # 0x75, 0x01, // Report Size (1) 311 # 0x95, 0x01, // Report Count (1) 313 # 0x81, 0x02, // Input (Data,Var,Abs) 315 # 0x81, 0x03, // Input (Cnst,Var,Abs) 317 # 0x09, 0x47, // Usage (Confidence) 319 # 0x81, 0x02, // Input (Data,Var,Abs) 321 # 0x95, 0x05, // Report Count (5) 323 # 0x81, 0x03, // Input (Cnst,Var,Abs) 325 # 0x75, 0x10, // Report Size (16) 327 # 0x09, 0x51, // Usage (Contact Id) 329 # 0x95, 0x01, // Report Count (1) 331 # 0x81, 0x02, // Input (Data,Var,Abs) 333 # 0x05, 0x01, // Usage Page (Generic Desktop) 335 # 0x75, 0x10, // Report Size (16) 337 # 0x95, 0x01, // Report Count (1) 339 # 0x55, 0x0e, // Unit Exponent (-2) 341 # 0x65, 0x11, // Unit (Centimeter,SILinear) 343 # 0x09, 0x30, // Usage (X) 345 # 0x26, 0xc8, 0x35, // Logical Maximum (13768) 347 # 0x35, 0x00, // Physical Minimum (0) 350 # 0x46, 0x72, 0x0d, // Physical Maximum (3442) 352 # 0x81, 0x02, // Input (Data,Var,Abs) 355 # 0x46, 0x90, 0x07, // Physical Maximum (1936) 357 # 0x09, 0x31, // Usage (Y) 360 # 0x26, 0x40, 0x1e, // Logical Maximum (7744) 362 # 0x81, 0x02, // Input (Data,Var,Abs) 365 # 0xc0, // End Collection 367 # 0x05, 0x0d, // Usage Page (Digitizers) 368 # 0x27, 0xff, 0xff, 0x00, 0x00, // Logical Maximum (65535) 370 # 0x75, 0x10, // Report Size (16) 375 # 0x95, 0x01, // Report Count (1) 377 # 0x09, 0x56, // Usage (Scan Time) 379 # 0x81, 0x02, // Input (Data,Var,Abs) 381 # 0x85, 0x0c, // Report ID (12) 383 # 0x09, 0x55, // Usage (Contact Max) 385 # 0x75, 0x08, // Report Size (8) 387 # 0x95, 0x01, // Report Count (1) 389 # 0x26, 0xff, 0x00, // Logical Maximum (255) 391 # 0xb1, 0x02, // Feature (Data,Var,Abs) 394 # 0x85, 0x0a, // Report ID (10) 396 # 0x06, 0x00, 0xff, // Usage Page (Vendor Defined Page 1) 398 # 0x09, 0xc5, // Usage (Vendor Usage 0xc5) 401 # 0x96, 0x00, 0x01, // Report Count (256) 403 # 0xb1, 0x02, // Feature (Data,Var,Abs) 406 # 0xc0, // End Collection 408 # 0x06, 0x11, 0xff, // Usage Page (Vendor Usage Page 0xff11) 409 # 0x09, 0x11, // Usage (Vendor Usage 0x11) 412 # 0xa1, 0x01, // Collection (Application) 414 # 0x85, 0x03, // Report ID (3) 416 # 0xa1, 0x02, // Collection (Logical) 418 # 0x09, 0x00, // Usage (Vendor Usage 0x00) 420 # 0x75, 0x08, // Report Size (8) 422 # 0x15, 0x00, // Logical Minimum (0) 424 # 0x26, 0xff, 0x00, // Logical Maximum (255) 426 # 0x95, 0x27, // Report Count (39) 429 # 0x81, 0x02, // Input (Data,Var,Abs) 431 # 0xc0, // End Collection 433 # 0x85, 0x02, // Report ID (2) 434 # 0x09, 0x00, // Usage (Vendor Usage 0x00) 436 # 0x95, 0x01, // Report Count (1) 438 # 0xb1, 0x02, // Feature (Data,Var,Abs) 440 # 0x85, 0x03, // Report ID (3) 442 # 0x09, 0x00, // Usage (Vendor Usage 0x00) 444 # 0x95, 0x3f, // Report Count (63) 446 # 0xb1, 0x02, // Feature (Data,Var,Abs) 448 # 0x85, 0x04, // Report ID (4) 450 # 0x09, 0x00, // Usage (Vendor Usage 0x00) 452 # 0x95, 0x0f, // Report Count (15) 454 # 0xb1, 0x02, // Feature (Data,Var,Abs) 456 # 0x85, 0x07, // Report ID (7) 458 # 0x09, 0x00, // Usage (Vendor Usage 0x00) 460 # 0x96, 0x00, 0x01, // Report Count (256) 462 # 0xb1, 0x02, // Feature (Data,Var,Abs) 465 # 0x85, 0x08, // Report ID (8) 467 # 0x09, 0x00, // Usage (Vendor Usage 0x00) 469 # 0x96, 0x87, 0x00, // Report Count (135) 471 # 0xb1, 0x02, // Feature (Data,Var,Abs) 474 # 0x85, 0x09, // Report ID (9) 476 # 0x09, 0x00, // Usage (Vendor Usage 0x00) 478 # 0x96, 0x3f, 0x00, // Report Count (63) 480 # 0xb1, 0x02, // Feature (Data,Var,Abs) 483 # 0x85, 0x0d, // Report ID (13) 485 # 0x09, 0x00, // Usage (Vendor Usage 0x00) 487 # 0x95, 0x07, // Report Count (7) 489 # 0xb1, 0x02, // Feature (Data,Var,Abs) 491 # 0xc0, // End Collection 493 # 0x05, 0x0d, // Usage Page (Digitizers) 494 # 0x09, 0x0e, // Usage (Device Configuration) 496 # 0xa1, 0x01, // Collection (Application) 498 # 0x85, 0x0e, // Report ID (14) 500 # 0x09, 0x23, // Usage (Device Settings) 502 # 0xa1, 0x02, // Collection (Logical) 504 # 0x09, 0x52, // Usage (Inputmode) 506 # 0x09, 0x53, // Usage (Device Index) 508 # 0x15, 0x00, // Logical Minimum (0) 510 # 0x25, 0x0a, // Logical Maximum (10) 512 # 0x75, 0x08, // Report Size (8) 514 # 0x95, 0x02, // Report Count (2) 516 # 0xb1, 0x02, // Feature (Data,Var,Abs) 518 # 0xc0, // End Collection 520 # 0xc0, // End Collection 521 # 0x05, 0x0d, // Usage Page (Digitizers) 522 # 0x09, 0x02, // Usage (Pen) 524 # 0xa1, 0x01, // Collection (Application) 526 # 0x85, 0x06, // Report ID (6) 528 # 0xa4, // Push 530 # 0x09, 0x20, // Usage (Stylus) 531 # 0xa1, 0x00, // Collection (Physical) 533 # 0x09, 0x42, // Usage (Tip Switch) 535 # 0x09, 0x44, // Usage (Barrel Switch) 537 # 0x09, 0x45, // Usage (Eraser) 539 # 0x09, 0x3c, // Usage (Invert) 541 # 0x09, 0x5a, // Usage (Secondary Barrel Switch) 543 # 0x09, 0x32, // Usage (In Range) 545 # 0x15, 0x00, // Logical Minimum (0) 547 # 0x25, 0x01, // Logical Maximum (1) 549 # 0x75, 0x01, // Report Size (1) 551 # 0x95, 0x06, // Report Count (6) 553 # 0x81, 0x02, // Input (Data,Var,Abs) 555 # 0x95, 0x02, // Report Count (2) 557 # 0x81, 0x03, // Input (Cnst,Var,Abs) 559 # 0x05, 0x01, // Usage Page (Generic Desktop) 561 # 0x09, 0x30, // Usage (X) 563 # 0x27, 0x70, 0x86, 0x00, 0x00, // Logical Maximum (34416) 565 # 0x47, 0x70, 0x86, 0x00, 0x00, // Physical Maximum (34416) 570 # 0x65, 0x11, // Unit (Centimeter,SILinear) 575 # 0x55, 0x0d, // Unit Exponent (-3) 577 # 0x75, 0x10, // Report Size (16) 579 # 0x95, 0x01, // Report Count (1) 581 # 0x81, 0x02, // Input (Data,Var,Abs) 583 # 0x09, 0x31, // Usage (Y) 585 # 0x27, 0x9f, 0x4b, 0x00, 0x00, // Logical Maximum (19359) 587 # 0x47, 0x9f, 0x4b, 0x00, 0x00, // Physical Maximum (19359) 592 # 0x81, 0x02, // Input (Data,Var,Abs) 597 # 0x45, 0x00, // Physical Maximum (0) 599 # 0x65, 0x00, // Unit (None) 601 # 0x55, 0x00, // Unit Exponent (0) 603 # 0x05, 0x0d, // Usage Page (Digitizers) 605 # 0x09, 0x30, // Usage (Tip Pressure) 607 # 0x26, 0xff, 0x0f, // Logical Maximum (4095) 609 # 0x75, 0x10, // Report Size (16) 612 # 0x81, 0x02, // Input (Data,Var,Abs) 614 # 0x06, 0x00, 0xff, // Usage Page (Vendor Defined Page 1) 616 # 0x09, 0x5b, // Usage (Vendor Usage 0x5b) 619 # 0x16, 0x00, 0x80, // Logical Minimum (-32768) 621 # 0x26, 0xff, 0x7f, // Logical Maximum (32767) 624 # 0x75, 0x10, // Report Size (16) 627 # 0x81, 0x02, // Input (Data,Var,Abs) 629 # 0x05, 0x0d, // Usage Page (Digitizers) 631 # 0x09, 0x5b, // Usage (Transducer Serial Number) 633 # 0x17, 0x00, 0x00, 0x00, 0x80, // Logical Minimum (-2147483648) 635 # 0x27, 0xff, 0xff, 0xff, 0x7f, // Logical Maximum (2147483647) 640 # 0x75, 0x20, // Report Size (32) 645 # 0x81, 0x02, // Input (Data,Var,Abs) 647 # 0x06, 0x00, 0xff, // Usage Page (Vendor Defined Page 1) 649 # 0x09, 0x00, // Usage (Undefined) 652 # 0x75, 0x08, // Report Size (8) 654 # 0x26, 0xff, 0x00, // Logical Maximum (255) 656 # 0x15, 0x00, // Logical Minimum (0) 659 # 0x81, 0x02, // Input (Data,Var,Abs) 661 # 0x05, 0x0d, // Usage Page (Digitizers) 663 # 0x09, 0x3b, // Usage (Battery Strength) 665 # 0x81, 0x02, // Input (Data,Var,Abs) 667 # 0x65, 0x14, // Unit (Degrees,EngRotation) 669 # 0x55, 0x00, // Unit Exponent (0) 671 # 0x16, 0xa6, 0xff, // Logical Minimum (-90) 673 # 0x26, 0x5a, 0x00, // Logical Maximum (90) 676 # 0x36, 0xa6, 0xff, // Physical Minimum (-90) 679 # 0x46, 0x5a, 0x00, // Physical Maximum (90) 682 # 0x75, 0x08, // Report Size (8) 685 # 0x09, 0x3d, // Usage (X Tilt) 687 # 0x81, 0x02, // Input (Data,Var,Abs) 689 # 0x09, 0x3e, // Usage (Y Tilt) 691 # 0x81, 0x02, // Input (Data,Var,Abs) 693 # 0xc0, // End Collection 695 # 0xb4, // Pop 696 # 0x85, 0x13, // Report ID (19) 697 # 0x06, 0x00, 0xff, // Usage Page (Vendor Defined Page 1) 699 # 0x09, 0xc5, // Usage (Vendor Usage 0xc5) 702 # 0x96, 0x00, 0x01, // Report Count (256) 704 # 0xb1, 0x02, // Feature (Data,Var,Abs) 707 # 0xc0, // End Collection 709 # 0x06, 0x11, 0xff, // Usage Page (Vendor Usage Page 0xff11) 710 # 0x09, 0x02, // Usage (Vendor Usage 0x02) 713 # 0xa1, 0x01, // Collection (Application) 715 # 0x85, 0x0b, // Report ID (11) 717 # 0xa4, // Push 719 # 0x09, 0x20, // Usage (Vendor Usage 0x20) 720 # 0xa1, 0x00, // Collection (Physical) 722 # 0x09, 0x42, // Usage (Vendor Usage 0x42) 724 # 0x09, 0x44, // Usage (Vendor Usage 0x44) 726 # 0x09, 0x45, // Usage (Vendor Usage 0x45) 728 # 0x09, 0x3c, // Usage (Vendor Usage 0x3c) 730 # 0x09, 0x5a, // Usage (Vendor Usage 0x5a) 732 # 0x09, 0x32, // Usage (Vendor Usage 0x32) 734 # 0x15, 0x00, // Logical Minimum (0) 736 # 0x25, 0x01, // Logical Maximum (1) 738 # 0x75, 0x01, // Report Size (1) 740 # 0x95, 0x06, // Report Count (6) 742 # 0x81, 0x02, // Input (Data,Var,Abs) 744 # 0x95, 0x02, // Report Count (2) 746 # 0x81, 0x03, // Input (Cnst,Var,Abs) 748 # 0x05, 0x01, // Usage Page (Generic Desktop) 750 # 0x09, 0x30, // Usage (X) 752 # 0x27, 0x70, 0x86, 0x00, 0x00, // Logical Maximum (34416) 754 # 0x47, 0x70, 0x86, 0x00, 0x00, // Physical Maximum (34416) 759 # 0x65, 0x11, // Unit (Centimeter,SILinear) 764 # 0x55, 0x0d, // Unit Exponent (-3) 766 # 0x75, 0x10, // Report Size (16) 768 # 0x95, 0x01, // Report Count (1) 770 # 0x81, 0x02, // Input (Data,Var,Abs) 772 # 0x09, 0x31, // Usage (Y) 774 # 0x27, 0x9f, 0x4b, 0x00, 0x00, // Logical Maximum (19359) 776 # 0x47, 0x9f, 0x4b, 0x00, 0x00, // Physical Maximum (19359) 781 # 0x81, 0x02, // Input (Data,Var,Abs) 786 # 0x45, 0x00, // Physical Maximum (0) 788 # 0x65, 0x00, // Unit (None) 790 # 0x55, 0x00, // Unit Exponent (0) 792 # 0x05, 0x0d, // Usage Page (Digitizers) 794 # 0x09, 0x30, // Usage (Tip Pressure) 796 # 0x26, 0xff, 0x0f, // Logical Maximum (4095) 798 # 0x75, 0x10, // Report Size (16) 801 # 0x81, 0x02, // Input (Data,Var,Abs) 803 # 0x06, 0x00, 0xff, // Usage Page (Vendor Defined Page 1) 805 # 0x09, 0x5b, // Usage (Vendor Usage 0x5b) 808 # 0x16, 0x00, 0x80, // Logical Minimum (-32768) 810 # 0x26, 0xff, 0x7f, // Logical Maximum (32767) 813 # 0x75, 0x10, // Report Size (16) 816 # 0x81, 0x02, // Input (Data,Var,Abs) 818 # 0x05, 0x0d, // Usage Page (Digitizers) 820 # 0x09, 0x5b, // Usage (Transducer Serial Number) 822 # 0x17, 0x00, 0x00, 0x00, 0x80, // Logical Minimum (-2147483648) 824 # 0x27, 0xff, 0xff, 0xff, 0x7f, // Logical Maximum (2147483647) 829 # 0x75, 0x20, // Report Size (32) 834 # 0x81, 0x02, // Input (Data,Var,Abs) 836 # 0x06, 0x00, 0xff, // Usage Page (Vendor Defined Page 1) 838 # 0x09, 0x00, // Usage (Undefined) 841 # 0x75, 0x08, // Report Size (8) 843 # 0x26, 0xff, 0x00, // Logical Maximum (255) 845 # 0x15, 0x00, // Logical Minimum (0) 848 # 0x81, 0x02, // Input (Data,Var,Abs) 850 # 0x05, 0x0d, // Usage Page (Digitizers) 852 # 0x09, 0x3b, // Usage (Battery Strength) 854 # 0x81, 0x02, // Input (Data,Var,Abs) 856 # 0x65, 0x14, // Unit (Degrees,EngRotation) 858 # 0x55, 0x00, // Unit Exponent (0) 860 # 0x16, 0xa6, 0xff, // Logical Minimum (-90) 862 # 0x26, 0x5a, 0x00, // Logical Maximum (90) 865 # 0x36, 0xa6, 0xff, // Physical Minimum (-90) 868 # 0x46, 0x5a, 0x00, // Physical Maximum (90) 871 # 0x75, 0x08, // Report Size (8) 874 # 0x09, 0x3d, // Usage (X Tilt) 876 # 0x81, 0x02, // Input (Data,Var,Abs) 878 # 0x09, 0x3e, // Usage (Y Tilt) 880 # 0x81, 0x02, // Input (Data,Var,Abs) 882 # 0xc0, // End Collection 884 # 0xb4, // Pop 885 # 0x06, 0x00, 0xff, // Usage Page (Vendor Defined Page 1) 886 # 0x75, 0x08, // Report Size (8) 889 # 0x15, 0x00, // Logical Minimum (0) 891 # 0x26, 0xff, 0x00, // Logical Maximum (255) 893 # 0x85, 0x05, // Report ID (5) 896 # 0x09, 0x00, // Usage (Undefined) 898 # 0x95, 0x3a, // Report Count (58) 900 # 0x81, 0x02, // Input (Data,Var,Abs) 902 # 0x85, 0x10, // Report ID (16) 904 # 0x09, 0x00, // Usage (Undefined) 906 # 0x95, 0x14, // Report Count (20) 908 # 0x81, 0x02, // Input (Data,Var,Abs) 910 # 0x85, 0x0f, // Report ID (15) 912 # 0x09, 0x00, // Usage (Undefined) 914 # 0x95, 0x28, // Report Count (40) 916 # 0x81, 0x02, // Input (Data,Var,Abs) 918 # 0x85, 0x0f, // Report ID (15) 920 # 0x09, 0x00, // Usage (Undefined) 922 # 0x95, 0x07, // Report Count (7) 924 # 0xb1, 0x02, // Feature (Data,Var,Abs) 926 # 0x85, 0x11, // Report ID (17) 928 # 0x09, 0x00, // Usage (Undefined) 930 # 0x95, 0x09, // Report Count (9) 932 # 0xb1, 0x02, // Feature (Data,Var,Abs) 934 # 0x85, 0x05, // Report ID (5) 936 # 0x09, 0x00, // Usage (Undefined) 938 # 0x95, 0x08, // Report Count (8) 940 # 0xb1, 0x02, // Feature (Data,Var,Abs) 942 # 0x85, 0x10, // Report ID (16) 944 # 0x09, 0x00, // Usage (Undefined) 946 # 0x96, 0x3f, 0x00, // Report Count (63) 948 # 0xb1, 0x02, // Feature (Data,Var,Abs) 951 # 0x85, 0x0b, // Report ID (11) 953 # 0x09, 0x00, // Usage (Undefined) 955 # 0x96, 0x3f, 0x00, // Report Count (63) 957 # 0xb1, 0x02, // Feature (Data,Var,Abs) 960 # 0xc0, // End Collection 962 # 0x05, 0x01, // Usage Page (Generic Desktop) 963 # 0x09, 0x02, // Usage (Mouse) 965 # 0xa1, 0x01, // Collection (Application) 967 # 0x85, 0x01, // Report ID (1) 969 # 0x09, 0x01, // Usage (Pointer) 971 # 0xa1, 0x00, // Collection (Physical) 973 # 0x05, 0x09, // Usage Page (Button) 975 # 0x19, 0x01, // Usage Minimum (1) 977 # 0x29, 0x02, // Usage Maximum (2) 979 # 0x15, 0x00, // Logical Minimum (0) 981 # 0x25, 0x01, // Logical Maximum (1) 983 # 0x95, 0x02, // Report Count (2) 985 # 0x75, 0x01, // Report Size (1) 987 # 0x81, 0x02, // Input (Data,Var,Abs) 989 # 0x95, 0x01, // Report Count (1) 991 # 0x75, 0x06, // Report Size (6) 993 # 0x81, 0x03, // Input (Cnst,Var,Abs) 995 # 0x05, 0x01, // Usage Page (Generic Desktop) 997 # 0x09, 0x30, // Usage (X) 999 # 0x09, 0x31, // Usage (Y) 1001 # 0x26, 0xff, 0x7f, // Logical Maximum (32767) 1003 # 0x75, 0x10, // Report Size (16) 1006 # 0x95, 0x02, // Report Count (2) 1008 # 0x81, 0x02, // Input (Data,Var,Abs) 1010 # 0xc0, // End Collection 1012 # 0xc0, // End Collection 1013 fwupd-2.0.10/plugins/wacom-raw/fu-wacom-aes-device.c000066400000000000000000000224321501337203100221600ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-wacom-aes-device.h" #include "fu-wacom-common.h" struct _FuWacomAesDevice { FuWacomDevice parent_instance; }; G_DEFINE_TYPE(FuWacomAesDevice, fu_wacom_aes_device, FU_TYPE_WACOM_DEVICE) static gboolean fu_wacom_aes_device_add_recovery_hwid(FuWacomAesDevice *self, GError **error) { guint16 pid; g_autoptr(FuStructWacomRawRequest) st_req = fu_struct_wacom_raw_request_new(); g_autoptr(FuStructWacomRawBlVerifyResponse) st_rsp = NULL; fu_struct_wacom_raw_request_set_report_id(st_req, FU_WACOM_RAW_BL_REPORT_ID_SET); fu_struct_wacom_raw_request_set_cmd(st_req, FU_WACOM_RAW_BL_CMD_VERIFY_FLASH); fu_struct_wacom_raw_request_set_echo(st_req, 0x01); fu_struct_wacom_raw_request_set_addr(st_req, FU_WACOM_RAW_BL_START_ADDR); fu_struct_wacom_raw_request_set_size8(st_req, FU_WACOM_RAW_BL_BYTES_CHECK / 8); if (!fu_wacom_device_set_feature(FU_WACOM_DEVICE(self), st_req->data, st_req->len, error)) { g_prefix_error(error, "failed to send: "); return FALSE; } if (!fu_wacom_device_get_feature(FU_WACOM_DEVICE(self), st_req->data, st_req->len, error)) { g_prefix_error(error, "failed to receive: "); return FALSE; } st_rsp = fu_struct_wacom_raw_bl_verify_response_parse(st_req->data, st_req->len, 0x0, error); if (st_rsp == NULL) return FALSE; if (fu_struct_wacom_raw_bl_verify_response_get_size8(st_rsp) != fu_struct_wacom_raw_request_get_size8(st_req)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "firmware does not support this feature"); return FALSE; } pid = fu_struct_wacom_raw_bl_verify_response_get_pid(st_rsp); if (pid == 0xFFFF || pid == 0x0000) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "invalid recovery product ID %04x", pid); return FALSE; } /* add recovery IDs */ fu_device_add_instance_u16(FU_DEVICE(self), "VEN", 0x2D1F); fu_device_add_instance_u16(FU_DEVICE(self), "DEV", pid); if (!fu_device_build_instance_id(FU_DEVICE(self), error, "HIDRAW", "VID", "PID", NULL)) return FALSE; fu_device_add_instance_u16(FU_DEVICE(self), "VEN", 0x056A); return fu_device_build_instance_id(FU_DEVICE(self), error, "HIDRAW", "VID", "PID", NULL); } static gboolean fu_wacom_aes_device_query_operation_mode(FuWacomAesDevice *self, FuWacomRawOperationMode *mode, GError **error) { g_autoptr(GByteArray) st_req = fu_struct_wacom_raw_fw_query_mode_request_new(); g_autoptr(GByteArray) st_rsp = NULL; if (!fu_wacom_device_get_feature(FU_WACOM_DEVICE(self), st_req->data, st_req->len, error)) return FALSE; st_rsp = fu_struct_wacom_raw_fw_query_mode_response_parse(st_req->data, st_req->len, 0x0, error); if (st_rsp == NULL) return FALSE; if (mode != NULL) *mode = fu_struct_wacom_raw_fw_query_mode_response_get_mode(st_rsp); return TRUE; } static gboolean fu_wacom_aes_device_setup(FuDevice *device, GError **error) { FuWacomAesDevice *self = FU_WACOM_AES_DEVICE(device); FuWacomRawOperationMode mode = 0; g_autoptr(GError) error_local = NULL; /* find out if in bootloader mode already */ if (!fu_wacom_aes_device_query_operation_mode(self, &mode, error)) return FALSE; if (mode == FU_WACOM_RAW_OPERATION_MODE_BOOTLOADER) { fu_device_set_version(device, "0.0"); /* get the recovery PID if supported */ if (!fu_wacom_aes_device_add_recovery_hwid(self, &error_local)) g_debug("failed to get HwID: %s", error_local->message); } else if (mode == FU_WACOM_RAW_OPERATION_MODE_RUNTIME) { g_autofree gchar *version = NULL; g_autoptr(FuStructWacomRawFwStatusRequest) st_req = fu_struct_wacom_raw_fw_status_request_new(); g_autoptr(FuStructWacomRawFwStatusResponse) st_rsp = NULL; /* get firmware version */ if (!fu_wacom_device_get_feature(FU_WACOM_DEVICE(self), st_req->data, st_req->len, error)) return FALSE; st_rsp = fu_struct_wacom_raw_fw_status_response_parse(st_req->data, st_req->len, 0x0, error); if (st_rsp == NULL) return FALSE; version = g_strdup_printf( "%04x.%02x", fu_struct_wacom_raw_fw_status_response_get_version_major(st_rsp), fu_struct_wacom_raw_fw_status_response_get_version_minor(st_rsp)); fu_device_set_version(device, version); } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to query operation mode, got 0x%x", mode); return FALSE; } /* success */ return TRUE; } static gboolean fu_wacom_aes_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuWacomDevice *self = FU_WACOM_DEVICE(device); g_autoptr(FuStructWacomRawRequest) st_req = fu_struct_wacom_raw_request_new(); if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in runtime mode, skipping"); return TRUE; } fu_struct_wacom_raw_request_set_report_id(st_req, FU_WACOM_RAW_BL_REPORT_ID_TYPE); fu_struct_wacom_raw_request_set_cmd(st_req, FU_WACOM_RAW_BL_TYPE_FINALIZER); if (!fu_wacom_device_set_feature(self, st_req->data, st_req->len, error)) { g_prefix_error(error, "failed to finalize the device: "); return FALSE; } /* does the device have to replug to bootloader mode */ if (fu_device_has_private_flag(device, FU_WACOM_RAW_DEVICE_FLAG_REQUIRES_WAIT_FOR_REPLUG)) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); } else { /* wait for device back to runtime mode */ fu_device_sleep(device, 500); /* ms */ fu_device_remove_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); } /* success */ return TRUE; } static gboolean fu_wacom_aes_device_erase_all(FuWacomAesDevice *self, FuProgress *progress, GError **error) { g_autoptr(FuStructWacomRawRequest) st_req = fu_struct_wacom_raw_request_new(); fu_struct_wacom_raw_request_set_report_id(st_req, FU_WACOM_RAW_BL_REPORT_ID_SET); fu_struct_wacom_raw_request_set_cmd(st_req, FU_WACOM_RAW_BL_CMD_ALL_ERASE); fu_struct_wacom_raw_request_set_echo(st_req, fu_wacom_device_get_echo_next(FU_WACOM_DEVICE(self))); if (!fu_wacom_device_cmd(FU_WACOM_DEVICE(self), st_req, NULL, 2000, /* this takes a long time */ FU_WACOM_DEVICE_CMD_FLAG_POLL_ON_WAITING, error)) { g_prefix_error(error, "failed to send eraseall command: "); return FALSE; } fu_device_sleep_full(FU_DEVICE(self), 2000, progress); return TRUE; } static gboolean fu_wacom_aes_device_write_block(FuWacomAesDevice *self, guint32 idx, guint32 address, const guint8 *data, gsize datasz, GError **error) { gsize blocksz = fu_wacom_device_get_block_sz(FU_WACOM_DEVICE(self)); g_autoptr(FuStructWacomRawRequest) st_req = fu_struct_wacom_raw_request_new(); /* check size */ if (datasz != blocksz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "block size 0x%x != 0x%x untested", (guint)datasz, (guint)blocksz); return FALSE; } /* write */ fu_struct_wacom_raw_request_set_report_id(st_req, FU_WACOM_RAW_BL_REPORT_ID_SET); fu_struct_wacom_raw_request_set_cmd(st_req, FU_WACOM_RAW_BL_CMD_WRITE_FLASH); fu_struct_wacom_raw_request_set_echo(st_req, (guint8)idx + 1); fu_struct_wacom_raw_request_set_addr(st_req, address); fu_struct_wacom_raw_request_set_size8(st_req, datasz / 8); if (!fu_struct_wacom_raw_request_set_data(st_req, data, datasz, error)) return FALSE; if (!fu_wacom_device_cmd(FU_WACOM_DEVICE(self), st_req, NULL, 1, /* ms */ FU_WACOM_DEVICE_CMD_FLAG_POLL_ON_WAITING, error)) { g_prefix_error(error, "failed to write block %u: ", idx); return FALSE; } return TRUE; } static gboolean fu_wacom_aes_device_write_firmware(FuDevice *device, FuChunkArray *chunks, FuProgress *progress, GError **error) { FuWacomAesDevice *self = FU_WACOM_AES_DEVICE(device); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 28, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 72, NULL); /* erase */ if (!fu_wacom_aes_device_erase_all(self, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* write */ for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_wacom_aes_device_write_block(self, fu_chunk_get_idx(chk), fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)i, (gsize)fu_chunk_array_length(chunks)); } fu_progress_step_done(progress); return TRUE; } static void fu_wacom_aes_device_init(FuWacomAesDevice *self) { fu_device_set_name(FU_DEVICE(self), "Wacom AES Device"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PAIR); } static void fu_wacom_aes_device_class_init(FuWacomAesDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); FuWacomDeviceClass *wac_device_class = FU_WACOM_DEVICE_CLASS(klass); device_class->setup = fu_wacom_aes_device_setup; device_class->attach = fu_wacom_aes_device_attach; wac_device_class->write_firmware = fu_wacom_aes_device_write_firmware; } fwupd-2.0.10/plugins/wacom-raw/fu-wacom-aes-device.h000066400000000000000000000004771501337203100221720ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-wacom-device.h" #define FU_TYPE_WACOM_AES_DEVICE (fu_wacom_aes_device_get_type()) G_DECLARE_FINAL_TYPE(FuWacomAesDevice, fu_wacom_aes_device, FU, WACOM_AES_DEVICE, FuWacomDevice) fwupd-2.0.10/plugins/wacom-raw/fu-wacom-common.c000066400000000000000000000053521501337203100214450ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-wacom-common.h" gboolean fu_wacom_common_check_reply(const FuStructWacomRawRequest *st_req, const FuStructWacomRawResponse *st_rsp, GError **error) { if (fu_struct_wacom_raw_response_get_report_id(st_rsp) != FU_WACOM_RAW_BL_REPORT_ID_GET) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "report ID failed, expected 0x%02x, got 0x%02x", (guint)FU_WACOM_RAW_BL_REPORT_ID_GET, fu_struct_wacom_raw_request_get_report_id(st_req)); return FALSE; } if (fu_struct_wacom_raw_request_get_cmd(st_req) != fu_struct_wacom_raw_response_get_cmd(st_rsp)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "cmd failed, expected 0x%02x, got 0x%02x", fu_struct_wacom_raw_request_get_cmd(st_req), fu_struct_wacom_raw_response_get_cmd(st_rsp)); return FALSE; } if (fu_struct_wacom_raw_request_get_echo(st_req) != fu_struct_wacom_raw_response_get_echo(st_rsp)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "echo failed, expected 0x%02x, got 0x%02x", fu_struct_wacom_raw_request_get_echo(st_req), fu_struct_wacom_raw_response_get_echo(st_rsp)); return FALSE; } return TRUE; } gboolean fu_wacom_common_rc_set_error(const FuStructWacomRawResponse *st_rsp, GError **error) { FuWacomRawRc rc = fu_struct_wacom_raw_response_get_resp(st_rsp); if (rc == FU_WACOM_RAW_RC_OK) return TRUE; if (rc == FU_WACOM_RAW_RC_BUSY) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_BUSY, "device is busy"); return FALSE; } if (rc == FU_WACOM_RAW_RC_MCUTYPE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "MCU type does not match"); return FALSE; } if (rc == FU_WACOM_RAW_RC_PID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "PID does not match"); return FALSE; } if (rc == FU_WACOM_RAW_RC_CHECKSUM1) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "checksum1 does not match"); return FALSE; } if (rc == FU_WACOM_RAW_RC_CHECKSUM2) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "checksum2 does not match"); return FALSE; } if (rc == FU_WACOM_RAW_RC_TIMEOUT) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT, "command timed out"); return FALSE; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unknown error 0x%02x", rc); return FALSE; } gboolean fu_wacom_common_block_is_empty(const guint8 *data, guint16 datasz) { for (guint16 i = 0; i < datasz; i++) { if (data[i] != 0xff) return FALSE; } return TRUE; } fwupd-2.0.10/plugins/wacom-raw/fu-wacom-common.h000066400000000000000000000012421501337203100214440ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-wacom-raw-struct.h" #define FU_WACOM_RAW_CMD_RETRIES 1000 #define FU_WACOM_RAW_BL_START_ADDR (0x11FF8) #define FU_WACOM_RAW_BL_BYTES_CHECK 8 #define FU_WACOM_RAW_BL_TYPE_FINALIZER 0x00 gboolean fu_wacom_common_rc_set_error(const FuStructWacomRawResponse *st_rsp, GError **error); gboolean fu_wacom_common_check_reply(const FuStructWacomRawRequest *st_req, const FuStructWacomRawResponse *st_rsp, GError **error); gboolean fu_wacom_common_block_is_empty(const guint8 *data, guint16 datasz); fwupd-2.0.10/plugins/wacom-raw/fu-wacom-device.c000066400000000000000000000304021501337203100214060ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-wacom-common.h" #include "fu-wacom-device.h" typedef struct { guint flash_block_size; guint32 flash_base_addr; guint8 echo_next; } FuWacomDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuWacomDevice, fu_wacom_device, FU_TYPE_HIDRAW_DEVICE) #define GET_PRIVATE(o) (fu_wacom_device_get_instance_private(o)) static void fu_wacom_device_to_string(FuDevice *device, guint idt, GString *str) { FuWacomDevice *self = FU_WACOM_DEVICE(device); FuWacomDevicePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append_hex(str, idt, "FlashBlockSize", priv->flash_block_size); fwupd_codec_string_append_hex(str, idt, "FlashBaseAddr", priv->flash_base_addr); fwupd_codec_string_append_hex(str, idt, "EchoNext", priv->echo_next); } #define FU_WACOM_RAW_ECHO_MIN 0xA0 #define FU_WACOM_RAW_ECHO_MAX 0xFE guint8 fu_wacom_device_get_echo_next(FuWacomDevice *self) { FuWacomDevicePrivate *priv = GET_PRIVATE(self); priv->echo_next++; if (priv->echo_next > FU_WACOM_RAW_ECHO_MAX) priv->echo_next = FU_WACOM_RAW_ECHO_MIN; return priv->echo_next; } gsize fu_wacom_device_get_block_sz(FuWacomDevice *self) { FuWacomDevicePrivate *priv = GET_PRIVATE(self); return priv->flash_block_size; } gboolean fu_wacom_device_check_mpu(FuWacomDevice *self, GError **error) { guint8 rsp_value = 0; g_autoptr(FuStructWacomRawRequest) st_req = fu_struct_wacom_raw_request_new(); fu_struct_wacom_raw_request_set_report_id(st_req, FU_WACOM_RAW_BL_REPORT_ID_SET); fu_struct_wacom_raw_request_set_cmd(st_req, FU_WACOM_RAW_BL_CMD_GET_MPUTYPE); fu_struct_wacom_raw_request_set_echo(st_req, fu_wacom_device_get_echo_next(self)); if (!fu_wacom_device_cmd(self, st_req, &rsp_value, 0, FU_WACOM_DEVICE_CMD_FLAG_NO_ERROR_CHECK, error)) { g_prefix_error(error, "failed to get MPU type: "); return FALSE; } /* W9013 */ if (rsp_value == 0x2e) { fu_device_add_instance_id_full(FU_DEVICE(self), "WacomEMR_W9013", FU_DEVICE_INSTANCE_FLAG_QUIRKS); return TRUE; } /* W9021 */ if (rsp_value == 0x45) { fu_device_add_instance_id_full(FU_DEVICE(self), "WacomEMR_W9021", FU_DEVICE_INSTANCE_FLAG_QUIRKS); return TRUE; } /* unsupported */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "MPU is not W9013 or W9021: 0x%x", rsp_value); return FALSE; } static gboolean fu_wacom_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuWacomDevice *self = FU_WACOM_DEVICE(device); g_autoptr(FuStructWacomRawFwDetachRequest) st = fu_struct_wacom_raw_fw_detach_request_new(); g_autoptr(GError) error_local = NULL; if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in bootloader mode, skipping"); return TRUE; } if (!fu_wacom_device_set_feature(self, st->data, st->len, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INTERNAL)) { g_debug("ignoring: %s", error_local->message); } else { g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to switch to bootloader mode: "); return FALSE; } } /* does the device have to replug to bootloader mode */ if (fu_device_has_private_flag(device, FU_WACOM_RAW_DEVICE_FLAG_REQUIRES_WAIT_FOR_REPLUG)) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); } else { fu_device_sleep(device, 300); /* ms */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); } return TRUE; } static gboolean fu_wacom_device_check_mode(FuWacomDevice *self, GError **error) { guint8 rsp_value = 0; g_autoptr(FuStructWacomRawRequest) st_req = fu_struct_wacom_raw_request_new(); fu_struct_wacom_raw_request_set_report_id(st_req, FU_WACOM_RAW_BL_REPORT_ID_SET); fu_struct_wacom_raw_request_set_cmd(st_req, FU_WACOM_RAW_BL_CMD_CHECK_MODE); fu_struct_wacom_raw_request_set_echo(st_req, fu_wacom_device_get_echo_next(self)); if (!fu_wacom_device_cmd(self, st_req, &rsp_value, 0, FU_WACOM_DEVICE_CMD_FLAG_NO_ERROR_CHECK, error)) { g_prefix_error(error, "failed to check mode: "); return FALSE; } if (rsp_value != 0x06) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "check mode failed, mode=0x%02x", rsp_value); return FALSE; } return TRUE; } static gboolean fu_wacom_device_set_version_bootloader(FuWacomDevice *self, GError **error) { guint8 rsp_value = 0; g_autofree gchar *version = NULL; g_autoptr(FuStructWacomRawRequest) st_req = fu_struct_wacom_raw_request_new(); fu_struct_wacom_raw_request_set_report_id(st_req, FU_WACOM_RAW_BL_REPORT_ID_SET); fu_struct_wacom_raw_request_set_cmd(st_req, FU_WACOM_RAW_BL_CMD_GET_BLVER); fu_struct_wacom_raw_request_set_echo(st_req, fu_wacom_device_get_echo_next(self)); if (!fu_wacom_device_cmd(self, st_req, &rsp_value, 0, FU_WACOM_DEVICE_CMD_FLAG_NO_ERROR_CHECK, error)) { g_prefix_error(error, "failed to get bootloader version: "); return FALSE; } version = g_strdup_printf("%u", rsp_value); fu_device_set_version_bootloader(FU_DEVICE(self), version); return TRUE; } static gboolean fu_wacom_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuWacomDevice *self = FU_WACOM_DEVICE(device); FuWacomDevicePrivate *priv = GET_PRIVATE(self); FuWacomDeviceClass *klass = FU_WACOM_DEVICE_GET_CLASS(device); g_autoptr(GBytes) fw = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* use the correct image from the firmware */ g_debug("using element at addr 0x%0x", (guint)fu_firmware_get_addr(firmware)); /* check start address and size */ if (fu_firmware_get_addr(firmware) != priv->flash_base_addr) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "base addr invalid: 0x%05x", (guint)fu_firmware_get_addr(firmware)); return FALSE; } fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* we're in bootloader mode now */ if (!fu_wacom_device_check_mode(self, error)) return FALSE; if (!fu_wacom_device_set_version_bootloader(self, error)) return FALSE; /* flash chunks */ chunks = fu_chunk_array_new_from_bytes(fw, priv->flash_base_addr, FU_CHUNK_PAGESZ_NONE, priv->flash_block_size); return klass->write_firmware(device, chunks, progress, error); } gboolean fu_wacom_device_get_feature(FuWacomDevice *self, guint8 *data, guint datasz, GError **error) { return fu_hidraw_device_get_feature(FU_HIDRAW_DEVICE(self), data, datasz, FU_IOCTL_FLAG_NONE, error); } gboolean fu_wacom_device_set_feature(FuWacomDevice *self, const guint8 *data, guint datasz, GError **error) { return fu_hidraw_device_set_feature(FU_HIDRAW_DEVICE(self), data, datasz, FU_IOCTL_FLAG_NONE, error); } static gboolean fu_wacom_device_cmd_response(FuWacomDevice *self, const FuStructWacomRawRequest *st_req, guint8 *rsp_value, FuWacomDeviceCmdFlags flags, GError **error) { guint8 buf[FU_STRUCT_WACOM_RAW_RESPONSE_SIZE] = {FU_WACOM_RAW_BL_REPORT_ID_GET, 0x0}; g_autoptr(FuStructWacomRawRequest) st_rsp = NULL; if (!fu_wacom_device_get_feature(self, buf, sizeof(buf), error)) { g_prefix_error(error, "failed to receive: "); return FALSE; } st_rsp = fu_struct_wacom_raw_response_parse(buf, sizeof(buf), 0x0, error); if (st_rsp == NULL) return FALSE; if (!fu_wacom_common_check_reply(st_req, st_rsp, error)) return FALSE; if ((flags & FU_WACOM_DEVICE_CMD_FLAG_NO_ERROR_CHECK) == 0) { if (!fu_wacom_common_rc_set_error(st_rsp, error)) return FALSE; } /* optional */ if (rsp_value != NULL) *rsp_value = fu_struct_wacom_raw_response_get_resp(st_rsp); /* success */ return TRUE; } typedef struct { const FuStructWacomRawRequest *st_req; guint8 *rsp_value; FuWacomDeviceCmdFlags flags; } FuWacomRawResponseHelper; static gboolean fu_wacom_device_cmd_response_cb(FuDevice *device, gpointer user_data, GError **error) { FuWacomDevice *self = FU_WACOM_DEVICE(device); FuWacomRawResponseHelper *helper = (FuWacomRawResponseHelper *)user_data; return fu_wacom_device_cmd_response(self, helper->st_req, helper->rsp_value, helper->flags, error); } gboolean fu_wacom_device_cmd(FuWacomDevice *self, const FuStructWacomRawRequest *st_req, guint8 *rsp_value, guint delay_ms, FuWacomDeviceCmdFlags flags, GError **error) { if (!fu_wacom_device_set_feature(self, st_req->data, st_req->len, error)) { g_prefix_error(error, "failed to send: "); return FALSE; } fu_device_sleep(FU_DEVICE(self), delay_ms); if (flags & FU_WACOM_DEVICE_CMD_FLAG_POLL_ON_WAITING) { FuWacomRawResponseHelper helper = {.st_req = st_req, .rsp_value = rsp_value, .flags = flags}; return fu_device_retry_full(FU_DEVICE(self), fu_wacom_device_cmd_response_cb, FU_WACOM_RAW_CMD_RETRIES, delay_ms, &helper, error); } return fu_wacom_device_cmd_response(self, st_req, rsp_value, flags, error); } static gboolean fu_wacom_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error) { FuWacomDevice *self = FU_WACOM_DEVICE(device); FuWacomDevicePrivate *priv = GET_PRIVATE(self); guint64 tmp = 0; if (g_strcmp0(key, "WacomI2cFlashBlockSize") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXSIZE, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->flash_block_size = tmp; return TRUE; } if (g_strcmp0(key, "WacomI2cFlashBaseAddr") == 0) { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; priv->flash_base_addr = tmp; return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "quirk key not supported"); return FALSE; } static void fu_wacom_device_replace(FuDevice *device, FuDevice *donor) { g_return_if_fail(FU_IS_WACOM_DEVICE(device)); g_return_if_fail(FU_IS_WACOM_DEVICE(donor)); /* copy private instance data */ if (fu_device_has_private_flag(donor, FU_WACOM_RAW_DEVICE_FLAG_REQUIRES_WAIT_FOR_REPLUG)) { fu_device_add_private_flag(device, FU_WACOM_RAW_DEVICE_FLAG_REQUIRES_WAIT_FOR_REPLUG); } } static void fu_wacom_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 4, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 92, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 4, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static void fu_wacom_device_init(FuWacomDevice *self) { FuWacomDevicePrivate *priv = GET_PRIVATE(self); priv->echo_next = FU_WACOM_RAW_ECHO_MIN; fu_device_add_protocol(FU_DEVICE(self), "com.wacom.raw"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_INTERNAL); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_NEEDS_REBOOT); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_READ); fu_udev_device_add_open_flag(FU_UDEV_DEVICE(self), FU_IO_CHANNEL_OPEN_FLAG_WRITE); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PAIR); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_IHEX_FIRMWARE); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_register_private_flag(FU_DEVICE(self), FU_WACOM_RAW_DEVICE_FLAG_REQUIRES_WAIT_FOR_REPLUG); } static void fu_wacom_device_class_init(FuWacomDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->to_string = fu_wacom_device_to_string; device_class->write_firmware = fu_wacom_device_write_firmware; device_class->detach = fu_wacom_device_detach; device_class->set_quirk_kv = fu_wacom_device_set_quirk_kv; device_class->set_progress = fu_wacom_device_set_progress; device_class->replace = fu_wacom_device_replace; } fwupd-2.0.10/plugins/wacom-raw/fu-wacom-device.h000066400000000000000000000026721501337203100214230ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-wacom-common.h" #define FU_TYPE_WACOM_DEVICE (fu_wacom_device_get_type()) G_DECLARE_DERIVABLE_TYPE(FuWacomDevice, fu_wacom_device, FU, WACOM_DEVICE, FuHidrawDevice) struct _FuWacomDeviceClass { FuHidrawDeviceClass parent_class; gboolean (*write_firmware)(FuDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error); }; typedef enum { FU_WACOM_DEVICE_CMD_FLAG_NONE = 0, FU_WACOM_DEVICE_CMD_FLAG_POLL_ON_WAITING = 1 << 0, FU_WACOM_DEVICE_CMD_FLAG_NO_ERROR_CHECK = 1 << 1, } FuWacomDeviceCmdFlags; #define FU_WACOM_RAW_DEVICE_FLAG_REQUIRES_WAIT_FOR_REPLUG "requires-wait-for-replug" guint8 fu_wacom_device_get_echo_next(FuWacomDevice *self); gboolean fu_wacom_device_set_feature(FuWacomDevice *self, const guint8 *data, guint datasz, GError **error); gboolean fu_wacom_device_get_feature(FuWacomDevice *self, guint8 *data, guint datasz, GError **error); gboolean fu_wacom_device_cmd(FuWacomDevice *self, const FuStructWacomRawRequest *st_req, guint8 *rsp_value, guint delay_ms, FuWacomDeviceCmdFlags flags, GError **error); gboolean fu_wacom_device_erase_all(FuWacomDevice *self, GError **error); gboolean fu_wacom_device_check_mpu(FuWacomDevice *self, GError **error); gsize fu_wacom_device_get_block_sz(FuWacomDevice *self); fwupd-2.0.10/plugins/wacom-raw/fu-wacom-emr-device.c000066400000000000000000000222021501337203100221660ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-wacom-common.h" #include "fu-wacom-emr-device.h" struct _FuWacomEmrDevice { FuWacomDevice parent_instance; }; G_DEFINE_TYPE(FuWacomEmrDevice, fu_wacom_emr_device, FU_TYPE_WACOM_DEVICE) static gboolean fu_wacom_emr_device_setup(FuDevice *device, GError **error) { FuWacomEmrDevice *self = FU_WACOM_EMR_DEVICE(device); /* check MPU type */ if (!fu_wacom_device_check_mpu(FU_WACOM_DEVICE(self), error)) return FALSE; /* get firmware version */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { fu_device_set_version_raw(device, 0); } else { guint16 fw_ver; guint8 data[19] = {0x03, 0x0}; /* 0x03 is an unknown ReportID */ if (!fu_wacom_device_get_feature(FU_WACOM_DEVICE(self), data, sizeof(data), error)) return FALSE; if (!fu_memread_uint16_safe(data, sizeof(data), 11, &fw_ver, G_LITTLE_ENDIAN, error)) return FALSE; fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER); fu_device_set_version_raw(device, fw_ver); } /* success */ return TRUE; } static guint8 fu_wacom_emr_device_calc_checksum(guint8 init1, const guint8 *buf, gsize bufsz) { return init1 + ~(fu_sum8(buf, bufsz)) + 1; } static gboolean fu_wacom_emr_device_w9013_erase_data(FuWacomEmrDevice *self, GError **error) { g_autoptr(FuStructWacomRawRequest) st_req = fu_struct_wacom_raw_request_new(); guint8 *buf = st_req->data + FU_STRUCT_WACOM_RAW_REQUEST_OFFSET_ADDR; fu_struct_wacom_raw_request_set_report_id(st_req, FU_WACOM_RAW_BL_REPORT_ID_SET); fu_struct_wacom_raw_request_set_cmd(st_req, FU_WACOM_RAW_BL_CMD_ERASE_DATAMEM); fu_struct_wacom_raw_request_set_echo(st_req, fu_wacom_device_get_echo_next(FU_WACOM_DEVICE(self))); buf[0] = 0x00; /* erased block */ buf[1] = fu_wacom_emr_device_calc_checksum(0x05 + 0x00 + 0x07 + 0x00, (const guint8 *)&st_req, 4); if (!fu_wacom_device_cmd(FU_WACOM_DEVICE(self), st_req, NULL, 1, /* ms */ FU_WACOM_DEVICE_CMD_FLAG_POLL_ON_WAITING, error)) { g_prefix_error(error, "failed to erase datamem: "); return FALSE; } g_usleep(50); return TRUE; } static gboolean fu_wacom_emr_device_w9013_erase_code(FuWacomEmrDevice *self, guint8 idx, guint8 block_nr, GError **error) { g_autoptr(FuStructWacomRawRequest) st_req = fu_struct_wacom_raw_request_new(); guint8 *buf = st_req->data + FU_STRUCT_WACOM_RAW_REQUEST_OFFSET_ADDR; fu_struct_wacom_raw_request_set_report_id(st_req, FU_WACOM_RAW_BL_REPORT_ID_SET); fu_struct_wacom_raw_request_set_cmd(st_req, FU_WACOM_RAW_BL_CMD_ERASE_FLASH); fu_struct_wacom_raw_request_set_echo(st_req, idx); buf[0] = block_nr; buf[1] = fu_wacom_emr_device_calc_checksum(0x05 + 0x00 + 0x07 + 0x00, (const guint8 *)&st_req, 4); if (!fu_wacom_device_cmd(FU_WACOM_DEVICE(self), st_req, NULL, 1, /* ms */ FU_WACOM_DEVICE_CMD_FLAG_POLL_ON_WAITING, error)) { g_prefix_error(error, "failed to erase codemem: "); return FALSE; } g_usleep(50); return TRUE; } static gboolean fu_wacom_emr_device_w9021_erase_all(FuWacomEmrDevice *self, GError **error) { g_autoptr(FuStructWacomRawRequest) st_req = fu_struct_wacom_raw_request_new(); fu_struct_wacom_raw_request_set_report_id(st_req, FU_WACOM_RAW_BL_REPORT_ID_SET); fu_struct_wacom_raw_request_set_cmd(st_req, FU_WACOM_RAW_BL_CMD_ALL_ERASE); fu_struct_wacom_raw_request_set_echo(st_req, 0x01); if (!fu_wacom_device_cmd(FU_WACOM_DEVICE(self), st_req, NULL, 2000, /* this takes a long time */ FU_WACOM_DEVICE_CMD_FLAG_POLL_ON_WAITING, error)) { g_prefix_error(error, "failed to send eraseall command: "); return FALSE; } g_usleep(50); return TRUE; } static gboolean fu_wacom_emr_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuWacomDevice *self = FU_WACOM_DEVICE(device); g_autoptr(FuStructWacomRawRequest) st_req = fu_struct_wacom_raw_request_new(); if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in runtime mode, skipping"); return TRUE; } fu_struct_wacom_raw_request_set_report_id(st_req, FU_WACOM_RAW_BL_REPORT_ID_SET); fu_struct_wacom_raw_request_set_cmd(st_req, FU_WACOM_RAW_BL_CMD_ATTACH); fu_struct_wacom_raw_request_set_echo(st_req, fu_wacom_device_get_echo_next(FU_WACOM_DEVICE(self))); if (!fu_wacom_device_set_feature(self, st_req->data, st_req->len, error)) { g_prefix_error(error, "failed to switch to runtime mode: "); return FALSE; } /* does the device have to replug to bootloader mode */ if (fu_device_has_private_flag(device, FU_WACOM_RAW_DEVICE_FLAG_REQUIRES_WAIT_FOR_REPLUG)) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); } else { fu_device_remove_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); } /* success */ return TRUE; } static gboolean fu_wacom_emr_device_write_block(FuWacomEmrDevice *self, guint32 idx, guint32 address, const guint8 *data, gsize datasz, GError **error) { gsize blocksz = fu_wacom_device_get_block_sz(FU_WACOM_DEVICE(self)); g_autoptr(FuStructWacomRawRequest) st_req = fu_struct_wacom_raw_request_new(); guint8 *data_unused = st_req->data + FU_STRUCT_WACOM_RAW_REQUEST_OFFSET_DATA_UNUSED; /* check size */ if (datasz > FU_STRUCT_WACOM_RAW_REQUEST_SIZE_DATA) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "data size 0x%x too large for packet", (guint)datasz); return FALSE; } if (datasz != blocksz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "block size 0x%x != 0x%x untested", (guint)datasz, (guint)blocksz); return FALSE; } /* data */ fu_struct_wacom_raw_request_set_report_id(st_req, FU_WACOM_RAW_BL_REPORT_ID_SET); fu_struct_wacom_raw_request_set_cmd(st_req, FU_WACOM_RAW_BL_CMD_WRITE_FLASH); fu_struct_wacom_raw_request_set_echo(st_req, (guint8)idx + 1); fu_struct_wacom_raw_request_set_addr(st_req, address); fu_struct_wacom_raw_request_set_size8(st_req, datasz / 8); if (!fu_struct_wacom_raw_request_set_data(st_req, data, datasz, error)) return FALSE; /* cmd and data checksums */ data_unused[0] = fu_wacom_emr_device_calc_checksum(0x05 + 0x00 + 0x4c + 0x00, st_req->data, 8); data_unused[1] = fu_wacom_emr_device_calc_checksum(0x00, data, datasz); if (!fu_wacom_device_cmd(FU_WACOM_DEVICE(self), st_req, NULL, 1, FU_WACOM_DEVICE_CMD_FLAG_NONE, error)) { g_prefix_error(error, "failed to write at 0x%x: ", address); return FALSE; } return TRUE; } static gboolean fu_wacom_emr_device_write_firmware(FuDevice *device, FuChunkArray *chunks, FuProgress *progress, GError **error) { FuWacomEmrDevice *self = FU_WACOM_EMR_DEVICE(device); guint8 idx = 0; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 10, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 90, NULL); /* erase W9013 */ if (fu_device_has_instance_id(device, "WacomEMR_W9013", FU_DEVICE_INSTANCE_FLAG_QUIRKS)) { if (!fu_wacom_emr_device_w9013_erase_data(self, error)) return FALSE; for (guint i = 127; i >= 8; i--) { if (!fu_wacom_emr_device_w9013_erase_code(self, idx++, i, error)) return FALSE; } } /* erase W9021 */ if (fu_device_has_instance_id(device, "WacomEMR_W9021", FU_DEVICE_INSTANCE_FLAG_QUIRKS)) { if (!fu_wacom_emr_device_w9021_erase_all(self, error)) return FALSE; } fu_progress_step_done(progress); /* write */ for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (fu_wacom_common_block_is_empty(fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk))) continue; if (!fu_wacom_emr_device_write_block(self, fu_chunk_get_idx(chk), fu_chunk_get_address(chk), fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) return FALSE; fu_progress_set_percentage_full(fu_progress_get_child(progress), (gsize)i + 1, (gsize)fu_chunk_array_length(chunks)); } fu_progress_step_done(progress); return TRUE; } static gchar * fu_wacom_emr_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_device_get_version_format(device)); } static void fu_wacom_emr_device_init(FuWacomEmrDevice *self) { fu_device_set_name(FU_DEVICE(self), "Wacom EMR Device"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PAIR); } static void fu_wacom_emr_device_class_init(FuWacomEmrDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); FuWacomDeviceClass *wac_device_class = FU_WACOM_DEVICE_CLASS(klass); device_class->setup = fu_wacom_emr_device_setup; device_class->attach = fu_wacom_emr_device_attach; device_class->convert_version = fu_wacom_emr_device_convert_version; wac_device_class->write_firmware = fu_wacom_emr_device_write_firmware; } fwupd-2.0.10/plugins/wacom-raw/fu-wacom-emr-device.h000066400000000000000000000004771501337203100222050ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-wacom-device.h" #define FU_TYPE_WACOM_EMR_DEVICE (fu_wacom_emr_device_get_type()) G_DECLARE_FINAL_TYPE(FuWacomEmrDevice, fu_wacom_emr_device, FU, WACOM_EMR_DEVICE, FuWacomDevice) fwupd-2.0.10/plugins/wacom-raw/fu-wacom-raw-plugin.c000066400000000000000000000037211501337203100222400ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-wacom-aes-device.h" #include "fu-wacom-common.h" #include "fu-wacom-emr-device.h" #include "fu-wacom-raw-plugin.h" struct _FuWacomRawPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuWacomRawPlugin, fu_wacom_raw_plugin, FU_TYPE_PLUGIN) static void fu_wacom_raw_plugin_device_registered(FuPlugin *plugin, FuDevice *device) { /* is internal DRM device */ if (FU_IS_DRM_DEVICE(device) && fu_device_has_flag(device, FWUPD_DEVICE_FLAG_INTERNAL)) { GPtrArray *devices = fu_plugin_get_devices(plugin); for (guint i = 0; i < devices->len; i++) { FuDevice *device_tmp = g_ptr_array_index(devices, i); fu_device_add_child(device, device_tmp); } fu_plugin_cache_add(plugin, "drm", device); } } static gboolean fu_wacom_raw_plugin_device_created(FuPlugin *plugin, FuDevice *device, GError **error) { FuDevice *drm_device = fu_plugin_cache_lookup(plugin, "drm"); if (drm_device != NULL) fu_device_add_child(drm_device, device); return TRUE; } static void fu_wacom_raw_plugin_init(FuWacomRawPlugin *self) { } static void fu_wacom_raw_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); FuContext *ctx = fu_plugin_get_context(plugin); fu_context_add_quirk_key(ctx, "WacomI2cFlashBlockSize"); fu_context_add_quirk_key(ctx, "WacomI2cFlashBaseAddr"); fu_context_add_quirk_key(ctx, "WacomI2cFlashSize"); fu_plugin_add_device_gtype(plugin, FU_TYPE_WACOM_AES_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_WACOM_EMR_DEVICE); fu_plugin_add_udev_subsystem(plugin, "hidraw"); } static void fu_wacom_raw_plugin_class_init(FuWacomRawPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_wacom_raw_plugin_constructed; plugin_class->device_created = fu_wacom_raw_plugin_device_created; plugin_class->device_registered = fu_wacom_raw_plugin_device_registered; } fwupd-2.0.10/plugins/wacom-raw/fu-wacom-raw-plugin.h000066400000000000000000000003641501337203100222450ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuWacomRawPlugin, fu_wacom_raw_plugin, FU, WACOM_RAW_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/wacom-raw/fu-wacom-raw.rs000066400000000000000000000046331501337203100211510ustar00rootroot00000000000000// Copyright (C) 2024 Richard Hughes // SPDX-License-Identifier: LGPL-2.1+ #[repr(u8)] enum FuWacomRawOperationMode { Runtime = 0x00, Bootloader = 0x02, } enum FuWacomRawRc { Ok = 0x00, Busy = 0x80, Mcutype = 0x0C, Pid = 0x0D, Checksum1 = 0x81, Checksum2 = 0x82, Timeout = 0x87, InProgress = 0xFF, } // bootloader #[repr(u8)] enum FuWacomRawBlReportId { Type = 0x02, Set = 0x07, Get = 0x08, } #[repr(u8)] enum FuWacomRawBlCmd { EraseFlash = 0x00, WriteFlash = 0x01, VerifyFlash = 0x02, Attach = 0x03, GetBlver = 0x04, GetMputype = 0x05, CheckMode = 0x07, EraseDatamem = 0x0E, AllErase = 0x90, } #[derive(New, Getters)] #[repr(C, packed)] struct FuStructWacomRawRequest { report_id: FuWacomRawBlReportId, cmd: u8, echo: u8, addr: u32le, size8: u8, data: [u8; 128], data_unused: [u8; 121], } #[derive(Parse)] #[repr(C, packed)] struct FuStructWacomRawResponse { report_id: FuWacomRawBlReportId, cmd: u8, echo: u8, resp: u8, _unused: [u8; 132], } #[derive(Parse)] #[repr(C, packed)] struct FuStructWacomRawBlVerifyResponse { report_id: FuWacomRawBlReportId, cmd: FuWacomRawBlCmd, echo: u8, addr: u32le, size8: u8, _unknown1: [u8; 6], pid: u16le, _unknown2: [u8; 120], } // firmware #[repr(u8)] enum FuWacomRawFwReportId { General = 0x02, Status = 0x04, } #[repr(u8)] enum FuWacomRawFwCmd { QueryMode = 0x00, Detach = 0x02, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructWacomRawFwStatusRequest { report_id: FuWacomRawFwReportId == Status, _reserved2: [u8; 15], } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructWacomRawFwStatusResponse { report_id: FuWacomRawFwReportId == Status, _reserved1: [u8; 10], version_major: u16le, version_minor: u8, _reserved2: [u8; 2], } #[derive(New, Default)] #[repr(C, packed)] struct FuStructWacomRawFwDetachRequest { report_id: FuWacomRawFwReportId == General, cmd: FuWacomRawFwCmd == Detach, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructWacomRawFwQueryModeRequest { report_id: FuWacomRawFwReportId == General, cmd: FuWacomRawFwCmd == QueryMode, } #[derive(Parse, Default)] #[repr(C, packed)] struct FuStructWacomRawFwQueryModeResponse { report_id: FuWacomRawFwReportId == General, mode: FuWacomRawOperationMode, } fwupd-2.0.10/plugins/wacom-raw/meson.build000066400000000000000000000011401501337203100204260ustar00rootroot00000000000000if host_machine.system() == 'linux' plugins += {meson.current_source_dir().split('/')[-1]: true} cargs = ['-DG_LOG_DOMAIN="FuPluginWacomRaw"'] plugin_quirks += files('wacom-raw.quirk') plugin_builtins += static_library('fu_plugin_wacom_raw', rustgen.process('fu-wacom-raw.rs'), sources: [ 'fu-wacom-raw-plugin.c', 'fu-wacom-common.c', 'fu-wacom-device.c', 'fu-wacom-aes-device.c', 'fu-wacom-emr-device.c', ], include_directories: plugin_incdirs, c_args: cargs, dependencies: plugin_deps, link_with: plugin_libs, ) device_tests += files( 'tests/wacom-g14t.json', ) endif fwupd-2.0.10/plugins/wacom-raw/tests/000077500000000000000000000000001501337203100174325ustar00rootroot00000000000000fwupd-2.0.10/plugins/wacom-raw/tests/wacom-g14t.json000066400000000000000000000007271501337203100222160ustar00rootroot00000000000000{ "name": "Wacom G14T", "interactive": false, "steps": [ { "url": "80bfa20d1ace301fe5c4a2b514c4b58cf108992483ed92b6195296b4e54db13e-G14T_5309_v0002_rev07.cab", "emulation-url": "d1eefe9a81a62807755f815f6fc8eae06dd479e7238513740f347eaf5d3f01ac-G14T_5309_v0002_rev07.zip", "components": [ { "version": "0002.07", "guids": [ "284cbc14-c704-5cf0-b0a1-f7a0c0688438" ] } ] } ] } fwupd-2.0.10/plugins/wacom-raw/wacom-raw.quirk000066400000000000000000000056641501337203100212550ustar00rootroot00000000000000# Devices that do "replug" and thus don't change VID:PID to the bootloader # need to have an extra GUID of WacomAES or WacomEMR added so that the flash # constants are set correctly. # This device has USB I/F [HIDRAW\VEN_056A&DEV_5315] Plugin = wacom_raw Guid[quirk] = WacomAES Flags = requires-wait-for-replug CounterpartGuid = HIDRAW\VEN_056A&DEV_0094 # Lenovo X1 Yoga Gen7 5308 [HIDRAW\VEN_056A&DEV_5308] Plugin = wacom_raw Guid[quirk] = WacomAES # Lenovo X1 Yoga Gen7 5309 [HIDRAW\VEN_056A&DEV_5309] Plugin = wacom_raw Guid[quirk] = WacomAES # Lenovo X1 Yoga Gen7 530A [HIDRAW\VEN_056A&DEV_530A] Plugin = wacom_raw Guid[quirk] = WacomAES # Lenovo X1 Yoga Gen7 530B [HIDRAW\VEN_056A&DEV_530B] Plugin = wacom_raw Guid[quirk] = WacomAES # Lenovo X1 Yoga Gen7 530E [HIDRAW\VEN_056A&DEV_530E] Plugin = wacom_raw Guid[quirk] = WacomAES # Lenovo X1 Yoga Gen7 52B5 [HIDRAW\VEN_056A&DEV_52B5] Plugin = wacom_raw Guid[quirk] = WacomAES # Lenovo X1 Yoga Gen7 52C4 [HIDRAW\VEN_056A&DEV_52C4] Plugin = wacom_raw Guid[quirk] = WacomAES # Dell Chromebook Enterprise 5300 [HIDRAW\VEN_2D1F&DEV_4946] Plugin = wacom_raw Guid[quirk] = WacomAES # Moffet 14-LGD-TPK [HIDRAW\VEN_2D1F&DEV_4970] Plugin = wacom_raw Guid[quirk] = WacomAES Flags = self-recovery # Moffet 14-Sharp-HH [HIDRAW\VEN_2D1F&DEV_4971] Plugin = wacom_raw Guid[quirk] = WacomAES Flags = self-recovery # Moffet 14-Sharp-VIA [HIDRAW\VEN_2D1F&DEV_4972] Plugin = wacom_raw Guid[quirk] = WacomAES Flags = self-recovery # Dell Latitude 5175 [HIDRAW\VEN_056A&DEV_4807] Plugin = wacom_raw Guid[quirk] = WacomAES # Dell XPS 12 9250 [HIDRAW\VEN_056A&DEV_4822] Plugin = wacom_raw Guid[quirk] = WacomAES # Dell Venue 8 Pro 5855 [HIDRAW\VEN_056A&DEV_4824] Plugin = wacom_raw Guid[quirk] = WacomAES # Dell XPS 13 9365 [HIDRAW\VEN_056A&DEV_4831] Plugin = wacom_raw Guid[quirk] = WacomAES # Dell Latitude 5285 [HIDRAW\VEN_056A&DEV_484C] Plugin = wacom_raw Guid[quirk] = WacomAES # Dell Latitude 7390 2-in-1 [HIDRAW\VEN_056A&DEV_4841] Plugin = wacom_raw Guid[quirk] = WacomAES # Dell XPS-15 9575 [HIDRAW\VEN_056A&DEV_4875] Plugin = wacom_raw Guid[quirk] = WacomAES # Dell Latitude 7400 2-in-1 [HIDRAW\VEN_056A&DEV_48C9] Plugin = wacom_raw Guid[quirk] = WacomAES # Dell XPS-15 9570 [HIDRAW\VEN_056A&DEV_488F] Plugin = wacom_raw Guid[quirk] = WacomAES # Dell XPS 13 7390 2-in-1 [HIDRAW\VEN_056A&DEV_48ED] Plugin = wacom_raw Guid[quirk] = WacomAES # AES bootloader mode [HIDRAW\VEN_056A&DEV_0094] Plugin = wacom_raw Guid[quirk] = WacomAES Flags = is-bootloader # EMR bootloader mode [HIDRAW\VEN_056A&DEV_012B] Plugin = wacom_raw Guid[quirk] = WacomEMR Flags = is-bootloader [WacomEMR_W9013] WacomI2cFlashBlockSize = 64 WacomI2cFlashBaseAddr = 0x2000 FirmwareSizeMax = 0x1e000 [WacomEMR_W9021] WacomI2cFlashBlockSize = 256 WacomI2cFlashBaseAddr = 0x3000 FirmwareSizeMax = 0x3c000 [WacomEMR] GType = FuWacomEmrDevice [WacomAES] GType = FuWacomAesDevice WacomI2cFlashBlockSize = 128 WacomI2cFlashBaseAddr = 0x8000 FirmwareSizeMax = 0x28000 fwupd-2.0.10/plugins/wacom-usb/000077500000000000000000000000001501337203100162705ustar00rootroot00000000000000fwupd-2.0.10/plugins/wacom-usb/README.md000066400000000000000000000034451501337203100175550ustar00rootroot00000000000000--- title: Plugin: Wacom USB --- ## Introduction Wacom provides interactive pen displays, pen tablets, and styluses to equip and inspire everyone make the world a more creative place. From 2016 Wacom has been using a HID-based proprietary flashing algorithm which has been documented by support team at Wacom and provided under NDA under the understanding it would be used to build a plugin under a LGPLv2+ license. Wacom devices are actually composite devices, with the main ARM CPU being programmed using a more complicated erase, write, verify algorithm based on a historical update protocol. The "sub-module" devices use a newer protocol, again based on HID, but are handled differently depending on their type. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in the following formats: * Touch module: Intel HEX file format * Bluetooth module: Unknown airoflash file format * EMR module: Plain SREC file format * Main module: SREC file format, with a custom `WACOM` vendor header This plugin supports the following protocol ID: * `com.wacom.usb` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_056A&PID_0378` ## Update Behavior The firmware is deployed when the device is in normal runtime mode, and the device will reset when the new firmware has been written. ## Vendor ID Security The vendor ID is set from the USB vendor, for example set to `USB:0x056A` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.2.2`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Jason Gerecke: @jigpu fwupd-2.0.10/plugins/wacom-usb/data/000077500000000000000000000000001501337203100172015ustar00rootroot00000000000000fwupd-2.0.10/plugins/wacom-usb/data/lsusb.txt000066400000000000000000000037021501337203100210740ustar00rootroot00000000000000Bus 001 Device 023: ID 056a:0378 Wacom Co., Ltd CTL-6100WL [Intuos BT (M)] Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 2.00 bDeviceClass 0 bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 64 idVendor 0x056a Wacom Co., Ltd idProduct 0x0378 CTL-6100WL [Intuos BT (M)] bcdDevice 1.66 iManufacturer 1 Wacom Co.,Ltd. iProduct 2 Intuos BT M iSerial 3 8BH00U2012294 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 0x0022 bNumInterfaces 1 bConfigurationValue 1 iConfiguration 0 bmAttributes 0x80 (Bus Powered) MaxPower 500mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 1 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 0 bInterfaceProtocol 0 iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.10 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 759 Report Descriptors: ** UNAVAILABLE ** Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0040 1x 64 bytes bInterval 1 Device Status: 0x0000 (Bus Powered) fwupd-2.0.10/plugins/wacom-usb/fu-self-test.c000066400000000000000000000060331501337203100207540ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fu-wac-common.h" #include "fu-wac-firmware.h" #include "fu-wac-struct.h" static void fu_wac_firmware_parse_func(void) { gboolean ret; g_autofree gchar *fn = NULL; g_autoptr(FuFirmware) firmware = fu_wac_firmware_new(); g_autoptr(FuFirmware) img = NULL; g_autoptr(GBytes) blob_block = NULL; g_autoptr(GError) error = NULL; g_autoptr(GFile) file = NULL; /* parse the test file */ fn = g_test_build_filename(G_TEST_DIST, "tests", "test.wac", NULL); if (!g_file_test(fn, G_FILE_TEST_EXISTS)) { g_test_skip("no data file found"); return; } file = g_file_new_for_path(fn); ret = fu_firmware_parse_file(firmware, file, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, &error); g_assert_no_error(error); g_assert_true(ret); /* get image data */ img = fu_firmware_get_image_by_id(firmware, 0, &error); g_assert_no_error(error); g_assert_nonnull(img); /* get block */ blob_block = fu_firmware_write_chunk(img, 0x8008000, 1024, &error); g_assert_no_error(error); g_assert_nonnull(blob_block); fu_wac_buffer_dump("IMG", FU_WAC_REPORT_ID_MODULE, g_bytes_get_data(blob_block, NULL), g_bytes_get_size(blob_block)); } static void fu_wac_firmware_xml_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autofree gchar *csum1 = NULL; g_autofree gchar *csum2 = NULL; g_autofree gchar *xml_out = NULL; g_autofree gchar *xml_src = NULL; g_autoptr(FuFirmware) firmware1 = fu_wac_firmware_new(); g_autoptr(FuFirmware) firmware2 = fu_wac_firmware_new(); g_autoptr(GError) error = NULL; /* build and write */ filename = g_test_build_filename(G_TEST_DIST, "tests", "wacom-usb.builder.xml", NULL); ret = g_file_get_contents(filename, &xml_src, NULL, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_firmware_build_from_xml(firmware1, xml_src, &error); g_assert_no_error(error); g_assert_true(ret); csum1 = fu_firmware_get_checksum(firmware1, G_CHECKSUM_SHA1, &error); g_assert_no_error(error); g_assert_cmpstr(csum1, ==, "346f6196449b356777cf241f6edb039d503b88a1"); /* ensure we can round-trip */ xml_out = fu_firmware_export_to_xml(firmware1, FU_FIRMWARE_EXPORT_FLAG_NONE, &error); g_assert_no_error(error); ret = fu_firmware_build_from_xml(firmware2, xml_out, &error); g_assert_no_error(error); g_assert_true(ret); csum2 = fu_firmware_get_checksum(firmware2, G_CHECKSUM_SHA1, &error); g_assert_cmpstr(csum1, ==, csum2); } int main(int argc, char **argv) { (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); g_type_ensure(FU_TYPE_SREC_FIRMWARE); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); /* log everything */ (void)g_setenv("G_MESSAGES_DEBUG", "all", FALSE); /* tests go here */ g_test_add_func("/wac/firmware{parse}", fu_wac_firmware_parse_func); g_test_add_func("/wac/firmware{xml}", fu_wac_firmware_xml_func); return g_test_run(); } fwupd-2.0.10/plugins/wacom-usb/fu-wac-android-device.c000066400000000000000000000014361501337203100224750ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-wac-android-device.h" struct _FuWacAndroidDevice { FuHidDevice parent_instance; }; G_DEFINE_TYPE(FuWacAndroidDevice, fu_wac_android_device, FU_TYPE_HID_DEVICE) static void fu_wac_android_device_init(FuWacAndroidDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "com.wacom.usb"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_INPUT_TABLET); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_inhibit(FU_DEVICE(self), "hw", "Switch into PC mode by holding down the " "two outermost ExpressKeys for 4 seconds"); } static void fu_wac_android_device_class_init(FuWacAndroidDeviceClass *klass) { } fwupd-2.0.10/plugins/wacom-usb/fu-wac-android-device.h000066400000000000000000000005031501337203100224740ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_WAC_ANDROID_DEVICE (fu_wac_android_device_get_type()) G_DECLARE_FINAL_TYPE(FuWacAndroidDevice, fu_wac_android_device, FU, WAC_ANDROID_DEVICE, FuHidDevice) fwupd-2.0.10/plugins/wacom-usb/fu-wac-common.c000066400000000000000000000010131501337203100210770ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fu-wac-common.h" #include "fu-wac-struct.h" void fu_wac_buffer_dump(const gchar *title, guint8 cmd, const guint8 *buf, gsize sz) { g_autofree gchar *tmp = NULL; tmp = g_strdup_printf("%s %s (%" G_GSIZE_FORMAT ")", title, fu_wac_report_id_to_string(cmd), sz); fu_dump_raw(G_LOG_DOMAIN, tmp, buf, sz); } fwupd-2.0.10/plugins/wacom-usb/fu-wac-common.h000066400000000000000000000010571501337203100211140ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_WAC_PACKET_LEN 512 #define FU_WAC_REPORT_ID_COMMAND 0x01 #define FU_WAC_REPORT_ID_STATUS 0x02 #define FU_WAC_REPORT_ID_CONTROL 0x03 #define FU_WAC_REPORT_ID_GET_FIRMWARE_VERSION_MAIN 0x07 #define FU_WAC_REPORT_ID_GET_FIRMWARE_VERSION_TOUCH 0x07 #define FU_WAC_REPORT_ID_GET_FIRMWARE_VERSION_BLUETOOTH 0x16 void fu_wac_buffer_dump(const gchar *title, guint8 cmd, const guint8 *buf, gsize sz); fwupd-2.0.10/plugins/wacom-usb/fu-wac-device.c000066400000000000000000000743131501337203100210630ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-wac-common.h" #include "fu-wac-device.h" #include "fu-wac-firmware.h" #include "fu-wac-module-bluetooth-id6.h" #include "fu-wac-module-bluetooth-id9.h" #include "fu-wac-module-bluetooth.h" #include "fu-wac-module-scaler.h" #include "fu-wac-module-sub-cpu.h" #include "fu-wac-module-touch-id7.h" #include "fu-wac-module-touch.h" #include "fu-wac-struct.h" typedef struct { guint32 start_addr; guint32 block_sz; guint16 write_sz; /* bit 15 is write protection flag */ } FuWacFlashDescriptor; #define FU_WAC_DEVICE_TIMEOUT 5000 /* ms */ #define FU_WAC_DEVICE_MODULE_RETRY_DELAY 100 /* ms */ struct _FuWacDevice { FuHidDevice parent_instance; GPtrArray *flash_descriptors; GArray *checksums; guint32 status_word; guint16 firmware_index; guint16 loader_ver; guint16 read_data_sz; guint16 write_word_sz; guint16 write_block_sz; /* usb transfer size */ guint16 nr_flash_blocks; guint16 configuration; }; G_DEFINE_TYPE(FuWacDevice, fu_wac_device, FU_TYPE_HID_DEVICE) static gboolean fu_wac_device_flash_descriptor_is_wp(const FuWacFlashDescriptor *fd) { return fd->write_sz & 0x8000; } static void fu_wac_device_flash_descriptor_to_string(FuWacFlashDescriptor *fd, guint idt, GString *str) { fwupd_codec_string_append_hex(str, idt, "StartAddr", fd->start_addr); fwupd_codec_string_append_hex(str, idt, "BlockSize", fd->block_sz); fwupd_codec_string_append_hex(str, idt, "WriteSize", fd->write_sz & ~0x8000); fwupd_codec_string_append_bool(str, idt, "Protected", fu_wac_device_flash_descriptor_is_wp(fd)); } static void fu_wac_device_to_string(FuDevice *device, guint idt, GString *str) { FuWacDevice *self = FU_WAC_DEVICE(device); g_autofree gchar *status_str = NULL; if (self->firmware_index != 0xffff) { g_autofree gchar *tmp = g_strdup_printf("0x%04x", self->firmware_index); fwupd_codec_string_append(str, idt, "FwIndex", tmp); } if (self->loader_ver > 0) { g_autofree gchar *tmp = g_strdup_printf("0x%04x", (guint)self->loader_ver); fwupd_codec_string_append(str, idt, "LoaderVer", tmp); } if (self->read_data_sz > 0) { g_autofree gchar *tmp = g_strdup_printf("0x%04x", (guint)self->read_data_sz); fwupd_codec_string_append(str, idt, "ReadDataSize", tmp); } if (self->write_word_sz > 0) { g_autofree gchar *tmp = g_strdup_printf("0x%04x", (guint)self->write_word_sz); fwupd_codec_string_append(str, idt, "WriteWordSize", tmp); } if (self->write_block_sz > 0) { g_autofree gchar *tmp = g_strdup_printf("0x%04x", (guint)self->write_block_sz); fwupd_codec_string_append(str, idt, "WriteBlockSize", tmp); } if (self->nr_flash_blocks > 0) { g_autofree gchar *tmp = g_strdup_printf("0x%04x", (guint)self->nr_flash_blocks); fwupd_codec_string_append(str, idt, "NrFlashBlocks", tmp); } if (self->configuration != 0xffff) { g_autofree gchar *tmp = g_strdup_printf("0x%04x", (guint)self->configuration); fwupd_codec_string_append(str, idt, "Configuration", tmp); } for (guint i = 0; i < self->flash_descriptors->len; i++) { FuWacFlashDescriptor *fd = g_ptr_array_index(self->flash_descriptors, i); g_autofree gchar *title = g_strdup_printf("FlashDescriptor%02u", i); fwupd_codec_string_append(str, idt, title, ""); fu_wac_device_flash_descriptor_to_string(fd, idt + 1, str); } status_str = fu_wac_device_status_to_string(self->status_word); fwupd_codec_string_append(str, idt, "Status", status_str); } gboolean fu_wac_device_get_feature_report(FuWacDevice *self, guint8 *buf, gsize bufsz, FuHidDeviceFlags flags, GError **error) { guint8 cmd = buf[0]; /* hit hardware */ if (!fu_hid_device_get_report(FU_HID_DEVICE(self), cmd, buf, bufsz, FU_WAC_DEVICE_TIMEOUT, flags | FU_HID_DEVICE_FLAG_IS_FEATURE, error)) return FALSE; /* check packet */ if (buf[0] != cmd) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "command response was %i expected %i", buf[0], cmd); return FALSE; } return TRUE; } gboolean fu_wac_device_set_feature_report(FuWacDevice *self, guint8 *buf, gsize bufsz, FuHidDeviceFlags flags, GError **error) { return fu_hid_device_set_report(FU_HID_DEVICE(self), buf[0], buf, bufsz, FU_WAC_DEVICE_TIMEOUT, flags | FU_HID_DEVICE_FLAG_IS_FEATURE | FU_HID_DEVICE_FLAG_RETRY_FAILURE, error); } static gboolean fu_wac_device_ensure_flash_descriptors(FuWacDevice *self, GError **error) { gsize sz = (self->nr_flash_blocks * 10) + 1; g_autofree guint8 *buf = NULL; /* already done */ if (self->flash_descriptors->len > 0) return TRUE; /* hit hardware */ buf = g_malloc(sz); memset(buf, 0xff, sz); buf[0] = FU_WAC_REPORT_ID_GET_FLASH_DESCRIPTOR; if (!fu_wac_device_get_feature_report(self, buf, sz, FU_HID_DEVICE_FLAG_NONE, error)) return FALSE; /* parse */ for (guint i = 0; i < self->nr_flash_blocks; i++) { g_autofree FuWacFlashDescriptor *fd = g_new0(FuWacFlashDescriptor, 1); const guint blksz = 0x0A; if (!fu_memread_uint32_safe(buf, sz, (i * blksz) + 1, &fd->start_addr, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memread_uint32_safe(buf, sz, (i * blksz) + 5, &fd->block_sz, G_LITTLE_ENDIAN, error)) return FALSE; if (!fu_memread_uint16_safe(buf, sz, (i * blksz) + 9, &fd->write_sz, G_LITTLE_ENDIAN, error)) return FALSE; g_ptr_array_add(self->flash_descriptors, g_steal_pointer(&fd)); } if (self->flash_descriptors->len > G_MAXUINT16) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "too many flash descriptors for hardware: 0x%x", self->flash_descriptors->len); return FALSE; } g_info("added %u flash descriptors", self->flash_descriptors->len); return TRUE; } static gboolean fu_wac_device_ensure_status(FuWacDevice *self, GError **error) { g_autofree gchar *str = NULL; guint8 buf[] = {[0] = FU_WAC_REPORT_ID_GET_STATUS, [1 ... 4] = 0xff}; /* hit hardware */ if (!fu_wac_device_get_feature_report(self, buf, sizeof(buf), FU_HID_DEVICE_FLAG_NONE, error)) return FALSE; /* parse */ self->status_word = fu_memread_uint32(buf + 1, G_LITTLE_ENDIAN); str = fu_wac_device_status_to_string(self->status_word); g_debug("status now: %s", str); return TRUE; } static gboolean fu_wac_device_ensure_checksums(FuWacDevice *self, GError **error) { gsize sz = (self->nr_flash_blocks * 4) + 5; guint32 updater_version; g_autofree guint8 *buf = g_malloc(sz); /* hit hardware */ memset(buf, 0xff, sz); buf[0] = FU_WAC_REPORT_ID_GET_CHECKSUMS; if (!fu_wac_device_get_feature_report(self, buf, sz, FU_HID_DEVICE_FLAG_NONE, error)) return FALSE; /* parse */ updater_version = fu_memread_uint32(buf + 1, G_LITTLE_ENDIAN); g_info("updater-version: %" G_GUINT32_FORMAT, updater_version); /* get block checksums */ g_array_set_size(self->checksums, 0); for (guint i = 0; i < self->nr_flash_blocks; i++) { guint32 csum = fu_memread_uint32(buf + 5 + (i * 4), G_LITTLE_ENDIAN); g_debug("checksum block %02u: 0x%08x", i, (guint)csum); g_array_append_val(self->checksums, csum); } g_debug("added %u checksums", self->flash_descriptors->len); return TRUE; } static gboolean fu_wac_device_ensure_firmware_index(FuWacDevice *self, GError **error) { guint8 buf[] = {[0] = FU_WAC_REPORT_ID_GET_CURRENT_FIRMWARE_IDX, [1 ... 2] = 0xff}; /* hit hardware */ if (!fu_wac_device_get_feature_report(self, buf, sizeof(buf), FU_HID_DEVICE_FLAG_NONE, error)) return FALSE; /* parse */ self->firmware_index = fu_memread_uint16(buf + 1, G_LITTLE_ENDIAN); return TRUE; } static gboolean fu_wac_device_ensure_parameters(FuWacDevice *self, GError **error) { guint8 buf[] = {[0] = FU_WAC_REPORT_ID_GET_PARAMETERS, [1 ... 12] = 0xff}; /* hit hardware */ if (!fu_wac_device_get_feature_report(self, buf, sizeof(buf), FU_HID_DEVICE_FLAG_NONE, error)) return FALSE; /* parse */ self->loader_ver = fu_memread_uint16(buf + 1, G_LITTLE_ENDIAN); self->read_data_sz = fu_memread_uint16(buf + 3, G_LITTLE_ENDIAN); self->write_word_sz = fu_memread_uint16(buf + 5, G_LITTLE_ENDIAN); self->write_block_sz = fu_memread_uint16(buf + 7, G_LITTLE_ENDIAN); self->nr_flash_blocks = fu_memread_uint16(buf + 9, G_LITTLE_ENDIAN); self->configuration = fu_memread_uint16(buf + 11, G_LITTLE_ENDIAN); return TRUE; } static gboolean fu_wac_device_write_block(FuWacDevice *self, guint32 addr, GBytes *blob, GError **error) { const guint8 *tmp; gsize bufsz = self->write_block_sz + 5; gsize sz = 0; g_autofree guint8 *buf = NULL; /* check size */ tmp = g_bytes_get_data(blob, &sz); if (sz > self->write_block_sz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "packet was too large at %" G_GSIZE_FORMAT " bytes", sz); return FALSE; } /* build packet */ buf = g_malloc(bufsz); memset(buf, 0xff, bufsz); buf[0] = FU_WAC_REPORT_ID_WRITE_BLOCK; fu_memwrite_uint32(buf + 1, addr, G_LITTLE_ENDIAN); if (sz > 0) { if (!fu_memcpy_safe(buf, bufsz, 0x5, /* dst */ tmp, sz, 0x0, /* src */ sz, error)) return FALSE; } /* hit hardware */ return fu_wac_device_set_feature_report(self, buf, bufsz, FU_HID_DEVICE_FLAG_NONE, error); } static gboolean fu_wac_device_erase_block(FuWacDevice *self, guint32 addr, GError **error) { guint8 buf[] = {[0] = FU_WAC_REPORT_ID_ERASE_BLOCK, [1 ... 4] = 0xff}; /* build packet */ fu_memwrite_uint32(buf + 1, addr, G_LITTLE_ENDIAN); /* hit hardware */ return fu_wac_device_set_feature_report(self, buf, sizeof(buf), FU_HID_DEVICE_FLAG_NONE, error); } gboolean fu_wac_device_update_reset(FuWacDevice *self, GError **error) { guint8 buf[] = {[0] = FU_WAC_REPORT_ID_UPDATE_RESET, [1 ... 4] = 0xff}; /* hit hardware */ return fu_wac_device_set_feature_report(self, buf, sizeof(buf), FU_HID_DEVICE_FLAG_NONE, error); } static gboolean fu_wac_device_set_checksum_of_block(FuWacDevice *self, guint16 block_nr, guint32 checksum, GError **error) { guint8 buf[] = {[0] = FU_WAC_REPORT_ID_SET_CHECKSUM_FOR_BLOCK, [1 ... 6] = 0xff}; /* build packet */ fu_memwrite_uint16(buf + 1, block_nr, G_LITTLE_ENDIAN); fu_memwrite_uint32(buf + 3, checksum, G_LITTLE_ENDIAN); /* hit hardware */ return fu_wac_device_set_feature_report(self, buf, sizeof(buf), FU_HID_DEVICE_FLAG_NONE, error); } static gboolean fu_wac_device_calculate_checksum_of_block(FuWacDevice *self, guint16 block_nr, GError **error) { guint8 buf[] = {[0] = FU_WAC_REPORT_ID_CALCULATE_CHECKSUM_FOR_BLOCK, [1 ... 2] = 0xff}; /* build packet */ fu_memwrite_uint16(buf + 1, block_nr, G_LITTLE_ENDIAN); /* hit hardware */ return fu_wac_device_set_feature_report(self, buf, sizeof(buf), FU_HID_DEVICE_FLAG_NONE, error); } static gboolean fu_wac_device_write_checksum_table(FuWacDevice *self, GError **error) { guint8 buf[] = {[0] = FU_WAC_REPORT_ID_WRITE_CHECKSUM_TABLE, [1 ... 4] = 0xff}; /* hit hardware */ return fu_wac_device_set_feature_report(self, buf, sizeof(buf), FU_HID_DEVICE_FLAG_NONE, error); } gboolean fu_wac_device_switch_to_flash_loader(FuWacDevice *self, GError **error) { guint8 buf[] = {[0] = FU_WAC_REPORT_ID_SWITCH_TO_FLASH_LOADER, [1] = 0x05, [2] = 0x6a}; /* hit hardware */ return fu_wac_device_set_feature_report(self, buf, sizeof(buf), FU_HID_DEVICE_FLAG_NONE, error); } static gboolean fu_wac_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuWacDevice *self = FU_WAC_DEVICE(device); gsize blocks_done = 0; gsize blocks_total = 0; g_autofree guint32 *csum_local = NULL; g_autoptr(FuFirmware) img = NULL; g_autoptr(GHashTable) fd_blobs = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 95, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 2, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, NULL); /* get current selected device */ if (!fu_wac_device_ensure_firmware_index(self, error)) return FALSE; /* use the correct image from the firmware */ img = fu_firmware_get_image_by_idx(firmware, self->firmware_index == 1 ? 1 : 0, error); if (img == NULL) return FALSE; g_debug("using image at addr 0x%0x", (guint)fu_firmware_get_addr(img)); /* get firmware parameters (page sz and transfer sz) */ if (!fu_wac_device_ensure_parameters(self, error)) return FALSE; /* get the current flash descriptors */ if (!fu_wac_device_ensure_flash_descriptors(self, error)) return FALSE; /* get the updater protocol version */ if (!fu_wac_device_ensure_checksums(self, error)) return FALSE; fu_progress_step_done(progress); /* clear all checksums of pages */ for (guint i = 0; i < self->flash_descriptors->len; i++) { FuWacFlashDescriptor *fd = g_ptr_array_index(self->flash_descriptors, i); if (fu_wac_device_flash_descriptor_is_wp(fd)) continue; if (!fu_wac_device_set_checksum_of_block(self, i, 0x0, error)) return FALSE; } fu_progress_step_done(progress); /* get the blobs for each chunk */ fd_blobs = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)g_bytes_unref); for (guint i = 0; i < self->flash_descriptors->len; i++) { FuWacFlashDescriptor *fd = g_ptr_array_index(self->flash_descriptors, i); GBytes *blob_block; g_autoptr(GBytes) blob_tmp = NULL; if (fu_wac_device_flash_descriptor_is_wp(fd)) continue; blob_tmp = fu_firmware_write_chunk(img, fd->start_addr, fd->block_sz, NULL); if (blob_tmp == NULL) break; blob_block = fu_bytes_pad(blob_tmp, fd->block_sz, 0xFF); g_hash_table_insert(fd_blobs, fd, blob_block); } /* checksum actions post-write */ blocks_total = g_hash_table_size(fd_blobs); /* write the data into the flash page */ csum_local = g_new0(guint32, self->flash_descriptors->len); for (guint i = 0; i < self->flash_descriptors->len; i++) { FuWacFlashDescriptor *fd = g_ptr_array_index(self->flash_descriptors, i); GBytes *blob_block; g_autoptr(FuChunkArray) chunks = NULL; /* if page is protected */ if (fu_wac_device_flash_descriptor_is_wp(fd)) continue; /* get data for page */ blob_block = g_hash_table_lookup(fd_blobs, fd); if (blob_block == NULL) break; /* ignore empty blocks */ if (fu_bytes_is_empty(blob_block)) { g_debug("empty block, ignoring"); fu_progress_set_percentage_full(fu_progress_get_child(progress), blocks_done++, blocks_total); continue; } /* erase entire block */ if (!fu_wac_device_erase_block(self, i, error)) return FALSE; /* write block in chunks */ chunks = fu_chunk_array_new_from_bytes(blob_block, fd->start_addr, FU_CHUNK_PAGESZ_NONE, self->write_block_sz); for (guint j = 0; j < fu_chunk_array_length(chunks); j++) { g_autoptr(FuChunk) chk = NULL; g_autoptr(GBytes) blob_chunk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, j, error); if (chk == NULL) return FALSE; blob_chunk = fu_chunk_get_bytes(chk); if (!fu_wac_device_write_block(self, fu_chunk_get_address(chk), blob_chunk, error)) return FALSE; } /* calculate expected checksum and save to device RAM */ csum_local[i] = GUINT32_TO_LE(/* nocheck:blocked */ fu_sum32w_bytes(blob_block, G_LITTLE_ENDIAN)); g_debug("block checksum %02u: 0x%08x", i, csum_local[i]); if (!fu_wac_device_set_checksum_of_block(self, i, csum_local[i], error)) return FALSE; /* update device progress */ fu_progress_set_percentage_full(fu_progress_get_child(progress), blocks_done++, blocks_total); } fu_progress_step_done(progress); /* check at least one block was written */ if (blocks_done == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "empty firmware image or all blocks write-protected"); return FALSE; } /* calculate CRC inside device */ for (guint i = 0; i < self->flash_descriptors->len; i++) { if (!fu_wac_device_calculate_checksum_of_block(self, i, error)) return FALSE; } /* read all CRC of all pages and verify with local CRC */ if (!fu_wac_device_ensure_checksums(self, error)) return FALSE; for (guint i = 0; i < self->flash_descriptors->len; i++) { FuWacFlashDescriptor *fd = g_ptr_array_index(self->flash_descriptors, i); GBytes *blob_block; guint32 csum_rom; /* if page is protected */ if (fu_wac_device_flash_descriptor_is_wp(fd)) continue; /* no more written pages */ blob_block = g_hash_table_lookup(fd_blobs, fd); if (blob_block == NULL) continue; if (fu_bytes_is_empty(blob_block)) continue; /* check checksum matches */ csum_rom = g_array_index(self->checksums, guint32, i); if (csum_rom != csum_local[i]) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed local checksum at block %u, " "got 0x%08x expected 0x%08x", i, (guint)csum_rom, (guint)csum_local[i]); return FALSE; } g_debug("matched checksum at block %u of 0x%08x", i, csum_rom); } fu_progress_step_done(progress); /* store host CRC into flash */ if (!fu_wac_device_write_checksum_table(self, error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_wac_device_add_modules_bluetooth(FuWacDevice *self, GError **error) { g_autofree gchar *name = NULL; g_autofree gchar *name_id6 = NULL; g_autoptr(FuWacModule) module = NULL; g_autoptr(FuWacModule) module_id6 = NULL; guint16 fw_ver; /* it can take up to 5s to get the new version after a fw update */ for (guint i = 0; i < 5; i++) { guint8 buf[] = {[0] = FU_WAC_REPORT_ID_GET_FIRMWARE_VERSION_BLUETOOTH, [1 ... 14] = 0xff}; if (!fu_wac_device_get_feature_report(self, buf, sizeof(buf), FU_HID_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "Failed to get GetFirmwareVersionBluetooth: "); return FALSE; } if (!fu_memread_uint16_safe(buf, sizeof(buf), 1, &fw_ver, G_LITTLE_ENDIAN, error)) return FALSE; if (fw_ver != 0) break; fu_device_sleep(FU_DEVICE(self), 1000); /* ms */ } /* Success! But legacy bluetooth can't tell us which module the device needs. * Initialize both and rely on the firmware update containing the appropriate * package. */ name = g_strdup_printf("%s [Legacy Bluetooth Module]", fu_device_get_name(FU_DEVICE(self))); module = fu_wac_module_bluetooth_new(FU_DEVICE(self)); fu_device_add_child(FU_DEVICE(self), FU_DEVICE(module)); fu_device_set_name(FU_DEVICE(module), name); fu_device_set_version_raw(FU_DEVICE(module), fw_ver); name_id6 = g_strdup_printf("%s [Legacy Bluetooth Module (ID6)]", fu_device_get_name(FU_DEVICE(self))); module_id6 = fu_wac_module_bluetooth_id6_new(FU_DEVICE(self)); fu_device_add_child(FU_DEVICE(self), FU_DEVICE(module_id6)); fu_device_set_name(FU_DEVICE(module_id6), name_id6); fu_device_set_version_raw(FU_DEVICE(module_id6), fw_ver); return TRUE; } static gboolean fu_wac_device_add_modules_legacy(FuWacDevice *self, GError **error) { g_autoptr(GError) error_bt = NULL; /* optional bluetooth */ if (!fu_wac_device_add_modules_bluetooth(self, &error_bt)) g_debug("no bluetooth hardware: %s", error_bt->message); return TRUE; } static gboolean fu_wac_device_add_modules_cb(FuDevice *device, gpointer user_data, GError **error) { guint8 buf[] = {[0] = FU_WAC_REPORT_ID_FW_DESCRIPTOR, [1 ... 31] = 0xff}; GByteArray *out = (GByteArray *)user_data; if (!fu_wac_device_get_feature_report(FU_WAC_DEVICE(device), buf, sizeof(buf), FU_HID_DEVICE_FLAG_NONE, error)) { g_prefix_error(error, "failed to get DeviceFirmwareDescriptor: "); return FALSE; } /* verify bootloader is compatible */ if (buf[1] != 0x01) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "bootloader major version not compatible"); return FALSE; } /* verify the number of submodules is possible */ if (buf[3] > (512 - 4) / 4) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "number of submodules is impossible"); return FALSE; } /* copy here, since version 0 is valid for transitional module state */ if (!fu_memcpy_safe(out->data, out->len, 0, buf, sizeof(buf), 0, out->len, error)) return FALSE; /* validate versions of each module */ for (guint8 i = 0; i < buf[3]; i++) { guint8 fw_type = buf[(i * 4) + 4] & ~0x80; guint16 ver; /* check if module is in transitional state or requires re-flashing */ if (!fu_memread_uint16_safe(buf, sizeof(buf), (i * 4) + 5, &ver, G_BIG_ENDIAN, error)) return FALSE; if (ver == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "module %u has error state", fw_type); return FALSE; } } return TRUE; } static gboolean fu_wac_device_add_modules(FuWacDevice *self, GError **error) { g_autofree gchar *version_bootloader = NULL; guint16 boot_ver; guint8 number_modules; gsize offset = 0; g_autoptr(FuStructModuleDesc) st_desc = NULL; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GError) error_local = NULL; g_byte_array_set_size(buf, 32); /* wait for all modules started successfully */ if (!fu_device_retry_full(FU_DEVICE(self), fu_wac_device_add_modules_cb, FU_WAC_DEVICE_MODULE_RETRY_DELAY, FU_WAC_DEVICE_TIMEOUT / FU_WAC_DEVICE_MODULE_RETRY_DELAY, buf, &error_local)) { if (error_local->code != FWUPD_ERROR_INVALID_DATA) { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } g_warning("%s", error_local->message); } fu_dump_raw(G_LOG_DOMAIN, "modules", buf->data, buf->len); /* bootloader version */ st_desc = fu_struct_module_desc_parse(buf->data, buf->len, offset, error); if (st_desc == NULL) return FALSE; boot_ver = fu_struct_module_desc_get_bootloader_version(st_desc); version_bootloader = fu_version_from_uint16(boot_ver, FWUPD_VERSION_FORMAT_BCD); fu_device_set_version_bootloader(FU_DEVICE(self), version_bootloader); fu_device_set_version_bootloader_raw(FU_DEVICE(self), boot_ver); /* no padding */ offset += FU_STRUCT_MODULE_DESC_SIZE; /* get versions of each module */ number_modules = fu_struct_module_desc_get_number_modules(st_desc); for (guint8 i = 0; i < number_modules; i++) { guint32 ver; guint16 ver2; FuWacModuleFwType fw_type; g_autofree gchar *name = NULL; g_autoptr(FuStructModuleItem) st_item = NULL; g_autoptr(FuWacModule) module = NULL; st_item = fu_struct_module_item_parse(buf->data, buf->len, offset, error); if (st_item == NULL) return FALSE; ver = fu_struct_module_item_get_version(st_item); ver2 = fu_struct_module_item_get_version2(st_item); /* * When ver2 is available and not 0, it is appended to ver in order to make it BCD * 32bits, otherwise it stays BCD 16bits. */ if (ver2 != 0xFF && ver2 != 0) { ver = (ver << 16); ver |= (ver2 << 8); } fw_type = fu_struct_module_item_get_kind(st_item) & 0x7F; switch (fw_type) { case FU_WAC_MODULE_FW_TYPE_TOUCH: module = fu_wac_module_touch_new(FU_DEVICE(self)); name = g_strdup_printf("%s [Touch Module]", fu_device_get_name(FU_DEVICE(self))); fu_device_add_child(FU_DEVICE(self), FU_DEVICE(module)); fu_device_set_name(FU_DEVICE(module), name); fu_device_set_version_raw(FU_DEVICE(module), ver); break; case FU_WAC_MODULE_FW_TYPE_TOUCH_ID7: module = fu_wac_module_touch_id7_new(FU_DEVICE(self)); name = g_strdup_printf("%s [Touch Module]", fu_device_get_name(FU_DEVICE(self))); fu_device_add_child(FU_DEVICE(self), FU_DEVICE(module)); fu_device_set_name(FU_DEVICE(module), name); fu_device_set_summary(FU_DEVICE(module), "ID7"); fu_device_set_version_raw(FU_DEVICE(module), ver); break; case FU_WAC_MODULE_FW_TYPE_BLUETOOTH: module = fu_wac_module_bluetooth_new(FU_DEVICE(self)); name = g_strdup_printf("%s [Bluetooth Module]", fu_device_get_name(FU_DEVICE(self))); fu_device_add_child(FU_DEVICE(self), FU_DEVICE(module)); fu_device_set_name(FU_DEVICE(module), name); fu_device_set_version_raw(FU_DEVICE(module), ver); break; case FU_WAC_MODULE_FW_TYPE_BLUETOOTH_ID6: module = fu_wac_module_bluetooth_id6_new(FU_DEVICE(self)); name = g_strdup_printf("%s [Bluetooth Module]", fu_device_get_name(FU_DEVICE(self))); fu_device_add_child(FU_DEVICE(self), FU_DEVICE(module)); fu_device_set_name(FU_DEVICE(module), name); fu_device_set_summary(FU_DEVICE(module), "ID6"); fu_device_set_version_raw(FU_DEVICE(module), ver); break; case FU_WAC_MODULE_FW_TYPE_SCALER: module = fu_wac_module_scaler_new(FU_DEVICE(self)); name = g_strdup_printf("%s [Scaler Module]", fu_device_get_name(FU_DEVICE(self))); fu_device_add_child(FU_DEVICE(self), FU_DEVICE(module)); fu_device_set_name(FU_DEVICE(module), name); fu_device_set_version_raw(FU_DEVICE(module), ver); break; case FU_WAC_MODULE_FW_TYPE_BLUETOOTH_ID9: module = fu_wac_module_bluetooth_id9_new(FU_DEVICE(self)); name = g_strdup_printf("%s [Bluetooth Module]", fu_device_get_name(FU_DEVICE(self))); fu_device_add_child(FU_DEVICE(self), FU_DEVICE(module)); fu_device_set_name(FU_DEVICE(module), name); fu_device_set_summary(FU_DEVICE(module), "ID9"); fu_device_set_version_raw(FU_DEVICE(module), ver); break; case FU_WAC_MODULE_FW_TYPE_SUB_CPU: module = fu_wac_module_sub_cpu_new(FU_DEVICE(self)); name = g_strdup_printf("%s [Sub CPU Module]", fu_device_get_name(FU_DEVICE(self))); fu_device_add_child(FU_DEVICE(self), FU_DEVICE(module)); fu_device_set_name(FU_DEVICE(module), name); fu_device_set_version_raw(FU_DEVICE(module), ver); break; case FU_WAC_MODULE_FW_TYPE_MAIN: fu_device_set_version_raw(FU_DEVICE(self), ver); break; default: g_warning("unknown submodule type 0x%0x", fw_type); break; } offset += FU_STRUCT_MODULE_ITEM_SIZE; } return TRUE; } static gboolean fu_wac_device_setup(FuDevice *device, GError **error) { FuWacDevice *self = FU_WAC_DEVICE(device); /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_wac_device_parent_class)->setup(device, error)) return FALSE; /* get current status */ if (!fu_wac_device_ensure_status(self, error)) return FALSE; /* get version of each sub-module */ if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_USE_RUNTIME_VERSION)) { if (!fu_wac_device_add_modules_legacy(self, error)) return FALSE; } else { if (!fu_wac_device_add_modules(self, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_wac_device_close(FuDevice *device, GError **error) { /* reattach wacom.ko */ if (!fu_usb_device_release_interface(FU_USB_DEVICE(device), 0x00, /* HID */ FU_USB_DEVICE_CLAIM_FLAG_KERNEL_DRIVER, error)) { g_prefix_error(error, "failed to re-attach interface: "); return FALSE; } /* The hidcore subsystem uses a generic power_supply that has a deferred * work item that will lock the device. When removing the power_supply, * we take the lock, then cancel the work item which needs to take the * lock too. This needs to be fixed in the kernel, but for the moment * this should let the kernel unstick itself. */ fu_device_sleep(device, 20); /* ms */ /* FuUsbDevice->close */ return FU_DEVICE_CLASS(fu_wac_device_parent_class)->close(device, error); } static void fu_wac_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 98, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload"); } static gchar * fu_wac_device_convert_version(FuDevice *device, guint64 version_raw) { if (version_raw > G_MAXUINT16) return fu_version_from_uint32(version_raw, fu_device_get_version_format(device)); return fu_version_from_uint16(version_raw, fu_device_get_version_format(device)); } static void fu_wac_device_init(FuWacDevice *self) { self->flash_descriptors = g_ptr_array_new_with_free_func(g_free); self->checksums = g_array_new(FALSE, FALSE, sizeof(guint32)); self->configuration = 0xffff; self->firmware_index = 0xffff; fu_device_add_protocol(FU_DEVICE(self), "com.wacom.usb"); fu_device_add_icon(FU_DEVICE(self), FU_DEVICE_ICON_INPUT_TABLET); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_FLAGS); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_BCD); fu_device_set_install_duration(FU_DEVICE(self), 10); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_WAC_FIRMWARE); fu_device_retry_set_delay(FU_DEVICE(self), 30); /* ms */ } static void fu_wac_device_finalize(GObject *object) { FuWacDevice *self = FU_WAC_DEVICE(object); g_ptr_array_unref(self->flash_descriptors); g_array_unref(self->checksums); G_OBJECT_CLASS(fu_wac_device_parent_class)->finalize(object); } static void fu_wac_device_class_init(FuWacDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); object_class->finalize = fu_wac_device_finalize; device_class->write_firmware = fu_wac_device_write_firmware; device_class->to_string = fu_wac_device_to_string; device_class->setup = fu_wac_device_setup; device_class->close = fu_wac_device_close; device_class->set_progress = fu_wac_device_set_progress; device_class->convert_version = fu_wac_device_convert_version; } fwupd-2.0.10/plugins/wacom-usb/fu-wac-device.h000066400000000000000000000013371501337203100210640ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_WAC_DEVICE (fu_wac_device_get_type()) G_DECLARE_FINAL_TYPE(FuWacDevice, fu_wac_device, FU, WAC_DEVICE, FuHidDevice) gboolean fu_wac_device_get_feature_report(FuWacDevice *self, guint8 *buf, gsize bufsz, FuHidDeviceFlags flags, GError **error); gboolean fu_wac_device_set_feature_report(FuWacDevice *self, guint8 *buf, gsize bufsz, FuHidDeviceFlags flags, GError **error); gboolean fu_wac_device_switch_to_flash_loader(FuWacDevice *self, GError **error); gboolean fu_wac_device_update_reset(FuWacDevice *self, GError **error); fwupd-2.0.10/plugins/wacom-usb/fu-wac-firmware.c000066400000000000000000000250621501337203100214350ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-wac-firmware.h" #include "fu-wac-struct.h" struct _FuWacFirmware { FuFirmware parent_instance; }; G_DEFINE_TYPE(FuWacFirmware, fu_wac_firmware, FU_TYPE_FIRMWARE) #define FU_WAC_FIRMWARE_TOKENS_MAX 100000 /* lines */ #define FU_WAC_FIRMWARE_SECTIONS_MAX 10 typedef struct { guint32 addr; guint32 sz; guint32 prog_start_addr; } FuFirmwareWacHeaderRecord; typedef struct { FuFirmware *firmware; FuFirmwareParseFlags flags; GPtrArray *header_infos; GString *image_buffer; guint8 images_cnt; } FuWacFirmwareTokenHelper; static gboolean fu_wac_firmware_tokenize_cb(GString *token, guint token_idx, gpointer user_data, GError **error) { FuWacFirmwareTokenHelper *helper = (FuWacFirmwareTokenHelper *)user_data; g_autofree gchar *cmd = NULL; /* sanity check */ if (token_idx > FU_WAC_FIRMWARE_TOKENS_MAX) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "file has too many lines"); return FALSE; } /* remove WIN32 line endings */ g_strdelimit(token->str, "\r\x1a", '\0'); token->len = strlen(token->str); /* ignore blank lines */ cmd = g_strndup(token->str, 2); if (g_strcmp0(cmd, "") == 0) return TRUE; /* Wacom-specific metadata */ if (g_strcmp0(cmd, "WA") == 0) { /* header info record */ if (token->len > 3 && memcmp(token->str + 2, "COM", 3) == 0) { guint8 header_image_cnt = 0; if (token->len != 40) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid header, got %" G_GSIZE_FORMAT " bytes", token->len); return FALSE; } /* sanity check */ if (helper->header_infos->len > FU_WAC_FIRMWARE_SECTIONS_MAX) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "too many metadata sections: %u", helper->header_infos->len); return FALSE; } if (!fu_firmware_strparse_uint4_safe(token->str, token->len, 5, &header_image_cnt, error)) return FALSE; for (guint j = 0; j < header_image_cnt; j++) { g_autofree FuFirmwareWacHeaderRecord *hdr = NULL; hdr = g_new0(FuFirmwareWacHeaderRecord, 1); if (!fu_firmware_strparse_uint32_safe(token->str, token->len, (j * 16) + 6, &hdr->addr, error)) return FALSE; if (!fu_firmware_strparse_uint32_safe(token->str, token->len, (j * 16) + 14, &hdr->sz, error)) return FALSE; g_debug("header_fw%u_addr: 0x%x", j, hdr->addr); g_debug("header_fw%u_sz: 0x%x", j, hdr->sz); g_ptr_array_add(helper->header_infos, g_steal_pointer(&hdr)); } return TRUE; } /* firmware headline record */ if (token->len == 13) { FuFirmwareWacHeaderRecord *hdr; guint8 idx = 0; if (!fu_firmware_strparse_uint4_safe(token->str, token->len, 2, &idx, error)) return FALSE; if (idx == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "headline %u invalid", idx); return FALSE; } if (idx > helper->header_infos->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "headline %u exceeds header count %u", idx, helper->header_infos->len); return FALSE; } if (idx - 1 != helper->images_cnt) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "headline %u is not in sorted order", idx); return FALSE; } hdr = g_ptr_array_index(helper->header_infos, idx - 1); if (!fu_firmware_strparse_uint32_safe(token->str, token->len, 3, &hdr->prog_start_addr, error)) return FALSE; if (hdr->prog_start_addr != hdr->addr) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "programming address 0x%x != " "base address 0x%0x for idx %u", hdr->prog_start_addr, hdr->addr, idx); return FALSE; } g_debug("programing-start-address: 0x%x", hdr->prog_start_addr); return TRUE; } g_debug("unknown Wacom-specific metadata"); return TRUE; } /* start */ if (g_strcmp0(cmd, "S0") == 0) { if (helper->image_buffer->len > 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "duplicate S0 without S7"); return FALSE; } g_string_append_printf(helper->image_buffer, "%s\n", token->str); return TRUE; } /* these are things we want to include in the image */ if (g_strcmp0(cmd, "S1") == 0 || g_strcmp0(cmd, "S2") == 0 || g_strcmp0(cmd, "S3") == 0 || g_strcmp0(cmd, "S5") == 0 || g_strcmp0(cmd, "S7") == 0 || g_strcmp0(cmd, "S8") == 0 || g_strcmp0(cmd, "S9") == 0) { if (helper->image_buffer->len == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "%s without S0", cmd); return FALSE; } g_string_append_printf(helper->image_buffer, "%s\n", token->str); } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid SREC command on line %u: %s", token_idx + 1, cmd); return FALSE; } /* end */ if (g_strcmp0(cmd, "S7") == 0) { g_autoptr(GBytes) blob = NULL; g_autoptr(GBytes) fw_srec = NULL; g_autoptr(FuFirmware) firmware_srec = fu_srec_firmware_new(); g_autoptr(FuFirmware) img = fu_firmware_new(); FuFirmwareWacHeaderRecord *hdr; /* get the correct relocated start address */ if (helper->images_cnt >= helper->header_infos->len) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "%s without header", cmd); return FALSE; } hdr = g_ptr_array_index(helper->header_infos, helper->images_cnt); if (helper->image_buffer->len == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "%s with missing image buffer", cmd); return FALSE; } /* parse SREC file and add as image */ blob = g_bytes_new(helper->image_buffer->str, helper->image_buffer->len); fu_srec_firmware_set_addr_min(FU_SREC_FIRMWARE(firmware_srec), hdr->addr); if (!fu_firmware_parse_bytes(firmware_srec, blob, 0x0, helper->flags | FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error)) return FALSE; fw_srec = fu_firmware_get_bytes(firmware_srec, error); if (fw_srec == NULL) return FALSE; fu_firmware_set_bytes(img, fw_srec); fu_firmware_set_addr(img, fu_firmware_get_addr(firmware_srec)); fu_firmware_set_idx(img, helper->images_cnt); if (!fu_firmware_add_image_full(helper->firmware, img, error)) return FALSE; helper->images_cnt++; /* clear the image buffer */ g_string_set_size(helper->image_buffer, 0); } /* success */ return TRUE; } static gboolean fu_wac_firmware_validate(FuFirmware *firmware, GInputStream *stream, gsize offset, GError **error) { return fu_struct_wac_firmware_hdr_validate_stream(stream, offset, error); } static gboolean fu_wac_firmware_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { g_autoptr(GPtrArray) header_infos = g_ptr_array_new_with_free_func(g_free); g_autoptr(GString) image_buffer = g_string_new(NULL); FuWacFirmwareTokenHelper helper = {.firmware = firmware, .flags = flags, .header_infos = header_infos, .image_buffer = image_buffer, .images_cnt = 0}; /* tokenize */ if (!fu_strsplit_stream(stream, 0x0, "\n", fu_wac_firmware_tokenize_cb, &helper, error)) return FALSE; /* verify data is complete */ if (helper.image_buffer->len > 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "truncated data: no S7"); return FALSE; } /* ensure this matched the header */ if (helper.header_infos->len != helper.images_cnt) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "not enough images %u for header count %u", helper.images_cnt, header_infos->len); return FALSE; } /* success */ return TRUE; } static guint8 fu_wac_firmware_calc_checksum(GByteArray *buf) { return fu_sum8(buf->data, buf->len) ^ 0xFF; } static GByteArray * fu_wac_firmware_write(FuFirmware *firmware, GError **error) { g_autoptr(GPtrArray) images = fu_firmware_get_images(firmware); g_autoptr(GString) str = g_string_new(NULL); g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GByteArray) buf_hdr = g_byte_array_new(); /* fw header */ if (images->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no firmware images found"); return NULL; } for (guint i = 0; i < images->len; i++) { FuFirmware *img = g_ptr_array_index(images, i); fu_byte_array_append_uint32(buf_hdr, fu_firmware_get_addr(img), G_BIG_ENDIAN); fu_byte_array_append_uint32(buf_hdr, fu_firmware_get_size(img), G_BIG_ENDIAN); } g_string_append_printf(str, "WACOM%u", images->len); for (guint i = 0; i < buf_hdr->len; i++) g_string_append_printf(str, "%02X", buf_hdr->data[i]); g_string_append_printf(str, "%02X\n", fu_wac_firmware_calc_checksum(buf_hdr)); /* payload */ for (guint i = 0; i < images->len; i++) { FuFirmware *img = g_ptr_array_index(images, i); g_autoptr(GBytes) img_blob = NULL; g_autoptr(GByteArray) buf_img = g_byte_array_new(); /* img header */ g_string_append_printf(str, "WA%u", (guint)fu_firmware_get_idx(img) + 1); fu_byte_array_append_uint32(buf_img, fu_firmware_get_addr(img), G_BIG_ENDIAN); for (guint j = 0; j < buf_img->len; j++) g_string_append_printf(str, "%02X", buf_img->data[j]); g_string_append_printf(str, "%02X\n", fu_wac_firmware_calc_checksum(buf_img)); /* srec */ img_blob = fu_firmware_write(img, error); if (img_blob == NULL) return NULL; g_string_append_len(str, (const gchar *)g_bytes_get_data(img_blob, NULL), g_bytes_get_size(img_blob)); } /* success */ g_byte_array_append(buf, (const guint8 *)str->str, str->len); return g_steal_pointer(&buf); } static void fu_wac_firmware_init(FuWacFirmware *self) { fu_firmware_set_images_max(FU_FIRMWARE(self), 1024); g_type_ensure(FU_TYPE_SREC_FIRMWARE); } static void fu_wac_firmware_class_init(FuWacFirmwareClass *klass) { FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); firmware_class->validate = fu_wac_firmware_validate; firmware_class->parse = fu_wac_firmware_parse; firmware_class->write = fu_wac_firmware_write; } FuFirmware * fu_wac_firmware_new(void) { return FU_FIRMWARE(g_object_new(FU_TYPE_WAC_FIRMWARE, NULL)); } fwupd-2.0.10/plugins/wacom-usb/fu-wac-firmware.h000066400000000000000000000005161501337203100214370ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_WAC_FIRMWARE (fu_wac_firmware_get_type()) G_DECLARE_FINAL_TYPE(FuWacFirmware, fu_wac_firmware, FU, WAC_FIRMWARE, FuFirmware) FuFirmware * fu_wac_firmware_new(void); fwupd-2.0.10/plugins/wacom-usb/fu-wac-module-bluetooth-id6.c000066400000000000000000000140741501337203100235720ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2021 Jason Gerecke * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-wac-common.h" #include "fu-wac-device.h" #include "fu-wac-module-bluetooth-id6.h" #include "fu-wac-struct.h" struct _FuWacModuleBluetoothId6 { FuWacModule parent_instance; }; G_DEFINE_TYPE(FuWacModuleBluetoothId6, fu_wac_module_bluetooth_id6, FU_TYPE_WAC_MODULE) #define FU_WAC_MODULE_BLUETOOTH_ID6_CRC8_POLYNOMIAL 0x31 #define FU_WAC_MODULE_BLUETOOTH_ID6_PAYLOAD_SZ 256 #define FU_WAC_MODULE_BLUETOOTH_ID6_START_NORMAL 0x00 #define FU_WAC_MODULE_BLUETOOTH_ID6_START_FULLERASE 0xFE #define FU_WAC_MODULE_BLUETOOTH_ID6_START_TIMEOUT 60000 /* ms */ #define FU_WAC_MODULE_BLUETOOTH_ID6_END_TIMEOUT 60000 /* ms */ static guint8 fu_wac_module_bluetooth_id6_reverse_bits(guint8 value) { guint8 reverse = 0; for (gint i = 0; i < 8; i++) { reverse <<= 1; reverse |= (value & 0x01); value >>= 1; } return reverse; } /* this doesn't appear to be any kind of standard CRC-8 */ static guint8 fu_wac_module_bluetooth_id6_calculate_crc(const guint8 *buf, gsize bufsz) { const guint8 polynomial = FU_WAC_MODULE_BLUETOOTH_ID6_CRC8_POLYNOMIAL; guint32 crc = 0x00; for (gsize j = bufsz; j > 0; j--) { crc ^= (*(buf++) << 8); for (guint32 i = 8; i; i--) { if (crc & 0x8000) crc ^= ((polynomial | 0x100) << 7); crc <<= 1; } } crc = (guint8)(crc >> 8); return fu_wac_module_bluetooth_id6_reverse_bits(crc); } static gboolean fu_wac_module_bluetooth_id6_write_blob(FuWacModule *self, GInputStream *stream, FuProgress *progress, GError **error) { g_autoptr(FuChunkArray) chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_WAC_MODULE_BLUETOOTH_ID6_PAYLOAD_SZ, error); if (chunks == NULL) return FALSE; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; guint8 buf[FU_WAC_MODULE_BLUETOOTH_ID6_PAYLOAD_SZ + 7] = {0x00, 0x01, 0xFF}; g_autoptr(GBytes) blob_chunk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; /* build data packet */ fu_memwrite_uint32(buf + 0x3, 0x0, G_LITTLE_ENDIAN); /* addr, always zero */ memcpy(buf + 0x7, /* nocheck:blocked */ fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); buf[2] = fu_wac_module_bluetooth_id6_calculate_crc( buf + 0x7, FU_WAC_MODULE_BLUETOOTH_ID6_PAYLOAD_SZ); /* include 0xFF for the possibly incomplete last chunk */ blob_chunk = g_bytes_new(buf, sizeof(buf)); g_debug("writing block %u of %u", i, fu_chunk_array_length(chunks) - 1); if (!fu_wac_module_set_feature(self, FU_WAC_MODULE_COMMAND_DATA, blob_chunk, fu_progress_get_child(progress), FU_WAC_MODULE_POLL_INTERVAL, FU_WAC_MODULE_DATA_TIMEOUT, error)) { g_prefix_error(error, "failed to write block %u of %u: ", i, fu_chunk_array_length(chunks) - 1); return FALSE; } fu_progress_step_done(progress); } /* success */ return TRUE; } static gboolean fu_wac_module_bluetooth_id6_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuWacModule *self = FU_WAC_MODULE(device); const guint8 buf_start[] = {FU_WAC_MODULE_BLUETOOTH_ID6_START_NORMAL}; g_autoptr(GBytes) blob_start = g_bytes_new_static(buf_start, sizeof(buf_start)); g_autoptr(GInputStream) stream = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 8, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 59, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 33, NULL); /* get default image */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) { g_prefix_error(error, "wacom bluetooth-id6 module failed to get stream: "); return FALSE; } /* start, which will erase the module */ if (!fu_wac_module_set_feature(self, FU_WAC_MODULE_COMMAND_START, blob_start, fu_progress_get_child(progress), FU_WAC_MODULE_POLL_INTERVAL, FU_WAC_MODULE_BLUETOOTH_ID6_START_TIMEOUT, error)) { g_prefix_error(error, "wacom bluetooth-id6 module failed to erase: "); return FALSE; } fu_progress_step_done(progress); /* data */ if (!fu_wac_module_bluetooth_id6_write_blob(self, stream, fu_progress_get_child(progress), error)) { g_prefix_error(error, "wacom bluetooth-id6 module failed to write: "); return FALSE; } fu_progress_step_done(progress); /* end */ if (!fu_wac_module_set_feature(self, FU_WAC_MODULE_COMMAND_END, NULL, fu_progress_get_child(progress), FU_WAC_MODULE_POLL_INTERVAL, FU_WAC_MODULE_BLUETOOTH_ID6_END_TIMEOUT, error)) { g_prefix_error(error, "wacom bluetooth-id6 module failed to end: "); return FALSE; } fu_progress_step_done(progress); /* success */ return TRUE; } static void fu_wac_module_bluetooth_id6_init(FuWacModuleBluetoothId6 *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_set_install_duration(FU_DEVICE(self), 120); fu_device_set_remove_delay(FU_DEVICE(self), 300000); } static void fu_wac_module_bluetooth_id6_class_init(FuWacModuleBluetoothId6Class *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_wac_module_bluetooth_id6_write_firmware; } FuWacModule * fu_wac_module_bluetooth_id6_new(FuDevice *proxy) { FuWacModule *module = NULL; module = g_object_new(FU_TYPE_WAC_MODULE_BLUETOOTH_ID6, "proxy", proxy, "fw-type", FU_WAC_MODULE_FW_TYPE_BLUETOOTH_ID6, NULL); return module; } fwupd-2.0.10/plugins/wacom-usb/fu-wac-module-bluetooth-id6.h000066400000000000000000000007661501337203100236020ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2021 Jason Gerecke * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-wac-module.h" #define FU_TYPE_WAC_MODULE_BLUETOOTH_ID6 (fu_wac_module_bluetooth_id6_get_type()) G_DECLARE_FINAL_TYPE(FuWacModuleBluetoothId6, fu_wac_module_bluetooth_id6, FU, WAC_MODULE_BLUETOOTH_ID6, FuWacModule) FuWacModule * fu_wac_module_bluetooth_id6_new(FuDevice *proxy); fwupd-2.0.10/plugins/wacom-usb/fu-wac-module-bluetooth-id9.c000066400000000000000000000240771501337203100236010ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2021-2023 Jason Gerecke * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-wac-common.h" #include "fu-wac-device.h" #include "fu-wac-module-bluetooth-id9.h" #include "fu-wac-struct.h" struct _FuWacModuleBluetoothId9 { FuWacModule parent_instance; }; G_DEFINE_TYPE(FuWacModuleBluetoothId9, fu_wac_module_bluetooth_id9, FU_TYPE_WAC_MODULE) #define FU_WAC_MODULE_BLUETOOTH_ID9_PAYLOAD_SZ 256 #define FU_WAC_MODULE_BLUETOOTH_ID9_START_NORMAL 0x00 #define FU_WAC_MODULE_BLUETOOTH_ID9_CMD_NORMAL 0x00 #define FU_WAC_MODULE_BLUETOOTH_ID9_CMD_FULLERASE 0xFE #define FU_WAC_MODULE_BLUETOOTH_ID9_LOADER_RAM 0x02 #define FU_WAC_MODULE_BLUETOOTH_ID9_LOADER_BEGIN 0x03 #define FU_WAC_MODULE_BLUETOOTH_ID9_LOADER_DATA 0x04 #define FU_WAC_MODULE_BLUETOOTH_ID9_POLL_INTERVAL 5 /* ms */ #define FU_WAC_MODULE_BLUETOOTH_ID9_START_TIMEOUT 75000 /* ms */ #define FU_WAC_MODULE_BLUETOOTH_ID9_DATA_TIMEOUT 10000 /* ms */ #define FU_WAC_MODULE_BLUETOOTH_ID9_END_TIMEOUT 10000 /* ms */ /* CRC(concat(spi_cmd, data)) */ static gboolean fu_wac_module_bluetooth_id9_calculate_crc32(GByteArray *buf, GInputStream *stream, guint32 *crc, GError **error) { g_autoptr(GBytes) blob = g_bytes_new(buf->data, buf->len); g_autoptr(GInputStream) composite_stream = fu_composite_input_stream_new(); fu_composite_input_stream_add_bytes(FU_COMPOSITE_INPUT_STREAM(composite_stream), blob); if (!fu_composite_input_stream_add_stream(FU_COMPOSITE_INPUT_STREAM(composite_stream), stream, error)) return FALSE; return fu_input_stream_compute_crc32(composite_stream, FU_CRC_KIND_B32_STANDARD, crc, error); } static FuChunk * fu_wac_module_bluetooth_id9_get_startcmd(GInputStream *stream, gboolean full_erase, GError **error) { gsize streamsz = 0; guint8 command = full_erase ? FU_WAC_MODULE_BLUETOOTH_ID9_CMD_FULLERASE : FU_WAC_MODULE_BLUETOOTH_ID9_CMD_NORMAL; guint32 crc = ~0; g_autoptr(GByteArray) loader_cmd = fu_struct_id9_loader_cmd_new(); g_autoptr(GByteArray) spi_cmd = fu_struct_id9_spi_cmd_new(); g_autoptr(GByteArray) unknown_cmd = fu_struct_id9_unknown_cmd_new(); if (!fu_input_stream_size(stream, &streamsz, error)) return NULL; fu_struct_id9_unknown_cmd_set_size(unknown_cmd, streamsz); fu_struct_id9_spi_cmd_set_size(spi_cmd, streamsz + FU_STRUCT_ID9_UNKNOWN_CMD_SIZE); if (!fu_struct_id9_spi_cmd_set_data(spi_cmd, unknown_cmd, error)) return NULL; fu_struct_id9_loader_cmd_set_command(loader_cmd, command); fu_struct_id9_loader_cmd_set_size(loader_cmd, streamsz + FU_STRUCT_ID9_SPI_CMD_SIZE); if (!fu_wac_module_bluetooth_id9_calculate_crc32(spi_cmd, stream, &crc, error)) return NULL; fu_struct_id9_loader_cmd_set_crc(loader_cmd, crc); if (!fu_struct_id9_loader_cmd_set_data(loader_cmd, spi_cmd, error)) return NULL; if (!fu_struct_id9_loader_cmd_validate(loader_cmd->data, loader_cmd->len, 0, error)) return NULL; return fu_chunk_bytes_new(g_bytes_new(loader_cmd->data, loader_cmd->len)); } static gboolean fu_wac_module_bluetooth_id9_write_block(FuWacModule *self, guint8 phase, FuChunk *chunk, FuProgress *progress, GError **error) { g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GBytes) blob_chunk = NULL; fu_byte_array_append_uint8(buf, phase); g_byte_array_append(buf, fu_chunk_get_data(chunk), fu_chunk_get_data_sz(chunk)); blob_chunk = g_bytes_new(buf->data, buf->len); return fu_wac_module_set_feature(self, FU_WAC_MODULE_COMMAND_DATA, blob_chunk, fu_progress_get_child(progress), FU_WAC_MODULE_BLUETOOTH_ID9_POLL_INTERVAL, FU_WAC_MODULE_BLUETOOTH_ID9_DATA_TIMEOUT, error); } static gboolean fu_wac_module_bluetooth_id9_write_blocks(FuWacModule *self, guint8 phase, GInputStream *stream, gsize block_len, FuProgress *progress, GError **error) { g_autoptr(FuChunkArray) chunks = NULL; chunks = fu_chunk_array_new_from_stream(stream, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, block_len, error); if (chunks == NULL) return FALSE; fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; if (!fu_wac_module_bluetooth_id9_write_block(self, phase, chk, progress, error)) return FALSE; fu_progress_step_done(progress); } return TRUE; } static FuFirmware * fu_wac_module_bluetooth_id9_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { const guint8 *blob; gsize blob_len = 0; guint16 loader_len = 0; gsize payload_len = 0; g_autoptr(GBytes) fw = NULL; g_autoptr(GBytes) loader_bytes = NULL; g_autoptr(GBytes) payload_bytes = NULL; g_autoptr(FuFirmware) firmware = fu_firmware_new(); g_autoptr(FuFirmware) loader_fw = NULL; g_autoptr(FuFirmware) payload_fw = NULL; /* convert to blob */ fw = fu_input_stream_read_bytes(stream, 0, G_MAXSIZE, NULL, error); if (fw == NULL) return NULL; /* The firmware file is formatted as a 2 byte "length" field * followed by bytes of loader code. The remainder * payload is firmware data. */ blob = g_bytes_get_data(fw, &blob_len); if (!fu_memread_uint16_safe(blob, blob_len, 0, &loader_len, G_BIG_ENDIAN, error)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid firmware size"); return NULL; } if (blob_len < 2 || loader_len > blob_len - 2) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "invalid firmware loader size"); return NULL; } loader_bytes = fu_bytes_new_offset(fw, 2, loader_len, error); if (loader_bytes == NULL) return NULL; loader_fw = fu_firmware_new_from_bytes(loader_bytes); fu_firmware_set_id(loader_fw, FU_FIRMWARE_ID_HEADER); fu_firmware_add_image(firmware, loader_fw); payload_len = blob_len - 2 - loader_len; payload_bytes = fu_bytes_new_offset(fw, 2 + loader_len, payload_len, error); if (payload_bytes == NULL) return NULL; payload_fw = fu_firmware_new_from_bytes(payload_bytes); fu_firmware_set_id(payload_fw, FU_FIRMWARE_ID_PAYLOAD); fu_firmware_add_image(firmware, payload_fw); return g_steal_pointer(&firmware); } static gboolean fu_wac_module_bluetooth_id9_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuWacModule *self = FU_WAC_MODULE(device); const guint8 buf_start[] = {FU_WAC_MODULE_BLUETOOTH_ID9_START_NORMAL}; g_autoptr(GBytes) blob_start = g_bytes_new_static(buf_start, sizeof(buf_start)); g_autoptr(GInputStream) stream_loader = NULL; g_autoptr(GInputStream) stream_payload = NULL; g_autoptr(FuChunk) cmd = NULL; /* get firmware images */ stream_loader = fu_firmware_get_image_by_id_stream(firmware, FU_FIRMWARE_ID_HEADER, error); if (stream_loader == NULL) return FALSE; stream_payload = fu_firmware_get_image_by_id_stream(firmware, FU_FIRMWARE_ID_PAYLOAD, error); if (stream_payload == NULL) return FALSE; cmd = fu_wac_module_bluetooth_id9_get_startcmd(stream_payload, FALSE, error); if (cmd == NULL) return FALSE; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 22, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 67, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 10, NULL); /* start */ if (!fu_wac_module_set_feature(self, FU_WAC_MODULE_COMMAND_START, blob_start, fu_progress_get_child(progress), FU_WAC_MODULE_BLUETOOTH_ID9_POLL_INTERVAL, FU_WAC_MODULE_BLUETOOTH_ID9_START_TIMEOUT, error)) return FALSE; fu_progress_step_done(progress); /* transfer flash programmer to device RAM */ if (!fu_wac_module_bluetooth_id9_write_blocks(self, FU_WAC_MODULE_BLUETOOTH_ID9_LOADER_RAM, stream_loader, FU_WAC_MODULE_BLUETOOTH_ID9_PAYLOAD_SZ, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* send "flash start" command to programer */ if (!fu_wac_module_bluetooth_id9_write_block(self, FU_WAC_MODULE_BLUETOOTH_ID9_LOADER_BEGIN, cmd, progress, error)) return FALSE; /* transfer payload for programming */ if (!fu_wac_module_bluetooth_id9_write_blocks(self, FU_WAC_MODULE_BLUETOOTH_ID9_LOADER_DATA, stream_payload, FU_WAC_MODULE_BLUETOOTH_ID9_PAYLOAD_SZ, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* end */ if (!fu_wac_module_set_feature(self, FU_WAC_MODULE_COMMAND_END, NULL, fu_progress_get_child(progress), FU_WAC_MODULE_BLUETOOTH_ID9_POLL_INTERVAL, FU_WAC_MODULE_BLUETOOTH_ID9_END_TIMEOUT, error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static void fu_wac_module_bluetooth_id9_init(FuWacModuleBluetoothId9 *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_set_install_duration(FU_DEVICE(self), 15); } static void fu_wac_module_bluetooth_id9_class_init(FuWacModuleBluetoothId9Class *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_wac_module_bluetooth_id9_write_firmware; device_class->prepare_firmware = fu_wac_module_bluetooth_id9_prepare_firmware; } FuWacModule * fu_wac_module_bluetooth_id9_new(FuDevice *proxy) { FuWacModule *module = NULL; module = g_object_new(FU_TYPE_WAC_MODULE_BLUETOOTH_ID9, "proxy", proxy, "fw-type", FU_WAC_MODULE_FW_TYPE_BLUETOOTH_ID9, NULL); return module; } fwupd-2.0.10/plugins/wacom-usb/fu-wac-module-bluetooth-id9.h000066400000000000000000000007761501337203100236060ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2021-2023 Jason Gerecke * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-wac-module.h" #define FU_TYPE_WAC_MODULE_BLUETOOTH_ID9 (fu_wac_module_bluetooth_id9_get_type()) G_DECLARE_FINAL_TYPE(FuWacModuleBluetoothId9, fu_wac_module_bluetooth_id9, FU, WAC_MODULE_BLUETOOTH_ID9, FuWacModule) FuWacModule * fu_wac_module_bluetooth_id9_new(FuDevice *proxy); fwupd-2.0.10/plugins/wacom-usb/fu-wac-module-bluetooth.c000066400000000000000000000153201501337203100231050ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-wac-common.h" #include "fu-wac-device.h" #include "fu-wac-module-bluetooth.h" #include "fu-wac-struct.h" struct _FuWacModuleBluetooth { FuWacModule parent_instance; }; G_DEFINE_TYPE(FuWacModuleBluetooth, fu_wac_module_bluetooth, FU_TYPE_WAC_MODULE) #define FU_WAC_MODULE_BLUETOOTH_PAYLOAD_SZ 256 #define FU_WAC_MODULE_BLUETOOTH_ADDR_USERDATA_START 0x3000 #define FU_WAC_MODULE_BLUETOOTH_ADDR_USERDATA_STOP 0x8000 typedef struct { guint8 preamble[7]; guint32 addr; guint8 crc; guint8 cdata[FU_WAC_MODULE_BLUETOOTH_PAYLOAD_SZ]; } FuWacModuleBluetoothBlockData; static void fu_wac_module_bluetooth_calculate_crc_byte(guint8 *crc, guint8 data) { guint8 c[8]; guint8 m[8]; guint8 r[8]; /* find out what bits are set */ for (guint i = 0; i < 8; i++) { c[i] = (*crc & (1 << i)) != 0; m[i] = (data & (1 << i)) != 0; } /* do CRC on byte */ r[7] = (c[7] ^ m[4] ^ c[3] ^ m[3] ^ c[4] ^ m[6] ^ c[1] ^ m[0]); r[6] = (c[6] ^ m[5] ^ c[2] ^ m[4] ^ c[3] ^ m[7] ^ c[0] ^ m[1]); r[5] = (c[5] ^ m[6] ^ c[1] ^ m[5] ^ c[2] ^ m[2]); r[4] = (c[4] ^ m[7] ^ c[0] ^ m[6] ^ c[1] ^ m[3]); r[3] = (m[7] ^ m[0] ^ c[7] ^ c[0] ^ m[3] ^ c[4] ^ m[6] ^ c[1]); r[2] = (m[1] ^ c[6] ^ m[0] ^ c[7] ^ m[3] ^ c[4] ^ m[7] ^ c[0] ^ m[6] ^ c[1]); r[1] = (m[2] ^ c[5] ^ m[1] ^ c[6] ^ m[4] ^ c[3] ^ m[7] ^ c[0]); r[0] = (m[3] ^ c[4] ^ m[2] ^ c[5] ^ m[5] ^ c[2]); /* copy back into CRC */ *crc = 0; for (guint i = 0; i < 8; i++) { if (r[i] == 0) continue; FU_BIT_SET(*crc, i); } } static guint8 fu_wac_module_bluetooth_calculate_crc(const guint8 *data, gsize sz) { guint8 crc = 0; for (gsize i = 0; i < sz; i++) fu_wac_module_bluetooth_calculate_crc_byte(&crc, data[i]); return crc; } static GPtrArray * fu_wac_module_bluetooth_parse_blocks(const guint8 *data, gsize sz, gboolean skip_user_data, GError **error) { const guint8 preamble[] = {0x02, 0x00, 0x0f, 0x06, 0x01, 0x08, 0x01}; GPtrArray *blocks = g_ptr_array_new_with_free_func(g_free); for (guint addr = 0x0; addr < sz; addr += FU_WAC_MODULE_BLUETOOTH_PAYLOAD_SZ) { g_autofree FuWacModuleBluetoothBlockData *bd = NULL; gsize cdata_sz = FU_WAC_MODULE_BLUETOOTH_PAYLOAD_SZ; /* user data area */ if (skip_user_data && addr >= FU_WAC_MODULE_BLUETOOTH_ADDR_USERDATA_START && addr < FU_WAC_MODULE_BLUETOOTH_ADDR_USERDATA_STOP) continue; bd = g_new0(FuWacModuleBluetoothBlockData, 1); bd->addr = addr; memcpy(bd->preamble, preamble, sizeof(preamble)); /* nocheck:blocked */ memset(bd->cdata, 0xff, FU_WAC_MODULE_BLUETOOTH_PAYLOAD_SZ); /* if file is not in multiples of payload size */ if (addr + FU_WAC_MODULE_BLUETOOTH_PAYLOAD_SZ >= sz) cdata_sz = sz - addr; if (!fu_memcpy_safe(bd->cdata, sizeof(bd->cdata), 0x0, /* dst */ data, sz, addr, /* src */ cdata_sz, error)) return NULL; bd->crc = fu_wac_module_bluetooth_calculate_crc(bd->cdata, FU_WAC_MODULE_BLUETOOTH_PAYLOAD_SZ); g_ptr_array_add(blocks, g_steal_pointer(&bd)); } return blocks; } static gboolean fu_wac_module_bluetooth_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuWacModule *self = FU_WAC_MODULE(device); const guint8 *data; gsize len = 0; const guint8 buf_start[] = {0x00}; g_autoptr(GPtrArray) blocks = NULL; g_autoptr(GBytes) blob_start = g_bytes_new_static(buf_start, 1); g_autoptr(GBytes) fw = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 20, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 79, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, NULL); /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) { g_prefix_error(error, "wacom bluetooth module failed to get bytes: "); return FALSE; } /* build each data packet */ data = g_bytes_get_data(fw, &len); blocks = fu_wac_module_bluetooth_parse_blocks(data, len, TRUE, error); if (blocks == NULL) { g_prefix_error(error, "wacom bluetooth module failed to parse: "); return FALSE; } /* start, which will erase the module */ if (!fu_wac_module_set_feature(self, FU_WAC_MODULE_COMMAND_START, blob_start, fu_progress_get_child(progress), FU_WAC_MODULE_POLL_INTERVAL, FU_WAC_MODULE_START_TIMEOUT, error)) { g_prefix_error(error, "wacom bluetooth module failed to erase: "); return FALSE; } fu_progress_step_done(progress); /* data */ for (guint i = 0; i < blocks->len; i++) { FuWacModuleBluetoothBlockData *bd = g_ptr_array_index(blocks, i); guint8 buf[256 + 11]; g_autoptr(GBytes) blob_chunk = NULL; /* build data packet */ memset(buf, 0xff, sizeof(buf)); memcpy(&buf[0], bd->preamble, 7); /* nocheck:blocked */ fu_memwrite_uint24(buf + 0x7, bd->addr, G_LITTLE_ENDIAN); buf[10] = bd->crc; memcpy(&buf[11], bd->cdata, sizeof(bd->cdata)); /* nocheck:blocked */ blob_chunk = g_bytes_new(buf, sizeof(buf)); if (!fu_wac_module_set_feature(self, FU_WAC_MODULE_COMMAND_DATA, blob_chunk, fu_progress_get_child(progress), FU_WAC_MODULE_POLL_INTERVAL, FU_WAC_MODULE_DATA_TIMEOUT, error)) { g_prefix_error(error, "wacom bluetooth module failed to write: "); return FALSE; } /* update progress */ fu_progress_set_percentage_full(fu_progress_get_child(progress), i + 1, blocks->len); } fu_progress_step_done(progress); /* end */ if (!fu_wac_module_set_feature(self, FU_WAC_MODULE_COMMAND_END, NULL, fu_progress_get_child(progress), FU_WAC_MODULE_POLL_INTERVAL, FU_WAC_MODULE_END_TIMEOUT, error)) { g_prefix_error(error, "wacom bluetooth module failed to end: "); return FALSE; } fu_progress_step_done(progress); /* success */ return TRUE; } static void fu_wac_module_bluetooth_init(FuWacModuleBluetooth *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_set_install_duration(FU_DEVICE(self), 30); } static void fu_wac_module_bluetooth_class_init(FuWacModuleBluetoothClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_wac_module_bluetooth_write_firmware; } FuWacModule * fu_wac_module_bluetooth_new(FuDevice *proxy) { FuWacModule *module = NULL; module = g_object_new(FU_TYPE_WAC_MODULE_BLUETOOTH, "proxy", proxy, "fw-type", FU_WAC_MODULE_FW_TYPE_BLUETOOTH, NULL); return module; } fwupd-2.0.10/plugins/wacom-usb/fu-wac-module-bluetooth.h000066400000000000000000000006501501337203100231120ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-wac-module.h" #define FU_TYPE_WAC_MODULE_BLUETOOTH (fu_wac_module_bluetooth_get_type()) G_DECLARE_FINAL_TYPE(FuWacModuleBluetooth, fu_wac_module_bluetooth, FU, WAC_MODULE_BLUETOOTH, FuWacModule) FuWacModule * fu_wac_module_bluetooth_new(FuDevice *proxy); fwupd-2.0.10/plugins/wacom-usb/fu-wac-module-scaler.c000066400000000000000000000121331501337203100223500ustar00rootroot00000000000000/* * Copyright 2022 Aaron Skomra * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-wac-common.h" #include "fu-wac-device.h" #include "fu-wac-module-scaler.h" #include "fu-wac-struct.h" struct _FuWacModuleScaler { FuWacModule parent_instance; }; G_DEFINE_TYPE(FuWacModuleScaler, fu_wac_module_scaler, FU_TYPE_WAC_MODULE) #define FU_WAC_MODULE_SCALER_CRC8_POLYNOMIAL 0x07 #define FU_WAC_MODULE_SCALER_PAYLOAD_SZ 256 typedef struct __attribute__((__packed__)) { /* nocheck:blocked */ guint8 addr[3]; guint8 crc; guint8 cdata[FU_WAC_MODULE_SCALER_PAYLOAD_SZ]; } FuWacModuleScalerBlockData; static GPtrArray * fu_wac_module_scaler_parse_blocks(const guint8 *data, gsize sz, GError **error) { GPtrArray *blocks = g_ptr_array_new_with_free_func(g_free); for (guint addr = 0x0; addr < sz; addr += FU_WAC_MODULE_SCALER_PAYLOAD_SZ) { g_autofree FuWacModuleScalerBlockData *bd = NULL; gsize cdata_sz = FU_WAC_MODULE_SCALER_PAYLOAD_SZ; bd = g_new0(FuWacModuleScalerBlockData, 1); fu_memwrite_uint24(bd->addr, addr, G_BIG_ENDIAN); memset(bd->cdata, 0xff, sizeof(bd->cdata)); if (addr + FU_WAC_MODULE_SCALER_PAYLOAD_SZ >= sz) cdata_sz = sz - addr; if (!fu_memcpy_safe(bd->cdata, sizeof(bd->cdata), 0x0, /* dst */ data, sz, addr, /* src */ cdata_sz, error)) return NULL; bd->crc = fu_crc8(FU_CRC_KIND_B8_STANDARD, bd->cdata, sizeof(bd->cdata)); g_ptr_array_add(blocks, g_steal_pointer(&bd)); } return blocks; } static gboolean fu_wac_module_scaler_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuWacModule *self = FU_WAC_MODULE(device); const guint8 *data; gsize len = 0; guint8 buf_start[4] = {0}; g_autoptr(GPtrArray) blocks = NULL; g_autoptr(GBytes) blob_start = NULL; g_autoptr(GBytes) fw = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 8, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 59, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 33, NULL); /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) { g_prefix_error(error, "wacom scaler module failed to get bytes: "); return FALSE; } /* build each data packet */ data = g_bytes_get_data(fw, &len); fu_memwrite_uint32(buf_start, len, G_LITTLE_ENDIAN); blob_start = g_bytes_new_static(buf_start, 4); blocks = fu_wac_module_scaler_parse_blocks(data, len, error); if (blocks == NULL) { g_prefix_error(error, "wacom scaler module failed to parse blocks: "); return FALSE; } /* start, which will erase the module */ if (!fu_wac_module_set_feature(self, FU_WAC_MODULE_COMMAND_START, blob_start, fu_progress_get_child(progress), FU_WAC_MODULE_POLL_INTERVAL, FU_WAC_MODULE_START_TIMEOUT, error)) { g_prefix_error(error, "wacom scaler module failed to erase: "); return FALSE; } fu_progress_step_done(progress); /* data */ for (guint i = 0; i < blocks->len; i++) { FuWacModuleScalerBlockData *bd = g_ptr_array_index(blocks, i); guint8 buf[FU_WAC_MODULE_SCALER_PAYLOAD_SZ + 4]; g_autoptr(GBytes) blob_chunk = NULL; /* build data packet */ memset(buf, 0xff, sizeof(buf)); memcpy(&buf[0], bd->addr, 3); /* nocheck:blocked */ buf[3] = bd->crc; memcpy(&buf[4], bd->cdata, sizeof(bd->cdata)); /* nocheck:blocked */ blob_chunk = g_bytes_new(buf, sizeof(*bd)); if (!fu_wac_module_set_feature(self, FU_WAC_MODULE_COMMAND_DATA, blob_chunk, fu_progress_get_child(progress), FU_WAC_MODULE_POLL_INTERVAL, FU_WAC_MODULE_DATA_TIMEOUT, error)) { g_prefix_error(error, "wacom scaler module failed to write: "); return FALSE; } /* update progress */ fu_progress_set_percentage_full(fu_progress_get_child(progress), i + 1, blocks->len); } fu_progress_step_done(progress); /* end */ if (!fu_wac_module_set_feature(self, FU_WAC_MODULE_COMMAND_END, NULL, fu_progress_get_child(progress), FU_WAC_MODULE_POLL_INTERVAL, FU_WAC_MODULE_END_TIMEOUT, error)) { g_prefix_error(error, "wacom scaler module failed to end: "); return FALSE; } fu_progress_step_done(progress); /* success */ return TRUE; } static void fu_wac_module_scaler_init(FuWacModuleScaler *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_set_install_duration(FU_DEVICE(self), 120); } static void fu_wac_module_scaler_class_init(FuWacModuleScalerClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_wac_module_scaler_write_firmware; } FuWacModule * fu_wac_module_scaler_new(FuDevice *proxy) { FuWacModule *module = NULL; module = g_object_new(FU_TYPE_WAC_MODULE_SCALER, "proxy", proxy, "fw-type", FU_WAC_MODULE_FW_TYPE_SCALER, NULL); return module; } fwupd-2.0.10/plugins/wacom-usb/fu-wac-module-scaler.h000066400000000000000000000006611501337203100223600ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2021 Jason Gerecke * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-wac-module.h" #define FU_TYPE_WAC_MODULE_SCALER (fu_wac_module_scaler_get_type()) G_DECLARE_FINAL_TYPE(FuWacModuleScaler, fu_wac_module_scaler, FU, WAC_MODULE_SCALER, FuWacModule) FuWacModule * fu_wac_module_scaler_new(FuDevice *proxy); fwupd-2.0.10/plugins/wacom-usb/fu-wac-module-sub-cpu.c000066400000000000000000000156061501337203100224650ustar00rootroot00000000000000/* * Copyright 2024 Jason Gerecke * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-wac-common.h" #include "fu-wac-device.h" #include "fu-wac-module-sub-cpu.h" #include "fu-wac-struct.h" struct _FuWacModuleSubCpu { FuWacModule parent_instance; }; G_DEFINE_TYPE(FuWacModuleSubCpu, fu_wac_module_sub_cpu, FU_TYPE_WAC_MODULE) #define FU_WAC_MODULE_SUB_CPU_PAYLOAD_SZ 256 #define FU_WAC_MODULE_SUB_CPU_START_NORMAL 0x00 static FuChunk * fu_wac_module_sub_cpu_create_chunk(GPtrArray *srec_records, guint32 *record_num, GError **error) { FuChunk *chunk; guint32 base_addr = 0; guint32 expect_addr = 0; g_autoptr(GByteArray) data = g_byte_array_new(); g_autoptr(GBytes) blob = NULL; for (; *record_num < srec_records->len; *record_num += 1) { FuSrecFirmwareRecord *rcd = g_ptr_array_index(srec_records, *record_num); GByteArray *src = rcd->buf; /* skip non-data records */ if (!(rcd->kind == FU_FIRMWARE_SREC_RECORD_KIND_S1_DATA_16 || rcd->kind == FU_FIRMWARE_SREC_RECORD_KIND_S2_DATA_24 || rcd->kind == FU_FIRMWARE_SREC_RECORD_KIND_S3_DATA_32)) continue; /* initialize, if necessary */ if (!base_addr) { base_addr = rcd->addr; expect_addr = rcd->addr; } /* Stop appending data to this block if we've reached an * address discontinuity */ if (rcd->addr != expect_addr) break; /* Stop appending data to this block if we've run out of * available space */ if (data->len + src->len > FU_WAC_MODULE_SUB_CPU_PAYLOAD_SZ) { if (data->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "record too big for a single block"); return NULL; } break; } /* copy data into this block and prepare for next iteration */ g_byte_array_append(data, src->data, src->len); expect_addr += src->len; } blob = g_bytes_new(data->data, data->len); chunk = fu_chunk_bytes_new(g_steal_pointer(&blob)); fu_chunk_set_address(chunk, base_addr); return chunk; } static GPtrArray * fu_wac_module_sub_cpu_parse_chunks(FuSrecFirmware *srec_firmware, guint32 *data_len, GError **error) { GPtrArray *chunks = g_ptr_array_new_with_free_func(g_free); guint record_num = 0; GPtrArray *records = fu_srec_firmware_get_records(srec_firmware); *data_len = 0; while (record_num < records->len) { g_autofree FuChunk *chunk = fu_wac_module_sub_cpu_create_chunk(records, &record_num, error); if (chunk == NULL) return NULL; *data_len += fu_chunk_get_data_sz(chunk); g_ptr_array_add(chunks, g_steal_pointer(&chunk)); } return chunks; } static GBytes * fu_wac_module_sub_cpu_build_packet(FuChunk *chunk, GError **error) { guint8 buf[FU_WAC_MODULE_SUB_CPU_PAYLOAD_SZ + 5]; memset(buf, 0xff, sizeof(buf)); fu_memwrite_uint32(&buf[0], fu_chunk_get_address(chunk), G_BIG_ENDIAN); buf[4] = fu_chunk_get_data_sz(chunk) / 2; if (!fu_memcpy_safe(buf, sizeof(buf), 5, /* dst */ fu_chunk_get_data(chunk), fu_chunk_get_data_sz(chunk), 0, /* src */ fu_chunk_get_data_sz(chunk), error)) { g_prefix_error(error, "wacom sub_cpu module failed to build packet: "); return NULL; } return g_bytes_new(buf, sizeof(buf)); } static gboolean fu_wac_module_sub_cpu_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuWacModule *self = FU_WAC_MODULE(device); guint8 buf_start[4] = {}; guint32 firmware_len = 0; g_autoptr(GPtrArray) chunks = NULL; g_autoptr(GBytes) blob_start = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 2, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 97, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, NULL); /* build each data packet */ chunks = fu_wac_module_sub_cpu_parse_chunks(FU_SREC_FIRMWARE(firmware), &firmware_len, error); if (chunks == NULL) return FALSE; /* build start command */ fu_memwrite_uint32(buf_start, firmware_len, G_LITTLE_ENDIAN); blob_start = g_bytes_new_static(buf_start, sizeof(buf_start)); /* start, which will erase the module */ if (!fu_wac_module_set_feature(self, FU_WAC_MODULE_COMMAND_START, blob_start, fu_progress_get_child(progress), FU_WAC_MODULE_POLL_INTERVAL, FU_WAC_MODULE_START_TIMEOUT, error)) { g_prefix_error(error, "wacom sub_cpu module failed to erase: "); return FALSE; } fu_progress_step_done(progress); /* data */ for (guint i = 0; i < chunks->len; i++) { FuChunk *chunk = g_ptr_array_index(chunks, i); g_autoptr(GBytes) blob_chunk = fu_wac_module_sub_cpu_build_packet(chunk, error); if (blob_chunk == NULL) return FALSE; if (!fu_wac_module_set_feature(self, FU_WAC_MODULE_COMMAND_DATA, blob_chunk, fu_progress_get_child(progress), FU_WAC_MODULE_POLL_INTERVAL, FU_WAC_MODULE_DATA_TIMEOUT, error)) { g_prefix_error(error, "wacom sub_cpu module failed to write: "); return FALSE; } /* update progress */ fu_progress_set_percentage_full(fu_progress_get_child(progress), i + 1, chunks->len); } fu_progress_step_done(progress); /* end */ if (!fu_wac_module_set_feature(self, FU_WAC_MODULE_COMMAND_END, NULL, fu_progress_get_child(progress), FU_WAC_MODULE_POLL_INTERVAL, FU_WAC_MODULE_END_TIMEOUT, error)) { g_prefix_error(error, "wacom sub_cpu module failed to end: "); return FALSE; } fu_progress_step_done(progress); /* success */ return TRUE; } static FuFirmware * fu_wac_module_sub_cpu_prepare_firmware(FuDevice *self, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { FuFirmware *firmware_srec = fu_srec_firmware_new(); if (!fu_firmware_parse_stream(firmware_srec, stream, 0, flags | FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, error)) { g_prefix_error(error, "wacom sub_cpu failed to parse firmware: "); return NULL; } return firmware_srec; } static void fu_wac_module_sub_cpu_init(FuWacModuleSubCpu *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_set_install_duration(FU_DEVICE(self), 15); } static void fu_wac_module_sub_cpu_class_init(FuWacModuleSubCpuClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_wac_module_sub_cpu_write_firmware; device_class->prepare_firmware = fu_wac_module_sub_cpu_prepare_firmware; } FuWacModule * fu_wac_module_sub_cpu_new(FuDevice *proxy) { FuWacModule *module = NULL; module = g_object_new(FU_TYPE_WAC_MODULE_SUB_CPU, "proxy", proxy, "fw-type", FU_WAC_MODULE_FW_TYPE_SUB_CPU, NULL); return module; } fwupd-2.0.10/plugins/wacom-usb/fu-wac-module-sub-cpu.h000066400000000000000000000006021501337203100224600ustar00rootroot00000000000000/* * Copyright 2024 Jason Gerecke * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-wac-module.h" #define FU_TYPE_WAC_MODULE_SUB_CPU (fu_wac_module_sub_cpu_get_type()) G_DECLARE_FINAL_TYPE(FuWacModuleSubCpu, fu_wac_module_sub_cpu, FU, WAC_MODULE_SUB_CPU, FuWacModule) FuWacModule * fu_wac_module_sub_cpu_new(FuDevice *proxy); fwupd-2.0.10/plugins/wacom-usb/fu-wac-module-touch-id7.c000066400000000000000000000252561501337203100227140ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2023 Joshua Dickens * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-wac-device.h" #include "fu-wac-module-touch-id7.h" #include "fu-wac-struct.h" struct _FuWacModuleTouchId7 { FuWacModule parent_instance; }; typedef struct { guint32 op_id; const guint8 *buf; gsize bufsz; gsize offset; } WtaInfo; typedef struct { guint32 header_size; guint16 firmware_number; } WtaFileHeader; typedef struct { guint32 file_name_length; guint32 start_address; guint8 ic_id; guint8 ma_id; guint32 block_count; } WtaRecordHeader; G_DEFINE_TYPE(FuWacModuleTouchId7, fu_wac_module_touch_id7, FU_TYPE_WAC_MODULE) /** * Read and advance past a WTA file header. * * File Header format: * { * u32: Starting symbol for the file (WTA) * u32: Header Size * u8[]: Variable-length padding to bring the header to match Header Size * u16: Number of Firmware * u8[]: Padding/Unnecessary Data * } * */ static gboolean fu_wac_module_touch_id7_read_file_header(WtaFileHeader *header, WtaInfo *info, GError **error) { info->offset += 4; if (!fu_memread_uint32_safe(info->buf, info->bufsz, info->offset, &header->header_size, G_LITTLE_ENDIAN, error)) return FALSE; info->offset += header->header_size - 8; if (!fu_memread_uint16_safe(info->buf, info->bufsz, info->offset, &header->firmware_number, G_LITTLE_ENDIAN, error)) return FALSE; info->offset += 16; return TRUE; } /** * fu_wac_module_touch_id7_read_record_header: * * Read and advance past a WTA record header. * * Header format: * { * u32: Length of filename * char[]: Variable-length null-terminated filename string * u8[]: Variable-length padding to bring filename to a multiple of 4 bytes * u8: Firmware Type * u8[]: 3 Bytes padding to bring Firmware Type to a multiple of 4 bytes * u32: Start address * u32: Segment Size * u8: IC_ID * u8: MA_ID * u8[]: 2 Bytes padding to bring IC_ID/MA_ID to a multiple of 4 bytes * u32: Block Count * } * */ static gboolean fu_wac_module_touch_id7_read_record_header(WtaRecordHeader *header, WtaInfo *info, GError **error) { if (!fu_memread_uint32_safe(info->buf, info->bufsz, info->offset, &header->file_name_length, G_LITTLE_ENDIAN, error)) return FALSE; info->offset += header->file_name_length + 8; if (!fu_memread_uint32_safe(info->buf, info->bufsz, info->offset, &header->start_address, G_LITTLE_ENDIAN, error)) return FALSE; info->offset += 8; if (!fu_memread_uint8_safe(info->buf, info->bufsz, info->offset, &header->ic_id, error)) return FALSE; info->offset += 1; if (!fu_memread_uint8_safe(info->buf, info->bufsz, info->offset, &header->ma_id, error)) return FALSE; info->offset += 3; if (!fu_memread_uint32_safe(info->buf, info->bufsz, info->offset, &header->block_count, G_LITTLE_ENDIAN, error)) return FALSE; info->offset += 4; /* success */ return TRUE; } /** * fu_wac_module_touch_id7_generate_command: * @header: A #WtaRecordHeader * @cmd: The type of command to be sent * @op_id: The operation serial number * @buf: The buffer to write the command into * * Generate a standard touch id7 command preamble. * */ static void fu_wac_module_touch_id7_generate_command(const WtaRecordHeader *header, guint8 cmd, guint16 op_id, guint8 *buf) { buf[0] = cmd; buf[1] = header->ic_id; buf[2] = header->ma_id; fu_memwrite_uint32(&buf[3], op_id, G_LITTLE_ENDIAN); fu_memwrite_uint32(&buf[7], header->start_address, G_LITTLE_ENDIAN); } /** * fu_wac_module_touch_id7_write_block: * * Write the data of a single firmware block to the device. * */ static gboolean fu_wac_module_touch_id7_write_block(FuWacModule *self, WtaInfo *info, FuProgress *progress, WtaRecordHeader *record_hdr, GError **error) { g_autoptr(GPtrArray) chunks = NULL; g_autoptr(GByteArray) st_blk = NULL; /* generate chunks off of the raw block data */ st_blk = fu_struct_wta_block_header_parse(info->buf, info->bufsz, info->offset, error); if (st_blk == NULL) return FALSE; info->offset += FU_STRUCT_WTA_BLOCK_HEADER_SIZE; chunks = fu_chunk_array_new(info->buf + info->offset, fu_struct_wta_block_header_get_block_size(st_blk), fu_struct_wta_block_header_get_block_start(st_blk), 0x0, /* page_sz */ FU_WAC_MODULE_CHUNK_SIZE); /* packet_sz */ /* write data */ for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); guint8 buf[11 + FU_WAC_MODULE_CHUNK_SIZE]; g_autoptr(GBytes) blob_chunk = NULL; buf[0] = FU_WAC_MODULE_COMMAND_DATA; buf[1] = record_hdr->ic_id; buf[2] = record_hdr->ma_id; fu_memwrite_uint32(&buf[3], info->op_id, G_LITTLE_ENDIAN); fu_memwrite_uint32(&buf[7], fu_chunk_get_address(chk), G_LITTLE_ENDIAN); memcpy(&buf[11], /* nocheck:blocked */ fu_chunk_get_data(chk), FU_WAC_MODULE_CHUNK_SIZE); blob_chunk = g_bytes_new(buf, sizeof(buf)); if (!fu_wac_module_set_feature(self, FU_WAC_MODULE_COMMAND_DATA, blob_chunk, fu_progress_get_child(progress), FU_WAC_MODULE_POLL_INTERVAL, FU_WAC_MODULE_DATA_TIMEOUT, error)) { g_prefix_error(error, "failed to write block %u: ", info->op_id); return FALSE; } info->op_id++; /* rough estimate based on file size with some added to handle the extra firmware * record start and end commands */ fu_progress_set_percentage_full(fu_progress_get_child(progress), info->op_id, info->bufsz / FU_WAC_MODULE_CHUNK_SIZE + 10); } /* incrementing data to the next block */ info->offset += fu_struct_wta_block_header_get_block_size(st_blk); return TRUE; } /** * fu_wac_module_touch_id7_write_record: * * Start and end the write process for a single touch id7 firmware record and * the block(s) it contains. * A touch id7 firmware record acts as it's own mini update with the device, * with a start and end command each individual record. * A single touch id7 firmware record can contain one or more blocks that have * the raw data for writing. */ static gboolean fu_wac_module_touch_id7_write_record(FuWacModule *self, WtaInfo *info, FuProgress *progress, GError **error) { WtaRecordHeader record_hdr = {0x0}; g_autoptr(GBytes) blob_start = NULL; g_autoptr(GBytes) blob_end = NULL; guint8 command[11]; if (!fu_wac_module_touch_id7_read_record_header(&record_hdr, info, error)) return FALSE; /* start firmware record command */ fu_wac_module_touch_id7_generate_command(&record_hdr, FU_WAC_MODULE_COMMAND_START, info->op_id, command); blob_start = g_bytes_new(command, sizeof(command)); if (!fu_wac_module_set_feature(self, FU_WAC_MODULE_COMMAND_DATA, blob_start, fu_progress_get_child(progress), FU_WAC_MODULE_POLL_INTERVAL, FU_WAC_MODULE_DATA_TIMEOUT, error)) return FALSE; info->op_id++; /* write each block */ for (guint32 i = 0; i < record_hdr.block_count; i++) { if (!fu_wac_module_touch_id7_write_block(self, info, progress, &record_hdr, error)) return FALSE; } /* end firmware record command */ fu_wac_module_touch_id7_generate_command(&record_hdr, FU_WAC_MODULE_COMMAND_END, info->op_id, command); blob_end = g_bytes_new(command, sizeof(command)); if (!fu_wac_module_set_feature(self, FU_WAC_MODULE_COMMAND_DATA, blob_end, fu_progress_get_child(progress), FU_WAC_MODULE_POLL_INTERVAL, FU_WAC_MODULE_DATA_TIMEOUT, error)) return FALSE; info->op_id++; return TRUE; } /** * fu_wac_module_touch_id7_write_firmware: * * Start and End the overall update process for touch id7 firmware and the * record(s) it contains. * A touch id7 firmware will usually contain 3 firmware record(s) but could * potentially have less or more. */ static gboolean fu_wac_module_touch_id7_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuWacModule *self = FU_WAC_MODULE(device); g_autoptr(GBytes) blob = NULL; WtaInfo info; WtaFileHeader file_hdr; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 2, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 97, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, NULL); g_debug("using element at addr 0x%0x", (guint)fu_firmware_get_addr(firmware)); blob = fu_firmware_get_bytes(firmware, error); if (blob == NULL) return FALSE; /* start, which will erase the module */ if (!fu_wac_module_set_feature(self, FU_WAC_MODULE_COMMAND_START, NULL, fu_progress_get_child(progress), FU_WAC_MODULE_POLL_INTERVAL, FU_WAC_MODULE_START_TIMEOUT, error)) return FALSE; fu_progress_step_done(progress); /* set basic info */ info.offset = 0x0; info.buf = g_bytes_get_data(blob, &info.bufsz); info.op_id = 1; if (!fu_wac_module_touch_id7_read_file_header(&file_hdr, &info, error)) return FALSE; /* write each firmware record */ for (guint i = 0; i < file_hdr.firmware_number; i++) { if (!fu_wac_module_touch_id7_write_record(self, &info, progress, error)) return FALSE; /* increment data to the next firmware record */ info.offset += 14; } fu_progress_step_done(progress); /* end */ if (!fu_wac_module_set_feature(self, FU_WAC_MODULE_COMMAND_END, NULL, fu_progress_get_child(progress), FU_WAC_MODULE_POLL_INTERVAL, FU_WAC_MODULE_END_TIMEOUT, error)) return FALSE; fu_progress_step_done(progress); /* success */ return TRUE; } static void fu_wac_module_touch_id7_init(FuWacModuleTouchId7 *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_set_install_duration(FU_DEVICE(self), 90); } static void fu_wac_module_touch_id7_class_init(FuWacModuleTouchId7Class *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_wac_module_touch_id7_write_firmware; } FuWacModule * fu_wac_module_touch_id7_new(FuDevice *proxy) { FuWacModule *module = NULL; module = g_object_new(FU_TYPE_WAC_MODULE_TOUCH_ID7, "proxy", proxy, "fw-type", FU_WAC_MODULE_FW_TYPE_TOUCH_ID7, NULL); return module; } fwupd-2.0.10/plugins/wacom-usb/fu-wac-module-touch-id7.h000066400000000000000000000010101501337203100226770ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * Copyright 2023 Joshua Dickens * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-wac-module.h" #define FU_TYPE_WAC_MODULE_TOUCH_ID7 (fu_wac_module_touch_id7_get_type()) G_DECLARE_FINAL_TYPE(FuWacModuleTouchId7, fu_wac_module_touch_id7, FU, WAC_MODULE_TOUCH_ID7, FuWacModule) #define FU_WAC_MODULE_CHUNK_SIZE 128 FuWacModule * fu_wac_module_touch_id7_new(FuDevice *proxy); fwupd-2.0.10/plugins/wacom-usb/fu-wac-module-touch.c000066400000000000000000000104441501337203100222240ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-wac-device.h" #include "fu-wac-module-touch.h" #include "fu-wac-struct.h" struct _FuWacModuleTouch { FuWacModule parent_instance; }; G_DEFINE_TYPE(FuWacModuleTouch, fu_wac_module_touch, FU_TYPE_WAC_MODULE) static gboolean fu_wac_module_touch_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuWacModule *self = FU_WAC_MODULE(device); g_autoptr(GInputStream) stream = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_ERASE, 10, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 90, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 10, NULL); g_debug("using element at addr 0x%0x", (guint)fu_firmware_get_addr(firmware)); /* build each data packet */ stream = fu_firmware_get_stream(firmware, error); if (stream == NULL) { g_prefix_error(error, "wacom touch module failed to get stream: "); return FALSE; } chunks = fu_chunk_array_new_from_stream(stream, fu_firmware_get_addr(firmware), FU_CHUNK_PAGESZ_NONE, 128, error); if (chunks == NULL) return FALSE; /* start, which will erase the module */ if (!fu_wac_module_set_feature(self, FU_WAC_MODULE_COMMAND_START, NULL, fu_progress_get_child(progress), FU_WAC_MODULE_POLL_INTERVAL, FU_WAC_MODULE_START_TIMEOUT, error)) { g_prefix_error(error, "wacom touch module failed to erase: "); return FALSE; } fu_progress_step_done(progress); /* data */ for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; guint8 buf[128 + 7] = {0xff}; g_autoptr(GBytes) blob_chunk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; /* build G11T data packet */ memset(buf, 0xff, sizeof(buf)); buf[0] = 0x01; /* writing */ buf[1] = fu_chunk_get_idx(chk) + 1; fu_memwrite_uint32(&buf[2], fu_chunk_get_address(chk), G_LITTLE_ENDIAN); buf[6] = 0x10; /* no idea! */ if (!fu_memcpy_safe(buf, sizeof(buf), 0x07, /* dst */ fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), 0x0, /* src */ fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "wacom touch module failed to memcpy: "); return FALSE; } blob_chunk = g_bytes_new(buf, sizeof(buf)); if (!fu_wac_module_set_feature(self, FU_WAC_MODULE_COMMAND_DATA, blob_chunk, fu_progress_get_child(progress), FU_WAC_MODULE_POLL_INTERVAL, FU_WAC_MODULE_DATA_TIMEOUT, error)) { g_prefix_error(error, "failed to write block %u: ", fu_chunk_get_idx(chk)); return FALSE; } /* update progress */ fu_progress_set_percentage_full(fu_progress_get_child(progress), i + 1, fu_chunk_array_length(chunks)); } fu_progress_step_done(progress); /* end */ if (!fu_wac_module_set_feature(self, FU_WAC_MODULE_COMMAND_END, NULL, fu_progress_get_child(progress), FU_WAC_MODULE_POLL_INTERVAL, FU_WAC_MODULE_END_TIMEOUT, error)) { g_prefix_error(error, "wacom touch module failed to end: "); return FALSE; } fu_progress_step_done(progress); /* success */ return TRUE; } static void fu_wac_module_touch_init(FuWacModuleTouch *self) { fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_set_install_duration(FU_DEVICE(self), 30); fu_device_set_firmware_gtype(FU_DEVICE(self), FU_TYPE_IHEX_FIRMWARE); } static void fu_wac_module_touch_class_init(FuWacModuleTouchClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); device_class->write_firmware = fu_wac_module_touch_write_firmware; } FuWacModule * fu_wac_module_touch_new(FuDevice *proxy) { FuWacModule *module = NULL; module = g_object_new(FU_TYPE_WAC_MODULE_TOUCH, "proxy", proxy, "fw-type", FU_WAC_MODULE_FW_TYPE_TOUCH, NULL); return module; } fwupd-2.0.10/plugins/wacom-usb/fu-wac-module-touch.h000066400000000000000000000005641501337203100222330ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-wac-module.h" #define FU_TYPE_WAC_MODULE_TOUCH (fu_wac_module_touch_get_type()) G_DECLARE_FINAL_TYPE(FuWacModuleTouch, fu_wac_module_touch, FU, WAC_MODULE_TOUCH, FuWacModule) FuWacModule * fu_wac_module_touch_new(FuDevice *proxy); fwupd-2.0.10/plugins/wacom-usb/fu-wac-module.c000066400000000000000000000232601501337203100211040ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "fu-wac-common.h" #include "fu-wac-device.h" #include "fu-wac-module.h" #include "fu-wac-struct.h" typedef struct { guint8 fw_type; guint8 command; guint8 status; } FuWacModulePrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuWacModule, fu_wac_module, FU_TYPE_DEVICE) #define GET_PRIVATE(o) (fu_wac_module_get_instance_private(o)) enum { PROP_0, PROP_FW_TYPE, PROP_LAST }; static void fu_wac_module_to_string(FuDevice *device, guint idt, GString *str) { FuWacModule *self = FU_WAC_MODULE(device); FuWacModulePrivate *priv = GET_PRIVATE(self); fwupd_codec_string_append(str, idt, "FwType", fu_wac_module_fw_type_to_string(priv->fw_type)); fwupd_codec_string_append(str, idt, "Status", fu_wac_module_status_to_string(priv->status)); fwupd_codec_string_append(str, idt, "Command", fu_wac_module_command_to_string(priv->command)); } static gboolean fu_wac_module_refresh(FuWacModule *self, GError **error) { FuWacDevice *parent_device = FU_WAC_DEVICE(fu_device_get_parent(FU_DEVICE(self))); FuWacModulePrivate *priv = GET_PRIVATE(self); guint8 buf[] = {[0] = FU_WAC_REPORT_ID_MODULE, [1 ... FU_WAC_PACKET_LEN - 1] = 0xff}; /* get from hardware */ if (!fu_wac_device_get_feature_report(parent_device, buf, sizeof(buf), FU_HID_DEVICE_FLAG_ALLOW_TRUNC, error)) { g_prefix_error(error, "failed to refresh status: "); fu_error_convert(error); return FALSE; } /* check fw type */ if (priv->fw_type != buf[1]) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Submodule GetFeature fw_Type invalid " "got 0x%02x expected 0x%02x", (guint)buf[1], (guint)priv->fw_type); return FALSE; } /* current phase and status */ if (priv->command != buf[2] || priv->status != buf[3]) { priv->command = buf[2]; priv->status = buf[3]; g_debug("command: %s, status: %s", fu_wac_module_command_to_string(priv->command), fu_wac_module_status_to_string(priv->status)); } /* success */ return TRUE; } static gboolean fu_wac_module_refresh_cb(FuDevice *device, gpointer user_data, GError **error) { FuWacModule *self = FU_WAC_MODULE(device); FuWacModulePrivate *priv = GET_PRIVATE(self); g_autoptr(GError) error_local = NULL; if (!fu_wac_module_refresh(self, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) return TRUE; g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } /* retry not necessary for unrecoverable errors */ if (priv->status != FU_WAC_MODULE_STATUS_BUSY) return TRUE; if (priv->status != FU_WAC_MODULE_STATUS_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "refresh returned status 0x%x [%s]", priv->status, fu_wac_module_status_to_string(priv->status)); return FALSE; } /* success */ return TRUE; } gboolean fu_wac_module_set_feature(FuWacModule *self, guint8 command, GBytes *blob, /* optional */ FuProgress *progress, guint poll_interval, /* ms */ guint busy_timeout, /* ms */ GError **error) { FuWacDevice *parent_device = FU_WAC_DEVICE(fu_device_get_parent(FU_DEVICE(self))); FuWacModulePrivate *priv = GET_PRIVATE(self); const guint8 *data; gsize len = 0; guint delay_ms = fu_device_has_flag(FU_DEVICE(parent_device), FWUPD_DEVICE_FLAG_EMULATED) ? 10 : poll_interval; guint busy_poll_loops = busy_timeout / delay_ms; guint8 buf[] = {[0] = FU_WAC_REPORT_ID_MODULE, [1] = priv->fw_type, [2] = command, [3 ... FU_WAC_PACKET_LEN - 1] = 0xff}; /* sanity check */ g_return_val_if_fail(FU_IS_WAC_MODULE(self), FALSE); g_return_val_if_fail(FU_IS_WAC_DEVICE(parent_device), FALSE); /* verify the size of the blob */ if (blob != NULL) { data = g_bytes_get_data(blob, &len); if (!fu_memcpy_safe(buf, sizeof(buf), 0x03, /* dst */ data, len, 0x0, /* src */ len, error)) { g_prefix_error(error, "Submodule blob larger than buffer: "); return FALSE; } } /* tell the daemon the current status */ switch (command) { case FU_WAC_MODULE_COMMAND_START: fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_ERASE); break; case FU_WAC_MODULE_COMMAND_DATA: fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE); break; case FU_WAC_MODULE_COMMAND_END: fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_VERIFY); break; default: break; } /* send to hardware */ if (!fu_wac_device_set_feature_report(parent_device, buf, sizeof(buf), FU_HID_DEVICE_FLAG_ALLOW_TRUNC, error)) { g_prefix_error(error, "failed to set module feature: "); return FALSE; } /* wait for hardware */ if (busy_poll_loops > 0) { fu_device_sleep(FU_DEVICE(self), delay_ms); /* settle before polling status */ if (!fu_device_retry_full(FU_DEVICE(self), fu_wac_module_refresh_cb, busy_poll_loops, delay_ms, NULL, error)) { g_prefix_error(error, "failed to set feature %s: ", fu_wac_module_command_to_string(command)); return FALSE; } if (priv->status != FU_WAC_MODULE_STATUS_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "refresh returned status 0x%x [%s]", priv->status, fu_wac_module_status_to_string(priv->status)); return FALSE; } } /* success */ return TRUE; } static gboolean fu_wac_module_cleanup(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDevice *parent = fu_device_get_parent(device); g_autoptr(FuDeviceLocker) locker = fu_device_locker_new(parent, error); if (locker == NULL) return FALSE; return fu_device_cleanup(parent, progress, flags, error); } static gchar * fu_wac_module_convert_version(FuDevice *device, guint64 version_raw) { if (version_raw > G_MAXUINT16) return fu_version_from_uint32(version_raw, fu_device_get_version_format(device)); return fu_version_from_uint16(version_raw, fu_device_get_version_format(device)); } static void fu_wac_module_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuWacModule *self = FU_WAC_MODULE(object); FuWacModulePrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_FW_TYPE: g_value_set_uint(value, priv->fw_type); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_wac_module_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuWacModule *self = FU_WAC_MODULE(object); FuWacModulePrivate *priv = GET_PRIVATE(self); switch (prop_id) { case PROP_FW_TYPE: priv->fw_type = g_value_get_uint(value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_wac_module_init(FuWacModule *self) { fu_device_add_protocol(FU_DEVICE(self), "com.wacom.usb"); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(FU_DEVICE(self), FU_DEVICE_PRIVATE_FLAG_MD_SET_FLAGS); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_BCD); fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); } static void fu_wac_module_constructed(GObject *object) { FuWacModule *self = FU_WAC_MODULE(object); FuWacModulePrivate *priv = GET_PRIVATE(self); FuDevice *proxy = fu_device_get_proxy(FU_DEVICE(self)); /* not set in tests */ if (proxy != NULL) { g_autofree gchar *devid = NULL; /* set vendor ID */ fu_device_build_vendor_id_u16(FU_DEVICE(self), "USB", fu_device_get_vid(FU_DEVICE(proxy))); /* set USB physical and logical IDs */ fu_device_incorporate(FU_DEVICE(self), proxy, FU_DEVICE_INCORPORATE_FLAG_PHYSICAL_ID); fu_device_set_logical_id(FU_DEVICE(self), fu_wac_module_fw_type_to_string(priv->fw_type)); /* append the firmware kind to the generated GUID */ devid = g_strdup_printf("USB\\VID_%04X&PID_%04X-%s", fu_device_get_vid(proxy), fu_device_get_pid(proxy), fu_wac_module_fw_type_to_string(priv->fw_type)); fu_device_add_instance_id(FU_DEVICE(self), devid); } G_OBJECT_CLASS(fu_wac_module_parent_class)->constructed(object); } static void fu_wac_module_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 100, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload"); } static void fu_wac_module_class_init(FuWacModuleClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); GParamSpec *pspec; FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); /* properties */ object_class->get_property = fu_wac_module_get_property; object_class->set_property = fu_wac_module_set_property; /** * FuWacModule:fw-type: * * The firmware kind. */ pspec = g_param_spec_uint("fw-type", NULL, NULL, 0, G_MAXUINT, 0, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_FW_TYPE, pspec); object_class->constructed = fu_wac_module_constructed; device_class->to_string = fu_wac_module_to_string; device_class->cleanup = fu_wac_module_cleanup; device_class->set_progress = fu_wac_module_set_progress; device_class->convert_version = fu_wac_module_convert_version; } fwupd-2.0.10/plugins/wacom-usb/fu-wac-module.h000066400000000000000000000013511501337203100211060ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_WAC_MODULE (fu_wac_module_get_type()) G_DECLARE_DERIVABLE_TYPE(FuWacModule, fu_wac_module, FU, WAC_MODULE, FuDevice) struct _FuWacModuleClass { FuDeviceClass parent_class; }; #define FU_WAC_MODULE_POLL_INTERVAL 100 /* ms */ #define FU_WAC_MODULE_START_TIMEOUT 15000 /* ms */ #define FU_WAC_MODULE_DATA_TIMEOUT 10000 /* ms */ #define FU_WAC_MODULE_END_TIMEOUT 10000 /* ms */ gboolean fu_wac_module_set_feature(FuWacModule *self, guint8 command, GBytes *blob, FuProgress *progress, guint poll_interval, guint busy_timeout, GError **error); fwupd-2.0.10/plugins/wacom-usb/fu-wac-plugin.c000066400000000000000000000101211501337203100211050ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-wac-android-device.h" #include "fu-wac-device.h" #include "fu-wac-firmware.h" #include "fu-wac-module-bluetooth-id6.h" #include "fu-wac-module-bluetooth-id9.h" #include "fu-wac-module-bluetooth.h" #include "fu-wac-module-scaler.h" #include "fu-wac-module-sub-cpu.h" #include "fu-wac-module-touch-id7.h" #include "fu-wac-module-touch.h" #include "fu-wac-plugin.h" struct _FuWacPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuWacPlugin, fu_wac_plugin, FU_TYPE_PLUGIN) static gboolean fu_wac_plugin_write_firmware(FuPlugin *plugin, FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDevice *parent = fu_device_get_parent(device); g_autoptr(FuDeviceLocker) locker = NULL; locker = fu_device_locker_new(parent != NULL ? parent : device, error); if (locker == NULL) return FALSE; return fu_device_write_firmware(device, firmware, progress, flags, error); } static gboolean fu_wac_plugin_composite_prepare(FuPlugin *self, GPtrArray *devices, GError **error) { for (guint i = 0; i < devices->len; i++) { FuDevice *device = g_ptr_array_index(devices, i); if (FU_IS_WAC_DEVICE(device)) { g_autoptr(FuDeviceLocker) locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; g_info("switching main device to flash loader"); if (!fu_wac_device_switch_to_flash_loader(FU_WAC_DEVICE(device), error)) return FALSE; } } return TRUE; } static gboolean fu_wac_plugin_composite_cleanup(FuPlugin *self, GPtrArray *devices, GError **error) { g_autoptr(FuWacDevice) main_device = NULL; /* find the main device in transaction (which may *be* the main device, or just a proxy) */ for (guint i = 0; i < devices->len; i++) { FuDevice *device_tmp = g_ptr_array_index(devices, i); if (FU_IS_WAC_DEVICE(device_tmp)) { g_set_object(&main_device, FU_WAC_DEVICE(device_tmp)); break; } if (FU_IS_WAC_MODULE(device_tmp)) { g_set_object(&main_device, FU_WAC_DEVICE(fu_device_get_proxy(device_tmp))); break; } } /* reset */ if (main_device != NULL) { g_autoptr(FuDeviceLocker) locker = fu_device_locker_new(main_device, error); if (locker == NULL) return FALSE; g_info("resetting main device"); fu_device_add_flag(FU_DEVICE(main_device), FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); if (!fu_wac_device_update_reset(main_device, error)) return FALSE; } /* success */ return TRUE; } static void fu_wac_plugin_init(FuWacPlugin *self) { } static void fu_wac_plugin_object_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_set_name(plugin, "wacom_usb"); } static void fu_wac_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_set_device_gtype_default(plugin, FU_TYPE_WAC_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_WAC_ANDROID_DEVICE); fu_plugin_add_device_gtype(plugin, FU_TYPE_WAC_MODULE_BLUETOOTH); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_WAC_MODULE_BLUETOOTH_ID6); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_WAC_MODULE_BLUETOOTH_ID9); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_WAC_MODULE_SCALER); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_WAC_MODULE_SUB_CPU); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_WAC_MODULE_TOUCH); /* coverage */ fu_plugin_add_device_gtype(plugin, FU_TYPE_WAC_MODULE_TOUCH_ID7); /* coverage */ fu_plugin_add_firmware_gtype(plugin, "wacom", FU_TYPE_WAC_FIRMWARE); } static void fu_wac_plugin_class_init(FuWacPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->constructed = fu_wac_plugin_object_constructed; plugin_class->constructed = fu_wac_plugin_constructed; plugin_class->write_firmware = fu_wac_plugin_write_firmware; plugin_class->composite_prepare = fu_wac_plugin_composite_prepare; plugin_class->composite_cleanup = fu_wac_plugin_composite_cleanup; } fwupd-2.0.10/plugins/wacom-usb/fu-wac-plugin.h000066400000000000000000000003431501337203100211170ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuWacPlugin, fu_wac_plugin, FU, WAC_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/wacom-usb/fu-wac.rs000066400000000000000000000062331501337203100200240ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ValidateStream, Default)] #[repr(C, packed)] struct FuStructWacFirmwareHdr { magic: [char; 5] == "WACOM", } #[derive(Parse)] #[repr(C, packed)] struct FuStructWtaBlockHeader { block_start: u32le, block_size: u32le, } #[derive(ToString)] enum FuWacReportId { FwDescriptor = 0xCB, // GET_FEATURE SwitchToFlashLoader = 0xCC, // SET_FEATURE QuitAndReset = 0xCD, // SET_FEATURE ReadBlockData = 0xD1, // GET_FEATURE WriteBlock = 0xD2, // SET_FEATURE EraseBlock = 0xD3, // SET_FEATURE SetReadAddress = 0xD4, // GET_FEATURE GetStatus = 0xD5, // GET_FEATURE UpdateReset = 0xD6, // SET_FEATURE WriteWord = 0xD7, // SET_FEATURE GetParameters = 0xD8, // GET_FEATURE GetFlashDescriptor = 0xD9, // GET_FEATURE GetChecksums = 0xDA, // GET_FEATURE SetChecksumForBlock = 0xDB, // SET_FEATURE CalculateChecksumForBlock = 0xDC, // SET_FEATURE WriteChecksumTable = 0xDE, // SET_FEATURE GetCurrentFirmwareIdx = 0xE2, // GET_FEATURE Module = 0xE4, } #[derive(ToString)] #[repr(u8)] enum FuWacModuleFwType { Touch = 0x00, Bluetooth = 0x01, EmrCorrection = 0x02, BluetoothHid = 0x03, Scaler = 0x04, BluetoothId6 = 0x06, TouchId7 = 0x07, BluetoothId9 = 0x09, SubCpu = 0x0A, Main = 0x3F, } #[derive(ToString)] enum FuWacModuleCommand { Start = 0x01, Data = 0x02, End = 0x03, } #[derive(ToString)] enum FuWacModuleStatus { Ok, Busy, ErrCrc, ErrCmd, ErrHwAccessFail, ErrFlashNoSupport, ErrModeWrong, ErrMpuNoSupport, ErrVersionNoSupport, ErrErase, ErrWrite, ErrExit, Err, ErrInvalidOp, ErrWrongImage, } #[derive(ToBitString)] enum FuWacDeviceStatus { Unknown = 0, Writing = 1 << 0, Erasing = 1 << 1, ErrorWrite = 1 << 2, ErrorErase = 1 << 3, WriteProtected = 1 << 4, } #[derive(New, Default)] #[repr(C, packed)] struct FuStructId9UnknownCmd { unknown1: u16be == 0x7050, unknown2: u32be == 0, size: u16be, // Size of payload to be transferred } #[derive(New, Default)] #[repr(C, packed)] struct FuStructId9SpiCmd { command: u8 == 0x91, start_addr: u32be == 0, size: u16be, // sizeof(data) + size of payload data: FuStructId9UnknownCmd, } #[derive(New,Validate)] #[repr(C, packed)] struct FuStructId9LoaderCmd { command: u8, size: u16be, // sizeof(data) + size of payload crc: u32be, // CRC(concat(data, payload)) data: FuStructId9SpiCmd, } #[derive(Parse)] #[repr(C, packed)] struct FuStructModuleDesc { _report_id: u8, bootloader_version: u16be, number_modules: u8, // FuStructModuleItem[number_modules] } #[derive(Parse)] #[repr(C, packed)] struct FuStructModuleItem { kind: FuWacModuleFwType, version: u16be, version2: u8, } fwupd-2.0.10/plugins/wacom-usb/meson.build000066400000000000000000000034341501337203100204360ustar00rootroot00000000000000plugins += {meson.current_source_dir().split('/')[-1]: true} cargs = ['-DG_LOG_DOMAIN="FuPluginWacomUsb"'] plugin_quirks += files('wacom-usb.quirk') plugin_builtin_wac = static_library('fu_plugin_wac', rustgen.process( 'fu-wac.rs', # fuzzing ), sources: [ 'fu-wac-common.c', 'fu-wac-android-device.c', 'fu-wac-device.c', 'fu-wac-firmware.c', # fuzzing 'fu-wac-module.c', 'fu-wac-module-bluetooth.c', 'fu-wac-module-bluetooth-id6.c', 'fu-wac-module-bluetooth-id9.c', 'fu-wac-module-scaler.c', 'fu-wac-module-sub-cpu.c', 'fu-wac-module-touch.c', 'fu-wac-module-touch-id7.c', 'fu-wac-plugin.c', ], include_directories: plugin_incdirs, c_args: cargs, dependencies: plugin_deps, link_with: plugin_libs, ) plugin_builtins += plugin_builtin_wac enumeration_data += files( 'tests/wacom-movink-13-setup.json', ) device_tests += files( 'tests/wacom-intuos-bt-m.json', 'tests/wacom-intuos-bt-s.json', 'tests/wacom-movink-13.json', ) if get_option('tests') install_data(['tests/wacom-usb.builder.xml'], install_dir: join_paths(installed_test_datadir, 'tests')) env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) e = executable( 'wacom-usb-self-test', rustgen.process('fu-wac.rs'), sources: [ 'fu-self-test.c', ], include_directories: plugin_incdirs, dependencies: plugin_deps, link_with: [ plugin_libs, plugin_builtin_wac, ], install: true, install_rpath: libdir_pkg, install_dir: installed_test_bindir, c_args: [ cargs, '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('wacom-usb-self-test', e, env: env) # added to installed-tests endif fwupd-2.0.10/plugins/wacom-usb/tests/000077500000000000000000000000001501337203100174325ustar00rootroot00000000000000fwupd-2.0.10/plugins/wacom-usb/tests/wacom-intuos-bt-m.json000066400000000000000000000015141501337203100236100ustar00rootroot00000000000000{ "name": "Wacom Intuos BT-M", "interactive": false, "steps": [ { "url": "d16b682de56c42b134f1e5af7f9926c62dc246df851648e49aa1d1d0e5b38532-Wacom-Intuos_BT-M_MainFW-1.66.cab", "emulation-url": "48a669a65a69b999afdb172882959e30c25b61d3095f47b04d9207b019be74cf-Wacom-Intuos_BT-M_MainFW-1.66.zip", "components": [ { "name": "main", "version": "1.66", "guids": [ "edf56833-dbe5-56ca-a651-734b01bb02ba" ] } ] }, { "url": "1ee4f3dc9fd08acd4c6bc833b25e7061f85ddd40122148d66940cd3ddd748920-Wacom-Intuos_BT-M_BluetoothFW-1.12.cab", "components": [ { "name": "bluetooth", "version": "1.12", "guids": [ "230fb992-35c7-56b1-8236-7f5674a04153" ] } ] } ] } fwupd-2.0.10/plugins/wacom-usb/tests/wacom-intuos-bt-s.json000066400000000000000000000017771501337203100236310ustar00rootroot00000000000000{ "name": "Wacom Intuos BT-S", "interactive": false, "steps": [ { "url": "0d9314e86d28f78f5dc37d71c5cc5205b681334fe64b28f1ec95b5eedc6e1ea6-Wacom-CTL-4100WL-1.10.cab", "components": [ { "name": "bluetooth", "version": "1.12", "guids": [ "e317b626-4c1d-5892-8b4c-aafec894a4c9" ] }, { "name": "main", "version": "1.10", "guids": [ "dc80ba55-c5f7-5195-b6f4-23c2940bcaec" ] } ] }, { "url": "cfec6515263c0d358d40d7b8fb6472214015225210dfa2db8dd307e896f03dcf-Wacom-CTL-4100WL-1.11.cab", "components": [ { "name": "bluetooth", "version": "1.12", "guids": [ "e317b626-4c1d-5892-8b4c-aafec894a4c9" ] }, { "name": "main", "version": "1.11", "guids": [ "dc80ba55-c5f7-5195-b6f4-23c2940bcaec" ] } ] } ] } fwupd-2.0.10/plugins/wacom-usb/tests/wacom-movink-13-setup.json000066400000000000000000000136631501337203100243240ustar00rootroot00000000000000{ "UsbDevices": [ { "GType": "FuUsbDevice", "PlatformId": "3-3.3", "Created": "2024-11-08T05:04:25.219046Z", "IdVendor": 1386, "IdProduct": 1008, "Device": 258, "USB": 512, "Manufacturer": 1, "Product": 2, "SerialNumber": 3, "UsbConfigDescriptors": [ { "ConfigurationValue": 1 } ], "UsbInterfaces": [ { "Length": 9, "DescriptorType": 4, "InterfaceClass": 3, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 129, "Interval": 1, "MaxPacketSize": 64 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 1, "InterfaceClass": 255, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 4, "MaxPacketSize": 64 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 2, "InterfaceClass": 3, "InterfaceProtocol": 2, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 133, "Interval": 255, "MaxPacketSize": 16 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 3, "InterfaceClass": 3, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 130, "Interval": 255, "MaxPacketSize": 64 } ] }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 4, "InterfaceClass": 3, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 131, "Interval": 1, "MaxPacketSize": 64 } ] } ], "UsbEvents": [ { "Id": "#d5a801ad", "Data": "usb" }, { "Id": "#ad8c58d3", "Data": "usb" }, { "Id": "#1075ed5c", "Data": "usb_device" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=261\nDEVNAME=bus/usb/003/006\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=56a/3f0/102\nTYPE=0/0/0\nBUSNUM=003\nDEVNUM=006" }, { "Id": "#d432c663", "Data": "bus/usb/003/006" }, { "Id": "#9b895db2" }, { "Id": "#66f3e150" }, { "Id": "#d410b6c7" }, { "Id": "#1075ed5c", "Data": "usb_device" }, { "Id": "#4693935e", "Data": "003" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=261\nDEVNAME=bus/usb/003/006\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=56a/3f0/102\nTYPE=0/0/0\nBUSNUM=003\nDEVNUM=006" }, { "Id": "#1ab3ae0a", "Data": "006" }, { "Id": "#4693935e", "Data": "003" }, { "Id": "#bddbca22", "Data": "MAJOR=189\nMINOR=261\nDEVNAME=bus/usb/003/006\nDEVTYPE=usb_device\nDRIVER=usb\nPRODUCT=56a/3f0/102\nTYPE=0/0/0\nBUSNUM=003\nDEVNUM=006" }, { "Id": "#1ab3ae0a", "Data": "006" }, { "Id": "#1fcf122d", "Data": "V2Fjb20gTW92aW5rIDEzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" }, { "Id": "#028c3a0e", "Data": "M0xIUzFIMTAwMDExNQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" }, { "Id": "#948499c0", "Data": "1SAAAAA=" }, { "Id": "#05c7b0ea", "Data": "ywFIBD8BAgAEAQACBQEB/wAABwD///////////////8=" }, { "Id": "#b5c8e07f", "Data": "BQEJgKEBhf6hAAmCCYMVACUBdQGVAoEGlQaBA8DABQ0JAqEBhQYJIDUARQAVACUBoQAJVCUBdQiVAYECCUIJRAlaBg3/CV0FDQlFCTwJMnUBlQeBApUBgQMFAQkwZRFVDUfAcgAAJsBydRCVAYECdQiBAwkxR4xAAAAmjEB1EJUBgQJ1CIEDBQ0JMGUAVQBFACb/D3UQgQIJPQk+ZRRVDjbY3EYoIxbY3CYoI3UQlQKBAgYN/wlBZRRVADZM/0azABZ8/CaDA3UQlQGBCgoDDWUAVQA1AEUAFQAm/weBAgoyATUARQAVACb/AHUIlQGBAglbCVwXAAAAgCf///9/dSCVAoECCXcVACb/D3UQlQGBAgUNCVYVACf//wAAdRCVAYEKBg3/CiACFQAn//8AAHUQlQGBCsDABg3/CQKhAYUeCSA1AEUAFQAlAaEACVQlAXUIlQGBAglCCUQJWgldCUUJPAkyCTYlAXUBlQiBAgowAWURVQ1HUHQAACeg6AAAdRiVAYECCjEBRxxCAAAnOIQAAIECCTBVAGUARQAVACb/H3UQgQIJPQk+ZRRVADWmRVoVpiVadRCVAoECCUFlFFUANkz/RrMAFnz8JoMDdRCVAYEKCgMNZQBVADUARQAVACb/B4ECCjIBNQBFABUAJv8AdQiVAYECCVsJXBcAAACAJ////391IJUCgQIJdxUAJv8PdRCVAYECCVYVACf//wAAdRCVAYEKCiACFQAn//8AAHUQlQGBCsCFEWUAVQA1AEUACTmhAAk5oQA1AEUAGhAJKhIJFQAlAXUBlQOBApUFgQPAFQAlAHUIlQeBA8CFE2UAVQA1AEUAChMQoQAVACUAdQiVAYEDChMQoQA1AEUAdQeVAYEDClQEFQAlAXUBlQGBAsAKExChADUARQAKOhAVACUHdQOVAYECdQWBA8AVACUAdQiVBYEDwAkOoQKFAgoCEBUBJQJ1CJUBsQKFAwoDEBUAJv8AlQGxAoUECgQQFQAlAZUBsQKFBwoJEBUAJv8AlQGxArEDCgcQCQAKCBAJAAkACQAn//8AAHUQlQaxAgkAJQB1CJUBsQOFDAowDQoxDQoyDQozDWURVQ01AEbIABUAJpABdRCVBLEChQ0KDRBlAFUARQAlAXUIlQGxAoUUChQQJv8AlQ2xAoUcChUQJv8AlRqxAoXMCswQlQKxAsAJDqEChToKOhAlB3UIlQGxAsAKrBChAhUAJv8AdQiFrAkAlr8AgQKFFQkAlQ6xAoUzCQCVErEChUQJAJUEsQKFRQkAlSCxAoVgCQCVP7EChWEJAJU+sQKFYgkAlT6xAoVlCQCVBLEChWYJAJUEsQKFZwkAlQSxAoVoCQCVEbEChW8JAJU+sQKFzQkAlQKxAsCF0QkBlgQBsQKF0gkBlgQBsQKF0wkBlgQAsQKF1AkBlgQAsQKF1QkBlgQAsQKF1gkBlgQAsQKF1wkBlhQAsQKF2AkBlgwAsQKF2QkBlgAFsQKF2gkBlgQCsQKF2wkBlgYAsQKF3AkBlgIAsQKF3QkBlgQAsQKF3gkBlgQAsQKF3wkBliIAsQKF4AkBlgEAsQKF4QkBlgIAsQKF4gkBlgIAsQKF4wkBlgIAsQKF5AkBlv8BsQKFywkBlh8AsQLA" }, { "Id": "#e62a5e2f", "Error": -1 } ] } ] } fwupd-2.0.10/plugins/wacom-usb/tests/wacom-movink-13.json000066400000000000000000000012611501337203100231550ustar00rootroot00000000000000{ "name": "Wacom Movink 13", "interactive": false, "steps": [ { "emulation-file": "@enumeration_datadir@/wacom-movink-13-setup.json", "components": [ { "version": "1.2", "version-bootloader": "1.48", "guids": [ "8be194af-06c7-5f53-8441-2d0b01b045ee" ] }, { "name": "scaler", "version": "1.0.2.0", "guids": [ "d68637e9-ac2d-5128-8515-08a8d28295aa" ] }, { "name": "touch", "version": "0.7", "guids": [ "87a37392-f3be-5ff3-bd58-86f7238f6456" ] } ] } ] } fwupd-2.0.10/plugins/wacom-usb/tests/wacom-usb.builder.xml000066400000000000000000000004611501337203100234770ustar00rootroot00000000000000 0x0 0x8008000 aGVsbG8gd29ybGQ= 0x1 0x8040000 aGVsbG8gd29ybGQ= fwupd-2.0.10/plugins/wacom-usb/wacom-usb.quirk000066400000000000000000000056771501337203100212610ustar00rootroot00000000000000# Movink (USB) [DTH135K0C] [USB\VID_056A&PID_03F0] Plugin = wacom_usb GType = FuWacDevice # Intuos Pro medium (2nd-gen USB) [PTH-660] [USB\VID_056A&PID_0357] Plugin = wacom_usb Flags = use-runtime-version # Intuos Pro large (2nd-gen USB) [PTH-860] [USB\VID_056A&PID_0358] Plugin = wacom_usb Flags = use-runtime-version # Intuos S 3rd-gen (USB) [CTL-4100] [USB\VID_056A&PID_0374] Plugin = wacom_usb Flags = use-runtime-version,no-serial-number # Intuos S 3rd-gen (USB) [CTL-4100 - Android Mode] [USB\VID_2D1F&PID_0374] Plugin = wacom_usb GType = FuWacAndroidDevice # Intuos M 3rd-gen (USB) [CTL-6100] [USB\VID_056A&PID_0375] Plugin = wacom_usb Flags = use-runtime-version,no-serial-number # Intuos M 3rd-gen (USB) [CTL-6100 - Android Mode] [USB\VID_2D1F&PID_0375] Plugin = wacom_usb GType = FuWacAndroidDevice # Intuos BT S 3rd-gen (USB) [CTL-4100WL] [USB\VID_056A&PID_0376] Plugin = wacom_usb Flags = use-runtime-version,no-serial-number # Intuos BT S 3rd-gen (USB) [CTL-4100WL - Android Mode] [USB\VID_2D1F&PID_0376] Plugin = wacom_usb GType = FuWacAndroidDevice # Intuos BT M 3rd-gen (USB) [CTL-6100WL] [USB\VID_056A&PID_0378] Plugin = wacom_usb Flags = use-runtime-version,no-serial-number # Intuos BT M 3rd-gen (USB) [CTL-6100WL - Android Mode] [USB\VID_2D1F&PID_0378] Plugin = wacom_usb GType = FuWacAndroidDevice # Intuos Pro Small (2nd-gen USB) [PTH-460] [USB\VID_056A&PID_0392] Plugin = wacom_usb # Intuos BT S 3rd-gen, Rev 2 (USB) [CTL-4100WL] [USB\VID_056A&PID_03C5] Plugin = wacom_usb Flags = use-runtime-version,no-serial-number # Intuos BT S 3rd-gen, Rev 2 (USB) [CTL-4100WL - Android Mode] [USB\VID_2D1F&PID_03C5] Plugin = wacom_usb GType = FuWacAndroidDevice # Intuos BT M 3rd-gen, Rev 2 (USB) [CTL-6100WL] [USB\VID_056A&PID_03C7] Plugin = wacom_usb Flags = use-runtime-version,no-serial-number # Intuos BT M 3rd-gen, Rev 2 (USB) [CTL-6100WL - Android Mode] [USB\VID_2D1F&PID_03C7] Plugin = wacom_usb GType = FuWacAndroidDevice # Intuos Pro Small (2nd-gen USB v2) [PTH-460] [USB\VID_056A&PID_03DC] Plugin = wacom_usb # Cintiq Pro 27 (USB) [DTH271] [USB\VID_056A&PID_03C0] Plugin = wacom_usb # Wacom One 13 (USB) [DTH-134] [USB\VID_056A&PID_03CB] Plugin = wacom_usb # Wacom One 12 (USB) [DTC-121] [USB\VID_056A&PID_03CE] Plugin = wacom_usb # DTH134 (USB) [DTH-134] [USB\VID_056A&PID_03EC] Plugin = wacom_usb # DTC121 (USB) [DTC-121] [USB\VID_056A&PID_03ED] Plugin = wacom_usb # Wacom One Pen tablet small (USB) [CTC4110WL] [USB\VID_0531&PID_0100] Plugin = wacom_usb Flags = no-serial-number # Wacom One Pen tablet small (USB) [CTC4110WL - Android Mode] [USB\VID_0531&PID_0104] Plugin = wacom_usb # Wacom One Pen tablet medium (USB) [CTC6110WL] [USB\VID_0531&PID_0102] Plugin = wacom_usb Flags = no-serial-number # Wacom One Pen tablet medium (USB) [CTC6110WL - Android Mode] [USB\VID_0531&PID_0105] Plugin = wacom_usb # Cintiq Pro 17 (USB) [DTH172] [USB\VID_056A&PID_03C4] Plugin = wacom_usb # Cintiq Pro 22 (USB) [DTH227] [USB\VID_056A&PID_03D0] Plugin = wacom_usb fwupd-2.0.10/plugins/wistron-dock/000077500000000000000000000000001501337203100170165ustar00rootroot00000000000000fwupd-2.0.10/plugins/wistron-dock/README.md000066400000000000000000000026441501337203100203030ustar00rootroot00000000000000--- title: Plugin: Wistron Dock --- ## Introduction Wistron use a generic flashing protocol for dock devices supplied to various OEMs. The protocol is compatible with designs that use Nuvoton, Infineon, and GD MCUs. ## Firmware Format The daemon will decompress the cabinet archive and extract a firmware blob in a zipped file format. The archive must contain exactly one file with each of these extensions: * `.wdfl.sig` * `.wdfl` * `.bin` This plugin supports the following protocol ID: * `com.wistron.dock` ## GUID Generation These devices use the standard USB DeviceInstanceId values, e.g. * `USB\VID_0FB8&PID_0010` Devices also have additional instance IDs which corresponds MCU type, e.g. * `USB\VID_0FB8&PID_0010&MCUID_M4521` ## Update Behavior The device enters DFU mode, then writes the fixed-size WDFL signature and WDFL data, then writes blocks of variable sized data. Finally it clears DFU mode and the user can re-plug the USB-C cable to trigger the update. ## Vendor ID Security The vendor ID is set from the USB vendor, in this example set to `USB:0x0FB8` ## External Interface Access This plugin requires read/write access to `/dev/bus/usb`. ## Version Considerations This plugin has been available since fwupd version `1.8.9`. ## Owners Anyone can submit a pull request to modify this plugin, but the following people should be consulted before making major or functional changes: * Felix Chen: @a999153 fwupd-2.0.10/plugins/wistron-dock/fu-wistron-dock-common.h000066400000000000000000000031341501337203100235110ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_WISTRON_DOCK_CMD_ICP_ENTER 0x81 #define FU_WISTRON_DOCK_CMD_ICP_EXIT 0x82 #define FU_WISTRON_DOCK_CMD_ICP_ADDRESS 0x84 #define FU_WISTRON_DOCK_CMD_ICP_READBLOCK 0x85 #define FU_WISTRON_DOCK_CMD_ICP_WRITEBLOCK 0x86 #define FU_WISTRON_DOCK_CMD_ICP_MCUID 0x87 #define FU_WISTRON_DOCK_CMD_ICP_BBINFO 0x88 /* bb code information */ #define FU_WISTRON_DOCK_CMD_ICP_USERINFO 0x89 /* user code information */ #define FU_WISTRON_DOCK_CMD_ICP_DONE 0x5A #define FU_WISTRON_DOCK_CMD_ICP_ERROR 0xFF #define FU_WISTRON_DOCK_CMD_ICP_EXIT_WDRESET 0x01 /* exit ICP with watch dog reset */ #define FU_WISTRON_DOCK_CMD_DFU_ENTER 0x91 #define FU_WISTRON_DOCK_CMD_DFU_EXIT 0x92 #define FU_WISTRON_DOCK_CMD_DFU_ADDRESS 0x93 #define FU_WISTRON_DOCK_CMD_DFU_READIMG_BLOCK 0x94 #define FU_WISTRON_DOCK_CMD_DFU_WRITEIMG_BLOCK 0x95 #define FU_WISTRON_DOCK_CMD_DFU_VERIFY 0x96 #define FU_WISTRON_DOCK_CMD_DFU_COMPOSITE_VER 0x97 #define FU_WISTRON_DOCK_CMD_DFU_WRITE_WDFL_SIG 0x98 #define FU_WISTRON_DOCK_CMD_DFU_WRITE_WDFL_DATA 0x99 #define FU_WISTRON_DOCK_CMD_DFU_VERIFY_WDFL 0x9A #define FU_WISTRON_DOCK_CMD_DFU_SERINAL_NUMBER 0x9B #define FU_WISTRON_DOCK_CMD_DFU_DONE 0x5A #define FU_WISTRON_DOCK_CMD_DFU_ERROR 0xFF #define FU_WISTRON_DOCK_WDIT_SIZE 512 /* bytes */ #define FU_WISTRON_DOCK_WDIT_TAG_ID 0x4954 /* 'IT' */ #define FU_WISTRON_DOCK_WDFL_SIG_SIZE 256 /* bytes */ #define FU_WISTRON_DOCK_WDFL_DATA_SIZE 1328 /* bytes */ fwupd-2.0.10/plugins/wistron-dock/fu-wistron-dock-device.c000066400000000000000000000637551501337203100234720ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * Copyright 2022 Wistron * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-wistron-dock-common.h" #include "fu-wistron-dock-device.h" #include "fu-wistron-dock-struct.h" struct _FuWistronDockDevice { FuHidDevice parent_instance; guint8 component_idx; guint8 update_phase; guint8 status_code; guint8 imgmode; gchar *icp_bbinfo; gchar *icp_userinfo; guint device_insert_id; }; G_DEFINE_TYPE(FuWistronDockDevice, fu_wistron_dock_device, FU_TYPE_HID_DEVICE) #define FU_WISTRON_DOCK_TRANSFER_BLOCK_SIZE 512 /* bytes */ #define FU_WISTRON_DOCK_TRANSFER_TIMEOUT 5000 /* ms */ #define FU_WISTRON_DOCK_TRANSFER_RETRY_COUNT 5 #define FU_WISTRON_DOCK_TRANSFER_RETRY_DELAY 100 /* ms */ #define FU_WISTRON_DOCK_ID_USB_CONTROL 0x06 /* 7 bytes */ #define FU_WISTRON_DOCK_ID_USB_BLOCK 0x07 /* 512 bytes */ #define FU_WISTRON_DOCK_ID_IMG_CONTROL 0x16 /* 7 bytes */ #define FU_WISTRON_DOCK_ID_DOCK_IMG_DATA 0x17 /* 512 bytes */ #define FU_WISTRON_DOCK_ID_DOCK_WDIT 0x20 /* 512 bytes */ #define FU_WISTRON_DOCK_ID_DOCK_WDFL_SIG 0x21 /* 256 bytes */ #define FU_WISTRON_DOCK_ID_DOCK_WDFL_DATA 0x22 /* 1440 bytes */ #define FU_WISTRON_DOCK_ID_DOCK_SN 0x23 /* 32 bytes */ static void fu_wistron_dock_device_to_string(FuDevice *device, guint idt, GString *str) { FuWistronDockDevice *self = FU_WISTRON_DOCK_DEVICE(device); fwupd_codec_string_append(str, idt, "ComponentIdx", fu_wistron_dock_component_idx_to_string(self->component_idx)); fwupd_codec_string_append(str, idt, "UpdatePhase", fu_wistron_dock_update_phase_to_string(self->update_phase)); fwupd_codec_string_append(str, idt, "StatusCode", fu_wistron_dock_status_code_to_string(self->status_code)); fwupd_codec_string_append_hex(str, idt, "ImgMode", self->imgmode); fwupd_codec_string_append(str, idt, "IcpBbInfo", self->icp_bbinfo); fwupd_codec_string_append(str, idt, "IcpUserInfo", self->icp_userinfo); } typedef struct { guint8 *cmd; gsize cmdsz; guint8 *buf; gsize bufsz; gboolean check_result; } FuWistronDockHelper; static gboolean fu_wistron_dock_device_control_cb(FuDevice *device, gpointer user_data, GError **error) { FuWistronDockHelper *helper = (FuWistronDockHelper *)user_data; if (!fu_hid_device_set_report(FU_HID_DEVICE(device), helper->cmd[0], /* value */ helper->cmd, helper->cmdsz, FU_WISTRON_DOCK_TRANSFER_TIMEOUT, FU_HID_DEVICE_FLAG_IS_FEATURE, error)) return FALSE; if (helper->check_result) { if (!fu_hid_device_get_report(FU_HID_DEVICE(device), helper->buf[0], /* value */ helper->buf, helper->bufsz, FU_WISTRON_DOCK_TRANSFER_TIMEOUT, FU_HID_DEVICE_FLAG_IS_FEATURE, error)) return FALSE; if (helper->buf[7] != FU_WISTRON_DOCK_CMD_ICP_DONE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not icp-done, got 0x%02x", helper->cmd[7]); return FALSE; } } return TRUE; } static gboolean fu_wistron_dock_device_control_write(FuWistronDockDevice *self, guint8 *cmd, gsize cmdsz, gboolean check_result, GError **error) { FuWistronDockHelper helper = {.cmd = cmd, .cmdsz = cmdsz, .buf = cmd, .bufsz = cmdsz, .check_result = check_result}; return fu_device_retry_full(FU_DEVICE(self), fu_wistron_dock_device_control_cb, FU_WISTRON_DOCK_TRANSFER_RETRY_COUNT, FU_WISTRON_DOCK_TRANSFER_RETRY_DELAY, &helper, error); } static gboolean fu_wistron_dock_device_control_read(FuWistronDockDevice *self, guint8 *cmd, gsize cmdsz, guint8 *buf, gsize bufsz, gboolean check_result, GError **error) { FuWistronDockHelper helper = {.cmd = cmd, .cmdsz = cmdsz, .buf = buf, .bufsz = bufsz, .check_result = check_result}; return fu_device_retry_full(FU_DEVICE(self), fu_wistron_dock_device_control_cb, FU_WISTRON_DOCK_TRANSFER_RETRY_COUNT, FU_WISTRON_DOCK_TRANSFER_RETRY_DELAY, &helper, error); } static gboolean fu_wistron_dock_device_data_write_cb(FuDevice *device, gpointer user_data, GError **error) { FuWistronDockHelper *helper = (FuWistronDockHelper *)user_data; if (!fu_hid_device_set_report(FU_HID_DEVICE(device), helper->cmd[0], /* value */ helper->cmd, helper->cmdsz, FU_WISTRON_DOCK_TRANSFER_TIMEOUT, FU_HID_DEVICE_FLAG_IS_FEATURE, error)) return FALSE; if (!fu_hid_device_set_report(FU_HID_DEVICE(device), helper->buf[0], /* value */ helper->buf, helper->bufsz, FU_WISTRON_DOCK_TRANSFER_TIMEOUT, FU_HID_DEVICE_FLAG_IS_FEATURE, error)) return FALSE; if (!fu_hid_device_get_report(FU_HID_DEVICE(device), helper->cmd[0], /* value */ helper->cmd, helper->cmdsz, FU_WISTRON_DOCK_TRANSFER_TIMEOUT, FU_HID_DEVICE_FLAG_IS_FEATURE, error)) return FALSE; if (helper->cmd[7] != FU_WISTRON_DOCK_CMD_ICP_DONE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not icp-done, got 0x%02x", helper->cmd[7]); return FALSE; } return TRUE; } static gboolean fu_wistron_dock_device_data_write(FuWistronDockDevice *self, guint8 *cmd, gsize cmdsz, guint8 *buf, gsize bufsz, GError **error) { FuWistronDockHelper helper = {.cmd = cmd, .cmdsz = cmdsz, .buf = buf, .bufsz = bufsz}; return fu_device_retry_full(FU_DEVICE(self), fu_wistron_dock_device_data_write_cb, FU_WISTRON_DOCK_TRANSFER_RETRY_COUNT, FU_WISTRON_DOCK_TRANSFER_RETRY_DELAY, &helper, error); } static gboolean fu_wistron_dock_device_write_wdfl_sig(FuWistronDockDevice *self, const guint8 *buf, gsize bufsz, GError **error) { guint8 cmd[8] = {FU_WISTRON_DOCK_ID_IMG_CONTROL, FU_WISTRON_DOCK_CMD_DFU_WRITE_WDFL_SIG}; guint8 towrite[FU_WISTRON_DOCK_WDFL_SIG_SIZE + 1] = {FU_WISTRON_DOCK_ID_DOCK_WDFL_SIG}; if (!fu_memcpy_safe(towrite, sizeof(towrite), 0x1, /* dst */ buf, bufsz, 0x0, /* src */ bufsz, error)) return FALSE; return fu_wistron_dock_device_data_write(self, cmd, sizeof(cmd), towrite, sizeof(towrite), error); } static gboolean fu_wistron_dock_device_write_wdfl_data(FuWistronDockDevice *self, const guint8 *buf, gsize bufsz, GError **error) { guint8 cmd[8] = {FU_WISTRON_DOCK_ID_IMG_CONTROL, FU_WISTRON_DOCK_CMD_DFU_WRITE_WDFL_DATA}; guint8 towrite[FU_WISTRON_DOCK_WDFL_DATA_SIZE + 1] = {FU_WISTRON_DOCK_ID_DOCK_WDFL_DATA}; if (!fu_memcpy_safe(towrite, sizeof(towrite), 0x1, /* dst */ buf, bufsz, 0x0, /* src */ bufsz, error)) return FALSE; return fu_wistron_dock_device_data_write(self, cmd, sizeof(cmd), towrite, sizeof(towrite), error); } static gboolean fu_wistron_dock_device_set_img_address(FuWistronDockDevice *self, guint32 addr, GError **error) { guint8 cmd[8] = {FU_WISTRON_DOCK_ID_IMG_CONTROL, FU_WISTRON_DOCK_CMD_DFU_ADDRESS}; fu_memwrite_uint32(cmd + 2, addr, G_BIG_ENDIAN); return fu_wistron_dock_device_control_write(self, cmd, sizeof(cmd), TRUE, error); } static gboolean fu_wistron_dock_device_write_img_data(FuWistronDockDevice *self, const guint8 *buf, gsize bufsz, GError **error) { guint8 cmd[8] = {FU_WISTRON_DOCK_ID_IMG_CONTROL, FU_WISTRON_DOCK_CMD_DFU_WRITEIMG_BLOCK}; guint8 towrite[FU_WISTRON_DOCK_TRANSFER_BLOCK_SIZE + 1] = { FU_WISTRON_DOCK_ID_DOCK_IMG_DATA}; if (!fu_memcpy_safe(towrite, sizeof(towrite), 0x1, /* dst */ buf, bufsz, 0x0, /* src */ bufsz, error)) return FALSE; return fu_wistron_dock_device_data_write(self, cmd, sizeof(cmd), towrite, sizeof(towrite), error); } static gboolean fu_wistron_dock_device_write_blocks(FuWistronDockDevice *self, FuChunkArray *chunks, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, fu_chunk_array_length(chunks)); for (guint i = 0; i < fu_chunk_array_length(chunks); i++) { g_autoptr(FuChunk) chk = NULL; /* prepare chunk */ chk = fu_chunk_array_index(chunks, i, error); if (chk == NULL) return FALSE; /* set address */ if (!fu_wistron_dock_device_set_img_address(self, fu_chunk_get_address(chk), error)) { g_prefix_error(error, "failed to set img address 0x%x", (guint)fu_chunk_get_address(chk)); return FALSE; } /* write */ if (!fu_wistron_dock_device_write_img_data(self, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk), error)) { g_prefix_error(error, "failed to write img data 0x%x", (guint)fu_chunk_get_address(chk)); return FALSE; } /* update progress */ fu_progress_step_done(progress); } /* success */ return TRUE; } static FuFirmware * fu_wistron_dock_device_prepare_firmware(FuDevice *device, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { g_autoptr(FuFirmware) firmware = fu_archive_firmware_new(); g_autoptr(FuFirmware) fw_cbin = NULL; g_autoptr(FuFirmware) fw_new = fu_firmware_new(); g_autoptr(FuFirmware) fw_wdfl = NULL; g_autoptr(FuFirmware) fw_wsig = NULL; /* unzip and get images */ if (!fu_firmware_parse_stream(firmware, stream, 0x0, flags, error)) return NULL; fw_wsig = fu_archive_firmware_get_image_fnmatch(FU_ARCHIVE_FIRMWARE(firmware), "*.wdfl.sig", error); if (fw_wsig == NULL) return NULL; fw_wdfl = fu_archive_firmware_get_image_fnmatch(FU_ARCHIVE_FIRMWARE(firmware), "*.wdfl", error); if (fw_wdfl == NULL) return NULL; fw_cbin = fu_archive_firmware_get_image_fnmatch(FU_ARCHIVE_FIRMWARE(firmware), "*.bin", error); if (fw_cbin == NULL) return NULL; /* sanity check sizes */ if (fu_firmware_get_size(fw_wsig) < FU_WISTRON_DOCK_WDFL_SIG_SIZE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "WDFL signature size invalid, got 0x%x, expected >= 0x%x", (guint)fu_firmware_get_size(fw_wsig), (guint)FU_WISTRON_DOCK_WDFL_SIG_SIZE); return NULL; } if (fu_firmware_get_size(fw_wdfl) != FU_WISTRON_DOCK_WDFL_DATA_SIZE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "WDFL size invalid, got 0x%x, expected 0x%x", (guint)fu_firmware_get_size(fw_wdfl), (guint)FU_WISTRON_DOCK_WDFL_DATA_SIZE); return NULL; } /* success */ fu_firmware_set_id(fw_wsig, FU_FIRMWARE_ID_SIGNATURE); fu_firmware_add_image(fw_new, fw_wsig); fu_firmware_set_id(fw_wdfl, FU_FIRMWARE_ID_HEADER); fu_firmware_add_image(fw_new, fw_wdfl); fu_firmware_set_id(fw_cbin, FU_FIRMWARE_ID_PAYLOAD); fu_firmware_add_image(fw_new, fw_cbin); return g_steal_pointer(&fw_new); } static gboolean fu_wistron_dock_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuWistronDockDevice *self = FU_WISTRON_DOCK_DEVICE(device); g_autoptr(GInputStream) stream_cbin = NULL; g_autoptr(GBytes) fw_wdfl = NULL; g_autoptr(GBytes) fw_wsig = NULL; g_autoptr(FuChunkArray) chunks = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "write-wdfl-signature"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 1, "write-wdfl-data"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 98, "write-payload"); /* write WDFL signature */ fw_wsig = fu_firmware_get_image_by_id_bytes(firmware, FU_FIRMWARE_ID_SIGNATURE, error); if (fw_wsig == NULL) return FALSE; if (!fu_wistron_dock_device_write_wdfl_sig(self, g_bytes_get_data(fw_wsig, NULL), g_bytes_get_size(fw_wsig), error)) { g_prefix_error(error, "failed to write WDFL signature: "); return FALSE; } fu_progress_step_done(progress); /* write WDFL data */ fw_wdfl = fu_firmware_get_image_by_id_bytes(firmware, FU_FIRMWARE_ID_HEADER, error); if (fw_wdfl == NULL) return FALSE; if (!fu_wistron_dock_device_write_wdfl_data(self, g_bytes_get_data(fw_wdfl, NULL), g_bytes_get_size(fw_wdfl), error)) { g_prefix_error(error, "failed to write WDFL data: "); return FALSE; } fu_progress_step_done(progress); /* write each block */ stream_cbin = fu_firmware_get_image_by_id_stream(firmware, FU_FIRMWARE_ID_PAYLOAD, error); if (stream_cbin == NULL) return FALSE; chunks = fu_chunk_array_new_from_stream(stream_cbin, FU_CHUNK_ADDR_OFFSET_NONE, FU_CHUNK_PAGESZ_NONE, FU_WISTRON_DOCK_TRANSFER_BLOCK_SIZE, error); if (chunks == NULL) return FALSE; if (!fu_wistron_dock_device_write_blocks(self, chunks, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to write payload: "); return FALSE; } fu_progress_step_done(progress); /* success! */ return TRUE; } static gboolean fu_wistron_dock_device_ensure_mcuid(FuWistronDockDevice *self, GError **error) { guint8 cmd[8] = {FU_WISTRON_DOCK_ID_USB_CONTROL, FU_WISTRON_DOCK_CMD_ICP_MCUID}; guint8 buf[8] = {FU_WISTRON_DOCK_ID_USB_CONTROL}; g_autofree gchar *tmp = NULL; if (!fu_wistron_dock_device_control_read(self, cmd, sizeof(cmd), buf, sizeof(buf), TRUE, error)) return FALSE; tmp = fu_memstrsafe(buf, sizeof(buf), 2, 5, error); if (tmp == NULL) return FALSE; fu_device_add_instance_str(FU_DEVICE(self), "MCUID", tmp); return fu_device_build_instance_id(FU_DEVICE(self), error, "USB", "VID", "PID", "MCUID", NULL); } static gboolean fu_wistron_dock_device_ensure_bbinfo(FuWistronDockDevice *self, GError **error) { guint8 cmd[8] = {FU_WISTRON_DOCK_ID_USB_CONTROL, FU_WISTRON_DOCK_CMD_ICP_BBINFO}; guint8 buf[8] = {FU_WISTRON_DOCK_ID_USB_CONTROL}; if (!fu_wistron_dock_device_control_read(self, cmd, sizeof(cmd), buf, sizeof(buf), TRUE, error)) return FALSE; g_free(self->icp_bbinfo); self->icp_bbinfo = g_strdup_printf("%u.%u.%u", buf[2], buf[3], buf[4]); return TRUE; } static gboolean fu_wistron_dock_device_ensure_userinfo(FuWistronDockDevice *self, GError **error) { guint8 cmd[8] = {FU_WISTRON_DOCK_ID_USB_CONTROL, FU_WISTRON_DOCK_CMD_ICP_USERINFO}; guint8 buf[8] = {FU_WISTRON_DOCK_ID_USB_CONTROL}; if (!fu_wistron_dock_device_control_read(self, cmd, sizeof(cmd), buf, sizeof(buf), TRUE, error)) return FALSE; g_free(self->icp_userinfo); self->icp_userinfo = g_strdup_printf("%u.%u.%u", buf[2], buf[3], buf[4]); return TRUE; } static gboolean fu_wistron_dock_device_parse_wdit_img(FuWistronDockDevice *self, const guint8 *buf, gsize bufsz, gsize offset, guint8 device_cnt, GError **error) { for (guint j = 0; j < device_cnt; j++) { guint32 version_raw = 0; guint8 status; g_autofree gchar *name = NULL; g_autofree gchar *version0 = NULL; g_autofree gchar *version1 = NULL; g_autofree gchar *version2 = NULL; g_autoptr(GByteArray) st = NULL; /* parse */ st = fu_struct_wistron_dock_wdit_img_parse(buf, bufsz, offset, error); if (st == NULL) return FALSE; /* versions */ version_raw = fu_struct_wistron_dock_wdit_img_get_version_build(st); if (version_raw != 0) version0 = fu_version_from_uint32(version_raw, FWUPD_VERSION_FORMAT_QUAD); version_raw = fu_struct_wistron_dock_wdit_img_get_version1(st); if (version_raw != 0) version1 = fu_version_from_uint32(version_raw, FWUPD_VERSION_FORMAT_QUAD); version_raw = fu_struct_wistron_dock_wdit_img_get_version2(st); if (version_raw != 0) version2 = fu_version_from_uint32(version_raw, FWUPD_VERSION_FORMAT_QUAD); /* name */ name = fu_struct_wistron_dock_wdit_img_get_name(st); status = fu_struct_wistron_dock_wdit_img_get_status(st); g_debug("%s: bld:%s, img1:%s, img2:%s", name, version0, version1, version2); g_debug(" - comp-id:%u, mode:%u, status:%u/%u", fu_struct_wistron_dock_wdit_img_get_comp_id(st), fu_struct_wistron_dock_wdit_img_get_mode(st), (guint)status & 0x0F, (guint)(status & 0xF0) >> 4); offset += st->len; } /* success */ return TRUE; } static gboolean fu_wistron_dock_device_ensure_wdit(FuWistronDockDevice *self, GError **error) { guint8 update_state = 0x0; guint8 buf[FU_WISTRON_DOCK_WDIT_SIZE + 1] = {FU_WISTRON_DOCK_ID_DOCK_WDIT}; g_autoptr(GByteArray) st = NULL; /* get WDIT */ if (!fu_hid_device_get_report(FU_HID_DEVICE(self), buf[0], /* value */ buf, sizeof(buf), FU_WISTRON_DOCK_TRANSFER_TIMEOUT, FU_HID_DEVICE_FLAG_IS_FEATURE | FU_HID_DEVICE_FLAG_RETRY_FAILURE | FU_HID_DEVICE_FLAG_ALLOW_TRUNC, error)) return FALSE; /* unpack */ st = fu_struct_wistron_dock_wdit_parse(buf, sizeof(buf), 0x0, error); if (st == NULL) return FALSE; if (fu_struct_wistron_dock_wdit_get_tag_id(st) != FU_WISTRON_DOCK_WDIT_TAG_ID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "WDIT tag invalid, expected 0x%x, got 0x%x", (guint)FU_WISTRON_DOCK_WDIT_TAG_ID, fu_struct_wistron_dock_wdit_get_tag_id(st)); return FALSE; } /* verify VID & PID */ if (fu_struct_wistron_dock_wdit_get_vid(st) != fu_device_get_vid(FU_DEVICE(self)) || fu_struct_wistron_dock_wdit_get_pid(st) != fu_device_get_pid(FU_DEVICE(self))) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "USB VID:PID invalid, expected %04X:%04X, got %04X:%04X", (guint)fu_device_get_vid(FU_DEVICE(self)), (guint)fu_device_get_pid(FU_DEVICE(self)), fu_struct_wistron_dock_wdit_get_vid(st), fu_struct_wistron_dock_wdit_get_pid(st)); return FALSE; } /* image mode */ self->imgmode = fu_struct_wistron_dock_wdit_get_imgmode(st); if (self->imgmode == 0) fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); else if (self->imgmode == 1) fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); /* update state */ update_state = fu_struct_wistron_dock_wdit_get_update_state(st); self->update_phase = (update_state & 0xF0) >> 4; if (self->update_phase == FU_WISTRON_DOCK_UPDATE_PHASE_DOWNLOAD) fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); else fu_device_remove_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER); if (fu_wistron_dock_update_phase_to_string(self->update_phase) == NULL) g_warning("unknown update_phase 0x%02x", self->update_phase); self->component_idx = update_state & 0xF; if (fu_wistron_dock_component_idx_to_string(self->component_idx) == NULL) g_warning("unknown component_idx 0x%02x", self->component_idx); /* status code */ self->status_code = fu_struct_wistron_dock_wdit_get_status_code(st); if (fu_wistron_dock_status_code_to_string(self->status_code) == NULL) g_warning("unknown status_code 0x%02x", self->status_code); /* composite version */ fu_device_set_version_raw(FU_DEVICE(self), fu_struct_wistron_dock_wdit_get_composite_version(st)); /* for debugging only */ if (!fu_wistron_dock_device_parse_wdit_img( self, buf, sizeof(buf), st->len + 0x1, MIN(fu_struct_wistron_dock_wdit_get_device_cnt(st), 32), error)) { g_prefix_error(error, "failed to parse imgs: "); return FALSE; } /* adding the MCU while flashing the device, ignore until it comes back in runtime mode */ if (self->update_phase == FU_WISTRON_DOCK_UPDATE_PHASE_DEPLOY && self->status_code == FU_WISTRON_DOCK_STATUS_CODE_UPDATING) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "ignoring device in MCU mode"); return FALSE; } /* success */ return TRUE; } static gboolean fu_wistron_dock_device_setup(FuDevice *device, GError **error) { FuWistronDockDevice *self = FU_WISTRON_DOCK_DEVICE(device); /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_wistron_dock_device_parent_class)->setup(device, error)) return FALSE; if (!fu_wistron_dock_device_ensure_mcuid(self, error)) { g_prefix_error(error, "failed to get MCUID: "); return FALSE; } if (!fu_wistron_dock_device_ensure_bbinfo(self, error)) { g_prefix_error(error, "failed to get BBINFO: "); return FALSE; } if (!fu_wistron_dock_device_ensure_userinfo(self, error)) { g_prefix_error(error, "failed to get USERINFO: "); return FALSE; } if (!fu_wistron_dock_device_ensure_wdit(self, error)) { g_prefix_error(error, "failed to get WDIT: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_wistron_dock_device_detach(FuDevice *device, FuProgress *progress, GError **error) { FuWistronDockDevice *self = FU_WISTRON_DOCK_DEVICE(device); guint8 cmd[8] = {FU_WISTRON_DOCK_ID_IMG_CONTROL, FU_WISTRON_DOCK_CMD_DFU_ENTER}; /* sanity check */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in bootloader mode, skipping"); return TRUE; } if (!fu_wistron_dock_device_control_write(self, cmd, sizeof(cmd), FALSE, error)) return FALSE; return fu_wistron_dock_device_ensure_wdit(self, error); } static gboolean fu_wistron_dock_device_insert_cb(gpointer user_data) { FuWistronDockDevice *self = FU_WISTRON_DOCK_DEVICE(user_data); g_autoptr(FwupdRequest) request = fwupd_request_new(); g_autoptr(GError) error_local = NULL; /* interactive request to start the SPI write */ fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_IMMEDIATE); fwupd_request_set_id(request, FWUPD_REQUEST_ID_INSERT_USB_CABLE); fwupd_request_add_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); if (!fu_device_emit_request(FU_DEVICE(self), request, NULL, &error_local)) g_warning("%s", error_local->message); /* success */ self->device_insert_id = 0; return G_SOURCE_REMOVE; } static gboolean fu_wistron_dock_device_cleanup(FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuWistronDockDevice *self = FU_WISTRON_DOCK_DEVICE(device); /* ensure the timeout has been cleared, even on error */ if (self->device_insert_id != 0) { g_source_remove(self->device_insert_id); self->device_insert_id = 0; } return TRUE; } static gboolean fu_wistron_dock_device_attach(FuDevice *device, FuProgress *progress, GError **error) { FuWistronDockDevice *self = FU_WISTRON_DOCK_DEVICE(device); guint8 cmd[8] = {FU_WISTRON_DOCK_ID_IMG_CONTROL, FU_WISTRON_DOCK_CMD_DFU_EXIT}; g_autoptr(FwupdRequest) request = fwupd_request_new(); /* sanity check */ if (!fu_wistron_dock_device_ensure_wdit(self, error)) return FALSE; if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { g_debug("already in runtime mode, skipping"); return TRUE; } if (!fu_wistron_dock_device_control_write(self, cmd, sizeof(cmd), FALSE, error)) return FALSE; fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); /* the user has to remove the USB cable, wait 15 seconds, then re-insert it */ fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_IMMEDIATE); fwupd_request_set_id(request, FWUPD_REQUEST_ID_REMOVE_USB_CABLE); fwupd_request_add_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); if (!fu_device_emit_request(device, request, progress, error)) return FALSE; /* set a timeout, which will trigger as we're waiting for the device -- * no sync sleep is possible as the device will re-enumerate one more time */ fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_BUSY); self->device_insert_id = g_timeout_add_seconds(20, fu_wistron_dock_device_insert_cb, self); /* success */ return TRUE; } static void fu_wistron_dock_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 0, "prepare-fw"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0, "detach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 20, "write"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 5, "attach"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 75, "reload"); } static gchar * fu_wistron_dock_device_convert_version(FuDevice *device, guint64 version_raw) { return fu_version_from_uint32(version_raw, fu_device_get_version_format(device)); } static void fu_wistron_dock_device_init(FuWistronDockDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "com.wistron.dock"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE); fu_device_add_request_flag(FU_DEVICE(self), FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); fu_device_set_remove_delay(FU_DEVICE(self), 5 * 60 * 1000); } static void fu_wistron_dock_device_finalize(GObject *object) { FuWistronDockDevice *self = FU_WISTRON_DOCK_DEVICE(object); if (self->device_insert_id != 0) g_source_remove(self->device_insert_id); g_free(self->icp_bbinfo); g_free(self->icp_userinfo); G_OBJECT_CLASS(fu_wistron_dock_device_parent_class)->finalize(object); } static void fu_wistron_dock_device_class_init(FuWistronDockDeviceClass *klass) { FuDeviceClass *device_class = FU_DEVICE_CLASS(klass); GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_wistron_dock_device_finalize; device_class->to_string = fu_wistron_dock_device_to_string; device_class->prepare_firmware = fu_wistron_dock_device_prepare_firmware; device_class->write_firmware = fu_wistron_dock_device_write_firmware; device_class->attach = fu_wistron_dock_device_attach; device_class->detach = fu_wistron_dock_device_detach; device_class->setup = fu_wistron_dock_device_setup; device_class->cleanup = fu_wistron_dock_device_cleanup; device_class->set_progress = fu_wistron_dock_device_set_progress; device_class->convert_version = fu_wistron_dock_device_convert_version; } fwupd-2.0.10/plugins/wistron-dock/fu-wistron-dock-device.h000066400000000000000000000005441501337203100234620ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_WISTRON_DOCK_DEVICE (fu_wistron_dock_device_get_type()) G_DECLARE_FINAL_TYPE(FuWistronDockDevice, fu_wistron_dock_device, FU, WISTRON_DOCK_DEVICE, FuHidDevice) fwupd-2.0.10/plugins/wistron-dock/fu-wistron-dock-plugin.c000066400000000000000000000014221501337203100235100ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-wistron-dock-device.h" #include "fu-wistron-dock-plugin.h" struct _FuWistronDockPlugin { FuPlugin parent_instance; }; G_DEFINE_TYPE(FuWistronDockPlugin, fu_wistron_dock_plugin, FU_TYPE_PLUGIN) static void fu_wistron_dock_plugin_init(FuWistronDockPlugin *self) { } static void fu_wistron_dock_plugin_constructed(GObject *obj) { FuPlugin *plugin = FU_PLUGIN(obj); fu_plugin_add_device_gtype(plugin, FU_TYPE_WISTRON_DOCK_DEVICE); } static void fu_wistron_dock_plugin_class_init(FuWistronDockPluginClass *klass) { FuPluginClass *plugin_class = FU_PLUGIN_CLASS(klass); plugin_class->constructed = fu_wistron_dock_plugin_constructed; } fwupd-2.0.10/plugins/wistron-dock/fu-wistron-dock-plugin.h000066400000000000000000000003751501337203100235230ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_DECLARE_FINAL_TYPE(FuWistronDockPlugin, fu_wistron_dock_plugin, FU, WISTRON_DOCK_PLUGIN, FuPlugin) fwupd-2.0.10/plugins/wistron-dock/fu-wistron-dock.rs000066400000000000000000000022151501337203100224170ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(ToString)] #[repr(u8)] enum FuWistronDockStatusCode { Enter = 0x1, Prepare = 0x2, Updating = 0x3, Complete = 0x4, // unplug cable to trigger update } #[derive(Parse)] #[repr(C, packed)] struct FuStructWistronDockWdit { hid_id: u8, tag_id: u16be, vid: u16le, pid: u16le, imgmode: u8, update_state: u8, status_code: FuWistronDockStatusCode, composite_version: u32be, device_cnt: u8, reserved: u8, } #[derive(ToString)] #[repr(u8)] enum FuWistronDockComponentIdx { Mcu = 0x0, Pd = 0x1, Audio = 0x2, Usb = 0x3, Mst = 0x4, Spi = 0xA, Dock = 0xF, } #[derive(Parse)] #[repr(C, packed)] struct FuStructWistronDockWditImg { comp_id: FuWistronDockComponentIdx, mode: u8, // 0=single, 1=dual-s, 2=dual-a status: u8, // 0=unknown, 1=valid, 2=invalid reserved: u8, version_build: u32be, version1: u32be, version2: u32be, name: [char; 32], } #[derive(ToString)] enum FuWistronDockUpdatePhase { Download = 0x1, Deploy = 0x2, } fwupd-2.0.10/plugins/wistron-dock/meson.build000066400000000000000000000007721501337203100211660ustar00rootroot00000000000000plugins += {meson.current_source_dir().split('/')[-1]: true} cargs = ['-DG_LOG_DOMAIN="FuPluginWistronDock"'] plugin_quirks += files('wistron-dock.quirk') plugin_builtins += static_library('fu_plugin_wistron_dock', rustgen.process('fu-wistron-dock.rs'), sources: [ 'fu-wistron-dock-device.c', 'fu-wistron-dock-plugin.c', ], include_directories: plugin_incdirs, link_with: plugin_libs, c_args: cargs, dependencies: plugin_deps, ) device_tests += files('tests/wistron-dock-40b7.json') fwupd-2.0.10/plugins/wistron-dock/tests/000077500000000000000000000000001501337203100201605ustar00rootroot00000000000000fwupd-2.0.10/plugins/wistron-dock/tests/wistron-dock-40b7.json000066400000000000000000000015651501337203100241570ustar00rootroot00000000000000{ "name": "Wistron Dock 40B7", "interactive": true, "steps": [ { "url": "f00838f10d6faf58ff57cbfcfca390ed63bd804e4c654f19ff89423b286dc5d2-lda_viking_r_composite.cab", "emulation-url": "08c477c340e51990fc5df84907149f513aef65ae7f828b469e0a14ac01579a34-lda_viking_r_composite.zip", "components": [ { "version": "1.0.1.4", "guids": [ "78d3f11b-6ad9-5739-a650-2f0d2955a710" ] } ] }, { "url": "43bcfb3278ad2baf918af6abc972e0de9a0de66a25b012177b00d96664819010-lda_viking_r_composite.cab", "emulation-url": "330411c3eaaba8b7e8b51108fddc614963af5b5fcc0fd328683197491584f387-lda_viking_r_composite2.zip", "components": [ { "version": "1.0.1.6", "guids": [ "78d3f11b-6ad9-5739-a650-2f0d2955a710" ] } ] } ] } fwupd-2.0.10/plugins/wistron-dock/wistron-dock.quirk000066400000000000000000000002101501337203100225070ustar00rootroot00000000000000# Wistron Travel Dock [USB\VID_0FB8&PID_0010] Plugin = wistron_dock # Wistron USB-C Dock [USB\VID_17EF&PID_30EF] Plugin = wistron_dock fwupd-2.0.10/po/000077500000000000000000000000001501337203100133305ustar00rootroot00000000000000fwupd-2.0.10/po/.gitignore000066400000000000000000000000061501337203100153140ustar00rootroot00000000000000*.pot fwupd-2.0.10/po/LINGUAS000066400000000000000000000002221501337203100143510ustar00rootroot00000000000000af ar ast ca cs da de en_GB eo es eu fi fr fur gl he hi hr hu id it ja ka kk ko ky lt nl oc pa pl pt pt_BR ro ru si sk sl sr sv tr uk zh_CN zh_TW fwupd-2.0.10/po/POTFILES.in000066400000000000000000000007751501337203100151160ustar00rootroot00000000000000data/remotes.d/lvfs.metainfo.xml data/remotes.d/lvfs-testing.metainfo.xml libfwupdplugin/fu-bios-settings.c libfwupdplugin/tests/bios-attrs/dell-xps13-9310/strings.txt policy/org.freedesktop.fwupd.policy.in plugins/tpm/fu-tpm-eventlog.c plugins/uefi-capsule/fu-uefi-capsule-plugin.c plugins/uefi-dbx/fu-dbxtool.c src/fu-console.c src/fu-debug.c src/fu-engine-helper.c src/fu-main.c src/fu-remote-list.c src/fu-security-attr-common.c src/fu-tool.c src/fu-util.c src/fu-util-bios-setting.c src/fu-util-common.c fwupd-2.0.10/po/POTFILES.skip000066400000000000000000000000761501337203100154500ustar00rootroot00000000000000data/fwupd.service.in data/org.freedesktop.fwupd.metainfo.xml fwupd-2.0.10/po/af.po000066400000000000000000000142201501337203100142550ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # F Wolff , 2019 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Afrikaans (http://app.transifex.com/freedesktop/fwupd/language/af/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: af\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "%.0f minuut oor" msgstr[1] "%.0f minute oor" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u dag" msgstr[1] "%u dae" #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u uur" msgstr[1] "%u ure" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minuut" msgstr[1] "%u minute" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u sekonde" msgstr[1] "%u sekondes" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Ouderdom" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "’n Bywerking vereis dat die stelsel herbegin om te voltooi." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Antwoord ja op alle vrae" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Kanselleer" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "Gekanselleer" #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Verander" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Kontrolesom" #. TRANSLATORS: error message msgid "Command not found" msgstr "Opdrag nie gevind nie" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "Pak tans uit…" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Beskrywing" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Toestel bygevoeg:" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Toestel verander:" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Toestel verwyder:" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "Toestelle wat suksesvol bygewerk is:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "Toestelle wat nie korrek bygewerk is nie:" #. TRANSLATORS: success msgid "Done!" msgstr "Klaar!" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "Gradeer tans %s af…" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Laai tans af…" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Geaktiveer" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Vee tans uit…" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Lêernaam" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "Lêernaamhandtekening" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Gevind" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "Ledig…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "Installeer tans op %s…" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "Minder as een minuut oor" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "Laai tans…" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "Metadata-URI" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "Goed" #. TRANSLATORS: remote filename base msgid "Password" msgstr "Wagwoord" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Prioriteit" msgid "Proceed with upload?" msgstr "Gaan voort met oplaai?" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Lees tans…" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Internetverbinding is nodig" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Herbegin nou?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Herbegin tans toestel…" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Skeduleer tans…" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Wys toestelle wat nie bygewerk kan word nie" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Opsomming" #. show the user the entire data blob msgid "Target" msgstr "Teiken" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Tipe" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Onbekend" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Werk nou by?" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "Werk tans %s by…" #. TRANSLATORS: remote filename base msgid "Username" msgstr "Gebruikernaam" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "Verifieer tans…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Weergawe" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "Wag tans…" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Skryf tans…" fwupd-2.0.10/po/ar.po000066400000000000000000000305731501337203100143020ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Omar TS , 2024-2025 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Arabic (http://app.transifex.com/freedesktop/fwupd/language/ar/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: ar\n" "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" msgid "Activate the new firmware on the device" msgstr "نشّط البرنامج الثابت الجديد على الجهاز" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "طبّق Ù…Ù„ÙØ§Øª التحديث" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "المصادقة مطلوبة للرجوع إلى إصدار أقدم من البرنامج الثابت على جهاز قابل للإزالة" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "المصادقة مطلوبة للرجوع إلى إصدار أقدم من البرنامج الثابت على هذا الجهاز" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to enable emulation data collection" msgstr "المصادقة مطلوبة لتمكين جمع بيانات المحاكاة" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to fix a host security issue" msgstr "المصادقة مطلوبة لإصلاح مشكلة أمان المضيÙ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to load hardware emulation data" msgstr "المصادقة مطلوبة لتحميل بيانات محاكاة الأجهزة" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "المصادقة مطلوبة لتعديل إعدادات البايوس" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "المصادقة مطلوبة لتعديل جهاز التحكم عن بعد المكوَّن والمستخدم لتحديثات البرامج الثابتة" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "المصادقة مطلوبة لتعديل تكوين البرنامج الخÙÙŠ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "المصادقة مطلوبة لقراءة إعدادات البايوس" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to reset daemon configuration to defaults" msgstr "المصادقة مطلوبة لإعادة تعيين تكوين البرنامج الخÙÙŠ إلى الإعدادات Ø§Ù„Ø§ÙØªØ±Ø§Ø¶ÙŠØ©" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to save hardware emulation data" msgstr "المصادقة مطلوبة Ù„Ø­ÙØ¸ بيانات محاكاة الأجهزة" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "المصادقة مطلوبة لتعيين قائمة البرامج الثابتة المعتمدة" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "المصادقة مطلوبة لتوقيع البيانات باستخدام شهادة العميل" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to stop the firmware update service" msgstr "المصادقة مطلوبة لإيقا٠خدمة تحديث البرامج الثابتة" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "المصادقة مطلوبة للتبديل إلى إصدار البرنامج الثابت الجديد" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to undo the fix for a host security issue" msgstr "المصادقة مطلوبة للتراجع عن إصلاح مشكلة أمان المضيÙ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "المصادقة مطلوبة Ù„ÙØªØ­ الجهاز" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "المصادقة مطلوبة لتحديث البرنامج الثابت على جهاز قابل للإزالة" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "المصادقة مطلوبة لتحديث البرنامج الثابت على هذا الجهاز" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "المصادقة مطلوبة لتحديث المجموع الاختباري المخزن للجهاز" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "ØªÙØ³Ù„ّم تحديثات البايوس عبر LVFS أو Windows Update" msgid "Enable" msgstr "ÙŠÙمكَÙÙ†" msgid "Enable emulation data collection" msgstr "مكّن جمع بيانات المحاكاة" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "ممكّن" msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "تÙمكّن هذه Ø§Ù„ÙˆØ¸ÙŠÙØ© على مسؤوليتك الخاصة، مما يعني أنه يتعين عليك الاتصال بالشركة المصنعة للمعدات الأصلية بخصوص أي مشاكل ناجمة عن هذه التحديثات. يجب Ùقط تقديم المشكلات المتعلقة بعملية التحديث Ù†ÙØ³Ù‡Ø§ إلى $OS_RELEASE:BUG_REPORT_URL$" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "ÙØ´Ù„ ÙÙŠ تحليل الحجج" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "ÙØ´Ù„ ÙÙŠ تحليل الملÙ" msgid "Get BIOS settings" msgstr "احصل على إعدادات البايوس" msgid "Install old version of signed system firmware" msgstr "ثبّت الإصدار القديم من البرامج الثابتة للنظام الموقَّعة" msgid "Install old version of unsigned system firmware" msgstr "ثبّت الإصدار القديم من البرامج الثابتة للنظام غير الموقَّعة" msgid "Install signed device firmware" msgstr "ثبّت البرامج الثابتة للجهاز الموقّعة" msgid "Install signed system firmware" msgstr "تثبيت البرامج الثابتة للنظام الموقَّعة" msgid "Install unsigned device firmware" msgstr "ثبّت البرامج الثابتة للجهاز غير الموقَّعة" msgid "Install unsigned system firmware" msgstr "تثبيت البرامج الثابتة للنظام غير الموقَّعة" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "جار٠تثبيت تحديث البرنامج الثابت..." msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "خدمة البرامج الثابتة لموردي Linux (البرامج الثابتة المستقرة)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "خدمة البرامج الثابتة لمورد Linux (اختبار البرامج الثابتة)" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "قائمة الإدخالات ÙÙŠ dbx" #. TRANSLATORS: command description msgid "Load device emulation data" msgstr "حمّل بيانات محاكاة الجهاز" msgid "Modify a configured remote" msgstr "عدّل جهاز التحكم عن بعد المكوَّن" msgid "Modify daemon configuration" msgstr "عدّل التكوين الخÙÙŠ" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "أظهر قيمة PCR واحدة Ùقط" msgid "Reset daemon configuration" msgstr "أعد تعيين التكوين الخÙÙŠ" #. TRANSLATORS: command description msgid "Save device emulation data" msgstr "Ø­ÙØ¸ بيانات محاكاة الجهاز" msgid "Security hardening for HSI" msgstr "التشديد الأمني ​​لـ HSI" msgid "Set one or more BIOS settings" msgstr "عيّن واحدًا أو أكثر من إعدادات البايوس" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "يضبط قائمة البرامج الثابتة المعتمدة" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "Ø³ØªÙØ·Ø¨Ù‘Ù‚ الإعدادات بعد إعادة تشغيل النظام" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "عرض معلومات التصحيح الإضاÙية" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "عرض النسخة المحسوبة من dbx" msgid "Sign data using the client certificate" msgstr "سجّل البيانات باستخدام شهادة العميل" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "سجّل البيانات باستخدام شهادة العميل" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "حدد مل٠قاعدة بيانات dbx" msgid "Stop the fwupd service" msgstr "أوق٠خدمة fwupd" #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "LVFS هي خدمة مجانية تعمل ككيان قانوني مستقل وليس لها أي اتصال بـ $OS_RELEASE:NAME$. ربما لم يتحقق موزّعك من أيّ من تحديثات البرامج الثابتة للتأكد من تواÙقها مع نظامك أو الأجهزة المتصلة. تÙÙˆÙّر ÙƒØ§ÙØ© البرامج الثابتة Ùقط من قبل الشركة المصنعة للمعدات الأصلية." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "قد يعمل هذا البرنامج بشكل صحيح Ùقط كجذر" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "يحتوي جهاز التحكم عن بعد هذا على برامج ثابتة غير محظورة، ولكنها لا تزال قيد الاختبار من قبل بائع الأجهزة. يجب عليك التأكد من أن لديك طريقة للرجوع إلى إصدار أقدم يدويًا ÙÙŠ حالة ÙØ´Ù„ تحديث البرنامج الثابت." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "ستقوم هذه الأداة بقراءة وتحليل سجل أحداث TPM من البرنامج الثابت للنظام." msgid "Unlock the device to allow access" msgstr "الغ Ù‚ÙÙ„ الجهاز للسماح بالوصول" msgid "Update the stored device verification information" msgstr "حدّث معلومات التحقق من الجهاز المخزن" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "الأداة المساعدة لسجل أحداث Fwupd TPM" fwupd-2.0.10/po/ast.po000066400000000000000000000036711501337203100144660ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # enolp , 2017 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Asturian (http://www.transifex.com/freedesktop/fwupd/language/ast/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: ast\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. TRANSLATORS: this is when a device is hotplugged msgid "Added" msgstr "Amestóse" #. TRANSLATORS: this is when a device ctrl+c's a watch msgid "Cancelled" msgstr "Encaboxóse" #. TRANSLATORS: this is when a device is hotplugged #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Camudóse" #. TRANSLATORS: this is the encryption method used when writing msgid "Cipher" msgstr "Cifráu" #. TRANSLATORS: detected a DFU device msgid "Found" msgstr "Alcontróse" #. TRANSLATORS: Appstream ID for the hardware type msgid "ID" msgstr "ID" msgid "Mode" msgstr "Mou" #. TRANSLATORS: interface name, e.g. "Flash" #. TRANSLATORS: device name, e.g. 'ColorHug2' #. TRANSLATORS: section header for the release name msgid "Name" msgstr "Nome" #. TRANSLATORS: DFU protocol version, e.g. 1.1 msgid "Protocol" msgstr "Protocolu" #. TRANSLATORS: these are areas of memory on the chip msgid "Region" msgstr "Rexón" #. TRANSLATORS: this is when a device is hotplugged msgid "Removed" msgstr "Desanicióse" #. TRANSLATORS: serial number, e.g. '00012345' msgid "Serial" msgstr "Serial" #. TRANSLATORS: device state, i.e. appIDLE msgid "State" msgstr "Estáu" #. TRANSLATORS: probably not run as root... #. TRANSLATORS: device has failed to report status #. TRANSLATORS: device status, e.g. "OK" msgid "Status" msgstr "Estáu" #. TRANSLATORS: transfer size in bytes msgid "Transfer Size" msgstr "Tamañu de tresferencia" fwupd-2.0.10/po/ca.po000066400000000000000000004013711501337203100142610ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Antoni Bella Pérez , 2017-2025 # Robert Antoni Buj i Gelonch , 2017 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Catalan (http://app.transifex.com/freedesktop/fwupd/language/ca/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: ca\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "Manca %.0f minut" msgstr[1] "Manquen %.0f minuts" #. TRANSLATORS: BMC refers to baseboard management controller which #. * is the device that updates all the other firmware on the system #, c-format msgid "%s BMC Update" msgstr "Actualització del BMC %s" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "Actualització de la bateria %s" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "Actualització del microcodi de la CPU %s" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "Actualització de la càmera %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "Actualització de la configuració %s" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "Actualització ME del consumidor %s" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "Actualització del controlador %s" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "Actualització ME corporativa de %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "Actualització del dispositiu %s" #. TRANSLATORS: Video Display refers to the laptop internal display or #. * external monitor #, c-format msgid "%s Display Update" msgstr "Actualització de la pantalla %s" #. TRANSLATORS: Dock refers to the port replicator hardware laptops are #. * cradled in, or lowered onto #, c-format msgid "%s Dock Update" msgstr "Actualització del moll %s" #. TRANSLATORS: drive refers to a storage device, e.g. SATA disk #, c-format msgid "%s Drive Update" msgstr "Actualització del controlador %s" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "Actualització del controlador incrustat %s" #. TRANSLATORS: a device that can read your fingerprint pattern #, c-format msgid "%s Fingerprint Reader Update" msgstr "Actualització del lector d'empremtes %s" #. TRANSLATORS: flash refers to solid state storage, e.g. UFS or eMMC #, c-format msgid "%s Flash Drive Update" msgstr "Actualització de la unitat flaix %s" #. TRANSLATORS: GPU refers to a Graphics Processing Unit, e.g. #. * the "video card" #, c-format msgid "%s GPU Update" msgstr "Actualització de la GPU %s" #. TRANSLATORS: a large pressure-sensitive drawing area typically used #. * by artists and digital artists #, c-format msgid "%s Graphics Tablet Update" msgstr "Actualització de la tauleta gràfica %s" #. TRANSLATORS: two miniature speakers attached to your ears #, c-format msgid "%s Headphones Update" msgstr "Actualització dels auriculars amb micròfon %s" #. TRANSLATORS: headphones with an integrated microphone #, c-format msgid "%s Headset Update" msgstr "Actualització dels auriculars %s" #. TRANSLATORS: an input device used by gamers, e.g. a joystick #, c-format msgid "%s Input Controller Update" msgstr "Actualització del controlador d'entrada %s" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "Actualització del teclat %s" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "Actualització ME de %s" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "Actualització del ratolí %s" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "Actualització del controlador de xarxa %s" #. TRANSLATORS: SSD refers to a Solid State Drive, e.g. non-rotating #. * SATA or NVMe disk #, c-format msgid "%s SSD Update" msgstr "Actualització de l'SSD %s" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "Actualització del controlador d'emmagatzematge %s" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "Actualització del sistema %s" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "Actualització del TPM %s" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "Actualització del controlador Thunderbolt %s" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "Actualització del ratolí tàctil %s" #. TRANSLATORS: Dock refers to the port replicator device connected #. * by plugging in a USB cable -- which may or may not also provide power #, c-format msgid "%s USB Dock Update" msgstr "Actualització del moll USB %s" #. TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth #. * device that stays in the USB port so the wireless peripheral works #, c-format msgid "%s USB Receiver Update" msgstr "Actualització del receptor USB %s" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "Actualització de %s" #. TRANSLATORS: warn the user before updating, %1 is a device name #, c-format msgid "%s and all connected devices may not be usable while updating." msgstr "%s i tots els dispositius connectats poden no ser usables en actualitzar." #. TRANSLATORS: %1 refers to some kind of security test, e.g. "Encrypted RAM". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s appeared: %s" msgstr "%s ha aparegut: %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "UEFI platform #. key". #. * %2 and %3 refer to results value, e.g. "Valid" and "Invalid" #, c-format msgid "%s changed: %s → %s" msgstr "%s ha canviat: %s → %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "SPI BIOS #. region". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s disappeared: %s" msgstr "%s ha desaparegut: %s" #. TRANSLATORS: the device has a reason it can't update, e.g. laptop lid #. closed #, c-format msgid "%s is not currently updatable" msgstr "%s actualment no es pot actualitzar" #. TRANSLATORS: %1 is a device name, e.g. "ThinkPad Universal #. * ThunderBolt 4 Dock" and %2 is "fwupdmgr activate" #, c-format msgid "%s is pending activation; use %s to complete the update." msgstr "%s està pendent d'activació; s'sa %s per a completar l'actualització." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "Mode de fabricació %s" #. TRANSLATORS: warn the user before #. * updating, %1 is a device name #, c-format msgid "%s must remain connected for the duration of the update to avoid damage." msgstr "%s haurà d'estar connectat durant tota l'actualització per a evitar danys." #. TRANSLATORS: warn the user before updating, %1 is a machine #. * name #, c-format msgid "%s must remain plugged into a power source for the duration of the update to avoid damage." msgstr "%s haurà de romandre connectat a una font d'alimentació durant tota l'actualització per a evitar danys." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "Superposa %s" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s versió %s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "Versió %s" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u dia" msgstr[1] "%u dies" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] " %u dispositiu té una actualització de microprogramari disponible." msgstr[1] "%u dispositius tenen una actualització de microprogramari disponible." #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device is not the best known configuration." msgid_plural "%u devices are not the best known configuration." msgstr[0] "%u dispositiu no és la millor configuració coneguda." msgstr[1] "%u dispositius no són la millor configuració coneguda." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u hora" msgstr[1] "%u hores" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minut" msgstr[1] "%u minuts" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u segon" msgstr[1] "%u segons" #. TRANSLATORS: device tests can be specific to a CPU type #, c-format msgid "%u test was skipped" msgid_plural "%u tests were skipped" msgstr[0] "S'ha omès %u prova" msgstr[1] "S'han omès %u proves" #. TRANSLATORS: first percentage is current value, 2nd percentage is the #. * lowest limit the firmware update is allowed for the update to happen #, c-format msgid "%u%% (threshold %u%%)" msgstr "%u%% (llindar %u%%)" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(obsolet)" #. TRANSLATORS: HSI event title msgid "A TPM PCR is now an invalid value" msgstr "Un PCR en el TPM ara té un valor no vàlid" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "AMD Firmware Replay Protection" msgstr "Protecció de reinjecció de microprogramari d'AMD" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "AMD Firmware Write Protection" msgstr "Protecció d'escriptura del microprogramari d'AMD" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "AMD Secure Processor Rollback Protection" msgstr "Protecció contra la reversió del processador segur d'AMD" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "ARCHIVE FIRMWARE METAINFO [FIRMWARE] [METAINFO] [JCATFILE]" msgstr "ARXIU MICROPROGRAMARI METAINFO [MICROPROGRAMARI] [METAINFO] [FITXER_JCAT]" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "Acció requerida:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "Activa els dispositius." #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "Activa els dispositius pendents." msgid "Activate the new firmware on the device" msgstr "Activa el microprogramari nou al dispositiu" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "Activació de l'actualització del microprogramari" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "Activa l'actualització del microprogramari per a" #. TRANSLATORS: command description msgid "Adds devices to watch for future emulation" msgstr "Afegeix els dispositius que es vigilaran per a una emulació futura" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Antiguitat" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "Voleu acceptar i habilitar el remot?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Àlies per a «%s»" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are now valid" msgstr "Tots els PCR en el TPM ara són vàlids" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are valid" msgstr "Tots els PCR en el TPM són vàlids" #. TRANSLATORS: an application is preventing system updates msgid "All devices are prevented from update by system inhibit" msgstr "La inhibició del sistema impedeix que s'actualitzin tots els dispositius" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "Tots els dispositius del mateix tipus s'actualitzaran al mateix temps" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Permet tornar a la versió anterior del microprogramari" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "Permetre tornar a instal·lar les versions existents del microprogramari" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "Permet canviar de branca de microprogramari" #. TRANSLATORS: error message msgid "Already exists, and no --force specified" msgstr "Ja existeix, i no --force specified" #. TRANSLATORS: is not the main firmware stream msgid "Alternate branch" msgstr "Branca alternativa" #. TRANSLATORS: another application is updating the device already msgid "An update is in progress" msgstr "Hi ha una actualització en curs" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "Una actualització requereix un reinici per a completar-se." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "Una actualització requereix que s'aturi el sistema per a finalitzar." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Respon sí a totes les preguntes" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Aplica una actualització fins i tot quan no se us aconselli." #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Aplica els fitxers d'actualització." #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "S'està aplicant l'actualització…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Microprogramari aprovat:" msgstr[1] "Microprogramari aprovat:" #. TRANSLATORS: command description msgid "Asks the daemon to quit" msgstr "Demana al dimoni sortir" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "Adjunta al mode microprogramari." #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "S'està autenticant..." #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "Es requereixen els detalls d'autenticació" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Es requereix autenticació per a desactualitzar el microprogramari en un dispositiu extraïble" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Es requereix autenticació per a desactualitzar el microprogramari en aquesta màquina" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to enable emulation data collection" msgstr "Es requereix autenticació per a permetre la recollida de les dades d'emulació" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to fix a host security issue" msgstr "Cal autenticació per a solucionar un problema de seguretat a l'amfitrió" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to load hardware emulation data" msgstr "Es requereix autenticació per a carregar les dades d'emulació en el maquinari" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "Cal autenticació per a modificar les opcions de configuració del BIOS" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "Es necessita l'autenticació per a modificar un remot configurat emprat per a les actualitzacions del microprogramari" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "Es requereix autenticació per a modificar la configuració del dimoni" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "Cal autenticació per a llegir les opcions de configuració del BIOS" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to reset daemon configuration to defaults" msgstr "És necessària l'autenticació per a restablir la configuració del dimoni als valors predeterminats" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to save hardware emulation data" msgstr "Es requereix autenticació per a desar les dades d'emulació del maquinari" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "Es requereix autenticació per a establir la llista de microprogramari aprovat" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "Es requereix autenticació per a signar les dades emprant el certificat del client" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to stop the firmware update service" msgstr "Es requereix autenticació per a aturar el servei d'actualització del microprogramari" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "Es requereix autenticació per a canviar a la nova versió del microprogramari" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to undo the fix for a host security issue" msgstr "Cal autenticació per a desfer la solució d'un problema de seguretat a l'amfitrió" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Es requereix autenticació per a desbloquejar un dispositiu" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Es requereix autenticació per a actualitzar el microprogramari en un dispositiu extraïble" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Es requereix autenticació per a actualitzar el microprogramari en aquesta màquina" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "Es requereix autenticació per a actualitzar les sumes de verificació emmagatzemades pels dispositius" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "Informa automàticament" #. TRANSLATORS: we can auto-uninhibit after a timeout #, c-format msgid "Automatically uninhibiting in %ums…" msgstr "Desinhibició automàtica a %u ms…" #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "Voleu pujar-lo automàticament cada vegada?" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS Firmware Updates" msgstr "Actualitzacions de microprogramari del BIOS" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS Rollback Protection" msgstr "Protecció contra la reversió del BIOS" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS firmware updates" msgstr "Actualitzacions de microprogramari del BIOS" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS rollback protection" msgstr "Protecció contra la reversió del BIOS" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "Actualitzacions del BIOS lliurades mitjançant LVFS o Windows Update" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "BUILDER-XML FILENAME-DST" msgstr "CONSTRUCTOR_XML NOM_FITXER_DEST" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "Bateria" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "Vincula el controlador actual." #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "Fitxers del microprogramari bloquejat:" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "Versió bloquejada" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "Microprogramari bloquejat:" #. TRANSLATORS: command description msgid "Blocks a specific firmware from being installed" msgstr "Bloqueja la instal·lació d'un microprogramari específic." #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "Versió del carregador d'arrencada" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "Branca" #. TRANSLATORS: command description msgid "Build a cabinet archive from a firmware blob and XML metadata" msgstr "Construeix un arxiu Cabinet a partir d’un blob de microprogramari i metadades en XML" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "Construeix un fitxer de microprogramari." #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * Utilized by OS means the distribution enabled it msgid "CET OS Support" msgstr "Suport CET del SO" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * enabled means supported by the processor msgid "CET Platform" msgstr "Plataforma CET" #. TRANSLATORS: longer description msgid "CPU Microcode must be updated to mitigate against various information-disclosure security issues." msgstr "El microcodi de la CPU s'ha d'actualitzar per a mitigar diversos problemes de seguretat de divulgació d'informació." #. TRANSLATORS: we can save all device enumeration events for emulation msgid "Can tag for emulation" msgstr "Es pot etiquetar per a emular" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Cancel·la" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "S'ha cancel·lat" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "No s'ha pogut aplicar perquè l'actualització de la dbx ja s'ha aplicat." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "S'ha canviat" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "Comprova que la suma criptogràfica coincideix amb el microprogramari." #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Suma de verificació" #. TRANSLATORS: get interactive prompt, where branch is the #. * supplier of the firmware, e.g. "non-free" or "free" msgid "Choose branch" msgstr "Tria una branca" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "Tria un dispositiu" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "Tria un microprogramari" #. TRANSLATORS: get interactive prompt msgid "Choose release" msgstr "Tria un llançament" #. TRANSLATORS: get interactive prompt msgid "Choose volume" msgstr "Tria un volum" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "Esborra els resultats de l'última actualització." #. TRANSLATORS: error message msgid "Command not found" msgstr "No s'ha trobat cap ordre" #. TRANSLATORS: is not supported by the vendor msgid "Community supported" msgstr "Sostinguda per la comunitat" #. TRANSLATORS: command description msgid "Compares two versions for equality" msgstr "Compara dues versions per a comprovar-ne la igualtat" #. TRANSLATORS: title prefix for the BIOS settings dialog msgid "Configuration Change Suggested" msgstr "Canvi de configuració suggerit" #. TRANSLATORS: no peeking msgid "Configuration is only readable by the system administrator" msgstr "La configuració només la pot llegir l'administrador del sistema" #. TRANSLATORS: longer description msgid "Control-Flow Enforcement Technology detects and prevents certain methods for running malicious software on the device." msgstr "La tecnologia de compliment del flux de control detecta i impedeix certs mètodes per a executar programari maliciós en el dispositiu." #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology msgid "Control-flow Enforcement Technology" msgstr "Tecnologia de compliment del flux de control" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "Converteix un fitxer de microprogramari." #. TRANSLATORS: command description msgid "Create an EFI boot entry" msgstr "Crea una entrada d'arrencada EFI" #. TRANSLATORS: when the update was built msgid "Created" msgstr "Creat" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "Crítica" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "Està disponible la verificació de la suma criptogràfica" #. TRANSLATORS: current value of a BIOS setting msgid "Current Value" msgstr "Valor actual" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "Versió actual" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "ID_DISPOSITIU|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Opcions per a la depuració" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "S'està descomprimint..." #. TRANSLATORS: command description msgid "Delete an EFI boot entry" msgstr "Suprimeix una entrada d'arrencada EFI" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Descripció" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "Separa del mode carregador d'arrencada." #. TRANSLATORS: more details about the update link msgid "Details" msgstr "Detalls" #. TRANSLATORS: the best known configuration is a set of software that we know #. works #. * well together. In the OEM and ODM industries it is often called a BKC msgid "Deviate from the best known configuration?" msgstr "Cal desviar-se de la millor configuració coneguda?" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "Etiquetes del dispositiu" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "ID del dispositiu" #. TRANSLATORS: description of the device requests msgid "Device Requests" msgstr "Sol·licituds del dispositiu" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "S'ha afegit el dispositiu:" #. TRANSLATORS: the device is already connected msgid "Device already exists" msgstr "El dispositiu ja existeix" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "L'energia de la bateria del dispositiu és massa baixa" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse #, c-format msgid "Device battery power is too low (%u%%, requires %u%%)" msgstr "L'energia de la bateria és massa baixa (%u%%, requereix %u%%)" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "El dispositiu pot recuperar fallades de flaix" #. TRANSLATORS: lid means "laptop top cover" msgid "Device cannot be updated while the lid is closed" msgstr "El dispositiu no es podrà actualitzar mentre la tapa estigui tancada" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "S'ha canviat el dispositiu:" #. TRANSLATORS: a version check is required for all firmware msgid "Device firmware is required to have a version check" msgstr "És obligatori el microprogramari del dispositiu per a comprovar la versió" #. TRANSLATORS: emulated means we are pretending to be a different model msgid "Device is emulated" msgstr "El dispositiu està emulat" #. TRANSLATORS: device cannot be interrupted, for instance taking a phone call msgid "Device is in use" msgstr "El dispositiu està en ús" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "El dispositiu està bloquejat" #. TRANSLATORS: we have two ways of communicating with the device, so we hide #. one msgid "Device is lower priority than an equivalent device" msgstr "El dispositiu té una prioritat més baixa que la d'un dispositiu equivalent" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "El dispositiu és necessari per a instal·lar totes les versions subministrades" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "El dispositiu és inabastable" #. TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode msgid "Device is unreachable, or out of wireless range" msgstr "El dispositiu no és accessible o es troba fora de l'abast" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "El dispositiu es podrà usar durant tota l'actualització" #. TRANSLATORS: usually this is when we're waiting for a reboot msgid "Device is waiting for the update to be applied" msgstr "El dispositiu està esperant que s'apliqui l'actualització" #. TRANSLATORS: success, so say thank you to the user msgid "Device list uploaded successfully, thanks!" msgstr "S'ha publicat amb èxit la llista de dispositius, gràcies!" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "S'ha eliminat el dispositiu:" #. TRANSLATORS: as in, wired mains power for a laptop msgid "Device requires AC power to be connected" msgstr "El dispositiu requereix que es connecti l'alimentació" #. TRANSLATORS: device does not have a display connected msgid "Device requires a display to be plugged in" msgstr "El dispositiu requereix que hi hagi connectada una pantalla" #. TRANSLATORS: The device cannot be updated due to missing vendor's license." msgid "Device requires a software license to update" msgstr "El dispositiu requereix una llicència de programari per a actualitzar" #. TRANSLATORS: longer description msgid "Device software updates are provided for this device." msgstr "Es proporcionen actualitzacions de programari de dispositiu per a aquest dispositiu." #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "Etapes d'actualització del dispositiu" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "El dispositiu admet el canvi a una branca diferent de microprogramari" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "Cal activar l'actualització del dispositiu" #. TRANSLATORS: save the old firmware to disk before installing the new one msgid "Device will backup firmware before installing" msgstr "El dispositiu farà una còpia de seguretat del microprogramari abans d'instal·lar" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "El dispositiu no tornarà a aparèixer un cop finalitzada l'actualització" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "Els dispositius que s'han actualitzat correctament:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "Els dispositius que no s'han actualitzat correctament:" #. TRANSLATORS: message letting the user there is an update #. * waiting, but there is a reason it cannot be deployed msgid "Devices with firmware updates that need user action: " msgstr "Dispositius amb actualitzacions de microprogramari que necessiten l'acció de l'usuari:" #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "Dispositius sense actualitzacions de microprogramari disponibles:" #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "Dispositius amb la versió més recent del microprogramari disponible:" #. TRANSLATORS: this is for the device tests msgid "Did not find any devices with matching GUIDs" msgstr "No s'ha trobat cap dispositiu amb GUID coincident" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "Inhabilitada" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "Inhabilita un remot indicat." #. TRANSLATORS: command description msgid "Disables virtual testing devices" msgstr "Inhabilita els dispositius virtuals de prova" #. TRANSLATORS: the OS the release was tested on msgid "Distribution" msgstr "Distribució" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "No comprovar si hi ha metadades antigues" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "No comprovar si hi ha un historial sense informar" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "No comprovar si la baixada des del remot ha d'estar habilitada" #. TRANSLATORS: command line option msgid "Do not check or prompt for reboot after update" msgstr "No verifiquis ni demanis reiniciar després d'actualitzar" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "No incloure el prefix de domini del registre" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "No incloure el prefix de la marca de temps" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "No realitzar les comprovacions de seguretat al dispositiu" #. TRANSLATORS: command line option msgid "Do not prompt for devices" msgstr "No demanis pels dispositius" #. TRANSLATORS: command line option msgid "Do not prompt to fix security issues" msgstr "No demanis resoldre problemes de seguretat" #. TRANSLATORS: command line option msgid "Do not search the firmware when parsing" msgstr "No cercar el microprogramari quan s'analitzi" #. TRANSLATORS: warning message shown after update has been scheduled msgid "Do not turn off your computer or remove the AC adaptor while the update is in progress." msgstr "No apagueu l'ordinador ni desendolleu l'adaptador de CA mentre estigui en curs l'actualització." #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "No escriure a la base de dades de l'historial" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "Enteneu les conseqüències de canviar la branca del microprogramari?" #. TRANSLATORS: ask the user if it's okay to convert, #. * "it" being the data contained in the EFI boot entry msgid "Do you want to convert it now?" msgstr "Voleu convertir-la ara?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "Voleu inhabilitar aquesta característica per a futures actualitzacions?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "Voleu refrescar ara aquest remot?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "Voleu pujar automàticament els informes per a futures actualitzacions?" #. TRANSLATORS: command line option msgid "Don't prompt for authentication (less details may be shown)" msgstr "No demanis autenticació (pot mostrar menys detalls)" #. TRANSLATORS: success msgid "Done!" msgstr "Fet!" #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "Voleu desactualitzar %s des de la %s a la %s?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "Desactualitza el microprogramari en un dispositiu." #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "S'està desactualitzant %s…" #. TRANSLATORS: command description msgid "Download a file" msgstr "Descarrega un fitxer" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "S'està descarregant..." #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "Aboca les dades al SMBIOS des d'un fitxer." #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "Durada" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "EMULATION-FILE [ARCHIVE-FILE]" msgstr "FITXER-DEMULACIÓ [ARXIU-FITXER]" #. TRANSLATORS: longer description msgid "Each system should have tests to ensure firmware security." msgstr "Cada sistema haurà de tenir proves per a assegurar la seguretat del microprogramari." #. TRANSLATORS: command description msgid "Emulate a device using a JSON manifest" msgstr "Emula un dispositiu mitjançant un manifest en JSON" #. TRANSLATORS: this device is not actually real msgid "Emulated" msgstr "Emulat" #. TRANSLATORS: Title: if we are emulating a different host msgid "Emulated host" msgstr "Amfitrió emulat" msgid "Enable" msgstr "Habilita" msgid "Enable emulation data collection" msgstr "Activa la recollida de les dades d'emulació" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "Voleu habilitar el remot nou?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "Voleu habilitar aquest remot?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Habilitat" #. TRANSLATORS: Plugin is active only if hardware is found msgid "Enabled if hardware matches" msgstr "Activat si hi ha cap coincidència de maquinari" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "Habilita un remot indicat." #. TRANSLATORS: command description msgid "Enables virtual testing devices" msgstr "Habilita els dispositius virtuals de prova" #. TRANSLATORS: longer description msgid "Enabling firmware updates for the BIOS allows fixing security issues." msgstr "Habilitar actualitzacions de microprogramari per al BIOS permet solucionar problemes de seguretat." msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "Si habiliteu aquesta funcionalitat, ho fareu sota el vostre propi risc, el qual significa que haureu de posar-vos en contacte amb el fabricant original de l'equip quant a qualsevol problema causat per aquestes actualitzacions. A $OS_RELEASE:BUG_REPORT_URL$, només s'han de presentar els problemes amb el procés d'actualització." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "Si habiliteu aquest remot, ho fareu sota el vostre propi risc." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Encriptada" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "RAM encriptada" msgid "Encrypted RAM makes it impossible for information that is stored in device memory to be read if the memory chip is removed and accessed." msgstr "La RAM encriptada fa impossible que es pugui llegir la informació que s'emmagatzema a la memòria del dispositiu si es treu i s'accedeix al xip de memòria." #. TRANSLATORS: the vendor is no longer supporting the device msgid "End of life" msgstr "Final de la vida" #. TRANSLATORS: The BIOS setting can only be changed to fixed values msgid "Enumeration" msgstr "Enumeració" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "Esborra tot l'historial de les actualitzacions de microprogramari." #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "S'està esborrant..." #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "Surt després d'un petit retard" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "Surt una vegada s'hagi carregat el motor" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "Exporta a XML una estructura de fitxers del microprogramari" #. TRANSLATORS: command description msgid "Export firmware history for manual upload" msgstr "Exporta l'historial del microprogramari per a la càrrega manual" #. TRANSLATORS: command description msgid "Extract a firmware blob to images" msgstr "Extreu un blob de microprogramari en imatges." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "FITXER" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "FITXER [ID_DISPOSITIU|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "NOM_FITXER" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "NOM_FITXER CERTIFICAT CLAU_PRIVADA" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "NOM_FITXER ID_DISPOSITIU" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME OFFSET DATA [FIRMWARE-TYPE]" msgstr "NOM_FITXER DESPLAÇAMENT DADES [TIPUS_MICROPROGRAMARI]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "NOM_FITXER [ID_DISPOSITIU|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "NOM_FITXER [TIPUS_MICROPROGRAMARI]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" msgstr "NOM_FITXER_ORIG NOM_FITXER_DEST [TIPUS_MICROPROGRAMARI_ORIG] [TIPUS_MICROPROGRAMARI_DEST]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" msgstr "NOM_DE_FITXER|SUMA_DE_VERIFICACIÓ_1[,SUMA_DE_VERIFICACIÓ_2][,SUMA_DE_VERIFICACIÓ_3]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "Ha fallat" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Ha fallat en aplicar l'actualització" #. TRANSLATORS: error message for Windows msgid "Failed to connect to Windows service, please ensure it's running." msgstr "Ha fallat en connectar amb el servei de Windows, assegureu-vos que s'està executant." #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "Ha fallat en connectar amb el dimoni" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "Ha fallat en carregar una dbx local" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "Ha fallat en carregar una dbx del sistema" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "Ha fallat en bloquejar" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Ha fallat en analitzar els arguments" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "Ha fallat en analitzar el fitxer" #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #, c-format msgid "Failed to parse flags for %s" msgstr "S'ha fallat en analitzar els indicadors de %s" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "Ha fallat en analitzar una dbx local" #. TRANSLATORS: a feature is something like "can show an image" msgid "Failed to set front-end features" msgstr "Ha fallat en establir les característiques del frontal" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "Ha fallat en validar el contingut ESP" #. TRANSLATORS: item is FALSE msgid "False" msgstr "Fals" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Nom del fitxer" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "Signatura del nom del fitxer" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "Nom del fitxer d'origen" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "El nom del fitxer és obligatori" #. TRANSLATORS: command line option msgid "Filter with a set of device flags using a ~ prefix to exclude, e.g. 'internal,~needs-reboot'" msgstr "Filtra amb un conjunt d'etiquetes del dispositiu amb un prefix «~» per a excloure, p. ex., «internal,~needs-reboot»" #. TRANSLATORS: command line option msgid "Filter with a set of release flags using a ~ prefix to exclude, e.g. 'trusted-release,~trusted-metadata'" msgstr "Filtreu amb un conjunt de marques de llançament utilitzant un prefix ~ per a excloure, p. ex. «trusted-release,~trusted-metadata»" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware Attestation" msgstr "Certificació del microprogramari" #. TRANSLATORS: longer description msgid "Firmware Attestation checks device software using a reference copy, to make sure that it has not been changed." msgstr "La certificació del microprogramari comprova el programari del dispositiu mitjançant una còpia de referència, per a assegurar-se que no ha estat canviat." #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "Descriptor del BIOS per al microprogramari" #. TRANSLATORS: longer description msgid "Firmware BIOS Descriptor protects device firmware memory from being tampered with." msgstr "El descriptor del BIOS per al microprogramari protegeix que la memòria de microprogramari del dispositiu es mantingui sense alterar." #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "Regió de microprogramari del BIOS" #. TRANSLATORS: longer description msgid "Firmware BIOS Region protects device firmware memory from being tampered with." msgstr "La regió de microprogramari del BIOS protegeix que la memòria de microprogramari del dispositiu no es mantingui alterada." #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "URI base del microprogramari" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "Servei de D-Bus per a l'actualització de microprogramari" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Dimoni per a l'actualització de microprogramari" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "Verificació de l'actualitzador de microprogramari" msgid "Firmware Updater Verification checks that software used for updating has not been tampered with." msgstr "La verificació de l'actualitzador de microprogramari comprova que el programari emprat per a l'actualització no ha estat alterat." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "Actualitzacions de microprogramari" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "Utilitat per al microprogramari" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "Protecció d'escriptura del microprogramari" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "Bloca la protecció d'escriptura del microprogramari" #. TRANSLATORS: longer description msgid "Firmware Write Protection protects device firmware memory from being tampered with." msgstr "La protecció d'escriptura del microprogramari protegeix que la memòria de microprogramari del dispositiu sigui manipulada." #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "Certificació del microprogramari" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "El microprogramari ja està bloquejat" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "El microprogramari ja no està bloquejat" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "Les metadades del microprogramari no s'han actualitzat durant %u dia i podria ser que no estiguin actualitzades." msgstr[1] "Les metadades del microprogramari no s'han actualitzat durant %u dies i podria ser que no estiguin actualitzades." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "Actualitzacions de microprogramari" #. TRANSLATORS: user needs to run a command, %1 is 'fwupdmgr unlock' #, c-format msgid "Firmware updates disabled; run '%s' to enable" msgstr "L'actualització del microprogramari està inhabilitada. Executeu «%s» per a habilitar-la" #. TRANSLATORS: command description msgid "Fix a specific host security attribute" msgstr "Corregeix un atribut de seguretat específic de l'amfitrió" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fix reverted successfully" msgstr "Solució revertida amb èxit" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fixed successfully" msgstr "S'ha solucionat correctament" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "Etiquetes" #. TRANSLATORS: command line option msgid "Force the action by relaxing some runtime checks" msgstr "Força l'acció relaxant algunes comprovacions del temps d'execució" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "S'ha trobat" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "S'ha detectat un encriptatge de tot el disc" #. TRANSLATORS: we might ask the user the recovery key when next booting #. Windows msgid "Full disk encryption secrets may be invalidated when updating" msgstr "Els secrets d'encriptatge de tot el disc es poden invalidar en actualitzar" #. TRANSLATORS: Title: if the part has been fused msgid "Fused Platform" msgstr "Plataforma fusionada" #. TRANSLATORS: Title: if the part has been fused msgid "Fused platform" msgstr "Plataforma fusionada" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "El GUID" msgstr[1] "Els GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "GUID|DEVICE-ID" msgstr "GUID|ID_DISPOSITIU" msgid "Get BIOS settings" msgstr "Obtén les opcions de configuració del BIOS" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "Obté totes les etiquetes admeses per «fwupd»." #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Obté tots els dispositius que admeten actualitzacions de microprogramari." #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "Obté tots els connectors habilitats registrats amb el sistema." #. TRANSLATORS: command description msgid "Get all known version formats" msgstr "Obtèn tots els formats de versió coneguts" #. TRANSLATORS: command description msgid "Get device report metadata" msgstr "Obtén les metadades de l'informe del dispositiu" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Obté la informació sobre un fitxer de microprogramari." #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "Obté els remots configurats." #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "Obté els atributs de seguretat de l'amfitrió." #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "Obtén la llista del microprogramari aprovat" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "Obté la llista del microprogramari bloquejat." #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Obté la llista d'actualitzacions per al maquinari connectat." #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "Obté els alliberaments per a un dispositiu." #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Obté els resultats de l'última actualització." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "FITXER-HWIDS" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "El maquinari està esperant a ser endollat" #. TRANSLATORS: the release urgency msgid "High" msgstr "Alta" #. TRANSLATORS: title for host security events msgid "Host Security Events" msgstr "Esdeveniments de seguretat a l'amfitrió" #. TRANSLATORS: error message for unsupported feature #, c-format msgid "Host Security ID (HSI) is not supported" msgstr "L'ID de seguretat de l'amfitrió (HSI) no està admès" #. TRANSLATORS: success, so say thank you to the user msgid "Host Security ID attributes uploaded successfully, thanks!" msgstr "Els atributs de l'ID de seguretat de l'amfitrió s'han pujat correctament, gràcies!" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "ID de seguretat de l'amfitrió:" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX" msgstr "ÃNDEX" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX KEY [VALUE]" msgstr "ÃNDEX CLAU [VALOR]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX NAME TARGET [MOUNTPOINT]" msgstr "ÃNDEX NOM OBJECTIU [PUNT_DE_MUNTATGE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX1,INDEX2" msgstr "ÃNDEX1,ÃNDEX2" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INHIBIT-ID" msgstr "ID_INHIBEIX" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "Protecció IOMMU" #. TRANSLATORS: longer description msgid "IOMMU Protection prevents connected devices from accessing unauthorized parts of system memory." msgstr "La protecció IOMMU impedeix que els dispositius connectats accedeixin a parts no autoritzades de la memòria del sistema." #. TRANSLATORS: HSI event title msgid "IOMMU device protection disabled" msgstr "Està inhabilitada la protecció IOMMU de dispositius" #. TRANSLATORS: HSI event title msgid "IOMMU device protection enabled" msgstr "Està habilitada la protecció IOMMU de dispositius" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "Està ociós..." #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "Ignora les comprovacions estrictes de SSL quan es baixin els fitxers" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "Ignora les falles de la suma de verificació del microprogramari" #. TRANSLATORS: command line option msgid "Ignore firmware hardware mismatch failures" msgstr "Ignora les falles de discrepància del maquinari del microprogramari" #. TRANSLATORS: command line option msgid "Ignore non-critical firmware requirements" msgstr "Ignora els requisits no crítics del microprogramari" #. TRANSLATORS: try to help msgid "Ignoring SSL strict checks, to do this automatically in the future export DISABLE_SSL_STRICT in your environment" msgstr "S'ignoren les comprovacions estrictes de SSL, per a fer-ho automàticament en el futur, exporteu DISABLE_SSL_STRICT en el vostre entorn" #. TRANSLATORS: show the user a generic image that can be themed msgid "Image" msgstr "Imatge" #. TRANSLATORS: show the user a random image from the internet msgid "Image (custom)" msgstr "Imatge (personalitzada)" #. TRANSLATORS: the inhibit ID is a short string like dbus-123456 #, c-format msgid "Inhibit ID is %s." msgstr "L'ID d'inhibició és %s." #. TRANSLATORS: command description msgid "Inhibit the system to prevent upgrades" msgstr "Inhibeix el sistema per a evitar actualitzacions" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "Durada de la instal·lació" #. TRANSLATORS: command description msgid "Install a firmware file in cabinet format on this hardware" msgstr "Instal·la en aquest maquinari un fitxer de microprogramari en el format Cabinet" #. TRANSLATORS: command description msgid "Install a raw firmware blob on a device" msgstr "Instal·la un blob de microprogramari RAW en un dispositiu" #. TRANSLATORS: command description msgid "Install a specific firmware file on all devices that match" msgstr "Instal·la un fitxer de microprogramari específic en tots els dispositius que coincideixin" #. TRANSLATORS: command description msgid "Install a specific firmware on a device, all possible devices will also be installed once the CAB matches" msgstr "Instal·la un microprogramari específic en un dispositiu, tots els dispositius possibles també s'instal·laran una vegada que coincideixi el CAB" msgid "Install old version of signed system firmware" msgstr "Instal·la la versió antiga del microprogramari signat per al sistema" msgid "Install old version of unsigned system firmware" msgstr "Instal·la la versió antiga del microprogramari sense signar per al sistema" msgid "Install signed device firmware" msgstr "Instal·la microprogramari signat per al dispositiu" msgid "Install signed system firmware" msgstr "Instal·la microprogramari signat per al sistema" msgid "Install unsigned device firmware" msgstr "Instal·la microprogramari sense signar per al dispositiu" msgid "Install unsigned system firmware" msgstr "Instal·la microprogramari sense signar per al sistema" #. TRANSLATORS: stay on one firmware version unless the new version is #. explicitly #. * specified msgid "Installing a specific release is explicitly required" msgstr "Es requereix explícitament la instal·lació d'una versió específica" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "S'està instal·lant l'actualització de microprogramari..." #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "S'està instal·lant a %s…" #. TRANSLATORS: if it breaks, you get to keep both pieces msgid "Installing this update may also void any device warranty." msgstr "La instal·lació d'aquesta actualització també pot anul·lar qualsevol garantia del dispositiu." #. TRANSLATORS: The BIOS setting only accepts integers in a fixed range msgid "Integer" msgstr "Sencer" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "BootGuard d'Intel" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM Protected" msgstr "Protegit amb l'ACM per al BootGuard d'Intel" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "Protegit amb l'ACM per al BootGuard d'Intel" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard Error Policy" msgstr "Política d'error del BootGuard d'Intel" msgid "Intel BootGuard Error Policy ensures the device does not continue to start if its device software has been tampered with." msgstr "La política d'error del BootGuard d'Intel assegura que el dispositiu no continuarà l'inici si s'ha alterat el seu programari de dispositiu." #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard Fuse" msgstr "Fusible del BootGuard d'Intel" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "Fusible OTP del BootGuard d'Intel" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard Verified Boot" msgstr "Arrencada verificada per BootGuard d'Intel" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Política d'error del BootGuard d'Intel" #. TRANSLATORS: longer description msgid "Intel BootGuard prevents unauthorized device software from operating when the device is started." msgstr "El BootGuard d'Intel impedeix que el programari de dispositiu sense autorització funcioni quan s'inicia el dispositiu." #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Arrencada verificada per al BootGuard d'Intel" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS Mitigation" msgstr "Mitigació GDS d'Intel" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS mitigation" msgstr "Mitigació GDS d'Intel" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Manufacturing Mode" msgstr "Mode de fabricació del motor de gestió d'Intel" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the "override" is #. enabled #. * with a jumper -- luckily it is probably not accessible to end users on #. consumer #. * boards msgid "Intel Management Engine Override" msgstr "Superposa el motor de gestió d'Intel" #. TRANSLATORS: longer description msgid "Intel Management Engine Override disables checks for device software tampering." msgstr "El fet de superposar el motor de gestió d'Intel desactivarà les comprovacions per a la manipulació de programari del dispositiu." #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Version" msgstr "Versió del motor de gestió d'Intel" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "Dispositiu intern" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "No vàlid" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "Arguments no vàlids" #. TRANSLATORS: error message msgid "Invalid arguments, expected GUID" msgstr "Arguments no vàlids, s'esperava un GUID" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX KEY [VALUE]" msgstr "Arguments no vàlids, s'esperava un ÃNDEX CLAU [VALOR]" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX NAME TARGET [MOUNTPOINT]" msgstr "Arguments no vàlids, s'esperava un ÃNDEX NOM OBJECTIU [PUNT_DE_MUNTATGE]" #. TRANSLATOR: This is the error message for #. * incorrect parameter msgid "Invalid arguments, expected an AppStream ID" msgstr "Arguments no vàlids, s'esperava un ID d'AppStream" #. TRANSLATORS: error message msgid "Invalid arguments, expected at least ARCHIVE FIRMWARE METAINFO" msgstr "Arguments no vàlids, com a mínim s'esperaven un ARXIU MICROPROGRAMARI METAINFO" #. TRANSLATORS: error message msgid "Invalid arguments, expected base-16 integer" msgstr "Arguments no vàlids, s'esperava un sencer de base-16" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "Està desactualitzada" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "Està en el mode carregador d'arrencada" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "Està actualitzada" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "Problema" msgstr[1] "Problemes" #. TRANSLATORS: HSI event title msgid "Kernel is no longer tainted" msgstr "El nucli ja no està contaminat" #. TRANSLATORS: HSI event title msgid "Kernel is tainted" msgstr "El nucli està contaminat" #. TRANSLATORS: HSI event title msgid "Kernel lockdown disabled" msgstr "Està inhabilitat el bloqueig de parts del nucli" #. TRANSLATORS: HSI event title msgid "Kernel lockdown enabled" msgstr "Està habilitat el bloqueig de parts del nucli" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "UBICACIÓ" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "Última modificació" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "Manca menys d'un minut" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "Llicència" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "Bloqueig del nucli Linux" #. TRANSLATORS: longer description msgid "Linux Kernel Lockdown mode prevents administrator (root) accounts from accessing and changing critical parts of system software." msgstr "El mode de bloqueig del nucli Linux evita que els comptes d'administrador (root) accedir i canviar a parts crítiques del programari del sistema." msgid "Linux Kernel Swap temporarily saves information to disk as you work. If the information is not protected, it could be accessed by someone if they obtained the disk." msgstr "L'intercanvi del nucli de Linux desa temporalment informació en el disc mentre treballeu. Si la informació no està protegida, algú podria accedir-hi si obté el disc." #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "Verificació del nucli Linux" msgid "Linux Kernel Verification makes sure that critical system software has not been tampered with. Using device drivers which are not provided with the system can prevent this security feature from working correctly." msgstr "La verificació del nucli de Linux s'assegura que el programari crític del sistema no ha estat alterat. L'ús de controladors de dispositiu que no es proporcionen amb el sistema pot evitar que aquesta característica de seguretat funcioni correctament." #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "Intercanvi de Linux" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Servei de microprogramari del proveïdor Linux (microprogramari estable)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Servei de microprogramari del proveïdor Linux (microprogramari en proves)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Nucli Linux" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Bloqueig del nucli Linux" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Intercanvi de Linux" #. TRANSLATORS: command description msgid "List EFI boot files" msgstr "Llista els fitxers d'arrencada EFI" #. TRANSLATORS: command description msgid "List EFI boot parameters" msgstr "Llista els paràmetres d'arrencada EFI" #. TRANSLATORS: command description msgid "List EFI variables with a specific GUID" msgstr "Llista les variables EFI amb un GUID específic" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "Llista les entrades a la dbx." #. TRANSLATORS: command description msgid "List the available firmware GTypes" msgstr "Llista els GTipus de microprogramari disponibles" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "Llista els tipus de microprogramari disponibles." #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "Llista els fitxers en l'ESP." #. TRANSLATORS: command description msgid "Load device emulation data" msgstr "Carrega les dades d'emulació del dispositiu" #. TRANSLATORS: the plugin was created from a .so object, and was not built-in msgid "Loaded from an external module" msgstr "Carregat des d'un mòdul extern" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "S'està carregant..." #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "Blocada" #. TRANSLATORS: the release urgency msgid "Low" msgstr "Baixa" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refers #. * to the private/public key used to secure loading of firmware msgid "MEI Key Manifest" msgstr "Manifest de la clau MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refer #. * to the private/public key used to secure loading of firmware msgid "MEI key manifest" msgstr "Manifest de la clau MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "Mode de fabricació MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "Superposa el MEI" msgid "MEI version" msgstr "Versió del MEI" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "Habilita manualment connectors específics" #. TRANSLATORS: longer description msgid "Manufacturing Mode is used when the device is manufactured and security features are not yet enabled." msgstr "El mode de fabricació s'empra quan el dispositiu s'està fabricant i les característiques de seguretat encara no estan habilitades." #. TRANSLATORS: Longest valid string for BIOS setting msgid "Maximum length" msgstr "Longitud màxima" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "Valor màxim" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "Mitjana" #. TRANSLATORS: ask the user to do a simple task which should be translated msgid "Message" msgstr "Missatge" #. TRANSLATORS: ask the user a question, and it will not be translated msgid "Message (custom)" msgstr "Missatge (personalitzat)" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "Signatura de les metadades" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "URI de les metadades" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "Les metadades es poden obtenir des del servei de microprogramari del proveïdor Linux." #. TRANSLATORS: error message for a user who ran fwupdmgr #. * refresh recently -- %1 is '--force' #, c-format msgid "Metadata is up to date; use %s to refresh again." msgstr "Les metadades estan actualitzades. Useu %s per a actualitzar de nou." #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "Versió mínima" #. TRANSLATORS: Shortest valid string for BIOS setting msgid "Minimum length" msgstr "Longitud mínima" #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "Valor mínim" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "Modifica un valor de la configuració del dimoni" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "Modifica un remot indicat." msgid "Modify a configured remote" msgstr "Modifica un remot configurat" msgid "Modify daemon configuration" msgstr "Modifica la configuració del dimoni" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "Monitora el dimoni per als esdeveniments." #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "Munta l'ESP." #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "Requereix un reinici després de la instal·lació" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "Cal reiniciar" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "Requereix aturar després de la instal·lació" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "Versió nova" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "No s'ha especificat cap acció!" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "No hi ha cap desactualització per a %s" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "No s'ha trobat cap ID de microprogramari" #. TRANSLATORS: nothing found msgid "No firmware found" msgstr "No s'ha trobat cap microprogramari" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "No s'ha detectat cap maquinari amb capacitat per a l'actualització del microprogramari" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "No hi ha cap llançament disponible" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "Actualment, no hi ha cap remot habilitat, de manera que no hi ha metadades disponibles." #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "No hi ha cap remot disponible" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "Dispositius no actualitzables" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "No hi ha cap actualització disponible" msgid "No updates available for remaining devices" msgstr "No hi ha cap actualització disponible per als dispositius restants" #. TRANSLATORS: error message #, c-format msgid "No volume matched %s" msgstr "No hi ha cap volum coincident %s" #. TRANSLATORS: version cannot be installed due to policy msgid "Not approved" msgstr "No aprovada" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "No s'ha trobat" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "No admesa" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "D'acord" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "Bé!" #. TRANSLATORS: the firmware old version msgid "Old version" msgstr "Versió antiga" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "Mostra només un únic valor de PCR" #. TRANSLATORS: command line option msgid "Only use peer-to-peer networking when downloading files" msgstr "Usa només la xarxa d'igual a igual per a baixar els fitxers" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "Només es permeten actualitzacions de la versió" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "Preferència sobre el camí ESP predeterminat" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Firmware" msgstr "Microprogramari des de P2P" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Metadata" msgstr "Metadades des de P2P" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "CAMÃ" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "Analitza i mostra els detalls sobre un fitxer de microprogramari." #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "S'està analitzant l'actualització de la dbx..." #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "S'està analitzant la dbx del sistema..." #. TRANSLATORS: remote filename base msgid "Password" msgstr "Contrasenya" #. TRANSLATORS: command description msgid "Patch a firmware blob at a known offset" msgstr "Apedaça un blob de microprogramari amb un desplaçament conegut" msgid "Payload" msgstr "Part útil" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "Pendent" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "Voleu efectuar l'operació?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "Depuració de la plataforma" #. TRANSLATORS: longer description msgid "Platform Debugging allows device security features to be disabled. This should only be used by hardware manufacturers." msgstr "La depuració de la plataforma permet que es desactivin les característiques de seguretat del dispositiu. Això només ho han d'emprar els fabricants de maquinari." #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "Depuració de la plataforma" #. TRANSLATORS: 'recovery key' here refers to a code, rather than a physical #. metal thing msgid "Please ensure you have the volume recovery key before continuing." msgstr "Abans de continuar, assegureu-vos que teniu la clau de recuperació del volum." #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "Introduïu un número del 0 al %u: " #. TRANSLATORS: the user isn't reading the question -- %1 is 'Y' and %2 is #. * 'N' #, c-format msgid "Please enter either %s or %s: " msgstr "Introduïu %s o %s:" #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "Manquen les dependències del connector" #. TRANSLATORS: The plugin is only for testing msgid "Plugin is only for testing" msgstr "El connector només és per a proves" #. TRANSLATORS: Possible values for a bios setting msgid "Possible Values" msgstr "Valors possibles" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA Protection" msgstr "Protecció DMA durant la prearrencada" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "Protecció DMA durant la prearrencada" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is disabled" msgstr "La protecció DMA durant la prearrencada està desactivada" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is enabled" msgstr "La protecció DMA durant la prearrencada està activada" #. TRANSLATORS: longer description msgid "Pre-boot DMA protection prevents devices from accessing system memory after being connected to the computer." msgstr "La protecció de DMA en la prearrencada impedeix que els dispositius accedeixin a la memòria del sistema després de connectar-los amb l'ordinador." #. TRANSLATORS: warning message msgid "Press unlock on the device to continue the update process." msgstr "Premeu desbloqueig en el dispositiu per a continuar amb el procés d'actualització." #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "Versió anterior" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Prioritat" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "Problemes" msgid "Proceed with upload?" msgstr "Voleu continuar amb l'enviament?" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Processor Security Checks" msgstr "Comprovacions de seguretat del processador" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "Processor rollback protection" msgstr "Protecció contra la reversió del processador" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "Propietari" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "ID_REMOT" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID KEY VALUE" msgstr "ID_REMOT CLAU VALOR" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "Només lectura" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "Llegeix un blob de microprogramari des d'un dispositiu." #. TRANSLATORS: command description msgid "Read a firmware from a device" msgstr "Llegeix un nicroprogramari des d'un dispositiu" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "Llegint des de %s…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "S'està llegint..." #. TRANSLATORS: Plugin is active and in use msgid "Ready" msgstr "Llest" #. TRANSLATORS: how often we should refresh the metadata msgid "Refresh Interval" msgstr "Interval de refresc" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "Refresca les metadades des del servidor remot." #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "Voleu reinstal·lar %s a %s?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "Torna a instal·lar el microprogramari actual en el dispositiu." #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "Torna a instal·lar el microprogramari a un dispositiu." #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "Branca de llançament" #. TRANSLATORS: release attributes msgid "Release Flags" msgstr "Etiquetes de llançament" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "ID de llançament" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "ID remot" #. TRANSLATORS: command description msgid "Removes devices to watch for future emulation" msgstr "Elimina els dispositius que es vigilaran per a una emulació futura" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "URI de l'informe" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "Informat al servidor remot" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "No s'ha trobat el sistema de fitxers «efivarfs» requerit" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "No s'ha trobat el maquinari requerit" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "Requereix un carregador d'arrencada" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Requereix connexió a Internet" msgid "Reset daemon configuration" msgstr "Restableix la configuració del dimoni" #. TRANSLATORS: sets something in the daemon configuration file msgid "Resets a daemon configuration section" msgstr "Restableix una secció de configuració del dimoni" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Voleu reiniciar ara?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "Voleu reiniciar el dimoni per a fer efectiu el canvi?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "S'està reiniciant el dispositiu..." #. TRANSLATORS: command description msgid "Retrieve BIOS settings. If no arguments are passed all settings are returned" msgstr "Es recuperen les opcions de configuració del BIOS. Si no es transmeten arguments, es retornarà tota la configuració" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "Retorna tots els ID del maquinari de la màquina." #. TRANSLATORS: ask the user to upload msgid "Review and upload report now?" msgstr "Voleu revisar i penjar ara l'informe?" #. TRANSLATORS: longer description msgid "Rollback Protection prevents device software from being downgraded to an older version that has security problems." msgstr "La protecció contra la reversió evita que el programari del dispositiu torni a una versió anterior que potser conté problemes de seguretat." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr get-upgrades` #, c-format msgid "Run `%s` for more information." msgstr "Executeu «%s» per a obtenir més informació." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr sync` #, c-format msgid "Run `%s` to complete this action." msgstr "Executeu «%s» per a completar aquesta acció." #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "Executa la rutina de neteja de la composició del connector en usar install-blob" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "Executa la rutina de preparació de la composició del connector en usar install-blob" #. TRANSLATORS: command description msgid "Run the post-reboot cleanup action" msgstr "Executa l'acció de neteja posterior al reinici" #. TRANSLATORS: tell a user how to get information #, c-format msgid "Run without '%s' to see" msgstr "Per a veure executa sense «%s»" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "El nucli executat és massa antic" #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "Sufix d'execució" #. TRANSLATORS: Software Bill of Materials link msgid "SBOM" msgstr "SBOM" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SECTION" msgstr "SECCIÓ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING VALUE" msgstr "VALOR DE CONFIGURACIÓ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING1 VALUE1 [SETTING2] [VALUE2]" msgstr "CONFIG_1 VALOR_1 [CONFIG_2] [VALOR_2]" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "SMAP" msgstr "SMAP" #. TRANSLATORS: Title: Whether firmware is locked down msgid "SMM locked down" msgstr "SMM tancat" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "Descriptor del BIOS per a l'SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "Regió del BIOS per a l'SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "Bloca l'SPI" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "SPI replay protection" msgstr "Protecció de repetició per a l'SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "Escriu l'SPI" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "SPI write protection" msgstr "Protecció d'escriptura per a l'SPI" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "SUBSISTEMA CONTROLADOR [ID_DISPOSITIU|GUID]" #. TRANSLATORS: command description msgid "Save a file that allows generation of hardware IDs" msgstr "Desa un fitxer que permet la generació dels ID de maquinari" #. TRANSLATORS: command description msgid "Save device emulation data" msgstr "Desa les dades d'emulació del dispositiu" #. TRANSLATORS: key for a offline report filename msgid "Saved report" msgstr "Informe desat" #. TRANSLATORS: Scalar increment for integer BIOS setting msgid "Scalar Increment" msgstr "Augment escalar" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Planificació..." #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "Està inhabilitada l'arrencada segura" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "Està habilitada l'arrencada segura" msgid "Security hardening for HSI" msgstr "Reforç de la seguretat per a HSI" #. TRANSLATORS: the %1 is a URL to a wiki page #, c-format msgid "See %s for more details." msgstr "Per a més detalls, vegeu %s." #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "Per a més informació, vegeu %s." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Dispositiu seleccionat" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Volum seleccionat" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "Número de sèrie" #. TRANSLATORS: Configured a BIOS setting to a value #, c-format msgid "Set BIOS setting '%s' using '%s'." msgstr "Estableix la configuració «%s» del BIOS a «%s»." #. TRANSLATORS: command description msgid "Set a BIOS setting" msgstr "Estableix una configuració del BIOS" msgid "Set one or more BIOS settings" msgstr "Estableix una o més opcions de configuració del BIOS" #. TRANSLATORS: command description msgid "Set or remove an EFI boot hive entry" msgstr "Estableix o suprimeix una entrada de rusc d'arrencada EFI" #. TRANSLATORS: command description msgid "Set the EFI boot next" msgstr "A continuació, establiu l'arrencada EFI" #. TRANSLATORS: command description msgid "Set the EFI boot order" msgstr "Estableix l'ordre d'arrencada EFI" #. TRANSLATORS: command line option msgid "Set the download retries for transient errors" msgstr "Estableix els reintents de baixada per als errors transitoris" #. TRANSLATORS: command description msgid "Sets one or more BIOS settings" msgstr "Estableix una o més opcions de configuració del BIOS" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Estableix la llista de microprogramari aprovat" #. TRANSLATORS: type of BIOS setting msgid "Setting type" msgstr "Tipus de configuració" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "La configuració s'aplicarà després de reiniciar el sistema" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "Comparteix l'historial de microprogramari amb els desenvolupadors." #. TRANSLATORS: command line option msgid "Show all results" msgstr "Mostra tots els resultats" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "Mostra les versions del client i el dimoni" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "Mostra informació detallada del dimoni per a un domini concret" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "Mostra informació de depuració per a tots els dominis" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Mostra les opcions per a la depuració" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Mostra els dispositius que no són actualitzables" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Mostra informació de depuració addicional." #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "Mostra l'historial de les actualitzacions de microprogramari." #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "Mostra la versió calculada de la dbx." #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "Voleu aturar-lo ara?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "Signa un microprogramari amb una clau nova" msgid "Sign data using the client certificate" msgstr "Signa les dades emprant el certificat del client" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "Signa les dades emprant el certificat del client" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "Signa les dades enviades amb el certificat del client" msgid "Signature" msgstr "Signatura" #. TRANSLATORS: firmware is verified on-device the payload using strong crypto msgid "Signed Payload" msgstr "Part útil signada" #. TRANSLATORS: file size of the download msgid "Size" msgstr "Mida" #. TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM msgid "Some of the platform secrets may be invalidated when updating this firmware." msgstr "Alguns dels secrets de la plataforma es poden invalidar en actualitzar aquest microprogramari." #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "Origen" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "Especifica el fitxer de base de dades dbx." msgid "Stop the fwupd service" msgstr "Atura el servei del «fwupd»" #. TRANSLATORS: The BIOS setting accepts strings msgid "String" msgstr "Cadena" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "Amb èxit" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "S'han activat correctament tots els dispositius" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "El remot s'ha inhabilitat correctament" #. TRANSLATORS: comment explaining result of command msgid "Successfully disabled test devices" msgstr "Els dispositius de prova s'han inhabilitat amb èxit" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "S'han baixat les metadades noves amb èxit:" #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "S'ha habilitat i refrescat el remot amb èxit" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "El remot s'ha habilitat correctament" #. TRANSLATORS: comment explaining result of command msgid "Successfully enabled test devices" msgstr "Els dispositius de prova s'han habilitat amb èxit" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "El microprogramari s'ha instal·lat correctament" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "S'ha modificat amb èxit el valor de la configuració" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "El remot ha estat modificat amb èxit" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "S'han refrescat manualment amb èxit les metadades" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration section" msgstr "S'ha restablert amb èxit la secció de configuració" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration values" msgstr "S'han restablert correctament els valors de la configuració" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "S'han actualitzat correctament les sumes de verificació del dispositiu" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "S'ha enviat %u informe correctament" msgstr[1] "S'han enviat %u informes correctament" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "S'han verificat correctament les sumes de verificació del dispositiu" #. TRANSLATORS: the device showed up in time #, c-format msgid "Successfully waited %.0fms for device" msgstr "S'han esperat amb èxit %.0f ms per al dispositiu" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Resum" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "Supervisor Mode Access Prevention" msgstr "Prevenció d'accés al mode supervisor" #. TRANSLATORS: longer description msgid "Supervisor Mode Access Prevention ensures critical parts of device memory are not accessed by less secure programs." msgstr "La prevenció de l'accés al mode supervisor assegura que les parts crítiques de la memòria del dispositiu no són accedides per programes menys segurs." #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "Admesa" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "CPU admesa" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "Admès en el servidor remot" #. TRANSLATORS: Title: a better sleep state msgid "Suspend To Idle" msgstr "Suspèn a inactiu" #. TRANSLATORS: Title: sleep state msgid "Suspend To RAM" msgstr "Suspèn a la RAM" #. TRANSLATORS: longer description msgid "Suspend to Idle allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Suspèn a inactiu permet que el dispositiu s'adormi ràpidament per a estalviar energia. Si bé el dispositiu ha estat suspès, es podria treure físicament la memòria i accedir a la informació." #. TRANSLATORS: longer description msgid "Suspend to RAM allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Suspèn a la RAM permet que el dispositiu s'adormi ràpidament per a estalviar energia. Si bé el dispositiu ha estat suspès, es podria treure físicament la memòria i accedir a la informació." #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "Suspensió a inactiu" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "Suspensió a la RAM" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "Voleu canviar la branca des de %s a %s?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "Canvia la branca de microprogramari en el dispositiu." #. TRANSLATORS: command description msgid "Sync firmware versions to the chosen configuration" msgstr "Sincronitza les versions del microprogramari amb la configuració triada" #. TRANSLATORS: Title: Whether firmware is locked down msgid "System Management Mode" msgstr "Mode de gestió del sistema" #. TRANSLATORS: this CLI tool is now preventing system updates msgid "System Update Inhibited" msgstr "S'ha inhibit l'actualització del sistema" #. TRANSLATORS: longer description msgid "System management mode is used by the firmware to access resident BIOS code and data." msgstr "El microprogramari empra el mode de gestió del sistema per a accedir al codi i a les dades residents al BIOS." #. TRANSLATORS: as in laptop battery power msgid "System power is too low" msgstr "L'energia del sistema és massa baixa" #. TRANSLATORS: as in laptop battery power #, c-format msgid "System power is too low (%u%%, requires %u%%)" msgstr "L'energia del sistema és massa baixa (%u%%, requereix un %u%%)" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "El sistema requereix una font d'alimentació externa" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "TEXT" #. TRANSLATORS: longer description msgid "TPM (Trusted Platform Module) is a computer chip that detects when hardware components have been tampered with." msgstr "El TPM (mòdul de plataforma de confiança) és un xip d'ordinador que detecta quan s'han manipulat els components de maquinari." #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "Reconstrucció PCR0 del TPM" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is invalid" msgstr "La reconstrucció PCR0 del TPM no és vàlida" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is now valid" msgstr "La reconstrucció PCR0 del TPM ara és vàlida" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM Platform Configuration" msgstr "Configuració de la plataforma TPM" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM Reconstruction" msgstr "Reconstrucció TPM" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM empty PCRs" msgstr "Els PCR buits en el TPM" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM versió 2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "Etiqueta" msgstr[1] "Etiquetes" #. TRANSLATORS: we're saving all USB events for emulation msgid "Tagged for emulation" msgstr "Etiquetat per a l'emulació" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "Contaminada" #. show the user the entire data blob msgid "Target" msgstr "Objectiu" #. TRANSLATORS: command description msgid "Test a device using a JSON manifest" msgstr "Prova un dispositiu emprant un manifest JSON" #. TRANSLATORS: when the release was tested msgid "Tested" msgstr "Provada" #. TRANSLATORS: the %s is a vendor name, e.g. Lenovo #, c-format msgid "Tested by %s" msgstr "Provada per %s" #. TRANSLATORS: someone we trust has tested this msgid "Tested by trusted vendor" msgstr "Provat per un venedor de confiança" #. TRANSLATORS: the boot entry was in a legacy format msgid "The EFI boot entry is not in hive format, and shim may not be new enough to read it." msgstr "L'entrada d'arrencada EFI no està en el format rusc i és possible que la correcció no sigui prou nova per a llegir-la." #. TRANSLATORS: try to treat the legacy format as a hive msgid "The EFI boot entry was not in hive format, falling back" msgstr "L'entrada d'arrencada EFI no estava en format rusc, es retrocedeix" #. TRANSLATORS: longer description msgid "The Intel Management Engine Key Manifest must be valid so that the device firmware can be trusted by the CPU." msgstr "El manifest de la clau del motor de gestió d'Intel ha de ser vàlid perquè la CPU pugui confiar en el microprogramari del dispositiu." #. TRANSLATORS: longer description msgid "The Intel Management Engine controls device components and needs to have a recent version to avoid security issues." msgstr "El motor de gestió d'Intel controla els components del dispositiu i necessita tenir una versió recent per a evitar problemes de seguretat." #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "El LVFS és un servei gratuït que funciona com una entitat legal independent i no té cap vincle amb la $OS_RELEASE:NAME$. És possible que el vostre distribuïdor no hagi verificat cap de les actualitzacions del microprogramari per a la compatibilitat amb el vostre sistema o els dispositius connectats. Tot el microprogramari només és proporcionat pel fabricant original dels equips." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Platform Configuration is used to check whether the device start process has been modified." msgstr "La configuració de la plataforma TPM (mòdul de plataforma de confiança) s'empra per a comprovar si s'ha modificat el procés d'inici del dispositiu." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Reconstruction is used to check whether the device start process has been modified." msgstr "La reconstrucció del TPM (mòdul de plataforma de confiança) s'empra per a comprovar si s'ha modificat el procés d'inici del dispositiu." #. TRANSLATORS: this is more background on a security measurement problem msgid "The TPM PCR0 differs from reconstruction." msgstr "El PCR0 del TPM difereix de la reconstrucció." #. TRANSLATORS: longer description msgid "The UEFI Platform Key is used to determine if device software comes from a trusted source." msgstr "La clau de la plataforma UEFI s'empra per a determinar si el programari del dispositiu prové d'una font de confiança." #. TRANSLATORS: HSI event title msgid "The UEFI certificate store is now up to date" msgstr "El magatzem de certificats UEFI ja està actualitzat" #. TRANSLATORS: longer description msgid "The UEFI db contains the list of valid certificates that can be used to authorize what EFI binaries are allowed to run." msgstr "La base de dades UEFI conté la llista dels certificats vàlids que es poden usar per a autoritzar quins binaris EFI es poden executar." #. TRANSLATORS: longer description msgid "The UEFI system can set up memory attributes at boot which prevent common exploits from running." msgstr "El sistema UEFI pot configurar atributs de memòria a l'arrencada que impedeixen que s’executin els explotadors habituals." #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "El dimoni ha carregat codi de tercers i ja no és admès pels desenvolupadors originals!" #. TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name #, c-format msgid "The firmware from %s is not supplied by %s, the hardware vendor." msgstr "El microprogramari des de %s no és proporcionat per %s, el proveïdor de maquinari." #. TRANSLATORS: try to help msgid "The system clock has not been set correctly and downloading files may fail." msgstr "El rellotge del sistema no s'ha configurat correctament i pot fallar la descàrrega de fitxers." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been re-inserted." msgstr "L'actualització continuarà quan s'hagi tornat a endollar el cable USB del dispositiu." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged and then re-inserted." msgstr "L'actualització continuarà quan el cable USB del dispositiu s'hagi desconnectat i es torni a inserir." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged." msgstr "L'actualització continuarà quan s'hagi desconnectat el cable USB del dispositiu." #. TRANSLATORS: warning message msgid "The update will continue when the device power cable has been removed and re-inserted." msgstr "L'actualització continuarà quan s'hagi retirat i tornat a inserir el cable d'alimentació del dispositiu." #. TRANSLATORS: naughty vendor msgid "The vendor did not supply any release notes." msgstr "El proveïdor no ha subministrat les notes de llançament." #. TRANSLATORS: now list devices with unfixed high-priority issues msgid "There are devices with issues:" msgstr "Hi ha dispositius amb problemes:" #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "No hi ha cap fitxer de microprogramari bloquejat." #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "No hi ha cap microprogramari aprovat." #. TRANSLATORS: %1 is the current device version number, and %2 is the #. command name, e.g. `fwupdmgr sync` #, c-format msgid "This device will be reverted back to %s when the %s command is performed." msgstr "Aquest dispositiu es revertirà a %s quan es realitzi l'ordre %s." #. TRANSLATORS: the vendor did not upload this msgid "This firmware is provided by LVFS community members and is not provided (or supported) by the original hardware vendor." msgstr "Aquest microprogramari és proporcionat pels membres de la comunitat LVFS i no és proporcionada (o admesa) pel proveïdor de maquinari original." #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "Aquest paquet no ha estat validat, i per això podria no funcionar adequadament." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "Aquest programa només pot funcionar correctament com a «root»" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "Aquest remot conté microprogramari que no està embargat, però encara l'ha de provar el proveïdor del maquinari. Haureu d'assegurar-vos que teniu una manera de desactualitzar manualment el microprogramari si l'actualització del microprogramari no funciona." #. TRANSLATORS: error message msgid "This system doesn't support firmware settings" msgstr "Aquest sistema no admet la configuració del microprogramari" #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "Aquest sistema té problemes d'execució HSI." #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "Aquest sistema té un nivell baix de seguretat HSI." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "Aquesta eina permet a un administrador aplicar les actualitzacions des de la dbx UEFI." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "Aquesta eina permet que un administrador consulti i controli el dimoni «fwupd», el qual li permetrà realitzar accions com instal·lar o degradar la versió de microprogramari." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "Aquesta eina permet a un administrador usar els connectors de «fwupd» sense estar instal·lats en el sistema amfitrió." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can add a kernel argument of '%s', but it will only be active after restarting the computer." msgstr "Aquesta eina pot afegir un argument del nucli de «%s», però només estarà actiu després de reiniciar l'ordinador." #. TRANSLATORS: the %1 is a BIOS setting name. #. * %2 and %3 are the values, e.g. "True" or "Windows10" #, c-format msgid "This tool can change the BIOS setting '%s' from '%s' to '%s' automatically, but it will only be active after restarting the computer." msgstr "Aquesta eina pot canviar la configuració «%s» del BIOS des de «%s» a «%s» de forma automàtica, però només estarà activa una vegada es reiniciï l'ordinador." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can change the kernel argument from '%s' to '%s', but it will only be active after restarting the computer." msgstr "Aquesta eina pot canviar l'argument del nucli des de «%s» a «%s», però només estarà actiu després de reiniciar l'ordinador." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "Aquesta eina llegirà i analitzarà el registre d'esdeveniments TPM des del microprogramari del sistema." #. TRANSLATORS: the update state of the specific device msgid "Transient failure" msgstr "Fallada transitòria" #. TRANSLATORS: item is TRUE msgid "True" msgstr "Cert" #. TRANSLATORS: We verified the metadata against the server msgid "Trusted metadata" msgstr "Metadades de confiança" #. TRANSLATORS: We verified the payload against the server msgid "Trusted payload" msgstr "Part útil de confiança" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Tipus" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI Bootservice Variables" msgstr "Variables del servei d'arrencada UEFI" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition may not be set up correctly" msgstr "És possible que la partició ESP de la UEFI no estigui configurada correctament" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "No s'ha detectat o configurat la partició ESP de la UEFI" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI Memory Protection" msgstr "Protecció de la memòria UEFI" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "Clau de la plataforma UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "Arrencada segura UEFI" #. TRANSLATORS: longer description msgid "UEFI Secure Boot prevents malicious software from being loaded when the device starts." msgstr "L'arrencada segura UEFI evita que es carregui programari maliciós quan s'inicia el dispositiu." #. TRANSLATORS: longer description msgid "UEFI boot service variables should not be readable from runtime mode." msgstr "Les variables del servei d'arrencada UEFI no s'haurien de poder llegir des del mode en temps d'execució." #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI bootservice variables" msgstr "Variables del servei d'arrencada UEFI" #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "Les actualitzacions de la càpsula UEFI no estan disponibles o habilitades a la configuració del microprogramari" #. TRANSLATORS: Title: is UEFI db up-to-date msgid "UEFI db" msgstr "Bd UEFI" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "Utilitat dbx UEFI" #. TRANSLATORS: system is not booted in UEFI mode msgid "UEFI firmware can not be updated in legacy BIOS mode" msgstr "El microprogramari UEFI no es pot actualitzar en el mode BIOS heretat" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI memory protection" msgstr "Protecció de la memòria UEFI" #. TRANSLATORS: HSI event title msgid "UEFI memory protection enabled and locked" msgstr "La protecció de la memòria UEFI està activada i bloquejada" #. TRANSLATORS: HSI event title msgid "UEFI memory protection enabled but not locked" msgstr "La protecció de la memòria UEFI està activada però no bloquejada" #. TRANSLATORS: HSI event title msgid "UEFI memory protection is now locked" msgstr "La protecció de la memòria UEFI ara està bloquejada" #. TRANSLATORS: HSI event title msgid "UEFI memory protection is now unlocked" msgstr "La protecció de la memòria UEFI ara està desbloquejada" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "Clau de la plataforma UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "Arrancada segura de la UEFI" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "No s'ha pogut connectar amb el servei" #. TRANSLATORS: error message msgid "Unable to find attribute" msgstr "No es pot trobar atribut" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "Desvincula el controlador actual." #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "Microprogramari desbloquejat:" #. TRANSLATORS: command description msgid "Unblocks a specific firmware from being installed" msgstr "Desbloqueja la instal·lació d'un microprogramari específic." #. TRANSLATORS: command description msgid "Undo the host security attribute fix" msgstr "Desfés la correcció de l'atribut de seguretat de l'amfitrió" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "No està encriptada" #. TRANSLATORS: command description msgid "Uninhibit the system to allow upgrades" msgstr "Desinhibeix el sistema per a permetre actualitzacions" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Desconegut" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Dispositiu desconegut" msgid "Unlock the device to allow access" msgstr "Desbloqueja el dispositiu per a permetre l'accés" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "Desblocada" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "Desbloqueja el dispositiu per a accedir al microprogramari." #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "Desmunta l'ESP" #. TRANSLATORS: message shown after device has been marked for emulation msgid "Unplug and replug the device to continue the update process." msgstr "Desendolleu i torneu a endollar el dispositiu per a continuar amb el procés d'actualització." #. TRANSLATORS: firmware payload is unsigned and it is possible to modify it msgid "Unsigned Payload" msgstr "Part útil sense signar" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "Versió %s no admesa del dimoni, la versió del client és %s" #. TRANSLATORS: Suffix: the HSI result msgid "Untainted" msgstr "Sense contaminar" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "Actualitzable" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Error en actualitzar" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "Imatge d'actualització" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "Missatge de l'actualització" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "Estat en actualitzar" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "Que falli en actualitzar és un problema conegut, visiteu aquest URL per a obtenir més informació:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Voleu actualitzar ara?" #. TRANSLATORS: command description msgid "Update the stored cryptographic hash with current ROM contents" msgstr "Actualitza la suma criptogràfica emmagatzemada amb el contingut actual de la ROM." msgid "Update the stored device verification information" msgstr "Actualitza la informació de verificació dels dispositius emmagatzemats" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "Actualitza les metadades emmagatzemades amb el contingut actual." #. TRANSLATORS: command description msgid "Updates all specified devices to latest firmware version, or all devices if unspecified" msgstr "Actualitza tots els dispositius especificats a la versió més recent del microprogramari o tots els dispositius sense especificar" #. TRANSLATORS: how many local devices can expect updates now #, c-format msgid "Updates have been published for %u local device" msgid_plural "Updates have been published for %u of %u local devices" msgstr[0] "S'han publicat les actualitzacions per a %u dispositiu local" msgstr[1] "S'han publicat les actualitzacions de %u per a %u dispositius locals" msgid "Updating" msgstr "S'està actualitzant" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "S'està actualitzant %s…" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "Voleu actualitzar %s des de la %s a la %s?" #. TRANSLATORS: ask the user to upload msgid "Upload data now?" msgstr "Voleu publicar ara les dades?" #. TRANSLATORS: command description msgid "Upload the list of updatable devices to a remote server" msgstr "Publica la llista dels dispositius actualitzables a un servidor remot" #. TRANSLATORS: ask the user to share, %s is something #. * like: "Linux Vendor Firmware Service" #, c-format msgid "Upload these anonymous results to the %s to help other users?" msgstr "Voleu pujar aquests resultats anònims al %s per a ajudar als altres usuaris?" #. TRANSLATORS: explain why we want to upload #, c-format msgid "Uploading a device list allows the %s team to know what hardware exists, and allows us to put pressure on vendors that do not upload firmware updates for their hardware." msgstr "Carregar una llista de dispositius permet a l'equip de %s saber quin maquinari hi ha, i ens permet pressionar els proveïdors que no publiquen actualitzacions de microprogramari per al seu maquinari." #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "L'enviament dels informes de microprogramari ajudarà als proveïdors de maquinari a identificar amb rapidesa les actualitzacions fallides i satisfactòries sobre els dispositius reals." #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "Urgència" #. TRANSLATORS: explain how to get help, %1 is #. * 'fwupdtool --help' #. TRANSLATORS: explain how to get help, #. * where $1 is something like 'fwupdmgr --help' #, c-format msgid "Use %s for help" msgstr "Useu %s per a obtenir ajuda" #. TRANSLATORS: CTRL^C [holding control, and then pressing C] will exit the #. program msgid "Use CTRL^C to cancel." msgstr "Useu CTRL^C per a cancel·lar." #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "S'ha notificat a l'usuari" #. TRANSLATORS: remote filename base msgid "Username" msgstr "Nom d'usuari" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "VERSION1 VERSION2 [FORMAT]" msgstr "VERSIÓ_1 VERSIÓ_2 [FORMAT]" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "Vàlid" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "S'està validant el contingut ESP..." #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "Variant" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "Venedor" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "S'està verificant..." #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Versió" #. TRANSLATORS: the fwupd version the release was tested on msgid "Version[fwupd]" msgstr "Versió[fwupd]" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "AVÃS" #. TRANSLATORS: command description msgid "Wait for a device to appear" msgstr "Espereu fins que aparegui un dispositiu" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "S'està esperant…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "Mira per a canvis al maquinari." #. TRANSLATORS: check various UEFI and ACPI tables are unchanged after the #. update msgid "Will measure elements of system integrity around an update" msgstr "Mesurarà els elements d'integritat del sistema al voltant d'una actualització" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "Fitxer d'escriptura:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "S'està escrivint..." #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from a recovery or installation disk, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "Hauríeu d'assegurar-vos que us sentiu còmode restaurant la configuració des d'un disc de recuperació o instal·lació, ja que aquest canvi pot provocar que el sistema no arrenqui a Linux o provocar una altra inestabilitat del sistema." #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from the system firmware setup, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "Heu d'assegurar-vos que esteu còmode restablint la configuració des de la configuració del microprogramari del sistema, ja que aquest canvi pot fer que el sistema no arrenqui a Linux o que causi alguna altra inestabilitat en el sistema." #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "És possible que el vostre distribuïdor no hagi verificat cap de les actualitzacions del microprogramari per a la compatibilitat amb el vostre sistema o els dispositius connectats." #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "El vostre maquinari pot malmetre's amb aquest microprogramari, i la instal·lació d'aquesta versió pot anul·lar qualsevol garantia amb %s." #. TRANSLATORS: BKC is the industry name for the best known configuration and #. is a set #. * of firmware that works together #, c-format msgid "Your system is set up to the BKC of %s." msgstr "El vostre sistema està establert a la BKC de %s." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[APPSTREAM_ID]" msgstr "[ID_APPSTREAM]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[SUMA_DE_VERIFICACIÓ]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[ID_DISPOSITIU|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[ID_DISPOSITIU|GUID] [BRANCA]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [VERSION]" msgstr "[ID_DISPOSITIU|GUID] [VERSIÓ]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE]" msgstr "[DISPOSITIU]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILE FILE_SIG REMOTE-ID]" msgstr "[FITXER SIGNATURA_FITXER ID_REMOT]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[NOM_FITXER_1] [NOM_FITXER_2]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FWUPD-VERSION]" msgstr "[VERSIÓ-FWUPD]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[REASON] [TIMEOUT]" msgstr "[RAÓ] [TEMPS_D'ESPERA]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SECTION] KEY VALUE" msgstr "[SECCIÓ] CLAU VALOR" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2] [--no-authenticate]" msgstr "[CONFIG_1] [CONFIG_2] [--no-authenticate]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2]..." msgstr "[CONFIGURACIÓ_1] [CONFIGURACIÓ_2]..." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[FITXER_SMBIOS|FITXER_HWIDS]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "predeterminat" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "Utilitat per al registre d'esdeveniments TPM del fwupd" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "Connectors del «fwupd»" fwupd-2.0.10/po/cs.po000066400000000000000000004010161501337203100142770ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Ascii Wolf , 2017,2019-2020,2022 # Ascii Wolf , 2017 # Marek ÄŒernocký , 2016,2018 # Pavel Borecki , 2020-2023,2025 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Czech (http://app.transifex.com/freedesktop/fwupd/language/cs/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: cs\n" "Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "Zbývá %.0f minuta" msgstr[1] "Zbývají %.0f minuty" msgstr[2] "Zbývá %.0f minut" msgstr[3] "Zbývají %.0f minuty" #. TRANSLATORS: BMC refers to baseboard management controller which #. * is the device that updates all the other firmware on the system #, c-format msgid "%s BMC Update" msgstr "Aktualizace zařízení pro vestavÄ›nou zprávu (BMC) %s" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "Aktualizace pro akumulátor %s" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "Aktualizace mikrokódu pro procesor %s" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "Aktualizace pro kameru %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "Aktualizace nastavení pro %s" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "Aktualizace spotÅ™ebitelského ME v %s" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "Aktualizace pro Å™adiÄ %s" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "Aktualizace podnikového ME v %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "Aktualizace pro zařízení %s" #. TRANSLATORS: Video Display refers to the laptop internal display or #. * external monitor #, c-format msgid "%s Display Update" msgstr "Aktualizace pro displej %s" #. TRANSLATORS: Dock refers to the port replicator hardware laptops are #. * cradled in, or lowered onto #, c-format msgid "%s Dock Update" msgstr "Aktualizace pro dok %s" #. TRANSLATORS: drive refers to a storage device, e.g. SATA disk #, c-format msgid "%s Drive Update" msgstr "%s aktualizace pro disk" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "Aktualizace vestavÄ›ného Å™adiÄe (EC) v %s" #. TRANSLATORS: a device that can read your fingerprint pattern #, c-format msgid "%s Fingerprint Reader Update" msgstr "Aktualizace pro ÄteÄku otisků prstů %s" #. TRANSLATORS: flash refers to solid state storage, e.g. UFS or eMMC #, c-format msgid "%s Flash Drive Update" msgstr "%s aktualizace pro flash disk" #. TRANSLATORS: GPU refers to a Graphics Processing Unit, e.g. #. * the "video card" #, c-format msgid "%s GPU Update" msgstr "%s aktualizace pro GPU" #. TRANSLATORS: a large pressure-sensitive drawing area typically used #. * by artists and digital artists #, c-format msgid "%s Graphics Tablet Update" msgstr "Aktualizace pro grafický tablet %s" #. TRANSLATORS: two miniature speakers attached to your ears #, c-format msgid "%s Headphones Update" msgstr "Aktualizace pro sluchátka %s" #. TRANSLATORS: headphones with an integrated microphone #, c-format msgid "%s Headset Update" msgstr "Aktualizace pro náhlavní soupravu %s" #. TRANSLATORS: an input device used by gamers, e.g. a joystick #, c-format msgid "%s Input Controller Update" msgstr "Aktualizace pro herní ovladaÄ %s" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "Aktualizace pro klávesnici %s" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "Aktualizace ME v %s" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "Aktualizace pro myÅ¡ %s" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "Aktualizace pro síťovou kartu %s" #. TRANSLATORS: SSD refers to a Solid State Drive, e.g. non-rotating #. * SATA or NVMe disk #, c-format msgid "%s SSD Update" msgstr "%s aktualizace pro SSD" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "Aktualizace pro %s Å™adiÄ ÃºložiÅ¡tÄ›" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "Celková aktualizace pro %s" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "Aktualizace pro TPM %s" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "Aktualizace pro Å™adiÄ Thunderbolt v %s" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "Aktualizace pro touchpad %s" #. TRANSLATORS: Dock refers to the port replicator device connected #. * by plugging in a USB cable -- which may or may not also provide power #, c-format msgid "%s USB Dock Update" msgstr "Aktualizace pro USB dok %s" #. TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth #. * device that stays in the USB port so the wireless peripheral works #, c-format msgid "%s USB Receiver Update" msgstr "Aktualizace pro USB pÅ™ijímaÄ %s" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "Aktualizace pro %s" #. TRANSLATORS: warn the user before updating, %1 is a device name #, c-format msgid "%s and all connected devices may not be usable while updating." msgstr "%s a k nÄ›mu pÅ™ipojená zařízení nemusí být v průbÄ›hu aktualizace použitelná." #. TRANSLATORS: %1 refers to some kind of security test, e.g. "Encrypted RAM". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s appeared: %s" msgstr "%s se objevilo: %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "UEFI platform #. key". #. * %2 and %3 refer to results value, e.g. "Valid" and "Invalid" #, c-format msgid "%s changed: %s → %s" msgstr "%s se zmÄ›nilo: %s → %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "SPI BIOS #. region". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s disappeared: %s" msgstr "%s zmizelo: %s" #. TRANSLATORS: the device has a reason it can't update, e.g. laptop lid #. closed #, c-format msgid "%s is not currently updatable" msgstr "%s v tuto chvíli není možné aktualizovat" #. TRANSLATORS: %1 is a device name, e.g. "ThinkPad Universal #. * ThunderBolt 4 Dock" and %2 is "fwupdmgr activate" #, c-format msgid "%s is pending activation; use %s to complete the update." msgstr "%s Äeká na aktivaci; dokonÄete aktualizaci spuÅ¡tÄ›ním %s." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "režim pro výrobce v %s" #. TRANSLATORS: warn the user before #. * updating, %1 is a device name #, c-format msgid "%s must remain connected for the duration of the update to avoid damage." msgstr "Je tÅ™eba, aby %s zůstalo v průbÄ›hu aktualizace pÅ™ipojené, jinak hrozí poÅ¡kození." #. TRANSLATORS: warn the user before updating, %1 is a machine #. * name #, c-format msgid "%s must remain plugged into a power source for the duration of the update to avoid damage." msgstr "Je tÅ™eba, aby %s bylo po dobu aktualizace pÅ™ipojené ke zdroji napájení z elektrické sítÄ›, jinak hrozí poÅ¡kození." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "hw pÅ™epnutí %s" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s verze %s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "Verze %s" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u den" msgstr[1] "%u dny" msgstr[2] "%u dnů" msgstr[3] "%u dny" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "Je k dispozici novÄ›jší firmware pro %u zařízení." msgstr[1] "Jsou k dispozici novÄ›jší firmware pro %u zařízení." msgstr[2] "Jsou k dispozici novÄ›jší firmware pro %u zařízení." msgstr[3] "Jsou k dispozici novÄ›jší firmware pro %u zařízení." #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device is not the best known configuration." msgid_plural "%u devices are not the best known configuration." msgstr[0] "Firmware na %u zařízení není na verzi, která by byla osvÄ›dÄená v kombinaci s verzemi firmwarů ostatních zařízení." msgstr[1] "Firmware na %u zařízeních není na verzi, která by byla osvÄ›dÄená v kombinaci s verzemi firmwarů ostatních zařízení." msgstr[2] "Firmware na %u zařízeních není na verzi, která by byla osvÄ›dÄená v kombinaci s verzemi firmwarů ostatních zařízení." msgstr[3] "Firmware na %u zařízeních není na verzi, která by byla osvÄ›dÄená v kombinaci s verzemi firmwarů ostatních zařízení." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u hodina" msgstr[1] "%u hodiny" msgstr[2] "%u hodin" msgstr[3] "%u hodiny" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minuta" msgstr[1] "%u minuty" msgstr[2] "%u minut" msgstr[3] "%u minuty" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u sekunda" msgstr[1] "%u sekundy" msgstr[2] "%u sekund" msgstr[3] "%u sekundy" #. TRANSLATORS: device tests can be specific to a CPU type #, c-format msgid "%u test was skipped" msgid_plural "%u tests were skipped" msgstr[0] "%u test byl pÅ™eskoÄen" msgstr[1] "%u testy byly pÅ™eskoÄeny" msgstr[2] "%u testů bylo pÅ™eskoÄeno" msgstr[3] "%u testy byly pÅ™eskoÄeny" #. TRANSLATORS: first percentage is current value, 2nd percentage is the #. * lowest limit the firmware update is allowed for the update to happen #, c-format msgid "%u%% (threshold %u%%)" msgstr "%u%% (dolní hranice %u%%)" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(zastaralé)" #. TRANSLATORS: HSI event title msgid "A TPM PCR is now an invalid value" msgstr "PCR registr v TPM nyní má neplatnou hodnotu" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "AMD Firmware Replay Protection" msgstr "AMD – zamezení znovupoužití dříve použitého" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "AMD Firmware Write Protection" msgstr "AMD – zamezení zápisu firmware" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "AMD Secure Processor Rollback Protection" msgstr "AMD zamezení instalace starší verze na procesoru" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "ARCHIVE FIRMWARE METAINFO [FIRMWARE] [METAINFO] [JCATFILE]" msgstr "ARCHIV FIRMWARE METAINFO [FIRMWARE] [METAINFO] [JCATSOUBOR]" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "Vyžadovaná akce:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "Aktivovat zařízení" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "Aktivovat Äekající zařízení" msgid "Activate the new firmware on the device" msgstr "Aktivovat nový firmware na zařízení" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "Aktivace aktualizovaného firmwaru" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "Aktivuje se aktualizace firmware pro" #. TRANSLATORS: command description msgid "Adds devices to watch for future emulation" msgstr "PÅ™idá zařízení pro hlídání budoucí emulace" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Stáří" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "Odsouhlasit a povolit vzdálený zdroj?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Alternativa (alias) pro %s" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are now valid" msgstr "VÅ¡echny TPM PCR registry jsou nyní platné" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are valid" msgstr "VÅ¡echny PCR registry jsou platné" #. TRANSLATORS: an application is preventing system updates msgid "All devices are prevented from update by system inhibit" msgstr "Aktualizaci veÅ¡kerých zařízení je bránÄ›no systémovým inhibitorem" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "VeÅ¡kerá zařízení stejného typu budou aktualizována naráz" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Povolit návrat ke starší verzi firmwaru" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "Povolit pÅ™einstalaci stávající verze firmwaru" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "Umožnit pÅ™epnutí varianty firmware" #. TRANSLATORS: error message msgid "Already exists, and no --force specified" msgstr "Už existuje a není zadáno --force" #. TRANSLATORS: is not the main firmware stream msgid "Alternate branch" msgstr "Alternativní vÄ›tev" #. TRANSLATORS: another application is updating the device already msgid "An update is in progress" msgstr "Probíhá aktualizace" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "NÄ›která z aktualizací vyžaduje pro dokonÄení restart poÄítaÄe." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "NÄ›která z aktualizací vyžaduje pro dokonÄení vypnutí poÄítaÄe." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Na vÅ¡echny dotazy odpovÄ›dÄ›t ano" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Provést aktualizaci i když to není doporuÄeno" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Použít soubory s aktualizací" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "ProvádÄ›ní aktualizace…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Schválený firmware:" msgstr[1] "Schválené firmwary:" msgstr[2] "Schválených firmwarů:" msgstr[3] "Schválené firmwary:" #. TRANSLATORS: command description msgid "Asks the daemon to quit" msgstr "Požádá proces služby aby se ukonÄila" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "Napojit do režimu firmwaru" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Ověřování se…" #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "Je zapotÅ™ebí podrobností k ověření se" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "K návratu na starší verzi firmwaru na výmÄ›nném zařízení je vyžadováno ověření se" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "K návratu na starší verzi firmwaru na tomto poÄítaÄi je vyžadováno ověření se" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to enable emulation data collection" msgstr "Pro zapnutí shromažÄování dat emulace je vyžadováno ověření" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to fix a host security issue" msgstr "Pro opravu problému se zabezpeÄením stroje je tÅ™eba se ověřit" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to load hardware emulation data" msgstr "Pro naÄtení dat emulace hardware je vyžadováno ověření" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "Pro zmÄ›nu BIOS nastavení je tÅ™eba se ověřit" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "Ke zmÄ›nÄ› nastaveného vzdáleného zdroje, který se používá pro aktualizace firmwarů, je vyžadováno ověření se" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "Ke zmÄ›nÄ› nastavení procesu služby je vyžadováno ověření se" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "Pro naÄtení nastavení BIOS je tÅ™eba se ověřit" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to reset daemon configuration to defaults" msgstr "K vrácení nastavení procesu služby do výchozího stavu je vyžadováno ověření se" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to save hardware emulation data" msgstr "Pro uložení dat emulace hardware je vyžadováno ověření" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "Pro nastavení seznamu schválených firmwarů je vyžadováno ověření se" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "Pro podepsání dat klientským certifikátem je vyžadováno ověření se" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to stop the firmware update service" msgstr "K zastavení služby aktualizací firmwarů je vyžadováno ověření" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "Pro pÅ™epnutí na novou verzi firmwaru je vyžadováno ověření se" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to undo the fix for a host security issue" msgstr "Pro vrácení do stavu pÅ™ed opravou problému se zabezpeÄením stroje je tÅ™eba se ověřit" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Pro odemknutí zařízení je požadováno ověření se" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "K aktualizaci firmwaru na výmÄ›nném zařízení je vyžadováno ověření se" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "K aktualizaci firmwaru na tomto poÄítaÄi je vyžadováno ověření se" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "K aktualizaci uložených kontrolních souÄtů pro zařízení je vyžadováno ověření se" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "Automatické odesílání hlášení" #. TRANSLATORS: we can auto-uninhibit after a timeout #, c-format msgid "Automatically uninhibiting in %ums…" msgstr "Automaticky zruÅ¡it inhibování za %ums…" #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "Pokaždé automaticky odeslat?" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS Firmware Updates" msgstr "Aktualizace BIOS firmwaru" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS Rollback Protection" msgstr "Zamezení instalace starší verze BIOS" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS firmware updates" msgstr "Aktualizace BIOS firmwaru" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS rollback protection" msgstr "Zamezení instalace starší verze BIOS" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "Aktualizace BIOS doruÄené prostÅ™ednictvím LVFS nebo Windows Update" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "BUILDER-XML FILENAME-DST" msgstr "XML-PRO-SESTAVENà CÃLOVÃ-SOUBOR" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "Akumulátor/baterie" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "Napojit nový ovladaÄ jádra" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "Blokované soubory s firmware:" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "Blokovaná verze" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "Blokuje se firmware:" #. TRANSLATORS: command description msgid "Blocks a specific firmware from being installed" msgstr "Zablokovat instalaci konkrétního firmwaru" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "Verze zavadÄ›Äe firmware" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "VÄ›tev" #. TRANSLATORS: command description msgid "Build a cabinet archive from a firmware blob and XML metadata" msgstr "Sestavit cab archiv z binárního souboru s firmwarem a XML metadat" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "Sestavit soubor s firmware" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * Utilized by OS means the distribution enabled it msgid "CET OS Support" msgstr "Podpora CET v OS" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * enabled means supported by the processor msgid "CET Platform" msgstr "CET platforma" #. TRANSLATORS: longer description msgid "CPU Microcode must be updated to mitigate against various information-disclosure security issues." msgstr "Aby bylo možné zmírnit dopad různých problémů s úniky různých informací z procesoru, je tÅ™eba zaktualizovat jeho mikrokód" #. TRANSLATORS: we can save all device enumeration events for emulation msgid "Can tag for emulation" msgstr "Může oznaÄit pro emulaci" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Storno" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "ZruÅ¡eno" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "Tuto aktualizaci dbx už není možné použít, protože už je používána." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "ZmÄ›nÄ›no" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "Zkontrolovat zda se kryptografický otisk shoduje s firmware" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Kontrolní souÄet" #. TRANSLATORS: get interactive prompt, where branch is the #. * supplier of the firmware, e.g. "non-free" or "free" msgid "Choose branch" msgstr "Vyberte variantu" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "Vyberte zařízení" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "Vyberte firmware" #. TRANSLATORS: get interactive prompt msgid "Choose release" msgstr "Vyberte vydán" #. TRANSLATORS: get interactive prompt msgid "Choose volume" msgstr "Vyberte svazek" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "Smazat výsledky z poslední aktualizace" #. TRANSLATORS: error message msgid "Command not found" msgstr "Příkaz nenalezen" #. TRANSLATORS: is not supported by the vendor msgid "Community supported" msgstr "Podporováno komunitou" #. TRANSLATORS: command description msgid "Compares two versions for equality" msgstr "Porovná dvÄ› verze ohlednÄ› shodnosti" #. TRANSLATORS: title prefix for the BIOS settings dialog msgid "Configuration Change Suggested" msgstr "DoporuÄena zmÄ›na nastavení" #. TRANSLATORS: no peeking msgid "Configuration is only readable by the system administrator" msgstr "Nastavení je Äitelné pouze pro správce systému" #. TRANSLATORS: longer description msgid "Control-Flow Enforcement Technology detects and prevents certain methods for running malicious software on the device." msgstr "Technologie Control-Flow Enforcement (vynucování řízení toku) zjišťuje a brání urÄitým metodám spouÅ¡tÄ›ní Å¡kodlivého software na zařízení." #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology msgid "Control-flow Enforcement Technology" msgstr "Technologie vynucování řízení toku" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "PÅ™evést soubor s firmware" #. TRANSLATORS: command description msgid "Create an EFI boot entry" msgstr "VytvoÅ™it položku EFI zavádÄ›ní" #. TRANSLATORS: when the update was built msgid "Created" msgstr "VytvoÅ™eno" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "Kritická" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "Je k dispozici kryptografické ověření otisku dat" #. TRANSLATORS: current value of a BIOS setting msgid "Current Value" msgstr "Stávající hodnota" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "Stávající verze" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "IDENTIF-ZAŘÃZENÃ|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "PÅ™edvolby ladÄ›ní" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "Rozbalování…" #. TRANSLATORS: command description msgid "Delete an EFI boot entry" msgstr "Smazat položku EFI zavádÄ›ní" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Popis" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "Odpojit do režimu zavadÄ›Äe" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "Podrobnosti" #. TRANSLATORS: the best known configuration is a set of software that we know #. works #. * well together. In the OEM and ODM industries it is often called a BKC msgid "Deviate from the best known configuration?" msgstr "Odchýlit se od osvÄ›dÄené kombinace verzí firmwarů vÅ¡ech zařízení?" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "Příznaky zařízení" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "Identifikátor zařízení" #. TRANSLATORS: description of the device requests msgid "Device Requests" msgstr "Požadavky ze zařízení" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "PÅ™idáno zařízení:" #. TRANSLATORS: the device is already connected msgid "Device already exists" msgstr "Zařízení už existuje" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "Akumulátor/baterie v zařízení je příliÅ¡ vybitý" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse #, c-format msgid "Device battery power is too low (%u%%, requires %u%%)" msgstr "Akumulátor/baterie v zařízení je příliÅ¡ vybitý (%u%%, vyžaduje %u%%)" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "Zařízení se dovede vzpamatovat z nezdaru pÅ™i zápisu firmware" #. TRANSLATORS: lid means "laptop top cover" msgid "Device cannot be updated while the lid is closed" msgstr "Zařízení není možné zaktualizovat, pokud je víko s displejem pÅ™iklopené" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "ZmÄ›nÄ›no zařízení:" #. TRANSLATORS: a version check is required for all firmware msgid "Device firmware is required to have a version check" msgstr "Je tÅ™eba, aby firmware zařízení mÄ›l kontrolu verze" #. TRANSLATORS: emulated means we are pretending to be a different model msgid "Device is emulated" msgstr "Zařízení je emulováno" #. TRANSLATORS: device cannot be interrupted, for instance taking a phone call msgid "Device is in use" msgstr "Zařízení je používáno" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "Zařízení je uzamÄeno" #. TRANSLATORS: we have two ways of communicating with the device, so we hide #. one msgid "Device is lower priority than an equivalent device" msgstr "Zařízení je nižší priority, než ekvivalentní zařízení" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "Zařízení vyžaduje instalaci vÅ¡ech poskytnutých vydání" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "Zařízení není dosažitelné" #. TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode msgid "Device is unreachable, or out of wireless range" msgstr "Zařízení není dosažitelné nebo se nachází mimo dosah signálu" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "Zařízení je možné používat i v průbÄ›hu aktualizace" #. TRANSLATORS: usually this is when we're waiting for a reboot msgid "Device is waiting for the update to be applied" msgstr "Zařízení Äeká na instalaci aktualizací" #. TRANSLATORS: success, so say thank you to the user msgid "Device list uploaded successfully, thanks!" msgstr "Seznam zařízení úspěšnÄ› odeslán – dÄ›kujeme!" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Odebráno zařízení:" #. TRANSLATORS: as in, wired mains power for a laptop msgid "Device requires AC power to be connected" msgstr "Zařízení vyžaduje, aby bylo pÅ™ipojeno napájení z elektrické zásuvky" #. TRANSLATORS: device does not have a display connected msgid "Device requires a display to be plugged in" msgstr "Zařízení vyžaduje aby byla pÅ™ipojená obrazovka" #. TRANSLATORS: The device cannot be updated due to missing vendor's license." msgid "Device requires a software license to update" msgstr "Aby ho bylo možné zaktualizovat, zařízení vyžaduje softwarovou licenci" #. TRANSLATORS: longer description msgid "Device software updates are provided for this device." msgstr "Pro toto zařízení jsou poskytovány aktualizace softwaru." #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "Zařízení podporuje rozfázované aktualizace" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "Zařízení podporuje pÅ™epínání mezi různými vÄ›tvemi firmware" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "U daného zařízení je tÅ™eba nejdříve aktivovat režim aktualizace" #. TRANSLATORS: save the old firmware to disk before installing the new one msgid "Device will backup firmware before installing" msgstr "Zařízení provede zálohu stávajícího firmware pÅ™ed instalací nového" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "Po dokonÄení aktualizace se zařízení hned znovu neobjeví" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "Zařízení, která byla úspěšnÄ› aktualizována:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "Zařízení, která nebyla úspěšnÄ› aktualizována:" #. TRANSLATORS: message letting the user there is an update #. * waiting, but there is a reason it cannot be deployed msgid "Devices with firmware updates that need user action: " msgstr "Zařízení u kterých aktualizace firmware vyžaduje akci uživatele:" #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "Zařízení, pro která nejsou k dispozici aktualizace firmware:" #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "Zařízení s nejnovÄ›jšími dostupnými verzemi firmware:" #. TRANSLATORS: this is for the device tests msgid "Did not find any devices with matching GUIDs" msgstr "Nenalezena žádná zařízení, která mají odpovídající GUID identifikátory" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "Zakázáno" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "Zakázat daný vzdálený zdroj" #. TRANSLATORS: command description msgid "Disables virtual testing devices" msgstr "Vypíná virtuální testovací zařízení" #. TRANSLATORS: the OS the release was tested on msgid "Distribution" msgstr "Distribuce" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "Nekontrolovat stáří metadat" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "Nekontrolovat nenahlášení historie" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "Nekontrolovat zda by mÄ›lo být zapnuté stahování ze vzdálených zdrojů" #. TRANSLATORS: command line option msgid "Do not check or prompt for reboot after update" msgstr "Po aktualizaci nekontrolovat nebo se neptat na restart" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "NepÅ™idávat pÅ™edponu oblasti zaznamenávání" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "NepÅ™idávat pÅ™edponu s datem a Äasem" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "NeprovádÄ›t přípravné kontroly zařízení" #. TRANSLATORS: command line option msgid "Do not prompt for devices" msgstr "Nedotazovat se na zařízení" #. TRANSLATORS: command line option msgid "Do not prompt to fix security issues" msgstr "Nevyzývat k opravení problémů se zabezpeÄením" #. TRANSLATORS: command line option msgid "Do not search the firmware when parsing" msgstr "PÅ™i zpracovávání nehledat firmware" #. TRANSLATORS: warning message shown after update has been scheduled msgid "Do not turn off your computer or remove the AC adaptor while the update is in progress." msgstr "V průbÄ›hu aktualizace poÄítaÄ nevypínejte ani ho neodpojujte od napájení ze zásuvky." #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "Nezapisovat do databáze historie" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "Rozumíte důsledkům zmÄ›ny vÄ›tve firmwaru?" #. TRANSLATORS: ask the user if it's okay to convert, #. * "it" being the data contained in the EFI boot entry msgid "Do you want to convert it now?" msgstr "Chcete provést konverzi?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "Chcete pro budoucí aktualizace tuto funkci vypnout?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "Chcete nyní tento vzdálený zdroj znovu naÄíst?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "Chcete pro budoucí aktualizace odesílat hlášení automaticky?" #. TRANSLATORS: command line option msgid "Don't prompt for authentication (less details may be shown)" msgstr "Nepožadovat ověření se (následkem toho může být k dispozici ménÄ› podrobností)" #. TRANSLATORS: success msgid "Done!" msgstr "Hotovo!" #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "Vrátit se ke starší verzi u %s a to z %s na %s?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "Vrátit firmware v zařízení na starší verzi" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "Vracení %s na starší verzi…" #. TRANSLATORS: command description msgid "Download a file" msgstr "Stáhnout si soubor" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Stahování…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "Vypsat data SMBIOS ze souboru" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "Délka instalace" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "EMULATION-FILE [ARCHIVE-FILE]" msgstr "SOUBOR-EMULACE [SOUBOR-ARCHIVU]" #. TRANSLATORS: longer description msgid "Each system should have tests to ensure firmware security." msgstr "Každý systém by mÄ›l mít testy pro zajiÅ¡tÄ›ní zabezpeÄení firmware." #. TRANSLATORS: command description msgid "Emulate a device using a JSON manifest" msgstr "Emulovat zařízení pomocí JSON manifestu" #. TRANSLATORS: this device is not actually real msgid "Emulated" msgstr "Emulováno" #. TRANSLATORS: Title: if we are emulating a different host msgid "Emulated host" msgstr "Emulovaný stroj" msgid "Enable" msgstr "Zapnout" msgid "Enable emulation data collection" msgstr "Zapnout shromažÄování údajů emulace" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "Povolit nový vzdálený zdroj?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "Povolit tento vzdálený zdroj?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Povoleno" #. TRANSLATORS: Plugin is active only if hardware is found msgid "Enabled if hardware matches" msgstr "Zapnuto pokud je odpovídající hardware" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "Povolit zadaný vzdálený zdroj" #. TRANSLATORS: command description msgid "Enables virtual testing devices" msgstr "Zapíná virtuální testovací zařízení" #. TRANSLATORS: longer description msgid "Enabling firmware updates for the BIOS allows fixing security issues." msgstr "Zapnutí aktualizací firmware pro BIOS umožňuje opravovat problémy se zabezpeÄením." msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "Zapnutí této funkce je na vaÅ¡e vlastní riziko. To znamená, že v případÄ› problémů způsobených aktualizací je tÅ™eba, abyste se obrátili přímo na výrobce daného vybavení. Na adresu $OS_RELEASE:BUG_REPORT_URL$ by mÄ›ly být hlášeny pouze problémy s aktualizaÄním procesem jako takovým." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "Povolení tohoto vzdáleného zdroje je na vaÅ¡e vlastní riziko." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Å ifrováno" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "Å ifrovaná RAM" msgid "Encrypted RAM makes it impossible for information that is stored in device memory to be read if the memory chip is removed and accessed." msgstr "Å ifrovaná operaÄní paměť znemožňuje Ätení informací z pamÄ›ti zařízení v případÄ›, že je z nÄ›j paměťový modul vyjmut a pokouÅ¡eno se z nÄ›ho Äíst." #. TRANSLATORS: the vendor is no longer supporting the device msgid "End of life" msgstr "Podpora od výrobce skonÄila" #. TRANSLATORS: The BIOS setting can only be changed to fixed values msgid "Enumeration" msgstr "VýÄet" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "Smazat veÅ¡kerou historii aktualizací firmware" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Mazání…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "SkonÄit po krátké prodlevÄ›" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "SkonÄit po naÄtení výkonné Äásti" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "Exportovat strukturu souboru s firmwarem do XML" #. TRANSLATORS: command description msgid "Export firmware history for manual upload" msgstr "Exportovat historii firmwarů pro ruÄní nahrání" #. TRANSLATORS: command description msgid "Extract a firmware blob to images" msgstr "Rozbalit firmware z blobu do obrazů" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "SOUBOR" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "SOUBOR [IDENTIF-ZAŘÃZENÃ|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "SOUBOR" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "SOUBOR CERTIFIKÃT SOUKROMÃ-KLÃÄŒ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "SOUBOR IDENTIFIKÃTOR-ZAŘÃZENÃ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME OFFSET DATA [FIRMWARE-TYPE]" msgstr "SOUBOR POSUN DATA [TYP-FIRMWARE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "SOUBOR [IDENTIFIKÃTOR-ZAŘÃZ|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "SOUBOR [TYP-FIRMWARE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" msgstr "ZDROJOVÃ-SOUBOR CÃLOVÃ-SOUBOR [ZDROJ-TYP-FIRMWARE] [CÃL-TYP-FIRMWARE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" msgstr "SOUBOR|KONTROLNÃ-SOUÄŒET1[,KONTROLNÃ-SOUÄŒET2][,KONTROLNÃ-SOUÄŒET3]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "NezdaÅ™ilo se" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Aktualizaci se nepodaÅ™ilo provést" #. TRANSLATORS: error message for Windows msgid "Failed to connect to Windows service, please ensure it's running." msgstr "NepodaÅ™ilo se pÅ™ipojit k Windows službÄ› – zajistÄ›te, že je spuÅ¡tÄ›ná." #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "NepodaÅ™ilo se spojit s procesem služby" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "NepodaÅ™ilo se naÄíst místní dbx" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "NepodaÅ™ilo se naÄíst systémové dbx" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "NepodaÅ™ilo se uzamknout" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Zpracování argumentů se nezdaÅ™ilo" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "Soubor se nepodaÅ™ilo zpracovat" #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #, c-format msgid "Failed to parse flags for %s" msgstr "NepodaÅ™ilo se zpracovat příznak pro %s" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "NepodaÅ™ilo se zpracovat místní dbx" #. TRANSLATORS: a feature is something like "can show an image" msgid "Failed to set front-end features" msgstr "NepodaÅ™ilo se nastavit funkce nadstavby" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "NepodaÅ™ilo se ověřit obsah ESP oddílu" #. TRANSLATORS: item is FALSE msgid "False" msgstr "Nepravda" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Soubor" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "Podpis souboru" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "Původ souboru" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Je zapotÅ™ebí zadat název souboru" #. TRANSLATORS: command line option msgid "Filter with a set of device flags using a ~ prefix to exclude, e.g. 'internal,~needs-reboot'" msgstr "Filtrovat podle sady příznaků zařízení (s tím, že pÅ™edpona ~ slouží pro vynechání), napÅ™. 'internal,~needs-reboot' (vestavÄ›né, nevyžadující restart)" #. TRANSLATORS: command line option msgid "Filter with a set of release flags using a ~ prefix to exclude, e.g. 'trusted-release,~trusted-metadata'" msgstr "Filtrovat podle sady příznaků vydání (s tím, že pÅ™edpona ~ slouží pro vynechání), napÅ™. 'trusted-release,~trusted-metadata'" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware Attestation" msgstr "Atestace firmware" #. TRANSLATORS: longer description msgid "Firmware Attestation checks device software using a reference copy, to make sure that it has not been changed." msgstr "Atestace firmware kontroluje software zařízení pomocí referenÄní kopie, aby bylo jisté, že nebyl zmÄ›nÄ›n." #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "BIOS popisovaÄ firmware" #. TRANSLATORS: longer description msgid "Firmware BIOS Descriptor protects device firmware memory from being tampered with." msgstr "Firmware BIOS Descriptor chrání paměť firmware zařízení proti úpravám." #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "BIOS oblast firmware" #. TRANSLATORS: longer description msgid "Firmware BIOS Region protects device firmware memory from being tampered with." msgstr "Firmware BIOS Region chrání paměť firmware zařízení proti úpravám." #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "Základní URI firmwaru" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "Služba D-Bus pro aktualizaci firmwaru" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Proces služby pro aktualizaci firmwaru" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "Ověřování nástroje pro aktualizaci firmware" msgid "Firmware Updater Verification checks that software used for updating has not been tampered with." msgstr "Ověřování nástroje pro aktualizaci firmware kontroluje, že se software, použitým pro aktualizaci, nebylo manipulováno." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "Aktualizace firmware" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "Nástroj pro práci s firmwary" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "Ochrana firmware vůÄi pÅ™epsání" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "Zámek ochrany firmware vůÄi pÅ™epsání" #. TRANSLATORS: longer description msgid "Firmware Write Protection protects device firmware memory from being tampered with." msgstr "Zamezení zápisu firmware chrání paměť firmware zařízení proti úpravám." #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "Atestace firmware" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "Firmware už je blokován" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "Firmware jeÅ¡tÄ› není blokován" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "Metadata o firmwarech nebyla aktualizována už celý %u den a nemusí být proto aktuální." msgstr[1] "Metadata o firmwarech nebyla aktualizována už celé %u dny a nemusí být proto aktuální." msgstr[2] "Metadata o firmwarech nebyla aktualizována už celých %u dní a nemusí být proto aktuální." msgstr[3] "Metadata o firmwarech nebyla aktualizována už celé %u dny a nemusí být proto aktuální." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "Aktualizace firmware" #. TRANSLATORS: user needs to run a command, %1 is 'fwupdmgr unlock' #, c-format msgid "Firmware updates disabled; run '%s' to enable" msgstr "Aktualizace firmware jsou zakázané; pro jejich povolení spusÅ¥te „%s“" #. TRANSLATORS: command description msgid "Fix a specific host security attribute" msgstr "Opravit konkrétní atribut zabezpeÄení stroje" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fix reverted successfully" msgstr "ÚspěšnÄ› vráceno do stavu pÅ™ed opravou" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fixed successfully" msgstr "ÚspěšnÄ› opraveno" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "Příznaky" #. TRANSLATORS: command line option msgid "Force the action by relaxing some runtime checks" msgstr "Vynutit akci uvolnÄ›ním nÄ›kterých kontrol pÅ™i vykonávání" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Nalezeno" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "ZjiÅ¡tÄ›no celodiskové Å¡ifrování" #. TRANSLATORS: we might ask the user the recovery key when next booting #. Windows msgid "Full disk encryption secrets may be invalidated when updating" msgstr "PÅ™i aktualizaci mohou být zneplatnÄ›na tajemství pro celodiskové Å¡ifrování" #. TRANSLATORS: Title: if the part has been fused msgid "Fused Platform" msgstr "Platforma s jednorázovým nastavením (fuse)" #. TRANSLATORS: Title: if the part has been fused msgid "Fused platform" msgstr "Platforma s jednorázovým nastavením (fuse)" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" msgstr[1] "GUID identifikátory" msgstr[2] "GUID identifikátorů" msgstr[3] "GUID identifikátory" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "GUID|DEVICE-ID" msgstr "GUID|ID-ZARIZENI" msgid "Get BIOS settings" msgstr "Získat nastavení BIOS" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "Vypsat vÅ¡echny příznaky zařízení podporované fwupd" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Vypsat vÅ¡echna zařízení podporující aktualizaci firmwaru" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "Vypsat vÅ¡echny povolené zásuvné moduly registrované v systému" #. TRANSLATORS: command description msgid "Get all known version formats" msgstr "Získat veÅ¡keré známé formáty verzí" #. TRANSLATORS: command description msgid "Get device report metadata" msgstr "Získat metadata výkazu o zařízení" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Zjistit podrobnosti o souboru s firmwarem" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "Zjistit nastavené vzdálené zdroje" #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "Zjistit atributy zabezpeÄení hostitele" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "Získat seznam schválených firmwarů" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "Získat seznam blokovaných firmwarů" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Vypsat seznam aktualizací pro pÅ™ipojený hardware" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "Vypsat vydání pro zařízení" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Vypsat výsledky z poslední aktualizace" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "SOUBOR-S-IDENTIF-HARDWARE" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "Hardware Äeká na opÄ›tovné pÅ™ipojení" #. TRANSLATORS: the release urgency msgid "High" msgstr "Vysoká" #. TRANSLATORS: title for host security events msgid "Host Security Events" msgstr "Události zabezpeÄení na hostiteli" #. TRANSLATORS: error message for unsupported feature #, c-format msgid "Host Security ID (HSI) is not supported" msgstr "Identif. zabezpeÄení hostitele (HSI) není podporován" #. TRANSLATORS: success, so say thank you to the user msgid "Host Security ID attributes uploaded successfully, thanks!" msgstr "Atributy identifikátoru zabezpeÄení hostitele úspěšnÄ› nahrány – dÄ›kujeme!" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "Identifikátor zabezpeÄení hostitele:" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX" msgstr "INDEX" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX KEY [VALUE]" msgstr "INDEX KLÃÄŒ [HODNOTA]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX NAME TARGET [MOUNTPOINT]" msgstr "INDEX NÃZEV CÃL [PŘÃPOJNÃ-BOD]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX1,INDEX2" msgstr "INDEX1,INDEX2" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INHIBIT-ID" msgstr "IDENTIF-INHIBITORU" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "Ochrana IOMMU" #. TRANSLATORS: longer description msgid "IOMMU Protection prevents connected devices from accessing unauthorized parts of system memory." msgstr "Ochrana IOMMU brání pÅ™ipojeným zařízení v přístupu k Äástem systémové pamÄ›ti, ke kterým nejsou pověřené k přístupu." #. TRANSLATORS: HSI event title msgid "IOMMU device protection disabled" msgstr "ochrana IOMMU zařízení vypnuta" #. TRANSLATORS: HSI event title msgid "IOMMU device protection enabled" msgstr "ochrana IOMMU zařízení zapnuta" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "NeÄinné…" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "PÅ™i stahování souborů ignorovat nÄ›které nedostatky SSL certifikátů" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "Ignorovat nesprávný kontrolní souÄet firmware" #. TRANSLATORS: command line option msgid "Ignore firmware hardware mismatch failures" msgstr "Ignorovat neshody firmware s hardware" #. TRANSLATORS: command line option msgid "Ignore non-critical firmware requirements" msgstr "Ignorovat nekritické požadavky firmware" #. TRANSLATORS: try to help msgid "Ignoring SSL strict checks, to do this automatically in the future export DISABLE_SSL_STRICT in your environment" msgstr "Ignorují se striktní SSL kontroly. Pokud chcete, aby se tak příštÄ› stalo automaticky, nastavte (a exportujte) promÄ›nnou prostÅ™edí DISABLE_SSL_STRICT" #. TRANSLATORS: show the user a generic image that can be themed msgid "Image" msgstr "Obrázek" #. TRANSLATORS: show the user a random image from the internet msgid "Image (custom)" msgstr "Obrázek (uživatelsky urÄený)" #. TRANSLATORS: the inhibit ID is a short string like dbus-123456 #, c-format msgid "Inhibit ID is %s." msgstr "Identif. blokátoru je %s." #. TRANSLATORS: command description msgid "Inhibit the system to prevent upgrades" msgstr "Inhibovat systém a zabránit tak pÅ™echodům na novÄ›jší verze" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "Délka instalace" #. TRANSLATORS: command description msgid "Install a firmware file in cabinet format on this hardware" msgstr "Nainstalovat soubor s firmwarem (ve formátu cab) na tento hardware" #. TRANSLATORS: command description msgid "Install a raw firmware blob on a device" msgstr "Nainstalovat na zařízení holý binární soubor s firmwarem" #. TRANSLATORS: command description msgid "Install a specific firmware file on all devices that match" msgstr "Nainstalovat konkrétní soubor s firmware na vÅ¡ech zařízeních, se kterými je ve shodÄ›" #. TRANSLATORS: command description msgid "Install a specific firmware on a device, all possible devices will also be installed once the CAB matches" msgstr "Nainstalovat konkrétní firmware na zařízení, veÅ¡kerá možná zařízení budou také nainstalována, pokud jim CAB odpovídá" msgid "Install old version of signed system firmware" msgstr "Instalace starší verze podepsaného systémového firmware" msgid "Install old version of unsigned system firmware" msgstr "Instalace starší verze nepodepsaného systémového firmware" msgid "Install signed device firmware" msgstr "Instalace podepsaného firmwaru zařízení" msgid "Install signed system firmware" msgstr "Instalace podepsaného systémového firmwaru" msgid "Install unsigned device firmware" msgstr "Instalace nepodepsaného firmwaru zařízení" msgid "Install unsigned system firmware" msgstr "Instalace nepodepsaného systémového firmwaru" #. TRANSLATORS: stay on one firmware version unless the new version is #. explicitly #. * specified msgid "Installing a specific release is explicitly required" msgstr "Je výslovnÄ› vyžadována instalace konkrétního vydání" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "Instaluje se aktualizace firmwaru…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "Instalace na %s…" #. TRANSLATORS: if it breaks, you get to keep both pieces msgid "Installing this update may also void any device warranty." msgstr "Instalace této aktualizace také může zneplatnit jakékoli záruky na zařízení." #. TRANSLATORS: The BIOS setting only accepts integers in a fixed range msgid "Integer" msgstr "Celé Äíslo" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM Protected" msgstr "ChránÄ›no Intel BootGuard ACM" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "ChránÄ›no Intel BootGuard ACM" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard Error Policy" msgstr "Co Intel BootGuard udÄ›lá v případÄ› chyb" msgid "Intel BootGuard Error Policy ensures the device does not continue to start if its device software has been tampered with." msgstr "Zásada chyb Intel BootGuard zajišťuje, že zařízení nebude pokraÄovat ve startu, pokud se software zařízení bylo manipulováno." #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard Fuse" msgstr "Jednoráz. trvalé pÅ™epnutí (fuse) pro Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "Jednorázové trvalé pÅ™epnutí (OTP fuse) pro Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard Verified Boot" msgstr "Ověřované zavádÄ›ní s Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Co Intel BootGuard udÄ›lá v případÄ› chyb" #. TRANSLATORS: longer description msgid "Intel BootGuard prevents unauthorized device software from operating when the device is started." msgstr "Intel BootGuard brání fungování neověřeného software zařízení, když je zařízení spouÅ¡tÄ›no." #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Ověřované zavádÄ›ní s Intel BootGuard" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS Mitigation" msgstr "ZmírnÄ›ní Intel GDS" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS mitigation" msgstr "ZmírnÄ›ní Intel GDS" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Manufacturing Mode" msgstr "Intel Management Engine – režim pro výrobce" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the "override" is #. enabled #. * with a jumper -- luckily it is probably not accessible to end users on #. consumer #. * boards msgid "Intel Management Engine Override" msgstr "Intel Management Engine – pÅ™ebití" #. TRANSLATORS: longer description msgid "Intel Management Engine Override disables checks for device software tampering." msgstr "PÅ™ebití Intel Management Engine vypíná kontroly manipulace se software zařízení." #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Version" msgstr "Verze Intel Management Engine" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "VestavÄ›né zařízení" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "Neplatné" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "Neplatné argumenty" #. TRANSLATORS: error message msgid "Invalid arguments, expected GUID" msgstr "Neplatný argument – oÄekáváno GUID" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX KEY [VALUE]" msgstr "Neplatné argumenty, oÄekáváno INDEX KEY [HODNOTA]" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX NAME TARGET [MOUNTPOINT]" msgstr "Neplatné argumenty, oÄekáváno INDEX NÃZEV CÃL [PŘÃPOJNÃ-BOD]" #. TRANSLATOR: This is the error message for #. * incorrect parameter msgid "Invalid arguments, expected an AppStream ID" msgstr "Neplatný argument – oÄekáván AppStream identif." #. TRANSLATORS: error message msgid "Invalid arguments, expected at least ARCHIVE FIRMWARE METAINFO" msgstr "Neplatný argument – oÄekáváno pÅ™injemenším ARCHIVE FIRMWARE METAINFO" #. TRANSLATORS: error message msgid "Invalid arguments, expected base-16 integer" msgstr "Neplané argumenty – oÄekáváno celé Äíslo ve formátu base-16" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "Je starší verzí" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "Je v režimu zavadÄ›Äe firmware" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "Je novÄ›jší verzí" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "Problém" msgstr[1] "Problémy" msgstr[2] "Problémů" msgstr[3] "Problémy" #. TRANSLATORS: HSI event title msgid "Kernel is no longer tainted" msgstr "Jádro už není pozmÄ›nÄ›no" #. TRANSLATORS: HSI event title msgid "Kernel is tainted" msgstr "Jádro je pozmÄ›nÄ›no" #. TRANSLATORS: HSI event title msgid "Kernel lockdown disabled" msgstr "UzamÄení jádra vypnuto" #. TRANSLATORS: HSI event title msgid "Kernel lockdown enabled" msgstr "UzamÄení jádra zapnuto" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "UMÃSTÄšNÃ" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "Naposledy zmÄ›nÄ›no" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "Zbývá ménÄ› než jedna minuta" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "Licence" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "UzamÄení linuxového jádra" #. TRANSLATORS: longer description msgid "Linux Kernel Lockdown mode prevents administrator (root) accounts from accessing and changing critical parts of system software." msgstr "UzamÄený režim linuxového jádra brání úÄtům s právy správy (root) v přístupu a zmÄ›nám kritických Äástí systémového software." msgid "Linux Kernel Swap temporarily saves information to disk as you work. If the information is not protected, it could be accessed by someone if they obtained the disk." msgstr "Odkládací prostor (swap) linuxového jádra za provozu doÄasnÄ› ukládá informace na disk. Pokud tyto nejsou chránÄ›ny, může se k nim nÄ›kdo dostat, pokud získá přístup k disku." #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "Ověření linuxového jádra" msgid "Linux Kernel Verification makes sure that critical system software has not been tampered with. Using device drivers which are not provided with the system can prevent this security feature from working correctly." msgstr "Ověřování neporuÄenosti linuxového jádra zajišťuje, že s kritickým systémovým software nebylo manipulováno. Použití ovladaÄů zařízení, které nejsou poskytovány systémem, mohou této bezpeÄnostní funkci bránit ve správném fungování." #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "Linuxový odkládací prostor" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Linux Vendor Firmware Service (stabilní firmware)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Linux Vendor Firmware Service (testovací firmware)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Linuxové jádro" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "UzamÄení linuxového jádra" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Linuxový odkládací prostor" #. TRANSLATORS: command description msgid "List EFI boot files" msgstr "Vypsat soubory EFI zavádÄ›ní" #. TRANSLATORS: command description msgid "List EFI boot parameters" msgstr "Vypsat parametry EFI zavádÄ›ní" #. TRANSLATORS: command description msgid "List EFI variables with a specific GUID" msgstr "Vypsat EFI promÄ›nné s konkrétním GUID" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "Vypsat položky v dbx" #. TRANSLATORS: command description msgid "List the available firmware GTypes" msgstr "Vypsat GTypy firmware, které jsou k dispozici" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "Vypsat typy firmwaru, které jsou k dispozici" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "Vypsat soubory na ESP oddílu" #. TRANSLATORS: command description msgid "Load device emulation data" msgstr "NaÄíst data emulace zařízení" #. TRANSLATORS: the plugin was created from a .so object, and was not built-in msgid "Loaded from an external module" msgstr "NaÄteno z externího modulu" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "NaÄítání…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "ZamÄeno" #. TRANSLATORS: the release urgency msgid "Low" msgstr "Nízká" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refers #. * to the private/public key used to secure loading of firmware msgid "MEI Key Manifest" msgstr "Manifest klíÄe MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refer #. * to the private/public key used to secure loading of firmware msgid "MEI key manifest" msgstr "Manifest klíÄe MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "režim pro výrobce v MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "hw pÅ™epnutí MEI" msgid "MEI version" msgstr "Verze MEI" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "RuÄnÄ› zapnout konkrétní zásuvné moduly" #. TRANSLATORS: longer description msgid "Manufacturing Mode is used when the device is manufactured and security features are not yet enabled." msgstr "Režim pro výrobce je používán pÅ™i výrobÄ› zařízení a funkce pro zabezpeÄení jeÅ¡tÄ› nejsou zapnuté." #. TRANSLATORS: Longest valid string for BIOS setting msgid "Maximum length" msgstr "Nejdelší platná délka" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "Nejvyšší platná hodnota" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "StÅ™ední" #. TRANSLATORS: ask the user to do a simple task which should be translated msgid "Message" msgstr "Zpráva" #. TRANSLATORS: ask the user a question, and it will not be translated msgid "Message (custom)" msgstr "Zpráva (uživatelsky urÄená)" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "Podpis metadat" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "URI pro metadata" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "Metadata lze získat ze služby Linux Vendor Firmware Service." #. TRANSLATORS: error message for a user who ran fwupdmgr #. * refresh recently -- %1 is '--force' #, c-format msgid "Metadata is up to date; use %s to refresh again." msgstr "Metadata jsou aktuální. Pokud je pÅ™esto chcete znovu naÄíst, použijte pÅ™epínaÄ %s." #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "Nejstarší použitelná verze" #. TRANSLATORS: Shortest valid string for BIOS setting msgid "Minimum length" msgstr "Nejkratší platné délka" #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "Nejnižší platná hodnota" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "ZmÄ›nit hodnotu v nastavení procesu služby" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "ZmÄ›nit zadaný vzdálený zdroj" msgid "Modify a configured remote" msgstr "Upravit nastavený vzdálený zdroj" msgid "Modify daemon configuration" msgstr "Upravit nastavení procesu služby" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "Sledovat události v procesu služby" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "PÅ™ipojit ESP oddíl" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "Po instalaci vyžaduje restart" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "Vyžaduje restart" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "Po instalaci vyžaduje vypnutí" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "Nová verze" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "Není zadána žádné akce!" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "Pro %s nejsou k dispozici žádné starší verze" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "Nenalezeny žádné identifikátory firmware" #. TRANSLATORS: nothing found msgid "No firmware found" msgstr "Nenalezen žádný firmware" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "NezjiÅ¡tÄ›n žádný hardware vybavený pro aktualizaci firmware v nÄ›m." #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "Nejsou k dispozici žádná vydání" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "V tuto chvíli nejsou povolené žádné vzdálené zdroje, takže nejsou k dispozici žádná metadata." #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "Nejsou k dispozici žádné vzdálené zdroje" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "Žádná zařízení, u kterých by bylo možné zaktualizovat" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "Nejsou k dispozici žádné aktualizace" msgid "No updates available for remaining devices" msgstr "Pro zbývající zařízení nejsou k dispozici žádné aktualizace" #. TRANSLATORS: error message #, c-format msgid "No volume matched %s" msgstr "S %s se neshoduje žádný svazek" #. TRANSLATORS: version cannot be installed due to policy msgid "Not approved" msgstr "Neschváleno" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "Nenalezeno" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "Nepodporováno" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "V pořádku" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "OK!" #. TRANSLATORS: the firmware old version msgid "Old version" msgstr "Stará verze" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "Zobrazit pouze jedinou PCR hodnotu" #. TRANSLATORS: command line option msgid "Only use peer-to-peer networking when downloading files" msgstr "Stahovat soubory pouze ze sítÄ› klient-klient (p2p)" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "Zařízení umožňuje pouze aktualizace na novÄ›jší verze" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "PÅ™epsat výchozí popis umístÄ›ní na ESP oddílu" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Firmware" msgstr "Firmware z P2P" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Metadata" msgstr "Metadata z P2P" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "POPIS-UMÃSTÄšNÃ" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "Zpracovat a zobrazit podrobnosti o souboru s firmwarem" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "Zpracovávání aktualizace dbx…" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "Zpracovávání systémového dbx…" #. TRANSLATORS: remote filename base msgid "Password" msgstr "Heslo" #. TRANSLATORS: command description msgid "Patch a firmware blob at a known offset" msgstr "Opravit blob firmware na známém posunu" msgid "Payload" msgstr "Obsah" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "ÄŒeká" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "Provést operaci" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "LadÄ›ní platformy" #. TRANSLATORS: longer description msgid "Platform Debugging allows device security features to be disabled. This should only be used by hardware manufacturers." msgstr "LadÄ›ní platformy umožňuje vypínat funkce zabezpeÄení zařízení. MÄ›lo by být používáno pouze výrobci zařízení." #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "LadÄ›ní platformy" #. TRANSLATORS: 'recovery key' here refers to a code, rather than a physical #. metal thing msgid "Please ensure you have the volume recovery key before continuing." msgstr "Než budete pokraÄovat, zajistÄ›te si, abyste mÄ›li k dispozici záchranný klÃ­Ä pro svazek." #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "Zadejte prosím Äíslo od 0 do %u:" #. TRANSLATORS: the user isn't reading the question -- %1 is 'Y' and %2 is #. * 'N' #, c-format msgid "Please enter either %s or %s: " msgstr "Zadejte buÄ %s nebo %s: " #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "Chybí komponenty, na kterých zásuvný modul závisí" #. TRANSLATORS: The plugin is only for testing msgid "Plugin is only for testing" msgstr "Zásuvný modul slouží pouze pro testování" #. TRANSLATORS: Possible values for a bios setting msgid "Possible Values" msgstr "Možné hodnoty" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA Protection" msgstr "Ochrana přímého přístupu do pamÄ›ti pÅ™ed startem operaÄního systému" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "Ochrana přímého přístupu do pamÄ›ti pÅ™ed startem operaÄního systému" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is disabled" msgstr "Ochrana přímého přístupu do pamÄ›ti pÅ™ed startem operaÄního systému je vypnuta" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is enabled" msgstr "Ochrana přímého přístupu do pamÄ›ti pÅ™ed startem operaÄního systému je zapnuta" #. TRANSLATORS: longer description msgid "Pre-boot DMA protection prevents devices from accessing system memory after being connected to the computer." msgstr "Ochrana přímého přístupu do pamÄ›ti (DMA) brání zařízení pÅ™istupovat k systémové pamÄ›ti poté co jsou pÅ™ipojena k poÄítaÄi." #. TRANSLATORS: warning message msgid "Press unlock on the device to continue the update process." msgstr "Aby bylo možné pokraÄovat v procesu aktualizace, stisknÄ›te na zařízení odemknutí." #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "PÅ™edchozí verze" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Priorita" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "Problémy" msgid "Proceed with upload?" msgstr "PokraÄovat v nahrávání?" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Processor Security Checks" msgstr "Kontroly zabezpeÄení procesoru" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "Processor rollback protection" msgstr "Zamezení instalace starší verze na procesoru" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "UzavÅ™ená" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "IDENTIFIKÃTOR-VZDÃLENÉHO-ZDROJE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID KEY VALUE" msgstr "HODNOTA KLÃÄŒE IDENTIF-VZDÃLENÉHO-ZDROJE" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "Pouze pro Ätení" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "VyÄíst blob firmwaru ze zařízení" #. TRANSLATORS: command description msgid "Read a firmware from a device" msgstr "VyÄíst firmware ze zařízení" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "ÄŒtení z %s…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "ÄŒtení…" #. TRANSLATORS: Plugin is active and in use msgid "Ready" msgstr "PÅ™ipraveno" #. TRANSLATORS: how often we should refresh the metadata msgid "Refresh Interval" msgstr "Interval opÄ›tovného naÄítání" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "Aktualizovat metadata ze vzdáleného serveru" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "PÅ™einstalovat %s na %s?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "PÅ™einstalovat stávající firmware na zařízení" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "PÅ™einstalovat firmware na zařízení" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "VÄ›tev vydání" #. TRANSLATORS: release attributes msgid "Release Flags" msgstr "Příznaky vydání" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "Identif. vydání" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "Identif. vzdáleného zdroje" #. TRANSLATORS: command description msgid "Removes devices to watch for future emulation" msgstr "Odebere zařízení pro hlídání budoucí emulace" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "URI pro hlášení" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "Nahlášeno na vzdálený server" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "Nebyl nalezen potÅ™ebný souborový systém efivarfs" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "Požadovaný hardware nenalezen" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "Vyžaduje ruÄní pÅ™epnutí do zavadÄ›Äe firmware" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Vyžaduje pÅ™ipojení k Internetu" msgid "Reset daemon configuration" msgstr "Vrátit nastavení procesu služby do výchozího stavu" #. TRANSLATORS: sets something in the daemon configuration file msgid "Resets a daemon configuration section" msgstr "Vrátí sekci nastavení procesu služby do výchozího stavu" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Restartovat nyní?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "Restartovat proces služby, aby se zmÄ›ny uplatnily?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Restartování zařízení…" #. TRANSLATORS: command description msgid "Retrieve BIOS settings. If no arguments are passed all settings are returned" msgstr "Získává nastavení z BIOS. Pokud nejsou uvedeny žádné argumenty, jsou vypsána veÅ¡kerá nastavení" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "Vypsat vÅ¡echny identifikátory hardwaru poÄítaÄe" #. TRANSLATORS: ask the user to upload msgid "Review and upload report now?" msgstr "Prohlédnout si a odeslat hlášení nyní?" #. TRANSLATORS: longer description msgid "Rollback Protection prevents device software from being downgraded to an older version that has security problems." msgstr "Zamezení instalace starší verze brání pÅ™echodu na starší verzi software zařízení (mohly by obsahovat už známé problémy v zabezpeÄení)." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr get-upgrades` #, c-format msgid "Run `%s` for more information." msgstr "Podrobnosti získáte spuÅ¡tÄ›ním „%s“." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr sync` #, c-format msgid "Run `%s` to complete this action." msgstr "Pokud chcete tuto akci dokonÄit, spusÅ¥te `%s`." #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "PÅ™i použití install-blob spustit zásuvný modul rutina vyÄiÅ¡tÄ›ní složeného" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "PÅ™i použití install-blob spustit zásuvný modul rutina pÅ™ipravení složeného" #. TRANSLATORS: command description msgid "Run the post-reboot cleanup action" msgstr "Spustit akci ÄiÅ¡tÄ›ní po restartu" #. TRANSLATORS: tell a user how to get information #, c-format msgid "Run without '%s' to see" msgstr "Pokud chcete jen zobrazit, spusÅ¥te bez „%s“" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "Provozované jádro systému je příliÅ¡ staré" #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "Přípona bÄ›hového prostÅ™edí" #. TRANSLATORS: Software Bill of Materials link msgid "SBOM" msgstr "Soupiska použitého software" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SECTION" msgstr "SEKCE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING VALUE" msgstr "HODNOTA NASTAVENI" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING1 VALUE1 [SETTING2] [VALUE2]" msgstr "NASTAVENÃ1 HODNOTA1 [NASTAVENÃ2] [HODNOTA2]" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "SMAP" msgstr "SMAP" #. TRANSLATORS: Title: Whether firmware is locked down msgid "SMM locked down" msgstr "SMM režim uzamÄen" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "PopisovaÄ SPI BIOS" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "Oblast SPI BIOS" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "SPI uzamÄení" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "SPI replay protection" msgstr "Ochrana SPI pÅ™ed replay" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "SPI zápis" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "SPI write protection" msgstr "Ochrana zápisu do SPI" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "PODSYSTÉM OVLADAÄŒ [IDENTIF-ZAŘÃZENÃ|GUID]" #. TRANSLATORS: command description msgid "Save a file that allows generation of hardware IDs" msgstr "Uložit soubor který umožňuje vytváření identifikátorů hardware" #. TRANSLATORS: command description msgid "Save device emulation data" msgstr "Uložit data emulace zařízení" #. TRANSLATORS: key for a offline report filename msgid "Saved report" msgstr "Uložené hlášení" #. TRANSLATORS: Scalar increment for integer BIOS setting msgid "Scalar Increment" msgstr "Skalární přírůstek" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Plánování aktualizace…" #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "Secure Boot vypnut" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "Secure Boot zapnut" msgid "Security hardening for HSI" msgstr "Posílení zabezpeÄení pro HSI" #. TRANSLATORS: the %1 is a URL to a wiki page #, c-format msgid "See %s for more details." msgstr "Podrobnosti viz %s." #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "Podrobnosti viz %s." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Vybrané zařízení" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Vybraný svazek" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "Sériové Äíslo" #. TRANSLATORS: Configured a BIOS setting to a value #, c-format msgid "Set BIOS setting '%s' using '%s'." msgstr "Nastavit volbu v BIOS „%s“ pomocí „%s." #. TRANSLATORS: command description msgid "Set a BIOS setting" msgstr "Provést nastavení v BIOS" msgid "Set one or more BIOS settings" msgstr "Nastavit jedno nebo více nastavení v BIOS" #. TRANSLATORS: command description msgid "Set or remove an EFI boot hive entry" msgstr "Nastavit nebo odebrat položku hive EFI zavádÄ›ní" #. TRANSLATORS: command description msgid "Set the EFI boot next" msgstr "Nastavit příští EFI zavádÄ›ní" #. TRANSLATORS: command description msgid "Set the EFI boot order" msgstr "Nastavit poÅ™adí EFI zavádÄ›ní" #. TRANSLATORS: command line option msgid "Set the download retries for transient errors" msgstr "Nastavit opakované pokusy o stažení pro pÅ™echodné chyby" #. TRANSLATORS: command description msgid "Sets one or more BIOS settings" msgstr "Nastaví jedno nebo více nastavení v BIOS" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Nastavuje seznam schválených firmwarů" #. TRANSLATORS: type of BIOS setting msgid "Setting type" msgstr "Typ nastavení" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "Nastavení se projeví po restartu stroje" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "Sdílet historii firmwaru s vývojáři" #. TRANSLATORS: command line option msgid "Show all results" msgstr "Zobrazit vÅ¡echny výsledky" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "Zobrazit verzi klienta a procesu služby" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "Zobrazit podrobnÄ›jší informace z procesu služby pro konkrétní oblast" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "Zapnout vypisování ladicích informací pro vÅ¡echny oblasti" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Zobrazit pÅ™edvolby ladÄ›ní" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Zobrazit zařízení, která nelze aktualizovat" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Zobrazovat doplňující informace pro ladÄ›ní" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "Zobrazit historii aktualizací firmwaru" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "Zobrazit vypoÄtenou verzi dbx" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "Vypnout nyní?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "Podepsat firmware novým klíÄem" msgid "Sign data using the client certificate" msgstr "Podepsat data klientským certifikátem" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "Podepsat data klientským certifikátem" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "Podepsat nahraná data klientským certifikátem" msgid "Signature" msgstr "Podpis" #. TRANSLATORS: firmware is verified on-device the payload using strong crypto msgid "Signed Payload" msgstr "Podepsaný obsah" #. TRANSLATORS: file size of the download msgid "Size" msgstr "Velikost" #. TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM msgid "Some of the platform secrets may be invalidated when updating this firmware." msgstr "PÅ™i aktualizaci tohoto firmwaru mohou být nÄ›která tajemství uložená v rámci platformy zneplatnÄ›na." #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "Zdroj" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "Zadejte soubor dbx databáze" msgid "Stop the fwupd service" msgstr "Zastavit službu fwupd" #. TRANSLATORS: The BIOS setting accepts strings msgid "String" msgstr "ŘetÄ›zec" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "ÚspÄ›ch" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "VÅ¡echna zařízení úspěšnÄ› aktivována" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "Vzdálený zdroj úspěšnÄ› zakázán" #. TRANSLATORS: comment explaining result of command msgid "Successfully disabled test devices" msgstr "Testovací zařízení úspěšnÄ› zakázány" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "Nová metadata úspěšnÄ› stažena:" #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "Vzdálený zdroj úspěšnÄ› povolen a znovu naÄten" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "Vzdálený zdroj úspěšnÄ› povolen" #. TRANSLATORS: comment explaining result of command msgid "Successfully enabled test devices" msgstr "Testovací zařízení úspěšnÄ› povoleny" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "Firmware úspěšnÄ› nainstalován" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "Hodnota v nastavení úspěšnÄ› zmÄ›nÄ›na" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "Vzdálený zdroj úspěšnÄ› zmÄ›nÄ›n" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "Metadata úspěšnÄ› ruÄnÄ› aktualizována" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration section" msgstr "Sekce nastavení úspěšnÄ› vrácena do výchozího stavu" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration values" msgstr "Hodnoty nastavení úspěšnÄ› navráceny do výchozího stavu" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "Kontrolní souÄty pro zařízení úspěšnÄ› zaktualizovány" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "ÚspěšnÄ› odesláno %u hlášení" msgstr[1] "ÚspěšnÄ› odeslána %u hlášení" msgstr[2] "ÚspěšnÄ› odesláno %u hlášení" msgstr[3] "ÚspěšnÄ› odeslána %u hlášení" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "Kontrolní souÄty pro zařízení úspěšnÄ› ověřeny" #. TRANSLATORS: the device showed up in time #, c-format msgid "Successfully waited %.0fms for device" msgstr "ÚspěšnÄ› poÄkáno %.0fms na zařízení" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Souhrn" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "Supervisor Mode Access Prevention" msgstr "ZabránÄ›ní v přístupu k režimu supervizora" #. TRANSLATORS: longer description msgid "Supervisor Mode Access Prevention ensures critical parts of device memory are not accessed by less secure programs." msgstr "Supervisor Mode Access Prevention zajišťuje, že kritické Äásti pamÄ›ti zařízení nejsou přístupné ménÄ› bezpeÄným programům." #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "Podporováno" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "Podporovaný procesor" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "Podporováno na vzdáleném serveru" #. TRANSLATORS: Title: a better sleep state msgid "Suspend To Idle" msgstr "Uspat do neÄinnosti" #. TRANSLATORS: Title: sleep state msgid "Suspend To RAM" msgstr "Uspat do pamÄ›ti" #. TRANSLATORS: longer description msgid "Suspend to Idle allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Uspání do neÄinnosti umožňuje zařízení rychlé pÅ™ejít do režimu spánku a Å¡etÅ™it tak energií. V dobÄ›, kdy je zařízení uspané, je možné z nÄ›ho fyzicky vyjmout operaÄní paměť a Äíst z ní informace." #. TRANSLATORS: longer description msgid "Suspend to RAM allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Uspání do pamÄ›ti umožňuje zařízení rychlé pÅ™ejít do režimu spánku a Å¡etÅ™it tak energií. V dobÄ›, kdy je zařízení uspané, je možné z nÄ›ho fyzicky vyjmout operaÄní paměť a Äíst z ní informace." #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "Uspat do neÄinnosti" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "Uspat do pamÄ›ti" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "PÅ™epnout vÄ›tev z %s na %s?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "PÅ™epnout vÄ›tev firmware na zařízení" #. TRANSLATORS: command description msgid "Sync firmware versions to the chosen configuration" msgstr "Synchronizovat verze firmwarů ke zvolenému nastavení" #. TRANSLATORS: Title: Whether firmware is locked down msgid "System Management Mode" msgstr "Režim správy systému" #. TRANSLATORS: this CLI tool is now preventing system updates msgid "System Update Inhibited" msgstr "Aktualizace systému blokována" #. TRANSLATORS: longer description msgid "System management mode is used by the firmware to access resident BIOS code and data." msgstr "Režim System management je používán firmwarem pro přístup k rezidentnímu BIOS kódu a datům." #. TRANSLATORS: as in laptop battery power msgid "System power is too low" msgstr "Akumulátor stroje je příliÅ¡ vybitý" #. TRANSLATORS: as in laptop battery power #, c-format msgid "System power is too low (%u%%, requires %u%%)" msgstr "Akumulátor stroje je příliÅ¡ vybitý (%u%%, vyžaduje %u%%)" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "PoÄítaÄ vyžaduje externí napájení" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "TEXT" #. TRANSLATORS: longer description msgid "TPM (Trusted Platform Module) is a computer chip that detects when hardware components have been tampered with." msgstr "TPM (Trusted Platform Module) je poÄítaÄový Äip, který zjišťuje zda bylo s hardwarovými souÄástmi manipulováno." #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "Rekonstrukce TPM PCR0" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is invalid" msgstr "Rekonstrukce TPM PCR0 není platná" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is now valid" msgstr "Rekonstrukce TPM PCR0 je nyní platná" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM Platform Configuration" msgstr "Nastavení TPM platformy" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM Reconstruction" msgstr "Rekonstrukce TPM" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM empty PCRs" msgstr "Prázdné PCR registry v TPM" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "Å títek" msgstr[1] "Å títky" msgstr[2] "Å títků" msgstr[3] "Å títky" #. TRANSLATORS: we're saving all USB events for emulation msgid "Tagged for emulation" msgstr "OznaÄeno k emulaci" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "„PoskvrnÄ›no“" #. show the user the entire data blob msgid "Target" msgstr "Cíl" #. TRANSLATORS: command description msgid "Test a device using a JSON manifest" msgstr "VyzkouÅ¡et zařízení pomocí JSON manifestu" #. TRANSLATORS: when the release was tested msgid "Tested" msgstr "VyzkouÅ¡eno" #. TRANSLATORS: the %s is a vendor name, e.g. Lenovo #, c-format msgid "Tested by %s" msgstr "VyzkouÅ¡eno výrobcem %s" #. TRANSLATORS: someone we trust has tested this msgid "Tested by trusted vendor" msgstr "VyzkouÅ¡eno důvÄ›ryhodným výrobcem" #. TRANSLATORS: the boot entry was in a legacy format msgid "The EFI boot entry is not in hive format, and shim may not be new enough to read it." msgstr "Položka EFI zavádÄ›ní není ve formátu hive a může se stát, že shim nebude dostateÄnÄ› nový aby ji naÄetl." #. TRANSLATORS: try to treat the legacy format as a hive msgid "The EFI boot entry was not in hive format, falling back" msgstr "Položka EFI zavádÄ›ní systému nebyla ve formátu hive – bude použito náhradní Å™eÅ¡ení" #. TRANSLATORS: longer description msgid "The Intel Management Engine Key Manifest must be valid so that the device firmware can be trusted by the CPU." msgstr "Je tÅ™eba, aby manifest klíÄe správního engine (ME) byl platný, aby z pohledu procesoru mohl být firmware považován za důvÄ›ryhodný." #. TRANSLATORS: longer description msgid "The Intel Management Engine controls device components and needs to have a recent version to avoid security issues." msgstr "Intel Management Engine ovládá souÄásti zařízení a aby bylo zamezeno problémům se zabezpeÄením, je tÅ™eba mít jeho nejnovÄ›jší verzi." #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "LVFS je bezplatná služba, které funguje jako nezávislý právní subjekt a nemá žádné vazby na systém $OS_RELEASE:NAME$. Kompatibilita aktualizací firmware s pÅ™ipojenými zařízeními, Äi vámi používaným systémem, nemusí být poskytovatelem vámi používané distribuce ověřována. VeÅ¡kerý firmware je poskytován pouze přímo od výrobců daného vybavení." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Platform Configuration is used to check whether the device start process has been modified." msgstr "Nastavení TPM (Trusted Platform Module) platformy slouží k ověření, zda nebylo s procesem startu zařízení manipulováno." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Reconstruction is used to check whether the device start process has been modified." msgstr "TPM (Trusted Platform Module) Reconstruction slouží k ověření, zda nebylo s procesem startu zařízení manipulováno." #. TRANSLATORS: this is more background on a security measurement problem msgid "The TPM PCR0 differs from reconstruction." msgstr "PCR0 v TPM se liší od rekonstrukce." #. TRANSLATORS: longer description msgid "The UEFI Platform Key is used to determine if device software comes from a trusted source." msgstr "UEFI Platform Key slouží ke ověřování zda software na zařízení pochází z důvÄ›ryhodného zdroje." #. TRANSLATORS: HSI event title msgid "The UEFI certificate store is now up to date" msgstr "UEFI úložiÅ¡tÄ› certifikátů je nyní aktuální" #. TRANSLATORS: longer description msgid "The UEFI db contains the list of valid certificates that can be used to authorize what EFI binaries are allowed to run." msgstr "UEFI db obsahuje seznam platných certifikátů, které je možné použít pro pověřování toho, které EFI spustitelné soubory budou možné spouÅ¡tÄ›t." #. TRANSLATORS: longer description msgid "The UEFI system can set up memory attributes at boot which prevent common exploits from running." msgstr "UEFI systém může nastavovat atributy pamÄ›ti pÅ™i startu, které brání spouÅ¡tÄ›ní běžných exploitů" #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "Proces služby naÄetl spustitelný kód, pocházející od tÅ™etí strany. Vývojáři původního procesu služby už proto jeho podporu nemají ve svých rukou!" #. TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name #, c-format msgid "The firmware from %s is not supplied by %s, the hardware vendor." msgstr "Firmware z %s nepochází od %s (výrobce daného hardware)." #. TRANSLATORS: try to help msgid "The system clock has not been set correctly and downloading files may fail." msgstr "Systémové hodiny nemají správný Äas – stahování souborů se kvůli tomu nemusí zdaÅ™it." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been re-inserted." msgstr "Aktualizace bude pokraÄovat opÄ›tovném pÅ™ipojení USB kabelu." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged and then re-inserted." msgstr "Aktualizace bude pokraÄovat po odpojení a opÄ›tovném pÅ™ipojení USB kabelu." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged." msgstr "Aktualizace bude pokraÄovat po odpojení USB kabelu." #. TRANSLATORS: warning message msgid "The update will continue when the device power cable has been removed and re-inserted." msgstr "Aktualizace bude pokraÄovat po odpojení a opÄ›tovném pÅ™ipojení napájecího kabelu zařízení." #. TRANSLATORS: naughty vendor msgid "The vendor did not supply any release notes." msgstr "Výrobce neposkytl žádné poznámky k vydání." #. TRANSLATORS: now list devices with unfixed high-priority issues msgid "There are devices with issues:" msgstr "Jsou zde zařízení, která mají problémy:" #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "Nejsou zde žádné blokované soubory s firmware" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "Není k dispozici žádný schválený firmware." #. TRANSLATORS: %1 is the current device version number, and %2 is the #. command name, e.g. `fwupdmgr sync` #, c-format msgid "This device will be reverted back to %s when the %s command is performed." msgstr "Vykonáním příkazu %s bude toto zařízení vráceno zpÄ›t na verzi %s." #. TRANSLATORS: the vendor did not upload this msgid "This firmware is provided by LVFS community members and is not provided (or supported) by the original hardware vendor." msgstr "Tento firmware je poskytován Äleny komunity LVFS a není poskytován (ani podporován) původním výrobcem hardware." #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "Tento balíÄek nebyl ověřen – může se stát, že nebude fungovat správnÄ›." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "Tento program může správnÄ› fungovat jen pod uživatelem root" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "Tento vzdálený zdroj obsahuje firmware, který už sice je možné šířit, ale zatím je u výrobce stále jeÅ¡tÄ› ve stádiu testování. MÄ›li byste si zajistit, že znáte způsob, jak se ruÄnÄ› vrátit ke starší verzi firmwaru, kdyby pÅ™i aktualizaci doÅ¡lo k nezdaru." #. TRANSLATORS: error message msgid "This system doesn't support firmware settings" msgstr "Tento systém nepodporuje nastavování firmware" #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "Tento systém má problémy s bÄ›hovým prostÅ™edím HSI." #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "Tento systém má nízkou úroveň zabezpeÄení HSI." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "Tento nástroj správci umožňuje provádÄ›t UEFI dbx aktualizace." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "Tento nástroj správci umožňuje dotazovat a ovládat proces služby fwupd a tím provádÄ›t akce jako napÅ™. instalaci nebo návrat ke starší verzi firmware." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "Tento nástroj správci umožňuje použít zásuvné moduly pro fwupd bez toho, aby se instalovaly na hostitelský systém." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can add a kernel argument of '%s', but it will only be active after restarting the computer." msgstr "Tento nástroj může pÅ™idat argument jádra systému „%s“, ale toto se projeví až po restartu poÄítaÄe." #. TRANSLATORS: the %1 is a BIOS setting name. #. * %2 and %3 are the values, e.g. "True" or "Windows10" #, c-format msgid "This tool can change the BIOS setting '%s' from '%s' to '%s' automatically, but it will only be active after restarting the computer." msgstr "Tento nástroj může zmÄ›nit nastavení BIOS „%s“ z „%s“ na „%s“ automaticky, ale toto se projeví až po restartu poÄítaÄe." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can change the kernel argument from '%s' to '%s', but it will only be active after restarting the computer." msgstr "Tento nástroj může zmÄ›nit argument jádra systému z „%s“ na „%s“, ale toto se projeví až po restartu poÄítaÄe." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "Tento nástroj vyÄte ze systémového firmwaru záznam událostí v TPM a zpracuje ho." #. TRANSLATORS: the update state of the specific device msgid "Transient failure" msgstr "PÅ™echodný nezdar" #. TRANSLATORS: item is TRUE msgid "True" msgstr "Pravda" #. TRANSLATORS: We verified the metadata against the server msgid "Trusted metadata" msgstr "DůvÄ›ryhodná metadata" #. TRANSLATORS: We verified the payload against the server msgid "Trusted payload" msgstr "DůvÄ›ryhodný obsah" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Typ" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI Bootservice Variables" msgstr "UEFI Bootservice Variables" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition may not be set up correctly" msgstr "Je možné, že UEFI ESP oddíl není správnÄ› uspořádán" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "NezjiÅ¡tÄ›n (Äi nenastaven) UEFI ESP oddíl" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI Memory Protection" msgstr "UEFI ochrana pamÄ›ti" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "KlÃ­Ä platformy v UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "UEFI Secure Boot" #. TRANSLATORS: longer description msgid "UEFI Secure Boot prevents malicious software from being loaded when the device starts." msgstr "UEFI Secure Boot brání naÄtení záškodnickému softwaru pÅ™i spouÅ¡tÄ›ní zařízení." #. TRANSLATORS: longer description msgid "UEFI boot service variables should not be readable from runtime mode." msgstr "PromÄ›nné UEFI boot service by nemÄ›ly být Äitelné z bÄ›hového režimu." #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI bootservice variables" msgstr "PromÄ›nné UEFI bootservice" #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "Nejsou k dispozici (nebo jsou vypnuté v nastavení firmware) aktualizace typu UEFI capsule" #. TRANSLATORS: Title: is UEFI db up-to-date msgid "UEFI db" msgstr "UEFI db" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "Nástroj pro UEFI dbx" #. TRANSLATORS: system is not booted in UEFI mode msgid "UEFI firmware can not be updated in legacy BIOS mode" msgstr "UEFI firmware není možné aktualizovat, pokud je pÅ™epnutý do režimu legacy BIOS" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI memory protection" msgstr "UEFI ochrana pamÄ›ti" #. TRANSLATORS: HSI event title msgid "UEFI memory protection enabled and locked" msgstr "UEFI ochrana pamÄ›ti zapnuta a uzamÄena" #. TRANSLATORS: HSI event title msgid "UEFI memory protection enabled but not locked" msgstr "UEFI ochrana pamÄ›ti zapnuta, ale neuzamÄena" #. TRANSLATORS: HSI event title msgid "UEFI memory protection is now locked" msgstr "UEFI ochrana pamÄ›ti je nyní uzamÄena" #. TRANSLATORS: HSI event title msgid "UEFI memory protection is now unlocked" msgstr "UEFI ochrana pamÄ›ti je nyní odemÄena" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "KlÃ­Ä platformy v UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "UEFI secure boot" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "Nedaří se pÅ™ipojit ke službÄ›" #. TRANSLATORS: error message msgid "Unable to find attribute" msgstr "Atribut se nedaří nalézt" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "Odpojit stávající ovladaÄ" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "Odblokovává se firmware:" #. TRANSLATORS: command description msgid "Unblocks a specific firmware from being installed" msgstr "Odblokovat instalaci konkrétního firmwaru" #. TRANSLATORS: command description msgid "Undo the host security attribute fix" msgstr "Vrátit do stavu pÅ™ed opravou atributu zabezpeÄení stroje" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "NeÅ¡ifrováno" #. TRANSLATORS: command description msgid "Uninhibit the system to allow upgrades" msgstr "ZruÅ¡it inhibování systému a umožnit tak pÅ™echody na novÄ›jší verze" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Neznámo" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Neznámé zařízení" msgid "Unlock the device to allow access" msgstr "Odemknout zařízení a umožnit tak do nÄ›j přístup" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "OdemÄeno" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "Odemknout zařízení pro přístup k firmwaru" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "Odpojit ESP oddíl" #. TRANSLATORS: message shown after device has been marked for emulation msgid "Unplug and replug the device to continue the update process." msgstr "Aby bylo možné pokraÄovat v procesu aktualizace, zařízení odpojte a pÅ™ipojte nazpÄ›t." #. TRANSLATORS: firmware payload is unsigned and it is possible to modify it msgid "Unsigned Payload" msgstr "Nepodepsaný obsah" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "Nepodporovaná verze procesu služby %s, verze klienta je %s" #. TRANSLATORS: Suffix: the HSI result msgid "Untainted" msgstr "„NeposkvrnÄ›no“" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "Možné aktualizovat" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Chyba pÅ™i aktualizaci" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "Aktualizovat obraz" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "Zpráva z aktualizace" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "Stav aktualizace" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "Problém s nezdaÅ™enou aktualizací je už znám, další informace naleznete na této adrese:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Aktualizovat nyní?" #. TRANSLATORS: command description msgid "Update the stored cryptographic hash with current ROM contents" msgstr "Aktualizovat uložený kryptografický otisk stávajícím obsahem ROM pamÄ›ti" msgid "Update the stored device verification information" msgstr "Aktualizovat uložené informace ohlednÄ› ověření správnosti pro zařízení" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "Aktualizovat uložená metadata stávajícím obsahem" #. TRANSLATORS: command description msgid "Updates all specified devices to latest firmware version, or all devices if unspecified" msgstr "Aktualizuje firmware veÅ¡kerých zadaných zařízení na nejnovÄ›jší verzi, případnÄ› na vÅ¡ech zařízeních, pokud nejsou žádná vyjmenována" #. TRANSLATORS: how many local devices can expect updates now #, c-format msgid "Updates have been published for %u local device" msgid_plural "Updates have been published for %u of %u local devices" msgstr[0] "Pro %u lokální zařízení byly vydány aktualizace" msgstr[1] "Pro %u z %u lokálních zařízení byly vydány aktualizace" msgstr[2] "Pro %u z %u lokálních zařízení byly vydány aktualizace" msgstr[3] "Pro %u z %u lokálních zařízení byly vydány aktualizace" msgid "Updating" msgstr "Aktualizuje se" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "Aktualizace %s…" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "PÅ™ejít na novÄ›jší verzi u %s a to z %s na %s?" #. TRANSLATORS: ask the user to upload msgid "Upload data now?" msgstr "Odeslat data nyní?" #. TRANSLATORS: command description msgid "Upload the list of updatable devices to a remote server" msgstr "Odeslat seznam zařízení, která je možné aktualizovat, na vzdálený server" #. TRANSLATORS: ask the user to share, %s is something #. * like: "Linux Vendor Firmware Service" #, c-format msgid "Upload these anonymous results to the %s to help other users?" msgstr "Nahrát tyto anonymní výsledky na %s a pomoci tak ostatním uživatelům?" #. TRANSLATORS: explain why we want to upload #, c-format msgid "Uploading a device list allows the %s team to know what hardware exists, and allows us to put pressure on vendors that do not upload firmware updates for their hardware." msgstr "Odesílání seznamů zařízení umožní týmu %s se dozvÄ›dÄ›t, jaký hardware existuje a umožní nám vyvíjet tlak na výrobce, kteří nenahrávají aktualizace firmware pro jejich hardware." #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "Posíláním hlášení o firmwarech pomůžete výrobcům hardwaru rychle rozpoznat nezdaÅ™ené a úspěšné aktualizace na zařízeních, tak jak jsou provozována v reálných podmínkách." #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "Naléhavost" #. TRANSLATORS: explain how to get help, %1 is #. * 'fwupdtool --help' #. TRANSLATORS: explain how to get help, #. * where $1 is something like 'fwupdmgr --help' #, c-format msgid "Use %s for help" msgstr "NápovÄ›du vyvoláte pomocí %s" #. TRANSLATORS: CTRL^C [holding control, and then pressing C] will exit the #. program msgid "Use CTRL^C to cancel." msgstr "Zrušíte stisknutím CTRL+C." #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "Uživatel byl upozornÄ›n" #. TRANSLATORS: remote filename base msgid "Username" msgstr "Uživatelské jméno" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "VERSION1 VERSION2 [FORMAT]" msgstr "VERZE1 VERZE2 [FORMÃT]" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "Platné" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "Ověřování obsahu ESP oddílu…" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "Varianta" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "Výrobce" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "Ověřování zapsaného…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Verze" #. TRANSLATORS: the fwupd version the release was tested on msgid "Version[fwupd]" msgstr "Verze[fwupd]" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "VAROVÃNÃ" #. TRANSLATORS: command description msgid "Wait for a device to appear" msgstr "PoÄkat až se zařízení objeví" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "ÄŒekání…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "Sledovat zmÄ›ny hardwaru" #. TRANSLATORS: check various UEFI and ACPI tables are unchanged after the #. update msgid "Will measure elements of system integrity around an update" msgstr "Zkontroluje prvky neporuÅ¡enosti systému po aktualizaci" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "Zapisování souboru:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Zapisování…" #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from a recovery or installation disk, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "MÄ›li byste ověřit že dokážete vrátit nastavení zpÄ›t ze záchranného nebo instalaÄního média, protože tato zmÄ›na může způsobit že systém nenastartuje do Linuxu nebo způsobí jiné nestability systému." #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from the system firmware setup, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "MÄ›li byste ověřit že dokážete vrátit nastavení zpÄ›t z rozhraní firmware, protože tato zmÄ›na může způsobit že systém nenastartuje do Linuxu nebo způsobí jiné nestability systému." #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "Kompatibilita aktualizací firmware s pÅ™ipojenými zařízeními, Äi vámi používaným systémem, nemusí být poskytovatelem vámi používané distribuce ověřována. " #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "Použitím tohoto firmware může být hardware poÅ¡kozen a instalací tohoto vydání může být ztracena záruka od %s." #. TRANSLATORS: BKC is the industry name for the best known configuration and #. is a set #. * of firmware that works together #, c-format msgid "Your system is set up to the BKC of %s." msgstr "Váš systém má osvÄ›dÄené uspořádání %s." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[APPSTREAM_ID]" msgstr "[APPSTREAM_IDENTIF]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[KONTROLNÃ-SOUÄŒET]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[IDENTIF-ZAŘÃZENÃ|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[IDENTIF-ZAŘÃZENÃ|GUID] [VÄšTEV]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [VERSION]" msgstr "[IDENTIF-ZAŘÃZENÃ|GUID] [VERZE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE]" msgstr "[ZAŘÃZENÃ]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILE FILE_SIG REMOTE-ID]" msgstr "[SOUBOR PODPIS_SOUBORU IDENTIF-VZDÃLENÉHO-ZDROJE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[SOUBOR1] [SOUBOR2]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FWUPD-VERSION]" msgstr "[VERZE-FWUPD]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[REASON] [TIMEOUT]" msgstr "[DÅ®VOD] [ÄŒASOVÃ-LIMIT]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SECTION] KEY VALUE" msgstr "[SEKCE] KLÃÄŒ HODNOTA" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2] [--no-authenticate]" msgstr "[NASTAVENÃ1] [NASTAVENÃ2] [--no-authenticate]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2]..." msgstr "[NASTAVENi1] [NASTAVENI2]…" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[SOUBOR-S-SMBIOS|SOUBOR-S-IDENTIF-HARDWARE]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "výchozí" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "fwupd nástroj pro práci se záznamy událostí v TPM" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "zásuvné moduly pro fwupd" fwupd-2.0.10/po/da.po000066400000000000000000001722501501337203100142630ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # scootergrisen, 2019 # scootergrisen, 2019-2021 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Danish (http://app.transifex.com/freedesktop/fwupd/language/da/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: da\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "%.0f minut tilbage" msgstr[1] "%.0f minutter tilbage" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "%s Opdatering for batteri" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "%s opdatering af CPU-mikrokode" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "%s Opdatering for kamera" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "Opdatering for konfiguration %s" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "%s ME-opdatering for forbruger" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "Opdatering af controller %s" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "%s ME-opdatering for virksomhed" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "Enhedsopdatering for %s" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "Opdatering af indlejret controller %s" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "%s Opdatering for tastatur" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "ME-opdatering for %s" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "%s Opdatering for mus" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "Opdatering af netværksgrænseflade %s" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "Opdatering af lagercontroller %s" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "Systemopdatering for %s" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "%s Opdatering for TPM" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "Opdatering af Thunderbolt-controller %s" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "%s Opdatering for pegeplade" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "Opdatering for %s" #. TRANSLATORS: warn the user before updating, %1 is a device name #, c-format msgid "%s and all connected devices may not be usable while updating." msgstr "%s og alle tilsluttede enheder vil mÃ¥ske ikke være anvendelige under opdatering." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "%s-fabrikstilstand" #. TRANSLATORS: warn the user before #. * updating, %1 is a device name #, c-format msgid "%s must remain connected for the duration of the update to avoid damage." msgstr "%s skal være tilsluttet under hele opdateringen, for at undgÃ¥ skade." #. TRANSLATORS: warn the user before updating, %1 is a machine #. * name #, c-format msgid "%s must remain plugged into a power source for the duration of the update to avoid damage." msgstr "%s skal være tilsluttet en strømkilde under hele opdateringen, for at undgÃ¥ skade." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "%s-tilsidesættelse" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "%s-version" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u dag" msgstr[1] "%u dage" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "%u enhed har en tilgængelig firmwareopgradering." msgstr[1] "%u enheder har en tilgængelig firmwareopgradering." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u time" msgstr[1] "%u timer" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minut" msgstr[1] "%u minutter" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u sekund" msgstr[1] "%u sekunder" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(forældet)" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "Handling kræves:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "Aktivér enheder" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "Aktiverer afventende enheder" msgid "Activate the new firmware on the device" msgstr "Aktivér den nye firmware pÃ¥ enheden" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "Aktiverer firmwareopdatering" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "Aktiverer firmwareopdatering for" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Alder" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "Accepter og aktivér fjernen?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Alias til %s" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "Alle enheder af den samme type opdateres samtidigt" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Tillad nedgradering af firmwareversioner" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "Tillad geninstallering af eksisterende firmwareversioner" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "Tillad skift af firmwaregren" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "For at fuldføre en opdatering skal systemet genstartes." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "For at fuldføre en opdatering skal systemet lukkes ned." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Svar ja til alle spørgsmÃ¥l" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Anvend opdatering, selv nÃ¥r det ikke tilrÃ¥des" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Anvend opdateringsfiler" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "Anvender opdatering …" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Godkendt firmware:" msgstr[1] "Godkendt firmware:" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "Tilkobl til firmwaretilstand" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Autentificerer …" #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "Der kræves autentifikationsdetaljer" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Der kræves autentifikation for at nedgradere firmwaren pÃ¥ en flytbar enhed" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Der kræves autentifikation for at nedgradere firmwaren pÃ¥ maskinen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "Der kræves autentifikation for at redigere en konfigureret fjern som bruges til firmwareopdateringer" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "Der kræves autentifikation for at redigere dæmonkonfiguration" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "Der kræves autentifikation for at indstille listen over godkendt firmware" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "Der kræves autentifikation for at underskrive data med klientcertifikatet" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "Der kræves autentifikation for at skifte til den nye firmwareversion" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Der kræves autentifikation for at lÃ¥se enhed op" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Der kræves autentifikation for at opdatere firmwaren pÃ¥ en flytbar enhed" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Der kræves autentifikation for at opdatere firmwaren pÃ¥ maskinen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "Der kræves autentifikation for at opdatere de gemte checksumme for enheden" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "Automatisk rapportering" #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "Upload automatisk hver gang?" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "BUILDER-XML FILENAME-DST" msgstr "BUILDER-XML FILNAVN-DST" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "Bind ny kernedriver" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "Blokerede firmwarefiler:" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "Blokerer firmware:" #. TRANSLATORS: command description msgid "Blocks a specific firmware from being installed" msgstr "Blokerer en bestemt firmware fra at blive installeret" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "Opstartsindlæser version" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "Gren" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "Byg en firmwarefil" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Annuller" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "Annulleret" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "Kan ikke anvende eftersom dbx-opdatering allerede er blevet anvendt." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Ændret" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "Tjekker om den kryptografiske hash passer med firmwaren" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Checksum" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "Rydder resultaterne fra den sidste opdatering" #. TRANSLATORS: error message msgid "Command not found" msgstr "Kommandoen blev ikke fundet" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "Konverter en firmwarefil" #. TRANSLATORS: when the update was built msgid "Created" msgstr "Oprettet" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "Kritisk" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "Bekræftelse af kryptografisk hash er tilgængelig" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "Nuværende version" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "ENHEDS-ID|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Fejlsøgningsindstillinger" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "Udpakker …" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Beskrivelse" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "Frakobl til opstartsindlæsertilstand" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "Detaljer" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "Enhedsflag" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "Enheds-id" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Enhed tilføjet:" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "Enheden kan gendannes efter mislykkedes flash" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Enhed ændret:" #. TRANSLATORS: a version check is required for all firmware msgid "Device firmware is required to have a version check" msgstr "Enhedsfirmware kræves for at have et versiontjek" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "Enheden er lÃ¥st" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "Enhed kræves for at installere alle leverede udgivelser" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "Enheden er utilgængelig" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "Enheden kan anvendes under hele opdateringen" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Enhed fjernet:" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "Enhedstrin-opdateringer" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "Enheden understøtter at der kan skiftes til en anden gren med firmware" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "Enhedsopdatering behøver aktivering" #. TRANSLATORS: save the old firmware to disk before installing the new one msgid "Device will backup firmware before installing" msgstr "Enheden sikkerhedskopierer firmwaren inden installation" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "Enheden vises ikke igen nÃ¥r opdateringen er færdig" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "Enheder som det lykkedes at opdatere:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "Enheder som ikke blev opdateret korrekt:" #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "Enheder uden nogen tilgængelige firmwareopdateringer: " #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "Enheder med den seneste tilgængelige firmwareversion:" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "Deaktiveret" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "Deaktiverer en angivet fjern" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "Tjek ikke efter gammel metadata" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "Tjek ikke efter historik som ikke er blevet rapporteret" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "Tjek ikke om download af fjerne skal aktiveres" #. TRANSLATORS: command line option msgid "Do not check or prompt for reboot after update" msgstr "Tjek ikke eller spørg om genstart, efter opdatering" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "Medtag ikke præfiks for logdomæne" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "Medtag ikke tidsstempelpræfiks" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "Udfør ikke sikkerhedstjek af enhed" #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "Skriv ikke til historikdatabasen" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "ForstÃ¥r du de konsekvenser som er forbundet med ændring af firmwaregrenen?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "Vil du deaktivere funktionaliteten for fremtidige opdateringer?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "Vil du opdatere fjernen nu?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "Vil du uploade rapporter automatisk for fremtidige opdateringer?" #. TRANSLATORS: success msgid "Done!" msgstr "Færdig!" #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "Nedgrader %s fra %s til %s?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "Nedgraderer firmwaren pÃ¥ en enhed" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "Nedgraderer %s …" #. TRANSLATORS: command description msgid "Download a file" msgstr "Download en fil" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Downloader …" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "Dump SMBIOS-data fra en fil" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "Varighed" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "Aktivér ny fjern?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "Aktivér fjernen?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Aktiveret" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "Aktiverer en angivet fjern" msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "Aktivering af funktionen sker pÃ¥ egen risiko. Det betydet at du skal kontakte din oprindelige udstyrsproducent vedrørende eventuelle problemer forÃ¥rsaget af opdateringerne. Det er kun problemer med selv opdateringsprocessen som skal indsende pÃ¥ $OS_RELEASE:BUG_REPORT_URL$." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "Aktivering af fjernen sker pÃ¥ egen risiko." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Krypteret" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "Krypteret RAM" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "Slet al historik over firmwareopdateringer" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Sletter …" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "Afslut efter en lille forsinkelse" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "Afslut efter motoren er indlæst" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "Eksportér en firmwarefilstruktur til XML" #. TRANSLATORS: command description msgid "Extract a firmware blob to images" msgstr "Udpak en firmwareblob til aftryk" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "FIL" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "FIL [ENHEDS-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "FILNAVN" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "FILNAVN CERTIFIKAT PRIVAT-NØGLE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "FILNAVN ENHEDS-ID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "FILNAVN [ENHEDS-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "FILNAVN [FIRMWARETYPE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" msgstr "FILNAVN-KILDE FILNAVN-DST [FIRMWARETYPE-KILDE] [FIRMWARE-TYPE-DST]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "Mislykkedes" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Kunne ikke anvende opdatering" #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "Kunne ikke oprette forbindelse til dæmonen" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "Kunne ikke indlæse lokal dbx" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "Kunne ikke indlæse systemets dbx" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "Kunne ikke lÃ¥se" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Kunne ikke fortolke argumenter" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "Kunne ikke fortolke fil" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "Kunne ikke fortolke lokal dbx" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "Kunne ikke validere ESP-indhold" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Filnavn" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "Filnavnets underskrift" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "Filnavnets kilde" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Filnavn kræves" #. TRANSLATORS: command line option msgid "Filter with a set of device flags using a ~ prefix to exclude, e.g. 'internal,~needs-reboot'" msgstr "Filtrer med et sæt enhedsflag med et ~-præfiks for at udelukke, f.eks. 'intern,~behøver-genstart'" #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "Grund-URI for firmware" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "D-Bus-tjeneste for firmwareopdatering" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Firmwareopdateringsdæmon" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "Firmwareredskab" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "Firmwareattest" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "Firmware er allerede blokeret" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "Firmware er ikke allerede blokeret" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "Firmwaremetadata er ikke blevet opdateret i %u dag og kan være forældet." msgstr[1] "Firmwaremetadata er ikke blevet opdateret i %u dage og kan være forældet." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "Firmwareopdateringer" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "Flag" #. TRANSLATORS: command line option msgid "Force the action by relaxing some runtime checks" msgstr "Gennemtving handlingen ved at afslappe visse runtimetjek" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Fundet" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" msgstr[1] "GUID'er" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "Hent alle enhedsflag som understøttes af fwupd" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Hent alle enheder som understøtter firmwareopdateringer" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "Hent alle aktiverede plugins som er registreret med systemet" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Hent detaljer om en firmwarefil" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "Henter de konfigurerede fjerne" #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "Henter værtens sikkerhedsattributter" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "Henter listen over godkendt firmware" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "Henter listen over blokerede firmware" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Henter listen over opdateringer for tilsluttet hardware" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "Henter resultaterne fra en enhed" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Henter resultaterne fra den sidste opdatering" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "HWID'ER-FIL" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "Hardwaren venter pÃ¥ at bliver gentilkoblet" #. TRANSLATORS: the release urgency msgid "High" msgstr "Høj" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "Værtens sikkerheds-ID:" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "Inaktiv …" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "Ignorer strikse SSL-tjek ved download af filer" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "Ignorer mislykkede checksum for firmware" #. TRANSLATORS: command line option msgid "Ignore firmware hardware mismatch failures" msgstr "Ignorer mislykkedes firmwarehardware som ikke passer sammen" #. TRANSLATORS: try to help msgid "Ignoring SSL strict checks, to do this automatically in the future export DISABLE_SSL_STRICT in your environment" msgstr "Ignorerer strikse SSL-tjeks. Eksportér DISABLE_SSL_STRICT i dit miljø for at gøre det automatisk i fremtiden" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "Varighed for installation" msgid "Install signed device firmware" msgstr "Installer enhedsfirmware der er underskrevet" msgid "Install signed system firmware" msgstr "Installer systemfirmware der er underskrevet" msgid "Install unsigned device firmware" msgstr "Installer enhedsfirmware der ikke er underskrevet" msgid "Install unsigned system firmware" msgstr "Installer systemfirmware der ikke er underskrevet" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "Installerer firmwareopdateringer …" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "Installerer pÃ¥ %s …" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "Intel BootGuard ACM-beskyttet" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "Intel BootGuard OTP-fuse" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Intel BootGuard regler om fejl" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Intel BootGuard bekræftet start" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "Intern enhed" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "Ugyldig" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "Er i opstartsindlæsertilstand" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "Problemstilling" msgstr[1] "Problemstillinger" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "PLACERING" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "Sidst redigeret" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "Mindre end et minut tilbage" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "Licens" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Linux Vendor Firmware Service (stabilt firmware)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Linux Vendor Firmware Service (testning firmware)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Linux-kerne" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Nedlukning af Linux-kerne" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Linux-swap" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "Vis poster i dbx" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "Vis de tilgængelige firmwaretyper" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "Viser filer pÃ¥ ESP'en" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "Indlæser …" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "LÃ¥st" #. TRANSLATORS: the release urgency msgid "Low" msgstr "Lav" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "MEI-fabrikstilstand" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "MEI-tilsidesættelse" msgid "MEI version" msgstr "MEI-version" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "Aktivér bestemte plugins manuelt" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "Mellem" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "Underskrift for metadata" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "Metadata-URI" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "Metadata kan hentes fra Linux Vendor Firmware Service." #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "Minimum version" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "Rediger en værdi i dæmonkonfiguration" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "Redigerer en angivet fjern" msgid "Modify a configured remote" msgstr "Rediger en konfigureret fjern" msgid "Modify daemon configuration" msgstr "Rediger dæmonkonfiguration" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "OvervÃ¥g dæmonen for hændelser" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "Monterer ESP'en" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "Genstart efter installation er nødvendig" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "Genstart er nødvendig" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "Nedlukning efter installation er nødvendig" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "Ny version" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "Der er ikke angivet nogen handling!" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "Ingen nedgraderinger til %s" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "Fandt ingen firmware-id'er" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Der blev ikke fundet nogen hardware med firmware som kan opdateres" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "Ingen tilgængelige udgivelser" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "Der er ikke nogen metadata da der ikke er aktiveret nogen fjerne." #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "Ingen tilgængelige fjerne" msgid "No updates available for remaining devices" msgstr "Ingen opdateringer tilgængelige for tilbageværende enheder" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "Ikke fundet" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "Ikke-understøttet" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "OK" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "Vis kun én PCR-værdi" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "Kun versionsopgraderinger er tilladte" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "Tilsidesæt standard-ESP-stien" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "STI" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "Fortolk og vis deltaljer om en firmwarefil" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "Fortolker dbx-opdatering …" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "Fortolker systemets dbx …" #. TRANSLATORS: remote filename base msgid "Password" msgstr "Adgangskode" msgid "Payload" msgstr "Nyttelast" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "Afventer" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "Udfør handling?" #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "Indtast venligst et tal nummer 0 og %u: " #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "Plugin-afhængigheder mangler" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "Præopstart DMA-beskyttelse" #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "Forrige version" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Prioritet" msgid "Proceed with upload?" msgstr "Fortsæt med upload?" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "Proprietær" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "FJERN-ID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID KEY VALUE" msgstr "FJERN-ID NØGLE VÆRDI" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "Læs en firmwareblob fra en enhed" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "Læser fra %s …" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Læser …" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "Opdater metadata fra fjernserver" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "Geninstaller %s til %s?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "Geninstaller den nuværende firmware pÃ¥ enheden" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "Geninstaller firmware pÃ¥ en enhed" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "Udgivelsesgren" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "Fjern-id" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "Rapport-URI" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "Rapporteret til fjernserver" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "Krævede efivarfs-filsystem blev ikke fundet" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "Det krævede hardware blev ikke fundet" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "Kræver en opstartsindlæser" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Kræver internetforbindelse" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Genstart nu?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "Genstart dæmonen sÃ¥ ændringerne kan træde i kraft?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Genstarter enhed …" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "Returner alle maskinens hardware-id'er" #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "Kør oprydningsrutinen for pluginkomposition nÃ¥r install-blob bruges" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "Kør forberedelsesrutinen for pluginkomposition nÃ¥r install-blob bruges" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "Kørende kerne er for gammel" #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "Suffiks for runtime" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "SPI BIOS-beskriver" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "SPI BIOS-region" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "SPI-lÃ¥s" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "SPI-skriv" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "UNDERSYSTEM DRIVER [ENHEDS-ID|GUID]" #. TRANSLATORS: command description msgid "Save a file that allows generation of hardware IDs" msgstr "Gem en fil der gør det muligt at generere hardware-id'er" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Planlægger …" #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "Se %s for mere information." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Valgte enhed" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Valgte diskomrÃ¥de" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "Serienummer" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Indstiller listen over godkendt firmware" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "Del historik over firmware med udviklerne" #. TRANSLATORS: command line option msgid "Show all results" msgstr "Vis alle resultater" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "Vis klient- og dæmonversioner" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "Vis uddybende information for dæmon for et bestemt domæne" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "Vis fejlsøgningsinformation for alle domæner" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Vis fejlsøgningsindstillinger" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Vis enheder som ikke kan opdateres" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Vis ekstra fejlsøgningsinformation" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "Vis historik over firmwareopdateringer" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "Vis den udregnede version af dbx'en" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "Luk ned nu?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "Underskriv en firmware med en ny nøgle" msgid "Sign data using the client certificate" msgstr "Underskriv data med klientcertifikat" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "Underskriv data med klientcertifikat" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "Underskriv den uploadede data med klientcertifikatet" msgid "Signature" msgstr "Underskrift" #. TRANSLATORS: file size of the download msgid "Size" msgstr "Størrelse" #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "Kilde" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "Angiv dbx-databasefilen" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "Lykkedes" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "Det lykkedes at aktivere alle enheder" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "Det lykkedes at deaktivere fjern" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "Det lykkedes at downloade ny metadata: " #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "Det lykkedes at aktivere og opdatere fjernen" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "Det lykkedes at aktivere fjern" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "Det lykkedes at installere firmware" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "Det lykkedes at redigere konfigurationsværdi" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "Det lykkedes at redigere fjern" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "Det lykkedes at opdatere metadata manuelt" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "Opdatering af enhedens tjeksumme lykkedes" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "Det lykkedes at uploade %u rapport" msgstr[1] "Det lykkedes at uploade %u rapporter" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "Bekræftelse af enhedens tjeksumme lykkedes" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Opsummering" #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "Understøttet" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "Understøttes pÃ¥ fjernserver" #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "Suspender-til-inaktiv" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "Suspender-til-ram" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "Skift gren fra %s til %s?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "Skift firmwaregrenen pÃ¥ enheden" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "Systemet kræver ekstern strømkilde" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "TEKST" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "TPM PCR0-rekonstruktion" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "Tainted" #. show the user the entire data blob msgid "Target" msgstr "MÃ¥l" #. TRANSLATORS: command description msgid "Test a device using a JSON manifest" msgstr "Test en enhed med et JSON-manifest" #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "LVFS'en er en gratis tjeneste der opererer som en selvstændig juridisk enhed og har ingen forbindelse med $OS_RELEASE:NAME$. Din distributør har mÃ¥ske ikke bekræftet nogen af firmwareopdateringerne for kompatibilitet med dit system eller tilsluttede enheder. Al firmware leveres kun af den oprindelige udstyrsproducent." #. TRANSLATORS: this is more background on a security measurement problem msgid "The TPM PCR0 differs from reconstruction." msgstr "TPM PCR0'en er ikke magen til rekonstruktionen." #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "Dæmonen har indlæst tredjepartskode og understøttes ikke længere af opstrømsudviklerne!" #. TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name #, c-format msgid "The firmware from %s is not supplied by %s, the hardware vendor." msgstr "Firmwaren fra %s leveres ikke af hardwareleverandøren %s." #. TRANSLATORS: try to help msgid "The system clock has not been set correctly and downloading files may fail." msgstr "Systemets ur er ikke blevet indstillet korrekt og download af filer kan mislykkes." #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "Der er ikke nogen blokerede firmwarefiler" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "Der er ikke nogen godkendt firmware." #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "Pakken er ikke blevet valideret — den virker mÃ¥ske ikke ordentligt." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "Programmet virker mÃ¥ske kun korrekt som root" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "Fjernen indeholder firmware som der ikke er embargo pÃ¥, men som stadigvæk testes af hardwareproducenten. Du skal sikre dig at du har en manuel mÃ¥de til at nedgradere firmwaren pÃ¥ hvis firmwareopdateringen mislykkedes." #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "Systemet har problemer med HSI-runtime." #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "Systemet har et lavt HSI-sikkerhedsniveau." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "Værktøjet giver en administrator mulighed for at anvende UEFI-dbx-opdateringer." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "Værktøjet giver en administrator mulighed for at sætte i kø og styre fwupd-dæmonen, hvorved denne kan udføre handlinger sÃ¥som at installere eller nedgradere firmware." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "Værktøjet giver en administrator mulighed for at bruge fwupd-plugins uden at installere dem pÃ¥ værtssystemet." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "Værktøjet læser og fortolker TPM-hændelsesloggen fra systemfirmwaren." #. TRANSLATORS: the update state of the specific device msgid "Transient failure" msgstr "ForbigÃ¥ende mislykkedes" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Type" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "UEFI ESP-partition ikke registreret eller konfigureret" #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "UEFI-kapselopdateringer ikke tilgængelige eller aktiveret i firmwareopsætning" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "UEFI-dbx-redskab" #. TRANSLATORS: system is not booted in UEFI mode msgid "UEFI firmware can not be updated in legacy BIOS mode" msgstr "UEFI-firmware kan ikke opdateres i forældet BIOS-tilstand" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "UEFI-platformsnøgle" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "UEFI sikkeropstart" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "Kan ikke oprette forbindelse til tjeneste" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "Fjern binding af nuværende driver" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "Fjerner blokering af firmware:" #. TRANSLATORS: command description msgid "Unblocks a specific firmware from being installed" msgstr "Fjerner blokering af en bestemt firmware fra at blive installeret" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "Dekrypteret" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Ukendt" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Ukendt enhed" msgid "Unlock the device to allow access" msgstr "LÃ¥s enheden op for at tillade adgang" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "LÃ¥st op" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "LÃ¥ser op for enheden for at fÃ¥ adgang til firmwaren" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "Afmonterer ESP'en" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "Ikke-understøttet dæmonversion %s, klientversionen er %s" #. TRANSLATORS: Suffix: the HSI result msgid "Untainted" msgstr "Untainted" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "Kan opdateres" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Fejl ved opdatering" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "Opdateringsmeddelelse" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "Opdateringstilstand" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "Opdateringer som mislykkes er et velkendt problem. Besøg URL'en for mere information:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Opdater nu?" #. TRANSLATORS: command description msgid "Update the stored cryptographic hash with current ROM contents" msgstr "Opdater den gemte kryptografiske hash med indholdet fra den nuværende ROM" msgid "Update the stored device verification information" msgstr "Opdaterer de gemte informationer om enhedsverifikation" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "Opdater det gemte metadata med det nuværende indhold" msgid "Updating" msgstr "Opdaterer" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "Opdaterer %s …" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "Opgrader %s fra %s til %s?" #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "Upload af firmwarerapporter hjælper hardwareproducenter til hurtigt at identificere opdateringer som fejlede og lykkedes pÃ¥ rigtigt hardware." #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "Vigtighed" #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "Brugeren er blevet underrettet" #. TRANSLATORS: remote filename base msgid "Username" msgstr "Brugernavn" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "Gyldig" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "Validerer ESP-indhold …" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "Variant" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "Producent" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "Verificerer …" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Version" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "Venter …" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "Hold øje med hardwareændringer" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "Skriver fil:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Skriver …" #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "Din distributør har mÃ¥ske ikke verificeret kompatibiliteten af firmwareopdateringerne med dit system eller tilsluttede enheder." #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "Der er mulighed for at din hardware kan tage skade hvis firmwaren bruges og installation af udgivelsen kan ugyldiggøre garantien med %s." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[CHECKSUM]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[ENHEDS-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[ENHEDS-ID|GUID] [GREN]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILE FILE_SIG REMOTE-ID]" msgstr "[FIL FILUNDERSKRIFT FJERN-ID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[FILNAVN1] [FILNAVN2]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[SMBIOS-FIL|HWID'ER-FIL]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "standard" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "fwupd TPM-hændelseslogredskab" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "fwupd-plugins" fwupd-2.0.10/po/de.po000066400000000000000000003753671501337203100143050ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Alexander Barton, 2024 # Christian , 2023 # Ettore Atalan , 2018,2021-2025 # F Bausch, 2024 # Marco Tedaldi , 2015 # Mario Blättermann , 2025 # Wolfgang Stöggl , 2015 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: German (http://app.transifex.com/freedesktop/fwupd/language/de/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: de\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "%.0f Minute verbleibt" msgstr[1] "%.0f Minuten verbleiben" #. TRANSLATORS: BMC refers to baseboard management controller which #. * is the device that updates all the other firmware on the system #, c-format msgid "%s BMC Update" msgstr "%s BMC-Aktualisierung" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "%s Akku-Aktualisierung" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "%s CPU-Microcode-Aktualisierung" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "%s Kameraaktualisierung" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "%s Konfigurationsaktualisierung" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "%s Aktualisierung der ME für Verbraucher" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "%s Controller-Aktualisierung" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "%s Aktualisierung der ME für Unternehmen" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "%s Geräteaktualisierung" #. TRANSLATORS: Video Display refers to the laptop internal display or #. * external monitor #, c-format msgid "%s Display Update" msgstr "%s Bildschirmaktualisierung" #. TRANSLATORS: Dock refers to the port replicator hardware laptops are #. * cradled in, or lowered onto #, c-format msgid "%s Dock Update" msgstr "%s Dock-Aktualisierung" #. TRANSLATORS: drive refers to a storage device, e.g. SATA disk #, c-format msgid "%s Drive Update" msgstr "%s Laufwerksaktualisierung" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "%s Eingebetteter-Controller-Aktualisierung" #. TRANSLATORS: a device that can read your fingerprint pattern #, c-format msgid "%s Fingerprint Reader Update" msgstr "%s Fingerabdruckleser-Aktualisierung" #. TRANSLATORS: flash refers to solid state storage, e.g. UFS or eMMC #, c-format msgid "%s Flash Drive Update" msgstr "%s Flash-Laufwerk-Aktualisierung" #. TRANSLATORS: GPU refers to a Graphics Processing Unit, e.g. #. * the "video card" #, c-format msgid "%s GPU Update" msgstr "%s GPU-Aktualisierung" #. TRANSLATORS: a large pressure-sensitive drawing area typically used #. * by artists and digital artists #, c-format msgid "%s Graphics Tablet Update" msgstr "%s Grafiktablett-Aktualisierung" #. TRANSLATORS: two miniature speakers attached to your ears #, c-format msgid "%s Headphones Update" msgstr "%s Kopfhörer-Aktualisierung" #. TRANSLATORS: headphones with an integrated microphone #, c-format msgid "%s Headset Update" msgstr "%s Headset-Aktualisierung" #. TRANSLATORS: an input device used by gamers, e.g. a joystick #, c-format msgid "%s Input Controller Update" msgstr "%s Eingabe-Controller-Aktualisierung" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "%s Tastaturaktualisierung" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "%s ME-Aktualisierung" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "%s Mausaktualisierung" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "%s Netzwerkschnittstellenaktualisierung" #. TRANSLATORS: SSD refers to a Solid State Drive, e.g. non-rotating #. * SATA or NVMe disk #, c-format msgid "%s SSD Update" msgstr "%s SSD-Aktualisierung" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "%s Speicher-Controller-Aktualisierung" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "%s Systemaktualisierung" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "%s TPM-Aktualisierung" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "%s Thunderbolt-Controller-Aktualisierung" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "%s Tastfeld-Aktualisierung" #. TRANSLATORS: Dock refers to the port replicator device connected #. * by plugging in a USB cable -- which may or may not also provide power #, c-format msgid "%s USB Dock Update" msgstr "%s USB-Dock-Aktualisierung" #. TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth #. * device that stays in the USB port so the wireless peripheral works #, c-format msgid "%s USB Receiver Update" msgstr "%s USB-Empfänger-Aktualisierung" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "%s Aktualisierung" #. TRANSLATORS: warn the user before updating, %1 is a device name #, c-format msgid "%s and all connected devices may not be usable while updating." msgstr "%s und alle angeschlossenen Geräte sind während der Aktualisierung möglicherweise nicht nutzbar." #. TRANSLATORS: %1 refers to some kind of security test, e.g. "Encrypted RAM". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s appeared: %s" msgstr "%s erschienen: %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "UEFI platform #. key". #. * %2 and %3 refer to results value, e.g. "Valid" and "Invalid" #, c-format msgid "%s changed: %s → %s" msgstr "%s geändert: %s → %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "SPI BIOS #. region". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s disappeared: %s" msgstr "%s verschwunden: %s" #. TRANSLATORS: the device has a reason it can't update, e.g. laptop lid #. closed #, c-format msgid "%s is not currently updatable" msgstr "%s ist derzeit nicht aktualisierbar" #. TRANSLATORS: %1 is a device name, e.g. "ThinkPad Universal #. * ThunderBolt 4 Dock" and %2 is "fwupdmgr activate" #, c-format msgid "%s is pending activation; use %s to complete the update." msgstr "%s ist noch nicht aktiviert; verwenden Sie %s, um die Aktualisierung abzuschließen." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "%s Herstellungsmodus" #. TRANSLATORS: warn the user before #. * updating, %1 is a device name #, c-format msgid "%s must remain connected for the duration of the update to avoid damage." msgstr "%s muss während der gesamten Dauer der Aktualisierung angeschlossen bleiben, um Schäden zu vermeiden." #. TRANSLATORS: warn the user before updating, %1 is a machine #. * name #, c-format msgid "%s must remain plugged into a power source for the duration of the update to avoid damage." msgstr "%s muss während der gesamten Dauer der Aktualisierung an eine Stromquelle angeschlossen bleiben, um Schäden zu vermeiden." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "%s überschreiben" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "%s Version" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u Tag" msgstr[1] "%u Tage" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "Für %u Gerät ist eine Firmware-Aktualisierung verfügbar." msgstr[1] "Für %u Geräte ist eine Firmware-Aktualisierung verfügbar." #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device is not the best known configuration." msgid_plural "%u devices are not the best known configuration." msgstr[0] "%u Gerät ist nicht die beste bekannte Konfiguration." msgstr[1] "%u Geräte sind nicht die beste bekannte Konfiguration." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u Stunde" msgstr[1] "%u Stunden" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u Minute" msgstr[1] "%u Minuten" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u Sekunde" msgstr[1] "%u Sekunden" #. TRANSLATORS: device tests can be specific to a CPU type #, c-format msgid "%u test was skipped" msgid_plural "%u tests were skipped" msgstr[0] "%u Test wurde übersprungen" msgstr[1] "%u Tests wurden übersprungen" #. TRANSLATORS: first percentage is current value, 2nd percentage is the #. * lowest limit the firmware update is allowed for the update to happen #, c-format msgid "%u%% (threshold %u%%)" msgstr "%u%% (Schwellenwert %u%%)" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(veraltet)" #. TRANSLATORS: HSI event title msgid "A TPM PCR is now an invalid value" msgstr "Ein TPM PCR ist jetzt ein ungültiger Wert" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "AMD Firmware Replay Protection" msgstr "AMD-Firmware-Wiederholungsschutz" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "AMD Firmware Write Protection" msgstr "AMD-Firmware-Schreibschutz" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "AMD Secure Processor Rollback Protection" msgstr "Rückrollschutz des AMD Sicherheitsprozessors" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "ARCHIVE FIRMWARE METAINFO [FIRMWARE] [METAINFO] [JCATFILE]" msgstr "ARCHIV FIRMWARE METAINFO [FIRMWARE] [METAINFO] [JCAT-DATEI]" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "Aktion erforderlich:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "Geräte aktivieren" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "Ausstehende Geräte aktivieren" msgid "Activate the new firmware on the device" msgstr "Die neue Firmware auf dem Gerät aktivieren" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "Firmware-Aktualisierung wird aktiviert" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "Firmware-Aktualisierung wird aktiviert für" #. TRANSLATORS: command description msgid "Adds devices to watch for future emulation" msgstr "Fügt Geräte hinzu, die für künftige Emulationen zu beobachten sind" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Alter" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "Zustimmen und die Gegenstelle aktivieren?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Verweis auf %s" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are now valid" msgstr "Alle TPM PCRs sind jetzt gültig" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are valid" msgstr "Alle TPM PCRs sind gültig" #. TRANSLATORS: an application is preventing system updates msgid "All devices are prevented from update by system inhibit" msgstr "Alle Geräte werden durch eine Systemblockierung an der Aktualisierung gehindert" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "Alle Geräte desselben Typs werden zur gleichen Zeit aktualisiert" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Herabstufung von Firmware-Versionen zulassen" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "Neuinstallation vorhandener Firmware-Versionen erlauben" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "Wechsel des Firmware-Zweigs erlauben" #. TRANSLATORS: error message msgid "Already exists, and no --force specified" msgstr "Bereits vorhanden, und kein --force angegeben" #. TRANSLATORS: is not the main firmware stream msgid "Alternate branch" msgstr "Alternativer Zweig" #. TRANSLATORS: another application is updating the device already msgid "An update is in progress" msgstr "Eine Aktualisierung ist in Bearbeitung" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "Ein Neustart ist erforderlich, um eine Aktualisierung abzuschließen." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "Für eine Aktualisierung muss das System zum Abschluss heruntergefahren werden." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Alle Fragen mit Ja beantworten" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Aktualisierung auch dann anwenden, wenn dies nicht empfohlen wird" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Aktualisierungsdateien anwenden" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "Aktualisierung wird angewendet …" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Genehmigte Firmware:" msgstr[1] "Genehmigte Firmware:" #. TRANSLATORS: command description msgid "Asks the daemon to quit" msgstr "Fordert den Daemon zum Beenden auf" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "An den Firmware-Modus anhängen" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Authentifizierung …" #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "Authentifizierungsdetails sind erforderlich" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Zum Herabstufen der Firmware auf einem entfernbaren Gerät ist eine Authentifizierung erforderlich" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Zum Herabstufen der Firmware auf diesem System ist eine Authentifizierung erforderlich" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to enable emulation data collection" msgstr "Zum Aktivieren der Emulationsdatenerfassung ist eine Authentifizierung erforderlich." #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to fix a host security issue" msgstr "Zur Behebung eines Host-Sicherheitsproblems ist eine Authentifizierung erforderlich" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to load hardware emulation data" msgstr "Zum Laden von Hardware-Emulationsdaten ist eine Authentifizierung erforderlich" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "Zum Ändern der BIOS-Einstellungen ist eine Authentifizierung erforderlich" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "Zum Ändern einer konfigurierten Gegenstelle, die für Firmware-Aktualisierungen verwendet wird, ist eine Authentifizierung erforderlich" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "Zum Ändern der Daemon-Konfiguration ist eine Authentifizierung erforderlich" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "Zum Lesen der BIOS-Einstellungen ist eine Authentifizierung erforderlich" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to reset daemon configuration to defaults" msgstr "Zum Zurücksetzen der Daemon-Konfiguration auf die Standardeinstellungen ist eine Authentifizierung erforderlich" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to save hardware emulation data" msgstr "Zum Speichern von Hardware-Emulationsdaten ist eine Authentifizierung erforderlich" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "Zum Festlegen der Liste der zugelassenen Firmware ist eine Authentifizierung erforderlich" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "Zum Signieren von Daten mit dem Client-Zertifikat ist eine Authentifizierung erforderlich" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to stop the firmware update service" msgstr "Zum Stoppen des Firmware-Aktualisierungsdienstes ist eine Authentifizierung erforderlich" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "Zum Wechsel auf die neue Firmware-Version ist eine Authentifizierung erforderlich" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to undo the fix for a host security issue" msgstr "Zum Zurücknehmen der Behebung eines Host-Sicherheitsproblems ist eine Authentifizierung erforderlich" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Zum Entsperren eines Geräts ist eine Authentifizierung erforderlich" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Zum Aktualisieren der Firmware auf einem entfernbaren Gerät ist eine Authentifizierung erforderlich" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Zum Aktualisieren der Firmware auf diesem System ist eine Authentifizierung erforderlich" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "Zur Aktualisierung der gespeicherten Prüfsummen für das Gerät ist eine Authentifizierung erforderlich" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "Automatische Berichterstattung" #. TRANSLATORS: we can auto-uninhibit after a timeout #, c-format msgid "Automatically uninhibiting in %ums…" msgstr "Wird in %u ms automatisch entriegelt..." #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "Jedes Mal automatisch hochladen?" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS Firmware Updates" msgstr "BIOS Firmware-Aktualisierungen" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS Rollback Protection" msgstr "BIOS-Rückrollschutz" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS firmware updates" msgstr "BIOS Firmware-Aktualisierungen" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS rollback protection" msgstr "BIOS-Rückrollschutz" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "BIOS-Aktualisierungen werden über LVFS oder Windows Update bereitgestellt" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "BUILDER-XML FILENAME-DST" msgstr "BUILDER-XML DATEINAME-ZIEL" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "Akku" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "Neuen Kernel-Treiber einbinden" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "Blockierte Firmware-Dateien:" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "Blockierte Version" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "Firmware wird blockiert:" #. TRANSLATORS: command description msgid "Blocks a specific firmware from being installed" msgstr "Blockiert die Installation einer bestimmten Firmware" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "Bootloader-Version" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "Zweig" #. TRANSLATORS: command description msgid "Build a cabinet archive from a firmware blob and XML metadata" msgstr "Cabinet-Archiv aus einem Firmware-Blob und XML-Metadaten erstellen" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "Eine Firmware-Datei bauen" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * Utilized by OS means the distribution enabled it msgid "CET OS Support" msgstr "CET-BS-Unterstützung" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * enabled means supported by the processor msgid "CET Platform" msgstr "CET-Plattform" #. TRANSLATORS: longer description msgid "CPU Microcode must be updated to mitigate against various information-disclosure security issues." msgstr "Der CPU-Mikrocode muss aktualisiert werden, um verschiedene Sicherheitsprobleme bei der Informationspreisgabe abzumildern." #. TRANSLATORS: we can save all device enumeration events for emulation msgid "Can tag for emulation" msgstr "Kann für die Emulation markieren" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Abbrechen" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "Abgebrochen" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "Eine bereits angewandte dbx-Aktualisierung kann nicht angewendet werden." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Geändert" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "Überprüft, ob der kryptografische Hash mit der Firmware übereinstimmt" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Prüfsumme" #. TRANSLATORS: get interactive prompt, where branch is the #. * supplier of the firmware, e.g. "non-free" or "free" msgid "Choose branch" msgstr "Zweig auswählen" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "Gerät auswählen" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "Firmware auswählen" #. TRANSLATORS: get interactive prompt msgid "Choose release" msgstr "Version auswählen" #. TRANSLATORS: get interactive prompt msgid "Choose volume" msgstr "Datenträger auswählen" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "Bereinigt die Ergebnisse der letzten Aktualisierung" #. TRANSLATORS: error message msgid "Command not found" msgstr "Befehl nicht gefunden" #. TRANSLATORS: is not supported by the vendor msgid "Community supported" msgstr "Von der Gemeinschaft unterstützt" #. TRANSLATORS: command description msgid "Compares two versions for equality" msgstr "Vergleicht zwei Versionen auf Gleichheit" #. TRANSLATORS: title prefix for the BIOS settings dialog msgid "Configuration Change Suggested" msgstr "Vorgeschlagene Konfigurationsänderung" #. TRANSLATORS: no peeking msgid "Configuration is only readable by the system administrator" msgstr "Konfiguration ist nur für den Systemadministrator lesbar" #. TRANSLATORS: longer description msgid "Control-Flow Enforcement Technology detects and prevents certain methods for running malicious software on the device." msgstr "Die Kontrollfluss-Durchsetzungstechnologie erkennt und verhindert bestimmte Methoden zur Ausführung bösartiger Software auf dem Gerät." #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology msgid "Control-flow Enforcement Technology" msgstr "Kontrollfluss-Durchsetzungstechnologie" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "Eine Firmware-Datei konvertieren" #. TRANSLATORS: command description msgid "Create an EFI boot entry" msgstr "EFI-Boot-Eintrag erstellen" #. TRANSLATORS: when the update was built msgid "Created" msgstr "Erstellt" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "Kritisch" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "Kryptografische Hash-Verifizierung ist verfügbar" #. TRANSLATORS: current value of a BIOS setting msgid "Current Value" msgstr "Aktueller Wert" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "Aktuelle Version" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "GERÄTEKENNUNG|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Defektlokalisierungsoptionen" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "Entpacken …" #. TRANSLATORS: command description msgid "Delete an EFI boot entry" msgstr "EFI-Boot-Eintrag löschen" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Beschreibung" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "Vom Bootloader-Modus lösen" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "Details" #. TRANSLATORS: the best known configuration is a set of software that we know #. works #. * well together. In the OEM and ODM industries it is often called a BKC msgid "Deviate from the best known configuration?" msgstr "Von der besten bekannten Konfiguration abweichen?" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "Geräte-Bitschalter" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "Gerätekennung" #. TRANSLATORS: description of the device requests msgid "Device Requests" msgstr "Geräteanfragen" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Gerät hinzugefügt:" #. TRANSLATORS: the device is already connected msgid "Device already exists" msgstr "Gerät existiert bereits" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "Geräteakku ist zu schwach" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse #, c-format msgid "Device battery power is too low (%u%%, requires %u%%)" msgstr "Geräteakkuleistung ist zu gering (%u%%, erfordert %u%%)" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "Das Gerät kann sich nach Fehlern beim Aufspielen wiederherstellen" #. TRANSLATORS: lid means "laptop top cover" msgid "Device cannot be updated while the lid is closed" msgstr "Gerät kann nicht aktualisiert werden, wenn der Deckel geschlossen ist" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Gerät geändert:" #. TRANSLATORS: a version check is required for all firmware msgid "Device firmware is required to have a version check" msgstr "Versionsprüfung der Geräte-Firmware ist erforderlich" #. TRANSLATORS: emulated means we are pretending to be a different model msgid "Device is emulated" msgstr "Gerät wird emuliert" #. TRANSLATORS: device cannot be interrupted, for instance taking a phone call msgid "Device is in use" msgstr "Gerät ist in Gebrauch" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "Gerät ist gesperrt" #. TRANSLATORS: we have two ways of communicating with the device, so we hide #. one msgid "Device is lower priority than an equivalent device" msgstr "Gerät hat eine niedrigere Priorität als ein gleichwertiges Gerät" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "Das Gerät muss alle bereitgestellten Versionen nacheinander installieren." #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "Gerät ist nicht erreichbar" #. TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode msgid "Device is unreachable, or out of wireless range" msgstr "Gerät ist unerreichbar oder außerhalb der Funkreichweite" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "Das Gerät ist während der Dauer der Aktualisierung nutzbar" #. TRANSLATORS: usually this is when we're waiting for a reboot msgid "Device is waiting for the update to be applied" msgstr "Gerät wartet auf die Anwendung der Aktualisierung" #. TRANSLATORS: success, so say thank you to the user msgid "Device list uploaded successfully, thanks!" msgstr "Geräteliste erfolgreich hochgeladen, danke!" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Gerät entfernt:" #. TRANSLATORS: as in, wired mains power for a laptop msgid "Device requires AC power to be connected" msgstr "Gerät muss an eine Wechselstromquelle angeschlossen werden" #. TRANSLATORS: device does not have a display connected msgid "Device requires a display to be plugged in" msgstr "Gerät benötigt einen angeschlossenen Bildschirm" #. TRANSLATORS: The device cannot be updated due to missing vendor's license." msgid "Device requires a software license to update" msgstr "Gerät erfordert eine Softwarelizenz für die Aktualisierung" #. TRANSLATORS: longer description msgid "Device software updates are provided for this device." msgstr "Für dieses Gerät werden Gerätesoftware-Aktualisierungen bereitgestellt." #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "Gerät führt Aktualisierungen in Etappen durch" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "Gerät unterstützt den Wechsel zu einem anderen Zweig der Firmware" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "Geräteaktualisierung erfordert Aktivierung" #. TRANSLATORS: save the old firmware to disk before installing the new one msgid "Device will backup firmware before installing" msgstr "Das Gerät sichert die Firmware vor der Installation" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "Gerät wird nach Abschluss der Aktualisierung nicht wieder angezeigt" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "Erfolgreich aktualisierte Geräte:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "Nicht korrekt aktualisierte Geräte:" #. TRANSLATORS: message letting the user there is an update #. * waiting, but there is a reason it cannot be deployed msgid "Devices with firmware updates that need user action: " msgstr "Geräte mit Firmware-Aktualisierungen, die eine Benutzeraktion erfordern:" #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "Geräte mit keinen verfügbaren Firmware-Aktualisierungen: " #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "Geräte mit der neuesten verfügbaren Firmware-Version:" #. TRANSLATORS: this is for the device tests msgid "Did not find any devices with matching GUIDs" msgstr "Es wurden keine Geräte mit passenden GUIDs gefunden" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "Deaktiviert" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "Deaktiviert eine gegebene Gegenstelle" #. TRANSLATORS: command description msgid "Disables virtual testing devices" msgstr "Deaktiviert virtuelle Prüfgeräte" #. TRANSLATORS: the OS the release was tested on msgid "Distribution" msgstr "Distribution" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "Nicht auf alte Metadaten prüfen" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "Nicht auf nicht erfassten Verlauf prüfen" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "Nicht prüfen, ob Gegenstellen zum Herunterladen aktiviert werden sollen" #. TRANSLATORS: command line option msgid "Do not check or prompt for reboot after update" msgstr "Nach der Aktualisierung nicht prüfen oder zum Neustart auffordern" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "Protokollierungsdomänen-Präfix nicht einbeziehen" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "Zeitstempel-Präfix nicht einbeziehen" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "Keine Gerätesicherheitsüberprüfungen durchführen" #. TRANSLATORS: command line option msgid "Do not prompt for devices" msgstr "Nicht nach Geräten fragen" #. TRANSLATORS: command line option msgid "Do not prompt to fix security issues" msgstr "Nicht zum Beheben von Sicherheitsproblemen auffordern" #. TRANSLATORS: command line option msgid "Do not search the firmware when parsing" msgstr "Beim Parsen die Firmware nicht durchsuchen" #. TRANSLATORS: warning message shown after update has been scheduled msgid "Do not turn off your computer or remove the AC adaptor while the update is in progress." msgstr "Schalten Sie Ihren Rechner nicht aus und entfernen Sie nicht den Netzadapter, während die Aktualisierung durchgeführt wird." #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "Nicht in die Verlaufsdatenbank schreiben" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "Verstehen Sie die Folgen einer Änderung des Firmware-Zweigs?" #. TRANSLATORS: ask the user if it's okay to convert, #. * "it" being the data contained in the EFI boot entry msgid "Do you want to convert it now?" msgstr "Möchten Sie es jetzt konvertieren?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "Möchten Sie diese Funktion für zukünftige Aktualisierungen deaktivieren?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "Möchten Sie diese Gegenstelle jetzt auffrischen?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "Möchten Sie Berichte für künftige Aktualisierungen automatisch hochladen?" #. TRANSLATORS: command line option msgid "Don't prompt for authentication (less details may be shown)" msgstr "Nicht zur Authentifizierung auffordern (es werden möglicherweise weniger Details angezeigt)" #. TRANSLATORS: success msgid "Done!" msgstr "Fertig." #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "%s von %s auf %s herabstufen?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "Stuft die Firmware auf einem Gerät herab" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "%s wird herabgestuft …" #. TRANSLATORS: command description msgid "Download a file" msgstr "Herunterladen einer Datei" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Herunterladen …" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "SMBIOS-Daten aus einer Datei ausgeben" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "Dauer" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "EMULATION-FILE [ARCHIVE-FILE]" msgstr "EMULATIONSDATEI [ARCHIVDATEI]" #. TRANSLATORS: longer description msgid "Each system should have tests to ensure firmware security." msgstr "Jedes System sollte Tests haben, um die Sicherheit der Firmware zu gewährleisten." #. TRANSLATORS: command description msgid "Emulate a device using a JSON manifest" msgstr "Ein Gerät mithilfe eines JSON-Manifests emulieren" #. TRANSLATORS: this device is not actually real msgid "Emulated" msgstr "Emuliert" #. TRANSLATORS: Title: if we are emulating a different host msgid "Emulated host" msgstr "Emulierter Host" msgid "Enable" msgstr "Aktivieren" msgid "Enable emulation data collection" msgstr "Emulationsdatenerfassung aktivieren" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "Neue Gegenstelle aktivieren?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "Diese Gegenstelle aktivieren?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Aktiviert" #. TRANSLATORS: Plugin is active only if hardware is found msgid "Enabled if hardware matches" msgstr "Aktiviert, wenn die Hardware passt" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "Aktiviert eine gegebene Gegenstelle" #. TRANSLATORS: command description msgid "Enables virtual testing devices" msgstr "Aktiviert virtuelle Prüfgeräte" #. TRANSLATORS: longer description msgid "Enabling firmware updates for the BIOS allows fixing security issues." msgstr "Das Aktivieren von Firmware-Aktualisierungen für das BIOS ermöglicht das Beheben von Sicherheitsproblemen." msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "Die Aktivierung dieser Funktionalität erfolgt auf eigene Gefahr, d.h. Sie müssen sich bei Problemen, die durch diese Aktualisierungen verursacht werden, an Ihren Erstausrüster wenden. Nur Probleme mit dem Aktualisierungsprozess selbst sollten unter $OS_RELEASE:BUG_REPORT_URL$ eingereicht werden." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "Die Aktivierung dieser Gegenstelle erfolgt auf eigene Gefahr." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Verschlüsselt" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "Verschlüsselter RAM" msgid "Encrypted RAM makes it impossible for information that is stored in device memory to be read if the memory chip is removed and accessed." msgstr "Verschlüsselter RAM macht es unmöglich, dass im Gerätespeicher gespeicherte Informationen gelesen werden können, wenn der Speicherchip entfernt und darauf zugegriffen wird." #. TRANSLATORS: the vendor is no longer supporting the device msgid "End of life" msgstr "Lebensende" #. TRANSLATORS: The BIOS setting can only be changed to fixed values msgid "Enumeration" msgstr "Aufzählung" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "Gesamten Firmware-Aktualisierungsverlauf löschen" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Löschen …" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "Nach einer kurzen Verzögerung beenden" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "Nach dem Laden der Engine beenden" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "Eine Firmware-Dateistruktur nach XML exportieren" #. TRANSLATORS: command description msgid "Export firmware history for manual upload" msgstr "Firmware-Verlauf für manuelles Hochladen exportieren" #. TRANSLATORS: command description msgid "Extract a firmware blob to images" msgstr "Einen Firmware-Blob in Abbilder extrahieren" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "DATEI" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "DATEI [GERÄTEKENNUNG|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "DATEINAME" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "DATEINAME ZERTIFIKAT PRIVATER-SCHLÜSSEL" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "DATEINAME GERÄTEKENNUNG" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME OFFSET DATA [FIRMWARE-TYPE]" msgstr "DATEINAME ADRESSABSTAND DATEN [FIRMWARE-TYP]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "DATEINAME [GERÄTEKENNUNG|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "DATEINAME [FIRMWARE-TYP]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" msgstr "DATEINAME-QUELLE DATEINAME-ZIEL [FIRMWARE-TYP-QUELLE] [FIRMWARE-TYP-ZIEL]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" msgstr "DATEINAME|PRÜFSUMME1[,PRÜFSUMME2][,PRÜFSUMME3]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "Fehlgeschlagen" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Anwenden der Aktualisierung ist fehlgeschlagen" #. TRANSLATORS: error message for Windows msgid "Failed to connect to Windows service, please ensure it's running." msgstr "Verbindung zum Windows-Dienst fehlgeschlagen, bitte stellen Sie sicher, dass er läuft." #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "Verbindung mit dem Daemon ist fehlgeschlagen" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "Laden der lokalen dbx ist fehlgeschlagen" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "Laden der System-dbx ist fehlgeschlagen" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "Sperren ist fehlgeschlagen" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Parsen der Argumente ist fehlgeschlagen" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "Parsen der Datei ist fehlgeschlagen" #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #, c-format msgid "Failed to parse flags for %s" msgstr "Fehler beim Parsen der Flags für %s" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "Parsen der lokalen dbx ist fehlgeschlagen" #. TRANSLATORS: a feature is something like "can show an image" msgid "Failed to set front-end features" msgstr "Fehler beim Festlegen von Front-End-Funktionen" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "Validierung des ESP-Inhalts ist fehlgeschlagen" #. TRANSLATORS: item is FALSE msgid "False" msgstr "Falsch" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Dateiname" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "Dateinamen-Signatur" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "Dateinamen-Quelle" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Dateiname erforderlich" #. TRANSLATORS: command line option msgid "Filter with a set of device flags using a ~ prefix to exclude, e.g. 'internal,~needs-reboot'" msgstr "Mit einer Reihe von Geräte-Bitschaltern filtern, wobei ein ~-Präfix zum Ausschluss verwendet wird, z. B. 'internal,~needs-reboot'." #. TRANSLATORS: command line option msgid "Filter with a set of release flags using a ~ prefix to exclude, e.g. 'trusted-release,~trusted-metadata'" msgstr "Mit einer Reihe von Veröffentlichungsbitschaltern filtern, wobei ein ~-Präfix zum Ausschluss verwendet wird, z. B. 'trusted-release,~trusted-metadata'." #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware Attestation" msgstr "Firmware-Attestierung" #. TRANSLATORS: longer description msgid "Firmware Attestation checks device software using a reference copy, to make sure that it has not been changed." msgstr "Bei der Firmware-Attestierung wird die Gerätesoftware anhand einer Referenzkopie überprüft, um sicherzustellen, dass sie nicht geändert wurde." #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "Firmware-BIOS-Deskriptor" #. TRANSLATORS: longer description msgid "Firmware BIOS Descriptor protects device firmware memory from being tampered with." msgstr "Firmware-BIOS-Deskriptor schützt den Firmware-Speicher des Geräts vor Manipulationen." #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "Firmware-BIOS-Region" #. TRANSLATORS: longer description msgid "Firmware BIOS Region protects device firmware memory from being tampered with." msgstr "Firmware-BIOS-Region schützt den Firmware-Speicher des Geräts vor Manipulationen." #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "Firmware Basis-URI" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "D-Bus-Dienst für Firmware-Aktualisierung" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Hintergrundprogramm für Firmware-Aktualisierung" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "Firmware-Aktualisierungsprogramm-Verifizierung" msgid "Firmware Updater Verification checks that software used for updating has not been tampered with." msgstr "Die Firmware-Aktualisierungsprogramm-Verifizierung prüft, ob die für die Aktualisierung verwendete Software nicht manipuliert wurde." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "Firmware-Aktualisierungen" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "Firmware-Dienstprogramm" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "Firmware-Schreibschutz" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "Firmware-Schreibschutz-Sperre" #. TRANSLATORS: longer description msgid "Firmware Write Protection protects device firmware memory from being tampered with." msgstr "Firmware-Schreibschutz schützt den Firmware-Speicher des Geräts vor Manipulationen." #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "Firmware-Attestierung" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "Firmware ist bereits blockiert" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "Firmware ist nicht bereits blockiert." #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "Firmware-Metadaten wurden seit %u Tag nicht aktualisiert und sind möglicherweise nicht auf dem neuesten Stand." msgstr[1] "Firmware-Metadaten wurden seit %u Tagen nicht aktualisiert und sind möglicherweise nicht auf dem neuesten Stand." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "Firmware-Aktualisierungen" #. TRANSLATORS: user needs to run a command, %1 is 'fwupdmgr unlock' #, c-format msgid "Firmware updates disabled; run '%s' to enable" msgstr "Firmware-Aktualisierungen deaktiviert; rufe '%s' auf, um sie zu aktivieren." #. TRANSLATORS: command description msgid "Fix a specific host security attribute" msgstr "Behebung eines bestimmten Host-Sicherheitsattributs" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fix reverted successfully" msgstr "Korrektur erfolgreich zurückgenommen" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fixed successfully" msgstr "Erfolgreich korrigiert" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "Bitschalter" #. TRANSLATORS: command line option msgid "Force the action by relaxing some runtime checks" msgstr "Aktion durch Lockerung einiger Laufzeitüberprüfungen erzwingen" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Gefunden" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "Vollständige Festplattenverschlüsselung erkannt" #. TRANSLATORS: we might ask the user the recovery key when next booting #. Windows msgid "Full disk encryption secrets may be invalidated when updating" msgstr "Die Geheimnisse der vollständigen Festplattenverschlüsselung können bei der Aktualisierung ungültig werden" #. TRANSLATORS: Title: if the part has been fused msgid "Fused Platform" msgstr "Abgesicherte Plattform" #. TRANSLATORS: Title: if the part has been fused msgid "Fused platform" msgstr "Abgesicherte Plattform" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" msgstr[1] "GUIDs" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "GUID|DEVICE-ID" msgstr "GUID|GERÄTEKENNUNG" msgid "Get BIOS settings" msgstr "BIOS-Einstellungen abrufen" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "Alle von fwupd unterstützten Geräte-Bitschalter abrufen" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Alle Geräte ermitteln, die Firmware-Aktualisierungen unterstützen" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "Alle aktivierten und im System registrierten Plugins ermitteln" #. TRANSLATORS: command description msgid "Get all known version formats" msgstr "Alle bekannten Versionsformate abrufen" #. TRANSLATORS: command description msgid "Get device report metadata" msgstr "Gerätebericht-Metadaten abrufen" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Ermittelt Details über eine Firmware-Datei" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "Ruft die konfigurierten Gegenstellen ab" #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "Ruft die Sicherheitsattribute des Hosts ab" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "Holt die Liste der genehmigten Firmware" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "Holt die Liste der blockierten Firmware" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Ermittelt die Liste der Aktualisierungen für angeschlossene Hardware" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "Ermittelt die Versionen für ein Gerät" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Ermittelt die Ergebnisse der letzten Aktualisierung" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "HWIDS-DATEI" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "Hardware wartet darauf, wieder angeschlossen zu werden" #. TRANSLATORS: the release urgency msgid "High" msgstr "Hoch" #. TRANSLATORS: title for host security events msgid "Host Security Events" msgstr "Host-Sicherheitsereignisse" #. TRANSLATORS: error message for unsupported feature #, c-format msgid "Host Security ID (HSI) is not supported" msgstr "Host-Sicherheitskennung (HSI) wird nicht unterstützt" #. TRANSLATORS: success, so say thank you to the user msgid "Host Security ID attributes uploaded successfully, thanks!" msgstr "Die Attribute der Sicherheitskennung des Hosts wurden erfolgreich hochgeladen, danke!" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "Host-Sicherheitskennung:" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX" msgstr "INDEX" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX KEY [VALUE]" msgstr "INDEXSCHLÜSSEL [WERT]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX NAME TARGET [MOUNTPOINT]" msgstr "INDEXNAME [EINHÄNGEPUNKT]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX1,INDEX2" msgstr "INDEX1,INDEX2" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INHIBIT-ID" msgstr "BLOCKIERUNGSKENNUNG" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "IOMMU-Schutz" #. TRANSLATORS: longer description msgid "IOMMU Protection prevents connected devices from accessing unauthorized parts of system memory." msgstr "IOMMU-Schutz verhindert, dass angeschlossene Geräte auf nicht autorisierte Teile des Systemspeichers zugreifen." #. TRANSLATORS: HSI event title msgid "IOMMU device protection disabled" msgstr "IOMMU-Geräteschutz deaktiviert" #. TRANSLATORS: HSI event title msgid "IOMMU device protection enabled" msgstr "IOMMU-Geräteschutz aktiviert" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "Bereit …" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "Strenge SSL-Prüfungen beim Herunterladen von Dateien ignorieren" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "Fehlerhafte Firmware-Prüfsummen ignorieren" #. TRANSLATORS: command line option msgid "Ignore firmware hardware mismatch failures" msgstr "Firmware-Hardware-Nichtübereinstimmungsfehler ignorieren" #. TRANSLATORS: command line option msgid "Ignore non-critical firmware requirements" msgstr "Unkritische Firmware-Anforderungen ignorieren" #. TRANSLATORS: try to help msgid "Ignoring SSL strict checks, to do this automatically in the future export DISABLE_SSL_STRICT in your environment" msgstr "Strenge SSL-Prüfungen werden ignoriert; um dies in Zukunft automatisch zu tun, exportieren Sie DISABLE_SSL_STRICT in Ihrer Umgebung" #. TRANSLATORS: show the user a generic image that can be themed msgid "Image" msgstr "Bild" #. TRANSLATORS: show the user a random image from the internet msgid "Image (custom)" msgstr "Bild (benutzerdefiniert)" #. TRANSLATORS: the inhibit ID is a short string like dbus-123456 #, c-format msgid "Inhibit ID is %s." msgstr "Blockierungskennung lautet %s." #. TRANSLATORS: command description msgid "Inhibit the system to prevent upgrades" msgstr "System blockieren, um Aktualisierungen zu verhindern" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "Installationsdauer" #. TRANSLATORS: command description msgid "Install a firmware file in cabinet format on this hardware" msgstr "Eine Firmware-Datei im Cabinet-Format auf dieser Hardware installieren" #. TRANSLATORS: command description msgid "Install a raw firmware blob on a device" msgstr "Rohen Firmware-Blob auf einem Gerät installieren" #. TRANSLATORS: command description msgid "Install a specific firmware file on all devices that match" msgstr "Bestimmte Firmware-Datei auf allen zutreffenden Geräten installieren" #. TRANSLATORS: command description msgid "Install a specific firmware on a device, all possible devices will also be installed once the CAB matches" msgstr "Eine bestimmte Firmware auf einem Gerät installieren, alle möglichen Geräte werden ebenfalls installiert, sobald das CAB übereinstimmt" msgid "Install old version of signed system firmware" msgstr "Alte Version der signierten System-Firmware installieren" msgid "Install old version of unsigned system firmware" msgstr "Alte Version der nicht-signierten System-Firmware installieren" msgid "Install signed device firmware" msgstr "Signierte Geräte-Firmware installieren" msgid "Install signed system firmware" msgstr "Signierte System-Firmware installieren" msgid "Install unsigned device firmware" msgstr "Nicht-signierte Geräte-Firmware installieren" msgid "Install unsigned system firmware" msgstr "Nicht-signierte System-Firmware installieren" #. TRANSLATORS: stay on one firmware version unless the new version is #. explicitly #. * specified msgid "Installing a specific release is explicitly required" msgstr "Die Installation einer bestimmten Version ist ausdrücklich erforderlich" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "Firmware-Aktualisierung wird installiert …" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "Wird auf %s installiert …" #. TRANSLATORS: if it breaks, you get to keep both pieces msgid "Installing this update may also void any device warranty." msgstr "Durch die Installation dieser Aktualisierung kann auch die Gerätegarantie erlöschen." #. TRANSLATORS: The BIOS setting only accepts integers in a fixed range msgid "Integer" msgstr "Ganzzahl" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM Protected" msgstr "Intel BootGuard ACM-geschützt" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "Intel BootGuard ACM-geschützt" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard Error Policy" msgstr "Intel BootGuard-Fehlerrichtlinie" msgid "Intel BootGuard Error Policy ensures the device does not continue to start if its device software has been tampered with." msgstr "Die Intel BootGuard-Fehlerrichtlinie stellt sicher, dass das Gerät nicht weiter startet, wenn seine Gerätesoftware manipuliert wurde." #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard Fuse" msgstr "Intel BootGuard-Sicherung" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "Einmalig programmierbare Intel BootGuard-Sicherung" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard Verified Boot" msgstr "Verifizierter Start mit Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Intel BootGuard-Fehlerrichtlinie" #. TRANSLATORS: longer description msgid "Intel BootGuard prevents unauthorized device software from operating when the device is started." msgstr "Intel BootGuard verhindert, dass nicht autorisierte Gerätesoftware beim Start des Geräts ausgeführt wird." #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Verifizierter Start mit Intel BootGuard" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS Mitigation" msgstr "Intel GDS-Milderung" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS mitigation" msgstr "Intel GDS-Milderung" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Manufacturing Mode" msgstr "Herstellungsmodus der Intel Verwaltungs-Engine" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the "override" is #. enabled #. * with a jumper -- luckily it is probably not accessible to end users on #. consumer #. * boards msgid "Intel Management Engine Override" msgstr "Übergehen der Intel Verwaltungs-Engine" #. TRANSLATORS: longer description msgid "Intel Management Engine Override disables checks for device software tampering." msgstr "Die Intel Verwaltungs-Engine-Übergehung deaktiviert die Prüfung auf Manipulationen der Gerätesoftware." #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Version" msgstr "Version der Intel Verwaltungs-Engine" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "Internes Gerät" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "Ungültig" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "Ungültige Argumente" #. TRANSLATORS: error message msgid "Invalid arguments, expected GUID" msgstr "Ungültige Argumente, erwartete GUID" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX KEY [VALUE]" msgstr "Ungültige Argumente, erwartete INDEXSCHLÜSSEL [WERT]" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX NAME TARGET [MOUNTPOINT]" msgstr "Ungültige Argumente, erwartete INDEXNAME ZIEL [EINHÄNGEPUNKT]" #. TRANSLATOR: This is the error message for #. * incorrect parameter msgid "Invalid arguments, expected an AppStream ID" msgstr "Ungültige Argumente. Es wurde eine AppStream-Kennung erwartet." #. TRANSLATORS: error message msgid "Invalid arguments, expected at least ARCHIVE FIRMWARE METAINFO" msgstr "Ungültige Argumente, erwartete wenigstens ARCHIV FIRMWARE METAINFO" #. TRANSLATORS: error message msgid "Invalid arguments, expected base-16 integer" msgstr "Ungültige Argumente, erwartete Basis-16-Ganzzahl" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "Ist Zurückstufung" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "Ist im Bootloader-Modus" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "Ist Aktualisierung" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "Anliegen" msgstr[1] "Anliegen" #. TRANSLATORS: HSI event title msgid "Kernel is no longer tainted" msgstr "Kernel ist nicht mehr verschmutzt" #. TRANSLATORS: HSI event title msgid "Kernel is tainted" msgstr "Kernel ist verschmutzt" #. TRANSLATORS: HSI event title msgid "Kernel lockdown disabled" msgstr "Kernel-Sperrung deaktiviert" #. TRANSLATORS: HSI event title msgid "Kernel lockdown enabled" msgstr "Kernel-Sperrung aktiviert" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "ORT" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "Zuletzt geändert" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "Weniger als eine Minute verbleiben" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "Lizenz" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "Linux-Kernel-Sperrung" #. TRANSLATORS: longer description msgid "Linux Kernel Lockdown mode prevents administrator (root) accounts from accessing and changing critical parts of system software." msgstr "Die Abriegelung des Linux-Kernels verhindert, dass Administratorkonten (root) auf kritische Teile der Systemsoftware zugreifen und diese ändern können." msgid "Linux Kernel Swap temporarily saves information to disk as you work. If the information is not protected, it could be accessed by someone if they obtained the disk." msgstr "Die Auslagerung des Linux-Kernels speichert temporär Informationen auf der Festplatte, während Sie arbeiten. Wenn die Informationen nicht geschützt sind, könnte jemand auf sie zugreifen, wenn er die Festplatte erbeutet." #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "Linux-Kernel-Verifizierung" msgid "Linux Kernel Verification makes sure that critical system software has not been tampered with. Using device drivers which are not provided with the system can prevent this security feature from working correctly." msgstr "Die Linux-Kernel-Verifizierung stellt sicher, dass kritische Systemsoftware nicht manipuliert wurde. Die Verwendung von Gerätetreibern, die nicht im Lieferumfang des Systems enthalten sind, kann verhindern, dass diese Sicherheitsfunktion korrekt funktioniert." #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "Linux-Auslagerung" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Linux-Hersteller-Firmwaredienst (stabile Firmware)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Linux-Hersteller-Firmwaredienst (Test-Firmware)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Linux-Kernel" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Linux-Kernel-Sperrung" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Linux-Auslagerung" #. TRANSLATORS: command description msgid "List EFI boot files" msgstr "EFI-Boot-Dateien auflisten" #. TRANSLATORS: command description msgid "List EFI boot parameters" msgstr "EFI-Boot-Parameter auflisten" #. TRANSLATORS: command description msgid "List EFI variables with a specific GUID" msgstr "EFI-Variablen mit einer bestimmten GUID auflisten" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "Einträge in dbx auflisten" #. TRANSLATORS: command description msgid "List the available firmware GTypes" msgstr "Verfügbare Firmware-GTypen auflisten" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "Verfügbare Firmware-Typen auflisten" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "Listet die Dateien auf dem ESP auf" #. TRANSLATORS: command description msgid "Load device emulation data" msgstr "Geräteemulationsdaten laden" #. TRANSLATORS: the plugin was created from a .so object, and was not built-in msgid "Loaded from an external module" msgstr "Von einem externen Modul geladen" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "Laden …" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "Gesperrt" #. TRANSLATORS: the release urgency msgid "Low" msgstr "Niedrig" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refers #. * to the private/public key used to secure loading of firmware msgid "MEI Key Manifest" msgstr "MEI-Schlüsselmanifest" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refer #. * to the private/public key used to secure loading of firmware msgid "MEI key manifest" msgstr "MEI-Schlüsselmanifest" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "MEI-Herstellungsmodus" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "MEI überschreiben" msgid "MEI version" msgstr "MEI-Version" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "Manuelles Aktivieren bestimmter Plugins" #. TRANSLATORS: longer description msgid "Manufacturing Mode is used when the device is manufactured and security features are not yet enabled." msgstr "Der Herstellungsmodus wird verwendet, wenn das Gerät hergestellt wird und die Sicherheitsfunktionen noch nicht aktiviert sind." #. TRANSLATORS: Longest valid string for BIOS setting msgid "Maximum length" msgstr "Maximale Länge" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "Maximaler Wert" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "Mittel" #. TRANSLATORS: ask the user to do a simple task which should be translated msgid "Message" msgstr "Mitteilung" #. TRANSLATORS: ask the user a question, and it will not be translated msgid "Message (custom)" msgstr "Mitteilung (benutzerdefiniert)" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "Metadaten-Signatur" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "Metadaten-URI" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "Metadaten können über den Linux-Hersteller-Firmwaredienst bezogen werden." #. TRANSLATORS: error message for a user who ran fwupdmgr #. * refresh recently -- %1 is '--force' #, c-format msgid "Metadata is up to date; use %s to refresh again." msgstr "Metadaten sind aktuell; benutze %s um sie erneut zu aktualisieren." #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "Minimale Version" #. TRANSLATORS: Shortest valid string for BIOS setting msgid "Minimum length" msgstr "Minimale Länge" #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "Minimaler Wert" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "Ändert einen Daemon-Konfigurationswert" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "Modifiziert eine gegebene Gegenstelle" msgid "Modify a configured remote" msgstr "Eine konfigurierte Gegenstelle modifizieren" msgid "Modify daemon configuration" msgstr "Daemon-Konfiguration bearbeiten" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "Hintergrundprogramm auf Ereignisse überwachen" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "Hängt das ESP ein" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "Benötigt einen Neustart nach der Installation" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "Benötigt Neustart" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "Benötigt ein Herunterfahren nach der Installation" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "Neue Version" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "Keine Aktion angegeben!" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "Keine Herabstufungen für %s" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "Keine Firmware-Kennungen gefunden" #. TRANSLATORS: nothing found msgid "No firmware found" msgstr "Keine Firmware gefunden" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Es wurde keine Hardware erkannt, deren Firmware aktualisiert werden kann" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "Keine Freigaben verfügbar" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "Da derzeit keine Gegenstellen aktiviert sind, sind keine Metadaten verfügbar." #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "Keine Gegenstelle verfügbar" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "Keine aktualisierbaren Geräte" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "Keine Aktualisierungen verfügbar" msgid "No updates available for remaining devices" msgstr "Für die verbleibenden Geräte sind keine Aktualisierungen verfügbar" #. TRANSLATORS: error message #, c-format msgid "No volume matched %s" msgstr "Kein Volume passt zu %s" #. TRANSLATORS: version cannot be installed due to policy msgid "Not approved" msgstr "Nicht genehmigt" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "Nicht gefunden" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "Nicht unterstützt" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "Ok" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "OK!" #. TRANSLATORS: the firmware old version msgid "Old version" msgstr "Alte Version" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "Nur einzelnen PCR-Wert anzeigen" #. TRANSLATORS: command line option msgid "Only use peer-to-peer networking when downloading files" msgstr "Beim Herunterladen von Dateien nur Peer-to-Peer-Netzwerke verwenden" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "Nur Versionsaktualisierungen sind erlaubt" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "Standard-ESP-Pfad überschreiben" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Firmware" msgstr "P2P-Firmware" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Metadata" msgstr "P2P-Metadaten" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "PFAD" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "Details zu einer Firmware-Datei parsen und anzeigen" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "dbx-Aktualisierung wird geparst …" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "System-dbx wird geparst …" #. TRANSLATORS: remote filename base msgid "Password" msgstr "Passwort" #. TRANSLATORS: command description msgid "Patch a firmware blob at a known offset" msgstr "Einen Firmware-Blob an einem bekannten Offset patchen" msgid "Payload" msgstr "Nutzdaten" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "Ausstehend" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "Operation durchführen?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "Plattform-Defektlokalisierung" #. TRANSLATORS: longer description msgid "Platform Debugging allows device security features to be disabled. This should only be used by hardware manufacturers." msgstr "Mit Plattform-Defektlokalisierung können die Sicherheitsfunktionen des Geräts deaktiviert werden. Dies sollte nur von Hardware-Herstellern verwendet werden." #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "Plattform-Defektlokalisierung" #. TRANSLATORS: 'recovery key' here refers to a code, rather than a physical #. metal thing msgid "Please ensure you have the volume recovery key before continuing." msgstr "Bitte stellen Sie sicher, dass Sie den Wiederherstellungsschlüssel für den Datenträger haben, bevor Sie fortfahren." #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "Bitte geben Sie eine Zahl von 0 bis %u ein: " #. TRANSLATORS: the user isn't reading the question -- %1 is 'Y' and %2 is #. * 'N' #, c-format msgid "Please enter either %s or %s: " msgstr "Bitte geben Sie entweder %s oder %s ein: " #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "Fehlende Plugin-Abhängigkeiten" #. TRANSLATORS: The plugin is only for testing msgid "Plugin is only for testing" msgstr "Plugin ist nur zum Testen" #. TRANSLATORS: Possible values for a bios setting msgid "Possible Values" msgstr "Mögliche Werte" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA Protection" msgstr "DMA-Schutz vor dem Booten" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "DMA-Schutz vor dem Booten" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is disabled" msgstr "DMA-Schutz vor dem Booten ist deaktiviert" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is enabled" msgstr "DMA-Schutz vor dem Booten ist aktiviert" #. TRANSLATORS: longer description msgid "Pre-boot DMA protection prevents devices from accessing system memory after being connected to the computer." msgstr "Der DMA-Schutz vor dem Booten verhindert, dass Geräte auf den Systemspeicher zugreifen, nachdem sie an den Rechner angeschlossen wurden." #. TRANSLATORS: warning message msgid "Press unlock on the device to continue the update process." msgstr "Drücken Sie auf dem Gerät auf Entsperren, um den Aktualisierungsvorgang fortzusetzen." #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "Vorherige Version" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Priorität" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "Probleme" msgid "Proceed with upload?" msgstr "Mit dem Hochladen fortfahren?" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Processor Security Checks" msgstr "Prozessor-Sicherheitsprüfungen" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "Processor rollback protection" msgstr "Prozessor-Rückrollschutz" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "Proprietär" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "GEGENSTELLENKENNUNG" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID KEY VALUE" msgstr "GEGENSTELLENKENNUNG SCHLÜSSEL WERT" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "Nur lesen" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "Firmware-Blob von einem Gerät lesen" #. TRANSLATORS: command description msgid "Read a firmware from a device" msgstr "Firmware von einem Gerät lesen" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "Von %s wird gelesen …" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Lesen …" #. TRANSLATORS: Plugin is active and in use msgid "Ready" msgstr "Bereit" #. TRANSLATORS: how often we should refresh the metadata msgid "Refresh Interval" msgstr "Auffrischungsintervall" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "Metadaten von entferntem Server aktualisieren" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "%s auf %s neu installieren?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "Aktuelle Firmware auf dem Gerät neu installieren" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "Firmware auf einem Gerät neu installieren" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "Veröffentlichungszweig" #. TRANSLATORS: release attributes msgid "Release Flags" msgstr "Veröffentlichungsbitschalter" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "Veröffentlichungskennung" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "Gegenstellenkennung" #. TRANSLATORS: command description msgid "Removes devices to watch for future emulation" msgstr "Entfernt Geräte, die für zukünftige Emulationen zu beobachten sind" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "Bericht-URI" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "An den entfernten Server gemeldet" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "Erforderliches efivarfs-Dateisystem wurde nicht gefunden" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "Erforderliche Hardware wurde nicht gefunden" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "Erfordert einen Bootloader" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Erfordert Internetverbindung" msgid "Reset daemon configuration" msgstr "Daemon-Konfiguration zurücksetzen" #. TRANSLATORS: sets something in the daemon configuration file msgid "Resets a daemon configuration section" msgstr "Setzt einen Abschnitt der Daemon-Konfiguration zurück" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Jetzt neu starten?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "Den Daemon neu starten, damit die Änderung wirksam wird?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Gerät wird neu gestartet …" #. TRANSLATORS: command description msgid "Retrieve BIOS settings. If no arguments are passed all settings are returned" msgstr "BIOS-Einstellungen abrufen. Wenn keine Argumente übergeben werden, werden alle Einstellungen zurückgegeben" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "Alle Hardware-Kennungen für das System zurückgeben" #. TRANSLATORS: ask the user to upload msgid "Review and upload report now?" msgstr "Bericht jetzt überprüfen und hochladen?" #. TRANSLATORS: longer description msgid "Rollback Protection prevents device software from being downgraded to an older version that has security problems." msgstr "Der Rückrollschutz verhindert, dass die Gerätesoftware auf eine ältere Version heruntergestuft wird, die Sicherheitsprobleme aufweist." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr get-upgrades` #, c-format msgid "Run `%s` for more information." msgstr "Führen Sie `%s` für weitere Informationen aus." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr sync` #, c-format msgid "Run `%s` to complete this action." msgstr "Führen Sie `%s` aus, um diese Aktion abzuschließen." #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "Führe die Plugin-Composite-Säuberungsroutine aus, wenn install-blob genutzt wird" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "Führe die Plugin-Composite-Vorbereitungsroutine aus, wenn install-blob genutzt wird" #. TRANSLATORS: command description msgid "Run the post-reboot cleanup action" msgstr "Post-Neustart-Bereinigungsaktion durchführen" #. TRANSLATORS: tell a user how to get information #, c-format msgid "Run without '%s' to see" msgstr "Ohne '%s' ausführen, um etwas zu sehen" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "Laufender Kernel ist zu alt" #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "Laufzeit-Suffix" #. TRANSLATORS: Software Bill of Materials link msgid "SBOM" msgstr "Software-Stückliste" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SECTION" msgstr "ABSCHNITT" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING VALUE" msgstr "EINSTELLUNGSWERT" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING1 VALUE1 [SETTING2] [VALUE2]" msgstr "EINSTELLUNG1 WERT1 [EINSTELLUNG2] [WERT2]" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "SMAP" msgstr "SMAP" #. TRANSLATORS: Title: Whether firmware is locked down msgid "SMM locked down" msgstr "SMM gesperrt" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "SPI-BIOS-Deskriptor" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "SPI BIOS-Region" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "SPI sperren" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "SPI replay protection" msgstr "SPI-Wiederholungsschutz" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "SPI schreiben" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "SPI write protection" msgstr "SPI-Schreibschutz" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "SUBSYSTEM TREIBER [GERÄTEKENNUNG|GUID]" #. TRANSLATORS: command description msgid "Save a file that allows generation of hardware IDs" msgstr "Eine Datei speichern, die die Erstellung von Hardware-Kennungen ermöglicht" #. TRANSLATORS: command description msgid "Save device emulation data" msgstr "Geräteemulationsdaten speichern" #. TRANSLATORS: key for a offline report filename msgid "Saved report" msgstr "Gespeicherter Bericht" #. TRANSLATORS: Scalar increment for integer BIOS setting msgid "Scalar Increment" msgstr "Skalares Inkrement" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Wird geplant …" #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "Secure Boot deaktiviert" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "Secure Boot aktiviert" msgid "Security hardening for HSI" msgstr "Strengere Sicherheit für HSI" #. TRANSLATORS: the %1 is a URL to a wiki page #, c-format msgid "See %s for more details." msgstr "Siehe %s für weitere Details." #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "Siehe %s für weitere Informationen." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Ausgewähltes Gerät" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Ausgewählter Datenträger" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "Seriennummer" #. TRANSLATORS: Configured a BIOS setting to a value #, c-format msgid "Set BIOS setting '%s' using '%s'." msgstr "BIOS-Einstellung »%s« mittels »%s« festlegen." #. TRANSLATORS: command description msgid "Set a BIOS setting" msgstr "Eine BIOS-Einstellung festlegen" msgid "Set one or more BIOS settings" msgstr "Eine oder mehrere BIOS-Einstellungen festlegen" #. TRANSLATORS: command description msgid "Set or remove an EFI boot hive entry" msgstr "EFI-Boot-hive-Eintrag festlegen oder entfernen" #. TRANSLATORS: command description msgid "Set the EFI boot next" msgstr "Nächsten EFI-Boot festlegen" #. TRANSLATORS: command description msgid "Set the EFI boot order" msgstr "EFI-Boot-Reihenfolge festlegen" #. TRANSLATORS: command line option msgid "Set the download retries for transient errors" msgstr "Legen Sie die Wiederholungsversuche für das Herunterladen bei vorübergehenden Fehlern fest" #. TRANSLATORS: command description msgid "Sets one or more BIOS settings" msgstr "Legt eine oder mehrere BIOS-Einstellungen fest" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Legt die Liste der zugelassenen Firmware fest" #. TRANSLATORS: type of BIOS setting msgid "Setting type" msgstr "Einstellungstyp" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "Einstellungen werden nach einem Neustart des Systems wirksam" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "Firmware-Verlauf mit den Entwicklern teilen" #. TRANSLATORS: command line option msgid "Show all results" msgstr "Alle Ergebnisse anzeigen" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "Client- und Hintergrundprogramm-Versionen anzeigen" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "Ausführliche Daemon-Informationen für eine bestimme Domäne anzeigen" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "Informationen zur Defektlokalisierung für alle Domänen anzeigen" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Defektlokalisierungsoptionen anzeigen" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Nicht aktualisierbare Geräte anzeigen" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Zusätzliche Informationen zur Defektlokalisierung anzeigen" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "Verlauf von Firmware-Aktualisierungen anzeigen" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "Berechnete Version der dbx anzeigen" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "Jetzt herunterfahren?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "Eine Firmware mit einem neuen Schlüssel signieren" msgid "Sign data using the client certificate" msgstr "Daten mit dem Client-Zertifikat signieren" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "Daten mit dem Client-Zertifikat signieren" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "Die hochgeladenen Daten mit dem Client-Zertifikat signieren" msgid "Signature" msgstr "Signatur" #. TRANSLATORS: firmware is verified on-device the payload using strong crypto msgid "Signed Payload" msgstr "Signierte Nutzdaten" #. TRANSLATORS: file size of the download msgid "Size" msgstr "Größe" #. TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM msgid "Some of the platform secrets may be invalidated when updating this firmware." msgstr "Einige der Plattformgeheimnisse können durch die Aktualisierung dieser Firmware ungültig werden." #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "Quelle" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "dbx-Datenbankdatei angeben" msgid "Stop the fwupd service" msgstr "Dienst fwupd stoppen" #. TRANSLATORS: The BIOS setting accepts strings msgid "String" msgstr "Zeichenkette" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "Erfolg" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "Alle Geräte erfolgreich aktiviert" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "Erfolgreich deaktivierte Gegenstelle" #. TRANSLATORS: comment explaining result of command msgid "Successfully disabled test devices" msgstr "Prüfgeräte erfolgreich deaktiviert" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "Neue Metadaten wurden erfolgreich heruntergeladen: " #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "Gegenstelle erfolgreich aktiviert und aufgefrischt" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "Erfolgreich aktivierte Gegenstelle " #. TRANSLATORS: comment explaining result of command msgid "Successfully enabled test devices" msgstr "Prüfgeräte erfolgreich aktiviert" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "Erfolgreich installierte Firmware" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "Konfigurationswert erfolgreich geändert" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "Erfolgreich modifizierte Gegenstelle" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "Metadaten erfolgreich manuell aufgefrischt" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration section" msgstr "Konfigurationsabschnitt erfolgreich zurückgesetzt" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration values" msgstr "Konfigurationswerte erfolgreich zurückgesetzt" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "Die Geräte-Prüfsummen wurden erfolgreich aktualisiert" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "%u Bericht erfolgreich hochgeladen" msgstr[1] "%u Berichte erfolgreich hochgeladen" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "Geräteprüfsummen erfolgreich verifiziert" #. TRANSLATORS: the device showed up in time #, c-format msgid "Successfully waited %.0fms for device" msgstr "Erfolgreich %.0f ms auf das Gerät gewartet" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Zusammenfassung" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "Supervisor Mode Access Prevention" msgstr "Zugriffsverhinderung im Supervisor-Modus" #. TRANSLATORS: longer description msgid "Supervisor Mode Access Prevention ensures critical parts of device memory are not accessed by less secure programs." msgstr "Die Zugriffsverhinderung im Supervisor-Modus stellt sicher, dass weniger sichere Programme nicht auf kritische Teile des Gerätespeichers zugreifen können." #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "Unterstützt" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "Unterstützte CPU" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "Unterstützt auf dem entfernten Server" #. TRANSLATORS: Title: a better sleep state msgid "Suspend To Idle" msgstr "Im Leerlauf anhalten" #. TRANSLATORS: Title: sleep state msgid "Suspend To RAM" msgstr "Im RAM anhalten" #. TRANSLATORS: longer description msgid "Suspend to Idle allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Im Leerlauf anhalten ermöglicht es dem Gerät, sich schnell schlafen zu legen, um Strom zu sparen. Während das Gerät angehalten ist, kann sein Speicher physisch entfernt und auf seine Informationen zugegriffen werden." #. TRANSLATORS: longer description msgid "Suspend to RAM allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Im RAM anhalten ermöglicht es dem Gerät, sich schnell schlafen zu legen, um Strom zu sparen. Während das Gerät angehalten ist, kann sein Speicher physisch entfernt und auf seine Informationen zugegriffen werden." #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "Im Leerlauf anhalten" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "Im RAM anhalten" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "Zweig von %s nach %s wechseln?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "Den Firmware-Zweig auf dem Gerät wechseln" #. TRANSLATORS: command description msgid "Sync firmware versions to the chosen configuration" msgstr "Firmware-Versionen mit der ausgewählten Konfiguration synchronisieren" #. TRANSLATORS: Title: Whether firmware is locked down msgid "System Management Mode" msgstr "Systemverwaltungsmodus" #. TRANSLATORS: this CLI tool is now preventing system updates msgid "System Update Inhibited" msgstr "Systemaktualisierung blockiert" #. TRANSLATORS: longer description msgid "System management mode is used by the firmware to access resident BIOS code and data." msgstr "Der Systemverwaltungsmodus wird von der Firmware für den Zugriff auf speicherresidenten BIOS-Code und Daten verwendet." #. TRANSLATORS: as in laptop battery power msgid "System power is too low" msgstr "Stromversorgung des Systems ist zu niedrig" #. TRANSLATORS: as in laptop battery power #, c-format msgid "System power is too low (%u%%, requires %u%%)" msgstr "Das System ist zu schwach (%u%%, erfordert %u%%)" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "System benötigt externe Stromquelle" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "TEXT" #. TRANSLATORS: longer description msgid "TPM (Trusted Platform Module) is a computer chip that detects when hardware components have been tampered with." msgstr "TPM (Trusted Platform Module) ist ein Computer-Chip, der erkennt, wenn Hardware-Komponenten manipuliert wurden." #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "TPM-PCR0-Rekonstruktion" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is invalid" msgstr "TPM-PCR0-Rekonstruktion ist ungültig" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is now valid" msgstr "TPM-PCR0-Rekonstruktion ist jetzt gültig" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM Platform Configuration" msgstr "TPM-Plattformkonfiguration" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM Reconstruction" msgstr "TPM-Rekonstruktion" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM empty PCRs" msgstr "TPM leere PCRs" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "Kennzeichen" msgstr[1] "Kennzeichen" #. TRANSLATORS: we're saving all USB events for emulation msgid "Tagged for emulation" msgstr "Zur Emulation markiert" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "Verdorben" #. show the user the entire data blob msgid "Target" msgstr "Ziel" #. TRANSLATORS: command description msgid "Test a device using a JSON manifest" msgstr "Ein Gerät mit einem JSON-Manifest testen" #. TRANSLATORS: when the release was tested msgid "Tested" msgstr "Geprüft" #. TRANSLATORS: the %s is a vendor name, e.g. Lenovo #, c-format msgid "Tested by %s" msgstr "Geprüft von %s" #. TRANSLATORS: someone we trust has tested this msgid "Tested by trusted vendor" msgstr "Getestet von einem vertrauenswürdigen Anbieter" #. TRANSLATORS: the boot entry was in a legacy format msgid "The EFI boot entry is not in hive format, and shim may not be new enough to read it." msgstr "Der EFI-Boot-Eintrag ist nicht im hive-Format, und shim ist möglicherweise nicht neu genug, um ihn zu lesen." #. TRANSLATORS: try to treat the legacy format as a hive msgid "The EFI boot entry was not in hive format, falling back" msgstr "Der EFI-Boot-Eintrag war nicht im hive-Format, Rückgriff auf Reservesystem" #. TRANSLATORS: longer description msgid "The Intel Management Engine Key Manifest must be valid so that the device firmware can be trusted by the CPU." msgstr "Das Schlüsselmanifest der Intel Verwaltungs-Engine muss gültig sein, damit die Geräte-Firmware von der CPU als vertrauenswürdig eingestuft werden kann." #. TRANSLATORS: longer description msgid "The Intel Management Engine controls device components and needs to have a recent version to avoid security issues." msgstr "Die Intel Verwaltungs-Engine steuert Gerätekomponenten und muss auf dem neuesten Stand sein, um Sicherheitsprobleme zu vermeiden." #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "Der LVFS ist ein kostenloser Dienst, der als unabhängige juristische Person arbeitet und keine Verbindung zu $OS_RELEASE:NAME$ hat. Ihr Distributor hat möglicherweise keine der Firmware-Aktualisierungen auf Kompatibilität mit Ihrem System oder den angeschlossenen Geräten verifiziert. Die gesamte Firmware wird nur vom Originalhersteller zur Verfügung gestellt." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Platform Configuration is used to check whether the device start process has been modified." msgstr "Die TPM (Trusted Platform Module)-Plattformkonfiguration wird verwendet, um zu überprüfen, ob der Gerätestartprozess geändert wurde." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Reconstruction is used to check whether the device start process has been modified." msgstr "Die TPM (Trusted Platform Module)-Rekonstruktion wird verwendet, um zu überprüfen, ob der Gerätestartprozess geändert wurde." #. TRANSLATORS: this is more background on a security measurement problem msgid "The TPM PCR0 differs from reconstruction." msgstr "Das TPM PCR0 unterscheidet sich von der Rekonstruktion." #. TRANSLATORS: longer description msgid "The UEFI Platform Key is used to determine if device software comes from a trusted source." msgstr "Der UEFI-Plattformschlüssel wird verwendet, um festzustellen, ob die Gerätesoftware aus einer vertrauenswürdigen Quelle stammt." #. TRANSLATORS: HSI event title msgid "The UEFI certificate store is now up to date" msgstr "Der UEFI-Zertifikatspeicher ist jetzt auf dem neuesten Stand" #. TRANSLATORS: longer description msgid "The UEFI db contains the list of valid certificates that can be used to authorize what EFI binaries are allowed to run." msgstr "Die UEFI-DB enthält die Liste der gültigen Zertifikate, die zur Autorisierung der EFI-Binärdateien verwendet werden können." #. TRANSLATORS: longer description msgid "The UEFI system can set up memory attributes at boot which prevent common exploits from running." msgstr "Das UEFI-System kann beim Booten Speicherattribute einrichten, die die Ausführung gängiger Exploits verhindern." #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "Der Daemon hat Code von Drittanbietern geladen und wird von den Upstream-Entwicklern nicht mehr unterstützt!" #. TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name #, c-format msgid "The firmware from %s is not supplied by %s, the hardware vendor." msgstr "Die Firmware von %s wird nicht von %s, dem Hardwarehersteller, geliefert." #. TRANSLATORS: try to help msgid "The system clock has not been set correctly and downloading files may fail." msgstr "Die Systemuhr wurde nicht korrekt eingestellt und das Herunterladen von Dateien kann fehlschlagen." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been re-inserted." msgstr "Die Aktualisierung wird fortgesetzt, wenn das USB-Kabel des Geräts wieder eingesteckt wird." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged and then re-inserted." msgstr "Die Aktualisierung wird fortgesetzt, wenn das USB-Kabel des Geräts abgezogen und dann wieder eingesteckt wird." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged." msgstr "Die Aktualisierung wird fortgesetzt, wenn das USB-Kabel des Geräts abgezogen wurde." #. TRANSLATORS: warning message msgid "The update will continue when the device power cable has been removed and re-inserted." msgstr "Die Aktualisierung wird fortgesetzt, wenn das Netzkabel des Geräts entfernt und wieder eingesteckt wird." #. TRANSLATORS: naughty vendor msgid "The vendor did not supply any release notes." msgstr "Der Anbieter hat keine Versionshinweise zur Verfügung gestellt." #. TRANSLATORS: now list devices with unfixed high-priority issues msgid "There are devices with issues:" msgstr "Es gibt Geräte mit Problemen:" #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "Es gibt keine blockierten Firmware-Dateien" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "Es gibt keine genehmigte Firmware." #. TRANSLATORS: %1 is the current device version number, and %2 is the #. command name, e.g. `fwupdmgr sync` #, c-format msgid "This device will be reverted back to %s when the %s command is performed." msgstr "Dieses Gerät wird wieder auf %s zurückgesetzt, wenn der Befehl %s ausgeführt wird." #. TRANSLATORS: the vendor did not upload this msgid "This firmware is provided by LVFS community members and is not provided (or supported) by the original hardware vendor." msgstr "Diese Firmware wird von Mitgliedern der LVFS-Gemeinschaft zur Verfügung gestellt und wird nicht vom ursprünglichen Hardwareanbieter bereitgestellt (oder unterstützt)." #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "Dieses Paket wurde nicht validiert und könnte nicht richtig funktionieren." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "Dieses Programm funktioniert möglicherweise nur als root korrekt" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "Diese Gegenstelle enthält Firmware, die nicht unter Embargo steht, sondern vom Hardwareanbieter noch getestet wird. Sie sollten sicherstellen, dass Sie eine Möglichkeit haben, die Firmware manuell zurückzustufen, falls die Firmware-Aktualisierung fehlschlägt." #. TRANSLATORS: error message msgid "This system doesn't support firmware settings" msgstr "Dieses System unterstützt keine Firmware-Einstellungen" #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "Dieses System hat HSI-Laufzeitprobleme." #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "Dieses System hat eine niedrige HSI-Sicherheitsstufe." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "Mit diesem Werkzeug kann ein Administrator UEFI dbx-Aktualisierungen anwenden." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "Mit diesem Werkzeug kann ein Administrator den fwupd-Daemon abfragen und steuern, sodass er Aktionen wie die Installation oder das Herabstufen von Firmware durchführen kann." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "Dieses Werkzeug ermöglicht es einem Administrator, die fwupd-Plugins zu verwenden, ohne auf dem Host-System installiert sein zu müssen." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can add a kernel argument of '%s', but it will only be active after restarting the computer." msgstr "Dieses Werkzeug kann den Kernel-Parameter »%s« hinzufügen, aber dieser wird erst nach einem Neustart des Rechners aktiv." #. TRANSLATORS: the %1 is a BIOS setting name. #. * %2 and %3 are the values, e.g. "True" or "Windows10" #, c-format msgid "This tool can change the BIOS setting '%s' from '%s' to '%s' automatically, but it will only be active after restarting the computer." msgstr "Dieses Werkzeug kann die BIOS-Einstellung »%s« automatisch von »%s« auf »%s« ändern, aber sie wird erst nach einem Neustart des Rechners aktiv." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can change the kernel argument from '%s' to '%s', but it will only be active after restarting the computer." msgstr "Dieses Werkzeug kann den Kernel-Parameter von »%s« auf »%s« ändern, aber dieser wird erst nach einem Neustart des Rechners aktiv." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "Dieses Werkzeug liest und parst das TPM-Ereignisprotokoll aus der System-Firmware." #. TRANSLATORS: the update state of the specific device msgid "Transient failure" msgstr "Vorübergehender Fehler" #. TRANSLATORS: item is TRUE msgid "True" msgstr "Wahr" #. TRANSLATORS: We verified the metadata against the server msgid "Trusted metadata" msgstr "Vertrauenswürdige Metadaten" #. TRANSLATORS: We verified the payload against the server msgid "Trusted payload" msgstr "Vertrauenswürdige Nutzdaten" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Typ" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI Bootservice Variables" msgstr "UEFI-Bootdienst-Variablen" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition may not be set up correctly" msgstr "Die UEFI-ESP-Partition ist möglicherweise nicht korrekt eingerichtet" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "UEFI ESP-Partition nicht erkannt oder konfiguriert" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI Memory Protection" msgstr "UEFI-Speicherschutz" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "UEFI-Plattformschlüssel" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "Sicherer UEFI-Boot" #. TRANSLATORS: longer description msgid "UEFI Secure Boot prevents malicious software from being loaded when the device starts." msgstr "UEFI Secure Boot verhindert, dass bösartige Software beim Start des Geräts geladen wird." #. TRANSLATORS: longer description msgid "UEFI boot service variables should not be readable from runtime mode." msgstr "UEFI-Bootdienst-Variablen sollten im Laufzeitmodus nicht lesbar sein." #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI bootservice variables" msgstr "UEFI-Bootdienst-Variablen" #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "Aktualisierung der UEFI-Kapsel nicht verfügbar oder in der Firmware-Einrichtung aktiviert" #. TRANSLATORS: Title: is UEFI db up-to-date msgid "UEFI db" msgstr "UEFI-DB" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "UEFI dbx-Dienstprogramm" #. TRANSLATORS: system is not booted in UEFI mode msgid "UEFI firmware can not be updated in legacy BIOS mode" msgstr "UEFI-Firmware kann im veralteten BIOS-Modus nicht aktualisiert werden" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI memory protection" msgstr "UEFI-Speicherschutz" #. TRANSLATORS: HSI event title msgid "UEFI memory protection enabled and locked" msgstr "UEFI-Speicherschutz aktiviert und gesperrt" #. TRANSLATORS: HSI event title msgid "UEFI memory protection enabled but not locked" msgstr "UEFI-Speicherschutz aktiviert, aber nicht gesperrt" #. TRANSLATORS: HSI event title msgid "UEFI memory protection is now locked" msgstr "UEFI-Speicherschutz ist jetzt gesperrt" #. TRANSLATORS: HSI event title msgid "UEFI memory protection is now unlocked" msgstr "UEFI-Speicherschutz ist jetzt entsperrt" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "UEFI-Plattformschlüssel" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "Sicherer UEFI-Boot" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "Es konnte keine Verbindung zum Dienst hergestellt werden" #. TRANSLATORS: error message msgid "Unable to find attribute" msgstr "Attribut kann nicht gefunden werden" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "Aktuellen Treiber entbinden" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "Firmware entblocken:" #. TRANSLATORS: command description msgid "Unblocks a specific firmware from being installed" msgstr "Entblockt die Installation einer bestimmten Firmware" #. TRANSLATORS: command description msgid "Undo the host security attribute fix" msgstr "Korrektur des Sicherheitsattributs des Hosts rückgängig machen" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "Entschlüsselt" #. TRANSLATORS: command description msgid "Uninhibit the system to allow upgrades" msgstr "System entblocken, um Aktualisierungen zu ermöglichen" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Unbekannt" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Unbekanntes Gerät" msgid "Unlock the device to allow access" msgstr "Das Gerät entsperren, um Zugriff zu ermöglichen" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "Entsperrt" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "Entsperrt das Gerät für Zugriff auf die Firmware" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "Hängt das ESP aus" #. TRANSLATORS: message shown after device has been marked for emulation msgid "Unplug and replug the device to continue the update process." msgstr "Stecken Sie das Gerät aus und wieder ein, um den Aktualisierungsvorgang fortzusetzen." #. TRANSLATORS: firmware payload is unsigned and it is possible to modify it msgid "Unsigned Payload" msgstr "Unsignierte Nutzdaten" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "Nicht unterstützte Daemon-Version %s, Client-Version ist %s" #. TRANSLATORS: Suffix: the HSI result msgid "Untainted" msgstr "Unverdorben" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "Aktualisierbar" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Aktualisierungsfehler" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "Aktualisierungsabbild" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "Aktualisierungsmeldung" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "Aktualisierungsstatus" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "Der Aktualisierungsfehler ist ein bekanntes Problem, besuchen Sie diese URL für weitere Informationen:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Jetzt aktualisieren?" #. TRANSLATORS: command description msgid "Update the stored cryptographic hash with current ROM contents" msgstr "Den gespeicherten kryptografischen Hash mit dem aktuellen ROM-Inhalt aktualisieren" msgid "Update the stored device verification information" msgstr "Gespeicherte Geräteverifizierungsinformationen aktualisieren" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "Die gespeicherten Metadaten mit dem aktuellen Inhalt aktualisieren" #. TRANSLATORS: command description msgid "Updates all specified devices to latest firmware version, or all devices if unspecified" msgstr "Aktualisiert alle angegebenen Geräte auf die neueste Firmware-Version, oder alle Geräte, wenn nicht angegeben" #. TRANSLATORS: how many local devices can expect updates now #, c-format msgid "Updates have been published for %u local device" msgid_plural "Updates have been published for %u of %u local devices" msgstr[0] "Es wurden Aktualisierungen für %u lokales Gerät veröffentlicht" msgstr[1] "Es wurden Aktualisierungen für %u von %u der lokalen Geräte veröffentlicht" msgid "Updating" msgstr "Wird aktualisiert" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "%s wird aktualisiert …" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "%s von %s auf %s aktualisieren?" #. TRANSLATORS: ask the user to upload msgid "Upload data now?" msgstr "Daten jetzt hochladen?" #. TRANSLATORS: command description msgid "Upload the list of updatable devices to a remote server" msgstr "Liste der aktualisierbaren Geräte auf einen entfernten Server hochladen" #. TRANSLATORS: ask the user to share, %s is something #. * like: "Linux Vendor Firmware Service" #, c-format msgid "Upload these anonymous results to the %s to help other users?" msgstr "Diese anonymen Ergebnisse zum %s hochladen, um anderen Nutzern zu helfen?" #. TRANSLATORS: explain why we want to upload #, c-format msgid "Uploading a device list allows the %s team to know what hardware exists, and allows us to put pressure on vendors that do not upload firmware updates for their hardware." msgstr "Durch das Hochladen einer Geräteliste weiß das Team von %s, welche Hardware vorhanden ist, und wir können Druck auf Hersteller ausüben, die keine Firmware-Aktualisierungen für ihre Hardware hochladen." #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "Das Hochladen von Firmware-Berichten hilft Hardwareherstellern, fehlerhafte und erfolgreiche Aktualisierungen auf realen Geräten schnell zu erkennen." #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "Dringlichkeit" #. TRANSLATORS: explain how to get help, %1 is #. * 'fwupdtool --help' #. TRANSLATORS: explain how to get help, #. * where $1 is something like 'fwupdmgr --help' #, c-format msgid "Use %s for help" msgstr "Verwenden Sie %s für Hilfe" #. TRANSLATORS: CTRL^C [holding control, and then pressing C] will exit the #. program msgid "Use CTRL^C to cancel." msgstr "Verwenden Sie STRG^C zum Abbrechen." #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "Benutzer wurde benachrichtigt" #. TRANSLATORS: remote filename base msgid "Username" msgstr "Benutzername" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "VERSION1 VERSION2 [FORMAT]" msgstr "VERSION1 VERSION2 [FORMAT]" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "Gültig" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "Validierung der ESP-Inhalte …" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "Variante" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "Anbieter" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "Wird verifiziert…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Version" #. TRANSLATORS: the fwupd version the release was tested on msgid "Version[fwupd]" msgstr "Version[fwupd]" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "WARNUNG" #. TRANSLATORS: command description msgid "Wait for a device to appear" msgstr "Warten, bis ein Gerät erscheint" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "Warten …" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "Auf Hardware-Änderungen achten" #. TRANSLATORS: check various UEFI and ACPI tables are unchanged after the #. update msgid "Will measure elements of system integrity around an update" msgstr "Misst Elemente der Systemintegrität im Zusammenhang mit einer Aktualisierung" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "Datei wird geschrieben:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Wird geschrieben …" #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from a recovery or installation disk, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "Sie sollten sich vergewissern, dass Sie die Einstellung aus einer Wiederherstellung oder von Installations-Disketten zurückspielen können, weil diese Änderung dazu führen kann, dass das System nicht mehr Linux startet oder andere Systeminstabilitäten verursacht." #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from the system firmware setup, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "Sie sollten sich vergewissern, dass Sie die Einstellung aus der System-Firmware-Einrichtung wiederherstellen können, da diese Änderung dazu führen kann, dass das System nicht in Linux startet oder andere Systeminstabilitäten verursacht." #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "Ihr Händler hat möglicherweise keine der Firmware-Aktualisierungen auf Kompatibilität mit Ihrem System oder den angeschlossenen Geräten verifiziert." #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "Ihre Hardware kann durch die Verwendung dieser Firmware beschädigt werden und die Installation dieser Version kann zum Erlöschen jeglicher Garantie mit %s führen." #. TRANSLATORS: BKC is the industry name for the best known configuration and #. is a set #. * of firmware that works together #, c-format msgid "Your system is set up to the BKC of %s." msgstr "Ihr System ist auf die BKC von %s eingestellt." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[APPSTREAM_ID]" msgstr "[APPSTREAM_KENNUNG]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[PRÜFSUMME]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[GERÄTEKENNUNG|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[GERÄTEKENNUNG|GUID] [ZWEIG]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [VERSION]" msgstr "[GERÄTEKENNUNG|GUID] [VERSION]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE]" msgstr "[GERÄT]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILE FILE_SIG REMOTE-ID]" msgstr "[DATEI DATEI_SIG GEGENSTELLENKENNUNG]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[DATEINAME1] [DATEINAME2]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FWUPD-VERSION]" msgstr "[FWUPD-VERSION]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[REASON] [TIMEOUT]" msgstr "[GRUND] [ZEITÜBERSCHREITUNG]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SECTION] KEY VALUE" msgstr "[ABSCHNITT] SCHLÜSSEL WERT" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2] [--no-authenticate]" msgstr "[EINSTELLUNG1] [EINSTELLUNG2] [--no-authenticate]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2]..." msgstr "[EINSTELLUNG1] [EINSTELLUNG2]..." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[SMBIOS-DATEI|HWIDS-DATEI]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "Standard" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "fwupd TPM-Ereignisprotokolldienstprogramm" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "fwupd-Plugins" fwupd-2.0.10/po/en_GB.po000066400000000000000000003552641501337203100146610ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Andi Chandler , 2019-2020,2022 # Bruce Cowan , 2024 # Richard Hughes , 2015,2017-2025 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: English (United Kingdom) (http://app.transifex.com/freedesktop/fwupd/language/en_GB/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: en_GB\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "%.0f minute remaining" msgstr[1] "%.0f minutes remaining" #. TRANSLATORS: BMC refers to baseboard management controller which #. * is the device that updates all the other firmware on the system #, c-format msgid "%s BMC Update" msgstr "%s BMC Update" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "%s Battery Update" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "%s CPU Microcode Update" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "%s Camera Update" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "%s Configuration Update" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "%s Consumer ME Update" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "%s Controller Update" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "%s Corporate ME Update" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "%s Device Update" #. TRANSLATORS: Video Display refers to the laptop internal display or #. * external monitor #, c-format msgid "%s Display Update" msgstr "%s Display Update" #. TRANSLATORS: Dock refers to the port replicator hardware laptops are #. * cradled in, or lowered onto #, c-format msgid "%s Dock Update" msgstr "%s Dock Update" #. TRANSLATORS: drive refers to a storage device, e.g. SATA disk #, c-format msgid "%s Drive Update" msgstr "%s Drive Update" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "%s Embedded Controller Update" #. TRANSLATORS: a device that can read your fingerprint pattern #, c-format msgid "%s Fingerprint Reader Update" msgstr "%s Fingerprint Reader Update" #. TRANSLATORS: flash refers to solid state storage, e.g. UFS or eMMC #, c-format msgid "%s Flash Drive Update" msgstr "%s Flash Drive Update" #. TRANSLATORS: GPU refers to a Graphics Processing Unit, e.g. #. * the "video card" #, c-format msgid "%s GPU Update" msgstr "%s GPU Update" #. TRANSLATORS: a large pressure-sensitive drawing area typically used #. * by artists and digital artists #, c-format msgid "%s Graphics Tablet Update" msgstr "%s Graphics Tablet Update" #. TRANSLATORS: two miniature speakers attached to your ears #, c-format msgid "%s Headphones Update" msgstr "%s Headphones Update" #. TRANSLATORS: headphones with an integrated microphone #, c-format msgid "%s Headset Update" msgstr "%s Headset Update" #. TRANSLATORS: an input device used by gamers, e.g. a joystick #, c-format msgid "%s Input Controller Update" msgstr "%s Input Controller Update" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "%s Keyboard Update" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "%s ME Update" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "%s Mouse Update" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "%s Network Interface Update" #. TRANSLATORS: SSD refers to a Solid State Drive, e.g. non-rotating #. * SATA or NVMe disk #, c-format msgid "%s SSD Update" msgstr "%s SSD Update" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "%s Storage Controller Update" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "%s System Update" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "%s TPM Update" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "%s Thunderbolt Controller Update" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "%s Touchpad Update" #. TRANSLATORS: Dock refers to the port replicator device connected #. * by plugging in a USB cable -- which may or may not also provide power #, c-format msgid "%s USB Dock Update" msgstr "%s USB Dock Update" #. TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth #. * device that stays in the USB port so the wireless peripheral works #, c-format msgid "%s USB Receiver Update" msgstr "%s USB Receiver Update" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "%s Update" #. TRANSLATORS: warn the user before updating, %1 is a device name #, c-format msgid "%s and all connected devices may not be usable while updating." msgstr "%s and all connected devices may not be usable while updating." #. TRANSLATORS: %1 refers to some kind of security test, e.g. "Encrypted RAM". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s appeared: %s" msgstr "%s appeared: %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "UEFI platform #. key". #. * %2 and %3 refer to results value, e.g. "Valid" and "Invalid" #, c-format msgid "%s changed: %s → %s" msgstr "%s changed: %s → %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "SPI BIOS #. region". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s disappeared: %s" msgstr "%s disappeared: %s" #. TRANSLATORS: the device has a reason it can't update, e.g. laptop lid #. closed #, c-format msgid "%s is not currently updatable" msgstr "%s is not currently updatable" #. TRANSLATORS: %1 is a device name, e.g. "ThinkPad Universal #. * ThunderBolt 4 Dock" and %2 is "fwupdmgr activate" #, c-format msgid "%s is pending activation; use %s to complete the update." msgstr "%s is pending activation; use %s to complete the update." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "%s manufacturing mode" #. TRANSLATORS: warn the user before #. * updating, %1 is a device name #, c-format msgid "%s must remain connected for the duration of the update to avoid damage." msgstr "%s must remain connected for the duration of the update to avoid damage." #. TRANSLATORS: warn the user before updating, %1 is a machine #. * name #, c-format msgid "%s must remain plugged into a power source for the duration of the update to avoid damage." msgstr "%s must remain plugged into a power source for the duration of the update to avoid damage." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "%s override" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "%s version" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u day" msgstr[1] "%u days" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "%u device has a firmware upgrade available." msgstr[1] "%u devices have a firmware upgrade available." #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device is not the best known configuration." msgid_plural "%u devices are not the best known configuration." msgstr[0] "%u device is not the best known configuration." msgstr[1] "%u devices are not the best known configuration." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u hour" msgstr[1] "%u hours" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minute" msgstr[1] "%u minutes" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u second" msgstr[1] "%u seconds" #. TRANSLATORS: device tests can be specific to a CPU type #, c-format msgid "%u test was skipped" msgid_plural "%u tests were skipped" msgstr[0] "%u test was skipped" msgstr[1] "%u tests were skipped" #. TRANSLATORS: first percentage is current value, 2nd percentage is the #. * lowest limit the firmware update is allowed for the update to happen #, c-format msgid "%u%% (threshold %u%%)" msgstr "%u%% (threshold %u%%)" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(obsoleted)" #. TRANSLATORS: HSI event title msgid "A TPM PCR is now an invalid value" msgstr "A TPM PCR is now an invalid value" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "AMD Firmware Replay Protection" msgstr "AMD Firmware Replay Protection" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "AMD Firmware Write Protection" msgstr "AMD Firmware Write Protection" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "AMD Secure Processor Rollback Protection" msgstr "AMD Secure Processor Rollback Protection" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "ARCHIVE FIRMWARE METAINFO [FIRMWARE] [METAINFO] [JCATFILE]" msgstr "ARCHIVE FIRMWARE METAINFO [FIRMWARE] [METAINFO] [JCATFILE]" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "Action Required:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "Activate devices" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "Activate pending devices" msgid "Activate the new firmware on the device" msgstr "Activate the new firmware on the device" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "Activating firmware update" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "Activating firmware update for" #. TRANSLATORS: command description msgid "Adds devices to watch for future emulation" msgstr "Adds devices to watch for future emulation" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Age" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "Agree and enable the remote?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Alias to %s" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are now valid" msgstr "All TPM PCRs are now valid" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are valid" msgstr "All TPM PCRs are valid" #. TRANSLATORS: an application is preventing system updates msgid "All devices are prevented from update by system inhibit" msgstr "All devices are prevented from update by system inhibit" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "All devices of the same type will be updated at the same time" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Allow downgrading firmware versions" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "Allow reinstalling existing firmware versions" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "Allow switching firmware branch" #. TRANSLATORS: error message msgid "Already exists, and no --force specified" msgstr "Already exists, and no --force specified" #. TRANSLATORS: is not the main firmware stream msgid "Alternate branch" msgstr "Alternate branch" #. TRANSLATORS: another application is updating the device already msgid "An update is in progress" msgstr "An update is in progress" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "An update requires a reboot to complete." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "An update requires the system to shutdown to complete." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Answer yes to all questions" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Apply update even when not advised" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Apply update files" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "Applying update…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Approved firmware:" msgstr[1] "Approved firmware:" #. TRANSLATORS: command description msgid "Asks the daemon to quit" msgstr "Asks the daemon to quit" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "Attach to firmware mode" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Authenticating…" #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "Authentication details are required" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Authentication is required to downgrade the firmware on a removable device" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Authentication is required to downgrade the firmware on this machine" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to enable emulation data collection" msgstr "Authentication is required to enable emulation data collection" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to fix a host security issue" msgstr "Authentication is required to fix a host security issue" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to load hardware emulation data" msgstr "Authentication is required to load hardware emulation data" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "Authentication is required to modify BIOS settings" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "Authentication is required to modify a configured remote used for firmware updates" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "Authentication is required to modify daemon configuration" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "Authentication is required to read BIOS settings" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to reset daemon configuration to defaults" msgstr "Authentication is required to reset daemon configuration to defaults" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to save hardware emulation data" msgstr "Authentication is required to save hardware emulation data" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "Authentication is required to set the list of approved firmware" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "Authentication is required to sign data using the client certificate" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to stop the firmware update service" msgstr "Authentication is required to stop the firmware update service" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "Authentication is required to switch to the new firmware version" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to undo the fix for a host security issue" msgstr "Authentication is required to undo the fix for a host security issue" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Authentication is required to unlock a device" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Authentication is required to update the firmware on a removable device" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Authentication is required to update the firmware on this machine" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "Authentication is required to update the stored checksums for the device" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "Automatic Reporting" #. TRANSLATORS: we can auto-uninhibit after a timeout #, c-format msgid "Automatically uninhibiting in %ums…" msgstr "Automatically uninhibiting in %ums…" #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "Automatically upload every time?" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS Firmware Updates" msgstr "BIOS Firmware Updates" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS Rollback Protection" msgstr "BIOS Rollback Protection" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS firmware updates" msgstr "BIOS firmware updates" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS rollback protection" msgstr "BIOS rollback protection" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "BIOS updates delivered via LVFS or Windows Update" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "BUILDER-XML FILENAME-DST" msgstr "BUILDER-XML FILENAME-DST" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "Battery" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "Bind new kernel driver" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "Blocked firmware files:" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "Blocked version" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "Blocking firmware:" #. TRANSLATORS: command description msgid "Blocks a specific firmware from being installed" msgstr "Blocks a specific firmware from being installed" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "Bootloader Version" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "Branch" #. TRANSLATORS: command description msgid "Build a cabinet archive from a firmware blob and XML metadata" msgstr "Build a cabinet archive from a firmware blob and XML metadata" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "Build a firmware file" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * Utilized by OS means the distribution enabled it msgid "CET OS Support" msgstr "CET OS Support" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * enabled means supported by the processor msgid "CET Platform" msgstr "CET Platform" #. TRANSLATORS: longer description msgid "CPU Microcode must be updated to mitigate against various information-disclosure security issues." msgstr "CPU Microcode must be updated to mitigate against various information-disclosure security issues." #. TRANSLATORS: we can save all device enumeration events for emulation msgid "Can tag for emulation" msgstr "Can tag for emulation" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Cancel" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "Cancelled" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "Cannot apply as dbx update has already been applied." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Changed" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "Checks cryptographic hash matches firmware" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Checksum" #. TRANSLATORS: get interactive prompt, where branch is the #. * supplier of the firmware, e.g. "non-free" or "free" msgid "Choose branch" msgstr "Choose branch" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "Choose device" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "Choose firmware" #. TRANSLATORS: get interactive prompt msgid "Choose release" msgstr "Choose release" #. TRANSLATORS: get interactive prompt msgid "Choose volume" msgstr "Choose volume" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "Clears the results from the last update" #. TRANSLATORS: error message msgid "Command not found" msgstr "Command not found" #. TRANSLATORS: is not supported by the vendor msgid "Community supported" msgstr "Community supported" #. TRANSLATORS: command description msgid "Compares two versions for equality" msgstr "Compares two versions for equality" #. TRANSLATORS: title prefix for the BIOS settings dialog msgid "Configuration Change Suggested" msgstr "Configuration Change Suggested" #. TRANSLATORS: no peeking msgid "Configuration is only readable by the system administrator" msgstr "Configuration is only readable by the system administrator" #. TRANSLATORS: longer description msgid "Control-Flow Enforcement Technology detects and prevents certain methods for running malicious software on the device." msgstr "Control-Flow Enforcement Technology detects and prevents certain methods for running malicious software on the device." #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology msgid "Control-flow Enforcement Technology" msgstr "Control-flow Enforcement Technology" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "Convert a firmware file" #. TRANSLATORS: command description msgid "Create an EFI boot entry" msgstr "Create an EFI boot entry" #. TRANSLATORS: when the update was built msgid "Created" msgstr "Created" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "Critical" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "Cryptographic hash verification is available" #. TRANSLATORS: current value of a BIOS setting msgid "Current Value" msgstr "Current Value" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "Current version" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "DEVICE-ID|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Debugging Options" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "Decompressing…" #. TRANSLATORS: command description msgid "Delete an EFI boot entry" msgstr "Delete an EFI boot entry" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Description" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "Detach to bootloader mode" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "Details" #. TRANSLATORS: the best known configuration is a set of software that we know #. works #. * well together. In the OEM and ODM industries it is often called a BKC msgid "Deviate from the best known configuration?" msgstr "Deviate from the best known configuration?" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "Device Flags" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "Device ID" #. TRANSLATORS: description of the device requests msgid "Device Requests" msgstr "Device Requests" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Device added:" #. TRANSLATORS: the device is already connected msgid "Device already exists" msgstr "Device already exists" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "Device battery power is too low" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse #, c-format msgid "Device battery power is too low (%u%%, requires %u%%)" msgstr "Device battery power is too low (%u%%, requires %u%%)" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "Device can recover flash failures" #. TRANSLATORS: lid means "laptop top cover" msgid "Device cannot be updated while the lid is closed" msgstr "Device cannot be updated while the lid is closed" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Device changed:" #. TRANSLATORS: a version check is required for all firmware msgid "Device firmware is required to have a version check" msgstr "Device firmware is required to have a version check" #. TRANSLATORS: emulated means we are pretending to be a different model msgid "Device is emulated" msgstr "Device is emulated" #. TRANSLATORS: device cannot be interrupted, for instance taking a phone call msgid "Device is in use" msgstr "Device is in use" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "Device is locked" #. TRANSLATORS: we have two ways of communicating with the device, so we hide #. one msgid "Device is lower priority than an equivalent device" msgstr "Device is lower priority than an equivalent device" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "Device is required to install all provided releases" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "Device is unreachable" #. TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode msgid "Device is unreachable, or out of wireless range" msgstr "Device is unreachable, or out of wireless range" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "Device is usable for the duration of the update" #. TRANSLATORS: usually this is when we're waiting for a reboot msgid "Device is waiting for the update to be applied" msgstr "Device is waiting for the update to be applied" #. TRANSLATORS: success, so say thank you to the user msgid "Device list uploaded successfully, thanks!" msgstr "Device list uploaded successfully, thanks!" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Device removed:" #. TRANSLATORS: as in, wired mains power for a laptop msgid "Device requires AC power to be connected" msgstr "Device requires AC power to be connected" #. TRANSLATORS: device does not have a display connected msgid "Device requires a display to be plugged in" msgstr "Device requires a display to be plugged in" #. TRANSLATORS: The device cannot be updated due to missing vendor's license." msgid "Device requires a software license to update" msgstr "Device requires a software license to update" #. TRANSLATORS: longer description msgid "Device software updates are provided for this device." msgstr "Device software updates are provided for this device." #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "Device stages updates" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "Device supports switching to a different branch of firmware" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "Device update needs activation" #. TRANSLATORS: save the old firmware to disk before installing the new one msgid "Device will backup firmware before installing" msgstr "Device will backup firmware before installing" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "Device will not re-appear after update completes" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "Devices that have been updated successfully:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "Devices that were not updated correctly:" #. TRANSLATORS: message letting the user there is an update #. * waiting, but there is a reason it cannot be deployed msgid "Devices with firmware updates that need user action: " msgstr "Devices with firmware updates that need user action: " #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "Devices with no available firmware updates: " #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "Devices with the latest available firmware version:" #. TRANSLATORS: this is for the device tests msgid "Did not find any devices with matching GUIDs" msgstr "Did not find any devices with matching GUIDs" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "Disabled" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "Disables a given remote" #. TRANSLATORS: command description msgid "Disables virtual testing devices" msgstr "Disables virtual testing devices" #. TRANSLATORS: the OS the release was tested on msgid "Distribution" msgstr "Distribution" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "Do not check for old metadata" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "Do not check for unreported history" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "Do not check if download remotes should be enabled" #. TRANSLATORS: command line option msgid "Do not check or prompt for reboot after update" msgstr "Do not check or prompt for reboot after update" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "Do not include log domain prefix" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "Do not include timestamp prefix" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "Do not perform device safety checks" #. TRANSLATORS: command line option msgid "Do not prompt for devices" msgstr "Do not prompt for devices" #. TRANSLATORS: command line option msgid "Do not prompt to fix security issues" msgstr "Do not prompt to fix security issues" #. TRANSLATORS: command line option msgid "Do not search the firmware when parsing" msgstr "Do not search the firmware when parsing" #. TRANSLATORS: warning message shown after update has been scheduled msgid "Do not turn off your computer or remove the AC adaptor while the update is in progress." msgstr "Do not turn off your computer or remove the AC adaptor while the update is in progress." #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "Do not write to the history database" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "Do you understand the consequences of changing the firmware branch?" #. TRANSLATORS: ask the user if it's okay to convert, #. * "it" being the data contained in the EFI boot entry msgid "Do you want to convert it now?" msgstr "Do you want to convert it now?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "Do you want to disable this feature for future updates?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "Do you want to refresh this remote now?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "Do you want to upload reports automatically for future updates?" #. TRANSLATORS: command line option msgid "Don't prompt for authentication (less details may be shown)" msgstr "Don't prompt for authentication (less details may be shown)" #. TRANSLATORS: success msgid "Done!" msgstr "Done!" #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "Downgrade %s from %s to %s?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "Downgrades the firmware on a device" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "Downgrading %s…" #. TRANSLATORS: command description msgid "Download a file" msgstr "Download a file" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Downloading…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "Dump SMBIOS data from a file" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "Duration" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "EMULATION-FILE [ARCHIVE-FILE]" msgstr "EMULATION-FILE [ARCHIVE-FILE]" #. TRANSLATORS: longer description msgid "Each system should have tests to ensure firmware security." msgstr "Each system should have tests to ensure firmware security." #. TRANSLATORS: command description msgid "Emulate a device using a JSON manifest" msgstr "Emulate a device using a JSON manifest" #. TRANSLATORS: this device is not actually real msgid "Emulated" msgstr "Emulated" #. TRANSLATORS: Title: if we are emulating a different host msgid "Emulated host" msgstr "Emulated host" msgid "Enable" msgstr "Enable" msgid "Enable emulation data collection" msgstr "Enable emulation data collection" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "Enable new remote?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "Enable this remote?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Enabled" #. TRANSLATORS: Plugin is active only if hardware is found msgid "Enabled if hardware matches" msgstr "Enabled if hardware matches" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "Enables a given remote" #. TRANSLATORS: command description msgid "Enables virtual testing devices" msgstr "Enables virtual testing devices" #. TRANSLATORS: longer description msgid "Enabling firmware updates for the BIOS allows fixing security issues." msgstr "Enabling firmware updates for the BIOS allows fixing security issues." msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "Enabling this remote is done at your own risk." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Encrypted" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "Encrypted RAM" msgid "Encrypted RAM makes it impossible for information that is stored in device memory to be read if the memory chip is removed and accessed." msgstr "Encrypted RAM makes it impossible for information that is stored in device memory to be read if the memory chip is removed and accessed." #. TRANSLATORS: the vendor is no longer supporting the device msgid "End of life" msgstr "End of life" #. TRANSLATORS: The BIOS setting can only be changed to fixed values msgid "Enumeration" msgstr "Enumeration" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "Erase all firmware update history" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Erasing…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "Exit after a small delay" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "Exit after the engine has loaded" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "Export a firmware file structure to XML" #. TRANSLATORS: command description msgid "Export firmware history for manual upload" msgstr "Export firmware history for manual upload" #. TRANSLATORS: command description msgid "Extract a firmware blob to images" msgstr "Extract a firmware blob to images" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "FILE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "FILE [DEVICE-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "FILENAME" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "FILENAME CERTIFICATE PRIVATE-KEY" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "FILENAME DEVICE-ID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME OFFSET DATA [FIRMWARE-TYPE]" msgstr "FILENAME OFFSET DATA [FIRMWARE-TYPE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "FILENAME [DEVICE-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "FILENAME [FIRMWARE-TYPE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" msgstr "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" msgstr "FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "Failed" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Failed to apply update" #. TRANSLATORS: error message for Windows msgid "Failed to connect to Windows service, please ensure it's running." msgstr "Failed to connect to Windows service, please ensure it's running." #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "Failed to connect to daemon" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "Failed to load local dbx" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "Failed to load system dbx" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "Failed to lock" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Failed to parse arguments" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "Failed to parse file" #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #, c-format msgid "Failed to parse flags for %s" msgstr "Failed to parse flags for %s" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "Failed to parse local dbx" #. TRANSLATORS: a feature is something like "can show an image" msgid "Failed to set front-end features" msgstr "Failed to set front-end features" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "Failed to validate ESP contents" #. TRANSLATORS: item is FALSE msgid "False" msgstr "False" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Filename" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "Filename Signature" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "Filename Source" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Filename required" #. TRANSLATORS: command line option msgid "Filter with a set of device flags using a ~ prefix to exclude, e.g. 'internal,~needs-reboot'" msgstr "Filter with a set of device flags using a ~ prefix to exclude, e.g. 'internal,~needs-reboot'" #. TRANSLATORS: command line option msgid "Filter with a set of release flags using a ~ prefix to exclude, e.g. 'trusted-release,~trusted-metadata'" msgstr "Filter with a set of release flags using a ~ prefix to exclude, e.g. 'trusted-release,~trusted-metadata'" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware Attestation" msgstr "Firmware Attestation" #. TRANSLATORS: longer description msgid "Firmware Attestation checks device software using a reference copy, to make sure that it has not been changed." msgstr "Firmware Attestation checks device software using a reference copy, to make sure that it has not been changed." #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "Firmware BIOS Descriptor" #. TRANSLATORS: longer description msgid "Firmware BIOS Descriptor protects device firmware memory from being tampered with." msgstr "Firmware BIOS Descriptor protects device firmware memory from being tampered with." #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "Firmware BIOS Region" #. TRANSLATORS: longer description msgid "Firmware BIOS Region protects device firmware memory from being tampered with." msgstr "Firmware BIOS Region protects device firmware memory from being tampered with." #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "Firmware Base URI" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "Firmware Update D-Bus Service" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Firmware Update Daemon" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "Firmware Updater Verification" msgid "Firmware Updater Verification checks that software used for updating has not been tampered with." msgstr "Firmware Updater Verification checks that software used for updating has not been tampered with." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "Firmware Updates" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "Firmware Utility" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "Firmware Write Protection" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "Firmware Write Protection Lock" #. TRANSLATORS: longer description msgid "Firmware Write Protection protects device firmware memory from being tampered with." msgstr "Firmware Write Protection protects device firmware memory from being tampered with." #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "Firmware attestation" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "Firmware is already blocked" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "Firmware is not already blocked" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "Firmware metadata has not been updated for %u day and may not be up to date." msgstr[1] "Firmware metadata has not been updated for %u days and may not be up to date." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "Firmware updates" #. TRANSLATORS: user needs to run a command, %1 is 'fwupdmgr unlock' #, c-format msgid "Firmware updates disabled; run '%s' to enable" msgstr "Firmware updates disabled; run '%s' to enable" #. TRANSLATORS: command description msgid "Fix a specific host security attribute" msgstr "Fix a specific host security attribute" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fix reverted successfully" msgstr "Fix reverted successfully" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fixed successfully" msgstr "Fixed successfully" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "Flags" #. TRANSLATORS: command line option msgid "Force the action by relaxing some runtime checks" msgstr "Force the action by relaxing some runtime checks" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Found" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "Full Disk Encryption Detected" #. TRANSLATORS: we might ask the user the recovery key when next booting #. Windows msgid "Full disk encryption secrets may be invalidated when updating" msgstr "Full disk encryption secrets may be invalidated when updating" #. TRANSLATORS: Title: if the part has been fused msgid "Fused Platform" msgstr "Fused Platform" #. TRANSLATORS: Title: if the part has been fused msgid "Fused platform" msgstr "Fused platform" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" msgstr[1] "GUIDs" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "GUID|DEVICE-ID" msgstr "GUID|DEVICE-ID" msgid "Get BIOS settings" msgstr "Get BIOS settings" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "Get all device flags supported by fwupd" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Get all devices that support firmware updates" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "Get all enabled plug-ins registered with the system" #. TRANSLATORS: command description msgid "Get all known version formats" msgstr "Get all known version formats" #. TRANSLATORS: command description msgid "Get device report metadata" msgstr "Get device report metadata" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Gets details about a firmware file" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "Gets the configured remotes" #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "Gets the host security attributes" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "Gets the list of approved firmware" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "Gets the list of blocked firmware" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Gets the list of updates for connected hardware" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "Gets the releases for a device" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Gets the results from the last update" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "HWIDS-FILE" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "Hardware is waiting to be replugged" #. TRANSLATORS: the release urgency msgid "High" msgstr "High" #. TRANSLATORS: title for host security events msgid "Host Security Events" msgstr "Host Security Events" #. TRANSLATORS: error message for unsupported feature #, c-format msgid "Host Security ID (HSI) is not supported" msgstr "Host Security ID (HSI) is not supported" #. TRANSLATORS: success, so say thank you to the user msgid "Host Security ID attributes uploaded successfully, thanks!" msgstr "Host Security ID attributes uploaded successfully, thanks!" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "Host Security ID:" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX" msgstr "INDEX" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX KEY [VALUE]" msgstr "INDEX KEY [VALUE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX NAME TARGET [MOUNTPOINT]" msgstr "INDEX NAME TARGET [MOUNTPOINT]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX1,INDEX2" msgstr "INDEX1,INDEX2" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INHIBIT-ID" msgstr "INHIBIT-ID" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "IOMMU Protection" #. TRANSLATORS: longer description msgid "IOMMU Protection prevents connected devices from accessing unauthorized parts of system memory." msgstr "IOMMU Protection prevents connected devices from accessing unauthorised parts of system memory." #. TRANSLATORS: HSI event title msgid "IOMMU device protection disabled" msgstr "IOMMU device protection disabled" #. TRANSLATORS: HSI event title msgid "IOMMU device protection enabled" msgstr "IOMMU device protection enabled" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "Idle…" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "Ignore SSL strict checks when downloading files" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "Ignore firmware checksum failures" #. TRANSLATORS: command line option msgid "Ignore firmware hardware mismatch failures" msgstr "Ignore firmware hardware mismatch failures" #. TRANSLATORS: command line option msgid "Ignore non-critical firmware requirements" msgstr "Ignore non-critical firmware requirements" #. TRANSLATORS: try to help msgid "Ignoring SSL strict checks, to do this automatically in the future export DISABLE_SSL_STRICT in your environment" msgstr "Ignoring SSL strict checks, to do this automatically in the future export DISABLE_SSL_STRICT in your environment" #. TRANSLATORS: show the user a generic image that can be themed msgid "Image" msgstr "Image" #. TRANSLATORS: show the user a random image from the internet msgid "Image (custom)" msgstr "Image (custom)" #. TRANSLATORS: the inhibit ID is a short string like dbus-123456 #, c-format msgid "Inhibit ID is %s." msgstr "Inhibit ID is %s." #. TRANSLATORS: command description msgid "Inhibit the system to prevent upgrades" msgstr "Inhibit the system to prevent upgrades" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "Install Duration" #. TRANSLATORS: command description msgid "Install a firmware file in cabinet format on this hardware" msgstr "Install a firmware file in cabinet format on this hardware" #. TRANSLATORS: command description msgid "Install a raw firmware blob on a device" msgstr "Install a raw firmware blob on a device" #. TRANSLATORS: command description msgid "Install a specific firmware file on all devices that match" msgstr "Install a specific firmware file on all devices that match" #. TRANSLATORS: command description msgid "Install a specific firmware on a device, all possible devices will also be installed once the CAB matches" msgstr "Install a specific firmware on a device, all possible devices will also be installed once the CAB matches" msgid "Install old version of signed system firmware" msgstr "Install old version of signed system firmware" msgid "Install old version of unsigned system firmware" msgstr "Install old version of unsigned system firmware" msgid "Install signed device firmware" msgstr "Install signed device firmware" msgid "Install signed system firmware" msgstr "Install signed system firmware" msgid "Install unsigned device firmware" msgstr "Install unsigned device firmware" msgid "Install unsigned system firmware" msgstr "Install unsigned system firmware" #. TRANSLATORS: stay on one firmware version unless the new version is #. explicitly #. * specified msgid "Installing a specific release is explicitly required" msgstr "Installing a specific release is explicitly required" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "Installing firmware update…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "Installing on %s…" #. TRANSLATORS: if it breaks, you get to keep both pieces msgid "Installing this update may also void any device warranty." msgstr "Installing this update may also void any device warranty." #. TRANSLATORS: The BIOS setting only accepts integers in a fixed range msgid "Integer" msgstr "Integer" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM Protected" msgstr "Intel BootGuard ACM Protected" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "Intel BootGuard ACM protected" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard Error Policy" msgstr "Intel BootGuard Error Policy" msgid "Intel BootGuard Error Policy ensures the device does not continue to start if its device software has been tampered with." msgstr "Intel BootGuard Error Policy ensures the device does not continue to start if its device software has been tampered with." #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard Fuse" msgstr "Intel BootGuard Fuse" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "Intel BootGuard OTP fuse" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard Verified Boot" msgstr "Intel BootGuard Verified Boot" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Intel BootGuard error policy" #. TRANSLATORS: longer description msgid "Intel BootGuard prevents unauthorized device software from operating when the device is started." msgstr "Intel BootGuard prevents unauthorised device software from operating when the device is started." #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Intel BootGuard verified boot" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS Mitigation" msgstr "Intel GDS Mitigation" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS mitigation" msgstr "Intel GDS mitigation" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Manufacturing Mode" msgstr "Intel Management Engine Manufacturing Mode" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the "override" is #. enabled #. * with a jumper -- luckily it is probably not accessible to end users on #. consumer #. * boards msgid "Intel Management Engine Override" msgstr "Intel Management Engine Override" #. TRANSLATORS: longer description msgid "Intel Management Engine Override disables checks for device software tampering." msgstr "Intel Management Engine Override disables checks for device software tampering." #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Version" msgstr "Intel Management Engine Version" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "Internal device" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "Invalid" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "Invalid arguments" #. TRANSLATORS: error message msgid "Invalid arguments, expected GUID" msgstr "Invalid arguments, expected GUID" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX KEY [VALUE]" msgstr "Invalid arguments, expected INDEX KEY [VALUE]" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX NAME TARGET [MOUNTPOINT]" msgstr "Invalid arguments, expected INDEX NAME TARGET [MOUNTPOINT]" #. TRANSLATOR: This is the error message for #. * incorrect parameter msgid "Invalid arguments, expected an AppStream ID" msgstr "Invalid arguments, expected an AppStream ID" #. TRANSLATORS: error message msgid "Invalid arguments, expected at least ARCHIVE FIRMWARE METAINFO" msgstr "Invalid arguments, expected at least ARCHIVE FIRMWARE METAINFO" #. TRANSLATORS: error message msgid "Invalid arguments, expected base-16 integer" msgstr "Invalid arguments, expected base-16 integer" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "Is downgrade" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "Is in bootloader mode" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "Is upgrade" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "Issue" msgstr[1] "Issues" #. TRANSLATORS: HSI event title msgid "Kernel is no longer tainted" msgstr "Kernel is no longer tainted" #. TRANSLATORS: HSI event title msgid "Kernel is tainted" msgstr "Kernel is tainted" #. TRANSLATORS: HSI event title msgid "Kernel lockdown disabled" msgstr "Kernel lockdown disabled" #. TRANSLATORS: HSI event title msgid "Kernel lockdown enabled" msgstr "Kernel lockdown enabled" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "LOCATION" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "Last modified" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "Less than one minute remaining" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "Licence" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "Linux Kernel Lockdown" #. TRANSLATORS: longer description msgid "Linux Kernel Lockdown mode prevents administrator (root) accounts from accessing and changing critical parts of system software." msgstr "Linux Kernel Lockdown mode prevents administrator (root) accounts from accessing and changing critical parts of system software." msgid "Linux Kernel Swap temporarily saves information to disk as you work. If the information is not protected, it could be accessed by someone if they obtained the disk." msgstr "Linux Kernel Swap temporarily saves information to disk as you work. If the information is not protected, it could be accessed by someone if they obtained the disk." #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "Linux Kernel Verification" msgid "Linux Kernel Verification makes sure that critical system software has not been tampered with. Using device drivers which are not provided with the system can prevent this security feature from working correctly." msgstr "Linux Kernel Verification makes sure that critical system software has not been tampered with. Using device drivers which are not provided with the system can prevent this security feature from working correctly." #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "Linux Swap" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Linux Vendor Firmware Service (stable firmware)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Linux Vendor Firmware Service (testing firmware)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Linux kernel" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Linux kernel lockdown" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Linux swap" #. TRANSLATORS: command description msgid "List EFI boot files" msgstr "List EFI boot files" #. TRANSLATORS: command description msgid "List EFI boot parameters" msgstr "List EFI boot parameters" #. TRANSLATORS: command description msgid "List EFI variables with a specific GUID" msgstr "List EFI variables with a specific GUID" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "List entries in dbx" #. TRANSLATORS: command description msgid "List the available firmware GTypes" msgstr "List the available firmware GTypes" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "List the available firmware types" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "Lists files on the ESP" #. TRANSLATORS: command description msgid "Load device emulation data" msgstr "Load device emulation data" #. TRANSLATORS: the plugin was created from a .so object, and was not built-in msgid "Loaded from an external module" msgstr "Loaded from an external module" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "Loading…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "Locked" #. TRANSLATORS: the release urgency msgid "Low" msgstr "Low" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refers #. * to the private/public key used to secure loading of firmware msgid "MEI Key Manifest" msgstr "MEI Key Manifest" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refer #. * to the private/public key used to secure loading of firmware msgid "MEI key manifest" msgstr "MEI key manifest" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "MEI manufacturing mode" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "MEI override" msgid "MEI version" msgstr "MEI version" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "Manually enable specific plug-ins" #. TRANSLATORS: longer description msgid "Manufacturing Mode is used when the device is manufactured and security features are not yet enabled." msgstr "Manufacturing Mode is used when the device is manufactured and security features are not yet enabled." #. TRANSLATORS: Longest valid string for BIOS setting msgid "Maximum length" msgstr "Maximum length" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "Maximum value" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "Medium" #. TRANSLATORS: ask the user to do a simple task which should be translated msgid "Message" msgstr "Message" #. TRANSLATORS: ask the user a question, and it will not be translated msgid "Message (custom)" msgstr "Message (custom)" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "Metadata Signature" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "Metadata URI" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "Metadata can be obtained from the Linux Vendor Firmware Service." #. TRANSLATORS: error message for a user who ran fwupdmgr #. * refresh recently -- %1 is '--force' #, c-format msgid "Metadata is up to date; use %s to refresh again." msgstr "Metadata is up to date; use %s to refresh again." #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "Minimum Version" #. TRANSLATORS: Shortest valid string for BIOS setting msgid "Minimum length" msgstr "Minimum length" #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "Minimum value" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "Modifies a daemon configuration value" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "Modifies a given remote" msgid "Modify a configured remote" msgstr "Modify a configured remote" msgid "Modify daemon configuration" msgstr "Modify daemon configuration" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "Monitor the daemon for events" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "Mounts the ESP" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "Needs a reboot after installation" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "Needs reboot" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "Needs shutdown after installation" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "New version" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "No action specified!" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "No downgrades for %s" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "No firmware IDs found" #. TRANSLATORS: nothing found msgid "No firmware found" msgstr "No firmware found" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "No hardware detected with firmware update capability" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "No releases available" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "No remotes are currently enabled so no metadata is available." #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "No remotes available" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "No updatable devices" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "No updates available" msgid "No updates available for remaining devices" msgstr "No updates available for remaining devices" #. TRANSLATORS: error message #, c-format msgid "No volume matched %s" msgstr "No volume matched %s" #. TRANSLATORS: version cannot be installed due to policy msgid "Not approved" msgstr "Not approved" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "Not found" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "Not supported" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "OK" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "OK!" #. TRANSLATORS: the firmware old version msgid "Old version" msgstr "Old version" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "Only show single PCR value" #. TRANSLATORS: command line option msgid "Only use peer-to-peer networking when downloading files" msgstr "Only use peer-to-peer networking when downloading files" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "Only version upgrades are allowed" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "Override the default ESP path" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Firmware" msgstr "P2P Firmware" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Metadata" msgstr "P2P Metadata" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "PATH" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "Parse and show details about a firmware file" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "Parsing dbx update…" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "Parsing system dbx…" #. TRANSLATORS: remote filename base msgid "Password" msgstr "Password" #. TRANSLATORS: command description msgid "Patch a firmware blob at a known offset" msgstr "Patch a firmware blob at a known offset" msgid "Payload" msgstr "Payload" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "Pending" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "Perform operation?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "Platform Debugging" #. TRANSLATORS: longer description msgid "Platform Debugging allows device security features to be disabled. This should only be used by hardware manufacturers." msgstr "Platform Debugging allows device security features to be disabled. This should only be used by hardware manufacturers." #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "Platform debugging" #. TRANSLATORS: 'recovery key' here refers to a code, rather than a physical #. metal thing msgid "Please ensure you have the volume recovery key before continuing." msgstr "Please ensure you have the volume recovery key before continuing." #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "Please enter a number from 0 to %u: " #. TRANSLATORS: the user isn't reading the question -- %1 is 'Y' and %2 is #. * 'N' #, c-format msgid "Please enter either %s or %s: " msgstr "Please enter either %s or %s: " #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "Plug-in dependencies missing" #. TRANSLATORS: The plugin is only for testing msgid "Plugin is only for testing" msgstr "Plug-in is only for testing" #. TRANSLATORS: Possible values for a bios setting msgid "Possible Values" msgstr "Possible Values" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA Protection" msgstr "Pre-boot DMA Protection" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "Pre-boot DMA protection" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is disabled" msgstr "Pre-boot DMA protection is disabled" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is enabled" msgstr "Pre-boot DMA protection is enabled" #. TRANSLATORS: longer description msgid "Pre-boot DMA protection prevents devices from accessing system memory after being connected to the computer." msgstr "Pre-boot DMA protection prevents devices from accessing system memory after being connected to the computer." #. TRANSLATORS: warning message msgid "Press unlock on the device to continue the update process." msgstr "Press unlock on the device to continue the update process." #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "Previous version" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Priority" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "Problems" msgid "Proceed with upload?" msgstr "Proceed with upload?" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Processor Security Checks" msgstr "Processor Security Checks" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "Processor rollback protection" msgstr "Processor rollback protection" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "Proprietary" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "REMOTE-ID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID KEY VALUE" msgstr "REMOTE-ID KEY VALUE" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "Read Only" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "Read a firmware blob from a device" #. TRANSLATORS: command description msgid "Read a firmware from a device" msgstr "Read a firmware from a device" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "Reading from %s…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Reading…" #. TRANSLATORS: Plugin is active and in use msgid "Ready" msgstr "Ready" #. TRANSLATORS: how often we should refresh the metadata msgid "Refresh Interval" msgstr "Refresh Interval" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "Refresh metadata from remote server" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "Reinstall %s to %s?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "Reinstall current firmware on the device" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "Reinstall firmware on a device" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "Release Branch" #. TRANSLATORS: release attributes msgid "Release Flags" msgstr "Release Flags" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "Release ID" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "Remote ID" #. TRANSLATORS: command description msgid "Removes devices to watch for future emulation" msgstr "Removes devices to watch for future emulation" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "Report URI" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "Reported to remote server" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "Required efivarfs filesystem was not found" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "Required hardware was not found" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "Requires a bootloader" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Requires internet connection" msgid "Reset daemon configuration" msgstr "Reset daemon configuration" #. TRANSLATORS: sets something in the daemon configuration file msgid "Resets a daemon configuration section" msgstr "Resets a daemon configuration section" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Restart now?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "Restart the daemon to make the change effective?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Restarting device…" #. TRANSLATORS: command description msgid "Retrieve BIOS settings. If no arguments are passed all settings are returned" msgstr "Retrieve BIOS settings. If no arguments are passed all settings are returned" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "Return all the hardware IDs for the machine" #. TRANSLATORS: ask the user to upload msgid "Review and upload report now?" msgstr "Review and upload report now?" #. TRANSLATORS: longer description msgid "Rollback Protection prevents device software from being downgraded to an older version that has security problems." msgstr "Rollback Protection prevents device software from being downgraded to an older version that has security problems." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr get-upgrades` #, c-format msgid "Run `%s` for more information." msgstr "Run `%s` for more information." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr sync` #, c-format msgid "Run `%s` to complete this action." msgstr "Run `%s` to complete this action." #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "Run the plug-in composite cleanup routine when using install-blob" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "Run the plug-in composite prepare routine when using install-blob" #. TRANSLATORS: command description msgid "Run the post-reboot cleanup action" msgstr "Run the post-reboot cleanup action" #. TRANSLATORS: tell a user how to get information #, c-format msgid "Run without '%s' to see" msgstr "Run without '%s' to see" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "Running kernel is too old" #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "Runtime Suffix" #. TRANSLATORS: Software Bill of Materials link msgid "SBOM" msgstr "SBOM" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SECTION" msgstr "SECTION" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING VALUE" msgstr "SETTING VALUE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING1 VALUE1 [SETTING2] [VALUE2]" msgstr "SETTING1 VALUE1 [SETTING2] [VALUE2]" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "SMAP" msgstr "SMAP" #. TRANSLATORS: Title: Whether firmware is locked down msgid "SMM locked down" msgstr "SMM locked down" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "SPI BIOS Descriptor" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "SPI BIOS region" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "SPI lock" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "SPI replay protection" msgstr "SPI replay protection" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "SPI write" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "SPI write protection" msgstr "SPI write protection" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" #. TRANSLATORS: command description msgid "Save a file that allows generation of hardware IDs" msgstr "Save a file that allows generation of hardware IDs" #. TRANSLATORS: command description msgid "Save device emulation data" msgstr "Save device emulation data" #. TRANSLATORS: key for a offline report filename msgid "Saved report" msgstr "Saved report" #. TRANSLATORS: Scalar increment for integer BIOS setting msgid "Scalar Increment" msgstr "Scalar Increment" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Scheduling…" #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "Secure Boot disabled" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "Secure Boot enabled" msgid "Security hardening for HSI" msgstr "Security hardening for HSI" #. TRANSLATORS: the %1 is a URL to a wiki page #, c-format msgid "See %s for more details." msgstr "See %s for more details." #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "See %s for more information." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Selected device" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Selected volume" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "Serial Number" #. TRANSLATORS: Configured a BIOS setting to a value #, c-format msgid "Set BIOS setting '%s' using '%s'." msgstr "Set BIOS setting '%s' using '%s'." #. TRANSLATORS: command description msgid "Set a BIOS setting" msgstr "Set a BIOS setting" msgid "Set one or more BIOS settings" msgstr "Set one or more BIOS settings" #. TRANSLATORS: command description msgid "Set or remove an EFI boot hive entry" msgstr "Set or remove an EFI boot hive entry" #. TRANSLATORS: command description msgid "Set the EFI boot next" msgstr "Set the EFI boot next" #. TRANSLATORS: command description msgid "Set the EFI boot order" msgstr "Set the EFI boot order" #. TRANSLATORS: command line option msgid "Set the download retries for transient errors" msgstr "Set the download retries for transient errors" #. TRANSLATORS: command description msgid "Sets one or more BIOS settings" msgstr "Sets one or more BIOS settings" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Sets the list of approved firmware" #. TRANSLATORS: type of BIOS setting msgid "Setting type" msgstr "Setting type" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "Settings will apply after system reboots" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "Share firmware history with the developers" #. TRANSLATORS: command line option msgid "Show all results" msgstr "Show all results" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "Show client and daemon versions" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "Show daemon verbose information for a particular domain" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "Show debugging information for all domains" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Show debugging options" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Show devices that are not updatable" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Show extra debugging information" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "Show history of firmware updates" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "Show the calculated version of the dbx" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "Shutdown now?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "Sign a firmware with a new key" msgid "Sign data using the client certificate" msgstr "Sign data using the client certificate" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "Sign data using the client certificate" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "Sign the uploaded data with the client certificate" msgid "Signature" msgstr "Signature" #. TRANSLATORS: firmware is verified on-device the payload using strong crypto msgid "Signed Payload" msgstr "Signed Payload" #. TRANSLATORS: file size of the download msgid "Size" msgstr "Size" #. TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM msgid "Some of the platform secrets may be invalidated when updating this firmware." msgstr "Some of the platform secrets may be invalidated when updating this firmware." #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "Source" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "Specify the dbx database file" msgid "Stop the fwupd service" msgstr "Stop the fwupd service" #. TRANSLATORS: The BIOS setting accepts strings msgid "String" msgstr "String" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "Success" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "Successfully activated all devices" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "Successfully disabled remote" #. TRANSLATORS: comment explaining result of command msgid "Successfully disabled test devices" msgstr "Successfully disabled test devices" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "Successfully downloaded new metadata: " #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "Successfully enabled and refreshed remote" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "Successfully enabled remote" #. TRANSLATORS: comment explaining result of command msgid "Successfully enabled test devices" msgstr "Successfully enabled test devices" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "Successfully installed firmware" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "Successfully modified configuration value" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "Successfully modified remote" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "Successfully refreshed metadata manually" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration section" msgstr "Successfully reset configuration section" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration values" msgstr "Successfully reset configuration values" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "Successfully updated device checksums" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "Successfully uploaded %u report" msgstr[1] "Successfully uploaded %u reports" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "Successfully verified device checksums" #. TRANSLATORS: the device showed up in time #, c-format msgid "Successfully waited %.0fms for device" msgstr "Successfully waited %.0fms for device" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Summary" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "Supervisor Mode Access Prevention" msgstr "Supervisor Mode Access Prevention" #. TRANSLATORS: longer description msgid "Supervisor Mode Access Prevention ensures critical parts of device memory are not accessed by less secure programs." msgstr "Supervisor Mode Access Prevention ensures critical parts of device memory are not accessed by less secure programs." #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "Supported" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "Supported CPU" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "Supported on remote server" #. TRANSLATORS: Title: a better sleep state msgid "Suspend To Idle" msgstr "Suspend To Idle" #. TRANSLATORS: Title: sleep state msgid "Suspend To RAM" msgstr "Suspend To RAM" #. TRANSLATORS: longer description msgid "Suspend to Idle allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Suspend to Idle allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." #. TRANSLATORS: longer description msgid "Suspend to RAM allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Suspend to RAM allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "Suspend-to-idle" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "Suspend-to-ram" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "Switch branch from %s to %s?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "Switch the firmware branch on the device" #. TRANSLATORS: command description msgid "Sync firmware versions to the chosen configuration" msgstr "Sync firmware versions to the chosen configuration" #. TRANSLATORS: Title: Whether firmware is locked down msgid "System Management Mode" msgstr "System Management Mode" #. TRANSLATORS: this CLI tool is now preventing system updates msgid "System Update Inhibited" msgstr "System Update Inhibited" #. TRANSLATORS: longer description msgid "System management mode is used by the firmware to access resident BIOS code and data." msgstr "System management mode is used by the firmware to access resident BIOS code and data." #. TRANSLATORS: as in laptop battery power msgid "System power is too low" msgstr "System power is too low" #. TRANSLATORS: as in laptop battery power #, c-format msgid "System power is too low (%u%%, requires %u%%)" msgstr "System power is too low (%u%%, requires %u%%)" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "System requires external power source" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "TEXT" #. TRANSLATORS: longer description msgid "TPM (Trusted Platform Module) is a computer chip that detects when hardware components have been tampered with." msgstr "TPM (Trusted Platform Module) is a computer chip that detects when hardware components have been tampered with." #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "TPM PCR0 reconstruction" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is invalid" msgstr "TPM PCR0 reconstruction is invalid" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is now valid" msgstr "TPM PCR0 reconstruction is now valid" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM Platform Configuration" msgstr "TPM Platform Configuration" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM Reconstruction" msgstr "TPM Reconstruction" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM empty PCRs" msgstr "TPM empty PCRs" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "Tag" msgstr[1] "Tags" #. TRANSLATORS: we're saving all USB events for emulation msgid "Tagged for emulation" msgstr "Tagged for emulation" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "Tainted" #. show the user the entire data blob msgid "Target" msgstr "Target" #. TRANSLATORS: command description msgid "Test a device using a JSON manifest" msgstr "Test a device using a JSON manifest" #. TRANSLATORS: when the release was tested msgid "Tested" msgstr "Tested" #. TRANSLATORS: the %s is a vendor name, e.g. Lenovo #, c-format msgid "Tested by %s" msgstr "Tested by %s" #. TRANSLATORS: someone we trust has tested this msgid "Tested by trusted vendor" msgstr "Tested by trusted vendor" #. TRANSLATORS: the boot entry was in a legacy format msgid "The EFI boot entry is not in hive format, and shim may not be new enough to read it." msgstr "The EFI boot entry is not in hive format, and shim may not be new enough to read it." #. TRANSLATORS: try to treat the legacy format as a hive msgid "The EFI boot entry was not in hive format, falling back" msgstr "The EFI boot entry was not in hive format, falling back" #. TRANSLATORS: longer description msgid "The Intel Management Engine Key Manifest must be valid so that the device firmware can be trusted by the CPU." msgstr "The Intel Management Engine Key Manifest must be valid so that the device firmware can be trusted by the CPU." #. TRANSLATORS: longer description msgid "The Intel Management Engine controls device components and needs to have a recent version to avoid security issues." msgstr "The Intel Management Engine controls device components and needs to have a recent version to avoid security issues." #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Platform Configuration is used to check whether the device start process has been modified." msgstr "The TPM (Trusted Platform Module) Platform Configuration is used to check whether the device start process has been modified." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Reconstruction is used to check whether the device start process has been modified." msgstr "The TPM (Trusted Platform Module) Reconstruction is used to check whether the device start process has been modified." #. TRANSLATORS: this is more background on a security measurement problem msgid "The TPM PCR0 differs from reconstruction." msgstr "The TPM PCR0 differs from reconstruction." #. TRANSLATORS: longer description msgid "The UEFI Platform Key is used to determine if device software comes from a trusted source." msgstr "The UEFI Platform Key is used to determine if device software comes from a trusted source." #. TRANSLATORS: HSI event title msgid "The UEFI certificate store is now up to date" msgstr "The UEFI certificate store is now up to date" #. TRANSLATORS: longer description msgid "The UEFI db contains the list of valid certificates that can be used to authorize what EFI binaries are allowed to run." msgstr "The UEFI db contains the list of valid certificates that can be used to authorize what EFI binaries are allowed to run." #. TRANSLATORS: longer description msgid "The UEFI system can set up memory attributes at boot which prevent common exploits from running." msgstr "The UEFI system can set up memory attributes at boot which prevent common exploits from running." #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" #. TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name #, c-format msgid "The firmware from %s is not supplied by %s, the hardware vendor." msgstr "The firmware from %s is not supplied by %s, the hardware vendor." #. TRANSLATORS: try to help msgid "The system clock has not been set correctly and downloading files may fail." msgstr "The system clock has not been set correctly and downloading files may fail." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been re-inserted." msgstr "The update will continue when the device USB cable has been re-inserted." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged and then re-inserted." msgstr "The update will continue when the device USB cable has been unplugged and then re-inserted." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged." msgstr "The update will continue when the device USB cable has been unplugged." #. TRANSLATORS: warning message msgid "The update will continue when the device power cable has been removed and re-inserted." msgstr "The update will continue when the device power cable has been removed and re-inserted." #. TRANSLATORS: naughty vendor msgid "The vendor did not supply any release notes." msgstr "The vendor did not supply any release notes." #. TRANSLATORS: now list devices with unfixed high-priority issues msgid "There are devices with issues:" msgstr "There are devices with issues:" #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "There are no blocked firmware files" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "There is no approved firmware." #. TRANSLATORS: %1 is the current device version number, and %2 is the #. command name, e.g. `fwupdmgr sync` #, c-format msgid "This device will be reverted back to %s when the %s command is performed." msgstr "This device will be reverted back to %s when the %s command is performed." #. TRANSLATORS: the vendor did not upload this msgid "This firmware is provided by LVFS community members and is not provided (or supported) by the original hardware vendor." msgstr "This firmware is provided by LVFS community members and is not provided (or supported) by the original hardware vendor." #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "This package has not been validated, it may not work properly." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "This program may only work correctly as root" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." #. TRANSLATORS: error message msgid "This system doesn't support firmware settings" msgstr "This system doesn't support firmware settings" #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "This system has HSI runtime issues." #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "This system has a low HSI security level." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "This tool allows an administrator to apply UEFI dbx updates." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "This tool allows an administrator to use the fwupd plug-ins without being installed on the host system." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can add a kernel argument of '%s', but it will only be active after restarting the computer." msgstr "This tool can add a kernel argument of '%s', but it will only be active after restarting the computer." #. TRANSLATORS: the %1 is a BIOS setting name. #. * %2 and %3 are the values, e.g. "True" or "Windows10" #, c-format msgid "This tool can change the BIOS setting '%s' from '%s' to '%s' automatically, but it will only be active after restarting the computer." msgstr "This tool can change the BIOS setting '%s' from '%s' to '%s' automatically, but it will only be active after restarting the computer." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can change the kernel argument from '%s' to '%s', but it will only be active after restarting the computer." msgstr "This tool can change the kernel argument from '%s' to '%s', but it will only be active after restarting the computer." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "This tool will read and parse the TPM event log from the system firmware." #. TRANSLATORS: the update state of the specific device msgid "Transient failure" msgstr "Transient failure" #. TRANSLATORS: item is TRUE msgid "True" msgstr "True" #. TRANSLATORS: We verified the metadata against the server msgid "Trusted metadata" msgstr "Trusted metadata" #. TRANSLATORS: We verified the payload against the server msgid "Trusted payload" msgstr "Trusted payload" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Type" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI Bootservice Variables" msgstr "UEFI Bootservice Variables" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition may not be set up correctly" msgstr "UEFI ESP partition may not be set up correctly" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "UEFI ESP partition not detected or configured" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI Memory Protection" msgstr "UEFI Memory Protection" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "UEFI Platform Key" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "UEFI Secure Boot" #. TRANSLATORS: longer description msgid "UEFI Secure Boot prevents malicious software from being loaded when the device starts." msgstr "UEFI Secure Boot prevents malicious software from being loaded when the device starts." #. TRANSLATORS: longer description msgid "UEFI boot service variables should not be readable from runtime mode." msgstr "UEFI boot service variables should not be readable from runtime mode." #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI bootservice variables" msgstr "UEFI bootservice variables" #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "UEFI capsule updates not available or enabled in firmware setup" #. TRANSLATORS: Title: is UEFI db up-to-date msgid "UEFI db" msgstr "UEFI db" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "UEFI dbx Utility" #. TRANSLATORS: system is not booted in UEFI mode msgid "UEFI firmware can not be updated in legacy BIOS mode" msgstr "UEFI firmware can not be updated in legacy BIOS mode" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI memory protection" msgstr "UEFI memory protection" #. TRANSLATORS: HSI event title msgid "UEFI memory protection enabled and locked" msgstr "UEFI memory protection enabled and locked" #. TRANSLATORS: HSI event title msgid "UEFI memory protection enabled but not locked" msgstr "UEFI memory protection enabled but not locked" #. TRANSLATORS: HSI event title msgid "UEFI memory protection is now locked" msgstr "UEFI memory protection is now locked" #. TRANSLATORS: HSI event title msgid "UEFI memory protection is now unlocked" msgstr "UEFI memory protection is now unlocked" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "UEFI platform key" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "UEFI secure boot" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "Unable to connect to service" #. TRANSLATORS: error message msgid "Unable to find attribute" msgstr "Unable to find attribute" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "Unbind current driver" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "Unblocking firmware:" #. TRANSLATORS: command description msgid "Unblocks a specific firmware from being installed" msgstr "Unblocks a specific firmware from being installed" #. TRANSLATORS: command description msgid "Undo the host security attribute fix" msgstr "Undo the host security attribute fix" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "Unencrypted" #. TRANSLATORS: command description msgid "Uninhibit the system to allow upgrades" msgstr "Uninhibit the system to allow upgrades" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Unknown" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Unknown Device" msgid "Unlock the device to allow access" msgstr "Unlock the device to allow access" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "Unlocked" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "Unlocks the device for firmware access" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "Unmounts the ESP" #. TRANSLATORS: message shown after device has been marked for emulation msgid "Unplug and replug the device to continue the update process." msgstr "Unplug and replug the device to continue the update process." #. TRANSLATORS: firmware payload is unsigned and it is possible to modify it msgid "Unsigned Payload" msgstr "Unsigned Payload" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "Unsupported daemon version %s, client version is %s" #. TRANSLATORS: Suffix: the HSI result msgid "Untainted" msgstr "Untainted" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "Updatable" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Update Error" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "Update Image" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "Update Message" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "Update State" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "Update failure is a known issue, visit this URL for more information:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Update now?" #. TRANSLATORS: command description msgid "Update the stored cryptographic hash with current ROM contents" msgstr "Update the stored cryptographic hash with current ROM contents" msgid "Update the stored device verification information" msgstr "Update the stored device verification information" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "Update the stored metadata with current contents" #. TRANSLATORS: command description msgid "Updates all specified devices to latest firmware version, or all devices if unspecified" msgstr "Updates all specified devices to latest firmware version, or all devices if unspecified" #. TRANSLATORS: how many local devices can expect updates now #, c-format msgid "Updates have been published for %u local device" msgid_plural "Updates have been published for %u of %u local devices" msgstr[0] "Updates have been published for %u local device" msgstr[1] "Updates have been published for %u of %u local devices" msgid "Updating" msgstr "Updating" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "Updating %s…" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "Upgrade %s from %s to %s?" #. TRANSLATORS: ask the user to upload msgid "Upload data now?" msgstr "Upload data now?" #. TRANSLATORS: command description msgid "Upload the list of updatable devices to a remote server" msgstr "Upload the list of updatable devices to a remote server" #. TRANSLATORS: ask the user to share, %s is something #. * like: "Linux Vendor Firmware Service" #, c-format msgid "Upload these anonymous results to the %s to help other users?" msgstr "Upload these anonymous results to the %s to help other users?" #. TRANSLATORS: explain why we want to upload #, c-format msgid "Uploading a device list allows the %s team to know what hardware exists, and allows us to put pressure on vendors that do not upload firmware updates for their hardware." msgstr "Uploading a device list allows the %s team to know what hardware exists, and allows us to put pressure on vendors that do not upload firmware updates for their hardware." #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "Urgency" #. TRANSLATORS: explain how to get help, %1 is #. * 'fwupdtool --help' #. TRANSLATORS: explain how to get help, #. * where $1 is something like 'fwupdmgr --help' #, c-format msgid "Use %s for help" msgstr "Use %s for help" #. TRANSLATORS: CTRL^C [holding control, and then pressing C] will exit the #. program msgid "Use CTRL^C to cancel." msgstr "Use CTRL^C to cancel." #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "User has been notified" #. TRANSLATORS: remote filename base msgid "Username" msgstr "Username" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "VERSION1 VERSION2 [FORMAT]" msgstr "VERSION1 VERSION2 [FORMAT]" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "Valid" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "Validating ESP contents…" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "Variant" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "Vendor" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "Verifying…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Version" #. TRANSLATORS: the fwupd version the release was tested on msgid "Version[fwupd]" msgstr "Version[fwupd]" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "WARNING" #. TRANSLATORS: command description msgid "Wait for a device to appear" msgstr "Wait for a device to appear" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "Waiting…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "Watch for hardware changes" #. TRANSLATORS: check various UEFI and ACPI tables are unchanged after the #. update msgid "Will measure elements of system integrity around an update" msgstr "Will measure elements of system integrity around an update" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "Writing file:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Writing…" #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from a recovery or installation disk, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "You should ensure you are comfortable restoring the setting from a recovery or installation disk, as this change may cause the system to not boot into Linux or cause other system instability." #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from the system firmware setup, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "You should ensure you are comfortable restoring the setting from the system firmware setup, as this change may cause the system to not boot into Linux or cause other system instability." #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." #. TRANSLATORS: BKC is the industry name for the best known configuration and #. is a set #. * of firmware that works together #, c-format msgid "Your system is set up to the BKC of %s." msgstr "Your system is set up to the BKC of %s." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[APPSTREAM_ID]" msgstr "[APPSTREAM_ID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[CHECKSUM]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[DEVICE-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[DEVICE-ID|GUID] [BRANCH]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [VERSION]" msgstr "[DEVICE-ID|GUID] [VERSION]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE]" msgstr "[DEVICE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILE FILE_SIG REMOTE-ID]" msgstr "[FILE FILE_SIG REMOTE-ID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[FILENAME1] [FILENAME2]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FWUPD-VERSION]" msgstr "[FWUPD-VERSION]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[REASON] [TIMEOUT]" msgstr "[REASON] [TIMEOUT]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SECTION] KEY VALUE" msgstr "[SECTION] KEY VALUE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2] [--no-authenticate]" msgstr "[SETTING1] [SETTING2] [--no-authenticate]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2]..." msgstr "[SETTING1] [SETTING2]..." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[SMBIOS-FILE|HWIDS-FILE]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "default" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "fwupd TPM event log utility" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "fwupd plug-ins" fwupd-2.0.10/po/eo.po000066400000000000000000000036461501337203100143040ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # kristjan , 2019 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Esperanto (http://www.transifex.com/freedesktop/fwupd/language/eo/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: eo\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u sekundo" msgstr[1] "%u sekundoj" #. TRANSLATORS: this is when a device is hotplugged msgid "Added" msgstr "Aldonita" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Nuligi" #. TRANSLATORS: this is when a device ctrl+c's a watch msgid "Cancelled" msgstr "Nuligita" #. TRANSLATORS: this is when a device is hotplugged #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "ÅœanÄita" #. TRANSLATORS: get interactive prompt msgid "Choose a device:" msgstr "Elektu aparaton:" #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Priskribo" #. success msgid "Done!" msgstr "Farita!" #. TRANSLATORS: detected a DFU device msgid "Found" msgstr "Trovita" msgid "ID" msgstr "ID" msgid "Mode" msgstr "ReÄimo" #. TRANSLATORS: interface name, e.g. "Flash" #. TRANSLATORS: device name, e.g. 'ColorHug2' msgid "Name" msgstr "Nomo" msgid "OK" msgstr "Bone" #. TRANSLATORS: DFU protocol version, e.g. 1.1 msgid "Protocol" msgstr "Protokolo" #. TRANSLATORS: these are areas of memory on the chip msgid "Region" msgstr "Regiono" #. TRANSLATORS: this is when a device is hotplugged msgid "Removed" msgstr "Forigita" msgid "Target" msgstr "Celo" #. TRANSLATORS: currect daemon status is unknown msgid "Unknown" msgstr "Nekonata" fwupd-2.0.10/po/es.po000066400000000000000000003713311501337203100143070ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Adolfo Jayme-Barrientos, 2021 # Adolfo Jayme Barrientos, 2021 # César Enrique García , 2023 # D Marzal, 2024 # Francisco Serrador, 2022-2025 # Francisco Serrador , 2022 # Giovanni Alfredo Garciliano Diaz , 2024 # Santiago FN, 2024 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Spanish (http://app.transifex.com/freedesktop/fwupd/language/es/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: es\n" "Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "%.0f minuto restante" msgstr[1] "%.0f minutos restantes" msgstr[2] "%.0f minutos restantes" #. TRANSLATORS: BMC refers to baseboard management controller which #. * is the device that updates all the other firmware on the system #, c-format msgid "%s BMC Update" msgstr "Actualización %s BMC" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "Actualización %s de Batería" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "Actualización %s de Mmicrocódigo CPU" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "Actualización %s de Cámara" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "Actualización %s de Configuración" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "Actualización %s ME del Cliente" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "Actualización %s del Controlador" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "Actualización %s ME de Fabricante" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "Actualización %s de Dispositivo" #. TRANSLATORS: Video Display refers to the laptop internal display or #. * external monitor #, c-format msgid "%s Display Update" msgstr "Actualización %s de Pantalla" #. TRANSLATORS: Dock refers to the port replicator hardware laptops are #. * cradled in, or lowered onto #, c-format msgid "%s Dock Update" msgstr "Actualización %s de Muelle" #. TRANSLATORS: drive refers to a storage device, e.g. SATA disk #, c-format msgid "%s Drive Update" msgstr "Actualización %s de Dispositivo" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "Actualización %s de Controlador Empotrado" #. TRANSLATORS: a device that can read your fingerprint pattern #, c-format msgid "%s Fingerprint Reader Update" msgstr "Actualización %s del Lector de Huellas" #. TRANSLATORS: flash refers to solid state storage, e.g. UFS or eMMC #, c-format msgid "%s Flash Drive Update" msgstr "Actualización %s de Unidad Flash" #. TRANSLATORS: GPU refers to a Graphics Processing Unit, e.g. #. * the "video card" #, c-format msgid "%s GPU Update" msgstr "Actualización de GPU %s" #. TRANSLATORS: a large pressure-sensitive drawing area typically used #. * by artists and digital artists #, c-format msgid "%s Graphics Tablet Update" msgstr "%s Tabla de Gráficos de Actualización" #. TRANSLATORS: two miniature speakers attached to your ears #, c-format msgid "%s Headphones Update" msgstr "Actualizar %s auriculares" #. TRANSLATORS: headphones with an integrated microphone #, c-format msgid "%s Headset Update" msgstr "%s Actualización del conjunto de cabeceras" #. TRANSLATORS: an input device used by gamers, e.g. a joystick #, c-format msgid "%s Input Controller Update" msgstr "Actualizar Controlador %s de Entrada" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "Actualización %s de Teclado" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "Actualización %s ME" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "Actualización %s del Ratón" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "Actualización %s del Interfaz de Red" #. TRANSLATORS: SSD refers to a Solid State Drive, e.g. non-rotating #. * SATA or NVMe disk #, c-format msgid "%s SSD Update" msgstr "%s SSD Actualización" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "Actualización %s del Controlador de Almacenaje" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "Actualización %s del Sistema" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "Actualización %s de TPM" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "Actualización %s del Controlador Thunderbolt" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "Actualización %s de Touchpad" #. TRANSLATORS: Dock refers to the port replicator device connected #. * by plugging in a USB cable -- which may or may not also provide power #, c-format msgid "%s USB Dock Update" msgstr "%s Muelle de USB Actualización" #. TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth #. * device that stays in the USB port so the wireless peripheral works #, c-format msgid "%s USB Receiver Update" msgstr "Receptor USB %s Actualizado" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "Actualización %s" #. TRANSLATORS: warn the user before updating, %1 is a device name #, c-format msgid "%s and all connected devices may not be usable while updating." msgstr "%s y todos los dispositivos conectados tal vez no están utilizables mientras se actualizan." #. TRANSLATORS: %1 refers to some kind of security test, e.g. "Encrypted RAM". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s appeared: %s" msgstr "%s ha aparecido: %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "UEFI platform #. key". #. * %2 and %3 refer to results value, e.g. "Valid" and "Invalid" #, c-format msgid "%s changed: %s → %s" msgstr "%s ha cambiado: %s → %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "SPI BIOS #. region". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s disappeared: %s" msgstr "%s ha desaparecido: %s" #. TRANSLATORS: the device has a reason it can't update, e.g. laptop lid #. closed #, c-format msgid "%s is not currently updatable" msgstr "%s no es actualizable actualmente" #. TRANSLATORS: %1 is a device name, e.g. "ThinkPad Universal #. * ThunderBolt 4 Dock" and %2 is "fwupdmgr activate" #, c-format msgid "%s is pending activation; use %s to complete the update." msgstr "%s está pendiente de activación, use %s para completar la actualización." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "%s modo de fabricante" #. TRANSLATORS: warn the user before #. * updating, %1 is a device name #, c-format msgid "%s must remain connected for the duration of the update to avoid damage." msgstr "%s debe permanecer conectado para la duración de la actualización para evitar daño." #. TRANSLATORS: warn the user before updating, %1 is a machine #. * name #, c-format msgid "%s must remain plugged into a power source for the duration of the update to avoid damage." msgstr "%s debe permanecer enchufado en un agente de alimentación para la duración de la actualización para evitar daño." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "%s sobrecarga" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "Versión %s" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u día" msgstr[1] "%u días" msgstr[2] "%u días" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "%u dispositivo tiene una actualización disponible." msgstr[1] "%u dispositivos tienen una actualización disponible." msgstr[2] "%u dispositivos tienen una actualización disponible." #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device is not the best known configuration." msgid_plural "%u devices are not the best known configuration." msgstr[0] "%u dispositivo no es la mejor configuración conocida." msgstr[1] "%u dispositivos no son la mejor configuración conocida" msgstr[2] "%u dispositivos no son la mejor configuración conocida." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u hora" msgstr[1] "%u horas" msgstr[2] "%u horas" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minuto" msgstr[1] "%u minutos" msgstr[2] "%u minutos" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u segundo" msgstr[1] "%u segundos" msgstr[2] "%u segundos" #. TRANSLATORS: device tests can be specific to a CPU type #, c-format msgid "%u test was skipped" msgid_plural "%u tests were skipped" msgstr[0] "Fue omitida %u prueba" msgstr[1] "Fueron omitidas %u pruebas" msgstr[2] "Fueron omitidas %u pruebas" #. TRANSLATORS: first percentage is current value, 2nd percentage is the #. * lowest limit the firmware update is allowed for the update to happen #, c-format msgid "%u%% (threshold %u%%)" msgstr "%u%% (umbral %u%%)" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(obsoleto)" #. TRANSLATORS: HSI event title msgid "A TPM PCR is now an invalid value" msgstr "Un PCR TPM ahora es un valor no válido" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "AMD Firmware Replay Protection" msgstr "Repetición de Protección AMD" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "AMD Firmware Write Protection" msgstr "Escritura Firmware AMD" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "AMD Secure Processor Rollback Protection" msgstr "Protección de Reversión del Procesador AMD Seguro" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "ARCHIVE FIRMWARE METAINFO [FIRMWARE] [METAINFO] [JCATFILE]" msgstr "METAINFO del ARCHIVADOR FIRMWARE [FIRMWARE] [METAINFO] [JCATFILE]" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "Operación requerida:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "Activar dispositivos" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "Activar dispositivos pendientes" msgid "Activate the new firmware on the device" msgstr "Activa el firmware nuevo en el dispositivo" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "Activando actualización del firmware" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "Activando actualización del firmware para" #. TRANSLATORS: command description msgid "Adds devices to watch for future emulation" msgstr "Añade dispositivos que vigilen para emulación futura." #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Edad" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "¿Acuerda y habilita el remoto?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Alias a %s" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are now valid" msgstr "Todos los PCR TPM son válidos ahora" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are valid" msgstr "Todos los PCR TPM son válidos" #. TRANSLATORS: an application is preventing system updates msgid "All devices are prevented from update by system inhibit" msgstr "Las actualizaciones de todos los dispositivos están bloqueadas ya que el sistema está inhibido" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "Todos los dispositivos del mismo tipo serán actualizados a la vez" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Permitir bajar versiones de firmware" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "Permitir reinstalar versiones de firmware existentes" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "Permitir intercambiar rama de firmware" #. TRANSLATORS: error message msgid "Already exists, and no --force specified" msgstr "Ya existe, y sin especificar --force" #. TRANSLATORS: is not the main firmware stream msgid "Alternate branch" msgstr "Ramificación alterna" #. TRANSLATORS: another application is updating the device already msgid "An update is in progress" msgstr "Está en progreso una actualización" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "Una actualización requiere un rearranque para completarla." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "Una actualización requiere que el sistema se apague para completarla." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Responda sí a todas las preguntas" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Aplica actualización incluso cuando no sea advertido" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Aplicar archivos de actualización" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "Aplicando actualización…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Firmware aprobado" msgstr[1] "Firmware aprobado" msgstr[2] "Firmware aprobado" #. TRANSLATORS: command description msgid "Asks the daemon to quit" msgstr "Solicita al demonio que salga" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "Acoplar al modo firmware" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Autenticando…" #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "Detalles requeridos para la autenticación" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Se requiere autenticación para degradar el firmware a una versión anterior en un dispositivo extraíble" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Se requiere autenticación para degradar la versión del firmware en esta máquina" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to enable emulation data collection" msgstr "Se requiere autenticación para habilitar la colección de datos de emulación" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to fix a host security issue" msgstr "La autenticación es requerida para reparar un problema de seguridad del hospedaje" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to load hardware emulation data" msgstr "Se requiere autenticación para cargar datos de emulación de hardware" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "Se requiere autenticación para modificar los parámetros del BIOS" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "Se requiere autenticación para modificar un remoto configurado utilizado para actualizaciones de firmware" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "Se requiere autenticación para modificar la configuración del demonio" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "Requiere autenticarse para leer parámetros de BIOS" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to reset daemon configuration to defaults" msgstr "Autenticación requerida para reiniciar la configuración del demonio a su estado por defecto" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to save hardware emulation data" msgstr "Se requiere autenticación para guardar datos de emulación del hardware" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "Se requiere autenticación para establecer el listado de firmware aprobado" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "Se requiere autenticación para firmar datos utilizando el certificado del cliente" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to stop the firmware update service" msgstr "Se requiere autenticación para detener el servicio de actualización del firmware" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "Se requiere autenticación para cambiar a la versión de firmware nueva" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to undo the fix for a host security issue" msgstr "La Autenticación está requerida para deshacer la reparación para un problema de seguridad del hospedaje" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "La autenticación es requerida para desbloquear un dispositivo" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Se requiere autenticación para actualizar el firmware en un dispositivo extraíble" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Autenticación es requerida para actualizar el firmware en esta máquina" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "Se requiere autenticación para actualizar las sumas de verificación almacenadas para el dispositivo" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "Informes Automáticos" #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "¿Subo automáticamente cada vez?" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS Firmware Updates" msgstr "Actualizaciones del Firmware de BIOS" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS Rollback Protection" msgstr "Retroceso de Protección de BIOS" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS firmware updates" msgstr "Actualiza firmware de BIOS" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS rollback protection" msgstr "Retrocede protección BIOS" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "BIOS actualiza enviados vía LVFS o Windows Update" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "BUILDER-XML FILENAME-DST" msgstr "BUILDER-XML NOMBREARCHIVO-DST" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "Batería" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "Vincula nuevo controlador del kernel" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "Archivos de firmware bloqueados:" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "Versión bloqueada" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "Firmware bloqueado:" #. TRANSLATORS: command description msgid "Blocks a specific firmware from being installed" msgstr "Bloquea un origen del firmware específico siendo instalado" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "Versión del Arranque" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "Rama" #. TRANSLATORS: command description msgid "Build a cabinet archive from a firmware blob and XML metadata" msgstr "Construye un archivado gabinete desde un globo de firmware y metadatos XML" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "Compila un archivo firmware" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * Utilized by OS means the distribution enabled it msgid "CET OS Support" msgstr "Admite SO CET" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * enabled means supported by the processor msgid "CET Platform" msgstr "Plataforma CET" #. TRANSLATORS: longer description msgid "CPU Microcode must be updated to mitigate against various information-disclosure security issues." msgstr "El micro-código de CPU debe ser actualizarlo para mitigar varios informes descubiertos en problemas de seguridad." #. TRANSLATORS: we can save all device enumeration events for emulation msgid "Can tag for emulation" msgstr "Puede etiquetar para emulación" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Cancelar" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "Cancelado" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "No se puede aplicar como actualización dbx ya ha sido aplicado." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Cambiado" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "Comprueba coincidencias hash criptográficas del firmware" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Sumatorio" #. TRANSLATORS: get interactive prompt, where branch is the #. * supplier of the firmware, e.g. "non-free" or "free" msgid "Choose branch" msgstr "Elija rama" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "Elegir dispositivo" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "Elija firmware" #. TRANSLATORS: get interactive prompt msgid "Choose release" msgstr "Elija la publicación" #. TRANSLATORS: get interactive prompt msgid "Choose volume" msgstr "Elija volúmen" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "Vacía el resultado desde la última actualización" #. TRANSLATORS: error message msgid "Command not found" msgstr "No se encontró la orden" #. TRANSLATORS: is not supported by the vendor msgid "Community supported" msgstr "Comunitariamente mantenido" #. TRANSLATORS: command description msgid "Compares two versions for equality" msgstr "Compara dos versiones para igualdad" #. TRANSLATORS: title prefix for the BIOS settings dialog msgid "Configuration Change Suggested" msgstr "Cambio Sugerido de Configuración" #. TRANSLATORS: no peeking msgid "Configuration is only readable by the system administrator" msgstr "Configuración solamente leíble por el administrador del sistema" #. TRANSLATORS: longer description msgid "Control-Flow Enforcement Technology detects and prevents certain methods for running malicious software on the device." msgstr "Tecnología de Mejora del Control de Flujo detecta y previene ciertos métodos para ejecución de software malicioso en el dispositivo." #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology msgid "Control-flow Enforcement Technology" msgstr "Tecnología de Mejora de Flujo-Controlado" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "Convierta un archivo firmware" #. TRANSLATORS: command description msgid "Create an EFI boot entry" msgstr "Crea un apunte de arranque EFI" #. TRANSLATORS: when the update was built msgid "Created" msgstr "Creado" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "Crítica" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "Hay disponible verificación hash criptográfica" #. TRANSLATORS: current value of a BIOS setting msgid "Current Value" msgstr "Valor Actual" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "Versión actual" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "DEVICE-ID|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Opciones de depuración" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "Descomprimiendo…" #. TRANSLATORS: command description msgid "Delete an EFI boot entry" msgstr "Borra un apunte de arranque EFI" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Descripción" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "Suelta un modo de cargador de arranque" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "Detalles" #. TRANSLATORS: the best known configuration is a set of software that we know #. works #. * well together. In the OEM and ODM industries it is often called a BKC msgid "Deviate from the best known configuration?" msgstr "¿Desviado desde la configuración mejor conocida?" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "Indicadores del Dispositivo" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "ID de dispositivo" #. TRANSLATORS: description of the device requests msgid "Device Requests" msgstr "Solicitudes de Dispositivo" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Dispositivo añadido:" #. TRANSLATORS: the device is already connected msgid "Device already exists" msgstr "El dispositivo ya existe" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "La carga de batería del dispositivo es muy baja" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse #, c-format msgid "Device battery power is too low (%u%%, requires %u%%)" msgstr "La carga de la batería del dispositivo es muy baja (%u%%, requiere %u%%)" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "El dispositivo puede recuperar errores de flash" #. TRANSLATORS: lid means "laptop top cover" msgid "Device cannot be updated while the lid is closed" msgstr "El dispositivo no puede ser actualizado mientras la tapa esté cerrada" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Dispositivo modificado:" #. TRANSLATORS: a version check is required for all firmware msgid "Device firmware is required to have a version check" msgstr "El dispositivo firmware es necesario para hacer una comprobación de versión" #. TRANSLATORS: emulated means we are pretending to be a different model msgid "Device is emulated" msgstr "El dispositivo está emulado" #. TRANSLATORS: device cannot be interrupted, for instance taking a phone call msgid "Device is in use" msgstr "Dispositivo en uso" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "El dispositivo está bloqueado" #. TRANSLATORS: we have two ways of communicating with the device, so we hide #. one msgid "Device is lower priority than an equivalent device" msgstr "El dispositivo tiene una prioridad menor que el dispositivo equivalente" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "El dispositivo es requerido para instalar todas las publicaciones proporcionadas" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "Dispositivo no alcanzable" #. TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode msgid "Device is unreachable, or out of wireless range" msgstr "El dispositivo no es alcanzable, o está fuera del conexión sin cableado" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "El dispositivo es utilizable durante la actualización" #. TRANSLATORS: usually this is when we're waiting for a reboot msgid "Device is waiting for the update to be applied" msgstr "El dispositivo está esperando a que se actualice para ser aplicado" #. TRANSLATORS: success, so say thank you to the user msgid "Device list uploaded successfully, thanks!" msgstr "Listado de dispositivos correctamente subido, ¡gracias!" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Dispositivo retirado:" #. TRANSLATORS: as in, wired mains power for a laptop msgid "Device requires AC power to be connected" msgstr "El dispositivo requiere electricidad AC para conectarlo" #. TRANSLATORS: device does not have a display connected msgid "Device requires a display to be plugged in" msgstr "El dispositivo requiere una pantalla para ser enchufada" #. TRANSLATORS: The device cannot be updated due to missing vendor's license." msgid "Device requires a software license to update" msgstr "El dispositivo requiere una licencia de software para actualizarse" #. TRANSLATORS: longer description msgid "Device software updates are provided for this device." msgstr "Actualizaciones del software del dispositivo son proporcionados para este dispositivo." #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "Escenarios de actualizaciones del dispositivo" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "Dispositivo mantiene intercambio a una rama de firmware diferente" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "Actualización de dispositivo requiere activación" #. TRANSLATORS: save the old firmware to disk before installing the new one msgid "Device will backup firmware before installing" msgstr "Dispositivo respaldará el firmware antes de instalar" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "Dispositivo no re-aparecerá tras actualización termine" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "Dispositivos que han sido actualizados correctamente:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "Dispositivos que no fueron actualizados correctamente:" #. TRANSLATORS: message letting the user there is an update #. * waiting, but there is a reason it cannot be deployed msgid "Devices with firmware updates that need user action: " msgstr "Dispositivos con actualizaciones de firmware que necesitan interacción del usuario:" #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "Dispositivos sin actualizaciones del firmware disponible: " #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "Dispositivos con la última versión disponible del firmware:" #. TRANSLATORS: this is for the device tests msgid "Did not find any devices with matching GUIDs" msgstr "No encontró ningún dispositivo con los GUID coincidan" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "Deshabilitado" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "Desactiva un remoto proporcionado" #. TRANSLATORS: command description msgid "Disables virtual testing devices" msgstr "Desactiva dispositivos de pruebas virtuales" #. TRANSLATORS: the OS the release was tested on msgid "Distribution" msgstr "Distribución" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "No marcar para metadatos antiguos" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "No marcar para historial no comunicado" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "No marcar si las descargas remotas serían activadas" #. TRANSLATORS: command line option msgid "Do not check or prompt for reboot after update" msgstr "No marque o solicite para rearrancar tras actualizar" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "No incluye prefijo del boletín de dominio" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "No incluye prefijo de hora" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "No realiza comprobaciones seguras del dispositivo" #. TRANSLATORS: command line option msgid "Do not prompt for devices" msgstr "No se solicita para dispositivos" #. TRANSLATORS: command line option msgid "Do not prompt to fix security issues" msgstr "No solicitar para reparar problemas de seguridad" #. TRANSLATORS: command line option msgid "Do not search the firmware when parsing" msgstr "No buscar el firmware cuando analice la interpretación" #. TRANSLATORS: warning message shown after update has been scheduled msgid "Do not turn off your computer or remove the AC adaptor while the update is in progress." msgstr "No apague su equipo o retire el adaptador CA mientras la actualización está en proceso." #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "No se lee a la base de datos histórica" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "¿Comprende las consecuencias de cambiar la rama del firmware?" #. TRANSLATORS: ask the user if it's okay to convert, #. * "it" being the data contained in the EFI boot entry msgid "Do you want to convert it now?" msgstr "¿Desea convertirlo ahora?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "¿No desea deshabilitar esta característica para actualizaciones futuras?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "¿Quiere recargar ahora este remoto?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "¿Desea subir repositorios automáticamente para actualizaciones futuras?" #. TRANSLATORS: command line option msgid "Don't prompt for authentication (less details may be shown)" msgstr "No solicitar autenticación (menor detalle puede ser mostrado)" #. TRANSLATORS: success msgid "Done!" msgstr "Finalizado." #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "¿Degradar %s desde %s a %s?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "Degrada el firmware en un dispositivo" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "Degradando %s…" #. TRANSLATORS: command description msgid "Download a file" msgstr "Descargar un archivo" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Descargando…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "Vuelca datos SMBIOS desde un archivo" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "Duración" #. TRANSLATORS: longer description msgid "Each system should have tests to ensure firmware security." msgstr "Cada sistema tendría pruebas para verificar la seguridad del firmware." #. TRANSLATORS: command description msgid "Emulate a device using a JSON manifest" msgstr "Emula un dispositivo utilizando un manifiesto JSON" #. TRANSLATORS: this device is not actually real msgid "Emulated" msgstr "Emulado" #. TRANSLATORS: Title: if we are emulating a different host msgid "Emulated host" msgstr "Anfitrión emulado" msgid "Enable" msgstr "Activar" msgid "Enable emulation data collection" msgstr "Habilita la colección de datos de emulación" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "¿Habilitar remoto nuevo?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "¿Habilitar este remoto?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Activado" #. TRANSLATORS: Plugin is active only if hardware is found msgid "Enabled if hardware matches" msgstr "Habilitado si coincide con el hardware" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "Activa un remoto proporcionado" #. TRANSLATORS: command description msgid "Enables virtual testing devices" msgstr "Habilita dispositivos con prueva virtual" #. TRANSLATORS: longer description msgid "Enabling firmware updates for the BIOS allows fixing security issues." msgstr "Activa actualizaciones del firmware para la BIOS permite reparar problemas del seguridad." msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "Active esta funcionalidad bajo su responsabilidad, lo que significa que tiene que contactar con el fabricante del equipo original ante cualquier problema causado por estas actualizaciones. Solo se deben reportar los problemas con el mismo proceso de actualización en $OS_RELEASE:BUG_REPORT_URL$." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "Habilitar este remoto es realizado a su propio riesgo." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Cifrado" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "RAM cifrada" msgid "Encrypted RAM makes it impossible for information that is stored in device memory to be read if the memory chip is removed and accessed." msgstr "RAM cifrado lo hace imposible para información que esté almacenada en memoria del dispositivo para ser leído si el chip de memoria está extraído y accedido." #. TRANSLATORS: the vendor is no longer supporting the device msgid "End of life" msgstr "Final de vida" #. TRANSLATORS: The BIOS setting can only be changed to fixed values msgid "Enumeration" msgstr "Enumeración" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "Elimina todo el historial de actualización del firmware" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Borrando…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "Salir después de una pequeña pausa" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "Salir después que sea cargado el motor" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "Exporta una estructura del archivo firmware a XML" #. TRANSLATORS: command description msgid "Export firmware history for manual upload" msgstr "Exportar historial del firmware para carga manual" #. TRANSLATORS: command description msgid "Extract a firmware blob to images" msgstr "Extrae una gota de firmware a imágenes" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "ARCHIVO" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "ARCHIVO [ID-DISPOSITIVO|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "NOMBRE-DE-ARCHIVO" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "ID-ARCHIVO CERTIFICA LLAVE-PRIVADA" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "ID-NOMBRE ID-DISPOSITIVO" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME OFFSET DATA [FIRMWARE-TYPE]" msgstr "NOMBRE-ARCHIVO DESPLAZAMIENTO [TIPO-FIRMWARE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "NOMBRE-ARCHIVO [ID-DISPOSITIVO|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "NOMBRE-ARCHIVO [TIPO-FIRMWARE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" msgstr "NOMRE-ARCHIVO-SRC NOMBRE-ARCHIVO-DST [TIPO-FIRMWARE-SRC] [TIPO-FIRMWARE-DST]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" msgstr "ID-ARCHIVO|COMPROBANTE1[,COMPROBANTE2][,COMPROBANTE3]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "Erróneo" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Error al aplicar actualización" #. TRANSLATORS: error message for Windows msgid "Failed to connect to Windows service, please ensure it's running." msgstr "Ha fallado al conectar al servicio Windows, debe asegurar que esté en ejecución." #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "Error al conectar al demonio" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "Erróneo en carga dbx local" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "Erróneo en carga dbx del sistema" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "Error al bloquear" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "No se pudieron procesar los argumentos" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "No se pudo procesar el archivo" #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #, c-format msgid "Failed to parse flags for %s" msgstr "Ha fallado al interpretar indicadores para %s" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "Intérprete erróneo del dbx local" #. TRANSLATORS: a feature is something like "can show an image" msgid "Failed to set front-end features" msgstr "Erróneo al fijar características de front-end" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "Error al validar el contenido de ESP" #. TRANSLATORS: item is FALSE msgid "False" msgstr "Falso" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "ID de archivo" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "Firma del ID de archivo" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "Nombre del Archivo Origen" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Se requiere el nombre de archivo" #. TRANSLATORS: command line option msgid "Filter with a set of device flags using a ~ prefix to exclude, e.g. 'internal,~needs-reboot'" msgstr "Filtro con un conjunto de indicadores del dispositivo utilizando un prefijo ~ a excluir, p. ej. 'internet, ~necesita-rearrancar'" #. TRANSLATORS: command line option msgid "Filter with a set of release flags using a ~ prefix to exclude, e.g. 'trusted-release,~trusted-metadata'" msgstr "Filtrar con un conjunto de indicadores de publicación utilizando un prefijo ~ para excluir, p. ej., 'trusted-release,~trusted-metadata'" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware Attestation" msgstr "Prueba de Firmware" #. TRANSLATORS: longer description msgid "Firmware Attestation checks device software using a reference copy, to make sure that it has not been changed." msgstr "Garantía de firmware comprueba que el software del dispositivo utiliza una copia de referencia, para asegurar que no haya sido modificado." #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "Descriptor BIOS Firmware" #. TRANSLATORS: longer description msgid "Firmware BIOS Descriptor protects device firmware memory from being tampered with." msgstr "Descriptor del Firmware de BIOS protege dispositivo de memoria del firmware con el cual se manipuló." #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "Región BIOS Firmware" #. TRANSLATORS: longer description msgid "Firmware BIOS Region protects device firmware memory from being tampered with." msgstr "Región Firmware BIOS protege memoria del firmware del dispositivo con el cual se está manipulando." #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "URI de Firmware Base" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "Actualizar Firmware de Servicio D-Bus" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Demonio de Actualización de Firmware" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "Verificación para Actualizador del Firmware" msgid "Firmware Updater Verification checks that software used for updating has not been tampered with." msgstr "La Verificación de Actualizador del Firmware comprueba que el software utilizado para actualizar no ha sido manipulado." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "Actualizaciones del firmware" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "Utilidad Firmware" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "Protección de Escritura del Firmware" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "Bloqueo de Protección de Escritura Firmware" #. TRANSLATORS: longer description msgid "Firmware Write Protection protects device firmware memory from being tampered with." msgstr "Protección de Escritura del Firmware del de la memoria del firmware del dispositivo con el que es condonado. " #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "Garantía de firmware" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "Firmware ya está bloqueado" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "Firmware aún no está bloqueado" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "Los metadatos de firmware no han sido actualizados para %u día y puede no estar al día." msgstr[1] "Los metadatos de firmware no han sido actualizados para %u días y puede no estar al día." msgstr[2] "Los metadatos de firmware no han sido actualizados para %u días y puede no estar al día." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "Actualizaciones del firmware" #. TRANSLATORS: user needs to run a command, %1 is 'fwupdmgr unlock' #, c-format msgid "Firmware updates disabled; run '%s' to enable" msgstr "Actualizaciones de firmware desactivadas; ejecute '%s' para activarlo" #. TRANSLATORS: command description msgid "Fix a specific host security attribute" msgstr "Repara un atributo de seguridad del hospedaje específico" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fix reverted successfully" msgstr "Reparación revertida correctamente" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fixed successfully" msgstr "Reparado correctamente" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "Indicadores" #. TRANSLATORS: command line option msgid "Force the action by relaxing some runtime checks" msgstr "Fuerza la acción relajando algunas comprobaciones en tiempo de ejecución" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Encontrado" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "Detectado Cifrado de Disco Completo" #. TRANSLATORS: we might ask the user the recovery key when next booting #. Windows msgid "Full disk encryption secrets may be invalidated when updating" msgstr "Secretos de cifrado de disco completo tal vez está invalidado cuando modernice" #. TRANSLATORS: Title: if the part has been fused msgid "Fused Platform" msgstr "Plataforma Fusionada" #. TRANSLATORS: Title: if the part has been fused msgid "Fused platform" msgstr "Plataforma fusionada" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" msgstr[1] "GUID" msgstr[2] "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "GUID|DEVICE-ID" msgstr "GUID|ID-DISPOSITIVO" msgid "Get BIOS settings" msgstr "Obtener parámetros de BIOS" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "Obtenga todos los indicadores del dispositivo admitido por fwpd" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Obtiene todos los dispositivos que mantengan actualizaciones de firmware" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "Obtiene todos los complementos registrados activados con el sistema" #. TRANSLATORS: command description msgid "Get all known version formats" msgstr "Obtiene todos los formatos de versión conocidos" #. TRANSLATORS: command description msgid "Get device report metadata" msgstr "Obtenga el informe de metadatos del dispositivo" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Obtiene detalles acerca de un archivo firmware" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "Obtiene los remotos configurados" #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "Obtiene los atributos de seguridad del hospedaje" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "Obtiene el listado de firmware aprobado" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "Obtiene el listado de firmware bloqueado" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Obtiene el listado de actualizaciones para hardware conectado" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "Obtiene las publicaciones para un dispositivo" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Obtiene los resultados desde la última actualización" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "HWIDS-ARCHIVO" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "El hardware está esperando que sea enchufado" #. TRANSLATORS: the release urgency msgid "High" msgstr "Alta" #. TRANSLATORS: title for host security events msgid "Host Security Events" msgstr "Eventos de Seguridad de Hospedaje" #. TRANSLATORS: error message for unsupported feature #, c-format msgid "Host Security ID (HSI) is not supported" msgstr "ID de Seguridad de Hospedaje (HSI) no está mantenido" #. TRANSLATORS: success, so say thank you to the user msgid "Host Security ID attributes uploaded successfully, thanks!" msgstr "Atributos de ID de seguridad del anfitrión correctamente subido, ¡gracias!" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "ID de seguridad de hospedaje:" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX" msgstr "ÃNDICE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX KEY [VALUE]" msgstr "ÃNDICE LLAVE [VALOR]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX NAME TARGET [MOUNTPOINT]" msgstr "ÃNDICE NOMBRE DESTINO [PUNTO_MONTAJE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX1,INDEX2" msgstr "ÃNDICE1,ÃNDICE2" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INHIBIT-ID" msgstr "INHIBIR-ID" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "Protección de dispositivo IOMMU" #. TRANSLATORS: longer description msgid "IOMMU Protection prevents connected devices from accessing unauthorized parts of system memory." msgstr "Protección IOMMU previene dispositivos conectados desde partes de acceso no autorizadas de memoria del sistema." #. TRANSLATORS: HSI event title msgid "IOMMU device protection disabled" msgstr "Protección de dispositivo IOMMU desactivado" #. TRANSLATORS: HSI event title msgid "IOMMU device protection enabled" msgstr "Protección de dispositivo IOMMU activado" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "Inactivo…" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "Descarta comprobaciones estrictas de SSL cuando descargue archivos" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "Hace caso omiso a errores del sumatorio del firmware" #. TRANSLATORS: command line option msgid "Ignore firmware hardware mismatch failures" msgstr "Hace caso omiso a errores del concordancia del hardware" #. TRANSLATORS: command line option msgid "Ignore non-critical firmware requirements" msgstr "Ignorar requisitos del firmware no críticos" #. TRANSLATORS: try to help msgid "Ignoring SSL strict checks, to do this automatically in the future export DISABLE_SSL_STRICT in your environment" msgstr "Descartando comprobaciones estrictas SSL, para hacer esto automáticamente en la futura exportación DISABLE_SSL_STRICT dentro de su entorno" #. TRANSLATORS: show the user a generic image that can be themed msgid "Image" msgstr "Imagen" #. TRANSLATORS: show the user a random image from the internet msgid "Image (custom)" msgstr "Imagen (personal)" #. TRANSLATORS: the inhibit ID is a short string like dbus-123456 #, c-format msgid "Inhibit ID is %s." msgstr "ID inhibido es %s." #. TRANSLATORS: command description msgid "Inhibit the system to prevent upgrades" msgstr "Inhibe el sistema para bloquear actualizaciones" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "Duración de Instalación" #. TRANSLATORS: command description msgid "Install a firmware file in cabinet format on this hardware" msgstr "Instala un archivo firmware en formato de cabina en este hardware" #. TRANSLATORS: command description msgid "Install a raw firmware blob on a device" msgstr "Instale un globo firmware en crudo en un dispositivo" #. TRANSLATORS: command description msgid "Install a specific firmware file on all devices that match" msgstr "Instala un archivo firmware específico en todos los dispositivos que coincidan" #. TRANSLATORS: command description msgid "Install a specific firmware on a device, all possible devices will also be installed once the CAB matches" msgstr "Instala un firmware específico en un dispositivo, todos los dispositivos posibles además serán instalados una vez que coincida el CAB" msgid "Install old version of signed system firmware" msgstr "Instala versión antigua de firmware del sistema firmado" msgid "Install old version of unsigned system firmware" msgstr "Instalar una versión antigua de firmware del sistema no firmado" msgid "Install signed device firmware" msgstr "Instalar firmware del dispositivo firmado" msgid "Install signed system firmware" msgstr "Instalar firmware del sistema firmado" msgid "Install unsigned device firmware" msgstr "Instalar firmware del dispositivo no firmado" msgid "Install unsigned system firmware" msgstr "Instalar firmware del sistema sin firmar" #. TRANSLATORS: stay on one firmware version unless the new version is #. explicitly #. * specified msgid "Installing a specific release is explicitly required" msgstr "Se requiere una publicación específica para ser instalado" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "Instalando actualización del firmware…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "Instalando en %s…" #. TRANSLATORS: if it breaks, you get to keep both pieces msgid "Installing this update may also void any device warranty." msgstr "Instalando esta actualización tal vez anule cualquier garantía del dispositivo." #. TRANSLATORS: The BIOS setting only accepts integers in a fixed range msgid "Integer" msgstr "Entero" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM Protected" msgstr "ACM protegido Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "ACM protegido Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard Error Policy" msgstr "Error de normativa Intel BootGuard" msgid "Intel BootGuard Error Policy ensures the device does not continue to start if its device software has been tampered with." msgstr "Normativa Errónea de Intel BootGuard asegura que el dispositivo no continúe iniciar si su software del dispositivo haya sido manipulado." #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard Fuse" msgstr "Intel BootGuard Fuse" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "Intel BootGuard OTP fuse" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard Verified Boot" msgstr "Arranque verificado Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Error de normativa Intel BootGuard" #. TRANSLATORS: longer description msgid "Intel BootGuard prevents unauthorized device software from operating when the device is started." msgstr "Intel BootGuard impide software de dispositivo no autorizado al operar cuando el dispositivo es arrancado." #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Arranque verificado Intel BootGuard" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS Mitigation" msgstr "Mitigación de GDS de Intel" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS mitigation" msgstr "Mitigación de GDS de Intel" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Manufacturing Mode" msgstr "Modo de Manufactura del Motor de Gestión Intel" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the "override" is #. enabled #. * with a jumper -- luckily it is probably not accessible to end users on #. consumer #. * boards msgid "Intel Management Engine Override" msgstr "Modo de Manufactura del Motor de Gestión Intel" #. TRANSLATORS: longer description msgid "Intel Management Engine Override disables checks for device software tampering." msgstr "Descarta el Gestor de Motor Intel desactiva las comprobaciones para manipulación de software del dispositivo." #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Version" msgstr "Versión del Motor de Gestión Intel" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "Dispositivo interno" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "No válida" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "Argumentos no válidos" #. TRANSLATORS: error message msgid "Invalid arguments, expected GUID" msgstr "Argumentos no válidos, se esperaba GUID" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX KEY [VALUE]" msgstr "Argumentos no válidos, se esperaba INDEX KEY [VALOR]" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX NAME TARGET [MOUNTPOINT]" msgstr "Argumentos no válidos, se esperaba INDEX NAME TARGET [PUNTO-MONTAJE]" #. TRANSLATOR: This is the error message for #. * incorrect parameter msgid "Invalid arguments, expected an AppStream ID" msgstr "Argumentos no válidos, se esperaba un ID de AppStream" #. TRANSLATORS: error message msgid "Invalid arguments, expected at least ARCHIVE FIRMWARE METAINFO" msgstr "Argumentos no válidos, esperados al menos ARCHIVE FIRMWARE METAINFO" #. TRANSLATORS: error message msgid "Invalid arguments, expected base-16 integer" msgstr "Argumentos no válidos, se esperaba entero con base-16" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "Está degradada" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "Está dentro del modo cargador de arranque" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "Está modernizada" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "Informe" msgstr[1] "Informes" msgstr[2] "Informes" #. TRANSLATORS: HSI event title msgid "Kernel is no longer tainted" msgstr "El núcleo ya no está contaminado" #. TRANSLATORS: HSI event title msgid "Kernel is tainted" msgstr "El núcleo está contaminado" #. TRANSLATORS: HSI event title msgid "Kernel lockdown disabled" msgstr "Bloqueo desactivado del kernel" #. TRANSLATORS: HSI event title msgid "Kernel lockdown enabled" msgstr "Bloqueo activado del kernel" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "LOCALIZACIÓN" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "Último modificado" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "Falta menos de un minuto" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "Licencia" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "Kernel Linux bloqueado" #. TRANSLATORS: longer description msgid "Linux Kernel Lockdown mode prevents administrator (root) accounts from accessing and changing critical parts of system software." msgstr "El modo Cerrojo del Kernel Linux previene al administrador (root) las cuentas desde el acceso y modificación de partes críticas del software del sistema." msgid "Linux Kernel Swap temporarily saves information to disk as you work. If the information is not protected, it could be accessed by someone if they obtained the disk." msgstr "El Intercambio del Kernel Linux guarda información temporalmente al disco para que pueda trabajar. Si la información no está protegida, pudo ser accedido por alguno si obtuvieron el disco." #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "Verificación de Kernel Linux" msgid "Linux Kernel Verification makes sure that critical system software has not been tampered with. Using device drivers which are not provided with the system can prevent this security feature from working correctly." msgstr "La Verificación del Kernel Linux asegura que el software crítico del sistema no ha sido manoseado. Utilizando controladores del dispositivo los cuales no sean proporcionados con el sistema pueden prevenir esta característica de seguridad desde el trabajo correcto." #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "Intercambio (swap) de Linux" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "«Linux Vendor Firmware Service» (firmware estable)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "«Linux Vendor Firmware Service» (firmware de pruebas)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Kernel Linux" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Kernel Linux bloqueado" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Intercambio de Linux" #. TRANSLATORS: command description msgid "List EFI boot files" msgstr "Listado de ficheros de arranque EFI" #. TRANSLATORS: command description msgid "List EFI boot parameters" msgstr "Listado de parámetros de arranque EFI" #. TRANSLATORS: command description msgid "List EFI variables with a specific GUID" msgstr "Enumera variables EFI con un GUID específico" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "Listado de apuntes en dbx" #. TRANSLATORS: command description msgid "List the available firmware GTypes" msgstr "Enumera los tipos GType del firmware que estén disponibles." #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "Enumera los tipos de firmware disponibles" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "Enumera archivos en la ESP" #. TRANSLATORS: command description msgid "Load device emulation data" msgstr "Carga los datos de emulación de dispositivo" #. TRANSLATORS: the plugin was created from a .so object, and was not built-in msgid "Loaded from an external module" msgstr "Cargado desde un módulo externo" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "Cargando…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "Bloqueado" #. TRANSLATORS: the release urgency msgid "Low" msgstr "Baja" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refers #. * to the private/public key used to secure loading of firmware msgid "MEI Key Manifest" msgstr "Declaración de llave MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refer #. * to the private/public key used to secure loading of firmware msgid "MEI key manifest" msgstr "Manifestar clave MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "Modo manufacturante MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "Sobrecarga MEI" msgid "MEI version" msgstr "Versión MEI" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "Activa manualmente complementos específicos" #. TRANSLATORS: longer description msgid "Manufacturing Mode is used when the device is manufactured and security features are not yet enabled." msgstr "Modo Fabricante se utiliza cuando el dispositivo es fabricado y las características de seguridad aún no están activadas." #. TRANSLATORS: Longest valid string for BIOS setting msgid "Maximum length" msgstr "Longitud máxima" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "Valor máximo" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "Mediana" #. TRANSLATORS: ask the user to do a simple task which should be translated msgid "Message" msgstr "Mensaje" #. TRANSLATORS: ask the user a question, and it will not be translated msgid "Message (custom)" msgstr "Mensaje (personal)" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "Firma Metadatos" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "URI Metadatos" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "Metadatos pueden obtenerse desde el Servicio de Firmware del Proveedor Linux." #. TRANSLATORS: error message for a user who ran fwupdmgr #. * refresh recently -- %1 is '--force' #, c-format msgid "Metadata is up to date; use %s to refresh again." msgstr "Los metadatos están al día; utilice %s para recargar de nuevo." #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "Versión Mínima" #. TRANSLATORS: Shortest valid string for BIOS setting msgid "Minimum length" msgstr "Longitud mínima" #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "Valor mínimo" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "Codifica un valor de configuración del demonio" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "Modifica un remoto proporcionado" msgid "Modify a configured remote" msgstr "Modificar un remoto configurado" msgid "Modify daemon configuration" msgstr "Modifica la configuración del demonio" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "Monitoriza al demonio para eventos" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "Monta el ESP" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "Necesita rearrancar tras la instalación" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "Requiere reinicio" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "Necesita apagar tras la instalación" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "Versión nueva" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "No se especificó ninguna acción." #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "Ninguna degradación para %s" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "Ningún ID de firmware encontrado" #. TRANSLATORS: nothing found msgid "No firmware found" msgstr "Ningún firmware encontrado" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Ningún hardware detectado con capacidad de actualización del firmware" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "Ninguna liberación disponible" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "Ningunos remotos están activados actualmente tal que ningún metadato está disponible." #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "Ningún remoto disponible" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "Ningún dispositivo actualizable" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "No hay actualizaciones disponibles" msgid "No updates available for remaining devices" msgstr "No hay actualizaciones disponibles para dispositivos restantes" #. TRANSLATORS: error message #, c-format msgid "No volume matched %s" msgstr "No coincidía ningún %s" #. TRANSLATORS: version cannot be installed due to policy msgid "Not approved" msgstr "No aprobado" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "No encontrado" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "No compatible" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "APROBADO" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "¡Perfecto!" #. TRANSLATORS: the firmware old version msgid "Old version" msgstr "Versión anterior" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "Solamente muestra el único valor de PCR" #. TRANSLATORS: command line option msgid "Only use peer-to-peer networking when downloading files" msgstr "Solamente utilizar redes pareja a pareja cuando descargue archivos" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "Solamente las modernizaciones de versión están permitidas" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "Supera la ruta del ESP predeterminado" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Firmware" msgstr "Firmware P2P" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Metadata" msgstr "Metadatos P2P" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "RUTA" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "Interpreta y muestra detalles acerca de un archivo de firmware" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "Interpretando actualización dbx…" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "Interpretando sistema dbx…" #. TRANSLATORS: remote filename base msgid "Password" msgstr "Contraseña" #. TRANSLATORS: command description msgid "Patch a firmware blob at a known offset" msgstr "Parchea una gota de firmware en un desplazamiento conocido" msgid "Payload" msgstr "Carga" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "Pendiente" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "¿Realizar operación?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "Depuración de Plataforma" #. TRANSLATORS: longer description msgid "Platform Debugging allows device security features to be disabled. This should only be used by hardware manufacturers." msgstr "Depuración de plataforma permite características de seguridad del dispositivo que sean desactivadas. Esto solamente serían utilizadas por fabricante del hardware." #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "Depuración de plataforma" #. TRANSLATORS: 'recovery key' here refers to a code, rather than a physical #. metal thing msgid "Please ensure you have the volume recovery key before continuing." msgstr "Asegure que tenga la tecla de recuperación del volumen antes de continuar." #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "Introduzca un número desde 0 hasta %u: " #. TRANSLATORS: the user isn't reading the question -- %1 is 'Y' and %2 is #. * 'N' #, c-format msgid "Please enter either %s or %s: " msgstr "Introduzca o bien %s o bien %s:" #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "Dependencias ausentes del complemento" #. TRANSLATORS: The plugin is only for testing msgid "Plugin is only for testing" msgstr "El complemento es solo para pruebas" #. TRANSLATORS: Possible values for a bios setting msgid "Possible Values" msgstr "Valores Posibles" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA Protection" msgstr "Protección DMA de pre-arranque" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "Protección DMA de pre-arranque" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is disabled" msgstr "Protección DMA de pre-arranque está desactivada" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is enabled" msgstr "Protección DMA de pre-arranque está activada" #. TRANSLATORS: longer description msgid "Pre-boot DMA protection prevents devices from accessing system memory after being connected to the computer." msgstr "La protección del DMA del pre-arranque previene acceder a dispositivos a la memoria del sistema después de ser conectada al equipo." #. TRANSLATORS: warning message msgid "Press unlock on the device to continue the update process." msgstr "Presione desbloquear en el dispositivo para continuar el proceso de actualización." #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "Versión anterior" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Prioridad" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "Problemas" msgid "Proceed with upload?" msgstr "¿Proceder con subida?" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Processor Security Checks" msgstr "Comprobaciones de Seguridad del Procesador" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "Processor rollback protection" msgstr "Procesador retira protección" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "Titular" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "ID-REMOTO" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID KEY VALUE" msgstr "ID-REMOTO CLAVE VALOR" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "Solo Lectura" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "Lee una gota del firmware desde un dispositivo" #. TRANSLATORS: command description msgid "Read a firmware from a device" msgstr "Lea un firmware desde un dispositivo" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "Leyendo desde %s…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Leyendo…" #. TRANSLATORS: Plugin is active and in use msgid "Ready" msgstr "Preparado" #. TRANSLATORS: how often we should refresh the metadata msgid "Refresh Interval" msgstr "Intervalo de recarga" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "Recarga metadatos desde servidor remoto" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "¿Reinstalar %s a %s?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "Reinstala firmware actual en el dispositivo" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "Reinstalar firmware en un dispositivo" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "Ramificación de Publicación" #. TRANSLATORS: release attributes msgid "Release Flags" msgstr "Indicadores de Publicación" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "ID Publicación" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "ID Remoto" #. TRANSLATORS: command description msgid "Removes devices to watch for future emulation" msgstr "Extrae dispositivos para vigilar en emulación futura." #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "URI de Comunicado" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "Comunicado para servidor remoto" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "Sistema de archivos efivarfs requerido no fue encontrado" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "Hardware requerido no fue encontrado" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "Requiere un cargador de arranque" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Requiere conexión a Internet" msgid "Reset daemon configuration" msgstr "Reiniciar la configuración del demonio" #. TRANSLATORS: sets something in the daemon configuration file msgid "Resets a daemon configuration section" msgstr "Restablece una sección de la configuración del demonio" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "¿Reiniciar ahora?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "¿Reinicio el demonio para crear el cambio efectivo?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Reiniciando dispositivo…" #. TRANSLATORS: command description msgid "Retrieve BIOS settings. If no arguments are passed all settings are returned" msgstr "Obtiene la configuración de la BIOS. Si no se proporciona ningún argumente se devuelve toda la configuración." #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "Devuelve todos los ID de hardware para la máquina" #. TRANSLATORS: ask the user to upload msgid "Review and upload report now?" msgstr "¿Ahora informo de revisión y subida?" #. TRANSLATORS: longer description msgid "Rollback Protection prevents device software from being downgraded to an older version that has security problems." msgstr "Retroceder Protección previene software de dispositivo sea degradado a una versión más antigua que tenga problemas de seguridad." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr get-upgrades` #, c-format msgid "Run `%s` for more information." msgstr "Ejecute `%s` para más información." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr sync` #, c-format msgid "Run `%s` to complete this action." msgstr "Ejecute `%s` para completar esta acción." #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "Ejecuta el complemento compuesto vacía rutina cuando utiliza gota de instalación" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "Ejecute el complemento compuesto de rutina preparada cuando utilice install-blob" #. TRANSLATORS: command description msgid "Run the post-reboot cleanup action" msgstr "Ejecute la acción de limpieza posterior al rearranque" #. TRANSLATORS: tell a user how to get information #, c-format msgid "Run without '%s' to see" msgstr "Ejecutar sin '%s' para ver" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "Ejecutando kernel que es demasiado antiguo" #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "Sufijo de tiempo de ejecución" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SECTION" msgstr "SECCIÓN" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING VALUE" msgstr "DECLARANDO VALOR" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING1 VALUE1 [SETTING2] [VALUE2]" msgstr "PARÃMETRO1 VALOR1 [PARÃMETRO2] [VALOR2]" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "SMAP" msgstr "SMAP" #. TRANSLATORS: Title: Whether firmware is locked down msgid "SMM locked down" msgstr "SMM bloqueado" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "Descriptor BIOS SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "Región BIOS SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "Candado SPI" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "SPI replay protection" msgstr "SPI solicita protección" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "Escritura SPI" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "SPI write protection" msgstr "Protección de escritura SPI" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "CONTROLADOR DE SUBSISTEMA [ID-DISPOSITIVO|GUID]" #. TRANSLATORS: command description msgid "Save a file that allows generation of hardware IDs" msgstr "Guarda un archivo que permita generación de los ID hardware" #. TRANSLATORS: command description msgid "Save device emulation data" msgstr "Guarda los datos de emulación de dispositivo" #. TRANSLATORS: key for a offline report filename msgid "Saved report" msgstr "Informe guardado" #. TRANSLATORS: Scalar increment for integer BIOS setting msgid "Scalar Increment" msgstr "Incremento Escalar" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Planificando…" #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "Arranque Seguro desactivado" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "Arranque Seguro activado" msgid "Security hardening for HSI" msgstr "Endurecimiento de seguridad para HSI" #. TRANSLATORS: the %1 is a URL to a wiki page #, c-format msgid "See %s for more details." msgstr "Vea %s para más detalles." #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "Vea %s para más información." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Dispositivo seleccionado" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Volumen seleccionado" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "Número de Serie" #. TRANSLATORS: Configured a BIOS setting to a value #, c-format msgid "Set BIOS setting '%s' using '%s'." msgstr "Fija parámetros de la BIOS '%s' utilizando '%s'." #. TRANSLATORS: command description msgid "Set a BIOS setting" msgstr "Fije un parámetro de la BIOS" msgid "Set one or more BIOS settings" msgstr "Fije uno o más parámetros de BIOS" #. TRANSLATORS: command description msgid "Set or remove an EFI boot hive entry" msgstr "Pone o quita un apunte de arranque EFI" #. TRANSLATORS: command description msgid "Set the EFI boot next" msgstr "Establece el siguiente arranque de EFI" #. TRANSLATORS: command description msgid "Set the EFI boot order" msgstr "Ponga el orden de arranque de EFI" #. TRANSLATORS: command line option msgid "Set the download retries for transient errors" msgstr "Fija reintentos de descarga para errores transitorios" #. TRANSLATORS: command description msgid "Sets one or more BIOS settings" msgstr "Fija uno o más parámetros de BIOS" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Establece el firmware aprobado del listado" #. TRANSLATORS: type of BIOS setting msgid "Setting type" msgstr "Tipo paramétrico" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "Los parámetros aplicarán tras reiniciar el sistema" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "Compartir historial de firmware con los desarrolladores" #. TRANSLATORS: command line option msgid "Show all results" msgstr "Muestra todos los resultados" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "Mostrar versiones del cliente y el servicio" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "Muestra verborrea de información del demonio para un dominio particular" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "Muestra información de depuración para todos los dominios" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Mostrar información extra de depuración" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Muestra dispositivos que no son actualizables" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Mostrar información de depuración adicional" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "Muestra historial de actualizaciones del firmware" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "Muestra la versión calculada del dbx" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "¿Apago ahora?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "Firma un firmware con una llave nueva" msgid "Sign data using the client certificate" msgstr "Firmar datos utilizando el certificado del cliente" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "Firmar datos utilizando el certificado del cliente" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "Firma los datos subidos con el certificado del cliente" msgid "Signature" msgstr "Firma" #. TRANSLATORS: firmware is verified on-device the payload using strong crypto msgid "Signed Payload" msgstr "Carga Firmada" #. TRANSLATORS: file size of the download msgid "Size" msgstr "Tamaño" #. TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM msgid "Some of the platform secrets may be invalidated when updating this firmware." msgstr "Alguno de los secretos de la plataforma tal vez sea invalidada cuando actualice este firmware." #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "Origen" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "Específicamente el archivo de la base de datos sbx" msgid "Stop the fwupd service" msgstr "Detiene el servicio fwupd" #. TRANSLATORS: The BIOS setting accepts strings msgid "String" msgstr "Cadena" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "Correcto" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "Se activaron correctamente todos los dispositivos" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "Remoto desactivado correctamente" #. TRANSLATORS: comment explaining result of command msgid "Successfully disabled test devices" msgstr "Se desactivaron correctamente los dispositivos de prueba" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "Descarga correctamente metadatos nuevos: " #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "Remoto activado y recargado correctamente" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "Remoto activado correctamente" #. TRANSLATORS: comment explaining result of command msgid "Successfully enabled test devices" msgstr "Se activaron correctamente los dispositivos de prueba" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "Firmware instalado correctamente" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "Valor de configuración modificado correctamente" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "Remoto modificado correctamente" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "Metadatos actualizados manualmente correctamente" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration section" msgstr "Sección de configuración restablecida correctamente" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration values" msgstr "Valores de configuración reiniciados correctamente" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "Sumatorio de dispositivo actualizado correctamente" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "Se ha subido %u parte correctamente" msgstr[1] "Se han subido %u partes correctamente" msgstr[2] "Se han subido %u partes correctamente" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "Comprobación correctamente verificada del dispositivo" #. TRANSLATORS: the device showed up in time #, c-format msgid "Successfully waited %.0fms for device" msgstr "Ha esperado correctamente %.0f ms para dispositivo" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Resumen" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "Supervisor Mode Access Prevention" msgstr "Prevención de Supervisor de Modo de Acceso" #. TRANSLATORS: longer description msgid "Supervisor Mode Access Prevention ensures critical parts of device memory are not accessed by less secure programs." msgstr "Prevención de Supervisor del Modo de Acceso asegura partes críticas de la memoria del dispositivo no está accedido por programas menos seguros." #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "Mantenido" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "CPU admitida" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "Mantenido en servidor remoto" #. TRANSLATORS: Title: a better sleep state msgid "Suspend To Idle" msgstr "Suspendido a Descanso" #. TRANSLATORS: Title: sleep state msgid "Suspend To RAM" msgstr "Suspender a RAM" #. TRANSLATORS: longer description msgid "Suspend to Idle allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Suspensión para Descanso permite que el dispositivo se duerma rápidamente con el fin de guardar energía. Mientras que el dispositivo esté suspendido, su memoria pudo ser retirada físicamente y su información accedida." #. TRANSLATORS: longer description msgid "Suspend to RAM allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Suspensión a RAM permite al dispositivo que rápidamente se vuelva durmiente con el fin de guardar energía. Mientras el dispositivo ha estado suspendido, su memoria pudo ser físicamente retirada y su información accedida." #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "Suspendido-a-descanso" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "Suspender a RAM" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "¿Ramificación intercambiada desde %s hasta %s?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "Intercambiar rama firmware en el dispositivo" #. TRANSLATORS: command description msgid "Sync firmware versions to the chosen configuration" msgstr "Sincroniza versiones del firmware para la configuración deseada" #. TRANSLATORS: Title: Whether firmware is locked down msgid "System Management Mode" msgstr "Modo de Gestión del Sistema" #. TRANSLATORS: this CLI tool is now preventing system updates msgid "System Update Inhibited" msgstr "Actualización de Sistema Inhibido" #. TRANSLATORS: longer description msgid "System management mode is used by the firmware to access resident BIOS code and data." msgstr "El firmware se utiliza en el modo de gestión del sistema para acceder al código BIOS residente y los datos." #. TRANSLATORS: as in laptop battery power msgid "System power is too low" msgstr "La energía del sistema es muy baja" #. TRANSLATORS: as in laptop battery power #, c-format msgid "System power is too low (%u%%, requires %u%%)" msgstr "La energía del sistema es muy escasa (%u%%, requiere %u%%)" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "Sistema requiere energía externa" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "TEXTO" #. TRANSLATORS: longer description msgid "TPM (Trusted Platform Module) is a computer chip that detects when hardware components have been tampered with." msgstr "TPM (Trusted Platform Module) es un chip del equipo que detecta cuando los componentes del hardware han sido manipulados." #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "Reconstrucción PCRO de TPM" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is invalid" msgstr "Reconstrucción PCR0 de TPM no es válida" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is now valid" msgstr "Reconstrucción PCR0 de TPM ahora es válida" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM Platform Configuration" msgstr "Configuración de Plataforma TPM" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM Reconstruction" msgstr "Reconstrucción TPM" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM empty PCRs" msgstr "Los PCR vacíos de TPM" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "Etiqueta" msgstr[1] "Etiquetas" msgstr[2] "Etiquetas" #. TRANSLATORS: we're saving all USB events for emulation msgid "Tagged for emulation" msgstr "Etiquetado para emulación" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "Contaminado" #. show the user the entire data blob msgid "Target" msgstr "Objetivo" #. TRANSLATORS: command description msgid "Test a device using a JSON manifest" msgstr "Testea un dispositivo utilizando una declaración JSON" #. TRANSLATORS: when the release was tested msgid "Tested" msgstr "Probado" #. TRANSLATORS: the %s is a vendor name, e.g. Lenovo #, c-format msgid "Tested by %s" msgstr "Probado por %s" #. TRANSLATORS: someone we trust has tested this msgid "Tested by trusted vendor" msgstr "Probado por proveedor confiado" #. TRANSLATORS: the boot entry was in a legacy format msgid "The EFI boot entry is not in hive format, and shim may not be new enough to read it." msgstr "El apunte de arranque EFI no es un formato colmena, y es posible que la cuña no sea suficiente nueva para leerla." #. TRANSLATORS: try to treat the legacy format as a hive msgid "The EFI boot entry was not in hive format, falling back" msgstr "El apunte EFI no estaba en el formato hive, retrocede" #. TRANSLATORS: longer description msgid "The Intel Management Engine Key Manifest must be valid so that the device firmware can be trusted by the CPU." msgstr "El Motor de Gestión Intel Manifiesta Clave debe ser válida tal que el firmware del dispositivo pueda ser confiado por la CPU." #. TRANSLATORS: longer description msgid "The Intel Management Engine controls device components and needs to have a recent version to avoid security issues." msgstr "El Motor de Gestión de Intel controla componentes del dispositivo y necesita tener una versión reciente para evitar problemas de seguridad." #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "El LVFS es un servicio libre que opera como una entidad independiente legal y no tienen contacto con $OS_RELEASE:NAME$. Su distribuidor no está verificado y ninguna de las actualizaciones del firmware para compatibilidad con su sistema o dispositivos conectados. Todo el firmware está distribuido únicamente por el fabricante del equipo original." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Platform Configuration is used to check whether the device start process has been modified." msgstr "La Configuración de la Plataforma TPM (Trusted Platform Module) se utiliza si el dispositivo inicial del proceso ha sido modificado." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Reconstruction is used to check whether the device start process has been modified." msgstr "La Reconstrucción de TPM (Trusted Platform Module) se utiliza para comprobar si el proceso de arranque del dispositivo ha sido modificado." #. TRANSLATORS: this is more background on a security measurement problem msgid "The TPM PCR0 differs from reconstruction." msgstr "El PCR0 TPM difiere desde reconstrucción." #. TRANSLATORS: longer description msgid "The UEFI Platform Key is used to determine if device software comes from a trusted source." msgstr "La Clave de Plataforma UEFI es utilizada para determinar si el software del dispositivo viene desde un origen confiado." #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "¡El demonio ha cargado código de 3ª parte y ya no está mantenido por los desarrolladores actuales!" #. TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name #, c-format msgid "The firmware from %s is not supplied by %s, the hardware vendor." msgstr "El firmware desde %s no está suministrado por %s, el fabricante del hardware." #. TRANSLATORS: try to help msgid "The system clock has not been set correctly and downloading files may fail." msgstr "El reloj del sistema no ha sido fijado correctamente y los archivos descargados pueden fallar." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been re-inserted." msgstr "La actualización continuará cuando el dispositivo del cable USB ha sido re-insertado." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged and then re-inserted." msgstr "La actualización continuará cuando el cable USB del dispositivo haya sido desenchufado y después re-insertado." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged." msgstr "La actualización continuará cuando el cable del dispositivo USB haya sido desenchufado." #. TRANSLATORS: warning message msgid "The update will continue when the device power cable has been removed and re-inserted." msgstr "La actualización continuará cuando el cable de energía del dispositivo haya sido desenchufado y vuelto a enchufar." #. TRANSLATORS: naughty vendor msgid "The vendor did not supply any release notes." msgstr "El proveedor no admite ninguna nota de publicación." #. TRANSLATORS: now list devices with unfixed high-priority issues msgid "There are devices with issues:" msgstr "Hay dispositivos con problemas:" #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "No hay archivos firmware bloqueados" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "No hay ningún firmware aprobado." #. TRANSLATORS: %1 is the current device version number, and %2 is the #. command name, e.g. `fwupdmgr sync` #, c-format msgid "This device will be reverted back to %s when the %s command is performed." msgstr "Este dispositivo será revertido a %s cuando la instrucción %s sea realizada." #. TRANSLATORS: the vendor did not upload this msgid "This firmware is provided by LVFS community members and is not provided (or supported) by the original hardware vendor." msgstr "Este firmware se proporcionó por miembros de la comunidad LVFS y no proporcionó (o mantuvo) por el proveedor del hardware original." #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "Este paquete no ha sido validado, quizá no funciona apropiadamente." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "Este programa tal vez solo funciona correctamente como root" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "Este remoto contiene firmware que no está embargado, pero aún está siendo probado por el proveedor de hardware. Debería asegurarse que tenga una manera de revertir el firmware a la versión anterior manualmente si falla la actualización del firmware." #. TRANSLATORS: error message msgid "This system doesn't support firmware settings" msgstr "Este sistema no mantiene parámetros del firmware" #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "Este sistema tiene problemas en tiempo de ejecución HSI." #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "Este sistema tiene un nivel de seguridad HSI bajo." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "Esta herramienta permite un administrados para aplicar actualizaciones dbx de UEFI." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "Esta herramienta permite un administrados para solicitar y controla el demonio fwupd, permitiéndolos entonces realizar acciones tales como instalar o bajar la versión del firmware." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "Esta herramienta permite que un administrador utilice los complementos fwupd sin ser instalados en el sistema hospedado." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can add a kernel argument of '%s', but it will only be active after restarting the computer." msgstr "Esta herramienta puede añadir un argumento del kernel de '%s', pero solo estará activo tras rearrancar el equipo." #. TRANSLATORS: the %1 is a BIOS setting name. #. * %2 and %3 are the values, e.g. "True" or "Windows10" #, c-format msgid "This tool can change the BIOS setting '%s' from '%s' to '%s' automatically, but it will only be active after restarting the computer." msgstr "Esta herramienta puede cambiar los parámetros de BIOS «%s» desde «%s» a «%s» automáticamente, pero serán activados únicamente tras rearrancar el equipo." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can change the kernel argument from '%s' to '%s', but it will only be active after restarting the computer." msgstr "Esta herramienta puede cambiar el argumento del kernel desde '%s' hasta '%s', pero solamente será activa tras rearrancar el equipo." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "Esta herramienta leerá e interpretará boletín de evento TPM desde el firmware del sistema." #. TRANSLATORS: the update state of the specific device msgid "Transient failure" msgstr "Transición errónea" #. TRANSLATORS: item is TRUE msgid "True" msgstr "Cierto" #. TRANSLATORS: We verified the metadata against the server msgid "Trusted metadata" msgstr "Metadatos confiados" #. TRANSLATORS: We verified the payload against the server msgid "Trusted payload" msgstr "Carga confiada" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Tipo" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI Bootservice Variables" msgstr "Variables del servicio de arranque de UEFI" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition may not be set up correctly" msgstr "La partición ESP de la UEFI puede que no esté correctamente configurada" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "Partición ESP UEFI no detectada o configurada" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "Llave de Plataforma UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "Arranque Seguro UEFI" #. TRANSLATORS: longer description msgid "UEFI Secure Boot prevents malicious software from being loaded when the device starts." msgstr "UEFI Secure Boot previene software malicioso sea cargado cuando el dispositivo de inicie." #. TRANSLATORS: longer description msgid "UEFI boot service variables should not be readable from runtime mode." msgstr "Variables del servicio de arranque UEFI no sería legible desde modo en tiempo de ejecución." #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI bootservice variables" msgstr "Variables de servicio de arranque UEFI" #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "Actualizaciones de cápsulas UEFI no disponible o activada en configuración de firmware" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "Utilidad dbx UEFI" #. TRANSLATORS: system is not booted in UEFI mode msgid "UEFI firmware can not be updated in legacy BIOS mode" msgstr "Firmware UEFI no puede ser actualizado en modo de BIOS heredada" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "Clave UEFI de la plataforma" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "Arranque seguro UEFI" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "Imposible conectar al servicio" #. TRANSLATORS: error message msgid "Unable to find attribute" msgstr "No es capaz de encontrar un atributo" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "Desvincula controlador actual" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "Desbloquear firmware:" #. TRANSLATORS: command description msgid "Unblocks a specific firmware from being installed" msgstr "Desbloquea un firmware específico para que sea instalado" #. TRANSLATORS: command description msgid "Undo the host security attribute fix" msgstr "Deshace la reparación del atributo de seguridad del hospedaje" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "Descifrado" #. TRANSLATORS: command description msgid "Uninhibit the system to allow upgrades" msgstr "Desinhibe el sistema para permitir actualizaciones" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Desconocido/a" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Dispositivo Desconocido" msgid "Unlock the device to allow access" msgstr "Desbloquea el dispositivo para permitir acceso" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "Desbloqueado" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "Desbloquea el dispositivo para acceso al firmware" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "Desmontajes de ESP" #. TRANSLATORS: message shown after device has been marked for emulation msgid "Unplug and replug the device to continue the update process." msgstr "Desenchufe y enchufe de nuevo el dispositivo para continuar el proceso de actualización." #. TRANSLATORS: firmware payload is unsigned and it is possible to modify it msgid "Unsigned Payload" msgstr "Carga No Firmada" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "Versión %s no mantenida del demonio, versión de cliente es %s" #. TRANSLATORS: Suffix: the HSI result msgid "Untainted" msgstr "No contaminado" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "Actualizable" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Actualizar Error" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "Actualizar Imagen" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "Actualizar Mensaje" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "Actualizar Estado" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "Error de actualización es un problema conocido, visite este URL para más información:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "¿Actualizar ahora?" #. TRANSLATORS: command description msgid "Update the stored cryptographic hash with current ROM contents" msgstr "Actualiza el hash criptográfico almacenado con el contenido de la actual ROM" msgid "Update the stored device verification information" msgstr "Actualizar la información de verificación del dispositivo almacenada" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "Actualiza los metadatos almacenados con contenido actual" #. TRANSLATORS: command description msgid "Updates all specified devices to latest firmware version, or all devices if unspecified" msgstr "Actualiza todos los dispositivos especificados a la última versión del firmware, o todos los dispositivos si no especificados" #. TRANSLATORS: how many local devices can expect updates now #, c-format msgid "Updates have been published for %u local device" msgid_plural "Updates have been published for %u of %u local devices" msgstr[0] "Ha sido publicada %u actualización de dispositivo local" msgstr[1] "Han sido publicadas %u actualizaciones de %u dispositivos locales" msgstr[2] "Han sido publicadas %u actualizaciones de %u dispositivos locales" msgid "Updating" msgstr "Actualizando" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "Actualizando %s…" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "¿Moderniza %s desde %s hasta %s?" #. TRANSLATORS: ask the user to upload msgid "Upload data now?" msgstr "¿Subir los datos ahora?" #. TRANSLATORS: command description msgid "Upload the list of updatable devices to a remote server" msgstr "Subir el listado de dispositivos actualizables a un servidor remoto" #. TRANSLATORS: ask the user to share, %s is something #. * like: "Linux Vendor Firmware Service" #, c-format msgid "Upload these anonymous results to the %s to help other users?" msgstr "¿Subo estos resultados anónimos al %s para ayudar a otros usuarios?" #. TRANSLATORS: explain why we want to upload #, c-format msgid "Uploading a device list allows the %s team to know what hardware exists, and allows us to put pressure on vendors that do not upload firmware updates for their hardware." msgstr "Cargar un listado de dispositivos permite que el equipo %s conozca qué hardware existe, y nos permite poner presión en los proveedores que no suban actualizaciones del firmware para su hardware." #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "Subiendo comunicados firmware ayuda proveedores de hardware para identificar rápidamente fallos y actualizaciones correctas en dispositivos reales." #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "Urgencia" #. TRANSLATORS: explain how to get help, %1 is #. * 'fwupdtool --help' #. TRANSLATORS: explain how to get help, #. * where $1 is something like 'fwupdmgr --help' #, c-format msgid "Use %s for help" msgstr "Utilice %s para ayuda" #. TRANSLATORS: CTRL^C [holding control, and then pressing C] will exit the #. program msgid "Use CTRL^C to cancel." msgstr "Utilice CTRL^C para cancelar." #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "El usuario ha sido notificado" #. TRANSLATORS: remote filename base msgid "Username" msgstr "Nombre del usuario" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "VERSION1 VERSION2 [FORMAT]" msgstr "VERSIÓN1 VERSIÓN2 [FORMATO]" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "Válido" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "Validación de contenidos ESP…" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "Variante" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "Proveedor" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "Verificando…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Versión" #. TRANSLATORS: the fwupd version the release was tested on msgid "Version[fwupd]" msgstr "Versión[fwupd]" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "AVISO:" #. TRANSLATORS: command description msgid "Wait for a device to appear" msgstr "Espera que aparezca un dispositivo" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "Esperando…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "Vigía para cambios del hardware" #. TRANSLATORS: check various UEFI and ACPI tables are unchanged after the #. update msgid "Will measure elements of system integrity around an update" msgstr "Medirá elementos del integridad del sistema relativo a una actualización" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "Escribiendo archivo:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Escribiendo…" #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from a recovery or installation disk, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "Aseguraría estar confortable restaurando las opciones desde un disco de recuperación o instalación, como esto cambia puede causar que el sistema no arranque en Linux o cause otra inestabilidad del sistema." #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from the system firmware setup, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "Aseguraría que sea confortable restaurando los parámetros desde la configuración del firmware del sistema, como este cambio pueda causar que el sistema no arranque en Linux o causar otra inestabilidad del sistema." #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "Su distribuidor tal vez no ha verificado ninguna de las actualizaciones de firmware para compatibilidad con su sistema o dispositivos conectados." #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "Su hardware tal vez está dañado utilizando este firmware, e instalando esta publicación tal vez anula cualquier garantía con %s." #. TRANSLATORS: BKC is the industry name for the best known configuration and #. is a set #. * of firmware that works together #, c-format msgid "Your system is set up to the BKC of %s." msgstr "Su sistema está actualizado a la BKC de %s." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[APPSTREAM_ID]" msgstr "[APPSTREAM_ID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[COMPROBANTE-SUMATORIO]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[ID-DISPOSITIVO|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[ID-DISPOSITIVO|GUID] [RAMIFICACIÓN]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [VERSION]" msgstr "[ID-DISPOSITIVO|GUID] [VERSIÓN]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE]" msgstr "[DISPOSITIVO]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILE FILE_SIG REMOTE-ID]" msgstr "[ARCHIVO FIRMA_ARCH ID-REMOTO]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[ARCHIVO-NOMBRE1] [ARCHIVO-NOMBRE2]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SECTION] KEY VALUE" msgstr "[SECCIÓN] CLAVE VALOR" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2] [--no-authenticate]" msgstr "[PARÃMETRO1] [PARÃMETRO2] [--no-authenticate]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2]..." msgstr "[OPCIÓN1] [OPCIÓN2]…" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[ARCHIVO-SMBIOS|ARCHIVO-HWIDS]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "predeterminado" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "evento TPM de fwupd para utilidad del boletín" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "complementos fwpd" fwupd-2.0.10/po/eu.po000066400000000000000000000620721501337203100143100ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # assar , 2017,2023-2024 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Basque (http://app.transifex.com/freedesktop/fwupd/language/eu/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: eu\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "Minutu%.0f falta da" msgstr[1] "%.0f minutu falta dira" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "%s bertsioa" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(zaharkituta)" #. TRANSLATORS: command description msgid "Activate devices" msgstr "Aktibatu gailua" msgid "Activate the new firmware on the device" msgstr "Aktibatu firmware berria gailuan" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Adina" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "Onartu eta gaitu urrunekoa?" #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Erantzun bai galdera guztiei" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Aplikatu eguneratzea hori egitea gomendatzen ez bada ere" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Aplikatu eguneratze-fitxategiak" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "Eguneratzea aplikatzen…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Onartutako firmwarea:" msgstr[1] "Onartutako firmwarea:" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Autentifikatzen…" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Autentifikazioa behar da gailu aldagarri bateko firmwarearen bertsio zaharragoa instalatzeko" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Autentifikazioa behar da makina honetako firmwarearen bertsio zaharragoa instalatzeko" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "Autentifikazioa behar da BIOS ezarpenak aldatzeko" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "Autentifikazioa behar da daemon-aren konfigurazioa aldatzeko" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "Autentifikazioa behar da BIOS ezarpenak irakurtzeko" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "Autentifikazioa behar da onartutako firmwarearen zerrenda ezartzeko" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "Autentifikazioa behar da datuak bezeroaren ziurtagiria erabiliz sinatzeko" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "Autentifikazioa behar da firmwarearen bertsio berrira aldatzeko" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Autentifikatu behar da gailu bat desblokeatzeko" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Autentifikazioa behar da gailu aldagarri bateko firmwarea eguneratzeko" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Autentifikazioa behar da makina honetako firmwarea eguneratzeko" #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "Automatikoki eguneratu guztietan?" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "LVFS edo Windows Update bidez banatutako BIOS eguneratzeak" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "Bateria" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "Blokeatutako firmware-fitxategiak:" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "Firmwarea blokeatzen:" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "Adarra" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Utzi" #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Aldatua" #. TRANSLATORS: get interactive prompt, where branch is the #. * supplier of the firmware, e.g. "non-free" or "free" msgid "Choose branch" msgstr "Aukeratu adarra" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "Aukeratu firmwarea" #. TRANSLATORS: get interactive prompt msgid "Choose release" msgstr "Aukeratu argitalpena" #. TRANSLATORS: get interactive prompt msgid "Choose volume" msgstr "Aukeratu bolumena" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "Azken eguneraketako emaitzak garbitzen ditu" #. TRANSLATORS: error message msgid "Command not found" msgstr "Ez da komandoa aurkitu" #. TRANSLATORS: when the update was built msgid "Created" msgstr "Sortze-data" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "Kritikoa" #. TRANSLATORS: current value of a BIOS setting msgid "Current Value" msgstr "Uneko balioa" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "Uneko bertsioa" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Arazketa-aukerak" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "Deskonprimatzen…" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Deskribapena" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "Xehetasunak" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "Gailuaren IDa" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Gailua gehitu da:" #. TRANSLATORS: the device is already connected msgid "Device already exists" msgstr "Gailua badago lehendik" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Gailua aldatu da:" #. TRANSLATORS: device cannot be interrupted, for instance taking a phone call msgid "Device is in use" msgstr "Gailua erabiltzen ari da" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "Gailua blokeatuta dago" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Gailua kendu da:" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "Ongi eguneratu diren gailuak:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "Ongi eguneratu ez diren gailuak:" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "Desgaituta" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "Emandako urruneko bat desgaitzen du" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "Ez egiaztatu metadatu zaharrak" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "Ez egiaztatu jakinarazi ez den historia" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "Ez egiaztatu urruneko deskargak gaitu behar diren ala ez" #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "Ez idatzi historiaren datu-basean" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "Urruneko hau orain freskatu nahi al duzu?" #. TRANSLATORS: success msgid "Done!" msgstr "Egina!" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Deskargatzen…" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "Iraupena" msgid "Enable" msgstr "Gaitu" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "Gaitu urruneko berria?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "Gaitu urruneko hau?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Gaituta" #. TRANSLATORS: Plugin is active only if hardware is found msgid "Enabled if hardware matches" msgstr "Gaituta hardwarea bat badator" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "Emandako urruneko bat gaitzen du" #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Zifratuta" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Ezabatzen…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "Irten atzerapen txiki baten ondoren" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "Irten motorra kargatu ondoren" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "FITXATEGI-IZENA" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "Hutsegitea" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Eguneratzearen aplikazioak huts egin du" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Argumentuen analisiak huts egin du" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "Huts egin du fitxategiaren analisiak" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "Huts egin du dbx lokala analizatzeak" #. TRANSLATORS: item is FALSE msgid "False" msgstr "Faltsua" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Fitxategi-izena" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Fitxategi-izena behar da" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "Firmwarea jadanik blokeatuta dago" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "Firmwarea ez dago jadanik blokeatuta" #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "Firmware-eguneratzeak" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "Banderak" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Aurkitua" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUIDa" msgid "Get BIOS settings" msgstr "Eskuratu BIOSaren ezarpenak" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "Onartutako firmwarearen zerrenda eskuratzen du" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Azken eguneraketako emaitzak eskuratzen ditu" #. TRANSLATORS: the release urgency msgid "High" msgstr "Altua" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "Inaktibo…" #. TRANSLATORS: show the user a generic image that can be themed msgid "Image" msgstr "Irudia" #. TRANSLATORS: show the user a random image from the internet msgid "Image (custom)" msgstr "Irudia (pertsonalizatua)" msgid "Install old version of signed system firmware" msgstr "Instalatu sinatutako sistema-firmwarearen bertsio zaharra" msgid "Install old version of unsigned system firmware" msgstr "Instalatu sinatu gabeko sistema-firmwarearen bertsio zaharra" msgid "Install signed device firmware" msgstr "Instalatu sinatutako gailu-firmwarea" msgid "Install signed system firmware" msgstr "Instalatu sinatutako sistema-firmwarea" msgid "Install unsigned device firmware" msgstr "Instalatu sinatu gabeko gailu-firmwarea" msgid "Install unsigned system firmware" msgstr "Instalatu sinatu gabeko sistema-firmwarea" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "Firmwarearen eguneratzea instalatzen..." #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "Barneko gailua" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "Baliogabea" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "Argumentu baliogabeak" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "Arazoa" msgstr[1] "Arazoak" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "Minutu bat baino gutxiago falta da" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "Lizentzia" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "Linux trukatze-espazioa" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Linux kernela" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Linux kernelaren blokeoa" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Linux trukatze-espazioa" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "ESPko fitxategiak zerrendatzen ditu" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "Kargatzen…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "Blokeatuta" #. TRANSLATORS: the release urgency msgid "Low" msgstr "Baxua" msgid "MEI version" msgstr "MEI bertsioa" #. TRANSLATORS: Longest valid string for BIOS setting msgid "Maximum length" msgstr "Luzera maximoa" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "Balio maximoa" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "Ertaina" #. TRANSLATORS: ask the user to do a simple task which should be translated msgid "Message" msgstr "Mezua" #. TRANSLATORS: ask the user a question, and it will not be translated msgid "Message (custom)" msgstr "Mezua (pertsonalizatua)" #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "Bertsio minimoa" #. TRANSLATORS: Shortest valid string for BIOS setting msgid "Minimum length" msgstr "Luzera minimoa" #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "Balio minimoa" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "Emandako urruneko bat aldatzen du" msgid "Modify a configured remote" msgstr "Aldatu konfiguratutako urruneko bat" msgid "Modify daemon configuration" msgstr "Aldatu daemon-aren konfigurazioa" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "ESPa muntatzen du" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "Berrabiarazi behar da" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "Bertsio berria" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "Ez da ekintzarik zehaztu" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "Ez da firmwarearen IDrik aurkitu" #. TRANSLATORS: nothing found msgid "No firmware found" msgstr "Ez da firmwarerik aurkitu" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "Ez dago argitalpenik eskuragarri" #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "Ez dago urrunekorik erabilgarri" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "Ez dago eguneratu daitekeen gailurik" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "Ez dago eguneraketarik erabilgarri" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "Ez da aurkitu" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "Ados" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "Ados" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "dbx eguneratzea analizatzen…" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "Sistemaren dx-a analizatzen…" #. TRANSLATORS: remote filename base msgid "Password" msgstr "Pasahitza" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "Zain" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "Gauzatu eragiketa?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "Plataforma-arazketa" #. TRANSLATORS: Possible values for a bios setting msgid "Possible Values" msgstr "Balio posibleak" #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "Aurreko bertsioa" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Lehentasuna" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "Arazoak" msgid "Proceed with upload?" msgstr "Jarraitu kargarekin?" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "Jabeduna" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "Irakurtzeko soilik" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Irakurtzen…" #. TRANSLATORS: how often we should refresh the metadata msgid "Refresh Interval" msgstr "Freskatze-tartea" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Interneteko konexioa behar du" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Berrabiarazi orain?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "Berrabiarazi daemon-a aldaketa gauzatzeko?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Gailua berrabiarazten…" #. TRANSLATORS: ask the user to upload msgid "Review and upload report now?" msgstr "Berrikusi eta kargatu txostena orain?" #. TRANSLATORS: Scalar increment for integer BIOS setting msgid "Scalar Increment" msgstr "Gehikuntza eskalarra" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Programatzen…" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Hautatutako bolumena" #. TRANSLATORS: command description msgid "Set a BIOS setting" msgstr "Ezarri BIOS ezarpen bat" msgid "Set one or more BIOS settings" msgstr "Ezarri BIOS ezarpen bat edo batzuk" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Onartutako firmwarearen zerrenda ezartzen du" #. TRANSLATORS: type of BIOS setting msgid "Setting type" msgstr "Ezarpen mota" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "Ezarpenak sistema berrabiarazi ondoren aplikatuko dira" #. TRANSLATORS: command line option msgid "Show all results" msgstr "Erakutsi emaitza guztiak" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Erakutsi arazketa-aukerak" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Erakutsi eguneragarriak ez diren gailuak" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Erakutsi arazketa-informazio gehigarria" msgid "Sign data using the client certificate" msgstr "Sinatu datuak bezeroaren ziurtagiria erabiliz" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "Sinatu datuak bezeroaren ziurtagiria erabiliz" msgid "Signature" msgstr "Sinadura" #. TRANSLATORS: file size of the download msgid "Size" msgstr "Tamaina" #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "Iturburua" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "Zehaztu dbx datu-base fitxategia" #. TRANSLATORS: The BIOS setting accepts strings msgid "String" msgstr "Katea" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "Arrakasta" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "Gailu guztiak ongi aktibatu dira" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "Urrunekoa ongi desgaitu da" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "Metadatu berriak ongi deskargatu dira:" #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "Urrunekoa ongi gaitu eta freskatu da" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "Urrunekoa ongi gaitu da" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "Firmwarea ongi instalatu da" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "Konfigurazio-balioa ongi aldatu da" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "Urrunekoa ongi aldatu da" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "Metadatuak ongi freskatu dira eskuz" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Laburpena" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "Onartutako PUZa" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "Sistemak kanpoko energia-iturria behar du" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "Etiketa" msgstr[1] "Etiketak" #. show the user the entire data blob msgid "Target" msgstr "Helburua" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "Ez dago onartutako firmwarerik." #. TRANSLATORS: item is TRUE msgid "True" msgstr "Egia" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Mota" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "Ezin da zerbitzuarekin konektatu" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "Firmwarea desblokeatzen:" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "Zifratu gabea" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Ezezaguna" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Gailu ezezaguna" msgid "Unlock the device to allow access" msgstr "Desblokeatu gailua sarbidea ahalbidetzeko" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "Desblokeatuta" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "ESPa desmuntatzen du" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "Eguneragarria" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Errorea eguneratzean" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Eguneratu orain?" msgid "Updating" msgstr "Eguneratzen" #. TRANSLATORS: CTRL^C [holding control, and then pressing C] will exit the #. program msgid "Use CTRL^C to cancel." msgstr "Erabili CTRL^C bertan behera uzteko." #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "Erabiltzaileari jakinarazpena bidali zaio" #. TRANSLATORS: remote filename base msgid "Username" msgstr "Erabiltzaile-izena" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "Baliozkoa" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "Aldaera" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "Hornitzailea" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "Egiaztatzen…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Bertsioa" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "ABISUA" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "Itxaroten…" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Idazten…" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "lehenetsia" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "fwupd pluginak" fwupd-2.0.10/po/fi.po000066400000000000000000003674031501337203100143030ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Jiri Grönroos , 2017-2018,2020-2022 # Kimmo Kujansuu , 2019-2022,2024-2025 # Ricky Tigg, 2024 # Timo Jyrinki , 2021 # Ville-Pekka Vainio , 2021,2023 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Finnish (http://app.transifex.com/freedesktop/fwupd/language/fi/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: fi\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "%.0f minuutti jäljellä" msgstr[1] "%.0f minuuttia jäljellä" #. TRANSLATORS: BMC refers to baseboard management controller which #. * is the device that updates all the other firmware on the system #, c-format msgid "%s BMC Update" msgstr "BMC:n %s päivitys" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "Laitteen %s akkupäivitys" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "Laitteen %s suorittimen mikrokoodipäivitys" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "Laitteen %s kamerapäivitys" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "Laitteen %s kokoonpanon päivitys" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "Laitteen %s kuluttajan ME-päivitys" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "\"%s\"-ohjaimen päivitys" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "Laitteen %s yrityksen ME-päivitys" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "Laitteen %s laitepäivitys" #. TRANSLATORS: Video Display refers to the laptop internal display or #. * external monitor #, c-format msgid "%s Display Update" msgstr "Näytön %s päivitys" #. TRANSLATORS: Dock refers to the port replicator hardware laptops are #. * cradled in, or lowered onto #, c-format msgid "%s Dock Update" msgstr "Telakan %s päivitys" #. TRANSLATORS: drive refers to a storage device, e.g. SATA disk #, c-format msgid "%s Drive Update" msgstr "Levyn %s päivitys" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "Laitteen %s sulautetun ohjaimen päivitys" #. TRANSLATORS: a device that can read your fingerprint pattern #, c-format msgid "%s Fingerprint Reader Update" msgstr "Sormenjälkilukijan %s päivitys" #. TRANSLATORS: flash refers to solid state storage, e.g. UFS or eMMC #, c-format msgid "%s Flash Drive Update" msgstr "Flash-levyn %s päivitys" #. TRANSLATORS: GPU refers to a Graphics Processing Unit, e.g. #. * the "video card" #, c-format msgid "%s GPU Update" msgstr "Näytönohjaimen %s päivitys" #. TRANSLATORS: a large pressure-sensitive drawing area typically used #. * by artists and digital artists #, c-format msgid "%s Graphics Tablet Update" msgstr "Piirtopöydän %s päivitys" #. TRANSLATORS: two miniature speakers attached to your ears #, c-format msgid "%s Headphones Update" msgstr "%s kuulokkeiden päivitys" #. TRANSLATORS: headphones with an integrated microphone #, c-format msgid "%s Headset Update" msgstr "%s kuulokemikrofoni päivitys" #. TRANSLATORS: an input device used by gamers, e.g. a joystick #, c-format msgid "%s Input Controller Update" msgstr "Laitteen %s ohjaimen päivitys" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "Näppäimistön %s päivitys" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "Laitteen %s ME-päivitys" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "Hiiren %s päivitys" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "â€%s†-verkkolaitteen päivitys" #. TRANSLATORS: SSD refers to a Solid State Drive, e.g. non-rotating #. * SATA or NVMe disk #, c-format msgid "%s SSD Update" msgstr "SSD:n %s päivitys" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "â€%s†-tallennustilaohjaimen päivitys" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "Laitteen %s järjestelmäpäivitys" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "Laitteen %s TPM-päivitys" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "Laitteen %s Thunderbolt-ohjaimen päivitys" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "Laitteen %s kosketuslevypäivitys" #. TRANSLATORS: Dock refers to the port replicator device connected #. * by plugging in a USB cable -- which may or may not also provide power #, c-format msgid "%s USB Dock Update" msgstr "USB-telakan %s päivitys" #. TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth #. * device that stays in the USB port so the wireless peripheral works #, c-format msgid "%s USB Receiver Update" msgstr "USB-vastaanottimen %s päivitys" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "Laitteen %s päivitys" #. TRANSLATORS: warn the user before updating, %1 is a device name #, c-format msgid "%s and all connected devices may not be usable while updating." msgstr "%s ja kaikki kytketyt laitteet eivät välttämättä ole käyttökelpoisia päivityksen aikana." #. TRANSLATORS: %1 refers to some kind of security test, e.g. "Encrypted RAM". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s appeared: %s" msgstr "%s näkyy: %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "UEFI platform #. key". #. * %2 and %3 refer to results value, e.g. "Valid" and "Invalid" #, c-format msgid "%s changed: %s → %s" msgstr "%s muutettu: %s → %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "SPI BIOS #. region". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s disappeared: %s" msgstr "%s kadonnut: %s" #. TRANSLATORS: the device has a reason it can't update, e.g. laptop lid #. closed #, c-format msgid "%s is not currently updatable" msgstr "%s ei ole tällä hetkellä päivitettävissä" #. TRANSLATORS: %1 is a device name, e.g. "ThinkPad Universal #. * ThunderBolt 4 Dock" and %2 is "fwupdmgr activate" #, c-format msgid "%s is pending activation; use %s to complete the update." msgstr "%s odottaa aktivointia; viimeistele päivitys painamalla %s." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "%s-valmistustila" #. TRANSLATORS: warn the user before #. * updating, %1 is a device name #, c-format msgid "%s must remain connected for the duration of the update to avoid damage." msgstr "%s on oltava kytkettynä päivityksen ajaksi vaurioiden välttämiseksi." #. TRANSLATORS: warn the user before updating, %1 is a machine #. * name #, c-format msgid "%s must remain plugged into a power source for the duration of the update to avoid damage." msgstr "%s on oltava kytkettynä virtalähteeseen päivityksen ajaksi vaurioiden välttämiseksi." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "%s ohita" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v. %s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "%s-versio" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u päivä" msgstr[1] "%u päivää" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "%u laitteille on saatavilla laiteohjelmistopäivitys." msgstr[1] "%u laitteille on saatavilla laiteohjelmistopäivitys." #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device is not the best known configuration." msgid_plural "%u devices are not the best known configuration." msgstr[0] "%u laitteet eivät ole tunnetuin kokoonpano." msgstr[1] "%u laitetta eivät ole tunnetuin kokoonpano." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u tunti" msgstr[1] "%u tuntia" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minuutti" msgstr[1] "%u minuuttia" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u sekunti" msgstr[1] "%u sekuntia" #. TRANSLATORS: device tests can be specific to a CPU type #, c-format msgid "%u test was skipped" msgid_plural "%u tests were skipped" msgstr[0] "%u testit ohitettiin" msgstr[1] "%u testit ohitettiin" #. TRANSLATORS: first percentage is current value, 2nd percentage is the #. * lowest limit the firmware update is allowed for the update to happen #, c-format msgid "%u%% (threshold %u%%)" msgstr "%u%% (tarvitaan %u%%)" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(vanhentunut)" #. TRANSLATORS: HSI event title msgid "A TPM PCR is now an invalid value" msgstr "TPM PCR on nyt virheellisellä arvolla" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "AMD Firmware Replay Protection" msgstr "AMD Firmware Replay Protection" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "AMD Firmware Write Protection" msgstr "AMD Firmware Write Protection" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "AMD Secure Processor Rollback Protection" msgstr "AMD Secure Processor Rollback Protection" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "ARCHIVE FIRMWARE METAINFO [FIRMWARE] [METAINFO] [JCATFILE]" msgstr "ARKISTO LAITEOHJELMISTO METATIETO [LAITEOHJELMISTO] [METATIETO] [JCAT-TIEDOSTO]" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "Toimia vaaditaan:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "Aktivoi laitteet" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "Aktivoi odottavat laitteet" msgid "Activate the new firmware on the device" msgstr "Aktivoi laitteessa oleva uusi laiteohjelmisto" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "Laiteohjelmistopäivityksen aktivointi" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "Käynnistetään laiteohjelmiston päivitys" #. TRANSLATORS: command description msgid "Adds devices to watch for future emulation" msgstr "Lisää katsottavia laitteita tulevaa emulointia varten" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Ikä" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "Hyväksytäänkö ja otetaanko lähde käyttöön?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Toinen nimi komennolle %s" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are now valid" msgstr "Kaikki TPM PCR:t ovat nyt kelvollisia" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are valid" msgstr "Kaikki TPM PCR:t ovat kelvollisia" #. TRANSLATORS: an application is preventing system updates msgid "All devices are prevented from update by system inhibit" msgstr "Järjestelmän esto estää kaikkia laitteita päivittämästä" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "Kaikki samantyyppiset laitteet päivitetään samaan aikaan" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Salli laiteohjelmistoversioiden alentaminen" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "Salli laiteohjelmiston nykyisten versioiden asentaminen uudelleen" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "Salli laiteohjelmiston haaran vaihtaminen" #. TRANSLATORS: error message msgid "Already exists, and no --force specified" msgstr "On jo olemassa, eikä --force ominaisuutta määritettynä" #. TRANSLATORS: is not the main firmware stream msgid "Alternate branch" msgstr "Vaihtoehtoinen haara" #. TRANSLATORS: another application is updating the device already msgid "An update is in progress" msgstr "Päivitys on kesken" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "Päivitys vaatii uudelleenkäynnistyksen." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "Päivitys edellyttää järjestelmän sammuttamista." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Vastaa kaikkiin kysymyksiin kyllä" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Asenna päivitys myös silloin, kun sitä ei suositella" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Asenna päivitystiedostot" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "Asennetaan päivitystä..." #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Hyväksytty laiteohjelmisto:" msgstr[1] "Hyväksytty laiteohjelmisto:" #. TRANSLATORS: command description msgid "Asks the daemon to quit" msgstr "Pyydä taustaprosessia lopettamaan" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "Liity laiteohjelmistotilaan" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Tunnistaudutaan…" #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "Todennus vaaditaan" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Erillisen laitteen laiteohjelmiston version alentaminen vaatii tunnistautumisen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Tämän järjestelmän laiteohjelmiston version alentaminen vaatii tunnistautumisen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to enable emulation data collection" msgstr "Todennus vaaditaan laitteiston emulointitietojen keräämiseksi" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to fix a host security issue" msgstr "Host tietoturvaongelman korjaaminen edellyttää todennusta" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to load hardware emulation data" msgstr "Todennus vaaditaan laitteiston emulointitietojen lataamiseen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "BIOS-asetusten muuttaminen vaatii tunnistautumisen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "Laiteohjelmistojen päivityslähteen asetusten muokkaaminen vaatii tunnistautumisen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "Taustaprosessin kokoonpanon muokkaaminen vaatii tunnistautumisen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "BIOS-asetusten lukeminen vaatii tunnistautumisen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to reset daemon configuration to defaults" msgstr "Todennus vaaditaan taustaprosessin palauttamiseksi oletuksiin" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to save hardware emulation data" msgstr "Todennus vaaditaan laitteiston emulointitietojen tallentamiseen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "Hyväksyttyjen laiteohjelmistojen luettelon tallentaminen vaatii tunnistautumisen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "Tietojen allekirjoittaminen asiakaan sertifikaatin avulla vaatii tunnistautumisen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to stop the firmware update service" msgstr "Todennus vaaditaan päivityspalvelun pysäyttämiseksi" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "Uuden laiteohjelmistoversion käyttöönotto vaatii tunnistautumisen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to undo the fix for a host security issue" msgstr "Todennus vaaditaan, jotta host tietoturvaongelman korjaus voidaan kumota" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Laitteen lukituksen avaaminen vaatii tunnistautumisen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Erillisen laitteen laiteohjelmiston päivittäminen vaatii tunnistautumisen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Tämän järjestelmän laiteohjelmiston päivittäminen vaatii tunnistautumisen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "Tallennettujen tarkistussummien päivitys vaatii tunnistautumisen" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "Automaattinen raportointi" #. TRANSLATORS: we can auto-uninhibit after a timeout #, c-format msgid "Automatically uninhibiting in %ums…" msgstr "Automaattinen esto poistetaan %u ms…" #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "Lähetetäänkö automaattisesti joka kerta?" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS Firmware Updates" msgstr "BIOS Firmware Updates" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS Rollback Protection" msgstr "BIOS Rollback Protection" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS firmware updates" msgstr "BIOS-laiteohjelmistopäivitykset" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS rollback protection" msgstr "BIOS-versiopalautuksen suojaus" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "BIOS-päivitykset toimitetaan LVFS:n tai Windows Updaten kautta" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "BUILDER-XML FILENAME-DST" msgstr "LUONTI-XML TIEDOSTONIMI-KOHDE" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "Akku" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "Sido uusi kernel-ohjain" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "Estetyt laiteohjelmistotiedostot:" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "Estetty versio" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "Laiteohjelmiston estäminen:" #. TRANSLATORS: command description msgid "Blocks a specific firmware from being installed" msgstr "Estää tietyn laiteohjelmiston asentamisen" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "Käynnistyslataimen versio" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "Haara" #. TRANSLATORS: command description msgid "Build a cabinet archive from a firmware blob and XML metadata" msgstr "Rakenna cabinet-arkisto blob-laiteohjelmistosta ja XML-metatiedoista" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "Luo laiteohjelmistotiedosto" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * Utilized by OS means the distribution enabled it msgid "CET OS Support" msgstr "CET OS tuki" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * enabled means supported by the processor msgid "CET Platform" msgstr "CET Platform" #. TRANSLATORS: longer description msgid "CPU Microcode must be updated to mitigate against various information-disclosure security issues." msgstr "CPU-mikrokoodi on päivitettävä erilaisten tietojen paljastamisen turvaongelmien lieventämiseksi." #. TRANSLATORS: we can save all device enumeration events for emulation msgid "Can tag for emulation" msgstr "Voidaan merkitä emulointiin" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Peru" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "Peruttu" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "Ei voida asentaa, koska dbx-päivitys on jo asennettu." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Muutettu" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "Tarkistaa, että kryptografinen tiiviste vastaa laiteohjelmistoa" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Tarkistussumma" #. TRANSLATORS: get interactive prompt, where branch is the #. * supplier of the firmware, e.g. "non-free" or "free" msgid "Choose branch" msgstr "Valitse haara" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "Valitse laite" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "Valitse laiteohjelmisto" #. TRANSLATORS: get interactive prompt msgid "Choose release" msgstr "Valitse julkaisu" #. TRANSLATORS: get interactive prompt msgid "Choose volume" msgstr "Valitse asema" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "Tyhjennä viimeisimmän päivityksen tulokset" #. TRANSLATORS: error message msgid "Command not found" msgstr "Komentoa ei löytynyt" #. TRANSLATORS: is not supported by the vendor msgid "Community supported" msgstr "Yhteisön tukema" #. TRANSLATORS: command description msgid "Compares two versions for equality" msgstr "Vertailee kahta versiota rinnakkain" #. TRANSLATORS: title prefix for the BIOS settings dialog msgid "Configuration Change Suggested" msgstr "Ehdotetaan kokoonpanomuutosta" #. TRANSLATORS: no peeking msgid "Configuration is only readable by the system administrator" msgstr "Vain pääkäyttäjä voi lukea kokoonpanoa" #. TRANSLATORS: longer description msgid "Control-Flow Enforcement Technology detects and prevents certain methods for running malicious software on the device." msgstr "Ohjausvirtauksen valvontatekniikka havaitsee ja estää tietyt menetelmät haittaohjelmien suorittamiselle laitteella." #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology msgid "Control-flow Enforcement Technology" msgstr "Ohjaus-virtausvahvistustekniikka" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "Muunna laiteohjelmistotiedosto" #. TRANSLATORS: command description msgid "Create an EFI boot entry" msgstr "Luo EFI boot merkintä" #. TRANSLATORS: when the update was built msgid "Created" msgstr "Luotu" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "Kriittinen" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "Kryptografisen tiivisteen tarkistus on käytettävissä" #. TRANSLATORS: current value of a BIOS setting msgid "Current Value" msgstr "Nykyinen arvo" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "Nykyinen versio" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "LAITETUNNUS|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Vianjäljitysvalinnat" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "Puretaan…" #. TRANSLATORS: command description msgid "Delete an EFI boot entry" msgstr "Poista EFI boot merkintä" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Kuvaus" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "Irrota käynnistyslataimen tilaan" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "Tiedot" #. TRANSLATORS: the best known configuration is a set of software that we know #. works #. * well together. In the OEM and ODM industries it is often called a BKC msgid "Deviate from the best known configuration?" msgstr "Poiketaanko parhaasta tunnetusta kokoonpanosta?" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "Laitteen liput" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "Laitetunnus" #. TRANSLATORS: description of the device requests msgid "Device Requests" msgstr "Laitepyynnöt" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Laite lisätty:" #. TRANSLATORS: the device is already connected msgid "Device already exists" msgstr "Laite on jo olemassa." #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "Laitteen akun lataus on liian vähissä" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse #, c-format msgid "Device battery power is too low (%u%%, requires %u%%)" msgstr "Laitteen akussa on liian vähän virtaa (%u%%, tarvitaan %u%%)" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "Laite voi palautua asennuksen virheistä" #. TRANSLATORS: lid means "laptop top cover" msgid "Device cannot be updated while the lid is closed" msgstr "Laitetta ei voi päivittää, jos kansi on suljettu" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Laite muutettu:" #. TRANSLATORS: a version check is required for all firmware msgid "Device firmware is required to have a version check" msgstr "Laiteohjelmisto vaatii version tarkistamista" #. TRANSLATORS: emulated means we are pretending to be a different model msgid "Device is emulated" msgstr "Laite on emuloitu" #. TRANSLATORS: device cannot be interrupted, for instance taking a phone call msgid "Device is in use" msgstr "Laite on käytössä" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "Laite on lukittu" #. TRANSLATORS: we have two ways of communicating with the device, so we hide #. one msgid "Device is lower priority than an equivalent device" msgstr "Laitteen prioriteetti on alhaisempi kuin vastaava laite" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "Laite edellyttää asentamaan kaikki toimitetut versiot" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "Laite ei ole tavoitettavissa" #. TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode msgid "Device is unreachable, or out of wireless range" msgstr "Laitteeseen ei saada yhteyttä tai se on kantaman ulkopuolella" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "Laitetta voidaan käyttää päivityksen aikana" #. TRANSLATORS: usually this is when we're waiting for a reboot msgid "Device is waiting for the update to be applied" msgstr "Laite odottaa päivityksen toteuttamista" #. TRANSLATORS: success, so say thank you to the user msgid "Device list uploaded successfully, thanks!" msgstr "Laiteluettelo lähetetty, kiitos!" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Laite poistettu:" #. TRANSLATORS: as in, wired mains power for a laptop msgid "Device requires AC power to be connected" msgstr "Laite vaatii verkkovirran olevan kytketty" #. TRANSLATORS: device does not have a display connected msgid "Device requires a display to be plugged in" msgstr "Laite vaatii näytön kytkemisen" #. TRANSLATORS: The device cannot be updated due to missing vendor's license." msgid "Device requires a software license to update" msgstr "Laitteen päivitys tarvitsee ohjelmistolisenssin" #. TRANSLATORS: longer description msgid "Device software updates are provided for this device." msgstr "Tälle laitteelle tarjotaan ohjelmistopäivityksiä" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "Laite valmistelee päivitykset" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "Laite tukee vaihtamista toiseen laiteohjelmiston haaraan" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "Laitteen päivitys tarvitsee aktivointia" #. TRANSLATORS: save the old firmware to disk before installing the new one msgid "Device will backup firmware before installing" msgstr "Laite varmuuskopioi laiteohjelmiston ennen asennusta" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "Laitetta ei näytetä uudelleen päivityksen päätyttyä" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "Onnistuneesti päivitetyt laitteet:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "Laitteet, joita ei päivitetty oikein:" #. TRANSLATORS: message letting the user there is an update #. * waiting, but there is a reason it cannot be deployed msgid "Devices with firmware updates that need user action: " msgstr "Laitteet, joissa on firmware päivityksiä, jotka vaativat käyttäjän toimia:" #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "Laitteet, joille ei ole saatavilla laiteohjelmiston päivityksiä:" #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "Laitteet, joille on asennettu uusin versio laiteohjelmistosta:" #. TRANSLATORS: this is for the device tests msgid "Did not find any devices with matching GUIDs" msgstr "Laitteita ei löytynyt vastaavilla GUID-tunnuksilla" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "Ei käytössä" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "Poista annettu lähde" #. TRANSLATORS: command description msgid "Disables virtual testing devices" msgstr "Poistaa käytöstä virtuaaliset testauslaitteet" #. TRANSLATORS: the OS the release was tested on msgid "Distribution" msgstr "Jakelu" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "Älä tarkista metatietojen vanhentumista" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "Älä tarkista ilmoittamattomien historiaa" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "Älä tarkista, pitäisikö verkkolähteitä ottaa käyttöön" #. TRANSLATORS: command line option msgid "Do not check or prompt for reboot after update" msgstr "Älä pyydä tai tarkista uudelleenkäynnistystä päivityksen jälkeen" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "Älä sisällytä lokiin toimialueen etuliitettä" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "Älä sisällytä aikaleiman etuliitettä" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "Älä suorita laitteen turvallisuustarkastuksia" #. TRANSLATORS: command line option msgid "Do not prompt for devices" msgstr "Älä pyydä laitteita" #. TRANSLATORS: command line option msgid "Do not prompt to fix security issues" msgstr "Älä pyydä turvallisuusongelmien korjausta" #. TRANSLATORS: command line option msgid "Do not search the firmware when parsing" msgstr "Älä etsi laiteohjelmistoja samalla kun jäsennetään" #. TRANSLATORS: warning message shown after update has been scheduled msgid "Do not turn off your computer or remove the AC adaptor while the update is in progress." msgstr "Älä sammuta tietokonettasi äläkä irrota virtajohtoa kun päivitys on kesken." #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "Älä kirjoita historiatietokantaan" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "Ymmärrätkö laiteohjelmiston haaran vaihtamisen seuraukset?" #. TRANSLATORS: ask the user if it's okay to convert, #. * "it" being the data contained in the EFI boot entry msgid "Do you want to convert it now?" msgstr "Haluatko muuntaa sen heti?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "Haluatko poistaa tämän ominaisuuden käytöstä tulevia päivityksiä varten?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "Haluatko päivittää tämän lähteen nyt?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "Haluatko lähettää raportteja automaattisesti tulevia päivityksiä varten?" #. TRANSLATORS: command line option msgid "Don't prompt for authentication (less details may be shown)" msgstr "Älä pyydä todennusta (saatetaan näyttää vähemmän tietoja)" #. TRANSLATORS: success msgid "Done!" msgstr "Valmis!" #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "Alennetaanko laitteen %s laiteohjelmisto versiosta %s versioon %s?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "Alentaa laitteen laiteohjelmistoa" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "Alennetaan laitetta %s…" #. TRANSLATORS: command description msgid "Download a file" msgstr "Lataa tiedosto" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Ladataan…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "Pura SMBIOS-tiedot tiedostosta" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "Kesto" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "EMULATION-FILE [ARCHIVE-FILE]" msgstr "EMULATION-FILE [ARCHIVE-FILE]" #. TRANSLATORS: longer description msgid "Each system should have tests to ensure firmware security." msgstr "Jokaiselle järjestelmälle pitäisi olla testejä, jotka varmistavat laiteohjelmiston turvallisuuden." #. TRANSLATORS: command description msgid "Emulate a device using a JSON manifest" msgstr "Emuloi laitetta käyttäen JSON-manifestia" #. TRANSLATORS: this device is not actually real msgid "Emulated" msgstr "Emuloitu" #. TRANSLATORS: Title: if we are emulating a different host msgid "Emulated host" msgstr "Emuloitu kone" msgid "Enable" msgstr "Ota käyttöön" msgid "Enable emulation data collection" msgstr "Käytä emulointitietojen keräämistä" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "Otetaanko uusi lähde käyttöön?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "Otetaanko tämä lähde käyttöön?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Käytössä" #. TRANSLATORS: Plugin is active only if hardware is found msgid "Enabled if hardware matches" msgstr "Käytössä, jos laitteisto vastaa" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "Ota käyttöön annettu lähde" #. TRANSLATORS: command description msgid "Enables virtual testing devices" msgstr "Ottaa käyttöön virtuaaliset testauslaitteet" #. TRANSLATORS: longer description msgid "Enabling firmware updates for the BIOS allows fixing security issues." msgstr "BIOSin laiteohjelmistopäivitysten ottaminen käyttöön mahdollistaa tietoturvaongelmien korjaamisen." msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "Tämän toiminnon käyttöönotto tapahtuu omalla vastuullasi, joten sinun on otettava yhteyttä alkuperäiseen laitevalmistajaan näiden päivitysten aiheuttamista ongelmista. Vain päivitysprosessiin liittyvät ongelmat pitäisi ilmoittaa osoitteeseen $OS_RELEASE:BUG_REPORT_URL$." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "Tämän lähteen käyttöönotto tapahtuu omalla vastuullasi." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Salattu" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "Salattu RAM-muisti" msgid "Encrypted RAM makes it impossible for information that is stored in device memory to be read if the memory chip is removed and accessed." msgstr "Kun keskusmuisti on salatttu, laitteen muistiin tallennettua tietoa ei voi lukea, vaikka muistisiru poistettaisiin ja sitä käytettäisiin." #. TRANSLATORS: the vendor is no longer supporting the device msgid "End of life" msgstr "Käyttöaika loppu" #. TRANSLATORS: The BIOS setting can only be changed to fixed values msgid "Enumeration" msgstr "Enumeraatio" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "Poista kaikki laiteohjelmiston päivityshistoria" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Poistetaan…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "Poistu pienen viiveen jälkeen" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "Poistu kun moottori on ladattu" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "Vie laiteohjelmiston tiedostorakenne XML-muotoon" #. TRANSLATORS: command description msgid "Export firmware history for manual upload" msgstr "Vie laiteohjelmistohistoria manuaalista lähetystä varten" #. TRANSLATORS: command description msgid "Extract a firmware blob to images" msgstr "Pura blob-laiteohjelmiston levykuviksi" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "TIEDOSTO" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "TIEDOSTO [LAITETUNNUS|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "TIEDOSTONIMI" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "TIEDOSTONIMI SERTIFIKAATTI YKSITYINEN AVAIN" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "TIEDOSTONIMI LAITETUNNUS" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME OFFSET DATA [FIRMWARE-TYPE]" msgstr "TIEDOSTONIMI SIIRTYMÄ DATA [LAITEOHJELMISTON-TYYPPI]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "TIEDOSTONIMI [LAITETUNNUS|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "TIEDOSTONIMI [LAITEOHJELMISTON-TYYPPI]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" msgstr "TIEDOSTONIMI-LÄHDE TIEDOSTONIMI-KOHDE [LAITEOHJELMISTON-TYYPPI-LÄHDE] [LAITEOHJELMISTON-TYYPPI-KOHDE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" msgstr "FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "Epäonnistui" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Päivityksen asentaminen epäonnistui" #. TRANSLATORS: error message for Windows msgid "Failed to connect to Windows service, please ensure it's running." msgstr "Windows-palveluun yhdistäminen epäonnistui. Varmista, että se on käynnissä." #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "Yhteys taustaprosessiin epäonnistui" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "Paikallisen dbx-tiedoston lataus epäonnistui" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "Järjestelmän dbx-tiedoston lataus epäonnistui" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "Lukitseminen epäonnistui" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Parametrien jäsentäminen epäonnistui" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "Tiedoston jäsentäminen epäonnistui" #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #, c-format msgid "Failed to parse flags for %s" msgstr "Kohteen %s lippujen jäsentäminen epäonnistui" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "Paikallisen dbx-tiedoston lukeminen epäonnistui" #. TRANSLATORS: a feature is something like "can show an image" msgid "Failed to set front-end features" msgstr "Käyttöliittymän ominaisuuksien asettaminen epäonnistui" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "ESP:n sisällön vahvistaminen epäonnistui" #. TRANSLATORS: item is FALSE msgid "False" msgstr "Epätosi" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Tiedostonimi" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "Tiedoston allekirjoitus" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "Tiedostonimen lähde" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Tiedostonimi vaaditaan" #. TRANSLATORS: command line option msgid "Filter with a set of device flags using a ~ prefix to exclude, e.g. 'internal,~needs-reboot'" msgstr "Suodata käyttäen laitelippuja. ~ poissulkee, esimerkiksi â€internal,~needs-rebootâ€" #. TRANSLATORS: command line option msgid "Filter with a set of release flags using a ~ prefix to exclude, e.g. 'trusted-release,~trusted-metadata'" msgstr "Suodata käyttäen julkaisulippuja. ~ poissulkee, esimerkiksi â€trusted-release,~trusted-metadataâ€" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware Attestation" msgstr "Laiteohjelmiston vahvistus" #. TRANSLATORS: longer description msgid "Firmware Attestation checks device software using a reference copy, to make sure that it has not been changed." msgstr "Laiteohjelmiston vahvistaminen tarkastaa, että laiteohjelmisto käyttää suositeltua versiota, jotta voidaan olla varmoja että ohjelmistoa ei ole muokattu." #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "Laiteohjelmiston BIOS-tunnus" #. TRANSLATORS: longer description msgid "Firmware BIOS Descriptor protects device firmware memory from being tampered with." msgstr "Laiteohjelmiston BIOS-kuvain estää laiteohjelmiston muistin peukaloimisen." #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "Laiteohjelmiston BIOS-alue" #. TRANSLATORS: longer description msgid "Firmware BIOS Region protects device firmware memory from being tampered with." msgstr "Laiteohjelmiston BIOS-alue estää laiteohjelmiston muistin peukaloimisen." #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "Firmware pohja URI" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "Laiteohjelmistopäivityksen D-Bus-palvelu" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Laiteohjelmistopäivityksen taustaprosessi" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "Laiteohjelmistopäivittimen varmennus" msgid "Firmware Updater Verification checks that software used for updating has not been tampered with." msgstr "Laiteohjelmistopäivittimen varmennus varmentaa, että päivittämiseen käytettyä ohjelmistoa ei ole peukaloitu." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "Laiteohjelmistopäivitykset" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "Laiteohjelmistotyökalu" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "Laiteohjelmiston kirjoitussuojaus" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "Laiteohjelman kirjoitussuojauksen lukko" #. TRANSLATORS: longer description msgid "Firmware Write Protection protects device firmware memory from being tampered with." msgstr "Laiteohjelmiston kirjoitussuojaus estää laiteohjelmiston muistin peukaloimisen." #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "Laiteohjelmiston vahvistaminen" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "Laiteohjelmisto on jo estetty" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "Laiteohjelmistoa ei ole jo estetty" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "Laiteohjelmiston metatietoja ei ole päivitetty %upäivään ja ne eivät välttämättä ole ajan tasalla." msgstr[1] "Laiteohjelmiston metatietoja ei ole päivitetty %u päivään ja ne eivät välttämättä ole ajan tasalla." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "Laiteohjelmistopäivitykset" #. TRANSLATORS: user needs to run a command, %1 is 'fwupdmgr unlock' #, c-format msgid "Firmware updates disabled; run '%s' to enable" msgstr "Päivitykset poistettu käytöstä; ota käyttöön suorittamalla \"%s\"" #. TRANSLATORS: command description msgid "Fix a specific host security attribute" msgstr "Korjaa tietty isäntätietoturvaattribuutti" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fix reverted successfully" msgstr "Korjaus palautettu onnistuneesti" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fixed successfully" msgstr "Korjattu onnistuneesti" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "Liput" #. TRANSLATORS: command line option msgid "Force the action by relaxing some runtime checks" msgstr "Pakota toiminta helpottamalla joitakin ajonaikaisia tarkistuksia" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Löydetty" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "Levyn salaus havaittu" #. TRANSLATORS: we might ask the user the recovery key when next booting #. Windows msgid "Full disk encryption secrets may be invalidated when updating" msgstr "Levyn salaus voi mitätöityä päivityksen aikana" #. TRANSLATORS: Title: if the part has been fused msgid "Fused Platform" msgstr "Fused Platform" #. TRANSLATORS: Title: if the part has been fused msgid "Fused platform" msgstr "Sulattu alusta" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUIDs" msgstr[1] "GUID:t" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "GUID|DEVICE-ID" msgstr "GUID|LAITETUNNUS" msgid "Get BIOS settings" msgstr "Hae BIOS-asetukset" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "Hae kaikki fwupd:n tukemat laiteliput" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Hae kaikki laiteohjelmistopäivityksiä tukevat laitteet" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "Hae kaikki järjestelmään rekisteröidyt laajennukset" #. TRANSLATORS: command description msgid "Get all known version formats" msgstr "Hae kaikki tunnetut versioformaatit" #. TRANSLATORS: command description msgid "Get device report metadata" msgstr "Hae laitteistoraportin metatiedot" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Hae tietoja laiteohjelmistotiedostosta" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "Hae konfiguroidut lähteet" #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "Hae koneen tietoturva-attribuutit" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "Hae hyväksytyn laiteohjelmiston luettelo" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "Hakee estettyjen laiteohjelmistojen luettelon" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Hae luettelo liitettyjen laitteiden päivityksistä" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "Hakee laitteen julkaisut" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Hakee viimeisimmän päivityksen tulokset" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "HWIDS-TIEDOSTO" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "Laitteisto odottaa uudelleenkytkemistä" #. TRANSLATORS: the release urgency msgid "High" msgstr "Korkea" #. TRANSLATORS: title for host security events msgid "Host Security Events" msgstr "Koneen suojaustapahtumat" #. TRANSLATORS: error message for unsupported feature #, c-format msgid "Host Security ID (HSI) is not supported" msgstr "Host Security ID (HSI) ei ole tuettu" #. TRANSLATORS: success, so say thank you to the user msgid "Host Security ID attributes uploaded successfully, thanks!" msgstr "Koneen turvatunnisteen (HSI) attribuuttien lähetys onnistui. Kiitos!" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "Koneen turvatunniste Host Security ID:" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX" msgstr "INDEX" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX KEY [VALUE]" msgstr "INDEX KEY [VALUE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX NAME TARGET [MOUNTPOINT]" msgstr "INDEX NAME TARGET [MOUNTPOINT]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX1,INDEX2" msgstr "INDEX1,INDEX2" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INHIBIT-ID" msgstr "ESTON-TUNNISTE" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "IOMMU-suojaus" #. TRANSLATORS: longer description msgid "IOMMU Protection prevents connected devices from accessing unauthorized parts of system memory." msgstr "IOMMU:n suojaus estää liitettyjä laitteita käyttämästä järjestelmämuistin yksityisiä osia." #. TRANSLATORS: HSI event title msgid "IOMMU device protection disabled" msgstr "IOMMU-laitteen suojaus poistettu" #. TRANSLATORS: HSI event title msgid "IOMMU device protection enabled" msgstr "IOMMU-laitteen suojaus käytössä" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "Jouten…" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "Ohita tiukat SSL-tarkistukset tiedostoja ladattaessa" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "Ohita laiteohjelmiston tarkistussumman virheet" #. TRANSLATORS: command line option msgid "Ignore firmware hardware mismatch failures" msgstr "Ohita laitteiden yhteensopimattomuuden virheet" #. TRANSLATORS: command line option msgid "Ignore non-critical firmware requirements" msgstr "Ohita ei-kriittiset firmware vaatimukset" #. TRANSLATORS: try to help msgid "Ignoring SSL strict checks, to do this automatically in the future export DISABLE_SSL_STRICT in your environment" msgstr "Ohitetaan tiukat SSL-tarkistukset. Jos haluat tehdä tämän jatkossa automaattisesti, ota käyttöön DISABLE_SSL_STRICT-ympäristömuuttuja" #. TRANSLATORS: show the user a generic image that can be themed msgid "Image" msgstr "Kuva" #. TRANSLATORS: show the user a random image from the internet msgid "Image (custom)" msgstr "Kuva (muokattu)" #. TRANSLATORS: the inhibit ID is a short string like dbus-123456 #, c-format msgid "Inhibit ID is %s." msgstr "Eston tunniste on %s." #. TRANSLATORS: command description msgid "Inhibit the system to prevent upgrades" msgstr "Ota käyttöön päivitysten estäminen" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "Asennuksen kesto" #. TRANSLATORS: command description msgid "Install a firmware file in cabinet format on this hardware" msgstr "Asenna cabinet-muotoinen laiteohjelmisto tähän laitteistoon" #. TRANSLATORS: command description msgid "Install a raw firmware blob on a device" msgstr "Asenna raaka blob-laiteohjelmisto laitteeseen" #. TRANSLATORS: command description msgid "Install a specific firmware file on all devices that match" msgstr "Asenna tietty laiteohjelmistotiedosto kaikkiin laitteisiin, joihin se sopii" #. TRANSLATORS: command description msgid "Install a specific firmware on a device, all possible devices will also be installed once the CAB matches" msgstr "Asenna tietty laiteohjelmisto laitteeseen. Asennetaan kaikkiin mahdollisiin laitteisiin, kun CAB täsmää." msgid "Install old version of signed system firmware" msgstr "Asenna järjestelmän laiteohjelmiston allekirjoitettu vanha versio" msgid "Install old version of unsigned system firmware" msgstr "Asenna järjestelmän laiteohjelmiston allekirjoittamaton vanha versio" msgid "Install signed device firmware" msgstr "Asenna allekirjoitettu laiteohjelmisto" msgid "Install signed system firmware" msgstr "Asenna allekirjoitettu firmware" msgid "Install unsigned device firmware" msgstr "Asenna allekirjoittamaton laiteohjelmisto" msgid "Install unsigned system firmware" msgstr "Asenna allekirjoittamaton järjestelmän laiteohjelmisto" #. TRANSLATORS: stay on one firmware version unless the new version is #. explicitly #. * specified msgid "Installing a specific release is explicitly required" msgstr "Juuri tietyn julkaisuversion asentaminen vaaditaan" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "Asennetaan laiteohjelmistopäivitystä…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "Asentaa laitteelle %s…" #. TRANSLATORS: if it breaks, you get to keep both pieces msgid "Installing this update may also void any device warranty." msgstr "Tämän päivityksen asentaminen saattaa kumota laitteen takuun." #. TRANSLATORS: The BIOS setting only accepts integers in a fixed range msgid "Integer" msgstr "Kokonaisluku" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM Protected" msgstr "Intel BootGuard ACM -suojattu" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "Intel BootGuard ACM -suojattu" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard Error Policy" msgstr "Intel BootGuard -virhekäytäntö" msgid "Intel BootGuard Error Policy ensures the device does not continue to start if its device software has been tampered with." msgstr "Intel BootGuardin virhekäytäntö varmistaa, että laitteen käynnistys ei jatku, jos sen laiteohjelmistoa on peukaloitu." #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard Fuse" msgstr "Intel BootGuard -sulake" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "Intel BootGuardin OTP-sulake" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard Verified Boot" msgstr "Intel BootGuardilla vahvistettu käynnistys" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Intel BootGuard -virhekäytäntö" #. TRANSLATORS: longer description msgid "Intel BootGuard prevents unauthorized device software from operating when the device is started." msgstr "Intel BootGuard estää luvattoman laiteohjelmiston toiminnan, kun laite käynnistyy." #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Intel BootGuardilla vahvistettu käynnistys" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS Mitigation" msgstr "Intel GDS Mitigation" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS mitigation" msgstr "Intel GDS:n lieventäminen" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Manufacturing Mode" msgstr "Intel Management Engine -valmistustila" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the "override" is #. enabled #. * with a jumper -- luckily it is probably not accessible to end users on #. consumer #. * boards msgid "Intel Management Engine Override" msgstr "Intel Management Enginen ohitus" #. TRANSLATORS: longer description msgid "Intel Management Engine Override disables checks for device software tampering." msgstr "Intel Management Enginen ohitus ottaa laiteohjelmiston peukaloinnin tunnistuksen pois käytöstä." #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Version" msgstr "Intel Management Enginen versio" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "Sisäinen laite" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "Virheellinen" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "Virheelliset argumentit" #. TRANSLATORS: error message msgid "Invalid arguments, expected GUID" msgstr "Virheelliset argumentit, GUID" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX KEY [VALUE]" msgstr "Virheelliset argumentit, INDEX KEY [VALUE]" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX NAME TARGET [MOUNTPOINT]" msgstr "Virheelliset argumentit, INDEX NAME TARGET [MOUNTPOINT]" #. TRANSLATOR: This is the error message for #. * incorrect parameter msgid "Invalid arguments, expected an AppStream ID" msgstr "Virheelliset argumentit, AppStream ID" #. TRANSLATORS: error message msgid "Invalid arguments, expected at least ARCHIVE FIRMWARE METAINFO" msgstr "Virheelliset argumentit, ainakin ARCHIVE FIRMWARE METAINFO" #. TRANSLATORS: error message msgid "Invalid arguments, expected base-16 integer" msgstr "Virheelliset argumentit base-16 integer" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "On vanhempi" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "On käynnistyslataintilassa" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "On päivitys" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "Kysymykset" msgstr[1] "Viat" #. TRANSLATORS: HSI event title msgid "Kernel is no longer tainted" msgstr "Kernel ei ole enää saastunut" #. TRANSLATORS: HSI event title msgid "Kernel is tainted" msgstr "Kernel on saastunut" #. TRANSLATORS: HSI event title msgid "Kernel lockdown disabled" msgstr "Kernelin lukitus on poistettu" #. TRANSLATORS: HSI event title msgid "Kernel lockdown enabled" msgstr "Kernel on lukittu" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "SIJAINTI" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "Viimeksi muokattu" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "Jäljellä on alle minuutti" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "Lisenssi" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "Linux kernel lukitus" #. TRANSLATORS: longer description msgid "Linux Kernel Lockdown mode prevents administrator (root) accounts from accessing and changing critical parts of system software." msgstr "Linux Kernel lukitustila estää pääkäyttäjiä (root) lukemasta ja muokkaamasta järjestelmäohjelmistojen kriittisiä osia." msgid "Linux Kernel Swap temporarily saves information to disk as you work. If the information is not protected, it could be accessed by someone if they obtained the disk." msgstr "Linux Kernel Swap tallentaa tietoja käytön aikana väliaikaisesti levylle. Jos näitä tietoja ei suojata, levyn käsiinsä saava taho pystyy lukemaan ne." #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "Linux kernel vahvistus" msgid "Linux Kernel Verification makes sure that critical system software has not been tampered with. Using device drivers which are not provided with the system can prevent this security feature from working correctly." msgstr "Linux Kernel varmennus varmentaa, että kriittistä järjestelmäohjelmistoa ei ole peukaloitu. Sellaisten laiteajurien käyttö, joita ei toimiteta järjestelmän mukana, saattaa estää tämän turvallisuusominaisuuden oikeanlaisen toiminnan." #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "Linux Swap" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Linux-laitetoimittajien laiteohjelmistopalvelu (vakaat laiteohjelmistot)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Linux-laitetoimittajien laiteohjelmistopalvelu (testattavat laiteohjelmistot)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Linux kernel" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Linux kernel lukitus" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Linux swap" #. TRANSLATORS: command description msgid "List EFI boot files" msgstr "Lista EFI boot tiedostoista" #. TRANSLATORS: command description msgid "List EFI boot parameters" msgstr "Lista EFI boot parametreistä" #. TRANSLATORS: command description msgid "List EFI variables with a specific GUID" msgstr "Luetteloi tietyn GUID:n EFI-muuttujat" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "Listaa dbx:n merkinnät" #. TRANSLATORS: command description msgid "List the available firmware GTypes" msgstr "Luettelo käytettävissä olevista GTypeseistä" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "Luettelo käytettävissä olevista laiteohjelmistotyypeistä" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "Luetteloi ESP:n tiedostot" #. TRANSLATORS: command description msgid "Load device emulation data" msgstr "Lataa laitteen emulointitiedot" #. TRANSLATORS: the plugin was created from a .so object, and was not built-in msgid "Loaded from an external module" msgstr "Ladattu ulkoisesta moduulista" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "Ladataan…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "Lukittu" #. TRANSLATORS: the release urgency msgid "Low" msgstr "Matala" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refers #. * to the private/public key used to secure loading of firmware msgid "MEI Key Manifest" msgstr "MEI-avainmanifesti" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refer #. * to the private/public key used to secure loading of firmware msgid "MEI key manifest" msgstr "MEI-avainmanifesti" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "MEI-valmistustila" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "MEI-ohitus" msgid "MEI version" msgstr "MEI-versio" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "Ota manuaalisesti tietyt laajennukset käyttöön" #. TRANSLATORS: longer description msgid "Manufacturing Mode is used when the device is manufactured and security features are not yet enabled." msgstr "Valmistustilaa käytetään laitteen valmistuksen aikana, kun turvallisuusominaisuudet eivät ole vielä käytössä." #. TRANSLATORS: Longest valid string for BIOS setting msgid "Maximum length" msgstr "Enimmäispituus" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "Enimmäisarvo" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "Keskitaso" #. TRANSLATORS: ask the user to do a simple task which should be translated msgid "Message" msgstr "Viesti" #. TRANSLATORS: ask the user a question, and it will not be translated msgid "Message (custom)" msgstr "Viesti (muokattu)" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "Metatietojen allekirjoitus" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "Metatietojen URI" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "Metatiedot voidaan hankkia Linux Vendor Firmware -palvelusta." #. TRANSLATORS: error message for a user who ran fwupdmgr #. * refresh recently -- %1 is '--force' #, c-format msgid "Metadata is up to date; use %s to refresh again." msgstr "Metatiedot ovat ajan tasalla; käytä %s virkistää uudelleen." #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "Vähimmäisversio" #. TRANSLATORS: Shortest valid string for BIOS setting msgid "Minimum length" msgstr "Vähimmäispituus" #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "Vähimmäisarvo" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "Muokkaa taustaprosessin kokoonpanoarvoa" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "Muokkaa annettua lähdettä" msgid "Modify a configured remote" msgstr "Muokkaa lähteen asetuksia" msgid "Modify daemon configuration" msgstr "Muokkaa taustaprosessin kokoonpanoa" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "Seuraa tapahtumien taustaprosessia" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "Liitä ESP" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "Tarvitsee uudelleenkäynnistyksen asennuksen jälkeen" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "Uudelleenkäynnistys tarvitaan" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "Tarvitsee sammutuksen asennuksen jälkeen" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "Uusi versio" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "Toimintoa ei ole määritetty!" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "Ei versioalennusta laitteelle %s" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "Laiteohjelmiston tunnuksia ei löytynyt" #. TRANSLATORS: nothing found msgid "No firmware found" msgstr "Laiteohjelmistoa ei löytynyt" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Ei löytynyt laitteita, joille voisi asentaa laiteohjelmistopäivityksiä" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "Ei julkaisuja saatavilla" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "Mitään lähteitä ei ole tällä hetkellä käytössä, joten metatietoja ei ole saatavilla." #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "Lähteitä ei saatavilla" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "Ei päivitettäviä laitteita" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "Päivityksiä ei ole saatavilla" msgid "No updates available for remaining devices" msgstr "Muille laitteille ei ole saatavilla päivityksiä" #. TRANSLATORS: error message #, c-format msgid "No volume matched %s" msgstr "Ei vastaavaa asemaa %s" #. TRANSLATORS: version cannot be installed due to policy msgid "Not approved" msgstr "Hylätty" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "Ei löydy" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "Ei tueta" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "Kaikki hyvin" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "Kaikki hyvin!" #. TRANSLATORS: the firmware old version msgid "Old version" msgstr "Vanha versio" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "Näytä vain yksi PCR-arvo" #. TRANSLATORS: command line option msgid "Only use peer-to-peer networking when downloading files" msgstr "Käytä vain vertaisverkkoa tiedostojen lataamiseen" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "Vain versioiden päivitykset ovat sallittuja" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "Ohita oletusarvoinen ESP-polku" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Firmware" msgstr "P2P-laiteohjelmisto" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Metadata" msgstr "P2P-metatiedot" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "POLKU" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "Jäsennä ja näytä tiedot laiteohjelmistotiedostosta" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "Jäsennetään dbx-päivitystä..." #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "Jäsennetään järjestelmän dbx:ää..." #. TRANSLATORS: remote filename base msgid "Password" msgstr "Salasana" #. TRANSLATORS: command description msgid "Patch a firmware blob at a known offset" msgstr "Korjaa laiteohjelmiston blob tunnetulla vastineella" msgid "Payload" msgstr "Tietosisältö" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "Odottava" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "Suoritetaanko toiminto?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "Alustan virheenjäljitys" #. TRANSLATORS: longer description msgid "Platform Debugging allows device security features to be disabled. This should only be used by hardware manufacturers." msgstr "Alustan virheenjäljityksen avulla voidaan ottaa laitteen turvallisuusominaisuudet pois käytöstä. Vain laitevalmistajien tulisi käyttää tätä ominaisuutta." #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "Alustan virheenjäljitys" #. TRANSLATORS: 'recovery key' here refers to a code, rather than a physical #. metal thing msgid "Please ensure you have the volume recovery key before continuing." msgstr "Varmista, että sinulla on aseman palautusavain ennen kuin jatkat." #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "Anna numero 0 –%u: " #. TRANSLATORS: the user isn't reading the question -- %1 is 'Y' and %2 is #. * 'N' #, c-format msgid "Please enter either %s or %s: " msgstr "Valitse %s tai %s: " #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "Laajennuksen riippuvuudet puuttuvat" #. TRANSLATORS: The plugin is only for testing msgid "Plugin is only for testing" msgstr "Laajennus on vain testausta varten" #. TRANSLATORS: Possible values for a bios setting msgid "Possible Values" msgstr "Mahdolliset arvot" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA Protection" msgstr "Käynnistystä edeltävä DMA-suojaus" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "Käynnistystä edeltävä DMA-suojaus" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is disabled" msgstr "Käynnistystä edeltävä DMA-suojaus on poistettu käytöstä" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is enabled" msgstr "Käynnistystä edeltävä DMA-suojaus on käytössä" #. TRANSLATORS: longer description msgid "Pre-boot DMA protection prevents devices from accessing system memory after being connected to the computer." msgstr "Käynnistystä edeltävä DMA-suojaus estää laitteita käyttämästä järjestelmän muistia, kun ne kytketään tietokoneeseen." #. TRANSLATORS: warning message msgid "Press unlock on the device to continue the update process." msgstr "Paina laitteen lukituksen avauspainiketta, jotta päivitys voi jatkua." #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "Aiempi versio" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Prioriteetti" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "Ongelmat" msgid "Proceed with upload?" msgstr "Jatketaanko lähettämistä?" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Processor Security Checks" msgstr "Processor Security Checks" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "Processor rollback protection" msgstr "Suorittimen versiopalautuksen suojaus" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "Suljettu" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "LÄHDETUNNUS" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID KEY VALUE" msgstr "LÄHDETUNNUS AVAIN ARVO" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "Vain luku" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "Lue laiteohjelmiston \"blob\" laitteesta" #. TRANSLATORS: command description msgid "Read a firmware from a device" msgstr "Lue laiteohjelmisto laitteelta" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "Lukee laitteelta %s…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Luetaan…" #. TRANSLATORS: Plugin is active and in use msgid "Ready" msgstr "Valmis" #. TRANSLATORS: how often we should refresh the metadata msgid "Refresh Interval" msgstr "Päivitysväli" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "Päivitä metatiedot etäpalvelimelta" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "Asennetaanko laitteeseen %s uudelleen laiteohjelmiston versio%s?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "Asenna nykyinen laiteohjelmisto laitteeseen uudelleen" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "Asenna laiteohjelmisto uudelleen laitteelle" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "Julkaisuhaara" #. TRANSLATORS: release attributes msgid "Release Flags" msgstr "Julkaisuliput" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "Julkaisutunnus" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "Etätunnus" #. TRANSLATORS: command description msgid "Removes devices to watch for future emulation" msgstr "Poistaa katsottavat laitteet tulevaa emulointia varten" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "Ilmoitus-URI" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "Raportoitu etäpalvelimelle" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "Vaadittua efivarfs-tiedostojärjestelmää ei löytynyt" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "Vaadittavaa laitteistoa ei löytynyt" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "Vaatii käynnistyslataimen" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Edellyttää Internet-yhteyttä" msgid "Reset daemon configuration" msgstr "Nollaa taustaprosessin määritykset" #. TRANSLATORS: sets something in the daemon configuration file msgid "Resets a daemon configuration section" msgstr "Nollaa taustaprosessin määritysosio" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Käynnistetäänkö uudelleen nyt?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "Käynnistetäänkö taustaprosessi uudelleen, jotta muutos tulee voimaan?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Käynnistetään laite uudelleen…" #. TRANSLATORS: command description msgid "Retrieve BIOS settings. If no arguments are passed all settings are returned" msgstr "Nouda BIOS-asetuksia. Jos parametreja ei anneta, noudetaan kaikki asetukset." #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "Palauta kaikki koneen laitetunnukset" #. TRANSLATORS: ask the user to upload msgid "Review and upload report now?" msgstr "Tarkasteletko ja lähetätkö raportin nyt?" #. TRANSLATORS: longer description msgid "Rollback Protection prevents device software from being downgraded to an older version that has security problems." msgstr "Versiopalautuksen suojaus estää laiteohjelmiston version varhentamisen vanhempaan versioon, jossa on turvallisuusongelmia." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr get-upgrades` #, c-format msgid "Run `%s` for more information." msgstr "Suorita \"%s\" saadaksesi lisätietoja." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr sync` #, c-format msgid "Run `%s` to complete this action." msgstr "Suorita \"%s\" ja viimeistele tämä toiminto." #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "Suorita liitännäisen yhteinen siivousrutiini, kun käytetään komentoa install-blob" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "Suorita liitännäisen yhteinen valmistelurutiini, kun käytetään komentoa install-blob" #. TRANSLATORS: command description msgid "Run the post-reboot cleanup action" msgstr "Suorita uudelleenkäynnistyksen jälkeinen puhdistustoiminto" #. TRANSLATORS: tell a user how to get information #, c-format msgid "Run without '%s' to see" msgstr "Suorita ilman valitsinta â€%s†nähdäksesi" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "Ajossa oleva kernel on liian vanha" #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "Ajonaikainen pääte" #. TRANSLATORS: Software Bill of Materials link msgid "SBOM" msgstr "SBOM" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SECTION" msgstr "SECTION" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING VALUE" msgstr "ASETUS ARVO" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING1 VALUE1 [SETTING2] [VALUE2]" msgstr "ASETUS1 ARVO1 [ASETUS2] [ARVO2]" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "SMAP" msgstr "SMAP" #. TRANSLATORS: Title: Whether firmware is locked down msgid "SMM locked down" msgstr "SMM lukittu" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "SPI-BIOS-tunnus" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "SPI-BIOS-alue" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "SPI-lukko" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "SPI replay protection" msgstr "SPI-toiston suojaus" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "SPI-kirjoitus" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "SPI write protection" msgstr "SPI-kirjoitussuojaus" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "ALIJÄRJESTELMÄ AJURI [LAITETUNNUS|GUID]" #. TRANSLATORS: command description msgid "Save a file that allows generation of hardware IDs" msgstr "Tallenna tiedosto, joka mahdollistaa laitetunnusten luomisen" #. TRANSLATORS: command description msgid "Save device emulation data" msgstr "Tallenna laitteen emulointitiedot" #. TRANSLATORS: key for a offline report filename msgid "Saved report" msgstr "Raportti tallennettu" #. TRANSLATORS: Scalar increment for integer BIOS setting msgid "Scalar Increment" msgstr "Lukuarvon kasvattaminen" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Ajoitetaan…" #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "Secure Boot poistettu" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "Secure Boot käytössä" msgid "Security hardening for HSI" msgstr "HSI turvallisuuden käsittely" #. TRANSLATORS: the %1 is a URL to a wiki page #, c-format msgid "See %s for more details." msgstr "Lisätietoja sivulla %s" #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "Katso %s saadaksesi lisätietoja." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Valittu laite" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Valittu asema" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "Sarjanumero" #. TRANSLATORS: Configured a BIOS setting to a value #, c-format msgid "Set BIOS setting '%s' using '%s'." msgstr "Asetettiin BIOS-asetus â€%s†arvoon â€%sâ€." #. TRANSLATORS: command description msgid "Set a BIOS setting" msgstr "Aseta BIOS-asetus" msgid "Set one or more BIOS settings" msgstr "Aseta yksi tai useampi BIOS-asetus" #. TRANSLATORS: command description msgid "Set or remove an EFI boot hive entry" msgstr "Aseta tai poista EFI boot hive merkintä" #. TRANSLATORS: command description msgid "Set the EFI boot next" msgstr "Aseta EFI boot seuraavaksi" #. TRANSLATORS: command description msgid "Set the EFI boot order" msgstr "Aseta EFI boot käynnistysjärjestys" #. TRANSLATORS: command line option msgid "Set the download retries for transient errors" msgstr "Aseta lataus yrittämään uudelleen virheiden varalta" #. TRANSLATORS: command description msgid "Sets one or more BIOS settings" msgstr "Asettaa yhden tai useamman BIOS-asetuksen" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Tallentaa hyväksyttyjen laiteohjelmistojen luettelon" #. TRANSLATORS: type of BIOS setting msgid "Setting type" msgstr "Asetuksen tyyppi" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "Asetukset tulevat voimaan, kun järjestelmä on käynnistetty uudelleen" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "Jaa laiteohjelmiston historia kehittäjien kanssa" #. TRANSLATORS: command line option msgid "Show all results" msgstr "Näytä kaikki tulokset" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "Näytä asiakas- ja taustaprosessin versiot" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "Näytä taustaprosessin täydelliset tiedot tietylle verkkotunnukselle" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "Näytä kaikkien verkkotunnusten virheenkorjaustiedot" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Näytä vianjäljitysvalinnat" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Näytä laitteet, joita ei voi päivittää" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Näytä ylimääräiset virheenkorjaustiedot" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "Näytä laiteohjelmistopäivitysten historia" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "Näytä dbx:n laskettu versio" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "Sammutetaanko nyt?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "Allekirjoita laiteohjelmisto uudella avaimella" msgid "Sign data using the client certificate" msgstr "Allekirjoita tietoja käyttämällä asiakkaan sertifikaattia" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "Allekirjoita tietoja käyttämällä asiakkaan sertifikaattia" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "Allekirjoita lähetetyt tiedot asiakkaan sertifikaatilla" msgid "Signature" msgstr "Allekirjoitus" #. TRANSLATORS: firmware is verified on-device the payload using strong crypto msgid "Signed Payload" msgstr "Allekirjoitettu hyötykuorma" #. TRANSLATORS: file size of the download msgid "Size" msgstr "Koko" #. TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM msgid "Some of the platform secrets may be invalidated when updating this firmware." msgstr "Jotkut alustasalaukset voidaan mitätöidä tätä laiteohjelmistoa päivitettäessä." #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "Lähde" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "Määritä dbx-tietokantatiedosto" msgid "Stop the fwupd service" msgstr "Pysäytä fwupd-palvelu" #. TRANSLATORS: The BIOS setting accepts strings msgid "String" msgstr "Merkkijono" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "Onnistui" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "Kaikkien laitteiden aktivointi onnistui" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "Lähteen poistaminen onnistui" #. TRANSLATORS: comment explaining result of command msgid "Successfully disabled test devices" msgstr "Testilaitteiden käytöstä poistaminen onnistui" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "Uusien metatietojen lataus onnistui: " #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "Lähteen käyttöönotto ja päivitys onnistui" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "Lähteen käyttöönotto onnistui" #. TRANSLATORS: comment explaining result of command msgid "Successfully enabled test devices" msgstr "Testilaitteet on otettu käyttöön" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "Laiteohjelmiston asennus onnistui" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "Kokoonpanoarvon muokkaaminen onnistui" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "Lähteen muokkaus onnistui" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "Metatietojen manuaalinen päivitys onnistui" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration section" msgstr "Määritysten nollaus onnistui" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration values" msgstr "Määritysarvojen nollaus onnistui" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "Laitteiden tarkistussummien päivitys onnistui" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "Raporttien lataus %u onnistui" msgstr[1] "%u raportin lähetys onnistui" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "Laitteiden tarkistussummat vahvistettu onnistuneesti" #. TRANSLATORS: the device showed up in time #, c-format msgid "Successfully waited %.0fms for device" msgstr "Odotettiin laitetta %.0fms ja se ilmestyi" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Yhteenveto" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "Supervisor Mode Access Prevention" msgstr "Valvojatilan käytön esto" #. TRANSLATORS: longer description msgid "Supervisor Mode Access Prevention ensures critical parts of device memory are not accessed by less secure programs." msgstr "Valvojatilan käytön esto varmistaa, että vähemmän turvalliset ohjelmat eivät pääse käsiksi laitteen muistin kriittisiin osiin." #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "Tuettu" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "Tuettu suoritin" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "Tuettu etäpalvelimella" #. TRANSLATORS: Title: a better sleep state msgid "Suspend To Idle" msgstr "Keskeytä tyhjäkäynnille" #. TRANSLATORS: Title: sleep state msgid "Suspend To RAM" msgstr "Keskeytä RAM-muistiin" #. TRANSLATORS: longer description msgid "Suspend to Idle allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Keskeyttämällä tyhjäkäynnille laite voi mennä nopeasti unitilaan virran säästämiseksi. Kun laite on unitilassa, sen muisti voidaan fyysisesti irrottaa ja muistin sisältämät tiedot voidaan lukea." #. TRANSLATORS: longer description msgid "Suspend to RAM allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Keskeyttämällä RAM-muistiin laite voi mennä nopeasti unitilaan virran säästämiseksi. Kun laite on unitilassa, sen muisti voidaan fyysisesti irrottaa ja muistin sisältämät tiedot voidaan lukea." #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "Keskeytä tyhjäkäynnille" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "Keskeytä RAM-muistiin" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "Vaihdatko haaran %s haaraksi %s?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "Vaihda laitteen laiteohjelmiston haaraa" #. TRANSLATORS: command description msgid "Sync firmware versions to the chosen configuration" msgstr "Synkronoi laiteohjelmistoversiot valittuun kokoonpanoon" #. TRANSLATORS: Title: Whether firmware is locked down msgid "System Management Mode" msgstr "System Management Mode" #. TRANSLATORS: this CLI tool is now preventing system updates msgid "System Update Inhibited" msgstr "Järjestelmäpäivitys on estetty" #. TRANSLATORS: longer description msgid "System management mode is used by the firmware to access resident BIOS code and data." msgstr "Firmware laiteohjelmisto käyttää järjestelmän hallintatilaa BIOS:in koodin ja tietojen käyttämiseen." #. TRANSLATORS: as in laptop battery power msgid "System power is too low" msgstr "Laitteen virta on liian alhainen" #. TRANSLATORS: as in laptop battery power #, c-format msgid "System power is too low (%u%%, requires %u%%)" msgstr "Laitteen virta on alhainen (%u%%, vaatii %u%%)" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "Järjestelmä vaatii ulkoisen virtalähteen" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "TEKSTI" #. TRANSLATORS: longer description msgid "TPM (Trusted Platform Module) is a computer chip that detects when hardware components have been tampered with." msgstr "TPM (Trusted Platform Module) on tietokonesiru, joka tunnistaa, jos laitteistokomponentteja on peukaloitu." #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "TPM PCR0 -jälleenrakennus" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is invalid" msgstr "TPM PCR0 -jälleenrakennus on virheellinen" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is now valid" msgstr "TPM PCR0 -jälleenrakennus on nyt kelvollinen" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM Platform Configuration" msgstr "TPM-alustan kokoonpano" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM Reconstruction" msgstr "TPM:n jälleenrakennus" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM empty PCRs" msgstr "TPM tyhjät PCR:t" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "Tunniste" msgstr[1] "Tunnisteet" #. TRANSLATORS: we're saving all USB events for emulation msgid "Tagged for emulation" msgstr "Merkitty emuloitavaksi" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "Ydin on tainted-tilassa" #. show the user the entire data blob msgid "Target" msgstr "Kohde" #. TRANSLATORS: command description msgid "Test a device using a JSON manifest" msgstr "Testaa laitetta käyttäen JSON-manifestia" #. TRANSLATORS: when the release was tested msgid "Tested" msgstr "Testattu" #. TRANSLATORS: the %s is a vendor name, e.g. Lenovo #, c-format msgid "Tested by %s" msgstr "Valmistajan %s testaama" #. TRANSLATORS: someone we trust has tested this msgid "Tested by trusted vendor" msgstr "Luotettavan valmistajan testaama" #. TRANSLATORS: the boot entry was in a legacy format msgid "The EFI boot entry is not in hive format, and shim may not be new enough to read it." msgstr "EFI boot merkintä ei ollut hive-formaatissa, eikä shim-tiedosto välttämättä ole riittävän uusi sen lukemiseen." #. TRANSLATORS: try to treat the legacy format as a hive msgid "The EFI boot entry was not in hive format, falling back" msgstr "EFI boot ei ollut hive-formaatissa ja palautui takaisin" #. TRANSLATORS: longer description msgid "The Intel Management Engine Key Manifest must be valid so that the device firmware can be trusted by the CPU." msgstr "Intel Management Engine -avainmanifestin täytyy olla kelvollinen, jotta suoritin voi luottaa laiteohjelmistoon." #. TRANSLATORS: longer description msgid "The Intel Management Engine controls device components and needs to have a recent version to avoid security issues." msgstr "Intel Management Engine hallitsee laitteistokomponentteja. Sen versio on oltava tuore, jotta vältytään turvallisuusongelmilta." #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "LVFS on ilmainen palvelu, joka toimii itsenäisenä oikeushenkilönä eikä sillä ole yhteyttä jakelijaan $OS_RELEASE:NAME$. Jakelijasi ei ehkä ole varmistanut laiteohjelmistopäivitysten yhteensopivuutta järjestelmän tai liitettyjen laitteiden kanssa. Kaikki laiteohjelmistot tulevat alkuperäisiltä laitevalmistajilta." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Platform Configuration is used to check whether the device start process has been modified." msgstr "TPM-alustan (Trusted Platform Module) kokoonpanon avulla tarkastetaan, onko laitteen käynnistysprosessia muutettu." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Reconstruction is used to check whether the device start process has been modified." msgstr "TPM:n (Trusted Platform Module) jälleenrakennuksen avulla tarkastetaan, onko laitteen käynnistysprosessia muutettu." #. TRANSLATORS: this is more background on a security measurement problem msgid "The TPM PCR0 differs from reconstruction." msgstr "TPM PCR0 eroaa uudelleenrakennetusta." #. TRANSLATORS: longer description msgid "The UEFI Platform Key is used to determine if device software comes from a trusted source." msgstr "UEFI:n alusta-avaimen perusteella tiedetään, tuleeko laiteohjelmisto luotettavasta lähteestä." #. TRANSLATORS: HSI event title msgid "The UEFI certificate store is now up to date" msgstr "UEFI-varmenteet on nyt päivitetty" #. TRANSLATORS: longer description msgid "The UEFI db contains the list of valid certificates that can be used to authorize what EFI binaries are allowed to run." msgstr "UEFI-tietokanta sisältää luettelon voimassa olevista varmenteista, joilla valtuutetaan EFI-binäärien suoritusoikeudet." #. TRANSLATORS: longer description msgid "The UEFI system can set up memory attributes at boot which prevent common exploits from running." msgstr "UEFI voi määrittää käynnistyksen yhteydessä muistin ominaisuuksia, jotka estää hyökkäysten suorittamisen." #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "Palvelu on ladannut 3. osapuolen koodia, eivätkä kehittäjät enää jatkossa tue sitä!" #. TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name #, c-format msgid "The firmware from %s is not supplied by %s, the hardware vendor." msgstr "Laiteohjelmiston on toimittanut %s eikä laitteen toimittaja %s." #. TRANSLATORS: try to help msgid "The system clock has not been set correctly and downloading files may fail." msgstr "Järjestelmän kelloa ei ole asetettu oikein ja tiedostojen lataaminen saattaa epäonnistua." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been re-inserted." msgstr "Päivitys jatkuu, kun laitteen USB-kaapeli on kiinnitetty uudestaan." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged and then re-inserted." msgstr "Päivitys jatkuu, kun laitteen USB-kaapeli on irrotettu ja kiinnitetty uudestaan." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged." msgstr "Päivitys jatkuu, kun laitteen USB-kaapeli on irrotettu." #. TRANSLATORS: warning message msgid "The update will continue when the device power cable has been removed and re-inserted." msgstr "Päivitys jatkuu, kun laitteen virtajohto on irrotettu ja asetettu takaisin." #. TRANSLATORS: naughty vendor msgid "The vendor did not supply any release notes." msgstr "Valmistaja ei toimittanut julkaisutietoja." #. TRANSLATORS: now list devices with unfixed high-priority issues msgid "There are devices with issues:" msgstr "Seuraavissa laitteissa on ongelmia:" #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "Estettyjä laiteohjelmistotiedostoja ei ole" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "Hyväksyttyä laiteohjelmistoa ei ole." #. TRANSLATORS: %1 is the current device version number, and %2 is the #. command name, e.g. `fwupdmgr sync` #, c-format msgid "This device will be reverted back to %s when the %s command is performed." msgstr "Laite palautetaan versioon %s, kun komento %s suoritetaan." #. TRANSLATORS: the vendor did not upload this msgid "This firmware is provided by LVFS community members and is not provided (or supported) by the original hardware vendor." msgstr "Tämä laiteohjelmisto on tarjolla LVFS-yhteisöjäsenten toimesta, sitä ei ole tarjottu (tai tuettu) alkuperäisen laitevalmistajan toimesta." #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "Tätä pakettia ei ole vahvistettu, se ei välttämättä toimi oikein." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "Tämä ohjelma toimii oikein vain root-käyttäjän oikeuksin" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "Tämä lähde sisältää laiteohjelmiston, joka ei ole vientikiellossa, mutta jota laitteistotoimittaja testaa edelleen. Varmista, että voit päivittää laiteohjelmiston takaisin vanhempaan versioon manuaalisesti, jos päivitys epäonnistuu." #. TRANSLATORS: error message msgid "This system doesn't support firmware settings" msgstr "Tämä järjestelmä ei tue laiteohjelmiston asetuksia" #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "Järjestelmässä on ajonaikainen HSI-ongelma." #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "Tämän järjestelmän HSI-tietoturvataso on alhainen." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "Tämän työkalun avulla järjestelmänvalvoja voi asentaa UEFI-dbx-päivityksiä." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "Tämän työkalun avulla järjestelmänvalvoja voi kysellä ja hallita fwupd-palvelua, jolloin voi suorittaa toimintoja, kuten laiteohjelmiston asennus tai päivittäminen." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "Tämän työkalun avulla järjestelmänvalvoja voi käyttää fwupd-laajennuksia asentamatta niitä järjestelmään." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can add a kernel argument of '%s', but it will only be active after restarting the computer." msgstr "Tämä työkalu voi lisätä kernel argumentin \"%s\", mutta se on aktiivinen vasta tietokoneen uudelleenkäynnistyksen jälkeen." #. TRANSLATORS: the %1 is a BIOS setting name. #. * %2 and %3 are the values, e.g. "True" or "Windows10" #, c-format msgid "This tool can change the BIOS setting '%s' from '%s' to '%s' automatically, but it will only be active after restarting the computer." msgstr "Tämä työkalu voi vaihtaa BIOS-asetuksen â€%s†automaattisesti arvosta â€%s†arvoon â€%sâ€, mutta muutos tulee voimaan vasta uudelleenkäynnistyksen jälkeen." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can change the kernel argument from '%s' to '%s', but it will only be active after restarting the computer." msgstr "Tämä työkalu voi muuttaa kernel argumentin \"%s\":sta \"%s\":ksi, mutta se on aktiivinen vasta tietokoneen uudelleenkäynnistyksen jälkeen." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "Tämä työkalu lukee ja jäsentää TPM-tapahtumalokin järjestelmän laiteohjelmistosta." #. TRANSLATORS: the update state of the specific device msgid "Transient failure" msgstr "Ohimenevä häiriö" #. TRANSLATORS: item is TRUE msgid "True" msgstr "Tosi" #. TRANSLATORS: We verified the metadata against the server msgid "Trusted metadata" msgstr "Luotettu metatieto" #. TRANSLATORS: We verified the payload against the server msgid "Trusted payload" msgstr "Luotettu lataus" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Tyyppi" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI Bootservice Variables" msgstr "UEFI Boot käynnistyspalvelun muuttujat" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition may not be set up correctly" msgstr "UEFIn ESP-osion asetuksia ei ole ehkä tehty oikein" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "UEFIn ESP-osiota ei havaittu tai määritetty" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI Memory Protection" msgstr "UEFI Memory Protection" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "UEFI Platform Key" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "UEFI Secure Boot" #. TRANSLATORS: longer description msgid "UEFI Secure Boot prevents malicious software from being loaded when the device starts." msgstr "UEFI Secure Boot estää haittaohjelmien lataamisen laitteen käynnistyessä." #. TRANSLATORS: longer description msgid "UEFI boot service variables should not be readable from runtime mode." msgstr "EFI boot palvelun muuttujia ei pitäisi pystyä lukemaan ajon aikana." #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI bootservice variables" msgstr "UEFI Boot käynnistyspalvelun muuttujat" #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "UEFI-kapselipäivitykset eivät ole saatavilla tai niitä ei ole otettu käyttöön laiteohjelmiston asetuksissa" #. TRANSLATORS: Title: is UEFI db up-to-date msgid "UEFI db" msgstr "UEFI db" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "UEFI-dbx-apuohjelma" #. TRANSLATORS: system is not booted in UEFI mode msgid "UEFI firmware can not be updated in legacy BIOS mode" msgstr "UEFI-laiteohjelmiston päivitys ei onnistu vanhassa BIOS-tilassa" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI memory protection" msgstr "UEFI-muistin suojaus" #. TRANSLATORS: HSI event title msgid "UEFI memory protection enabled and locked" msgstr "UEFI-muistisuojaus on käytössä ja lukittu" #. TRANSLATORS: HSI event title msgid "UEFI memory protection enabled but not locked" msgstr "UEFI-muistisuojaus on käytössä, mutta ei lukittu" #. TRANSLATORS: HSI event title msgid "UEFI memory protection is now locked" msgstr "UEFI-muistisuojaus on nyt lukittu" #. TRANSLATORS: HSI event title msgid "UEFI memory protection is now unlocked" msgstr "UEFI-muistisuojaus on nyt poistettu" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "UEFI:n alusta-avain" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "UEFI secure boot" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "Yhteys palveluun ei onnistu" #. TRANSLATORS: error message msgid "Unable to find attribute" msgstr "Attribuuttia ei löydy" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "Irrota nykyinen ajuri" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "Laiteohjelmiston eston poistaminen:" #. TRANSLATORS: command description msgid "Unblocks a specific firmware from being installed" msgstr "Poistaa asennuseston laiteohjelmistosta" #. TRANSLATORS: command description msgid "Undo the host security attribute fix" msgstr "Kumoa isäntätietoturvaattribuutin korjaus" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "Salaamaton" #. TRANSLATORS: command description msgid "Uninhibit the system to allow upgrades" msgstr "Poista päivitysten esto käytöstä" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Tuntematon" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Tuntematon laite" msgid "Unlock the device to allow access" msgstr "Avaa laitteen lukitus salliaksesi käytön" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "Auki" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "Avaa laitteen lukituksen, jotta laiteohjelmistoon on pääsy" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "Irrota ESP" #. TRANSLATORS: message shown after device has been marked for emulation msgid "Unplug and replug the device to continue the update process." msgstr "Irrota laite ja kytke se uudelleen jatkaaksesi päivitysprosessia." #. TRANSLATORS: firmware payload is unsigned and it is possible to modify it msgid "Unsigned Payload" msgstr "Allekirjoittamaton hyötykuorma" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "Taustaprosessin versiota %s ei tueta, asiakasohjelman versio on %s" #. TRANSLATORS: Suffix: the HSI result msgid "Untainted" msgstr "Ydin ei ole tainted-tilassa" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "Päivitettävissä" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Päivitysvirhe" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "Päivityskuva" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "Päivityksen viesti" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "Päivityksen tila" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "Päivityksen epäonnistuminen on tunnettu ongelma. Saat lisätietoja tästä URL-osoitteesta:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Päivitetäänkö nyt?" #. TRANSLATORS: command description msgid "Update the stored cryptographic hash with current ROM contents" msgstr "Päivitä tallennettu kryptografinen tiiviste nykyisellä ROM-sisällöllä" msgid "Update the stored device verification information" msgstr "Päivitä laitteen tallennetut vahvistustiedot" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "Päivitä tallennetut metatiedot nykyisellä sisällöllä" #. TRANSLATORS: command description msgid "Updates all specified devices to latest firmware version, or all devices if unspecified" msgstr "Päivittää kaikki määritetyt laitteet uusimpaan laiteohjelmistoversioon - tai kaikki laitteet, jos määritystä ei ole annettu" #. TRANSLATORS: how many local devices can expect updates now #, c-format msgid "Updates have been published for %u local device" msgid_plural "Updates have been published for %u of %u local devices" msgstr[0] "Päivitykset on julkaistu %u paikalliselle laitteelle" msgstr[1] "Päivityksiä on julkaistu %u paikallisesta laitteesta %u" msgid "Updating" msgstr "Päivitetään" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "Päivittää laitetta %s…" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "Päivitetäänkö laitteen %s laiteohjelmisto versiosta %s versioon %s?" #. TRANSLATORS: ask the user to upload msgid "Upload data now?" msgstr "Lähetätkö tiedot nyt?" #. TRANSLATORS: command description msgid "Upload the list of updatable devices to a remote server" msgstr "Lataa päivitettävien laitteiden luettelo etäpalvelimelle" #. TRANSLATORS: ask the user to share, %s is something #. * like: "Linux Vendor Firmware Service" #, c-format msgid "Upload these anonymous results to the %s to help other users?" msgstr "Lähetetäänkö nämä anonyymit tulokset palveluun %s, jotta niiden avulla voidaan auttaa muita käyttäjiä?" #. TRANSLATORS: explain why we want to upload #, c-format msgid "Uploading a device list allows the %s team to know what hardware exists, and allows us to put pressure on vendors that do not upload firmware updates for their hardware." msgstr "Laiteluettelon lähettäminen %s tiimille, antaa tiedon mitä laitteita on olemassa ja mahdollistaa painostuksen valmistajille, jotka eivät lataa firmware päivityksiä laitteilleen." #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "Laiteohjelmistoraporttien lähettäminen auttaa laitteistotoimittajia tunnistamaan nopeasti virheelliset ja onnistuneet päivitykset todellisissa laitteissa." #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "Kiireellisyys" #. TRANSLATORS: explain how to get help, %1 is #. * 'fwupdtool --help' #. TRANSLATORS: explain how to get help, #. * where $1 is something like 'fwupdmgr --help' #, c-format msgid "Use %s for help" msgstr "Käytä %ssaadaksesi apua" #. TRANSLATORS: CTRL^C [holding control, and then pressing C] will exit the #. program msgid "Use CTRL^C to cancel." msgstr "Peru painamalla CTRL^C." #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "Käyttäjälle on ilmoitettu" #. TRANSLATORS: remote filename base msgid "Username" msgstr "Käyttäjätunnus" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "VERSION1 VERSION2 [FORMAT]" msgstr "VERSION1 VERSION2 [FORMAT]" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "Kunnossa" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "Vahvistetaan ESP:n sisältöä..." #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "Muunnelma" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "Toimittaja" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "Vahvistetaan…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Versio" #. TRANSLATORS: the fwupd version the release was tested on msgid "Version[fwupd]" msgstr "Versio[fwupd]" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "VAROITUS" #. TRANSLATORS: command description msgid "Wait for a device to appear" msgstr "Odota että laite ilmestyy" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "Odotetaan…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "Seuraa laitteiston muutoksia" #. TRANSLATORS: check various UEFI and ACPI tables are unchanged after the #. update msgid "Will measure elements of system integrity around an update" msgstr "Tarkistaa järjestelmän eheyden yksityiskohtia päivityksen ympärillä" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "Kirjoitetaan tiedosto:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Kirjoitetaan…" #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from a recovery or installation disk, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "Varmista, että voit palauttaa asetuksia palautus- tai asennuslevyltä, koska tämä muutos saattaa aiheuttaa sen, että järjestelmä ei käynnisty Linuxiin tai aiheuttaa muuta järjestelmän epävakautta." #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from the system firmware setup, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "Varmista, että voit palauttaa asetukset järjestelmän laiteohjelmiston asetuksista, koska tämä muutos saattaa aiheuttaa sen, että järjestelmä ei käynnisty Linuxiin tai aiheuttaa muuta järjestelmän epävakautta." #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "Jakelijasi ei ehkä ole varmistanut laiteohjelmistopäivitysten yhteensopivuutta järjestelmän tai liitettyjen laitteiden kanssa." #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "Laitteistosi saattaa vaurioitua tämän laiteohjelmiston käytöllä. Asentaminen voi mitätöidä toimittajan %s takuun." #. TRANSLATORS: BKC is the industry name for the best known configuration and #. is a set #. * of firmware that works together #, c-format msgid "Your system is set up to the BKC of %s." msgstr "Järjestelmä on asetettu BKC-tilaan \"paras tunnettu konfiguraatio\" %s." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[APPSTREAM_ID]" msgstr "[APPSTREAM_ID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[TARKISTESUMMA]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[LAITETUNNUS|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[LAITETUNNUS|GUID] [HAARA]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [VERSION]" msgstr "[LAITETUNNUS|GUID] [VERSIO]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE]" msgstr "[LAITE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILE FILE_SIG REMOTE-ID]" msgstr "[TIEDOSTO TIEDOSTON_ALLEKIRJOITUS LÄHDETUNNUS]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[TIEDOSTONIMI1] [TIEDOSTONIMI2]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FWUPD-VERSION]" msgstr "[FWUPD-VERSION]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[REASON] [TIMEOUT]" msgstr "[REASON] [TIMEOUT]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SECTION] KEY VALUE" msgstr "[SECTION] KEY VALUE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2] [--no-authenticate]" msgstr "[ASETUS1] [ASETUS2] [--no-authenticate]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2]..." msgstr "[SETTING1] [SETTING2]..." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[SMBIOS-TIEDOSTO|HWIDS-TIEDOSTO]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "oletus" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "fwupd:n TPM-tapahtumalokin apuohjelma" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "fwupd-liitännäiset" fwupd-2.0.10/po/fr.po000066400000000000000000000776141501337203100143160ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Claude Paroz , 2021 # Corentin Noël , 2020 # Franck , 2015 # Julien Humbert , 2020-2021 # Paragoumba , 2024 # Yolopix ​, 2022 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: French (http://app.transifex.com/freedesktop/fwupd/language/fr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: fr\n" "Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "%.0f minute restante" msgstr[1] "%.0f minutes restantes" msgstr[2] "%.0f minutes restantes" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "Mise à jour de %s" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "Version %s" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u jour" msgstr[1] "%u jours" msgstr[2] "%u jours" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "Une mise à jour de micrologiciel est disponible pour %u appareil." msgstr[1] "Une mise à jour de micrologiciel est disponible pour %u appareils." msgstr[2] "Une mise à jour de micrologiciel est disponible pour %u appareils." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u heure" msgstr[1] "%u heures" msgstr[2] "%u heures" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minute" msgstr[1] "%u minutes" msgstr[2] "%u minutes" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u seconde" msgstr[1] "%u secondes" msgstr[2] "%u secondes" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(obsolète)" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "Activation de la mise à jour du micrologiciel" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "Activation de la mise à jour de micrologiciel pour" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Âge" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Alias de %s" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "Une mise à jour nécessite un redémarrage pour se terminer." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "Une mise à jour nécessite que le système soit éteint pour se terminer." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Répondre oui à toutes les questions" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Appliquer la mise à jour même quand ce n'est pas conseillé" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Appliquer les fichiers de mise à jour" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "Application de la mise à jour…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Micrologiciel approuvé :" msgstr[1] "Micrologiciels approuvés :" msgstr[2] "Micrologiciels approuvés :" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Authentification…" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "Authentification requise pour lire les paramètres du BIOS" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "Une authentification est nécessaire pour définir la liste des micrologiciels approuvés" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "Une authentification est nécessaire pour signer les données en utilisant le certificat du client" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Authentification requise pour déverrouiller un périphérique" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Authentification requise pour mettre à jour le micrologiciel sur un périphérique amovible" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Une authentification est nécessaire pour mettre à jour le micrologiciel sur cette machine" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "Rapports automatiques" #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "Envoyer automatiquement à chaque fois ?" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "Mises à jour BIOS délivrées par LVFS ou Windows Update" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "Batterie" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "Version du chargeur d’amorçage" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "Branche" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Annuler" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "Annulé" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "Impossible d'appliquer car la mise à jour dbx a déjà été appliquée." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Modifié" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "Vérifie que l'empreinte cryptographique correspond au micrologiciel" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Somme de contrôle" #. TRANSLATORS: error message msgid "Command not found" msgstr "Commande non trouvée" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "Critique" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "Version actuelle" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Options de débogage" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "Décompression…" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Description" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "Détails" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "Drapeaux de périphérique" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "Identifiant de l'appareil" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Périphérique ajouté :" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Périphérique modifié :" #. TRANSLATORS: emulated means we are pretending to be a different model msgid "Device is emulated" msgstr "L'appareil est émulé" #. TRANSLATORS: device cannot be interrupted, for instance taking a phone call msgid "Device is in use" msgstr "L'appareil est utilisé" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "Le périphérique est verrouillé" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "L'appareil est injoignable" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "Le périphérique est utilisable pendant la durée de la mise à jour" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Périphérique retiré :" #. TRANSLATORS: as in, wired mains power for a laptop msgid "Device requires AC power to be connected" msgstr "L'appareil doit être branché au secteur" #. TRANSLATORS: device does not have a display connected msgid "Device requires a display to be plugged in" msgstr "L'appareil nécessite qu'un écran soit branché" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "Désactivé" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "Ne pas vérifier d'anciennes métadonnées" #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "Ne pas écrire dans la base de données de l'historique" #. TRANSLATORS: success msgid "Done!" msgstr "Terminé !" #. TRANSLATORS: command description msgid "Download a file" msgstr "Télécharger un fichier" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Téléchargement…" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "Durée" #. TRANSLATORS: this device is not actually real msgid "Emulated" msgstr "Émulé" msgid "Enable" msgstr "Activer" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Activé" msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "L'activation de cette fonctionnalité est à vos propres risques, ce qui signifie que vous devrez contacter le fabricant d'origine de votre équipement au sujet de tout problème éventuel causé par ces mises à jour. Seuls les problèmes liés au processus de mise à jour doivent être signalés à $OS_RELEASE:BUG_REPORT_URL$." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Chiffré" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "RAM chiffrée" #. TRANSLATORS: the vendor is no longer supporting the device msgid "End of life" msgstr "Fin de vie" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "Effacer tout l'historique de mise à jour du micrologiciel" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Effacement…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "Quitter après un bref délai" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "Quitter après le chargement du moteur" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "FICHIER" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "Échec" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Échec d'application de la mise à jour" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "Échec de chargement du dbx local" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "Échec de chargement du dbx système" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "Échec de verrouillage" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Echec de l'analyse des paramètres" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "Échec d'analyse du dbx local" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "Échec de validation des contenus ESP" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Nom de fichier" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "Signature de nom de fichier" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "Source de nom de fichier" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Nom de fichier obligatoire" #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "URI de base du micrologiciel" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "Service D-Bus de mise à jour des micrologiciels" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Service de mise à jour de micrologiciel" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "Attestation de micrologiciel" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "Le micrologiciel est déjà bloqué" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "Le micrologiciel n'est pas déjà bloqué" #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "Mises à jour de micrologiciels" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "Drapeaux" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Trouvé" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "Chiffrement complet du disque détecté" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" msgstr[1] "GUIDs" msgstr[2] "GUIDs" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" msgid "Get BIOS settings" msgstr "Récupérer les paramètres du BIOS" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Obtenir la liste des périphériques supportant les mises à jour de micrologiciel" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Obtenir les détails d'un fichier de micrologiciel" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "Le matériel attend d'être rebranché" #. TRANSLATORS: the release urgency msgid "High" msgstr "Haute" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "Identifiant de sécurité de l'hôte :" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "En attente…" #. TRANSLATORS: show the user a generic image that can be themed msgid "Image" msgstr "Image" #. TRANSLATORS: show the user a random image from the internet msgid "Image (custom)" msgstr "Image (customisée)" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "Durée d'installation" msgid "Install signed device firmware" msgstr "Installer le micrologiciel signé du périphérique" msgid "Install signed system firmware" msgstr "Installer le micrologiciel signé du système" msgid "Install unsigned system firmware" msgstr "Installer le micrologiciel non signé du système" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "Installation de la mise à jour du micrologiciel..." #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "Installation sur %s…" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "Périphérique interne" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "Invalide" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "Est en mode bootloader" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "Dernière modification" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "Moins d’une minute restante" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "Licence" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Noyau Linux" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Espace d'échange (swap) Linux" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "Afficher la liste des entrées de la base dbx" #. TRANSLATORS: the plugin was created from a .so object, and was not built-in msgid "Loaded from an external module" msgstr "Chargé depuis un module externe" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "Chargement…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "Verrouillé" #. TRANSLATORS: the release urgency msgid "Low" msgstr "Faible" msgid "MEI version" msgstr "Version MEI" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "Moyenne" #. TRANSLATORS: ask the user to do a simple task which should be translated msgid "Message" msgstr "Message" #. TRANSLATORS: ask the user a question, and it will not be translated msgid "Message (custom)" msgstr "Message (customisé)" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "Signature de métadonnées" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "URI de métadonnées" #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "Version minimum" msgid "Modify daemon configuration" msgstr "Modifiez la configuration du démon" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "Doit redémarrer après une mise à jour" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "Doit redémarrer" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "Nouvelle version" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "Aucune action indiquée !" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "Aucune rétrogradation pour %s" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "Aucun identifiant de micrologiciel trouvé" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Aucun matériel ayant des capacités de mise à jour du micrologiciel n'a été détecté" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "Pas de mise à jour disponible" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "Non trouvé" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "Non pris en charge" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "OK" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "Seules les mises à jour de version sont autorisées" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "CHEMIN" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "Analyse de la mise à jour dbx…" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "Analyse de la base dbx système…" #. TRANSLATORS: remote filename base msgid "Password" msgstr "Mot de passe" msgid "Payload" msgstr "Charge utile" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "En attente" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "Effectuer l'opération ?" #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "Veuillez saisir un nombre de 0 à %u :" #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "Dépendances de greffons manquantes" #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "Version précédente" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Priorité" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "Problèmes" msgid "Proceed with upload?" msgstr "Continuer avec le téléversement ?" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "Propriétaire" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "Lecture depuis %s…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Lecture…" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "Réinstaller le micrologiciel sur un périphérique" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "URI des rapports" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "Système de fichier efivarfs requis non trouvé" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "Le matériel requis n'a pas été trouvé" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Redémarrer maintenant ?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "Redémarrer le service pour rendre la modification effective ?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Redémarrage du périphérique…" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Planification..." #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "Démarrage sécurisé désactivé" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "Démarrage sécurisé activé" #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "Voir %s pour plus d'informations." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Périphérique sélectionné" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Volume sélectionné" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "Numéro de série" msgid "Set one or more BIOS settings" msgstr "Définissez un ou plusieurs paramètres du BIOS" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Définir la liste des micrologiciels approuvés" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "Les paramètres vont s’appliquer quand le système va redémarrer" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "Partager l'historique du micrologiciel avec les développeurs" #. TRANSLATORS: command line option msgid "Show all results" msgstr "Afficher tous les résultats" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Montrer les options de débogage" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Montre des informations de débogage complémentaires" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "Afficher la version calculée de la base dbx" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "Éteindre maintenant ?" msgid "Sign data using the client certificate" msgstr "Signer les données en utilisant le certificat du client" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "Signer les données en utilisant le certificat du client" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "Signer les données envoyées avec le certificat du client" msgid "Signature" msgstr "Signature" #. TRANSLATORS: file size of the download msgid "Size" msgstr "Taille" #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "Source" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "Indiquer le fichier de base de données dbx" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "Succès" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "Tous les périphériques ont été activés avec succès" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "La valeur de configuration a été modifiée avec succès" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "Succès de l'envoi de %u rapport" msgstr[1] "Succès de l'envoi de %u rapports" msgstr[2] "Succès de l'envoi de %u rapports" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Résumé" #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "Pris en charge" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "Le système nécessite une source d'alimentation externe" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "TEXTE" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: we're saving all USB events for emulation msgid "Tagged for emulation" msgstr "Marqué pour émulation" #. show the user the entire data blob msgid "Target" msgstr "Cible" #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "LVFS est un service libre qui opère en tant qu'entité légale indépendante et n'est aucunement connecté à $OS_RELEASE:NAME$. Votre distributeur n'a pas forcément vérifié la compatibilité des mises à jour de micrologiciel avec votre système ou avec les appareils connectés. Tous les micrologiciels ne sont fournis que par le fabricant original de votre équipement." #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "Il n'y a aucun micrologiciel approuvé." #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "Ce paquet n'a pas été validé, il peut ne pas fonctionner correctement." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "Cet outil permet à un administrateur d'appliquer les mises à jour dbx UEFI." #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Type" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "Partition ESP UEFI non détectée ou non configurée" #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "Les mises à jour de capsule UEFI ne sont pas disponibles ou pas activées dans la configuration du micrologiciel" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "Utilitaire dbx UEFI" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "Clé de plate-forme UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "Démarrage sécurisé UEFI" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "Impossible de se connecter au service" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "Déchiffré" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Inconnu" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Appareil inconnu" msgid "Unlock the device to allow access" msgstr "Déverrouillez l'appareil pour autoriser l'accès" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "Déverrouillé" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "Déverrouille le périphérique pour l'accès au micrologiciel" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "Mise à jour possible" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Erreur de mise à jour" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "Message de mise à jour" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "État de mise à jour" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "Cet échec de mise à jour est un problème connu, visitez cette URL pour plus d'informations :" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Mettre à jour maintenant ?" msgid "Updating" msgstr "Mise à jour" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "Mise à jour de %s…" #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "Urgence" #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "L'utilisateur a été notifié" #. TRANSLATORS: remote filename base msgid "Username" msgstr "Nom d’utilisateur" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "Valide" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "Validation des contenus ESP…" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "Variante" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "Fournisseur" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "Vérification…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Version" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "En attente…" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "Écriture du fichier :" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Écriture…" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[SOMME DE CONTRÔLE]" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "Greffons fwupd" fwupd-2.0.10/po/fur.po000066400000000000000000001530431501337203100144720ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Fabio Tomat , 2017-2018,2020,2023-2024 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Friulian (http://app.transifex.com/freedesktop/fwupd/language/fur/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: fur\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "Al mancjie %.0f minût" msgstr[1] "A mancjin %.0f minûts" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "Inzornament dispositîf %s" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "Inzornament di %s" #. TRANSLATORS: the device has a reason it can't update, e.g. laptop lid #. closed #, c-format msgid "%s is not currently updatable" msgstr "Pal moment nol è pussibil inzornâ %s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "Modalitât costrutôr %s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "Override %s" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "Version di %s" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "%u dispositîf al à un inzornament firmware disponibil." msgstr[1] "%u dispositîfs a àn un inzornament firmware disponibil." #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device is not the best known configuration." msgid_plural "%u devices are not the best known configuration." msgstr[0] "%u dispositîf no e la miôr configurazion cognossude." msgstr[1] "%u dispositîfs no son la miôr configurazion cognossude." #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "AMD Firmware Write Protection" msgstr "Protezion de scriture dal firmware AMD" #. TRANSLATORS: command description msgid "Activate devices" msgstr "Ative dispositîfs" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "Ative dispositîfs in spiete" msgid "Activate the new firmware on the device" msgstr "Ative il gnûf firmware sul dispositîf" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "Ativazion inzornament firmware" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "Ativazion dal inzornament firmware par" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Etât" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "Acetâ e abilitâ la sorzint esterne?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Alias a %s" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Permet di tornâ indaûr aes versions di firmware precedentis" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "Permet di tornâ a instalâ lis versions dal firmware esistentis" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "Permet di cambiâ ram dal firmware" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "Un inzornament al à bisugne che si torni a inviâ il computer par finî." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "Un inzornament, par completâsi, al à bisugne che si distudedi il sisteme." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Rispuint di sì a dutis lis domandis" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Apliche l'inzornament ancje cuant che nol è conseât" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Apliche i files di inzornament" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "Daûr a aplicâ l'inzornament…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Firmware aprovât:" msgstr[1] "Firmware aprovâts:" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "Zonte ae modalitât firmware" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Daûr a autenticâ…" #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "I detais de autenticazion a son necessaris" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "La autenticazion e je necessarie par tornâ indaûr ae version precedente dal firmware suntun dispositîf estraibil " #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "La autenticazion e je necessarie par tornâ indaûr ae version precedente dal firmware su cheste machine" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to fix a host security issue" msgstr "E covente la autenticazion par comedâ un probleme di sigurece dal host" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "La autenticazion e je necessarie par modificâ lis impostazions dal BIOS" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "La autenticazion e je necessarie par modificâ une sorzint esterne configurade, doprade pai inzornaments dal firmware" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "La autenticazion e je necessarie par modificâ la configurazion dal demoni" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "La autenticazion e je necessarie par lei lis impostazions BIOS" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to reset daemon configuration to defaults" msgstr "E covente la autenticazion par ripristinâ la configurazion dal demoni ai valôrs predefinîts" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "La autenticazion e je necessarie par stabilî la liste dai firmware aprovâts" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "La autenticazion e je necessarie par firmâ i dâts doprant il certificât dal client" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "La autenticazion e je necessarie par passâ ae gnove version dal firmware" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to undo the fix for a host security issue" msgstr "E covente la autenticazion par disfâ il justament par un probleme di sigurece dal host" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "La autenticazion e je necessarie par sblocâ un dispositîf" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "La autenticazion e je necessarie par inzornâ il firmware suntun dispositîf estraibil" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "La autenticazion e je necessarie par inzornâ il firmware su cheste machine" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "La autenticazion e je necessarie par inzornâ i checksum archiviâts pal dispositîf" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "Inzornaments BIOS dâts fûr vie LVFS o Windows Update" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "Files di firmware blocâts:" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "Firmware blocât:" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "Compile un file di firmware" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * Utilized by OS means the distribution enabled it msgid "CET OS Support" msgstr "Supuart SO CET" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * enabled means supported by the processor msgid "CET Platform" msgstr "Plateforme CET" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Anule" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "Anulât" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "Impussibil aplicâ viodût che l'inzornament dbx al è za stât aplicât." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Modificât" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Checksum" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "Al nete i risultâts dal ultin inzornament" #. TRANSLATORS: error message msgid "Command not found" msgstr "Comant no cjatât" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "Convertìs un file di firmware" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Opzions di debug" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "Daûr a decomprimi…" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Descrizion" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "Variabilis/flags dispositîf" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "ID dispositîf" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Dispositîf zontât:" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Dispositîf modificât:" #. TRANSLATORS: emulated means we are pretending to be a different model msgid "Device is emulated" msgstr "Il dispositîf al è emulât" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "Il dispositîf al è blocât" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "Impussibil rivâ al dispositîf" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Dispositîf gjavât:" #. TRANSLATORS: longer description msgid "Device software updates are provided for this device." msgstr "Su chest dispositîf a son dâts fûr inzornaments." #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "Il dispositîf al apliche i inzornaments a tapis" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "Dispositîfs che a son stâts inzornâts cun sucès:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "Dispositîfs che no son stâts inzornâts ben:" #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "Dispositîfs cence inzornaments firmware disponibii:" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "Disabilitât" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "Al disabilite une sorzint esterne indicade" #. TRANSLATORS: the OS the release was tested on msgid "Distribution" msgstr "Distribuzion" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "No sta controlâ se lis sorzint esternis dal discjariament a àn di sei abilitadis" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "No sta includi il prefìs il domini dal regjistri" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "No sta includi il prefìs date/ore" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "No sta eseguî i controi di sigurece dal dispositîf" #. TRANSLATORS: command line option msgid "Do not prompt for devices" msgstr "No sta domandâ dispositîfs" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "Desideristu inzornâ cheste sorzint esterne cumò?" #. TRANSLATORS: success msgid "Done!" msgstr "Fat!" #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "Puartâ indaûr %s de version %s ae %s?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "Al torne indaûr ae version precedente dal firmware suntun dispositîf" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "Daûr a degradâ di version %s…" #. TRANSLATORS: command description msgid "Download a file" msgstr "Discjame un file" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Daûr a discjariâ…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "Scrîf jù i dâts SMBIOS di un file" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "Durade" #. TRANSLATORS: Title: if we are emulating a different host msgid "Emulated host" msgstr "Host emulât" msgid "Enable" msgstr "Abilite" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "Abilitâ gnove sorzint esterne?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "Abilitâ cheste sorzint esterne?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Abilitât" #. TRANSLATORS: Plugin is active only if hardware is found msgid "Enabled if hardware matches" msgstr "Abilitât se l'hardware al corispuint" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "Al abilite une sorzint esterne indicade" msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "Si abilite cheste funzionalitât a propri pericul, che al significhe che, par ogni probleme causât di chescj inzornaments, si à di contatâ il produtôr origjinâl dal imprest. Dome i problemis che si àn cul sôl procès di inzornament a àn di sei inviâts a $OS_RELEASE:BUG_REPORT_URL$." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "La abilitazion di cheste sorzint esterne e ven fate a to pericul." #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "RAM cifrade" msgid "Encrypted RAM makes it impossible for information that is stored in device memory to be read if the memory chip is removed and accessed." msgstr "La RAM cifrade e fâs in mût che al sedi impussibil lei lis informazions archiviadis te memorie dal dispositîf, se il banc di memorie al ven gjavât e doprât." #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "Scancele dute la cronologjie dai inzornaments dal firmware" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Daûr a scancelâ…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "Jes dopo un piçul ritart" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "Jes dopo che il motôr al à cjariât" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "NONFILE" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Impussibil aplicâ l'inzornament" #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "No si è rivâts a conetisi al demoni" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "Impussibil cjariâ il dbx locâl" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "Impussibil cjariâ il dbx di sisteme" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr " Impussibil blocâ" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "No si è rivâts a analizâ i argoments" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "No si è rivâts a analizâ il file" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "Impussibil analizâ il dbx locâl" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "Impussibil convalidâ i contignûts ESP" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Non file" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "Firme non file" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "Sorzint dal non di file" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Non di file necessari" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware Attestation" msgstr "Attestazion firmware" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "Descritôr BIOS dal firmware" #. TRANSLATORS: longer description msgid "Firmware BIOS Descriptor protects device firmware memory from being tampered with." msgstr "Il Descritôr BIOS dal firmware al protêç des modifichis la memorie firmware dal dispositîf." #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "Regjon BIOS dal firmware" #. TRANSLATORS: longer description msgid "Firmware BIOS Region protects device firmware memory from being tampered with." msgstr "La Regjon BIOS dal firmware e protêç des modifichis la memorie firmware dal dispositîf ." #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "URI de base dal firmware" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "Servizi D-Bus inzornament firmware" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Demoni di inzornament firmware" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "Verifiche dal inzornadôr dal firmware" #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "Inzornaments firmware" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "Utilitât firmware" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "Protezion di scriture dal firmware" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "Bloc di protezion de scriture dal firmware" #. TRANSLATORS: longer description msgid "Firmware Write Protection protects device firmware memory from being tampered with." msgstr "La protezion de scriture dal firmware e protêç des modifichis la memorie dal firmware dai dispositîfs." #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "Attestazion firmware" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "I metadâts dal firmware no son stâts inzornâts par %u zornade e a podaressin jessi vielis." msgstr[1] "I metadâts dal firmware no son stâts inzornâts par %u dîs e a podaressin jessi vielis." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "Inzornaments firmware" #. TRANSLATORS: command line option msgid "Force the action by relaxing some runtime checks" msgstr "Sfuarce la azion smolant cualchi control in timp di esecuzion" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Cjatât" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" msgstr[1] "GUIDs" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" msgid "Get BIOS settings" msgstr "Oten impostazions BIOS" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "Oten dutis lis variabilis/flags supuartadis di fwupd" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Oten ducj i dispositîfs che a supuartin i inzornaments dal firmware" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Al oten detais su un file di firmware" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "Al oten lis sorzint esternis configuradis" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "Al oten la liste dai firmware aprovâts" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "Al oten la liste dai firmware blocâts" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Al oten la liste di inzornaments pal hardware tacât" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "Al oten lis publicazions par un dispositîf" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Al oten i risultâts dal ultin inzornament" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "Protezion IOMMU" #. TRANSLATORS: longer description msgid "IOMMU Protection prevents connected devices from accessing unauthorized parts of system memory." msgstr "La Protezion IOMMU e impedìs ai dispositîfs colegâts di acedi a parts no autorizadis de memorie dal sisteme." #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "In polse…" msgid "Install old version of signed system firmware" msgstr "Instale la version vecje dal firmware di sisteme firmât" msgid "Install old version of unsigned system firmware" msgstr "Instale la version vecje dal firmware di sisteme cence firme" msgid "Install signed device firmware" msgstr "Instasle firmware di dispositîf firmât" msgid "Install signed system firmware" msgstr "Instale firmware di sisteme firmât" msgid "Install unsigned device firmware" msgstr "Instale firmware di dispositîf cence firme" msgid "Install unsigned system firmware" msgstr "Instale firmware di sisteme cence firme" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "Daûr a instalâ l'inzornament dal firmware…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "Daûr a instalâ su %s…" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM Protected" msgstr "ACM protet di Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "ACM protet di Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard Error Policy" msgstr "Politiche sui erôrs di Intel BootGuard" msgid "Intel BootGuard Error Policy ensures the device does not continue to start if its device software has been tampered with." msgstr "La Politiche sui erôrs di Intel BootGuard e garantìs che il dispositîf nol continui a inviâsi se il software dal dispositîf al è stât modificât." #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "Fusibil OTP di Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard Verified Boot" msgstr "Inviament verificât di Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Politiche di erôr di Intel BootGuard" #. TRANSLATORS: longer description msgid "Intel BootGuard prevents unauthorized device software from operating when the device is started." msgstr "Intel BootGuard al impedìs al software di dispositîfs no-autorizâts dal operâ cuant che il dispositîf al è inviât." #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Inviament verificât di Intel BootGuard" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Manufacturing Mode" msgstr "Modalitât di produzion dal Intel Management Engine" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the "override" is #. enabled #. * with a jumper -- luckily it is probably not accessible to end users on #. consumer #. * boards msgid "Intel Management Engine Override" msgstr "Intel Management Engine Override" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Version" msgstr "Version dal Intel Management Engine" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "Dispositîf interni" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "Argoments no valits" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "Al è pussibil puartâ indaûr di version" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "Al mancje mancul di un minût" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "Lockdown dal kernel Linux" #. TRANSLATORS: longer description msgid "Linux Kernel Lockdown mode prevents administrator (root) accounts from accessing and changing critical parts of system software." msgstr "La modalitât Lokdown dal Kernel Linux e impedìs ai accounts dai aministradôrs (root) di acedi e cambiâ parts critichis dal software di sisteme." msgid "Linux Kernel Swap temporarily saves information to disk as you work. If the information is not protected, it could be accessed by someone if they obtained the disk." msgstr "Il Swap dal kernel Linux, vâl a dî la memorie di scambi, e salve in mût temporani informazions su disc intant che tu lavoris. Se lis informazions no son protetis, cualchidun al podarès acedi a chestis informazions se al oten il disc." #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "Verifiche dal kernel Linux" msgid "Linux Kernel Verification makes sure that critical system software has not been tampered with. Using device drivers which are not provided with the system can prevent this security feature from working correctly." msgstr "La verifiche dal kernel Linux e garantìs che il software critic di sisteme nol sedi modificât. Se a vegnin doprâts dirvers di dispositîf che no son stâts dâts fûr cul sisteme, al è pussibil impedî di lavorâ ben a cheste funzionalitât di sigurece." #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "Swap di Linux" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Servizi Firmware dal vendidôr di Linux (firmware stabil)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Servizi Firmware dal vendidôr di Linux (firmware di prove)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Kernel Linux" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Lockdown dal kernel Linux" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Swap di Linux" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "Vôs de liste in dbx" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "Liste i gjenars di firmware disponibii" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "Daûr a cjariâ…" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refer #. * to the private/public key used to secure loading of firmware msgid "MEI key manifest" msgstr "Manifest de clâf MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "Modalitât costrutôr MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "Override MEI" msgid "MEI version" msgstr "Version MEI" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "Firme metadâts" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "URI metadata" #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "Version minime" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "Al modifiche un valôr di configurazion dal demoni" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "Al modifiche une sorzint esterne indicade" msgid "Modify a configured remote" msgstr "Modifiche une sorzint esterne configurade" msgid "Modify daemon configuration" msgstr "Modifiche la configurazion dal demoni" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "Monitore il demoni pai events" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr " Al necessite di tornâ a inviâ il sisteme" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "Gnove version" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "Nissune azion specificade!" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "Nissune degradazion di version par %s" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "Nissun ID di firmware cjatât" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Nissun hardware rilevât un funzionalitâts di inzornament dal firmware" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "Nissune publicazion disponibile" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "In chest moment no je abilitade nissune sorzint esterne duncje nissun metadât al è disponibil." #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "Nissune sorzint esterne disponibile" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "Nissun dispositîf che si pues inzornâ" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "Nissun inzornament disponibil" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "No cjatât" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "Va ben" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "Va ben!" #. TRANSLATORS: the firmware old version msgid "Old version" msgstr "Version vecje" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "Mostre dome il valôr PCR" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "Passe parsore al valôr dal percors ESP predefinît" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "PERCORS" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "Analize e mostre i detais in merit a un file di firmware" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "Daûr a analizâ l'inzornament di dbx…" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "Daûr a analizâ il dbx di sisteme…" #. TRANSLATORS: remote filename base msgid "Password" msgstr "Password" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "Debug de plateforme" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "Debug de plateforme" #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "Inserìs un numar di 0 a %u: " #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA Protection" msgstr "Protezion DMA di pre-inviament" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "Protezion DMA pre-inviament" #. TRANSLATORS: longer description msgid "Pre-boot DMA protection prevents devices from accessing system memory after being connected to the computer." msgstr "La protezion DMA di pre-inviament e impedìs ai dispositîfs di acedi ae memorie di sisteme dopo che si son colegâts al computer." #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Prioritât" msgid "Proceed with upload?" msgstr "Procedi cul inviament?" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Processor Security Checks" msgstr "Controi di sigurece dal processôr" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "Processor rollback protection" msgstr "Protezion di ripristinament dal processôr" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "Lei di un dispositîf un file binari di firmware" #. TRANSLATORS: command description msgid "Read a firmware from a device" msgstr "Lei di un dispositîf un firmware" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "Daûr a lei di %s…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Daûr a lei…" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "Inzorne i metadâts dal servidôr esterni" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "Tornâ a instalâ %s %s? " #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "Torne instale il firmware atuâl sul dispositîf" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "Torne instale un firmware suntun dispositîf" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "ID publicazion" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "ID esterni" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "URI segnalazion" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "Segnalât su servidôr esterni" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Al à bisugne de conession a internet" msgid "Reset daemon configuration" msgstr "Ristabilìs configurazion dal demoni" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Tornâ a inviâ cumò?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "Tornâ a inviâ il demoni par rindi efetive la modifiche?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Daûr a tornâ a inviâ il dispositîf…" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "Torne ducj i ID dal hardware pe machine" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "SMAP" msgstr "SMAP" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "Descritôr dal BIOS SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "Regjon BIOS SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "Bloc SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "Scriture SPI" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "SPI write protection" msgstr "Protezion de scriture SPI" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Daûr a planificâ…" #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "Viôt %s par vê plui informazions." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Dispositîf selezionât" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Volum selezionât" msgid "Set one or more BIOS settings" msgstr "Configure une o plui impostazion BIOS" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Al stabilìs la liste dai firmware aprovâts" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "Lis impostazions a vignaran aplicadis daspò che il sisteme si tornarà a inviâ" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "Condivît la conologjie dai firmware cui svilupadôrs" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "Mostre versions di client e demoni" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "Mostre lis informazions prolissis dal demoni par un domini particolâr" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "Mostre lis informazions di debug par ducj i dominis" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Mostre opzions di debug" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Mostre i dispositîfs che no si puedin inzornâ" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Mostre informazions di debug adizionâls" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "Mostre la cronologjie dai inzoronaments dal firmware" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "Mostre la version calcolade dal dbx" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "Distudâ cumò?" msgid "Sign data using the client certificate" msgstr "Firme i dâts doprant il certificât dal client" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "Firme i dâts doprant il certificât dal client" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "Firme i dâts cjamâts in rêt cul certificât dal client" msgid "Signature" msgstr "Firme" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "Specifiche il file de base di dâts dal dbx" #. TRANSLATORS: The BIOS setting accepts strings msgid "String" msgstr "Stringhe" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "Ducj i dispositîfs a son stâts ativâts cun sucès" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "Sorzint esterne disabilitade cun sucès" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "Gnûf metadât discjariât cun sucès:" #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "Sorzint esterne inzornade e abilitade cun sucès" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "Sorzint esterne abilitade cun sucès" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "Firmware instalât cun sucès" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "Valôr di configurazion modificât cun sucès" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "Sorzint modificade cun sucès" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "Metadât inzornât a man cun sucès" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "Sumis di control dai dispositîfs inzornadis cun sucès" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "%u rapuart inviât cun sucès" msgstr[1] "%u rapuarts inviâts cun sucès" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "Sumis di control dai dispositîfs verificadis cun sucès" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Sintesi" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "CPU supuartade" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "Supuartât su servidôr esterni" #. TRANSLATORS: Title: a better sleep state msgid "Suspend To Idle" msgstr "Sospension su inativitât" #. TRANSLATORS: Title: sleep state msgid "Suspend To RAM" msgstr "Sospension su RAM" #. TRANSLATORS: longer description msgid "Suspend to Idle allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Sospension su inativitât al permet al dispositîf di lâ in polse subite, cussì di sparagnâ energjie. Intant che il dispositîf al è sospindût, al è pussibil gjavâ in mût fisic il banc di memorie, e chest doprât par acedi aes informazions." #. TRANSLATORS: longer description msgid "Suspend to RAM allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "La sospension su RAM e permet al dispositîf di lâ in polse subite, cussì di sparagnâ energjie. Intant che il dispositîf al è sospindût, al è pussibil gjavâ in mût fisic il banc di memorie, e chest doprât par acedi aes informazions." #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "Suspend-to-idle" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "Suspend-to-ram" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "Cambiâ ram di %s a %s? " #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "Cambie il ram dal firmware sul dispositîf" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "Ricostruzion di PCR0 dal TPM" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM Platform Configuration" msgstr "Configurazion de plateforme TPM" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM Reconstruction" msgstr "Ricostruzion dal TPM" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM empty PCRs" msgstr "PCRs vueits di TPM" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "Etichete" msgstr[1] "Etichetis" #. show the user the entire data blob msgid "Target" msgstr "Destinazion" #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "Il LVFS — Servizi firmware dal vendidôr di linux — al è un servizi gratuit che al opere come entitât legâl indipendente e no à conession cun $OS_RELEASE:NAME$. Il to distributôr al podarès no vê verificât la compatibilitât di nissun dai inzornaments firmware cul vuestri sisteme o cui dispositîfs tacâts. Ducj i firmware a son furnîts dome dal produtôr origjinâl dal imprest." #. TRANSLATORS: longer description msgid "The UEFI Platform Key is used to determine if device software comes from a trusted source." msgstr "La clâf de plateforme UEFI e ven doprade par determinâ se il software dal dispositîf al rive di une sorzint afidabile." #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "No'nd è nissun file di firmware blocât" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "No'nd è nissun firmware aprovât." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "Chest program al pues lavorâ in maniere juste dome come root" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "Cheste sorzint esterne e conten firmware che no son sot di embargo, ma a son ancjemò in prove dal vendidôr dal hardware. Tu varessis di sigurâti di vê une maniere par puartâ indaûr a man il firmware ae version precedente, se l'inzornament al falìs." #. TRANSLATORS: error message msgid "This system doesn't support firmware settings" msgstr "Chest sisteme nol supuarte lis configurazions dai firmwares" #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "Chest imprest al permet a un aministradôr di aplicâ i inzornaments dbx UEFI." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "Chest strument al leiarà e al analizarà il regjistri events TPM dal firmware di sisteme." #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Gjenar" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "Clâf de plateforme UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "Inviament sigûr di UEFI" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI bootservice variables" msgstr "Variabilis dal servizi di inviament UEFI" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "Utilitât dbx di UEFI" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "Clâf de plateforme UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "Inviament sigûr di UEFI" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "Impussibil conetisi al servizi" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "Firmware sblocât:" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "No cognossût" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Dispositîf no cognossût" msgid "Unlock the device to allow access" msgstr "Sbloche il dispositîf par permeti l'acès" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "Al sbloche il dispositîf pal acès al firmware" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "Version %s dal demoni no supuartade, la version dal client e je la %s" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "Si pues inzornâ" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Erôr inzornament" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "Imagjin di inzornament" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "Stât inzornament" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "Il faliment dal inzornament al è un probleme cognossût, visite chest URL par vê plui informazions:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Inzornâ cumò?" msgid "Update the stored device verification information" msgstr "Inzorne lis informazions di verifiche dal dispositîf archiviadis" msgid "Updating" msgstr "Daûr a inzornâ" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "Daûr a inzornâ %s…" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "Inzornâ %s de version %s ae %s?" #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "Inviâ i rapuarts dal firmware al jude i vendidôrs di hardware a identificâ subite i inzornaments bogns e falimentârs sui dispositîfs reâi." #. TRANSLATORS: remote filename base msgid "Username" msgstr "Non utent" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "Daûr a convalidâ i contignûts ESP…" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "Daûr a verificâ…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Version" #. TRANSLATORS: the fwupd version the release was tested on msgid "Version[fwupd]" msgstr "Version[fwupd]" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "ATENZION" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "In spiete…" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "Daûr a scrivi il file:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Daûr a scrivi…" #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "Il to distributôr al podarès no vê verificât nissun dai inzornaments firmware pe compatibilitât cui dispositîfs tacâts o cul to sisteme." #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "Utilitât di regjistrazion events TPM di fwupd" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "plugins di fwupd" fwupd-2.0.10/po/gl.po000066400000000000000000000570001501337203100142740ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Fran Diéguez , 2020 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Galician (http://app.transifex.com/freedesktop/fwupd/language/gl/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: gl\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] " Falta %.0f minuto" msgstr[1] " Faltan %.0f minutos" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "Modo de fabricación %s" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "Activar os dispositivos pendentes" msgid "Activate the new firmware on the device" msgstr "Activar o novo firmware no dispositivo" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "Aceptar e activar o remoto?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Alias a %s" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Permitir a desactualización de versións de firmware" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "Permitir a reinstalación de versións de firmware existentes" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Aplicar actualización incluso cando non se avise" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Aplicar ficheiros de actualización" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "Aplicando actualización…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Firmware aprovados:" msgstr[1] "Firmware aprovado:" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "Anexarse ao modo de firmware" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Autenticando…" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Requírese autenticación para desactualizar o firmware nun dispositivo extraíbel" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Requírese autenticación para desactualizar o firmware nesta máquina" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "Requírese autenticación para modificar a configuración do remoto usado para actualizar firmwares" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "Requírese autenticación para modificar a configuración do demonio" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "Requírese autenticación para estabelecer a lista do firmware aprovado" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "Requírese autenticación para asinar os datos usando o certificado do cliente" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "Requírese autenticación para trocar a unha nova versión do firmware" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Requírese autenticación para desbloquear un dispositivo" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Requírese autenticación para actualizar o firmware nun dispositivo extraíbel" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Requírese autenticación para actualizar o firmware nesta máquina" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "Requírese autenticación para actualizar as sumas de verificación para o dispositivo" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Cancelar" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "Cancelado" #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Cambiado" #. TRANSLATORS: error message msgid "Command not found" msgstr "Orde non atopada" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "Converter un ficheiro de firmware" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Opcións de depuración" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "Descomprimindo…" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "Desanexarse ao modo de firmware" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Dispositivo engadido:" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Dispositivo cambiado" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Dispositivo retirado:" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "Dispositivos que foron actualizados con éxito:" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "Desactivado" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "Non incluír o dominio do rexistro" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "Non incluír o prefixo de marca de tempo" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "Non levar a cabo comprobacións de dispositivo" #. TRANSLATORS: success msgid "Done!" msgstr "Feito!" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "Desactualizando %s" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Descargando…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "Volcar os datos da SMBIOS desde un ficheiro " #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "Desexa activar este remoto?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Activad" msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "Active esta funcionalidade baixo o seu risco, o que significa que ten que contactar co seu fabricante de equipamento orixinal se ten calquera problema con estas actualizacións. Só os problemas co proceso de actualización en si deberían enviarse en $OS_RELEASE:BUG_REPORT_URL$." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Cifrad" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "RAM cifrada" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Borrando…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "Saír despois dun pequeno atraso" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "Saír despois de que se cargue o motor" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "Fallido" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Produciuse un fallo ao aplicar a actualización" #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "Produciuse un fallo ao conectarse ao demoni" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "Produciuse un fallo ao cargar o dbx local" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "Produciuse un fallo ao cargar o dbx do sistema" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Produciuse un fallo ao analizar os argumentos" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "Produciuse un fallo ao analizar o ficheiro" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "Produciuse un fallo ao analizar a dbx local" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "Produciuse un fallo ao validar os contidos do ESP" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Requírese un nome de ficheiro" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "Servizo D-Bus da Actualización do Firmware" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Demonio de Actualización de Firmware" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "Utilidade de Firmware" #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "Actualizacións de firmware" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Atopado" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "Obter todas as bandeiras de dispositivos admitidos por fwupd" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Obtén todos os dispositivos que admiten actualizacións de firmware" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "Obter todos os engadidos activos rexistrados no sistema" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Obtén a información sobre o ficheiro de firmware" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "Obten os remotos configurados" #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "Obtén os atributos de seguranza do equipo" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Obtén a lista de actualizacións para o hardware conectado" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "ID de seguranza do equipo:" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "Ocioso…" msgid "Install signed device firmware" msgstr "Instalar firmware de dispositivo asinado" msgid "Install signed system firmware" msgstr "Instalar firmware do sistema asinado" msgid "Install unsigned device firmware" msgstr "Instalar firmware de dispositivo non asinado" msgid "Install unsigned system firmware" msgstr "Instalar firmware do sistema non asinado" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "Instalando actualización do firmware…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "Instalando en %s…" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "ACM protexido Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Política de erro de Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Arrinque verificado de Intel BootGuard" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "Non válido" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "Falta menos dun minuto" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Servizo de Linux Vendor Firmware (firmware estábel)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Servizo de Linux Vendor Firmware (firmware de probas)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Núcleo de Linux" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Bloqueo de kernel de Linux" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Swap de Linux" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "Mostrar as entradas no dbx" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "Lista todos os tipos de firmware dispoñíbeis" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "Cargando…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "Bloqueado" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "Modo de fabricación MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "Omitir MEI" msgid "MEI version" msgstr "Versión do MEI" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "Activar manualmente engadidos específicos" msgid "Modify a configured remote" msgstr "Modificar un remoto configuración" msgid "Modify daemon configuration" msgstr "Modificación configuración do demonio" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "Monitorizar eventos no demonio" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "Non se require ningunha acción!" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "Non se atoparon IDs de firmware" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "Non hai publicacións dispoñíbeis" #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "Non hai remotos dispoñíbeis" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "Non atopado" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "Non compatíbel" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "OK" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "Mostrar só un valor de PCR" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "Sobrescribir a ruta predefinida do ESP" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "Analixar e mostrar a información dun ficheiro de firmware" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "Analizando a dbx de actualizacións…" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "Analizando o dbx do sistema…" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "Protección de DMA pre-arrinque" msgid "Proceed with upload?" msgstr "Desexa seguir coa subida?" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "Ler un blob de firmware desde un dispositivo" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "Lendo de %s…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Lendo…" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "Actualiza os metadatos desde un servidor remoto" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "Reinstalar firmware nun dispositivo" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Require conexión a internet" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "Desexa reiniciar o demonio para facer o cambio efectivo?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Reiniciando dispositivo…" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "Devolve todos os IDs do hardware da máquina" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "Rexión da BIOS Do SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "Bloqueo do SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "Escritura do SPI" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Planificando…" #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Seleccionar un dispositivo" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Volume seleccionado" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Estabelece a lista do firmware aprobado" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "Mostrar as versións de cliente e demonio" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Mostrar opcións de depuración" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Mostrar os dispositivos que non son actualizábeis" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Mostrar información de depuración adicional" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "Mostrar o historial das actualizacións de firmware" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "Mostrar a versión calculada do dbx" msgid "Sign data using the client certificate" msgstr "Asinar os datos usando o certificafo do cliente" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "Asinar os datos usando o certificafo do cliente" msgid "Signature" msgstr "Sinatura" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "Especificar o ficheiro de base de datos dbx" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "Remoto desactivado correctamente" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "Remoto activado correctamente" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "Instalación do firmware exitosa" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "Remoto modficado correctamente" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "Comprobáronse correctamente as sumas de verificación do dispositivo" #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "Compatíbel" #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "Suspend-a-ocioso" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "Suspender-a-ram" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "Reconstrución do PCR0 de TPM" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. show the user the entire data blob msgid "Target" msgstr "Obxectivo" #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "O LVFS é o servizo que opera como unha entidade legal independente e non ten ligazón con $OS_RELEASE:NAME$. O seu distribuidor podería non ter que comprobar se as actualizacións de firmware teñen compatibilidade co seu sistema ou dispositivos conectados. Todos os firmware son fornecidos por fabricantes de equipamento orixinal." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "Este programa podería funcionar correctamente só como root" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "Utilidade UEFI dbx" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "Arrinque seguro de UEFI" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "Descifrado" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Descoñecido" msgid "Unlock the device to allow access" msgstr "Desbloquear o dispositivo para permitir o acceso" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "Desbloqueado" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Actualizar agora" msgid "Update the stored device verification information" msgstr "Actualizar a información de verificación almacenada no dispositivo" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "Actualizaar os metadatos almacenados cos contidos actuais" msgid "Updating" msgstr "Actualizando" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "Actualizando %s…" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "Válido" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "Validando os contidos do ESP…" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "Verificando…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Versión" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "Agardando…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "Facer seguimento dos cambios de hardware" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Escribindo…" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "Utilidade de rexistro de eventos de TPM de fwupd" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "Engadidos de fwupd" fwupd-2.0.10/po/he.po000066400000000000000000001555231501337203100142770ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # dhead666 , 2015 # gk , 2015 # 63f334ffc0709ba0fc2361b80bf3c0f0_00ffd1e , 2021 # Yaron Shahrabani , 2021,2023-2024 # Yosef Or Boczko , 2023 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Hebrew (http://app.transifex.com/freedesktop/fwupd/language/he/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: he\n" "Plural-Forms: nplurals=3; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: 2;\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "נותרה דקה %.0f" msgstr[1] "נותרו %.0f דקות" msgstr[2] "נותרו %.0f דקות" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "עדכון סוללה %s" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "עדכון מצלמה %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "עדכון הגדרות %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "עדכון התקן %s" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "עדכון בקר משובץ של %s" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "עדכון מקלדת %s" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "עדכון עכבר %s" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "עדכון מנשק רשת %s" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "עדכון בקר ×חסון %s" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "עדכון מערכת %s" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "עדכון TPM %s" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "עדכון משטח מגע %s" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "עדכון %s" #. TRANSLATORS: warn the user before updating, %1 is a device name #, c-format msgid "%s and all connected devices may not be usable while updating." msgstr "%s וכל ×”×”×ª×§× ×™× ×”×ž×—×•×‘×¨×™× ×¢×©×•×™×™× ×œ× ×œ×”×™×•×ª ×©×ž×™×©×™× ×‘×¢×ª העדכון." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "מצב ייצור של %s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "גרסת %s" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "×™×•× %u" msgstr[1] "×™×•×ž×™×™× (%u)" msgstr[2] "%u ימי×" #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "שעה %u" msgstr[1] "×©×¢×ª×™×™× (%u)" msgstr[2] "%u שעות" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "דקה %u" msgstr[1] "%u דקות" msgstr[2] "%u דקות" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "שנייה %u" msgstr[1] "%u שניות" msgstr[2] "%u שניות" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(×œ× ×ª×§×£ עוד)" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "פעולה נדרשת:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "הפעלת התקני×" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "×”×”×ª×§× ×™× ×”×ž×ž×ª×™× ×™× ×ž×•×¤×¢×œ×™×" msgid "Activate the new firmware on the device" msgstr "הפעלת קושחה חדשה בהתקן" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "עדכון הקושחה מופעל" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "עדכון הקושחה מופעל עבור" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "גיל" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "×œ×”×¡×›×™× ×•×œ×”×¤×¢×™×œ ×ת המרוחק?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "כינוי עבור %s" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "כל ×”×”×ª×§× ×™× ×ž×ותו הסוג יעודכנו ב×ותו הזמן" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "ל×פשר שנמוך גרס×ות קושחה" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "ל×פשר להתקין מחדש גרס×ות קושחה קיימות" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "ל×פשר מעבר ענפי קושחה" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "נדרשת הפעלה מחדש להשלמת עדכון." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "נדרש כיבוי המערכת כדי ×œ×”×©×œ×™× ×¢×“×›×•×Ÿ." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "לענות כן על כל הש×לות" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "החלת עדכון ×פילו ×× ×œ× ×ž×•×ž×œ×¥" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "החלת קובצי עדכון" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "העדכון חל…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "קושחה מ×ומתת:" msgstr[1] "קושחה מ×ומתת:" msgstr[2] "קושחה מ×ומתת:" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "הצמדה למצב קושחה" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "מתבצע ×ימות…" #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "פרטי ×ימות נחוצי×" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "נדרש ×ימות כדי לשנמך חומרה על התקן נשלף" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "נדרש ×ימות כדי לשנמך ×ת הקושחה במכונה ×”×–×ת" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to fix a host security issue" msgstr "נדרש ×ימות כדי לתקן תקלות ×בטחה במ×רח" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to load hardware emulation data" msgstr "נדרש ×ימות כדי לטעון נתוני הדמיית התקן" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "נדרש ×ימות כדי לשנות הגדרות BIOS" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "נדרש ×ימות כדי לשנות מרוחק מוגדר לעדכוני קושחה" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "נדרש ×ימות לשינוי הגדרות הסוכן" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "נדרשת ×ימות על מנת ×œ×§×¨×•× ×”×’×“×¨×•×ª BIOS" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to reset daemon configuration to defaults" msgstr "נדרש ×ימות כדי ל×פס ×ת הגדרות הסוכן לברירות המחדל" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "נדרש ×ימות כדי להגדיר ×ת רשימת הקושחות המותרות" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "נדרש ×ימות כדי ×œ×—×ª×•× × ×ª×•× ×™× ×‘×מצעות ×ישור לקוח" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to stop the firmware update service" msgstr "נדרש ×ימות כדי לעצור ×ת שירות עדכון הקושחה" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "נדרש ×ימות כדי לעבור לגרסת הקושחה החדשה" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to undo the fix for a host security issue" msgstr "נדרש ×ימות כדי לבטל ×ת התיקון לתקלת ×בטחת המ×רח" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "נדרש ×ימות כדי לשחרר התקן" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "נדרש ×ימות כדי לעדכן חומרה על התקן נשלף" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "×ימות משתמש נדרש לעדכון קושחה מערכת זו" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "נדרש ×ימות כדי לעדכן ×ת סכומי הביקורת עבור ההתקן" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "דיווח ×וטומטי" #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "להעלות ×וטומטית בכל פע×?" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "עדכוני ×”Ö¾BIOS ×ž×•×¢×‘×¨×™× ×“×¨×š LVFS ×ו Windows Update" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "×יגוד למנהל התקן ליבה חדש" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "קובצי קושחה חסומי×:" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "קושחה חוסמת:" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "גרסת מנהל טעינה" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "×¢× ×£" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "בניית קובץ קושחה" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "ביטול" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "בוטל" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "×œ× × ×™×ª×Ÿ להחיל כיוון שכבר חל עדכון dbx." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "השתנה" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "בודק שהגיבוב הקריפטוגרפי תו×× ×œ×§×•×©×—×”" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "×¡×›×•× ×‘×™×§×•×¨×ª" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "בחירת התקן" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "בחירת קושחה" #. TRANSLATORS: error message msgid "Command not found" msgstr "פקודה ×œ× × ×ž×¦××”" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "המרת קובץ קושחה" #. TRANSLATORS: when the update was built msgid "Created" msgstr "מועד יצירה" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "קריטי" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "גרסה נוכחית" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "×פשרויות ניפוי שגי×ות" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "מתבצעת פריסה…" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "תי×ור" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "ניתוק למצב מנהל טעינה" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "פרטי×" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "דגלוני התקן" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "מזהה התקן" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "נוסף התקן:" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "השתנה התקן:" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "ההתקן נעול" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "ההתקן ×ינו נגיש" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "הוסר התקני×:" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "עדכון ההתקן דורש הפעלה" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "×”×ª×§× ×™× ×©×¢×•×“×›× ×• כר×וי:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "×”×ª×§× ×™× ×©×œ× ×¢×•×“×›× ×• כר×וי:" #. TRANSLATORS: message letting the user there is an update #. * waiting, but there is a reason it cannot be deployed msgid "Devices with firmware updates that need user action: " msgstr "×”×ª×§× ×™× ×¢× ×¢×“×›×•× ×™ קושחה ×©×¦×¨×™×›×™× ×”×ª×¢×¨×‘×•×ª משתמש:" #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "×”×ª×§× ×™× ×œ×œ× ×¢×“×›×•× ×™ קושחה זמיני×:" #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "×”×ª×§× ×™× ×¢× ×’×¨×¡×ª הקושחה העדכנית ביותר שזמינה:" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "מושבת" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "משבית מרוחק מסוי×" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "×œ× ×œ×‘×“×•×§ נתוני על ישני×" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "×œ× ×œ×‘×“×•×§ היסטוריה ×œ× ×ž×“×•×•×—×ª" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "×œ× ×œ×‘×“×•×§ ×× ×”×ž×¨×•×—×§×™× ×©×”×ª×§×‘×œ×• ××ž×•×¨×™× ×œ×”×™×•×ª מופעלי×" #. TRANSLATORS: command line option msgid "Do not check or prompt for reboot after update" msgstr "×œ× ×œ×‘×“×•×§ ×ו לבקש הפעלה מחדש ל×חר עדכון" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "×œ× ×œ×›×œ×•×œ קידומת חותמת זמן" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "×œ× ×œ×‘×¦×¢ בדיקות בטיחות על ההתקן" #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "×œ× ×œ×›×ª×•×‘ למסד ×”× ×ª×•× ×™× ×©×œ ההיסטוריה" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "×”×× ×”×©×œ×›×•×ª מעבר ×¢× ×£ הקושחה ברורות לך?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "להשבתי ×ת היכולת ×”×–×ת ×œ×¢×“×›×•× ×™× ×¢×ª×™×“×™×™×?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "לרענן ×ת המרוחק ×”×–×” כעת?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "להעלות ×ת הדוחות ×וטומטית ×œ×¢×“×›×•× ×™× ×¢×ª×™×“×™×™×?" #. TRANSLATORS: success msgid "Done!" msgstr "הסתיי×!" #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "לשנמך ×ת %s מגרסה %s ×ל %s?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "משנמך קושחה על התקן" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "%s משונמך…" #. TRANSLATORS: command description msgid "Download a file" msgstr "הורדת קובץ" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "מתבצעת הורדה…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "משיכת נתוני SMBIOS מקובץ" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "משך" msgid "Enable" msgstr "הפעלה" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "להפעיל מרוחק חדש?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "להפעיל ×ת המרוחק ×”×–×”?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "מופעל" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "מפעיל מרוחק מסוי×" msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "הפעלת יכולת ×–×ת ×”×™× ×¢×œ ×חריותך בלבד, כלומר שעליך ליצור קשר ×¢× ×¡×¤×§ הציוד המקורי שלך בנוגע לתקלות שנגרמות על ידי ×”×¢×“×›×•× ×™× ×”×לה. רק תקלות בתהליך העדכון עצמו ×מורות להיות מתועדות ×צל $OS_RELEASE:BUG_REPORT_URL$." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "הפעלת מרוחק ×–×” ×”×™× ×¢×œ ×חריותך בלבד." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "מוצפן" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "זיכרון מוצפן" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "למחוק ×ת כל היסטוריית עדכוני הקושחה" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "מתבצעת מחיקה…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "יצי××” ל×חר השהייה קצרה" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "יצי××” ל×חר טעינת מנוע התכנה" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "×™×™×¦×•× ×ž×‘× ×” קובצי הקושחה ל־XML" #. TRANSLATORS: command description msgid "Extract a firmware blob to images" msgstr "חילוץ מקטע בינרי מהקושחה לקובצי דמות" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "ש×-קובץ" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "נכשל" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "החלת העדכון נכשלה" #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "החיבור לסוכן נכשל" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "טעינת ×”Ö¾dbx המקומי נכשלה" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "טעינת ×”Ö¾dbx של המערכת נכשלה" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "הנעילה נכשלה" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr " פענוח ×”××¨×’×•×ž× ×˜×™× × ×›×©×œ" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "ניתוח הקובץ נכשל" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "פענוח ×”Ö¾dbx המקומי נכשל" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "×ימות תוכן ×”Ö¾ESP נכשל" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "×©× ×§×•×‘×¥" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "חתימת ×©× ×§×•×‘×¥" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "מקור ×©× ×§×•×‘×¥" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "נדרש ×©× ×§×•×‘×¥" #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "כתובת בסיס קושחה" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "שירות D-Bus עדכון קושחה" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "שדון עדכון קושחה" #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "עדכוני קושחה" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "כלי קושחה" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "הקושחה כבר חסומה" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "הקושחה ××™× ×” חסומה כבר" #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "עדכוני קושחה" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "דגלי×" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "נמצ×" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "מזהה-קבוצה" msgid "Get BIOS settings" msgstr "משיכת הגדרות ×”Ö¾BIOS" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "מקבל ×ת כל דגלוני ×”×”×ª×§× ×™× ×©× ×ª×ž×›×™× ×¢×œ ידי fwupd" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "מציג כל ×”×ž×›×©×™×¨×™× ×”×ª×•×ž×›×™× ×‘×¢×“×›×•× ×™ קושחה" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "קבלת כל ×”×ª×•×¡×¤×™× ×”×¤×¢×™×œ×™× ×©× ×¨×©×ž×• במערכת" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "מציג ×¤×¨×˜×™× ×ודות קובץ קושחה" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "מושכת ×ת ×”×ž×¨×•×—×§×™× ×”×ž×•×’×“×¨×™×" #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "מושך ×ת מ×פייני ×”×בטחה של המ×רח" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "מקבל ×ת רשימת הקושחות שעברו ×ישור" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "מקבל ×ת רשימת הקושחות החסומות" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "מקבל ×ת רשימת ×”×¢×“×›×•× ×™× ×œ×—×•×ž×¨×” שמחוברת" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "מקבל ×ת התוצ×ות מהעדכון ×”×חרון" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "החומרה ממתינה לחיבור מחדש" #. TRANSLATORS: the release urgency msgid "High" msgstr "גבוה" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "מזהה ×בטחת מ×רח:" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "הגנת IOMMU" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "בהמתנה…" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "התעלמות מבדיקות מחמירות של SSL בעת הורדת קבצי×" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "התעלמות מכשלונות ×¡×™×›×•× ×‘×™×§×•×¨×ª של קושחות" #. TRANSLATORS: command line option msgid "Ignore firmware hardware mismatch failures" msgstr "התעלמות מכשלי הת×מה בין קושחה לחומרה" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "משך התקנה" msgid "Install old version of signed system firmware" msgstr "התקנת גרסה ישנה של קושחת מערכת חתומה" msgid "Install old version of unsigned system firmware" msgstr "התקנת גרסה ישנה של קושחת מערכת ×©×œ× × ×—×ª×ž×”" msgid "Install signed device firmware" msgstr "התקנת קושחת התקן חתומה" msgid "Install signed system firmware" msgstr "התקנת קושחת מערכת חתומה" msgid "Install unsigned device firmware" msgstr "התקנת קושחת התקן ×©×œ× × ×—×ª×ž×”" msgid "Install unsigned system firmware" msgstr "התקנת קושחת מערכת ×©×œ× × ×—×ª×ž×”" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "עדכון הקושחה מותקן…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "מותקן על %s…" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "BootGuard של ×ינטל" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "מדיניות שגי×ות של BootGuard של ×ינטל" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "התקן פנימי" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "שגוי" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "במצב מנהל טעינה" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "תקלה" msgstr[1] "תקלות" msgstr[2] "תקלות" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "שינוי ×חרון" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "נותרה פחות מדקה" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "רישיון" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "נעילת ליבת לינוקס" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "×ימות ליבת לינוקס" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "שטח החלפה של לינוקס" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "שירות קושחת ×™×¦×¨× ×™× ×œ×œ×™× ×•×§×¡ (קושחה יציבה)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "שירות קושחת ×™×¦×¨× ×™× ×œ×œ×™× ×•×§×¡ (קושחה ניסיונית)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "הליבה של לינוקס" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "×זור החלפה של לינוקס" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "הצגת רשומות ב־dbx" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "הצגת סוגי הקושחות הזמיני×" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "מציג ×§×‘×¦×™× ×‘Ö¾ESP" #. TRANSLATORS: command description msgid "Load device emulation data" msgstr "טעינת נתוני הדמיית התקן" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "בטעינה…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "נעול" #. TRANSLATORS: the release urgency msgid "Low" msgstr "נמוך" msgid "MEI version" msgstr "גרסת MEI" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "להפעיל ידנית ×ª×•×¡×¤×™× ×ž×¡×•×™×ž×™×" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "בינוני" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "חתימת נתוני על" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "כתובת נתוני על" #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "גרסה מזערית" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "משנה מרוחק נתון" msgid "Modify a configured remote" msgstr "שינוי מרוחק מוגדר" msgid "Modify daemon configuration" msgstr "שינוי הגדרות סוכן" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "מעקב ×חר הסוכן ל×יתור ×ירועי×" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "מעגן ×ת ×”Ö¾ESP" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "דורש הפעלה מחדש ל×חר ההתקנה" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "דורש כיבוי ל×חר ההתקנה" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "גרסה חדשה" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "×œ× ×¦×•×™× ×” פעולה!" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "×ין ×©× ×ž×•×›×™× ×¢×‘×•×¨ %s" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "×œ× × ×ž×¦×ו מזהי קושחה" #. TRANSLATORS: nothing found msgid "No firmware found" msgstr "×œ× × ×ž×¦××” קושחה" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "×œ× ×ותרה חומרה בעלת יכולת עדכון קושחה" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "×ין מהדורות זמינות" #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "×ין ×ž×¨×•×—×§×™× ×–×ž×™× ×™×" msgid "No updates available for remaining devices" msgstr "×ין ×¢×“×›×•× ×™× ×–×ž×™× ×™× ×œ×”×ª×§× ×™× ×©× ×•×ª×¨×•" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "×œ× × ×ž×¦×" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "×œ× × ×ª×ž×š" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "×ישור" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "הצגת ערך PCR יחיד" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "מותר עדכוני גרס×ות בלבד" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "מעקף נתיב ×”Ö¾ESP כברירת מחדל" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "נתיב" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "פענוח והצגת ×¤×¨×˜×™× ×¢×œ קובץ קושחה" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "עדכון ×”Ö¾dbx מפוענח…" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "×”Ö¾dbx של המערכת מפוענח…" #. TRANSLATORS: remote filename base msgid "Password" msgstr "סיסמה" msgid "Payload" msgstr "מטען" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "לבצע פעולה?" #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "× × ×œ×ž×œ× ×ž×¡×¤×¨ מ־0 עד %u:" #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "תלויות התוסף חסרות" #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "גרסה קודמת" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "עדיפות" msgid "Proceed with upload?" msgstr "להמשיך בהעל××”?" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "×§× ×™×™× ×™" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "קרי×ת מקטע בינרי של קושחה מהתקן" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "× ×§×¨× ×ž×ª×•×š %s…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "מתבצעת קרי×ה…" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "רענון נתוני העל משרת מרוחק" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "להתקין ×ת %s ×ל %s מחדש?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "התקנת הקושחה הנוכחית על ההתקן מחדש" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "התקנת קושחה על התקן מחדש" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "×¢× ×£ הפצה" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "מזהה מרוחק" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "כתובת לדיווח" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "דווח לשרת המרוחק" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "מערכת ×§×‘×¦×™× efivarfs נחוצה ×œ× × ×ž×¦××”" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "החומרה הנדרשת ×œ× × ×ž×¦××”" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "דורש מנהל טעינה" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "דורש חיבור ל×ינטרנט" msgid "Reset daemon configuration" msgstr "×יפוס הגדרות סוכן" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "להפעיל כעת מחדש?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "להפעיל ×ת הסוכן מחדש כדי ×©×”×©×™× ×•×™×™× ×™×™×›× ×¡×• לתוקף?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "ההתקן מופעל מחדש…" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "החזרת כל מזהי החומרה למכונה" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "הליבה הפעילה ישנה מדי" #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "סיומת סביבת הרצה" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "נעילת SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "כתיבת SPI" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "מתבצע תזמון…" msgid "Security hardening for HSI" msgstr "הקשחת ×בטחת מידע ל־HSI" #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "ניתן לפנות ×ל %s למידע נוסף." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "התקן נבחר" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "כרך נבחר" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "מספר סידורי" msgid "Set one or more BIOS settings" msgstr "שינוי הגדרה ×חת ×ו יותר ב־BIOS" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "הגדרת רשימת הקושחות המותרות" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "ההגדרות תוחלנה ל×חר הפעלת המערכת מחדש" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "שיתוף היסטוריית הקושחה ×¢× ×”×ž×¤×ª×—×™×" #. TRANSLATORS: command line option msgid "Show all results" msgstr "הצגת כל התוצ×ות" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "הצגת גרס×ות לקוח וסוכן" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "להציג פרטי ניפוי תקלות לכל שמות התחו×" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "הצג ×פשרויות ניפוי שגי×ות" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "להציג ×”×ª×§× ×™× ×©×œ× × ×™×ª×Ÿ לעדכן" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "הצגת מידע ניפוי שגי×ות מורחב" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "הצגת היסטוריית עדכוני קושחה" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "הצגת הגרסה המחושבת של ×”Ö¾dbx" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "לכבות כעת?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "×œ×—×ª×•× ×¢×œ חומרה ×¢× ×ž×¤×ª×— חדש" msgid "Sign data using the client certificate" msgstr "×œ×—×ª×•× ×¢×œ ×”× ×ª×•× ×™× ×‘×¢×–×¨×ª ×ישור הלקוח" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "×œ×—×ª×•× ×¢×œ ×”× ×ª×•× ×™× ×‘×¢×–×¨×ª ×ישור הלקוח" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "×œ×—×ª×•× ×¢×œ ×”× ×ª×•× ×™× ×©× ×©×œ×—×™× ×¢× ×ישור הלקוח" msgid "Signature" msgstr "חתימה" #. TRANSLATORS: file size of the download msgid "Size" msgstr "גודל" #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "מקור" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "ציון קובץ מסד הנתוני מסוג dbx" msgid "Stop the fwupd service" msgstr "עצירת שירות fwupd" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "כל ×”×”×ª×§× ×™× ×”×•×¤×¢×œ×• בהצלחה" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "נתוני על ×—×“×©×™× ×”×ª×§×‘×œ×• בהצלחה:" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "המרוחק הופעל בהצלחה" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "הקושחה הותקנה בהצלחה" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "ערך התצורה נערך בהצלחה" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "המרוחק נערך בהצלחה" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "נתוני העל התרעננו ידנית בהצלחה" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "תקציר" #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "נתמך" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "מעבד נתמך" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "נתמך בשרת המרוחק" #. TRANSLATORS: Title: sleep state msgid "Suspend To RAM" msgstr "השהיה לזיכרון (RAM)" #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "השהיה למצב המתנה" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "השהיה לזיכרון" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "לעבור ×¢× ×£ מ־%s ×ל %s?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "החלפת ×¢× ×£ הקושחה בהתקן" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "המערכת דורשת מקור חשמל חיצוני" #. show the user the entire data blob msgid "Target" msgstr "יעד" #. TRANSLATORS: command description msgid "Test a device using a JSON manifest" msgstr "בדיקת התקן ×¢× ×ž× ×™×¤×¡×˜ JSON" #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "â€LVFS ×”×•× ×©×™×¨×•×ª חינמי שמתפקד כיישות חוקית בלתי תלוי ו×ין לה ×©×•× ×§×©×¨ ×¢× $OS_RELEASE:NAME$. יתכן ×›×™ המפיץ שלך ×œ× ×ימת ברמת ת×ימות ××£ ×חד מעדכוני הקושחה מול המערכת שלך ×ו ×”×”×ª×§× ×™× ×”×ž×—×•×‘×¨×™× ×ליה. כל הקושחה מסופקת ×ך ורק על ידי יצרני הציוד המקוריי×." #. TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name #, c-format msgid "The firmware from %s is not supplied by %s, the hardware vendor." msgstr "הקושחה ×”×–×ת מבית %s ××™× ×” מסופקת על ידי %s, יצרן החומרה" #. TRANSLATORS: try to help msgid "The system clock has not been set correctly and downloading files may fail." msgstr "שעון המערכת ×ינו מכוון והורדת ×§×‘×¦×™× ×¢×œ×•×œ×” להיכשל." #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "×ין קובצי קושחה חסומי×" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "×ין קושחה שעברה ×ימות." #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "החבילה ×œ× ×¢×‘×¨×” ×ימות, יכול להיות ×©×œ× ×ª×¢×‘×•×“ כר×וי." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "כנר××” שתוכנית זו תעבוד כר×וי רק ×¢× ×ž×©×ª×ž×© על (root)" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "צד מרוחק ×–×” כולל קושחה ×©×œ× × ×¤×¡×œ×” ×ך עדיין נבדקת על ידי ספק החומרה. עליך ×œ×•×•×“× ×©×™×© לך דרך ידנית לשנמך ×ת הקושחה במקרה שעדכון הקושחה נכשל." #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "למערכת יש תקלות בסביבת הרצת ×”Ö¾HSI." #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "רמת ×בטחת ×”Ö¾HSI של המערכת נמוכה." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "כלי ×–×” מ×פשר להנהלה להחיל עדכוני dbx ב־UEFI." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "כלי ×–×” מ×פשר למנהלי המערכת לתש×ל ולשלוט בסוכן fwupd, ×”×•× ×ž×פשר ×œ×”× ×œ×‘×¦×¢ פעולות כגון התקנת ×ו שנמוך קושחה." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "כלי ×–×” מ×פשר להנהלה להשתמש בתוספי fwupd מבלי שיותקנו על המערכת המ×רחת." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "כלי ×–×” ×™×§×¨× ×•×™×¤×¢× ×— ×ת יומן ×”××™×¨×•×¢×™× ×©×œ ×”Ö¾TPM מקושחת המערכת." #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "סוג" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "מחיצת ESP של UEFI ×œ× ×–×•×”×ª×” ×ו הוגדרה" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "מפתח פלטפורמה של UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "טעינה מ×ובטחת של UEFI" #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "עדכוני כמוסה של UEFI ××™× × ×–×ž×™× ×™× ×ו ×ž×•×¤×¢×œ×™× ×‘×ª×¦×•×¨×ª הקושחה" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "כלי dbx ל־UEFI" #. TRANSLATORS: system is not booted in UEFI mode msgid "UEFI firmware can not be updated in legacy BIOS mode" msgstr "××™ ×פשר לעדכן קושחת UEFI במצב BIOS מיושן" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "מפתח פלטפורמה ב־UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "טעינה מ×ובטחת של UEFI" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "×œ× × ×™×ª×Ÿ להתחבר לשירות" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "ביטול ×יגוד מנהל ההתקן הנוכחי" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "קושחה ×œ× ×—×•×¡×ž×ª:" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "×œ× ×ž×•×¦×¤×Ÿ" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "×œ× ×™×“×•×¢" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "התקן ×œ× ×™×“×•×¢" msgid "Unlock the device to allow access" msgstr "יש לשחרר ×ת ההתקן כדי ל×פשר גישה" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "משוחרר" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "משחרר ×ת חסימת ההתקן לגישת קושחה" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "מנתק ×ת ×”Ö¾ESP" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "גרסת הסוכן %s ××™× ×” נתמכת, גרסת הלקוח ×”×™× %s" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "ניתן לעדכון" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "שגי×ת עדכון" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "הודעת עדכון" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "מצב עדכון" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "כשל בעדכון ×–×ת תקלה מוכרת, יש לבקר בכתובת למידע נוסף:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "לעדכן עכשיו?" msgid "Update the stored device verification information" msgstr "עדכון פרטי ×ימות ההתקן המ×וחסני×" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "עדכון נתוני העל המ××•×—×¡× ×™× ×‘×ª×›× ×™× × ×•×›×—×™×™×" msgid "Updating" msgstr "מתבצע עדכון" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "%s מתעדכן…" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "לשדרג ×ת %s מגרסה %s ×ל %s?" #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "דחיפות" #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "נשלחה הודעה למשתמש" #. TRANSLATORS: remote filename base msgid "Username" msgstr "×©× ×ž×©×ª×ž×©" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "תקף" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "תכני ×”Ö¾ESP ×¢×•×‘×¨×™× ×ימות…" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "הגוון" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "ספק" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "מתבצע ×ימות…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "גרסה" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "×זהרה" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "בהמתנה…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "מעקב ×חר ×©×™× ×•×™×™× ×‘×—×•×ž×¨×”" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "קובץ נכתב:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "מתבצעת כתיבה…" #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "יכול להיות שהחומרה שלך תיפגע עקב השימוש בקושחה ×”×–×ת והתקנת המהדורה ×”×–×ת עשויה לפגוע ב×חריות ×¢× %s." #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "בררת מחדל" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "כלי לתיעוד ×ירועי TPM של fwupd" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "×ª×•×¡×¤×™× ×©×œ fwupd" fwupd-2.0.10/po/hi.po000066400000000000000000004510431501337203100142770ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Prashant Gupta , 2015 # Scrambled777 , 2024 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Hindi (http://app.transifex.com/freedesktop/fwupd/language/hi/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: hi\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "%.0f मिनट शेष" msgstr[1] "%.0f मिनट शेष" #. TRANSLATORS: BMC refers to baseboard management controller which #. * is the device that updates all the other firmware on the system #, c-format msgid "%s BMC Update" msgstr "%s BMC अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "%s बैटरी अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "%s CPU माइकà¥à¤°à¥‹à¤•ोड अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "%s कैमरा अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "%s विनà¥à¤¯à¤¾à¤¸ अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "%s उपभोकà¥à¤¤à¤¾ ME अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "%s नियंतà¥à¤°à¤• अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "%s कॉरà¥à¤ªà¥‹à¤°à¥‡à¤Ÿ ME अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "%s उपकरण अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: Video Display refers to the laptop internal display or #. * external monitor #, c-format msgid "%s Display Update" msgstr "%s डिसà¥à¤ªà¥à¤²à¥‡ अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: Dock refers to the port replicator hardware laptops are #. * cradled in, or lowered onto #, c-format msgid "%s Dock Update" msgstr "%s डॉक अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: drive refers to a storage device, e.g. SATA disk #, c-format msgid "%s Drive Update" msgstr "%s डà¥à¤°à¤¾à¤‡à¤µ अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "%s à¤à¤‚बेडेड नियंतà¥à¤°à¤• अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: a device that can read your fingerprint pattern #, c-format msgid "%s Fingerprint Reader Update" msgstr "%s फिंगरपà¥à¤°à¤¿à¤‚ट रीडर अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: flash refers to solid state storage, e.g. UFS or eMMC #, c-format msgid "%s Flash Drive Update" msgstr "%s फ़à¥à¤²à¥ˆà¤¶ डà¥à¤°à¤¾à¤‡à¤µ अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: GPU refers to a Graphics Processing Unit, e.g. #. * the "video card" #, c-format msgid "%s GPU Update" msgstr "%s GPU अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: a large pressure-sensitive drawing area typically used #. * by artists and digital artists #, c-format msgid "%s Graphics Tablet Update" msgstr "%s गà¥à¤°à¤¾à¤«à¤¿à¤•à¥à¤¸ टेबलेट अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "%s कीबोरà¥à¤¡ अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "%s ME अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "%s माउस अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "%s नेटवरà¥à¤• इंटरफ़ेस अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: SSD refers to a Solid State Drive, e.g. non-rotating #. * SATA or NVMe disk #, c-format msgid "%s SSD Update" msgstr "%s SSD अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "%s संगà¥à¤°à¤¹à¤£ नियंतà¥à¤°à¤• अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "%s सिसà¥à¤Ÿà¤® अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "%s TPM अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "%s थंडरबोलà¥à¤Ÿ नियंतà¥à¤°à¤• अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "%s टचपैड अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: Dock refers to the port replicator device connected #. * by plugging in a USB cable -- which may or may not also provide power #, c-format msgid "%s USB Dock Update" msgstr "%s USB डॉक अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth #. * device that stays in the USB port so the wireless peripheral works #, c-format msgid "%s USB Receiver Update" msgstr "%s USB रिसीवर अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "%s अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: warn the user before updating, %1 is a device name #, c-format msgid "%s and all connected devices may not be usable while updating." msgstr "अदà¥à¤¯à¤¤à¤¨ करते समय %s और सभी जà¥à¥œà¥‡ हà¥à¤ उपकरण उपयोगी नहीं होंगें।" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "Encrypted RAM". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s appeared: %s" msgstr "%1$s पà¥à¤°à¤¦à¤°à¥à¤¶à¤¿à¤¤: %2$s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "UEFI platform #. key". #. * %2 and %3 refer to results value, e.g. "Valid" and "Invalid" #, c-format msgid "%s changed: %s → %s" msgstr "%1$s परिवरà¥à¤¤à¤¿à¤¤: %2$s → %3$s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "SPI BIOS #. region". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s disappeared: %s" msgstr "%1$s गायब: %2$s" #. TRANSLATORS: the device has a reason it can't update, e.g. laptop lid #. closed #, c-format msgid "%s is not currently updatable" msgstr "%s अभी अदà¥à¤¯à¤¤à¤¨ करने योगà¥à¤¯ नहीं है" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "%s विनिरà¥à¤®à¤¾à¤£ मोड" #. TRANSLATORS: warn the user before #. * updating, %1 is a device name #, c-format msgid "%s must remain connected for the duration of the update to avoid damage." msgstr "कà¥à¤·à¤¤à¤¿ से बचने के लिठ%s को अदà¥à¤¯à¤¤à¤¨ की अवधि तक जà¥à¤¡à¤¼à¥‡ रहना चाहिà¤à¥¤" #. TRANSLATORS: warn the user before updating, %1 is a machine #. * name #, c-format msgid "%s must remain plugged into a power source for the duration of the update to avoid damage." msgstr "कà¥à¤·à¤¤à¤¿ से बचने के लिठअदà¥à¤¯à¤¤à¤¨ की अवधि के दौरान %s को ऊरà¥à¤œà¤¾ सà¥à¤°à¥‹à¤¤ से जà¥à¥œà¤¾ रहना चाहिà¤à¥¤" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "%s अधà¥à¤¯à¤¾à¤°à¥‹à¤¹à¤£" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "%s संसà¥à¤•रण" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u दिन" msgstr[1] "%u दिन" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "%u उपकरण के लिठफरà¥à¤®à¤µà¥‡à¤¯à¤° उनà¥à¤¨à¤¯à¤¨ उपलबà¥à¤§ है।" msgstr[1] "%u उपकरणों के लिठफरà¥à¤®à¤µà¥‡à¤¯à¤° उनà¥à¤¨à¤¯à¤¨ उपलबà¥à¤§ है।" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device is not the best known configuration." msgid_plural "%u devices are not the best known configuration." msgstr[0] "%u उपकरण सरà¥à¤µà¥‹à¤¤à¥à¤¤à¤® जà¥à¤žà¤¾à¤¤ विनà¥à¤¯à¤¾à¤¸ नहीं है।" msgstr[1] "%u उपकरण सरà¥à¤µà¥‹à¤¤à¥à¤¤à¤® जà¥à¤žà¤¾à¤¤ विनà¥à¤¯à¤¾à¤¸ नहीं हैं." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u घंटा" msgstr[1] "%u घंटे" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u मिनट" msgstr[1] "%u मिनट" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u सेकेंड" msgstr[1] "%u सेकेंड" #. TRANSLATORS: first percentage is current value, 2nd percentage is the #. * lowest limit the firmware update is allowed for the update to happen #, c-format msgid "%u%% (threshold %u%%)" msgstr "%u%% (सीमा %u%%)" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(अपà¥à¤°à¤šà¤²à¤¿à¤¤)" #. TRANSLATORS: HSI event title msgid "A TPM PCR is now an invalid value" msgstr "TPM PCR अब à¤à¤• अमानà¥à¤¯ मान है" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "AMD Firmware Replay Protection" msgstr "AMD फरà¥à¤®à¤µà¥‡à¤¯à¤° रीपà¥à¤²à¥‡ सà¥à¤°à¤•à¥à¤·à¤¾" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "AMD Firmware Write Protection" msgstr "AMD फरà¥à¤®à¤µà¥‡à¤¯à¤° लेखन सà¥à¤°à¤•à¥à¤·à¤¾" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "AMD Secure Processor Rollback Protection" msgstr "AMD सà¥à¤°à¤•à¥à¤·à¤¿à¤¤ पà¥à¤°à¥‹à¤¸à¥‡à¤¸à¤° रोलबैक सà¥à¤°à¤•à¥à¤·à¤¾" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "ARCHIVE FIRMWARE METAINFO [FIRMWARE] [METAINFO] [JCATFILE]" msgstr "पà¥à¤°à¤¾à¤²à¥‡à¤– फरà¥à¤®à¤µà¥‡à¤¯à¤° मेटाइनà¥à¤«à¥‹ [फरà¥à¤®à¤µà¥‡à¤¯à¤°] [मेटाइनà¥à¤«à¥‹] [JCATFILE]" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "कारà¥à¤°à¤µà¤¾à¤ˆ आवशà¥à¤¯à¤•:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "उपकरणों को सकà¥à¤°à¤¿à¤¯ करें" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "लंबित उपकरणों को सकà¥à¤°à¤¿à¤¯ करें" msgid "Activate the new firmware on the device" msgstr "उपकरण पर नया फरà¥à¤®à¤µà¥‡à¤¯à¤° सकà¥à¤°à¤¿à¤¯ करें" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° अदà¥à¤¯à¤¤à¤¨ सकà¥à¤°à¤¿à¤¯ किया जा रहा है" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "इसके लिठफरà¥à¤®à¤µà¥‡à¤¯à¤° अदà¥à¤¯à¤¤à¤¨ सकà¥à¤°à¤¿à¤¯ किया जा रहा है" #. TRANSLATORS: command description msgid "Adds devices to watch for future emulation" msgstr "भविषà¥à¤¯ में अनà¥à¤•रण हेतॠदेखने के लिठउपकरण जोड़ता है" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "आयà¥" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "सहमति दें और रिमोट सकà¥à¤·à¤® करें?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "%s का उपनाम" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are now valid" msgstr "सभी TPM PCR अब मानà¥à¤¯ हैं" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are valid" msgstr "सभी TPM PCR मानà¥à¤¯ हैं" #. TRANSLATORS: an application is preventing system updates msgid "All devices are prevented from update by system inhibit" msgstr "सिसà¥à¤Ÿà¤® अवरोध दà¥à¤µà¤¾à¤°à¤¾ सभी उपकरणों को अदà¥à¤¯à¤¤à¤¨ होने से रोका जाता है" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "à¤à¤• ही पà¥à¤°à¤•ार के सभी उपकरण à¤à¤• ही समय में अदà¥à¤¯à¤¤à¤¿à¤¤ किठजाà¤à¤‚गे" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° संसà¥à¤•रणों को अपगà¥à¤°à¥‡à¤¡ करने की अनà¥à¤®à¤¤à¤¿ दें" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "मौजूदा फरà¥à¤®à¤µà¥‡à¤¯à¤° संसà¥à¤•रणों को पà¥à¤¨à¤ƒ सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ करने की अनà¥à¤®à¤¤à¤¿ दें" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° शाखा बदलने की अनà¥à¤®à¤¤à¤¿ दें" #. TRANSLATORS: is not the main firmware stream msgid "Alternate branch" msgstr "वैकलà¥à¤ªà¤¿à¤• शाखा" #. TRANSLATORS: another application is updating the device already msgid "An update is in progress" msgstr "अदà¥à¤¯à¤¤à¤¨ पà¥à¤°à¤—ति पर है" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "अदà¥à¤¯à¤¤à¤¨ को पूरा करने के लिठरीबूट की आवशà¥à¤¯à¤•ता है।" #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "अदà¥à¤¯à¤¤à¤¨ को पूरा करने के लिठसिसà¥à¤Ÿà¤® को शटडाउन करना आवशà¥à¤¯à¤• है।" #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "सभी पà¥à¤°à¤¶à¥à¤¨à¥‹à¤‚ का उतà¥à¤¤à¤° हां में दें" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "सलाह न मिलने पर भी अदà¥à¤¯à¤¤à¤¨ लागू करें" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "अदà¥à¤¯à¤¤à¤¨ फाइलें लागू करें" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "अदà¥à¤¯à¤¤à¤¨ लागू किया जा रहा है…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "सà¥à¤µà¥€à¤•ृत फरà¥à¤®à¤µà¥‡à¤¯à¤°:" msgstr[1] "सà¥à¤µà¥€à¤•ृत फरà¥à¤®à¤µà¥‡à¤¯à¤°:" #. TRANSLATORS: command description msgid "Asks the daemon to quit" msgstr "डेमन को छोड़ने के लिठकहता है" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° मोड से जोड़ें" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "पà¥à¤°à¤®à¤¾à¤£à¥€à¤•रण किया जा रहा है…" #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "पà¥à¤°à¤®à¤¾à¤£à¥€à¤•रण विवरण आवशà¥à¤¯à¤• हैं" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "हटाने योगà¥à¤¯ उपकरण पर फरà¥à¤®à¤µà¥‡à¤¯à¤° को पदावनत करने के लिठपà¥à¤°à¤®à¤¾à¤£à¥€à¤•रण की आवशà¥à¤¯à¤•ता होती है" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "इस मशीन पर फरà¥à¤®à¤µà¥‡à¤¯à¤° को पदावनत करने के लिठपà¥à¤°à¤®à¤¾à¤£à¥€à¤•रण की आवशà¥à¤¯à¤•ता है" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to fix a host security issue" msgstr "होसà¥à¤Ÿ सà¥à¤°à¤•à¥à¤·à¤¾ समसà¥à¤¯à¤¾ को ठीक करने के लिठपà¥à¤°à¤®à¤¾à¤£à¥€à¤•रण आवशà¥à¤¯à¤• है" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "BIOS सेटिंगà¥à¤¸ को संशोधित करने के लिठपà¥à¤°à¤®à¤¾à¤£à¥€à¤•रण आवशà¥à¤¯à¤• है" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° अदà¥à¤¯à¤¤à¤¨ के लिठउपयोग किठजाने वाले विनà¥à¤¯à¤¸à¥à¤¤ रिमोट को संशोधित करने के लिठपà¥à¤°à¤®à¤¾à¤£à¥€à¤•रण की आवशà¥à¤¯à¤•ता होती है" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "डेमॉन विनà¥à¤¯à¤¾à¤¸ को संशोधित करने के लिठपà¥à¤°à¤®à¤¾à¤£à¥€à¤•रण आवशà¥à¤¯à¤• है" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "BIOS सेटिंगà¥à¤¸ को पढ़ने के लिठपà¥à¤°à¤®à¤¾à¤£à¥€à¤•रण आवशà¥à¤¯à¤• है" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "अनà¥à¤®à¥‹à¤¦à¤¿à¤¤ फरà¥à¤®à¤µà¥‡à¤¯à¤° की सूची तय करने के लिठपà¥à¤°à¤®à¤¾à¤£à¥€à¤•रण आवशà¥à¤¯à¤• है" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "कà¥à¤²à¤¾à¤‡à¤‚ट पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° का उपयोग करके डेटा पर हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° करने के लिठपà¥à¤°à¤®à¤¾à¤£à¥€à¤•रण आवशà¥à¤¯à¤• है" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "नठफरà¥à¤®à¤µà¥‡à¤¯à¤° संसà¥à¤•रण पर जाने के लिठपà¥à¤°à¤®à¤¾à¤£à¥€à¤•रण आवशà¥à¤¯à¤• है" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to undo the fix for a host security issue" msgstr "होसà¥à¤Ÿ सà¥à¤°à¤•à¥à¤·à¤¾ समसà¥à¤¯à¤¾ के समाधान को पूरà¥à¤µà¤µà¤¤ करने के लिठपà¥à¤°à¤®à¤¾à¤£à¥€à¤•रण आवशà¥à¤¯à¤• है" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "किसी उपकरण को अनलॉक करने के लिठपà¥à¤°à¤®à¤¾à¤£à¥€à¤•रण आवशà¥à¤¯à¤• है" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "हटाने योगà¥à¤¯ उपकरण पर फरà¥à¤®à¤µà¥‡à¤¯à¤° को अदà¥à¤¯à¤¤à¤¨ करने के लिठपà¥à¤°à¤®à¤¾à¤£à¥€à¤•रण आवशà¥à¤¯à¤• है" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "इस मशीन पर फरà¥à¤®à¤µà¥‡à¤¯à¤° को अदà¥à¤¯à¤¤à¤¨ करने के लिठपà¥à¤°à¤®à¤¾à¤£à¥€à¤•रण आवशà¥à¤¯à¤• है" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "उपकरण के लिठसंगà¥à¤°à¤¹à¥€à¤¤ चेकसम को अदà¥à¤¯à¤¤à¤¨ करने के लिठपà¥à¤°à¤®à¤¾à¤£à¥€à¤•रण आवशà¥à¤¯à¤• है" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "सà¥à¤µà¤šà¤¾à¤²à¤¿à¤¤ रिपोरà¥à¤Ÿà¤¿à¤‚ग" #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "हर बार सà¥à¤µà¤šà¤¾à¤²à¤¿à¤¤ रूप से अपलोड करें?" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS Firmware Updates" msgstr "BIOS फरà¥à¤®à¤µà¥‡à¤¯à¤° अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS Rollback Protection" msgstr "BIOS रोलबैक सà¥à¤°à¤•à¥à¤·à¤¾" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS firmware updates" msgstr "BIOS फरà¥à¤®à¤µà¥‡à¤¯à¤° अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS rollback protection" msgstr "BIOS रोलबैक सà¥à¤°à¤•à¥à¤·à¤¾" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "BIOS अदà¥à¤¯à¤¤à¤¨ LVFS या Windows अदà¥à¤¯à¤¤à¤¨ के माधà¥à¤¯à¤® से वितरित किठजाते हैं" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "BUILDER-XML FILENAME-DST" msgstr "बिलà¥à¤¡à¤°-XML फाइलनाम-DST" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "बैटरी" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "नये करà¥à¤¨à¥‡à¤² डà¥à¤°à¤¾à¤‡à¤µà¤° को बाइंड करें" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "अवरà¥à¤¦à¥à¤§ फरà¥à¤®à¤µà¥‡à¤¯à¤° फाइलें:" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "अवरà¥à¤¦à¥à¤§ संसà¥à¤•रण" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° को अवरà¥à¤¦à¥à¤§ किया जा रहा है:" #. TRANSLATORS: command description msgid "Blocks a specific firmware from being installed" msgstr "किसी विशिषà¥à¤Ÿ फरà¥à¤®à¤µà¥‡à¤¯à¤° को सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ होने से रोकता है" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "बूटलोडर संसà¥à¤•रण" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "शाखा" #. TRANSLATORS: command description msgid "Build a cabinet archive from a firmware blob and XML metadata" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° बà¥à¤²à¥‰à¤¬ और XML मेटाडेटा से à¤à¤• कैबिनेट संगà¥à¤°à¤¹ बनाà¤à¤‚" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° फाइल बनाà¤à¤‚" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * Utilized by OS means the distribution enabled it msgid "CET OS Support" msgstr "CET OS समरà¥à¤¥à¤¨" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * enabled means supported by the processor msgid "CET Platform" msgstr "CET पà¥à¤²à¥‡à¤Ÿà¤«à¤¾à¤°à¥à¤®" #. TRANSLATORS: longer description msgid "CPU Microcode must be updated to mitigate against various information-disclosure security issues." msgstr "विभिनà¥à¤¨ सूचना-पà¥à¤°à¤•टीकरण सà¥à¤°à¤•à¥à¤·à¤¾ समसà¥à¤¯à¤¾à¤“ं को कम करने के लिठCPU माइकà¥à¤°à¥‹à¤•ोड को अदà¥à¤¯à¤¤à¤¨ किया जाना चाहिà¤à¥¤" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "रदà¥à¤¦ करें" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "रदà¥à¤¦" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "लागू नहीं किया जा सकता कà¥à¤¯à¥‹à¤‚कि dbx अदà¥à¤¯à¤¤à¤¨ पहले ही लागू किया जा चà¥à¤•ा है।" #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "बदल गया" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "कà¥à¤°à¤¿à¤ªà¥à¤Ÿà¥‹à¤—à¥à¤°à¤¾à¤«à¤¿à¤• हैश फरà¥à¤®à¤µà¥‡à¤¯à¤° से मिलान की जांच करता है" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "चेकसम" #. TRANSLATORS: get interactive prompt, where branch is the #. * supplier of the firmware, e.g. "non-free" or "free" msgid "Choose branch" msgstr "शाखा चà¥à¤¨à¥‡à¤‚" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "उपकरण चà¥à¤¨à¥‡à¤‚" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° चà¥à¤¨à¥‡à¤‚" #. TRANSLATORS: get interactive prompt msgid "Choose release" msgstr "रिलीज़ चà¥à¤¨à¥‡à¤‚" #. TRANSLATORS: get interactive prompt msgid "Choose volume" msgstr "वॉलà¥à¤¯à¥‚म चà¥à¤¨à¥‡à¤‚" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "अंतिम अदà¥à¤¯à¤¤à¤¨ से परिणाम साफ करता है" #. TRANSLATORS: error message msgid "Command not found" msgstr "कमांड नहीं मिला" #. TRANSLATORS: is not supported by the vendor msgid "Community supported" msgstr "समà¥à¤¦à¤¾à¤¯ समरà¥à¤¥à¤¿à¤¤" #. TRANSLATORS: title prefix for the BIOS settings dialog msgid "Configuration Change Suggested" msgstr "विनà¥à¤¯à¤¾à¤¸ परिवरà¥à¤¤à¤¨ का सà¥à¤à¤¾à¤µ दिया गया" #. TRANSLATORS: no peeking msgid "Configuration is only readable by the system administrator" msgstr "विनà¥à¤¯à¤¾à¤¸ केवल सिसà¥à¤Ÿà¤® पà¥à¤°à¤¶à¤¾à¤¸à¤• दà¥à¤µà¤¾à¤°à¤¾ पढ़ने योगà¥à¤¯ है" #. TRANSLATORS: longer description msgid "Control-Flow Enforcement Technology detects and prevents certain methods for running malicious software on the device." msgstr "नियंतà¥à¤°à¤£-पà¥à¤°à¤µà¤¾à¤¹ पà¥à¤°à¤µà¤°à¥à¤¤à¤¨ पà¥à¤°à¥Œà¤¦à¥à¤¯à¥‹à¤—िकी उपकरण पर दà¥à¤°à¥à¤­à¤¾à¤µà¤¨à¤¾à¤ªà¥‚रà¥à¤£ सॉफà¥à¤Ÿà¤µà¥‡à¤¯à¤° चलाने के कà¥à¤› तरीकों का पता लगाती है और उनà¥à¤¹à¥‡à¤‚ रोकती है।" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology msgid "Control-flow Enforcement Technology" msgstr "नियंतà¥à¤°à¤£-पà¥à¤°à¤µà¤¾à¤¹ पà¥à¤°à¤µà¤°à¥à¤¤à¤¨ पà¥à¤°à¥Œà¤¦à¥à¤¯à¥‹à¤—िकी" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° फाइल परिवरà¥à¤¤à¤¿à¤¤ करें" #. TRANSLATORS: when the update was built msgid "Created" msgstr "निरà¥à¤®à¤¿à¤¤" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "गंभीर" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "कà¥à¤°à¤¿à¤ªà¥à¤Ÿà¥‹à¤—à¥à¤°à¤¾à¤«à¤¿à¤• हैश सतà¥à¤¯à¤¾à¤ªà¤¨ उपलबà¥à¤§ है" #. TRANSLATORS: current value of a BIOS setting msgid "Current Value" msgstr "वरà¥à¤¤à¤®à¤¾à¤¨ मान" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "वरà¥à¤¤à¤®à¤¾à¤¨ संसà¥à¤•रण" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "उपकरण-ID|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "डिबगिंग विकलà¥à¤ª" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "विसंपीड़न…" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "विवरण" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "बूटलोडर मोड से अलग करें" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "विवरण" #. TRANSLATORS: the best known configuration is a set of software that we know #. works #. * well together. In the OEM and ODM industries it is often called a BKC msgid "Deviate from the best known configuration?" msgstr "सरà¥à¤µà¥‹à¤¤à¥à¤¤à¤® जà¥à¤žà¤¾à¤¤ विनà¥à¤¯à¤¾à¤¸ से विचलन?" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "उपकरण फà¥à¤²à¥ˆà¤—" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "उपकरण ID" #. TRANSLATORS: description of the device requests msgid "Device Requests" msgstr "उपकरण अनà¥à¤°à¥‹à¤§" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "उपकरण जोड़ा गया:" #. TRANSLATORS: the device is already connected msgid "Device already exists" msgstr "उपकरण पहले से मौजूद है" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "उपकरण की बैटरी ऊरà¥à¤œà¤¾ बहà¥à¤¤ कम है" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse #, c-format msgid "Device battery power is too low (%u%%, requires %u%%)" msgstr "उपकरण की बैटरी ऊरà¥à¤œà¤¾ बहà¥à¤¤ कम है (%u%%, %u%% की आवशà¥à¤¯à¤•ता है)" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "उपकरण फ़à¥à¤²à¥ˆà¤¶ विफलताओं से वापसी कर सकता है" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "उपकरण बदला गया:" #. TRANSLATORS: a version check is required for all firmware msgid "Device firmware is required to have a version check" msgstr "उपकरण फरà¥à¤®à¤µà¥‡à¤¯à¤° का संसà¥à¤•रण जांचना आवशà¥à¤¯à¤• है" #. TRANSLATORS: emulated means we are pretending to be a different model msgid "Device is emulated" msgstr "उपकरण अनà¥à¤•रणीय है" #. TRANSLATORS: device cannot be interrupted, for instance taking a phone call msgid "Device is in use" msgstr "उपकरण पà¥à¤°à¤¯à¥‹à¤— में है" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "उपकरण लॉक है" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "सभी पà¥à¤°à¤¦à¤¤à¥à¤¤ रिलीज़ों को सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ करने के लिठउपकरण की आवशà¥à¤¯à¤•ता है" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "उपकरण पहà¥à¤‚च-योगà¥à¤¯ नहीं है" #. TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode msgid "Device is unreachable, or out of wireless range" msgstr "उपकरण पहà¥à¤‚च योगà¥à¤¯ नहीं है, या वायरलेस सीमा से बाहर है" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "अदà¥à¤¯à¤¤à¤¨ की अवधि के लिठउपकरण पà¥à¤°à¤¯à¥‹à¤— योगà¥à¤¯ है" #. TRANSLATORS: usually this is when we're waiting for a reboot msgid "Device is waiting for the update to be applied" msgstr "उपकरण अदà¥à¤¯à¤¤à¤¨ लागू होने की पà¥à¤°à¤¤à¥€à¤•à¥à¤·à¤¾ कर रहा है" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "उपकरण हटाया गया:" #. TRANSLATORS: as in, wired mains power for a laptop msgid "Device requires AC power to be connected" msgstr "उपकरण को AC ऊरà¥à¤œà¤¾ से जोड़ने की आवशà¥à¤¯à¤•ता है" #. TRANSLATORS: device does not have a display connected msgid "Device requires a display to be plugged in" msgstr "उपकरण को पà¥à¤²à¤— इन करने के लिठडिसà¥à¤ªà¥à¤²à¥‡ की आवशà¥à¤¯à¤•ता होती है" #. TRANSLATORS: The device cannot be updated due to missing vendor's license." msgid "Device requires a software license to update" msgstr "उपकरण को अदà¥à¤¯à¤¤à¤¨ करने के लिठसॉफà¥à¤Ÿà¤µà¥‡à¤¯à¤° लाइसेंस की आवशà¥à¤¯à¤•ता है" #. TRANSLATORS: longer description msgid "Device software updates are provided for this device." msgstr "इस उपकरण के लिठउपकरण सॉफà¥à¤Ÿà¤µà¥‡à¤¯à¤° अदà¥à¤¯à¤¤à¤¨ पà¥à¤°à¤¦à¤¾à¤¨ किठगठहैं।" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "उपकरण अदà¥à¤¯à¤¤à¤¨à¥‹à¤‚ को चरणबदà¥à¤§ करता है" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "उपकरण फरà¥à¤®à¤µà¥‡à¤¯à¤° की भिनà¥à¤¨ शाखा पर बदलने का समरà¥à¤¥à¤¨ करता है" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "उपकरण अदà¥à¤¯à¤¤à¤¨ को सकà¥à¤°à¤¿à¤¯à¤£ की आवशà¥à¤¯à¤•ता है" #. TRANSLATORS: save the old firmware to disk before installing the new one msgid "Device will backup firmware before installing" msgstr "सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ करने से पहले उपकरण फरà¥à¤®à¤µà¥‡à¤¯à¤° का बैकअप लेगा" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "अदà¥à¤¯à¤¤à¤¨ पूरà¥à¤£ होने के बाद उपकरण दोबारा दिखाई नहीं देगा" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "उपकरण जिनà¥à¤¹à¥‡à¤‚ सफलतापूरà¥à¤µà¤• अदà¥à¤¯à¤¤à¤¨ किया गया:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "उपकरण जो सही ढंग से अदà¥à¤¯à¤¤à¤¨ नहीं किठगà¤:" #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "बिना फरà¥à¤®à¤µà¥‡à¤¯à¤° अदà¥à¤¯à¤¤à¤¨ वाले उपकरण: " #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "नवीनतम उपलबà¥à¤§ फरà¥à¤®à¤µà¥‡à¤¯à¤° संसà¥à¤•रण वाले उपकरण:" #. TRANSLATORS: this is for the device tests msgid "Did not find any devices with matching GUIDs" msgstr "मेल खाने वाले GUID वाला कोई उपकरण नहीं मिला" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "अकà¥à¤·à¤®" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "पà¥à¤°à¤¦à¤¤à¥à¤¤ रिमोट को अकà¥à¤·à¤® करता है" #. TRANSLATORS: the OS the release was tested on msgid "Distribution" msgstr "वितरण" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "पà¥à¤°à¤¾à¤¨à¥‡ मेटाडेटा की जांच न करें" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "असूचित इतिहास की जांच न करें" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "यह जांच न करें कि कà¥à¤¯à¤¾ डाउनलोड रिमोट सकà¥à¤·à¤® होना चाहिà¤" #. TRANSLATORS: command line option msgid "Do not check or prompt for reboot after update" msgstr "अदà¥à¤¯à¤¤à¤¨ के बाद रीबूट के लिठजांच या संकेत न करें" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "लॉग डोमेन उपसरà¥à¤— शामिल न करें" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "टाइमसà¥à¤Ÿà¥ˆà¤®à¥à¤ª उपसरà¥à¤— शामिल न करें" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "उपकरण सà¥à¤°à¤•à¥à¤·à¤¾ जांच न करें" #. TRANSLATORS: command line option msgid "Do not prompt for devices" msgstr "उपकरणों के लिठसंकेत न दें" #. TRANSLATORS: command line option msgid "Do not prompt to fix security issues" msgstr "सà¥à¤°à¤•à¥à¤·à¤¾ समसà¥à¤¯à¤¾à¤“ं को ठीक करने के लिठसंकेत न दें" #. TRANSLATORS: command line option msgid "Do not search the firmware when parsing" msgstr "विशà¥à¤²à¥‡à¤·à¤£ करते समय फरà¥à¤®à¤µà¥‡à¤¯à¤° न खोजें" #. TRANSLATORS: warning message shown after update has been scheduled msgid "Do not turn off your computer or remove the AC adaptor while the update is in progress." msgstr "जब अदà¥à¤¯à¤¤à¤¨ चल रहा हो तो अपना कंपà¥à¤¯à¥‚टर बंद न करें या AC à¤à¤¡à¤¾à¤ªà¥à¤Ÿà¤° न हटाà¤à¤‚।" #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "इतिहास डेटाबेस में न लिखें" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "कà¥à¤¯à¤¾ आप फरà¥à¤®à¤µà¥‡à¤¯à¤° शाखा बदलने के परिणामों को समà¤à¤¤à¥‡ हैं?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "कà¥à¤¯à¤¾ आप भविषà¥à¤¯ के अदà¥à¤¯à¤¤à¤¨ के लिठइस सà¥à¤µà¤¿à¤§à¤¾ को अकà¥à¤·à¤® करना चाहते हैं?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "कà¥à¤¯à¤¾ आप अब इस रिमोट को ताजा करना चाहते हैं?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "कà¥à¤¯à¤¾ आप भविषà¥à¤¯ के अदà¥à¤¯à¤¤à¤¨ के लिठसà¥à¤µà¤šà¤¾à¤²à¤¿à¤¤ रूप से रिपोरà¥à¤Ÿ अपलोड करना चाहते हैं?" #. TRANSLATORS: command line option msgid "Don't prompt for authentication (less details may be shown)" msgstr "पà¥à¤°à¤®à¤¾à¤£à¥€à¤•रण के लिठसंकेत न दें (कम विवरण दिखाया जा सकता है)" #. TRANSLATORS: success msgid "Done!" msgstr "संपनà¥à¤¨!" #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "%1$s को %2$s से %3$s तक पदावनत करें?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "उपकरण पर फरà¥à¤®à¤µà¥‡à¤¯à¤° को पदावनत करता है" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "%s को पदावनत किया जा रहा है…" #. TRANSLATORS: command description msgid "Download a file" msgstr "फाइल डाउनलोड करें" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "डाउनलोड किया जा रहा है…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "किसी फाइल से SMBIOS डेटा डंप करें" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "अवधि" #. TRANSLATORS: longer description msgid "Each system should have tests to ensure firmware security." msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° सà¥à¤°à¤•à¥à¤·à¤¾ सà¥à¤¨à¤¿à¤¶à¥à¤šà¤¿à¤¤ करने के लिठपà¥à¤°à¤¤à¥à¤¯à¥‡à¤• सिसà¥à¤Ÿà¤® में परीकà¥à¤·à¤£ होने चाहिà¤à¥¤" #. TRANSLATORS: command description msgid "Emulate a device using a JSON manifest" msgstr "JSON मेनिफेसà¥à¤Ÿ का उपयोग करके उपकरण का अनà¥à¤•रण करें" #. TRANSLATORS: this device is not actually real msgid "Emulated" msgstr "अनà¥à¤•रणित" #. TRANSLATORS: Title: if we are emulating a different host msgid "Emulated host" msgstr "अनà¥à¤•रणीय होसà¥à¤Ÿ" msgid "Enable" msgstr "सकà¥à¤·à¤® करें" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "नया रिमोट सकà¥à¤·à¤® करें?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "इस रिमोट को सकà¥à¤·à¤® करें?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "सकà¥à¤·à¤®" #. TRANSLATORS: Plugin is active only if hardware is found msgid "Enabled if hardware matches" msgstr "यदि हारà¥à¤¡à¤µà¥‡à¤¯à¤° मेल खाता है तो सकà¥à¤·à¤® किया जाता है" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "पà¥à¤°à¤¦à¤¤à¥à¤¤ रिमोट को सकà¥à¤·à¤® करता है" #. TRANSLATORS: longer description msgid "Enabling firmware updates for the BIOS allows fixing security issues." msgstr "BIOS के लिठफरà¥à¤®à¤µà¥‡à¤¯à¤° अदà¥à¤¯à¤¤à¤¨ सकà¥à¤·à¤® करने से सà¥à¤°à¤•à¥à¤·à¤¾ समसà¥à¤¯à¤¾à¤“ं को ठीक किया जा सकता है।" msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "इस कारà¥à¤¯à¤•à¥à¤·à¤®à¤¤à¤¾ को सकà¥à¤·à¤® करना आपके अपने जोखिम पर किया जाता है, जिसका अरà¥à¤¥ है कि आपको इन अदà¥à¤¯à¤¤à¤¨à¥‹à¤‚ के कारण होने वाली किसी भी समसà¥à¤¯à¤¾ के संबंध में अपने मूल उपकरण निरà¥à¤®à¤¾à¤¤à¤¾ से संपरà¥à¤• करना होगा। केवल अदà¥à¤¯à¤¤à¤¨ पà¥à¤°à¤•à¥à¤°à¤¿à¤¯à¤¾ से जà¥à¤¡à¤¼à¥€ समसà¥à¤¯à¤¾à¤“ं को ही $OS_RELEASE:BUG_REPORT_URL$ पर दरà¥à¤œ किया जाना चाहिà¤à¥¤" #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "इस रिमोट को सकà¥à¤·à¤® करना आपके अपने जोखिम पर किया जाता है।" #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "कूटलेखित" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "कूटलेखित RAM" msgid "Encrypted RAM makes it impossible for information that is stored in device memory to be read if the memory chip is removed and accessed." msgstr "यदि मेमोरी चिप को हटा कर उस तक पहà¥à¤‚चने की कोशिश की जाती है तो कूटलेखित RAM उपकरण मेमोरी में संगà¥à¤°à¤¹à¥€à¤¤ जानकारी को पढ़ना असंभव बना देता है।" #. TRANSLATORS: the vendor is no longer supporting the device msgid "End of life" msgstr "जीवन के अनà¥à¤¤ पर" #. TRANSLATORS: The BIOS setting can only be changed to fixed values msgid "Enumeration" msgstr "गणना" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "सभी फरà¥à¤®à¤µà¥‡à¤¯à¤° अदà¥à¤¯à¤¤à¤¨ इतिहास मिटाà¤à¤‚" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "मिटाया जा रहा है…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "कà¥à¤› देर के बाद बाहर निकलें" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "इंजन लोड होने के बाद बाहर निकलें" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° फाइल संरचना को XML में निरà¥à¤¯à¤¾à¤¤ करें" #. TRANSLATORS: command description msgid "Export firmware history for manual upload" msgstr "हसà¥à¤¤à¤šà¤¾à¤²à¤¿à¤¤ अपलोड के लिठफरà¥à¤®à¤µà¥‡à¤¯à¤° इतिहास निरà¥à¤¯à¤¾à¤¤ करें" #. TRANSLATORS: command description msgid "Extract a firmware blob to images" msgstr "छवियों के लिठफरà¥à¤®à¤µà¥‡à¤¯à¤° बà¥à¤²à¥‰à¤¬ निकालें" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "फाइल" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "फाइल [उपकरण-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "फाइलनाम" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "फाइलनाम पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° निजी-कà¥à¤‚जी" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "फाइलनाम उपकरण-ID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME OFFSET DATA [FIRMWARE-TYPE]" msgstr "फाइलनाम ऑफ़सेट डेटा [फरà¥à¤®à¤µà¥‡à¤¯à¤°-पà¥à¤°à¤•ार]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "फाइलनाम [उपकरण-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "फाइलनाम [फरà¥à¤®à¤µà¥‡à¤¯à¤°-पà¥à¤°à¤•ार]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" msgstr "फाइलनाम-SRC फाइलनाम-DST [फरà¥à¤®à¤µà¥‡à¤¯à¤°-पà¥à¤°à¤•ार-SRC] [फरà¥à¤®à¤µà¥‡à¤¯à¤°-पà¥à¤°à¤•ार-DST]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" msgstr "फाइलनाम|चेकसम1[,चेकसà¥à¤®2][,चेकसà¥à¤®3]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "असफल" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "अदà¥à¤¯à¤¤à¤¨ लागू करने में विफल" #. TRANSLATORS: error message for Windows msgid "Failed to connect to Windows service, please ensure it's running." msgstr "Windows सेवा से जà¥à¤¡à¤¼à¤¨à¥‡ में विफल, कृपया सà¥à¤¨à¤¿à¤¶à¥à¤šà¤¿à¤¤ करें कि यह चल रही है।" #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "डेमॉन से जà¥à¤¡à¤¼à¤¨à¥‡ में विफल" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "सà¥à¤¥à¤¾à¤¨à¥€à¤¯ dbx लोड करने में विफल" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "सिसà¥à¤Ÿà¤® dbx लोड करने में विफल" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "लॉक करने में विफल" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "तरà¥à¤•ों का विशà¥à¤²à¥‡à¤·à¤£ करने में विफल" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "फाइल का विशà¥à¤²à¥‡à¤·à¤£ करने में विफल" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "सà¥à¤¥à¤¾à¤¨à¥€à¤¯ dbx का विशà¥à¤²à¥‡à¤·à¤£ करने में विफल" #. TRANSLATORS: a feature is something like "can show an image" msgid "Failed to set front-end features" msgstr "फà¥à¤°à¤‚ट-à¤à¤‚ड सà¥à¤µà¤¿à¤§à¤¾à¤à¤‚ निरà¥à¤§à¤¾à¤°à¤¿à¤¤ करने में विफल" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "ESP सामगà¥à¤°à¥€ सतà¥à¤¯à¤¾à¤ªà¤¿à¤¤ करने में विफल" #. TRANSLATORS: item is FALSE msgid "False" msgstr "गलत" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "फाइलनाम" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "फाइलनाम हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "फाइलनाम सà¥à¤°à¥‹à¤¤" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "फाइलनाम आवशà¥à¤¯à¤•" #. TRANSLATORS: command line option msgid "Filter with a set of device flags using a ~ prefix to exclude, e.g. 'internal,~needs-reboot'" msgstr "बहिषà¥à¤•ृत करने के लिठ~ उपसरà¥à¤— का उपयोग करके उपकरण फà¥à¤²à¥ˆà¤— के समूह के साथ फिलà¥à¤Ÿà¤° करें, उदाहरण के लिठ'internal,~needs-reboot'" #. TRANSLATORS: command line option msgid "Filter with a set of release flags using a ~ prefix to exclude, e.g. 'trusted-release,~trusted-metadata'" msgstr "बाहर करने के लिठ~ उपसरà¥à¤— का उपयोग करके रिलीज़ फà¥à¤²à¥ˆà¤— के समूह के साथ फिलà¥à¤Ÿà¤° करें, उदाहरण के लिठ'trusted-release,~trusted-metadata'" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware Attestation" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° सतà¥à¤¯à¤¾à¤ªà¤¨" #. TRANSLATORS: longer description msgid "Firmware Attestation checks device software using a reference copy, to make sure that it has not been changed." msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° सतà¥à¤¯à¤¾à¤ªà¤¨ à¤à¤• संदरà¥à¤­ पà¥à¤°à¤¤à¤¿ का उपयोग करके उपकरण सॉफà¥à¤Ÿà¤µà¥‡à¤¯à¤° की जांच करता है, यह सà¥à¤¨à¤¿à¤¶à¥à¤šà¤¿à¤¤ करने के लिठकि इसे बदला नहीं गया है।" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° BIOS डिसà¥à¤•à¥à¤°à¤¿à¤ªà¥à¤Ÿà¤°" #. TRANSLATORS: longer description msgid "Firmware BIOS Descriptor protects device firmware memory from being tampered with." msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° BIOS डिसà¥à¤•à¥à¤°à¤¿à¤ªà¥à¤Ÿà¤° उपकरण फरà¥à¤®à¤µà¥‡à¤¯à¤° मेमोरी को छेड़छाड़ होने से बचाता है।" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° BIOS कà¥à¤·à¥‡à¤¤à¥à¤°" #. TRANSLATORS: longer description msgid "Firmware BIOS Region protects device firmware memory from being tampered with." msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° BIOS कà¥à¤·à¥‡à¤¤à¥à¤° उपकरण फरà¥à¤®à¤µà¥‡à¤¯à¤° मेमोरी को छेड़छाड़ से बचाता है।" #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° बेस URI" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° अदà¥à¤¯à¤¤à¤¨ डी-बस सेवा" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° अदà¥à¤¯à¤¤à¤¨ डेमॉन" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° अदà¥à¤¯à¤¤à¤¨à¤•रà¥à¤¤à¤¾ सतà¥à¤¯à¤¾à¤ªà¤¨" msgid "Firmware Updater Verification checks that software used for updating has not been tampered with." msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° अदà¥à¤¯à¤¤à¤¨à¤•रà¥à¤¤à¤¾ सतà¥à¤¯à¤¾à¤ªà¤¨ यह जांचता है कि अदà¥à¤¯à¤¤à¤¨ करने के लिठउपयोग किठगठसॉफà¥à¤Ÿà¤µà¥‡à¤¯à¤° के साथ छेड़छाड़ तो नहीं की गई है।" #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° उपयोगिता" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° लेखन सà¥à¤°à¤•à¥à¤·à¤¾" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° लेखन सà¥à¤°à¤•à¥à¤·à¤¾ लॉक" #. TRANSLATORS: longer description msgid "Firmware Write Protection protects device firmware memory from being tampered with." msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° लेखन सà¥à¤°à¤•à¥à¤·à¤¾ उपकरण फरà¥à¤®à¤µà¥‡à¤¯à¤° मेमोरी को छेड़छाड़ से बचाता है।" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° सतà¥à¤¯à¤¾à¤ªà¤¨" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° पहले से ही अवरà¥à¤¦à¥à¤§ है" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° पहले से अवरà¥à¤¦à¥à¤§ नहीं है" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "फरà¥à¤®à¤µà¥‡à¤¯à¤° मेटाडेटा को %u दिन से अदà¥à¤¯à¤¤à¤¨ नहीं किया गया है और हो सकता है कि वह अदà¥à¤¯à¤¤à¤¿à¤¤ न हो।" msgstr[1] "फ़रà¥à¤®à¤µà¥‡à¤¯à¤° मेटाडेटा %u दिनों से अदà¥à¤¯à¤¤à¤¨ नहीं किया गया है और संभवतः अदà¥à¤¯à¤¤à¤¿à¤¤ भी नहीं है।" #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° अदà¥à¤¯à¤¤à¤¨" #. TRANSLATORS: command description msgid "Fix a specific host security attribute" msgstr "विशिषà¥à¤Ÿ होसà¥à¤Ÿ सà¥à¤°à¤•à¥à¤·à¤¾ विशेषता को ठीक करें" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fix reverted successfully" msgstr "सà¥à¤§à¤¾à¤° सफलतापूरà¥à¤µà¤• पà¥à¤°à¤¤à¥à¤¯à¤¾à¤µà¤°à¥à¤¤à¤¿à¤¤" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fixed successfully" msgstr "सफलतापूरà¥à¤µà¤• ठीक किया गया" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "फ़à¥à¤²à¥ˆà¤—" #. TRANSLATORS: command line option msgid "Force the action by relaxing some runtime checks" msgstr "कà¥à¤› रनटाइम जांचों में ढील देकर कारà¥à¤°à¤µà¤¾à¤ˆ को बाधà¥à¤¯ करें" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "मिला" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "पूरà¥à¤£ डिसà¥à¤• कूटलेखन का पता चला" #. TRANSLATORS: we might ask the user the recovery key when next booting #. Windows msgid "Full disk encryption secrets may be invalidated when updating" msgstr "अदà¥à¤¯à¤¤à¤¨ करते समय पूरà¥à¤£ डिसà¥à¤• कूटलेखन रहसà¥à¤¯ अमानà¥à¤¯ हो सकते हैं" #. TRANSLATORS: Title: if the part has been fused msgid "Fused Platform" msgstr "फ़à¥à¤¯à¥‚जà¥à¤¡ पà¥à¤²à¥‡à¤Ÿà¤«à¤¾à¤°à¥à¤®" #. TRANSLATORS: Title: if the part has been fused msgid "Fused platform" msgstr "फ़à¥à¤¯à¥‚ज़à¥à¤¡ पà¥à¤²à¥‡à¤Ÿà¤«à¤¼à¥‰à¤°à¥à¤®" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" msgstr[1] "GUID's" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "GUID|DEVICE-ID" msgstr "GUID|उपकरण-ID" msgid "Get BIOS settings" msgstr "BIOS सेटिंगà¥à¤¸ पà¥à¤°à¤¾à¤ªà¥à¤¤ करें" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "fwupd दà¥à¤µà¤¾à¤°à¤¾ समरà¥à¤¥à¤¿à¤¤ सभी उपकरण फà¥à¤²à¥ˆà¤— पà¥à¤°à¤¾à¤ªà¥à¤¤ करें" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° अदà¥à¤¯à¤¤à¤¨à¥‹à¤‚ का समरà¥à¤¥à¤¨ करने वाले सभी उपकरण पà¥à¤°à¤¾à¤ªà¥à¤¤ करें" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "सिसà¥à¤Ÿà¤® के साथ पंजीकृत सभी सकà¥à¤·à¤® पà¥à¤²à¤—इनà¥à¤¸ पà¥à¤°à¤¾à¤ªà¥à¤¤ करें" #. TRANSLATORS: command description msgid "Get device report metadata" msgstr "उपकरण रिपोरà¥à¤Ÿ मेटाडेटा पà¥à¤°à¤¾à¤ªà¥à¤¤ करें" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° फाइल की अधिक जानकारी पà¥à¤°à¤¾à¤ªà¥à¤¤ करें " #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "विनà¥à¤¯à¤¸à¥à¤¤ रिमोट पà¥à¤°à¤¾à¤ªà¥à¤¤ करें" #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "होसà¥à¤Ÿ सà¥à¤°à¤•à¥à¤·à¤¾ विशेषताà¤à¤‚ पà¥à¤°à¤¾à¤ªà¥à¤¤ करता है" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "सà¥à¤µà¥€à¤•ृत फरà¥à¤®à¤µà¥‡à¤¯à¤° की सूची पà¥à¤°à¤¾à¤ªà¥à¤¤ करता है" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "अवरà¥à¤¦à¥à¤§ फरà¥à¤®à¤µà¥‡à¤¯à¤° की सूची पà¥à¤°à¤¾à¤ªà¥à¤¤ करता है" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "जà¥à¥œà¥‡ हारà¥à¤¡à¤µà¥‡à¤¯à¤° के लिठअदà¥à¤¯à¤¤à¤¨à¥‹à¤‚ की सूची पà¥à¤°à¤¾à¤ªà¥à¤¤ करता है" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "उपकरण के लिठरिलीज़ पà¥à¤°à¤¾à¤ªà¥à¤¤ करता है" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "अंतिम अदà¥à¤¯à¤¤à¤¨ से परिणाम पà¥à¤°à¤¾à¤ªà¥à¤¤ करता है" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "HWIDS-फाइल" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "हारà¥à¤¡à¤µà¥‡à¤¯à¤° पà¥à¤¨à¤ƒ जोड़े जाने की पà¥à¤°à¤¤à¥€à¤•à¥à¤·à¤¾ कर रहा है" #. TRANSLATORS: the release urgency msgid "High" msgstr "उचà¥à¤š" #. TRANSLATORS: title for host security events msgid "Host Security Events" msgstr "होसà¥à¤Ÿ की सà¥à¤°à¤•à¥à¤·à¤¾ घटनाà¤à¤‚" #. TRANSLATORS: error message for unsupported feature #, c-format msgid "Host Security ID (HSI) is not supported" msgstr "होसà¥à¤Ÿ सà¥à¤°à¤•à¥à¤·à¤¾ ID (HSI) समरà¥à¤¥à¤¿à¤¤ नहीं है" #. TRANSLATORS: success, so say thank you to the user msgid "Host Security ID attributes uploaded successfully, thanks!" msgstr "होसà¥à¤Ÿ सà¥à¤°à¤•à¥à¤·à¤¾ ID विशेषताà¤à¤‚ सफलतापूरà¥à¤µà¤• अपलोड हो गये, धनà¥à¤¯à¤µà¤¾à¤¦!" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "होसà¥à¤Ÿ सà¥à¤°à¤•à¥à¤·à¤¾ ID:" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INHIBIT-ID" msgstr "अवरोधक-ID" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "IOMMU सà¥à¤°à¤•à¥à¤·à¤¾" #. TRANSLATORS: longer description msgid "IOMMU Protection prevents connected devices from accessing unauthorized parts of system memory." msgstr "IOMMU सà¥à¤°à¤•à¥à¤·à¤¾ जà¥à¥œà¥‡ हà¥à¤ डिवाइसों को सिसà¥à¤Ÿà¤® मेमोरी के अनधिकृत भागों तक पहà¥à¤‚चने से रोकती है।" #. TRANSLATORS: HSI event title msgid "IOMMU device protection disabled" msgstr "IOMMU उपकरण सà¥à¤°à¤•à¥à¤·à¤¾ अकà¥à¤·à¤®" #. TRANSLATORS: HSI event title msgid "IOMMU device protection enabled" msgstr "IOMMU उपकरण सà¥à¤°à¤•à¥à¤·à¤¾ सकà¥à¤·à¤®" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "निषà¥à¤•à¥à¤°à¤¿à¤¯â€¦" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "फाइलें डाउनलोड करते समय SSL की सखà¥à¤¤ जांच पर धà¥à¤¯à¤¾à¤¨ न दें" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° चेकसम विफलताओं पर धà¥à¤¯à¤¾à¤¨ न दें" #. TRANSLATORS: command line option msgid "Ignore firmware hardware mismatch failures" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° हारà¥à¤¡à¤µà¥‡à¤¯à¤° बेमेल विफलताओं पर धà¥à¤¯à¤¾à¤¨ न दें" #. TRANSLATORS: try to help msgid "Ignoring SSL strict checks, to do this automatically in the future export DISABLE_SSL_STRICT in your environment" msgstr "भविषà¥à¤¯ में सà¥à¤µà¤šà¤¾à¤²à¤¿à¤¤ रूप से à¤à¤¸à¤¾ करने के लिठSSL सखà¥à¤¤ जांचों को नजरअंदाज करते हà¥à¤ अपने परिवेश में DISABLE_SSL_STRICT निरà¥à¤¯à¤¾à¤¤ करें" #. TRANSLATORS: show the user a generic image that can be themed msgid "Image" msgstr "छवि" #. TRANSLATORS: show the user a random image from the internet msgid "Image (custom)" msgstr "छवि (तदनà¥à¤•ूल)" #. TRANSLATORS: the inhibit ID is a short string like dbus-123456 #, c-format msgid "Inhibit ID is %s." msgstr "अवरोधक ID %s है।" #. TRANSLATORS: command description msgid "Inhibit the system to prevent upgrades" msgstr "उनà¥à¤¨à¤¯à¤¨ को रोकने के लिठसिसà¥à¤Ÿà¤® को बाधित करें" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "सà¥à¤¥à¤¾à¤ªà¤¨à¤¾ अवधि" #. TRANSLATORS: command description msgid "Install a firmware file in cabinet format on this hardware" msgstr "इस हारà¥à¤¡à¤µà¥‡à¤¯à¤° पर कैबिनेट पà¥à¤°à¤¾à¤°à¥‚प में फरà¥à¤®à¤µà¥‡à¤¯à¤° फाइल सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ करें" #. TRANSLATORS: command description msgid "Install a raw firmware blob on a device" msgstr "किसी उपकरण पर रॉ फरà¥à¤®à¤µà¥‡à¤¯à¤° बà¥à¤²à¥‰à¤¬ सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ करें" #. TRANSLATORS: command description msgid "Install a specific firmware file on all devices that match" msgstr "मेल खाने वाले सभी उपकरणों पर à¤à¤• विशिषà¥à¤Ÿ फरà¥à¤®à¤µà¥‡à¤¯à¤° फाइल सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ करें" #. TRANSLATORS: command description msgid "Install a specific firmware on a device, all possible devices will also be installed once the CAB matches" msgstr "किसी उपकरण पर à¤à¤• विशिषà¥à¤Ÿ फरà¥à¤®à¤µà¥‡à¤¯à¤° सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ करें, CAB मैच होने पर सभी संभावित उपकरण भी सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ कर दिठजाà¤à¤‚गे" msgid "Install old version of signed system firmware" msgstr "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¤¿à¤¤ सिसà¥à¤Ÿà¤® फरà¥à¤®à¤µà¥‡à¤¯à¤° का पà¥à¤°à¤¾à¤¨à¤¾ संसà¥à¤•रण सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ करें" msgid "Install old version of unsigned system firmware" msgstr "अहसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¤¿à¤¤ सिसà¥à¤Ÿà¤® फरà¥à¤®à¤µà¥‡à¤¯à¤° का पà¥à¤°à¤¾à¤¨à¤¾ संसà¥à¤•रण सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ करें" msgid "Install signed device firmware" msgstr "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¤¿à¤¤ उपकरण फरà¥à¤®à¤µà¥‡à¤¯à¤° सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ करें" msgid "Install signed system firmware" msgstr "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¤¿à¤¤ सिसà¥à¤Ÿà¤® फरà¥à¤®à¤µà¥‡à¤¯à¤° सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ करें" msgid "Install unsigned device firmware" msgstr "अहसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¤¿à¤¤ उपकरण फरà¥à¤®à¤µà¥‡à¤¯à¤° सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ करें" msgid "Install unsigned system firmware" msgstr "अहसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¤¿à¤¤ सिसà¥à¤Ÿà¤® फरà¥à¤®à¤µà¥‡à¤¯à¤° सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ करें" #. TRANSLATORS: stay on one firmware version unless the new version is #. explicitly #. * specified msgid "Installing a specific release is explicitly required" msgstr "à¤à¤• विशिषà¥à¤Ÿ रिलीज़ को सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ करना सà¥à¤ªà¤·à¥à¤Ÿ रूप से आवशà¥à¤¯à¤• है" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° अदà¥à¤¯à¤¤à¤¨ सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ किया जा रहा है…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "%s पर सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ हो रहा है…" #. TRANSLATORS: if it breaks, you get to keep both pieces msgid "Installing this update may also void any device warranty." msgstr "इस अदà¥à¤¯à¤¤à¤¨ को सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ करने से किसी भी उपकरण की वारंटी भी समापà¥à¤¤ हो सकती है।" #. TRANSLATORS: The BIOS setting only accepts integers in a fixed range msgid "Integer" msgstr "पूरà¥à¤£à¤¾à¤‚क" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM Protected" msgstr "Intel BootGuard ACM संरकà¥à¤·à¤¿à¤¤" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "Intel BootGuard ACM संरकà¥à¤·à¤¿à¤¤" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard Error Policy" msgstr "Intel BootGuard तà¥à¤°à¥à¤Ÿà¤¿ नीति" msgid "Intel BootGuard Error Policy ensures the device does not continue to start if its device software has been tampered with." msgstr "Intel BootGuard तà¥à¤°à¥à¤Ÿà¤¿ नीति यह सà¥à¤¨à¤¿à¤¶à¥à¤šà¤¿à¤¤ करती है कि यदि उपकरण के सॉफà¥à¤Ÿà¤µà¥‡à¤¯à¤° के साथ छेड़छाड़ की गई है तो उपकरण चालू नहीं रहेगी।" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard Fuse" msgstr "Intel BootGuard फà¥à¤¯à¥‚ज" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "Intel BootGuard OTP फà¥à¤¯à¥‚ज" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard Verified Boot" msgstr "Intel BootGuard सतà¥à¤¯à¤¾à¤ªà¤¿à¤¤ बूट" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Intel BootGuard तà¥à¤°à¥à¤Ÿà¤¿ नीति" #. TRANSLATORS: longer description msgid "Intel BootGuard prevents unauthorized device software from operating when the device is started." msgstr "Intel BootGuard उपकरण चालू होने पर अनधिकृत उपकरण सॉफà¥à¤Ÿà¤µà¥‡à¤¯à¤° को संचालित होने से रोकता है।" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Intel BootGuard सतà¥à¤¯à¤¾à¤ªà¤¿à¤¤ बूट" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS Mitigation" msgstr "Intel GDS शमन" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS mitigation" msgstr "Intel GDS शमन" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Manufacturing Mode" msgstr "Intel Management Engine विनिरà¥à¤®à¤¾à¤£ मोड" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the "override" is #. enabled #. * with a jumper -- luckily it is probably not accessible to end users on #. consumer #. * boards msgid "Intel Management Engine Override" msgstr "Intel Management Engine अधà¥à¤¯à¤¾à¤°à¥‹à¤¹à¤£" #. TRANSLATORS: longer description msgid "Intel Management Engine Override disables checks for device software tampering." msgstr "Intel Management Engine अधà¥à¤¯à¤¾à¤°à¥‹à¤¹à¤£ उपकरण सॉफà¥à¤Ÿà¤µà¥‡à¤¯à¤° से छेड़छाड़ की जांच को अकà¥à¤·à¤® कर देता है।" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Version" msgstr "Intel Management Engine संसà¥à¤•रण" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "आंतरिक उपकरण" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "अमानà¥à¤¯" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "अमानà¥à¤¯ तरà¥à¤•" #. TRANSLATORS: error message msgid "Invalid arguments, expected GUID" msgstr "अमानà¥à¤¯ तरà¥à¤•, अपेकà¥à¤·à¤¿à¤¤ GUID" #. TRANSLATOR: This is the error message for #. * incorrect parameter msgid "Invalid arguments, expected an AppStream ID" msgstr "अमानà¥à¤¯ तरà¥à¤•, à¤à¤• à¤à¤ªà¤¸à¥à¤Ÿà¥à¤°à¥€à¤® ID अपेकà¥à¤·à¤¿à¤¤ है" #. TRANSLATORS: error message msgid "Invalid arguments, expected at least ARCHIVE FIRMWARE METAINFO" msgstr "अमानà¥à¤¯ तरà¥à¤•, कम से कम पà¥à¤°à¤¾à¤²à¥‡à¤– फरà¥à¤®à¤µà¥‡à¤¯à¤° मेटाइनà¥à¤«à¥‹ अपेकà¥à¤·à¤¿à¤¤ है" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "पदावनत है" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "बूटलोडर मोड में है" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "उनà¥à¤¨à¤¯à¤¨ है" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "समसà¥à¤¯à¤¾" msgstr[1] "समसà¥à¤¯à¤¾à¤à¤" #. TRANSLATORS: HSI event title msgid "Kernel is no longer tainted" msgstr "करà¥à¤¨à¥‡à¤² अब दागी नहीं है" #. TRANSLATORS: HSI event title msgid "Kernel is tainted" msgstr "करà¥à¤¨à¥‡à¤² दागी है" #. TRANSLATORS: HSI event title msgid "Kernel lockdown disabled" msgstr "करà¥à¤¨à¥‡à¤² लॉकडाउन अकà¥à¤·à¤®" #. TRANSLATORS: HSI event title msgid "Kernel lockdown enabled" msgstr "करà¥à¤¨à¥‡à¤² लॉकडाउन सकà¥à¤·à¤®" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "सà¥à¤¥à¤¾à¤¨" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "अंतिम संशोधित" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "à¤à¤• मिनट से भी कम समय शेष" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "लाइसेंस" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "Linux करà¥à¤¨à¥‡à¤² लॉकडाउन" #. TRANSLATORS: longer description msgid "Linux Kernel Lockdown mode prevents administrator (root) accounts from accessing and changing critical parts of system software." msgstr "Linux करà¥à¤¨à¥‡à¤² लॉकडाउन मोड पà¥à¤°à¤¶à¤¾à¤¸à¤• (रूट) खातों को सिसà¥à¤Ÿà¤® सॉफà¥à¤Ÿà¤µà¥‡à¤¯à¤° के महतà¥à¤µà¤ªà¥‚रà¥à¤£ भागों तक पहà¥à¤‚चने और बदलने से रोकता है।" msgid "Linux Kernel Swap temporarily saves information to disk as you work. If the information is not protected, it could be accessed by someone if they obtained the disk." msgstr "जब आप काम करते हैं तो Linux करà¥à¤¨à¥‡à¤² सà¥à¤µà¥ˆà¤ª असà¥à¤¥à¤¾à¤¯à¥€ रूप से जानकारी को डिसà¥à¤• पर सहेजता है। यदि जानकारी सà¥à¤°à¤•à¥à¤·à¤¿à¤¤ नहीं है, तो डिसà¥à¤• पà¥à¤°à¤¾à¤ªà¥à¤¤ करने पर कोई वà¥à¤¯à¤•à¥à¤¤à¤¿ उस तक पहà¥à¤‚च सकता है।" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "लिनकà¥à¤¸ करà¥à¤¨à¥‡à¤² सतà¥à¤¯à¤¾à¤ªà¤¨" msgid "Linux Kernel Verification makes sure that critical system software has not been tampered with. Using device drivers which are not provided with the system can prevent this security feature from working correctly." msgstr "Linux करà¥à¤¨à¥‡à¤² सतà¥à¤¯à¤¾à¤ªà¤¨ यह सà¥à¤¨à¤¿à¤¶à¥à¤šà¤¿à¤¤ करता है कि महतà¥à¤µà¤ªà¥‚रà¥à¤£ सिसà¥à¤Ÿà¤® सॉफà¥à¤Ÿà¤µà¥‡à¤¯à¤° के साथ छेड़छाड़ नहीं की गई है। à¤à¤¸à¥‡ उपकरण डà¥à¤°à¤¾à¤‡à¤µà¤° का उपयोग करना जो सिसà¥à¤Ÿà¤® के साथ पà¥à¤°à¤¦à¤¾à¤¨ नहीं किठगठहैं, इस सà¥à¤°à¤•à¥à¤·à¤¾ सà¥à¤µà¤¿à¤§à¤¾ को सही ढंग से काम करने से रोक सकते हैं।" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "लिनकà¥à¤¸ सà¥à¤µà¥ˆà¤ª" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Linux विकà¥à¤°à¥‡à¤¤à¤¾ फरà¥à¤®à¤µà¥‡à¤¯à¤° सेवा (सà¥à¤¥à¤¿à¤° फरà¥à¤®à¤µà¥‡à¤¯à¤°)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Linux विकà¥à¤°à¥‡à¤¤à¤¾ फरà¥à¤®à¤µà¥‡à¤¯à¤° सेवा (परीकà¥à¤·à¤£ फरà¥à¤®à¤µà¥‡à¤¯à¤°)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Linux करà¥à¤¨à¥‡à¤²" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Linux करà¥à¤¨à¥‡à¤² लॉकडाउन" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Linux सà¥à¤µà¥ˆà¤ª" #. TRANSLATORS: command description msgid "List EFI variables with a specific GUID" msgstr "विशिषà¥à¤Ÿ GUID के साथ EFI चर सूचीबदà¥à¤§ करें" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "dbx में पà¥à¤°à¤µà¤¿à¤·à¥à¤Ÿà¤¿à¤¯à¤¾à¤‚ सूचीबदà¥à¤§ करें" #. TRANSLATORS: command description msgid "List the available firmware GTypes" msgstr "उपलबà¥à¤§ फरà¥à¤®à¤µà¥‡à¤¯à¤° GTypes की सूची बनाà¤à¤‚" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "उपलबà¥à¤§ फरà¥à¤®à¤µà¥‡à¤¯à¤° पà¥à¤°à¤•ारों की सूची बनाà¤à¤‚" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "ESP पर फाइलें सूचीबदà¥à¤§ करता है" #. TRANSLATORS: command description msgid "Load device emulation data" msgstr "उपकरण अनà¥à¤•रण डेटा लोड करें" #. TRANSLATORS: the plugin was created from a .so object, and was not built-in msgid "Loaded from an external module" msgstr "बाहरी मॉडà¥à¤¯à¥‚ल से लोड किया गया" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "लोड हो रहा है…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "बंद" #. TRANSLATORS: the release urgency msgid "Low" msgstr "निमà¥à¤¨" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refers #. * to the private/public key used to secure loading of firmware msgid "MEI Key Manifest" msgstr "MEI कà¥à¤‚जी मैनिफेसà¥à¤Ÿ" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refer #. * to the private/public key used to secure loading of firmware msgid "MEI key manifest" msgstr "MEI कà¥à¤‚जी मैनिफ़ेसà¥à¤Ÿ" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "MEI विनिरà¥à¤®à¤¾à¤£ मोड" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "MEI अधà¥à¤¯à¤¾à¤°à¥‹à¤¹à¤£" msgid "MEI version" msgstr "MEI संसà¥à¤•रण" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "विशिषà¥à¤Ÿ पà¥à¤²à¤—इनà¥à¤¸ को हसà¥à¤¤à¤šà¤¾à¤²à¤¿à¤¤ रूप से सकà¥à¤·à¤® करें" #. TRANSLATORS: longer description msgid "Manufacturing Mode is used when the device is manufactured and security features are not yet enabled." msgstr "विनिरà¥à¤®à¤¾à¤£ मोड का उपयोग तब किया जाता है जब उपकरण का निरà¥à¤®à¤¾à¤£ किया जाता है और सà¥à¤°à¤•à¥à¤·à¤¾ सà¥à¤µà¤¿à¤§à¤¾à¤à¤‚ अभी तक सकà¥à¤·à¤® नहीं हैं।" #. TRANSLATORS: Longest valid string for BIOS setting msgid "Maximum length" msgstr "अधिकतम लंबाई" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "अधिकतम मान" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "मधà¥à¤¯à¤®" #. TRANSLATORS: ask the user to do a simple task which should be translated msgid "Message" msgstr "संदेश" #. TRANSLATORS: ask the user a question, and it will not be translated msgid "Message (custom)" msgstr "संदेश (तदनà¥à¤•ूल)" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "मेटाडेटा हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "मेटाडेटा URI" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "मेटाडेटा Linux विकà¥à¤°à¥‡à¤¤à¤¾ फरà¥à¤®à¤µà¥‡à¤¯à¤° सेवा से पà¥à¤°à¤¾à¤ªà¥à¤¤ किया जा सकता है।" #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "नà¥à¤¯à¥‚नतम संसà¥à¤•रण" #. TRANSLATORS: Shortest valid string for BIOS setting msgid "Minimum length" msgstr "नà¥à¤¯à¥‚नतम लंबाई" #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "नà¥à¤¯à¥‚नतम मान" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "डेमॉन विनà¥à¤¯à¤¾à¤¸ मान को संशोधित करता है" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "पà¥à¤°à¤¦à¤¤à¥à¤¤ रिमोट को संशोधित करता है" msgid "Modify a configured remote" msgstr "विनà¥à¤¯à¤¸à¥à¤¤ रिमोट को संशोधित करें" msgid "Modify daemon configuration" msgstr "डेमॉन विनà¥à¤¯à¤¾à¤¸ संशोधित करें" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "घटनाओं के लिठडेमॉन पर नजर रखें" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "ESP माउंट करता है" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "सà¥à¤¥à¤¾à¤ªà¤¨à¤¾ के बाद रिबूट की आवशà¥à¤¯à¤•ता है" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "रीबूट आवशà¥à¤¯à¤•" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "सà¥à¤¥à¤¾à¤ªà¤¨à¤¾ के बाद शटडाउन की आवशà¥à¤¯à¤•ता है" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "नया संसà¥à¤•रण" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "कोई कारà¥à¤°à¤µà¤¾à¤ˆ निरà¥à¤¦à¤¿à¤·à¥à¤Ÿ नहीं!" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "%s के लिठकोई पदावनत नहीं" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "कोई फरà¥à¤®à¤µà¥‡à¤¯à¤° ID नहीं मिली" #. TRANSLATORS: nothing found msgid "No firmware found" msgstr "कोई फरà¥à¤®à¤µà¥‡à¤¯à¤° नहीं मिला" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° अदà¥à¤¯à¤¤à¤¨ कà¥à¤·à¤®à¤¤à¤¾ वाला कोई हारà¥à¤¡à¤µà¥‡à¤¯à¤° नहीं मिला" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "कोई रिलीज़ उपलबà¥à¤§ नहीं है" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "वरà¥à¤¤à¤®à¤¾à¤¨ में कोई रिमोट सकà¥à¤·à¤® नहीं है इसलिठकोई मेटाडेटा उपलबà¥à¤§ नहीं है।" #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "कोई रिमोट उपलबà¥à¤§ नहीं" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "कोई अदà¥à¤¯à¤¤à¤¨-योगà¥à¤¯ उपकरण नहीं" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "कोई अदà¥à¤¯à¤¤à¤¨ उपलबà¥à¤§ नहीं" msgid "No updates available for remaining devices" msgstr "शेष उपकरणों के लिठकोई अदà¥à¤¯à¤¤à¤¨ उपलबà¥à¤§ नहीं है" #. TRANSLATORS: version cannot be installed due to policy msgid "Not approved" msgstr "सà¥à¤µà¥€à¤•ृत नहीं" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "नहीं मिला" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "समरà¥à¤¥à¤¿à¤¤ नहीं" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "ठीक है" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "ठीक है!" #. TRANSLATORS: the firmware old version msgid "Old version" msgstr "पà¥à¤°à¤¾à¤¨à¤¾ संसà¥à¤•रण" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "केवल à¤à¤•ल PCR मान दिखाà¤à¤‚" #. TRANSLATORS: command line option msgid "Only use peer-to-peer networking when downloading files" msgstr "फाइल डाउनलोड करते समय केवल पियर-टू-पियर नेटवरà¥à¤•िंग का उपयोग करें" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "केवल संसà¥à¤•रण उनà¥à¤¨à¤¯à¤¨ की अनà¥à¤®à¤¤à¤¿ है" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "तयशà¥à¤¦à¤¾ ESP पथ अधिलेखित करें" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Firmware" msgstr "P2P फरà¥à¤®à¤µà¥‡à¤¯à¤°" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Metadata" msgstr "P2P मेटाडेटा" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "पथ" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° फाइल का विशà¥à¤²à¥‡à¤·à¤£ करें और उसके बारे में विवरण दिखाà¤à¤‚" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "dbx अदà¥à¤¯à¤¤à¤¨ का विशà¥à¤²à¥‡à¤·à¤£â€¦" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "सिसà¥à¤Ÿà¤® dbx का विशà¥à¤²à¥‡à¤·à¤£â€¦" #. TRANSLATORS: remote filename base msgid "Password" msgstr "पासवरà¥à¤¡" #. TRANSLATORS: command description msgid "Patch a firmware blob at a known offset" msgstr "जà¥à¤žà¤¾à¤¤ ऑफसेट पर फरà¥à¤®à¤µà¥‡à¤¯à¤° बà¥à¤²à¥‰à¤¬ को पैच करें" msgid "Payload" msgstr "नीतभार" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "लंबित" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "अभियान निषà¥à¤ªà¤¾à¤¦à¤¿à¤¤ करें?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "पà¥à¤²à¥‡à¤Ÿà¤«à¤¼à¥‰à¤°à¥à¤® डिबगिंग" #. TRANSLATORS: longer description msgid "Platform Debugging allows device security features to be disabled. This should only be used by hardware manufacturers." msgstr "पà¥à¤²à¥‡à¤Ÿà¤«à¤¼à¥‰à¤°à¥à¤® डिबगिंग उपकरण सà¥à¤°à¤•à¥à¤·à¤¾ सà¥à¤µà¤¿à¤§à¤¾à¤“ं को अकà¥à¤·à¤® करने की अनà¥à¤®à¤¤à¤¿ देता है। इसका उपयोग केवल हारà¥à¤¡à¤µà¥‡à¤¯à¤° निरà¥à¤®à¤¾à¤¤à¤¾à¤“ं दà¥à¤µà¤¾à¤°à¤¾ किया जाना चाहिà¤à¥¤" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "पà¥à¤²à¥‡à¤Ÿà¤«à¤¾à¤°à¥à¤® डिबगिंग" #. TRANSLATORS: 'recovery key' here refers to a code, rather than a physical #. metal thing msgid "Please ensure you have the volume recovery key before continuing." msgstr "कृपया जारी रखने से पहले सà¥à¤¨à¤¿à¤¶à¥à¤šà¤¿à¤¤ करें कि आपके पास वॉलà¥à¤¯à¥‚म पà¥à¤¨à¤°à¥à¤ªà¥à¤°à¤¾à¤ªà¥à¤¤à¤¿ कà¥à¤‚जी है।" #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "कृपया 0 से %u तक कोई संखà¥à¤¯à¤¾ दरà¥à¤œ करें: " #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "पà¥à¤²à¤—इन निरà¥à¤­à¤°à¤¤à¤¾à¤à¤‚ गायब हैं" #. TRANSLATORS: Possible values for a bios setting msgid "Possible Values" msgstr "संभावित मान" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA Protection" msgstr "पà¥à¤°à¥€-बूट DMA सà¥à¤°à¤•à¥à¤·à¤¾" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "पà¥à¤°à¥€-बूट DMA सà¥à¤°à¤•à¥à¤·à¤¾" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is disabled" msgstr "पà¥à¤°à¥€-बूट DMA सà¥à¤°à¤•à¥à¤·à¤¾ अकà¥à¤·à¤® है" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is enabled" msgstr "पà¥à¤°à¥€-बूट DMA सà¥à¤°à¤•à¥à¤·à¤¾ सकà¥à¤·à¤® है" #. TRANSLATORS: longer description msgid "Pre-boot DMA protection prevents devices from accessing system memory after being connected to the computer." msgstr "पà¥à¤°à¥€-बूट DMA सà¥à¤°à¤•à¥à¤·à¤¾ उपकरणों को कंपà¥à¤¯à¥‚टर से जोड़ने के बाद सिसà¥à¤Ÿà¤® मेमोरी तक पहà¥à¤‚चने से रोकती है।" #. TRANSLATORS: warning message msgid "Press unlock on the device to continue the update process." msgstr "अदà¥à¤¯à¤¤à¤¨ पà¥à¤°à¤•à¥à¤°à¤¿à¤¯à¤¾ जारी रखने के लिठउपकरण पर अनलॉक दबाà¤à¤‚।" #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "पिछला संसà¥à¤•रण" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "पà¥à¤°à¤¾à¤¥à¤®à¤¿à¤•ता" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "समसà¥à¤¯à¤¾à¤à¤‚" msgid "Proceed with upload?" msgstr "अपलोड जारी रखें?" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Processor Security Checks" msgstr "पà¥à¤°à¥‹à¤¸à¥‡à¤¸à¤° सà¥à¤°à¤•à¥à¤·à¤¾ जांच" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "Processor rollback protection" msgstr "पà¥à¤°à¥‹à¤¸à¥‡à¤¸à¤° रोलबैक सà¥à¤°à¤•à¥à¤·à¤¾" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "मालिकाना" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "रिमोट-ID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID KEY VALUE" msgstr "रिमोट-ID कà¥à¤‚जी मान" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "केवल पढें" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "किसी उपकरण से फरà¥à¤®à¤µà¥‡à¤¯à¤° बà¥à¤²à¥‰à¤¬ पढ़ें" #. TRANSLATORS: command description msgid "Read a firmware from a device" msgstr "किसी उपकरण से फरà¥à¤®à¤µà¥‡à¤¯à¤° पढ़ें" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "%s से पà¥à¤¾ जा रहा है…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "पà¥à¤¾ जा रहा है…" #. TRANSLATORS: how often we should refresh the metadata msgid "Refresh Interval" msgstr "ताजा अंतराल" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "रिमोट सरà¥à¤µà¤° से मेटाडेटा ताजा करें" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "%1$s को %2$s पर पà¥à¤¨à¤ƒ सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ करें?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "उपकरण पर वरà¥à¤¤à¤®à¤¾à¤¨ फरà¥à¤®à¤µà¥‡à¤¯à¤° को पà¥à¤¨à¤°à¥à¤¸à¥à¤¥à¤¾à¤ªà¤¿à¤¤ करें" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "उपकरण पर फरà¥à¤®à¤µà¥‡à¤¯à¤° पà¥à¤¨à¤ƒ सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ करें" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "रिलीज शाखा" #. TRANSLATORS: release attributes msgid "Release Flags" msgstr "रिलीज फà¥à¤²à¥ˆà¤—" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "रिलीज ID" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "रिमोट ID" #. TRANSLATORS: command description msgid "Removes devices to watch for future emulation" msgstr "भविषà¥à¤¯ के अनà¥à¤•रण पर नजर रखने के लिठउपकरणों को हटा देता है" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "रिपोरà¥à¤Ÿ URI" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "रिमोट सरà¥à¤µà¤° को रिपोरà¥à¤Ÿ किया गया" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "आवशà¥à¤¯à¤• efivarfs फाइल सिसà¥à¤Ÿà¤® नहीं मिला" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "आवशà¥à¤¯à¤• हारà¥à¤¡à¤µà¥‡à¤¯à¤° नहीं मिला" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "बूटलोडर आवशà¥à¤¯à¤•" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "इंटरनेट कनेकà¥à¤¶à¤¨ की आवशà¥à¤¯à¤•ता" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "अभी पà¥à¤¨à¤ƒ पà¥à¤°à¤¾à¤°à¤‚भ करें?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "परिवरà¥à¤¤à¤¨ को पà¥à¤°à¤­à¤¾à¤µà¥€ बनाने के लिठडेमॉन को पà¥à¤¨à¤ƒ आरंभ करें?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "उपकरण पà¥à¤¨à¤ƒ पà¥à¤°à¤¾à¤°à¤‚भ हो रहा है…" #. TRANSLATORS: command description msgid "Retrieve BIOS settings. If no arguments are passed all settings are returned" msgstr "BIOS सेटिंगà¥à¤¸ पà¥à¤¨à¤ƒ पà¥à¤°à¤¾à¤ªà¥à¤¤ करें। यदि कोई तरà¥à¤• पारित नहीं किया जाता है तो सभी सेटिंगà¥à¤¸ वापस आ जाती हैं" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "मशीन के लिठसभी हारà¥à¤¡à¤µà¥‡à¤¯à¤° ID लौटाà¤à¤‚" #. TRANSLATORS: ask the user to upload msgid "Review and upload report now?" msgstr "अभी रिपोरà¥à¤Ÿ की समीकà¥à¤·à¤¾ और अपलोड करें?" #. TRANSLATORS: longer description msgid "Rollback Protection prevents device software from being downgraded to an older version that has security problems." msgstr "रोलबैक पà¥à¤°à¥‹à¤Ÿà¥‡à¤•à¥à¤¶à¤¨ उपकरण सॉफà¥à¤Ÿà¤µà¥‡à¤¯à¤° को पà¥à¤°à¤¾à¤¨à¥‡ संसà¥à¤•रण में पदावनत होने से रोकता है जिसमें सà¥à¤°à¤•à¥à¤·à¤¾ समसà¥à¤¯à¤¾à¤à¤‚ होती हैं।" #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "install-blob का उपयोग करते समय पà¥à¤²à¤—इन कमà¥à¤ªà¥‹à¤œà¤¿à¤Ÿ कà¥à¤²à¥€à¤¨à¤…प रूटीन चलाà¤à¤‚" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "install-blob का उपयोग करते समय पà¥à¤²à¤—इन कंपोजिट पà¥à¤°à¤¿à¤ªà¥‡à¤¯à¤° रूटीन चलाà¤à¤‚" #. TRANSLATORS: command description msgid "Run the post-reboot cleanup action" msgstr "रिबूट के बाद सफाई कारà¥à¤°à¤µà¤¾à¤ˆ चलाà¤à¤‚" #. TRANSLATORS: tell a user how to get information #, c-format msgid "Run without '%s' to see" msgstr "देखने के लिठ'%s' के बिना चलाà¤à¤‚" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "चालू करà¥à¤¨à¥‡à¤² बहà¥à¤¤ पà¥à¤°à¤¾à¤¨à¤¾ है" #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "रनटाइम पà¥à¤°à¤¤à¥à¤¯à¤¯" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING VALUE" msgstr "सेटिंग मान" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING1 VALUE1 [SETTING2] [VALUE2]" msgstr "सेटिंग1 मान1 [सेटिंग2] [मान2]" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "SMAP" msgstr "SMAP" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "SPI BIOS डिसà¥à¤•à¥à¤°à¤¿à¤ªà¥à¤Ÿà¤°" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "SPI BIOS कà¥à¤·à¥‡à¤¤à¥à¤°" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "SPI लॉक" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "SPI replay protection" msgstr "SPI रीपà¥à¤²à¥‡ सà¥à¤°à¤•à¥à¤·à¤¾" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "SPI लिखें" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "SPI write protection" msgstr "SPI लेखन सà¥à¤°à¤•à¥à¤·à¤¾" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "उपपà¥à¤°à¤£à¤¾à¤²à¥€ डà¥à¤°à¤¾à¤‡à¤µà¤° [उपकरण-ID|GUID]" #. TRANSLATORS: command description msgid "Save a file that allows generation of hardware IDs" msgstr "à¤à¤• फाइल सहेजें जो हारà¥à¤¡à¤µà¥‡à¤¯à¤° ID बनाने की अनà¥à¤®à¤¤à¤¿ देती है" #. TRANSLATORS: command description msgid "Save device emulation data" msgstr "उपकरण अनà¥à¤•रण डेटा सहेजें" #. TRANSLATORS: key for a offline report filename msgid "Saved report" msgstr "सहेजी गई रिपोरà¥à¤Ÿ" #. TRANSLATORS: Scalar increment for integer BIOS setting msgid "Scalar Increment" msgstr "अदिश वृदà¥à¤§à¤¿" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "समय-निरà¥à¤§à¤¾à¤°à¤£â€¦" #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "सà¥à¤°à¤•à¥à¤·à¤¿à¤¤ बूट अकà¥à¤·à¤®" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "सà¥à¤°à¤•à¥à¤·à¤¿à¤¤ बूट सकà¥à¤·à¤®" msgid "Security hardening for HSI" msgstr "HSI के लिठसà¥à¤°à¤•à¥à¤·à¤¾ कड़ी" #. TRANSLATORS: the %1 is a URL to a wiki page #, c-format msgid "See %s for more details." msgstr "अधिक विवरण के लिठ%s देखें।" #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "अधिक जानकारी के लिठ%s देखें।" #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "चयनित उपकरण" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "चयनित वॉलà¥à¤¯à¥‚म" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "कà¥à¤°à¤®à¤¾à¤‚क" #. TRANSLATORS: Configured a BIOS setting to a value #, c-format msgid "Set BIOS setting '%s' using '%s'." msgstr "'%2$s' का उपयोग करके BIOS सेटिंग '%1$s' निरà¥à¤§à¤¾à¤°à¤¿à¤¤ करें।" #. TRANSLATORS: command description msgid "Set a BIOS setting" msgstr "BIOS सेटिंग निरà¥à¤§à¤¾à¤°à¤¿à¤¤ करें" msgid "Set one or more BIOS settings" msgstr "à¤à¤• या अधिक BIOS सेटिंगà¥à¤¸ निरà¥à¤§à¤¾à¤°à¤¿à¤¤ करें" #. TRANSLATORS: command description msgid "Sets one or more BIOS settings" msgstr "à¤à¤• या अधिक BIOS सेटिंग निरà¥à¤§à¤¾à¤°à¤¿à¤¤ करता है" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "अनà¥à¤®à¥‹à¤¦à¤¿à¤¤ फरà¥à¤®à¤µà¥‡à¤¯à¤° की सूची तय करता है" #. TRANSLATORS: type of BIOS setting msgid "Setting type" msgstr "सेटिंग पà¥à¤°à¤•ार" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "सिसà¥à¤Ÿà¤® रीबूट के बाद सेटिंगà¥à¤¸ लागू होंगी" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "डेवलपरà¥à¤¸ के साथ फरà¥à¤®à¤µà¥‡à¤¯à¤° इतिहास साà¤à¤¾ करें" #. TRANSLATORS: command line option msgid "Show all results" msgstr "सभी परिणाम दिखाà¤à¤‚" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "कà¥à¤²à¤¾à¤‡à¤‚ट और डेमॉन संसà¥à¤•रण दिखाà¤à¤‚" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "किसी विशेष डोमेन के लिठडेमॉन वरà¥à¤¬à¥‹à¤œà¤¼ जानकारी दिखाà¤à¤‚" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "सभी डोमेन के लिठडिबगिंग जानकारी दिखाà¤à¤‚" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "डिबगिंग विकलà¥à¤ª दिखाà¤à¤‚" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "वे उपकरण दिखाà¤à¤‚ जो अदà¥à¤¯à¤¤à¤¨ करने योगà¥à¤¯ नहीं हैं" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "अतिरिकà¥à¤¤ डिबगिंग जानकारी दिखाà¤à¤‚" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° अदà¥à¤¯à¤¤à¤¨à¥‹à¤‚ का इतिहास दिखाà¤à¤‚" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "dbx का परिकलित संसà¥à¤•रण दिखाà¤à¤‚" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "अभी बंद करें?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "नई कà¥à¤‚जी के साथ फरà¥à¤®à¤µà¥‡à¤¯à¤° पर हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° करें" msgid "Sign data using the client certificate" msgstr "कà¥à¤²à¤¾à¤‡à¤‚ट पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° का उपयोग करके डेटा पर हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° करें" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "कà¥à¤²à¤¾à¤‡à¤‚ट पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° का उपयोग कर डेटा पर हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° करें" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "कà¥à¤²à¤¾à¤‡à¤‚ट पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° के साथ अपलोड किठगठडेटा पर हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° करें" msgid "Signature" msgstr "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°" #. TRANSLATORS: firmware is verified on-device the payload using strong crypto msgid "Signed Payload" msgstr "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¤¿à¤¤ नीतभार" #. TRANSLATORS: file size of the download msgid "Size" msgstr "आकार" #. TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM msgid "Some of the platform secrets may be invalidated when updating this firmware." msgstr "इस फरà¥à¤®à¤µà¥‡à¤¯à¤° को अदà¥à¤¯à¤¤à¤¨ करते समय कà¥à¤› पà¥à¤²à¥‡à¤Ÿà¤«à¤¼à¥‰à¤°à¥à¤® रहसà¥à¤¯ अमानà¥à¤¯ हो सकते हैं।" #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "सà¥à¤°à¥‹à¤¤" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "dbx डेटाबेस फाइल निरà¥à¤¦à¤¿à¤·à¥à¤Ÿ करें" #. TRANSLATORS: The BIOS setting accepts strings msgid "String" msgstr "सà¥à¤Ÿà¥à¤°à¤¿à¤‚ग" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "सफल" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "सभी उपकरण सफलतापूरà¥à¤µà¤• सकà¥à¤°à¤¿à¤¯ हो गà¤" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "रिमोट सफलतापूरà¥à¤µà¤• अकà¥à¤·à¤® किया गया" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "नया मेटाडेटा सफलतापूरà¥à¤µà¤• डाउनलोड किया गया: " #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "रिमोट को सफलतापूरà¥à¤µà¤• सकà¥à¤·à¤® और ताजा किया गया" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "रिमोट सफलतापूरà¥à¤µà¤• सकà¥à¤·à¤®" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° सफलतापूरà¥à¤µà¤• सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ किया गया" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "विनà¥à¤¯à¤¾à¤¸ मान को सफलतापूरà¥à¤µà¤• संशोधित किया गया" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "रिमोट सफलतापूरà¥à¤µà¤• संशोधित" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "मेटाडेटा को हसà¥à¤¤à¤šà¤¾à¤²à¤¿à¤¤ रूप से सफलतापूरà¥à¤µà¤• ताजा किया गया" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "उपकरण चेकसम सफलतापूरà¥à¤µà¤• अदà¥à¤¯à¤¤à¤¨ किया गया" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "%u रिपोरà¥à¤Ÿ सफलतापूरà¥à¤µà¤• अपलोड हो गई" msgstr[1] "%u रिपोरà¥à¤Ÿ सफलतापूरà¥à¤µà¤• अपलोड की गई" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "उपकरण चेकसम सफलतापूरà¥à¤µà¤• सतà¥à¤¯à¤¾à¤ªà¤¿à¤¤" #. TRANSLATORS: the device showed up in time #, c-format msgid "Successfully waited %.0fms for device" msgstr "उपकरण के लिठसफलतापूरà¥à¤µà¤• %.0fms पà¥à¤°à¤¤à¥€à¤•à¥à¤·à¤¾ की गई" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "सारांश" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "Supervisor Mode Access Prevention" msgstr "परà¥à¤¯à¤µà¥‡à¤•à¥à¤·à¤• मोड पहà¥à¤‚च रोकथाम" #. TRANSLATORS: longer description msgid "Supervisor Mode Access Prevention ensures critical parts of device memory are not accessed by less secure programs." msgstr "सà¥à¤ªà¤°à¤µà¤¾à¤‡à¤œà¤¼à¤° मोड à¤à¤•à¥à¤¸à¥‡à¤¸ पà¥à¤°à¤¿à¤µà¥‡à¤‚शन यह सà¥à¤¨à¤¿à¤¶à¥à¤šà¤¿à¤¤ करता है कि उपकरण मेमोरी के महतà¥à¤µà¤ªà¥‚रà¥à¤£ हिसà¥à¤¸à¥‡ कम सà¥à¤°à¤•à¥à¤·à¤¿à¤¤ पà¥à¤°à¥‹à¤—à¥à¤°à¤¾à¤® दà¥à¤µà¤¾à¤°à¤¾ पहà¥à¤‚चे नहीं जाà¤à¤‚गे।" #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "समरà¥à¤¥à¤¿à¤¤" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "समरà¥à¤¥à¤¿à¤¤ CPU" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "रिमोट सरà¥à¤µà¤° पर समरà¥à¤¥à¤¿à¤¤" #. TRANSLATORS: Title: a better sleep state msgid "Suspend To Idle" msgstr "निषà¥à¤•à¥à¤°à¤¿à¤¯ पर निलंबित" #. TRANSLATORS: Title: sleep state msgid "Suspend To RAM" msgstr "RAM पर निलंबित" #. TRANSLATORS: longer description msgid "Suspend to Idle allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "निषà¥à¤•à¥à¤°à¤¿à¤¯ हेतॠनिलंबित करने से बिजली बचाने के लिठउपकरण जलà¥à¤¦à¥€ से सो जाता है। जबकि उपकरण को निलंबित कर दिया गया है, इसकी मेमोरी को भौतिक रूप से हटाया जा सकता है और इसकी जानकारी तक पहà¥à¤‚च बनाई जा सकती है।" #. TRANSLATORS: longer description msgid "Suspend to RAM allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "RAM को निलंबित करने से बिजली बचाने के लिठउपकरण जलà¥à¤¦à¥€ से सो जाता है। जबकि उपकरण को निलंबित कर दिया गया है, इसकी मेमोरी को भौतिक रूप से हटाया जा सकता है और इसकी जानकारी तक पहà¥à¤‚च बनाई जा सकती है।" #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "निषà¥à¤•à¥à¤°à¤¿à¤¯-पर-निलंबित" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "रैम-पर-निलंबित" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "शाखा को %1$s से %2$s में बदलें?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "उपकरण पर फरà¥à¤®à¤µà¥‡à¤¯à¤° शाखा बदलें" #. TRANSLATORS: command description msgid "Sync firmware versions to the chosen configuration" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° संसà¥à¤•रणों को चà¥à¤¨à¥‡ हà¥à¤ विनà¥à¤¯à¤¾à¤¸ में समनà¥à¤µà¤¯à¤¿à¤¤ करें" #. TRANSLATORS: this CLI tool is now preventing system updates msgid "System Update Inhibited" msgstr "सिसà¥à¤Ÿà¤® अदà¥à¤¯à¤¤à¤¨ बाधित" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "सिसà¥à¤Ÿà¤® को बाहरी ऊरà¥à¤œà¤¾ सà¥à¤°à¥‹à¤¤ की आवशà¥à¤¯à¤•ता है" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "पाठ" #. TRANSLATORS: longer description msgid "TPM (Trusted Platform Module) is a computer chip that detects when hardware components have been tampered with." msgstr "TPM (टà¥à¤°à¤¸à¥à¤Ÿà¥‡à¤¡ पà¥à¤²à¥‡à¤Ÿà¤«à¤¼à¥‰à¤°à¥à¤® मॉडà¥à¤¯à¥‚ल) à¤à¤• कंपà¥à¤¯à¥‚टर चिप है जो यह पता लगाती है कि हारà¥à¤¡à¤µà¥‡à¤¯à¤° घटकों के साथ छेड़छाड़ की गई है।" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "TPM PCR0 पà¥à¤¨à¤°à¥à¤¨à¤¿à¤°à¥à¤®à¤¾à¤£" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is invalid" msgstr "TPM PCR0 पà¥à¤¨à¤°à¥à¤¨à¤¿à¤°à¥à¤®à¤¾à¤£ अमानà¥à¤¯ है" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is now valid" msgstr "TPM PCR0 पà¥à¤¨à¤°à¥à¤¨à¤¿à¤°à¥à¤®à¤¾à¤£ अब मानà¥à¤¯ है" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM Platform Configuration" msgstr "TPM पà¥à¤²à¥‡à¤Ÿà¤«à¤¾à¤°à¥à¤® विनà¥à¤¯à¤¾à¤¸" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM Reconstruction" msgstr "TPM पà¥à¤¨à¤°à¥à¤¨à¤¿à¤°à¥à¤®à¤¾à¤£" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM empty PCRs" msgstr "TPM PCR खाली" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "टैग" msgstr[1] "टैग" #. TRANSLATORS: we're saving all USB events for emulation msgid "Tagged for emulation" msgstr "अनà¥à¤•रण हेतॠटैग किया गया" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "दूषित" #. show the user the entire data blob msgid "Target" msgstr "लकà¥à¤·à¥à¤¯" #. TRANSLATORS: command description msgid "Test a device using a JSON manifest" msgstr "JSON मेनिफेसà¥à¤Ÿ का उपयोग करके उपकरण का परीकà¥à¤·à¤£ करें" #. TRANSLATORS: when the release was tested msgid "Tested" msgstr "परीकà¥à¤·à¤¿à¤¤" #. TRANSLATORS: the %s is a vendor name, e.g. Lenovo #, c-format msgid "Tested by %s" msgstr "%s दà¥à¤µà¤¾à¤°à¤¾ परीकà¥à¤·à¤¿à¤¤" #. TRANSLATORS: someone we trust has tested this msgid "Tested by trusted vendor" msgstr "विशà¥à¤µà¤¸à¤¨à¥€à¤¯ विकà¥à¤°à¥‡à¤¤à¤¾ दà¥à¤µà¤¾à¤°à¤¾ परीकà¥à¤·à¤¿à¤¤" #. TRANSLATORS: longer description msgid "The Intel Management Engine Key Manifest must be valid so that the device firmware can be trusted by the CPU." msgstr "Intel Management Engine कà¥à¤‚जी मैनिफ़ेसà¥à¤Ÿ मानà¥à¤¯ होना चाहिठताकि उपकरण फरà¥à¤®à¤µà¥‡à¤¯à¤° पर CPU दà¥à¤µà¤¾à¤°à¤¾ भरोसा किया जा सके।" #. TRANSLATORS: longer description msgid "The Intel Management Engine controls device components and needs to have a recent version to avoid security issues." msgstr "Intel Management Engine उपकरण घटकों को नियंतà¥à¤°à¤¿à¤¤ करता है और सà¥à¤°à¤•à¥à¤·à¤¾ समसà¥à¤¯à¤¾à¤“ं से बचने के लिठनवीनतम संसà¥à¤•रण की आवशà¥à¤¯à¤•ता होती है।" #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "LVFS à¤à¤• निःशà¥à¤²à¥à¤• सेवा है जो à¤à¤• सà¥à¤µà¤¤à¤‚तà¥à¤° कानूनी इकाई के रूप में कारà¥à¤¯ करती है और इसका $OS_RELEASE:NAME$ से कोई संबंध नहीं है। हो सकता है कि आपके वितरक ने आपके सिसà¥à¤Ÿà¤® या जà¥à¥œà¥‡ हà¥à¤ उपकरण के साथ संगतता के लिठकिसी भी फरà¥à¤®à¤µà¥‡à¤¯à¤° अदà¥à¤¯à¤¤à¤¨ को सतà¥à¤¯à¤¾à¤ªà¤¿à¤¤ नहीं किया हो। सभी फरà¥à¤®à¤µà¥‡à¤¯à¤° केवल मूल उपकरण निरà¥à¤®à¤¾à¤¤à¤¾ दà¥à¤µà¤¾à¤°à¤¾ पà¥à¤°à¤¦à¤¾à¤¨ किठजाते हैं।" #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Platform Configuration is used to check whether the device start process has been modified." msgstr "TPM (टà¥à¤°à¤¸à¥à¤Ÿà¥‡à¤¡ पà¥à¤²à¥‡à¤Ÿà¤«à¤¼à¥‰à¤°à¥à¤® मॉडà¥à¤¯à¥‚ल) पà¥à¤²à¥‡à¤Ÿà¤«à¤¼à¥‰à¤°à¥à¤® विनà¥à¤¯à¤¾à¤¸ का उपयोग यह जांचने के लिठकिया जाता है कि उपकरण पà¥à¤°à¤¾à¤°à¤‚भ पà¥à¤°à¤•à¥à¤°à¤¿à¤¯à¤¾ को संशोधित किया गया है या नहीं।" #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Reconstruction is used to check whether the device start process has been modified." msgstr "TPM (टà¥à¤°à¤¸à¥à¤Ÿà¥‡à¤¡ पà¥à¤²à¥‡à¤Ÿà¤«à¤¼à¥‰à¤°à¥à¤® मॉडà¥à¤¯à¥‚ल) पà¥à¤¨à¤°à¥à¤¨à¤¿à¤°à¥à¤®à¤¾à¤£ का उपयोग यह जांचने के लिठकिया जाता है कि उपकरण पà¥à¤°à¤¾à¤°à¤‚भ पà¥à¤°à¤•à¥à¤°à¤¿à¤¯à¤¾ को संशोधित किया गया है या नहीं।" #. TRANSLATORS: this is more background on a security measurement problem msgid "The TPM PCR0 differs from reconstruction." msgstr "TPM PCR0 पà¥à¤¨à¤°à¥à¤¨à¤¿à¤°à¥à¤®à¤¾à¤£ से भिनà¥à¤¨ है।" #. TRANSLATORS: longer description msgid "The UEFI Platform Key is used to determine if device software comes from a trusted source." msgstr "UEFI पà¥à¤²à¥‡à¤Ÿà¤«à¤¼à¥‰à¤°à¥à¤® कà¥à¤‚जी का उपयोग यह निरà¥à¤§à¤¾à¤°à¤¿à¤¤ करने के लिठकिया जाता है कि उपकरण सॉफà¥à¤Ÿà¤µà¥‡à¤¯à¤° किसी विशà¥à¤µà¤¸à¤¨à¥€à¤¯ सà¥à¤°à¥‹à¤¤ से आता है या नहीं।" #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "डेमॉन ने तृतीय पकà¥à¤· कोड लोड कर दिया है और अब यह अपसà¥à¤Ÿà¥à¤°à¥€à¤® डेवलपरà¥à¤¸ दà¥à¤µà¤¾à¤°à¤¾ समरà¥à¤¥à¤¿à¤¤ नहीं है!" #. TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name #, c-format msgid "The firmware from %s is not supplied by %s, the hardware vendor." msgstr "%1$s के फरà¥à¤®à¤µà¥‡à¤¯à¤° की आपूरà¥à¤¤à¤¿ हारà¥à¤¡à¤µà¥‡à¤¯à¤° विकà¥à¤°à¥‡à¤¤à¤¾ %2$s दà¥à¤µà¤¾à¤°à¤¾ नहीं की जाती है।" #. TRANSLATORS: try to help msgid "The system clock has not been set correctly and downloading files may fail." msgstr "सिसà¥à¤Ÿà¤® घड़ी सही ढंग से निरà¥à¤§à¤¾à¤°à¤¿à¤¤ नहीं की गई है और फाइलें डाउनलोड करना विफल हो सकता है।" #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been re-inserted." msgstr "उपकरण में USB केबल दोबारा डालने पर अदà¥à¤¯à¤¤à¤¨ जारी रहेगा।" #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged and then re-inserted." msgstr "उपकरण USB केबल को अनपà¥à¤²à¤— करने और फिर से डालने पर अदà¥à¤¯à¤¤à¤¨ जारी रहेगा।" #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged." msgstr "उपकरण USB केबल अनपà¥à¤²à¤— होने पर अदà¥à¤¯à¤¤à¤¨ जारी रहेगा।" #. TRANSLATORS: warning message msgid "The update will continue when the device power cable has been removed and re-inserted." msgstr "उपकरण पावर केबल को हटाकर दोबारा डालने पर अदà¥à¤¯à¤¤à¤¨ जारी रहेगा।" #. TRANSLATORS: naughty vendor msgid "The vendor did not supply any release notes." msgstr "विकà¥à¤°à¥‡à¤¤à¤¾ ने कोई रिलीज़ नोट उपलबà¥à¤§ नहीं कराया।" #. TRANSLATORS: now list devices with unfixed high-priority issues msgid "There are devices with issues:" msgstr "à¤à¤¸à¥‡ उपकरण हैं जिनमें समसà¥à¤¯à¤¾à¤à¤‚ हैं:" #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "कोई अवरà¥à¤¦à¥à¤§ फरà¥à¤®à¤µà¥‡à¤¯à¤° फाइलें नहीं हैं" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "कोई सà¥à¤µà¥€à¤•ृत फरà¥à¤®à¤µà¥‡à¤¯à¤° नहीं है।" #. TRANSLATORS: %1 is the current device version number, and %2 is the #. command name, e.g. `fwupdmgr sync` #, c-format msgid "This device will be reverted back to %s when the %s command is performed." msgstr "%2$s कमांड निषà¥à¤ªà¤¾à¤¦à¤¿à¤¤ होने पर यह उपकरण वापस %1$s पर वापस आ जाà¤à¤—ा।" #. TRANSLATORS: the vendor did not upload this msgid "This firmware is provided by LVFS community members and is not provided (or supported) by the original hardware vendor." msgstr "यह फरà¥à¤®à¤µà¥‡à¤¯à¤° LVFS समà¥à¤¦à¤¾à¤¯ के सदसà¥à¤¯à¥‹à¤‚ दà¥à¤µà¤¾à¤°à¤¾ पà¥à¤°à¤¦à¤¾à¤¨ किया जाता है और मूल हारà¥à¤¡à¤µà¥‡à¤¯à¤° विकà¥à¤°à¥‡à¤¤à¤¾ दà¥à¤µà¤¾à¤°à¤¾ पà¥à¤°à¤¦à¤¾à¤¨ (या समरà¥à¤¥à¤¿à¤¤) नहीं किया जाता है।" #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "यह पैकेज सतà¥à¤¯à¤¾à¤ªà¤¿à¤¤ नहीं किया गया है, यह ठीक से काम नहीं कर सकता है।" #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "यह पà¥à¤°à¥‹à¤—à¥à¤°à¤¾à¤® केवल रूट के रूप में ही सही ढंग से काम कर सकता है" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "इस रिमोट में फरà¥à¤®à¤µà¥‡à¤¯à¤° है जिस पर पà¥à¤°à¤¤à¤¿à¤¬à¤‚ध नहीं है, लेकिन हारà¥à¤¡à¤µà¥‡à¤¯à¤° विकà¥à¤°à¥‡à¤¤à¤¾ दà¥à¤µà¤¾à¤°à¤¾ अभी भी इसका परीकà¥à¤·à¤£ किया जा रहा है। आपको यह सà¥à¤¨à¤¿à¤¶à¥à¤šà¤¿à¤¤ करना चाहिठकि फरà¥à¤®à¤µà¥‡à¤¯à¤° अदà¥à¤¯à¤¤à¤¨ विफल होने पर आपके पास फरà¥à¤®à¤µà¥‡à¤¯à¤° को हसà¥à¤¤à¤šà¤¾à¤²à¤¿à¤¤ रूप से पदावनत करने का à¤à¤• तरीका है।" #. TRANSLATORS: error message msgid "This system doesn't support firmware settings" msgstr "यह सिसà¥à¤Ÿà¤® फरà¥à¤®à¤µà¥‡à¤¯à¤° सेटिंगà¥à¤¸ का समरà¥à¤¥à¤¨ नहीं करता है" #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "इस सिसà¥à¤Ÿà¤® में HSI रनटाइम समसà¥à¤¯à¤¾à¤à¤‚ हैं।" #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "इस पà¥à¤°à¤£à¤¾à¤²à¥€ में निमà¥à¤¨ HSI सà¥à¤°à¤•à¥à¤·à¤¾ सà¥à¤¤à¤° है।" #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "यह टूल पà¥à¤°à¤¶à¤¾à¤¸à¤• को UEFI dbx अदà¥à¤¯à¤¤à¤¨ लागू करने की अनà¥à¤®à¤¤à¤¿ देता है।" #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "यह उपकरण à¤à¤• पà¥à¤°à¤¶à¤¾à¤¸à¤• को fwupd डेमॉन को कà¥à¤µà¥‡à¤°à¥€ करने और नियंतà¥à¤°à¤¿à¤¤ करने की अनà¥à¤®à¤¤à¤¿ देता है, जिससे उनà¥à¤¹à¥‡à¤‚ फरà¥à¤®à¤µà¥‡à¤¯à¤° सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ करने या पदावनत करने जैसी कारà¥à¤°à¤µà¤¾à¤‡à¤¯à¤¾à¤‚ करने की अनà¥à¤®à¤¤à¤¿ मिलती है।" #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "यह टूल à¤à¤• पà¥à¤°à¤¶à¤¾à¤¸à¤• को होसà¥à¤Ÿ सिसà¥à¤Ÿà¤® पर सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ किठबिना fwupd पà¥à¤²à¤—इनà¥à¤¸ का उपयोग करने की अनà¥à¤®à¤¤à¤¿ देता है।" #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can add a kernel argument of '%s', but it will only be active after restarting the computer." msgstr "यह उपकरण '%s' का करà¥à¤¨à¥‡à¤² तरà¥à¤• जोड़ सकता है, लेकिन यह केवल कंपà¥à¤¯à¥‚टर को पà¥à¤¨à¤°à¤¾à¤°à¤‚भ करने के बाद ही सकà¥à¤°à¤¿à¤¯ होगा।" #. TRANSLATORS: the %1 is a BIOS setting name. #. * %2 and %3 are the values, e.g. "True" or "Windows10" #, c-format msgid "This tool can change the BIOS setting '%s' from '%s' to '%s' automatically, but it will only be active after restarting the computer." msgstr "यह टूल सà¥à¤µà¤šà¤¾à¤²à¤¿à¤¤ रूप से BIOS सेटिंग '%1$s' को '%2$s' से '%3$s' में बदल सकता है, लेकिन यह केवल कंपà¥à¤¯à¥‚टर को पà¥à¤¨à¤°à¤¾à¤°à¤‚भ करने के बाद ही सकà¥à¤°à¤¿à¤¯ होगा।" #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can change the kernel argument from '%s' to '%s', but it will only be active after restarting the computer." msgstr "यह टूल करà¥à¤¨à¥‡à¤² तरà¥à¤• को '%1$s' से '%2$s' में बदल सकता है, लेकिन यह केवल कंपà¥à¤¯à¥‚टर को पà¥à¤¨à¤°à¤¾à¤°à¤‚भ करने के बाद ही सकà¥à¤°à¤¿à¤¯ होगा।" #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "यह टूल सिसà¥à¤Ÿà¤® फरà¥à¤®à¤µà¥‡à¤¯à¤° से TPM इवेंट लॉग को पढ़ेगा और विशà¥à¤²à¥‡à¤·à¤£ करेगा।" #. TRANSLATORS: the update state of the specific device msgid "Transient failure" msgstr "कà¥à¤·à¤£à¤¿à¤• विफलता" #. TRANSLATORS: item is TRUE msgid "True" msgstr "सही" #. TRANSLATORS: We verified the metadata against the server msgid "Trusted metadata" msgstr "विशà¥à¤µà¤¸à¤¨à¥€à¤¯ मेटाडेटा" #. TRANSLATORS: We verified the payload against the server msgid "Trusted payload" msgstr "विशà¥à¤µà¤¸à¤¨à¥€à¤¯ नीतभार" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "पà¥à¤°à¤•ार" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI Bootservice Variables" msgstr "UEFI बूटसेवा चर" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition may not be set up correctly" msgstr "UEFI ESP विभाजन सही ढंग से निरà¥à¤§à¤¾à¤°à¤¿à¤¤ नहीं किया जा सकता है" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "UEFI ESP विभाजन का पता नहीं चला या विनà¥à¤¯à¤¸à¥à¤¤ नहीं किया गया" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "UEFI पà¥à¤²à¥‡à¤Ÿà¤«à¤¾à¤°à¥à¤® कà¥à¤‚जी" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "UEFI सà¥à¤°à¤•à¥à¤·à¤¿à¤¤ बूट" #. TRANSLATORS: longer description msgid "UEFI Secure Boot prevents malicious software from being loaded when the device starts." msgstr "UEFI सिकà¥à¤¯à¥‹à¤° बूट उपकरण शà¥à¤°à¥‚ होने पर दà¥à¤°à¥à¤­à¤¾à¤µà¤¨à¤¾à¤ªà¥‚रà¥à¤£ सॉफà¥à¤Ÿà¤µà¥‡à¤¯à¤° को लोड होने से रोकता है।" #. TRANSLATORS: longer description msgid "UEFI boot service variables should not be readable from runtime mode." msgstr "UEFI बूट सेवा चर रनटाइम मोड से पढ़ने योगà¥à¤¯ नहीं होना चाहिà¤à¥¤" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI bootservice variables" msgstr "UEFI बूटसेवा चर" #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "UEFI कैपà¥à¤¸à¥‚ल अदà¥à¤¯à¤¤à¤¨ फरà¥à¤®à¤µà¥‡à¤¯à¤° सेटअप में उपलबà¥à¤§ या सकà¥à¤·à¤® नहीं हैं" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "UEFI dbx उपयोगिता" #. TRANSLATORS: system is not booted in UEFI mode msgid "UEFI firmware can not be updated in legacy BIOS mode" msgstr "UEFI फरà¥à¤®à¤µà¥‡à¤¯à¤° को लीगेसी BIOS मोड में अदà¥à¤¯à¤¤à¤¨ नहीं किया जा सकता है" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "UEFI पà¥à¤²à¥‡à¤Ÿà¤«à¤¼à¥‰à¤°à¥à¤® कà¥à¤‚जी" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "UEFI सà¥à¤°à¤•à¥à¤·à¤¿à¤¤ बूट" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "सेवा से जà¥à¤¡à¤¼à¤¨à¥‡ में असमरà¥à¤¥" #. TRANSLATORS: error message msgid "Unable to find attribute" msgstr "विशेषता खोजने में असमरà¥à¤¥" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "वरà¥à¤¤à¤®à¤¾à¤¨ डà¥à¤°à¤¾à¤‡à¤µà¤° को अनबाइंड करें" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° को अनवरà¥à¤¦à¥à¤§ किया जा रहा है:" #. TRANSLATORS: command description msgid "Unblocks a specific firmware from being installed" msgstr "किसी विशिषà¥à¤Ÿ फरà¥à¤®à¤µà¥‡à¤¯à¤° को सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ होने से अनवरà¥à¤¦à¥à¤§ करता है" #. TRANSLATORS: command description msgid "Undo the host security attribute fix" msgstr "होसà¥à¤Ÿ सà¥à¤°à¤•à¥à¤·à¤¾ विशेषता सà¥à¤§à¤¾à¤° को पूरà¥à¤µà¤µà¤¤ करें" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "अकूटलेखित" #. TRANSLATORS: command description msgid "Uninhibit the system to allow upgrades" msgstr "उनà¥à¤¨à¤¯à¤¨ की अनà¥à¤®à¤¤à¤¿ देने के लिठसिसà¥à¤Ÿà¤® को अवरोधमà¥à¤•à¥à¤¤ करें" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "अजà¥à¤žà¤¾à¤¤" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "अजà¥à¤žà¤¾à¤¤ उपकरण" msgid "Unlock the device to allow access" msgstr "पहà¥à¤‚च की अनà¥à¤®à¤¤à¤¿ देने के लिठउपकरण को अनलॉक करें" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "Unlocked" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° पहà¥à¤‚च के लिठउपकरण को अनलॉक करता है" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "ESP को अनमाउंट करता है" #. TRANSLATORS: message shown after device has been marked for emulation msgid "Unplug and replug the device to continue the update process." msgstr "अदà¥à¤¯à¤¤à¤¨ पà¥à¤°à¤•à¥à¤°à¤¿à¤¯à¤¾ जारी रखने के लिठउपकरण को अनपà¥à¤²à¤— करें और पà¥à¤¨à¤ƒ पà¥à¤²à¤— करें।" #. TRANSLATORS: firmware payload is unsigned and it is possible to modify it msgid "Unsigned Payload" msgstr "अहसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¤¿à¤¤ नीतभार" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "असमरà¥à¤¥à¤¿à¤¤ डेमॉन संसà¥à¤•रण %s, कà¥à¤²à¤¾à¤‡à¤‚ट संसà¥à¤•रण %s है" #. TRANSLATORS: Suffix: the HSI result msgid "Untainted" msgstr "अदूषित" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "अदà¥à¤¯à¤¤à¤¨-योगà¥à¤¯" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "अदà¥à¤¯à¤¤à¤¨ तà¥à¤°à¥à¤Ÿà¤¿" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "अदà¥à¤¯à¤¤à¤¨ छवि" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "अदà¥à¤¯à¤¤à¤¨ संदेश" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "अदà¥à¤¯à¤¤à¤¨ सà¥à¤¥à¤¿à¤¤à¤¿" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "अदà¥à¤¯à¤¤à¤¨ विफलता à¤à¤• जà¥à¤žà¤¾à¤¤ समसà¥à¤¯à¤¾ है, अधिक जानकारी के लिठइस URL पर जाà¤à¤‚:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "अभी अदà¥à¤¯à¤¤à¤¨ करें?" #. TRANSLATORS: command description msgid "Update the stored cryptographic hash with current ROM contents" msgstr "वरà¥à¤¤à¤®à¤¾à¤¨ ROM सामगà¥à¤°à¥€ के साथ संगà¥à¤°à¤¹à¥€à¤¤ कà¥à¤°à¤¿à¤ªà¥à¤Ÿà¥‹à¤—à¥à¤°à¤¾à¤«à¤¿à¤• हैश को अदà¥à¤¯à¤¤à¤¨ करें" msgid "Update the stored device verification information" msgstr "संगà¥à¤°à¤¹à¥€à¤¤ उपकरण सतà¥à¤¯à¤¾à¤ªà¤¨ जानकारी अदà¥à¤¯à¤¤à¤¨ करें" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "संगà¥à¤°à¤¹à¥€à¤¤ मेटाडेटा को वरà¥à¤¤à¤®à¤¾à¤¨ सामगà¥à¤°à¥€ के साथ अदà¥à¤¯à¤¤à¤¨ करें" #. TRANSLATORS: command description msgid "Updates all specified devices to latest firmware version, or all devices if unspecified" msgstr "सभी निरà¥à¤¦à¤¿à¤·à¥à¤Ÿ उपकरणों को नवीनतम फरà¥à¤®à¤µà¥‡à¤¯à¤° संसà¥à¤•रण में अदà¥à¤¯à¤¤à¤¨ करता है, या यदि अनिरà¥à¤¦à¤¿à¤·à¥à¤Ÿ है तो सभी उपकरणों को अदà¥à¤¯à¤¤à¤¨ करता है" msgid "Updating" msgstr "अदà¥à¤¯à¤¤à¤¨ किया जा रहा है" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "%s अदà¥à¤¯à¤¤à¤¿à¤¤ हो रहा है…" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "%1$s को %2$s से %3$s तक उनà¥à¤¨à¤¯à¤¨ करें?" #. TRANSLATORS: ask the user to share, %s is something #. * like: "Linux Vendor Firmware Service" #, c-format msgid "Upload these anonymous results to the %s to help other users?" msgstr "अनà¥à¤¯ उपयोगकरà¥à¤¤à¤¾à¤“ं की सहायता के लिठइन अनाम परिणामों को %s पर अपलोड करें?" #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "फरà¥à¤®à¤µà¥‡à¤¯à¤° रिपोरà¥à¤Ÿ अपलोड करने से हारà¥à¤¡à¤µà¥‡à¤¯à¤° विकà¥à¤°à¥‡à¤¤à¤¾à¤“ं को वासà¥à¤¤à¤µà¤¿à¤• उपकरणों पर विफल और सफल अदà¥à¤¯à¤¤à¤¨ की तà¥à¤°à¤‚त पहचान करने में मदद मिलती है।" #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "अतà¥à¤¯à¤¾à¤µà¤¶à¥à¤¯à¤•ता" #. TRANSLATORS: explain how to get help, %1 is #. * 'fwupdtool --help' #. TRANSLATORS: explain how to get help, #. * where $1 is something like 'fwupdmgr --help' #, c-format msgid "Use %s for help" msgstr "सहायता के लिठ%s का उपयोग करें" #. TRANSLATORS: CTRL^C [holding control, and then pressing C] will exit the #. program msgid "Use CTRL^C to cancel." msgstr "रदà¥à¤¦ करने के लिठCTRL^C का उपयोग करें।" #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "उपयोगकरà¥à¤¤à¤¾ को सूचित कर दिया गया" #. TRANSLATORS: remote filename base msgid "Username" msgstr "उपयोकà¥à¤¤à¤¾à¤¨à¤¾à¤®" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "मानà¥à¤¯" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "ESP सामगà¥à¤°à¥€ को सतà¥à¤¯à¤¾à¤ªà¤¿à¤¤ किया जा रहा है…" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "पà¥à¤°à¤•ार" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "विकà¥à¤°à¥‡à¤¤à¤¾" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "सतà¥à¤¯à¤¾à¤ªà¤¨ किया जा रहा है…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "संसà¥à¤•रण" #. TRANSLATORS: the fwupd version the release was tested on msgid "Version[fwupd]" msgstr "संसà¥à¤•रण[fwupd]" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "चेतावनी" #. TRANSLATORS: command description msgid "Wait for a device to appear" msgstr "उपकरण के पà¥à¤°à¤•ट होने की पà¥à¤°à¤¤à¥€à¤•à¥à¤·à¤¾ करें" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "पà¥à¤°à¤¤à¥€à¤•à¥à¤·à¤¾ की जा रही है…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "हारà¥à¤¡à¤µà¥‡à¤¯à¤° परिवरà¥à¤¤à¤¨à¥‹à¤‚ पर नजर रखें" #. TRANSLATORS: check various UEFI and ACPI tables are unchanged after the #. update msgid "Will measure elements of system integrity around an update" msgstr "किसी अदà¥à¤¯à¤¤à¤¨ के आसपास सिसà¥à¤Ÿà¤® अखंडता के ततà¥à¤µà¥‹à¤‚ को मापेगा" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "फाइल लेखन:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "लिखा जा रहा है…" #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from a recovery or installation disk, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "आपको यह सà¥à¤¨à¤¿à¤¶à¥à¤šà¤¿à¤¤ करना चाहिठकि आप पà¥à¤¨à¤°à¥à¤ªà¥à¤°à¤¾à¤ªà¥à¤¤à¤¿ या सà¥à¤¥à¤¾à¤ªà¤¨à¤¾ डिसà¥à¤• से सेटिंग को पà¥à¤¨à¤°à¥à¤¸à¥à¤¥à¤¾à¤ªà¤¿à¤¤ करने में सहज हैं, कà¥à¤¯à¥‹à¤‚कि इस परिवरà¥à¤¤à¤¨ के कारण सिसà¥à¤Ÿà¤® Linux में बूट नहीं हो सकता है या अनà¥à¤¯ सिसà¥à¤Ÿà¤® असà¥à¤¥à¤¿à¤°à¤¤à¤¾ का कारण बन सकता है।" #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from the system firmware setup, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "आपको यह सà¥à¤¨à¤¿à¤¶à¥à¤šà¤¿à¤¤ करना चाहिठकि आप सिसà¥à¤Ÿà¤® फरà¥à¤®à¤µà¥‡à¤¯à¤° सेटअप से सेटिंग को पà¥à¤¨à¤°à¥à¤¸à¥à¤¥à¤¾à¤ªà¤¿à¤¤ करने में सहज हैं, कà¥à¤¯à¥‹à¤‚कि इस परिवरà¥à¤¤à¤¨ के कारण सिसà¥à¤Ÿà¤® Linux में बूट नहीं हो सकता है या अनà¥à¤¯ सिसà¥à¤Ÿà¤® असà¥à¤¥à¤¿à¤°à¤¤à¤¾ का कारण बन सकता है।" #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "आपके वितरक ने आपके सिसà¥à¤Ÿà¤® या जà¥à¥œà¥‡ उपकरणों के साथ संगतता के लिठकिसी भी फरà¥à¤®à¤µà¥‡à¤¯à¤° अदà¥à¤¯à¤¤à¤¨ को सतà¥à¤¯à¤¾à¤ªà¤¿à¤¤ नहीं किया होगा।" #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "इस फरà¥à¤®à¤µà¥‡à¤¯à¤° का उपयोग करने से आपका हारà¥à¤¡à¤µà¥‡à¤¯à¤° कà¥à¤·à¤¤à¤¿à¤—à¥à¤°à¤¸à¥à¤¤ हो सकता है, और इस विजà¥à¤žà¤ªà¥à¤¤à¤¿ को सà¥à¤¥à¤¾à¤ªà¤¿à¤¤ करने से %s के साथ कोई भी वारंटी समापà¥à¤¤ हो सकती है।" #. TRANSLATORS: BKC is the industry name for the best known configuration and #. is a set #. * of firmware that works together #, c-format msgid "Your system is set up to the BKC of %s." msgstr "आपका सिसà¥à¤Ÿà¤® %s के BKC पर निरà¥à¤§à¤¾à¤°à¤¿à¤¤ है।" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[APPSTREAM_ID]" msgstr "[APPSTREAM_ID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[चेकसम]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[उपकरण-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[उपकरण-ID|GUID] [शाखा]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [VERSION]" msgstr "[उपकरण-ID|GUID] [संसà¥à¤•रण]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE]" msgstr "[उपकरण]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILE FILE_SIG REMOTE-ID]" msgstr "[फाइल FILE_SIG रिमोट-ID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[फाइलनाम1] [फाइलनाम2]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2] [--no-authenticate]" msgstr "[सेटिंग1] [सेटिंग2] [--no-authenticate]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[SMBIOS-फाइल|HWIDS-फाइल]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "तयशà¥à¤¦à¤¾" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "fwupd TPM इवेंट लॉग उपयोगिता" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "fwupd पà¥à¤²à¤—इनà¥à¤¸" fwupd-2.0.10/po/hr.po000066400000000000000000003035321501337203100143070ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # FIRST AUTHOR , 2016 # gogo , 2016 # Milo Ivir , 2020-2021 # Milo Ivir , 2023 # Milo Ivir , 2021 # gogo , 2016-2023 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Croatian (http://app.transifex.com/freedesktop/fwupd/language/hr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: hr\n" "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "%.0f minuta preostala" msgstr[1] "%.0f minute preostale" msgstr[2] "%.0f minuta preostalo" #. TRANSLATORS: BMC refers to baseboard management controller which #. * is the device that updates all the other firmware on the system #, c-format msgid "%s BMC Update" msgstr "%s BMC nadopuna" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "%s nadopuna baterije" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "%s nadopuna CPU mikrokôda" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "%s nadopuna kamere" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "%s nadopuna podeÅ¡avanja" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "%s nadopuna potroÅ¡aÄkog pogona upravljanja (ME)" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "%s nadopuna upravljaÄa" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "%s nadopuna korporativnog pogona upravljanja (ME)" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "%s nadopuna ureÄ‘aja" #. TRANSLATORS: Video Display refers to the laptop internal display or #. * external monitor #, c-format msgid "%s Display Update" msgstr "%s nadopuna zaslona" #. TRANSLATORS: Dock refers to the port replicator hardware laptops are #. * cradled in, or lowered onto #, c-format msgid "%s Dock Update" msgstr "%s nadopuna doka" #. TRANSLATORS: drive refers to a storage device, e.g. SATA disk #, c-format msgid "%s Drive Update" msgstr "%s nadopuna diska" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "%s nadopuna ugraÄ‘enog upravljaÄa" #. TRANSLATORS: a device that can read your fingerprint pattern #, c-format msgid "%s Fingerprint Reader Update" msgstr "%s nadopuna ÄitaÄa otisaka prsta" #. TRANSLATORS: flash refers to solid state storage, e.g. UFS or eMMC #, c-format msgid "%s Flash Drive Update" msgstr "%s nadopuna flash memorije" #. TRANSLATORS: GPU refers to a Graphics Processing Unit, e.g. #. * the "video card" #, c-format msgid "%s GPU Update" msgstr "%s GPU nadopuna" #. TRANSLATORS: a large pressure-sensitive drawing area typically used #. * by artists and digital artists #, c-format msgid "%s Graphics Tablet Update" msgstr "%s nadopuna grafiÄkog tableta" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "%s nadopuna tipkovnice" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "%s nadopuna pogona upravljanja (ME)" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "%s nadopuna miÅ¡a" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "%s nadopuna mrežnog suÄelja" #. TRANSLATORS: SSD refers to a Solid State Drive, e.g. non-rotating #. * SATA or NVMe disk #, c-format msgid "%s SSD Update" msgstr "%s nadopuna SSD diska" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "%s nadopuna kontrolera pohrane" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "%s nadopuna sustava" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "%s TPM nadopuna" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "%s nadopuna Thunderbolt kontrolera" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "%s touchpad nadopuna" #. TRANSLATORS: Dock refers to the port replicator device connected #. * by plugging in a USB cable -- which may or may not also provide power #, c-format msgid "%s USB Dock Update" msgstr "%s nadopuna USB doka" #. TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth #. * device that stays in the USB port so the wireless peripheral works #, c-format msgid "%s USB Receiver Update" msgstr "%s nadopuna USB prijemnika" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "%s nadopuna" #. TRANSLATORS: warn the user before updating, %1 is a device name #, c-format msgid "%s and all connected devices may not be usable while updating." msgstr "%s i svi spojeni ureÄ‘aji možda neće biti upotrebljivi tijekom nadopune." #. TRANSLATORS: %1 refers to some kind of security test, e.g. "Encrypted RAM". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s appeared: %s" msgstr "%s pojavljen: %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "UEFI platform #. key". #. * %2 and %3 refer to results value, e.g. "Valid" and "Invalid" #, c-format msgid "%s changed: %s → %s" msgstr "%s promijenjen: %s → %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "SPI BIOS #. region". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s disappeared: %s" msgstr "%s nestao: %s" #. TRANSLATORS: the device has a reason it can't update, e.g. laptop lid #. closed #, c-format msgid "%s is not currently updatable" msgstr "%s trenutno nije nadopunjiv" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "%s naÄin rada za proizvoÄ‘aÄe" #. TRANSLATORS: warn the user before #. * updating, %1 is a device name #, c-format msgid "%s must remain connected for the duration of the update to avoid damage." msgstr "%s mora ostati spojen tijekom trajanja nadopune kako bi se izbjeglo oÅ¡tećenje." #. TRANSLATORS: warn the user before updating, %1 is a machine #. * name #, c-format msgid "%s must remain plugged into a power source for the duration of the update to avoid damage." msgstr "%s mora ostati spojen s izvorom energije tijekom trajanja nadopune kako bi se izbjeglo oÅ¡tećenje." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "%s zaobilaženje" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "%s inaÄica" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u dan" msgstr[1] "%u dana" msgstr[2] "%u dana" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "%u ureÄ‘aj ima dostupnu nadopunu firmvera." msgstr[1] "%u ureÄ‘aja imaju dostupnu nadopunu firmvera." msgstr[2] "%u ureÄ‘aja ima dostupnu nadopunu firmvera." #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device is not the best known configuration." msgid_plural "%u devices are not the best known configuration." msgstr[0] "%u ureÄ‘aj nema najbolje poznato podeÅ¡avanje." msgstr[1] "%u ureÄ‘aja nema najbolje poznato podeÅ¡avanje." msgstr[2] "%u ureÄ‘aja nema najbolje poznato podeÅ¡avanje." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u sat" msgstr[1] "%u sata" msgstr[2] "%u sati" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minuta" msgstr[1] "%u minute" msgstr[2] "%u minuta" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u sekunda" msgstr[1] "%u sekunde" msgstr[2] "%u sekundi" #. TRANSLATORS: first percentage is current value, 2nd percentage is the #. * lowest limit the firmware update is allowed for the update to happen #, c-format msgid "%u%% (threshold %u%%)" msgstr "%u%% (najmanja inaÄica %u%%)" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(zastarjelo)" #. TRANSLATORS: HSI event title msgid "A TPM PCR is now an invalid value" msgstr "TPM PCR je sada nevaljana vrijednost" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "AMD Firmware Replay Protection" msgstr "AMD firmver zaÅ¡tita ponavljanja" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "AMD Firmware Write Protection" msgstr "AMD firmver zaÅ¡tita zpisivanja" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "AMD Secure Processor Rollback Protection" msgstr "ZaÅ¡tita od vraćanja na stariju inaÄicu AMD sigurnog procesora" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "Radnja je potrebna:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "Aktiviraj ureÄ‘aj" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "Aktiviraj ureÄ‘aje na Äekanju" msgid "Activate the new firmware on the device" msgstr "Aktiviraj novi firmver na ureÄ‘aju" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "Aktivacija nadopune firmvera" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "Aktivacija nadopune firmvera za" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Dob" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "Slažete li se s omogućavanjem udaljene lokacije?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Zamjena za %s" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are now valid" msgstr "Svi TPM PCR-ovi su sada valjani" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are valid" msgstr "Svi TPM PCR-ovi su valjani" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "Svi ureÄ‘aji iste vrste će biti nadopunjeni u isto vrijeme" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Dopusti vraćanje starije inaÄice firmvera" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "Dopusti ponovnu instalaciju firmvera postojeće inaÄice" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "Dopusti prebacivanje izmeÄ‘u firmver grana" #. TRANSLATORS: is not the main firmware stream msgid "Alternate branch" msgstr "Alternativni ogranak" #. TRANSLATORS: another application is updating the device already msgid "An update is in progress" msgstr "Sprovodi se aktualiziranje" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "Nadopuna zahtijeva ponovno pokretanje za zavrÅ¡etak." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "Nadopuna zahtijeva potpuno iskljuÄivanje sustava." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Odgovori 'da' na sva pitanja" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Primijeni nadopunu iako nije preporuÄljivo" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Primijeni nadopunu datoteka" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "Primjena nadopuna…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Odobreni firmver:" msgstr[1] "Odobreni firmveri:" msgstr[2] "Odobreni firmveri:" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "Prebaci u naÄin firmvera" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Ovjeravanje…" #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "Potrebne su pojedinosti ovjere" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Potrebna je ovjera za vraćanje starije inaÄice firmvera na uklonjivom ureÄ‘aju" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Potrebna je ovjera za vraćanje starije inaÄicu firmvera na ovom raÄunalu" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "Potrebna je ovjera za promjenu BIOS postavki" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "Potrebna je ovjera za promjenu udaljene lokacije koja se koristi za nadopunu firmvera" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "Potrebna je ovjera za promjenu podeÅ¡avanja pozadinskog programa" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "Potrebna je ovjera za Äitanje BIOS postavki" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "Potrebna je ovjera za postavljanje popisa odobrenih firmvera" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "Potrebna je ovjera za potpisivanje podataka vjerodajnicom klijenta" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "Potrebna je ovjera za prebacivanje na novu inaÄicu firmvera" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Potrebna je ovjera za otkljuÄavanje ureÄ‘aja" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Potrebna je ovjera za nadopunu firmvera na uklonjivom ureÄ‘aju" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Potrebna je ovjera za nadopunu firmvera na ovom raÄunalu" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "Potrebna je ovjera za nadopunu spremljenog kontrolnog zbroja ureÄ‘aja" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "Automatsko izvjeÅ¡tavanje" #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "Automatski poÅ¡alji svaki put?" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS Rollback Protection" msgstr "ZaÅ¡tita od vraćanja na stariju inaÄicu BIOS-a" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS rollback protection" msgstr "ZaÅ¡tita od vraćanja na stariju inaÄicu BIOS-a" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "BIOS nadopune isporuÄene putem LVFS-a ili Windows ažuriranja" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "BUILDER-XML FILENAME-DST" msgstr "IZGRADITELJ-XML NAZIV DATOTEKE-DST" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "Baterija" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "Poveži novi kernel upravljaÄki program" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "Datoteke blokiranog firmvera:" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "Blokirana inaÄica" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "Blokiranje firmvera:" #. TRANSLATORS: command description msgid "Blocks a specific firmware from being installed" msgstr "Blokira instalaciju odreÄ‘enog firmvera" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "InaÄica uÄitaÄa pokretanja" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "Ogranak" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "Izgradi datoteku firmvera" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Odustani" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "Prekinuto" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "Nemoguća primjena kao dbx nadopune jer je već primijenjeno." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Promijenjeno" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "Provjeri podudarnost kriptografske jedinstvene vrijednosti firmvera" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Kontrolni zbroj" #. TRANSLATORS: get interactive prompt, where branch is the #. * supplier of the firmware, e.g. "non-free" or "free" msgid "Choose branch" msgstr "Odaberi granu" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "Odaberi ureÄ‘aj" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "Odaberi firmver" #. TRANSLATORS: get interactive prompt msgid "Choose release" msgstr "Odaberi izdanje" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "Uklanja rezultate posljednje nadopune" #. TRANSLATORS: error message msgid "Command not found" msgstr "Naredba nije pronaÄ‘ena" #. TRANSLATORS: is not supported by the vendor msgid "Community supported" msgstr "Podržano od strane zajednice" #. TRANSLATORS: title prefix for the BIOS settings dialog msgid "Configuration Change Suggested" msgstr "Predložena promjena podeÅ¡avanja" #. TRANSLATORS: no peeking msgid "Configuration is only readable by the system administrator" msgstr "PodeÅ¡avanje može Äitati samo administrator sustava" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "Pretvori datoteku firmvera" #. TRANSLATORS: when the update was built msgid "Created" msgstr "Stvoreno" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "KritiÄna" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "Provjera kriptografske jedinstvene vrijednosti je dostupna" #. TRANSLATORS: current value of a BIOS setting msgid "Current Value" msgstr "Trenutna vrijednost" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "Trenutna inaÄica" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "UREÄAJ-ID|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Mogućnosti otklanjanja greÅ¡ke" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "Raspakiravanje…" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Opis" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "Prebaci u naÄin uÄitaÄa pokretanja" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "Pojedinosti" #. TRANSLATORS: the best known configuration is a set of software that we know #. works #. * well together. In the OEM and ODM industries it is often called a BKC msgid "Deviate from the best known configuration?" msgstr "Odustani od najbolje poznatog podeÅ¡avanja?" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "Oznaka ureÄ‘aja" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "ID ureÄ‘aja" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "UreÄ‘aj dodan:" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "Energija baterije ureÄ‘aja je preniska" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse #, c-format msgid "Device battery power is too low (%u%%, requires %u%%)" msgstr "energija baterije ureÄ‘aja je preniska (%u%%, zahtijeva %u%%)" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "UreÄ‘aj se može oporaviti od neuspjelog zapisivanja frimvera" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "UreÄ‘aj promijenjen:" #. TRANSLATORS: a version check is required for all firmware msgid "Device firmware is required to have a version check" msgstr "Potreban je firmver ureÄ‘aja za provjeru inaÄice" #. TRANSLATORS: emulated means we are pretending to be a different model msgid "Device is emulated" msgstr "UreÄ‘aj je emuliran" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "UreÄ‘aj je zakljuÄan" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "UreÄ‘aj je potreban za instalaciju svih dostupnih izdanja" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "UreÄ‘aj je nedostupan" #. TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode msgid "Device is unreachable, or out of wireless range" msgstr "UreÄ‘aj je nedostupan ili je izvan dometa bežiÄne mreže" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "UreÄ‘aj se može koristiti tijekom trajanja nadopune" #. TRANSLATORS: usually this is when we're waiting for a reboot msgid "Device is waiting for the update to be applied" msgstr "UreÄ‘aj Äeka na primjenu nadopune" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "UreÄ‘aj uklonjen:" #. TRANSLATORS: as in, wired mains power for a laptop msgid "Device requires AC power to be connected" msgstr "UreÄ‘aj zahtijeva vanjski izvor energije" #. TRANSLATORS: The device cannot be updated due to missing vendor's license." msgid "Device requires a software license to update" msgstr "UreÄ‘aj zahtijeva softversku licencu za nadopunu" #. TRANSLATORS: longer description msgid "Device software updates are provided for this device." msgstr "Nadopune softvera ureÄ‘aja su isporuÄene za ovaj ureÄ‘aj." #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "Nadopune ureÄ‘aja u fazama" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "UreÄ‘aj podržava prebacivanje na drugaÄiji ogranak firmvera" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "Nadopuni ureÄ‘aja je potrebna aktivacija" #. TRANSLATORS: save the old firmware to disk before installing the new one msgid "Device will backup firmware before installing" msgstr "UreÄ‘aj će sigurnosno kopirati firmver prije instalacije" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "UreÄ‘aj se neće ponovno pojaviti nakon zavrÅ¡etka nadopune" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "UreÄ‘aji koji su uspjeÅ¡no nadopunjeni:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "UreÄ‘aji koji nisu ispravno nadopunjeni:" #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "UreÄ‘aji bez dostupnih nadopuna firmvera: " #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "UreÄ‘aji s najnovijom dostupnom inaÄicom firmvera:" #. TRANSLATORS: this is for the device tests msgid "Did not find any devices with matching GUIDs" msgstr "Nije pronaÄ‘en niti jedan ureÄ‘aj s podudarajućim GUID-ovima" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "Onemogućeno" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "Onemogućuje zadane udaljene lokacije" #. TRANSLATORS: the OS the release was tested on msgid "Distribution" msgstr "Distribucija" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "Ne provjeravaj stare metapodatke" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "Ne provjeravaj neprijavljenu povijest" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "Ne provjeravaj trebaju li preuzete udaljene lokacije biti omogućene" #. TRANSLATORS: command line option msgid "Do not check or prompt for reboot after update" msgstr "Ne provjeravaj ili upitaj za ponovno pokretanje nakon nadopune" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "Ne ukljuÄuj prefiks domene zapisa" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "Ne ukljuÄuj prefiks vremena" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "Nemoj izvoditi provjere sigurnosti ureÄ‘aja" #. TRANSLATORS: command line option msgid "Do not prompt for devices" msgstr "Ne upitaj za ureÄ‘aje" #. TRANSLATORS: command line option msgid "Do not search the firmware when parsing" msgstr "Ne pretražuj firmver pri obradi" #. TRANSLATORS: warning message shown after update has been scheduled msgid "Do not turn off your computer or remove the AC adaptor while the update is in progress." msgstr "Ne iskljuÄujte svoje raÄunalo ili AC adapter tijekom procesa nadopune." #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "Ne zapisuj bazu podataka povijesti" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "Razumijete li posljedice promjene ogranka firmvera?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "Želite li onemogućiti ovu znaÄajku za buduće nadopune?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "Želite li odmah provjeriti ovu udaljenu lokaciju?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "Želite li poslati izvjeÅ¡taje automatski za buduće nadopune?" #. TRANSLATORS: command line option msgid "Don't prompt for authentication (less details may be shown)" msgstr "Ne pitaj za ovjeru (može biti prikazano manje pojedinosti)" #. TRANSLATORS: success msgid "Done!" msgstr "ZavrÅ¡eno!" #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "Vrati %s na stariju inaÄicu sa %s na %s?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "Vraća stariju inaÄicu firmvera na ureÄ‘aju" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "Vraćanje %s na stariju inaÄicu…" #. TRANSLATORS: command description msgid "Download a file" msgstr "Preuzmi datoteku" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Preuzimanje…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "IspiÅ¡i opÅ¡irnije podatke SMBIOS-a iz datoteke" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "Trajanje" #. TRANSLATORS: longer description msgid "Each system should have tests to ensure firmware security." msgstr "Svaki ureÄ‘aj bi se trebao testirati firmver, kako bi se osigurala sigurnost firmvera." #. TRANSLATORS: command description msgid "Emulate a device using a JSON manifest" msgstr "Emuliraj ureÄ‘aj koristeći JSON manifest" #. TRANSLATORS: this device is not actually real msgid "Emulated" msgstr "Emulirano" #. TRANSLATORS: Title: if we are emulating a different host msgid "Emulated host" msgstr "Emulirani poslužitelj" msgid "Enable" msgstr "Omogući" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "Omogući udaljenu lokaciju?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "Omogući ovu udaljenu lokaciju?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Omogućeno" #. TRANSLATORS: Plugin is active only if hardware is found msgid "Enabled if hardware matches" msgstr "Omogućeno ako se hardver podudara" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "Omogućuje zadane udaljene lokacije" msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "Omogućujete ovu funkcionalnost na vlastiti rizik, Å¡to znaÄi da morate kontaktirati svog izvornog proizvoÄ‘aÄa opreme u vezi problema uzrokovanih tim nadopunama. Samo probleme sa samim postupkom nadopune treba prijaviti na $OS_RELEASE:BUG_REPORT_URL$." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "Ovu udaljenu lokaciju omogućavate na vlastiti rizik." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Å ifrirano" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "Å ifrirani RAM" msgid "Encrypted RAM makes it impossible for information that is stored in device memory to be read if the memory chip is removed and accessed." msgstr "Å ifrirani RAM Äini nemogućim Äitanje informacija spremljenih u memoriji ureÄ‘aja ako je memorijski Äip uklonjen i može mu se pristupiti." #. TRANSLATORS: the vendor is no longer supporting the device msgid "End of life" msgstr "Kraj podrÅ¡ke" #. TRANSLATORS: The BIOS setting can only be changed to fixed values msgid "Enumeration" msgstr "Nabrajanje" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "ObriÅ¡i svu povijest nadopune firmvera" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Brisanje…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "IzaÄ‘i nakon kratke odgode" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "IzaÄ‘i nakon uÄitavanja pogona" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "Izvezi strukturu datoteke frimvera u XML" #. TRANSLATORS: command description msgid "Extract a firmware blob to images" msgstr "Izdvoji blob firmvera u slike" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "DATOTEKA" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "DATOTEKA [UREÄAJ-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "NAZIV DATOTEKE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "PRIVATNI-KLJUÄŒ NAZIV DATOTEKE VJERODAJNICE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "NAZIV DATOTEKE ID-UREÄAJA" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME OFFSET DATA [FIRMWARE-TYPE]" msgstr "NAZIV DATOTEKE POMAKA PODATAKA [FIRMVER-VRSTA]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "NAZIV DATOTEKA [UREÄAJ-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "NAZIV DATOTEKE [FIRMVER-VRSTA]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" msgstr "NAZIV DATOTEKE-SRC NAZIV DATOTEKE-DST [FIRMVER-VRSTA-SRC] [FIRMVER-VRSTA-DST]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" msgstr "NAZIV DATOTEKE|KONTROLNI ZBROJ1[,KONTROLNI ZBROJ2][,KONTROLNI ZBROJ3]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "Neuspjelo" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Neuspjela primjena nadopune" #. TRANSLATORS: error message for Windows msgid "Failed to connect to Windows service, please ensure it's running." msgstr "Neuspjelo povezivanje s Windows uslugom, provjerite je li pokrenuta." #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "Neuspjelo povezivanje s pozadinskim programom" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "Neuspjelo uÄitavanje lokalnog dbx-a" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "Neuspjelo uÄitavanje dbx-a sustava" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "Neuspjelo zakljuÄavanje" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Neuspjela obrada argumenata" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "Neuspjela obrada datoteke" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "Neuspjela obrada lokalnog dbx-a" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "Neuspjela provjera ESP sadržaja" #. TRANSLATORS: item is FALSE msgid "False" msgstr "Laž" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Naziv datoteke" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "Potpis naziva datoteke" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "Izvor naziva datoteke" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Potreban je naziv datoteke" #. TRANSLATORS: command line option msgid "Filter with a set of device flags using a ~ prefix to exclude, e.g. 'internal,~needs-reboot'" msgstr "Filtar sa skupom oznaka ureÄ‘aja koji koristi ~ prefiks za izuzimanje, npr. 'internal,~needs-reboot'" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware Attestation" msgstr "Frimver provjera" #. TRANSLATORS: longer description msgid "Firmware Attestation checks device software using a reference copy, to make sure that it has not been changed." msgstr "Frimver provjera provjerava softver ureÄ‘aja koristeći izvornu kopiju, kako bi se osiguralo da nije mijenjan." #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "Frimver BIOS opisnik" #. TRANSLATORS: longer description msgid "Firmware BIOS Descriptor protects device firmware memory from being tampered with." msgstr "Frimver BIOS opisnik Å¡titi memoriju frimvera ureÄ‘aja od neovlaÅ¡tenog mijenjanja." #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "Firmver BIOS podruÄje" #. TRANSLATORS: longer description msgid "Firmware BIOS Region protects device firmware memory from being tampered with." msgstr "Firmver BIOS podruÄje Å¡titi memoriju firmvera ureÄ‘aja od nevlaÅ¡tenog mijenjanja." #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "Osnovni URI frimvera" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "Firmver nadopuna D-Bus usluge" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Pozadinski program nadopune firmvera" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "Provjera firmver nadopunitelja" msgid "Firmware Updater Verification checks that software used for updating has not been tampered with." msgstr "Provjera firmver nadopunitelja provjerava je li softver za nadopunu neovlaÅ¡teno mijenjan." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "Nadopune frimvera" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "Firmver pomagalo" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "Firmver zaÅ¡tita zapisivanja" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "Frimver zakljuÄana zaÅ¡tita zapisivanja" #. TRANSLATORS: longer description msgid "Firmware Write Protection protects device firmware memory from being tampered with." msgstr "Firmver zaÅ¡tita zapisivanja Å¡titi memoriju firmvera ureÄ‘aja od nevlaÅ¡tenog mijenjanja." #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "Frimver provjera" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "Firmver je već blokiran" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "Firmver joÅ¡ nije blokiran" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "Metapodaci firmvera nisu nadopunjeni %u dan i možda nisu najnoviji." msgstr[1] "Metapodaci firmvera nisu nadopunjeni %u dana i možda nisu najnoviji." msgstr[2] "Metapodaci firmvera nisu nadopunjeni %u dana i možda nisu najnoviji." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "Nadopune frimvera" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "Oznake" #. TRANSLATORS: command line option msgid "Force the action by relaxing some runtime checks" msgstr "Prisili radnju zanemarujući neke provjere vremena pokretanja" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "PronaÄ‘eno" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "Potpuno Å¡ifriranje diska je otkriveno" #. TRANSLATORS: we might ask the user the recovery key when next booting #. Windows msgid "Full disk encryption secrets may be invalidated when updating" msgstr "Tajne potpunog Å¡ifriranja diska mogu biti poniÅ¡tene pri nadopuni" #. TRANSLATORS: Title: if the part has been fused msgid "Fused Platform" msgstr "Pregorjela platforma" #. TRANSLATORS: Title: if the part has been fused msgid "Fused platform" msgstr "Pregorjela platforma" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" msgstr[1] "GUID" msgstr[2] "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" msgid "Get BIOS settings" msgstr "Prikaži BIOS postavke" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "Prikaži sve oznake ureÄ‘aja koje podržava fwupd" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Prikaži sve ureÄ‘aje koji podržavaju nadopunu firmvera" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "Prikaži sve omogućene prikljuÄke registrirane sa sustavom" #. TRANSLATORS: command description msgid "Get device report metadata" msgstr "Preuzmi metapodatke izvjeÅ¡taja ureÄ‘aja" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Prikaži pojedinosti datoteke firmvera" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "Prikazuje udaljena podeÅ¡avanja" #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "Prikaži sigurnosne znaÄajke poslužitelja" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "Preuzima popis odobrenih firmvera" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "Preuzima popis blokiranih firmvera" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Prikaži popis nadopuna za povezani hardver" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "Prikazuje izdanja za ureÄ‘aj" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Prikaži rezultate posljednje nadopune" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "HWIDS-DATOTEKA" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "Hardver Äeka da ponovno bude spojen" #. TRANSLATORS: the release urgency msgid "High" msgstr "Visoka" #. TRANSLATORS: title for host security events msgid "Host Security Events" msgstr "DogaÄ‘aji sigurnosti raÄunala" #. TRANSLATORS: error message for unsupported feature #, c-format msgid "Host Security ID (HSI) is not supported" msgstr "ID sigurnosti sustava (HSI) nije podržan" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "Sigurnosni ID poslužitelja:" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "IOMMU zaÅ¡tita" #. TRANSLATORS: longer description msgid "IOMMU Protection prevents connected devices from accessing unauthorized parts of system memory." msgstr "IOMMU zaÅ¡tita sprjeÄava pristup povezanih uraÄ‘aja neovlaÅ¡tenim dijelovima memorije." #. TRANSLATORS: HSI event title msgid "IOMMU device protection disabled" msgstr "Onemogućena zaÅ¡tita IOMMU ureÄ‘aja" #. TRANSLATORS: HSI event title msgid "IOMMU device protection enabled" msgstr "Omogućena zaÅ¡tita IOMMU ureÄ‘aja" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "Mirovanje…" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "Zanemari SSL ograniÄene provjere pri preuzimanju datoteka" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "Zanemari neuspjele kontrolne zbrojeve firmvera" #. TRANSLATORS: command line option msgid "Ignore firmware hardware mismatch failures" msgstr "Zanemari neuspjela poklapanja hardvera firmvera" #. TRANSLATORS: try to help msgid "Ignoring SSL strict checks, to do this automatically in the future export DISABLE_SSL_STRICT in your environment" msgstr "Zanemaruju se stroge SSL provjere, kako bi se ovo ubuduće automatski izvezlo DISABLE_SSL_STRICT u svojem okruženju" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "Trajanje instalacije" #. TRANSLATORS: command description msgid "Install a specific firmware on a device, all possible devices will also be installed once the CAB matches" msgstr "Instaliraj odreÄ‘eni firmver na ureÄ‘aj, svi mogući ureÄ‘aji će biti instalirani jednom kada se CAB podudara" msgid "Install old version of signed system firmware" msgstr "Instaliraj staru inaÄicu potpisanog frimvera sustava" msgid "Install old version of unsigned system firmware" msgstr "Instaliraj staru inaÄicu nepotpisanog frimvera sustava" msgid "Install signed device firmware" msgstr "Instaliraj firmver potpisan ureÄ‘ajem" msgid "Install signed system firmware" msgstr "Instaliraj firmver potpisan sustavom" msgid "Install unsigned device firmware" msgstr "Instaliraj firmver nepotpisan ureÄ‘ajem" msgid "Install unsigned system firmware" msgstr "Instaliraj firmver nepotpisan sustavom" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "Instalacija nadopune firmvera…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "Instaliram na %s…" #. TRANSLATORS: if it breaks, you get to keep both pieces msgid "Installing this update may also void any device warranty." msgstr "Instaliranje ove nadopune može poniÅ¡titi svako jamstvo ureÄ‘aja." #. TRANSLATORS: The BIOS setting only accepts integers in a fixed range msgid "Integer" msgstr "Cijeli broj" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM Protected" msgstr "Intel BootGuard ACM zaÅ¡tićen" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "Intel BootGuard ACM zaÅ¡tićen" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard Error Policy" msgstr "Intel BootGuard pravilo greÅ¡ke" msgid "Intel BootGuard Error Policy ensures the device does not continue to start if its device software has been tampered with." msgstr "Intel BootGuard pravilo greÅ¡ke osigurava da se ureÄ‘aj ne nastavlja pokretati ako je njegov softver ureÄ‘aja neovlaÅ¡teno mijenjan." #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard Fuse" msgstr "Intel BootGuard OTP osiguraÄ" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "Intel BootGuard OTP osiguraÄ" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard Verified Boot" msgstr "Intel BootGuard provjereno pokretanje" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Intel BootGuard pravilo greÅ¡ke" #. TRANSLATORS: longer description msgid "Intel BootGuard prevents unauthorized device software from operating when the device is started." msgstr "Intel BootGuard sprjeÄava pristup neovlaÅ¡tenog softvera ureÄ‘aja iz operativnog sustava kada se ureÄ‘aj pokrene." #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Intel BootGuard provjereno pokretanje" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Manufacturing Mode" msgstr "NaÄin rada Intel pogona upravljanja" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the "override" is #. enabled #. * with a jumper -- luckily it is probably not accessible to end users on #. consumer #. * boards msgid "Intel Management Engine Override" msgstr "Zaobilaženje Intel pogona upravljanja" #. TRANSLATORS: longer description msgid "Intel Management Engine Override disables checks for device software tampering." msgstr "Zaobilaženje Intel pogona upravljanja (Intel Management Engine) onemogućava provjere za neovlaÅ¡tni softver ureÄ‘aja." #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Version" msgstr "InaÄica Intel pogona upravljanja" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "UnutraÅ¡nji ureÄ‘aj" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "Nevaljano" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "Nevaljani argumenti" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "Je nadogradnja na stariju inaÄicu" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "U naÄinu uÄitaÄa pokretanja je" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "Je nadogradnja" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "Problem" msgstr[1] "Problemi" msgstr[2] "Problemi" #. TRANSLATORS: HSI event title msgid "Kernel is no longer tainted" msgstr "Kernel viÅ¡e nije kontaminiran" #. TRANSLATORS: HSI event title msgid "Kernel is tainted" msgstr "Kernel je kontaminiran" #. TRANSLATORS: HSI event title msgid "Kernel lockdown disabled" msgstr "ZakljuÄavanje kernela onemogućeno" #. TRANSLATORS: HSI event title msgid "Kernel lockdown enabled" msgstr "ZakljuÄavanje kernela omogućeno" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "LOKACIJA" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "Posljednja promjena" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "Preostalo je manje od minute" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "Licenca" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "Linux kernel zakljuÄavanje" #. TRANSLATORS: longer description msgid "Linux Kernel Lockdown mode prevents administrator (root) accounts from accessing and changing critical parts of system software." msgstr "Linux Kernel zakljuÄavanje sprjeÄava administratorske (korijenske) raÄune da pristupe i mijenjaju kljuÄne dijelove softvera sustava." msgid "Linux Kernel Swap temporarily saves information to disk as you work. If the information is not protected, it could be accessed by someone if they obtained the disk." msgstr "Linux Kernel Swap privremeno sprema informacije na disk dok radite. Ako informacije nisu zaÅ¡tićene, netko im može pristupiti ako je u posjedu diska." #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "Linux Kernel provjera" msgid "Linux Kernel Verification makes sure that critical system software has not been tampered with. Using device drivers which are not provided with the system can prevent this security feature from working correctly." msgstr "Linux Kernel provjera osigurava da kljuÄni softver sustava nije mijenjan. KoriÅ¡tenje upravljaÄkih progrma ureÄ‘aja koji se ne isporuÄuju sustavom može sprjeÄiti ispravan rad ove znaÄajke." #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "Linux swap" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Firmver Usluga Linux ProizvoÄ‘aÄa (LVFS) (stabilni firmver)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Firmver Usluga Linux ProizvoÄ‘aÄa (LVFS) (testni firmver)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Linux kernel" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Linux kernel zakljuÄavanje" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Linux swap" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "Prikaži unose u dbx-u" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "Prikaži dostupne vrste firmvera" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "Prikazuje datoteke na ESP-u" #. TRANSLATORS: command description msgid "Load device emulation data" msgstr "UÄitaj emulirane podatke ureÄ‘aja" #. TRANSLATORS: the plugin was created from a .so object, and was not built-in msgid "Loaded from an external module" msgstr "UÄitano iz vanjskog modula" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "UÄitavanje…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "ZakljuÄano" #. TRANSLATORS: the release urgency msgid "Low" msgstr "Niska" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refers #. * to the private/public key used to secure loading of firmware msgid "MEI Key Manifest" msgstr "Manifest MEI kljuÄa" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refer #. * to the private/public key used to secure loading of firmware msgid "MEI key manifest" msgstr "Manifest MEI kljuÄa" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "MEI naÄin rada za proizvoÄ‘aÄe" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "MEI zaobilaženje" msgid "MEI version" msgstr "MEI inaÄica" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "RuÄno omogući odreÄ‘ene prikljuÄke" #. TRANSLATORS: longer description msgid "Manufacturing Mode is used when the device is manufactured and security features are not yet enabled." msgstr "NaÄin rada za proizvoÄ‘aÄe se koristi kada je ureÄ‘aj proizveden i sigurnosne znaÄajke joÅ¡ nisu omogućene." #. TRANSLATORS: Longest valid string for BIOS setting msgid "Maximum length" msgstr "Najveća duljina" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "NajviÅ¡a vrijednost" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "Srednja" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "Metapodaci potpisa" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "URI metapodataka" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "Metapodaci se mogu dobiti od Firmver Usluge Linux ProizvoÄ‘aÄa (LVFS)." #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "Najmanja inaÄica" #. TRANSLATORS: Shortest valid string for BIOS setting msgid "Minimum length" msgstr "Najmanja duljina" #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "Najmanja vrijednost" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "Mijenja vrijednost podeÅ¡avanja pozadinskog programa" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "Promjena zadane udaljene lokacije" msgid "Modify a configured remote" msgstr "Promijeni zadanu udaljenu lokaciju" msgid "Modify daemon configuration" msgstr "Prilagodi podeÅ¡avanje pozadinskog programa" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "Nadgledaj dogaÄ‘aje pozadinskim programom" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "Montira ESP" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "Potrebno je ponovno pokretanje nakon instalacije" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "Zahtijeva ponovno pokretanje" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "Potrebno je iskljuÄivanje nakon instalacije" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "Nova inaÄica" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "Nema zadane radnje!" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "Nema starijih inaÄica za %s" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "Nema pronaÄ‘enog ID firmvera" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Nema otkrivenog hardvera s mogućnosti nadopune firmvera" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "Nema dostupnih izdanja" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "Trenutno nema omogućenih udaljenih lokacija stoga nema dostupnih metapodataka." #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "Nema dostupnih udaljenih lokacija" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "Nema nadopunjivih ureÄ‘aja" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "Nema dostupnih nadopuna" msgid "No updates available for remaining devices" msgstr "Nema dostupnih nadopuna za preostale ureÄ‘aje" #. TRANSLATORS: version cannot be installed due to policy msgid "Not approved" msgstr "Nije odobreno" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "Nije pronaÄ‘eno" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "Nije podržano" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "U redu" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "U redu!" #. TRANSLATORS: the firmware old version msgid "Old version" msgstr "Stara inaÄica" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "Prikaži samo jednu PRC vrijednost" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "Samo nadogradnje inaÄice su dopuÅ¡tene" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "ZaobiÄ‘i zadanu ESP putanju" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "PUTANJA" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "Obradi i prikaži pojedinosti datoteke firmvera" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "Obrada dbx nadopune…" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "Obrada dbx-a sustava…" #. TRANSLATORS: remote filename base msgid "Password" msgstr "Lozinka" #. TRANSLATORS: command description msgid "Patch a firmware blob at a known offset" msgstr "Zakrpaj datoteku firmvera na poznati pomak" msgid "Payload" msgstr "Sadržaj prijenosa" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "Na Äekanju" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "Obavi radnju?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "Otklanjanje greÅ¡aka platforme" #. TRANSLATORS: longer description msgid "Platform Debugging allows device security features to be disabled. This should only be used by hardware manufacturers." msgstr "Otklanjanje greÅ¡aka platforme dopuÅ¡ta onemogućavanje sigurnosnih znaÄajki ureÄ‘aja. Ovo bi trebali koristit samo proizvoÄ‘aÄi hardvera." #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "Otklanjanje greÅ¡aka platforme" #. TRANSLATORS: 'recovery key' here refers to a code, rather than a physical #. metal thing msgid "Please ensure you have the volume recovery key before continuing." msgstr "Pobrinite se da imate kljuÄ oporavka ureÄ‘aja prije nastavka." #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "Odaberite broj od 0 do %u: " #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "Zavisnosti prikljuÄka nedostaju" #. TRANSLATORS: Possible values for a bios setting msgid "Possible Values" msgstr "Moguće vrijednosti" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA Protection" msgstr "DMA zaÅ¡tita predpokretanja" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "DMA zaÅ¡tita predpokretanja" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is disabled" msgstr "DMA zaÅ¡tita prije pokretanja je onemogućena" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is enabled" msgstr "DMA zaÅ¡tita prije pokretanja je omogućena" #. TRANSLATORS: longer description msgid "Pre-boot DMA protection prevents devices from accessing system memory after being connected to the computer." msgstr "DMA zaÅ¡tita predpokretanja sprjeÄava pristup ureÄ‘aja memoriji sustava nakon Å¡to se povežu s raÄunalom. " #. TRANSLATORS: warning message msgid "Press unlock on the device to continue the update process." msgstr "Pritisnite otkljuÄavanje na ureÄ‘aju za nastavak procesa nadopune." #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "PrijaÅ¡nja inaÄica" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Prioritet" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "Problemi" msgid "Proceed with upload?" msgstr "Nastavi sa slanjem?" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Processor Security Checks" msgstr "Sigurnosna provjera procesora" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "Processor rollback protection" msgstr "ZaÅ¡tita od vraćanja na stariju inaÄicu procesora" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "VlasniÄki" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "UDALJENI-ID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID KEY VALUE" msgstr "VRIJEDNOST KLJUÄŒA ID-UDALJENE_LOKACIJE" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "Dozvola Äitanja" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "OÄitaj blob firmvera s ureÄ‘aja" #. TRANSLATORS: command description msgid "Read a firmware from a device" msgstr "OÄitaj firmver s ureÄ‘aja" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "ÄŒitanje iz %s…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "ÄŒitanje…" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "Osvježi metapodatke s udaljenog poslužitelja" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "Ponovno instaliraj %s na %s?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "Ponovno instaliraj trenutni firmver na ureÄ‘aj" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "Ponovno instaliraj firmver na ureÄ‘aj" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "Ogranak izdanja" #. TRANSLATORS: release attributes msgid "Release Flags" msgstr "Oznake izdanja" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "ID izdanja" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "Udaljeni ID" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "URI izvjeÅ¡taja" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "Prijavljeno na udaljenom poslužitelju" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "Potrebni efivarfs datoteÄni sustav nije pronaÄ‘en" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "Potreban hardver nije pronaÄ‘en" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "Zahtijeva uÄitaÄa pokretanja" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Potreban je pristup internetu" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Ponovno pokreni odmah?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "Ponovno pokreni pozadinski program kako bi se promjene primijenile?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Ponovno pokretanje ureÄ‘aja…" #. TRANSLATORS: command description msgid "Retrieve BIOS settings. If no arguments are passed all settings are returned" msgstr "Preuzmi BIOS postavke. Ako se argumenati ne proslijede, postavke su nepromijenjene" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "Vrati sve ID-ove hardvera za ureÄ‘aj" #. TRANSLATORS: longer description msgid "Rollback Protection prevents device software from being downgraded to an older version that has security problems." msgstr "ZaÅ¡tita od vraćanja na stariju inaÄicu sprjeÄava softver ureÄ‘aja da ga vrati na stariju inaÄicu koja ima sigurnosne probleme." #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "Pokreni rutinu Äišćenja sastavljanja prikljuÄka kada se koristi install-blob" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "Pokreni rutinu pripreme sastavljanja prikljuÄka kada se koristi install-blob" #. TRANSLATORS: tell a user how to get information #, c-format msgid "Run without '%s' to see" msgstr "Pokrenite bez '%s' za prikaz" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "Pokrenuti kernel je prestar" #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "Sufiks vremenskog izvrÅ¡avanja" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING VALUE" msgstr "VRIJEDNOST POSTAVKE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING1 VALUE1 [SETTING2] [VALUE2]" msgstr "POSTAVKA1 VRIJEDNOST1 [POSTAVKA2] [VRIJEDNOST2]" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "SPI BIOS opisnik" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "SPI BIOS podruÄje" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "SPI zakljuÄavanje" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "SPI replay protection" msgstr "ZaÅ¡tita od SPI ponavljanja" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "SPI zapisivanje" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "SPI write protection" msgstr "ZaÅ¡tita od SPI zapisivanja" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "UPRAVLJAÄŒKI PROGRAM PODSUSTAVA [UREÄAJ-ID|GUID]" #. TRANSLATORS: command description msgid "Save a file that allows generation of hardware IDs" msgstr "Spremi datoteku koja omogućuje stvaranje ID-ova hardvera" #. TRANSLATORS: command description msgid "Save device emulation data" msgstr "Spremi emulirane podatke ureÄ‘aja" #. TRANSLATORS: Scalar increment for integer BIOS setting msgid "Scalar Increment" msgstr "Skalarni umnožak" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Zakazivanje…" #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "Sigurno pokretanje (SecureBoot) onemogućeno" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "Sigurno pokretanje (SecureBoot) omogućeno" #. TRANSLATORS: the %1 is a URL to a wiki page #, c-format msgid "See %s for more details." msgstr "Pogledajte %s za viÅ¡e informacija." #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "Pogledajte %s za viÅ¡e informacija." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Odabrani ureÄ‘aj" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Odabrani ureÄ‘aj" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "Serijski broj" #. TRANSLATORS: Configured a BIOS setting to a value #, c-format msgid "Set BIOS setting '%s' using '%s'." msgstr "Postavi BIOS postavku '%s' koristeći '%s'." #. TRANSLATORS: command description msgid "Set a BIOS setting" msgstr "Postavi BIOS postavku" msgid "Set one or more BIOS settings" msgstr "Postavi jednu ili viÅ¡e BIOS postavki" #. TRANSLATORS: command description msgid "Sets one or more BIOS settings" msgstr "Postavlja jednu ili viÅ¡e BIOS postavki" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Postavlja popis odobrenih firmvera" #. TRANSLATORS: type of BIOS setting msgid "Setting type" msgstr "Vrsta postavke" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "Postavke će se primijeniti nakon ponovnog pokretanja sustava" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "Podijeli povijest firmvera sa razvijateljima" #. TRANSLATORS: command line option msgid "Show all results" msgstr "Prikaži sve rezultate" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "Prikaži inaÄicu klijenta i pozadinskog programa" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "Prikaži dodatne informacije pozadinskog programa za odreÄ‘enu domenu" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "Prikaži informacije otklanjanja greÅ¡ke za sve domene" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Prikaži mogućnosti otklanjanja greÅ¡ke" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Prikaži ureÄ‘aje koji se ne mogu nadopuniti" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Prikaži dodatne informacije otklanjanja greÅ¡aka" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "Prikaži povijest nadopune metapodataka" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "Prikaži predviÄ‘enu inÄicu DBX-a" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "Odmah iskljuÄi?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "PotpiÅ¡i frimver s novim kljuÄem" msgid "Sign data using the client certificate" msgstr "PotpiÅ¡i podatke koristeći vjerodajnicu klijenta" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "PotpiÅ¡i podatke koristeći vjerodajnicu klijenta" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "PotpiÅ¡i poslane podatke s vjerodajnicom klijenta" msgid "Signature" msgstr "Potpis" #. TRANSLATORS: firmware is verified on-device the payload using strong crypto msgid "Signed Payload" msgstr "Potpisani sadržaj prijenosa" #. TRANSLATORS: file size of the download msgid "Size" msgstr "VeliÄina" #. TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM msgid "Some of the platform secrets may be invalidated when updating this firmware." msgstr "OdreÄ‘ene tajne platforme mogu biti poniÅ¡tene tijekom nadopune ovog firmvera." #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "Izvor" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "Odredi datoteku dbx baze podataka" #. TRANSLATORS: The BIOS setting accepts strings msgid "String" msgstr "Izraz" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "UspjeÅ¡no" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "Svi ureÄ‘aji su uspjeÅ¡no aktivirani" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "Udaljena lokacija je uspjeÅ¡no onemogućena" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "Novi metapodaci su uspjeÅ¡no preuzeti: " #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "Udaljena lokacija je uspjeÅ¡no omogućena i provjerena" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "Udaljena lokacija je uspjeÅ¡no omogućena" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "Firmver je uspjeÅ¡no instaliran" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "Vrijednost podeÅ¡avanja je uspjeÅ¡no promijenjenja" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "Udaljena lokacija je uspjeÅ¡no promijenjena" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "Metapodaci su ruÄno uspjeÅ¡no osvježni" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "Kontrolni zbroj ureÄ‘aja je uspjeÅ¡no nadopunjen" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "UspjeÅ¡no poslan %u izvjeÅ¡taj" msgstr[1] "UspjeÅ¡no poslana %u izvjeÅ¡taja" msgstr[2] "UspjeÅ¡no poslano %u izvjeÅ¡taja" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "Kontrolni zbroj ureÄ‘aja je uspjeÅ¡no provjeren" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Sažetak" #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "Podržano" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "Podržan je CPU" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "Podržano na udaljenom poslužitelju" #. TRANSLATORS: Title: a better sleep state msgid "Suspend To Idle" msgstr "Suspendiraj u mirovanje" #. TRANSLATORS: Title: sleep state msgid "Suspend To RAM" msgstr "Suspendiraj u RAM" #. TRANSLATORS: longer description msgid "Suspend to Idle allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Suspendiraj u mirovanje omogućuje ureÄ‘aju brz odlazak na spavanje u svrhu Å¡tednje energije. Dok je ureÄ‘aj suspendiran, njegova se memorija može fiziÄki ukloniti i na taj naÄin pristupiti informacijama." #. TRANSLATORS: longer description msgid "Suspend to RAM allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Suspendiraj u RAM omogućuje ureÄ‘aju brz odlazak na spavanje u svrhu Å¡tednje energije. Dok je ureÄ‘aj suspendiran, njegova se memorija može fiziÄki ukloniti i na taj naÄin pristupiti informacijama." #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "Suspendiraj-u-mirovanje" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "Suspendiraj-u-ram" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "Prebaci ogranak sa %s na %s?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "Zamijeni ogranak firmvera na ureÄ‘aju" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "Sustav zahtijeva vanjski izvor energije" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "TEKST" #. TRANSLATORS: longer description msgid "TPM (Trusted Platform Module) is a computer chip that detects when hardware components have been tampered with." msgstr "TPM (Pouzdani Modul Platforme) je raÄunalni Äip koji otkriva neovlaÅ¡tene promjene hardverskih komponenta." #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "TPM PCR0 rekonstrukcija" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is invalid" msgstr "TPM PCR0 rekonstrukcija je nevaljana" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is now valid" msgstr "TPM PCR0 rekonstrukcija je sada valjana" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM Platform Configuration" msgstr "PodeÅ¡avanje TPM platforme" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM Reconstruction" msgstr "TPM rekonstrukcija" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM empty PCRs" msgstr "TPM prazani PCR-ovi" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "Oznaka" msgstr[1] "Oznake" msgstr[2] "Oznake" #. TRANSLATORS: we're saving all USB events for emulation msgid "Tagged for emulation" msgstr "OznaÄeno za emulaciju" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "Pokvareno" #. show the user the entire data blob msgid "Target" msgstr "OdrediÅ¡te" #. TRANSLATORS: command description msgid "Test a device using a JSON manifest" msgstr "Testiraj ureÄ‘aj koristeći JSON manifest" #. TRANSLATORS: when the release was tested msgid "Tested" msgstr "Testirano" #. TRANSLATORS: the %s is a vendor name, e.g. Lenovo #, c-format msgid "Tested by %s" msgstr "Testirao %s" #. TRANSLATORS: longer description msgid "The Intel Management Engine Key Manifest must be valid so that the device firmware can be trusted by the CPU." msgstr "Manifest kljuÄa Intel pogon upravljanja (Intel Management Engine) mora biti valjan tako da firmver ureÄ‘aja može vjerovati CPU." #. TRANSLATORS: longer description msgid "The Intel Management Engine controls device components and needs to have a recent version to avoid security issues." msgstr "Intel pogon upravljanja (Intel Management Engine) upravlja komponentama ureÄ‘aja i mora biti najnovije inaÄice kako bi se izbjegli sigurnosni problemi." #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "LVFS (Firmver Usluga Linux ProizvoÄ‘aÄa) je besplatna usluga koja djeluje kao neovisna pravna osoba i nema veze sa $OS_RELEASE:NAME$. VaÅ¡ distributer možda nije provjerio nadopune firmvera za kompatibilnost s vaÅ¡im sustavom ili prikljuÄenim ureÄ‘ajima. Svi firmveri su pružani od strane izvornih proizvoÄ‘aÄa opreme." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Platform Configuration is used to check whether the device start process has been modified." msgstr "PodeÅ¡avanje TPM (Pouzdani Modul Platforme) platforme se koristi za provjeru je li proces pokretanja ureÄ‘aja mijenjan." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Reconstruction is used to check whether the device start process has been modified." msgstr "TPM (Pouzdani Modul Platforme) rekonstrukcija se koristi za provjeru je li proces pokretanja ureÄ‘aja mijenjan." #. TRANSLATORS: this is more background on a security measurement problem msgid "The TPM PCR0 differs from reconstruction." msgstr "TPM PCR0 razlikuje se od rekonstrukcije." #. TRANSLATORS: longer description msgid "The UEFI Platform Key is used to determine if device software comes from a trusted source." msgstr "UEFI kljuÄ platforme se koristi za otkrivanje dolazi li softver ureÄ‘aja iz pouzdanih izvora." #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "Pozadinski program je uÄitao kÈd treće strane i viÅ¡e nije podržan od upstream razvijatelja!" #. TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name #, c-format msgid "The firmware from %s is not supplied by %s, the hardware vendor." msgstr "%s firmver nije isporuÄen od strane %s dobavljaÄa hardvera." #. TRANSLATORS: try to help msgid "The system clock has not been set correctly and downloading files may fail." msgstr "Sat sustava nije pravilno postavljen i preuzimanje datoteka možda ne uspije." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been re-inserted." msgstr "Nadopuna će se nastaviti kada se kabel USB ureÄ‘aja ponovno poveže." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged and then re-inserted." msgstr "Nadopuna će se nastaviti kada se kabel USB ureÄ‘aja odspoji i ponvno prikljuÄi." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged." msgstr "Nadopuna će se nastaviti kada se kabel USB ureÄ‘aja odspoji." #. TRANSLATORS: naughty vendor msgid "The vendor did not supply any release notes." msgstr "ProizvoÄ‘aÄ nije objavio nikakve biljeÅ¡ke izdanja." #. TRANSLATORS: now list devices with unfixed high-priority issues msgid "There are devices with issues:" msgstr "Postoje ureÄ‘aji s problemima:" #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "Nema blokiranih firmvera" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "Ne postoji odobreni firmver." #. TRANSLATORS: %1 is the current device version number, and %2 is the #. command name, e.g. `fwupdmgr sync` #, c-format msgid "This device will be reverted back to %s when the %s command is performed." msgstr "Ovaj ureÄ‘aj će biti vraćen natrag na %s kada se %s naredba pokrene." #. TRANSLATORS: the vendor did not upload this msgid "This firmware is provided by LVFS community members and is not provided (or supported) by the original hardware vendor." msgstr "Ovaj firmver je omogućen od strane LVFS Älanova zajednice i nije omogućen (ili podržan) od strane izvornog proizvoÄ‘aÄa hardvera." #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "Ovaj paket joÅ¡ nije provjeren, možda neće ispravno raditi." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "Ovaj program možda radi ispravno samo ako je pokrenut kao korijenski korisnik" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "Ova udaljena lokacija sadrži firmver koji nije zabranjen, ali se joÅ¡ uvijek testira od strane proizvoÄ‘aÄa hardvera. Provjerite da imate naÄin na ruÄno vraćanje starije inaÄice firmvera ako nadopuna firmvera ne uspije." #. TRANSLATORS: error message msgid "This system doesn't support firmware settings" msgstr "Ovaj sustav ne podržava postavke firmvera" #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "Ovaj sustav ima HSI probleme s vremenskim izvrÅ¡avanjem." #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "Ovaj sustav ima nisku HSI sigurnosnu razinu." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "Ovaj alat omogućuje administratoru primjenu UEFI dbx nadopuna." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "Ovaj alat dopuÅ¡ta administratoru postavljanje upita i upravljanje fwupd pozadinskim programom, Å¡to omogućuje obavljanje radnji poput instalacije i vraćanje starije inaÄice frimvera." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "Ovaj alat dopuÅ¡ta administratoru koriÅ¡tenje fwupd prikljuÄaka bez instalacije na sustavu." #. TRANSLATORS: the %1 is a BIOS setting name. #. * %2 and %3 are the values, e.g. "True" or "Windows10" #, c-format msgid "This tool can change the BIOS setting '%s' from '%s' to '%s' automatically, but it will only be active after restarting the computer." msgstr "Ovaj alat može promijeniti BIOS postavku '%s' iz '%s' u '%s' automatski, ali će biti aktivno nakon ponovnog pokretanja raÄunala." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "Ovo pomagalo će oÄitati i obraditi TPM zapis dogaÄ‘aja iz frimvera sustava." #. TRANSLATORS: the update state of the specific device msgid "Transient failure" msgstr "Privremeni kvar" #. TRANSLATORS: item is TRUE msgid "True" msgstr "Istina" #. TRANSLATORS: We verified the metadata against the server msgid "Trusted metadata" msgstr "Pouzdani metapodaci" #. TRANSLATORS: We verified the payload against the server msgid "Trusted payload" msgstr "Pouzdan sadržaj prijenosa" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Vrsta" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "UEFI ESP particija nije otkrivena ili podeÅ¡ena" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "UEFI kljuÄ platforme" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "UEFI sigurno pokretanje (SecureBoot)" #. TRANSLATORS: longer description msgid "UEFI Secure Boot prevents malicious software from being loaded when the device starts." msgstr "UEFI sigurno pokretanje (SecureBoot) sprjeÄava uÄitavanje zlonamjernog softvera pri pokretanju ureÄ‘aja." #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "Nadopune UEFI kapsule nisu dostupne ili omogućene u frimver postavljanju" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "UEFI dbx pomagalo" #. TRANSLATORS: system is not booted in UEFI mode msgid "UEFI firmware can not be updated in legacy BIOS mode" msgstr "UEFI firmver ne može se nadopuniti u zastarjelom BIOS naÄinu" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "UEFI kljuÄ platforme" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "UEFI sigurno pokretanje (SecureBoot)" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "Neuspjelo povezivanje s udaljenom uslugom" #. TRANSLATORS: error message msgid "Unable to find attribute" msgstr "Nemoguć pronalazak svojstva" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "Prekini povezivanje s trenutnim upravljaÄkim programom" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "Neblokiranje firmvera:" #. TRANSLATORS: command description msgid "Unblocks a specific firmware from being installed" msgstr "Deblokira instalaciju odreÄ‘enog firmvera" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "Nije Å¡ifrirano" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Nepoznat" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Nepoznat ureÄ‘aj" msgid "Unlock the device to allow access" msgstr "OtkljuÄaj ureÄ‘aj za dopuÅ¡tenje pristupa" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "OtkljuÄano" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "OtkljuÄava ureÄ‘aj za pristup firmveru" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "Demontira ESP" #. TRANSLATORS: firmware payload is unsigned and it is possible to modify it msgid "Unsigned Payload" msgstr "Nepotpisani sadržaj prijenosa" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "Nepodržana inaÄica pozadinskog programa %s, inaÄica klijenta je %s" #. TRANSLATORS: Suffix: the HSI result msgid "Untainted" msgstr "Ispravno" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "Nadopunjivo" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "GreÅ¡ka nadopune" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "Slika nadopune" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "Poruka nadopune" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "Stanje nadopune" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "NeuspjeÅ¡na nadopuna je poznat problem, posjetite ovaj URL za viÅ¡e informacija:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Nadopuni odmah?" #. TRANSLATORS: command description msgid "Update the stored cryptographic hash with current ROM contents" msgstr "Nadopuni spremljenu kriptografsku jedinstvenu vrijednost s trenutnim sadržajem ROM-a" msgid "Update the stored device verification information" msgstr "Nadopuni spremljenu informaciju provjere ureÄ‘aja" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "Nadopuni pohranjene metapodatke s trenutnim sadržajem" #. TRANSLATORS: command description msgid "Updates all specified devices to latest firmware version, or all devices if unspecified" msgstr "Nadopuni sve navedene ureÄ‘aje na najnovije inaÄice firmvera, ili sve ureÄ‘aje ako nisu navedeni" msgid "Updating" msgstr "Nadopunjivanje" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "Nadopunjujem %s…" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "Nadogradi %s sa %s na %s?" #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "Slanje izvjeÅ¡taja firmvera pomaže proizvoÄ‘aÄima hardvera brzo otkrivanje nedostatka i brzu nadopunu na stvarnim ureÄ‘ajima." #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "Hitnost" #. TRANSLATORS: explain how to get help, %1 is #. * 'fwupdtool --help' #. TRANSLATORS: explain how to get help, #. * where $1 is something like 'fwupdmgr --help' #, c-format msgid "Use %s for help" msgstr "Koristi %s za pomoć" #. TRANSLATORS: CTRL^C [holding control, and then pressing C] will exit the #. program msgid "Use CTRL^C to cancel." msgstr "Koristi CTRL^C za prekidanje." #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "Korisnik je obavijeÅ¡ten" #. TRANSLATORS: remote filename base msgid "Username" msgstr "KorisniÄko ime" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "Valjano" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "Provjera ESP sadržaja…" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "Varijanta" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "ProizvoÄ‘aÄ" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "Provjeravanje…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "InaÄica" #. TRANSLATORS: the fwupd version the release was tested on msgid "Version[fwupd]" msgstr "InaÄica[fwupd]" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "UPOZORENJE" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "ÄŒekanje…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "Nadgledaj promjene hardvera" #. TRANSLATORS: check various UEFI and ACPI tables are unchanged after the #. update msgid "Will measure elements of system integrity around an update" msgstr "Mjerit će elemente integriteta sustava oko nadopune" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "Zapisivanje datoteke:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Zapisivanje…" #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from the system firmware setup, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "Trebali bi osigurati da možete vratiti postavke iz podeÅ¡avanja firmvera sustava, poÅ¡to ova promjena može uzrokovati nemogućnost pokretanja u Linux sustav ili uzrokovati druge nestabilnosti sustava." #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "VaÅ¡ distributer možda nije provjerio nadopune firmvera za kompatibilnost s vaÅ¡im sustavom ili prikljuÄenim ureÄ‘ajima." #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "VaÅ¡ hardver se može oÅ¡tetiti upotrebom ovog firmvera, instalacija ovog izdanja može poniÅ¡titi jamstvo sa %s." #. TRANSLATORS: BKC is the industry name for the best known configuration and #. is a set #. * of firmware that works together #, c-format msgid "Your system is set up to the BKC of %s." msgstr "VaÅ¡ sustav je postavljen na BKC od %s." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[KONTROLNI ZBROJ]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[UREÄAJ-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[UREÄAJ-ID|GUID] [OGRANAK]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [VERSION]" msgstr "[ID|GUID-UREÄAJA] [INAÄŒICA]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILE FILE_SIG REMOTE-ID]" msgstr "[DATOTEKA POTPIS_DATOTEKE ID-UDALJENE_LOKACIJE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[NAZIV DATOTEKE1] [NAZIV DATOTEKE2]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2] [--no-authenticate]" msgstr "[POSTAVKA1] [POSTAVKA2] [--no-authenticate]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[SMBIOS-DATOTEKA|HWIDS-DATOTEKA]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "zadano" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "fwupd TPM pomagalo zapisa dogaÄ‘aja" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "fwupd prikljuÄci" fwupd-2.0.10/po/hu.po000066400000000000000000003355711501337203100143220ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Balázs Meskó , 2017-2019 # Balázs Meskó , 2017-2019 # Balázs Úr , 2015-2018,2023 # Balázs Úr, 2015-2018 # Gabor Kelemen , 2016 # Gábor Kelemen , 2016 # kelemeng , 2016 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Hungarian (http://app.transifex.com/freedesktop/fwupd/language/hu/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: hu\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "%.0f perc van hátra" msgstr[1] "%.0f perc van hátra" #. TRANSLATORS: BMC refers to baseboard management controller which #. * is the device that updates all the other firmware on the system #, c-format msgid "%s BMC Update" msgstr "%s alaplapmenedzsmentvezérlÅ‘-frissítés" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "%s akkumulátorfrissítés" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "%s CPU-mikrokódfrissítés" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "%s kamerafrissítés" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "%s konfigurációs frissítés" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "%s fogyasztói menedzsmentmotor-frissítés" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "%s vezérlÅ‘frissítés" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "%s vállalati menedzsmentmotor-frissítés" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "%s eszközfrissítés" #. TRANSLATORS: Video Display refers to the laptop internal display or #. * external monitor #, c-format msgid "%s Display Update" msgstr "%s kijelzÅ‘frissítés" #. TRANSLATORS: Dock refers to the port replicator hardware laptops are #. * cradled in, or lowered onto #, c-format msgid "%s Dock Update" msgstr "%s dokkolófrissítés" #. TRANSLATORS: drive refers to a storage device, e.g. SATA disk #, c-format msgid "%s Drive Update" msgstr "%s meghajtófrissítés" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "%s beágyazottvezérlÅ‘-frissítés" #. TRANSLATORS: a device that can read your fingerprint pattern #, c-format msgid "%s Fingerprint Reader Update" msgstr "%s ujjlenyomatolvasó-frissítés" #. TRANSLATORS: flash refers to solid state storage, e.g. UFS or eMMC #, c-format msgid "%s Flash Drive Update" msgstr "%s flashmeghajtó-frissítés" #. TRANSLATORS: GPU refers to a Graphics Processing Unit, e.g. #. * the "video card" #, c-format msgid "%s GPU Update" msgstr "%s GPU-frissítés" #. TRANSLATORS: a large pressure-sensitive drawing area typically used #. * by artists and digital artists #, c-format msgid "%s Graphics Tablet Update" msgstr "%s rajztáblafrissítés" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "%s billentyűzetfrissítés" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "%s menedzsmentmotor-frissítés" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "%s egérfrissítés" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "%s hálózaticsatoló-frissítés" #. TRANSLATORS: SSD refers to a Solid State Drive, e.g. non-rotating #. * SATA or NVMe disk #, c-format msgid "%s SSD Update" msgstr "%s SSD-frissítés" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "%s tárolóvezérlÅ‘-frissítés" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "%s rendszerfrissítés" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "%s TPM-frissítés" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "%s Thunderbolt-vezérlÅ‘frissítés" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "%s érintÅ‘tábla-frissítés" #. TRANSLATORS: Dock refers to the port replicator device connected #. * by plugging in a USB cable -- which may or may not also provide power #, c-format msgid "%s USB Dock Update" msgstr "%s USB-dokkolófrissítés" #. TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth #. * device that stays in the USB port so the wireless peripheral works #, c-format msgid "%s USB Receiver Update" msgstr "%s USB-vevÅ‘frissítés" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "%s frissítés" #. TRANSLATORS: warn the user before updating, %1 is a device name #, c-format msgid "%s and all connected devices may not be usable while updating." msgstr "A(z) %s és az összes kapcsolódó eszköz használhatatlan lehet a frissítés alatt." #. TRANSLATORS: %1 refers to some kind of security test, e.g. "Encrypted RAM". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s appeared: %s" msgstr "A(z) %s megjelent: %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "UEFI platform #. key". #. * %2 and %3 refer to results value, e.g. "Valid" and "Invalid" #, c-format msgid "%s changed: %s → %s" msgstr "A(z) %s megváltozott: %s → %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "SPI BIOS #. region". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s disappeared: %s" msgstr "A(z) %s eltűnt: %s" #. TRANSLATORS: the device has a reason it can't update, e.g. laptop lid #. closed #, c-format msgid "%s is not currently updatable" msgstr "A(z) %s jelenleg nem frissíthetÅ‘" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "%s gyártói mód" #. TRANSLATORS: warn the user before #. * updating, %1 is a device name #, c-format msgid "%s must remain connected for the duration of the update to avoid damage." msgstr "A(z) %s eszköznek kapcsolódva kell maradnia a frissítés alatt a károsodások elkerüléséhez." #. TRANSLATORS: warn the user before updating, %1 is a machine #. * name #, c-format msgid "%s must remain plugged into a power source for the duration of the update to avoid damage." msgstr "A(z) %s eszköznek kapcsolódva kell maradnia az áramforráshoz a frissítés alatt a károsodások elkerüléséhez." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "%s felülbírálás" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "%s verzió" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u nap" msgstr[1] "%u nap" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "%u eszköz rendelkezik elérhetÅ‘ belsÅ‘vezérlÅ‘program-frissítéssel." msgstr[1] "%u eszköz rendelkezik elérhetÅ‘ belsÅ‘vezérlÅ‘program-frissítéssel." #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device is not the best known configuration." msgid_plural "%u devices are not the best known configuration." msgstr[0] "%u eszköz nem a legismertebb konfiguráció." msgstr[1] "%u eszköz nem a legismertebb konfiguráció." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u óra" msgstr[1] "%u óra" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u perc" msgstr[1] "%u perc" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u másodperc" msgstr[1] "%u másodperc" #. TRANSLATORS: first percentage is current value, 2nd percentage is the #. * lowest limit the firmware update is allowed for the update to happen #, c-format msgid "%u%% (threshold %u%%)" msgstr "%u%% (küszöbszint: %u%%)" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(elavult)" #. TRANSLATORS: HSI event title msgid "A TPM PCR is now an invalid value" msgstr "Egy TPM PCR most érvénytelen érték" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "AMD Firmware Replay Protection" msgstr "AMD belsÅ‘ vezérlÅ‘program visszajátszási védelem" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "AMD Firmware Write Protection" msgstr "AMD belsÅ‘ vezérlÅ‘program írásvédelme" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "AMD Secure Processor Rollback Protection" msgstr "AMD biztonságos processzor visszaállítási védelem" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "ARCHIVE FIRMWARE METAINFO [FIRMWARE] [METAINFO] [JCATFILE]" msgstr "ARCHÃVUM BELSÅ-VEZÉRLÅPROGRAM METAINFORMÃCIÓK [BELSÅ-VEZÉRLÅPROGRAM] [METAINFORMÃCIÓK] [JCAT-FÃJL]" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "Művelet szükséges:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "Eszközök aktiválása" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "FüggÅ‘ben lévÅ‘ eszközök aktiválása" msgid "Activate the new firmware on the device" msgstr "Az új belsÅ‘ vezérlÅ‘program aktiválása az eszközön" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "BelsÅ‘vezérlÅ‘program-frissítés aktiválása" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "BelsÅ‘vezérlÅ‘program-frissítés aktiválása ennél:" #. TRANSLATORS: command description msgid "Adds devices to watch for future emulation" msgstr "Hozzáadja a jövÅ‘beni emulációhoz megfigyelendÅ‘ eszközöket" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Életkor" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "Beleegyezik és engedélyezi a távoli tárolót?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Ãlnév ehhez: %s" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are now valid" msgstr "Most már az összes TPM PCR érvényes" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are valid" msgstr "Az összes TPM PCR érvényes" #. TRANSLATORS: an application is preventing system updates msgid "All devices are prevented from update by system inhibit" msgstr "Az összes eszköz frissítését megakadályozza a rendszer gátlása" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "Az összes azonos típusú eszköz egyszerre lesz frissítve" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Régebbi belsÅ‘vezérlÅ‘program-verziók telepítésének lehetÅ‘vé tétele" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "A meglévÅ‘ belsÅ‘vezérlÅ‘program-verziók újratelepítésének lehetÅ‘vé tétele" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "BelsÅ‘vezérlÅ‘program-ág váltásának lehetÅ‘vé tétele" #. TRANSLATORS: is not the main firmware stream msgid "Alternate branch" msgstr "Alternatív ág" #. TRANSLATORS: another application is updating the device already msgid "An update is in progress" msgstr "Egy frissítés folyamatban van" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "Egy frissítés újraindítást igényel a befejezéshez." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "Egy frissítés a rendszer leállítását igényli a befejezéshez." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Igen válaszolása az összes kérdésre" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Frissítés alkalmazása akkor is, ha nem ajánlott" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Frissítési fájlok alkalmazása" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "Frissítés alkalmazása…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Jóváhagyott belsÅ‘ vezérlÅ‘program:" msgstr[1] "Jóváhagyott belsÅ‘ vezérlÅ‘programok:" #. TRANSLATORS: command description msgid "Asks the daemon to quit" msgstr "Arra kéri a démont, hogy lépjen ki" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "Csatlakoztatás a belsÅ‘ vezérlÅ‘program módhoz" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Hitelesítés…" #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "Hitelesítési részletek szükségesek" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Hitelesítés szükséges a belsÅ‘ vezérlÅ‘program régebbi verziójának egy cserélhetÅ‘ eszközön történÅ‘ telepítéséhez" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Hitelesítés szükséges a belsÅ‘ vezérlÅ‘program régebbi verziójának ezen a gépen történÅ‘ telepítéséhez" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "Hitelesítés szükséges a BIOS-beállítások módosításához" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "Hitelesítés szükséges a belsÅ‘ vezérlÅ‘program frissítéseihez beállított távoli tároló módosításához" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "Hitelesítés szükséges a démon konfigurációjának módosításához" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "Hitelesítés szükséges a BIOS-beállítások olvasásához" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "Hitelesítés szükséges a jóváhagyott belsÅ‘ vezérlÅ‘programok listájának beállításához" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "Hitelesítés szükséges az adatok ügyféltanúsítvány használatával történÅ‘ aláírásához" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "Hitelesítés szükséges a belsÅ‘ vezérlÅ‘program új verziójára váltáshoz" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Hitelesítés szükséges az eszköz feloldásához" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Hitelesítés szükséges a belsÅ‘ vezérlÅ‘program egy cserélhetÅ‘ eszközön történÅ‘ frissítéséhez" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Hitelesítés szükséges a belsÅ‘ vezérlÅ‘program ezen a gépen történÅ‘ frissítéséhez" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "Hitelesítés szükséges az eszköz tárolt ellenÅ‘rzőösszegeinek frissítéséhez" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "Automatikus jelentés" #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "Automatikusan feltöltse minden alkalommal?" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS Rollback Protection" msgstr "BIOS visszaállítási védelem" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS rollback protection" msgstr "BIOS visszaállítási védelem" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "LVFS-en vagy Windows Update-en keresztül szállított BIOS-frissítések" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "BUILDER-XML FILENAME-DST" msgstr "ÖSSZEÃLLÃTÓ-XML FÃJLNÉV-CÉL" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "Akkumulátor" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "Új rendszermag-illesztÅ‘program kötése" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "Tiltott belsÅ‘vezérlÅ‘program-fájlok:" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "Tiltott verzió" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "BelsÅ‘ vezérlÅ‘program tiltása:" #. TRANSLATORS: command description msgid "Blocks a specific firmware from being installed" msgstr "Megtiltja egy bizonyos belsÅ‘ vezérlÅ‘program telepítését" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "RendszerbetöltÅ‘ verziója" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "Ãg" #. TRANSLATORS: command description msgid "Build a cabinet archive from a firmware blob and XML metadata" msgstr "Szekrényarchívum összeállítása belsÅ‘vezérlÅ‘program-bloból és XML-metaadatokból" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "BelsÅ‘vezérlÅ‘program-fájl összeállítása" #. TRANSLATORS: longer description msgid "CPU Microcode must be updated to mitigate against various information-disclosure security issues." msgstr "A CPU-mikrokódot frissíteni kell a különbözÅ‘ információfeltárási biztonsági problémák kárenyhítéséhez." #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Mégse" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "Megszakítva" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "Nem lehet alkalmazni, mivel a dbx-frissítés már alkalmazva lett." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Megváltoztatva" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "EllenÅ‘rzi, hogy a kriptográfiai kivonat egyezik-e a belsÅ‘ vezérlÅ‘programmal" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "EllenÅ‘rzőösszeg" #. TRANSLATORS: get interactive prompt, where branch is the #. * supplier of the firmware, e.g. "non-free" or "free" msgid "Choose branch" msgstr "Ãg kiválasztása" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "Eszköz kiválasztása" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "BelsÅ‘ vezérlÅ‘program kiválasztása" #. TRANSLATORS: get interactive prompt msgid "Choose release" msgstr "Kiadás kiválasztása" #. TRANSLATORS: get interactive prompt msgid "Choose volume" msgstr "Kötet kiválasztása" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "Törli a legutóbbi frissítésbÅ‘l származó eredményeket" #. TRANSLATORS: error message msgid "Command not found" msgstr "A parancs nem található" #. TRANSLATORS: is not supported by the vendor msgid "Community supported" msgstr "Közösség által támogatott" #. TRANSLATORS: title prefix for the BIOS settings dialog msgid "Configuration Change Suggested" msgstr "Konfigurációváltoztatás javasolva" #. TRANSLATORS: no peeking msgid "Configuration is only readable by the system administrator" msgstr "A beállítások csak a rendszergazda számára olvashatók" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "BelsÅ‘vezérlÅ‘program-fájl átalakítása" #. TRANSLATORS: when the update was built msgid "Created" msgstr "Létrehozva" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "Kritikus" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "Kriptográfiaikivonat-ellenÅ‘rzés érhetÅ‘ el" #. TRANSLATORS: current value of a BIOS setting msgid "Current Value" msgstr "Jelenlegi érték" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "Jelenlegi verzió" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "ESZKÖZAZONOSÃTÓ|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Hibakeresési kapcsolók" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "Kibontás…" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Leírás" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "Leválasztás a rendszerbetöltÅ‘ módhoz" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "Részletek" #. TRANSLATORS: the best known configuration is a set of software that we know #. works #. * well together. In the OEM and ODM industries it is often called a BKC msgid "Deviate from the best known configuration?" msgstr "Eltér a legismertebb konfigurációtól?" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "EszközjelzÅ‘k" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "Eszközazonosító" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Eszköz hozzáadva:" #. TRANSLATORS: the device is already connected msgid "Device already exists" msgstr "Az eszköz már létezik" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "Az eszköz akkumulátorszintje túl alacsony" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse #, c-format msgid "Device battery power is too low (%u%%, requires %u%%)" msgstr "Az eszköz akkumulátorszintje túl alacsony (%u%%, de %u%% szükséges)" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "Az eszköz képes helyreállítani a beírási hibákat" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Eszköz megváltoztatva:" #. TRANSLATORS: a version check is required for all firmware msgid "Device firmware is required to have a version check" msgstr "Az eszköz belsÅ‘ vezérlÅ‘programja szükséges a verzió-ellenÅ‘rzéshez" #. TRANSLATORS: emulated means we are pretending to be a different model msgid "Device is emulated" msgstr "Az eszköz emulált" #. TRANSLATORS: device cannot be interrupted, for instance taking a phone call msgid "Device is in use" msgstr "Az eszköz használatban van" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "Az eszköz zárolva van" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "Eszköz szükséges az összes szolgáltatott kiadás telepítéséhez" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "Az eszköz elérhetetlen" #. TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode msgid "Device is unreachable, or out of wireless range" msgstr "Az eszköz elérhetetlen vagy a vezeték nélküli hatótávolságon kívül van" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "Az eszköz használható a frissítés ideje alatt" #. TRANSLATORS: usually this is when we're waiting for a reboot msgid "Device is waiting for the update to be applied" msgstr "Az eszköz a frissítés alkalmazására vár" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Eszköz eltávolítva:" #. TRANSLATORS: as in, wired mains power for a laptop msgid "Device requires AC power to be connected" msgstr "Az eszközhöz elektromos áram csatlakoztatása szükséges" #. TRANSLATORS: The device cannot be updated due to missing vendor's license." msgid "Device requires a software license to update" msgstr "Az eszköz szoftverlicencet igényel a frissítéshez" #. TRANSLATORS: longer description msgid "Device software updates are provided for this device." msgstr "Eszközszoftver-frissítések vannak szolgáltatva ehhez az eszközhöz." #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "Az eszköz szakaszosan frissít" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "Az eszköz támogatja a belsÅ‘ vezérlÅ‘program különbözÅ‘ ágainak váltását" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "Az eszközfrissítés aktiválást igényel" #. TRANSLATORS: save the old firmware to disk before installing the new one msgid "Device will backup firmware before installing" msgstr "Az eszköz biztonsági mentést fog készíteni a belsÅ‘ vezérlÅ‘programról a telepítés elÅ‘tt" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "Az eszköz nem fog újra megjelenni a frissítés befejezése után" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "Eszközök, amelyek sikeresen frissítve lettek:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "Eszközök, amelyek nem lettek helyesen frissítve:" #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "Nem elérhetÅ‘ belsÅ‘vezérlÅ‘program-frissítésekkel rendelkezÅ‘ eszközök: " #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "A legújabb elérhetÅ‘ belsÅ‘vezérlÅ‘program-verzióval rendelkezÅ‘ eszközök:" #. TRANSLATORS: this is for the device tests msgid "Did not find any devices with matching GUIDs" msgstr "Nem találhatók illeszkedÅ‘ csoportazonosítókkal rendelkezÅ‘ eszközök" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "Letiltva" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "Letiltja az adott távoli tárolót" #. TRANSLATORS: the OS the release was tested on msgid "Distribution" msgstr "Disztribúció" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "Ne ellenÅ‘rizze a régi metaadatokat" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "Ne ellenÅ‘rizze a nem jelentett elÅ‘zményeket" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "Ne ellenÅ‘rizze, ha a távoli tárolók letöltését engedélyezni kell" #. TRANSLATORS: command line option msgid "Do not check or prompt for reboot after update" msgstr "Ne ellenÅ‘rizzen vagy kérjen újraindítást a frissítés után" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "Ne tartalmazza a naplózási tartomány elÅ‘tagot" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "Ne tartalmazza az idÅ‘bélyeg elÅ‘tagot" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "Ne végezzen eszközbiztonsági ellenÅ‘rzéseket" #. TRANSLATORS: command line option msgid "Do not prompt for devices" msgstr "Ne kérjen az eszközöknél" #. TRANSLATORS: command line option msgid "Do not prompt to fix security issues" msgstr "Ne kérdezzen rá a biztonsági hibák javítására" #. TRANSLATORS: command line option msgid "Do not search the firmware when parsing" msgstr "Ne keresse a belsÅ‘ vezérlÅ‘programot a feldolgozáskor" #. TRANSLATORS: warning message shown after update has been scheduled msgid "Do not turn off your computer or remove the AC adaptor while the update is in progress." msgstr "Ne kapcsolja ki a számítógépet vagy ne távolítsa el a hálózati csatlakozót a frissítési folyamat közben." #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "Ne írjon az elÅ‘zmények adatbázisába" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "Megértette a belsÅ‘ vezérlÅ‘program ágának megváltoztatásával járó következményeket?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "Szeretné letiltani ezt a funkciót a jövÅ‘beni frissítéseknél?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "Szeretné most frissíteni ezt a távoli tárolót?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "Szeretné automatikusan feltölteni a jelentéseket a jövÅ‘beni frissítéseknél?" #. TRANSLATORS: command line option msgid "Don't prompt for authentication (less details may be shown)" msgstr "Ne kérdezzen rá a hitelesítésre (kevesebb részlet jelenhet meg)" #. TRANSLATORS: success msgid "Done!" msgstr "Kész!" #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "Telepíti a(z) %s eszközön a régebbi verziót: %s → %s?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "A belsÅ‘ vezérlÅ‘program régebbi verzióját telepíti egy eszközön" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "%s régebbi verziójának telepítése…" #. TRANSLATORS: command description msgid "Download a file" msgstr "Fájl letöltése" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Letöltés…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "SMBIOS adatok kiírása egy fájlból" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "IdÅ‘tartam" #. TRANSLATORS: longer description msgid "Each system should have tests to ensure firmware security." msgstr "Minden rendszernek rendelkeznie kell tesztekkel a belsÅ‘ vezérlÅ‘program biztonságának biztosításához." #. TRANSLATORS: command description msgid "Emulate a device using a JSON manifest" msgstr "Eszköz emulálása JSON-nyilvántartás használatával" #. TRANSLATORS: this device is not actually real msgid "Emulated" msgstr "Emulált" #. TRANSLATORS: Title: if we are emulating a different host msgid "Emulated host" msgstr "Emulált kiszolgáló" msgid "Enable" msgstr "Engedélyezés" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "Engedélyezi az új távoli tárolót?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "Engedélyezi ezt a távoli tárolót?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Engedélyezve" #. TRANSLATORS: Plugin is active only if hardware is found msgid "Enabled if hardware matches" msgstr "Engedélyezve, ha a hardver egyezik" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "Engedélyezi az adott távoli tárolót" msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "Csak a saját felelÅ‘sségére engedélyezze ezt a funkciót, ami azt jelenti, hogy az eredeti termék gyártójával kell kapcsolatba lépnie, ha problémát okoz a frissítés. Csak a frissítési folyamattal kapcsolatos problémákat jelentse itt be: $OS_RELEASE:BUG_REPORT_URL$." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "Ennek a távoli tárolónak az engedélyezése saját felelÅ‘sségére történik." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Titkosított" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "Titkosított memória" msgid "Encrypted RAM makes it impossible for information that is stored in device memory to be read if the memory chip is removed and accessed." msgstr "A titkosított memória lehetetlenné teszi, hogy az eszköz memóriájában tárolt információk olvashatók legyenek, ha a memória-áramkört eltávolítják és hozzáférnek." #. TRANSLATORS: the vendor is no longer supporting the device msgid "End of life" msgstr "Életciklus vége" #. TRANSLATORS: The BIOS setting can only be changed to fixed values msgid "Enumeration" msgstr "Felsorolás" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "Az összes belsÅ‘vezérlÅ‘program-frissítési elÅ‘zmény törlése" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Törlés…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "Kilépés egy kis késleltetés után" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "Kilépés a motor betöltÅ‘dése után" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "BelsÅ‘vezérlÅ‘program-fájl szerkezetének exportálása XML-be" #. TRANSLATORS: command description msgid "Extract a firmware blob to images" msgstr "BelsÅ‘vezérlÅ‘program-blob kibontása lemezképekbe" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "FÃJL" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "FÃJL [ESZKÖZAZONOSÃTÓ|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "FÃJLNÉV" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "FÃJLNÉV TANÚSÃTVÃNY SZEMÉLYES-KULCS" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "FÃJLNÉV ESZKÖZAZONOSÃTÓ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME OFFSET DATA [FIRMWARE-TYPE]" msgstr "FÃJLNÉV ELTOLÃS ADATOK [BELSÅVEZÉRLÅPROGRAM-TÃPUS]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "FÃJLNÉV [ESZKÖZAZONOSÃTÓ|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "FÃJLNÉV [BELSÅVEZÉRLÅPROGRAM-TÃPUS]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" msgstr "FÃJLNÉV-FORRÃS FÃJLNÉV-CÉL [BELSÅVEZÉRLÅPROGRAM-TÃPUS-FORRÃS] [BELSÅVEZÉRLÅPROGRAM-TÃPUS-CÉL]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" msgstr "FÃJLNÉV|ELLENÅRZÅÖSSZEG1[,ELLENÅRZÅÖSSZEG2][,ELLENÅRZÅÖSSZEG3]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "Sikertelen" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Nem sikerült a frissítés alkalmazása" #. TRANSLATORS: error message for Windows msgid "Failed to connect to Windows service, please ensure it's running." msgstr "Nem sikerült kapcsolódni a Windows szolgáltatáshoz, gyÅ‘zÅ‘djön meg arról, hogy fut-e." #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "Nem sikerült a démonhoz kapcsolódni" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "Nem sikerült a helyi dbx betöltése" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "Nem sikerült a rendszer dbx betöltése" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "Nem sikerült a zárolás" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Nem sikerült az argumentumok feldolgozása" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "Nem sikerült a fájl feldolgozása" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "Nem sikerült a helyi dbx feldolgozása" #. TRANSLATORS: a feature is something like "can show an image" msgid "Failed to set front-end features" msgstr "Nem sikerült beállítani az elÅ‘tétprogram funkcióit" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "Nem sikerült az ESP-tartalom ellenÅ‘rzése" #. TRANSLATORS: item is FALSE msgid "False" msgstr "Hamis" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Fájlnév" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "Fájlnév aláírása" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "Fájlnév forrása" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Fájlnév szükséges" #. TRANSLATORS: command line option msgid "Filter with a set of device flags using a ~ prefix to exclude, e.g. 'internal,~needs-reboot'" msgstr "Szűrés eszközjelzÅ‘k megadásával, használja a ~ elÅ‘tagot a kihagyáshoz, például „internal,~needs-rebootâ€" #. TRANSLATORS: command line option msgid "Filter with a set of release flags using a ~ prefix to exclude, e.g. 'trusted-release,~trusted-metadata'" msgstr "Szűrés kiadásjelzÅ‘k megadásával, használja a ~ elÅ‘tagot a kihagyáshoz, például „trusted-release,~trusted-metadataâ€" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware Attestation" msgstr "BelsÅ‘ vezérlÅ‘program tanúsítás" #. TRANSLATORS: longer description msgid "Firmware Attestation checks device software using a reference copy, to make sure that it has not been changed." msgstr "A belsÅ‘ vezérlÅ‘program tanúsítása egy hivatkozási másolat használatával ellenÅ‘rzi eszközszoftvert annak biztosításához, hogy azt nem változtatták meg." #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "BelsÅ‘ vezérlÅ‘program BIOS-leírója" #. TRANSLATORS: longer description msgid "Firmware BIOS Descriptor protects device firmware memory from being tampered with." msgstr "A belsÅ‘ vezérlÅ‘program BIOS-leírója megvédi az eszköz belsÅ‘ vezérlÅ‘programjának memóriáját a manipulációtól." #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "BelsÅ‘ vezérlÅ‘program BIOS-ának területe" #. TRANSLATORS: longer description msgid "Firmware BIOS Region protects device firmware memory from being tampered with." msgstr "A belsÅ‘ vezérlÅ‘program BIOS-területe megvédi az eszköz belsÅ‘ vezérlÅ‘programjának memóriáját a manipulációtól." #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "BelsÅ‘ vezérlÅ‘program alap URI-ja" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "BelsÅ‘vezérlÅ‘program-frissítési D-busz-szolgáltatás" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "BelsÅ‘vezérlÅ‘program-frissítési démon" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "BelsÅ‘ vezérlÅ‘program frissítÅ‘jének ellenÅ‘rzése" msgid "Firmware Updater Verification checks that software used for updating has not been tampered with." msgstr "A belsÅ‘ vezérlÅ‘program frissítÅ‘jének ellenÅ‘rzése azt ellenÅ‘rzi, hogy a frissítéshez használt szoftvert nem manipulálták-e." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "BelsÅ‘ vezérlÅ‘program frissítések" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "BelsÅ‘ vezérlÅ‘program segédprogram" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "BelsÅ‘ vezérlÅ‘program írásvédelme" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "BelsÅ‘ vezérlÅ‘program írásvédelmének zárolása" #. TRANSLATORS: longer description msgid "Firmware Write Protection protects device firmware memory from being tampered with." msgstr "A belsÅ‘ vezérlÅ‘program írásvédelme megvédi az eszköz belsÅ‘ vezérlÅ‘programjának memóriáját a manipulációtól." #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "BelsÅ‘ vezérlÅ‘program tanúsítás" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "A belsÅ‘ vezérlÅ‘program már tiltva van" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "A belsÅ‘ vezérlÅ‘program még nincs tiltva" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "A belsÅ‘ vezérlÅ‘program metaadatai %u napja nem lettek frissítve, és elavultak lehetnek." msgstr[1] "A belsÅ‘ vezérlÅ‘program metaadatai %u napja nem lettek frissítve, és elavultak lehetnek." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "BelsÅ‘ vezérlÅ‘program frissítések" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "JelzÅ‘k" #. TRANSLATORS: command line option msgid "Force the action by relaxing some runtime checks" msgstr "A művelet kényszerítése néhány futásidejű ellenÅ‘rzés lazításával" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Megtalált" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "Teljes lemeztitkosítás észlelhetÅ‘" #. TRANSLATORS: we might ask the user the recovery key when next booting #. Windows msgid "Full disk encryption secrets may be invalidated when updating" msgstr "A teljes lemeztitkosítás titkai érvénytelenné válhatnak a frissítésekor" #. TRANSLATORS: Title: if the part has been fused msgid "Fused Platform" msgstr "Biztosított platform" #. TRANSLATORS: Title: if the part has been fused msgid "Fused platform" msgstr "Biztosított platform" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" msgstr[1] "GUID-k" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "GUID|DEVICE-ID" msgstr "GUID|ESZKÖZAZONOSÃTÓ" msgid "Get BIOS settings" msgstr "BIOS-beállítások lekérése" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "Az fwupd által támogatott összes eszközjelzÅ‘ lekérése" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Az összes eszköz lekérése, amelyek támogatják a belsÅ‘vezérlÅ‘program-frissítéseket" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "A rendszeren regisztrált összes engedélyezett bÅ‘vítmény lekérése" #. TRANSLATORS: command description msgid "Get device report metadata" msgstr "Eszközjelentés metaadatainak lekérése" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Részleteket kér le egy belsÅ‘ vezérlÅ‘program fájljáról" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "Lekéri a beállított távoli tárolókat" #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "Lekéri a kiszolgáló biztonsági attribútumait" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "Lekéri a jóváhagyott belsÅ‘ vezérlÅ‘programok listáját" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "Lekéri a tiltott belsÅ‘ vezérlÅ‘programok listáját" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Lekéri a frissítések listáját a csatlakoztatott hardverhez" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "Lekéri az eszközhöz tartozó kiadásokat" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Lekéri a legutóbbi frissítésbÅ‘l származó eredményeket" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "HWIDS-FÃJL" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "A hardver újracsatlakoztatásra vár" #. TRANSLATORS: the release urgency msgid "High" msgstr "Magas" #. TRANSLATORS: title for host security events msgid "Host Security Events" msgstr "A gép biztonsági eseményei" #. TRANSLATORS: error message for unsupported feature #, c-format msgid "Host Security ID (HSI) is not supported" msgstr "A kiszolgálóbiztonsági azonosító (HSI) nem támogatott" #. TRANSLATORS: success, so say thank you to the user msgid "Host Security ID attributes uploaded successfully, thanks!" msgstr "A kiszolgálóbiztonsági azonosító attribútumai sikeresen feltöltve, köszönjük!" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "Kiszolgálóbiztonsági azonosító:" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INHIBIT-ID" msgstr "GÃTLÓ-AZONOSÃTÓ" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "IOMMU védelem" #. TRANSLATORS: longer description msgid "IOMMU Protection prevents connected devices from accessing unauthorized parts of system memory." msgstr "Az IOMMU védelem megakadályozza a csatlakoztatott eszközöket abban, hogy hozzáférjenek a rendszermemória jogosulatlan részeihez." #. TRANSLATORS: HSI event title msgid "IOMMU device protection disabled" msgstr "Az IOMMU eszközvédelem letiltva" #. TRANSLATORS: HSI event title msgid "IOMMU device protection enabled" msgstr "Az IOMMU eszközvédelem engedélyezve" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "Üresjárat…" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "A szigorú SSL-ellenÅ‘rzések mellÅ‘zése a fájlok letöltésekor" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "BelsÅ‘ vezérlÅ‘program ellenÅ‘rzőösszeg-hibáinak figyelmen kívül hagyása" #. TRANSLATORS: command line option msgid "Ignore firmware hardware mismatch failures" msgstr "BelsÅ‘ vezérlÅ‘program hardvereltérési hibáinak figyelmen kívül hagyása" #. TRANSLATORS: try to help msgid "Ignoring SSL strict checks, to do this automatically in the future export DISABLE_SSL_STRICT in your environment" msgstr "Szigorú SSL-ellenÅ‘rzések mellÅ‘zése, ennek a jövÅ‘beni automatikus használatához exportálja az DISABLE_SSL_STRICT változót a környezetébe" #. TRANSLATORS: the inhibit ID is a short string like dbus-123456 #, c-format msgid "Inhibit ID is %s." msgstr "A gátló azonosító: %s." #. TRANSLATORS: command description msgid "Inhibit the system to prevent upgrades" msgstr "A rendszer gátlása a frissítések megakadályozásához" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "Telepítés idÅ‘tartama" #. TRANSLATORS: command description msgid "Install a firmware file in cabinet format on this hardware" msgstr "Egy belsÅ‘vezérlÅ‘program-fájl telepítése szekrényformátumban ezen az eszközön" #. TRANSLATORS: command description msgid "Install a raw firmware blob on a device" msgstr "Egy nyers belsÅ‘vezérlÅ‘program-blob telepítése egy eszközön" #. TRANSLATORS: command description msgid "Install a specific firmware file on all devices that match" msgstr "Egy bizonyos belsÅ‘vezérlÅ‘program-fájl telepítése az összes egyezÅ‘ eszközön" #. TRANSLATORS: command description msgid "Install a specific firmware on a device, all possible devices will also be installed once the CAB matches" msgstr "Egy bizonyos belsÅ‘ vezérlÅ‘program telepítése egy eszközön, az összes lehetséges eszköz is telepítve lesz, amikor a CAB egyezik" msgid "Install old version of signed system firmware" msgstr "Aláírt rendszer belsÅ‘ vezérprogram régi verziójának telepítése" msgid "Install old version of unsigned system firmware" msgstr "Nem aláírt rendszer belsÅ‘ vezérlÅ‘program régi verziójának telepítése" msgid "Install signed device firmware" msgstr "Aláírt eszköz belsÅ‘ vezérlÅ‘program telepítése" msgid "Install signed system firmware" msgstr "Aláírt rendszer belsÅ‘ vezérlÅ‘program telepítése" msgid "Install unsigned device firmware" msgstr "Nem aláírt eszköz belsÅ‘ vezérlÅ‘program telepítése" msgid "Install unsigned system firmware" msgstr "Nem aláírt rendszer belsÅ‘ vezérlÅ‘program telepítése" #. TRANSLATORS: stay on one firmware version unless the new version is #. explicitly #. * specified msgid "Installing a specific release is explicitly required" msgstr "Egy bizonyos kiadás telepítése kifejezetten szükséges" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "BelsÅ‘ vezérlÅ‘program frissítésének telepítése…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "Telepítés a(z) %s eszközön…" #. TRANSLATORS: if it breaks, you get to keep both pieces msgid "Installing this update may also void any device warranty." msgstr "A frissítés telepítése az eszköz garanciáját is érvénytelenítheti." #. TRANSLATORS: The BIOS setting only accepts integers in a fixed range msgid "Integer" msgstr "Egész szám" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM Protected" msgstr "Intel BootGuard ACM védett" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "Intel BootGuard ACM védett" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard Error Policy" msgstr "Intel BootGuard hibairányelv" msgid "Intel BootGuard Error Policy ensures the device does not continue to start if its device software has been tampered with." msgstr "Az Intel BootGuard hibairányelv azt biztosítja, hogy az eszköz nem folytatja az indulást, ha az eszközszoftverét manipulálták." #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard Fuse" msgstr "Intel BootGuard biztosíték" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "Intel BootGuard egyszer programozható biztosíték" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard Verified Boot" msgstr "Intel BootGuard által ellenÅ‘rzött rendszerindítás" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Intel BootGuard hibairányelv" #. TRANSLATORS: longer description msgid "Intel BootGuard prevents unauthorized device software from operating when the device is started." msgstr "Az Intel BootGuard megakadályozza a hitelesítetlen eszközszoftvereket abban, hogy az eszköz indulásakor működjenek." #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Intel BootGuard által ellenÅ‘rzött rendszerindítás" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS Mitigation" msgstr "Intel GDS kárenyhítés" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS mitigation" msgstr "Intel GDS kárenyhítés" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Manufacturing Mode" msgstr "Intel menedzsmentmotor gyártói mód" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the "override" is #. enabled #. * with a jumper -- luckily it is probably not accessible to end users on #. consumer #. * boards msgid "Intel Management Engine Override" msgstr "Intel menedzsmentmotor felülbírálás" #. TRANSLATORS: longer description msgid "Intel Management Engine Override disables checks for device software tampering." msgstr "Az Intel menedzsmentmotor felülbírálás letiltja az eszközszoftver manipulálásának ellenÅ‘rzéseit." #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Version" msgstr "Intel menedzsmentmotor verziója" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "BelsÅ‘ eszköz" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "Érvénytelen" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "Érvénytelen argumentumok" #. TRANSLATORS: error message msgid "Invalid arguments, expected GUID" msgstr "Érvénytelen argumentumok, GUID az elvárt" #. TRANSLATORS: error message msgid "Invalid arguments, expected at least ARCHIVE FIRMWARE METAINFO" msgstr "Érvénytelen argumentumok, legalább ARCHÃVUM BELSÅ-VEZÉRLÅPROGRAM METAINFORMÃCIÓK az elvárt" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "Régebbi verzió" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "RendszerbetöltÅ‘ módban van" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "Frissítés" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "Probléma" msgstr[1] "Problémák" #. TRANSLATORS: HSI event title msgid "Kernel is no longer tainted" msgstr "A rendszermag többé nem fertÅ‘zött" #. TRANSLATORS: HSI event title msgid "Kernel is tainted" msgstr "A rendszermag fertÅ‘zött" #. TRANSLATORS: HSI event title msgid "Kernel lockdown disabled" msgstr "A rendszermag lezárása letiltva" #. TRANSLATORS: HSI event title msgid "Kernel lockdown enabled" msgstr "A rendszermag lezárása engedélyezve" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "HELY" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "Utoljára módosítva" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "Kevesebb mint egy perc van hátra" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "Licenc" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "Linux rendszermag zárlat" #. TRANSLATORS: longer description msgid "Linux Kernel Lockdown mode prevents administrator (root) accounts from accessing and changing critical parts of system software." msgstr "A Linux rendszermag zárlat mód megakadályozza a rendszergazda (root) fiókokat abban, hogy hozzáférjenek és megváltoztassák a rendszerszoftver kritikus részeit." msgid "Linux Kernel Swap temporarily saves information to disk as you work. If the information is not protected, it could be accessed by someone if they obtained the disk." msgstr "A Linux rendszermag cserehely átmenetileg elmenti az információkat a lemezre a munkavégzés során. Ha az információk nincsenek védve, akkor azok elérhetÅ‘k másoknak is, ha megszerzik a lemezt." #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "Linux rendszermag ellenÅ‘rzés" msgid "Linux Kernel Verification makes sure that critical system software has not been tampered with. Using device drivers which are not provided with the system can prevent this security feature from working correctly." msgstr "A Linux rendszermag ellenÅ‘rzés azt biztosítja, hogy a kritikus rendszerszoftvert ne manipulálják. A nem a rendszerrel együtt szolgáltatott eszköz-illesztÅ‘programok használata megakadályozhatja ennek a biztonsági funkciónak a megfelelÅ‘ működését." #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "Linux cserehely" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Linux gyártói belsÅ‘ vezérlÅ‘program szolgáltatás (stabil belsÅ‘ vezérlÅ‘program)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Linux gyártói belsÅ‘ vezérlÅ‘program szolgáltatás (teszt belsÅ‘ vezérlÅ‘program)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Linux rendszermag" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Linux rendszermag zárlat" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Linux cserehely" #. TRANSLATORS: command description msgid "List EFI variables with a specific GUID" msgstr "Egy bizonyos csoportazonosítóval rendelkezÅ‘ EFI-változók felsorolása" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "A dbx-ben lévÅ‘ bejegyzések felsorolása" #. TRANSLATORS: command description msgid "List the available firmware GTypes" msgstr "Az elérhetÅ‘ belsÅ‘vezérlÅ‘program-G-típusok felsorolása" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "Az elérhetÅ‘ belsÅ‘vezérlÅ‘program-típusok felsorolása" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "Felsorolja az ESP-n lévÅ‘ fájlokat" #. TRANSLATORS: command description msgid "Load device emulation data" msgstr "Eszközemulációs adatok betöltése" #. TRANSLATORS: the plugin was created from a .so object, and was not built-in msgid "Loaded from an external module" msgstr "Betöltve egy külsÅ‘ modulból" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "Betöltés…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "Zárolt" #. TRANSLATORS: the release urgency msgid "Low" msgstr "Alacsony" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refers #. * to the private/public key used to secure loading of firmware msgid "MEI Key Manifest" msgstr "MEI kulcsnyilvántartás" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refer #. * to the private/public key used to secure loading of firmware msgid "MEI key manifest" msgstr "MEI kulcsnyilvántartás" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "MEI gyártói mód" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "MEI felülbírálás" msgid "MEI version" msgstr "MEI verzió" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "Bizonyos bÅ‘vítmények kézi engedélyezése" #. TRANSLATORS: longer description msgid "Manufacturing Mode is used when the device is manufactured and security features are not yet enabled." msgstr "A gyártói módot akkor használják, amikor az eszköz gyártása folyamatban van, és a biztonsági funkciók még nincsenek engedélyezve." #. TRANSLATORS: Longest valid string for BIOS setting msgid "Maximum length" msgstr "Legnagyobb hossz" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "Legnagyobb érték" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "Közepes" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "Metaadatok aláírása" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "Metaadatok URI-ja" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "A metaadatok nem szerezhetÅ‘ek be a Linux gyártói belsÅ‘ vezérlÅ‘program szolgáltatásból." #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "Legkisebb verzió" #. TRANSLATORS: Shortest valid string for BIOS setting msgid "Minimum length" msgstr "Legkisebb hossz" #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "Legkisebb érték" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "Módosít egy démonbeállítási értéket" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "Módosítja a megadott távoli tárolót" msgid "Modify a configured remote" msgstr "A beállított távoli tároló módosítása" msgid "Modify daemon configuration" msgstr "Démon konfigurációjának módosítása" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "A démon eseményeinek figyelése" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "Csatolja az ESP-t" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "Újraindítást igényel a telepítés után" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "Újraindítást igényel" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "Leállítást igényel a telepítés után" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "Új verzió" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "Nincs művelet megadva!" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "Nincsenek régebbi verziók a(z) %s eszközhöz" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "Nem találhatók belsÅ‘vezérlÅ‘program-azonosítók" #. TRANSLATORS: nothing found msgid "No firmware found" msgstr "Nem található belsÅ‘ vezérlÅ‘program" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Nem észlelhetÅ‘ belsÅ‘vezérlÅ‘program-frissítési képességgel rendelkezÅ‘ hardver" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "Nincsenek elérhetÅ‘ kiadások" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "Jelenleg nincsenek engedélyezett távoli tárolók, így nem érhetÅ‘ek el metaadatok." #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "Nincsenek elérhetÅ‘ távoli tárolók" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "Nincsenek frissíthetÅ‘ eszközök" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "Nincsenek elérhetÅ‘ frissítések" msgid "No updates available for remaining devices" msgstr "Nem érhetÅ‘k el frissítések a hátralévÅ‘ eszközökhöz" #. TRANSLATORS: version cannot be installed due to policy msgid "Not approved" msgstr "Nincs jóváhagyva" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "Nem található" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "Nem támogatott" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "OK" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "OK!" #. TRANSLATORS: the firmware old version msgid "Old version" msgstr "Régi verzió" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "Csak önálló PCR-értékek megjelenítése" #. TRANSLATORS: command line option msgid "Only use peer-to-peer networking when downloading files" msgstr "Csak egyenrangú hálózatkezelés használata a fájlok letöltésekor" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "Csak verziófrissítések engedélyezettek" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "Az alapértelmezett ESP-útvonal felülbírálása" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Firmware" msgstr "Egyenrangú belsÅ‘ vezérlÅ‘program" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Metadata" msgstr "Egyenrangú metaadatok" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "ÚTVONAL" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "BelsÅ‘vezérlÅ‘program-fájl részleteinek feldolgozása és megjelenítése" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "A dbx-frissítés feldolgozása…" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "A rendszer dbx feldolgozása…" #. TRANSLATORS: remote filename base msgid "Password" msgstr "Jelszó" #. TRANSLATORS: command description msgid "Patch a firmware blob at a known offset" msgstr "BelsÅ‘vezérlÅ‘program-blob befoltozása egy ismert eltolásnál" msgid "Payload" msgstr "Hasznos teher" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "FüggÅ‘ben" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "Végrehajtja a műveletet?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "Platform hibakeresése" #. TRANSLATORS: longer description msgid "Platform Debugging allows device security features to be disabled. This should only be used by hardware manufacturers." msgstr "A platform hibakeresése lehetÅ‘vé teszi az eszköz biztonsági funkcióinak letiltását. Ezt csak a hardvergyártóknak kellene használniuk." #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "Platform hibakeresése" #. TRANSLATORS: 'recovery key' here refers to a code, rather than a physical #. metal thing msgid "Please ensure you have the volume recovery key before continuing." msgstr "A folytatás elÅ‘tt gyÅ‘zÅ‘djön meg arról, hogy rendelkezik-e a kötet helyreállítási kulcsával." #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "Adjon meg egy számot 0 és %u között: " #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "A bÅ‘vítmény függÅ‘ségei hiányoznak" #. TRANSLATORS: Possible values for a bios setting msgid "Possible Values" msgstr "Lehetséges értékek" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA Protection" msgstr "Rendszerindítás elÅ‘tti közvetlen memória hozzáférés elleni védelem" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "Rendszerindítás elÅ‘tti közvetlen memória hozzáférés elleni védelem" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is disabled" msgstr "A rendszerindítás elÅ‘tti közvetlen memória hozzáférés elleni védelem letiltva" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is enabled" msgstr "A rendszerindítás elÅ‘tti közvetlen memória hozzáférés elleni védelem engedélyezve" #. TRANSLATORS: longer description msgid "Pre-boot DMA protection prevents devices from accessing system memory after being connected to the computer." msgstr "A rendszerindítás elÅ‘tti közvetlen memória hozzáférés elleni védelem megakadályozza az eszközöket abban, hogy hozzáférjenek a rendszermemóriához, miután kapcsolódtak a számítógéphez." #. TRANSLATORS: warning message msgid "Press unlock on the device to continue the update process." msgstr "Nyomja meg a feloldást a készüléken a frissítési folyamat folytatásához." #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "ElÅ‘zÅ‘ verzió" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Prioritás" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "Problémák" msgid "Proceed with upload?" msgstr "Folytatja a feltöltést?" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Processor Security Checks" msgstr "Processzor biztonsági ellenÅ‘rzései" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "Processor rollback protection" msgstr "Processzor visszaállítási védelem" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "Tulajdonosi" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "TÃVOLITÃROLÓ-AZONOSÃTÓ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID KEY VALUE" msgstr "TÃVOLITÃROLÓ-AZONOSÃTÓ KULCS ÉRTÉK" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "Csak olvasható" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "BelsÅ‘vezérlÅ‘program-blob beolvasása egy eszközrÅ‘l" #. TRANSLATORS: command description msgid "Read a firmware from a device" msgstr "BelsÅ‘ vezérlÅ‘program beolvasása egy eszközrÅ‘l" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "Olvasás a(z) %s eszközrÅ‘l…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Olvasás…" #. TRANSLATORS: how often we should refresh the metadata msgid "Refresh Interval" msgstr "Frissítési idÅ‘köz" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "Metaadatok frissítése a távoli kiszolgálóról" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "Újratelepíti a(z) %s eszközt ezzel: %s?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "A jelenlegi belsÅ‘ vezérlÅ‘program újratelepítése az eszközön" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "BelsÅ‘ vezérlÅ‘program újratelepítése egy eszközön" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "Kiadási ág" #. TRANSLATORS: release attributes msgid "Release Flags" msgstr "Kiadási jelzÅ‘k" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "Kiadási azonosító" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "Távoli azonosító" #. TRANSLATORS: command description msgid "Removes devices to watch for future emulation" msgstr "Eltávolítja a jövÅ‘beni emulációhoz megfigyelendÅ‘ eszközöket" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "Jelentési URI" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "Jelentve a távoli kiszolgálónak" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "A szükséges efivarfs fájlrendszer nem található" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "A szükséges hardver nem található" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "RendszerbetöltÅ‘t igényel" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Internetkapcsolat szükséges" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Újraindítja most?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "Újraindítja a démont a változtatás életbe léptetéséhez?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Eszköz újraindítása…" #. TRANSLATORS: command description msgid "Retrieve BIOS settings. If no arguments are passed all settings are returned" msgstr "BIOS-beállítások lekérése. Ha nincsenek argumentumok átadva, akkor az összes beállítás vissza lesz adva" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "A gép összes hardverazonosítójának visszaadása" #. TRANSLATORS: longer description msgid "Rollback Protection prevents device software from being downgraded to an older version that has security problems." msgstr "A visszaállítási védelem megakadályozza, hogy az eszközszoftvert visszaállítsák egy olyan régebbi verzióra, amely biztonsági problémákkal rendelkezik." #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "A bÅ‘vítmény összetett tisztítási rutinjának futtatása az install-blob használatakor" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "A bÅ‘vítmény összetett elÅ‘készítési rutinjának futtatása az install-blob használatakor" #. TRANSLATORS: tell a user how to get information #, c-format msgid "Run without '%s' to see" msgstr "Futtassa „%s†nélkül a megtekintéshez" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "A futó rendszermag túl régi" #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "Futásidejű utótag" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING VALUE" msgstr "BEÃLLÃTÃS ÉRTÉK" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING1 VALUE1 [SETTING2] [VALUE2]" msgstr "BEÃLLÃTÃS1 ÉRTÉK1 [BEÃLLÃTÃS2] [ÉRTÉK2]" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "SPI BIOS-leírója" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "SPI BIOS-területe" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "SPI zárolása" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "SPI replay protection" msgstr "SPI visszajátszási védelem" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "SPI írása" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "SPI write protection" msgstr "SPI írásvédelem" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "ALRENDSZER ILLESZTÅPROGRAM [ESZKÖZAZONOSÃTÓ|GUID]" #. TRANSLATORS: command description msgid "Save a file that allows generation of hardware IDs" msgstr "A hardverazonosítók előállítását lehetÅ‘vé tevÅ‘ fájl mentése" #. TRANSLATORS: command description msgid "Save device emulation data" msgstr "Eszközemulációs adatok mentése" #. TRANSLATORS: Scalar increment for integer BIOS setting msgid "Scalar Increment" msgstr "Skála növekmény" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Ütemezés…" #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "A biztonságos rendszerindítás letiltva" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "A biztonságos rendszerindítás engedélyezve" #. TRANSLATORS: the %1 is a URL to a wiki page #, c-format msgid "See %s for more details." msgstr "Nézze meg a(z) %s szócikket a további részletekért." #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "További információkért nézze meg a(z) %s webhelyet." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Kiválasztott eszköz" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Kiválasztott kötet" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "Sorozatszám" #. TRANSLATORS: Configured a BIOS setting to a value #, c-format msgid "Set BIOS setting '%s' using '%s'." msgstr "A(z) „%s†BIOS-beállítás megadása „%s†értékre." #. TRANSLATORS: command description msgid "Set a BIOS setting" msgstr "BIOS-beállítás megadása" msgid "Set one or more BIOS settings" msgstr "Egy vagy több BIOS-beállítás beállítása" #. TRANSLATORS: command description msgid "Sets one or more BIOS settings" msgstr "Beállít egy vagy több BIOS-beállítást" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Beállítja a jóváhagyott belsÅ‘ vezérlÅ‘programok listáját" #. TRANSLATORS: type of BIOS setting msgid "Setting type" msgstr "Beállítás típusa" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "A beállítások a rendszer újraindítása után lesznek alkalmazva" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "BelsÅ‘ vezérlÅ‘program elÅ‘zményeinek megosztása a fejlesztÅ‘kkel" #. TRANSLATORS: command line option msgid "Show all results" msgstr "Összes eredmény megjelenítése" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "Ügyfél és démon verzióinak megjelenítése" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "A démon részletes információinak megjelenítése egy adott tartományhoz" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "Hibakeresési információk megjelenítése az összes tartományhoz" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Hibakeresési kapcsolók megjelenítése" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Nem frissíthetÅ‘ eszközök megjelenítése" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "További hibakeresési információk megjelenítése" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "BelsÅ‘vezérlÅ‘program-frissítések elÅ‘zményeinek megjelenítése" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "A dbx számított verziójának megjelenítése" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "Leállítja most?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "BelsÅ‘ vezérlÅ‘program aláírása egy új kulccsal" msgid "Sign data using the client certificate" msgstr "Adatok aláírása az ügyféltanúsítvány használatával" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "Adatok aláírása az ügyféltanúsítvány használatával" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "Feltöltött adatok aláírása az ügyféltanúsítvánnyal" msgid "Signature" msgstr "Aláírás" #. TRANSLATORS: firmware is verified on-device the payload using strong crypto msgid "Signed Payload" msgstr "Aláírt hasznos teher" #. TRANSLATORS: file size of the download msgid "Size" msgstr "Méret" #. TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM msgid "Some of the platform secrets may be invalidated when updating this firmware." msgstr "Egyes platformtitkok érvénytelenné válhatnak ennek a belsÅ‘ vezérlÅ‘programnak a frissítésekor." #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "Forrás" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "A dbx-adatbázisfájl megadása" #. TRANSLATORS: The BIOS setting accepts strings msgid "String" msgstr "Karakterlánc" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "Sikeres" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "Az összes eszköz sikeresen aktiválva" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "A távoli tároló sikeresen letiltva" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "Az új metaadatok sikeresen letöltve: " #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "A távoli tároló sikeresen engedélyezve és frissítve" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "A távoli tároló sikeresen engedélyezve" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "A belsÅ‘ vezérlÅ‘program sikeresen telepítve" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "A konfigurációérték sikeresen módosítva" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "A távoli tároló sikeresen módosítva" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "A metaadatok sikeresen frissítve kézileg" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "Az eszköz-ellenÅ‘rzőösszegek sikeresen frissítve" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "%u jelentés sikeresen feltöltve" msgstr[1] "%u jelentés sikeresen feltöltve" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "Az eszköz-ellenÅ‘rzőösszegek sikeresen ellenÅ‘rizve" #. TRANSLATORS: the device showed up in time #, c-format msgid "Successfully waited %.0fms for device" msgstr "Sikeresen várakozva %.0f ezredmásodpercet az eszközhöz" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Összegzés" #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "Támogatott" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "Támogatott CPU" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "Távoli kiszolgálón támogatott" #. TRANSLATORS: Title: a better sleep state msgid "Suspend To Idle" msgstr "Felfüggesztés üresjáratba" #. TRANSLATORS: Title: sleep state msgid "Suspend To RAM" msgstr "Felfüggesztés a memóriába" #. TRANSLATORS: longer description msgid "Suspend to Idle allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Az üresjáratba való felfüggesztés lehetÅ‘vé teszi az eszköz számára, hogy gyorsan alvásba kerüljön az energia megtakarításához. Amíg az eszköz fel van függesztve, a memóriája fizikailag eltávolítható, és az információk hozzáférhetÅ‘k." #. TRANSLATORS: longer description msgid "Suspend to RAM allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "A memóriába való felfüggesztés lehetÅ‘vé teszi az eszköz számára, hogy gyorsan alvásba kerüljön az energia megtakarításához. Amíg az eszköz fel van függesztve, a memóriája fizikailag eltávolítható, és az információk hozzáférhetÅ‘k." #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "Felfüggesztés üresjáratba" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "Felfüggesztés a memóriába" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "Ãtváltja a(z) %s ágat %s ágra?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "A belsÅ‘vezérlÅ‘program-ág váltása az eszközön" #. TRANSLATORS: this CLI tool is now preventing system updates msgid "System Update Inhibited" msgstr "Rendszerfrissítés meggátolva" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "A rendszer külsÅ‘ energiaforrást igényel" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "SZÖVEG" #. TRANSLATORS: longer description msgid "TPM (Trusted Platform Module) is a computer chip that detects when hardware components have been tampered with." msgstr "A TPM (Trusted Platform Module – megbízható platform modul) egy számítógépes integrált áramkör, amely felismeri, ha a hardverösszetevÅ‘ket manipulálták." #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "TPM PCR0 újjáépítés" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is invalid" msgstr "A TPM PCR0 újjáépítés érvénytelen" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is now valid" msgstr "A TPM PCR0 újjáépítés most már érvényes" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM Platform Configuration" msgstr "TPM platformbeállítás" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM Reconstruction" msgstr "TPM újjáépítés" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM empty PCRs" msgstr "TPM üres PCR-ek" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM 2.0-s verzió" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "Címke" msgstr[1] "Címkék" #. TRANSLATORS: we're saving all USB events for emulation msgid "Tagged for emulation" msgstr "Megjelölve emulációhoz" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "FertÅ‘zött" #. show the user the entire data blob msgid "Target" msgstr "Cél" #. TRANSLATORS: command description msgid "Test a device using a JSON manifest" msgstr "Eszköz tesztelése JSON-nyilvántartás használatával" #. TRANSLATORS: when the release was tested msgid "Tested" msgstr "Tesztelt" #. TRANSLATORS: the %s is a vendor name, e.g. Lenovo #, c-format msgid "Tested by %s" msgstr "%s által tesztelt" #. TRANSLATORS: someone we trust has tested this msgid "Tested by trusted vendor" msgstr "Megbízható gyártó által tesztelt" #. TRANSLATORS: longer description msgid "The Intel Management Engine Key Manifest must be valid so that the device firmware can be trusted by the CPU." msgstr "Az Intel menedzsmentmotor kulcsnyilvántartásának érvényesnek kell lennie ahhoz, hogy az eszköz belsÅ‘ vezérlÅ‘programjában megbízhasson a processzor." #. TRANSLATORS: longer description msgid "The Intel Management Engine controls device components and needs to have a recent version to avoid security issues." msgstr "Az Intel menedzsmentmotor vezérli az eszközösszetevÅ‘ket, és a legújabb verzióval kell rendelkeznie a biztonsági problémák elkerüléséhez." #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "Az LVFS egy ingyenes szolgáltatás, amely független jogi entitásként működik, és nincs kapcsolata a(z) $OS_RELEASE:NAME$ operációs rendszerrel. A disztribúció szállítója nem biztos, hogy ellenÅ‘rizte kompatibilitási szempontból a belsÅ‘vezérlÅ‘program-frissítéseket. Mindent belsÅ‘ vezérlÅ‘programot csak az eredeti termék gyártója biztosít." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Platform Configuration is used to check whether the device start process has been modified." msgstr "A TPM (Trusted Platform Module – megbízható platform modul) platform konfigurációját annak ellenÅ‘rzéséhez használják, hogy az eszköz indulási folyamatát módosították-e." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Reconstruction is used to check whether the device start process has been modified." msgstr "A TPM (Trusted Platform Module – megbízható platform modul) újjáépítést annak ellenÅ‘rzéséhez használják, hogy az eszköz indulási folyamatát módosították-e." #. TRANSLATORS: this is more background on a security measurement problem msgid "The TPM PCR0 differs from reconstruction." msgstr "A TPM PCR0 eltér az újjáépítéstÅ‘l." #. TRANSLATORS: longer description msgid "The UEFI Platform Key is used to determine if device software comes from a trusted source." msgstr "Az UEFI platformkulcsot annak meghatározásához használják, hogy az eszközszoftver megbízható forrásból származik-e." #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "A démon harmadik féltÅ‘l származó kódot töltött be, és azokat a távoli tároló fejlesztÅ‘i többé már nem támogatják!" #. TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name #, c-format msgid "The firmware from %s is not supplied by %s, the hardware vendor." msgstr "A(z) %s gyártótól származó belsÅ‘ vezérlÅ‘programot nem a(z) %s, a hardver gyártója biztosította." #. TRANSLATORS: try to help msgid "The system clock has not been set correctly and downloading files may fail." msgstr "A rendszer órája nem lett helyesen beállítva, és a fájlok letöltése meghiúsulhat." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been re-inserted." msgstr "A frissítés akkor fog folytatódni, ha a készülék USB-kábele újra bedugásra kerül." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged and then re-inserted." msgstr "A frissítés akkor fog folytatódni, ha a készülék USB-kábele kihúzásra, majd újra bedugásra kerül." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged." msgstr "A frissítés akkor fog folytatódni, ha a készülék USB-kábele kihúzásra kerül." #. TRANSLATORS: naughty vendor msgid "The vendor did not supply any release notes." msgstr "A gyártó nem adott ki kiadási megjegyzéseket." #. TRANSLATORS: now list devices with unfixed high-priority issues msgid "There are devices with issues:" msgstr "Problémákkal rendelkezÅ‘ eszközök vannak:" #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "Nincsenek tiltott belsÅ‘vezérlÅ‘program-fájlok." #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "Nincs jóváhagyott belsÅ‘ vezérlÅ‘program." #. TRANSLATORS: %1 is the current device version number, and %2 is the #. command name, e.g. `fwupdmgr sync` #, c-format msgid "This device will be reverted back to %s when the %s command is performed." msgstr "Az eszköz vissza lesz állítva a(z) %s verzióra, ha a(z) %s parancs végrehajtásra kerül." #. TRANSLATORS: the vendor did not upload this msgid "This firmware is provided by LVFS community members and is not provided (or supported) by the original hardware vendor." msgstr "Ezt a belsÅ‘ vezérlÅ‘programot az LVFS közösség tagjai szolgáltatják, és nem az eredeti hardvergyártó biztosítja (vagy támogatja)." #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "Ezt a csomagot nem ellenÅ‘rizték, ezért elÅ‘fordulhat, hogy nem működik megfelelÅ‘en." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "Lehet, hogy ez a program csak rendszergazdaként működik megfelelÅ‘en" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "Ez a távoli tároló olyan belsÅ‘ vezérlÅ‘programot tartalmaz, amelyre nem vonatkozik embargó, de még teszteli a hardver gyártója. Érdemes meggyÅ‘zÅ‘dnie arról, hogy van-e mód a belsÅ‘ vezérlÅ‘program régebbi verziójának kézi telepítésére, ha a belsÅ‘ vezérlÅ‘program frissítése nem sikerül." #. TRANSLATORS: error message msgid "This system doesn't support firmware settings" msgstr "Ez a rendszer nem támogatja a belsÅ‘ vezérlÅ‘program beállításokat" #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "Ez a rendszer HSI futásidejű problémákkal rendelkezik." #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "Ez a rendszer alacsony HSI biztonsági szinttel rendelkezik." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "Ez az eszköz lehetÅ‘vé teszi a rendszergazdáknak az UEFI dbx-frissítések alkalmazását." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "Ez az eszköz lehetÅ‘vé teszi a rendszergazdáknak az fwupd démon lekérdezését és vezérlését, lehetÅ‘vé téve számukra olyan műveletek végrehajtását, mint a belsÅ‘ vezérlÅ‘program telepítése vagy régebbi verziójának telepítése." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "Ez az eszköz lehetÅ‘vé teszi a rendszergazdáknak az fwupd bÅ‘vítményeinek használatát anélkül, hogy telepítve lennének a gazdarendszeren." #. TRANSLATORS: the %1 is a BIOS setting name. #. * %2 and %3 are the values, e.g. "True" or "Windows10" #, c-format msgid "This tool can change the BIOS setting '%s' from '%s' to '%s' automatically, but it will only be active after restarting the computer." msgstr "Ez az eszköz képes automatikusan megváltoztatni a(z) „%s†BIOS-beállítást „%s†értékrÅ‘l „%s†értékre, de csak a számítógép újraindítása után lesz aktív." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "Ez az eszköz beolvassa és feldolgozza a TPM-eseménynaplót a rendszer belsÅ‘ vezérlÅ‘programból." #. TRANSLATORS: the update state of the specific device msgid "Transient failure" msgstr "Ãtmeneti hiba" #. TRANSLATORS: item is TRUE msgid "True" msgstr "Igaz" #. TRANSLATORS: We verified the metadata against the server msgid "Trusted metadata" msgstr "Megbízható metaadatok" #. TRANSLATORS: We verified the payload against the server msgid "Trusted payload" msgstr "Megbízható hasznos teher" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Típus" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI Bootservice Variables" msgstr "UEFI rendszerindítási szolgáltatás változói" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition may not be set up correctly" msgstr "Az UEFI ESP-partíció talán nincs megfelelÅ‘en beállítva" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "Az UEFI ESP-partíció nincs felismerve vagy beállítva" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "UEFI platformkulcs" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "UEFI biztonságos rendszerindítás" #. TRANSLATORS: longer description msgid "UEFI Secure Boot prevents malicious software from being loaded when the device starts." msgstr "Az UEFI biztonságos rendszerindítás megakadályozza, hogy rosszindulatú szoftverek töltÅ‘djenek be az eszköz indulásakor." #. TRANSLATORS: longer description msgid "UEFI boot service variables should not be readable from runtime mode." msgstr "Az UEFI rendszerindítási szolgáltatás változói nem lehetnek olvashatók futásidejű módból." #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI bootservice variables" msgstr "UEFI rendszerindítási szolgáltatás változói" #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "Az UEFI kapszulafrissítések nem érhetÅ‘k el vagy nincsenek engedélyezve a belsÅ‘ vezérlÅ‘program beállításában" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "UEFI dbx segédprogram" #. TRANSLATORS: system is not booted in UEFI mode msgid "UEFI firmware can not be updated in legacy BIOS mode" msgstr "Az UEFI belsÅ‘ vezérlÅ‘program nem frissíthetÅ‘ örökölt BIOS módban" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "UEFI platformkulcs" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "UEFI biztonságos rendszerindítás" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "Nem lehet kapcsolódni a szolgáltatáshoz" #. TRANSLATORS: error message msgid "Unable to find attribute" msgstr "Nem található attribútum" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "Jelenlegi illesztÅ‘program leválasztása" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "BelsÅ‘ vezérlÅ‘program tiltásának feloldása:" #. TRANSLATORS: command description msgid "Unblocks a specific firmware from being installed" msgstr "Megszünteti egy bizonyos belsÅ‘ vezérlÅ‘program telepítésének tiltását" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "Titkosítatlan" #. TRANSLATORS: command description msgid "Uninhibit the system to allow upgrades" msgstr "A rendszer gátlásának megszüntetése a frissítések lehetÅ‘vé tételéhez" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Ismeretlen" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Ismeretlen eszköz" msgid "Unlock the device to allow access" msgstr "Az eszköz feloldása a hozzáférés engedélyezéséhez" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "Feloldott" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "Feloldja az eszközt a belsÅ‘ vezérlÅ‘program eléréséhez" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "Leválasztja az ESP-t" #. TRANSLATORS: firmware payload is unsigned and it is possible to modify it msgid "Unsigned Payload" msgstr "Aláíratlan hasznos teher" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "Nem támogatott démonverzió: %s, az ügyfélverzió: %s" #. TRANSLATORS: Suffix: the HSI result msgid "Untainted" msgstr "Nem fertÅ‘zött" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "FrissíthetÅ‘" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Frissítési hiba" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "Frissítési lemezkép" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "Frissítési üzenet" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "Frissítés állapota" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "A feltöltési hiba ismert probléma, további információkért látogassa meg ezt az URL-t:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Frissíti most?" #. TRANSLATORS: command description msgid "Update the stored cryptographic hash with current ROM contents" msgstr "A tárolt kriptográfiai kivonat frissítése a jelenlegi ROM tartalmával" msgid "Update the stored device verification information" msgstr "A tárolt eszközellenÅ‘rzési információk frissítése" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "A tárolt metaadatok frissítése a jelenlegi tartalommal" #. TRANSLATORS: command description msgid "Updates all specified devices to latest firmware version, or all devices if unspecified" msgstr "Frissíti az összes megadott eszközt a legújabb belsÅ‘vezérlÅ‘program-verzióra, vagy az összes eszközt, ha nincs megadva" msgid "Updating" msgstr "Frissítés" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "%s frissítése…" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "Frissíti a(z) %s eszközön lévÅ‘ verziót: %s → %s?" #. TRANSLATORS: ask the user to share, %s is something #. * like: "Linux Vendor Firmware Service" #, c-format msgid "Upload these anonymous results to the %s to help other users?" msgstr "Feltölti ezeket a névtelen eredményeket a(z) %s szolgáltatásba, hogy segítsen más felhasználóknak?" #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "A belsÅ‘ vezérlÅ‘program jelentéseinek feltöltése segít a hardvergyártóknak, hogy gyorsan azonosítsák a hibás és sikeres frissítéseket valós eszközökön." #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "SürgÅ‘sség" #. TRANSLATORS: explain how to get help, %1 is #. * 'fwupdtool --help' #. TRANSLATORS: explain how to get help, #. * where $1 is something like 'fwupdmgr --help' #, c-format msgid "Use %s for help" msgstr "Használja a(z) „%s†parancsot a súgóért" #. TRANSLATORS: CTRL^C [holding control, and then pressing C] will exit the #. program msgid "Use CTRL^C to cancel." msgstr "Használja a CTRL^C billentyűkombinációt a megszakításhoz." #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "A felhasználó értesítve" #. TRANSLATORS: remote filename base msgid "Username" msgstr "Felhasználónév" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "Érvényes" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "Az ESP-tartalom ellenÅ‘rzése…" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "Változat" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "Gyártó" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "EllenÅ‘rzés…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Verzió" #. TRANSLATORS: the fwupd version the release was tested on msgid "Version[fwupd]" msgstr "Verzió[fwupd]" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "FIGYELMEZTETÉS" #. TRANSLATORS: command description msgid "Wait for a device to appear" msgstr "Várakozás egy eszköz megjelenésére" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "Várakozás…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "Hardverváltozások figyelése" #. TRANSLATORS: check various UEFI and ACPI tables are unchanged after the #. update msgid "Will measure elements of system integrity around an update" msgstr "Mérni fogja a rendszer integritásának elemeit egy frissítés körül" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "Fájl írása:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Ãrás…" #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from the system firmware setup, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "GyÅ‘zÅ‘djön meg arról, hogy kényelmesen vissza tudja állítani a beállításokat a rendszer belsÅ‘ vezérlÅ‘programjának beállításaiból, mivel ez a változtatás azt eredményezheti, hogy a rendszer nem tudja elindítani a Linuxot, vagy más módon teheti instabillá a rendszert." #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "A disztribúció szállítója nem biztos, hogy ellenÅ‘rizte kompatibilitási szempontból a belsÅ‘vezérlÅ‘program-frissítéseket a rendszerével és a kapcsolódó eszközeivel." #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "A hardver megsérülhet ennek a belsÅ‘ vezérlÅ‘programnak a használatával, és a kiadás telepítése érvénytelenítheti a(z) %s által vállalt garanciát." #. TRANSLATORS: BKC is the industry name for the best known configuration and #. is a set #. * of firmware that works together #, c-format msgid "Your system is set up to the BKC of %s." msgstr "A rendszere a(z) %s BKC-re van beállítva." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[ELLENÅRZÅÖSSZEG]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[ESZKÖZAZONOSÃTÓ|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[ESZKÖZAZONOSÃTÓ|GUID] [ÃG]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [VERSION]" msgstr "[ESZKÖZAZONOSÃTÓ|GUID] [VERZIÓ]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILE FILE_SIG REMOTE-ID]" msgstr "[FÃJL FÃJL-ALÃÃRÃS TÃVOLITÃROLÓ-AZONOSÃTÓ]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[FÃJLNÉV1] [FÃJLNÉV2]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2] [--no-authenticate]" msgstr "[BEÃLLÃTÃS1] [BEÃLLÃTÃS2] [--no-authenticate]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[SMBIOS-FÃJL|HWIDS-FÃJL]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "alapértelmezett" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "fwupd TPM-eseménynapló segédprogram" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "fwupd bÅ‘vítmények" fwupd-2.0.10/po/id.po000066400000000000000000003470011501337203100142710ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Andika Triwidada , 2017-2019,2021,2024 # Andika Triwidada , 2021 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Indonesian (http://app.transifex.com/freedesktop/fwupd/language/id/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: id\n" "Plural-Forms: nplurals=1; plural=0;\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "%.0f menit tersisa" #. TRANSLATORS: BMC refers to baseboard management controller which #. * is the device that updates all the other firmware on the system #, c-format msgid "%s BMC Update" msgstr "Pemutakhiran MBC %s" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "Pembaruan Baterai %s" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "Pembaruan Microcode CPU %s" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "Pembaruan Kamera %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "Pembaruan Konfigurasi %s" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "Pembaruan ME Konsumen %s" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "Pembaruan Pengontrol %s" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "Pembaruan ME Korporat %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "Pembaruan Perangkat %s" #. TRANSLATORS: Video Display refers to the laptop internal display or #. * external monitor #, c-format msgid "%s Display Update" msgstr "Pemutakhiran Tampilan %s" #. TRANSLATORS: Dock refers to the port replicator hardware laptops are #. * cradled in, or lowered onto #, c-format msgid "%s Dock Update" msgstr "Pemutakhiran Dok %s" #. TRANSLATORS: drive refers to a storage device, e.g. SATA disk #, c-format msgid "%s Drive Update" msgstr "Pemutakhiran Drive %s" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "Pembaruan Pengontrol Tertanam %s" #. TRANSLATORS: a device that can read your fingerprint pattern #, c-format msgid "%s Fingerprint Reader Update" msgstr "Pemutakhiran Pembaca Sidik Jari %s" #. TRANSLATORS: flash refers to solid state storage, e.g. UFS or eMMC #, c-format msgid "%s Flash Drive Update" msgstr "Pemutakhiran Flash Disk %s" #. TRANSLATORS: GPU refers to a Graphics Processing Unit, e.g. #. * the "video card" #, c-format msgid "%s GPU Update" msgstr "Pemutakhiran GPU %s" #. TRANSLATORS: a large pressure-sensitive drawing area typically used #. * by artists and digital artists #, c-format msgid "%s Graphics Tablet Update" msgstr "Pemutakhiran Tablet Grafis %s" #. TRANSLATORS: an input device used by gamers, e.g. a joystick #, c-format msgid "%s Input Controller Update" msgstr "Pembaruan Pengendali Masukan %s" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "Pembaruan Papan Ketik %s" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "Pembaruan ME %s" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "Pembaruan Tetikus %s" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "Pembaruan Antar Muka Jaringan %s" #. TRANSLATORS: SSD refers to a Solid State Drive, e.g. non-rotating #. * SATA or NVMe disk #, c-format msgid "%s SSD Update" msgstr "Pemutakhiran SSD %s" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "Pembaruan Pengontrol Penyimpanan %s" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "Pembaruan Sistem %s" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "Pembaruan TPM %s" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "Pembaruan Pengontrol Thunderbolt %s" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "Pembaruan Touchpad %s" #. TRANSLATORS: Dock refers to the port replicator device connected #. * by plugging in a USB cable -- which may or may not also provide power #, c-format msgid "%s USB Dock Update" msgstr "Pemutakhiran Dok USB %s" #. TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth #. * device that stays in the USB port so the wireless peripheral works #, c-format msgid "%s USB Receiver Update" msgstr "Pemutakhiran Penerima USB %s" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "Pembaruan %s" #. TRANSLATORS: warn the user before updating, %1 is a device name #, c-format msgid "%s and all connected devices may not be usable while updating." msgstr "%s dan semua perangkat yang terhubung mungkin tidak dapat digunakan saat memperbarui." #. TRANSLATORS: %1 refers to some kind of security test, e.g. "Encrypted RAM". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s appeared: %s" msgstr "%s muncul: %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "UEFI platform #. key". #. * %2 and %3 refer to results value, e.g. "Valid" and "Invalid" #, c-format msgid "%s changed: %s → %s" msgstr "%s berubah: %s → %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "SPI BIOS #. region". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s disappeared: %s" msgstr "%s menghilang: %s" #. TRANSLATORS: the device has a reason it can't update, e.g. laptop lid #. closed #, c-format msgid "%s is not currently updatable" msgstr "%s saat ini tidak dapat diperbarui" #. TRANSLATORS: %1 is a device name, e.g. "ThinkPad Universal #. * ThunderBolt 4 Dock" and %2 is "fwupdmgr activate" #, c-format msgid "%s is pending activation; use %s to complete the update." msgstr "%s sedang menunggu aktivasi; gunakan %s untuk menyelesaikan pembaruan." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "%s mode manufaktur" #. TRANSLATORS: warn the user before #. * updating, %1 is a device name #, c-format msgid "%s must remain connected for the duration of the update to avoid damage." msgstr "%s harus tetap terhubung selama pembaruan untuk menghindari kerusakan." #. TRANSLATORS: warn the user before updating, %1 is a machine #. * name #, c-format msgid "%s must remain plugged into a power source for the duration of the update to avoid damage." msgstr "%s harus tetap terhubung ke sumber tenaga selama pembaruan untuk menghindari kerusakan." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "%s menimpa" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "versi %s" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u hari" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "%u perangkat memiliki pembaruan firmware." #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device is not the best known configuration." msgid_plural "%u devices are not the best known configuration." msgstr[0] "%u perangkat bukanlah konfigurasi yang paling dikenal." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u jam" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u menit" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u detik" #. TRANSLATORS: first percentage is current value, 2nd percentage is the #. * lowest limit the firmware update is allowed for the update to happen #, c-format msgid "%u%% (threshold %u%%)" msgstr "%u%% (ambang batas %u%%)" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(usang)" #. TRANSLATORS: HSI event title msgid "A TPM PCR is now an invalid value" msgstr "Suatu PCR TPM sekarang adalah nilai yang tidak valid" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "AMD Firmware Replay Protection" msgstr "Perlindungan Replay Firmware AMD" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "AMD Firmware Write Protection" msgstr "Perlindungan Tulis Firmware AMD" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "AMD Secure Processor Rollback Protection" msgstr "Perlindungan Rollback Prosesor Aman AMD" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "ARCHIVE FIRMWARE METAINFO [FIRMWARE] [METAINFO] [JCATFILE]" msgstr "ARSIP FIRMWARE METAINFO [FIRMWARE] [METAINFO] [BERKAS­_JCAT]" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "Tindakan Diperlukan:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "Mengaktifkan perangkat" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "Aktifkan perangkat yang tertunda" msgid "Activate the new firmware on the device" msgstr "Aktifkan firmware baru pada perangkat" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "Mengaktifkan pemutakhiran firmware" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "Mengaktifkan pembaruan firmware untuk" #. TRANSLATORS: command description msgid "Adds devices to watch for future emulation" msgstr "Menambahkan perangkat yang akan diawasi untuk emulasi di masa mendatang" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Usia" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "Setuju dan aktifkan remote?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Alias ke %s" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are now valid" msgstr "Semua TPM PCR sekarang valid" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are valid" msgstr "Semua PCR TPM valid" #. TRANSLATORS: an application is preventing system updates msgid "All devices are prevented from update by system inhibit" msgstr "Semua perangkat dicegah dimutakhirkan oleh larangan sistem" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "Semua perangkat dengan tipe yang sama akan diperbarui pada saat yang sama" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Izinkan penuruntingkatan versi firmware" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "Izinkan menginstal ulang versi firmware yang ada" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "Perbolehkan beralih cabang firmware" #. TRANSLATORS: is not the main firmware stream msgid "Alternate branch" msgstr "Branch alternatif" #. TRANSLATORS: another application is updating the device already msgid "An update is in progress" msgstr "Pembaruan sedang berlangsung" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "Suatu pembaruan memerlukan boot ulang agar lengkap." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "Pembaruan membutuhkan sistem untuk dimatikan untuk menyelesaikan." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Menjawab ya untuk semua pertanyaan" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Terapkan pembaruan bahkan ketika tidak disarankan" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Menerapkan berkas pembaruan" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "Menerapkan pembaruan…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Firmware yang disetujui:" #. TRANSLATORS: command description msgid "Asks the daemon to quit" msgstr "Meminta daemon untuk berhenti" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "Cantolkan ke mode firmware" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Mengautentikasi…" #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "Detail otentikasi diperlukan" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Autentikasi diperlukan untuk menuruntingkatkan firmware pada perangkat lepas pasang" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Autentikasi diperlukan untuk menuruntingkatkan firmware pada mesin ini" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to enable emulation data collection" msgstr "Autentikasi diperlukan untuk memfungsikan pengumpulan data emulasi" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to fix a host security issue" msgstr "Autentikasi diperlukan untuk memperbaiki masalah keamanan host" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to load hardware emulation data" msgstr "Autentikasi diperlukan untuk memuat data emulasi perangkat keras" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "Autentikasi diperlukan untuk mengubah pengaturan BIOS" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "Autentikasi diperlukan untuk mengubah sebuah remote yang ditata yang dipakai untuk pembaruan firmware" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "Autentikasi diperlukan untuk mengubah konfigurasi daemon" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "Autentikasi diperlukan untuk membaca pengaturan BIOS" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to reset daemon configuration to defaults" msgstr "Autentikasi diperlukan untuk mengatur ulang konfigurasi daemon ke baku" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to save hardware emulation data" msgstr "Autentikasi diperlukan untuk menyimpan data emulasi perangkat keras" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "Autentikasi diperlukan untuk mengatur daftar firmware yang disetujui" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "Autentikasi diperlukan untuk menandatangani data menggunakan sertifikat klien" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to stop the firmware update service" msgstr "Autentikasi diperlukan untuk menghentikan layanan pembaruan firmware" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "Autentikasi diperlukan untuk beralih ke versi firmware baru" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to undo the fix for a host security issue" msgstr "Autentikasi diperlukan untuk membatalkan perbaikan masalah keamanan host" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Autentikasi diperlukan untuk membuka kunci suatu perangkat" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Autentikasi diperlukan untuk memutakhirkan firmware pada perangkat lepas pasang" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Autentikasi diperlukan untuk memutakhirkan firmware pada mesin ini" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "Autentikasi diperlukan untuk memutakhirkan checksum tersimpan bagi perangkat" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "Pelaporan Otomatis" #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "Unggah secara otomatis setiap kali?" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS Firmware Updates" msgstr "Pemutakhiran Firmware BIOS" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS Rollback Protection" msgstr "Perlindungan Rollback BIOS" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS firmware updates" msgstr "Pemutakhiran firmware BIOS" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS rollback protection" msgstr "Perlindungan rollback BIOS" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "Pembaruan BIOS dikirimkan melalui LVFS atau Pembaruan Windows" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "BUILDER-XML FILENAME-DST" msgstr "BUILDER-XML NAMABERKAS-DST" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "Baterai" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "Mengikat driver kernel baru" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "Berkas firmware yang diblokir:" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "Versi yang diblokir" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "Memblokir firmware:" #. TRANSLATORS: command description msgid "Blocks a specific firmware from being installed" msgstr "Memblokir firmware tertentu agar tidak diinstal" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "Versi Bootloader" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "Cabang" #. TRANSLATORS: command description msgid "Build a cabinet archive from a firmware blob and XML metadata" msgstr "Membuat arsip kabinet dari blob firmware dan metadata XML" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "Build berkas firmware" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * Utilized by OS means the distribution enabled it msgid "CET OS Support" msgstr "Dukungan OS CET" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * enabled means supported by the processor msgid "CET Platform" msgstr "Platform CET" #. TRANSLATORS: longer description msgid "CPU Microcode must be updated to mitigate against various information-disclosure security issues." msgstr "Microcode CPU harus diperbarui untuk mengurangi berbagai masalah-masalah keamanan pengungkapan informasi." #. TRANSLATORS: we can save all device enumeration events for emulation msgid "Can tag for emulation" msgstr "Dapat menandai untuk emulasi" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Batal" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "Dibatalkan" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "Tidak dapat diterapkan karena pembaruan dbx telah diterapkan." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Diubah" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "Memeriksa apakah hash kriptografis cocok dengan firmware" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Checksum" #. TRANSLATORS: get interactive prompt, where branch is the #. * supplier of the firmware, e.g. "non-free" or "free" msgid "Choose branch" msgstr "Pilih branch" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "Pilih perangkat" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "Pilih firmware" #. TRANSLATORS: get interactive prompt msgid "Choose release" msgstr "Pilih rilis" #. TRANSLATORS: get interactive prompt msgid "Choose volume" msgstr "Pilih volume" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "Membersihkan hasil dari pemutakhiran terakhir" #. TRANSLATORS: error message msgid "Command not found" msgstr "Perintah tidak ditemukan" #. TRANSLATORS: is not supported by the vendor msgid "Community supported" msgstr "Didukung komunitas" #. TRANSLATORS: title prefix for the BIOS settings dialog msgid "Configuration Change Suggested" msgstr "Perubahan Konfigurasi Disarankan" #. TRANSLATORS: no peeking msgid "Configuration is only readable by the system administrator" msgstr "Konfigurasi hanya dapat dibaca oleh administrator sistem" #. TRANSLATORS: longer description msgid "Control-Flow Enforcement Technology detects and prevents certain methods for running malicious software on the device." msgstr "Teknologi Control-Flow Enforcement mendeteksi dan mencegah metode-metode tertentu untuk menjalankan perangkat lunak jahat pada perangkat." #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology msgid "Control-flow Enforcement Technology" msgstr "Control-flow Enforcement Technology" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "Mengonversi berkas firmware" #. TRANSLATORS: when the update was built msgid "Created" msgstr "Dibuat" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "Kritis" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "Verifikasi hash kriptografis tersedia" #. TRANSLATORS: current value of a BIOS setting msgid "Current Value" msgstr "Nilai Sekarang" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "Versi saat ini" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "ID-PERANTI|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Opsi Pengawakutuan" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "Mendekompresi…" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Deskripsi" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "Lepaskan ke mode bootloader" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "Rincian" #. TRANSLATORS: the best known configuration is a set of software that we know #. works #. * well together. In the OEM and ODM industries it is often called a BKC msgid "Deviate from the best known configuration?" msgstr "Menyimpang dari konfigurasi yang paling dikenal?" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "Flag Perangkat" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "ID Perangkat" #. TRANSLATORS: description of the device requests msgid "Device Requests" msgstr "Permintaan Perangkat" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Perangkat ditambahkan:" #. TRANSLATORS: the device is already connected msgid "Device already exists" msgstr "Perangkat sudah ada" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "Daya baterai perangkat terlalu lemah" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse #, c-format msgid "Device battery power is too low (%u%%, requires %u%%)" msgstr "Daya baterai perangkat terlalu lemah (%u%%, membutuhkan %u%%)" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "Perangkat dapat memulihkan kegagalan flash" #. TRANSLATORS: lid means "laptop top cover" msgid "Device cannot be updated while the lid is closed" msgstr "Perangkat tidak dapat diperbarui saat lid-nya tertutup" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Perangkat diubah:" #. TRANSLATORS: a version check is required for all firmware msgid "Device firmware is required to have a version check" msgstr "Firmware perangkat harus memiliki pemeriksaan versi" #. TRANSLATORS: emulated means we are pretending to be a different model msgid "Device is emulated" msgstr "Perangkat diemulasi" #. TRANSLATORS: device cannot be interrupted, for instance taking a phone call msgid "Device is in use" msgstr "Perangkat sedang dipakai" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "Perangkat terkunci" #. TRANSLATORS: we have two ways of communicating with the device, so we hide #. one msgid "Device is lower priority than an equivalent device" msgstr "Perangkat memiliki prioritas lebih rendah daripada perangkat yang setara" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "Perangkat perlu menginstal semua rilis yang disediakan" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "Perangkat tidak terjangkau" #. TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode msgid "Device is unreachable, or out of wireless range" msgstr "Perangkat tidak dapat dijangkau, atau di luar jangkauan nirkabel" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "Perangkat dapat digunakan selama durasi pembaruan" #. TRANSLATORS: usually this is when we're waiting for a reboot msgid "Device is waiting for the update to be applied" msgstr "Perangkat sedang menunggu pembaruan diterapkan" #. TRANSLATORS: success, so say thank you to the user msgid "Device list uploaded successfully, thanks!" msgstr "Daftar perangkat berhasil diunggah, terima kasih!" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Perangkat dilepas:" #. TRANSLATORS: as in, wired mains power for a laptop msgid "Device requires AC power to be connected" msgstr "Perangkat membutuhkan daya AC tersambung" #. TRANSLATORS: device does not have a display connected msgid "Device requires a display to be plugged in" msgstr "Perangkat memerlukan tertancapnya suatu tampilan" #. TRANSLATORS: The device cannot be updated due to missing vendor's license." msgid "Device requires a software license to update" msgstr "Perangkat memerlukan lisensi perangkat lunak untuk memperbarui" #. TRANSLATORS: longer description msgid "Device software updates are provided for this device." msgstr "Pembaruan perangkat lunak perangkat disediakan untuk perangkat ini." #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "Pembaruan tahap perangkat" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "Perangkat mendukung beralih ke cabang firmware yang berbeda" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "Pembaruan perangkat membutuhkan aktivasi" #. TRANSLATORS: save the old firmware to disk before installing the new one msgid "Device will backup firmware before installing" msgstr "Perangkat akan mencadangkan firmware sebelum menginstal" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "Perangkat tidak akan muncul kembali setelah pembaruan selesai" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "Peranti yang sukses diperbarui:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "Peranti yang tidak diperbarui dengan benar:" #. TRANSLATORS: message letting the user there is an update #. * waiting, but there is a reason it cannot be deployed msgid "Devices with firmware updates that need user action: " msgstr "Perangkat dengan pembaruan firmware yang memerlukan tindakan pengguna: " #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "Perangkat tanpa pembaruan firmware yang tersedia: " #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "Perangkat dengan versi firmware terbaru yang tersedia:" #. TRANSLATORS: this is for the device tests msgid "Did not find any devices with matching GUIDs" msgstr "Tidak menemukan perangkat dengan GUID yang cocok" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "Dinonaktifkan" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "Menonaktifkan remote yang diberikan" #. TRANSLATORS: command description msgid "Disables virtual testing devices" msgstr "Menonaktifkan perangkat pengujian virtual" #. TRANSLATORS: the OS the release was tested on msgid "Distribution" msgstr "Distribusi" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "Jangan periksa untuk metadata lama" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "Jangan periksa untuk riwayat yang tak dilaporkan" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "Jangan periksa apakah remote unduhan harus diaktifkan" #. TRANSLATORS: command line option msgid "Do not check or prompt for reboot after update" msgstr "Jangan memeriksa atau meminta reboot setelah pembaruan" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "Jangan sertakan awalan domain log" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "Jangan sertakan awalan stempel waktu" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "Jangan melakukan pemeriksaan keamanan perangkat" #. TRANSLATORS: command line option msgid "Do not prompt for devices" msgstr "Jangan bertanya untuk perangkat" #. TRANSLATORS: command line option msgid "Do not prompt to fix security issues" msgstr "Jangan bertanya untuk memperbaiki masalah keamanan" #. TRANSLATORS: command line option msgid "Do not search the firmware when parsing" msgstr "Jangan mencari firmware saat mengurai" #. TRANSLATORS: warning message shown after update has been scheduled msgid "Do not turn off your computer or remove the AC adaptor while the update is in progress." msgstr "Jangan matikan komputer Anda atau lepaskan adaptor AC saat pembaruan sedang berlangsung." #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "Jangan menulis ke basis data riwayat" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "Apakah Anda memahami konsekuensi dari mengubah cabang firmware?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "Apakah Anda ingin menonaktifkan fitur ini untuk pembaruan di masa mendatang?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "Apakah Anda ingin menyegarkan remote ini sekarang?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "Apakah Anda ingin mengunggah laporan secara otomatis untuk pembaruan di masa mendatang?" #. TRANSLATORS: command line option msgid "Don't prompt for authentication (less details may be shown)" msgstr "Jangan meminta autentikasi (detail yang ditampilkan mungkin lebih sedikit)" #. TRANSLATORS: success msgid "Done!" msgstr "Selesai!" #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "Menuruntingkatkan %s dari %s ke %s?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "Menuruntingkatkan firmware pada suatu peranti" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "Menuruntingkatkan %s…" #. TRANSLATORS: command description msgid "Download a file" msgstr "Unduh suatu berkas" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Mengunduh…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "Curahkan data SMBIOS dari suatu berkas" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "Durasi" #. TRANSLATORS: longer description msgid "Each system should have tests to ensure firmware security." msgstr "Setiap sistem harus memiliki tes untuk memastikan keamanan firmware." #. TRANSLATORS: command description msgid "Emulate a device using a JSON manifest" msgstr "Mengemulasi perangkat menggunakan manifes JSON" #. TRANSLATORS: this device is not actually real msgid "Emulated" msgstr "Diemulasi" #. TRANSLATORS: Title: if we are emulating a different host msgid "Emulated host" msgstr "Host yang diemulasi" msgid "Enable" msgstr "Fungsikan" msgid "Enable emulation data collection" msgstr "Memfungsikan pengumpulan data emulasi" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "Aktifkan remote baru?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "Aktifkan remote ini?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Difungsikan" #. TRANSLATORS: Plugin is active only if hardware is found msgid "Enabled if hardware matches" msgstr "Diaktifkan jika perangkat keras cocok" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "Mengaktifkan remote yang diberikan" #. TRANSLATORS: command description msgid "Enables virtual testing devices" msgstr "Memfungsikan perangkat pengujian virtual" #. TRANSLATORS: longer description msgid "Enabling firmware updates for the BIOS allows fixing security issues." msgstr "Memfungsikan pembaruan firmware untuk BIOS memungkinkan memperbaiki masalah keamanan." msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "Mengaktifkan fungsi ini dilakukan dengan risiko Anda sendiri, yang berarti Anda harus menghubungi produsen peralatan asli Anda mengenai masalah yang disebabkan oleh pembaruan ini. Hanya masalah dengan proses pembaruan itu sendiri yang harus diajukan pada $OS_RELEASE:BUG_REPORT_URL$." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "Mengaktifkan remote ini dilakukan dengan risiko Anda sendiri." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Terenkripsi" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "RAM terenkripsi" msgid "Encrypted RAM makes it impossible for information that is stored in device memory to be read if the memory chip is removed and accessed." msgstr "RAM terenkripsi membuat informasi yang disimpan dalam memori perangkat tidak mungkin dibaca jika chip memori dilepas dan diakses." #. TRANSLATORS: the vendor is no longer supporting the device msgid "End of life" msgstr "Akhir hidup" #. TRANSLATORS: The BIOS setting can only be changed to fixed values msgid "Enumeration" msgstr "Enumerasi" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "Menghapus semua riwayat pembaruan firmware" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Menghapus…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "Keluar setelah tundaan sejenak" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "Keluar setelah mesin telah dimuat" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "Mengekspor struktur berkas firmware ke XML" #. TRANSLATORS: command description msgid "Export firmware history for manual upload" msgstr "Ekspor riwayat firmware untuk unggah manual" #. TRANSLATORS: command description msgid "Extract a firmware blob to images" msgstr "Mengekstrak blob firmware ke image" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "BERKAS" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "BERKAS [ID-PERANTI|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "NAMABERKAS" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "NAMABERKAS SERTIFIKAT KUNCI-PRIVAT" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "NAMABERKAS ID-PERANTI" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME OFFSET DATA [FIRMWARE-TYPE]" msgstr "DATA OFSET NAMA-BERKAS [TIPE-FIRMWARE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "NAMABERKAS [ID-PERANTI|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "NAMABERKAS [TIPE-FIRMWARE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" msgstr "NAMABERKAS-SRC NAMABERKAS-DST [TIPE-FIRMWARE-SRC] [TIPE-FIRMWARE-DST]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" msgstr "NAMABERKAS|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "Gagal" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Gagal menerapkan pembaruan" #. TRANSLATORS: error message for Windows msgid "Failed to connect to Windows service, please ensure it's running." msgstr "Gagal terhubung ke layanan Windows, pastikan itu berjalan." #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "Gagal menyambung ke daemon" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "Gagal memuat dbx lokal" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "Gagal memuat dbx sistem" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "Gagal mengunci" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Gagal mengurai argumen" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "Gagal mengurai berkas" #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #, c-format msgid "Failed to parse flags for %s" msgstr "Gagal mengurai flag untuk %s" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "Gagal mengurai dbx lokal" #. TRANSLATORS: a feature is something like "can show an image" msgid "Failed to set front-end features" msgstr "Gagal mengatur fitur front-end" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "Gagal memvalidasi konten ESP" #. TRANSLATORS: item is FALSE msgid "False" msgstr "Salah" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Nama Berkas" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "Tanda Tangan Nama Berkas" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "Sumber Nama Berkas" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Nama berkas diperlukan" #. TRANSLATORS: command line option msgid "Filter with a set of device flags using a ~ prefix to exclude, e.g. 'internal,~needs-reboot'" msgstr "Memfilter dengan suatu set flag perangkat menggunakan awalan ~ untuk mengecualikan, mis. 'internal, ~needs-reboot'" #. TRANSLATORS: command line option msgid "Filter with a set of release flags using a ~ prefix to exclude, e.g. 'trusted-release,~trusted-metadata'" msgstr "Filter dengan set flag rilis menggunakan awalan ~ untuk mengecualikan, misalnya 'trusted-release,~trusted-metadata'" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware Attestation" msgstr "Atestasi Firmware" #. TRANSLATORS: longer description msgid "Firmware Attestation checks device software using a reference copy, to make sure that it has not been changed." msgstr "Atestas Firmware memeriksa perangkat lunak perangkat menggunakan salinan referensi, untuk memastikan bahwa perangkat lunak tersebut tidak diubah." #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "Deskriptor BIOS Firmware" #. TRANSLATORS: longer description msgid "Firmware BIOS Descriptor protects device firmware memory from being tampered with." msgstr "Deskriptor BIOS Firmware melindungi memori firmware perangkat agar tidak dirusak." #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "Wilayah BIOS Firmware" #. TRANSLATORS: longer description msgid "Firmware BIOS Region protects device firmware memory from being tampered with." msgstr "Wilayah BIOS Firmware melindungi memori firmware perangkat agar tidak dirusak." #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "URI Basis Firmware" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "Layanan D-Bus Pemutakhiran Firmware" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Daemon Pemutakhiran Firmware" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "Verifikasi Pemutakhir Firmware" msgid "Firmware Updater Verification checks that software used for updating has not been tampered with." msgstr "Verifikasi Pembaruan Firmware memeriksa bahwa perangkat lunak yang digunakan untuk memperbarui belum dirusak." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "Pemutakhiran Firmware" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "Utilitas Firmware" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "Perlindungan Tulis Firmware" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "Kunci Perlindungan Tulis Firmware" #. TRANSLATORS: longer description msgid "Firmware Write Protection protects device firmware memory from being tampered with." msgstr "Perlindungan Tulis Firmware melindungi memori firmware perangkat agar tidak dirusak." #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "Pengesahan firmware" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "Firmware sudah diblokir" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "Firmware belum diblokir" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "Metadata firmware belum diperbarui selama %uhari dan mungkin tidak mutakhir." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "Pemutakhiran firmware" #. TRANSLATORS: user needs to run a command, %1 is 'fwupdmgr unlock' #, c-format msgid "Firmware updates disabled; run '%s' to enable" msgstr "Pembaruan firmware dinonaktifkan; jalankan '%s' untuk memfungsikan" #. TRANSLATORS: command description msgid "Fix a specific host security attribute" msgstr "Memperbaiki atribut keamanan host tertentu" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fix reverted successfully" msgstr "Perbaikan sukses dibatalkan" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fixed successfully" msgstr "Berhasil diperbaiki" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "Flag" #. TRANSLATORS: command line option msgid "Force the action by relaxing some runtime checks" msgstr "Paksa tindakan dengan merelaksasi beberapa pemeriksaan runtime" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Ditemukan" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "Enkripsi Disk Lengkap Terdeteksi" #. TRANSLATORS: we might ask the user the recovery key when next booting #. Windows msgid "Full disk encryption secrets may be invalidated when updating" msgstr "Rahasia enkripsi disk lengkap mungkin jadi tidak valid saat memperbarui" #. TRANSLATORS: Title: if the part has been fused msgid "Fused Platform" msgstr "Platform Fused" #. TRANSLATORS: Title: if the part has been fused msgid "Fused platform" msgstr "Platform yang memakai fuse" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "GUID|DEVICE-ID" msgstr "GUID|DEVICE-ID" msgid "Get BIOS settings" msgstr "Dapatkan pengaturan BIOS" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "Dapatkan semua flag perangkat yang didukung oleh fwupd" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Dapatkan semua perangkat yang mendukung pemutakhiran firmware" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "Dapatkan semua plugin yang diaktifkan yang terdaftar dengan sistem" #. TRANSLATORS: command description msgid "Get device report metadata" msgstr "Mendapatkan metadata laporan perangkat" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Dapatkan rincian tentang suatu berkas firmware" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "Dapatkan remote-remote yang terkonfigurasi" #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "Mendapatkan atribut keamanan host" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "Dapatkan daftar firmware yang disetujui" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "Mendapatkan daftar firmware yang diblokir" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Dapatkan daftar pemutakhiran bagi perangkat keras yang tersambung" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "Mendapatkan rilis-rilis bagi sebuah peranti" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Mendapatkan hasil dari pemutakhiran terakhir" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "BERKAS-HWIDS" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "Perangkat keras sedang menunggu untuk ditancapkan kembali" #. TRANSLATORS: the release urgency msgid "High" msgstr "Tinggi" #. TRANSLATORS: title for host security events msgid "Host Security Events" msgstr "Kejadian Keamanan Host" #. TRANSLATORS: error message for unsupported feature #, c-format msgid "Host Security ID (HSI) is not supported" msgstr "Host Security ID (HSI) tidak didukung" #. TRANSLATORS: success, so say thank you to the user msgid "Host Security ID attributes uploaded successfully, thanks!" msgstr "Atribut ID Keamanan Host berhasil diunggah, terima kasih!" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "ID Keamanan Host:" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INHIBIT-ID" msgstr "INHIBIT-ID" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "Perlindungan IOMMU" #. TRANSLATORS: longer description msgid "IOMMU Protection prevents connected devices from accessing unauthorized parts of system memory." msgstr "Perlindungan IOMMU mencegah perangkat yang terhubung mengakses bagian memori sistem yang tidak diotorisasi." #. TRANSLATORS: HSI event title msgid "IOMMU device protection disabled" msgstr "Proteksi perangkat IOMMU dinonaktifkan" #. TRANSLATORS: HSI event title msgid "IOMMU device protection enabled" msgstr "Proteksi perangkat IOMMU diaktifkan" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "Menganggur…" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "Abaikan pemeriksaan ketat SSL saat mengunduh berkas" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "Abaikan kegagalan checksum firmware" #. TRANSLATORS: command line option msgid "Ignore firmware hardware mismatch failures" msgstr "Abaikan kegagalan ketidakcocokan perangkat keras firmware" #. TRANSLATORS: command line option msgid "Ignore non-critical firmware requirements" msgstr "Abaikan persyaratan firmware yang tidak kritis" #. TRANSLATORS: try to help msgid "Ignoring SSL strict checks, to do this automatically in the future export DISABLE_SSL_STRICT in your environment" msgstr "Mengabaikan pemeriksaan ketat SSL, untuk melakukan ini secara otomatis di masa depan, eksporlah DISABLE_SSL_STRICT di lingkungan Anda" #. TRANSLATORS: show the user a generic image that can be themed msgid "Image" msgstr "Citra" #. TRANSLATORS: show the user a random image from the internet msgid "Image (custom)" msgstr "Citra (ubahan)" #. TRANSLATORS: the inhibit ID is a short string like dbus-123456 #, c-format msgid "Inhibit ID is %s." msgstr "ID Inhibit adalah %s." #. TRANSLATORS: command description msgid "Inhibit the system to prevent upgrades" msgstr "Melarang sistem untuk mencegah peningkatan" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "Durasi Instalasi" #. TRANSLATORS: command description msgid "Install a firmware file in cabinet format on this hardware" msgstr "Pasang berkas firmware dalam format kabinet pada perangkat keras ini" #. TRANSLATORS: command description msgid "Install a raw firmware blob on a device" msgstr "Pasang suatu blob firmware mentah pada perangkat" #. TRANSLATORS: command description msgid "Install a specific firmware file on all devices that match" msgstr "Pasang berkas firmware tertentu di semua perangkat yang cocok" #. TRANSLATORS: command description msgid "Install a specific firmware on a device, all possible devices will also be installed once the CAB matches" msgstr "Pasang firmware tertentu pada perangkat, semua perangkat yang mungkin juga akan dipasang setelah CAB cocok" msgid "Install old version of signed system firmware" msgstr "Menginstal versi lama firmware sistem yang ditandatangani" msgid "Install old version of unsigned system firmware" msgstr "Menginstal versi lama firmware sistem yang tidak ditandatangani" msgid "Install signed device firmware" msgstr "Pasang firmware perangkat yang ditandatangani" msgid "Install signed system firmware" msgstr "Pasang firmware sistem yang ditandatangani" msgid "Install unsigned device firmware" msgstr "Pasang firmware perangkat yang tak ditandatangani" msgid "Install unsigned system firmware" msgstr "Pasang firmware sistem yang tak ditandatangani" #. TRANSLATORS: stay on one firmware version unless the new version is #. explicitly #. * specified msgid "Installing a specific release is explicitly required" msgstr "Memasang suatu rilis spesifik diperlukan secara eksplisit" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "Sedang memasang pembaruan firmware…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "Memasang pada %s…" #. TRANSLATORS: if it breaks, you get to keep both pieces msgid "Installing this update may also void any device warranty." msgstr "Memasang pembaruan ini juga dapat membatalkan garansi perangkat apa pun." #. TRANSLATORS: The BIOS setting only accepts integers in a fixed range msgid "Integer" msgstr "Integer" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM Protected" msgstr "Intel BootGuard Terproteksi ACM" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "Intel BootGuard terproteksi ACM" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard Error Policy" msgstr "Kebijakan Kesalahan Intel BootGuard" msgid "Intel BootGuard Error Policy ensures the device does not continue to start if its device software has been tampered with." msgstr "Kebijakan Kesalahan Intel BootGuard memastikan perangkat tidak terus dimulai jika perangkat lunak perangkatnya telah dirusak." #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard Fuse" msgstr "Fuse Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "Intel BootGuard OTP fuse" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard Verified Boot" msgstr "Boot Terverifikasi Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Kebijakan kesalahan Intel BootGuard" #. TRANSLATORS: longer description msgid "Intel BootGuard prevents unauthorized device software from operating when the device is started." msgstr "Intel BootGuard mencegah perangkat lunak perangkat yang tidak sah beroperasi saat perangkat dimulai." #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Boot terverifikasi Intel BootGuard" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS Mitigation" msgstr "Mitigasi GDS Intel" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS mitigation" msgstr "Mitigasi GDS Intel" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Manufacturing Mode" msgstr "Mode Manufaktur Intel Management Engine" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the "override" is #. enabled #. * with a jumper -- luckily it is probably not accessible to end users on #. consumer #. * boards msgid "Intel Management Engine Override" msgstr "Penimpaan Mesin Manajemen Intel" #. TRANSLATORS: longer description msgid "Intel Management Engine Override disables checks for device software tampering." msgstr "Intel Management Engine Override menonaktifkan pemeriksaan untuk pengubahan perangkat lunak dalam perangkat." #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Version" msgstr "Versi Intel Management Engine" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "Perangkat internal" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "Tak valid" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "Argumen tidak valid" #. TRANSLATORS: error message msgid "Invalid arguments, expected GUID" msgstr "Argumen tidak valid, diharapkan GUID" #. TRANSLATOR: This is the error message for #. * incorrect parameter msgid "Invalid arguments, expected an AppStream ID" msgstr "Argumen tidak valid, diharapkan ID AppStream" #. TRANSLATORS: error message msgid "Invalid arguments, expected at least ARCHIVE FIRMWARE METAINFO" msgstr "Argumen tidak valid, diharapkan setidaknya ARCHIVE FIRMWARE METAINFO" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "Adalah turun tingkat" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "Sedang dalam mode bootloader" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "Adalah peningkatan" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "Masalah" #. TRANSLATORS: HSI event title msgid "Kernel is no longer tainted" msgstr "Kernel tidak lagi tercemar" #. TRANSLATORS: HSI event title msgid "Kernel is tainted" msgstr "Kernel tercemar" #. TRANSLATORS: HSI event title msgid "Kernel lockdown disabled" msgstr "Penguncian kernel dinonaktifkan" #. TRANSLATORS: HSI event title msgid "Kernel lockdown enabled" msgstr "Penguncian kernel diaktifkan" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "LOKASI" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "Modifikasi terakhir" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "Kurang dari satu menit tersisa" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "Lisensi" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "Penguncian Kernel Linux" #. TRANSLATORS: longer description msgid "Linux Kernel Lockdown mode prevents administrator (root) accounts from accessing and changing critical parts of system software." msgstr "Mode Linux Kernel Lockdown mencegah akun administrator (root) mengakses dan mengubah bagian-bagian kritis dari perangkat lunak sistem." msgid "Linux Kernel Swap temporarily saves information to disk as you work. If the information is not protected, it could be accessed by someone if they obtained the disk." msgstr "Linux Kernel Swap untuk sementara menyimpan informasi ke disk saat Anda bekerja. Jika informasi tidak dilindungi, itu dapat diakses oleh seseorang jika mereka mendapatkan disk." #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "Verifikasi Kernel Linux" msgid "Linux Kernel Verification makes sure that critical system software has not been tampered with. Using device drivers which are not provided with the system can prevent this security feature from working correctly." msgstr "Linux Kernel Verification memastikan bahwa perangkat lunak sistem kritis tidak diotak-atik. Memakai device driver yang tidak disediakan dengan sistem dapat mencegah fitur keamanan ini bekerja dengan benar." #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "Swap Linux" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Layanan Firmware Vendor Linux (firmware stabil)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Layanan Firmware Vendor Linux (firmware uji)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Kernel Linux" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Penguncian kernel Linux" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Linux swap" #. TRANSLATORS: command description msgid "List EFI boot files" msgstr "Mencantumkan daftar berkas boot EFI" #. TRANSLATORS: command description msgid "List EFI boot parameters" msgstr "Mencantumkan daftar parameter boot EFI" #. TRANSLATORS: command description msgid "List EFI variables with a specific GUID" msgstr "Mencantumkan variabel EFI dengan GUID tertentu" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "Membuat daftar entri di dbx" #. TRANSLATORS: command description msgid "List the available firmware GTypes" msgstr "Cantumkan daftar firmware GTypes yang tersedia" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "Daftar tipe firmware yang tersedia" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "Daftar berkas di ESP" #. TRANSLATORS: command description msgid "Load device emulation data" msgstr "Muat data emulasi perangkat" #. TRANSLATORS: the plugin was created from a .so object, and was not built-in msgid "Loaded from an external module" msgstr "Dimuat dari modul eksternal" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "Memuat…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "Terkunci" #. TRANSLATORS: the release urgency msgid "Low" msgstr "Rendah" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refers #. * to the private/public key used to secure loading of firmware msgid "MEI Key Manifest" msgstr "Manifes Kunci MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refer #. * to the private/public key used to secure loading of firmware msgid "MEI key manifest" msgstr "Manifes kunci MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "Mode manufaktur MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "MEI menimpa" msgid "MEI version" msgstr "Versi MEI" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "Aktifkan plugin tertentu secara manual" #. TRANSLATORS: longer description msgid "Manufacturing Mode is used when the device is manufactured and security features are not yet enabled." msgstr "Mode Manufaktur digunakan saat perangkat dibuat dan fitur keamanan belum diaktifkan." #. TRANSLATORS: Longest valid string for BIOS setting msgid "Maximum length" msgstr "Panjang maksimum" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "Nilai maksimum" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "Sedang" #. TRANSLATORS: ask the user to do a simple task which should be translated msgid "Message" msgstr "Pesan" #. TRANSLATORS: ask the user a question, and it will not be translated msgid "Message (custom)" msgstr "Pesan (ubahan)" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "Tanda Tangan Metadata" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "URI Metadata" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "Metadata dapat diperoleh dari Layanan Firmware Vendor Linux." #. TRANSLATORS: error message for a user who ran fwupdmgr #. * refresh recently -- %1 is '--force' #, c-format msgid "Metadata is up to date; use %s to refresh again." msgstr "Metadata mutakhir; gunakan %s untuk menyegarkan lagi." #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "Versi Minimum" #. TRANSLATORS: Shortest valid string for BIOS setting msgid "Minimum length" msgstr "Panjang minimum" #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "Nilai minimum" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "Memodifikasi nilai konfigurasi daemon" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "Mengubah suatu remote yang diberikan" msgid "Modify a configured remote" msgstr "Ubah suatu remote yang ditata" msgid "Modify daemon configuration" msgstr "Ubah konfigurasi daemon" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "Pantau daemon untuk kejadian" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "Kait ESP" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "Membutuhkan reboot setelah instalasi" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "Perlu reboot" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "Perlu dimatikan setelah instalasi" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "Versi baru" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "Tidak ada tindakan yang ditentukan!" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "Tidak ada penuruntingkatan untuk %s" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "Tidak ada ID firmware yang ditemukan" #. TRANSLATORS: nothing found msgid "No firmware found" msgstr "Tidak ditemukan firmware" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Tidak terdeteksi perangkat keras dengan kapabilitas pemutakhiran firmware" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "Tidak ada rilis yang tersedia" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "Saat ini tidak ada remote yang diaktifkan sehingga metadata tidak tersedia." #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "Tidak ada remote yang tersedia" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "Tidak ada perangkat yang dapat diperbarui" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "Tidak ada pembaruan yang tersedia" msgid "No updates available for remaining devices" msgstr "Tidak ada pembaruan yang tersedia untuk perangkat yang tersisa" #. TRANSLATORS: version cannot be installed due to policy msgid "Not approved" msgstr "Tidak disetujui" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "Tidak ditemukan" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "Tak didukung" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "OK" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "OK!" #. TRANSLATORS: the firmware old version msgid "Old version" msgstr "Versi lama" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "Hanya tampilkan nilai PCR tunggal" #. TRANSLATORS: command line option msgid "Only use peer-to-peer networking when downloading files" msgstr "Hanya gunakan jaringan peer-to-peer saat mengunduh berkas" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "Hanya peningkatan versi yang diizinkan" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "Timpa path ESP default" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Firmware" msgstr "Firmware P2P" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Metadata" msgstr "Metadata P2P" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "PATH" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "Uraikan dan tampilkan detail tentang berkas firmware" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "Mengurai pembaruan dbx…" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "Mengurai dbx sistem…" #. TRANSLATORS: remote filename base msgid "Password" msgstr "Kata Sandi" #. TRANSLATORS: command description msgid "Patch a firmware blob at a known offset" msgstr "Mem-patch blob firmware pada ofset yang diketahui" msgid "Payload" msgstr "Payload" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "Tertunda" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "Lakukan operasi?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "Pengawakutuan Platform" #. TRANSLATORS: longer description msgid "Platform Debugging allows device security features to be disabled. This should only be used by hardware manufacturers." msgstr "Debug Platform memungkinkan fitur keamanan perangkat dinonaktifkan. Ini hanya boleh digunakan oleh produsen perangkat keras." #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "Pengawakutuan platform" #. TRANSLATORS: 'recovery key' here refers to a code, rather than a physical #. metal thing msgid "Please ensure you have the volume recovery key before continuing." msgstr "Pastikan Anda memiliki kunci pemulihan volume sebelum melanjutkan." #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "Silakan masukkan angka dari 0 hingga %u: " #. TRANSLATORS: the user isn't reading the question -- %1 is 'Y' and %2 is #. * 'N' #, c-format msgid "Please enter either %s or %s: " msgstr "Silakan masukkan %s atau %s: " #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "Dependensi plugin kurang" #. TRANSLATORS: The plugin is only for testing msgid "Plugin is only for testing" msgstr "Plugin hanya untuk pengujian" #. TRANSLATORS: Possible values for a bios setting msgid "Possible Values" msgstr "Nilai yang Mungkin" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA Protection" msgstr "Perlindungan DMA Pra-Boot" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "Perlindungan DMA pra-boot" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is disabled" msgstr "Perlindungan DMA pra-boot dinonaktifkan" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is enabled" msgstr "Perlindungan DMA pra-boot diaktifkan" #. TRANSLATORS: longer description msgid "Pre-boot DMA protection prevents devices from accessing system memory after being connected to the computer." msgstr "Perlindungan DMA pra-boot mencegah perangkat mengakses memori sistem setelah terhubung ke komputer." #. TRANSLATORS: warning message msgid "Press unlock on the device to continue the update process." msgstr "Tekan buka kunci pada perangkat untuk melanjutkan proses pembaruan." #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "Versi sebelumnya" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Prioritas" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "Masalah" msgid "Proceed with upload?" msgstr "Lanjutkan mengunggah?" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Processor Security Checks" msgstr "Pemeriksaan Keamanan Prosesor" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "Processor rollback protection" msgstr "Perlindungan rollback prosesor" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "Proprietari" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "ID-REMOTE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID KEY VALUE" msgstr "ID-REMOTE KUNCI NILAI" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "Hanya Baca" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "Baca blob firmware dari perangkat" #. TRANSLATORS: command description msgid "Read a firmware from a device" msgstr "Membaca firmware dari perangkat" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "Membaca dari %s…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Membaca…" #. TRANSLATORS: Plugin is active and in use msgid "Ready" msgstr "Siap" #. TRANSLATORS: how often we should refresh the metadata msgid "Refresh Interval" msgstr "Selang Waktu Penyegaran" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "Segarkan metadata dari server remote" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "Pasang ulang %s ke %s?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "Menginstal ulang firmware saat ini di perangkat" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "Menginstal ulang firmware pada perangkat" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "Cabang Rilis" #. TRANSLATORS: release attributes msgid "Release Flags" msgstr "Flag Rilis" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "ID Rilis" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "ID Remote" #. TRANSLATORS: command description msgid "Removes devices to watch for future emulation" msgstr "Menghapus perangkat yang akan diawasi untuk emulasi di masa mendatang" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "URI Lapor" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "Dilaporkan ke server remote" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "Sistem berkas efivarfs yang diperlukan tidak ditemukan" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "Perangkat keras yang diperlukan tidak ditemukan" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "Membutuhkan bootloader" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Memerlukan koneksi internet" msgid "Reset daemon configuration" msgstr "Setel ulang konfigurasi daemon" #. TRANSLATORS: sets something in the daemon configuration file msgid "Resets a daemon configuration section" msgstr "Mengatur ulang bagian konfigurasi daemon" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Mulai ulang sekarang?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "Mulai ulang daemon untuk membuat perubahan itu efektif?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Memulai ulang perangkat…" #. TRANSLATORS: command description msgid "Retrieve BIOS settings. If no arguments are passed all settings are returned" msgstr "Ambil pengaturan BIOS. Jika tidak ada argumen yang diteruskan, semua pengaturan dikembalikan" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "Memberikan kembalian semua ID perangkat keras bagi mesin" #. TRANSLATORS: ask the user to upload msgid "Review and upload report now?" msgstr "Tinjau dan unggah laporan sekarang?" #. TRANSLATORS: longer description msgid "Rollback Protection prevents device software from being downgraded to an older version that has security problems." msgstr "Perlindungan Rollback mencegah perangkat lunak perangkat diturunkan ke versi lama yang memiliki masalah keamanan." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr get-upgrades` #, c-format msgid "Run `%s` for more information." msgstr "Jalankan '%s' untuk informasi selengkapnya." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr sync` #, c-format msgid "Run `%s` to complete this action." msgstr "Jalankan '%s' untuk menyelesaikan tindakan ini." #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "Menjalankan rutin pembersihan komposit plugin saat menggunakan install-blob" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "Menjalankan rutin persiapan komposit saat menggunakan install-blob" #. TRANSLATORS: command description msgid "Run the post-reboot cleanup action" msgstr "Jalankan tindakan pembersihan pasca-reboot" #. TRANSLATORS: tell a user how to get information #, c-format msgid "Run without '%s' to see" msgstr "Jalankan tanpa '%s' untuk melihat" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "Kernel yang sedang berjalan terlalu tua" #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "Akhiran Runtime" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SECTION" msgstr "BAGIAN" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING VALUE" msgstr "NILAI PENGATURAN" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING1 VALUE1 [SETTING2] [VALUE2]" msgstr "PENGATURAN1 NILAI1 [PENGATURAN2] [NILAI2]" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "SMAP" msgstr "SMAP" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "Deskriptor BIOS SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "Wilayah BIOS SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "SPI kunci" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "SPI replay protection" msgstr "Perlindungan replay SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "SPI tulis" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "SPI write protection" msgstr "Proteksi penulisan SPI" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "SUBSISTEM DRIVER [ID-PERANTI|GUID]" #. TRANSLATORS: command description msgid "Save a file that allows generation of hardware IDs" msgstr "Menyimpan berkas yang memungkinkan pembuatan ID perangkat keras" #. TRANSLATORS: command description msgid "Save device emulation data" msgstr "Simpan data emulasi perangkat" #. TRANSLATORS: key for a offline report filename msgid "Saved report" msgstr "Laporan tersimpan" #. TRANSLATORS: Scalar increment for integer BIOS setting msgid "Scalar Increment" msgstr "Kenaikan Nilai Skalar" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Menjadwalkan…" #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "Secure Boot dinonaktifkan" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "Secure Boot diaktifkan" msgid "Security hardening for HSI" msgstr "Hardening keamanan untuk HSI" #. TRANSLATORS: the %1 is a URL to a wiki page #, c-format msgid "See %s for more details." msgstr "Lihat %s untuk lebih jelasnya." #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "Lihat %s untuk informasi lebih lanjut." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Perangkat yang dipilih" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Volume yang dipilih" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "Nomor Seri" #. TRANSLATORS: Configured a BIOS setting to a value #, c-format msgid "Set BIOS setting '%s' using '%s'." msgstr "Setel pengaturan BIOS '%s' menggunakan '%s'." #. TRANSLATORS: command description msgid "Set a BIOS setting" msgstr "Atur pengaturan BIOS" msgid "Set one or more BIOS settings" msgstr "Atur satu atau beberapa pengaturan BIOS" #. TRANSLATORS: command line option msgid "Set the download retries for transient errors" msgstr "Menyetel percobaan ulang unduhan untuk kesalahan sementara" #. TRANSLATORS: command description msgid "Sets one or more BIOS settings" msgstr "Mengatur satu atau beberapa pengaturan BIOS" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Setel daftar firmware yang disetujui" #. TRANSLATORS: type of BIOS setting msgid "Setting type" msgstr "Jenis pengaturan" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "Pengaturan akan berlaku setelah reboot sistem" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "Membagikan riwayat firmware dengan para pengembang" #. TRANSLATORS: command line option msgid "Show all results" msgstr "Tampilkan semua hasil" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "Tampilkan versi daemon dan klien" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "Tampilkan informasi daemon rinci untuk domain tertentu" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "Tampilkan informasi debug untuk semua domain" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Tampilkan opsi debugging" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Tampilkan peranti yang tidak dapat dimutakhirkan" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Tampilkan informasi pengawakutuan ekstra" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "Tampilkan riwayat pembaruan firmware" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "Memperlihatkan versi terhitung dari dbx" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "Matikan sekarang?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "Menandatangani firmware dengan kunci baru" msgid "Sign data using the client certificate" msgstr "Tanda tangani data menggunakan sertifikat klien" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "Tanda tangani data menggunakan sertifikat klien" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "Menanda tangani data yang diunggah dengan sertifikat klien" msgid "Signature" msgstr "Tanda Tangan" #. TRANSLATORS: firmware is verified on-device the payload using strong crypto msgid "Signed Payload" msgstr "Payload yang Ditandatangani" #. TRANSLATORS: file size of the download msgid "Size" msgstr "Ukuran" #. TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM msgid "Some of the platform secrets may be invalidated when updating this firmware." msgstr "Beberapa rahasia platform mungkin menjadi tidak valid saat memperbarui firmware ini." #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "Sumber" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "Menentukan berkas basis data dbx" msgid "Stop the fwupd service" msgstr "Hentikan layanan fwupd" #. TRANSLATORS: The BIOS setting accepts strings msgid "String" msgstr "String" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "Sukses" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "Berhasil mengaktifkan semua perangkat" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "Remote yang berhasil dinonaktifkan" #. TRANSLATORS: comment explaining result of command msgid "Successfully disabled test devices" msgstr "Sukses menonaktifkan perangkat pengujian" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "Metadata baru berhasil diunduh: " #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "Berhasil mengaktifkan dan menyegarkan remote" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "Remote yang berhasil diaktifkan" #. TRANSLATORS: comment explaining result of command msgid "Successfully enabled test devices" msgstr "Sukses memfungsikan perangkat pengujian" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "Firmware berhasil diinstal" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "Nilai konfigurasi yang berhasil dimodifikasi" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "Remote yang berhasil dimodifikasi" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "Metadata berhasil disegarkan secara manual" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration section" msgstr "Sukses mengatur ulang bagian konfigurasi" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration values" msgstr "Berhasil mengatur ulang nilai konfigurasi" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "Checksum perangkat berhasil diperbarui" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "Berhasil mengunggah %u laporan" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "Checksum perangkat berhasil diverifikasi" #. TRANSLATORS: the device showed up in time #, c-format msgid "Successfully waited %.0fms for device" msgstr "Berhasil menunggu %.0fms untuk perangkat" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Ringkasan" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "Supervisor Mode Access Prevention" msgstr "Supervisor Mode Access Prevention" #. TRANSLATORS: longer description msgid "Supervisor Mode Access Prevention ensures critical parts of device memory are not accessed by less secure programs." msgstr "Supervisor Mode Access Prevention memastikan bagian-bagian kritis dari memori perangkat tidak diakses oleh program yang kurang aman." #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "Didukung" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "CPU yang didukung" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "Didukung di server remote" #. TRANSLATORS: Title: a better sleep state msgid "Suspend To Idle" msgstr "Suspensi Ke Menganggur" #. TRANSLATORS: Title: sleep state msgid "Suspend To RAM" msgstr "Suspensi Ke RAM" #. TRANSLATORS: longer description msgid "Suspend to Idle allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Sunpensi ke Menganggur memungkinkan perangkat untuk tidur dengan cepat agar menghemat daya. Meskipun perangkat telah ditangguhkan, memorinya dapat dicabut secara fisik dan informasinya diakses." #. TRANSLATORS: longer description msgid "Suspend to RAM allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Suspensi ke RAM memungkinkan perangkat untuk tidur dengan cepat agar menghemat daya. Meskipun perangkat telah disuspensi, memorinya dapat dicabut secara fisik dan informasinya diakses." #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "Suspend-to-idle" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "Suspend-to-ram" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "Beralih cabang dari %s ke %s?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "Beralih cabang firmware pada perangkat" #. TRANSLATORS: command description msgid "Sync firmware versions to the chosen configuration" msgstr "Sinkronkan versi firmware ke konfigurasi yang dipilih" #. TRANSLATORS: this CLI tool is now preventing system updates msgid "System Update Inhibited" msgstr "Pemutakhiran Sistem Dicegah" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "Sistem membutuhkan sumber daya eksternal" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "TEKS" #. TRANSLATORS: longer description msgid "TPM (Trusted Platform Module) is a computer chip that detects when hardware components have been tampered with." msgstr "TPM (Trusted Platform Module) adalah sebuah chip komputer yang mendeteksi ketika komponen perangkat keras telah diotak-atik." #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "Rekonstruksi TPM PCR0" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is invalid" msgstr "Rekonstruksi PCR0 TPM tidak valid" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is now valid" msgstr "Rekonstruksi PCR0 TPM kini valid" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM Platform Configuration" msgstr "Konfigurasi Platform TPM" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM Reconstruction" msgstr "Rekonstruksi TPM" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM empty PCRs" msgstr "PCR kosong TPM" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "Tag" #. TRANSLATORS: we're saving all USB events for emulation msgid "Tagged for emulation" msgstr "Di-tag untuk emulasi" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "Ternoda" #. show the user the entire data blob msgid "Target" msgstr "Target" #. TRANSLATORS: command description msgid "Test a device using a JSON manifest" msgstr "Menguji perangkat menggunakan manifest JSON" #. TRANSLATORS: when the release was tested msgid "Tested" msgstr "Diuji" #. TRANSLATORS: the %s is a vendor name, e.g. Lenovo #, c-format msgid "Tested by %s" msgstr "Diuji oleh %s" #. TRANSLATORS: someone we trust has tested this msgid "Tested by trusted vendor" msgstr "Diuji oleh vendor terpercaya" #. TRANSLATORS: longer description msgid "The Intel Management Engine Key Manifest must be valid so that the device firmware can be trusted by the CPU." msgstr "Manifes Kunci Mesin Manajemen Intel harus valid sehingga firmware perangkat dapat dipercaya oleh CPU." #. TRANSLATORS: longer description msgid "The Intel Management Engine controls device components and needs to have a recent version to avoid security issues." msgstr "Mesin Manajemen Intel mengontrol komponen perangkat dan harus memiliki versi terbaru untuk menghindari masalah keamanan." #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "LVFS adalah layanan gratis yang beroperasi sebagai badan hukum independen dan tidak memiliki koneksi dengan $OS_RELEASE:NAME$. Distributor Anda mungkin belum memverifikasi pembaruan firmware untuk kompatibilitas dengan sistem Anda atau perangkat yang terhubung. Semua firmware hanya disediakan oleh pabrikan peralatan asli." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Platform Configuration is used to check whether the device start process has been modified." msgstr "Konfigurasi Platform TPM (Trusted Platform Module) dipakai untuk memeriksa apakah proses awalan perangkat telah dimodifikasi." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Reconstruction is used to check whether the device start process has been modified." msgstr "Rekonstruksi TPM (Trusted Platform Module) dipakai untuk memeriksa apakah proses awalan perangkat telah dimodifikasi." #. TRANSLATORS: this is more background on a security measurement problem msgid "The TPM PCR0 differs from reconstruction." msgstr "TPM PCR0 berbeda dengan rekonstruksi." #. TRANSLATORS: longer description msgid "The UEFI Platform Key is used to determine if device software comes from a trusted source." msgstr "Kunci Platform UEFI digunakan untuk menentukan apakah perangkat lunak perangkat berasal dari sumber tepercaya." #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "Daemon telah memuat kode pihak ke-3 dan tidak lagi didukung oleh pengembang hulu!" #. TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name #, c-format msgid "The firmware from %s is not supplied by %s, the hardware vendor." msgstr "Firmware dari %s tidak disediakan oleh %s, vendor perangkat keras." #. TRANSLATORS: try to help msgid "The system clock has not been set correctly and downloading files may fail." msgstr "Jam sistem belum diatur dengan benar dan mengunduh berkas mungkin gagal." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been re-inserted." msgstr "Pembaruan akan berlanjut ketika kabel USB perangkat telah ditancapkan kembali." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged and then re-inserted." msgstr "Pembaruan akan berlanjut ketika kabel USB perangkat telah dicabut dan kemudian ditancapkan kembali." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged." msgstr "Pembaruan akan berlanjut ketika kabel USB perangkat telah dicabut." #. TRANSLATORS: warning message msgid "The update will continue when the device power cable has been removed and re-inserted." msgstr "Pembaruan akan berlanjut saat kabel daya perangkat telah dilepas dan dipasang kembali." #. TRANSLATORS: naughty vendor msgid "The vendor did not supply any release notes." msgstr "Vendor tidak memberikan catatan rilis apa pun." #. TRANSLATORS: now list devices with unfixed high-priority issues msgid "There are devices with issues:" msgstr "Ada perangkat yang bermasalah:" #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "Tidak ada berkas firmware yang diblokir" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "Tidak ada firmware yang disetujui." #. TRANSLATORS: %1 is the current device version number, and %2 is the #. command name, e.g. `fwupdmgr sync` #, c-format msgid "This device will be reverted back to %s when the %s command is performed." msgstr "Perangkat ini akan dikembalikan ke %s saat perintah %s dilakukan." #. TRANSLATORS: the vendor did not upload this msgid "This firmware is provided by LVFS community members and is not provided (or supported) by the original hardware vendor." msgstr "Firmware ini disediakan oleh anggota komunitas LVFS dan tidak disediakan (atau didukung) oleh vendor perangkat keras asli." #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "Paket ini belum divalidasi, mungkin tidak berfungsi dengan baik." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "Program ini hanya dapat berfungsi dengan benar sebagai root" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "Remote ini berisi firmware yang tidak diembargo, tetapi masih sedang diuji oleh vendor perangkat keras. Anda harus memastikan Anda memiliki cara untuk menurunkan versi firmware secara manual jika pembaruan firmware gagal." #. TRANSLATORS: error message msgid "This system doesn't support firmware settings" msgstr "Sistem ini tidak mendukung pengaturan firmware" #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "Sistem ini memiliki masalah runtime HSI." #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "Sistem ini memiliki tingkat keamanan HSI yang rendah." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "Alat ini memungkinkan administrator untuk menerapkan pembaruan UEFI dbx." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "Alat ini memungkinkan administrator untuk kuiri dan mengontrol daemon fwupd, yang memungkinkan mereka untuk melakukan tindakan seperti menginstal atau menurun-tingkatkan firmware." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "Alat ini memungkinkan administrator untuk menggunakan plugin fwupd tanpa diinstal pada sistem host." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can add a kernel argument of '%s', but it will only be active after restarting the computer." msgstr "Alat ini dapat menambahkan argumen kernel '%s', tetapi hanya akan aktif setelah memulai ulang komputer." #. TRANSLATORS: the %1 is a BIOS setting name. #. * %2 and %3 are the values, e.g. "True" or "Windows10" #, c-format msgid "This tool can change the BIOS setting '%s' from '%s' to '%s' automatically, but it will only be active after restarting the computer." msgstr "Alat ini dapat mengubah pengaturan BIOS '%s' dari '%s' menjadi '%s' secara otomatis, tetapi hanya akan aktif setelah memulai ulang komputer." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can change the kernel argument from '%s' to '%s', but it will only be active after restarting the computer." msgstr "Alat ini dapat mengubah argumen kernel dari '%s' menjadi '%s', tetapi hanya akan aktif setelah memulai ulang komputer." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "Alat ini akan membaca dan mengurai log kejadian TPM dari firmware sistem." #. TRANSLATORS: the update state of the specific device msgid "Transient failure" msgstr "Kegagalan transien" #. TRANSLATORS: item is TRUE msgid "True" msgstr "Benar" #. TRANSLATORS: We verified the metadata against the server msgid "Trusted metadata" msgstr "Metadata tepercaya" #. TRANSLATORS: We verified the payload against the server msgid "Trusted payload" msgstr "Payload tepercaya" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Tipe" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI Bootservice Variables" msgstr "Variabel UEFI Bootservice" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition may not be set up correctly" msgstr "Partisi UEFI ESP mungkin tidak disiapkan dengan benar" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "Partisi UEFI ESP tidak terdeteksi atau dikonfigurasi" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "Kunci Platform UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "Secure Boot UEFI" #. TRANSLATORS: longer description msgid "UEFI Secure Boot prevents malicious software from being loaded when the device starts." msgstr "UEFI Secure Boot mencegah perangkat lunak berbahaya dimuat saat perangkat dimulai." #. TRANSLATORS: longer description msgid "UEFI boot service variables should not be readable from runtime mode." msgstr "Variabel-variabel layanan boot UEFI mesti tidak dapat dibaca dari mode runtime." #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI bootservice variables" msgstr "Variabel bootservice UEFI" #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "Pembaruan kapsul UEFI tidak tersedia atau diaktifkan dalam penyiapan firmware" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "Utilitas dbx UEFI" #. TRANSLATORS: system is not booted in UEFI mode msgid "UEFI firmware can not be updated in legacy BIOS mode" msgstr "Firmware UEFI tidak dapat diperbarui dalam mode BIOS warisan" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "Kunci platform UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "Boot aman UEFI" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "Tidak dapat terhubung ke layanan" #. TRANSLATORS: error message msgid "Unable to find attribute" msgstr "Tidak dapat menemukan atribut" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "Lepas ikatan driver saat ini" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "Membuka blokir firmware:" #. TRANSLATORS: command description msgid "Unblocks a specific firmware from being installed" msgstr "Membuka blokir firmware tertentu agar tidak diinstal" #. TRANSLATORS: command description msgid "Undo the host security attribute fix" msgstr "Batalkan perbaikan atribut keamanan host" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "Tidak terenkripsi" #. TRANSLATORS: command description msgid "Uninhibit the system to allow upgrades" msgstr "Melepas larangan sistem untuk mengizinkan peningkatan" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Tidak diketahui" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Perangkat Tidak Dikenal" msgid "Unlock the device to allow access" msgstr "Buka kunci perangkat untuk mengizinkan akses" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "Tak terkunci" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "Membuka kunci perangkat bagi akses firmware" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "Lepas kait ESP" #. TRANSLATORS: message shown after device has been marked for emulation msgid "Unplug and replug the device to continue the update process." msgstr "Cabut dan pasang kembali perangkat untuk melanjutkan proses pembaruan." #. TRANSLATORS: firmware payload is unsigned and it is possible to modify it msgid "Unsigned Payload" msgstr "Payload yang Tidak Ditandatangani" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "Versi daemon tidak didukung %s, versi klien adalah %s" #. TRANSLATORS: Suffix: the HSI result msgid "Untainted" msgstr "Tidak ternoda" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "Dapat diperbarui" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Kesalahan Pembaruan" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "Perbarui Image" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "Pesan Pembaruan" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "Keadaan Pembaruan" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "Kegagalan pembaruan adalah masalah yang telah diketahui, kunjungi URL ini untuk informasi lebih lanjut:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Perbarui sekarang?" #. TRANSLATORS: command description msgid "Update the stored cryptographic hash with current ROM contents" msgstr "Memperbarui hash kriptografi yang disimpan dengan konten ROM saat ini" msgid "Update the stored device verification information" msgstr "Mutakhirkan informasi verifikasi perangkat yang tersimpan" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "Perbarui metadata yang disimpan dengan konten saat ini" #. TRANSLATORS: command description msgid "Updates all specified devices to latest firmware version, or all devices if unspecified" msgstr "Memperbarui semua perangkat yang ditentukan ke versi firmware terbaru, atau semua perangkat jika tidak ditentukan" #. TRANSLATORS: how many local devices can expect updates now #, c-format msgid "Updates have been published for %u local device" msgid_plural "Updates have been published for %u of %u local devices" msgstr[0] "Pembaruan telah dipublikasikan untuk %u dari %u perangkat lokal" msgid "Updating" msgstr "Memutakhirkan" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "Memutakhirkan %s…" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "Meningkatkan %s dari %s ke %s?" #. TRANSLATORS: ask the user to upload msgid "Upload data now?" msgstr "Unggah data sekarang?" #. TRANSLATORS: command description msgid "Upload the list of updatable devices to a remote server" msgstr "Mengunggah daftar perangkat yang dapat diperbarui ke server jarak jauh" #. TRANSLATORS: ask the user to share, %s is something #. * like: "Linux Vendor Firmware Service" #, c-format msgid "Upload these anonymous results to the %s to help other users?" msgstr "Unggah hasil anonim ini ke %s untuk membantu pengguna lain?" #. TRANSLATORS: explain why we want to upload #, c-format msgid "Uploading a device list allows the %s team to know what hardware exists, and allows us to put pressure on vendors that do not upload firmware updates for their hardware." msgstr "Mengunggah daftar perangkat memungkinkan tim %s untuk mengetahui perangkat keras apa yang ada, dan memungkinkan kami untuk menekan vendor yang tidak mengunggah pembaruan firmware bagi perangkat keras mereka." #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "Mengunggah laporan firmware membantu vendor perangkat keras untuk dengan cepat mengidentifikasi pembaruan yang gagal dan berhasil pada perangkat nyata." #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "Urgensi" #. TRANSLATORS: explain how to get help, %1 is #. * 'fwupdtool --help' #. TRANSLATORS: explain how to get help, #. * where $1 is something like 'fwupdmgr --help' #, c-format msgid "Use %s for help" msgstr "Gunakan %s untuk bantuan" #. TRANSLATORS: CTRL^C [holding control, and then pressing C] will exit the #. program msgid "Use CTRL^C to cancel." msgstr "Gunakan CTRL^C untuk membatalkan." #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "Pengguna telah diberitahu" #. TRANSLATORS: remote filename base msgid "Username" msgstr "Nama Pengguna" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "Valid" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "Memvalidasi konten ESP…" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "Varian" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "Vendor" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "Verifikasi…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Versi" #. TRANSLATORS: the fwupd version the release was tested on msgid "Version[fwupd]" msgstr "Versi[fwupd]" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "PERINGATAN" #. TRANSLATORS: command description msgid "Wait for a device to appear" msgstr "Menunggu perangkat muncul" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "Menunggu…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "Awasi perubahan perangkat keras" #. TRANSLATORS: check various UEFI and ACPI tables are unchanged after the #. update msgid "Will measure elements of system integrity around an update" msgstr "Akan mengukur elemen integritas sistem seputar pembaruan" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "Menulis berkas:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Menulis…" #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from a recovery or installation disk, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "Anda harus memastikan bahwa Anda merasa nyaman memulihkan pengaturan dari disk pemulihan atau instalasi, karena perubahan ini dapat menyebabkan sistem tidak boot ke Linux atau menyebabkan ketidakstabilan sistem lainnya." #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from the system firmware setup, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "Anda harus memastikan bahwa Anda merasa nyaman memulihkan pengaturan dari penyiapan firmware sistem, karena perubahan ini dapat menyebabkan sistem tidak bisa boot ke Linux atau menyebabkan ketidakstabilan sistem lainnya." #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "Distributor Anda mungkin belum memverifikasi pembaruan firmware untuk kompatibilitas dengan sistem Anda atau perangkat yang terhubung." #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "Perangkat keras Anda mungkin rusak menggunakan firmware ini, dan menginstal rilis ini dapat membatalkan garansi apa pun dengan %s." #. TRANSLATORS: BKC is the industry name for the best known configuration and #. is a set #. * of firmware that works together #, c-format msgid "Your system is set up to the BKC of %s." msgstr "Sistem Anda disiapkan sebagai BKC dari %s." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[APPSTREAM_ID]" msgstr "[APPSTREAM_ID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[CHECKSUM]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[ID-PERANTI|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[ID-PERANTI|GUID] [CABANG]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [VERSION]" msgstr "[ID-PERANGKAT|GUID] [VERSI]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE]" msgstr "[PERANGKAT]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILE FILE_SIG REMOTE-ID]" msgstr "[BERKAS TTD_BERKAS ID-REMOTE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[NAMABERKAS1] [NAMABERKAS2]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SECTION] KEY VALUE" msgstr "[BAGIAN] KUNCI NILAI" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2] [--no-authenticate]" msgstr "[PENGATURAN1] [PENGATURAN2] [--no-authenticate]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2]..." msgstr "[PENGATURAN1] [PENGATURAN2]..." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[BERKAS-SMBIOS|BERKAS-HWIDS]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "baku" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "Utilitas log kejadian TPM fwupd" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "plugin fwupd" fwupd-2.0.10/po/it.po000066400000000000000000002637641501337203100143260ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Daniele Guarascio, 2024 # Gianvito Cavasoli , 2016 # Milo Casagrande , 2017-2023 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Italian (http://app.transifex.com/freedesktop/fwupd/language/it/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: it\n" "Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "Manca %.0f minuto" msgstr[1] "Mancano %.0f minuti" msgstr[2] "Mancano %.0f minuti" #. TRANSLATORS: BMC refers to baseboard management controller which #. * is the device that updates all the other firmware on the system #, c-format msgid "%s BMC Update" msgstr "Aggiornamento BMC %s" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "Aggiornamento batteria %s" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "Aggiornamento microcode CPU %s" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "Aggiornamento fotocamera %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "Aggiornamento configurazione %s" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "Aggiornamento Consumer ME di %s" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "Aggiornamento unità di controllo %s" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "Aggiornamento Coporate ME di %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "Aggiornamento dispositivo %s" #. TRANSLATORS: Video Display refers to the laptop internal display or #. * external monitor #, c-format msgid "%s Display Update" msgstr "Aggiornamento schermo %s" #. TRANSLATORS: Dock refers to the port replicator hardware laptops are #. * cradled in, or lowered onto #, c-format msgid "%s Dock Update" msgstr "Aggiornamento dock %s" #. TRANSLATORS: drive refers to a storage device, e.g. SATA disk #, c-format msgid "%s Drive Update" msgstr "Aggiornamento unità %s" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "Aggiornamento unità di controllo integrata di %s" #. TRANSLATORS: a device that can read your fingerprint pattern #, c-format msgid "%s Fingerprint Reader Update" msgstr "Aggiornamento lettore impronte %s" #. TRANSLATORS: flash refers to solid state storage, e.g. UFS or eMMC #, c-format msgid "%s Flash Drive Update" msgstr "Aggiornamento unità dati %s" #. TRANSLATORS: GPU refers to a Graphics Processing Unit, e.g. #. * the "video card" #, c-format msgid "%s GPU Update" msgstr "Aggiornamento GPU %s" #. TRANSLATORS: a large pressure-sensitive drawing area typically used #. * by artists and digital artists #, c-format msgid "%s Graphics Tablet Update" msgstr "Aggiornamento tavoletta grafica %s" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "Aggiornamento tastiera %s" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "Aggiornamento ME di %s" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "Aggiornamento mouse %s" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "Aggiornamento interfaccia di rete %s" #. TRANSLATORS: SSD refers to a Solid State Drive, e.g. non-rotating #. * SATA or NVMe disk #, c-format msgid "%s SSD Update" msgstr "Aggiornamento SSD %s" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "Aggiornamento controller di archiviazione %s" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "Aggiornamento sistema %s" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "Aggiornamento TPM %s" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "Aggiornamento unità di controllo Thunderbolt %s" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "Aggiornamento touchpad %s" #. TRANSLATORS: Dock refers to the port replicator device connected #. * by plugging in a USB cable -- which may or may not also provide power #, c-format msgid "%s USB Dock Update" msgstr "Aggiornamento dock USB %s" #. TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth #. * device that stays in the USB port so the wireless peripheral works #, c-format msgid "%s USB Receiver Update" msgstr "Aggiornamento ricevitore USB %s" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "Aggiornamento di %s" #. TRANSLATORS: warn the user before updating, %1 is a device name #, c-format msgid "%s and all connected devices may not be usable while updating." msgstr "%s e tutti i dispositivi collegati potrebbero non essere utilizzabili durante l'aggiornamento." #. TRANSLATORS: %1 refers to some kind of security test, e.g. "Encrypted RAM". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s appeared: %s" msgstr "%s apparso: %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "UEFI platform #. key". #. * %2 and %3 refer to results value, e.g. "Valid" and "Invalid" #, c-format msgid "%s changed: %s → %s" msgstr "%s è cambiato: %s → %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "SPI BIOS #. region". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s disappeared: %s" msgstr "%s scomparso: %s" #. TRANSLATORS: the device has a reason it can't update, e.g. laptop lid #. closed #, c-format msgid "%s is not currently updatable" msgstr "%s non può essere aggiornato" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "Modalità costruttore %s" #. TRANSLATORS: warn the user before #. * updating, %1 is a device name #, c-format msgid "%s must remain connected for the duration of the update to avoid damage." msgstr "%s deve rimanere collegato durante l'aggiornamento per evitare possibili danni." #. TRANSLATORS: warn the user before updating, %1 is a machine #. * name #, c-format msgid "%s must remain plugged into a power source for the duration of the update to avoid damage." msgstr "%s deve rimanere collegato alla rete elettrica durante l'aggiornamento per evitare possibili danni." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "Override %s" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "Versione %s" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u giorno" msgstr[1] "%u giorni" msgstr[2] "%u giorni" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "%u dispositivo ha un aggiornamento firmware disponibile." msgstr[1] "%u dispositivi hanno un aggiornamento firmware disponibile." msgstr[2] "%u dispositivi hanno un aggiornamento firmware disponibile." #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device is not the best known configuration." msgid_plural "%u devices are not the best known configuration." msgstr[0] "%u dispositivo non dispone della configurazione migliore." msgstr[1] "%u dispositivi non dispongono della configurazione migliore." msgstr[2] "%u dispositivi non dispongono della configurazione migliore." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u ora" msgstr[1] "%u ore" msgstr[2] "%u ore" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minuto" msgstr[1] "%u minuti" msgstr[2] "%u minuti" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u secondo" msgstr[1] "%u secondi" msgstr[2] "%u secondi" #. TRANSLATORS: first percentage is current value, 2nd percentage is the #. * lowest limit the firmware update is allowed for the update to happen #, c-format msgid "%u%% (threshold %u%%)" msgstr "%u%% (soglia %u%%)" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(obsoleto)" #. TRANSLATORS: HSI event title msgid "A TPM PCR is now an invalid value" msgstr "Un PCR TPM è ora un valore non valido" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "AMD Firmware Replay Protection" msgstr "Protezione replay firmware AMD" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "AMD Firmware Write Protection" msgstr "Protezione scrittura firmware AMD" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "AMD Secure Processor Rollback Protection" msgstr "Protezione rollback sicuro del processore AMD" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "Azione richiesta:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "Attiva dispositivi" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "Attiva i dispositivi in attesa" msgid "Activate the new firmware on the device" msgstr "Attiva il nuovo firmware sul dispositivo" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "Attivazione aggiornamento firmware" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "Attivazione aggiornamento firmware per" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Età" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "Accettare e abilitare il remoto?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Alias di %s" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are now valid" msgstr "Tutti i PCR TPM non sono validi" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are valid" msgstr "Tutti i PCR TPM sono validi" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "Tutti i dispositivi dello stesso tipo saranno aggiornati allo stesso tempo" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Consente di tornare alle precedenti versioni del firmware" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "Consente la re-installazione di versioni esistenti del firmware" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "Consente il cambio del ramo firmware" #. TRANSLATORS: is not the main firmware stream msgid "Alternate branch" msgstr "Ramo alternativo" #. TRANSLATORS: another application is updating the device already msgid "An update is in progress" msgstr "Aggiornamento in corso" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "Per essere completato, un aggiornamento richiede un riavvio." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "Per essere completato, un aggiornamento richiede lo spegnimento del sistema." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Risponde affermativamente a tutte le domande" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Applica aggiornamento anche se non consigliato" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Applica file di aggiornamento" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "Applicazione aggiornamento…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Firmware approvato:" msgstr[1] "Firmware approvati:" msgstr[2] "Firmware approvati:" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "Collega in modalità firmware" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Autenticazione…" #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "Sono richiesti i dettagli di autenticazione" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "È richiesto autenticarsi per tornare al precedente firmware su un dispositivo rimovibile" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "È richiesto autenticarsi tornare al precedente firmware su questa macchina" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to fix a host security issue" msgstr "È richiesta l'autenticazione per risolvere un problema di sicurezza dell'host" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "È richiesto autenticarsi per modificare le impostazioni BIOS" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "È richiesto autenticarsi per modificare un remoto configurato utilizzato per aggiornamenti firmware" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "È richiesto autenticarsi per modificare la configurazione del demone" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "È richiesto autenticarsi per leggere le impostazioni BIOS" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to reset daemon configuration to defaults" msgstr "È richiesta l'autenticazione per poter resettare la configurazione del demone ai valori di default" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "È richiesto autenticarsi per impostare l'elenco dei firmware approvati" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "È richiesto autenticarsi per firmare i dati utilizzando il certificato del client" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "È richiesto autenticarsi per passare alla nuova versione del firmware " #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to undo the fix for a host security issue" msgstr "È richiesta l'autenticazione per annullare la risoluzione di un problema di sicurezza dell'host" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "È richiesto autenticarsi per sbloccare un dispositivo" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "È richiesto autenticarsi per aggiornare il firmware su un dispositivo rimovibile" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "È richiesto autenticarsi per aggiornare il firmware su questa macchina" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "È richiesto autenticarsi per aggiornare il codice di controllo del dispositivo salvato" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "Rapporti automatici" #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "Caricare automaticamente ogni volta?" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS Rollback Protection" msgstr "Protezione rollback BIOS" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS rollback protection" msgstr "Protezione rollback BIOS" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "Aggiornamenti BIOS forniti tramite LVFS o Windows Update" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "BUILDER-XML FILENAME-DST" msgstr "BUILDER-XML NOMEFILE-DST" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "Batteria" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "Vincola in nuovo driver kernel" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "File di firmware bloccati:" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "Versione bloccata" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "Firmware bloccato:" #. TRANSLATORS: command description msgid "Blocks a specific firmware from being installed" msgstr "Blocca un firmware specifico così da non installarlo" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "Versione bootloader" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "Ramo" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "Compila una file firmware" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Annulla" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "Annullato" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "Impossibile applicare poiché l'aggiornamento dbx è già stato applicato." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Modificato" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "Verifica che l'hash crittografico corrisponda col firmware" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Codice di controllo" #. TRANSLATORS: get interactive prompt, where branch is the #. * supplier of the firmware, e.g. "non-free" or "free" msgid "Choose branch" msgstr "Scegliere un ramo" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "Scegliere un dispositivo" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "Scegliere un firmware" #. TRANSLATORS: get interactive prompt msgid "Choose release" msgstr "Scegliere un rilascio" #. TRANSLATORS: get interactive prompt msgid "Choose volume" msgstr "Scegliere un volume" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "Pulisce i risultati dell'ultimo aggiornamento" #. TRANSLATORS: error message msgid "Command not found" msgstr "Comando non trovato" #. TRANSLATORS: is not supported by the vendor msgid "Community supported" msgstr "Supportata dalla comunità" #. TRANSLATORS: no peeking msgid "Configuration is only readable by the system administrator" msgstr "La configurazione può essere letta solamente dall'amministratore" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "Converte un file firmware" #. TRANSLATORS: when the update was built msgid "Created" msgstr "Creato" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "Critica" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "È disponibile la verifica dell'hash crittografico" #. TRANSLATORS: current value of a BIOS setting msgid "Current Value" msgstr "Valore attuale" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "Versione attuale" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "ID-DISPOSITIVO|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Opzioni di debug" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "Estrazione…" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Descrizione" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "Scollega in modalità bootloader" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "Dettagli" #. TRANSLATORS: the best known configuration is a set of software that we know #. works #. * well together. In the OEM and ODM industries it is often called a BKC msgid "Deviate from the best known configuration?" msgstr "Allontanarsi dalla migliore configurazione nota?" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "Flag dispositivo" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "ID dispositivo" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Dispositivo aggiunto:" #. TRANSLATORS: the device is already connected msgid "Device already exists" msgstr "Il dispositivo esiste già" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "La batteria del dispositivo è troppo bassa" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse #, c-format msgid "Device battery power is too low (%u%%, requires %u%%)" msgstr "La carica della batteria del dispositivo è troppo bassa (%u%%, richiesto %u%%)" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "Il dispositivo è in grado di correggere problemi di flash" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Dispositivo modificato:" #. TRANSLATORS: a version check is required for all firmware msgid "Device firmware is required to have a version check" msgstr "È richiesto il firmware dispositivo per eseguire un controllo della versione" #. TRANSLATORS: emulated means we are pretending to be a different model msgid "Device is emulated" msgstr "Il dispositivo è emulato" #. TRANSLATORS: device cannot be interrupted, for instance taking a phone call msgid "Device is in use" msgstr "Il dispositivo è in uso" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "Il dispositivo è bloccato" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "È richiesto un dispositivo per installare tutti i rilasci forniti" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "Il dispositivo non è raggiungibile" #. TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode msgid "Device is unreachable, or out of wireless range" msgstr "Il dispositivo non è raggiungibile o fuori portata" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "Il dispositivo è utilizzabile per la durata dell'aggiornamento" #. TRANSLATORS: usually this is when we're waiting for a reboot msgid "Device is waiting for the update to be applied" msgstr "Il dispositivo è in attesa del completamento dell'aggiornamento" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Dispositivo rimosso:" #. TRANSLATORS: as in, wired mains power for a laptop msgid "Device requires AC power to be connected" msgstr "Il dispositivo richiede di essere collegato all'alimentazione elettrica" #. TRANSLATORS: The device cannot be updated due to missing vendor's license." msgid "Device requires a software license to update" msgstr "Il dispositivo necessita di una licenza software per aggiornare" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "Il dispositivo applica gli aggiornamenti a fasi" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "Il dispositivo supporta il passaggio a un ramo diverso del firmware" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "L'aggiornamento del dispositivo richiede l'attivazione" #. TRANSLATORS: save the old firmware to disk before installing the new one msgid "Device will backup firmware before installing" msgstr "Il dispositivo eseguirà un backup del firmware prima dell'installazione" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "Il dispositivo non comparirà nuovamente dopo l'aggiornamento" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "Dispositivi aggiornati con successo:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "Dispositivi non aggiornati correttamente:" #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "Dispositivi senza aggiornamenti firmware:" #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "Dispositivi con l'ultima versione disponibile del firmware:" #. TRANSLATORS: this is for the device tests msgid "Did not find any devices with matching GUIDs" msgstr "Nessun dispositivo trovato con GUID corrispondenti" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "Disabilitato" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "Disabilita un remoto dato" #. TRANSLATORS: the OS the release was tested on msgid "Distribution" msgstr "Distribuzione" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "Non controlla i metadati vecchi" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "Non controlla la cronologia non segnalata " #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "Non controlla se lo scaricamento dai remoti deve essere abilitato" #. TRANSLATORS: command line option msgid "Do not check or prompt for reboot after update" msgstr "Non controlla o chiede se è necessario riavviare dopo un aggiornamento" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "Non include il prefisso del dominio" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "Non include il prefisso della marcatura temporale" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "Non esegue i controlli di sicurezza del dispositivo" #. TRANSLATORS: command line option msgid "Do not prompt for devices" msgstr "Non chiede i dispositivi" #. TRANSLATORS: command line option msgid "Do not prompt to fix security issues" msgstr "Non richiede di risolvere problemi di sicurezza" #. TRANSLATORS: command line option msgid "Do not search the firmware when parsing" msgstr "Non cerca il firmware durante l'analisi" #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "Non scrive la cronologia" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "Le conseguenze del cambio di ramo del firmware sono state comprese?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "Disabilitare questa funzionalità per i prossimi aggiornamenti?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "Aggiornare questo remoto ora?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "Caricare automaticamente i resoconti per i prossimi aggiornamenti?" #. TRANSLATORS: command line option msgid "Don't prompt for authentication (less details may be shown)" msgstr "Non chiede autenticazione (potrebbero essere mostrati meno dettagli)" #. TRANSLATORS: success msgid "Done!" msgstr "Fatto." #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "Arretrare %s da %s a %s?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "Torna a una vecchia versione del firmware su un dispositivo" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "Arretramento di %s…" #. TRANSLATORS: command description msgid "Download a file" msgstr "Scarica un file" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Scaricamento…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "Scarica i dati SMBIOS da un file" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "Durata" #. TRANSLATORS: command description msgid "Emulate a device using a JSON manifest" msgstr "Emula un dispositivo usando un manifesto JSON" #. TRANSLATORS: this device is not actually real msgid "Emulated" msgstr "Emulato" #. TRANSLATORS: Title: if we are emulating a different host msgid "Emulated host" msgstr "Host emulato" msgid "Enable" msgstr "Abilita" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "Abilitare il nuovo remoto?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "Abilitare questo remoto?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Abilitato" #. TRANSLATORS: Plugin is active only if hardware is found msgid "Enabled if hardware matches" msgstr "Abilitato se corrisponde all'hardware" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "Abilita un remoto dato" msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "Abilitare questa funzionalità a proprio rischio: in caso di problemi con gli aggiornamenti sarà necessario contattare l'OEM. Solamente i problemi legati al processo di aggiornamento possono essere inviati a $OS_RELEASE:BUG_REPORT_URL$." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "Abilitare questo remoto a proprio rischio." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Cifrato" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "RAM cifrata" #. TRANSLATORS: the vendor is no longer supporting the device msgid "End of life" msgstr "Fine vita" #. TRANSLATORS: The BIOS setting can only be changed to fixed values msgid "Enumeration" msgstr "Enumerazione" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "Elimina tutta la cronologia degli aggiornamenti firmware" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Eliminazione…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "Esce dopo una breve attesa" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "Esce dopo che il motore è stato caricato" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "Esporta la struttura di un file firmware in XML" #. TRANSLATORS: command description msgid "Extract a firmware blob to images" msgstr "Estrae un blob firmware in immagini" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "FILE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "FILE [ID-DISPOSITIVO|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "NOMEFILE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "NOMEFILE CERTIFICATO CHIAVE-PRIVATA" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "NOMEFILE ID-DISPOSITIVO" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME OFFSET DATA [FIRMWARE-TYPE]" msgstr "FILENAME OFFSET DATA [TIPO-FIRMWARE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "NOMEFILE [ID-DISPOSITIVO|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "NOMEFILE [TIPO-FIRMWARE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" msgstr "NOMEFILE-SRC NOMEFILE-DST [TIPO-FIRMWARE-SRC] [TIPO-FIRMWARE-DST]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" msgstr "NOMEFILE|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "Non riuscito" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Applicazione aggiornamento non riuscita" #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "Connessione al demone non riuscita" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "Caricamento dbx locale non riuscito" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "Caricamento dbx di sistema non riuscito" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "Blocco non riuscito" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Analisi degli argomenti non riuscita" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "Lettura del file non riuscita" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "Lettura dbx locale non riuscita" #. TRANSLATORS: a feature is something like "can show an image" msgid "Failed to set front-end features" msgstr "Impostazione delle funzionalità del front-end non riuscita" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "Verifica contenuti ESP non riuscita" #. TRANSLATORS: item is FALSE msgid "False" msgstr "Falso" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Nome file" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "Firma nome file" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "Sorgente nome file" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Nome file richiesto" #. TRANSLATORS: command line option msgid "Filter with a set of device flags using a ~ prefix to exclude, e.g. 'internal,~needs-reboot'" msgstr "Filtra tramite un insieme di flag utilizzando ~ per escludere, per esempio «internal, ~needs-reboot»" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware Attestation" msgstr "Convalida firmware" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "Descrittore BIOS firmware" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "Regione BIOS firmware" #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "URI di base del firmware" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "Servizio D-Bus di aggiornamento firmware" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Demone di aggiornamento firmware" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "Verifica aggiornamento firmware" #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "Aggiornamenti firmware" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "Strumento gestione firmware" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "Protezione scrittura firmware" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "Blocco protezione scrittura firmware" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "Convalida firmware" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "Il firmware è già bloccato" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "Il firmware non è bloccato" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "I metadati del firmware non sono stati controllati per %u giorno e potrebbero non essere aggiornati." msgstr[1] "I metadati del firmware non sono stati controllati per %u giorni e potrebbero non essere aggiornati." msgstr[2] "I metadati del firmware non sono stati controllati per %u giorni e potrebbero non essere aggiornati." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "Aggiornamenti firmware" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "Flag" #. TRANSLATORS: command line option msgid "Force the action by relaxing some runtime checks" msgstr "Forza l'azione riducendo alcuni controlli runtime" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Trovato" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "Rilevata cifratura del disco" #. TRANSLATORS: we might ask the user the recovery key when next booting #. Windows msgid "Full disk encryption secrets may be invalidated when updating" msgstr "I segreti di cifratura del disco potrebbero essere invalidati durante l'aggiornamento" #. TRANSLATORS: Title: if the part has been fused msgid "Fused Platform" msgstr "Piattaforma saldata" #. TRANSLATORS: Title: if the part has been fused msgid "Fused platform" msgstr "Piattaforma saldata" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" msgstr[1] "GUID" msgstr[2] "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "GUID|DEVICE-ID" msgstr "GUID|ID-DISPOSITIVO" msgid "Get BIOS settings" msgstr "Recupero impostazioni BIOS" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "Ottiene tutti i flag dispositivo supportati da fwupd" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Ottiene tutti i dispositivi che supportano gli aggiornamenti del firmware" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "Recupera tutti i plugin registrati nel sistema" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Ottiene le informazioni su un file di firmware" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "Ottiene i remoti configurati" #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "Recupera gli attributi di sicurezza dell'host" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "Recupera l'elenco dei firmware approvati" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "Recupera l'elenco del firmware bloccato" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Ottiene l'elenco degli aggiornamenti per l'hardware connesso" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "Ottiene i rilasci di un dispositivo" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Ottiene i risultati dell'ultimo aggiornamento" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "FILE-HWIDS" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "L'hardware è in attesa di essere ricollegato" #. TRANSLATORS: the release urgency msgid "High" msgstr "Elevata" #. TRANSLATORS: title for host security events msgid "Host Security Events" msgstr "Eventi sicurezza host" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "ID sicurezza host:" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "Protezione IOMMU" #. TRANSLATORS: HSI event title msgid "IOMMU device protection disabled" msgstr "Protezione dispositivo IOMMU disabilitata" #. TRANSLATORS: HSI event title msgid "IOMMU device protection enabled" msgstr "Protezione dispositivo IOMMU abilitata" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "Inattivo…" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "Ignora controlli SSL nello scaricare i file" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "Ignora i checksum del firmware non riusciti" #. TRANSLATORS: command line option msgid "Ignore firmware hardware mismatch failures" msgstr "Ignora corrispondenze errate firmware hardware" #. TRANSLATORS: try to help msgid "Ignoring SSL strict checks, to do this automatically in the future export DISABLE_SSL_STRICT in your environment" msgstr "Controlli SSL ignorati, per fare ciò automaticamente in futuro, esportare DISABLE_SSL_STRICT nel proprio ambiente" #. TRANSLATORS: the inhibit ID is a short string like dbus-123456 #, c-format msgid "Inhibit ID is %s." msgstr "L'ID d'inibizione è %s" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "Tempo di installazione" #. TRANSLATORS: command description msgid "Install a firmware file in cabinet format on this hardware" msgstr "Installa un file di firmware in formato cabinet su questo hardware" #. TRANSLATORS: command description msgid "Install a specific firmware file on all devices that match" msgstr "Installa un file di firmware specifico su tutti i dispositivi corrispondenti" msgid "Install old version of signed system firmware" msgstr "Installa versione precedente del firmware firmato di sistema" msgid "Install old version of unsigned system firmware" msgstr "Installa versione precedente del firmware non firmato di sistema" msgid "Install signed device firmware" msgstr "Installa firmware firmato del dispositivo" msgid "Install signed system firmware" msgstr "Installa firmware firmato di sistema" msgid "Install unsigned device firmware" msgstr "Installa firmware non firmato del dispositivo" msgid "Install unsigned system firmware" msgstr "Installa firmware non firmato di sistema" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "Installazione aggiornamento firmware…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "Installazione su %s…" #. TRANSLATORS: if it breaks, you get to keep both pieces msgid "Installing this update may also void any device warranty." msgstr "L'installazione dell'aggiornamento potrebbe annullare la garanzia del dispositivo." #. TRANSLATORS: The BIOS setting only accepts integers in a fixed range msgid "Integer" msgstr "Intero" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM Protected" msgstr "ACM protetto Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "ACM protetto da Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard Error Policy" msgstr "Regole di errore Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard Fuse" msgstr "Fusibile Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "Fusibile OTP Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard Verified Boot" msgstr "Avvio verificato Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Regole di errore Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Avvio verificato da Intel BootGuard" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS Mitigation" msgstr "Mitigazione GDS Intel" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS mitigation" msgstr "Mitigazione GDS Intel" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Manufacturing Mode" msgstr "Modalità costruttore Intel Management Engine" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the "override" is #. enabled #. * with a jumper -- luckily it is probably not accessible to end users on #. consumer #. * boards msgid "Intel Management Engine Override" msgstr "Disabilitazione Intel Management Engine" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Version" msgstr "Versione Intel Management Engine" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "Dispositivo interno" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "Non valido" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "Parametri non validi" #. TRANSLATORS: error message msgid "Invalid arguments, expected GUID" msgstr "Parametro non valido, atteso GUID" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "È retrocessione" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "È in modalità bootloader" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "È aggiornamento" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "Problema" msgstr[1] "Problemi" msgstr[2] "Problemi" #. TRANSLATORS: HSI event title msgid "Kernel is no longer tainted" msgstr "Il kernel è di nuovo integro" #. TRANSLATORS: HSI event title msgid "Kernel is tainted" msgstr "Il kernel non è integro" #. TRANSLATORS: HSI event title msgid "Kernel lockdown disabled" msgstr "Lockdown kernel disabilitato" #. TRANSLATORS: HSI event title msgid "Kernel lockdown enabled" msgstr "Lockdown kernel abilitato" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "POSIZIONE" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "Ultima modifica" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "Manca meno di un minuto" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "Licenza" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "Lockdown kernel Linux" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "Verifica kernel Linux" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "Swap Linux" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Linux Vendor Firmware Service (firmware stabile)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Linux Vendor Firmware Service (firmware in prova)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Kernel Linux" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Lockdown kernel Linux" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Swap Linux" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "Elenca le voci nel dbx" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "Elenca tutti i tipi di firmware disponibili" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "Elenca i file nell'ESP" #. TRANSLATORS: the plugin was created from a .so object, and was not built-in msgid "Loaded from an external module" msgstr "Caricato da un module esterno" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "Caricamento…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "Bloccato" #. TRANSLATORS: the release urgency msgid "Low" msgstr "Bassa" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refers #. * to the private/public key used to secure loading of firmware msgid "MEI Key Manifest" msgstr "Manifesto chiave MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refer #. * to the private/public key used to secure loading of firmware msgid "MEI key manifest" msgstr "Manifesto chiave MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "Modalità costruttore MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "Override MEI" msgid "MEI version" msgstr "Versione MEI" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "Abilita manualmente i plugin selezionati" #. TRANSLATORS: Longest valid string for BIOS setting msgid "Maximum length" msgstr "Lunghezza massima" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "Valore massimo" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "Media" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "Firma metadati" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "URI metadati" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "Metadati possono essere scaricati da Linux Vendor Firmware Service." #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "Versione minima" #. TRANSLATORS: Shortest valid string for BIOS setting msgid "Minimum length" msgstr "Lunghezza minima" #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "Valore minimo" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "Modifica il valore della configurazione del demone" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "Modifica un remoto" msgid "Modify a configured remote" msgstr "Modifica un remoto configurato" msgid "Modify daemon configuration" msgstr "Modifica la configurazione del demone" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "Controlla il demone per gli eventi" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "Monta l'ESP" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "Necessita un riavvio dopo l'installazione" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "Richiesto riavvio" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "Necessita l'arresto dopo l'installazione" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "Nuova versione" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "Nessuna azione specificata." #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "Nessuna versione precedente per %s" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "Nessun ID firmware trovato" #. TRANSLATORS: nothing found msgid "No firmware found" msgstr "Nessun firmware trovato" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Non è stato rilevato nessun hardware con capacità di aggiornamento del firmware" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "Nessuna versione disponibile" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "Non è abilitato alcun remoto e non sono disponibili metadati." #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "Nessun server disponibile" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "Nessun dispositivo aggiornabile" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "Nessun aggiornamento disponibile" msgid "No updates available for remaining devices" msgstr "Nessun aggiornamento disponibile per i dispositivi rimanenti" #. TRANSLATORS: version cannot be installed due to policy msgid "Not approved" msgstr "Non approvata" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "Non trovato" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "Non supportato" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "Fatto" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "Ok" #. TRANSLATORS: the firmware old version msgid "Old version" msgstr "Versione precedente" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "Mostra solo il valore PCR" #. TRANSLATORS: command line option msgid "Only use peer-to-peer networking when downloading files" msgstr "Usa solo reti peer-to-peer per scaricare i file" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "Sono consentiti solo avanzamenti di versione" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "Sovrascrive il percorso ESP predefinito" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Firmware" msgstr "Firmware P2P" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Metadata" msgstr "Metadati P2P" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "PERCORSO" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "Legge e mostra i dettagli riguardo a un file firmware" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "Lettura aggiornamento dbx…" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "Lettura dbx di sistema…" #. TRANSLATORS: remote filename base msgid "Password" msgstr "Password" #. TRANSLATORS: command description msgid "Patch a firmware blob at a known offset" msgstr "Esegue una patch di un blob firmware a un offset noto" msgid "Payload" msgstr "Carico" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "In attesa" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "Eseguire l'operazione?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "Debug piattaforma" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "Debug piattaforma" #. TRANSLATORS: 'recovery key' here refers to a code, rather than a physical #. metal thing msgid "Please ensure you have the volume recovery key before continuing." msgstr "Assicurarsi di avere la chiave di ripristino prima di continuare." #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "Inserire un numero tra 0 e %u:" #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "Dipendenze plugin mancanti" #. TRANSLATORS: Possible values for a bios setting msgid "Possible Values" msgstr "Valori possibili" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA Protection" msgstr "Protezione DMA pre-boot" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "Protezione DMA pre-boot" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is disabled" msgstr "La protezione pre-boot DMA non è abilitata" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is enabled" msgstr "La protezione pre-boot DMA è abilitata" #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "Versione precedente" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Priorità" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "Errori" msgid "Proceed with upload?" msgstr "Procedere con il caricamento?" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Processor Security Checks" msgstr "Controlli di sicurezza del processore" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "Processor rollback protection" msgstr "Protezione rollback del processore" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "Proprietaria" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "ID-REMOTO" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID KEY VALUE" msgstr "ID-REMOTO CHIAVE VALORE" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "Sola lettura" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "Legge un blob firmware da un dispositivo" #. TRANSLATORS: command description msgid "Read a firmware from a device" msgstr "Legge un firmware da un dispositivo" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "Lettura da %s…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Lettura…" #. TRANSLATORS: how often we should refresh the metadata msgid "Refresh Interval" msgstr "Intervallo aggiornamento" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "Ricarica i metadati dal server remoto" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "Reinstallare %s con %s?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "Installa nuovamente il firmware attuale sul dispositivo" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "Installa nuovamente il firmware su un dispositivo" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "Ramo di rilascio" #. TRANSLATORS: release attributes msgid "Release Flags" msgstr "Flag di rilascio" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "ID release" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "ID remoto" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "URI del rapporto" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "Segnalato al server remoto" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "Il file system richiesto efivarfs non è stato trovato" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "L'hardware richiesto non è stato trovato" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "Richiede un bootloader" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Richiede una connessione a Internet" msgid "Reset daemon configuration" msgstr "Resetta la configurazione del demone" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Riavviare ora?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "Riavviare il demone per applicare le modifiche?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Riavvio del dispositivo…" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "Fornisce tutti gli ID hardware per un dispositivo" #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "Esegue la procedura di pulizia del plugin quando si utilizza install-blob" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "Esegue la procedura di preparazione del plugin quando si utilizza install-blob" #. TRANSLATORS: tell a user how to get information #, c-format msgid "Run without '%s' to see" msgstr "Eseguire senza «%s» per vedere" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "La versione del kernel in esecuzione è troppo vecchia" #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "Suffisso di runtime" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING VALUE" msgstr "IMPOSTAZIONE VALORE" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "Descrittore BIOS SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "Regione BIOS SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "Blocco SPI" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "SPI replay protection" msgstr "Protezione replay SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "Scrittura SPI" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "SPI write protection" msgstr "Protezione scrittura SPI" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "SOTTOSISTEMA DRIVER [ID-DISPOSITIVO|GUID]" #. TRANSLATORS: command description msgid "Save a file that allows generation of hardware IDs" msgstr "Salva una file che consente di generare ID hardware" #. TRANSLATORS: Scalar increment for integer BIOS setting msgid "Scalar Increment" msgstr "Incremento scalare" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Pianificazione…" #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "Secure boot disabilitato" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "Secure boot abilitato" #. TRANSLATORS: the %1 is a URL to a wiki page #, c-format msgid "See %s for more details." msgstr "Per maggiori informazioni, consultare %s." #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "Per maggiori informazioni, consultare %s." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Dispositivo selezionato" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Volume selezionato" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "Numero di serie" #. TRANSLATORS: Configured a BIOS setting to a value #, c-format msgid "Set BIOS setting '%s' using '%s'." msgstr "Configura l'impostazione BIOS «%s» utilizzando «%s»." #. TRANSLATORS: command description msgid "Set a BIOS setting" msgstr "Imposta un parametro del BIOS" msgid "Set one or more BIOS settings" msgstr "Configura una o più impostazioni BIOS" #. TRANSLATORS: command description msgid "Sets one or more BIOS settings" msgstr "Configura una o più impostazioni BIOS" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Imposta l'elenco dei firmware approvati" #. TRANSLATORS: type of BIOS setting msgid "Setting type" msgstr "Tipo di impostazione" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "Le impostazioni saranno applicate dopo il riavvio" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "Condivide la cronologia del firmware con gli sviluppatori" #. TRANSLATORS: command line option msgid "Show all results" msgstr "Mostra tutti i risultati" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "Mostra la versione del client e del demone" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "Mostra informazioni prolisse del demone per un dominio" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "Mostra informazioni di debug per tutti i domini" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Mostra le opzioni di debug" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Mostra dispositivi non aggiornabili" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Mostra maggiori informazioni di debug" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "Mostra la cronologia degli aggiornamenti firmware" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "Mostra la versione calcolata del dbx" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "Spegnere ora?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "Firma un firmware con una chiave nuova" msgid "Sign data using the client certificate" msgstr "Firma i dati col certificato del client" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "Firma i dati col certificato del client" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "Firma i dati caricati col certificato del client" msgid "Signature" msgstr "Firma" #. TRANSLATORS: firmware is verified on-device the payload using strong crypto msgid "Signed Payload" msgstr "Dati firmati" #. TRANSLATORS: file size of the download msgid "Size" msgstr "Dimensione" #. TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM msgid "Some of the platform secrets may be invalidated when updating this firmware." msgstr "Alcuni dei segreti della piattaforma potrebbero essere invalidati durante l'aggiornamento del firmware" #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "Sorgente" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "Indica il file del database dbx" #. TRANSLATORS: The BIOS setting accepts strings msgid "String" msgstr "Stringa" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "Completato" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "Attivazione di tutti i dispositivi avvenuta con successo" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "Remoto disabilitato con successo" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "Nuovi metadati scaricati con successo:" #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "Remoto abilitato e aggiornato con successo" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "Remoto abilitato con successo" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "Firmware installato con successo" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "Valore della configurazione modificato con successo" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "Remoto modificato con successo" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "Metadati aggiornati manualmente con successo" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "Codici di controllo del dispositivo aggiornati con successo" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "%u rapporto caricato con successo" msgstr[1] "%u rapporti caricati con successo" msgstr[2] "%u rapporti caricati con successo" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "Codici di controllo del dispositivo verificati con successo" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Riepilogo" #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "Supportato" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "CPU supportata" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "Supportato sul server remoto" #. TRANSLATORS: Title: a better sleep state msgid "Suspend To Idle" msgstr "Sospensione" #. TRANSLATORS: Title: sleep state msgid "Suspend To RAM" msgstr "Sospensione in RAM" #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "Suspend-to-idle" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "Suspend-to-ram" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "Passare dal ramo %s a %s?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "Cambia il ramo del firmware sul dispositivo" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "Il sistema richiede una sorgente elettrica esterna" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "TESTO" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "Ricostruzione PCR0 TPM" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is invalid" msgstr "La ricostruzione PCR0 TPM non è valida" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is now valid" msgstr "La ricostruzione PCR0 TPM è ora valida" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM Platform Configuration" msgstr "Configurazione piattaforma TPM" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM Reconstruction" msgstr "Ricostruzione TPM" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM empty PCRs" msgstr "PCR TPM vuoti" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "Tag" msgstr[1] "Tag" msgstr[2] "Tag" #. TRANSLATORS: we're saving all USB events for emulation msgid "Tagged for emulation" msgstr "Marcato per l'emulazione" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "Non integro" #. show the user the entire data blob msgid "Target" msgstr "Obiettivo" #. TRANSLATORS: command description msgid "Test a device using a JSON manifest" msgstr "Verifica un dispositivo usando un manifesto JSON" #. TRANSLATORS: when the release was tested msgid "Tested" msgstr "Testato" #. TRANSLATORS: the %s is a vendor name, e.g. Lenovo #, c-format msgid "Tested by %s" msgstr "Testato da %s" #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "LVFS è un servizio gratuito che opera come entità legale indipendente e non ha alcun legame con $OS_RELEASE:NAME$. Il distributore potrebbe non aver verificato la compatibilità degli aggiornamenti firmware col proprio sistema o con i propri dispositivi collegati. Il firmware viene fornito solamente dall'OEM." #. TRANSLATORS: this is more background on a security measurement problem msgid "The TPM PCR0 differs from reconstruction." msgstr "TPM PCR0 è diverso dalla ricostruzione." #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "Il demone ha caricato codice di terze parti e non è più supportato dagli sviluppatori." #. TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name #, c-format msgid "The firmware from %s is not supplied by %s, the hardware vendor." msgstr "Il firmware su %s non è fornito da %s, il fornitore dell'hardware." #. TRANSLATORS: try to help msgid "The system clock has not been set correctly and downloading files may fail." msgstr "L'orologio di sistema non è stato impostato correttamente e lo scaricamento di file potrebbe non riuscire." #. TRANSLATORS: naughty vendor msgid "The vendor did not supply any release notes." msgstr "Il produttore non ha fornito note di rilascio." #. TRANSLATORS: now list devices with unfixed high-priority issues msgid "There are devices with issues:" msgstr "Dispositivi con problemi:" #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "Non ci sono file di firmware bloccati" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "Non ci sono firmware approvati." #. TRANSLATORS: %1 is the current device version number, and %2 is the #. command name, e.g. `fwupdmgr sync` #, c-format msgid "This device will be reverted back to %s when the %s command is performed." msgstr "Questo dispositivo verrà retrocesso alla versione %s all'esecuzione del comando %s. " #. TRANSLATORS: the vendor did not upload this msgid "This firmware is provided by LVFS community members and is not provided (or supported) by the original hardware vendor." msgstr "Questo firmware è fornito dalla comunità LVFS e non viene fornito o supportato dal produttore originale dell'hardware." #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "Questo pacchetto non è stato verificato, potrebbe non funzionare correttamente." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "Questo programma può funzionare correttamente solo come utente root" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "Questo remoto contiene firmware che non è bloccato, ma è ancora in fase di verifica dal produttore hardware. Assicurarsi di poter ripristinare, manualmente o con altre procedure, il vecchio firmware nel caso in cui l'aggiornamento non riuscisse." #. TRANSLATORS: error message msgid "This system doesn't support firmware settings" msgstr "Questo sistema non supporta le configurazioni del firmware" #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "Questo sistema presenta dei problemi di runtime HSI." #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "Questo sistema ha un livello di sicurezza HSI basso." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "Questo strumento consente a un amministratore di applicare aggiornamenti UEFI dbx." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "Questo strumento consente a un amministratore di inviare richieste e controllare il demone fwupd per eseguire azioni come l'installazione o la retrocessione del firmware." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "Questo strumento consente a un amministratore di utilizzare i plugin fwupd senza che siano installati sul sistema host." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "Questo strumento legge e analizza il registro eventi TPM dal firmware di sistema." #. TRANSLATORS: the update state of the specific device msgid "Transient failure" msgstr "Errore transitorio" #. TRANSLATORS: item is TRUE msgid "True" msgstr "Vero" #. TRANSLATORS: We verified the metadata against the server msgid "Trusted metadata" msgstr "Metadati verificati" #. TRANSLATORS: We verified the payload against the server msgid "Trusted payload" msgstr "Dati verificati" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Tipo" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI Bootservice Variables" msgstr "Variabili bootservice UEFI" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "Partizione ESP UEFI non rilavata o non configurata" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "Chiave piattaforma UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "Avvio sicuro UEFI" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI bootservice variables" msgstr "Variabili bootservice UEFI" #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "Aggiornamenti capsula UEFI non disponibili o non abilitati nella configurazione firmware" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "Strumento EUFI dbx" #. TRANSLATORS: system is not booted in UEFI mode msgid "UEFI firmware can not be updated in legacy BIOS mode" msgstr "Impossibile aggiornare il firmware UEFI in modalità BIOS legacy" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "Chiave piattaforma UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "Avvio sicuro UEFI" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "Impossibile collegarsi al servizio" #. TRANSLATORS: error message msgid "Unable to find attribute" msgstr "Impossibile trovare l'attributo" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "Svincola il driver attuale" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "Sblocco del firmware:" #. TRANSLATORS: command description msgid "Unblocks a specific firmware from being installed" msgstr "Sblocca un firmware specifico dal non essere installato" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "Non cifrato" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Sconosciuto" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Dispositivo sconosciuto" msgid "Unlock the device to allow access" msgstr "Sblocco del dispositivo per consentire l'accesso" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "Sbloccato" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "Sblocca il dispositivo per accedere al firmware" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "Smonta l'ESP" #. TRANSLATORS: firmware payload is unsigned and it is possible to modify it msgid "Unsigned Payload" msgstr "Dati non firmati" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "Demone versione %s non supportato, la versione del client è %s" #. TRANSLATORS: Suffix: the HSI result msgid "Untainted" msgstr "Integro" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "Aggiornabile" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Errore aggiornamento" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "Aggiorna immagine" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "Messaggio aggiornamento" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "Stato aggiornamento" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "Questo è un problema noto, consultare il seguente URL per maggiori informazioni:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Aggiornare ora?" #. TRANSLATORS: command description msgid "Update the stored cryptographic hash with current ROM contents" msgstr "Aggiorna l'hash crittografico archiviato con il contenuto della ROM" msgid "Update the stored device verification information" msgstr "Aggiornamento delle informazioni di verifica del dispositivo salvate" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "Aggiorna i metadati salvati con il contenuto attuale" #. TRANSLATORS: command description msgid "Updates all specified devices to latest firmware version, or all devices if unspecified" msgstr "Aggiorna tutti i dispositivi specificati all'ultima versione firmware o tutti i dispositivi se non specificato" msgid "Updating" msgstr "Aggiornamento in corso" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "Aggiornamento di %s…" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "Aggiornare %s da %s a %s?" #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "Inviare resoconti sul firmware aiuta gli sviluppatori a identificare velocemente aggiornamenti eseguiti con successo o non riusciti su dispositivi reali." #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "Urgenza" #. TRANSLATORS: explain how to get help, %1 is #. * 'fwupdtool --help' #. TRANSLATORS: explain how to get help, #. * where $1 is something like 'fwupdmgr --help' #, c-format msgid "Use %s for help" msgstr "Usare %s per l'aiuto" #. TRANSLATORS: CTRL^C [holding control, and then pressing C] will exit the #. program msgid "Use CTRL^C to cancel." msgstr "Usare CTRL^C per annullare." #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "Notifica inviata all'utente" #. TRANSLATORS: remote filename base msgid "Username" msgstr "Nome utente" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "Valido" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "Verifica contenuti ESP…" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "Variante" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "Fornitore" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "Verifica…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Versione" #. TRANSLATORS: the fwupd version the release was tested on msgid "Version[fwupd]" msgstr "Versione[fwupd]" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "AVVERTENZA" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "Attesa…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "Controlla le modifiche hardware" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "Scrittura file:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Scrittura…" #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "La propria distribuzione potrebbe non aver verificato la compatibilità degli aggiornamenti firmware col proprio sistema o col dispositivo collegato." #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "Utilizzando questo firmware, l'hardware potrebbe danneggiarsi; inoltre, l'installazione di questa versione potrebbe annullare la garanzia con %s." #. TRANSLATORS: BKC is the industry name for the best known configuration and #. is a set #. * of firmware that works together #, c-format msgid "Your system is set up to the BKC of %s." msgstr "Il sistema è impostato per il BKC di %s." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[CHECKSUM]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[ID-DISPOSITIVO|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[ID-DISPOSITIVO|GUID] [BRANCH]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [VERSION]" msgstr "[ID|GUID-DISPOSITIVO] [VERSIONE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILE FILE_SIG REMOTE-ID]" msgstr "[FILE SIG_FILE ID-REMOTO]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[NOMEFILE1] [NOMEFILE2]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[FILE-SMBIOS|FILE-HWIDS]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "predefinito" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "Strumento registro eventi TPM fwupd" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "Plugin fwupd" fwupd-2.0.10/po/its/000077500000000000000000000000001501337203100141275ustar00rootroot00000000000000fwupd-2.0.10/po/its/appdata.its000066400000000000000000000013501501337203100162610ustar00rootroot00000000000000 fwupd-2.0.10/po/its/appdata.loc000066400000000000000000000005121501337203100162360ustar00rootroot00000000000000 fwupd-2.0.10/po/ja.po000066400000000000000000000206201501337203100142620ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Green , 2021 # Nobuhiro Iwamatsu , 2021 # Ryo Nakano, 2024 # Takuro Onoue , 2021 # YOSHIDUMI, Rentaro, 2021 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Japanese (http://app.transifex.com/freedesktop/fwupd/language/ja/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: ja\n" "Plural-Forms: nplurals=1; plural=0;\n" msgid "Activate the new firmware on the device" msgstr "æ©Ÿå™¨ä¸Šã§æ–°ã—ã„ファームウェアを実行å¯èƒ½ã«ã™ã‚‹" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "%sã®åˆ¥å" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "å¤–ä»˜ã‘æ©Ÿå™¨ã§ãƒ•ァームウェアをダウングレードã™ã‚‹ã«ã¯èªè¨¼ãŒå¿…è¦ã§ã™" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "ã“ã®æ©Ÿå™¨ã®ãƒ•ァームウェアをダウングレードã™ã‚‹ã«ã¯èªè¨¼ãŒå¿…è¦ã§ã™" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "ファームウェア更新用ã®é é𔿧‹æˆã‚’変更ã™ã‚‹ã«ã¯èªè¨¼ãŒå¿…è¦ã§ã™" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "デーモン構æˆã®å¤‰æ›´ã«ã¯èªè¨¼ãŒå¿…è¦ã§ã™" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to reset daemon configuration to defaults" msgstr "デーモン構æˆã‚’デフォルトã«ãƒªã‚»ãƒƒãƒˆã™ã‚‹ã«ã¯èªè¨¼ãŒå¿…è¦ã§ã™" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "èªè¨¼æ¸ˆã¿ãƒ•ァームウェアã®ä¸€è¦§ã‚’設定ã™ã‚‹ã«ã¯èªè¨¼ãŒå¿…è¦ã§ã™" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "クライアント証明書を用ã„ã¦ãƒ‡ãƒ¼ã‚¿ã«ç½²åã™ã‚‹ã«ã¯èªè¨¼ãŒå¿…è¦ã§ã™" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to stop the firmware update service" msgstr "ãƒ•ã‚¡ãƒ¼ãƒ ã‚¦ã‚§ã‚¢æ›´æ–°ã‚µãƒ¼ãƒ“ã‚¹ã‚’åœæ­¢ã™ã‚‹ã«ã¯èªè¨¼ãŒå¿…è¦ã§ã™" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "新版ã®ãƒ•ァームウェアã«åˆ‡ã‚Šæ›¿ãˆã‚‹ã«ã¯èªè¨¼ãŒå¿…è¦ã§ã™" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "機器ã®ãƒ­ãƒƒã‚¯ã‚’解除ã™ã‚‹ã«ã¯èªè¨¼ãŒå¿…è¦ã§ã™" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "å¤–ä»˜ã‘æ©Ÿå™¨ã§ãƒ•ァームウェアを更新ã™ã‚‹ã«ã¯èªè¨¼ãŒå¿…è¦ã§ã™" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "当機ã§ãƒ•ァームウェアを更新ã™ã‚‹ã«ã¯èªè¨¼ãŒå¿…è¦ã§ã™" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "ä¿å­˜ã•ã‚ŒãŸæ©Ÿå™¨ã®æ¤œæŸ»åˆè¨ˆã‚’æ›´æ–°ã™ã‚‹ã«ã¯èªè¨¼ãŒå¿…è¦ã§ã™" #. TRANSLATORS: error message msgid "Command not found" msgstr "コマンドãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“" msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "ã“ã®æ©Ÿèƒ½ã‚’有効ã«ã™ã‚‹ã“ã¨ã¯ã€ãŠå®¢æ§˜è‡ªèº«ã®è²¬ä»»ã§è¡Œã£ã¦ãã ã•ã„。ã¤ã¾ã‚Šã€ã“ã‚Œã‚‰ã®æ›´æ–°ã«ã‚ˆã£ã¦ç™ºç”Ÿã—ãŸå•題ã«ã¤ã„ã¦ã¯ã€è£½é€ å…ƒã®ãƒ¡ãƒ¼ã‚«ãƒ¼ã«é€£çµ¡ã™ã‚‹å¿…è¦ãŒã‚りã¾ã™ã€‚更新プロセス自体ã«é–¢ã™ã‚‹å•題ã ã‘ã‚’ $OS_RELEASE:BUG_REPORT_URL$ ã¸æå‡ºã—ã¦ãã ã•ã„。" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "ファイルå" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "引数ã®è§£æžã«å¤±æ•—ã—ã¾ã—ãŸ" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "ファイルã®è§£æžã«å¤±æ•—ã—ã¾ã—ãŸ" msgid "Install old version of signed system firmware" msgstr "旧版ã®ç½²å済ã¿ã‚·ã‚¹ãƒ†ãƒ ãƒ•ァームウェアを導入ã™ã‚‹" msgid "Install old version of unsigned system firmware" msgstr "旧版ã®ç½²åã®ãªã„システムファームウェアを導入ã™ã‚‹" msgid "Install signed device firmware" msgstr "ç½²åæ¸ˆã¿ãƒ‡ãƒã‚¤ã‚¹ãƒ•ァームウェアを導入ã™ã‚‹" msgid "Install signed system firmware" msgstr "ç½²åæ¸ˆã¿ã‚·ã‚¹ãƒ†ãƒ ãƒ•ァームウェアを導入ã™ã‚‹" msgid "Install unsigned device firmware" msgstr "ç½²åã®ãªã„デãƒã‚¤ã‚¹ãƒ•ァームウェアを導入ã™ã‚‹" msgid "Install unsigned system firmware" msgstr "ç½²åã®ãªã„システムファームウェアを導入ã™ã‚‹" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "ãƒ•ã‚¡ãƒ¼ãƒ ã‚¦ã‚§ã‚¢ã®æ›´æ–°ã‚’インストールã—ã¦ã„ã¾ã™â€¦" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Linux ベンダーファームウェアサービス (安定版ファームウェア)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Linux ベンダーファームウェアサービス (試験版ファームウェア)" msgid "Modify a configured remote" msgstr "é é𔿧‹æˆã‚’変更ã™ã‚‹" msgid "Modify daemon configuration" msgstr "デーモン構æˆã‚’変更ã™ã‚‹" msgid "Reset daemon configuration" msgstr "デーモン構æˆã‚’リセットã™ã‚‹" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "èªè¨¼æ¸ˆã¿ãƒ•ァームウェアã®ä¸€è¦§ã‚’設定ã™ã‚‹" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "追加ã®ãƒ‡ãƒãƒƒã‚°æƒ…報を表示ã™ã‚‹" msgid "Sign data using the client certificate" msgstr "クライアント証明書を用ã„ã¦ãƒ‡ãƒ¼ã‚¿ã«ç½²åã™ã‚‹" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "クライアント証明書を用ã„ã¦ãƒ‡ãƒ¼ã‚¿ã«ç½²åã™ã‚‹" msgid "Stop the fwupd service" msgstr "fwupd ã‚µãƒ¼ãƒ“ã‚¹ã‚’åœæ­¢ã™ã‚‹" #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "LVFS ã¯ç‹¬ç«‹ã—ãŸæ³•人ã¨ã—ã¦æ©Ÿèƒ½ã™ã‚‹ç„¡æ–™ã®ã‚µãƒ¼ãƒ“スã§ã‚りã€$OS_RELEASE:NAME$ ã¨ã¯é–¢ä¿‚ã‚りã¾ã›ã‚“。ディストリビューターã¯ã€ã‚·ã‚¹ãƒ†ãƒ ã¾ãŸã¯æŽ¥ç¶šã•れã¦ã„るデãƒã‚¤ã‚¹ã¨ã®äº’æ›æ€§ã«ã¤ã„ã¦ã€ãƒ•ã‚¡ãƒ¼ãƒ ã‚¦ã‚§ã‚¢ã®æ›´æ–°ã‚’確èªã—ã¦ã„ãªã„å¯èƒ½æ€§ãŒã‚りã¾ã™ã€‚ã™ã¹ã¦ã®ãƒ•ァームウェアã¯ã€å…ƒã®æ©Ÿå™¨ã®è£½é€ å…ƒã‹ã‚‰ã®ã¿æä¾›ã•れã¦ã„ã¾ã™ã€‚" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "ã“ã®é éš”å´ã‚½ãƒ¼ã‚¹ã«ã¯ã€è¼¸å‡ºç¦æ­¢ã§ã¯ãªã„ファームウェアãŒå«ã¾ã‚Œã¦ã„ã¾ã™ãŒã€ãƒãƒ¼ãƒ‰ã‚¦ã‚§ã‚¢ãƒ™ãƒ³ãƒ€ãƒ¼ã«ã‚ˆã£ã¦æœªã ãƒ†ã‚¹ãƒˆä¸­ã§ã™ã€‚ãƒ•ã‚¡ãƒ¼ãƒ ã‚¦ã‚§ã‚¢ã®æ›´æ–°ã«å¤±æ•—ã—ãŸå ´åˆã¯ã€ãƒ•ァームウェアを手動ã§ãƒ€ã‚¦ãƒ³ã‚°ãƒ¬ãƒ¼ãƒ‰ã™ã‚‹æ–¹æ³•を確ä¿ã—ã¦ãŠãå¿…è¦ãŒã‚りã¾ã™ã€‚" msgid "Unlock the device to allow access" msgstr "アクセスを許å¯ã™ã‚‹ã«ã¯æ©Ÿå™¨ã®ãƒ­ãƒƒã‚¯ã‚’解除ã—ã¦ãã ã•ã„" msgid "Update the stored device verification information" msgstr "ä¿å­˜ã•ã‚ŒãŸæ©Ÿå™¨ã®æ¤œè¨¼æƒ…報を更新ã™ã‚‹" fwupd-2.0.10/po/ka.po000066400000000000000000001450441501337203100142730ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Georgian (http://app.transifex.com/freedesktop/fwupd/language/ka/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: ka\n" "Plural-Forms: nplurals=2; plural=(n!=1);\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "დáƒáƒ áƒ©áƒ”ნილირ%.0f წუთი" msgstr[1] "დáƒáƒ áƒ©áƒ”ნილირ%.0f წუთი" #. TRANSLATORS: BMC refers to baseboard management controller which #. * is the device that updates all the other firmware on the system #, c-format msgid "%s BMC Update" msgstr "%s BMC -ის გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "%s ელემენტის გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "%s CPU მიკრáƒáƒ™áƒáƒ“ის გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "%s კáƒáƒ›áƒ”რის გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "%s კáƒáƒœáƒ¤áƒ˜áƒ’ურáƒáƒªáƒ˜áƒ˜áƒ¡ გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "%s მáƒáƒ›áƒ®áƒ›áƒáƒ áƒ”ბლის ME-ის გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "%s კáƒáƒœáƒ¢áƒ áƒáƒšáƒ”რის გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "%s კáƒáƒ áƒžáƒáƒ áƒáƒ¢áƒ˜áƒ£áƒšáƒ˜ ME-ის გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "%s მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: Video Display refers to the laptop internal display or #. * external monitor #, c-format msgid "%s Display Update" msgstr "%s ეკრáƒáƒœáƒ˜áƒ¡ გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: Dock refers to the port replicator hardware laptops are #. * cradled in, or lowered onto #, c-format msgid "%s Dock Update" msgstr "%s დáƒáƒ™áƒ˜áƒ¡ გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: drive refers to a storage device, e.g. SATA disk #, c-format msgid "%s Drive Update" msgstr "%s დისკის გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "%s ჩáƒáƒ“გმული კáƒáƒœáƒ¢áƒ áƒáƒšáƒ”რის გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: flash refers to solid state storage, e.g. UFS or eMMC #, c-format msgid "%s Flash Drive Update" msgstr "%s Flash დისკის გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: GPU refers to a Graphics Processing Unit, e.g. #. * the "video card" #, c-format msgid "%s GPU Update" msgstr "%s GPU -ის გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "%s კლáƒáƒ•იáƒáƒ¢áƒ£áƒ áƒ˜áƒ¡ გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "%s ME-ის გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "%s თáƒáƒ’უნáƒáƒ¡ გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "%s ქსელის ინტერფეისის გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: SSD refers to a Solid State Drive, e.g. non-rotating #. * SATA or NVMe disk #, c-format msgid "%s SSD Update" msgstr "%s SSD -ის გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "%s სáƒáƒªáƒáƒ•ის კáƒáƒœáƒ¢áƒ áƒáƒšáƒ”რის გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "%s სისტემის გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "%s TPM გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "%s Thunderbolt კáƒáƒœáƒ¢áƒ áƒáƒšáƒ”რის გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "%s თáƒáƒ©áƒžáƒ”დის გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: Dock refers to the port replicator device connected #. * by plugging in a USB cable -- which may or may not also provide power #, c-format msgid "%s USB Dock Update" msgstr "%s USB დáƒáƒ™áƒ˜áƒ¡ გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth #. * device that stays in the USB port so the wireless peripheral works #, c-format msgid "%s USB Receiver Update" msgstr "%s USB მიმღების გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "%s გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: the device has a reason it can't update, e.g. laptop lid #. closed #, c-format msgid "%s is not currently updatable" msgstr "%s áƒáƒ›áƒŸáƒáƒ›áƒáƒ“ გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒáƒ“ი áƒáƒ áƒáƒ" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "%s დáƒáƒ›áƒ–áƒáƒ“ების რეჟიმი" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "%s გáƒáƒ“áƒáƒ¤áƒáƒ áƒ•áƒ" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "%s ვერსიáƒ" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u დღე" msgstr[1] "%u დღე" #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u სáƒáƒáƒ—ი" msgstr[1] "%u სáƒáƒáƒ—ი" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minute" msgstr[1] "%u minutes" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u წáƒáƒ›áƒ˜" msgstr[1] "%u წáƒáƒ›áƒ˜" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(მáƒáƒ«áƒ•ელებული)" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "AMD Firmware Replay Protection" msgstr "AMD-ის მიკრáƒáƒ™áƒáƒ“ის თáƒáƒ•იდáƒáƒœ ჩáƒáƒ¬áƒ”რისგáƒáƒœ დáƒáƒªáƒ•áƒ" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "AMD Firmware Write Protection" msgstr "AMD-ის მიკრáƒáƒ™áƒáƒ“ში ჩáƒáƒ¬áƒ”რისგáƒáƒœ დáƒáƒªáƒ•áƒ" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "სáƒáƒ­áƒ˜áƒ áƒ ქმედებáƒ:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ების გáƒáƒáƒ¥áƒ¢áƒ˜áƒ£áƒ áƒ”ბáƒ" msgid "Activate the new firmware on the device" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒáƒ–ე áƒáƒ®áƒáƒšáƒ˜ მიკრáƒáƒ™áƒáƒ“ის áƒáƒ¥áƒ¢áƒ˜áƒ•áƒáƒªáƒ˜áƒ" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "მიკრáƒáƒ™áƒáƒ“ის გáƒáƒœáƒáƒ®áƒšáƒ”ბის áƒáƒ¥áƒ¢áƒ˜áƒ•áƒáƒªáƒ˜áƒ" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "áƒáƒ¡áƒáƒ™áƒ˜" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "%s-ის მეტსáƒáƒ®áƒ”ლი" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "ერთი ტიპის მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ების ერთდრáƒáƒ£áƒšáƒáƒ“ გáƒáƒœáƒáƒ®áƒšáƒ“ებáƒ" #. TRANSLATORS: is not the main firmware stream msgid "Alternate branch" msgstr "áƒáƒšáƒ¢áƒ”რნáƒáƒ¢áƒ˜áƒ£áƒšáƒ˜ ბრენჩი" #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "ყველრკითხვისთვის დáƒáƒ“ებითი პáƒáƒ¡áƒ£áƒ®áƒ˜áƒ¡ გáƒáƒªáƒ”მáƒ" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "გáƒáƒœáƒáƒ®áƒšáƒ”ბის ფáƒáƒšáƒ”ბის გáƒáƒ“áƒáƒ¢áƒáƒ áƒ”ბáƒ" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "გáƒáƒœáƒáƒ®áƒšáƒ”ბის გáƒáƒ“áƒáƒ¢áƒáƒ áƒ”ბáƒâ€¦" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "áƒáƒ•თენტიკáƒáƒªáƒ˜áƒâ€¦" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "ელემენტი" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "დáƒáƒ‘ლáƒáƒ™áƒ˜áƒšáƒ˜ ვერსიáƒ" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "ჩáƒáƒ›áƒ¢áƒ•ირთáƒáƒ•ი კáƒáƒ“ის ვერსიáƒ" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "ბრენჩი" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "გáƒáƒ£áƒ¥áƒ›áƒ”ბáƒ" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "შეწყვეტილიáƒ" #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "შეცვლილიáƒ" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "სáƒáƒ™áƒáƒœáƒ¢áƒ áƒáƒšáƒ რიცხვი" #. TRANSLATORS: get interactive prompt, where branch is the #. * supplier of the firmware, e.g. "non-free" or "free" msgid "Choose branch" msgstr "áƒáƒ˜áƒ áƒ©áƒ˜áƒ”თ ბრენჩი" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "áƒáƒ˜áƒ áƒ©áƒ˜áƒ”თ მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒ" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "áƒáƒ˜áƒ áƒ©áƒ˜áƒ”თ მიკრáƒáƒ™áƒáƒ“ი" #. TRANSLATORS: get interactive prompt msgid "Choose release" msgstr "áƒáƒ˜áƒ áƒ©áƒ˜áƒ”თ რელიზი" #. TRANSLATORS: get interactive prompt msgid "Choose volume" msgstr "áƒáƒ˜áƒ áƒ©áƒ˜áƒ”თ ტáƒáƒ›áƒ˜" #. TRANSLATORS: error message msgid "Command not found" msgstr "ბრძáƒáƒœáƒ”ბრვერ ვიპáƒáƒ•ე" #. TRANSLATORS: is not supported by the vendor msgid "Community supported" msgstr "სáƒáƒ–გáƒáƒ“áƒáƒ”ბის მიერ მხáƒáƒ áƒ“áƒáƒ­áƒ”რილი" #. TRANSLATORS: when the update was built msgid "Created" msgstr "შექმნილიáƒ" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "კრიტიკული" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "ხელმისáƒáƒ¬áƒ•დáƒáƒ›áƒ˜áƒ ჰეშის კრიპტáƒáƒ’რáƒáƒ¤áƒ˜áƒ£áƒšáƒ˜ შემáƒáƒ¬áƒ›áƒ”ბáƒ" #. TRANSLATORS: current value of a BIOS setting msgid "Current Value" msgstr "მიმდინáƒáƒ áƒ” მნიშვნელáƒáƒ‘áƒ" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "მიმდინáƒáƒ áƒ” ვერსიáƒ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "DEVICE-ID|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "გáƒáƒ›áƒáƒ áƒ—ვის პáƒáƒ áƒáƒ›áƒ”ტრები" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "გáƒáƒ¨áƒšáƒâ€¦" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "áƒáƒ¦áƒ¬áƒ”რილáƒáƒ‘áƒ" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "დეტáƒáƒšáƒ”ბი" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის áƒáƒšáƒ›áƒ”ბი" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის ID" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘რდáƒáƒ”მáƒáƒ¢áƒ:" #. TRANSLATORS: the device is already connected msgid "Device already exists" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘რუკვე áƒáƒ áƒ¡áƒ”ბáƒáƒ‘ს" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის ელემენტის სიმძლáƒáƒ•რე მეტისმეტáƒáƒ“ დáƒáƒ‘áƒáƒšáƒ˜áƒ" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse #, c-format msgid "Device battery power is too low (%u%%, requires %u%%)" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის ელემენტის სიმძლáƒáƒ•რე მეტისმეტáƒáƒ“ დáƒáƒ‘áƒáƒšáƒ˜áƒ(%u%%, სáƒáƒ­áƒ˜áƒ áƒáƒ %u%%)" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒáƒ¡ შეუძლირფლეშის áƒáƒ•áƒáƒ áƒ˜áƒ˜áƒ¡ გáƒáƒ“áƒáƒ¢áƒáƒœáƒ" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘რშეიცვáƒáƒšáƒ:" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘რჩáƒáƒ™áƒ”ტილიáƒ" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘რმიუწვდáƒáƒ›áƒ”ლიáƒ" #. TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode msgid "Device is unreachable, or out of wireless range" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘რმიუწვდáƒáƒ›áƒ”ლირáƒáƒœ გáƒáƒ¡áƒ£áƒšáƒ˜áƒ უსáƒáƒ“ენრინტერნეტის წვდáƒáƒ›áƒ˜áƒ¡ áƒáƒ áƒ”áƒáƒšáƒ˜áƒ“áƒáƒœ" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘რგáƒáƒœáƒáƒ®áƒšáƒ”ბის დრáƒáƒ¡áƒáƒª გáƒáƒ›áƒáƒ§áƒ”ნებáƒáƒ“იáƒ" #. TRANSLATORS: usually this is when we're waiting for a reboot msgid "Device is waiting for the update to be applied" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘რგáƒáƒœáƒáƒ®áƒšáƒ”ბის დáƒáƒ¡áƒ áƒ£áƒšáƒ”ბáƒáƒ¡ ელáƒáƒ“ებáƒ" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘რწáƒáƒ¨áƒšáƒ˜áƒšáƒ˜áƒ:" #. TRANSLATORS: as in, wired mains power for a laptop msgid "Device requires AC power to be connected" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒáƒ¡ ესáƒáƒ­áƒ˜áƒ áƒáƒ”ბრ220V კვებáƒ" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის დáƒáƒœáƒ˜áƒ¡ გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒáƒ¡ áƒáƒ¥áƒ¢áƒ˜áƒ•áƒáƒªáƒ˜áƒ ესáƒáƒ­áƒ˜áƒ áƒáƒ”ბáƒ" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "გáƒáƒœáƒáƒ®áƒšáƒ”ბის დáƒáƒ¡áƒ áƒ£áƒšáƒ”ბის შემდეგ მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘რáƒáƒ¦áƒáƒ  გáƒáƒ›áƒáƒ©áƒœáƒ“ებáƒ" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "გáƒáƒ›áƒáƒ áƒ—ულიáƒ" #. TRANSLATORS: the OS the release was tested on msgid "Distribution" msgstr "გáƒáƒ•რცელებáƒ" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "გნებáƒáƒ•თ ეს დáƒáƒ¨áƒáƒ áƒ”ბული áƒáƒ®áƒšáƒ გáƒáƒœáƒáƒáƒ®áƒšáƒáƒ—?" #. TRANSLATORS: success msgid "Done!" msgstr "მზáƒáƒ“áƒáƒ!" #. TRANSLATORS: command description msgid "Download a file" msgstr "ფáƒáƒ˜áƒšáƒ˜áƒ¡ გáƒáƒ“მáƒáƒ¬áƒ”რáƒ" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "გáƒáƒ“მáƒáƒ¬áƒ”რáƒâ€¦" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "ხáƒáƒœáƒ’რძლáƒáƒ•áƒáƒ‘áƒ" #. TRANSLATORS: this device is not actually real msgid "Emulated" msgstr "ემულირებული" #. TRANSLATORS: Title: if we are emulating a different host msgid "Emulated host" msgstr "ემულირებული ჰáƒáƒ¡áƒ¢áƒ˜" msgid "Enable" msgstr "ჩáƒáƒ áƒ—ვáƒ" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "ჩáƒáƒ•რთრეს დáƒáƒ¨áƒáƒ áƒ”ბული?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "ჩáƒáƒ áƒ—ულიáƒ" #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "დáƒáƒ¨áƒ˜áƒ¤áƒ áƒ£áƒšáƒ˜" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "დáƒáƒ¨áƒ˜áƒ¤áƒ áƒ£áƒšáƒ˜ RAM" #. TRANSLATORS: the vendor is no longer supporting the device msgid "End of life" msgstr "ძáƒáƒšáƒ˜áƒáƒœ ძველი" #. TRANSLATORS: The BIOS setting can only be changed to fixed values msgid "Enumeration" msgstr "ჩáƒáƒ›áƒáƒœáƒáƒ—ვáƒáƒšáƒ˜" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "წáƒáƒ¨áƒšáƒâ€¦" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "გáƒáƒ›áƒáƒ¡áƒ•ლáƒáƒ›áƒ“ე მცირე დáƒáƒ§áƒáƒ•ნებáƒ" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "გáƒáƒ›áƒáƒ¡áƒ•ლრძრáƒáƒ•ის ჩáƒáƒ¢áƒ•ირთვის შემდეგ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "FILE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "ფáƒáƒ˜áƒšáƒ˜áƒ¡ სáƒáƒ®áƒ”ლი" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "FILENAME CERTIFICATE PRIVATE-KEY" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "FILENAME DEVICE-ID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME OFFSET DATA [FIRMWARE-TYPE]" msgstr "FILENAME OFFSET DATA [FIRMWARE-TYPE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "FILENAME [DEVICE-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "FILENAME [FIRMWARE-TYPE]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "შეცდáƒáƒ›áƒ" #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "დემáƒáƒœáƒ—áƒáƒœ მიერთებრჩáƒáƒ•áƒáƒ áƒ“áƒ" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "áƒáƒ áƒ’უმენტების დáƒáƒ›áƒ£áƒ¨áƒáƒ•ების შეცდáƒáƒ›áƒ" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "ფáƒáƒ˜áƒšáƒ˜áƒ¡ დáƒáƒ›áƒ£áƒ¨áƒáƒ•ების შეცდáƒáƒ›áƒ" #. TRANSLATORS: item is FALSE msgid "False" msgstr "მცდáƒáƒ áƒ˜" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "ფáƒáƒ˜áƒšáƒ˜áƒ¡ სáƒáƒ®áƒ”ლი" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "ფáƒáƒ˜áƒšáƒ˜áƒ¡ სáƒáƒ®áƒ”ლის ხელმáƒáƒ¬áƒ”რáƒ" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "ფáƒáƒ˜áƒšáƒ˜áƒ¡ სáƒáƒ®áƒ”ლის წყáƒáƒ áƒ" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "ფáƒáƒ˜áƒšáƒ˜áƒ¡ სáƒáƒ®áƒ”ლი áƒáƒ£áƒªáƒ˜áƒšáƒ”ბელიáƒ" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware Attestation" msgstr "მიკრáƒáƒ™áƒáƒ“ის áƒáƒ¢áƒ”სტáƒáƒªáƒ˜áƒ" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "მიკრáƒáƒ™áƒáƒ“ის BIOS-ის დესკრიპტáƒáƒ áƒ˜" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "მიკრáƒáƒ™áƒáƒ“ის BIOS-ის რეგიáƒáƒœáƒ˜" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "მიკრáƒáƒ™áƒáƒ“ის გáƒáƒ›áƒœáƒáƒ®áƒšáƒ”ბლის შემáƒáƒ¬áƒ›áƒ”ბáƒ" #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "მიკრáƒáƒ™áƒáƒ“ის გáƒáƒœáƒáƒ®áƒšáƒ”ბები" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "მიკრáƒáƒ™áƒáƒ“ის ჩáƒáƒ¬áƒ”რის დáƒáƒªáƒ•áƒ" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "მიკრáƒáƒ™áƒáƒ“ის ჩáƒáƒ¬áƒ”რის დáƒáƒªáƒ•ის დáƒáƒ‘ლáƒáƒ™áƒ•áƒ" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "მიკრáƒáƒ™áƒáƒ“ის áƒáƒ¢áƒ”სტáƒáƒªáƒ˜áƒ" #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "მიკრáƒáƒ™áƒáƒ“ის გáƒáƒœáƒáƒ®áƒšáƒ”ბები" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "áƒáƒšáƒ›áƒ”ბი" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "ნáƒáƒžáƒáƒ•ნი" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" msgstr[1] "GUIDs" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "GUID|DEVICE-ID" msgstr "GUID|DEVICE-ID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "HWIDS-FILE" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "áƒáƒžáƒáƒ áƒáƒ¢áƒ£áƒ áƒ თáƒáƒ•იდáƒáƒœ შეერთებáƒáƒ¡ ელáƒáƒ“ებáƒ" #. TRANSLATORS: the release urgency msgid "High" msgstr "მáƒáƒ¦áƒáƒšáƒ˜" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "ჰáƒáƒ¡áƒ¢áƒ˜áƒ¡ უსáƒáƒ¤áƒ áƒ—ხáƒáƒ”ბის ID:" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INHIBIT-ID" msgstr "INHIBIT-ID" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "IOMMU უსáƒáƒ¤áƒ áƒ—ხáƒáƒ”ბáƒ" #. TRANSLATORS: HSI event title msgid "IOMMU device protection disabled" msgstr "IOMMU მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის უსáƒáƒ¤áƒ áƒ—ხáƒáƒ”ბრგáƒáƒ›áƒáƒ áƒ—ულიáƒ" #. TRANSLATORS: HSI event title msgid "IOMMU device protection enabled" msgstr "IOMMU მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის უსáƒáƒ¤áƒ áƒ—ხáƒáƒ”ბრჩáƒáƒ áƒ—ულიáƒ" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "უქმე…" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "დáƒáƒ§áƒ”ნების დრáƒ" msgid "Install old version of unsigned system firmware" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის ხელმáƒáƒ£áƒ¬áƒ”რელი მიკრáƒáƒ™áƒáƒ“ის ძველი ვერსიის დáƒáƒ§áƒ”ნებáƒ" msgid "Install signed device firmware" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის ხელმáƒáƒ¬áƒ”რილი მიკრáƒáƒ™áƒáƒ“ის დáƒáƒ§áƒ”ნებáƒ" msgid "Install signed system firmware" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის ხელმáƒáƒ¬áƒ”რილი მიკრáƒáƒ™áƒáƒ“ის დáƒáƒ§áƒ”ნებáƒ" msgid "Install unsigned device firmware" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის ხელმáƒáƒ£áƒ¬áƒ”რელი მიკრáƒáƒ™áƒáƒ“ის დáƒáƒ§áƒ”ნებáƒ" msgid "Install unsigned system firmware" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის ხელმáƒáƒ£áƒ¬áƒ”რელი მიკრáƒáƒ™áƒáƒ“ის დáƒáƒ§áƒ”ნებáƒ" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "მიკრáƒáƒ™áƒáƒ“ის გáƒáƒœáƒáƒ®áƒšáƒ”ბის დáƒáƒ§áƒ”ნებáƒâ€¦" #. TRANSLATORS: The BIOS setting only accepts integers in a fixed range msgid "Integer" msgstr "მთელი რიცხვი" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM Protected" msgstr "Intel BootGuard ACM-ით დáƒáƒªáƒ£áƒšáƒ˜" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "Intel BootGuard ACM-ით დáƒáƒªáƒ£áƒšáƒ˜" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard Error Policy" msgstr "Intel BootGuard-ის შეცდáƒáƒ›áƒ”ბის პáƒáƒšáƒ˜áƒ¢áƒ˜áƒ™áƒ" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard Fuse" msgstr "Intel BootGuard-ის პáƒáƒ¢áƒ áƒ£áƒ¥áƒ˜" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "Intel BootGuard-ის OTP პáƒáƒ¢áƒ áƒ£áƒ¥áƒ˜" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard Verified Boot" msgstr "Intel BootGuard-ით შემáƒáƒ¬áƒ›áƒ”ბული ჩáƒáƒ¢áƒ•ირთვáƒ" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Intel BootGuard-ის შეცდáƒáƒ›áƒ”ბის პáƒáƒšáƒ˜áƒ¢áƒ˜áƒ™áƒ" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Intel BootGuard-ით შემáƒáƒ¬áƒ›áƒ”ბული ჩáƒáƒ¢áƒ•ირთვáƒ" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Manufacturing Mode" msgstr "Intel Management Engine -ის მწáƒáƒ áƒ›áƒáƒ”ბლის რეჟიმი" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the "override" is #. enabled #. * with a jumper -- luckily it is probably not accessible to end users on #. consumer #. * boards msgid "Intel Management Engine Override" msgstr "Intel Management Engine -ის გáƒáƒ“áƒáƒ¤áƒáƒ áƒ•áƒ" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Version" msgstr "Intel Management Engine -ის ვერსიáƒ" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "შიდრმáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒ" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ áƒáƒ áƒ’უმენტები" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "ვერსირჩáƒáƒ›áƒáƒ˜áƒ¬áƒ”ვáƒ" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "ჩáƒáƒ›áƒ¢áƒ•ირთáƒáƒ•ი კáƒáƒ“ის რეჟიმშიáƒ" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "გáƒáƒ£áƒ›áƒ¯áƒáƒ‘ესდებáƒ" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "Პრáƒáƒ‘ლემáƒ" msgstr[1] "სáƒáƒ™áƒ˜áƒ—ხები" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "მდებáƒáƒ áƒ”áƒáƒ‘áƒ" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "ბáƒáƒšáƒ ცვლილებáƒ" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "ლიცენზიáƒ" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "Linux-ის ბირთვი დáƒáƒ‘ლáƒáƒ™áƒ˜áƒšáƒ˜áƒ" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "Linux-ის ბირთვის შემáƒáƒ¬áƒ›áƒ”ბáƒ" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "Linux Swap" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Linux-ის ბირთვი" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Linux-ის ბირთვი დáƒáƒ‘ლáƒáƒ™áƒ˜áƒšáƒ˜áƒ" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Linux Swap" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "ჩáƒáƒ¢áƒ•ირთვáƒâ€¦" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "დáƒáƒ‘ლáƒáƒ™áƒ˜áƒšáƒ˜" #. TRANSLATORS: the release urgency msgid "Low" msgstr "დáƒáƒ‘áƒáƒšáƒ˜" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "MEI მწáƒáƒ áƒ›áƒáƒ”ბლის რეჟიმი" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "MEI გáƒáƒ“áƒáƒ¤áƒáƒ áƒ•áƒ" msgid "MEI version" msgstr "MEI-ის ვერსიáƒ" #. TRANSLATORS: Longest valid string for BIOS setting msgid "Maximum length" msgstr "მáƒáƒ¥áƒ¡áƒ˜áƒ›áƒáƒšáƒ£áƒ áƒ˜ სიგრძე" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "მáƒáƒ¥áƒ¡áƒ˜áƒ›áƒáƒšáƒ£áƒ áƒ˜ მნიშვნელáƒáƒ‘áƒ" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "სáƒáƒ¨áƒ£áƒáƒšáƒ" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "მეტáƒáƒ›áƒáƒœáƒáƒªáƒ”მების ხელმáƒáƒ¬áƒ”რáƒ" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "მეტáƒáƒ›áƒáƒœáƒáƒªáƒ”მების URI" #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "მინიმáƒáƒšáƒ£áƒ áƒ˜ ვერსიáƒ" #. TRANSLATORS: Shortest valid string for BIOS setting msgid "Minimum length" msgstr "მინიმáƒáƒšáƒ£áƒ áƒ˜ სიგრძე" #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "მინიმáƒáƒšáƒ£áƒ áƒ˜ მნიშვნელáƒáƒ‘áƒ" msgid "Modify daemon configuration" msgstr "დემáƒáƒœáƒ˜áƒ¡ კáƒáƒœáƒ¤áƒ˜áƒ’ურáƒáƒªáƒ˜áƒ˜áƒ¡ შეცვლáƒ" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "დáƒáƒ§áƒ”ნების შემდეგ სáƒáƒ­áƒ˜áƒ áƒáƒ”ბს გáƒáƒ“áƒáƒ¢áƒ•ირთვáƒáƒ¡" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "სáƒáƒ­áƒ˜áƒ áƒáƒ”ბს გáƒáƒ“áƒáƒ¢áƒ•ირთვáƒáƒ¡" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "დáƒáƒ§áƒ”ნების შემდეგ სáƒáƒ­áƒ˜áƒ áƒáƒ”ბს გáƒáƒ›áƒáƒ áƒ—ვáƒáƒ¡" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "áƒáƒ®áƒáƒšáƒ˜ ვერსიáƒ" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "ქმედებრმითითებული áƒáƒ áƒáƒ!" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "მიკრáƒáƒ™áƒáƒ“ის ID-ები ვერ ვიპáƒáƒ•ე" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "რელიზების გáƒáƒ áƒ”შე" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒáƒ“ი მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ები ვერ ვიპáƒáƒ•ე" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "გáƒáƒœáƒáƒ®áƒšáƒ”ბების ვერ ვიპáƒáƒ•ე" #. TRANSLATORS: version cannot be installed due to policy msgid "Not approved" msgstr "ნებáƒáƒ“áƒáƒ£áƒ áƒ—ველი" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "ნáƒáƒžáƒáƒ•ნი áƒáƒ áƒáƒ" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "მხáƒáƒ áƒ“áƒáƒ£áƒ­áƒ”რელიáƒ" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "დიáƒáƒ®" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "დიáƒáƒ®!" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "დáƒáƒ¨áƒ•ებულირმხáƒáƒšáƒáƒ“ ვერსიების გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "ნáƒáƒ’ულისხმები ESP-ის ბილიკის გáƒáƒ“áƒáƒ¤áƒáƒ áƒ•áƒ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "ბილიკი" #. TRANSLATORS: remote filename base msgid "Password" msgstr "პáƒáƒ áƒáƒšáƒ˜" msgid "Payload" msgstr "შემცველáƒáƒ‘áƒ" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "დáƒáƒ áƒ©áƒ”ნილი" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "შევáƒáƒ¡áƒ áƒ£áƒšáƒ áƒáƒžáƒ”რáƒáƒªáƒ˜áƒ?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "პლáƒáƒ¢áƒ¤áƒáƒ áƒ›áƒ˜áƒ¡ გáƒáƒ›áƒáƒ áƒ—ვáƒ" #. TRANSLATORS: 'recovery key' here refers to a code, rather than a physical #. metal thing msgid "Please ensure you have the volume recovery key before continuing." msgstr "გáƒáƒ’რძელების წინ დáƒáƒ áƒ¬áƒ›áƒ£áƒœáƒ“ით, რáƒáƒ› ტáƒáƒ›áƒ˜áƒ¡ áƒáƒ¦áƒ“გენის გáƒáƒ¡áƒáƒ¦áƒ”ბი ნáƒáƒ›áƒ“ვილáƒáƒ“ გáƒáƒ¥áƒ•თ." #. TRANSLATORS: Possible values for a bios setting msgid "Possible Values" msgstr "შესáƒáƒ«áƒšáƒ მნიშვნელáƒáƒ‘ები" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA Protection" msgstr "ჩáƒáƒ¢áƒ•ირთვáƒáƒ›áƒ“ელი DMA-ის დáƒáƒªáƒ•áƒ" #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "წინრვერსიáƒ" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "პრიáƒáƒ áƒ˜áƒ¢áƒ”ტი" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "პრáƒáƒ‘ლემები" msgid "Proceed with upload?" msgstr "დáƒáƒ•იწყრáƒáƒ¢áƒ•ირთვáƒ?" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Processor Security Checks" msgstr "პრáƒáƒªáƒ”სáƒáƒ áƒ˜áƒ¡ უსáƒáƒ¤áƒ áƒ—ხáƒáƒ”ბის შემáƒáƒ¬áƒ›áƒ”ბáƒ" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "დáƒáƒ®áƒ£áƒ áƒ£áƒšáƒ˜ კáƒáƒ“ი" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "REMOTE-ID" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "მხáƒáƒšáƒáƒ“ კითხვისთვის" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "წáƒáƒ™áƒ˜áƒ—ხვáƒâ€¦" #. TRANSLATORS: Plugin is active and in use msgid "Ready" msgstr "მზáƒáƒ“áƒáƒ" #. TRANSLATORS: how often we should refresh the metadata msgid "Refresh Interval" msgstr "გáƒáƒœáƒáƒ®áƒšáƒ”ბის შუáƒáƒšáƒ”დი" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "რელიზის ბრენჩი" #. TRANSLATORS: release attributes msgid "Release Flags" msgstr "რელიზის áƒáƒšáƒ›áƒ”ბი" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "რელიზის ID" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "დáƒáƒ¨áƒáƒ áƒ”ბულის ID" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "áƒáƒœáƒ’áƒáƒ áƒ˜áƒ¨áƒ˜áƒ¡ URI" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "სáƒáƒ­áƒ˜áƒ áƒáƒ”ბს ჩáƒáƒ›áƒ¢áƒ•ირთáƒáƒ• კáƒáƒ“ს" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "გáƒáƒ“áƒáƒ•ტვირთრáƒáƒ®áƒšáƒ?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის გáƒáƒ“áƒáƒ¢áƒ•ირთვáƒâ€¦" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SECTION" msgstr "სექციáƒ" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "SPI BIOS-ის დესკრიპტáƒáƒ áƒ˜" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "SPI BIOS-ის რეგიáƒáƒœáƒ˜" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "SPI ბლáƒáƒ™áƒ˜" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "SPI ჩáƒáƒ¬áƒ”რáƒ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "რიგში ჩáƒáƒ§áƒ”ნებáƒâ€¦" #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "დáƒáƒªáƒ£áƒšáƒ˜ ჩáƒáƒ¢áƒ•ირთვრგáƒáƒ›áƒáƒ áƒ—ულიáƒ" #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "áƒáƒ áƒ©áƒ”ული მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒ" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "áƒáƒ áƒ©áƒ”ული ტáƒáƒ›áƒ˜" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "სერიული ნáƒáƒ›áƒ”რი" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "პáƒáƒ áƒáƒ›áƒ”ტრები მხáƒáƒšáƒáƒ“ სისტემის გáƒáƒ“áƒáƒ¢áƒ•ირთვის შემდეგ გáƒáƒ“áƒáƒ¢áƒáƒ áƒ“ებáƒ" #. TRANSLATORS: command line option msgid "Show all results" msgstr "ყველრშედეგის ჩვენებáƒ" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "კლიენტისრდრდემáƒáƒœáƒ˜áƒ¡ ვერსიების ჩვენებáƒ" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "გáƒáƒ›áƒáƒ áƒ—ვის ინფáƒáƒ áƒ›áƒáƒªáƒ˜áƒ˜áƒ¡ ჩვენებრყველრდáƒáƒ›áƒ”ნისთვის" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "გáƒáƒ›áƒáƒ áƒ—ვის პáƒáƒ áƒáƒ›áƒ”ტრების ჩვენებáƒ" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "გáƒáƒ›áƒáƒ áƒ—ვის დáƒáƒ›áƒáƒ¢áƒ”ბითი ინფáƒáƒ áƒ›áƒáƒªáƒ˜áƒ˜áƒ¡ ჩვენებáƒ" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "გáƒáƒ›áƒáƒ•რთრáƒáƒ®áƒšáƒ?" msgid "Signature" msgstr "ხელმáƒáƒ¬áƒ”რáƒ" #. TRANSLATORS: file size of the download msgid "Size" msgstr "ზáƒáƒ›áƒ" #. TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM msgid "Some of the platform secrets may be invalidated when updating this firmware." msgstr "áƒáƒ› მიკრáƒáƒ™áƒáƒ“ის გáƒáƒœáƒáƒ®áƒšáƒ”ბისáƒáƒ¡ შეილებრპლáƒáƒ¢áƒ¤áƒáƒ áƒ›áƒ˜áƒ¡ ზáƒáƒ’იერი სáƒáƒ˜áƒ“უმლრდáƒáƒ˜áƒ™áƒáƒ áƒ’áƒáƒ¡." #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "წყáƒáƒ áƒ" #. TRANSLATORS: The BIOS setting accepts strings msgid "String" msgstr "სტრიქáƒáƒœáƒ˜" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "წáƒáƒ áƒ›áƒáƒ¢áƒ”ბáƒ" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "დáƒáƒ¨áƒáƒ áƒ”ბული წáƒáƒ áƒ›áƒáƒ¢áƒ”ბით გáƒáƒ›áƒáƒ˜áƒ áƒ—áƒ" #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "დáƒáƒ¨áƒáƒ áƒ”ბული წáƒáƒ áƒ›áƒáƒ¢áƒ”ბით ჩáƒáƒ˜áƒ áƒ—რდრგáƒáƒœáƒáƒ®áƒšáƒ“áƒ" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "დáƒáƒ¨áƒáƒ áƒ”ბული წáƒáƒ áƒ›áƒáƒ¢áƒ”ბით ჩáƒáƒ˜áƒ áƒ—áƒ" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "დáƒáƒ¨áƒáƒ áƒ”ბული წáƒáƒ áƒ›áƒáƒ¢áƒ”ბით შეიცვáƒáƒšáƒ" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "შეჯáƒáƒ›áƒ”ბáƒ" #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "მხáƒáƒ áƒ“áƒáƒ­áƒ”რილი" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "მხáƒáƒ áƒ“áƒáƒ­áƒ”რილი CPU" #. TRANSLATORS: Title: a better sleep state msgid "Suspend To Idle" msgstr "უქმáƒáƒ‘áƒáƒ›áƒ“ე შეჩერებáƒ" #. TRANSLATORS: Title: sleep state msgid "Suspend To RAM" msgstr "RAM-ში შეჩერებáƒ" #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "შეჩერებáƒ" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "RAM-ში შეჩერებáƒ" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "სისტემáƒáƒ¡ კვების გáƒáƒ áƒ” წყáƒáƒ áƒ ესáƒáƒ­áƒ˜áƒ áƒáƒ”ბáƒ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "ტექსტი" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM Platform Configuration" msgstr "TPM პლáƒáƒ¢áƒ¤áƒáƒ áƒ›áƒ˜áƒ¡ მáƒáƒ áƒ’ებáƒ" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM Reconstruction" msgstr "TPM -ის რემáƒáƒœáƒ¢áƒ˜" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "იáƒáƒ áƒšáƒ˜áƒ§áƒ˜" msgstr[1] "ჭდეები" #. TRANSLATORS: we're saving all USB events for emulation msgid "Tagged for emulation" msgstr "მáƒáƒœáƒ˜áƒ¨áƒœáƒ£áƒšáƒ˜ ემულáƒáƒªáƒ˜áƒ˜áƒ¡áƒ—ვის" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "გáƒáƒ¤áƒ£áƒ­áƒ”ბულიáƒ" #. show the user the entire data blob msgid "Target" msgstr "სáƒáƒ›áƒ˜áƒ–ნე წერტილი" #. TRANSLATORS: item is TRUE msgid "True" msgstr "ჭეშმáƒáƒ áƒ˜áƒ¢áƒ˜" #. TRANSLATORS: We verified the metadata against the server msgid "Trusted metadata" msgstr "სáƒáƒœáƒ“რმეტáƒáƒ›áƒáƒœáƒáƒªáƒ”მები" #. TRANSLATORS: We verified the payload against the server msgid "Trusted payload" msgstr "სáƒáƒœáƒ“რდáƒáƒ¢áƒ•ირთვáƒ" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "ტიპით" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "UEFI პლáƒáƒ¢áƒ¤áƒáƒ áƒ›áƒ˜áƒ¡ გáƒáƒ¡áƒáƒ¦áƒ”ბი" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "UEFI უსáƒáƒ¤áƒ áƒ—ხრჩáƒáƒ¢áƒ•ირთვáƒ" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "UEFI პლáƒáƒ¢áƒ¤áƒáƒ áƒ›áƒ˜áƒ¡ გáƒáƒ¡áƒáƒ¦áƒ”ბი" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "UEFI უსáƒáƒ¤áƒ áƒ—ხრჩáƒáƒ¢áƒ•ირთვáƒ" #. TRANSLATORS: error message msgid "Unable to find attribute" msgstr "áƒáƒ¢áƒ áƒ˜áƒ‘უტი ვერ ვიპáƒáƒ•ე" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "დáƒáƒ£áƒ¨áƒ˜áƒ¤áƒ áƒáƒ•ი" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "უცნáƒáƒ‘იáƒ" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "უცნáƒáƒ‘ი მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒ" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "გáƒáƒœáƒ‘ლáƒáƒ™áƒ˜áƒšáƒ˜" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒáƒ“ი" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "გáƒáƒœáƒáƒ®áƒšáƒ”ბის შეცდáƒáƒ›áƒ" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "áƒáƒ¡áƒšáƒ˜áƒ¡ გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "გáƒáƒœáƒáƒ®áƒšáƒ”ბის შეტყáƒáƒ‘ინებáƒ" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "გáƒáƒœáƒáƒ®áƒšáƒ”ბის მდგáƒáƒ›áƒáƒ áƒ”áƒáƒ‘áƒ" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "გáƒáƒœáƒ•áƒáƒáƒ®áƒšáƒ áƒáƒ®áƒšáƒ?" msgid "Updating" msgstr "გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "%s-ის გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒâ€¦" #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "სáƒáƒ¡áƒ¬áƒ áƒáƒ¤áƒáƒáƒ‘áƒ" #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "მáƒáƒ›áƒ®áƒ›áƒáƒ áƒ”ბელი გáƒáƒ¤áƒ áƒ—ხილებულიáƒ" #. TRANSLATORS: remote filename base msgid "Username" msgstr "მáƒáƒ›áƒ®áƒ›áƒáƒ áƒ”ბელი" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "სწáƒáƒ áƒ˜" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "ვáƒáƒ áƒ˜áƒáƒœáƒ¢áƒ˜" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "მáƒáƒ›áƒ¬áƒáƒ“ებელი" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "შემáƒáƒ¬áƒ›áƒ”ბáƒâ€¦" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "ვერსიáƒ" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "გáƒáƒ¤áƒ áƒ—ხილებáƒ" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "მáƒáƒšáƒáƒ“ინი…" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "ფáƒáƒ˜áƒšáƒ˜áƒ¡ ჩáƒáƒ¬áƒ”რáƒ:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "ჩáƒáƒ¬áƒ”რáƒâ€¦" #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "áƒáƒ› მიკრáƒáƒ™áƒáƒ“ით თქვენი áƒáƒžáƒáƒ áƒáƒ¢áƒ£áƒ áƒ შეიძლებრდáƒáƒ–იáƒáƒœáƒ“ეს. áƒáƒ›áƒáƒ•ე დრáƒáƒ¡ áƒáƒ› რელიზის დáƒáƒ§áƒ”ნებáƒáƒ› შეიძლებრ%s-ზე გáƒáƒ áƒáƒœáƒ¢áƒ˜áƒ მáƒáƒ’იხსნáƒáƒ—." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[CHECKSUM]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[DEVICE-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[FILENAME1] [FILENAME2]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[REASON] [TIMEOUT]" msgstr "[REASON] [TIMEOUT]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[SMBIOS-FILE|HWIDS-FILE]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "ნáƒáƒ’ულისხმები" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "fwupd დáƒáƒ›áƒáƒ¢áƒ”ბები" fwupd-2.0.10/po/kk.po000066400000000000000000000024751501337203100143050ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Baurzhan Muftakhidinov , 2017 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Kazakh (http://www.transifex.com/freedesktop/fwupd/language/kk/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: kk\n" "Plural-Forms: nplurals=1; plural=0;\n" #. TRANSLATORS: this is when a device is hotplugged msgid "Added" msgstr "ҚоÑылған" #. TRANSLATORS: this is when a device ctrl+c's a watch msgid "Cancelled" msgstr "Ð‘Ð°Ñ Ñ‚Ð°Ñ€Ñ‚Ñ‹Ð»Ò“Ð°Ð½" #. TRANSLATORS: this is when a device is hotplugged #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Өзгертілген" #. TRANSLATORS: read from device to host msgid "Erasing" msgstr "Өшірілуде" #. TRANSLATORS: read from device to host msgid "Reading" msgstr "Оқылуда" #. TRANSLATORS: this is when a device is hotplugged msgid "Removed" msgstr "Өшірілген" #. TRANSLATORS: read from device to host msgid "Verifying" msgstr "ТекÑерілуде" #. TRANSLATORS: write from host to device msgid "Writing" msgstr "Жазылуда" fwupd-2.0.10/po/ko.po000066400000000000000000003250511501337203100143070ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Seong-ho Cho , 2017,2019,2021-2024 # Shinjo Park , 2018-2021,2023 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Korean (http://app.transifex.com/freedesktop/fwupd/language/ko/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: ko\n" "Plural-Forms: nplurals=1; plural=0;\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "%.0fë¶„ 남ìŒ" #. TRANSLATORS: BMC refers to baseboard management controller which #. * is the device that updates all the other firmware on the system #, c-format msgid "%s BMC Update" msgstr "%s BMC ì—…ë°ì´íЏ" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "%s 배터리 ì—…ë°ì´íЏ" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "%s CPU 마ì´í¬ë¡œì½”드 ì—…ë°ì´íЏ" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "%s ì¹´ë©”ë¼ ì—…ë°ì´íЏ" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "%s 설정 ì—…ë°ì´íЏ" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "%s 소비ìžìš© ME ì—…ë°ì´íЏ" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "%s 컨트롤러 ì—…ë°ì´íЏ" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "%s 기업용 ME ì—…ë°ì´íЏ" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "%s 장치 ì—…ë°ì´íЏ" #. TRANSLATORS: Video Display refers to the laptop internal display or #. * external monitor #, c-format msgid "%s Display Update" msgstr "%s ë””ìŠ¤í”Œë ˆì´ ìž¥ì¹˜ ì—…ë°ì´íЏ" #. TRANSLATORS: Dock refers to the port replicator hardware laptops are #. * cradled in, or lowered onto #, c-format msgid "%s Dock Update" msgstr "%s ë„í¬ ì—…ë°ì´íЏ" #. TRANSLATORS: drive refers to a storage device, e.g. SATA disk #, c-format msgid "%s Drive Update" msgstr "%s 드ë¼ì´ë¸Œ ì—…ë°ì´íЏ" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "%s 임베디드 컨트롤러 ì—…ë°ì´íЏ" #. TRANSLATORS: a device that can read your fingerprint pattern #, c-format msgid "%s Fingerprint Reader Update" msgstr "%s 지문 스ìºë„ˆ ì—…ë°ì´íЏ" #. TRANSLATORS: flash refers to solid state storage, e.g. UFS or eMMC #, c-format msgid "%s Flash Drive Update" msgstr "%s 플래시 드ë¼ì´ë¸Œ ì—…ë°ì´íЏ" #. TRANSLATORS: GPU refers to a Graphics Processing Unit, e.g. #. * the "video card" #, c-format msgid "%s GPU Update" msgstr "%s GPU 장치 ì—…ë°ì´íЏ" #. TRANSLATORS: a large pressure-sensitive drawing area typically used #. * by artists and digital artists #, c-format msgid "%s Graphics Tablet Update" msgstr "%s 그래픽 태블릿 ì—…ë°ì´íЏ" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "%s 키보드 ì—…ë°ì´íЏ" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "%s ME ì—…ë°ì´íЏ" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "%s 마우스 ì—…ë°ì´íЏ" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "%s ë„¤íŠ¸ì›Œí¬ ì¸í„°íŽ˜ì´ìФ ì—…ë°ì´íЏ" #. TRANSLATORS: SSD refers to a Solid State Drive, e.g. non-rotating #. * SATA or NVMe disk #, c-format msgid "%s SSD Update" msgstr "%s SSD 장치 ì—…ë°ì´íЏ" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "%s 저장 장치 컨트롤러 ì—…ë°ì´íЏ" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "%s 시스템 ì—…ë°ì´íЏ" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "%s TPM ì—…ë°ì´íЏ" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "%s Thunderbolt 컨트롤러 ì—…ë°ì´íЏ" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "%s 터치패드 ì—…ë°ì´íЏ" #. TRANSLATORS: Dock refers to the port replicator device connected #. * by plugging in a USB cable -- which may or may not also provide power #, c-format msgid "%s USB Dock Update" msgstr "%s USB ë„í¬ ì—…ë°ì´íЏ" #. TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth #. * device that stays in the USB port so the wireless peripheral works #, c-format msgid "%s USB Receiver Update" msgstr "%s USB 수신 장치 ì—…ë°ì´íЏ" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "%s ì—…ë°ì´íЏ" #. TRANSLATORS: warn the user before updating, %1 is a device name #, c-format msgid "%s and all connected devices may not be usable while updating." msgstr "ì—…ë°ì´íЏ 중ì—는 %s ë° ì—°ê²°ëœ ëª¨ë“  장치를 사용할 수 ì—†ì„ ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤." #. TRANSLATORS: %1 refers to some kind of security test, e.g. "Encrypted RAM". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s appeared: %s" msgstr "%s 나타남: %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "UEFI platform #. key". #. * %2 and %3 refer to results value, e.g. "Valid" and "Invalid" #, c-format msgid "%s changed: %s → %s" msgstr "%s 바뀜: %s → %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "SPI BIOS #. region". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s disappeared: %s" msgstr "%s 사ë¼ì§: %s" #. TRANSLATORS: the device has a reason it can't update, e.g. laptop lid #. closed #, c-format msgid "%s is not currently updatable" msgstr "현재 %sì„(를) ì—…ë°ì´íŠ¸í•  수 없습니다" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "%s 제조 모드" #. TRANSLATORS: warn the user before #. * updating, %1 is a device name #, c-format msgid "%s must remain connected for the duration of the update to avoid damage." msgstr "장치 ì†ìƒì„ 방지하려면 ì—…ë°ì´íЏ 중 %sì˜ ì—°ê²°ì„ ê³„ì† ìœ ì§€í•´ì•¼ 합니다." #. TRANSLATORS: warn the user before updating, %1 is a machine #. * name #, c-format msgid "%s must remain plugged into a power source for the duration of the update to avoid damage." msgstr "장치 ì†ìƒì„ 방지하려면 ì—…ë°ì´íЏ 중 %sì„(를) ì „ì›ì— ê³„ì† ì—°ê²°í•´ ë‘어야 합니다." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "%s 재정ì˜" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "%s 버전" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%uì¼" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "장치 %uê°œì˜ íŽŒì›¨ì–´ë¥¼ ì—…ë°ì´íŠ¸í•  수 있습니다." #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device is not the best known configuration." msgid_plural "%u devices are not the best known configuration." msgstr[0] "장치 %u개가 알려진 최ì ì˜ 설정 ìƒíƒœê°€ 아닙니다." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u시간" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%uë¶„" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%uì´ˆ" #. TRANSLATORS: first percentage is current value, 2nd percentage is the #. * lowest limit the firmware update is allowed for the update to happen #, c-format msgid "%u%% (threshold %u%%)" msgstr "%u%% (임계율 %u%%)" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(사용ë˜ì§€ 않ìŒ)" #. TRANSLATORS: HSI event title msgid "A TPM PCR is now an invalid value" msgstr "TPM PCRì—서 ì´ì œ 올바르지 ì•Šì€ ê°’ì„ ê°€ì¡ŒìŠµë‹ˆë‹¤" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "AMD Firmware Replay Protection" msgstr "AMD 펌웨어 ìž¬ìƒ ë³´í˜¸" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "AMD Firmware Write Protection" msgstr "AMD 펌웨어 ê¸°ë¡ ë³´í˜¸" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "AMD Secure Processor Rollback Protection" msgstr "AMD 보안 프로세서 롤백 보호" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "ARCHIVE FIRMWARE METAINFO [FIRMWARE] [METAINFO] [JCATFILE]" msgstr "<ì•„ì¹´ì´ë¸Œ> <펌웨어> <메타정보> [<펌웨어>] [<메타정보>] []" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "ë™ìž‘ì´ í•„ìš”í•©ë‹ˆë‹¤:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "장치 활성화" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "대기 ì¤‘ì¸ ìž¥ì¹˜ 활성화" msgid "Activate the new firmware on the device" msgstr "ìž¥ì¹˜ì— ìƒˆ 펌웨어 활성화" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "펌웨어 ì—…ë°ì´íЏ 활성화 중" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "ë‹¤ìŒ ìž¥ì¹˜ì˜ íŽŒì›¨ì–´ ì—…ë°ì´íЏ 활성화 중:" #. TRANSLATORS: command description msgid "Adds devices to watch for future emulation" msgstr "향후 ì—뮬레ì´ì…˜ì„ ê°ì‹œí•  장치를 추가합니다" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "경과 기간" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "ë™ì˜í•˜ë©° ì›ê²© 저장소를 활성화하시겠습니까?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "%sì˜ ë³„ì¹­" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are now valid" msgstr "모든 TPM PCRì´ ì´ì œ 올바릅니다" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are valid" msgstr "모든 TPM PCRì´ ì˜¬ë°”ë¦…ë‹ˆë‹¤" #. TRANSLATORS: an application is preventing system updates msgid "All devices are prevented from update by system inhibit" msgstr "시스템 ë™ìž‘ 억제로 모든 장치 ì—…ë°ì´íŠ¸ë¥¼ 막았습니다" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "ë™ì¼í•œ 모든 형ì‹ì˜ 장치를 ë™ì‹œ ì—…ë°ì´íŠ¸í•©ë‹ˆë‹¤" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "펌웨어 다운그레ì´ë“œ 허용" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "기존 펌웨어 버전 재설치 허용" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "펌웨어 브랜치 전환 허용" #. TRANSLATORS: is not the main firmware stream msgid "Alternate branch" msgstr "대체 브랜치" #. TRANSLATORS: another application is updating the device already msgid "An update is in progress" msgstr "ì—…ë°ì´íŠ¸ë¥¼ 진행하고 있습니다" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "ì—…ë°ì´íŠ¸ë¥¼ 완료하려면 다시 시작해야 합니다." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "ì—…ë°ì´íŠ¸ë¥¼ 완료하려면 ì‹œìŠ¤í…œì„ ì¢…ë£Œí•´ì•¼ 합니다." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "모든 ì§ˆë¬¸ì— ì˜ˆë¡œ 답하기" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "필요하지 ì•Šì€ ê²½ìš°ì—ë„ ì—…ë°ì´íЏ ì ìš©" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "ì—…ë°ì´íЏ íŒŒì¼ ì ìš©" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "ì—…ë°ì´íЏ ì ìš© 중..." #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "í—ˆìš©ëœ íŽŒì›¨ì–´:" #. TRANSLATORS: command description msgid "Asks the daemon to quit" msgstr "ë°ëª¬ ë내기를 확ì¸í•©ë‹ˆë‹¤" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "펌웨어 ëª¨ë“œì— ì—°ê²°" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "ì¸ì¦ 중…" #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "ì¸ì¦ 세부 절차가 필요합니다" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "ì´ë™ì‹ ìž¥ì¹˜ì˜ íŽŒì›¨ì–´ë¥¼ ì´ì „ 버전으로 ë˜ëŒë¦¬ë ¤ë©´ ì¸ì¦í•´ì•¼ 합니다" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "ì´ ë¨¸ì‹ ì— ì´ì „ ë²„ì „ì˜ íŽŒì›¨ì–´ë¥¼ 설치하려면 ì¸ì¦í•´ì•¼ 합니다" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "BIOS 설정 ê°’ì„ ìˆ˜ì •í•˜ë ¤ë©´ ì¸ì¦ì´ 필요합니다" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "펌웨어 ì—…ë°ì´íŠ¸ì— ì‚¬ìš©í•  ì›ê²© ì„¤ì •ì„ ìˆ˜ì •í•˜ë ¤ë©´ ì¸ì¦í•´ì•¼ 합니다" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "ë°ëª¬ ì„¤ì •ì„ ìˆ˜ì •í•˜ë ¤ë©´ ì¸ì¦í•´ì•¼ 합니다" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "BIOS 설정 ê°’ì„ ì½ìœ¼ë ¤ë©´ ì¸ì¦ì´ 필요합니다" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "í—ˆìš©ëœ íŽŒì›¨ì–´ 목ë¡ì„ 설정하려면 ì¸ì¦í•´ì•¼ 합니다" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "í´ë¼ì´ì–¸íЏ ì¸ì¦ì„œë¡œ ë°ì´í„°ë¥¼ 서명하려면 ì¸ì¦í•´ì•¼ 합니다" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "새 펌웨어 버전으로 전환하려면 ì¸ì¦í•´ì•¼ 합니다" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "장치 ìž ê¸ˆì„ í•´ì œí•˜ë ¤ë©´ ì¸ì¦í•´ì•¼ 합니다" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "ì´ë™ì‹ ìž¥ì¹˜ì˜ íŽŒì›¨ì–´ë¥¼ ì—…ë°ì´íŠ¸í•˜ë ¤ë©´ ì¸ì¦í•´ì•¼ 합니다" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "ì´ ë¨¸ì‹ ì˜ íŽŒì›¨ì–´ë¥¼ ì—…ë°ì´íŠ¸í•˜ë ¤ë©´ ì¸ì¦í•´ì•¼ 합니다" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "ì €ìž¥ëœ ì²´í¬ì„¬ì„ ì—…ë°ì´íŠ¸í•˜ë ¤ë©´ ì¸ì¦í•´ì•¼ 합니다" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "ìžë™ ë³´ê³ " #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "매번 ìžë™ìœ¼ë¡œ 업로드하시겠습니까?" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS Firmware Updates" msgstr "BIOS 펌웨어 ì—…ë°ì´íЏ" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS Rollback Protection" msgstr "BIOS 롤백 보호" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS firmware updates" msgstr "BIOS 펌웨어 ì—…ë°ì´íЏ" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS rollback protection" msgstr "BIOS 롤백 보호" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "LVFS ë˜ëŠ” 윈ë„ìš° ì—…ë°ì´íЏì—서 BIOS ì—…ë°ì´íŠ¸ë¥¼ 받아왔습니다" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "BUILDER-XML FILENAME-DST" msgstr "BUILDER-XML FILENAME-DST" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "배터리" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "새 ì»¤ë„ ë“œë¼ì´ë²„ ë°”ì¸ë“œ" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "ì°¨ë‹¨ëœ íŽŒì›¨ì–´ 파ì¼:" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "막힌 버전" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "차단할 펌웨어:" #. TRANSLATORS: command description msgid "Blocks a specific firmware from being installed" msgstr "지정한 펌웨어 설치 ë°©ì§€" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "ë¶€íŠ¸ë¡œë” ë²„ì „" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "브랜치" #. TRANSLATORS: command description msgid "Build a cabinet archive from a firmware blob and XML metadata" msgstr "펌웨어 블롭과 XML 메타ë°ì´í„°ë¥¼ 활용하여 ìºë¹„ë‹› ì•„ì¹´ì´ë¸Œë¥¼ 만듭니다" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "펌웨어 íŒŒì¼ ë¹Œë“œ" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * Utilized by OS means the distribution enabled it msgid "CET OS Support" msgstr "CET OS ì§€ì›" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * enabled means supported by the processor msgid "CET Platform" msgstr "CET 플랫í¼" #. TRANSLATORS: longer description msgid "CPU Microcode must be updated to mitigate against various information-disclosure security issues." msgstr "다양한 ì •ë³´ 유출 보안 문제를 해결하려면 CPU 마ì´í¬ë¡œì½”드를 ì—…ë°ì´íŠ¸í•´ì•¼ 합니다." #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "취소" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "취소함" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "dbx ì—…ë°ì´íŠ¸ê°€ ì´ë¯¸ ì ìš©ë˜ì—ˆê¸° ë•Œë¬¸ì— ì ìš©í•  수 없습니다." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "바뀜" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "암호화 해시가 펌웨어와 ì¼ì¹˜í•˜ëŠ”ì§€ 확ì¸" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "ì²´í¬ì„¬" #. TRANSLATORS: get interactive prompt, where branch is the #. * supplier of the firmware, e.g. "non-free" or "free" msgid "Choose branch" msgstr "브랜치를 ì„ íƒí•˜ì‹­ì‹œì˜¤" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "장치를 ì„ íƒí•˜ì‹­ì‹œì˜¤" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "펌웨어를 ì„ íƒí•˜ì‹­ì‹œì˜¤" #. TRANSLATORS: get interactive prompt msgid "Choose release" msgstr "릴리스를 ì„ íƒí•˜ì‹­ì‹œì˜¤" #. TRANSLATORS: get interactive prompt msgid "Choose volume" msgstr "ë³¼ë¥¨ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "최근 ì—…ë°ì´íЏ ê²°ê³¼ 지우기" #. TRANSLATORS: error message msgid "Command not found" msgstr "ëª…ë ¹ì„ ì°¾ì„ ìˆ˜ 없습니다" #. TRANSLATORS: is not supported by the vendor msgid "Community supported" msgstr "커뮤니티 ì§€ì›" #. TRANSLATORS: title prefix for the BIOS settings dialog msgid "Configuration Change Suggested" msgstr "설정 변경 제안" #. TRANSLATORS: no peeking msgid "Configuration is only readable by the system administrator" msgstr "ì„¤ì •ì€ ì‹œìŠ¤í…œ 관리ìžë§Œ 확ì¸í•  수 있습니다" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology msgid "Control-flow Enforcement Technology" msgstr "제어 í름 ê°•ì œ 기술" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "펌웨어 íŒŒì¼ ë³€í™˜" #. TRANSLATORS: when the update was built msgid "Created" msgstr "ìƒì„±ë¨" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "매우 높ìŒ" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "암호화 해시 검사 사용 가능" #. TRANSLATORS: current value of a BIOS setting msgid "Current Value" msgstr "현재값" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "현재 버전" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "DEVICE-ID|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "디버깅 옵션" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "ì••ì¶• í•´ì œ 중…" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "설명" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "ë¶€íŠ¸ë¡œë” ëª¨ë“œë¡œ 전환" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "ìžì„¸í•œ ì •ë³´" #. TRANSLATORS: the best known configuration is a set of software that we know #. works #. * well together. In the OEM and ODM industries it is often called a BKC msgid "Deviate from the best known configuration?" msgstr "최ì ì˜ 설정ì—서 벗어납니까?" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "장치 플래그" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "장치 ID" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "장치 추가ë¨:" #. TRANSLATORS: the device is already connected msgid "Device already exists" msgstr "장치가 ì´ë¯¸ 있습니다" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "배터리 ì „ì› ì—¬ë¶„ì´ ë§¤ìš° 부족합니다" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse #, c-format msgid "Device battery power is too low (%u%%, requires %u%%)" msgstr "장치 배터리 ì „ì›ì´ 매우 부족합니다 (%u%%, í•„ìš” ì „ì› ë°±ë¶„ìœ¨ %u%%)" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "장치 ì—…ë°ì´íЏ 실패 시 복구 가능" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "장치 ìƒíƒœ 바뀜:" #. TRANSLATORS: a version check is required for all firmware msgid "Device firmware is required to have a version check" msgstr "장치 펌웨어ì—서 버전 확ì¸ì„ ì§€ì›í•´ì•¼ 함" #. TRANSLATORS: emulated means we are pretending to be a different model msgid "Device is emulated" msgstr "ì—뮬레ì´ì…˜ 장치" #. TRANSLATORS: device cannot be interrupted, for instance taking a phone call msgid "Device is in use" msgstr "장치를 사용하고 있습니다" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "장치가 ìž ê¹€" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "ìž¥ì¹˜ì— ì œê³µëœ ëª¨ë“  릴리스를 설치해야 함" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "ìž¥ì¹˜ì— ì ‘ê·¼í•  수 없습니다" #. TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode msgid "Device is unreachable, or out of wireless range" msgstr "장치가 ëŠì–´ì¡Œê±°ë‚˜, 무선 범위를 벗어났습니다" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "ì—…ë°ì´íЏ 중 장치를 사용할 수 있ìŒ" #. TRANSLATORS: usually this is when we're waiting for a reboot msgid "Device is waiting for the update to be applied" msgstr "ì—…ë°ì´íЏ ì ìš©ì„ 기다리고 있습니다" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "장치 제거ë¨:" #. TRANSLATORS: as in, wired mains power for a laptop msgid "Device requires AC power to be connected" msgstr "êµë¥˜ ì „ì›ì„ 연결해야합니다" #. TRANSLATORS: The device cannot be updated due to missing vendor's license." msgid "Device requires a software license to update" msgstr "장치 ì—…ë°ì´íŠ¸ì‹œ 프로그램 ë¼ì´ì„ ìŠ¤ê°€ 필요합니다" #. TRANSLATORS: longer description msgid "Device software updates are provided for this device." msgstr "ì´ ìž¥ì¹˜ì— ëŒ€í•œ 장치 프로그램 ì—…ë°ì´íŠ¸ê°€ 있습니다." #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "안전한 ì—…ë°ì´íЏ ì§€ì›" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "장치ì—서 펌웨어 브랜치 ì „í™˜ì„ ì§€ì›í•¨" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "장치 ì—…ë°ì´íЏ 활성화 í•„ìš”" #. TRANSLATORS: save the old firmware to disk before installing the new one msgid "Device will backup firmware before installing" msgstr "ìž¥ì¹˜ì— íŽŒì›¨ì–´ë¥¼ 설치하기 ì „ì— ë°±ì—…ì„ ìˆ˜í–‰í•¨" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "ì—…ë°ì´íЏ 후 장치가 다시 표시ë˜ì§€ 않ìŒ" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "성공ì ìœ¼ë¡œ ì—…ë°ì´íŠ¸ëœ ìž¥ì¹˜:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "올바르게 ì—…ë°ì´íЏë˜ì§€ ì•Šì€ ìž¥ì¹˜:" #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "펌웨어 ì—…ë°ì´íŠ¸ë¥¼ 사용할 수 없는 장치:" #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "최신 펌웨어가 ì„¤ì¹˜ëœ ìž¥ì¹˜:" #. TRANSLATORS: this is for the device tests msgid "Did not find any devices with matching GUIDs" msgstr "ì¼ì¹˜í•˜ëŠ” GUID 장치를 찾지 못했습니다" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "비활성화ë¨" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "지정한 ì›ê²© 저장소 비활성화" #. TRANSLATORS: the OS the release was tested on msgid "Distribution" msgstr "ë°°í¬íŒ" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "ì˜¤ëž˜ëœ ë©”íƒ€ë°ì´í„° 검사하지 않기" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "ë³´ê³ ë˜ì§€ ì•Šì€ ê³¼ê±° ê¸°ë¡ ê²€ì‚¬í•˜ì§€ 않기" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "ì›ê²© 저장소ì—서 다운로드 활성화 여부를 검사하지 않기" #. TRANSLATORS: command line option msgid "Do not check or prompt for reboot after update" msgstr "ì—…ë°ì´íЏ 후 다시 시작 묻거나 검사하지 않기" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "로그 ë„ë©”ì¸ ì ‘ë‘사를 í¬í•¨í•˜ì§€ 않기" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "timestamp ì ‘ë‘사를 í¬í•¨í•˜ì§€ 않기" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "장치 안정성 검사를 실행하지 않ìŒ" #. TRANSLATORS: command line option msgid "Do not prompt for devices" msgstr "ìž¥ì¹˜ì— ëŒ€í•´ 물어보지 않ìŒ" #. TRANSLATORS: command line option msgid "Do not prompt to fix security issues" msgstr "보안 문제 í•´ê²° 묻지 않기" #. TRANSLATORS: command line option msgid "Do not search the firmware when parsing" msgstr "í•´ì„시 펌웨어를 검색하지 않ìŒ" #. TRANSLATORS: warning message shown after update has been scheduled msgid "Do not turn off your computer or remove the AC adaptor while the update is in progress." msgstr "ì—…ë°ì´íŠ¸ë¥¼ 진행하는 ë™ì•ˆ 컴퓨터를 ë„거나 êµë¥˜ 어댑터를 뽑지 마십시오." #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "과거 ê¸°ë¡ ë°ì´í„°ë² ì´ìŠ¤ì— ê¸°ë¡í•˜ì§€ 않기" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "펌웨어 브랜치 변경 ì¡°ê±´ì„ ì´í•´í–ˆìŠµë‹ˆê¹Œ?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "다ìŒì— ì—…ë°ì´íŠ¸í•  때 ì´ ê¸°ëŠ¥ì„ ë¹„í™œì„±í™”í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "ì›ê²© 저장소를 새로 고치시겠습니까?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "다ìŒì— ì—…ë°ì´íŠ¸í•  때 보고서를 ìžë™ìœ¼ë¡œ 업로드하시겠습니까?" #. TRANSLATORS: command line option msgid "Don't prompt for authentication (less details may be shown)" msgstr "ì¸ì¦ì„ 확ì¸í•˜ì§€ 않ìŒ(ëœ ìžì„¸í•˜ê²Œ 나타냄)" #. TRANSLATORS: success msgid "Done!" msgstr "완료!" #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "%sì„(를) %sì—서 %s(으)로 다운그레ì´ë“œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "장치 펌웨어 다운그레ì´ë“œ" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "%s 다운그레ì´ë“œ 중..." #. TRANSLATORS: command description msgid "Download a file" msgstr "íŒŒì¼ ë‹¤ìš´ë¡œë“œ" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "다운로드 중…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "파ì¼ì— ì €ìž¥ëœ SMBIOS ë°ì´í„° ë¤í”„ 출력" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "ì˜ˆìƒ ì‹œê°„" #. TRANSLATORS: longer description msgid "Each system should have tests to ensure firmware security." msgstr "ê° ì‹œìŠ¤í…œì— ëŒ€í•´ 펌웨어 ë³´ì•ˆì„ í™•ì¸ ì‹œí—˜ì„ ìˆ˜í–‰í•´ì•¼ 합니다." #. TRANSLATORS: command description msgid "Emulate a device using a JSON manifest" msgstr "JSON 매니페스트로 장치 ì—뮬레ì´íŒ…합니다" #. TRANSLATORS: this device is not actually real msgid "Emulated" msgstr "ì—뮬레ì´íŒ…함" #. TRANSLATORS: Title: if we are emulating a different host msgid "Emulated host" msgstr "ì—뮬레ì´íŒ… 호스트" msgid "Enable" msgstr "활성" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "새 ì›ê²© 저장소를 활성화하시겠습니까?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "ì´ ì›ê²© 저장소를 활성화하시겠습니까?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "활성화ë¨" #. TRANSLATORS: Plugin is active only if hardware is found msgid "Enabled if hardware matches" msgstr "하드웨어가 ì¼ì¹˜í•˜ë©´ 활성" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "지정한 ì›ê²© 저장소 활성화" msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "ì´ ê¸°ëŠ¥ì˜ ì‚¬ìš©ì€ ë³¸ì¸ì˜ ì±…ìž„ì´ë©°, ì—…ë°ì´íŠ¸ë¡œ ì¸í•´ì„œ ìƒê¸´ 문제는 ì› ìž¥ì¹˜ ì œì¡°ì‚¬ì— ì§ì ‘ 보고해야 합니다. ì—…ë°ì´íЏ ì§„í–‰ 과정 ìžì²´ì˜ 문제는 $OS_RELEASE:BUG_REPORT_URL$(으)로 ë³´ê³ í•´ 주십시오." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "ì´ ì›ê²© 저장소를 설정하는 ê²ƒì€ ë³¸ì¸ì˜ 책임입니다." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "암호화ë¨" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "ì•”í˜¸í™”ëœ RAM" msgid "Encrypted RAM makes it impossible for information that is stored in device memory to be read if the memory chip is removed and accessed." msgstr "암호화 RAM ì •ì±…ì—서 메모리 ì¹©ì„ ì œê±°í–ˆê±°ë‚˜ 접근할 때 ì½ì–´ì•¼ í•  장치 ë©”ëª¨ë¦¬ì— ì •ë³´ë¥¼ 저장할 수 ì—†ë„ë¡ í•©ë‹ˆë‹¤." #. TRANSLATORS: the vendor is no longer supporting the device msgid "End of life" msgstr "ì§€ì› ë§Œë£Œ" #. TRANSLATORS: The BIOS setting can only be changed to fixed values msgid "Enumeration" msgstr "ì—´ê±°ê°’" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "모든 펌웨어 ì—…ë°ì´íЏ ê¸°ë¡ ì§€ìš°ê¸°" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "지우는 중…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "ì§§ì€ ëŒ€ê¸° 시간 경과 후 나가기" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "ì—”ì§„ì„ ë¶ˆëŸ¬ì˜¨ 후 나가기" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "펌웨어 íŒŒì¼ êµ¬ì¡°ë¥¼ XML로 내보내기" #. TRANSLATORS: command description msgid "Extract a firmware blob to images" msgstr "펌웨어 ë°”ì´ë„ˆë¦¬ 파ì¼ì—서 ì´ë¯¸ì§€ 추출" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "FILE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "FILE [DEVICE-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "FILENAME" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "FILENAME CERTIFICATE PRIVATE-KEY" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "FILENAME DEVICE-ID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME OFFSET DATA [FIRMWARE-TYPE]" msgstr "<파ì¼ì´ë¦„> <오프셋> <ë°ì´í„°> [<펌웨어형ì‹>]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "FILENAME [DEVICE-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "FILENAME [FIRMWARE-TYPE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" msgstr "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" msgstr "<파ì¼ì´ë¦„>|<ì²´í¬ì„¬1>[,<ì²´í¬ì„¬2>][,<ì²´í¬ì„¬3>]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "실패함" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "ì—…ë°ì´íŠ¸ë¥¼ ì ìš©í•  수 ì—†ìŒ" #. TRANSLATORS: error message for Windows msgid "Failed to connect to Windows service, please ensure it's running." msgstr "윈ë„ìš° 서비스 ì—°ê²° 실패. 실행중ì¸ì§€ 확ì¸í•˜ì‹­ì‹œì˜¤." #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "ë°ëª¬ì— ì—°ê²°í•  수 ì—†ìŒ" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "로컬 dbx를 불러올 수 ì—†ìŒ" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "시스템 dbx를 불러올 수 ì—†ìŒ" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "잠글 수 ì—†ìŒ" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "ì¸ìž í•´ì„ì— ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "파ì¼ì„ í•´ì„í•  수 ì—†ìŒ" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "로컬 dbx를 í•´ì„í•  수 ì—†ìŒ" #. TRANSLATORS: a feature is something like "can show an image" msgid "Failed to set front-end features" msgstr "프론트엔드 기능 ì„¤ì •ì— ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "ESP ë‚´ìš©ì„ ê²€ì‚¬í•  수 ì—†ìŒ" #. TRANSLATORS: item is FALSE msgid "False" msgstr "ê±°ì§“" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "íŒŒì¼ ì´ë¦„" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "íŒŒì¼ ì´ë¦„ 서명" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "íŒŒì¼ ì´ë¦„ ì›ë³¸" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "íŒŒì¼ ì´ë¦„ì´ í•„ìš”í•¨" #. TRANSLATORS: command line option msgid "Filter with a set of device flags using a ~ prefix to exclude, e.g. 'internal,~needs-reboot'" msgstr "플래그로 장치를 필터하며, ~ 기호를 ì•žì— ë¶™ì´ë©´ 제외할 수 있습니다. 예: 'internal,~needs-reboot'" #. TRANSLATORS: command line option msgid "Filter with a set of release flags using a ~ prefix to exclude, e.g. 'trusted-release,~trusted-metadata'" msgstr "플래그로 릴리스를 필터하며, ~ 기호를 ì•žì— ë¶™ì´ë©´ 제외할 수 있습니다. 예: 'trusted-release,~trusted-metadata'" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware Attestation" msgstr "펌웨어 ê²€ì¦" #. TRANSLATORS: longer description msgid "Firmware Attestation checks device software using a reference copy, to make sure that it has not been changed." msgstr "펌웨어 ì¦ëª… ì •ì±…ì—서는 장치 í”„ë¡œê·¸ëž¨ì˜ ë³€ê²½ 여부 í™•ì¸ ëª©ì ìœ¼ë¡œ 참조 복사를 사용하는지 검사합니다." #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "펌웨어 BIOS 서술ìž" #. TRANSLATORS: longer description msgid "Firmware BIOS Descriptor protects device firmware memory from being tampered with." msgstr "장치 펌웨어 서술ìžì—서 장치 펌웨어 ë©”ëª¨ë¦¬ì— íŽŒì›¨ì–´ë¥¼ ìž„ì˜ë¡œ 변경하는 행위를 막습니다." #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "펌웨어 BIOS ì˜ì—­" #. TRANSLATORS: longer description msgid "Firmware BIOS Region protects device firmware memory from being tampered with." msgstr "펌웨어 BIOS ì˜ì—­ì—서 장치 펌웨어 ë©”ëª¨ë¦¬ì— íŽŒì›¨ì–´ë¥¼ ìž„ì˜ë¡œ 변경하는 행위를 막습니다" #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "펌웨어 기본 URI" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "펌웨어 ì—…ë°ì´íЏ D-Bus 서비스" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "펌웨어 ì—…ë°ì´íЏ ë°ëª¬" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "펌웨어 ì—…ë°ì´í„° ê²€ì¦" msgid "Firmware Updater Verification checks that software used for updating has not been tampered with." msgstr "펌웨어 ì—…ë°ì´í„° ê²€ì¦ì—서 ì—…ë°ì´íŠ¸ì— ì‚¬ìš©í•˜ëŠ” í”„ë¡œê·¸ëž¨ì„ ìž„ì˜ë¡œ 바꾸지 않았는지 확ì¸í•©ë‹ˆë‹¤." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "펌웨어 ì—…ë°ì´íЏ" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "펌웨어 유틸리티" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "펌웨어 ê¸°ë¡ ë³´í˜¸" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "펌웨어 ê¸°ë¡ ë³´í˜¸ 잠금" #. TRANSLATORS: longer description msgid "Firmware Write Protection protects device firmware memory from being tampered with." msgstr "펌웨어 ê¸°ë¡ ë³´í˜¸ ì •ì±…ì—서 장치 펌웨어 ë©”ëª¨ë¦¬ì— íŽŒì›¨ì–´ë¥¼ ìž„ì˜ë¡œ 변경하는 행위를 막습니다" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "펌웨어 ê²€ì¦" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "펌웨어가 ì´ë¯¸ 차단ë¨" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "펌웨어가 ì´ë¯¸ 차단ë˜ì–´ 있지 않ìŒ" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "펌웨어 메타ë°ì´í„°ê°€ %uì¼ ë™ì•ˆ ì—…ë°ì´íЏë˜ì§€ 않았으므로 최신 ì •ë³´ê°€ 누ë½ë˜ì—ˆì„ ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "펌웨어 ì—…ë°ì´íЏ" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "플래그" #. TRANSLATORS: command line option msgid "Force the action by relaxing some runtime checks" msgstr "런타임 ì²´í¬ë¥¼ 비활성화하여 강제로 작업 실행" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "ì°¾ìŒ" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "ì „ì²´ ë””ìŠ¤í¬ ì•”í˜¸í™”ë¥¼ ê°ì§€í–ˆìŠµë‹ˆë‹¤" #. TRANSLATORS: we might ask the user the recovery key when next booting #. Windows msgid "Full disk encryption secrets may be invalidated when updating" msgstr "ì—…ë°ì´íŠ¸ë¥¼ 수행하면 ì „ì²´ ë””ìŠ¤í¬ ì•”í˜¸í™” 비밀키를 무효화할 수 있습니다" #. TRANSLATORS: Title: if the part has been fused msgid "Fused Platform" msgstr "퓨징 플랫í¼" #. TRANSLATORS: Title: if the part has been fused msgid "Fused platform" msgstr "퓨징한 플랫í¼" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "GUID|DEVICE-ID" msgstr "" msgid "Get BIOS settings" msgstr "BIOS 설정 가져오기" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "fwupd를 ì§€ì›í•˜ëŠ” 모든 장치 ì •ë³´ 가져오기" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "펌웨어 ì—…ë°ì´íŠ¸ë¥¼ ì§€ì›í•˜ëŠ” 모든 장치 ì •ë³´ 가져오기" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "ì‹œìŠ¤í…œì— ë“±ë¡ëœ 모든 활성 í”ŒëŸ¬ê·¸ì¸ ê°€ì ¸ì˜¤ê¸°" #. TRANSLATORS: command description msgid "Get device report metadata" msgstr "장치 보고서 메타ë°ì´í„° 가져오기" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "펌웨어 íŒŒì¼ ì„¸ë¶€ ì •ë³´ 가져오기" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "ì›ê²© 설정 ì •ë³´ 가져오기" #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "호스트 보안 ì†ì„± 가져오기" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "í—ˆìš©ëœ íŽŒì›¨ì–´ ëª©ë¡ ê°€ì ¸ì˜¤ê¸°" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "설치 ë°©ì§€ëœ íŽŒì›¨ì–´ ëª©ë¡ ê°€ì ¸ì˜¤ê¸°" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "연결한 í•˜ë“œì›¨ì–´ì˜ ì—…ë°ì´íЏ 목ë¡ì„ 가져옵니다" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "장치 펌웨어 릴리스 가져오기" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "최근 ì—…ë°ì´íЏ ê²°ê³¼ 가져오기" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "HWIDS-FILE" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "하드웨어를 다시 연결해야 함" #. TRANSLATORS: the release urgency msgid "High" msgstr "높ìŒ" #. TRANSLATORS: title for host security events msgid "Host Security Events" msgstr "호스트 보안 ì´ë²¤íЏ" #. TRANSLATORS: error message for unsupported feature #, c-format msgid "Host Security ID (HSI) is not supported" msgstr "호스트 보안 ID(HSI)를 ì§€ì›í•˜ì§€ 않습니다" #. TRANSLATORS: success, so say thank you to the user msgid "Host Security ID attributes uploaded successfully, thanks!" msgstr "호스트 보안 ID ì†ì„±ì„ 제대로 올렸습니다. ê°ì‚¬í•©ë‹ˆë‹¤!" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "호스트 보안 ID:" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INHIBIT-ID" msgstr "INHIBIT-ID" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "IOMMU 보호" #. TRANSLATORS: longer description msgid "IOMMU Protection prevents connected devices from accessing unauthorized parts of system memory." msgstr "IOMMU 보호 ì •ì±…ì—서는 연결한 ìž¥ì¹˜ì˜ ì‹œìŠ¤í…œ ë©”ëª¨ë¦¬ì˜ ë¹„í—ˆê°€ 부분 ì ‘ê·¼ì„ ë§‰ìŠµë‹ˆë‹¤." #. TRANSLATORS: HSI event title msgid "IOMMU device protection disabled" msgstr "IOMMU 장치 보호 해제함" #. TRANSLATORS: HSI event title msgid "IOMMU device protection enabled" msgstr "IOMMU 장치 보호 설정함" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "대기 중…" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "파ì¼ì„ 다운로드할 때 SSL 엄격한 검사 건너뛰기" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "펌웨어 ì²´í¬ì„¬ 오류 무시" #. TRANSLATORS: command line option msgid "Ignore firmware hardware mismatch failures" msgstr "펌웨어 하드웨어 ì¼ì¹˜ 오류 무시" #. TRANSLATORS: try to help msgid "Ignoring SSL strict checks, to do this automatically in the future export DISABLE_SSL_STRICT in your environment" msgstr "SSL 엄격한 검사 무시, ì°¨í›„ì— ìžë™ìœ¼ë¡œ ì ìš©í•˜ë ¤ë©´ DISABLE_SSL_STRICT 환경 변수를 지정해야 함" #. TRANSLATORS: the inhibit ID is a short string like dbus-123456 #, c-format msgid "Inhibit ID is %s." msgstr "억제 ID는 %s입니다." #. TRANSLATORS: command description msgid "Inhibit the system to prevent upgrades" msgstr "업그레ì´ë“œë¥¼ 막ë„ë¡ ì‹œìŠ¤í…œ ë™ìž‘ì„ ì–µì œí•©ë‹ˆë‹¤" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "설치 ì˜ˆìƒ ì‹œê°„" #. TRANSLATORS: command description msgid "Install a firmware file in cabinet format on this hardware" msgstr "ì´ í•˜ë“œì›¨ì–´ì— ìºë¹„ë‹› 형ì‹ì˜ 펌웨어 íŒŒì¼ ì„¤ì¹˜" #. TRANSLATORS: command description msgid "Install a raw firmware blob on a device" msgstr "ìž¥ì¹˜ì— ì›ì‹œ 펌웨어 블롭 설치" #. TRANSLATORS: command description msgid "Install a specific firmware file on all devices that match" msgstr "ì¼ì¹˜í•˜ëŠ” 모든 ìž¥ì¹˜ì— ì§€ì •í•œ 펌웨어 íŒŒì¼ ì„¤ì¹˜" #. TRANSLATORS: command description msgid "Install a specific firmware on a device, all possible devices will also be installed once the CAB matches" msgstr "ìž¥ì¹˜ì— ì§€ì • 펌웨어를 설치합니다. CABì´ ì¼ì¹˜í•˜ëŠ” 모든 가능한 장치ì—ë„ ì„¤ì¹˜í•©ë‹ˆë‹¤" msgid "Install old version of signed system firmware" msgstr "ì˜¤ëž˜ëœ ë²„ì „ì˜ ì„œëª…í•œ 시스템 펌웨어 설치" msgid "Install old version of unsigned system firmware" msgstr "ì˜¤ëž˜ëœ ë²„ì „ì˜ ì„œëª…í•˜ì§€ ì•Šì€ ì‹œìŠ¤í…œ 펌웨어 설치" msgid "Install signed device firmware" msgstr "ì„œëª…ëœ ìž¥ì¹˜ 펌웨어 설치" msgid "Install signed system firmware" msgstr "ì„œëª…ëœ ì‹œìŠ¤í…œ 펌웨어 설치" msgid "Install unsigned device firmware" msgstr "서명ë˜ì§€ ì•Šì€ ìž¥ì¹˜ 펌웨어 설치" msgid "Install unsigned system firmware" msgstr "서명ë˜ì§€ ì•Šì€ ì‹œìŠ¤í…œ 펌웨어 설치" #. TRANSLATORS: stay on one firmware version unless the new version is #. explicitly #. * specified msgid "Installing a specific release is explicitly required" msgstr "특정한 릴리스 설치를 명시ì ìœ¼ë¡œ 지정해야 함" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "펌웨어 ì—…ë°ì´íЏ 설치 중…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "%sì— ì„¤ì¹˜ 중..." #. TRANSLATORS: if it breaks, you get to keep both pieces msgid "Installing this update may also void any device warranty." msgstr "ì´ ì—…ë°ì´íŠ¸ë¥¼ 설치하면 장치 ë³´ì¦ì´ 깨질 수 있습니다." #. TRANSLATORS: The BIOS setting only accepts integers in a fixed range msgid "Integer" msgstr "정수값" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "ì¸í…” BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM Protected" msgstr "ì¸í…” BootGuard ACM 보호" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "ì¸í…” BootGuard ACM 보호ë¨" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard Error Policy" msgstr "ì¸í…” BootGuard 오류 ì •ì±…" msgid "Intel BootGuard Error Policy ensures the device does not continue to start if its device software has been tampered with." msgstr "ì¸í…” BootGuard 오류 ì •ì±…ì€ ìž¥ì¹˜ 소프트웨어가 ìž„ì˜ë¡œ 변경ë˜ì—ˆì„ 때 장치 시작 ìž‘ì—…ì´ ì¤‘ë‹¨ë˜ë„ë¡ í•©ë‹ˆë‹¤." #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard Fuse" msgstr "ì¸í…” BootGuard 퓨즈" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "ì¸í…” BootGuard OTP 퓨즈" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard Verified Boot" msgstr "ì¸í…” BootGuard ê²€ì¦ëœ 부트" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "ì¸í…” BootGuard 오류 ì •ì±…" #. TRANSLATORS: longer description msgid "Intel BootGuard prevents unauthorized device software from operating when the device is started." msgstr "ì¸í…” BootGuardì—서 장치 ë™ìž‘ì„ ì‹œìž‘í•  때 ì¸ì¦í•˜ì§€ ì•Šì€ ìž¥ì¹˜ í”„ë¡œê·¸ëž¨ì˜ ë™ìž‘ì„ ë§‰ìŠµë‹ˆë‹¤." #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "ì¸í…” BootGuard ê²€ì¦ëœ 부트" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS Mitigation" msgstr "ì¸í…” GDS 완화" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS mitigation" msgstr "ì¸í…” GDS 완화" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Manufacturing Mode" msgstr "ì¸í…” 관리 엔진 제조사 모드" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the "override" is #. enabled #. * with a jumper -- luckily it is probably not accessible to end users on #. consumer #. * boards msgid "Intel Management Engine Override" msgstr "ì¸í…” 관리 엔진 무시" #. TRANSLATORS: longer description msgid "Intel Management Engine Override disables checks for device software tampering." msgstr "ì¸í…” 관리 엔진 오버ë¼ì´ë“œëŠ” 장치 í”„ë¡œê·¸ëž¨ì˜ ë¶€ë‹¹ 변경 검사를 ë•니다." #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Version" msgstr "ì¸í…” 관리 엔진 버전" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "내장 장치" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "올바르지 않ìŒ" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "ë¶€ì ì ˆí•œ ì¸ìž ê°’" #. TRANSLATORS: error message msgid "Invalid arguments, expected GUID" msgstr "ìž˜ëª»ëœ ì¸ìž, GUID를 예ìƒí•¨" #. TRANSLATORS: error message msgid "Invalid arguments, expected at least ARCHIVE FIRMWARE METAINFO" msgstr "ë¶€ì ì ˆí•œ ì¸ìž. 최소한 <ì•„ì¹´ì´ë¸Œ> <펌웨어> <메타정보> ê°€ 필요합니다" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "다운그레ì´ë“œ 버전" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "현재 ë¶€íŠ¸ë¡œë” ëª¨ë“œì— ìžˆìŒ" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "업그레ì´ë“œ 버전" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "문제ì " #. TRANSLATORS: HSI event title msgid "Kernel is no longer tainted" msgstr "커ë„ì— ë”ì´ìƒ 문제 요소가 ì—†ìŒ" #. TRANSLATORS: HSI event title msgid "Kernel is tainted" msgstr "커ë„ì— ë¬¸ì œ 요소 ë°œìƒ" #. TRANSLATORS: HSI event title msgid "Kernel lockdown disabled" msgstr "ì»¤ë„ ìž ê¸ˆ 해제함" #. TRANSLATORS: HSI event title msgid "Kernel lockdown enabled" msgstr "ì»¤ë„ ìž ê¸ˆ 설정함" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "<위치>" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "마지막으로 수정한 ë‚ ì§œ" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "1ë¶„ 미만 남ìŒ" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "ë¼ì´ì„ ìФ" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "리눅스 ì»¤ë„ ë½ë‹¤ìš´" #. TRANSLATORS: longer description msgid "Linux Kernel Lockdown mode prevents administrator (root) accounts from accessing and changing critical parts of system software." msgstr "리눅스 ì»¤ë„ ìž ê¸ˆ 모드ì—서는 관리ìž(루트) ê³„ì •ì´ ì‹œìŠ¤í…œ í”„ë¡œê·¸ëž¨ì˜ ì¤‘ìš”í•œ ë¶€ë¶„ì„ ì ‘ê·¼í•˜ê±°ë‚˜ 바꾸는 행위를 막습니다." msgid "Linux Kernel Swap temporarily saves information to disk as you work. If the information is not protected, it could be accessed by someone if they obtained the disk." msgstr "리눅스 ì»¤ë„ ìŠ¤ì™‘ 기능ì—서는 작업한 ê²°ê³¼ 정보를 임시로 디스í¬ì— 저장합니다. 정보를 보호하는 ìƒíƒœê°€ 아니ë¼ë©´ 디스í¬ì— 접근한 누군가가 해당 ì •ë³´ì— ì ‘ê·¼í•  수 있습니다." #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "리눅스 ì»¤ë„ ê²€ì¦" msgid "Linux Kernel Verification makes sure that critical system software has not been tampered with. Using device drivers which are not provided with the system can prevent this security feature from working correctly." msgstr "리눅스 ì»¤ë„ ê²€ì¦ ê³¼ì •ì—서 중요한 시스템 í”„ë¡œê·¸ëž¨ì´ ìž„ì˜ë¡œ 변경하지 않았는지 확ì¸í•©ë‹ˆë‹¤. 시스템ì—서 제공하지 않는 장치 드ë¼ì´ë²„를 사용하면 ì´ ë³´ì•ˆ ê¸°ëŠ¥ì´ ì˜¬ë°”ë¥´ê²Œ ë™ìž‘하지 못하게 ë§‰ì„ ìˆ˜ 있습니다." #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "리눅스 스왑" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "리눅스 제조사 펌웨어 서비스(안정 버전 펌웨어)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "리눅스 제조사 펌웨어 서비스(테스트 버전 펌웨어)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "리눅스 커ë„" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "리눅스 ì»¤ë„ Lockdown" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "리눅스 스왑" #. TRANSLATORS: command description msgid "List EFI variables with a specific GUID" msgstr "지정한 GUID를 사용하는 EFI 변수 표시" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "dbxì˜ í•­ëª© 표시" #. TRANSLATORS: command description msgid "List the available firmware GTypes" msgstr "사용 가능한 펌웨어 GType ëª©ë¡ í‘œì‹œ" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "사용 가능한 펌웨어 종류 표시" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "ESPì— ì €ìž¥ëœ íŒŒì¼ í‘œì‹œ" #. TRANSLATORS: command description msgid "Load device emulation data" msgstr "장치 ì—뮬레ì´ì…˜ ë°ì´í„°ë¥¼ 불러옵니다" #. TRANSLATORS: the plugin was created from a .so object, and was not built-in msgid "Loaded from an external module" msgstr "외부 모듈ì—서 불러옴" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "불러오는 중…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "ìž ê¹€" #. TRANSLATORS: the release urgency msgid "Low" msgstr "ë‚®ìŒ" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refers #. * to the private/public key used to secure loading of firmware msgid "MEI Key Manifest" msgstr "MEI 키 매니페스트" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refer #. * to the private/public key used to secure loading of firmware msgid "MEI key manifest" msgstr "MEI 키 매니페스트" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "MEI 제조 모드" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "MEI 재정ì˜" msgid "MEI version" msgstr "MEI 버전" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "지정한 플러그ì¸ì„ 수ë™ìœ¼ë¡œ 활성화" #. TRANSLATORS: longer description msgid "Manufacturing Mode is used when the device is manufactured and security features are not yet enabled." msgstr "제조사 모드는 장치를 제조한 후 보안 ê¸°ëŠ¥ì„ ì•„ì§ ì¼œì§€ ì•Šì•˜ì„ ë•Œ 사용합니다." #. TRANSLATORS: Longest valid string for BIOS setting msgid "Maximum length" msgstr "최대 길ì´" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "최대값" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "중간" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "메타ë°ì´í„° 서명" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "메타ë°ì´í„° URI" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "리눅스 제조사 펌웨어 서비스ì—서 메타ë°ì´í„°ë¥¼ 가져올 수 있습니다." #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "최소 버전" #. TRANSLATORS: Shortest valid string for BIOS setting msgid "Minimum length" msgstr "최소 길ì´" #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "최소값" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "ë°ëª¬ 설정값 수정" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "지정한 ì›ê²© 저장소 수정" msgid "Modify a configured remote" msgstr "ì›ê²© 설정 수정" msgid "Modify daemon configuration" msgstr "ë°ëª¬ 설정 수정" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "ë°ëª¬ ì´ë²¤íЏ ê°ì‹œ" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "ESP 마운트" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "설치 후 다시 시작 필요함" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "다시 시작 í•„ìš”" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "설치 후 종료 필요함" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "새 버전" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "ë™ìž‘ì„ ì§€ì •í•˜ì§€ 않았습니다!" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "%sì˜ ë‹¤ìš´ê·¸ë ˆì´ë“œ ì—†ìŒ" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "펌웨어 ID를 ì°¾ì„ ìˆ˜ ì—†ìŒ" #. TRANSLATORS: nothing found msgid "No firmware found" msgstr "펌웨어가 없습니다" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "펌웨어를 ì—…ë°ì´íŠ¸í•  수 있는 하드웨어가 ì—†ìŒ" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "사용 가능한 릴리스 ì—†ìŒ" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "ì›ê²© 저장소가 설정ë˜ì§€ 않았으므로 메타ë°ì´í„°ë¥¼ 사용할 수 없습니다." #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "ì›ê²© 저장소 ì—†ìŒ" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "ì—…ë°ì´íŠ¸í•  수 있는 장치가 없습니다" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "ì—…ë°ì´íŠ¸ê°€ 없습니다" msgid "No updates available for remaining devices" msgstr "ë‚¨ì€ ìž¥ì¹˜ì˜ ì—…ë°ì´íŠ¸ê°€ 없습니다" #. TRANSLATORS: version cannot be installed due to policy msgid "Not approved" msgstr "ìŠ¹ì¸ ì•ˆí•¨" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "ì°¾ì„ ìˆ˜ ì—†ìŒ" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "ì§€ì›í•˜ì§€ 않ìŒ" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "확ì¸" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "확ì¸!" #. TRANSLATORS: the firmware old version msgid "Old version" msgstr "ì´ì „ 버전" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "ë‹¨ì¼ PCR ê°’ë§Œ 표시" #. TRANSLATORS: command line option msgid "Only use peer-to-peer networking when downloading files" msgstr "파ì¼ì„ 다운로드할 때 피어투피어 네트워í¬ë§Œ 사용" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "버전 업그레ì´ë“œë§Œ 가능합니다" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "기본 ESP 경로 재정ì˜" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "PATH" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "펌웨어 파ì¼ì„ 처리하고 세부 ì •ë³´ 표시" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "dbx ì—…ë°ì´íЏ í•´ì„ ì¤‘..." #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "시스템 dbx í•´ì„ ì¤‘..." #. TRANSLATORS: remote filename base msgid "Password" msgstr "암호" #. TRANSLATORS: command description msgid "Patch a firmware blob at a known offset" msgstr "알려진 ì˜¤í”„ì…‹ì— íŽŒì›¨ì–´ 블롭 패치" msgid "Payload" msgstr "페ì´ë¡œë“œ" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "대기 중" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "ìž‘ì—…ì„ ìˆ˜í–‰í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "í”Œëž«í¼ ë””ë²„ê¹…" #. TRANSLATORS: longer description msgid "Platform Debugging allows device security features to be disabled. This should only be used by hardware manufacturers." msgstr "í”Œëž«í¼ ë””ë²„ê¹…ì—서 장치 보안 기능 비활성화를 허용합니다. ì´ ê¸°ëŠ¥ì€ í•˜ë“œì›¨ì–´ 제조사ì—서만 사용해야 합니다." #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "í”Œëž«í¼ ë””ë²„ê¹…" #. TRANSLATORS: 'recovery key' here refers to a code, rather than a physical #. metal thing msgid "Please ensure you have the volume recovery key before continuing." msgstr "계ì†í•˜ì‹œ ì „ 볼륨 ë³µì› í‚¤ê°€ 있는지 확ì¸í•˜ì‹­ì‹œì˜¤." #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "0ì—서 %uê¹Œì§€ì˜ ìˆ«ìž ì¤‘ 하나를 입력하십시오:" #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "í”ŒëŸ¬ê·¸ì¸ ì˜ì¡´ì„±ì„ ì°¾ì„ ìˆ˜ ì—†ìŒ" #. TRANSLATORS: Possible values for a bios setting msgid "Possible Values" msgstr "가능한 ê°’" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA Protection" msgstr "부팅 ì „ DMA 보호" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "부트 ì „ DMA 보호" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is disabled" msgstr "부팅 ì „ DMA 보호를 해제했습니다" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is enabled" msgstr "부팅 ì „ DMA 보호를 설정했습니다" #. TRANSLATORS: longer description msgid "Pre-boot DMA protection prevents devices from accessing system memory after being connected to the computer." msgstr "부팅 ì „ DMA 보호 ì •ì±…ì—서 ì»´í“¨í„°ì— ì—°ê²°í•œ 장치ì—ì„œì˜ ì‹œìŠ¤í…œ 메모리 ì ‘ê·¼ì„ ë§‰ìŠµë‹ˆë‹¤." #. TRANSLATORS: warning message msgid "Press unlock on the device to continue the update process." msgstr "ì—…ë°ì´íЏ ê³¼ì •ì„ ê³„ì†í•˜ë ¤ë©´ 장치 잠금 해제를 누르십시오." #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "ì´ì „ 버전" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "ìš°ì„  순위" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "문제" msgid "Proceed with upload?" msgstr "업로드를 진행하시겠습니까?" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Processor Security Checks" msgstr "프로세서 보안 검사" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "Processor rollback protection" msgstr "프로세서 롤백 보호" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "ë…ì ì " #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "REMOTE-ID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID KEY VALUE" msgstr "REMOTE-ID KEY VALUE" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "ì½ê¸° ì „ìš©" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "장치ì—서 펌웨어 ë°”ì´ë„ˆë¦¬ ì½ê¸°" #. TRANSLATORS: command description msgid "Read a firmware from a device" msgstr "장치ì—서 펌웨어 ì½ê¸°" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "%sì—서 ì½ëŠ” 중..." #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "ì½ëŠ” 중…" #. TRANSLATORS: how often we should refresh the metadata msgid "Refresh Interval" msgstr "새로 고침 간격" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "ì›ê²© 서버ì—서 메타ë°ì´í„° 새로 고침" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "%sì— %sì„(를) 다시 설치하시겠습니까?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "ìž¥ì¹˜ì— í˜„ìž¬ 펌웨어 다시 설치" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "ìž¥ì¹˜ì— íŽŒì›¨ì–´ 다시 설치" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "릴리스 브랜치" #. TRANSLATORS: release attributes msgid "Release Flags" msgstr "출시 플래그" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "출시 ID" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "ì›ê²© ID" #. TRANSLATORS: command description msgid "Removes devices to watch for future emulation" msgstr "향후 ì—뮬레ì´ì…˜ì„ ê°ì‹œí•  장치를 제거합니다" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "보고서 URI" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "ì›ê²© ì„œë²„ì— ë³´ê³ ë¨" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "필요한 efivarfs íŒŒì¼ ì‹œìŠ¤í…œì„ ì°¾ì„ ìˆ˜ ì—†ìŒ" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "필요한 하드웨어를 ì°¾ì„ ìˆ˜ ì—†ìŒ" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "ë¶€íŠ¸ë¡œë” ëª¨ë“œ ì§„ìž… 필요함" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "ì¸í„°ë„· ì—°ê²° í•„ìš”" msgid "Reset daemon configuration" msgstr "ë°ëª¬ 설정 초기화" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "지금 다시 시작하시겠습니까?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "ë°ëª¬ì„ 다시 시작하여 변경 ì‚¬í•­ì„ ì ìš©í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "장치 다시 시작하는 중…" #. TRANSLATORS: command description msgid "Retrieve BIOS settings. If no arguments are passed all settings are returned" msgstr "BIOS ì„¤ì •ì„ ê°€ì ¸ì˜µë‹ˆë‹¤. 전달하는 ì¸ìž ê°’ì´ ì—†ìœ¼ë©´ 모든 설정 ê°’ì„ ë°˜í™˜í•©ë‹ˆë‹¤" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "ë¨¸ì‹ ì˜ ëª¨ë“  하드웨어 ID 반환" #. TRANSLATORS: longer description msgid "Rollback Protection prevents device software from being downgraded to an older version that has security problems." msgstr "롤백 보호 ì •ì±…ì—서 보안 문제를 야기하는 장치 í”„ë¡œê·¸ëž¨ì˜ ì´ì „ 버전 다운 그레ì´ë“œë¥¼ 막습니다." #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "install-blob 사용 시 í”ŒëŸ¬ê·¸ì¸ ì •ë¦¬ 루틴 실행" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "install-blob 사용 시 í”ŒëŸ¬ê·¸ì¸ ì¤€ë¹„ 루틴 실행" #. TRANSLATORS: tell a user how to get information #, c-format msgid "Run without '%s' to see" msgstr "확ì¸í•˜ë ¤ë©´ '%s'ì„(를) 빼고 실행하십시오" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "실행 ì¤‘ì¸ ì»¤ë„ì´ ë„ˆë¬´ 오래ë˜ì—ˆìŠµë‹ˆë‹¤" #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "런타임 접미사" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING VALUE" msgstr "<설정> <ê°’>" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING1 VALUE1 [SETTING2] [VALUE2]" msgstr "<설정1> <ê°’1> [<설정2>] [<ê°’2>]" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "SMAP" msgstr "SMAP" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "SPI BIOS 설명ìž" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "SPI BIOS ì˜ì—­" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "SPI 잠금" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "SPI replay protection" msgstr "SPI ë¦¬í”Œë ˆì´ ë³´í˜¸" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "SPI 기ë¡" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "SPI write protection" msgstr "SPI ê¸°ë¡ ë³´í˜¸" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" #. TRANSLATORS: command description msgid "Save a file that allows generation of hardware IDs" msgstr "하드웨어 ID를 ìƒì„±í•  수 있는 íŒŒì¼ ì €ìž¥" #. TRANSLATORS: command description msgid "Save device emulation data" msgstr "장치 ì—뮬레ì´ì…˜ ë°ì´í„°ë¥¼ 저장합니다" #. TRANSLATORS: Scalar increment for integer BIOS setting msgid "Scalar Increment" msgstr "ìŠ¤ì¹¼ë¼ ì¦ê°€ê°’" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "작업 ê³„íš ì¤‘â€¦" #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "보안 부팅 비활성" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "보안 부팅 활성" #. TRANSLATORS: the %1 is a URL to a wiki page #, c-format msgid "See %s for more details." msgstr "ìžì„¸í•œ 정보는 %s 사ì´íŠ¸ë¥¼ 참조하십시오." #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "ìžì„¸í•œ 정보는 %s 사ì´íŠ¸ë¥¼ 참조하십시오." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "ì„ íƒí•œ 장치" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "ì„ íƒí•œ 볼륨" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "ì¼ë ¨ 번호" #. TRANSLATORS: Configured a BIOS setting to a value #, c-format msgid "Set BIOS setting '%s' using '%s'." msgstr "'%s' BIOS ì„¤ì •ì„ '%s'(으)로 설정합니다." #. TRANSLATORS: command description msgid "Set a BIOS setting" msgstr "BIOS 설정 ê°’ì„ ì„¤ì •" msgid "Set one or more BIOS settings" msgstr "하나 ì´ìƒì˜ BIOS 설정 ê°’ 설정" #. TRANSLATORS: command description msgid "Sets one or more BIOS settings" msgstr "하나 ì´ìƒì˜ BIOS 설정 ê°’ì„ ì„¤ì •í•©ë‹ˆë‹¤" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "í—ˆìš©ëœ íŽŒì›¨ì–´ ëª©ë¡ ì„¤ì •" #. TRANSLATORS: type of BIOS setting msgid "Setting type" msgstr "설정 ë°ì´í„°í˜•" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "ì‹œìŠ¤í…œì„ ë‹¤ì‹œ 부팅하고 나면 ì„¤ì •ì„ ë°˜ì˜í•©ë‹ˆë‹¤" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "개발사와 펌웨어 ì—…ë°ì´íЏ ê¸°ë¡ ê³µìœ í•˜ê¸°" #. TRANSLATORS: command line option msgid "Show all results" msgstr "모든 ê²°ê³¼ 표시" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "í´ë¼ì´ì–¸íŠ¸ì™€ ë°ëª¬ 버전 표시" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "지정한 ë„ë©”ì¸ì˜ ìžì„¸í•œ ë°ëª¬ ì •ë³´ 표시" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "모든 ë„ë©”ì¸ì˜ 디버깅 ì •ë³´ 표시" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "디버깅 ì˜µì…˜ì„ í‘œì‹œí•©ë‹ˆë‹¤" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "ì—…ë°ì´íŠ¸í•  수 없는 장치 표시" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "추가 디버깅 ì •ë³´ 표시" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "펌웨어 ì—…ë°ì´íЏ ê¸°ë¡ ë³´ê¸°" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "dbxì˜ ê³„ì‚°ëœ ë²„ì „ 표시" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "지금 종료하시겠습니까?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "새 키로 펌웨어 서명" msgid "Sign data using the client certificate" msgstr "í´ë¼ì´ì–¸íЏ ì¸ì¦ì„œë¡œ ë°ì´í„° 서명" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "í´ë¼ì´ì–¸íЏ ì¸ì¦ì„œë¡œ ë°ì´í„° 서명" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "í´ë¼ì´ì–¸íЏ ì¸ì¦ì„œë¡œ ì—…ë¡œë“œëœ ë°ì´í„° 서명" msgid "Signature" msgstr "서명" #. TRANSLATORS: firmware is verified on-device the payload using strong crypto msgid "Signed Payload" msgstr "서명한 페ì´ë¡œë“œ" #. TRANSLATORS: file size of the download msgid "Size" msgstr "í¬ê¸°" #. TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM msgid "Some of the platform secrets may be invalidated when updating this firmware." msgstr "ì´ íŽŒì›¨ì–´ë¥¼ ì—…ë°ì´íŠ¸í•˜ë©´ 장치 비밀키 ì¼ë¶€ê°€ 훼ì†ë  수 있습니다." #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "ì›ë³¸" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "dbx ë°ì´í„°ë² ì´ìФ íŒŒì¼ ì§€ì •" #. TRANSLATORS: The BIOS setting accepts strings msgid "String" msgstr "문ìžì—´" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "성공" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "모든 장치를 활성화함" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "ì›ê²© 저장소를 비활성화함" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "새 메타ë°ì´í„°ë¥¼ 다운로드함:" #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "ì›ê²© 저장소를 활성화하고 새로 고침" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "ì›ê²© 저장소를 활성화함" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "펌웨어 설치 성공" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "ì„¤ì •ì„ ìˆ˜ì •í•¨" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "ì›ê²© 저장소를 수정함" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "메타ë°ì´í„°ë¥¼ 수ë™ìœ¼ë¡œ ì—…ë°ì´íŠ¸í•¨" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "장치 ì²´í¬ì„¬ì„ ì—…ë°ì´íŠ¸í•¨" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "보고서 %uê°œ 업로드함" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "장치 ì²´í¬ì„¬ì„ ê²€ì¦í•¨" #. TRANSLATORS: the device showed up in time #, c-format msgid "Successfully waited %.0fms for device" msgstr "%.0f밀리초ë™ì•ˆ 장치 ì‘답 ëŒ€ê¸°ì— ì„±ê³µí–ˆìŠµë‹ˆë‹¤" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "요약" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "Supervisor Mode Access Prevention" msgstr "ê´€ë¦¬ìž ëª¨ë“œ ì ‘ê·¼ ë°©ì§€" #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "ì§€ì›í•¨" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "ì§€ì› CPU" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "ì›ê²© 서버ì—서 ì§€ì›í•¨" #. TRANSLATORS: Title: a better sleep state msgid "Suspend To Idle" msgstr "최대 절전" #. TRANSLATORS: Title: sleep state msgid "Suspend To RAM" msgstr "RAM 대기" #. TRANSLATORS: longer description msgid "Suspend to Idle allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "대기 ìƒíƒœëŠ” 장치를 빨리 ì „ì› ì ˆì•½ 대기 모드로 진입하게 합니다. 장치가 대기 ìƒíƒœì— 들어가면, 메모리를 물리ì ìœ¼ë¡œ 제거하고 ì •ë³´ì— ì ‘ê·¼í•  수 있습니다." #. TRANSLATORS: longer description msgid "Suspend to RAM allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "RAM 대기 ìƒíƒœëŠ” 장치를 빨리 ì „ì› ì ˆì•½ 대기모드로 진입하게 합니다. 장치가 대기 ìƒíƒœì— 들어가면, 메모리를 물리ì ìœ¼ë¡œ 제거하고 ì •ë³´ì— ì ‘ê·¼í•  수 있습니다." #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "Idle 절전" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "RAM 절전" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "브랜치를 %sì—서 %s(으)로 전환하시겠습니까?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "ìž¥ì¹˜ì˜ íŽŒì›¨ì–´ 브랜치 전환" #. TRANSLATORS: this CLI tool is now preventing system updates msgid "System Update Inhibited" msgstr "시스템 ì—…ë°ì´íЏ ë™ìž‘ì„ ì–µì œí–ˆìŠµë‹ˆë‹¤" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "ì‹œìŠ¤í…œì— ì™¸ë¶€ ì „ì›ì„ 연결해야 함" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "TEXT" #. TRANSLATORS: longer description msgid "TPM (Trusted Platform Module) is a computer chip that detects when hardware components have been tampered with." msgstr "TPM(신뢰 í”Œëž«í¼ ëª¨ë“ˆ)ì€ í•˜ë“œì›¨ì–´ ë¶€í’ˆì˜ ìž„ì˜ ë³€ê²½ 여부를 ê°ì§€í•˜ëŠ” 컴퓨터 칩입니다." #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "TPM PCR0 재구축" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is invalid" msgstr "TPM PCR0 재구성 ìƒíƒœê°€ 올바르지 않습니다" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is now valid" msgstr "TPM PCR0 재구성 ìƒíƒœê°€ ì´ì œ 올바릅니다" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM Platform Configuration" msgstr "TPM í”Œëž«í¼ ì„¤ì •" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM Reconstruction" msgstr "TPM 재구성" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM empty PCRs" msgstr "TPMì´ PCRì„ ë¹„ì›ë‹ˆë‹¤" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "태그" #. TRANSLATORS: we're saving all USB events for emulation msgid "Tagged for emulation" msgstr "ì—뮬레ì´íŒ… 목ì ìœ¼ë¡œ 태깅함" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "오염ë¨" #. show the user the entire data blob msgid "Target" msgstr "대ìƒ" #. TRANSLATORS: command description msgid "Test a device using a JSON manifest" msgstr "JSON 매니페스트 장치를 시험합니다" #. TRANSLATORS: when the release was tested msgid "Tested" msgstr "시험완료" #. TRANSLATORS: the %s is a vendor name, e.g. Lenovo #, c-format msgid "Tested by %s" msgstr "%sì´(ê°€) 시험함" #. TRANSLATORS: someone we trust has tested this msgid "Tested by trusted vendor" msgstr "신뢰하는 제조사ì—서 시험했습니다" #. TRANSLATORS: longer description msgid "The Intel Management Engine Key Manifest must be valid so that the device firmware can be trusted by the CPU." msgstr "ì¸í…” 관리 엔진 키 매니페스트가 올바르게 설정ë˜ì–´ 있어야 CPUì—서 장치 펌웨어를 신뢰할 수 있습니다." #. TRANSLATORS: longer description msgid "The Intel Management Engine controls device components and needs to have a recent version to avoid security issues." msgstr "ì¸í…” 관리 ì—”ì§„ì€ ìž¥ì¹˜ 구성 요소를 관리하며 보안 문제를 막ë„ë¡ ìµœì‹  ë²„ì „ì„ ì„¤ì¹˜í•´ì•¼ 합니다." #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "LVFS는 $OS_RELEASE:NAME$와(ê³¼) 별개로 ìš´ì˜ë˜ëŠ” ë…ë¦½ëœ ë²•ì  ë‹¨ì²´ì—서 ìš´ì˜í•˜ëŠ” 무료 서비스입니다. ë°°í¬íŒ 개발사ì—서 펌웨어 ì—…ë°ì´íŠ¸ì™€ 시스템 ë° ì—°ê²°í•œ ìž¥ì¹˜ì˜ í˜¸í™˜ì„±ì„ ê²€ì¦í•œë‹¤ëŠ” ë³´ìž¥ì´ ì—†ìŠµë‹ˆë‹¤. 모든 펌웨어는 장치 제조사(OEM)ì—서 ì§ì ‘ 제공합니다." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Platform Configuration is used to check whether the device start process has been modified." msgstr "TPM(신뢰 í”Œëž«í¼ ëª¨ë“ˆ) í”Œëž«í¼ êµ¬ì„± ì •ì±…ì€ ìž¥ì¹˜ê°€ ìƒíƒœê°€ ë°”ë€ í”„ë¡œì„¸ìŠ¤ë¥¼ 시작하는지 여부를 확ì¸í•  때 사용합니다." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Reconstruction is used to check whether the device start process has been modified." msgstr "TPM(신뢰 í”Œëž«í¼ ëª¨ë“ˆ) 재구성 ì •ì±…ì€ ìž¥ì¹˜ê°€ ìƒíƒœê°€ ë°”ë€ í”„ë¡œì„¸ìŠ¤ë¥¼ 시작하는지 여부를 확ì¸í•  때 사용합니다." #. TRANSLATORS: this is more background on a security measurement problem msgid "The TPM PCR0 differs from reconstruction." msgstr "TPM PCR0ì´ ìž¬êµ¬ì¶•í•œ ê°’ê³¼ 다릅니다." #. TRANSLATORS: longer description msgid "The UEFI Platform Key is used to determine if device software comes from a trusted source." msgstr "UEFI í”Œëž«í¼ í‚¤ëŠ” 장치 í”„ë¡œê·¸ëž¨ì´ ì‹ ë¢°í•˜ëŠ” 공급ì›ì—서 제공하는지 여부를 확ì¸í•  때 사용합니다." #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "ë°ëª¬ì—서 ì œ3ìž ì½”ë“œë¥¼ 불러왔으며 업스트림 개발ìžëŠ” ë” ì´ìƒ ì§€ì›í•˜ì§€ 않습니다!" #. TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name #, c-format msgid "The firmware from %s is not supplied by %s, the hardware vendor." msgstr "%sì˜ íŽŒì›¨ì–´ëŠ” 하드웨어 제조사 %sì—서 제공하지 않았습니다." #. TRANSLATORS: try to help msgid "The system clock has not been set correctly and downloading files may fail." msgstr "시스템 시계가 올바르게 설정ë˜ì–´ 있지 않습니다. íŒŒì¼ ë‹¤ìš´ë¡œë“œê°€ 실패할 ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been re-inserted." msgstr "장치 USB ì¼€ì´ë¸”ì„ ë½‘ê³ ë‚œ 후 다시 연결하면 ì—…ë°ì´íŠ¸ë¥¼ 계ì†í•©ë‹ˆë‹¤." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged and then re-inserted." msgstr "장치 USB ì¼€ì´ë¸”ì„ ë½‘ê³ ë‚œ 후 다시 연결하면 ì—…ë°ì´íŠ¸ë¥¼ 계ì†í•©ë‹ˆë‹¤." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged." msgstr "장치 USB ì¼€ì´ë¸”ì„ ë½‘ìœ¼ë©´ ì—…ë°ì´íŠ¸ë¥¼ 계ì†í•©ë‹ˆë‹¤." #. TRANSLATORS: naughty vendor msgid "The vendor did not supply any release notes." msgstr "제조사ì—서 출시 기ë¡ì„ 제공하지 않았습니다." #. TRANSLATORS: now list devices with unfixed high-priority issues msgid "There are devices with issues:" msgstr "ìž¥ì¹˜ì— ë¬¸ì œê°€ 있습니다:" #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "ì°¨ë‹¨ëœ íŽŒì›¨ì–´ íŒŒì¼ ì—†ìŒ" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "í—ˆìš©ëœ íŽŒì›¨ì–´ê°€ 없습니다." #. TRANSLATORS: %1 is the current device version number, and %2 is the #. command name, e.g. `fwupdmgr sync` #, c-format msgid "This device will be reverted back to %s when the %s command is performed." msgstr "ì´ ìž¥ì¹˜ëŠ” %s(으)로 %s ëª…ë ¹ì„ ì‹¤í–‰í•˜ë©´ ë˜ëŒë¦½ë‹ˆë‹¤." #. TRANSLATORS: the vendor did not upload this msgid "This firmware is provided by LVFS community members and is not provided (or supported) by the original hardware vendor." msgstr "ì´ íŽŒì›¨ì–´ëŠ” LVFS 커뮤니티 구성ì›ì´ 제공하며, ì› í•˜ë“œì›¨ì–´ 제조사ì—서 제공(ë˜ëŠ” ì§€ì›)하지 않습니다." #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "ì´ íŒ¨í‚¤ì§€ëŠ” ê²€ì¦ë˜ì§€ 않았습니다. 올바르게 ìž‘ë™í•˜ì§€ ì•Šì„ ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "ì´ í”„ë¡œê·¸ëž¨ì€ ë£¨íŠ¸ 권한으로만 올바르게 ìž‘ë™í•  ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "ì´ ì›ê²© 저장소ì—서는 하드웨어 제조사ì—서 ê²€ì¦ ë‹¨ê³„ë¥¼ ì§„í–‰ ì¤‘ì¸ íŽŒì›¨ì–´ë¥¼ ë°°í¬í•©ë‹ˆë‹¤. 펌웨어 ì—…ë°ì´íЏ ë„중 ë° ì´í›„ 문제가 ë°œìƒí–ˆì„ 경우를 대비하여 ì§ì ‘ 펌웨어를 다운그레ì´ë“œí•  ë°©íŽ¸ì„ í™•ë³´í•˜ê¸°ë¥¼ 추천합니다." #. TRANSLATORS: error message msgid "This system doesn't support firmware settings" msgstr "ì´ ì‹œìŠ¤í…œì—서는 펌웨어 ì„¤ì •ì„ ì§€ì›í•˜ì§€ 않습니다" #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "ì‹œìŠ¤í…œì— HSI 런타임 문제가 있습니다." #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "시스템ì—서 HSI 보안 등급 ë‚®ìŒì„ 사용 중입니다." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "ì´ ë„구를 사용하면 시스템 관리ìžê°€ UEFI dbx ì—…ë°ì´íŠ¸ë¥¼ ì ìš©í•  수 있습니다." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "ì´ ë„구를 사용하면 시스템 관리ìžê°€ 펌웨어 설치 ë° ë‹¤ìš´ê·¸ë ˆì´ë“œ 등 ë™ìž‘ì„ ìˆ˜í–‰í•˜ë„ë¡ fwupd ë°ëª¬ì— 질ì˜í•˜ê³  제어할 수 있습니다." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "ì´ ë„구를 사용하면 시스템 관리ìžê°€ fwupd 플러그ì¸ì„ 호스트 ì‹œìŠ¤í…œì— ì„¤ì¹˜í•˜ì§€ 않고 사용할 수 있습니다." #. TRANSLATORS: the %1 is a BIOS setting name. #. * %2 and %3 are the values, e.g. "True" or "Windows10" #, c-format msgid "This tool can change the BIOS setting '%s' from '%s' to '%s' automatically, but it will only be active after restarting the computer." msgstr "ì´ ë„구ì—서 현재 '%s' BIOS ì„¤ì •ì„ '%s'ì—서 '%s'(으)로 ìžë™ìœ¼ë¡œ 바꿀 수 있습니다만, 컴퓨터를 다시 시작해야 합니다." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "ì´ ë„구는 시스템 펌웨어ì—서 TPM ì´ë²¤íЏ 로그를 ì½ì–´ì„œ í•´ì„합니다." #. TRANSLATORS: the update state of the specific device msgid "Transient failure" msgstr "ì¼ì‹œì  실패" #. TRANSLATORS: item is TRUE msgid "True" msgstr "ì°¸" #. TRANSLATORS: We verified the metadata against the server msgid "Trusted metadata" msgstr "신뢰하는 메타ë°ì´í„°" #. TRANSLATORS: We verified the payload against the server msgid "Trusted payload" msgstr "신뢰하는 페ì´ë¡œë“œ" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "형ì‹" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI Bootservice Variables" msgstr "UEFI 부트 서비스 변수" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition may not be set up correctly" msgstr "UEFI ESP íŒŒí‹°ì…˜ì´ ì˜¬ë°”ë¥´ê²Œ 설정ë˜ì§€ ì•Šì•˜ì„ ìˆ˜ë„ ìžˆìŒ" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "UEFI ESP íŒŒí‹°ì…˜ì´ ê°ì§€ë˜ì§€ 않았거나 설정ë˜ì§€ 않았ìŒ" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "UEFI í”Œëž«í¼ í‚¤" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "UEFI 보안 부트" #. TRANSLATORS: longer description msgid "UEFI Secure Boot prevents malicious software from being loaded when the device starts." msgstr "UEFI 보안 ë¶€íŒ…ì€ ìž¥ì¹˜ë¥¼ 시작할 때 ì•…ì˜ì ì¸ í”„ë¡œê·¸ëž¨ì„ ë¶ˆëŸ¬ì˜¤ì§€ 못하게 막아ì¤ë‹ˆë‹¤." #. TRANSLATORS: longer description msgid "UEFI boot service variables should not be readable from runtime mode." msgstr "UEFI 부트 서비스 변수는 런타임 모드ì—서 ì½ì–´ì˜¬ 수 없어야 합니다." #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI bootservice variables" msgstr "UEFI 부트 서비스 변수" #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "UEFI ìº¡ìŠ ì—…ë°ì´íŠ¸ë¥¼ 사용할 수 없거나 펌웨어 설정ì—서 비활성화ë¨" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "UEFI dbx 유틸리티" #. TRANSLATORS: system is not booted in UEFI mode msgid "UEFI firmware can not be updated in legacy BIOS mode" msgstr "레거시 BIOS 모드ì—서 UEFI 펌웨어를 ì—…ë°ì´íŠ¸í•  수 ì—†ìŒ" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "UEFI í”Œëž«í¼ í‚¤" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "UEFI 보안 부트" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "ì„œë¹„ìŠ¤ì— ì—°ê²°í•  수 ì—†ìŒ" #. TRANSLATORS: error message msgid "Unable to find attribute" msgstr "ì†ì„± í•­ëª©ì„ ì°¾ì„ ìˆ˜ 없습니다" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "현재 드ë¼ì´ë²„ ë°”ì¸ë“œ í•´ì œ" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "차단 해제할 펌웨어:" #. TRANSLATORS: command description msgid "Unblocks a specific firmware from being installed" msgstr "지정한 펌웨어 설치 ë°©ì§€ í•´ì œ" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "암호화ë˜ì§€ 않ìŒ" #. TRANSLATORS: command description msgid "Uninhibit the system to allow upgrades" msgstr "업그레ì´ë“œë¥¼ 허용하ë„ë¡ ì‹œìŠ¤í…œ 억제를 해제합니다" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "알 수 ì—†ìŒ" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "알 수 없는 장치" msgid "Unlock the device to allow access" msgstr "ì ‘ê·¼ì„ í—ˆìš©í•˜ë ¤ë©´ 장치 ìž ê¸ˆì„ í•´ì œí•˜ì‹­ì‹œì˜¤" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "잠금 í•´ì œë¨" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "íŽŒì›¨ì–´ì— ì ‘ê·¼í•  수 있ë„ë¡ ìž¥ì¹˜ ìž ê¸ˆì„ í•´ì œ" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "ESP 마운트 í•´ì œ" #. TRANSLATORS: firmware payload is unsigned and it is possible to modify it msgid "Unsigned Payload" msgstr "서명하지 ì•Šì€ íŽ˜ì´ë¡œë“œ" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "ì§€ì›í•˜ì§€ 않는 ë°ëª¬ 버전 %s, í´ë¼ì´ì–¸íЏ 버전 %s" #. TRANSLATORS: Suffix: the HSI result msgid "Untainted" msgstr "오염ë˜ì§€ 않ìŒ" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "ì—…ë°ì´íЏ 가능" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "ì—…ë°ì´íЏ 오류" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "ì—…ë°ì´íЏ ì´ë¯¸ì§€" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "ì—…ë°ì´íЏ 메시지" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "ì—…ë°ì´íЏ ìƒíƒœ" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "알 수 없는 ì´ìœ ë¡œ ì—…ë°ì´íŠ¸ê°€ 실패했습니다. ë” ë§Žì€ ì •ë³´ë¥¼ 보려면 ë‹¤ìŒ URLì„ ì°¸ì¡°í•˜ì‹­ì‹œì˜¤:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "지금 ì—…ë°ì´íŠ¸í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" #. TRANSLATORS: command description msgid "Update the stored cryptographic hash with current ROM contents" msgstr "현재 ROM 내용으로 ì €ìž¥ëœ ì•”í˜¸í™” 해시를 ì—…ë°ì´íЏ" msgid "Update the stored device verification information" msgstr "ì €ìž¥ëœ ìž¥ì¹˜ ê²€ì¦ ì •ë³´ ì—…ë°ì´íЏ" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "ì €ìž¥ëœ ë©”íƒ€ë°ì´í„°ë¥¼ 현재 내용으로 ì—…ë°ì´íЏ" #. TRANSLATORS: command description msgid "Updates all specified devices to latest firmware version, or all devices if unspecified" msgstr "모든 지정 장치를 최신 펌웨어 버전으로 ì—…ë°ì´íŠ¸í•©ë‹ˆë‹¤. 지정하지 않으면 모든 장치를 ì—…ë°ì´íŠ¸í•©ë‹ˆë‹¤." msgid "Updating" msgstr "ì—…ë°ì´íЏ" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "%s ì—…ë°ì´íЏ 중..." #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "%sì„(를) %sì—서 %s(으)로 업그레ì´ë“œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" #. TRANSLATORS: ask the user to share, %s is something #. * like: "Linux Vendor Firmware Service" #, c-format msgid "Upload these anonymous results to the %s to help other users?" msgstr "다른 사용ìžì—게 ë„ì›€ì„ ì¤„ ìµëª… 결과를 %sì— ì—…ë¡œë“œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "펌웨어 보고서를 업로드하면 하드웨어 제작사ì—서 실제 ìž¥ì¹˜ì— ì—…ë°ì´íŠ¸ë¥¼ ì ìš©í–ˆì„ 때 성공 ë° ì‹¤íŒ¨ 여부를 빠르게 알 수 있습니다." #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "중요ë„" #. TRANSLATORS: explain how to get help, %1 is #. * 'fwupdtool --help' #. TRANSLATORS: explain how to get help, #. * where $1 is something like 'fwupdmgr --help' #, c-format msgid "Use %s for help" msgstr "ë„움ë§ì„ 보려면 %s ëª…ë ¹ì„ ì‚¬ìš©í•˜ì‹­ì‹œì˜¤" #. TRANSLATORS: CTRL^C [holding control, and then pressing C] will exit the #. program msgid "Use CTRL^C to cancel." msgstr "취소하려면 CTRL^C를 누르십시오." #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "사용ìžì—게 ì•Œë¦¼ì´ í‘œì‹œë¨" #. TRANSLATORS: remote filename base msgid "Username" msgstr "ì‚¬ìš©ìž ì´ë¦„" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "올바름" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "ESP ë‚´ìš© 검사 중..." #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "파ìƒí˜•" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "제조사" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "ê²€ì¦ ì¤‘â€¦" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "버전" #. TRANSLATORS: the fwupd version the release was tested on msgid "Version[fwupd]" msgstr "버전[fwupd]" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "경고" #. TRANSLATORS: command description msgid "Wait for a device to appear" msgstr "장치 ì¶œí˜„ì„ ê¸°ë‹¤ë¦½ë‹ˆë‹¤" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "기다리는 중…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "하드웨어 변경 사항 ê°ì‹œ" #. TRANSLATORS: check various UEFI and ACPI tables are unchanged after the #. update msgid "Will measure elements of system integrity around an update" msgstr "ì—…ë°ì´íЏ 전후 시스템 무결성 요소를 확ì¸í•©ë‹ˆë‹¤" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "파ì¼ì— ê¸°ë¡ ì¤‘:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "쓰는 중…" #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from the system firmware setup, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "리눅스로 부팅할 수 없거나 ì‹œìŠ¤í…œì˜ ë¶ˆì•ˆì •ì„±ì„ ì•¼ê¸°í•  수 있으므로 시스템 펌웨어 ì„¤ì •ì„ ì‰½ê²Œ ë˜ëŒë¦´ 수 있는지 확ì¸í•´ì•¼ 합니다." #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "ë°°í¬íŒ 개발사ì—서 펌웨어 ì—…ë°ì´íŠ¸ì™€ 시스템 ë° ì—°ê²°ëœ ìž¥ì¹˜ê°„ì˜ í˜¸í™˜ì„±ì„ ê²€ì¦í•œë‹¤ëŠ” ë³´ìž¥ì´ ì—†ìŠµë‹ˆë‹¤." #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "ì´ íŽŒì›¨ì–´ë¥¼ ì‚¬ìš©í–ˆì„ ë•Œ 하드웨어가 ì†ìƒë  수 있으며, ì´ ë¦´ë¦¬ìŠ¤ë¥¼ 설치하면 %sì˜ ì œí’ˆ ë³´ì¦ì„ 무효화할 ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤." #. TRANSLATORS: BKC is the industry name for the best known configuration and #. is a set #. * of firmware that works together #, c-format msgid "Your system is set up to the BKC of %s." msgstr "시스템ì—서 %sì˜ BKC를 설정했습니다." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[CHECKSUM]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[DEVICE-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[DEVICE-ID|GUID] [BRANCH]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [VERSION]" msgstr "[<장치-ID>|] [<버전>]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILE FILE_SIG REMOTE-ID]" msgstr "[FILE FILE_SIG REMOTE-ID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[<파ì¼ì´ë¦„1>] [<파ì¼ì´ë¦„2>]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2] [--no-authenticate]" msgstr "[<설정1>] [<설정2>] [--no-authenticate]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[SMBIOS-FILE|HWIDS-FILE]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "기본값" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "fwupd TPM ì´ë²¤íЏ 로그 유틸리티" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "fwupd 플러그ì¸" fwupd-2.0.10/po/ky.po000066400000000000000000000032341501337203100143150ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Ilyas Bakirov , 2018 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Kyrgyz (http://www.transifex.com/freedesktop/fwupd/language/ky/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: ky\n" "Plural-Forms: nplurals=1; plural=0;\n" #. TRANSLATORS: this is when a device is hotplugged msgid "Added" msgstr "Кошулду" #. TRANSLATORS: chip ID, e.g. "0x58200204" msgid "Chip ID" msgstr "Чиптин ID'Ñи" #. TRANSLATORS: detected a DFU device msgid "Found" msgstr "Табылды" #. TRANSLATORS: Appstream ID for the hardware type msgid "ID" msgstr "ID" #. show message in progressbar #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing %s" msgstr "Орнотулууда: %s" msgid "Mode" msgstr "Режими" #. TRANSLATORS: DFU protocol version, e.g. 1.1 msgid "Protocol" msgstr "Протокол" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Окуулууда..." #. TRANSLATORS: device state, i.e. appIDLE msgid "State" msgstr "Ðбалы" #. TRANSLATORS: probably not run as root... #. TRANSLATORS: device has failed to report status #. TRANSLATORS: device status, e.g. "OK" msgid "Status" msgstr "СтатуÑ" #. TRANSLATORS: currect daemon status is unknown msgid "Unknown" msgstr "БелгиÑиз" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Жазылууда..." msgid "fwupd" msgstr "fwupd" fwupd-2.0.10/po/lt.po000066400000000000000000001646671501337203100143330ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Moo, 2019,2023 # Moo, 2020-2021 # Moo, 2023-2025 # Rimas Kudelis , 2023 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Lithuanian (http://app.transifex.com/freedesktop/fwupd/language/lt/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: lt\n" "Plural-Forms: nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "Liko %.0f minutÄ—" msgstr[1] "Liko %.0f minutÄ—s" msgstr[2] "Liko %.0f minuÄių" msgstr[3] "Liko %.0f minutÄ—s" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "„%s“ akumuliatoriaus naujinimas" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "„%s“ procesoriaus mikroprogramos naujinimas" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "„%s“ kameros naujinimas" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "„%s“ konfigÅ«racijos naujinimas" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "„%s“ valdiklio naujinimas" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "„%s“ įrenginio naujinimas" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "„%s“ įtaisytojo valdiklio naujinimas" #. TRANSLATORS: two miniature speakers attached to your ears #, c-format msgid "%s Headphones Update" msgstr "„%s“ ausinių naujinimas" #. TRANSLATORS: headphones with an integrated microphone #, c-format msgid "%s Headset Update" msgstr "„%s“ ausinių su mikrofonu naujinimas" #. TRANSLATORS: an input device used by gamers, e.g. a joystick #, c-format msgid "%s Input Controller Update" msgstr "„%s“ įvesties valdiklio naujinimas" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "„%s“ klaviatÅ«ros naujinimas" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "„%s“ ME naujinimas" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "„%s“ pelÄ—s naujinimas" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "„%s“ tinklo sÄ…sajos naujinimas" #. TRANSLATORS: SSD refers to a Solid State Drive, e.g. non-rotating #. * SATA or NVMe disk #, c-format msgid "%s SSD Update" msgstr "%s SSD naujinimas" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "„%s“ sistemos naujinimas" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "„%s“ „Thunderbolt“ valdiklio naujinimas" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "„%s“ jutiklinio kilimÄ—lio naujinimas" #. TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth #. * device that stays in the USB port so the wireless peripheral works #, c-format msgid "%s USB Receiver Update" msgstr "„%s“ USB imtuvo naujinimas" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "„%s“ naujinimas" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "Encrypted RAM". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s appeared: %s" msgstr "%s atsirado: %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "UEFI platform #. key". #. * %2 and %3 refer to results value, e.g. "Valid" and "Invalid" #, c-format msgid "%s changed: %s → %s" msgstr "%s pasikeitÄ—: %s → %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "SPI BIOS #. region". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s disappeared: %s" msgstr "%s iÅ¡nyko: %s" #. TRANSLATORS: %1 is a device name, e.g. "ThinkPad Universal #. * ThunderBolt 4 Dock" and %2 is "fwupdmgr activate" #, c-format msgid "%s is pending activation; use %s to complete the update." msgstr "„%s“ laukia aktyvacijos; naudokite „%s“ norÄ—dami užbaigti naujinimÄ…." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "%s nustelbimas" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "%s versija" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u diena" msgstr[1] "%u dienos" msgstr[2] "%u dienų" msgstr[3] "%u diena" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "%u įrenginiui yra prieinamas programinÄ—s aparatinÄ—s įrangos naujinimas." msgstr[1] "%u įrenginiams yra prieinamas programinÄ—s aparatinÄ—s įrangos naujinimas." msgstr[2] "%u įrenginių yra prieinamas programinÄ—s aparatinÄ—s įrangos naujinimas." msgstr[3] "%u įrenginiui yra prieinamas programinÄ—s aparatinÄ—s įrangos naujinimas." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u valanda" msgstr[1] "%u valandos" msgstr[2] "%u valandų" msgstr[3] "%u valanda" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minutÄ—" msgstr[1] "%u minutÄ—s" msgstr[2] "%u minuÄių" msgstr[3] "%u minutÄ—" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u sekundÄ—" msgstr[1] "%u sekundÄ—s" msgstr[2] "%u sekundžių" msgstr[3] "%u sekundÄ—" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "Reikalingas veiksmas:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "Aktyvuoti įrenginius" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "Aktyvuoti laukianÄius įrenginius" msgid "Activate the new firmware on the device" msgstr "Aktyvinti naujÄ…jÄ… aparatinÄ™ programinÄ™ įrangÄ… įrenginyje" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "Aktyvinamas aparatinÄ—s programinÄ—s įrangos naujinimas" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "Aktyvuojama aparatinÄ— programinÄ— įranga, skirta" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Amžius" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "Sutikti ir įjungti Å¡iÄ… nuotolinÄ™ saugyklÄ…?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Alternatyvus komandos „%s“ vardas" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "Visi to paties tipo įrenginiai bus atnaujinti vienu metu" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Leisti sendinti aparatinÄ—s programinÄ—s įrangos versijas" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "Leisti iÅ¡ naujo įdiegti esamas aparatinÄ—s programinÄ—s įrangos versijas" #. TRANSLATORS: another application is updating the device already msgid "An update is in progress" msgstr "Vyksta atnaujinimas" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "Naujinimo užbaigimui, reikia paleisti sistemÄ… iÅ¡ naujo." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "Naujinimo užbaigimui, reikia iÅ¡jungti sistemÄ…." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Atsakyti taip į visus klausimus" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Taikyti naujinimÄ…, netgi kai nepatartina" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Taikyti naujinimo failus" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "Taikomas naujinimas…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Patvirtinta programinÄ— aparatinÄ— įranga:" msgstr[1] "Patvirtinta programinÄ— aparatinÄ— įranga:" msgstr[2] "Patvirtinta programinÄ— aparatinÄ— įranga:" msgstr[3] "Patvirtinta programinÄ— aparatinÄ— įranga:" #. TRANSLATORS: command description msgid "Asks the daemon to quit" msgstr "PapraÅ¡o tarnybos baigti darbÄ…" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "PridÄ—ti į aparatinÄ—s programinÄ—s įrangos veiksenÄ…" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Nustatoma tapatybė…" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Norint keiÄiamajame įrenginyje sendinti aparatinÄ™ programinÄ™ įrangÄ…, reikalingas tapatybÄ—s nustatymas" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Norint Å¡iame įrenginyje sendinti aparatinÄ™ programinÄ™ įrangÄ…, reikalingas tapatybÄ—s nustatymas" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "Norint modifikuoti BIOS nuostatas, reikalingas tapatybÄ—s nustatymas" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "Norint modifikuoti aparatinÄ—s programinÄ—s įrangos naujinimams naudojamÄ… sukonfigÅ«ruotÄ… saugyklÄ…, reikalingas tapatybÄ—s nustatymas" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "Norint modifikuoti tarnybos konfigÅ«racijÄ…, reikalingas tapatybÄ—s nustatymas" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "Norint skaityti BIOS nuostatas, reikalingas tapatybÄ—s nustatymas" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to reset daemon configuration to defaults" msgstr "Norint atstatyti tarnybos konfigÅ«racijÄ… į numatytÄ…sias reikÅ¡mes, reikalingas tapatybÄ—s nustatymas" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "Norint nustatyti patvirtintos aparatinÄ—s programinÄ—s įrangos sÄ…rašą, reikalingas tapatybÄ—s nustatymas" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "Norint pasiraÅ¡yti duomenis naudojant kliento liudijimÄ…, reikalingas tapatybÄ—s nustatymas" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to stop the firmware update service" msgstr "Norint stabdyti programinÄ—s aparatinÄ—s įrangos atnaujinimo tarnybÄ…, reikalingas tapatybÄ—s nustatymas" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "Norint perjungti į naujÄ… aparatinÄ—s programinÄ—s įrangos versijÄ…, reikalingas tapatybÄ—s nustatymas" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Norint atrakinti įrenginį, reikalingas tapatybÄ—s nustatymas" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Norint keiÄiamajame įrenginyje atnaujinti aparatinÄ™ programinÄ™ įrangÄ…, reikalingas tapatybÄ—s nustatymas" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Norint Å¡iame įrenginyje atnaujinti aparatinÄ™ programinÄ™ įrangÄ…, reikalingas tapatybÄ—s nustatymas" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "Norint atnaujinti saugomas įrenginio kontrolines sumas, reikalingas tapatybÄ—s nustatymas" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS Firmware Updates" msgstr "BIOS programinÄ—s aparatinÄ—s įrangos atnaujinimai" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS firmware updates" msgstr "BIOS programinÄ—s aparatinÄ—s įrangos atnaujinimai" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "BIOS sistemos naujinimai iÅ¡ LVFS arba „Windows Update“" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "Akumuliatorius" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "Susieti naujÄ… branduolio tvarkyklÄ™" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "Užblokuota versija" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "Pradinio įkÄ—liklio versija" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Atsisakyti" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "Atsisakyta" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "Negalima taikyti, nes dbx naujinimas jau yra pritaikytas." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Pakeistas" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "KontrolinÄ— suma" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "IÅ¡valo rezultatus iÅ¡ paskutinio naujinimo" #. TRANSLATORS: error message msgid "Command not found" msgstr "Komanda nerasta" #. TRANSLATORS: no peeking msgid "Configuration is only readable by the system administrator" msgstr "KonfigÅ«racijÄ… gali skaityti tik sistemos administratorius" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "Konvertuoti aparatinÄ—s programinÄ—s įrangos failÄ…" #. TRANSLATORS: when the update was built msgid "Created" msgstr "Sukurtas" #. TRANSLATORS: current value of a BIOS setting msgid "Current Value" msgstr "DabartinÄ— reikÅ¡mÄ—" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "DabartinÄ— versija" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "Ä®RENGINIO-ID|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Derinimo parametrai" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "IÅ¡skleidžiama…" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "ApraÅ¡as" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "Atskirti į pradinio įkÄ—liklio veiksenÄ…" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "IÅ¡samiau" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "Ä®renginio požymiai" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "Ä®renginio ID" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "PridÄ—tas įrenginys:" #. TRANSLATORS: the device is already connected msgid "Device already exists" msgstr "Ä®renginys jau yra" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "Ä®renginio akumuliatoriaus įkrova yra per maža" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse #, c-format msgid "Device battery power is too low (%u%%, requires %u%%)" msgstr "Ä®renginio akumuliatoriaus įkrova yra per maža (%u%%, reikia %u%%)" #. TRANSLATORS: lid means "laptop top cover" msgid "Device cannot be updated while the lid is closed" msgstr "Ä®renginys negali bÅ«ti naujinamas, kol dangtis yra uždarytas" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Pakeistas įrenginys:" #. TRANSLATORS: device cannot be interrupted, for instance taking a phone call msgid "Device is in use" msgstr "Ä®renginys yra naudojamas" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "Ä®renginys yra užrakintas" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "Ä®renginys yra nepasiekiamas" #. TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode msgid "Device is unreachable, or out of wireless range" msgstr "Ä®renginys nepasiekiamas arba už belaidžio ryÅ¡io ribų" #. TRANSLATORS: usually this is when we're waiting for a reboot msgid "Device is waiting for the update to be applied" msgstr "Ä®renginys laukia, kol bus pritaikytas naujinimas" #. TRANSLATORS: success, so say thank you to the user msgid "Device list uploaded successfully, thanks!" msgstr "Ä®renginių sÄ…raÅ¡as sÄ—kmingai iÅ¡siųstas, dÄ—kojame!" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "PaÅ¡alintas įrenginys:" #. TRANSLATORS: The device cannot be updated due to missing vendor's license." msgid "Device requires a software license to update" msgstr "Ä®renginio atnaujinimui reikalinga programinÄ—s įrangos licencija" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "Ä®renginio atnaujinimas reikalauja aktyvacijos" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "Ä®renginiai, kurie buvo sÄ—kmingai atnaujinti:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "Ä®renginiai, kurių nepavyko atnaujinti:" #. TRANSLATORS: message letting the user there is an update #. * waiting, but there is a reason it cannot be deployed msgid "Devices with firmware updates that need user action: " msgstr "Ä®renginiai, kuriems prieinami programinÄ—s aparatinÄ—s įrangos naujinimai, reikalaujantys naudotojo veiksmų: " #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "Ä®renginiai, neturintys prieinamų aparatinÄ—s programinÄ—s įrangos naujinimų: " #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "Ä®renginiai, kuriuose jau įdiegta naujausia prieinama programinÄ—s aparatinÄ—s įrangos versija:" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "IÅ¡jungtas" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "IÅ¡jungia nurodytÄ… nuotolinÄ™ saugyklÄ…" #. TRANSLATORS: the OS the release was tested on msgid "Distribution" msgstr "Platinamasis paketas" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "Netikrinti ar yra senų metaduomenų" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "Netikrinti ar yra istorijos apie kuriÄ… nepraneÅ¡ta" #. TRANSLATORS: warning message shown after update has been scheduled msgid "Do not turn off your computer or remove the AC adaptor while the update is in progress." msgstr "NeiÅ¡junkite kompiuterio ir neiÅ¡traukite elektros maitinimo laido tol, kol vyksta naujinimas." #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "NeraÅ¡yti į istorijos duomenų bazÄ™" #. TRANSLATORS: success msgid "Done!" msgstr "Atlikta!" #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "Sendinti %s iÅ¡ %s į %s?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "Sendina aparatinÄ™ programinÄ™ įrangÄ… įrenginyje" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "Sendinamas %s…" #. TRANSLATORS: command description msgid "Download a file" msgstr "Atsisiųsti failÄ…" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "AtsisiunÄiama…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "IÅ¡kloti SMBIOS duomenis iÅ¡ failo" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "TrukmÄ—" msgid "Enable" msgstr "Ä®jungti" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "Ä®jungti Å¡iÄ… nuotolinÄ™ saugyklÄ…?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Ä®jungta" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "Ä®jungia nurodytÄ… nuotolinÄ™ saugyklÄ…" msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "Ä®jungdami šį funkcionalumÄ…, su juo susijusiÄ… rizikÄ… prisiimate sau. Apie bet kokias Å¡ių naujinimų sukeltas problemas turÄ—tumÄ—te praneÅ¡ti tiesiogiai atitinkamam aparatinÄ—s įrangos gamintojui. Tik problemas, susijusias su paÄiu naujinimo procesu, derÄ—tų registruoti adresu $OS_RELEASE:BUG_REPORT_URL$." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "Å ios nuotolinÄ—s saugyklos įjungimas yra jÅ«sų paÄių rizika." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Å ifruotas" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "PaÅ¡alinti visÄ… aparatinÄ—s programinÄ—s įrangos naujinimų istorijÄ…" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "IÅ¡trinama…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "IÅ¡eiti po nedidelÄ—s delsos" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "IÅ¡eiti įkÄ—lus modulį" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "Eksportuoti aparatinÄ—s programinÄ—s įrangos failo struktÅ«rÄ… į XML" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "FAILAS" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "FAILAS [Ä®RENGINIO-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "FAILO-VARDAS" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "FAILO-VARDAS LIUDIJIMAS PRIVATUSIS-RAKTAS" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "FAILO-VARDAS Ä®RENGINIO-ID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "FAILO-VARDAS [Ä®RENGINIO-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "FAILO-VARDAS [APARATINÄ–S-PROGRAMINÄ–S-Ä®RANGOS-TIPAS]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "PatyrÄ— nesÄ—kmÄ™" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Nepavyko pritaikyti naujinimo" #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "Nepavyko prisijungti prie tarnybos" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "Nepavyko įkelti vietinio dbx" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "Nepavyko įkelti sisteminio dbx" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "Nepavyko užrakinti" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Nepavyko iÅ¡analizuoti argumentų" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "Nepavyko iÅ¡analizuoti failo" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "Nepavyko patikrinti ESP turinio" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Failo vardas" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "Failo vardo paraÅ¡as" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Reikalingas failo vardas" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "PrograminÄ—s aparatinÄ—s įrangos BIOS deskriptorius" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "PrograminÄ—s aparatinÄ—s įrangos BIOS sritis" #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "Pagrindinis aparatinÄ—s programinÄ—s įrangos URI" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "AparatinÄ—s programinÄ—s įrangos naujinimo „D-Bus“ tarnyba" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "AparatinÄ—s programinÄ—s įrangos naujinimo tarnyba" #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "AparatinÄ—s programinÄ—s įrangos naujinimai" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "AparatinÄ—s programinÄ—s įrangos paslaugų programa" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "PrograminÄ—s aparatinÄ—s įrangos raÅ¡ymo apsauga" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "PrograminÄ—s aparatinÄ—s įrangos raÅ¡ymo apsaugos užraktas" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "AparatinÄ— programinÄ— įranga jau užblokuota" #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "AparatinÄ—s programinÄ—s įrangos naujinimai" #. TRANSLATORS: user needs to run a command, %1 is 'fwupdmgr unlock' #, c-format msgid "Firmware updates disabled; run '%s' to enable" msgstr "PrograminÄ—s aparatinÄ—s įrangos atnaujinimai iÅ¡jungti; norÄ—dami įjungti paleiskite „%s“" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "Požymiai" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Rastas" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "Aptiktas viso disko Å¡ifravimas" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" msgid "Get BIOS settings" msgstr "Gauti BIOS nuostatas" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "Gauti visus „fwupd“ palaikomus įrenginio požymius (gaireles)" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Gauti visus įrenginius, kurie palaiko aparatinÄ—s programinÄ—s įrangos naujinimus" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "Gauti visus įjungtus sistemoje registruotus įskiepius" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Gauna iÅ¡samesnÄ™ informacijÄ… apie aparatinÄ—s programinÄ—s įrangos failÄ…" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "Gauna sukonfigÅ«ruotas nuotolines saugyklas" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Gauna naujinimų sÄ…rašą prijungtai aparatinei įrangai" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "Gauna laidas įrenginiui" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Gauna rezultatus iÅ¡ paskutinio naujinimo" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "IOMMU apsauga" #. TRANSLATORS: HSI event title msgid "IOMMU device protection disabled" msgstr "IOMMU įrenginio apsauga iÅ¡jungta" #. TRANSLATORS: HSI event title msgid "IOMMU device protection enabled" msgstr "IOMMU įrenginio apsauga įjungta" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "Neveiklus…" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "Nepaisyti aparatinÄ—s programinÄ—s įrangos kontrolinių sumų neatitikimų" #. TRANSLATORS: command line option msgid "Ignore non-critical firmware requirements" msgstr "Nepaisyti nekritinių programinÄ—s aparatinÄ—s įrangos reikalavimų" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "Ä®diegimo trukmÄ—" msgid "Install old version of signed system firmware" msgstr "Ä®diegti senÄ… pasiraÅ¡ytos sistemos aparatinÄ—s programinÄ—s įrangos versijÄ…" msgid "Install old version of unsigned system firmware" msgstr "Ä®diegti senÄ… nepasiraÅ¡ytos sistemos aparatinÄ—s programinÄ—s įrangos versijÄ…" msgid "Install signed device firmware" msgstr "Ä®diegti pasiraÅ¡ytÄ… įrenginio aparatinÄ™ programinÄ™ įrangÄ…" msgid "Install signed system firmware" msgstr "Ä®diegti pasiraÅ¡ytÄ… sistemos aparatinÄ™ programinÄ™ įrangÄ…" msgid "Install unsigned device firmware" msgstr "Ä®diegti nepasiraÅ¡ytÄ… įrenginio aparatinÄ™ programinÄ™ įrangÄ…" msgid "Install unsigned system firmware" msgstr "Ä®diegti nepasiraÅ¡ytÄ… sistemos aparatinÄ™ programinÄ™ įrangÄ…" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "Diegiamas aparatinÄ—s programinÄ—s įrangos naujinimas…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "Ä®diegiama ties %s…" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "Vidinis įrenginys" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "Yra pradinio įkÄ—liklio veiksenoje" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "VIETA" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "Liko mažiau kaip minutÄ—" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "Licencija" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "„Linux Vendor Firmware Service“ (stabili aparatinÄ— programinÄ— įranga)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "„Linux Vendor Firmware Service“ (testuojama aparatinÄ— programinÄ— įranga)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Linux branduolys" #. TRANSLATORS: command description msgid "List EFI boot files" msgstr "IÅ¡vardyti EFI paleidimo failus" #. TRANSLATORS: command description msgid "List EFI boot parameters" msgstr "IÅ¡vardyti EFI paleidimo parametrus" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "IÅ¡vardyti dbx esanÄius įraÅ¡us" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "IÅ¡vardyti prieinamus aparatinÄ—s programinÄ—s įrangos tipus" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "IÅ¡vardija ESP esanÄius failus" #. TRANSLATORS: the plugin was created from a .so object, and was not built-in msgid "Loaded from an external module" msgstr "Ä®keltas iÅ¡ iÅ¡orinio modulio" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "Ä®keliama…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "Užrakintas" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "MEI nustelbimas" msgid "MEI version" msgstr "MEI versija" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "Didžiausia reikÅ¡mÄ—" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "Metaduomenų paraÅ¡as" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "Metaduomenų URI" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "Metaduomenys gali bÅ«ti gauti iÅ¡ „Linux Vendor Firmware Service“ paslaugos." #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "Mažiausia reikÅ¡mÄ—" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "Modifikuoja tarnybos konfigÅ«racijos reikÅ¡mÄ™" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "Modifikuoja nurodytÄ… nuotolinÄ™ saugyklÄ…" msgid "Modify a configured remote" msgstr "Modifikuoti sukonfigÅ«ruotÄ… nuotolinÄ™ saugyklÄ…" msgid "Modify daemon configuration" msgstr "Modifikuoti tarnybos konfigÅ«racijÄ…" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "StebÄ—ti tarnybÄ…, ar yra įvykių" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "Prijungia ESP" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "Po įdiegimo reikalauja paleidimo iÅ¡ naujo" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "Reikia paleisti iÅ¡ naujo" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "Po įdiegimo reikalauja iÅ¡jungimo" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "Nauja versija" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "Nenurodytas joks veiksmas!" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "NÄ—ra sendinimų, skirtų %s" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "Nerasta jokių aparatinÄ—s programinÄ—s įrangos ID" #. TRANSLATORS: nothing found msgid "No firmware found" msgstr "Nerasta jokios programinÄ—s aparatinÄ—s įrangos" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Neaptikta jokios aparatinÄ—s įrangos su aparatinÄ—s programinÄ—s įrangos naujinimo galimybÄ—mis" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "NÄ—ra prieinamų laidų" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "Å iuo metu nÄ—ra įjungtos jokios nuotolinÄ—s saugyklos, taigi, nÄ—ra prieinami jokie metaduomenys." #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "NÄ—ra prieinamų naujinimų" msgid "No updates available for remaining devices" msgstr "Likusiems įrenginiams nÄ—ra prieinamų naujinimų" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "Nerastas" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "Nepalaikomas" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "Gerai" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "Gerai!" #. TRANSLATORS: the firmware old version msgid "Old version" msgstr "Sena versija" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "Rodyti tik vienÄ… PCR reikÅ¡mÄ™" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "Nustelbti numatytÄ…jį ESS (angl. ESP) keliÄ…" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Firmware" msgstr "P2P programinÄ— aparatinÄ— įranga" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Metadata" msgstr "P2P metaduomenys" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "KELIAS" #. TRANSLATORS: remote filename base msgid "Password" msgstr "Slaptažodis" msgid "Payload" msgstr "Naudingoji apkrova" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "Atlikti operacijÄ…?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "Platformos derinimas" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "Platformos derinimas" #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "Ä®veskite skaiÄių nuo 0 iki %u: " #. TRANSLATORS: the user isn't reading the question -- %1 is 'Y' and %2 is #. * 'N' #, c-format msgid "Please enter either %s or %s: " msgstr "Ä®veskite %s arba %s: " #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "TrÅ«ksta įskiepio priklausomybių" #. TRANSLATORS: The plugin is only for testing msgid "Plugin is only for testing" msgstr "Ä®skiepis yra tik testavimui" #. TRANSLATORS: Possible values for a bios setting msgid "Possible Values" msgstr "Galimos reikÅ¡mÄ—s" #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "AnkstesnÄ— versija" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "PirmenybÄ—" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "Problemos" msgid "Proceed with upload?" msgstr "TÄ™sti iÅ¡siuntimÄ…?" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Processor Security Checks" msgstr "Procesoriaus saugumo patikros" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "NuosavybinÄ—" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "Tik skaitymui" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "Skaitoma iÅ¡ %s…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Skaitoma…" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "IÅ¡ naujo įkelti metaduomenis iÅ¡ nuotolinio serverio" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "Ä®diegti iÅ¡ naujo %s į %s?" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "IÅ¡ naujo įdiegti įrenginyje aparatinÄ™ programinÄ™ įrangÄ…" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "NuotolinÄ—s saugyklos ID" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "Ataskaitų URI" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "Nerasta reikiama efivarfs failų sistema" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "Nerasta reikiamos aparatinÄ—s įrangos" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "Reikalauja pradinio įkÄ—liklio" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Reikalauja interneto ryÅ¡io" msgid "Reset daemon configuration" msgstr "Atstatyti tarnybos konfigÅ«racijÄ…" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Paleisti iÅ¡ naujo dabar?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "Paleisti tarnybÄ… iÅ¡ naujo, kad pakeitimas įsigaliotų?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Ä®renginys paleidžiamas iÅ¡ naujo…" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "Grąžinti visus kompiuterio aparatinÄ—s įrangos ID" #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr sync` #, c-format msgid "Run `%s` to complete this action." msgstr "Paleiskite „%s“ norÄ—dami užbaigti šį veiksmÄ…." #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "Paleisti sudÄ—tinÄ™ įskiepio iÅ¡valymo programÄ…, naudojant install-blob" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "Paleisti sudÄ—tinÄ™ įskiepio paruoÅ¡imo programÄ…, naudojant install-blob" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "SPI BIOS deskriptorius" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "SPI BIOS sritis" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Suplanuojama…" #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "Saugus paleidimas iÅ¡jungtas" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "Saugus paleidimas įjungtas" #. TRANSLATORS: the %1 is a URL to a wiki page #, c-format msgid "See %s for more details." msgstr "IÅ¡samesnei informacijai apsilankykite adresu%s." #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "IÅ¡samesnÄ—s informacijos ieÅ¡kokite adresu %s." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Pasirinktas įrenginys" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "Serijos numeris" msgid "Set one or more BIOS settings" msgstr "Nustatyti vienÄ… ar daugiau BIOS nuostatų" #. TRANSLATORS: command description msgid "Sets one or more BIOS settings" msgstr "Nustato vienÄ… ar daugiau BIOS nuostatų" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Nustato patvirtintÄ… aparatinÄ—s programinÄ—s įrangos sÄ…rašą" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "Nuostatos bus pritaikytos paleidus sistemÄ… iÅ¡ naujo" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "Bendrinti aparatinÄ—s programinÄ—s įrangos istorijÄ… su plÄ—totojais" #. TRANSLATORS: command line option msgid "Show all results" msgstr "Rodyti visus rezultatus" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "Rodyti kliento ir tarnybos versijas" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Rodyti derinimo parametrus" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Rodyti negalimus naujinti įrenginius" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Rodyti papildomÄ… derinimo informacijÄ…" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "Rodyti aparatinÄ—s programinÄ—s įrangos naujinimų istorijÄ…" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "Rodyti apskaiÄiuotÄ… dbx versijÄ…" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "IÅ¡jungti dabar?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "PasiraÅ¡yti aparatinÄ™ programinÄ™ įrangÄ… nauju raktu" msgid "Sign data using the client certificate" msgstr "PasiraÅ¡yti duomenis, naudojant kliento liudijimÄ…" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "PasiraÅ¡yti duomenis, naudojant kliento liudijimÄ…" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "PasiraÅ¡yti iÅ¡siunÄiamus duomenis naudojant kliento liudijimÄ…" msgid "Signature" msgstr "ParaÅ¡as" #. TRANSLATORS: file size of the download msgid "Size" msgstr "Dydis" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "Nurodyti dbx duomenų bazÄ—s failÄ…" msgid "Stop the fwupd service" msgstr "Stabdyti fwupd tarnybÄ…" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "Pavyko" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "SÄ—kmingai aktyvuoti visi įrenginiai" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "SÄ—kmingai atsisiųsti nauji metaduomenys: " #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "AparatinÄ— programinÄ— įranga sÄ—kmingai įdiegta" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "SÄ—kmingai modifikuota konfigÅ«racijos reikÅ¡mÄ—" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration values" msgstr "SÄ—kmingai atstatytos konfigÅ«racijos reikÅ¡mÄ—s" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "SÄ—kmingai atnaujintos įrenginių kontrolinÄ—s sumos" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "SÄ—kmingai patikrintos įrenginių kontrolinÄ—s sumos" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Santrauka" #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "Palaikomas" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "Palaikomas procesorius" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "Palaikomas nuotoliniame serveryje" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "Sistema reikalauja iÅ¡orinio maitinimo Å¡altinio" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "TEKSTAS" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "ŽymÄ—" msgstr[1] "ŽymÄ—s" msgstr[2] "ŽymÄ—s" msgstr[3] "ŽymÄ—s" #. show the user the entire data blob msgid "Target" msgstr "Paskirtis" #. TRANSLATORS: when the release was tested msgid "Tested" msgstr "IÅ¡bandytas" #. TRANSLATORS: the %s is a vendor name, e.g. Lenovo #, c-format msgid "Tested by %s" msgstr "IÅ¡bandÄ— %s" #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "LVFS – tai nemokama paslauga, teikiama nepriklausomo juridinio asmens ir nesusijusi su „$OS_RELEASE:NAME$“. JÅ«sų OS platintojas gali nebÅ«ti patikrinÄ™s Å¡ių aparatinÄ—s programinÄ—s įrangos naujinimų suderinamumo su jÅ«sų sistema ar prijungtais įrenginiais. Visa aparatinÄ— programinÄ— įranga LVFS pateikiama tik aparatinÄ—s įrangos gamintojų." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been re-inserted." msgstr "Naujinimas bus pratÄ™stas tada, kai įrenginio USB laidas bus įkiÅ¡tas iÅ¡ naujo." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged and then re-inserted." msgstr "Naujinimas bus pratÄ™stas tada, kai įrenginio USB laidas bus iÅ¡trauktas ir vÄ—l įkiÅ¡tas." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged." msgstr "Naujinimas bus pratÄ™stas tada, kai įrenginio USB laidas bus iÅ¡trauktas." #. TRANSLATORS: warning message msgid "The update will continue when the device power cable has been removed and re-inserted." msgstr "Naujinimas bus pratÄ™stas tada, kai įrenginio elektros maitinimo laidas bus iÅ¡trauktas ir vÄ—l įkiÅ¡tas." #. TRANSLATORS: naughty vendor msgid "The vendor did not supply any release notes." msgstr "TiekÄ—jas nepateikÄ— jokios laidos informacijos." #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "NÄ—ra jokios patvirtintos aparatinÄ—s programinÄ—s įrangos." #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "Å is paketas nebuvo patvirtintas, jis gali tinkamai neveikti." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "Å i programa gali tinkamai veikti tik pagrindinio naudotojo teisÄ—mis" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "Å ioje nuotolinÄ—je saugykloje laikoma aparatinÄ— programinÄ— įranga, kurios testavimo aparatinÄ—s įrangos gamintojai dar neužbaigÄ—. Jei naudosite jÄ…, įsitikinkite, jog nepavykus naujinimui ar prireikus dÄ—l kitų priežasÄių, turÄ—site galimybÄ™ sugrąžinti ankstesnÄ™ aparatinÄ—s programinÄ—s įrangos versijÄ…." #. TRANSLATORS: error message msgid "This system doesn't support firmware settings" msgstr "Å i sistema nepalaiko programinÄ—s aparatinÄ—s įrangos nuostatų" #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "Å is įrankis leidžia administratoriui taikyti UEFI dbx naujinimus." #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Tipas" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition may not be set up correctly" msgstr "Gali bÅ«ti, kad UEFI ESP skaidinys nÄ—ra teisingai nustatytas" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "UEFI ESP skaidinys neaptiktas arba nesukonfigÅ«ruotas" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "UEFI platformos raktas" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "UEFI saugus paleidimas" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "UEFI dbx paslaugų programa" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "UEFI platformos raktas" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "UEFI saugus paleidimas" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "Atsieti dabartinÄ™ tvarkyklÄ™" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "NeÅ¡ifruotas" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Nežinoma" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Nežinomas įrenginys" msgid "Unlock the device to allow access" msgstr "Atrakinti įrenginį, kad bÅ«tų leista prieiga" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "Atrakintas" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "Atrakina įrenginį aparatinÄ—s programinÄ—s įrangos prieigai" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "Atjungia ESP" #. TRANSLATORS: message shown after device has been marked for emulation msgid "Unplug and replug the device to continue the update process." msgstr "Atjunkite įrenginį ir prijunkite jį iÅ¡ naujo, kad pratÄ™stumÄ—te naujinimo procesÄ…." #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "Nepalaikoma tarnybos versija %s, kliento programos versija yra %s" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "Galimas naujinti" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Naujinimo klaida" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "Naujinimo praneÅ¡imas" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "Naujinimo bÅ«sena" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "Naujinimo nesÄ—kmÄ— yra žinoma problema, iÅ¡samesnei informacijai apsilankykite Å¡iame URL:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Naujinti dabar?" msgid "Update the stored device verification information" msgstr "Naujinti saugomÄ… įrenginio patikrinimo informacijÄ…" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "Naujinti saugomus metaduomenis esamu turiniu" msgid "Updating" msgstr "Naujinama" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "Naujinama %s…" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "Naujinti %s iÅ¡ %s į %s?" #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "AparatinÄ—s programinÄ—s įrangos ataskaitų iÅ¡siuntimas padeda aparatinÄ—s įrangos tiekÄ—jams greitai atpažinti nesÄ—kmingus bei sÄ—kmingus naujinimus tikruose įrenginiuose." #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "Skubumas" #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "Naudotojui buvo praneÅ¡ta" #. TRANSLATORS: remote filename base msgid "Username" msgstr "Naudotojo vardas" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "Tikrinamas ESP turinys…" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "Variantas" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "TiekÄ—jas" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "Patikrinama…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Versija" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "Ä®SPÄ–JIMAS" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "Laukiama…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "StebÄ—ti aparatinÄ—s įrangos pakeitimus" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "RaÅ¡omas failas:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "RaÅ¡oma…" #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "Gali bÅ«ti, kad jÅ«sų platintojas nÄ—ra patvirtinÄ™s jokių aparatinÄ—s programinÄ—s įrangos naujinimų suderinamumo su jÅ«sų sistema ar prijungtais įrenginiais." #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "Naudojant Å¡iÄ… programinÄ™ aparatinÄ™ įrangÄ… jÅ«sų aparatinÄ— įranga gali bÅ«ti sugadinta, o įdiegiant Å¡iÄ… laidÄ… gali bÅ«ti nutraukta %s teikiama garantija." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[KONTROLINÄ–-SUMA]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[Ä®RENGINIO-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE]" msgstr "[Ä®RENGINYS]" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "„fwupd“ TPM įvykių žurnalo paslaugų programa" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "fwupd įskiepiai" fwupd-2.0.10/po/meson.build000066400000000000000000000003561501337203100154760ustar00rootroot00000000000000i18n.gettext(meson.project_name(), preset: 'glib', args: [ '--default-domain=' + meson.project_name(), ] ) run_target('fix-translations', command: [ fix_translations, join_paths(meson.project_source_root(), 'po') ] ) fwupd-2.0.10/po/nl.po000066400000000000000000001324171501337203100143110ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Philip Goto , 2023 # Richard E. van der Luit , 2017,2023 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Dutch (http://app.transifex.com/freedesktop/fwupd/language/nl/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: nl\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "%.0f minuut resterend" msgstr[1] "%.0f minuten resterend" #. TRANSLATORS: BMC refers to baseboard management controller which #. * is the device that updates all the other firmware on the system #, c-format msgid "%s BMC Update" msgstr "BMC-update voor %s" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "Batterij-update voor %s" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "CPU-microcode-update voor %s" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "Camera-update voor %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "Configuratie-update voor %s" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "Gebruikers-ME-update voor %s" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "Controller-update voor %s" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "Bedrijfs-ME-update voor %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "Apparaat-update voor %s" #. TRANSLATORS: Video Display refers to the laptop internal display or #. * external monitor #, c-format msgid "%s Display Update" msgstr "Scherm-update voor %s" #. TRANSLATORS: Dock refers to the port replicator hardware laptops are #. * cradled in, or lowered onto #, c-format msgid "%s Dock Update" msgstr "Dock-update voor %s" #. TRANSLATORS: drive refers to a storage device, e.g. SATA disk #, c-format msgid "%s Drive Update" msgstr "Schijf-update voor %s" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "Geïntegreerde controller-update voor %s" #. TRANSLATORS: a device that can read your fingerprint pattern #, c-format msgid "%s Fingerprint Reader Update" msgstr "Fingerafdruklezer-update voor %s" #. TRANSLATORS: flash refers to solid state storage, e.g. UFS or eMMC #, c-format msgid "%s Flash Drive Update" msgstr "Flashopslag-update voor %s" #. TRANSLATORS: GPU refers to a Graphics Processing Unit, e.g. #. * the "video card" #, c-format msgid "%s GPU Update" msgstr "GPU-update voor %s" #. TRANSLATORS: a large pressure-sensitive drawing area typically used #. * by artists and digital artists #, c-format msgid "%s Graphics Tablet Update" msgstr "Tekentablet-update voor %s" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "Toetsenbord-update voor %s" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "ME-update voor %s" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "Muis-update voor %s" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "Netwerkinterface-update voor %s" #. TRANSLATORS: SSD refers to a Solid State Drive, e.g. non-rotating #. * SATA or NVMe disk #, c-format msgid "%s SSD Update" msgstr "SSD-update voor %s" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "Opslag-controller-update voor %s" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "Systeem-update voor %s" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "TPM-update voor %s" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "Thunderbolt-controller-update voor %s" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "Touchpad-update voor %s" #. TRANSLATORS: Dock refers to the port replicator device connected #. * by plugging in a USB cable -- which may or may not also provide power #, c-format msgid "%s USB Dock Update" msgstr "USB-dock-update voor %s" #. TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth #. * device that stays in the USB port so the wireless peripheral works #, c-format msgid "%s USB Receiver Update" msgstr "USB-ontvanger-update voor %s" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "Update voor %s" #. TRANSLATORS: the device has a reason it can't update, e.g. laptop lid #. closed #, c-format msgid "%s is not currently updatable" msgstr "%s kan momenteel niet worden bijgewerkt" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "%s-productiemodus" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "%s-overschrijving" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "%s-versie" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u dag" msgstr[1] "%u dagen" #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u uur" msgstr[1] "%u uur" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minuut" msgstr[1] "%u minuten" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u seconde" msgstr[1] "%u seconden" #. TRANSLATORS: first percentage is current value, 2nd percentage is the #. * lowest limit the firmware update is allowed for the update to happen #, c-format msgid "%u%% (threshold %u%%)" msgstr "%u%% (drempelwaarde: %u%%)" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(verouderd)" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "Actie vereist:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "Apparaten activeren" msgid "Activate the new firmware on the device" msgstr "De nieuwe firmware op het apparaat activeren" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "Firmware-update activeren" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Leeftijd" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Alias voor %s" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Downgraden van oude firmware-versies toestaan" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "Herinstalleren van bestaande firmware-versies toestaan" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "Wisselen van firmware-tak toestaan" #. TRANSLATORS: is not the main firmware stream msgid "Alternate branch" msgstr "Alternatieve tak" #. TRANSLATORS: another application is updating the device already msgid "An update is in progress" msgstr "Een update is bezig" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "Een update vereist een herstart om te voltooien." #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Updatebestanden toepassen" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "Update toepassen…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Goedgekeurde firmware:" msgstr[1] "Goedgekeurde firmware:" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "Aan firmware-modus vastmaken" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Authenticeren…" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Om de firmware op een verwijderbaar apparaat te downgraden moet u toestemming verlenen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Om de firmware op deze computer te downgraden moet u toestemming verlenen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "Authenticatie is vereist om BIOS-instellingen aan te passen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "Authenticatie is vereist om daemon-configuratie aan te passen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "Authenticatie is vereist om BIOS-instellingen te lezen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "Authenticatie is vereist om naar de nieuwe firmwareversie om te schakelen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Om een apparaat te ontgrendelen moet u toestemming verlenen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Om de firmware op een verwijderbaar apparaat bij te werken moet u toestemming verlenen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Om de firmware op deze computer bij te werken moet u toestemming verlenen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "Om de opgeslagen controlesommen op het apparaat bij te werken moet u toestemming verlenen" #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "Automatisch elke keer uploaden?" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS rollback protection" msgstr "BIOS-terugrolbescherming" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "BIOS-updates geleverd via LVFS of Windows Update" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "Batterij" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "Geblokkeerde versie" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "Opstartladerversie" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "Tak" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Annuleren" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "Geannuleerd" #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Gewijzigd" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Controlesom" #. TRANSLATORS: get interactive prompt, where branch is the #. * supplier of the firmware, e.g. "non-free" or "free" msgid "Choose branch" msgstr "Tak selecteren" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "Apparaat kiezen" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "Firmware kiezen" #. TRANSLATORS: get interactive prompt msgid "Choose release" msgstr "Uitgave kiezen" #. TRANSLATORS: get interactive prompt msgid "Choose volume" msgstr "Volume selecteren" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "Wist de resultaten van de laatste update" #. TRANSLATORS: error message msgid "Command not found" msgstr "De opdracht kon niet worden gevonden" #. TRANSLATORS: is not supported by the vendor msgid "Community supported" msgstr "Ondersteund door gemeenschap" #. TRANSLATORS: when the update was built msgid "Created" msgstr "Aanmaakmoment" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "Kritiek" #. TRANSLATORS: current value of a BIOS setting msgid "Current Value" msgstr "Huidige waarde" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "Huidige versie" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "APPARAAT-ID|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Foutopsporingsopties" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "Uitpakken..." #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Omschrijving" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "Details" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "Apparaat-flags" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "Apparaat-ID" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Apparaat toegevoegd:" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "Batterij van apparaat is te leeg" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Apparaat gewijzigd:" #. TRANSLATORS: emulated means we are pretending to be a different model msgid "Device is emulated" msgstr "Apparaat is geëmuleerd" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "Apparaat is vergrendeld" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "Apparaat is onbereikbaar" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Apparaat verwijderd:" #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "Apparaten zonder beschikbare firmware-updates:" #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "Apparaten met de nieuwste beschikbare firmware-versie:" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "Uitgeschakeld" #. TRANSLATORS: the OS the release was tested on msgid "Distribution" msgstr "Distributie" #. TRANSLATORS: success msgid "Done!" msgstr "Afgerond!" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "%s downgraden…" #. TRANSLATORS: command description msgid "Download a file" msgstr "Bestand downloaden" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Downloaden…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "SMBIOS-gegevens vanuit een bestand dumpen" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "Duur" #. TRANSLATORS: this device is not actually real msgid "Emulated" msgstr "Geëmuleerd" #. TRANSLATORS: Title: if we are emulating a different host msgid "Emulated host" msgstr "Geëmuleerde host" msgid "Enable" msgstr "Inschakelen" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "Nieuwe remote inschakelen?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "Deze remote inschakelen?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Ingeschakeld" #. TRANSLATORS: Plugin is active only if hardware is found msgid "Enabled if hardware matches" msgstr "Ingeschakeld als hardware overeenkomt" #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Versleuteld" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "Versleuteld geheugen" #. TRANSLATORS: The BIOS setting can only be changed to fixed values msgid "Enumeration" msgstr "Opsomming" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "Alle firmware-updategeschiedenis wissen" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Wissen…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "Afsluiten na een korte vertraging" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "Afsluiten nadat de engine geladen is" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "BESTAND" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "BESTAND [APPARAAT-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "BESTANDSNAAM" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "BESTANDSNAAM APPARAAT-ID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "BESTANDSNAAM [APPARAAT-ID|GUID]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "Mislukt" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Update toepassen mislukt" #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "Verbinden met daemon mislukt" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "Vergrendelen mislukt" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Het doorvoeren van argumenten is mislukt" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "Bestand parsen mislukt" #. TRANSLATORS: item is FALSE msgid "False" msgstr "Onwaar" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Bestandsnaam" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Bestandsnaam vereist" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware Attestation" msgstr "Firmware-attestatie" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "Firmware-BIOS-descriptor" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "Firmware-BIOS-regio" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "Firmware Update D-Bus-dienst" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Firmware Update Daemon" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "Firmware-updater-verificatie" #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "Firmware-updates" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "Firmware-hulpmiddel" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "Firmware-schrijfbescherming" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "Firmware-schrijfbeschermingsvergrendeling" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "Firmware-attestatie" #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "Firmware-updates" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "Flags" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Gevonden" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "Volledige schijfversleuteling gedetecteerd" #. TRANSLATORS: Title: if the part has been fused msgid "Fused Platform" msgstr "Zekeringsplatform" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" msgstr[1] "GUID's" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" msgid "Get BIOS settings" msgstr "BIOS-instellingen verkrijgen" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "Alle apparaat-flags ondersteund door fwupd verkrijgen" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Alle apparaten verkrijgen die firmware-updates ondersteunen" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "Alle ingeschakelde plug-ins geregistreerd op het systeem verkrijgen" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Verkrijgt details over een firmware-bestand" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Verkrijgt een lijst van updates voor verbonden hardware" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Verkrijgt de resultaten van de laatste update" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "HWIDS-BESTAND" #. TRANSLATORS: the release urgency msgid "High" msgstr "Hoog" #. TRANSLATORS: title for host security events msgid "Host Security Events" msgstr "Host-beveiligingsgebeurtenissen" #. TRANSLATORS: error message for unsupported feature #, c-format msgid "Host Security ID (HSI) is not supported" msgstr "Host-beveiligings-ID (HSI) wordt niet ondersteund" #. TRANSLATORS: success, so say thank you to the user msgid "Host Security ID attributes uploaded successfully, thanks!" msgstr "Kenmerken van host-beveiligings-ID succesvol geüpload, bedankt!" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "Host-beveiligings-ID:" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: HSI event title msgid "IOMMU device protection disabled" msgstr "IOMMU-apparaatbeveiliging uitgeschakeld" #. TRANSLATORS: HSI event title msgid "IOMMU device protection enabled" msgstr "IOMMU-apparaatbeveiliging ingeschakeld" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "Slaapt..." #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "Installatietijd" msgid "Install old version of signed system firmware" msgstr "Oude versie van ondertekende systeemfirmware installeren" msgid "Install old version of unsigned system firmware" msgstr "Oude versie van niet-ondertekende systeemfirmware installeren" msgid "Install signed device firmware" msgstr "Ondertekende apparaatfirmware installeren" msgid "Install signed system firmware" msgstr "Ondertekende systeemfirmware installeren" msgid "Install unsigned device firmware" msgstr "Niet-ondertekende apparaatfirmware installeren" msgid "Install unsigned system firmware" msgstr "Niet-ondertekende systeemfirmware installeren" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "Firmware-update installeren…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "Installeren op %s…" #. TRANSLATORS: The BIOS setting only accepts integers in a fixed range msgid "Integer" msgstr "Geheel getal" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "Intern apparaat" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "Ongeldig" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "Ongeldige argumenten" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "Is downgrade" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "Is upgrade" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "Probleem" msgstr[1] "Problemen" #. TRANSLATORS: HSI event title msgid "Kernel is no longer tainted" msgstr "Kernel is niet langer aangetast" #. TRANSLATORS: HSI event title msgid "Kernel is tainted" msgstr "Kernel is aangetast" #. TRANSLATORS: HSI event title msgid "Kernel lockdown disabled" msgstr "Kernel-lockdown uitgeschakeld" #. TRANSLATORS: HSI event title msgid "Kernel lockdown enabled" msgstr "Kernel-lockdown ingeschakeld" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "LOCATIE" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "Laatst bewerkt" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "Minder dan één minuut resterend" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "Licentie" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Linux Vendor Firmware Service (stabiele firmware)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Linux Vendor Firmware Service (test-firmware)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Linux-kernel" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Linux-kernellockdown" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Linux-wisselgeheugen" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "Laden..." #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "Vergrendeld" #. TRANSLATORS: the release urgency msgid "Low" msgstr "Laag" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refer #. * to the private/public key used to secure loading of firmware msgid "MEI key manifest" msgstr "MEI-sleutelmanifest" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "MEI-productiemodus" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "MEI-overschrijving" msgid "MEI version" msgstr "MEI-versie" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "Handmatig specifieke plug-ins inschakelen" #. TRANSLATORS: Longest valid string for BIOS setting msgid "Maximum length" msgstr "Maximumlengte" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "Maximumwaarde" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "Gemiddeld" #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "Minimumversie" #. TRANSLATORS: Shortest valid string for BIOS setting msgid "Minimum length" msgstr "Minimumlengte" #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "Minimumwaarde" msgid "Modify a configured remote" msgstr "Geconfigureerde remote aanpassen" msgid "Modify daemon configuration" msgstr "Daemon-configuratie aanpassen" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "De achtergrondservice controleren op gebeurtenissen" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "Vereist een herstart na installatie" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "Vereist herstart" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "Vereist afsluiten na installatie" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "Nieuwe versie" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "Geen actie opgegeven!" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "Geen firmware-ID's gevonden" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Geen hardware aangetroffen die in staat is firmware bij te werken" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "Geen uitgaven beschikbaar" #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "Geen remotes beschikbaar" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "Geen apparaten die kunnen worden bijgewerkt" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "Geen updates beschikbaar" msgid "No updates available for remaining devices" msgstr "Geen updates beschikbaar voor resterende apparaten" #. TRANSLATORS: version cannot be installed due to policy msgid "Not approved" msgstr "Niet goedgekeurd" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "Niet gevonden" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "Niet ondersteund" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "Oké" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "OK!" #. TRANSLATORS: the firmware old version msgid "Old version" msgstr "Oude versie" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "PAD" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "dbx-update parsen…" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "Systeem-dbx parsen…" #. TRANSLATORS: remote filename base msgid "Password" msgstr "Wachtwoord" msgid "Payload" msgstr "Inhoud" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "Afwachtend" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "Handeling uitvoeren?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "Platform-foutopsporing" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "Platform-foutopsporing" #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "Voer een getal in tussen 0 en %u:" #. TRANSLATORS: Possible values for a bios setting msgid "Possible Values" msgstr "Mogelijke waarden" #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "Vorige versie" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Prioriteit" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "Problemen" msgid "Proceed with upload?" msgstr "Doorgaan met uploaden?" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "Processor rollback protection" msgstr "Processor-terugrolbescherming" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "Propriëtair" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "REMOTE-ID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID KEY VALUE" msgstr "REMOTE-ID SLEUTEL WAARDE" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "Alleen-lezen" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "Lezen van %s…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Lezen…" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "Metadata verversen vanuit externe server" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "Firmware op een apparaat herinstalleren" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "Uitgavetak" #. TRANSLATORS: release attributes msgid "Release Flags" msgstr "Uitgave-flags" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "Gemeld aan externe server" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "Vereiste hardware is niet gevonden" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "Vereist een opstartlader" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Vereist internetverbinding" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Nu herstarten?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Herstarten apparaat..." #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "Runtime-achtervoegsel" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING VALUE" msgstr "INSTELLING WAARDE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING1 VALUE1 [SETTING2] [VALUE2]" msgstr "INSTELLING1 WAARDE1 [INSTELLING2] [WAARDE2]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "SUBSYSTEEM STUURPROGRAMMA [APPARAAT-ID|GUID]" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Inplannen..." #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "Veilig opstarten uitgeschakeld" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "Veilig opstarten ingeschakeld" #. TRANSLATORS: the %1 is a URL to a wiki page #, c-format msgid "See %s for more details." msgstr "Zie %s voor meer details." #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "Zie %s voor meer informatie." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Geselecteerd apparaat" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Geselecteerd volume" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "Serienummer" #. TRANSLATORS: Configured a BIOS setting to a value #, c-format msgid "Set BIOS setting '%s' using '%s'." msgstr "BIOS-instelling ‘%s’ ingesteld op ‘%s’" msgid "Set one or more BIOS settings" msgstr "Eén of meerdere BIOS-instellingen aanpassen" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Stelt de lijst met goedgekeurde firmware in" #. TRANSLATORS: type of BIOS setting msgid "Setting type" msgstr "Instellingstype" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "Instellingen worden toegepast na herstarten van het systeem" #. TRANSLATORS: command line option msgid "Show all results" msgstr "Alle resultaten tonen" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "Client- en daemon-versies tonen" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Foutopsporingsopties weergeven" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Apparaten tonen die niet kunnen worden bijgewerkt" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Extra foutopsporingsinformatie weergeven" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "Geschiedenis van firmware-updates tonen" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "Nu afsluiten?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "Firmware met een nieuwe sleutel ondertekenen" msgid "Signature" msgstr "Handtekening" #. TRANSLATORS: firmware is verified on-device the payload using strong crypto msgid "Signed Payload" msgstr "Ondertekende inhoud" #. TRANSLATORS: file size of the download msgid "Size" msgstr "Grootte" #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "Bron" #. TRANSLATORS: The BIOS setting accepts strings msgid "String" msgstr "Tekenreeks" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "Voltooid" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "Firmware succesvol geïnstalleerd" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "%u rapport succesvol geüpload" msgstr[1] "%u rapporten succesvol geüpload" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "Apparaat-controlesommen succesvol geverifieerd" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Samenvatting" #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "Ondersteund" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "Ondersteunde CPU" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "Ondersteund op externe server" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "Tak wisselen van %s naar %s?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "Firmware-tak van het apparaat wisselen" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "Systeem vereist externe voedingsbron" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "TEKST" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "Aangetast" #. show the user the entire data blob msgid "Target" msgstr "Doel" #. TRANSLATORS: when the release was tested msgid "Tested" msgstr "Getest" #. TRANSLATORS: the %s is a vendor name, e.g. Lenovo #, c-format msgid "Tested by %s" msgstr "Getest door %s" #. TRANSLATORS: now list devices with unfixed high-priority issues msgid "There are devices with issues:" msgstr "Er zijn apparaten met problemen:" #. TRANSLATORS: error message msgid "This system doesn't support firmware settings" msgstr "Dit systeem ondersteunt firmware-instellingen niet" #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "Dit systeem heeft HSI-runtimeproblemen." #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "Dit systeem heeft een laag HSI-beveiligingsniveau." #. TRANSLATORS: item is TRUE msgid "True" msgstr "Waar" #. TRANSLATORS: We verified the metadata against the server msgid "Trusted metadata" msgstr "Vertrouwde metagegevens" #. TRANSLATORS: We verified the payload against the server msgid "Trusted payload" msgstr "Vertrouwde inhoud" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Type" #. TRANSLATORS: error message msgid "Unable to find attribute" msgstr "Kan eigenschap niet vinden" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "Niet-versleuteld" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Onbekend" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Onbekend apparaat" msgid "Unlock the device to allow access" msgstr "Ontgrendel het apparaat om toegang te verlenen" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "Ontgrendeld" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "Ontgrendelt het apparaat voor firmware-toegang" #. TRANSLATORS: firmware payload is unsigned and it is possible to modify it msgid "Unsigned Payload" msgstr "Niet-ondertekende inhoud" #. TRANSLATORS: Suffix: the HSI result msgid "Untainted" msgstr "Onaangetast" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "Kan worden bijgewerkt" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Updatefout" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "Update-afbeelding" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "Update-bericht" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "Update-toestand" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Nu bijwerken?" msgid "Update the stored device verification information" msgstr "Opgeslagen apparaatverificatie-informatie bijwerken" msgid "Updating" msgstr "Bijwerken" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "%s bijwerken…" #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "Urgentie" #. TRANSLATORS: explain how to get help, %1 is #. * 'fwupdtool --help' #. TRANSLATORS: explain how to get help, #. * where $1 is something like 'fwupdmgr --help' #, c-format msgid "Use %s for help" msgstr "Gebruik %s voor hulp" #. TRANSLATORS: CTRL^C [holding control, and then pressing C] will exit the #. program msgid "Use CTRL^C to cancel." msgstr "Gebruik Ctrl-C om te annuleren" #. TRANSLATORS: remote filename base msgid "Username" msgstr "Gebruikersnaam" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "Geldig" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "ESP-inhoud valideren…" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "Variant" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "Leverancier" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "Valideren..." #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Versie" #. TRANSLATORS: the fwupd version the release was tested on msgid "Version[fwupd]" msgstr "Versie[fwupd]" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "WAARSCHUWING" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "Wachten…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "Hardware-wijzigingen bijhouden" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "Bestand schrijven:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Schrijven..." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[CONTROLESOM]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[APPARAAT-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[APPARAAT-ID|GUID] [TAK]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [VERSION]" msgstr "[APPARAAT-ID|GUID] [VERSIE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[BESTANDSNAAM1][BESTANDSNAAM2]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2] [--no-authenticate]" msgstr "[INSTELLING1] [INSTELLING2] [--no-authenticate]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[SMBIOS-BESTAND|HWIDS-BESTAND]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "standaard" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "fwupd-plug-ins" fwupd-2.0.10/po/oc.po000066400000000000000000000077441501337203100143050ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Cédric Valmary , 2016 # Cédric Valmary , 2016 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Occitan (post 1500) (http://www.transifex.com/freedesktop/fwupd/language/oc/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: oc\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. TRANSLATORS: this is when a device is hotplugged msgid "Added" msgstr "Apondut" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Aliàs de %s" #. TRANSLATORS: this is when a device ctrl+c's a watch msgid "Cancelled" msgstr "Anullat" #. TRANSLATORS: this is when a device is hotplugged #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Cambiat" #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Soma de contraròtle" #. TRANSLATORS: error message msgid "Command not found" msgstr "Comanda pas trobada" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Opcions de desbugatge" #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Descripcion" #. TRANSLATORS: success #. success msgid "Done!" msgstr "Acabat !" #. TRANSLATORS: the first replacement is a display name #. * e.g. "ColorHugALS" and the second and third are #. * version numbers e.g. "1.2.3" #, c-format msgid "Downgrading %s from %s to %s... " msgstr "Retrogradacion de %s de %s en %s " #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "Quitar aprèp un brèu relambi" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "Quitar aprèp lo cargament del motor" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Fracàs de l'analisi dels paramètres" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "Servici D-Bus de mesa a jorn dels micrologicials" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Trobat" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "A single GUID" msgid "GUID" msgstr "GUID" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Obténer la lista dels periferics que supòrtan las mesas a jorn de micrologicial" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Obténer los detalhs d'un fichièr de micrologicial" #. TRANSLATORS: command description msgid "Install a firmware file on this hardware" msgstr "Installar un fichièr de micrologicial sus aqueste material" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Cap de material amb de capacitats de mesa a jorn del micrologicial es pas estat detectat" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "D'acòrdi" #. TRANSLATORS: the first replacement is a display name #. * e.g. "ColorHugALS" and the second is a version number #. * e.g. "1.2.3" #, c-format msgid "Reinstalling %s with %s... " msgstr "Reïnstallacion de %s en %s " #. TRANSLATORS: this is when a device is hotplugged msgid "Removed" msgstr "Suprimit" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Mostrar las opcions de desbugatge" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Mòstra d'informacions de desbugatge complementàrias" #. TRANSLATORS: the first replacement is a display name #. * e.g. "ColorHugALS" and the second and third are #. * version numbers e.g. "1.2.3" #, c-format msgid "Updating %s from %s to %s... " msgstr "Mesa a jorn de %s de %s en %s " #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Version" fwupd-2.0.10/po/pa.po000066400000000000000000000512621501337203100142760ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # A S Alam, 2021 # A S Alam, 2025 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Panjabi (Punjabi) (http://app.transifex.com/freedesktop/fwupd/language/pa/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: pa\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "%.0f ਮਿੰਟ ਬਾਕੀ" msgstr[1] "%.0f ਮਿੰਟ ਬਾਕੀ" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "%s ਸੰਰਚਨਾ ਅੱਪਡੇਟ" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "%s ਡਿਵਾਈਸ ਅੱਪਡੇਟ" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "%s ਸਿਸਟਮ ਅੱਪਡੇਟ" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "%s ਅੱਪਡੇਟ" #. TRANSLATORS: the device has a reason it can't update, e.g. laptop lid #. closed #, c-format msgid "%s is not currently updatable" msgstr "%s ਇਸ ਵੇਲੇ ਅੱਪਡੇਟ ਕਰਨ ਯੋਗ ਨਹੀਂ ਹੈ" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "%s ਵਰਜ਼ਨ" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u ਮਿੰਟ" msgstr[1] "%uਮਿੰਟ" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u ਸਕਿੰਟ" msgstr[1] "%u ਸਕਿੰਟ" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "AMD Firmware Replay Protection" msgstr "AMD ਫਿਰਮਵੇਅਰ ਰੀਪਲੇਅ ਸà©à¨°à©±à¨–ਿਆ" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "AMD Firmware Write Protection" msgstr "AMD ਫਿਰਮਵੇਅਰ ਲਿਖਣ ਸà©à¨°à©±à¨–ਿਆ" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "AMD Secure Processor Rollback Protection" msgstr "AMD ਸਕਿਉਰ ਪà©à¨°à©‹à¨¸à©ˆà¨¸à¨° ਰੋਲਬੈਕ ਸà©à¨°à©±à¨–ਿਆ" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "ਧਿਆਨ ਦੇਣ ਦੀ ਲੋੜ ਹੈ:" msgid "Activate the new firmware on the device" msgstr "ਡਿਵਾਈਸ ਉੱਤੇ ਨਵਾਂ ਫਿਰਮਵੇਅਰ ਸਰਗਰਮ ਕਰੋ" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "ਅੱਪਡੇਟ ਫ਼ਾਇਲਾਂ ਨੂੰ ਲਾਗੂ ਕਰੋ" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "…ਅੱਪਡੇਟ ਨੂੰ ਲਾਗੂ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "…ਪਰਮਾਣਿਤ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "ਹਟਾਉਣਯੋਗ ਡਿਵਾਈਸ ਉੱਤੇ ਫਿਰਮਵੇਅਰ ਡਾਊਨਗਰੇਡ ਕਰਨ ਲਈ ਪਰਮਾਣਕਿਤਾ ਚਾਹੀਦੀ ਹੈ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "ਇਸ ਮਸ਼ੀਨ ਉੱਤੇ ਫਿਰਮਵੇਅਰ ਨੂੰ ਡਾਊਨਗਰੇਡ ਕਰਨ ਲਈ ਪਰਮਾਣਕਿਤਾ ਚਾਹੀਦੀ ਹੈ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "BIOS ਸੈਟਿੰਗਾਂ ਨੂੰ ਸੋਧਣ ਲਈ ਪਰਮਾਣਿਤ ਹੋਣ ਦੀ ਲੋੜ ਹੈ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "BIOS ਸੈਟਿੰਗਾਂ ਨੂੰ ਪੜà©à¨¹à¨¨ ਲਈ ਪਰਮਾਣਿਤ ਹੋਣ ਦੀ ਲੋੜ ਹੈ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to stop the firmware update service" msgstr "ਫਿਰਮਵੇਅਰ ਅੱਪਡੇਟ ਸੇਵਾ ਨੂੰ ਰੋਕਣ ਲਈ ਪਰਮਾਣਿਤ ਹੋਣ ਦੀ ਲੋੜ ਹੈ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "ਡਿਵਾਈਸ ਨੂੰ ਅਣ-ਲਾਕ ਕਰਨ ਲਈ ਪਰਮਾਣਕਿਤਾ ਚਾਹੀਦੀ ਹੈ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "ਹਟਾਉਣਯੋਗ ਡਿਵਾਈਸ ਉੱਤੇ ਫਿਰਮਵੇਅਰ ਅੱਪਡੇਟ ਕਰਨ ਲਈ ਪਰਮਾਣਕਿਤਾ ਚਾਹੀਦੀ ਹੈ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "ਇਸ ਮਸ਼ੀਨ ਉੱਤੇ ਫਿਰਮਵੇਅਰ ਅੱਪਡੇਟ ਕਰਨ ਲਈ ਪਰਮਾਣਕਿਤਾ ਚਾਹੀਦੀ ਹੈ" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS Firmware Updates" msgstr "BIOS ਫਿਰਮਵੇਅਰ ਅੱਪਡੇਟ" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS firmware updates" msgstr "BIOS ਫਿਰਮਵੇਅਰ ਅੱਪਡੇਟ" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "BIOS ਅੱਪਡੇਟਾਂ ਨੂੰ LVFS ਜਾਂ ਵਿੰਡੋਜ਼ ਅੱਪਡੇਟ ਰਾਹੀਂ ਲਾਗੂ ਕੀਤੇ ਗਠਹਨ" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "ਰੱਦ ਕਰੋ" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "ਰੱਦ ਕੀਤਾ" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "ਡਿਵਾਈਸ ਨੂੰ ਚà©à¨£à©‹" #. TRANSLATORS: error message msgid "Command not found" msgstr "ਕਮਾਂਡ ਨਹੀਂ ਲੱਭੀ" #. TRANSLATORS: when the update was built msgid "Created" msgstr "ਬਣਾਇਆ" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "ਗੰਭੀਰ" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "…ਡਿ-ਕੰਪਰੈਸ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "ਵਰਣਨ" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "ਵੇਰਵੇ" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "ਡਿਵਾਈਸ ID" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "ਡਿਵਾਈਸ ਜੋੜਿਆ:" #. TRANSLATORS: success msgid "Done!" msgstr "ਮà©à¨•ੰਮਲ!" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "…ਡਾਊਨਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "ਲੱਗਣਾ ਸਮਾਂ" msgid "Enable" msgstr "ਸਮਰੱਥ" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "ਸਮਰੱਥ ਹੈ" msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "ਇਹ ਸਹੂਲਤ ਨੂੰ ਆਪਣੇ ਖਤਰੇ ਉੱਤੇ ਹੀ ਸਮਰੱਥ ਕਰੋ, ਜਿਸ ਦਾ ਅਰਥ ਹੋਵੇਗਾ ਕਿ ਇਹਨਾਂ ਅੱਪਡੇਟਾਂ ਨਾਲ ਆਈ ਕਿਸੇ ਵੀ ਸਮੱਸਿਆ ਵਾਸਤੇ ਤà©à¨¸à©€à¨‚ ਅਸਲ ਡਿਵਾਈਸ ਨਿਰਮਾਤਾ ਨਾਲ ਸੰਪਰਕ ਕਰੋਗੇ। ਅੱਪਡੇਟ ਕਰਨ ਦੀ ਕਾਰਵਾਈ ਸੰਬੰਧੀ ਕਿਸੇ ਵੀ ਸਮੱਸਿਆ ਬਾਰੇ $OS_RELEASE:BUG_REPORT_URL$ ਉੱਤੇ ਰਿਪੋਰਟ ਕਰੋ।" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "…ਮਿਟਾਇਆ ਜਾ ਰਿਹਾ ਹੈ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "ਫਾਇਲ-ਨਾਂ" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "ਅੱਪਡੇਟ ਲਾਗੂ ਕਰਨ ਲਈ ਅਸਫ਼ਲ" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "ਲਾਕ ਕਰਨ ਲਈ ਅਸਫ਼ਲ ਹੈ" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "ਫਾਇਲ-ਦਾ-ਨਾਂ" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr " ਫਾਇਲ-ਨਾਂ ਚਾਹੀਦਾ ਹੈ" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "ਫਿਰਮਵੇਅਰ ਅੱਪਡੇਟਰ ਤਸਦੀਕ" #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "ਫਿਰਮਵੇਅਰ ਅੱਪਡੇਟ" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "ਫਿਰਮਵੇਅਰ ਲਿਖਣ ਸà©à¨°à©±à¨–ਿਆ" #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "ਫਿਰਮਵੇਅਰ ਅੱਪਡੇਟ" msgid "Get BIOS settings" msgstr "BIOS ਸੈਟਿੰਗਾਂ ਉੱਤੇ ਜਾਓ" #. TRANSLATORS: the release urgency msgid "High" msgstr "ਵੱਧ" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" msgid "Install old version of signed system firmware" msgstr "ਸਾਈਨ ਕੀਤੇ ਸਿਸਟਮ ਫਿਰਮਵੇਅਰ ਦਾ ਪà©à¨°à¨¾à¨£à¨¾ ਵਰਜ਼ਨ ਇੰਸਟਾਲ ਕਰੋ" msgid "Install old version of unsigned system firmware" msgstr "ਬਿਨ-ਸਾਈਨ ਕੀਤੇ ਸਿਸਟਮ ਫਿਰਮਵੇਅਰ ਦਾ ਪà©à¨°à¨¾à¨£à¨¾ ਵਰਜ਼ਨ ਇੰਸਟਾਲ ਕਰੋ" msgid "Install signed device firmware" msgstr "ਸਾਈਨ ਕੀਤਾ ਡਿਵਾਈਸ ਫਿਰਮਵੇਅਰ ਇੰਸਟਾਲ ਕਰੋ" msgid "Install signed system firmware" msgstr "ਸਾਈਨ ਕੀਤਾ ਸਿਸਟਮ ਫਿਰਮਵੇਅਰ ਇੰਸਟਾਲ ਕਰੋ" msgid "Install unsigned device firmware" msgstr "ਬਿਨ-ਸਾਈਨ ਕੀਤਾ ਡਿਵਾਈਸ ਫਿਰਮਵੇਅਰ ਇੰਸਟਾਲ ਕਰੋ" msgid "Install unsigned system firmware" msgstr "ਬਿਨਾਂ-ਸਾਈਨ ਕੀਤਾ ਸਿਸਟਮ ਫਿਰਮਵੇਅਰ ਇੰਸਟਾਲ ਕਰੋ" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "ਫਿਰਮਵੇਅਰ ਅੱਪਡੇਟ ਇੰਸਟਾਲ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "…%s ਉੱਤੇ ਇੰਸਟਾਲ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "ਮਸਲਾ" msgstr[1] "ਮਸਲੇ" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "ਇੱਕ ਮਿੰਟ ਤੋਂ ਘੱਟ ਬਾਕੀ ਰਹਿੰਦਾ ਹੈ" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "ਲਸੰਸ" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "ਲੀਨਕਸ ਕਰਨਲ ਲਾਕ-ਡਾਊਨ" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "ਲੀਨਕਸ ਕਰਨਲ ਤਸਦੀਕ" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "ਲੀਨਕਸ ਵੇਂਡਰ ਫਿਰਮਵੇਅਰ ਸਰਵਿਸ (ਸਟੇਬਲ ਫਿਰਮਵੇਅਰ)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "ਲੀਨਕਸ ਵੇਂਡਰ ਫਿਰਮਵੇਅਰ ਸਰਵਿਸ (ਟੈਸਟਿੰਗ ਫਿਰਮਵੇਅਰ)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "ਲੀਨਕਸ ਕਰਨਲ " #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "ਲੀਨਕਸ ਕਰਨਲ ਲਾਕ-ਡਾਊਨ" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "ਲੀਨਕਸ ਸਵੈਪ" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "…ਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ" #. TRANSLATORS: the release urgency msgid "Low" msgstr "ਘੱਟ" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "ਠੀਕ-ਠਾਕ" msgid "Modify daemon configuration" msgstr "ਡੈਮਨ ਸੰਰਚਨਾ ਸੋਧੋ" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "ਮà©à©œ-ਚਾਲੂ ਕਰਨ ਦੀ ਲੋੜ ਹੈ" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "ਨਵਾਂ ਵਰਜ਼ਨ" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "ਕੋਈ ਕਾਰਵਾਈ ਨਹੀਂ ਦਿੱਤੀ ਗਈ!" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "ਪਾਥ" #. TRANSLATORS: remote filename base msgid "Password" msgstr "ਪਾਸਵਰਡ" #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "0 ਤੋਂ ਲੈ ਕੇ %u ਤੱਕ ਕੋਈ ਨੰਬਰ ਦਿਓ ਜੀ:" #. TRANSLATORS: the user isn't reading the question -- %1 is 'Y' and %2 is #. * 'N' #, c-format msgid "Please enter either %s or %s: " msgstr "%s ਜਾਂ %s ਵਿੱਚੋਂ ਕੋਈ ਇੱਕ ਦਿਓ ਜੀ:" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "ਪà©à¨°à©‹à¨ªà©à¨°à©‡à¨Ÿà¨°à©€" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "…%s ਤੋਂ ਪੜà©à¨¹à¨¿à¨† ਜਾ ਰਿਹਾ ਹੈ" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "…ਪੜà©à¨¹à¨¿à¨† ਜਾ ਰਿਹਾ ਹੈ" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "ਹà©à¨£à©‡ ਮà©à©œ-ਚਾਲੂ ਕਰਨਾ ਹੈ?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "…ਡਿਵਾਈਸ ਨੂੰ ਮà©à©œ-ਚਾਲੂ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "…ਸੈਡਿਊਲ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ" #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "ਸà©à¨°à©±à¨–ਿਅਤ ਬੂਟ ਅਸਮਰੱਥ ਹੈ" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "ਸà©à¨°à©±à¨–ਿਅਤ ਬੂਟ ਸਮਰੱਥ ਹੈ" #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "ਹੋਰ ਜਾਣਕਾਰੀ ਲਈ %s ਨੂੰ ਵੇਖੋ।" #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "ਡਿਵਾਈਸ ਨੂੰ ਚà©à¨£à©‹" msgid "Set one or more BIOS settings" msgstr "ਇੱਕ ਜਾਂ ਵੱਧ BIOS ਸੈਟਿੰਗਾਂ ਨੂੰ ਸੈੱਟ ਕਰੋ" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "ਸੈਟਿੰਗਾਂ ਸਿਸਟਮ ਦੇ ਮà©à©œ-ਚਾਲੂ ਹੋਣ ਦੇ ਬਾਅਦ ਲਾਗੂ ਹੋਣਗੀਆਂ" #. TRANSLATORS: file size of the download msgid "Size" msgstr "ਆਕਾਰ" #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "ਸਰੋਤ" msgid "Stop the fwupd service" msgstr " fwupd ਸੇਵਾ ਨੂੰ ਰੋਕੋ" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "ਸਫ਼ਲ" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "ਸਾਰ" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "ਸਹਾਇਕ CPU" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "ਸਿਸਟਮ ਨੂੰ ਬਾਹਰੀ ਪਾਵਰ ਸਰੋਤ ਚਾਹੀਦਾ ਹੈ" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "ਟੈਗ" msgstr[1] "ਟੈਗ" #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "LVFS ਮà©à¨«à¨¼à¨¤ ਸਰਵਿਸ ਹੈ, ਜੋ ਕਿ ਆਜ਼ਾਦ ਕਨੂੰਨੀ ਸੰਸਥਾ ਵਜੋਂ ਕੰਮ ਕਰਦੀ ਹੈ ਅਤੇ $OS_RELEASE:NAME$ ਨਾਲ ਕੋਈ ਸੰਬੰਧ ਨਹੀਂ ਹੈ। ਤà©à¨¹à¨¾à¨¡à©‡ ਡਿਸਟਰੀਬਿਊਟਰ ਨੇ ਤà©à¨¹à¨¾à¨¡à©‡ ਸਿਸਟਮ ਜਾਂ ਕਨੈਕਟ ਹੋਠਡਿਵਾਈਸਾਂ ਲਈ ਅਨà©à¨•ੂਲਤਾ ਵਾਸਤੇ ਫਿਰਮਵੇਅਰ ਅੱਪਡੇਟਾਂ ਨੂੰ ਤਸਦੀਕ ਨਹੀਂ ਕੀਤੇ ਹਨ। ਸਾਰੇ ਫਿਰਮਵੇਅਰ ਅਸਲ ਡਿਵਾਈਸ ਨਿਮਰਾਤਾਵਾਂ ਵਲੋਂ ਹੀ ਦਿੱਤੇ ਜਾ ਗਠਹਨ।" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "ਕਿਸਮ" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI Memory Protection" msgstr "UEFI ਮੈਮੋਰੀ ਸà©à¨°à©±à¨–ਿਆ" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "UEFI ਸਕਿਉਰ ਬੂਟ" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI memory protection" msgstr "UEFI ਮੈਮੋਰੀ ਸà©à¨°à©±à¨–ਿਆ" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "UEFI ਪਲੇਟਫਾਰਮ ਕੀ" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "UEFI ਸਕਿਉਰ ਬੂਟ" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "ਅਣਪਛਾਤਾ" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "ਅਣਪਛਾਤਾ ਡਿਵਾਈਸ" msgid "Unlock the device to allow access" msgstr "ਪਹà©à©°à¨š ਦੀ ਇਜਾਜ਼ਤ ਦੇਣ ਲਈ ਡਿਵਾਈਸ ਅਣ-ਲਾਕ ਕਰੋ" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "…%s ਨੂੰ ਅੱਪਡੇਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ" #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "ਜ਼ਰੂਰਤ" #. TRANSLATORS: remote filename base msgid "Username" msgstr "ਵਰਤੋਂਕਾਰ-ਨਾਂ" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "ਵੇਂਡਰ" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "…ਜਾਂਚਿਆ ਜਾ ਰਿਹਾ ਹੈ" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "ਵਰਜ਼ਨ" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "ਸਾਵਧਾਨ" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "…ਉਡੀਕ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "…ਲਿਖਿਆ ਜਾ ਰਿਹਾ ਹੈ" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "ਮੂਲ" fwupd-2.0.10/po/pl.po000066400000000000000000003602521501337203100143130ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Piotr DrÄ…g , 2015-2025 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Polish (http://app.transifex.com/freedesktop/fwupd/language/pl/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: pl\n" "Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "PozostaÅ‚a %.0f minuta" msgstr[1] "PozostaÅ‚y %.0f minuty" msgstr[2] "PozostaÅ‚o %.0f minut" msgstr[3] "PozostaÅ‚o %.0f minut" #. TRANSLATORS: BMC refers to baseboard management controller which #. * is the device that updates all the other firmware on the system #, c-format msgid "%s BMC Update" msgstr "Aktualizacja BMC %s" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "Aktualizacja akumulatora %s" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "Aktualizacja mikrokodu procesora %s" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "Aktualizacja aparatu %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "Aktualizacja konfiguracji %s" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "Aktualizacja podsystemu ME użytkownika koÅ„cowego urzÄ…dzenia %s" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "Aktualizacja kontrolera %s" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "Aktualizacja firmowego podsystemu ME urzÄ…dzenia %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "Aktualizacja urzÄ…dzenia %s" #. TRANSLATORS: Video Display refers to the laptop internal display or #. * external monitor #, c-format msgid "%s Display Update" msgstr "Aktualizacja ekranu %s" #. TRANSLATORS: Dock refers to the port replicator hardware laptops are #. * cradled in, or lowered onto #, c-format msgid "%s Dock Update" msgstr "Aktualizacja stacji dokujÄ…cej %s" #. TRANSLATORS: drive refers to a storage device, e.g. SATA disk #, c-format msgid "%s Drive Update" msgstr "Aktualizacja dysku %s" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "Aktualizacja wbudowanego kontrolera %s" #. TRANSLATORS: a device that can read your fingerprint pattern #, c-format msgid "%s Fingerprint Reader Update" msgstr "Aktualizacja czytnika odcisków palców %s" #. TRANSLATORS: flash refers to solid state storage, e.g. UFS or eMMC #, c-format msgid "%s Flash Drive Update" msgstr "Aktualizacja dysku półprzewodnikowego %s" #. TRANSLATORS: GPU refers to a Graphics Processing Unit, e.g. #. * the "video card" #, c-format msgid "%s GPU Update" msgstr "Aktualizacja karty graficznej %s" #. TRANSLATORS: a large pressure-sensitive drawing area typically used #. * by artists and digital artists #, c-format msgid "%s Graphics Tablet Update" msgstr "Aktualizacja tabletu graficznego %s" #. TRANSLATORS: two miniature speakers attached to your ears #, c-format msgid "%s Headphones Update" msgstr "Aktualizacja sÅ‚uchawek %s" #. TRANSLATORS: headphones with an integrated microphone #, c-format msgid "%s Headset Update" msgstr "Aktualizacja sÅ‚uchawek z mikrofonem %s" #. TRANSLATORS: an input device used by gamers, e.g. a joystick #, c-format msgid "%s Input Controller Update" msgstr "Aktualizacja kontrolera %s" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "Aktualizacja klawiatury %s" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "Aktualizacja podsystemu ME urzÄ…dzenia %s" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "Aktualizacja myszy %s" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "Aktualizacja interfejsu sieciowego %s" #. TRANSLATORS: SSD refers to a Solid State Drive, e.g. non-rotating #. * SATA or NVMe disk #, c-format msgid "%s SSD Update" msgstr "Aktualizacja dysku SSD %s" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "Aktualizacja kontrolera pamiÄ™ci masowej %s" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "Aktualizacja komputera %s" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "Aktualizacja TPM %s" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "Aktualizacja kontrolera Thunderbolt %s" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "Aktualizacja panelu dotykowego %s" #. TRANSLATORS: Dock refers to the port replicator device connected #. * by plugging in a USB cable -- which may or may not also provide power #, c-format msgid "%s USB Dock Update" msgstr "Aktualizacja replikatora portów USB %s" #. TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth #. * device that stays in the USB port so the wireless peripheral works #, c-format msgid "%s USB Receiver Update" msgstr "Aktualizacja odbiornika USB %s" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "Aktualizacja urzÄ…dzenia %s" #. TRANSLATORS: warn the user before updating, %1 is a device name #, c-format msgid "%s and all connected devices may not be usable while updating." msgstr "%s i wszystkie podłączone urzÄ…dzenia nie bÄ™dÄ… mogÅ‚y być używane podczas aktualizacji." #. TRANSLATORS: %1 refers to some kind of security test, e.g. "Encrypted RAM". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s appeared: %s" msgstr "„%s†pojawiÅ‚o siÄ™: %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "UEFI platform #. key". #. * %2 and %3 refer to results value, e.g. "Valid" and "Invalid" #, c-format msgid "%s changed: %s → %s" msgstr "„%s†zmieniÅ‚o siÄ™: %s → %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "SPI BIOS #. region". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s disappeared: %s" msgstr "„%s†zniknęło: %s" #. TRANSLATORS: the device has a reason it can't update, e.g. laptop lid #. closed #, c-format msgid "%s is not currently updatable" msgstr "Nie można obecnie zaktualizować urzÄ…dzenia %s" #. TRANSLATORS: %1 is a device name, e.g. "ThinkPad Universal #. * ThunderBolt 4 Dock" and %2 is "fwupdmgr activate" #, c-format msgid "%s is pending activation; use %s to complete the update." msgstr "UrzÄ…dzenie %s oczekuje na aktywacjÄ™, polecenie „%s†dokoÅ„czy aktualizacjÄ™." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "Tryb serwisowy %s" #. TRANSLATORS: warn the user before #. * updating, %1 is a device name #, c-format msgid "%s must remain connected for the duration of the update to avoid damage." msgstr "UrzÄ…dzenie %s musi być podłączone podczas trwania aktualizacji, aby uniknąć uszkodzenia." #. TRANSLATORS: warn the user before updating, %1 is a machine #. * name #, c-format msgid "%s must remain plugged into a power source for the duration of the update to avoid damage." msgstr "UrzÄ…dzenie %s musi być podłączone do prÄ…du podczas trwania aktualizacji, aby uniknąć uszkodzenia." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "ObejÅ›cie %s" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "Wersja %s" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u dzieÅ„" msgstr[1] "%u dni" msgstr[2] "%u dni" msgstr[3] "%u dni" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "%u urzÄ…dzenie ma dostÄ™pnÄ… aktualizacjÄ™ oprogramowania sprzÄ™towego." msgstr[1] "%u urzÄ…dzenia majÄ… dostÄ™pnÄ… aktualizacjÄ™ oprogramowania sprzÄ™towego." msgstr[2] "%u urzÄ…dzeÅ„ ma dostÄ™pnÄ… aktualizacjÄ™ oprogramowania sprzÄ™towego." msgstr[3] "%u urzÄ…dzeÅ„ ma dostÄ™pnÄ… aktualizacjÄ™ oprogramowania sprzÄ™towego." #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device is not the best known configuration." msgid_plural "%u devices are not the best known configuration." msgstr[0] "%u urzÄ…dzenie nie ma najlepszej znanej konfiguracji." msgstr[1] "%u urzÄ…dzenia nie majÄ… najlepszej znanej konfiguracji." msgstr[2] "%u urzÄ…dzeÅ„ nie ma najlepszej znanej konfiguracji." msgstr[3] "%u urzÄ…dzeÅ„ nie ma najlepszej znanej konfiguracji." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u godzina" msgstr[1] "%u godziny" msgstr[2] "%u godzin" msgstr[3] "%u godzin" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minuta" msgstr[1] "%u minuty" msgstr[2] "%u minut" msgstr[3] "%u minut" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u sekunda" msgstr[1] "%u sekundy" msgstr[2] "%u sekund" msgstr[3] "%u sekund" #. TRANSLATORS: device tests can be specific to a CPU type #, c-format msgid "%u test was skipped" msgid_plural "%u tests were skipped" msgstr[0] "PominiÄ™to %u test" msgstr[1] "PominiÄ™to %u testy" msgstr[2] "PominiÄ™to %u testów" msgstr[3] "PominiÄ™to %u testów" #. TRANSLATORS: first percentage is current value, 2nd percentage is the #. * lowest limit the firmware update is allowed for the update to happen #, c-format msgid "%u%% (threshold %u%%)" msgstr "%u%% (próg to %u%%)" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(przestarzaÅ‚e)" #. TRANSLATORS: HSI event title msgid "A TPM PCR is now an invalid value" msgstr "PCR TPM ma teraz nieprawidÅ‚owÄ… wartość" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "AMD Firmware Replay Protection" msgstr "Ochrona przed odtwarzaniem oprogramowania sprzÄ™towego AMD" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "AMD Firmware Write Protection" msgstr "Ochrona przed zapisem oprogramowania sprzÄ™towego AMD" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "AMD Secure Processor Rollback Protection" msgstr "Ochrona procesora zabezpieczeÅ„ AMD przed wycofaniem" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "ARCHIVE FIRMWARE METAINFO [FIRMWARE] [METAINFO] [JCATFILE]" msgstr "ARCHIWUM OPROGRAMOWANIE-SPRZĘTOWE METAINFO [OPROGRAMOWANIE-SPRZĘTOWE] [METAINFO] [PLIK-JCAT]" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "Wymagane dziaÅ‚anie:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "Aktywuje urzÄ…dzenia" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "Aktywuje oczekujÄ…ce urzÄ…dzenia" msgid "Activate the new firmware on the device" msgstr "Aktywacja nowego oprogramowania sprzÄ™towego na urzÄ…dzeniu" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "Aktywowanie aktualizacji oprogramowania sprzÄ™towego" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "Aktywowanie aktualizacji oprogramowania sprzÄ™towego dla" #. TRANSLATORS: command description msgid "Adds devices to watch for future emulation" msgstr "Dodaje urzÄ…dzenia do obserwowania do emulacji w przyszÅ‚oÅ›ci" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Wiek" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "Wyrazić zgodÄ™ i włączyć repozytorium?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Alias do „%sâ€" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are now valid" msgstr "Wszystkie PCR TPM sÄ… teraz prawidÅ‚owe" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are valid" msgstr "Wszystkie PCR TPM sÄ… prawidÅ‚owe" #. TRANSLATORS: an application is preventing system updates msgid "All devices are prevented from update by system inhibit" msgstr "Aktualizacja wszystkich urzÄ…dzeÅ„ jest uniemożliwiana przez wstrzymanie systemu" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "Wszystkie urzÄ…dzenia tego samego typu zostanÄ… zaktualizowane w tym samym czasie" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Umożliwia instalowanie poprzednich wersji oprogramowania sprzÄ™towego" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "Umożliwia ponowne instalowanie istniejÄ…cych wersji oprogramowania sprzÄ™towego" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "Umożliwia przełączanie gałęzi oprogramowania sprzÄ™towego" #. TRANSLATORS: error message msgid "Already exists, and no --force specified" msgstr "Już istnieje i nie podano --force" #. TRANSLATORS: is not the main firmware stream msgid "Alternate branch" msgstr "Alternatywna gałąź" #. TRANSLATORS: another application is updating the device already msgid "An update is in progress" msgstr "Trwa aktualizacja" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "UkoÅ„czenie aktualizacji wymaga ponownego uruchomienia." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "UkoÅ„czenie aktualizacji wymaga wyłączenia komputera." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Odpowiada tak na wszystkie pytania" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Zastosowuje aktualizacjÄ™ nawet, jeÅ›li nie jest zalecana" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Zastosowuje pliki aktualizacji" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "Zastosowywanie aktualizacji…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Zatwierdzone oprogramowanie sprzÄ™towe:" msgstr[1] "Zatwierdzone oprogramowanie sprzÄ™towe:" msgstr[2] "Zatwierdzone oprogramowanie sprzÄ™towe:" msgstr[3] "Zatwierdzone oprogramowanie sprzÄ™towe:" #. TRANSLATORS: command description msgid "Asks the daemon to quit" msgstr "Prosi usÅ‚ugÄ™ o zakoÅ„czenie dziaÅ‚ania" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "Podłącza do trybu oprogramowania sprzÄ™towego" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Uwierzytelnianie…" #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "Wymagane sÄ… informacje o uwierzytelnieniu" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Wymagane jest uwierzytelnienie, aby zainstalować poprzedniÄ… wersjÄ™ oprogramowania sprzÄ™towego urzÄ…dzenia wymiennego" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Wymagane jest uwierzytelnienie, aby zainstalować poprzedniÄ… wersjÄ™ oprogramowania sprzÄ™towego tego komputera" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to enable emulation data collection" msgstr "Wymagane jest uwierzytelnienie, aby włączyć zbieranie danych emulacji" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to fix a host security issue" msgstr "Wymagane jest uwierzytelnienie, aby naprawić problem z zabezpieczeniami komputera" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to load hardware emulation data" msgstr "Wymagane jest uwierzytelnienie, aby wczytać dane emulacji sprzÄ™tu" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "Wymagane jest uwierzytelnienie, aby zmodyfikować ustawienia BIOS-u" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "Wymagane jest uwierzytelnienie, aby zmodyfikować skonfigurowane repozytorium używane do aktualizacji oprogramowania sprzÄ™towego" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "Wymagane jest uwierzytelnienie, aby zmodyfikować konfiguracjÄ™ usÅ‚ugi" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "Wymagane jest uwierzytelnienie, aby odczytać ustawienia BIOS-u" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to reset daemon configuration to defaults" msgstr "Wymagane jest uwierzytelnienie, aby przywrócić konfiguracjÄ™ usÅ‚ugi do domyÅ›lnej" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to save hardware emulation data" msgstr "Wymagane jest uwierzytelnienie, aby zapisać dane emulacji sprzÄ™tu" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "Wymagane jest uwierzytelnienie, aby ustawić listÄ™ zatwierdzonego oprogramowania sprzÄ™towego" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "Wymagane jest uwierzytelnienie, aby podpisać dane za pomocÄ… certyfikatu klienta" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to stop the firmware update service" msgstr "Wymagane jest uwierzytelnienie, aby zatrzymać usÅ‚ugÄ™ aktualizacji oprogramowania sprzÄ™towego" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "Wymagane jest uwierzytelnienie, aby przełączyć na nowÄ… wersjÄ™ oprogramowania sprzÄ™towego" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to undo the fix for a host security issue" msgstr "Wymagane jest uwierzytelnienie, aby wycofać poprawkÄ™ problemu z zabezpieczeniami komputera" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Wymagane jest uwierzytelnienie, aby odblokować urzÄ…dzenie" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Wymagane jest uwierzytelnienie, aby zaktualizować oprogramowanie sprzÄ™towe wymiennego urzÄ…dzenia" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Wymagane jest uwierzytelnienie, aby zaktualizować oprogramowanie sprzÄ™towe tego komputera" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "Wymagane jest uwierzytelnienie, aby zaktualizować przechowywane sumy kontrolne dla urzÄ…dzenia" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "Automatyczne zgÅ‚aszanie" #. TRANSLATORS: we can auto-uninhibit after a timeout #, c-format msgid "Automatically uninhibiting in %ums…" msgstr "Automatyczne koÅ„czenie wstrzymania za %u ms…" #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "Automatycznie wysyÅ‚ać za każdym razem?" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS Firmware Updates" msgstr "Aktualizacje oprogramowania sprzÄ™towego BIOS-u" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS Rollback Protection" msgstr "Ochrona BIOS-u przed wycofaniem" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS firmware updates" msgstr "Aktualizacje oprogramowania sprzÄ™towego BIOS-u" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS rollback protection" msgstr "Ochrona BIOS-u przed wycofaniem" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "Aktualizacje BIOS-u dostarczane przez usÅ‚ugÄ™ LVFS lub Windows Update" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "BUILDER-XML FILENAME-DST" msgstr "BUILDER-XML NAZWA-PLIKU-DOCELOWEGO" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "Akumulator" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "DowiÄ…zuje nowy sterownik jÄ…dra" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "Zablokowane pliki oprogramowania sprzÄ™towego:" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "Zablokowana wersja" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "Blokowanie oprogramowania sprzÄ™towego:" #. TRANSLATORS: command description msgid "Blocks a specific firmware from being installed" msgstr "Blokuje możliwość instalacji podanego oprogramowania sprzÄ™towego" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "Wersja programu startowego" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "Gałąź" #. TRANSLATORS: command description msgid "Build a cabinet archive from a firmware blob and XML metadata" msgstr "Buduje archiwum CAB z zamkniÄ™tego oprogramowania sprzÄ™towego i metadanych XML" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "Buduje plik oprogramowania sprzÄ™towego" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * Utilized by OS means the distribution enabled it msgid "CET OS Support" msgstr "ObsÅ‚uga CET przez system operacyjny" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * enabled means supported by the processor msgid "CET Platform" msgstr "Platforma CET" #. TRANSLATORS: we can save all device enumeration events for emulation msgid "Can tag for emulation" msgstr "Może być oznaczone do emulacji" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Anuluj" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "Anulowano" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "Nie można zastosować, ponieważ aktualizacja bazy dbx zostaÅ‚a już zastosowana." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Zmieniono" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "Sprawdza, czy kryptograficzna suma kontrolna pasuje do oprogramowania sprzÄ™towego" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Suma kontrolna" #. TRANSLATORS: get interactive prompt, where branch is the #. * supplier of the firmware, e.g. "non-free" or "free" msgid "Choose branch" msgstr "Wybór gałęzi" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "Wybór urzÄ…dzenia" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "Wybór oprogramowania sprzÄ™towego" #. TRANSLATORS: get interactive prompt msgid "Choose release" msgstr "Wybór wydania" #. TRANSLATORS: get interactive prompt msgid "Choose volume" msgstr "Wybór woluminu" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "Usuwa wyniki z ostatniej aktualizacji" #. TRANSLATORS: error message msgid "Command not found" msgstr "Nie odnaleziono polecenia" #. TRANSLATORS: is not supported by the vendor msgid "Community supported" msgstr "Wspierane przez spoÅ‚eczność" #. TRANSLATORS: command description msgid "Compares two versions for equality" msgstr "Porównuje dwie wersje pod wzglÄ™dem równoÅ›ci" #. TRANSLATORS: title prefix for the BIOS settings dialog msgid "Configuration Change Suggested" msgstr "Proponowana zmiana konfiguracji" #. TRANSLATORS: no peeking msgid "Configuration is only readable by the system administrator" msgstr "Tylko administrator komputera może odczytywać konfiguracjÄ™" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology msgid "Control-flow Enforcement Technology" msgstr "Technologia egzekwowania przepÅ‚ywu sterowania" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "Konwertuje plik oprogramowania sprzÄ™towego" #. TRANSLATORS: command description msgid "Create an EFI boot entry" msgstr "Tworzy wpis uruchamiania EFI" #. TRANSLATORS: when the update was built msgid "Created" msgstr "Utworzono" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "Krytyczna" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "DostÄ™pne jest sprawdzenie poprawnoÅ›ci kryptograficznej sumy kontrolnej" #. TRANSLATORS: current value of a BIOS setting msgid "Current Value" msgstr "Obecna wartość" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "Obecna wersja" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "IDENTYFIKATOR-URZÄ„DZENIA|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Opcje debugowania" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "Dekompresowanie…" #. TRANSLATORS: command description msgid "Delete an EFI boot entry" msgstr "Usuwa wpis uruchamiania EFI" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Opis" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "Odłącza do trybu programu startowego" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "Informacje" #. TRANSLATORS: the best known configuration is a set of software that we know #. works #. * well together. In the OEM and ODM industries it is often called a BKC msgid "Deviate from the best known configuration?" msgstr "Odejść od najlepszej znanej konfiguracji?" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "Flagi urzÄ…dzenia" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "Identyfikator urzÄ…dzenia" #. TRANSLATORS: description of the device requests msgid "Device Requests" msgstr "Żądania urzÄ…dzeÅ„" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Dodano urzÄ…dzenie:" #. TRANSLATORS: the device is already connected msgid "Device already exists" msgstr "UrzÄ…dzenie już istnieje" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "Poziom naÅ‚adowania akumulatora urzÄ…dzenia jest za niski" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse #, c-format msgid "Device battery power is too low (%u%%, requires %u%%)" msgstr "Poziom naÅ‚adowania akumulatora urzÄ…dzenia jest za niski (%u%%, wymaga %u%%)" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "UrzÄ…dzenie może przywrócić siÄ™ po niepowodzeniu wgrywania" #. TRANSLATORS: lid means "laptop top cover" msgid "Device cannot be updated while the lid is closed" msgstr "Nie można zaktualizować urzÄ…dzenia, kiedy pokrywa jest zamkniÄ™ta" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Zmieniono urzÄ…dzenie:" #. TRANSLATORS: a version check is required for all firmware msgid "Device firmware is required to have a version check" msgstr "Oprogramowanie sprzÄ™towe urzÄ…dzenia musi mieć test wersji" #. TRANSLATORS: emulated means we are pretending to be a different model msgid "Device is emulated" msgstr "UrzÄ…dzenie jest emulowane" #. TRANSLATORS: device cannot be interrupted, for instance taking a phone call msgid "Device is in use" msgstr "UrzÄ…dzenie jest używane" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "UrzÄ…dzenie jest zablokowane" #. TRANSLATORS: we have two ways of communicating with the device, so we hide #. one msgid "Device is lower priority than an equivalent device" msgstr "UrzÄ…dzenie ma niższy priorytet niż jego odpowiednik" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "UrzÄ…dzenie musi zainstalować wszystkie podane wydania" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "Nie można komunikować siÄ™ z urzÄ…dzeniem" #. TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode msgid "Device is unreachable, or out of wireless range" msgstr "Nie można komunikować siÄ™ z urzÄ…dzeniem lub jest poza zasiÄ™giem" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "UrzÄ…dzenie może być używane podczas trwania aktualizacji" #. TRANSLATORS: usually this is when we're waiting for a reboot msgid "Device is waiting for the update to be applied" msgstr "UrzÄ…dzenie oczekuje na zastosowanie aktualizacji" #. TRANSLATORS: success, so say thank you to the user msgid "Device list uploaded successfully, thanks!" msgstr "PomyÅ›lnie wysÅ‚ano listÄ™ urzÄ…dzeÅ„, dziÄ™kujemy!" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "UsuniÄ™to urzÄ…dzenie:" #. TRANSLATORS: as in, wired mains power for a laptop msgid "Device requires AC power to be connected" msgstr "UrzÄ…dzenie wymaga podłączenia do prÄ…du" #. TRANSLATORS: device does not have a display connected msgid "Device requires a display to be plugged in" msgstr "UrzÄ…dzenie wymaga, aby ekran byÅ‚ podłączony" #. TRANSLATORS: The device cannot be updated due to missing vendor's license." msgid "Device requires a software license to update" msgstr "UrzÄ…dzenie wymaga licencji na oprogramowanie, aby zaktualizować" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "UrzÄ…dzenie przygotowuje aktualizacje" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "UrzÄ…dzenie obsÅ‚uguje przełączanie na innÄ… gałąź oprogramowania sprzÄ™towego" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "Aktualizacja urzÄ…dzenia wymaga aktywacji" #. TRANSLATORS: save the old firmware to disk before installing the new one msgid "Device will backup firmware before installing" msgstr "UrzÄ…dzenie wykona kopiÄ™ zapasowÄ… oprogramowania sprzÄ™towego przed instalacjÄ…" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "UrzÄ…dzenie nie pojawi siÄ™ z powrotem po ukoÅ„czeniu aktualizacji" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "PomyÅ›lnie zaktualizowane urzÄ…dzenia:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "UrzÄ…dzenia, które nie zostaÅ‚y poprawnie zaktualizowane:" #. TRANSLATORS: message letting the user there is an update #. * waiting, but there is a reason it cannot be deployed msgid "Devices with firmware updates that need user action: " msgstr "UrzÄ…dzenia z aktualizacjami oprogramowania sprzÄ™towego wymagajÄ…cymi dziaÅ‚ania użytkownika:" #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "UrzÄ…dzenia bez dostÄ™pnych aktualizacji oprogramowania sprzÄ™towego: " #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "UrzÄ…dzenia z najnowszÄ… dostÄ™pnÄ… wersjÄ… oprogramowania sprzÄ™towego:" #. TRANSLATORS: this is for the device tests msgid "Did not find any devices with matching GUIDs" msgstr "Nie odnaleziono żadnych urzÄ…dzeÅ„ o pasujÄ…cych GUID" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "Wyłączone" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "Wyłącza podane repozytorium" #. TRANSLATORS: command description msgid "Disables virtual testing devices" msgstr "Wyłącza wirtualne urzÄ…dzenia testowe" #. TRANSLATORS: the OS the release was tested on msgid "Distribution" msgstr "Dystrybucja" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "Bez sprawdzania przestarzaÅ‚ych metadanych" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "Bez sprawdzania niezgÅ‚oszonej historii" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "Bez sprawdzania, czy repozytoria pobierania majÄ… być włączone" #. TRANSLATORS: command line option msgid "Do not check or prompt for reboot after update" msgstr "Bez sprawdzania ani pytania o ponowne uruchomienie po aktualizacji" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "Bez dołączania przedrostka domeny dziennika" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "Bez dołączania przedrostka czasu" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "Bez wykonywania testów bezpieczeÅ„stwa urzÄ…dzenia" #. TRANSLATORS: command line option msgid "Do not prompt for devices" msgstr "Bez pytania o urzÄ…dzenia" #. TRANSLATORS: command line option msgid "Do not prompt to fix security issues" msgstr "Bez pytania o naprawienie problemów zabezpieczeÅ„" #. TRANSLATORS: command line option msgid "Do not search the firmware when parsing" msgstr "Bez wyszukiwania oprogramowania sprzÄ™towego podczas przetwarzania" #. TRANSLATORS: warning message shown after update has been scheduled msgid "Do not turn off your computer or remove the AC adaptor while the update is in progress." msgstr "Nie należy wyłączać komputera ani odłączać zasilacza w czasie trwania aktualizacji." #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "Bez zapisywania do bazy danych historii" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "Czy rozumiesz konsekwencje zmiany gałęzi oprogramowania sprzÄ™towego?" #. TRANSLATORS: ask the user if it's okay to convert, #. * "it" being the data contained in the EFI boot entry msgid "Do you want to convert it now?" msgstr "Przekonwertować teraz?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "Wyłączyć tÄ™ funkcjÄ™ dla przyszÅ‚ych aktualizacji?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "OdÅ›wieżyć to repozytorium teraz?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "Automatycznie wysyÅ‚ać zgÅ‚oszenia dla przyszÅ‚ych aktualizacji?" #. TRANSLATORS: command line option msgid "Don't prompt for authentication (less details may be shown)" msgstr "Bez pytania o uwierzytelnienie (może być wyÅ›wietlanych mniej informacji)" #. TRANSLATORS: success msgid "Done!" msgstr "Gotowe." #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "Zainstalować poprzedniÄ… wersjÄ™ urzÄ…dzenia %s z %s do %s?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "Instaluje poprzedniÄ… wersjÄ™ oprogramowania sprzÄ™towego urzÄ…dzenia" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "Instalowanie poprzedniej wersji urzÄ…dzenia %s…" #. TRANSLATORS: command description msgid "Download a file" msgstr "Pobiera plik" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Pobieranie…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "Zrzuca dane SMBIOS z pliku" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "Czas trwania" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "EMULATION-FILE [ARCHIVE-FILE]" msgstr "PLIK-EMULACJI [PLIK-ARCHIWUM]" #. TRANSLATORS: command description msgid "Emulate a device using a JSON manifest" msgstr "Emuluje urzÄ…dzenie za pomocÄ… manifestu JSON" #. TRANSLATORS: this device is not actually real msgid "Emulated" msgstr "Emulowane" #. TRANSLATORS: Title: if we are emulating a different host msgid "Emulated host" msgstr "Emulowany komputer" msgid "Enable" msgstr "Włącz" msgid "Enable emulation data collection" msgstr "Włączenie zbierania danych emulacji" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "Włączyć nowe repozytorium?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "Włączyć to repozytorium?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Włączone" #. TRANSLATORS: Plugin is active only if hardware is found msgid "Enabled if hardware matches" msgstr "Włączone, jeÅ›li sprzÄ™t pasuje" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "Włącza podane repozytorium" #. TRANSLATORS: command description msgid "Enables virtual testing devices" msgstr "Włącza wirtualne urzÄ…dzenia testowe" msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "Włączenie tej funkcjonalnoÅ›ci jest wykonywane na wÅ‚asne ryzyko, co oznacza, że należy skontaktować siÄ™ z oryginalnym producentem sprzÄ™tu w sprawie ewentualnych problemów spowodowanych przez te aktualizacje. Tylko problemy z samym procesem aktualizacji powinny być zgÅ‚aszane pod adresem $OS_RELEASE:BUG_REPORT_URL$." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "Włączenie tego repozytorium wykonywane jest na wÅ‚asne ryzyko." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Zaszyfrowane" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "Zaszyfrowana pamięć RAM" #. TRANSLATORS: the vendor is no longer supporting the device msgid "End of life" msgstr "Niewspierane" #. TRANSLATORS: The BIOS setting can only be changed to fixed values msgid "Enumeration" msgstr "Wyliczenie" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "Usuwa całą historiÄ™ aktualizacji oprogramowania sprzÄ™towego" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Usuwanie zawartoÅ›ci…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "KoÅ„czy dziaÅ‚anie po maÅ‚ym opóźnieniu" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "KoÅ„czy dziaÅ‚anie po wczytaniu mechanizmu" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "Eksportuje strukturÄ™ pliku oprogramowania sprzÄ™towego do pliku XML" #. TRANSLATORS: command description msgid "Export firmware history for manual upload" msgstr "Eksportuje historiÄ™ oprogramowania sprzÄ™towego do rÄ™cznego wysÅ‚ania" #. TRANSLATORS: command description msgid "Extract a firmware blob to images" msgstr "Rozpakowuje zamkniÄ™te oprogramowanie sprzÄ™towe do obrazów" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "PLIK" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "PLIK [IDENTYFIKATOR-URZÄ„DZENIA|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "NAZWA-PLIKU" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "NAZWA-PLIKU CERTYFIKAT KLUCZ-PRYWATNY" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "NAZWA-PLIKU IDENTYFIKATOR-URZÄ„DZENIA" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME OFFSET DATA [FIRMWARE-TYPE]" msgstr "NAZWA-PLIKU WYRÓWNANIE DANE [TYP-OPROGRAMOWANIA-SPRZĘTOWEGO]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "NAZWA-PLIKU [IDENTYFIKATOR-URZÄ„DZENIA|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "NAZWA-PLIKU [TYP-OPROGRAMOWANIA-SPRZĘTOWEGO]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" msgstr "NAZWA-PLIKU-ŹRÓDÅOWEGO NAZWA-PLIKU-DOCELOWEGO [TYP-ŹRÓDÅOWEGO-OPROGRAMOWANIA-SPRZĘTOWEGO] [TYP-DOCELOWEGO-OPROGRAMOWANIA-SPRZĘTOWEGO]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" msgstr "NAZWA-PLIKU|SUMA-KONTROLNA1[,SUMA-KONTROLNA2][,SUMA-KONTROLNA3]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "Nie powiodÅ‚o siÄ™" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Zastosowanie aktualizacji siÄ™ nie powiodÅ‚o" #. TRANSLATORS: error message for Windows msgid "Failed to connect to Windows service, please ensure it's running." msgstr "Połączenie z usÅ‚ugÄ… systemu Windows siÄ™ nie powiodÅ‚o, proszÄ™ siÄ™ upewnić, że ona dziaÅ‚a." #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "Połączenie z usÅ‚ugÄ… siÄ™ nie powiodÅ‚o" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "Wczytanie lokalnej bazy dbx siÄ™ nie powiodÅ‚o" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "Wczytanie systemowej bazy dbx siÄ™ nie powiodÅ‚o" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "Zablokowanie siÄ™ nie powiodÅ‚o" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Przetworzenie parametrów siÄ™ nie powiodÅ‚o" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "Przetworzenie pliku siÄ™ nie powiodÅ‚o" #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #, c-format msgid "Failed to parse flags for %s" msgstr "Przetworzenie flag dla opcji %s siÄ™ nie powiodÅ‚o" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "Przetworzenie lokalnej bazy dbx siÄ™ nie powiodÅ‚o" #. TRANSLATORS: a feature is something like "can show an image" msgid "Failed to set front-end features" msgstr "Ustawienie funkcji interfejsu siÄ™ nie powiodÅ‚o" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "Sprawdzenie poprawnoÅ›ci zawartoÅ›ci ESP siÄ™ nie powiodÅ‚o" #. TRANSLATORS: item is FALSE msgid "False" msgstr "FaÅ‚sz" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Nazwa pliku" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "Podpis nazwy pliku" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "ŹródÅ‚o nazwy pliku" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Wymagana jest nazwa pliku" #. TRANSLATORS: command line option msgid "Filter with a set of device flags using a ~ prefix to exclude, e.g. 'internal,~needs-reboot'" msgstr "Filtruje za pomocÄ… zestawu flag urzÄ…dzeÅ„, przedrostek ~ wyklucza, np. „internal,~needs-rebootâ€" #. TRANSLATORS: command line option msgid "Filter with a set of release flags using a ~ prefix to exclude, e.g. 'trusted-release,~trusted-metadata'" msgstr "Filtruje za pomocÄ… zestawu flag wydaÅ„, przedrostek ~ wyklucza, np. „trusted-release,~trusted-metadataâ€" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware Attestation" msgstr "ZaÅ›wiadczenie oprogramowania sprzÄ™towego" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "Deskryptor BIOS-u oprogramowania sprzÄ™towego" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "Region BIOS-u oprogramowania sprzÄ™towego" #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "Podstawowy adres URI oprogramowania sprzÄ™towego" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "UsÅ‚uga D-Bus aktualizacji oprogramowania sprzÄ™towego" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "UsÅ‚uga aktualizacji oprogramowania sprzÄ™towego" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "Weryfikacja aktualizatora oprogramowania sprzÄ™towego" #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "Aktualizacje oprogramowania sprzÄ™towego" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "NarzÄ™dzie oprogramowania sprzÄ™towego" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "Ochrona przed zapisem oprogramowania sprzÄ™towego" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "Blokada ochrony przed zapisem oprogramowania sprzÄ™towego" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "ZaÅ›wiadczenie oprogramowania sprzÄ™towego" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "Oprogramowanie sprzÄ™towe jest już zablokowane" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "Oprogramowanie nie jest już zablokowane" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "Metadane oprogramowania sprzÄ™towego nie zostaÅ‚y zaktualizowane przez %u dzieÅ„ i mogÄ… nie być aktualne." msgstr[1] "Metadane oprogramowania sprzÄ™towego nie zostaÅ‚y zaktualizowane przez %u dni i mogÄ… nie być aktualne." msgstr[2] "Metadane oprogramowania sprzÄ™towego nie zostaÅ‚y zaktualizowane przez %u dni i mogÄ… nie być aktualne." msgstr[3] "Metadane oprogramowania sprzÄ™towego nie zostaÅ‚y zaktualizowane przez %u dni i mogÄ… nie być aktualne." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "Aktualizacje oprogramowania sprzÄ™towego" #. TRANSLATORS: user needs to run a command, %1 is 'fwupdmgr unlock' #, c-format msgid "Firmware updates disabled; run '%s' to enable" msgstr "Aktualizacje oprogramowania sprzÄ™towego sÄ… wyłączone; wykonanie polecenia „%s†je włączy" #. TRANSLATORS: command description msgid "Fix a specific host security attribute" msgstr "Naprawia podany atrybut zabezpieczeÅ„ komputera" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fix reverted successfully" msgstr "PomyÅ›lnie wycofano poprawkÄ™" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fixed successfully" msgstr "PomyÅ›lnie naprawiono" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "Flagi" #. TRANSLATORS: command line option msgid "Force the action by relaxing some runtime checks" msgstr "Wymusza dziaÅ‚anie przez rozluźnienie części testów uruchamiania" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Odnaleziono" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "Wykryto peÅ‚ne szyfrowanie dysku" #. TRANSLATORS: we might ask the user the recovery key when next booting #. Windows msgid "Full disk encryption secrets may be invalidated when updating" msgstr "HasÅ‚a peÅ‚nego szyfrowania dysku mogÄ… zostać unieważnione podczas aktualizacji" #. TRANSLATORS: Title: if the part has been fused msgid "Fused Platform" msgstr "Platforma z bezpiecznikiem" #. TRANSLATORS: Title: if the part has been fused msgid "Fused platform" msgstr "Platforma z bezpiecznikiem" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" msgstr[1] "GUID" msgstr[2] "GUID" msgstr[3] "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "GUID|DEVICE-ID" msgstr "GUID|IDENTYFIKATOR-URZÄ„DZENIA" msgid "Get BIOS settings" msgstr "Uzyskanie ustawieÅ„ BIOS-u" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "Uzyskuje wszystkie flagi urzÄ…dzeÅ„ obsÅ‚ugiwane przez usÅ‚ugÄ™ fwupd" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Uzyskuje wszystkie urzÄ…dzenia obsÅ‚ugujÄ…ce aktualizacje oprogramowania sprzÄ™towego" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "Uzyskuje wszystkie włączone wtyczki zarejestrowane na komputerze" #. TRANSLATORS: command description msgid "Get all known version formats" msgstr "Uzyskuje wszystkie znane formaty wersji" #. TRANSLATORS: command description msgid "Get device report metadata" msgstr "Uzyskuje metadane zgÅ‚oszenia urzÄ…dzenia" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Uzyskuje informacje o pliku oprogramowania sprzÄ™towego" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "Uzyskuje skonfigurowane repozytoria" #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "Uzyskuje atrybuty zabezpieczeÅ„ komputera" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "Uzyskuje listÄ™ zatwierdzonego oprogramowania sprzÄ™towego" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "Uzyskuje listÄ™ zablokowanego oprogramowania sprzÄ™towego" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Uzyskuje listÄ™ aktualizacji dla podłączonego sprzÄ™tu" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "Uzyskuje wydania dla urzÄ…dzenia" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Uzyskuje wyniki z ostatniej aktualizacji" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "PLIK-HWIDS" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "SprzÄ™t czeka na ponowne podłączenie" #. TRANSLATORS: the release urgency msgid "High" msgstr "Wysoka" #. TRANSLATORS: title for host security events msgid "Host Security Events" msgstr "Zdarzenia zabezpieczeÅ„ komputera" #. TRANSLATORS: error message for unsupported feature #, c-format msgid "Host Security ID (HSI) is not supported" msgstr "Identyfikator zabezpieczeÅ„ komputera (HSI) jest nieobsÅ‚ugiwany" #. TRANSLATORS: success, so say thank you to the user msgid "Host Security ID attributes uploaded successfully, thanks!" msgstr "PomyÅ›lnie wysÅ‚ano atrybuty identyfikatora zabezpieczeÅ„ komputera." #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "Identyfikator zabezpieczeÅ„ komputera:" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX" msgstr "INDEKS" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX KEY [VALUE]" msgstr "INDEKS KLUCZ [WARTOŚĆ]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX NAME TARGET [MOUNTPOINT]" msgstr "INDEKS NAZWA CEL [PUNKT-MONTOWANIA]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX1,INDEX2" msgstr "INDEKS1,INDEKS2" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INHIBIT-ID" msgstr "IDENTYFIKATOR-WSTRZYMANIA" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "Ochrona IOMMU" #. TRANSLATORS: HSI event title msgid "IOMMU device protection disabled" msgstr "Wyłączono ochronÄ™ urzÄ…dzenia IOMMU" #. TRANSLATORS: HSI event title msgid "IOMMU device protection enabled" msgstr "Włączono ochronÄ™ urzÄ…dzenia IOMMU" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "Bezczynne…" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "Ignoruje Å›cisÅ‚e testy SSL podczas pobierania plików" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "Ignoruje niepowodzenia sum kontrolnych oprogramowania sprzÄ™towego" #. TRANSLATORS: command line option msgid "Ignore firmware hardware mismatch failures" msgstr "Ignoruje niepowodzenia zgodnoÅ›ci sprzÄ™tu z oprogramowaniem sprzÄ™towym" #. TRANSLATORS: command line option msgid "Ignore non-critical firmware requirements" msgstr "Ignoruje niekrytyczne wymagania oprogramowania sprzÄ™towego" #. TRANSLATORS: try to help msgid "Ignoring SSL strict checks, to do this automatically in the future export DISABLE_SSL_STRICT in your environment" msgstr "Ignorowanie Å›cisÅ‚ych testów SSL, aby robić to automatycznie w przyszÅ‚oÅ›ci, należy wyeksportować zmiennÄ… DISABLE_SSL_STRICT w środowisku" #. TRANSLATORS: show the user a generic image that can be themed msgid "Image" msgstr "Obraz" #. TRANSLATORS: show the user a random image from the internet msgid "Image (custom)" msgstr "Obraz (niestandardowy)" #. TRANSLATORS: the inhibit ID is a short string like dbus-123456 #, c-format msgid "Inhibit ID is %s." msgstr "Identyfikator wstrzymania to %s." #. TRANSLATORS: command description msgid "Inhibit the system to prevent upgrades" msgstr "Wstrzymuje system, aby uniemożliwić aktualizacje" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "Czas trwania instalacji" #. TRANSLATORS: command description msgid "Install a firmware file in cabinet format on this hardware" msgstr "Instaluje plik oprogramowania sprzÄ™towego w formacie CAB na tym sprzÄ™cie" #. TRANSLATORS: command description msgid "Install a raw firmware blob on a device" msgstr "Instaluje surowe zamkniÄ™te oprogramowanie sprzÄ™towe na urzÄ…dzeniu" #. TRANSLATORS: command description msgid "Install a specific firmware file on all devices that match" msgstr "Instaluje podany plik oprogramowania sprzÄ™towego na wszystkich pasujÄ…cych urzÄ…dzeniach" #. TRANSLATORS: command description msgid "Install a specific firmware on a device, all possible devices will also be installed once the CAB matches" msgstr "Instaluje podane oprogramowanie sprzÄ™towe na urzÄ…dzeniu, wszystkie możliwe urzÄ…dzenia także zostanÄ… zainstalowane, jeÅ›li CAB pasuje" msgid "Install old version of signed system firmware" msgstr "Instalacja poprzedniej wersji podpisanego oprogramowania sprzÄ™towego komputera" msgid "Install old version of unsigned system firmware" msgstr "Instalacja poprzedniej wersji niepodpisanego oprogramowania sprzÄ™towego komputera" msgid "Install signed device firmware" msgstr "Instalacja podpisanego oprogramowania sprzÄ™towego urzÄ…dzenia" msgid "Install signed system firmware" msgstr "Instalacja podpisanego oprogramowania sprzÄ™towego komputera" msgid "Install unsigned device firmware" msgstr "Instalacja niepodpisanego oprogramowania sprzÄ™towego urzÄ…dzenia" msgid "Install unsigned system firmware" msgstr "Instalacja niepodpisanego oprogramowania sprzÄ™towego komputera" #. TRANSLATORS: stay on one firmware version unless the new version is #. explicitly #. * specified msgid "Installing a specific release is explicitly required" msgstr "Wymagane jest instalowanie konkretnego wydania" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "Instalowanie aktualizacji oprogramowania sprzÄ™towego…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "Instalowanie na urzÄ…dzeniu %s…" #. TRANSLATORS: if it breaks, you get to keep both pieces msgid "Installing this update may also void any device warranty." msgstr "Zainstalowanie tej aktualizacji może także spowodować utratÄ™ wszelkiej gwarancji na urzÄ…dzenie." #. TRANSLATORS: The BIOS setting only accepts integers in a fixed range msgid "Integer" msgstr "Liczba caÅ‚kowita" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM Protected" msgstr "ACM chronione przez Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "ACM chronione przez Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard Error Policy" msgstr "Zasady postÄ™powania Intel BootGuard w przypadku błędów" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard Fuse" msgstr "Bezpiecznik Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "Bezpiecznik OTP Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard Verified Boot" msgstr "Uruchamianie zweryfikowane przez Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Zasady postÄ™powania Intel BootGuard w przypadku błędów" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Uruchamianie zweryfikowane przez Intel BootGuard" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS Mitigation" msgstr "ZaÅ‚atanie dziury GDS procesorów Intel" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS mitigation" msgstr "ZaÅ‚atanie dziury GDS procesorów Intel" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Manufacturing Mode" msgstr "Tryb serwisowy Intel Management Engine" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the "override" is #. enabled #. * with a jumper -- luckily it is probably not accessible to end users on #. consumer #. * boards msgid "Intel Management Engine Override" msgstr "ObejÅ›cie Intel Management Engine" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Version" msgstr "Wersja Intel Management Engine" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "WewnÄ™trzne urzÄ…dzenie" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "NieprawidÅ‚owe" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "NieprawidÅ‚owe parametry" #. TRANSLATORS: error message msgid "Invalid arguments, expected GUID" msgstr "NieprawidÅ‚owe parametry, oczekiwano GUID" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX KEY [VALUE]" msgstr "NieprawidÅ‚owe parametry, oczekiwano INDEKS KLUCZ [WARTOŚĆ]" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX NAME TARGET [MOUNTPOINT]" msgstr "NieprawidÅ‚owe parametry, oczekiwano INDEKS NAZWA CEL [PUNKT-MONTOWANIA]" #. TRANSLATOR: This is the error message for #. * incorrect parameter msgid "Invalid arguments, expected an AppStream ID" msgstr "NieprawidÅ‚owe parametry, oczekiwano identyfikatora AppStream" #. TRANSLATORS: error message msgid "Invalid arguments, expected at least ARCHIVE FIRMWARE METAINFO" msgstr "NieprawidÅ‚owe parametry, oczekiwano co najmniej ARCHIWUM OPROGRAMOWANIE-SPRZĘTOWE METAINFO" #. TRANSLATORS: error message msgid "Invalid arguments, expected base-16 integer" msgstr "NieprawidÅ‚owe parametry, oczekiwano szesnastkowej liczby caÅ‚kowitej" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "Jest instalacjÄ… poprzedniej wersji" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "Jest w trybie programu startowego" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "Jest aktualizacjÄ…" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "Błąd" msgstr[1] "Błędy" msgstr[2] "Błędy" msgstr[3] "Błędy" #. TRANSLATORS: HSI event title msgid "Kernel is no longer tainted" msgstr "JÄ…dro nie jest już skażone" #. TRANSLATORS: HSI event title msgid "Kernel is tainted" msgstr "JÄ…dro jest skażone" #. TRANSLATORS: HSI event title msgid "Kernel lockdown disabled" msgstr "Wyłączono blokadÄ™ jÄ…dra" #. TRANSLATORS: HSI event title msgid "Kernel lockdown enabled" msgstr "Włączono blokadÄ™ jÄ…dra" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "POÅOÅ»ENIE" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "Ostatnia modyfikacja" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "PozostaÅ‚a mniej niż jedna minuta" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "Licencja" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "Blokada jÄ…dra Linux" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "Weryfikacja jÄ…dra Linux" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "Obszar wymiany systemu Linux" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Linux Vendor Firmware Service (stabilne oprogramowanie sprzÄ™towe)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Linux Vendor Firmware Service (testowe oprogramowanie sprzÄ™towe)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "JÄ…dro Linux" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Blokada jÄ…dra Linux" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Obszar wymiany systemu Linux" #. TRANSLATORS: command description msgid "List EFI boot files" msgstr "WyÅ›wietla listÄ™ plików uruchamiania EFI" #. TRANSLATORS: command description msgid "List EFI boot parameters" msgstr "WyÅ›wietla listÄ™ parametrów uruchamiania EFI" #. TRANSLATORS: command description msgid "List EFI variables with a specific GUID" msgstr "WyÅ›wietla listÄ™ zmiennych EFI o podanym GUID" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "WyÅ›wietla listÄ™ wpisów w bazie dbx" #. TRANSLATORS: command description msgid "List the available firmware GTypes" msgstr "WyÅ›wietla listÄ™ GType dostÄ™pnego oprogramowania sprzÄ™towego" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "WyÅ›wietla listÄ™ dostÄ™pnych typów oprogramowania sprzÄ™towego" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "WyÅ›wietla listÄ™ plików na ESP" #. TRANSLATORS: command description msgid "Load device emulation data" msgstr "Wczytuje dane emulacji urzÄ…dzenia" #. TRANSLATORS: the plugin was created from a .so object, and was not built-in msgid "Loaded from an external module" msgstr "Wczytane z zewnÄ™trznego moduÅ‚u" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "Wczytywanie…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "Zablokowane" #. TRANSLATORS: the release urgency msgid "Low" msgstr "Niska" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refers #. * to the private/public key used to secure loading of firmware msgid "MEI Key Manifest" msgstr "Manifest klucza MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refer #. * to the private/public key used to secure loading of firmware msgid "MEI key manifest" msgstr "Manifest klucza MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "Tryb serwisowy MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "ObejÅ›cie MEI" msgid "MEI version" msgstr "Wersja MEI" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "RÄ™cznie włącza podane wtyczki" #. TRANSLATORS: Longest valid string for BIOS setting msgid "Maximum length" msgstr "Maksymalna dÅ‚ugość" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "Maksymalna wartość" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "Åšrednia" #. TRANSLATORS: ask the user to do a simple task which should be translated msgid "Message" msgstr "Komunikat" #. TRANSLATORS: ask the user a question, and it will not be translated msgid "Message (custom)" msgstr "Komunikat (niestandardowy)" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "Podpis metadanych" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "Adres URI metadanych" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "Metadane można uzyskać z serwisu Linux Vendor Firmware Service." #. TRANSLATORS: error message for a user who ran fwupdmgr #. * refresh recently -- %1 is '--force' #, c-format msgid "Metadata is up to date; use %s to refresh again." msgstr "Metadane sÄ… aktualne, opcja %s odÅ›wieży je ponownie." #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "Minimalna wersja" #. TRANSLATORS: Shortest valid string for BIOS setting msgid "Minimum length" msgstr "Minimalna dÅ‚ugość" #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "Minimalna wartość" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "Modyfikuje wartość konfiguracji usÅ‚ugi" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "Modyfikuje podane repozytorium" msgid "Modify a configured remote" msgstr "Modyfikacja skonfigurowanego repozytorium" msgid "Modify daemon configuration" msgstr "Modyfikacja konfiguracji usÅ‚ugi" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "Monitoruje zdarzenia usÅ‚ugi" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "Montuje MSP" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "Wymaga ponownego uruchomienia po instalacji" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "Wymaga ponownego uruchomienia" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "Wymaga wyłączenia komputera po instalacji" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "Nowa wersja" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "Nie podano dziaÅ‚ania." #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "Brak poprzednich wersji dla urzÄ…dzenia %s" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "Nie odnaleziono identyfikatorów oprogramowania sprzÄ™towego" #. TRANSLATORS: nothing found msgid "No firmware found" msgstr "Nie odnaleziono oprogramowania sprzÄ™towego" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Nie wykryto sprzÄ™tu z możliwoÅ›ciÄ… aktualizacji jego oprogramowania" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "Brak dostÄ™pnych wydaÅ„" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "Å»adne repozytoria nie sÄ… obecnie włączone, wiÄ™c żadne metadane nie sÄ… dostÄ™pne." #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "Brak dostÄ™pnych repozytoriów" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "Brak urzÄ…dzeÅ„, które można aktualizować" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "Brak dostÄ™pnych aktualizacji" msgid "No updates available for remaining devices" msgstr "Brak dostÄ™pnych aktualizacji dla pozostaÅ‚ych urzÄ…dzeÅ„" #. TRANSLATORS: error message #, c-format msgid "No volume matched %s" msgstr "Å»aden wolumin nie pasuje do %s" #. TRANSLATORS: version cannot be installed due to policy msgid "Not approved" msgstr "Niezatwierdzone" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "Nie odnaleziono" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "NieobsÅ‚ugiwane" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "OK" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "OK!" #. TRANSLATORS: the firmware old version msgid "Old version" msgstr "Poprzednia wersja" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "WyÅ›wietla tylko jednÄ… wartość PCR" #. TRANSLATORS: command line option msgid "Only use peer-to-peer networking when downloading files" msgstr "Używa tylko sieci P2P podczas pobierania plików" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "Dozwolone sÄ… tylko aktualizacje wersji" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "ZastÄ™puje domyÅ›lnÄ… Å›cieżkÄ™ ESP" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Firmware" msgstr "Oprogramowanie sprzÄ™towe P2P" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Metadata" msgstr "Metadane P2P" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "ÅšCIEÅ»KA" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "Przetwarza i wyÅ›wietla informacje o pliku oprogramowania sprzÄ™towego" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "Przetwarzanie aktualizacji bazy dbx…" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "Przetwarzanie systemowej bazy dbx…" #. TRANSLATORS: remote filename base msgid "Password" msgstr "HasÅ‚o" #. TRANSLATORS: command description msgid "Patch a firmware blob at a known offset" msgstr "Åata zamkniÄ™te oprogramowanie sprzÄ™towe pod znanym wyrównaniem" msgid "Payload" msgstr "Dane" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "OczekujÄ…ce" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "Wykonać dziaÅ‚anie?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "Debugowanie platformy" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "Debugowanie platformy" #. TRANSLATORS: 'recovery key' here refers to a code, rather than a physical #. metal thing msgid "Please ensure you have the volume recovery key before continuing." msgstr "Przed kontynuacjÄ… proszÄ™ siÄ™ upewnić, że posiadany jest klucz odzyskiwania woluminu." #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "ProszÄ™ podać liczbÄ™ od 0 do %u: " #. TRANSLATORS: the user isn't reading the question -- %1 is 'Y' and %2 is #. * 'N' #, c-format msgid "Please enter either %s or %s: " msgstr "ProszÄ™ podać %s lub %s:" #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "Brak zależnoÅ›ci wtyczki" #. TRANSLATORS: The plugin is only for testing msgid "Plugin is only for testing" msgstr "Wtyczka jest tylko do testowania" #. TRANSLATORS: Possible values for a bios setting msgid "Possible Values" msgstr "Możliwe wartoÅ›ci" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA Protection" msgstr "Ochrona DMA przed uruchomieniem" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "Ochrona DMA przed uruchomieniem" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is disabled" msgstr "Ochrona DMA przed uruchomieniem jest wyłączona" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is enabled" msgstr "Ochrona DMA przed uruchomieniem jest włączona" #. TRANSLATORS: warning message msgid "Press unlock on the device to continue the update process." msgstr "ProszÄ™ nacisnąć „Odblokuj†na urzÄ…dzeniu, aby kontynuować aktualizacjÄ™." #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "Poprzednia wersja" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Priorytet" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "Problemy" msgid "Proceed with upload?" msgstr "Kontynuować wysyÅ‚anie?" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Processor Security Checks" msgstr "Testy zabezpieczeÅ„ procesora" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "Processor rollback protection" msgstr "Ochrona procesora przed wycofaniem" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "WÅ‚asnoÅ›ciowe" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "IDENTYFIKATOR-REPOZYTORIUM" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID KEY VALUE" msgstr "IDENTYFIKATOR-REPOZYTORIUM KLUCZ WARTOŚĆ" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "Tylko do odczytu" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "Odczytuje zamkniÄ™te oprogramowanie sprzÄ™towe z urzÄ…dzenia" #. TRANSLATORS: command description msgid "Read a firmware from a device" msgstr "Odczytuje oprogramowanie sprzÄ™towe z urzÄ…dzenia" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "Odczytywanie z urzÄ…dzenia %s…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Odczytywanie…" #. TRANSLATORS: Plugin is active and in use msgid "Ready" msgstr "Gotowe" #. TRANSLATORS: how often we should refresh the metadata msgid "Refresh Interval" msgstr "Czas miÄ™dzy odÅ›wieżeniami" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "OdÅ›wieża metadane ze zdalnego serwera" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "Ponownie zainstalować urzÄ…dzenie %s do wersji %s?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "Ponownie instaluje obecne oprogramowanie sprzÄ™towe na urzÄ…dzeniu" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "Ponownie instaluje oprogramowanie sprzÄ™towe na urzÄ…dzeniu" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "Gałąź wydania" #. TRANSLATORS: release attributes msgid "Release Flags" msgstr "Flagi wydania" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "Identyfikator wydania" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "Identyfikator repozytorium" #. TRANSLATORS: command description msgid "Removes devices to watch for future emulation" msgstr "Usuwa urzÄ…dzenia do obserwowania do emulacji w przyszÅ‚oÅ›ci" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "Adres URI zgÅ‚oszenia" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "ZgÅ‚oszone do zdalnego serwera" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "Nie odnaleziono wymaganego systemu plików „efivarfsâ€" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "Nie odnaleziono wymaganego oprogramowania sprzÄ™towego" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "Wymaga programu startowego" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Wymaga połączenia z Internetem" msgid "Reset daemon configuration" msgstr "Przywrócenie konfiguracji usÅ‚ugi" #. TRANSLATORS: sets something in the daemon configuration file msgid "Resets a daemon configuration section" msgstr "Przywraca sekcjÄ™ konfiguracji usÅ‚ugi" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Uruchomić ponownie?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "Uruchomić usÅ‚ugÄ™ ponownie, aby uwzglÄ™dnić zmianÄ™?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Ponowne uruchamianie urzÄ…dzenia…" #. TRANSLATORS: command description msgid "Retrieve BIOS settings. If no arguments are passed all settings are returned" msgstr "Pobiera ustawienia BIOS-u. JeÅ›li żadne parametry nie zostanÄ… przekazane, zwracane sÄ… wszystkie ustawienia" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "Zwraca wszystkie identyfikatory sprzÄ™tu dla komputera" #. TRANSLATORS: ask the user to upload msgid "Review and upload report now?" msgstr "Przejrzeć i wysÅ‚ać zgÅ‚oszenie?" #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr get-upgrades` #, c-format msgid "Run `%s` for more information." msgstr "Polecenie „%s†wyÅ›wietli wiÄ™cej informacji." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr sync` #, c-format msgid "Run `%s` to complete this action." msgstr "Polecenie „%s†dokoÅ„czy to dziaÅ‚anie." #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "Wykonuje procedurÄ™ czyszczenia skÅ‚adania wtyczki podczas używania „install-blobâ€" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "Wykonuje procedurÄ™ przygotowania skÅ‚adania wtyczki podczas używania „install-blobâ€" #. TRANSLATORS: command description msgid "Run the post-reboot cleanup action" msgstr "Wykonuje dziaÅ‚anie czyszczenia po ponownym uruchomieniu" #. TRANSLATORS: tell a user how to get information #, c-format msgid "Run without '%s' to see" msgstr "Wykonanie bez „%s†wyÅ›wietli" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "DziaÅ‚ajÄ…ce jÄ…dro jest za stare" #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "Przyrostek uruchamiania" #. TRANSLATORS: Software Bill of Materials link msgid "SBOM" msgstr "SBOM" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SECTION" msgstr "SEKCJA" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING VALUE" msgstr "USTAWIENIE WARTOŚĆ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING1 VALUE1 [SETTING2] [VALUE2]" msgstr "USTAWIENIE1 WARTOŚĆ1 [USTAWIENIE2] [WARTOŚĆ2]" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "SMAP" msgstr "SMAP" #. TRANSLATORS: Title: Whether firmware is locked down msgid "SMM locked down" msgstr "SMM jest zablokowane" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "Deskryptor BIOS-u SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "Region BIOS-u SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "Blokada SPI" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "SPI replay protection" msgstr "Ochrona przed odtwarzaniem SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "Zapis SPI" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "SPI write protection" msgstr "Ochrona przed zapisem SPI" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "PODSYSTEM STEROWNIK [IDENTYFIKATOR-URZÄ„DZENIA|GUID]" #. TRANSLATORS: command description msgid "Save a file that allows generation of hardware IDs" msgstr "Zapisuje plik umożliwiajÄ…cy utworzenie identyfikatorów sprzÄ™tu" #. TRANSLATORS: command description msgid "Save device emulation data" msgstr "Zapisuje dane emulacji urzÄ…dzenia" #. TRANSLATORS: key for a offline report filename msgid "Saved report" msgstr "Zapisane zgÅ‚oszenie" #. TRANSLATORS: Scalar increment for integer BIOS setting msgid "Scalar Increment" msgstr "Przyrost skalarny" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Planowanie…" #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "Wyłączono Secure Boot" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "Włączono Secure Boot" msgid "Security hardening for HSI" msgstr "Wzmacnianie zabezpieczeÅ„ dla HSI" #. TRANSLATORS: the %1 is a URL to a wiki page #, c-format msgid "See %s for more details." msgstr "%s zawiera wiÄ™cej informacji." #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "%s zawiera wiÄ™cej informacji." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Wybrane urzÄ…dzenie" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Wybrany wolumin" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "Numer seryjny" #. TRANSLATORS: Configured a BIOS setting to a value #, c-format msgid "Set BIOS setting '%s' using '%s'." msgstr "Ustawienie ustawienia BIOS-u „%s†na „%sâ€." #. TRANSLATORS: command description msgid "Set a BIOS setting" msgstr "Ustawia ustawienie BIOS-u" msgid "Set one or more BIOS settings" msgstr "Ustawienie co najmniej jednego ustawienia BIOS-u" #. TRANSLATORS: command description msgid "Set or remove an EFI boot hive entry" msgstr "Ustawia lub usuwa wpis „hive†uruchamiania EFI" #. TRANSLATORS: command description msgid "Set the EFI boot next" msgstr "Ustawia uruchamianie EFI jako nastÄ™pne" #. TRANSLATORS: command description msgid "Set the EFI boot order" msgstr "Ustawia kolejność uruchamiania EFI" #. TRANSLATORS: command line option msgid "Set the download retries for transient errors" msgstr "Ustawia liczbÄ™ prób pobrania w przypadku przejÅ›ciowych błędów" #. TRANSLATORS: command description msgid "Sets one or more BIOS settings" msgstr "Ustawia co najmniej jedno ustawienie BIOS-u" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Ustawienie listy zatwierdzonego oprogramowania sprzÄ™towego" #. TRANSLATORS: type of BIOS setting msgid "Setting type" msgstr "Typ ustawienia" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "Ustawienia zostanÄ… zastosowane po ponownym uruchomieniu komputera" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "UdostÄ™pnia historiÄ™ oprogramowania sprzÄ™towego programistom" #. TRANSLATORS: command line option msgid "Show all results" msgstr "WyÅ›wietla wszystkie wyniki" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "WyÅ›wietla wersje klienta i usÅ‚ugi" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "WyÅ›wietla wiÄ™cej informacji o usÅ‚udze dla konkretnej domeny" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "WyÅ›wietla informacje o debugowaniu dla wszystkich domen" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "WyÅ›wietla opcje debugowania" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "WyÅ›wietla urzÄ…dzenia, których nie można aktualizować" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "WyÅ›wietla dodatkowe informacje o debugowaniu" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "WyÅ›wietla historiÄ™ aktualizacji oprogramowania sprzÄ™towego" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "WyÅ›wietla obliczonÄ… wersjÄ™ bazy dbx" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "Wyłączyć teraz?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "Podpisuje oprogramowanie sprzÄ™towe nowym kluczem" msgid "Sign data using the client certificate" msgstr "Podpisanie danych za pomocÄ… certyfikatu klienta" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "Podpisanie danych za pomocÄ… certyfikatu klienta" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "Podpisuje wysÅ‚ane dane za pomocÄ… certyfikatu klienta" msgid "Signature" msgstr "Podpis" #. TRANSLATORS: firmware is verified on-device the payload using strong crypto msgid "Signed Payload" msgstr "Podpisane dane" #. TRANSLATORS: file size of the download msgid "Size" msgstr "Rozmiar" #. TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM msgid "Some of the platform secrets may be invalidated when updating this firmware." msgstr "Część haseÅ‚ platformy może zostać unieważnionych podczas aktualizowania tego oprogramowania sprzÄ™towego." #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "ŹródÅ‚o" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "Podaje plik bazy dbx" msgid "Stop the fwupd service" msgstr "Zatrzymanie usÅ‚ugi fwupd" #. TRANSLATORS: The BIOS setting accepts strings msgid "String" msgstr "CiÄ…g" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "Powodzenie" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "PomyÅ›lnie aktywowano wszystkie urzÄ…dzenia" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "PomyÅ›lnie wyłączono repozytorium" #. TRANSLATORS: comment explaining result of command msgid "Successfully disabled test devices" msgstr "PomyÅ›lnie wyłączono urzÄ…dzenia testowe" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "PomyÅ›lnie pobrano nowe metadane: " #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "PomyÅ›lnie włączono i odÅ›wieżono repozytorium" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "PomyÅ›lnie włączono repozytorium" #. TRANSLATORS: comment explaining result of command msgid "Successfully enabled test devices" msgstr "PomyÅ›lnie włączono urzÄ…dzenia testowe" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "PomyÅ›lnie zainstalowano oprogramowanie sprzÄ™towe" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "PomyÅ›lnie zmodyfikowano wartość konfiguracji" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "PomyÅ›lnie zmodyfikowano repozytorium" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "PomyÅ›lnie rÄ™cznie odÅ›wieżono metadane" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration section" msgstr "PomyÅ›lnie przywrócono sekcjÄ™ konfiguracji" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration values" msgstr "PomyÅ›lnie przywrócono wartoÅ›ci konfiguracji" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "PomyÅ›lnie zaktualizowano sumy kontrolne urzÄ…dzenia" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "PomyÅ›lnie wysÅ‚ano %u zgÅ‚oszenie" msgstr[1] "PomyÅ›lnie wysÅ‚ano %u zgÅ‚oszenia" msgstr[2] "PomyÅ›lnie wysÅ‚ano %u zgÅ‚oszeÅ„" msgstr[3] "PomyÅ›lnie wysÅ‚ano %u zgÅ‚oszeÅ„" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "PomyÅ›lnie sprawdzono poprawność sum kontrolnych urzÄ…dzenia" #. TRANSLATORS: the device showed up in time #, c-format msgid "Successfully waited %.0fms for device" msgstr "PomyÅ›lnie odczekano %.0f ms na urzÄ…dzenie" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Podsumowanie" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "Supervisor Mode Access Prevention" msgstr "Zapobieganie dostÄ™pu do trybu nadzorcy" #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "ObsÅ‚ugiwane" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "ObsÅ‚ugiwany procesor" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "ObsÅ‚ugiwane na zdalnym serwerze" #. TRANSLATORS: Title: a better sleep state msgid "Suspend To Idle" msgstr "UÅ›pienie do trybu bezczynnoÅ›ci" #. TRANSLATORS: Title: sleep state msgid "Suspend To RAM" msgstr "UÅ›pienie do pamiÄ™ci RAM" #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "UÅ›pienie do trybu bezczynnoÅ›ci" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "UÅ›pienie do pamiÄ™ci RAM" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "Przełączyć gałąź z %s na %s?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "Przełącza gałąź oprogramowania sprzÄ™towego na urzÄ…dzeniu" #. TRANSLATORS: command description msgid "Sync firmware versions to the chosen configuration" msgstr "Synchronizuje wersje oprogramowania sprzÄ™towego do wybranej konfiguracji" #. TRANSLATORS: Title: Whether firmware is locked down msgid "System Management Mode" msgstr "Tryb zarzÄ…dzania komputerem" #. TRANSLATORS: this CLI tool is now preventing system updates msgid "System Update Inhibited" msgstr "Aktualizacja systemu jest wstrzymana" #. TRANSLATORS: as in laptop battery power msgid "System power is too low" msgstr "Poziom naÅ‚adowania komputera jest za niski" #. TRANSLATORS: as in laptop battery power #, c-format msgid "System power is too low (%u%%, requires %u%%)" msgstr "Poziom naÅ‚adowania komputera jest za niski (%u%%, wymaga %u%%)" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "Komputer wymaga zewnÄ™trznego źródÅ‚a zasilania" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "TEKST" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "Rekonstrukcja PCR0 TPM" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is invalid" msgstr "Rekonstrukcja PCR0 TPM jest nieprawidÅ‚owa" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is now valid" msgstr "Rekonstrukcja PCR0 TPM jest teraz prawidÅ‚owa" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM Platform Configuration" msgstr "Konfiguracja platformy TPM" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM Reconstruction" msgstr "Rekonstrukcja TPM" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM empty PCRs" msgstr "Puste PCR TPM" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "Etykieta" msgstr[1] "Etykiety" msgstr[2] "Etykiety" msgstr[3] "Etykiety" #. TRANSLATORS: we're saving all USB events for emulation msgid "Tagged for emulation" msgstr "Oznaczone do emulacji" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "Skażone" #. show the user the entire data blob msgid "Target" msgstr "Cel" #. TRANSLATORS: command description msgid "Test a device using a JSON manifest" msgstr "Testuje urzÄ…dzenie za pomocÄ… manifestu JSON" #. TRANSLATORS: when the release was tested msgid "Tested" msgstr "Przetestowane" #. TRANSLATORS: the %s is a vendor name, e.g. Lenovo #, c-format msgid "Tested by %s" msgstr "Przetestowane przez: %s" #. TRANSLATORS: someone we trust has tested this msgid "Tested by trusted vendor" msgstr "Przetestowane przez zaufanego dostawcÄ™" #. TRANSLATORS: the boot entry was in a legacy format msgid "The EFI boot entry is not in hive format, and shim may not be new enough to read it." msgstr "Wpis uruchamiania EFI nie jest w formacie „hiveâ€, a program shim może nie być wystarczajÄ…co nowy, aby go odczytać." #. TRANSLATORS: try to treat the legacy format as a hive msgid "The EFI boot entry was not in hive format, falling back" msgstr "Wpis uruchamiania EFI nie jest w formacie „hiveâ€, używanie zapasowego" #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "LVFS to wolny serwis dziaÅ‚ajÄ…cy jako niezależny podmiot prawny niemajÄ…cy zwiÄ…zków z systemem $OS_RELEASE:NAME$. Dystrybutor używanego systemu mógÅ‚ nie zweryfikować żadnych aktualizacji oprogramowania sprzÄ™towego pod kÄ…tem zgodnoÅ›ci z używanym komputerem lub podłączonymi urzÄ…dzeniami. Każde oprogramowanie sprzÄ™towe jest dostarczane wyłącznie przez oryginalnego producenta sprzÄ™tu." #. TRANSLATORS: this is more background on a security measurement problem msgid "The TPM PCR0 differs from reconstruction." msgstr "PCR0 TPM różni siÄ™ od rekonstrukcji." #. TRANSLATORS: HSI event title msgid "The UEFI certificate store is now up to date" msgstr "Przechowalnia certyfikatów UEFI jest teraz aktualna" #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "UsÅ‚uga wczytaÅ‚a kod firmy trzeciej i nie jest już wspierana przez jej programistów!" #. TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name #, c-format msgid "The firmware from %s is not supplied by %s, the hardware vendor." msgstr "Oprogramowanie sprzÄ™towe od firmy %s nie jest dostarczane przez firmÄ™ %s, dostawcÄ™ sprzÄ™tu." #. TRANSLATORS: try to help msgid "The system clock has not been set correctly and downloading files may fail." msgstr "Zegar komputera nie jest poprawnie ustawiony i pobieranie plików może siÄ™ nie powieść." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been re-inserted." msgstr "Aktualizacja zostanie kontynuowana po ponownym podłączeniu kabla USB urzÄ…dzenia." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged and then re-inserted." msgstr "Aktualizacja zostanie kontynuowana po odłączeniu i ponownym podłączeniu kabla USB urzÄ…dzenia." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged." msgstr "Aktualizacja zostanie kontynuowana po odłączeniu kabla USB urzÄ…dzenia." #. TRANSLATORS: warning message msgid "The update will continue when the device power cable has been removed and re-inserted." msgstr "Aktualizacja zostanie kontynuowana po odłączeniu i ponownym podłączeniu kabla zasilajÄ…cego urzÄ…dzenia." #. TRANSLATORS: naughty vendor msgid "The vendor did not supply any release notes." msgstr "Dostawca nie dostarczyÅ‚ żadnych informacji o wydaniu." #. TRANSLATORS: now list devices with unfixed high-priority issues msgid "There are devices with issues:" msgstr "UrzÄ…dzenia z problemami:" #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "Nie ma zablokowanych plików oprogramowania sprzÄ™towego" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "Nie ma zatwierdzonego oprogramowania sprzÄ™towego." #. TRANSLATORS: %1 is the current device version number, and %2 is the #. command name, e.g. `fwupdmgr sync` #, c-format msgid "This device will be reverted back to %s when the %s command is performed." msgstr "To urzÄ…dzenie zostanie przywrócone do wersji %s po wykonaniu polecenia %s." #. TRANSLATORS: the vendor did not upload this msgid "This firmware is provided by LVFS community members and is not provided (or supported) by the original hardware vendor." msgstr "To oprogramowanie sprzÄ™towe jest dostarczane przez czÅ‚onków spoÅ‚ecznoÅ›ci LVFS i nie jest dostarczane (ani wspierane) przez oryginalnego dostawcÄ™ sprzÄ™tu." #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "Ten pakiet nie zostaÅ‚ zweryfikowany, może nie dziaÅ‚ać poprawnie." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "Ten program może dziaÅ‚ać poprawnie tylko jako root" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "To repozytorium zawiera oprogramowanie sprzÄ™towe nieobjÄ™te embargo, ale nadal testowane przez dostawcÄ™ sprzÄ™tu. Należy upewnić siÄ™, że istnieje możliwość rÄ™cznego zainstalowania poprzedniej wersji oprogramowania sprzÄ™towego w razie niepowodzenia aktualizacji." #. TRANSLATORS: error message msgid "This system doesn't support firmware settings" msgstr "Ten komputer nie obsÅ‚uguje ustawieÅ„ oprogramowania sprzÄ™towego" #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "Ten komputer ma problemy HSI uruchamiania." #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "Ten komputer ma niski poziom zabezpieczeÅ„ HSI." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "To narzÄ™dzie umożliwia administratorowi zastosowywanie aktualizacji bazy dbx dla UEFI." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "To narzÄ™dzie umożliwia administratorowi odpytywanie i sterowanie usÅ‚ugÄ… fwupd, umożliwiajÄ…c wykonywanie takich dziaÅ‚aÅ„, jak instalowanie lub instalowanie poprzedniej wersji oprogramowania sprzÄ™towego." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "To narzÄ™dzie umożliwia administratorowi używanie wtyczek fwupd bez ich instalacji na komputerze." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can add a kernel argument of '%s', but it will only be active after restarting the computer." msgstr "To narzÄ™dzie może dodać parametr jÄ…dra „%sâ€, ale zmiana bÄ™dzie aktywna dopiero po ponownym uruchomieniu komputera." #. TRANSLATORS: the %1 is a BIOS setting name. #. * %2 and %3 are the values, e.g. "True" or "Windows10" #, c-format msgid "This tool can change the BIOS setting '%s' from '%s' to '%s' automatically, but it will only be active after restarting the computer." msgstr "To narzÄ™dzie może automatycznie zmienić ustawienie BIOS-u „%s†z „%s†na „%sâ€, ale zmiana bÄ™dzie aktywna dopiero po ponownym uruchomieniu komputera." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can change the kernel argument from '%s' to '%s', but it will only be active after restarting the computer." msgstr "To narzÄ™dzie może zmienić parametr jÄ…dra z „%s†na „%sâ€, ale zmiana bÄ™dzie aktywna dopiero po ponownym uruchomieniu komputera." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "To narzÄ™dzie odczyta i przetworzy dziennik zdarzeÅ„ TPM z oprogramowania sprzÄ™towego komputera." #. TRANSLATORS: the update state of the specific device msgid "Transient failure" msgstr "PrzejÅ›ciowe niepowodzenie" #. TRANSLATORS: item is TRUE msgid "True" msgstr "Prawda" #. TRANSLATORS: We verified the metadata against the server msgid "Trusted metadata" msgstr "Zweryfikowane metadane" #. TRANSLATORS: We verified the payload against the server msgid "Trusted payload" msgstr "Zweryfikowane dane" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Typ" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI Bootservice Variables" msgstr "Zmienne usÅ‚ugi uruchamiania UEFI" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition may not be set up correctly" msgstr "Partycja ESP UEFI może nie być poprawnie skonfigurowana" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "Nie wykryto lub nie skonfigurowano partycji ESP UEFI" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI Memory Protection" msgstr "Ochrona pamiÄ™ci UEFI" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "Klucz platformy UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "Zabezpieczone uruchamianie UEFI" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI bootservice variables" msgstr "Zmienne usÅ‚ugi uruchamiania UEFI" #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "Aktualizacje kapsuÅ‚owe UEFI sÄ… niedostÄ™pne lub wyłączone w konfiguracji oprogramowania sprzÄ™towego" #. TRANSLATORS: Title: is UEFI db up-to-date msgid "UEFI db" msgstr "Baza danych UEFI" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "NarzÄ™dzie bazy dbx dla UEFI" #. TRANSLATORS: system is not booted in UEFI mode msgid "UEFI firmware can not be updated in legacy BIOS mode" msgstr "Nie można aktualizować oprogramowania sprzÄ™towego UEFI w trybie przestarzaÅ‚ego BIOS-u" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI memory protection" msgstr "Ochrona pamiÄ™ci UEFI" #. TRANSLATORS: HSI event title msgid "UEFI memory protection enabled and locked" msgstr "Ochrona pamiÄ™ci UEFI jest włączona i zablokowana" #. TRANSLATORS: HSI event title msgid "UEFI memory protection enabled but not locked" msgstr "Ochrona pamiÄ™ci UEFI jest włączona, ale nie jest zablokowana" #. TRANSLATORS: HSI event title msgid "UEFI memory protection is now locked" msgstr "Ochrona pamiÄ™ci UEFI jest teraz zablokowana" #. TRANSLATORS: HSI event title msgid "UEFI memory protection is now unlocked" msgstr "Ochrona pamiÄ™ci UEFI jest teraz odblokowana" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "Klucz platformy UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "Zabezpieczone uruchamianie UEFI" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "Nie można połączyć siÄ™ z usÅ‚ugÄ…" #. TRANSLATORS: error message msgid "Unable to find attribute" msgstr "Nie można odnaleźć atrybutu" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "OdwiÄ…zuje bieżący sterownik" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "Odblokowywanie oprogramowania sprzÄ™towego:" #. TRANSLATORS: command description msgid "Unblocks a specific firmware from being installed" msgstr "Odblokowuje możliwość instalacji podanego oprogramowania sprzÄ™towego" #. TRANSLATORS: command description msgid "Undo the host security attribute fix" msgstr "Wycofuje poprawkÄ™ atrybutu zabezpieczeÅ„ komputera" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "Niezaszyfrowane" #. TRANSLATORS: command description msgid "Uninhibit the system to allow upgrades" msgstr "KoÅ„czy wstrzymanie systemu, aby zezwolić na aktualizacje" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Nieznane" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Nieznane urzÄ…dzenie" msgid "Unlock the device to allow access" msgstr "Odblokowanie urzÄ…dzenia" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "Odblokowane" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "Odblokowuje urzÄ…dzenie" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "Odmontowuje ESP" #. TRANSLATORS: message shown after device has been marked for emulation msgid "Unplug and replug the device to continue the update process." msgstr "ProszÄ™ odłączyć i ponownie podłączyć urzÄ…dzenie, aby kontynuować aktualizacjÄ™." #. TRANSLATORS: firmware payload is unsigned and it is possible to modify it msgid "Unsigned Payload" msgstr "Niepodpisane dane" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "NieobsÅ‚ugiwana wersja usÅ‚ugi %s, wersja klienta to %s" #. TRANSLATORS: Suffix: the HSI result msgid "Untainted" msgstr "Nieskażone" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "Można aktualizować" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Błąd aktualizacji" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "Ilustracja aktualizacji" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "Komunikat aktualizacji" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "Stan aktualizacji" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "Niepowodzenie aktualizacji to znany problem, pod tym adresem dostÄ™pnych jest wiÄ™cej informacji:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Zaktualizować teraz?" #. TRANSLATORS: command description msgid "Update the stored cryptographic hash with current ROM contents" msgstr "Aktualizuje przechowywanÄ… kryptograficznÄ… sumÄ™ kontrolnÄ… bieżącÄ… zawartoÅ›ciÄ… pamiÄ™ci ROM" msgid "Update the stored device verification information" msgstr "Aktualizacja przechowywanych informacji o poprawnoÅ›ci urzÄ…dzenia" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "Aktualizuje przechowywane metadane obecnÄ… zawartoÅ›ciÄ…" #. TRANSLATORS: command description msgid "Updates all specified devices to latest firmware version, or all devices if unspecified" msgstr "Aktualizuje wszystkie podane urzÄ…dzenia do najnowszej wersji oprogramowania sprzÄ™towego, lub wszystkie urzÄ…dzenia, jeÅ›li nie podano żadnych" #. TRANSLATORS: how many local devices can expect updates now #, c-format msgid "Updates have been published for %u local device" msgid_plural "Updates have been published for %u of %u local devices" msgstr[0] "Aktualizacje zostaÅ‚y opublikowane dla %u lokalnego urzÄ…dzenia" msgstr[1] "Aktualizacje zostaÅ‚y opublikowane dla %u z %u lokalnych urzÄ…dzeÅ„" msgstr[2] "Aktualizacje zostaÅ‚y opublikowane dla %u z %u lokalnych urzÄ…dzeÅ„" msgstr[3] "Aktualizacje zostaÅ‚y opublikowane dla %u z %u lokalnych urzÄ…dzeÅ„" msgid "Updating" msgstr "Aktualizowanie" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "Aktualizowanie urzÄ…dzenia %s…" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "Zaktualizować urzÄ…dzenie %s z wersji %s na %s?" #. TRANSLATORS: ask the user to upload msgid "Upload data now?" msgstr "WysÅ‚ać dane?" #. TRANSLATORS: command description msgid "Upload the list of updatable devices to a remote server" msgstr "WysyÅ‚a listÄ™ urzÄ…dzeÅ„, które można aktualizować do zdalnego serwera" #. TRANSLATORS: ask the user to share, %s is something #. * like: "Linux Vendor Firmware Service" #, c-format msgid "Upload these anonymous results to the %s to help other users?" msgstr "WysÅ‚ać te anonimowe wyniki do serwisu %s, aby pomóc innym użytkownikom?" #. TRANSLATORS: explain why we want to upload #, c-format msgid "Uploading a device list allows the %s team to know what hardware exists, and allows us to put pressure on vendors that do not upload firmware updates for their hardware." msgstr "WysÅ‚anie listy urzÄ…dzeÅ„ informuje zespół %s o istniejÄ…cych urzÄ…dzeniach, co pomaga nam wywierać presjÄ™ na dostawców, którzy nie publikujÄ… aktualizacji oprogramowania dla swojego sprzÄ™tu." #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "WysyÅ‚anie zgÅ‚oszeÅ„ o oprogramowaniu sprzÄ™towym pomaga dostawcom sprzÄ™tu szybko identyfikować nieudane i udane aktualizacje na prawdziwych urzÄ…dzeniach." #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "Pilność" #. TRANSLATORS: explain how to get help, %1 is #. * 'fwupdtool --help' #. TRANSLATORS: explain how to get help, #. * where $1 is something like 'fwupdmgr --help' #, c-format msgid "Use %s for help" msgstr "Polecenie %s wyÅ›wietli pomoc" #. TRANSLATORS: CTRL^C [holding control, and then pressing C] will exit the #. program msgid "Use CTRL^C to cancel." msgstr "NaciÅ›niÄ™cie CTRL^C anuluje." #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "Użytkownik zostaÅ‚ powiadomiony" #. TRANSLATORS: remote filename base msgid "Username" msgstr "Nazwa użytkownika" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "VERSION1 VERSION2 [FORMAT]" msgstr "WERSJA1 WERSJA2 [FORMAT]" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "PrawidÅ‚owe" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "Sprawdzanie poprawnoÅ›ci zawartoÅ›ci ESP…" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "Wariant" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "Dostawca" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "Sprawdzanie poprawnoÅ›ci…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Wersja" #. TRANSLATORS: the fwupd version the release was tested on msgid "Version[fwupd]" msgstr "Wersja[fwupd]" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "OSTRZEÅ»ENIE" #. TRANSLATORS: command description msgid "Wait for a device to appear" msgstr "Odczekuje na pojawienie siÄ™ urzÄ…dzenia" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "Oczekiwanie…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "Obserwuje zmiany sprzÄ™tu" #. TRANSLATORS: check various UEFI and ACPI tables are unchanged after the #. update msgid "Will measure elements of system integrity around an update" msgstr "Dokona pomiaru elementów spójnoÅ›ci komputera wokół aktualizacji" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "Zapisywanie pliku:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Zapisywanie…" #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from a recovery or installation disk, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "Należy siÄ™ upewnić, że użytkownik jest w stanie przywrócić ustawienia z dysku przywracania lub instalacji, ponieważ ta zmiana może spowodować, że komputer nie bÄ™dzie mógÅ‚ uruchomić systemu Linux lub spowodować inny rodzaj niestabilnoÅ›ci komputera." #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from the system firmware setup, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "Należy siÄ™ upewnić, że użytkownik jest w stanie przywrócić ustawienia w konfiguracji oprogramowania sprzÄ™towego komputera, ponieważ ta zmiana może spowodować, że komputer nie bÄ™dzie mógÅ‚ uruchomić systemu Linux lub spowodować inny rodzaj niestabilnoÅ›ci komputera." #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "Używana dystrybucja mogÅ‚a nie sprawdzić zgodnoÅ›ci aktualizacji oprogramowania sprzÄ™towego z komputerem i podłączonymi urzÄ…dzeniami." #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "SprzÄ™t może zostać uszkodzony przez użycie tego oprogramowania sprzÄ™towego, a zainstalowanie tego wydania może spowodować utratÄ™ gwarancji od firmy %s." #. TRANSLATORS: BKC is the industry name for the best known configuration and #. is a set #. * of firmware that works together #, c-format msgid "Your system is set up to the BKC of %s." msgstr "Komputer jest skonfigurowany wedÅ‚ug BKC %s." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[APPSTREAM_ID]" msgstr "[IDENTYFIKATOR_APPSTREAM]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[SUMA-KONTROLNA]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[IDENTYFIKATOR-URZÄ„DZENIA|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[IDENTYFIKATOR-URZÄ„DZENIA|GUID] [GAÅĄŹ]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [VERSION]" msgstr "[IDENTYFIKATOR-URZÄ„DZENIA|GUID] [WERSJA]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE]" msgstr "[URZÄ„DZENIE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILE FILE_SIG REMOTE-ID]" msgstr "[PLIK PODPIS_PLIKU IDENTYFIKATOR-REPOZYTORIUM]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[NAZWA-PLIKU1] [NAZWA-PLIKU2]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FWUPD-VERSION]" msgstr "[WERSJA-FWUPD]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[REASON] [TIMEOUT]" msgstr "[POWÓD] [CZAS-OCZEKIWANIA]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SECTION] KEY VALUE" msgstr "[SEKCJA] KLUCZ WARTOŚĆ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2] [--no-authenticate]" msgstr "[USTAWIENIE1] [USTAWIENIE2] [--no-authenticate]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2]..." msgstr "[USTAWIENIE1] [USTAWIENIE2]…" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[PLIK-SMBIOS|PLIK-HWIDS]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "domyÅ›lna" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "NarzÄ™dzie dziennika zdarzeÅ„ TPM usÅ‚ugi fwupd" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "Wtyczki usÅ‚ugi fwupd" fwupd-2.0.10/po/pt.po000066400000000000000000002050511501337203100143160ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Hugo Carvalho , 2021 # Luis Filipe Teixeira , 2023-2024 # Peter J. Mello , 2020 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Portuguese (http://app.transifex.com/freedesktop/fwupd/language/pt/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: pt\n" "Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "%.0f minuto restante" msgstr[1] "%.0f minutos restantes" msgstr[2] "%.0f minutos restantes" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "Atualização da bateria para %s" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "Atualização de microcódigo de CPU para %s" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "Atualização de câmera para %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "Atualização da configuração de %s" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "Atualização consumidor-final de ME para %s" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "Atualização de controlador para %s" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "Atualização corporativa de ME para %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "Atualização do dispositivo %s" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "Atualização do controlador embarcado %s" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "Atualização do teclado para %s" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "Atualização de ME para %s" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "Atualização do rato para %s" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "Atualização da interface de rede %s" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "Atualização do controlador de armazenamento %s" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "Atualização do sistema %s" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "Atualização de TPM para %s" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "Atualização de controlador Thunderbolt para %s" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "Atualização do touchpad para %s" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "Atualização de %s" #. TRANSLATORS: warn the user before updating, %1 is a device name #, c-format msgid "%s and all connected devices may not be usable while updating." msgstr "%s e todos os dispositivos conectados não podem ser usados durante a atualização." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "Modo de fabricação de %s" #. TRANSLATORS: warn the user before #. * updating, %1 is a device name #, c-format msgid "%s must remain connected for the duration of the update to avoid damage." msgstr "%s deve permanecer conectado durante a atualização para evitar danos." #. TRANSLATORS: warn the user before updating, %1 is a machine #. * name #, c-format msgid "%s must remain plugged into a power source for the duration of the update to avoid damage." msgstr "%s deve permanecer conectado a uma fonte de energia durante a atualização para evitar danos." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "substituição de %s" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "Versão %s" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u dia" msgstr[1] "%u dias" msgstr[2] "%u dias" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] " %u dispositivo tem uma atualização de firmware disponível." msgstr[1] "%u dispositivos têm uma atualização de firmware disponível." msgstr[2] "%u dispositivos têm uma atualização de firmware disponível." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u hora" msgstr[1] "%u horas" msgstr[2] "%u horas" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minuto" msgstr[1] "%u minutos" msgstr[2] "%u minutos" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u segundo" msgstr[1] "%u segundos" msgstr[2] "%u segundos" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(obsoleto)" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "Ação necessária:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "Ativa dispositivos" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "Ativa dispositivos pendentes" msgid "Activate the new firmware on the device" msgstr "Ativar o novo firmware no dispositivo" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "Ativando atualização de firmware" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "Ativando atualização de firmware para" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Idade" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "Aceitar e ativar o remoto?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Atalho para %s" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "Todos os dispositivos do mesmo tipo serão atualizados ao mesmo tempo" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Permite fazer downgrade de versões de firmware" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "Permite reinstalar versões de firmware existentes" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "Permite alternar a ramificação do firmware" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "Uma atualização requer uma reinicialização para ser concluída." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "Uma atualização requer que o sistema seja desligado por completo." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Responde sim para todas as perguntas" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Aplica a atualização mesmo quando não recomendado" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Aplica ficheiros de atualização" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "Aplicando atualização…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Firmware aprovado:" msgstr[1] "Firmwares aprovados:" msgstr[2] "Firmwares aprovados:" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "Modo anexar ao firmware" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "A autenticar…" #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "Os detalhes de autenticação são necessários" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Autenticação é necessária para fazer downgrade da versão do firmware em um dispositivo removível" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Autenticação é necessária para fazer downgrade da versão do firmware nesta máquina" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to fix a host security issue" msgstr "A autenticação é necessária para corrigir um problema de segurança do anfitrião" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "A autenticação é necessária para modificar as definições do BIOS" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "Autenticação é necessária para modificar um remoto configurado usado para atualizações de firmware" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "Autenticação é necessária para modificar uma configuração de daemon" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "A autenticação é necessária para ler as definições do BIOS" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "Autenticação é necessária para definir a lista de firmwares aprovados" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "Autenticação é necessária para assinar dados usando o certificado do cliente" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "Autenticação é necessária para alternar para nova versão de firmware" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to undo the fix for a host security issue" msgstr "A autenticação é necessária para anular a correção de um problema de segurança do anfitrião" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Autenticação é necessária para desbloquear um dispositivo" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Autenticação é necessária para atualizar o firmware em dispositivo removível" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Autenticação é necessária para atualizar o firmware nesta máquina" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "Autenticação é necessária para atualizar as somas de verificação armazenadas para o dispositivo" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "Relatórios automáticos" #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "Enviar automaticamente toda vez?" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS firmware updates" msgstr "Actualizações de firmware do BIOS" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "Atualizações do BIOS fornecidas via LVFS ou Windows Update" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "BUILDER-XML FILENAME-DST" msgstr "XML-CONSTRUTOR NOME-DE-FICHEIRO-DEST" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "Vincula novo driver de kernel" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "Arquivos de firmware bloqueados:" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "Bloqueando firmware:" #. TRANSLATORS: command description msgid "Blocks a specific firmware from being installed" msgstr "Impede que um firmware específico seja instalado" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "Versão do gestor de arranque" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "Ramificação" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "Compila um ficheiro de firmware" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Cancelar" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "Cancelado" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "Não foi possível aplicar, pois a atualização de dbx já foi aplicada." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Alterado" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "Verifica se o hash criptográfico corresponde ao firmware" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Soma de verificação" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "Limpa os resultados da última atualização" #. TRANSLATORS: error message msgid "Command not found" msgstr "Comando não encontrado" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "Converte um ficheiro de firmware" #. TRANSLATORS: when the update was built msgid "Created" msgstr "Criado" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "Crítica" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "A verificação criptográfica de hash está disponível" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "Versão atual" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "ID-DISPOSITIVO|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Opções de depuração" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "A descomprimir…" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Descrição" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "Modo desanexar ao gestor de arranque" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "Detalhes" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "Opções do dispositivo" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "ID do dispositivo" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Dispositivo adicionado:" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "O dispositivo pode recuperar falhas de flashing" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Dispositivo modificado:" #. TRANSLATORS: a version check is required for all firmware msgid "Device firmware is required to have a version check" msgstr "Um firmware de dispositivo é necessário para ter uma verificação de versão" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "O dispositivo está bloqueado" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "Um dispositivo é necessário para instalar todos os lançamentos fornecidos" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "O dispositivo está inalcançável" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "O dispositivo pode ser usado durante a atualização" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Dispositivo removido:" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "Atualizações de estágios do dispositivo" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "O dispositivo tem suporte para alternar para uma ramificação diferente do firmware" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "A atualização do dispositivo precisa de ativação" #. TRANSLATORS: save the old firmware to disk before installing the new one msgid "Device will backup firmware before installing" msgstr "O dispositivo fará backup do firmware antes de instalar" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "O dispositivo não aparecerá novamente após a atualização ser concluída" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "Dispositivos que foram atualizados com sucesso:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "Dispositivos que não foram atualizados com sucesso:" #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "Dispositivos com nenhuma atualização de firmware disponível: " #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "Dispositivos com a versão mais recente do firmware disponível:" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "Desabilitado" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "Desabilita um remoto dado" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "Não verifica por metadados antigos" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "Não verifica por histórico não relatado" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "Não verifica se remotos para transferência devem estar ativados" #. TRANSLATORS: command line option msgid "Do not check or prompt for reboot after update" msgstr "Não verifica nem solicita configuração para reiniciar após atualizar" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "Não inclui prefixo de domínio de log" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "Não inclui prefixo com marca de tempo" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "Não realiza verificações de segurança de dispositivo" #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "Não escreve no banco de dados de histórico" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "Entende as consequências de trocar a ramificação do firmware?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "Deseja desativar este recurso para atualizações futuras?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "Deseja atualizar este remoto agora?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "Deseja enviar relatórios automaticamente para atualizações futuras?" #. TRANSLATORS: success msgid "Done!" msgstr "Feito!" #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "Fazer downgrade %s de %s para %s?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "Faz downgrade da versão do firmware em um dispositivo" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "A reverter %s…" #. TRANSLATORS: command description msgid "Download a file" msgstr "Transferir um ficheiro" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "A transferir…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "Despeja dados SMBIOS a partir de um ficheiro" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "Duração" msgid "Enable" msgstr "Ativar" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "Ativar novo remoto?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "Ativar esse remoto?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Ativado" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "Ativa um remoto dado" msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "A ativação desta funcionalidade é feita por sua conta e risco, o que significa que precisa de entrar em contato com o fabricante do equipamento original sobre quaisquer problemas causados por estas atualizações. Apenas problemas com o processo de atualização devem ser relatados em $OS_RELEASE:BUG_REPORT_URL$." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "A ativação deste remoto é feito a seu próprio custo e risco." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Criptografado" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "RAM criptografada" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "Apaga todo histórico de atualização de firmware" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "A apagar…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "Sair após pequeno atraso" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "Sair após o carregamento do motor" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "Exporta a estrutura de ficheiro de um firmware para XML" #. TRANSLATORS: command description msgid "Extract a firmware blob to images" msgstr "Extrai um blob de firmware para imagens" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "FICHEIRO" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "FICHEIRO [ID-DISPOSITIVO|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "NOMEDOFICHEIRO" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "NOME-DE-FICHEIRO CERTIFICADO CHAVE-PRIVADA" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "NOME-DE-FICHEIRO ID-DISPOSITIVO" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "NOME-DE-FICHEIRO [ID-DISPOSITIVO|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "NOME-DE-FICHEIRO [TIPO-FIRMWARE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" msgstr "NOME-DE-FICHEIRO-ORIG NOME-DE-FICHEIRO-DEST [TIPO-FIRMWARE-ORIG] [TIPO-FIRMWARE-DEST]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "Falhou" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Falha ao aplicar a atualização" #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "Falha ao se ligar ao daemon" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "Falha ao carregar o dbx local" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "Falha ao carregar o dbx do sistema" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "Falha ao bloquear" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Falha ao interpretar argumentos" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "Falha ao analisar o ficheiro" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "Falha ao analisar o dbx local" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "Falha ao validar o conteúdo da ESP" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Nome de ficheiro" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "Assinatura de nome de ficheiro" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "Fonte do nome do ficheiro" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Nome de ficheiro necessário" #. TRANSLATORS: command line option msgid "Filter with a set of device flags using a ~ prefix to exclude, e.g. 'internal,~needs-reboot'" msgstr "Filtra com um conjunto de opções de dispositivo usando um prefixo ~ para excluir, p.ex. \"internal,~needs-reboot\"" #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "URI base de firmware" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "Serviço D-Bus de Atualização de Firmware" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Daemon de Atualização de Firmware" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "Utilitário de Firmware" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "Atestação de firmware" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "O firmware já está bloqueado" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "O firmware ainda não está bloqueado" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "Os metadados do firmware não foram atualizados a %u dia e podem não estar atualizados." msgstr[1] "Os metadados do firmware não foram atualizados a %u dias e podem não estar atualizados." msgstr[2] "Os metadados do firmware não foram atualizados a %u dias e podem não estar atualizados." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "Atualizações de firmware" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "Opções" #. TRANSLATORS: command line option msgid "Force the action by relaxing some runtime checks" msgstr "Força a ação relaxando alguns verificações em tempo de execução" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Encontrado" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "Encriptação completa do disco detetada" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" msgstr[1] "GUIDs" msgstr[2] "GUIDs" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" msgid "Get BIOS settings" msgstr "Obter definições do BIOS" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "Obtém todas as opções de dispositivo suportadas pelo fwupd" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Obtém todos os dispositivos que oferecem suporte às atualizações de firmware" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "Obtém todos os plugins registados com o sistema" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Obtém detalhes sobre um ficheiro de firmware" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "Obtém os remotos configurados" #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "Obtém os atributos de segurança do host" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "Obtém a lista de firmware aprovado" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "Obtém a lista de firmwares bloqueados" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Obtém a lista de atualizações para os hardwares conectados" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "Obtém os lançamentos para um dispositivo" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Obtém os resultados da última atualização" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "FICHEIRO-HWIDS" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "O hardware está aguardando para ser reconectado" #. TRANSLATORS: the release urgency msgid "High" msgstr "Alta" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "ID de segurança do host:" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "Inativo…" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "Ignora verificações estritas de SSL ao baixar ficheiros" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "Ignora falhas de soma de verificação de firmware" #. TRANSLATORS: command line option msgid "Ignore firmware hardware mismatch failures" msgstr "Ignora falhas de incompatibilidade de hardware de firmware" #. TRANSLATORS: try to help msgid "Ignoring SSL strict checks, to do this automatically in the future export DISABLE_SSL_STRICT in your environment" msgstr "Ignorando as verificações estritas de SSL; para fazer isso automaticamente no futuro, exporte DISABLE_SSL_STRICT no seu ambiente" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "Duração de instalação" msgid "Install old version of signed system firmware" msgstr "Instalar a versão antiga do firmware do sistema assinado" msgid "Install old version of unsigned system firmware" msgstr "Instalar a versão antiga do firmware do sistema não assinado" msgid "Install signed device firmware" msgstr "Instalar firmware assinado no dispositivo" msgid "Install signed system firmware" msgstr "Instalar firmware assinado no sistema" msgid "Install unsigned device firmware" msgstr "Instalar firmware não assinado no dispositivo" msgid "Install unsigned system firmware" msgstr "Instalar firmware não assinado no sistema" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "Instalando atualização de firmware…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "Instalando em %s…" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "ACM protegido com Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "Fuse programável (OTP) do Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Política de erro do Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Inicialização verificada com Intel BootGuard" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "Dispositivo interno" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "Inválido" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "Está no modo gestor de arranque" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "Problema" msgstr[1] "Problemas" msgstr[2] "Problemas" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "LOCAL" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "Última modificação" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "Menos que um minuto restante" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "Licença" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Linux Vendor Firmware Service (firmware estável)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Linux Vendor Firmware Service (firmware de teste)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Kernel Linux" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Confinamento do kernel Linux" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Swap do Linux" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "Lista entradas no dbx" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "Lista os tipos de firmware disponível" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "Lista ficheiros na ESP" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "A carregar…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "Bloqueado" #. TRANSLATORS: the release urgency msgid "Low" msgstr "Baixa" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "Modo de fabricação do MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "Substituição de MEI" msgid "MEI version" msgstr "Versão MEI" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "Ativa manualmente plugins específicos" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "Média" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "Assinatura de metadados" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "URI de metadados" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "Metadados podem ser obtidos do Linux Vendor Firmware Service." #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "Versão mínima" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "Modifica um valor de configuração do daemon" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "Modifica um remoto dado" msgid "Modify a configured remote" msgstr "Modificar um remoto configurado" msgid "Modify daemon configuration" msgstr "Modificar a configuração do daemon" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "Monitora o daemon por eventos" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "Monta a ESP" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "Precisa de uma reinício após a instalação" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "Precisa reiniciar" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "Precisa de desligamento após a instalação" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "Nova versão" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "Nenhuma ação especificada!" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "Nenhum downgrade para %s" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "Nenhum ID de firmware encontrado" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Nenhum periférico com capacidade de atualização de firmware foi detectado" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "Nenhum lançamento disponível" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "Nenhum remoto está atualmente ativado, então nenhum metadado está disponível." #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "Nenhum remoto disponível" msgid "No updates available for remaining devices" msgstr "Nenhuma atualização disponível para os dispositivos restantes" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "Não encontrado" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "Não suportado" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "OK" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "OK!" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "Mostra apenas valor único de PCR" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "Apenas atualizações de versão são permitidas" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "Substitui o caminho padrão da ESP" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "CAMINHO" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "Analisa e mostra detalhes sobre um ficheiro de firmware" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "Analisando a atualização de dbx…" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "Analisando o dbx do sistema…" #. TRANSLATORS: remote filename base msgid "Password" msgstr "Senha" msgid "Payload" msgstr "Carga" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "Pendente" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "Efetuar a operação?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "Depuração de plataforma" #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "Por favor, insira um número de 0 a %u: " #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "Dependências de plugin ausentes" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "Proteção de DMA pré-inicialização" #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "Versão anterior" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Prioridade" msgid "Proceed with upload?" msgstr "Prosseguir com o envio?" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "Privativa" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "ID-REMOTO" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID KEY VALUE" msgstr "ID-REMOTO CHAVE VALOR" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "Lê um blob de firmware de um dispositivo" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "Lendo de %s…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "A ler…" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "Renova metadados do servidor remoto" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "Reinstalar %s para %s?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "Reinstala o firmware atual no dispositivo" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "Reinstala firmware em um dispositivo" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "Ramificação de lançamento" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "ID da versão" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "ID remoto" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "URI do relatório" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "Relatado ao servidor remoto" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "O sistema de ficheiros efivarfs necessário não foi encontrado" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "O hardware necessário não foi encontrado" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "Requer um gestor de arranque" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Requer ligação à Internet" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Reiniciar agora?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "Reiniciar o daemon para tornar a mudança efetiva?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "A reiniciar o dispositivo…" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "Retorna todos os IDs de hardware para a máquina" #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "Executa a rotina de limpeza da composição de plugin ao usar install-blob" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "Executa a rotina de preparação da composição de plugin ao usar install-blob" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "O kernel em uso é muito antigo" #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "Sufixo de tempo de execução" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "Descritor de BIOS do SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "Região BIOS do SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "Bloqueio de SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "Escrita de SPI" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "SUBSISTEMA DRIVER [ID-DISPOSITIVO|GUID]" #. TRANSLATORS: command description msgid "Save a file that allows generation of hardware IDs" msgstr "Guardar um ficheiro que permite a geração de IDs de hardware" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "A agendar…" msgid "Security hardening for HSI" msgstr "Reforço da segurança das HSI" #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "Veja %s para mais informações." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Dispositivo selecionado" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Volume selecionado" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "Número de série" msgid "Set one or more BIOS settings" msgstr "Definir uma ou mais definições do BIOS" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Define a lista de firmwares aprovados" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "As definições serão aplicadas após a reinicialização do sistema" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "Compartilha histórico de firmware com os desenvolvedores" #. TRANSLATORS: command line option msgid "Show all results" msgstr "Mostra todos os resultados" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "Mostra as versões do cliente e do daemon" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "Mostrar informações detalhadas do daemon para um domínio em particular" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "Mostra informações de depuração para todos domínios" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Mostrar opções de depuração" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Mostra dispositivos que não são atualizáveis" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Mostra informações adicionais de depuração" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "Mostra histórico de atualizações de firmware" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "Mostra a versão calculada do dbx" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "Desligar agora?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "Assina um firmware com uma nova chave" msgid "Sign data using the client certificate" msgstr "Assina dados usando o certificado cliente" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "Assina dados usando o certificado cliente" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "Assina os dados enviados com o certificado cliente" msgid "Signature" msgstr "Assinatura" #. TRANSLATORS: file size of the download msgid "Size" msgstr "Tamanho" #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "Fonte" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "Especifica o ficheiro da base de dados dbx" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "Sucesso" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "Todos os dispositivos ativados com sucesso" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "Remoto desabilitado com sucesso" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "Transferidos com sucesso novos metadados: " #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "Remoto ativado e atualizado com sucesso" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "Remoto ativado com sucesso" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "Firmware instalado com sucesso" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "Valor da configuração modificada com sucesso" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "Remoto modificado com sucesso" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "Metadados atualizados manualmente com sucesso" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "Somas de verificação de dispositivo atualizadas com sucesso" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "Enviado com sucesso %u relatório" msgstr[1] "Enviados com sucesso %u relatórios" msgstr[2] "Enviados com sucesso %u relatórios" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "Somas de verificação de dispositivo verificadas com sucesso" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Resumo" #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "Suportado" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "CPU suportado" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "Suporte no servidor remoto" #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "Suspensão para inativo" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "Suspensão para RAM" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "Trocar ramificação de %s para %s ?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "Alterna a ramificação do firmware no dispositivo" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "O sistema exige uma fonte de alimentação externa" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "TEXTO" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "Reconstrução de PCR0 de TPM" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM empty PCRs" msgstr "PCRs vazios de TPM" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "Etiqueta" msgstr[1] "Etiquetas" msgstr[2] "Etiquetas" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "Contaminado" #. show the user the entire data blob msgid "Target" msgstr "Destino" #. TRANSLATORS: command description msgid "Test a device using a JSON manifest" msgstr "Testa um dispositivo usando um manifesto JSON" #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "O LVFS é um serviço livre que opera como uma entidade legal independente e tem nenhuma ligação com $OS_RELEASE:NAME$. O seu distribuidor pode não ter verificado alguma das atualizações de firmware por compatibilidade com O seu sistema ou dispositivos ligados. Todo o firmware é fornecido apenas pelo fabricante do equipamento original." #. TRANSLATORS: this is more background on a security measurement problem msgid "The TPM PCR0 differs from reconstruction." msgstr "O TPM PCR0 diverge da reconstrução." #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "O daemon carregou código de terceiros e não é mais suportado pelos desenvolvedores upstream!" #. TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name #, c-format msgid "The firmware from %s is not supplied by %s, the hardware vendor." msgstr "O firmware de %s não é fornecido por %s, o fornecedor de hardware." #. TRANSLATORS: try to help msgid "The system clock has not been set correctly and downloading files may fail." msgstr "O relógio do sistema não foi definido corretamente e baixar ficheiros pode resultar em falha." #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "Não há ficheiros de firmware bloqueados" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "Nenhum firmware aprovado." #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "Este pacote não foi validado, pode não funcionar corretamente." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "Esse programa só pode funcionar corretamente como root" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "Este remoto contém firmware que não está embargado, mas ainda está a ser testado pelo fornecedor do hardware. Deve garantir que tem uma maneira de reverter manualmente do firmware se a atualização do firmware falhar." #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "Este sistema possui problemas de tempo de execução de HSI." #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "Este sistema possui um nível de segurança de HSI baixo." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "Esta ferramenta permite que um administrador aplique atualizações de dbx UEFI." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "Esta ferramenta permite que um administrador consulte e controle o daemon fwupd, permitindo que ele execute ações como instalar ou fazer downgrade do firmware." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "Esta ferramenta permite que um administrador use os plugins do fwupd sem estarem instalados no sistema host." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "Esta ferramenta vai ler e analisar o log de evento de TPM do firmware do sistema." #. TRANSLATORS: the update state of the specific device msgid "Transient failure" msgstr "Falha transitória" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Tipo" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "Partição UEFI ESP não detectada ou configurada" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI bootservice variables" msgstr "Variáveis do serviço de arranque UEFI" #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "Atualizações de cápsula UEFI não disponíveis ou habilitadas na configuração do firmware" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "Utilitário de dbx UEFI" #. TRANSLATORS: system is not booted in UEFI mode msgid "UEFI firmware can not be updated in legacy BIOS mode" msgstr "Firmware UEFI não pode ser atualizado no modo BIOS legado" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "Chave de plataforma UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "Secure boot de UEFI" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "Não foi possível conectar ao serviço" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "Desvincula o driver atual" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "Desbloqueando firmware:" #. TRANSLATORS: command description msgid "Unblocks a specific firmware from being installed" msgstr "Retira impedimento de um firmware específico ser instalado" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "Descriptografado" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Desconhecido" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Dispositivo desconhecido" msgid "Unlock the device to allow access" msgstr "Desbloquear o dispositivo para permitir acesso" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "Desbloqueado" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "Desbloqueia o dispositivo para acesso do firmware" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "Desmonta a ESP" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "Sem suporte ao daemon na versão %s, a versão do cliente é %s" #. TRANSLATORS: Suffix: the HSI result msgid "Untainted" msgstr "Descontaminado" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "Atualizável" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Erro de atualização" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "Mensagem de atualização" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "Estado da atualização" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "A falha de atualização é um problema conhecido, visite esta URL para mais informações:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Atualizar agora?" #. TRANSLATORS: command description msgid "Update the stored cryptographic hash with current ROM contents" msgstr "Atualiza o hash criptográfico armazenado com o atual conteúdo da ROM" msgid "Update the stored device verification information" msgstr "Atualizar as informações de verificação armazenadas do dispositivo" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "Atualiza os metadados armazenados com o conteúdo atual" msgid "Updating" msgstr "Atualizando" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "Atualizando %s…" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "Atualizar %s de %s para %s?" #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "O envio de relatórios de firmware ajuda os fornecedores de hardware a identificar rapidamente atualizações com falha e êxito em dispositivos." #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "Urgência" #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "O utilizador foi notificado" #. TRANSLATORS: remote filename base msgid "Username" msgstr "Nome de utilizador" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "Válido" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "Validando conteúdo da ESP…" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "Variação" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "Fornecedor" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "A verificar…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Versão" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "ATENÇÃO" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "A aguardar…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "Monitora alterações no hardware" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "A gravar ficheiro:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "A gravar…" #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "O seu distribuidor pode não ter verificado nenhuma das atualizações de firmware por compatibilidade com seu sistema ou dispositivos ligados." #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "O seu hardware pode ser danificado usando este firmware e instalar esta versão pode anular qualquer garantia com %s." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[SOMA-VERIFICAÇÃO]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[ID-DISPOSITIVO|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[ID-DISPOSITIVO|GUID] [RAMIFICAÇÃO]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILE FILE_SIG REMOTE-ID]" msgstr "[FICHEIRO FICHEIRO-ASSINATURA ID-REMOTO]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[NOME-DE-FICHEIRO1] [NOME-DE-FICHEIRO2]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[FICHEIRO-SMBIOS|FICHEIRO-HWIDS]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "padrão" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "Utilitário de registo de eventos TPM do fwupd" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "Plugins do fwupd" fwupd-2.0.10/po/pt_BR.po000066400000000000000000003766511501337203100147200ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Derek W. Stavis , 2015 # Derek W. Stavis , 2016 # Derek W. Stavis , 2015-2016 # F Bausch, 2024-2025 # Rafael Fontenelle , 2017-2018 # Rafael Fontenelle , 2015-2025 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Portuguese (Brazil) (http://app.transifex.com/freedesktop/fwupd/language/pt_BR/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: pt_BR\n" "Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "%.0f minuto restante" msgstr[1] "%.0f minutos restantes" msgstr[2] "%.0f minutos restantes" #. TRANSLATORS: BMC refers to baseboard management controller which #. * is the device that updates all the other firmware on the system #, c-format msgid "%s BMC Update" msgstr "Atualização de BMC para %s" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "Atualização da bateria para %s" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "Atualização de microcódigo de CPU para %s" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "Atualização de câmera para %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "Atualização da configuração de %s " #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "Atualização consumidor-final de ME para %s " #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "Atualização de controlador para %s" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "Atualização corporativa de ME para %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "Atualização do dispositivo %s" #. TRANSLATORS: Video Display refers to the laptop internal display or #. * external monitor #, c-format msgid "%s Display Update" msgstr "Atualização de tela para %s" #. TRANSLATORS: Dock refers to the port replicator hardware laptops are #. * cradled in, or lowered onto #, c-format msgid "%s Dock Update" msgstr "Atualização da base %s" #. TRANSLATORS: drive refers to a storage device, e.g. SATA disk #, c-format msgid "%s Drive Update" msgstr "Atualização de unidade %s" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "Atualização do controlador embarcado %s" #. TRANSLATORS: a device that can read your fingerprint pattern #, c-format msgid "%s Fingerprint Reader Update" msgstr "Atualização do leitor de impressão digital %s" #. TRANSLATORS: flash refers to solid state storage, e.g. UFS or eMMC #, c-format msgid "%s Flash Drive Update" msgstr "Atualização de unidade flash %s" #. TRANSLATORS: GPU refers to a Graphics Processing Unit, e.g. #. * the "video card" #, c-format msgid "%s GPU Update" msgstr "Atualização da GPU %s" #. TRANSLATORS: a large pressure-sensitive drawing area typically used #. * by artists and digital artists #, c-format msgid "%s Graphics Tablet Update" msgstr "Atualização do tablet gráfico %s" #. TRANSLATORS: two miniature speakers attached to your ears #, c-format msgid "%s Headphones Update" msgstr "Atualização do fone de ouvido %s" #. TRANSLATORS: headphones with an integrated microphone #, c-format msgid "%s Headset Update" msgstr "Atualização do headset %s" #. TRANSLATORS: an input device used by gamers, e.g. a joystick #, c-format msgid "%s Input Controller Update" msgstr "Atualização do controlador de entrada %s" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "Atualização do teclado para %s" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "Atualização de ME para %s" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "Atualização do mouse para %s" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "Atualização da interface de rede %s" #. TRANSLATORS: SSD refers to a Solid State Drive, e.g. non-rotating #. * SATA or NVMe disk #, c-format msgid "%s SSD Update" msgstr "Atualização de SSD %s" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "Atualização do controlador de armazenamento %s" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "Atualização do sistema %s" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "Atualização de TPM para %s" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "Atualização de controlador Thunderbolt para %s" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "Atualização do touchpad para %s" #. TRANSLATORS: Dock refers to the port replicator device connected #. * by plugging in a USB cable -- which may or may not also provide power #, c-format msgid "%s USB Dock Update" msgstr "Atualização da base USB %s" #. TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth #. * device that stays in the USB port so the wireless peripheral works #, c-format msgid "%s USB Receiver Update" msgstr "Atualização de receptor USB para %s" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "Atualização de %s" #. TRANSLATORS: warn the user before updating, %1 is a device name #, c-format msgid "%s and all connected devices may not be usable while updating." msgstr "%s e todos os dispositivos conectados não podem ser usados durante a atualização." #. TRANSLATORS: %1 refers to some kind of security test, e.g. "Encrypted RAM". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s appeared: %s" msgstr "%s apareceu: %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "UEFI platform #. key". #. * %2 and %3 refer to results value, e.g. "Valid" and "Invalid" #, c-format msgid "%s changed: %s → %s" msgstr "%s alterado: %s → %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "SPI BIOS #. region". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s disappeared: %s" msgstr "%s desapareceu: %s" #. TRANSLATORS: the device has a reason it can't update, e.g. laptop lid #. closed #, c-format msgid "%s is not currently updatable" msgstr "%s não é atualmente atualizável:" #. TRANSLATORS: %1 is a device name, e.g. "ThinkPad Universal #. * ThunderBolt 4 Dock" and %2 is "fwupdmgr activate" #, c-format msgid "%s is pending activation; use %s to complete the update." msgstr "%s está pendente ativação. use %s para concluir a atualização" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "Modo de fabricação de %s" #. TRANSLATORS: warn the user before #. * updating, %1 is a device name #, c-format msgid "%s must remain connected for the duration of the update to avoid damage." msgstr "%s deve permanecer conectado durante a atualização para evitar danos." #. TRANSLATORS: warn the user before updating, %1 is a machine #. * name #, c-format msgid "%s must remain plugged into a power source for the duration of the update to avoid damage." msgstr "%s deve permanecer conectado a uma fonte de energia durante a atualização para evitar danos." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "Substituição de %s" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "versão %s " #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u dia" msgstr[1] "%u dias" msgstr[2] "%u dias" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] " %u dispositivo tem uma atualização de firmware disponível." msgstr[1] "%u dispositivos têm uma atualização de firmware disponível." msgstr[2] "%u dispositivos têm uma atualização de firmware disponível." #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device is not the best known configuration." msgid_plural "%u devices are not the best known configuration." msgstr[0] "%u dispositivo não é a configuração mais conhecida." msgstr[1] "%u dispositivos não são a configuração mais conhecida." msgstr[2] "%u dispositivos não são a configuração mais conhecida." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u hora" msgstr[1] "%u horas" msgstr[2] "%u horas" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minuto" msgstr[1] "%u minutos" msgstr[2] "%u minutos" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u segundo" msgstr[1] "%u segundos" msgstr[2] "%u segundos" #. TRANSLATORS: device tests can be specific to a CPU type #, c-format msgid "%u test was skipped" msgid_plural "%u tests were skipped" msgstr[0] "%u teste foi ignorado" msgstr[1] "%u testes foram ignorado" msgstr[2] "%u testes foram ignorados" #. TRANSLATORS: first percentage is current value, 2nd percentage is the #. * lowest limit the firmware update is allowed for the update to happen #, c-format msgid "%u%% (threshold %u%%)" msgstr "%u%% (limiar %u%%)" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(obsoleto)" #. TRANSLATORS: HSI event title msgid "A TPM PCR is now an invalid value" msgstr "Um PCR de TPM é agora um valor inválido" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "AMD Firmware Replay Protection" msgstr "Proteção contra reprodução de firmware AMD" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "AMD Firmware Write Protection" msgstr "Proteção contra gravação de firmware AMD" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "AMD Secure Processor Rollback Protection" msgstr "Proteção contra reversão de processador seguro AMD" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "ARCHIVE FIRMWARE METAINFO [FIRMWARE] [METAINFO] [JCATFILE]" msgstr "PACOTE FIRMWARE METAINFO [FIRMWARE] [METAINFO] [ARQUIVOJCAT]" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "Ação necessária:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "Habilita dispositivos" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "Ativa dispositivos pendentes" msgid "Activate the new firmware on the device" msgstr "Ativar o novo firmware no dispositivo" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "Ativando atualização de firmware" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "Ativando atualização de firmware para" #. TRANSLATORS: command description msgid "Adds devices to watch for future emulation" msgstr "Adiciona dispositivos para monitorar para emulação futura" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Idade" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "Concordar e habilitar o remoto?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Apelido para %s" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are now valid" msgstr "Todos PCRs de TPM são agora válidos" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are valid" msgstr "Todos PCRs de TPM são válidos" #. TRANSLATORS: an application is preventing system updates msgid "All devices are prevented from update by system inhibit" msgstr "Todos os dispositivos estão impedidos de atualizar por inibição do sistema" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "Todos os dispositivos do mesmo tipo serão atualizados ao mesmo tempo" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Permite fazer downgrade de versões de firmware" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "Permite reinstalar versões de firmware existentes" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "Permite alternar a ramificação do firmware" #. TRANSLATORS: error message msgid "Already exists, and no --force specified" msgstr "Já existe, e --force não foi especificado" #. TRANSLATORS: is not the main firmware stream msgid "Alternate branch" msgstr "Ramificação alternativa" #. TRANSLATORS: another application is updating the device already msgid "An update is in progress" msgstr "Uma atualização está em progresso" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "Uma atualização requer uma reinicialização para ser concluída." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "Uma atualização requer que o sistema seja desligado por completo." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Responde sim para todas as perguntas" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Aplica a atualização mesmo quando não recomendado" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Aplica arquivos de atualização" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "Aplicando atualização…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Firmware aprovado:" msgstr[1] "Firmwares aprovados:" msgstr[2] "Firmwares aprovados:" #. TRANSLATORS: command description msgid "Asks the daemon to quit" msgstr "Pede para o daemon sair" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "Modo anexar ao firmware" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Autenticando…" #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "Os detalhes de autenticação são necessários" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Autenticação é necessária para desatualizar a versão do firmware em um dispositivo removível" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Autenticação é necessária para desatualizar a versão do firmware nesta máquina" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to enable emulation data collection" msgstr "Autenticação é necessária para habilitar coleta de dados de emulação" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to fix a host security issue" msgstr "Autenticação é necessária para corrigir um problema de segurança do host" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to load hardware emulation data" msgstr "Autenticação é necessária para carregar dados de emulação de hardware" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "Autenticação é necessária para modificar as configurações da BIOS" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "Autenticação é necessária para modificar um remoto configurado usado para atualizações de firmware" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "Autenticação é necessária para modificar uma configuração de daemon" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "Autenticação é necessária para ler as configurações da BIOS" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to reset daemon configuration to defaults" msgstr "Autenticação é necessária para redefinir uma configuração de daemon para o padrão" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to save hardware emulation data" msgstr "Autenticação é necessária para salvar dados de emulação de hardware" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "Autenticação é necessária para definir a lista de firmwares aprovados" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "Autenticação é necessária para assinar dados usando o certificado do cliente" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to stop the firmware update service" msgstr "Autenticação é necessária para parar o serviço de atualização de firmware" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "Autenticação é necessária para alternar para nova versão de firmware" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to undo the fix for a host security issue" msgstr "Autenticação é necessária para desfazer a correção de um problema de segurança do host" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Autenticação é necessária para desbloquear um dispositivo" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Autenticação é necessária para atualizar o firmware em dispositivo removível" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Autenticação é necessária para atualizar o firmware nesta máquina" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "Autenticação é necessária para atualizar as somas de verificação armazenadas para o dispositivo" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "Relatórios automáticos" #. TRANSLATORS: we can auto-uninhibit after a timeout #, c-format msgid "Automatically uninhibiting in %ums…" msgstr "Desinibindo automaticamente em %ums…" #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "Enviar automaticamente toda vez?" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS Firmware Updates" msgstr "Atualizações de firmware da BIOS" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS Rollback Protection" msgstr "Proteção contra reversão da BIOS" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS firmware updates" msgstr "Atualizações de firmware da BIOS" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS rollback protection" msgstr "Proteção contra reversão da BIOS" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "Atualizações do BIOS entregues via LVFS ou Windows Update" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "BUILDER-XML FILENAME-DST" msgstr "XML-CONSTRUTOR NOME-DE-ARQUIVO-DEST" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "Bateria" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "Vincula novo driver de kernel" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "Arquivos de firmware bloqueados:" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "Versão bloqueada" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "Bloqueando firmware:" #. TRANSLATORS: command description msgid "Blocks a specific firmware from being installed" msgstr "Impede que um firmware específico seja instalado" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "Versão do gerenciador de boot" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "Ramificação" #. TRANSLATORS: command description msgid "Build a cabinet archive from a firmware blob and XML metadata" msgstr "Constrói um arquivo de gabinete a partir de um blob de firmware e metadados XML" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "Compila um arquivo de firmware" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * Utilized by OS means the distribution enabled it msgid "CET OS Support" msgstr "Suporte a CET pelo sistema operacional" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * enabled means supported by the processor msgid "CET Platform" msgstr "CET Platform" #. TRANSLATORS: longer description msgid "CPU Microcode must be updated to mitigate against various information-disclosure security issues." msgstr "Microcódigo de CPU deve ser atualizado para mitigar vários problemas de segurança na divulgação de informações." #. TRANSLATORS: we can save all device enumeration events for emulation msgid "Can tag for emulation" msgstr "Pode etiquetar para emulação" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Cancelar" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "Cancelado" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "Não foi possível aplicar, pois a atualização de dbx já foi aplicada." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Alterado" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "Verifica se o hash criptográfico corresponde ao firmware" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Soma de verificação" #. TRANSLATORS: get interactive prompt, where branch is the #. * supplier of the firmware, e.g. "non-free" or "free" msgid "Choose branch" msgstr "Escolha a ramificação" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "Escolha o dispositivo" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "Escolha o firmware" #. TRANSLATORS: get interactive prompt msgid "Choose release" msgstr "Escolha a versão" #. TRANSLATORS: get interactive prompt msgid "Choose volume" msgstr "Escolha o volume" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "Limpa os resultados da última atualização" #. TRANSLATORS: error message msgid "Command not found" msgstr "Comando não encontrado" #. TRANSLATORS: is not supported by the vendor msgid "Community supported" msgstr "Suporte mantido pela comunidade" #. TRANSLATORS: command description msgid "Compares two versions for equality" msgstr "Compara duas versões para igualdade" #. TRANSLATORS: title prefix for the BIOS settings dialog msgid "Configuration Change Suggested" msgstr "Alteração de configuração sugerida" #. TRANSLATORS: no peeking msgid "Configuration is only readable by the system administrator" msgstr "A configuração só pode ser lida pelo administrador do sistema" #. TRANSLATORS: longer description msgid "Control-Flow Enforcement Technology detects and prevents certain methods for running malicious software on the device." msgstr "O Control-Flow Enforcement Technology detecta e previne certos métodos de execução de software malicioso no dispositivo." #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology msgid "Control-flow Enforcement Technology" msgstr "Control-flow Enforcement Technology" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "Converte um arquivo de firmware" #. TRANSLATORS: command description msgid "Create an EFI boot entry" msgstr "Cria uma entrada de inicialização EFI" #. TRANSLATORS: when the update was built msgid "Created" msgstr "Criada" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "Crítica" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "A verificação criptográfica de hash está disponível" #. TRANSLATORS: current value of a BIOS setting msgid "Current Value" msgstr "Valor atual" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "Versão atual" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "ID-DISPOSITIVO|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Opções de depuração" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "Descompactando…" #. TRANSLATORS: command description msgid "Delete an EFI boot entry" msgstr "Exclui uma entrada de inicialização EFI" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Descrição" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "Modo desanexar ao gerenciador de boot" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "Detalhes" #. TRANSLATORS: the best known configuration is a set of software that we know #. works #. * well together. In the OEM and ODM industries it is often called a BKC msgid "Deviate from the best known configuration?" msgstr "Desviar da configuração mais conhecida?" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "Opções do dispositivo" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "ID do dispositivo" #. TRANSLATORS: description of the device requests msgid "Device Requests" msgstr "Solicitações de dispositivo" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Dispositivo adicionado:" #. TRANSLATORS: the device is already connected msgid "Device already exists" msgstr "O dispositivo já existe" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "A carga da bateria do dispositivo está muito baixa" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse #, c-format msgid "Device battery power is too low (%u%%, requires %u%%)" msgstr "A carga da bateria do dispositivo está muito baixa (%u%%, requer %u%%)" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "O dispositivo pode recuperar falhas de flashing" #. TRANSLATORS: lid means "laptop top cover" msgid "Device cannot be updated while the lid is closed" msgstr "O dispositivo não pode ser atualizado enquanto a tampa estiver fechada" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Dispositivo modificado:" #. TRANSLATORS: a version check is required for all firmware msgid "Device firmware is required to have a version check" msgstr "Um firmware de dispositivo é necessário para ter uma verificação de versão" #. TRANSLATORS: emulated means we are pretending to be a different model msgid "Device is emulated" msgstr "O dispositivo é emulado" #. TRANSLATORS: device cannot be interrupted, for instance taking a phone call msgid "Device is in use" msgstr "O dispositivo está em uso" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "O dispositivo está bloqueado" #. TRANSLATORS: we have two ways of communicating with the device, so we hide #. one msgid "Device is lower priority than an equivalent device" msgstr "O dispositivo tem prioridade menor que um dispositivo equivalente" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "Um dispositivo é necessário para instalar todos os lançamentos fornecidos" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "O dispositivo está inalcançável" #. TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode msgid "Device is unreachable, or out of wireless range" msgstr "O dispositivo está inacessível ou fora do alcance sem fio" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "O dispositivo pode ser usado durante a atualização" #. TRANSLATORS: usually this is when we're waiting for a reboot msgid "Device is waiting for the update to be applied" msgstr "O dispositivo está aguardando a atualização ser aplicada" #. TRANSLATORS: success, so say thank you to the user msgid "Device list uploaded successfully, thanks!" msgstr "Lista de dispositivos enviada com sucesso, obrigado!" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Dispositivo removido:" #. TRANSLATORS: as in, wired mains power for a laptop msgid "Device requires AC power to be connected" msgstr "O dispositivo requer alimentação elétrica na tomada para ser conectado" #. TRANSLATORS: device does not have a display connected msgid "Device requires a display to be plugged in" msgstr "O dispositivo requer uma tela conectada" #. TRANSLATORS: The device cannot be updated due to missing vendor's license." msgid "Device requires a software license to update" msgstr "O dispositivo requer uma licença de software para atualização" #. TRANSLATORS: longer description msgid "Device software updates are provided for this device." msgstr "Atualizações de software de dispositivo são fornecidas para este dispositivo." #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "Atualizações de estágios do dispositivo" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "O dispositivo tem suporte para alternar para uma ramificação diferente do firmware" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "A atualização do dispositivo precisa de ativação" #. TRANSLATORS: save the old firmware to disk before installing the new one msgid "Device will backup firmware before installing" msgstr "O dispositivo fará backup do firmware antes de instalar" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "O dispositivo não aparecerá novamente após a atualização ser concluída" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "Dispositivos que foram atualizados com sucesso:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "Dispositivos que não foram atualizados com sucesso:" #. TRANSLATORS: message letting the user there is an update #. * waiting, but there is a reason it cannot be deployed msgid "Devices with firmware updates that need user action: " msgstr "Dispositivos com atualizações de firmware que precisam de ação do usuário: " #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "Dispositivos com nenhuma atualização de firmware disponível:" #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "Dispositivos com a versão mais recente do firmware disponível:" #. TRANSLATORS: this is for the device tests msgid "Did not find any devices with matching GUIDs" msgstr "Não foi encontrado qualquer dispositivo com GUID correspondente" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "Desabilitado" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "Desabilita um remoto dado" #. TRANSLATORS: command description msgid "Disables virtual testing devices" msgstr "Desabilita dispositivos de teste vrituais" #. TRANSLATORS: the OS the release was tested on msgid "Distribution" msgstr "Distribuição" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "Não verifica por metadados antigos" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "Não verifica por histórico não relatado" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "Não verifica se remotos para dowload devem estar habilitados" #. TRANSLATORS: command line option msgid "Do not check or prompt for reboot after update" msgstr "Não verifica nem solicita configuração para reiniciar após atualizar" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "Não inclui prefixo de domínio de log" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "Não inclui prefixo com marca de tempo" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "Não realiza verificações de segurança de dispositivo" #. TRANSLATORS: command line option msgid "Do not prompt for devices" msgstr "Não solicita dispositivos" #. TRANSLATORS: command line option msgid "Do not prompt to fix security issues" msgstr "Não solicita a correção de problemas de segurança" #. TRANSLATORS: command line option msgid "Do not search the firmware when parsing" msgstr "Não busca o firmware ao analisar" #. TRANSLATORS: warning message shown after update has been scheduled msgid "Do not turn off your computer or remove the AC adaptor while the update is in progress." msgstr "Não desligue seu computador nem remova o cabo de alimentação da tomada enquanto a atualização está em progresso." #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "Não escreve no banco de dados de histórico" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "Você entende as consequências de trocar a ramificação do firmware?" #. TRANSLATORS: ask the user if it's okay to convert, #. * "it" being the data contained in the EFI boot entry msgid "Do you want to convert it now?" msgstr "Você deseja convertê-la agora?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "Você deseja desabilitar este recurso para atualizações futuras?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "Você deseja atualizar este remoto agora?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "Você deseja enviar relatórios automaticamente para atualizações futuras?" #. TRANSLATORS: command line option msgid "Don't prompt for authentication (less details may be shown)" msgstr "Não solicita autenticação (menos detalhes podem ser mostrados)" #. TRANSLATORS: success msgid "Done!" msgstr "Feito!" #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "Desatualizar %s de %s para %s?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "Desatualiza a versão do firmware em um dispositivo" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "Fazendo downgrade %s…" #. TRANSLATORS: command description msgid "Download a file" msgstr "Baixa um arquivo" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Baixando…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "Despeja dados SMBIOS a partir de um arquivo" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "Duração" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "EMULATION-FILE [ARCHIVE-FILE]" msgstr "ARQUIVO-DE-EMULAÇÃO [ARQUIVO-DE-ARQUIVAMENTO]" #. TRANSLATORS: longer description msgid "Each system should have tests to ensure firmware security." msgstr "Cada sistema deve ter testes para garantir a segurança do firmware." #. TRANSLATORS: command description msgid "Emulate a device using a JSON manifest" msgstr "Emula um dispositivo usando um manifesto JSON" #. TRANSLATORS: this device is not actually real msgid "Emulated" msgstr "Emulado" #. TRANSLATORS: Title: if we are emulating a different host msgid "Emulated host" msgstr "Host emulado" msgid "Enable" msgstr "Habilitar" msgid "Enable emulation data collection" msgstr "Habilitar coleta de dados de emulação" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "Habilitar novo remoto?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "Habilitar esse remoto?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Habilitado" #. TRANSLATORS: Plugin is active only if hardware is found msgid "Enabled if hardware matches" msgstr "Habilitado se o hardware corresponder" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "Habilita um remoto dado" #. TRANSLATORS: command description msgid "Enables virtual testing devices" msgstr "Habilita dispositivos de teste vrituais" #. TRANSLATORS: longer description msgid "Enabling firmware updates for the BIOS allows fixing security issues." msgstr "Habilitar atualizações de firmware para o BIOS permite corrigir problemas de segurança." msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "A habilitação dessa funcionalidade é feita por sua conta e risco, o que significa que você precisa entrar em contato com o fabricante do equipamento original sobre quaisquer problemas causados por essas atualizações. Somente problemas com o processo de atualização devem ser relatados em $OS_RELEASE:BUG_REPORT_URL$." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "A habilitação deste remoto é feito a seu próprio custo e risco." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Criptografado" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "RAM criptografada" msgid "Encrypted RAM makes it impossible for information that is stored in device memory to be read if the memory chip is removed and accessed." msgstr "A RAM criptografada torna impossível que as informações armazenadas na memória do dispositivo sejam lidas se o chip de memória for removido e acessado." #. TRANSLATORS: the vendor is no longer supporting the device msgid "End of life" msgstr "Fim de vida" #. TRANSLATORS: The BIOS setting can only be changed to fixed values msgid "Enumeration" msgstr "Enumeração" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "Apaga todo histórico de atualização de firmware" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Apagando…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "Sair após pequeno atraso" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "Sair após o carregamento do motor" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "Exporta a estrutura de arquivo de um firmware para XML" #. TRANSLATORS: command description msgid "Export firmware history for manual upload" msgstr "Exporta histórico de firmware para envio manual" #. TRANSLATORS: command description msgid "Extract a firmware blob to images" msgstr "Extrai um blob de firmware para imagens" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "ARQUIVO" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "ARQUIVO [ID-DISPOSITIVO|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "NOME-DE-ARQUIVO" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "NOME-DE-ARQUIVO CERTIFICADO CHAVE-PRIVADA" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "NOME-DE-ARQUIVO ID-DISPOSITIVO" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME OFFSET DATA [FIRMWARE-TYPE]" msgstr "NOME-DE-ARQUIVO DESLOCAMENTO DADOS [TIPO-DO-FIRMWARE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "NOME-DE-ARQUIVO [ID-DISPOSITIVO|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "NOME-DE-ARQUIVO [TIPO-FIRMWARE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" msgstr "NOME-DE-ARQUIVO-ORIG NOME-DE-ARQUIVO-DEST [TIPO-FIRMWARE-ORIG] [TIPO-FIRMWARE-DEST]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" msgstr "NOME-DE-ARQUIVO|SOMA-VERIFICAÇÃO1[,SOMA-VERIFICAÇÃO2][,SOMA-VERIFICAÇÃO3]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "Falhou" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Falha ao aplicar a atualização" #. TRANSLATORS: error message for Windows msgid "Failed to connect to Windows service, please ensure it's running." msgstr "Falha ao se conectar ao serviço do Windows. Por favor, certifique-se de que esteja em execução." #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "Falha ao se conectar ao daemon" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "Falha ao carregar o dbx local" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "Falha ao carregar o dbx do sistema" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "Falha ao bloquear" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Falha ao interpretar argumentos" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "Falha ao analisar o arquivo" #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #, c-format msgid "Failed to parse flags for %s" msgstr "Falha ao analisar opções para %s" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "Falha ao analisar o dbx local" #. TRANSLATORS: a feature is something like "can show an image" msgid "Failed to set front-end features" msgstr "Falha ao definir recursos de frontend" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "Falha ao validar o conteúdo da ESP" #. TRANSLATORS: item is FALSE msgid "False" msgstr "Falso" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Nome de arquivo" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "Assinatura de nome de arquivo" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "Fonte do nome do arquivo" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Nome de arquivo necessário" #. TRANSLATORS: command line option msgid "Filter with a set of device flags using a ~ prefix to exclude, e.g. 'internal,~needs-reboot'" msgstr "Filtra com um conjunto de opções de dispositivo usando um prefixo ~ para excluir, p.ex. \"internal,~needs-reboot\"" #. TRANSLATORS: command line option msgid "Filter with a set of release flags using a ~ prefix to exclude, e.g. 'trusted-release,~trusted-metadata'" msgstr "Filtra com um conjunto de opções de versão usando um prefixo ~ para excluir, p.ex., \"trusted-release,~trusted-metadata\"" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware Attestation" msgstr "Atestação de firmware" #. TRANSLATORS: longer description msgid "Firmware Attestation checks device software using a reference copy, to make sure that it has not been changed." msgstr "A atestação de firmware verifica o software do dispositivo usando uma cópia de referência para garantir que ele não foi alterado." #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "Descritor da BIOS do firmware" #. TRANSLATORS: longer description msgid "Firmware BIOS Descriptor protects device firmware memory from being tampered with." msgstr "O descritor da BIOS do firmware protege a memória do firmware do dispositivo contra adulteração." #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "Região da BIOS do firmware" #. TRANSLATORS: longer description msgid "Firmware BIOS Region protects device firmware memory from being tampered with." msgstr "A região da BIOS do firmware protege a memória do firmware do dispositivo contra adulteração." #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "URI base de firmware" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "Serviço D-Bus de Atualização de Firmware" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Daemon de Atualização de Firmware" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "Verificação do atualizador de firmware" msgid "Firmware Updater Verification checks that software used for updating has not been tampered with." msgstr "Verificação do atualizador de firmware conferem se o software usado para atualização não foi adulterado." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "Atualizações de firmware" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "Utilitário de Firmware" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "Proteção contra gravação de firmware" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "Bloqueio de proteção contra gravação de firmware" #. TRANSLATORS: longer description msgid "Firmware Write Protection protects device firmware memory from being tampered with." msgstr "Proteção contra gravação de firmware protege a memória de firmware de dispositivos contra adulteração." #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "Atestação de firmware" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "O firmware já está bloqueado" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "O firmware ainda não está bloqueado" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "Os metadados do firmware não foram atualizados a %u dia e podem não estar atualizados." msgstr[1] "Os metadados do firmware não foram atualizados a %u dias e podem não estar atualizados." msgstr[2] "Os metadados do firmware não foram atualizados a %u dias e podem não estar atualizados." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "Atualizações de firmware" #. TRANSLATORS: user needs to run a command, %1 is 'fwupdmgr unlock' #, c-format msgid "Firmware updates disabled; run '%s' to enable" msgstr "Atualizações do firmware estão desabilitadas; execute '%s' para habilitar" #. TRANSLATORS: command description msgid "Fix a specific host security attribute" msgstr "Corrige um atributo específico de segurança do host" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fix reverted successfully" msgstr "Correção revertida com sucesso" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fixed successfully" msgstr "Corrigido com sucesso" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "Opções" #. TRANSLATORS: command line option msgid "Force the action by relaxing some runtime checks" msgstr "Força a ação relaxando alguns verificações em tempo de execução" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Encontrado" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "Criptografia de disco completo detectada" #. TRANSLATORS: we might ask the user the recovery key when next booting #. Windows msgid "Full disk encryption secrets may be invalidated when updating" msgstr "Os segredos de criptografia de disco completa podem ser invalidados ao atualizar" #. TRANSLATORS: Title: if the part has been fused msgid "Fused Platform" msgstr "Plataforma fundida" #. TRANSLATORS: Title: if the part has been fused msgid "Fused platform" msgstr "Plataforma fundida" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" msgstr[1] "GUIDs" msgstr[2] "GUIDs" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "GUID|DEVICE-ID" msgstr "GUID|ID-DISPOSITIVO" msgid "Get BIOS settings" msgstr "Obter as configurações da BIOS" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "Obtém todas as opções de dispositivo suportadas pelo fwupd" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Obtém todos os dispositivos que oferecem suporte às atualizações de firmware" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "Obtém todos os plugins registrados com o sistema" #. TRANSLATORS: command description msgid "Get all known version formats" msgstr "Obtém todos os formatos de versão conhecidos" #. TRANSLATORS: command description msgid "Get device report metadata" msgstr "Obtém metadados de relatório do dispositivo" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Obtém detalhes sobre um arquivo de firmware" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "Obtém os remotos configurados" #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "Obtém os atributos de segurança do host" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "Obtém a lista de firmware aprovado" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "Obtém a lista de firmwares bloqueados" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Obtém a lista de atualizações para os hardwares conectados" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "Obtém os lançamentos para um dispositivo" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Obtém os resultados da última atualização" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "ARQUIVO-HWIDS" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "O hardware está aguardando para ser reconectado" #. TRANSLATORS: the release urgency msgid "High" msgstr "Alta" #. TRANSLATORS: title for host security events msgid "Host Security Events" msgstr "Eventos de segurança do host" #. TRANSLATORS: error message for unsupported feature #, c-format msgid "Host Security ID (HSI) is not supported" msgstr "ID de segurança do host (HSI) não suportado" #. TRANSLATORS: success, so say thank you to the user msgid "Host Security ID attributes uploaded successfully, thanks!" msgstr "Atributos de ID de segurança do host enviados com sucesso, obrigado!" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "ID de segurança do host:" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX" msgstr "ÃNDICE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX KEY [VALUE]" msgstr "ÃNDICE CHAVE [VALOR]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX NAME TARGET [MOUNTPOINT]" msgstr "ÃNDICE NOME ALVO [PONTO-DE-MONTAGEM]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX1,INDEX2" msgstr "ÃNDICE1,ÃNDICE2" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INHIBIT-ID" msgstr "ID-INIBIÇÃO" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "Proteção de IOMMU" #. TRANSLATORS: longer description msgid "IOMMU Protection prevents connected devices from accessing unauthorized parts of system memory." msgstr "A proteção IOMMU impede que dispositivos conectados acessem partes não autorizadas da memória do sistema." #. TRANSLATORS: HSI event title msgid "IOMMU device protection disabled" msgstr "Proteção de dispositivo IOMMU desabilitada" #. TRANSLATORS: HSI event title msgid "IOMMU device protection enabled" msgstr "Proteção de dispositivo IOMMU habilitada" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "Ocioso…" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "Ignora verificações estritas de SSL ao baixar arquivos" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "Ignora falhas de soma de verificação de firmware" #. TRANSLATORS: command line option msgid "Ignore firmware hardware mismatch failures" msgstr "Ignora falhas de incompatibilidade de hardware de firmware" #. TRANSLATORS: command line option msgid "Ignore non-critical firmware requirements" msgstr "Ignora requisitos não críticos de firmware" #. TRANSLATORS: try to help msgid "Ignoring SSL strict checks, to do this automatically in the future export DISABLE_SSL_STRICT in your environment" msgstr "Ignorando as verificações estritas de SSL; para fazer isso automaticamente no futuro, exporte DISABLE_SSL_STRICT em seu ambiente" #. TRANSLATORS: show the user a generic image that can be themed msgid "Image" msgstr "Imagem" #. TRANSLATORS: show the user a random image from the internet msgid "Image (custom)" msgstr "Imagem (personalizada)" #. TRANSLATORS: the inhibit ID is a short string like dbus-123456 #, c-format msgid "Inhibit ID is %s." msgstr "ID de inibição é %s." #. TRANSLATORS: command description msgid "Inhibit the system to prevent upgrades" msgstr "Inibe o sistema para evitar atualizações" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "Duração de instalação" #. TRANSLATORS: command description msgid "Install a firmware file in cabinet format on this hardware" msgstr "Instala um arquivo de firmware em formato gabinete (cabinet) neste hardware" #. TRANSLATORS: command description msgid "Install a raw firmware blob on a device" msgstr "Instala um firmware raw em um dispositivo" #. TRANSLATORS: command description msgid "Install a specific firmware file on all devices that match" msgstr "Instala um arquivo de firmware específico em todos os dispositivos que corresponderem" #. TRANSLATORS: command description msgid "Install a specific firmware on a device, all possible devices will also be installed once the CAB matches" msgstr "Instala um firmware específico em um dispositivo, todos os dispositivos possíveis também serão instalados assim que o CAB corresponder" msgid "Install old version of signed system firmware" msgstr "Instalar versão antiga do firmware do sistema assinado" msgid "Install old version of unsigned system firmware" msgstr "Instalar a versão antiga do firmware do sistema não assinado" msgid "Install signed device firmware" msgstr "Instalar firmware assinado no dispositivo" msgid "Install signed system firmware" msgstr "Instalar firmware assinado no sistema" msgid "Install unsigned device firmware" msgstr "Instalar firmware não assinado no dispositivo" msgid "Install unsigned system firmware" msgstr "Instalar firmware não assinado no sistema" #. TRANSLATORS: stay on one firmware version unless the new version is #. explicitly #. * specified msgid "Installing a specific release is explicitly required" msgstr "Instalação de uma versão específica é explicitamente exigida" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "Instalando atualização de firmware…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "Instalando em %s…" #. TRANSLATORS: if it breaks, you get to keep both pieces msgid "Installing this update may also void any device warranty." msgstr "A instalação desta atualização também pode anular qualquer garantia do dispositivo." #. TRANSLATORS: The BIOS setting only accepts integers in a fixed range msgid "Integer" msgstr "Inteiro" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM Protected" msgstr "ACM protegido com Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "ACM protegido com Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard Error Policy" msgstr "Política de erros da Intel BootGuard" msgid "Intel BootGuard Error Policy ensures the device does not continue to start if its device software has been tampered with." msgstr "A política de erro do Intel BootGuard garante que o dispositivo não continue iniciando se o software do dispositivo tiver sido adulterado." #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard Fuse" msgstr "Fuse de Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "Fuse programável (OTP) do Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard Verified Boot" msgstr "Inicialização verificada por Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Política de erro do Intel BootGuard" #. TRANSLATORS: longer description msgid "Intel BootGuard prevents unauthorized device software from operating when the device is started." msgstr "O Intel BootGuard impede que software de dispositivo não autorizado opere quando o dispositivo é iniciado." #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Inicialização verificada com Intel BootGuard" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS Mitigation" msgstr "Mitigação da Intel GDS" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS mitigation" msgstr "Mitigação da Intel GDS" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Manufacturing Mode" msgstr "Modo de fabricação do Mecanismo de Gerenciamento Intel" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the "override" is #. enabled #. * with a jumper -- luckily it is probably not accessible to end users on #. consumer #. * boards msgid "Intel Management Engine Override" msgstr "Substituição de Mecanismo de Gerenciamento Intel" #. TRANSLATORS: longer description msgid "Intel Management Engine Override disables checks for device software tampering." msgstr "Substituição de Mecanismo de Gerenciamento Intel desabilita verificações de adulteração de software do dispositivo." #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Version" msgstr "Versão do Mecanismo de Gerenciamento Intel" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "Dispositivo interno" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "Inválido" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "Argumentos inválidos" #. TRANSLATORS: error message msgid "Invalid arguments, expected GUID" msgstr "Argumentos inválidos, esperava GUID" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX KEY [VALUE]" msgstr "Argumentos inválidos, esperava-se ÃNDICE CHAVE [VALOR]" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX NAME TARGET [MOUNTPOINT]" msgstr "Argumentos inválidos, esperada ÃNDICE NOME ALVO [PONTO-DE-MONTAGEM]" #. TRANSLATOR: This is the error message for #. * incorrect parameter msgid "Invalid arguments, expected an AppStream ID" msgstr "Argumentos inválidos, esperada um ID AppStream" #. TRANSLATORS: error message msgid "Invalid arguments, expected at least ARCHIVE FIRMWARE METAINFO" msgstr "Argumentos inválidos, esperava pelo menos PACOTE FIRMWARE METAINFO" #. TRANSLATORS: error message msgid "Invalid arguments, expected base-16 integer" msgstr "Argumentos inválidos, esperava-se inteiro base 16" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "É desatualização" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "Está no modo gerenciador de boot" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "É atualização" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "Problema" msgstr[1] "Problemas" msgstr[2] "Problemas" #. TRANSLATORS: HSI event title msgid "Kernel is no longer tainted" msgstr "Kernel não está mais contaminado" #. TRANSLATORS: HSI event title msgid "Kernel is tainted" msgstr "Kernel está contaminado" #. TRANSLATORS: HSI event title msgid "Kernel lockdown disabled" msgstr "Lockdown do kernel desabilitado" #. TRANSLATORS: HSI event title msgid "Kernel lockdown enabled" msgstr "Lockdown do kernel habilitado" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "LOCAL" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "Última modificação" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "Menos que um minuto restante" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "Licença" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "Lockdown do kernel Linux" #. TRANSLATORS: longer description msgid "Linux Kernel Lockdown mode prevents administrator (root) accounts from accessing and changing critical parts of system software." msgstr "O modo lockdown do kernel Linux impede que contas de administrador (root) acessem e alterem partes críticas do software do sistema." msgid "Linux Kernel Swap temporarily saves information to disk as you work. If the information is not protected, it could be accessed by someone if they obtained the disk." msgstr "A swap do kernel Linux armazena temporariamente informações no disco enquanto você trabalha. Se as informações não estiverem protegidas, elas podem ser acessadas por alguém que obteve o disco." #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "Verificação do kernel Linux" msgid "Linux Kernel Verification makes sure that critical system software has not been tampered with. Using device drivers which are not provided with the system can prevent this security feature from working correctly." msgstr "A verificação do Kernel Linux garante que o software crítico do sistema não foi adulterado. Usar drivers de dispositivo que não são fornecidos com o sistema pode impedir que esse recurso de segurança funcione corretamente." #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "Swap do Linux" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Linux Vendor Firmware Service (firmware estável)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Linux Vendor Firmware Service (firmware de teste)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Kernel Linux" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Lockdown do kernel Linux" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Swap do Linux" #. TRANSLATORS: command description msgid "List EFI boot files" msgstr "Lista arquivos de inicialização EFI" #. TRANSLATORS: command description msgid "List EFI boot parameters" msgstr "Lista parâmetros de inicialização EFI" #. TRANSLATORS: command description msgid "List EFI variables with a specific GUID" msgstr "Lista variáveis EFi com um GUID específico" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "Lista entradas no dbx" #. TRANSLATORS: command description msgid "List the available firmware GTypes" msgstr "Lista os GTypes de firmware disponível" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "Lista os tipos de firmware disponível" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "Lista arquivos na ESP" #. TRANSLATORS: command description msgid "Load device emulation data" msgstr "Carregar dados de emulação do dispositivo" #. TRANSLATORS: the plugin was created from a .so object, and was not built-in msgid "Loaded from an external module" msgstr "Carregado de um módulo externo" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "Carregando…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "Bloqueado" #. TRANSLATORS: the release urgency msgid "Low" msgstr "Baixa" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refers #. * to the private/public key used to secure loading of firmware msgid "MEI Key Manifest" msgstr "Manifesto de chave MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refer #. * to the private/public key used to secure loading of firmware msgid "MEI key manifest" msgstr "Manifesto de chave de MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "Modo de fabricação do MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "Substituição de MEI" msgid "MEI version" msgstr "versão MEI" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "Habilita manualmente plugins específicos" #. TRANSLATORS: longer description msgid "Manufacturing Mode is used when the device is manufactured and security features are not yet enabled." msgstr "O modo de fabricação é usado quando o dispositivo foi fabricado e os recursos de segurança ainda não estão habilitados." #. TRANSLATORS: Longest valid string for BIOS setting msgid "Maximum length" msgstr "Comprimento máximo" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "Valor máximo" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "Média" #. TRANSLATORS: ask the user to do a simple task which should be translated msgid "Message" msgstr "Mensagem" #. TRANSLATORS: ask the user a question, and it will not be translated msgid "Message (custom)" msgstr "Mensagem (personalizada)" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "Assinatura de metadados" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "URI de metadados" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "Metadados podem ser obtidos do Linux Vendor Firmware Service." #. TRANSLATORS: error message for a user who ran fwupdmgr #. * refresh recently -- %1 is '--force' #, c-format msgid "Metadata is up to date; use %s to refresh again." msgstr "Os metadados estão atualizados; use %s para atualizar novamente." #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "Versão mínima" #. TRANSLATORS: Shortest valid string for BIOS setting msgid "Minimum length" msgstr "Comprimento mínimo" #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "Valor mínimo" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "Modifica um valor de configuração do daemon" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "Modifica um remoto dado" msgid "Modify a configured remote" msgstr "Modificar um remoto configurado" msgid "Modify daemon configuration" msgstr "Modificar a configuração do daemon" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "Monitora o daemon por eventos" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "Monta a ESP" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "Precisa de uma reinicialização após a instalação" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "Precisa reiniciar" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "Precisa de desligamento após a instalação" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "Nova versão" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "Nenhuma ação especificada!" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "Nenhuma desatualização para %s" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "Nenhum ID de firmware encontrado" #. TRANSLATORS: nothing found msgid "No firmware found" msgstr "Nenhum firmware encontrado" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Nenhum periférico com capacidade de atualização de firmware foi detectado" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "Nenhum lançamento disponível" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "Nenhum remoto está atualmente habilitado, então nenhum metadado está disponível." #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "Nenhum remoto disponível" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "Nenhum dispositivo atualizável" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "Nenhuma atualização disponível" msgid "No updates available for remaining devices" msgstr "Nenhuma atualização disponível para os dispositivos restantes" #. TRANSLATORS: error message #, c-format msgid "No volume matched %s" msgstr "Nenhum volume correspondeu a %s" #. TRANSLATORS: version cannot be installed due to policy msgid "Not approved" msgstr "Não aprovada" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "Não encontrado" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "Não suportado" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "OK" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "OK!" #. TRANSLATORS: the firmware old version msgid "Old version" msgstr "Versão antiga" #. TRANSLATORS: command line option msgid "Only install onto emulated devices" msgstr "Só instala em dispositivos emulados" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "Mostra apenas valor único de PCR" #. TRANSLATORS: command line option msgid "Only use peer-to-peer networking when downloading files" msgstr "Usa somente rede ponto a ponto ao baixar arquivos" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "Apenas atualizações de versão são permitidas" #. TRANSLATORS: command line option msgid "Output in JSON format (disables all interactive prompts)" msgstr "Saída no formato JSON (desabilita todos os prompts interativos)" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "Substitui o caminho padrão da ESP" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Firmware" msgstr "Firmware de P2P" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Metadata" msgstr "Metadados de P2P" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "CAMINHO" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "Analisa e mostra detalhes sobre um arquivo de firmware" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "Analisando a atualização de dbx…" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "Analisando o dbx do sistema…" #. TRANSLATORS: remote filename base msgid "Password" msgstr "Senha" #. TRANSLATORS: command description msgid "Patch a firmware blob at a known offset" msgstr "Corrige um blob de firmware em um deslocamento conhecido" msgid "Payload" msgstr "Carga" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "Pendente" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "Efetuar a operação?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "Depuração de plataforma" #. TRANSLATORS: longer description msgid "Platform Debugging allows device security features to be disabled. This should only be used by hardware manufacturers." msgstr "A depuração de plataforma permite que recursos de segurança do dispositivo sejam desabilitados. Isso deve ser usado somente por fabricantes de hardware." #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "Depuração da plataforma" #. TRANSLATORS: 'recovery key' here refers to a code, rather than a physical #. metal thing msgid "Please ensure you have the volume recovery key before continuing." msgstr "Certifique-se de ter a chave de recuperação de volume antes de continuar." #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "Por favor, insira um número de 0 a %u: " #. TRANSLATORS: the user isn't reading the question -- %1 is 'Y' and %2 is #. * 'N' #, c-format msgid "Please enter either %s or %s: " msgstr "Por favor, insira %s ou %s: " #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "Dependências de plugin ausentes" #. TRANSLATORS: The plugin is only for testing msgid "Plugin is only for testing" msgstr "O plugin é apenas para testes" #. TRANSLATORS: Possible values for a bios setting msgid "Possible Values" msgstr "Valores possíveis" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA Protection" msgstr "Proteção de DMA pré-inicialização" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "Proteção de DMA pré-inicialização" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is disabled" msgstr "Proteção de DMA pré-inicialização está desabilitada" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is enabled" msgstr "Proteção de DMA pré-inicialização está habilitada" #. TRANSLATORS: longer description msgid "Pre-boot DMA protection prevents devices from accessing system memory after being connected to the computer." msgstr "A proteção DMA de pré-inicialização impede que dispositivos acessem a memória do sistema após serem conectados ao computador." #. TRANSLATORS: warning message msgid "Press unlock on the device to continue the update process." msgstr "Pressione o desbloqueio no dispositivo para continuar o processo de atualização." #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "Versão anterior" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Prioridade" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "Problemas" msgid "Proceed with upload?" msgstr "Prosseguir com o envio?" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Processor Security Checks" msgstr "Verificações de segurança do processador" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "Processor rollback protection" msgstr "Proteção contra reversão do processador" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "Privativa" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "ID-REMOTO" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID KEY VALUE" msgstr "ID-REMOTO CHAVE VALOR" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "Somente leitura" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "Lê um blob de firmware de um dispositivo" #. TRANSLATORS: command description msgid "Read a firmware from a device" msgstr "Lê um firmware de um dispositivo" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "Lendo de %s…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Lendo…" #. TRANSLATORS: Plugin is active and in use msgid "Ready" msgstr "Pronto" #. TRANSLATORS: how often we should refresh the metadata msgid "Refresh Interval" msgstr "Intervalo de atualização" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "Renova metadados do servidor remoto" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "Reinstalar %s para %s?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "Reinstala o firmware atual no dispositivo" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "Reinstala firmware em um dispositivo" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "Ramificação de lançamento" #. TRANSLATORS: release attributes msgid "Release Flags" msgstr "Opções da versão" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "ID Versão" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "ID remoto" #. TRANSLATORS: command description msgid "Removes devices to watch for future emulation" msgstr "Remove dispositivos para monitorar para emulação futura" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "URI do relatório" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "Relatado ao servidor remoto" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "O sistema de arquivos efivarfs necessário não foi encontrado" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "O hardware necessário não foi encontrado" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "Requer um gerenciador de boot" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Requer conexão com a Internet" msgid "Reset daemon configuration" msgstr "Redefinir a configuração do daemon" #. TRANSLATORS: sets something in the daemon configuration file msgid "Resets a daemon configuration section" msgstr "Redefine uma seção de configuração de daemon" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Reiniciar agora?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "Reiniciar o daemon para tornar a mudança efetiva?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Reiniciando dispositivo…" #. TRANSLATORS: command description msgid "Retrieve BIOS settings. If no arguments are passed all settings are returned" msgstr "Recupera configurações do BIOS. Se nenhum argumento for passado, todas as configurações serão retornadas" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "Retorna todos os IDs de hardware para a máquina" #. TRANSLATORS: ask the user to upload msgid "Review and upload report now?" msgstr "Rever e enviar o relatório agora?" #. TRANSLATORS: longer description msgid "Rollback Protection prevents device software from being downgraded to an older version that has security problems." msgstr "A proteção contra reversão impede que o software do dispositivo seja rebaixado para uma versão mais antiga que tenha problemas de segurança." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr get-upgrades` #, c-format msgid "Run `%s` for more information." msgstr "Execute `%s` para mais informações." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr sync` #, c-format msgid "Run `%s` to complete this action." msgstr "Execute `%s` para concluir esta ação." #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "Executa a rotina de limpeza da composição de plugin ao usar install-blob" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "Executa a rotina de preparação da composição de plugin ao usar install-blob" #. TRANSLATORS: command description msgid "Run the post-reboot cleanup action" msgstr "Executa uma ação de limpeza pós-reinicialização" #. TRANSLATORS: tell a user how to get information #, c-format msgid "Run without '%s' to see" msgstr "Execute sem \"%s\" para ver" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "O kernel em uso é muito antigo" #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "Sufixo de tempo de execução" #. TRANSLATORS: Software Bill of Materials link msgid "SBOM" msgstr "SBOM" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SECTION" msgstr "SEÇÃO" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING VALUE" msgstr "CONFIGURAÇÃO VALOR" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING1 VALUE1 [SETTING2] [VALUE2]" msgstr "CONFIGURAÇÃO1 VALOR1 [CONFIGURAÇÃO2] [VALOR2]" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "SMAP" msgstr "SMAP" #. TRANSLATORS: Title: Whether firmware is locked down msgid "SMM locked down" msgstr "SMM bloqueado" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "Descritor de BIOS do SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "Região BIOS do SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "Bloqueio de SPI" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "SPI replay protection" msgstr "Proteção contra repetição de SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "Escrita de SPI" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "SPI write protection" msgstr "Proteção contra escrita de SPI" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "SUBSISTEMA DRIVER [ID-DISPOSITIVO|GUID]" #. TRANSLATORS: command description msgid "Save a file that allows generation of hardware IDs" msgstr "Salva um arquivo que permite a geração de IDs de hardware" #. TRANSLATORS: command description msgid "Save device emulation data" msgstr "Salvar dados de emulação do dispositivo" #. TRANSLATORS: key for a offline report filename msgid "Saved report" msgstr "Relatório salvo" #. TRANSLATORS: Scalar increment for integer BIOS setting msgid "Scalar Increment" msgstr "Incremento escalar" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Agendando…" #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "Inicialização segura desabilitada" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "Inicialização segura habilitada" msgid "Security hardening for HSI" msgstr "Reforço de segurança para HSI" #. TRANSLATORS: the %1 is a URL to a wiki page #, c-format msgid "See %s for more details." msgstr "Veja %s para mais detalhes." #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "Veja %s para mais informações." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Dispositivo selecionado" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Volume selecionado" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "Número de série" #. TRANSLATORS: Configured a BIOS setting to a value #, c-format msgid "Set BIOS setting '%s' using '%s'." msgstr "Defina a configuração da BIOS '%s' usando '%s'." #. TRANSLATORS: command description msgid "Set a BIOS setting" msgstr "Define uma configuração da BIOS" msgid "Set one or more BIOS settings" msgstr "Definir as configurações de uma ou mais BIOS" #. TRANSLATORS: command description msgid "Set or remove an EFI boot hive entry" msgstr "Define ou remove uma entrada de hive de inicialização EFI" #. TRANSLATORS: command description msgid "Set the EFI boot next" msgstr "Define a próxima inicialização EFI" #. TRANSLATORS: command description msgid "Set the EFI boot order" msgstr "Define a ordem de inicialização EFI" #. TRANSLATORS: command line option msgid "Set the download retries for transient errors" msgstr "Define as tentativas de download para erros temporários" #. TRANSLATORS: command description msgid "Sets one or more BIOS settings" msgstr "Define as configurações de uma ou mais BIOS" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Definir a lista de firmwares aprovados" #. TRANSLATORS: type of BIOS setting msgid "Setting type" msgstr "Tipo de configuração" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "As configurações serão aplicadas após a reinicialização do sistema" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "Compartilha histórico de firmware com os desenvolvedores" #. TRANSLATORS: command line option msgid "Show all results" msgstr "Mostra todos os resultados" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "Mostra as versões do cliente e do daemon" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "Mostrar informações detalhadas do daemon para um domínio em particular" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "Mostra informações de depuração para todos domínios" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Mostrar opções de depuração" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Mostra dispositivos que não são atualizáveis" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Mostra informações adicionais de depuração" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "Mostra histórico de atualizações de firmware" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "Mostra a versão calculada do dbx" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "Desligar agora?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "Assina um firmware com uma nova chave" msgid "Sign data using the client certificate" msgstr "Assina dados usando o certificado cliente" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "Assina dados usando o certificado cliente" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "Assina os dados enviados com o certificado cliente" msgid "Signature" msgstr "Assinatura" #. TRANSLATORS: firmware is verified on-device the payload using strong crypto msgid "Signed Payload" msgstr "Carga útil assinada" #. TRANSLATORS: file size of the download msgid "Size" msgstr "Tamanho" #. TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM msgid "Some of the platform secrets may be invalidated when updating this firmware." msgstr "Alguns dos segredos da plataforma podem ser invalidados ao atualizar este firmware." #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "Fonte" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "Especifica o arquivo de banco de dados dbx" msgid "Stop the fwupd service" msgstr "Parar o serviço do fwupd" #. TRANSLATORS: The BIOS setting accepts strings msgid "String" msgstr "String" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "Sucesso" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "Todos os dispositivos ativados com sucesso" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "Remoto desabilitado com sucesso" #. TRANSLATORS: comment explaining result of command msgid "Successfully disabled test devices" msgstr "Dispositivos de teste desabilitados com sucesso" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "Baixados com sucesso novos metadados:" #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "Remoto habilitado e atualizado com sucesso" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "Remoto habilitado com sucesso" #. TRANSLATORS: comment explaining result of command msgid "Successfully enabled test devices" msgstr "Dispositivos de teste habilitados com sucesso" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "Firmware instalado com sucesso" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "Valor da configuração modificada com sucesso" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "Remoto modificado com sucesso" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "Metadados atualizados manualmente com sucesso" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration section" msgstr "Seção da configuração redefinida com sucesso" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration values" msgstr "Valores da configuração redefinidos com sucesso" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "Somas de verificação de dispositivo atualizadas com sucesso" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "Enviado com sucesso %u relatório" msgstr[1] "Enviados com sucesso %u relatórios" msgstr[2] "Enviados com sucesso %u relatórios" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "Somas de verificação de dispositivo verificadas com sucesso" #. TRANSLATORS: the device showed up in time #, c-format msgid "Successfully waited %.0fms for device" msgstr "Aguardado com sucesso %.0fms pelo dispositivo" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Resumo" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "Supervisor Mode Access Prevention" msgstr "Prevenção de acesso ao modo supervisor" #. TRANSLATORS: longer description msgid "Supervisor Mode Access Prevention ensures critical parts of device memory are not accessed by less secure programs." msgstr "A prevenção de acesso ao modo supervisor garante que partes críticas da memória do dispositivo não sejam acessadas por programas menos seguros." #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "Suportado" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "CPU suportada" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "Suporte no servidor remoto" #. TRANSLATORS: Title: a better sleep state msgid "Suspend To Idle" msgstr "Suspender para ocioso" #. TRANSLATORS: Title: sleep state msgid "Suspend To RAM" msgstr "Suspender para RAM" #. TRANSLATORS: longer description msgid "Suspend to Idle allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Suspender para ocioso permite que o dispositivo entre rapidamente em modo de suspensão para economizar energia. Enquanto o dispositivo estiver suspenso, sua memória pode ser fisicamente removida e suas informações acessadas." #. TRANSLATORS: longer description msgid "Suspend to RAM allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Suspender para RAM permite que o dispositivo entre rapidamente em modo de suspensão para economizar energia. Enquanto o dispositivo estiver suspenso, sua memória pode ser fisicamente removida e suas informações acessadas." #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "Suspensão para inativo" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "Suspensão para RAM" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "Trocar ramificação de %s para %s ?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "Alterna a ramificação do firmware no dispositivo" #. TRANSLATORS: command description msgid "Sync firmware versions to the chosen configuration" msgstr "Sincroniza versões de firmware com a configuração escolhida" #. TRANSLATORS: Title: Whether firmware is locked down msgid "System Management Mode" msgstr "Modo de gerenciamento do sistema" #. TRANSLATORS: this CLI tool is now preventing system updates msgid "System Update Inhibited" msgstr "Atualização do sistema inibida" #. TRANSLATORS: longer description msgid "System management mode is used by the firmware to access resident BIOS code and data." msgstr "O modo de gerenciamento do sistema é usado pelo firmware para acessar o código e os dados residentes da BIOS." #. TRANSLATORS: as in laptop battery power msgid "System power is too low" msgstr "A energia do sistema está muito baixa" #. TRANSLATORS: as in laptop battery power #, c-format msgid "System power is too low (%u%%, requires %u%%)" msgstr "A energia do sistema está muito baixa (%u%%, requer %u%%)" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "O sistema exige uma fonte de alimentação externa" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "TEXTO" #. TRANSLATORS: longer description msgid "TPM (Trusted Platform Module) is a computer chip that detects when hardware components have been tampered with." msgstr "TPM (Trusted Platform Module) é um chip de computador que detecta quando componentes de hardware foram adulterados." #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "Reconstrução de PCR0 de TPM" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is invalid" msgstr "Reconstrução PCR0 de TPM é inválido" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is now valid" msgstr "Reconstrução PCR0 de TPM é agora válido" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM Platform Configuration" msgstr "Configuração de plataforma TPM" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM Reconstruction" msgstr "Reconstrução de TPM" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM empty PCRs" msgstr "PCRs vazios de TMP" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "Tag" msgstr[1] "Tags" msgstr[2] "Tags" #. TRANSLATORS: we're saving all USB events for emulation msgid "Tagged for emulation" msgstr "Etiquetado para emulação" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "Contaminado" #. show the user the entire data blob msgid "Target" msgstr "Alvo" #. TRANSLATORS: command description msgid "Test a device using a JSON manifest" msgstr "Testa um dispositivo usando um manifesto JSON" #. TRANSLATORS: when the release was tested msgid "Tested" msgstr "Testado" #. TRANSLATORS: the %s is a vendor name, e.g. Lenovo #, c-format msgid "Tested by %s" msgstr "Testado por %s" #. TRANSLATORS: someone we trust has tested this msgid "Tested by trusted vendor" msgstr "Testado por um fornecedor confiável" #. TRANSLATORS: the boot entry was in a legacy format msgid "The EFI boot entry is not in hive format, and shim may not be new enough to read it." msgstr "A entrada de inicialização EFI não está no formato hive e o shim pode não ser novo o suficiente para lê-la." #. TRANSLATORS: try to treat the legacy format as a hive msgid "The EFI boot entry was not in hive format, falling back" msgstr "A entrada de inicialização EFI não estava no formato hive, retrocedendo" #. TRANSLATORS: longer description msgid "The Intel Management Engine Key Manifest must be valid so that the device firmware can be trusted by the CPU." msgstr "O manifesto de chave do Mecanismo de Gerenciamento Intel deve ser válido para que o firmware do dispositivo seja confiável para a CPU." #. TRANSLATORS: longer description msgid "The Intel Management Engine controls device components and needs to have a recent version to avoid security issues." msgstr "O Mecanismo de Gerenciamento Intel controla os componentes do dispositivo e precisa ter uma versão recente para evitar problemas de segurança." #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "O LVFS é um serviço livre que opera como uma entidade legal independente e tem nenhuma conexão com $OS_RELEASE:NAME$. Seu distribuidor pode não ter verificado alguma das atualizações de firmware por compatibilidade com seu sistema ou dispositivos conectados. Todo firmware é fornecido apenas pelo fabricante do equipamento original." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Platform Configuration is used to check whether the device start process has been modified." msgstr "A configuração de plataforma TPM (Trusted Platform Module) é usada para verificar se o processo de inicialização do dispositivo foi modificado." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Reconstruction is used to check whether the device start process has been modified." msgstr "A reconstrução de TPM (Trusted Platform Module) é usada para verificar se o processo de inicialização do dispositivo foi modificado." #. TRANSLATORS: this is more background on a security measurement problem msgid "The TPM PCR0 differs from reconstruction." msgstr "O TPM PCR0 diverge da reconstrução." #. TRANSLATORS: longer description msgid "The UEFI Platform Key is used to determine if device software comes from a trusted source." msgstr "A chave de plataforma UEFI é usada para determinar se o software do dispositivo vem de uma fonte confiável." #. TRANSLATORS: HSI event title msgid "The UEFI certificate store is now up to date" msgstr "O armazenamento de certificados UEFI agora está atualizado" #. TRANSLATORS: longer description msgid "The UEFI db contains the list of valid certificates that can be used to authorize what EFI binaries are allowed to run." msgstr "O banco de dados UEFI contém a lista de certificados válidos que podem ser usados para autorizar quais binários EFI podem ser executados." #. TRANSLATORS: longer description msgid "The UEFI system can set up memory attributes at boot which prevent common exploits from running." msgstr "O sistema UEFI pode configurar atributos de memória na inicialização que impedem a execução de exploits comuns." #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "O daemon carregou código de terceiros e não é mais suportado pelos desenvolvedores upstream!" #. TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name #, c-format msgid "The firmware from %s is not supplied by %s, the hardware vendor." msgstr "O firmware de %s não é fornecido por %s, o fornecedor de hardware." #. TRANSLATORS: try to help msgid "The system clock has not been set correctly and downloading files may fail." msgstr "O relógio do sistema não foi definido corretamente e baixar arquivos pode resultar em falha." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been re-inserted." msgstr "A atualização continuará quando o cabo USB do dispositivo tiver sido reinserido." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged and then re-inserted." msgstr "A atualização continuará quando o cabo USB do dispositivo tiver sido desconectado e reinserido." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged." msgstr "A atualização continuará quando o cabo USB do dispositivo tiver sido desconectado." #. TRANSLATORS: warning message msgid "The update will continue when the device power cable has been removed and re-inserted." msgstr "A atualização continuará quando o cabo de alimentação do dispositivo for removido e reinserido." #. TRANSLATORS: naughty vendor msgid "The vendor did not supply any release notes." msgstr "O fornecedor não forneceu nenhuma nota de lançamento." #. TRANSLATORS: now list devices with unfixed high-priority issues msgid "There are devices with issues:" msgstr "Existem dispositivos com problemas:" #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "Não há arquivos de firmware bloqueados" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "Nenhum firmware aprovado." #. TRANSLATORS: %1 is the current device version number, and %2 is the #. command name, e.g. `fwupdmgr sync` #, c-format msgid "This device will be reverted back to %s when the %s command is performed." msgstr "Este dispositivo será revertido para %s quando o comando %s for executado." #. TRANSLATORS: the vendor did not upload this msgid "This firmware is provided by LVFS community members and is not provided (or supported) by the original hardware vendor." msgstr "Este firmware é fornecido pelos membros da comunidade LVFS e não é fornecido (ou suportado) pelo fornecedor de hardware original." #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "Este pacote não foi validado, ele pode não funcionar corretamente." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "Esse programa só pode funcionar corretamente como root" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "Este remoto contém firmware que não está embargado, mas ainda está sendo testado pelo fornecedor do hardware. Você deve garantir que você tenha uma maneira de desatualizar manualmente o firmware se a atualização do firmware falhar." #. TRANSLATORS: error message msgid "This system doesn't support firmware settings" msgstr "Este sistema não oferece suporte a configurações de firmware" #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "Este sistema possui problemas de tempo de execução de HSI." #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "Este sistema possui um nível de segurança de HSI baixo." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "Esta ferramenta permite que um administrador aplique atualizações de dbx UEFI." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "Esta ferramenta permite que um administrador consulte e controle o daemon fwupd, permitindo que ele execute ações como instalar ou fazer downgrade do firmware." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "Esta ferramenta permite que um administrador use os plugins do fwupd sem estarem instalados no sistema host" #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can add a kernel argument of '%s', but it will only be active after restarting the computer." msgstr "Esta ferramenta pode adicionar um argumento de kernel de '%s', mas só ficará ativo após reiniciar o computador." #. TRANSLATORS: the %1 is a BIOS setting name. #. * %2 and %3 are the values, e.g. "True" or "Windows10" #, c-format msgid "This tool can change the BIOS setting '%s' from '%s' to '%s' automatically, but it will only be active after restarting the computer." msgstr "Esta ferramenta pode alterar a configuração da BIOS '%s' de '%s' para '%s' automaticamente, mas só ficará ativa após reiniciar o computador." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can change the kernel argument from '%s' to '%s', but it will only be active after restarting the computer." msgstr "Esta ferramenta pode alterar o argumento de kernel de '%s' para '%s', mas só ficará ativo após reiniciar o computador." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "Esta ferramenta vai ler e analisar o log de evento de TPM do firmware do sistema." #. TRANSLATORS: the update state of the specific device msgid "Transient failure" msgstr "Falha transitória" #. TRANSLATORS: item is TRUE msgid "True" msgstr "Verdadeiro" #. TRANSLATORS: We verified the metadata against the server msgid "Trusted metadata" msgstr "Metadados confiável" #. TRANSLATORS: We verified the payload against the server msgid "Trusted payload" msgstr "Carga útil confiável" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Tipo" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI Bootservice Variables" msgstr "Variáveis de bootservice UEFI" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition may not be set up correctly" msgstr "Partição ESP de UEFI pode não estar configurada corretamente" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "Partição ESP de UEFI não detectada ou configurada" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI Memory Protection" msgstr "Proteção de memória UEFI" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "Chave de plataforma UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "Secure boot de UEFI" #. TRANSLATORS: longer description msgid "UEFI Secure Boot prevents malicious software from being loaded when the device starts." msgstr "O Secure Boot de UEFI impede que software malicioso seja carregado quando o dispositivo é inicializado." #. TRANSLATORS: longer description msgid "UEFI boot service variables should not be readable from runtime mode." msgstr "As variáveis ​​do serviço de inicialização UEFI não devem ser legíveis no modo de tempo de execução." #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI bootservice variables" msgstr "Variáveis de bootservice UEFI" #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "Atualizações de cápsula UEFI não disponíveis ou habilitadas na configuração do firmware" #. TRANSLATORS: Title: is UEFI db up-to-date msgid "UEFI db" msgstr "db de UEFI" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "Utilitário de dbx UEFI" #. TRANSLATORS: system is not booted in UEFI mode msgid "UEFI firmware can not be updated in legacy BIOS mode" msgstr "Firmware UEFI não pode ser atualizado no modo BIOS legado" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI memory protection" msgstr "Proteção de memória UEFI" #. TRANSLATORS: HSI event title msgid "UEFI memory protection enabled and locked" msgstr "Proteção de memória UEFI habilitada e bloqueada" #. TRANSLATORS: HSI event title msgid "UEFI memory protection enabled but not locked" msgstr "Proteção de memória UEFI habilitada, mas não bloqueada" #. TRANSLATORS: HSI event title msgid "UEFI memory protection is now locked" msgstr "Proteção de memória UEFI está agora bloqueada" #. TRANSLATORS: HSI event title msgid "UEFI memory protection is now unlocked" msgstr "Proteção de memória UEFI está agora desbloqueada" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "Chave de plataforma UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "Secure boot de UEFI" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "Não foi possível conectar ao serviço" #. TRANSLATORS: error message msgid "Unable to find attribute" msgstr "Não foi possível encontrar o atributo" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "Desvincula o driver atual" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "Desbloqueando firmware:" #. TRANSLATORS: command description msgid "Unblocks a specific firmware from being installed" msgstr "Retira impedimento de um firmware específico ser instalado" #. TRANSLATORS: command description msgid "Undo the host security attribute fix" msgstr "Desfaz uma correção de atributo de segurança do host" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "Descriptografado" #. TRANSLATORS: command description msgid "Uninhibit the system to allow upgrades" msgstr "Cancela a inibição do sistema para permitir atualizações" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Desconhecido" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Dispositivo desconhecido" msgid "Unlock the device to allow access" msgstr "Desbloquear o dispositivo para permitir acesso" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "Desbloqueado" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "Desbloqueia o dispositivo para acesso do firmware" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "Desmonta a ESP" #. TRANSLATORS: message shown after device has been marked for emulation msgid "Unplug and replug the device to continue the update process." msgstr "Desconecte e reconecte o dispositivo para continuar o processo de atualização." #. TRANSLATORS: firmware payload is unsigned and it is possible to modify it msgid "Unsigned Payload" msgstr "Carga útil não assinada" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "Sem suporte ao daemon na versão %s, a versão do cliente é %s" #. TRANSLATORS: Suffix: the HSI result msgid "Untainted" msgstr "Descontaminado" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "Atualizável" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Erro de atualização" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "Atualizar imagem" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "Mensagem de atualização" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "Estado da atualização" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "A falha de atualização é um problema conhecido, visite essa URL para mais informações:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Atualizar agora?" #. TRANSLATORS: command description msgid "Update the stored cryptographic hash with current ROM contents" msgstr "Atualiza o hash criptográfico armazenado com o atual conteúdo da ROM" msgid "Update the stored device verification information" msgstr "Atualizar as informações de verificação armazenadas do dispositivo" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "Atualiza os metadados armazenados com o conteúdo atual" #. TRANSLATORS: command description msgid "Updates all specified devices to latest firmware version, or all devices if unspecified" msgstr "Atualiza todos os dispositivos especificados para a versão de firmware mais recente ou todos os dispositivos se não especificados" #. TRANSLATORS: how many local devices can expect updates now #, c-format msgid "Updates have been published for %u local device" msgid_plural "Updates have been published for %u of %u local devices" msgstr[0] "Atualizações foram publicadas para %u dispositivo local" msgstr[1] "Atualizações foram publicadas para %u de %u dispositivos locais" msgstr[2] "Atualizações foram publicadas para %u de %u dispositivos locais" msgid "Updating" msgstr "Atualizando" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "Atualizando %s…" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "Atualizar %s de %s para %s?" #. TRANSLATORS: ask the user to upload msgid "Upload data now?" msgstr "Enviar dados agora?" #. TRANSLATORS: command description msgid "Upload the list of updatable devices to a remote server" msgstr "Envia a lista de dispositivos atualizáveis ​​para um servidor remoto" #. TRANSLATORS: ask the user to share, %s is something #. * like: "Linux Vendor Firmware Service" #, c-format msgid "Upload these anonymous results to the %s to help other users?" msgstr "Enviar esses resultados anônimos para %s para ajudar outros usuários?" #. TRANSLATORS: explain why we want to upload #, c-format msgid "Uploading a device list allows the %s team to know what hardware exists, and allows us to put pressure on vendors that do not upload firmware updates for their hardware." msgstr "O envio de uma lista de dispositivos permite que a equipe %s saiba qual hardware existe e nos permite pressionar os fornecedores que não carregam atualizações de firmware para seus hardwares." #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "O envio de relatórios de firmware ajuda os fornecedores de hardware a identificar rapidamente atualizações com falha e êxito em dispositivos reais." #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "Urgência" #. TRANSLATORS: explain how to get help, %1 is #. * 'fwupdtool --help' #. TRANSLATORS: explain how to get help, #. * where $1 is something like 'fwupdmgr --help' #, c-format msgid "Use %s for help" msgstr "Use %s para ajuda" #. TRANSLATORS: CTRL^C [holding control, and then pressing C] will exit the #. program msgid "Use CTRL^C to cancel." msgstr "Use CTRL^C para cancelar." #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "O usuário foi notificado" #. TRANSLATORS: remote filename base msgid "Username" msgstr "Nome de usuário" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "VERSION1 VERSION2 [FORMAT]" msgstr "VERSÃO1 VERSÃO2 [FORMATO]" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "Válido" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "Validando conteúdo da ESP…" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "Variação" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "Fornecedor" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "Verificando…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Versão" #. TRANSLATORS: the fwupd version the release was tested on msgid "Version[fwupd]" msgstr "Versão[fwupd]" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "AVISO" #. TRANSLATORS: command description msgid "Wait for a device to appear" msgstr "Aguarda até que um dispositivo apareça" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "Aguardando…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "Monitora alterações no hardware" #. TRANSLATORS: check various UEFI and ACPI tables are unchanged after the #. update msgid "Will measure elements of system integrity around an update" msgstr "Serão medidos os elementos de integridade do sistema em torno de uma atualização" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "Escrevendo arquivo:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Escrevendo…" #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from a recovery or installation disk, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "Você deve se certificar de que se sente confortável restaurando a configuração de um disco de recuperação ou instalação, pois essa alteração pode fazer com que o sistema não inicialize no Linux ou causar outra instabilidade no sistema." #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from the system firmware setup, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "Você deve se certificar de que se sente confortável restaurando a configuração do firmware do sistema, pois essa alteração pode fazer com que o sistema não inicialize no Linux ou causar outra instabilidade no sistema." #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "Seu distribuidor pode não ter verificado nenhuma das atualizações de firmware por compatibilidade com seu sistema ou dispositivos conectados." #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "Seu hardware pode ser danificado usando este firmware e instalar esta versão pode anular qualquer garantia com %s." #. TRANSLATORS: BKC is the industry name for the best known configuration and #. is a set #. * of firmware that works together #, c-format msgid "Your system is set up to the BKC of %s." msgstr "Seu sistema está configurado para o BKC de %s." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[APPSTREAM_ID]" msgstr "[ID_APPSTREAM]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[SOMA-VERIFICAÇÃO]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[ID-DISPOSITIVO|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[ID-DISPOSITIVO|GUID] [RAMIFICAÇÃO]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [VERSION]" msgstr "[ID-DISPOSITIVO|GUID] [VERSÃO]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE]" msgstr "[DISPOSITIVO]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILE FILE_SIG REMOTE-ID]" msgstr "[ARQUIVO ARQUIVO-ASSINATURA ID-REMOTO]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[NOME-DE-ARQUIVO1] [NOME-DE-ARQUIVO2]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FWUPD-VERSION]" msgstr "[VERSÃO-FWUPD]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[REASON] [TIMEOUT]" msgstr "[MOTIVO] [TEMPO-LIMITE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SECTION] KEY VALUE" msgstr "[SEÇÃO] CHAVE VALOR" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2] [--no-authenticate]" msgstr "[CONFIGURAÇÃO1] [CONFIGURAÇÃO2] [--no-authenticate]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2]..." msgstr "[CONFIGURAÇÃO1] [CONFIGURAÇÃO2]..." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[ARQUIVO-SMBIOS|ARQUIVO-HWIDS]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "padrão" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "Utilitário de registro de eventos TPM do fwupd" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "Plugins do fwupd" fwupd-2.0.10/po/ro.po000066400000000000000000004016221501337203100143150ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Remus-Gabriel Chelu , 2024-2025 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Romanian (http://app.transifex.com/freedesktop/fwupd/language/ro/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: ro\n" "Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "%.0f minut rămas" msgstr[1] "%.0f minute rămase" msgstr[2] "%.0f de minute rămase" #. TRANSLATORS: BMC refers to baseboard management controller which #. * is the device that updates all the other firmware on the system #, c-format msgid "%s BMC Update" msgstr "Actualizarea BMC %s" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "Actualizarea bateriei %s" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "Actualizarea microcodului CPU %s" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "Actualizarea camerei/aparatului foto %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "Actualizarea configuraÈ›iei %s" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "Actualizarea ME pentru consumatori a %s" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "Actualizarea controlorului %s" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "Actualizarea ME corporativă pentru %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "Actualizarea dispozitivului %s" #. TRANSLATORS: Video Display refers to the laptop internal display or #. * external monitor #, c-format msgid "%s Display Update" msgstr "Actualizarea ecranului %s" #. TRANSLATORS: Dock refers to the port replicator hardware laptops are #. * cradled in, or lowered onto #, c-format msgid "%s Dock Update" msgstr "Actualizarea docului %s" #. TRANSLATORS: drive refers to a storage device, e.g. SATA disk #, c-format msgid "%s Drive Update" msgstr "Actualizarea unității %s" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "Actualizarea controlorului încorporat %s" #. TRANSLATORS: a device that can read your fingerprint pattern #, c-format msgid "%s Fingerprint Reader Update" msgstr "Actualizarea cititorului de amprente digitale %s" #. TRANSLATORS: flash refers to solid state storage, e.g. UFS or eMMC #, c-format msgid "%s Flash Drive Update" msgstr "Actualizarea unității flash %s" #. TRANSLATORS: GPU refers to a Graphics Processing Unit, e.g. #. * the "video card" #, c-format msgid "%s GPU Update" msgstr "Actualizarea GPU %s" #. TRANSLATORS: a large pressure-sensitive drawing area typically used #. * by artists and digital artists #, c-format msgid "%s Graphics Tablet Update" msgstr "Actualizarea tabletei grafice %s" #. TRANSLATORS: two miniature speakers attached to your ears #, c-format msgid "%s Headphones Update" msgstr "Actualizarea căștilor auriculare %s" #. TRANSLATORS: headphones with an integrated microphone #, c-format msgid "%s Headset Update" msgstr "Actualizarea căștilor cu microfon %s" #. TRANSLATORS: an input device used by gamers, e.g. a joystick #, c-format msgid "%s Input Controller Update" msgstr "Actualizare pentru controlorul de intrare al %s" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "Actualizarea tastaturii %s" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "Actualizarea ME pentru %s" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "Actualizarea mouse-ului %s" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "Actualizarea interfeÈ›ei de reÈ›ea %s" #. TRANSLATORS: SSD refers to a Solid State Drive, e.g. non-rotating #. * SATA or NVMe disk #, c-format msgid "%s SSD Update" msgstr "Actualizarea discului SSD %s" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "Actualizarea controlorului de stocare %s" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "Actualizarea sistemului %s" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "Actualizarea TPM %s" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "Actualizarea controlorului Thunderbolt %s" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "Actualizarea touchpad-ului %s" #. TRANSLATORS: Dock refers to the port replicator device connected #. * by plugging in a USB cable -- which may or may not also provide power #, c-format msgid "%s USB Dock Update" msgstr "Actualizarea docului USB %s" #. TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth #. * device that stays in the USB port so the wireless peripheral works #, c-format msgid "%s USB Receiver Update" msgstr "Actualizarea receptorului USB %s" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "Actualizarea %s" #. TRANSLATORS: warn the user before updating, %1 is a device name #, c-format msgid "%s and all connected devices may not be usable while updating." msgstr "Este posibil ca %s È™i toate dispozitivele conectate să nu poată fi utilizate în timpul actualizării." #. TRANSLATORS: %1 refers to some kind of security test, e.g. "Encrypted RAM". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s appeared: %s" msgstr "%s a apărut: %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "UEFI platform #. key". #. * %2 and %3 refer to results value, e.g. "Valid" and "Invalid" #, c-format msgid "%s changed: %s → %s" msgstr "%s a fost modificat: %s → %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "SPI BIOS #. region". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s disappeared: %s" msgstr "%s a dispărut: %s" #. TRANSLATORS: the device has a reason it can't update, e.g. laptop lid #. closed #, c-format msgid "%s is not currently updatable" msgstr "%s nu este actualizabil în prezent" #. TRANSLATORS: %1 is a device name, e.g. "ThinkPad Universal #. * ThunderBolt 4 Dock" and %2 is "fwupdmgr activate" #, c-format msgid "%s is pending activation; use %s to complete the update." msgstr "%s este în aÈ™teptarea activării; utilizaÈ›i %s pentru a finaliza actualizarea." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "Modul de fabricaÈ›ie %s" #. TRANSLATORS: warn the user before #. * updating, %1 is a device name #, c-format msgid "%s must remain connected for the duration of the update to avoid damage." msgstr "%s trebuie să rămână conectat pe durata actualizării pentru a evita deteriorarea." #. TRANSLATORS: warn the user before updating, %1 is a machine #. * name #, c-format msgid "%s must remain plugged into a power source for the duration of the update to avoid damage." msgstr "%s trebuie să rămână conectat la o sursă de alimentare pe durata actualizării pentru a evita deteriorarea." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "Se suprascrie %s" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "versiunea %s" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u zi" msgstr[1] "%u zile" msgstr[2] "%u de zile" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "%u dispozitiv are o actualizare de firmware disponibilă." msgstr[1] "%u dispozitive au o actualizare de firmware disponibilă." msgstr[2] "%u de dispozitive au o actualizare de firmware disponibilă." #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device is not the best known configuration." msgid_plural "%u devices are not the best known configuration." msgstr[0] "%u dispozitiv nu este la cea mai bună configuraÈ›ie cunoscută." msgstr[1] "%u dispozitive nu sunt la cea mai bună configuraÈ›ie cunoscută." msgstr[2] "%u de dispozitive nu sunt la cea mai bună configuraÈ›ie cunoscută." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u oră" msgstr[1] "%u ore" msgstr[2] "%u de ore" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minut" msgstr[1] "%u minute" msgstr[2] "%u de minute" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u secundă" msgstr[1] "%u secunde" msgstr[2] "%u de secunde" #. TRANSLATORS: device tests can be specific to a CPU type #, c-format msgid "%u test was skipped" msgid_plural "%u tests were skipped" msgstr[0] "%u test a fost omis" msgstr[1] "%u teste au fost omise" msgstr[2] "%u de teste au fost omise" #. TRANSLATORS: first percentage is current value, 2nd percentage is the #. * lowest limit the firmware update is allowed for the update to happen #, c-format msgid "%u%% (threshold %u%%)" msgstr "%u%% (pragul este de %u%%)" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(învechit)" #. TRANSLATORS: HSI event title msgid "A TPM PCR is now an invalid value" msgstr "Un PCR TPM are acum o valoare nevalidă" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "AMD Firmware Replay Protection" msgstr "ProtecÈ›ie la reluare a firmware-ului AMD" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "AMD Firmware Write Protection" msgstr "ProtecÈ›ie la scriere a firmware-ului AMD" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "AMD Secure Processor Rollback Protection" msgstr "ProtecÈ›ie de restaurare securizată la starea anterioară a procesorului AMD" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "ARCHIVE FIRMWARE METAINFO [FIRMWARE] [METAINFO] [JCATFILE]" msgstr "ARHIVÄ‚ FIRMWARE METAINFO [FIRMWARE] [METAINFO] [ FIȘIER-JCAT]" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "AcÈ›iune necesară:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "Activează dispozitivele" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "Activează dispozitivele în aÈ™teptare" msgid "Activate the new firmware on the device" msgstr "Activează noul firmware pe dispozitiv" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "Se activează actualizarea firmware-ului" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "Se activează actualizarea firmware-ului pentru" #. TRANSLATORS: command description msgid "Adds devices to watch for future emulation" msgstr "Adaugă dispozitive de monitorizat pentru emulare viitoare" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Vârstă" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "AcceptaÈ›i È™i activaÈ›i depozitul ca sursă de software?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Alias pentru %s" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are now valid" msgstr "Toate PCR-urile TPM sunt acum valide" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are valid" msgstr "Toate PCR-urile TPM sunt valide" #. TRANSLATORS: an application is preventing system updates msgid "All devices are prevented from update by system inhibit" msgstr "Toate dispozitivele sunt împiedicate să se actualizeze prin inhibarea sistemului" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "Toate dispozitivele de acelaÈ™i tip vor fi actualizate în acelaÈ™i timp" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "PermiteÈ›i retrogradarea versiunilor de firmware" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "Permite reinstalarea versiunilor de firmware existente" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "Permite schimbarea ramurii firmware" #. TRANSLATORS: error message msgid "Already exists, and no --force specified" msgstr "Există deja È™i nu este specificată opÈ›iunea „--forceâ€" #. TRANSLATORS: is not the main firmware stream msgid "Alternate branch" msgstr "Ramură alternativă" #. TRANSLATORS: another application is updating the device already msgid "An update is in progress" msgstr "O actualizare este în curs de desfășurare" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "O actualizare necesită o repornire pentru a fi finalizată." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "O actualizare necesită oprirea sistemului pentru a fi finalizată." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Răspunde afirmativ la toate întrebările" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Aplică actualizarea chiar È™i atunci când nu este recomandată" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Aplică fiÈ™ierele de actualizare" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "Se aplică actualizarea…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Firmware aprobat:" msgstr[1] "Firmware aprobate:" msgstr[2] "Firmware aprobate:" #. TRANSLATORS: command description msgid "Asks the daemon to quit" msgstr "Solicită demonului să termine" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "AtaÈ™ează în modul firmware" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Se autentifică…" #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "Sunt necesare detaliile de autentificare" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Autentificarea este necesară pentru a retrograda firmware-ul la o versiune anterioară pe un dispozitiv amovibil" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Autentificarea este necesară pentru a retrograda firmware-ul la o versiune anterioară pe această maÈ™ină" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to enable emulation data collection" msgstr "Autentificarea este necesară pentru a activa colectarea datelor de emulare" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to fix a host security issue" msgstr "Autentificarea este necesară pentru a rezolva o problemă de securitate a gazdei" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to load hardware emulation data" msgstr "Autentificarea este necesară pentru încărcarea datelor de emulare hardware" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "Autentificarea este necesară pentru a modifica parametrii BIOS" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "Autentificarea este necesară pentru a modifica un depozit configurat utilizat pentru actualizările firmware" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "Autentificarea este necesară pentru a modifica configuraÈ›ia daemonului" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "Autentificarea este necesară pentru a citi parametrii BIOS" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to reset daemon configuration to defaults" msgstr "Autentificarea este necesară pentru a restabili configuraÈ›ia demonului la valorile implicite" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to save hardware emulation data" msgstr "Autentificarea este necesară pentru a salva datele de emulare hardware" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "Autentificarea este necesară pentru a defini lista de firmware aprobat" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "Autentificarea este necesară pentru semnarea datelor utilizând certificatul clientului" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to stop the firmware update service" msgstr "Autentificarea este necesară pentru a opri serviciul de actualizare a firmware-ului" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "Autentificarea este necesară pentru a trece la noua versiune de firmware" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to undo the fix for a host security issue" msgstr "Autentificarea este necesară pentru a anula remedierea unei probleme de securitate a gazdei" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Autentificarea este necesară pentru a debloca un dispozitiv" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Autentificarea este necesară pentru a actualiza firmware-ul pe un dispozitiv amovibil" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Autentificarea este necesară pentru a actualiza firmware-ul acestei maÈ™ini" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "Autentificarea este necesară pentru a actualiza sumele de control stocate pentru dispozitiv" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "Raportare automată" #. TRANSLATORS: we can auto-uninhibit after a timeout #, c-format msgid "Automatically uninhibiting in %ums…" msgstr "Dezinhibare automată în %ums..." #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "ÃŽncărcaÈ›i automat de fiecare dată?" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS Firmware Updates" msgstr "Actualizări ale firmware-ului BIOS" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS Rollback Protection" msgstr "ProtecÈ›ie de restaurare la starea anterioară a BIOS-ului" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS firmware updates" msgstr "Actualizări ale firmware-ului BIOS" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS rollback protection" msgstr "ProtecÈ›ie de restaurare la starea anterioară a BIOS-ului" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "Actualizări BIOS livrate prin LVFS sau Windows Update" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "BUILDER-XML FILENAME-DST" msgstr "CONSTRUCTOR-XML NUME_FIȘIER-DEST" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "Baterie" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "Asociază noul controlor de nucleu" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "FiÈ™iere firmware blocate:" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "Versiune blocată" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "Se blochează firmware-ul:" #. TRANSLATORS: command description msgid "Blocks a specific firmware from being installed" msgstr "Blochează instalarea unui firmware specificat" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "Versiunea încărcătorului de pornire" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "Ramura" #. TRANSLATORS: command description msgid "Build a cabinet archive from a firmware blob and XML metadata" msgstr "ConstruieÈ™te o arhivă de cabinet dintr- o colecÈ›ie (blob) de firmware È™i metadate XML" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "ConstruieÈ™te un fiÈ™ier firmware" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * Utilized by OS means the distribution enabled it msgid "CET OS Support" msgstr "Suport pentru sistemul de operare CET" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * enabled means supported by the processor msgid "CET Platform" msgstr "Platforma CET" #. TRANSLATORS: longer description msgid "CPU Microcode must be updated to mitigate against various information-disclosure security issues." msgstr "Microcodul CPU trebuie actualizat pentru a atenua diverse probleme de securitate legate de divulgarea informaÈ›iilor." #. TRANSLATORS: we can save all device enumeration events for emulation msgid "Can tag for emulation" msgstr "Se poate eticheta pentru emulare" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Anulează" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "OperaÈ›ia a fost anulată" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "Nu se poate aplica deoarece actualizarea dbx a fost deja aplicată." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Modificat" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "Verifică dacă suma de control criptografică corespunde firmware-ului" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Sumă de control" #. TRANSLATORS: get interactive prompt, where branch is the #. * supplier of the firmware, e.g. "non-free" or "free" msgid "Choose branch" msgstr "AlegeÈ›i ramura" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "AlegeÈ›i dispozitivul" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "AlegeÈ›i firmware-ul" #. TRANSLATORS: get interactive prompt msgid "Choose release" msgstr "AlegeÈ›i versiunea" #. TRANSLATORS: get interactive prompt msgid "Choose volume" msgstr "AlegeÈ›i volumul" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "Elimină rezultatele de la ultima actualizare" #. TRANSLATORS: error message msgid "Command not found" msgstr "Comanda nu a fost găsită" #. TRANSLATORS: is not supported by the vendor msgid "Community supported" msgstr "SusÈ›inut de comunitate" #. TRANSLATORS: command description msgid "Compares two versions for equality" msgstr "Compară două versiuni pentru egalitate" #. TRANSLATORS: title prefix for the BIOS settings dialog msgid "Configuration Change Suggested" msgstr "Modificare de configurare sugerată" #. TRANSLATORS: no peeking msgid "Configuration is only readable by the system administrator" msgstr "ConfiguraÈ›ia poate fi citită numai de către administratorul sistemului" #. TRANSLATORS: longer description msgid "Control-Flow Enforcement Technology detects and prevents certain methods for running malicious software on the device." msgstr "Tehnologia de îmbunătățire a controlului fluxului detectează È™i previne anumite metode de rulare a software-ului maliÈ›ios pe dispozitiv." #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology msgid "Control-flow Enforcement Technology" msgstr "Tehnologia de îmbunătățire a controlului fluxului" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "ConverteÈ™te un fiÈ™ier firmware" #. TRANSLATORS: command description msgid "Create an EFI boot entry" msgstr "Creează o intrare de pornire EFI" #. TRANSLATORS: when the update was built msgid "Created" msgstr "Creat" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "Critică" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "Verificarea sumei de control criptografice este disponibilă" #. TRANSLATORS: current value of a BIOS setting msgid "Current Value" msgstr "Valoarea actuală" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "Versiunea curentă" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "ID-DISPOZITIV|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "OpÈ›iuni de depanare" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "Se decomprimă…" #. TRANSLATORS: command description msgid "Delete an EFI boot entry" msgstr "Șterge o intrare de pornire EFI" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Descriere" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "DetaÈ™ează în modul încărcător de pornire" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "Detalii" #. TRANSLATORS: the best known configuration is a set of software that we know #. works #. * well together. In the OEM and ODM industries it is often called a BKC msgid "Deviate from the best known configuration?" msgstr "Vă abateÈ›i de la cea mai bună configuraÈ›ie cunoscută?" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "Fanioane dispozitiv" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "ID dispozitiv" #. TRANSLATORS: description of the device requests msgid "Device Requests" msgstr "CerinÈ›e dispozitiv" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Dispozitiv adăugat:" #. TRANSLATORS: the device is already connected msgid "Device already exists" msgstr "Dispozitivul există deja" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "Capacitatea bateriei dispozitivului este prea mică" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse #, c-format msgid "Device battery power is too low (%u%%, requires %u%%)" msgstr "Capacitatea bateriei dispozitivului este prea mică (%u%%, necesită %u%%)" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "Dispozitivul poate recupera eÈ™ecurile flash" #. TRANSLATORS: lid means "laptop top cover" msgid "Device cannot be updated while the lid is closed" msgstr "Dispozitivul nu poate fi actualizat în timp ce capacul este închis" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Dispozitiv modificat:" #. TRANSLATORS: a version check is required for all firmware msgid "Device firmware is required to have a version check" msgstr "Firmware-ul dispozitivului este necesar pentru a avea o verificare a versiunii" #. TRANSLATORS: emulated means we are pretending to be a different model msgid "Device is emulated" msgstr "Dispozitivul este emulat" #. TRANSLATORS: device cannot be interrupted, for instance taking a phone call msgid "Device is in use" msgstr "Dispozitivul este în uz" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "Dispozitivul este blocat" #. TRANSLATORS: we have two ways of communicating with the device, so we hide #. one msgid "Device is lower priority than an equivalent device" msgstr "Dispozitivul are o prioritate mai mică decât un dispozitiv echivalent" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "Dispozitivul este necesar pentru a instala toate versiunile furnizate" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "Dispozitivul nu este accesibil" #. TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode msgid "Device is unreachable, or out of wireless range" msgstr "Dispozitivul este inaccesibil sau în afara razei de acÈ›iune a semnalului" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "Dispozitivul este utilizabil pe durata actualizării" #. TRANSLATORS: usually this is when we're waiting for a reboot msgid "Device is waiting for the update to be applied" msgstr "Dispozitivul aÈ™teaptă ca actualizarea să fie aplicată" #. TRANSLATORS: success, so say thank you to the user msgid "Device list uploaded successfully, thanks!" msgstr "Lista de dispozitive a fost încărcată cu succes, vă mulÈ›umim!" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Dispozitiv eliminat:" #. TRANSLATORS: as in, wired mains power for a laptop msgid "Device requires AC power to be connected" msgstr "Dispozitivul necesită alimentare cu curent alternativ pentru a fi conectat" #. TRANSLATORS: device does not have a display connected msgid "Device requires a display to be plugged in" msgstr "Dispozitivul necesită conectarea unui ecran" #. TRANSLATORS: The device cannot be updated due to missing vendor's license." msgid "Device requires a software license to update" msgstr "Dispozitivul necesită o licență software pentru actualizare" #. TRANSLATORS: longer description msgid "Device software updates are provided for this device." msgstr "Sunt furnizate actualizări software pentru acest dispozitiv." #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "Actualizări pe etape ale dispozitivului" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "Dispozitivul oferă suport pentru trecerea la o altă ramură de firmware" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "Actualizarea dispozitivului necesită activarea" #. TRANSLATORS: save the old firmware to disk before installing the new one msgid "Device will backup firmware before installing" msgstr "Dispozitivul va face o copie de rezervă a firmware-ului înainte de instalare" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "Dispozitivul nu va reapărea după finalizarea actualizării" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "Dispozitive care au fost actualizate cu succes:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "Dispozitive care nu au fost actualizate corect:" #. TRANSLATORS: message letting the user there is an update #. * waiting, but there is a reason it cannot be deployed msgid "Devices with firmware updates that need user action: " msgstr "Dispozitive cu actualizări de firmware care necesită intervenÈ›ia utilizatorului: " #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "Dispozitive fără actualizări firmware disponibile: " #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "Dispozitive cu cea mai recentă versiune de firmware disponibilă:" #. TRANSLATORS: this is for the device tests msgid "Did not find any devices with matching GUIDs" msgstr "Nu a fost găsit niciun dispozitiv cu GUID-uri potrivite" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "dezactivat" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "Dezactivează un depozit furnizat ca sursă de software" #. TRANSLATORS: command description msgid "Disables virtual testing devices" msgstr "Dezactivează dispozitivele de testare virtuale" #. TRANSLATORS: the OS the release was tested on msgid "Distribution" msgstr "DistribuÈ›ie" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "Nu verifică dacă există metadate vechi" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "Nu verifică dacă există istoric neraportat" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "Nu verifică dacă depozitele de descărcare trebuie să fie activate" #. TRANSLATORS: command line option msgid "Do not check or prompt for reboot after update" msgstr "Nu verifică sau nu solicită repornirea după actualizare" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "Nu include prefixul domeniului de jurnalizare" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "Nu include prefixul marcajului de timp" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "Nu efectuează verificări ale siguranÈ›ei dispozitivului" #. TRANSLATORS: command line option msgid "Do not prompt for devices" msgstr "Nu solicită dispozitive" #. TRANSLATORS: command line option msgid "Do not prompt to fix security issues" msgstr "Nu solicită remedierea problemelor de securitate" #. TRANSLATORS: command line option msgid "Do not search the firmware when parsing" msgstr "Nu caută firmware-ul atunci când analizează" #. TRANSLATORS: warning message shown after update has been scheduled msgid "Do not turn off your computer or remove the AC adaptor while the update is in progress." msgstr "Nu opriÈ›i calculatorul È™i nu scoateÈ›i adaptorul de curent alternativ în timp ce actualizarea este în curs." #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "Nu scrie în baza de date a istoricului" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "ÃŽnÈ›elegeÈ›i consecinÈ›ele schimbării ramurii firmware?" #. TRANSLATORS: ask the user if it's okay to convert, #. * "it" being the data contained in the EFI boot entry msgid "Do you want to convert it now?" msgstr "DoriÈ›i să o convertiÈ›i acum?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "DoriÈ›i să dezactivaÈ›i această caracteristică pentru actualizările viitoare?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "DoriÈ›i să reîmprospătaÈ›i acest depozit de sursă de software acum?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "DoriÈ›i să încărcaÈ›i automat rapoarte pentru actualizările viitoare?" #. TRANSLATORS: command line option msgid "Don't prompt for authentication (less details may be shown)" msgstr "Nu solicită autentificarea (pot fi afiÈ™ate mai puÈ›ine detalii)" #. TRANSLATORS: success msgid "Done!" msgstr "Gata!" #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "RetrogradaÈ›i versiunea %s de la %s la %s?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "Retrogradează versiunea firmware a unui dispozitiv" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "Se retrogradează %s…" #. TRANSLATORS: command description msgid "Download a file" msgstr "Descarcă un fiÈ™ier" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Se descarcă…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "Descarcă datele SMBIOS dintr-un fiÈ™ier" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "Durata" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "EMULATION-FILE [ARCHIVE-FILE]" msgstr "FIȘIER-DE-EMULARE [FIȘIER-ARHIVÄ‚]" #. TRANSLATORS: longer description msgid "Each system should have tests to ensure firmware security." msgstr "Fiecare sistem trebuie să dispună de teste pentru a asigura securitatea firmware-ului." #. TRANSLATORS: command description msgid "Emulate a device using a JSON manifest" msgstr "Emulează un dispozitiv utilizând un manifest JSON" #. TRANSLATORS: this device is not actually real msgid "Emulated" msgstr "Emulat" #. TRANSLATORS: Title: if we are emulating a different host msgid "Emulated host" msgstr "Gazdă emulată" msgid "Enable" msgstr "Activare" msgid "Enable emulation data collection" msgstr "Activează colectarea datelor de emulare" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "ActivaÈ›i noul depozit ca sursă de software?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "ActivaÈ›i acest depozit ca sursă de software?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Activat" #. TRANSLATORS: Plugin is active only if hardware is found msgid "Enabled if hardware matches" msgstr "Activat dacă hardware-ul se potriveÈ™te" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "Activează un depozit furnizat ca sursă de software" #. TRANSLATORS: command description msgid "Enables virtual testing devices" msgstr "Activează dispozitive de testare virtuale" #. TRANSLATORS: longer description msgid "Enabling firmware updates for the BIOS allows fixing security issues." msgstr "Activarea actualizărilor firmware pentru BIOS permite remedierea problemelor de securitate." msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "Activarea acestei funcÈ›ionalități este făcută pe propriul dvs. risc, ceea ce înseamnă că trebuie să contactaÈ›i producătorul echipamentului original cu privire la orice probleme cauzate de aceste actualizări. Numai problemele legate de procesul de actualizare în sine ar trebui să fie depuse la $OS_RELEASE:BUG_REPORT_URL$." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "Activarea acestui depozit se face pe propriul dvs. risc." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Criptat" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "Memorie RAM criptată" msgid "Encrypted RAM makes it impossible for information that is stored in device memory to be read if the memory chip is removed and accessed." msgstr "Memoria RAM criptată face imposibilă citirea informaÈ›iilor stocate în memoria dispozitivului dacă cipul de memorie este scos È™i accesat." #. TRANSLATORS: the vendor is no longer supporting the device msgid "End of life" msgstr "SfârÈ™itul vieÈ›ii utile" #. TRANSLATORS: The BIOS setting can only be changed to fixed values msgid "Enumeration" msgstr "Enumerare" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "Șterge tot istoricul actualizărilor firmware" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Se È™terge…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "Iese după o mică întârziere" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "Iese după ce motorul a fost încărcat" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "Exportă o structură de fiÈ™iere firmware în XML" #. TRANSLATORS: command description msgid "Export firmware history for manual upload" msgstr "Export istoricul firmware pentru încărcare manuală" #. TRANSLATORS: command description msgid "Extract a firmware blob to images" msgstr "Extrage o colecÈ›ie binară de firmware în imagini" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "FIȘIER" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "FIȘIER [ID-DISPOZITIV|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "NUME_FIȘIER" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "NUME_FIȘIER CERTIFICAT CHEIE-PRIVATÄ‚" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "NUME_FIȘIER ID-DISPOZITIV" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME OFFSET DATA [FIRMWARE-TYPE]" msgstr "NUME_FIȘIER POZIÈšIE DATE [TIP-FIRMWARE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "NUME_FIȘIER [ID-DISPOZITIV|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "NUME_FIȘIER [TIP-FIRMWARE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" msgstr "FIȘIER-SURSÄ‚ FIȘIER-DEST [ TIP-FIRMWARE-SURSÄ‚] [TIP-FIRMWARE-DEST]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" msgstr "NUME-FIȘIER|SUMÄ‚-DE-CONTROL1[,SUMÄ‚-DE-CONTROL2][,SUMÄ‚-DE-CONTROL3]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "EÈ™ec" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Aplicarea actualizării a eÈ™uat" #. TRANSLATORS: error message for Windows msgid "Failed to connect to Windows service, please ensure it's running." msgstr "A eÈ™uat conectarea la serviciul Windows, vă rugăm să vă asiguraÈ›i că acesta rulează." #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "Conexiunea cu demonul «fwupd» via d-bus a eÈ™uat" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "A eÈ™uat încărcarea bazei de date dbx locale" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "A eÈ™uat încărcarea bazei de date dbx a sistemului" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "Blocarea a eÈ™uat" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Analizarea argumentelor a eÈ™uat" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "Analizarea fiÈ™ierului a eÈ™uat" #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #, c-format msgid "Failed to parse flags for %s" msgstr "Analizarea opÈ›iunilor pentru „%s†a eÈ™uat" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "A eÈ™uat analizarea bazei de date dbx locale" #. TRANSLATORS: a feature is something like "can show an image" msgid "Failed to set front-end features" msgstr "Configurarea caracteristicilor de interfață a eÈ™uat" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "Validarea conÈ›inutului ESP a eÈ™uat" #. TRANSLATORS: item is FALSE msgid "False" msgstr "Fals" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Identificatorul fiÈ™ierului" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "Semnătura identificatorului de fiÈ™ier" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "Numele fiÈ™ierului sursă" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Nume de fiÈ™ier necesar" #. TRANSLATORS: command line option msgid "Filter with a set of device flags using a ~ prefix to exclude, e.g. 'internal,~needs-reboot'" msgstr "Filtrează cu un set de fanioane de dispozitiv folosind un prefix ~ pentru a exclude, de exemplu „internal,~needs-rebootâ€" #. TRANSLATORS: command line option msgid "Filter with a set of release flags using a ~ prefix to exclude, e.g. 'trusted-release,~trusted-metadata'" msgstr "Filtrează cu un set de fanioane de lansare folosind un prefix ~ pentru a exclude, de exemplu „trusted-release,~trusted-metadataâ€" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware Attestation" msgstr "Atestarea firmware" #. TRANSLATORS: longer description msgid "Firmware Attestation checks device software using a reference copy, to make sure that it has not been changed." msgstr "Atestarea firmware verifică software-ul dispozitivului utilizând o copie de referință, pentru a se asigura că acesta nu a fost modificat." #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "Descriptorul firmware BIOS" #. TRANSLATORS: longer description msgid "Firmware BIOS Descriptor protects device firmware memory from being tampered with." msgstr "Descriptorul firmware BIOS protejează memoria firmware a dispozitivului împotriva modificărilor." #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "Regiunea firmware BIOS" #. TRANSLATORS: longer description msgid "Firmware BIOS Region protects device firmware memory from being tampered with." msgstr "Regiunea firmware BIOS protejează memoria firmware a dispozitivului împotriva modificărilor." #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "Adresa URI de bază pentru firmware" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "Serviciul D-Bus de actualizare a firmware-ului" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Demon de actualizare a firmware-ului" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "Verificarea programului de actualizare a firmware-ului" msgid "Firmware Updater Verification checks that software used for updating has not been tampered with." msgstr "Verificarea programului de actualizare a firmware-ului verifică dacă software-ul utilizat pentru actualizare nu a fost modificat." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "Actualizări firmware" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "Instrument pentru firmware" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "ProtecÈ›ie la scriere a firmware-ului" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "Blocare de protecÈ›ie la scriere a firmware-ului" #. TRANSLATORS: longer description msgid "Firmware Write Protection protects device firmware memory from being tampered with." msgstr "ProtecÈ›ia la scriere a firmware-ului protejează memoria firmware a dispozitivului împotriva modificărilor." #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "Atestarea firmware" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "Firmware-ul este deja blocat" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "Firmware-ul nu este încă blocat" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "Metadatele firmware-ului nu au fost actualizate de %u zi È™i este posibil să nu fie actualizate." msgstr[1] "Metadatele firmware-ului nu au fost actualizate de %u zile È™i este posibil să nu fie actualizate." msgstr[2] "Metadatele firmware-ului nu au fost actualizate de %u de zile È™i este posibil să nu fie actualizate." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "Actualizări firmware" #. TRANSLATORS: user needs to run a command, %1 is 'fwupdmgr unlock' #, c-format msgid "Firmware updates disabled; run '%s' to enable" msgstr "Actualizări firmware dezactivate; executaÈ›i «%s» pentru activare" #. TRANSLATORS: command description msgid "Fix a specific host security attribute" msgstr "Corectează un anumit atribut de securitate al gazdei" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fix reverted successfully" msgstr "Remediere anulată cu succes" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fixed successfully" msgstr "Remediat cu succes" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "Fanioane" #. TRANSLATORS: command line option msgid "Force the action by relaxing some runtime checks" msgstr "ForÈ›ează acÈ›iunea prin relaxarea unor verificări în timpul execuÈ›iei" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Găsit" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "A fost detectată criptarea completă a discului" #. TRANSLATORS: we might ask the user the recovery key when next booting #. Windows msgid "Full disk encryption secrets may be invalidated when updating" msgstr "Secretele de criptare completă a discului pot fi invalidate la actualizare" #. TRANSLATORS: Title: if the part has been fused msgid "Fused Platform" msgstr "Platformă fuzionată" #. TRANSLATORS: Title: if the part has been fused msgid "Fused platform" msgstr "Platformă fuzionată" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" msgstr[1] "GUID-uri" msgstr[2] "GUID-uri" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "GUID|DEVICE-ID" msgstr "GUID|ID-DISPOZITIV" msgid "Get BIOS settings" msgstr "ObÈ›ine configuraÈ›ia BIOS" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "ObÈ›ine toate fanioanele de dispozitiv acceptate de «fwupd»" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "ObÈ›ine toate dispozitivele care acceptă actualizări ale firmware-ului" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "ObÈ›ine toate modulele activate înregistrate în sistem" #. TRANSLATORS: command description msgid "Get all known version formats" msgstr "ObÈ›ine toate formatele de versiune cunoscute" #. TRANSLATORS: command description msgid "Get device report metadata" msgstr "ObÈ›ine metadatele raportului dispozitivului" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "ObÈ›ine detalii despre un fiÈ™ier firmware" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "ObÈ›ine depozitele configurate" #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "ObÈ›ine atributele de securitate ale gazdei" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "ObÈ›ine lista de firmware aprobat" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "ObÈ›ine lista firmware-ului blocat" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "ObÈ›ine lista de actualizări pentru hardware-ul conectat" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "ObÈ›ine versiunile pentru un dispozitiv" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "ObÈ›ine rezultatele de la ultima actualizare" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "FIȘIER-HWIDS" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "Hardware-ul aÈ™teaptă să fie reconectat" #. TRANSLATORS: the release urgency msgid "High" msgstr "ÃŽnaltă" #. TRANSLATORS: title for host security events msgid "Host Security Events" msgstr "Evenimente de securitate ale gazdei" #. TRANSLATORS: error message for unsupported feature #, c-format msgid "Host Security ID (HSI) is not supported" msgstr "ID-ul de securitate al gazdei (HSI) nu este acceptat" #. TRANSLATORS: success, so say thank you to the user msgid "Host Security ID attributes uploaded successfully, thanks!" msgstr "Atributele ID-ului de securitate al gazdei (HSI) au fost încărcate cu succes, vă mulÈ›umim!" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "ID-ul de securitate al gazdei:" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX" msgstr "INDEX" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX KEY [VALUE]" msgstr "INDEX CHEIE [VALOARE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX NAME TARGET [MOUNTPOINT]" msgstr "INDEX NUMELE ÈšINTEI [PUNCT DE MONTARE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX1,INDEX2" msgstr "INDEX1,INDEX2" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INHIBIT-ID" msgstr "ID-INHIBARE" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "ProtecÈ›ia IOMMU" #. TRANSLATORS: longer description msgid "IOMMU Protection prevents connected devices from accessing unauthorized parts of system memory." msgstr "ProtecÈ›ia IOMMU împiedică dispozitivele conectate să acceseze părÈ›i neautorizate ale memoriei sistemului." #. TRANSLATORS: HSI event title msgid "IOMMU device protection disabled" msgstr "ProtecÈ›ia dispozitivului IOMMU este dezactivată" #. TRANSLATORS: HSI event title msgid "IOMMU device protection enabled" msgstr "ProtecÈ›ia dispozitivului IOMMU este activată" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "Inactiv…" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "Ignoră verificările stricte SSL atunci când descarcă fiÈ™iere" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "Ignoră eÈ™ecurile sumei de control a firmware-ului" #. TRANSLATORS: command line option msgid "Ignore firmware hardware mismatch failures" msgstr "Ignoră eÈ™ecurile de nepotrivire firmware hardware" #. TRANSLATORS: command line option msgid "Ignore non-critical firmware requirements" msgstr "Ignoră cerinÈ›ele firmware necritice" #. TRANSLATORS: try to help msgid "Ignoring SSL strict checks, to do this automatically in the future export DISABLE_SSL_STRICT in your environment" msgstr "Se ignoră verificările stricte ale SSL, pentru a face acest lucru automat în viitor exportaÈ›i „DISABLE_SSL_STRICT†în mediul dvs. rulând «export DISABLE_SSL_STRICT»" #. TRANSLATORS: show the user a generic image that can be themed msgid "Image" msgstr "Imagine" #. TRANSLATORS: show the user a random image from the internet msgid "Image (custom)" msgstr "Imagine (personalizată)" #. TRANSLATORS: the inhibit ID is a short string like dbus-123456 #, c-format msgid "Inhibit ID is %s." msgstr "ID-ul de inhibare este %s." #. TRANSLATORS: command description msgid "Inhibit the system to prevent upgrades" msgstr "Inhibă sistemul pentru a preveni actualizările" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "Durata instalării" #. TRANSLATORS: command description msgid "Install a firmware file in cabinet format on this hardware" msgstr "Instalează un fiÈ™ier firmware în format cabinet pe acest hardware" #. TRANSLATORS: command description msgid "Install a raw firmware blob on a device" msgstr "Instalează un „blob†(colecÈ›ie binară) de firmware brut pe un dispozitiv" #. TRANSLATORS: command description msgid "Install a specific firmware file on all devices that match" msgstr "Instalează un fiÈ™ier firmware specific pe toate dispozitivele care se potrivesc" #. TRANSLATORS: command description msgid "Install a specific firmware on a device, all possible devices will also be installed once the CAB matches" msgstr "Instalează firmware-ul specificat pe un dispozitiv, toate dispozitivele posibile vor fi de asemenea instalate dacă conÈ›inutul fiÈ™ierului CAB se potriveÈ™te" msgid "Install old version of signed system firmware" msgstr "Instalează versiunea veche a firmware-ului semnat al sistemului" msgid "Install old version of unsigned system firmware" msgstr "Instalează versiunea veche a firmware-ului nesemnat al sistemului" msgid "Install signed device firmware" msgstr "Instalează firmware-ul semnat al dispozitivului" msgid "Install signed system firmware" msgstr "Instalează firmware de sistem semnat" msgid "Install unsigned device firmware" msgstr "Instalează firmware-ul nesemnat al dispozitivului" msgid "Install unsigned system firmware" msgstr "Instalează firmware de sistem nesemnat" #. TRANSLATORS: stay on one firmware version unless the new version is #. explicitly #. * specified msgid "Installing a specific release is explicitly required" msgstr "Instalarea unei versiuni specifice este necesară în mod explicit" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "Se instalează actualizarea firmware-ului…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "Se instalează pe %s…" #. TRANSLATORS: if it breaks, you get to keep both pieces msgid "Installing this update may also void any device warranty." msgstr "Instalarea acestei actualizări poate anula, de asemenea, orice garanÈ›ie a dispozitivului." #. TRANSLATORS: The BIOS setting only accepts integers in a fixed range msgid "Integer" msgstr "Număr întreg" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM Protected" msgstr "ACM protejat de Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "ACM protejat de Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard Error Policy" msgstr "Politica de eroare Intel BootGuard" msgid "Intel BootGuard Error Policy ensures the device does not continue to start if its device software has been tampered with." msgstr "Politica de erori Intel BootGuard garantează că dispozitivul nu continuă să pornească dacă software-ul său a fost modificat." #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard Fuse" msgstr "Siguranță Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "Siguranță programabilă o singură dată a Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard Verified Boot" msgstr "Pornire verificată Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Politica de eroare Intel BootGuard" #. TRANSLATORS: longer description msgid "Intel BootGuard prevents unauthorized device software from operating when the device is started." msgstr "Intel BootGuard împiedică software-ul neautorizat al dispozitivului să funcÈ›ioneze atunci când dispozitivul este pornit." #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Pornire verificată Intel BootGuard" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS Mitigation" msgstr "Atenuarea vulnerabilității GDS din procesoarele Intel" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS mitigation" msgstr "Atenuarea vulnerabilității GDS din procesoarele Intel" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Manufacturing Mode" msgstr "Modul de fabricaÈ›ie al Intel Management Engine" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the "override" is #. enabled #. * with a jumper -- luckily it is probably not accessible to end users on #. consumer #. * boards msgid "Intel Management Engine Override" msgstr "Intel Management Engine Override" #. TRANSLATORS: longer description msgid "Intel Management Engine Override disables checks for device software tampering." msgstr "Intel Management Engine Override dezactivează verificările pentru posibilele modificări ale software-ului dispozitivului." #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Version" msgstr "Versiunea Intel Management Engine" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "Dispozitiv intern" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "Nevalid" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "Argumente nevalide" #. TRANSLATORS: error message msgid "Invalid arguments, expected GUID" msgstr "Argumente nevalide, se aÈ™tepta GUID" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX KEY [VALUE]" msgstr "Argumente nevalide, aÈ™teptate INDEX CHEIE [VALOARE]" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX NAME TARGET [MOUNTPOINT]" msgstr "Argumente nevalide, aÈ™teptate INDEX NUMELE ÈšINTEI [PUNCT DE MONTARE]" #. TRANSLATOR: This is the error message for #. * incorrect parameter msgid "Invalid arguments, expected an AppStream ID" msgstr "Argumente nevalide, se aÈ™tepta un ID AppStream" #. TRANSLATORS: error message msgid "Invalid arguments, expected at least ARCHIVE FIRMWARE METAINFO" msgstr "Argumente nevalide, aÈ™teptate cel puÈ›in ARCHIVE FIRMWARE METAINFO" #. TRANSLATORS: error message msgid "Invalid arguments, expected base-16 integer" msgstr "Argumente nevalide, se aÈ™tepta un număr întreg în baza-16" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "Este o retrogradare" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "Este în modul încărcător de pornire" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "Este o actualizare" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "Problemă" msgstr[1] "Probleme" msgstr[2] "De probleme" #. TRANSLATORS: HSI event title msgid "Kernel is no longer tainted" msgstr "Nucleul nu mai este „contaminatâ€" #. TRANSLATORS: HSI event title msgid "Kernel is tainted" msgstr "Nucleul este „contaminatâ€" #. TRANSLATORS: HSI event title msgid "Kernel lockdown disabled" msgstr "Blocarea nucleului este dezactivată" #. TRANSLATORS: HSI event title msgid "Kernel lockdown enabled" msgstr "Blocarea nucleului este activată" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "LOCAÈšIA" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "Ultima modificare" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "A rămas mai puÈ›in de un minut" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "LicenÈ›a" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "Modul de blocare a nucleului Linux" #. TRANSLATORS: longer description msgid "Linux Kernel Lockdown mode prevents administrator (root) accounts from accessing and changing critical parts of system software." msgstr "Modul de blocare a nucleului Linux împiedică conturile de administrator (root) să acceseze È™i să modifice părÈ›i critice ale software-ului de sistem." msgid "Linux Kernel Swap temporarily saves information to disk as you work. If the information is not protected, it could be accessed by someone if they obtained the disk." msgstr "FuncÈ›ia spaÈ›iului de interschimb (swap) a nucleului Linux salvează temporar informaÈ›ii pe disc în timp ce lucraÈ›i. Dacă informaÈ›iile nu sunt protejate, acestea ar putea fi accesate de cineva dacă ar obÈ›ine discul." #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "Verificarea nucleului Linux" msgid "Linux Kernel Verification makes sure that critical system software has not been tampered with. Using device drivers which are not provided with the system can prevent this security feature from working correctly." msgstr "Verificarea nucleului Linux se asigură că software-ul critic al sistemului nu a fost modificat. Utilizarea controlorilor de dispozitiv care nu sunt furnizaÈ›i cu sistemul poate împiedica funcÈ›ionarea corectă a acestei funcÈ›ii de securitate." #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "SpaÈ›iu de interschimb (swap) Linux" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Serviciul de furnizare a firmware-ului Linux de la producător „LVFS†(firmware stabil)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Serviciul de furnizare a firmware-ului Linux de la producător „LVFS†(firmware aflat în testare)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Nucleul Linux" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Blocarea nucleului Linux" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "SpaÈ›iu de interschimb (swap) Linux" #. TRANSLATORS: command description msgid "List EFI boot files" msgstr "Listează fiÈ™ierele de pornire EFI" #. TRANSLATORS: command description msgid "List EFI boot parameters" msgstr "Listează parametrii de pornire EFI" #. TRANSLATORS: command description msgid "List EFI variables with a specific GUID" msgstr "Listează variabilele EFI cu GUID-ul specificat" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "Listează intrările din dbx" #. TRANSLATORS: command description msgid "List the available firmware GTypes" msgstr "Listează firmware-urile GType disponibile" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "Listează tipurile de firmware disponibile" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "Listează fiÈ™ierele de pe ESP" #. TRANSLATORS: command description msgid "Load device emulation data" msgstr "ÃŽncarcă datele de emulare a dispozitivului" #. TRANSLATORS: the plugin was created from a .so object, and was not built-in msgid "Loaded from an external module" msgstr "ÃŽncărcat de la un modul extern" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "Se încarcă…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "Blocat" #. TRANSLATORS: the release urgency msgid "Low" msgstr "Joasă" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refers #. * to the private/public key used to secure loading of firmware msgid "MEI Key Manifest" msgstr "Manifest cheie MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refer #. * to the private/public key used to secure loading of firmware msgid "MEI key manifest" msgstr "Manifest cheie MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "Modul de fabricaÈ›ie MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "ÃŽnlocuire MEI" msgid "MEI version" msgstr "Versiunea MEI" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "Activează manual modulele specificate" #. TRANSLATORS: longer description msgid "Manufacturing Mode is used when the device is manufactured and security features are not yet enabled." msgstr "Modul de fabricaÈ›ie este utilizat atunci când dispozitivul este fabricat È™i caracteristicile de securitate nu sunt încă activate." #. TRANSLATORS: Longest valid string for BIOS setting msgid "Maximum length" msgstr "Lungimea maximă" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "Valoarea maximă" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "Medie" #. TRANSLATORS: ask the user to do a simple task which should be translated msgid "Message" msgstr "Mesaj" #. TRANSLATORS: ask the user a question, and it will not be translated msgid "Message (custom)" msgstr "Mesaj (personalizat)" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "Semnătura metadatelor" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "Adresa URI a metadatelor" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "Metadatele pot fi obÈ›inute de la Serviciul de furnizare a firmware-ului Linux de la producător „LVFSâ€." #. TRANSLATORS: error message for a user who ran fwupdmgr #. * refresh recently -- %1 is '--force' #, c-format msgid "Metadata is up to date; use %s to refresh again." msgstr "Metadatele sunt la zi; utilizaÈ›i „%s†pentru a actualiza din nou." #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "Versiunea minimă" #. TRANSLATORS: Shortest valid string for BIOS setting msgid "Minimum length" msgstr "Lungimea minimă" #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "Valoarea minimă" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "Modifică o valoare de configurare a demonului" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "Modifică un depozit furnizat ca sursă de software" msgid "Modify a configured remote" msgstr "Modifică un depozit configurat" msgid "Modify daemon configuration" msgstr "Modifică configuraÈ›ia demonului" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "Monitorizează demonul pentru evenimente" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "Montează ESP-ul" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "Necesită repornirea sistemului după instalare" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "Necesită repornirea" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "Necesită închiderea sistemului după instalare" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "Versiunea nouă" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "Nicio acÈ›iune specificată!" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "Nu există retrogradări de versiune pentru %s" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "Nu s-au găsit ID-uri de firmware" #. TRANSLATORS: nothing found msgid "No firmware found" msgstr "Nu s-a găsit firmware" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Nu a fost detectat niciun hardware cu capacitate de actualizare a firmware-ului" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "Nu există versiuni disponibile" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "Niciun depozit nu este activat în prezent, aÈ™a că nu sunt disponibile metadate." #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "Nu există depozite de sursă de software disponibile" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "Nu există dispozitive actualizabile" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "Nu există actualizări disponibile" msgid "No updates available for remaining devices" msgstr "Nu sunt disponibile actualizări pentru celelalte dispozitive" #. TRANSLATORS: error message #, c-format msgid "No volume matched %s" msgstr "Niciun volum nu se potriveÈ™te cu %s" #. TRANSLATORS: version cannot be installed due to policy msgid "Not approved" msgstr "Nu este aprobată" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "Negăsit" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "Necompatibil" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "Ok" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "Ok!" #. TRANSLATORS: the firmware old version msgid "Old version" msgstr "Versiunea veche" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "AfiÈ™ează doar o singură valoare PCR" #. TRANSLATORS: command line option msgid "Only use peer-to-peer networking when downloading files" msgstr "Utilizează numai reÈ›ele „peer-to-peer†atunci când descarcă fiÈ™iere" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "Sunt permise numai actualizările de versiune" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "Suprascrie ruta implicită ESP" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Firmware" msgstr "Firmware P2P" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Metadata" msgstr "Metadate P2P" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "RUTA" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "Analizează È™i afiÈ™ează detalii despre un fiÈ™ier firmware" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "Se analizează actualizarea bazei de date dbx…" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "Se analizează baza de date dbx a sistemului…" #. TRANSLATORS: remote filename base msgid "Password" msgstr "Parola" #. TRANSLATORS: command description msgid "Patch a firmware blob at a known offset" msgstr "Aplică o corecÈ›ie (patch) unui blob (colecÈ›ie binară) de firmware la o poziÈ›ie cunoscută" msgid "Payload" msgstr "Date de descărcat" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "ÃŽn aÈ™teptare" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "EfectuaÈ›i operaÈ›ia?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "Depanarea platformei" #. TRANSLATORS: longer description msgid "Platform Debugging allows device security features to be disabled. This should only be used by hardware manufacturers." msgstr "Depanarea platformei permite dezactivarea caracteristicilor de securitate ale dispozitivului. Aceasta ar trebui să fie utilizată numai de către producătorii de hardware." #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "Depanarea platformei" #. TRANSLATORS: 'recovery key' here refers to a code, rather than a physical #. metal thing msgid "Please ensure you have the volume recovery key before continuing." msgstr "Vă rugăm să vă asiguraÈ›i că aveÈ›i cheia de recuperare a volumului înainte de a continua." #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "IntroduceÈ›i un număr de la 0 la %u: " #. TRANSLATORS: the user isn't reading the question -- %1 is 'Y' and %2 is #. * 'N' #, c-format msgid "Please enter either %s or %s: " msgstr "IntroduceÈ›i %s sau %s: " #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "Lipsesc dependenÈ›ele modulului" #. TRANSLATORS: The plugin is only for testing msgid "Plugin is only for testing" msgstr "Modulul este doar pentru testare" #. TRANSLATORS: Possible values for a bios setting msgid "Possible Values" msgstr "Valorile posibile" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA Protection" msgstr "ProtecÈ›ia DMA pre-pornire" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "ProtecÈ›ia DMA pre-pornire" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is disabled" msgstr "ProtecÈ›ia DMA pre-pornire este dezactivată" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is enabled" msgstr "ProtecÈ›ia DMA pre-pornire este activată" #. TRANSLATORS: longer description msgid "Pre-boot DMA protection prevents devices from accessing system memory after being connected to the computer." msgstr "ProtecÈ›ia DMA pre-pornire împiedică dispozitivele să acceseze memoria sistemului după ce sunt conectate la calculator." #. TRANSLATORS: warning message msgid "Press unlock on the device to continue the update process." msgstr "ApăsaÈ›i butonul de deblocare pe dispozitiv pentru a continua procesul de actualizare." #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "Versiunea anterioară" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Prioritate" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "Probleme" msgid "Proceed with upload?" msgstr "ContinuaÈ›i cu încărcarea?" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Processor Security Checks" msgstr "Verificări de securitate ale procesorului" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "Processor rollback protection" msgstr "ProtecÈ›ie de restaurare la starea anterioară a procesorului" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "Proprietar" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "ID-DEPOZIT" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID KEY VALUE" msgstr "ID-DEPOZIT VALOARE CHEIE" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "Numai citire" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "CiteÈ™te un „blob†(colecÈ›ie binară) de firmware dintr-un dispozitiv" #. TRANSLATORS: command description msgid "Read a firmware from a device" msgstr "CiteÈ™te firmware-ul dintr-un dispozitiv" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "Se citeÈ™te din %s…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Se citeÈ™te…" #. TRANSLATORS: Plugin is active and in use msgid "Ready" msgstr "în funcÈ›iune" #. TRANSLATORS: how often we should refresh the metadata msgid "Refresh Interval" msgstr "Intervalul de reîmprospătare" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "Actualizează metadatele de la serverul de la distanță" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "ReinstalaÈ›i %s în %s?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "Reinstalează firmware-ul curent pe dispozitiv" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "Reinstalează firmware-ul pe un dispozitiv" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "Ramura de lansare" #. TRANSLATORS: release attributes msgid "Release Flags" msgstr "Fanioane de publicare" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "ID-ul de publicare" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "ID depozit" #. TRANSLATORS: command description msgid "Removes devices to watch for future emulation" msgstr "Elimină dispozitivele de monitorizat pentru emulare viitoare" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "Adresa URI pentru trimiterea rapoartelor" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "Raportat la serverul de la distanță" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "Sistemul de fiÈ™iere efivarfs necesar nu a fost găsit" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "Hardware-ul necesar nu a fost găsit" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "Necesită un încărcător de pornire" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Necesită conexiune la internet" msgid "Reset daemon configuration" msgstr "RestabileÈ™te configuraÈ›ia daemonului" #. TRANSLATORS: sets something in the daemon configuration file msgid "Resets a daemon configuration section" msgstr "RestabileÈ™te o secÈ›iune de configurare a demonului" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "ReporniÈ›i sistemul acum?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "ReporniÈ›i demonul pentru ca modificarea să intre în vigoare?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Se reporneÈ™te dispozitivul…" #. TRANSLATORS: command description msgid "Retrieve BIOS settings. If no arguments are passed all settings are returned" msgstr "ObÈ›ine parametrii BIOS. Dacă nu sunt trecute argumente, sunt returnaÈ›i toÈ›i parametrii" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "Returnează toate ID-urile hardware pentru maÈ™ină" #. TRANSLATORS: ask the user to upload msgid "Review and upload report now?" msgstr "RevizuiÈ›i È™i încărcaÈ›i raportul acum?" #. TRANSLATORS: longer description msgid "Rollback Protection prevents device software from being downgraded to an older version that has security problems." msgstr "ProtecÈ›ia de restaurare la starea anterioară previne ca software-ul dispozitivului să fie retrogradat la o versiune mai veche care are probleme de securitate." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr get-upgrades` #, c-format msgid "Run `%s` for more information." msgstr "RulaÈ›i «%s» pentru mai multe informaÈ›ii." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr sync` #, c-format msgid "Run `%s` to complete this action." msgstr "RulaÈ›i «%s» pentru a finaliza această acÈ›iune." #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "Rulează rutina de curățare a modulului compozit atunci când se utilizează „install-blobâ€" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "Rulează rutina de pregătire a modulului compozit atunci când se utilizează „install-blobâ€" #. TRANSLATORS: command description msgid "Run the post-reboot cleanup action" msgstr "Rulează acÈ›iunea de curățare după-repornire" #. TRANSLATORS: tell a user how to get information #, c-format msgid "Run without '%s' to see" msgstr "RulaÈ›i fără „%s†pentru a vedea" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "Nucleul în funcÈ›iune este prea vechi" #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "Sufixul de timp de execuÈ›ie" #. TRANSLATORS: Software Bill of Materials link msgid "SBOM" msgstr "SBOM" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SECTION" msgstr "SECÈšIUNE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING VALUE" msgstr "PARAMETRU VALOARE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING1 VALUE1 [SETTING2] [VALUE2]" msgstr "PARAMETRU1 VALOARE1 [ PARAMETRU2] [VALOARE2]" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "SMAP" msgstr "SMAP" #. TRANSLATORS: Title: Whether firmware is locked down msgid "SMM locked down" msgstr "SMM blocat" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "Descriptorul BIOS SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "Regiunea BIOS SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "Blocare SPI" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "SPI replay protection" msgstr "ProtecÈ›ie la reluarea SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "Scriere SPI" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "SPI write protection" msgstr "ProtecÈ›ie la scriere SPI" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "SUBSISTEM CONTROLOR [ID DISPOZITIV|GUID]" #. TRANSLATORS: command description msgid "Save a file that allows generation of hardware IDs" msgstr "Salvează un fiÈ™ier care permite generarea ID-urilor hardware" #. TRANSLATORS: command description msgid "Save device emulation data" msgstr "Salvează datele de emulare a dispozitivului" #. TRANSLATORS: key for a offline report filename msgid "Saved report" msgstr "Raport salvat" #. TRANSLATORS: Scalar increment for integer BIOS setting msgid "Scalar Increment" msgstr "Increment scalar" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Se planifică…" #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "Pornirea sigură „Secure Boot†este dezactivată" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "Pornirea sigură „Secure Boot†este activată" msgid "Security hardening for HSI" msgstr "ÃŽntărirea securității pentru HSI" #. TRANSLATORS: the %1 is a URL to a wiki page #, c-format msgid "See %s for more details." msgstr "ConsultaÈ›i %s pentru mai multe detalii." #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "ConsultaÈ›i %s pentru mai multe informaÈ›ii." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Dispozitiv selectat" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Volum selectat" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "Numărul de serie" #. TRANSLATORS: Configured a BIOS setting to a value #, c-format msgid "Set BIOS setting '%s' using '%s'." msgstr "Configurează parametrul BIOS „%s†utilizând „%sâ€." #. TRANSLATORS: command description msgid "Set a BIOS setting" msgstr "StabileÈ™te un parametru BIOS" msgid "Set one or more BIOS settings" msgstr "Configurează unul sau mai mulÈ›i parametri BIOS" #. TRANSLATORS: command description msgid "Set or remove an EFI boot hive entry" msgstr "Configurează sau elimină o intrare hive de pornire EFI" #. TRANSLATORS: command description msgid "Set the EFI boot next" msgstr "StabileÈ™te următoarea pornire EFI" #. TRANSLATORS: command description msgid "Set the EFI boot order" msgstr "StabileÈ™te ordinea de pornire EFI" #. TRANSLATORS: command line option msgid "Set the download retries for transient errors" msgstr "StabileÈ™te numărul de reîncercări de descărcare în cazul unor erori tranzitorii" #. TRANSLATORS: command description msgid "Sets one or more BIOS settings" msgstr "StabileÈ™te unul sau mai mulÈ›i parametri BIOS" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "StabileÈ™te lista de firmware aprobat" #. TRANSLATORS: type of BIOS setting msgid "Setting type" msgstr "Tipul parametrului" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "Configurările făcute se vor aplica după repornirea sistemului" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "Partajează istoricul firmware-ului cu dezvoltatorii" #. TRANSLATORS: command line option msgid "Show all results" msgstr "AfiÈ™ează toate rezultatele" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "AfiÈ™ează versiunile de client È™i demon" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "AfiÈ™ează informaÈ›iile detaliate ale demonului pentru un anumit domeniu" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "AfiÈ™ează informaÈ›ii de depanare pentru toate domeniile" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "AfiÈ™ează opÈ›iunile de depanare" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "AfiÈ™ează dispozitivele care nu sunt actualizabile" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "AfiÈ™ează informaÈ›ii suplimentare de depanare" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "AfiÈ™ează istoricul actualizărilor firmware" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "AfiÈ™ează versiunea calculată a dbx" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "ÃŽnchideÈ›i sistemul acum?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "Semnează un firmware cu o cheie nouă" msgid "Sign data using the client certificate" msgstr "Semnează datele utilizând certificatul clientului" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "Semnează datele utilizând certificatul clientului" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "Semnează datele încărcate cu certificatul clientului" msgid "Signature" msgstr "Semnătură" #. TRANSLATORS: firmware is verified on-device the payload using strong crypto msgid "Signed Payload" msgstr "Date semnate" #. TRANSLATORS: file size of the download msgid "Size" msgstr "Dimensiune" #. TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM msgid "Some of the platform secrets may be invalidated when updating this firmware." msgstr "Unele dintre secretele platformei pot fi invalidate la actualizarea acestui firmware." #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "Sursa" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "Specifică fiÈ™ierul bazei de date dbx" msgid "Stop the fwupd service" msgstr "OpreÈ™te serviciul fwupd" #. TRANSLATORS: The BIOS setting accepts strings msgid "String" msgstr "Șir" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "Succes" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "Au fost activate cu succes toate dispozitivele" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "Depozit dezactivat cu succes" #. TRANSLATORS: comment explaining result of command msgid "Successfully disabled test devices" msgstr "Dispozitive de testare dezactivate cu succes" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "Au fost descărcate cu succes metadatele noi: " #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "Depozit activat È™i actualizat cu succes" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "Depozit activat cu succes" #. TRANSLATORS: comment explaining result of command msgid "Successfully enabled test devices" msgstr "Dispozitive de testare activate cu succes" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "Firmware instalat cu succes" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "Valoare de configurare modificată cu succes" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "Depozit modificat cu succes" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "Actualizarea manuală a metadatelor a reuÈ™it" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration section" msgstr "SecÈ›iune de configurare reiniÈ›ializată cu succes" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration values" msgstr "S-au reiniÈ›ializat cu succes valorile de configurare" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "Sumele de control ale dispozitivului au fost actualizate cu succes" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "A fost încărcat cu succes %u raport" msgstr[1] "Au fost încărcate cu succes %u rapoarte" msgstr[2] "Au fost încărcate cu succes %u de rapoarte" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "Sumele de control ale dispozitivelor au fost verificate cu succes" #. TRANSLATORS: the device showed up in time #, c-format msgid "Successfully waited %.0fms for device" msgstr "S-a aÈ™teptat cu succes %.0fms pentru dispozitiv" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Rezumat" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "Supervisor Mode Access Prevention" msgstr "Prevenirea accesului în modul Supervizor" #. TRANSLATORS: longer description msgid "Supervisor Mode Access Prevention ensures critical parts of device memory are not accessed by less secure programs." msgstr "Prevenirea accesului în modul Supervizor asigură că părÈ›ile critice ale memoriei dispozitivului nu sunt accesate de programe mai puÈ›in sigure." #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "Compatibil" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "CPU acceptat" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "Acceptat pe serverul de la distanță" #. TRANSLATORS: Title: a better sleep state msgid "Suspend To Idle" msgstr "Suspendarea la modul inactiv" #. TRANSLATORS: Title: sleep state msgid "Suspend To RAM" msgstr "Suspendare la RAM" #. TRANSLATORS: longer description msgid "Suspend to Idle allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Suspendarea la modul inactiv permite dispozitivului să intre rapid în modul inactiv pentru a economisi energie. ÃŽn timp ce dispozitivul a fost suspendat, memoria sa poate fi scoasă fizic È™i informaÈ›iile sale pot fi accesate." #. TRANSLATORS: longer description msgid "Suspend to RAM allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Suspendarea în RAM permite dispozitivului să intre rapid în stare de repaus pentru a economisi energie. ÃŽn timp ce dispozitivul a fost suspendat, memoria sa poate fi scoasă fizic È™i informaÈ›iile sale pot fi accesate." #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "Suspendarea la modul inactiv" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "Suspendare la RAM" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "SchimbaÈ›i de la ramura %s la %s?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "Schimbă ramura firmware a dispozitivului" #. TRANSLATORS: command description msgid "Sync firmware versions to the chosen configuration" msgstr "Sincronizează versiunile firmware cu configuraÈ›ia aleasă" #. TRANSLATORS: Title: Whether firmware is locked down msgid "System Management Mode" msgstr "Modul de gestionare a sistemului" #. TRANSLATORS: this CLI tool is now preventing system updates msgid "System Update Inhibited" msgstr "Actualizarea sistemului inhibată" #. TRANSLATORS: longer description msgid "System management mode is used by the firmware to access resident BIOS code and data." msgstr "Modul de gestionare a sistemului este utilizat de firmware pentru a accesa codul È™i datele BIOS rezidente." #. TRANSLATORS: as in laptop battery power msgid "System power is too low" msgstr "Energia sistemului este prea scăzută" #. TRANSLATORS: as in laptop battery power #, c-format msgid "System power is too low (%u%%, requires %u%%)" msgstr "Energia sistemului este prea scăzută (%u%%, necesită %u%%)" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "Sistemul necesită o sursă de alimentare externă" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "TEXT" #. TRANSLATORS: longer description msgid "TPM (Trusted Platform Module) is a computer chip that detects when hardware components have been tampered with." msgstr "TPM (Trusted Platform Module) este un cip de calculator care detectează atunci când componentele hardware au fost modificate." #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "ReconstrucÈ›ie PCR0 TPM" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is invalid" msgstr "ReconstrucÈ›ia PCR0 TPM nu este validă" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is now valid" msgstr "ReconstrucÈ›ia PCR0 TPM este acum validă" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM Platform Configuration" msgstr "Configurarea platformei TPM" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM Reconstruction" msgstr "ReconstrucÈ›ia TPM" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM empty PCRs" msgstr "PCR-uri goale de TPM" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "Eticheta" msgstr[1] "Etichete" msgstr[2] "Etichete" #. TRANSLATORS: we're saving all USB events for emulation msgid "Tagged for emulation" msgstr "Etichetat pentru emulare" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "„Contaminatâ€" #. show the user the entire data blob msgid "Target" msgstr "Èšinta" #. TRANSLATORS: command description msgid "Test a device using a JSON manifest" msgstr "Testează un dispozitiv utilizând un manifest JSON" #. TRANSLATORS: when the release was tested msgid "Tested" msgstr "Testat" #. TRANSLATORS: the %s is a vendor name, e.g. Lenovo #, c-format msgid "Tested by %s" msgstr "Testat de %s" #. TRANSLATORS: someone we trust has tested this msgid "Tested by trusted vendor" msgstr "Testat de un producător de încredere" #. TRANSLATORS: the boot entry was in a legacy format msgid "The EFI boot entry is not in hive format, and shim may not be new enough to read it." msgstr "Intrarea de pornire EFI nu este în format „hiveâ€, iar «shim» poate să nu fie suficient de nou pentru a o citi." #. TRANSLATORS: try to treat the legacy format as a hive msgid "The EFI boot entry was not in hive format, falling back" msgstr "Intrarea de pornire EFI nu a fost în formatul hive, se trece la opÈ›iunea de rezervă" #. TRANSLATORS: longer description msgid "The Intel Management Engine Key Manifest must be valid so that the device firmware can be trusted by the CPU." msgstr "Manifestul de chei al Intel Management Engine trebuie să fie valid, astfel încât CPU-ul să poată avea încredere în firmware-ul dispozitivului." #. TRANSLATORS: longer description msgid "The Intel Management Engine controls device components and needs to have a recent version to avoid security issues." msgstr "Intel Management Engine controlează componentele dispozitivului È™i trebuie să aibă o versiune recentă pentru a evita problemele de securitate." #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "LVFS este un serviciu gratuit care funcÈ›ionează ca o entitate juridică independentă È™i nu are nicio legătură cu $OS_RELEASE:NAME$. Este posibil ca distribuitorul dvs. să nu fi verificat compatibilitatea vreuneia dintre actualizările firmware cu sistemul dvs. sau cu dispozitivele conectate. Toate firmware-urile sunt furnizate numai de către producătorul echipamentului original." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Platform Configuration is used to check whether the device start process has been modified." msgstr "ConfiguraÈ›ia platformei TPM („Trusted Platform Moduleâ€: modul de platformă de încredere) este utilizată pentru a verifica dacă procesul de pornire a dispozitivului a fost modificat." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Reconstruction is used to check whether the device start process has been modified." msgstr "ReconstrucÈ›ia TPM (Trusted Platform Module) este utilizată pentru a verifica dacă procesul de pornire a dispozitivului a fost modificat." #. TRANSLATORS: this is more background on a security measurement problem msgid "The TPM PCR0 differs from reconstruction." msgstr "PCR0 TPM este diferit de reconstrucÈ›ie." #. TRANSLATORS: longer description msgid "The UEFI Platform Key is used to determine if device software comes from a trusted source." msgstr "Cheia platformei UEFI este utilizată pentru a determina dacă software-ul dispozitivului provine dintr-o sursă de încredere." #. TRANSLATORS: HSI event title msgid "The UEFI certificate store is now up to date" msgstr "Stocul de certificate UEFI este acum actualizat" #. TRANSLATORS: longer description msgid "The UEFI db contains the list of valid certificates that can be used to authorize what EFI binaries are allowed to run." msgstr "Baza de date UEFI (UEFI db) conÈ›ine lista certificatelor valide care pot fi utilizate pentru a autoriza ce binare EFI pot rula." #. TRANSLATORS: longer description msgid "The UEFI system can set up memory attributes at boot which prevent common exploits from running." msgstr "Sistemul UEFI poate configura atribute de memorie la pornire care împiedică executarea unor atacuri de exploatare a breÈ™elor de securitate comune." #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "Demonul a încărcat cod de la terÈ›e părÈ›i È™i nu mai este întreÈ›inut de dezvoltatorii iniÈ›iali!" #. TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name #, c-format msgid "The firmware from %s is not supplied by %s, the hardware vendor." msgstr "Firmware-ul de la %s nu este furnizat de %s, fabricantul de hardware." #. TRANSLATORS: try to help msgid "The system clock has not been set correctly and downloading files may fail." msgstr "Ceasul sistemului nu a fost configurat corect, iar descărcarea fiÈ™ierelor poate eÈ™ua." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been re-inserted." msgstr "Actualizarea va continua după ce cablul USB al dispozitivului a fost reinserat." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged and then re-inserted." msgstr "Actualizarea va continua după ce cablul USB al dispozitivului a fost deconectat È™i apoi reinserat." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged." msgstr "Actualizarea va continua după ce cablul USB al dispozitivului a fost deconectat." #. TRANSLATORS: warning message msgid "The update will continue when the device power cable has been removed and re-inserted." msgstr "Actualizarea va continua după ce cablul de alimentare al dispozitivului a fost scos È™i introdus din nou." #. TRANSLATORS: naughty vendor msgid "The vendor did not supply any release notes." msgstr "Furnizorul nu a furnizat nicio notă de lansare." #. TRANSLATORS: now list devices with unfixed high-priority issues msgid "There are devices with issues:" msgstr "Există dispozitive cu probleme:" #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "Nu există fiÈ™iere firmware blocate" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "Nu există niciun firmware aprobat." #. TRANSLATORS: %1 is the current device version number, and %2 is the #. command name, e.g. `fwupdmgr sync` #, c-format msgid "This device will be reverted back to %s when the %s command is performed." msgstr "Acest dispozitiv va fi readus la %s atunci când se efectuează comanda %s." #. TRANSLATORS: the vendor did not upload this msgid "This firmware is provided by LVFS community members and is not provided (or supported) by the original hardware vendor." msgstr "Acest firmware este furnizat de membrii comunității LVFS È™i nu este furnizat (sau susÈ›inut) de către fabricantul hardware original." #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "Acest pachet nu a fost validat, este posibil să nu funcÈ›ioneze corect." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "Acest program poate funcÈ›iona corect doar ca root" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "Acest depozit conÈ›ine firmware care nu este sub interdicÈ›ie, dar este încă în curs de testare de către fabricantul de hardware. Trebuie să vă asiguraÈ›i că aveÈ›i o modalitate de a actualiza manual firmware-ul dacă actualizarea firmware-ului eÈ™uează." #. TRANSLATORS: error message msgid "This system doesn't support firmware settings" msgstr "Acest sistem nu acceptă configurări firmware" #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "Acest sistem are probleme în timpul de execuÈ›ie HSI." #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "Acest sistem are un nivel de securitate HSI scăzut." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "Acest instrument permite unui administrator să aplice actualizări dbx UEFI." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "Acest instrument permite unui administrator să interogheze È™i să controleze demonul «fwupd», permițându-i să efectueze acÈ›iuni precum instalarea sau retrogradarea firmware-ului." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "Acest instrument permite unui administrator să utilizeze modulele «fwupd» fără a fi instalate pe sistemul gazdă." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can add a kernel argument of '%s', but it will only be active after restarting the computer." msgstr "Acest instrument poate adăuga un argument de nucleu „%sâ€, dar acesta va fi activ numai după repornirea calculatorului." #. TRANSLATORS: the %1 is a BIOS setting name. #. * %2 and %3 are the values, e.g. "True" or "Windows10" #, c-format msgid "This tool can change the BIOS setting '%s' from '%s' to '%s' automatically, but it will only be active after restarting the computer." msgstr "Acest instrument poate schimba automat parametrul BIOS „%s†de la „%s†la „%sâ€, dar modificarea va fi activă numai după repornirea calculatorului." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can change the kernel argument from '%s' to '%s', but it will only be active after restarting the computer." msgstr "Acest instrument poate schimba argumentul nucleului de la „%s†la „%sâ€, dar acesta va deveni activ numai după repornirea calculatorului." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "Acest instrument va citi È™i va analiza jurnalul evenimentelor TPM din firmware-ul sistemului." #. TRANSLATORS: the update state of the specific device msgid "Transient failure" msgstr "EÈ™ec tranzitoriu" #. TRANSLATORS: item is TRUE msgid "True" msgstr "Adevărat" #. TRANSLATORS: We verified the metadata against the server msgid "Trusted metadata" msgstr "Metadate de încredere" #. TRANSLATORS: We verified the payload against the server msgid "Trusted payload" msgstr "Date de încredere" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Tip" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI Bootservice Variables" msgstr "Variabile ale serviciului de pornire UEFI" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition may not be set up correctly" msgstr "Este posibil ca partiÈ›ia ESP UEFI să nu fie configurată corect" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "PartiÈ›ia ESP UEFI nu este detectată sau configurată" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI Memory Protection" msgstr "ProtecÈ›ie a memoriei UEFI" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "Cheia platformei UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "Pornirea securizată UEFI" #. TRANSLATORS: longer description msgid "UEFI Secure Boot prevents malicious software from being loaded when the device starts." msgstr "UEFI Secure Boot previne încărcarea software-ului maliÈ›ios la pornirea dispozitivului." #. TRANSLATORS: longer description msgid "UEFI boot service variables should not be readable from runtime mode." msgstr "Variabilele serviciului de pornire UEFI nu ar trebui să poată fi citite din modul de execuÈ›ie." #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI bootservice variables" msgstr "Variabile ale serviciului de pornire UEFI" #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "Actualizările capsulei UEFI nu sunt disponibile sau activate în configurarea firmware-ului" #. TRANSLATORS: Title: is UEFI db up-to-date msgid "UEFI db" msgstr "Baza de date UEFI (UEFI db)" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "Instrument dbx UEFI" #. TRANSLATORS: system is not booted in UEFI mode msgid "UEFI firmware can not be updated in legacy BIOS mode" msgstr "Firmware-ul UEFI nu poate fi actualizat în modul BIOS vechi" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI memory protection" msgstr "ProtecÈ›ie a memoriei UEFI" #. TRANSLATORS: HSI event title msgid "UEFI memory protection enabled and locked" msgstr "ProtecÈ›ia memoriei UEFI este activată È™i blocată" #. TRANSLATORS: HSI event title msgid "UEFI memory protection enabled but not locked" msgstr "ProtecÈ›ia memoriei UEFI este activată, dar nu este blocată" #. TRANSLATORS: HSI event title msgid "UEFI memory protection is now locked" msgstr "ProtecÈ›ia memoriei UEFI este acum blocată" #. TRANSLATORS: HSI event title msgid "UEFI memory protection is now unlocked" msgstr "ProtecÈ›ia memoriei UEFI este acum deblocată" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "Cheia platformei UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "Pornirea securizată UEFI" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "Nu se poate conecta la serviciu" #. TRANSLATORS: error message msgid "Unable to find attribute" msgstr "Nu se poate găsi atributul" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "Eliberează controlorul curent" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "Se deblochează firmware-ul:" #. TRANSLATORS: command description msgid "Unblocks a specific firmware from being installed" msgstr "Deblochează instalarea unui firmware specificat" #. TRANSLATORS: command description msgid "Undo the host security attribute fix" msgstr "Anulează corectarea atributului de securitate al gazdei" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "Necriptat" #. TRANSLATORS: command description msgid "Uninhibit the system to allow upgrades" msgstr "Dezinhibă sistemul pentru a permite actualizările" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Necunoscut" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Dispozitiv necunoscut" msgid "Unlock the device to allow access" msgstr "DeblocaÈ›i dispozitivul pentru a permite accesul" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "Deblocat" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "Deblochează dispozitivul pentru a permite accesul la firmware" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "Demontează ESP-ul" #. TRANSLATORS: message shown after device has been marked for emulation msgid "Unplug and replug the device to continue the update process." msgstr "DeconectaÈ›i È™i reconectaÈ›i dispozitivul pentru a continua procesul de actualizare." #. TRANSLATORS: firmware payload is unsigned and it is possible to modify it msgid "Unsigned Payload" msgstr "Date nesemnate" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "Versiunea demonului %s nu este compatibilă, versiunea clientului este %s" #. TRANSLATORS: Suffix: the HSI result msgid "Untainted" msgstr "„Necontaminatâ€" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "Actualizabil" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Eroare la actualizare" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "Actualizează imaginea" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "Mesajul de actualizare" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "Starea actualizării" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "EÈ™ecul actualizării este o problemă cunoscută, vizitaÈ›i această adresă URL pentru mai multe informaÈ›ii:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "ActualizaÈ›i acum?" #. TRANSLATORS: command description msgid "Update the stored cryptographic hash with current ROM contents" msgstr "Actualizează suma de control criptografică stocată cu conÈ›inutul actual al ROM" msgid "Update the stored device verification information" msgstr "Actualizează informaÈ›iile de verificare a dispozitivului stocate" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "Actualizează metadatele stocate cu conÈ›inutul curent" #. TRANSLATORS: command description msgid "Updates all specified devices to latest firmware version, or all devices if unspecified" msgstr "Actualizează toate dispozitivele specificate la cea mai recentă versiune de firmware sau toate dispozitivele dacă nu se specifică niciunul" #. TRANSLATORS: how many local devices can expect updates now #, c-format msgid "Updates have been published for %u local device" msgid_plural "Updates have been published for %u of %u local devices" msgstr[0] "Au fost publicate actualizări pentru %u dispozitiv local" msgstr[1] "Au fost publicate actualizări pentru %u din %u dispozitive locale" msgstr[2] "Au fost publicate actualizări pentru %u din %u de dispozitive locale" msgid "Updating" msgstr "Se actualizează" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "Se actualizează %s…" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "ActualizaÈ›i %s de la %s la %s?" #. TRANSLATORS: ask the user to upload msgid "Upload data now?" msgstr "ÃŽncărcaÈ›i datele acum?" #. TRANSLATORS: command description msgid "Upload the list of updatable devices to a remote server" msgstr "ÃŽncarcă lista de dispozitive actualizabile pe un server de la distanță" #. TRANSLATORS: ask the user to share, %s is something #. * like: "Linux Vendor Firmware Service" #, c-format msgid "Upload these anonymous results to the %s to help other users?" msgstr "ÃŽncărcaÈ›i aceste rezultate anonime la %s pentru a ajuta alÈ›i utilizatori?" #. TRANSLATORS: explain why we want to upload #, c-format msgid "Uploading a device list allows the %s team to know what hardware exists, and allows us to put pressure on vendors that do not upload firmware updates for their hardware." msgstr "ÃŽncărcarea unei liste de dispozitive permite echipei %s să È™tie ce hardware există È™i ne permite să facem presiuni asupra fabricanÈ›ilor care nu încarcă actualizări de firmware pentru hardware-ul lor." #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "ÃŽncărcarea rapoartelor de firmware ajută producătorii de hardware să identifice rapid actualizările eÈ™uate È™i reuÈ™ite pe dispozitive reale." #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "Urgentă" #. TRANSLATORS: explain how to get help, %1 is #. * 'fwupdtool --help' #. TRANSLATORS: explain how to get help, #. * where $1 is something like 'fwupdmgr --help' #, c-format msgid "Use %s for help" msgstr "UtilizaÈ›i «%s» pentru ajutor" #. TRANSLATORS: CTRL^C [holding control, and then pressing C] will exit the #. program msgid "Use CTRL^C to cancel." msgstr "UtilizaÈ›i «CTRL^C» pentru a anula." #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "Utilizatorul a fost notificat" #. TRANSLATORS: remote filename base msgid "Username" msgstr "Numele utilizatorului" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "VERSION1 VERSION2 [FORMAT]" msgstr "VERSIUNEA1 VERSIUNEA2 [FORMAT]" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "Valid" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "Se validează conÈ›inutul ESP…" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "Varianta" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "Producător" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "Se verifică…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Versiunea" #. TRANSLATORS: the fwupd version the release was tested on msgid "Version[fwupd]" msgstr "Versiune[fwupd]" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "AVERTISMENT" #. TRANSLATORS: command description msgid "Wait for a device to appear" msgstr "AÈ™teaptă să apară un dispozitiv" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "Se aÈ™teaptă…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "Monitorizează modificările hardware" #. TRANSLATORS: check various UEFI and ACPI tables are unchanged after the #. update msgid "Will measure elements of system integrity around an update" msgstr "Va măsura elementele de integritate a sistemului în jurul unei actualizări" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "Se scrie fiÈ™ierul:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Se scrie…" #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from a recovery or installation disk, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "Trebuie să vă asiguraÈ›i că vă este uÈ™or să restauraÈ›i configuraÈ›ia de pe un disc de recuperare sau de instalare, deoarece această modificare poate face ca sistemul să nu pornească în Linux sau să provoace alte instabilități ale sistemului." #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from the system firmware setup, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "Trebuie să vă asiguraÈ›i că vă este uÈ™or să restauraÈ›i parametrii din configuraÈ›ia firmware a sistemului, deoarece această modificare poate face ca sistemul să nu pornească în Linux sau să cauzeze instabilitatea sistemului." #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "Este posibil ca distribuitorul dvs. să nu fi verificat compatibilitatea actualizărilor firmware cu sistemul dvs. sau cu dispozitivele conectate." #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "Hardware-ul dvs. poate fi deteriorat folosind acest firmware, iar instalarea acestei versiuni poate anula orice garanÈ›ie cu %s." #. TRANSLATORS: BKC is the industry name for the best known configuration and #. is a set #. * of firmware that works together #, c-format msgid "Your system is set up to the BKC of %s." msgstr "Sistemul dvs. este configurat la „BKC†(cea mai bună configuraÈ›ie cunoscută) de %s." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[APPSTREAM_ID]" msgstr "[ID_APPSTREAM]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[SUMÄ‚_DE_CONTROL]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[ID-DISPOZITIV|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[ID-DISPOZITIV|GUID] [RAMURÄ‚]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [VERSION]" msgstr "[ID-DISPOZITIV|GUID] [VERSIUNE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE]" msgstr "[DISPOZITIV]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILE FILE_SIG REMOTE-ID]" msgstr "[FIȘIER SEMNÄ‚TURÄ‚_FIȘIER ID-DEPOZIT]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[ NUME_FIȘIER1 ] [ NUME_FIȘIER2 ]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FWUPD-VERSION]" msgstr "[VERSIUNEA-FWUPD]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[REASON] [TIMEOUT]" msgstr "[MOTIV] [TIMP_AȘTEPTARE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SECTION] KEY VALUE" msgstr "[SECÈšIUNE] CHEIE VALOARE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2] [--no-authenticate]" msgstr "[PARAMETRU1] [PARAMETRU2] [--no-authenticate]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2]..." msgstr "[PARAMETRU1] [ PARAMETRU2]..." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[FIȘIER-SMBIOS|FIȘIER-HWIDS]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "implicit" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "Instrument de înregistrare a evenimentelor TPM a «fwupd»" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "Module fwupd" fwupd-2.0.10/po/ru.po000066400000000000000000001750031501337203100143240ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Igor , 2017 # Serge Vylekzhanin , 2015-2019 # Sergej A. , 2022-2023 # Sergej A. , 2022 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Russian (http://app.transifex.com/freedesktop/fwupd/language/ru/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: ru\n" "Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "ОÑталаÑÑŒ %.0f минута" msgstr[1] "ОÑталоÑÑŒ %.0f минуты" msgstr[2] "ОÑталоÑÑŒ %.0f минут" msgstr[3] "ОÑталоÑÑŒ %.0f минут" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "Обновление аккумулÑтора %s" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "Обновление камеры %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "Обновление конфигурации %s" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "Обновление подÑиÑтемы Consumer МЕ уÑтройÑтва %s" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "Обновление контроллера %s" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "Обновление подÑиÑтемы Corporate МЕ уÑтройÑтва %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "Обновление уÑтройÑтва %s" #. TRANSLATORS: Video Display refers to the laptop internal display or #. * external monitor #, c-format msgid "%s Display Update" msgstr "Обновление диÑÐ¿Ð»ÐµÑ %s" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "Обновление вÑтроенного контроллера %s" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "Обновление клавиатуры %s" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "Обновление подÑиÑтемы МЕ уÑтройÑтва %s" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "Обновление мыши %s" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "Обновление Ñетевого интерфейÑа %s" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "Обновление ÑиÑтемы %s" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "Обновление ÑенÑорной панели %s" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "Обновление уÑтройÑтва %s" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "%s верÑиÑ" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u день" msgstr[1] "%u днÑ" msgstr[2] "%u дней" msgstr[3] "%u дней" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "%u device has a firmware upgrade available." msgstr[1] "%u уÑтройÑтва имеют доÑтупное обновление прошивки." msgstr[2] "%u уÑтройÑтв имеют доÑтупное обновление прошивки." msgstr[3] "%u уÑтройÑтво имеет доÑтупное обновление прошивки." #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device is not the best known configuration." msgid_plural "%u devices are not the best known configuration." msgstr[0] "%u уÑтройÑтво Ñ Ð½Ðµ Ñамой лучшей извеÑтной конфигурацией." msgstr[1] "%u уÑтройÑтва Ñ Ð½Ðµ Ñамой лучшей извеÑтной конфигурацией." msgstr[2] "%u уÑтройÑтв Ñ Ð½Ðµ Ñамой лучшей извеÑтной конфигурацией." msgstr[3] "%u уÑтройÑтв Ñ Ð½Ðµ Ñамой лучшей извеÑтной конфигурацией." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u чаÑ" msgstr[1] "%u чаÑа" msgstr[2] "%u чаÑов" msgstr[3] "%u чаÑов" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u минута" msgstr[1] "%u минуты" msgstr[2] "%u минут" msgstr[3] "%u минут" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u Ñекунда" msgstr[1] "%u Ñекунды" msgstr[2] "%u Ñекунд" msgstr[3] "%u Ñекунд" #. TRANSLATORS: first percentage is current value, 2nd percentage is the #. * lowest limit the firmware update is allowed for the update to happen #, c-format msgid "%u%% (threshold %u%%)" msgstr "%u%% (пороговое значение - %u%%)" #. TRANSLATORS: HSI event title msgid "A TPM PCR is now an invalid value" msgstr "PCR TPM теперь имеет недейÑтвительное значение" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "Ðеобходимое дейÑтвие:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "Ðктивировать уÑтройÑтва" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "ЗадейÑтвовать ожидающие уÑтройÑтва" msgid "Activate the new firmware on the device" msgstr "Ðктивировать новую прошивку на уÑтройÑтве" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "ÐÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ñ Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾ÑˆÐ¸Ð²ÐºÐ¸" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "ÐÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ñ Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾ÑˆÐ¸Ð²ÐºÐ¸ длÑ" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "ВозраÑÑ‚" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "СоглаÑитьÑÑ Ð¸ активировать репозиторий?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "ПÑевдоним %s" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Разрешить понижение верÑий прошивок" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "Разрешить переуÑтановку ÑущеÑтвующих верÑий прошивки" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "Разрешить переключение веток прошивки" #. TRANSLATORS: is not the main firmware stream msgid "Alternate branch" msgstr "ÐÐ»ÑŒÑ‚ÐµÑ€Ð½Ð°Ñ‚Ð¸Ð²Ð½Ð°Ñ Ð²ÐµÑ‚ÐºÐ°" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "Завершение Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ñ‚Ñ€ÐµÐ±ÑƒÐµÑ‚ перезагрузки." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "Завершение Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ñ‚Ñ€ÐµÐ±ÑƒÐµÑ‚ Ð²Ñ‹ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ñ ÑиÑтемы." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Ответить да на вÑе вопроÑÑ‹" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Применить обновление, даже еÑли Ñто не ÑвлÑетÑÑ Ñ€ÐµÐºÐ¾Ð¼ÐµÐ½Ð´Ð¾Ð²Ð°Ð½Ð½Ñ‹Ð¼" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Применить файлы обновлениÑ" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "Применение обновлениÑ…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "ÐžÐ´Ð¾Ð±Ñ€ÐµÐ½Ð½Ð°Ñ Ð¿Ñ€Ð¾ÑˆÐ¸Ð²ÐºÐ°:" msgstr[1] "ÐžÐ´Ð¾Ð±Ñ€ÐµÐ½Ð½Ð°Ñ Ð¿Ñ€Ð¾ÑˆÐ¸Ð²ÐºÐ°:" msgstr[2] "ÐžÐ´Ð¾Ð±Ñ€ÐµÐ½Ð½Ð°Ñ Ð¿Ñ€Ð¾ÑˆÐ¸Ð²ÐºÐ°:" msgstr[3] "ÐžÐ´Ð¾Ð±Ñ€ÐµÐ½Ð½Ð°Ñ Ð¿Ñ€Ð¾ÑˆÐ¸Ð²ÐºÐ°:" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "Подключить в режим прошивки" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "ÐутентификациÑ…" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Ð”Ð»Ñ Ð¿Ð¾Ð½Ð¸Ð¶ÐµÐ½Ð¸Ñ Ð²ÐµÑ€Ñии прошивки на Ñъёмном уÑтройÑтве требуетÑÑ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Ð”Ð»Ñ Ð¿Ð¾Ð½Ð¸Ð¶ÐµÐ½Ð¸Ñ Ð²ÐµÑ€Ñии прошивки на Ñтой машине требуетÑÑ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "Ð”Ð»Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð½Ð°Ñтроек BIOS требуетÑÑ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "Ð”Ð»Ñ Ð¼Ð¾Ð´Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ð¸ наÑтроенного репозиториÑ, иÑпользуемого Ð´Ð»Ñ Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾ÑˆÐ¸Ð²ÐºÐ¸, требуетÑÑ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "Ð”Ð»Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð½Ð°Ñтроек фоновой Ñлужбы требуетÑÑ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "Ð”Ð»Ñ ÑƒÑтановки ÑпиÑка одобренных прошивок требуетÑÑ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "Ð”Ð»Ñ Ð¿Ð¾Ð´Ð¿Ð¸Ñи данных Ñ Ð¸Ñпользованием Ñертификата клиента требуетÑÑ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "Ð”Ð»Ñ Ð¿ÐµÑ€ÐµÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ñ Ð½Ð° новую верÑию прошивки требуетÑÑ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Ð”Ð»Ñ Ñ€Ð°Ð·Ð±Ð»Ð¾ÐºÐ¸Ñ€Ð¾Ð²ÐºÐ¸ уÑтройÑтва требуетÑÑ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Ð”Ð»Ñ Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾ÑˆÐ¸Ð²ÐºÐ¸ на Ñъёмном уÑтройÑтве требуетÑÑ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Ð”Ð»Ñ Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾ÑˆÐ¸Ð²ÐºÐ¸ на Ñтой машине требуетÑÑ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "Ð”Ð»Ñ Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ñ…Ñ€Ð°Ð½Ð¸Ð¼Ñ‹Ñ… контрольных Ñумм уÑтройÑтва требуетÑÑ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "ÐžÐ±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ BIOS доÑтавлÑÑŽÑ‚ÑÑ Ñ‡ÐµÑ€ÐµÐ· LVFS или Центр Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Windows." #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "ÐккумулÑтор" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "ПривÑзать новый драйвер Ñдра" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "Блокированные файлы прошивки:" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "Ð—Ð°Ð±Ð»Ð¾ÐºÐ¸Ñ€Ð¾Ð²Ð°Ð½Ð½Ð°Ñ Ð²ÐµÑ€ÑиÑ" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "Ð‘Ð»Ð¾ÐºÐ¸Ñ€Ð¾Ð²Ð°Ð½Ð½Ð°Ñ Ð¿Ñ€Ð¾ÑˆÐ¸Ð²ÐºÐ°:" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "ВерÑÐ¸Ñ Ð·Ð°Ð³Ñ€ÑƒÐ·Ñ‡Ð¸ÐºÐ°" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "Ветка" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "Собрать файл прошивки" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Отменить" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "Отменено" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "Ðе удалоÑÑŒ применить, поÑкольку обновление dbx уже применено." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Изменено" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "ÐšÐ¾Ð½Ñ‚Ñ€Ð¾Ð»ÑŒÐ½Ð°Ñ Ñумма" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "ОчиÑтить результаты c поÑледнего обновлениÑ" #. TRANSLATORS: error message msgid "Command not found" msgstr "Команда не найдена" #. TRANSLATORS: is not supported by the vendor msgid "Community supported" msgstr "ПоддерживаетÑÑ ÑообщеÑтвом" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "Преобразовать файл прошивки" #. TRANSLATORS: when the update was built msgid "Created" msgstr "Создано" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "Критичный" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "Ð¢ÐµÐºÑƒÑ‰Ð°Ñ Ð²ÐµÑ€ÑиÑ" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Параметры отладки" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "РаÑпаковка…" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "ОпиÑание" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "Отключить в режим загрузчика" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "Подробнее" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "Флаги уÑтройÑтва" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "Идентификатор уÑтройÑтва" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Добавлено уÑтройÑтво:" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Заменено уÑтройÑтво:" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "УÑтройÑтво заблокировано" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "УÑтройÑтво недоÑтупно" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Удалено уÑтройÑтво:" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "УÑтройÑтво поддерживает переключение на другую ветку прошивки" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "УÑтройÑтва, которые были уÑпешно обновлены:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "УÑтройÑтва, которые не были правильно обновлены:" #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "УÑтройÑтва без доÑтупных обновлений прошивки:" #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "УÑтройÑтва Ñ Ð¿Ð¾Ñледней доÑтупной верÑией прошивки:" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "Ðе активировано" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "Деактивировать данный репозиторий" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "Ðе проверÑть Ñтарые метаданные" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "Ðе проверÑть незарегиÑтрированную иÑторию" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "Ðе проверÑть, включены ли репозитории" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "Ðе включать Ð¿Ñ€ÐµÑ„Ð¸ÐºÑ Ð´Ð¾Ð¼ÐµÐ½Ð° журнала" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "Ðе включать Ð¿Ñ€ÐµÑ„Ð¸ÐºÑ Ð¼ÐµÑ‚ÐºÐ¸ времени" #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "Ðе ÑохранÑть в базу данных иÑтории" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "Понимаете ли вы поÑледÑÑ‚Ð²Ð¸Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð²ÐµÑ‚ÐºÐ¸ прошивки?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "Хотите отключить Ñту функцию Ð´Ð»Ñ Ð±ÑƒÐ´ÑƒÑ‰Ð¸Ñ… обновлений?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "Хотите ÑÐµÐ¹Ñ‡Ð°Ñ Ð¾Ð±Ð½Ð¾Ð²Ð¸Ñ‚ÑŒ удалённый репозиторий?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "Хотите автоматичеÑки загружать отчёты Ð´Ð»Ñ Ð±ÑƒÐ´ÑƒÑ‰Ð¸Ñ… обновлений?" #. TRANSLATORS: success msgid "Done!" msgstr "Готово!" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "Понизить верÑию прошивки на уÑтройÑтве" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "Понижение верÑии прошивки уÑтройÑтва %s…" #. TRANSLATORS: command description msgid "Download a file" msgstr "Скачать файл" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Получение..." #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "ЗапиÑать данные SMBIOS из файла" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "ПродолжительноÑть" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "Включить новый удалённый репозиторий?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "Ðктивировать Ñтот репозиторий?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Ðктивировано" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "Ðктивировать данный репозиторий" msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "Ðктивирование Ñтого функционала оÑущеÑтвлÑетÑÑ Ð½Ð° ваш Ñтрах и риÑк. Это означает, что вам Ñледует обратитьÑÑ Ðº производителю Ð¾Ð±Ð¾Ñ€ÑƒÐ´Ð¾Ð²Ð°Ð½Ð¸Ñ Ð¾Ñ‚Ð½Ð¾Ñительно любых проблем, вызванных Ñтими обновлениÑми. Только проблемы Ñ Ñ„Ð°ÐºÑ‚Ð¸Ñ‡ÐµÑким процеÑÑом Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ñледует Ñообщать в $OS_RELEASE:BUG_REPORT_URL$." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "ÐÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ñ Ñтого Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ Ð¾ÑущеÑтвлÑетÑÑ Ð½Ð° Ñвой Ñтрах и риÑк." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Зашифровано" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "Шифрование RAM" #. TRANSLATORS: the vendor is no longer supporting the device msgid "End of life" msgstr "Окончание поддержки" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "Стереть вÑÑŽ иÑторию обновлений прошивки" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Стирание…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "Выйти поÑле небольшой задержки" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "Выйти поÑле загрузки движка" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "ЭкÑпорт Ñтруктуры файла прошивки в XML" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "ФÐЙЛ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "ИМЯ_ФÐЙЛÐ" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "Ðе удалоÑÑŒ" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Ðе удалоÑÑŒ применить обновление" #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "Ðе удалоÑÑŒ подключитьÑÑ Ðº фоновой Ñлужбе" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "Ðе удалоÑÑŒ загрузить локальную dbx" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "Ðе удалоÑÑŒ загрузить ÑиÑтемную dbx" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "Ðе удалоÑÑŒ заблокировать" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Ðе удалоÑÑŒ разобрать аргументы" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "Ðе удалоÑÑŒ проанализировать файл" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "Ðе удалоÑÑŒ обработать локальную dbx" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "Ðе удалоÑÑŒ проверить Ñодержимое ESP" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Ð˜Ð¼Ñ Ñ„Ð°Ð¹Ð»Ð°" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "ПодпиÑÑŒ имени файла" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "Ð˜Ð¼Ñ Ñ„Ð°Ð¹Ð»Ð° иÑточника" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "ТребуетÑÑ Ð¸Ð¼Ñ Ñ„Ð°Ð¹Ð»Ð°" #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "База URI микропрограммы" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "D-Bus Ñлужба Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾ÑˆÐ¸Ð²ÐºÐ¸" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Ð¤Ð¾Ð½Ð¾Ð²Ð°Ñ Ñлужба Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾ÑˆÐ¸Ð²ÐºÐ¸" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "СредÑтво работы Ñ Ð¿Ñ€Ð¾ÑˆÐ¸Ð²ÐºÐ°Ð¼Ð¸" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "Прошивка уже заблокирована" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "Прошивка еще не заблокирована" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "Метаданные прошивки не обновлÑлиÑÑŒ в течение %u Ð´Ð½Ñ Ð¸ могут быть уÑтаревшими." msgstr[1] "Метаданные прошивки не обновлÑлиÑÑŒ в течение %u дней и могут быть уÑтаревшими." msgstr[2] "Метаданные прошивки не обновлÑлиÑÑŒ в течение %u дней и могут быть уÑтаревшими." msgstr[3] "Метаданные прошивки не обновлÑлиÑÑŒ в течение %u дней и могут быть уÑтаревшими." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "ÐžÐ±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾ÑˆÐ¸Ð²ÐºÐ¸" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "Флаги" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Ðайдено" #. TRANSLATORS: we might ask the user the recovery key when next booting #. Windows msgid "Full disk encryption secrets may be invalidated when updating" msgstr "Ключи ÑˆÐ¸Ñ„Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ð²Ñего диÑка могут утратить Ñилу при обновлении" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" msgstr[1] "GUID" msgstr[2] "GUID" msgstr[3] "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "Получить вÑе флаги уÑтройÑтв, поддерживаемые fwupd" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Получить вÑе уÑтройÑтва, которые поддерживают Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾ÑˆÐ¸Ð²ÐºÐ¸" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "Получить вÑе активированные плагины, зарегиÑтрированные в ÑиÑтеме" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Получить ÑÐ²ÐµÐ´ÐµÐ½Ð¸Ñ Ð¾ файле прошивки" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "Получить наÑтроенные репозитории" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Получить ÑпиÑок обновлений Ð´Ð»Ñ Ð¿Ð¾Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ð¾Ð³Ð¾ оборудованиÑ" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "Получить релизы Ð´Ð»Ñ ÑƒÑтройÑтва" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Получить результаты Ñ Ð¿Ð¾Ñледнего обновлениÑ" #. TRANSLATORS: the release urgency msgid "High" msgstr "Ð’Ñ‹Ñокий" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "БездейÑтвие…" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "Игнорировать Ñтрогие проверки SSL при загрузке файлов" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "ПродолжительноÑть уÑтановки" msgid "Install old version of signed system firmware" msgstr "УÑтановить Ñтарую верÑию подпиÑанной ÑиÑтемной прошивки" msgid "Install old version of unsigned system firmware" msgstr "УÑтановить Ñтарую верÑию неподпиÑанной ÑиÑтемной прошивки" msgid "Install signed device firmware" msgstr "УÑтановить подпиÑанную прошивку уÑтройÑтва" msgid "Install signed system firmware" msgstr "УÑтановить подпиÑанную ÑиÑтемную прошивку" msgid "Install unsigned device firmware" msgstr "УÑтановить неподпиÑанную прошивку уÑтройÑтва" msgid "Install unsigned system firmware" msgstr "УÑтановить неподпиÑанную ÑиÑтемную прошивку" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "УÑтановка Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾ÑˆÐ¸Ð²ÐºÐ¸â€¦" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "УÑтановка на уÑтройÑтво %s…" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "Intel BootGuard защищено ACM" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Политика ошибок Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "ÐŸÑ€Ð¾Ð²ÐµÑ€ÐµÐ½Ð½Ð°Ñ Ð·Ð°Ð³Ñ€ÑƒÐ·ÐºÐ° Intel BootGuard" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "Внутреннее уÑтройÑтво" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "ÐедопуÑтимые аргументы" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "Более ÑтараÑ" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "Обновление" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "Проблема" msgstr[1] "Проблемы" msgstr[2] "Проблем" msgstr[3] "Проблема" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "РÐСПОЛОЖЕÐИЕ" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "ПоÑледнее изменение" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "ОÑталоÑÑŒ меньше минуты" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "ЛицензиÑ" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Linux Vendor Firmware Service (ÑÑ‚Ð°Ð±Ð¸Ð»ÑŒÐ½Ð°Ñ Ð¿Ñ€Ð¾ÑˆÐ¸Ð²ÐºÐ°)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Linux Vendor Firmware Service (теÑÑ‚Ð¾Ð²Ð°Ñ Ð¿Ñ€Ð¾ÑˆÐ¸Ð²ÐºÐ°)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Ядро линукÑ" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Ядро Ð»Ð¸Ð½ÑƒÐºÑ Ñ Ñ„ÑƒÐ½ÐºÑ†Ð¸ÐµÐ¹ lockdown" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "СпиÑок запиÑей в dbx" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "ВывеÑти ÑпиÑок файлов на ESP" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "Загрузка…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "Заблокировано" #. TRANSLATORS: the release urgency msgid "Low" msgstr "Ðизкий" msgid "MEI version" msgstr "ВерÑÐ¸Ñ MEI" #. TRANSLATORS: Longest valid string for BIOS setting msgid "Maximum length" msgstr "МакÑÐ¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ Ð´Ð»Ð¸Ð½Ð°" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "МакÑимальное значение" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "Средний" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "URI метаданных" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "Метаданные можно получить в Linux Vendor Firmware Service." #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "ÐœÐ¸Ð½Ð¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ Ð²ÐµÑ€ÑиÑ" #. TRANSLATORS: Shortest valid string for BIOS setting msgid "Minimum length" msgstr "ÐœÐ¸Ð½Ð¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ Ð´Ð»Ð¸Ð½Ð°" #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "Минимальное значение" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "Модифицировать данный репозиторий" msgid "Modify a configured remote" msgstr "Изменить наÑтроенный репозиторий" msgid "Modify daemon configuration" msgstr "Изменить наÑтройки фоновой Ñлужбы" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "Следить за ÑобытиÑми в фоновой Ñлужбе" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "Монтировать ESP" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "ТребуетÑÑ Ð¿ÐµÑ€ÐµÐ·Ð°Ð³Ñ€ÑƒÐ·ÐºÐ°" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "ÐÐ¾Ð²Ð°Ñ Ð²ÐµÑ€ÑиÑ" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "Ðе определено никаких дейÑтвий." #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "Идентификаторы прошивок не найдены" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Ðе обнаружено Ð¾Ð±Ð¾Ñ€ÑƒÐ´Ð¾Ð²Ð°Ð½Ð¸Ñ Ñ Ð²Ð¾Ð·Ð¼Ð¾Ð¶Ð½Ð¾Ñтью Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾ÑˆÐ¸Ð²ÐºÐ¸" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "Ðет доÑтупных выпуÑков" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "Ð’ наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ ÑƒÑ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ð¸ не активированы, поÑтому метаданные недоÑтупны." #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "Ðет доÑтупных репозиториев" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "Ðет пригодных Ð´Ð»Ñ Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ ÑƒÑтройÑтв" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "Ðет доÑтупных обновлений" msgid "No updates available for remaining devices" msgstr "Ðет доÑтупных обновлений Ð´Ð»Ñ Ð¾Ñтальных уÑтройÑтв" #. TRANSLATORS: version cannot be installed due to policy msgid "Not approved" msgstr "Ðе одобреннаÑ" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "Ðе найдено" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "Ðе поддерживаетÑÑ" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "ОК" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "OK!" #. TRANSLATORS: the firmware old version msgid "Old version" msgstr "Ð¡Ñ‚Ð°Ñ€Ð°Ñ Ð²ÐµÑ€ÑиÑ" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "Показывать только одно значение PCR" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "Переопределить путь ESP по умолчанию" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "ПУТЬ" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "Разобрать и показать детали о файле прошивки" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "Обработка Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ dbx…" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "Обработка ÑиÑтемной dbx…" #. TRANSLATORS: remote filename base msgid "Password" msgstr "Пароль" msgid "Payload" msgstr "ÐŸÐ¾Ð»ÐµÐ·Ð½Ð°Ñ Ð½Ð°Ð³Ñ€ÑƒÐ·ÐºÐ°" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "Ð’ очереди" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "Выполнить операцию?" #. TRANSLATORS: 'recovery key' here refers to a code, rather than a physical #. metal thing msgid "Please ensure you have the volume recovery key before continuing." msgstr "Прежде чем продолжить, убедитеÑÑŒ, что у Ð²Ð°Ñ ÐµÑть ключ воÑÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ñ‚Ð¾Ð¼Ð°." #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "ПожалуйÑта, введите чиÑло от 0 до %u:" #. TRANSLATORS: Possible values for a bios setting msgid "Possible Values" msgstr "Возможные значениÑ" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "ÐŸÑ€ÐµÐ´Ð·Ð°Ð³Ñ€ÑƒÐ·Ð¾Ñ‡Ð½Ð°Ñ Ð·Ð°Ñ‰Ð¸Ñ‚Ð° DMA" #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "ÐŸÑ€ÐµÐ´Ñ‹Ð´ÑƒÑ‰Ð°Ñ Ð²ÐµÑ€ÑиÑ" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Приоритет" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "Проблемы" msgid "Proceed with upload?" msgstr "Продолжить загрузку?" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "ПроприетарнаÑ" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "Только чтение" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "Чтение из %s…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Чтение…" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "Обновить метаданные Ñ ÑƒÐ´Ð°Ð»ÐµÐ½Ð½Ð¾Ð³Ð¾ Ñервера" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "ПереуÑтановить прошивку на уÑтройÑтве" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "Ветка выпуÑка" #. TRANSLATORS: release attributes msgid "Release Flags" msgstr "Флаги выпуÑка" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "ID выпуÑка" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "Идентификатор репозиториÑ" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "URI Ð´Ð»Ñ Ð¾Ñ‚Ñ‡ÐµÑ‚Ð°" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "ТребуетÑÑ Ð¿Ð¾Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ðµ к Ñети Интернет" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Перезагрузить ÑейчаÑ?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "ПерезапуÑтить фоновую Ñлужбу, чтобы Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð²Ñтупили в Ñилу?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "ПерезапуÑк уÑтройÑтва…" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "Показать идентификаторы вÑех уÑтройÑтв на машине" #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "ЗапуÑтить процедуру очиÑтки ÑоÑтавного плагина, иÑÐ¿Ð¾Ð»ÑŒÐ·ÑƒÑ Ð´Ð²Ð¾Ð¸Ñ‡Ð½ÑƒÑŽ уÑтановку" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "ЗапуÑтить процедуру подготовки ÑоÑтавного плагина, иÑÐ¿Ð¾Ð»ÑŒÐ·ÑƒÑ Ð´Ð²Ð¾Ð¸Ñ‡Ð½ÑƒÑŽ уÑтановку" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "ДеÑкриптор SPI BIOS" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "Регион SPI BIOS" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "Блокировка SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "ЗапиÑÑŒ SPI" #. TRANSLATORS: Scalar increment for integer BIOS setting msgid "Scalar Increment" msgstr "СкалÑрное приращение" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Планировка…" #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "Secure Boot отключен" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "Secure Boot включен" #. TRANSLATORS: the %1 is a URL to a wiki page #, c-format msgid "See %s for more details." msgstr "Подробнее Ñмотрите %s." #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "Подробнее Ñмотрите %s." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Выбранное уÑтройÑтво" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Выбранный том" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "Серийный номер" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "УÑтановить ÑпиÑок одобренных прошивок" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "ÐаÑтройки вÑтупÑÑ‚ в Ñилу поÑле перезагрузки ÑиÑтемы" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "ПоделитьÑÑ Ð¸Ñторией прошивки Ñ Ñ€Ð°Ð·Ñ€Ð°Ð±Ð¾Ñ‚Ñ‡Ð¸ÐºÐ°Ð¼Ð¸" #. TRANSLATORS: command line option msgid "Show all results" msgstr "Показать вÑе результаты" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "Показать верÑии клиента и фоновой Ñлужбы" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "Показать подробную информацию о фоновой Ñлужбе Ð´Ð»Ñ Ð¾Ð¿Ñ€ÐµÐ´ÐµÐ»ÐµÐ½Ð½Ð¾Ð³Ð¾ домена" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "Показать отладочную информацию Ð´Ð»Ñ Ð²Ñех доменов" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Показать параметры отладки" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Показать уÑтройÑтва, обновление Ð´Ð»Ñ ÐºÐ¾Ñ‚Ð¾Ñ€Ñ‹Ñ… невозможно" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Показать дополнительную отладочную информацию" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "Показать иÑторию обновлений прошивки" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "Показать раÑÑчитанную верÑию dbx" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "Выключить ÑейчаÑ?" msgid "Sign data using the client certificate" msgstr "ПодпиÑать данные Ñ Ð¸Ñпользованием Ñертификата клиента" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "ПодпиÑать данные Ñ Ð¸Ñпользованием Ñертификата клиента" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "ПодпиÑать загруженные данные Ñ Ð¸Ñпользованием Ñертификата клиента" msgid "Signature" msgstr "ПодпиÑÑŒ" #. TRANSLATORS: firmware is verified on-device the payload using strong crypto msgid "Signed Payload" msgstr "ПодпиÑанное Ñодержимое" #. TRANSLATORS: file size of the download msgid "Size" msgstr "Размер" #. TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM msgid "Some of the platform secrets may be invalidated when updating this firmware." msgstr "При обновлении Ñтой прошивки некоторые ключи платформы могут утратить Ñилу." #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "ИÑточник" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "Указать файл базы данных dbx" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "УÑпешно" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "Ð’Ñе уÑтройÑтва уÑпешно активированы" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "Удалённый репозиторий уÑпешно отключен" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "УÑпешно загружены новые метаданные:" #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "Удалённый репозиторий уÑпешно подключен и обновлён" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "Удалённый репозиторий уÑпешно подключен" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "Прошивка уÑпешно уÑтановлена" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "УÑпешно изменено значение в наÑтройках" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "УÑпешно изменено удалённое хранилище" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "Метаданные уÑпешно обновлены вручную" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "Контрольные Ñуммы уÑтройÑтва уÑпешно обновлены" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "УÑпешно загружен %u отчёт" msgstr[1] "УÑпешно загружено %u отчёта" msgstr[2] "УÑпешно загружено %u отчётов" msgstr[3] "УÑпешно загружено %u отчётов" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "Контрольные Ñуммы уÑтройÑтва уÑпешно проверены" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Сводка" #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "ПоддерживаетÑÑ" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "Поддерживаемый CPU" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "ПереключитьÑÑ Ñ Ð²ÐµÑ‚ÐºÐ¸ %s на %s?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "Переключить ветку прошивки на уÑтройÑтве" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "РеконÑÑ‚ÑƒÑ€ÐºÑ†Ð¸Ñ PCR0 TPM" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is invalid" msgstr "РеконÑÑ‚Ñ€ÑƒÐºÑ†Ð¸Ñ PCR0 TPM недейÑтвительна" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM empty PCRs" msgstr "ПуÑтые PCR TPM" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "Метка" msgstr[1] "Метки" msgstr[2] "Меток" msgstr[3] "Метки" #. show the user the entire data blob msgid "Target" msgstr "Цель" #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "LVFS — Ñто беÑплатный ÑервиÑ, дейÑтвующий как незавиÑимое юридичеÑкое лицо, которое не имеет ÑвÑзи Ñ ÑиÑтемой $OS_RELEASE:NAME$. Ваш раÑпроÑтранитель ÑиÑтемы может не проверÑть какие-либо Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾ÑˆÐ¸Ð²ÐºÐ¸ на ÑовмеÑтимоÑть Ñ ÑиÑтемой или подключенными уÑтройÑтвами. ÐšÐ°Ð¶Ð´Ð°Ñ Ð¿Ñ€Ð¾ÑˆÐ¸Ð²ÐºÐ° поÑтавлÑетÑÑ Ñ‚Ð¾Ð»ÑŒÐºÐ¾ производителем оригинального оборудованиÑ." #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "Ðет заблокированных файлов прошивки" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "Одобренной прошивки нет." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "Эта программа может корректно работать только как root" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "Этот репозиторий Ñодержит прошивки, которое не запрещены, но вÑе еще теÑтируютÑÑ Ð¿Ñ€Ð¾Ð¸Ð·Ð²Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÐµÐ¼ оборудованиÑ. Ð’Ñ‹ должны убедитьÑÑ, что у Ð²Ð°Ñ ÐµÑть ÑпоÑоб вручную понизить верÑию прошивки уÑтройÑтва в Ñлучае ÑÐ±Ð¾Ñ Ð¿Ñ€Ð¸ обновлении." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "Этот инÑтрумент позволÑет админиÑтратору применÑть Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ UEFI dbx." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "Этот инÑтрумент читает и обрабатывает журнал Ñобытий TPM, который заполнÑетÑÑ ÑиÑтемными прошивками." #. TRANSLATORS: We verified the metadata against the server msgid "Trusted metadata" msgstr "Ðадёжные метаданные" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Тип" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "Утилита Ð´Ð»Ñ UEFI dbx" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "Ключ платформы UEFI" #. TRANSLATORS: error message msgid "Unable to find attribute" msgstr "Ðе удалоÑÑŒ найти атрибут" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "ОтвÑзать тукущий драйвер" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "Ð Ð°Ð·Ð±Ð»Ð¾ÐºÐ¸Ñ€Ð¾Ð²Ð°Ð½Ð½Ð°Ñ Ð¿Ñ€Ð¾ÑˆÐ¸Ð²ÐºÐ°:" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "Ðе зашифровано" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "ÐеизвеÑтно" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "ÐеизвеÑтное уÑтройÑтво" msgid "Unlock the device to allow access" msgstr "Разблокировать уÑтройÑтво Ð´Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð´Ð¾Ñтупа" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "Разблокировано" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "Разблокировать уÑтройÑтво Ð´Ð»Ñ Ð´Ð¾Ñтупа к прошивке" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "Размонтировать ESP" #. TRANSLATORS: firmware payload is unsigned and it is possible to modify it msgid "Unsigned Payload" msgstr "ÐеподпиÑанное Ñодержимое" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "ÐÐµÐ¿Ð¾Ð´Ð´ÐµÑ€Ð¶Ð¸Ð²Ð°ÐµÐ¼Ð°Ñ Ñ„Ð¾Ð½Ð¾Ð²Ð°Ñ Ñлужба верÑии %s, клиент верÑии %s" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Ошибка обновлениÑ" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "Изображение обновлениÑ" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "Сообщение об обновлении" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "Обновить ÑтатуÑ" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "Ошибка Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ â€” извеÑÑ‚Ð½Ð°Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ð°, поÑетите Ñтот URL Ð´Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð´Ð¾Ð¿Ð¾Ð»Ð½Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð¾Ð¹ информации:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Обновить ÑейчаÑ?" msgid "Update the stored device verification information" msgstr "Обновить хранимую проверочную информацию уÑтройÑтва" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "Обновить Ñохраненные метаданные Ñ Ñ‚ÐµÐºÑƒÑ‰Ð¸Ð¼ Ñодержимым" msgid "Updating" msgstr "Обновление" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "Обновление уÑтройÑтва %s…" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "Обновить %s Ñ %s до %s?" #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "Загрузка отчетов о прошивке помогает поÑтавщикам Ð¾Ð±Ð¾Ñ€ÑƒÐ´Ð¾Ð²Ð°Ð½Ð¸Ñ Ð±Ñ‹Ñтро определÑть неудачные и уÑпешные Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð½Ð° реальных уÑтройÑтвах." #. TRANSLATORS: remote filename base msgid "Username" msgstr "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "Проверка Ñодержимого ESP…" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "Вариант" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "Производитель" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "Проверка…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "ВерÑиÑ" #. TRANSLATORS: the fwupd version the release was tested on msgid "Version[fwupd]" msgstr "ВерÑиÑ[fwupd]" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "Ожидание…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "Следить за аппаратными изменениÑми" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "ЗапиÑÑŒ файла:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "ЗапиÑь…" #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "Ваш раÑпроÑтранитель ÑиÑтемы может не проверÑть какие-либо Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾ÑˆÐ¸Ð²ÐºÐ¸ на ÑовмеÑтимоÑть Ñ ÑиÑтемой или подключенными уÑтройÑтвами." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[ID-УСТРОЙСТВÐ|GUID] [ВЕТКÐ]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "по умолчанию" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "Утилита Ð´Ð»Ñ Ñ€Ð°Ð±Ð¾Ñ‚Ñ‹ Ñ Ð·Ð°Ð¿Ð¸ÑÑми журнала Ñобытий fwupd TPM" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "плагины fwupd" fwupd-2.0.10/po/si.po000066400000000000000000003037401501337203100143120ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # HelaBasa Group , 2021-2022 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Sinhala (http://app.transifex.com/freedesktop/fwupd/language/si/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: si\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "විනà·à¶©à·’ %.0f à¶šà·Š ඉතිරියි" msgstr[1] "විනà·à¶©à·’ %.0f à¶šà·Š ඉතිරියි" #. TRANSLATORS: BMC refers to baseboard management controller which #. * is the device that updates all the other firmware on the system #, c-format msgid "%s BMC Update" msgstr "%s BMC යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "%s à¶¶à·à¶§à¶»à·’ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "%s CPU මයික්â€à¶»à·œà¶šà·à¶©à·Š යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "%s à¶šà·à¶¸à¶»à· යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "%s වින්â€à¶ºà·à·ƒ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "%s à¶´à·à¶»à·’à¶·à·à¶œà·’à¶š ME යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "%s à¶´à·à¶½à¶š යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "%s ආයතනික ME යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "%s à¶‹à¶´à·à¶‚à¶œ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම" #. TRANSLATORS: Video Display refers to the laptop internal display or #. * external monitor #, c-format msgid "%s Display Update" msgstr "%s සංදර්à·à¶š යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "%s Embedded Controller Update" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "%s යතුරුපුවරු යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "%s ME යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "%s මූසික යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "%s à¶¢à·à¶½ අතුරුමුහුණත යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "%s ගබඩ෠පà·à¶½à¶š යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "%s පද්ධති යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "%s TPM යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶±" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "%s Thunderbolt Controller යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "%s Touchpad යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම" #. TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth #. * device that stays in the USB port so the wireless peripheral works #, c-format msgid "%s USB Receiver Update" msgstr "%s USB ග්â€à¶»à·à·„කය෠යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "%s යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කරන්න" #. TRANSLATORS: warn the user before updating, %1 is a device name #, c-format msgid "%s and all connected devices may not be usable while updating." msgstr "%s සහ සියලුම සම්බන්ධිත à¶‹à¶´à·à¶‚à¶œ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීමේදී à¶·à·à·€à·’à¶­ à¶šà·… නොහà·à¶š." #. TRANSLATORS: %1 refers to some kind of security test, e.g. "Encrypted RAM". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s appeared: %s" msgstr "%s පෙනී සිටියේය: %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "UEFI platform #. key". #. * %2 and %3 refer to results value, e.g. "Valid" and "Invalid" #, c-format msgid "%s changed: %s → %s" msgstr "%s වෙනස් විය: %s → %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "SPI BIOS #. region". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s disappeared: %s" msgstr "%s අතුරුදහන් විය: %s" #. TRANSLATORS: the device has a reason it can't update, e.g. laptop lid #. closed #, c-format msgid "%s is not currently updatable" msgstr "%s දà·à¶±à¶§ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± à¶šà·… නොහà·à¶š" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "%s නිෂ්පà·à¶¯à¶± මà·à¶¯à·’ලිය" #. TRANSLATORS: warn the user before #. * updating, %1 is a device name #, c-format msgid "%s must remain connected for the duration of the update to avoid damage." msgstr "à·„à·à¶±à·’ය වළක්ව෠ගà·à¶±à·“ම සඳහ෠යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± à¶šà·à¶½à¶º සඳහ෠%s සම්බන්ධව තිබිය යුතුය." #. TRANSLATORS: warn the user before updating, %1 is a machine #. * name #, c-format msgid "%s must remain plugged into a power source for the duration of the update to avoid damage." msgstr "à·„à·à¶±à·’ය වළක්ව෠ගà·à¶±à·“ම සඳහ෠යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± à¶šà·à¶½à·ƒà·“මà·à·€ සඳහ෠%s à¶¶à¶½ à¶´à·Šâ€à¶»à¶·à·€à¶ºà¶šà¶§ සම්බන්ධ à¶šà·… යුතුය." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "%s අභිබව෠යයි" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "අනුවà·à¶¯à¶º %s" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "දින %u යි" msgstr[1] "දින %u යි" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "%u à¶‹à¶´à·à¶‚ගයේ ස්ථිරà·à¶‚à¶œ à¶‹à¶­à·Šà·à·Šâ€à¶»à·šà¶«à·’ගත කිරීමක් ඇත." msgstr[1] "%u à¶‹à¶´à·à¶‚à¶œ සඳහ෠ස්ථිරà·à¶‚à¶œ à¶‹à¶­à·Šà·à·Šâ€à¶»à·šà¶«à·’ගත කිරීමක් ඇත." #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device is not the best known configuration." msgid_plural "%u devices are not the best known configuration." msgstr[0] "%u à¶‹à¶´à·à¶‚ගය හොඳම දන්න෠වින්â€à¶ºà·à·ƒà¶º නොවේ." msgstr[1] "%u à¶‹à¶´à·à¶‚à¶œ හොඳම දන්න෠වින්â€à¶ºà·à·ƒà¶º නොවේ." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "à¶´à·à¶º %u යි" msgstr[1] "à¶´à·à¶º %u යි" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "විනà·à¶©à·’ %u" msgstr[1] "විනà·à¶©à·’ %u" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "à¶­à¶­à·Šà¶´à¶» %u" msgstr[1] "à¶­à¶­à·Šà¶´à¶» %u" #. TRANSLATORS: first percentage is current value, 2nd percentage is the #. * lowest limit the firmware update is allowed for the update to happen #, c-format msgid "%u%% (threshold %u%%)" msgstr "%u%% (ඉන්පසුව %u%%)" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(යල් à¶´à·à¶± ගිය)" #. TRANSLATORS: HSI event title msgid "A TPM PCR is now an invalid value" msgstr "TPM PCR දà·à¶±à·Š වලංගු නොවන අගයකි" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "à¶…à·€à·à·Šâ€à¶º à¶šà·Šâ€à¶»à·’යà·à¶¸à·à¶»à·Šà¶œ:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "à¶‹à¶´à·à¶‚à¶œ සක්රිය කරන්න" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "පොරොත්තු à¶‹à¶´à·à¶‚à¶œ සක්â€à¶»à·’ය කරන්න" msgid "Activate the new firmware on the device" msgstr "à¶‹à¶´à·à¶‚ගයේ නව ස්ථිරà·à¶‚à¶œ සක්රිය කරන්න" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "ස්ථිරà·à¶‚à¶œ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම සක්රිය කිරීම" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "සඳහ෠ස්ථිරà·à¶‚à¶œ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම සක්රිය කිරීම" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "වයස" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "à¶‘à¶šà¶Ÿ වී දුරස්ථ à¶´à·à¶½à¶šà¶º සබල කරන්නද?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "%s à¶§ à¶…à¶´à¶±à·à¶¸à¶º" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are now valid" msgstr "සියලුම TPM PCRs දà·à¶±à·Š වලංගු වේ" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are valid" msgstr "සියලුම TPM PCR වලංගු වේ" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "එකම වර්ගයේ සියලුම à¶‹à¶´à·à¶‚à¶œ එකම අවස්ථà·à·€à·šà¶¯à·“ම යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± වේ" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "ස්ථිරà·à¶‚à¶œ අනුවà·à¶¯ à¶´à·„à¶­ හෙලීමට ඉඩ දෙන්න" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "පවතින ස්ථිරà·à¶‚à¶œ අනුවà·à¶¯ à¶±à·à·€à¶­ ස්ථà·à¶´à¶±à¶º කිරීමට ඉඩ දෙන්න" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "ස්ථිරà·à¶‚à¶œ à·à·à¶›à·à·€ මà·à¶»à·” කිරීමට ඉඩ දෙන්න" #. TRANSLATORS: is not the main firmware stream msgid "Alternate branch" msgstr "විකල්ප à·à·à¶›à·à·€" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "යà·à·€à¶­à·Šà¶šà·à¶½à·“නයක් සම්පූර්ණ කිරීමට à¶±à·à·€à¶­ à¶´à¶«à¶œà·à¶±à·Šà·€à·“මක් à¶…à·€à·à·Šâ€à¶º වේ." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "යà·à·€à¶­à·Šà¶šà·à¶½à·“නයක් සම්පූර්ණ කිරීමට පද්ධතිය වස෠දà·à¶¸à·“ම à¶…à·€à·à·Šâ€à¶º වේ." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "සියලුම à¶´à·Šâ€à¶»à·à·Šà¶± වලට ඔව් පිළිතුරු දෙන්න" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "උපදෙස් නොදෙන විට පව෠යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කරන්න" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "යà·à·€à¶­à·Šà¶šà·à¶½ ගොනු යෙà·à¶¯à¶±à·Šà¶±" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "යà·à·€à¶­à·Šà¶šà·à¶½à¶º යොදමින්…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "අනුමත ස්ථිරà·à¶‚à¶œ:" msgstr[1] "අනුමත ස්ථිරà·à¶‚à¶œ:" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "ස්ථිරà·à¶‚à¶œ මà·à¶¯à·’ලියට අමුණන්න" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "…සත්â€à¶ºà·à¶´à¶±à¶º කිරීම" #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "සත්â€à¶ºà·à¶´à¶± විස්තර à¶…à·€à·à·Šâ€à¶ºà¶ºà·’" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "ඉවත් à¶šà·… à·„à·à¶šà·’ à¶‹à¶´à·à¶‚ගයක ස්ථිරà·à¶‚à¶œ à¶´à·„à¶­ හෙලීමට සත්â€à¶ºà·à¶´à¶±à¶º à¶…à·€à·à·Šâ€à¶º වේ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "මෙම යන්ත්â€à¶»à¶ºà·š ස්ථිරà·à¶‚à¶œ à¶´à·„à¶­ හෙලීමට සත්â€à¶ºà·à¶´à¶±à¶º à¶…à·€à·à·Šâ€à¶º වේ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "ස්ථිරà·à¶‚à¶œ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම් සඳහ෠භà·à·€à·’ත෠කරන වින්â€à¶ºà·à·ƒ à¶šà·… දුරස්ථ à¶´à·à¶½à¶šà¶ºà¶šà·Š වෙනස් කිරීමට සත්â€à¶ºà·à¶´à¶±à¶º à¶…à·€à·à·Šâ€à¶º වේ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "ඩීමන් වින්â€à¶ºà·à·ƒà¶º වෙනස් කිරීමට සත්â€à¶ºà·à¶´à¶±à¶º à¶…à·€à·à·Šâ€à¶º වේ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "අනුමත ස්ථිරà·à¶‚à¶œ à¶½à·à¶ºà·’ස්තුව à·ƒà·à¶šà·ƒà·“මට සත්â€à¶ºà·à¶´à¶±à¶º à¶…à·€à·à·Šâ€à¶º වේ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "සේවà·à¶¯à·à¶ºà¶š සහතිකය à¶·à·à·€à·’තයෙන් දත්ත අත්සන් කිරීමට සත්â€à¶ºà·à¶´à¶±à¶º à¶…à·€à·à·Šâ€à¶º වේ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "නව ස්ථිරà·à¶‚à¶œ අනුවà·à¶¯à¶º වෙත මà·à¶»à·” වීමට සත්â€à¶ºà·à¶´à¶±à¶º à¶…à·€à·à·Šâ€à¶º වේ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "à¶‹à¶´à·à¶‚ගයක් අගුලු à·„à·à¶»à·“මට සත්â€à¶ºà·à¶´à¶±à¶º à¶…à·€à·à·Šâ€à¶º වේ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "ඉවත් à¶šà·… à·„à·à¶šà·’ à¶‹à¶´à·à¶‚ගයක ස්ථිරà·à¶‚à¶œ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීමට සත්â€à¶ºà·à¶´à¶±à¶º à¶…à·€à·à·Šâ€à¶º වේ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "මෙම යන්ත්â€à¶»à¶ºà·š ස්ථිරà·à¶‚à¶œ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීමට සත්â€à¶ºà·à¶´à¶±à¶º à¶…à·€à·à·Šâ€à¶º වේ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "à¶‹à¶´à·à¶‚ගය සඳහ෠ගබඩ෠කර ඇති චෙක්සම් යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීමට සත්â€à¶ºà·à¶´à¶±à¶º à¶…à·€à·à·Šâ€à¶º වේ" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "ස්වයංක්â€à¶»à·“ය à·€à·à¶»à·Šà¶­à·à¶šà¶»à¶«à¶º" #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "සෑම විටම ස්වයංක්â€à¶»à·“යව උඩුගත කරන්නද?" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "BUILDER-XML FILENAME-DST" msgstr "BUILDER-XML ගොනු à¶±à·à¶¸à¶º-DST" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "à¶¶à·à¶§à¶»à·’" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "නව කර්නල් à¶°à·à·€à¶šà¶º à¶¶à·à¶³à¶±à·Šà¶±" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "අවහිර à¶šà·… ස්ථිරà·à¶‚à¶œ ගොනු:" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "අවහිර à¶šà·… අනුවà·à¶¯à¶º" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "ස්ථිරà·à¶‚à¶œ අවහිර කිරීම:" #. TRANSLATORS: command description msgid "Blocks a specific firmware from being installed" msgstr "නිà·à·Šà¶ à·’à¶­ ස්ථිරà·à¶‚à¶œ ස්ථà·à¶´à¶±à¶º කිරීම අවහිර කරයි" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "Bootloader අනුවà·à¶¯à¶º" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "à·à·à¶›à·à·€" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "ස්ථිරà·à¶‚à¶œ ගොනුවක් à·ƒà·à¶¯à¶±à·Šà¶±" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "අවලංගු කරන්න" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "අවලංගු කෙරිණි" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "dbx යà·à·€à¶­à·Šà¶šà·à¶½à·“නය දà·à¶±à¶§à¶¸à¶­à·Š යොදව෠ඇති à¶¶à·à·€à·’න් අයදුම් à¶šà·… නොහà·à¶š." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "වෙනස් විය" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "ගුප්ත ලේඛන à·„à·à·‚à·Š à¶œà·à¶½à¶´à·“ම් ස්ථිරà·à¶‚à¶œ පරීක්ෂ෠කරයි" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "චෙක්සම්" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "අවසà·à¶± යà·à·€à¶­à·Šà¶šà·à¶½à·“නයෙන් à¶´à·Šâ€à¶»à¶­à·’ඵල හිස් කරයි" #. TRANSLATORS: error message msgid "Command not found" msgstr "විධà·à¶±à¶º සොයà·à¶œà¶­ නොහà·à¶šà·’" #. TRANSLATORS: is not supported by the vendor msgid "Community supported" msgstr "à¶´à·Šâ€à¶»à¶¢à·à·€à¶œà·š සහà·à¶º" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "ස්ථිරà·à¶‚à¶œ ගොනුවක් පරිවර්තනය කරන්න" #. TRANSLATORS: when the update was built msgid "Created" msgstr "නිර්මà·à¶«à¶º à¶šà·…à·" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "විවේචනà·à¶­à·Šà¶¸à¶š" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "ගුප්ත ලේඛන à·„à·à·‚à·Š සත්â€à¶ºà·à¶´à¶±à¶º ලබ෠ගත à·„à·à¶šà·’ය" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "වත්මන් අනුවà·à¶¯à¶º" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "DEVICE-ID|මà·à¶»à·Šà¶œà·à¶´à¶¯à·šà·à¶º" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "දà·à· නිරà·à¶šà¶»à¶« විකල්ප" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "විසංයà·à¶¢à¶±à¶ºâ€¦" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "විස්තර" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "ඇරඹුම් à¶šà·à¶»à¶š මà·à¶¯à·’ලියට වෙන් කරන්න" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "විස්තර" #. TRANSLATORS: the best known configuration is a set of software that we know #. works #. * well together. In the OEM and ODM industries it is often called a BKC msgid "Deviate from the best known configuration?" msgstr "හොඳින්ම දන්න෠වින්â€à¶ºà·à·ƒà¶ºà·™à¶±à·Š à¶¶à·à·„à·à¶» වන්නද?" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "à¶‹à¶´à·à¶‚à¶œ කොඩි" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "à¶‹à¶´à·à¶‚à¶œ à·„à·à¶³à·”නුම්පත" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "à¶‹à¶´à·à¶‚ගය à¶‘à¶šà¶­à·” à¶šà·…à·š:" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "à¶‹à¶´à·à¶‚ගයේ à¶¶à·à¶§à¶»à·’ බලය ඉත෠අඩුය" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse #, c-format msgid "Device battery power is too low (%u%%, requires %u%%)" msgstr "à¶‹à¶´à·à¶‚à¶œ à¶¶à·à¶§à¶»à·’ බලය ඉත෠අඩුයි (%u%%, %u%% à¶…à·€à·à·Šâ€à¶ºà¶ºà·’)" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "à¶‹à¶´à·à¶‚ගයට ෆ්ලෑෂ් අසමත්වීම් à¶±à·à·€à¶­ ලබ෠ගත à·„à·à¶š" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "à¶‹à¶´à·à¶‚ගය වෙනස් විය:" #. TRANSLATORS: a version check is required for all firmware msgid "Device firmware is required to have a version check" msgstr "අනුවà·à¶¯ පරීක්ෂà·à·€à¶šà·Š කිරීමට à¶‹à¶´à·à¶‚à¶œ ස්ථිරà·à¶‚à¶œ à¶…à·€à·à·Šâ€à¶º වේ" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "à¶‹à¶´à·à¶‚ගය අගුලු දම෠ඇත" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "සපය෠ඇති සියලුම නිකුතු ස්ථà·à¶´à¶±à¶º කිරීමට à¶‹à¶´à·à¶‚ගය à¶…à·€à·à·Šâ€à¶º වේ" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "à¶‹à¶´à·à¶‚ගය ළඟ෠විය නොහà·à¶š" #. TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode msgid "Device is unreachable, or out of wireless range" msgstr "à¶‹à¶´à·à¶‚ගය වෙත ළඟ෠විය නොහà·à¶š, à¶±à·à¶­à·„ොත් à¶»à·à·„à·à¶±à·Š රහිත à¶´à¶»à·à·ƒà¶ºà·™à¶±à·Š à¶´à·’à¶§à¶­" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± à¶šà·à¶½à¶º සඳහ෠උපà·à¶‚ගය à¶·à·à·€à·’à¶­à· à¶šà·… à·„à·à¶šà·’ය" #. TRANSLATORS: usually this is when we're waiting for a reboot msgid "Device is waiting for the update to be applied" msgstr "à¶‹à¶´à·à¶‚ගය යà·à·€à¶­à·Šà¶šà·à¶½à·“නය යෙදෙන තෙක් බල෠සිටී" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "à¶‹à¶´à·à¶‚ගය ඉවත් කරන ලදී:" #. TRANSLATORS: as in, wired mains power for a laptop msgid "Device requires AC power to be connected" msgstr "à¶‹à¶´à·à¶‚ගයට සම්බන්ධ වීමට AC බලය à¶…à·€à·à·Šâ€à¶ºà¶ºà·’" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "à¶‹à¶´à·à¶‚à¶œ අදියර යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම්" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "ස්ථිරà·à¶‚ගයේ වෙනත් à·à·à¶›à·à·€à¶šà¶§ මà·à¶»à·” වීමට à¶‹à¶´à·à¶‚ගය සහà·à¶º දක්වයි" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "à¶‹à¶´à·à¶‚à¶œ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම සක්â€à¶»à·’ය කිරීම à¶…à·€à·à·Šâ€à¶ºà¶ºà·’" #. TRANSLATORS: save the old firmware to disk before installing the new one msgid "Device will backup firmware before installing" msgstr "à¶‹à¶´à·à¶‚ගය ස්ථà·à¶´à¶±à¶º කිරීමට පෙර ස්ථිරà·à¶‚à¶œ උපස්ථ කරයි" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "යà·à·€à¶­à·Šà¶šà·à¶½à·“නය සම්පූර්ණ වූ පසු à¶‹à¶´à·à¶‚ගය à¶±à·à·€à¶­ දිස් නොවනු ඇත" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "à·ƒà·à¶»à·Šà¶®à¶šà·€ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කරන ලද à¶‹à¶´à·à¶‚à¶œ:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "නිවà·à¶»à¶¯à·’à·€ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± නොකළ à¶‹à¶´à·à¶‚à¶œ:" #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "පවතින ස්ථිරà·à¶‚à¶œ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± නොමà·à¶­à·’ à¶‹à¶´à·à¶‚à¶œ: " #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "පවතින නවතම ස්ථිරà·à¶‚à¶œ අනුවà·à¶¯à¶º සහිත à¶‹à¶´à·à¶‚à¶œ:" #. TRANSLATORS: this is for the device tests msgid "Did not find any devices with matching GUIDs" msgstr "à¶œà·à·…පෙන GUID සහිත à¶‹à¶´à·à¶‚à¶œ කිසිවක් සොය෠ගත්තේ à¶±à·à¶­" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "à¶…à¶¶à¶½ à¶šà¶» ඇත" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "ලබ෠දී ඇති දුරස්ථ à¶´à·à¶½à¶šà¶ºà¶šà·Š à¶…à¶¶à¶½ කරයි" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "à¶´à·à¶»à¶«à·’ à¶´à·à¶»à¶¯à¶­à·Šà¶­ සඳහ෠පරීක්ෂ෠නොකරන්න" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "à·€à·à¶»à·Šà¶­à· නොකළ ඉතිහà·à·ƒà¶º පරීක්ෂ෠නොකරන්න" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "à¶¶à·à¶œà·à¶±à·“ම් දුරස්ථ සක්â€à¶»à·“ය à¶šà·… යුතුදà·à¶ºà·’ පරීක්ෂ෠නොකරන්න" #. TRANSLATORS: command line option msgid "Do not check or prompt for reboot after update" msgstr "යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීමෙන් පසු à¶±à·à·€à¶­ à¶´à¶«à¶œà·à¶±à·Šà·€à·“ම සඳහ෠පරීක්ෂ෠නොකරන්න" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "ලොග් වසම් උපසර්ගය ඇතුළත් නොකරන්න" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "වේල෠මුද්දර උපසර්ගය ඇතුළත් නොකරන්න" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "à¶‹à¶´à·à¶‚à¶œ ආරක්ෂණ පරීක්ෂà·à·€à¶±à·Š සිදු නොකරන්න" #. TRANSLATORS: command line option msgid "Do not prompt for devices" msgstr "à¶‹à¶´à·à¶‚à¶œ සඳහ෠විමසන්න à¶‘à¶´à·" #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "ඉතිහà·à·ƒ දත්ත ගබඩà·à·€à¶§ ලියන්න à¶‘à¶´à·" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "ස්ථිරà·à¶‚à¶œ à·à·à¶›à·à·€ වෙනස් කිරීමේ ප්රතිවිපà·à¶š ඔබට තේරෙනවà·à¶¯?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "ඔබට à¶…à¶±à·à¶œà¶­ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± සඳහ෠මෙම විà·à·šà·‚à·à¶‚ගය à¶…à¶¶à¶½ කිරීමට à¶…à·€à·à·Šâ€à¶ºà¶¯?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "ඔබට දà·à¶±à·Š මෙම දුරස්ථ à¶´à·à¶½à¶šà¶º à¶±à·à·€à·”ම් කිරීමට à¶…à·€à·à·Šâ€à¶ºà¶¯?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "ඔබට à¶…à¶±à·à¶œà¶­ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± සඳහ෠වà·à¶»à·Šà¶­à· ස්වයංක්â€à¶»à·“යව උඩුගත කිරීමට à¶…à·€à·à·Šâ€à¶ºà¶¯?" #. TRANSLATORS: success msgid "Done!" msgstr "අහවරයි!" #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "%s %s සිට %sදක්ව෠පහත් කරන්නද?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "à¶‹à¶´à·à¶‚ගයක ස්ථිරà·à¶‚à¶œ à¶´à·„à¶­ හෙළයි" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "à¶´à·„à¶­ හෙලීම %s…" #. TRANSLATORS: command description msgid "Download a file" msgstr "ගොනුවක් à¶¶à·à¶œà¶±à·Šà¶±" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "à¶¶à·à¶œà¶­ වෙමින්…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "ගොනුවකින් SMBIOS දත්ත ඩම්ප් කරන්න" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "à¶šà·à¶½ සීමà·à·€" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "නව දුරස්ථ à¶´à·à¶½à¶šà¶º සබල කරන්නද?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "මෙම දුරස්ථ à¶´à·à¶½à¶šà¶º සබල කරන්නද?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "සබල à¶šà¶» ඇත" #. TRANSLATORS: Plugin is active only if hardware is found msgid "Enabled if hardware matches" msgstr "දෘඪà·à¶‚à¶œ à¶œà·à¶½à¶´à·™à¶±à·Šà¶±à·š නම් සබල à¶šà¶» ඇත" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "දී ඇති දුරස්ථ à¶´à·à¶½à¶šà¶ºà¶šà·Š සබල කරයි" msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "මෙම à¶šà·Šâ€à¶»à·’යà·à¶šà·à¶»à·“ත්වය සක්â€à¶»à·“ය කිරීම ඔබගේම අවදà·à¶±à¶¸à¶šà·’න් සිදු කෙරේ, එයින් අදහස් වන්නේ මෙම යà·à·€à¶­à·Šà¶šà·à¶½à·“නයන් නිස෠ඇති වන à¶œà·à¶§à·…à·” සම්බන්ධයෙන් ඔබ ඔබේ මුල් උපකරණ නිෂ්පà·à¶¯à¶šà¶ºà· හ෠සම්බන්ධ විය යුතු බවයි. $OS_RELEASE:BUG_REPORT_URL$ à·„à·’ ගොනු à¶šà·… යුත්තේ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± à¶šà·Šâ€à¶»à·’යà·à·€à¶½à·’යේ ඇති à¶œà·à¶§à¶½à·” පමණි." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "මෙම දුරස්ථ à¶´à·à¶½à¶šà¶º සබල කිරීම ඔබගේම අවදà·à¶±à¶¸à¶šà·’න් සිදු කෙරේ." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "සංකේතිතයි" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "සංකේතනය à¶šà·… RAM" #. TRANSLATORS: the vendor is no longer supporting the device msgid "End of life" msgstr "ජීවිතයේ අවසà·à¶±à¶º" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "සියලුම ස්ථිරà·à¶‚à¶œ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± ඉතිහà·à·ƒà¶º මකන්න" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "මà·à¶šà·™à¶¸à·’න්…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "කුඩ෠ප්â€à¶»à¶¸à·à¶¯à¶ºà¶šà·’න් පසු පිටවන්න" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "එන්ජිම පූරණය වූ පසු පිටවන්න" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "ස්ථිරà·à¶‚à¶œ ගොනු ව්â€à¶ºà·”හයක් XML වෙත අපනයනය කරන්න" #. TRANSLATORS: command description msgid "Extract a firmware blob to images" msgstr "පින්තූර වෙත ස්ථිරà·à¶‚à¶œ බ්ලොබ් උපුට෠ගන්න" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "ගොනුව" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "ගොනුව [à¶‹à¶´à·à¶‚ගය-ID|මà·à¶»à·Šà¶œà·à¶´à¶¯à·šà·à¶º]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "ගොනුවේ නම" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "ගොනු à¶±à·à¶¸ සහතිකය පුද්ගලික යතුර" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "FILENAME à¶‹à¶´à·à¶‚ගය-ID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME OFFSET DATA [FIRMWARE-TYPE]" msgstr "FILENAME OFFSET දත්ත [FIRMWARE-TYPE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "ගොනු à¶±à·à¶¸à¶º [DEVICE-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "FILENAME [FIRMWARE-TYPE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" msgstr "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" msgstr "FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "අසමත් විය" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "යà·à·€à¶­à·Šà¶šà·à¶½à¶º යෙදීමට අසමත් විය" #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "ඩීමන් වෙත සම්බන්ධ වීමට අසමත් විය" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "දේà·à·“ය dbx පූරණය කිරීමට අසමත් විය" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "පද්ධතිය dbx පූරණය කිරීමට අසමත් විය" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "අගුළු ලෑමට අසමත් විය" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "තර්ක විග්â€à¶»à·„ කිරීමට අසමත් විය" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "ගොනුව විග්â€à¶»à·„ කිරීමට අසමත් විය" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "දේà·à·“ය dbx විග්â€à¶»à·„ කිරීමට අසමත් විය" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "ESP අන්තර්ගතයන් වලංගු කිරීමට අසමත් විය" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "ගොනුවේ නම" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "ගොනු à¶±à·à¶¸à¶º අත්සන" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "ගොනු à¶±à·à¶¸à¶º මූලà·à·à·Šà¶»à¶º" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "ගොනුවේ නම à¶…à·€à·à·Šâ€à¶ºà¶ºà·’" #. TRANSLATORS: command line option msgid "Filter with a set of device flags using a ~ prefix to exclude, e.g. 'internal,~needs-reboot'" msgstr "à¶¶à·à·„à·à¶» කිරීමට ~ උපසර්ගයක් à¶·à·à·€à·’ත෠කරමින් à¶‹à¶´à·à¶‚à¶œ à¶°à¶¢ කට්ටලයක් සමඟ පෙරහන් කරන්න, උද෠'à¶…à¶·à·Šâ€à¶ºà¶±à·Šà¶­à¶»,~needs-reboot'" #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "ස්ථිරà·à¶‚à¶œ පදනම URI" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "ස්ථිරà·à¶‚à¶œ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± D-බස් සේවà·à·€" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "ස්ථිරà·à¶‚à¶œ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± Daemon" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "ස්ථිරà·à¶‚à¶œ උපයà·à¶œà·’à¶­à·" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "ස්ථිරà·à¶‚à¶œ සහතික කිරීම" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "ස්ථිරà·à¶‚à¶œ දà·à¶±à¶§à¶¸à¶­à·Š අවහිර à¶šà¶» ඇත" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "ස්ථිරà·à¶‚à¶œ දà·à¶±à¶§à¶¸à¶­à·Š අවහිර à¶šà¶» à¶±à·à¶­" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "ස්ථිරà·à¶‚à¶œ à¶´à·à¶»à¶¯à¶­à·Šà¶­ දින %u à¶šà·Š සඳහ෠යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± à¶šà¶» නොමà·à¶­à·’ à¶…à¶­à¶» යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± නොවිය à·„à·à¶š." msgstr[1] "ස්ථිරà·à¶‚à¶œ à¶´à·à¶»à¶¯à¶­à·Šà¶­ දින %u à¶šà·Š සඳහ෠යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± à¶šà¶» නොමà·à¶­à·’ à¶…à¶­à¶» යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± නොවිය à·„à·à¶š." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "ස්ථිරà·à¶‚à¶œ යà·à·€à¶­à·Šà¶šà·à¶½" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "කොඩි" #. TRANSLATORS: command line option msgid "Force the action by relaxing some runtime checks" msgstr "සමහර à¶°à·à·€à¶± à¶šà·à¶½ චෙක්පත් ලිහිල් කිරීමෙන් à¶šà·Šâ€à¶»à·’යà·à·€ à¶¶à¶½ කරන්න" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "හමු විය" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "සම්පූර්ණ à¶­à·à¶§à·’ සංකේතනය à¶…à¶±à·à·€à¶»à¶«à¶º විය" #. TRANSLATORS: we might ask the user the recovery key when next booting #. Windows msgid "Full disk encryption secrets may be invalidated when updating" msgstr "යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීමේදී සම්පූර්ණ à¶­à·à¶§à·’ සංකේතà·à¶‚à¶šà¶± රහස් අවලංගු විය à·„à·à¶š" #. TRANSLATORS: Title: if the part has been fused msgid "Fused platform" msgstr "විලයන වේදිකà·à·€" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "මà·à¶»à·Šà¶œà·à¶´à¶¯à·šà·à¶º" msgstr[1] "GUIDs" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "මà·à¶»à·Šà¶œà·à¶´à¶¯à·šà·à¶º" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "fwupd මගින් සහය දක්වන සියලුම à¶‹à¶´à·à¶‚à¶œ කොඩි ලබ෠ගන්න" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "ස්ථිරà·à¶‚à¶œ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම් සඳහ෠සහය දක්වන සියලුම à¶‹à¶´à·à¶‚à¶œ ලබ෠ගන්න" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "සියලුම සක්â€à¶»à·“ය ප්ලගීන පද්ධතිය සමඟ ලියà·à¶´à¶¯à·’à¶‚à¶ à·’ à¶šà¶» ගන්න" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "ස්ථිරà·à¶‚à¶œ ගොනුවක් à¶´à·’à·…à·’à¶¶à¶³ විස්තර ලබ෠ගනී" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "වින්â€à¶ºà·à·ƒà¶œà¶­ දුරස්ථයන් ලබ෠ගනී" #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "සත්කà·à¶»à¶š ආරක්ෂක ගුණà·à¶‚à¶œ ලබ෠ගනී" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "අනුමත ස්ථිරà·à¶‚à¶œ à¶½à·à¶ºà·’ස්තුව ලබ෠ගනී" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "අවහිර à¶šà·… ස්ථිරà·à¶‚à¶œ à¶½à·à¶ºà·’ස්තුව ලබ෠ගනී" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "සම්බන්ධිත දෘඩà·à¶‚à¶œ සඳහ෠යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± à¶½à·à¶ºà·’ස්තුව ලබ෠ගනී" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "à¶‹à¶´à·à¶‚ගයක් සඳහ෠නිකුත් කිරීම් ලබ෠ගනී" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "අවසà·à¶± යà·à·€à¶­à·Šà¶šà·à¶½à·“නයෙන් à¶´à·Šâ€à¶»à¶­à·’ඵල ලබ෠ගනී" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "HWIDS-FILE" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "දෘඪà·à¶‚à¶œ à¶±à·à·€à¶­ පිරවීමට බල෠සිටී" #. TRANSLATORS: the release urgency msgid "High" msgstr "ඉහළ" #. TRANSLATORS: title for host security events msgid "Host Security Events" msgstr "සත්කà·à¶»à¶š ආරක්ෂක සිදුවීම්" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "සත්කà·à¶»à¶š ආරක්ෂක ID:" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: HSI event title msgid "IOMMU device protection disabled" msgstr "IOMMU à¶‹à¶´à·à¶‚à¶œ ආරක්ෂණය à¶…à¶¶à¶½ à¶šà¶» ඇත" #. TRANSLATORS: HSI event title msgid "IOMMU device protection enabled" msgstr "IOMMU à¶‹à¶´à·à¶‚à¶œ ආරක්ෂණය සබල à¶šà¶» ඇත" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "නිෂ්ක්â€à¶»à·“ය…" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "ගොනු à¶¶à·à¶œà¶­ කිරීමේදී SSL දà·à¶©à·’ චෙක්පත් නොසලක෠හරින්න" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "ස්ථිරà·à¶‚à¶œ චෙක්සම් අසමත්වීම් නොසලක෠හරින්න" #. TRANSLATORS: command line option msgid "Ignore firmware hardware mismatch failures" msgstr "ස්ථිරà·à¶‚à¶œ දෘඪà·à¶‚à¶œ නොගà·à¶½à¶´à·“ම à¶…à·ƒà·à¶»à·Šà¶®à¶š වීම නොසලක෠හරින්න" #. TRANSLATORS: try to help msgid "Ignoring SSL strict checks, to do this automatically in the future export DISABLE_SSL_STRICT in your environment" msgstr "SSL දà·à¶©à·’ චෙක්පත් නොසලක෠හà·à¶»à·“ම, à¶…à¶±à·à¶œà¶­à¶ºà·šà¶¯à·“ මෙය ස්වයංක්â€à¶»à·“යව සිදු කිරීම සඳහ෠ඔබේ පරිසරය තුළ DISABLE_SSL_STRICT අපනයනය කරන්න" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "ස්ථà·à¶´à¶± à¶šà·à¶½à¶º" msgid "Install old version of signed system firmware" msgstr "අත්සන් à¶šà·… පද්ධති ස්ථිරà·à¶‚ගයේ à¶´à·à¶»à¶«à·’ අනුවà·à¶¯à¶º ස්ථà·à¶´à¶±à¶º කරන්න" msgid "Install old version of unsigned system firmware" msgstr "අත්සන් නොකළ පද්ධති ස්ථිරà·à¶‚ගයේ à¶´à·à¶»à¶«à·’ අනුවà·à¶¯à¶º ස්ථà·à¶´à¶±à¶º කරන්න" msgid "Install signed device firmware" msgstr "අත්සන් කරන ලද à¶‹à¶´à·à¶‚à¶œ ස්ථිරà·à¶‚à¶œ ස්ථà·à¶´à¶±à¶º කරන්න" msgid "Install signed system firmware" msgstr "අත්සන් à¶šà·… පද්ධති ස්ථිරà·à¶‚à¶œ ස්ථà·à¶´à¶±à¶º කරන්න" msgid "Install unsigned device firmware" msgstr "අත්සන් නොකළ à¶‹à¶´à·à¶‚à¶œ ස්ථිරà·à¶‚à¶œ ස්ථà·à¶´à¶±à¶º කරන්න" msgid "Install unsigned system firmware" msgstr "අත්සන් නොකළ පද්ධති ස්ථිරà·à¶‚à¶œ ස්ථà·à¶´à¶±à¶º කරන්න" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "ස්ථිරà·à¶‚ගයේ අනුවà·à¶¯à¶º ස්ථà·à¶´à¶±à¶º වෙමින්…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "…මත %sකිරීම" #. TRANSLATORS: if it breaks, you get to keep both pieces msgid "Installing this update may also void any device warranty." msgstr "මෙම යà·à·€à¶­à·Šà¶šà·à¶½à·“නය ස්ථà·à¶´à¶±à¶º කිරීමෙන් ඕනෑම à¶‹à¶´à·à¶‚à¶œ වගකීමක් ද අවලංගු විය à·„à·à¶š." #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "Intel BootGuard ACM ආරක්ෂිතයි" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "Intel BootGuard OTP ෆියුස්" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Intel BootGuard දà·à·‚ à¶´à·Šâ€à¶»à¶­à·’පත්තිය" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Intel BootGuard සත්â€à¶ºà·à¶´à¶±à¶º à¶šà·… ඇරඹුම" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "අභ්යන්තර à¶‹à¶´à·à¶‚ගය" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "වලංගු නෙà·à·€à·™à·Š" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "à¶´à·„à¶­ හෙලනු à¶½à·à¶¶à·š" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "à¶¶à·–à¶§à·Šà¶½à·à¶©à¶»à·Š මà·à¶¯à·’ලියේ ඇත" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "à·€à·à¶©à·’දියුණු වේ" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "නිකුත් කිරීම" msgstr[1] "à¶œà·à¶§à¶½à·”" #. TRANSLATORS: HSI event title msgid "Kernel is no longer tainted" msgstr "කර්නලය තවදුරටත් අපිරිසිදු නොවේ" #. TRANSLATORS: HSI event title msgid "Kernel is tainted" msgstr "කර්නලය අපිරිසිදු වේ" #. TRANSLATORS: HSI event title msgid "Kernel lockdown disabled" msgstr "කර්නලය අගුලු දà·à¶¸à·“ම à¶…à¶¶à¶½ à¶šà¶» ඇත" #. TRANSLATORS: HSI event title msgid "Kernel lockdown enabled" msgstr "කර්නල් අගුලු දà·à¶¸à·“ම සබල à¶šà¶» ඇත" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "ස්ථà·à¶±à¶º" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "අවසන් වරට වෙනස් කරන ලදී" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "මිනිත්තුවකට වඩ෠අඩු à¶šà·à¶½à¶ºà¶šà·Š ඉතිරිව ඇත" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "බලපත්රය" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Linux Vendor Firmware Service (ස්ථà·à·€à¶» ස්ථිරà·à¶‚à¶œ)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Linux Vendor Firmware Service (ස්ථිරà·à¶‚à¶œ පරීක්ෂ෠කිරීම)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "ලිනක්ස් කර්නලය" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "ලිනක්ස් කර්නලය අගුලු දà·à¶¸à·“ම" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "ලිනක්ස් හුවමà·à¶»à·”à·€" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "dbx à·„à·’ ඇතුළත් කිරීම් à¶½à·à¶ºà·’ස්තුගත කරන්න" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "පවතින ස්ථිරà·à¶‚à¶œ වර්ග à¶½à·à¶ºà·’ස්තුගත කරන්න" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "ESP මත ගොනු à¶½à·à¶ºà·’ස්තුගත කරයි" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "පූරණය වෙමින්…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "අගුළු ල෠ඇත" #. TRANSLATORS: the release urgency msgid "Low" msgstr "à¶…à¶©à·”" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "MEI නිෂ්පà·à¶¯à¶± මà·à¶¯à·’ලිය" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "MEI අභිබව෠යයි" msgid "MEI version" msgstr "MEI අනුවà·à¶¯à¶º" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "විà·à·šà·‚à·’à¶­ ප්ලගීන අතින් සක්â€à¶»à·“ය කරන්න" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "මධ්යම" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "à¶´à·à¶»à¶¯à¶­à·Šà¶­ අත්සන" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "à¶´à·à¶»à¶¯à¶­à·Šà¶­ URI" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "à¶´à·à¶»à¶¯à¶­à·Šà¶­ Linux Vendor Firmware Service වෙතින් ලබà·à¶œà¶­ à·„à·à¶š." #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "අවම අනුවà·à¶¯à¶º" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "ඩීමන් වින්â€à¶ºà·à·ƒ අගයක් වෙනස් කරයි" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "දී ඇති දුරස්ථ à¶´à·à¶½à¶šà¶ºà¶šà·Š වෙනස් කරයි" msgid "Modify a configured remote" msgstr "වින්â€à¶ºà·à·ƒà¶œà¶­ දුරස්ථ à¶´à·à¶½à¶šà¶ºà¶šà·Š වෙනස් කරන්න" msgid "Modify daemon configuration" msgstr "ඩීමන් වින්â€à¶ºà·à·ƒà¶º වෙනස් කරන්න" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "සිදුවීම් සඳහ෠ඩීමන් නිරීක්ෂණය කරන්න" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "ESP සවි කරයි" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "ස්ථà·à¶´à¶±à¶º කිරීමෙන් පසු à¶±à·à·€à¶­ ආරම්භ කිරීම à¶…à·€à·à·Šà¶º වේ" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "à¶±à·à·€à¶­ à¶´à¶«à¶œà·à¶±à·Šà·€à·“ම à¶…à·€à·à·Šâ€à¶ºà¶ºà·’" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "ස්ථà·à¶´à¶±à¶º කිරීමෙන් පසු වස෠දà·à¶¸à·“ම à¶…à·€à·à·Šà¶º වේ" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "නව අනුවà·à¶¯à¶º" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "à¶šà·Šâ€à¶»à·’යà·à·€à¶šà·Š සඳහන් à¶šà¶» à¶±à·à¶­!" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "%sසඳහ෠පහත් කිරීම් නොමà·à¶­" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "ස්ථිරà·à¶‚à¶œ à·„à·à¶³à·”නුම් කිසිවක් හමු නොවීය" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "ස්ථිරà·à¶‚à¶œ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීමේ à·„à·à¶šà·’යà·à·€ ඇති දෘඪà·à¶‚à¶œ à¶…à¶±à·à·€à¶»à¶«à¶º à¶šà¶» ගෙන නොමà·à¶­" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "නිකුත් කිරීම් නොමà·à¶­" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "කිසිදු දුරස්ථ à¶´à·à¶½à¶šà¶ºà¶šà·Š දà·à¶±à¶§ සබල à¶šà¶» නොමà·à¶­à·’ නිස෠පà·à¶»à¶¯à¶­à·Šà¶­ නොමà·à¶­." #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "දුරස්ථ à¶±à·à¶­" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± à¶šà·… à·„à·à¶šà·’ à¶‹à¶´à·à¶‚à¶œ නොමà·à¶­" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± නොමà·à¶­" msgid "No updates available for remaining devices" msgstr "ඉතිරි à¶‹à¶´à·à¶‚à¶œ සඳහ෠යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± නොමà·à¶­" #. TRANSLATORS: version cannot be installed due to policy msgid "Not approved" msgstr "අනුමත à¶šà¶» à¶±à·à¶­" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "හමු නෙà·à·€à·’à¶«à·’" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "සහà·à¶º නොදක්වයි" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "හරි" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "හරි!" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "තනි PCR අගය පමණක් පෙන්වන්න" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "අනුවà·à¶¯ à¶‹à¶­à·Šà·à·Šâ€à¶»à·šà¶«à·’ කිරීමට පමණක් අවසර ඇත" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "පෙරනිමි ESP මà·à¶»à·Šà¶œà¶º අභිබව෠යන්න" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "මà·à¶»à·Šà¶œà¶º" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "ස්ථිරà·à¶‚à¶œ ගොනුවක් à¶´à·’à·…à·’à¶¶à¶³ විස්තර විග්â€à¶»à·„ à¶šà¶» පෙන්වන්න" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "dbx යà·à·€à¶­à·Šà¶šà·à¶½à·“න…විග්â€à¶»à·„ කිරීම" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "විග්â€à¶»à·„ කිරීමේ පද්ධතිය dbx…" #. TRANSLATORS: remote filename base msgid "Password" msgstr "මුරපදය" #. TRANSLATORS: command description msgid "Patch a firmware blob at a known offset" msgstr "දන්න෠ඕෆ්සෙට් à¶‘à¶šà¶š ස්ථිරà·à¶‚à¶œ බ්ලොබ් à¶‘à¶šà¶šà·Š à¶´à·à¶ à·Š කරන්න" msgid "Payload" msgstr "ගෙවීම" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "පොරොත්තුවෙන්" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "මෙහෙයුම සිදු කරන්නද?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "වේදික෠නිදොස්කරණය" #. TRANSLATORS: 'recovery key' here refers to a code, rather than a physical #. metal thing msgid "Please ensure you have the volume recovery key before continuing." msgstr "කරුණà·à¶šà¶» දිගටම කරගෙන යà·à¶¸à¶§ පෙර ඔබ සතුව à·à¶¶à·Šà¶¯ à¶´à·Šâ€à¶»à¶­à·’à·ƒà·à¶°à¶± යතුර ඇති බවට සහතික වන්න." #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "කරුණà·à¶šà¶» 0 සිට %uදක්ව෠අංකයක් ඇතුළු කරන්න: " #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "ප්ලගින à¶´à¶»à·à¶ºà¶­à·Šà¶­à¶­à· අතුරුදහන්" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "පෙර-ආරම්භක DMA ආරක්ෂà·à·€" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is disabled" msgstr "පෙර-ආරම්භක DMA ආරක්ෂà·à·€ à¶…à¶šà·Šâ€à¶»à·“ය à¶šà¶» ඇත" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is enabled" msgstr "පෙර-ආරම්භක DMA ආරක්ෂà·à·€ සබල à¶šà¶» ඇත" #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "පෙර අනුවà·à¶¯à¶º" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "ප්රමුඛත්වය" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "à¶œà·à¶§à¶½à·”" msgid "Proceed with upload?" msgstr "උඩුගත කිරීම සමඟ ඉදිරියට යන්නද?" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "හිමිකà·à¶»" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "දුරස්ථ-ID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID KEY VALUE" msgstr "දුරස්ථ-ID යතුරු අගය" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "à¶‹à¶´à·à¶‚ගයකින් ස්ථිරà·à¶‚à¶œ බ්ලොබ් කියවන්න" #. TRANSLATORS: command description msgid "Read a firmware from a device" msgstr "à¶‹à¶´à·à¶‚ගයකින් ස්ථිරà·à¶‚à¶œ කියවන්න" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "%s වෙතින් කියà·à·€à·™à¶¸à·’න්…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "කියවෙමින්…" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "දුරස්ථ සේවà·à¶¯à·à¶ºà¶šà¶ºà·™à¶±à·Š à¶´à·à¶»-දත්ත à¶±à·à·€à·”ම් කරන්න" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "%s සිට %sදක්ව෠නà·à·€à¶­ ස්ථà·à¶´à¶±à¶º කරන්නද?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "à¶‹à¶´à·à¶‚ගයේ වත්මන් ස්ථිරà·à¶‚à¶œ à¶±à·à·€à¶­ ස්ථà·à¶´à¶±à¶º කරන්න" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "à¶‹à¶´à·à¶‚ගයක ස්ථිරà·à¶‚à¶œ à¶±à·à·€à¶­ ස්ථà·à¶´à¶±à¶º කරන්න" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "නිදහස් à·à·à¶›à·à·€" #. TRANSLATORS: release attributes msgid "Release Flags" msgstr "කොඩි නිකුත් කරන්න" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "නිදහස් à·„à·à¶³à·”නුම්පත" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "දුරස්ථ à·„à·à¶³à·”නුම්පත" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "URI à·€à·à¶»à·Šà¶­à· කරන්න" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "දුරස්ථ සේවà·à¶¯à·à¶ºà¶šà¶º වෙත à·€à·à¶»à·Šà¶­à· කරන ලදී" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "à¶…à·€à·à·Šâ€à¶º efivarfs ගොනු පද්ධතිය සොයà·à¶œà¶­ නොහà·à¶šà·’ විය" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "à¶…à·€à·à·Šâ€à¶º දෘඩà·à¶‚à¶œ හමු නොවීය" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "ඇරඹුම් à¶šà·à¶»à¶šà¶ºà¶šà·Š à¶…à·€à·à·Šâ€à¶º වේ" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "අන්තර්ජà·à¶½ සම්බන්ධතà·à·€ à¶…à·€à·à·Šâ€à¶ºà¶ºà·’" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "දà·à¶±à·Š à¶±à·à·€à¶­ ආරම්භ කරන්නද?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "වෙනස් කිරීම ඵලදà·à¶ºà·“ කිරීමට ඩීමන් à¶±à·à·€à¶­ ආරම්භ කරන්නද?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "à¶‹à¶´à·à¶‚ගය යළි ඇරඹෙමින්…" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "යන්ත්â€à¶»à¶º සඳහ෠සියලුම දෘඪà·à¶‚à¶œ à·„à·à¶³à·”නුම්පත් ආපසු දෙන්න" #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "Install-blob à¶·à·à·€à·’ත෠කරන විට ප්ලගින සංයුක්ත පිරිසිදු කිරීමේ පුරුද්ද à¶šà·Šâ€à¶»à·’යà·à¶­à·Šà¶¸à¶š කරන්න" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "ස්ථà·à¶´à¶±à¶º-බ්ලොබ් à¶·à·à·€à·’ත෠කරන විට ප්ලගින සංයුක්ත සූදà·à¶±à¶¸à·Š කිරීමේ දින චර්යà·à·€ à¶°à·à·€à¶±à¶º කරන්න" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "à¶°à·à·€à¶± කර්නලය පරණ à·€à·à¶©à·’යි" #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "à¶°à·à·€à¶± à¶šà·à¶½ උපසර්ගය" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "SPI BIOS විස්තරකය" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "SPI BIOS à¶šà¶½à·à¶´à¶º" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "SPI අගුල" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "SPI replay protection" msgstr "SPI à¶±à·à·€à¶­ à¶°à·à·€à¶±à¶º ආරක්ෂà·à·€" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "SPI ලියන්න" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "SPI write protection" msgstr "SPI ලිවීමේ ආරක්ෂà·à·€" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "à¶‹à¶´ පද්ධති à¶°à·à·€à¶šà¶º [à¶‹à¶´à·à¶‚ගය-ID|මà·à¶»à·Šà¶œà·à¶´à¶¯à·šà·à¶º]" #. TRANSLATORS: command description msgid "Save a file that allows generation of hardware IDs" msgstr "දෘඪà·à¶‚à¶œ à·„à·à¶³à·”නුම්පත් ජනනය කිරීමට ඉඩ දෙන ගොනුවක් සුරකින්න" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "උපලේඛනගත කිරීම…" #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "ආරක්ෂිත ඇරඹුම් à¶…à¶šà·Šâ€à¶»à·“ය à¶šà¶» ඇත" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "ආරක්ෂිත ඇරඹුම් සක්රිය à¶šà¶» ඇත" #. TRANSLATORS: the %1 is a URL to a wiki page #, c-format msgid "See %s for more details." msgstr "à·€à·à¶©à·’ විස්තර සඳහ෠%s බලන්න." #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "à¶­à·€ තොරතුරු සඳහ෠%s බලන්න." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "à¶­à·à¶»à·à¶œà¶­à·Š à¶‹à¶´à·à¶‚ගය" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "à¶­à·à¶»à·à¶œà¶­à·Š පරිමà·à·€" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "අන්රක්රමික අංකය" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "අනුමත ස්ථිරà·à¶‚à¶œ à¶½à·à¶ºà·’ස්තුව සකසයි" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "ස්ථිරà·à¶‚à¶œ ඉතිහà·à·ƒà¶º සංවර්ධකයින් සමඟ බෙද෠ගන්න" #. TRANSLATORS: command line option msgid "Show all results" msgstr "සියළුම à¶´à·Šâ€à¶»à¶­à·’ඵල පෙන්වන්න" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "සේවà·à¶¯à·à¶ºà¶š සහ ඩීමන් අනුවà·à¶¯ පෙන්වන්න" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "විà·à·šà·‚à·’à¶­ වසමක් සඳහ෠ඩේමන් à·€à·à¶ à·’à¶š තොරතුරු පෙන්වන්න" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "සියලුම වසම් සඳහ෠නිදොස් කිරීමේ තොරතුරු පෙන්වන්න" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "නිදොස් කිරීමේ විකල්ප පෙන්වන්න" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± à¶šà·… නොහà·à¶šà·’ à¶‹à¶´à·à¶‚à¶œ පෙන්වන්න" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "අමතර දà·à· නිරà·à¶šà¶»à¶« තොරතුරු පෙන්වන්න" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "ස්ථිරà·à¶‚à¶œ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± ඉතිහà·à·ƒà¶º පෙන්වන්න" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "dbx à·„à·’ ගණනය à¶šà·… අනුවà·à¶¯à¶º පෙන්වන්න" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "දà·à¶±à·Š වස෠දමන්නද?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "නව යතුරක් සමඟ ස්ථිරà·à¶‚ගයක් අත්සන් කරන්න" msgid "Sign data using the client certificate" msgstr "සේවà·à¶¯à·à¶ºà¶š සහතිකය à¶·à·à·€à·’තයෙන් දත්ත අත්සන් කරන්න" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "සේවà·à¶¯à·à¶ºà¶š සහතිකය à¶·à·à·€à·’තයෙන් දත්ත අත්සන් කරන්න" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "උඩුගත à¶šà·… දත්ත සේවà·à¶¯à·à¶ºà¶š සහතිකය සමඟ අත්සන් කරන්න" msgid "Signature" msgstr "අත්සන" #. TRANSLATORS: firmware is verified on-device the payload using strong crypto msgid "Signed Payload" msgstr "අත්සන් à¶šà·… à¶´à·šà¶½à·à¶©à·Š" #. TRANSLATORS: file size of the download msgid "Size" msgstr "ප්රමà·à¶«à¶º" #. TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM msgid "Some of the platform secrets may be invalidated when updating this firmware." msgstr "මෙම ස්ථිරà·à¶‚à¶œ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීමේදී සමහර වේදික෠රහස් අවලංගු විය à·„à·à¶š." #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "මූලà·à·à·Šà¶»à¶º" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "dbx දත්ත සමුද෠ගොනුව සඳහන් කරන්න" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "à·ƒà·à¶»à·Šà¶®à¶šà¶­à·Šà·€à¶º" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "සියලුම à¶‹à¶´à·à¶‚à¶œ à·ƒà·à¶»à·Šà¶®à¶šà·€ සක්â€à¶»à·’ය à¶šà¶» ඇත" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "දුරස්ථ à¶´à·à¶½à¶šà¶º à·ƒà·à¶»à·Šà¶®à¶šà·€ à¶…à¶¶à¶½ කරන ලදී" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "නව à¶´à·à¶»à¶¯à¶­à·Šà¶­ à·ƒà·à¶»à·Šà¶®à¶šà·€ à¶¶à·à¶œà¶±à·Šà¶±à· ලදී: " #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "දුරස්ථ à¶´à·à¶½à¶šà¶º à·ƒà·à¶»à·Šà¶®à¶šà·€ සබල à¶šà¶» à¶±à·à·€à·”ම් කරන ලදී" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "දුරස්ථ à¶´à·à¶½à¶šà¶º à·ƒà·à¶»à·Šà¶®à¶šà·€ සබල à¶šà¶» ඇත" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "ස්ථිරà·à¶‚à¶œ à·ƒà·à¶»à·Šà¶®à¶šà·€ ස්ථà·à¶´à¶±à¶º à¶šà¶» ඇත" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "වින්â€à¶ºà·à·ƒ අගය à·ƒà·à¶»à·Šà¶®à¶šà·€ වෙනස් කරන ලදී" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "දුරස්ථ à¶´à·à¶½à¶šà¶º à·ƒà·à¶»à·Šà¶®à¶šà·€ වෙනස් කරන ලදී" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "à¶´à·à¶»à¶¯à¶­à·Šà¶­ අතින් à·ƒà·à¶»à·Šà¶®à¶šà·€ à¶±à·à·€à·”ම් කරන ලදී" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "à¶‹à¶´à·à¶‚à¶œ චෙක්සම් à·ƒà·à¶»à·Šà¶®à¶šà·€ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කරන ලදී" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "à·€à·à¶»à·Šà¶­à·à·€ %u à¶šà·Š à·ƒà·à¶»à·Šà¶®à¶šà·€ උඩුගත කරන ලදී" msgstr[1] "à·€à·à¶»à·Šà¶­à· %u à¶šà·Š à·ƒà·à¶»à·Šà¶®à¶šà·€ උඩුගත කරන ලදී" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "à¶‹à¶´à·à¶‚à¶œ චෙක්සම් à·ƒà·à¶»à·Šà¶®à¶šà·€ සත්â€à¶ºà·à¶´à¶±à¶º කරන ලදී" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "à·ƒà·à¶»à·à¶‚à·à¶º" #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "සහà·à¶º දක්වයි" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "සහය දක්වන CPU" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "දුරස්ථ සේවà·à¶¯à·à¶ºà¶šà¶ºà·š සහය දක්වයි" #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "අත්හිටුවීම-නිෂ්ක්රීය කිරීම" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "සස්පෙන්ඩ්-à¶§-à¶»à·à¶¸à·Š" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "à·à·à¶›à·à·€ %s සිට %sදක්ව෠මà·à¶»à·” කරන්නද?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "à¶‹à¶´à·à¶‚ගයේ ස්ථිරà·à¶‚à¶œ à·à·à¶›à·à·€ මà·à¶»à·” කරන්න" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "පද්ධතියට à¶¶à·à·„à·’à¶» à¶¶à¶½à·à¶šà·Šà¶­à·’ ප්රභවයක් à¶…à·€à·à·Šà¶º වේ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "පෙළ" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "TPM PCR0 à¶´à·Šâ€à¶»à¶­à·’සංස්කරණය" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is invalid" msgstr "TPM PCR0 à¶´à·Šâ€à¶»à¶­à·’නිර්මà·à¶«à¶º වලංගු à¶±à·à¶­" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is now valid" msgstr "TPM PCR0 à¶´à·Šâ€à¶»à¶­à·’නිර්මà·à¶«à¶º දà·à¶±à·Š වලංගුයි" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM empty PCRs" msgstr "TPM හිස් PCRs" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM අනු:2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "à¶§à·à¶œà·Š කරන්න" msgstr[1] "à¶§à·à¶œà·Š" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "කිලිටි වෙලà·" #. show the user the entire data blob msgid "Target" msgstr "ඉලක්කය" #. TRANSLATORS: command description msgid "Test a device using a JSON manifest" msgstr "JSON මà·à¶±à·’ෆෙස්ටයක් à¶·à·à·€à·’තයෙන් à¶‹à¶´à·à¶‚ගයක් පරීක්ෂ෠කරන්න" #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "LVFS යනු ස්වà·à¶°à·“à¶± නෛතික ආයතනයක් ලෙස à¶šà·Šâ€à¶»à·’යà·à¶­à·Šà¶¸à¶š වන නිදහස් සේවà·à·€à¶šà·Š වන à¶…à¶­à¶» $OS_RELEASE:NAME$ සමඟ කිසිදු සම්බන්ධයක් නොමà·à¶­. ඔබේ බෙදà·à·„රින්න෠ඔබේ පද්ධතිය හ෠සම්බන්ධිත à¶‹à¶´à·à¶‚à¶œ සමඟ à¶œà·à·…පීම සඳහ෠ස්ථිරà·à¶‚à¶œ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිසිවක් සත්â€à¶ºà·à¶´à¶±à¶º à¶šà¶» නොතිබිය à·„à·à¶šà·’ය. සියලුම ස්ථිරà·à¶‚à¶œ සපයනු ලබන්නේ මුල් උපකරණ නිෂ්පà·à¶¯à¶šà¶ºà· විසින් පමණි." #. TRANSLATORS: this is more background on a security measurement problem msgid "The TPM PCR0 differs from reconstruction." msgstr "TPM PCR0 à¶´à·Šâ€à¶»à¶­à·’සංස්කරණයෙන් වෙනස් වේ." #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "ඩීමන් 3වන à¶´à·à¶»à·Šà·à·Šà·€ කේතය පූරණය à¶šà¶» ඇති à¶…à¶­à¶» උඩුගං බල෠සංවර්ධකයින් විසින් තවදුරටත් සහà·à¶º නොදක්වයි!" #. TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name #, c-format msgid "The firmware from %s is not supplied by %s, the hardware vendor." msgstr "%s සිට ස්ථිරà·à¶‚à¶œ සපයනු ලබන්නේ දෘඪà·à¶‚à¶œ වෙළෙන්ද෠වන %sවිසිනි." #. TRANSLATORS: try to help msgid "The system clock has not been set correctly and downloading files may fail." msgstr "පද්ධති ඔරලà·à·ƒà·”à·€ නිවà·à¶»à¶¯à·’à·€ සකස෠නොමà·à¶­à·’ à¶…à¶­à¶» ගොනු à¶¶à·à¶œà¶­ කිරීම à¶…à·ƒà·à¶»à·Šà¶®à¶š විය à·„à·à¶š." #. TRANSLATORS: naughty vendor msgid "The vendor did not supply any release notes." msgstr "වෙළෙන්ද෠නිකුත් කිරීමේ සටහන් කිසිවක් සපය෠නà·à¶­." #. TRANSLATORS: now list devices with unfixed high-priority issues msgid "There are devices with issues:" msgstr "à¶œà·à¶§à·…à·” සහිත à¶‹à¶´à·à¶‚à¶œ à¶­à·’à¶¶à·š:" #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "අවහිර à¶šà·… ස්ථිරà·à¶‚à¶œ ගොනු නොමà·à¶­" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "අනුමත ස්ථිරà·à¶‚à¶œ නොමà·à¶­." #. TRANSLATORS: %1 is the current device version number, and %2 is the #. command name, e.g. `fwupdmgr sync` #, c-format msgid "This device will be reverted back to %s when the %s command is performed." msgstr "%s විධà·à¶±à¶º à¶šà·Šâ€à¶»à·’යà·à¶­à·Šà¶¸à¶š කරන විට මෙම à¶‹à¶´à·à¶‚ගය à¶±à·à·€à¶­ %s වෙත à¶´à·Šâ€à¶»à¶­à·’වර්තනය වේ." #. TRANSLATORS: the vendor did not upload this msgid "This firmware is provided by LVFS community members and is not provided (or supported) by the original hardware vendor." msgstr "මෙම ස්ථිරà·à¶‚à¶œ සපයනු ලබන්නේ LVFS à¶´à·Šâ€à¶»à¶¢à· à·ƒà·à¶¸à·à¶¢à·’කයින් විසින් වන à¶…à¶­à¶» මුල් දෘඪà·à¶‚à¶œ වෙළෙන්ද෠විසින් සපයනු නොලà·à¶¶à·š (හ෠සහà·à¶º නොදක්වයි)." #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "මෙම à¶´à·à¶šà·šà¶¢à¶º වලංගු à¶šà¶» à¶±à·à¶­, එය නිවà·à¶»à¶¯à·’à·€ à¶šà·Šâ€à¶»à·’ය෠නොකරනු ඇත." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "මෙම à·€à·à¶©à·ƒà¶§à·„à¶± නිවà·à¶»à¶¯à·’à·€ à¶šà·Šâ€à¶»à·’ය෠කළ à·„à·à¶šà·Šà¶šà·š root ලෙස පමණි" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "මෙම දුරස්ථ à¶´à·à¶½à¶šà¶ºà·š සම්බà·à¶°à¶š නොමà·à¶­à·’ ස්ථිරà·à¶‚à¶œ අඩංගු නමුත් දෘඪà·à¶‚à¶œ වෙළෙන්ද෠විසින් තවමත් පරීක්ෂ෠කරනු à¶½à·à¶¶à·š. ස්ථිරà·à¶‚à¶œ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම à¶…à·ƒà·à¶»à·Šà¶®à¶š වුවහොත් ස්ථිරà·à¶‚à¶œ හස්තීයව à¶´à·„à¶­ හෙළීමට ඔබට à¶šà·Šâ€à¶»à¶¸à¶ºà¶šà·Š ඇති à¶¶à·€ ඔබ සහතික විය යුතුය." #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "මෙම පද්ධතියට HSI à¶°à·à·€à¶± à¶šà·à¶½ à¶œà·à¶§à·…à·” ඇත." #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "මෙම පද්ධතියට à¶…à¶©à·” HSI ආරක්ෂක මට්ටමක් ඇත." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "මෙම මෙවලම පරිපà·à¶½à¶šà¶ºà·™à¶šà·”à¶§ UEFI dbx යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± යෙදීමට ඉඩ සලසයි." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "මෙම මෙවලම පරිපà·à¶½à¶šà¶ºà·™à¶šà·”à¶§ fwupd daemon විමස෠පà·à¶½à¶±à¶º කිරීමට ඉඩ සලසයි, ස්ථිරà·à¶‚à¶œ ස්ථà·à¶´à¶±à¶º කිරීම à·„à· à¶´à·„à¶­ හෙලීම à·€à·à¶±à·’ à¶šà·Šâ€à¶»à·’ය෠සිදු කිරීමට ඔවුන්ට ඉඩ සලසයි." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "මෙම මෙවලම පරිපà·à¶½à¶šà¶ºà·™à¶šà·”à¶§ à¶°à·à¶»à¶š පද්ධතියේ ස්ථà·à¶´à¶±à¶º නොකර fwupd ප්ලගීන à¶·à·à·€à·’ත෠කිරීමට ඉඩ සලසයි." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "මෙම මෙවලම පද්ධති ස්ථිරà·à¶‚ගයෙන් TPM සිදුවීම් ලොගය කියව෠විග්â€à¶»à·„ කරයි." #. TRANSLATORS: the update state of the specific device msgid "Transient failure" msgstr "à¶­à·à·€à¶šà·à¶½à·’à¶š à¶…à·ƒà·à¶»à·Šà¶®à¶šà¶­à·Šà·€à¶º" #. TRANSLATORS: We verified the metadata against the server msgid "Trusted metadata" msgstr "විà·à·Šà·€à·à·ƒà¶¯à·à¶ºà¶š à¶´à·à¶»à¶¯à¶­à·Šà¶­" #. TRANSLATORS: We verified the payload against the server msgid "Trusted payload" msgstr "විà·à·Šà·€à·à·ƒà¶±à·“ය ගෙවීම" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "ටයිප් කරන්න" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "UEFI ESP කොටස à¶…à¶±à·à·€à¶»à¶«à¶º à¶šà¶» හ෠වින්â€à¶ºà·à·ƒ à¶šà¶» නොමà·à¶­" #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "UEFI à¶šà·à¶´à·Šà·ƒà·’යුල යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± ලබ෠ගත නොහà·à¶š හ෠ස්ථිරà·à¶‚à¶œ à·ƒà·à¶šà·ƒà·”ම තුළ සබල à¶šà¶» ඇත" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "UEFI dbx උපයà·à¶œà·’à¶­à·" #. TRANSLATORS: system is not booted in UEFI mode msgid "UEFI firmware can not be updated in legacy BIOS mode" msgstr "UEFI ස්ථිරà·à¶‚à¶œ à¶´à·à¶»à¶«à·’ BIOS ආකà·à¶»à¶ºà·™à¶±à·Š යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± à¶šà·… නොහà·à¶š" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "UEFI වේදික෠යතුර" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "UEFI ආරක්ෂිත ඇරඹුම" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "සේවà·à·€à¶§ සම්බන්ධ විය නොහà·à¶š" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "වත්මන් à¶°à·à·€à¶šà¶º ඉවත් කරන්න" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "ස්ථිරà·à¶‚à¶œ අවහිර කිරීම ඉවත් කිරීම:" #. TRANSLATORS: command description msgid "Unblocks a specific firmware from being installed" msgstr "නිà·à·Šà¶ à·’à¶­ ස්ථිරà·à¶‚ගයක් ස්ථà·à¶´à¶±à¶º කිරීමෙන් අවහිර කිරීම ඉවත් කරයි" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "විසංකේතිතයි" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "නොදනී" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "හඳුනà·à¶±à·œà¶œà¶­à·Š උපකරණය" msgid "Unlock the device to allow access" msgstr "à¶´à·Šâ€à¶»à·€à·šà·à¶ºà¶§ ඉඩ දීමට à¶‹à¶´à·à¶‚ගය අගුළු හරින්න" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "අගුළු à·„à·à¶» ඇත" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "ස්ථිරà·à¶‚à¶œ à¶´à·Šâ€à¶»à·€à·šà·à¶º සඳහ෠උපà·à¶‚ගය අගුළු හරියි" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "ESP ගලවයි" #. TRANSLATORS: firmware payload is unsigned and it is possible to modify it msgid "Unsigned Payload" msgstr "අත්සන් නොකළ ගෙවීම" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "සහà·à¶º නොදක්වන ඩීමන් අනුවà·à¶¯à¶º %s, සේවà·à¶¯à·à¶ºà¶š අනුවà·à¶¯à¶º %sවේ" #. TRANSLATORS: Suffix: the HSI result msgid "Untainted" msgstr "අපිරිසිදු" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± à¶šà·… à·„à·à¶šà·’" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± දà·à·‚යකි" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "රූපය යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කරන්න" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "පණිවිඩය යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කරන්න" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "තත්වය යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කරන්න" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± à¶…à·ƒà·à¶»à·Šà¶®à¶š වීම දන්න෠ගà·à¶§à·…ුවකි, à·€à·à¶©à·’දුර තොරතුරු සඳහ෠මෙම URL වෙත පිවිසෙන්න:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "දà·à¶±à·Š යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කරන්න?" #. TRANSLATORS: command description msgid "Update the stored cryptographic hash with current ROM contents" msgstr "ගබඩ෠කර ඇති ගුප්ත ලේඛන à·„à·à·‚à·Š වත්මන් ROM අන්තර්ගතය සමඟ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කරන්න" msgid "Update the stored device verification information" msgstr "ගබඩ෠කර ඇති à¶‹à¶´à·à¶‚à¶œ සත්â€à¶ºà·à¶´à¶± තොරතුරු යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කරන්න" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "ගබඩ෠කර ඇති à¶´à·à¶»à¶¯à¶­à·Šà¶­ වත්මන් අන්තර්ගතය සමඟ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කරන්න" #. TRANSLATORS: command description msgid "Updates all specified devices to latest firmware version, or all devices if unspecified" msgstr "සියලුම නිà·à·Šà¶ à·’à¶­ à¶‹à¶´à·à¶‚à¶œ නවතම ස්ථිරà·à¶‚à¶œ අනුවà·à¶¯à¶º වෙත යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කරයි, හ෠නිà·à·Šà¶ à·’à¶­à·€ දක්ව෠නොමà·à¶­à·’ නම් සියලුම à¶‹à¶´à·à¶‚à¶œ" msgid "Updating" msgstr "යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිරීම" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "%s යà·à·€à¶­à·Šà¶šà·à¶½ වෙමින්…" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "%s %s සිට %sදක්ව෠උත්à·à·Šâ€à¶»à·šà¶«à·’ කරන්නද?" #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "ස්ථිරà·à¶‚à¶œ à·€à·à¶»à·Šà¶­à· උඩුගත කිරීම à·ƒà·à¶¶à·‘ à¶‹à¶´à·à¶‚ගවල à¶…à·ƒà·à¶»à·Šà¶®à¶š සහ à·ƒà·à¶»à·Šà¶®à¶š යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± ඉක්මනින් හඳුන෠ගà·à¶±à·“මට දෘඪà·à¶‚à¶œ වෙළෙන්දන්ට à¶‹à¶´à¶šà·à¶» කරයි." #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "හදිසිය" #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "පරිà·à·“ලකයà·à¶§ දà·à¶±à·”ම් දී ඇත" #. TRANSLATORS: remote filename base msgid "Username" msgstr "පරිà·à·“ලක à¶±à·à¶¸à¶º" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "වලංගුයි" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "ESP අන්තර්ගතයන් වලංගු කිරීම…" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "ප්රභේදය" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "වෙළෙන්දà·" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "සත්â€à¶ºà·à¶´à¶±à¶ºâ€¦" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "අනුවà·à¶¯à¶º" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "à¶»à·à¶³à·™à¶¸à·’න්…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "දෘඪà·à¶‚à¶œ වෙනස්කම් සඳහ෠නරඹන්න" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "ලිපිගොනු ලිවීම:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "ලියවෙමින්…" #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "ඔබේ බෙදà·à·„රින්න෠ඔබේ පද්ධතිය හ෠සම්බන්ධිත à¶‹à¶´à·à¶‚à¶œ සමඟ à¶œà·à·…පීම සඳහ෠ස්ථිරà·à¶‚à¶œ යà·à·€à¶­à·Šà¶šà·à¶½à·“à¶± කිසිවක් සත්â€à¶ºà·à¶´à¶±à¶º à¶šà¶» නොතිබිය à·„à·à¶šà·’ය." #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "මෙම ස්ථිරà·à¶‚à¶œ à¶·à·à·€à·’තයෙන් ඔබේ දෘඪà·à¶‚ගයට à·„à·à¶±à·’ සිදු à·€à·’ය à·„à·à¶šà·’ à¶…à¶­à¶», මෙම නිකුතුව ස්ථà·à¶´à¶±à¶º කිරීමෙන් %sසමඟ ඇති ඕනෑම වගකීමක් අවලංගු විය à·„à·à¶š." #. TRANSLATORS: BKC is the industry name for the best known configuration and #. is a set #. * of firmware that works together #, c-format msgid "Your system is set up to the BKC of %s." msgstr "ඔබේ පද්ධතිය %sà·„à·’ BKC ලෙස සකස෠ඇත." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[CHECKSUM]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[DEVICE-ID|මà·à¶»à·Šà¶œà·à¶´à¶¯à·šà·à¶º]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[DEVICE-ID|GUID] [BRANCH]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [VERSION]" msgstr "[DEVICE-ID|GUID] [VERSION]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILE FILE_SIG REMOTE-ID]" msgstr "[FILE FILE_SIG දුරස්ථ-ID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[FILENAME1] [FILENAME2]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[SMBIOS-FILE|HWIDS-FILE]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "පෙරනිමිය" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "fwupd TPM සිදුවීම් ලොග් උපයà·à¶œà·“à¶­à·à·€" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "fwupd ප්ලගීන" fwupd-2.0.10/po/sk.po000066400000000000000000000137231501337203100143130ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # DuÅ¡an Kazik , 2015-2017 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Slovak (http://app.transifex.com/freedesktop/fwupd/language/sk/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: sk\n" "Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n >= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);\n" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Prezývka príkazu %s" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Umožní zníženie verzií firmvéru" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Vyžaduje sa overenie totožnosti na prechod na starÅ¡iu verziu firmvéru vymeniteľného zariadenia" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Vyžaduje sa overenie totožnosti na prechod na starÅ¡iu verziu firmvéru v tomto poÄítaÄi" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Na odomknutie zariadenia sa vyžaduje overenie totožnosti" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Vyžaduje sa overenie totožnosti na aktualizovanie firmvéru vymeniteľného zariadenia " #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Na aktualizovanie firmvéru v tomto poÄítaÄi je potrebné overenie totožnosti" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "ZruÅ¡ené" #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Zmenené" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Kontrolný medzisúÄet" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "Vymaže výsledky z poslednej aktualizácie" #. TRANSLATORS: error message msgid "Command not found" msgstr "Príkaz nenájdený" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Voľby ladenia" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Popis" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Pridané zariadenie:" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Zmenené zariadenie:" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Odstránené zariadenie:" #. TRANSLATORS: success msgid "Done!" msgstr "Hotovo!" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "SkonÄí po krátkom oneskorení" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "SkonÄí po naÄítaní jadra" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Zlyhalo analyzovanie parametrov" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "Služba zbernice D-Bus na aktualizovanie firmvéru" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Démon aktualizácie firmvéru" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "Nástroj pre firmvéry" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Nájdené" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Získa vÅ¡etky zariadenia, ktoré podporujú aktualizovanie firmvéru" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Získa podrobnosti o súbore firmvéru" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Získa zoznam aktualizácií pre pripojený hardvér" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Získa výsledky z poslednej aktualizácie" msgid "Install signed device firmware" msgstr "NainÅ¡taluje podpísaný firmvér zariadenia" msgid "Install signed system firmware" msgstr "NainÅ¡taluje podpísaný firmvér systému" msgid "Install unsigned device firmware" msgstr "NainÅ¡taluje nepodpísaný firmvér zariadenia" msgid "Install unsigned system firmware" msgstr "NainÅ¡taluje nepodpísaný firmvér systému" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "Sleduje démona kvôli udalostiam" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Nezistil sa žiadny hardvér s možnosÅ¥ou aktualizácie firmvéru" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "OK" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "Obnoví metaúdaje zo vzdialeného servera" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Zobrazí voľby ladenia" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "ZobrazovaÅ¥ dodatoÄné ladiace informácie" msgid "Unlock the device to allow access" msgstr "Odomknúť zariadenie na umožnenie prístupu" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "Odomkne zariadenie pre prístup k firmvéru" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Verzia" fwupd-2.0.10/po/sl.po000066400000000000000000003771111501337203100143200ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Martin Srebotnjak , 2021,2024-2025 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Slovenian (http://app.transifex.com/freedesktop/fwupd/language/sl/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: sl\n" "Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "Preostalih je %.0f minut" msgstr[1] "Preostala je %.0f minuta" msgstr[2] "Preostali sta %.0f minuti" msgstr[3] "Preostale so %.0f minute" #. TRANSLATORS: BMC refers to baseboard management controller which #. * is the device that updates all the other firmware on the system #, c-format msgid "%s BMC Update" msgstr "Posodobitev BMC %s" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "Posodobitev baterije %s" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "Posodobitev mikrokode CPE %s" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "Posodobitev kamere %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "Posodobitev nastavitev %s" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "Posodobitev uporabniÅ¡kega upravljalnika %s" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "Posodobitev krmilnika %s" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "Posodobitev poslovnega upravljalnika %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "Posodobitev naprave %s" #. TRANSLATORS: Video Display refers to the laptop internal display or #. * external monitor #, c-format msgid "%s Display Update" msgstr "Posodobitev zaslona %s" #. TRANSLATORS: Dock refers to the port replicator hardware laptops are #. * cradled in, or lowered onto #, c-format msgid "%s Dock Update" msgstr "Posodobitev priklopne postaje %s" #. TRANSLATORS: drive refers to a storage device, e.g. SATA disk #, c-format msgid "%s Drive Update" msgstr "Posodobitev naprave %s" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "Posodobitev vgrajenega krmilnika %s" #. TRANSLATORS: a device that can read your fingerprint pattern #, c-format msgid "%s Fingerprint Reader Update" msgstr "Posodobitev bralnika prstnih odtisov %s" #. TRANSLATORS: flash refers to solid state storage, e.g. UFS or eMMC #, c-format msgid "%s Flash Drive Update" msgstr "Posodobitev bliskovnega pogona %s" #. TRANSLATORS: GPU refers to a Graphics Processing Unit, e.g. #. * the "video card" #, c-format msgid "%s GPU Update" msgstr "Posodobitev GPE %s" #. TRANSLATORS: a large pressure-sensitive drawing area typically used #. * by artists and digital artists #, c-format msgid "%s Graphics Tablet Update" msgstr "Posodobitev grafiÄne tablice %s" #. TRANSLATORS: two miniature speakers attached to your ears #, c-format msgid "%s Headphones Update" msgstr "Posodobitev sluÅ¡alk %s" #. TRANSLATORS: headphones with an integrated microphone #, c-format msgid "%s Headset Update" msgstr "Posodobitev sluÅ¡alk z mikrofonom %s" #. TRANSLATORS: an input device used by gamers, e.g. a joystick #, c-format msgid "%s Input Controller Update" msgstr "Posodobitev vhodnega krmilnika %s" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "Posodobitev tipkovnice %s" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "Posodobitev ME %s" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "Posodobitev miÅ¡ke %s" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "Posodobitev omrežnega vmesnika %s" #. TRANSLATORS: SSD refers to a Solid State Drive, e.g. non-rotating #. * SATA or NVMe disk #, c-format msgid "%s SSD Update" msgstr "Posodobitev SSD %s" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "Posodobitev krmilnika shrambe %s" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "Posodobitev sistema %s" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "Posodobitev modula okolja TPM %s" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "Posodobitev krmilnika Thunderbolt %s" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "Posodobitev sledilne ploÅ¡Äice %s" #. TRANSLATORS: Dock refers to the port replicator device connected #. * by plugging in a USB cable -- which may or may not also provide power #, c-format msgid "%s USB Dock Update" msgstr "Posodobitev priklopne postaje USB %s" #. TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth #. * device that stays in the USB port so the wireless peripheral works #, c-format msgid "%s USB Receiver Update" msgstr "Posodobitev sprejemnika USB %s" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "Posodobitev %s" #. TRANSLATORS: warn the user before updating, %1 is a device name #, c-format msgid "%s and all connected devices may not be usable while updating." msgstr "%s in vse povezane naprave med posodabljanjem morda ne bodo uporabne." #. TRANSLATORS: %1 refers to some kind of security test, e.g. "Encrypted RAM". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s appeared: %s" msgstr "%s se je pojavil: %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "UEFI platform #. key". #. * %2 and %3 refer to results value, e.g. "Valid" and "Invalid" #, c-format msgid "%s changed: %s → %s" msgstr "%s spremenjeno: %s → %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "SPI BIOS #. region". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s disappeared: %s" msgstr "%s je izginil: %s" #. TRANSLATORS: the device has a reason it can't update, e.g. laptop lid #. closed #, c-format msgid "%s is not currently updatable" msgstr "%s trenutno ni mogoÄe posodobiti" #. TRANSLATORS: %1 is a device name, e.g. "ThinkPad Universal #. * ThunderBolt 4 Dock" and %2 is "fwupdmgr activate" #, c-format msgid "%s is pending activation; use %s to complete the update." msgstr "%s Äaka na aktivacijo; uporabite %s za dokonÄanje posodobitve." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "NaÄin izdelave %s" #. TRANSLATORS: warn the user before #. * updating, %1 is a device name #, c-format msgid "%s must remain connected for the duration of the update to avoid damage." msgstr "%s mora ostati povezana ves Äas trajanja posodobitve, da se prepreÄi Å¡koda." #. TRANSLATORS: warn the user before updating, %1 is a machine #. * name #, c-format msgid "%s must remain plugged into a power source for the duration of the update to avoid damage." msgstr "%s mora ostati prikljuÄen na vir napajanja ves Äas trajanja posodobitve, da se prepreÄi Å¡koda." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "Preglasitev %s" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "RazliÄica %s" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u dni" msgstr[1] "%u dan" msgstr[2] "%u dneva" msgstr[3] "%u dnevi" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "%u naprav ima na voljo posodobitev vdelane programske opreme." msgstr[1] "%u naprava ima na voljo posodobitev vdelane programske opreme." msgstr[2] "%u napravi imata na voljo posodobitev vdelane programske opreme." msgstr[3] "%u naprave imajo na voljo posodobitev vdelane programske opreme." #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device is not the best known configuration." msgid_plural "%u devices are not the best known configuration." msgstr[0] " %u naprave niso najboljÅ¡a znana konfiguracija." msgstr[1] " %u naprava ni najboljÅ¡a znana konfiguracija." msgstr[2] "%u napravi nista najboljÅ¡a znana konfiguracija." msgstr[3] "%u naprave niso najboljÅ¡a znana konfiguracija." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u ur" msgstr[1] "%u ura" msgstr[2] "%u uri" msgstr[3] "%u ure" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minut" msgstr[1] "%u minuta" msgstr[2] "%u minuti" msgstr[3] "%u minute" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u s" msgstr[1] "%u s" msgstr[2] "%u s" msgstr[3] "%u s" #. TRANSLATORS: device tests can be specific to a CPU type #, c-format msgid "%u test was skipped" msgid_plural "%u tests were skipped" msgstr[0] "%u preizkusov je preskoÄenih" msgstr[1] "%u preizkus je preskoÄen" msgstr[2] "%u preizkusa sta preskoÄena" msgstr[3] "%u preizkusi so preskoÄeni" #. TRANSLATORS: first percentage is current value, 2nd percentage is the #. * lowest limit the firmware update is allowed for the update to happen #, c-format msgid "%u%% (threshold %u%%)" msgstr "%u %% (prag %u%%)" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(zastarelo)" #. TRANSLATORS: HSI event title msgid "A TPM PCR is now an invalid value" msgstr "PCR TPM je zdaj neveljavna vrednost" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "AMD Firmware Replay Protection" msgstr "ZaÅ¡Äita pred ponovnim predvajanjem vdelane programske opreme AMD" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "AMD Firmware Write Protection" msgstr "ZaÅ¡Äita pred pisanjem vdelane programske opreme AMD" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "AMD Secure Processor Rollback Protection" msgstr "Varnostna zaÅ¡Äita pred povrnitvijo procesorja AMD (AMD Secure Processor Rollback Protection)" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "ARCHIVE FIRMWARE METAINFO [FIRMWARE] [METAINFO] [JCATFILE]" msgstr "ARHIV VDELANA-PROG-OPREMA METAPODATKI [VDELANA-PROG-OPREMA] [METAPODATKI] [DATOTEKAJCAT]" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "Potrebni je ukrepanje:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "Aktiviraj naprave" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "Aktiviraj ÄakajoÄe naprave" msgid "Activate the new firmware on the device" msgstr "Aktiviraj novo strojno programsko opremo v napravi" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "Aktiviranje posodobitve vdelane programske opreme" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "Aktiviranje posodobitve vdelane programske opreme za" #. TRANSLATORS: command description msgid "Adds devices to watch for future emulation" msgstr "Doda naprave za spremljanje prihodnje emulacije" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Starost" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "Se strinjate in želite omogoÄiti oddaljeni strežnik?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Vzdevek za %s" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are now valid" msgstr "Vsi PCR-ji TPM so zdaj veljavni" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are valid" msgstr "Vsi PCR-ji TPM so veljavni" #. TRANSLATORS: an application is preventing system updates msgid "All devices are prevented from update by system inhibit" msgstr "Sistem prepreÄuje posodobitev vsem napravam" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "Vse naprave iste vrste bodo posodobljene hkrati" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Dovoli zamenjavo razliÄic vdelane programske opreme s starejÅ¡o razliÄico" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "Dovoli vnoviÄno namestitev obstojeÄih razliÄic vdelane programske opreme" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "Dovoli preklapljanje veje vdelane programske opreme" #. TRANSLATORS: error message msgid "Already exists, and no --force specified" msgstr "Že obstaja, parameter --force ni naveden" #. TRANSLATORS: is not the main firmware stream msgid "Alternate branch" msgstr "Nadomestna veja" #. TRANSLATORS: another application is updating the device already msgid "An update is in progress" msgstr "Posodobitev je v teku" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "Posodobitev zahteva vnoviÄni zagon, da bo dokonÄana." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "Posodobitev zahteva zaustavitev sistema, da bo dokonÄana." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Na vsa vpraÅ¡anja odgovori pritrdilno" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Uporabi posodobitev, tudi je bila odsvetovana" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Uveljavi posodobitvene datoteke" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "Uveljavljanje posodobitve …" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Odobrena strojna programska oprema:" msgstr[1] "Odobrena strojna programska oprema:" msgstr[2] "Odobrena strojna programska oprema:" msgstr[3] "Odobrena strojna programska oprema:" #. TRANSLATORS: command description msgid "Asks the daemon to quit" msgstr "NaroÄi prikritemu procesu, naj neha delovati" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "Pripni na naÄin vdelane programske opreme" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Preverjanje pristnosti..." #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "Zahtevane so podrobnosti preverjanja pristnosti" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Preverjanje pristnosti je potrebno za vrnitev na starejÅ¡o razliÄico vdelane programske opreme na izmenljivi napravi" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Preverjanje pristnosti je potrebno za vrnitev na starejÅ¡o razliÄico vdelane programske opreme v tem raÄunalniku" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to enable emulation data collection" msgstr "Za omogoÄanje zbiranja podatkov emulacije strojne opreme je potrebno preverjanje pristnosti" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to fix a host security issue" msgstr "Preverjanje pristnosti je potrebno za odpravljanje težave z varnostjo gostitelja" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to load hardware emulation data" msgstr "Za nalaganje podatkov emulacije strojne opreme je potrebno preverjanje pristnosti" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "Za spreminjanje nastavitev BIOS-a je potrebno preverjanje pristnosti" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "Preverjanje pristnosti je potrebno za spreminjanje nastavitev oddaljenega strežnika, ki se uporablja za posodobitve vdelane programske opreme" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "Za spreminjanje prilagoditve prikritega procesa je potrebno preverjanje pristnosti" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "Za branje nastavitev BIOS-a je potrebno preverjanje pristnosti" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to reset daemon configuration to defaults" msgstr "Za spreminjanje prilagoditve prikritega procesa na privzeto je potrebno preverjanje pristnosti" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to save hardware emulation data" msgstr "Za shranjevanje podatkov emulacije strojne opreme je potrebno preverjanje pristnosti" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "Preverjanje pristnosti je potrebno za nastavitev seznama odobrene vdelane programske opreme" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "Preverjanje pristnosti je potrebno za podpisovanje podatkov s potrdilom odjemalca" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to stop the firmware update service" msgstr "Preverjanje pristnosti je potrebno za ustavitev storitve posodabljanja strojno programske opreme" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "Preverjanje pristnosti je potrebno za preklop na novo razliÄico strojne programske opreme" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to undo the fix for a host security issue" msgstr "Preverjanje pristnosti je potrebno, Äe želite razveljaviti popravek za težavo z varnostjo gostitelja" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Za odklepanje naprave je potrebno preverjanje pristnosti" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Preverjanje pristnosti je potrebno za posodobitev strojno programske opreme v izmenljivi napravi" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Preverjanje pristnosti je potrebno za posodobitev strojno programske opreme v tem raÄunalniku" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "Preverjanje pristnosti je potrebno za posodobitev shranjenih kontrolnih vsot za napravo" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "Samodejno poroÄanje" #. TRANSLATORS: we can auto-uninhibit after a timeout #, c-format msgid "Automatically uninhibiting in %ums…" msgstr "Samodejno sproÅ¡Äanje sistema Äez %u ms ..." #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "Želite vsakiÄ samodejno naložiti?" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS Firmware Updates" msgstr "Posodobitve vdelane programske opreme BIOS-a" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS Rollback Protection" msgstr "ZaÅ¡Äita pred vraÄanjem BIOS-a" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS firmware updates" msgstr "Posodobitve vdelane programske opreme BIOS-a" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS rollback protection" msgstr "ZaÅ¡Äita pred povrnitvijo BIOS-a" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "Posodobitve BIOS-a, dostavljene prek LVFS ali Windows Update" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "BUILDER-XML FILENAME-DST" msgstr "BUILDER-XML IMEDATOTEKE-DST" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "Baterija" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "Poveži novi gonilnik jedra" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "Blokirane datoteke vdelane programske opreme:" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "Blokirana razliÄica" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "Blokiranje vdelane programske opreme:" #. TRANSLATORS: command description msgid "Blocks a specific firmware from being installed" msgstr "Blokira namestitev doloÄene vdelane programske opreme" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "RazliÄica zagonskega nalagalnika" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "Veja" #. TRANSLATORS: command description msgid "Build a cabinet archive from a firmware blob and XML metadata" msgstr "Ustvari arhiv vsebnika iz zbirke dvojiÅ¡kih podatkov in metapodatkov XML vdelane programske opreme" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "Ustvari datoteko vdelane programske opreme" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * Utilized by OS means the distribution enabled it msgid "CET OS Support" msgstr "Podpora za operacijski sistem CET" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * enabled means supported by the processor msgid "CET Platform" msgstr "Platforma CET" #. TRANSLATORS: longer description msgid "CPU Microcode must be updated to mitigate against various information-disclosure security issues." msgstr "Mikrokodo CPE je treba posodobiti, da se ublažijo razliÄna varnostna vpraÅ¡anja razkritja informacij." #. TRANSLATORS: we can save all device enumeration events for emulation msgid "Can tag for emulation" msgstr "Lahko oznaÄite za emulacijo" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "PrekliÄi" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "Preklicano" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "Ni možno uveljaviti, ker je bila posodobitev dbx že uveljavljena." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Spremenjeno" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "Preverja, da se kriptografska kontrolna vsota ujema z vdelano programsko opremo" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Kontrolna vsota" #. TRANSLATORS: get interactive prompt, where branch is the #. * supplier of the firmware, e.g. "non-free" or "free" msgid "Choose branch" msgstr "Izberite vejo" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "Izberite napravo" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "Izberite strojno programsko opremo" #. TRANSLATORS: get interactive prompt msgid "Choose release" msgstr "Izberite izdajo" #. TRANSLATORS: get interactive prompt msgid "Choose volume" msgstr "Izberite nosilec" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "PoÄisti rezultate zadnje posodobitve." #. TRANSLATORS: error message msgid "Command not found" msgstr "Ukaz ni mogoÄe najti" #. TRANSLATORS: is not supported by the vendor msgid "Community supported" msgstr "Podprto s strani skupnosti" #. TRANSLATORS: command description msgid "Compares two versions for equality" msgstr "Primerja dve razliÄici, Äe sta enaki" #. TRANSLATORS: title prefix for the BIOS settings dialog msgid "Configuration Change Suggested" msgstr "Predlagana sprememba konfiguracije" #. TRANSLATORS: no peeking msgid "Configuration is only readable by the system administrator" msgstr "Konfiguracijo lahko prebere le skrbnik sistema" #. TRANSLATORS: longer description msgid "Control-Flow Enforcement Technology detects and prevents certain methods for running malicious software on the device." msgstr "Tehnologija Control-Flow Enforcement Technology zazna in prepreÄi doloÄene naÄine za izvajanje zlonamerne programske opreme v napravi." #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology msgid "Control-flow Enforcement Technology" msgstr "Tehnologija nadzora in nadzora pretoka" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "Pretvori datoteko vdelane programske opreme" #. TRANSLATORS: command description msgid "Create an EFI boot entry" msgstr "Ustvari zagonski vnos za EFI" #. TRANSLATORS: when the update was built msgid "Created" msgstr "Ustvarjeno" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "KritiÄno" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "Na voljo je preverjanje kriptografske kontrolne vsote" #. TRANSLATORS: current value of a BIOS setting msgid "Current Value" msgstr "Trenutna vrednost" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "Trenutna razliÄica" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "ID-NAPRAVE|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Možnosti razhroÅ¡Äevanja" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "RazÅ¡irjanje ..." #. TRANSLATORS: command description msgid "Delete an EFI boot entry" msgstr "IzbriÅ¡e zagonski vnos za EFI" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Opis" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "Odklopi v naÄin zagonskega nalagalnika" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "Podrobnosti" #. TRANSLATORS: the best known configuration is a set of software that we know #. works #. * well together. In the OEM and ODM industries it is often called a BKC msgid "Deviate from the best known configuration?" msgstr "Odstopate od najbolj znane konfiguracije?" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "Zastavice naprave" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "DoloÄilo ID naprave" #. TRANSLATORS: description of the device requests msgid "Device Requests" msgstr "Zahteve naprave" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Dodana naprava:" #. TRANSLATORS: the device is already connected msgid "Device already exists" msgstr "Naprava že obstaja" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "Napetost baterije naprave je prenizka" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse #, c-format msgid "Device battery power is too low (%u%%, requires %u%%)" msgstr "Napolnjenost baterije naprave je prenizka (%u %%, zahtevano je vsaj %u %%)" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "Naprava lahko obnovi napake bliskovnega pisanja" #. TRANSLATORS: lid means "laptop top cover" msgid "Device cannot be updated while the lid is closed" msgstr "Naprave ni mogoÄe posodobiti, ko je pokrov zaprt" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Spremenjena naprava:" #. TRANSLATORS: a version check is required for all firmware msgid "Device firmware is required to have a version check" msgstr "Vdelana programska oprema naprave mora imeti preverjanje razliÄice" #. TRANSLATORS: emulated means we are pretending to be a different model msgid "Device is emulated" msgstr "Naprava je emulirana" #. TRANSLATORS: device cannot be interrupted, for instance taking a phone call msgid "Device is in use" msgstr "Naprava je v uporabi" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "Naprava je zaklenjena" #. TRANSLATORS: we have two ways of communicating with the device, so we hide #. one msgid "Device is lower priority than an equivalent device" msgstr "Naprava ima nižjo prednost kot ekvivalentna naprava" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "Naprava je potrebna za namestitev vseh priloženih izdaj" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "Naprava je nedosegljiva" #. TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode msgid "Device is unreachable, or out of wireless range" msgstr "Naprava je nedosegljiva ali je zunaj brezžiÄnega obmoÄja" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "Naprava je uporabna ves Äas trajanja posodobitve" #. TRANSLATORS: usually this is when we're waiting for a reboot msgid "Device is waiting for the update to be applied" msgstr "Naprava je pripravljena za uveljavitev posodobitve" #. TRANSLATORS: success, so say thank you to the user msgid "Device list uploaded successfully, thanks!" msgstr "Seznam naprav je uspeÅ¡no naložen, hvala!" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Odstranjena naprava:" #. TRANSLATORS: as in, wired mains power for a laptop msgid "Device requires AC power to be connected" msgstr "Naprava mora biti prikljuÄena na elektriÄno omrežje" #. TRANSLATORS: device does not have a display connected msgid "Device requires a display to be plugged in" msgstr "Naprava zahteva, da je zaslon prikljuÄen" #. TRANSLATORS: The device cannot be updated due to missing vendor's license." msgid "Device requires a software license to update" msgstr "Naprava za posodobitev potrebuje licenco programske opreme" #. TRANSLATORS: longer description msgid "Device software updates are provided for this device." msgstr "Za to napravo so na voljo posodobitve programske opreme naprave." #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "Naprava ima veÄ stopenj posodobitve" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "Naprava podpira preklop na drugo vejo vdelane programske opreme" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "Posodobitev naprave je treba aktivirati" #. TRANSLATORS: save the old firmware to disk before installing the new one msgid "Device will backup firmware before installing" msgstr "Naprava bo pred namestitvijo varnostno kopirala vdelano programsko opremo" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "Naprava se po dokonÄanju posodobitve ne bo veÄ prikazala" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "Naprave, ki so bile uspeÅ¡no posodobljene:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "Naprave, ki niso bile pravilno posodobljene:" #. TRANSLATORS: message letting the user there is an update #. * waiting, but there is a reason it cannot be deployed msgid "Devices with firmware updates that need user action: " msgstr "Naprave s posodobitvami vdelane programske opreme, ki zahtevajo ukrepanje:" #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "Naprave brez razpoložljivih posodobitev vdelane programske opreme: " #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "Naprave z najnovejÅ¡o razliÄico vdelane programske opreme, ki je na voljo:" #. TRANSLATORS: this is for the device tests msgid "Did not find any devices with matching GUIDs" msgstr "Ni najdenih naprav z ujemajoÄimi se GUID-ji" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "OnemogoÄeno" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "OnemogoÄi dani oddaljeni strežnik" #. TRANSLATORS: command description msgid "Disables virtual testing devices" msgstr "OnemogoÄi navidezne preizkusne naprave" #. TRANSLATORS: the OS the release was tested on msgid "Distribution" msgstr "Distribucija" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "Ne preverjaj, ali so na voljo stari metapodatki" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "Ne preverjaj, ali je na voljo neprijavljena zgodovina" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "Ne preverjaj, ali naj bodo omogoÄeni oddaljeni strežniki za prenos" #. TRANSLATORS: command line option msgid "Do not check or prompt for reboot after update" msgstr "Po posodobitvi ne preverjaj in ne pozivaj k vnoviÄnemu zagonu" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "Ne vkljuÄi predpone domene dnevnika" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "Ne vkljuÄi predpone Äasovnega žiga" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "Ne izvajaj varnostnih preverjanj naprav" #. TRANSLATORS: command line option msgid "Do not prompt for devices" msgstr "Ne povpraÅ¡uj po napravah" #. TRANSLATORS: command line option msgid "Do not prompt to fix security issues" msgstr "Ne pozivaj k odpravljanju varnostnih težav" #. TRANSLATORS: command line option msgid "Do not search the firmware when parsing" msgstr "Med razÄlenjevanjem ne iÅ¡Äi vdelane programske opreme" #. TRANSLATORS: warning message shown after update has been scheduled msgid "Do not turn off your computer or remove the AC adaptor while the update is in progress." msgstr "Med posodabljanjem ne izklapljajte raÄunalnika in ne odstranjujte napajalnika." #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "Ne piÅ¡i v zbirko podatkov zgodovine" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "Ali razumete posledice spremembe veje vdelane programske opreme?" #. TRANSLATORS: ask the user if it's okay to convert, #. * "it" being the data contained in the EFI boot entry msgid "Do you want to convert it now?" msgstr "Ali ga želite zdaj pretvoriti?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "Ali želite onemogoÄiti to funkcijo za prihodnje posodobitve?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "Ali želite zdaj osvežiti ta oddaljeni strežnik?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "Ali želite samodejno naložiti poroÄila za prihodnje posodobitve?" #. TRANSLATORS: command line option msgid "Don't prompt for authentication (less details may be shown)" msgstr "Ne pozivaj k preverjanju pristnosti (morda bo prikazanih manj podrobnosti)" #. TRANSLATORS: success msgid "Done!" msgstr "Opravljeno!" #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "Želite povrniti %s s %s na starejÅ¡o %s?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "Zamenja vdelano programsko opremo naprave s starejÅ¡o razliÄico" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "Povrnitev na starejÅ¡o razliÄico %s ..." #. TRANSLATORS: command description msgid "Download a file" msgstr "Prejmite datoteko" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Poteka prejemanje …" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "Odloži podatke SMBIOS-a iz datoteke" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "Trajanje" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "EMULATION-FILE [ARCHIVE-FILE]" msgstr "DATOTEKA-EMULACIJE [ARHIVSKA-DATOTEKA]" #. TRANSLATORS: longer description msgid "Each system should have tests to ensure firmware security." msgstr "Vsak sistem mora imeti preizkuse za zagotovitev varnosti vdelane programske opreme." #. TRANSLATORS: command description msgid "Emulate a device using a JSON manifest" msgstr "Posnemajte napravo z manifestom JSON" #. TRANSLATORS: this device is not actually real msgid "Emulated" msgstr "Emulirani" #. TRANSLATORS: Title: if we are emulating a different host msgid "Emulated host" msgstr "Emulirani gostitelj" msgid "Enable" msgstr "OmogoÄi" msgid "Enable emulation data collection" msgstr "OmogoÄi zbiranje podatkov emulacije" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "Želite omogoÄiti nov oddaljeni strežnik?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "Želite omogoÄiti ta oddaljeni strežnik?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "OmogoÄeno" #. TRANSLATORS: Plugin is active only if hardware is found msgid "Enabled if hardware matches" msgstr "OmogoÄeno, Äe se strojna oprema ujema" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "OmogoÄi dani oddaljeni strežnik" #. TRANSLATORS: command description msgid "Enables virtual testing devices" msgstr "OmogoÄi navidezne preizkusne naprave" #. TRANSLATORS: longer description msgid "Enabling firmware updates for the BIOS allows fixing security issues." msgstr "ÄŒe omogoÄite posodobitve vdelane programske opreme za BIOS, lahko odpravite varnostne težave." msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "To funkcijo omogoÄite na lastno odgovornost, kar pomeni, da se morate glede morebitnih težav, ki jih povzroÄijo te posodobitve, obrniti na proizvajalca originalne opreme. Samo težave s samim postopkom posodabljanja je treba vložiti na $OS_RELEASE:BUG_REPORT_URL$." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "Ta oddaljeni strežnik omogoÄite na lastno odgovornost." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Å ifrirano" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "Å ifrirani RAM" msgid "Encrypted RAM makes it impossible for information that is stored in device memory to be read if the memory chip is removed and accessed." msgstr "Å ifrirani RAM onemogoÄa branje informacij, ki so shranjene v pomnilniku naprave, Äe je pomnilniÅ¡ki Äip odstranjen in je dostopano do njega." #. TRANSLATORS: the vendor is no longer supporting the device msgid "End of life" msgstr "Konec življenjske dobe" #. TRANSLATORS: The BIOS setting can only be changed to fixed values msgid "Enumeration" msgstr "Å tevilÄenje" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "IzbriÅ¡i vso zgodovino posodobitev vdelane programske opreme" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Poteka brisanje..." #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "KonÄaj po kratki zakasnitvi" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "KonÄaj, ko se programnik naloži" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "Izvozi strukturo datoteke vdelane programske opreme v XML" #. TRANSLATORS: command description msgid "Export firmware history for manual upload" msgstr "Izvozi zgodovino vdelane programske opreme za roÄni prenos" #. TRANSLATORS: command description msgid "Extract a firmware blob to images" msgstr "Izvlecite zbirko podatkov vdelane programske opreme v slike" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "DATOTEKA" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "DATOTEKA [ID-NAPRAVE|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "IMEDATOTEKE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "IME-DATOTEKE POTRDILO OSEBNI-KLJUÄŒ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "IMEDATOTEKE ID-NAPRAVE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME OFFSET DATA [FIRMWARE-TYPE]" msgstr "IME-DATOTEKE ODMIK PODATKI [VRSTA-VDELANE-PROGRAMSKE-OPREME]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "IME-DATOTEKE [ID-NAPRAVE|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "IME-DATOTEKE [VRSTA-VDELANE-PROGRAMSKE-OPREME]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" msgstr "IMEDATOTEKE-SRC IMEDATOTEKE-DST [FIRMWARE-VRSTA-SRC] [FIRMWARE-VRSTA-DST]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" msgstr "IMEDATOTEKE|KONTR-VSOTA1[,KONTR-VSOTA2][,KONTR-VSOTA3]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "Napaka" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Posodobitve ni bilo mogoÄe uveljaviti" #. TRANSLATORS: error message for Windows msgid "Failed to connect to Windows service, please ensure it's running." msgstr "Povezave s storitvijo Windows ni možno vzpostaviti, prepriÄajte se, da se izvaja." #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "Povezave s prikritim procesom ni bilo mogoÄe vzpostaviti" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "Ni bilo mogoÄe naložiti krajevnega dbx" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "Sistemskega dbx ni bilo mogoÄe naložiti" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "Zaklepanje je spodletelo" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Argumentov ni bilo mogoÄe razÄleniti" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "RazÄlenjevanje datoteke je spodletelo" #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #, c-format msgid "Failed to parse flags for %s" msgstr "Zastavic za %s ni bilo mogoÄe razÄleniti" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "RazÄlenitev krajevnega dbx ni uspela" #. TRANSLATORS: a feature is something like "can show an image" msgid "Failed to set front-end features" msgstr "Nastavljanje funkcij proÄelja ni uspelo" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "Preverjanje vsebine ESP ni uspelo" #. TRANSLATORS: item is FALSE msgid "False" msgstr "napak" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Ime datoteke" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "Podpis imena datoteke" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "Vir imena datoteke" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Zahtevano je ime datoteke" #. TRANSLATORS: command line option msgid "Filter with a set of device flags using a ~ prefix to exclude, e.g. 'internal,~needs-reboot'" msgstr "Filtriraj z nizom zastavic naprave s predpono ~, da izkljuÄite, npr. \"notranji,~potreben-ponovni zagon\" oz. 'internal,~needs-reboot'" #. TRANSLATORS: command line option msgid "Filter with a set of release flags using a ~ prefix to exclude, e.g. 'trusted-release,~trusted-metadata'" msgstr "Filtriraj z naborom zastavic za izdajo s predpono ~, da se izkljuÄijo npr. »zaupanja-vredna-izdaja,~zaupanja-vredni-metapodatki« oz. 'trusted-release,~trusted-metadata'" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware Attestation" msgstr "Atest strojne programske opreme" #. TRANSLATORS: longer description msgid "Firmware Attestation checks device software using a reference copy, to make sure that it has not been changed." msgstr "Potrdilo o vdelani programski opremi preveri programsko opremo naprave z referenÄno kopijo in se prepriÄa, da ni bila spremenjena." #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "Opisnik SPI BIOS-a vdelane programske opreme" #. TRANSLATORS: longer description msgid "Firmware BIOS Descriptor protects device firmware memory from being tampered with." msgstr "Deskriptor vdelane programske opreme BIOS-a Å¡Äiti pomnilnik vdelane programske opreme naprave pred nedovoljenimi posegi." #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "PodroÄje BIOS-a vdelane programske opreme" #. TRANSLATORS: longer description msgid "Firmware BIOS Region protects device firmware memory from being tampered with." msgstr "Firmware BIOS Region Å¡Äiti pomnilnik vdelane programske opreme naprave pred nedovoljenimi posegi." #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "Osnovni URI vdelane programske opreme" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "Storitev D-Bus posodobitve vdelane programske opreme" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Prikriti proces posodobitve vdelane programske opreme" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "Preverjanje orodja za posodabljanje vdelane programske opreme" msgid "Firmware Updater Verification checks that software used for updating has not been tampered with." msgstr "Preverjanje orodja za posodabljanje vdelane programske opreme preveri, ali programska oprema, uporabljena za posodabljanje, ni bila spremenjena." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "Posodobitve vdelane programske opreme" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "PripomoÄek za vdelano programsko opremo" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "ZaÅ¡Äita pred pisanjem vdelane programske opreme" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "ZaÅ¡Äitni zaklep pred pisanjem vdelane programske opreme" #. TRANSLATORS: longer description msgid "Firmware Write Protection protects device firmware memory from being tampered with." msgstr "ZaÅ¡Äita pred zapisovanjem vdelane programske opreme Å¡Äiti pomnilnik vdelane programske opreme naprave pred nedovoljenimi posegi." #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "Atest strojne programske opreme" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "Vdelana programska oprema je že blokirana" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "Vdelana programska oprema Å¡e ni blokirana" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "Metapodatki vdelane programske oprem niso bili posodobljeni %u dan in morda niso aktualni." msgstr[1] "Metapodatki vdelane programske oprem niso bili posodobljeni %u dni in morda niso aktualni." msgstr[2] "Metapodatki vdelane programske oprem niso bili posodobljeni %u dni in morda niso aktualni." msgstr[3] "Metapodatki vdelane programske oprem niso bili posodobljeni %u dni in morda niso aktualni." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "Posodobitve vdelane programske opreme" #. TRANSLATORS: user needs to run a command, %1 is 'fwupdmgr unlock' #, c-format msgid "Firmware updates disabled; run '%s' to enable" msgstr "Posodobitve vdelane programske opreme so onemogoÄene; zaženite »%s«, da jih omogoÄite" #. TRANSLATORS: command description msgid "Fix a specific host security attribute" msgstr "Popravi doloÄen varnostni atribut gostitelja" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fix reverted successfully" msgstr "Popravek je bil uspeÅ¡no povrnjen" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fixed successfully" msgstr "UspeÅ¡no popravljeno" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "Zastavice" #. TRANSLATORS: command line option msgid "Force the action by relaxing some runtime checks" msgstr "Vsilite dejanje s sprostitvijo nekaterih preverjanj med izvajanjem" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Najdeno" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "Zaznano je popolno Å¡ifriranje diska" #. TRANSLATORS: we might ask the user the recovery key when next booting #. Windows msgid "Full disk encryption secrets may be invalidated when updating" msgstr "Skrivnosti polnega Å¡ifriranja diska se lahko pri posodabljanju razveljavijo" #. TRANSLATORS: Title: if the part has been fused msgid "Fused Platform" msgstr "Spojena platforma" #. TRANSLATORS: Title: if the part has been fused msgid "Fused platform" msgstr "Spojena platforma" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID-ji" msgstr[1] "GUID" msgstr[2] "GUID-ja" msgstr[3] "GUID-ji" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "GUID|DEVICE-ID" msgstr "GUID|ID-NAPRAVE" msgid "Get BIOS settings" msgstr "Pridobi nastavitve BIOS-a" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "Pridobi vse zastavice naprav, ki jih podpira fwupd" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Pridobi vse naprave, ki podpirajo posodobitve vdelane programske opreme" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "Pridobi vse omogoÄene vstavke, registrirane v sistemu" #. TRANSLATORS: command description msgid "Get all known version formats" msgstr "Pridobi vse znane oblike razliÄic" #. TRANSLATORS: command description msgid "Get device report metadata" msgstr "Pridobi metapodatke poroÄila o napravi" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Pridobi podrobnosti o datoteki strojne programske opreme" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "Pridobi nastavljene oddaljene strežnike" #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "Pridobi varnostne atribute gostitelja" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "Pridobi seznam odobrene vdelane programske opreme" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "Pridobi seznam blokirane vdelane programske opreme" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Pridobi seznam posodobitev za prikljuÄeno strojno opremo" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "Pridobi izdaje za napravo" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Pridobi rezultate zadnje posodobitve" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "DATOTEKA-HWIDS" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "Strojna oprema Äaka na ponovno prikljuÄitev" #. TRANSLATORS: the release urgency msgid "High" msgstr "Visoko" #. TRANSLATORS: title for host security events msgid "Host Security Events" msgstr "Dogodki varnosti gostitelja" #. TRANSLATORS: error message for unsupported feature #, c-format msgid "Host Security ID (HSI) is not supported" msgstr "Varnostni ID gostitelja (HSI) ni podprt" #. TRANSLATORS: success, so say thank you to the user msgid "Host Security ID attributes uploaded successfully, thanks!" msgstr "Atributi varnostnega ID-ja gostitelja so bili uspeÅ¡no naloženi, hvala!" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "Varnostni ID gostitelja:" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX" msgstr "INDEKS" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX KEY [VALUE]" msgstr "INDEKS KLJUÄŒ [VREDNOST]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX NAME TARGET [MOUNTPOINT]" msgstr "INDEKS IME CILJ [TOÄŒKAPRIKLOPA]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX1,INDEX2" msgstr "INDEKS1,INDEKS2" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INHIBIT-ID" msgstr "ID-PREPREÄŒI" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "ZaÅ¡Äita IOMMU" #. TRANSLATORS: longer description msgid "IOMMU Protection prevents connected devices from accessing unauthorized parts of system memory." msgstr "ZaÅ¡Äita IOMMU povezanim napravam prepreÄuje dostop do nepooblaÅ¡Äenih delov sistemskega pomnilnika." #. TRANSLATORS: HSI event title msgid "IOMMU device protection disabled" msgstr "ZaÅ¡Äita naprav IOMMU je onemogoÄena" #. TRANSLATORS: HSI event title msgid "IOMMU device protection enabled" msgstr "OmogoÄena je zaÅ¡Äita naprav IOMMU" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "Nedejavno …" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "Prezri stroga preverjanja SSL pri prenosu datotek" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "Prezri napake kontrolne vsote vdelane programske opreme" #. TRANSLATORS: command line option msgid "Ignore firmware hardware mismatch failures" msgstr "Prezri napake pri neujemanju strojne opreme" #. TRANSLATORS: command line option msgid "Ignore non-critical firmware requirements" msgstr "Prezri ne kritiÄne zahteve vdelane programske opreme" #. TRANSLATORS: try to help msgid "Ignoring SSL strict checks, to do this automatically in the future export DISABLE_SSL_STRICT in your environment" msgstr "Ignoriranje strogih preverjanj SSL, da to storite samodejno v prihodnjih izvoznih DISABLE_SSL_STRICT v svojem okolju" #. TRANSLATORS: show the user a generic image that can be themed msgid "Image" msgstr "Slika" #. TRANSLATORS: show the user a random image from the internet msgid "Image (custom)" msgstr "Slika (po meri)" #. TRANSLATORS: the inhibit ID is a short string like dbus-123456 #, c-format msgid "Inhibit ID is %s." msgstr "ID prepreÄitve je %s." #. TRANSLATORS: command description msgid "Inhibit the system to prevent upgrades" msgstr "Nastavite sistem, da prepreÄi nadgradnje" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "Trajanje namestitve" #. TRANSLATORS: command description msgid "Install a firmware file in cabinet format on this hardware" msgstr "Na to strojno opremo namesti datoteko vdelane programske opreme v obliki paketa" #. TRANSLATORS: command description msgid "Install a raw firmware blob on a device" msgstr "Namestite neobdelano dvojiÅ¡ko zbirko podatkov vdelane programske opreme v napravo" #. TRANSLATORS: command description msgid "Install a specific firmware file on all devices that match" msgstr "Namesti doloÄeno datoteko vdelane programske opreme v vse naprave, ki se ujemajo" #. TRANSLATORS: command description msgid "Install a specific firmware on a device, all possible devices will also be installed once the CAB matches" msgstr "Namestite doloÄeno vdelano programsko opremo na napravo, vse možne naprave bodo prav tako nameÅ¡Äene, Äe se CAB ujema" msgid "Install old version of signed system firmware" msgstr "Namesti staro razliÄico podpisane sistemske strojne programske opreme" msgid "Install old version of unsigned system firmware" msgstr "Namesti staro razliÄico nepodpisane sistemske strojne programske opreme" msgid "Install signed device firmware" msgstr "Namesti podpisano strojno programsko opremo naprave" msgid "Install signed system firmware" msgstr "Namesti podpisano sistemsko strojno programsko opremo" msgid "Install unsigned device firmware" msgstr "Namesti nepodpisano strojno programsko opremo naprave" msgid "Install unsigned system firmware" msgstr "Namesti nepodpisano sistemsko strojno programsko opremo" #. TRANSLATORS: stay on one firmware version unless the new version is #. explicitly #. * specified msgid "Installing a specific release is explicitly required" msgstr "Namestitev doloÄene izdaje je izrecno zahtevana" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "NameÅ¡Äanje posodobitve strojne programske opreme ..." #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "NameÅ¡Äanje na %s ..." #. TRANSLATORS: if it breaks, you get to keep both pieces msgid "Installing this update may also void any device warranty." msgstr "Namestitev te posodobitve lahko razveljavi tudi garancijo naprave." #. TRANSLATORS: The BIOS setting only accepts integers in a fixed range msgid "Integer" msgstr "integer" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM Protected" msgstr "ZaÅ¡Äiteno z Intel BootGuard ACM" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "ZaÅ¡Äiteno z Intel BootGuard ACM" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard Error Policy" msgstr "Pravilnik o napakah Intel BootGuard" msgid "Intel BootGuard Error Policy ensures the device does not continue to start if its device software has been tampered with." msgstr "Pravilnik o napakah Intel BootGuard zagotavlja, da se naprava ne zažene naprej, Äe je bila spremenjena programska oprema naprave." #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard Fuse" msgstr "Varovalka Intel Boot Guard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "Varovalka Intel BootGuard OTP" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard Verified Boot" msgstr "Preverjeni zagon Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Pravilnik o napakah Intel BootGuard" #. TRANSLATORS: longer description msgid "Intel BootGuard prevents unauthorized device software from operating when the device is started." msgstr "Intel BootGuard prepreÄi delovanje nepooblaÅ¡Äene programske opreme naprave ob zagonu naprave." #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Preverjen zagon Intel BootGuard" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS Mitigation" msgstr "Blaženje posledic Intel GDS" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS mitigation" msgstr "Blaženje posledic Intel GDS" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Manufacturing Mode" msgstr "NaÄin izdelave mehanizma Intel Management Engine" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the "override" is #. enabled #. * with a jumper -- luckily it is probably not accessible to end users on #. consumer #. * boards msgid "Intel Management Engine Override" msgstr "Preglasitev mehanizma Intel Management Engine" #. TRANSLATORS: longer description msgid "Intel Management Engine Override disables checks for device software tampering." msgstr "Intel Management Engine Override onemogoÄi preverjanja nedovoljenih posegov v programsko opremo naprave." #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Version" msgstr "RazliÄica orodja Intel Management Engine" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "Notranja naprava" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "Neveljavno" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "Neveljavni argumenti" #. TRANSLATORS: error message msgid "Invalid arguments, expected GUID" msgstr "Neveljavni argumenti, priÄakovan GUID" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX KEY [VALUE]" msgstr "Neveljavni argumenti, priÄakovano je INDEKS KLJUÄŒ [VREDNOST]" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX NAME TARGET [MOUNTPOINT]" msgstr "Neveljavni argumenti, priÄakovano je INDEKS IME CILJ [TOÄŒKAPRIKLOPA]" #. TRANSLATOR: This is the error message for #. * incorrect parameter msgid "Invalid arguments, expected an AppStream ID" msgstr "Neveljavni argumenti, priÄakovan je ID AppStream" #. TRANSLATORS: error message msgid "Invalid arguments, expected at least ARCHIVE FIRMWARE METAINFO" msgstr "Neveljavni argumenti, priÄakovani vsaj ARHIV VDELANAPROGRAMSKAOPREMA METAPODATKI" #. TRANSLATORS: error message msgid "Invalid arguments, expected base-16 integer" msgstr "Neveljavni argumenti, priÄakovano je celo Å¡tevilo base-16" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "Je povrnitev na starejÅ¡o razliÄico" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "Je v naÄinu zagonskega nalagalnika" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "Je nadgradnja" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "Težave" msgstr[1] "Težava" msgstr[2] "Težavi" msgstr[3] "Težave" #. TRANSLATORS: HSI event title msgid "Kernel is no longer tainted" msgstr "Jedro ni veÄ okuženo" #. TRANSLATORS: HSI event title msgid "Kernel is tainted" msgstr "Jedro je okuženo" #. TRANSLATORS: HSI event title msgid "Kernel lockdown disabled" msgstr "Zaklepanje jedra je onemogoÄeno" #. TRANSLATORS: HSI event title msgid "Kernel lockdown enabled" msgstr "OmogoÄeno zaklepanje jedra" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "MESTO" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "Nazadnje spremenjeno" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "Preostalo je manj kot eno minuto" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "Dovoljenje" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "Zaklepanje jedra Linuxa" #. TRANSLATORS: longer description msgid "Linux Kernel Lockdown mode prevents administrator (root) accounts from accessing and changing critical parts of system software." msgstr "NaÄin zaklepanja jedra Linuxa skrbniÅ¡kim (korenskim) raÄunom prepreÄuje dostop do kritiÄnih delov sistemske programske opreme in njihovo spreminjanje." msgid "Linux Kernel Swap temporarily saves information to disk as you work. If the information is not protected, it could be accessed by someone if they obtained the disk." msgstr "Zamenjava jedra Linuxa med delom zaÄasno shrani informacije na disk. ÄŒe podatki niso zaÅ¡Äiteni, lahko do njih dostopa nekdo, Äe je dobil dostop do diska." #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "Preverjanje jedra Linuxa" msgid "Linux Kernel Verification makes sure that critical system software has not been tampered with. Using device drivers which are not provided with the system can prevent this security feature from working correctly." msgstr "Preverjanje jedra Linuxa zagotavlja, da kritiÄna sistemska programska oprema ni bila spremenjena. Uporaba gonilnikov naprav, ki niso priloženi sistemu, lahko prepreÄi pravilno delovanje te varnostne funkcije." #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "Izmenjevalni prostor Linux" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Storitev dobaviteljev strojne programske opreme za Linux (stabilna strojna programska oprema)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Storitev dobaviteljev strojne programske opreme za Linux (preizkusna strojna programska oprema)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Jedro Linuxa" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Zaklepanje jedra Linuxa" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Izmenjevalni prostor Linux" #. TRANSLATORS: command description msgid "List EFI boot files" msgstr "IzpiÅ¡i seznam datotek zagona EFI" #. TRANSLATORS: command description msgid "List EFI boot parameters" msgstr "IzpiÅ¡i seznam parametrov zagona EFI" #. TRANSLATORS: command description msgid "List EFI variables with a specific GUID" msgstr "IzpiÅ¡i seznam spremenljivk EFI z doloÄenim GUID-om" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "IzpiÅ¡i vnose v dbx" #. TRANSLATORS: command description msgid "List the available firmware GTypes" msgstr "IzpiÅ¡e seznam razpoložljive vdelane programske opreme GTypes" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "IzpiÅ¡e seznam razpoložljivih vrst vdelane programske opreme" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "IzpiÅ¡e datoteke v ESP" #. TRANSLATORS: command description msgid "Load device emulation data" msgstr "Naloži emulacijske podatke naprave" #. TRANSLATORS: the plugin was created from a .so object, and was not built-in msgid "Loaded from an external module" msgstr "Naloženo iz zunanjega modula" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "Poteka nalaganje …" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "Zaklenjen" #. TRANSLATORS: the release urgency msgid "Low" msgstr "Nizko" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refers #. * to the private/public key used to secure loading of firmware msgid "MEI Key Manifest" msgstr "Manifest kljuÄa MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refer #. * to the private/public key used to secure loading of firmware msgid "MEI key manifest" msgstr "Manifest kljuÄa MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "NaÄin izdelave MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "Preglasitev MEI" msgid "MEI version" msgstr "RazliÄica MEI" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "RoÄno omogoÄi doloÄene vstavke" #. TRANSLATORS: longer description msgid "Manufacturing Mode is used when the device is manufactured and security features are not yet enabled." msgstr "NaÄin izdelave se uporablja, ko je naprava izdelana, varnostne funkcije pa Å¡e niso omogoÄene." #. TRANSLATORS: Longest valid string for BIOS setting msgid "Maximum length" msgstr "NajveÄja dolžina" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "N_ajveÄja vrednost" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "Srednje" #. TRANSLATORS: ask the user to do a simple task which should be translated msgid "Message" msgstr "SporoÄila" #. TRANSLATORS: ask the user a question, and it will not be translated msgid "Message (custom)" msgstr "SporoÄilo (po meri)" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "Podpis metapodatkov" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "URI metapodatkov" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "Metapodatke je mogoÄe pridobiti iz storitve vdelane programske opreme ponudnika Linuxa (Linux Vendor Firmware Service)." #. TRANSLATORS: error message for a user who ran fwupdmgr #. * refresh recently -- %1 is '--force' #, c-format msgid "Metadata is up to date; use %s to refresh again." msgstr "Metapodatki so posodobljeni; Uporabite %s za ponovno osvežitev." #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "NajmanjÅ¡a razliÄica" #. TRANSLATORS: Shortest valid string for BIOS setting msgid "Minimum length" msgstr "NajmanjÅ¡a dolžina" #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "NajmanjÅ¡a vrednost" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "Spremeni konfiguracijsko vrednost prikritega procesa" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "Spremeni dani oddaljeni strežnik" msgid "Modify a configured remote" msgstr "Spremeni nastavljeni oddaljeni strežnik" msgid "Modify daemon configuration" msgstr "Spremeni prilagoditev prikritega procesa" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "Spremljaj dogodke prikritega procesa" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "Priklopi ESP" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "Po namestitvi je potreben ponovni zagon" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "Potreben je ponovni zagon" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "Po namestitvi je potrebna zaustavitev" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "Nova razliÄica" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "Nobeno dejanje ni doloÄeno!" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "Ni povrnitev na starejÅ¡o razliÄico za %s" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "ID-jev vdelane programske opreme ni bilo mogoÄe najti" #. TRANSLATORS: nothing found msgid "No firmware found" msgstr "Vdelane programske opreme ni bilo mogoÄe najti" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Zaznana ni nobena strojna oprema z možnostjo posodobitve vdelane programske opreme" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "Izdaje niso na voljo" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "Oddaljeni strežniki trenutno niso omogoÄeni, zato metapodatki niso na voljo." #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "Oddaljeni strežniki niso na voljo" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "Ni naprav, ki jih je mogoÄe posodobiti" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "Posodobitve niso na voljo" msgid "No updates available for remaining devices" msgstr "Za preostale naprave ni na voljo nobenih posodobitev" #. TRANSLATORS: error message #, c-format msgid "No volume matched %s" msgstr "Noben pogon se ne ujema s/z %s" #. TRANSLATORS: version cannot be installed due to policy msgid "Not approved" msgstr "Ni odobreno" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "Predmeta ni mogoÄe najti" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "Ni podprto" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "V redu" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "V redu!" #. TRANSLATORS: the firmware old version msgid "Old version" msgstr "Stara razliÄica" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "Pokaži samo eno vrednost PCR" #. TRANSLATORS: command line option msgid "Only use peer-to-peer networking when downloading files" msgstr "Omrežje vrstnikov uporabljaj samo pri prenosu datotek" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "Dovoljene so samo nadgradnje razliÄic" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "Preglasi kot privzeto pot ESP" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Firmware" msgstr "Vdelana programska oprema P2P" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Metadata" msgstr "Metapodatki P2P" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "POT" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "RazÄleni in prikaži podrobnosti o datoteki vdelane programske opreme" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "RazÄlenjevanje posodobitve dbx ..." #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "RazÄlenjevanje sistemskega dbx ..." #. TRANSLATORS: remote filename base msgid "Password" msgstr "Geslo" #. TRANSLATORS: command description msgid "Patch a firmware blob at a known offset" msgstr "Popravi dvojiÅ¡ko zbirko podatkov vdelane programske opreme pri znanem odmiku" msgid "Payload" msgstr "Obremenitev" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "V Äakanju" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "Želite izvesti opravilo?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "Odpravljanje napak na platformi" #. TRANSLATORS: longer description msgid "Platform Debugging allows device security features to be disabled. This should only be used by hardware manufacturers." msgstr "Odpravljanje napak na platformi omogoÄa onemogoÄanje varnostnih funkcij naprave. To naj uporabljajo samo proizvajalci strojne opreme." #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "Odpravljanje napak na platformi" #. TRANSLATORS: 'recovery key' here refers to a code, rather than a physical #. metal thing msgid "Please ensure you have the volume recovery key before continuing." msgstr "Pred nadaljevanjem se prepriÄajte, da imate kljuÄ za obnovitev nosilca." #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "Vnesite Å¡tevilko od 0 do %u: " #. TRANSLATORS: the user isn't reading the question -- %1 is 'Y' and %2 is #. * 'N' #, c-format msgid "Please enter either %s or %s: " msgstr "Vnesite %s ali %s:" #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "Manjkajo odvisnosti vstavkov" #. TRANSLATORS: The plugin is only for testing msgid "Plugin is only for testing" msgstr "Vstavek je namenjen le preizkuÅ¡anju" #. TRANSLATORS: Possible values for a bios setting msgid "Possible Values" msgstr "Možne vrednosti" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA Protection" msgstr "ZaÅ¡Äita DMA pred zagonom" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "ZaÅ¡Äita DMA pred zagonom" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is disabled" msgstr "ZaÅ¡Äita DMA pred zagonom je onemogoÄena" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is enabled" msgstr "ZaÅ¡Äita DMA pred zagonom je omogoÄena" #. TRANSLATORS: longer description msgid "Pre-boot DMA protection prevents devices from accessing system memory after being connected to the computer." msgstr "ZaÅ¡Äita DMA pred zagonom prepreÄuje napravam dostop do sistemskega pomnilnika, ko so prikljuÄene na raÄunalnik." #. TRANSLATORS: warning message msgid "Press unlock on the device to continue the update process." msgstr "Na napravi pritisnite odklepanje, da nadaljujete postopek posodabljanja." #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "PrejÅ¡nja razliÄica" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Prednost" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "Težave" msgid "Proceed with upload?" msgstr "Ali želite nadaljevati z nalaganjem na strežnik?" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Processor Security Checks" msgstr "Varnostni pregledi procesorja" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "Processor rollback protection" msgstr "ZaÅ¡Äita pred povrnitvijo procesorja" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "LastniÅ¡ko" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "ID-ODDALJENEGA" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID KEY VALUE" msgstr "ID-ODDALJENI KLJUÄŒ VREDNOST" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "Samo za branje" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "Preberi dvojiÅ¡ko zbirko podatkov vdelane programske opreme iz naprave" #. TRANSLATORS: command description msgid "Read a firmware from a device" msgstr "Preberi vdelano programsko opremo iz naprave" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "Branje iz %s ..." #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Poteka branje ..." #. TRANSLATORS: Plugin is active and in use msgid "Ready" msgstr "Pripravljeno" #. TRANSLATORS: how often we should refresh the metadata msgid "Refresh Interval" msgstr "Interval osveževanja" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "Osveži metapodatke iz oddaljenega strežnika" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "Želite ponovno namestiti %s na %s?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "V napravo znova namestite trenutno vdelano programsko opremo" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "VnoviÄno namesti vdelano programsko opremo na napravo" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "Veja izdaje" #. TRANSLATORS: release attributes msgid "Release Flags" msgstr "Zastavice izdaje" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "ID izdaje" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "ID oddaljenega strežnika" #. TRANSLATORS: command description msgid "Removes devices to watch for future emulation" msgstr "Odstrani naprave za spremljanje prihodnje emulacije" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "URI poroÄila" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "Prijavljeno oddaljenemu strežniku" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "Zahtevanega datoteÄnega sistema efivarfs ni bilo mogoÄe najti" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "Zahtevane strojne opreme ni bilo mogoÄe najti" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "Zahteva zagonski nalagalnik" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Zahteva internetno povezavo" msgid "Reset daemon configuration" msgstr "Ponastavi prilagoditev prikritega procesa" #. TRANSLATORS: sets something in the daemon configuration file msgid "Resets a daemon configuration section" msgstr "Ponastavi razdelek prilagoditev prikritega procesa" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Želite ponovno zagnati zdaj?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "Želite znova zagnati prikriti proces, da bo sprememba uveljavljena?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Ponovni zagon naprave ..." #. TRANSLATORS: command description msgid "Retrieve BIOS settings. If no arguments are passed all settings are returned" msgstr "Pridobi nastavitve BIOS-a. ÄŒe argumenti niso posredovani, so vrnjene vse nastavitve" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "Vrnite vse ID-je strojne opreme za napravo" #. TRANSLATORS: ask the user to upload msgid "Review and upload report now?" msgstr "Želite pregledati in naložiti poroÄilo zdaj?" #. TRANSLATORS: longer description msgid "Rollback Protection prevents device software from being downgraded to an older version that has security problems." msgstr "ZaÅ¡Äita pred povrnitvijo prepreÄuje zamenjavo programske opreme naprave s starejÅ¡o razliÄico, ki ima varnostne težave." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr get-upgrades` #, c-format msgid "Run `%s` for more information." msgstr "Za veÄ informacij zaženite »%s«." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr sync` #, c-format msgid "Run `%s` to complete this action." msgstr "Zaženite »%s«, da dokonÄate to dejanje." #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "Zaženite rutino ÄiÅ¡Äenja vstavkov, ko uporabljate zbirko dvojiÅ¡kih podatkov za namestitev" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "Zaženite rutino priprave kompozitnih vstavkov pri uporabi zbirke dvojiÅ¡kih podatkov za namestitev (install-blob)" #. TRANSLATORS: command description msgid "Run the post-reboot cleanup action" msgstr "Zaženi dejanje ÄiÅ¡Äenja po ponovnem zagonu" #. TRANSLATORS: tell a user how to get information #, c-format msgid "Run without '%s' to see" msgstr "Izvedite brez '%s' za ogled" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "Izvajalno jedro je prestaro" #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "Pripona izvajalne datoteke" #. TRANSLATORS: Software Bill of Materials link msgid "SBOM" msgstr "SBOM" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SECTION" msgstr "RAZDELEK" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING VALUE" msgstr "NASTAVITEV VREDNOST" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING1 VALUE1 [SETTING2] [VALUE2]" msgstr "NASTAVITEV1 VREDNOST1 [NASTAVITEV2] [VREDNOST2]" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "SMAP" msgstr "SMAP" #. TRANSLATORS: Title: Whether firmware is locked down msgid "SMM locked down" msgstr "SMM (NUS) zaklenjen" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "Deskriptor SPI BIOS-a" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "Regija SPI BIOS-a" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "Zaklepanje SPI" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "SPI replay protection" msgstr "ZaÅ¡Äita pred ponovnim predvajanjem SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "Pisanje SPI" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "SPI write protection" msgstr "ZaÅ¡Äita pred zapisovanjem SPI" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "PODSISTEM GONILNIK [ID-NAPRAVE|GUID]" #. TRANSLATORS: command description msgid "Save a file that allows generation of hardware IDs" msgstr "Shranite datoteko, ki omogoÄa tvorjenje ID-jev strojne opreme" #. TRANSLATORS: command description msgid "Save device emulation data" msgstr "Shrani emulacijske podatke naprave" #. TRANSLATORS: key for a offline report filename msgid "Saved report" msgstr "Shranjeno poroÄilo" #. TRANSLATORS: Scalar increment for integer BIOS setting msgid "Scalar Increment" msgstr "Skalarni prirastek" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "ÄŒasovno naÄrtovanje procesov ..." #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "Varni zagon je onemogoÄen" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "Varni zagon je omogoÄen" msgid "Security hardening for HSI" msgstr "Utrjevanje varnosti za HSI" #. TRANSLATORS: the %1 is a URL to a wiki page #, c-format msgid "See %s for more details." msgstr "Za veÄ podrobnosti glejte %s." #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "Za veÄ podrobnosti glejte %s." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Izbrana naprava" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Izbrani nosilec" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "Serijska Å¡tevilka" #. TRANSLATORS: Configured a BIOS setting to a value #, c-format msgid "Set BIOS setting '%s' using '%s'." msgstr "Nastavite nastavitev BIOS-a »%s« z uporabo možnosti »%s«." #. TRANSLATORS: command description msgid "Set a BIOS setting" msgstr "DoloÄite nastavitev BIOS-a" msgid "Set one or more BIOS settings" msgstr "Nastavite eno ali veÄ nastavitev BIOS-a" #. TRANSLATORS: command description msgid "Set or remove an EFI boot hive entry" msgstr "Nastavite ali odstranite zagonski vnos satovja za EFI" #. TRANSLATORS: command description msgid "Set the EFI boot next" msgstr "Nadalje nastavite zagon EFI" #. TRANSLATORS: command description msgid "Set the EFI boot order" msgstr "Nastavite zaporedje zagona EFI" #. TRANSLATORS: command line option msgid "Set the download retries for transient errors" msgstr "Nastavite Å¡tevilo ponovnih poskusov za obÄasne napake" #. TRANSLATORS: command description msgid "Sets one or more BIOS settings" msgstr "Nastavi eno ali veÄ nastavitev BIOS-a" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Nastavi seznam odobrene vdelane programske opreme" #. TRANSLATORS: type of BIOS setting msgid "Setting type" msgstr "Vrsta nastavitve" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "Nastavitve bodo veljale po ponovnem zagonu sistema" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "Deli zgodovino vdelane programske opreme z razvijalci" #. TRANSLATORS: command line option msgid "Show all results" msgstr "Pokaži vse rezultate" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "Pokaži razliÄici odjemalca in prikritega procesa" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "Pokaži opisne informacije prikritega procesa za doloÄeno domeno" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "Pokaži informacije razhroÅ¡Äevanja za vse domene" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Pokaži možnosti razhroÅ¡Äevanja" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Pokaži naprave, ki jih ni mogoÄe posodobiti" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Pokaži dodatne podatke za razhroÅ¡Äevanje" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "Pokaži zgodovino posodobitev strojne programske opreme" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "Pokaži izraÄunano razliÄico dbx" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "Želite zaustaviti zdaj?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "PodpiÅ¡i vdelano programsko opremo z novim kljuÄem" msgid "Sign data using the client certificate" msgstr "PodpiÅ¡i podatke s potrdilom odjemalca" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "PodpiÅ¡i podatke s potrdilom odjemalca" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "PodpiÅ¡i naložene podatke s potrdilom odjemalca" msgid "Signature" msgstr "Podpis" #. TRANSLATORS: firmware is verified on-device the payload using strong crypto msgid "Signed Payload" msgstr "Podpisani tovor" #. TRANSLATORS: file size of the download msgid "Size" msgstr "Velikost" #. TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM msgid "Some of the platform secrets may be invalidated when updating this firmware." msgstr "Nekatere skrivnosti platforme se lahko razveljavijo s posodabljanjem te vdelane programske opreme." #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "Vir" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "DoloÄite datoteko zbirke podatkov dbx" msgid "Stop the fwupd service" msgstr "Ustavi storitev fwupd" #. TRANSLATORS: The BIOS setting accepts strings msgid "String" msgstr "Niz znakov" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "UspeÅ¡no zakljuÄeno" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "UspeÅ¡no aktivirane vse naprave" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "UspeÅ¡no onemogoÄen oddaljeni strežnik" #. TRANSLATORS: comment explaining result of command msgid "Successfully disabled test devices" msgstr "UspeÅ¡no onemogoÄene preizkusne naprave" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "UspeÅ¡no preneseni novi metapodatki: " #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "UspeÅ¡no omogoÄen in osvežen oddaljeni strežnik" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "UspeÅ¡no omogoÄen oddaljeni strežnik" #. TRANSLATORS: comment explaining result of command msgid "Successfully enabled test devices" msgstr "UspeÅ¡no omogoÄene preizkusne naprave" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "UspeÅ¡no nameÅ¡Äena vdelana programska oprema" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "UspeÅ¡no spremenjena konfiguracijska vrednost" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "UspeÅ¡no spremenjen oddaljeni strežnik" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "RoÄno uspeÅ¡no osveženi metapodatki" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration section" msgstr "UspeÅ¡no ponastavljena konfiguracijska vrednost" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration values" msgstr "UspeÅ¡no ponastavljene konfiguracijske vrednosti" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "UspeÅ¡no posodobljene kontrolne vsote naprave" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "UspeÅ¡no naloženih %u poroÄil" msgstr[1] "UspeÅ¡no naloženo %u poroÄilo" msgstr[2] "UspeÅ¡no naloženi %u poroÄili" msgstr[3] "UspeÅ¡no naložena %u poroÄila" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "UspeÅ¡no preverjene kontrolne vsote naprave" #. TRANSLATORS: the device showed up in time #, c-format msgid "Successfully waited %.0fms for device" msgstr "UspeÅ¡no Äakanje %.0fms za napravo" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Povzetek" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "Supervisor Mode Access Prevention" msgstr "PrepreÄevanje dostopa v naÄinu nadzornika" #. TRANSLATORS: longer description msgid "Supervisor Mode Access Prevention ensures critical parts of device memory are not accessed by less secure programs." msgstr "PrepreÄitev dostopa do naÄina nadzornika zagotavlja, da do kritiÄnih delov pomnilnika naprave ne dostopajo manj varni programi." #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "Podprto" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "Podprta CPE" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "Podprto na oddaljenem strežniku" #. TRANSLATORS: Title: a better sleep state msgid "Suspend To Idle" msgstr "ZaÄasna prekinitev v mirovanje" #. TRANSLATORS: Title: sleep state msgid "Suspend To RAM" msgstr "ZaÄasna prekinitev v pomnilnik RAM" #. TRANSLATORS: longer description msgid "Suspend to Idle allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Prekinitev v mirovanje omogoÄa napravi, da hitro preide v stanje mirovanja, da prihrani energijo. Ko je bila naprava zaÄasno prekinjena, je bil njen pomnilnik fiziÄno odstranjen in dostop do informacij." #. TRANSLATORS: longer description msgid "Suspend to RAM allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "Prekinitev v RAM omogoÄa, da naprava hitro preide v stanje pripravljenosti, da prihrani energijo. Ko je bila naprava zaÄasno prekinjena, je bil njen pomnilnik fiziÄno odstranjen in dostop do informacij." #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "ZaÄasna prekinitev v mirovanje" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "ZaÄasna prekinitev v RAM" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "Želite preklopiti vejo iz %s v %s?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "Preklopite vejo vdelane programske opreme na napravi" #. TRANSLATORS: command description msgid "Sync firmware versions to the chosen configuration" msgstr "Sinhronizirajte razliÄice vdelane programske opreme z izbrano konfiguracijo" #. TRANSLATORS: Title: Whether firmware is locked down msgid "System Management Mode" msgstr "NaÄin upravljanja sistema" #. TRANSLATORS: this CLI tool is now preventing system updates msgid "System Update Inhibited" msgstr "Posodobitev sistema je prepreÄena" #. TRANSLATORS: longer description msgid "System management mode is used by the firmware to access resident BIOS code and data." msgstr "NaÄin upravljanja sistema uporablja programje strojne opreme za dostop do kode in podatkov rezidenÄnega BIOS-a." #. TRANSLATORS: as in laptop battery power msgid "System power is too low" msgstr "Napolnjenost sistemske baterije je prenizka" #. TRANSLATORS: as in laptop battery power #, c-format msgid "System power is too low (%u%%, requires %u%%)" msgstr "Napolnjenost sistemske baterije je prenizka (%u %%, potrebnih je %u %%)" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "Sistem zahteva zunanji vir napajanja" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "BESEDILO" #. TRANSLATORS: longer description msgid "TPM (Trusted Platform Module) is a computer chip that detects when hardware components have been tampered with." msgstr "TPM (Trusted Platform Module) je raÄunalniÅ¡ki Äip, ki zazna, kdaj so bile spremenjene komponente strojne opreme." #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "Rekonstrukcija TPM PCR0" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is invalid" msgstr "Rekonstrukcija TPM PCR0 ni veljavna" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is now valid" msgstr "Rekonstrukcija TPM PCR0 je zdaj veljavna" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM Platform Configuration" msgstr "Konfiguracija platforme TPM" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM Reconstruction" msgstr "Rekonstrukcija TPM" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM empty PCRs" msgstr "Prazni PCR-ji TPM" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "Oznake" msgstr[1] "Oznaka" msgstr[2] "Oznaki" msgstr[3] "Oznake" #. TRANSLATORS: we're saving all USB events for emulation msgid "Tagged for emulation" msgstr "OznaÄeno za emulacijo" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "V nevarnosti" #. show the user the entire data blob msgid "Target" msgstr "Cilj" #. TRANSLATORS: command description msgid "Test a device using a JSON manifest" msgstr "Preskusite napravo z manifestom JSON" #. TRANSLATORS: when the release was tested msgid "Tested" msgstr "PreizkuÅ¡eno" #. TRANSLATORS: the %s is a vendor name, e.g. Lenovo #, c-format msgid "Tested by %s" msgstr "PreizkuÅ¡eno s strani %s" #. TRANSLATORS: someone we trust has tested this msgid "Tested by trusted vendor" msgstr "PreizkuÅ¡eno s strani zaupanja vrednega proizvajalca" #. TRANSLATORS: the boot entry was in a legacy format msgid "The EFI boot entry is not in hive format, and shim may not be new enough to read it." msgstr "Zagonski vnos za EFI ni v obliki satovja, povezovalni element morda ni dovolj nov, da bi ga lahko prebral." #. TRANSLATORS: try to treat the legacy format as a hive msgid "The EFI boot entry was not in hive format, falling back" msgstr "Zagonski vnos za EFI ni v obliki satovja, povrnitev v prejÅ¡nje stanje" #. TRANSLATORS: longer description msgid "The Intel Management Engine Key Manifest must be valid so that the device firmware can be trusted by the CPU." msgstr "Manifest kljuÄa Intel Management Engine mora biti veljaven, tako da lahko CPE zaupa strojni programski opremi naprave." #. TRANSLATORS: longer description msgid "The Intel Management Engine controls device components and needs to have a recent version to avoid security issues." msgstr "Intel Management Engine nadzoruje komponente naprave in mora imeti najnovejÅ¡o razliÄico, da se izogne varnostnim težavam." #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "LVFS je brezplaÄna storitev, ki deluje kot samostojna pravna oseba in nima nobene povezave z $OS_RELEASE:NAME$. VaÅ¡ distributer morda ni preveril nobene posodobitve strojno programske opreme za združljivost z vaÅ¡im sistemom ali prikljuÄenimi napravami. Vso strojno programsko opremo zagotavlja samo originalni proizvajalec opreme." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Platform Configuration is used to check whether the device start process has been modified." msgstr "Konfiguracija platforme modula zaupanja TPM (Trusted Platform Module) se uporablja za preverjanje, ali je bil postopek zagona naprave spremenjen." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Reconstruction is used to check whether the device start process has been modified." msgstr "Rekonstrukcija modula zaupanja TPM (Trusted Platform Module) se uporablja za preverjanje, ali je bil postopek zagona naprave spremenjen." #. TRANSLATORS: this is more background on a security measurement problem msgid "The TPM PCR0 differs from reconstruction." msgstr "TPM PCR0 se razlikuje od rekonstrukcije." #. TRANSLATORS: longer description msgid "The UEFI Platform Key is used to determine if device software comes from a trusted source." msgstr "KljuÄ platforme UEFI se uporablja za ugotavljanje, ali programska oprema naprave prihaja iz zaupanja vrednega vira." #. TRANSLATORS: HSI event title msgid "The UEFI certificate store is now up to date" msgstr "Shramba potrdil UEFI je zdaj posodobljena" #. TRANSLATORS: longer description msgid "The UEFI db contains the list of valid certificates that can be used to authorize what EFI binaries are allowed to run." msgstr "Zbirka podatkov UEFI vsebuje seznam veljavnih potrdil, ki jih lahko uporabite za overjanje, katere izvrÅ¡ne datoteke EFI je dovoljeno izvajati." #. TRANSLATORS: longer description msgid "The UEFI system can set up memory attributes at boot which prevent common exploits from running." msgstr "Sistem UEFI lahko nastavi atribute pomnilnika ob zagonu, ki prepreÄujejo pogosta vsiljena izvajanja pred izvrÅ¡itvijo." #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "Prikriti proces je naložil kodo tretje strani in razvijalci novih razliÄic je ne podpirajo veÄ!" #. TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name #, c-format msgid "The firmware from %s is not supplied by %s, the hardware vendor." msgstr "Vdelane programske opreme podjetja %s ne dobavlja %s, izdelovalec strojne opreme." #. TRANSLATORS: try to help msgid "The system clock has not been set correctly and downloading files may fail." msgstr "Sistemska ura ni bila pravilno nastavljena in prenos datotek morda ne bo uspel." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been re-inserted." msgstr "Posodobitev se bo nadaljevala, ko bo kabel naprave USB znova vstavljen." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged and then re-inserted." msgstr "Posodobitev se bo nadaljevala, ko bo kabel naprave USB izkljuÄen in nato znova vstavljen." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged." msgstr "Posodobitev se bo nadaljevala, ko bo kabel naprave USB izkljuÄen." #. TRANSLATORS: warning message msgid "The update will continue when the device power cable has been removed and re-inserted." msgstr "Posodobitev se bo nadaljevala, ko bo napajalni kabel naprave odstranjen in znova vstavljen." #. TRANSLATORS: naughty vendor msgid "The vendor did not supply any release notes." msgstr "Proizvajalec ni predložil opomb ob izdaji." #. TRANSLATORS: now list devices with unfixed high-priority issues msgid "There are devices with issues:" msgstr "Obstajajo naprave s težavami:" #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "Ni blokiranih datotek vdelane programske opreme" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "Ni odobrene vdelane programske opreme." #. TRANSLATORS: %1 is the current device version number, and %2 is the #. command name, e.g. `fwupdmgr sync` #, c-format msgid "This device will be reverted back to %s when the %s command is performed." msgstr "Ta naprava bo povrnjena nazaj na %s, ko bo izveden ukaz %s." #. TRANSLATORS: the vendor did not upload this msgid "This firmware is provided by LVFS community members and is not provided (or supported) by the original hardware vendor." msgstr "To vdelano programsko opremo zagotavljajo Älani skupnosti LVFS in je ne zagotavlja (ali podpira) prvotni prodajalec strojne opreme." #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "Ta paket ni bil preverjen, morda ne bo deloval pravilno." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "Ta program lahko pravilno deluje le kot root" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "Ta oddaljeni strežnik vsebuje vdelano programsko opremo, na katero ne velja embargo, vendar jo Å¡e vedno preizkuÅ¡a prodajalec strojne opreme. PrepriÄajte se, da imate naÄin za roÄno zamenjavo vdelane programske opreme, Äe posodobitev vdelane programske opreme ne uspe." #. TRANSLATORS: error message msgid "This system doesn't support firmware settings" msgstr "Ta sistem ne podpira nastavitev vdelane programske opreme" #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "Ta sistem ima težave z izvajanjem HSI." #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "Ta sistem ima nizko raven varnosti HSI." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "To orodje omogoÄa skrbniku, da uporabi posodobitve dbx UEFI." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "To orodje omogoÄa skrbniku, da poizveduje in nadzoruje demona fwupd, kar mu omogoÄa izvajanje dejanj, kot je namestitev ali zamenjava vdelane programske opreme." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "To orodje skrbniku omogoÄa uporabo vstavkov fwupd, ne da bi bili nameÅ¡Äeni v gostiteljskem sistemu." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can add a kernel argument of '%s', but it will only be active after restarting the computer." msgstr "To orodje lahko doda argument jedra »%s«, vendar bo aktiven Å¡ele po vnoviÄnem zagonu raÄunalnika." #. TRANSLATORS: the %1 is a BIOS setting name. #. * %2 and %3 are the values, e.g. "True" or "Windows10" #, c-format msgid "This tool can change the BIOS setting '%s' from '%s' to '%s' automatically, but it will only be active after restarting the computer." msgstr "To orodje lahko samodejno spremeni nastavitev BIOS-a »%s« iz »%s« v »%s«, vendar bo aktivna Å¡ele po ponovnem zagonu raÄunalnika." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can change the kernel argument from '%s' to '%s', but it will only be active after restarting the computer." msgstr "To orodje lahko spremeni argument jedra iz »%s« v »%s«, vendar bo aktiven Å¡ele po vnoviÄnem zagonu raÄunalnika." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "To orodje bo prebralo in razÄlenilo dnevnik dogodkov TPM iz vdelane programske opreme sistema." #. TRANSLATORS: the update state of the specific device msgid "Transient failure" msgstr "ZaÄasen neuspeh" #. TRANSLATORS: item is TRUE msgid "True" msgstr "prav" #. TRANSLATORS: We verified the metadata against the server msgid "Trusted metadata" msgstr "Zaupanja vredni metapodatki" #. TRANSLATORS: We verified the payload against the server msgid "Trusted payload" msgstr "Zaupanja vreden tovor" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Vrsta" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI Bootservice Variables" msgstr "Spremenljivke zagonske storitve UEFI" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition may not be set up correctly" msgstr "Razdelek UEFI ESP morda ni pravilno nastavljen" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "Razdelek UEFI ESP ni zaznan ali konfiguriran" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI Memory Protection" msgstr "ZaÅ¡Äita pomnilnika UEFI" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "KljuÄ platforme UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "Varni zagon vmesnika UEFI" #. TRANSLATORS: longer description msgid "UEFI Secure Boot prevents malicious software from being loaded when the device starts." msgstr "Varni zagon UEFI Secure Boot prepreÄuje nalaganje zlonamerne programske opreme ob zagonu naprave." #. TRANSLATORS: longer description msgid "UEFI boot service variables should not be readable from runtime mode." msgstr "Spremenljivke zagonske storitve UEFI ne smejo biti berljive iz naÄina izvajanja." #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI bootservice variables" msgstr "Spremenljivke zagonske storitve UEFI" #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "Posodobitve kapsul UEFI niso na voljo ali omogoÄene pri nastavitvi vdelane programske opreme" #. TRANSLATORS: Title: is UEFI db up-to-date msgid "UEFI db" msgstr "Db UEFI" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "PripomoÄek UEFI dbx" #. TRANSLATORS: system is not booted in UEFI mode msgid "UEFI firmware can not be updated in legacy BIOS mode" msgstr "Vdelane programske opreme UEFI ni mogoÄe posodobiti v zastarelem naÄinu BIOS-a" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI memory protection" msgstr "ZaÅ¡Äita pomnilnika UEFI" #. TRANSLATORS: HSI event title msgid "UEFI memory protection enabled and locked" msgstr "ZaÅ¡Äita pomnilnika UEFI je omogoÄena in zaklenjena" #. TRANSLATORS: HSI event title msgid "UEFI memory protection enabled but not locked" msgstr "ZaÅ¡Äita pomnilnika UEFI je omogoÄena, vendar ni zaklenjena" #. TRANSLATORS: HSI event title msgid "UEFI memory protection is now locked" msgstr "ZaÅ¡Äita pomnilnika UEFI je zdaj zaklenjena" #. TRANSLATORS: HSI event title msgid "UEFI memory protection is now unlocked" msgstr "ZaÅ¡Äita pomnilnika UEFI je zdaj odklenjena" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "KljuÄ platforme UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "Varni zagon vmesnika UEFI" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "Povezave s storitvijo ni možno vzpostaviti" #. TRANSLATORS: error message msgid "Unable to find attribute" msgstr "Atributa ni mogoÄe najti" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "Razveži trenutni gonilnik" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "Deblokiranje vdelane programske opreme:" #. TRANSLATORS: command description msgid "Unblocks a specific firmware from being installed" msgstr "Odblokira namestitev doloÄene vdelane programske opreme" #. TRANSLATORS: command description msgid "Undo the host security attribute fix" msgstr "Razveljavi popravek varnostnega atributa gostitelja" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "NeÅ¡ifrirano" #. TRANSLATORS: command description msgid "Uninhibit the system to allow upgrades" msgstr "Nastavite sistem, da omogoÄi nadgradnje" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Neznano" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Neznana naprava" msgid "Unlock the device to allow access" msgstr "Odkleni napravo za omogoÄanje dostopa" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "Odklenjeno" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "Odklene napravo za dostop do vdelane programske opreme" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "Odklopi ESP" #. TRANSLATORS: message shown after device has been marked for emulation msgid "Unplug and replug the device to continue the update process." msgstr "Odklopite napravo in jo znova prikljuÄite, da nadaljujete postopek posodabljanja." #. TRANSLATORS: firmware payload is unsigned and it is possible to modify it msgid "Unsigned Payload" msgstr "Nepodpisani tovor" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "Nepodprta razliÄica prikritega procesa %s, razliÄica odjemalca je %s" #. TRANSLATORS: Suffix: the HSI result msgid "Untainted" msgstr "Neomadeževano" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "Posodobljivo" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Napaka pri posodobitvi" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "Posodobi sliko" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "Posodobi sporoÄilo" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "Posodobi stanje" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "Napaka pri posodobitvi je znana težava, za veÄ informacij obiÅ¡Äite ta URL:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Želite posodobiti zdaj?" #. TRANSLATORS: command description msgid "Update the stored cryptographic hash with current ROM contents" msgstr "Posodobite shranjeno kriptografsko kontrolno vsoto s trenutno vsebino ROM-a" msgid "Update the stored device verification information" msgstr "Posodobi shranjene informacije za preverjanje naprave" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "Posodobi shranjene metapodatke s trenutno vsebino" #. TRANSLATORS: command description msgid "Updates all specified devices to latest firmware version, or all devices if unspecified" msgstr "Posodobi vse navedene naprave na najnovejÅ¡o razliÄico vdelane programske opreme ali vse naprave, Äe niso navedene" #. TRANSLATORS: how many local devices can expect updates now #, c-format msgid "Updates have been published for %u local device" msgid_plural "Updates have been published for %u of %u local devices" msgstr[0] "Posodobitve so objavljene za %u krajevnih naprav" msgstr[1] "Posodobitve so objavljene za %u od %u krajevnih naprav" msgstr[2] "Posodobitve so objavljene za %u od %u krajevnih naprav" msgstr[3] "Posodobitve so objavljene za %u od %u krajevnih naprav" msgid "Updating" msgstr "Posodabljanje" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "Posodabljanje %s ..." #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "Želite nadgraditi %s s %s na %s?" #. TRANSLATORS: ask the user to upload msgid "Upload data now?" msgstr "Želite zdaj naložiti podatke?" #. TRANSLATORS: command description msgid "Upload the list of updatable devices to a remote server" msgstr "Naloži seznam posodobljivih naprav na oddaljeni strežnik" #. TRANSLATORS: ask the user to share, %s is something #. * like: "Linux Vendor Firmware Service" #, c-format msgid "Upload these anonymous results to the %s to help other users?" msgstr "Želite naložiti te anonimne rezultate na %s, da pomagate drugim uporabnikom?" #. TRANSLATORS: explain why we want to upload #, c-format msgid "Uploading a device list allows the %s team to know what hardware exists, and allows us to put pressure on vendors that do not upload firmware updates for their hardware." msgstr "Nalaganje seznama naprav omogoÄa ekipi %s, da so seznanjeni, kakÅ¡na strojna oprema obstaja, ter da pritiskamo na tiste proizvajalce, ki ne nalagajo posodobitev vdelane programske opreme za svojo strojno opremo." #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "Nalaganje poroÄil o vdelani programski opremi pomaga prodajalcem strojne opreme, da hitro prepoznajo neuspeÅ¡ne in uspeÅ¡ne posodobitve v resniÄnih napravah." #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "Nujnost" #. TRANSLATORS: explain how to get help, %1 is #. * 'fwupdtool --help' #. TRANSLATORS: explain how to get help, #. * where $1 is something like 'fwupdmgr --help' #, c-format msgid "Use %s for help" msgstr "Uporabite %s za pomoÄ" #. TRANSLATORS: CTRL^C [holding control, and then pressing C] will exit the #. program msgid "Use CTRL^C to cancel." msgstr "Za preklic uporabite CTRL^C." #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "Uporabnik je bil obveÅ¡Äen" #. TRANSLATORS: remote filename base msgid "Username" msgstr "UporabniÅ¡ko ime" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "VERSION1 VERSION2 [FORMAT]" msgstr "VERSION1 VERSION2 [FORMAT]" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "Veljavno" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "Preverjanje vsebine ESP ..." #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "RazliÄica" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "Ponudnik" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "Poteka preverjanje ..." #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "RazliÄica" #. TRANSLATORS: the fwupd version the release was tested on msgid "Version[fwupd]" msgstr "RazliÄica[fwupd]" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "OPOZORILO" #. TRANSLATORS: command description msgid "Wait for a device to appear" msgstr "PoÄakajte, da se naprava prikaže" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "ÄŒakanje …" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "Spremljaj spremembe strojne opreme" #. TRANSLATORS: check various UEFI and ACPI tables are unchanged after the #. update msgid "Will measure elements of system integrity around an update" msgstr "Izmeri elemente integritete sistema okoli posodobitve" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "Zapisovanje datoteke:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Poteka zapisovanje ..." #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from a recovery or installation disk, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "PrepriÄajte se, da želite obnoviti nastavitev z obnovitvenega ali namestitvenega diska, saj lahko ta sprememba povzroÄi, da se sistem ne zažene v Linux ali povzroÄi drugo nestabilnost sistema." #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from the system firmware setup, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "PrepriÄajte se, da želite obnoviti nastavitev iz nastavitve vdelane programske opreme sistema, saj lahko ta sprememba povzroÄi, da se sistem ne zažene v Linux ali povzroÄi drugo nestabilnost sistema." #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "VaÅ¡ distributer morda ni preveril nobene posodobitve vdelane programske opreme za združljivost z vaÅ¡im sistemom ali prikljuÄenimi napravami." #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "S to vdelano programsko opremo se lahko poÅ¡koduje vaÅ¡a strojna oprema in namestitev te izdaje lahko razveljavi garancijo %s." #. TRANSLATORS: BKC is the industry name for the best known configuration and #. is a set #. * of firmware that works together #, c-format msgid "Your system is set up to the BKC of %s." msgstr "VaÅ¡ sistem je nastavljen na BKC %s." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[APPSTREAM_ID]" msgstr "[APPSTREAM_ID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[KONTROLNAVSOTA]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[ID-NAPRAVE|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[ID-NAPRAVE|GUID] [VEJA]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [VERSION]" msgstr "[ID-NAPRAVE|GUID] [RAZLIÄŒICA]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE]" msgstr "[NAPRAVA]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILE FILE_SIG REMOTE-ID]" msgstr "[DATOTEKA PODP_DATOTEKE ID-ODDALJENEGA]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[IMEDATOTEKE1] [IMEDATOTEKE2]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FWUPD-VERSION]" msgstr "[FWUPD-RAZLIÄŒICA]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[REASON] [TIMEOUT]" msgstr "[REASON] [TIMEOUT]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SECTION] KEY VALUE" msgstr "[RAZDELEK] KLJUÄŒ VREDNOST" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2] [--no-authenticate]" msgstr "[NASTAVITEV1] [NASTAVITEV2] [--no-authenticate]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2]..." msgstr "[NASTAVITEV1] [NASTAVITEV2]..." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[DATOTEKA-SMBIOS|DATOTEKA-HWIDS]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "privzeto" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "PripomoÄek dnevnika dogodkov fwupd TPM" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "Vstavki fwupd" fwupd-2.0.10/po/sr.po000066400000000000000000000350771501337203100143300ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # MiloÅ¡ Popović , 2016 # Марко М. КоÑтић (Marko M. Kostić) , 2015-2018 # МироÑлав Ðиколић , 2017 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Serbian (http://app.transifex.com/freedesktop/fwupd/language/sr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: sr\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "СтароÑÑ‚" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "ÐÐ»Ð¸Ñ˜Ð°Ñ Ð½Ð° %s" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Дозволи уназађивање издања фирмвера" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "Потребно је поново покретање да би Ñе иÑправка применила." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Одговори Ñа да на Ñва питања" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Идентификујем…" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Потребна је пријава за уназађивање фирмвера на преноÑивом уређају" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Потребна је пријава за уназађивање фирмвера на овој машини" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "Идентификовање је потребно за измену подешеног удаљеног Ñервера који Ñе кориÑти за ажурирања фирмвера" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Потребна је пријава за откључавање уређаја" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Потребна је пријава за ажурирање фирмвера на преноÑивом уређају" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Потребна је пријава за ажурирање фирмвера на овој машини" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "Потребна је пријава за ажурирање причуваних Ñума провере за уређај" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "Отказао" #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Променио" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Чек-Ñума" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "ЧиÑти резултате поÑледњег ажурирања" #. TRANSLATORS: error message msgid "Command not found" msgstr "Ðаредба није пронађена" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Опције отклањања проблема" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "РаÑпакујем…" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "ОпиÑ" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Додат је уређај:" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Промењен је уређај:" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Уклоњен је уређај:" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "Уређаји који Ñу ажурирани иÑправно:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "Уређаји који ниÑу ажурирани иÑправно:" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "Ðе проверавај Ñтаре метаподатке" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "Ðе проверавај непоÑлати иÑторијат" #. TRANSLATORS: success msgid "Done!" msgstr "Урађено!" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "Уназађује фирмвер на уређају" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Преузимам…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "Ишчитај SMBIOS податке из датотеке" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Омогућено" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "Обриши Ñав иÑторијат ажурирања фирмвера" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Бришем…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "Изађи након малог заÑтоја" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "Изађи након учитавања мотора" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Ðе могу да обрадим аргументе" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Ðазив датотеке" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "ÐŸÐ¾Ñ‚Ð¿Ð¸Ñ Ð½Ð°Ð·Ð¸Ð²Ð° датотеке" #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "ОÑновни URI фирмвера" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "Д-Ð‘ÑƒÑ ÑƒÑлуга ажурирања фирмвера" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Демон за ажурирање фирмвера" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "Ðлатка за фирмвер" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "Метаподаци фирмвера ниÑу ажурирани %u дан и можда Ñу заÑтарели." msgstr[1] "Метаподаци фирмвера ниÑу ажурирани %u дана и можда Ñу заÑтарели." msgstr[2] "Метаподаци фирмвера ниÑу ажурирани %u дана и можда Ñу заÑтарели." #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Ðашао" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "ГУИД" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Добави Ñве уређаје који подржавају ажурирање фирмвера" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Добави појединоÑти о датотеци Ñа фирмвером" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "Добавља подешена удаљена меÑта" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Добави ÑпиÑак Ñвих ажурирања за повезани уређај" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "Добавља издања за уређај" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Добавља резултате поÑледњег ажурирања" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "Мирујем…" msgid "Install signed device firmware" msgstr "ИнÑталирајте потпиÑани фирмвер за уређаје" msgid "Install signed system firmware" msgstr "ИнÑталирајте потпиÑани ÑиÑтемÑки фирмвер" msgid "Install unsigned device firmware" msgstr "ИнÑталирајте непотпиÑани фирмвер за уређаје" msgid "Install unsigned system firmware" msgstr "ИнÑталирајте непотпиÑани ÑиÑтемÑки фирмвер" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "ИнÑталирам ажурирање фирмвера…" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "Учитавам…" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "URI метаподатака" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "Мења дати удаљени Ñервер" msgid "Modify a configured remote" msgstr "Измени подешени удаљени Ñервер" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "Прати демона за догађајима" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Ðема хардвера којем Ñе може ажурирати фирмвер" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "У реду" #. TRANSLATORS: remote filename base msgid "Password" msgstr "Лозинка" msgid "Payload" msgstr "Товар" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "ВажноÑÑ‚" msgid "Proceed with upload?" msgstr "ÐаÑтавити Ñа отпремањем?" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Читам…" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "ОÑвежава метаподатке Ñа удаљеног Ñервера" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "Удаљени ИБ" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "URI извештаја" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Захтева везу Ñа интернетом" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Поново покренути Ñада?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Поново покрећем уређај…" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "Враћа Ñве ИБ-јеве хардвера на машини" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Заказујем…" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "Подели иÑторијат фирмвера Ñа програмерима" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "Прикажи издања клијента и демона" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Прикажи опције за отклањање проблема" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Прикажи додатне податке за отклањање проблема" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "Прикажи иÑторијат ажурирања фирмвера" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Сажетак" #. show the user the entire data blob msgid "Target" msgstr "Мета" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Ð’Ñ€Ñта" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Ðепознато" msgid "Unlock the device to allow access" msgstr "Откључајте уређај да биÑте дозволили приÑтуп" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "Откључава уређај за приÑтуп фирмверу" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "Узрок неуÑпеха ажурирања је познат, погледајте ову адреÑу за више података:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Ðжурирати Ñада?" msgid "Update the stored device verification information" msgstr "Ðжурирајте причуване податке потврђивања уређаја" #. TRANSLATORS: remote filename base msgid "Username" msgstr "КориÑничко име" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "Проверавам…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Издање" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "Чекам…" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Пишем…" fwupd-2.0.10/po/sv.po000066400000000000000000003700231501337203100143250ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Anders Jonsson , 2017,2019-2025 # andhe , 2017 # Josef Andersson , 2015,2017-2018 # Josef Andersson , 2015,2017 # Luna Jernberg , 2020-2021 # Sebastian Rasmussen , 2018-2020 # Sebastian Rasmussen , 2018 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Swedish (http://app.transifex.com/freedesktop/fwupd/language/sv/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: sv\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "%.0f minut kvarstÃ¥r" msgstr[1] "%.0f minuter kvarstÃ¥r" #. TRANSLATORS: BMC refers to baseboard management controller which #. * is the device that updates all the other firmware on the system #, c-format msgid "%s BMC Update" msgstr "%s BMC-uppdatering" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "%s-batteriuppdatering" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "%s CPU-mikrokodsuppdatering" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "%s-kamerauppdatering" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "%s-konfigurationsuppdatering" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "%s Consumer ME-uppdatering" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "%s-styrenhetsuppdatering" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "%s Corporate ME-uppdatering" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "%s-enhetsuppdatering" #. TRANSLATORS: Video Display refers to the laptop internal display or #. * external monitor #, c-format msgid "%s Display Update" msgstr "%s-skärmuppdatering" #. TRANSLATORS: Dock refers to the port replicator hardware laptops are #. * cradled in, or lowered onto #, c-format msgid "%s Dock Update" msgstr "%s-dockuppdatering" #. TRANSLATORS: drive refers to a storage device, e.g. SATA disk #, c-format msgid "%s Drive Update" msgstr "%s-enhetsuppdatering" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "%s inbäddad styrenhetsuppdatering" #. TRANSLATORS: a device that can read your fingerprint pattern #, c-format msgid "%s Fingerprint Reader Update" msgstr "%s-fingeravtrycksläsaruppdatering" #. TRANSLATORS: flash refers to solid state storage, e.g. UFS or eMMC #, c-format msgid "%s Flash Drive Update" msgstr "%s-flashenhetsuppdatering" #. TRANSLATORS: GPU refers to a Graphics Processing Unit, e.g. #. * the "video card" #, c-format msgid "%s GPU Update" msgstr "%s GPU-uppdatering" #. TRANSLATORS: a large pressure-sensitive drawing area typically used #. * by artists and digital artists #, c-format msgid "%s Graphics Tablet Update" msgstr "%s-ritplatteuppdatering" #. TRANSLATORS: two miniature speakers attached to your ears #, c-format msgid "%s Headphones Update" msgstr "%s-hörlursuppdatering" #. TRANSLATORS: headphones with an integrated microphone #, c-format msgid "%s Headset Update" msgstr "%s-headsetuppdatering" #. TRANSLATORS: an input device used by gamers, e.g. a joystick #, c-format msgid "%s Input Controller Update" msgstr "%s-inmatningsstyrenhetsuppdatering" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "%s-tangentbordsuppdatering" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "%s ME-uppdatering" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "%s-musuppdatering" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "%s-nätverksgränssnittsuppdatering" #. TRANSLATORS: SSD refers to a Solid State Drive, e.g. non-rotating #. * SATA or NVMe disk #, c-format msgid "%s SSD Update" msgstr "%s SSD-uppdatering" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "%s-lagringsstyrenhetsuppdatering" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "%s-systemuppdatering" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "%s TPM-uppdatering" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "%s Thunderbolt-styrenhetsuppdatering" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "%s-pekplatteuppdatering" #. TRANSLATORS: Dock refers to the port replicator device connected #. * by plugging in a USB cable -- which may or may not also provide power #, c-format msgid "%s USB Dock Update" msgstr "%s USB-dockuppdatering" #. TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth #. * device that stays in the USB port so the wireless peripheral works #, c-format msgid "%s USB Receiver Update" msgstr "%s USB-mottagaruppdatering" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "%s-uppdatering" #. TRANSLATORS: warn the user before updating, %1 is a device name #, c-format msgid "%s and all connected devices may not be usable while updating." msgstr "%s och alla anslutna enheter kanske inte gÃ¥r att använda under uppdatering." #. TRANSLATORS: %1 refers to some kind of security test, e.g. "Encrypted RAM". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s appeared: %s" msgstr "%s dök upp: %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "UEFI platform #. key". #. * %2 and %3 refer to results value, e.g. "Valid" and "Invalid" #, c-format msgid "%s changed: %s → %s" msgstr "%s ändrad: %s → %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "SPI BIOS #. region". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s disappeared: %s" msgstr "%s försvann: %s" #. TRANSLATORS: the device has a reason it can't update, e.g. laptop lid #. closed #, c-format msgid "%s is not currently updatable" msgstr "%s gÃ¥r för tillfället inte uppdatera" #. TRANSLATORS: %1 is a device name, e.g. "ThinkPad Universal #. * ThunderBolt 4 Dock" and %2 is "fwupdmgr activate" #, c-format msgid "%s is pending activation; use %s to complete the update." msgstr "%s väntar pÃ¥ aktivering, använd %s för att slutföra uppdateringen." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "%s-tillverkningsläge" #. TRANSLATORS: warn the user before #. * updating, %1 is a device name #, c-format msgid "%s must remain connected for the duration of the update to avoid damage." msgstr "%s mÃ¥ste förbli anslutna under tiden som uppdateringen pÃ¥gÃ¥r för att undvika skador." #. TRANSLATORS: warn the user before updating, %1 is a machine #. * name #, c-format msgid "%s must remain plugged into a power source for the duration of the update to avoid damage." msgstr "%s mÃ¥ste förbli ansluten till en strömkälla under tiden som uppdateringen pÃ¥gÃ¥r för att undvika skada." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "%s-Ã¥sidosättning" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "%s-version" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u dag" msgstr[1] "%u dagar" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "%u enhet har en uppgradering för fast programvara tillgänglig." msgstr[1] "%u enheter har en uppgradering för fast programvara tillgänglig." #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device is not the best known configuration." msgid_plural "%u devices are not the best known configuration." msgstr[0] "%u enhet har inte den mest kända konfigurationen." msgstr[1] "%u enheter har inte den mest kända konfigurationen." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u timme" msgstr[1] "%u timmar" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minut" msgstr[1] "%u minuter" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u sekund" msgstr[1] "%u sekunder" #. TRANSLATORS: device tests can be specific to a CPU type #, c-format msgid "%u test was skipped" msgid_plural "%u tests were skipped" msgstr[0] "%u test hoppades över" msgstr[1] "%u test hoppades över" #. TRANSLATORS: first percentage is current value, 2nd percentage is the #. * lowest limit the firmware update is allowed for the update to happen #, c-format msgid "%u%% (threshold %u%%)" msgstr "%u%% (tröskelvärde %u%%)" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(förÃ¥ldrad)" #. TRANSLATORS: HSI event title msgid "A TPM PCR is now an invalid value" msgstr "Ett TPM PCR har nu ett ogiltigt värde" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "AMD Firmware Replay Protection" msgstr "Ã…teruppspelningsskydd för AMD:s fasta programvara" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "AMD Firmware Write Protection" msgstr "Skrivskydd för AMD:s fasta programvara" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "AMD Secure Processor Rollback Protection" msgstr "Säkert AMD-tillbakarullningsskydd för processorn" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "ARCHIVE FIRMWARE METAINFO [FIRMWARE] [METAINFO] [JCATFILE]" msgstr "ARKIV FAST_PROGRAMVARA METAINFO [FAST_PROGRAMVARA] [METAINFO] [JCAT-FIL]" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "Ã…tgärd krävs:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "Aktivera enheter" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "Aktivera väntande enheter" msgid "Activate the new firmware on the device" msgstr "Aktiverar den nya fasta programvaran pÃ¥ enheten" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "Aktiverar uppdatering av fast programvara" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "Aktiverar uppdatering av fast programvara för" #. TRANSLATORS: command description msgid "Adds devices to watch for future emulation" msgstr "Lägger till enheter att bevaka för framtida emulering" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Ã…lder" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "Samtyck och aktivera fjärrkällan?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Alias för %s" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are now valid" msgstr "Alla TPM PCR är nu giltiga" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are valid" msgstr "Alla TPM PCR är giltiga" #. TRANSLATORS: an application is preventing system updates msgid "All devices are prevented from update by system inhibit" msgstr "Alla enheter förhindras att uppdatera av systemhindrande" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "Alla enheter av samma typ kommer uppdateras samtidigt" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "TillÃ¥t att nedgradera versioner av fast programvara" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "TillÃ¥t ominstallation av befintliga versioner av fast programvara" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "TillÃ¥t att byta gren för fast programvara" #. TRANSLATORS: error message msgid "Already exists, and no --force specified" msgstr "Finns redan, och --force angavs inte" #. TRANSLATORS: is not the main firmware stream msgid "Alternate branch" msgstr "Alternativ gren" #. TRANSLATORS: another application is updating the device already msgid "An update is in progress" msgstr "En uppdatering pÃ¥gÃ¥r" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "En uppdatering kräver en omstart för att slutföras." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "En uppdatering kräver att systemet stängs ned för att slutföras." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Svara ja pÃ¥ alla frÃ¥gor" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Tillämpa uppdatering även när det inte rekommenderas" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Tillämpa uppdateringsfiler" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "Tillämpar uppdatering…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Godkänd fast programvara:" msgstr[1] "Godkänd fast programvara:" #. TRANSLATORS: command description msgid "Asks the daemon to quit" msgstr "Ber demonen att avsluta" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "Fäst till fast programvaruläge" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Autentiserar…" #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "Autentiseringsdetaljer krävs" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Autentisering krävs för att nedgradera den fasta programvaran för en flyttbar enhet" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Autentisering krävs för att nedgradera den fasta programvaran pÃ¥ denna maskin" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to enable emulation data collection" msgstr "Autentisering krävs för att aktivera emuleringsdatainsamling" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to fix a host security issue" msgstr "Autentisering krävs för att fixa ett säkerhetsproblem pÃ¥ värden" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to load hardware emulation data" msgstr "Autentisering krävs för att läsa in hÃ¥rdvaruemuleringsdata" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "Autentisering krävs för att ändra BIOS-inställningar" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "Autentisering krävs för att ändra en konfigurerad fjärrkälla som används för uppdateringar av fast programvara" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "Autentisering krävs för att modifiera demonkonfiguration" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "Autentisering krävs för att läsa BIOS-inställningar" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to reset daemon configuration to defaults" msgstr "Autentisering krävs för att Ã¥terställa demonkonfigurationer till standardvärden" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to save hardware emulation data" msgstr "Autentisering krävs för att spara hÃ¥rdvaruemuleringsdata" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "Autentisering krävs för att sätta listan över godkänd fast programvara" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "Autentisering krävs för att signera data med klientcertifikatet" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to stop the firmware update service" msgstr "Autentisering krävs för att stoppa tjänsten för uppdatering av fast programvara" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "Autentisering krävs för att växla till den nya fasta programvaruversionen" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to undo the fix for a host security issue" msgstr "Autentisering krävs för att Ã¥terställa fixen för ett säkerhetsproblem pÃ¥ värden" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Autentisering krävs för att lÃ¥sa upp en enhet" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Autentisering krävs för att uppdatera den fasta programvaran pÃ¥ en flyttbar enhet" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Autentisering krävs för att uppdatera den fasta programvaran för denna maskin" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "Autentisering krävs för att uppdatera lagrade kontrollsummor för enheten" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "Automatisk rapportering" #. TRANSLATORS: we can auto-uninhibit after a timeout #, c-format msgid "Automatically uninhibiting in %ums…" msgstr "Släpper automatiskt hindrande om %ums…" #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "Skicka automatiskt varje gÃ¥ng?" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS Firmware Updates" msgstr "Uppdateringar för fast BIOS-programvara" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS Rollback Protection" msgstr "BIOS-tillbakarullningsskydd" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS firmware updates" msgstr "Uppdateringar för fast BIOS-programvara" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS rollback protection" msgstr "BIOS-tillbakarullningsskydd" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "BIOS-uppdateringar levererade via LVFS eller Windows Update" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "BUILDER-XML FILENAME-DST" msgstr "BYGGAR-XML FILNAMN-MÃ…L" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "Batteri" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "Bind ny kärndrivrutin" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "Blockerade fast programvarufiler:" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "Blockerad version" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "Blockera fast programvara:" #. TRANSLATORS: command description msgid "Blocks a specific firmware from being installed" msgstr "Blockerar en specifik fast programvara frÃ¥n att installeras" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "Starthanterarversion" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "Gren" #. TRANSLATORS: command description msgid "Build a cabinet archive from a firmware blob and XML metadata" msgstr "Bygg ett kabinettarkiv frÃ¥n en fast programvaru-blob och XML-metadata" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "Bygg en fast programvarufil" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * Utilized by OS means the distribution enabled it msgid "CET OS Support" msgstr "OS-stöd för CET" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * enabled means supported by the processor msgid "CET Platform" msgstr "CET-plattform" #. TRANSLATORS: longer description msgid "CPU Microcode must be updated to mitigate against various information-disclosure security issues." msgstr "CPU-mikrokod mÃ¥ste uppdateras för att förmildra mot diverse informationsavslöjande säkerhetsproblem." #. TRANSLATORS: we can save all device enumeration events for emulation msgid "Can tag for emulation" msgstr "Kan tagga för emulering" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "Avbryt" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "Avbruten" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "Kan inte tillämpa eftersom dbx-uppdatering redan har tillämpats." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Ändrad" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "Kontrollerar att kryptografisk hash matchar fast programvara" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Kontrollsumma" #. TRANSLATORS: get interactive prompt, where branch is the #. * supplier of the firmware, e.g. "non-free" or "free" msgid "Choose branch" msgstr "Välj gren" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "Välj enhet" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "Välj fast programvara" #. TRANSLATORS: get interactive prompt msgid "Choose release" msgstr "Välj utgÃ¥va" #. TRANSLATORS: get interactive prompt msgid "Choose volume" msgstr "Välj volym" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "Rensar resultaten frÃ¥n senaste uppdateringen" #. TRANSLATORS: error message msgid "Command not found" msgstr "Kommandot hittades inte" #. TRANSLATORS: is not supported by the vendor msgid "Community supported" msgstr "Stöds av gemenskapen" #. TRANSLATORS: command description msgid "Compares two versions for equality" msgstr "Jämför om tvÃ¥ versioner är lika" #. TRANSLATORS: title prefix for the BIOS settings dialog msgid "Configuration Change Suggested" msgstr "Föreslagen ändring av konfiguration" #. TRANSLATORS: no peeking msgid "Configuration is only readable by the system administrator" msgstr "Konfigurationen är endast läsbar av systemadministratören" #. TRANSLATORS: longer description msgid "Control-Flow Enforcement Technology detects and prevents certain methods for running malicious software on the device." msgstr "Control-Flow Enforcement Technology upptäcker och förhindrar vissa metoder för att köra skadlig programvara pÃ¥ enheten." #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology msgid "Control-flow Enforcement Technology" msgstr "Control-flow Enforcement Technology" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "Konvertera en fast programvarufil" #. TRANSLATORS: command description msgid "Create an EFI boot entry" msgstr "Skapa en EFI-uppstartspost" #. TRANSLATORS: when the update was built msgid "Created" msgstr "Skapad" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "Kritisk" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "Kryptografisk hashverifiering är tillgänglig" #. TRANSLATORS: current value of a BIOS setting msgid "Current Value" msgstr "Aktuellt värde" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "Aktuell version" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "ENHETS-ID|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Felsökningsalternativ" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "Dekomprimerar…" #. TRANSLATORS: command description msgid "Delete an EFI boot entry" msgstr "Ta bort en EFI-uppstartspost" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Beskrivning" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "Koppla frÃ¥n till starthanterarläge" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "Detaljer" #. TRANSLATORS: the best known configuration is a set of software that we know #. works #. * well together. In the OEM and ODM industries it is often called a BKC msgid "Deviate from the best known configuration?" msgstr "Avvik frÃ¥n den mest kända konfigurationen?" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "Enhetsflaggor" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "Enhets-ID" #. TRANSLATORS: description of the device requests msgid "Device Requests" msgstr "Enhetsbegäran" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Enhet tillagd:" #. TRANSLATORS: the device is already connected msgid "Device already exists" msgstr "Enheten finns redan" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "Enheten har för lite batteri" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse #, c-format msgid "Device battery power is too low (%u%%, requires %u%%)" msgstr "Enheten har för lite batteri (%u%%, behöver %u%%)" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "Enhet kan Ã¥terhämta sig frÃ¥n flashningsfel" #. TRANSLATORS: lid means "laptop top cover" msgid "Device cannot be updated while the lid is closed" msgstr "Enheten kan inte uppdateras medan locket är stängt." #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Enhet ändrad:" #. TRANSLATORS: a version check is required for all firmware msgid "Device firmware is required to have a version check" msgstr "Fast programvara för enhet krävs för att ha en versionskontroll" #. TRANSLATORS: emulated means we are pretending to be a different model msgid "Device is emulated" msgstr "Enheten är emulerad" #. TRANSLATORS: device cannot be interrupted, for instance taking a phone call msgid "Device is in use" msgstr "Enheten används" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "Enhet är lÃ¥st" #. TRANSLATORS: we have two ways of communicating with the device, so we hide #. one msgid "Device is lower priority than an equivalent device" msgstr "Enheten har lägre prioritet än en likvärdig enhet" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "Enhet krävs för att installera alla tillgängliga utgÃ¥vor" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "Enheten kan inte nÃ¥s" #. TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode msgid "Device is unreachable, or out of wireless range" msgstr "Enheten kan inte nÃ¥s, eller är utanför den trÃ¥dlösa räckvidden" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "Enhet gÃ¥r använda under tiden som uppdateringen pÃ¥gÃ¥r" #. TRANSLATORS: usually this is when we're waiting for a reboot msgid "Device is waiting for the update to be applied" msgstr "Enheten väntar pÃ¥ att uppdateringen ska tillämpas" #. TRANSLATORS: success, so say thank you to the user msgid "Device list uploaded successfully, thanks!" msgstr "Lyckades skicka enhetslista, tack!" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Enhet borttagen:" #. TRANSLATORS: as in, wired mains power for a laptop msgid "Device requires AC power to be connected" msgstr "Enheten behöver vara ansluten till nätspänning" #. TRANSLATORS: device does not have a display connected msgid "Device requires a display to be plugged in" msgstr "Enheten kräver att en skärm är inpluggad" #. TRANSLATORS: The device cannot be updated due to missing vendor's license." msgid "Device requires a software license to update" msgstr "Enheten kräver en programvarulicens för att uppdateras" #. TRANSLATORS: longer description msgid "Device software updates are provided for this device." msgstr "Enhetsprogramuppdateringar tillhandahÃ¥lls för denna enhet." #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "Enhet genomför uppdatering i steg" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "Enhet stöder byte till en annan gren av fast programvara" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "Enhetsuppdatering kräver aktivering" #. TRANSLATORS: save the old firmware to disk before installing the new one msgid "Device will backup firmware before installing" msgstr "Enheten kommer säkerhetskopiera fast programvara innan den installeras" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "Enhet kommer inte att dyka upp igen efter att uppdateringen färdigställs" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "Enheter som har uppdaterats:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "Enheter som inte uppdaterades korrekt:" #. TRANSLATORS: message letting the user there is an update #. * waiting, but there is a reason it cannot be deployed msgid "Devices with firmware updates that need user action: " msgstr "Enheter med fast programvaruuppdateringar som kräver användarÃ¥tgärd:" #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "Enheter utan tillgängliga uppdateringar för fast programvara: " #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "Enheter med den senaste tillgängliga uppdateringen för fast programvara:" #. TRANSLATORS: this is for the device tests msgid "Did not find any devices with matching GUIDs" msgstr "Hittade inga enheter med matchande GUID" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "Inaktiverad" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "Inaktiverar en given fjärrkälla" #. TRANSLATORS: command description msgid "Disables virtual testing devices" msgstr "Inaktiverar virtuella testenheter" #. TRANSLATORS: the OS the release was tested on msgid "Distribution" msgstr "Distribution" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "Kontrollera inte gammal metadata" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "Kontrollera inte ej rapporterad historik" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "Kontrollera inte om fjärrkällor för hämtning ska aktiveras" #. TRANSLATORS: command line option msgid "Do not check or prompt for reboot after update" msgstr "Varken kontrollera eller frÃ¥ga om omstart efter uppdatering" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "Inkludera inte loggdomänsprefix" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "Inkludera inte tidsstämpelprefix" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "Utför inte säkerhetskontroller för enheter" #. TRANSLATORS: command line option msgid "Do not prompt for devices" msgstr "FrÃ¥ga inte om enheter" #. TRANSLATORS: command line option msgid "Do not prompt to fix security issues" msgstr "FrÃ¥ga inte om att fixa säkerhetsproblem" #. TRANSLATORS: command line option msgid "Do not search the firmware when parsing" msgstr "Sök inte den fasta programvaran under tolkning" #. TRANSLATORS: warning message shown after update has been scheduled msgid "Do not turn off your computer or remove the AC adaptor while the update is in progress." msgstr "SlÃ¥ inte av din dator och ta inte bort nätadaptern medan uppdateringen pÃ¥gÃ¥r." #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "Skriv inte till historikdatabasen" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "FörstÃ¥r du konsekvenserna av att byta gren av fast programvara?" #. TRANSLATORS: ask the user if it's okay to convert, #. * "it" being the data contained in the EFI boot entry msgid "Do you want to convert it now?" msgstr "Vill du konvertera den nu?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "Vill du inaktivera den här funktionen för framtida uppdateringar?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "Vill du uppdatera den här fjärrkällan nu?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "Vill du skicka rapporter automatiskt för framtida uppdateringar?" #. TRANSLATORS: command line option msgid "Don't prompt for authentication (less details may be shown)" msgstr "FrÃ¥ga inte om autentisering (mindre detaljer kan visas)" #. TRANSLATORS: success msgid "Done!" msgstr "Klar!" #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "Nedgradera %s frÃ¥n %s till %s?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "Nedgradera fast programvara pÃ¥ en enhet" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "Nedgraderar %s…" #. TRANSLATORS: command description msgid "Download a file" msgstr "Hämta en fil" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Hämtar…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "Dumpa SMBIOS-data frÃ¥n en fil" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "Tid" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "EMULATION-FILE [ARCHIVE-FILE]" msgstr "EMULERINGSFIL [ARKIVFIL]" #. TRANSLATORS: longer description msgid "Each system should have tests to ensure firmware security." msgstr "Varje system bör ha test för att säkerställa den fasta programvarans säkerhet." #. TRANSLATORS: command description msgid "Emulate a device using a JSON manifest" msgstr "Emulera en enhet med ett JSON-manifest" #. TRANSLATORS: this device is not actually real msgid "Emulated" msgstr "Emulerad" #. TRANSLATORS: Title: if we are emulating a different host msgid "Emulated host" msgstr "Emulerad värd" msgid "Enable" msgstr "Aktivera" msgid "Enable emulation data collection" msgstr "Aktivera datasamling för emulering" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "Aktivera ny fjärrkälla?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "Aktivera denna fjärrkälla?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Aktiverad" #. TRANSLATORS: Plugin is active only if hardware is found msgid "Enabled if hardware matches" msgstr "Aktiverad om hÃ¥rdvaran matchar" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "Aktiverar en given fjärrkälla" #. TRANSLATORS: command description msgid "Enables virtual testing devices" msgstr "Aktiverar virtuella testenheter" #. TRANSLATORS: longer description msgid "Enabling firmware updates for the BIOS allows fixing security issues." msgstr "Aktivering av fasta programvaruuppdateringar för BIOS möjliggör fixande av säkerhetsproblem." msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "Att aktivera denna funktionalitet görs pÃ¥ egen risk, vilket betyder att du mÃ¥ste kontakta den ursprungliga tillverkaren av din utrustning om problem som orsakas av dessa uppdateringar. Endast problem med själva uppdateringsprocessen ska rapporteras pÃ¥ $OS_RELEASE:BUG_REPORT_URL$." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "Att aktivera denna fjärrkälla görs pÃ¥ egen risk." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Krypterad" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "Krypterat RAM" msgid "Encrypted RAM makes it impossible for information that is stored in device memory to be read if the memory chip is removed and accessed." msgstr "Krypterad RAM gör det omöjligt för information som är lagrad pÃ¥ enhetsminnet att läsas om minneschipet tas bort och koms Ã¥t." #. TRANSLATORS: the vendor is no longer supporting the device msgid "End of life" msgstr "Har nÃ¥tt slutet pÃ¥ sin livstid" #. TRANSLATORS: The BIOS setting can only be changed to fixed values msgid "Enumeration" msgstr "Uppräkning" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "Ta bort all historik för fast programvara" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Raderar…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "Avsluta efter en kort fördröjning" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "Avsluta efter att motorn har lästs in" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "Exportera en fast programvarufilstruktur till XML" #. TRANSLATORS: command description msgid "Export firmware history for manual upload" msgstr "Exportera historik för fast programvara för manuell sändning" #. TRANSLATORS: command description msgid "Extract a firmware blob to images" msgstr "Extrahera en fast programvaru-blob till avbilder" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "FIL" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "FIL [ENHETS-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "FILNAMN" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "FILNAMN CERTIFIKAT PRIVAT-NYCKEL" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "FILNAMN ENHETS-ID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME OFFSET DATA [FIRMWARE-TYPE]" msgstr "FILNAMN AVSTÃ…ND DATA [FAST-PROGRAMVARUTYP]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "FILNAMN [ENHETS-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "FILNAMN [FAST-PROGRAMVARUTYP]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" msgstr "FILNAMN-KÄL FILNAMN-MÃ…L [FAST-PROGRAMVARUTYP-KÄL] [FAST-PROGRAMVARUTYP-MÃ…L]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" msgstr "FILNAMN|KONTROLLSUMMA1[,KONTROLLSUMMA2][,KONTROLLSUMMA3]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "Misslyckades" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Misslyckades med att tillämpa uppdatering" #. TRANSLATORS: error message for Windows msgid "Failed to connect to Windows service, please ensure it's running." msgstr "Misslyckades med att ansluta till Windows-tjänst, säkerställ att den körs." #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "Misslyckades med att ansluta till demon" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "Misslyckades med att läsa in lokal dbx" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "Misslyckades med att läsa in system-dbx" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "Misslyckades med att lÃ¥sa" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Misslyckades med att tolka argument" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "Misslyckades med att tolka fil" #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #, c-format msgid "Failed to parse flags for %s" msgstr "Misslyckades med att tolka flaggor för %s" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "Misslyckades med att tolka lokal dbx" #. TRANSLATORS: a feature is something like "can show an image" msgid "Failed to set front-end features" msgstr "Misslyckades med att sätta funktioner för framände" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "Misslyckades med att validera ESP-innehÃ¥ll" #. TRANSLATORS: item is FALSE msgid "False" msgstr "Falskt" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Filnamn" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "Filnamnssignatur" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "Filnamnskälla" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Filnamn krävs" #. TRANSLATORS: command line option msgid "Filter with a set of device flags using a ~ prefix to exclude, e.g. 'internal,~needs-reboot'" msgstr "Filtrera via en uppsättning av enhetsflaggor genom att använda ett ~-prefix för att exkludera, t.ex. â€internal,~needs-rebootâ€" #. TRANSLATORS: command line option msgid "Filter with a set of release flags using a ~ prefix to exclude, e.g. 'trusted-release,~trusted-metadata'" msgstr "Filtrera med en uppsättning utgÃ¥veflaggor och prefixet ~ för att exkludera, t.ex. 'trusted-release,~trusted-metadata'" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware Attestation" msgstr "Attestering av fast programvara" #. TRANSLATORS: longer description msgid "Firmware Attestation checks device software using a reference copy, to make sure that it has not been changed." msgstr "Attestering av fast programvara kontrollerar enhetsprogramvara med en referenskopia för att se att den inte har ändrats." #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "BIOS-beskrivare för fast programvara" #. TRANSLATORS: longer description msgid "Firmware BIOS Descriptor protects device firmware memory from being tampered with." msgstr "BIOS-beskrivare för fast programvara skyddar enhetens minne för fast programvara frÃ¥n att mixtras med." #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "BIOS-region för fast programvara" #. TRANSLATORS: longer description msgid "Firmware BIOS Region protects device firmware memory from being tampered with." msgstr "BIOS-region för fast programvara skyddar enhetens minne för fast programvara frÃ¥n att mixtras med." #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "Fast programvara bas-URI" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "D-Bus-tjänst för uppdatering av fast programvara" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Uppdateringsdemon för fast programvara" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "Verifiering av uppdateraren av fast programvara" msgid "Firmware Updater Verification checks that software used for updating has not been tampered with." msgstr "Verifiering av uppdateraren av fast programvara kontrollerar att programvaran som används för uppdatering inte har mixtrats med." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "Uppdateringar för fast programvara" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "Fast programvaruverktyg" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "Skrivskydd för fast programvara" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "SkrivskyddslÃ¥s för fast programvara" #. TRANSLATORS: longer description msgid "Firmware Write Protection protects device firmware memory from being tampered with." msgstr "Skrivskydd för fast programvara skyddar enhetens minne för fast programvara frÃ¥n att mixtras med." #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "Attestering av fast programvara" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "Fast programvara är redan blockerad" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "Fast programvara är inte redan blockerad" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "Metadata för fast programvara har inte uppdaterats pÃ¥ %u dag och kan vara inaktuell." msgstr[1] "Metadata för fast programvara har inte uppdaterats pÃ¥ %u dagar och kan vara inaktuell." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "Uppdateringar för fast programvara" #. TRANSLATORS: user needs to run a command, %1 is 'fwupdmgr unlock' #, c-format msgid "Firmware updates disabled; run '%s' to enable" msgstr "Uppdateringar av fast programvara inaktiverade; kör â€%s†för att aktivera" #. TRANSLATORS: command description msgid "Fix a specific host security attribute" msgstr "Fixa ett specifikt säkerhetsattribut pÃ¥ värden" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fix reverted successfully" msgstr "Fixen Ã¥terställdes" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fixed successfully" msgstr "Fixades" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "Flaggor" #. TRANSLATORS: command line option msgid "Force the action by relaxing some runtime checks" msgstr "Tvinga Ã¥tgärden genom att lätta pÃ¥ nÃ¥gra kontroller vid körning" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Hittad" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "Full diskkryptering upptäckt" #. TRANSLATORS: we might ask the user the recovery key when next booting #. Windows msgid "Full disk encryption secrets may be invalidated when updating" msgstr "Krypteringshemligheter för hel disk kan göras ogiltiga vid uppdatering" #. TRANSLATORS: Title: if the part has been fused msgid "Fused Platform" msgstr "Plattform med säkring" #. TRANSLATORS: Title: if the part has been fused msgid "Fused platform" msgstr "Plattform med säkring" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" msgstr[1] "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "GUID|DEVICE-ID" msgstr "GUID|ENHETS-ID" msgid "Get BIOS settings" msgstr "ErhÃ¥ll BIOS-inställningar" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "Hämta alla enhetsflaggor som stöds av fwupd" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Hämta alla enheter som stödjer uppdateringar av fast programvara" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "ErhÃ¥ll alla aktiverade insticksmoduler som är registrerade i systemet" #. TRANSLATORS: command description msgid "Get all known version formats" msgstr "Hämta alla kända versionsformat" #. TRANSLATORS: command description msgid "Get device report metadata" msgstr "Hämta rapportmetadata för enhet" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Hämtar detaljer om en fast programvarufil" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "Ger de konfigurerade fjärrkällorna" #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "Hämtar värdens säkerhetsattribut" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "Hämtar listan över godkänd fast programvara" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "Hämtar listan över blockerad fast programvara" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Hämtar listan över uppdateringar för ansluten hÃ¥rdvara" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "ErhÃ¥ll utgÃ¥van för en enhet" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Hämtar resultaten frÃ¥n senaste uppdateringen" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "HWIDS-FIL" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "HÃ¥rdvara väntar pÃ¥ att bli utdragen/Ã¥terinsatt" #. TRANSLATORS: the release urgency msgid "High" msgstr "Hög" #. TRANSLATORS: title for host security events msgid "Host Security Events" msgstr "Säkerhetshändelser för värd" #. TRANSLATORS: error message for unsupported feature #, c-format msgid "Host Security ID (HSI) is not supported" msgstr "Värdsäkerhets-ID (HSI) stöds inte" #. TRANSLATORS: success, so say thank you to the user msgid "Host Security ID attributes uploaded successfully, thanks!" msgstr "ID-attribut för värdsäkerhet skickades, tack!" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "Säkerhets-ID för värd:" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX" msgstr "INDEX" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX KEY [VALUE]" msgstr "INDEX NYCKEL [VÄRDE]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX NAME TARGET [MOUNTPOINT]" msgstr "INDEX NAMN MÃ…L [MONTERINGSPUNKT]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX1,INDEX2" msgstr "INDEX1,INDEX2" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INHIBIT-ID" msgstr "HINDRANDE-ID" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "IOMMU-skydd" #. TRANSLATORS: longer description msgid "IOMMU Protection prevents connected devices from accessing unauthorized parts of system memory." msgstr "IOMMU-skydd förhindrar anslutna enheter frÃ¥n att komma Ã¥t ej auktoriserade delar av systemminnet." #. TRANSLATORS: HSI event title msgid "IOMMU device protection disabled" msgstr "IOMMU-enhetsskydd inaktiverat" #. TRANSLATORS: HSI event title msgid "IOMMU device protection enabled" msgstr "IOMMU-enhetsskydd aktiverat" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "Inaktiv…" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "Ignorera strikta SSL-kontroller vid hämtning av filer" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "Ignorera fel i kontrollsumman för fast programvara" #. TRANSLATORS: command line option msgid "Ignore firmware hardware mismatch failures" msgstr "Ignorera fel i matchning av hÃ¥rdvara för fast programvara" #. TRANSLATORS: command line option msgid "Ignore non-critical firmware requirements" msgstr "Ignorera icke-kritiska fast programvarukrav" #. TRANSLATORS: try to help msgid "Ignoring SSL strict checks, to do this automatically in the future export DISABLE_SSL_STRICT in your environment" msgstr "Ignorerar strikta SSL-kontroller, för att göra detta automatiskt i framtiden exportera DISABLE_SSL_STRICT i din miljö" #. TRANSLATORS: show the user a generic image that can be themed msgid "Image" msgstr "Avbild" #. TRANSLATORS: show the user a random image from the internet msgid "Image (custom)" msgstr "Avbild (anpassad)" #. TRANSLATORS: the inhibit ID is a short string like dbus-123456 #, c-format msgid "Inhibit ID is %s." msgstr "Hindrande-ID är %s." #. TRANSLATORS: command description msgid "Inhibit the system to prevent upgrades" msgstr "Hindra systemet sÃ¥ uppgraderingar förhindras" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "Installationstid" #. TRANSLATORS: command description msgid "Install a firmware file in cabinet format on this hardware" msgstr "Installera en fast programvarufil i kabinettformat pÃ¥ denna hÃ¥rdvara" #. TRANSLATORS: command description msgid "Install a raw firmware blob on a device" msgstr "Installera en rÃ¥ fast programvaru-blob pÃ¥ en enhet" #. TRANSLATORS: command description msgid "Install a specific firmware file on all devices that match" msgstr "Installera en specifik fast programvara pÃ¥ alla enheter som matchar" #. TRANSLATORS: command description msgid "Install a specific firmware on a device, all possible devices will also be installed once the CAB matches" msgstr "Installera specifik fast programvara pÃ¥ en enhet, alla möjliga enheter kommer ocksÃ¥ installeras när CAB-arkivet matchar" msgid "Install old version of signed system firmware" msgstr "Installera en gammal version av signerad fast programvara för systemet" msgid "Install old version of unsigned system firmware" msgstr "Installera en gammal version av osignerad fast programvara för systemet" msgid "Install signed device firmware" msgstr "Installera signerad fast programvara för enhet" msgid "Install signed system firmware" msgstr "Installera signerad fast programvara för systemet" msgid "Install unsigned device firmware" msgstr "Installera osignerad fast programvara för enhet" msgid "Install unsigned system firmware" msgstr "Installera osignerad fast programvara för systemet" #. TRANSLATORS: stay on one firmware version unless the new version is #. explicitly #. * specified msgid "Installing a specific release is explicitly required" msgstr "Det krävs att explicit installera en specifik utgÃ¥va" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "Installerar uppdatering för fast programvara…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "Installerar pÃ¥ %s…" #. TRANSLATORS: if it breaks, you get to keep both pieces msgid "Installing this update may also void any device warranty." msgstr "Installation av denna uppdatering kan ocksÃ¥ ogiltigförklara eventuell garanti för enheten." #. TRANSLATORS: The BIOS setting only accepts integers in a fixed range msgid "Integer" msgstr "Heltal" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM Protected" msgstr "Intel BootGuard ACM-skyddad" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "Intel BootGuard ACM-skyddad" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard Error Policy" msgstr "Intel BootGuards felpolicy" msgid "Intel BootGuard Error Policy ensures the device does not continue to start if its device software has been tampered with." msgstr "Intel BootGuards felpolicy säkerställer att enheten inte fortsätter starta om dess enhetsprogramvara har mixtrats med." #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard Fuse" msgstr "Intel BootGuard-säkring" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "Intel BootGuard OTP-säkring" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard Verified Boot" msgstr "Intel BootGuard verifierad uppstart" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Intel BootGuard-felpolicy" #. TRANSLATORS: longer description msgid "Intel BootGuard prevents unauthorized device software from operating when the device is started." msgstr "Intel BootGuard förhindrar ej auktoriserad enhetsprogramvara frÃ¥n att köras dÃ¥ enheten startas." #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Intel BootGuard verifierad uppstart" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS Mitigation" msgstr "Intel GDS-förmildrande" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS mitigation" msgstr "Intel GDS-förmildrande" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Manufacturing Mode" msgstr "Intel Management Engine-tillverkningsläge" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the "override" is #. enabled #. * with a jumper -- luckily it is probably not accessible to end users on #. consumer #. * boards msgid "Intel Management Engine Override" msgstr "Intel Management Engine-Ã¥sidosättning" #. TRANSLATORS: longer description msgid "Intel Management Engine Override disables checks for device software tampering." msgstr "Intel Management Engine-Ã¥sidosättning inaktiverar kontroller om enhetens programvara mixtrats med." #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Version" msgstr "Intel Management Engine-version" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "Intern enhet" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "Ogiltig" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "Ogiltiga argument" #. TRANSLATORS: error message msgid "Invalid arguments, expected GUID" msgstr "Ogiltiga argument, GUID förväntades" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX KEY [VALUE]" msgstr "Ogiltiga argument, INDEX NYCKEL [VÄRDE] förväntades" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX NAME TARGET [MOUNTPOINT]" msgstr "Ogiltiga argument, INDEX NAMN MÃ…L [MONTERINGSPUNKT] förväntades" #. TRANSLATOR: This is the error message for #. * incorrect parameter msgid "Invalid arguments, expected an AppStream ID" msgstr "Ogiltiga argument, ett AppStream-ID förväntades" #. TRANSLATORS: error message msgid "Invalid arguments, expected at least ARCHIVE FIRMWARE METAINFO" msgstr "Ogiltiga argument, förväntade Ã¥tminstone ARKIV FAST_PROGRAMVARA METAINFO" #. TRANSLATORS: error message msgid "Invalid arguments, expected base-16 integer" msgstr "Ogiltiga argument, heltal i bas 16 förväntades" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "Är nedgradering" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "Är i starthanterarläge" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "Är uppgradering" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "Problem" msgstr[1] "Problem" #. TRANSLATORS: HSI event title msgid "Kernel is no longer tainted" msgstr "Kärnan är ej längre befläckad" #. TRANSLATORS: HSI event title msgid "Kernel is tainted" msgstr "Kärnan är befläckad" #. TRANSLATORS: HSI event title msgid "Kernel lockdown disabled" msgstr "Lockdown för Linux-kärnan inaktiverad" #. TRANSLATORS: HSI event title msgid "Kernel lockdown enabled" msgstr "Lockdown för Linux-kärnan aktiverad" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "PLATS" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "Senast ändrad" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "Mindre än en minut kvarstÃ¥r" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "Licens" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "Lockdown för Linux-kärnan" #. TRANSLATORS: longer description msgid "Linux Kernel Lockdown mode prevents administrator (root) accounts from accessing and changing critical parts of system software." msgstr "Lockdown-läge för Linux-kärnan förhindrar administratörskonton (root) frÃ¥n att komma Ã¥t och ändra kritiska delar av systemprogramvara." msgid "Linux Kernel Swap temporarily saves information to disk as you work. If the information is not protected, it could be accessed by someone if they obtained the disk." msgstr "Linux-kärnans växlingsutrymme sparar tillfälligt information pÃ¥ disk under tiden du arbetar. Om informationen inte är skyddad skulle det vara möjligt för nÃ¥gon att komma Ã¥t den om de fick tag pÃ¥ disken." #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "Verifiering av Linux-kärnan" msgid "Linux Kernel Verification makes sure that critical system software has not been tampered with. Using device drivers which are not provided with the system can prevent this security feature from working correctly." msgstr "Verifiering av Linux-kärnan säkerställer att kritisk systemprogramvara inte har mixtrats med. Att använda enhetsdrivrutiner som inte tillhandahÃ¥lls med systemet kan förhindra denna säkerhetsfunktion att fungera som den ska." #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "Linux-växlingsutrymme" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Linux Vendor Firmware Service (stabil fast programvara)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Linux Vendor Firmware Service (fast programvara för testning)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Linux-kärna" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Lockdown för Linux-kärnan" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Linux-växlingsutrymme" #. TRANSLATORS: command description msgid "List EFI boot files" msgstr "Lista EFI-uppstartsfiler" #. TRANSLATORS: command description msgid "List EFI boot parameters" msgstr "Lista EFI-uppstartsparametrar" #. TRANSLATORS: command description msgid "List EFI variables with a specific GUID" msgstr "Lista EFI-variabler med ett specifikt GUID" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "Lista poster i dbx" #. TRANSLATORS: command description msgid "List the available firmware GTypes" msgstr "Lista tillgängliga GType-typer för fast programvara" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "Lista de tillgängliga typerna av fast programvara" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "Listar filer pÃ¥ ESP" #. TRANSLATORS: command description msgid "Load device emulation data" msgstr "Läs in enhetens emuleringsdata" #. TRANSLATORS: the plugin was created from a .so object, and was not built-in msgid "Loaded from an external module" msgstr "Inläst frÃ¥n en extern modul" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "Läser in…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "LÃ¥st" #. TRANSLATORS: the release urgency msgid "Low" msgstr "LÃ¥g" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refers #. * to the private/public key used to secure loading of firmware msgid "MEI Key Manifest" msgstr "MEI-nyckelmanifest" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refer #. * to the private/public key used to secure loading of firmware msgid "MEI key manifest" msgstr "MEI-nyckelmanifest" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "MEI-tillverkningsläge" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "MEI-Ã¥sidosättning" msgid "MEI version" msgstr "MEI-version" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "Aktivera manuellt specifika insticksmoduler" #. TRANSLATORS: longer description msgid "Manufacturing Mode is used when the device is manufactured and security features are not yet enabled." msgstr "Tillverkningsläge används när enheten tillverkas och säkerhetsfunktioner ännu inte aktiverats." #. TRANSLATORS: Longest valid string for BIOS setting msgid "Maximum length" msgstr "Största längd" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "Högsta värde" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "Medel" #. TRANSLATORS: ask the user to do a simple task which should be translated msgid "Message" msgstr "Meddelande" #. TRANSLATORS: ask the user a question, and it will not be translated msgid "Message (custom)" msgstr "Meddelande (anpassat)" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "Metadatasignatur" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "Metadata-URI" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "Metadata kan hämtas frÃ¥n Linux Vendor Firmware Service." #. TRANSLATORS: error message for a user who ran fwupdmgr #. * refresh recently -- %1 is '--force' #, c-format msgid "Metadata is up to date; use %s to refresh again." msgstr "Metadata är aktuellt; använd %s för att uppdatera igen." #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "Minimiversion" #. TRANSLATORS: Shortest valid string for BIOS setting msgid "Minimum length" msgstr "Minsta längd" #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "Lägsta värde" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "Modifierar ett konfigurationsvärde för demonen" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "Modifierar en given fjärrkälla" msgid "Modify a configured remote" msgstr "Modifiera en konfigurerad fjärrkälla" msgid "Modify daemon configuration" msgstr "Modifiera demonkonfiguration" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "Övervaka demonen för händelser" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "Monterar ESP" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "Kräver en omstart efter installation" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "Kräver omstart" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "Kräver en nedstängning efter installation" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "Ny version" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "Ingen Ã¥tgärd angiven!" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "Inga nedgraderingar för %s" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "Inget fast programvaru-ID hittat" #. TRANSLATORS: nothing found msgid "No firmware found" msgstr "Ingen fast programvara hittades" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Ingen uppdateringsbar hÃ¥rdvara upptäcktes" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "Inga utgÃ¥vor tillgängliga" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "Inga fjärrkällor är aktiverade för närvarande, sÃ¥ ingen metadata är tillgänglig." #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "Inga fjärrkällor tillgängliga" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "Inga enheter gÃ¥r att uppdatera" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "Inga uppdateringar tillgängliga" msgid "No updates available for remaining devices" msgstr "Inga uppdateringar tillgängliga för kvarvarande enheter" #. TRANSLATORS: error message #, c-format msgid "No volume matched %s" msgstr "Ingen volym matchade %s" #. TRANSLATORS: version cannot be installed due to policy msgid "Not approved" msgstr "Inte godkänd" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "Hittades inte" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "Stöds inte" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "OK" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "OK!" #. TRANSLATORS: the firmware old version msgid "Old version" msgstr "Gammal version" #. TRANSLATORS: command line option msgid "Only install onto emulated devices" msgstr "Installera endast till emulerade enheter" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "Visa endast enkelt PCR-värde" #. TRANSLATORS: command line option msgid "Only use peer-to-peer networking when downloading files" msgstr "Använd endast P2P-nätverk vid hämtning av filer" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "Endast versionsuppgraderingar är tillÃ¥tna" #. TRANSLATORS: command line option msgid "Output in JSON format (disables all interactive prompts)" msgstr "Utdata i JSON-format (inaktiverar alla interaktiva frÃ¥gor)" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "Ã…sidosätt standardsökväg för ESP" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Firmware" msgstr "Fast programvara frÃ¥n P2P" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Metadata" msgstr "P2P-metadata" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "SÖKVÄG" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "Tolka och visa detaljer om en fast programvarufil" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "Tolkar dbx-uppdatering…" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "Tolkar system-dbx…" #. TRANSLATORS: remote filename base msgid "Password" msgstr "Lösenord" #. TRANSLATORS: command description msgid "Patch a firmware blob at a known offset" msgstr "Patcha en fast programvaru-blob pÃ¥ ett känt avstÃ¥nd" msgid "Payload" msgstr "Nyttolast" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "Väntande" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "Utför operationen?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "Plattformsfelsökning" #. TRANSLATORS: longer description msgid "Platform Debugging allows device security features to be disabled. This should only be used by hardware manufacturers." msgstr "Plattformsfelsökning tillÃ¥ter att enhetens säkerhetsfunktioner inaktiveras. Detta bör endast göras av hÃ¥rdvarutillverkare." #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "Plattformsfelsökning" #. TRANSLATORS: 'recovery key' here refers to a code, rather than a physical #. metal thing msgid "Please ensure you have the volume recovery key before continuing." msgstr "Se till att du har volymÃ¥terställningsnyckeln innan du fortsätter." #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "Ange en siffra mellan 0 och %u: " #. TRANSLATORS: the user isn't reading the question -- %1 is 'Y' and %2 is #. * 'N' #, c-format msgid "Please enter either %s or %s: " msgstr "Ange antingen %s eller %s:" #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "Beroenden för insticksmodul saknas" #. TRANSLATORS: The plugin is only for testing msgid "Plugin is only for testing" msgstr "Insticksmodulen är endast för tester" #. TRANSLATORS: Possible values for a bios setting msgid "Possible Values" msgstr "Möjliga värden" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA Protection" msgstr "DMA-skydd före uppstart" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "DMA-skydd före uppstart" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is disabled" msgstr "DMA-skydd före uppstart är inaktiverat" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is enabled" msgstr "DMA-skydd före uppstart är aktiverat" #. TRANSLATORS: longer description msgid "Pre-boot DMA protection prevents devices from accessing system memory after being connected to the computer." msgstr "DMA-skydd före uppstart förhindrar enheter frÃ¥n att komma Ã¥t systemminne efter att de anslutits till datorn." #. TRANSLATORS: warning message msgid "Press unlock on the device to continue the update process." msgstr "Tryck lÃ¥s upp pÃ¥ enheten för att fortsätta uppdateringsprocessen." #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "FöregÃ¥ende version" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Prioritet" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "Problem" msgid "Proceed with upload?" msgstr "Fortsätt med att skicka upp?" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Processor Security Checks" msgstr "Processorsäkerhetskontroller" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "Processor rollback protection" msgstr "Processortillbakarullningsskydd" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "Proprietär" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "FJÄRR-ID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID KEY VALUE" msgstr "FJÄRR-ID NYCKEL VÄRDE" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "Skrivskyddad" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "Läs en fast programvaru-blob frÃ¥n en enhet" #. TRANSLATORS: command description msgid "Read a firmware from a device" msgstr "Läs om fast programvara frÃ¥n en enhet" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "Läser frÃ¥n %s…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Läser…" #. TRANSLATORS: Plugin is active and in use msgid "Ready" msgstr "Redo" #. TRANSLATORS: how often we should refresh the metadata msgid "Refresh Interval" msgstr "Uppdateringsintervall" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "Uppdatera metadata frÃ¥n fjärrserver" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "Ã…terinstallera %s till %s?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "Ã…terinstallera aktuell fast programvara pÃ¥ enheten" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "Installera om fast programvara pÃ¥ en enhet" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "UtgÃ¥vegren" #. TRANSLATORS: release attributes msgid "Release Flags" msgstr "UtgÃ¥veflaggor" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "UtgÃ¥va-ID" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "Fjärrkälla-ID" #. TRANSLATORS: command description msgid "Removes devices to watch for future emulation" msgstr "Tar bort enheter att bevaka för framtida emulering" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "Rapport-URI" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "Rapporterat till fjärrserver" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "Nödvändigt efivarfs-filsystem hittades inte" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "HÃ¥rdvara som krävs hittades inte" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "Kräver en starthanterare" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Kräver internetanslutning" msgid "Reset daemon configuration" msgstr "Ã…terställ demonkonfiguration" #. TRANSLATORS: sets something in the daemon configuration file msgid "Resets a daemon configuration section" msgstr "Ã…terställer en demons konfigurationsavsnitt" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Starta om nu?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "Starta om demonen för att göra sÃ¥ att ändringarna fÃ¥r effekt?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Startar om enhet…" #. TRANSLATORS: command description msgid "Retrieve BIOS settings. If no arguments are passed all settings are returned" msgstr "ErhÃ¥ll BIOS-inställningar. Om inga argument skickas med returneras alla inställningar" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "Returnera alla hÃ¥rdvaru-ID:n för maskinen" #. TRANSLATORS: ask the user to upload msgid "Review and upload report now?" msgstr "Granska och skicka rapport nu?" #. TRANSLATORS: longer description msgid "Rollback Protection prevents device software from being downgraded to an older version that has security problems." msgstr "Tillbakarullningsskydd förhindrar enhetsprogramvara frÃ¥n att nedgraderas till en äldre version som har säkerhetsproblem." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr get-upgrades` #, c-format msgid "Run `%s` for more information." msgstr "Kör â€%s†för mer information." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr sync` #, c-format msgid "Run `%s` to complete this action." msgstr "Kör â€%s†för att slutföra denna Ã¥tgärd." #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "Kör uppstädningssammansättningsrutin för insticksmodulen när install-blob används" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "Kör förberedelsesammansättningsrutin för insticksmodulen när install-blob används" #. TRANSLATORS: command description msgid "Run the post-reboot cleanup action" msgstr "Kör upprensningsÃ¥tgärden efter uppstart" #. TRANSLATORS: tell a user how to get information #, c-format msgid "Run without '%s' to see" msgstr "Kör utan â€%s†för att se" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "Kärnan som körs är för gammal" #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "Exekveringssuffix" #. TRANSLATORS: Software Bill of Materials link msgid "SBOM" msgstr "SBOM" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SECTION" msgstr "AVSNITT" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING VALUE" msgstr "INSTÄLLNING VÄRDE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING1 VALUE1 [SETTING2] [VALUE2]" msgstr "INSTÄLLNING1 VÄRDE1 [INSTÄLLNING2] [VÄRDE2]" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "SMAP" msgstr "SMAP" #. TRANSLATORS: Title: Whether firmware is locked down msgid "SMM locked down" msgstr "SMM lÃ¥st" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "SPI BIOS-beskrivare" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "SPI BIOS-region" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "SPI lÃ¥s" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "SPI replay protection" msgstr "SPI-Ã¥teruppspelningsskydd" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "SPI skriv" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "SPI write protection" msgstr "SPI-skrivskydd" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "UNDERSYSTEM DRIVRUTIN [ENHETS-ID|GUID]" #. TRANSLATORS: command description msgid "Save a file that allows generation of hardware IDs" msgstr "Spara en fil som möjliggör generering av hÃ¥rdvaru-ID:n" #. TRANSLATORS: command description msgid "Save device emulation data" msgstr "Spara enhetens emuleringsdata" #. TRANSLATORS: key for a offline report filename msgid "Saved report" msgstr "Sparad rapport" #. TRANSLATORS: Scalar increment for integer BIOS setting msgid "Scalar Increment" msgstr "Skalär ökning" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Schemalägger…" #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "Secure Boot inaktiverad" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "Secure Boot aktiverad" msgid "Security hardening for HSI" msgstr "Säkerhetsskärpning för HSI" #. TRANSLATORS: the %1 is a URL to a wiki page #, c-format msgid "See %s for more details." msgstr "Se %s för mer detaljer." #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "Se %s för mer information." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Vald enhet" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Vald volym" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "Serienummer" #. TRANSLATORS: Configured a BIOS setting to a value #, c-format msgid "Set BIOS setting '%s' using '%s'." msgstr "Ställde in BIOS-inställningen â€%s†till â€%sâ€." #. TRANSLATORS: command description msgid "Set a BIOS setting" msgstr "Ställ in en BIOS-inställning" msgid "Set one or more BIOS settings" msgstr "Ställ in en eller flera BIOS-inställningar" #. TRANSLATORS: command description msgid "Set or remove an EFI boot hive entry" msgstr "Ställ in eller ta bort en EFI-uppstartskupepost" #. TRANSLATORS: command description msgid "Set the EFI boot next" msgstr "Ställ in EFI-uppstarten härnäst" #. TRANSLATORS: command description msgid "Set the EFI boot order" msgstr "Ställ in EFI-uppstartsordningen" #. TRANSLATORS: command line option msgid "Set the download retries for transient errors" msgstr "Ställ in antalet försök att hämta igen för tillfälliga fel" #. TRANSLATORS: command description msgid "Sets one or more BIOS settings" msgstr "Ställer in en eller flera BIOS-inställningar" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Sätter listan av godkänd fast programvara" #. TRANSLATORS: type of BIOS setting msgid "Setting type" msgstr "Inställningstyp" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "Inställningar kommer börja gälla efter systemet startats om" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "Dela historik för fast programvara med utvecklarna" #. TRANSLATORS: command line option msgid "Show all results" msgstr "Visa alla resultat" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "Visa klient- och demon-version" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "Visa utförlig information frÃ¥n demonen för en specifik domän" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "Visa felsökningsinformation för alla domäner" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Visa felsökningsalternativ" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Visa enheter som inte kan uppdateras" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Visa extra felsökningsinformation" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "Visa historik över uppdateringar för fast programvara" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "Visa den beräknade versionen av dbx" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "Stäng ner nu?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "Signera en fast programvara med en ny nyckel" msgid "Sign data using the client certificate" msgstr "Signera data med klientcertifikatet" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "Signera data med klientcertifikatet" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "Signera skickade data med klientcertifikatet" msgid "Signature" msgstr "Signatur" #. TRANSLATORS: firmware is verified on-device the payload using strong crypto msgid "Signed Payload" msgstr "Signerad nyttolast" #. TRANSLATORS: file size of the download msgid "Size" msgstr "Storlek" #. TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM msgid "Some of the platform secrets may be invalidated when updating this firmware." msgstr "NÃ¥gra av plattformshemligheterna kan göras ogiltiga när denna fasta programvara uppdateras." #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "Källa" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "Ange dbx-databasfilen" msgid "Stop the fwupd service" msgstr "Stoppa fwupd-tjänsten" #. TRANSLATORS: The BIOS setting accepts strings msgid "String" msgstr "Sträng" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "Slutfördes" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "Aktiverade framgÃ¥ngsrikt alla enheter" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "Inaktiverade framgÃ¥ngsrikt fjärrkälla" #. TRANSLATORS: comment explaining result of command msgid "Successfully disabled test devices" msgstr "Lyckades inaktivera testenheter" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "Hämtade framgÃ¥ngsrikt ny metadata: " #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "Aktiverade och uppdaterade framgÃ¥ngsrikt fjärrkälla" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "Aktiverade framgÃ¥ngsrikt fjärrkälla" #. TRANSLATORS: comment explaining result of command msgid "Successfully enabled test devices" msgstr "Lyckades aktivera testenheter" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "Installerade framgÃ¥ngsrikt fast programvara" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "Modifierade framgÃ¥ngsrikt konfigurationsvärde" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "Modifierade framgÃ¥ngsrikt fjärrkälla" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "Uppdaterade framgÃ¥ngsrikt metadata manuellt" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration section" msgstr "Lyckades Ã¥terställa konfigurationsavsnitt" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration values" msgstr "Lyckades Ã¥terställa konfigurationsvärden" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "Uppdaterade framgÃ¥ngsrikt enhetskontrollsummor" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "Skickade framgÃ¥ngsrikt %u rapport" msgstr[1] "Skickade framgÃ¥ngsrikt %u rapporter" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "Verifierade framgÃ¥ngsrikt enhetskontrollsummor" #. TRANSLATORS: the device showed up in time #, c-format msgid "Successfully waited %.0fms for device" msgstr "Väntade %.0fms pÃ¥ enhet" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Sammanfattning" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "Supervisor Mode Access Prevention" msgstr "Supervisor Mode Access Prevention" #. TRANSLATORS: longer description msgid "Supervisor Mode Access Prevention ensures critical parts of device memory are not accessed by less secure programs." msgstr "Supervisor Mode Access Prevention säkerställer att kritiska delar av enhetsminne inte koms Ã¥t av mindre säkra program." #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "Stöds" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "CPU som stöds" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "Stöds pÃ¥ fjärrserver" #. TRANSLATORS: Title: a better sleep state msgid "Suspend To Idle" msgstr "GÃ¥ till inaktivt läge" #. TRANSLATORS: Title: sleep state msgid "Suspend To RAM" msgstr "GÃ¥ till vänteläge i RAM" #. TRANSLATORS: longer description msgid "Suspend to Idle allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "GÃ¥ till inaktivt läge lÃ¥ter enheten snabbt sova för att spara ström. När enheten är i vänteläge skulle dess minne fysiskt kunna plockas bort och dess information kommas Ã¥t." #. TRANSLATORS: longer description msgid "Suspend to RAM allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "GÃ¥ till vänteläge i RAM lÃ¥ter enheten snabbt sova för att spara ström. När enheten är i vänteläge skulle dess minne fysiskt kunna plockas bort och dess information kommas Ã¥t." #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "GÃ¥-till-inaktivt-läge" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "GÃ¥-till-vänteläge-i-ram" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "Byt gren frÃ¥n %s till %s?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "Byt gren för fast programvara pÃ¥ enheten" #. TRANSLATORS: command description msgid "Sync firmware versions to the chosen configuration" msgstr "Synkronisera versioner för fast programvara till den valda konfigurationen" #. TRANSLATORS: Title: Whether firmware is locked down msgid "System Management Mode" msgstr "Systemhanteringsläge" #. TRANSLATORS: this CLI tool is now preventing system updates msgid "System Update Inhibited" msgstr "Systemuppdatering hindrad." #. TRANSLATORS: longer description msgid "System management mode is used by the firmware to access resident BIOS code and data." msgstr "Systemhanteringsläge används av den fasta programvaran för att komma Ã¥t inbyggd BIOS-kod och data." #. TRANSLATORS: as in laptop battery power msgid "System power is too low" msgstr "Systemet har för lite batteri" #. TRANSLATORS: as in laptop battery power #, c-format msgid "System power is too low (%u%%, requires %u%%)" msgstr "Systemet har för lite batteri (%u%%, kräver %u%%)" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "Systemet kräver extern strömkälla" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "TEXT" #. TRANSLATORS: longer description msgid "TPM (Trusted Platform Module) is a computer chip that detects when hardware components have been tampered with." msgstr "TPM (Trusted Platform Module) är ett datorchip som upptäcker när hÃ¥rdvarukomponenter har mixtrats med." #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "TPM PCR0-rekonstruktion" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is invalid" msgstr "TPM PCR0-rekonstruktion är ogiltig" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is now valid" msgstr "TPM PCR0-rekonstruktion är nu giltig" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM Platform Configuration" msgstr "TPM-plattformskonfiguration" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM Reconstruction" msgstr "TPM-rekonstruktion" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM empty PCRs" msgstr "TPM har tomma PCR" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "Tag" msgstr[1] "Taggar" #. TRANSLATORS: we're saving all USB events for emulation msgid "Tagged for emulation" msgstr "Taggad för emulering" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "Befläckad" #. show the user the entire data blob msgid "Target" msgstr "MÃ¥l" #. TRANSLATORS: command description msgid "Test a device using a JSON manifest" msgstr "Testa en enhet med ett JSON-manifest" #. TRANSLATORS: when the release was tested msgid "Tested" msgstr "Testad" #. TRANSLATORS: the %s is a vendor name, e.g. Lenovo #, c-format msgid "Tested by %s" msgstr "Testad av %s" #. TRANSLATORS: someone we trust has tested this msgid "Tested by trusted vendor" msgstr "Testad av en betrodd leverantör" #. TRANSLATORS: the boot entry was in a legacy format msgid "The EFI boot entry is not in hive format, and shim may not be new enough to read it." msgstr "EFI-uppstartsposten är inte i kupformat, och mellanlägg kanske inte är nytt nog för att läsa den." #. TRANSLATORS: try to treat the legacy format as a hive msgid "The EFI boot entry was not in hive format, falling back" msgstr "EFI-uppstartsposten var inte i kupformat, använder reserv" #. TRANSLATORS: longer description msgid "The Intel Management Engine Key Manifest must be valid so that the device firmware can be trusted by the CPU." msgstr "Nyckelmanifestet för Intel Management Engine mÃ¥ste vara giltigt sÃ¥ att enhetens fasta programvara kan vara betrodd av processorn." #. TRANSLATORS: longer description msgid "The Intel Management Engine controls device components and needs to have a recent version to avoid security issues." msgstr "Intel Management Engine kontrollerar enhetskomponenter och behöver ha en tillräckligt ny version för att undvika säkerhetsproblem." #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "LVFS är en fri tjänst som fungerar som en oberoende juridisk person och har ingen koppling till $OS_RELEASE:NAME$. Din distributör kanske inte har bekräftat att nÃ¥gon av uppdateringarna för fast programvara är kompatibla med ditt system eller anslutna enheter. All fast programvara tillhandahÃ¥lls endast av den ursprungliga tillverkaren av utrustning." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Platform Configuration is used to check whether the device start process has been modified." msgstr "Plattformskonfigurationen för TPM (Trusted Platform Module) används för att kontrollera om enhetens startprocess har ändrats." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Reconstruction is used to check whether the device start process has been modified." msgstr "Rekonstruktionen för TPM (Trusted Platform Module) används för att kontrollera om enhetens startprocess har ändrats." #. TRANSLATORS: this is more background on a security measurement problem msgid "The TPM PCR0 differs from reconstruction." msgstr "TPM PCR0 skiljer sig frÃ¥n rekonstruktion." #. TRANSLATORS: longer description msgid "The UEFI Platform Key is used to determine if device software comes from a trusted source." msgstr "UEFI-plattformsnyckeln används för att avgöra om enhetsprogramvara kommer frÃ¥n en betrodd källa." #. TRANSLATORS: HSI event title msgid "The UEFI certificate store is now up to date" msgstr "UEFI-certifikatlagringen är nu uppdaterad" #. TRANSLATORS: longer description msgid "The UEFI db contains the list of valid certificates that can be used to authorize what EFI binaries are allowed to run." msgstr "UEFI-databasen innehÃ¥ller listan med giltiga certifikat som kan användas för att auktorisera vilka EFI-binärer som tillÃ¥ts köra." #. TRANSLATORS: longer description msgid "The UEFI system can set up memory attributes at boot which prevent common exploits from running." msgstr "UEFI-systemet kan konfigurera minnesattribut vid uppstart som förhindrar vanliga attackmetoder frÃ¥n att köras." #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "Demonen har läst in tredjepartskod och stöds inte längre av uppströmsutvecklarna!" #. TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name #, c-format msgid "The firmware from %s is not supplied by %s, the hardware vendor." msgstr "Fast programvara frÃ¥n %s levereras inte av %s, hÃ¥rdvarutillverkaren." #. TRANSLATORS: try to help msgid "The system clock has not been set correctly and downloading files may fail." msgstr "Systemklockan har inte ställts in korrekt och hämtning av filer kan misslyckas." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been re-inserted." msgstr "Uppdateringen kommer fortsätta när enhetens USB-kabel har stoppats in igen." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged and then re-inserted." msgstr "Uppdateringen kommer fortsätta när enhetens USB-kabel har kopplats frÃ¥n och stoppats in igen." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged." msgstr "Uppdateringen kommer fortsätta när enhetens USB-kabel har kopplats frÃ¥n." #. TRANSLATORS: warning message msgid "The update will continue when the device power cable has been removed and re-inserted." msgstr "Uppdateringen kommer fortsätta när enhetens strömkabel har dragits ut och stoppats in igen." #. TRANSLATORS: naughty vendor msgid "The vendor did not supply any release notes." msgstr "Tillverkaren tillhandahöll inte nÃ¥gra kommentarer till utgÃ¥van." #. TRANSLATORS: now list devices with unfixed high-priority issues msgid "There are devices with issues:" msgstr "Det finns enheter med problem:" #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "Det finns inga blockerade fast programvarufiler" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "Det finns ingen godkänd fast programvara." #. TRANSLATORS: %1 is the current device version number, and %2 is the #. command name, e.g. `fwupdmgr sync` #, c-format msgid "This device will be reverted back to %s when the %s command is performed." msgstr "Denna enhet kommer att Ã¥terställas till %s när kommandot %s körs." #. TRANSLATORS: the vendor did not upload this msgid "This firmware is provided by LVFS community members and is not provided (or supported) by the original hardware vendor." msgstr "Denna fasta programvara tillhandahÃ¥lls av medlemmar av LVFS-gemenskapen och varken tillhandahÃ¥lls (eller stöds) av den ursprungliga hÃ¥rdvarutillverkaren." #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "Detta paket har inte validerats, det kanske inte fungerar korrekt." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "Detta program kommer endast fungera korrekt som root" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "Denna fjärrkälla innehÃ¥ller fast programvara som inte är under embargo, men fortfarande testas av hÃ¥rdvarutillverkare. Du bör säkerställa att du har ett sätt att manuellt nedgradera den fasta programvaran om uppdateringen misslyckas." #. TRANSLATORS: error message msgid "This system doesn't support firmware settings" msgstr "Detta system stöder inte inställningar för fast programvara" #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "Detta system har problem med HSI-exekvering." #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "Detta system har en lÃ¥g HSI-säkerhetsnivÃ¥." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "Detta verktyg gör det möjligt för en administratör att tillämpa UEFI dbx-uppdateringar." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "Detta verktyg lÃ¥ter en administratör ställa frÃ¥gor till och styra fwupd-demonen, vilket lÃ¥ter dem utföra Ã¥tgärder som att installera eller nedgradera fast programvara." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "Detta verktyg tillÃ¥ter en administratör att använda fwupd-insticksmoduler utan att dessa är installerade pÃ¥ värdsystemet." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can add a kernel argument of '%s', but it will only be active after restarting the computer." msgstr "Detta verktyg kan lägga till ett kärnargument â€%sâ€, men det kommer aktiveras först efter att datorn startats om." #. TRANSLATORS: the %1 is a BIOS setting name. #. * %2 and %3 are the values, e.g. "True" or "Windows10" #, c-format msgid "This tool can change the BIOS setting '%s' from '%s' to '%s' automatically, but it will only be active after restarting the computer." msgstr "Detta verktyg kan automatiskt ändra BIOS-inställningen â€%s†frÃ¥n â€%s†till â€%sâ€, men den kommer aktiveras först efter att datorn startats om." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can change the kernel argument from '%s' to '%s', but it will only be active after restarting the computer." msgstr "Detta verktyg kan ändra kärnargumentet frÃ¥n â€%s†till â€%sâ€, men det kommer aktiveras först efter att datorn startats om." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "Detta verktyg läser och analyserar TPM-händelseloggen frÃ¥n systemets fasta programvara." #. TRANSLATORS: the update state of the specific device msgid "Transient failure" msgstr "Tillfälligt misslyckande" #. TRANSLATORS: item is TRUE msgid "True" msgstr "Sant" #. TRANSLATORS: We verified the metadata against the server msgid "Trusted metadata" msgstr "Betrodda metadata" #. TRANSLATORS: We verified the payload against the server msgid "Trusted payload" msgstr "Betrodd nyttolast" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Typ" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI Bootservice Variables" msgstr "UEFI-uppstartstjänstvariabler" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition may not be set up correctly" msgstr "UEFI ESP-partition kanske inte konfigurerats korrekt" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "UEFI ESP-partition upptäcktes inte eller är inte konfigurerad" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI Memory Protection" msgstr "UEFI-minnesskydd" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "UEFI-plattformsnyckel" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "UEFI säkerhetsstart" #. TRANSLATORS: longer description msgid "UEFI Secure Boot prevents malicious software from being loaded when the device starts." msgstr "UEFI säkerhetsstart förhindrar skadlig programvara frÃ¥n att läsas in när enheten startas." #. TRANSLATORS: longer description msgid "UEFI boot service variables should not be readable from runtime mode." msgstr "UEFI-uppstartstjänstvariabler fÃ¥r inte vara läsbara frÃ¥n körtidsläge." #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI bootservice variables" msgstr "UEFI-uppstartstjänstvariabler" #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "UEFI-kapseluppdateringar är inte tillgängliga eller aktiverade i konfiguration för fast programvara" #. TRANSLATORS: Title: is UEFI db up-to-date msgid "UEFI db" msgstr "UEFI-db" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "UEFI dbx-verktyg" #. TRANSLATORS: system is not booted in UEFI mode msgid "UEFI firmware can not be updated in legacy BIOS mode" msgstr "Fast UEFI-programvara kan inte uppdateras i äldre BIOS-läge" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI memory protection" msgstr "UEFI-minnesskydd" #. TRANSLATORS: HSI event title msgid "UEFI memory protection enabled and locked" msgstr "UEFI-minnesskydd aktiverat och lÃ¥st" #. TRANSLATORS: HSI event title msgid "UEFI memory protection enabled but not locked" msgstr "UEFI-minnesskydd aktiverat men inte lÃ¥st" #. TRANSLATORS: HSI event title msgid "UEFI memory protection is now locked" msgstr "UEFI-minnesskydd är nu lÃ¥st" #. TRANSLATORS: HSI event title msgid "UEFI memory protection is now unlocked" msgstr "UEFI-minnesskydd är nu olÃ¥st" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "UEFI-plattformsnyckel" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "UEFI säkerhetsstart" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "Kunde inte ansluta till tjänst" #. TRANSLATORS: error message msgid "Unable to find attribute" msgstr "Kunde inte hitta attribut" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "Lösgör aktuell drivrutin" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "Avblockera fast programvara:" #. TRANSLATORS: command description msgid "Unblocks a specific firmware from being installed" msgstr "Avblockerar en specifik fast programvara frÃ¥n att installeras" #. TRANSLATORS: command description msgid "Undo the host security attribute fix" msgstr "Ã…ngra säkerhetsattributfixen pÃ¥ värden" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "Okrypterad" #. TRANSLATORS: command description msgid "Uninhibit the system to allow upgrades" msgstr "Släpp systemets hindrande sÃ¥ uppgraderingar tillÃ¥ts" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Okänd" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Okänd enhet" msgid "Unlock the device to allow access" msgstr "LÃ¥s upp enheten för att tillÃ¥ta Ã¥tkomst" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "UpplÃ¥st" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "LÃ¥ser upp enheten för fast programvaruÃ¥tkomst" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "Avmonterar ESP" #. TRANSLATORS: message shown after device has been marked for emulation msgid "Unplug and replug the device to continue the update process." msgstr "Koppla ur och koppla in enheten igen för att fortsätta uppdateringsprocessen." #. TRANSLATORS: firmware payload is unsigned and it is possible to modify it msgid "Unsigned Payload" msgstr "Osignerad nyttolast" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "Demonversion %s stöds inte, klientversionen är %s" #. TRANSLATORS: Suffix: the HSI result msgid "Untainted" msgstr "Obefläckad" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "Uppdateringsbar" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Uppdateringsfel" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "Uppdatera avbild" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "Uppdateringsmeddelande" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "UppdateringstillstÃ¥nd" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "Misslyckad uppdatering är ett känt fel, besök denna URL för mer information:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Uppdatera nu?" #. TRANSLATORS: command description msgid "Update the stored cryptographic hash with current ROM contents" msgstr "Uppdatera den lagrade kryptografiska hashen med aktuellt ROM-innehÃ¥ll" msgid "Update the stored device verification information" msgstr "Uppdatera den lagrade enhetens verifikationsinformation" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "Uppdatera lagrad metadata med aktuellt innehÃ¥ll" #. TRANSLATORS: command description msgid "Updates all specified devices to latest firmware version, or all devices if unspecified" msgstr "Uppdaterar alla angivna enheter till senaste version av fast programvara, eller alla enheter om ej angivet" #. TRANSLATORS: how many local devices can expect updates now #, c-format msgid "Updates have been published for %u local device" msgid_plural "Updates have been published for %u of %u local devices" msgstr[0] "Uppdateringar har publicerats för %u lokal enhet" msgstr[1] "Uppdateringar har publicerats för %u av %u lokala enheter" msgid "Updating" msgstr "Uppdaterar" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "Uppdaterar %s…" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "Uppgradera %s frÃ¥n %s till %s?" #. TRANSLATORS: ask the user to upload msgid "Upload data now?" msgstr "Skicka data nu?" #. TRANSLATORS: command description msgid "Upload the list of updatable devices to a remote server" msgstr "Skicka listan över uppdateringsbara enheter till en fjärrserver" #. TRANSLATORS: ask the user to share, %s is something #. * like: "Linux Vendor Firmware Service" #, c-format msgid "Upload these anonymous results to the %s to help other users?" msgstr "Skicka dessa anonyma resultat till %s för att hjälpa andra användare?" #. TRANSLATORS: explain why we want to upload #, c-format msgid "Uploading a device list allows the %s team to know what hardware exists, and allows us to put pressure on vendors that do not upload firmware updates for their hardware." msgstr "Att skicka en enhetslista lÃ¥ter %s-gruppen veta vilken hÃ¥rdvara som finns, och lÃ¥ter oss pressa tillverkar som inte skickar fast programvaruuppdateringar för sin hÃ¥rdvara." #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "Att skicka upp rapporter för fast programvara hjälper hÃ¥rdvarutillverkare att snabbt identifiera trasiga och fungerande uppdateringar pÃ¥ riktiga enheter." #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "Viktighet" #. TRANSLATORS: explain how to get help, %1 is #. * 'fwupdtool --help' #. TRANSLATORS: explain how to get help, #. * where $1 is something like 'fwupdmgr --help' #, c-format msgid "Use %s for help" msgstr "Använd %s för hjälp" #. TRANSLATORS: CTRL^C [holding control, and then pressing C] will exit the #. program msgid "Use CTRL^C to cancel." msgstr "Använd CTRL^C för att avbryta." #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "Användare har aviserats" #. TRANSLATORS: remote filename base msgid "Username" msgstr "Användarnamn" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "VERSION1 VERSION2 [FORMAT]" msgstr "VERSION1 VERSION2 [FORMAT]" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "Giltig" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "Validerar ESP-innehÃ¥ll…" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "Variant" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "Tillverkare" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "Verifierar…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Version" #. TRANSLATORS: the fwupd version the release was tested on msgid "Version[fwupd]" msgstr "Version[fwupd]" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "VARNING" #. TRANSLATORS: command description msgid "Wait for a device to appear" msgstr "Vänta pÃ¥ att en enhet ska dyka upp" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "Väntar…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "Övervaka hÃ¥rdvaruändringar" #. TRANSLATORS: check various UEFI and ACPI tables are unchanged after the #. update msgid "Will measure elements of system integrity around an update" msgstr "Kommer mäta element av systemintegritet vid en uppdatering" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "Skriver fil:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Skriver…" #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from a recovery or installation disk, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "Du bör säkerställa att du är bekväm med att Ã¥terställa inställningen frÃ¥n en Ã¥terställnings- eller installationsdisk, dÃ¥ denna ändring kan fÃ¥ systemet att inte kunna starta Linux eller orsaka annan systeminstabilitet." #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from the system firmware setup, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "Du bör säkerställa att du är bekväm med att Ã¥terställa inställningen frÃ¥n systemets konfiguration för fast programvara, dÃ¥ denna ändring kan fÃ¥ systemet att inte kunna starta Linux eller orsaka annan systeminstabilitet." #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "Din distributör kanske inte har verifierat nÃ¥gon av uppdateringarna av fast programvara för kompatibilitet med ditt system eller anslutna enheter." #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "Din hÃ¥rdvara kan skadas med den här fasta programvaran och att installera denna utgÃ¥va kan ogiltigförklara all garanti hos %s." #. TRANSLATORS: BKC is the industry name for the best known configuration and #. is a set #. * of firmware that works together #, c-format msgid "Your system is set up to the BKC of %s." msgstr "Ditt system är inställt till BKC för %s." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[APPSTREAM_ID]" msgstr "[APPSTREAM-ID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[KONTROLLSUMMA]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[ENHETS-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[ENHETS-ID|GUID] [GREN]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [VERSION]" msgstr "[ENHETS-ID|GUID] [VERSION]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE]" msgstr "[ENHET]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILE FILE_SIG REMOTE-ID]" msgstr "[FIL FILSIG FJÄRR-ID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[FILNAMN1] [FILNAMN2]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FWUPD-VERSION]" msgstr "[FWUPD-VERSION]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[REASON] [TIMEOUT]" msgstr "[ORSAK][TIDSGRÄNS-UPPNÃ…DD]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SECTION] KEY VALUE" msgstr "[AVSNITT] NYCKEL VÄRDE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2] [--no-authenticate]" msgstr "[INSTÄLLNING1] [INSTÄLLNING2] [--no-authenticate]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2]..." msgstr "[INSTÄLLNING1] [INSTÄLLNING2]…" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[SMBIOS-FIL|HWIDS-FIL]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "standard" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "fwupd TPM-händelseloggverktyg" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "fwupd-insticksmoduler" fwupd-2.0.10/po/test-deps000077500000000000000000000030221501337203100151630ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: LGPL-2.1-or-later """ Check dependencies needed for rasterization """ import sys import os err = [] try: import gi except ImportError: err.append("missing dependency python gobject introspection (python3-gi)") try: gi.require_version("Pango", "1.0") from gi.repository import Pango except NameError: pass except ValueError: err.append("missing pango gobject introspection library") try: gi.require_version("PangoCairo", "1.0") from gi.repository import PangoCairo except NameError: pass except ValueError: err.append("missing pangocairo gobject introspection library") try: gi.require_version("cairo", "1.0") from gi.repository import cairo except NameError: pass except ValueError: err.append("missing cairo gobject introspection library") try: import cairo except NameError: pass except ImportError: err.append("missing dependency python cairo (python3-cairo)") # check that LINUGAS lists every language with a .po file try: linguas_fn = sys.argv[1] except IndexError: linguas_fn = open("po/LINGUAS") with open(linguas_fn) as f: langs = f.read().splitlines() for root, dirs, files in os.walk("po"): for file in files: if not file.endswith(".po"): continue l = file.split(".po") if len(l) > 1 and not l[0] in langs: err = 1 err.append("missing translations for %s" % l[0]) for msg in err: print(f"Error: {msg}", file=sys.stderr) sys.exit(len(err)) fwupd-2.0.10/po/tr.po000066400000000000000000001554701501337203100143310ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Emin Tufan Çetin , 2020 # Emir SARI, 2024 # Muhammet Kara , 2016 # b83946de5835331df42b9ffcc43e6a33_05e65cd <73a30e0a984b2291d4915f37112ad292_814039>, 2019-2020,2022-2023 # Sabri Ünal , 2020 # Serdar SaÄŸlam , 2020 # yunus kaba , 2021 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Turkish (http://app.transifex.com/freedesktop/fwupd/language/tr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: tr\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "%.0f dakika kaldı" msgstr[1] "%.0f dakika kaldı" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "%s Tüketici ME Güncellemesi" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "%s Denetleyici Güncellemesi" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "%s Kurumsal ME Güncellemesi" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "%s Aygıt Güncellemesi" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "%s Gömülü Denetleyici Güncellemesi" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "%s ME Güncellemesi" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "%s Sistem Güncellemesi" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "%s Thunderbolt Denetleyici Güncellemesi" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "%s Güncellemesi" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "%s fabrika kipi" #. TRANSLATORS: warn the user before updating, %1 is a machine #. * name #, c-format msgid "%s must remain plugged into a power source for the duration of the update to avoid damage." msgstr "%s, hasar oluÅŸmaması için güncelleme süresince güç kaynağına baÄŸlı kalmalıdır." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "%s geçersiz kılması" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "%s sürümü" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u gün" msgstr[1] "%u gün" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "%u aygıtın donanım yazılımı yükseltmesi var." msgstr[1] "%u aygıtın donanım yazılımı yükseltmesi var." #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device is not the best known configuration." msgid_plural "%u devices are not the best known configuration." msgstr[0] "%u aygıtı bilinen en iyi yapılandırma deÄŸildir" msgstr[1] "%u aygıtları bilinen en iyi yapılandırma deÄŸildir" #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u saat" msgstr[1] "%u saat" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u dakika" msgstr[1] "%u dakika" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u saniye" msgstr[1] "%u saniye" #. TRANSLATORS: command description msgid "Activate devices" msgstr "Etkin aygıtlar" msgid "Activate the new firmware on the device" msgstr "Aygıttaki yeni donanım yazılımını etkinleÅŸtir" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "Donanım yazılımı güncellemesini etkinleÅŸtir" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "YaÅŸ" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "%s arması" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Donanım yazılımı sürümünün düşürülmesine izin ver" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "Varolan donanım yazılımı sürümlerinin yeniden kurulumuna izin ver" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "DiÄŸer kollara geçiÅŸ için izin verin" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "Güncelleme iÅŸlemi için yeniden baÅŸlatma gerekir." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "Güncelleme iÅŸlemi için sistemin kapanması gerekir." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Tüm sorulara evet yanıtı ver" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "Belirtilmese bile güncellemeyi uygula" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "Güncelleme dosyalarını uygula" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "Güncelleme uygulanıyor…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Onaylı ürün yazılımı:" msgstr[1] "Onaylı donanım yazılımı:" #. TRANSLATORS: command description msgid "Asks the daemon to quit" msgstr "Art alan hizmetinden çıkmasını ister" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Kimlik doÄŸrulanıyor…" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Çıkarılabilir aygıt üzerindeki donanım yazılımının sürümünü düşürmek için kimlik doÄŸrulama gerekir" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Bu makine üzerindeki donanım yazılımının sürümünü düşürmek için kimlik doÄŸrulama gerekir" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to fix a host security issue" msgstr "Bir makine güvenlik sorununu düzeltmek için kimlik doÄŸrulma gerekir" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "BIOS ayarlarını deÄŸiÅŸtirmek için kimlik doÄŸrulama gerekir" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "Donanım yazılımı güncellemeleri için kullanılan uzak yapılandırmayı deÄŸiÅŸtirmek için kimlik doÄŸrulama gerekir" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "Ardalan süreci yapılandırmasını deÄŸiÅŸtirmek için kimlik doÄŸrulama gerekir" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "BIOS ayarlarını okumak için kimlik doÄŸrulama gerekir" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "Onaylı donanım yazılımı listesini ayarlamak için kimlik doÄŸrulama gerekir" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "İstemci sertifikasını kullanarak veri imzası için kimlik doÄŸrulama gerekir" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "Bu makine üzerindeki donanım yazılımını güncellemek için kimlik doÄŸrulama gerekir" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to undo the fix for a host security issue" msgstr "Bir makine güvenlik sorunu düzeltmesini geri almak için kimlik doÄŸrulma gerekir" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Bir aygıtın kilidini açmak için kimlik doÄŸrulama gerekir" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Çıkarılabilir aygıt üzerindeki donanım yazılımını güncellemek için kimlik doÄŸrulama gerekir" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Bu makine üzerindeki donanım yazılımını güncellemek için kimlik doÄŸrulama gerekir" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "Aygıt için depolanan saÄŸlama toplamlarını güncellemek için kimlik doÄŸrulama gerekir" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "Otomatik Raporla" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS firmware updates" msgstr "BIOS donanım yazılımı güncellemeleri" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS rollback protection" msgstr "BIOS geri sarma koruması" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "BIOS güncellemeleri LVFS veya Windows Update tarafından teslim edildi" #. TRANSLATORS: command description msgid "Blocks a specific firmware from being installed" msgstr "Belirli bir donanım yazılımının kurulmasını engeller" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "Önyükleyici Sürümü" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * Utilized by OS means the distribution enabled it msgid "CET OS Support" msgstr "CET İşletim Sistemi DesteÄŸi" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * enabled means supported by the processor msgid "CET Platform" msgstr "CET Platformu" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "İptal" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "İptal Edildi" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "dbx güncellemesi halihazırda uygulandığından uygulanamıyor." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "DeÄŸiÅŸti" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "SaÄŸlama Toplamı" #. TRANSLATORS: get interactive prompt, where branch is the #. * supplier of the firmware, e.g. "non-free" or "free" msgid "Choose branch" msgstr "Dal seç" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "Aygıt seç" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "Donanım yazılımı seç" #. TRANSLATORS: get interactive prompt msgid "Choose volume" msgstr "Bölüm seç" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "Son güncellemenin sonuçlarını temizler" #. TRANSLATORS: error message msgid "Command not found" msgstr "Komut bulunamadı" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology msgid "Control-flow Enforcement Technology" msgstr "Denetim Akışı Zorlama Teknolojisi (CET)" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "Kritik" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "Var olan sürüm" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Hata Ayıklama Seçenekleri" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "Sıkıştırma açılıyor…" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "Açıklama" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "Ayrıntılar" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "Aygıt Bayrakları" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "Aygıt KimliÄŸi" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Aygıt eklendi:" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "Aygıtın pil gücü çok düşük" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Aygıt deÄŸiÅŸti:" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "Aygıt kilitli" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "Aygıt saÄŸlanan tüm sürümlerin kurulumunu gerektiriyor" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "Aygıta eriÅŸilemiyor" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Aygıt çıkarıldı:" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "Aygıt güncellemesinin etkinleÅŸtirilmesi gerekiyor" #. TRANSLATORS: save the old firmware to disk before installing the new one msgid "Device will backup firmware before installing" msgstr "Aygıt, kurulumdan önce donanım yazılımını yedekleyecek" #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "Donanım yazılımı güncellemesi olmayan aygıtlar:" #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "Var olan en son donanım yazılımı sürümüne sahip aygıtlar:" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "Devredışı" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "Eski üst verileri kontrol etme" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "Bildirilmeyen geçmiÅŸi kontrol etme" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "Günlük etki alanı önekini içerme" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "Zaman damgası önekini içerme" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "Aygıt güvenlik denetimlerini yapma" #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "GeçmiÅŸi veri tabanına yazma" #. TRANSLATORS: success msgid "Done!" msgstr "Tamamlandı!" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "Bir aygıttaki üretici yazılımı sürümünü düşürür" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "%s sürümü düşürülüyor…" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "İndiriliyor…" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "Süre" #. TRANSLATORS: Title: if we are emulating a different host msgid "Emulated host" msgstr "Öykünülen makine" msgid "Enable" msgstr "EtkinleÅŸtir" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "Bu uzaktan kumanda etkinleÅŸtirilsin mi?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "EtkinleÅŸtirildi" msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "Bu iÅŸlevselliÄŸin etkinleÅŸtirilmesinin riski size aittir; bu, bu güncellemelerin neden olduÄŸu sorunlarla ilgili olarak özgün ekipman üreticinizle iletiÅŸime geçmeniz gerektiÄŸi anlamına gelir. Yalnızca güncelleme iÅŸleminin kendisiyle ilgili sorunlar $OS_RELEASE:BUG_REPORT_URL$ adresine bildirilmelidir." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "Bu uzaktan kumandayı etkinleÅŸtirmek kendi sorumluluÄŸunuzdadır." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "ÅžifrelenmiÅŸ" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "Åžifreli RAM" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "Tüm donanım yazılımı güncelleme geçmiÅŸini sil" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Siliniyor…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "Küçük bir gecikme sonrası çık" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "İşletke yüklendikten sonra çık" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "FILE" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "DOSYA-ADI" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "DOSYAADI AYGIT-KİMLİĞİ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "DOSYAADI [AYGIT-KİMLİĞİ|GUID]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "BaÅŸarısız" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Güncelleme uygulanamadı" #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "Ardalan sürecine baÄŸlanamadı" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "Yerel dbx yüklenemedi" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "Sistem dbx'i yüklenemedi" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "Kilitleme baÅŸarısız." #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Argümanlar ayrıştırılamadı" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "Dosya ayrıştırılamadı" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "Yerel dbx ayrıştırılamadı" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "ESP içeriÄŸi doÄŸrulanamadı" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Dosya adı" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "Dosya Adı İmzası" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Dosya adı gerekli" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware Attestation" msgstr "Donanım Yazılımı TasdiÄŸi" #. TRANSLATORS: longer description msgid "Firmware Attestation checks device software using a reference copy, to make sure that it has not been changed." msgstr "Donanım Yazılımı TasdiÄŸi, deÄŸiÅŸtirilmediÄŸinden emin olmak için aygıt yazılımını bir baÅŸvuru kopyası kullanarak denetler." #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "Donanım Yazılımı BIOS Açıklayıcısı" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "Donanım Yazılımı BIOS Bölgesi" #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "Donanım Yazılımı Temel URI" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "Donanım Yazılımı Güncellemesi D-Bus Hizmeti" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Donanım Yazılımı Güncellemesi Ardalan Süreci" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "Donanım Yazılımı Güncelleyicisi DoÄŸrulaması" #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "Donanım Yazılımı Güncellemeleri" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "Donanım Yazılımı Yardımcı Programı" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "Donanım Yazılımı Yazma Koruması" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "Donanım Yazılımı Yazma Koruması Kilidi" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "Donanım yazılımı tasdiÄŸi" #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "Donanım yazılımı güncellemeleri" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "Bayraklar" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Bulundu" #. TRANSLATORS: Title: if the part has been fused msgid "Fused platform" msgstr "ErimiÅŸ platform" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" msgstr[1] "GUIDs" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" msgid "Get BIOS settings" msgstr "BIOS ayarlarını al" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "fwupd tarafından desteklenen tüm aygıt bayraklarını getir" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "EtkinleÅŸtirilmiÅŸ tüm eklentileri sisteme kaydettir" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Donanım yazılımı dosyasıyla ilgili ayrıntıları al" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "BaÄŸlı donanım için güncelleme listesini al" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "Bir aygıt için sürümleri alır" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Son güncellemeden sonuçları alır" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "HWIDS-FILE" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "Donanım yeniden takılmayı bekliyor" #. TRANSLATORS: the release urgency msgid "High" msgstr "Yüksek" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "Host Güvenlik KimliÄŸi:" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "IOMMU Koruması" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "BoÅŸta…" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "Dosyaları indirirken katı SSL kontrollerini yoksay" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "Kurulum Süresi" #. TRANSLATORS: command description msgid "Install a specific firmware on a device, all possible devices will also be installed once the CAB matches" msgstr "Bir aygıta belirli bir donanım yazılımı kur, CAB eÅŸleÅŸtiÄŸinde olası tüm aygıtlar da kurulacak" msgid "Install old version of signed system firmware" msgstr "İmzalı sistem donanım yazılımının eski sürümünü kur" msgid "Install old version of unsigned system firmware" msgstr "İmzasız sistem donanım yazılımının eski sürümünü kur" msgid "Install signed device firmware" msgstr "İmzalı aygıt donanım yazılımını kur" msgid "Install signed system firmware" msgstr "İmzalı sistem donanım yazılımını kur" msgid "Install unsigned device firmware" msgstr "İmzasız aygıt donanım yazılımını kur" msgid "Install unsigned system firmware" msgstr "İmzasız sistem donanım yazılımını kur" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "Donanım yazılımı güncellemesi kuruluyor…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "Kuruluyor %s…" #. TRANSLATORS: if it breaks, you get to keep both pieces msgid "Installing this update may also void any device warranty." msgstr "Bu güncellemenin kurulması, herhangi bir aygıt garantisini de geçersiz kılabilir." #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM Protected" msgstr "Intel BootGuard ACM Korumalı" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "Intel BootGuard ACM korumalı" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard Error Policy" msgstr "Intel BootGuard Hata Politikası" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard Fuse" msgstr "Intel BootGuard Sigortası" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "Intel BootGuard OTP sigortası" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard Verified Boot" msgstr "Intel BootGuard DoÄŸrulanmış Önyükleme" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Intel BootGuard hata politikası" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Intel BootGuard doÄŸrulanmış önyükleme" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS mitigation" msgstr "Intel GDS yatıştırması" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Manufacturing Mode" msgstr "Intel Yönetim İşletkesi Fabrika Kipi" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the "override" is #. enabled #. * with a jumper -- luckily it is probably not accessible to end users on #. consumer #. * boards msgid "Intel Management Engine Override" msgstr "Intel Yönetim İşletkesi Geçersiz Kılması" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Version" msgstr "Intel Yönetim İşletkesi Sürümü" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "Dahili aygıt" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "Geçersiz" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "Yükletme" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "Sorunlar" msgstr[1] "Sorunlar" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "Son deÄŸiÅŸtirilme" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "Bir dakikadan daha az kaldı" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "Lisans" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "Linux ÇekirdeÄŸi Karantinası" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "Linux ÇekirdeÄŸi DoÄŸrulaması" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "Linux Takas Alanı" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Linux Satıcı Donanım Yazılımı Hizmeti (Kararlı Donanım Yazılımı)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Linux Satıcı Donanım Yazılımı Hizmeti (Sınama Donanım Yazılımı)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Linux çekirdeÄŸi" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Linux çekirdeÄŸi karantinası" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Linux takas alanı" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "dbx içindeki girdileri listele" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "Kullanılabilir donanım yazılımı türlerini listele" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "Yükleniyor…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "Kilitli" #. TRANSLATORS: the release urgency msgid "Low" msgstr "Düşük" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refers #. * to the private/public key used to secure loading of firmware msgid "MEI Key Manifest" msgstr "MEI Anahtar Manifestosu" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refer #. * to the private/public key used to secure loading of firmware msgid "MEI key manifest" msgstr "MEI anahtar manifestosu" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "MEI fabrika kipi" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "MEI geçersiz kılması" msgid "MEI version" msgstr "MEI sürümü" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "Orta" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "Üst Veri İmzası" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "Üst veri URI" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "Üst veri Linux Tedarikçi Donanım Yazılımı Hizmetinden edinilebilir" #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "Asgari Sürüm" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "Bir art alan hizmeti yapılandırma deÄŸerini deÄŸiÅŸtirir" msgid "Modify a configured remote" msgstr "Uzak yapılandırmayı deÄŸiÅŸtir" msgid "Modify daemon configuration" msgstr "Ardalan süreci yapılandırmasını deÄŸiÅŸtir" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "Olaylar için art alan hizmetini gözetle" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "Kurulumdan sonra yeniden baÅŸlatma gerekiyor" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "Kurulumdan sonra kapatma gerekiyor" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "Yeni sürüm" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "Belirtilen eylem yok!" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "Donanım yazılımı kimlik bilgisi bulunamadı" #. TRANSLATORS: nothing found msgid "No firmware found" msgstr "Donanım yazılımı bulunamadı" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Donanım yazılımı güncelleme yeteneÄŸine sahip donanım saptanamadı" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "Sürüm bulunamadı" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "Åžu anda uzaktan kumanda etkin deÄŸil, bu nedenle üst veri yok." #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "Uzaktan kumanda bulunamadı" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "Güncellenebilir aygıt yok" msgid "No updates available for remaining devices" msgstr "Kalan aygıtlar için güncelleme yok" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "Bulunamadı" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "Desteklenmiyor" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "Tamam" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "Yalnızca tek PCR deÄŸerini göster" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "Sadece sürüm yükseltmelerine izin veriliyor" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "Öntanımlı ESP yolunu geçersiz kıl" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "YOL" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "Donanım yazılımı dosyası hakkındaki ayrıntıları ayrıştır ve göster" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "dbx güncellemesi ayrıştırılıyor…" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "Sistem dbx'i ayrıştırılıyor…" #. TRANSLATORS: remote filename base msgid "Password" msgstr "Parola" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "Platform Hata Ayıklaması" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "Platform hata ayıklaması" #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "0-%u arası bir sayı girin:" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA Protection" msgstr "Önyükleme Öncesi DMA Koruması" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "Önyükleme öncesi DMA koruması" #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "Önceki sürüm" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "Öncelik" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Processor Security Checks" msgstr "İşlemci Güvenlik Denetimleri" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "Processor rollback protection" msgstr "İşlemci geri sarma koruması" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "Sahipli" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "REMOTE-ID" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "Åžuradan okunuyor %s…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Okunuyor…" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "Üst veriyi uzak sunucudan yenile" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr " %s %s yeniden kurulsun mu?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "Var olan donanım yazılımını aygıta yeniden kur" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "Donanım yazılımını aygıta yeniden kur" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "Uzak Kimlik" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "Rapor URI" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "Uzak sunucuya bildirildi" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "İnternet baÄŸlantısı gerektirir" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Yeniden baÅŸlatılsın mı?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "DeÄŸiÅŸikliklerin etkili olması için art alan hizmeti yeniden baÅŸlatılsın mı?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "Aygıt yeniden baÅŸlatılıyor…" #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "install-blob kullanırken eklenti bileÅŸik temizleme yordamını çalıştır" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "install-blob kullanırken eklenti bileÅŸik hazırlama yordamını çalıştır" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "SMAP" msgstr "SMAP" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "SPI BIOS açıklayıcısı" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "SPI BIOS bölgesi" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "SPI kilidi" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "SPI replay protection" msgstr "SPI yeniden oynatma koruması" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "SPI yazımı" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "SPI write protection" msgstr "SPI yazma koruması" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "ALTSİSTEM SÜRÜCÜ [AYGIT-KİMLİĞİ|GUID]" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Zamanlanıyor…" msgid "Security hardening for HSI" msgstr "HSI için güvenlik güçlendirmesi" #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "Daha fazlası için tıklayınız. %s " #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Seçili aygıt" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Bölüm seçildi" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "Seri Numarası" msgid "Set one or more BIOS settings" msgstr "Bir veya daha çok BIOS ayarı ayarla" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Onaylı donanım yazılımı listesini ayarlar" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "Ayarlar, sistem yeniden baÅŸlatıldıktan sonra uygulanacaktır" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "Donanım yazılım geçmiÅŸini geliÅŸtiricilerle paylaÅŸ" #. TRANSLATORS: command line option msgid "Show all results" msgstr "Sonuçları göster" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "İstemci ve art alan hizmeti sürümlerini göster" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "Belirli bir etki alanı için ardalan süreci ayrıntılı bilgisini göster" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "Tüm etki alanları için hata ayıklama bilgisini göster" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Hata ayıklama seçeneklerini göster" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Güncellenemeyen aygıtları göster" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Ek hata ayıklama bilgisini göster" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "Donanım yazılımı güncellemelerinin geçmiÅŸini göster" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "dbx'in hesaplanan sürümünü göster" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "Kapatılsın mı?" msgid "Sign data using the client certificate" msgstr "İstemci sertifikasını kullanarak veri imzala" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "İstemci sertifikasını kullanarak veri imzala" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "Karşıya yüklenen verileri istemci sertifikasıyla imzala" msgid "Signature" msgstr "İmza" #. TRANSLATORS: file size of the download msgid "Size" msgstr "Boyut" #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "Kaynak" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "dbx veri tabanı dosyasını belirt" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "Tüm aygıtlar baÅŸarıyla etkinleÅŸtirildi" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "Yeni üst veri baÅŸarıyla indirildi:" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "Donanım yazılımı baÅŸarıyla kuruldu" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "Yapılandırma deÄŸeri baÅŸarıyla deÄŸiÅŸtirildi" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "Üst veri baÅŸarıyla yenilendi" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "Aygıt saÄŸlama toplamları baÅŸarıyla güncellendi" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "Aygıt saÄŸlaması toplamı baÅŸarıyla doÄŸrulandı" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Özet" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "Supervisor Mode Access Prevention" msgstr "Süpervizör Kipi EriÅŸim Önlemesi (SMAP)" #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "Destekleniyor" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "Desteklenen CPU" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "Uzak sunucuda desteklenir" #. TRANSLATORS: Title: a better sleep state msgid "Suspend To Idle" msgstr "BoÅŸ Duruma Askıya Al" #. TRANSLATORS: Title: sleep state msgid "Suspend To RAM" msgstr "RAM'e Askıya Al" #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "BoÅŸ duruma askıya al" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "RAM'e askıya al" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "METİN" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "TPM PCR0 yeniden yapımı" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM Platform Configuration" msgstr "TPM Platform Yapılandırması" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM Reconstruction" msgstr "TPM Yeniden Yapımı" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM empty PCRs" msgstr "TPM boÅŸ platform yapılandırma yazmaçları" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. show the user the entire data blob msgid "Target" msgstr "Hedef" #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "LVFS, bağımsız bir tüzel kiÅŸilik olarak çalışan ve $OS_RELEASE:NAME$ ile hiçbir baÄŸlantısı olmayan ücretsiz bir hizmettir. Dağıtımcınız, donanım yazılımı güncellemelerinden herhangi birinin sisteminizle veya baÄŸlı aygıtlarınızla uyumluluÄŸunu doÄŸrulamamış olabilir. Tüm ürün yazılımı yalnızca özgün ekipman üreticisi tarafından saÄŸlanır." #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "Art alan hizmeti 3. taraf kodu yükledi ve artık upstream geliÅŸtiricileri tarafından desteklenmiyor!" #. TRANSLATORS: now list devices with unfixed high-priority issues msgid "There are devices with issues:" msgstr "Sorunlu aygıtlar var:" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "Onaylı donanım yazılımı yok." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "Bu program yalnızca kök eriÅŸimi ile düzgün çalışabilir" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "Bu uzak konum, ambargoya tabi olmayan ancak hâlâ donanım satıcısı tarafından sınanmakta olan donanım yazılımı içermektedir. Donanım yazılımı güncellemesi baÅŸarısız olursa donanım yazılımını elle düşürmenin bir yolunun olduÄŸundan emin olmalısınız." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "Bu araç, bir yöneticinin UEFI dbx güncellemelerini uygulamasına olanak tanır." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "Bu araç, yöneticinin fwupd art alan hizmetini sorgulamasına ve denetlemesine izin vererek, yöneticinin donanım yazılımı kurulması ya da düşürülmesi gibi eylemleri gerçekleÅŸtirmesini saÄŸlar." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "Bu araç, bir yöneticinin ana sisteme kurulmadan fwupd eklentilerini kullanmasına izin verir." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "Bu araç, TPM olay günlüğünü donanım yazılımdan okur ve ayrıştırır" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Tür" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI Bootservice Variables" msgstr "UEFI Önyükleme Hizmeti DeÄŸiÅŸkenleri" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "UEFI Platform Anahtarı" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "UEFI Güvenli Önyükleme" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI bootservice variables" msgstr "UEFI önyükleme hizmeti deÄŸiÅŸkenleri" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "UEFI dbx İzlencesi" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "UEFI platform anahtarı" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "UEFI güvenli önyükleme" #. TRANSLATORS: command description msgid "Unblocks a specific firmware from being installed" msgstr "Belirli bir donanım yazılımının kurulması engelini kaldırır" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "ÅžifrelenmemiÅŸ" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Bilinmeyen" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Bilinmeyen Aygıt" msgid "Unlock the device to allow access" msgstr "EriÅŸime izin vermek için aygıtın kilidini açın" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "Serbest" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "Aygıt yazılımı eriÅŸimi için aygıt kilidini açar" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "Desteklenmeyen art alan hizmeti sürümü %s, istemci sürümü %s" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "Güncellenebilir" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Güncelleme Hatası" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "Güncelleme İletisi" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "Güncelleme Durumu" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "Güncelleme hatası bilinen bir sorundur, daha fazla bilgi için bu URL'yi ziyaret edin:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Åžimdi güncellensin mi?" msgid "Update the stored device verification information" msgstr "Depolanan aygıt doÄŸrulama bilgilerini güncelle" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "Güncelleniyor %s…" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr " %s, %s sürümünden %s sürümüne yükseltilsin mi?" #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "Kullanıcı bilgilendirildi" #. TRANSLATORS: remote filename base msgid "Username" msgstr "Kullanıcı adı" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "Geçerli" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "ESP içeriÄŸi doÄŸrulanıyor…" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "Türev" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "Üretici" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "DoÄŸrulanıyor…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "Sürüm" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "UYARI" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "Bekliyor…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "Donanım deÄŸiÅŸikliklerini izle" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "Dosyaya yazılıyor:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "Yazılıyor…" #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "Hizmet saÄŸlayıcınız, sisteminizle veya baÄŸlı aygıtlarla uyumluluk için herhangi bir donanım yazılımı güncellemesini doÄŸrulamamış olabilir." #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "Donanımınız bu donanım yazılımı kullanılarak zarar görebilir ve bu sürümün kurulması, %sile ilgili tüm garantileri geçersiz kılabilir." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[DEVICE-ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[AYGIT-KİMLİĞİ|GUID] [DAL]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [VERSION]" msgstr "[AYGIT-KİMLİĞİ|GUID] [SÜRÜM]" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "fwupd TPM olay günlüğü izlencesi" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "fwupd eklentileri" fwupd-2.0.10/po/uk.po000066400000000000000000004651151501337203100143230ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Yuri Chornoivan , 2015-2025 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Ukrainian (http://app.transifex.com/freedesktop/fwupd/language/uk/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: uk\n" "Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "ЛишилаÑÑ %.0f хвилина" msgstr[1] "ЛишилоÑÑ %.0f хвилини" msgstr[2] "ЛишилоÑÑ %.0f хвилин" msgstr[3] "ЛишилаÑÑ %.0f хвилина" #. TRANSLATORS: BMC refers to baseboard management controller which #. * is the device that updates all the other firmware on the system #, c-format msgid "%s BMC Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ BMC %s" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ð°ÐºÑƒÐ¼ÑƒÐ»Ñторів %s" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸ процеÑора %s" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ ÐºÐ°Ð¼ÐµÑ€Ð¸ %s" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½ÑŒ %s" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ %s Consumer ME" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ ÐºÐ¾Ð½Ñ‚Ñ€Ð¾Ð»ÐµÑ€Ð° %s" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ %s Corporate ME" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ð¿Ñ€Ð¸Ñтрою %s" #. TRANSLATORS: Video Display refers to the laptop internal display or #. * external monitor #, c-format msgid "%s Display Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð¸ÑÐ¿Ð»ÐµÑ %s" #. TRANSLATORS: Dock refers to the port replicator hardware laptops are #. * cradled in, or lowered onto #, c-format msgid "%s Dock Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ñтанції %s" #. TRANSLATORS: drive refers to a storage device, e.g. SATA disk #, c-format msgid "%s Drive Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ð´Ð¸Ñка %s" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ð²Ð±ÑƒÐ´Ð¾Ð²Ð°Ð½Ð¾Ð³Ð¾ контролера %s" #. TRANSLATORS: a device that can read your fingerprint pattern #, c-format msgid "%s Fingerprint Reader Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð·Ñ‡Ð¸Ñ‚ÑƒÐ²Ð°Ñ‡Ð° відбитків %s" #. TRANSLATORS: flash refers to solid state storage, e.g. UFS or eMMC #, c-format msgid "%s Flash Drive Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ñ„Ð»ÐµÑˆ-диÑка %s" #. TRANSLATORS: GPU refers to a Graphics Processing Unit, e.g. #. * the "video card" #, c-format msgid "%s GPU Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ð²Ñ–Ð´ÐµÐ¾ÐºÐ°Ñ€Ñ‚ÐºÐ¸ %s" #. TRANSLATORS: a large pressure-sensitive drawing area typically used #. * by artists and digital artists #, c-format msgid "%s Graphics Tablet Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð³Ñ€Ð°Ñ„Ñ–Ñ‡Ð½Ð¾Ð³Ð¾ планшета %s" #. TRANSLATORS: two miniature speakers attached to your ears #, c-format msgid "%s Headphones Update" msgstr "%s ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ð½Ð°Ð²ÑƒÑˆÐ½Ð¸ÐºÑ–Ð²" #. TRANSLATORS: headphones with an integrated microphone #, c-format msgid "%s Headset Update" msgstr "%s ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ð³Ð°Ñ€Ð½Ñ–Ñ‚ÑƒÑ€Ð¸" #. TRANSLATORS: an input device used by gamers, e.g. a joystick #, c-format msgid "%s Input Controller Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ ÐºÐ¾Ð½Ñ‚Ñ€Ð¾Ð»ÐµÑ€Ð° Ð²Ð²ÐµÐ´ÐµÐ½Ð½Ñ %s" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ ÐºÐ»Ð°Ð²Ñ–Ð°Ñ‚ÑƒÑ€Ð¸ %s" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ ME %s" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ð¼Ð¸ÑˆÑ– %s" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ñ–Ð½Ñ‚ÐµÑ€Ñ„ÐµÐ¹Ñу мережі %s" #. TRANSLATORS: SSD refers to a Solid State Drive, e.g. non-rotating #. * SATA or NVMe disk #, c-format msgid "%s SSD Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ SSD %s" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ ÐºÐ¾Ð½Ñ‚Ñ€Ð¾Ð»ÐµÑ€Ð° Ñховища даних %s" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ ÑиÑтеми %s" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ TPM %s" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ ÐºÐ¾Ð½Ñ‚Ñ€Ð¾Ð»ÐµÑ€Ð° Thunderbolt %s" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ ÑенÑорної панелі %s" #. TRANSLATORS: Dock refers to the port replicator device connected #. * by plugging in a USB cable -- which may or may not also provide power #, c-format msgid "%s USB Dock Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ñтанції USB %s" #. TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth #. * device that stays in the USB port so the wireless peripheral works #, c-format msgid "%s USB Receiver Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ USB-приймача %s" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ %s" #. TRANSLATORS: warn the user before updating, %1 is a device name #, c-format msgid "%s and all connected devices may not be usable while updating." msgstr "%s Ñ– уÑÑ– з'єднані приÑтрою можуть бути недоÑтупними протÑгом оновленнÑ." #. TRANSLATORS: %1 refers to some kind of security test, e.g. "Encrypted RAM". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s appeared: %s" msgstr "З'ÑвивÑÑ %s: %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "UEFI platform #. key". #. * %2 and %3 refer to results value, e.g. "Valid" and "Invalid" #, c-format msgid "%s changed: %s → %s" msgstr "Змінено %s: %s → %s" #. TRANSLATORS: %1 refers to some kind of security test, e.g. "SPI BIOS #. region". #. %2 refers to a result value, e.g. "Invalid" #, c-format msgid "%s disappeared: %s" msgstr "Зник %s: %s" #. TRANSLATORS: the device has a reason it can't update, e.g. laptop lid #. closed #, c-format msgid "%s is not currently updatable" msgstr "%s зараз непридатний до оновленнÑ" #. TRANSLATORS: %1 is a device name, e.g. "ThinkPad Universal #. * ThunderBolt 4 Dock" and %2 is "fwupdmgr activate" #, c-format msgid "%s is pending activation; use %s to complete the update." msgstr "%s очікує на активацію; ÑкориÑтайтеÑÑ %s Ð´Ð»Ñ Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "Режим виробництва %s" #. TRANSLATORS: warn the user before #. * updating, %1 is a device name #, c-format msgid "%s must remain connected for the duration of the update to avoid damage." msgstr "Ð”Ð»Ñ ÑƒÐ½Ð¸ÐºÐ½ÐµÐ½Ð½Ñ Ð¿Ð¾ÑˆÐºÐ¾Ð´Ð¶ÐµÐ½ÑŒ %s має лишатиÑÑ Ð·'єднаним із комп'ютером протÑгом оновленнÑ." #. TRANSLATORS: warn the user before updating, %1 is a machine #. * name #, c-format msgid "%s must remain plugged into a power source for the duration of the update to avoid damage." msgstr "Ð”Ð»Ñ ÑƒÐ½Ð¸ÐºÐ½ÐµÐ½Ð½Ñ Ð¿Ð¾ÑˆÐºÐ¾Ð´Ð¶ÐµÐ½ÑŒ %s має лишатиÑÑ Ð·'єднаним із джерелом Ð¶Ð¸Ð²Ð»ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾Ñ‚Ñгом оновленнÑ." #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "ÐŸÐµÑ€ÐµÐ²Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ %s" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s, верÑÑ–Ñ %s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "ВерÑÑ–Ñ %s" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u день" msgstr[1] "%u дні" msgstr[2] "%u днів" msgstr[3] "%u день" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "Ð”Ð»Ñ %u приÑтрою випущено Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸." msgstr[1] "Ð”Ð»Ñ %u приÑтроїв випущено Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼." msgstr[2] "Ð”Ð»Ñ %u приÑтроїв випущено Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼." msgstr[3] "Ð”Ð»Ñ %u приÑтрою випущено Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼." #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device is not the best known configuration." msgid_plural "%u devices are not the best known configuration." msgstr[0] "%uприÑтрій не Ñ” найкращою відомою конфігурацією." msgstr[1] "%u приÑтрої не Ñ” найкращою відомою конфігурацією." msgstr[2] "%u приÑтрої не Ñ” найкращою відомою конфігурацією." msgstr[3] "%u приÑтрій не Ñ” найкращою відомою конфігурацією." #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u година" msgstr[1] "%u години" msgstr[2] "%u годин" msgstr[3] "%u година" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u хвилина" msgstr[1] "%u хвилини" msgstr[2] "%u хвилин" msgstr[3] "%u хвилина" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u Ñекунда" msgstr[1] "%u Ñекунди" msgstr[2] "%u Ñекунд" msgstr[3] "%u Ñекунда" #. TRANSLATORS: device tests can be specific to a CPU type #, c-format msgid "%u test was skipped" msgid_plural "%u tests were skipped" msgstr[0] "%u перевірку пропущено" msgstr[1] "%u перевірки пропущено" msgstr[2] "%u перевірок пропущено" msgstr[3] "%u перевірку пропущено" #. TRANSLATORS: first percentage is current value, 2nd percentage is the #. * lowest limit the firmware update is allowed for the update to happen #, c-format msgid "%u%% (threshold %u%%)" msgstr "%u%% (порогове Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ â€” %u%%)" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(Ñ” заÑтарілим)" #. TRANSLATORS: HSI event title msgid "A TPM PCR is now an invalid value" msgstr "PCR TPM тепер має некоректне значеннÑ" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "AMD Firmware Replay Protection" msgstr "ЗахиÑÑ‚ Ð²Ñ–Ð´Ñ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸ AMD" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "AMD Firmware Write Protection" msgstr "ЗахиÑÑ‚ від запиÑу мікропрограми AMD" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "AMD Secure Processor Rollback Protection" msgstr "ЗахиÑÑ‚ від Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¿Ð¾Ð¿ÐµÑ€ÐµÐ´Ð½Ñ–Ñ… Ñтанів процеÑора AMD" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "ARCHIVE FIRMWARE METAINFO [FIRMWARE] [METAINFO] [JCATFILE]" msgstr "ÐРХІВ МІКРОПРОГРÐМРМЕТÐІÐФО [МІКРОПРОГРÐМÐ] [МЕТÐІÐФО] [ФÐЙЛJCAT]" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "Потрібна діÑ:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "ЗадіÑти приÑтрої" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "ЗадіÑти приÑтрої у черзі очікуваннÑ" msgid "Activate the new firmware on the device" msgstr "ÐÐºÑ‚Ð¸Ð²Ð°Ñ†Ñ–Ñ Ð½Ð¾Ð²Ð¾Ñ— мікропрограми на приÑтрої" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "Ðктивуємо Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "Ðктивуємо Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸ длÑ" #. TRANSLATORS: command description msgid "Adds devices to watch for future emulation" msgstr "Додає приÑтрої Ð´Ð»Ñ ÑпоÑÑ‚ÐµÑ€ÐµÐ¶ÐµÐ½Ð½Ñ Ñ‰Ð¾Ð´Ð¾ майбутньої емулÑції" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "Вік" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "Згодні, увімкнути віддалене Ñховище?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "Інша назва %s" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are now valid" msgstr "Тепер уÑÑ– PCR TPM Ñ” коректними" #. TRANSLATORS: HSI event title msgid "All TPM PCRs are valid" msgstr "УÑÑ– PCR TPM Ñ” коректними" #. TRANSLATORS: an application is preventing system updates msgid "All devices are prevented from update by system inhibit" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð½Ð° уÑÑ–Ñ… приÑтроÑÑ… заборонено ÑиÑтемою" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "УÑÑ– приÑтрої одного типу буде оновлено одночаÑно" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "Дозволити Ð·Ð½Ð¸Ð¶ÐµÐ½Ð½Ñ Ð²ÐµÑ€Ñій мікропрограми" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "Дозволити перевÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð½Ð°Ñвних верÑій мікропрограми" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "Дозволити Ð¿ÐµÑ€ÐµÐ¼Ð¸ÐºÐ°Ð½Ð½Ñ Ð³Ñ–Ð»Ð¾Ðº мікропрограми" #. TRANSLATORS: error message msgid "Already exists, and no --force specified" msgstr "Вже Ñ–Ñнує, а --force не вказано" #. TRANSLATORS: is not the main firmware stream msgid "Alternate branch" msgstr "Ðльтернативна гілка" #. TRANSLATORS: another application is updating the device already msgid "An update is in progress" msgstr "Виконуємо оновленнÑ" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "Ð”Ð»Ñ Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ñлід перезавантажити ÑиÑтему." #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "Ð”Ð»Ñ Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ ÑиÑтему Ñлід вимкнути." #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "Відповідати «так» на уÑÑ– питаннÑ" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "ЗаÑтоÑувати оновленнÑ, навіть Ñкщо воно не Ñ” рекомендованим" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "ЗаÑтоÑувати файли оновлень" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "ЗаÑтоÑовуємо оновленнÑ…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "Схвалена мікропрограма:" msgstr[1] "Схвалені мікропрограми:" msgstr[2] "Схвалені мікропрограми:" msgstr[3] "Схвалена мікропрограма:" #. TRANSLATORS: command description msgid "Asks the daemon to quit" msgstr "ÐадÑилає фоновій Ñлужбі запит щодо виходу" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "ДолучитиÑÑ Ð´Ð¾ режиму мікропрограми" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "Проходимо розпізнаваннÑ…" #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "Потрібні дані Ð´Ð»Ñ Ñ€Ð¾Ð·Ð¿Ñ–Ð·Ð½Ð°Ð²Ð°Ð½Ð½Ñ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "Ð”Ð»Ñ Ð²ÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð·Ð°Ñтарілої верÑÑ–Ñ— мікропрограми на портативний приÑтрій Ñлід пройти розпізнаваннÑ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "Ð”Ð»Ñ Ð²ÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð·Ð°Ñтарілої верÑÑ–Ñ— мікропрограми на цей комп’ютер Ñлід пройти розпізнаваннÑ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to enable emulation data collection" msgstr "Щоб увімкнути збірку даних емулÑції, Ñлід пройти розпізнаваннÑ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to fix a host security issue" msgstr "Ð”Ð»Ñ Ð²Ð¸Ð¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ð¸ із захиÑтом Ñлід пройти розпізнаваннÑ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to load hardware emulation data" msgstr "Щоб завантажити дані емулÑції обладнаннÑ, Ñлід пройти розпізнаваннÑ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "Ð”Ð»Ñ Ð²Ð½ÐµÑÐµÐ½Ð½Ñ Ð·Ð¼Ñ–Ð½ до атрибутів BIOS Ñлід пройти розпізнаваннÑ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "Ð”Ð»Ñ Ð²Ð½ÐµÑÐµÐ½Ð½Ñ Ð·Ð¼Ñ–Ð½ до запиÑів налаштованих віддалених приÑтроїв, Ñкі викориÑтовуютьÑÑ Ð´Ð»Ñ Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼, Ñлід пройти розпізнаваннÑ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "Ð”Ð»Ñ Ð²Ð½ÐµÑÐµÐ½Ð½Ñ Ð·Ð¼Ñ–Ð½ до налаштувань фонової Ñлужби Ñлід пройти розпізнаваннÑ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "Ð”Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ Ð¿Ð°Ñ€Ð°Ð¼ÐµÑ‚Ñ€Ñ–Ð² BIOS Ñлід пройти розпізнаваннÑ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to reset daemon configuration to defaults" msgstr "Ð”Ð»Ñ Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ñ‚Ð¸Ð¿Ð¾Ð²Ð¸Ñ… налаштувань фонової Ñлужби Ñлід пройти розпізнаваннÑ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to save hardware emulation data" msgstr "Щоб зберегти дані емулÑції обладнаннÑ, Ñлід пройти розпізнаваннÑ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "Щоб вÑтановити ÑпиÑок Ñхвалених мікропрограм, вам Ñлід пройти розпізнаваннÑ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "Щоб підпиÑати дані за допомогою клієнтÑького Ñертифіката, вам Ñлід пройти розпізнаваннÑ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to stop the firmware update service" msgstr "Щоб отримати доÑтуп до Ñлужби Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸, вам Ñлід пройти розпізнаваннÑ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "Щоб перемкнутиÑÑ Ð½Ð° нову верÑÑ–ÑŽ мікропрограми, вам Ñлід пройти розпізнаваннÑ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to undo the fix for a host security issue" msgstr "Ð”Ð»Ñ ÑкаÑÑƒÐ²Ð°Ð½Ð½Ñ Ð²Ð¸Ð¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ із захиÑтом вузла Ñлід пройти розпізнаваннÑ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "Щоб розблокувати приÑтрій, Ñлід пройти розпізнаваннÑ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "Ð”Ð»Ñ Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸ на портативному приÑтрої Ñлід пройти розпізнаваннÑ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "Щоб отримати доÑтуп до Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸ цього комп’ютера, вам Ñлід пройти розпізнаваннÑ" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "Щоб отримати доÑтуп до Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð·Ð±ÐµÑ€ÐµÐ¶ÐµÐ½Ð¸Ñ… контрольних Ñум Ð´Ð»Ñ Ð¿Ñ€Ð¸Ñтрою, вам Ñлід пройти розпізнаваннÑ" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "Ðвтоматичне звітуваннÑ" #. TRANSLATORS: we can auto-uninhibit after a timeout #, c-format msgid "Automatically uninhibiting in %ums…" msgstr "Ðвтоматичне ÑкаÑÑƒÐ²Ð°Ð½Ð½Ñ ÑƒÑÐ¿Ð°Ð´ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð·Ð° %uмÑ…" #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "Ðвтоматично вивантажувати кожного разу?" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS Firmware Updates" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸ BIOS" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS Rollback Protection" msgstr "ЗахиÑÑ‚ від Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¿Ð¾Ð¿ÐµÑ€ÐµÐ´Ð½Ñ–Ñ… Ñтанів BIOS" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS firmware updates" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸ BIOS" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "BIOS rollback protection" msgstr "ЗахиÑÑ‚ від Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¿Ð¾Ð¿ÐµÑ€ÐµÐ´Ð½Ñ–Ñ… Ñтанів BIOS" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ BIOS, Ñкі надано за допомогою LVFS або Windows Update" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "BUILDER-XML FILENAME-DST" msgstr "XML-ЗБИРÐÐÐЯ ÐÐЗВÐ-ФÐЙЛÐ-ДЖ" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "ÐкумулÑтор" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "Пов'Ñзати новий драйвер Ñдра" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "Заблоковані файли мікропрограм:" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "Заблокована верÑÑ–Ñ" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "Блокуємо мікропрограму:" #. TRANSLATORS: command description msgid "Blocks a specific firmware from being installed" msgstr "Блокує вÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¿ÐµÐ²Ð½Ð¾Ñ— мікропрограми" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "ВерÑÑ–Ñ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÑƒÐ²Ð°Ñ‡Ð°" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "Гілка" #. TRANSLATORS: command description msgid "Build a cabinet archive from a firmware blob and XML metadata" msgstr "Зібрати архів cab з бінарної мікропрограми та метаданих XML" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "Зібрати файл мікропрограми" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * Utilized by OS means the distribution enabled it msgid "CET OS Support" msgstr "Підтримка CET в ОС" #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology, #. * enabled means supported by the processor msgid "CET Platform" msgstr "Платформа CET" #. TRANSLATORS: longer description msgid "CPU Microcode must be updated to mitigate against various information-disclosure security issues." msgstr "Ð”Ð»Ñ Ð±Ð¾Ñ€Ð¾Ñ‚ÑŒÐ±Ð¸ з різноманітними проблемами Ñ€Ð¾Ð·ÐºÑ€Ð¸Ñ‚Ñ‚Ñ Ð´Ð°Ð½Ð¸Ñ… має бути оновлено мікрокод процеÑора." #. TRANSLATORS: we can save all device enumeration events for emulation msgid "Can tag for emulation" msgstr "Можна вÑтановлювати мітки Ð´Ð»Ñ ÐµÐ¼ÑƒÐ»Ñції" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "СкаÑувати" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "СкаÑовано" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "Ðе вдалоÑÑ Ð·Ð°ÑтоÑувати, оÑкільки Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ dbx вже заÑтоÑовано." #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "Змінено" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "Перевірити відповідніÑть криптографічних хешів мікропрограми" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "Контрольна Ñума" #. TRANSLATORS: get interactive prompt, where branch is the #. * supplier of the firmware, e.g. "non-free" or "free" msgid "Choose branch" msgstr "Виберіть гілку" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "Виберіть приÑтрій" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "Виберіть мікропрограму" #. TRANSLATORS: get interactive prompt msgid "Choose release" msgstr "Виберіть випуÑк" #. TRANSLATORS: get interactive prompt msgid "Choose volume" msgstr "Виберіть том" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "Вилучає результати оÑтаннього оновленнÑ" #. TRANSLATORS: error message msgid "Command not found" msgstr "Такої команди не знайдено" #. TRANSLATORS: is not supported by the vendor msgid "Community supported" msgstr "Підтримка Ñпільноти" #. TRANSLATORS: command description msgid "Compares two versions for equality" msgstr "Порівнює дві верÑÑ–Ñ— на ÑхожіÑть" #. TRANSLATORS: title prefix for the BIOS settings dialog msgid "Configuration Change Suggested" msgstr "Запропоновано зміни у налаштуваннÑÑ…" #. TRANSLATORS: no peeking msgid "Configuration is only readable by the system administrator" msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¼Ð¾Ð¶Ð½Ð° читати лише від імені адмініÑтратора ÑиÑтеми" #. TRANSLATORS: longer description msgid "Control-Flow Enforcement Technology detects and prevents certain methods for running malicious software on the device." msgstr "Ð¢ÐµÑ…Ð½Ð¾Ð»Ð¾Ð³Ñ–Ñ Control-Flow Enforcement виÑвлÑÑ” Ñ– запобігає певним методам запуÑку зловмиÑниками програмного Ð·Ð°Ð±ÐµÐ·Ð¿ÐµÑ‡ÐµÐ½Ð½Ñ Ð½Ð° приÑтрої." #. TRANSLATORS: Title: CET = Control-flow Enforcement Technology msgid "Control-flow Enforcement Technology" msgstr "Ð¢ÐµÑ…Ð½Ð¾Ð»Ð¾Ð³Ñ–Ñ Ð¿Ñ€Ð¸Ð¼ÑƒÑового ÐºÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð¾Ñ‚Ð¾ÐºÐ¾Ð¼ (CET)" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "Перетворити файл мікропрограми" #. TRANSLATORS: command description msgid "Create an EFI boot entry" msgstr "Створити Ð·Ð°Ð¿Ð¸Ñ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ EFI" #. TRANSLATORS: when the update was built msgid "Created" msgstr "Створено" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "Критичний" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "ДоÑтупна перевірка криптографічної хеш-Ñуми" #. TRANSLATORS: current value of a BIOS setting msgid "Current Value" msgstr "Поточне значеннÑ" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "Поточна верÑÑ–Ñ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "ІД_ПРИСТРОЮ|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "Параметри діагноÑтики" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "РозпаковуваннÑ…" #. TRANSLATORS: command description msgid "Delete an EFI boot entry" msgstr "Вилучити Ð·Ð°Ð¿Ð¸Ñ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ EFI" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "ОпиÑ" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "Від'єднатиÑÑ Ð´Ð¾ режиму завантажувача" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "Подробиці" #. TRANSLATORS: the best known configuration is a set of software that we know #. works #. * well together. In the OEM and ODM industries it is often called a BKC msgid "Deviate from the best known configuration?" msgstr "Вилучити з найкращої відомої конфігурації?" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "Прапорці приÑтрою" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "Ід. приÑтрою" #. TRANSLATORS: description of the device requests msgid "Device Requests" msgstr "Запити щодо приÑтрою" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "Додано приÑтрій:" #. TRANSLATORS: the device is already connected msgid "Device already exists" msgstr "ПриÑтрій вже Ñ–Ñнує" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "Рівень зарÑду акумулÑтора приÑтрою Ñ” надто низьким" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse #, c-format msgid "Device battery power is too low (%u%%, requires %u%%)" msgstr "Рівень зарÑду акумулÑтора приÑтрою Ñ” надто низьким (%u%%, має бути принаймні %u%%)" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "ПриÑтрій може відновлюватиÑÑ Ð¿Ñ€Ð¸ невдалих оновленнÑÑ…" #. TRANSLATORS: lid means "laptop top cover" msgid "Device cannot be updated while the lid is closed" msgstr "Мікропрограму приÑтрою не можна оновлювати, доки закрито кришку" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "Змінено приÑтрій:" #. TRANSLATORS: a version check is required for all firmware msgid "Device firmware is required to have a version check" msgstr "Ð”Ð»Ñ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€ÐºÐ¸ верÑÑ–Ñ— потрібна мікропрограма приÑтрою" #. TRANSLATORS: emulated means we are pretending to be a different model msgid "Device is emulated" msgstr "Емульований приÑтрій" #. TRANSLATORS: device cannot be interrupted, for instance taking a phone call msgid "Device is in use" msgstr "ПриÑтрій викориÑтано Ñторонніми заÑобами" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "ПриÑтрій заблоковано" #. TRANSLATORS: we have two ways of communicating with the device, so we hide #. one msgid "Device is lower priority than an equivalent device" msgstr "ПріоритетніÑть приÑтрою Ñ” нижчою за пріоритетніÑть еквівалентного приÑтрою" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "Ð”Ð»Ñ Ð²ÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ ÑƒÑÑ–Ñ… наданих випуÑків потрібен приÑтрій" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "ПриÑтрій Ñ” недоÑтупним" #. TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode msgid "Device is unreachable, or out of wireless range" msgstr "ПриÑтрій Ñ” недоÑтупним або перебуває поза доÑÑжніÑтю бездротового зв'Ñзку" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "ПриÑтроєм можна кориÑтуватиÑÑ Ð¿Ñ€Ð¾Ñ‚Ñгом оновленнÑ" #. TRANSLATORS: usually this is when we're waiting for a reboot msgid "Device is waiting for the update to be applied" msgstr "ПриÑтрій очікує на заÑтоÑÑƒÐ²Ð°Ð½Ð½Ñ Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ" #. TRANSLATORS: success, so say thank you to the user msgid "Device list uploaded successfully, thanks!" msgstr "СпиÑок приÑтроїв уÑпішно вивантажено, дÑкуємо!" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "Вилучено приÑтрій:" #. TRANSLATORS: as in, wired mains power for a laptop msgid "Device requires AC power to be connected" msgstr "ПриÑтрій потребує з'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ñ–Ð· джерелом змінного Ñтруму" #. TRANSLATORS: device does not have a display connected msgid "Device requires a display to be plugged in" msgstr "ПриÑтрій вимагає з'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð· комп'ютером диÑплеÑ" #. TRANSLATORS: The device cannot be updated due to missing vendor's license." msgid "Device requires a software license to update" msgstr "ПриÑтрій потребує Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð»Ñ–Ñ†ÐµÐ½Ð·ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð½Ð¾Ð³Ð¾ забезпеченнÑ" #. TRANSLATORS: longer description msgid "Device software updates are provided for this device." msgstr "Ð”Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ приÑтрою надаватимутьÑÑ Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð½Ð¾Ð³Ð¾ забезпеченнÑ." #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "ПриÑтрій із покроковим оновленнÑм" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "У приÑтрої передбачено підтримку Ð¿ÐµÑ€ÐµÐ¼Ð¸ÐºÐ°Ð½Ð½Ñ Ð½Ð° іншу гілку мікропрограми" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¸Ñтрою потребує активації" #. TRANSLATORS: save the old firmware to disk before installing the new one msgid "Device will backup firmware before installing" msgstr "ПриÑтрій Ñтворить резервну копію мікропрограми перед вÑтановленнÑм" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "ПриÑтрій не з'ÑвитьÑÑ Ñƒ ÑпиÑку приÑтроїв піÑÐ»Ñ Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "ПриÑтрої, Ð´Ð»Ñ Ñких вдалоÑÑ ÑƒÑпішно оновити дані:" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "ПриÑтрої, Ð´Ð»Ñ Ñких не вдалоÑÑ Ð¾Ð½Ð¾Ð²Ð¸Ñ‚Ð¸ дані належним чином:" #. TRANSLATORS: message letting the user there is an update #. * waiting, but there is a reason it cannot be deployed msgid "Devices with firmware updates that need user action: " msgstr "ПриÑтрої із оновленнÑми мікропрограм, Ñкі потребують дій від кориÑтувача:" #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "ПриÑтрої, Ð´Ð»Ñ Ñких немає оновлень мікропрограми:" #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "ПриÑтрої із найÑвіжішою доÑтупною верÑією мікропрограми:" #. TRANSLATORS: this is for the device tests msgid "Did not find any devices with matching GUIDs" msgstr "Ðе вдалоÑÑ Ð·Ð½Ð°Ð¹Ñ‚Ð¸ жодного приÑтрою із відповідними GUID" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "Вимкнено" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "Вимикає вказане віддалене Ñховище" #. TRANSLATORS: command description msgid "Disables virtual testing devices" msgstr "Вимикає віртуальні теÑтові приÑтрої" #. TRANSLATORS: the OS the release was tested on msgid "Distribution" msgstr "ДиÑтрибутив" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "Ðе перевірÑти, чи Ñ” заÑтарілі метадані" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "Ðе перевірÑти, чи Ñ” ненадіÑлані звіти у журналі" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "Ðе перевірÑти, чи має бути увімкнено віддалені джерела Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð´Ð°Ð½Ð¸Ñ…" #. TRANSLATORS: command line option msgid "Do not check or prompt for reboot after update" msgstr "Ðе перевірÑти потребу Ñ– не пропонувати перезавантажити ÑиÑтему піÑÐ»Ñ Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "Ðе включати Ð¿Ñ€ÐµÑ„Ñ–ÐºÑ Ð´Ð¾Ð¼ÐµÐ½Ñƒ журналу" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "Ðе включати Ð¿Ñ€ÐµÑ„Ñ–ÐºÑ Ñ‡Ð°Ñової позначки" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "Ðе виконувати перевірок безпечноÑті Ð´Ð»Ñ Ð¿Ñ€Ð¸Ñтрою" #. TRANSLATORS: command line option msgid "Do not prompt for devices" msgstr "Ðе запитувати про приÑтрої" #. TRANSLATORS: command line option msgid "Do not prompt to fix security issues" msgstr "Ðе питати про дії щодо проблем із захиÑтом" #. TRANSLATORS: command line option msgid "Do not search the firmware when parsing" msgstr "Ðе шукати мікропрограму при обробці" #. TRANSLATORS: warning message shown after update has been scheduled msgid "Do not turn off your computer or remove the AC adaptor while the update is in progress." msgstr "Ðе вимикайте ваш комп'ютер Ñ– не виймайте адаптер мережевого живленнÑ, доки Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð½Ðµ буде завершено." #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "Ðе запиÑувати дані до бази даних журналу" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "Чи розумієте ви наÑлідки зміни гілки мікропрограми?" #. TRANSLATORS: ask the user if it's okay to convert, #. * "it" being the data contained in the EFI boot entry msgid "Do you want to convert it now?" msgstr "Хочете виконати Ð¿ÐµÑ€ÐµÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð·Ð°Ñ€Ð°Ð·?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "Хочете вимкнути цю можливіÑть Ð´Ð»Ñ Ð½Ð°Ñтупних оновлень?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "Хочете оÑвіжити це віддалене Ñховище зараз?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "Хочете вивантажувати звіти автоматично Ð´Ð»Ñ Ð½Ð°Ñтупних оновлень?" #. TRANSLATORS: command line option msgid "Don't prompt for authentication (less details may be shown)" msgstr "Ðе проÑити про Ñ€Ð¾Ð·Ð¿Ñ–Ð·Ð½Ð°Ð²Ð°Ð½Ð½Ñ (буде показано менше подробиць)" #. TRANSLATORS: success msgid "Done!" msgstr "Виконано!" #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "Знизити верÑÑ–ÑŽ %s з %s до %s?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "Знижує верÑÑ–ÑŽ мікропрограми на приÑтрої" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "Знижуємо верÑÑ–ÑŽ %s…" #. TRANSLATORS: command description msgid "Download a file" msgstr "Отримати файл" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "Отримуємо дані…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "ЗапиÑати дані SMBIOS з файла" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "ТриваліÑть" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "EMULATION-FILE [ARCHIVE-FILE]" msgstr "ФÐЙЛ-ЕМУЛЯЦІЇ [ФÐЙЛ-ÐРХІВУ]" #. TRANSLATORS: longer description msgid "Each system should have tests to ensure firmware security." msgstr "У кожній ÑиÑтемі мають бути перевірки, Ñкі забезпечують захиÑÑ‚ мікропрограми." #. TRANSLATORS: command description msgid "Emulate a device using a JSON manifest" msgstr "Емулювати приÑтрій за допомогою маніфеÑта JSON" #. TRANSLATORS: this device is not actually real msgid "Emulated" msgstr "Емульовано" #. TRANSLATORS: Title: if we are emulating a different host msgid "Emulated host" msgstr "Емульований вузол" msgid "Enable" msgstr "Увімкнути" msgid "Enable emulation data collection" msgstr "Увімкнути збірку даних емулÑції" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "Увімкнути нове віддалене Ñховище?" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "Увімкнути це віддалене Ñховище?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "Увімкнено" #. TRANSLATORS: Plugin is active only if hardware is found msgid "Enabled if hardware matches" msgstr "Увімкнено, Ñкщо Ð¾Ð±Ð»Ð°Ð´Ð½Ð°Ð½Ð½Ñ Ñ” відповідним" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "Вмикає вказане віддалене Ñховище" #. TRANSLATORS: command description msgid "Enables virtual testing devices" msgstr "Вмикає віртуальні теÑтові приÑтрої" #. TRANSLATORS: longer description msgid "Enabling firmware updates for the BIOS allows fixing security issues." msgstr "Ð’Ð¼Ð¸ÐºÐ°Ð½Ð½Ñ Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½ÑŒ мікропрограми Ð´Ð»Ñ BIOS уможливлює Ð²Ð¸Ð¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ із захиÑтом." msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "ÐаÑлідки Ð²Ð¼Ð¸ÐºÐ°Ð½Ð½Ñ Ñ†Ñ–Ñ”Ñ— можливоÑті покладаютьÑÑ Ð½Ð° ваÑ. Це означає, що уÑÑ– проблеми, пов'Ñзані із цими оновленнÑми, ви маєте вирішувати із виробником обладнаннÑ. ПовідомлÑти розробникам диÑтрибутива за адреÑою $OS_RELEASE:BUG_REPORT_URL$ Ñлід лише про помилки у процеÑÑ– оновленнÑ." #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "Ð’Ð¼Ð¸ÐºÐ°Ð½Ð½Ñ Ñ†ÑŒÐ¾Ð³Ð¾ віддаленого Ñховища виконано під вашу відповідальніÑть." #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "Зашировано" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "Шифрована пам'Ñть" msgid "Encrypted RAM makes it impossible for information that is stored in device memory to be read if the memory chip is removed and accessed." msgstr "Ð¨Ð¸Ñ„Ñ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð¾Ð¿ÐµÑ€Ð°Ñ‚Ð¸Ð²Ð½Ð¾Ñ— пам'Ñті робить неможливим Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ Ñ–Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ—, Ñка зберігаєтьÑÑ Ñƒ пам'Ñті приÑтрою, Ñкщо чіп пам'Ñті вилучено з приÑтрою Ñ– піддано обробці з метою Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð´Ð¾Ñтупу до даних." #. TRANSLATORS: the vendor is no longer supporting the device msgid "End of life" msgstr "Кінець Ñтроку дії" #. TRANSLATORS: The BIOS setting can only be changed to fixed values msgid "Enumeration" msgstr "Перелік" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "Витерти увеÑÑŒ журнал оновлень мікропрограми" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "Витираємо…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "Завершити роботу з невеличкою затримкою" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "Завершити роботу піÑÐ»Ñ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ñ€ÑƒÑˆÑ–Ñ" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "ЕкÑпортувати Ñтруктуру файла мікропрограми до XML" #. TRANSLATORS: command description msgid "Export firmware history for manual upload" msgstr "ЕкÑпортувати журнал мікропрограми Ð´Ð»Ñ Ð²Ð¸Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ð²Ñ€ÑƒÑ‡Ð½Ñƒ" #. TRANSLATORS: command description msgid "Extract a firmware blob to images" msgstr "Видобувати бінарну мікропрограму до образів" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "ФÐЙЛ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "ФÐЙЛ [ІД_ПРИСТРОЮ|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "ÐÐЗВÐ_ФÐЙЛÐ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "ÐÐЗВÐ-ФÐЙЛРСЕРТИФІКÐТ ЗÐКРИТИЙ-КЛЮЧ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "ÐÐЗВÐ_ФÐЙЛРІД_ПРИСТРОЮ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME OFFSET DATA [FIRMWARE-TYPE]" msgstr "ÐÐЗВÐ-ФÐЙЛРВІДСТУП ДÐÐІ [ТИП-МІКРОПРОГРÐМИ]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "ÐÐЗВÐ_ФÐЙЛР[ІД_ПРИСТРОЮ|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "ÐÐЗВÐ-ФÐЙЛР[ТИП-МІКРОПРОГРÐМИ]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" msgstr "ÐÐЗВÐ_ФÐЙЛÐ_ДЖ ÐÐЗВÐ_ФÐЙЛÐ_ПРИЗР[ТИП-МІКРОПРОГРÐМИ-ДЖ] [ТИП-МІКРОПРОГРÐМИ-ПРИЗÐ]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]" msgstr "ÐÐЗВÐ_ФÐЙЛÐ|КОÐТР_СУМÐ1[,КОÐТР_СУМÐ2][,КОÐТР_СУМÐ3]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "Помилка" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "Ðе вдалоÑÑ Ð·Ð°ÑтоÑувати оновленнÑ" #. TRANSLATORS: error message for Windows msgid "Failed to connect to Windows service, please ensure it's running." msgstr "Ðе вдалоÑÑ Ð²Ñтановити з'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð·Ñ– Ñлужбою Windows. Будь лаÑка, переконайтеÑÑ, що Ñ—Ñ— запущено." #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "Ðе вдалоÑÑ Ð²Ñтановити з'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ñ–Ð· фоновою Ñлужбою" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "Ðе вдалоÑÑ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶Ð¸Ñ‚Ð¸ локальний dbx" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "Ðе вдалоÑÑ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶Ð¸Ñ‚Ð¸ ÑиÑтемний dbx" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "Ðе вдалоÑÑ Ð·Ð°Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ñ‚Ð¸" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "Ðе вдалоÑÑ Ð¾Ð±Ñ€Ð¾Ð±Ð¸Ñ‚Ð¸ аргументи" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "Ðе вдалоÑÑ Ð¾Ð±Ñ€Ð¾Ð±Ð¸Ñ‚Ð¸ файл" #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #. TRANSLATORS: the user didn't read the man page, %1 is '--filter' #. TRANSLATORS: the user didn't read the man page, #. * %1 is '--filter-release' #, c-format msgid "Failed to parse flags for %s" msgstr "Ðе вдалоÑÑ Ð¾Ð±Ñ€Ð¾Ð±Ð¸Ñ‚Ð¸ прапорці до %s" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "Ðе вдалоÑÑ Ð¾Ð±Ñ€Ð¾Ð±Ð¸Ñ‚Ð¸ локальний dbx" #. TRANSLATORS: a feature is something like "can show an image" msgid "Failed to set front-end features" msgstr "Ðе вдалоÑÑ Ð²Ñтановити можливоÑті оболонки" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "Ðе вдалоÑÑ Ð²Ñтановити чинніÑть даних ESP" #. TRANSLATORS: item is FALSE msgid "False" msgstr "ÐÑ–" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "Ðазва файла" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "ÐŸÑ–Ð´Ð¿Ð¸Ñ Ð½Ð°Ð·Ð²Ð¸ файла" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "Джерело файла" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "Слід вказати назву файла" #. TRANSLATORS: command line option msgid "Filter with a set of device flags using a ~ prefix to exclude, e.g. 'internal,~needs-reboot'" msgstr "Фільтрувати за набором прапорців приÑтроїв за допомогою префікÑа Ð²Ð¸ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ ~, наприклад «internal,~needs-reboot»" #. TRANSLATORS: command line option msgid "Filter with a set of release flags using a ~ prefix to exclude, e.g. 'trusted-release,~trusted-metadata'" msgstr "Фільтрувати за набором прапорців випуÑку з викориÑтаннÑм префікÑа ~ Ð´Ð»Ñ Ð²Ð¸ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ, приклад: 'trusted-release,~trusted-metadata'" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware Attestation" msgstr "ÐтеÑÑ‚Ð°Ñ†Ñ–Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸" #. TRANSLATORS: longer description msgid "Firmware Attestation checks device software using a reference copy, to make sure that it has not been changed." msgstr "ÐтеÑÑ‚Ð°Ñ†Ñ–Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸ перевірÑÑ” програмне Ð·Ð°Ð±ÐµÐ·Ð¿ÐµÑ‡ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¸Ñтрою за еталонною копією, щоб переконатиÑÑ, що до нього не було внеÑено змін." #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "ДеÑкриптор BIOS мікропрограми" #. TRANSLATORS: longer description msgid "Firmware BIOS Descriptor protects device firmware memory from being tampered with." msgstr "ЗахиÑÑ‚ lдеÑкриптора BIOS захищає пам'Ñть мікропрограми приÑтрою від внеÑÐµÐ½Ð½Ñ Ð·Ð¼Ñ–Ð½." #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "Регіон BIOS мікропрограми" #. TRANSLATORS: longer description msgid "Firmware BIOS Region protects device firmware memory from being tampered with." msgstr "ЗахиÑÑ‚ регіону BIOS захищає пам'Ñть мікропрограми приÑтрою від внеÑÐµÐ½Ð½Ñ Ð·Ð¼Ñ–Ð½." #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "ОÑновна адреÑа мікропрограми" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "Служба D-Bus Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "Служба Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "Перевірка заÑобу Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸" msgid "Firmware Updater Verification checks that software used for updating has not been tampered with." msgstr "Перевірка заÑобу Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸ перевірÑÑ”, чи не було внеÑено зміни до програмного Ð·Ð°Ð±ÐµÐ·Ð¿ÐµÑ‡ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "ЗаÑіб роботи з мікропрограмами" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "ЗахиÑÑ‚ мікропрограми від запиÑу" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "Ð‘Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð·Ð°Ñ…Ð¸Ñту від запиÑу мікропрограми" #. TRANSLATORS: longer description msgid "Firmware Write Protection protects device firmware memory from being tampered with." msgstr "ЗахиÑÑ‚ від запиÑу мікропрограми захищає пам'Ñть мікропрограми приÑтрою від внеÑÐµÐ½Ð½Ñ Ð·Ð¼Ñ–Ð½." #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "ЗаÑÐ²Ñ–Ð´Ñ‡ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "Мікропрограму вже заблоковано" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "Мікропрограму ще не заблоковано" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "Метадані мікропрограми не оновлювалиÑÑ %u день, можливо, вони вже не Ñ” актуальними." msgstr[1] "Метадані мікропрограми не оновлювалиÑÑ %u дні, можливо, вони вже не Ñ” актуальними." msgstr[2] "Метадані мікропрограми не оновлювалиÑÑ %u днів, можливо, вони вже не Ñ” актуальними." msgstr[3] "Метадані мікропрограми не оновлювалиÑÑ %u днів, можливо, вони вже не Ñ” актуальними." #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸" #. TRANSLATORS: user needs to run a command, %1 is 'fwupdmgr unlock' #, c-format msgid "Firmware updates disabled; run '%s' to enable" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸ вимкнено; віддайте команду '%s', щоб Ñ—Ñ… увімкнути" #. TRANSLATORS: command description msgid "Fix a specific host security attribute" msgstr "Виправити певний атрибут захиÑту вузла" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fix reverted successfully" msgstr "Ð’Ð¸Ð¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð½Ñ ÑƒÑпішно ÑкаÑовано" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fixed successfully" msgstr "УÑпішно виправлено" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "Прапорці" #. TRANSLATORS: command line option msgid "Force the action by relaxing some runtime checks" msgstr "Виконати дію примуÑово шлÑхом поÑÐ»Ð°Ð±Ð»ÐµÐ½Ð½Ñ Ð´ÐµÑких перевірок під Ñ‡Ð°Ñ Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "Знайдено" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "ВиÑвлено повне ÑˆÐ¸Ñ„Ñ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð´Ð¸Ñка" #. TRANSLATORS: we might ask the user the recovery key when next booting #. Windows msgid "Full disk encryption secrets may be invalidated when updating" msgstr "Ключі ÑˆÐ¸Ñ„Ñ€ÑƒÐ²Ð°Ð½Ð½Ñ ÑƒÑього диÑка можуть втрати чинніÑть при оновленні" #. TRANSLATORS: Title: if the part has been fused msgid "Fused Platform" msgstr "Злита платформа" #. TRANSLATORS: Title: if the part has been fused msgid "Fused platform" msgstr "Злита платформа" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" msgstr[1] "GUID" msgstr[2] "GUID" msgstr[3] "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "GUID|DEVICE-ID" msgstr "GUID|ІД-ПРИСТРОЮ" msgid "Get BIOS settings" msgstr "ÐžÑ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð¿Ð°Ñ€Ð°Ð¼ÐµÑ‚Ñ€Ñ–Ð² BIOS" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "Отримати уÑÑ– прапорці приÑтроїв, підтримку Ñких передбачено у fwupd" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "Отримати ÑпиÑок уÑÑ–Ñ… приÑтроїв, у Ñких передбачено Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "Отримати ÑпиÑок уÑÑ–Ñ… додатків, Ñкі зареєÑтровано у ÑиÑтемі" #. TRANSLATORS: command description msgid "Get all known version formats" msgstr "Отримати уÑÑ– відомі формати верÑій" #. TRANSLATORS: command description msgid "Get device report metadata" msgstr "Отримати метадані звіту щодо приÑтрою" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "Отримати параметри файла мікропрограми" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "Отримує налаштовані віддалені приÑтрої" #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "Отримує атрибути захиÑту оÑновної ÑиÑтеми" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "Отримує ÑпиÑок Ñхвалених мікропрограм" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "Отримує ÑпиÑок заблокованих мікропрограм" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "Отримує ÑпиÑок оновлень Ð´Ð»Ñ Ð·â€™Ñ”Ð´Ð½Ð°Ð½Ð¾Ð³Ð¾ обладнаннÑ" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "Отримує випуÑки Ð´Ð»Ñ Ð¿Ñ€Ð¸Ñтрою" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "Отримує результати з оÑтаннього оновленнÑ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "ФÐЙЛ-HWIDS" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "ÐžÐ±Ð»Ð°Ð´Ð½Ð°Ð½Ð½Ñ Ð¾Ñ‡Ñ–ÐºÑƒÑ” на повторне з'єднаннÑ" #. TRANSLATORS: the release urgency msgid "High" msgstr "ВиÑокий" #. TRANSLATORS: title for host security events msgid "Host Security Events" msgstr "Події захиÑту вузла" #. TRANSLATORS: error message for unsupported feature #, c-format msgid "Host Security ID (HSI) is not supported" msgstr "Підтримки Host Security ID (HSI) не передбачено" #. TRANSLATORS: success, so say thank you to the user msgid "Host Security ID attributes uploaded successfully, thanks!" msgstr "Ідентифікатор захиÑту вузла уÑпішно оновлено, дÑкуємо!" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "Ід. захиÑту оÑновної ÑиÑтеми:" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX" msgstr "ІÐДЕКС" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX KEY [VALUE]" msgstr "ІÐДЕКС КЛЮЧ [ЗÐÐЧЕÐÐЯ]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX NAME TARGET [MOUNTPOINT]" msgstr "ІÐДЕКС ÐÐЗВРПРИЗÐÐЧЕÐÐЯ [ТОЧКРМОÐТУВÐÐÐЯ]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INDEX1,INDEX2" msgstr "ІÐДЕКС1,ІÐДЕКС2" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "INHIBIT-ID" msgstr "ІД-ЗÐБОРОÐИ" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "ЗахиÑÑ‚ IOMMU" #. TRANSLATORS: longer description msgid "IOMMU Protection prevents connected devices from accessing unauthorized parts of system memory." msgstr "ЗахиÑÑ‚ IOMMU заборонÑÑ” з'єднаним приÑтроÑм доÑтуп до неуповноважених чаÑтин пам'Ñті ÑиÑтеми." #. TRANSLATORS: HSI event title msgid "IOMMU device protection disabled" msgstr "Вимкнено захиÑÑ‚ приÑтрою IOMMU" #. TRANSLATORS: HSI event title msgid "IOMMU device protection enabled" msgstr "Увімкнено захиÑÑ‚ приÑтрою IOMMU" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "БездіÑльніÑть…" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "Ігнорувати результати Ñтрогої перевірки SSL під Ñ‡Ð°Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ñ„Ð°Ð¹Ð»Ñ–Ð²" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "Ігнорувати помилки при перевірці контрольної Ñуми мікропрограми" #. TRANSLATORS: command line option msgid "Ignore firmware hardware mismatch failures" msgstr "Ігнорувати помилки, пов'Ñзані із невідповідніÑтю Ð¾Ð±Ð»Ð°Ð´Ð½Ð°Ð½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ñ–" #. TRANSLATORS: command line option msgid "Ignore non-critical firmware requirements" msgstr "Ігнорувати некритичні вимоги щодо мікропрограм" #. TRANSLATORS: try to help msgid "Ignoring SSL strict checks, to do this automatically in the future export DISABLE_SSL_STRICT in your environment" msgstr "Ігноруємо Ñтрогі перевірки SSL. Щоб зробити Ñ–Ð³Ð½Ð¾Ñ€ÑƒÐ²Ð°Ð½Ð½Ñ Ñƒ майбутньому автоматичним, екÑпортуйте до вашого Ñередовища змінну DISABLE_SSL_STRICT" #. TRANSLATORS: show the user a generic image that can be themed msgid "Image" msgstr "ЗображеннÑ" #. TRANSLATORS: show the user a random image from the internet msgid "Image (custom)" msgstr "Ð—Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ (нетипове)" #. TRANSLATORS: the inhibit ID is a short string like dbus-123456 #, c-format msgid "Inhibit ID is %s." msgstr "Ідентифікатор заборони – %s." #. TRANSLATORS: command description msgid "Inhibit the system to prevent upgrades" msgstr "Заборонити ÑиÑтемі запобігати оновленнÑм" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "ТриваліÑть вÑтановленнÑ" #. TRANSLATORS: command description msgid "Install a firmware file in cabinet format on this hardware" msgstr "Ð’Ñтановити файл мікропрограми у форматі cab на це обладнаннÑ" #. TRANSLATORS: command description msgid "Install a raw firmware blob on a device" msgstr "Ð’Ñтановити проÑту бінарну мікропрограму на приÑтрій" #. TRANSLATORS: command description msgid "Install a specific firmware file on all devices that match" msgstr "Ð’Ñтановити певний файл мікропрограми на уÑÑ– відповідні приÑтрої" #. TRANSLATORS: command description msgid "Install a specific firmware on a device, all possible devices will also be installed once the CAB matches" msgstr "Ð’Ñтановити вказану мікропрограму на приÑтрій. Буде також вÑтановлено на уÑÑ– можливі приÑтрої, щойно буде вÑтановлено відповідніÑть CAB" msgid "Install old version of signed system firmware" msgstr "Ð’Ñтановити Ñтару верÑÑ–ÑŽ підпиÑаної мікропрограми ÑиÑтеми" msgid "Install old version of unsigned system firmware" msgstr "Ð’Ñтановити Ñтару верÑÑ–ÑŽ непідпиÑаної мікропрограми ÑиÑтеми" msgid "Install signed device firmware" msgstr "Ð’Ñтановити підпиÑану мікропрограму приÑтрою" msgid "Install signed system firmware" msgstr "Ð’Ñтановити підпиÑану мікропрограму ÑиÑтеми" msgid "Install unsigned device firmware" msgstr "Ð’Ñтановити непідпиÑану мікропрограму приÑтрою" msgid "Install unsigned system firmware" msgstr "Ð’Ñтановити непідпиÑану мікропрограму ÑиÑтеми" #. TRANSLATORS: stay on one firmware version unless the new version is #. explicitly #. * specified msgid "Installing a specific release is explicitly required" msgstr "Явно вказано вÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð²ÐºÐ°Ð·Ð°Ð½Ð¾Ð³Ð¾ випуÑку" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "Ð’Ñтановлюємо Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸â€¦" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "Ð’Ñтановлюємо на %s…" #. TRANSLATORS: if it breaks, you get to keep both pieces msgid "Installing this update may also void any device warranty." msgstr "Ð’ÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ñ†ÑŒÐ¾Ð³Ð¾ Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ð¾Ð¶Ðµ призвеÑти до Ð¿Ñ€Ð¸Ð¿Ð¸Ð½ÐµÐ½Ð½Ñ Ð±ÑƒÐ´ÑŒ-Ñких гарантійних зобов'Ñзань щодо приÑтрою." #. TRANSLATORS: The BIOS setting only accepts integers in a fixed range msgid "Integer" msgstr "Ціле чиÑло" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM Protected" msgstr "ЗахиÑÑ‚ ACM Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "Intel BootGuard захищено ACM" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard Error Policy" msgstr "Правила обробки помилок Intel BootGuard" msgid "Intel BootGuard Error Policy ensures the device does not continue to start if its device software has been tampered with." msgstr "Правила обробки помилок Intel BootGuard запобігають продовженню запуÑку приÑтрою, Ñкщо до програмного Ð·Ð°Ð±ÐµÐ·Ð¿ÐµÑ‡ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¸Ñтрою було внеÑено зміни." #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard Fuse" msgstr "Intel BootGuard Fuse" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "OTP FUSE Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard Verified Boot" msgstr "Перевірене Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Правила обробки помилок Intel BootGuard" #. TRANSLATORS: longer description msgid "Intel BootGuard prevents unauthorized device software from operating when the device is started." msgstr "Intel BootGuard запобігає роботі неуповноваженого програмного Ð·Ð°Ð±ÐµÐ·Ð¿ÐµÑ‡ÐµÐ½Ð½Ñ Ð½Ð° приÑтрої піÑÐ»Ñ Ð¹Ð¾Ð³Ð¾ запуÑку." #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Перевірене Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Intel BootGuard" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS Mitigation" msgstr "Обхід Intel GDS" #. TRANSLATORS: Title: GDS is where the CPU leaks information msgid "Intel GDS mitigation" msgstr "Обхід Intel GDS" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Manufacturing Mode" msgstr "Режим виробництва Intel Management Engine" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the "override" is #. enabled #. * with a jumper -- luckily it is probably not accessible to end users on #. consumer #. * boards msgid "Intel Management Engine Override" msgstr "ÐŸÐµÑ€ÐµÐ²Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Intel Management Engine" #. TRANSLATORS: longer description msgid "Intel Management Engine Override disables checks for device software tampering." msgstr "ÐŸÐµÑ€ÐµÐ²Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Intel Management Engine вимикає перевірки щодо внеÑÐµÐ½Ð½Ñ Ð·Ð¼Ñ–Ð½ до програмного Ð·Ð°Ð±ÐµÐ·Ð¿ÐµÑ‡ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¸Ñтрою." #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Version" msgstr "ВерÑÑ–Ñ Intel Management Engine" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "Внутрішній приÑтрій" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "Ðекоректна" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "Ðекоректні аргументи" #. TRANSLATORS: error message msgid "Invalid arguments, expected GUID" msgstr "Ðекоректні аргументи, мало бути вказано GUID" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX KEY [VALUE]" msgstr "Ðекоректні аргументи, мало бути ІÐДЕКС КЛЮЧ [ЗÐÐЧЕÐÐЯ]" #. TRANSLATORS: error message msgid "Invalid arguments, expected INDEX NAME TARGET [MOUNTPOINT]" msgstr "Ðекоректні аргументи, мало бути ІÐДЕКС ÐÐЗВРПРИЗÐÐЧЕÐÐЯ [ТОЧКРМОÐТУВÐÐÐЯ]" #. TRANSLATOR: This is the error message for #. * incorrect parameter msgid "Invalid arguments, expected an AppStream ID" msgstr "Ðекоректні аргументи, мало бути вказано ідентифікатор AppStream" #. TRANSLATORS: error message msgid "Invalid arguments, expected at least ARCHIVE FIRMWARE METAINFO" msgstr "Ðекоректні аргументи. Мало бути вказано принаймні ÐРХІВ МІКРОПРОГРÐМРМЕТÐІÐФО" #. TRANSLATORS: error message msgid "Invalid arguments, expected base-16 integer" msgstr "Ðекоректні аргументи, мало бути вказано ціле чиÑло за оÑновою 16" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "Є Ñтарішою верÑією" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "Перебуває у режимі завантажувача" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "Є оновленнÑм" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "Вада" msgstr[1] "Вади" msgstr[2] "Вади" msgstr[3] "Вада" #. TRANSLATORS: HSI event title msgid "Kernel is no longer tainted" msgstr "Ядро більше не викориÑтовує Ñторонні модулі" #. TRANSLATORS: HSI event title msgid "Kernel is tainted" msgstr "Ядро викориÑтовує Ñторонні модулі" #. TRANSLATORS: HSI event title msgid "Kernel lockdown disabled" msgstr "Вимкнено Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ñдра" #. TRANSLATORS: HSI event title msgid "Kernel lockdown enabled" msgstr "Увімкнено Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ñдра" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "МІСЦЕ" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "ВоÑтаннє змінено" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "ЛишилоÑÑ Ð¼ÐµÐ½ÑˆÐµ за хвилину" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "ЛіцензуваннÑ" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "Ð‘Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ñдра Linux" #. TRANSLATORS: longer description msgid "Linux Kernel Lockdown mode prevents administrator (root) accounts from accessing and changing critical parts of system software." msgstr "Режим Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ñдра Linux заборонÑÑ” адмініÑтративним обліковим запиÑам (root) доÑтуп до критичних чаÑтин ÑиÑтеми Ñ– внеÑÐµÐ½Ð½Ñ Ð´Ð¾ них змін." msgid "Linux Kernel Swap temporarily saves information to disk as you work. If the information is not protected, it could be accessed by someone if they obtained the disk." msgstr "Резервна пам'Ñть Ñдра Linux зберігає на диÑку дані під Ñ‡Ð°Ñ Ñ€Ð¾Ð±Ð¾Ñ‚Ð¸ ÑиÑтеми. Якщо дані не захищено, хтоÑÑŒ, хто отримає диÑк, може отримати до них доÑтуп." #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "Перевірка Ñдра Linux" msgid "Linux Kernel Verification makes sure that critical system software has not been tampered with. Using device drivers which are not provided with the system can prevent this security feature from working correctly." msgstr "Перевірка Ñдра Linux захищає критичне програмне Ð·Ð°Ð±ÐµÐ·Ð¿ÐµÑ‡ÐµÐ½Ð½Ñ ÑиÑтеми від внеÑÐµÐ½Ð½Ñ Ð·Ð¼Ñ–Ð½. ВикориÑÑ‚Ð°Ð½Ð½Ñ Ð´Ñ€Ð°Ð¹Ð²ÐµÑ€Ñ–Ð² приÑтроїв, Ñкі не Ñ” чаÑтиною ÑиÑтеми, може заважати належній роботі цієї можливоÑті захиÑту." #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "Резервна пам'Ñть Linux" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Служба Ð½Ð°Ð´Ð°Ð½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼ Ð´Ð»Ñ Linux від виробника (Ñтабільна мікропрограма)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Служба Ð½Ð°Ð´Ð°Ð½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼ Ð´Ð»Ñ Linux від виробника (теÑтова мікропрограма)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Ядро Linux" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Ð‘Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ñдра Linux" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Рез. пам'Ñть Linux" #. TRANSLATORS: command description msgid "List EFI boot files" msgstr "ВивеÑти ÑпиÑок файлів Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ EFI" #. TRANSLATORS: command description msgid "List EFI boot parameters" msgstr "ВивеÑти ÑпиÑок параметрів Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ EFI" #. TRANSLATORS: command description msgid "List EFI variables with a specific GUID" msgstr "ВивеÑти ÑпиÑок змінних EFI із певним GUID" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "ВивеÑти ÑпиÑок запиÑів у dbx" #. TRANSLATORS: command description msgid "List the available firmware GTypes" msgstr "ВивеÑти ÑпиÑок доÑтупних GType мікропрограми" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "ВивеÑти ÑпиÑок доÑтупних типів мікропрограм" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "Виводить ÑпиÑок файлів у ESP" #. TRANSLATORS: command description msgid "Load device emulation data" msgstr "Завантажити дані емулÑції приÑтрою" #. TRANSLATORS: the plugin was created from a .so object, and was not built-in msgid "Loaded from an external module" msgstr "Завантажено із зовнішнього модулÑ" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "ЗавантаженнÑ…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "Заблоковано" #. TRANSLATORS: the release urgency msgid "Low" msgstr "Ðизький" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refers #. * to the private/public key used to secure loading of firmware msgid "MEI Key Manifest" msgstr "МаніфеÑÑ‚ ключа MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine, and key refer #. * to the private/public key used to secure loading of firmware msgid "MEI key manifest" msgstr "МаніфеÑÑ‚ ключа MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "Режим виробництва MEI" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "ÐŸÐµÑ€ÐµÐ²Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ MEI" msgid "MEI version" msgstr "ВерÑÑ–Ñ MEI" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "Увімкнути певні додатки вручну" #. TRANSLATORS: longer description msgid "Manufacturing Mode is used when the device is manufactured and security features are not yet enabled." msgstr "Режим виробництва викориÑтовують піÑÐ»Ñ Ð²Ð¸Ñ€Ð¾Ð±Ð»ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¸Ñтрою, коли можливоÑті захиÑту ще не увімкнено." #. TRANSLATORS: Longest valid string for BIOS setting msgid "Maximum length" msgstr "МакÑимальна довжина" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "МакÑимальне значеннÑ" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "Середній" #. TRANSLATORS: ask the user to do a simple task which should be translated msgid "Message" msgstr "ПовідомленнÑ" #. TRANSLATORS: ask the user a question, and it will not be translated msgid "Message (custom)" msgstr "ÐŸÐ¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ (нетипове)" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "ÐŸÑ–Ð´Ð¿Ð¸Ñ Ð¼ÐµÑ‚Ð°Ð´Ð°Ð½Ð¸Ñ…" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "ÐдреÑа метаданих" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "Метадані можна отримати від Ñлужби Ð½Ð°Ð´Ð°Ð½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼ Ð´Ð»Ñ Linux від виробника." #. TRANSLATORS: error message for a user who ran fwupdmgr #. * refresh recently -- %1 is '--force' #, c-format msgid "Metadata is up to date; use %s to refresh again." msgstr "Метадані Ñ” актуальними; ÑкориÑтайтеÑÑ %s Ð´Ð»Ñ Ð¾ÑÐ²Ñ–Ð¶ÐµÐ½Ð½Ñ Ð²Ñ–Ð´Ð¾Ð¼Ð¾Ñтей." #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "Мінімальна верÑÑ–Ñ" #. TRANSLATORS: Shortest valid string for BIOS setting msgid "Minimum length" msgstr "Мінімальна довжина" #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "Мінімальне значеннÑ" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "Змінює Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½ÑŒ фонової Ñлужби" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "Змінює вказаний Ð·Ð°Ð¿Ð¸Ñ Ð²Ñ–Ð´Ð´Ð°Ð»ÐµÐ½Ð¾Ð³Ð¾ приÑтрою" msgid "Modify a configured remote" msgstr "Зміна налаштованих віддалених приÑтроїв" msgid "Modify daemon configuration" msgstr "Зміна налаштувань фонової Ñлужби" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "Стежити за подіÑми у фоновій Ñлужбі" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "Монтує ESP" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "Потребує Ð¿ÐµÑ€ÐµÐ·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ð¿Ñ–ÑÐ»Ñ Ð²ÑтановленнÑ" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "Потребує перезавантаженнÑ" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "Потребує Ð²Ð¸Ð¼Ð¸ÐºÐ°Ð½Ð½Ñ Ð¿Ñ–ÑÐ»Ñ Ð²ÑтановленнÑ" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "Ðова верÑÑ–Ñ" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "Ðе вказано дії!" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "Ðемає знижень верÑÑ–Ñ— Ð´Ð»Ñ %s" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "Ðе знайдено ідентифікаторів мікропрограм" #. TRANSLATORS: nothing found msgid "No firmware found" msgstr "Ðе знайдено мікропрограми" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "Ðе виÑвлено Ð¾Ð±Ð»Ð°Ð´Ð½Ð°Ð½Ð½Ñ Ñ–Ð· передбаченою можливіÑтю Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "Ðемає доÑтупних випуÑків" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "Зараз не увімкнено жодного віддаленого Ñховища, отже, метадані недоÑтупні." #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "Ðемає доÑтупних віддалених Ñховищ" #. TRANSLATORS: this is an error string msgid "No updatable devices" msgstr "Ðемає придатних до Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¸Ñтроїв" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "Ðемає доÑтупних оновлень" msgid "No updates available for remaining devices" msgstr "Ð”Ð»Ñ Ñ€ÐµÑˆÑ‚Ð¸ приÑтроїв немає доÑтупних оновлень" #. TRANSLATORS: error message #, c-format msgid "No volume matched %s" msgstr "Ðемає відповідного тому %s" #. TRANSLATORS: version cannot be installed due to policy msgid "Not approved" msgstr "Ðе затверджено" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "Ðе знайдено" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "Ðемає підтримки" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "Гаразд" #. TRANSLATORS: this is for the device tests msgid "OK!" msgstr "Гаразд!" #. TRANSLATORS: the firmware old version msgid "Old version" msgstr "Стара верÑÑ–Ñ" #. TRANSLATORS: command line option msgid "Only install onto emulated devices" msgstr "Ð’Ñтановити лише на емульовані приÑтрої" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "Показувати лише одне Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ PCR" #. TRANSLATORS: command line option msgid "Only use peer-to-peer networking when downloading files" msgstr "ВикориÑтовувати лише рівнорангові з'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð¼ÐµÑ€ÐµÐ¶Ñ– при отриманні файлів" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "Дозволено лише Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð²ÐµÑ€ÑÑ–Ñ—" #. TRANSLATORS: command line option msgid "Output in JSON format (disables all interactive prompts)" msgstr "ВивеÑти дані у форматі JSON (вимикає уÑÑ– інтерактивні запити)" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "Перевизначити типовий шлÑÑ… до ESP" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Firmware" msgstr "Мікропрограма P2P" #. TRANSLATORS: if we can get metadata from peer-to-peer clients msgid "P2P Metadata" msgstr "Метадані P2P" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "ШЛЯХ" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "Обробити Ñ– показати параметри файла мікропрограми" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "ОброблÑємо Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ dbx…" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "ОброблÑємо ÑиÑтемний dbx…" #. TRANSLATORS: remote filename base msgid "Password" msgstr "Пароль" #. TRANSLATORS: command description msgid "Patch a firmware blob at a known offset" msgstr "ÐаклаÑти латку на бінарний файл мікропрограми за відомим відÑтупом" msgid "Payload" msgstr "ВміÑÑ‚" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "У черзі" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "Виконати дію?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "ДіагноÑтика платформи" #. TRANSLATORS: longer description msgid "Platform Debugging allows device security features to be disabled. This should only be used by hardware manufacturers." msgstr "ДіагноÑтика платформи надає змогу вимикати можливоÑті захиÑту приÑтрою. Цією можливіÑтю мають кориÑтуватиÑÑ Ð»Ð¸ÑˆÐµ виробники обладнаннÑ." #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform debugging" msgstr "ДіагноÑтика платформи" #. TRANSLATORS: 'recovery key' here refers to a code, rather than a physical #. metal thing msgid "Please ensure you have the volume recovery key before continuing." msgstr "Будь лаÑка, переконайтеÑÑ, що у Ð²Ð°Ñ Ñ” ключ Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ñ‚Ð¾Ð¼Ñƒ, перш ніж продовжувати обробку." #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "Будь лаÑка, введіть чиÑло від 0 до %u: " #. TRANSLATORS: the user isn't reading the question -- %1 is 'Y' and %2 is #. * 'N' #, c-format msgid "Please enter either %s or %s: " msgstr "Будь лаÑка, введіть %s або %s: " #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "Ðе вÑтановлено залежноÑті додатка" #. TRANSLATORS: The plugin is only for testing msgid "Plugin is only for testing" msgstr "Додаток призначено лише Ð´Ð»Ñ Ñ‚ÐµÑтуваннÑ" #. TRANSLATORS: Possible values for a bios setting msgid "Possible Values" msgstr "Можливі значеннÑ" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA Protection" msgstr "ЗахиÑÑ‚ DMA до завантаженнÑ" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "ЗахиÑÑ‚ DMA до завантаженнÑ" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is disabled" msgstr "ЗахиÑÑ‚ DMA перед завантаженнÑм вимкнено" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is enabled" msgstr "ЗахиÑÑ‚ DMA перед завантаженнÑм увімкнено" #. TRANSLATORS: longer description msgid "Pre-boot DMA protection prevents devices from accessing system memory after being connected to the computer." msgstr "ЗахиÑÑ‚ DMA перед завантаженнÑм запобігає доÑтупу з боку приÑтроїв до ÑиÑтемної пам'Ñті піÑÐ»Ñ Ð²ÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð·'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¸Ñтроїв з комп'ютером." #. TRANSLATORS: warning message msgid "Press unlock on the device to continue the update process." msgstr "Будь лаÑка, розблокуйте приÑтрій, щоб продовжити процедуру оновленнÑ." #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "ÐŸÐ¾Ð¿ÐµÑ€ÐµÐ´Ð½Ñ Ð²ÐµÑ€ÑÑ–Ñ" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "ПріоритетніÑть" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "Проблеми" msgid "Proceed with upload?" msgstr "Продовжити вивантаженнÑ?" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Processor Security Checks" msgstr "Перевірки захиÑту процеÑора" #. TRANSLATORS: Title: if firmware enforces rollback protection msgid "Processor rollback protection" msgstr "ЗахиÑÑ‚ від Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¿Ð¾Ð¿ÐµÑ€ÐµÐ´Ð½Ñ–Ñ… Ñтанів процеÑора" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "Пропрієтарна" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "ВІДДÐЛ-ІД" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID KEY VALUE" msgstr "ВІДДÐЛ-ІД КЛЮЧ ЗÐÐЧЕÐÐЯ" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "Лише читаннÑ" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "Прочитати бінарну мікропрограму з приÑтрою" #. TRANSLATORS: command description msgid "Read a firmware from a device" msgstr "Прочитати мікропрограму з приÑтрою" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "Читаємо з %s…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "Читаємо…" #. TRANSLATORS: Plugin is active and in use msgid "Ready" msgstr "Готово" #. TRANSLATORS: how often we should refresh the metadata msgid "Refresh Interval" msgstr "Інтервал оновленнÑ" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "Оновити метадані з віддаленого Ñервера" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "Повторно вÑтановити %s до %s?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "Повторно вÑтановити мікропрограму на приÑтрій" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "Повторно вÑтановити мікропрограму на приÑтрій" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "Гілка випуÑків" #. TRANSLATORS: release attributes msgid "Release Flags" msgstr "Прапорці випуÑку" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "Ідентифікатор випуÑку" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "Віддалений ідентифікатор" #. TRANSLATORS: command description msgid "Removes devices to watch for future emulation" msgstr "Вилучає приÑтрої зі ÑпоÑÑ‚ÐµÑ€ÐµÐ¶ÐµÐ½Ð½Ñ Ñ‰Ð¾Ð´Ð¾ майбутньої емулÑції" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "ÐдреÑа звіту" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "Повідомлено на віддаленому Ñервері" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "Ðе знайдено відповідної файлової ÑиÑтеми efivarfs" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "Ðе знайдено відповідного обладнаннÑ" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "Потребує завантажувача" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "Потребує з'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ñ–Ð· інтернетом" msgid "Reset daemon configuration" msgstr "Ð’Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¿Ð¾Ñ‡Ð°Ñ‚ÐºÐ¾Ð²Ð¸Ñ… налаштувань фонової Ñлужби" #. TRANSLATORS: sets something in the daemon configuration file msgid "Resets a daemon configuration section" msgstr "Скидає розділ налаштувань фонової Ñлужби до типових значень" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "Перезавантажити зараз?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "ПерезапуÑтити фонову Ñлужбу, щоб зміни набули чинноÑті?" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "ПерезапуÑкаємо приÑтрій…" #. TRANSLATORS: command description msgid "Retrieve BIOS settings. If no arguments are passed all settings are returned" msgstr "Отримати параметри BIOS. Якщо аргументів не передано, буде повернуто уÑÑ– параметри" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "Повернути уÑÑ– ідентифікатори апаратного Ð·Ð°Ð±ÐµÐ·Ð¿ÐµÑ‡ÐµÐ½Ð½Ñ ÐºÐ¾Ð¼Ð¿â€™ÑŽÑ‚ÐµÑ€Ð°" #. TRANSLATORS: ask the user to upload msgid "Review and upload report now?" msgstr "ПереглÑнути Ñ– вивантажити звіт зараз?" #. TRANSLATORS: longer description msgid "Rollback Protection prevents device software from being downgraded to an older version that has security problems." msgstr "ЗахиÑÑ‚ від Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¿Ð¾Ð¿ÐµÑ€ÐµÐ´Ð½Ñ–Ñ… верÑій запобігає зниженню верÑій програмного Ð·Ð°Ð±ÐµÐ·Ð¿ÐµÑ‡ÐµÐ½Ð½Ñ Ð´Ð¾ тих, у Ñких виÑвлено проблеми із захиÑтом." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr get-upgrades` #, c-format msgid "Run `%s` for more information." msgstr "Віддайте команду «%s», щоб дізнатиÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ." #. TRANSLATORS: this is shown in the MOTD -- %1 is the #. * command name, e.g. `fwupdmgr sync` #, c-format msgid "Run `%s` to complete this action." msgstr "Віддайте команду «%s», щоб завершити цю дію." #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "ЗапуÑтити процедуру Ñ‡Ð¸Ñ‰ÐµÐ½Ð½Ñ ÐºÐ¾Ð¼Ð¿Ð¾Ð·Ð¸Ñ†Ñ–Ñ— додатків при викориÑтанні install-blob" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "ЗапуÑтити процедуру Ð¿Ñ€Ð¸Ð³Ð¾Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ð¼Ð¿Ð¾Ð·Ð¸Ñ†Ñ–Ñ— додатків при викориÑтанні install-blob" #. TRANSLATORS: command description msgid "Run the post-reboot cleanup action" msgstr "Виконати дію з Ñ‡Ð¸Ñ‰ÐµÐ½Ð½Ñ Ð¿Ñ–ÑÐ»Ñ Ð¿ÐµÑ€ÐµÐ·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ" #. TRANSLATORS: tell a user how to get information #, c-format msgid "Run without '%s' to see" msgstr "ЗапуÑтіть без «%s», щоб переглÑнути" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "Поточне Ñдро Ñ” надто заÑтарілим" #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "Динамічний ÑуфікÑ" #. TRANSLATORS: Software Bill of Materials link msgid "SBOM" msgstr "SBOM" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SECTION" msgstr "РОЗДІЛ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING VALUE" msgstr "ПÐРÐМЕТР ЗÐÐЧЕÐÐЯ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SETTING1 VALUE1 [SETTING2] [VALUE2]" msgstr "ПÐРÐМЕТР1 ЗÐÐЧЕÐÐЯ1 [ПÐРÐМЕТР2] [ЗÐÐЧЕÐÐЯ2]" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "SMAP" msgstr "SMAP" #. TRANSLATORS: Title: Whether firmware is locked down msgid "SMM locked down" msgstr "SMM заблоковано" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "ДеÑкриптор SPI BIOS" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "Регіон BIOS SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "Ð‘Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ SPI" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "SPI replay protection" msgstr "ЗахиÑÑ‚ від Ð²Ñ–Ð´Ñ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ SPI" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "Ð—Ð°Ð¿Ð¸Ñ SPI" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "SPI write protection" msgstr "ЗахиÑÑ‚ від запиÑу SPI" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "ПІДСИСТЕМРДРÐЙВЕР [ІД_ПРИСТРОЮ|GUID]" #. TRANSLATORS: command description msgid "Save a file that allows generation of hardware IDs" msgstr "Зберегти файл, за допомогою Ñкого можна буде Ñтворити апаратні ідентифікатори" #. TRANSLATORS: command description msgid "Save device emulation data" msgstr "Зберегти дані емулÑції приÑтрою" #. TRANSLATORS: key for a offline report filename msgid "Saved report" msgstr "Збережений звіт" #. TRANSLATORS: Scalar increment for integer BIOS setting msgid "Scalar Increment" msgstr "СкалÑрний крок" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "Плануємо…" #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "Secure Boot вимкнено" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "Увімкнено Secure Boot" msgid "Security hardening for HSI" msgstr "Ð—Ð¼Ñ–Ñ†Ð½ÐµÐ½Ð½Ñ Ð·Ð°Ñ…Ð¸Ñту Ð´Ð»Ñ HSI" #. TRANSLATORS: the %1 is a URL to a wiki page #, c-format msgid "See %s for more details." msgstr "Див. %s, щоб дізнатиÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ." #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "Див. %s, щоб дізнатиÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ." #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "Вибрано приÑтрій" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "Вибраний том" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "Серійний номер" #. TRANSLATORS: Configured a BIOS setting to a value #, c-format msgid "Set BIOS setting '%s' using '%s'." msgstr "Ð’Ñтановити параметр BIOS «%s» за допомогою «%s»." #. TRANSLATORS: command description msgid "Set a BIOS setting" msgstr "Ð’Ñтановити параметр BIOS" msgid "Set one or more BIOS settings" msgstr "Ð’Ñтановити один або декілька параметрів BIOS" #. TRANSLATORS: command description msgid "Set or remove an EFI boot hive entry" msgstr "Ð’Ñтановити або вилучити Ð·Ð°Ð¿Ð¸Ñ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ hive EFI" #. TRANSLATORS: command description msgid "Set the EFI boot next" msgstr "Ð’Ñтановити наÑтупне Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ EFI" #. TRANSLATORS: command description msgid "Set the EFI boot order" msgstr "Ð’Ñтановити порÑдок Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ EFI" #. TRANSLATORS: command line option msgid "Set the download retries for transient errors" msgstr "Ð’Ñтановити кількіÑть повторних Ñпроб Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¸ тимчаÑових помилках" #. TRANSLATORS: command description msgid "Sets one or more BIOS settings" msgstr "Ð’Ñтановити один або декілька параметрів BIOS" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "Ð’ÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ ÑпиÑку Ñхвалених мікропрограм" #. TRANSLATORS: type of BIOS setting msgid "Setting type" msgstr "Тип параметра" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "Параметри буде заÑтоÑовано піÑÐ»Ñ Ð¿ÐµÑ€ÐµÐ·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ ÑиÑтеми" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "ПоділитиÑÑ Ð¶ÑƒÑ€Ð½Ð°Ð»Ð¾Ð¼ оновлень із розробниками" #. TRANSLATORS: command line option msgid "Show all results" msgstr "Показати уÑÑ– результати" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "ВивеÑти дані щодо верÑій клієнат Ñ– фонової Ñлужби" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "Показати докладні дані фонової Ñлужби Ð´Ð»Ñ Ð¿ÐµÐ²Ð½Ð¾Ð³Ð¾ домену" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "Показати діагноÑтичні дані Ð´Ð»Ñ ÑƒÑÑ–Ñ… доменів" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "Показувати параметри діагноÑтики" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "Показати приÑтрої, Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ñких неможливе" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "Показати додаткові діагноÑтичні дані" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "Показати журнал оновлень мікропрограми" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "ВивеÑти обчиÑлену верÑÑ–ÑŽ dbx" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "Вимкнути зараз?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "ПідпиÑати мікропрограму новим ключем" msgid "Sign data using the client certificate" msgstr "ПідпиÑÑƒÐ²Ð°Ð½Ð½Ñ Ð´Ð°Ð½Ð¸Ñ… клієнтÑьким Ñертифікатом" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "ПідпиÑÑƒÐ²Ð°Ð½Ð½Ñ Ð´Ð°Ð½Ð¸Ñ… клієнтÑьким Ñертифікатом" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "ПідпиÑати вивантажені дані клієнтÑьким Ñертифікатом" msgid "Signature" msgstr "ПідпиÑ" #. TRANSLATORS: firmware is verified on-device the payload using strong crypto msgid "Signed Payload" msgstr "ПідпиÑаний вміÑÑ‚" #. TRANSLATORS: file size of the download msgid "Size" msgstr "Розмір" #. TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM msgid "Some of the platform secrets may be invalidated when updating this firmware." msgstr "Якщо оновити цю мікропрограму, деÑкі ключі платформи можуть втратити чинніÑть." #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "Джерело" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "Вказати файл бази даних dbx" msgid "Stop the fwupd service" msgstr "Зупинити Ñлужбу fwupd" #. TRANSLATORS: The BIOS setting accepts strings msgid "String" msgstr "РÑдок" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "УÑпіх" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "УÑпішно активовано уÑÑ– приÑтрої" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "УÑпішно вимкнено Ð·Ð°Ð¿Ð¸Ñ Ð²Ñ–Ð´Ð´Ð°Ð»ÐµÐ½Ð¾Ð³Ð¾ Ñховища" #. TRANSLATORS: comment explaining result of command msgid "Successfully disabled test devices" msgstr "УÑпішно вимкнено теÑтові приÑтрої" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "УÑпішно отримано нові метадані:" #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "УÑпішно увімкнено Ñ– оÑвіжено віддалене Ñховище" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "УÑпішно увімкнено Ð·Ð°Ð¿Ð¸Ñ Ð²Ñ–Ð´Ð´Ð°Ð»ÐµÐ½Ð¾Ð³Ð¾ Ñховища" #. TRANSLATORS: comment explaining result of command msgid "Successfully enabled test devices" msgstr "УÑпішно увімкнено теÑтові приÑтрої" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "Мікропрограму уÑпішно вÑтановлено" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "УÑпішно змінено Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ñƒ налаштуваннÑÑ…" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "УÑпішно змінено віддалене Ñховище" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "Метадані уÑпішно оновлено вручну" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration section" msgstr "УÑпішно Ñкинуто розділ налаштувань" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully reset configuration values" msgstr "УÑпішно Ñкинуто Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½ÑŒ" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "УÑпішно оновлено контрольні Ñуми приÑтрою" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "УÑпішно вивантажено %u звіт" msgstr[1] "УÑпішно вивантажено %u звіти" msgstr[2] "УÑпішно вивантажено %u звітів" msgstr[3] "УÑпішно вивантажено %u звіт" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "УÑпішно перевірено контрольні Ñуми приÑтрою" #. TRANSLATORS: the device showed up in time #, c-format msgid "Successfully waited %.0fms for device" msgstr "УÑпішне Ð¾Ñ‡Ñ–ÐºÑƒÐ²Ð°Ð½Ð½Ñ %.0fÐ¼Ñ Ð½Ð° приÑтрій" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "Резюме" #. TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention msgid "Supervisor Mode Access Prevention" msgstr "Ð—Ð°Ð¿Ð¾Ð±Ñ–Ð³Ð°Ð½Ð½Ñ Ð´Ð¾Ñтупу до режиму наглÑдача" #. TRANSLATORS: longer description msgid "Supervisor Mode Access Prevention ensures critical parts of device memory are not accessed by less secure programs." msgstr "Supervisor Mode Access Prevention заборонÑÑ” малобезпечним програмам доÑтуп до пам'Ñті приÑтрою." #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "Є підтримка" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Supported CPU" msgstr "Підтримувані процеÑори" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "Підтримуваний на віддаленому Ñервері" #. TRANSLATORS: Title: a better sleep state msgid "Suspend To Idle" msgstr "ПриÑиплÑÐ½Ð½Ñ Ð±ÐµÐ·Ð´Ñ–ÑльноÑті" #. TRANSLATORS: Title: sleep state msgid "Suspend To RAM" msgstr "ÐŸÑ€Ð¸Ð·ÑƒÐ¿Ð¸Ð½ÐµÐ½Ð½Ñ Ð´Ð¾ RAM" #. TRANSLATORS: longer description msgid "Suspend to Idle allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "ÐŸÑ€Ð¸Ð·ÑƒÐ¿Ð¸Ð½ÐµÐ½Ð½Ñ Ð´Ð¾ Ñтану бездіÑльноÑті надає змогу приÑтрою швидко перейти у Ñтан Ñну Ð´Ð»Ñ Ð·Ð°Ð¾Ñ‰Ð°Ð´Ð¶ÐµÐ½Ð½Ñ ÐµÐ»ÐµÐºÑ‚Ñ€Ð¾ÐµÐ½ÐµÑ€Ð³Ñ–Ñ—. Доки приÑтрій перебуває у Ñтані призупиненнÑ, його пам'Ñть може бути фізично видалено із отриманнÑм доÑтупу до даних у пам'Ñті." #. TRANSLATORS: longer description msgid "Suspend to RAM allows the device to quickly go to sleep in order to save power. While the device has been suspended, its memory could be physically removed and its information accessed." msgstr "ÐŸÑ€Ð¸Ð·ÑƒÐ¿Ð¸Ð½ÐµÐ½Ð½Ñ Ñ–Ð· запиÑом до оперативної пам'Ñті надає змогу приÑтрою швидко перейти у Ñтан Ñну Ð´Ð»Ñ Ð·Ð°Ð¾Ñ‰Ð°Ð´Ð¶ÐµÐ½Ð½Ñ ÐµÐ»ÐµÐºÑ‚Ñ€Ð¾ÐµÐ½ÐµÑ€Ð³Ñ–Ñ—. Доки приÑтрій перебуває у Ñтані призупиненнÑ, його пам'Ñть може бути фізично видалено із отриманнÑм доÑтупу до даних у пам'Ñті." #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "ПриÑиплÑÐ½Ð½Ñ Ð±ÐµÐ·Ð´Ñ–ÑльноÑті" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "ПриÑиплÑÐ½Ð½Ñ Ð´Ð¾ пам'Ñті" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "ПеремкнутиÑÑ Ð· гілки %s на %s?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "Перемкнути гілку мікропрограми на приÑтрої" #. TRANSLATORS: command description msgid "Sync firmware versions to the chosen configuration" msgstr "Синхронізувати верÑÑ–Ñ— мікропрограм із вибраною конфігурацією" #. TRANSLATORS: Title: Whether firmware is locked down msgid "System Management Mode" msgstr "Режим ÐºÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ ÑиÑтемою" #. TRANSLATORS: this CLI tool is now preventing system updates msgid "System Update Inhibited" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ ÑиÑтеми заборонено" #. TRANSLATORS: longer description msgid "System management mode is used by the firmware to access resident BIOS code and data." msgstr "Режим ÐºÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ ÑиÑтемою викориÑтовуєтьÑÑ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¾ÑŽ Ð´Ð»Ñ Ð´Ð¾Ñтупу до поточного коду BIOS та даних." #. TRANSLATORS: as in laptop battery power msgid "System power is too low" msgstr "Ð–Ð¸Ð²Ð»ÐµÐ½Ð½Ñ ÑиÑтеми Ñ” надто низьким" #. TRANSLATORS: as in laptop battery power #, c-format msgid "System power is too low (%u%%, requires %u%%)" msgstr "Рівень Ð¶Ð¸Ð²Ð»ÐµÐ½Ð½Ñ ÑиÑтеми Ñ” надто низьким (%u%%, потрібно %u%%)" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "СиÑтема потребує зовнішнього джерела живленнÑ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "ТЕКСТ" #. TRANSLATORS: longer description msgid "TPM (Trusted Platform Module) is a computer chip that detects when hardware components have been tampered with." msgstr "TPM (Trusted Platform Module або довірений модуль платформи) Ñ” комп'ютерним чіпом, Ñкий виÑвлÑÑ” зміни в апаратних компонентах." #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "Ð’Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ PCR0 TPM" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is invalid" msgstr "Ð’Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ PCR0 TPM Ñ” некоректним" #. TRANSLATORS: HSI event title msgid "TPM PCR0 reconstruction is now valid" msgstr "Ð’Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ PCR0 TPM тепер Ñ” коректним" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM Platform Configuration" msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð»Ð°Ñ‚Ñ„Ð¾Ñ€Ð¼Ð¸ TPM" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM Reconstruction" msgstr "Ð’Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ TPM" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM empty PCRs" msgstr "Порожні PCR TPM" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM 2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "Мітка" msgstr[1] "Мітки" msgstr[2] "Мітки" msgstr[3] "Мітки" #. TRANSLATORS: we're saving all USB events for emulation msgid "Tagged for emulation" msgstr "Позначено Ð´Ð»Ñ ÐµÐ¼ÑƒÐ»Ñції" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "ÐечиÑте" #. show the user the entire data blob msgid "Target" msgstr "Ціль" #. TRANSLATORS: command description msgid "Test a device using a JSON manifest" msgstr "Перевірити приÑтрій за допомогою маніфеÑту JSON" #. TRANSLATORS: when the release was tested msgid "Tested" msgstr "Перевірено" #. TRANSLATORS: the %s is a vendor name, e.g. Lenovo #, c-format msgid "Tested by %s" msgstr "Перевірено %s" #. TRANSLATORS: someone we trust has tested this msgid "Tested by trusted vendor" msgstr "Перевірено надійним поÑтачальником" #. TRANSLATORS: the boot entry was in a legacy format msgid "The EFI boot entry is not in hive format, and shim may not be new enough to read it." msgstr "Ð—Ð°Ð¿Ð¸Ñ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ EFI не форматований Ð´Ð»Ñ hive, Ñ– shim може бути недоÑтатньо Ð´Ð»Ñ Ð¹Ð¾Ð³Ð¾ читаннÑ." #. TRANSLATORS: try to treat the legacy format as a hive msgid "The EFI boot entry was not in hive format, falling back" msgstr "Ð—Ð°Ð¿Ð¸Ñ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ EFI не форматований Ð´Ð»Ñ hive, викориÑтовуємо резервний варіант" #. TRANSLATORS: longer description msgid "The Intel Management Engine Key Manifest must be valid so that the device firmware can be trusted by the CPU." msgstr "Щоб процеÑор вважав мікропрограму приÑтрою надійною, має бути чинним маніфеÑÑ‚ ключа Intel Management Engine." #. TRANSLATORS: longer description msgid "The Intel Management Engine controls device components and needs to have a recent version to avoid security issues." msgstr "Intel Management Engine керує компонентами приÑтроїв Ñ– потребує Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð¾ найÑвіжішої верÑÑ–Ñ—, щоб уникнути проблем із захиÑтом." #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "LVFS Ñ” безкоштовною Ñлужбою, Ñка працює Ñк незалежна юридична Ð¾Ð´Ð¸Ð½Ð¸Ñ†Ñ Ñ– не має зв'Ñзку з $OS_RELEASE:NAME$. Розробники вашої операційної ÑиÑтеми, можливо, не перевірÑли жодні з цих оновлень на ÑуміÑніÑть із ÑиÑтемою або з'єднаними із комп'ютером приÑтроÑми. УÑÑ– мікропрограми надаютьÑÑ Ð»Ð¸ÑˆÐµ Ñамими виробниками обладнаннÑ." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Platform Configuration is used to check whether the device start process has been modified." msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð»Ð°Ñ‚Ñ„Ð¾Ñ€Ð¼Ð¸ TPM (Trusted Platform Module або довірений модуль платформи) викориÑтовують Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€ÐºÐ¸, чи не було внеÑено змін до процедури запуÑку приÑтрою." #. TRANSLATORS: longer description msgid "The TPM (Trusted Platform Module) Reconstruction is used to check whether the device start process has been modified." msgstr "Ð’Ñ–Ð´Ñ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ TPM (Trusted Platform Module або довірений модуль платформи) викориÑтовують Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€ÐºÐ¸, чи не було внеÑено змін до процедури запуÑку приÑтрою." #. TRANSLATORS: this is more background on a security measurement problem msgid "The TPM PCR0 differs from reconstruction." msgstr "PCR0 TPM відрізнÑєтьÑÑ Ð²Ñ–Ð´ реконÑтрукції." #. TRANSLATORS: longer description msgid "The UEFI Platform Key is used to determine if device software comes from a trusted source." msgstr "Ключ платформи UEFI викориÑтовують Ð´Ð»Ñ Ð²Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ, чи походить програмне Ð·Ð°Ð±ÐµÐ·Ð¿ÐµÑ‡ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¸Ñтрою із надійного джерела." #. TRANSLATORS: HSI event title msgid "The UEFI certificate store is now up to date" msgstr "Сховище Ñертифікатів UEFI міÑтить актуальні дані" #. TRANSLATORS: longer description msgid "The UEFI db contains the list of valid certificates that can be used to authorize what EFI binaries are allowed to run." msgstr "У базі даних UEFI зберігаєтьÑÑ ÑпиÑок коректних Ñертифікатів, Ñкими можна ÑкориÑтатиÑÑ Ð´Ð»Ñ ÑƒÐ¿Ð¾Ð²Ð½Ð¾Ð²Ð°Ð¶ÐµÐ½Ð½Ñ Ð·Ð°Ð¿ÑƒÑку виконуваних файлів EFI." #. TRANSLATORS: longer description msgid "The UEFI system can set up memory attributes at boot which prevent common exploits from running." msgstr "СиÑтема UEFI може вÑтановлювати атрибути пам'Ñті під Ñ‡Ð°Ñ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ, Ñкі запобігають запуÑку типових екÑплойтів." #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "Ð¦Ñ Ñ„Ð¾Ð½Ð¾Ð²Ð° Ñлужба завантажила Ñторонній код — Ñ—Ñ— підтримка більше не здійÑнюєтьÑÑ Ñ€Ð¾Ð·Ñ€Ð¾Ð±Ð½Ð¸ÐºÐ°Ð¼Ð¸ оÑновної гілки програми!" #. TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name #, c-format msgid "The firmware from %s is not supplied by %s, the hardware vendor." msgstr "Мікропрограма з %s не поÑтачаєтьÑÑ %s, поÑтачальником обладнаннÑ." #. TRANSLATORS: try to help msgid "The system clock has not been set correctly and downloading files may fail." msgstr "Годинник ÑиÑтеми налаштовано неправильно — це може зашкодити отриманню файлів." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been re-inserted." msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð±ÑƒÐ´Ðµ продовжено піÑÐ»Ñ Ñ‚Ð¾Ð³Ð¾, Ñк кабель USB буде повторно вÑтавлено до приÑтрою" #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged and then re-inserted." msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð±ÑƒÐ´Ðµ продовжено піÑÐ»Ñ Ñ‚Ð¾Ð³Ð¾, Ñк кабель USB буде вийнÑто з приÑтрою Ñ– вÑтавлено знову." #. TRANSLATORS: warning message shown after update has been scheduled msgid "The update will continue when the device USB cable has been unplugged." msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð±ÑƒÐ´Ðµ продовжено піÑÐ»Ñ Ñ‚Ð¾Ð³Ð¾, Ñк кабель USB буде вийнÑто з приÑтрою." #. TRANSLATORS: warning message msgid "The update will continue when the device power cable has been removed and re-inserted." msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð±ÑƒÐ´Ðµ продовжено піÑÐ»Ñ Ñ‚Ð¾Ð³Ð¾, Ñк кабель Ð¶Ð¸Ð²Ð»ÐµÐ½Ð½Ñ Ð±ÑƒÐ´Ðµ вийнÑто з приÑтрою Ñ– вÑтавлено знову." #. TRANSLATORS: naughty vendor msgid "The vendor did not supply any release notes." msgstr "Виробником не надано ніÑких нотаток щодо випуÑку." #. TRANSLATORS: now list devices with unfixed high-priority issues msgid "There are devices with issues:" msgstr "Маємо приÑтрої із проблемами:" #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "Заблокованих файлів мікропрограм немає" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "Ðемає Ñхвалених мікропрограм." #. TRANSLATORS: %1 is the current device version number, and %2 is the #. command name, e.g. `fwupdmgr sync` #, c-format msgid "This device will be reverted back to %s when the %s command is performed." msgstr "Цей приÑтрій буде повернуто до %s у результаті Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð¸ %s." #. TRANSLATORS: the vendor did not upload this msgid "This firmware is provided by LVFS community members and is not provided (or supported) by the original hardware vendor." msgstr "Цю мікропрограму Ñтворено учаÑниками Ñпільноти LVFS. Її не було надано початковим виробником обладнаннÑ. Виробник також не здійÑнюватиме підтримки цієї мікропрограми." #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "Цей пакунок не було перевірено. Його належну роботу не можна гарантувати." #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "Програма зможе працювати належними чином лише від імені кориÑтувача root" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "У цьому Ñховищі міÑтитьÑÑ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð°, вÑтановлювати Ñку не заборонено, але Ñка уÑе ще перебуває у процеÑÑ– теÑÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð²Ð¸Ñ€Ð¾Ð±Ð½Ð¸ÐºÐ¾Ð¼ обладнаннÑ. Вам Ñлід переконатиÑÑ, що ви зможете вÑтановити Ñтабільну верÑÑ–ÑŽ мікропрограми, Ñкщо процедура Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð·Ð°Ð·Ð½Ð°Ñ” невдачі." #. TRANSLATORS: error message msgid "This system doesn't support firmware settings" msgstr "У цій ÑиÑтемі не передбачено підтримки параметрів мікропрограми" #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "Ð¦Ñ ÑиÑтема має вади у роботі HSI." #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "Ð¦Ñ ÑиÑтема має низький рівень захиÑту HSI." #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "За допомогою цієї програми адмініÑтратор може заÑтоÑувати Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð¾ dbx UEFI." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "За допомогою цього інÑтрумента адмініÑтратор може опитувати фонову Ñлужбу fwupd Ñ– керувати нею. Це уможливлює Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ Ñ‚Ð°ÐºÐ¸Ñ… дій, Ñк вÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼ або Ð·Ð½Ð¸Ð¶ÐµÐ½Ð½Ñ Ð²ÐµÑ€ÑÑ–Ñ— мікропрограм." #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "За допомогою цього інÑтрумента адмініÑтратор може ÑкориÑтатиÑÑ Ð´Ð¾Ð´Ð°Ñ‚ÐºÐ°Ð¼Ð¸ fwupd без вÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ñ—Ñ… до оÑновної ÑиÑтеми." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can add a kernel argument of '%s', but it will only be active after restarting the computer." msgstr "Цей інÑтрумент може додавати аргумент Ñдра «%s», але нове Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ð±ÑƒÐ´Ðµ задіÑно лише піÑÐ»Ñ Ð¿ÐµÑ€ÐµÐ·Ð°Ð¿ÑƒÑку комп'ютера." #. TRANSLATORS: the %1 is a BIOS setting name. #. * %2 and %3 are the values, e.g. "True" or "Windows10" #, c-format msgid "This tool can change the BIOS setting '%s' from '%s' to '%s' automatically, but it will only be active after restarting the computer." msgstr "Цей інÑтрумент здатен змінити параметр BIOS «%s» з «%s» на «%s» автоматично, але нове Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ð±ÑƒÐ´Ðµ задіÑно лише піÑÐ»Ñ Ð¿ÐµÑ€ÐµÐ·Ð°Ð¿ÑƒÑку комп'ютера." #. TRANSLATORS: the %1 is a kernel command line key=value #, c-format msgid "This tool can change the kernel argument from '%s' to '%s', but it will only be active after restarting the computer." msgstr "Цей інÑтрумент може змінювати аргумент Ñдра з «%s» на «%s», але нове Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ð±ÑƒÐ´Ðµ задіÑно лише піÑÐ»Ñ Ð¿ÐµÑ€ÐµÐ·Ð°Ð¿ÑƒÑку комп'ютера." #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "Цей інÑтрумент читає Ñ– оброблÑÑ” журнал подій TPM, Ñкий заповнюєтьÑÑ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð°Ð¼Ð¸ ÑиÑтеми." #. TRANSLATORS: the update state of the specific device msgid "Transient failure" msgstr "Проміжна помилка" #. TRANSLATORS: item is TRUE msgid "True" msgstr "Так" #. TRANSLATORS: We verified the metadata against the server msgid "Trusted metadata" msgstr "Ðадійні метадані" #. TRANSLATORS: We verified the payload against the server msgid "Trusted payload" msgstr "Ðадійні дані" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "Тип" #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI Bootservice Variables" msgstr "Змінні Ñлужби Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ UEFI" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition may not be set up correctly" msgstr "Можливо, неправильно налаштовано розділ ESP UEFI" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "Розділ ESP UEFI не виÑвлено або не налаштовано" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI Memory Protection" msgstr "ЗахиÑÑ‚ пам'Ñті UEFI" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "Ключ платформи UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "Secure Boot UEFI" #. TRANSLATORS: longer description msgid "UEFI Secure Boot prevents malicious software from being loaded when the device starts." msgstr "Secure Boot UEFI запобігає завантаженню програмного Ð·Ð°Ð±ÐµÐ·Ð¿ÐµÑ‡ÐµÐ½Ð½Ñ Ð·Ð»Ð¾Ð²Ð¼Ð¸Ñників при запуÑку приÑтрою." #. TRANSLATORS: longer description msgid "UEFI boot service variables should not be readable from runtime mode." msgstr "Змінні Ñлужби Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ UEFI повинні бути недоÑтупним Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ Ð´Ð»Ñ Ñ€ÐµÐ¶Ð¸Ð¼Ñƒ Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼." #. TRANSLATORS: Title: Bootservice is when only readable from early-boot msgid "UEFI bootservice variables" msgstr "Змінні Ñлужби Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ UEFI" #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "КапÑульні Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ UEFI Ñ” недоÑтупними або Ñ—Ñ… не увімкнено у налаштуваннÑÑ… мікропрограми" #. TRANSLATORS: Title: is UEFI db up-to-date msgid "UEFI db" msgstr "База даних UEFI" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "ЗаÑіб роботи з dbx UEFI" #. TRANSLATORS: system is not booted in UEFI mode msgid "UEFI firmware can not be updated in legacy BIOS mode" msgstr "У режимі ÑуміÑноÑті із заÑтарілим BIOS не можна оновити мікропрограму UEFI" #. TRANSLATORS: Title: is UEFI early-boot memory protection turned on msgid "UEFI memory protection" msgstr "ЗахиÑÑ‚ пам'Ñті UEFI" #. TRANSLATORS: HSI event title msgid "UEFI memory protection enabled and locked" msgstr "ЗахиÑÑ‚ пам'Ñті UEFI увімкнено Ñ– заблоковано" #. TRANSLATORS: HSI event title msgid "UEFI memory protection enabled but not locked" msgstr "ЗахиÑÑ‚ пам'Ñті UEFI увімкнено, але не заблоковано" #. TRANSLATORS: HSI event title msgid "UEFI memory protection is now locked" msgstr "ЗахиÑÑ‚ пам'Ñті UEFI тепер заблоковано" #. TRANSLATORS: HSI event title msgid "UEFI memory protection is now unlocked" msgstr "ЗахиÑÑ‚ пам'Ñті UEFI тепер розблоковано" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "Ключ платформи UEFI" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "UEFI secure boot" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "Ðе вдалоÑÑ Ð²Ñтановити з'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð·Ñ– Ñлужбою" #. TRANSLATORS: error message msgid "Unable to find attribute" msgstr "Ðе вдалоÑÑ Ð·Ð½Ð°Ð¹Ñ‚Ð¸ атрибут" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "Відв'Ñзати поточний драйвер" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "Розблокуємо мікропрограму:" #. TRANSLATORS: command description msgid "Unblocks a specific firmware from being installed" msgstr "Розблоковує вÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¿ÐµÐ²Ð½Ð¾Ñ— мікропрограми" #. TRANSLATORS: command description msgid "Undo the host security attribute fix" msgstr "СкаÑувати Ð²Ð¸Ð¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð½Ñ Ð°Ñ‚Ñ€Ð¸Ð±ÑƒÑ‚Ð° захиÑту вузла" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "Розшифровано" #. TRANSLATORS: command description msgid "Uninhibit the system to allow upgrades" msgstr "СкаÑувати заборону ÑиÑтемі дозволÑти оновленнÑ" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "Ðевідомий" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "Ðевідомий приÑтрій" msgid "Unlock the device to allow access" msgstr "Ð Ð¾Ð·Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¸Ñтрою Ð´Ð»Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð´Ð¾Ñтупу" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "Розблоковано" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "Розблоковує приÑтрій Ð´Ð»Ñ Ð´Ð¾Ñтупу до мікропрограми" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "Демонтує ESP" #. TRANSLATORS: message shown after device has been marked for emulation msgid "Unplug and replug the device to continue the update process." msgstr "Від'єднайте Ñ– повторно з'єднайте приÑтрій, щоб продовжити процедуру оновленнÑ." #. TRANSLATORS: firmware payload is unsigned and it is possible to modify it msgid "Unsigned Payload" msgstr "ÐепідпиÑаний вміÑÑ‚" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "Ðепідтримувана верÑÑ–Ñ Ñ„Ð¾Ð½Ð¾Ð²Ð¾Ñ— Ñлужби %s, верÑÑ–Ñ ÐºÐ»Ñ–Ñ”Ð½Ñ‚Ð° — %s" #. TRANSLATORS: Suffix: the HSI result msgid "Untainted" msgstr "Очищене" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "Придатний до оновленнÑ" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "Помилка оновленнÑ" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "Ð—Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "ÐŸÐ¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ñ‰Ð¾Ð´Ð¾ оновленнÑ" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "Стан оновленнÑ" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "Ðам відомо про помилку під Ñ‡Ð°Ñ Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ. Будь лаÑка, відвідайте цю адреÑу, щоб дізнатиÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "Оновити зараз?" #. TRANSLATORS: command description msgid "Update the stored cryptographic hash with current ROM contents" msgstr "Оновити збережений криптографічний хеш на оÑнові поточних даних ROM" msgid "Update the stored device verification information" msgstr "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð·Ð±ÐµÑ€ÐµÐ¶ÐµÐ½Ð¸Ñ… даних щодо верифікації приÑтрою" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "Оновити збережені метадані поточними даними" #. TRANSLATORS: command description msgid "Updates all specified devices to latest firmware version, or all devices if unspecified" msgstr "Оновлює уÑÑ– вказані приÑтрої до найÑвіжішої верÑÑ–Ñ— мікропрограми. Якщо приÑтрої не вказано, оновлює уÑÑ– приÑтрої." #. TRANSLATORS: how many local devices can expect updates now #, c-format msgid "Updates have been published for %u local device" msgid_plural "Updates have been published for %u of %u local devices" msgstr[0] "Оприлюднено Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ %u локального приÑтрою" msgstr[1] "Оприлюднено Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ %u з %u локальних приÑтроїв" msgstr[2] "Оприлюднено Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ %u з %u локальних приÑтроїв" msgstr[3] "Оприлюднено Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ %u з %u локального приÑтрою" msgid "Updating" msgstr "ОновленнÑ" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "Оновлюємо %s…" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "Оновити %s з %s до %s?" #. TRANSLATORS: ask the user to upload msgid "Upload data now?" msgstr "Вивантажити дані?" #. TRANSLATORS: command description msgid "Upload the list of updatable devices to a remote server" msgstr "Вивантажити ÑпиÑок придатних до Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¸Ñтроїв на віддалений Ñервер" #. TRANSLATORS: ask the user to share, %s is something #. * like: "Linux Vendor Firmware Service" #, c-format msgid "Upload these anonymous results to the %s to help other users?" msgstr "Вивантажити ці анонімні результати до %s, щоб допомогти іншим кориÑтувачам?" #. TRANSLATORS: explain why we want to upload #, c-format msgid "Uploading a device list allows the %s team to know what hardware exists, and allows us to put pressure on vendors that do not upload firmware updates for their hardware." msgstr "Ð’Ð¸Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ ÑпиÑку приÑтроїв надаÑть команді %s дані щодо того, Ñким обладнаннÑм кориÑтуютьÑÑ, Ñ– змогу тиÑнути на виробників, Ñкі не вивантажують Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼ Ð´Ð»Ñ Ñ—Ñ…Ð½ÑŒÐ¾Ð³Ð¾ обладнаннÑ." #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "Ð’Ð¸Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ð·Ð²Ñ–Ñ‚Ñ–Ð² щодо мікропрограм допомагає виробникам Ð¾Ð±Ð»Ð°Ð´Ð½Ð°Ð½Ð½Ñ ÑˆÐ²Ð¸Ð´ÐºÐ¾ виÑвлÑти проблеми та дізнаватиÑÑ Ð¿Ñ€Ð¾ уÑпішне Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð½Ð° реальних приÑтроÑÑ…." #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "Рівень важливоÑті" #. TRANSLATORS: explain how to get help, %1 is #. * 'fwupdtool --help' #. TRANSLATORS: explain how to get help, #. * where $1 is something like 'fwupdmgr --help' #, c-format msgid "Use %s for help" msgstr "СкориÑтайтеÑÑ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð¾ÑŽ %s, щоб дізнатиÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ" #. TRANSLATORS: CTRL^C [holding control, and then pressing C] will exit the #. program msgid "Use CTRL^C to cancel." msgstr "СкориÑтайтеÑÑ CTRL^C, щоб ÑкаÑувати." #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "Сповіщено кориÑтувача" #. TRANSLATORS: remote filename base msgid "Username" msgstr "КориÑтувач" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "VERSION1 VERSION2 [FORMAT]" msgstr "ВЕРСІЯ1 ВЕРСІЯ2 [ФОРМÐТ]" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "Коректна" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "ПеревірÑємо дані ESP…" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "Варіант" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "Виробник" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "ПеревірÑємо…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "ВерÑÑ–Ñ" #. TRANSLATORS: the fwupd version the release was tested on msgid "Version[fwupd]" msgstr "ВерÑÑ–Ñ[fwupd]" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "УВÐГÐ" #. TRANSLATORS: command description msgid "Wait for a device to appear" msgstr "Очікувати на поÑву приÑтрою" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "Очікуємо…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "СпоÑтерігати за змінами у обладнанні" #. TRANSLATORS: check various UEFI and ACPI tables are unchanged after the #. update msgid "Will measure elements of system integrity around an update" msgstr "Виконає Ð²Ð¸Ð¼Ñ–Ñ€ÑŽÐ²Ð°Ð½Ð½Ñ ÐµÐ»ÐµÐ¼ÐµÐ½Ñ‚Ñ–Ð² ціліÑноÑті ÑиÑтеми при оновленні" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "ЗапиÑуємо файл:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "ЗапиÑуємо…" #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from a recovery or installation disk, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "Ви маєте переконатиÑÑ, що зможете відновити конфігурацію за допомогою відновлювального диÑка або диÑка Ð´Ð»Ñ Ð²ÑтановленнÑ, оÑкільки Ñ†Ñ Ð·Ð¼Ñ–Ð½Ð° може Ñпричинити до неможливоÑті Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ ÑиÑтеми Linux або Ñпричинити інші неÑтабільноÑті у роботі ÑиÑтеми." #. TRANSLATORS: the user has to manually recover; we can't do it msgid "You should ensure you are comfortable restoring the setting from the system firmware setup, as this change may cause the system to not boot into Linux or cause other system instability." msgstr "Ви маєте переконатиÑÑ, що зможете відновити параметр із налаштувань ÑиÑтемної мікропрограми, оÑкільки Ñ†Ñ Ð·Ð¼Ñ–Ð½Ð° може Ñпричинити до неможливоÑті Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ ÑиÑтеми Linux або Ñпричинити інші неÑтабільноÑті у роботі ÑиÑтеми." #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "Виробник вашого диÑтрибутива може не перевірÑти уÑÑ– Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–ÐºÑ€Ð¾Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸ на ÑуміÑніÑть із вашою ÑиÑтемою або з’єднаними із нею приÑтроÑми." #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "Ваше Ð¾Ð±Ð»Ð°Ð´Ð½Ð°Ð½Ð½Ñ Ð¼Ð¾Ð¶Ðµ бути пошкоджено через викориÑÑ‚Ð°Ð½Ð½Ñ Ñ†Ñ–Ñ”Ñ— мікропрограми. Ð’ÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ñ†ÑŒÐ¾Ð³Ð¾ випуÑку може призвеÑти до відмови %s від гарантійних зобов'Ñзань." #. TRANSLATORS: BKC is the industry name for the best known configuration and #. is a set #. * of firmware that works together #, c-format msgid "Your system is set up to the BKC of %s." msgstr "Вашу ÑиÑтему налаштовано на найкращу відому конфігурацію %s." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[APPSTREAM_ID]" msgstr "[ІДЕÐТИФІКÐТОР_APPSTREAM]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[КОÐТРОЛЬÐÐ_СУМÐ]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[ІД_ПРИСТРОЮ|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[ІД_ПРИСТРОЮ|GUID] [ГІЛКÐ]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [VERSION]" msgstr "[ІД_ПРИСТРОЮ|GUID] [ВЕРСІЯ]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE]" msgstr "[ПРИСТРІЙ]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILE FILE_SIG REMOTE-ID]" msgstr "[ФÐЙЛ ПІДПИС_ФÐЙЛРВІДДÐЛ-ІД]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[ÐÐЗВÐФÐЙЛÐ1] [ÐÐЗВÐФÐЙЛÐ2]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FWUPD-VERSION]" msgstr "[ВЕРСІЯ-FWUPD]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[REASON] [TIMEOUT]" msgstr "[ПРИЧИÐÐ] [ЧÐС ОЧІКУВÐÐÐЯ]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SECTION] KEY VALUE" msgstr "[РОЗДІЛ] КЛЮЧ ЗÐÐЧЕÐÐЯ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2] [--no-authenticate]" msgstr "[ПÐРÐМЕТР1] [ПÐРÐМЕТР2] [--no-authenticate]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SETTING1] [SETTING2]..." msgstr "[ПÐРÐМЕТР1] [ПÐРÐМЕТР2]..." #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[ФÐЙЛ-SMBIOS|ФÐЙЛ-HWIDS]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "типова" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "Допоміжна програма Ð´Ð»Ñ Ñ€Ð¾Ð±Ð¾Ñ‚Ð¸ із запиÑами подій журналу TPM fwupd" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "Додатки fwupd" fwupd-2.0.10/po/zh_CN.po000066400000000000000000001701201501337203100146720ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Boyuan Yang <073plan@gmail.com>, 2017,2020,2022 # Dingzhong Chen , 2020-2021 # Dingzhong Chen , 2016-2019 # Bubu, 2024 # Mingcong Bai , 2017-2018 # Mingye Wang , 2016 # Mingye Wang , 2015 # Richard Hughes , 2023 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Chinese (China) (http://app.transifex.com/freedesktop/fwupd/language/zh_CN/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: zh_CN\n" "Plural-Forms: nplurals=1; plural=0;\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "还剩 %.0f 分钟" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "%s 电池更新" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "%s CPU å¾®ç æ›´æ–°" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "%s æ‘„åƒå¤´æ›´æ–°" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "%s é…置更新" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "%s 消费者 ME æ›´æ–°" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "%s 控制器更新" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "%s ä¼ä¸š ME æ›´æ–°" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "%s 设备更新" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "%s åµŒå…¥å¼æŽ§åˆ¶å™¨æ›´æ–°" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "%s 键盘更新" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "%s 管ç†å¼•擎(ME)更新" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "%s 鼠标更新" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "%s ç½‘ç»œæŽ¥å£æ›´æ–°" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "%s 存储控制器更新" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "%s 系统更新" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "%s TPM æ›´æ–°" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "%s Thunderbolt 控制器更新" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "%s è§¦æ‘¸æ¿æ›´æ–°" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "%s æ›´æ–°" #. TRANSLATORS: warn the user before updating, %1 is a device name #, c-format msgid "%s and all connected devices may not be usable while updating." msgstr "更新期间 %s 和所有连接的设备å¯èƒ½æ— æ³•使用。" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s manufacturing mode" msgstr "%s 生产模å¼" #. TRANSLATORS: warn the user before #. * updating, %1 is a device name #, c-format msgid "%s must remain connected for the duration of the update to avoid damage." msgstr "æ›´æ–°æœŸé—´å¿…é¡»ä¿æŒ %s 的连接以é¿å…æŸå。" #. TRANSLATORS: warn the user before updating, %1 is a machine #. * name #, c-format msgid "%s must remain plugged into a power source for the duration of the update to avoid damage." msgstr "æ›´æ–°æœŸé—´å¿…é¡»ä¿æŒ %s 接入电æºä»¥é¿å…æŸå。" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s override" msgstr "%s 覆写" #. TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number #, c-format msgid "%s v%s" msgstr "%s v%s" #. TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT #, c-format msgid "%s version" msgstr "%s 版本" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u 天" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "%u 个设备有å¯å‡çº§çš„固件。" #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u å°æ—¶" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u 分钟" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u ç§’" #. TRANSLATORS: this is shown as a suffix for obsoleted tests msgid "(obsoleted)" msgstr "(已过时)" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "所需æ“作:" #. TRANSLATORS: command description msgid "Activate devices" msgstr "激活设备" #. TRANSLATORS: command description msgid "Activate pending devices" msgstr "激活待处ç†çš„设备" msgid "Activate the new firmware on the device" msgstr "激活设备上的新固件" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update" msgstr "正在激活固件更新" #. TRANSLATORS: shown when shutting down to switch to the new version msgid "Activating firmware update for" msgstr "正在激活固件更新给" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "å·²å‘布时间" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "åŒæ„å¹¶å¯ç”¨æ­¤è¿œç¨‹æºå—?" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "%s 的别å" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "所有相åŒç±»åž‹çš„设备会在相åŒä¸€æ—¶é—´æ›´æ–°" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "å…许é™çº§å›ºä»¶ç‰ˆæœ¬" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "å…è®¸é‡æ–°å®‰è£…现有的固件版本" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "å…许切æ¢å›ºä»¶åˆ†æ”¯" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "更新需è¦é‡å¯è®¾å¤‡æ‰èƒ½å®Œæˆã€‚" #. TRANSLATORS: explain why msgid "An update requires the system to shutdown to complete." msgstr "更新需è¦ç³»ç»Ÿå…³æœºæ‰èƒ½å®Œæˆã€‚" #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "一律用“是â€å›žç­”问题" #. TRANSLATORS: command line option msgid "Apply update even when not advised" msgstr "应用更新,å³ä½¿ä¸å»ºè®®" #. TRANSLATORS: command line option msgid "Apply update files" msgstr "应用更新文件" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "正在应用更新……" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "批准的固件:" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "附加到固件模å¼" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "正在认è¯â€¦" #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "需è¦èº«ä»½éªŒè¯è¯¦ç»†ä¿¡æ¯" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "需è¦è®¤è¯ï¼šåœ¨å¯ç§»åŠ¨è®¾å¤‡ä¸Šé™çº§å›ºä»¶" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "需è¦è®¤è¯ï¼šåœ¨æ­¤æœºå™¨ä¸Šé™çº§å›ºä»¶" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "需è¦è®¤è¯ï¼šä¿®æ”¹ç”¨äºŽå›ºä»¶æ›´æ–°çš„å·²é…置远程æº" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "需è¦è®¤è¯ï¼šä¿®æ”¹å®ˆæŠ¤è¿›ç¨‹é…ç½®" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "需è¦è®¤è¯ï¼šè®¾å®šæ‰¹å‡†å›ºä»¶çš„列表" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "需è¦è®¤è¯ï¼šä½¿ç”¨å®¢æˆ·ç«¯è¯ä¹¦ç­¾åæ•°æ®" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "需è¦è®¤è¯ï¼šåˆ‡æ¢åˆ°æ–°å›ºä»¶ç‰ˆæœ¬" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "需è¦è®¤è¯ï¼šè§£é”设备" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "需è¦è®¤è¯ï¼šåœ¨å¯ç§»åŠ¨è®¾å¤‡ä¸Šå‡çº§å›ºä»¶" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "需è¦è®¤è¯ï¼šåœ¨æ­¤æœºå™¨ä¸Šå‡çº§å›ºä»¶" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "需è¦è®¤è¯ï¼šä¸ºè®¾å¤‡æ›´æ–°å­˜å‚¨çš„æ ¡éªŒå’Œ" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "自动报告" #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "æ¯æ¬¡éƒ½è‡ªåŠ¨ä¸Šä¼ ï¼Ÿ" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS firmware updates" msgstr "BIOS 固件更新" #. TRANSLATORS: description of a BIOS setting msgid "BIOS updates delivered via LVFS or Windows Update" msgstr "BIOS 更新通过 LVFS 或 Windows Update æä¾›ã€‚" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "BUILDER-XML FILENAME-DST" msgstr "生æˆå™¨XML 目标文件å" #. TRANSLATORS: command description msgid "Bind new kernel driver" msgstr "绑定新内核驱动" #. TRANSLATORS: there follows a list of hashes msgid "Blocked firmware files:" msgstr "å—阻的固件文件:" #. TRANSLATORS: we will not offer this firmware to the user msgid "Blocking firmware:" msgstr "阻止的固件:" #. TRANSLATORS: command description msgid "Blocks a specific firmware from being installed" msgstr "阻止指定的固件被安装" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "引导程åºç‰ˆæœ¬" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "分支" #. TRANSLATORS: command description msgid "Build a firmware file" msgstr "生æˆå›ºä»¶æ–‡ä»¶" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "å–æ¶ˆ" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "已喿¶ˆ" #. TRANSLATORS: same or newer update already applied msgid "Cannot apply as dbx update has already been applied." msgstr "无法应用 dbx 更新因其更新已应用。" #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "å·²å˜æ›´" #. TRANSLATORS: command description msgid "Checks cryptographic hash matches firmware" msgstr "检查加密哈希与固件是å¦åŒ¹é…" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "校验和" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "清除从最åŽä¸€æ¬¡æ›´æ–°èŽ·å–的结果" #. TRANSLATORS: error message msgid "Command not found" msgstr "未找到命令" #. TRANSLATORS: title prefix for the BIOS settings dialog msgid "Configuration Change Suggested" msgstr "é…ç½®å˜æ›´å»ºè®®" #. TRANSLATORS: command description msgid "Convert a firmware file" msgstr "转æ¢å›ºä»¶æ–‡ä»¶" #. TRANSLATORS: when the update was built msgid "Created" msgstr "创建日期" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "关键" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "支æŒåŠ å¯†å“ˆå¸ŒéªŒè¯" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "当å‰ç‰ˆæœ¬" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "DEVICE-ID|GUID" msgstr "设备ID|GUID" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "调试选项" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "正在解压缩…" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "æè¿°" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "拆离到引导加载器模å¼" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "详情" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "设备标志" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "设备 ID" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "已添加设备:" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "设备å¯ä»Žåˆ·æœºå¤±è´¥æ¢å¤" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "已更改设备:" #. TRANSLATORS: a version check is required for all firmware msgid "Device firmware is required to have a version check" msgstr "设备固件需è¦ç‰ˆæœ¬æ£€æŸ¥" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "设备已é”定" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "设备需è¦å®‰è£…所有æä¾›çš„版本" #. TRANSLATORS: currently unreachable, perhaps because it is in a lower power #. state #. * or is out of wireless range msgid "Device is unreachable" msgstr "设备ä¸å¯è®¿é—®" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "设备在更新期间å¯ä½¿ç”¨" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "已移除设备:" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "设备阶段更新" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "设备支æŒåˆ‡æ¢åˆ°ä¸åŒçš„固件分支" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "è®¾å¤‡æ›´æ–°éœ€è¦æ¿€æ´»" #. TRANSLATORS: save the old firmware to disk before installing the new one msgid "Device will backup firmware before installing" msgstr "设备将在安装å‰å¤‡ä»½å›ºä»¶" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "更新完åŽè®¾å¤‡ä¸ä¼šå†æ˜¾ç¤º" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "æˆåŠŸæ›´æ–°çš„è®¾å¤‡ï¼š" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "未正确更新的设备:" #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no device #. * upgrade available due to missing on LVFS #. TRANSLATORS: message letting the user know no #. * device upgrade available due to missing on LVFS msgid "Devices with no available firmware updates: " msgstr "无固件更新å¯ç”¨çš„设备:" #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available #. TRANSLATORS: message letting the user know no device upgrade available #. TRANSLATORS: message letting the user know no device #. * upgrade available msgid "Devices with the latest available firmware version:" msgstr "固件已是最新的设备:" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "å·²ç¦ç”¨" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "ç¦ç”¨ç»™å®šçš„远程æº" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "ä¸è¦æŸ¥æ‰¾è€çš„元数æ®" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "ä¸è¦æŸ¥æ‰¾æœªæŠ¥å‘Šåކå²" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "ä¸è¦æ£€æŸ¥ä¸‹è½½è¿œç¨‹æ˜¯å¦å·²å¯ç”¨" #. TRANSLATORS: command line option msgid "Do not check or prompt for reboot after update" msgstr "æ›´æ–°åŽä¸è¦æ£€æŸ¥æˆ–æç¤ºæ˜¯å¦è¦é‡å¯" #. TRANSLATORS: turn on all debugging msgid "Do not include log domain prefix" msgstr "ä¸åŒ…嫿—¥å¿—域å‰ç¼€" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "ä¸åŒ…嫿—¶é—´æˆ³å‰ç¼€" #. TRANSLATORS: command line option msgid "Do not perform device safety checks" msgstr "ä¸è¦æ‰§è¡Œè®¾å¤‡å®‰å…¨æ£€æŸ¥" #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "ä¸è¦å†™å…¥åކ岿•°æ®åº“" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "ä½ æ¸…æ¥šæ›´æ”¹å›ºä»¶åˆ†æ”¯çš„åŽæžœå—?" #. TRANSLATORS: offer to disable this nag msgid "Do you want to disable this feature for future updates?" msgstr "ä½ è¦åœ¨å°†æ¥çš„æ›´æ–°ä¸­ç¦ç”¨æ­¤åŠŸèƒ½å—?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "ä½ è¦çŽ°åœ¨åˆ·æ–°è¿™ä¸ªè¿œç¨‹åº“å—?" #. TRANSLATORS: offer to stop asking the question msgid "Do you want to upload reports automatically for future updates?" msgstr "ä½ è¦å°†æ¥çš„æ›´æ–°ä¸­è‡ªåŠ¨ä¸Šä¼ æŠ¥å‘Šå—?" #. TRANSLATORS: success msgid "Done!" msgstr "完æˆï¼" #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "é™çº§ %s,从 %s 到 %s?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "é™çº§è®¾å¤‡ä¸Šçš„固件" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "正在下载 %s……" #. TRANSLATORS: command description msgid "Download a file" msgstr "下载文件" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "正在下载..." #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "从文件转储 SMBIOS æ•°æ®" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "æ—¶é•¿" msgid "Enable" msgstr "å¯ç”¨" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "£å¯ç”¨æ–°çš„远程库" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "è¦å¯ç”¨æ­¤è¿œç¨‹æºå—?" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "å·²å¯ç”¨" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "å¯ç”¨ç»™å®šçš„远程æº" msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "å¯ç”¨æ­¤åŠŸèƒ½çš„é£Žé™©è‡ªè´Ÿï¼Œè¿™æ„味ç€ä½ å¿…须就这些更新引起的任何问题与原始设备制造商è”ç³»ã€‚åªæœ‰æ›´æ–°è¿‡ç¨‹æœ¬èº«çš„问题å¯ä»¥æäº¤åˆ° $OS_RELEASE:BUG_REPORT_URL$。" #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "å¯ç”¨æ­¤è¿œç¨‹æºåŽæžœè‡ªè´Ÿã€‚" #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "已加密" #. TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME msgid "Encrypted RAM" msgstr "加密内存" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "清除所有固件更新历å²" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "正在擦除…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "在短暂的延迟åŽé€€å‡º" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "在引擎加载åŽé€€å‡º" #. TRANSLATORS: command description msgid "Export a firmware file structure to XML" msgstr "导出 XML 结构的固件文件" #. TRANSLATORS: command description msgid "Extract a firmware blob to images" msgstr "æå–固件比特å—到映åƒ" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE" msgstr "文件" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILE [DEVICE-ID|GUID]" msgstr "文件 [设备ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME" msgstr "文件å" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME CERTIFICATE PRIVATE-KEY" msgstr "文件åç§° è¯ä¹¦ ç§é’¥" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME DEVICE-ID" msgstr "文件å 设备ID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [DEVICE-ID|GUID]" msgstr "文件å [设备ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME [FIRMWARE-TYPE]" msgstr "文件å [固件类型]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]" msgstr "æºæ–‡ä»¶å 目标文件å [æºå›ºä»¶ç±»åž‹] [目标固件类型]" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "已失败" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "应用更新失败" #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "连接到守护进程失败" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "加载本地 dbx 失败" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "加载系统 dbx 失败" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "é”定失败" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "未能解æžå‚æ•°" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "è§£æžæ–‡ä»¶å¤±è´¥" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "è§£æžæœ¬åœ° dbx 失败" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "éªŒè¯ ESP 内容失败" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "文件å" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "文件åç­¾å" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "æ–‡ä»¶æ¥æº" #. TRANSLATORS: user did not include a filename parameter msgid "Filename required" msgstr "éœ€è¦æ–‡ä»¶å" #. TRANSLATORS: command line option msgid "Filter with a set of device flags using a ~ prefix to exclude, e.g. 'internal,~needs-reboot'" msgstr "筛选器为一组设备标志,使用 ~ å‰ç¼€æ¥æŽ’除设备,示例 \"internal,~needs-reboot\"" #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "固件库 URI" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "固件更新 D-Bus æœåŠ¡" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "固件更新守护程åº" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "固件实用程åº" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "å›ºä»¶å†™ä¿æŠ¤" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "å›ºä»¶å†™ä¿æŠ¤é”" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware attestation" msgstr "å›ºä»¶è¯æ˜Ž" #. TRANSLATORS: user selected something not possible msgid "Firmware is already blocked" msgstr "固件已被阻止" #. TRANSLATORS: user selected something not possible msgid "Firmware is not already blocked" msgstr "固件已ç»ä¸å—阻" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "固件元数æ®å·²æœ‰ %u 天未更新,数æ®å¯èƒ½ä¸æ˜¯æœ€æ–°çš„。" #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware updates" msgstr "固件更新" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "标志" #. TRANSLATORS: command line option msgid "Force the action by relaxing some runtime checks" msgstr "通过放宽一些è¿è¡Œæ—¶æ£€æŸ¥æ¥å¼ºåˆ¶æ“作" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "找到" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "探测到全盘加密" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" #. TRANSLATORS: command description msgid "Get all device flags supported by fwupd" msgstr "èŽ·å– fwupd 支æŒçš„全部设备标志" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "èŽ·å¾—æ‰€æœ‰æ”¯æŒæ›´æ–°å›ºä»¶çš„硬件列表" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "èŽ·å–æ‰€æœ‰åœ¨ç³»ç»Ÿæ³¨å†Œçš„å¯ç”¨æ’ä»¶" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "èŽ·å–æœ‰å…³æŸå›ºä»¶æ–‡ä»¶çš„详细信æ¯" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "获å–å·²é…置的远程æº" #. TRANSLATORS: command description msgid "Gets the host security attributes" msgstr "获å–主机安全属性" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "获å–已批准固件的列表" #. TRANSLATORS: command description msgid "Gets the list of blocked firmware" msgstr "获å–å—阻固件的列表" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "获å–已连接硬件的å¯ç”¨æ›´æ–°åˆ—表" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "获å–用于设备的å‘行版本" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "从最åŽä¸€æ¬¡æ›´æ–°ä¸­èŽ·å–结果" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "HWIDS-FILE" msgstr "HWIDS文件" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "ç¡¬ä»¶æ­£ç­‰å¾…é‡æ–°æ’å…¥" #. TRANSLATORS: the release urgency msgid "High" msgstr "高" #. TRANSLATORS: this is a string like 'HSI:2-U' msgid "Host Security ID:" msgstr "主机安全 ID" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU" msgstr "IOMMU" #. TRANSLATORS: HSI event title msgid "IOMMU device protection disabled" msgstr "IOMMU è®¾å¤‡ä¿æŠ¤å·²ç¦ç”¨" #. TRANSLATORS: HSI event title msgid "IOMMU device protection enabled" msgstr "IOMMU è®¾å¤‡ä¿æŠ¤å·²å¯ç”¨" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "空闲…" #. TRANSLATORS: command line option msgid "Ignore SSL strict checks when downloading files" msgstr "下载文件时忽略 SSL 严格检查" #. TRANSLATORS: command line option msgid "Ignore firmware checksum failures" msgstr "忽略固件校验和错误" #. TRANSLATORS: command line option msgid "Ignore firmware hardware mismatch failures" msgstr "忽略固件硬件ä¸åŒ¹é…错误" #. TRANSLATORS: try to help msgid "Ignoring SSL strict checks, to do this automatically in the future export DISABLE_SSL_STRICT in your environment" msgstr "正在忽略 SSL 严格检查,è¦åœ¨ä¹‹åŽè‡ªåŠ¨åº”ç”¨è¿™ä¸€é€‰é¡¹è¯·åœ¨ä½ çš„çŽ¯å¢ƒé‡Œ export DISABLE_SSL_STRICT" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "安装时长" #. TRANSLATORS: command description msgid "Install a specific firmware file on all devices that match" msgstr "在所有匹é…的设备上安装特定的固件文件" msgid "Install old version of signed system firmware" msgstr "安装旧版本的已签å系统固件" msgid "Install old version of unsigned system firmware" msgstr "安装旧版本的未签å系统固件" msgid "Install signed device firmware" msgstr "安装已签å的设备固件" msgid "Install signed system firmware" msgstr "安装已签å的系统固件" msgid "Install unsigned device firmware" msgstr "安装未签å的设备固件" msgid "Install unsigned system firmware" msgstr "安装未签å的系统固件" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "正在安装固件更新..." #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "正在安装到 %s……" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM protected" msgstr "Intel BootGuard ACM ä¿æŠ¤" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * OTP = one time programmable msgid "Intel BootGuard OTP fuse" msgstr "Intel BootGuard OTP ä¿é™©ä¸" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard error policy" msgstr "Intel BootGuard 错误策略" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard verified boot" msgstr "Intel BootGuard 验任å¯åЍ" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "内部设备" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "无效" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "å¤„äºŽå¼•å¯¼ç¨‹åºæ¨¡å¼" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "问题" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "LOCATION" msgstr "ä½ç½®" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "最åŽä¿®æ”¹" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "还剩ä¸åˆ°ä¸€åˆ†é’Ÿ" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "许å¯è¯" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Linux 供应商固件æœåŠ¡ï¼ˆç¨³å®šå›ºä»¶ï¼‰" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Linux 供应商固件æœåŠ¡ï¼ˆæµ‹è¯•å›ºä»¶ï¼‰" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Linux 内核" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Linux 内核é”定" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux swap" msgstr "Linux 交æ¢åŒº" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "列出 dbx 中的æ¡ç›®" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "列出å¯ç”¨çš„固件类型" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "列出 ESP 分区中的文件" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "正在加载…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "å·²é”定" #. TRANSLATORS: the release urgency msgid "Low" msgstr "低" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "MEI manufacturing mode" msgstr "MEI 生产模å¼" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the #. * "override" is the physical PIN that can be driven to #. * logic high -- luckily it is probably not accessible to #. * end users on consumer boards msgid "MEI override" msgstr "MEI 覆写" msgid "MEI version" msgstr "MEI 版本" #. TRANSLATORS: command line option msgid "Manually enable specific plugins" msgstr "手动å¯ç”¨æŒ‡å®šæ’ä»¶" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "中" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "元数æ®ç­¾å" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "å…ƒæ•°æ® URI" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "å¯ä»Ž Linux 供应商固件æœåŠ¡èŽ·å–元数æ®ã€‚" #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "最å°ç‰ˆæœ¬" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "修改守护进程的é…置值" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "修改给定的远程æº" msgid "Modify a configured remote" msgstr "修改已é…置的远程æº" msgid "Modify daemon configuration" msgstr "修改守护进程é…ç½®" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "监视守护程åºé‡Œçš„事件" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "挂载 ESP 分区" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "安装完åŽéœ€è¦é‡å¯" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "需è¦é‡å¯" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "安装完åŽéœ€è¦å…³æœº" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "新版本" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "未指定æ“作ï¼" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "%s æ— å¯ç”¨é™çº§" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "未找到固件 ID" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "æ²¡æœ‰æ£€æµ‹åˆ°æ”¯æŒæ›´æ–°å›ºä»¶çš„硬件" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "æ— å¯ç”¨å‘布版本" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "当å‰å°šæœªå¯ç”¨ä»»ä½•远程æºï¼Œå› æ­¤æ— å¯ç”¨å…ƒæ•°æ®ã€‚" #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "æ— å¯ç”¨è¿œç¨‹æº" msgid "No updates available for remaining devices" msgstr "其余设备没有å¯ç”¨æ›´æ–°" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "未找到" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "未支æŒ" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "确定" #. TRANSLATORS: command line option msgid "Only show single PCR value" msgstr "åªæ˜¾ç¤ºå• PCR 值" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "åªå…许版本å‡çº§" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "覆盖默认的 ESP 路径" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "PATH" msgstr "路径" #. TRANSLATORS: command description msgid "Parse and show details about a firmware file" msgstr "è§£æžå¹¶æ˜¾ç¤ºå›ºä»¶æ–‡ä»¶çš„详细信æ¯" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "æ­£åœ¨è§£æž dbx 更新……" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "正在解æžç³»ç»Ÿ dbx……" #. TRANSLATORS: remote filename base msgid "Password" msgstr "密ç " #. TRANSLATORS: command description msgid "Patch a firmware blob at a known offset" msgstr "在已知åç§»é‡å¤„对固件二进制文件打补ä¸" msgid "Payload" msgstr "è½½è·" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "待处ç†" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "执行æ“作?" #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "请输入一个 0 到 %u 之间的数字:" #. TRANSLATORS: Failed to open plugin, hey Arch users msgid "Plugin dependencies missing" msgstr "æ’ä»¶ä¾èµ–缺失" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "预å¯åЍ DMA ä¿æŠ¤" #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "上个版本" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "优先级" msgid "Proceed with upload?" msgstr "确定è¦ä¸Šä¼ å—?" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "ç§æœ‰" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID" msgstr "远程ID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "REMOTE-ID KEY VALUE" msgstr "远程ID é”® 值" #. TRANSLATORS: command description msgid "Read a firmware blob from a device" msgstr "从设备读å–固件比特å—(blob)" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "æ­£åœ¨è¯»å– %s……" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "正在读å–…" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "刷新æ¥è‡ªè¿œç¨‹æœåŠ¡å™¨çš„å…ƒæ•°æ®" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "釿–°å®‰è£… %s 到 %s?" #. TRANSLATORS: command description msgid "Reinstall current firmware on the device" msgstr "åœ¨è®¾å¤‡ä¸Šé‡æ–°å®‰è£…当å‰çš„固件" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "釿–°å®‰è£…设备上的固件" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "å‘行分支" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "è¿œç¨‹æº ID" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "报告 URI" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "已报告到远程æœåС噍" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "所需 efivarfs 文件系统未找到" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "所需设备未找到" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "需è¦å¼•导程åº" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "需è¦äº’è”网连接" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "è¦çŽ°åœ¨é‡å¯è®¾å¤‡å—?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "é‡å¯å®ˆæŠ¤è¿›ç¨‹ä»¥è®©æ›´æ”¹ç”Ÿæ•ˆï¼Ÿ" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "正在é‡å¯è®¾å¤‡â€¦" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "返回机器的所有硬件 ID" #. TRANSLATORS: command line option msgid "Run the plugin composite cleanup routine when using install-blob" msgstr "å½“ä½¿ç”¨å®‰è£…æ¯”ç‰¹å—æ—¶è¿è¡Œæ’ä»¶ç»„åˆæ¸…ç†ä¾‹ç¨‹" #. TRANSLATORS: command line option msgid "Run the plugin composite prepare routine when using install-blob" msgstr "å½“ä½¿ç”¨å®‰è£…æ¯”ç‰¹å—æ—¶è¿è¡Œæ’件组åˆå‡†å¤‡ä¾‹ç¨‹" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "è¿è¡Œçš„å†…æ ¸å¤ªè€æ—§" #. TRANSLATORS: this is the HSI suffix msgid "Runtime Suffix" msgstr "è¿è¡Œæ—¶åŽç¼€" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "SPI BIOS æè¿°ç¬¦" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "SPI BIOS 区域" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "SPI é”" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "SPI 写入" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "SUBSYSTEM DRIVER [DEVICE-ID|GUID]" msgstr "å­ç³»ç»Ÿ 驱动 [设备ID|GUID]" #. TRANSLATORS: command description msgid "Save a file that allows generation of hardware IDs" msgstr "ä¿å­˜å…许生æˆç¡¬ä»¶ ID 的文件" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "正在计划…" #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "安全å¯åЍ已ç¦ç”¨" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "安全å¯åЍ已å¯ç”¨" #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "查看 %s èŽ·å–æ›´å¤šä¿¡æ¯ã€‚" #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "所选设备" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "所选å·" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "åºåˆ—å·" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "设定批准固件的列表" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "设置将在系统é‡å¯åŽç”Ÿæ•ˆ" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "与开å‘者分享固件历å²" #. TRANSLATORS: command line option msgid "Show all results" msgstr "显示所有结果" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "显示客户端åŠå®ˆæŠ¤ç¨‹åºç‰ˆæœ¬" #. TRANSLATORS: this is for daemon development msgid "Show daemon verbose information for a particular domain" msgstr "显示特定域的守护进程详细信æ¯" #. TRANSLATORS: turn on all debugging msgid "Show debugging information for all domains" msgstr "显示所有域的调试信æ¯" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "显示调试选项" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "显示ä¸å¯æ›´æ–°çš„设备" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "显示é¢å¤–调试信æ¯" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "显示固件更新历å²" #. TRANSLATORS: command line option msgid "Show the calculated version of the dbx" msgstr "显示 dbx 的计算版本" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "现在关机?" #. TRANSLATORS: command description msgid "Sign a firmware with a new key" msgstr "用新密钥签å固件" msgid "Sign data using the client certificate" msgstr "使用客户端è¯ä¹¦ç­¾åæ•°æ®" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "使用客户端è¯ä¹¦ç­¾åæ•°æ®" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "以客户端è¯ä¹¦ç­¾å上传的数æ®" msgid "Signature" msgstr "ç­¾å" #. TRANSLATORS: file size of the download msgid "Size" msgstr "大å°" #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "æ¥æº" #. TRANSLATORS: command line option msgid "Specify the dbx database file" msgstr "指定 dbx æ•°æ®åº“文件" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "æˆåŠŸ" #. TRANSLATORS: success message -- where activation is making the new #. * firmware take effect, usually after updating offline msgid "Successfully activated all devices" msgstr "激活所有设备æˆåŠŸ" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "ç¦ç”¨è¿œç¨‹æºæˆåŠŸ" #. TRANSLATORS: success message -- where 'metadata' is information #. * about available firmware on the remote server msgid "Successfully downloaded new metadata: " msgstr "å·²æˆåŠŸä¸‹è½½æ–°å…ƒæ•°æ®ï¼š" #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "å·²æˆåŠŸå¯ç”¨å’Œåˆ·æ–°è¿œç¨‹åº“" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "å¯ç”¨è¿œç¨‹æºæˆåŠŸ" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "安装固件æˆåŠŸ" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "修改é…置值æˆåŠŸ" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "ä¿®æ”¹è¿œç¨‹æºæˆåŠŸ" #. TRANSLATORS: success message -- the user can do this by-hand too msgid "Successfully refreshed metadata manually" msgstr "æ‰‹åŠ¨åˆ·æ–°å…ƒæ•°æ®æˆåŠŸ" #. TRANSLATORS: success message when user refreshes device checksums msgid "Successfully updated device checksums" msgstr "更新设备校验和æˆåŠŸ" #. TRANSLATORS: success message -- where the user has uploaded #. * success and/or failure reports to the remote server #, c-format msgid "Successfully uploaded %u report" msgid_plural "Successfully uploaded %u reports" msgstr[0] "上传 %u 篇报告æˆåŠŸ" #. TRANSLATORS: success message when user verified device checksums msgid "Successfully verified device checksums" msgstr "验è¯è®¾å¤‡æ ¡éªŒå’ŒæˆåŠŸ" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "概览" #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "已支æŒ" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "支æŒäºŽè¿œç¨‹æœåС噍" #. TRANSLATORS: Title: a better sleep state msgid "Suspend-to-idle" msgstr "挂起到空闲" #. TRANSLATORS: Title: sleep state msgid "Suspend-to-ram" msgstr "挂起到内存" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "切æ¢åˆ†æ”¯ï¼Œä»Ž %s 到 %s?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "切æ¢è®¾å¤‡çš„固件分支" #. TRANSLATORS: command description msgid "Sync firmware versions to the chosen configuration" msgstr "åŒæ­¥å›ºä»¶ç‰ˆæœ¬è‡³é€‰å®šçš„é…ç½®" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "系统需è¦å¤–接电æº" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "TEXT" msgstr "文本" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM PCR0 reconstruction" msgstr "TPM PCR0 é‡å»º" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "标签" #. TRANSLATORS: Suffix: the HSI result msgid "Tainted" msgstr "å—æ±¡æŸ“" #. show the user the entire data blob msgid "Target" msgstr "目标" #. TRANSLATORS: command description msgid "Test a device using a JSON manifest" msgstr "使用 JSON æ¸…å•æµ‹è¯•设备" #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "LVFS 是个å…è´¹æœåŠ¡ï¼Œä»¥ç‹¬ç«‹æ³•å¾‹å®žä½“è¿ä½œï¼Œä¸Ž $OS_RELEASE:NAME$ 无关。你的å‘行商å¯èƒ½æœªå¯¹å›ºä»¶æ›´æ–°ä¸Žä½ çš„系统或连接的设备兼容性åšè¿‡ä»»ä½•验è¯ã€‚所有固件都由原始设备制造商æä¾›ã€‚" #. TRANSLATORS: this is more background on a security measurement problem msgid "The TPM PCR0 differs from reconstruction." msgstr "TPM PCR0 è·Ÿé‡å»ºçš„ä¸åŒã€‚" #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "守护进程已ç»åŠ è½½äº†ç¬¬ä¸‰æ–¹ä»£ç ï¼Œå¹¶ä¸”上游开å‘者ä¸å†æ”¯æŒã€‚" #. TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name #, c-format msgid "The firmware from %s is not supplied by %s, the hardware vendor." msgstr "æ¥æº %s 的固件并éžç”±ç¡¬ä»¶ä¾›åº”商 %s æä¾›ã€‚" #. TRANSLATORS: try to help msgid "The system clock has not been set correctly and downloading files may fail." msgstr "系统时钟没有设置正确,文件下载å¯èƒ½ä¼šå¤±è´¥ã€‚" #. TRANSLATORS: nothing to show msgid "There are no blocked firmware files" msgstr "没有å—阻的固件文件" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "没有批准的固件。" #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "此软件包未ç»éªŒè¯ï¼Œå¯èƒ½æ— æ³•正常工作。" #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "此程åºåªèƒ½ä»¥ root 身份正常è¿è¡Œ" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "此远程æºåŒ…嫿œªç¦æ­¢çš„固件,但其ä»ç»è¿‡ç¡¬ä»¶ä¾›åº”商测试。你应该确ä¿å¦‚果固件更新失败时你能够é™çº§å›ºä»¶ã€‚" #. TRANSLATORS: this is instructions on how to improve the HSI suffix msgid "This system has HSI runtime issues." msgstr "该系统的 HSI è¿è¡Œæ—¶æœ‰é—®é¢˜ã€‚" #. TRANSLATORS: this is instructions on how to improve the HSI security level msgid "This system has a low HSI security level." msgstr "该系统的 HSI 安全级别较低。" #. TRANSLATORS: description of dbxtool msgid "This tool allows an administrator to apply UEFI dbx updates." msgstr "此工具å…许管ç†å‘˜åº”用 UEFI dbx 更新。" #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "此工具å…许管ç†å‘˜æŸ¥è¯¢å’ŒæŽ§åˆ¶ fwupd 守护进程,让其执行如安装或者é™çº§å›ºä»¶ç­‰æ“作。" #. TRANSLATORS: CLI description msgid "This tool allows an administrator to use the fwupd plugins without being installed on the host system." msgstr "此工具å¯ä»¥è®©ç®¡ç†å‘˜ä¸ç”¨å®‰è£… fwupd æ’件到主机系统上就å¯ä»¥ä½¿ç”¨å®ƒä»¬ã€‚" #. TRANSLATORS: CLI description msgid "This tool will read and parse the TPM event log from the system firmware." msgstr "此工具将读å–和解æžç³»ç»Ÿå›ºä»¶ä¸­çš„ TPM 事件日志。" #. TRANSLATORS: the update state of the specific device msgid "Transient failure" msgstr "暂时失败" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "类型" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "UEFI ESP 分区未检测到或未é…ç½®" #. TRANSLATORS: capsule updates are an optional BIOS feature msgid "UEFI capsule updates not available or enabled in firmware setup" msgstr "UEFI 胶囊更新在固件设置中ä¸å¯ç”¨æˆ–未å¯ç”¨" #. TRANSLATORS: program name msgid "UEFI dbx Utility" msgstr "UEFI dbx 实用工具" #. TRANSLATORS: system is not booted in UEFI mode msgid "UEFI firmware can not be updated in legacy BIOS mode" msgstr "UEFI 固件无法在传统 BIOS 模å¼ä¸‹æ›´æ–°" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI platform key" msgstr "UEFI å¹³å°å¯†é’¥" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "UEFI 安全引导" #. TRANSLATORS: error message msgid "Unable to connect to service" msgstr "无法连接到æœåŠ¡" #. TRANSLATORS: command description msgid "Unbind current driver" msgstr "解除当å‰é©±åŠ¨çš„ç»‘å®š" #. TRANSLATORS: we will now offer this firmware to the user msgid "Unblocking firmware:" msgstr "正在解除阻止固件:" #. TRANSLATORS: command description msgid "Unblocks a specific firmware from being installed" msgstr "解除阻止指定的固件被安装" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "未加密" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "未知" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "未知设备" msgid "Unlock the device to allow access" msgstr "è§£é”设备以å…许访问" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "已解é”" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "为固件访问解é”设备" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "å¸è½½ ESP 分区" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "æœªå—æ”¯æŒçš„守护进程版本 %s,客户端版本为 %s" #. TRANSLATORS: Suffix: the HSI result msgid "Untainted" msgstr "无污染" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "坿›´æ–°" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "更新错误" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "更新消æ¯" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "更新状æ€" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "更新失败为已知问题,请访问此 URL 以获å–详情:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "现在更新å—?" #. TRANSLATORS: command description msgid "Update the stored cryptographic hash with current ROM contents" msgstr "ä»¥å½“å‰ ROM 内容更新存储的加密哈希" msgid "Update the stored device verification information" msgstr "更新存储的设备验è¯ä¿¡æ¯" #. TRANSLATORS: command description msgid "Update the stored metadata with current contents" msgstr "使用当å‰å†…容更新存储的元数æ®" msgid "Updating" msgstr "正在更新" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "正在更新 %s……" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "å‡çº§ %s,从 %s 到 %s?" #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "上传固件报告å¯å¸®åŠ©ç¡¬ä»¶ä¾›åº”å•†å°½å¿«ç¡®å®šçœŸå®žè®¾å¤‡ä¸Šæ›´æ–°çš„æˆåŠŸåŠå¤±è´¥æ¡ˆä¾‹ã€‚" #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "紧急性" #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "已通知用户" #. TRANSLATORS: remote filename base msgid "Username" msgstr "用户å" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "有效" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "æ­£åœ¨éªŒè¯ ESP 内容……" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "å˜åž‹" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "供应商" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "正在验è¯â€¦" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "版本" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "等待中..." #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "监视硬件更改" #. TRANSLATORS: decompressing images from a container firmware msgid "Writing file:" msgstr "写入文件:" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "正在写入…" #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "您的å‘行商å¯èƒ½å°šæœªè®¤è¯ä»»ä½•固件的系统åŠè®¾å¤‡å…¼å®¹æ€§ã€‚" #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "你的硬件使用此固件å¯èƒ½ä¼šæŸå,而且安装此版本固件å¯èƒ½å¤±åŽ» %s çš„ä¿ä¿®ã€‚" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[CHECKSUM]" msgstr "[校验和]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID]" msgstr "[设备ID|GUID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[DEVICE-ID|GUID] [BRANCH]" msgstr "[设备ID|GUID] [分支]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILE FILE_SIG REMOTE-ID]" msgstr "[文件 文件签å 远程ID]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[FILENAME1] [FILENAME2]" msgstr "[文件å1] [文件å2]" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgid "[SMBIOS-FILE|HWIDS-FILE]" msgstr "[SMBIOS文件|HWIDS文件]" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "默认" #. TRANSLATORS: program name msgid "fwupd TPM event log utility" msgstr "fwupd TPM 事件日志工具" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "fwupd plugins" msgstr "fwupd æ’ä»¶" fwupd-2.0.10/po/zh_TW.po000066400000000000000000001605121501337203100147300ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the fwupd package. # # Translators: # Cheng-Chia Tseng , 2017-2018,2024 msgid "" msgstr "" "Project-Id-Version: fwupd\n" "Report-Msgid-Bugs-To: \n" "Language-Team: Chinese (Taiwan) (http://app.transifex.com/freedesktop/fwupd/language/zh_TW/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: zh_TW\n" "Plural-Forms: nplurals=1; plural=0;\n" #. TRANSLATORS: more than a minute #, c-format msgid "%.0f minute remaining" msgid_plural "%.0f minutes remaining" msgstr[0] "剩下 %.0f 分é˜" #. TRANSLATORS: BMC refers to baseboard management controller which #. * is the device that updates all the other firmware on the system #, c-format msgid "%s BMC Update" msgstr "「%sã€BMC æ›´æ–°" #. TRANSLATORS: battery refers to the system power source #, c-format msgid "%s Battery Update" msgstr "「%sã€é›»æ± æ›´æ–°" #. TRANSLATORS: the CPU microcode is firmware loaded onto the CPU #. * at system bootup #, c-format msgid "%s CPU Microcode Update" msgstr "「%sã€CPU 微碼更新" #. TRANSLATORS: camera can refer to the laptop internal #. * camera in the bezel or external USB webcam #, c-format msgid "%s Camera Update" msgstr "「%sã€ç›¸æ©Ÿæ›´æ–°" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Secure Boot` #, c-format msgid "%s Configuration Update" msgstr "「%sã€çµ„態設定更新" #. TRANSLATORS: ME stands for Management Engine, where #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Consumer ME Update" msgstr "「%sã€æ¶ˆè²»è€…ç´š ME æ›´æ–°" #. TRANSLATORS: the controller is a device that has other devices #. * plugged into it, for example ThunderBolt, FireWire or USB, #. * the first %s is the device name, e.g. 'Intel ThunderBolt` #, c-format msgid "%s Controller Update" msgstr "「%sã€æŽ§åˆ¶å™¨æ›´æ–°" #. TRANSLATORS: ME stands for Management Engine (with Intel AMT), #. * where the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Corporate ME Update" msgstr "「%sã€ä¼æ¥­ç´š ME æ›´æ–°" #. TRANSLATORS: a specific part of hardware, #. * the first %s is the device name, e.g. 'Unifying Receiver` #, c-format msgid "%s Device Update" msgstr "「%sã€è£ç½®æ›´æ–°" #. TRANSLATORS: Video Display refers to the laptop internal display or #. * external monitor #, c-format msgid "%s Display Update" msgstr "「%sã€é¡¯ç¤ºå™¨æ›´æ–°" #. TRANSLATORS: Dock refers to the port replicator hardware laptops are #. * cradled in, or lowered onto #, c-format msgid "%s Dock Update" msgstr "「%sã€åº§è‡ºæ›´æ–°" #. TRANSLATORS: drive refers to a storage device, e.g. SATA disk #, c-format msgid "%s Drive Update" msgstr "「%sã€è£ç½®æ›´æ–°" #. TRANSLATORS: the EC is typically the keyboard controller chip, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Embedded Controller Update" msgstr "「%sã€å…§åµŒæŽ§åˆ¶å™¨æ›´æ–°" #. TRANSLATORS: a device that can read your fingerprint pattern #, c-format msgid "%s Fingerprint Reader Update" msgstr "「%sã€æŒ‡ç´‹è®€å–器更新" #. TRANSLATORS: flash refers to solid state storage, e.g. UFS or eMMC #, c-format msgid "%s Flash Drive Update" msgstr "「%sã€å¿«é–ƒè£ç½®æ›´æ–°" #. TRANSLATORS: GPU refers to a Graphics Processing Unit, e.g. #. * the "video card" #, c-format msgid "%s GPU Update" msgstr "「%sã€GPU æ›´æ–°" #. TRANSLATORS: a large pressure-sensitive drawing area typically used #. * by artists and digital artists #, c-format msgid "%s Graphics Tablet Update" msgstr "「%sã€ç¹ªåœ–æ¿æ›´æ–°" #. TRANSLATORS: Keyboard refers to an input device for typing #, c-format msgid "%s Keyboard Update" msgstr "「%sã€éµç›¤æ›´æ–°" #. TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s ME Update" msgstr "「%sã€ME æ›´æ–°" #. TRANSLATORS: Mouse refers to a handheld input device #, c-format msgid "%s Mouse Update" msgstr "「%sã€æ»‘é¼ æ›´æ–°" #. TRANSLATORS: Network Interface refers to the physical #. * PCI card, not the logical wired connection #, c-format msgid "%s Network Interface Update" msgstr "「%sã€ç¶²è·¯ä»‹é¢æ›´æ–°" #. TRANSLATORS: SSD refers to a Solid State Drive, e.g. non-rotating #. * SATA or NVMe disk #, c-format msgid "%s SSD Update" msgstr "「%sã€SSD æ›´æ–°" #. TRANSLATORS: Storage Controller is typically a RAID or SAS adapter #, c-format msgid "%s Storage Controller Update" msgstr "「%sã€å„²å­˜æŽ§åˆ¶å™¨æ›´æ–°" #. TRANSLATORS: the entire system, e.g. all internal devices, #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s System Update" msgstr "「%sã€ç³»çµ±æ›´æ–°" #. TRANSLATORS: TPM refers to a Trusted Platform Module #, c-format msgid "%s TPM Update" msgstr "「%sã€TPM æ›´æ–°" #. TRANSLATORS: the Thunderbolt controller is a device that #. * has other high speed Thunderbolt devices plugged into it; #. * the first %s is the system name, e.g. 'ThinkPad P50` #, c-format msgid "%s Thunderbolt Controller Update" msgstr "「%sã€Thunderbolt 控制器更新" #. TRANSLATORS: TouchPad refers to a flat input device #, c-format msgid "%s Touchpad Update" msgstr "「%sã€è§¸æŽ§æ¿æ›´æ–°" #. TRANSLATORS: Dock refers to the port replicator device connected #. * by plugging in a USB cable -- which may or may not also provide power #, c-format msgid "%s USB Dock Update" msgstr "「%sã€USB 座臺更新" #. TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth #. * device that stays in the USB port so the wireless peripheral works #, c-format msgid "%s USB Receiver Update" msgstr "「%sã€USB 接收器更新" #. TRANSLATORS: this is the fallback where we don't know if the release #. * is updating the system, the device, or a device class, or something else #. -- #. * the first %s is the device name, e.g. 'ThinkPad P50` #, c-format msgid "%s Update" msgstr "「%sã€æ›´æ–°" #. TRANSLATORS: duration in days! #, c-format msgid "%u day" msgid_plural "%u days" msgstr[0] "%u 天" #. TRANSLATORS: this is shown in the MOTD #, c-format msgid "%u device has a firmware upgrade available." msgid_plural "%u devices have a firmware upgrade available." msgstr[0] "%u 個è£ç½®æœ‰å¯ç”¨çš„韌體更新。" #. TRANSLATORS: duration in minutes #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u å°æ™‚" #. TRANSLATORS: duration in minutes #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u 分é˜" #. TRANSLATORS: duration in seconds #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u ç§’" #. TRANSLATORS: Title: if hardware enforces control of SPI replays msgid "AMD Firmware Replay Protection" msgstr "AMD éŸŒé«”é‡æ”¾ä¿è­·" #. TRANSLATORS: Title: if hardware enforces control of SPI writes msgid "AMD Firmware Write Protection" msgstr "AMD 韌體寫入ä¿è­·" #. TRANSLATORS: the user needs to do something, e.g. remove the device msgid "Action Required:" msgstr "需è¦é€²è¡Œæ“作:" #. TRANSLATORS: the age of the metadata msgid "Age" msgstr "å¹´æ­²" #. TRANSLATORS: should the remote still be enabled msgid "Agree and enable the remote?" msgstr "åŒæ„並且啟用é ç«¯ç«™é»žï¼Ÿ" #. TRANSLATORS: this is a command alias, e.g. 'get-devices' #, c-format msgid "Alias to %s" msgstr "%s 的別å" #. TRANSLATORS: on some systems certain devices have to have matching #. versions, #. * e.g. the EFI driver for a given network card cannot be different msgid "All devices of the same type will be updated at the same time" msgstr "所有相åŒé¡žåž‹çš„è£ç½®æœƒåŒæ™‚一併更新" #. TRANSLATORS: command line option msgid "Allow downgrading firmware versions" msgstr "å…許é™ç´šéŸŒé«”版本" #. TRANSLATORS: command line option msgid "Allow reinstalling existing firmware versions" msgstr "å…è¨±é‡æ–°å®‰è£ç¾å­˜çš„韌體版本" #. TRANSLATORS: command line option msgid "Allow switching firmware branch" msgstr "å…許切æ›éŸŒé«”分支" #. TRANSLATORS: is not the main firmware stream msgid "Alternate branch" msgstr "替代分支" #. TRANSLATORS: another application is updating the device already msgid "An update is in progress" msgstr "有進行中的更新" #. TRANSLATORS: explain why we want to reboot msgid "An update requires a reboot to complete." msgstr "æœ‰æ›´æ–°å¿…é ˆé‡æ–°é–‹æ©Ÿæ‰èƒ½å®Œæˆã€‚" #. TRANSLATORS: command line option msgid "Answer yes to all questions" msgstr "全部的å•題都回答是" #. TRANSLATORS: actually sending the update to the hardware msgid "Applying update…" msgstr "套用更新中…" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "Approved firmware:" msgid_plural "Approved firmware:" msgstr[0] "已批准的韌體:" #. TRANSLATORS: command description msgid "Asks the daemon to quit" msgstr "請幕後程å¼é›¢é–‹" #. TRANSLATORS: command description msgid "Attach to firmware mode" msgstr "連接至韌體模å¼" #. TRANSLATORS: waiting for user to authenticate msgid "Authenticating…" msgstr "æ ¸å°ä¸­â€¦" #. TRANSLATORS: user needs to run a command msgid "Authentication details are required" msgstr "需è¦èº«ä»½æ ¸å°è³‡æ–™" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on a removable device" msgstr "必須先通éŽèº«ä»½æ ¸å°æ‰èƒ½é™ç´šå¯ç§»é™¤è£ç½®ä¸Šçš„韌體" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to downgrade the firmware on this machine" msgstr "必須先通éŽèº«ä»½æ ¸å°æ‰èƒ½é™ç´šé€™è‡ºæ©Ÿå™¨ä¸Šçš„韌體" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify BIOS settings" msgstr "必須先通éŽèº«ä»½æ ¸å°æ‰èƒ½ä¿®æ”¹ BIOS 設定" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify a configured remote used for firmware updates" msgstr "必須通éŽèº«ä»½æ ¸å°æ‰èƒ½ä¿®æ”¹è¨­å®šä½œéŸŒé«”æ›´æ–°çš„é ç«¯ç«™é»ž" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to modify daemon configuration" msgstr "必須先通éŽèº«ä»½æ ¸å°æ‰èƒ½ä¿®æ”¹å¹•後程å¼çµ„態設定" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to read BIOS settings" msgstr "必須先通éŽèº«ä»½æ ¸å°æ‰èƒ½è®€å– BIOS 設定" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to set the list of approved firmware" msgstr "必須先通éŽèº«ä»½æ ¸å°æ‰èƒ½è¨­å®šå·²æ‰¹å‡†éŸŒé«”的清單" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to sign data using the client certificate" msgstr "必須先通éŽèº«ä»½æ ¸å°æ‰èƒ½ä¿®æ”¹å®¢æˆ¶ç«¯æ†‘證簽署資料" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to switch to the new firmware version" msgstr "必須先通éŽèº«ä»½æ ¸å°æ‰èƒ½åˆ‡æ›åˆ°æ–°ç‰ˆéŸŒé«”" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to unlock a device" msgstr "必須先通éŽèº«ä»½æ ¸å°æ‰èƒ½è§£éŽ–è£ç½®" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on a removable device" msgstr "必須先通éŽèº«ä»½æ ¸å°æ‰èƒ½æ›´æ–°å¯ç§»é™¤è£ç½®ä¸Šçš„韌體" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the firmware on this machine" msgstr "必須先通éŽèº«ä»½æ ¸å°æ‰èƒ½æ›´æ–°é€™è‡ºæ©Ÿå™¨ä¸Šçš„韌體" #. TRANSLATORS: this is the PolicyKit modal dialog msgid "Authentication is required to update the stored checksums for the device" msgstr "必須先通éŽèº«ä»½æ ¸å°æ‰èƒ½æ›´æ–°è£ç½®çš„儲存校驗計算碼" #. TRANSLATORS: Boolean value to automatically send reports msgid "Automatic Reporting" msgstr "自動報告" #. TRANSLATORS: can we JFDI? msgid "Automatically upload every time?" msgstr "è¦æ¯æ¬¡è‡ªå‹•上傳嗎?" #. TRANSLATORS: Title: Whether BIOS Firmware updates is enabled msgid "BIOS Firmware Updates" msgstr "BIOS 韌體更新" #. TRANSLATORS: refers to the battery inside the peripheral device msgid "Battery" msgstr "電池" #. TRANSLATORS: version cannot be installed due to policy msgid "Blocked version" msgstr "å°éŽ–çš„ç‰ˆæœ¬" #. TRANSLATORS: firmware version of bootloader msgid "Bootloader Version" msgstr "開機載入器版本" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Branch" msgstr "分支" #. TRANSLATORS: this is to abort the interactive prompt msgid "Cancel" msgstr "å–æ¶ˆ" #. TRANSLATORS: this is when a device ctrl+c's a watch #. TRANSLATORS: this is from ctrl+c msgid "Cancelled" msgstr "已喿¶ˆ" #. TRANSLATORS: this is when the daemon state changes msgid "Changed" msgstr "已變更" #. TRANSLATORS: hash to that exact firmware archive #. TRANSLATORS: remote checksum msgid "Checksum" msgstr "校驗碼" #. TRANSLATORS: get interactive prompt, where branch is the #. * supplier of the firmware, e.g. "non-free" or "free" msgid "Choose branch" msgstr "鏿“‡åˆ†æ”¯" #. TRANSLATORS: get interactive prompt msgid "Choose device" msgstr "鏿“‡è£ç½®" #. TRANSLATORS: get interactive prompt msgid "Choose firmware" msgstr "鏿“‡éŸŒé«”" #. TRANSLATORS: get interactive prompt msgid "Choose volume" msgstr "鏿“‡å·å†Š" #. TRANSLATORS: command description msgid "Clears the results from the last update" msgstr "æ¸…é™¤ä¸Šæ¬¡æ›´æ–°çš„çµæžœ" #. TRANSLATORS: error message msgid "Command not found" msgstr "找ä¸åˆ°å‘½ä»¤" #. TRANSLATORS: is not supported by the vendor msgid "Community supported" msgstr "社群支æ´" #. TRANSLATORS: title prefix for the BIOS settings dialog msgid "Configuration Change Suggested" msgstr "建議修改組態設定" #. TRANSLATORS: no peeking msgid "Configuration is only readable by the system administrator" msgstr "組態設定åªèƒ½ç”±ç³»çµ±ç®¡ç†å“¡è®€å–" #. TRANSLATORS: when the update was built msgid "Created" msgstr "建立時間" #. TRANSLATORS: the release urgency msgid "Critical" msgstr "é‡å¤§" #. TRANSLATORS: Device supports some form of checksum verification msgid "Cryptographic hash verification is available" msgstr "密碼學雜湊核驗å¯ä»¥ä½¿ç”¨" #. TRANSLATORS: current value of a BIOS setting msgid "Current Value" msgstr "ç›®å‰çš„值" #. TRANSLATORS: version number of current firmware msgid "Current version" msgstr "ç›®å‰çš„版本" #. TRANSLATORS: for the --verbose arg msgid "Debugging Options" msgstr "除錯é¸é …" #. TRANSLATORS: decompressing the firmware file msgid "Decompressing…" msgstr "解壓縮中…" #. TRANSLATORS: description of BIOS setting #. TRANSLATORS: multiline description of device msgid "Description" msgstr "æè¿°èªªæ˜Ž" #. TRANSLATORS: command description msgid "Detach to bootloader mode" msgstr "斷開至開機載入器模å¼" #. TRANSLATORS: more details about the update link msgid "Details" msgstr "詳細資訊" #. TRANSLATORS: description of device ability msgid "Device Flags" msgstr "è£ç½®æ——標" #. TRANSLATORS: ID for hardware, typically a SHA1 sum msgid "Device ID" msgstr "è£ç½® ID" #. TRANSLATORS: description of the device requests msgid "Device Requests" msgstr "è£ç½®è«‹æ±‚" #. TRANSLATORS: this is when a device is hotplugged msgid "Device added:" msgstr "è£ç½®å·²åŠ å…¥ï¼š" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse msgid "Device battery power is too low" msgstr "è£ç½®é›»æ± é›»é‡éŽä½Ž" #. TRANSLATORS: for example the batteries *inside* the Bluetooth mouse #, c-format msgid "Device battery power is too low (%u%%, requires %u%%)" msgstr "è£ç½®é›»æ± é›»é‡éŽä½Žï¼ˆ%u%%ï¼Œè¦æ±‚為 %u%%)" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device can recover flash failures" msgstr "è£ç½®å¯ä»¥å¾žé–ƒåˆ·å¤±æ•—中還原" #. TRANSLATORS: this is when a device has been updated msgid "Device changed:" msgstr "è£ç½®å·²è®Šæ›´ï¼š" #. TRANSLATORS: a version check is required for all firmware msgid "Device firmware is required to have a version check" msgstr "è£ç½®éŸŒé«”需è¦é€²è¡Œç‰ˆæœ¬æª¢æŸ¥" #. TRANSLATORS: device cannot be interrupted, for instance taking a phone call msgid "Device is in use" msgstr "è£ç½®æ­£åœ¨ä½¿ç”¨ä¸­" #. TRANSLATORS: Is locked and can be unlocked msgid "Device is locked" msgstr "è£ç½®å·²éŽ–å®š" #. TRANSLATORS: the device cannot update from A->C and has to go A->B->C msgid "Device is required to install all provided releases" msgstr "è£ç½®éœ€è¦å®‰è£æ¯ä¸€å€‹æä¾›çš„發行版本" #. TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode msgid "Device is unreachable, or out of wireless range" msgstr "è£ç½®ç„¡æ³•觸åŠï¼Œæˆ–是超出無線收訊範åœ" #. TRANSLATORS: Device remains usable during update msgid "Device is usable for the duration of the update" msgstr "è£ç½®åœ¨æ›´æ–°æœŸé–“å¯ä»¥ä½¿ç”¨" #. TRANSLATORS: usually this is when we're waiting for a reboot msgid "Device is waiting for the update to be applied" msgstr "è£ç½®æ­£åœ¨ç­‰å€™æ›´æ–°å¥—用" #. TRANSLATORS: this is when a device is hotplugged msgid "Device removed:" msgstr "è£ç½®å·²ç§»é™¤ï¼š" #. TRANSLATORS: as in, wired mains power for a laptop msgid "Device requires AC power to be connected" msgstr "è£ç½®è¦æ±‚連接 AC é›»æº" #. TRANSLATORS: The device cannot be updated due to missing vendor's license." msgid "Device requires a software license to update" msgstr "è£ç½®éœ€è¦è»Ÿé«”授權æ‰èƒ½æ›´æ–°" #. TRANSLATORS: Device supports a safety mechanism for flashing msgid "Device stages updates" msgstr "è£ç½®åˆ†æ®µå‚™å¾…æ›´æ–°" #. TRANSLATORS: there is more than one supplier of the firmware msgid "Device supports switching to a different branch of firmware" msgstr "è£ç½®æ”¯æ´åˆ‡æ›åˆ°ä¸åŒåˆ†æ”¯çš„韌體" #. TRANSLATORS: Device update needs to be separately activated msgid "Device update needs activation" msgstr "è£ç½®æ›´æ–°éœ€è¦å€‹åˆ¥å•Ÿå‹•" #. TRANSLATORS: Device will not return after update completes msgid "Device will not re-appear after update completes" msgstr "è£ç½®åœ¨æ›´æ–°å®Œæˆå¾Œä¸æœƒå†æ¬¡å‡ºç¾" #. TRANSLATORS: a list of successful updates msgid "Devices that have been updated successfully:" msgstr "æˆåŠŸæ›´æ–°çš„è£ç½®ï¼š" #. TRANSLATORS: a list of failed updates msgid "Devices that were not updated correctly:" msgstr "無法正確更新的è£ç½®ï¼š" #. TRANSLATORS: Plugin is inactive and not used #. TRANSLATORS: Suffix: the HSI result msgid "Disabled" msgstr "å·²åœç”¨" #. TRANSLATORS: command description msgid "Disables a given remote" msgstr "åœç”¨æŒ‡å®šçš„é ç«¯ç«™é»ž" #. TRANSLATORS: the OS the release was tested on msgid "Distribution" msgstr "散布版" #. TRANSLATORS: command line option msgid "Do not check for old metadata" msgstr "ä¸è¦æª¢æŸ¥ä¸­ä»‹è³‡æ–™æ˜¯å¦è€èˆŠ" #. TRANSLATORS: command line option msgid "Do not check for unreported history" msgstr "ä¸è¦æª¢æŸ¥æ˜¯å¦æœ‰å°šæœªå ±å‘Šçš„æ­·å²" #. TRANSLATORS: command line option msgid "Do not check if download remotes should be enabled" msgstr "ä¸è¦æª¢æŸ¥ä¸‹è¼‰é ç«¯ç«™é»žæ˜¯å¦æ‡‰è¢«å•Ÿç”¨" #. TRANSLATORS: turn on all debugging msgid "Do not include timestamp prefix" msgstr "ä¸è¦åŒ…嫿™‚間戳å‰ç¶´" #. TRANSLATORS: command line option msgid "Do not write to the history database" msgstr "ä¸è¦å¯«å…¥æ­·å²è³‡æ–™åº«ä¸­" #. TRANSLATORS: should the branch be changed msgid "Do you understand the consequences of changing the firmware branch?" msgstr "您是å¦çž­è§£åˆ‡æ›éŸŒé«”分支的後果?" #. TRANSLATORS: ask if we can update the metadata msgid "Do you want to refresh this remote now?" msgstr "您è¦ç¾åœ¨é‡æ–°æ•´ç†é€™å€‹é ç«¯ç«™é»žå—Žï¼Ÿ" #. TRANSLATORS: success msgid "Done!" msgstr "完æˆï¼" #. TRANSLATORS: message letting the user know an downgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Downgrade %s from %s to %s?" msgstr "è¦å°‡ %s 從 %s é™ç´šåˆ° %s 嗎?" #. TRANSLATORS: command description msgid "Downgrades the firmware on a device" msgstr "é™ç´šè£ç½®çš„韌體" #. TRANSLATORS: %1 is a device name #, c-format msgid "Downgrading %s…" msgstr "正在é™ç´š %s…" #. TRANSLATORS: downloading from a remote server msgid "Downloading…" msgstr "下載中…" #. TRANSLATORS: command description msgid "Dump SMBIOS data from a file" msgstr "å¾žæª”æ¡ˆå‚¾å° SMBIOUS 資料" #. TRANSLATORS: length of time the update takes to apply msgid "Duration" msgstr "時間" #. TRANSLATORS: this device is not actually real msgid "Emulated" msgstr "模擬" msgid "Enable" msgstr "啟用" #. TRANSLATORS: a remote here is like a 'repo' or software source msgid "Enable new remote?" msgstr "啟用新é ç«¯ç«™é»žï¼Ÿ" #. TRANSLATORS: Turn on the remote msgid "Enable this remote?" msgstr "啟用此é ç«¯ç«™é»žï¼Ÿ" #. TRANSLATORS: if the remote is enabled #. TRANSLATORS: Suffix: the HSI result msgid "Enabled" msgstr "啟用" #. TRANSLATORS: Plugin is active only if hardware is found msgid "Enabled if hardware matches" msgstr "硬體相符æ‰å•Ÿç”¨" #. TRANSLATORS: command description msgid "Enables a given remote" msgstr "啟用指定的é ç«¯ç«™é»ž" msgid "Enabling this functionality is done at your own risk, which means you have to contact your original equipment manufacturer regarding any problems caused by these updates. Only problems with the update process itself should be filed at $OS_RELEASE:BUG_REPORT_URL$." msgstr "啟用此功能必須自負風險。這代表若您é­é‡åˆ°é€™äº›æ›´æ–°å°Žè‡´çš„任何å•題,您必須å‘原始設備供應商è¯çµ¡ä¸¦åæ‡‰ã€‚åªæœ‰åœ¨æ‚¨é‡åˆ°æ›´æ–°éŽç¨‹æœ¬èº«çš„å•é¡Œæ™‚ï¼Œæ‰æ˜¯å‘ $OS_RELEASE:BUG_REPORT_URL$ 回報。" #. TRANSLATORS: show the user a warning msgid "Enabling this remote is done at your own risk." msgstr "è‹¥è¦å•Ÿç”¨æ­¤é ç«¯ç«™é»žï¼Œè«‹è‡ªè² é¢¨éšªã€‚" #. TRANSLATORS: Suffix: the HSI result msgid "Encrypted" msgstr "有加密" #. TRANSLATORS: the vendor is no longer supporting the device msgid "End of life" msgstr "ç”Ÿå‘½é€±æœŸçµæŸ" #. TRANSLATORS: The BIOS setting can only be changed to fixed values msgid "Enumeration" msgstr "列舉" #. TRANSLATORS: command description msgid "Erase all firmware update history" msgstr "抹除所有韌體更新歷å²" #. TRANSLATORS: erasing contents of the flash chips msgid "Erasing…" msgstr "抹除中…" #. TRANSLATORS: exit after we've started up, used for user profiling msgid "Exit after a small delay" msgstr "ç¶“éŽä¸€æ®µçŸ­æš«å»¶é²å¾Œä¾¿é›¢é–‹" #. TRANSLATORS: exit straight away, used for automatic profiling msgid "Exit after the engine has loaded" msgstr "引擎載入後離開" #. TRANSLATORS: the update state of the specific device msgid "Failed" msgstr "失敗" #. TRANSLATORS: dbx file failed to be applied as an update msgid "Failed to apply update" msgstr "無法套用更新" #. TRANSLATORS: error message for Windows msgid "Failed to connect to Windows service, please ensure it's running." msgstr "無法連線到 Windows æœå‹™ï¼Œè«‹ç¢ºå®šå®ƒæœ‰åœ¨åŸ·è¡Œã€‚" #. TRANSLATORS: could not contact the fwupd service over D-Bus msgid "Failed to connect to daemon" msgstr "無法連線到幕後程å¼" #. TRANSLATORS: could not read existing system data #. TRANSLATORS: could not read file msgid "Failed to load local dbx" msgstr "無法載入本機 dbx" #. TRANSLATORS: could not read existing system data msgid "Failed to load system dbx" msgstr "無法載入系統 dbx" #. TRANSLATORS: another fwupdtool instance is already running msgid "Failed to lock" msgstr "鎖定失敗" #. TRANSLATORS: the user didn't read the man page msgid "Failed to parse arguments" msgstr "無法剖æžå¼•數" #. TRANSLATORS: failed to read measurements file msgid "Failed to parse file" msgstr "ç„¡æ³•å‰–æžæª”案" #. TRANSLATORS: could not parse file msgid "Failed to parse local dbx" msgstr "ç„¡æ³•å‰–æžæœ¬æ©Ÿ dbx" #. TRANSLATORS: a feature is something like "can show an image" msgid "Failed to set front-end features" msgstr "無法設定å‰ç«¯åŠŸèƒ½" #. TRANSLATORS: something with a blocked hash exists #. * in the users ESP -- which would be bad! msgid "Failed to validate ESP contents" msgstr "無法核驗 ESP 內容" #. TRANSLATORS: item is FALSE msgid "False" msgstr "False" #. TRANSLATORS: filename of the local file msgid "Filename" msgstr "檔å" #. TRANSLATORS: filename of the local file msgid "Filename Signature" msgstr "檔å簽章" #. TRANSLATORS: full path of the remote.conf file msgid "Filename Source" msgstr "檔å來æº" #. TRANSLATORS: Title: if we can verify the firmware checksums msgid "Firmware Attestation" msgstr "韌體核實" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware BIOS Descriptor" msgstr "韌體 BIOS æè¿°å…ƒ" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "Firmware BIOS Region" msgstr "韌體 BIOS 領域" #. TRANSLATORS: remote URI msgid "Firmware Base URI" msgstr "韌體基礎 URI" #. TRANSLATORS: program summary msgid "Firmware Update D-Bus Service" msgstr "韌體更新 D-Bus æœå‹™" #. TRANSLATORS: program name msgid "Firmware Update Daemon" msgstr "韌體更新幕後程å¼" #. TRANSLATORS: Title: if the fwupd plugins are all present and correct msgid "Firmware Updater Verification" msgstr "韌體更新器驗證" #. TRANSLATORS: Title: if firmware updates are available msgid "Firmware Updates" msgstr "韌體更新" #. TRANSLATORS: program name msgid "Firmware Utility" msgstr "韌體公用程å¼" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection" msgstr "韌體寫入ä¿è­·" #. TRANSLATORS: Title: firmware refers to the flash chip in the computer msgid "Firmware Write Protection Lock" msgstr "韌體寫入ä¿è­·éŽ–" #. TRANSLATORS: the metadata is very out of date; %u is a number > 1 #, c-format msgid "Firmware metadata has not been updated for %u day and may not be up to date." msgid_plural "Firmware metadata has not been updated for %u days and may not be up to date." msgstr[0] "韌體中介資料已有 %u 天未更新,å¯èƒ½ä¸æ˜¯æœ€æ–°ç‹€æ…‹ã€‚" #. TRANSLATORS: we've fixed a security problem on the machine msgid "Fixed successfully" msgstr "å·²æˆåŠŸä¿®æ­£" #. TRANSLATORS: description of plugin state, e.g. disabled msgid "Flags" msgstr "旗標" #. TRANSLATORS: Suffix: the HSI result msgid "Found" msgstr "有找到" #. TRANSLATORS: title text, shown as a warning msgid "Full Disk Encryption Detected" msgstr "已嵿¸¬åˆ°å…¨ç£ç¢ŸåР坆" #. TRANSLATORS: global ID common to all similar hardware msgid "GUID" msgid_plural "GUIDs" msgstr[0] "GUID" #. TRANSLATORS: command argument: uppercase, spaces->dashes msgctxt "command-argument" msgid "GUID" msgstr "GUID" msgid "Get BIOS settings" msgstr "å–å¾— BIOS 設定" #. TRANSLATORS: command description msgid "Get all devices that support firmware updates" msgstr "å–得所有支æ´éŸŒé«”æ›´æ–°çš„è£ç½®" #. TRANSLATORS: command description msgid "Get all enabled plugins registered with the system" msgstr "å–得所有系統中註冊的啟用æ’ä»¶" #. TRANSLATORS: command description msgid "Get device report metadata" msgstr "å–å¾—è£ç½®å ±å‘Šä¸­ä»‹è³‡æ–™" #. TRANSLATORS: command description msgid "Gets details about a firmware file" msgstr "å–得韌體檔案的相關細節" #. TRANSLATORS: command description msgid "Gets the configured remotes" msgstr "å–得設定的é ç«¯ç«™é»ž" #. TRANSLATORS: firmware approved by the admin msgid "Gets the list of approved firmware" msgstr "å–得已批准韌體的清單" #. TRANSLATORS: command description msgid "Gets the list of updates for connected hardware" msgstr "å–得連接硬體的更新清單" #. TRANSLATORS: command description msgid "Gets the releases for a device" msgstr "å–å¾—è£ç½®çš„發行版本" #. TRANSLATORS: command description msgid "Gets the results from the last update" msgstr "å–å¾—ä¸Šæ¬¡æ›´æ–°çš„çµæžœ" #. TRANSLATORS: the hardware is waiting to be replugged msgid "Hardware is waiting to be replugged" msgstr "ç¡¬é«”æ­£åœ¨ç­‰å€™é‡æ–°æ’å…¥" #. TRANSLATORS: the release urgency msgid "High" msgstr "高" #. TRANSLATORS: Title: #. * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit msgid "IOMMU Protection" msgstr "IOMMU ä¿è­·" #. TRANSLATORS: daemon is inactive msgid "Idle…" msgstr "閒置…" #. TRANSLATORS: show the user a generic image that can be themed msgid "Image" msgstr "å½±åƒ" #. TRANSLATORS: show the user a random image from the internet msgid "Image (custom)" msgstr "å½±åƒï¼ˆè‡ªè¨‚)" #. TRANSLATORS: length of time the update takes to apply msgid "Install Duration" msgstr "å®‰è£æ™‚é–“" msgid "Install old version of signed system firmware" msgstr "安è£å·²ç°½ç½²çš„舊版系統韌體" msgid "Install old version of unsigned system firmware" msgstr "å®‰è£æœªç°½ç½²çš„舊版系統韌體" msgid "Install signed device firmware" msgstr "安è£å·²ç°½ç½²çš„è£ç½®éŸŒé«”" msgid "Install signed system firmware" msgstr "安è£å·²ç°½ç½²çš„系統韌體" msgid "Install unsigned device firmware" msgstr "å®‰è£æœªç°½ç½²çš„è£ç½®éŸŒé«”" msgid "Install unsigned system firmware" msgstr "å®‰è£æœªç°½ç½²çš„系統韌體" #. TRANSLATORS: this is shown when updating the firmware after the reboot msgid "Installing firmware update…" msgstr "安è£éŸŒé«”更新中…" #. TRANSLATORS: %1 is a device name #, c-format msgid "Installing on %s…" msgstr "正安è£åˆ° %s…" #. TRANSLATORS: The BIOS setting only accepts integers in a fixed range msgid "Integer" msgstr "整數" #. TRANSLATORS: Title: BootGuard is a trademark from Intel msgid "Intel BootGuard" msgstr "Intel BootGuard" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * ACM means to verify the integrity of Initial Boot Block msgid "Intel BootGuard ACM Protected" msgstr "Intel BootGuard ACM ä¿è­·" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * error policy is what to do on failure msgid "Intel BootGuard Error Policy" msgstr "Intel BootGuard 錯誤政策" #. TRANSLATORS: Title: BootGuard is a trademark from Intel, #. * verified boot refers to the way the boot process is verified msgid "Intel BootGuard Verified Boot" msgstr "Intel BootGuard 驗證開機" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Manufacturing Mode" msgstr "Intel 管ç†å¼•擎出廠模å¼" #. TRANSLATORS: Title: MEI = Intel Management Engine, and the "override" is #. enabled #. * with a jumper -- luckily it is probably not accessible to end users on #. consumer #. * boards msgid "Intel Management Engine Override" msgstr "Intel 管ç†å¼•擎凌駕" #. TRANSLATORS: Title: MEI = Intel Management Engine msgid "Intel Management Engine Version" msgstr "Intel 管ç†å¼•擎版本" #. TRANSLATORS: Device cannot be removed easily msgid "Internal device" msgstr "內部è£ç½®" #. TRANSLATORS: Suffix: the HSI result msgid "Invalid" msgstr "無效" #. TRANSLATORS: error message msgid "Invalid arguments" msgstr "無效的引數" #. TRANSLATORS: version is older msgid "Is downgrade" msgstr "是é™ç´š" #. TRANSLATORS: Is currently in bootloader mode msgid "Is in bootloader mode" msgstr "是開機載入器模å¼" #. TRANSLATORS: version is newer msgid "Is upgrade" msgstr "是å‡ç´š" #. TRANSLATORS: issue fixed with the release, e.g. CVE msgid "Issue" msgid_plural "Issues" msgstr[0] "議題" #. TRANSLATORS: HSI event title msgid "Kernel lockdown disabled" msgstr "核心鎖定已åœç”¨" #. TRANSLATORS: HSI event title msgid "Kernel lockdown enabled" msgstr "核心鎖定已啟用" #. TRANSLATORS: the original time/date the device was modified msgid "Last modified" msgstr "上次修改日期" #. TRANSLATORS: time remaining for completing firmware flash msgid "Less than one minute remaining" msgstr "剩ä¸åˆ°ä¸€åˆ†é˜" #. TRANSLATORS: e.g. GPLv2+, Proprietary etc msgid "License" msgstr "æŽˆæ¬Šæ¢æ¬¾" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux Kernel Lockdown" msgstr "Linux 核心鎖定" msgid "Linux Kernel Swap temporarily saves information to disk as you work. If the information is not protected, it could be accessed by someone if they obtained the disk." msgstr "Linux æ ¸å¿ƒäº¤æ›æ©Ÿåˆ¶æœƒåœ¨æ‚¨é€²è¡Œä½œæ¥­æ™‚將一些資訊暫時儲存到ç£ç¢Ÿä¸Šã€‚如果這些資訊沒有ä¿è­·ï¼Œåˆ¥äººåªè¦æ‹¿åˆ°ç£ç¢Ÿå°±èƒ½å¤ å­˜å–它。" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux Kernel Verification" msgstr "Linux 核心驗證" #. TRANSLATORS: Title: swap space or swap partition msgid "Linux Swap" msgstr "Linux 交æ›ç©ºé–“" msgid "Linux Vendor Firmware Service (stable firmware)" msgstr "Linux 廠商韌體æœå‹™ï¼ˆç©©å®šç‰ˆéŸŒé«”)" msgid "Linux Vendor Firmware Service (testing firmware)" msgstr "Linux 廠商韌體æœå‹™ï¼ˆæ¸¬è©¦ä¸­éŸŒé«”)" #. TRANSLATORS: Title: if it's tainted or not msgid "Linux kernel" msgstr "Linux 核心" #. TRANSLATORS: Title: lockdown is a security mode of the kernel msgid "Linux kernel lockdown" msgstr "Linux 核心鎖定" #. TRANSLATORS: command line option msgid "List entries in dbx" msgstr "列出 dbx 中的æ¢ç›®" #. TRANSLATORS: command description msgid "List the available firmware types" msgstr "列出å¯ç”¨çš„韌體類型" #. TRANSLATORS: command description msgid "Lists files on the ESP" msgstr "列出 ESP 上的檔案" #. TRANSLATORS: parsing the firmware information msgid "Loading…" msgstr "載入中…" #. TRANSLATORS: Suffix: the HSI result msgid "Locked" msgstr "有鎖定" #. TRANSLATORS: the release urgency msgid "Low" msgstr "低" #. TRANSLATORS: Longest valid string for BIOS setting msgid "Maximum length" msgstr "最大長度" #. TRANSLATORS: Highest valid integer for BIOS setting msgid "Maximum value" msgstr "最大值" #. TRANSLATORS: the release urgency msgid "Medium" msgstr "中" #. TRANSLATORS: ask the user to do a simple task which should be translated msgid "Message" msgstr "訊æ¯" #. TRANSLATORS: ask the user a question, and it will not be translated msgid "Message (custom)" msgstr "訊æ¯ï¼ˆè‡ªè¨‚)" #. TRANSLATORS: remote URI msgid "Metadata Signature" msgstr "中介資料簽章" #. TRANSLATORS: remote URI msgid "Metadata URI" msgstr "中介資料 URI" #. TRANSLATORS: explain why no metadata available msgid "Metadata can be obtained from the Linux Vendor Firmware Service." msgstr "中介資料å¯å¾ž Linux 廠商韌體æœå‹™å–得。" #. TRANSLATORS: smallest version number installable on device msgid "Minimum Version" msgstr "最å°ç‰ˆæœ¬" #. TRANSLATORS: Shortest valid string for BIOS setting msgid "Minimum length" msgstr "最短長度" #. TRANSLATORS: Lowest valid integer for BIOS setting msgid "Minimum value" msgstr "最å°å€¼" #. TRANSLATORS: sets something in the daemon configuration file msgid "Modifies a daemon configuration value" msgstr "修改一個幕後程å¼çš„組態設定值" #. TRANSLATORS: command description msgid "Modifies a given remote" msgstr "修改指定的é ç«¯ç«™é»ž" msgid "Modify a configured remote" msgstr "修改設定的é ç«¯ç«™é»ž" msgid "Modify daemon configuration" msgstr "修改幕後程å¼çµ„態設定" #. TRANSLATORS: command description msgid "Monitor the daemon for events" msgstr "ç›£æŽ§å¹•å¾Œç¨‹å¼æ˜¯å¦æœ‰æ´»å‹•" #. TRANSLATORS: command description msgid "Mounts the ESP" msgstr "掛載 ESP" #. TRANSLATORS: Requires a reboot to apply firmware or to reload hardware msgid "Needs a reboot after installation" msgstr "需è¦åœ¨å®‰è£å¾Œé‡æ–°é–‹æ©Ÿ" #. TRANSLATORS: the update state of the specific device msgid "Needs reboot" msgstr "需è¦é‡æ–°é–‹æ©Ÿ" #. TRANSLATORS: Requires system shutdown to apply firmware msgid "Needs shutdown after installation" msgstr "需è¦åœ¨å®‰è£å¾Œé—œæ©Ÿ" #. TRANSLATORS: version number of new firmware msgid "New version" msgstr "新版本" #. TRANSLATORS: user did not tell the tool what to do msgid "No action specified!" msgstr "未指定動作ï¼" #. TRANSLATORS: message letting the user know no device downgrade available #. * %1 is the device name #, c-format msgid "No downgrades for %s" msgstr "「%sã€æ²’有å¯ç”¨çš„é™ç´š" #. TRANSLATORS: nothing found msgid "No firmware IDs found" msgstr "沒有找到韌體 ID" #. TRANSLATORS: nothing found msgid "No firmware found" msgstr "沒有找到韌體" #. TRANSLATORS: nothing attached that can be upgraded msgid "No hardware detected with firmware update capability" msgstr "æœªåµæ¸¬åˆ°å…·å‚™éŸŒé«”更新能力的硬體" #. TRANSLATORS: no repositories to download from msgid "No releases available" msgstr "沒有å¯ç”¨çš„發行版本" #. TRANSLATORS: explain why no metadata available msgid "No remotes are currently enabled so no metadata is available." msgstr "ç›®å‰æ²’有啟用的é ç«¯ç«™é»žï¼Œå› è€Œæ²’有中介資料å¯ç”¨ã€‚" #. TRANSLATORS: no repositories to download from msgid "No remotes available" msgstr "沒有å¯ç”¨çš„é ç«¯ç«™é»ž" #. TRANSLATORS: this is an error string msgid "No updates available" msgstr "沒有å¯ç”¨çš„æ›´æ–°" #. TRANSLATORS: version cannot be installed due to policy msgid "Not approved" msgstr "未被批准" #. TRANSLATORS: Suffix: the HSI result msgid "Not found" msgstr "找ä¸åˆ°" #. TRANSLATORS: Suffix: the HSI result msgid "Not supported" msgstr "䏿”¯æ´" #. TRANSLATORS: Suffix: the HSI result msgid "OK" msgstr "確定" #. TRANSLATORS: the firmware old version msgid "Old version" msgstr "舊版本" #. TRANSLATORS: some devices can only be updated to a new semver and cannot #. * be downgraded or reinstalled with the existing version msgid "Only version upgrades are allowed" msgstr "僅å…許版本å‡ç´š" #. TRANSLATORS: command line option msgid "Override the default ESP path" msgstr "凌駕é è¨­ ESP 路徑" #. TRANSLATORS: reading new dbx from the update msgid "Parsing dbx update…" msgstr "æ­£åœ¨å‰–æž dbx 更新…" #. TRANSLATORS: reading existing dbx from the system msgid "Parsing system dbx…" msgstr "正在剖æžç³»çµ± dbx…" #. TRANSLATORS: remote filename base msgid "Password" msgstr "密碼" msgid "Payload" msgstr "實載" #. TRANSLATORS: the update state of the specific device msgid "Pending" msgstr "待處ç†" #. TRANSLATORS: prompt to apply the update msgid "Perform operation?" msgstr "進行æ“作?" #. TRANSLATORS: Title: Allows debugging of parts using proprietary hardware msgid "Platform Debugging" msgstr "平臺除錯" #. TRANSLATORS: 'recovery key' here refers to a code, rather than a physical #. metal thing msgid "Please ensure you have the volume recovery key before continuing." msgstr "在繼續之å‰ï¼Œè«‹å…ˆç¢ºèªæ‚¨æœ‰å„²å­˜å€é‚„原金鑰。" #. TRANSLATORS: the user isn't reading the question #, c-format msgid "Please enter a number from 0 to %u: " msgstr "請輸入 0 到 %u 之間的數字:" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA Protection" msgstr "é å…ˆé–‹æ©Ÿ DMA ä¿è­·" #. TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack msgid "Pre-boot DMA protection" msgstr "é å…ˆé–‹æ©Ÿ DMA ä¿è­·" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is disabled" msgstr "é å…ˆé–‹æ©Ÿ DMA ä¿è­·å·²åœç”¨" #. TRANSLATORS: HSI event title msgid "Pre-boot DMA protection is enabled" msgstr "é å…ˆé–‹æ©Ÿ DMA ä¿è­·å·²å•Ÿç”¨" #. TRANSLATORS: longer description msgid "Pre-boot DMA protection prevents devices from accessing system memory after being connected to the computer." msgstr "「é å…ˆé–‹æ©Ÿ DMA ä¿è­·ã€é˜²æ­¢è£ç½®åœ¨é€£æŽ¥åˆ°é›»è…¦ä¸Šä¹‹å¾Œå­˜å–系統記憶體。" #. TRANSLATORS: version number of previous firmware msgid "Previous version" msgstr "å‰ä¸€å€‹ç‰ˆæœ¬" #. TRANSLATORS: the numeric priority msgid "Priority" msgstr "優先等級" #. TRANSLATORS: reasons the device is not updatable msgid "Problems" msgstr "å•題" msgid "Proceed with upload?" msgstr "繼續上傳?" #. TRANSLATORS: Title: if fwupd supports HSI on this chip msgid "Processor Security Checks" msgstr "處ç†å™¨å®‰å…¨æ€§æª¢æŸ¥" #. TRANSLATORS: a non-free software license msgid "Proprietary" msgstr "專有" #. TRANSLATORS: BIOS setting is read only msgid "Read Only" msgstr "唯讀" #. TRANSLATORS: %1 is a device name #, c-format msgid "Reading from %s…" msgstr "正從 %s 讀å–…" #. TRANSLATORS: reading from the flash chips msgid "Reading…" msgstr "讀å–中…" #. TRANSLATORS: how often we should refresh the metadata msgid "Refresh Interval" msgstr "釿–°æ•´ç†é–“éš”" #. TRANSLATORS: command description msgid "Refresh metadata from remote server" msgstr "釿•´é ç«¯ä¼ºæœå™¨çš„中介資料" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 is a version string #, c-format msgid "Reinstall %s to %s?" msgstr "è¦é‡æ–°å®‰è£ %s 到 %s 嗎?" #. TRANSLATORS: command description msgid "Reinstall firmware on a device" msgstr "在è£ç½®ä¸Šé‡æ–°å®‰è£éŸŒé«”" #. TRANSLATORS: the stream of firmware, e.g. nonfree msgid "Release Branch" msgstr "發行版分支" #. TRANSLATORS: release attributes msgid "Release Flags" msgstr "發行版旗標" #. TRANSLATORS: the exact component on the server msgid "Release ID" msgstr "發行版 ID" #. TRANSLATORS: the server the file is coming from #. TRANSLATORS: remote identifier, e.g. lvfs-testing msgid "Remote ID" msgstr "é ç«¯ ID" #. TRANSLATORS: URI to send success/failure reports msgid "Report URI" msgstr "報告 URI" #. TRANSLATORS: Has been reported to a metadata server msgid "Reported to remote server" msgstr "已報告給é ç«¯ä¼ºæœå™¨" #. TRANSLATORS: the user is using Gentoo/Arch and has screwed something up msgid "Required efivarfs filesystem was not found" msgstr "沒有找到需è¦çš„ efivarfs 檔案系統" #. TRANSLATORS: not required for this system msgid "Required hardware was not found" msgstr "沒有找到需è¦çš„硬體" #. TRANSLATORS: Requires a bootloader mode to be manually enabled by the user msgid "Requires a bootloader" msgstr "è¦æ±‚è¦æœ‰é–‹æ©Ÿè¼‰å…¥å™¨" #. TRANSLATORS: metadata is downloaded msgid "Requires internet connection" msgstr "必須有網際網路連線" #. TRANSLATORS: reboot to apply the update msgid "Restart now?" msgstr "ç¾åœ¨é‡æ–°å•Ÿå‹•?" #. TRANSLATORS: changes only take effect on restart msgid "Restart the daemon to make the change effective?" msgstr "è¦é‡æ–°å•Ÿå‹•幕後程å¼ä¾†ä½¿è®Šæ›´ç”Ÿæ•ˆå—Žï¼Ÿ" #. TRANSLATORS: restarting the device to pick up new F/W msgid "Restarting device…" msgstr "釿–°å•Ÿå‹•è£ç½®ä¸­â€¦" #. TRANSLATORS: command description msgid "Return all the hardware IDs for the machine" msgstr "回傳所有機器的硬體 ID" #. TRANSLATORS: ask the user to upload msgid "Review and upload report now?" msgstr "ç¾åœ¨å¯©é–±å¾Œä¸Šå‚³å ±å‘Šå—Žï¼Ÿ" #. TRANSLATORS: The kernel does not support this plugin msgid "Running kernel is too old" msgstr "執行中的核心太舊了" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS Descriptor" msgstr "SPI BIOS 敘述文字" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI BIOS region" msgstr "SPI BIOS å€åŸŸ" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI lock" msgstr "SPI 鎖定" #. TRANSLATORS: Title: SPI refers to the flash chip in the computer msgid "SPI write" msgstr "SPI 寫入" #. TRANSLATORS: scheduling an update to be done on the next boot msgid "Scheduling…" msgstr "排程中…" #. TRANSLATORS: HSI event title msgid "Secure Boot disabled" msgstr "安全開機已åœç”¨" #. TRANSLATORS: HSI event title msgid "Secure Boot enabled" msgstr "安全開機已啟用" #. TRANSLATORS: the %1 is a URL to a wiki page #, c-format msgid "See %s for more details." msgstr "更多資訊請見 %s。" #. TRANSLATORS: %s is a link to a website #, c-format msgid "See %s for more information." msgstr "更多資訊請見 %s。" #. TRANSLATORS: device has been chosen by the daemon for the user msgid "Selected device" msgstr "å·²é¸è£ç½®" #. TRANSLATORS: Volume has been chosen by the user msgid "Selected volume" msgstr "å·²é¸å·å†Š" #. TRANSLATORS: serial number of hardware msgid "Serial Number" msgstr "åºè™Ÿ" msgid "Set one or more BIOS settings" msgstr "設定一個或多個 BIOS é¸é …" #. TRANSLATORS: command description msgid "Sets one or more BIOS settings" msgstr "設定一個或多個 BIOS é¸é …" #. TRANSLATORS: firmware approved by the admin msgid "Sets the list of approved firmware" msgstr "設定已批准韌體的清單" #. TRANSLATORS: type of BIOS setting msgid "Setting type" msgstr "設定型別" #. TRANSLATORS: description of a BIOS setting msgid "Settings will apply after system reboots" msgstr "è¨­å®šæœƒåœ¨ç³»çµ±é‡æ–°é–‹æ©Ÿå¾Œå¥—用" #. TRANSLATORS: command description msgid "Share firmware history with the developers" msgstr "和開發者分享韌體歷å²" #. TRANSLATORS: command line option msgid "Show all results" msgstr "é¡¯ç¤ºæ‰€æœ‰çµæžœ" #. TRANSLATORS: command line option msgid "Show client and daemon versions" msgstr "顯示客戶端與幕後程å¼ç‰ˆæœ¬" #. TRANSLATORS: for the --verbose arg msgid "Show debugging options" msgstr "顯示除錯é¸é …" #. TRANSLATORS: command line option msgid "Show devices that are not updatable" msgstr "顯示ä¸å¯æ›´æ–°çš„è£ç½®" #. TRANSLATORS: command line option msgid "Show extra debugging information" msgstr "顯示é¡å¤–除錯資訊" #. TRANSLATORS: command description msgid "Show history of firmware updates" msgstr "顯示韌體更新的歷å²" #. TRANSLATORS: shutdown to apply the update msgid "Shutdown now?" msgstr "ç¾åœ¨é—œæ©Ÿï¼Ÿ" msgid "Sign data using the client certificate" msgstr "使用客戶端憑證簽署資料" #. TRANSLATORS: command description msgctxt "command-description" msgid "Sign data using the client certificate" msgstr "使用客戶端憑證簽署資料" #. TRANSLATORS: command line option msgid "Sign the uploaded data with the client certificate" msgstr "使用客戶端憑證簽署上傳的資料" msgid "Signature" msgstr "簽章" #. TRANSLATORS: file size of the download msgid "Size" msgstr "大å°" #. TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM msgid "Some of the platform secrets may be invalidated when updating this firmware." msgstr "有些平臺暗記å¯èƒ½åœ¨æ›´æ–°æ­¤éŸŒé«”æ™‚ç„¡æ³•é€šéŽæ ¸é©—。" #. TRANSLATORS: source (as in code) link msgid "Source" msgstr "來æº" #. TRANSLATORS: The BIOS setting accepts strings msgid "String" msgstr "字串" #. TRANSLATORS: the update state of the specific device msgid "Success" msgstr "æˆåŠŸ" #. TRANSLATORS: success message msgid "Successfully disabled remote" msgstr "å·²æˆåŠŸåœç”¨é ç«¯ç«™é»ž" #. TRANSLATORS: success message msgid "Successfully enabled and refreshed remote" msgstr "å·²æˆåŠŸå•Ÿç”¨ä¸¦é‡æ–°æ•´ç†é ç«¯ç«™é»ž" #. TRANSLATORS: success message msgid "Successfully enabled remote" msgstr "å·²æˆåŠŸå•Ÿç”¨é ç«¯ç«™é»ž" #. TRANSLATORS: success message msgid "Successfully installed firmware" msgstr "å·²æˆåŠŸå®‰è£éŸŒé«”" #. TRANSLATORS: success message -- a per-system setting value msgid "Successfully modified configuration value" msgstr "æˆåŠŸä¿®æ”¹çµ„æ…‹è¨­å®šå€¼" #. TRANSLATORS: success message for a per-remote setting change msgid "Successfully modified remote" msgstr "å·²æˆåŠŸä¿®æ”¹é ç«¯ç«™é»ž" #. TRANSLATORS: one line summary of device msgid "Summary" msgstr "摘è¦" #. TRANSLATORS: Suffix: the HSI result msgid "Supported" msgstr "有支æ´" #. TRANSLATORS: Is found in current metadata msgid "Supported on remote server" msgstr "在é ç«¯ä¼ºæœå™¨ä¸Šæ”¯æ´" #. TRANSLATORS: show and ask user to confirm -- #. * %1 is the old branch name, %2 is the new branch name #, c-format msgid "Switch branch from %s to %s?" msgstr "將分支從 %s 切æ›åˆ° %s?" #. TRANSLATORS: command description msgid "Switch the firmware branch on the device" msgstr "切æ›è£ç½®ä¸Šä½¿ç”¨çš„韌體的分支" #. TRANSLATORS: Must be plugged into an outlet msgid "System requires external power source" msgstr "ç³»çµ±è¦æ±‚接上外部電æº" #. TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be #. empty msgid "TPM Platform Configuration" msgstr "TPM 平臺組態" #. TRANSLATORS: Title: the PCR is rebuilt from the TPM event log msgid "TPM Reconstruction" msgstr "TPM 釿§‹" #. TRANSLATORS: Title: TPM = Trusted Platform Module msgid "TPM v2.0" msgstr "TPM v2.0" #. TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 msgid "Tag" msgid_plural "Tags" msgstr[0] "標記" #. TRANSLATORS: we're saving all USB events for emulation msgid "Tagged for emulation" msgstr "標記為模擬" #. show the user the entire data blob msgid "Target" msgstr "目標" #. TRANSLATORS: when the release was tested msgid "Tested" msgstr "ç¶“éŽæ¸¬è©¦" #. TRANSLATORS: the %s is a vendor name, e.g. Lenovo #, c-format msgid "Tested by %s" msgstr "ç¶“éŽ %s 測試" #. TRANSLATORS: someone we trust has tested this msgid "Tested by trusted vendor" msgstr "ç¶“éŽå—信任廠商測試" #. TRANSLATORS: do not translate the variables marked using $ msgid "The LVFS is a free service that operates as an independent legal entity and has no connection with $OS_RELEASE:NAME$. Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices. All firmware is provided only by the original equipment manufacturer." msgstr "LVFS(Linux 廠商韌體æœå‹™ï¼ŒLinux Vendor Firmware Service)是由ç¨ç«‹æ³•人é‹ä½œçš„å…è²»æœå‹™ï¼Œè€Œèˆ‡ $OS_RELEASE:NAME$ 沒有關è¯ã€‚您的系統散布商å¯èƒ½å°šæœªé©—è­‰éŽä»»ä½•韌體更新與您系統間或連接è£ç½®ä¸Šçš„相容性。所有本æœå‹™ä¸­çš„韌體僅由原始設備製造商æä¾›ã€‚" #. TRANSLATORS: the user is SOL for support... msgid "The daemon has loaded 3rd party code and is no longer supported by the upstream developers!" msgstr "幕後程å¼å·²è¼‰å…¥ç¬¬ä¸‰æ–¹ç¨‹å¼ç¢¼ï¼Œä¸Šæ¸¸é–‹ç™¼è€…ä¸å†èƒ½å¤ æä¾›æ”¯æ´ï¼" #. TRANSLATORS: naughty vendor msgid "The vendor did not supply any release notes." msgstr "廠商沒有æä¾›ä»»ä½•發行註記。" #. TRANSLATORS: now list devices with unfixed high-priority issues msgid "There are devices with issues:" msgstr "有些è£ç½®æœ‰å•題:" #. TRANSLATORS: approved firmware has been checked by #. * the domain administrator msgid "There is no approved firmware." msgstr "沒有已批准的韌體。" #. TRANSLATORS: unsupported build of the package msgid "This package has not been validated, it may not work properly." msgstr "此軟體包尚未核驗,而å¯èƒ½ä¸æœƒæ­£å¸¸é‹ä½œã€‚" #. TRANSLATORS: we're poking around as a power user msgid "This program may only work correctly as root" msgstr "此程å¼åƒ…有 root 身份æ‰èƒ½æ­£å¸¸é‹ä½œ" msgid "This remote contains firmware which is not embargoed, but is still being tested by the hardware vendor. You should ensure you have a way to manually downgrade the firmware if the firmware update fails." msgstr "這個é ç«¯ç«™é»žåŒ…嫿œªåˆ—å…¥ç¦é‹ï¼Œä½†ä»è™•於硬體廠商測試階段的韌體。您應該確ä¿è‡ªå·±åœ¨éŸŒé«”更新失敗時有方法能夠手動é™ç´šéŸŒé«”。" #. TRANSLATORS: error message msgid "This system doesn't support firmware settings" msgstr "æ­¤ç³»çµ±ä¸æ”¯æ´éŸŒé«”設定" #. TRANSLATORS: CLI description msgid "This tool allows an administrator to query and control the fwupd daemon, allowing them to perform actions such as installing or downgrading firmware." msgstr "這個工具讓系統管ç†å“¡èƒ½æŸ¥è©¢æˆ–控制 fwupd 幕後程å¼ï¼Œè®“äººèƒ½å¤ é€²è¡ŒéŸŒé«”å®‰è£æˆ–é™ç´šç­‰ç­‰æ“作。" #. TRANSLATORS: item is TRUE msgid "True" msgstr "True" #. TRANSLATORS: We verified the metadata against the server msgid "Trusted metadata" msgstr "å—信任的中介資料" #. TRANSLATORS: We verified the payload against the server msgid "Trusted payload" msgstr "å—信任的實載階段" #. TRANSLATORS: remote type, e.g. remote or local msgid "Type" msgstr "類型" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition may not be set up correctly" msgstr "UEFI ESP 分割å€å¯èƒ½æ²’有正確設置" #. TRANSLATORS: partition refers to something on disk, again, hey Arch users msgid "UEFI ESP partition not detected or configured" msgstr "æœªåµæ¸¬åˆ° UEFI ESP åˆ†å‰²å€æˆ–是未設定" #. TRANSLATORS: Title: PK is the 'platform key' for the machine msgid "UEFI Platform Key" msgstr "UEFI 平臺金鑰" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI Secure Boot" msgstr "UEFI 安全開機" #. TRANSLATORS: Title: SB is a way of locking down UEFI msgid "UEFI secure boot" msgstr "UEFI 安全開機" #. TRANSLATORS: error message msgid "Unable to find attribute" msgstr "找ä¸åˆ°å±¬æ€§" #. TRANSLATORS: Suffix: the HSI result msgid "Unencrypted" msgstr "未加密" #. TRANSLATORS: current daemon status is unknown #. TRANSLATORS: we don't know the license of the update #. TRANSLATORS: unknown release urgency #. TRANSLATORS: Suffix: the fallback HSI result msgid "Unknown" msgstr "未知" #. TRANSLATORS: Name of hardware msgid "Unknown Device" msgstr "未知è£ç½®" msgid "Unlock the device to allow access" msgstr "解鎖è£ç½®ä»¥å…許存å–" #. TRANSLATORS: Suffix: the HSI result msgid "Unlocked" msgstr "已解鎖" #. TRANSLATORS: command description msgid "Unlocks the device for firmware access" msgstr "解鎖è£ç½®ä»¥ä¾›éŸŒé«”å­˜å–" #. TRANSLATORS: command description msgid "Unmounts the ESP" msgstr "å¸è¼‰ ESP" #. TRANSLATORS: error message #, c-format msgid "Unsupported daemon version %s, client version is %s" msgstr "䏿”¯æ´çš„幕後程å¼ç‰ˆæœ¬ %s,客戶端版本是 %s" #. TRANSLATORS: Device is updatable in this or any other mode msgid "Updatable" msgstr "å¯ä»¥æ›´æ–°" #. TRANSLATORS: error message from last update attempt msgid "Update Error" msgstr "更新錯誤" #. TRANSLATORS: helpful image for the update msgid "Update Image" msgstr "æ›´æ–°å½±åƒ" #. TRANSLATORS: helpful messages for the update msgid "Update Message" msgstr "更新訊æ¯" #. TRANSLATORS: hardware state, e.g. "pending" msgid "Update State" msgstr "更新狀態" #. TRANSLATORS: the server sent the user a small message msgid "Update failure is a known issue, visit this URL for more information:" msgstr "更新失敗是已知議題,請造訪此 URL 瞭解更多資訊:" #. TRANSLATORS: ask if we can update metadata msgid "Update now?" msgstr "是å¦ç«‹åˆ»æ›´æ–°ï¼Ÿ" msgid "Update the stored device verification information" msgstr "更新儲存的è£ç½®æ ¸é©—資訊" msgid "Updating" msgstr "更新中" #. TRANSLATORS: %1 is a device name #, c-format msgid "Updating %s…" msgstr "正在更新 %s…" #. TRANSLATORS: message letting the user know an upgrade is available #. * %1 is the device name and %2 and %3 are version strings #, c-format msgid "Upgrade %s from %s to %s?" msgstr "è¦è®“ %s 從 %s 更新到 %s 嗎?" #. TRANSLATORS: explain why we want to upload msgid "Uploading firmware reports helps hardware vendors to quickly identify failing and successful updates on real devices." msgstr "上傳韌體報告å¯ä»¥å”助硬體廠商快速辨識出更新作業在真實è£ç½®ä¸Šæ˜¯å¤±æ•—é‚„æˆåŠŸã€‚" #. TRANSLATORS: how important the release is msgid "Urgency" msgstr "緊急程度" #. TRANSLATORS: explain how to get help, %1 is #. * 'fwupdtool --help' #. TRANSLATORS: explain how to get help, #. * where $1 is something like 'fwupdmgr --help' #, c-format msgid "Use %s for help" msgstr "使用 %s 來å–得說明" #. TRANSLATORS: User has been notified msgid "User has been notified" msgstr "已通知使用者" #. TRANSLATORS: remote filename base msgid "Username" msgstr "使用者å稱" #. TRANSLATORS: Suffix: the HSI result msgid "Valid" msgstr "有效" #. TRANSLATORS: ESP refers to the EFI System Partition msgid "Validating ESP contents…" msgstr "核驗 ESP 內容中…" #. TRANSLATORS: one line variant of release (e.g. 'China') msgid "Variant" msgstr "變體" #. TRANSLATORS: manufacturer of hardware msgid "Vendor" msgstr "供應商" #. TRANSLATORS: verifying we wrote the firmware correctly msgid "Verifying…" msgstr "核驗中…" #. TRANSLATORS: the detected version number of the dbx msgid "Version" msgstr "版本" #. TRANSLATORS: the fwupd version the release was tested on msgid "Version[fwupd]" msgstr "版本[fwupd]" #. TRANSLATORS: this is a prefix on the console msgid "WARNING" msgstr "警告" #. TRANSLATORS: command description msgid "Wait for a device to appear" msgstr "等待è£ç½®å‡ºç¾" #. TRANSLATORS: waiting for device to do something msgid "Waiting…" msgstr "等候中…" #. TRANSLATORS: command description msgid "Watch for hardware changes" msgstr "ç›£çœ‹ç¡¬é«”æ˜¯å¦æœ‰è®Šæ›´" #. TRANSLATORS: writing to the flash chips msgid "Writing…" msgstr "寫入中…" #. TRANSLATORS: show the user a warning msgid "Your distributor may not have verified any of the firmware updates for compatibility with your system or connected devices." msgstr "您的系統散布商å¯èƒ½å°šæœªé©—è­‰éŽä»»ä½•韌體更新與您系統間或連接è£ç½®ä¸Šçš„相容性。" #. TRANSLATORS: %1 is the device vendor name #, c-format msgid "Your hardware may be damaged using this firmware, and installing this release may void any warranty with %s." msgstr "您的硬體å¯èƒ½åœ¨ä½¿ç”¨æ­¤éŸŒé«”ä¹‹å¾Œææ¯€ï¼Œæ­¤å¤–安è£è©²ç™¼è¡Œç‰ˆå¯èƒ½æœƒè®“ %s çš„ä¿å›ºå¤±æ•ˆã€‚" #. TRANSLATORS: this is the default branch name when unset msgid "default" msgstr "é è¨­" fwupd-2.0.10/policy/000077500000000000000000000000001501337203100142115ustar00rootroot00000000000000fwupd-2.0.10/policy/its/000077500000000000000000000000001501337203100150105ustar00rootroot00000000000000fwupd-2.0.10/policy/its/polkit.its000066400000000000000000000004651501337203100170400ustar00rootroot00000000000000 fwupd-2.0.10/policy/its/polkit.loc000066400000000000000000000003031501337203100170050ustar00rootroot00000000000000 fwupd-2.0.10/policy/meson.build000066400000000000000000000015421501337203100163550ustar00rootroot00000000000000install_data('org.freedesktop.fwupd.rules', install_dir: join_paths(datadir, 'polkit-1', 'rules.d')) #newer polkit has the ITS rules included if polkit.version().version_compare('>0.113') i18n.merge_file( input: 'org.freedesktop.fwupd.policy.in', output: 'org.freedesktop.fwupd.policy', install: true, install_dir: join_paths(datadir, 'polkit-1', 'actions') , type: 'xml', po_dir: join_paths(meson.project_source_root(), 'po') ) #older polkit is missing ITS rules and will fail else i18n.merge_file( input: 'org.freedesktop.fwupd.policy.in', output: 'org.freedesktop.fwupd.policy', install: true, install_dir: join_paths(datadir, 'polkit-1', 'actions') , type: 'xml', data_dirs: join_paths(meson.project_source_root(), 'policy'), po_dir: join_paths(meson.project_source_root(), 'po') ) endif fwupd-2.0.10/policy/org.freedesktop.fwupd-unsafe.rules000066400000000000000000000002731501337203100227730ustar00rootroot00000000000000polkit.addRule(function(action, subject) { if (action.id.startsWith("org.freedesktop.fwupd.") && subject.isInGroup("wheel")) { return polkit.Result.YES; } }); fwupd-2.0.10/policy/org.freedesktop.fwupd.policy.in000066400000000000000000000276201501337203100222730ustar00rootroot00000000000000 System firmware update https://github.com/fwupd/fwupd application-x-firmware Stop the fwupd service Authentication is required to stop the firmware update service auth_admin no auth_admin_keep Install signed system firmware Authentication is required to update the firmware on this machine auth_admin no yes Install unsigned system firmware Authentication is required to update the firmware on this machine auth_admin no auth_admin_keep org.freedesktop.fwupd.update-internal-trusted Install old version of signed system firmware Authentication is required to downgrade the firmware on this machine auth_admin no auth_admin_keep org.freedesktop.fwupd.update-internal Install old version of unsigned system firmware Authentication is required to downgrade the firmware on this machine auth_admin no auth_admin_keep org.freedesktop.fwupd.downgrade-internal-trusted Install signed device firmware Authentication is required to update the firmware on a removable device auth_admin no yes Install unsigned device firmware Authentication is required to update the firmware on a removable device auth_admin no auth_admin_keep org.freedesktop.fwupd.update-hotplug-trusted Install signed device firmware Authentication is required to downgrade the firmware on a removable device auth_admin no auth_admin_keep Install unsigned device firmware Authentication is required to downgrade the firmware on a removable device auth_admin no auth_admin_keep org.freedesktop.fwupd.downgrade-hotplug-trusted Unlock the device to allow access Authentication is required to unlock a device auth_admin no auth_admin_keep Modify daemon configuration Authentication is required to modify daemon configuration auth_admin no auth_admin_keep org.freedesktop.fwupd.modify-remote Reset daemon configuration Authentication is required to reset daemon configuration to defaults auth_admin no auth_admin_keep org.freedesktop.fwupd.modify-config Activate the new firmware on the device Authentication is required to switch to the new firmware version auth_admin no auth_admin_keep Update the stored device verification information Authentication is required to update the stored checksums for the device auth_admin no auth_admin_keep Modify a configured remote Authentication is required to modify a configured remote used for firmware updates auth_admin no auth_admin_keep Sets the list of approved firmware Authentication is required to set the list of approved firmware auth_admin no auth_admin_keep Sign data using the client certificate Authentication is required to sign data using the client certificate auth_admin no auth_admin_keep Get BIOS settings Authentication is required to read BIOS settings auth_admin no auth_admin_keep Set one or more BIOS settings Authentication is required to modify BIOS settings auth_admin no auth_admin org.freedesktop.fwupd.get-bios-settings Security hardening for HSI Authentication is required to fix a host security issue auth_admin no auth_admin Security hardening for HSI Authentication is required to undo the fix for a host security issue auth_admin no auth_admin Load device emulation data Authentication is required to load hardware emulation data auth_admin no auth_admin_keep Save device emulation data Authentication is required to save hardware emulation data auth_admin no auth_admin_keep org.freedesktop.fwupd.emulation-load Enable emulation data collection Authentication is required to enable emulation data collection auth_admin no auth_admin_keep org.freedesktop.fwupd.emulation-load fwupd-2.0.10/policy/org.freedesktop.fwupd.rules000066400000000000000000000003741501337203100215160ustar00rootroot00000000000000polkit.addRule(function(action, subject) { if (action.id == "org.freedesktop.fwupd.update-internal" && subject.active == true && subject.local == true && subject.isInGroup("wheel")) { return polkit.Result.YES; } }); fwupd-2.0.10/src/000077500000000000000000000000001501337203100135015ustar00rootroot00000000000000fwupd-2.0.10/src/fu-bluez-backend.c000066400000000000000000000165161501337203100167740ustar00rootroot00000000000000/* * Copyright 2021 Ricardo Cañuelo * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuBackend" #include "config.h" #include "fu-bluez-backend.h" #include "fu-bluez-device.h" struct _FuBluezBackend { FuBackend parent_instance; GDBusObjectManager *object_manager; }; G_DEFINE_TYPE(FuBluezBackend, fu_bluez_backend, FU_TYPE_BACKEND) #define FU_BLUEZ_BACKEND_TIMEOUT 1500 /* ms */ static void fu_bluez_backend_object_properties_changed(FuBluezBackend *self, GDBusProxy *proxy) { const gchar *path = g_dbus_proxy_get_object_path(proxy); gboolean suitable; FuDevice *device_tmp; g_autoptr(FuBluezDevice) dev = NULL; g_autoptr(GVariant) val_connected = NULL; g_autoptr(GVariant) val_paired = NULL; g_autoptr(GVariant) val_services_resolved = NULL; /* device is suitable */ val_connected = g_dbus_proxy_get_cached_property(proxy, "Connected"); if (val_connected == NULL) return; val_paired = g_dbus_proxy_get_cached_property(proxy, "Paired"); if (val_paired == NULL) return; val_services_resolved = g_dbus_proxy_get_cached_property(proxy, "ServicesResolved"); if (val_services_resolved == NULL) return; suitable = g_variant_get_boolean(val_connected) && g_variant_get_boolean(val_paired) && g_variant_get_boolean(val_services_resolved); /* is this an existing device we've previously added */ device_tmp = fu_backend_lookup_by_id(FU_BACKEND(self), path); if (device_tmp != NULL) { if (suitable) { g_debug("ignoring suitable changed BlueZ device: %s", path); return; } g_debug("removing unsuitable BlueZ device: %s", path); fu_backend_device_removed(FU_BACKEND(self), device_tmp); return; } /* not paired and connected */ if (!suitable) { g_debug("%s connected=%i, paired=%i, services resolved=%i, ignoring", path, g_variant_get_boolean(val_connected), g_variant_get_boolean(val_paired), g_variant_get_boolean(val_services_resolved)); return; } /* create device */ dev = g_object_new(FU_TYPE_BLUEZ_DEVICE, "backend-id", path, "object-manager", self->object_manager, "proxy", proxy, NULL); g_info("adding suitable BlueZ device: %s", path); fu_backend_device_added(FU_BACKEND(self), FU_DEVICE(dev)); } static void fu_bluez_backend_object_properties_changed_cb(GDBusProxy *proxy, GVariant *changed_properties, GStrv invalidated_properties, FuBluezBackend *self) { fu_bluez_backend_object_properties_changed(self, proxy); } static void fu_bluez_backend_object_added(FuBluezBackend *self, GDBusObject *object) { g_autoptr(GDBusInterface) iface = NULL; iface = g_dbus_object_get_interface(object, "org.bluez.Device1"); if (iface == NULL) return; g_signal_connect(G_DBUS_INTERFACE(iface), "g-properties-changed", G_CALLBACK(fu_bluez_backend_object_properties_changed_cb), self); fu_bluez_backend_object_properties_changed(self, G_DBUS_PROXY(iface)); } static void fu_bluez_backend_object_added_cb(GDBusObjectManager *manager, GDBusObject *object, FuBluezBackend *self) { fu_bluez_backend_object_added(self, object); } static void fu_bluez_backend_object_removed_cb(GDBusObjectManager *manager, GDBusObject *object, FuBluezBackend *self) { const gchar *path = g_dbus_object_get_object_path(object); FuDevice *device_tmp; device_tmp = fu_backend_lookup_by_id(FU_BACKEND(self), path); if (device_tmp == NULL) return; g_info("removing BlueZ device: %s", path); fu_backend_device_removed(FU_BACKEND(self), device_tmp); } typedef struct { GDBusObjectManager *object_manager; GMainLoop *loop; GError **error; GCancellable *cancellable; guint timeout_id; } FuBluezBackendHelper; static void fu_bluez_backend_helper_free(FuBluezBackendHelper *helper) { if (helper->object_manager != NULL) g_object_unref(helper->object_manager); if (helper->timeout_id != 0) g_source_remove(helper->timeout_id); g_cancellable_cancel(helper->cancellable); g_main_loop_unref(helper->loop); g_free(helper); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuBluezBackendHelper, fu_bluez_backend_helper_free) static void fu_bluez_backend_connect_cb(GObject *source_object, GAsyncResult *res, gpointer user_data) { FuBluezBackendHelper *helper = (FuBluezBackendHelper *)user_data; helper->object_manager = g_dbus_object_manager_client_new_for_bus_finish(res, helper->error); g_main_loop_quit(helper->loop); } static gboolean fu_bluez_backend_timeout_cb(gpointer user_data) { FuBluezBackendHelper *helper = (FuBluezBackendHelper *)user_data; g_cancellable_cancel(helper->cancellable); helper->timeout_id = 0; return G_SOURCE_REMOVE; } static gboolean fu_bluez_backend_setup(FuBackend *backend, FuBackendSetupFlags flags, FuProgress *progress, GError **error) { FuBluezBackend *self = FU_BLUEZ_BACKEND(backend); g_autoptr(FuBluezBackendHelper) helper = g_new0(FuBluezBackendHelper, 1); /* in some circumstances the bluez daemon will just hang... do not wait * forever and make fwupd startup also fail */ helper->error = error; helper->loop = g_main_loop_new(NULL, FALSE); helper->cancellable = g_cancellable_new(); helper->timeout_id = g_timeout_add(FU_BLUEZ_BACKEND_TIMEOUT, fu_bluez_backend_timeout_cb, helper); g_dbus_object_manager_client_new_for_bus(G_BUS_TYPE_SYSTEM, G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE, "org.bluez", "/", NULL, NULL, NULL, helper->cancellable, fu_bluez_backend_connect_cb, helper); g_main_loop_run(helper->loop); if (helper->object_manager == NULL) return FALSE; self->object_manager = g_steal_pointer(&helper->object_manager); if (flags & FU_BACKEND_SETUP_FLAG_USE_HOTPLUG) { g_signal_connect(G_DBUS_OBJECT_MANAGER(self->object_manager), "object-added", G_CALLBACK(fu_bluez_backend_object_added_cb), self); g_signal_connect(G_DBUS_OBJECT_MANAGER(self->object_manager), "object-removed", G_CALLBACK(fu_bluez_backend_object_removed_cb), self); } return TRUE; } static gboolean fu_bluez_backend_coldplug(FuBackend *backend, FuProgress *progress, GError **error) { FuBluezBackend *self = FU_BLUEZ_BACKEND(backend); g_autolist(GDBusObject) objects = NULL; /* failed to set up */ if (self->object_manager == NULL) return TRUE; objects = g_dbus_object_manager_get_objects(self->object_manager); for (GList *l = objects; l != NULL; l = l->next) { GDBusObject *object = G_DBUS_OBJECT(l->data); fu_bluez_backend_object_added(self, object); } return TRUE; } static void fu_bluez_backend_finalize(GObject *object) { FuBluezBackend *self = FU_BLUEZ_BACKEND(object); if (self->object_manager != NULL) g_object_unref(self->object_manager); G_OBJECT_CLASS(fu_bluez_backend_parent_class)->finalize(object); } static void fu_bluez_backend_init(FuBluezBackend *self) { } static void fu_bluez_backend_class_init(FuBluezBackendClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuBackendClass *backend_class = FU_BACKEND_CLASS(klass); object_class->finalize = fu_bluez_backend_finalize; backend_class->setup = fu_bluez_backend_setup; backend_class->coldplug = fu_bluez_backend_coldplug; } FuBackend * fu_bluez_backend_new(FuContext *ctx) { return FU_BACKEND(g_object_new(FU_TYPE_BLUEZ_BACKEND, "name", "bluez", "context", ctx, "device-gtype", FU_TYPE_BLUEZ_DEVICE, NULL)); } fwupd-2.0.10/src/fu-bluez-backend.h000066400000000000000000000005111501337203100167650ustar00rootroot00000000000000/* * Copyright 2021 * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-backend.h" #define FU_TYPE_BLUEZ_BACKEND (fu_bluez_backend_get_type()) G_DECLARE_FINAL_TYPE(FuBluezBackend, fu_bluez_backend, FU, BLUEZ_BACKEND, FuBackend) FuBackend * fu_bluez_backend_new(FuContext *ctx) G_GNUC_NON_NULL(1); fwupd-2.0.10/src/fu-cabinet.c000066400000000000000000000753331501337203100156750ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuCabinet" #include "config.h" #include #include "fu-cabinet.h" /** * FuCabinet: * * Cabinet archive parser and writer. * * See also: [class@FuArchive] */ struct _FuCabinet { FuCabFirmware parent_instance; gchar *container_checksum; gchar *container_checksum_alt; XbBuilder *builder; XbSilo *silo; JcatContext *jcat_context; JcatFile *jcat_file; }; G_DEFINE_TYPE(FuCabinet, fu_cabinet, FU_TYPE_CAB_FIRMWARE) /** * fu_cabinet_set_jcat_context: (skip): * @self: a #FuCabinet * @jcat_context: (nullable): a Jcat context * * Sets the Jcat context, which is used for setting the trust flags on the * each release in the archive. * * Since: 1.4.0 **/ void fu_cabinet_set_jcat_context(FuCabinet *self, JcatContext *jcat_context) { g_return_if_fail(FU_IS_CABINET(self)); g_return_if_fail(JCAT_IS_CONTEXT(jcat_context)); g_set_object(&self->jcat_context, jcat_context); } /** * fu_cabinet_get_silo: (skip): * @self: a #FuCabinet * @error: (nullable): optional return location for an error * * Gets the silo that represents the superset metadata of all the metainfo files * found in the archive. * * Returns: (transfer full): a #XbSilo, or %NULL if the archive has not been parsed * * Since: 1.4.0 **/ XbSilo * fu_cabinet_get_silo(FuCabinet *self, GError **error) { g_return_val_if_fail(FU_IS_CABINET(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); if (self->silo == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "no silo"); return NULL; } return g_object_ref(self->silo); } /** * fu_cabinet_add_file: * @self: a #FuCabinet * @basename: filename * @data: file data * * Adds a file to the silo. * * Since: 1.6.0 **/ void fu_cabinet_add_file(FuCabinet *self, const gchar *basename, GBytes *data) { g_autoptr(FuCabImage) img = fu_cab_image_new(); g_return_if_fail(FU_IS_CABINET(self)); g_return_if_fail(basename != NULL); g_return_if_fail(data != NULL); fu_firmware_set_bytes(FU_FIRMWARE(img), data); fu_firmware_set_id(FU_FIRMWARE(img), basename); fu_firmware_add_image(FU_FIRMWARE(self), FU_FIRMWARE(img)); } /* sets the firmware and signature blobs on XbNode */ static gboolean fu_cabinet_parse_release(FuCabinet *self, XbNode *release, GError **error) { const gchar *csum_filename = NULL; gsize streamsz = 0; g_autofree gchar *basename = NULL; g_autoptr(FuFirmware) img_blob = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(GError) error_local2 = NULL; g_autoptr(XbNode) artifact = NULL; g_autoptr(XbNode) csum_tmp = NULL; g_autoptr(XbNode) metadata_trust = NULL; g_autoptr(XbNode) nsize = NULL; g_autoptr(JcatItem) item = NULL; g_autoptr(GBytes) release_flags_blob = NULL; g_autoptr(GBytes) filename_blob = NULL; FwupdReleaseFlags release_flags = FWUPD_RELEASE_FLAG_NONE; /* we set this with XbBuilderSource before the silo was created */ metadata_trust = xb_node_query_first(release, "../../info/metadata_trust", NULL); if (metadata_trust != NULL) release_flags |= FWUPD_RELEASE_FLAG_TRUSTED_METADATA; /* look for source artifact first */ artifact = xb_node_query_first(release, "artifacts/artifact[@type='source']", NULL); if (artifact != NULL) { csum_filename = xb_node_query_text(artifact, "filename", NULL); csum_tmp = xb_node_query_first(artifact, "checksum[@type='sha256']", NULL); if (csum_tmp == NULL) csum_tmp = xb_node_query_first(artifact, "checksum", NULL); } else { csum_tmp = xb_node_query_first(release, "checksum[@target='content']", NULL); if (csum_tmp != NULL) csum_filename = xb_node_get_attr(csum_tmp, "filename"); } /* if this isn't true, a firmware needs to set in the metainfo.xml file * something like: */ if (csum_filename == NULL) csum_filename = "firmware.bin"; /* get the main firmware file */ basename = g_path_get_basename(csum_filename); img_blob = fu_firmware_get_image_by_id(FU_FIRMWARE(self), basename, &error_local2); if (img_blob == NULL) { /* we have to set this exact error code */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, error_local2->message); return FALSE; } filename_blob = g_bytes_new(basename, strlen(basename) + 1); xb_node_set_data(release, "fwupd::FirmwareBasename", filename_blob); /* set as metadata if unset, but error if specified and incorrect */ stream = fu_firmware_get_stream(img_blob, error); if (stream == NULL) return FALSE; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; nsize = xb_node_query_first(release, "size[@type='installed']", NULL); if (nsize != NULL) { guint64 size = 0; if (!fu_strtoull(xb_node_get_text(nsize), &size, 0, G_MAXSIZE, FU_INTEGER_BASE_AUTO, error)) return FALSE; if (size != streamsz) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "contents size invalid, expected " "%" G_GSIZE_FORMAT ", got %" G_GUINT64_FORMAT, streamsz, size); return FALSE; } } else { guint64 size = streamsz; g_autoptr(GBytes) blob_sz = g_bytes_new(&size, sizeof(guint64)); xb_node_set_data(release, "fwupd::ReleaseSize", blob_sz); } /* set if unspecified, but error out if specified and incorrect */ if (csum_tmp != NULL && xb_node_get_text(csum_tmp) != NULL) { const gchar *checksum_old = xb_node_get_text(csum_tmp); GChecksumType checksum_type = fwupd_checksum_guess_kind(checksum_old); g_autofree gchar *checksum = NULL; checksum = fu_input_stream_compute_checksum(stream, checksum_type, error); if (checksum == NULL) return FALSE; if (g_strcmp0(checksum, checksum_old) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "contents checksum invalid, expected %s, got %s", checksum, xb_node_get_text(csum_tmp)); return FALSE; } } /* the jcat file signed the *checksum of the payload*, not the payload itself */ item = jcat_file_get_item_by_id(self->jcat_file, basename, NULL); if (item != NULL && jcat_item_has_target(item)) { g_autofree gchar *checksum_sha256 = NULL; g_autofree gchar *checksum_sha512 = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(GPtrArray) results = NULL; g_autoptr(JcatBlob) blob_target_sha256 = NULL; g_autoptr(JcatBlob) blob_target_sha512 = NULL; g_autoptr(JcatItem) item_target = jcat_item_new(basename); /* add SHA-256 */ checksum_sha256 = fu_input_stream_compute_checksum(stream, G_CHECKSUM_SHA256, error); if (checksum_sha256 == NULL) return FALSE; blob_target_sha256 = jcat_blob_new_utf8(JCAT_BLOB_KIND_SHA256, checksum_sha256); jcat_item_add_blob(item_target, blob_target_sha256); /* add SHA-512 */ checksum_sha512 = fu_input_stream_compute_checksum(stream, G_CHECKSUM_SHA512, error); if (checksum_sha512 == NULL) return FALSE; blob_target_sha512 = jcat_blob_new_utf8(JCAT_BLOB_KIND_SHA512, checksum_sha512); jcat_item_add_blob(item_target, blob_target_sha512); results = jcat_context_verify_target(self->jcat_context, item_target, item, JCAT_VERIFY_FLAG_REQUIRE_CHECKSUM | JCAT_VERIFY_FLAG_REQUIRE_SIGNATURE, &error_local); if (results == NULL) { g_info("failed to verify indirect payload %s: %s", basename, error_local->message); } else { g_info("verified indirect payload %s: %u", basename, results->len); release_flags |= FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD; } } else if (item != NULL) { g_autoptr(GBytes) blob = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(GPtrArray) results = NULL; /* verify the binary item */ blob = fu_firmware_get_bytes(img_blob, error); if (blob == NULL) return FALSE; results = jcat_context_verify_item(self->jcat_context, blob, item, JCAT_VERIFY_FLAG_REQUIRE_CHECKSUM | JCAT_VERIFY_FLAG_REQUIRE_SIGNATURE, &error_local); if (results == NULL) { g_info("failed to verify payload %s: %s", basename, error_local->message); } else { g_info("verified payload %s: %u", basename, results->len); release_flags |= FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD; } } else { g_autofree gchar *basename_sig = NULL; g_autoptr(FuFirmware) img_sig = NULL; /* legacy GPG detached signature */ basename_sig = g_strdup_printf("%s.asc", basename); img_sig = fu_firmware_get_image_by_id(FU_FIRMWARE(FU_CAB_FIRMWARE(self)), basename_sig, NULL); if (img_sig != NULL) { g_autoptr(JcatResult) jcat_result = NULL; g_autoptr(JcatBlob) jcat_blob = NULL; g_autoptr(GBytes) blob = NULL; g_autoptr(GBytes) data_sig = NULL; g_autoptr(GError) error_local = NULL; blob = fu_firmware_get_bytes(img_blob, error); if (blob == NULL) return FALSE; data_sig = fu_firmware_get_bytes(img_sig, error); if (data_sig == NULL) return FALSE; jcat_blob = jcat_blob_new(JCAT_BLOB_KIND_GPG, data_sig); jcat_result = jcat_context_verify_blob(self->jcat_context, blob, jcat_blob, JCAT_VERIFY_FLAG_REQUIRE_SIGNATURE, &error_local); if (jcat_result == NULL) { g_info("failed to verify payload %s using detached: %s", basename, error_local->message); } else { g_info("verified payload %s using detached", basename); release_flags |= FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD; } } } /* this means we can get the data from fu_keyring_get_release_flags */ release_flags_blob = g_bytes_new(&release_flags, sizeof(release_flags)); xb_node_set_data(release, "fwupd::ReleaseFlags", release_flags_blob); /* success */ return TRUE; } static gint fu_cabinet_sort_cb(XbBuilderNode *bn1, XbBuilderNode *bn2, gpointer user_data) { guint64 prio1 = xb_builder_node_get_attr_as_uint(bn1, "priority"); guint64 prio2 = xb_builder_node_get_attr_as_uint(bn2, "priority"); if (prio1 > prio2) return -1; if (prio1 < prio2) return 1; return 0; } static gboolean fu_cabinet_sort_priority_cb(XbBuilderFixup *self, XbBuilderNode *bn, gpointer user_data, GError **error) { xb_builder_node_sort_children(bn, fu_cabinet_sort_cb, user_data); return TRUE; } static XbBuilderNode * _xb_builder_node_get_child_by_element_attr(XbBuilderNode *bn, const gchar *element, const gchar *attr_name, const gchar *attr_value, const gchar *attr2_name, const gchar *attr2_value) { GPtrArray *bcs = xb_builder_node_get_children(bn); for (guint i = 0; i < bcs->len; i++) { XbBuilderNode *bc = g_ptr_array_index(bcs, i); if (g_strcmp0(xb_builder_node_get_element(bc), element) != 0) continue; if (g_strcmp0(xb_builder_node_get_attr(bc, attr_name), attr_value) != 0) continue; if (g_strcmp0(xb_builder_node_get_attr(bc, attr2_name), attr2_value) == 0) return g_object_ref(bc); } return NULL; } static void fu_cabinet_ensure_container_checksum(XbBuilderNode *bn, const gchar *type, const gchar *checksum) { g_autoptr(XbBuilderNode) csum = NULL; /* verify it exists */ csum = _xb_builder_node_get_child_by_element_attr(bn, "checksum", "type", type, "target", "container"); if (csum == NULL) { csum = xb_builder_node_insert(bn, "checksum", "type", type, "target", "container", NULL); } /* verify it is correct */ if (g_strcmp0(xb_builder_node_get_text(csum), checksum) != 0) { if (xb_builder_node_get_text(csum) != NULL) { g_warning("invalid container checksum %s, fixing up to %s", xb_builder_node_get_text(csum), checksum); } xb_builder_node_set_text(csum, checksum, -1); } } static gboolean fu_cabinet_ensure_container_checksum_cb(XbBuilderFixup *builder_fixup, XbBuilderNode *bn, gpointer user_data, GError **error) { FuCabinet *self = FU_CABINET(user_data); /* not us */ if (g_strcmp0(xb_builder_node_get_element(bn), "release") != 0) return TRUE; fu_cabinet_ensure_container_checksum(bn, "sha1", self->container_checksum); fu_cabinet_ensure_container_checksum(bn, "sha256", self->container_checksum_alt); return TRUE; } static void fu_cabinet_fixup_checksum_children(XbBuilderNode *bn, const gchar *element, const gchar *attr_name, const gchar *attr_value) { GPtrArray *bcs = xb_builder_node_get_children(bn); for (guint i = 0; i < bcs->len; i++) { XbBuilderNode *bc = g_ptr_array_index(bcs, i); if (g_strcmp0(xb_builder_node_get_element(bc), element) != 0) continue; if (attr_value == NULL || g_strcmp0(xb_builder_node_get_attr(bc, attr_name), attr_value) == 0) { const gchar *tmp = xb_builder_node_get_text(bc); if (tmp != NULL) { g_autofree gchar *lowercase = g_ascii_strdown(tmp, -1); xb_builder_node_set_text(bc, lowercase, -1); } } } } static gboolean fu_cabinet_set_lowercase_checksum_cb(XbBuilderFixup *builder_fixup, XbBuilderNode *bn, gpointer user_data, GError **error) { if (g_strcmp0(xb_builder_node_get_element(bn), "artifact") == 0) /* don't care whether it's sha256, sha1 or something else so don't check for * specific value */ fu_cabinet_fixup_checksum_children(bn, "checksum", "type", NULL); else if (g_strcmp0(xb_builder_node_get_element(bn), "release") == 0) fu_cabinet_fixup_checksum_children(bn, "checksum", "target", "content"); return TRUE; } static gboolean fu_cabinet_fixup_strip_inner_text_cb(XbBuilderFixup *self, XbBuilderNode *bn, gpointer user_data, GError **error) { if (xb_builder_node_get_first_child(bn) == NULL) xb_builder_node_add_flag(bn, XB_BUILDER_NODE_FLAG_STRIP_TEXT); return TRUE; } /* adds each image to the silo */ static gboolean fu_cabinet_build_silo_file(FuCabinet *self, FuFirmware *img, FwupdReleaseFlags release_flags, GError **error) { g_autoptr(GBytes) blob = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_autoptr(XbBuilderNode) bn_info = xb_builder_node_new("info"); /* indicate the metainfo file was signed */ if (release_flags & FWUPD_RELEASE_FLAG_TRUSTED_METADATA) xb_builder_node_insert_text(bn_info, "metadata_trust", NULL, NULL); xb_builder_node_insert_text(bn_info, "filename", fu_firmware_get_id(img), NULL); xb_builder_source_set_info(source, bn_info); /* rewrite to be under a components root */ xb_builder_source_set_prefix(source, "components"); /* parse file */ blob = fu_firmware_get_bytes(img, error); if (blob == NULL) return FALSE; if (!xb_builder_source_load_bytes(source, blob, XB_BUILDER_SOURCE_FLAG_NONE, &error_local)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "could not parse MetaInfo XML: %s", error_local->message); return FALSE; } xb_builder_import_source(self->builder, source); /* success */ return TRUE; } static gboolean fu_cabinet_build_silo_metainfo(FuCabinet *self, FuFirmware *img, GError **error) { FwupdReleaseFlags release_flags = FWUPD_RELEASE_FLAG_NONE; const gchar *fn = fu_firmware_get_id(img); g_autoptr(JcatItem) item = NULL; /* validate against the Jcat file */ item = jcat_file_get_item_by_id(self->jcat_file, fn, NULL); if (item == NULL) { g_info("failed to verify %s: no JcatItem", fn); } else if (self->jcat_context != NULL) { g_autoptr(GError) error_local = NULL; g_autoptr(GPtrArray) results = NULL; g_autoptr(GBytes) blob = NULL; blob = fu_firmware_get_bytes(img, error); if (blob == NULL) return FALSE; results = jcat_context_verify_item(self->jcat_context, blob, item, JCAT_VERIFY_FLAG_REQUIRE_CHECKSUM | JCAT_VERIFY_FLAG_REQUIRE_SIGNATURE, &error_local); if (results == NULL) { g_info("failed to verify %s: %s", fn, error_local->message); } else { g_info("verified metadata %s: %u", fn, results->len); release_flags |= FWUPD_RELEASE_FLAG_TRUSTED_METADATA; } } /* actually parse the XML now */ g_info("processing file: %s", fn); if (!fu_cabinet_build_silo_file(self, img, release_flags, error)) { g_prefix_error(error, "%s could not be loaded: ", fn); return FALSE; } /* success */ return TRUE; } /* load the firmware.jcat files if included */ static gboolean fu_cabinet_build_jcat_folder(FuCabinet *self, FuFirmware *img, GError **error) { const gchar *fn = fu_firmware_get_id(img); if (fn == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no extraction name set"); return FALSE; } if (g_str_has_suffix(fn, ".jcat")) { g_autoptr(GInputStream) istream = NULL; istream = fu_firmware_get_stream(img, error); if (istream == NULL) return FALSE; /* TODO: move this to libjcat? */ if (!g_seekable_seek(G_SEEKABLE(istream), 0x0, G_SEEK_SET, NULL, error)) return FALSE; if (!jcat_file_import_stream(self->jcat_file, istream, JCAT_IMPORT_FLAG_NONE, NULL, error)) { g_prefix_error(error, "failed to import JCat stream: "); return FALSE; } } return TRUE; } /* adds each image to the silo */ static gboolean fu_cabinet_build_silo_folder(FuCabinet *self, FuFirmware *img, GError **error) { const gchar *fn = fu_firmware_get_id(img); if (fn == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no extraction name set"); return FALSE; } if (g_str_has_suffix(fn, ".metainfo.xml")) { if (!fu_cabinet_build_silo_metainfo(self, img, error)) return FALSE; } return TRUE; } static gboolean fu_cabinet_build_silo(FuCabinet *self, GError **error) { g_autoptr(GPtrArray) imgs = NULL; g_autoptr(XbBuilderFixup) fixup1 = NULL; g_autoptr(XbBuilderFixup) fixup2 = NULL; g_autoptr(XbBuilderFixup) fixup3 = NULL; g_autoptr(XbBuilderFixup) fixup4 = NULL; /* verbose profiling */ if (g_getenv("FWUPD_XMLB_VERBOSE") != NULL) { xb_builder_set_profile_flags(self->builder, XB_SILO_PROFILE_FLAG_XPATH | XB_SILO_PROFILE_FLAG_DEBUG); } /* load Jcat */ imgs = fu_firmware_get_images(FU_FIRMWARE(FU_CAB_FIRMWARE(self))); if (self->jcat_context != NULL) { for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); if (!fu_cabinet_build_jcat_folder(self, img, error)) return FALSE; } } /* adds each metainfo file to the silo */ for (guint i = 0; i < imgs->len; i++) { FuFirmware *img = g_ptr_array_index(imgs, i); if (!fu_cabinet_build_silo_folder(self, img, error)) return FALSE; } /* sort the components by priority */ fixup1 = xb_builder_fixup_new("OrderByPriority", fu_cabinet_sort_priority_cb, NULL, NULL); xb_builder_fixup_set_max_depth(fixup1, 0); xb_builder_add_fixup(self->builder, fixup1); /* ensure the container checksum is always set */ fixup2 = xb_builder_fixup_new("EnsureContainerChecksum", fu_cabinet_ensure_container_checksum_cb, self, NULL); xb_builder_add_fixup(self->builder, fixup2); fixup3 = xb_builder_fixup_new("LowerCaseCheckSum", fu_cabinet_set_lowercase_checksum_cb, self, NULL); xb_builder_add_fixup(self->builder, fixup3); /* strip inner nodes without children */ fixup4 = xb_builder_fixup_new("TextStripInner", fu_cabinet_fixup_strip_inner_text_cb, self, NULL); xb_builder_add_fixup(self->builder, fixup4); /* did we get any valid files */ self->silo = xb_builder_compile(self->builder, XB_BUILDER_COMPILE_FLAG_SINGLE_ROOT, NULL, error); if (self->silo == NULL) { fwupd_error_convert(error); return FALSE; } /* build the index */ if (!xb_silo_query_build_index(self->silo, "components/component[@type='firmware']/provides/firmware", "type", error)) { fwupd_error_convert(error); return FALSE; } if (!xb_silo_query_build_index(self->silo, "components/component[@type='firmware']/provides/firmware", NULL, error)) { fwupd_error_convert(error); return FALSE; } /* success */ return TRUE; } static gboolean fu_cabinet_sign_filename(FuCabinet *self, const gchar *filename, JcatContext *jcat_context, JcatFile *jcat_file, GBytes *cert, GBytes *privkey, GError **error) { g_autoptr(FuFirmware) img = NULL; g_autoptr(GBytes) source_blob = NULL; g_autoptr(JcatBlob) jcat_blob_csum = NULL; g_autoptr(JcatBlob) jcat_blob_sig = NULL; g_autoptr(JcatEngine) jcat_engine_csum = NULL; g_autoptr(JcatEngine) jcat_engine_sig = NULL; g_autoptr(JcatItem) jcat_item = NULL; /* sign the file using the engine */ img = fu_firmware_get_image_by_id(FU_FIRMWARE(self), filename, error); if (img == NULL) return FALSE; source_blob = fu_firmware_get_bytes(img, error); if (source_blob == NULL) return FALSE; jcat_item = jcat_file_get_item_by_id(jcat_file, filename, NULL); if (jcat_item == NULL) { jcat_item = jcat_item_new(filename); jcat_file_add_item(jcat_file, jcat_item); } /* add SHA256 checksum */ jcat_engine_csum = jcat_context_get_engine(jcat_context, JCAT_BLOB_KIND_SHA256, error); if (jcat_engine_csum == NULL) return FALSE; jcat_blob_csum = jcat_engine_self_sign(jcat_engine_csum, source_blob, JCAT_SIGN_FLAG_NONE, error); if (jcat_blob_csum == NULL) return FALSE; jcat_item_add_blob(jcat_item, jcat_blob_csum); /* sign using PKCS#7 */ jcat_engine_sig = jcat_context_get_engine(jcat_context, JCAT_BLOB_KIND_PKCS7, error); if (jcat_engine_sig == NULL) return FALSE; jcat_blob_sig = jcat_engine_pubkey_sign(jcat_engine_sig, source_blob, cert, privkey, JCAT_SIGN_FLAG_ADD_TIMESTAMP | JCAT_SIGN_FLAG_ADD_CERT, error); if (jcat_blob_sig == NULL) return FALSE; jcat_item_add_blob(jcat_item, jcat_blob_sig); return TRUE; } static gboolean fu_cabinet_sign_enumerate_metainfo(FuCabinet *self, GPtrArray *files, GError **error) { g_autoptr(GError) error_local = NULL; g_autoptr(GPtrArray) nodes = NULL; g_autoptr(XbSilo) silo = NULL; /* get all the firmware referenced by the metainfo files */ silo = fu_cabinet_get_silo(self, error); if (silo == NULL) return FALSE; nodes = xb_silo_query(silo, "components/component[@type='firmware']/info/filename", 0, &error_local); if (nodes == NULL) { if (g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT) || g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { g_debug("ignoring: %s", error_local->message); g_ptr_array_add(files, g_strdup("firmware.metainfo.xml")); } else { g_propagate_error(error, g_steal_pointer(&error_local)); fwupd_error_convert(error); return FALSE; } } else { for (guint i = 0; i < nodes->len; i++) { XbNode *n = g_ptr_array_index(nodes, i); g_debug("adding: %s", xb_node_get_text(n)); g_ptr_array_add(files, g_strdup(xb_node_get_text(n))); } } /* success */ return TRUE; } static gboolean fu_cabinet_sign_enumerate_firmware(FuCabinet *self, GPtrArray *files, GError **error) { g_autoptr(GError) error_local = NULL; g_autoptr(GPtrArray) nodes = NULL; g_autoptr(XbSilo) silo = NULL; silo = fu_cabinet_get_silo(self, error); if (silo == NULL) return FALSE; nodes = xb_silo_query(silo, "components/component[@type='firmware']/releases/" "release/checksum[@target='content']", 0, &error_local); if (nodes == NULL) { if (g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT) || g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { g_debug("ignoring: %s", error_local->message); g_ptr_array_add(files, g_strdup("firmware.bin")); } else { g_propagate_error(error, g_steal_pointer(&error_local)); fwupd_error_convert(error); return FALSE; } } else { for (guint i = 0; i < nodes->len; i++) { XbNode *n = g_ptr_array_index(nodes, i); g_debug("adding: %s", xb_node_get_attr(n, "filename")); g_ptr_array_add(files, g_strdup(xb_node_get_attr(n, "filename"))); } } /* success */ return TRUE; } /** * fu_cabinet_sign: * @self: a #FuCabinet * @cert: a PCKS#7 certificate * @privkey: a private key * @flags: signing flags, e.g. %FU_CABINET_SIGN_FLAG_NONE * @error: (nullable): optional return location for an error * * Sign the cabinet archive using JCat. * * Returns: %TRUE for success * * Since: 1.6.0 **/ gboolean fu_cabinet_sign(FuCabinet *self, GBytes *cert, GBytes *privkey, FuCabinetSignFlags flags, GError **error) { g_autoptr(FuFirmware) img = NULL; g_autoptr(GBytes) new_bytes = NULL; g_autoptr(GOutputStream) ostr = NULL; g_autoptr(GPtrArray) filenames = g_ptr_array_new_with_free_func(g_free); g_autoptr(JcatContext) jcat_context = jcat_context_new(); g_autoptr(JcatFile) jcat_file = jcat_file_new(); g_return_val_if_fail(FU_IS_CABINET(self), FALSE); g_return_val_if_fail(cert != NULL, FALSE); g_return_val_if_fail(privkey != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* load existing .jcat file if it exists */ img = fu_firmware_get_image_by_id(FU_FIRMWARE(self), "firmware.jcat", NULL); if (img != NULL) { g_autoptr(GInputStream) stream = fu_firmware_get_stream(img, error); if (stream == NULL) return FALSE; if (!jcat_file_import_stream(jcat_file, stream, JCAT_IMPORT_FLAG_NONE, NULL, error)) return FALSE; } /* get all the metainfo.xml and firmware.bin files */ if (!fu_cabinet_sign_enumerate_metainfo(self, filenames, error)) return FALSE; if (!fu_cabinet_sign_enumerate_firmware(self, filenames, error)) return FALSE; /* sign all the files */ for (guint i = 0; i < filenames->len; i++) { const gchar *filename = g_ptr_array_index(filenames, i); if (!fu_cabinet_sign_filename(self, filename, jcat_context, jcat_file, cert, privkey, error)) return FALSE; } /* export new JCat file and add it to the archive */ ostr = g_memory_output_stream_new_resizable(); if (!jcat_file_export_stream(jcat_file, ostr, JCAT_EXPORT_FLAG_NONE, NULL, error)) return FALSE; new_bytes = g_memory_output_stream_steal_as_bytes(G_MEMORY_OUTPUT_STREAM(ostr)); fu_cabinet_add_file(self, "firmware.jcat", new_bytes); return TRUE; } static gboolean fu_cabinet_parse(FuFirmware *firmware, GInputStream *stream, FuFirmwareParseFlags flags, GError **error) { FuCabinet *self = FU_CABINET(firmware); g_autoptr(GError) error_local = NULL; g_autoptr(GPtrArray) components = NULL; g_autoptr(XbQuery) query = NULL; g_return_val_if_fail(FU_IS_CABINET(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); g_return_val_if_fail(self->silo == NULL, FALSE); /* decompress and calculate container hashes */ if (stream != NULL) { if (!FU_FIRMWARE_CLASS(fu_cabinet_parent_class) ->parse(firmware, stream, flags | FU_FIRMWARE_PARSE_FLAG_CACHE_STREAM, error)) return FALSE; self->container_checksum = fu_firmware_get_checksum(firmware, G_CHECKSUM_SHA1, error); if (self->container_checksum == NULL) return FALSE; self->container_checksum_alt = fu_firmware_get_checksum(firmware, G_CHECKSUM_SHA256, error); if (self->container_checksum_alt == NULL) return FALSE; } /* build xmlb silo */ if (!fu_cabinet_build_silo(self, error)) return FALSE; /* sanity check */ components = xb_silo_query(self->silo, "components/component", 0, &error_local); if (components == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "archive contained no valid metadata: %s", error_local->message); return FALSE; } /* prepare query */ query = xb_query_new_full(self->silo, "releases/release", XB_QUERY_FLAG_FORCE_NODE_CACHE, error); if (query == NULL) return FALSE; /* process each listed release */ for (guint i = 0; i < components->len; i++) { XbNode *component = g_ptr_array_index(components, i); g_autoptr(GPtrArray) releases = NULL; if (g_strcmp0(xb_node_get_attr(component, "type"), "generic") == 0) continue; releases = xb_node_query_full(component, query, &error_local); if (releases == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no releases in metainfo file: %s", error_local->message); return FALSE; } for (guint j = 0; j < releases->len; j++) { XbNode *rel = g_ptr_array_index(releases, j); g_info("processing release: %s", xb_node_get_attr(rel, "version")); if (!fu_cabinet_parse_release(self, rel, error)) return FALSE; } } /* success */ return TRUE; } GPtrArray * fu_cabinet_get_components(FuCabinet *self, GError **error) { g_autoptr(GError) error_local = NULL; g_autoptr(GPtrArray) components = NULL; g_return_val_if_fail(FU_IS_CABINET(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); components = xb_silo_query(self->silo, "components/component[@type='firmware']", 0, &error_local); if (components == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no components: %s", error_local->message); return NULL; } return g_steal_pointer(&components); } XbNode * fu_cabinet_get_component(FuCabinet *self, const gchar *id, GError **error) { g_autofree gchar *xpath = NULL; g_autoptr(XbNode) xn = NULL; g_return_val_if_fail(FU_IS_CABINET(self), NULL); g_return_val_if_fail(id != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); xpath = g_strdup_printf("components/component/id[text()='%s']/..", id); xn = xb_silo_query_first(self->silo, xpath, error); if (xn == NULL) { fwupd_error_convert(error); return NULL; } return g_steal_pointer(&xn); } static void fu_cabinet_init(FuCabinet *self) { fu_cab_firmware_set_only_basename(FU_CAB_FIRMWARE(self), TRUE); fu_firmware_set_size_max(FU_FIRMWARE(self), G_MAXUINT32); /* ~4GB */ self->builder = xb_builder_new(); self->jcat_file = jcat_file_new(); self->jcat_context = jcat_context_new(); jcat_context_blob_kind_allow(self->jcat_context, JCAT_BLOB_KIND_SHA256); jcat_context_blob_kind_allow(self->jcat_context, JCAT_BLOB_KIND_SHA512); jcat_context_blob_kind_allow(self->jcat_context, JCAT_BLOB_KIND_PKCS7); jcat_context_blob_kind_allow(self->jcat_context, JCAT_BLOB_KIND_GPG); } static void fu_cabinet_finalize(GObject *obj) { FuCabinet *self = FU_CABINET(obj); if (self->silo != NULL) g_object_unref(self->silo); if (self->builder != NULL) g_object_unref(self->builder); g_free(self->container_checksum); g_free(self->container_checksum_alt); g_object_unref(self->jcat_context); g_object_unref(self->jcat_file); G_OBJECT_CLASS(fu_cabinet_parent_class)->finalize(obj); } static void fu_cabinet_class_init(FuCabinetClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuFirmwareClass *firmware_class = FU_FIRMWARE_CLASS(klass); object_class->finalize = fu_cabinet_finalize; firmware_class->parse = fu_cabinet_parse; } /** * fu_cabinet_new: * * Returns: a #FuCabinet * * Since: 1.4.0 **/ FuCabinet * fu_cabinet_new(void) { return g_object_new(FU_TYPE_CABINET, NULL); } fwupd-2.0.10/src/fu-cabinet.h000066400000000000000000000023331501337203100156700ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include #include "fu-cab-firmware.h" #define FU_TYPE_CABINET (fu_cabinet_get_type()) G_DECLARE_FINAL_TYPE(FuCabinet, fu_cabinet, FU, CABINET, FuCabFirmware) /** * FuCabinetSignFlags: * @FU_CABINET_SIGN_FLAG_NONE: No flags set * * The flags to use when signing the archive. **/ typedef enum { FU_CABINET_SIGN_FLAG_NONE = 0, /*< private >*/ FU_CABINET_SIGN_FLAG_LAST } FuCabinetSignFlags; FuCabinet * fu_cabinet_new(void); void fu_cabinet_set_jcat_context(FuCabinet *self, JcatContext *jcat_context) G_GNUC_NON_NULL(1); gboolean fu_cabinet_sign(FuCabinet *self, GBytes *cert, GBytes *privkey, FuCabinetSignFlags flags, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1, 2, 3); void fu_cabinet_add_file(FuCabinet *self, const gchar *basename, GBytes *data) G_GNUC_NON_NULL(1, 2, 3); XbSilo * fu_cabinet_get_silo(FuCabinet *self, GError **error) G_GNUC_NON_NULL(1); GPtrArray * fu_cabinet_get_components(FuCabinet *self, GError **error) G_GNUC_NON_NULL(1); XbNode * fu_cabinet_get_component(FuCabinet *self, const gchar *id, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/src/fu-client-list.c000066400000000000000000000132261501337203100165100ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuClientList" #include "config.h" #include "fu-client-list.h" struct _FuClientList { GObject parent_instance; GPtrArray *array; /* (element-type FuClientListItem) */ GDBusConnection *connection; /* nullable */ }; typedef struct { FuClientList *self; /* no-ref */ FuClient *client; /* ref */ guint watcher_id; } FuClientListItem; G_DEFINE_TYPE(FuClientList, fu_client_list, G_TYPE_OBJECT) enum { PROP_0, PROP_CONNECTION, PROP_LAST }; enum { SIGNAL_ADDED, SIGNAL_REMOVED, SIGNAL_LAST }; static guint signals[SIGNAL_LAST] = {0}; static void fu_client_list_emit_added(FuClientList *self, FuClient *client) { g_debug("client %s added", fu_client_get_sender(client)); g_signal_emit(self, signals[SIGNAL_ADDED], 0, client); } static void fu_client_list_emit_removed(FuClientList *self, FuClient *client) { g_debug("client %s removed", fu_client_get_sender(client)); g_signal_emit(self, signals[SIGNAL_REMOVED], 0, client); } static void fu_client_list_sender_name_vanished_cb(GDBusConnection *connection, const gchar *name, gpointer user_data) { FuClientListItem *item = (FuClientListItem *)user_data; FuClientList *self = FU_CLIENT_LIST(item->self); g_autoptr(FuClient) client = g_object_ref(item->client); fu_client_remove_flag(client, FU_CLIENT_FLAG_ACTIVE); g_ptr_array_remove(self->array, item); fu_client_list_emit_removed(self, client); } FuClient * fu_client_list_register(FuClientList *self, const gchar *sender) { FuClient *client; FuClientListItem *item; g_return_val_if_fail(FU_IS_CLIENT_LIST(self), NULL); /* already exists */ client = fu_client_list_get_by_sender(self, sender); if (client != NULL) return client; /* create and watch */ item = g_new0(FuClientListItem, 1); item->self = self; item->client = fu_client_new(sender); if (self->connection != NULL && sender != NULL) { item->watcher_id = g_bus_watch_name_on_connection(self->connection, sender, G_BUS_NAME_WATCHER_FLAGS_NONE, NULL, fu_client_list_sender_name_vanished_cb, item, NULL); } g_ptr_array_add(self->array, item); /* success */ fu_client_list_emit_added(self, item->client); return g_object_ref(item->client); } GPtrArray * fu_client_list_get_all(FuClientList *self) { g_autoptr(GPtrArray) array = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_return_val_if_fail(FU_IS_CLIENT_LIST(self), NULL); for (guint i = 0; i < self->array->len; i++) { FuClientListItem *item = g_ptr_array_index(self->array, i); g_ptr_array_add(array, g_object_ref(item->client)); } return g_steal_pointer(&array); } FuClient * fu_client_list_get_by_sender(FuClientList *self, const gchar *sender) { g_return_val_if_fail(FU_IS_CLIENT_LIST(self), NULL); for (guint i = 0; i < self->array->len; i++) { FuClientListItem *item = g_ptr_array_index(self->array, i); if (g_strcmp0(sender, fu_client_get_sender(item->client)) == 0) return g_object_ref(item->client); } return NULL; } static void fu_client_list_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuClientList *self = FU_CLIENT_LIST(object); switch (prop_id) { case PROP_CONNECTION: g_value_set_object(value, self->connection); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_client_list_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuClientList *self = FU_CLIENT_LIST(object); switch (prop_id) { case PROP_CONNECTION: self->connection = g_value_dup_object(value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_client_list_item_free(FuClientListItem *item) { if (item->watcher_id > 0) g_bus_unwatch_name(item->watcher_id); g_object_unref(item->client); g_free(item); } static void fu_client_list_init(FuClientList *self) { self->array = g_ptr_array_new_with_free_func((GDestroyNotify)fu_client_list_item_free); } static void fu_client_list_finalize(GObject *obj) { FuClientList *self = FU_CLIENT_LIST(obj); g_ptr_array_unref(self->array); if (self->connection != NULL) g_object_unref(self->connection); G_OBJECT_CLASS(fu_client_list_parent_class)->finalize(obj); } static void fu_client_list_class_init(FuClientListClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); GParamSpec *pspec; object_class->get_property = fu_client_list_get_property; object_class->set_property = fu_client_list_set_property; object_class->finalize = fu_client_list_finalize; pspec = g_param_spec_object("connection", NULL, NULL, G_TYPE_DBUS_CONNECTION, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_CONNECTION, pspec); signals[SIGNAL_ADDED] = g_signal_new("added", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 1, FU_TYPE_CLIENT); signals[SIGNAL_REMOVED] = g_signal_new("removed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 1, FU_TYPE_CLIENT); } FuClientList * fu_client_list_new(GDBusConnection *connection) { FuClientList *self; g_return_val_if_fail(connection == NULL || G_IS_DBUS_CONNECTION(connection), NULL); self = g_object_new(FU_TYPE_CLIENT_LIST, "connection", connection, NULL); return FU_CLIENT_LIST(self); } fwupd-2.0.10/src/fu-client-list.h000066400000000000000000000012061501337203100165100ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-client.h" #define FU_TYPE_CLIENT_LIST (fu_client_list_get_type()) G_DECLARE_FINAL_TYPE(FuClientList, fu_client_list, FU, CLIENT_LIST, GObject) FuClientList * fu_client_list_new(GDBusConnection *connection); GPtrArray * fu_client_list_get_all(FuClientList *self) G_GNUC_NON_NULL(1); FuClient * fu_client_list_register(FuClientList *self, const gchar *sender) G_GNUC_NON_NULL(1); FuClient * fu_client_list_get_by_sender(FuClientList *self, const gchar *sender) G_GNUC_NON_NULL(1); fwupd-2.0.10/src/fu-client.c000066400000000000000000000101131501337203100155270ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuClient" #include "config.h" #include "fu-client.h" struct _FuClient { GObject parent_instance; gchar *sender; GHashTable *hints; /* str:str */ FwupdFeatureFlags feature_flags; FuClientFlag flags; }; G_DEFINE_TYPE(FuClient, fu_client, G_TYPE_OBJECT) enum { PROP_0, PROP_SENDER, PROP_FLAGS, PROP_LAST }; void fu_client_set_feature_flags(FuClient *self, FwupdFeatureFlags feature_flags) { g_return_if_fail(FU_IS_CLIENT(self)); self->feature_flags = feature_flags; } FwupdFeatureFlags fu_client_get_feature_flags(FuClient *self) { g_return_val_if_fail(FU_IS_CLIENT(self), FWUPD_FEATURE_FLAG_NONE); return self->feature_flags; } const gchar * fu_client_get_sender(FuClient *self) { g_return_val_if_fail(FU_IS_CLIENT(self), NULL); return self->sender; } const gchar * fu_client_lookup_hint(FuClient *self, const gchar *key) { g_return_val_if_fail(FU_IS_CLIENT(self), NULL); g_return_val_if_fail(key != NULL, NULL); return g_hash_table_lookup(self->hints, key); } void fu_client_insert_hint(FuClient *self, const gchar *key, const gchar *value) { g_return_if_fail(FU_IS_CLIENT(self)); g_return_if_fail(key != NULL); g_return_if_fail(value != NULL); g_hash_table_insert(self->hints, g_strdup(key), g_strdup(value)); } static void fu_client_add_flag(FuClient *self, FuClientFlag flag) { g_return_if_fail(FU_IS_CLIENT(self)); g_return_if_fail(flag != FU_CLIENT_FLAG_NONE); if ((self->flags & flag) > 0) return; self->flags |= flag; g_object_notify(G_OBJECT(self), "flags"); } void fu_client_remove_flag(FuClient *self, FuClientFlag flag) { g_return_if_fail(FU_IS_CLIENT(self)); g_return_if_fail(flag != FU_CLIENT_FLAG_NONE); if ((self->flags & flag) == 0) return; self->flags &= ~flag; g_object_notify(G_OBJECT(self), "flags"); } gboolean fu_client_has_flag(FuClient *self, FuClientFlag flag) { g_return_val_if_fail(FU_IS_CLIENT(self), FALSE); g_return_val_if_fail(flag != FU_CLIENT_FLAG_NONE, FALSE); return (self->flags & flag) > 0; } static void fu_client_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuClient *self = FU_CLIENT(object); switch (prop_id) { case PROP_SENDER: g_value_set_string(value, self->sender); break; case PROP_FLAGS: g_value_set_uint64(value, self->flags); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_client_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuClient *self = FU_CLIENT(object); switch (prop_id) { case PROP_SENDER: self->sender = g_value_dup_string(value); break; case PROP_FLAGS: fu_client_add_flag(self, (FuClientFlag)g_value_get_uint64(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_client_init(FuClient *self) { self->hints = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); } static void fu_client_finalize(GObject *obj) { FuClient *self = FU_CLIENT(obj); g_free(self->sender); g_hash_table_unref(self->hints); G_OBJECT_CLASS(fu_client_parent_class)->finalize(obj); } static void fu_client_class_init(FuClientClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); GParamSpec *pspec; object_class->finalize = fu_client_finalize; object_class->get_property = fu_client_get_property; object_class->set_property = fu_client_set_property; pspec = g_param_spec_string("sender", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_SENDER, pspec); pspec = g_param_spec_uint64("flags", NULL, NULL, 0, G_MAXUINT64, 0, G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_FLAGS, pspec); } FuClient * fu_client_new(const gchar *sender) { return FU_CLIENT(g_object_new(FU_TYPE_CLIENT, "sender", sender, "flags", (guint64)FU_CLIENT_FLAG_ACTIVE, NULL)); } fwupd-2.0.10/src/fu-client.h000066400000000000000000000017361501337203100155470ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-engine-struct.h" #define FU_TYPE_CLIENT (fu_client_get_type()) G_DECLARE_FINAL_TYPE(FuClient, fu_client, FU, CLIENT, GObject) FuClient * fu_client_new(const gchar *sender); const gchar * fu_client_get_sender(FuClient *self) G_GNUC_NON_NULL(1); const gchar * fu_client_lookup_hint(FuClient *self, const gchar *key) G_GNUC_NON_NULL(1, 2); void fu_client_insert_hint(FuClient *self, const gchar *key, const gchar *value) G_GNUC_NON_NULL(1, 2, 3); void fu_client_set_feature_flags(FuClient *self, FwupdFeatureFlags feature_flags) G_GNUC_NON_NULL(1); FwupdFeatureFlags fu_client_get_feature_flags(FuClient *self) G_GNUC_NON_NULL(1); void fu_client_remove_flag(FuClient *self, FuClientFlag flag) G_GNUC_NON_NULL(1); gboolean fu_client_has_flag(FuClient *self, FuClientFlag flag) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); fwupd-2.0.10/src/fu-console.c000066400000000000000000000455411501337203100157300ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuProgressBar" #include "config.h" #include #include #ifdef HAVE_READLINE #include #include #endif #include "fu-console.h" #ifdef _WIN32 #include #include #endif struct _FuConsole { GObject parent_instance; GMainContext *main_ctx; FwupdStatus status; gboolean spinner_count_up; /* width in visible chars */ guint spinner_idx; /* width in visible chars */ guint length_percentage; /* width in visible chars */ guint length_status; /* width in visible chars */ guint percentage; GSource *timer_source; gint64 last_animated; /* monotonic */ GTimer *time_elapsed; gdouble last_estimate; gboolean interactive; gboolean contents_to_clear; }; G_DEFINE_TYPE(FuConsole, fu_console, G_TYPE_OBJECT) gboolean fu_console_setup(FuConsole *self, GError **error) { #ifdef _WIN32 HANDLE hOut; DWORD dwMode = 0; /* enable VT sequences */ hOut = GetStdHandle(STD_OUTPUT_HANDLE); if (hOut == INVALID_HANDLE_VALUE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to get stdout [%u]", (guint)GetLastError()); return FALSE; } if (!GetConsoleMode(hOut, &dwMode)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to get mode [%u]", (guint)GetLastError()); return FALSE; } dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; if (!SetConsoleMode(hOut, dwMode)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to set mode [%u]", (guint)GetLastError()); return FALSE; } if (!SetConsoleOutputCP(CP_UTF8)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to set output UTF-8 [%u]", (guint)GetLastError()); return FALSE; } if (!SetConsoleCP(CP_UTF8)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to set UTF-8 [%u]", (guint)GetLastError()); return FALSE; } #else if (isatty(fileno(stdin)) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not a TTY"); return FALSE; } #endif /* success */ return TRUE; } static void fu_console_erase_line(FuConsole *self) { if (!self->interactive) return; g_print("\033[G"); } static void fu_console_reset_line(FuConsole *self) { if (self->contents_to_clear) { fu_console_erase_line(self); g_print("\n"); self->contents_to_clear = FALSE; } } void fu_console_print_kv(FuConsole *self, const gchar *title, const gchar *msg) { gsize title_len; g_auto(GStrv) lines = NULL; if (msg == NULL) return; fu_console_reset_line(self); g_print("%s:", title); /* pad */ title_len = fu_strwidth(title) + 1; lines = g_strsplit(msg, "\n", -1); for (guint j = 0; lines[j] != NULL; j++) { for (gsize i = title_len; i < 25; i++) g_print(" "); g_print("%s\n", lines[j]); title_len = 0; } } #ifndef HAVE_READLINE static gchar * readline(const gchar *prompt) /* nocheck:name */ { char buffer[64] = {0}; if (prompt != NULL) g_print("%s\n", prompt); if (!fgets(buffer, sizeof(buffer), stdin)) return NULL; g_strdelimit(buffer, "\n", '\0'); return g_strndup(buffer, sizeof(buffer)); } #endif guint fu_console_input_uint(FuConsole *self, guint maxnum, const gchar *format, ...) { gint retval; guint answer = 0; va_list args; g_autofree gchar *tmp = NULL; g_autofree gchar *prompt = NULL; va_start(args, format); tmp = g_strdup_vprintf(format, args); va_end(args); fu_console_print_full(self, FU_CONSOLE_PRINT_FLAG_NONE, "%s [0-%u]: ", tmp, maxnum); do { g_autofree gchar *buffer = readline(prompt); /* get a number */ retval = sscanf(buffer, "%u", &answer); /* positive */ if (retval == 1 && answer <= maxnum) break; if (prompt == NULL) { /* TRANSLATORS: the user isn't reading the question */ prompt = g_strdup_printf(_("Please enter a number from 0 to %u: "), maxnum); } } while (TRUE); return answer; } gboolean fu_console_input_bool(FuConsole *self, gboolean def, const gchar *format, ...) { va_list args; g_autofree gchar *tmp = NULL; g_autofree gchar *prompt = NULL; va_start(args, format); tmp = g_strdup_vprintf(format, args); va_end(args); fu_console_print_full(self, FU_CONSOLE_PRINT_FLAG_NONE, "%s [%s]: ", tmp, def ? "Y|n" : "y|N"); do { g_autofree gchar *buffer = readline(prompt); if (!strlen(buffer)) return def; buffer[0] = g_ascii_toupper(buffer[0]); if (g_strcmp0(buffer, "Y") == 0) return TRUE; if (g_strcmp0(buffer, "N") == 0) return FALSE; if (prompt == NULL) { /* TRANSLATORS: the user isn't reading the question -- %1 is 'Y' and %2 is * 'N' */ prompt = g_strdup_printf(_("Please enter either %s or %s: "), "Y", "N"); } } while (TRUE); return FALSE; } static GPtrArray * fu_console_strsplit_words(const gchar *text, guint line_len) { g_auto(GStrv) tokens = NULL; g_autoptr(GPtrArray) lines = g_ptr_array_new_with_free_func(g_free); g_autoptr(GString) curline = g_string_new(NULL); /* sanity check */ if (text == NULL || text[0] == '\0') return NULL; if (line_len == 0) return NULL; /* tokenize the string */ tokens = g_strsplit(text, " ", -1); for (guint i = 0; tokens[i] != NULL; i++) { /* current line plus new token is okay */ if (curline->len + fu_strwidth(tokens[i]) < line_len) { g_string_append_printf(curline, "%s ", tokens[i]); continue; } /* too long, so remove space, add newline and dump */ if (curline->len > 0) g_string_truncate(curline, curline->len - 1); g_ptr_array_add(lines, g_strdup(curline->str)); g_string_truncate(curline, 0); g_string_append_printf(curline, "%s ", tokens[i]); } /* any incomplete line? */ if (curline->len > 0) { g_string_truncate(curline, curline->len - 1); g_ptr_array_add(lines, g_strdup(curline->str)); } return g_steal_pointer(&lines); } static void fu_console_box_line(const gchar *start, const gchar *text, const gchar *end, const gchar *padding, guint width) { guint offset = 0; if (start != NULL) { offset += fu_strwidth(start); g_print("%s", start); } if (text != NULL) { offset += fu_strwidth(text); g_print("%s", text); } if (end != NULL) offset += fu_strwidth(end); for (guint i = offset; i < width; i++) g_print("%s", padding); if (end != NULL) g_print("%s\n", end); } void fu_console_line(FuConsole *self, guint width) { g_autoptr(GString) str = g_string_new_len(NULL, width); for (guint i = 0; i < width; i++) g_string_append(str, "─"); fu_console_print_literal(self, str->str); } void fu_console_box(FuConsole *self, const gchar *title, const gchar *body, guint width) { /* nothing to do */ if (title == NULL && body == NULL) return; /* header */ fu_console_reset_line(self); fu_console_box_line("â•”", NULL, "â•—", "â•", width); /* optional title */ if (title != NULL) { g_autoptr(GPtrArray) lines = fu_console_strsplit_words(title, width - 4); for (guint j = 0; j < lines->len; j++) { const gchar *line = g_ptr_array_index(lines, j); fu_console_box_line("â•‘ ", line, " â•‘", " ", width); } } /* join */ if (title != NULL && body != NULL) fu_console_box_line("â• ", NULL, "â•£", "â•", width); /* optional body */ if (body != NULL) { gboolean has_nonempty = FALSE; g_auto(GStrv) split = g_strsplit(body, "\n", -1); for (guint i = 0; split[i] != NULL; i++) { g_autoptr(GPtrArray) lines = fu_console_strsplit_words(split[i], width - 4); if (lines == NULL) { if (has_nonempty) { fu_console_box_line("â•‘ ", NULL, " â•‘", " ", width); has_nonempty = FALSE; } continue; } for (guint j = 0; j < lines->len; j++) { const gchar *line = g_ptr_array_index(lines, j); fu_console_box_line("â•‘ ", line, " â•‘", " ", width); } has_nonempty = TRUE; } } /* footer */ fu_console_box_line("╚", NULL, "â•", "â•", width); } static const gchar * fu_console_status_to_string(FwupdStatus status) { switch (status) { case FWUPD_STATUS_IDLE: /* TRANSLATORS: daemon is inactive */ return _("Idle…"); break; case FWUPD_STATUS_DECOMPRESSING: /* TRANSLATORS: decompressing the firmware file */ return _("Decompressing…"); break; case FWUPD_STATUS_LOADING: /* TRANSLATORS: parsing the firmware information */ return _("Loading…"); break; case FWUPD_STATUS_DEVICE_RESTART: /* TRANSLATORS: restarting the device to pick up new F/W */ return _("Restarting device…"); break; case FWUPD_STATUS_DEVICE_READ: /* TRANSLATORS: reading from the flash chips */ return _("Reading…"); break; case FWUPD_STATUS_DEVICE_WRITE: /* TRANSLATORS: writing to the flash chips */ return _("Writing…"); break; case FWUPD_STATUS_DEVICE_ERASE: /* TRANSLATORS: erasing contents of the flash chips */ return _("Erasing…"); break; case FWUPD_STATUS_DEVICE_VERIFY: /* TRANSLATORS: verifying we wrote the firmware correctly */ return _("Verifying…"); break; case FWUPD_STATUS_SCHEDULING: /* TRANSLATORS: scheduling an update to be done on the next boot */ return _("Scheduling…"); break; case FWUPD_STATUS_DOWNLOADING: /* TRANSLATORS: downloading from a remote server */ return _("Downloading…"); break; case FWUPD_STATUS_WAITING_FOR_AUTH: /* TRANSLATORS: waiting for user to authenticate */ return _("Authenticating…"); break; case FWUPD_STATUS_DEVICE_BUSY: case FWUPD_STATUS_WAITING_FOR_USER: /* TRANSLATORS: waiting for device to do something */ return _("Waiting…"); break; default: break; } /* TRANSLATORS: current daemon status is unknown */ return _("Unknown"); } static gboolean _fu_status_is_predictable(FwupdStatus status) { if (status == FWUPD_STATUS_DEVICE_ERASE) return TRUE; if (status == FWUPD_STATUS_DEVICE_VERIFY) return TRUE; if (status == FWUPD_STATUS_DEVICE_READ) return TRUE; if (status == FWUPD_STATUS_DEVICE_WRITE) return TRUE; if (status == FWUPD_STATUS_DOWNLOADING) return TRUE; return FALSE; } static gboolean fu_console_estimate_ready(FuConsole *self, guint percentage) { gdouble old; gdouble elapsed; /* now invalid */ if (percentage == 0 || percentage == 100) { g_timer_start(self->time_elapsed); self->last_estimate = 0; return FALSE; } /* allow-list things that make sense... */ if (!_fu_status_is_predictable(self->status)) return FALSE; old = self->last_estimate; elapsed = g_timer_elapsed(self->time_elapsed, NULL); self->last_estimate = elapsed / percentage * (100 - percentage); /* estimate is ready if we have decreased */ return old > self->last_estimate; } static gchar * fu_console_time_remaining_str(FuConsole *self) { /* less than 5 seconds remaining */ if (self->last_estimate < 5) return NULL; /* less than 60 seconds remaining */ if (self->last_estimate < 60) { /* TRANSLATORS: time remaining for completing firmware flash */ return g_strdup(_("Less than one minute remaining")); } return g_strdup_printf( /* TRANSLATORS: more than a minute */ ngettext("%.0f minute remaining", "%.0f minutes remaining", self->last_estimate / 60), self->last_estimate / 60); } static void fu_console_refresh(FuConsole *self) { const gchar *title; guint i; g_autoptr(GString) str = g_string_new(NULL); /* sanity check */ if (self->status == FWUPD_STATUS_IDLE || self->status == FWUPD_STATUS_UNKNOWN) return; /* erase previous line */ fu_console_erase_line(self); /* add status */ title = fu_console_status_to_string(self->status); g_string_append(str, title); for (i = fu_strwidth(str->str); i < self->length_status; i++) g_string_append_c(str, ' '); /* add console */ g_string_append(str, "["); if (self->percentage > 0) { for (i = 0; i < (self->length_percentage - 1) * self->percentage / 100; i++) g_string_append_c(str, '*'); for (i = i + 1; i < self->length_percentage; i++) g_string_append_c(str, ' '); } else { const gchar chars[] = { '-', '\\', '|', '/', }; for (i = 0; i < self->spinner_idx; i++) g_string_append_c(str, ' '); g_string_append_c(str, chars[i / 4 % G_N_ELEMENTS(chars)]); for (i = i + 1; i < self->length_percentage - 1; i++) g_string_append_c(str, ' '); } g_string_append_c(str, ']'); /* once we have good data show an estimate of time remaining */ if (fu_console_estimate_ready(self, self->percentage)) { g_autofree gchar *remaining = fu_console_time_remaining_str(self); if (remaining != NULL) g_string_append_printf(str, " %s…", remaining); } /* dump to screen */ g_print("%s", str->str); self->contents_to_clear = TRUE; } /** * fu_console_print_full: * @self: a #FuConsole * @flags; a #FuConsolePrintFlags, e.g. %FU_CONSOLE_PRINT_FLAG_STDERR * @text: string * * Clears the console, and prints the text. **/ void fu_console_print_full(FuConsole *self, FuConsolePrintFlags flags, const gchar *format, ...) { va_list args; g_autoptr(GString) str = g_string_new(NULL); va_start(args, format); g_string_append_vprintf(str, format, args); va_end(args); if (flags & FU_CONSOLE_PRINT_FLAG_WARNING) { /* TRANSLATORS: this is a prefix on the console */ g_autofree gchar *fmt = fu_console_color_format(_("WARNING"), FU_CONSOLE_COLOR_RED); g_string_prepend(str, ": "); g_string_prepend(str, fmt); flags |= FU_CONSOLE_PRINT_FLAG_STDERR; } fu_console_reset_line(self); if (flags & FU_CONSOLE_PRINT_FLAG_STDERR) { g_printerr("%s", str->str); } else { g_print("%s", str->str); } } void fu_console_print_literal(FuConsole *self, const gchar *text) { fu_console_reset_line(self); g_print("%s\n", text); } /** * fu_console_print: * @self: a #FuConsole * @text: string * * Clears the console, prints the text and prints a newline. **/ void fu_console_print(FuConsole *self, const gchar *format, ...) { va_list args; g_autofree gchar *tmp = NULL; va_start(args, format); tmp = g_strdup_vprintf(format, args); va_end(args); fu_console_print_literal(self, tmp); } /** * fu_console_set_progress_title: * @self: A #FuConsole * @title: A string * * Sets console title **/ void fu_console_set_progress_title(FuConsole *self, const gchar *title) { if (!self->interactive) return; fu_console_erase_line(self); g_print("%s\n", title); fu_console_refresh(self); } /** * fu_console_set_main_context: * @self: A #FuConsole * @main_ctx: (nullable): main context * * Sets console main context to use for animations. **/ void fu_console_set_main_context(FuConsole *self, GMainContext *main_ctx) { self->main_ctx = g_main_context_ref(main_ctx); } static void fu_console_spin_inc(FuConsole *self) { /* reset */ self->last_animated = g_get_monotonic_time(); /* up to down */ if (self->spinner_count_up) { if (++self->spinner_idx > self->length_percentage - 3) self->spinner_count_up = FALSE; } else { if (--self->spinner_idx == 0) self->spinner_count_up = TRUE; } } static gboolean fu_console_spin_cb(gpointer user_data) { FuConsole *self = FU_CONSOLE(user_data); /* move the spinner index up to down */ fu_console_spin_inc(self); /* update the terminal */ fu_console_refresh(self); return G_SOURCE_CONTINUE; } static void fu_console_spin_end(FuConsole *self) { if (self->timer_source != NULL) { g_source_destroy(self->timer_source); self->timer_source = NULL; /* reset when the spinner has been stopped */ g_timer_start(self->time_elapsed); } /* go back to the start when we next go into unknown percentage mode */ self->spinner_idx = 0; self->spinner_count_up = TRUE; } static void fu_console_spin_start(FuConsole *self) { if (self->timer_source != NULL) g_source_destroy(self->timer_source); self->timer_source = g_timeout_source_new(40); g_source_set_callback(self->timer_source, fu_console_spin_cb, self, NULL); g_source_attach(self->timer_source, self->main_ctx); } /** * fu_console_set_progress: * @self: A #FuConsole * @status: A #FwupdStatus * @percentage: unsigned integer * * Refreshes the progress bar with the new percentage and status. **/ void fu_console_set_progress(FuConsole *self, FwupdStatus status, guint percentage) { g_return_if_fail(FU_IS_CONSOLE(self)); /* not useful */ if (status == FWUPD_STATUS_UNKNOWN) return; /* ignore duplicates */ if (self->status == status && self->percentage == percentage) return; /* cache */ self->status = status; self->percentage = percentage; /* dumb */ if (!self->interactive) { g_printerr("%s: %u%%\n", fu_console_status_to_string(status), percentage); return; } /* if the main loop isn't spinning and we've not had a chance to * execute the callback just do the refresh now manually */ if (percentage == 0 && status != FWUPD_STATUS_IDLE && self->status != FWUPD_STATUS_UNKNOWN) { if ((g_get_monotonic_time() - self->last_animated) / 1000 > 40) { fu_console_spin_inc(self); fu_console_refresh(self); } } /* enable or disable the spinner timeout */ if (percentage > 0) { fu_console_spin_end(self); } else { fu_console_spin_start(self); } /* update the terminal */ fu_console_refresh(self); } /** * fu_console_set_interactive: * @self: A #FuConsole * @interactive: #gboolean * * Marks the console as interactive or not **/ void fu_console_set_interactive(FuConsole *self, gboolean interactive) { g_return_if_fail(FU_IS_CONSOLE(self)); self->interactive = interactive; } /** * fu_console_set_status_length: * @self: A #FuConsole * @len: unsigned integer * * Sets the width of the progressbar status, which must be greater that 3. **/ void fu_console_set_status_length(FuConsole *self, guint len) { g_return_if_fail(FU_IS_CONSOLE(self)); g_return_if_fail(len > 3); self->length_status = len; } /** * fu_console_set_percentage_length: * @self: A #FuConsole * @len: unsigned integer * * Sets the width of the progressbar percentage, which must be greater that 3. **/ void fu_console_set_percentage_length(FuConsole *self, guint len) { g_return_if_fail(FU_IS_CONSOLE(self)); g_return_if_fail(len > 3); self->length_percentage = len; } void fu_console_beep(FuConsole *self, guint count) { for (guint i = 0; i < count; i++) { if (i > 0) g_usleep(250000); g_print("\007"); } } static void fu_console_init(FuConsole *self) { self->length_percentage = 40; self->length_status = 25; self->spinner_count_up = TRUE; self->time_elapsed = g_timer_new(); self->interactive = TRUE; } static void fu_console_finalize(GObject *obj) { FuConsole *self = FU_CONSOLE(obj); fu_console_reset_line(self); if (self->timer_source != 0) g_source_destroy(self->timer_source); if (self->main_ctx != NULL) g_main_context_unref(self->main_ctx); g_timer_destroy(self->time_elapsed); G_OBJECT_CLASS(fu_console_parent_class)->finalize(obj); } static void fu_console_class_init(FuConsoleClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_console_finalize; } /** * fu_console_new: * * Creates a new #FuConsole **/ FuConsole * fu_console_new(void) { FuConsole *self; self = g_object_new(FU_TYPE_CONSOLE, NULL); return FU_CONSOLE(self); } fwupd-2.0.10/src/fu-console.h000066400000000000000000000045541501337203100157340ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_CONSOLE (fu_console_get_type()) G_DECLARE_FINAL_TYPE(FuConsole, fu_console, FU, CONSOLE, GObject) typedef enum { FU_CONSOLE_COLOR_BLACK = 30, FU_CONSOLE_COLOR_RED = 31, FU_CONSOLE_COLOR_GREEN = 32, FU_CONSOLE_COLOR_YELLOW = 33, FU_CONSOLE_COLOR_BLUE = 34, FU_CONSOLE_COLOR_MAGENTA = 35, FU_CONSOLE_COLOR_CYAN = 36, FU_CONSOLE_COLOR_WHITE = 37, } FuConsoleColor; typedef enum { FU_CONSOLE_PRINT_FLAG_NONE = 0, FU_CONSOLE_PRINT_FLAG_STDERR = 1 << 0, FU_CONSOLE_PRINT_FLAG_WARNING = 1 << 1, } FuConsolePrintFlags; gchar * fu_console_color_format(const gchar *text, FuConsoleColor fg_color) G_GNUC_NON_NULL(1); FuConsole * fu_console_new(void); gboolean fu_console_setup(FuConsole *self, GError **error) G_GNUC_NON_NULL(1); guint fu_console_input_uint(FuConsole *self, guint maxnum, const gchar *format, ...) G_GNUC_PRINTF(3, 4) G_GNUC_NON_NULL(1, 3); gboolean fu_console_input_bool(FuConsole *self, gboolean def, const gchar *format, ...) G_GNUC_PRINTF(3, 4) G_GNUC_NON_NULL(1, 3); void fu_console_print_full(FuConsole *self, FuConsolePrintFlags flags, const gchar *format, ...) G_GNUC_PRINTF(3, 4) G_GNUC_NON_NULL(1, 3); void fu_console_print(FuConsole *self, const gchar *format, ...) G_GNUC_PRINTF(2, 3) G_GNUC_NON_NULL(1); void fu_console_print_literal(FuConsole *self, const gchar *text) G_GNUC_NON_NULL(1); void fu_console_print_kv(FuConsole *self, const gchar *title, const gchar *msg) G_GNUC_NON_NULL(1); void fu_console_line(FuConsole *self, guint width) G_GNUC_NON_NULL(1); void fu_console_box(FuConsole *self, const gchar *title, const gchar *body, guint width) G_GNUC_NON_NULL(1); void fu_console_beep(FuConsole *self, guint count) G_GNUC_NON_NULL(1); void fu_console_set_progress(FuConsole *self, FwupdStatus status, guint percentage) G_GNUC_NON_NULL(1); void fu_console_set_status_length(FuConsole *self, guint len) G_GNUC_NON_NULL(1); void fu_console_set_percentage_length(FuConsole *self, guint len) G_GNUC_NON_NULL(1); void fu_console_set_progress_title(FuConsole *self, const gchar *title) G_GNUC_NON_NULL(1); void fu_console_set_interactive(FuConsole *self, gboolean interactive) G_GNUC_NON_NULL(1); void fu_console_set_main_context(FuConsole *self, GMainContext *main_ctx) G_GNUC_NON_NULL(1); fwupd-2.0.10/src/fu-daemon.c000066400000000000000000000200541501337203100155210ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuMain" #include "config.h" #ifdef HAVE_MALLOC_H #include #endif #ifdef HAVE_MMAN_H #include #endif #include "fu-context-private.h" #include "fu-daemon.h" typedef struct { FuEngine *engine; GMainLoop *loop; FuDaemonMachineKind machine_kind; guint housekeeping_id; gboolean update_in_progress; gboolean pending_stop; guint process_quit_id; } FuDaemonPrivate; G_DEFINE_TYPE_WITH_PRIVATE(FuDaemon, fu_daemon, G_TYPE_OBJECT) #define GET_PRIVATE(o) (fu_daemon_get_instance_private(o)) #define FU_DAEMON_HOUSEKEEPING_DELAY 10 /* seconds */ FuEngine * fu_daemon_get_engine(FuDaemon *self) { FuDaemonPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DAEMON(self), NULL); return priv->engine; } void fu_daemon_set_machine_kind(FuDaemon *self, FuDaemonMachineKind machine_kind) { FuDaemonPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DAEMON(self)); priv->machine_kind = machine_kind; } FuDaemonMachineKind fu_daemon_get_machine_kind(FuDaemon *self) { FuDaemonPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DAEMON(self), 0); return priv->machine_kind; } void fu_daemon_set_update_in_progress(FuDaemon *self, gboolean update_in_progress) { FuDaemonPrivate *priv = GET_PRIVATE(self); g_return_if_fail(FU_IS_DAEMON(self)); priv->update_in_progress = update_in_progress; } gboolean fu_daemon_get_pending_stop(FuDaemon *self) { FuDaemonPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DAEMON(self), FALSE); return priv->pending_stop; } static gboolean fu_daemon_schedule_housekeeping_cb(gpointer user_data) { FuDaemon *self = FU_DAEMON(user_data); FuDaemonPrivate *priv = GET_PRIVATE(self); FuContext *ctx = fu_engine_get_context(priv->engine); #ifdef HAVE_MALLOC_TRIM /* drop heap except one page */ malloc_trim(0); #endif /* anything that listens to the context can perform actions now */ fu_context_housekeeping(ctx); /* success */ priv->housekeeping_id = 0; return G_SOURCE_REMOVE; } void fu_daemon_schedule_housekeeping(FuDaemon *self) { FuDaemonPrivate *priv = GET_PRIVATE(self); guint delay = FU_DAEMON_HOUSEKEEPING_DELAY; if (priv->update_in_progress) return; if (priv->housekeeping_id != 0) g_source_remove(priv->housekeeping_id); if (g_getenv("CI") != NULL) delay = 1; priv->housekeeping_id = g_timeout_add_seconds(delay, fu_daemon_schedule_housekeeping_cb, self); } static gboolean fu_daemon_schedule_process_quit_cb(gpointer user_data) { FuDaemon *self = FU_DAEMON(user_data); FuDaemonPrivate *priv = GET_PRIVATE(self); g_autoptr(GError) error = NULL; g_info("daemon asked to quit, shutting down"); priv->process_quit_id = 0; if (!fu_daemon_stop(self, &error)) g_warning("failed to stop daemon, will wait: %s\n", error->message); return G_SOURCE_REMOVE; } void fu_daemon_schedule_process_quit(FuDaemon *self) { FuDaemonPrivate *priv = GET_PRIVATE(self); /* busy? */ if (priv->update_in_progress) { g_warning("asked to quit during a firmware update, ignoring"); return; } /* allow the daemon to respond to the request, then quit */ if (priv->process_quit_id != 0) g_source_remove(priv->process_quit_id); priv->process_quit_id = g_idle_add(fu_daemon_schedule_process_quit_cb, self); } static gboolean fu_daemon_check_syscall_filtering(GError **error) { #ifdef HAVE_MMAN_H if (g_getenv("FWUPD_SYSCALL_FILTER") != NULL) { const gsize bufsz = 10; g_autofree guint8 *buf = g_malloc0(bufsz); if (mlock(buf, bufsz) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_BROKEN_SYSTEM, "syscall filtering is configured but not working"); munlock(buf, bufsz); return FALSE; } g_debug("syscall filtering is working"); } #endif /* success */ return TRUE; } gboolean fu_daemon_setup(FuDaemon *self, const gchar *socket_address, GError **error) { FuDaemonClass *klass = FU_DAEMON_GET_CLASS(self); FuDaemonPrivate *priv = GET_PRIVATE(self); FuEngine *engine = fu_daemon_get_engine(self); const gchar *machine_kind = g_getenv("FWUPD_MACHINE_KIND"); guint timer_max_ms; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GTimer) timer = g_timer_new(); g_return_val_if_fail(FU_IS_DAEMON(self), FALSE); g_return_val_if_fail(FU_IS_ENGINE(engine), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* check that the process manager is preventing access to dangerous system calls */ if (!fu_daemon_check_syscall_filtering(error)) return FALSE; /* allow overriding for development */ if (machine_kind != NULL) { priv->machine_kind = fu_daemon_machine_kind_from_string(machine_kind); if (priv->machine_kind == FU_DAEMON_MACHINE_KIND_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "invalid machine kind specified: %s", machine_kind); return FALSE; } } /* proxy on */ if (!klass->setup(self, socket_address, progress, error)) return FALSE; /* how did we do */ timer_max_ms = fu_config_get_value_u64(FU_CONFIG(fu_engine_get_config(engine)), "fwupd", "IdleInhibitStartupThreshold"); if (timer_max_ms > 0) { guint timer_ms = g_timer_elapsed(timer, NULL) * 1000.f; if (timer_ms > timer_max_ms) { g_autofree gchar *reason = g_strdup_printf("daemon-startup-%ums-max-%ums", timer_ms, timer_max_ms); fu_engine_idle_inhibit(engine, FU_IDLE_INHIBIT_TIMEOUT, reason); } } /* a good place to do the traceback */ if (fu_progress_get_profile(progress)) { g_autofree gchar *str = fu_progress_traceback(progress); if (str != NULL) g_print("\n%s\n", str); } /* success */ return TRUE; } gboolean fu_daemon_start(FuDaemon *self, GError **error) { FuDaemonClass *klass = FU_DAEMON_GET_CLASS(self); FuDaemonPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DAEMON(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* super useful for debugging */ if (g_getenv("FWUPD_VERBOSE") != NULL) { GHashTableIter iter; const gchar *key; const gchar *value; g_autoptr(GHashTable) metadata = NULL; g_autoptr(GString) str = g_string_new("report metadata:"); metadata = fu_engine_get_report_metadata(priv->engine, error); if (metadata == NULL) return FALSE; g_hash_table_iter_init(&iter, metadata); while (g_hash_table_iter_next(&iter, (gpointer *)&key, (gpointer *)&value)) g_string_append_printf(str, "\n - %s=%s", key, value); g_debug("%s", str->str); } /* optional */ if (klass->start != NULL && !klass->start(self, error)) return FALSE; fu_daemon_schedule_housekeeping(self); g_main_loop_run(priv->loop); return TRUE; } gboolean fu_daemon_stop(FuDaemon *self, GError **error) { FuDaemonClass *klass = FU_DAEMON_GET_CLASS(self); FuDaemonPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(FU_IS_DAEMON(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (priv->update_in_progress) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "in a firmware update, ignoring"); priv->pending_stop = TRUE; return FALSE; } /* optional */ if (klass->stop != NULL && !klass->stop(self, error)) return FALSE; g_main_loop_quit(priv->loop); return TRUE; } static void fu_daemon_init(FuDaemon *self) { FuDaemonPrivate *priv = GET_PRIVATE(self); g_autoptr(FuContext) ctx = fu_context_new(); priv->engine = fu_engine_new(ctx); priv->loop = g_main_loop_new(NULL, FALSE); } static void fu_daemon_finalize(GObject *obj) { FuDaemon *self = FU_DAEMON(obj); FuDaemonPrivate *priv = GET_PRIVATE(self); if (priv->loop != NULL) g_main_loop_unref(priv->loop); if (priv->housekeeping_id != 0) g_source_remove(priv->housekeeping_id); if (priv->process_quit_id != 0) g_source_remove(priv->process_quit_id); if (priv->engine != NULL) g_object_unref(priv->engine); G_OBJECT_CLASS(fu_daemon_parent_class)->finalize(obj); } static void fu_daemon_class_init(FuDaemonClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_daemon_finalize; } fwupd-2.0.10/src/fu-daemon.h000066400000000000000000000026371501337203100155350ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-daemon-struct.h" #include "fu-engine.h" #define FU_TYPE_DAEMON (fu_daemon_get_type()) G_DECLARE_DERIVABLE_TYPE(FuDaemon, fu_daemon, FU, DAEMON, GObject) struct _FuDaemonClass { GObjectClass parent_class; gboolean (*setup)(FuDaemon *self, const gchar *socket_address, FuProgress *progress, GError **error); gboolean (*start)(FuDaemon *self, GError **error); gboolean (*stop)(FuDaemon *self, GError **error); }; FuDaemon * fu_daemon_new(void); void fu_daemon_schedule_housekeeping(FuDaemon *self) G_GNUC_NON_NULL(1); void fu_daemon_schedule_process_quit(FuDaemon *self) G_GNUC_NON_NULL(1); gboolean fu_daemon_setup(FuDaemon *self, const gchar *socket_address, GError **error) G_GNUC_NON_NULL(1); gboolean fu_daemon_start(FuDaemon *self, GError **error) G_GNUC_NON_NULL(1); gboolean fu_daemon_stop(FuDaemon *self, GError **error) G_GNUC_NON_NULL(1); FuEngine * fu_daemon_get_engine(FuDaemon *self) G_GNUC_NON_NULL(1); FuDaemonMachineKind fu_daemon_get_machine_kind(FuDaemon *self) G_GNUC_NON_NULL(1); void fu_daemon_set_machine_kind(FuDaemon *self, FuDaemonMachineKind machine_kind) G_GNUC_NON_NULL(1); void fu_daemon_set_update_in_progress(FuDaemon *self, gboolean update_in_progress) G_GNUC_NON_NULL(1); gboolean fu_daemon_get_pending_stop(FuDaemon *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/src/fu-daemon.rs000066400000000000000000000003201501337203100157150ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(FromString)] enum FuDaemonMachineKind { Unknown, Physical, Virtual, Container, } fwupd-2.0.10/src/fu-dbus-daemon.c000066400000000000000000002676301501337203100164710ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuMain" #include "config.h" #include #ifdef HAVE_GIO_UNIX #include #include #endif #include #include #include "fwupd-device-private.h" #include "fwupd-enums-private.h" #include "fwupd-remote-private.h" #include "fwupd-request-private.h" #include "fwupd-security-attr-private.h" #include "fu-bios-settings-private.h" #include "fu-client-list.h" #include "fu-dbus-daemon.h" #include "fu-device-private.h" #include "fu-engine-helper.h" #include "fu-engine-requirements.h" #include "fu-polkit-authority.h" #include "fu-release.h" #include "fu-security-attrs-private.h" #ifdef HAVE_GIO_UNIX #include "fu-unix-seekable-input-stream.h" #endif struct _FuDbusDaemon { FuDaemon parent_instance; GDBusConnection *connection; GDBusNodeInfo *introspection_daemon; GDBusProxy *proxy_uid; FuClientList *client_list; guint32 clients_inhibit_id; FuPolkitAuthority *authority; FwupdStatus status; /* last emitted */ guint percentage; /* last emitted */ guint owner_id; GPtrArray *system_inhibits; }; G_DEFINE_TYPE(FuDbusDaemon, fu_dbus_daemon, FU_TYPE_DAEMON) static void fu_dbus_daemon_engine_changed_cb(FuEngine *engine, FuDbusDaemon *self) { /* not yet connected */ if (self->connection == NULL) return; g_dbus_connection_emit_signal(self->connection, NULL, FWUPD_DBUS_PATH, FWUPD_DBUS_INTERFACE, "Changed", NULL, NULL); fu_daemon_schedule_housekeeping(FU_DAEMON(self)); } static void fu_dbus_daemon_engine_device_added_cb(FuEngine *engine, FuDevice *device, FuDbusDaemon *self) { GVariant *val; /* not yet connected */ if (self->connection == NULL) return; val = fwupd_codec_to_variant(FWUPD_CODEC(device), FWUPD_CODEC_FLAG_NONE); g_dbus_connection_emit_signal(self->connection, NULL, FWUPD_DBUS_PATH, FWUPD_DBUS_INTERFACE, "DeviceAdded", g_variant_new_tuple(&val, 1), NULL); fu_daemon_schedule_housekeeping(FU_DAEMON(self)); } static void fu_dbus_daemon_engine_device_removed_cb(FuEngine *engine, FuDevice *device, FuDbusDaemon *self) { GVariant *val; /* not yet connected */ if (self->connection == NULL) return; val = fwupd_codec_to_variant(FWUPD_CODEC(device), FWUPD_CODEC_FLAG_NONE); g_dbus_connection_emit_signal(self->connection, NULL, FWUPD_DBUS_PATH, FWUPD_DBUS_INTERFACE, "DeviceRemoved", g_variant_new_tuple(&val, 1), NULL); fu_daemon_schedule_housekeeping(FU_DAEMON(self)); } static void fu_dbus_daemon_engine_device_changed_cb(FuEngine *engine, FuDevice *device, FuDbusDaemon *self) { GVariant *val; /* not yet connected */ if (self->connection == NULL) return; val = fwupd_codec_to_variant(FWUPD_CODEC(device), FWUPD_CODEC_FLAG_NONE); g_dbus_connection_emit_signal(self->connection, NULL, FWUPD_DBUS_PATH, FWUPD_DBUS_INTERFACE, "DeviceChanged", g_variant_new_tuple(&val, 1), NULL); fu_daemon_schedule_housekeeping(FU_DAEMON(self)); } static void fu_dbus_daemon_engine_device_request_cb(FuEngine *engine, FwupdRequest *request, FuDbusDaemon *self) { GVariant *val; /* not yet connected */ if (self->connection == NULL) return; val = fwupd_codec_to_variant(FWUPD_CODEC(request), FWUPD_CODEC_FLAG_NONE); g_dbus_connection_emit_signal(self->connection, NULL, FWUPD_DBUS_PATH, FWUPD_DBUS_INTERFACE, "DeviceRequest", g_variant_new_tuple(&val, 1), NULL); } static void fu_dbus_daemon_emit_property_changed(FuDbusDaemon *self, const gchar *property_name, GVariant *property_value) { GVariantBuilder builder; GVariantBuilder invalidated_builder; /* not yet connected */ if (self->connection == NULL) { g_variant_unref(g_variant_ref_sink(property_value)); return; } /* build the dict */ g_variant_builder_init(&invalidated_builder, G_VARIANT_TYPE("as")); g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); g_variant_builder_add(&builder, "{sv}", property_name, property_value); g_dbus_connection_emit_signal( self->connection, NULL, FWUPD_DBUS_PATH, "org.freedesktop.DBus.Properties", "PropertiesChanged", g_variant_new("(sa{sv}as)", FWUPD_DBUS_INTERFACE, &builder, &invalidated_builder), NULL); g_variant_builder_clear(&builder); g_variant_builder_clear(&invalidated_builder); } static void fu_dbus_daemon_set_status(FuDbusDaemon *self, FwupdStatus status) { /* sanity check */ if (self->status == status) return; self->status = status; g_debug("Emitting PropertyChanged('Status'='%s')", fwupd_status_to_string(status)); fu_dbus_daemon_emit_property_changed(self, "Status", g_variant_new_uint32(status)); } static void fu_dbus_daemon_engine_status_changed_cb(FuEngine *engine, FwupdStatus status, FuDbusDaemon *self) { fu_dbus_daemon_set_status(self, status); /* engine has gone idle */ if (status == FWUPD_STATUS_SHUTDOWN) fu_daemon_stop(FU_DAEMON(self), NULL); } static FuEngineRequest * fu_dbus_daemon_create_request(FuDbusDaemon *self, const gchar *sender, GError **error) { FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); FwupdCodecFlags converter_flags = FWUPD_CODEC_FLAG_NONE; guint calling_uid = 0; g_autoptr(FuClient) client = NULL; g_autoptr(FuEngineRequest) request = fu_engine_request_new(sender); g_autoptr(GVariant) value = NULL; /* if using FWUPD_DBUS_SOCKET... */ if (sender == NULL) { fu_engine_request_set_converter_flags(request, FWUPD_CODEC_FLAG_TRUSTED); return g_object_ref(request); } /* did the client set the list of supported features or any hints */ client = fu_client_list_get_by_sender(self->client_list, sender); if (client != NULL) { const gchar *locale = fu_client_lookup_hint(client, "locale"); if (locale != NULL) fu_engine_request_set_locale(request, locale); fu_engine_request_set_feature_flags(request, fu_client_get_feature_flags(client)); } /* are we root and therefore trusted? */ if (self->proxy_uid == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "org.freedesktop.DBus is not available"); return NULL; } value = g_dbus_proxy_call_sync(self->proxy_uid, "GetConnectionUnixUser", g_variant_new("(s)", sender), G_DBUS_CALL_FLAGS_NONE, 2000, NULL, error); if (value == NULL) { g_prefix_error(error, "failed to read user id of caller: "); return NULL; } g_variant_get(value, "(u)", &calling_uid); if (fu_engine_is_uid_trusted(engine, calling_uid)) converter_flags |= FWUPD_CODEC_FLAG_TRUSTED; fu_engine_request_set_converter_flags(request, converter_flags); /* success */ return g_object_ref(request); } static GVariant * fu_dbus_daemon_device_array_to_variant(FuDbusDaemon *self, FuEngineRequest *request, GPtrArray *devices, GError **error) { FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); FwupdCodecFlags flags = fu_engine_request_get_converter_flags(request); if (fu_engine_config_get_show_device_private(fu_engine_get_config(engine))) flags |= FWUPD_CODEC_FLAG_TRUSTED; return fwupd_codec_array_to_variant(devices, flags); } typedef struct { GDBusMethodInvocation *invocation; FuEngineRequest *request; FuProgress *progress; FuClient *client; glong client_sender_changed_id; GPtrArray *releases; GPtrArray *action_ids; GPtrArray *checksums; GPtrArray *errors; guint64 flags; GInputStream *stream; FuDbusDaemon *self; gchar *device_id; gchar *remote_id; gchar *section; gchar *key; gchar *value; gint32 handle; FuCabinet *cabinet; GHashTable *bios_settings; /* str:str */ gboolean is_fix; } FuMainAuthHelper; static void fu_dbus_daemon_auth_helper_free(FuMainAuthHelper *helper) { /* always return to IDLE even in event of an auth error */ fu_dbus_daemon_set_status(helper->self, FWUPD_STATUS_IDLE); if (helper->cabinet != NULL) g_object_unref(helper->cabinet); if (helper->stream != NULL) g_object_unref(helper->stream); if (helper->request != NULL) g_object_unref(helper->request); if (helper->progress != NULL) g_object_unref(helper->progress); if (helper->releases != NULL) g_ptr_array_unref(helper->releases); if (helper->action_ids != NULL) g_ptr_array_unref(helper->action_ids); if (helper->checksums != NULL) g_ptr_array_unref(helper->checksums); if (helper->errors != NULL) g_ptr_array_unref(helper->errors); if (helper->client_sender_changed_id > 0) g_signal_handler_disconnect(helper->client, helper->client_sender_changed_id); if (helper->client != NULL) g_object_unref(helper->client); g_free(helper->device_id); g_free(helper->remote_id); g_free(helper->section); g_free(helper->key); g_free(helper->value); g_object_unref(helper->invocation); if (helper->bios_settings != NULL) g_hash_table_unref(helper->bios_settings); g_free(helper); } static void fu_dbus_daemon_method_invocation_return_gerror(GDBusMethodInvocation *invocation, GError *error) { fu_error_convert(&error); g_dbus_method_invocation_return_gerror(invocation, error); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuMainAuthHelper, fu_dbus_daemon_auth_helper_free) #pragma clang diagnostic pop static void fu_dbus_daemon_authorize_unlock_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(FuMainAuthHelper) helper = (FuMainAuthHelper *)user_data; g_autoptr(GError) error = NULL; FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(helper->self)); /* get result */ if (!fu_polkit_authority_check_finish(FU_POLKIT_AUTHORITY(source), res, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* authenticated */ if (!fu_engine_unlock(engine, helper->device_id, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* success */ g_dbus_method_invocation_return_value(helper->invocation, NULL); } static void fu_dbus_daemon_authorize_get_bios_settings_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(FuMainAuthHelper) helper = (FuMainAuthHelper *)user_data; g_autoptr(GError) error = NULL; g_autoptr(FuBiosSettings) attrs = NULL; FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(helper->self)); FuContext *ctx = fu_engine_get_context(engine); GVariant *val = NULL; /* get result */ if (!fu_polkit_authority_check_finish(FU_POLKIT_AUTHORITY(source), res, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* authenticated */ attrs = fu_context_get_bios_settings(ctx); val = fwupd_codec_to_variant(FWUPD_CODEC(attrs), FWUPD_CODEC_FLAG_TRUSTED); g_dbus_method_invocation_return_value(helper->invocation, val); } static void fu_dbus_daemon_authorize_set_bios_settings_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(FuMainAuthHelper) helper = (FuMainAuthHelper *)user_data; g_autoptr(GError) error = NULL; FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(helper->self)); /* get result */ if (!fu_polkit_authority_check_finish(FU_POLKIT_AUTHORITY(source), res, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* authenticated */ if (!fu_engine_modify_bios_settings(engine, helper->bios_settings, FALSE, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* success */ g_dbus_method_invocation_return_value(helper->invocation, NULL); } static void fu_dbus_daemon_authorize_set_approved_firmware_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(FuMainAuthHelper) helper = (FuMainAuthHelper *)user_data; g_autoptr(GError) error = NULL; FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(helper->self)); /* get result */ if (!fu_polkit_authority_check_finish(FU_POLKIT_AUTHORITY(source), res, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* success */ for (guint i = 0; i < helper->checksums->len; i++) { const gchar *csum = g_ptr_array_index(helper->checksums, i); fu_engine_add_approved_firmware(engine, csum); } g_dbus_method_invocation_return_value(helper->invocation, NULL); } static void fu_dbus_daemon_authorize_set_blocked_firmware_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(FuMainAuthHelper) helper = (FuMainAuthHelper *)user_data; g_autoptr(GError) error = NULL; FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(helper->self)); /* get result */ if (!fu_polkit_authority_check_finish(FU_POLKIT_AUTHORITY(source), res, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* success */ if (!fu_engine_set_blocked_firmware(engine, helper->checksums, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } g_dbus_method_invocation_return_value(helper->invocation, NULL); } static void fu_dbus_daemon_authorize_fix_host_security_attr_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(FuMainAuthHelper) helper = (FuMainAuthHelper *)user_data; g_autoptr(GError) error = NULL; FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(helper->self)); /* get result */ if (!fu_polkit_authority_check_finish(FU_POLKIT_AUTHORITY(source), res, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* success */ if (!fu_engine_fix_host_security_attr(engine, helper->key, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } g_dbus_method_invocation_return_value(helper->invocation, NULL); } static void fu_dbus_daemon_authorize_undo_host_security_attr_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(FuMainAuthHelper) helper = (FuMainAuthHelper *)user_data; g_autoptr(GError) error = NULL; FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(helper->self)); /* get result */ if (!fu_polkit_authority_check_finish(FU_POLKIT_AUTHORITY(source), res, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* success */ if (!fu_engine_undo_host_security_attr(engine, helper->key, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } g_dbus_method_invocation_return_value(helper->invocation, NULL); } static void fu_dbus_daemon_authorize_self_sign_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(FuMainAuthHelper) helper = (FuMainAuthHelper *)user_data; g_autofree gchar *sig = NULL; g_autoptr(GError) error = NULL; FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(helper->self)); /* get result */ if (!fu_polkit_authority_check_finish(FU_POLKIT_AUTHORITY(source), res, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* authenticated */ sig = fu_engine_self_sign(engine, helper->value, helper->flags, &error); if (sig == NULL) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* success */ g_dbus_method_invocation_return_value(helper->invocation, g_variant_new("(s)", sig)); } static void fu_dbus_daemon_modify_config_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(FuMainAuthHelper) helper = (FuMainAuthHelper *)user_data; g_autoptr(GError) error = NULL; FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(helper->self)); /* get result */ if (!fu_polkit_authority_check_finish(FU_POLKIT_AUTHORITY(source), res, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } if (!fu_engine_modify_config(engine, helper->section, helper->key, helper->value, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* success */ g_dbus_method_invocation_return_value(helper->invocation, NULL); } static void fu_dbus_daemon_reset_config_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(FuMainAuthHelper) helper = (FuMainAuthHelper *)user_data; g_autoptr(GError) error = NULL; FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(helper->self)); /* get result */ if (!fu_polkit_authority_check_finish(FU_POLKIT_AUTHORITY(source), res, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } if (!fu_engine_reset_config(engine, helper->section, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* success */ g_dbus_method_invocation_return_value(helper->invocation, NULL); } static void fu_dbus_daemon_progress_percentage_changed_cb(FuProgress *progress, guint percentage, FuDbusDaemon *self) { /* sanity check */ if (self->percentage == percentage) return; self->percentage = percentage; g_debug("Emitting PropertyChanged('Percentage'='%u%%')", percentage); fu_dbus_daemon_emit_property_changed(self, "Percentage", g_variant_new_uint32(percentage)); } static void fu_dbus_daemon_progress_status_changed_cb(FuProgress *progress, FwupdStatus status, FuDbusDaemon *self) { fu_dbus_daemon_set_status(self, status); } static void fu_dbus_daemon_authorize_activate_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(FuMainAuthHelper) helper = (FuMainAuthHelper *)user_data; g_autoptr(GError) error = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(helper->self)); /* get result */ if (!fu_polkit_authority_check_finish(FU_POLKIT_AUTHORITY(source), res, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* progress */ fu_progress_set_profile(progress, g_getenv("FWUPD_VERBOSE") != NULL); g_signal_connect(FU_PROGRESS(progress), "percentage-changed", G_CALLBACK(fu_dbus_daemon_progress_percentage_changed_cb), helper->self); g_signal_connect(FU_PROGRESS(progress), "status-changed", G_CALLBACK(fu_dbus_daemon_progress_status_changed_cb), helper->self); /* authenticated */ if (!fu_engine_activate(engine, helper->device_id, progress, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* success */ g_dbus_method_invocation_return_value(helper->invocation, NULL); } static void fu_dbus_daemon_authorize_verify_update_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(FuMainAuthHelper) helper = (FuMainAuthHelper *)user_data; g_autoptr(GError) error = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(helper->self)); /* get result */ if (!fu_polkit_authority_check_finish(FU_POLKIT_AUTHORITY(source), res, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* progress */ fu_progress_set_profile(progress, g_getenv("FWUPD_VERBOSE") != NULL); g_signal_connect(FU_PROGRESS(progress), "percentage-changed", G_CALLBACK(fu_dbus_daemon_progress_percentage_changed_cb), helper->self); g_signal_connect(FU_PROGRESS(progress), "status-changed", G_CALLBACK(fu_dbus_daemon_progress_status_changed_cb), helper->self); /* authenticated */ if (!fu_engine_verify_update(engine, helper->device_id, progress, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* success */ g_dbus_method_invocation_return_value(helper->invocation, NULL); } static void fu_dbus_daemon_authorize_modify_remote_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(FuMainAuthHelper) helper = (FuMainAuthHelper *)user_data; g_autoptr(GError) error = NULL; FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(helper->self)); /* get result */ if (!fu_polkit_authority_check_finish(FU_POLKIT_AUTHORITY(source), res, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* authenticated */ if (!fu_engine_modify_remote(engine, helper->remote_id, helper->key, helper->value, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* success */ g_dbus_method_invocation_return_value(helper->invocation, NULL); } static FuPolkitAuthorityCheckFlags fu_dbus_daemon_engine_request_get_authority_check_flags(FuEngineRequest *request) { FuPolkitAuthorityCheckFlags auth_flags = FU_POLKIT_AUTHORITY_CHECK_FLAG_ALLOW_USER_INTERACTION; if (fu_engine_request_has_converter_flag(request, FWUPD_CODEC_FLAG_TRUSTED)) auth_flags |= FU_POLKIT_AUTHORITY_CHECK_FLAG_USER_IS_TRUSTED; return auth_flags; } #ifdef HAVE_GIO_UNIX static void fu_dbus_daemon_authorize_install_queue(FuMainAuthHelper *helper); static void fu_dbus_daemon_authorize_install_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(FuMainAuthHelper) helper = (FuMainAuthHelper *)user_data; g_autoptr(GError) error = NULL; /* get result */ if (!fu_polkit_authority_check_finish(FU_POLKIT_AUTHORITY(source), res, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* do the next authentication action ID */ fu_dbus_daemon_authorize_install_queue(g_steal_pointer(&helper)); } static void fu_dbus_daemon_authorize_install_queue(FuMainAuthHelper *helper_ref) { FuDbusDaemon *self = helper_ref->self; g_autoptr(FuMainAuthHelper) helper = helper_ref; g_autoptr(GError) error = NULL; gboolean ret; FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(helper->self)); /* still more things to to authenticate */ if (helper->action_ids->len > 0) { g_autofree gchar *action_id = g_strdup(g_ptr_array_index(helper->action_ids, 0)); g_autofree gchar *sender = g_strdup(fu_client_get_sender(helper->client)); g_autoptr(FuEngineRequest) request = g_object_ref(helper->request); g_ptr_array_remove_index(helper->action_ids, 0); fu_polkit_authority_check( self->authority, sender, action_id, fu_dbus_daemon_engine_request_get_authority_check_flags(request), NULL, fu_dbus_daemon_authorize_install_cb, g_steal_pointer(&helper)); return; } /* all authenticated, so install all the things */ fu_progress_set_profile(helper->progress, g_getenv("FWUPD_VERBOSE") != NULL); g_signal_connect(FU_PROGRESS(helper->progress), "percentage-changed", G_CALLBACK(fu_dbus_daemon_progress_percentage_changed_cb), helper->self); g_signal_connect(FU_PROGRESS(helper->progress), "status-changed", G_CALLBACK(fu_dbus_daemon_progress_status_changed_cb), helper->self); /* all authenticated, so install all the things */ fu_daemon_set_update_in_progress(FU_DAEMON(self), TRUE); ret = fu_engine_install_releases(engine, helper->request, helper->releases, helper->cabinet, helper->progress, helper->flags, &error); fu_daemon_set_update_in_progress(FU_DAEMON(self), FALSE); if (fu_daemon_get_pending_stop(FU_DAEMON(self))) { g_set_error_literal(&error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "daemon was stopped"); fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } if (!ret) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* success */ g_dbus_method_invocation_return_value(helper->invocation, NULL); } #endif /* HAVE_GIO_UNIX */ #ifdef HAVE_GIO_UNIX static gint fu_dbus_daemon_release_sort_cb(gconstpointer a, gconstpointer b) { FuRelease *release1 = *((FuRelease **)a); FuRelease *release2 = *((FuRelease **)b); return fu_release_compare(release1, release2); } static gboolean fu_dbus_daemon_install_with_helper_device(FuMainAuthHelper *helper, XbNode *component, FuDevice *device, GError **error) { FuDbusDaemon *self = helper->self; FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error_local = NULL; g_autoptr(GPtrArray) releases = NULL; /* is this component valid for the device */ fu_release_set_device(release, device); fu_release_set_request(release, helper->request); if (helper->remote_id != NULL) { fu_release_set_remote(release, fu_engine_get_remote_by_id(engine, helper->remote_id, NULL)); } if (!fu_release_load(release, helper->cabinet, component, NULL, helper->flags | FWUPD_INSTALL_FLAG_FORCE, &error_local)) { g_ptr_array_add(helper->errors, g_steal_pointer(&error_local)); return TRUE; } if (!fu_engine_requirements_check(engine, release, helper->flags | FWUPD_INSTALL_FLAG_IGNORE_REQUIREMENTS, &error_local)) { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_debug("first pass requirement on %s:%s failed: %s", fu_device_get_id(device), xb_node_query_text(component, "id", NULL), error_local->message); } g_ptr_array_add(helper->errors, g_steal_pointer(&error_local)); return TRUE; } /* sync update message from CAB */ fu_device_ensure_from_component(device, component); fu_device_incorporate_from_component(device, component); /* post-ensure checks */ if (!fu_release_check_version(release, component, helper->flags, &error_local)) { g_ptr_array_add(helper->errors, g_steal_pointer(&error_local)); return TRUE; } /* install each intermediate release */ releases = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_INSTALL_ALL_RELEASES)) { g_autoptr(GPtrArray) rels = NULL; g_autoptr(XbQuery) query = NULL; /* we get this one "for free" */ g_ptr_array_add(releases, g_object_ref(release)); query = xb_query_new_full(xb_node_get_silo(component), "releases/release", XB_QUERY_FLAG_FORCE_NODE_CACHE, error); if (query == NULL) return FALSE; rels = xb_node_query_full(component, query, NULL); /* add all but the first entry */ for (guint i = 1; i < rels->len; i++) { XbNode *rel = g_ptr_array_index(rels, i); g_autoptr(FuRelease) release2 = fu_release_new(); g_autoptr(GError) error_loop = NULL; fu_release_set_device(release2, device); fu_release_set_request(release2, helper->request); if (!fu_release_load(release2, helper->cabinet, component, rel, helper->flags, &error_loop)) { g_ptr_array_add(helper->errors, g_steal_pointer(&error_loop)); continue; } g_ptr_array_add(releases, g_object_ref(release2)); } } else { g_ptr_array_add(releases, g_object_ref(release)); } /* make a second pass */ for (guint i = 0; i < releases->len; i++) { FuRelease *release_tmp = g_ptr_array_index(releases, i); if (!fu_engine_requirements_check(engine, release_tmp, helper->flags, &error_local)) { g_debug("second pass requirement on %s:%s failed: %s", fu_device_get_id(device), xb_node_query_text(component, "id", NULL), error_local->message); g_ptr_array_add(helper->errors, g_steal_pointer(&error_local)); continue; } if (!fu_engine_check_trust(engine, release_tmp, &error_local)) { g_ptr_array_add(helper->errors, g_steal_pointer(&error_local)); continue; } /* get the action IDs for the valid device */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { const gchar *action_id = fu_release_get_action_id(release_tmp); if (!g_ptr_array_find(helper->action_ids, action_id, NULL)) g_ptr_array_add(helper->action_ids, g_strdup(action_id)); } g_ptr_array_add(helper->releases, g_object_ref(release_tmp)); } /* success */ return TRUE; } static gboolean fu_dbus_daemon_install_with_helper(FuMainAuthHelper *helper_ref, GError **error) { FuDbusDaemon *self = helper_ref->self; g_autoptr(FuMainAuthHelper) helper = helper_ref; g_autoptr(GPtrArray) components = NULL; g_autoptr(GPtrArray) devices_possible = NULL; FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(helper->self)); /* get a list of devices that in some way match the device_id */ if (g_strcmp0(helper->device_id, FWUPD_DEVICE_ID_ANY) == 0) { devices_possible = fu_engine_get_devices(engine, error); if (devices_possible == NULL) return FALSE; } else { g_autoptr(FuDevice) device = NULL; device = fu_engine_get_device(engine, helper->device_id, error); if (device == NULL) return FALSE; devices_possible = fu_engine_get_devices_by_composite_id(engine, fu_device_get_composite_id(device), error); if (devices_possible == NULL) return FALSE; } /* parse silo */ helper->cabinet = fu_engine_build_cabinet_from_stream(engine, helper->stream, error); if (helper->cabinet == NULL) return FALSE; /* for each component in the silo */ components = fu_cabinet_get_components(helper->cabinet, error); if (components == NULL) return FALSE; helper->action_ids = g_ptr_array_new_with_free_func(g_free); helper->releases = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); helper->errors = g_ptr_array_new_with_free_func((GDestroyNotify)g_error_free); helper->remote_id = fu_engine_get_remote_id_for_stream(engine, helper->stream); /* do any devices pass the requirements */ for (guint i = 0; i < components->len; i++) { XbNode *component = g_ptr_array_index(components, i); for (guint j = 0; j < devices_possible->len; j++) { FuDevice *device = g_ptr_array_index(devices_possible, j); /* emulating */ if ((helper->flags & FWUPD_INSTALL_FLAG_ONLY_EMULATED) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { g_debug("skipping non-emulated %s", fu_device_get_id(device)); continue; } g_debug("testing device %u [%s] with component %u", j, fu_device_get_id(device), i); if (!fu_dbus_daemon_install_with_helper_device(helper, component, device, error)) return FALSE; } } /* order the install tasks by the device priority */ g_ptr_array_sort(helper->releases, fu_dbus_daemon_release_sort_cb); /* nothing suitable */ if (helper->releases->len == 0) { GError *error_tmp = fu_engine_error_array_get_best(helper->errors); g_propagate_error(error, error_tmp); return FALSE; } /* authenticate all things in the action_ids */ fu_dbus_daemon_set_status(self, FWUPD_STATUS_WAITING_FOR_AUTH); fu_dbus_daemon_authorize_install_queue(g_steal_pointer(&helper)); return TRUE; } #endif /* HAVE_GIO_UNIX */ static gboolean fu_dbus_daemon_device_id_valid(const gchar *device_id, GError **error) { if (g_strcmp0(device_id, FWUPD_DEVICE_ID_ANY) == 0) return TRUE; if (device_id != NULL && strlen(device_id) >= 4) return TRUE; g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid device ID: %s", device_id); return FALSE; } typedef struct { gchar *id; gchar *sender; guint watcher_id; } FuDbusDaemonSystemInhibit; static void fu_dbus_daemon_system_inhibit_free(FuDbusDaemonSystemInhibit *inhibit) { g_bus_unwatch_name(inhibit->watcher_id); g_free(inhibit->id); g_free(inhibit->sender); g_free(inhibit); } static void fu_dbus_daemon_ensure_system_inhibit(FuDbusDaemon *self) { FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); FuContext *ctx = fu_engine_get_context(engine); if (self->system_inhibits->len > 0) { fu_context_add_flag(ctx, FU_CONTEXT_FLAG_SYSTEM_INHIBIT); return; } fu_context_remove_flag(ctx, FU_CONTEXT_FLAG_SYSTEM_INHIBIT); } static void fu_dbus_daemon_inhibit_name_vanished_cb(GDBusConnection *connection, const gchar *name, gpointer user_data) { FuDbusDaemon *self = FU_DBUS_DAEMON(user_data); for (guint i = 0; i < self->system_inhibits->len; i++) { FuDbusDaemonSystemInhibit *inhibit = g_ptr_array_index(self->system_inhibits, i); if (g_strcmp0(inhibit->sender, name) == 0) { g_debug("removing %s as %s vanished without calling Uninhibit", inhibit->id, name); g_ptr_array_remove_index(self->system_inhibits, i); fu_dbus_daemon_ensure_system_inhibit(self); break; } } } #ifdef HAVE_GIO_UNIX static void fu_dbus_daemon_client_flags_notify_cb(FuClient *client, GParamSpec *pspec, FuMainAuthHelper *helper) { if (!fu_client_has_flag(client, FU_CLIENT_FLAG_ACTIVE)) { g_info("%s vanished before completion of install on %s", fu_client_get_sender(client), helper->device_id); fu_progress_add_flag(helper->progress, FU_PROGRESS_FLAG_NO_SENDER); } } #endif static GInputStream * fu_dbus_daemon_invocation_get_input_stream(GDBusMethodInvocation *invocation, GError **error) { #ifdef HAVE_GIO_UNIX GDBusMessage *message; GUnixFDList *fd_list; gint fd; g_autoptr(GInputStream) stream = NULL; /* get the fd */ message = g_dbus_method_invocation_get_message(invocation); fd_list = g_dbus_message_get_unix_fd_list(message); if (fd_list == NULL || g_unix_fd_list_get_length(fd_list) != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid handle"); return NULL; } fd = g_unix_fd_list_get(fd_list, 0, error); if (fd < 0) return NULL; /* get details about the file (will close the fd when done) */ stream = fu_unix_seekable_input_stream_new(fd, TRUE); if (stream == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid stream"); return NULL; } return g_steal_pointer(&stream); #else g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unsupported feature"); return NULL; #endif } static GOutputStream * fu_dbus_daemon_invocation_get_output_stream(GDBusMethodInvocation *invocation, GError **error) { #ifdef HAVE_GIO_UNIX GDBusMessage *message; GUnixFDList *fd_list; gint fd; g_autoptr(GOutputStream) stream = NULL; /* get the fd */ message = g_dbus_method_invocation_get_message(invocation); fd_list = g_dbus_message_get_unix_fd_list(message); if (fd_list == NULL || g_unix_fd_list_get_length(fd_list) != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid handle"); return NULL; } fd = g_unix_fd_list_get(fd_list, 0, error); if (fd < 0) return NULL; /* get details about the file (will close the fd when done) */ stream = g_unix_output_stream_new(fd, TRUE); if (stream == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid stream"); return NULL; } return g_steal_pointer(&stream); #else g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unsupported feature"); return NULL; #endif } static gboolean fu_dbus_daemon_hsi_supported(FuDbusDaemon *self, GError **error) { #ifdef HAVE_HSI g_autofree gchar *sysfsfwdir = NULL; g_autofree gchar *xen_privileged_fn = NULL; if (g_getenv("UMOCKDEV_DIR") != NULL) return TRUE; if (fu_daemon_get_machine_kind(FU_DAEMON(self)) == FU_DAEMON_MACHINE_KIND_PHYSICAL) return TRUE; sysfsfwdir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR_FW_ATTRIB); /* privileged xen can access most hardware */ xen_privileged_fn = g_build_filename(sysfsfwdir, "hypervisor", "start_flags", "privileged", NULL); if (g_file_test(xen_privileged_fn, G_FILE_TEST_EXISTS)) { g_autofree gchar *contents = NULL; if (g_file_get_contents(xen_privileged_fn, &contents, NULL, NULL)) { if (g_strcmp0(contents, "1") == 0) return TRUE; } } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "HSI unavailable for hypervisor"); #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "HSI support not enabled"); #endif return FALSE; } static void fu_dbus_daemon_method_get_devices(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); GVariant *val; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) devices = NULL; devices = fu_engine_get_devices(engine, &error); if (devices == NULL) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } val = fu_dbus_daemon_device_array_to_variant(self, request, devices, &error); if (val == NULL) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } g_dbus_method_invocation_return_value(invocation, val); } static void fu_dbus_daemon_method_get_plugins(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); GVariant *val; val = fwupd_codec_array_to_variant(fu_engine_get_plugins(engine), FWUPD_CODEC_FLAG_NONE); g_dbus_method_invocation_return_value(invocation, val); } static void fu_dbus_daemon_method_get_releases(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); const gchar *device_id; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) releases = NULL; g_variant_get(parameters, "(&s)", &device_id); if (!fu_dbus_daemon_device_id_valid(device_id, &error)) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } releases = fu_engine_get_releases(engine, request, device_id, &error); if (releases == NULL) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } g_dbus_method_invocation_return_value( invocation, fwupd_codec_array_to_variant(releases, FWUPD_CODEC_FLAG_NONE)); } static void fu_dbus_daemon_method_get_approved_firmware(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); GPtrArray *checksums = fu_engine_get_approved_firmware(engine); GVariantBuilder builder; GVariant *val; g_variant_builder_init(&builder, G_VARIANT_TYPE("as")); for (guint i = 0; i < checksums->len; i++) { const gchar *checksum = g_ptr_array_index(checksums, i); g_variant_builder_add_value(&builder, g_variant_new_string(checksum)); } val = g_variant_builder_end(&builder); g_dbus_method_invocation_return_value(invocation, g_variant_new_tuple(&val, 1)); } static void fu_dbus_daemon_method_get_blocked_firmware(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); GPtrArray *checksums = fu_engine_get_blocked_firmware(engine); GVariantBuilder builder; GVariant *val; g_variant_builder_init(&builder, G_VARIANT_TYPE("as")); for (guint i = 0; i < checksums->len; i++) { const gchar *checksum = g_ptr_array_index(checksums, i); g_variant_builder_add_value(&builder, g_variant_new_string(checksum)); } val = g_variant_builder_end(&builder); g_dbus_method_invocation_return_value(invocation, g_variant_new_tuple(&val, 1)); } static void fu_dbus_daemon_method_get_report_metadata(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); const gchar *key; const gchar *value; GHashTableIter iter; GVariantBuilder builder; GVariant *val; g_autoptr(GError) error = NULL; g_autoptr(GHashTable) metadata = NULL; metadata = fu_engine_get_report_metadata(engine, &error); if (metadata == NULL) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } g_variant_builder_init(&builder, G_VARIANT_TYPE("a{ss}")); g_hash_table_iter_init(&iter, metadata); while (g_hash_table_iter_next(&iter, (gpointer *)&key, (gpointer *)&value)) { g_variant_builder_add_value(&builder, g_variant_new("{ss}", key, value)); } val = g_variant_builder_end(&builder); g_dbus_method_invocation_return_value(invocation, g_variant_new_tuple(&val, 1)); } static void fu_dbus_daemon_method_set_approved_firmware(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { g_autofree gchar *checksums_str = NULL; g_auto(GStrv) checksums = NULL; g_autoptr(FuMainAuthHelper) helper = NULL; g_variant_get(parameters, "(^as)", &checksums); checksums_str = g_strjoinv(",", checksums); /* authenticate */ fu_dbus_daemon_set_status(self, FWUPD_STATUS_WAITING_FOR_AUTH); helper = g_new0(FuMainAuthHelper, 1); helper->self = self; helper->flags = FU_FIRMWARE_PARSE_FLAG_NO_SEARCH; helper->request = g_object_ref(request); helper->invocation = g_object_ref(invocation); helper->checksums = g_ptr_array_new_with_free_func(g_free); for (guint i = 0; checksums[i] != NULL; i++) g_ptr_array_add(helper->checksums, g_strdup(checksums[i])); fu_polkit_authority_check(self->authority, fu_engine_request_get_sender(request), "org.freedesktop.fwupd.set-approved-firmware", fu_dbus_daemon_engine_request_get_authority_check_flags(request), NULL, fu_dbus_daemon_authorize_set_approved_firmware_cb, g_steal_pointer(&helper)); } static void fu_dbus_daemon_method_set_blocked_firmware(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { g_autofree gchar *checksums_str = NULL; g_auto(GStrv) checksums = NULL; g_autoptr(FuMainAuthHelper) helper = NULL; g_variant_get(parameters, "(^as)", &checksums); checksums_str = g_strjoinv(",", checksums); /* authenticate */ fu_dbus_daemon_set_status(self, FWUPD_STATUS_WAITING_FOR_AUTH); helper = g_new0(FuMainAuthHelper, 1); helper->self = self; helper->request = g_object_ref(request); helper->invocation = g_object_ref(invocation); helper->checksums = g_ptr_array_new_with_free_func(g_free); for (guint i = 0; checksums[i] != NULL; i++) g_ptr_array_add(helper->checksums, g_strdup(checksums[i])); fu_polkit_authority_check(self->authority, fu_engine_request_get_sender(request), "org.freedesktop.fwupd.set-approved-firmware", fu_dbus_daemon_engine_request_get_authority_check_flags(request), NULL, fu_dbus_daemon_authorize_set_blocked_firmware_cb, g_steal_pointer(&helper)); } static void fu_dbus_daemon_authorize_quit_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(FuMainAuthHelper) helper = (FuMainAuthHelper *)user_data; g_autoptr(GError) error = NULL; /* get result */ if (!fu_polkit_authority_check_finish(FU_POLKIT_AUTHORITY(source), res, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* success */ fu_daemon_schedule_process_quit(FU_DAEMON(helper->self)); g_dbus_method_invocation_return_value(helper->invocation, NULL); } static void fu_dbus_daemon_method_quit(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { g_autoptr(FuMainAuthHelper) helper = NULL; /* is root */ if (fu_engine_request_has_converter_flag(request, FWUPD_CODEC_FLAG_TRUSTED)) { fu_daemon_schedule_process_quit(FU_DAEMON(self)); g_dbus_method_invocation_return_value(invocation, NULL); return; } /* authenticate */ fu_dbus_daemon_set_status(self, FWUPD_STATUS_WAITING_FOR_AUTH); helper = g_new0(FuMainAuthHelper, 1); helper->self = self; helper->request = g_object_ref(request); helper->invocation = g_object_ref(invocation); fu_polkit_authority_check(self->authority, fu_engine_request_get_sender(request), "org.freedesktop.fwupd.quit", fu_dbus_daemon_engine_request_get_authority_check_flags(request), NULL, fu_dbus_daemon_authorize_quit_cb, g_steal_pointer(&helper)); } static void fu_dbus_daemon_method_self_sign(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { GVariant *prop_value; const gchar *prop_key; g_autofree gchar *value = NULL; g_autoptr(FuMainAuthHelper) helper = NULL; g_autoptr(GVariantIter) iter = NULL; g_variant_get(parameters, "(sa{sv})", &value, &iter); /* get flags */ helper = g_new0(FuMainAuthHelper, 1); while (g_variant_iter_next(iter, "{&sv}", &prop_key, &prop_value)) { g_debug("got option %s", prop_key); if (g_strcmp0(prop_key, "add-timestamp") == 0 && g_variant_get_boolean(prop_value) == TRUE) helper->flags |= JCAT_SIGN_FLAG_ADD_TIMESTAMP; if (g_strcmp0(prop_key, "add-cert") == 0 && g_variant_get_boolean(prop_value) == TRUE) helper->flags |= JCAT_SIGN_FLAG_ADD_CERT; g_variant_unref(prop_value); } /* authenticate */ fu_dbus_daemon_set_status(self, FWUPD_STATUS_WAITING_FOR_AUTH); helper->self = self; helper->value = g_steal_pointer(&value); helper->request = g_object_ref(request); helper->invocation = g_object_ref(invocation); fu_polkit_authority_check(self->authority, fu_engine_request_get_sender(request), "org.freedesktop.fwupd.self-sign", fu_dbus_daemon_engine_request_get_authority_check_flags(request), NULL, fu_dbus_daemon_authorize_self_sign_cb, g_steal_pointer(&helper)); } static void fu_dbus_daemon_method_get_downgrades(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); const gchar *device_id; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) releases = NULL; g_variant_get(parameters, "(&s)", &device_id); if (!fu_dbus_daemon_device_id_valid(device_id, &error)) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } releases = fu_engine_get_downgrades(engine, request, device_id, &error); if (releases == NULL) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } g_dbus_method_invocation_return_value( invocation, fwupd_codec_array_to_variant(releases, FWUPD_CODEC_FLAG_NONE)); } static void fu_dbus_daemon_method_get_upgrades(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); const gchar *device_id; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) releases = NULL; g_variant_get(parameters, "(&s)", &device_id); if (!fu_dbus_daemon_device_id_valid(device_id, &error)) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } releases = fu_engine_get_upgrades(engine, request, device_id, &error); if (releases == NULL) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } g_dbus_method_invocation_return_value( invocation, fwupd_codec_array_to_variant(releases, FWUPD_CODEC_FLAG_NONE)); } static void fu_dbus_daemon_method_get_remotes(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) remotes = NULL; remotes = fu_engine_get_remotes(engine, &error); if (remotes == NULL) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } g_dbus_method_invocation_return_value( invocation, fwupd_codec_array_to_variant(remotes, FWUPD_CODEC_FLAG_NONE)); } static void fu_dbus_daemon_method_get_history(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); GVariant *val; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) devices = NULL; devices = fu_engine_get_history(engine, &error); if (devices == NULL) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } val = fu_dbus_daemon_device_array_to_variant(self, request, devices, &error); if (val == NULL) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } g_dbus_method_invocation_return_value(invocation, val); } static void fu_dbus_daemon_method_get_host_security_attrs(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); g_autoptr(GError) error = NULL; g_autoptr(FuSecurityAttrs) attrs = NULL; if (!fu_dbus_daemon_hsi_supported(self, &error)) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } attrs = fu_engine_get_host_security_attrs(engine); g_dbus_method_invocation_return_value(invocation, fu_security_attrs_to_variant(attrs)); } static void fu_dbus_daemon_method_get_host_security_events(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { #ifdef HAVE_HSI FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); guint limit = 0; g_autoptr(FuSecurityAttrs) attrs = NULL; g_autoptr(GError) error = NULL; g_variant_get(parameters, "(u)", &limit); attrs = fu_engine_get_host_security_events(engine, limit, &error); if (attrs == NULL) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } g_dbus_method_invocation_return_value(invocation, fu_security_attrs_to_variant(attrs)); #else g_dbus_method_invocation_return_error_literal(invocation, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "HSI support not enabled"); #endif } static void fu_dbus_daemon_method_clear_results(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); const gchar *device_id; g_autoptr(GError) error = NULL; g_variant_get(parameters, "(&s)", &device_id); if (!fu_engine_clear_results(engine, device_id, &error)) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } g_dbus_method_invocation_return_value(invocation, NULL); } static void fu_dbus_daemon_authorize_emulation_load_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(FuMainAuthHelper) helper = (FuMainAuthHelper *)user_data; g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = NULL; FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(helper->self)); /* get result */ if (!fu_polkit_authority_check_finish(FU_POLKIT_AUTHORITY(source), res, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* get stream */ stream = fu_dbus_daemon_invocation_get_input_stream(helper->invocation, &error); if (stream == NULL) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* load data into engine */ if (!fu_engine_emulation_load(engine, stream, &error)) { g_dbus_method_invocation_return_error(helper->invocation, error->domain, error->code, "failed to load emulation data: %s", error->message); return; } /* success */ g_dbus_method_invocation_return_value(helper->invocation, NULL); } static void fu_dbus_daemon_method_emulation_load(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { gint32 fd_handle = 0; g_autoptr(FuMainAuthHelper) helper = NULL; g_variant_get(parameters, "(h)", &fd_handle); fu_dbus_daemon_set_status(self, FWUPD_STATUS_WAITING_FOR_AUTH); helper = g_new0(FuMainAuthHelper, 1); helper->self = self; helper->request = g_object_ref(request); helper->invocation = g_object_ref(invocation); helper->handle = fd_handle; fu_polkit_authority_check(self->authority, fu_engine_request_get_sender(request), "org.freedesktop.fwupd.emulation-load", fu_dbus_daemon_engine_request_get_authority_check_flags(request), NULL, fu_dbus_daemon_authorize_emulation_load_cb, g_steal_pointer(&helper)); } static void fu_dbus_daemon_authorize_emulation_save_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(FuMainAuthHelper) helper = (FuMainAuthHelper *)user_data; g_autoptr(GError) error = NULL; g_autoptr(GOutputStream) stream = NULL; FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(helper->self)); /* get result */ if (!fu_polkit_authority_check_finish(FU_POLKIT_AUTHORITY(source), res, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* get stream */ stream = fu_dbus_daemon_invocation_get_output_stream(helper->invocation, &error); if (stream == NULL) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* save data from engine */ if (!fu_engine_emulation_save(engine, stream, &error)) { fu_dbus_daemon_method_invocation_return_gerror(helper->invocation, error); return; } /* success */ g_dbus_method_invocation_return_value(helper->invocation, NULL); } static void fu_dbus_daemon_method_emulation_save(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { gint32 fd_handle = 0; g_autoptr(FuMainAuthHelper) helper = NULL; g_variant_get(parameters, "(h)", &fd_handle); fu_dbus_daemon_set_status(self, FWUPD_STATUS_WAITING_FOR_AUTH); helper = g_new0(FuMainAuthHelper, 1); helper->self = self; helper->request = g_object_ref(request); helper->invocation = g_object_ref(invocation); helper->handle = fd_handle; fu_polkit_authority_check(self->authority, fu_engine_request_get_sender(request), "org.freedesktop.fwupd.emulation-save", fu_dbus_daemon_engine_request_get_authority_check_flags(request), NULL, fu_dbus_daemon_authorize_emulation_save_cb, g_steal_pointer(&helper)); } static void fu_dbus_daemon_authorize_modify_device_internal(FuEngine *engine, const gchar *device_id, const gchar *key, const gchar *value, GDBusMethodInvocation *invocation) { g_autoptr(GError) error = NULL; if (!fu_engine_modify_device(engine, device_id, key, value, &error)) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } g_dbus_method_invocation_return_value(invocation, NULL); } static void fu_dbus_daemon_authorize_modify_device_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(FuMainAuthHelper) helper = (FuMainAuthHelper *)user_data; fu_dbus_daemon_authorize_modify_device_internal( fu_daemon_get_engine(FU_DAEMON(helper->self)), helper->device_id, helper->key, helper->value, helper->invocation); } static gboolean fu_dbus_daemon_method_modify_device_flag_needs_auth(const gchar *key, const gchar *value) { if (g_strcmp0(key, "Flags") != 0) return FALSE; if (g_strcmp0(value, "emulation-tag") == 0) return TRUE; if (g_strcmp0(value, "~emulation-tag") == 0) return TRUE; return FALSE; } static void fu_dbus_daemon_method_modify_device(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { const gchar *device_id; const gchar *key; const gchar *value; g_variant_get(parameters, "(&s&s&s)", &device_id, &key, &value); if (fu_dbus_daemon_method_modify_device_flag_needs_auth(key, value)) { g_autoptr(FuMainAuthHelper) helper = g_new0(FuMainAuthHelper, 1); helper->self = self; helper->request = g_object_ref(request); helper->invocation = g_object_ref(invocation); helper->device_id = g_strdup(device_id); helper->key = g_strdup(key); helper->value = g_strdup(value); fu_dbus_daemon_set_status(self, FWUPD_STATUS_WAITING_FOR_AUTH); fu_polkit_authority_check( self->authority, fu_engine_request_get_sender(request), "org.freedesktop.fwupd.emulation-tag", fu_dbus_daemon_engine_request_get_authority_check_flags(request), NULL, fu_dbus_daemon_authorize_modify_device_cb, g_steal_pointer(&helper)); } else fu_dbus_daemon_authorize_modify_device_internal( fu_daemon_get_engine(FU_DAEMON(self)), device_id, key, value, invocation); } static void fu_dbus_daemon_method_get_results(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); GVariant *val; g_autoptr(GError) error = NULL; const gchar *device_id = NULL; g_autoptr(FwupdDevice) result = NULL; g_variant_get(parameters, "(&s)", &device_id); if (!fu_dbus_daemon_device_id_valid(device_id, &error)) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } result = fu_engine_get_results(engine, device_id, &error); if (result == NULL) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } val = fwupd_codec_to_variant(FWUPD_CODEC(result), FWUPD_CODEC_FLAG_TRUSTED); g_dbus_method_invocation_return_value(invocation, g_variant_new_tuple(&val, 1)); } static void fu_dbus_daemon_method_update_metadata(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { #ifdef HAVE_GIO_UNIX FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); GDBusMessage *message; GUnixFDList *fd_list; const gchar *remote_id = NULL; gint fd_data; gint fd_sig; g_autoptr(GError) error = NULL; g_variant_get(parameters, "(&shh)", &remote_id, &fd_data, &fd_sig); /* update the metadata store */ message = g_dbus_method_invocation_get_message(invocation); fd_list = g_dbus_message_get_unix_fd_list(message); if (fd_list == NULL || g_unix_fd_list_get_length(fd_list) != 2) { g_set_error(&error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid handle"); fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } fd_data = g_unix_fd_list_get(fd_list, 0, &error); if (fd_data < 0) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } fd_sig = g_unix_fd_list_get(fd_list, 1, &error); if (fd_sig < 0) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } /* store new metadata (will close the fds when done) */ if (!fu_engine_update_metadata(engine, remote_id, fd_data, fd_sig, &error)) { g_prefix_error(&error, "Failed to update metadata for %s: ", remote_id); fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } g_dbus_method_invocation_return_value(invocation, NULL); #else g_dbus_method_invocation_return_error_literal(invocation, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unsupported feature"); #endif /* HAVE_GIO_UNIX */ } static void fu_dbus_daemon_method_unlock(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { const gchar *device_id = NULL; g_autoptr(FuMainAuthHelper) helper = NULL; g_autoptr(GError) error = NULL; g_variant_get(parameters, "(&s)", &device_id); if (!fu_dbus_daemon_device_id_valid(device_id, &error)) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } /* authenticate */ fu_dbus_daemon_set_status(self, FWUPD_STATUS_WAITING_FOR_AUTH); helper = g_new0(FuMainAuthHelper, 1); helper->self = self; helper->request = g_object_ref(request); helper->invocation = g_object_ref(invocation); helper->device_id = g_strdup(device_id); fu_polkit_authority_check(self->authority, fu_engine_request_get_sender(request), "org.freedesktop.fwupd.device-unlock", fu_dbus_daemon_engine_request_get_authority_check_flags(request), NULL, fu_dbus_daemon_authorize_unlock_cb, g_steal_pointer(&helper)); } static void fu_dbus_daemon_method_activate(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { const gchar *device_id = NULL; g_autoptr(FuMainAuthHelper) helper = NULL; g_autoptr(GError) error = NULL; g_variant_get(parameters, "(&s)", &device_id); if (!fu_dbus_daemon_device_id_valid(device_id, &error)) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } /* authenticate */ fu_dbus_daemon_set_status(self, FWUPD_STATUS_WAITING_FOR_AUTH); helper = g_new0(FuMainAuthHelper, 1); helper->self = self; helper->request = g_object_ref(request); helper->invocation = g_object_ref(invocation); helper->device_id = g_strdup(device_id); fu_polkit_authority_check(self->authority, fu_engine_request_get_sender(request), "org.freedesktop.fwupd.device-activate", fu_dbus_daemon_engine_request_get_authority_check_flags(request), NULL, fu_dbus_daemon_authorize_activate_cb, g_steal_pointer(&helper)); } static void fu_dbus_daemon_method_modify_config(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { g_autofree gchar *key = NULL; g_autofree gchar *section = NULL; g_autofree gchar *value = NULL; g_autoptr(FuMainAuthHelper) helper = NULL; g_variant_get(parameters, "(sss)", §ion, &key, &value); /* authenticate */ helper = g_new0(FuMainAuthHelper, 1); helper->self = self; helper->section = g_steal_pointer(§ion); helper->key = g_steal_pointer(&key); helper->value = g_steal_pointer(&value); helper->request = g_object_ref(request); helper->invocation = g_object_ref(invocation); fu_polkit_authority_check(self->authority, fu_engine_request_get_sender(request), "org.freedesktop.fwupd.modify-config", fu_dbus_daemon_engine_request_get_authority_check_flags(request), NULL, fu_dbus_daemon_modify_config_cb, g_steal_pointer(&helper)); } static void fu_dbus_daemon_method_reset_config(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { g_autofree gchar *section = NULL; g_autoptr(FuMainAuthHelper) helper = NULL; g_variant_get(parameters, "(s)", §ion); /* authenticate */ helper = g_new0(FuMainAuthHelper, 1); helper->self = self; helper->section = g_steal_pointer(§ion); helper->request = g_object_ref(request); helper->invocation = g_object_ref(invocation); fu_polkit_authority_check(self->authority, fu_engine_request_get_sender(request), "org.freedesktop.fwupd.reset-config", fu_dbus_daemon_engine_request_get_authority_check_flags(request), NULL, fu_dbus_daemon_reset_config_cb, g_steal_pointer(&helper)); } static void fu_dbus_daemon_method_modify_remote(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { const gchar *remote_id = NULL; const gchar *key = NULL; const gchar *value = NULL; g_autoptr(FuMainAuthHelper) helper = NULL; /* check the id exists */ g_variant_get(parameters, "(&s&s&s)", &remote_id, &key, &value); /* create helper object */ helper = g_new0(FuMainAuthHelper, 1); helper->request = g_object_ref(request); helper->invocation = g_object_ref(invocation); helper->remote_id = g_strdup(remote_id); helper->key = g_strdup(key); helper->value = g_strdup(value); helper->self = self; /* authenticate */ fu_dbus_daemon_set_status(self, FWUPD_STATUS_WAITING_FOR_AUTH); fu_polkit_authority_check(self->authority, fu_engine_request_get_sender(request), "org.freedesktop.fwupd.modify-remote", fu_dbus_daemon_engine_request_get_authority_check_flags(request), NULL, fu_dbus_daemon_authorize_modify_remote_cb, g_steal_pointer(&helper)); } static void fu_dbus_daemon_method_verify_update(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { const gchar *device_id = NULL; g_autoptr(FuMainAuthHelper) helper = NULL; g_autoptr(GError) error = NULL; /* check the id exists */ g_variant_get(parameters, "(&s)", &device_id); if (!fu_dbus_daemon_device_id_valid(device_id, &error)) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } /* create helper object */ helper = g_new0(FuMainAuthHelper, 1); helper->request = g_object_ref(request); helper->invocation = g_object_ref(invocation); helper->device_id = g_strdup(device_id); helper->self = self; /* authenticate */ fu_dbus_daemon_set_status(self, FWUPD_STATUS_WAITING_FOR_AUTH); fu_polkit_authority_check(self->authority, fu_engine_request_get_sender(request), "org.freedesktop.fwupd.verify-update", fu_dbus_daemon_engine_request_get_authority_check_flags(request), NULL, fu_dbus_daemon_authorize_verify_update_cb, g_steal_pointer(&helper)); } static void fu_dbus_daemon_method_verify(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); const gchar *device_id = NULL; g_autoptr(GError) error = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_variant_get(parameters, "(&s)", &device_id); if (!fu_dbus_daemon_device_id_valid(device_id, &error)) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } /* progress */ fu_progress_set_profile(progress, g_getenv("FWUPD_VERBOSE") != NULL); g_signal_connect(FU_PROGRESS(progress), "percentage-changed", G_CALLBACK(fu_dbus_daemon_progress_percentage_changed_cb), self); g_signal_connect(FU_PROGRESS(progress), "status-changed", G_CALLBACK(fu_dbus_daemon_progress_status_changed_cb), self); if (!fu_engine_verify(engine, device_id, progress, &error)) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } g_dbus_method_invocation_return_value(invocation, NULL); } static void fu_dbus_daemon_method_set_feature_flags(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { guint64 feature_flags_u64 = 0; g_autoptr(FuClient) client = NULL; g_variant_get(parameters, "(t)", &feature_flags_u64); /* old flags for the same sender will be automatically destroyed */ client = fu_client_list_register(self->client_list, fu_engine_request_get_sender(request)); fu_client_set_feature_flags(client, feature_flags_u64); g_dbus_method_invocation_return_value(invocation, NULL); } static void fu_dbus_daemon_method_set_hints(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { const gchar *prop_key; const gchar *prop_value; g_autoptr(FuClient) client = NULL; g_autoptr(GVariantIter) iter = NULL; g_variant_get(parameters, "(a{ss})", &iter); client = fu_client_list_register(self->client_list, fu_engine_request_get_sender(request)); while (g_variant_iter_next(iter, "{&s&s}", &prop_key, &prop_value)) { g_debug("got hint %s=%s", prop_key, prop_value); fu_client_insert_hint(client, prop_key, prop_value); } g_dbus_method_invocation_return_value(invocation, NULL); } static void fu_dbus_daemon_method_inhibit(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { FuDbusDaemonSystemInhibit *inhibit; const gchar *reason = NULL; g_variant_get(parameters, "(&s)", &reason); /* watch */ inhibit = g_new0(FuDbusDaemonSystemInhibit, 1); inhibit->sender = g_strdup(fu_engine_request_get_sender(request)); inhibit->id = g_strdup_printf("dbus-%i", g_random_int_range(1, G_MAXINT - 1)); /* nocheck:blocked */ inhibit->watcher_id = g_bus_watch_name_on_connection(self->connection, fu_engine_request_get_sender(request), G_BUS_NAME_WATCHER_FLAGS_NONE, NULL, fu_dbus_daemon_inhibit_name_vanished_cb, self, NULL); g_ptr_array_add(self->system_inhibits, inhibit); fu_dbus_daemon_ensure_system_inhibit(self); g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", inhibit->id)); } static void fu_dbus_daemon_method_install(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { #ifdef HAVE_GIO_UNIX FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); GVariant *prop_value; const gchar *device_id = NULL; const gchar *prop_key; gint32 fd_handle = 0; g_autoptr(FuMainAuthHelper) helper = NULL; g_autoptr(GError) error = NULL; g_autoptr(GVariantIter) iter = NULL; /* check the id exists */ g_variant_get(parameters, "(&sha{sv})", &device_id, &fd_handle, &iter); if (!fu_dbus_daemon_device_id_valid(device_id, &error)) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } /* create helper object */ helper = g_new0(FuMainAuthHelper, 1); helper->request = g_object_ref(request); helper->progress = fu_progress_new(G_STRLOC); helper->invocation = g_object_ref(invocation); helper->device_id = g_strdup(device_id); helper->self = self; /* get flags */ while (g_variant_iter_next(iter, "{&sv}", &prop_key, &prop_value)) { g_debug("got option %s", prop_key); if (g_strcmp0(prop_key, "install-flags") == 0) helper->flags = g_variant_get_uint64(prop_value); /* these are all set by libfwupd < 2.0.x; parse for compatibility */ if (g_strcmp0(prop_key, "allow-older") == 0 && g_variant_get_boolean(prop_value) == TRUE) helper->flags |= FWUPD_INSTALL_FLAG_ALLOW_OLDER; if (g_strcmp0(prop_key, "allow-reinstall") == 0 && g_variant_get_boolean(prop_value) == TRUE) helper->flags |= FWUPD_INSTALL_FLAG_ALLOW_REINSTALL; if (g_strcmp0(prop_key, "allow-branch-switch") == 0 && g_variant_get_boolean(prop_value) == TRUE) helper->flags |= FWUPD_INSTALL_FLAG_ALLOW_BRANCH_SWITCH; g_variant_unref(prop_value); } /* get stream */ helper->stream = fu_dbus_daemon_invocation_get_input_stream(invocation, &error); if (helper->stream == NULL) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } /* relax these */ if (fu_engine_config_get_ignore_requirements(fu_engine_get_config(engine))) helper->flags |= FWUPD_INSTALL_FLAG_IGNORE_REQUIREMENTS; /* install all the things in the store */ helper->client = fu_client_list_register(self->client_list, fu_engine_request_get_sender(request)); helper->client_sender_changed_id = g_signal_connect(FU_CLIENT(helper->client), "notify::flags", G_CALLBACK(fu_dbus_daemon_client_flags_notify_cb), helper); if (!fu_dbus_daemon_install_with_helper(g_steal_pointer(&helper), &error)) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } #else g_dbus_method_invocation_return_error_literal(invocation, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unsupported feature"); #endif /* HAVE_GIO_UNIX */ } static void fu_dbus_daemon_method_uninhibit(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { const gchar *inhibit_id = NULL; gboolean found = FALSE; g_variant_get(parameters, "(&s)", &inhibit_id); /* find by id, then uninhibit device */ for (guint i = 0; i < self->system_inhibits->len; i++) { FuDbusDaemonSystemInhibit *inhibit = g_ptr_array_index(self->system_inhibits, i); if (g_strcmp0(inhibit->id, inhibit_id) == 0) { g_ptr_array_remove_index(self->system_inhibits, i); fu_dbus_daemon_ensure_system_inhibit(self); found = TRUE; break; } } if (!found) { g_dbus_method_invocation_return_error_literal(invocation, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "Cannot find inhibit ID"); return; } g_dbus_method_invocation_return_value(invocation, NULL); } static void fu_dbus_daemon_method_get_details(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { #ifdef HAVE_GIO_UNIX FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); gint32 fd_handle = 0; g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(GPtrArray) results = NULL; /* get parameters */ g_variant_get(parameters, "(h)", &fd_handle); /* get stream */ stream = fu_dbus_daemon_invocation_get_input_stream(invocation, &error); if (stream == NULL) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } /* get details about the file */ results = fu_engine_get_details(engine, request, stream, &error); if (results == NULL) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } g_dbus_method_invocation_return_value( invocation, fwupd_codec_array_to_variant(results, FWUPD_CODEC_FLAG_TRUSTED)); #else g_dbus_method_invocation_return_error_literal(invocation, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unsupported feature"); #endif /* HAVE_GIO_UNIX */ } static void fu_dbus_daemon_method_get_bios_settings(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); gboolean authenticate = fu_engine_request_get_feature_flags(request) & FWUPD_FEATURE_FLAG_ALLOW_AUTHENTICATION; if (!authenticate) { /* if we cannot authenticate and the peer is not * inherently trusted, only return a non-sensitive * subset of the settings */ g_autoptr(FuBiosSettings) attrs = fu_context_get_bios_settings(fu_engine_get_context(engine)); g_dbus_method_invocation_return_value( invocation, fwupd_codec_to_variant(FWUPD_CODEC(attrs), fu_engine_request_get_converter_flags(request))); } else { g_autoptr(FuMainAuthHelper) helper = NULL; /* authenticate */ fu_dbus_daemon_set_status(self, FWUPD_STATUS_WAITING_FOR_AUTH); helper = g_new0(FuMainAuthHelper, 1); helper->self = self; helper->request = g_object_ref(request); helper->invocation = g_object_ref(invocation); fu_polkit_authority_check( self->authority, fu_engine_request_get_sender(request), "org.freedesktop.fwupd.get-bios-settings", fu_dbus_daemon_engine_request_get_authority_check_flags(request), NULL, fu_dbus_daemon_authorize_get_bios_settings_cb, g_steal_pointer(&helper)); } } static void fu_dbus_daemon_method_set_bios_settings(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { const gchar *key; const gchar *value; g_autoptr(FuMainAuthHelper) helper = NULL; g_autoptr(GVariantIter) iter = NULL; g_variant_get(parameters, "(a{ss})", &iter); /* authenticate */ fu_dbus_daemon_set_status(self, FWUPD_STATUS_WAITING_FOR_AUTH); helper = g_new0(FuMainAuthHelper, 1); helper->self = self; helper->request = g_object_ref(request); helper->invocation = g_object_ref(invocation); helper->bios_settings = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); while (g_variant_iter_next(iter, "{&s&s}", &key, &value)) { g_debug("got setting %s=%s", key, value); g_hash_table_insert(helper->bios_settings, g_strdup(key), g_strdup(value)); } fu_polkit_authority_check(self->authority, fu_engine_request_get_sender(request), "org.freedesktop.fwupd.set-bios-settings", fu_dbus_daemon_engine_request_get_authority_check_flags(request), NULL, fu_dbus_daemon_authorize_set_bios_settings_cb, g_steal_pointer(&helper)); } static void fu_dbus_daemon_method_fix_host_security_attr(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { const gchar *appstream_id = NULL; g_autoptr(FuMainAuthHelper) helper = NULL; g_variant_get(parameters, "(&s)", &appstream_id); /* authenticate */ fu_dbus_daemon_set_status(self, FWUPD_STATUS_WAITING_FOR_AUTH); helper = g_new0(FuMainAuthHelper, 1); helper->self = self; helper->request = g_object_ref(request); helper->invocation = g_object_ref(invocation); helper->key = g_strdup(appstream_id); helper->is_fix = TRUE; fu_polkit_authority_check(self->authority, fu_engine_request_get_sender(request), "org.freedesktop.fwupd.fix-host-security-attr", fu_dbus_daemon_engine_request_get_authority_check_flags(request), NULL, fu_dbus_daemon_authorize_fix_host_security_attr_cb, g_steal_pointer(&helper)); } static void fu_dbus_daemon_method_undo_host_security_attr(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation) { const gchar *appstream_id = NULL; g_autoptr(FuMainAuthHelper) helper = NULL; g_variant_get(parameters, "(&s)", &appstream_id); /* authenticate */ fu_dbus_daemon_set_status(self, FWUPD_STATUS_WAITING_FOR_AUTH); helper = g_new0(FuMainAuthHelper, 1); helper->self = self; helper->request = g_object_ref(request); helper->invocation = g_object_ref(invocation); helper->key = g_strdup(appstream_id); helper->is_fix = FALSE; fu_polkit_authority_check(self->authority, fu_engine_request_get_sender(request), "org.freedesktop.fwupd.undo-host-security-attr", fu_dbus_daemon_engine_request_get_authority_check_flags(request), NULL, fu_dbus_daemon_authorize_undo_host_security_attr_cb, g_steal_pointer(&helper)); } typedef void (*FuDbusDaemonMethodFunc)(FuDbusDaemon *self, GVariant *parameters, FuEngineRequest *request, GDBusMethodInvocation *invocation); static void fu_dbus_daemon_method_call(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { FuDbusDaemon *self = FU_DBUS_DAEMON(user_data); FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); g_autoptr(FuEngineRequest) request = NULL; g_autoptr(GError) error = NULL; g_autoptr(GString) parameters_str = NULL; struct { const gchar *name; FuDbusDaemonMethodFunc func; } method_funcs[] = { {"GetDevices", fu_dbus_daemon_method_get_devices}, {"GetPlugins", fu_dbus_daemon_method_get_plugins}, {"GetReleases", fu_dbus_daemon_method_get_releases}, {"GetApprovedFirmware", fu_dbus_daemon_method_get_approved_firmware}, {"GetBlockedFirmware", fu_dbus_daemon_method_get_blocked_firmware}, {"GetReportMetadata", fu_dbus_daemon_method_get_report_metadata}, {"SetApprovedFirmware", fu_dbus_daemon_method_set_approved_firmware}, {"SetBlockedFirmware", fu_dbus_daemon_method_set_blocked_firmware}, {"Quit", fu_dbus_daemon_method_quit}, {"SelfSign", fu_dbus_daemon_method_self_sign}, {"GetDowngrades", fu_dbus_daemon_method_get_downgrades}, {"GetUpgrades", fu_dbus_daemon_method_get_upgrades}, {"GetRemotes", fu_dbus_daemon_method_get_remotes}, {"GetHistory", fu_dbus_daemon_method_get_history}, {"GetHostSecurityAttrs", fu_dbus_daemon_method_get_host_security_attrs}, {"GetHostSecurityEvents", fu_dbus_daemon_method_get_host_security_events}, {"ClearResults", fu_dbus_daemon_method_clear_results}, {"EmulationLoad", fu_dbus_daemon_method_emulation_load}, {"EmulationSave", fu_dbus_daemon_method_emulation_save}, {"ModifyDevice", fu_dbus_daemon_method_modify_device}, {"GetResults", fu_dbus_daemon_method_get_results}, {"UpdateMetadata", fu_dbus_daemon_method_update_metadata}, {"Unlock", fu_dbus_daemon_method_unlock}, {"Activate", fu_dbus_daemon_method_activate}, {"ModifyConfig", fu_dbus_daemon_method_modify_config}, {"ResetConfig", fu_dbus_daemon_method_reset_config}, {"ModifyRemote", fu_dbus_daemon_method_modify_remote}, {"VerifyUpdate", fu_dbus_daemon_method_verify_update}, {"Verify", fu_dbus_daemon_method_verify}, {"SetFeatureFlags", fu_dbus_daemon_method_set_feature_flags}, {"SetHints", fu_dbus_daemon_method_set_hints}, {"Inhibit", fu_dbus_daemon_method_inhibit}, {"Uninhibit", fu_dbus_daemon_method_uninhibit}, {"Install", fu_dbus_daemon_method_install}, {"GetDetails", fu_dbus_daemon_method_get_details}, {"GetBiosSettings", fu_dbus_daemon_method_get_bios_settings}, {"SetBiosSettings", fu_dbus_daemon_method_set_bios_settings}, {"FixHostSecurityAttr", fu_dbus_daemon_method_fix_host_security_attr}, {"UndoHostSecurityAttr", fu_dbus_daemon_method_undo_host_security_attr}, }; /* build request */ request = fu_dbus_daemon_create_request(self, sender, &error); if (request == NULL) { fu_dbus_daemon_method_invocation_return_gerror(invocation, error); return; } /* activity */ fu_engine_idle_reset(engine); /* be helpful */ parameters_str = g_variant_print_string(parameters, NULL, TRUE); g_debug("Called %s%s", method_name, parameters_str->str); /* call the correct vfunc */ for (guint i = 0; i < G_N_ELEMENTS(method_funcs); i++) { if (g_strcmp0(method_name, method_funcs[i].name) == 0) { method_funcs[i].func(self, parameters, request, invocation); return; } } g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD, "no such method %s", method_name); } static GVariant * fu_dbus_daemon_get_property(GDBusConnection *connection_, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GError **error, gpointer user_data) { FuDbusDaemon *self = FU_DBUS_DAEMON(user_data); FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); /* activity */ fu_engine_idle_reset(engine); if (g_strcmp0(property_name, "DaemonVersion") == 0) return g_variant_new_string(PACKAGE_VERSION); if (g_strcmp0(property_name, "HostBkc") == 0) return g_variant_new_string(fu_engine_get_host_bkc(engine)); if (g_strcmp0(property_name, "Tainted") == 0) return g_variant_new_boolean(FALSE); if (g_strcmp0(property_name, "Status") == 0) return g_variant_new_uint32(self->status); if (g_strcmp0(property_name, "Percentage") == 0) return g_variant_new_uint32(self->percentage); if (g_strcmp0(property_name, FWUPD_RESULT_KEY_BATTERY_LEVEL) == 0) { FuContext *ctx = fu_engine_get_context(engine); return g_variant_new_uint32(fu_context_get_battery_level(ctx)); } if (g_strcmp0(property_name, FWUPD_RESULT_KEY_BATTERY_THRESHOLD) == 0) { FuContext *ctx = fu_engine_get_context(engine); return g_variant_new_uint32(fu_context_get_battery_threshold(ctx)); } if (g_strcmp0(property_name, "HostVendor") == 0) return g_variant_new_string(fu_engine_get_host_vendor(engine)); if (g_strcmp0(property_name, "HostProduct") == 0) return g_variant_new_string(fu_engine_get_host_product(engine)); if (g_strcmp0(property_name, "HostMachineId") == 0) { const gchar *tmp = fu_engine_get_host_machine_id(engine); if (tmp == NULL) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "failed to get daemon property %s", property_name); return NULL; } return g_variant_new_string(tmp); } if (g_strcmp0(property_name, "HostSecurityId") == 0) { #ifdef HAVE_HSI g_autofree gchar *tmp = fu_engine_get_host_security_id(engine, NULL); return g_variant_new_string(tmp); #else g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "failed to get daemon property %s", property_name); return NULL; #endif } if (g_strcmp0(property_name, "Interactive") == 0) return g_variant_new_boolean(isatty(fileno(stdout)) != 0); if (g_strcmp0(property_name, "OnlyTrusted") == 0) { return g_variant_new_boolean( fu_engine_config_get_only_trusted(fu_engine_get_config(engine))); } /* return an error */ g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY, "failed to get daemon property %s", property_name); return NULL; } static gboolean fu_dbus_daemon_register_object(FuDbusDaemon *self, GError **error) { guint registration_id; static const GDBusInterfaceVTable interface_vtable = {fu_dbus_daemon_method_call, fu_dbus_daemon_get_property, NULL}; registration_id = g_dbus_connection_register_object(self->connection, FWUPD_DBUS_PATH, self->introspection_daemon->interfaces[0], &interface_vtable, self, /* user_data */ NULL, /* user_data_free_func */ NULL); /* GError** */ if (registration_id == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "unspecified failure"); return FALSE; } /* success */ return TRUE; } static void fu_dbus_daemon_client_list_ensure_inhibit(FuDbusDaemon *self) { FuEngine *engine = fu_daemon_get_engine(FU_DAEMON(self)); g_autoptr(GPtrArray) clients = fu_client_list_get_all(self->client_list); g_debug("connected clients: %u", clients->len); if (clients->len > 0 && self->clients_inhibit_id == 0) { self->clients_inhibit_id = fu_engine_idle_inhibit(engine, FU_IDLE_INHIBIT_TIMEOUT, "connected-clients"); } else if (clients->len == 0 && self->clients_inhibit_id != 0) { fu_engine_idle_uninhibit(engine, self->clients_inhibit_id); self->clients_inhibit_id = 0; } } static void fu_dbus_daemon_client_list_added_cb(FuClientList *client_list, FuClient *client, gpointer user_data) { FuDbusDaemon *self = FU_DBUS_DAEMON(user_data); fu_dbus_daemon_client_list_ensure_inhibit(self); } static void fu_dbus_daemon_client_list_removed_cb(FuClientList *client_list, FuClient *client, gpointer user_data) { FuDbusDaemon *self = FU_DBUS_DAEMON(user_data); fu_dbus_daemon_client_list_ensure_inhibit(self); } static void fu_dbus_daemon_set_connection(FuDbusDaemon *self, GDBusConnection *connection) { g_set_object(&self->connection, connection); if (connection != NULL) { g_autoptr(FuClientList) client_list = fu_client_list_new(connection); g_signal_connect(client_list, "added", G_CALLBACK(fu_dbus_daemon_client_list_added_cb), self); g_signal_connect(client_list, "removed", G_CALLBACK(fu_dbus_daemon_client_list_removed_cb), self); g_set_object(&self->client_list, client_list); } } static void fu_dbus_daemon_dbus_bus_acquired_cb(GDBusConnection *connection, const gchar *name, gpointer user_data) { FuDbusDaemon *self = FU_DBUS_DAEMON(user_data); g_autoptr(GError) error = NULL; /* connect to D-Bus directly */ self->proxy_uid = g_dbus_proxy_new_sync(connection, G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES | G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS, NULL, "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", NULL, &error); if (self->proxy_uid == NULL) { g_warning("cannot connect to DBus: %s", error->message); return; } fu_dbus_daemon_set_connection(self, connection); if (!fu_dbus_daemon_register_object(self, &error)) { g_warning("cannot register object: %s", error->message); return; } } static void fu_dbus_daemon_dbus_name_acquired_cb(GDBusConnection *connection, const gchar *name, gpointer user_data) { g_debug("acquired name: %s", name); } static void fu_dbus_daemon_dbus_name_lost_cb(GDBusConnection *connection, const gchar *name, gpointer user_data) { FuDbusDaemon *self = FU_DBUS_DAEMON(user_data); g_warning("another service has claimed the dbus name %s", name); fu_daemon_stop(FU_DAEMON(self), NULL); } static void fu_dbus_daemon_dbus_connection_closed_cb(GDBusConnection *connection, gboolean remote_peer_vanished, GError *error, gpointer user_data) { if (remote_peer_vanished) g_info("client connection closed: %s", error != NULL ? error->message : "unknown"); } static gboolean fu_dbus_daemon_dbus_new_connection_cb(GDBusServer *server, GDBusConnection *connection, gpointer user_data) { FuDbusDaemon *self = FU_DBUS_DAEMON(user_data); fu_dbus_daemon_set_connection(self, connection); g_signal_connect(connection, "closed", G_CALLBACK(fu_dbus_daemon_dbus_connection_closed_cb), self); return fu_dbus_daemon_register_object(self, NULL); } static GDBusNodeInfo * fu_dbus_daemon_load_introspection(const gchar *filename, GError **error) { g_autoptr(GBytes) data = NULL; g_autofree gchar *path = NULL; /* lookup data */ path = g_build_filename("/org/freedesktop/fwupd", filename, NULL); data = g_resources_lookup_data(path, G_RESOURCE_LOOKUP_FLAGS_NONE, error); if (data == NULL) return NULL; /* build introspection from XML */ return g_dbus_node_info_new_for_xml(g_bytes_get_data(data, NULL), error); } static gboolean fu_dbus_daemon_setup(FuDaemon *daemon, const gchar *socket_address, FuProgress *progress, GError **error) { FuDbusDaemon *self = FU_DBUS_DAEMON(daemon); FuEngine *engine = fu_daemon_get_engine(daemon); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_profile(progress, g_getenv("FWUPD_VERBOSE") != NULL); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 99, "load-engine"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "load-introspection"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "load-authority"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "own-name"); /* load engine */ g_signal_connect(FU_ENGINE(engine), "changed", G_CALLBACK(fu_dbus_daemon_engine_changed_cb), self); g_signal_connect(FU_ENGINE(engine), "device-added", G_CALLBACK(fu_dbus_daemon_engine_device_added_cb), self); g_signal_connect(FU_ENGINE(engine), "device-removed", G_CALLBACK(fu_dbus_daemon_engine_device_removed_cb), self); g_signal_connect(FU_ENGINE(engine), "device-changed", G_CALLBACK(fu_dbus_daemon_engine_device_changed_cb), self); g_signal_connect(FU_ENGINE(engine), "device-request", G_CALLBACK(fu_dbus_daemon_engine_device_request_cb), self); g_signal_connect(FU_ENGINE(engine), "status-changed", G_CALLBACK(fu_dbus_daemon_engine_status_changed_cb), self); if (!fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_HWINFO | FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_EXTERNAL_PLUGINS | FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS | FU_ENGINE_LOAD_FLAG_ENSURE_CLIENT_CERT | FU_ENGINE_LOAD_FLAG_DEVICE_HOTPLUG, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to load engine: "); return FALSE; } fu_progress_step_done(progress); /* load introspection from file */ self->introspection_daemon = fu_dbus_daemon_load_introspection(FWUPD_DBUS_INTERFACE ".xml", error); if (self->introspection_daemon == NULL) { g_prefix_error(error, "failed to load introspection: "); return FALSE; } fu_progress_step_done(progress); /* get authority */ self->authority = fu_polkit_authority_new(); if (!fu_polkit_authority_load(self->authority, error)) return FALSE; fu_progress_step_done(progress); /* own the object */ if (socket_address != NULL) { g_autofree gchar *guid = g_dbus_generate_guid(); g_autoptr(GDBusServer) server = NULL; server = g_dbus_server_new_sync(socket_address, G_DBUS_SERVER_FLAGS_AUTHENTICATION_ALLOW_ANONYMOUS, guid, NULL, NULL, error); if (server == NULL) { g_prefix_error(error, "failed to create D-Bus server: "); return FALSE; } g_message("using socket address: %s", g_dbus_server_get_client_address(server)); g_dbus_server_start(server); g_signal_connect(server, "new-connection", G_CALLBACK(fu_dbus_daemon_dbus_new_connection_cb), self); } else { self->owner_id = g_bus_own_name(G_BUS_TYPE_SYSTEM, FWUPD_DBUS_SERVICE, G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | G_BUS_NAME_OWNER_FLAGS_REPLACE, fu_dbus_daemon_dbus_bus_acquired_cb, fu_dbus_daemon_dbus_name_acquired_cb, fu_dbus_daemon_dbus_name_lost_cb, self, NULL); } fu_progress_step_done(progress); /* success */ return TRUE; } static void fu_dbus_daemon_init(FuDbusDaemon *self) { self->status = FWUPD_STATUS_IDLE; self->system_inhibits = g_ptr_array_new_with_free_func((GDestroyNotify)fu_dbus_daemon_system_inhibit_free); } static void fu_dbus_daemon_finalize(GObject *obj) { FuDbusDaemon *self = FU_DBUS_DAEMON(obj); g_ptr_array_unref(self->system_inhibits); if (self->client_list != NULL) g_object_unref(self->client_list); if (self->owner_id > 0) g_bus_unown_name(self->owner_id); if (self->proxy_uid != NULL) g_object_unref(self->proxy_uid); if (self->connection != NULL) g_object_unref(self->connection); if (self->authority != NULL) g_object_unref(self->authority); if (self->introspection_daemon != NULL) g_dbus_node_info_unref(self->introspection_daemon); G_OBJECT_CLASS(fu_dbus_daemon_parent_class)->finalize(obj); } static void fu_dbus_daemon_class_init(FuDbusDaemonClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuDaemonClass *daemon_class = FU_DAEMON_CLASS(klass); object_class->finalize = fu_dbus_daemon_finalize; daemon_class->setup = fu_dbus_daemon_setup; } FuDaemon * fu_daemon_new(void) { return FU_DAEMON(g_object_new(FU_TYPE_DBUS_DAEMON, NULL)); } fwupd-2.0.10/src/fu-dbus-daemon.h000066400000000000000000000004341501337203100164610ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-daemon.h" #define FU_TYPE_DBUS_DAEMON (fu_dbus_daemon_get_type()) G_DECLARE_FINAL_TYPE(FuDbusDaemon, fu_dbus_daemon, FU, DBUS_DAEMON, FuDaemon) fwupd-2.0.10/src/fu-debug.c000066400000000000000000000205111501337203100153420ustar00rootroot00000000000000/* * Copyright 2010 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuDebug" #include "config.h" #include #include #include #include #include #ifdef _WIN32 #include #include #endif typedef struct { GOptionGroup *group; GLogLevelFlags log_level; gboolean console; gboolean no_timestamp; gboolean no_domain; gchar **daemon_verbose; #ifdef _WIN32 HANDLE event_source; #endif } FuDebug; static const gchar * fu_debug_log_level_to_string(GLogLevelFlags log_level) { if (log_level == G_LOG_LEVEL_ERROR) return "error"; if (log_level == G_LOG_LEVEL_CRITICAL) return "critical"; if (log_level == G_LOG_LEVEL_WARNING) return "warning"; if (log_level == G_LOG_LEVEL_MESSAGE) return "message"; if (log_level == G_LOG_LEVEL_INFO) return "info"; if (log_level == G_LOG_LEVEL_DEBUG) return "debug"; return NULL; } static void fu_debug_free(FuDebug *self) { g_option_group_set_parse_hooks(self->group, NULL, NULL); g_option_group_unref(self->group); g_strfreev(self->daemon_verbose); #ifdef _WIN32 DeregisterEventSource(self->event_source); #endif g_free(self); } static gboolean fu_debug_filter_cb(FuDebug *self, const gchar *log_domain, GLogLevelFlags log_level) { /* trivial */ if (log_level <= self->log_level) return TRUE; /* filter on domain */ if (self->daemon_verbose != NULL && log_domain != NULL) return g_strv_contains((const char *const *)self->daemon_verbose, log_domain); /* nope */ return FALSE; } #ifdef _WIN32 static void fu_debug_handler_win32(FuDebug *self, GLogLevelFlags log_level, const gchar *msg) { WORD ev_type = 0x0; /* nothing to do */ if (self->event_source == NULL) return; /* map levels */ switch (log_level) { case G_LOG_LEVEL_INFO: case G_LOG_LEVEL_MESSAGE: ev_type = EVENTLOG_INFORMATION_TYPE; break; case G_LOG_LEVEL_WARNING: ev_type = EVENTLOG_WARNING_TYPE; break; case G_LOG_LEVEL_ERROR: case G_LOG_LEVEL_CRITICAL: ev_type = EVENTLOG_ERROR_TYPE; break; default: return; break; } /* add to log */ ReportEventA(self->event_source, ev_type, FWUPD_CATEGORY_GENERIC, FWUPD_MESSAGE_GENERIC, NULL, 1, 0, (const char **)&msg, NULL); } #endif static void fu_debug_handler_cb(const gchar *log_domain, GLogLevelFlags log_level, const gchar *message, gpointer user_data) { FuDebug *self = (FuDebug *)user_data; g_autofree gchar *timestamp = NULL; g_autofree gchar *message_safe = NULL; g_autoptr(GString) domain = NULL; /* should ignore */ if (!fu_debug_filter_cb(self, log_domain, log_level)) return; /* make sure passwords never appear in logs */ message_safe = fu_strpassmask(message); #ifdef _WIN32 /* use Windows event log */ fu_debug_handler_win32(self, log_level, message_safe); #endif /* time header */ if (!self->no_timestamp) { g_autoptr(GDateTime) dt = g_date_time_new_now_utc(); timestamp = g_strdup_printf("%02i:%02i:%02i.%03i", g_date_time_get_hour(dt), g_date_time_get_minute(dt), g_date_time_get_second(dt), g_date_time_get_microsecond(dt) / 1000); } /* pad out domain */ if (!self->no_domain) { /* each file should have set this */ if (log_domain == NULL) log_domain = "FIXME"; domain = g_string_new(log_domain); for (gsize i = domain->len; i < 20; i++) g_string_append(domain, " "); } /* to file */ if (!self->console) { g_autofree gchar *ascii_message = g_str_to_ascii(message_safe, NULL); if (timestamp != NULL) g_printerr("%s ", timestamp); if (domain != NULL) g_printerr("%s ", domain->str); g_printerr("%s\n", ascii_message); return; } /* plain output */ if (g_getenv("NO_COLOR") != NULL) { if (timestamp != NULL) g_printerr("%s ", timestamp); if (domain != NULL) g_printerr("%s ", domain->str); g_printerr("%s\n", message_safe); return; } /* to screen */ switch (log_level) { case G_LOG_LEVEL_ERROR: case G_LOG_LEVEL_CRITICAL: case G_LOG_LEVEL_WARNING: /* critical in red */ if (timestamp != NULL) g_printerr("%c[%dm%s ", 0x1B, 32, timestamp); if (domain != NULL) g_printerr("%s ", domain->str); g_printerr("%c[%dm%s\n%c[%dm", 0x1B, 31, message_safe, 0x1B, 0); break; default: /* debug in blue */ if (timestamp != NULL) g_printerr("%c[%dm%s ", 0x1B, 32, timestamp); if (domain != NULL) g_printerr("%s ", domain->str); g_printerr("%c[%dm%s\n%c[%dm", 0x1B, 34, message_safe, 0x1B, 0); break; } } static gboolean fu_debug_verbose_arg_cb(const gchar *option_name, const gchar *value, gpointer user_data, GError **error) { FuDebug *self = (FuDebug *)user_data; if (self->log_level == G_LOG_LEVEL_MESSAGE) { self->log_level = G_LOG_LEVEL_INFO; return TRUE; } if (self->log_level == G_LOG_LEVEL_INFO) { self->log_level = G_LOG_LEVEL_DEBUG; return TRUE; } g_set_error_literal(error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, "No further debug level supported"); return FALSE; } static gboolean fu_debug_pre_parse_hook(GOptionContext *context, GOptionGroup *group, gpointer data, GError **error) { FuDebug *self = (FuDebug *)data; const GOptionEntry entries[] = { {"verbose", 'v', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, (GOptionArgFunc)fu_debug_verbose_arg_cb, /* TRANSLATORS: turn on all debugging */ N_("Show debugging information for all domains"), NULL}, {"no-timestamp", '\0', 0, G_OPTION_ARG_NONE, &self->no_timestamp, /* TRANSLATORS: turn on all debugging */ N_("Do not include timestamp prefix"), NULL}, {"no-domain", '\0', 0, G_OPTION_ARG_NONE, &self->no_domain, /* TRANSLATORS: turn on all debugging */ N_("Do not include log domain prefix"), NULL}, {"daemon-verbose", '\0', 0, G_OPTION_ARG_STRING_ARRAY, &self->daemon_verbose, /* TRANSLATORS: this is for daemon development */ N_("Show daemon verbose information for a particular domain"), "DOMAIN"}, {NULL}}; /* set from FuConfig */ if (g_strcmp0(g_getenv("FWUPD_VERBOSE"), "*") == 0) self->log_level = G_LOG_LEVEL_DEBUG; g_option_group_add_entries(group, entries); return TRUE; } static gboolean fu_debug_post_parse_hook(GOptionContext *context, GOptionGroup *group, gpointer data, GError **error) { FuDebug *self = (FuDebug *)data; /* for compat */ if (self->log_level == G_LOG_LEVEL_DEBUG) (void)g_setenv("FWUPD_VERBOSE", "1", FALSE); /* redirect all domains */ g_log_set_default_handler(fu_debug_handler_cb, self); /* are we on an actual TTY? */ self->console = (isatty(fileno(stderr)) == 1); g_info("verbose to %s (on console %i)", fu_debug_log_level_to_string(self->log_level), self->console); return TRUE; } #ifdef _WIN32 static void fu_debug_setup_event_source(FuDebug *self) { HKEY key; gchar msgfile[MAX_PATH]; DWORD dwData = EVENTLOG_ERROR_TYPE | EVENTLOG_WARNING_TYPE | EVENTLOG_INFORMATION_TYPE; if (RegCreateKeyExA(HKEY_LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\Services\\" "EventLog\\Application\\fwupd", 0, NULL, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, NULL, &key, NULL) != ERROR_SUCCESS) { g_warning("RegCreateKeyExA failed [%u]", (guint)GetLastError()); return; } GetModuleFileNameA(NULL, msgfile, MAX_PATH); RegSetValueExA(key, "EventMessageFile", 0, REG_EXPAND_SZ, (BYTE *)msgfile, strlen(msgfile) + 1); RegSetValueExA(key, "TypesSupported", 0, REG_DWORD, (BYTE *)&dwData, sizeof(dwData)); RegCloseKey(key); /* good to go */ self->event_source = RegisterEventSourceA(NULL, "fwupd"); } #endif /* (transfer): full */ GOptionGroup * fu_debug_get_option_group(void) { FuDebug *self = g_new0(FuDebug, 1); self->log_level = G_LOG_LEVEL_MESSAGE; self->group = g_option_group_new("debug", /* TRANSLATORS: for the --verbose arg */ _("Debugging Options"), /* TRANSLATORS: for the --verbose arg */ _("Show debugging options"), self, (GDestroyNotify)fu_debug_free); g_option_group_set_parse_hooks(self->group, fu_debug_pre_parse_hook, fu_debug_post_parse_hook); #ifdef _WIN32 fu_debug_setup_event_source(self); #endif return g_option_group_ref(self->group); } fwupd-2.0.10/src/fu-debug.h000066400000000000000000000003011501337203100153420ustar00rootroot00000000000000/* * Copyright 2010 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include GOptionGroup * fu_debug_get_option_group(void); fwupd-2.0.10/src/fu-device-list.c000066400000000000000000001126201501337203100164670ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuDeviceList" #include "config.h" #include #include "fu-device-list.h" #include "fu-device-private.h" #include "fu-engine.h" /** * FuDeviceList: * * This list of devices provides a way to find a device using either the * device-id or a GUID. * * The device list will emit ::added and ::removed signals when the device list * has been changed. If the #FuDevice has changed during a device replug then * the ::changed signal will be emitted instead of ::added and then ::removed. * * See also: [class@FuDevice] */ static void fu_device_list_finalize(GObject *obj); struct _FuDeviceList { GObject parent_instance; GPtrArray *devices; /* of FuDeviceItem */ GRWLock devices_mutex; }; enum { SIGNAL_ADDED, SIGNAL_REMOVED, SIGNAL_CHANGED, SIGNAL_LAST }; static guint signals[SIGNAL_LAST] = {0}; typedef struct { FuDevice *device; FuDevice *device_old; FuDeviceList *self; /* no ref */ guint remove_id; } FuDeviceItem; static void fu_device_list_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_EXTENDED(FuDeviceList, fu_device_list, G_TYPE_OBJECT, 0, G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fu_device_list_codec_iface_init)); static void fu_device_list_emit_device_added(FuDeviceList *self, FuDevice *device) { g_info("::added %s [%s]", fu_device_get_id(device), fu_device_get_name(device)); g_signal_emit(self, signals[SIGNAL_ADDED], 0, device); } static void fu_device_list_emit_device_removed(FuDeviceList *self, FuDevice *device) { g_info("::removed %s [%s]", fu_device_get_id(device), fu_device_get_name(device)); g_signal_emit(self, signals[SIGNAL_REMOVED], 0, device); } static void fu_device_list_emit_device_changed(FuDeviceList *self, FuDevice *device) { g_info("::changed %s [%s]", fu_device_get_id(device), fu_device_get_name(device)); g_signal_emit(self, signals[SIGNAL_CHANGED], 0, device); } static void fu_device_list_add_string(FwupdCodec *codec, guint idt, GString *str) { FuDeviceList *self = FU_DEVICE_LIST(codec); g_rw_lock_reader_lock(&self->devices_mutex); for (guint i = 0; i < self->devices->len; i++) { FuDeviceItem *item = g_ptr_array_index(self->devices, i); gboolean wfr; g_string_append_printf(str, "%u [%p] %s\n", i, item, item->remove_id != 0 ? "IN_TIMEOUT" : ""); wfr = fu_device_has_flag(item->device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); g_string_append_printf(str, "new: %s [%p] %s\n", fu_device_get_id(item->device), item->device, wfr ? "WAIT_FOR_REPLUG" : ""); if (item->device_old != NULL) { wfr = fu_device_has_flag(item->device_old, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); g_string_append_printf(str, "old: %s [%p] %s\n", fu_device_get_id(item->device_old), item->device_old, wfr ? "WAIT_FOR_REPLUG" : ""); } } g_rw_lock_reader_unlock(&self->devices_mutex); } /* we cannot use fu_device_get_children() as this will not find "parent-only" * logical relationships added using fu_device_add_parent_guid() */ static GPtrArray * fu_device_list_get_children(FuDeviceList *self, FuDevice *device) { GPtrArray *devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_rw_lock_reader_lock(&self->devices_mutex); for (guint i = 0; i < self->devices->len; i++) { FuDeviceItem *item = g_ptr_array_index(self->devices, i); if (device == fu_device_get_parent(item->device)) g_ptr_array_add(devices, g_object_ref(item->device)); } g_rw_lock_reader_unlock(&self->devices_mutex); return devices; } static void fu_device_list_depsolve_order_full(FuDeviceList *self, FuDevice *device, guint depth) { g_autoptr(GPtrArray) children = NULL; /* ourself */ fu_device_set_order(device, depth); /* optional children */ children = fu_device_list_get_children(self, device); for (guint i = 0; i < children->len; i++) { FuDevice *child = g_ptr_array_index(children, i); if (fu_device_has_private_flag(child, FU_DEVICE_PRIVATE_FLAG_INSTALL_PARENT_FIRST)) { fu_device_list_depsolve_order_full(self, child, depth + 1); } else { fu_device_list_depsolve_order_full(self, child, depth - 1); } } } /** * fu_device_list_depsolve_order: * @self: a device list * @device: a device * * Sets the device order using the logical parent->child relationships -- by default * the child is updated first, unless the device has set private flag * %FU_DEVICE_PRIVATE_FLAG_INSTALL_PARENT_FIRST. * * Since: 1.5.0 **/ void fu_device_list_depsolve_order(FuDeviceList *self, FuDevice *device) { g_autoptr(FuDevice) root = fu_device_get_root(device); if (fu_device_has_private_flag(root, FU_DEVICE_PRIVATE_FLAG_EXPLICIT_ORDER)) return; fu_device_list_depsolve_order_full(self, root, 0); } /** * fu_device_list_get_all: * @self: a device list * * Returns all the devices that have been added to the device list. * This includes devices that are no longer active, for instance where a * different plugin has taken over responsibility of the #FuDevice. * * Returns: (transfer container) (element-type FuDevice): the devices * * Since: 1.0.2 **/ GPtrArray * fu_device_list_get_all(FuDeviceList *self) { GPtrArray *devices; g_return_val_if_fail(FU_IS_DEVICE_LIST(self), NULL); devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_rw_lock_reader_lock(&self->devices_mutex); for (guint i = 0; i < self->devices->len; i++) { FuDeviceItem *item = g_ptr_array_index(self->devices, i); g_ptr_array_add(devices, g_object_ref(item->device)); } for (guint i = 0; i < self->devices->len; i++) { FuDeviceItem *item = g_ptr_array_index(self->devices, i); if (item->device_old == NULL) continue; g_ptr_array_add(devices, g_object_ref(item->device_old)); } g_rw_lock_reader_unlock(&self->devices_mutex); return devices; } /** * fu_device_list_get_active: * @self: a device list * * Returns all the active devices that have been added to the device list. * An active device is defined as a device that is currently connected and has * is owned by a plugin. * * Returns: (transfer container) (element-type FuDevice): the devices * * Since: 1.0.2 **/ GPtrArray * fu_device_list_get_active(FuDeviceList *self) { GPtrArray *devices; g_return_val_if_fail(FU_IS_DEVICE_LIST(self), NULL); devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_rw_lock_reader_lock(&self->devices_mutex); for (guint i = 0; i < self->devices->len; i++) { FuDeviceItem *item = g_ptr_array_index(self->devices, i); if (fu_device_has_private_flag(item->device, FU_DEVICE_PRIVATE_FLAG_UNCONNECTED)) continue; if (fu_device_has_inhibit(item->device, "hidden")) continue; g_ptr_array_add(devices, g_object_ref(item->device)); } g_rw_lock_reader_unlock(&self->devices_mutex); return devices; } static FuDeviceItem * fu_device_list_find_by_device(FuDeviceList *self, FuDevice *device) { g_autoptr(GRWLockReaderLocker) locker = g_rw_lock_reader_locker_new(&self->devices_mutex); g_return_val_if_fail(locker != NULL, NULL); for (guint i = 0; i < self->devices->len; i++) { FuDeviceItem *item = g_ptr_array_index(self->devices, i); if (item->device == device) return item; } for (guint i = 0; i < self->devices->len; i++) { FuDeviceItem *item = g_ptr_array_index(self->devices, i); if (item->device_old == device) return item; } return NULL; } static FuDeviceItem * fu_device_list_find_by_guid(FuDeviceList *self, const gchar *guid) { g_autoptr(GRWLockReaderLocker) locker = g_rw_lock_reader_locker_new(&self->devices_mutex); g_return_val_if_fail(locker != NULL, NULL); for (guint i = 0; i < self->devices->len; i++) { FuDeviceItem *item = g_ptr_array_index(self->devices, i); if (fu_device_has_guid(item->device, guid)) return item; } for (guint i = 0; i < self->devices->len; i++) { FuDeviceItem *item = g_ptr_array_index(self->devices, i); if (item->device_old == NULL) continue; if (fu_device_has_guid(item->device_old, guid)) return item; } return NULL; } static FuDeviceItem * fu_device_list_find_by_connection(FuDeviceList *self, const gchar *physical_id, const gchar *logical_id) { g_autoptr(GRWLockReaderLocker) locker = NULL; if (physical_id == NULL) return NULL; locker = g_rw_lock_reader_locker_new(&self->devices_mutex); g_return_val_if_fail(locker != NULL, NULL); for (guint i = 0; i < self->devices->len; i++) { FuDeviceItem *item_tmp = g_ptr_array_index(self->devices, i); FuDevice *device = item_tmp->device; if (device != NULL && g_strcmp0(fu_device_get_physical_id(device), physical_id) == 0 && g_strcmp0(fu_device_get_logical_id(device), logical_id) == 0) return item_tmp; } for (guint i = 0; i < self->devices->len; i++) { FuDeviceItem *item_tmp = g_ptr_array_index(self->devices, i); FuDevice *device = item_tmp->device_old; if (device != NULL && g_strcmp0(fu_device_get_physical_id(device), physical_id) == 0 && g_strcmp0(fu_device_get_logical_id(device), logical_id) == 0) return item_tmp; } return NULL; } static gint fu_device_list_item_sort_by_priority_cb(gconstpointer a, gconstpointer b) { const FuDeviceItem *item1 = *((FuDeviceItem **)a); const FuDeviceItem *item2 = *((FuDeviceItem **)b); if (fu_device_get_priority(item1->device) < fu_device_get_priority(item2->device)) return 1; if (fu_device_get_priority(item1->device) > fu_device_get_priority(item2->device)) return -1; return 0; } static GPtrArray * fu_device_list_filter_by_id(FuDeviceList *self, const gchar *device_id, GError **error) { gsize device_id_len; g_autoptr(GPtrArray) items = g_ptr_array_new(); g_return_val_if_fail(device_id != NULL, NULL); /* support abbreviated hashes */ device_id_len = strlen(device_id); g_rw_lock_reader_lock(&self->devices_mutex); for (guint i = 0; i < self->devices->len; i++) { FuDeviceItem *item_tmp = g_ptr_array_index(self->devices, i); const gchar *ids[] = {fu_device_get_id(item_tmp->device), fu_device_get_equivalent_id(item_tmp->device), NULL}; for (guint j = 0; ids[j] != NULL; j++) { if (strncmp(ids[j], device_id, device_id_len) == 0) { g_ptr_array_add(items, item_tmp); break; } } } g_rw_lock_reader_unlock(&self->devices_mutex); if (items->len > 0) { g_ptr_array_sort(items, fu_device_list_item_sort_by_priority_cb); return g_steal_pointer(&items); } /* only search old devices if we didn't find the active device */ g_rw_lock_reader_lock(&self->devices_mutex); for (guint i = 0; i < self->devices->len; i++) { FuDeviceItem *item_tmp = g_ptr_array_index(self->devices, i); const gchar *ids[3] = {NULL}; if (item_tmp->device_old == NULL) continue; ids[0] = fu_device_get_id(item_tmp->device_old); ids[1] = fu_device_get_equivalent_id(item_tmp->device_old); for (guint j = 0; ids[j] != NULL; j++) { if (strncmp(ids[j], device_id, device_id_len) == 0) { g_ptr_array_add(items, item_tmp); break; } } } g_rw_lock_reader_unlock(&self->devices_mutex); if (items->len > 0) { g_ptr_array_sort(items, fu_device_list_item_sort_by_priority_cb); return g_steal_pointer(&items); } /* failed */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "device ID %s was not found", device_id); return NULL; } static FuDeviceItem * fu_device_list_find_by_id(FuDeviceList *self, const gchar *device_id, GError **error) { FuDeviceItem *item0; g_autoptr(GPtrArray) items = NULL; items = fu_device_list_filter_by_id(self, device_id, error); if (items == NULL) return NULL; /* check there are not more devices that have the same priority */ item0 = g_ptr_array_index(items, 0); for (guint i = 1; i < items->len; i++) { FuDeviceItem *item_tmp = g_ptr_array_index(items, i); if (fu_device_get_priority(item_tmp->device) == fu_device_get_priority(item0->device)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device ID %s was not unique", device_id); return NULL; } } return item0; } /** * fu_device_list_get_old: * @self: a device list * @device: a device * * Returns the old device associated with the currently active device. * * Returns: (transfer full): the device, or %NULL if not found * * Since: 1.0.3 **/ FuDevice * fu_device_list_get_old(FuDeviceList *self, FuDevice *device) { FuDeviceItem *item = fu_device_list_find_by_device(self, device); if (item == NULL) return NULL; if (item->device_old == NULL) return NULL; return g_object_ref(item->device_old); } static FuDeviceItem * fu_device_list_get_by_guids_removed(FuDeviceList *self, GPtrArray *guids) { g_autoptr(GRWLockReaderLocker) locker = g_rw_lock_reader_locker_new(&self->devices_mutex); g_return_val_if_fail(locker != NULL, NULL); for (guint i = 0; i < self->devices->len; i++) { FuDeviceItem *item = g_ptr_array_index(self->devices, i); if (item->remove_id == 0) continue; for (guint j = 0; j < guids->len; j++) { const gchar *guid = g_ptr_array_index(guids, j); if (fu_device_has_guid(item->device, guid) || fu_device_has_instance_id(item->device, guid, FU_DEVICE_INSTANCE_FLAG_COUNTERPART | FU_DEVICE_INSTANCE_FLAG_VISIBLE)) return item; } } for (guint i = 0; i < self->devices->len; i++) { FuDeviceItem *item = g_ptr_array_index(self->devices, i); if (item->device_old == NULL) continue; if (item->remove_id == 0) continue; for (guint j = 0; j < guids->len; j++) { const gchar *guid = g_ptr_array_index(guids, j); if (fu_device_has_guid(item->device_old, guid) || fu_device_has_instance_id(item->device_old, guid, FU_DEVICE_INSTANCE_FLAG_COUNTERPART | FU_DEVICE_INSTANCE_FLAG_VISIBLE)) return item; } } return NULL; } static gboolean fu_device_list_device_delayed_remove_cb(gpointer user_data) { FuDeviceItem *item = (FuDeviceItem *)user_data; FuDeviceList *self = FU_DEVICE_LIST(item->self); /* no longer valid */ item->remove_id = 0; /* remove any children associated with device */ if (!fu_device_has_private_flag(item->device, FU_DEVICE_PRIVATE_FLAG_NO_AUTO_REMOVE_CHILDREN)) { GPtrArray *children = fu_device_get_children(item->device); for (guint j = 0; j < children->len; j++) { FuDevice *child = g_ptr_array_index(children, j); FuDeviceItem *child_item; child_item = fu_device_list_find_by_id(self, fu_device_get_id(child), NULL); if (child_item == NULL) { g_info("device %s not found", fu_device_get_id(child)); continue; } fu_device_list_emit_device_removed(self, child); g_rw_lock_writer_lock(&self->devices_mutex); g_ptr_array_remove(self->devices, child_item); g_rw_lock_writer_unlock(&self->devices_mutex); } } /* just remove now */ g_info("doing delayed removal"); fu_device_list_emit_device_removed(self, item->device); g_rw_lock_writer_lock(&self->devices_mutex); g_ptr_array_remove(self->devices, item); g_rw_lock_writer_unlock(&self->devices_mutex); return G_SOURCE_REMOVE; } static void fu_device_list_remove_with_delay(FuDeviceItem *item) { /* give the hardware time to re-enumerate or the user time to * re-insert the device with a magic button pressed */ g_info("waiting %ums for %s device removal", fu_device_get_remove_delay(item->device), fu_device_get_name(item->device)); item->remove_id = g_timeout_add(fu_device_get_remove_delay(item->device), fu_device_list_device_delayed_remove_cb, item); } static gboolean fu_device_list_should_remove_with_delay(FuDevice *device) { if (fu_device_get_remove_delay(device) == 0) return FALSE; if (!fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_DELAYED_REMOVAL) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG)) return FALSE; return TRUE; } /** * fu_device_list_remove: * @self: a device list * @device: a device * * Removes a specific device from the list if it exists. * * If the @device has a remove-delay set then a timeout will be started. If * the exact same #FuDevice is added to the list with fu_device_list_add() * within the timeout then only a ::changed signal will be emitted. * * If there is no remove-delay set, the ::removed signal will be emitted * straight away. * * Since: 1.0.2 **/ void fu_device_list_remove(FuDeviceList *self, FuDevice *device) { FuDeviceItem *item; g_return_if_fail(FU_IS_DEVICE_LIST(self)); g_return_if_fail(FU_IS_DEVICE(device)); /* check the device already exists */ item = fu_device_list_find_by_id(self, fu_device_get_id(device), NULL); if (item == NULL) { g_info("device %s not found", fu_device_get_id(device)); return; } /* we can't do anything with an unconnected device */ fu_device_add_private_flag(item->device, FU_DEVICE_PRIVATE_FLAG_UNCONNECTED); /* ensure never fired if the remove delay is changed */ if (item->remove_id > 0) { g_source_remove(item->remove_id); item->remove_id = 0; } /* delay the removal and check for replug */ if (fu_device_list_should_remove_with_delay(item->device)) { fu_device_list_remove_with_delay(item); return; } /* remove any children associated with device */ if (!fu_device_has_private_flag(item->device, FU_DEVICE_PRIVATE_FLAG_NO_AUTO_REMOVE_CHILDREN)) { GPtrArray *children = fu_device_get_children(device); for (guint j = 0; j < children->len; j++) { FuDevice *child = g_ptr_array_index(children, j); FuDeviceItem *child_item; child_item = fu_device_list_find_by_id(self, fu_device_get_id(child), NULL); if (child_item == NULL) { g_info("device %s not found", fu_device_get_id(child)); continue; } fu_device_list_emit_device_removed(self, child); g_rw_lock_writer_lock(&self->devices_mutex); g_ptr_array_remove(self->devices, child_item); g_rw_lock_writer_unlock(&self->devices_mutex); } } /* remove right now */ fu_device_list_emit_device_removed(self, item->device); g_rw_lock_writer_lock(&self->devices_mutex); g_ptr_array_remove(self->devices, item); g_rw_lock_writer_unlock(&self->devices_mutex); } /** * fu_device_list_remove_all: * @self: a device list * * Removes all devices from the list. * * Since: 2.0.0 **/ void fu_device_list_remove_all(FuDeviceList *self) { g_return_if_fail(FU_IS_DEVICE_LIST(self)); g_rw_lock_writer_lock(&self->devices_mutex); g_ptr_array_set_size(self->devices, 0); g_rw_lock_writer_unlock(&self->devices_mutex); } static void fu_device_list_add_missing_guids(FuDevice *device_new, FuDevice *device_old) { GPtrArray *guids_old = fu_device_get_guids(device_old); for (guint i = 0; i < guids_old->len; i++) { const gchar *guid_tmp = g_ptr_array_index(guids_old, i); if (!fu_device_has_guid(device_new, guid_tmp) && !fu_device_has_instance_id(device_new, guid_tmp, FU_DEVICE_INSTANCE_FLAG_COUNTERPART)) { if (fu_device_has_private_flag( device_new, FU_DEVICE_PRIVATE_FLAG_ADD_COUNTERPART_GUIDS)) { g_info("adding GUID %s to device", guid_tmp); fu_device_add_instance_id_full(device_new, guid_tmp, FU_DEVICE_INSTANCE_FLAG_COUNTERPART); } else { g_info("not adding GUID %s to device, use " "FU_DEVICE_PRIVATE_FLAG_ADD_COUNTERPART_GUIDS if required", guid_tmp); } } } } static void fu_device_list_item_finalized_cb(gpointer data, GObject *where_the_object_was) { FuDeviceItem *item = (FuDeviceItem *)data; FuDeviceList *self = FU_DEVICE_LIST(item->self); g_critical("FuDevice %p was finalized without being removed from " "FuDeviceList, removing item!", where_the_object_was); g_rw_lock_writer_lock(&self->devices_mutex); g_ptr_array_remove(self->devices, item); g_rw_lock_writer_unlock(&self->devices_mutex); } static void fu_device_list_item_set_device_old(FuDeviceItem *item, FuDevice *device) { fu_device_set_parent(device, NULL); fu_device_remove_children(device); g_set_object(&item->device_old, device); } /* this should never be required, and yet here we are */ static void fu_device_list_item_set_device(FuDeviceItem *item, FuDevice *device) { if (item->device != NULL) { g_object_weak_unref(G_OBJECT(item->device), fu_device_list_item_finalized_cb, item); } if (device != NULL) { g_object_weak_ref(G_OBJECT(device), fu_device_list_item_finalized_cb, item); } g_set_object(&item->device, device); } static void fu_device_list_clear_wait_for_replug(FuDeviceList *self, FuDeviceItem *item) { g_autofree gchar *str = NULL; /* clear timeout if scheduled */ if (item->remove_id != 0) { g_source_remove(item->remove_id); item->remove_id = 0; } /* remove flag on both old and new devices */ if (fu_device_has_flag(item->device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG)) { g_info("%s device came back, clearing flag", fu_device_get_id(item->device)); fu_device_remove_flag(item->device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); } if (item->device_old != NULL) { if (fu_device_has_flag(item->device_old, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG)) { g_info("%s old device came back, clearing flag", fu_device_get_id(item->device_old)); fu_device_remove_flag(item->device_old, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); } } fu_device_remove_private_flag(item->device, FU_DEVICE_PRIVATE_FLAG_UNCONNECTED); /* debug */ str = fwupd_codec_to_string(FWUPD_CODEC(self)); g_debug("\n%s", str); } static void _fu_device_incorporate_problem_update_in_progress(FuDevice *self, FuDevice *donor) { if (fu_device_has_problem(donor, FWUPD_DEVICE_PROBLEM_UPDATE_IN_PROGRESS)) { g_info("moving inhibit update-in-progress to active device"); fu_device_add_problem(self, FWUPD_DEVICE_PROBLEM_UPDATE_IN_PROGRESS); fu_device_remove_problem(donor, FWUPD_DEVICE_PROBLEM_UPDATE_IN_PROGRESS); } } static void fu_device_list_replace(FuDeviceList *self, FuDeviceItem *item, FuDevice *device) { GPtrArray *children = fu_device_get_children(item->device); g_autofree gchar *str = NULL; /* run the optional device-specific subclass */ fu_device_replace(device, item->device); /* copy over any GUIDs that used to exist */ fu_device_list_add_missing_guids(device, item->device); /* incorporate properties from the old device */ fu_device_incorporate(device, item->device, FU_DEVICE_INCORPORATE_FLAG_VENDOR_IDS | FU_DEVICE_INCORPORATE_FLAG_UPDATE_ERROR | FU_DEVICE_INCORPORATE_FLAG_UPDATE_STATE); /* copy inhibit */ _fu_device_incorporate_problem_update_in_progress(item->device, device); /* copy over the version strings if not set */ if (fu_device_get_version(item->device) != NULL && fu_device_get_version(device) == NULL) { const gchar *version = fu_device_get_version(item->device); guint64 raw = fu_device_get_version_raw(item->device); g_info("copying old version %s to new device", version); fu_device_set_version_format(device, fu_device_get_version_format(item->device)); fu_device_set_version(device, version); /* nocheck:set-version */ fu_device_set_version_raw(device, raw); } /* always use the runtime version */ if (fu_device_has_private_flag(item->device, FU_DEVICE_PRIVATE_FLAG_USE_RUNTIME_VERSION) && fu_device_has_flag(item->device, FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER)) { const gchar *version = fu_device_get_version(item->device); guint64 raw = fu_device_get_version_raw(item->device); g_info("forcing runtime version %s to new device", version); fu_device_set_version_format(device, fu_device_get_version_format(item->device)); fu_device_set_version(device, version); /* nocheck:set-version */ fu_device_set_version_raw(device, raw); } /* seems like a sane assumption if we've tagged the runtime mode as signed */ fu_device_incorporate_flag(device, item->device, FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); fu_device_incorporate_flag(device, item->device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); /* never unset */ if (fu_device_has_flag(item->device, FWUPD_DEVICE_FLAG_EMULATION_TAG)) fu_device_add_flag(device, FWUPD_DEVICE_FLAG_EMULATION_TAG); /* device won't come back in right mode */ fu_device_incorporate_flag(device, item->device, FWUPD_DEVICE_FLAG_WILL_DISAPPEAR); /* copy the parent if not already set */ if (fu_device_get_parent(item->device) != NULL && fu_device_get_parent(item->device) != device && fu_device_get_parent(device) != item->device && fu_device_get_parent(device) == NULL) { FuDevice *parent = fu_device_get_parent(item->device); g_info("copying parent %s to new device", fu_device_get_id(parent)); fu_device_set_parent(device, parent); } /* copy the children */ for (guint i = 0; i < children->len; i++) { FuDevice *child = g_ptr_array_index(children, i); g_info("copying child %s to new device", fu_device_get_id(child)); fu_device_add_child(device, child); } /* assign the new device */ fu_device_list_item_set_device_old(item, item->device); fu_device_list_item_set_device(item, device); fu_device_list_emit_device_changed(self, device); /* debug */ str = fwupd_codec_to_string(FWUPD_CODEC(self)); g_debug("\n%s", str); /* we were waiting for this... */ fu_device_list_clear_wait_for_replug(self, item); } /** * fu_device_list_add: * @self: a device list * @device: a device * * Adds a specific device to the device list if not already present. * * If the @device (or a compatible @device) has been previously removed within * the remove-timeout then only the ::changed signal will be emitted on calling * this function. Otherwise the ::added signal will be emitted straight away. * * Compatible devices are defined as #FuDevice objects that share at least one * device GUID. If a compatible device is matched then the vendor ID and * version will be copied to the new object if they are not already set. * * Any GUIDs present on the old device and not on the new device will be * inherited and do not have to be copied over by plugins manually. * * Returns: (transfer none): a device, or %NULL if not found * * Since: 1.0.2 **/ void fu_device_list_add(FuDeviceList *self, FuDevice *device) { FuDeviceItem *item; g_return_if_fail(FU_IS_DEVICE_LIST(self)); g_return_if_fail(FU_IS_DEVICE(device)); /* make tests easier */ fu_device_convert_instance_ids(device); /* is the device waiting to be replugged? */ item = fu_device_list_find_by_id(self, fu_device_get_id(device), NULL); if (item != NULL) { /* literally the same object */ if (device == item->device) { g_info("found existing device %s", fu_device_get_id(device)); fu_device_list_clear_wait_for_replug(self, item); fu_device_list_emit_device_changed(self, device); return; } /* the old device again */ if (item->device_old != NULL && device == item->device_old) { g_info("found old device %s, swapping", fu_device_get_id(device)); fu_device_remove_private_flag(item->device, FU_DEVICE_PRIVATE_FLAG_UNCONNECTED); _fu_device_incorporate_problem_update_in_progress(device, item->device); fu_device_incorporate(item->device, device, FU_DEVICE_INCORPORATE_FLAG_UPDATE_ERROR | FU_DEVICE_INCORPORATE_FLAG_UPDATE_ERROR); g_set_object(&item->device_old, item->device); fu_device_list_item_set_device(item, device); fu_device_list_clear_wait_for_replug(self, item); fu_device_list_emit_device_changed(self, device); return; } /* adding a device with a lower priority */ if (!fu_device_has_flag(item->device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG) && fu_device_get_priority(item->device) > fu_device_get_priority(device) && g_strcmp0(fu_device_get_plugin(item->device), fu_device_get_plugin(device)) != 0) { g_info("ignoring %s from %s as a higher prio device from %s already exists", fu_device_get_id(device), fu_device_get_plugin(device), fu_device_get_plugin(item->device)); fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_EMULATION_TAG); return; } /* same ID, different object */ g_info("found existing device %s, reusing item", fu_device_get_id(item->device)); fu_device_list_replace(self, item, device); fu_device_remove_private_flag(device, FU_DEVICE_PRIVATE_FLAG_UNCONNECTED); return; } /* verify a device with same connection does not already exist */ item = fu_device_list_find_by_connection(self, fu_device_get_physical_id(device), fu_device_get_logical_id(device)); if (item != NULL && item->remove_id != 0) { g_info("found physical device %s recently removed, reusing " "item from plugin %s for plugin %s", fu_device_get_id(item->device), fu_device_get_plugin(item->device), fu_device_get_plugin(device)); fu_device_list_replace(self, item, device); fu_device_remove_private_flag(device, FU_DEVICE_PRIVATE_FLAG_UNCONNECTED); return; } /* verify a compatible device does not already exist */ item = fu_device_list_get_by_guids_removed(self, fu_device_get_guids(device)); if (item == NULL) { g_autoptr(GPtrArray) guids = fu_device_get_counterpart_guids(device); item = fu_device_list_get_by_guids_removed(self, guids); } if (item != NULL) { if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID)) { g_info("found compatible device %s recently removed, reusing " "item from plugin %s for plugin %s", fu_device_get_id(item->device), fu_device_get_plugin(item->device), fu_device_get_plugin(device)); fu_device_list_replace(self, item, device); fu_device_remove_private_flag(device, FU_DEVICE_PRIVATE_FLAG_UNCONNECTED); return; } g_info("not adding matching %s for device add, use " "FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID if required", fu_device_get_id(item->device)); } /* this can never be true */ fu_device_remove_private_flag(device, FU_DEVICE_PRIVATE_FLAG_UNCONNECTED); /* add helper */ item = g_new0(FuDeviceItem, 1); item->self = self; /* no ref */ fu_device_list_item_set_device(item, device); g_rw_lock_writer_lock(&self->devices_mutex); g_ptr_array_add(self->devices, item); g_rw_lock_writer_unlock(&self->devices_mutex); fu_device_list_emit_device_added(self, device); } /** * fu_device_list_get_by_guid: * @self: a device list * @guid: a device GUID * @error: (nullable): optional return location for an error * * Finds a specific device that has the matching GUID. * * Returns: (transfer full): a device, or %NULL if not found * * Since: 1.0.2 **/ FuDevice * fu_device_list_get_by_guid(FuDeviceList *self, const gchar *guid, GError **error) { FuDeviceItem *item; g_return_val_if_fail(FU_IS_DEVICE_LIST(self), NULL); g_return_val_if_fail(guid != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); item = fu_device_list_find_by_guid(self, guid); if (item != NULL) return g_object_ref(item->device); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "GUID %s was not found", guid); return NULL; } static GPtrArray * fu_device_list_get_wait_for_replug(FuDeviceList *self) { GPtrArray *devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); for (guint i = 0; i < self->devices->len; i++) { FuDeviceItem *item_tmp = g_ptr_array_index(self->devices, i); if (fu_device_has_flag(item_tmp->device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG) && !fu_device_has_flag(item_tmp->device, FWUPD_DEVICE_FLAG_EMULATED)) g_ptr_array_add(devices, g_object_ref(item_tmp->device)); } return devices; } /** * fu_device_list_wait_for_replug: * @self: a device list * @error: (nullable): optional return location for an error * * Waits for all the devices with %FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG to replug. * * If the device does not exist this function returns without an error. * * Returns: %TRUE for success * * Since: 1.1.2 **/ gboolean fu_device_list_wait_for_replug(FuDeviceList *self, GError **error) { guint remove_delay = 0; g_autoptr(GTimer) timer = g_timer_new(); g_autoptr(GPtrArray) devices_wfr1 = NULL; g_autoptr(GPtrArray) devices_wfr2 = NULL; g_return_val_if_fail(FU_IS_DEVICE_LIST(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* not required, or possibly literally just happened */ devices_wfr1 = fu_device_list_get_wait_for_replug(self); if (devices_wfr1->len == 0) { g_info("no replug or re-enumerate required"); return TRUE; } /* use the maximum of all the devices */ for (guint i = 0; i < devices_wfr1->len; i++) { FuDevice *device_tmp = g_ptr_array_index(devices_wfr1, i); if (fu_device_get_remove_delay(device_tmp) > remove_delay) remove_delay = fu_device_get_remove_delay(device_tmp); } /* plugin did not specify */ if (remove_delay == 0) { remove_delay = FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE; g_warning("plugin did not specify a remove delay, " "so guessing we should wait %ums for replug", remove_delay); } else { g_info("waiting %ums for replug", remove_delay); } /* time to unplug and then re-plug */ do { g_autoptr(GPtrArray) devices_wfr_tmp = NULL; g_usleep(1000); g_main_context_iteration(NULL, FALSE); devices_wfr_tmp = fu_device_list_get_wait_for_replug(self); if (devices_wfr_tmp->len == 0) break; } while (g_timer_elapsed(timer, NULL) * 1000.f < remove_delay); /* check that no other devices are still waiting for replug */ devices_wfr2 = fu_device_list_get_wait_for_replug(self); if (devices_wfr2->len > 0) { g_autoptr(GPtrArray) device_ids = g_ptr_array_new_with_free_func(g_free); g_autofree gchar *device_ids_str = NULL; g_autofree gchar *str = NULL; /* dump to console */ str = fwupd_codec_to_string(FWUPD_CODEC(self)); g_debug("\n%s", str); /* unset and build error string */ for (guint i = 0; i < devices_wfr2->len; i++) { FuDevice *device_tmp = g_ptr_array_index(devices_wfr2, i); fu_device_remove_flag(device_tmp, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); g_ptr_array_add(device_ids, g_strdup(fu_device_get_id(device_tmp))); } device_ids_str = fu_strjoin(",", device_ids); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "device %s did not come back", device_ids_str); return FALSE; } /* the loop was quit without the timer */ g_info("waited for replug"); return TRUE; } /** * fu_device_list_get_by_id: * @self: a device list * @device_id: a device ID, typically a SHA1 hash * @error: (nullable): optional return location for an error * * Finds a specific device using the ID string. This function also supports * using abbreviated hashes. * * Returns: (transfer full): a device, or %NULL if not found * * Since: 1.0.2 **/ FuDevice * fu_device_list_get_by_id(FuDeviceList *self, const gchar *device_id, GError **error) { FuDeviceItem *item; g_return_val_if_fail(FU_IS_DEVICE_LIST(self), NULL); g_return_val_if_fail(device_id != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* multiple things matched */ item = fu_device_list_find_by_id(self, device_id, error); if (item == NULL) return NULL; /* something found */ return g_object_ref(item->device); } static void fu_device_list_item_free(FuDeviceItem *item) { if (item->remove_id != 0) g_source_remove(item->remove_id); if (item->device_old != NULL) g_object_unref(item->device_old); fu_device_list_item_set_device(item, NULL); g_free(item); } static void fu_device_list_codec_iface_init(FwupdCodecInterface *iface) { iface->add_string = fu_device_list_add_string; } static void fu_device_list_dispose(GObject *obj) { FuDeviceList *self = FU_DEVICE_LIST(obj); if (self->devices != NULL) g_ptr_array_set_size(self->devices, 0); G_OBJECT_CLASS(fu_device_list_parent_class)->dispose(obj); } static void fu_device_list_class_init(FuDeviceListClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->dispose = fu_device_list_dispose; object_class->finalize = fu_device_list_finalize; /** * FuDeviceList::added: * @self: the #FuDeviceList instance that emitted the signal * @device: the #FuDevice * * The ::added signal is emitted when a device has been added to the list. **/ signals[SIGNAL_ADDED] = g_signal_new("added", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, FU_TYPE_DEVICE); /** * FuDeviceList::removed: * @self: the #FuDeviceList instance that emitted the signal * @device: the #FuDevice * * The ::removed signal is emitted when a device has been removed from the list. **/ signals[SIGNAL_REMOVED] = g_signal_new("removed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, FU_TYPE_DEVICE); /** * FuDeviceList::changed: * @self: the #FuDeviceList instance that emitted the signal * @device: the #FuDevice * * The ::changed signal is emitted when a device has changed. **/ signals[SIGNAL_CHANGED] = g_signal_new("changed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, FU_TYPE_DEVICE); } static void fu_device_list_init(FuDeviceList *self) { self->devices = g_ptr_array_new_with_free_func((GDestroyNotify)fu_device_list_item_free); g_rw_lock_init(&self->devices_mutex); } static void fu_device_list_finalize(GObject *obj) { FuDeviceList *self = FU_DEVICE_LIST(obj); g_rw_lock_clear(&self->devices_mutex); g_ptr_array_unref(self->devices); G_OBJECT_CLASS(fu_device_list_parent_class)->finalize(obj); } /** * fu_device_list_new: * * Creates a new device list. * * Returns: (transfer full): a device list * * Since: 1.0.2 **/ FuDeviceList * fu_device_list_new(void) { FuDeviceList *self; self = g_object_new(FU_TYPE_DEVICE_LIST, NULL); return FU_DEVICE_LIST(self); } fwupd-2.0.10/src/fu-device-list.h000066400000000000000000000023461501337203100164770ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_DEVICE_LIST (fu_device_list_get_type()) G_DECLARE_FINAL_TYPE(FuDeviceList, fu_device_list, FU, DEVICE_LIST, GObject) FuDeviceList * fu_device_list_new(void); void fu_device_list_add(FuDeviceList *self, FuDevice *device) G_GNUC_NON_NULL(1, 2); void fu_device_list_remove(FuDeviceList *self, FuDevice *device) G_GNUC_NON_NULL(1, 2); void fu_device_list_remove_all(FuDeviceList *self) G_GNUC_NON_NULL(1); GPtrArray * fu_device_list_get_all(FuDeviceList *self) G_GNUC_NON_NULL(1); GPtrArray * fu_device_list_get_active(FuDeviceList *self) G_GNUC_NON_NULL(1); FuDevice * fu_device_list_get_old(FuDeviceList *self, FuDevice *device) G_GNUC_NON_NULL(1, 2); FuDevice * fu_device_list_get_by_id(FuDeviceList *self, const gchar *device_id, GError **error) G_GNUC_NON_NULL(1, 2); FuDevice * fu_device_list_get_by_guid(FuDeviceList *self, const gchar *guid, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_device_list_wait_for_replug(FuDeviceList *self, GError **error) G_GNUC_NON_NULL(1); void fu_device_list_depsolve_order(FuDeviceList *self, FuDevice *device) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/src/fu-engine-config.c000066400000000000000000000345121501337203100167720ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuEngineConfig" #include "config.h" #include #include "fu-engine-config.h" struct _FuEngineConfig { FuConfig parent_instance; GPtrArray *disabled_devices; /* (element-type utf-8) */ GPtrArray *disabled_plugins; /* (element-type utf-8) */ GPtrArray *approved_firmware; /* (element-type utf-8) */ GPtrArray *blocked_firmware; /* (element-type utf-8) */ GPtrArray *uri_schemes; /* (element-type utf-8) */ GPtrArray *trusted_reports; /* (element-type FwupdReport) */ GArray *trusted_uids; /* (element-type guint64) */ gchar *host_bkc; gchar *esp_location; }; G_DEFINE_TYPE(FuEngineConfig, fu_engine_config, FU_TYPE_CONFIG) static gboolean fu_engine_config_report_from_flags(FwupdReport *report, const gchar *flags_str, GError **error) { g_auto(GStrv) flags_strv = g_strsplit(flags_str, ",", -1); for (guint i = 0; flags_strv[i] != NULL; i++) { FwupdReportFlags flag = fwupd_report_flag_from_string(flags_strv[i]); if (flag == FWUPD_REPORT_FLAG_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "report flag '%s' unknown", flags_strv[i]); return FALSE; } fwupd_report_add_flag(report, flag); } return TRUE; } static FwupdReport * fu_engine_config_report_from_spec(FuEngineConfig *self, const gchar *report_spec, GError **error) { g_auto(GStrv) parts = g_strsplit(report_spec, "&", -1); g_autoptr(FwupdReport) report = fwupd_report_new(); for (guint i = 0; parts[i] != NULL; i++) { g_autofree gchar *value = NULL; g_auto(GStrv) kv = g_strsplit(parts[i], "=", 2); if (g_strv_length(kv) != 2) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to parse report specifier key=value %s", parts[i]); return NULL; } if (g_str_has_prefix(kv[1], "$")) value = g_get_os_info(kv[1] + 1); if (value == NULL) value = g_strdup(kv[1]); if (g_strcmp0(kv[0], "VendorId") == 0) { guint64 tmp = 0; if (g_strcmp0(value, "$OEM") == 0) { fwupd_report_add_flag(report, FWUPD_REPORT_FLAG_FROM_OEM); } else { if (!fu_strtoull(value, &tmp, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "failed to parse '%s': ", value); return NULL; } fwupd_report_set_vendor_id(report, tmp); } } else if (g_strcmp0(kv[0], "DistroId") == 0) { fwupd_report_set_distro_id(report, value); } else if (g_strcmp0(kv[0], "DistroVariant") == 0) { fwupd_report_set_distro_variant(report, value); } else if (g_strcmp0(kv[0], "DistroVersion") == 0) { fwupd_report_set_distro_version(report, value); } else if (g_strcmp0(kv[0], "RemoteId") == 0) { fwupd_report_set_remote_id(report, value); } else if (g_strcmp0(kv[0], "Flags") == 0) { if (!fu_engine_config_report_from_flags(report, value, error)) return NULL; } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "failed to parse report specifier key %s", kv[0]); return NULL; } } /* success */ return g_steal_pointer(&report); } static void fu_engine_config_reload(FuEngineConfig *self) { g_auto(GStrv) approved_firmware = NULL; g_auto(GStrv) blocked_firmware = NULL; g_auto(GStrv) disabled_devices = NULL; g_auto(GStrv) plugins = NULL; g_auto(GStrv) report_specs = NULL; g_auto(GStrv) uids = NULL; g_auto(GStrv) uri_schemes = NULL; g_autofree gchar *domains = NULL; g_autofree gchar *host_bkc = NULL; g_autofree gchar *esp_location = NULL; /* get disabled devices */ g_ptr_array_set_size(self->disabled_devices, 0); disabled_devices = fu_config_get_value_strv(FU_CONFIG(self), "fwupd", "DisabledDevices"); if (disabled_devices != NULL) { for (guint i = 0; disabled_devices[i] != NULL; i++) g_ptr_array_add(self->disabled_devices, g_strdup(disabled_devices[i])); } /* get disabled plugins */ g_ptr_array_set_size(self->disabled_plugins, 0); plugins = fu_config_get_value_strv(FU_CONFIG(self), "fwupd", "DisabledPlugins"); if (plugins != NULL) { for (guint i = 0; plugins[i] != NULL; i++) { g_autofree gchar *plugin_name = fu_strstrip(plugins[i]); if (plugin_name == NULL || plugin_name[0] == '\0') continue; g_strdelimit(plugin_name, "-", '_'); g_ptr_array_add(self->disabled_plugins, g_steal_pointer(&plugin_name)); } } /* get approved firmware */ g_ptr_array_set_size(self->approved_firmware, 0); approved_firmware = fu_config_get_value_strv(FU_CONFIG(self), "fwupd", "ApprovedFirmware"); if (approved_firmware != NULL) { for (guint i = 0; approved_firmware[i] != NULL; i++) g_ptr_array_add(self->approved_firmware, g_strdup(approved_firmware[i])); } /* get blocked firmware */ g_ptr_array_set_size(self->blocked_firmware, 0); blocked_firmware = fu_config_get_value_strv(FU_CONFIG(self), "fwupd", "BlockedFirmware"); if (blocked_firmware != NULL) { for (guint i = 0; blocked_firmware[i] != NULL; i++) g_ptr_array_add(self->blocked_firmware, g_strdup(blocked_firmware[i])); } /* get download schemes */ g_ptr_array_set_size(self->uri_schemes, 0); uri_schemes = fu_config_get_value_strv(FU_CONFIG(self), "fwupd", "UriSchemes"); if (uri_schemes != NULL) { for (guint i = 0; uri_schemes[i] != NULL; i++) g_ptr_array_add(self->uri_schemes, g_strdup(uri_schemes[i])); } /* get the domains to run in verbose */ domains = fu_config_get_value(FU_CONFIG(self), "fwupd", "VerboseDomains"); if (domains != NULL && domains[0] != '\0') (void)g_setenv("FWUPD_VERBOSE", domains, FALSE); /* fetch host best known configuration */ host_bkc = fu_config_get_value(FU_CONFIG(self), "fwupd", "HostBkc"); if (host_bkc != NULL && host_bkc[0] != '\0') self->host_bkc = g_steal_pointer(&host_bkc); /* fetch hardcoded ESP mountpoint */ esp_location = fu_config_get_value(FU_CONFIG(self), "fwupd", "EspLocation"); if (esp_location != NULL && esp_location[0] != '\0') self->esp_location = g_steal_pointer(&esp_location); /* get trusted uids */ g_array_set_size(self->trusted_uids, 0); uids = fu_config_get_value_strv(FU_CONFIG(self), "fwupd", "TrustedUids"); if (uids != NULL) { for (guint i = 0; uids[i] != NULL; i++) { guint64 val = 0; g_autoptr(GError) error_local = NULL; if (!fu_strtoull(uids[i], &val, 0, G_MAXUINT64, FU_INTEGER_BASE_AUTO, &error_local)) { g_warning("failed to parse UID '%s': %s", uids[i], error_local->message); continue; } g_array_append_val(self->trusted_uids, val); } } /* get trusted reports */ g_ptr_array_set_size(self->trusted_reports, 0); report_specs = fu_config_get_value_strv(FU_CONFIG(self), "fwupd", "TrustedReports"); if (report_specs != NULL) { for (guint i = 0; report_specs[i] != NULL; i++) { g_autoptr(GError) error_local = NULL; FwupdReport *report = fu_engine_config_report_from_spec(self, report_specs[i], &error_local); if (report == NULL) { g_warning("failed to parse %s: %s", report_specs[i], error_local->message); continue; } g_ptr_array_add(self->trusted_reports, report); } } } static void fu_engine_config_changed_cb(FuEngineConfig *config, gpointer user_data) { FuEngineConfig *self = FU_ENGINE_CONFIG(config); fu_engine_config_reload(self); } guint fu_engine_config_get_idle_timeout(FuEngineConfig *self) { return fu_config_get_value_u64(FU_CONFIG(self), "fwupd", "IdleTimeout"); } GPtrArray * fu_engine_config_get_disabled_devices(FuEngineConfig *self) { g_return_val_if_fail(FU_IS_ENGINE_CONFIG(self), NULL); return self->disabled_devices; } GArray * fu_engine_config_get_trusted_uids(FuEngineConfig *self) { g_return_val_if_fail(FU_IS_ENGINE_CONFIG(self), NULL); return self->trusted_uids; } GPtrArray * fu_engine_config_get_trusted_reports(FuEngineConfig *self) { g_return_val_if_fail(FU_IS_CONFIG(self), NULL); return self->trusted_reports; } GPtrArray * fu_engine_config_get_blocked_firmware(FuEngineConfig *self) { g_return_val_if_fail(FU_IS_ENGINE_CONFIG(self), NULL); return self->blocked_firmware; } guint fu_engine_config_get_uri_scheme_prio(FuEngineConfig *self, const gchar *scheme) { guint idx = 0; if (!g_ptr_array_find_with_equal_func(self->uri_schemes, scheme, g_str_equal, &idx)) return G_MAXUINT; return idx; } guint64 fu_engine_config_get_archive_size_max(FuEngineConfig *self) { return fu_config_get_value_u64(FU_CONFIG(self), "fwupd", "ArchiveSizeMax"); } GPtrArray * fu_engine_config_get_disabled_plugins(FuEngineConfig *self) { g_return_val_if_fail(FU_IS_ENGINE_CONFIG(self), NULL); return self->disabled_plugins; } GPtrArray * fu_engine_config_get_approved_firmware(FuEngineConfig *self) { g_return_val_if_fail(FU_IS_ENGINE_CONFIG(self), NULL); return self->approved_firmware; } gboolean fu_engine_config_get_update_motd(FuEngineConfig *self) { return fu_config_get_value_bool(FU_CONFIG(self), "fwupd", "UpdateMotd"); } gboolean fu_engine_config_get_ignore_power(FuEngineConfig *self) { return fu_config_get_value_bool(FU_CONFIG(self), "fwupd", "IgnorePower"); } gboolean fu_engine_config_get_only_trusted(FuEngineConfig *self) { return fu_config_get_value_bool(FU_CONFIG(self), "fwupd", "OnlyTrusted"); } gboolean fu_engine_config_get_show_device_private(FuEngineConfig *self) { return fu_config_get_value_bool(FU_CONFIG(self), "fwupd", "ShowDevicePrivate"); } gboolean fu_engine_config_get_test_devices(FuEngineConfig *self) { return fu_config_get_value_bool(FU_CONFIG(self), "fwupd", "TestDevices"); } gboolean fu_engine_config_get_ignore_requirements(FuEngineConfig *self) { return fu_config_get_value_bool(FU_CONFIG(self), "fwupd", "IgnoreRequirements"); } gboolean fu_engine_config_get_release_dedupe(FuEngineConfig *self) { return fu_config_get_value_bool(FU_CONFIG(self), "fwupd", "ReleaseDedupe"); } FuReleasePriority fu_engine_config_get_release_priority(FuEngineConfig *self) { g_autofree gchar *tmp = fu_config_get_value(FU_CONFIG(self), "fwupd", "ReleasePriority"); return fu_release_priority_from_string(tmp); } FuP2pPolicy fu_engine_config_get_p2p_policy(FuEngineConfig *self) { FuP2pPolicy p2p_policy = FU_P2P_POLICY_NOTHING; g_autofree gchar *tmp = fu_config_get_value(FU_CONFIG(self), "fwupd", "P2pPolicy"); g_auto(GStrv) split = g_strsplit(tmp, ",", -1); for (guint i = 0; split[i] != NULL; i++) p2p_policy |= fu_p2p_policy_from_string(split[i]); return p2p_policy; } gboolean fu_engine_config_get_enumerate_all_devices(FuEngineConfig *self) { return fu_config_get_value_bool(FU_CONFIG(self), "fwupd", "EnumerateAllDevices"); } const gchar * fu_engine_config_get_host_bkc(FuEngineConfig *self) { g_return_val_if_fail(FU_IS_ENGINE_CONFIG(self), NULL); return self->host_bkc; } const gchar * fu_engine_config_get_esp_location(FuEngineConfig *self) { g_return_val_if_fail(FU_IS_ENGINE_CONFIG(self), NULL); return self->esp_location; } static gchar * fu_engine_config_archive_size_max_default(void) { guint64 memory_size = fu_common_get_memory_size(); guint64 archive_size_max = memory_size > 0 ? MIN(memory_size / 4, G_MAXUINT32) : 512 * 0x100000; return g_strdup_printf("%" G_GUINT64_FORMAT, archive_size_max); } static void fu_engine_config_set_default(FuEngineConfig *self, const gchar *key, const gchar *value) { fu_config_set_default(FU_CONFIG(self), "fwupd", key, value); } static void fu_engine_config_init(FuEngineConfig *self) { g_autofree gchar *archive_size_max_default = fu_engine_config_archive_size_max_default(); self->disabled_devices = g_ptr_array_new_with_free_func(g_free); self->disabled_plugins = g_ptr_array_new_with_free_func(g_free); self->approved_firmware = g_ptr_array_new_with_free_func(g_free); self->blocked_firmware = g_ptr_array_new_with_free_func(g_free); self->trusted_uids = g_array_new(FALSE, FALSE, sizeof(guint64)); self->trusted_reports = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); self->uri_schemes = g_ptr_array_new_with_free_func(g_free); g_signal_connect(self, "loaded", G_CALLBACK(fu_engine_config_changed_cb), NULL); g_signal_connect(self, "changed", G_CALLBACK(fu_engine_config_changed_cb), NULL); /* defaults changed here will also be reflected in the fwupd.conf man page */ fu_engine_config_set_default(self, "ApprovedFirmware", NULL); fu_engine_config_set_default(self, "ArchiveSizeMax", archive_size_max_default); fu_engine_config_set_default(self, "BlockedFirmware", NULL); fu_engine_config_set_default(self, "DisabledDevices", NULL); fu_engine_config_set_default(self, "DisabledPlugins", ""); fu_engine_config_set_default(self, "EnumerateAllDevices", "false"); fu_engine_config_set_default(self, "EspLocation", NULL); fu_engine_config_set_default(self, "HostBkc", NULL); fu_engine_config_set_default(self, "IdleTimeout", "300"); /* s */ fu_engine_config_set_default(self, "IdleInhibitStartupThreshold", "500"); /* ms */ fu_engine_config_set_default(self, "IgnorePower", "false"); fu_engine_config_set_default(self, "IgnoreRequirements", "false"); fu_engine_config_set_default(self, "OnlyTrusted", "true"); fu_engine_config_set_default(self, "P2pPolicy", FU_DEFAULT_P2P_POLICY); fu_engine_config_set_default(self, "ReleaseDedupe", "true"); fu_engine_config_set_default(self, "ReleasePriority", "local"); fu_engine_config_set_default(self, "ShowDevicePrivate", "true"); fu_engine_config_set_default(self, "TestDevices", "false"); fu_engine_config_set_default(self, "TrustedReports", "VendorId=$OEM"); fu_engine_config_set_default(self, "TrustedUids", NULL); fu_engine_config_set_default(self, "UpdateMotd", "true"); fu_engine_config_set_default(self, "UriSchemes", "file;https;http;ipfs"); fu_engine_config_set_default(self, "VerboseDomains", NULL); } static void fu_engine_config_finalize(GObject *obj) { FuEngineConfig *self = FU_ENGINE_CONFIG(obj); g_ptr_array_unref(self->disabled_devices); g_ptr_array_unref(self->disabled_plugins); g_ptr_array_unref(self->approved_firmware); g_ptr_array_unref(self->blocked_firmware); g_ptr_array_unref(self->uri_schemes); g_ptr_array_unref(self->trusted_reports); g_array_unref(self->trusted_uids); g_free(self->host_bkc); g_free(self->esp_location); G_OBJECT_CLASS(fu_engine_config_parent_class)->finalize(obj); } static void fu_engine_config_class_init(FuEngineConfigClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_engine_config_finalize; } FuEngineConfig * fu_engine_config_new(void) { return FU_ENGINE_CONFIG(g_object_new(FU_TYPE_ENGINE_CONFIG, NULL)); } fwupd-2.0.10/src/fu-engine-config.h000066400000000000000000000043311501337203100167730ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-engine-struct.h" #define FU_TYPE_ENGINE_CONFIG (fu_engine_config_get_type()) G_DECLARE_FINAL_TYPE(FuEngineConfig, fu_engine_config, FU, ENGINE_CONFIG, FuConfig) FuEngineConfig * fu_engine_config_new(void); guint64 fu_engine_config_get_archive_size_max(FuEngineConfig *self) G_GNUC_NON_NULL(1); guint fu_engine_config_get_idle_timeout(FuEngineConfig *self) G_GNUC_NON_NULL(1); GPtrArray * fu_engine_config_get_disabled_devices(FuEngineConfig *self) G_GNUC_NON_NULL(1); GPtrArray * fu_engine_config_get_disabled_plugins(FuEngineConfig *self) G_GNUC_NON_NULL(1); GArray * fu_engine_config_get_trusted_uids(FuEngineConfig *self) G_GNUC_NON_NULL(1); GPtrArray * fu_engine_config_get_trusted_reports(FuEngineConfig *self) G_GNUC_NON_NULL(1); GPtrArray * fu_engine_config_get_approved_firmware(FuEngineConfig *self) G_GNUC_NON_NULL(1); GPtrArray * fu_engine_config_get_blocked_firmware(FuEngineConfig *self) G_GNUC_NON_NULL(1); guint fu_engine_config_get_uri_scheme_prio(FuEngineConfig *self, const gchar *scheme) G_GNUC_NON_NULL(1, 2); gboolean fu_engine_config_get_update_motd(FuEngineConfig *self) G_GNUC_NON_NULL(1); gboolean fu_engine_config_get_enumerate_all_devices(FuEngineConfig *self) G_GNUC_NON_NULL(1); gboolean fu_engine_config_get_ignore_power(FuEngineConfig *self) G_GNUC_NON_NULL(1); gboolean fu_engine_config_get_only_trusted(FuEngineConfig *self) G_GNUC_NON_NULL(1); gboolean fu_engine_config_get_show_device_private(FuEngineConfig *self) G_GNUC_NON_NULL(1); gboolean fu_engine_config_get_test_devices(FuEngineConfig *self) G_GNUC_NON_NULL(1); gboolean fu_engine_config_get_ignore_requirements(FuEngineConfig *self) G_GNUC_NON_NULL(1); gboolean fu_engine_config_get_release_dedupe(FuEngineConfig *self) G_GNUC_NON_NULL(1); FuReleasePriority fu_engine_config_get_release_priority(FuEngineConfig *self) G_GNUC_NON_NULL(1); FuP2pPolicy fu_engine_config_get_p2p_policy(FuEngineConfig *self) G_GNUC_NON_NULL(1); const gchar * fu_engine_config_get_host_bkc(FuEngineConfig *self) G_GNUC_NON_NULL(1); const gchar * fu_engine_config_get_esp_location(FuEngineConfig *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/src/fu-engine-emulator.c000066400000000000000000000255201501337203100173540ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuEngine" #include "config.h" #include "fu-archive.h" #include "fu-context-private.h" #include "fu-device-private.h" #include "fu-engine-emulator.h" struct _FuEngineEmulator { GObject parent_instance; FuEngine *engine; GHashTable *phase_blobs; /* (element-type utf-8 GBytes) */ }; G_DEFINE_TYPE(FuEngineEmulator, fu_engine_emulator, G_TYPE_OBJECT) enum { PROP_0, PROP_ENGINE, PROP_LAST }; static gchar * fu_engine_emulator_phase_to_filename(FuEngineEmulatorPhase phase, guint write_cnt) { if (write_cnt == FU_ENGINE_EMULATOR_WRITE_COUNT_DEFAULT) return g_strdup_printf("%s.json", fu_engine_emulator_phase_to_string(phase)); return g_strdup_printf("%s-%u.json", fu_engine_emulator_phase_to_string(phase), write_cnt); } gboolean fu_engine_emulator_save(FuEngineEmulator *self, GOutputStream *stream, GError **error) { GHashTableIter iter; gboolean got_json = FALSE; gpointer key; gpointer value; g_autoptr(GByteArray) buf = NULL; g_autoptr(FuArchive) archive = fu_archive_new(NULL, FU_ARCHIVE_FLAG_NONE, NULL); g_return_val_if_fail(FU_IS_ENGINE_EMULATOR(self), FALSE); g_return_val_if_fail(G_IS_OUTPUT_STREAM(stream), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* sanity check */ g_hash_table_iter_init(&iter, self->phase_blobs); while (g_hash_table_iter_next(&iter, &key, &value)) { fu_archive_add_entry(archive, (const gchar *)key, (GBytes *)value); got_json = TRUE; } if (!got_json) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no emulation data, perhaps no devices have been added?"); return FALSE; } /* write */ buf = fu_archive_write(archive, FU_ARCHIVE_FORMAT_ZIP, FU_ARCHIVE_COMPRESSION_GZIP, error); if (buf == NULL) return FALSE; if (!g_output_stream_write_all(stream, buf->data, buf->len, NULL, NULL, error)) { fu_error_convert(error); return FALSE; } if (!g_output_stream_flush(stream, NULL, error)) { fu_error_convert(error); return FALSE; } /* success */ g_hash_table_remove_all(self->phase_blobs); return TRUE; } static gboolean fu_engine_emulator_load_json_blob(FuEngineEmulator *self, GBytes *json_blob, GError **error) { GPtrArray *backends = fu_context_get_backends(fu_engine_get_context(self->engine)); JsonNode *root; g_autoptr(JsonParser) parser = json_parser_new(); /* parse */ if (!json_parser_load_from_data(parser, g_bytes_get_data(json_blob, NULL), g_bytes_get_size(json_blob), error)) return FALSE; /* load into all backends */ root = json_parser_get_root(parser); for (guint i = 0; i < backends->len; i++) { FuBackend *backend = g_ptr_array_index(backends, i); if (!fwupd_codec_from_json(FWUPD_CODEC(backend), root, error)) return FALSE; } /* success */ return TRUE; } gboolean fu_engine_emulator_load_phase(FuEngineEmulator *self, FuEngineEmulatorPhase phase, guint write_cnt, GError **error) { GBytes *json_blob; g_autofree gchar *fn = fu_engine_emulator_phase_to_filename(phase, write_cnt); json_blob = g_hash_table_lookup(self->phase_blobs, fn); if (json_blob == NULL) return TRUE; return fu_engine_emulator_load_json_blob(self, json_blob, error); } static void fu_engine_emulator_to_json(FuEngineEmulator *self, GPtrArray *devices, JsonBuilder *json_builder) { /* not always correct, but we want to remain compatible with all the old emulation files */ json_builder_begin_object(json_builder); fwupd_codec_json_append(json_builder, "FwupdVersion", PACKAGE_VERSION); json_builder_set_member_name(json_builder, "UsbDevices"); json_builder_begin_array(json_builder); for (guint i = 0; i < devices->len; i++) { FuDevice *device = g_ptr_array_index(devices, i); /* interesting? */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATION_TAG)) continue; json_builder_begin_object(json_builder); fu_device_add_json(device, json_builder, FWUPD_CODEC_FLAG_NONE); json_builder_end_object(json_builder); } json_builder_end_array(json_builder); json_builder_end_object(json_builder); /* we've recorded these, now drop them */ for (guint i = 0; i < devices->len; i++) { FuDevice *device = g_ptr_array_index(devices, i); if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATION_TAG)) continue; fu_device_clear_events(device); } } gboolean fu_engine_emulator_save_phase(FuEngineEmulator *self, FuEngineEmulatorPhase phase, guint write_cnt, GError **error) { GBytes *blob_old; g_autofree gchar *blob_new_safe = NULL; g_autofree gchar *fn = fu_engine_emulator_phase_to_filename(phase, write_cnt); g_autoptr(GBytes) blob_new = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(GOutputStream) ostream = g_memory_output_stream_new_resizable(); g_autoptr(JsonBuilder) json_builder = json_builder_new(); g_autoptr(JsonGenerator) json_generator = NULL; g_autoptr(JsonNode) json_root = NULL; /* all devices in all backends */ devices = fu_engine_get_devices(self->engine, error); if (devices == NULL) return FALSE; fu_engine_emulator_to_json(self, devices, json_builder); json_root = json_builder_get_root(json_builder); json_generator = json_generator_new(); json_generator_set_pretty(json_generator, TRUE); json_generator_set_root(json_generator, json_root); blob_old = g_hash_table_lookup(self->phase_blobs, fn); if (!json_generator_to_stream(json_generator, ostream, NULL, error)) return FALSE; if (!g_output_stream_close(ostream, NULL, error)) return FALSE; blob_new = g_memory_output_stream_steal_as_bytes(G_MEMORY_OUTPUT_STREAM(ostream)); if (g_bytes_get_size(blob_new) == 0) { g_info("no data for phase %s [%u]", fu_engine_emulator_phase_to_string(phase), write_cnt); return TRUE; } if (blob_old != NULL && g_bytes_compare(blob_old, blob_new) == 0) { g_info("JSON unchanged for phase %s [%u]", fu_engine_emulator_phase_to_string(phase), write_cnt); return TRUE; } blob_new_safe = fu_strsafe_bytes(blob_new, 8000); g_info("JSON %s for phase %s [%u]: %s...", blob_old == NULL ? "added" : "changed", fu_engine_emulator_phase_to_string(phase), write_cnt, blob_new_safe); g_hash_table_insert(self->phase_blobs, g_steal_pointer(&fn), g_steal_pointer(&blob_new)); /* success */ return TRUE; } static gboolean fu_engine_emulator_load_phases(FuEngineEmulator *self, FuArchive *archive, guint write_cnt, gboolean *got_json, GError **error) { for (FuEngineEmulatorPhase phase = FU_ENGINE_EMULATOR_PHASE_SETUP; phase < FU_ENGINE_EMULATOR_PHASE_LAST; phase++) { g_autofree gchar *fn = fu_engine_emulator_phase_to_filename(phase, write_cnt); g_autoptr(GBytes) blob = NULL; /* not found */ blob = fu_archive_lookup_by_fn(archive, fn, NULL); if (blob == NULL || g_bytes_get_size(blob) == 0) continue; *got_json = TRUE; g_info("emulation for phase %s [%u]", fu_engine_emulator_phase_to_string(phase), write_cnt); if (write_cnt == FU_ENGINE_EMULATOR_WRITE_COUNT_DEFAULT && phase == FU_ENGINE_EMULATOR_PHASE_SETUP) { if (!fu_engine_emulator_load_json_blob(self, blob, error)) return FALSE; } else { g_hash_table_insert(self->phase_blobs, g_steal_pointer(&fn), g_steal_pointer(&blob)); } } /* success */ return TRUE; } gboolean fu_engine_emulator_load(FuEngineEmulator *self, GInputStream *stream, GError **error) { gboolean got_json = FALSE; const gchar *json_empty = "{\"UsbDevices\":[]}"; g_autoptr(FuArchive) archive = NULL; g_autoptr(GBytes) json_blob = g_bytes_new_static(json_empty, strlen(json_empty)); g_autoptr(GError) error_archive = NULL; g_return_val_if_fail(FU_IS_ENGINE_EMULATOR(self), FALSE); g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* unload any existing devices */ if (!fu_engine_emulator_load_json_blob(self, json_blob, error)) return FALSE; g_hash_table_remove_all(self->phase_blobs); /* load archive */ archive = fu_archive_new_stream(stream, FU_ARCHIVE_FLAG_NONE, &error_archive); if (archive == NULL) { g_autoptr(GBytes) blob = NULL; g_debug("no archive found, using JSON as phase setup: %s", error_archive->message); blob = fu_input_stream_read_bytes(stream, 0, G_MAXSIZE, NULL, error); if (blob == NULL) return FALSE; return fu_engine_emulator_load_json_blob(self, blob, error); } /* load JSON files from archive */ for (guint write_cnt = 0; write_cnt < FU_ENGINE_EMULATOR_WRITE_COUNT_MAX; write_cnt++) { if (!fu_engine_emulator_load_phases(self, archive, write_cnt, &got_json, error)) return FALSE; } if (!got_json) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no emulation data found in archive"); return FALSE; } /* success */ return TRUE; } static void fu_engine_emulator_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuEngineEmulator *self = FU_ENGINE_EMULATOR(object); switch (prop_id) { case PROP_ENGINE: g_value_set_object(value, self->engine); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_engine_emulator_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuEngineEmulator *self = FU_ENGINE_EMULATOR(object); switch (prop_id) { case PROP_ENGINE: self->engine = g_value_dup_object(value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_engine_emulator_init(FuEngineEmulator *self) { self->phase_blobs = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_bytes_unref); } static void fu_engine_emulator_finalize(GObject *obj) { FuEngineEmulator *self = FU_ENGINE_EMULATOR(obj); g_hash_table_unref(self->phase_blobs); G_OBJECT_CLASS(fu_engine_emulator_parent_class)->finalize(obj); } static void fu_engine_emulator_dispose(GObject *obj) { FuEngineEmulator *self = FU_ENGINE_EMULATOR(obj); g_clear_object(&self->engine); G_OBJECT_CLASS(fu_engine_emulator_parent_class)->dispose(obj); } static void fu_engine_emulator_class_init(FuEngineEmulatorClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); GParamSpec *pspec; object_class->finalize = fu_engine_emulator_finalize; object_class->dispose = fu_engine_emulator_dispose; object_class->get_property = fu_engine_emulator_get_property; object_class->set_property = fu_engine_emulator_set_property; pspec = g_param_spec_object("engine", NULL, NULL, FU_TYPE_ENGINE, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_ENGINE, pspec); } FuEngineEmulator * fu_engine_emulator_new(FuEngine *engine) { return FU_ENGINE_EMULATOR(g_object_new(FU_TYPE_ENGINE_EMULATOR, "engine", engine, NULL)); } fwupd-2.0.10/src/fu-engine-emulator.h000066400000000000000000000023271501337203100173610ustar00rootroot00000000000000/* * Copyright 2024 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-engine-struct.h" #include "fu-engine.h" #define FU_TYPE_ENGINE_EMULATOR (fu_engine_emulator_get_type()) G_DECLARE_FINAL_TYPE(FuEngineEmulator, fu_engine_emulator, FU, ENGINE_EMULATOR, GObject) /* the default write count, e.g. for composite actions */ #define FU_ENGINE_EMULATOR_WRITE_COUNT_DEFAULT 0 /* the maximum times a device can use FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED */ #define FU_ENGINE_EMULATOR_WRITE_COUNT_MAX 5 FuEngineEmulator * fu_engine_emulator_new(FuEngine *engine) G_GNUC_NON_NULL(1); gboolean fu_engine_emulator_save(FuEngineEmulator *self, GOutputStream *stream, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_engine_emulator_load(FuEngineEmulator *self, GInputStream *stream, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_engine_emulator_load_phase(FuEngineEmulator *self, FuEngineEmulatorPhase phase, guint write_cnt, GError **error) G_GNUC_NON_NULL(1); gboolean fu_engine_emulator_save_phase(FuEngineEmulator *self, FuEngineEmulatorPhase phase, guint write_cnt, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/src/fu-engine-helper.c000066400000000000000000000353731501337203100170120ustar00rootroot00000000000000/* * Copyright 2020 Mario Limonciello * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuEngine" #include "config.h" #include #include #include "fwupd-device-private.h" #include "fu-engine-helper.h" #include "fu-engine.h" static FwupdRelease * fu_engine_get_release_with_tag(FuEngine *self, FuEngineRequest *request, FwupdDevice *dev, const gchar *host_bkc, GError **error) { g_autoptr(GPtrArray) rels = NULL; g_auto(GStrv) host_bkcs = g_strsplit(host_bkc, ",", -1); /* find the newest release that matches */ rels = fu_engine_get_releases(self, request, fwupd_device_get_id(dev), error); if (rels == NULL) return NULL; for (guint i = 0; i < rels->len; i++) { FwupdRelease *rel = g_ptr_array_index(rels, i); for (guint j = 0; host_bkcs[j] != NULL; j++) { if (fwupd_release_has_tag(rel, host_bkcs[j])) return g_object_ref(rel); } } /* no match */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no matching releases for device"); return NULL; } gboolean fu_engine_update_motd(FuEngine *self, GError **error) { const gchar *host_bkc = fu_engine_get_host_bkc(self); guint upgrade_count = 0; guint sync_count = 0; g_autoptr(FuEngineRequest) request = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(GString) str = g_string_new(NULL); g_autofree gchar *target = NULL; /* a subset of what fwupdmgr can do */ request = fu_engine_request_new(NULL); fu_engine_request_set_feature_flags(request, FWUPD_FEATURE_FLAG_DETACH_ACTION | FWUPD_FEATURE_FLAG_UPDATE_ACTION); /* get devices from daemon, we even want to know if it's nothing */ devices = fu_engine_get_devices(self, NULL); if (devices != NULL) { for (guint i = 0; i < devices->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices, i); g_autoptr(GPtrArray) rels = NULL; /* get the releases for this device */ if (!fu_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE)) continue; rels = fu_engine_get_upgrades(self, request, fwupd_device_get_id(dev), NULL); if (rels == NULL) continue; upgrade_count++; } if (host_bkc != NULL) { for (guint i = 0; i < devices->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices, i); g_autoptr(FwupdRelease) rel = NULL; if (!fu_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE)) continue; rel = fu_engine_get_release_with_tag(self, request, dev, host_bkc, NULL); if (rel == NULL) continue; if (g_strcmp0(fwupd_device_get_version(dev), fwupd_release_get_version(rel)) != 0) sync_count++; } } } /* If running under systemd unit, use the directory as a base */ if (g_getenv("RUNTIME_DIRECTORY") != NULL) { target = g_build_filename(g_getenv("RUNTIME_DIRECTORY"), MOTD_FILE, NULL); /* otherwise use the cache directory */ } else { g_autofree gchar *directory = fu_path_from_kind(FU_PATH_KIND_CACHEDIR_PKG); target = g_build_filename(directory, MOTD_DIR, MOTD_FILE, NULL); } /* create the directory and file, even if zero devices; we want an empty file then */ if (!fu_path_mkdir_parent(target, error)) return FALSE; /* nag about syncing or updating, but never both */ if (sync_count > 0) { g_string_append(str, "\n"); g_string_append_printf(str, /* TRANSLATORS: this is shown in the MOTD */ ngettext("%u device is not the best known configuration.", "%u devices are not the best known configuration.", sync_count), sync_count); g_string_append(str, "\n"); g_string_append_printf(str, /* TRANSLATORS: this is shown in the MOTD -- %1 is the * command name, e.g. `fwupdmgr sync` */ _("Run `%s` to complete this action."), "fwupdmgr sync"); g_string_append(str, "\n\n"); } else if (upgrade_count > 0) { g_string_append(str, "\n"); g_string_append_printf(str, /* TRANSLATORS: this is shown in the MOTD */ ngettext("%u device has a firmware upgrade available.", "%u devices have a firmware upgrade available.", upgrade_count), upgrade_count); g_string_append(str, "\n"); g_string_append_printf(str, /* TRANSLATORS: this is shown in the MOTD -- %1 is the * command name, e.g. `fwupdmgr get-upgrades` */ _("Run `%s` for more information."), "fwupdmgr get-upgrades"); g_string_append(str, "\n\n"); } /* success, with an empty file if nothing to say */ g_debug("writing motd target %s", target); return g_file_set_contents(target, str->str, str->len, error); } gboolean fu_engine_update_devices_file(FuEngine *self, GError **error) { FwupdCodecFlags flags = FWUPD_CODEC_FLAG_NONE; gsize len; g_autoptr(JsonBuilder) builder = NULL; g_autoptr(JsonGenerator) generator = NULL; g_autoptr(JsonNode) root = NULL; g_autoptr(GPtrArray) devices = NULL; g_autofree gchar *data = NULL; g_autofree gchar *directory = NULL; g_autofree gchar *target = NULL; if (fu_engine_config_get_show_device_private(fu_engine_get_config(self))) flags |= FWUPD_CODEC_FLAG_TRUSTED; builder = json_builder_new(); json_builder_begin_object(builder); devices = fu_engine_get_devices(self, NULL); if (devices != NULL) fwupd_codec_array_to_json(devices, "Devices", builder, flags); root = json_builder_get_root(builder); generator = json_generator_new(); json_generator_set_pretty(generator, TRUE); json_generator_set_root(generator, root); data = json_generator_to_data(generator, &len); if (data == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to convert to JSON string"); return FALSE; } directory = fu_path_from_kind(FU_PATH_KIND_CACHEDIR_PKG); target = g_build_filename(directory, "devices.json", NULL); return g_file_set_contents(target, data, (gssize)len, error); } static void fu_engine_integrity_add_measurement(GHashTable *self, const gchar *id, GBytes *blob) { g_autofree gchar *csum = g_compute_checksum_for_bytes(G_CHECKSUM_SHA256, blob); g_hash_table_insert(self, g_strdup(id), g_steal_pointer(&csum)); } static void fu_engine_integrity_measure_acpi(FuContext *ctx, GHashTable *self) { g_autofree gchar *path = fu_path_from_kind(FU_PATH_KIND_ACPI_TABLES); const gchar *tables[] = { "SLIC", "MSDM", "TPM2", }; for (guint i = 0; i < G_N_ELEMENTS(tables); i++) { g_autofree gchar *fn = g_build_filename(path, tables[i], NULL); g_autoptr(GBytes) blob = NULL; blob = fu_bytes_get_contents(fn, NULL); if (blob != NULL && g_bytes_get_size(blob) > 0) { g_autofree gchar *id = g_strdup_printf("ACPI:%s", tables[i]); fu_engine_integrity_add_measurement(self, id, blob); } } } static void fu_engine_integrity_measure_uefi(FuContext *ctx, GHashTable *self) { FuEfivars *efivars = fu_context_get_efivars(ctx); struct { const gchar *guid; const gchar *name; } keys[] = { {FU_EFIVARS_GUID_EFI_GLOBAL, "BootCurrent"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "KEK"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "KEKDefault"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "OsIndications"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "OsIndicationsSupported"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "PK"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "PKDefault"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "SecureBoot"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "SetupMode"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "SignatureSupport"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "VendorKeys"}, {FU_EFIVARS_GUID_SECURITY_DATABASE, "db"}, {FU_EFIVARS_GUID_SECURITY_DATABASE, "dbDefault"}, {FU_EFIVARS_GUID_SECURITY_DATABASE, "dbx"}, {FU_EFIVARS_GUID_SECURITY_DATABASE, "dbxDefault"}, }; /* important keys */ for (guint i = 0; i < G_N_ELEMENTS(keys); i++) { g_autoptr(GBytes) blob = fu_efivars_get_data_bytes(efivars, keys[i].guid, keys[i].name, NULL, NULL); if (blob != NULL) { g_autofree gchar *id = g_strdup_printf("UEFI:%s", keys[i].name); fu_engine_integrity_add_measurement(self, id, blob); } } /* Boot#### */ for (guint i = 0; i < 0xFF; i++) { g_autoptr(GBytes) blob = fu_efivars_get_boot_data(efivars, i, NULL); if (blob != NULL && g_bytes_get_size(blob) > 0) { const guint8 needle[] = "f\0w\0u\0p\0d"; g_autofree gchar *id = g_strdup_printf("UEFI:Boot%04X", i); if (fu_memmem_safe(g_bytes_get_data(blob, NULL), g_bytes_get_size(blob), needle, sizeof(needle), NULL, NULL)) { g_debug("skipping %s as fwupd found", id); continue; } fu_engine_integrity_add_measurement(self, id, blob); } } } GHashTable * fu_engine_integrity_new(FuContext *ctx, GError **error) { g_autoptr(GHashTable) self = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); g_return_val_if_fail(error == NULL || *error == NULL, NULL); fu_engine_integrity_measure_uefi(ctx, self); fu_engine_integrity_measure_acpi(ctx, self); /* nothing of use */ if (g_hash_table_size(self) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no measurements"); return NULL; } /* success */ return g_steal_pointer(&self); } gchar * fu_engine_integrity_to_string(GHashTable *self) { GHashTableIter iter; gpointer key, value; g_autoptr(GPtrArray) array = g_ptr_array_new_with_free_func(g_free); g_return_val_if_fail(self != NULL, NULL); /* sanity check */ if (g_hash_table_size(self) == 0) return NULL; /* build into KV array */ g_hash_table_iter_init(&iter, self); while (g_hash_table_iter_next(&iter, &key, &value)) { g_ptr_array_add(array, g_strdup_printf("%s=%s", (const gchar *)key, (const gchar *)value)); } return fu_strjoin("\n", array); } static const GError * fu_engine_error_array_find(GPtrArray *errors, FwupdError error_code) { for (guint j = 0; j < errors->len; j++) { const GError *error = g_ptr_array_index(errors, j); if (g_error_matches(error, FWUPD_ERROR, error_code)) return error; } return NULL; } static guint fu_engine_error_array_count(GPtrArray *errors, FwupdError error_code) { guint cnt = 0; for (guint j = 0; j < errors->len; j++) { const GError *error = g_ptr_array_index(errors, j); if (g_error_matches(error, FWUPD_ERROR, error_code)) cnt++; } return cnt; } static gboolean fu_engine_error_array_matches_any(GPtrArray *errors, FwupdError *error_codes) { for (guint j = 0; j < errors->len; j++) { const GError *error = g_ptr_array_index(errors, j); gboolean matches_any = FALSE; for (guint i = 0; error_codes[i] != FWUPD_ERROR_LAST; i++) { if (g_error_matches(error, FWUPD_ERROR, error_codes[i])) { matches_any = TRUE; break; } } if (!matches_any) return FALSE; } return TRUE; } /** * fu_engine_error_array_get_best: * @errors: (element-type GError): array of errors * * Finds the 'best' error to show the user from a array of errors, creating a * completely bespoke error where required. * * Returns: (transfer full): a #GError, never %NULL **/ GError * fu_engine_error_array_get_best(GPtrArray *errors) { FwupdError err_prio[] = {FWUPD_ERROR_INVALID_FILE, FWUPD_ERROR_VERSION_SAME, FWUPD_ERROR_VERSION_NEWER, FWUPD_ERROR_NOT_SUPPORTED, FWUPD_ERROR_INTERNAL, FWUPD_ERROR_NOT_FOUND, FWUPD_ERROR_LAST}; FwupdError err_all_uptodate[] = {FWUPD_ERROR_VERSION_SAME, FWUPD_ERROR_NOT_FOUND, FWUPD_ERROR_NOT_SUPPORTED, FWUPD_ERROR_LAST}; FwupdError err_all_newer[] = {FWUPD_ERROR_VERSION_NEWER, FWUPD_ERROR_VERSION_SAME, FWUPD_ERROR_NOT_FOUND, FWUPD_ERROR_NOT_SUPPORTED, FWUPD_ERROR_LAST}; /* are all the errors either GUID-not-matched or version-same? */ if (fu_engine_error_array_count(errors, FWUPD_ERROR_VERSION_SAME) > 1 && fu_engine_error_array_matches_any(errors, err_all_uptodate)) { return g_error_new(FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "All updatable firmware is already installed"); } /* are all the errors either GUID-not-matched or version same or newer? */ if (fu_engine_error_array_count(errors, FWUPD_ERROR_VERSION_NEWER) > 1 && fu_engine_error_array_matches_any(errors, err_all_newer)) { return g_error_new(FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "All updatable devices already have newer versions"); } /* get the most important single error */ for (guint i = 0; err_prio[i] != FWUPD_ERROR_LAST; i++) { const GError *error_tmp = fu_engine_error_array_find(errors, err_prio[i]); if (error_tmp != NULL) return g_error_copy(error_tmp); } /* fall back to something */ return g_error_new(FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "No supported devices found"); } /** * fu_engine_build_machine_id: * @salt: (nullable): optional salt * @error: (nullable): optional return location for an error * * Gets a salted hash of the /etc/machine-id contents. This can be used to * identify a specific machine. It is not possible to recover the original * machine-id from the machine-hash. * * Returns: the SHA256 machine hash, or %NULL if the ID is not present **/ gchar * fu_engine_build_machine_id(const gchar *salt, GError **error) { const gchar *machine_id; gsize bufsz = 0; g_autofree gchar *buf = NULL; g_autoptr(GChecksum) csum = NULL; g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* in test mode */ machine_id = g_getenv("FWUPD_MACHINE_ID"); if (machine_id != NULL) { buf = g_strdup(machine_id); bufsz = strlen(buf); } else { const gchar *fn = NULL; g_autoptr(GPtrArray) fns = g_ptr_array_new_with_free_func(g_free); /* one of these has to exist */ g_ptr_array_add(fns, g_build_filename(FWUPD_SYSCONFDIR, "machine-id", NULL)); g_ptr_array_add( fns, g_build_filename(FWUPD_LOCALSTATEDIR, "lib", "dbus", "machine-id", NULL)); g_ptr_array_add(fns, g_strdup("/etc/machine-id")); g_ptr_array_add(fns, g_strdup("/var/lib/dbus/machine-id")); g_ptr_array_add(fns, g_strdup("/var/db/dbus/machine-id")); /* this is the hardcoded path for homebrew, e.g. `sudo dbus-uuidgen --ensure` */ g_ptr_array_add(fns, g_strdup("/usr/local/var/lib/dbus/machine-id")); for (guint i = 0; i < fns->len; i++) { const gchar *fn_tmp = g_ptr_array_index(fns, i); if (g_file_test(fn_tmp, G_FILE_TEST_EXISTS)) { fn = fn_tmp; break; } } if (fn == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_READ, "The machine-id is not present"); return NULL; } if (!g_file_get_contents(fn, &buf, &bufsz, error)) return NULL; if (bufsz == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_READ, "The machine-id is present but unset"); return NULL; } } csum = g_checksum_new(G_CHECKSUM_SHA256); if (salt != NULL) g_checksum_update(csum, (const guchar *)salt, (gssize)strlen(salt)); g_checksum_update(csum, (const guchar *)buf, (gssize)bufsz); return g_strdup(g_checksum_get_string(csum)); } fwupd-2.0.10/src/fu-engine-helper.h000066400000000000000000000012101501337203100167760ustar00rootroot00000000000000/* * Copyright 2020 Mario Limonciello * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-engine.h" gboolean fu_engine_update_motd(FuEngine *self, GError **error) G_GNUC_NON_NULL(1); gboolean fu_engine_update_devices_file(FuEngine *self, GError **error) G_GNUC_NON_NULL(1); GHashTable * fu_engine_integrity_new(FuContext *ctx, GError **error); gchar * fu_engine_integrity_to_string(GHashTable *self); GError * fu_engine_error_array_get_best(GPtrArray *errors); gchar * fu_engine_build_machine_id(const gchar *salt, GError **error); fwupd-2.0.10/src/fu-engine-request.c000066400000000000000000000103461501337203100172140ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuEngine" #include "config.h" #include #include "fu-engine-request.h" struct _FuEngineRequest { GObject parent_instance; FuEngineRequestFlag flags; FwupdFeatureFlags feature_flags; FwupdCodecFlags converter_flags; gchar *sender; gchar *locale; }; static void fu_engine_request_codec_iface_init(FwupdCodecInterface *iface); G_DEFINE_TYPE_WITH_CODE(FuEngineRequest, fu_engine_request, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE(FWUPD_TYPE_CODEC, fu_engine_request_codec_iface_init)) static void fu_engine_request_add_string(FwupdCodec *codec, guint idt, GString *str) { FuEngineRequest *self = FU_ENGINE_REQUEST(codec); if (self->flags != FU_ENGINE_REQUEST_FLAG_NONE) { g_autofree gchar *flags = fu_engine_request_flag_to_string(self->flags); fwupd_codec_string_append(str, idt, "Flags", flags); } fwupd_codec_string_append_hex(str, idt, "FeatureFlags", self->feature_flags); fwupd_codec_string_append_hex(str, idt, "ConverterFlags", self->converter_flags); fwupd_codec_string_append(str, idt, "Locale", self->locale); } static void fu_engine_request_codec_iface_init(FwupdCodecInterface *iface) { iface->add_string = fu_engine_request_add_string; } const gchar * fu_engine_request_get_sender(FuEngineRequest *self) { g_return_val_if_fail(FU_IS_ENGINE_REQUEST(self), NULL); return self->sender; } FwupdFeatureFlags fu_engine_request_get_feature_flags(FuEngineRequest *self) { g_return_val_if_fail(FU_IS_ENGINE_REQUEST(self), FALSE); return self->feature_flags; } const gchar * fu_engine_request_get_locale(FuEngineRequest *self) { g_return_val_if_fail(FU_IS_ENGINE_REQUEST(self), NULL); return self->locale; } void fu_engine_request_add_flag(FuEngineRequest *self, FuEngineRequestFlag flag) { g_return_if_fail(FU_IS_ENGINE_REQUEST(self)); self->flags |= flag; } gboolean fu_engine_request_has_flag(FuEngineRequest *self, FuEngineRequestFlag flag) { g_return_val_if_fail(FU_IS_ENGINE_REQUEST(self), FU_ENGINE_REQUEST_FLAG_NONE); return (self->flags & flag) > 0; } void fu_engine_request_set_feature_flags(FuEngineRequest *self, FwupdFeatureFlags feature_flags) { g_return_if_fail(FU_IS_ENGINE_REQUEST(self)); self->feature_flags = feature_flags; } void fu_engine_request_set_locale(FuEngineRequest *self, const gchar *locale) { g_return_if_fail(FU_IS_ENGINE_REQUEST(self)); /* not changed */ if (g_strcmp0(self->locale, locale) == 0) return; g_free(self->locale); self->locale = g_strdup(locale); /* remove the UTF8 suffix as it is not present in the XML */ if (self->locale != NULL) g_strdelimit(self->locale, ".", '\0'); } gboolean fu_engine_request_has_feature_flag(FuEngineRequest *self, FwupdFeatureFlags feature_flag) { g_return_val_if_fail(FU_IS_ENGINE_REQUEST(self), FALSE); return (self->feature_flags & feature_flag) > 0; } FwupdCodecFlags fu_engine_request_get_converter_flags(FuEngineRequest *self) { g_return_val_if_fail(FU_IS_ENGINE_REQUEST(self), FALSE); return self->converter_flags; } void fu_engine_request_set_converter_flags(FuEngineRequest *self, FwupdCodecFlags converter_flags) { g_return_if_fail(FU_IS_ENGINE_REQUEST(self)); self->converter_flags = converter_flags; } gboolean fu_engine_request_has_converter_flag(FuEngineRequest *self, FwupdCodecFlags device_flag) { g_return_val_if_fail(FU_IS_ENGINE_REQUEST(self), FALSE); return (self->converter_flags & device_flag) > 0; } static void fu_engine_request_init(FuEngineRequest *self) { self->flags = FU_ENGINE_REQUEST_FLAG_NONE; self->converter_flags = FWUPD_CODEC_FLAG_NONE; self->feature_flags = FWUPD_FEATURE_FLAG_NONE; } static void fu_engine_request_finalize(GObject *obj) { FuEngineRequest *self = FU_ENGINE_REQUEST(obj); g_free(self->sender); g_free(self->locale); G_OBJECT_CLASS(fu_engine_request_parent_class)->finalize(obj); } static void fu_engine_request_class_init(FuEngineRequestClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_engine_request_finalize; } FuEngineRequest * fu_engine_request_new(const gchar *sender) { FuEngineRequest *self; self = g_object_new(FU_TYPE_ENGINE_REQUEST, NULL); self->sender = g_strdup(sender); return FU_ENGINE_REQUEST(self); } fwupd-2.0.10/src/fu-engine-request.h000066400000000000000000000030561501337203100172210ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-engine-struct.h" #define FU_TYPE_ENGINE_REQUEST (fu_engine_request_get_type()) G_DECLARE_FINAL_TYPE(FuEngineRequest, fu_engine_request, FU, ENGINE_REQUEST, GObject) FuEngineRequest * fu_engine_request_new(const gchar *sender); const gchar * fu_engine_request_get_sender(FuEngineRequest *self) G_GNUC_NON_NULL(1); void fu_engine_request_add_flag(FuEngineRequest *self, FuEngineRequestFlag flag) G_GNUC_NON_NULL(1); gboolean fu_engine_request_has_flag(FuEngineRequest *self, FuEngineRequestFlag flag) G_GNUC_WARN_UNUSED_RESULT G_GNUC_NON_NULL(1); FwupdFeatureFlags fu_engine_request_get_feature_flags(FuEngineRequest *self) G_GNUC_NON_NULL(1); void fu_engine_request_set_feature_flags(FuEngineRequest *self, FwupdFeatureFlags feature_flags) G_GNUC_NON_NULL(1); const gchar * fu_engine_request_get_locale(FuEngineRequest *self) G_GNUC_NON_NULL(1); void fu_engine_request_set_locale(FuEngineRequest *self, const gchar *locale) G_GNUC_NON_NULL(1); gboolean fu_engine_request_has_feature_flag(FuEngineRequest *self, FwupdFeatureFlags feature_flag) G_GNUC_NON_NULL(1); gboolean fu_engine_request_has_converter_flag(FuEngineRequest *self, FwupdCodecFlags device_flag) G_GNUC_NON_NULL(1); FwupdCodecFlags fu_engine_request_get_converter_flags(FuEngineRequest *self) G_GNUC_NON_NULL(1); void fu_engine_request_set_converter_flags(FuEngineRequest *self, FwupdCodecFlags device_flags) G_GNUC_NON_NULL(1); fwupd-2.0.10/src/fu-engine-requirements.c000066400000000000000000000625521501337203100202550ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuEngine" #include "config.h" #include "fu-engine-requirements.h" static gboolean fu_engine_requirements_require_vercmp(XbNode *req, const gchar *version, FwupdVersionFormat fmt, GError **error) { gboolean ret = FALSE; gint rc = 0; const gchar *tmp = xb_node_get_attr(req, "compare"); const gchar *version_req = xb_node_get_attr(req, "version"); if (g_strcmp0(tmp, "eq") == 0) { rc = fu_version_compare(version, version_req, fmt); ret = rc == 0; } else if (g_strcmp0(tmp, "ne") == 0) { rc = fu_version_compare(version, version_req, fmt); ret = rc != 0; } else if (g_strcmp0(tmp, "lt") == 0) { rc = fu_version_compare(version, version_req, fmt); ret = rc < 0; } else if (g_strcmp0(tmp, "gt") == 0) { rc = fu_version_compare(version, version_req, fmt); ret = rc > 0; } else if (g_strcmp0(tmp, "le") == 0) { rc = fu_version_compare(version, version_req, fmt); ret = rc <= 0; } else if (g_strcmp0(tmp, "ge") == 0) { rc = fu_version_compare(version, version_req, fmt); ret = rc >= 0; } else if (g_strcmp0(tmp, "glob") == 0) { ret = g_pattern_match_simple(version_req, version); } else if (g_strcmp0(tmp, "regex") == 0) { ret = g_regex_match_simple(version_req, version, 0, 0); } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to compare [%s] and [%s]", version_req, version); return FALSE; } /* set error */ if (!ret) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed predicate [%s %s %s]", version_req, tmp, version); } return ret; } static gboolean fu_engine_requirements_check_not_child(FuEngine *self, XbNode *req, FuDevice *device, GError **error) { GPtrArray *children = fu_device_get_children(device); /* only supported */ if (g_strcmp0(xb_node_get_element(req), "firmware") != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot handle not-child %s requirement", xb_node_get_element(req)); return FALSE; } /* check each child */ for (guint i = 0; i < children->len; i++) { FuDevice *child = g_ptr_array_index(children, i); const gchar *version = fu_device_get_version(child); if (version == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no version provided by %s, child of %s", fu_device_get_name(child), fu_device_get_name(device)); return FALSE; } if (fu_engine_requirements_require_vercmp(req, version, fu_device_get_version_format(child), NULL)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Not compatible with child device version %s", version); return FALSE; } } return TRUE; } static gboolean fu_engine_requirements_check_vendor_id(FuEngine *self, XbNode *req, FuDevice *device, GError **error) { GPtrArray *vendor_ids; const gchar *vendor_ids_metadata; g_autofree gchar *vendor_ids_device = NULL; /* devices without vendor IDs should not exist! */ vendor_ids = fu_device_get_vendor_ids(device); if (vendor_ids->len == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device [%s] has no vendor ID", fu_device_get_id(device)); return FALSE; } /* metadata with empty vendor IDs should not exist! */ vendor_ids_metadata = xb_node_get_attr(req, "version"); if (vendor_ids_metadata == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "metadata has no vendor ID"); return FALSE; } /* it is always safe to use a regex, even for simple strings */ vendor_ids_device = fu_strjoin("|", vendor_ids); if (!g_regex_match_simple(vendor_ids_metadata, vendor_ids_device, 0, 0)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "Not compatible with vendor %s: got %s", vendor_ids_device, vendor_ids_metadata); return FALSE; } /* success */ return TRUE; } static gboolean _fu_device_has_guids_any(FuDevice *self, gchar **guids) { g_return_val_if_fail(FU_IS_DEVICE(self), FALSE); g_return_val_if_fail(guids != NULL, FALSE); for (guint i = 0; guids[i] != NULL; i++) { if (fu_device_has_guid(self, guids[i])) return TRUE; } return FALSE; } static gboolean fu_engine_requirements_check_firmware(FuEngine *self, XbNode *req, FuDevice *device, const gchar *fwupd_version, FwupdInstallFlags flags, GError **error) { const gchar *version; const gchar *depth_str; gint64 depth = G_MAXINT64; g_autoptr(FuDevice) device_actual = g_object_ref(device); g_autoptr(GError) error_local = NULL; g_auto(GStrv) guids = NULL; /* look at the parent device */ depth_str = xb_node_get_attr(req, "depth"); if (depth_str != NULL) { if (!fu_strtoll(depth_str, &depth, -1, 10, FU_INTEGER_BASE_AUTO, error)) return FALSE; for (gint64 i = 0; i < depth; i++) { FuDevice *device_tmp = fu_device_get_parent(device_actual); if (device_tmp == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "No parent device for %s " "(%" G_GINT64_FORMAT "/%" G_GINT64_FORMAT ")", fu_device_get_name(device_actual), i, depth); return FALSE; } g_set_object(&device_actual, device_tmp); } } /* check fwupd version requirement */ if (depth < 0) { if (fu_version_compare(fwupd_version, "1.9.7", FWUPD_VERSION_FORMAT_UNKNOWN) < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "requirement 'child firmware' also needs %s >= 1.9.7", FWUPD_DBUS_SERVICE); return FALSE; } } else if (depth == 0) { if (fu_version_compare(fwupd_version, "1.6.1", FWUPD_VERSION_FORMAT_UNKNOWN) < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "requirement 'sibling firmware' also needs %s >= 1.6.1", FWUPD_DBUS_SERVICE); return FALSE; } } else if (depth == 1) { if (fu_version_compare(fwupd_version, "1.3.4", FWUPD_VERSION_FORMAT_UNKNOWN) < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "requirement 'parent firmware' also needs %s >= 1.3.4", FWUPD_DBUS_SERVICE); return FALSE; } } /* old firmware version */ if (xb_node_get_text(req) == NULL) { version = fu_device_get_version(device_actual); if (!fu_engine_requirements_require_vercmp( req, version, fu_device_get_version_format(device_actual), &error_local)) { if (g_strcmp0(xb_node_get_attr(req, "compare"), "ge") == 0) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "Not compatible with firmware version %s, requires >= %s", version, xb_node_get_attr(req, "version")); } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "Not compatible with firmware version: %s", error_local->message); } return FALSE; } return TRUE; } /* bootloader version */ if (g_strcmp0(xb_node_get_text(req), "bootloader") == 0) { version = fu_device_get_version_bootloader(device_actual); if (!fu_engine_requirements_require_vercmp( req, version, fu_device_get_version_format(device_actual), &error_local)) { if (g_strcmp0(xb_node_get_attr(req, "compare"), "ge") == 0) { g_set_error( error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Not compatible with bootloader version %s, requires >= %s", version, xb_node_get_attr(req, "version")); } else { g_debug("Bootloader is not compatible: %s", error_local->message); g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Bootloader is not compatible"); } return FALSE; } return TRUE; } /* vendor ID */ if (g_strcmp0(xb_node_get_text(req), "vendor-id") == 0) { if (flags & FU_FIRMWARE_PARSE_FLAG_IGNORE_VID_PID) return TRUE; return fu_engine_requirements_check_vendor_id(self, req, device_actual, error); } /* child version */ if (g_strcmp0(xb_node_get_text(req), "not-child") == 0) return fu_engine_requirements_check_not_child(self, req, device_actual, error); /* another device, specified by GUID|GUID|GUID */ guids = g_strsplit(xb_node_get_text(req), "|", -1); for (guint i = 0; guids[i] != NULL; i++) { if (!fwupd_guid_is_valid(guids[i])) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "%s is not a valid GUID", guids[i]); return FALSE; } } /* find if any of the other devices exists */ if (depth == G_MAXINT64) { g_autoptr(FuDevice) device_tmp = NULL; for (guint i = 0; guids[i] != NULL; i++) { g_autoptr(GPtrArray) devices = fu_engine_get_devices_by_guid(self, guids[i], NULL); if (devices != NULL && devices->len > 0) { device_tmp = g_object_ref(g_ptr_array_index(devices, 0)); break; } } if (device_tmp == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "No other device %s found", xb_node_get_text(req)); return FALSE; } g_set_object(&device_actual, device_tmp); } else if (depth == -1) { GPtrArray *children; FuDevice *child = NULL; /* look for a child */ children = fu_device_get_children(device); for (guint i = 0; i < children->len; i++) { child = g_ptr_array_index(children, i); if (_fu_device_has_guids_any(child, guids)) break; child = NULL; } if (child == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "No child found with GUID of %s", xb_node_get_text(req)); return FALSE; } g_set_object(&device_actual, child); /* look for a sibling */ } else if (depth == 0) { FuDevice *child = NULL; FuDevice *parent = fu_device_get_parent(device_actual); GPtrArray *children; /* no parent, so look for GUIDs on this device */ if (parent == NULL) { if (!_fu_device_has_guids_any(device_actual, guids)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "No GUID of %s on device %s", xb_node_get_text(req), fu_device_get_name(device_actual)); return FALSE; } return TRUE; } children = fu_device_get_children(parent); for (guint i = 0; i < children->len; i++) { child = g_ptr_array_index(children, i); if (_fu_device_has_guids_any(child, guids)) break; child = NULL; } if (child == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "No sibling found with GUID of %s", xb_node_get_text(req)); return FALSE; } g_set_object(&device_actual, child); /* verify the parent device has the GUID */ } else { if (!_fu_device_has_guids_any(device_actual, guids)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "No GUID of %s on parent device %s", xb_node_get_text(req), fu_device_get_name(device_actual)); return FALSE; } } /* check fwupd version requirement */ if (depth == G_MAXINT64 && fu_device_get_version(device_actual) != NULL) { if (fu_version_compare(fwupd_version, "1.1.0", FWUPD_VERSION_FORMAT_UNKNOWN) < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "requirement 'firmware with version' also needs %s >= 1.1.0", FWUPD_DBUS_SERVICE); return FALSE; } } if (depth == G_MAXINT64 && fu_device_get_version(device_actual) == NULL) { if (fu_version_compare(fwupd_version, "1.2.11", FWUPD_VERSION_FORMAT_UNKNOWN) < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "requirement 'firmware no version' also needs %s >= 1.2.11", FWUPD_DBUS_SERVICE); return FALSE; } } /* get the version of the other device */ version = fu_device_get_version(device_actual); if (version != NULL && xb_node_get_attr(req, "compare") != NULL && !fu_engine_requirements_require_vercmp(req, version, fu_device_get_version_format(device_actual), &error_local)) { if (g_strcmp0(xb_node_get_attr(req, "compare"), "ge") == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "Not compatible with %s version %s, requires >= %s", fu_device_get_name(device_actual), version, xb_node_get_attr(req, "version")); } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "Not compatible with %s: %s", fu_device_get_name(device_actual), error_local->message); } return FALSE; } /* success */ return TRUE; } static gboolean fu_engine_requirements_check_id(FuEngine *self, XbNode *req, GError **error) { FuContext *ctx = fu_engine_get_context(self); g_autoptr(GError) error_local = NULL; const gchar *version; /* sanity check */ if (xb_node_get_text(req) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no requirement value supplied"); return FALSE; } version = fu_context_get_runtime_version(ctx, xb_node_get_text(req)); if (version == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no version available for %s", xb_node_get_text(req)); return FALSE; } if (!fu_engine_requirements_require_vercmp(req, version, FWUPD_VERSION_FORMAT_UNKNOWN, &error_local)) { if (g_strcmp0(xb_node_get_attr(req, "compare"), "ge") == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "Not compatible with %s version %s, requires >= %s", xb_node_get_text(req), version, xb_node_get_attr(req, "version")); } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "Not compatible with %s version: %s", xb_node_get_text(req), error_local->message); } return FALSE; } g_debug("requirement %s %s %s -> %s passed", xb_node_get_attr(req, "version"), xb_node_get_attr(req, "compare"), version, xb_node_get_text(req)); return TRUE; } static gboolean fu_engine_requirements_check_hardware(FuEngine *self, XbNode *req, const gchar *fwupd_version, GError **error) { FuContext *ctx = fu_engine_get_context(self); const gchar *fwupd_required = "1.0.1"; g_auto(GStrv) hwid_split = NULL; /* sanity check */ if (xb_node_get_text(req) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no requirement value supplied"); return FALSE; } /* check fwupd version requirement */ if (g_strstr_len(xb_node_get_text(req), -1, "|") != NULL) fwupd_required = "1.0.8"; if (fu_version_compare(fwupd_version, fwupd_required, FWUPD_VERSION_FORMAT_UNKNOWN) < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "requirement 'hardware' also needs %s >= %s", FWUPD_DBUS_SERVICE, fwupd_required); return FALSE; } /* split and treat as OR */ hwid_split = g_strsplit(xb_node_get_text(req), "|", -1); for (guint i = 0; hwid_split[i] != NULL; i++) { if (fu_context_has_hwid_guid(ctx, hwid_split[i])) { g_debug("HWID provided %s", hwid_split[i]); return TRUE; } } /* nothing matched */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no HWIDs matched %s", xb_node_get_text(req)); return FALSE; } static gboolean fu_engine_requirements_check_not_hardware(FuEngine *self, XbNode *req, const gchar *fwupd_version, GError **error) { FuContext *ctx = fu_engine_get_context(self); g_auto(GStrv) hwid_split = NULL; /* check fwupd version requirement */ if (fu_version_compare(fwupd_version, "1.9.10", FWUPD_VERSION_FORMAT_UNKNOWN) < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "requirement 'not_hardware' also needs %s >= 1.9.10", FWUPD_DBUS_SERVICE); return FALSE; } /* sanity check */ if (xb_node_get_text(req) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no requirement value supplied"); return FALSE; } /* split and treat as OR */ hwid_split = g_strsplit(xb_node_get_text(req), "|", -1); for (guint i = 0; hwid_split[i] != NULL; i++) { if (fu_context_has_hwid_guid(ctx, hwid_split[i])) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "%s HWIDs matched", hwid_split[i]); return FALSE; } } /* nothing matched */ return TRUE; } static gboolean fu_engine_requirements_check_client(FuEngine *self, FuEngineRequest *request, XbNode *req, const gchar *fwupd_version, GError **error) { FwupdFeatureFlags flags; g_auto(GStrv) feature_split = NULL; /* sanity check */ if (xb_node_get_text(req) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no requirement value supplied"); return FALSE; } /* check fwupd version requirement */ if (fu_version_compare(fwupd_version, "1.4.5", FWUPD_VERSION_FORMAT_UNKNOWN) < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "requirement 'client' also needs %s >= 1.4.5", FWUPD_DBUS_SERVICE); return FALSE; } /* split and treat as AND */ feature_split = g_strsplit(xb_node_get_text(req), "|", -1); flags = fu_engine_request_get_feature_flags(request); for (guint i = 0; feature_split[i] != NULL; i++) { FwupdFeatureFlags flag = fwupd_feature_flag_from_string(feature_split[i]); /* not recognized */ if (flag == FWUPD_FEATURE_FLAG_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "client requirement %s unknown", feature_split[i]); return FALSE; } /* not supported */ if ((flags & flag) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "client requirement %s not supported", feature_split[i]); return FALSE; } } /* success */ return TRUE; } static gboolean fu_engine_requirements_check_hard(FuEngine *self, FuRelease *release, XbNode *req, const gchar *fwupd_version, FwupdInstallFlags flags, GError **error) { FuContext *ctx = fu_engine_get_context(self); FuDevice *device = fu_release_get_device(release); FuEngineRequest *request = fu_release_get_request(release); /* ensure component requirement */ if (g_strcmp0(xb_node_get_element(req), "id") == 0) return fu_engine_requirements_check_id(self, req, error); /* ensure firmware requirement */ if (g_strcmp0(xb_node_get_element(req), "firmware") == 0) { if (device == NULL) return TRUE; return fu_engine_requirements_check_firmware(self, req, device, fwupd_version, flags, error); } /* ensure hardware requirement */ if (g_strcmp0(xb_node_get_element(req), "hardware") == 0) { if (device == NULL || fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) return TRUE; if (!fu_context_has_flag(ctx, FU_CONTEXT_FLAG_LOADED_HWINFO)) return TRUE; return fu_engine_requirements_check_hardware(self, req, fwupd_version, error); } if (g_strcmp0(xb_node_get_element(req), "not_hardware") == 0) { if (!fu_context_has_flag(ctx, FU_CONTEXT_FLAG_LOADED_HWINFO)) return TRUE; return fu_engine_requirements_check_not_hardware(self, req, fwupd_version, error); } /* ensure client requirement */ if (g_strcmp0(xb_node_get_element(req), "client") == 0) { return fu_engine_requirements_check_client(self, request, req, fwupd_version, error); } /* not supported */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot handle requirement type %s", xb_node_get_element(req)); return FALSE; } static gboolean fu_engine_requirements_check_soft(FuEngine *self, FuRelease *release, XbNode *req, const gchar *fwupd_version, FwupdInstallFlags flags, GError **error) { g_autoptr(GError) error_local = NULL; if (!fu_engine_requirements_check_hard(self, release, req, fwupd_version, flags, &error_local)) { if (flags & FWUPD_INSTALL_FLAG_IGNORE_REQUIREMENTS) { g_info("ignoring soft-requirement: %s", error_local->message); return TRUE; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } return TRUE; } static gboolean fu_engine_requirements_is_specific_req(XbNode *req) { if (g_strcmp0(xb_node_get_element(req), "firmware") == 0 && xb_node_get_attr(req, "depth") != NULL) return TRUE; if (g_strcmp0(xb_node_get_element(req), "hardware") == 0) return TRUE; return FALSE; } static gchar * fu_engine_requirements_get_newest_fwupd_version(FuEngine *self, FuRelease *release, GError **error) { const gchar *newest_version = "1.0.0"; GPtrArray *reqs = fu_release_get_hard_reqs(release); /* trivial case */ if (reqs == NULL) return g_strdup(newest_version); /* find the newest fwupd requirement */ for (guint i = 0; i < reqs->len; i++) { XbNode *req = g_ptr_array_index(reqs, i); if (g_strcmp0(xb_node_get_text(req), FWUPD_DBUS_SERVICE) == 0 && g_strcmp0(xb_node_get_attr(req, "compare"), "ge") == 0) { const gchar *version = xb_node_get_attr(req, "version"); if (version == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no version provided for requirement %s", xb_node_get_text(req)); return NULL; } /* is this unique, or newer than what we have */ if (newest_version == NULL || fu_version_compare(version, newest_version, FWUPD_VERSION_FORMAT_UNKNOWN) > 0) { newest_version = version; } } } return g_strdup(newest_version); } gboolean fu_engine_requirements_check(FuEngine *self, FuRelease *release, FwupdInstallFlags flags, GError **error) { FuDevice *device = fu_release_get_device(release); GPtrArray *reqs; gboolean has_hardware_req = FALSE; gboolean has_not_hardware_req = FALSE; gboolean has_specific_requirement = FALSE; g_autofree gchar *fwupd_version = NULL; /* get the newest fwupd version requirement */ fwupd_version = fu_engine_requirements_get_newest_fwupd_version(self, release, error); if (fwupd_version == NULL) return FALSE; /* sanity check */ if (device != NULL && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "%s [%s] is not updatable", fu_device_get_name(device), fu_device_get_id(device)); return FALSE; } /* verify protocol */ if (device != NULL && fu_release_get_protocol(release) != NULL && !fu_device_has_protocol(device, fu_release_get_protocol(release))) { g_autofree gchar *protocols = fu_strjoin(",", fu_device_get_protocols(device)); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "release needs protocol %s but device has %s", fu_release_get_protocol(release), protocols); return FALSE; } /* hard requirements */ reqs = fu_release_get_hard_reqs(release); if (reqs != NULL) { for (guint i = 0; i < reqs->len; i++) { XbNode *req = g_ptr_array_index(reqs, i); if (!fu_engine_requirements_check_hard(self, release, req, fwupd_version, flags, error)) return FALSE; if (fu_engine_requirements_is_specific_req(req)) has_specific_requirement = TRUE; if (g_strcmp0(xb_node_get_element(req), "hardware") == 0) has_hardware_req = TRUE; else if (g_strcmp0(xb_node_get_element(req), "not_hardware") == 0) has_not_hardware_req = TRUE; } } /* it does not make sense to allowlist and denylist at the same time */ if (has_hardware_req && has_not_hardware_req) { g_set_error_literal( error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "using hardware and not_hardware at the same time is not supported"); return FALSE; } /* if a device uses a generic ID (i.e. not matching the OEM) then check to make sure the * firmware is specific enough, e.g. by using a CHID or depth requirement */ if (device != NULL && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED) && fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_ENFORCE_REQUIRES) && !has_specific_requirement) { #ifdef SUPPORTED_BUILD g_set_error_literal( error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "generic GUID requires a CHID, child, parent or sibling requirement"); return FALSE; #else if ((flags & FWUPD_INSTALL_FLAG_IGNORE_REQUIREMENTS) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "generic GUID requires --force, a CHID, child, parent " "or sibling requirement"); return FALSE; } g_info("ignoring enforce-requires requirement due to --force"); #endif } /* soft requirements */ reqs = fu_release_get_soft_reqs(release); if (reqs != NULL) { for (guint i = 0; i < reqs->len; i++) { XbNode *req = g_ptr_array_index(reqs, i); if (!fu_engine_requirements_check_soft(self, release, req, fwupd_version, flags, error)) return FALSE; } } /* success */ return TRUE; } fwupd-2.0.10/src/fu-engine-requirements.h000066400000000000000000000005541501337203100202540ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-engine.h" #include "fu-release.h" gboolean fu_engine_requirements_check(FuEngine *engine, FuRelease *release, FwupdInstallFlags flags, GError **error) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/src/fu-engine.c000066400000000000000000010633171501337203100155350ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuEngine" #include "config.h" #include #ifdef HAVE_GIO_UNIX #include #endif #ifdef HAVE_PASSIM #include #endif #include #include #ifdef HAVE_UTSNAME_H #include #endif #ifdef HAVE_AUXV_H #include #endif #include #ifdef _WIN32 #include #include #include #endif #include #include "fwupd-common-private.h" #include "fwupd-device-private.h" #include "fwupd-enums-private.h" #include "fwupd-remote-private.h" #include "fwupd-resources.h" #include "fwupd-security-attr-private.h" #include "fu-backend-private.h" #include "fu-bios-setting.h" #include "fu-bios-settings-private.h" #include "fu-config-private.h" #include "fu-context-private.h" #include "fu-coswid-firmware.h" #include "fu-debug.h" #include "fu-device-list.h" #include "fu-device-private.h" #include "fu-device-progress.h" #include "fu-engine-emulator.h" #include "fu-engine-helper.h" #include "fu-engine-request.h" #include "fu-engine-requirements.h" #include "fu-engine.h" #include "fu-history.h" #include "fu-idle.h" #include "fu-plugin-builtin.h" #include "fu-plugin-list.h" #include "fu-plugin-private.h" #include "fu-release.h" #include "fu-remote-list.h" #include "fu-remote.h" #include "fu-security-attr-common.h" #include "fu-security-attrs-private.h" #include "fu-udev-device-private.h" #include "fu-uefi-backend.h" #include "fu-usb-backend.h" #include "fu-usb-device-fw-ds20.h" #include "fu-usb-device-ms-ds20.h" #include "fu-usb-device-private.h" #ifdef HAVE_GIO_UNIX #include "fu-unix-seekable-input-stream.h" #endif #ifdef HAVE_UDEV #include "fu-udev-backend.h" #endif #ifdef HAVE_BLUEZ #include "fu-bluez-backend.h" #endif /* only needed until we hard depend on jcat 0.1.3 */ #include #ifdef HAVE_SYSTEMD #include "fu-systemd.h" #endif #define MINIMUM_BATTERY_PERCENTAGE_FALLBACK 10 #define FU_ENGINE_UPDATE_MOTD_DELAY 5 /* s */ #define FU_ENGINE_MAX_METADATA_SIZE 0x2000000 /* 32MB */ #define FU_ENGINE_MAX_SIGNATURE_SIZE 0x100000 /* 1MB */ static void fu_engine_constructed(GObject *obj); static void fu_engine_finalize(GObject *obj); static void fu_engine_ensure_security_attrs(FuEngine *self); static void fu_engine_md_refresh_device(FuEngine *self, FuDevice *device); struct _FuEngine { GObject parent_instance; FuEngineConfig *config; FuRemoteList *remote_list; FuDeviceList *device_list; gboolean only_trusted; gboolean write_history; gboolean host_emulation; guint percentage; FuHistory *history; FuIdle *idle; XbSilo *silo; XbQuery *query_component_by_guid; XbQuery *query_container_checksum1; /* container checksum -> release */ XbQuery *query_container_checksum2; /* artifact checksum -> release */ XbQuery *query_tag_by_guid_version; guint coldplug_id; FuPluginList *plugin_list; GPtrArray *plugin_filter; FuContext *ctx; GHashTable *approved_firmware; /* (nullable) */ GHashTable *blocked_firmware; /* (nullable) */ FuEngineEmulator *emulation; GHashTable *device_changed_allowlist; /* (element-type str int) */ gchar *host_machine_id; JcatContext *jcat_context; gboolean loaded; FuSecurityAttrs *host_security_attrs; GPtrArray *local_monitors; /* (element-type GFileMonitor) */ GMainLoop *acquiesce_loop; guint acquiesce_id; guint acquiesce_delay; guint update_motd_id; FuEngineEmulatorPhase emulator_phase; guint emulator_write_cnt; #ifdef HAVE_PASSIM PassimClient *passim_client; #endif }; enum { PROP_0, PROP_CONTEXT, PROP_LAST }; enum { SIGNAL_CHANGED, SIGNAL_DEVICE_ADDED, SIGNAL_DEVICE_REMOVED, SIGNAL_DEVICE_CHANGED, SIGNAL_DEVICE_REQUEST, SIGNAL_STATUS_CHANGED, SIGNAL_LAST }; static guint signals[SIGNAL_LAST] = {0}; G_DEFINE_TYPE(FuEngine, fu_engine, G_TYPE_OBJECT) gboolean fu_engine_get_loaded(FuEngine *self) { return self->loaded; } static gboolean fu_engine_update_motd_timeout_cb(gpointer user_data) { FuEngine *self = FU_ENGINE(user_data); g_autoptr(GError) error_local = NULL; /* busy */ if (fu_idle_has_inhibit(self->idle, FU_IDLE_INHIBIT_SIGNALS)) return G_SOURCE_CONTINUE; /* update now */ if (!fu_engine_update_motd(self, &error_local)) g_info("failed to update MOTD: %s", error_local->message); self->update_motd_id = 0; return G_SOURCE_REMOVE; } static void fu_engine_update_motd_reset(FuEngine *self) { g_info("resetting update motd timeout"); if (self->update_motd_id != 0) g_source_remove(self->update_motd_id); self->update_motd_id = g_timeout_add_seconds(FU_ENGINE_UPDATE_MOTD_DELAY, fu_engine_update_motd_timeout_cb, self); } static void fu_engine_emit_changed(FuEngine *self) { g_autoptr(GError) error = NULL; /* do nothing */ if (!self->loaded) return; g_signal_emit(self, signals[SIGNAL_CHANGED], 0); fu_engine_idle_reset(self); /* update the motd */ if (fu_engine_config_get_update_motd(self->config)) fu_engine_update_motd_reset(self); /* update the list of devices */ if (!fu_engine_update_devices_file(self, &error)) g_info("failed to update list of devices: %s", error->message); } static void fu_engine_emit_device_changed_safe(FuEngine *self, FuDevice *device) { /* do nothing */ if (!self->loaded) return; /* invalidate host security attributes */ fu_security_attrs_remove_all(self->host_security_attrs); g_signal_emit(self, signals[SIGNAL_DEVICE_CHANGED], 0, device); } /* get the latest version of the device */ static void fu_engine_emit_device_changed(FuEngine *self, const gchar *device_id) { g_autoptr(FuDevice) device = NULL; g_autoptr(GError) error = NULL; /* get the latest version of this */ device = fu_device_list_get_by_id(self->device_list, device_id, &error); if (device == NULL) { g_warning("cannot emit device-changed: %s", error->message); return; } fu_engine_emit_device_changed_safe(self, device); } FuContext * fu_engine_get_context(FuEngine *self) { return self->ctx; } static void fu_engine_set_status(FuEngine *self, FwupdStatus status) { /* emit changed */ g_signal_emit(self, signals[SIGNAL_STATUS_CHANGED], 0, status); } static void fu_engine_generic_notify_cb(FuDevice *device, GParamSpec *pspec, FuEngine *self) { if (fu_idle_has_inhibit(self->idle, FU_IDLE_INHIBIT_SIGNALS) && !g_hash_table_contains(self->device_changed_allowlist, fu_device_get_id(device))) { g_debug("suppressing notification from %s as transaction is in progress", fu_device_get_id(device)); return; } fu_engine_emit_device_changed(self, fu_device_get_id(device)); } static void fu_engine_ensure_device_problem_priority_full(FuEngine *self, FuDevice *device, FuDevice *device_tmp) { /* not a match */ if (g_strcmp0(fu_device_get_id(device_tmp), fu_device_get_equivalent_id(device)) != 0 && g_strcmp0(fu_device_get_equivalent_id(device_tmp), fu_device_get_id(device)) != 0) return; /* new device is better */ if (fu_device_get_priority(device_tmp) < fu_device_get_priority(device)) { fu_device_add_problem(device_tmp, FWUPD_DEVICE_PROBLEM_LOWER_PRIORITY); fu_device_remove_problem(device, FWUPD_DEVICE_PROBLEM_LOWER_PRIORITY); return; } /* old device is better */ if (fu_device_get_priority(device_tmp) > fu_device_get_priority(device)) { fu_device_remove_problem(device_tmp, FWUPD_DEVICE_PROBLEM_LOWER_PRIORITY); fu_device_add_problem(device, FWUPD_DEVICE_PROBLEM_LOWER_PRIORITY); return; } /* the plugin needs to tell us which one is better! */ g_warning("no priority difference, unsetting both"); fu_device_remove_problem(device, FWUPD_DEVICE_PROBLEM_LOWER_PRIORITY); fu_device_remove_problem(device_tmp, FWUPD_DEVICE_PROBLEM_LOWER_PRIORITY); } static void fu_engine_ensure_device_problem_priority(FuEngine *self, FuDevice *device) { g_autoptr(GPtrArray) devices = fu_device_list_get_active(self->device_list); for (guint i = 0; i < devices->len; i++) { FuDevice *device_tmp = g_ptr_array_index(devices, i); if (g_strcmp0(fu_device_get_id(device_tmp), fu_device_get_id(device)) == 0) continue; fu_engine_ensure_device_problem_priority_full(self, device, device_tmp); } } static void fu_engine_device_equivalent_id_notify_cb(FuDevice *device, GParamSpec *pspec, FuEngine *self) { /* make sure the lower priority equivalent device has the problem */ fu_engine_ensure_device_problem_priority(self, device); } static void fu_engine_history_notify_cb(FuDevice *device, GParamSpec *pspec, FuEngine *self) { if (self->write_history) { g_autoptr(GError) error_local = NULL; if (!fu_history_modify_device(self->history, device, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_debug("ignoring: %s", error_local->message); } else { g_warning("failed to record history for %s: %s", fu_device_get_id(device), error_local->message); } } } fu_engine_emit_device_changed(self, fu_device_get_id(device)); } static void fu_engine_device_request_cb(FuDevice *device, FwupdRequest *request, FuEngine *self) { g_info("Emitting DeviceRequest('Message'='%s')", fwupd_request_get_message(request)); g_signal_emit(self, signals[SIGNAL_DEVICE_REQUEST], 0, request); } static void fu_engine_set_emulator_phase(FuEngine *self, FuEngineEmulatorPhase emulator_phase) { g_info("install phase now %s", fu_engine_emulator_phase_to_string(emulator_phase)); self->emulator_phase = emulator_phase; } static void fu_engine_watch_device(FuEngine *self, FuDevice *device) { g_autoptr(FuDevice) device_old = fu_device_list_get_old(self->device_list, device); if (device_old != NULL) { g_signal_handlers_disconnect_by_func(device_old, fu_engine_generic_notify_cb, self); g_signal_handlers_disconnect_by_func(device_old, fu_engine_history_notify_cb, self); g_signal_handlers_disconnect_by_func(device_old, fu_engine_device_request_cb, self); } g_signal_connect(FU_DEVICE(device), "notify::flags", G_CALLBACK(fu_engine_generic_notify_cb), self); g_signal_connect(FU_DEVICE(device), "notify::problems", G_CALLBACK(fu_engine_generic_notify_cb), self); g_signal_connect(FU_DEVICE(device), "notify::update-message", G_CALLBACK(fu_engine_generic_notify_cb), self); g_signal_connect(FU_DEVICE(device), "notify::update-image", G_CALLBACK(fu_engine_generic_notify_cb), self); g_signal_connect(FU_DEVICE(device), "notify::update-state", G_CALLBACK(fu_engine_history_notify_cb), self); g_signal_connect(FU_DEVICE(device), "notify::update-error", G_CALLBACK(fu_engine_history_notify_cb), self); g_signal_connect(FU_DEVICE(device), "notify::equivalent-id", G_CALLBACK(fu_engine_device_equivalent_id_notify_cb), self); g_signal_connect(FU_DEVICE(device), "request", G_CALLBACK(fu_engine_device_request_cb), self); } static void fu_engine_ensure_device_power_inhibit(FuEngine *self, FuDevice *device) { if (fu_engine_config_get_ignore_power(self->config)) return; if (fu_device_is_updatable(device) && fu_device_has_flag(device, FWUPD_DEVICE_FLAG_REQUIRE_AC) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED) && !fu_power_state_is_ac(fu_context_get_power_state(self->ctx))) { fu_device_add_problem(device, FWUPD_DEVICE_PROBLEM_REQUIRE_AC_POWER); } else { fu_device_remove_problem(device, FWUPD_DEVICE_PROBLEM_REQUIRE_AC_POWER); } if (fu_device_is_updatable(device) && !fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_IGNORE_SYSTEM_POWER) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED) && fu_context_get_battery_level(self->ctx) != FWUPD_BATTERY_LEVEL_INVALID && fu_context_get_battery_threshold(self->ctx) != FWUPD_BATTERY_LEVEL_INVALID && fu_context_get_battery_level(self->ctx) < fu_context_get_battery_threshold(self->ctx)) { fu_device_add_problem(device, FWUPD_DEVICE_PROBLEM_SYSTEM_POWER_TOO_LOW); } else { fu_device_remove_problem(device, FWUPD_DEVICE_PROBLEM_SYSTEM_POWER_TOO_LOW); } } static void fu_engine_ensure_device_lid_inhibit(FuEngine *self, FuDevice *device) { if (fu_device_is_updatable(device) && fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_NO_LID_CLOSED) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED) && fu_context_get_lid_state(self->ctx) == FU_LID_STATE_CLOSED) { fu_device_add_problem(device, FWUPD_DEVICE_PROBLEM_LID_IS_CLOSED); return; } fu_device_remove_problem(device, FWUPD_DEVICE_PROBLEM_LID_IS_CLOSED); } static void fu_engine_ensure_device_display_required_inhibit(FuEngine *self, FuDevice *device) { if (fu_device_is_updatable(device) && fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_DISPLAY_REQUIRED) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED) && fu_context_get_display_state(self->ctx) == FU_DISPLAY_STATE_DISCONNECTED) { fu_device_add_problem(device, FWUPD_DEVICE_PROBLEM_DISPLAY_REQUIRED); return; } fu_device_remove_problem(device, FWUPD_DEVICE_PROBLEM_DISPLAY_REQUIRED); } static void fu_engine_ensure_device_system_inhibit(FuEngine *self, FuDevice *device) { if (fu_context_has_flag(self->ctx, FU_CONTEXT_FLAG_SYSTEM_INHIBIT) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { fu_device_add_problem(device, FWUPD_DEVICE_PROBLEM_SYSTEM_INHIBIT); return; } fu_device_remove_problem(device, FWUPD_DEVICE_PROBLEM_SYSTEM_INHIBIT); } static gboolean fu_engine_acquiesce_timeout_cb(gpointer user_data) { FuEngine *self = FU_ENGINE(user_data); g_info("system acquiesced after %ums", self->acquiesce_delay); g_main_loop_quit(self->acquiesce_loop); self->acquiesce_id = 0; return G_SOURCE_REMOVE; } static void fu_engine_acquiesce_reset(FuEngine *self) { if (!g_main_loop_is_running(self->acquiesce_loop)) return; g_info("resetting system acquiesce timeout"); if (self->acquiesce_id != 0) g_source_remove(self->acquiesce_id); self->acquiesce_id = g_timeout_add(self->acquiesce_delay, fu_engine_acquiesce_timeout_cb, self); } static void fu_engine_wait_for_acquiesce(FuEngine *self, guint acquiesce_delay) { if (acquiesce_delay == 0) return; self->acquiesce_delay = acquiesce_delay; self->acquiesce_id = g_timeout_add(acquiesce_delay, fu_engine_acquiesce_timeout_cb, self); g_main_loop_run(self->acquiesce_loop); } static void fu_engine_ensure_context_flag_save_events(FuEngine *self) { g_autoptr(GError) error_local = NULL; if (!fu_history_has_emulation_tag(self->history, NULL, &error_local)) { g_debug("ignoring: %s", error_local->message); fu_context_remove_flag(self->ctx, FU_CONTEXT_FLAG_SAVE_EVENTS); return; } fu_context_add_flag(self->ctx, FU_CONTEXT_FLAG_SAVE_EVENTS); } static void fu_engine_device_added_cb(FuDeviceList *device_list, FuDevice *device, FuEngine *self) { fu_engine_watch_device(self, device); fu_engine_ensure_device_problem_priority(self, device); fu_engine_ensure_device_power_inhibit(self, device); fu_engine_ensure_device_lid_inhibit(self, device); fu_engine_ensure_device_display_required_inhibit(self, device); fu_engine_ensure_device_system_inhibit(self, device); fu_engine_acquiesce_reset(self); g_signal_emit(self, signals[SIGNAL_DEVICE_ADDED], 0, device); } static void fu_engine_device_runner_device_removed(FuEngine *self, FuDevice *device) { GPtrArray *plugins = fu_plugin_list_get_all(self->plugin_list); for (guint j = 0; j < plugins->len; j++) { FuPlugin *plugin_tmp = g_ptr_array_index(plugins, j); fu_plugin_runner_device_removed(plugin_tmp, device); } } static void fu_engine_device_removed_cb(FuDeviceList *device_list, FuDevice *device, FuEngine *self) { fu_engine_device_runner_device_removed(self, device); fu_engine_acquiesce_reset(self); g_signal_handlers_disconnect_by_data(device, self); g_signal_emit(self, signals[SIGNAL_DEVICE_REMOVED], 0, device); } static void fu_engine_device_changed_cb(FuDeviceList *device_list, FuDevice *device, FuEngine *self) { fu_engine_watch_device(self, device); fu_engine_emit_device_changed(self, fu_device_get_id(device)); fu_engine_acquiesce_reset(self); } /* add any client-side BKC tags */ static gboolean fu_engine_add_local_release_metadata(FuEngine *self, FuRelease *release, GError **error) { FuDevice *dev = fu_release_get_device(release); GPtrArray *guids; /* no device matched */ if (dev == NULL) return TRUE; /* not set up */ if (self->query_tag_by_guid_version == NULL) return TRUE; /* use prepared query for each GUID */ guids = fu_device_get_guids(dev); for (guint i = 0; i < guids->len; i++) { const gchar *guid = g_ptr_array_index(guids, i); g_autoptr(GError) error_local = NULL; g_autoptr(GPtrArray) tags = NULL; g_auto(XbQueryContext) context = XB_QUERY_CONTEXT_INIT(); /* bind GUID and then query */ xb_value_bindings_bind_str(xb_query_context_get_bindings(&context), 0, guid, NULL); xb_value_bindings_bind_str(xb_query_context_get_bindings(&context), 1, fu_release_get_version(release), NULL); tags = xb_silo_query_with_context(self->silo, self->query_tag_by_guid_version, &context, &error_local); if (tags == NULL) { if (g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) || g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) continue; g_propagate_error(error, g_steal_pointer(&error_local)); fwupd_error_convert(error); return FALSE; } for (guint j = 0; j < tags->len; j++) { XbNode *tag = g_ptr_array_index(tags, j); fu_release_add_tag(release, xb_node_get_text(tag)); } } /* success */ return TRUE; } /* private, for self tests */ void fu_engine_add_remote(FuEngine *self, FwupdRemote *remote) { g_return_if_fail(FU_IS_ENGINE(self)); g_return_if_fail(FWUPD_IS_REMOTE(remote)); fu_remote_list_add_remote(self->remote_list, remote); } static void fu_engine_release_remote_id_changed_cb(FuRelease *release, GParamSpec *pspec, FuEngine *self) { FwupdRemote *remote; const gchar *remote_id = fwupd_release_get_remote_id(FWUPD_RELEASE(release)); if (remote_id == NULL) return; remote = fu_remote_list_get_by_id(self->remote_list, remote_id); if (remote == NULL) { g_warning("no remote found for %s", remote_id); return; } fu_release_set_remote(release, remote); } static gboolean fu_engine_compare_report_trusted(FwupdReport *report_trusted, FwupdReport *report) { if (fwupd_report_has_flag(report_trusted, FWUPD_REPORT_FLAG_FROM_OEM) && !fwupd_report_has_flag(report, FWUPD_REPORT_FLAG_FROM_OEM)) return FALSE; if (fwupd_report_has_flag(report_trusted, FWUPD_REPORT_FLAG_IS_UPGRADE) && !fwupd_report_has_flag(report, FWUPD_REPORT_FLAG_IS_UPGRADE)) return FALSE; if (fwupd_report_get_vendor_id(report_trusted) != 0) { if (fwupd_report_get_vendor_id(report_trusted) != fwupd_report_get_vendor_id(report)) return FALSE; } if (fwupd_report_get_distro_id(report_trusted) != NULL) { if (g_strcmp0(fwupd_report_get_distro_id(report_trusted), fwupd_report_get_distro_id(report)) != 0) return FALSE; } if (fwupd_report_get_distro_version(report_trusted) != NULL) { if (g_strcmp0(fwupd_report_get_distro_version(report_trusted), fwupd_report_get_distro_version(report)) != 0) return FALSE; } if (fwupd_report_get_distro_variant(report_trusted) != NULL) { if (g_strcmp0(fwupd_report_get_distro_variant(report_trusted), fwupd_report_get_distro_variant(report)) != 0) return FALSE; } if (fwupd_report_get_remote_id(report_trusted) != NULL) { if (g_strcmp0(fwupd_report_get_remote_id(report_trusted), fwupd_report_get_remote_id(report)) != 0) return FALSE; } return TRUE; } static void fu_engine_add_trusted_report(FuEngine *self, FuRelease *release) { GPtrArray *reports = fu_release_get_reports(release); GPtrArray *trusted_reports = fu_engine_config_get_trusted_reports(self->config); for (guint i = 0; i < reports->len; i++) { FwupdReport *report = g_ptr_array_index(reports, i); for (guint j = 0; j < trusted_reports->len; j++) { FwupdReport *trusted_report = g_ptr_array_index(trusted_reports, j); if (fu_engine_compare_report_trusted(trusted_report, report)) { g_autofree gchar *str = fwupd_codec_to_string(FWUPD_CODEC(trusted_report)); g_debug("add trusted-report to %s:%s as trusted: %s", fu_release_get_appstream_id(release), fu_release_get_version(release), str); fu_release_add_flag(release, FWUPD_RELEASE_FLAG_TRUSTED_REPORT); return; } } } } gboolean fu_engine_load_release(FuEngine *self, FuRelease *release, FuCabinet *cabinet, XbNode *component, XbNode *rel, FwupdInstallFlags install_flags, GError **error) { g_return_val_if_fail(FU_IS_ENGINE(self), FALSE); g_return_val_if_fail(FU_IS_RELEASE(release), FALSE); g_return_val_if_fail(cabinet == NULL || FU_IS_CABINET(cabinet), FALSE); g_return_val_if_fail(XB_IS_NODE(component), FALSE); g_return_val_if_fail(rel == NULL || XB_IS_NODE(rel), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* load release from XML */ fu_release_set_config(release, self->config); /* set the FwupdRemote when the remote ID is set */ g_signal_connect(FU_RELEASE(release), "notify::remote-id", G_CALLBACK(fu_engine_release_remote_id_changed_cb), self); /* requirements we can check without the daemon */ if (!fu_release_load(release, cabinet, component, rel, install_flags, error)) return FALSE; /* relax these */ if (fu_engine_config_get_ignore_requirements(self->config)) install_flags |= FWUPD_INSTALL_FLAG_IGNORE_REQUIREMENTS; /* additional requirements */ if (!fu_engine_requirements_check(self, release, install_flags, error)) return FALSE; /* match component properties */ if (fu_release_has_flag(release, FWUPD_RELEASE_FLAG_TRUSTED_METADATA)) { FuDevice *device = fu_release_get_device(release); if (device != NULL) { fu_device_ensure_from_component(device, component); if (rel != NULL) fu_device_ensure_from_release(device, rel); } } /* post-ensure checks */ if (!fu_release_check_version(release, component, install_flags, error)) return FALSE; /* add any client-side BKC tags */ if (!fu_engine_add_local_release_metadata(self, release, error)) return FALSE; /* add the trusted report metadata if appropriate */ fu_engine_add_trusted_report(self, release); /* success */ return TRUE; } /* finds the release for the first firmware in the silo that matches this * container or artifact checksum */ static XbNode * fu_engine_get_release_for_checksum(FuEngine *self, const gchar *csum) { g_auto(XbQueryContext) context = XB_QUERY_CONTEXT_INIT(); xb_value_bindings_bind_str(xb_query_context_get_bindings(&context), 0, csum, NULL); if (self->query_container_checksum1 != NULL) { g_autoptr(XbNode) rel = xb_silo_query_first_with_context(self->silo, self->query_container_checksum1, &context, NULL); if (rel != NULL) return g_steal_pointer(&rel); } if (self->query_container_checksum2 != NULL) { g_autoptr(XbNode) rel = xb_silo_query_first_with_context(self->silo, self->query_container_checksum2, &context, NULL); if (rel != NULL) return g_steal_pointer(&rel); } /* failed */ return NULL; } /* does this exist in any enabled remote */ gchar * fu_engine_get_remote_id_for_stream(FuEngine *self, GInputStream *stream) { GChecksumType checksum_types[] = {G_CHECKSUM_SHA256, G_CHECKSUM_SHA1, 0}; g_return_val_if_fail(FU_IS_ENGINE(self), NULL); g_return_val_if_fail(G_IS_INPUT_STREAM(stream), NULL); for (guint i = 0; checksum_types[i] != 0; i++) { g_autofree gchar *csum = NULL; g_autoptr(XbNode) rel = NULL; csum = fu_input_stream_compute_checksum(stream, checksum_types[i], NULL); if (csum != NULL) rel = fu_engine_get_release_for_checksum(self, csum); if (rel != NULL) { const gchar *remote_id = xb_node_query_text(rel, "../../../custom/value[@key='fwupd::RemoteId']", NULL); if (remote_id != NULL) return g_strdup(remote_id); } } return NULL; } /** * fu_engine_unlock: * @self: a #FuEngine * @device_id: a device ID * @error: (nullable): optional return location for an error * * Unlocks a device. * * Returns: %TRUE for success **/ gboolean fu_engine_unlock(FuEngine *self, const gchar *device_id, GError **error) { FuPlugin *plugin; g_autoptr(FuDevice) device = NULL; g_return_val_if_fail(FU_IS_ENGINE(self), FALSE); g_return_val_if_fail(device_id != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* check the device exists */ device = fu_device_list_get_by_id(self->device_list, device_id, error); if (device == NULL) return FALSE; /* get the plugin */ plugin = fu_plugin_list_find_by_name(self->plugin_list, fu_device_get_plugin(device), error); if (plugin == NULL) return FALSE; /* run the correct plugin that added this */ if (!fu_plugin_runner_unlock(plugin, device, error)) return FALSE; /* make the UI update */ fu_engine_emit_device_changed_safe(self, device); fu_engine_emit_changed(self); return TRUE; } gboolean fu_engine_reset_config(FuEngine *self, const gchar *section, GError **error) { /* reset, effective next reboot */ return fu_config_reset_defaults(FU_CONFIG(self->config), section, error); } gboolean fu_engine_modify_config(FuEngine *self, const gchar *section, const gchar *key, const gchar *value, GError **error) { FuPlugin *plugin; g_return_val_if_fail(FU_IS_ENGINE(self), FALSE); g_return_val_if_fail(section != NULL, FALSE); g_return_val_if_fail(key != NULL, FALSE); g_return_val_if_fail(value != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* check keys are valid */ if (g_strcmp0(section, "fwupd") == 0) { const gchar *keys[] = { "ArchiveSizeMax", "ApprovedFirmware", "BlockedFirmware", "DisabledDevices", "DisabledPlugins", "EnumerateAllDevices", "EspLocation", "HostBkc", "IdleTimeout", "IgnorePower", "OnlyTrusted", "P2pPolicy", "ReleaseDedupe", "ReleasePriority", "ShowDevicePrivate", "TestDevices", "TrustedReports", "TrustedUids", "UpdateMotd", "UriSchemes", "VerboseDomains", NULL, }; if (!g_strv_contains(keys, key)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "key %s not supported for [%s]", key, section); return FALSE; } /* many options need a reboot after this */ if (!fu_config_set_value(FU_CONFIG(self->config), section, key, value, error)) return FALSE; /* reload remotes */ if (g_strcmp0(key, "TestDevices") == 0 && !fu_remote_list_set_testing_remote_enabled( self->remote_list, fu_engine_config_get_test_devices(self->config), error)) return FALSE; return TRUE; } /* handled per-plugin */ plugin = fu_plugin_list_find_by_name(self->plugin_list, section, error); if (plugin == NULL) return FALSE; return fu_plugin_runner_modify_config(plugin, key, value, error); } /** * fu_engine_modify_remote: * @self: a #FuEngine * @remote_id: a remote ID * @key: the key, e.g. `Enabled` * @value: the key, e.g. `true` * @error: (nullable): optional return location for an error * * Updates the verification silo entry for a specific device. * * Returns: %TRUE for success **/ gboolean fu_engine_modify_remote(FuEngine *self, const gchar *remote_id, const gchar *key, const gchar *value, GError **error) { const gchar *keys[] = { "ApprovalRequired", "AutomaticReports", "AutomaticSecurityReports", "Enabled", "FirmwareBaseURI", "MetadataURI", "ReportURI", "Username", "Password", NULL, }; /* check keys are valid */ if (!g_strv_contains(keys, key)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "key %s not supported", key); return FALSE; } return fu_remote_list_set_key_value(self->remote_list, remote_id, key, value, error); } static gboolean fu_engine_modify_single_bios_setting(FuEngine *self, const gchar *key, const gchar *value, gboolean force_ro, GError **error) { FwupdBiosSetting *attr = fu_context_get_bios_setting(self->ctx, key); if (attr == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "attribute not found"); return FALSE; } if (!fwupd_bios_setting_write_value(attr, value, error)) return FALSE; if (force_ro) fwupd_bios_setting_set_read_only(attr, TRUE); return TRUE; } /** * fu_engine_modify_bios_settings: * @self: a #FuEngine * @settings: Hashtable of settings/values to configure * @force_ro: a #gboolean indicating if BIOS settings should also be made read-only * @error: (nullable): optional return location for an error * * Use the kernel API to set one or more BIOS settings. * * Returns: %TRUE for success **/ gboolean fu_engine_modify_bios_settings(FuEngine *self, GHashTable *settings, gboolean force_ro, GError **error) { g_autoptr(FuBiosSettings) bios_settings = fu_context_get_bios_settings(self->ctx); gboolean changed = FALSE; GHashTableIter iter; gpointer key, value; g_return_val_if_fail(FU_IS_ENGINE(self), FALSE); g_return_val_if_fail(settings != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); g_hash_table_iter_init(&iter, settings); while (g_hash_table_iter_next(&iter, &key, &value)) { g_autoptr(GError) error_local = NULL; if (value == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "attribute %s missing value", (const gchar *)key); return FALSE; } if (g_strcmp0(key, FWUPD_BIOS_SETTING_SELF_TEST) == 0) { if (fu_bios_settings_get_attr(bios_settings, key) == NULL) { g_autoptr(FwupdBiosSetting) attr = fu_bios_setting_new(); fwupd_bios_setting_set_name(attr, key); fu_bios_settings_add_attribute(bios_settings, attr); } changed = TRUE; continue; } if (!fu_engine_modify_single_bios_setting(self, key, value, force_ro, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) { g_debug("%s", error_local->message); continue; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } changed = TRUE; } if (!changed) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "no BIOS settings needed to be changed"); return FALSE; } if (fu_bios_settings_get_attr(bios_settings, FWUPD_BIOS_SETTING_PENDING_REBOOT) != NULL) { if (!fu_bios_settings_get_pending_reboot(bios_settings, &changed, error)) return FALSE; g_info("pending_reboot is now %d", changed); } return TRUE; } static gboolean fu_engine_remove_device_flag(FuEngine *self, const gchar *device_id, FwupdDeviceFlags flag, GError **error) { g_autoptr(FuDevice) device = NULL; if (flag == FWUPD_DEVICE_FLAG_NOTIFIED) { device = fu_history_get_device_by_id(self->history, device_id, error); if (device == NULL) return FALSE; fu_device_remove_flag(device, flag); return fu_history_modify_device(self->history, device, error); } if (flag == FWUPD_DEVICE_FLAG_EMULATED) { device = fu_device_list_get_by_id(self->device_list, device_id, error); if (device == NULL) return FALSE; if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device %s is not emulated", fu_device_get_id(device)); return FALSE; } if (fu_device_get_backend(device) == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device %s requires backend", fu_device_get_id(device)); return FALSE; } fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); fu_backend_device_removed(fu_device_get_backend(device), device); return TRUE; } if (flag == FWUPD_DEVICE_FLAG_EMULATION_TAG) { device = fu_device_list_get_by_id(self->device_list, device_id, error); if (device == NULL) return FALSE; if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATION_TAG)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device %s is not tagged for emulation", fu_device_get_id(device)); return FALSE; } fu_device_remove_flag(device, flag); if (!fu_history_remove_emulation_tag(self->history, fu_device_get_id(device), error)) return FALSE; fu_engine_ensure_context_flag_save_events(self); return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "flag cannot be removed from client"); return FALSE; } static void fu_engine_emit_device_request_replug_and_install(FuEngine *self, FuDevice *device) { g_autoptr(FwupdRequest) request = fwupd_request_new(); fwupd_request_set_id(request, FWUPD_REQUEST_ID_REPLUG_INSTALL); fwupd_request_set_device_id(request, fu_device_get_id(device)); fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_IMMEDIATE); fwupd_request_add_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); fwupd_request_set_message(request, "Unplug and replug the device, then install the firmware."); g_signal_emit(self, signals[SIGNAL_DEVICE_REQUEST], 0, request); } static void fu_engine_emit_device_request_restart_daemon(FuEngine *self, FuDevice *device) { g_autoptr(FwupdRequest) request = fwupd_request_new(); fwupd_request_set_id(request, FWUPD_REQUEST_ID_RESTART_DAEMON); fwupd_request_set_device_id(request, fu_device_get_id(device)); fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_IMMEDIATE); fwupd_request_add_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); fwupd_request_set_message( request, "Please restart the fwupd service so device enumeration is recorded."); g_signal_emit(self, signals[SIGNAL_DEVICE_REQUEST], 0, request); } static gboolean fu_engine_add_device_flag(FuEngine *self, const gchar *device_id, FwupdDeviceFlags flag, GError **error) { g_autoptr(FuDevice) device = NULL; if (flag == FWUPD_DEVICE_FLAG_REPORTED || flag == FWUPD_DEVICE_FLAG_NOTIFIED) { device = fu_history_get_device_by_id(self->history, device_id, error); if (device == NULL) return FALSE; fu_device_add_flag(device, flag); return fu_history_modify_device(self->history, device, error); } if (flag == FWUPD_DEVICE_FLAG_EMULATION_TAG) { device = fu_device_list_get_by_id(self->device_list, device_id, error); if (device == NULL) return FALSE; if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_CAN_EMULATION_TAG)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device %s cannot be tagged for emulation", fu_device_get_id(device)); return FALSE; } if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATION_TAG)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device %s is already tagged for emulation", fu_device_get_id(device)); return FALSE; } fu_device_add_flag(device, flag); if (!fu_history_add_emulation_tag(self->history, fu_device_get_id(device), error)) return FALSE; if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_INTERNAL)) { fu_engine_emit_device_request_restart_daemon(self, device); } else { fu_engine_emit_device_request_replug_and_install(self, device); } fu_engine_ensure_context_flag_save_events(self); return TRUE; } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "flag cannot be added from client"); return FALSE; } static gboolean fu_engine_modify_device_flags(FuEngine *self, const gchar *device_id, const gchar *value, GError **error) { /* add or remove a subset of device flags */ if (g_str_has_prefix(value, "~")) { return fu_engine_remove_device_flag(self, device_id, fwupd_device_flag_from_string(value + 1), error); } return fu_engine_add_device_flag(self, device_id, fwupd_device_flag_from_string(value), error); } /** * fu_engine_modify_device: * @self: a #FuEngine * @device_id: a device ID * @key: the key, e.g. `Flags` * @value: the key, e.g. `reported` * @error: (nullable): optional return location for an error * * Sets the reported flag for a specific device. This ensures that other * front-end clients for fwupd do not report the same event. * * Returns: %TRUE for success **/ gboolean fu_engine_modify_device(FuEngine *self, const gchar *device_id, const gchar *key, const gchar *value, GError **error) { g_return_val_if_fail(FU_IS_ENGINE(self), FALSE); g_return_val_if_fail(device_id != NULL, FALSE); g_return_val_if_fail(key != NULL, FALSE); g_return_val_if_fail(value != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (g_strcmp0(key, "Flags") == 0) return fu_engine_modify_device_flags(self, device_id, value, error); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "key %s not supported", key); return FALSE; } static const gchar * fu_engine_checksum_type_to_string(GChecksumType checksum_type) { if (checksum_type == G_CHECKSUM_SHA1) return "sha1"; if (checksum_type == G_CHECKSUM_SHA256) return "sha256"; if (checksum_type == G_CHECKSUM_SHA512) return "sha512"; return "sha1"; } /** * fu_engine_verify_update: * @self: a #FuEngine * @device_id: a device ID * @error: (nullable): optional return location for an error * * Updates the verification silo entry for a specific device. * * Returns: %TRUE for success **/ gboolean fu_engine_verify_update(FuEngine *self, const gchar *device_id, FuProgress *progress, GError **error) { FuPlugin *plugin; GPtrArray *checksums; GPtrArray *guids; g_autofree gchar *fn = NULL; g_autofree gchar *localstatedir = NULL; g_autoptr(FuDevice) device = NULL; g_autoptr(GFile) file = NULL; g_autoptr(XbBuilder) builder = xb_builder_new(); g_autoptr(XbBuilderNode) component = NULL; g_autoptr(XbBuilderNode) provides = NULL; g_autoptr(XbBuilderNode) release = NULL; g_autoptr(XbBuilderNode) releases = NULL; g_autoptr(XbSilo) silo = NULL; g_autoptr(FuDeviceProgress) device_progress = NULL; g_return_val_if_fail(FU_IS_ENGINE(self), FALSE); g_return_val_if_fail(device_id != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* check the devices still exists */ device = fu_device_list_get_by_id(self->device_list, device_id, error); if (device == NULL) return FALSE; device_progress = fu_device_progress_new(device, progress); g_return_val_if_fail(device_progress != NULL, FALSE); /* get the plugin */ plugin = fu_plugin_list_find_by_name(self->plugin_list, fu_device_get_plugin(device), error); if (plugin == NULL) return FALSE; /* get the checksum */ checksums = fu_device_get_checksums(device); if (checksums->len == 0) { if (!fu_plugin_runner_verify(plugin, device, progress, FU_PLUGIN_VERIFY_FLAG_NONE, error)) return FALSE; fu_engine_emit_device_changed_safe(self, device); } /* we got nothing */ if (checksums->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device verification not supported"); return FALSE; } /* build XML */ component = xb_builder_node_insert(NULL, "component", "type", "firmware", NULL); provides = xb_builder_node_insert(component, "provides", NULL); guids = fu_device_get_guids(device); for (guint i = 0; i < guids->len; i++) { const gchar *guid = g_ptr_array_index(guids, i); g_autoptr(XbBuilderNode) provide = NULL; provide = xb_builder_node_insert(provides, "firmware", "type", "flashed", NULL); xb_builder_node_set_text(provide, guid, -1); } releases = xb_builder_node_insert(component, "releases", NULL); release = xb_builder_node_insert(releases, "release", "version", fu_device_get_version(device), NULL); for (guint i = 0; i < checksums->len; i++) { const gchar *checksum = g_ptr_array_index(checksums, i); GChecksumType kind = fwupd_checksum_guess_kind(checksum); g_autoptr(XbBuilderNode) csum = NULL; csum = xb_builder_node_insert(release, "checksum", "type", fu_engine_checksum_type_to_string(kind), "target", "content", NULL); xb_builder_node_set_text(csum, checksum, -1); } xb_builder_import_node(builder, component); /* save silo */ localstatedir = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR_PKG); fn = g_strdup_printf("%s/verify/%s.xml", localstatedir, device_id); if (!fu_path_mkdir_parent(fn, error)) return FALSE; file = g_file_new_for_path(fn); silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, error); if (silo == NULL) { fwupd_error_convert(error); return FALSE; } if (!xb_silo_export_file(silo, file, XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE, NULL, error)) return FALSE; /* success */ return TRUE; } static XbNode * fu_engine_get_component_by_guid(FuEngine *self, const gchar *guid) { g_autoptr(GError) error_local = NULL; g_autoptr(XbNode) component = NULL; g_auto(XbQueryContext) context = XB_QUERY_CONTEXT_INIT(); /* no components in silo */ if (self->query_component_by_guid == NULL) return NULL; xb_query_context_set_flags(&context, XB_QUERY_FLAG_USE_INDEXES); xb_value_bindings_bind_str(xb_query_context_get_bindings(&context), 0, guid, NULL); component = xb_silo_query_first_with_context(self->silo, self->query_component_by_guid, &context, &error_local); if (component == NULL) { if (!g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) && !g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) g_warning("ignoring: %s", error_local->message); return NULL; } return g_object_ref(component); } XbNode * fu_engine_get_component_by_guids(FuEngine *self, FuDevice *device) { GPtrArray *guids = fu_device_get_guids(device); XbNode *component = NULL; fu_device_convert_instance_ids(device); for (guint i = 0; i < guids->len; i++) { const gchar *guid = g_ptr_array_index(guids, i); component = fu_engine_get_component_by_guid(self, guid); if (component != NULL) break; } return component; } static XbNode * fu_engine_verify_from_local_metadata(FuEngine *self, FuDevice *device, GError **error) { g_autofree gchar *fn = NULL; g_autofree gchar *localstatedir = NULL; g_autofree gchar *xpath = NULL; g_autoptr(GFile) file = NULL; g_autoptr(XbBuilder) builder = xb_builder_new(); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_autoptr(XbNode) release = NULL; g_autoptr(XbSilo) silo = NULL; localstatedir = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR_PKG); fn = g_strdup_printf("%s/verify/%s.xml", localstatedir, fu_device_get_id(device)); file = g_file_new_for_path(fn); if (!g_file_query_exists(file, NULL)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "failed to find %s", fn); return NULL; } if (!xb_builder_source_load_file(source, file, XB_BUILDER_SOURCE_FLAG_NONE, NULL, error)) { fwupd_error_convert(error); return NULL; } xb_builder_import_source(builder, source); silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, error); if (silo == NULL) { fwupd_error_convert(error); return NULL; } xpath = g_strdup_printf("component/releases/release[@version='%s']", fu_device_get_version(device)); release = xb_silo_query_first(silo, xpath, error); if (release == NULL) return NULL; /* silo has to have same lifecycle as node */ g_object_set_data_full(G_OBJECT(release), "XbSilo", g_steal_pointer(&silo), (GDestroyNotify)g_object_unref); return g_steal_pointer(&release); } static XbNode * fu_engine_verify_from_system_metadata(FuEngine *self, FuDevice *device, GError **error) { FwupdVersionFormat fmt = fu_device_get_version_format(device); GPtrArray *guids = fu_device_get_guids(device); g_autoptr(XbQuery) query = NULL; /* prepare query with bound GUID parameter */ query = xb_query_new_full(self->silo, "components/component[@type='firmware']/" "provides/firmware[@type='flashed'][text()=?]/" "../../releases/release", XB_QUERY_FLAG_OPTIMIZE | XB_QUERY_FLAG_USE_INDEXES, error); if (query == NULL) { fu_error_convert(error); return NULL; } /* use prepared query for each GUID */ for (guint i = 0; i < guids->len; i++) { const gchar *guid = g_ptr_array_index(guids, i); g_autoptr(GError) error_local = NULL; g_autoptr(GPtrArray) releases = NULL; g_auto(XbQueryContext) context = XB_QUERY_CONTEXT_INIT(); /* bind GUID and then query */ xb_value_bindings_bind_str(xb_query_context_get_bindings(&context), 0, guid, NULL); releases = xb_silo_query_with_context(self->silo, query, &context, &error_local); if (releases == NULL) { if (g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) || g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) { g_debug("could not find %s: %s", guid, error_local->message); continue; } g_propagate_error(error, g_steal_pointer(&error_local)); fwupd_error_convert(error); return NULL; } for (guint j = 0; j < releases->len; j++) { XbNode *rel = g_ptr_array_index(releases, j); const gchar *rel_ver = xb_node_get_attr(rel, "version"); g_autofree gchar *tmp_ver = fu_version_parse_from_format(rel_ver, fmt); if (fu_version_compare(tmp_ver, fu_device_get_version(device), fmt) == 0) return g_object_ref(rel); } } /* not found */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "failed to find release"); return NULL; } /** * fu_engine_verify: * @self: a #FuEngine * @device_id: a device ID * @error: (nullable): optional return location for an error * * Verifies a device firmware checksum using the verification silo entry. * * Returns: %TRUE for success **/ gboolean fu_engine_verify(FuEngine *self, const gchar *device_id, FuProgress *progress, GError **error) { FuPlugin *plugin; GPtrArray *checksums; g_autoptr(FuDevice) device = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(GString) xpath_csum = g_string_new(NULL); g_autoptr(XbNode) csum = NULL; g_autoptr(XbNode) release = NULL; g_return_val_if_fail(FU_IS_ENGINE(self), FALSE); g_return_val_if_fail(device_id != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* check the id exists */ device = fu_device_list_get_by_id(self->device_list, device_id, error); if (device == NULL) return FALSE; /* get the plugin */ plugin = fu_plugin_list_find_by_name(self->plugin_list, fu_device_get_plugin(device), error); if (plugin == NULL) return FALSE; /* update the device firmware hashes if possible */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE)) { if (!fu_plugin_runner_verify(plugin, device, progress, FU_PLUGIN_VERIFY_FLAG_NONE, error)) return FALSE; } /* find component in local metadata */ release = fu_engine_verify_from_local_metadata(self, device, &error_local); if (release == NULL) { if (!g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) && !g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } /* try again with the system metadata */ if (release == NULL) { g_autoptr(GError) error_system = NULL; release = fu_engine_verify_from_system_metadata(self, device, &error_system); if (release == NULL) { if (!g_error_matches(error_system, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND) && !g_error_matches(error_system, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA)) { g_propagate_error(error, g_steal_pointer(&error_system)); return FALSE; } } } if (release == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "No release found for version %s", fu_device_get_version(device)); return FALSE; } /* get the matching checksum */ checksums = fu_device_get_checksums(device); if (checksums->len == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "No device checksums for %s", fu_device_get_version(device)); return FALSE; } /* do any of the checksums in the release match any in the device */ for (guint j = 0; j < checksums->len; j++) { const gchar *hash_tmp = g_ptr_array_index(checksums, j); xb_string_append_union(xpath_csum, "checksum[@target='device'][text()='%s']", hash_tmp); xb_string_append_union(xpath_csum, "checksum[@target='content'][text()='%s']", hash_tmp); } csum = xb_node_query_first(release, xpath_csum->str, NULL); if (csum == NULL) { g_autofree gchar *checksums_device = fu_strjoin("|", checksums); g_autoptr(GString) checksums_metadata = g_string_new(NULL); g_autoptr(GPtrArray) csums = NULL; g_autoptr(GString) xpath = g_string_new(NULL); /* get all checksums to display a useful error */ xb_string_append_union(xpath, "checksum[@target='device']"); if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE)) xb_string_append_union(xpath, "checksum[@target='content']"); csums = xb_node_query(release, xpath->str, 0, NULL); if (csums == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "No stored checksums for %s", fu_device_get_version(device)); return FALSE; } for (guint i = 0; i < csums->len; i++) { XbNode *csum_tmp = g_ptr_array_index(csums, i); xb_string_append_union(checksums_metadata, "%s", xb_node_get_text(csum_tmp)); } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "For %s %s expected %s, got %s", fu_device_get_name(device), fu_device_get_version(device), checksums_metadata->str, checksums_device); return FALSE; } /* success */ return TRUE; } gboolean fu_engine_check_trust(FuEngine *self, FuRelease *release, GError **error) { g_autofree gchar *str = fu_release_to_string(release); g_debug("checking trust of %s", str); if (fu_engine_config_get_only_trusted(self->config) && !fu_release_has_flag(release, FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD)) { g_autofree gchar *sysconfdir = fu_path_from_kind(FU_PATH_KIND_SYSCONFDIR_PKG); g_autofree gchar *fn = g_build_filename(sysconfdir, "fwupd.conf", NULL); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "firmware signature missing or not trusted; " "set OnlyTrusted=false in %s ONLY if you are a firmware developer", fn); return FALSE; } return TRUE; } void fu_engine_idle_reset(FuEngine *self) { fu_idle_reset(self->idle); } guint32 fu_engine_idle_inhibit(FuEngine *self, FuIdleInhibit inhibit, const gchar *reason) { return fu_idle_inhibit(self->idle, inhibit, reason); } void fu_engine_idle_uninhibit(FuEngine *self, guint32 token) { fu_idle_uninhibit(self->idle, token); } static gchar * fu_engine_get_boot_time(void) { g_autofree gchar *buf = NULL; g_auto(GStrv) lines = NULL; if (!g_file_get_contents("/proc/stat", &buf, NULL, NULL)) return NULL; lines = g_strsplit(buf, "\n", -1); for (guint i = 0; lines[i] != NULL; i++) { if (g_str_has_prefix(lines[i], "btime ")) return g_strdup(lines[i] + 6); } return NULL; } static FuDevice * fu_engine_get_cpu_device(FuEngine *self) { g_autoptr(GPtrArray) devices = fu_device_list_get_active(self->device_list); for (guint i = 0; i < devices->len; i++) { FuDevice *device = g_ptr_array_index(devices, i); if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_HOST_CPU)) return g_object_ref(device); } return NULL; } static void fu_engine_get_report_metadata_cpu_device(FuEngine *self, GHashTable *hash) { g_autoptr(FuDevice) device = NULL; device = fu_engine_get_cpu_device(self); if (device == NULL) { g_info("failed to find CPU device"); return; } if (fu_device_get_vendor(device) == NULL || fu_device_get_name(device) == NULL) { g_info("not enough data to include CpuModel"); return; } g_hash_table_insert( hash, g_strdup("CpuModel"), g_strdup_printf("%s %s", fu_device_get_vendor(device), fu_device_get_name(device))); } static gboolean fu_engine_get_report_metadata_os_release(GHashTable *hash, GError **error) { #ifdef HOST_MACHINE_SYSTEM_DARWIN g_autofree gchar *stdout = NULL; g_autofree gchar *sw_vers = g_find_program_in_path("sw_vers"); g_auto(GStrv) split = NULL; struct { const gchar *key; const gchar *val; } kvs[] = {{"ProductName:", "DistroName"}, {"ProductVersion:", FWUPD_RESULT_KEY_DISTRO_VERSION}, {"BuildVersion:", FWUPD_RESULT_KEY_DISTRO_VARIANT}, {NULL, NULL}}; /* macOS */ if (sw_vers == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_READ, "No os-release found"); return FALSE; } /* parse from format: * ProductName: Mac OS X * ProductVersion: 10.14.6 * BuildVersion: 18G103 */ if (!g_spawn_command_line_sync(sw_vers, &stdout, NULL, NULL, error)) return FALSE; split = g_strsplit(stdout, "\n", -1); for (guint j = 0; split[j] != NULL; j++) { for (guint i = 0; kvs[i].key != NULL; i++) { if (g_str_has_prefix(split[j], kvs[i].key)) { g_autofree gchar *tmp = g_strdup(split[j] + strlen(kvs[i].key)); g_hash_table_insert(hash, g_strdup(kvs[i].val), g_strdup(g_strstrip(tmp))); } } } g_hash_table_insert(hash, g_strdup(FWUPD_RESULT_KEY_DISTRO_ID), g_strdup("macos")); #else struct { const gchar *key; const gchar *val; } distro_kv[] = {{G_OS_INFO_KEY_ID, FWUPD_RESULT_KEY_DISTRO_ID}, {G_OS_INFO_KEY_NAME, "DistroName"}, {G_OS_INFO_KEY_PRETTY_NAME, "DistroPrettyName"}, {G_OS_INFO_KEY_VERSION_ID, FWUPD_RESULT_KEY_DISTRO_VERSION}, {"VARIANT_ID", FWUPD_RESULT_KEY_DISTRO_VARIANT}, {NULL, NULL}}; /* get all required os-release keys */ for (guint i = 0; distro_kv[i].key != NULL; i++) { g_autofree gchar *tmp = g_get_os_info(distro_kv[i].key); if (tmp != NULL) { g_hash_table_insert(hash, g_strdup(distro_kv[i].val), g_steal_pointer(&tmp)); } } #endif return TRUE; } static GHashTable * fu_engine_load_os_release(const gchar *filename, GError **error) { g_autofree gchar *buf = NULL; g_autofree gchar *filename2 = g_strdup(filename); g_auto(GStrv) lines = NULL; g_autoptr(GHashTable) hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); /* load each line */ if (!g_file_get_contents(filename2, &buf, NULL, error)) return NULL; lines = g_strsplit(buf, "\n", -1); for (guint i = 0; lines[i] != NULL; i++) { gsize len, off = 0; g_auto(GStrv) split = NULL; /* split up into sections */ split = g_strsplit(lines[i], "=", 2); if (g_strv_length(split) < 2) continue; /* remove double quotes if set both ends */ len = strlen(split[1]); if (len == 0) continue; if (split[1][0] == '\"' && split[1][len - 1] == '\"') { off++; len -= 2; } g_hash_table_insert(hash, g_strdup(split[0]), g_strndup(split[1] + off, len)); } return g_steal_pointer(&hash); } static gboolean fu_engine_get_report_metadata_lsb_release(GHashTable *hash, GError **error) { const gchar *fn = "/etc/lsb-release"; g_autoptr(GHashTable) os_release = NULL; struct { const gchar *key; const gchar *val; } distro_kv[] = {{"CHROMEOS_RELEASE_TRACK", "DistroReleaseTrack"}, {"CHROMEOS_RELEASE_BOARD", "DistroReleaseBoard"}, {NULL, NULL}}; if (!g_file_test(fn, G_FILE_TEST_EXISTS)) return TRUE; os_release = fu_engine_load_os_release(fn, error); if (os_release == NULL) return FALSE; for (guint i = 0; distro_kv[i].key != NULL; i++) { const gchar *tmp = g_hash_table_lookup(os_release, distro_kv[i].key); if (tmp != NULL) g_hash_table_insert(hash, g_strdup(distro_kv[i].val), g_strdup(tmp)); } return TRUE; } static gboolean fu_engine_get_report_metadata_kernel_cmdline(GHashTable *hash, GError **error) { g_autofree gchar *cmdline = NULL; cmdline = fu_common_get_kernel_cmdline(error); if (cmdline == NULL) return FALSE; if (cmdline[0] != '\0') g_hash_table_insert(hash, g_strdup("KernelCmdline"), g_steal_pointer(&cmdline)); return TRUE; } static void fu_engine_add_report_metadata_bool(GHashTable *hash, const gchar *key, gboolean value) { g_hash_table_insert(hash, g_strdup(key), g_strdup(value ? "True" : "False")); } #ifdef HAVE_PASSIM static void fu_engine_ensure_passim_client(FuEngine *self) { g_autoptr(GError) error_local = NULL; /* disabled */ if (fu_engine_config_get_p2p_policy(self->config) == FU_P2P_POLICY_NOTHING) return; /* already loaded */ if (passim_client_get_version(self->passim_client) != NULL) return; /* connect to passimd */ if (!passim_client_load(self->passim_client, &error_local)) g_debug("failed to load Passim: %s", error_local->message); if (passim_client_get_version(self->passim_client) != NULL) { fu_engine_add_runtime_version(self, "org.freedesktop.Passim", passim_client_get_version(self->passim_client)); } } #endif GHashTable * fu_engine_get_report_metadata(FuEngine *self, GError **error) { GHashTable *compile_versions = fu_context_get_compile_versions(self->ctx); GHashTable *runtime_versions = fu_context_get_runtime_versions(self->ctx); const gchar *tmp; gchar *btime; #ifdef HAVE_UTSNAME_H struct utsname name_tmp; #endif g_autoptr(GHashTable) hash = NULL; g_autoptr(GList) compile_keys = g_hash_table_get_keys(compile_versions); g_autoptr(GList) runtime_keys = g_hash_table_get_keys(runtime_versions); /* convert all the runtime and compile-time versions */ hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); for (GList *l = compile_keys; l != NULL; l = l->next) { const gchar *id = l->data; const gchar *version = g_hash_table_lookup(compile_versions, id); g_hash_table_insert(hash, g_strdup_printf("CompileVersion(%s)", id), g_strdup(version)); } for (GList *l = runtime_keys; l != NULL; l = l->next) { const gchar *id = l->data; const gchar *version = g_hash_table_lookup(runtime_versions, id); g_hash_table_insert(hash, g_strdup_printf("RuntimeVersion(%s)", id), g_strdup(version)); } fu_engine_get_report_metadata_cpu_device(self, hash); if (!fu_engine_get_report_metadata_os_release(hash, error)) return NULL; if (!fu_engine_get_report_metadata_lsb_release(hash, error)) return NULL; if (!fu_engine_get_report_metadata_kernel_cmdline(hash, error)) return NULL; /* these affect the report credibility */ #ifdef SUPPORTED_BUILD fu_engine_add_report_metadata_bool(hash, "FwupdSupported", TRUE); #else fu_engine_add_report_metadata_bool(hash, "FwupdSupported", FALSE); #endif /* find out what BKC is being targeted to understand "odd" upgrade paths */ tmp = fu_engine_config_get_host_bkc(self->config); if (tmp != NULL) g_hash_table_insert(hash, g_strdup("HostBkc"), g_strdup(tmp)); #ifdef HAVE_PASSIM /* this is useful to know if passim support is actually helping bandwidth use */ fu_engine_ensure_passim_client(self); g_hash_table_insert( hash, g_strdup("PassimDownloadSaving"), g_strdup_printf("%" G_GUINT64_FORMAT, passim_client_get_download_saving(self->passim_client))); #endif /* DMI data */ if (fu_context_has_flag(self->ctx, FU_CONTEXT_FLAG_LOADED_HWINFO)) { struct { const gchar *hwid; const gchar *name; } keys[] = {{FU_HWIDS_KEY_BASEBOARD_MANUFACTURER, "HostBaseboardManufacturer"}, {FU_HWIDS_KEY_BASEBOARD_PRODUCT, "HostBaseboardProduct"}, {FU_HWIDS_KEY_BIOS_MAJOR_RELEASE, "HostBiosMajorRelease"}, {FU_HWIDS_KEY_BIOS_MINOR_RELEASE, "HostBiosMinorRelease"}, {FU_HWIDS_KEY_BIOS_VENDOR, "HostBiosVendor"}, {FU_HWIDS_KEY_BIOS_VERSION, "HostBiosVersion"}, {FU_HWIDS_KEY_FIRMWARE_MAJOR_RELEASE, "HostFirmwareMajorRelease"}, {FU_HWIDS_KEY_FIRMWARE_MINOR_RELEASE, "HostFirmwareMinorRelease"}, {FU_HWIDS_KEY_ENCLOSURE_KIND, "HostEnclosureKind"}, {FU_HWIDS_KEY_FAMILY, "HostFamily"}, {FU_HWIDS_KEY_MANUFACTURER, "HostVendor"}, {FU_HWIDS_KEY_PRODUCT_NAME, "HostProduct"}, {FU_HWIDS_KEY_PRODUCT_SKU, "HostSku"}, {NULL, NULL}}; for (guint i = 0; keys[i].hwid != NULL; i++) { tmp = fu_context_get_hwid_value(self->ctx, keys[i].hwid); if (tmp != NULL) g_hash_table_insert(hash, g_strdup(keys[i].name), g_strdup(tmp)); } } /* kernel version is often important for debugging failures */ #ifdef HAVE_UTSNAME_H memset(&name_tmp, 0, sizeof(struct utsname)); if (uname(&name_tmp) >= 0) { g_hash_table_insert(hash, g_strdup("CpuArchitecture"), g_strdup(name_tmp.machine)); g_hash_table_insert(hash, g_strdup("KernelName"), g_strdup(name_tmp.sysname)); g_hash_table_insert(hash, g_strdup("KernelVersion"), g_strdup(name_tmp.release)); } #endif #ifdef HAVE_AUXV_H /* this is the architecture of the userspace, e.g. i686 would be returned for * glibc-2.40-17.fc41.i686 on kernel-6.12.9-200.fc41.x86_64 */ g_hash_table_insert(hash, g_strdup("PlatformArchitecture"), g_strdup((const gchar *)getauxval(AT_PLATFORM))); #endif /* add the kernel boot time so we can detect a reboot */ btime = fu_engine_get_boot_time(); if (btime != NULL) g_hash_table_insert(hash, g_strdup("BootTime"), btime); /* add context information */ g_hash_table_insert( hash, g_strdup("PowerState"), g_strdup(fu_power_state_to_string(fu_context_get_power_state(self->ctx)))); g_hash_table_insert( hash, g_strdup("DisplayState"), g_strdup(fu_display_state_to_string(fu_context_get_display_state(self->ctx)))); g_hash_table_insert(hash, g_strdup("LidState"), g_strdup(fu_lid_state_to_string(fu_context_get_lid_state(self->ctx)))); g_hash_table_insert(hash, g_strdup("BatteryLevel"), g_strdup_printf("%u", fu_context_get_battery_level(self->ctx))); g_hash_table_insert(hash, g_strdup("BatteryThreshold"), g_strdup_printf("%u", fu_context_get_battery_threshold(self->ctx))); return g_steal_pointer(&hash); } /** * fu_engine_composite_prepare: * @self: a #FuEngine * @devices: (element-type #FuDevice): devices that will be updated * @error: (nullable): optional return location for an error * * Calls into the plugin loader, informing each plugin of the pending upgrade(s). * * Any failure in any plugin will abort all of the actions before they are started. * * Returns: %TRUE for success **/ gboolean fu_engine_composite_prepare(FuEngine *self, GPtrArray *devices, GError **error) { GPtrArray *plugins = fu_plugin_list_get_all(self->plugin_list); gboolean any_emulated = FALSE; /* we are emulating a device */ for (guint i = 0; i < devices->len; i++) { FuDevice *device = g_ptr_array_index(devices, i); if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) any_emulated = TRUE; } if (any_emulated) { if (!fu_engine_emulator_load_phase(self->emulation, self->emulator_phase, FU_ENGINE_EMULATOR_WRITE_COUNT_DEFAULT, error)) return FALSE; } for (guint j = 0; j < plugins->len; j++) { FuPlugin *plugin_tmp = g_ptr_array_index(plugins, j); if (!fu_plugin_runner_composite_prepare(plugin_tmp, devices, error)) return FALSE; } /* save to emulated phase */ if (fu_context_has_flag(self->ctx, FU_CONTEXT_FLAG_SAVE_EVENTS) && !any_emulated) { if (!fu_engine_emulator_save_phase(self->emulation, self->emulator_phase, FU_ENGINE_EMULATOR_WRITE_COUNT_DEFAULT, error)) return FALSE; } /* wait for any device to disconnect and reconnect */ if (!fu_device_list_wait_for_replug(self->device_list, error)) { g_prefix_error(error, "failed to wait for composite prepare: "); return FALSE; } /* success */ return TRUE; } /** * fu_engine_composite_cleanup: * @self: a #FuEngine * @devices: (element-type #FuDevice): devices that will be updated * @error: (nullable): optional return location for an error * * Calls into the plugin loader, informing each plugin of the pending upgrade(s). * * Returns: %TRUE for success **/ gboolean fu_engine_composite_cleanup(FuEngine *self, GPtrArray *devices, GError **error) { GPtrArray *plugins = fu_plugin_list_get_all(self->plugin_list); gboolean any_emulated = FALSE; /* we are emulating a device */ for (guint i = 0; i < devices->len; i++) { FuDevice *device = g_ptr_array_index(devices, i); if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) any_emulated = TRUE; } if (any_emulated) { if (!fu_engine_emulator_load_phase(self->emulation, self->emulator_phase, FU_ENGINE_EMULATOR_WRITE_COUNT_DEFAULT, error)) return FALSE; } for (guint j = 0; j < plugins->len; j++) { FuPlugin *plugin_tmp = g_ptr_array_index(plugins, j); if (!fu_plugin_runner_composite_cleanup(plugin_tmp, devices, error)) return FALSE; } /* save to emulated phase */ if (fu_context_has_flag(self->ctx, FU_CONTEXT_FLAG_SAVE_EVENTS) && !any_emulated) { if (!fu_engine_emulator_save_phase(self->emulation, self->emulator_phase, FU_ENGINE_EMULATOR_WRITE_COUNT_DEFAULT, error)) return FALSE; } /* wait for any device to disconnect and reconnect */ if (!fu_device_list_wait_for_replug(self->device_list, error)) { g_prefix_error(error, "failed to wait for composite cleanup: "); return FALSE; } /* success */ return TRUE; } static gint fu_engine_sort_release_device_order_release_version_cb(gconstpointer a, gconstpointer b) { FuRelease *na = *((FuRelease **)a); FuRelease *nb = *((FuRelease **)b); return fu_release_compare(na, nb); } static gboolean fu_engine_publish_release(FuEngine *self, FuRelease *release, GError **error) { #ifdef HAVE_PASSIM FuDevice *device = fu_release_get_device(release); GInputStream *stream = fu_release_get_stream(release); /* lazy load */ fu_engine_ensure_passim_client(self); /* send to passimd, if enabled and running */ if (passim_client_get_version(self->passim_client) != NULL && fu_engine_config_get_p2p_policy(self->config) & FU_P2P_POLICY_FIRMWARE) { gsize streamsz = 0; g_autofree gchar *basename = g_path_get_basename(fu_release_get_filename(release)); g_autofree gchar *checksum = NULL; g_autoptr(GError) error_passim = NULL; g_autoptr(PassimItem) passim_item = passim_item_new(); if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT) || fu_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN)) passim_item_add_flag(passim_item, PASSIM_ITEM_FLAG_NEXT_REBOOT); passim_item_set_max_age(passim_item, 30 * 24 * 60 * 60); passim_item_set_share_limit(passim_item, 50); passim_item_set_basename(passim_item, basename); checksum = fu_input_stream_compute_checksum(stream, G_CHECKSUM_SHA256, error); if (checksum == NULL) return FALSE; if (!fu_input_stream_size(stream, &streamsz, error)) return FALSE; passim_item_set_size(passim_item, streamsz); passim_item_set_stream(passim_item, stream); passim_item_set_hash(passim_item, checksum); if (!passim_client_publish(self->passim_client, passim_item, &error_passim)) { if (!g_error_matches(error_passim, G_IO_ERROR, G_IO_ERROR_EXISTS)) { g_warning("failed to publish firmware to Passim: %s", error_passim->message); } } else { g_debug("published %s to Passim", passim_item_get_hash(passim_item)); } } #endif /* success */ return TRUE; } static gboolean fu_engine_install_release_version_check(FuEngine *self, FuRelease *release, FuDevice *device, GError **error) { FwupdVersionFormat fmt = fu_device_get_version_format(device); const gchar *version_rel = fu_release_get_version(release); const gchar *version_old = fu_release_get_device_version_old(release); if (version_rel != NULL && fu_version_compare(version_old, version_rel, fmt) != 0 && fu_version_compare(version_old, fu_device_get_version(device), fmt) == 0 && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_INSTALL_SKIP_VERSION_CHECK) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION)) { fu_device_set_update_state(device, FWUPD_UPDATE_STATE_FAILED); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "device version not updated on success, %s != %s", version_rel, fu_device_get_version(device)); return FALSE; } /* success */ return TRUE; } /** * fu_engine_install_releases: * @self: a #FuEngine * @request: a #FuEngineRequest * @releases: (element-type FuRelease): a device * @cabinet: a #FuCabinet * @flags: install flags, e.g. %FWUPD_DEVICE_FLAG_UPDATABLE * @error: (nullable): optional return location for an error * * Installs a specific firmware file on one or more install tasks. * * By this point all the requirements and tests should have been done in * fu_engine_requirements_check() so this should not fail before running * the plugin loader. * * Returns: %TRUE for success **/ gboolean fu_engine_install_releases(FuEngine *self, FuEngineRequest *request, GPtrArray *releases, FuCabinet *cabinet, FuProgress *progress, FwupdInstallFlags flags, GError **error) { g_autoptr(FuIdleLocker) locker = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(GPtrArray) devices_new = NULL; /* do not allow auto-shutdown during this time */ locker = fu_idle_locker_new(self->idle, FU_IDLE_INHIBIT_TIMEOUT | FU_IDLE_INHIBIT_SIGNALS, "update"); g_return_val_if_fail(locker != NULL, FALSE); /* use an allow-list for device-changed signals -- only allow any of the composite update * devices to emit signals for the duration of the install */ for (guint i = 0; i < releases->len; i++) { FuRelease *release = g_ptr_array_index(releases, i); FuDevice *device = fu_release_get_device(release); g_hash_table_insert(self->device_changed_allowlist, g_strdup(fu_device_get_id(device)), GUINT_TO_POINTER(1)); } /* install these in the right order */ g_ptr_array_sort(releases, fu_engine_sort_release_device_order_release_version_cb); /* notify the plugins about the composite action */ devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); for (guint i = 0; i < releases->len; i++) { FuRelease *release = g_ptr_array_index(releases, i); FuDevice *device = fu_release_get_device(release); const gchar *logical_id = fu_device_get_logical_id(device); g_info("composite update %u: %s %s->%s (%s, order:%i: priority:%u)", i + 1, fu_device_get_id(device), fu_device_get_version(device), fu_release_get_version(release), logical_id != NULL ? logical_id : "n/a", fu_device_get_order(device), (guint)fu_release_get_priority(release)); g_ptr_array_add(devices, g_object_ref(device)); } fu_engine_set_emulator_phase(self, FU_ENGINE_EMULATOR_PHASE_COMPOSITE_PREPARE); if (!fu_engine_composite_prepare(self, devices, error)) { g_prefix_error(error, "failed to prepare composite action: "); return FALSE; } /* all authenticated, so install all the things */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, releases->len); for (guint i = 0; i < releases->len; i++) { FuRelease *release = g_ptr_array_index(releases, i); GInputStream *stream = fu_release_get_stream(release); if (stream == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no stream for release"); return FALSE; } if (!fu_engine_install_release(self, release, stream, fu_progress_get_child(progress), flags, error)) { g_autoptr(GError) error_local = NULL; if (!fu_engine_composite_cleanup(self, devices, &error_local)) { g_warning("failed to cleanup failed composite action: %s", error_local->message); } return FALSE; } fu_progress_step_done(progress); } /* set all the device statuses back to unknown */ for (guint i = 0; i < releases->len; i++) { FuRelease *release = g_ptr_array_index(releases, i); FuDevice *device = fu_release_get_device(release); fwupd_device_set_status(FWUPD_DEVICE(device), FWUPD_STATUS_UNKNOWN); } /* get a new list of devices in case they replugged */ devices_new = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); for (guint i = 0; i < devices->len; i++) { FuDevice *device; g_autoptr(FuDevice) device_new = NULL; g_autoptr(GError) error_local = NULL; device = g_ptr_array_index(devices, i); device_new = fu_device_list_get_by_id(self->device_list, fu_device_get_id(device), &error_local); if (device_new == NULL) { g_info("failed to find new device: %s", error_local->message); continue; } g_ptr_array_add(devices_new, g_steal_pointer(&device_new)); } /* notify the plugins about the composite action */ fu_engine_set_emulator_phase(self, FU_ENGINE_EMULATOR_PHASE_COMPOSITE_CLEANUP); if (!fu_engine_composite_cleanup(self, devices_new, error)) { g_prefix_error(error, "failed to cleanup composite action: "); return FALSE; } /* for online updates, verify the version changed if not a re-install */ for (guint i = 0; i < releases->len; i++) { FuRelease *release = g_ptr_array_index(releases, i); FuDevice *device = fu_release_get_device(release); g_autoptr(FuDevice) device_new = NULL; g_autoptr(GError) error_local = NULL; device_new = fu_device_list_get_by_id(self->device_list, fu_device_get_id(device), &error_local); if (device_new == NULL) { g_info("failed to find new device: %s", error_local->message); continue; } if (!fu_engine_install_release_version_check(self, release, device_new, error)) return FALSE; } /* upload to Passim */ for (guint i = 0; i < releases->len; i++) { FuRelease *release = g_ptr_array_index(releases, i); if (!fu_engine_publish_release(self, release, error)) return FALSE; } /* allow capturing setup again */ fu_engine_set_emulator_phase(self, FU_ENGINE_EMULATOR_PHASE_SETUP); /* make the UI update */ fu_engine_emit_changed(self); return TRUE; } static void fu_engine_update_release_integrity(FuEngine *self, FuRelease *release, const gchar *key) { g_autoptr(GHashTable) integrity = fu_engine_integrity_new(self->ctx, NULL); if (integrity != NULL) { g_autofree gchar *str = fu_engine_integrity_to_string(integrity); fu_release_add_metadata_item(release, key, str); } } static gboolean fu_engine_add_release_metadata(FuEngine *self, FuRelease *release, GError **error) { g_autoptr(GHashTable) metadata_device = NULL; g_autoptr(GHashTable) metadata_hash = NULL; /* build the version metadata */ metadata_hash = fu_engine_get_report_metadata(self, error); if (metadata_hash == NULL) return FALSE; fu_release_add_metadata(release, metadata_hash); metadata_device = fu_device_report_metadata_pre(fu_release_get_device(release)); if (metadata_device != NULL) fu_release_add_metadata(release, metadata_device); return TRUE; } static gboolean fu_engine_add_release_plugin_metadata(FuEngine *self, FuRelease *release, FuPlugin *plugin, GError **error) { GPtrArray *metadata_sources; /* build the version metadata */ if (fu_plugin_get_report_metadata(plugin) != NULL) fu_release_add_metadata(release, fu_plugin_get_report_metadata(plugin)); /* allow other plugins to contribute metadata too */ metadata_sources = fu_plugin_get_rules(plugin, FU_PLUGIN_RULE_METADATA_SOURCE); if (metadata_sources != NULL) { for (guint i = 0; i < metadata_sources->len; i++) { FuPlugin *plugin_tmp; const gchar *plugin_name = g_ptr_array_index(metadata_sources, i); g_autoptr(GError) error_local = NULL; plugin_tmp = fu_plugin_list_find_by_name(self->plugin_list, plugin_name, &error_local); if (plugin_tmp == NULL) { g_debug("could not add metadata for %s: %s", plugin_name, error_local->message); continue; } if (fu_plugin_get_report_metadata(plugin_tmp) != NULL) { fwupd_release_add_metadata( FWUPD_RELEASE(release), fu_plugin_get_report_metadata(plugin_tmp)); } } } /* measure the "old" system state */ if (fu_plugin_has_flag(plugin, FWUPD_PLUGIN_FLAG_MEASURE_SYSTEM_INTEGRITY)) { fu_engine_update_release_integrity(self, release, "SystemIntegrityOld"); } return TRUE; } static gboolean fu_engine_save_into_backup_remote(FuEngine *self, GBytes *fw, GError **error) { FwupdRemote *remote_tmp = fu_remote_list_get_by_id(self->remote_list, "backup"); g_autofree gchar *localstatepkg = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR_PKG); g_autofree gchar *backupdir = g_build_filename(localstatepkg, "backup", NULL); g_autofree gchar *backupdir_uri = g_strdup_printf("file://%s", backupdir); g_autofree gchar *remotes_path = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR_REMOTES); g_autofree gchar *remotes_fn = g_build_filename(remotes_path, "backup.conf", NULL); g_autofree gchar *archive_checksum = g_compute_checksum_for_bytes(G_CHECKSUM_SHA256, fw); g_autofree gchar *archive_basename = g_strdup_printf("%s.cab", archive_checksum); g_autofree gchar *archive_fn = g_build_filename(backupdir, archive_basename, NULL); g_autoptr(FwupdRemote) remote = fwupd_remote_new(); /* save archive if required */ if (!g_file_test(archive_fn, G_FILE_TEST_EXISTS)) { g_info("saving archive to %s", archive_fn); if (!fu_bytes_set_contents(archive_fn, fw, error)) return FALSE; } /* already exists as an enabled remote */ if (remote_tmp != NULL && fwupd_remote_has_flag(remote_tmp, FWUPD_REMOTE_FLAG_ENABLED)) return TRUE; /* just enable */ if (remote_tmp != NULL) { g_info("enabling remote %s", fwupd_remote_get_id(remote_tmp)); fwupd_remote_add_flag(remote_tmp, FWUPD_REMOTE_FLAG_ENABLED); return fu_remote_save_to_filename(remote_tmp, remotes_fn, NULL, error); } /* create a new remote we can use for re-installing */ g_info("creating new backup remote"); fwupd_remote_add_flag(remote, FWUPD_REMOTE_FLAG_ENABLED); fwupd_remote_set_title(remote, "Backup"); fwupd_remote_set_metadata_uri(remote, backupdir_uri); return fu_remote_save_to_filename(remote, remotes_fn, NULL, error); } /** * fu_engine_install_release: * @self: a #FuEngine * @release: a #FuRelease * @stream: the #GInputStream of the .cab file * @progress: a #FuProgress * @flags: install flags, e.g. %FWUPD_INSTALL_FLAG_ALLOW_OLDER * @error: (nullable): optional return location for an error * * Installs a specific release on a device. * * By this point all the requirements and tests should have been done in * fu_engine_requirements_check() so this should not fail before running * the plugin loader. * * Returns: %TRUE for success **/ gboolean fu_engine_install_release(FuEngine *self, FuRelease *release, GInputStream *stream, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuDevice *device_orig = fu_release_get_device(release); FuEngineRequest *request = fu_release_get_request(release); FuPlugin *plugin; FwupdFeatureFlags feature_flags = FWUPD_FEATURE_FLAG_NONE; GInputStream *stream_fw; const gchar *tmp; g_autoptr(FuDevice) device = NULL; g_autoptr(FuDevice) device_tmp = NULL; g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_ENGINE(self), FALSE); g_return_val_if_fail(FU_IS_RELEASE(release), FALSE); g_return_val_if_fail(FU_IS_PROGRESS(progress), FALSE); g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* optional for tests */ if (request != NULL) feature_flags = fu_engine_request_get_feature_flags(request); /* add the checksum of the container blob if not already set */ if (fwupd_release_get_checksums(FWUPD_RELEASE(release))->len == 0) { GChecksumType checksum_types[] = {G_CHECKSUM_SHA256, G_CHECKSUM_SHA1, 0}; for (guint i = 0; checksum_types[i] != 0; i++) { g_autofree gchar *checksum = fu_input_stream_compute_checksum(stream, checksum_types[i], error); if (checksum == NULL) return FALSE; fwupd_release_add_checksum(FWUPD_RELEASE(release), checksum); } } /* not in bootloader mode */ device = g_object_ref(fu_release_get_device(release)); if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) { /* both optional; the plugin can specify a fallback */ tmp = fwupd_release_get_detach_caption(FWUPD_RELEASE(release)); if (tmp != NULL) fu_device_set_update_message(device, tmp); tmp = fwupd_release_get_detach_image(FWUPD_RELEASE(release)); if (tmp != NULL) fu_device_set_update_image(device, tmp); } /* save to persistent storage so that the device can recover without a network */ if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_SAVE_INTO_BACKUP_REMOTE)) { g_autoptr(GBytes) blob_cab = fu_input_stream_read_bytes(stream, 0, G_MAXSIZE, NULL, error); if (blob_cab == NULL) return FALSE; if (!fu_engine_save_into_backup_remote(self, blob_cab, error)) return FALSE; } /* set this for the callback */ self->write_history = (flags & FWUPD_INSTALL_FLAG_NO_HISTORY) == 0; /* get per-release firmware blob */ stream_fw = fu_release_get_stream(release); if (stream_fw == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to get firmware stream from release"); return FALSE; } /* get the plugin */ plugin = fu_plugin_list_find_by_name(self->plugin_list, fu_device_get_plugin(device), error); if (plugin == NULL) return FALSE; /* add device to database */ if ((flags & FWUPD_INSTALL_FLAG_NO_HISTORY) == 0) { if (!fu_engine_add_release_metadata(self, release, error)) return FALSE; if (!fu_engine_add_release_plugin_metadata(self, release, plugin, error)) return FALSE; if (!fu_history_add_device(self->history, device, release, error)) return FALSE; } /* install firmware blob */ if (!fu_engine_install_blob(self, device, stream_fw, progress, flags, feature_flags, &error_local)) { FwupdUpdateState state = fu_device_get_update_state(device); if (state != FWUPD_UPDATE_STATE_FAILED && state != FWUPD_UPDATE_STATE_FAILED_TRANSIENT) fu_device_set_update_state(device_orig, FWUPD_UPDATE_STATE_FAILED); else fu_device_set_update_state(device_orig, state); fu_device_set_update_error(device_orig, error_local->message); g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } /* the device may have changed */ device_tmp = fu_device_list_get_by_id(self->device_list, fu_device_get_id(device), error); if (device_tmp == NULL) { g_prefix_error(error, "failed to get device after install: "); return FALSE; } g_set_object(&device, device_tmp); /* update state (which updates the database if required) */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT) || fu_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN)) { fu_device_set_update_state(device_orig, FWUPD_UPDATE_STATE_NEEDS_REBOOT); return TRUE; } /* mark success unless needs a reboot */ if (fu_device_get_update_state(device) != FWUPD_UPDATE_STATE_NEEDS_REBOOT) fu_device_set_update_state(device, FWUPD_UPDATE_STATE_SUCCESS); /* wait for the system to acquiesce if required */ if (fu_device_get_acquiesce_delay(device_orig) > 0 && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_BUSY); fu_engine_wait_for_acquiesce(self, fu_device_get_acquiesce_delay(device_orig)); } /* success */ return TRUE; } /** * fu_engine_get_plugins: * @self: a #FuPluginList * * Gets all the plugins that have been added. * * Returns: (transfer none) (element-type FuPlugin): the plugins * * Since: 1.0.8 **/ GPtrArray * fu_engine_get_plugins(FuEngine *self) { g_return_val_if_fail(FU_IS_ENGINE(self), NULL); return fu_plugin_list_get_all(self->plugin_list); } /** * fu_engine_get_plugin_by_name: * @self: a #FuPluginList * @name: a plugin name, e.g. `dfu` * @error: (nullable): optional return location for an error * * Gets a specific plugin. * * Returns: (transfer none): a plugin, or %NULL * * Since: 1.9.6 **/ FuPlugin * fu_engine_get_plugin_by_name(FuEngine *self, const gchar *name, GError **error) { g_return_val_if_fail(FU_IS_ENGINE(self), NULL); return fu_plugin_list_find_by_name(self->plugin_list, name, error); } gboolean fu_engine_emulation_load(FuEngine *self, GInputStream *stream, GError **error) { g_return_val_if_fail(FU_IS_ENGINE(self), FALSE); g_return_val_if_fail(G_IS_INPUT_STREAM(stream), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return fu_engine_emulator_load(self->emulation, stream, error); } gboolean fu_engine_emulation_save(FuEngine *self, GOutputStream *stream, GError **error) { return fu_engine_emulator_save(self->emulation, stream, error); } /** * fu_engine_get_device: * @self: a #FuEngine * @device_id: a device ID * @error: (nullable): optional return location for an error * * Gets a specific device, optionally loading an emulated phase. * * Returns: (transfer full): a device, or %NULL if not found **/ FuDevice * fu_engine_get_device(FuEngine *self, const gchar *device_id, GError **error) { g_autoptr(FuDevice) device = NULL; /* we are emulating a device */ if (self->emulator_phase != FU_ENGINE_EMULATOR_PHASE_SETUP) { g_autoptr(FuDevice) device_old = NULL; device_old = fu_device_list_get_by_id(self->device_list, device_id, NULL); if (device_old != NULL && fu_device_has_flag(device_old, FWUPD_DEVICE_FLAG_EMULATED)) { if (!fu_engine_emulator_load_phase(self->emulation, self->emulator_phase, self->emulator_write_cnt, error)) return NULL; } } /* wait for any device to disconnect and reconnect */ if (!fu_device_list_wait_for_replug(self->device_list, error)) { g_prefix_error(error, "failed to wait for device: "); return NULL; } /* get the new device */ device = fu_device_list_get_by_id(self->device_list, device_id, error); if (device == NULL) return NULL; /* success */ return g_steal_pointer(&device); } /* same as FuDevice->prepare, but with the device open */ static gboolean fu_engine_device_prepare(FuEngine *self, FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { g_autoptr(FuDeviceLocker) locker = fu_device_locker_new(device, error); if (locker == NULL) { g_prefix_error(error, "failed to open device for prepare: "); return FALSE; } /* check battery level is sane */ if (fu_device_get_battery_level(device) > 0 && fu_device_get_battery_level(device) < fu_device_get_battery_threshold(device)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_BATTERY_LEVEL_TOO_LOW, "battery level is too low: %u%%", fu_device_get_battery_level(device)); return FALSE; } return fu_device_prepare(device, progress, flags, error); } /* same as FuDevice->cleanup, but with the device open */ static gboolean fu_engine_device_cleanup(FuEngine *self, FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { g_autoptr(FuDeviceLocker) locker = NULL; if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_WILL_DISAPPEAR)) { g_info("skipping device cleanup due to will-disappear flag"); return TRUE; } locker = fu_device_locker_new(device, error); if (locker == NULL) { g_prefix_error(error, "failed to open device for cleanup: "); return FALSE; } return fu_device_cleanup(device, progress, flags, error); } static gboolean fu_engine_device_check_power(FuEngine *self, FuDevice *device, FwupdInstallFlags flags, GError **error) { if (fu_engine_config_get_ignore_power(self->config)) return TRUE; /* not charging */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_REQUIRE_AC) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED) && !fu_power_state_is_ac(fu_context_get_power_state(self->ctx))) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_AC_POWER_REQUIRED, "Cannot install update " "when not on AC power unless forced"); return FALSE; } /* not enough just in case */ if (!fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_IGNORE_SYSTEM_POWER) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED) && fu_context_get_battery_level(self->ctx) != FWUPD_BATTERY_LEVEL_INVALID && fu_context_get_battery_threshold(self->ctx) != FWUPD_BATTERY_LEVEL_INVALID && fu_context_get_battery_level(self->ctx) < fu_context_get_battery_threshold(self->ctx)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_BATTERY_LEVEL_TOO_LOW, "Cannot install update when system battery " "is not at least %u%% unless forced", fu_context_get_battery_threshold(self->ctx)); return FALSE; } /* success */ return TRUE; } static FuFirmware * fu_engine_prepare_firmware(FuEngine *self, const gchar *device_id, GInputStream *stream, FuProgress *progress, FuFirmwareParseFlags flags, GError **error) { g_autoptr(FuDevice) device = NULL; /* the device and plugin both may have changed */ device = fu_engine_get_device(self, device_id, error); if (device == NULL) { g_prefix_error(error, "failed to get device before prepare firmware: "); return NULL; } return fu_device_prepare_firmware(device, stream, progress, flags, error); } static gboolean fu_engine_prepare(FuEngine *self, const gchar *device_id, FuProgress *progress, FwupdInstallFlags flags, GError **error) { GPtrArray *plugins = fu_plugin_list_get_all(self->plugin_list); g_autofree gchar *str = NULL; g_autoptr(FuDevice) device = NULL; /* the device and plugin both may have changed */ device = fu_engine_get_device(self, device_id, error); if (device == NULL) { g_prefix_error(error, "failed to get device before update prepare: "); return FALSE; } fu_device_add_problem(device, FWUPD_DEVICE_PROBLEM_UPDATE_IN_PROGRESS); if (!fu_engine_device_check_power(self, device, flags, error)) return FALSE; str = fu_device_to_string(device); g_info("prepare -> %s", str); if (!fu_engine_device_prepare(self, device, progress, flags, error)) return FALSE; for (guint j = 0; j < plugins->len; j++) { FuPlugin *plugin_tmp = g_ptr_array_index(plugins, j); if (!fu_plugin_runner_prepare(plugin_tmp, device, progress, flags, error)) return FALSE; } /* save to emulated phase */ if (fu_context_has_flag(self->ctx, FU_CONTEXT_FLAG_SAVE_EVENTS) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { if (!fu_engine_emulator_save_phase(self->emulation, self->emulator_phase, FU_ENGINE_EMULATOR_WRITE_COUNT_DEFAULT, error)) return FALSE; } /* wait for any device to disconnect and reconnect */ if (!fu_device_list_wait_for_replug(self->device_list, error)) { g_prefix_error(error, "failed to wait for prepare replug: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_engine_cleanup(FuEngine *self, const gchar *device_id, FuProgress *progress, FwupdInstallFlags flags, GError **error) { GPtrArray *plugins = fu_plugin_list_get_all(self->plugin_list); g_autofree gchar *str = NULL; g_autoptr(FuDevice) device = NULL; /* the device and plugin both may have changed */ device = fu_engine_get_device(self, device_id, error); if (device == NULL) { g_prefix_error(error, "failed to get device before update cleanup: "); return FALSE; } fu_device_remove_problem(device, FWUPD_DEVICE_PROBLEM_UPDATE_IN_PROGRESS); str = fu_device_to_string(device); g_info("cleanup -> %s", str); if (!fu_engine_device_cleanup(self, device, progress, flags, error)) return FALSE; for (guint j = 0; j < plugins->len; j++) { FuPlugin *plugin_tmp = g_ptr_array_index(plugins, j); if (!fu_plugin_runner_cleanup(plugin_tmp, device, progress, flags, error)) return FALSE; } /* save to emulated phase */ if (fu_context_has_flag(self->ctx, FU_CONTEXT_FLAG_SAVE_EVENTS) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { if (!fu_engine_emulator_save_phase(self->emulation, self->emulator_phase, FU_ENGINE_EMULATOR_WRITE_COUNT_DEFAULT, error)) return FALSE; } /* wait for any device to disconnect and reconnect */ if (!fu_device_list_wait_for_replug(self->device_list, error)) { g_prefix_error(error, "failed to wait for cleanup replug: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_engine_detach(FuEngine *self, const gchar *device_id, FuProgress *progress, FwupdFeatureFlags feature_flags, GError **error) { FuPlugin *plugin; g_autofree gchar *str = NULL; g_autoptr(FuDevice) device = NULL; g_autoptr(FuDeviceLocker) poll_locker = NULL; g_autoptr(FuDeviceProgress) device_progress = NULL; /* the device and plugin both may have changed */ device = fu_engine_get_device(self, device_id, error); if (device == NULL) { g_prefix_error(error, "failed to get device before update detach: "); return FALSE; } device_progress = fu_device_progress_new(device, progress); g_return_val_if_fail(device_progress != NULL, FALSE); /* pause the polling */ poll_locker = fu_device_poll_locker_new(device, error); if (poll_locker == NULL) return FALSE; str = fu_device_to_string(device); g_info("detach -> %s", str); plugin = fu_plugin_list_find_by_name(self->plugin_list, fu_device_get_plugin(device), error); if (plugin == NULL) return FALSE; if (!fu_plugin_runner_detach(plugin, device, progress, error)) return FALSE; /* support older clients without the ability to do immediate requests */ if ((feature_flags & FWUPD_FEATURE_FLAG_REQUESTS) == 0 && fu_device_get_request_cnt(device, FWUPD_REQUEST_KIND_IMMEDIATE) > 0) { /* fallback to something sane */ if (fu_device_get_update_message(device) == NULL) { g_autofree gchar *tmp = NULL; tmp = g_strdup_printf("Device %s needs to manually be put in update mode", fu_device_get_name(device)); fu_device_set_update_message(device, tmp); } /* abort and require client to re-submit */ fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NEEDS_USER_ACTION, fu_device_get_update_message(device)); return FALSE; } /* save to emulated phase */ if (fu_context_has_flag(self->ctx, FU_CONTEXT_FLAG_SAVE_EVENTS) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { if (!fu_engine_emulator_save_phase(self->emulation, self->emulator_phase, self->emulator_write_cnt, error)) return FALSE; } /* wait for any device to disconnect and reconnect */ if (!fu_device_list_wait_for_replug(self->device_list, error)) { g_prefix_error(error, "failed to wait for detach replug: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_engine_attach(FuEngine *self, const gchar *device_id, FuProgress *progress, GError **error) { FuPlugin *plugin; g_autofree gchar *str = NULL; g_autoptr(FuDevice) device = NULL; g_autoptr(FuDeviceLocker) poll_locker = NULL; g_autoptr(FuDeviceProgress) device_progress = NULL; /* the device and plugin both may have changed */ device = fu_engine_get_device(self, device_id, error); if (device == NULL) { g_prefix_error(error, "failed to get device before update attach: "); return FALSE; } device_progress = fu_device_progress_new(device, progress); g_return_val_if_fail(device_progress != NULL, FALSE); str = fu_device_to_string(device); g_info("attach -> %s", str); plugin = fu_plugin_list_find_by_name(self->plugin_list, fu_device_get_plugin(device), error); if (plugin == NULL) return FALSE; /* pause the polling */ poll_locker = fu_device_poll_locker_new(device, error); if (poll_locker == NULL) return FALSE; if (!fu_plugin_runner_attach(plugin, device, progress, error)) return FALSE; /* save to emulated phase */ if (fu_context_has_flag(self->ctx, FU_CONTEXT_FLAG_SAVE_EVENTS) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { if (!fu_engine_emulator_save_phase(self->emulation, self->emulator_phase, self->emulator_write_cnt, error)) return FALSE; } /* wait for any device to disconnect and reconnect */ if (!fu_device_list_wait_for_replug(self->device_list, error)) { g_prefix_error(error, "failed to wait for attach replug: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_engine_set_progress(FuEngine *self, const gchar *device_id, FuProgress *progress, GError **error) { g_autoptr(FuDevice) device = NULL; /* the device and plugin both may have changed */ device = fu_engine_get_device(self, device_id, error); if (device == NULL) { g_prefix_error(error, "failed to get device before setting progress: "); return FALSE; } fu_device_set_progress(device, progress); return TRUE; } gboolean fu_engine_activate(FuEngine *self, const gchar *device_id, FuProgress *progress, GError **error) { FuPlugin *plugin; g_autofree gchar *str = NULL; g_autoptr(FuDevice) device = NULL; g_return_val_if_fail(FU_IS_ENGINE(self), FALSE); g_return_val_if_fail(device_id != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* check the device exists */ device = fu_device_list_get_by_id(self->device_list, device_id, error); if (device == NULL) return FALSE; str = fu_device_to_string(device); g_info("activate -> %s", str); plugin = fu_plugin_list_find_by_name(self->plugin_list, fu_device_get_plugin(device), error); if (plugin == NULL) return FALSE; if (!fu_plugin_runner_activate(plugin, device, progress, error)) return FALSE; fu_engine_emit_device_changed_safe(self, device); fu_engine_emit_changed(self); return TRUE; } static gboolean fu_engine_reload(FuEngine *self, const gchar *device_id, GError **error) { FuPlugin *plugin; g_autofree gchar *str = NULL; g_autoptr(FuDevice) device = NULL; /* the device and plugin both may have changed */ device = fu_engine_get_device(self, device_id, error); if (device == NULL) { g_prefix_error(error, "failed to get device before update reload: "); return FALSE; } str = fu_device_to_string(device); g_info("reload -> %s", str); plugin = fu_plugin_list_find_by_name(self->plugin_list, fu_device_get_plugin(device), error); if (plugin == NULL) return FALSE; if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_WILL_DISAPPEAR)) { g_info("skipping reload due to will-disappear flag"); return TRUE; } if (!fu_plugin_runner_reload(plugin, device, error)) { g_prefix_error(error, "failed to reload device: "); return FALSE; } /* match again any metadata-provided values */ fu_engine_md_refresh_device(self, device); /* save to emulated phase */ if (fu_context_has_flag(self->ctx, FU_CONTEXT_FLAG_SAVE_EVENTS) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { if (!fu_engine_emulator_save_phase(self->emulation, self->emulator_phase, self->emulator_write_cnt, error)) return FALSE; } /* wait for any device to disconnect and reconnect */ if (!fu_device_list_wait_for_replug(self->device_list, error)) { g_prefix_error(error, "failed to wait for reload replug: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_engine_write_firmware(FuEngine *self, const gchar *device_id, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuPlugin *plugin; g_autofree gchar *str = NULL; g_autoptr(FuDevice) device = NULL; g_autoptr(FuDeviceLocker) poll_locker = NULL; g_autoptr(FuDeviceProgress) device_progress = NULL; g_autoptr(GError) error_write = NULL; /* the device and plugin both may have changed */ device = fu_engine_get_device(self, device_id, error); if (device == NULL) { g_prefix_error(error, "failed to get device before update: "); return FALSE; } device_progress = fu_device_progress_new(device, progress); g_return_val_if_fail(device_progress != NULL, FALSE); /* pause the polling */ poll_locker = fu_device_poll_locker_new(device, error); if (poll_locker == NULL) return FALSE; str = fu_device_to_string(device); g_info("update -> %s", str); plugin = fu_plugin_list_find_by_name(self->plugin_list, fu_device_get_plugin(device), error); if (plugin == NULL) return FALSE; if (!fu_plugin_runner_write_firmware(plugin, device, firmware, progress, flags, &error_write)) { g_autofree gchar *str_write = NULL; g_autoptr(GError) error_attach = NULL; g_autoptr(GError) error_cleanup = NULL; if (g_error_matches(error_write, FWUPD_ERROR, FWUPD_ERROR_AC_POWER_REQUIRED) || g_error_matches(error_write, FWUPD_ERROR, FWUPD_ERROR_BATTERY_LEVEL_TOO_LOW) || g_error_matches(error_write, FWUPD_ERROR, FWUPD_ERROR_NEEDS_USER_ACTION) || g_error_matches(error_write, FWUPD_ERROR, FWUPD_ERROR_BROKEN_SYSTEM)) { fu_device_set_update_state(device, FWUPD_UPDATE_STATE_FAILED_TRANSIENT); } else { fu_device_set_update_state(device, FWUPD_UPDATE_STATE_FAILED); } /* this is really helpful for debugging, as we want to dump the device *before* * we run cleanup */ str_write = fu_device_to_string(device); g_debug("failed write-firmware '%s': %s", error_write->message, str_write); /* attach back into runtime then cleanup */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { fu_engine_set_emulator_phase(self, FU_ENGINE_EMULATOR_PHASE_ATTACH); fu_progress_reset(progress); if (!fu_plugin_runner_attach(plugin, device, progress, &error_attach)) { g_warning("failed to attach device after failed update: %s", error_attach->message); } fu_engine_set_emulator_phase(self, FU_ENGINE_EMULATOR_PHASE_CLEANUP); fu_progress_reset(progress); if (!fu_engine_cleanup(self, device_id, progress, flags, &error_cleanup)) { g_warning("failed to update-cleanup after failed update: %s", error_cleanup->message); } } /* return error to client */ g_propagate_error(error, g_steal_pointer(&error_write)); return FALSE; } /* save to emulated phase */ if (fu_context_has_flag(self->ctx, FU_CONTEXT_FLAG_SAVE_EVENTS) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { if (!fu_engine_emulator_save_phase(self->emulation, self->emulator_phase, self->emulator_write_cnt, error)) return FALSE; } /* abort loop before waiting for replug */ if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_INSTALL_LOOP_RESTART)) return TRUE; /* wait for any device to disconnect and reconnect */ if (!fu_device_list_wait_for_replug(self->device_list, error)) { g_prefix_error(error, "failed to wait for write-firmware replug: "); return FALSE; } /* success */ return TRUE; } GBytes * fu_engine_firmware_dump(FuEngine *self, FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(FuDeviceLocker) poll_locker = NULL; /* pause the polling */ poll_locker = fu_device_poll_locker_new(device, error); if (poll_locker == NULL) return NULL; /* open, read, close */ locker = fu_device_locker_new(device, error); if (locker == NULL) { g_prefix_error(error, "failed to open device for firmware read: "); return NULL; } return fu_device_dump_firmware(device, progress, error); } FuFirmware * fu_engine_firmware_read(FuEngine *self, FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) { g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(FuDeviceLocker) poll_locker = NULL; /* pause the polling */ poll_locker = fu_device_poll_locker_new(device, error); if (poll_locker == NULL) return NULL; /* open, read, close */ locker = fu_device_locker_new(device, error); if (locker == NULL) { g_prefix_error(error, "failed to open device for firmware read: "); return NULL; } return fu_device_read_firmware(device, progress, error); } static gboolean fu_engine_install_loop(FuEngine *self, const gchar *device_id, GInputStream *stream_fw, FwupdInstallFlags flags, FwupdFeatureFlags feature_flags, gboolean *write_complete, FuProgress *progress, GError **error) { g_autoptr(FuDevice) device = NULL; g_autoptr(FuDevice) device_tmp = NULL; g_autoptr(FuFirmware) firmware = NULL; /* not ideal; but best we can do as we don't know how many writes are required */ fu_progress_reset(progress); /* progress */ if (!fu_engine_set_progress(self, device_id, progress, error)) return FALSE; if (fu_progress_get_steps(progress) == 0) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED); fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 1, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, NULL); } else if (fu_progress_get_steps(progress) != 5) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "FuDevice->set_progress did not set " "prepare-firmware,detach,write,attach,reload steps"); return FALSE; } /* some emulations are storing events on the bootloader device */ device = fu_engine_get_device(self, device_id, error); if (device == NULL) { g_prefix_error(error, "failed to get device for flags: "); return FALSE; } /* do not rely on the plugin clearing these */ if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_INSTALL_LOOP_RESTART)) { g_debug("clearing install-loop-restart"); fu_device_remove_private_flag(device, FU_DEVICE_PRIVATE_FLAG_INSTALL_LOOP_RESTART); } if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED)) { g_debug("clearing another-write-required"); fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED); } /* detach->parse->install or parse->detach->install */ if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_DETACH_PREPARE_FIRMWARE)) { /* detach to bootloader mode */ fu_engine_set_emulator_phase(self, FU_ENGINE_EMULATOR_PHASE_DETACH); if (!fu_engine_detach(self, device_id, fu_progress_get_child(progress), feature_flags, error)) { g_prefix_error(error, "failed to detach: "); return FALSE; } fu_progress_step_done(progress); /* parse firmware */ fu_engine_set_emulator_phase(self, FU_ENGINE_EMULATOR_PHASE_INSTALL); firmware = fu_engine_prepare_firmware(self, device_id, stream_fw, fu_progress_get_child(progress), FU_FIRMWARE_PARSE_FLAG_CACHE_STREAM, error); if (firmware == NULL) return FALSE; fu_progress_step_done(progress); /* install */ if (!fu_engine_write_firmware(self, device_id, firmware, fu_progress_get_child(progress), flags, error)) { g_prefix_error(error, "failed to write-firmware: "); return FALSE; } fu_progress_step_done(progress); } else { /* parse firmware */ firmware = fu_engine_prepare_firmware(self, device_id, stream_fw, fu_progress_get_child(progress), FU_FIRMWARE_PARSE_FLAG_CACHE_STREAM, error); if (firmware == NULL) return FALSE; fu_progress_step_done(progress); /* detach to bootloader mode */ fu_engine_set_emulator_phase(self, FU_ENGINE_EMULATOR_PHASE_DETACH); if (!fu_engine_detach(self, device_id, fu_progress_get_child(progress), feature_flags, error)) { g_prefix_error(error, "failed to detach: "); return FALSE; } fu_progress_step_done(progress); /* install */ fu_engine_set_emulator_phase(self, FU_ENGINE_EMULATOR_PHASE_INSTALL); if (!fu_engine_write_firmware(self, device_id, firmware, fu_progress_get_child(progress), flags, error)) { g_prefix_error(error, "failed to write-firmware: "); return FALSE; } fu_progress_step_done(progress); } /* abort loop */ if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_INSTALL_LOOP_RESTART)) { g_debug("restarting install loop, aborting with success"); return TRUE; } /* attach into runtime mode */ fu_engine_set_emulator_phase(self, FU_ENGINE_EMULATOR_PHASE_ATTACH); if (!fu_engine_attach(self, device_id, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to attach: "); return FALSE; } fu_progress_step_done(progress); /* abort loop */ if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_INSTALL_LOOP_RESTART)) { g_debug("restarting install loop, aborting with success"); return TRUE; } /* get the new version number */ fu_engine_set_emulator_phase(self, FU_ENGINE_EMULATOR_PHASE_RELOAD); if (!fu_engine_reload(self, device_id, error)) { g_prefix_error(error, "failed to reload: "); return FALSE; } fu_progress_step_done(progress); /* abort loop */ device_tmp = fu_engine_get_device(self, device_id, error); if (device_tmp == NULL) { g_prefix_error(error, "failed to get device after reload: "); return FALSE; } if (fu_device_has_flag(device_tmp, FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED)) { g_debug("another write required, aborting with success"); return TRUE; } /* success */ *write_complete = TRUE; return TRUE; } gboolean fu_engine_install_blob(FuEngine *self, FuDevice *device, GInputStream *stream_fw, FuProgress *progress, FwupdInstallFlags flags, FwupdFeatureFlags feature_flags, GError **error) { gboolean write_complete = FALSE; gsize streamsz = 0; g_autofree gchar *device_id = NULL; g_autoptr(GTimer) timer = g_timer_new(); g_autoptr(FuDeviceProgress) device_progress = fu_device_progress_new(device, progress); g_return_val_if_fail(device_progress != NULL, FALSE); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_NO_PROFILE); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "prepare"); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 98, NULL); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "cleanup"); /* test the firmware is not an empty blob */ if (!fu_input_stream_size(stream_fw, &streamsz, error)) return FALSE; if (streamsz == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "Firmware is invalid as has zero size"); return FALSE; } /* mark this as modified even if we actually fail to do the update */ fu_device_set_modified_usec(device, g_get_real_time()); /* signal to all the plugins the update is about to happen */ device_id = g_strdup(fu_device_get_id(device)); fu_engine_set_emulator_phase(self, FU_ENGINE_EMULATOR_PHASE_PREPARE); if (!fu_engine_prepare(self, device_id, fu_progress_get_child(progress), flags, error)) return FALSE; fu_progress_step_done(progress); /* plugins can set FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED to run again, but they * must return TRUE rather than an error */ for (self->emulator_write_cnt = 0; self->emulator_write_cnt < FU_ENGINE_EMULATOR_WRITE_COUNT_MAX && !write_complete; self->emulator_write_cnt++) { if (!fu_engine_install_loop(self, device_id, stream_fw, flags, feature_flags, &write_complete, fu_progress_get_child(progress), error)) return FALSE; } if (!write_complete) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "aborting device write loop, limit %u", (guint)FU_ENGINE_EMULATOR_WRITE_COUNT_MAX); return FALSE; } self->emulator_write_cnt = FU_ENGINE_EMULATOR_WRITE_COUNT_DEFAULT; fu_progress_step_done(progress); /* update history database */ fu_device_set_update_state(device, FWUPD_UPDATE_STATE_SUCCESS); fu_device_set_install_duration(device, g_timer_elapsed(timer, NULL)); if ((flags & FWUPD_INSTALL_FLAG_NO_HISTORY) == 0) { if (!fu_history_modify_device(self->history, device, error)) { g_prefix_error(error, "failed to set success: "); return FALSE; } } /* signal to all the plugins the update has happened */ fu_engine_set_emulator_phase(self, FU_ENGINE_EMULATOR_PHASE_CLEANUP); if (!fu_engine_cleanup(self, device_id, fu_progress_get_child(progress), flags, error)) return FALSE; fu_progress_step_done(progress); /* make the UI update */ fu_engine_emit_device_changed(self, device_id); g_info("Updating %s took %f seconds", fu_device_get_name(device), g_timer_elapsed(timer, NULL)); return TRUE; } static FuDevice * fu_engine_get_item_by_id_fallback_history(FuEngine *self, const gchar *id, GError **error) { g_autoptr(GPtrArray) devices = NULL; /* not a wildcard */ if (g_strcmp0(id, FWUPD_DEVICE_ID_ANY) != 0) { g_autoptr(FuDevice) dev = NULL; g_autoptr(GError) error_local = NULL; /* get this one device */ dev = fu_history_get_device_by_id(self->history, id, &error_local); if (dev == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "Failed to find %s in history database: %s", id, error_local->message); return NULL; } /* only useful */ if (fu_device_get_update_state(dev) == FWUPD_UPDATE_STATE_SUCCESS || fu_device_get_update_state(dev) == FWUPD_UPDATE_STATE_FAILED_TRANSIENT || fu_device_get_update_state(dev) == FWUPD_UPDATE_STATE_FAILED) { return g_steal_pointer(&dev); } /* nothing in database */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "Device %s has no results to report", fu_device_get_id(dev)); return NULL; } /* allow '*' for any */ devices = fu_history_get_devices(self->history, error); if (devices == NULL) return NULL; for (guint i = 0; i < devices->len; i++) { FuDevice *dev = g_ptr_array_index(devices, i); if (fu_device_get_update_state(dev) == FWUPD_UPDATE_STATE_SUCCESS || fu_device_get_update_state(dev) == FWUPD_UPDATE_STATE_FAILED_TRANSIENT || fu_device_get_update_state(dev) == FWUPD_UPDATE_STATE_FAILED) return g_object_ref(dev); } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "Failed to find any useful results to report"); return NULL; } static gboolean fu_engine_create_silo_index(FuEngine *self, GError **error) { g_autoptr(GPtrArray) components = NULL; g_autoptr(GError) error_container_checksum1 = NULL; g_autoptr(GError) error_container_checksum2 = NULL; g_autoptr(GError) error_tag_by_guid_version = NULL; /* print what we've got */ components = xb_silo_query(self->silo, "components/component[@type='firmware']", 0, NULL); if (components == NULL) return TRUE; g_info("%u components now in silo", components->len); /* clear old prepared queries */ g_clear_object(&self->query_component_by_guid); g_clear_object(&self->query_container_checksum1); g_clear_object(&self->query_container_checksum2); g_clear_object(&self->query_tag_by_guid_version); /* build the index */ if (!xb_silo_query_build_index(self->silo, "components/component", "type", error)) { fwupd_error_convert(error); return FALSE; } if (!xb_silo_query_build_index(self->silo, "components/component[@type='firmware']/provides/firmware", "type", error)) { fwupd_error_convert(error); return FALSE; } if (!xb_silo_query_build_index(self->silo, "components/component/provides/firmware", NULL, error)) { fwupd_error_convert(error); return FALSE; } if (!xb_silo_query_build_index(self->silo, "components/component[@type='firmware']/tags/tag", "namespace", error)) { fwupd_error_convert(error); return FALSE; } /* create prepared queries to save time later */ self->query_component_by_guid = xb_query_new_full(self->silo, "components/component/provides/firmware[@type=$'flashed'][text()=?]/" "../..", XB_QUERY_FLAG_OPTIMIZE, error); if (self->query_component_by_guid == NULL) { g_prefix_error(error, "failed to prepare query: "); return FALSE; } /* old-style and new-style */ self->query_container_checksum1 = xb_query_new_full(self->silo, "components/component[@type='firmware']/releases/release/" "checksum[@target='container'][text()=?]/..", XB_QUERY_FLAG_OPTIMIZE, &error_container_checksum1); if (self->query_container_checksum1 == NULL) g_debug("ignoring prepared query: %s", error_container_checksum1->message); self->query_container_checksum2 = xb_query_new_full(self->silo, "components/component[@type='firmware']/releases/release/" "artifacts/artifact[@type='binary']/checksum[text()=?]/" "../../..", XB_QUERY_FLAG_OPTIMIZE, &error_container_checksum2); if (self->query_container_checksum2 == NULL) g_debug("ignoring prepared query: %s", error_container_checksum2->message); /* prepare tag query with bound GUID parameter */ self->query_tag_by_guid_version = xb_query_new_full(self->silo, "local/components/component[@merge='append']/provides/" "firmware[text()=?]/../../releases/release[@version=?]/../../" "tags/tag", XB_QUERY_FLAG_OPTIMIZE, &error_tag_by_guid_version); if (self->query_tag_by_guid_version == NULL) g_debug("ignoring prepared query: %s", error_tag_by_guid_version->message); /* success */ return TRUE; } /* for the self tests */ void fu_engine_set_silo(FuEngine *self, XbSilo *silo) { g_autoptr(GError) error_local = NULL; g_return_if_fail(FU_IS_ENGINE(self)); g_return_if_fail(XB_IS_SILO(silo)); g_set_object(&self->silo, silo); if (!fu_engine_create_silo_index(self, &error_local)) g_warning("failed to create indexes: %s", error_local->message); } static gboolean fu_engine_appstream_upgrade_cb(XbBuilderFixup *self, XbBuilderNode *bn, gpointer user_data, GError **error) { if (g_strcmp0(xb_builder_node_get_element(bn), "metadata") == 0) xb_builder_node_set_element(bn, "custom"); return TRUE; } static GInputStream * fu_engine_builder_cabinet_adapter_cb(XbBuilderSource *source, XbBuilderSourceCtx *ctx, gpointer user_data, GCancellable *cancellable, GError **error) { FuEngine *self = FU_ENGINE(user_data); GInputStream *stream = xb_builder_source_ctx_get_stream(ctx); g_autoptr(FuCabinet) cabinet = NULL; g_autoptr(XbSilo) silo = NULL; g_autofree gchar *xml = NULL; /* convert the CAB into metadata XML */ cabinet = fu_engine_build_cabinet_from_stream(self, stream, error); if (cabinet == NULL) return NULL; silo = fu_cabinet_get_silo(cabinet, error); if (silo == NULL) return NULL; xml = xb_silo_export(silo, XB_NODE_EXPORT_FLAG_NONE, error); if (xml == NULL) return NULL; return g_memory_input_stream_new_from_data(g_steal_pointer(&xml), -1, g_free); } static XbBuilderSource * fu_engine_create_metadata_builder_source(FuEngine *self, const gchar *fn, GError **error) { g_autoptr(GFile) file = g_file_new_for_path(fn); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_info("using %s as metadata source", fn); xb_builder_source_add_simple_adapter(source, "application/vnd.ms-cab-compressed," "com.microsoft.cab," ".cab," "application/octet-stream", fu_engine_builder_cabinet_adapter_cb, self, NULL); if (!xb_builder_source_load_file(source, file, XB_BUILDER_SOURCE_FLAG_WATCH_FILE | XB_BUILDER_SOURCE_FLAG_WATCH_DIRECTORY, NULL, error)) { fwupd_error_convert(error); return NULL; } return g_steal_pointer(&source); } static gboolean fu_engine_create_metadata(FuEngine *self, XbBuilder *builder, FwupdRemote *remote, GError **error) { g_autoptr(GPtrArray) files = NULL; const gchar *path; /* find all files in directory */ path = fwupd_remote_get_filename_cache(remote); if (path == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no filename cache for %s", fwupd_remote_get_id(remote)); return FALSE; } files = fu_path_get_files(path, error); if (files == NULL) return FALSE; /* add each source */ for (guint i = 0; i < files->len; i++) { g_autoptr(XbBuilderNode) custom = NULL; g_autoptr(XbBuilderSource) source = NULL; g_autoptr(GError) error_local = NULL; const gchar *fn = g_ptr_array_index(files, i); g_autofree gchar *fn_lowercase = g_ascii_strdown(fn, -1); /* check is cab file */ if (!g_str_has_suffix(fn_lowercase, ".cab")) { g_info("ignoring: %s", fn); continue; } /* build source for file */ source = fu_engine_create_metadata_builder_source(self, fn, &error_local); if (source == NULL) { g_warning("failed to create builder source: %s", error_local->message); continue; } /* add metadata */ custom = xb_builder_node_new("custom"); xb_builder_node_insert_text(custom, "value", fn, "key", "fwupd::FilenameCache", NULL); xb_builder_node_insert_text(custom, "value", fwupd_remote_get_id(remote), "key", "fwupd::RemoteId", NULL); xb_builder_source_set_info(source, custom); xb_builder_import_source(builder, source); } return TRUE; } static void fu_engine_ensure_device_supported(FuEngine *self, FuDevice *device) { gboolean is_supported = FALSE; gboolean update_pending = FALSE; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) releases = NULL; g_autoptr(FuEngineRequest) request = NULL; /* all flags set */ request = fu_engine_request_new(NULL); fu_engine_request_add_flag(request, FU_ENGINE_REQUEST_FLAG_NO_REQUIREMENTS); fu_engine_request_add_flag(request, FU_ENGINE_REQUEST_FLAG_ANY_RELEASE); fu_engine_request_set_feature_flags(request, ~0); /* get all releases that pass the requirements */ releases = fu_engine_get_releases_for_device(self, request, device, &error); if (releases == NULL) { if (!g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO) && !g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_warning("failed to get releases for %s: %s", fu_device_get_name(device), error->message); } } else { if (releases->len > 0) is_supported = TRUE; for (guint i = 0; i < releases->len; i++) { FuRelease *release = FU_RELEASE(g_ptr_array_index(releases, i)); if (fu_release_has_flag(release, FWUPD_RELEASE_FLAG_IS_UPGRADE)) { update_pending = TRUE; break; } } if (update_pending) { fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_UPDATE_PENDING); } else { fu_device_remove_private_flag(device, FU_DEVICE_PRIVATE_FLAG_UPDATE_PENDING); } } /* was supported, now unsupported */ if (!is_supported) { if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_SUPPORTED)) { fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_SUPPORTED); fu_engine_emit_device_changed_safe(self, device); } return; } /* was unsupported, now supported */ if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_SUPPORTED)) { fu_device_add_flag(device, FWUPD_DEVICE_FLAG_SUPPORTED); fu_engine_emit_device_changed_safe(self, device); } } static void fu_engine_md_refresh_device(FuEngine *self, FuDevice *device) { g_autoptr(XbNode) component = fu_engine_get_component_by_guids(self, device); /* set or clear the SUPPORTED flag */ fu_engine_ensure_device_supported(self, device); /* fixup the name and format as needed */ if (component != NULL && !fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_MD_ONLY_CHECKSUM)) fu_device_ensure_from_component(device, component); } static void fu_engine_md_refresh_devices(FuEngine *self) { g_autoptr(GPtrArray) devices = fu_device_list_get_active(self->device_list); for (guint i = 0; i < devices->len; i++) { FuDevice *device = g_ptr_array_index(devices, i); fu_engine_md_refresh_device(self, device); } } static gboolean fu_engine_load_metadata_store_local(FuEngine *self, XbBuilder *builder, FuPathKind path_kind, GError **error) { g_autofree gchar *fn = fu_path_from_kind(path_kind); g_autofree gchar *metadata_path = g_build_filename(fn, "local.d", NULL); g_autoptr(GError) error_local = NULL; g_autoptr(GPtrArray) metadata_fns = NULL; metadata_fns = fu_path_glob(metadata_path, "*.xml", &error_local); if (metadata_fns == NULL) { g_info("ignoring: %s", error_local->message); return TRUE; } for (guint i = 0; i < metadata_fns->len; i++) { const gchar *path = g_ptr_array_index(metadata_fns, i); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_autoptr(GFile) file = g_file_new_for_path(path); g_info("loading local metadata: %s", path); if (!xb_builder_source_load_file(source, file, XB_BUILDER_SOURCE_FLAG_NONE, NULL, error)) { fwupd_error_convert(error); return FALSE; } xb_builder_source_set_prefix(source, "local"); xb_builder_import_source(builder, source); } /* success */ return TRUE; } static gboolean fu_engine_load_metadata_store(FuEngine *self, FuEngineLoadFlags flags, GError **error) { GPtrArray *remotes; XbBuilderCompileFlags compile_flags = XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID; g_autoptr(GFile) xmlb = NULL; g_autoptr(XbBuilder) builder = xb_builder_new(); /* clear existing silo */ g_clear_object(&self->silo); #ifdef SOURCE_VERSION /* invalidate the cache if the fwupd version changes */ xb_builder_append_guid(builder, SOURCE_VERSION); #endif /* verbose profiling */ if (g_getenv("FWUPD_XMLB_VERBOSE") != NULL) { xb_builder_set_profile_flags(builder, XB_SILO_PROFILE_FLAG_XPATH | XB_SILO_PROFILE_FLAG_DEBUG); } /* load each enabled metadata file */ remotes = fu_remote_list_get_all(self->remote_list); for (guint i = 0; i < remotes->len; i++) { const gchar *path = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(GFile) file = NULL; g_autoptr(XbBuilderFixup) fixup = NULL; g_autoptr(XbBuilderNode) custom = NULL; g_autoptr(XbBuilderSource) source = xb_builder_source_new(); FwupdRemote *remote = g_ptr_array_index(remotes, i); if (!fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_ENABLED)) continue; path = fwupd_remote_get_filename_cache(remote); if (!g_file_test(path, G_FILE_TEST_EXISTS)) continue; /* generate all metadata on demand */ if (fwupd_remote_get_kind(remote) == FWUPD_REMOTE_KIND_DIRECTORY) { g_info("loading metadata for remote '%s'", fwupd_remote_get_id(remote)); if (!fu_engine_create_metadata(self, builder, remote, &error_local)) { g_warning("failed to generate remote %s: %s", fwupd_remote_get_id(remote), error_local->message); } continue; } /* save the remote-id in the custom metadata space */ file = g_file_new_for_path(path); if (!xb_builder_source_load_file(source, file, XB_BUILDER_SOURCE_FLAG_NONE, NULL, &error_local)) { g_warning("failed to load remote %s: %s", fwupd_remote_get_id(remote), error_local->message); continue; } /* fix up any legacy installed files */ fixup = xb_builder_fixup_new("AppStreamUpgrade", fu_engine_appstream_upgrade_cb, self, NULL); xb_builder_fixup_set_max_depth(fixup, 3); xb_builder_source_add_fixup(source, fixup); /* add metadata */ custom = xb_builder_node_new("custom"); xb_builder_node_insert_text(custom, "value", path, "key", "fwupd::FilenameCache", NULL); xb_builder_node_insert_text(custom, "value", fwupd_remote_get_id(remote), "key", "fwupd::RemoteId", NULL); xb_builder_source_set_info(source, custom); /* we need to watch for changes? */ xb_builder_import_source(builder, source); } /* add any client-side data, e.g. BKC tags */ if (!fu_engine_load_metadata_store_local(self, builder, FU_PATH_KIND_LOCALSTATEDIR_PKG, error)) return FALSE; if (!fu_engine_load_metadata_store_local(self, builder, FU_PATH_KIND_DATADIR_PKG, error)) return FALSE; /* on a read-only filesystem don't care about the cache GUID */ if (flags & FU_ENGINE_LOAD_FLAG_READONLY) compile_flags |= XB_BUILDER_COMPILE_FLAG_IGNORE_GUID; /* ensure silo is up to date */ if (flags & FU_ENGINE_LOAD_FLAG_NO_CACHE) { g_autoptr(GFileIOStream) iostr = NULL; xmlb = g_file_new_tmp(NULL, &iostr, error); if (xmlb == NULL) return FALSE; } else { g_autofree gchar *cachedirpkg = fu_path_from_kind(FU_PATH_KIND_CACHEDIR_PKG); g_autofree gchar *xmlbfn = g_build_filename(cachedirpkg, "metadata.xmlb", NULL); xmlb = g_file_new_for_path(xmlbfn); } self->silo = xb_builder_ensure(builder, xmlb, compile_flags, NULL, error); if (self->silo == NULL) { g_prefix_error(error, "cannot create metadata.xmlb: "); return FALSE; } /* success */ return fu_engine_create_silo_index(self, error); } static void fu_engine_remote_list_ensure_p2p_policy_remote(FuEngine *self, FwupdRemote *remote) { if (fwupd_remote_get_kind(remote) == FWUPD_REMOTE_KIND_DOWNLOAD) { FuP2pPolicy p2p_policy = fu_engine_config_get_p2p_policy(self->config); if (p2p_policy & FU_P2P_POLICY_METADATA) fwupd_remote_add_flag(remote, FWUPD_REMOTE_FLAG_ALLOW_P2P_METADATA); else fwupd_remote_remove_flag(remote, FWUPD_REMOTE_FLAG_ALLOW_P2P_METADATA); if (p2p_policy & FU_P2P_POLICY_FIRMWARE) fwupd_remote_add_flag(remote, FWUPD_REMOTE_FLAG_ALLOW_P2P_FIRMWARE); else fwupd_remote_remove_flag(remote, FWUPD_REMOTE_FLAG_ALLOW_P2P_FIRMWARE); } } static void fu_engine_config_changed_cb(FuEngineConfig *config, FuEngine *self) { GPtrArray *remotes = fu_remote_list_get_all(self->remote_list); fu_idle_set_timeout(self->idle, fu_engine_config_get_idle_timeout(config)); /* allow changing the hardcoded ESP location */ if (fu_engine_config_get_esp_location(config) != NULL) fu_context_set_esp_location(self->ctx, fu_engine_config_get_esp_location(config)); /* amend P2P policy */ for (guint i = 0; i < remotes->len; i++) { FwupdRemote *remote = g_ptr_array_index(remotes, i); fu_engine_remote_list_ensure_p2p_policy_remote(self, remote); } } static void fu_engine_metadata_changed(FuEngine *self) { g_autoptr(GError) error_local = NULL; if (!fu_engine_load_metadata_store(self, FU_ENGINE_LOAD_FLAG_NONE, &error_local)) g_warning("Failed to reload metadata store: %s", error_local->message); /* set device properties from the metadata */ fu_engine_md_refresh_devices(self); /* invalidate host security attributes */ fu_security_attrs_remove_all(self->host_security_attrs); /* make the UI update */ fu_engine_emit_changed(self); } static void fu_engine_remote_list_changed_cb(FuRemoteList *remote_list, FuEngine *self) { fu_engine_metadata_changed(self); } static void fu_engine_remote_list_added_cb(FuRemoteList *remote_list, FwupdRemote *remote, FuEngine *self) { FuReleasePriority priority = fu_engine_config_get_release_priority(self->config); if (priority == FU_RELEASE_PRIORITY_LOCAL && fwupd_remote_get_kind(remote) != FWUPD_REMOTE_KIND_DOWNLOAD) { g_debug("priority local and %s is not download remote, so bumping", fwupd_remote_get_id(remote)); fwupd_remote_set_priority(remote, fwupd_remote_get_priority(remote) + 1000); } else if (priority == FU_RELEASE_PRIORITY_REMOTE && fwupd_remote_get_kind(remote) == FWUPD_REMOTE_KIND_DOWNLOAD) { g_debug("priority remote and %s is download remote, so bumping", fwupd_remote_get_id(remote)); fwupd_remote_set_priority(remote, fwupd_remote_get_priority(remote) + 1000); } /* set the p2p policy */ fu_engine_remote_list_ensure_p2p_policy_remote(self, remote); } static gint fu_engine_sort_jcat_results_timestamp_cb(gconstpointer a, gconstpointer b) { JcatResult *ra = *((JcatResult **)a); JcatResult *rb = *((JcatResult **)b); if (jcat_result_get_timestamp(ra) < jcat_result_get_timestamp(rb)) return 1; if (jcat_result_get_timestamp(ra) > jcat_result_get_timestamp(rb)) return -1; return 0; } static JcatResult * fu_engine_get_newest_signature_jcat_result(GPtrArray *results, GError **error) { /* sort by timestamp, newest first */ g_ptr_array_sort(results, fu_engine_sort_jcat_results_timestamp_cb); /* get the first signature, ignoring the checksums */ for (guint i = 0; i < results->len; i++) { JcatResult *result = g_ptr_array_index(results, i); if (jcat_result_get_method(result) == JCAT_BLOB_METHOD_SIGNATURE) return g_object_ref(result); } /* should never happen due to %JCAT_VERIFY_FLAG_REQUIRE_SIGNATURE */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no signature method in results"); return NULL; } static JcatResult * fu_engine_get_system_jcat_result(FuEngine *self, FwupdRemote *remote, GError **error) { g_autoptr(GBytes) blob = NULL; g_autoptr(GInputStream) istream = NULL; g_autoptr(GPtrArray) results = NULL; g_autoptr(JcatItem) jcat_item = NULL; g_autoptr(JcatFile) jcat_file = jcat_file_new(); blob = fu_bytes_get_contents(fwupd_remote_get_filename_cache(remote), error); if (blob == NULL) return NULL; istream = fu_input_stream_from_path(fwupd_remote_get_filename_cache_sig(remote), error); if (istream == NULL) return NULL; if (!jcat_file_import_stream(jcat_file, istream, JCAT_IMPORT_FLAG_NONE, NULL, error)) { fu_error_convert(error); return NULL; } jcat_item = jcat_file_get_item_default(jcat_file, error); if (jcat_item == NULL) { fu_error_convert(error); return NULL; } results = jcat_context_verify_item(self->jcat_context, blob, jcat_item, JCAT_VERIFY_FLAG_REQUIRE_CHECKSUM | JCAT_VERIFY_FLAG_REQUIRE_SIGNATURE, error); if (results == NULL) { fu_error_convert(error); return NULL; } /* return the newest signature */ return fu_engine_get_newest_signature_jcat_result(results, error); } static gboolean fu_engine_validate_result_timestamp(JcatResult *jcat_result, JcatResult *jcat_result_old, GError **error) { gint64 delta = 0; g_return_val_if_fail(JCAT_IS_RESULT(jcat_result), FALSE); g_return_val_if_fail(JCAT_IS_RESULT(jcat_result_old), FALSE); if (jcat_result_get_timestamp(jcat_result) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "no signing timestamp"); return FALSE; } if (jcat_result_get_timestamp(jcat_result_old) > 0) { delta = jcat_result_get_timestamp(jcat_result) - jcat_result_get_timestamp(jcat_result_old); } if (delta < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "new signing timestamp was %" G_GINT64_FORMAT " seconds older", -delta); return FALSE; } if (delta > 0) g_info("timestamp increased, so no rollback"); return TRUE; } /** * fu_engine_update_metadata_bytes: * @self: a #FuEngine * @remote_id: a remote ID, e.g. `lvfs` * @bytes_raw: Blob of metadata * @bytes_sig: Blob of metadata signature, typically Jcat binary format * @error: (nullable): optional return location for an error * * Updates the metadata for a specific remote. * * Returns: %TRUE for success **/ gboolean fu_engine_update_metadata_bytes(FuEngine *self, const gchar *remote_id, GBytes *bytes_raw, GBytes *bytes_sig, GError **error) { FwupdRemote *remote; g_autoptr(GError) error_local = NULL; g_autoptr(GInputStream) istream = NULL; g_autoptr(GPtrArray) results = NULL; g_autoptr(JcatFile) jcat_file = jcat_file_new(); g_autoptr(JcatItem) jcat_item = NULL; g_autoptr(JcatResult) jcat_result = NULL; g_autoptr(JcatResult) jcat_result_old = NULL; g_return_val_if_fail(FU_IS_ENGINE(self), FALSE); g_return_val_if_fail(remote_id != NULL, FALSE); g_return_val_if_fail(bytes_raw != NULL, FALSE); g_return_val_if_fail(bytes_sig != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* check remote is valid */ remote = fu_remote_list_get_by_id(self->remote_list, remote_id); if (remote == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "remote %s not found", remote_id); return FALSE; } if (!fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_ENABLED)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "remote %s not enabled", remote_id); return FALSE; } /* verify JCatFile, or create a dummy one from legacy data */ istream = g_memory_input_stream_new_from_bytes(bytes_sig); if (!jcat_file_import_stream(jcat_file, istream, JCAT_IMPORT_FLAG_NONE, NULL, error)) return FALSE; /* this should only be signing one thing */ jcat_item = jcat_file_get_item_default(jcat_file, error); if (jcat_item == NULL) return FALSE; results = jcat_context_verify_item(self->jcat_context, bytes_raw, jcat_item, JCAT_VERIFY_FLAG_REQUIRE_SIGNATURE | JCAT_VERIFY_FLAG_REQUIRE_CHECKSUM, error); if (results == NULL) return FALSE; /* return the newest signature */ jcat_result = fu_engine_get_newest_signature_jcat_result(results, error); if (jcat_result == NULL) return FALSE; /* verify the metadata was signed later than the existing * metadata for this remote to mitigate a rollback attack */ jcat_result_old = fu_engine_get_system_jcat_result(self, remote, &error_local); if (jcat_result_old == NULL) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE)) { g_info("no existing valid keyrings: %s", error_local->message); } else { g_warning("could not get existing keyring result: %s", error_local->message); } } else { if (!fu_engine_validate_result_timestamp(jcat_result, jcat_result_old, error)) return FALSE; } /* save XML and signature to remotes.d */ if (!fu_bytes_set_contents(fwupd_remote_get_filename_cache(remote), bytes_raw, error)) return FALSE; #ifdef HAVE_PASSIM /* lazy load */ fu_engine_ensure_passim_client(self); /* send to passimd, if enabled and running */ if (passim_client_get_version(self->passim_client) != NULL && fwupd_remote_get_username(remote) == NULL && fwupd_remote_get_password(remote) == NULL && fu_engine_config_get_p2p_policy(self->config) & FU_P2P_POLICY_METADATA) { g_autofree gchar *basename = g_path_get_basename(fwupd_remote_get_filename_cache(remote)); g_autoptr(GError) error_passim = NULL; g_autoptr(PassimItem) passim_item = passim_item_new(); passim_item_set_basename(passim_item, basename); passim_item_set_bytes(passim_item, bytes_raw); passim_item_set_max_age(passim_item, fwupd_remote_get_refresh_interval(remote)); passim_item_set_share_limit(passim_item, 50); if (!passim_client_publish(self->passim_client, passim_item, &error_passim)) { if (!g_error_matches(error_passim, G_IO_ERROR, G_IO_ERROR_EXISTS)) { g_warning("failed to publish metadata to Passim: %s", error_passim->message); } } else { g_debug("published %s to Passim", passim_item_get_hash(passim_item)); } } #endif /* save signature to remotes.d */ if (!fu_bytes_set_contents(fwupd_remote_get_filename_cache_sig(remote), bytes_sig, error)) return FALSE; if (!fu_engine_load_metadata_store(self, FU_ENGINE_LOAD_FLAG_NONE, error)) return FALSE; /* refresh SUPPORTED flag on devices */ fu_engine_md_refresh_devices(self); /* invalidate host security attributes */ fu_security_attrs_remove_all(self->host_security_attrs); /* make the UI update */ fu_engine_emit_changed(self); return TRUE; } /** * fu_engine_update_metadata: * @self: a #FuEngine * @remote_id: a remote ID, e.g. `lvfs` * @fd: file descriptor of the metadata * @fd_sig: file descriptor of the metadata signature * @error: (nullable): optional return location for an error * * Updates the metadata for a specific remote. * * Note: this will close the fds when done * * Returns: %TRUE for success **/ gboolean fu_engine_update_metadata(FuEngine *self, const gchar *remote_id, gint fd, gint fd_sig, GError **error) { #ifdef HAVE_GIO_UNIX g_autoptr(GBytes) bytes_raw = NULL; g_autoptr(GBytes) bytes_sig = NULL; g_autoptr(GInputStream) stream_fd = NULL; g_autoptr(GInputStream) stream_sig = NULL; g_return_val_if_fail(FU_IS_ENGINE(self), FALSE); g_return_val_if_fail(remote_id != NULL, FALSE); g_return_val_if_fail(fd > 0, FALSE); g_return_val_if_fail(fd_sig > 0, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* ensures the fd's are closed on error */ stream_fd = fu_unix_seekable_input_stream_new(fd, TRUE); stream_sig = fu_unix_seekable_input_stream_new(fd_sig, TRUE); /* read the entire file into memory */ bytes_raw = fu_input_stream_read_bytes(stream_fd, 0, FU_ENGINE_MAX_METADATA_SIZE, NULL, error); if (bytes_raw == NULL) return FALSE; /* read signature */ bytes_sig = fu_input_stream_read_bytes(stream_sig, 0, FU_ENGINE_MAX_SIGNATURE_SIZE, NULL, error); if (bytes_sig == NULL) return FALSE; /* update with blobs */ return fu_engine_update_metadata_bytes(self, remote_id, bytes_raw, bytes_sig, error); #else g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Not supported as is unavailable"); return FALSE; #endif } /** * fu_engine_build_cabinet_from_stream: * @self: a #FuEngine * @stream: a #GInputStream * @error: (nullable): optional return location for an error * * Creates a silo from a .cab file blob. * * Returns: (transfer container): a #XbSilo, or %NULL **/ FuCabinet * fu_engine_build_cabinet_from_stream(FuEngine *self, GInputStream *stream, GError **error) { g_autoptr(FuCabinet) cabinet = fu_cabinet_new(); g_return_val_if_fail(FU_IS_ENGINE(self), NULL); g_return_val_if_fail(G_IS_INPUT_STREAM(stream), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* load file */ fu_engine_set_status(self, FWUPD_STATUS_DECOMPRESSING); fu_firmware_set_size_max(FU_FIRMWARE(cabinet), fu_engine_config_get_archive_size_max(self->config)); fu_cabinet_set_jcat_context(cabinet, self->jcat_context); if (!fu_firmware_parse_stream(FU_FIRMWARE(cabinet), stream, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return NULL; return g_steal_pointer(&cabinet); } static FuDevice * fu_engine_get_result_from_component(FuEngine *self, FuEngineRequest *request, FuCabinet *cabinet, XbNode *component, GError **error) { g_autoptr(FuDevice) dev = NULL; g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error_local = NULL; g_autoptr(GError) error_reqs = NULL; g_autoptr(GPtrArray) provides = NULL; g_autoptr(GPtrArray) tags = NULL; g_autoptr(XbNode) rel = NULL; g_autoptr(XbQuery) query = NULL; dev = fu_device_new(self->ctx); provides = xb_node_query(component, "provides/firmware[@type=$'flashed']", 0, &error_local); if (provides == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to get release: %s", error_local->message); return NULL; } for (guint i = 0; i < provides->len; i++) { XbNode *prov = XB_NODE(g_ptr_array_index(provides, i)); const gchar *guid; g_autoptr(FuDevice) device = NULL; /* is a online or offline update appropriate */ guid = xb_node_get_text(prov); if (guid == NULL) continue; device = fu_device_list_get_by_guid(self->device_list, guid, NULL); if (device != NULL) { fu_device_incorporate(dev, device, FU_DEVICE_INCORPORATE_FLAG_ALL); } else { fu_device_inhibit(dev, "not-found", "Device was not found"); } /* add GUID */ fu_device_add_instance_id(dev, guid); } fu_device_convert_instance_ids(dev); if (fu_device_get_guids(dev)->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "component has no GUIDs"); return NULL; } /* add tags */ tags = xb_node_query(component, "tags/tag[@namespace=$'lvfs']", 0, NULL); if (tags != NULL) { for (guint i = 0; i < tags->len; i++) { XbNode *tag = g_ptr_array_index(tags, i); fu_release_add_tag(release, xb_node_get_text(tag)); } } /* add EOL flag */ if (xb_node_get_attr(component, "date_eol") != NULL) fu_device_add_flag(dev, FWUPD_DEVICE_FLAG_END_OF_LIFE); /* check we can install it */ fu_release_set_device(release, dev); fu_release_set_request(release, request); query = xb_query_new_full(xb_node_get_silo(component), "releases/release", XB_QUERY_FLAG_FORCE_NODE_CACHE, error); if (query == NULL) return NULL; rel = xb_node_query_first_full(component, query, &error_local); if (rel == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to get release: %s", error_local->message); return NULL; } if (!fu_engine_load_release( self, release, cabinet, component, rel, FU_FIRMWARE_PARSE_FLAG_IGNORE_VID_PID | FWUPD_INSTALL_FLAG_ALLOW_REINSTALL | FWUPD_INSTALL_FLAG_ALLOW_BRANCH_SWITCH | FWUPD_INSTALL_FLAG_ALLOW_OLDER, &error_reqs)) { if (!fu_device_has_inhibit(dev, "not-found")) fu_device_inhibit(dev, "failed-reqs", error_reqs->message); /* continue */ } /* success */ fu_device_add_release(dev, FWUPD_RELEASE(release)); return g_steal_pointer(&dev); } static gint fu_engine_get_details_sort_cb(gconstpointer a, gconstpointer b) { FuDevice *device1 = *((FuDevice **)a); FuDevice *device2 = *((FuDevice **)b); if (!fu_device_has_flag(device1, FWUPD_DEVICE_FLAG_UPDATABLE) && fu_device_has_flag(device2, FWUPD_DEVICE_FLAG_UPDATABLE)) return 1; if (fu_device_has_flag(device1, FWUPD_DEVICE_FLAG_UPDATABLE) && !fu_device_has_flag(device2, FWUPD_DEVICE_FLAG_UPDATABLE)) return -1; return 0; } /** * fu_engine_get_details: * @self: a #FuEngine * @request: a #FuEngineRequest * @stream: a seekable #GInputStream * @error: (nullable): optional return location for an error * * Gets the details about a local file. * * Note: this will close the fd when done * * Returns: (transfer container) (element-type FuDevice): results **/ GPtrArray * fu_engine_get_details(FuEngine *self, FuEngineRequest *request, GInputStream *stream, GError **error) { GChecksumType checksum_types[] = {G_CHECKSUM_SHA256, G_CHECKSUM_SHA1, 0}; g_autoptr(GPtrArray) components = NULL; g_autoptr(GPtrArray) details = NULL; g_autoptr(GPtrArray) checksums = g_ptr_array_new_with_free_func(g_free); g_autoptr(FuCabinet) cabinet = NULL; g_autoptr(XbNode) rel_by_csum = NULL; g_return_val_if_fail(FU_IS_ENGINE(self), NULL); g_return_val_if_fail(G_IS_INPUT_STREAM(stream), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); cabinet = fu_engine_build_cabinet_from_stream(self, stream, error); if (cabinet == NULL) { g_prefix_error(error, "failed to load file: "); return NULL; } components = fu_cabinet_get_components(cabinet, error); if (components == NULL) return NULL; /* calculate the checksums of the blob */ for (guint i = 0; checksum_types[i] != 0; i++) { g_autofree gchar *checksum = fu_input_stream_compute_checksum(stream, checksum_types[i], error); if (checksum == NULL) return NULL; g_ptr_array_add(checksums, g_steal_pointer(&checksum)); } /* does this exist in any enabled remote */ for (guint i = 0; i < checksums->len; i++) { const gchar *csum = g_ptr_array_index(checksums, i); rel_by_csum = fu_engine_get_release_for_checksum(self, csum); if (rel_by_csum != NULL) break; } /* create results with all the metadata in */ details = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); for (guint i = 0; i < components->len; i++) { XbNode *component = g_ptr_array_index(components, i); FuDevice *dev; g_autoptr(FuRelease) rel = fu_release_new(); dev = fu_engine_get_result_from_component(self, request, cabinet, component, error); if (dev == NULL) return NULL; fu_device_add_release(dev, FWUPD_RELEASE(rel)); if (rel_by_csum != NULL) { const gchar *remote_id = xb_node_query_text(rel_by_csum, "../../../custom/value[@key='fwupd::RemoteId']", NULL); if (remote_id != NULL) fu_release_set_remote_id(rel, remote_id); fu_device_add_flag(dev, FWUPD_DEVICE_FLAG_SUPPORTED); } /* add the checksum of the container blob */ for (guint j = 0; j < checksums->len; j++) { const gchar *csum = g_ptr_array_index(checksums, j); fu_release_add_checksum(rel, csum); } g_ptr_array_add(details, dev); } /* order multiple devices so that the one that passes the requirement * is listed first */ g_ptr_array_sort(details, fu_engine_get_details_sort_cb); return g_steal_pointer(&details); } static gint fu_engine_sort_devices_by_priority_name(gconstpointer a, gconstpointer b) { FuDevice *dev_a = *((FuDevice **)a); FuDevice *dev_b = *((FuDevice **)b); gint prio_a = fu_device_get_priority(dev_a); gint prio_b = fu_device_get_priority(dev_b); const gchar *name_a = fu_device_get_name(dev_a); const gchar *name_b = fu_device_get_name(dev_b); if (prio_a > prio_b) return -1; if (prio_a < prio_b) return 1; if (g_strcmp0(name_a, name_b) > 0) return 1; if (g_strcmp0(name_a, name_b) < 0) return -1; return 0; } /** * fu_engine_get_devices: * @self: a #FuEngine * @error: (nullable): optional return location for an error * * Gets the list of devices. * * Returns: (transfer container) (element-type FwupdDevice): results **/ GPtrArray * fu_engine_get_devices(FuEngine *self, GError **error) { g_autoptr(GPtrArray) devices = NULL; g_return_val_if_fail(FU_IS_ENGINE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); devices = fu_device_list_get_active(self->device_list); if (devices->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "No detected devices"); return NULL; } g_ptr_array_sort(devices, fu_engine_sort_devices_by_priority_name); return g_steal_pointer(&devices); } /** * fu_engine_get_devices_by_guid: * @self: a #FuEngine * @guid: a GUID * @error: (nullable): optional return location for an error * * Gets a specific device. * * Returns: (transfer full): a device, or %NULL if not found **/ GPtrArray * fu_engine_get_devices_by_guid(FuEngine *self, const gchar *guid, GError **error) { g_autoptr(GPtrArray) devices = NULL; g_autoptr(GPtrArray) devices_tmp = NULL; /* find the devices by GUID */ devices_tmp = fu_device_list_get_active(self->device_list); devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); for (guint i = 0; i < devices_tmp->len; i++) { FuDevice *dev_tmp = g_ptr_array_index(devices_tmp, i); if (fu_device_has_guid(dev_tmp, guid)) g_ptr_array_add(devices, g_object_ref(dev_tmp)); } /* nothing */ if (devices->len == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "failed to find any device providing %s", guid); return NULL; } /* success */ return g_steal_pointer(&devices); } /** * fu_engine_get_devices_by_composite_id: * @self: a #FuEngine * @composite_id: a device ID * @error: (nullable): optional return location for an error * * Gets all active devices that match a specific composite ID. * * Returns: (transfer full) (element-type FuDevice): devices **/ GPtrArray * fu_engine_get_devices_by_composite_id(FuEngine *self, const gchar *composite_id, GError **error) { g_autoptr(GPtrArray) devices = NULL; g_autoptr(GPtrArray) devices_tmp = NULL; /* find the devices by composite ID */ devices_tmp = fu_device_list_get_active(self->device_list); devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); for (guint i = 0; i < devices_tmp->len; i++) { FuDevice *dev_tmp = g_ptr_array_index(devices_tmp, i); if (g_strcmp0(fu_device_get_composite_id(dev_tmp), composite_id) == 0) g_ptr_array_add(devices, g_object_ref(dev_tmp)); } /* nothing */ if (devices->len == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "failed to find any device with composite ID %s", composite_id); return NULL; } /* success */ return g_steal_pointer(&devices); } #ifdef HAVE_HSI static void fu_engine_get_history_set_hsi_attrs(FuEngine *self, FuDevice *device) { g_autoptr(GPtrArray) vals = NULL; g_autofree gchar *host_security_id = fu_engine_get_host_security_id(self, NULL); /* ensure up to date */ fu_engine_ensure_security_attrs(self); /* add attributes */ vals = fu_security_attrs_get_all(self->host_security_attrs, NULL); for (guint i = 0; i < vals->len; i++) { FwupdSecurityAttr *attr = g_ptr_array_index(vals, i); const gchar *tmp; tmp = fwupd_security_attr_result_to_string(fwupd_security_attr_get_result(attr)); fu_device_set_metadata(device, fwupd_security_attr_get_appstream_id(attr), tmp); } /* computed value */ fu_device_set_metadata(device, "HSI", host_security_id); } #endif static void fu_engine_fixup_history_device(FuEngine *self, FuDevice *device) { FwupdRelease *release; GPtrArray *csums; /* get the checksums */ release = fu_device_get_release_default(device); if (release == NULL) { g_warning("no checksums from release history"); return; } /* find the checksum that matches */ csums = fwupd_release_get_checksums(release); for (guint j = 0; j < csums->len; j++) { const gchar *csum = g_ptr_array_index(csums, j); g_autoptr(XbNode) rel = fu_engine_get_release_for_checksum(self, csum); if (rel != NULL) { g_autoptr(GError) error_local = NULL; g_autoptr(XbNode) component = NULL; component = xb_node_query_first(rel, "../..", &error_local); if (component == NULL) { g_warning("failed to load component: %s", error_local->message); continue; } fu_release_set_device(FU_RELEASE(release), device); if (!fu_release_load(FU_RELEASE(release), NULL, component, rel, FWUPD_INSTALL_FLAG_NONE, &error_local)) { g_warning("failed to load release: %s", error_local->message); continue; } fu_device_add_flag(device, FWUPD_DEVICE_FLAG_SUPPORTED); break; } } } /** * fu_engine_get_history: * @self: a #FuEngine * @error: (nullable): optional return location for an error * * Gets the list of history. * * Returns: (transfer container) (element-type FwupdDevice): results **/ GPtrArray * fu_engine_get_history(FuEngine *self, GError **error) { g_autoptr(GPtrArray) devices_all = NULL; g_autoptr(GPtrArray) devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_return_val_if_fail(FU_IS_ENGINE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); devices_all = fu_history_get_devices(self->history, error); if (devices_all == NULL) return NULL; for (guint i = 0; i < devices_all->len; i++) { FuDevice *dev = g_ptr_array_index(devices_all, i); if (fu_device_has_flag(dev, FWUPD_DEVICE_FLAG_EMULATED)) continue; g_ptr_array_add(devices, g_object_ref(dev)); } if (devices->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "No history"); return NULL; } #ifdef HAVE_HSI /* if this is the system firmware device, add the HSI attrs */ for (guint i = 0; i < devices->len; i++) { FuDevice *dev = g_ptr_array_index(devices, i); if (fu_device_has_private_flag(dev, FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE)) fu_engine_get_history_set_hsi_attrs(self, dev); } #endif /* try to set the remote ID for each device */ for (guint i = 0; i < devices->len; i++) { FuDevice *dev = g_ptr_array_index(devices, i); fu_engine_fixup_history_device(self, dev); } return g_steal_pointer(&devices); } /** * fu_engine_get_remotes: * @self: a #FuEngine * @error: (nullable): optional return location for an error * * Gets the list of remotes in use by the engine. * * Returns: (transfer container) (element-type FwupdRemote): results **/ GPtrArray * fu_engine_get_remotes(FuEngine *self, GError **error) { GPtrArray *remotes; g_return_val_if_fail(FU_IS_ENGINE(self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); remotes = fu_remote_list_get_all(self->remote_list); if (remotes->len == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "No remotes configured"); return NULL; } /* deep copy so the remote list can be kept up to date */ return g_ptr_array_copy(remotes, (GCopyFunc)g_object_ref, NULL); } /** * fu_engine_get_remote_by_id: * @self: a #FuEngine * @remote_id: a string representation of a remote * @error: (nullable): optional return location for an error * * Gets the FwupdRemote object. * * Returns: FwupdRemote **/ FwupdRemote * fu_engine_get_remote_by_id(FuEngine *self, const gchar *remote_id, GError **error) { g_autoptr(GPtrArray) remotes = NULL; remotes = fu_engine_get_remotes(self, error); if (remotes == NULL) return NULL; for (guint i = 0; i < remotes->len; i++) { FwupdRemote *remote = g_ptr_array_index(remotes, i); if (g_strcmp0(remote_id, fwupd_remote_get_id(remote)) == 0) return remote; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Couldn't find remote %s", remote_id); return NULL; } static gint fu_engine_sort_releases_cb(gconstpointer a, gconstpointer b, gpointer user_data) { FuDevice *device = FU_DEVICE(user_data); FuRelease *rel_a = FU_RELEASE(*((FuRelease **)a)); FuRelease *rel_b = FU_RELEASE(*((FuRelease **)b)); gint rc; /* first by branch */ rc = g_strcmp0(fu_release_get_branch(rel_b), fu_release_get_branch(rel_a)); if (rc != 0) return rc; /* then by version */ rc = fu_version_compare(fu_release_get_version(rel_b), fu_release_get_version(rel_a), fu_device_get_version_format(device)); if (rc != 0) return rc; /* then by priority */ return fu_release_compare(rel_a, rel_b); } static gboolean fu_engine_check_release_is_approved(FuEngine *self, FwupdRelease *rel) { GPtrArray *csums = fwupd_release_get_checksums(rel); if (self->approved_firmware == NULL) return FALSE; for (guint i = 0; i < csums->len; i++) { const gchar *csum = g_ptr_array_index(csums, i); g_info("checking %s against approved list", csum); if (g_hash_table_lookup(self->approved_firmware, csum) != NULL) return TRUE; } return FALSE; } static gboolean fu_engine_check_release_is_blocked(FuEngine *self, FuRelease *release) { GPtrArray *csums = fu_release_get_checksums(release); if (self->blocked_firmware == NULL) return FALSE; for (guint i = 0; i < csums->len; i++) { const gchar *csum = g_ptr_array_index(csums, i); if (g_hash_table_lookup(self->blocked_firmware, csum) != NULL) return TRUE; } return FALSE; } static gboolean fu_engine_add_releases_for_device_component(FuEngine *self, FuEngineRequest *request, FuDevice *device, XbNode *component, GPtrArray *releases, GError **error) { FwupdFeatureFlags feature_flags; FwupdVersionFormat fmt = fu_device_get_version_format(device); g_autoptr(GError) error_local = NULL; g_autoptr(GPtrArray) releases_tmp = NULL; FwupdInstallFlags install_flags = FU_FIRMWARE_PARSE_FLAG_IGNORE_VID_PID | FWUPD_INSTALL_FLAG_ALLOW_BRANCH_SWITCH | FWUPD_INSTALL_FLAG_ALLOW_REINSTALL | FWUPD_INSTALL_FLAG_ALLOW_OLDER; /* get all releases */ releases_tmp = xb_node_query(component, "releases/release", 0, &error_local); if (releases_tmp == NULL) { if (g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) return TRUE; if (g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) return TRUE; g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } feature_flags = fu_engine_request_get_feature_flags(request); for (guint i = 0; i < releases_tmp->len; i++) { XbNode *rel = g_ptr_array_index(releases_tmp, i); const gchar *remote_id; const gchar *update_message; const gchar *update_image; const gchar *update_request_id; gint vercmp; GPtrArray *checksums; GPtrArray *locations; g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error_loop = NULL; /* create new FwupdRelease for the XbNode */ fu_release_set_request(release, request); fu_release_set_device(release, device); if (!fu_engine_load_release(self, release, NULL, /* cabinet */ component, rel, install_flags, &error_loop)) { g_debug("failed to set release for component: %s", error_loop->message); continue; } /* fall back to quirk-provided value */ if (fwupd_release_get_install_duration(FWUPD_RELEASE(release)) == 0) { fwupd_release_set_install_duration(FWUPD_RELEASE(release), fu_device_get_install_duration(device)); } /* invalid */ locations = fwupd_release_get_locations(FWUPD_RELEASE(release)); if (locations->len == 0) { g_autofree gchar *str = fwupd_codec_to_string(FWUPD_CODEC(release)); g_debug("no locations for %s", str); continue; } checksums = fu_release_get_checksums(release); if (checksums->len == 0) { g_autofree gchar *str = fwupd_codec_to_string(FWUPD_CODEC(release)); g_debug("no locations for %s", str); continue; } /* different branch */ if (g_strcmp0(fu_release_get_branch(release), fu_device_get_branch(device)) != 0) { if ((feature_flags & FWUPD_FEATURE_FLAG_SWITCH_BRANCH) == 0) { g_info("client does not understand branches, skipping %s:%s", fu_release_get_branch(release), fu_release_get_version(release)); continue; } fu_release_add_flag(release, FWUPD_RELEASE_FLAG_IS_ALTERNATE_BRANCH); } /* test for upgrade or downgrade */ vercmp = fu_version_compare(fu_release_get_version(release), fu_device_get_version(device), fmt); if (vercmp > 0) fu_release_add_flag(release, FWUPD_RELEASE_FLAG_IS_UPGRADE); else if (vercmp < 0) fu_release_add_flag(release, FWUPD_RELEASE_FLAG_IS_DOWNGRADE); /* lower than allowed to downgrade to */ if (fu_device_get_version_lowest(device) != NULL && fu_version_compare(fu_release_get_version(release), fu_device_get_version_lowest(device), fmt) < 0) { fu_release_add_flag(release, FWUPD_RELEASE_FLAG_BLOCKED_VERSION); } /* manually blocked */ if (fu_engine_check_release_is_blocked(self, release)) fu_release_add_flag(release, FWUPD_RELEASE_FLAG_BLOCKED_APPROVAL); /* check if remote is filtering firmware */ remote_id = fwupd_release_get_remote_id(FWUPD_RELEASE(release)); if (remote_id != NULL) { FwupdRemote *remote = fu_engine_get_remote_by_id(self, remote_id, NULL); if (remote != NULL && fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_APPROVAL_REQUIRED) && !fu_engine_check_release_is_approved(self, FWUPD_RELEASE(release))) { fu_release_add_flag(release, FWUPD_RELEASE_FLAG_BLOCKED_APPROVAL); } } /* add update message if exists but device doesn't already have one */ update_message = fwupd_release_get_update_message(FWUPD_RELEASE(release)); if (fu_device_get_update_message(device) == NULL && update_message != NULL) { fu_device_set_update_message(device, update_message); } update_image = fwupd_release_get_update_image(FWUPD_RELEASE(release)); if (fu_device_get_update_image(device) == NULL && update_image != NULL) { fu_device_set_update_image(device, update_image); } update_request_id = fu_release_get_update_request_id(release); if (fu_device_get_update_request_id(device) == NULL && update_request_id != NULL) { fu_device_add_request_flag(device, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); fu_device_set_update_request_id(device, update_request_id); } /* success */ g_ptr_array_add(releases, g_steal_pointer(&release)); /* if we're only checking for SUPPORTED then *any* release is good enough */ if (fu_engine_request_has_flag(request, FU_ENGINE_REQUEST_FLAG_ANY_RELEASE) && releases->len > 0) break; } /* success */ return TRUE; } static const gchar * fu_engine_get_branch_fallback(const gchar *nullable_branch) { if (nullable_branch == NULL) return "default"; return nullable_branch; } GPtrArray * fu_engine_get_releases_for_device(FuEngine *self, FuEngineRequest *request, FuDevice *device, GError **error) { GPtrArray *device_guids; g_autoptr(GPtrArray) branches = NULL; g_autoptr(GPtrArray) releases = NULL; /* no components in silo */ if (self->query_component_by_guid == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no components in silo"); return NULL; } /* get device version */ if (!fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_MD_SET_VERSION) && !fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_MD_SET_FLAGS)) { const gchar *version = fu_device_get_version(device); if (version == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no version set"); return NULL; } } /* only show devices that can be updated */ if (!fu_engine_request_has_feature_flag(request, FWUPD_FEATURE_FLAG_SHOW_PROBLEMS) && !fu_device_is_updatable(device)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "is not updatable"); return NULL; } /* only show devices that can be updated */ if (!fu_engine_request_has_feature_flag(request, FWUPD_FEATURE_FLAG_REQUESTS_NON_GENERIC) && fu_device_has_request_flag(device, FWUPD_REQUEST_FLAG_NON_GENERIC_MESSAGE)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "is not updatable as requires a non-generic request"); return NULL; } /* get all the components that provide any of these GUIDs */ device_guids = fu_device_get_guids(device); releases = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); for (guint j = 0; j < device_guids->len; j++) { const gchar *guid = g_ptr_array_index(device_guids, j); g_autoptr(GError) error_local = NULL; g_autoptr(GPtrArray) components = NULL; g_auto(XbQueryContext) context = XB_QUERY_CONTEXT_INIT(); xb_query_context_set_flags(&context, XB_QUERY_FLAG_USE_INDEXES); xb_value_bindings_bind_str(xb_query_context_get_bindings(&context), 0, guid, NULL); components = xb_silo_query_with_context(self->silo, self->query_component_by_guid, &context, &error_local); if (components == NULL) { g_debug("%s was not found: %s", guid, error_local->message); continue; } /* find all the releases that pass all the requirements */ g_debug("%s matched %u components", guid, components->len); for (guint i = 0; i < components->len; i++) { XbNode *component = XB_NODE(g_ptr_array_index(components, i)); g_autoptr(GError) error_tmp = NULL; if (!fu_engine_add_releases_for_device_component(self, request, device, component, releases, &error_tmp)) { g_debug("%s", error_tmp->message); continue; } } g_debug("%s matched %u releases", guid, releases->len); /* if we're only checking for SUPPORTED then *any* release is good enough */ if (fu_engine_request_has_flag(request, FU_ENGINE_REQUEST_FLAG_ANY_RELEASE) && releases->len > 0) break; } /* are there multiple branches available */ branches = g_ptr_array_new_with_free_func(g_free); g_ptr_array_add(branches, g_strdup(fu_engine_get_branch_fallback(fu_device_get_branch(device)))); for (guint i = 0; i < releases->len; i++) { FwupdRelease *rel_tmp = FWUPD_RELEASE(g_ptr_array_index(releases, i)); const gchar *branch_tmp = fu_engine_get_branch_fallback(fwupd_release_get_branch(rel_tmp)); if (g_ptr_array_find_with_equal_func(branches, branch_tmp, g_str_equal, NULL)) continue; g_ptr_array_add(branches, g_strdup(branch_tmp)); } if (branches->len > 1) fu_device_add_flag(device, FWUPD_DEVICE_FLAG_HAS_MULTIPLE_BRANCHES); /* return the compound error */ if (releases->len == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "No releases found"); return NULL; } return g_steal_pointer(&releases); } /** * fu_engine_get_releases: * @self: a #FuEngine * @request: a #FuEngineRequest * @device_id: a device ID * @error: (nullable): optional return location for an error * * Gets the releases available for a specific device. * * Returns: (transfer container) (element-type FwupdDevice): results **/ GPtrArray * fu_engine_get_releases(FuEngine *self, FuEngineRequest *request, const gchar *device_id, GError **error) { g_autoptr(FuDevice) device = NULL; g_autoptr(GPtrArray) releases = NULL; g_autoptr(GPtrArray) releases_deduped = NULL; g_return_val_if_fail(FU_IS_ENGINE(self), NULL); g_return_val_if_fail(FU_IS_ENGINE_REQUEST(request), NULL); g_return_val_if_fail(device_id != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* find the device */ device = fu_device_list_get_by_id(self->device_list, device_id, error); if (device == NULL) return NULL; /* get all the releases for the device */ releases = fu_engine_get_releases_for_device(self, request, device, error); if (releases == NULL) return NULL; if (releases->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "No releases for device"); return NULL; } g_ptr_array_sort_with_data(releases, fu_engine_sort_releases_cb, device); /* dedupe by container checksum */ if (fu_engine_config_get_release_dedupe(self->config)) { g_autoptr(GHashTable) checksums = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); releases_deduped = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); for (guint i = 0; i < releases->len; i++) { FuRelease *rel = g_ptr_array_index(releases, i); GPtrArray *csums = fu_release_get_checksums(rel); gboolean found = FALSE; /* find existing */ for (guint j = 0; j < csums->len; j++) { const gchar *csum = g_ptr_array_index(csums, j); g_autofree gchar *key = g_strdup_printf("%s:%s", csum, fu_release_get_version(rel)); if (g_hash_table_contains(checksums, key)) { found = TRUE; break; } g_hash_table_add(checksums, g_steal_pointer(&key)); } if (found) { g_debug("found higher priority release for %s, skipping", fu_release_get_version(rel)); continue; } g_ptr_array_add(releases_deduped, g_object_ref(rel)); } } else { releases_deduped = g_ptr_array_ref(releases); } /* success */ return g_steal_pointer(&releases_deduped); } /** * fu_engine_get_downgrades: * @self: a #FuEngine * @request: a #FuEngineRequest * @device_id: a device ID * @error: (nullable): optional return location for an error * * Gets the downgrades available for a specific device. * * Returns: (transfer container) (element-type FwupdDevice): results **/ GPtrArray * fu_engine_get_downgrades(FuEngine *self, FuEngineRequest *request, const gchar *device_id, GError **error) { g_autoptr(FuDevice) device = NULL; g_autoptr(GPtrArray) releases = NULL; g_autoptr(GPtrArray) releases_tmp = NULL; g_autoptr(GString) error_str = g_string_new(NULL); g_return_val_if_fail(FU_IS_ENGINE(self), NULL); g_return_val_if_fail(device_id != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* find the device */ device = fu_device_list_get_by_id(self->device_list, device_id, error); if (device == NULL) return NULL; /* get all the releases for the device */ releases_tmp = fu_engine_get_releases_for_device(self, request, device, error); if (releases_tmp == NULL) return NULL; releases = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); for (guint i = 0; i < releases_tmp->len; i++) { FwupdRelease *rel_tmp = g_ptr_array_index(releases_tmp, i); /* same as installed */ if (!fwupd_release_has_flag(rel_tmp, FWUPD_RELEASE_FLAG_IS_UPGRADE) && !fwupd_release_has_flag(rel_tmp, FWUPD_RELEASE_FLAG_IS_DOWNGRADE)) { g_string_append_printf(error_str, "%s=same, ", fwupd_release_get_version(rel_tmp)); g_debug("ignoring %s as the same as %s", fwupd_release_get_version(rel_tmp), fu_device_get_version(device)); continue; } /* newer than current */ if (fwupd_release_has_flag(rel_tmp, FWUPD_RELEASE_FLAG_IS_UPGRADE)) { g_string_append_printf(error_str, "%s=newer, ", fwupd_release_get_version(rel_tmp)); g_debug("ignoring %s as newer than %s", fwupd_release_get_version(rel_tmp), fu_device_get_version(device)); continue; } /* don't show releases we are not allowed to downgrade to */ if (fwupd_release_has_flag(rel_tmp, FWUPD_RELEASE_FLAG_BLOCKED_VERSION)) { g_string_append_printf(error_str, "%s=lowest, ", fwupd_release_get_version(rel_tmp)); g_debug("ignoring %s as older than lowest %s", fwupd_release_get_version(rel_tmp), fu_device_get_version_lowest(device)); continue; } /* different branch */ if (fwupd_release_has_flag(rel_tmp, FWUPD_RELEASE_FLAG_IS_ALTERNATE_BRANCH)) { g_info("ignoring release %s as branch %s, and device is %s", fwupd_release_get_version(rel_tmp), fwupd_release_get_branch(rel_tmp), fu_device_get_branch(device)); continue; } g_ptr_array_add(releases, g_object_ref(rel_tmp)); } if (error_str->len > 2) g_string_truncate(error_str, error_str->len - 2); if (releases->len == 0) { if (error_str->len > 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "current version is %s: %s", fu_device_get_version(device), error_str->str); } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "current version is %s", fu_device_get_version(device)); } return NULL; } g_ptr_array_sort_with_data(releases, fu_engine_sort_releases_cb, device); return g_steal_pointer(&releases); } GPtrArray * fu_engine_get_approved_firmware(FuEngine *self) { GPtrArray *checksums = g_ptr_array_new_with_free_func(g_free); if (self->approved_firmware != NULL) { g_autoptr(GList) keys = g_hash_table_get_keys(self->approved_firmware); for (GList *l = keys; l != NULL; l = l->next) { const gchar *csum = l->data; g_ptr_array_add(checksums, g_strdup(csum)); } } return checksums; } void fu_engine_add_approved_firmware(FuEngine *self, const gchar *checksum) { if (self->approved_firmware == NULL) { self->approved_firmware = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); } g_hash_table_add(self->approved_firmware, g_strdup(checksum)); } GPtrArray * fu_engine_get_blocked_firmware(FuEngine *self) { GPtrArray *checksums = g_ptr_array_new_with_free_func(g_free); if (self->blocked_firmware != NULL) { g_autoptr(GList) keys = g_hash_table_get_keys(self->blocked_firmware); for (GList *l = keys; l != NULL; l = l->next) { const gchar *csum = l->data; g_ptr_array_add(checksums, g_strdup(csum)); } } return checksums; } static void fu_engine_add_blocked_firmware(FuEngine *self, const gchar *checksum) { if (self->blocked_firmware == NULL) { self->blocked_firmware = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); } g_hash_table_add(self->blocked_firmware, g_strdup(checksum)); } gboolean fu_engine_set_blocked_firmware(FuEngine *self, GPtrArray *checksums, GError **error) { /* update in-memory hash */ if (self->blocked_firmware != NULL) { g_hash_table_unref(self->blocked_firmware); self->blocked_firmware = NULL; } for (guint i = 0; i < checksums->len; i++) { const gchar *csum = g_ptr_array_index(checksums, i); fu_engine_add_blocked_firmware(self, csum); } /* save database */ if (!fu_history_clear_blocked_firmware(self->history, error)) return FALSE; for (guint i = 0; i < checksums->len; i++) { const gchar *csum = g_ptr_array_index(checksums, i); if (!fu_history_add_blocked_firmware(self->history, csum, error)) return FALSE; } return TRUE; } gchar * fu_engine_self_sign(FuEngine *self, const gchar *value, JcatSignFlags flags, GError **error) { g_autoptr(JcatBlob) jcat_signature = NULL; g_autoptr(JcatEngine) jcat_engine = NULL; g_autoptr(JcatResult) jcat_result = NULL; g_autoptr(GBytes) payload = NULL; /* create detached signature and verify */ jcat_engine = jcat_context_get_engine(self->jcat_context, JCAT_BLOB_KIND_PKCS7, error); if (jcat_engine == NULL) return NULL; payload = g_bytes_new(value, strlen(value)); jcat_signature = jcat_engine_self_sign(jcat_engine, payload, flags, error); if (jcat_signature == NULL) return NULL; jcat_result = jcat_engine_self_verify(jcat_engine, payload, jcat_blob_get_data(jcat_signature), JCAT_VERIFY_FLAG_NONE, error); if (jcat_result == NULL) return NULL; return jcat_blob_get_data_as_string(jcat_signature); } /** * fu_engine_get_upgrades: * @self: a #FuEngine * @request: a #FuEngineRequest * @device_id: a device ID * @error: (nullable): optional return location for an error * * Gets the upgrades available for a specific device. * * Returns: (transfer container) (element-type FwupdDevice): results **/ GPtrArray * fu_engine_get_upgrades(FuEngine *self, FuEngineRequest *request, const gchar *device_id, GError **error) { g_autoptr(FuDevice) device = NULL; g_autoptr(GPtrArray) releases = NULL; g_autoptr(GPtrArray) releases_tmp = NULL; g_autoptr(GString) error_str = g_string_new(NULL); g_return_val_if_fail(FU_IS_ENGINE(self), NULL); g_return_val_if_fail(device_id != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* find the device */ device = fu_device_list_get_by_id(self->device_list, device_id, error); if (device == NULL) return NULL; /* there is no point checking each release */ if (!fu_device_is_updatable(device)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "Device is not updatable"); return NULL; } /* stay on one firmware version unless the new version is explicitly specified */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_ONLY_EXPLICIT_UPDATES)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "Installing a specific release is explicitly required"); return NULL; } /* don't show upgrades again until we reboot */ if (fu_device_get_update_state(device) == FWUPD_UPDATE_STATE_NEEDS_REBOOT) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "A reboot is pending"); return NULL; } /* get all the releases for the device */ releases_tmp = fu_engine_get_releases_for_device(self, request, device, error); if (releases_tmp == NULL) return NULL; releases = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); for (guint i = 0; i < releases_tmp->len; i++) { FwupdRelease *rel_tmp = g_ptr_array_index(releases_tmp, i); /* same as installed */ if (!fwupd_release_has_flag(rel_tmp, FWUPD_RELEASE_FLAG_IS_UPGRADE) && !fwupd_release_has_flag(rel_tmp, FWUPD_RELEASE_FLAG_IS_DOWNGRADE)) { g_string_append_printf(error_str, "%s=same, ", fwupd_release_get_version(rel_tmp)); g_debug("ignoring %s == %s", fwupd_release_get_version(rel_tmp), fu_device_get_version(device)); continue; } /* older than current */ if (fwupd_release_has_flag(rel_tmp, FWUPD_RELEASE_FLAG_IS_DOWNGRADE)) { g_string_append_printf(error_str, "%s=older, ", fwupd_release_get_version(rel_tmp)); g_debug("ignoring %s < %s", fwupd_release_get_version(rel_tmp), fu_device_get_version(device)); continue; } /* not approved */ if (fwupd_release_has_flag(rel_tmp, FWUPD_RELEASE_FLAG_BLOCKED_APPROVAL)) { g_string_append_printf(error_str, "%s=not-approved, ", fwupd_release_get_version(rel_tmp)); g_debug("ignoring %s as not approved as required by %s", fwupd_release_get_version(rel_tmp), fwupd_release_get_remote_id(rel_tmp)); continue; } /* different branch */ if (fwupd_release_has_flag(rel_tmp, FWUPD_RELEASE_FLAG_IS_ALTERNATE_BRANCH)) { g_info("ignoring release %s as branch %s, and device is %s", fwupd_release_get_version(rel_tmp), fwupd_release_get_branch(rel_tmp), fu_device_get_branch(device)); continue; } g_ptr_array_add(releases, g_object_ref(rel_tmp)); } if (error_str->len > 2) g_string_truncate(error_str, error_str->len - 2); if (releases->len == 0) { if (error_str->len > 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "current version is %s: %s", fu_device_get_version(device), error_str->str); } else { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "current version is %s", fu_device_get_version(device)); } return NULL; } g_ptr_array_sort_with_data(releases, fu_engine_sort_releases_cb, device); return g_steal_pointer(&releases); } /** * fu_engine_clear_results: * @self: a #FuEngine * @device_id: a device ID * @error: (nullable): optional return location for an error * * Clear the historical state of a specific device operation. * * Returns: %TRUE for success **/ gboolean fu_engine_clear_results(FuEngine *self, const gchar *device_id, GError **error) { g_autoptr(FuDevice) device = NULL; FuPlugin *plugin; g_return_val_if_fail(FU_IS_ENGINE(self), FALSE); g_return_val_if_fail(device_id != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* find the device */ device = fu_engine_get_item_by_id_fallback_history(self, device_id, error); if (device == NULL) return FALSE; /* already set on the database */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_NOTIFIED)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "device already has notified flag"); return FALSE; } /* call into the plugin if it still exists */ plugin = fu_plugin_list_find_by_name(self->plugin_list, fu_device_get_plugin(device), error); if (plugin != NULL) { if (!fu_plugin_runner_clear_results(plugin, device, error)) return FALSE; } /* if the update never got run, unstage it */ if (fu_device_get_update_state(device) == FWUPD_UPDATE_STATE_PENDING) fu_device_set_update_state(device, FWUPD_UPDATE_STATE_UNKNOWN); /* override */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NOTIFIED); return fu_history_modify_device(self->history, device, error); } /** * fu_engine_get_results: * @self: a #FuEngine * @device_id: a device ID * @error: (nullable): optional return location for an error * * Gets the historical state of a specific device operation. * * Returns: (transfer container): a device, or %NULL **/ FwupdDevice * fu_engine_get_results(FuEngine *self, const gchar *device_id, GError **error) { FwupdRelease *rel; g_autoptr(FuDevice) device = NULL; g_return_val_if_fail(FU_IS_ENGINE(self), NULL); g_return_val_if_fail(device_id != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* find the device */ device = fu_engine_get_item_by_id_fallback_history(self, device_id, error); if (device == NULL) return NULL; /* the notification has already been shown to the user */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_NOTIFIED)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "User has already been notified about %s [%s]", fu_device_get_name(device), fu_device_get_id(device)); return NULL; } /* try to set some release properties for the UI */ fu_engine_fixup_history_device(self, device); /* we did not either record or find the AppStream ID */ rel = fu_device_get_release_default(device); if (rel == NULL || fwupd_release_get_appstream_id(rel) == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "device %s appstream id was not found", fu_device_get_id(device)); return NULL; } /* success */ return g_object_ref(FWUPD_DEVICE(device)); } static void fu_engine_plugins_startup(FuEngine *self, FuProgress *progress) { GPtrArray *plugins = fu_plugin_list_get_all(self->plugin_list); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, plugins->len); for (guint i = 0; i < plugins->len; i++) { g_autoptr(GError) error = NULL; FuPlugin *plugin = g_ptr_array_index(plugins, i); if (!fu_plugin_runner_startup(plugin, fu_progress_get_child(progress), &error)) { fu_plugin_add_flag(plugin, FWUPD_PLUGIN_FLAG_DISABLED); if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { fu_plugin_add_flag(plugin, FWUPD_PLUGIN_FLAG_NO_HARDWARE); } g_info("disabling plugin because: %s", error->message); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_CHILD_FINISHED); } fu_progress_step_done(progress); } } static void fu_engine_plugins_ready(FuEngine *self, FuProgress *progress) { GPtrArray *plugins = fu_plugin_list_get_all(self->plugin_list); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, plugins->len); for (guint i = 0; i < plugins->len; i++) { g_autoptr(GError) error = NULL; FuPlugin *plugin = g_ptr_array_index(plugins, i); if (!fu_plugin_runner_ready(plugin, fu_progress_get_child(progress), &error)) { if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { fu_plugin_add_flag(plugin, FWUPD_PLUGIN_FLAG_NO_HARDWARE); } g_info("disabling plugin because: %s", error->message); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_CHILD_FINISHED); } fu_progress_step_done(progress); } } static void fu_engine_plugins_coldplug(FuEngine *self, FuProgress *progress) { GPtrArray *plugins; g_autoptr(GString) str = g_string_new(NULL); /* exec */ plugins = fu_plugin_list_get_all(self->plugin_list); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, plugins->len); for (guint i = 0; i < plugins->len; i++) { g_autoptr(GError) error = NULL; FuPlugin *plugin = g_ptr_array_index(plugins, i); if (!fu_plugin_runner_coldplug(plugin, fu_progress_get_child(progress), &error)) { fu_plugin_add_flag(plugin, FWUPD_PLUGIN_FLAG_DISABLED); g_info("disabling plugin because: %s", error->message); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_CHILD_FINISHED); } fu_progress_step_done(progress); } /* print what we do have */ for (guint i = 0; i < plugins->len; i++) { FuPlugin *plugin = g_ptr_array_index(plugins, i); if (fu_plugin_has_flag(plugin, FWUPD_PLUGIN_FLAG_DISABLED)) continue; g_string_append_printf(str, "%s, ", fu_plugin_get_name(plugin)); } if (str->len > 2) { g_string_truncate(str, str->len - 2); g_info("using plugins: %s", str->str); } } static void fu_engine_plugin_device_register(FuEngine *self, FuDevice *device) { GPtrArray *backends = fu_context_get_backends(self->ctx); GPtrArray *plugins; if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_REGISTERED)) { g_warning("already registered %s, ignoring", fu_device_get_id(device)); return; } plugins = fu_plugin_list_get_all(self->plugin_list); for (guint i = 0; i < plugins->len; i++) { FuPlugin *plugin = g_ptr_array_index(plugins, i); fu_plugin_runner_device_register(plugin, device); } for (guint i = 0; i < backends->len; i++) { FuBackend *backend = g_ptr_array_index(backends, i); fu_backend_registered(backend, device); } fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_REGISTERED); } static void fu_engine_plugin_device_register_cb(FuPlugin *plugin, FuDevice *device, gpointer user_data) { FuEngine *self = FU_ENGINE(user_data); fu_engine_plugin_device_register(self, device); } static void fu_engine_plugin_device_added_cb(FuPlugin *plugin, FuDevice *device, gpointer user_data) { FuEngine *self = FU_ENGINE(user_data); /* plugin has prio and device not already set from quirk */ if (fu_plugin_get_priority(plugin) > 0 && fu_device_get_priority(device) == 0) { g_info("auto-setting %s priority to %u", fu_device_get_id(device), fu_plugin_get_priority(plugin)); fu_device_set_priority(device, fu_plugin_get_priority(plugin)); } fu_engine_add_device(self, device); } static void fu_engine_adopt_children_device(FuEngine *self, FuDevice *device, FuDevice *device_tmp) { if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE_CHILD) && fu_device_has_private_flag(device_tmp, FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE)) { fu_device_set_parent(device, device_tmp); fu_engine_ensure_device_supported(self, device_tmp); return; } if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE) && fu_device_has_private_flag(device_tmp, FU_DEVICE_PRIVATE_FLAG_HOST_FIRMWARE_CHILD)) { fu_device_set_parent(device_tmp, device); fu_engine_ensure_device_supported(self, device_tmp); return; } if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_HOST_CPU_CHILD) && fu_device_has_private_flag(device_tmp, FU_DEVICE_PRIVATE_FLAG_HOST_CPU)) { fu_device_set_parent(device, device_tmp); fu_engine_ensure_device_supported(self, device_tmp); return; } if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_HOST_CPU) && fu_device_has_private_flag(device_tmp, FU_DEVICE_PRIVATE_FLAG_HOST_CPU_CHILD)) { fu_device_set_parent(device_tmp, device); fu_engine_ensure_device_supported(self, device_tmp); return; } } static void fu_engine_set_device_parent(FuEngine *self, FuDevice *device, FuDevice *parent) { fu_device_set_parent(device, parent); fu_engine_ensure_device_supported(self, device); fu_engine_ensure_device_supported(self, parent); } static void fu_engine_adopt_children(FuEngine *self, FuDevice *device) { GPtrArray *guids; g_autoptr(GPtrArray) devices = fu_device_list_get_active(self->device_list); /* find the parent in any existing device */ for (guint i = 0; fu_device_get_parent(device) == NULL && i < devices->len; i++) { FuDevice *device_tmp = g_ptr_array_index(devices, i); fu_engine_adopt_children_device(self, device, device_tmp); } if (fu_device_get_parent(device) == NULL) { for (guint i = 0; i < devices->len; i++) { FuDevice *device_tmp = g_ptr_array_index(devices, i); if (!fu_device_has_private_flag( device_tmp, FU_DEVICE_PRIVATE_FLAG_AUTO_PARENT_CHILDREN)) continue; if (fu_device_get_physical_id(device_tmp) == NULL) continue; if (fu_device_has_parent_physical_id( device, fu_device_get_physical_id(device_tmp))) { fu_engine_set_device_parent(self, device, device_tmp); break; } } } if (fu_device_get_parent(device) == NULL) { for (guint i = 0; i < devices->len; i++) { FuDevice *device_tmp = g_ptr_array_index(devices, i); if (!fu_device_has_private_flag( device_tmp, FU_DEVICE_PRIVATE_FLAG_AUTO_PARENT_CHILDREN)) continue; if (fu_device_get_backend_id(device_tmp) == NULL) continue; if (fu_device_has_parent_backend_id(device, fu_device_get_backend_id(device_tmp))) { fu_engine_set_device_parent(self, device, device_tmp); break; } } } if (fu_device_get_parent(device) == NULL) { guids = fu_device_get_parent_guids(device); for (guint j = 0; j < guids->len; j++) { const gchar *guid = g_ptr_array_index(guids, j); for (guint i = 0; i < devices->len; i++) { FuDevice *device_tmp = g_ptr_array_index(devices, i); if (fu_device_has_guid(device_tmp, guid)) { fu_engine_set_device_parent(self, device, device_tmp); break; } } } } /* the new device is the parent to an existing child */ for (guint j = 0; j < devices->len; j++) { GPtrArray *parent_physical_ids = NULL; FuDevice *device_tmp = g_ptr_array_index(devices, j); if (fu_device_get_parent(device_tmp) != NULL) continue; parent_physical_ids = fu_device_get_parent_physical_ids(device_tmp); if (parent_physical_ids == NULL) continue; for (guint i = 0; i < parent_physical_ids->len; i++) { const gchar *parent_physical_id = g_ptr_array_index(parent_physical_ids, i); if (g_strcmp0(parent_physical_id, fu_device_get_physical_id(device)) == 0) fu_engine_set_device_parent(self, device_tmp, device); } } for (guint j = 0; j < devices->len; j++) { GPtrArray *parent_backend_ids = NULL; FuDevice *device_tmp = g_ptr_array_index(devices, j); if (fu_device_get_parent(device_tmp) != NULL) continue; parent_backend_ids = fu_device_get_parent_backend_ids(device_tmp); if (parent_backend_ids == NULL) continue; for (guint i = 0; i < parent_backend_ids->len; i++) { const gchar *parent_backend_id = g_ptr_array_index(parent_backend_ids, i); if (g_strcmp0(parent_backend_id, fu_device_get_backend_id(device)) == 0) fu_engine_set_device_parent(self, device_tmp, device); } } guids = fu_device_get_guids(device); for (guint j = 0; j < guids->len; j++) { const gchar *guid = g_ptr_array_index(guids, j); for (guint i = 0; i < devices->len; i++) { FuDevice *device_tmp = g_ptr_array_index(devices, i); if (fu_device_get_parent(device_tmp) != NULL) continue; if (fu_device_has_parent_guid(device_tmp, guid)) fu_engine_set_device_parent(self, device_tmp, device); } } } static void fu_engine_set_proxy_device(FuEngine *self, FuDevice *device) { GPtrArray *guids; g_autoptr(FuDevice) proxy = NULL; g_autoptr(GPtrArray) devices = NULL; if (fu_device_get_proxy(device) != NULL) return; if (fu_device_get_proxy_guid(device) == NULL) return; /* find the proxy GUID in any existing device */ proxy = fu_device_list_get_by_guid(self->device_list, fu_device_get_proxy_guid(device), NULL); if (proxy != NULL) { g_info("setting proxy of %s to %s for %s", fu_device_get_id(proxy), fu_device_get_id(device), fu_device_get_proxy_guid(device)); fu_device_set_proxy(device, proxy); return; } /* are we the parent of an existing device */ guids = fu_device_get_guids(device); for (guint j = 0; j < guids->len; j++) { const gchar *guid = g_ptr_array_index(guids, j); devices = fu_device_list_get_active(self->device_list); for (guint i = 0; i < devices->len; i++) { FuDevice *device_tmp = g_ptr_array_index(devices, i); if (g_strcmp0(fu_device_get_proxy_guid(device_tmp), guid) == 0) { g_info("adding proxy of %s to %s for %s", fu_device_get_id(device), fu_device_get_id(device_tmp), guid); fu_device_set_proxy(device_tmp, device); return; } } } /* nothing found */ g_warning("did not find proxy device %s", fu_device_get_proxy_guid(device)); } static void fu_engine_device_inherit_history(FuEngine *self, FuDevice *device) { g_autoptr(FuDevice) device_history = NULL; /* ignore */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) return; /* any success or failed update? */ device_history = fu_history_get_device_by_id(self->history, fu_device_get_id(device), NULL); if (device_history == NULL) return; /* in an offline environment we may have used the .cab file to find the version-format * to use for the device -- so when we reboot use the database as the archive data is no * longer available */ if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_MD_SET_VERFMT) && fu_device_get_version_format(device_history) != FWUPD_VERSION_FORMAT_UNKNOWN) { g_debug( "absorbing version format %s into %s from history database", fwupd_version_format_to_string(fu_device_get_version_format(device_history)), fu_device_get_id(device)); fu_device_set_version_format(device, fu_device_get_version_format(device_history)); } /* the device is still running the old firmware version and so if it * required activation before, it still requires it now -- note: * we can't just check for version_new=version to allow for re-installs */ if (fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_INHERIT_ACTIVATION) && fu_device_has_flag(device_history, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION)) { FwupdRelease *release = fu_device_get_release_default(device_history); if (fu_version_compare(fu_device_get_version(device), fwupd_release_get_version(release), fu_device_get_version_format(device)) != 0) { g_info("inheriting needs-activation for %s as version %s != %s", fu_device_get_name(device), fu_device_get_version(device), fwupd_release_get_version(release)); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION); } } } static void fu_engine_ensure_device_emulation_tag(FuEngine *self, FuDevice *device) { /* already done */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATION_TAG)) return; /* we matched this physical ID */ if (fu_device_get_id(device) == NULL) return; if (!fu_history_has_emulation_tag(self->history, fu_device_get_id(device), NULL)) return; /* success */ g_info("adding emulation-tag to %s", fu_device_get_backend_id(device)); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_EMULATION_TAG); } void fu_engine_add_device(FuEngine *self, FuDevice *device) { GPtrArray *disabled_devices; GPtrArray *device_guids; g_autoptr(XbNode) component = NULL; /* make tests easier */ device_guids = fu_device_get_guids(device); if (device_guids->len == 0) fu_device_convert_instance_ids(device); /* device still has no GUIDs set! */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE) && device_guids->len == 0 && fu_device_get_children(device)->len == 0) { g_warning("no GUIDs for device %s [%s]", fu_device_get_name(device), fu_device_get_id(device)); return; } /* is this GUID disabled */ disabled_devices = fu_engine_config_get_disabled_devices(self->config); for (guint i = 0; i < disabled_devices->len; i++) { const gchar *disabled_guid = g_ptr_array_index(disabled_devices, i); for (guint j = 0; j < device_guids->len; j++) { const gchar *device_guid = g_ptr_array_index(device_guids, j); if (g_strcmp0(disabled_guid, device_guid) == 0) { g_info("%s [%s] is disabled [%s], ignoring from %s", fu_device_get_name(device), fu_device_get_id(device), device_guid, fu_device_get_plugin(device)); return; } } } /* does the device not have an assigned protocol */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE) && fu_device_get_protocols(device)->len == 0) { g_warning("device %s [%s] does not define an update protocol", fu_device_get_id(device), fu_device_get_name(device)); } /* if this device is locked get some metadata from AppStream */ component = fu_engine_get_component_by_guids(self, device); if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_LOCKED)) { if (component != NULL) { g_autoptr(XbNode) rel = NULL; rel = xb_node_query_first(component, "releases/release", NULL); if (rel != NULL) { g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error_local = NULL; g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); fu_engine_request_add_flag(request, FU_ENGINE_REQUEST_FLAG_NO_REQUIREMENTS); fu_release_set_request(release, request); fu_release_set_device(release, device); if (!fu_engine_load_release(self, release, NULL, /* cabinet */ component, rel, FWUPD_INSTALL_FLAG_NONE, &error_local)) { g_warning("failed to set AppStream release: %s", error_local->message); } else { fu_device_add_release(device, FWUPD_RELEASE(release)); } } } } /* check if the device needs emulation-tag */ fu_engine_ensure_device_emulation_tag(self, device); /* set or clear the SUPPORTED flag */ fu_engine_ensure_device_supported(self, device); /* adopt any required children, which may or may not already exist */ fu_engine_adopt_children(self, device); /* set the proxy device if specified by GUID */ fu_engine_set_proxy_device(self, device); /* sometimes inherit flags from recent history */ fu_engine_device_inherit_history(self, device); /* notify all plugins about this new device */ if (!fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_REGISTERED)) fu_engine_plugin_device_register(self, device); if (fu_device_get_version_format(device) == FWUPD_VERSION_FORMAT_UNKNOWN && fu_version_guess_format(fu_device_get_version(device)) == FWUPD_VERSION_FORMAT_NUMBER) { fu_device_inhibit(device, "version-format", "VersionFormat is ambiguous"); } /* no vendor-id, and so no way to lock it down! */ if (fu_device_is_updatable(device) && fu_device_get_vendor_ids(device)->len == 0) { fu_device_inhibit(device, "vendor-id", "No vendor ID set"); } /* create new device */ fu_device_list_add(self->device_list, device); #ifndef SUPPORTED_BUILD /* we don't know if this device has a signed or unsigned payload */ if (fu_device_is_updatable(device) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD) && !fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_MD_SET_SIGNED)) { g_critical("%s [%s] does not declare signed/unsigned payload -- perhaps add " "fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD);", fu_device_get_plugin(device), fu_device_get_id(device)); } #endif /* clean up any state only valid for ->probe */ fu_device_probe_complete(device); /* fix order */ fu_device_list_depsolve_order(self->device_list, device); /* save to emulated phase, but avoid overwriting reload */ if (fu_context_has_flag(self->ctx, FU_CONTEXT_FLAG_SAVE_EVENTS) && self->emulator_phase == FU_ENGINE_EMULATOR_PHASE_SETUP && fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATION_TAG) && !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { g_autoptr(GError) error_local = NULL; if (!fu_engine_emulator_save_phase(self->emulation, self->emulator_phase, self->emulator_write_cnt, &error_local)) g_warning("failed to save phase: %s", error_local->message); } fu_engine_emit_changed(self); } static void fu_engine_plugin_rules_changed_cb(FuPlugin *plugin, gpointer user_data) { FuEngine *self = FU_ENGINE(user_data); GPtrArray *rules = fu_plugin_get_rules(plugin, FU_PLUGIN_RULE_INHIBITS_IDLE); if (rules == NULL) return; for (guint j = 0; j < rules->len; j++) { const gchar *tmp = g_ptr_array_index(rules, j); fu_idle_inhibit(self->idle, FU_IDLE_INHIBIT_TIMEOUT, tmp); } } static void fu_engine_context_security_changed_cb(FuContext *ctx, gpointer user_data) { FuEngine *self = FU_ENGINE(user_data); /* invalidate host security attributes */ fu_security_attrs_remove_all(self->host_security_attrs); /* make UI refresh */ fu_engine_emit_changed(self); } static void fu_engine_plugin_device_removed_cb(FuPlugin *plugin, FuDevice *device, gpointer user_data) { FuEngine *self = (FuEngine *)user_data; FuPlugin *plugin_old; g_autoptr(GError) error = NULL; /* get the plugin */ plugin_old = fu_plugin_list_find_by_name(self->plugin_list, fu_device_get_plugin(device), &error); if (plugin_old == NULL) { g_info("failed to find plugin %s: %s", fu_device_get_plugin(device), error->message); return; } /* check this came from the same plugin */ if (g_strcmp0(fu_plugin_get_name(plugin), fu_plugin_get_name(plugin_old)) != 0) { g_info("ignoring duplicate removal from %s", fu_plugin_get_name(plugin)); return; } /* make the UI update */ fu_device_list_remove(self->device_list, device); fu_engine_emit_changed(self); } /* this is called by the self tests as well */ void fu_engine_add_plugin(FuEngine *self, FuPlugin *plugin) { fu_plugin_list_add(self->plugin_list, plugin); } gboolean fu_engine_is_uid_trusted(FuEngine *self, guint64 calling_uid) { GArray *trusted; /* root is always trusted */ if (calling_uid == 0) return TRUE; trusted = fu_engine_config_get_trusted_uids(self->config); for (guint i = 0; i < trusted->len; i++) { if (calling_uid == g_array_index(trusted, guint64, i)) return TRUE; } return FALSE; } static gboolean fu_engine_is_test_plugin_disabled(FuEngine *self, FuPlugin *plugin) { if (!fu_plugin_has_flag(plugin, FWUPD_PLUGIN_FLAG_TEST_ONLY)) return FALSE; if (fu_engine_config_get_test_devices(self->config)) return FALSE; return TRUE; } static gboolean fu_engine_is_plugin_name_disabled(FuEngine *self, const gchar *name) { GPtrArray *disabled = fu_engine_config_get_disabled_plugins(self->config); for (guint i = 0; i < disabled->len; i++) { const gchar *name_tmp = g_ptr_array_index(disabled, i); if (g_strcmp0(name_tmp, name) == 0) return TRUE; } return FALSE; } static gboolean fu_engine_is_plugin_name_enabled(FuEngine *self, const gchar *name) { if (self->plugin_filter->len == 0) return TRUE; for (guint i = 0; i < self->plugin_filter->len; i++) { const gchar *name_tmp = g_ptr_array_index(self->plugin_filter, i); if (g_pattern_match_simple(name_tmp, name)) return TRUE; } return FALSE; } void fu_engine_add_plugin_filter(FuEngine *self, const gchar *plugin_glob) { GString *str; g_return_if_fail(FU_IS_ENGINE(self)); g_return_if_fail(plugin_glob != NULL); str = g_string_new(plugin_glob); g_string_replace(str, "-", "_", 0); g_ptr_array_add(self->plugin_filter, g_string_free(str, FALSE)); } static gboolean fu_engine_plugin_check_supported_cb(FuPlugin *plugin, const gchar *guid, FuEngine *self) { g_autoptr(XbNode) n = NULL; g_autofree gchar *xpath = NULL; if (fu_engine_config_get_enumerate_all_devices(self->config)) return TRUE; xpath = g_strdup_printf("components/component[@type='firmware']/" "provides/firmware[@type='flashed'][text()='%s']", guid); n = xb_silo_query_first(self->silo, xpath, NULL); return n != NULL; } FuEngineConfig * fu_engine_get_config(FuEngine *self) { g_return_val_if_fail(FU_IS_ENGINE(self), NULL); return self->config; } const gchar * fu_engine_get_host_vendor(FuEngine *self) { const gchar *result = NULL; g_return_val_if_fail(FU_IS_ENGINE(self), NULL); result = fu_context_get_hwid_value(self->ctx, FU_HWIDS_KEY_MANUFACTURER); return result != NULL ? result : "Unknown Vendor"; } const gchar * fu_engine_get_host_product(FuEngine *self) { const gchar *result = NULL; g_return_val_if_fail(FU_IS_ENGINE(self), NULL); result = fu_context_get_hwid_value(self->ctx, FU_HWIDS_KEY_PRODUCT_NAME); return result != NULL ? result : "Unknown Product"; } const gchar * fu_engine_get_host_machine_id(FuEngine *self) { g_return_val_if_fail(FU_IS_ENGINE(self), NULL); return self->host_machine_id; } const gchar * fu_engine_get_host_bkc(FuEngine *self) { g_return_val_if_fail(FU_IS_ENGINE(self), NULL); if (fu_engine_config_get_host_bkc(self->config) == NULL) return ""; return fu_engine_config_get_host_bkc(self->config); } #ifdef HAVE_HSI static void fu_engine_ensure_security_attrs_supported_cpu(FuEngine *self) { g_autoptr(FwupdSecurityAttr) attr = fwupd_security_attr_new(FWUPD_SECURITY_ATTR_ID_SUPPORTED_CPU); fwupd_security_attr_set_plugin(attr, "core"); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONTACT_OEM); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_MISSING_DATA); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_VALID); fu_security_attrs_append(self->host_security_attrs, attr); } static void fu_engine_ensure_security_attrs_tainted(FuEngine *self) { gboolean disabled_plugins = FALSE; GPtrArray *disabled = fu_engine_config_get_disabled_plugins(self->config); g_autoptr(FwupdSecurityAttr) attr = fwupd_security_attr_new(FWUPD_SECURITY_ATTR_ID_FWUPD_PLUGINS); fwupd_security_attr_set_plugin(attr, "core"); fwupd_security_attr_set_result_success(attr, FWUPD_SECURITY_ATTR_RESULT_NOT_TAINTED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE); fu_security_attrs_append(self->host_security_attrs, attr); for (guint i = 0; i < disabled->len; i++) { const gchar *name_tmp = g_ptr_array_index(disabled, i); if (!g_str_has_prefix(name_tmp, "test")) { disabled_plugins = TRUE; break; } } if (self->plugin_filter->len > 0 || disabled_plugins) { fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_TAINTED); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_ACTION_CONFIG_OS); return; } /* success */ fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS); } /* private */ gchar * fu_engine_get_host_security_id(FuEngine *self, const gchar *fwupd_version) { FuSmbiosChassisKind chassis_kind; g_return_val_if_fail(FU_IS_ENGINE(self), NULL); fu_engine_ensure_security_attrs(self); /* if emulating, force the chassis type to be valid */ chassis_kind = fu_context_get_chassis_kind(self->ctx); if (self->host_emulation && (chassis_kind == FU_SMBIOS_CHASSIS_KIND_OTHER || chassis_kind == FU_SMBIOS_CHASSIS_KIND_UNKNOWN)) { g_info("forcing chassis kind %s to be valid", fu_smbios_chassis_kind_to_string(chassis_kind)); chassis_kind = FU_SMBIOS_CHASSIS_KIND_DESKTOP; } switch (chassis_kind) { case FU_SMBIOS_CHASSIS_KIND_DESKTOP: case FU_SMBIOS_CHASSIS_KIND_LOW_PROFILE_DESKTOP: case FU_SMBIOS_CHASSIS_KIND_MINI_TOWER: case FU_SMBIOS_CHASSIS_KIND_TOWER: case FU_SMBIOS_CHASSIS_KIND_PORTABLE: case FU_SMBIOS_CHASSIS_KIND_LAPTOP: case FU_SMBIOS_CHASSIS_KIND_NOTEBOOK: case FU_SMBIOS_CHASSIS_KIND_ALL_IN_ONE: case FU_SMBIOS_CHASSIS_KIND_SUB_NOTEBOOK: case FU_SMBIOS_CHASSIS_KIND_LUNCH_BOX: case FU_SMBIOS_CHASSIS_KIND_MAIN_SERVER: case FU_SMBIOS_CHASSIS_KIND_TABLET: case FU_SMBIOS_CHASSIS_KIND_CONVERTIBLE: case FU_SMBIOS_CHASSIS_KIND_DETACHABLE: case FU_SMBIOS_CHASSIS_KIND_IOT_GATEWAY: case FU_SMBIOS_CHASSIS_KIND_EMBEDDED_PC: case FU_SMBIOS_CHASSIS_KIND_MINI_PC: case FU_SMBIOS_CHASSIS_KIND_STICK_PC: return fu_security_attrs_calculate_hsi(self->host_security_attrs, fwupd_version, FU_SECURITY_ATTRS_FLAG_ADD_VERSION); default: break; } return g_strdup_printf("HSI:INVALID:chassis[%s] (v%d.%d.%d)", fu_smbios_chassis_kind_to_string(chassis_kind), FWUPD_MAJOR_VERSION, FWUPD_MINOR_VERSION, FWUPD_MICRO_VERSION); } static gboolean fu_engine_record_security_attrs(FuEngine *self, GError **error) { g_autoptr(GPtrArray) attrs_array = NULL; g_autofree gchar *host_security_id = fu_engine_get_host_security_id(self, NULL); g_autofree gchar *json = NULL; /* convert attrs to json string */ json = fwupd_codec_to_json_string(FWUPD_CODEC(self->host_security_attrs), FWUPD_CODEC_FLAG_NONE, error); if (json == NULL) { g_prefix_error(error, "cannot convert current attrs to string: "); return FALSE; } /* check that we did not store this already last boot */ attrs_array = fu_history_get_security_attrs(self->history, 1, error); if (attrs_array == NULL) { g_prefix_error(error, "failed to get historical attr: "); return FALSE; } if (attrs_array->len > 0) { FuSecurityAttrs *attrs_tmp = g_ptr_array_index(attrs_array, 0); if (fu_security_attrs_equal(attrs_tmp, self->host_security_attrs)) { g_info("skipping writing HSI attrs to database as unchanged"); return TRUE; } } /* write new values */ if (!fu_history_add_security_attribute(self->history, json, host_security_id, error)) { g_prefix_error(error, "failed to write to DB: "); return FALSE; } /* success */ return TRUE; } static void fu_engine_security_attrs_depsolve(FuEngine *self) { g_autoptr(GPtrArray) items = NULL; /* set the obsoletes flag for each attr */ fu_security_attrs_depsolve(self->host_security_attrs); /* set the fallback names for clients without native translations */ items = fu_security_attrs_get_all(self->host_security_attrs, NULL); for (guint i = 0; i < items->len; i++) { FwupdSecurityAttr *attr = g_ptr_array_index(items, i); if (fwupd_security_attr_get_name(attr) == NULL) { g_autofree gchar *name_tmp = fu_security_attr_get_name(attr); if (name_tmp == NULL) { g_warning("failed to get fallback for %s", fwupd_security_attr_get_appstream_id(attr)); continue; } fwupd_security_attr_set_name(attr, name_tmp); } if (fwupd_security_attr_get_title(attr) == NULL) fwupd_security_attr_set_title(attr, fu_security_attr_get_title(attr)); if (fwupd_security_attr_get_description(attr) == NULL) { fwupd_security_attr_set_description(attr, fu_security_attr_get_description(attr)); } } } #endif /** * fu_history_get_previous_security_attr: * @self: a #FuHistory * @appstream_id: maximum number of attributes to return, or 0 for no limit * @current_setting: (nullable): current value * @error: return location for a #GError, or %NULL * * Gets the security attributes of the previous BIOS setting for the given * appstream ID and current BIOS config. * * Returns: (element-type #FuSecurityAttr) (transfer full): attr, or %NULL **/ static FwupdSecurityAttr * fu_engine_get_previous_bios_security_attr(FuEngine *self, const gchar *appstream_id, const gchar *current_setting, GError **error) { g_autoptr(GPtrArray) attrs_array = NULL; attrs_array = fu_history_get_security_attrs(self->history, 20, error); if (attrs_array == NULL) return NULL; for (guint i = 0; i < attrs_array->len; i++) { FuSecurityAttrs *attrs = g_ptr_array_index(attrs_array, i); g_autoptr(GPtrArray) attr_items = fu_security_attrs_get_all(attrs, NULL); for (guint j = 0; j < attr_items->len; j++) { FwupdSecurityAttr *attr = g_ptr_array_index(attr_items, j); if (g_strcmp0(appstream_id, fwupd_security_attr_get_appstream_id(attr)) == 0 && g_strcmp0(current_setting, fwupd_security_attr_get_bios_setting_current_value(attr)) != 0) { g_debug("found previous BIOS setting for %s: %s", appstream_id, fwupd_security_attr_get_bios_setting_id(attr)); return g_object_ref(attr); } } } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot find previous BIOS value"); return NULL; } /** * fu_engine_fix_host_security_attr: * @self: a #FuEngine * @appstream_id: the Appstream ID * @error: (nullable): optional return location for an error * * Fix one specific security attribute. * * Returns: %TRUE for success **/ gboolean fu_engine_fix_host_security_attr(FuEngine *self, const gchar *appstream_id, GError **error) { FuPlugin *plugin; FwupdBiosSetting *bios_attr; g_autoptr(FwupdSecurityAttr) hsi_attr = NULL; g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_ENGINE(self), FALSE); fu_engine_ensure_security_attrs(self); hsi_attr = fu_security_attrs_get_by_appstream_id(self->host_security_attrs, appstream_id, error); if (hsi_attr == NULL) return FALSE; if (!fwupd_security_attr_has_flag(hsi_attr, FWUPD_SECURITY_ATTR_FLAG_CAN_FIX)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot auto-fix attribute"); return FALSE; } plugin = fu_plugin_list_find_by_name(self->plugin_list, fwupd_security_attr_get_plugin(hsi_attr), error); if (plugin == NULL) return FALSE; /* first try the per-plugin vfunc */ if (!fu_plugin_runner_fix_host_security_attr(plugin, hsi_attr, &error_local)) { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } g_debug("ignoring %s", error_local->message); } else { g_info("fixed %s", fwupd_security_attr_get_appstream_id(hsi_attr)); return TRUE; } /* fall back to setting the BIOS attribute */ if (fwupd_security_attr_get_bios_setting_id(hsi_attr) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no BIOS setting ID set"); return FALSE; } bios_attr = fu_context_get_bios_setting(self->ctx, fwupd_security_attr_get_bios_setting_id(hsi_attr)); if (bios_attr == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot get BIOS setting %s", fwupd_security_attr_get_bios_setting_id(hsi_attr)); return FALSE; } return fwupd_bios_setting_write_value( bios_attr, fwupd_security_attr_get_bios_setting_target_value(hsi_attr), error); } /** * fu_engine_fix_host_security_attr: * @self: a #FuEngine * @appstream_id: the Appstream ID * @error: (nullable): optional return location for an error * * Revert the fix for one specific security attribute. * * Returns: %TRUE for success **/ gboolean fu_engine_undo_host_security_attr(FuEngine *self, const gchar *appstream_id, GError **error) { FuPlugin *plugin; FwupdBiosSetting *bios_attr; g_autoptr(FwupdSecurityAttr) hsi_attr = NULL; g_autoptr(FwupdSecurityAttr) hsi_attr_old = NULL; g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_ENGINE(self), FALSE); fu_engine_ensure_security_attrs(self); hsi_attr = fu_security_attrs_get_by_appstream_id(self->host_security_attrs, appstream_id, error); if (hsi_attr == NULL) return FALSE; if (!fwupd_security_attr_has_flag(hsi_attr, FWUPD_SECURITY_ATTR_FLAG_CAN_UNDO)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot auto-undo attribute"); return FALSE; } plugin = fu_plugin_list_find_by_name(self->plugin_list, fwupd_security_attr_get_plugin(hsi_attr), error); if (plugin == NULL) return FALSE; /* first try the per-plugin vfunc */ if (!fu_plugin_runner_undo_host_security_attr(plugin, hsi_attr, &error_local)) { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } /* fall back to setting the BIOS attribute */ if (fwupd_security_attr_get_bios_setting_id(hsi_attr) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no BIOS setting ID"); return FALSE; } bios_attr = fu_context_get_bios_setting(self->ctx, fwupd_security_attr_get_bios_setting_id(hsi_attr)); if (bios_attr == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "cannot get BIOS setting %s", fwupd_security_attr_get_bios_setting_id(hsi_attr)); return FALSE; } if (fwupd_security_attr_get_bios_setting_current_value(hsi_attr) == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no BIOS setting current value"); return FALSE; } hsi_attr_old = fu_engine_get_previous_bios_security_attr( self, appstream_id, fwupd_security_attr_get_bios_setting_current_value(hsi_attr), error); if (hsi_attr_old == NULL) return FALSE; return fwupd_bios_setting_write_value( bios_attr, fwupd_security_attr_get_bios_setting_current_value(hsi_attr_old), error); } static gboolean fu_engine_security_attrs_from_json(FuEngine *self, JsonNode *json_node, GError **error) { JsonObject *obj; /* sanity check */ if (!JSON_NODE_HOLDS_OBJECT(json_node)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not JSON object"); return FALSE; } /* not supplied */ obj = json_node_get_object(json_node); if (!json_object_has_member(obj, "SecurityAttributes")) return TRUE; if (!fwupd_codec_from_json(FWUPD_CODEC(self->host_security_attrs), json_node, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_engine_devices_from_json(FuEngine *self, JsonNode *json_node, GError **error) { JsonArray *array; JsonObject *obj; /* sanity check */ if (!JSON_NODE_HOLDS_OBJECT(json_node)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "not JSON object"); return FALSE; } /* not supplied */ obj = json_node_get_object(json_node); if (!json_object_has_member(obj, "Devices")) return TRUE; /* this has to exist */ array = json_object_get_array_member(obj, "Devices"); for (guint i = 0; i < json_array_get_length(array); i++) { JsonNode *node_tmp = json_array_get_element(array, i); g_autoptr(FuDevice) device = fu_device_new(self->ctx); if (!fwupd_codec_from_json(FWUPD_CODEC(device), node_tmp, error)) return FALSE; fu_device_set_plugin(device, "dummy"); fu_device_add_problem(device, FWUPD_DEVICE_PROBLEM_IS_EMULATED); if (!fu_device_setup(device, error)) return FALSE; fu_engine_add_device(self, device); } /* success */ return TRUE; } static gboolean fu_engine_load_host_emulation(FuEngine *self, const gchar *fn, GError **error) { g_autoptr(JsonParser) parser = json_parser_new(); g_autoptr(GFile) file = g_file_new_for_path(fn); g_autoptr(GInputStream) istream_json = NULL; g_autoptr(GInputStream) istream_raw = NULL; g_autoptr(FwupdSecurityAttr) attr = NULL; g_autoptr(FuBiosSettings) bios_settings = fu_context_get_bios_settings(self->ctx); /* add an attr so we know this is emulated and do not offer to upload results */ attr = fwupd_security_attr_new(FWUPD_SECURITY_ATTR_ID_HOST_EMULATION); fwupd_security_attr_set_plugin(attr, "core"); fwupd_security_attr_add_flag(attr, FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE); fwupd_security_attr_set_result(attr, FWUPD_SECURITY_ATTR_RESULT_ENABLED); fu_security_attrs_append(self->host_security_attrs, attr); /* add from file */ istream_raw = G_INPUT_STREAM(g_file_read(file, NULL, error)); if (istream_raw == NULL) return FALSE; if (g_str_has_suffix(fn, ".gz")) { g_autoptr(GConverter) conv = G_CONVERTER(g_zlib_decompressor_new(G_ZLIB_COMPRESSOR_FORMAT_GZIP)); istream_json = g_converter_input_stream_new(istream_raw, conv); } else { istream_json = g_object_ref(istream_raw); } if (!json_parser_load_from_stream(parser, istream_json, NULL, error)) return FALSE; if (!fu_engine_devices_from_json(self, json_parser_get_root(parser), error)) return FALSE; if (!fu_engine_security_attrs_from_json(self, json_parser_get_root(parser), error)) return FALSE; if (!fwupd_codec_from_json(FWUPD_CODEC(bios_settings), json_parser_get_root(parser), error)) return FALSE; #ifdef HAVE_HSI /* depsolve */ fu_engine_security_attrs_depsolve(self); #endif /* success */ return TRUE; } static void fu_engine_ensure_security_attrs(FuEngine *self) { #ifdef HAVE_HSI GPtrArray *plugins = fu_plugin_list_get_all(self->plugin_list); g_autoptr(GPtrArray) devices = fu_device_list_get_active(self->device_list); g_autoptr(GPtrArray) vals = NULL; g_autoptr(GError) error = NULL; /* already valid */ if (fu_security_attrs_is_valid(self->host_security_attrs) || self->host_emulation) return; /* built in */ fu_engine_ensure_security_attrs_supported_cpu(self); fu_engine_ensure_security_attrs_tainted(self); /* call into devices */ for (guint i = 0; i < devices->len; i++) { FuDevice *device = g_ptr_array_index(devices, i); fu_device_add_security_attrs(device, self->host_security_attrs); } /* call into plugins */ for (guint j = 0; j < plugins->len; j++) { FuPlugin *plugin_tmp = g_ptr_array_index(plugins, j); fu_plugin_runner_add_security_attrs(plugin_tmp, self->host_security_attrs); } /* sanity check */ vals = fu_security_attrs_get_all(self->host_security_attrs, NULL); for (guint i = 0; i < vals->len; i++) { FwupdSecurityAttr *attr = g_ptr_array_index(vals, i); if (fwupd_security_attr_get_result(attr) == FWUPD_SECURITY_ATTR_RESULT_UNKNOWN) { #ifdef SUPPORTED_BUILD g_debug("HSI attribute %s (from %s) had unknown result", fwupd_security_attr_get_appstream_id(attr), fwupd_security_attr_get_plugin(attr)); #else g_warning("HSI attribute %s (from %s) had unknown result", fwupd_security_attr_get_appstream_id(attr), fwupd_security_attr_get_plugin(attr)); #endif } } /* depsolve */ fu_engine_security_attrs_depsolve(self); /* record into the database (best effort) */ if (!fu_engine_record_security_attrs(self, &error)) g_warning("failed to record HSI attributes: %s", error->message); #endif } FuSecurityAttrs * fu_engine_get_host_security_attrs(FuEngine *self) { g_return_val_if_fail(FU_IS_ENGINE(self), NULL); fu_engine_ensure_security_attrs(self); return g_object_ref(self->host_security_attrs); } FuSecurityAttrs * fu_engine_get_host_security_events(FuEngine *self, guint limit, GError **error) { g_autoptr(FuSecurityAttrs) events = fu_security_attrs_new(); g_autoptr(GPtrArray) attrs_array = NULL; g_return_val_if_fail(FU_IS_ENGINE(self), NULL); attrs_array = fu_history_get_security_attrs(self->history, limit, error); if (attrs_array == NULL) return NULL; for (guint i = 1; i < attrs_array->len; i++) { FuSecurityAttrs *attrs_new = g_ptr_array_index(attrs_array, i - 1); FuSecurityAttrs *attrs_old = g_ptr_array_index(attrs_array, i - 0); g_autoptr(GPtrArray) diffs = fu_security_attrs_compare(attrs_old, attrs_new); for (guint j = 0; j < diffs->len; j++) { FwupdSecurityAttr *attr = g_ptr_array_index(diffs, j); if (fwupd_security_attr_get_title(attr) == NULL) { fwupd_security_attr_set_title(attr, fu_security_attr_get_title(attr)); } if (fwupd_security_attr_get_description(attr) == NULL) { fwupd_security_attr_set_description( attr, fu_security_attr_get_description(attr)); } fu_security_attrs_append_internal(events, attr); } } /* success */ return g_steal_pointer(&events); } static void fu_engine_load_plugins_filename(FuEngine *self, const gchar *filename, FuProgress *progress) { g_autofree gchar *name = NULL; g_autoptr(FuPlugin) plugin = NULL; g_autoptr(GError) error_local = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_name(progress, filename); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_NO_PROFILE); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 97, "add"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 3, "open"); /* sanity check */ name = fu_plugin_guess_name_from_fn(filename); if (name == NULL) { fu_progress_finished(progress); return; } /* open module */ plugin = fu_plugin_new(self->ctx); fu_plugin_set_name(plugin, name); fu_engine_add_plugin(self, plugin); fu_progress_step_done(progress); /* open the plugin and call ->load() */ if (!fu_plugin_open(plugin, filename, &error_local)) g_warning("cannot load: %s", error_local->message); fu_progress_step_done(progress); } static void fu_engine_load_plugins_filenames(FuEngine *self, GPtrArray *filenames, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, filenames->len); for (guint i = 0; i < filenames->len; i++) { const gchar *filename = g_ptr_array_index(filenames, i); fu_engine_load_plugins_filename(self, filename, fu_progress_get_child(progress)); fu_progress_step_done(progress); } } static void fu_engine_load_plugins_builtins(FuEngine *self, FuProgress *progress) { guint steps = 0; /* count possible steps */ for (guint i = 0; fu_plugin_externals[i] != NULL; i++) steps++; if (steps == 0) return; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, steps); for (guint i = 0; fu_plugin_externals[i] != NULL; i++) { GType plugin_gtype = fu_plugin_externals[i](); g_autoptr(FuPlugin) plugin = fu_plugin_new_from_gtype(plugin_gtype, self->ctx); fu_progress_set_name(fu_progress_get_child(progress), fu_plugin_get_name(plugin)); fu_engine_add_plugin(self, plugin); fu_progress_step_done(progress); } } static gboolean fu_engine_load_plugins(FuEngine *self, FuEngineLoadFlags flags, FuProgress *progress, GError **error) { g_autofree gchar *plugin_path = NULL; g_autoptr(GPtrArray) filenames = g_ptr_array_new_with_free_func(g_free); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_NO_PROFILE); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 13, "search"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 87, "load"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 5, "load-builtins"); /* search */ plugin_path = fu_path_from_kind(FU_PATH_KIND_LIBDIR_PKG); if (flags & FU_ENGINE_LOAD_FLAG_EXTERNAL_PLUGINS) { g_auto(GStrv) plugin_paths = g_strsplit(plugin_path, ",", 0); for (guint i = 0; plugin_paths[i] != NULL; i++) { g_autoptr(GPtrArray) filenames_tmp = NULL; g_autoptr(GError) error_local = NULL; filenames_tmp = fu_path_get_files(plugin_paths[i], &error_local); if (filenames_tmp == NULL) { g_debug("no external plugins found in %s: %s", plugin_paths[i], error_local->message); continue; } for (guint j = 0; j < filenames_tmp->len; j++) { const gchar *filename = g_ptr_array_index(filenames_tmp, j); if (!g_str_has_suffix(filename, ".so")) continue; g_ptr_array_add(filenames, g_strdup(filename)); } } } fu_progress_step_done(progress); /* load */ if (filenames != NULL) fu_engine_load_plugins_filenames(self, filenames, fu_progress_get_child(progress)); fu_progress_step_done(progress); /* load builtins */ if (flags & FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS) fu_engine_load_plugins_builtins(self, fu_progress_get_child(progress)); fu_progress_step_done(progress); /* success */ return TRUE; } static gboolean fu_engine_plugins_init(FuEngine *self, FuProgress *progress, GError **error) { GPtrArray *plugins = fu_plugin_list_get_all(self->plugin_list); g_autoptr(GPtrArray) plugins_disabled = g_ptr_array_new_with_free_func(g_free); g_autoptr(GPtrArray) plugins_disabled_rt = g_ptr_array_new_with_free_func(g_free); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, plugins->len); for (guint i = 0; i < plugins->len; i++) { FuPlugin *plugin = g_ptr_array_index(plugins, i); const gchar *name = fu_plugin_get_name(plugin); /* progress */ fu_progress_set_name(fu_progress_get_child(progress), name); /* is disabled */ if (fu_engine_is_plugin_name_disabled(self, name) || fu_engine_is_test_plugin_disabled(self, plugin) || !fu_engine_is_plugin_name_enabled(self, name)) { g_ptr_array_add(plugins_disabled, g_strdup(name)); fu_plugin_add_flag(plugin, FWUPD_PLUGIN_FLAG_DISABLED); fu_progress_step_done(progress); continue; } /* init plugin, adding device and firmware GTypes */ fu_plugin_runner_init(plugin); /* runtime disabled */ if (fu_plugin_has_flag(plugin, FWUPD_PLUGIN_FLAG_DISABLED)) { g_ptr_array_add(plugins_disabled_rt, g_strdup(name)); fu_progress_step_done(progress); continue; } /* watch for changes */ g_signal_connect(FU_PLUGIN(plugin), "device-added", G_CALLBACK(fu_engine_plugin_device_added_cb), self); g_signal_connect(FU_PLUGIN(plugin), "device-removed", G_CALLBACK(fu_engine_plugin_device_removed_cb), self); g_signal_connect(FU_PLUGIN(plugin), "device-register", G_CALLBACK(fu_engine_plugin_device_register_cb), self); g_signal_connect(FU_PLUGIN(plugin), "check-supported", G_CALLBACK(fu_engine_plugin_check_supported_cb), self); g_signal_connect(FU_PLUGIN(plugin), "rules-changed", G_CALLBACK(fu_engine_plugin_rules_changed_cb), self); fu_progress_step_done(progress); } /* show list */ if (plugins_disabled->len > 0) { g_autofree gchar *str = NULL; g_ptr_array_add(plugins_disabled, NULL); str = g_strjoinv(", ", (gchar **)plugins_disabled->pdata); g_info("plugins disabled: %s", str); } if (plugins_disabled_rt->len > 0) { g_autofree gchar *str = NULL; g_ptr_array_add(plugins_disabled_rt, NULL); str = g_strjoinv(", ", (gchar **)plugins_disabled_rt->pdata); g_info("plugins runtime-disabled: %s", str); } /* depsolve into the correct order */ if (!fu_plugin_list_depsolve(self->plugin_list, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_engine_cleanup_state(GError **error) { const gchar *filenames[] = {"/var/cache/app-info/xmls/fwupd-verify.xml", "/var/cache/app-info/xmls/fwupd.xml", NULL}; for (guint i = 0; filenames[i] != NULL; i++) { g_autoptr(GFile) file = g_file_new_for_path(filenames[i]); if (g_file_query_exists(file, NULL)) { if (!g_file_delete(file, NULL, error)) return FALSE; } } return TRUE; } static gboolean fu_engine_apply_default_bios_settings_policy(FuEngine *self, GError **error) { const gchar *tmp; g_autofree gchar *base = fu_path_from_kind(FU_PATH_KIND_SYSCONFDIR_PKG); g_autofree gchar *dirname = g_build_filename(base, "bios-settings.d", NULL); g_autoptr(FuBiosSettings) new_bios_settings = fu_bios_settings_new(); g_autoptr(GHashTable) hashtable = NULL; g_autoptr(GDir) dir = NULL; if (!g_file_test(dirname, G_FILE_TEST_EXISTS)) return TRUE; dir = g_dir_open(dirname, 0, error); if (dir == NULL) return FALSE; while ((tmp = g_dir_read_name(dir)) != NULL) { g_autofree gchar *data = NULL; g_autofree gchar *fn = NULL; if (!g_str_has_suffix(tmp, ".json")) continue; fn = g_build_filename(dirname, tmp, NULL); g_info("loading default BIOS settings policy from %s", fn); if (!g_file_get_contents(fn, &data, NULL, error)) return FALSE; if (!fwupd_codec_from_json_string(FWUPD_CODEC(new_bios_settings), data, error)) return FALSE; } hashtable = fu_bios_settings_to_hash_kv(new_bios_settings); return fu_engine_modify_bios_settings(self, hashtable, TRUE, error); } static void fu_engine_check_firmware_attributes(FuEngine *self, FuDevice *device, gboolean added) { const gchar *subsystem; if (!FU_IS_UDEV_DEVICE(device)) return; if (self->host_emulation) return; subsystem = fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device)); if (g_strcmp0(subsystem, "firmware-attributes") == 0) { g_autoptr(GError) error = NULL; if (added) { g_autoptr(FuBiosSettings) settings = fu_context_get_bios_settings(self->ctx); g_autoptr(GPtrArray) items = fu_bios_settings_get_all(settings); if (items->len > 0) { g_debug("ignoring add event for already loaded settings"); return; } } if (!fu_context_reload_bios_settings(self->ctx, &error)) { g_debug("%s", error->message); return; } if (!fu_engine_apply_default_bios_settings_policy(self, &error)) { if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) g_debug("%s", error->message); else g_warning("failed to apply BIOS settings policy: %s", error->message); return; } } } static void fu_engine_backend_device_removed_cb(FuBackend *backend, FuDevice *device, FuEngine *self) { g_autoptr(GPtrArray) devices = NULL; /* if this is for firmware attributes, reload that part of the daemon */ fu_engine_check_firmware_attributes(self, device, FALSE); /* debug */ g_debug("%s removed %s", fu_backend_get_name(backend), fu_device_get_backend_id(device)); /* go through each device and remove any that match */ devices = fu_device_list_get_active(self->device_list); for (guint i = 0; i < devices->len; i++) { FuDevice *device_tmp = g_ptr_array_index(devices, i); if (g_strcmp0(fu_device_get_backend_id(device_tmp), fu_device_get_backend_id(device)) == 0) { FuPlugin *plugin; if (fu_device_has_private_flag(device_tmp, FU_DEVICE_PRIVATE_FLAG_NO_AUTO_REMOVE)) { g_info("not auto-removing backend device %s [%s] due to flags", fu_device_get_name(device_tmp), fu_device_get_id(device_tmp)); continue; } plugin = fu_plugin_list_find_by_name(self->plugin_list, fu_device_get_plugin(device_tmp), NULL); if (plugin == NULL) continue; g_info("auto-removing backend device %s [%s]", fu_device_get_name(device_tmp), fu_device_get_id(device_tmp)); fu_plugin_device_remove(plugin, device_tmp); } } } static gboolean fu_engine_backend_device_added_run_plugin(FuEngine *self, FuDevice *device, const gchar *plugin_name, FuProgress *progress, GError **error) { FuPlugin *plugin; /* find plugin */ fu_progress_set_name(progress, plugin_name); plugin = fu_plugin_list_find_by_name(self->plugin_list, plugin_name, error); if (plugin == NULL) return FALSE; /* run the ->probe() then ->setup() vfuncs */ if (!fu_plugin_runner_backend_device_added(plugin, device, progress, error)) { #ifdef SUPPORTED_BUILD /* sanity check */ if (*error == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "%s failed but no error set", fu_device_get_backend_id(device)); return FALSE; } #endif return FALSE; } /* success */ return TRUE; } static void fu_engine_backend_device_added_run_plugins(FuEngine *self, FuDevice *device, FuProgress *progress) { g_autoptr(GPtrArray) possible_plugins = fu_device_get_possible_plugins(device); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, possible_plugins->len); for (guint i = 0; i < possible_plugins->len; i++) { const gchar *plugin_name = g_ptr_array_index(possible_plugins, i); g_autoptr(GError) error_local = NULL; if (!fu_engine_backend_device_added_run_plugin(self, device, plugin_name, fu_progress_get_child(progress), &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_debug("%s ignoring: %s", plugin_name, error_local->message); } else { g_warning("failed to add device %s: %s", fu_device_get_backend_id(device), error_local->message); } fu_progress_add_flag(progress, FU_PROGRESS_FLAG_CHILD_FINISHED); fu_progress_step_done(progress); continue; } fu_progress_step_done(progress); } } static void fu_engine_backend_device_added(FuEngine *self, FuDevice *device, FuProgress *progress) { g_autoptr(GError) error_local = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_NO_PROFILE); fu_progress_set_name(progress, fu_device_get_backend_id(device)); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 50, "probe-baseclass"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 50, "query-possible-plugins"); /* super useful for plugin development */ if (g_getenv("FWUPD_VERBOSE") != NULL) { g_autofree gchar *str = fu_device_to_string(FU_DEVICE(device)); g_debug("%s added %s", fu_device_get_backend_id(device), str); } /* add any extra quirks */ fu_device_set_context(device, self->ctx); if (!fu_device_probe(device, &error_local)) { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED) && !g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT)) { g_warning("failed to probe device %s: %s", fu_device_get_backend_id(device), error_local->message); } else { g_debug("failed to probe device %s : %s", fu_device_get_backend_id(device), error_local->message); } fu_progress_finished(progress); return; } fu_progress_step_done(progress); /* check if the device needs emulation-tag */ fu_engine_ensure_device_emulation_tag(self, device); /* super useful for plugin development */ if (g_getenv("FWUPD_VERBOSE") != NULL) { g_autofree gchar *str = fu_device_to_string(FU_DEVICE(device)); g_debug("%s added %s", fu_device_get_backend_id(device), str); } /* if this is for firmware attributes, reload that part of the daemon */ fu_engine_check_firmware_attributes(self, device, TRUE); /* can be specified using a quirk */ fu_engine_backend_device_added_run_plugins(self, device, fu_progress_get_child(progress)); fu_progress_step_done(progress); } static void fu_engine_backend_device_added_cb(FuBackend *backend, FuDevice *device, FuEngine *self) { g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GPtrArray) possible_plugins = NULL; fu_engine_backend_device_added(self, device, progress); /* free data cached during ->probe */ fu_device_probe_complete(device); /* there's no point keeping this in the cache */ possible_plugins = fu_device_get_possible_plugins(device); if (possible_plugins->len == 0) { g_debug("removing %s from backend cache as no possible plugin", fu_device_get_backend_id(device)); fu_backend_device_removed(backend, device); } } static void fu_engine_backend_device_changed_cb(FuBackend *backend, FuDevice *device, FuEngine *self) { GPtrArray *plugins = fu_plugin_list_get_all(self->plugin_list); g_autoptr(GPtrArray) devices = NULL; /* debug */ g_debug("%s changed %s", fu_backend_get_name(backend), fu_device_get_physical_id(device)); /* emit changed on any that match */ devices = fu_device_list_get_active(self->device_list); for (guint i = 0; i < devices->len; i++) { FuDevice *device_tmp = g_ptr_array_index(devices, i); if (!FU_IS_UDEV_DEVICE(device_tmp) || !FU_IS_UDEV_DEVICE(device)) continue; if (g_strcmp0(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device_tmp)), fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device))) == 0) { fu_udev_device_emit_changed(FU_UDEV_DEVICE(device)); } } /* update the device for emulated devices */ for (guint i = 0; i < devices->len; i++) { FuDevice *device_tmp = g_ptr_array_index(devices, i); if (!fu_device_has_flag(device_tmp, FWUPD_DEVICE_FLAG_EMULATED)) continue; if (!fu_device_has_flag(device_tmp, FWUPD_DEVICE_FLAG_CAN_EMULATION_TAG)) continue; if (g_strcmp0(fu_device_get_backend_id(device_tmp), fu_device_get_backend_id(device)) == 0) { g_debug("incorporating new device for %s", fu_device_get_id(device_tmp)); fu_device_incorporate(device_tmp, device, FU_DEVICE_INCORPORATE_FLAG_ALL); } } /* run all plugins */ for (guint j = 0; j < plugins->len; j++) { FuPlugin *plugin_tmp = g_ptr_array_index(plugins, j); g_autoptr(GError) error = NULL; if (!fu_plugin_runner_backend_device_changed(plugin_tmp, device, &error)) { #ifdef SUPPORTED_BUILD /* sanity check */ if (error == NULL) { g_critical( "failed to change device %s: exec failed but no error set!", fu_device_get_backend_id(device)); continue; } #endif if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_debug("%s ignoring: %s", fu_plugin_get_name(plugin_tmp), error->message); continue; } g_warning("%s failed to change device %s: %s", fu_plugin_get_name(plugin_tmp), fu_device_get_id(device), error->message); } } } static void fu_engine_load_quirks_for_hwid(FuEngine *self, const gchar *hwid) { FuPlugin *plugin; const gchar *value; g_auto(GStrv) plugins = NULL; /* does prefixed quirk exist */ value = fu_context_lookup_quirk_by_id(self->ctx, hwid, FU_QUIRKS_PLUGIN); if (value == NULL) return; plugins = g_strsplit(value, ",", -1); for (guint i = 0; plugins[i] != NULL; i++) { g_autoptr(GError) error_local = NULL; plugin = fu_plugin_list_find_by_name(self->plugin_list, plugins[i], &error_local); if (plugin == NULL) { g_info("no %s plugin for HwId %s: %s", plugins[i], hwid, error_local->message); continue; } g_info("enabling %s due to HwId %s", plugins[i], hwid); fu_plugin_remove_flag(plugin, FWUPD_PLUGIN_FLAG_REQUIRE_HWID); } } static gboolean fu_engine_update_history_device(FuEngine *self, FuDevice *dev_history, GError **error) { FuPlugin *plugin; FuRelease *rel_history; g_autofree gchar *btime = NULL; g_autoptr(FuDevice) dev = NULL; g_autoptr(GHashTable) metadata_device = NULL; /* is in the device list */ dev = fu_device_list_get_by_id(self->device_list, fu_device_get_id(dev_history), error); if (dev == NULL) return FALSE; /* does the installed version match what we tried to install * before fwupd was restarted */ rel_history = FU_RELEASE(fu_device_get_release_default(dev_history)); if (rel_history == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "no release for history FuDevice"); return FALSE; } /* is this the same boot time as when we scheduled the update, * i.e. has fwupd been restarted before we rebooted */ btime = fu_engine_get_boot_time(); if (g_strcmp0(fu_release_get_metadata_item(rel_history, "BootTime"), btime) == 0) { g_info("service restarted, but no reboot has taken place"); /* if it needed reboot then, it also needs it now... */ if (fu_device_get_update_state(dev_history) == FWUPD_UPDATE_STATE_NEEDS_REBOOT) { g_info("inheriting needs-reboot for %s", fu_device_get_name(dev)); fu_device_set_update_state(dev, FWUPD_UPDATE_STATE_NEEDS_REBOOT); } return TRUE; } /* save any additional report metadata */ metadata_device = fu_device_report_metadata_post(dev); if (metadata_device != NULL && g_hash_table_size(metadata_device) > 0) { fu_release_add_metadata(rel_history, metadata_device); if (!fu_history_modify_device_release(self->history, dev_history, rel_history, error)) { g_prefix_error(error, "failed to set metadata: "); return FALSE; } } /* measure the "new" system state */ plugin = fu_plugin_list_find_by_name(self->plugin_list, fu_device_get_plugin(dev), error); if (plugin == NULL) return FALSE; if (fu_plugin_has_flag(plugin, FWUPD_PLUGIN_FLAG_MEASURE_SYSTEM_INTEGRITY)) fu_engine_update_release_integrity(self, rel_history, "SystemIntegrityNew"); /* do any late-cleanup actions */ if (!fu_plugin_runner_reboot_cleanup(plugin, dev, error)) { g_prefix_error(error, "failed to do post-reboot cleanup: "); return FALSE; } /* the system is running with the new firmware version */ if (fu_version_compare(fu_device_get_version(dev), fu_release_get_version(rel_history), fu_device_get_version_format(dev)) == 0) { GPtrArray *checksums; g_info("installed version %s matching history %s", fu_device_get_version(dev), fu_release_get_version(rel_history)); /* copy over runtime checksums if set from probe() */ checksums = fu_device_get_checksums(dev); for (guint i = 0; i < checksums->len; i++) { const gchar *csum = g_ptr_array_index(checksums, i); fu_device_add_checksum(dev_history, csum); } fu_device_set_version_format(dev_history, fu_device_get_version_format(dev)); fu_device_set_version(dev_history, fu_device_get_version(dev)); fu_device_remove_flag(dev_history, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION); fu_device_set_update_state(dev_history, FWUPD_UPDATE_STATE_SUCCESS); return fu_history_modify_device_release(self->history, dev_history, rel_history, error); } /* does the plugin know the update failure */ if (!fu_plugin_runner_get_results(plugin, dev, error)) return FALSE; /* the plugin either can't tell us the error, or doesn't know itself */ if (fu_device_get_update_state(dev) != FWUPD_UPDATE_STATE_FAILED && fu_device_get_update_state(dev) != FWUPD_UPDATE_STATE_FAILED_TRANSIENT) { g_autoptr(GString) str = g_string_new("failed to run update on reboot: "); g_info("falling back to generic failure"); fu_device_set_update_state(dev_history, FWUPD_UPDATE_STATE_FAILED); g_string_append_printf(str, "expected %s and got %s", fu_release_get_version(rel_history), fu_device_get_version(dev)); fu_device_set_update_error(dev_history, str->str); } else { fu_device_set_update_state(dev_history, fu_device_get_update_state(dev)); fu_device_set_update_error(dev_history, fu_device_get_update_error(dev)); } /* update the state in the database */ return fu_history_modify_device_release(self->history, dev_history, rel_history, error); } static gboolean fu_engine_update_history_database(FuEngine *self, GError **error) { g_autoptr(GPtrArray) devices = NULL; /* get any devices */ devices = fu_history_get_devices(self->history, error); if (devices == NULL) return FALSE; for (guint i = 0; i < devices->len; i++) { FuDevice *dev = g_ptr_array_index(devices, i); g_autoptr(GError) error_local = NULL; /* not in the required state */ if (fu_device_get_update_state(dev) != FWUPD_UPDATE_STATE_NEEDS_REBOOT && fu_device_get_update_state(dev) != FWUPD_UPDATE_STATE_PENDING) continue; /* try to save the new update-state, but ignoring any error */ if (!fu_engine_update_history_device(self, dev, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_debug("failed to update history database: %s", error_local->message); continue; } g_warning("failed to update history database: %s", error_local->message); } } return TRUE; } static void fu_engine_ensure_client_certificate(FuEngine *self) { g_autoptr(GBytes) blob = g_bytes_new_static(NULL, 0); g_autoptr(GError) error_local = NULL; g_autoptr(JcatBlob) jcat_sig = NULL; g_autoptr(JcatEngine) jcat_engine = NULL; /* create keyring and sign dummy data to ensure certificate exists */ jcat_engine = jcat_context_get_engine(self->jcat_context, JCAT_BLOB_KIND_PKCS7, &error_local); if (jcat_engine == NULL) { g_message("failed to create keyring: %s", error_local->message); return; } jcat_sig = jcat_engine_self_sign(jcat_engine, blob, JCAT_SIGN_FLAG_NONE, &error_local); if (jcat_sig == NULL) { if (g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) { g_info("client certificate now exists: %s", error_local->message); return; } g_message("failed to sign using keyring: %s", error_local->message); return; } g_info("client certificate exists and working"); } static void fu_engine_context_set_battery_threshold(FuContext *ctx) { guint64 minimum_battery; g_autofree gchar *battery_str = NULL; g_autofree gchar *vendor_guid = NULL; g_autofree gchar *vendor = NULL; vendor = fu_context_get_hwid_replace_value(ctx, FU_HWIDS_KEY_MANUFACTURER, NULL); vendor_guid = fwupd_guid_hash_string(vendor); if (vendor_guid != NULL) { battery_str = g_strdup( fu_context_lookup_quirk_by_id(ctx, vendor_guid, FU_QUIRKS_BATTERY_THRESHOLD)); } if (battery_str == NULL) { minimum_battery = MINIMUM_BATTERY_PERCENTAGE_FALLBACK; } else { g_autoptr(GError) error_local = NULL; if (!fu_strtoull(battery_str, &minimum_battery, 0, 100, FU_INTEGER_BASE_AUTO, &error_local)) { g_warning("invalid minimum battery level specified: %s", error_local->message); minimum_battery = MINIMUM_BATTERY_PERCENTAGE_FALLBACK; } } fu_context_set_battery_threshold(ctx, minimum_battery); } static gboolean fu_engine_ensure_paths_exist(GError **error) { FuPathKind path_kinds[] = {FU_PATH_KIND_LOCALSTATEDIR_QUIRKS, FU_PATH_KIND_LOCALSTATEDIR_METADATA, FU_PATH_KIND_LOCALSTATEDIR_REMOTES, FU_PATH_KIND_CACHEDIR_PKG, FU_PATH_KIND_LAST}; for (guint i = 0; path_kinds[i] != FU_PATH_KIND_LAST; i++) { g_autofree gchar *fn = fu_path_from_kind(path_kinds[i]); if (!fu_path_mkdir(fn, error)) return FALSE; } return TRUE; } static void fu_engine_local_metadata_changed_cb(GFileMonitor *monitor, GFile *file, GFile *other_file, GFileMonitorEvent event_type, gpointer user_data) { FuEngine *self = FU_ENGINE(user_data); fu_engine_metadata_changed(self); } static gboolean fu_engine_load_local_metadata_watches(FuEngine *self, GError **error) { const FuPathKind path_kinds[] = {FU_PATH_KIND_DATADIR_PKG, FU_PATH_KIND_LOCALSTATEDIR_PKG}; /* add the watches even if the directory does not exist */ for (guint i = 0; i < G_N_ELEMENTS(path_kinds); i++) { GFileMonitor *monitor; g_autoptr(GFile) file = NULL; g_autoptr(GError) error_local = NULL; g_autofree gchar *base = fu_path_from_kind(path_kinds[i]); g_autofree gchar *fn = g_build_filename(base, "local.d", NULL); file = g_file_new_for_path(fn); monitor = g_file_monitor_directory(file, G_FILE_MONITOR_NONE, NULL, &error_local); if (monitor == NULL) { g_warning("failed to watch %s: %s", fn, error_local->message); continue; } g_signal_connect(monitor, "changed", G_CALLBACK(fu_engine_local_metadata_changed_cb), self); g_ptr_array_add(self->local_monitors, g_steal_pointer(&monitor)); } /* success */ return TRUE; } #ifdef _WIN32 static gchar * fu_engine_win32_registry_get_string(HKEY hkey, const gchar *subkey, const gchar *value, GError **error) { gchar buf[255] = {'\0'}; DWORD bufsz = sizeof(buf); LSTATUS rc; rc = RegGetValue(hkey, subkey, value, RRF_RT_REG_SZ, NULL, (PVOID)&buf, &bufsz); if (rc != ERROR_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to get registry string %s [0x%lX]", subkey, (unsigned long)rc); return NULL; } return g_strndup(buf, bufsz); } #endif static gboolean fu_engine_backends_coldplug_backend_add_devices(FuEngine *self, FuBackend *backend, FuProgress *progress, GError **error) { g_autoptr(GPtrArray) devices = fu_backend_get_devices(backend); /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, devices->len); for (guint i = 0; i < devices->len; i++) { FuDevice *device = g_ptr_array_index(devices, i); g_autoptr(GPtrArray) possible_plugins = NULL; fu_engine_backend_device_added(self, device, fu_progress_get_child(progress)); fu_progress_step_done(progress); /* free data cached during ->probe */ fu_device_probe_complete(device); /* there's no point keeping this in the cache */ possible_plugins = fu_device_get_possible_plugins(device); if (possible_plugins->len == 0) { g_debug("removing %s from backend cache as no possible plugin", fu_device_get_backend_id(device)); fu_backend_device_removed(backend, device); } } /* success */ return TRUE; } static gboolean fu_engine_backends_coldplug_backend(FuEngine *self, FuBackend *backend, FuProgress *progress, GError **error) { /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_NO_PROFILE); fu_progress_set_name(progress, fu_backend_get_name(backend)); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "coldplug"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 99, "add-devices"); /* coldplug */ if (!fu_backend_coldplug(backend, fu_progress_get_child(progress), error)) return FALSE; fu_progress_step_done(progress); /* add */ fu_engine_backends_coldplug_backend_add_devices(self, backend, fu_progress_get_child(progress), error); fu_progress_step_done(progress); /* success */ g_signal_connect(FU_BACKEND(backend), "device-added", G_CALLBACK(fu_engine_backend_device_added_cb), self); g_signal_connect(FU_BACKEND(backend), "device-removed", G_CALLBACK(fu_engine_backend_device_removed_cb), self); g_signal_connect(FU_BACKEND(backend), "device-changed", G_CALLBACK(fu_engine_backend_device_changed_cb), self); return TRUE; } static void fu_engine_backends_coldplug(FuEngine *self, FuProgress *progress) { GPtrArray *backends = fu_context_get_backends(self->ctx); fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, backends->len); for (guint i = 0; i < backends->len; i++) { FuBackend *backend = g_ptr_array_index(backends, i); g_autoptr(GError) error_backend = NULL; if (!fu_backend_get_enabled(backend)) { fu_progress_step_done(progress); continue; } if (!fu_engine_backends_coldplug_backend(self, backend, fu_progress_get_child(progress), &error_backend)) { if (g_error_matches(error_backend, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_debug("ignoring coldplug failure %s: %s", fu_backend_get_name(backend), error_backend->message); } else { g_warning("failed to coldplug backend %s: %s", fu_backend_get_name(backend), error_backend->message); } fu_progress_finished(fu_progress_get_child(progress)); } fu_progress_step_done(progress); } } /** * fu_engine_load: * @self: a #FuEngine * @flags: engine load flags, e.g. %FU_ENGINE_LOAD_FLAG_READONLY * @progress: a #FuProgress * @error: (nullable): optional return location for an error * * Load the firmware update engine so it is ready for use. * * Returns: %TRUE for success **/ gboolean fu_engine_load(FuEngine *self, FuEngineLoadFlags flags, FuProgress *progress, GError **error) { FuPlugin *plugin_uefi; FuQuirksLoadFlags quirks_flags = FU_QUIRKS_LOAD_FLAG_NONE; GPtrArray *backends = fu_context_get_backends(self->ctx); GPtrArray *plugins = fu_plugin_list_get_all(self->plugin_list); const gchar *host_emulate = g_getenv("FWUPD_HOST_EMULATE"); g_autoptr(GPtrArray) checksums_approved = NULL; g_autoptr(GPtrArray) checksums_blocked = NULL; g_autoptr(GError) error_quirks = NULL; g_autoptr(GError) error_json_devices = NULL; g_autoptr(GError) error_local = NULL; g_return_val_if_fail(FU_IS_ENGINE(self), FALSE); g_return_val_if_fail(FU_IS_PROGRESS(progress), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* avoid re-loading a second time if fu-tool or fu-util request to */ if (self->loaded) return TRUE; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_flag(progress, FU_PROGRESS_FLAG_NO_PROFILE); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "read-config"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "read-remotes"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "ensure-client-cert"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "write-db"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "load-plugins"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "load-quirks"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "load-hwinfo"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "load-appstream"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "backend-setup"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "plugins-init"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "hwid-quirks"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "plugins-setup"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 3, "plugins-coldplug"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 90, "backend-coldplug"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "plugins-ready"); fu_progress_add_step(progress, FWUPD_STATUS_LOADING, 1, "update-history-db"); /* sanity check libraries are in sync with daemon */ if (g_strcmp0(fwupd_version_string(), VERSION) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "libfwupd version %s does not match daemon %s", fwupd_version_string(), VERSION); return FALSE; } /* cache machine ID so we can use it from a sandboxed app */ #ifdef _WIN32 self->host_machine_id = fu_engine_win32_registry_get_string(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Cryptography", "MachineGuid", &error_local); #else self->host_machine_id = fu_engine_build_machine_id("fwupd", &error_local); #endif if (self->host_machine_id == NULL) g_info("failed to build machine-id: %s", error_local->message); /* ensure these exist before starting */ if (!fu_engine_ensure_paths_exist(error)) return FALSE; /* read config file */ if (!fu_config_load(FU_CONFIG(self->config), error)) { g_prefix_error(error, "Failed to load config: "); return FALSE; } fu_progress_step_done(progress); /* set the hardcoded ESP */ if (fu_engine_config_get_esp_location(self->config) != NULL) { fu_context_set_esp_location(self->ctx, fu_engine_config_get_esp_location(self->config)); } /* read remotes */ if (flags & FU_ENGINE_LOAD_FLAG_REMOTES) { FuRemoteListLoadFlags remote_list_flags = FU_REMOTE_LIST_LOAD_FLAG_FIX_METADATA_URI; if (fu_engine_config_get_test_devices(self->config)) remote_list_flags |= FU_REMOTE_LIST_LOAD_FLAG_TEST_REMOTE; if (flags & FU_ENGINE_LOAD_FLAG_READONLY) remote_list_flags |= FU_REMOTE_LIST_LOAD_FLAG_READONLY_FS; if (flags & FU_ENGINE_LOAD_FLAG_NO_CACHE) remote_list_flags |= FU_REMOTE_LIST_LOAD_FLAG_NO_CACHE; fu_remote_list_set_lvfs_metadata_format(self->remote_list, FU_LVFS_METADATA_FORMAT); if (!fu_remote_list_load(self->remote_list, remote_list_flags, error)) { g_prefix_error(error, "Failed to load remotes: "); return FALSE; } } fu_progress_step_done(progress); /* create client certificate */ if (flags & FU_ENGINE_LOAD_FLAG_ENSURE_CLIENT_CERT) fu_engine_ensure_client_certificate(self); fu_progress_step_done(progress); /* get hardcoded approved and blocked firmware */ checksums_approved = fu_engine_config_get_approved_firmware(self->config); for (guint i = 0; i < checksums_approved->len; i++) { const gchar *csum = g_ptr_array_index(checksums_approved, i); fu_engine_add_approved_firmware(self, csum); } checksums_blocked = fu_engine_config_get_blocked_firmware(self->config); for (guint i = 0; i < checksums_blocked->len; i++) { const gchar *csum = g_ptr_array_index(checksums_blocked, i); fu_engine_add_blocked_firmware(self, csum); } /* get extra firmware saved to the database */ checksums_approved = fu_history_get_approved_firmware(self->history, error); if (checksums_approved == NULL) return FALSE; for (guint i = 0; i < checksums_approved->len; i++) { const gchar *csum = g_ptr_array_index(checksums_approved, i); fu_engine_add_approved_firmware(self, csum); } checksums_blocked = fu_history_get_blocked_firmware(self->history, error); if (checksums_blocked == NULL) return FALSE; for (guint i = 0; i < checksums_blocked->len; i++) { const gchar *csum = g_ptr_array_index(checksums_blocked, i); fu_engine_add_blocked_firmware(self, csum); } fu_progress_step_done(progress); /* load plugins early, as we have to call ->load() *before* building quirk silo */ if (!fu_engine_load_plugins(self, flags, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to load plugins: "); return FALSE; } fu_progress_step_done(progress); /* migrate per-plugin settings into fwupd.conf */ plugin_uefi = fu_plugin_list_find_by_name(self->plugin_list, "uefi_capsule", NULL); if (plugin_uefi != NULL) { const gchar *tmp = fu_plugin_get_config_value(plugin_uefi, "OverrideESPMountPoint"); if (tmp != NULL && g_strcmp0(tmp, fu_engine_config_get_esp_location(self->config)) != 0) { g_info("migrating OverrideESPMountPoint=%s to EspLocation", tmp); if (!fu_config_set_value(FU_CONFIG(self->config), "fwupd", "EspLocation", tmp, error)) return FALSE; } } /* set up idle exit */ if ((flags & FU_ENGINE_LOAD_FLAG_NO_IDLE_SOURCES) == 0) fu_idle_set_timeout(self->idle, fu_engine_config_get_idle_timeout(self->config)); /* on a read-only filesystem don't care about the cache GUID */ if (flags & FU_ENGINE_LOAD_FLAG_READONLY) quirks_flags |= FU_QUIRKS_LOAD_FLAG_READONLY_FS; if (flags & FU_ENGINE_LOAD_FLAG_NO_CACHE) quirks_flags |= FU_QUIRKS_LOAD_FLAG_NO_CACHE; if (!fu_context_load_quirks(self->ctx, quirks_flags, &error_quirks)) g_warning("Failed to load quirks: %s", error_quirks->message); fu_progress_step_done(progress); /* do not mount disks if only loading readonly */ if (flags & FU_ENGINE_LOAD_FLAG_READONLY) fu_context_add_flag(self->ctx, FU_CONTEXT_FLAG_INHIBIT_VOLUME_MOUNT); /* load SMBIOS and the hwids */ if (flags & FU_ENGINE_LOAD_FLAG_HWINFO) { if (!fu_context_load_hwinfo(self->ctx, fu_progress_get_child(progress), FU_CONTEXT_HWID_FLAG_LOAD_ALL, error)) return FALSE; } fu_progress_step_done(progress); /* load AppStream metadata */ if (!fu_engine_load_metadata_store(self, flags, error)) { g_prefix_error(error, "Failed to load AppStream data: "); return FALSE; } fu_progress_step_done(progress); /* watch the local.d directories for changes */ if (!fu_engine_load_local_metadata_watches(self, error)) return FALSE; /* add the "built-in" firmware types */ fu_context_add_firmware_gtype(self->ctx, "raw", FU_TYPE_FIRMWARE); fu_context_add_firmware_gtype(self->ctx, "cab", FU_TYPE_CAB_FIRMWARE); fu_context_add_firmware_gtype(self->ctx, "dfu", FU_TYPE_DFU_FIRMWARE); fu_context_add_firmware_gtype(self->ctx, "fdt", FU_TYPE_FDT_FIRMWARE); fu_context_add_firmware_gtype(self->ctx, "csv", FU_TYPE_CSV_FIRMWARE); fu_context_add_firmware_gtype(self->ctx, "fit", FU_TYPE_FIT_FIRMWARE); fu_context_add_firmware_gtype(self->ctx, "dfuse", FU_TYPE_DFUSE_FIRMWARE); fu_context_add_firmware_gtype(self->ctx, "ifwi-cpd", FU_TYPE_IFWI_CPD_FIRMWARE); fu_context_add_firmware_gtype(self->ctx, "ifwi-fpt", FU_TYPE_IFWI_FPT_FIRMWARE); fu_context_add_firmware_gtype(self->ctx, "oprom", FU_TYPE_OPROM_FIRMWARE); fu_context_add_firmware_gtype(self->ctx, "fmap", FU_TYPE_FMAP_FIRMWARE); fu_context_add_firmware_gtype(self->ctx, "ihex", FU_TYPE_IHEX_FIRMWARE); fu_context_add_firmware_gtype(self->ctx, "linear", FU_TYPE_LINEAR_FIRMWARE); fu_context_add_firmware_gtype(self->ctx, "srec", FU_TYPE_SREC_FIRMWARE); fu_context_add_firmware_gtype(self->ctx, "hid-descriptor", FU_TYPE_HID_DESCRIPTOR); fu_context_add_firmware_gtype(self->ctx, "archive", FU_TYPE_ARCHIVE_FIRMWARE); fu_context_add_firmware_gtype(self->ctx, "smbios", FU_TYPE_SMBIOS); fu_context_add_firmware_gtype(self->ctx, "acpi-table", FU_TYPE_ACPI_TABLE); fu_context_add_firmware_gtype(self->ctx, "sbatlevel", FU_TYPE_SBATLEVEL_SECTION); fu_context_add_firmware_gtype(self->ctx, "edid", FU_TYPE_EDID); fu_context_add_firmware_gtype(self->ctx, "efi-file", FU_TYPE_EFI_FILE); fu_context_add_firmware_gtype(self->ctx, "efi-signature", FU_TYPE_EFI_SIGNATURE); fu_context_add_firmware_gtype(self->ctx, "efi-signature-list", FU_TYPE_EFI_SIGNATURE_LIST); fu_context_add_firmware_gtype(self->ctx, "efi-variable-authentication2", FU_TYPE_EFI_VARIABLE_AUTHENTICATION2); fu_context_add_firmware_gtype(self->ctx, "efi-load-option", FU_TYPE_EFI_LOAD_OPTION); fu_context_add_firmware_gtype(self->ctx, "efi-device-path-list", FU_TYPE_EFI_DEVICE_PATH_LIST); fu_context_add_firmware_gtype(self->ctx, "efi-filesystem", FU_TYPE_EFI_FILESYSTEM); fu_context_add_firmware_gtype(self->ctx, "efi-section", FU_TYPE_EFI_SECTION); fu_context_add_firmware_gtype(self->ctx, "efi-volume", FU_TYPE_EFI_VOLUME); fu_context_add_firmware_gtype(self->ctx, "ifd-bios", FU_TYPE_IFD_BIOS); fu_context_add_firmware_gtype(self->ctx, "ifd-firmware", FU_TYPE_IFD_FIRMWARE); fu_context_add_firmware_gtype(self->ctx, "cfu-offer", FU_TYPE_CFU_OFFER); fu_context_add_firmware_gtype(self->ctx, "cfu-payload", FU_TYPE_CFU_PAYLOAD); fu_context_add_firmware_gtype(self->ctx, "uswid", FU_TYPE_USWID_FIRMWARE); fu_context_add_firmware_gtype(self->ctx, "coswid", FU_TYPE_COSWID_FIRMWARE); fu_context_add_firmware_gtype(self->ctx, "pefile", FU_TYPE_PEFILE_FIRMWARE); fu_context_add_firmware_gtype(self->ctx, "elf", FU_TYPE_ELF_FIRMWARE); fu_context_add_firmware_gtype(self->ctx, "intel-thunderbolt", FU_TYPE_INTEL_THUNDERBOLT_FIRMWARE); fu_context_add_firmware_gtype(self->ctx, "intel-thunderbolt-nvm", FU_TYPE_INTEL_THUNDERBOLT_NVM); fu_context_add_firmware_gtype(self->ctx, "usb-device-fw-ds20", FU_TYPE_USB_DEVICE_FW_DS20); fu_context_add_firmware_gtype(self->ctx, "usb-device-ms-ds20", FU_TYPE_USB_DEVICE_MS_DS20); /* we are emulating a different host */ if (host_emulate != NULL) { g_autofree gchar *fn = NULL; /* did the user specify an absolute path */ if (g_file_test(host_emulate, G_FILE_TEST_EXISTS)) { fn = g_strdup(host_emulate); } else { g_autofree gchar *datadir = fu_path_from_kind(FU_PATH_KIND_DATADIR_PKG); fn = g_build_filename(datadir, "host-emulate.d", host_emulate, NULL); } if (!fu_engine_load_host_emulation(self, fn, error)) { g_prefix_error(error, "failed to load emulated host: "); return FALSE; } /* do not load actual hardware */ flags &= ~FU_ENGINE_LOAD_FLAG_COLDPLUG; self->host_emulation = TRUE; } /* set up backends */ if (flags & FU_ENGINE_LOAD_FLAG_COLDPLUG) { FuBackendSetupFlags backend_flags = FU_BACKEND_SETUP_FLAG_NONE; if (flags & FU_ENGINE_LOAD_FLAG_DEVICE_HOTPLUG) backend_flags |= FU_BACKEND_SETUP_FLAG_USE_HOTPLUG; for (guint i = 0; i < backends->len; i++) { FuBackend *backend = g_ptr_array_index(backends, i); g_autoptr(GError) error_backend = NULL; if (!fu_backend_setup(backend, backend_flags, fu_progress_get_child(progress), &error_backend)) { g_info("failed to setup backend %s: %s", fu_backend_get_name(backend), error_backend->message); continue; } } } fu_progress_step_done(progress); /* delete old data files */ if (!fu_engine_cleanup_state(error)) { g_prefix_error(error, "Failed to clean up: "); return FALSE; } /* init plugins, adding device and firmware GTypes */ if (!fu_engine_plugins_init(self, fu_progress_get_child(progress), error)) { g_prefix_error(error, "failed to init plugins: "); return FALSE; } fu_progress_step_done(progress); /* set quirks for each hwid */ if (fu_context_has_flag(self->ctx, FU_CONTEXT_FLAG_LOADED_HWINFO)) { GPtrArray *guids = fu_context_get_hwid_guids(self->ctx); for (guint i = 0; i < guids->len; i++) { const gchar *hwid = g_ptr_array_index(guids, i); fu_engine_load_quirks_for_hwid(self, hwid); } } fu_progress_step_done(progress); /* set up battery threshold */ if (fu_context_has_flag(self->ctx, FU_CONTEXT_FLAG_LOADED_HWINFO)) fu_engine_context_set_battery_threshold(self->ctx); /* watch the device list for updates and proxy */ g_signal_connect(FU_DEVICE_LIST(self->device_list), "added", G_CALLBACK(fu_engine_device_added_cb), self); g_signal_connect(FU_DEVICE_LIST(self->device_list), "removed", G_CALLBACK(fu_engine_device_removed_cb), self); g_signal_connect(FU_DEVICE_LIST(self->device_list), "changed", G_CALLBACK(fu_engine_device_changed_cb), self); fu_engine_set_status(self, FWUPD_STATUS_LOADING); /* add devices */ if (flags & FU_ENGINE_LOAD_FLAG_COLDPLUG) { fu_engine_ensure_context_flag_save_events(self); fu_engine_plugins_startup(self, fu_progress_get_child(progress)); fu_progress_step_done(progress); fu_engine_plugins_coldplug(self, fu_progress_get_child(progress)); fu_progress_step_done(progress); } else { fu_progress_step_done(progress); fu_progress_step_done(progress); } /* coldplug backends */ if (flags & FU_ENGINE_LOAD_FLAG_COLDPLUG) fu_engine_backends_coldplug(self, fu_progress_get_child(progress)); fu_progress_step_done(progress); /* coldplug done, so plugin is ready */ if (flags & FU_ENGINE_LOAD_FLAG_COLDPLUG) { fu_engine_plugins_ready(self, fu_progress_get_child(progress)); fu_progress_step_done(progress); } else { fu_progress_step_done(progress); } /* dump plugin information to the console */ if (g_getenv("FWUPD_VERBOSE") != NULL) { g_autoptr(GString) str = g_string_new(NULL); for (guint i = 0; i < backends->len; i++) { FuBackend *backend = g_ptr_array_index(backends, i); fu_backend_add_string(backend, 0, str); } for (guint i = 0; i < plugins->len; i++) { FuPlugin *plugin = g_ptr_array_index(plugins, i); if (fu_plugin_has_flag(plugin, FWUPD_PLUGIN_FLAG_DISABLED)) continue; fu_plugin_add_string(plugin, 0, str); } g_info("%s", str->str); } /* update the db for devices that were updated during the reboot */ if (!fu_engine_update_history_database(self, error)) return FALSE; fu_progress_step_done(progress); /* update the devices JSON file */ if (!fu_engine_update_devices_file(self, &error_json_devices)) g_info("failed to update list of devices: %s", error_json_devices->message); fu_engine_set_status(self, FWUPD_STATUS_IDLE); self->loaded = TRUE; /* let clients know engine finished starting up */ fu_engine_emit_changed(self); /* success */ return TRUE; } static void fu_engine_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { FuEngine *self = FU_ENGINE(object); switch (prop_id) { case PROP_CONTEXT: g_value_set_object(value, self->ctx); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_engine_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { FuEngine *self = FU_ENGINE(object); switch (prop_id) { case PROP_CONTEXT: g_set_object(&self->ctx, g_value_get_object(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; } } static void fu_engine_dispose(GObject *obj) { FuEngine *self = FU_ENGINE(obj); if (self->plugin_list != NULL) { GPtrArray *plugins = fu_plugin_list_get_all(self->plugin_list); for (guint i = 0; i < plugins->len; i++) { FuPlugin *plugin = g_ptr_array_index(plugins, i); g_signal_handlers_disconnect_by_data(plugin, self); } fu_plugin_list_remove_all(self->plugin_list); } if (self->device_list != NULL) fu_device_list_remove_all(self->device_list); if (self->config != NULL) g_signal_handlers_disconnect_by_data(self->config, self); if (self->ctx != NULL) { GPtrArray *backends = fu_context_get_backends(self->ctx); for (guint i = 0; i < backends->len; i++) { FuBackend *backend = g_ptr_array_index(backends, i); g_signal_handlers_disconnect_by_data(backend, self); } g_ptr_array_set_size(backends, 0); g_signal_handlers_disconnect_by_data(self->ctx, self); } g_clear_object(&self->ctx); G_OBJECT_CLASS(fu_engine_parent_class)->dispose(obj); } static void fu_engine_class_init(FuEngineClass *klass) { GParamSpec *pspec; GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->dispose = fu_engine_dispose; object_class->finalize = fu_engine_finalize; object_class->get_property = fu_engine_get_property; object_class->set_property = fu_engine_set_property; object_class->constructed = fu_engine_constructed; pspec = g_param_spec_object("context", NULL, NULL, FU_TYPE_CONTEXT, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_NAME); g_object_class_install_property(object_class, PROP_CONTEXT, pspec); /** * FuEngine::changed: * @self: the #FuEngine instance that emitted the signal * * The ::changed signal is emitted when the engine has changed, for instance when a device * state has been modified. **/ signals[SIGNAL_CHANGED] = g_signal_new("changed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); /** * FuEngine::device-added: * @self: the #FuEngine instance that emitted the signal * @device: the #FuDevice * * The ::device-added signal is emitted when a device has been added. **/ signals[SIGNAL_DEVICE_ADDED] = g_signal_new("device-added", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, FU_TYPE_DEVICE); /** * FuEngine::device-removed: * @self: the #FuEngine instance that emitted the signal * @device: the #FuDevice * * The ::device-removed signal is emitted when a device has been removed. **/ signals[SIGNAL_DEVICE_REMOVED] = g_signal_new("device-removed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, FU_TYPE_DEVICE); /** * FuEngine::device-changed: * @self: the #FuEngine instance that emitted the signal * @device: the #FuDevice * * The ::device-changed signal is emitted when a device has been changed. **/ signals[SIGNAL_DEVICE_CHANGED] = g_signal_new("device-changed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, FU_TYPE_DEVICE); /** * FuEngine::device-request: * @self: the #FuEngine instance that emitted the signal * @request: the #FwupdRequest * * The ::device-request signal is emitted when the engine has asked the front end for an * interactive request. **/ signals[SIGNAL_DEVICE_REQUEST] = g_signal_new("device-request", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, FWUPD_TYPE_REQUEST); /** * FuEngine::status-changed: * @self: the #FuEngine instance that emitted the signal * @status: the #FwupdStatus * * The ::status-changed signal is emitted when the daemon global status has changed. **/ signals[SIGNAL_STATUS_CHANGED] = g_signal_new("status-changed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__UINT, G_TYPE_NONE, 1, G_TYPE_UINT); } void fu_engine_add_runtime_version(FuEngine *self, const gchar *component_id, const gchar *version) { fu_context_add_runtime_version(self->ctx, component_id, version); } static void fu_engine_context_power_changed(FuEngine *self) { g_autoptr(GPtrArray) devices = fu_device_list_get_active(self->device_list); /* apply policy on any existing devices */ for (guint i = 0; i < devices->len; i++) { FuDevice *device = g_ptr_array_index(devices, i); fu_engine_ensure_device_power_inhibit(self, device); fu_engine_ensure_device_lid_inhibit(self, device); fu_engine_ensure_device_display_required_inhibit(self, device); fu_engine_ensure_device_system_inhibit(self, device); } } static void fu_engine_context_power_changed_cb(FuContext *ctx, GParamSpec *pspec, FuEngine *self) { if (fu_idle_has_inhibit(self->idle, FU_IDLE_INHIBIT_SIGNALS)) { g_debug("suppressing ::power-changed as transaction is in progress"); return; } fu_engine_context_power_changed(self); } static void fu_engine_idle_timeout_cb(FuIdle *idle, FuEngine *self) { fu_engine_set_status(self, FWUPD_STATUS_SHUTDOWN); } static void fu_engine_idle_inhibit_changed_cb(FuIdle *idle, GParamSpec *pspec, FuEngine *self) { if (!fu_idle_has_inhibit(idle, FU_IDLE_INHIBIT_SIGNALS) && g_hash_table_size(self->device_changed_allowlist) > 0) { g_debug("clearing device-changed allowlist as transaction done"); g_hash_table_remove_all(self->device_changed_allowlist); /* we might have suppressed this during the transaction, so ensure all the device * inhibits are being set up correctly */ fu_engine_context_power_changed(self); } } static void fu_engine_constructed(GObject *obj) { FuEngine *self = FU_ENGINE(obj); #ifdef HAVE_UTSNAME_H struct utsname uname_tmp; #endif g_autofree gchar *keyring_path = NULL; g_autofree gchar *pkidir_fw = NULL; g_autofree gchar *pkidir_md = NULL; g_autofree gchar *sysconfdir = NULL; g_signal_connect(FU_CONTEXT(self->ctx), "security-changed", G_CALLBACK(fu_engine_context_security_changed_cb), self); g_signal_connect(FU_CONTEXT(self->ctx), "notify::power-state", G_CALLBACK(fu_engine_context_power_changed_cb), self); g_signal_connect(FU_CONTEXT(self->ctx), "notify::lid-state", G_CALLBACK(fu_engine_context_power_changed_cb), self); g_signal_connect(FU_CONTEXT(self->ctx), "notify::display-state", G_CALLBACK(fu_engine_context_power_changed_cb), self); g_signal_connect(FU_CONTEXT(self->ctx), "notify::battery-level", G_CALLBACK(fu_engine_context_power_changed_cb), self); g_signal_connect(FU_CONTEXT(self->ctx), "notify::battery-threshold", G_CALLBACK(fu_engine_context_power_changed_cb), self); g_signal_connect(FU_CONTEXT(self->ctx), "notify::flags", G_CALLBACK(fu_engine_context_power_changed_cb), self); g_signal_connect(FU_CONFIG(self->config), "changed", G_CALLBACK(fu_engine_config_changed_cb), self); g_signal_connect(FU_REMOTE_LIST(self->remote_list), "changed", G_CALLBACK(fu_engine_remote_list_changed_cb), self); g_signal_connect(FU_REMOTE_LIST(self->remote_list), "added", G_CALLBACK(fu_engine_remote_list_added_cb), self); g_signal_connect(FU_IDLE(self->idle), "inhibit-changed", G_CALLBACK(fu_engine_idle_inhibit_changed_cb), self); g_signal_connect(FU_IDLE(self->idle), "timeout", G_CALLBACK(fu_engine_idle_timeout_cb), self); /* backends */ { g_autoptr(FuBackend) backend = fu_usb_backend_new(self->ctx); fu_context_add_backend(self->ctx, backend); } { g_autoptr(FuBackend) backend = fu_uefi_backend_new(self->ctx); fu_context_add_backend(self->ctx, backend); } #ifdef HAVE_UDEV { g_autoptr(FuBackend) backend = fu_udev_backend_new(self->ctx); fu_context_add_backend(self->ctx, backend); } #endif #ifdef HAVE_BLUEZ { g_autoptr(FuBackend) backend = fu_bluez_backend_new(self->ctx); fu_context_add_backend(self->ctx, backend); } #endif self->history = fu_history_new(self->ctx); self->emulation = fu_engine_emulator_new(self); /* setup Jcat context */ self->jcat_context = jcat_context_new(); jcat_context_blob_kind_allow(self->jcat_context, JCAT_BLOB_KIND_SHA256); jcat_context_blob_kind_allow(self->jcat_context, JCAT_BLOB_KIND_SHA512); jcat_context_blob_kind_allow(self->jcat_context, JCAT_BLOB_KIND_PKCS7); jcat_context_blob_kind_allow(self->jcat_context, JCAT_BLOB_KIND_GPG); keyring_path = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR_PKG); jcat_context_set_keyring_path(self->jcat_context, keyring_path); sysconfdir = fu_path_from_kind(FU_PATH_KIND_SYSCONFDIR); pkidir_fw = g_build_filename(sysconfdir, "pki", "fwupd", NULL); jcat_context_add_public_keys(self->jcat_context, pkidir_fw); pkidir_md = g_build_filename(sysconfdir, "pki", "fwupd-metadata", NULL); jcat_context_add_public_keys(self->jcat_context, pkidir_md); /* add some runtime versions of things the daemon depends on */ fu_engine_add_runtime_version(self, "org.freedesktop.fwupd", VERSION); fu_engine_add_runtime_version(self, "com.hughsie.libjcat", jcat_version_string()); fu_engine_add_runtime_version(self, "com.hughsie.libxmlb", xb_version_string()); /* optional kernel version */ #ifdef HAVE_UTSNAME_H memset(&uname_tmp, 0, sizeof(uname_tmp)); if (uname(&uname_tmp) >= 0) fu_engine_add_runtime_version(self, "org.kernel", uname_tmp.release); #endif fu_context_add_compile_version(self->ctx, "org.freedesktop.fwupd", VERSION); #ifdef SOURCE_VERSION if (g_strcmp0(SOURCE_VERSION, VERSION) != 0) fu_context_add_compile_version(self->ctx, "org.freedesktop.fwupd.source", SOURCE_VERSION); #endif fu_context_add_compile_version(self->ctx, "info.libusb", LIBUSB_VERSION); #ifdef HAVE_PASSIM { g_autofree gchar *version = g_strdup_printf("%i.%i.%i", PASSIM_MAJOR_VERSION, PASSIM_MINOR_VERSION, PASSIM_MICRO_VERSION); fu_context_add_compile_version(self->ctx, "org.freedesktop.Passim", version); } #endif { g_autofree gchar *version = g_strdup_printf("%i.%i.%i", JCAT_MAJOR_VERSION, JCAT_MINOR_VERSION, JCAT_MICRO_VERSION); fu_context_add_compile_version(self->ctx, "com.hughsie.libjcat", version); } { g_autofree gchar *version = g_strdup_printf("%i.%i.%i", XMLB_MAJOR_VERSION, XMLB_MINOR_VERSION, XMLB_MICRO_VERSION); fu_context_add_compile_version(self->ctx, "com.hughsie.libxmlb", version); } /* add optional snap version */ if (g_getenv("SNAP_REVISION") != NULL) { fu_context_add_compile_version(self->ctx, "io.snapcraft.fwupd", g_getenv("SNAP_REVISION")); } } static void fu_engine_init(FuEngine *self) { self->percentage = 0; self->config = fu_engine_config_new(); self->remote_list = fu_remote_list_new(); self->device_list = fu_device_list_new(); self->idle = fu_idle_new(); self->plugin_list = fu_plugin_list_new(); self->plugin_filter = g_ptr_array_new_with_free_func(g_free); self->host_security_attrs = fu_security_attrs_new(); self->local_monitors = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); self->acquiesce_loop = g_main_loop_new(NULL, FALSE); self->device_changed_allowlist = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); #ifdef HAVE_PASSIM self->passim_client = passim_client_new(); #endif /* register /org/freedesktop/fwupd globally */ g_resources_register(fu_get_resource()); } static void fu_engine_finalize(GObject *obj) { FuEngine *self = FU_ENGINE(obj); for (guint i = 0; i < self->local_monitors->len; i++) { GFileMonitor *monitor = g_ptr_array_index(self->local_monitors, i); g_file_monitor_cancel(monitor); } if (self->silo != NULL) g_object_unref(self->silo); if (self->query_component_by_guid != NULL) g_object_unref(self->query_component_by_guid); if (self->query_container_checksum1 != NULL) g_object_unref(self->query_container_checksum1); if (self->query_container_checksum2 != NULL) g_object_unref(self->query_container_checksum2); if (self->query_tag_by_guid_version != NULL) g_object_unref(self->query_tag_by_guid_version); if (self->coldplug_id != 0) g_source_remove(self->coldplug_id); if (self->approved_firmware != NULL) g_hash_table_unref(self->approved_firmware); if (self->blocked_firmware != NULL) g_hash_table_unref(self->blocked_firmware); if (self->acquiesce_id != 0) g_source_remove(self->acquiesce_id); if (self->update_motd_id != 0) g_source_remove(self->update_motd_id); if (self->emulation != NULL) g_object_unref(self->emulation); #ifdef HAVE_PASSIM if (self->passim_client != NULL) g_object_unref(self->passim_client); #endif g_main_loop_unref(self->acquiesce_loop); g_free(self->host_machine_id); g_object_unref(self->host_security_attrs); g_object_unref(self->idle); g_object_unref(self->config); g_object_unref(self->remote_list); g_object_unref(self->history); g_object_unref(self->device_list); g_object_unref(self->jcat_context); g_ptr_array_unref(self->plugin_filter); g_ptr_array_unref(self->local_monitors); g_hash_table_unref(self->device_changed_allowlist); g_object_unref(self->plugin_list); G_OBJECT_CLASS(fu_engine_parent_class)->finalize(obj); } FuEngine * fu_engine_new(FuContext *ctx) { return FU_ENGINE(g_object_new(FU_TYPE_ENGINE, "context", ctx, NULL)); } fwupd-2.0.10/src/fu-engine.h000066400000000000000000000243771501337203100155440ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include #include "fwupd-device.h" #include "fwupd-enums.h" #include "fu-cabinet.h" #include "fu-engine-config.h" #include "fu-release.h" #define FU_TYPE_ENGINE (fu_engine_get_type()) G_DECLARE_FINAL_TYPE(FuEngine, fu_engine, FU, ENGINE, GObject) /** * FuEngineLoadFlags: * @FU_ENGINE_LOAD_FLAG_NONE: No flags set * @FU_ENGINE_LOAD_FLAG_READONLY: Ignore readonly filesystem errors * @FU_ENGINE_LOAD_FLAG_COLDPLUG: Enumerate devices * @FU_ENGINE_LOAD_FLAG_REMOTES: Enumerate remotes * @FU_ENGINE_LOAD_FLAG_HWINFO: Load details about the hardware * @FU_ENGINE_LOAD_FLAG_NO_CACHE: Do not save persistent xmlb silos * @FU_ENGINE_LOAD_FLAG_NO_IDLE_SOURCES:Do not load idle sources * @FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS: Load built-in plugins * @FU_ENGINE_LOAD_FLAG_ENSURE_CLIENT_CERT: Ensure the client certificate exists * @FU_ENGINE_LOAD_FLAG_EXTERNAL_PLUGINS: Load external dload'ed plugins such as flashrom * @FU_ENGINE_LOAD_FLAG_DEVICE_HOTPLUG: Set up device hotplug * * The flags to use when loading the engine. **/ typedef enum { FU_ENGINE_LOAD_FLAG_NONE = 0, FU_ENGINE_LOAD_FLAG_READONLY = 1 << 0, FU_ENGINE_LOAD_FLAG_COLDPLUG = 1 << 1, FU_ENGINE_LOAD_FLAG_REMOTES = 1 << 2, FU_ENGINE_LOAD_FLAG_HWINFO = 1 << 3, FU_ENGINE_LOAD_FLAG_NO_CACHE = 1 << 4, FU_ENGINE_LOAD_FLAG_NO_IDLE_SOURCES = 1 << 5, FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS = 1 << 6, FU_ENGINE_LOAD_FLAG_ENSURE_CLIENT_CERT = 1 << 7, FU_ENGINE_LOAD_FLAG_EXTERNAL_PLUGINS = 1 << 8, FU_ENGINE_LOAD_FLAG_DEVICE_HOTPLUG = 1 << 9, /*< private >*/ FU_ENGINE_LOAD_FLAG_LAST } FuEngineLoadFlags; FuEngine * fu_engine_new(FuContext *ctx) G_GNUC_NON_NULL(1); gboolean fu_engine_get_loaded(FuEngine *self) G_GNUC_NON_NULL(1); void fu_engine_add_plugin_filter(FuEngine *self, const gchar *plugin_glob) G_GNUC_NON_NULL(1, 2); void fu_engine_idle_reset(FuEngine *self) G_GNUC_NON_NULL(1); guint32 fu_engine_idle_inhibit(FuEngine *self, FuIdleInhibit inhibit, const gchar *reason) G_GNUC_NON_NULL(1); void fu_engine_idle_uninhibit(FuEngine *self, guint32 token) G_GNUC_NON_NULL(1); gboolean fu_engine_load(FuEngine *self, FuEngineLoadFlags flags, FuProgress *progress, GError **error) G_GNUC_NON_NULL(1, 3); const gchar * fu_engine_get_host_vendor(FuEngine *self) G_GNUC_NON_NULL(1); const gchar * fu_engine_get_host_product(FuEngine *self) G_GNUC_NON_NULL(1); const gchar * fu_engine_get_host_machine_id(FuEngine *self) G_GNUC_NON_NULL(1); const gchar * fu_engine_get_host_bkc(FuEngine *self) G_GNUC_NON_NULL(1); gboolean fu_engine_is_uid_trusted(FuEngine *self, guint64 calling_uid) G_GNUC_NON_NULL(1); gchar * fu_engine_get_host_security_id(FuEngine *self, const gchar *fwupd_version) G_GNUC_NON_NULL(1); FuCabinet * fu_engine_build_cabinet_from_stream(FuEngine *self, GInputStream *stream, GError **error) G_GNUC_NON_NULL(1, 2); FuEngineConfig * fu_engine_get_config(FuEngine *self) G_GNUC_NON_NULL(1); GPtrArray * fu_engine_get_plugins(FuEngine *self) G_GNUC_NON_NULL(1); FuPlugin * fu_engine_get_plugin_by_name(FuEngine *self, const gchar *name, GError **error) G_GNUC_NON_NULL(1, 2); GPtrArray * fu_engine_get_devices(FuEngine *self, GError **error) G_GNUC_NON_NULL(1); FuDevice * fu_engine_get_device(FuEngine *self, const gchar *device_id, GError **error) G_GNUC_NON_NULL(1, 2); GPtrArray * fu_engine_get_devices_by_guid(FuEngine *self, const gchar *guid, GError **error) G_GNUC_NON_NULL(1, 2); GPtrArray * fu_engine_get_devices_by_composite_id(FuEngine *self, const gchar *composite_id, GError **error) G_GNUC_NON_NULL(1, 2); GPtrArray * fu_engine_get_history(FuEngine *self, GError **error) G_GNUC_NON_NULL(1); FwupdRemote * fu_engine_get_remote_by_id(FuEngine *self, const gchar *remote_id, GError **error) G_GNUC_NON_NULL(1, 2); GPtrArray * fu_engine_get_remotes(FuEngine *self, GError **error) G_GNUC_NON_NULL(1); GPtrArray * fu_engine_get_releases(FuEngine *self, FuEngineRequest *request, const gchar *device_id, GError **error) G_GNUC_NON_NULL(1, 2, 3); GPtrArray * fu_engine_get_downgrades(FuEngine *self, FuEngineRequest *request, const gchar *device_id, GError **error) G_GNUC_NON_NULL(1, 2, 3); GPtrArray * fu_engine_get_upgrades(FuEngine *self, FuEngineRequest *request, const gchar *device_id, GError **error) G_GNUC_NON_NULL(1, 2, 3); FwupdDevice * fu_engine_get_results(FuEngine *self, const gchar *device_id, GError **error) G_GNUC_NON_NULL(1, 2); FuSecurityAttrs * fu_engine_get_host_security_attrs(FuEngine *self) G_GNUC_NON_NULL(1); FuSecurityAttrs * fu_engine_get_host_security_events(FuEngine *self, guint limit, GError **error) G_GNUC_NON_NULL(1); GHashTable * fu_engine_get_report_metadata(FuEngine *self, GError **error) G_GNUC_NON_NULL(1); gboolean fu_engine_clear_results(FuEngine *self, const gchar *device_id, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_engine_update_metadata(FuEngine *self, const gchar *remote_id, gint fd, gint fd_sig, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_engine_update_metadata_bytes(FuEngine *self, const gchar *remote_id, GBytes *bytes_raw, GBytes *bytes_sig, GError **error) G_GNUC_NON_NULL(1, 2, 3); gboolean fu_engine_unlock(FuEngine *self, const gchar *device_id, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_engine_verify(FuEngine *self, const gchar *device_id, FuProgress *progress, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_engine_verify_update(FuEngine *self, const gchar *device_id, FuProgress *progress, GError **error) G_GNUC_NON_NULL(1, 2, 3); GBytes * fu_engine_firmware_dump(FuEngine *self, FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) G_GNUC_NON_NULL(1, 2, 3); FuFirmware * fu_engine_firmware_read(FuEngine *self, FuDevice *device, FuProgress *progress, FwupdInstallFlags flags, GError **error) G_GNUC_NON_NULL(1, 2, 3); gboolean fu_engine_modify_remote(FuEngine *self, const gchar *remote_id, const gchar *key, const gchar *value, GError **error) G_GNUC_NON_NULL(1, 2, 3, 4); gboolean fu_engine_modify_device(FuEngine *self, const gchar *device_id, const gchar *key, const gchar *value, GError **error) G_GNUC_NON_NULL(1, 2, 3, 4); gboolean fu_engine_composite_prepare(FuEngine *self, GPtrArray *devices, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_engine_composite_cleanup(FuEngine *self, GPtrArray *devices, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_engine_install_release(FuEngine *self, FuRelease *release, GInputStream *stream, FuProgress *progress, FwupdInstallFlags flags, GError **error) G_GNUC_NON_NULL(1, 2, 3, 4); gboolean fu_engine_install_blob(FuEngine *self, FuDevice *device, GInputStream *stream_fw, FuProgress *progress, FwupdInstallFlags flags, FwupdFeatureFlags feature_flags, GError **error) G_GNUC_NON_NULL(1, 2, 3, 4); gboolean fu_engine_install_releases(FuEngine *self, FuEngineRequest *request, GPtrArray *releases, FuCabinet *cabinet, FuProgress *progress, FwupdInstallFlags flags, GError **error) G_GNUC_NON_NULL(1, 2, 3, 4, 5); gboolean fu_engine_activate(FuEngine *self, const gchar *device_id, FuProgress *progress, GError **error) G_GNUC_NON_NULL(1, 2, 3); GPtrArray * fu_engine_get_approved_firmware(FuEngine *self) G_GNUC_NON_NULL(1); void fu_engine_add_approved_firmware(FuEngine *self, const gchar *checksum) G_GNUC_NON_NULL(1, 2); void fu_engine_set_approved_firmware(FuEngine *self, GPtrArray *checksums) G_GNUC_NON_NULL(1, 2); GPtrArray * fu_engine_get_blocked_firmware(FuEngine *self) G_GNUC_NON_NULL(1); gboolean fu_engine_set_blocked_firmware(FuEngine *self, GPtrArray *checksums, GError **error) G_GNUC_NON_NULL(1, 2); gchar * fu_engine_self_sign(FuEngine *self, const gchar *value, JcatSignFlags flags, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_engine_modify_config(FuEngine *self, const gchar *section, const gchar *key, const gchar *value, GError **error) G_GNUC_NON_NULL(1, 2, 3, 4); gboolean fu_engine_reset_config(FuEngine *self, const gchar *section, GError **error) G_GNUC_NON_NULL(1, 2); FuContext * fu_engine_get_context(FuEngine *self) G_GNUC_NON_NULL(1); GPtrArray * fu_engine_get_releases_for_device(FuEngine *self, FuEngineRequest *request, FuDevice *device, GError **error) G_GNUC_NON_NULL(1, 2, 3); /* for the self tests */ void fu_engine_add_device(FuEngine *self, FuDevice *device) G_GNUC_NON_NULL(1, 2); void fu_engine_add_plugin(FuEngine *self, FuPlugin *plugin) G_GNUC_NON_NULL(1, 2); void fu_engine_add_remote(FuEngine *self, FwupdRemote *remote) G_GNUC_NON_NULL(1, 2); void fu_engine_add_runtime_version(FuEngine *self, const gchar *component_id, const gchar *version) G_GNUC_NON_NULL(1, 2, 3); GPtrArray * fu_engine_get_details(FuEngine *self, FuEngineRequest *request, GInputStream *stream, GError **error) G_GNUC_NON_NULL(1, 2, 3); gboolean fu_engine_check_trust(FuEngine *self, FuRelease *release, GError **error) G_GNUC_NON_NULL(1, 2); void fu_engine_set_silo(FuEngine *self, XbSilo *silo) G_GNUC_NON_NULL(1, 2); XbNode * fu_engine_get_component_by_guids(FuEngine *self, FuDevice *device) G_GNUC_NON_NULL(1, 2); gboolean fu_engine_load_release(FuEngine *self, FuRelease *release, FuCabinet *cabinet, XbNode *component, XbNode *rel, FwupdInstallFlags install_flags, GError **error) G_GNUC_NON_NULL(1, 2, 4); gchar * fu_engine_get_remote_id_for_stream(FuEngine *self, GInputStream *stream) G_GNUC_NON_NULL(1, 2); gboolean fu_engine_modify_bios_settings(FuEngine *self, GHashTable *settings, gboolean force_ro, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_engine_emulation_load(FuEngine *self, GInputStream *stream, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_engine_emulation_save(FuEngine *self, GOutputStream *stream, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_engine_fix_host_security_attr(FuEngine *self, const gchar *appstream_id, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_engine_undo_host_security_attr(FuEngine *self, const gchar *appstream_id, GError **error) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/src/fu-engine.rs000066400000000000000000000024771501337203100157360ustar00rootroot00000000000000// Copyright 2023 Richard Hughes // SPDX-License-Identifier: LGPL-2.1-or-later #[derive(FromString)] enum FuReleasePriority { None, Local, Remote, } #[derive(FromString)] enum FuP2pPolicy { Nothing = 0x00, Metadata = 0x01, Firmware = 0x02, } #[derive(ToString)] enum FuEngineEmulatorPhase { Setup, Install, Attach, Detach, Prepare, Cleanup, Reload, CompositePrepare, CompositeCleanup, } #[derive(ToBitString)] enum FuEngineRequestFlag { None = 0, NoRequirements = 1 << 0, AnyRelease = 1 << 1, } #[derive(ToBitString)] enum FuIdleInhibit { None = 0, Timeout = 1 << 0, Signals = 1 << 1, } enum FuClientFlag { None = 0, Active = 1 << 0, } #[derive(ParseBytes, Default)] #[repr(C, packed)] struct FuStructUdevMonitorNetlinkHeader { prefix: [char; 8] == "libudev", magic: u32be == 0xFEEDCAFE, header_size: u32le, properties_off: u32le, properties_len: u32le, filter_subsystem_hash: u32le, filter_devtype_hash: u32le, filter_tag_bloom_hi: u32le, filter_tag_bloom_lo: u32le, } enum FuUdevMonitorNetlinkGroup { None, Kernel, Udev, } #[derive(FromString)] enum FuUdevAction { Unknown, Add, Remove, Change, Move, Online, Offline, Bind, Unbind, } fwupd-2.0.10/src/fu-history.c000066400000000000000000001335301501337203100157630ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuHistory" #include "config.h" #include #include #include #include #include "fwupd-security-attr-private.h" #include "fu-device-private.h" #include "fu-history.h" #include "fu-release.h" #include "fu-security-attr-common.h" /* * v1 legacy schema * v2 initial schema * v3 add checksum_device to history * v4 add protocol to history * v5 create table approved_firmware * v6 create table blocked_firmware * v7 create table hsi_history * v8 add release_id to history * v9 add appstream_id to history * v10 add version_format to history * v11 no changes, bumped due to bungled migration to v10 * v12 add install_duration to history * v13 add release_flags to history * v14 create table emulation_tag */ #define FU_HISTORY_CURRENT_SCHEMA_VERSION 14 static void fu_history_finalize(GObject *object); struct _FuHistory { GObject parent_instance; FuContext *ctx; sqlite3 *db; }; G_DEFINE_TYPE(FuHistory, fu_history, G_TYPE_OBJECT) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTOPTR_CLEANUP_FUNC(sqlite3_stmt, sqlite3_finalize); #pragma clang diagnostic pop static FuDevice * fu_history_device_from_stmt(sqlite3_stmt *stmt) { const gchar *tmp; FuDevice *device; g_autoptr(FuRelease) release = fu_release_new(); /* create new result */ device = fu_device_new(NULL); fu_device_add_release(device, FWUPD_RELEASE(release)); /* device_id */ tmp = (const gchar *)sqlite3_column_text(stmt, 0); if (tmp != NULL) fwupd_device_set_id(FWUPD_DEVICE(device), tmp); /* checksum */ tmp = (const gchar *)sqlite3_column_text(stmt, 1); if (tmp != NULL) fu_release_add_checksum(release, tmp); /* plugin */ tmp = (const gchar *)sqlite3_column_text(stmt, 2); if (tmp != NULL) fu_device_set_plugin(device, tmp); /* device_created */ fu_device_set_created_usec(device, sqlite3_column_int64(stmt, 3) * G_USEC_PER_SEC); /* device_modified */ fu_device_set_modified_usec(device, sqlite3_column_int64(stmt, 4) * G_USEC_PER_SEC); /* display_name */ tmp = (const gchar *)sqlite3_column_text(stmt, 5); if (tmp != NULL) fu_device_set_name(device, tmp); /* filename */ tmp = (const gchar *)sqlite3_column_text(stmt, 6); if (tmp != NULL) fu_release_set_filename(release, tmp); /* flags */ fu_device_set_flags(device, sqlite3_column_int64(stmt, 7) | FWUPD_DEVICE_FLAG_HISTORICAL); /* metadata */ tmp = (const gchar *)sqlite3_column_text(stmt, 8); if (tmp != NULL) { g_auto(GStrv) split = g_strsplit(tmp, ";", -1); for (guint i = 0; split[i] != NULL; i++) { g_auto(GStrv) kv = g_strsplit(split[i], "=", 2); if (g_strv_length(kv) != 2) continue; fu_release_add_metadata_item(release, kv[0], kv[1]); } } /* guid_default */ tmp = (const gchar *)sqlite3_column_text(stmt, 9); if (tmp != NULL) fu_device_add_instance_id_full(device, tmp, FU_DEVICE_INSTANCE_FLAG_VISIBLE); /* update_state */ fu_device_set_update_state(device, sqlite3_column_int(stmt, 10)); /* update_error */ tmp = (const gchar *)sqlite3_column_text(stmt, 11); fu_device_set_update_error(device, tmp); /* version_new */ tmp = (const gchar *)sqlite3_column_text(stmt, 12); if (tmp != NULL) fu_release_set_version(release, tmp); /* version_old */ tmp = (const gchar *)sqlite3_column_text(stmt, 13); if (tmp != NULL) fu_device_set_version(device, tmp); /* checksum_device */ tmp = (const gchar *)sqlite3_column_text(stmt, 14); if (tmp != NULL) fu_device_add_checksum(device, tmp); /* protocol */ tmp = (const gchar *)sqlite3_column_text(stmt, 15); if (tmp != NULL) fu_release_set_protocol(release, tmp); /* release_id */ tmp = (const gchar *)sqlite3_column_text(stmt, 16); if (tmp != NULL) fu_release_set_id(release, tmp); /* appstream_id */ tmp = (const gchar *)sqlite3_column_text(stmt, 17); if (tmp != NULL) fu_release_set_appstream_id(release, tmp); /* version_format */ fu_device_set_version_format(device, sqlite3_column_int(stmt, 18)); /* install_duration */ fu_device_set_install_duration(device, sqlite3_column_int(stmt, 19)); /* release flags */ fu_release_set_flags(release, sqlite3_column_int(stmt, 20)); /* success */ fu_device_convert_instance_ids(device); return device; } static gboolean fu_history_stmt_exec(FuHistory *self, sqlite3_stmt *stmt, GPtrArray *array, GError **error) { gint rc; if (array == NULL) { rc = sqlite3_step(stmt); } else { while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { FuDevice *device = fu_history_device_from_stmt(stmt); g_ptr_array_add(array, device); } } if (rc != SQLITE_DONE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to execute prepared statement: %s", sqlite3_errmsg(self->db)); return FALSE; } return TRUE; } static gboolean fu_history_create_database(FuHistory *self, GError **error) { gint rc; rc = sqlite3_exec(self->db, "BEGIN TRANSACTION;" "CREATE TABLE IF NOT EXISTS schema (" "created timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP," "version INTEGER DEFAULT 0);" "INSERT INTO schema (version) VALUES (0);" "CREATE TABLE IF NOT EXISTS history (" "device_id TEXT," "update_state INTEGER DEFAULT 0," "update_error TEXT," "filename TEXT," "display_name TEXT," "plugin TEXT," "device_created INTEGER DEFAULT 0," /* seconds */ "device_modified INTEGER DEFAULT 0," /* seconds */ "checksum TEXT DEFAULT NULL," "flags INTEGER DEFAULT 0," "metadata TEXT DEFAULT NULL," "guid_default TEXT DEFAULT NULL," "version_old TEXT," "version_new TEXT," "checksum_device TEXT DEFAULT NULL," "protocol TEXT DEFAULT NULL," "release_id TEXT DEFAULT NULL," "appstream_id TEXT DEFAULT NULL," "version_format INTEGER DEFAULT 0," "install_duration INTEGER DEFAULT 0," "release_flags INTEGER DEFAULT 0);" "CREATE TABLE IF NOT EXISTS approved_firmware (" "checksum TEXT);" "CREATE TABLE IF NOT EXISTS blocked_firmware (" "checksum TEXT);" "CREATE TABLE IF NOT EXISTS hsi_history (" "timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP," "hsi_details TEXT DEFAULT NULL," "hsi_score TEXT DEFAULT NULL);" "CREATE TABLE emulation_tag (device_id TEXT);" "CREATE UNIQUE INDEX idx_device_id ON emulation_tag (device_id);" "COMMIT;", NULL, NULL, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to prepare SQL for creating tables: %s", sqlite3_errmsg(self->db)); return FALSE; } return TRUE; } static gboolean fu_history_migrate_database_v1(FuHistory *self, GError **error) { gint rc; g_info("migrating v1 database by recreating table"); /* rename the table to something out the way */ rc = sqlite3_exec(self->db, "ALTER TABLE history RENAME TO history_old;", NULL, NULL, NULL); if (rc != SQLITE_OK) { g_debug("cannot rename v0 table: %s", sqlite3_errmsg(self->db)); return TRUE; } /* create new table */ if (!fu_history_create_database(self, error)) return FALSE; /* migrate the old entries to the new table */ rc = sqlite3_exec(self->db, "INSERT INTO history SELECT " "device_id, update_state, update_error, filename, " "display_name, plugin, device_created, device_modified, " "checksum, flags, metadata, guid_default, version_old, " "version_new, NULL, NULL, NULL, NULL, NULL, 0, 0 FROM history_old;" "DROP TABLE history_old;", NULL, NULL, NULL); if (rc != SQLITE_OK) { g_debug("no history to migrate: %s", sqlite3_errmsg(self->db)); return TRUE; } return TRUE; } static gboolean fu_history_migrate_database_v2(FuHistory *self, GError **error) { gint rc; rc = sqlite3_exec(self->db, "ALTER TABLE history ADD COLUMN checksum_device TEXT DEFAULT NULL;", NULL, NULL, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to alter database: %s", sqlite3_errmsg(self->db)); return FALSE; } return TRUE; } static gboolean fu_history_migrate_database_v3(FuHistory *self, GError **error) { gint rc; rc = sqlite3_exec(self->db, "ALTER TABLE history ADD COLUMN protocol TEXT DEFAULT NULL;", NULL, NULL, NULL); if (rc != SQLITE_OK) g_debug("ignoring database error: %s", sqlite3_errmsg(self->db)); return TRUE; } static gboolean fu_history_migrate_database_v4(FuHistory *self, GError **error) { gint rc; rc = sqlite3_exec(self->db, "CREATE TABLE IF NOT EXISTS approved_firmware (checksum TEXT);", NULL, NULL, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to create table: %s", sqlite3_errmsg(self->db)); return FALSE; } return TRUE; } static gboolean fu_history_migrate_database_v5(FuHistory *self, GError **error) { gint rc; rc = sqlite3_exec(self->db, "CREATE TABLE IF NOT EXISTS blocked_firmware (checksum TEXT);", NULL, NULL, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to create table: %s", sqlite3_errmsg(self->db)); return FALSE; } return TRUE; } static gboolean fu_history_migrate_database_v6(FuHistory *self, GError **error) { gint rc; rc = sqlite3_exec(self->db, "CREATE TABLE IF NOT EXISTS hsi_history (" "timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP," "hsi_details TEXT DEFAULT NULL," "hsi_score TEXT DEFAULT NULL);", NULL, NULL, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to create table: %s", sqlite3_errmsg(self->db)); return FALSE; } return TRUE; } static gboolean fu_history_migrate_database_v7(FuHistory *self, GError **error) { gint rc; rc = sqlite3_exec(self->db, "ALTER TABLE history ADD COLUMN release_id TEXT DEFAULT NULL;", NULL, NULL, NULL); if (rc != SQLITE_OK) g_debug("ignoring database error: %s", sqlite3_errmsg(self->db)); return TRUE; } static gboolean fu_history_migrate_database_v8(FuHistory *self, GError **error) { gint rc; rc = sqlite3_exec(self->db, "ALTER TABLE history ADD COLUMN appstream_id TEXT DEFAULT NULL;", NULL, NULL, NULL); if (rc != SQLITE_OK) g_debug("ignoring database error: %s", sqlite3_errmsg(self->db)); return TRUE; } static gboolean fu_history_migrate_database_v9(FuHistory *self, GError **error) { gint rc; rc = sqlite3_exec(self->db, "ALTER TABLE history ADD COLUMN version_format INTEGER DEFAULT 0;", NULL, NULL, NULL); if (rc != SQLITE_OK) g_debug("ignoring database error: %s", sqlite3_errmsg(self->db)); return TRUE; } static gboolean fu_history_migrate_database_v10(FuHistory *self, GError **error) { gint rc; rc = sqlite3_exec(self->db, "ALTER TABLE history ADD COLUMN install_duration INTEGER DEFAULT 0;", NULL, NULL, NULL); if (rc != SQLITE_OK) g_debug("ignoring database error: %s", sqlite3_errmsg(self->db)); return TRUE; } static gboolean fu_history_migrate_database_v11(FuHistory *self, GError **error) { gint rc; rc = sqlite3_exec(self->db, "ALTER TABLE history ADD COLUMN release_flags INTEGER DEFAULT 0;", NULL, NULL, NULL); if (rc != SQLITE_OK) g_debug("ignoring database error: %s", sqlite3_errmsg(self->db)); return TRUE; } static gboolean fu_history_migrate_database_v12(FuHistory *self, GError **error) { gint rc; rc = sqlite3_exec( self->db, "BEGIN TRANSACTION;" "CREATE TABLE IF NOT EXISTS emulation_tag (device_id TEXT);" "CREATE UNIQUE INDEX IF NOT EXISTS idx_device_id ON emulation_tag (device_id);" "COMMIT;", NULL, NULL, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to create table: %s", sqlite3_errmsg(self->db)); return FALSE; } return TRUE; } /* returns 0 if database is not initialized */ static guint fu_history_get_schema_version(FuHistory *self) { gint rc; g_autoptr(sqlite3_stmt) stmt = NULL; rc = sqlite3_prepare_v2(self->db, "SELECT version FROM schema LIMIT 1;", -1, &stmt, NULL); if (rc != SQLITE_OK) { g_debug("no schema version: %s", sqlite3_errmsg(self->db)); return 0; } rc = sqlite3_step(stmt); if (rc != SQLITE_ROW) { g_warning("failed prepare to get schema version: %s", sqlite3_errmsg(self->db)); return 0; } return sqlite3_column_int(stmt, 0); } static gboolean fu_history_create_or_migrate(FuHistory *self, guint schema_ver, GError **error) { gint rc; g_autoptr(sqlite3_stmt) stmt = NULL; if (schema_ver == 0) g_info("building initial database"); else if (schema_ver > 1) g_info("migrating v%u database by altering", schema_ver); switch (schema_ver) { /* create initial up-to-date database or migrate */ case 0: if (!fu_history_create_database(self, error)) return FALSE; break; case 1: if (!fu_history_migrate_database_v1(self, error)) return FALSE; break; case 2: if (!fu_history_migrate_database_v2(self, error)) return FALSE; /* fall through */ case 3: if (!fu_history_migrate_database_v3(self, error)) return FALSE; /* fall through */ case 4: if (!fu_history_migrate_database_v4(self, error)) return FALSE; /* fall through */ case 5: if (!fu_history_migrate_database_v5(self, error)) return FALSE; /* fall through */ case 6: if (!fu_history_migrate_database_v6(self, error)) return FALSE; /* fall through */ case 7: if (!fu_history_migrate_database_v7(self, error)) return FALSE; /* fall through */ case 8: if (!fu_history_migrate_database_v8(self, error)) return FALSE; /* fall through */ case 9: case 10: if (!fu_history_migrate_database_v9(self, error)) return FALSE; /* fall through */ case 11: if (!fu_history_migrate_database_v10(self, error)) return FALSE; /* fall through */ case 12: if (!fu_history_migrate_database_v11(self, error)) return FALSE; /* fall through */ case 13: if (!fu_history_migrate_database_v12(self, error)) return FALSE; /* no longer fall through */ break; default: /* this is probably okay, but return an error if we ever delete * or rename columns */ g_warning("schema version %u is unknown", schema_ver); return TRUE; } /* set new schema version */ rc = sqlite3_prepare_v2(self->db, "UPDATE schema SET version=?1;", -1, &stmt, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to prepare SQL for updating schema: %s", sqlite3_errmsg(self->db)); return FALSE; } sqlite3_bind_int(stmt, 1, FU_HISTORY_CURRENT_SCHEMA_VERSION); return fu_history_stmt_exec(self, stmt, NULL, error); } static gboolean fu_history_open(FuHistory *self, const gchar *filename, GError **error) { gint rc; g_debug("trying to open database '%s'", filename); rc = sqlite3_open(filename, &self->db); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_READ, "Can't open %s: %s", filename, sqlite3_errmsg(self->db)); return FALSE; } /* turn off the lookaside cache */ sqlite3_db_config(self->db, SQLITE_DBCONFIG_LOOKASIDE, NULL, 0, 0); return TRUE; } static gboolean fu_history_load(FuHistory *self, GError **error) { gint rc; guint schema_ver; g_autofree gchar *dirname = NULL; g_autofree gchar *filename = NULL; g_autoptr(GFile) file = NULL; /* already done */ if (self->db != NULL) return TRUE; g_return_val_if_fail(FU_IS_HISTORY(self), FALSE); g_return_val_if_fail(self->db == NULL, FALSE); /* create directory */ dirname = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR_PKG); file = g_file_new_for_path(dirname); if (!g_file_query_exists(file, NULL)) { if (!g_file_make_directory_with_parents(file, NULL, error)) return FALSE; } /* open */ filename = g_build_filename(dirname, "pending.db", NULL); if (!fu_history_open(self, filename, error)) return FALSE; /* check database */ schema_ver = fu_history_get_schema_version(self); if (schema_ver == 0) { g_autoptr(sqlite3_stmt) stmt_tmp = NULL; rc = sqlite3_prepare_v2(self->db, "SELECT * FROM history LIMIT 0;", -1, &stmt_tmp, NULL); if (rc == SQLITE_OK) schema_ver = 1; } /* create initial up-to-date database, or migrate */ g_debug("got schema version of %u", schema_ver); if (schema_ver != FU_HISTORY_CURRENT_SCHEMA_VERSION) { g_autoptr(GError) error_migrate = NULL; if (!fu_history_create_or_migrate(self, schema_ver, &error_migrate)) { /* this is fatal to the daemon, so delete the database * and try again with something empty */ g_warning("failed to migrate %s database: %s", filename, error_migrate->message); sqlite3_close(self->db); if (g_unlink(filename) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Can't delete %s", filename); return FALSE; } if (!fu_history_open(self, filename, error)) return FALSE; return fu_history_create_database(self, error); } } /* success */ return TRUE; } static gchar * fu_history_convert_hash_to_string(GHashTable *hash) { GString *str = g_string_new(NULL); g_autoptr(GList) keys = g_hash_table_get_keys(hash); for (GList *l = keys; l != NULL; l = l->next) { const gchar *key = l->data; const gchar *value = g_hash_table_lookup(hash, key); if (str->len > 0) g_string_append(str, ";"); g_string_append_printf(str, "%s=%s", key, value); } return g_string_free(str, FALSE); } /* unset some flags we don't want to store */ static FwupdDeviceFlags fu_history_get_device_flags_filtered(FuDevice *device) { FwupdDeviceFlags flags = fu_device_get_flags(device); flags &= ~FWUPD_DEVICE_FLAG_SUPPORTED; return flags; } /** * fu_history_modify_device: * @self: a #FuHistory * @device: a device * @error: (nullable): optional return location for an error * * Modify a device in the history database * * Returns: @TRUE if successful, @FALSE for failure * * Since: 1.0.4 **/ gboolean fu_history_modify_device(FuHistory *self, FuDevice *device, GError **error) { gint rc; g_autoptr(sqlite3_stmt) stmt = NULL; g_return_val_if_fail(FU_IS_HISTORY(self), FALSE); g_return_val_if_fail(FU_IS_DEVICE(device), FALSE); /* lazy load */ if (!fu_history_load(self, error)) return FALSE; /* overwrite entry if it exists */ g_debug("modifying device %s [%s]", fu_device_get_name(device), fu_device_get_id(device)); rc = sqlite3_prepare_v2(self->db, "UPDATE history SET " "update_state = ?1, " "update_error = ?2, " "checksum_device = ?6, " "device_modified = ?7, " "install_duration = ?8, " "flags = ?3 " "WHERE device_id = ?4;", -1, &stmt, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to prepare SQL to update history: %s", sqlite3_errmsg(self->db)); return FALSE; } sqlite3_bind_int(stmt, 1, fu_device_get_update_state(device)); sqlite3_bind_text(stmt, 2, fu_device_get_update_error(device), -1, SQLITE_STATIC); sqlite3_bind_int64(stmt, 3, fu_history_get_device_flags_filtered(device)); sqlite3_bind_text(stmt, 4, fu_device_get_id(device), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 5, fu_device_get_version(device), -1, SQLITE_STATIC); sqlite3_bind_text( stmt, 6, fwupd_checksum_get_by_kind(fu_device_get_checksums(device), G_CHECKSUM_SHA1), -1, SQLITE_STATIC); sqlite3_bind_int64(stmt, 7, fu_device_get_modified_usec(device) / G_USEC_PER_SEC); sqlite3_bind_int64(stmt, 8, fu_device_get_install_duration(device)); if (!fu_history_stmt_exec(self, stmt, NULL, error)) return FALSE; if (sqlite3_changes(self->db) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no device %s", fu_device_get_id(device)); return FALSE; } return TRUE; } /** * fu_history_modify_device_release: * @self: a #FuHistory * @device: a #FuDevice * @release: a #FuRelease * @error: (nullable): optional return location for an error * * Modify a device in the history database, also changing metadata from the new release. * * Returns: @TRUE if successful, @FALSE for failure * * Since: 1.8.8 **/ gboolean fu_history_modify_device_release(FuHistory *self, FuDevice *device, FuRelease *release, GError **error) { gint rc; g_autofree gchar *metadata = NULL; g_autoptr(sqlite3_stmt) stmt = NULL; g_return_val_if_fail(FU_IS_HISTORY(self), FALSE); g_return_val_if_fail(FU_IS_DEVICE(device), FALSE); /* lazy load */ if (!fu_history_load(self, error)) return FALSE; /* metadata is stored as a simple string */ metadata = fu_history_convert_hash_to_string(fu_release_get_metadata(release)); /* overwrite entry if it exists */ g_debug("modifying device %s [%s]", fu_device_get_name(device), fu_device_get_id(device)); rc = sqlite3_prepare_v2(self->db, "UPDATE history SET " "update_state = ?1, " "update_error = ?2, " "checksum_device = ?6, " "device_modified = ?7, " "metadata = ?8, " "flags = ?3 " "WHERE device_id = ?4;", -1, &stmt, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to prepare SQL to update history: %s", sqlite3_errmsg(self->db)); return FALSE; } sqlite3_bind_int(stmt, 1, fu_device_get_update_state(device)); sqlite3_bind_text(stmt, 2, fu_device_get_update_error(device), -1, SQLITE_STATIC); sqlite3_bind_int64(stmt, 3, fu_history_get_device_flags_filtered(device)); sqlite3_bind_text(stmt, 4, fu_device_get_id(device), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 5, fu_device_get_version(device), -1, SQLITE_STATIC); sqlite3_bind_text( stmt, 6, fwupd_checksum_get_by_kind(fu_device_get_checksums(device), G_CHECKSUM_SHA1), -1, SQLITE_STATIC); sqlite3_bind_int64(stmt, 7, fu_device_get_modified_usec(device) / G_USEC_PER_SEC); sqlite3_bind_text(stmt, 8, metadata, -1, SQLITE_STATIC); return fu_history_stmt_exec(self, stmt, NULL, error); } /** * fu_history_add_device: * @self: a #FuHistory * @device: a device * @release: a #FuRelease * @error: (nullable): optional return location for an error * * Adds a device to the history database * * Returns: @TRUE if successful, @FALSE for failure * * Since: 1.0.4 **/ gboolean fu_history_add_device(FuHistory *self, FuDevice *device, FuRelease *release, GError **error) { const gchar *checksum_device; const gchar *checksum = NULL; gint rc; g_autofree gchar *metadata = NULL; g_autoptr(sqlite3_stmt) stmt = NULL; g_return_val_if_fail(FU_IS_HISTORY(self), FALSE); g_return_val_if_fail(FU_IS_DEVICE(device), FALSE); g_return_val_if_fail(FU_IS_RELEASE(release), FALSE); /* lazy load */ if (!fu_history_load(self, error)) return FALSE; /* make tests easier */ fu_device_convert_instance_ids(device); /* ensure all old device(s) with this ID are removed */ if (!fu_history_remove_device(self, device, error)) return FALSE; g_debug("add device %s [%s]", fu_device_get_name(device), fu_device_get_id(device)); checksum = fwupd_checksum_get_by_kind(fu_release_get_checksums(release), G_CHECKSUM_SHA1); checksum_device = fwupd_checksum_get_by_kind(fu_device_get_checksums(device), G_CHECKSUM_SHA1); /* metadata is stored as a simple string */ metadata = fu_history_convert_hash_to_string(fu_release_get_metadata(release)); /* add */ rc = sqlite3_prepare_v2(self->db, "INSERT INTO history (device_id," "update_state," "update_error," "flags," "filename," "checksum," "display_name," "plugin," "guid_default," "metadata," "device_created," "device_modified," "version_old," "version_new," "checksum_device," "protocol," "release_id," "appstream_id," "version_format," "install_duration," "release_flags) " "VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10," "?11,?12,?13,?14,?15,?16,?17,?18,?19,?20,?21)", -1, &stmt, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to prepare SQL to insert history: %s", sqlite3_errmsg(self->db)); return FALSE; } sqlite3_bind_text(stmt, 1, fu_device_get_id(device), -1, SQLITE_STATIC); sqlite3_bind_int(stmt, 2, fu_device_get_update_state(device)); sqlite3_bind_text(stmt, 3, fu_device_get_update_error(device), -1, SQLITE_STATIC); sqlite3_bind_int64(stmt, 4, fu_history_get_device_flags_filtered(device)); sqlite3_bind_text(stmt, 5, fu_release_get_filename(release), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 6, checksum, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 7, fu_device_get_name(device), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 8, fu_device_get_plugin(device), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 9, fu_device_get_guid_default(device), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 10, metadata, -1, SQLITE_STATIC); sqlite3_bind_int64(stmt, 11, fu_device_get_created_usec(device) / G_USEC_PER_SEC); sqlite3_bind_int64(stmt, 12, fu_device_get_modified_usec(device) / G_USEC_PER_SEC); sqlite3_bind_text(stmt, 13, fu_device_get_version(device), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 14, fu_release_get_version(release), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 15, checksum_device, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 16, fu_release_get_protocol(release), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 17, fu_release_get_id(release), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 18, fu_release_get_appstream_id(release), -1, SQLITE_STATIC); sqlite3_bind_int(stmt, 19, fu_device_get_version_format(device)); sqlite3_bind_int(stmt, 20, fu_device_get_install_duration(device)); sqlite3_bind_int(stmt, 21, fu_release_get_flags(release)); return fu_history_stmt_exec(self, stmt, NULL, error); } /** * fu_history_remove_all: * @self: a #FuHistory * @error: (nullable): optional return location for an error * * Remove all devices from the history database * * Returns: @TRUE if successful, @FALSE for failure * * Since: 1.0.4 **/ gboolean fu_history_remove_all(FuHistory *self, GError **error) { gint rc; g_autoptr(sqlite3_stmt) stmt = NULL; g_return_val_if_fail(FU_IS_HISTORY(self), FALSE); /* lazy load */ if (!fu_history_load(self, error)) return FALSE; /* remove entries */ g_debug("removing all devices"); rc = sqlite3_prepare_v2(self->db, "DELETE FROM history;", -1, &stmt, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to prepare SQL to delete history: %s", sqlite3_errmsg(self->db)); return FALSE; } return fu_history_stmt_exec(self, stmt, NULL, error); } /** * fu_history_remove_device: * @self: a #FuHistory * @device: a device * @error: (nullable): optional return location for an error * * Remove a device from the history database * * Returns: @TRUE if successful, @FALSE for failure * * Since: 1.0.4 **/ gboolean fu_history_remove_device(FuHistory *self, FuDevice *device, GError **error) { gint rc; g_autoptr(sqlite3_stmt) stmt = NULL; g_return_val_if_fail(FU_IS_HISTORY(self), FALSE); g_return_val_if_fail(FU_IS_DEVICE(device), FALSE); /* lazy load */ if (!fu_history_load(self, error)) return FALSE; g_debug("remove device %s [%s]", fu_device_get_name(device), fu_device_get_id(device)); rc = sqlite3_prepare_v2(self->db, "DELETE FROM history WHERE device_id = ?1;", -1, &stmt, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to prepare SQL to delete history: %s", sqlite3_errmsg(self->db)); return FALSE; } sqlite3_bind_text(stmt, 1, fu_device_get_id(device), -1, SQLITE_STATIC); return fu_history_stmt_exec(self, stmt, NULL, error); } /** * fu_history_get_device_by_id: * @self: a #FuHistory * @device_id: a string * @error: (nullable): optional return location for an error * * Returns the device from the history database or NULL if not found * * Returns: (transfer full): a device * * Since: 1.0.4 **/ FuDevice * fu_history_get_device_by_id(FuHistory *self, const gchar *device_id, GError **error) { gint rc; g_autoptr(GPtrArray) array_tmp = NULL; g_autoptr(sqlite3_stmt) stmt = NULL; g_return_val_if_fail(FU_IS_HISTORY(self), NULL); g_return_val_if_fail(device_id != NULL, NULL); /* lazy load */ if (!fu_history_load(self, error)) return NULL; /* get all the devices */ rc = sqlite3_prepare_v2(self->db, "SELECT device_id, " "checksum, " "plugin, " "device_created, " "device_modified, " "display_name, " "filename, " "flags, " "metadata, " "guid_default, " "update_state, " "update_error, " "version_new, " "version_old, " "checksum_device, " "protocol, " "release_id, " "appstream_id, " "version_format, " "install_duration, " "release_flags FROM history WHERE " "device_id = ?1 ORDER BY device_created DESC " "LIMIT 1", -1, &stmt, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to prepare SQL to get history: %s", sqlite3_errmsg(self->db)); return NULL; } sqlite3_bind_text(stmt, 1, device_id, -1, SQLITE_STATIC); array_tmp = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); if (!fu_history_stmt_exec(self, stmt, array_tmp, error)) return NULL; if (array_tmp->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "No devices found"); return NULL; } return g_object_ref(g_ptr_array_index(array_tmp, 0)); } /** * fu_history_get_devices: * @self: a #FuHistory * @error: (nullable): optional return location for an error * * Gets the devices in the history database. * * Returns: (element-type #FuDevice) (transfer container): devices * * Since: 1.0.4 **/ GPtrArray * fu_history_get_devices(FuHistory *self, GError **error) { g_autoptr(GPtrArray) array = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_autoptr(sqlite3_stmt) stmt = NULL; gint rc; g_return_val_if_fail(FU_IS_HISTORY(self), NULL); /* lazy load */ if (self->db == NULL) { if (!fu_history_load(self, error)) return NULL; } /* get all the devices */ rc = sqlite3_prepare_v2(self->db, "SELECT device_id, " "checksum, " "plugin, " "device_created, " "device_modified, " "display_name, " "filename, " "flags, " "metadata, " "guid_default, " "update_state, " "update_error, " "version_new, " "version_old, " "checksum_device, " "protocol, " "release_id, " "appstream_id, " "version_format, " "install_duration, " "release_flags FROM history " "ORDER BY device_modified ASC;", -1, &stmt, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to prepare SQL to get history: %s", sqlite3_errmsg(self->db)); return NULL; } if (!fu_history_stmt_exec(self, stmt, array, error)) return NULL; return g_steal_pointer(&array); } /** * fu_history_get_approved_firmware: * @self: a #FuHistory * @error: (nullable): optional return location for an error * * Returns approved firmware records. * * Returns: (transfer full) (element-type gchar *): records * * Since: 1.2.6 **/ GPtrArray * fu_history_get_approved_firmware(FuHistory *self, GError **error) { g_autoptr(GPtrArray) array = g_ptr_array_new_with_free_func(g_free); gint rc; g_autoptr(sqlite3_stmt) stmt = NULL; g_return_val_if_fail(FU_IS_HISTORY(self), NULL); /* lazy load */ if (self->db == NULL) { if (!fu_history_load(self, error)) return NULL; } /* get all the approved firmware */ rc = sqlite3_prepare_v2(self->db, "SELECT checksum FROM approved_firmware;", -1, &stmt, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to prepare SQL to get checksum: %s", sqlite3_errmsg(self->db)); return NULL; } while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { const gchar *tmp = (const gchar *)sqlite3_column_text(stmt, 0); g_ptr_array_add(array, g_strdup(tmp)); } if (rc != SQLITE_DONE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to execute prepared statement: %s", sqlite3_errmsg(self->db)); return NULL; } return g_steal_pointer(&array); } /** * fu_history_clear_approved_firmware: * @self: a #FuHistory * @error: (nullable): optional return location for an error * * Clear all approved firmware records * * Returns: #TRUE for success, #FALSE for failure * * Since: 1.2.6 **/ gboolean fu_history_clear_approved_firmware(FuHistory *self, GError **error) { gint rc; g_autoptr(sqlite3_stmt) stmt = NULL; g_return_val_if_fail(FU_IS_HISTORY(self), FALSE); /* lazy load */ if (!fu_history_load(self, error)) return FALSE; /* remove entries */ rc = sqlite3_prepare_v2(self->db, "DELETE FROM approved_firmware;", -1, &stmt, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to prepare SQL to delete approved firmware: %s", sqlite3_errmsg(self->db)); return FALSE; } return fu_history_stmt_exec(self, stmt, NULL, error); } /** * fu_history_add_approved_firmware: * @self: a #FuHistory * @checksum: a string * @error: (nullable): optional return location for an error * * Add an approved firmware record to the database * * Returns: #TRUE for success, #FALSE for failure * * Since: 1.2.6 **/ gboolean fu_history_add_approved_firmware(FuHistory *self, const gchar *checksum, GError **error) { gint rc; g_autoptr(sqlite3_stmt) stmt = NULL; g_return_val_if_fail(FU_IS_HISTORY(self), FALSE); g_return_val_if_fail(checksum != NULL, FALSE); /* lazy load */ if (!fu_history_load(self, error)) return FALSE; /* add */ rc = sqlite3_prepare_v2(self->db, "INSERT INTO approved_firmware (checksum) " "VALUES (?1)", -1, &stmt, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to prepare SQL to insert checksum: %s", sqlite3_errmsg(self->db)); return FALSE; } sqlite3_bind_text(stmt, 1, checksum, -1, SQLITE_STATIC); return fu_history_stmt_exec(self, stmt, NULL, error); } /** * fu_history_get_blocked_firmware: * @self: a #FuHistory * @error: (nullable): optional return location for an error * * Returns blocked firmware records. * * Returns: (transfer full) (element-type gchar *): records * * Since: 1.4.6 **/ GPtrArray * fu_history_get_blocked_firmware(FuHistory *self, GError **error) { gint rc; g_autoptr(GPtrArray) array = g_ptr_array_new_with_free_func(g_free); g_autoptr(sqlite3_stmt) stmt = NULL; g_return_val_if_fail(FU_IS_HISTORY(self), NULL); /* lazy load */ if (self->db == NULL) { if (!fu_history_load(self, error)) return NULL; } /* get all the blocked firmware */ rc = sqlite3_prepare_v2(self->db, "SELECT checksum FROM blocked_firmware;", -1, &stmt, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to prepare SQL to get checksum: %s", sqlite3_errmsg(self->db)); return NULL; } while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { const gchar *tmp = (const gchar *)sqlite3_column_text(stmt, 0); g_ptr_array_add(array, g_strdup(tmp)); } if (rc != SQLITE_DONE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to execute prepared statement: %s", sqlite3_errmsg(self->db)); return NULL; } return g_steal_pointer(&array); } /** * fu_history_clear_blocked_firmware: * @self: a #FuHistory * @error: (nullable): optional return location for an error * * Clear all blocked firmware records * * Returns: #TRUE for success, #FALSE for failure * * Since: 1.4.6 **/ gboolean fu_history_clear_blocked_firmware(FuHistory *self, GError **error) { gint rc; g_autoptr(sqlite3_stmt) stmt = NULL; g_return_val_if_fail(FU_IS_HISTORY(self), FALSE); /* lazy load */ if (!fu_history_load(self, error)) return FALSE; /* remove entries */ rc = sqlite3_prepare_v2(self->db, "DELETE FROM blocked_firmware;", -1, &stmt, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to prepare SQL to delete blocked firmware: %s", sqlite3_errmsg(self->db)); return FALSE; } return fu_history_stmt_exec(self, stmt, NULL, error); } /** * fu_history_add_blocked_firmware: * @self: a #FuHistory * @checksum: a string * @error: (nullable): optional return location for an error * * Add an blocked firmware record to the database * * Returns: #TRUE for success, #FALSE for failure * * Since: 1.4.6 **/ gboolean fu_history_add_blocked_firmware(FuHistory *self, const gchar *checksum, GError **error) { gint rc; g_autoptr(sqlite3_stmt) stmt = NULL; g_return_val_if_fail(FU_IS_HISTORY(self), FALSE); g_return_val_if_fail(checksum != NULL, FALSE); /* lazy load */ if (!fu_history_load(self, error)) return FALSE; /* add */ rc = sqlite3_prepare_v2(self->db, "INSERT INTO blocked_firmware (checksum) " "VALUES (?1)", -1, &stmt, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to prepare SQL to insert checksum: %s", sqlite3_errmsg(self->db)); return FALSE; } sqlite3_bind_text(stmt, 1, checksum, -1, SQLITE_STATIC); return fu_history_stmt_exec(self, stmt, NULL, error); } gboolean fu_history_add_security_attribute(FuHistory *self, const gchar *security_attr_json, const gchar *hsi_score, GError **error) { gint rc; g_autoptr(sqlite3_stmt) stmt = NULL; g_return_val_if_fail(FU_IS_HISTORY(self), FALSE); /* lazy load */ if (!fu_history_load(self, error)) return FALSE; /* remove entries */ rc = sqlite3_prepare_v2(self->db, "INSERT INTO hsi_history (hsi_details, hsi_score)" "VALUES (?1, ?2)", -1, &stmt, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to prepare SQL to write security attribute: %s", sqlite3_errmsg(self->db)); return FALSE; } sqlite3_bind_text(stmt, 1, security_attr_json, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, hsi_score, -1, SQLITE_STATIC); return fu_history_stmt_exec(self, stmt, NULL, error); } /** * fu_history_get_security_attrs: * @self: a #FuHistory * @limit: maximum number of attributes to return, or 0 for no limit * @error: (nullable): optional return location for an error * * Gets the security attributes in the history database. * Attributes with the same stores JSON data will be deduplicated as required. * * Returns: (element-type #FuSecurityAttrs) (transfer container): attrs * * Since: 1.7.1 **/ GPtrArray * fu_history_get_security_attrs(FuHistory *self, guint limit, GError **error) { gint rc; guint old_hash = 0; g_autoptr(GPtrArray) array = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_autoptr(sqlite3_stmt) stmt = NULL; g_return_val_if_fail(FU_IS_HISTORY(self), NULL); /* lazy load */ if (self->db == NULL) { if (!fu_history_load(self, error)) return NULL; } /* get all the devices */ rc = sqlite3_prepare_v2(self->db, "SELECT timestamp, hsi_details FROM hsi_history " "ORDER BY timestamp DESC;", -1, &stmt, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to prepare SQL to get security attrs: %s", sqlite3_errmsg(self->db)); return NULL; } while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { const gchar *json; guint hash; const gchar *timestamp; g_autoptr(FuSecurityAttrs) attrs = fu_security_attrs_new(); g_autoptr(GDateTime) created_dt = NULL; g_autoptr(GTimeZone) tz_utc = g_time_zone_new_utc(); /* old */ timestamp = (const gchar *)sqlite3_column_text(stmt, 0); if (timestamp == NULL) continue; /* device_id */ json = (const gchar *)sqlite3_column_text(stmt, 1); if (json == NULL) continue; /* do not create dups */ hash = g_str_hash(json); if (hash == old_hash) { g_debug("skipping %s as unchanged", timestamp); continue; } old_hash = hash; /* parse JSON */ g_debug("parsing %s", timestamp); if (!fwupd_codec_from_json_string(FWUPD_CODEC(attrs), json, error)) return NULL; /* parse timestamp */ created_dt = g_date_time_new_from_iso8601(timestamp, tz_utc); if (created_dt != NULL) { guint64 created_unix = g_date_time_to_unix(created_dt); g_autoptr(GPtrArray) attr_array = fu_security_attrs_get_all(attrs, NULL); for (guint i = 0; i < attr_array->len; i++) { FwupdSecurityAttr *attr = g_ptr_array_index(attr_array, i); fwupd_security_attr_set_created(attr, created_unix); } } /* success */ g_ptr_array_add(array, g_steal_pointer(&attrs)); if (limit > 0 && array->len >= limit) { rc = SQLITE_DONE; break; } } if (rc != SQLITE_DONE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to execute prepared statement: %s", sqlite3_errmsg(self->db)); return NULL; } return g_steal_pointer(&array); } /** * fu_history_has_emulation_tag: * @self: a #FuHistory * @device_id: (nullable): a device ID, or %NULL for "any" * @error: (nullable): optional return location for an error * * Returns if the device has been tagged for emulation. * * Returns: %TRUE if tagged * * Since: 2.0.1 **/ gboolean fu_history_has_emulation_tag(FuHistory *self, const gchar *device_id, GError **error) { gint rc; g_autoptr(sqlite3_stmt) stmt = NULL; g_return_val_if_fail(FU_IS_HISTORY(self), FALSE); /* lazy load */ if (self->db == NULL) { if (!fu_history_load(self, error)) return FALSE; } /* get tagged device ID */ if (device_id != NULL) { rc = sqlite3_prepare_v2(self->db, "SELECT device_id FROM emulation_tag " "WHERE device_id = ?1 LIMIT 1;", -1, &stmt, NULL); } else { rc = sqlite3_prepare_v2(self->db, "SELECT device_id FROM emulation_tag LIMIT 1;", -1, &stmt, NULL); } if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to prepare SQL to get emulation tag: %s", sqlite3_errmsg(self->db)); return FALSE; } sqlite3_bind_text(stmt, 1, device_id, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); if (rc == SQLITE_DONE) { if (device_id == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no devices were found for emulation tag"); return FALSE; } g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "%s was not found for emulation tag", device_id); return FALSE; } if (rc != SQLITE_ROW) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE, "failed to execute prepared statement: %s", sqlite3_errmsg(self->db)); return FALSE; } /* success */ return TRUE; } /** * fu_history_add_emulation_tag: * @self: a #FuHistory * @device_id: a device ID * @error: (nullable): optional return location for an error * * Add a hash of a device to be tagged for emulation to the database * * Returns: #TRUE for success, #FALSE for failure * * Since: 2.0.1 **/ gboolean fu_history_add_emulation_tag(FuHistory *self, const gchar *device_id, GError **error) { gint rc; g_autoptr(sqlite3_stmt) stmt = NULL; g_return_val_if_fail(FU_IS_HISTORY(self), FALSE); g_return_val_if_fail(device_id != NULL, FALSE); /* lazy load */ if (!fu_history_load(self, error)) return FALSE; /* add */ rc = sqlite3_prepare_v2(self->db, "INSERT INTO emulation_tag (device_id) " "VALUES (?1)", -1, &stmt, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to prepare SQL to insert emulation tag: %s", sqlite3_errmsg(self->db)); return FALSE; } sqlite3_bind_text(stmt, 1, device_id, -1, SQLITE_STATIC); return fu_history_stmt_exec(self, stmt, NULL, error); } /** * fu_history_remove_emulation_tag: * @self: a #FuHistory * @error: (nullable): optional return location for an error * * Remove a hash of a device to be tagged for emulation from the database * * Returns: #TRUE for success, #FALSE for failure * * Since: 2.0.1 **/ gboolean fu_history_remove_emulation_tag(FuHistory *self, const gchar *device_id, GError **error) { gint rc; g_autoptr(sqlite3_stmt) stmt = NULL; g_return_val_if_fail(FU_IS_HISTORY(self), FALSE); g_return_val_if_fail(device_id != NULL, FALSE); /* lazy load */ if (!fu_history_load(self, error)) return FALSE; /* remove entries */ rc = sqlite3_prepare_v2(self->db, "DELETE FROM emulation_tag WHERE device_id = ?1;", -1, &stmt, NULL); if (rc != SQLITE_OK) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to prepare SQL to delete emulation tag: %s", sqlite3_errmsg(self->db)); return FALSE; } sqlite3_bind_text(stmt, 1, device_id, -1, SQLITE_STATIC); return fu_history_stmt_exec(self, stmt, NULL, error); } static void fu_history_housekeeping_cb(FuContext *ctx, FuHistory *self) { sqlite3_release_memory(G_MAXINT32); if (self->db != NULL) sqlite3_db_release_memory(self->db); } static void fu_history_dispose(GObject *object) { FuHistory *self = FU_HISTORY(object); if (self->ctx != NULL) g_signal_handlers_disconnect_by_data(self->ctx, self); g_clear_object(&self->ctx); G_OBJECT_CLASS(fu_history_parent_class)->dispose(object); } static void fu_history_class_init(FuHistoryClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_history_finalize; object_class->dispose = fu_history_dispose; } static void fu_history_init(FuHistory *self) { } static void fu_history_finalize(GObject *object) { FuHistory *self = FU_HISTORY(object); if (self->db != NULL) sqlite3_close(self->db); G_OBJECT_CLASS(fu_history_parent_class)->finalize(object); } /** * fu_history_new: * * Creates a new #FuHistory * * Since: 1.0.4 **/ FuHistory * fu_history_new(FuContext *ctx) { FuHistory *self; self = g_object_new(FU_TYPE_PENDING, NULL); self->ctx = g_object_ref(ctx); g_signal_connect(self->ctx, "housekeeping", G_CALLBACK(fu_history_housekeeping_cb), self); return FU_HISTORY(self); } fwupd-2.0.10/src/fu-history.h000066400000000000000000000045701501337203100157710ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-release.h" #define FU_TYPE_PENDING (fu_history_get_type()) G_DECLARE_FINAL_TYPE(FuHistory, fu_history, FU, HISTORY, GObject) FuHistory * fu_history_new(FuContext *ctx); gboolean fu_history_add_device(FuHistory *self, FuDevice *device, FuRelease *release, GError **error) G_GNUC_NON_NULL(1, 2, 3); gboolean fu_history_modify_device(FuHistory *self, FuDevice *device, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_history_modify_device_release(FuHistory *self, FuDevice *device, FuRelease *release, GError **error) G_GNUC_NON_NULL(1, 2, 3); gboolean fu_history_remove_device(FuHistory *self, FuDevice *device, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_history_remove_all(FuHistory *self, GError **error) G_GNUC_NON_NULL(1); FuDevice * fu_history_get_device_by_id(FuHistory *self, const gchar *device_id, GError **error) G_GNUC_NON_NULL(1, 2); GPtrArray * fu_history_get_devices(FuHistory *self, GError **error) G_GNUC_NON_NULL(1); gboolean fu_history_clear_approved_firmware(FuHistory *self, GError **error) G_GNUC_NON_NULL(1); gboolean fu_history_add_approved_firmware(FuHistory *self, const gchar *checksum, GError **error) G_GNUC_NON_NULL(1, 2); GPtrArray * fu_history_get_approved_firmware(FuHistory *self, GError **error) G_GNUC_NON_NULL(1); gboolean fu_history_clear_blocked_firmware(FuHistory *self, GError **error) G_GNUC_NON_NULL(1); gboolean fu_history_add_blocked_firmware(FuHistory *self, const gchar *checksum, GError **error) G_GNUC_NON_NULL(1, 2); GPtrArray * fu_history_get_blocked_firmware(FuHistory *self, GError **error) G_GNUC_NON_NULL(1); gboolean fu_history_add_security_attribute(FuHistory *self, const gchar *security_attr_json, const gchar *hsi_score, GError **error) G_GNUC_NON_NULL(1, 2, 3); GPtrArray * fu_history_get_security_attrs(FuHistory *self, guint limit, GError **error) G_GNUC_NON_NULL(1); gboolean fu_history_add_emulation_tag(FuHistory *self, const gchar *device_id, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_history_remove_emulation_tag(FuHistory *self, const gchar *device_id, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_history_has_emulation_tag(FuHistory *self, const gchar *device_id, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/src/fu-idle.c000066400000000000000000000120621501337203100151730ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuIdle" #include "config.h" #include "fu-idle.h" static void fu_idle_finalize(GObject *obj); struct _FuIdle { GObject parent_instance; GPtrArray *items; /* of FuIdleItem */ guint idle_id; guint timeout; FuIdleInhibit inhibit_old; }; enum { SIGNAL_INHIBIT_CHANGED, SIGNAL_TIMEOUT, SIGNAL_LAST }; static guint signals[SIGNAL_LAST] = {0}; typedef struct { FuIdleInhibit inhibit; gchar *reason; guint32 token; } FuIdleItem; G_DEFINE_TYPE(FuIdle, fu_idle, G_TYPE_OBJECT) static gboolean fu_idle_check_cb(gpointer user_data) { FuIdle *self = FU_IDLE(user_data); g_signal_emit(self, signals[SIGNAL_TIMEOUT], 0); return G_SOURCE_CONTINUE; } static void fu_idle_start(FuIdle *self) { if (self->idle_id != 0) return; if (self->timeout == 0) return; self->idle_id = g_timeout_add_seconds(self->timeout, fu_idle_check_cb, self); } static void fu_idle_stop(FuIdle *self) { if (self->idle_id == 0) return; g_source_remove(self->idle_id); self->idle_id = 0; } static void fu_idle_emit_inhibit_changed(FuIdle *self) { FuIdleInhibit inhibit_global = FU_IDLE_INHIBIT_NONE; fu_idle_reset(self); for (guint i = 0; i < self->items->len; i++) { FuIdleItem *item = g_ptr_array_index(self->items, i); inhibit_global |= item->inhibit; } if (self->inhibit_old != inhibit_global) { g_autofree gchar *inhibit_str = fu_idle_inhibit_to_string(inhibit_global); g_debug("now inhibited: %s", inhibit_str); g_signal_emit(self, signals[SIGNAL_INHIBIT_CHANGED], 0, inhibit_global); self->inhibit_old = inhibit_global; } } void fu_idle_reset(FuIdle *self) { g_return_if_fail(FU_IS_IDLE(self)); fu_idle_stop(self); if (!fu_idle_has_inhibit(self, FU_IDLE_INHIBIT_TIMEOUT)) fu_idle_start(self); } void fu_idle_uninhibit(FuIdle *self, guint32 token) { g_return_if_fail(FU_IS_IDLE(self)); g_return_if_fail(token != 0); for (guint i = 0; i < self->items->len; i++) { FuIdleItem *item = g_ptr_array_index(self->items, i); if (item->token == token) { g_autofree gchar *inhibit_str = fu_idle_inhibit_to_string(item->inhibit); g_debug("uninhibiting: %s by %s", inhibit_str, item->reason); g_ptr_array_remove_index(self->items, i); break; } } fu_idle_emit_inhibit_changed(self); } guint32 fu_idle_inhibit(FuIdle *self, FuIdleInhibit inhibit, const gchar *reason) { FuIdleItem *item; g_autofree gchar *inhibit_str = fu_idle_inhibit_to_string(inhibit); g_return_val_if_fail(FU_IS_IDLE(self), 0); g_return_val_if_fail(inhibit != FU_IDLE_INHIBIT_NONE, 0); g_debug("inhibiting: %s by %s", inhibit_str, reason); item = g_new0(FuIdleItem, 1); item->inhibit = inhibit; item->reason = g_strdup(reason); item->token = g_random_int_range(1, G_MAXINT); /* nocheck:blocked */ g_ptr_array_add(self->items, item); fu_idle_emit_inhibit_changed(self); return item->token; } gboolean fu_idle_has_inhibit(FuIdle *self, FuIdleInhibit inhibit) { g_return_val_if_fail(FU_IS_IDLE(self), FALSE); g_return_val_if_fail(inhibit != FU_IDLE_INHIBIT_NONE, FALSE); for (guint i = 0; i < self->items->len; i++) { FuIdleItem *item = g_ptr_array_index(self->items, i); if (item->inhibit & inhibit) return TRUE; } return FALSE; } void fu_idle_set_timeout(FuIdle *self, guint timeout) { g_return_if_fail(FU_IS_IDLE(self)); g_debug("setting timeout to %us", timeout); self->timeout = timeout; fu_idle_reset(self); } static void fu_idle_item_free(FuIdleItem *item) { g_free(item->reason); g_free(item); } FuIdleLocker * fu_idle_locker_new(FuIdle *self, FuIdleInhibit inhibit, const gchar *reason) { FuIdleLocker *locker = g_new0(FuIdleLocker, 1); locker->idle = g_object_ref(self); locker->token = fu_idle_inhibit(self, inhibit, reason); return locker; } void fu_idle_locker_free(FuIdleLocker *locker) { g_return_if_fail(locker != NULL); fu_idle_uninhibit(locker->idle, locker->token); g_object_unref(locker->idle); g_free(locker); } static void fu_idle_class_init(FuIdleClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_idle_finalize; signals[SIGNAL_INHIBIT_CHANGED] = g_signal_new("inhibit-changed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__UINT, G_TYPE_NONE, 1, G_TYPE_UINT); signals[SIGNAL_TIMEOUT] = g_signal_new("timeout", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); } static void fu_idle_init(FuIdle *self) { self->items = g_ptr_array_new_with_free_func((GDestroyNotify)fu_idle_item_free); } static void fu_idle_finalize(GObject *obj) { FuIdle *self = FU_IDLE(obj); fu_idle_stop(self); g_ptr_array_unref(self->items); G_OBJECT_CLASS(fu_idle_parent_class)->finalize(obj); } FuIdle * fu_idle_new(void) { FuIdle *self; self = g_object_new(FU_TYPE_IDLE, NULL); return FU_IDLE(self); } fwupd-2.0.10/src/fu-idle.h000066400000000000000000000021731501337203100152020ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-engine-struct.h" #define FU_TYPE_IDLE (fu_idle_get_type()) G_DECLARE_FINAL_TYPE(FuIdle, fu_idle, FU, IDLE, GObject) FuIdle * fu_idle_new(void); guint32 fu_idle_inhibit(FuIdle *self, FuIdleInhibit inhibit, const gchar *reason) G_GNUC_NON_NULL(1); void fu_idle_uninhibit(FuIdle *self, guint32 token) G_GNUC_NON_NULL(1); gboolean fu_idle_has_inhibit(FuIdle *self, FuIdleInhibit inhibit) G_GNUC_NON_NULL(1); void fu_idle_set_timeout(FuIdle *self, guint timeout) G_GNUC_NON_NULL(1); void fu_idle_reset(FuIdle *self) G_GNUC_NON_NULL(1); /** * FuIdleLocker: * @idle: A #FuIdle * @token: A #guint32 number * * A locker to prevent daemon from shutting down on its own **/ typedef struct { FuIdle *idle; guint32 token; } FuIdleLocker; FuIdleLocker * fu_idle_locker_new(FuIdle *self, FuIdleInhibit inhibit, const gchar *reason) G_GNUC_NON_NULL(1); void fu_idle_locker_free(FuIdleLocker *locker) G_GNUC_NON_NULL(1); G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuIdleLocker, fu_idle_locker_free) fwupd-2.0.10/src/fu-main-windows.c000066400000000000000000000106001501337203100166660ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later * * - sc create fwupd start="auto" binPath="C:\Program Files (x86)\fwupd\bin\fwupd.exe" * - sc delete fwupd */ #define G_LOG_DOMAIN "FuMain" #include "config.h" #include #include #include "fu-daemon.h" #include "fu-debug.h" static SERVICE_STATUS gSvcStatus = {.dwServiceType = SERVICE_WIN32_OWN_PROCESS, .dwServiceSpecificExitCode = 0}; static SERVICE_STATUS_HANDLE gSvcStatusHandle = 0; static FuDaemon *gDaemon = NULL; static void fu_main_svc_report_status(DWORD dwCurrentState, DWORD dwWin32ExitCode, DWORD dwWaitHint) { static DWORD dwCheckPoint = 1; gSvcStatus.dwCurrentState = dwCurrentState; gSvcStatus.dwWin32ExitCode = dwWin32ExitCode; gSvcStatus.dwWaitHint = dwWaitHint; if (dwCurrentState == SERVICE_START_PENDING) gSvcStatus.dwControlsAccepted = 0; else gSvcStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP; if (dwCurrentState == SERVICE_RUNNING || dwCurrentState == SERVICE_STOPPED) gSvcStatus.dwCheckPoint = 0; else gSvcStatus.dwCheckPoint = dwCheckPoint++; SetServiceStatus(gSvcStatusHandle, &gSvcStatus); } static gboolean fu_main_svc_control_stop_cb(gpointer user_data) { FuDaemon *daemon = FU_DAEMON(user_data); g_autoptr(GError) error = NULL; if (!fu_daemon_stop(daemon, &error)) g_warning("Failed to stop daemon, will wait: %s\n", error->message); return G_SOURCE_REMOVE; } static void fu_main_svc_control_cb(DWORD dwCtrl) { switch (dwCtrl) { case SERVICE_CONTROL_STOP: fu_main_svc_report_status(SERVICE_STOP_PENDING, NO_ERROR, 0); /* there is no user_data, because global state with threads is completely fine */ g_idle_add(fu_main_svc_control_stop_cb, gDaemon); fu_main_svc_report_status(gSvcStatus.dwCurrentState, NO_ERROR, 0); break; default: break; } } static void fu_main_svc_main_cb(DWORD dwArgc, LPSTR *lpszArgv) { g_autoptr(FuDaemon) daemon = gDaemon = fu_daemon_new(); g_autoptr(GError) error = NULL; g_autoptr(GOptionContext) context = g_option_context_new(NULL); /* parse debugging args */ g_option_context_add_group(context, fu_debug_get_option_group()); if (!g_option_context_parse(context, (gint *)&dwArgc, &lpszArgv, &error)) { g_printerr("Failed to parse command line: %s\n", error->message); return; } gSvcStatusHandle = RegisterServiceCtrlHandlerA((LPSTR) "fwupd", fu_main_svc_control_cb); if (gSvcStatusHandle == NULL) { g_warning("RegisterServiceCtrlHandlerA failed [%u]", (guint)GetLastError()); return; } /* set up the daemon, which includes coldplugging devices -- then run it */ fu_main_svc_report_status(SERVICE_START_PENDING, NO_ERROR, 1000); if (!fu_daemon_setup(daemon, FWUPD_DBUS_SOCKET_ADDRESS, &error)) { g_warning("Failed to load daemon: %s", error->message); return; } fu_main_svc_report_status(SERVICE_RUNNING, NO_ERROR, 0); if (!fu_daemon_start(daemon, &error)) { g_warning("Failed to start daemon: %s\n", error->message); return; } fu_main_svc_report_status(SERVICE_STOPPED, NO_ERROR, 0); } static int fu_main_console(int argc, char *argv[]) { g_autoptr(FuDaemon) daemon = gDaemon = fu_daemon_new(); g_autoptr(GError) error = NULL; g_autoptr(GOptionContext) context = g_option_context_new(NULL); /* parse debugging args */ g_option_context_add_group(context, fu_debug_get_option_group()); if (!g_option_context_parse(context, &argc, &argv, &error)) { g_printerr("Failed to parse command line: %s\n", error->message); return EXIT_FAILURE; } /* set up the daemon, which includes coldplugging devices -- then run it */ if (!fu_daemon_setup(daemon, FWUPD_DBUS_SOCKET_ADDRESS, &error)) { g_printerr("Failed to load daemon: %s\n", error->message); return EXIT_FAILURE; } if (!fu_daemon_start(daemon, &error)) { g_printerr("Failed to start daemon: %s\n", error->message); return EXIT_FAILURE; } /* success */ g_message("Daemon ready for requests"); return EXIT_SUCCESS; } int main(int argc, char *argv[]) { SERVICE_TABLE_ENTRYA svc_table[] = {{(LPSTR) "fwupd", fu_main_svc_main_cb}, {NULL, NULL}}; if (!StartServiceCtrlDispatcherA(svc_table)) { /* program is being run as a console application rather than as a service */ if (GetLastError() == ERROR_FAILED_SERVICE_CONTROLLER_CONNECT) return fu_main_console(argc, argv); g_printerr("StartServiceCtrlDispatcherA failed [%u]\n", (guint)GetLastError()); return EXIT_FAILURE; } return EXIT_SUCCESS; } fwupd-2.0.10/src/fu-main.c000066400000000000000000000146251501337203100152110ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuMain" #include "config.h" #include #include #include #include #include #ifdef HAVE_GIO_UNIX #include #endif #ifdef HAVE_SYSTEMD #include #endif #include "fu-daemon.h" #include "fu-debug.h" #ifdef HAVE_GIO_UNIX static gboolean fu_main_sigterm_cb(gpointer user_data) { FuDaemon *daemon = FU_DAEMON(user_data); g_autoptr(GError) error = NULL; g_warning("Received SIGTERM"); if (!fu_daemon_stop(daemon, &error)) g_warning("failed to stop daemon, will wait: %s\n", error->message); return G_SOURCE_CONTINUE; } #endif static gboolean fu_main_timed_exit_cb(gpointer user_data) { FuDaemon *daemon = FU_DAEMON(user_data); g_autoptr(GError) error = NULL; if (!fu_daemon_stop(daemon, &error)) g_warning("failed to stop daemon, will wait: %s\n", error->message); return G_SOURCE_REMOVE; } static void fu_main_argv_changed_cb(GFileMonitor *monitor, GFile *file, GFile *other_file, GFileMonitorEvent event_type, gpointer user_data) { FuDaemon *daemon = FU_DAEMON(user_data); g_autoptr(GError) error = NULL; g_info("binary changed, shutting down"); if (!fu_daemon_stop(daemon, &error)) g_warning("failed to stop daemon, will wait: %s\n", error->message); } static void fu_main_memory_monitor_warning_cb(GMemoryMonitor *memory_monitor, GMemoryMonitorWarningLevel level, FuDaemon *daemon) { g_autoptr(GError) error = NULL; g_info("OOM event, shutting down"); if (!fu_daemon_stop(daemon, &error)) g_warning("failed to stop daemon, will wait: %s\n", error->message); } static gboolean fu_main_is_hypervisor(void) { const gchar *flags; g_autoptr(GHashTable) cpu_attrs = NULL; cpu_attrs = fu_cpu_get_attrs(NULL); if (cpu_attrs == NULL) return FALSE; flags = g_hash_table_lookup(cpu_attrs, "flags"); if (flags == NULL) return FALSE; return g_strstr_len(flags, -1, "hypervisor") != NULL; } static gboolean fu_main_is_container(void) { g_autofree gchar *buf = NULL; gsize bufsz = 0; if (!g_file_get_contents("/proc/1/cgroup", &buf, &bufsz, NULL)) return FALSE; if (g_strstr_len(buf, (gssize)bufsz, "docker") != NULL) return TRUE; if (g_strstr_len(buf, (gssize)bufsz, "lxc") != NULL) return TRUE; return FALSE; } int main(int argc, char *argv[]) { gboolean immediate_exit = FALSE; gboolean timed_exit = FALSE; const gchar *socket_filename = g_getenv("FWUPD_DBUS_SOCKET"); const GOptionEntry options[] = { {"timed-exit", '\0', 0, G_OPTION_ARG_NONE, &timed_exit, /* TRANSLATORS: exit after we've started up, used for user profiling */ N_("Exit after a small delay"), NULL}, {"immediate-exit", '\0', 0, G_OPTION_ARG_NONE, &immediate_exit, /* TRANSLATORS: exit straight away, used for automatic profiling */ N_("Exit after the engine has loaded"), NULL}, {NULL}}; g_autofree gchar *socket_address = NULL; g_autoptr(GError) error = NULL; g_autoptr(GFile) argv0_file = g_file_new_for_path(argv[0]); g_autoptr(GOptionContext) context = NULL; g_autoptr(FuDaemon) daemon = fu_daemon_new(); g_autoptr(GFileMonitor) argv0_monitor = NULL; g_autoptr(GMemoryMonitor) memory_monitor = NULL; setlocale(LC_ALL, ""); bindtextdomain(GETTEXT_PACKAGE, FWUPD_LOCALEDIR); bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8"); textdomain(GETTEXT_PACKAGE); /* TRANSLATORS: program name */ g_set_application_name(_("Firmware Update Daemon")); context = g_option_context_new(NULL); g_option_context_add_main_entries(context, options, NULL); g_option_context_add_group(context, fu_debug_get_option_group()); /* TRANSLATORS: program summary */ g_option_context_set_summary(context, _("Firmware Update D-Bus Service")); if (!g_option_context_parse(context, &argc, &argv, &error)) { g_printerr("Failed to parse command line: %s\n", error->message); return EXIT_FAILURE; } /* detect the machine kind */ if (fu_main_is_hypervisor()) { fu_daemon_set_machine_kind(daemon, FU_DAEMON_MACHINE_KIND_VIRTUAL); } else if (fu_main_is_container()) { fu_daemon_set_machine_kind(daemon, FU_DAEMON_MACHINE_KIND_CONTAINER); } else { fu_daemon_set_machine_kind(daemon, FU_DAEMON_MACHINE_KIND_PHYSICAL); } #ifdef FWUPD_DBUS_SOCKET_ADDRESS /* this is set for macOS and Windows */ if (socket_filename == NULL) socket_filename = g_strdup(FWUPD_DBUS_SOCKET_ADDRESS); #endif /* convert from filename to address, if required */ if (socket_filename != NULL) { if (g_strrstr(socket_filename, "=") == NULL) { #ifndef HAVE_SYSTEMD /* this must be owned by root */ if (g_file_test(socket_filename, G_FILE_TEST_EXISTS)) g_unlink(socket_filename); #endif socket_address = g_strdup_printf("unix:path=%s", socket_filename); } else { socket_address = g_strdup(socket_filename); } } /* set up the daemon, which includes coldplugging devices */ if (!fu_daemon_setup(daemon, socket_address, &error)) { g_printerr("Failed to load daemon: %s\n", error->message); return EXIT_FAILURE; } #ifdef HAVE_GIO_UNIX g_unix_signal_add_full(G_PRIORITY_DEFAULT, SIGTERM, fu_main_sigterm_cb, daemon, NULL); #endif /* HAVE_GIO_UNIX */ /* restart the daemon if the binary gets replaced */ argv0_monitor = g_file_monitor_file(argv0_file, G_FILE_MONITOR_NONE, NULL, &error); g_signal_connect(G_FILE_MONITOR(argv0_monitor), "changed", G_CALLBACK(fu_main_argv_changed_cb), daemon); /* shut down on low memory event as we can just rescan hardware */ memory_monitor = g_memory_monitor_dup_default(); if (memory_monitor != NULL) { g_signal_connect(G_MEMORY_MONITOR(memory_monitor), "low-memory-warning", G_CALLBACK(fu_main_memory_monitor_warning_cb), daemon); } /* Only timeout and close the mainloop if we have specified it * on the command line */ if (immediate_exit) g_idle_add(fu_main_timed_exit_cb, daemon); else if (timed_exit) g_timeout_add_seconds(5, fu_main_timed_exit_cb, daemon); /* wait */ g_message("fwupd %s ready for requests (locale %s)", VERSION, g_getenv("LANG")); if (!fu_daemon_start(daemon, &error)) { g_printerr("Failed to start daemon: %s\n", error->message); return EXIT_FAILURE; } #ifdef HAVE_SYSTEMD /* notify the service manager */ sd_notify(0, "STOPPING=1"); #endif /* cancel to avoid a deadlock */ g_file_monitor_cancel(argv0_monitor); /* success */ return EXIT_SUCCESS; } fwupd-2.0.10/src/fu-plugin-list.c000066400000000000000000000243731501337203100165350ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuPluginList" #include "config.h" #include "fu-plugin-list.h" #include "fu-plugin-private.h" /** * FuPluginList: * * This list of plugins provides a way to get the specific plugin quickly using * a hash table and also any plugin-list specific functionality such as * sorting by dependency order. * * See also: [class@FuPlugin] */ static void fu_plugin_list_finalize(GObject *obj); struct _FuPluginList { GObject parent_instance; GPtrArray *plugins; /* of FuPlugin */ GHashTable *plugins_hash; /* of name : FuPlugin */ }; G_DEFINE_TYPE(FuPluginList, fu_plugin_list, G_TYPE_OBJECT) /** * fu_plugin_list_get_all: * @self: a #FuPluginList * * Gets all the plugins that have been added. * * Returns: (transfer none) (element-type FuPlugin): the plugins * * Since: 1.0.2 **/ GPtrArray * fu_plugin_list_get_all(FuPluginList *self) { g_return_val_if_fail(FU_IS_PLUGIN_LIST(self), NULL); return self->plugins; } static void fu_plugin_list_rules_changed_cb(FuPlugin *plugin, gpointer user_data) { FuPluginList *self = FU_PLUGIN_LIST(user_data); GPtrArray *rules = fu_plugin_get_rules(plugin, FU_PLUGIN_RULE_CONFLICTS); if (rules == NULL) return; for (guint j = 0; j < rules->len; j++) { const gchar *plugin_name = g_ptr_array_index(rules, j); FuPlugin *dep = fu_plugin_list_find_by_name(self, plugin_name, NULL); if (dep == NULL) continue; if (fu_plugin_has_flag(dep, FWUPD_PLUGIN_FLAG_DISABLED)) continue; g_info("late disabling %s as conflicts with %s", fu_plugin_get_name(dep), fu_plugin_get_name(plugin)); fu_plugin_add_flag(dep, FWUPD_PLUGIN_FLAG_DISABLED); } } /** * fu_plugin_list_add: * @self: a #FuPluginList * @plugin: a plugin * * Adds a plugin to the list. The plugin name is used for a hash key and must * be set before calling this function. * * Since: 1.0.2 **/ void fu_plugin_list_add(FuPluginList *self, FuPlugin *plugin) { g_return_if_fail(FU_IS_PLUGIN_LIST(self)); g_return_if_fail(FU_IS_PLUGIN(plugin)); g_return_if_fail(fu_plugin_get_name(plugin) != NULL); g_ptr_array_add(self->plugins, g_object_ref(plugin)); g_hash_table_insert(self->plugins_hash, g_strdup(fu_plugin_get_name(plugin)), g_object_ref(plugin)); g_signal_connect(FU_PLUGIN(plugin), "rules-changed", G_CALLBACK(fu_plugin_list_rules_changed_cb), self); } /** * fu_plugin_list_remove_all: * @self: a #FuPluginList * * Removed all the plugins from the list. * * Since: 2.0.0 **/ void fu_plugin_list_remove_all(FuPluginList *self) { g_return_if_fail(FU_IS_PLUGIN_LIST(self)); for (guint i = 0; i < self->plugins->len; i++) { FuPlugin *plugin = g_ptr_array_index(self->plugins, i); g_signal_handlers_disconnect_by_data(plugin, self); } g_ptr_array_set_size(self->plugins, 0); g_hash_table_remove_all(self->plugins_hash); } /** * fu_plugin_list_find_by_name: * @self: a #FuPluginList * @name: a plugin name, e.g. `dfu` * @error: (nullable): optional return location for an error * * Finds a specific plugin using the plugin name. * * Returns: (transfer none): a plugin, or %NULL * * Since: 1.0.2 **/ FuPlugin * fu_plugin_list_find_by_name(FuPluginList *self, const gchar *name, GError **error) { FuPlugin *plugin; g_return_val_if_fail(FU_IS_PLUGIN_LIST(self), NULL); g_return_val_if_fail(name != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* sanity check */ if (self->plugins->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no plugins loaded"); return NULL; } plugin = g_hash_table_lookup(self->plugins_hash, name); if (plugin == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no plugin %s found", name); return NULL; } return plugin; } static gint fu_plugin_list_sort_cb(gconstpointer a, gconstpointer b) { FuPlugin **pa = (FuPlugin **)a; FuPlugin **pb = (FuPlugin **)b; return fu_plugin_order_compare(*pa, *pb); } /** * fu_plugin_list_depsolve: * @self: a #FuPluginList * @error: (nullable): optional return location for an error * * Depsolves the list of plugins into the correct order. Some plugin methods * are called on all plugins and for some situations the order they are called * may be important. Use fu_plugin_add_rule() to affect the depsolved order * if required. * * Returns: %TRUE for success, or %FALSE if the set could not be depsolved * * Since: 1.0.2 **/ gboolean fu_plugin_list_depsolve(FuPluginList *self, GError **error) { FuPlugin *dep; GPtrArray *deps; gboolean changes; guint dep_loop_check = 0; g_return_val_if_fail(FU_IS_PLUGIN_LIST(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* order by deps */ do { changes = FALSE; for (guint i = 0; i < self->plugins->len; i++) { FuPlugin *plugin = g_ptr_array_index(self->plugins, i); deps = fu_plugin_get_rules(plugin, FU_PLUGIN_RULE_RUN_AFTER); if (deps == NULL) continue; for (guint j = 0; j < deps->len && !changes; j++) { const gchar *plugin_name = g_ptr_array_index(deps, j); dep = fu_plugin_list_find_by_name(self, plugin_name, NULL); if (dep == NULL) { g_info("cannot find plugin '%s' " "requested by '%s'", plugin_name, fu_plugin_get_name(plugin)); continue; } if (fu_plugin_has_flag(dep, FWUPD_PLUGIN_FLAG_DISABLED)) continue; if (fu_plugin_get_order(plugin) <= fu_plugin_get_order(dep)) { g_debug("%s [%u] to be ordered after %s [%u] " "so promoting to [%u]", fu_plugin_get_name(plugin), fu_plugin_get_order(plugin), fu_plugin_get_name(dep), fu_plugin_get_order(dep), fu_plugin_get_order(dep) + 1); fu_plugin_set_order(plugin, fu_plugin_get_order(dep) + 1); changes = TRUE; } } } for (guint i = 0; i < self->plugins->len; i++) { FuPlugin *plugin = g_ptr_array_index(self->plugins, i); deps = fu_plugin_get_rules(plugin, FU_PLUGIN_RULE_RUN_BEFORE); if (deps == NULL) continue; for (guint j = 0; j < deps->len && !changes; j++) { const gchar *plugin_name = g_ptr_array_index(deps, j); dep = fu_plugin_list_find_by_name(self, plugin_name, NULL); if (dep == NULL) { g_info("cannot find plugin '%s' " "requested by '%s'", plugin_name, fu_plugin_get_name(plugin)); continue; } if (fu_plugin_has_flag(dep, FWUPD_PLUGIN_FLAG_DISABLED)) continue; if (fu_plugin_get_order(plugin) >= fu_plugin_get_order(dep)) { g_debug("%s [%u] to be ordered before %s [%u] " "so promoting to [%u]", fu_plugin_get_name(plugin), fu_plugin_get_order(plugin), fu_plugin_get_name(dep), fu_plugin_get_order(dep), fu_plugin_get_order(dep) + 1); fu_plugin_set_order(dep, fu_plugin_get_order(plugin) + 1); changes = TRUE; } } } /* set priority as well */ for (guint i = 0; i < self->plugins->len; i++) { FuPlugin *plugin = g_ptr_array_index(self->plugins, i); deps = fu_plugin_get_rules(plugin, FU_PLUGIN_RULE_BETTER_THAN); if (deps == NULL) continue; for (guint j = 0; j < deps->len && !changes; j++) { const gchar *plugin_name = g_ptr_array_index(deps, j); dep = fu_plugin_list_find_by_name(self, plugin_name, NULL); if (dep == NULL) { g_info("cannot find plugin '%s' " "referenced by '%s'", plugin_name, fu_plugin_get_name(plugin)); continue; } if (fu_plugin_has_flag(dep, FWUPD_PLUGIN_FLAG_DISABLED)) continue; if (fu_plugin_get_priority(plugin) <= fu_plugin_get_priority(dep)) { g_debug("%s [%u] better than %s [%u] " "so bumping to [%u]", fu_plugin_get_name(plugin), fu_plugin_get_priority(plugin), fu_plugin_get_name(dep), fu_plugin_get_priority(dep), fu_plugin_get_priority(dep) + 1); fu_plugin_set_priority(plugin, fu_plugin_get_priority(dep) + 1); changes = TRUE; } } } /* check we're not stuck */ if (dep_loop_check++ > 100) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "got stuck in dep loop"); return FALSE; } } while (changes); /* check for conflicts */ for (guint i = 0; i < self->plugins->len; i++) { FuPlugin *plugin = g_ptr_array_index(self->plugins, i); if (fu_plugin_has_flag(plugin, FWUPD_PLUGIN_FLAG_DISABLED)) continue; deps = fu_plugin_get_rules(plugin, FU_PLUGIN_RULE_CONFLICTS); if (deps == NULL) continue; for (guint j = 0; j < deps->len; j++) { const gchar *plugin_name = g_ptr_array_index(deps, j); dep = fu_plugin_list_find_by_name(self, plugin_name, NULL); if (dep == NULL) continue; if (fu_plugin_has_flag(dep, FWUPD_PLUGIN_FLAG_DISABLED)) continue; g_info("disabling %s as conflicts with %s", fu_plugin_get_name(dep), fu_plugin_get_name(plugin)); fu_plugin_add_flag(dep, FWUPD_PLUGIN_FLAG_DISABLED); } } /* sort by order */ g_ptr_array_sort(self->plugins, fu_plugin_list_sort_cb); return TRUE; } static void fu_plugin_list_dispose(GObject *obj) { FuPluginList *self = FU_PLUGIN_LIST(obj); if (self->plugins != NULL) g_ptr_array_set_size(self->plugins, 0); if (self->plugins_hash != NULL) g_hash_table_remove_all(self->plugins_hash); G_OBJECT_CLASS(fu_plugin_list_parent_class)->dispose(obj); } static void fu_plugin_list_class_init(FuPluginListClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->dispose = fu_plugin_list_dispose; object_class->finalize = fu_plugin_list_finalize; } static void fu_plugin_list_init(FuPluginList *self) { self->plugins = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); self->plugins_hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_object_unref); } static void fu_plugin_list_finalize(GObject *obj) { FuPluginList *self = FU_PLUGIN_LIST(obj); g_ptr_array_unref(self->plugins); g_hash_table_unref(self->plugins_hash); G_OBJECT_CLASS(fu_plugin_list_parent_class)->finalize(obj); } /** * fu_plugin_list_new: * * Creates a new plugin list. * * Returns: (transfer full): a #FuPluginList * * Since: 1.0.2 **/ FuPluginList * fu_plugin_list_new(void) { FuPluginList *self; self = g_object_new(FU_TYPE_PLUGIN_LIST, NULL); return FU_PLUGIN_LIST(self); } fwupd-2.0.10/src/fu-plugin-list.h000066400000000000000000000014001501337203100165240ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_PLUGIN_LIST (fu_plugin_list_get_type()) G_DECLARE_FINAL_TYPE(FuPluginList, fu_plugin_list, FU, PLUGIN_LIST, GObject) FuPluginList * fu_plugin_list_new(void); void fu_plugin_list_add(FuPluginList *self, FuPlugin *plugin) G_GNUC_NON_NULL(1, 2); void fu_plugin_list_remove_all(FuPluginList *self) G_GNUC_NON_NULL(1); GPtrArray * fu_plugin_list_get_all(FuPluginList *self) G_GNUC_NON_NULL(1); FuPlugin * fu_plugin_list_find_by_name(FuPluginList *self, const gchar *name, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_plugin_list_depsolve(FuPluginList *self, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/src/fu-polkit-agent.c000066400000000000000000000136441501337203100166630ustar00rootroot00000000000000/* * Copyright 2011 Lennart Poettering * Copyright 2012 Matthias Klumpp * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuPolkitAgent" #include "config.h" #include #include #include #ifdef __linux__ #include #endif #include #include #include #include #include #include #include #include #include #include #include "fu-polkit-agent.h" struct _FuPolkitAgent { GObject parent_instance; pid_t agent_pid; }; G_DEFINE_TYPE(FuPolkitAgent, fu_polkit_agent, G_TYPE_OBJECT) static int fu_polkit_agent_fork_agent(FuPolkitAgent *self, const char *path, ...) { char **l; gboolean stderr_is_tty; gboolean stdout_is_tty; int fd; pid_t n_agent_pid; pid_t parent_pid; unsigned n, i; va_list ap; g_return_val_if_fail(path != NULL, 0); parent_pid = getpid(); /* spawns a temporary TTY agent, making sure it goes away when * we go away */ n_agent_pid = fork(); if (n_agent_pid < 0) return -errno; if (n_agent_pid != 0) { self->agent_pid = n_agent_pid; return 0; } #ifdef __linux__ /* make sure the agent goes away when the parent dies */ if (prctl(PR_SET_PDEATHSIG, SIGTERM) < 0) _exit(EXIT_FAILURE); #endif /* check whether our parent died before we were able * to set the death signal */ if (getppid() != parent_pid) _exit(EXIT_SUCCESS); /* TODO: it might be more clean to close all FDs so we don't leak them to the agent */ stdout_is_tty = isatty(STDOUT_FILENO); stderr_is_tty = isatty(STDERR_FILENO); if (!stdout_is_tty || !stderr_is_tty) { /* Detach from stdout/stderr. and reopen * /dev/tty for them. This is important to * ensure that when systemctl is started via * popen() or a similar call that expects to * read EOF we actually do generate EOF and * not delay this indefinitely by because we * keep an unused copy of stdin around. */ fd = open("/dev/tty", O_WRONLY); if (fd < 0) { g_critical("Failed to open /dev/tty: %m"); _exit(EXIT_FAILURE); } if (!stdout_is_tty) dup2(fd, STDOUT_FILENO); if (!stderr_is_tty) dup2(fd, STDERR_FILENO); if (fd > 2) close(fd); } /* count arguments */ va_start(ap, path); for (n = 0; va_arg(ap, char *); n++) ; va_end(ap); /* allocate strv */ l = alloca(sizeof(char *) * (n + 1)); /* fill in arguments */ va_start(ap, path); for (i = 0; i <= n; i++) l[i] = va_arg(ap, char *); va_end(ap); execv(path, l); _exit(EXIT_FAILURE); } static int fu_polkit_agent_close_nointr(int fd) { g_return_val_if_fail(fd >= 0, -1); for (;;) { int r; r = close(fd); if (r >= 0) return r; if (errno != EINTR) return -errno; } } static void fu_polkit_agent_close_nointr_nofail(int fd) { int saved_errno = errno; /* cannot fail, and guarantees errno is unchanged */ if (fu_polkit_agent_close_nointr(fd) != 0) { errno = EBADFD; return; } errno = saved_errno; } static int fu_polkit_agent_fd_wait_for_event(int fd, int event, uint64_t t) { struct pollfd pollfd = {0}; int r; pollfd.fd = fd; pollfd.events = event; r = poll(&pollfd, 1, t == (uint64_t)-1 ? -1 : (int)(t / 1000)); if (r < 0) return -errno; if (r == 0) return 0; return pollfd.revents; } static int fu_polkit_agent_wait_for_terminate(pid_t pid) { g_return_val_if_fail(pid >= 1, 0); for (;;) { int status; if (waitpid(pid, &status, 0) < 0) { if (errno == EINTR) continue; return -errno; } return 0; } } gboolean fu_polkit_agent_open(FuPolkitAgent *self, GError **error) { int r; int pipe_fd[2]; g_autofree gchar *notify_fd = NULL; g_autofree gchar *pkttyagent_fn = NULL; g_return_val_if_fail(FU_IS_POLKIT_AGENT(self), FALSE); /* already open */ if (self->agent_pid > 0) return TRUE; /* find binary */ pkttyagent_fn = g_find_program_in_path("pkttyagent"); if (pkttyagent_fn == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "missing pkttyagent"); return FALSE; } /* check STDIN here, not STDOUT, since this is about input, not output */ if (!isatty(STDIN_FILENO)) return TRUE; if (pipe(pipe_fd) < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to create pipe: %s", g_strerror(errno)); return FALSE; } /* fork pkttyagent */ notify_fd = g_strdup_printf("%i", pipe_fd[1]); r = fu_polkit_agent_fork_agent(self, pkttyagent_fn, pkttyagent_fn, "--notify-fd", notify_fd, "--fallback", NULL); if (r < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to fork TTY ask password agent: %s", g_strerror(-r)); fu_polkit_agent_close_nointr_nofail(pipe_fd[1]); fu_polkit_agent_close_nointr_nofail(pipe_fd[0]); return FALSE; } /* close the writing side, because that is the one for the agent */ fu_polkit_agent_close_nointr_nofail(pipe_fd[1]); /* wait until the agent closes the fd */ fu_polkit_agent_fd_wait_for_event(pipe_fd[0], POLLHUP, (uint64_t)-1); fu_polkit_agent_close_nointr_nofail(pipe_fd[0]); return TRUE; } static void fu_polkit_agent_init(FuPolkitAgent *self) { } static void fu_polkit_agent_finalize(GObject *obj) { FuPolkitAgent *self = FU_POLKIT_AGENT(obj); /* inform agent that we are done */ if (self->agent_pid > 0) { kill(self->agent_pid, SIGTERM); kill(self->agent_pid, SIGCONT); fu_polkit_agent_wait_for_terminate(self->agent_pid); } G_OBJECT_CLASS(fu_polkit_agent_parent_class)->finalize(obj); } static void fu_polkit_agent_class_init(FuPolkitAgentClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_polkit_agent_finalize; } FuPolkitAgent * fu_polkit_agent_new(void) { FuPolkitAgent *self; self = g_object_new(FU_TYPE_POLKIT_AGENT, NULL); return FU_POLKIT_AGENT(self); } fwupd-2.0.10/src/fu-polkit-agent.h000066400000000000000000000010121501337203100166520ustar00rootroot00000000000000/* * Copyright 2011 Lennart Poettering * Copyright 2012 Matthias Klumpp * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_POLKIT_AGENT (fu_polkit_agent_get_type()) G_DECLARE_FINAL_TYPE(FuPolkitAgent, fu_polkit_agent, FU, POLKIT_AGENT, GObject) FuPolkitAgent * fu_polkit_agent_new(void); gboolean fu_polkit_agent_open(FuPolkitAgent *self, GError **error); fwupd-2.0.10/src/fu-polkit-authority.c000066400000000000000000000100021501337203100175760ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuPolkitAuthority" #include "config.h" #ifdef HAVE_POLKIT #include #endif #include "fu-polkit-authority.h" struct _FuPolkitAuthority { GObject parent_instance; #ifdef HAVE_POLKIT PolkitAuthority *pkauthority; #endif }; G_DEFINE_TYPE(FuPolkitAuthority, fu_polkit_authority, G_TYPE_OBJECT) gboolean fu_polkit_authority_check_finish(FuPolkitAuthority *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(FU_IS_POLKIT_AUTHORITY(self), FALSE); g_return_val_if_fail(g_task_is_valid(res, self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); return g_task_propagate_boolean(G_TASK(res), error); } #ifdef HAVE_POLKIT static void fu_polkit_authority_check_cb(GObject *source_object, GAsyncResult *res, gpointer user_data) { g_autoptr(GError) error_local = NULL; g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(PolkitAuthorizationResult) auth = NULL; auth = polkit_authority_check_authorization_finish(POLKIT_AUTHORITY(source_object), res, &error_local); if (auth == NULL) { g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_AUTH_FAILED, "Could not check for auth: %s", error_local->message); return; } /* did not auth */ if (!polkit_authorization_result_get_is_authorized(auth)) { g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_AUTH_FAILED, "Failed to obtain auth"); return; } /* success */ g_task_return_boolean(task, TRUE); } #endif void fu_polkit_authority_check(FuPolkitAuthority *self, const gchar *sender, const gchar *action_id, FuPolkitAuthorityCheckFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_autoptr(GTask) task = g_task_new(self, cancellable, callback, user_data); #ifdef HAVE_POLKIT PolkitCheckAuthorizationFlags pkflags = POLKIT_CHECK_AUTHORIZATION_FLAGS_NONE; g_autofree gchar *owner = polkit_authority_get_owner(self->pkauthority); #endif g_return_if_fail(FU_IS_POLKIT_AUTHORITY(self)); g_return_if_fail(action_id != NULL); g_return_if_fail(callback != NULL); #ifdef HAVE_POLKIT if (owner != NULL && sender != NULL) { g_autoptr(PolkitSubject) pksubject = polkit_system_bus_name_new(sender); if (flags & FU_POLKIT_AUTHORITY_CHECK_FLAG_ALLOW_USER_INTERACTION) pkflags |= POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION; polkit_authority_check_authorization(self->pkauthority, pksubject, action_id, NULL, /* details */ pkflags, cancellable, fu_polkit_authority_check_cb, g_steal_pointer(&task)); return; } #endif /* fallback to the caller being euid=0 */ if ((flags & FU_POLKIT_AUTHORITY_CHECK_FLAG_USER_IS_TRUSTED) == 0) { g_task_return_new_error(task, FWUPD_ERROR, FWUPD_ERROR_AUTH_FAILED, "Failed to obtain auth as not trusted user"); return; } /* success */ g_task_return_boolean(task, TRUE); } static void fu_polkit_authority_init(FuPolkitAuthority *self) { } gboolean fu_polkit_authority_load(FuPolkitAuthority *self, GError **error) { #ifdef HAVE_POLKIT self->pkauthority = polkit_authority_get_sync(NULL, error); if (self->pkauthority == NULL) { g_prefix_error(error, "failed to load authority: "); return FALSE; } #endif return TRUE; } static void fu_polkit_authority_finalize(GObject *obj) { #ifdef HAVE_POLKIT FuPolkitAuthority *self = FU_POLKIT_AUTHORITY(obj); if (self->pkauthority != NULL) g_object_unref(self->pkauthority); #endif G_OBJECT_CLASS(fu_polkit_authority_parent_class)->finalize(obj); } static void fu_polkit_authority_class_init(FuPolkitAuthorityClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_polkit_authority_finalize; } FuPolkitAuthority * fu_polkit_authority_new(void) { FuPolkitAuthority *self; self = g_object_new(FU_TYPE_POLKIT_AUTHORITY, NULL); return FU_POLKIT_AUTHORITY(self); } fwupd-2.0.10/src/fu-polkit-authority.h000066400000000000000000000020511501337203100176100ustar00rootroot00000000000000/* * Copyright 2022 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_POLKIT_AUTHORITY (fu_polkit_authority_get_type()) G_DECLARE_FINAL_TYPE(FuPolkitAuthority, fu_polkit_authority, FU, POLKIT_AUTHORITY, GObject) typedef enum { FU_POLKIT_AUTHORITY_CHECK_FLAG_NONE = 0, FU_POLKIT_AUTHORITY_CHECK_FLAG_ALLOW_USER_INTERACTION = 1 << 0, FU_POLKIT_AUTHORITY_CHECK_FLAG_USER_IS_TRUSTED = 1 << 1, } FuPolkitAuthorityCheckFlags; FuPolkitAuthority * fu_polkit_authority_new(void); gboolean fu_polkit_authority_load(FuPolkitAuthority *self, GError **error) G_GNUC_NON_NULL(1); void fu_polkit_authority_check(FuPolkitAuthority *self, const gchar *sender, const gchar *action_id, FuPolkitAuthorityCheckFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) G_GNUC_NON_NULL(1, 3); gboolean fu_polkit_authority_check_finish(FuPolkitAuthority *self, GAsyncResult *res, GError **error) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/src/fu-polkit-test.c000066400000000000000000000010761501337203100165400ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "fu-polkit-agent.h" static void fu_polkit_agent_func(void) { gboolean ret; g_autoptr(FuPolkitAgent) polkit_agent = fu_polkit_agent_new(); g_autoptr(GError) error = NULL; ret = fu_polkit_agent_open(polkit_agent, &error); g_assert_no_error(error); g_assert_true(ret); } int main(int argc, char **argv) { g_test_init(&argc, &argv, NULL); g_test_add_func("/fwupd/polkit-agent", fu_polkit_agent_func); return g_test_run(); } fwupd-2.0.10/src/fu-release-common.c000066400000000000000000000011711501337203100171630ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuSpawn" #include "config.h" #include "fu-release-common.h" /** * fu_release_uri_get_scheme: * @uri: valid URI, e.g. `https://foo.bar/baz` * * Returns the USI scheme for the given URI. * * Returns: scheme value, or %NULL if invalid, e.g. `https` **/ gchar * fu_release_uri_get_scheme(const gchar *uri) { gchar *tmp; g_return_val_if_fail(uri != NULL, NULL); tmp = g_strstr_len(uri, -1, ":"); if (tmp == NULL || tmp[0] == '\0') return NULL; return g_utf8_strdown(uri, tmp - uri); } fwupd-2.0.10/src/fu-release-common.h000066400000000000000000000003321501337203100171660ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include gchar * fu_release_uri_get_scheme(const gchar *uri) G_GNUC_NON_NULL(1); fwupd-2.0.10/src/fu-release.c000066400000000000000000001236311501337203100157030ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuRelease" #include "config.h" #include "fu-device-private.h" #include "fu-release-common.h" #include "fu-release.h" /** * FuRelease: * * An installable entity that has been loaded and verified for a specific device. * * See also: [class@FwupdRelease] */ struct _FuRelease { FwupdRelease parent_instance; FuEngineRequest *request; FuDevice *device; FwupdRemote *remote; FuEngineConfig *config; GInputStream *stream; gchar *update_request_id; gchar *device_version_old; GPtrArray *soft_reqs; /* nullable, element-type XbNode */ GPtrArray *hard_reqs; /* nullable, element-type XbNode */ guint64 priority; }; G_DEFINE_TYPE(FuRelease, fu_release, FWUPD_TYPE_RELEASE) static gboolean fu_release_ensure_trust_flags(FuRelease *self, XbNode *rel, GError **error); gchar * fu_release_to_string(FuRelease *self) { const guint idt = 1; g_autoptr(GString) str = g_string_new(NULL); g_return_val_if_fail(FU_IS_RELEASE(self), NULL); /* parent */ fwupd_codec_add_string(FWUPD_CODEC(self), 0, str); /* instance */ if (self->request != NULL) fwupd_codec_add_string(FWUPD_CODEC(self->request), idt, str); if (self->device != NULL) fwupd_codec_string_append(str, idt, "Device", fu_device_get_id(self->device)); fwupd_codec_string_append(str, idt, "DeviceVersionOld", self->device_version_old); if (self->remote != NULL) fwupd_codec_string_append(str, idt, "Remote", fwupd_remote_get_id(self->remote)); fwupd_codec_string_append_bool(str, idt, "HasConfig", self->config != NULL); fwupd_codec_string_append_bool(str, idt, "HasStream", self->stream != NULL); fwupd_codec_string_append(str, idt, "UpdateRequestId", self->update_request_id); if (self->soft_reqs != NULL) fwupd_codec_string_append_hex(str, idt, "SoftReqs", self->soft_reqs->len); if (self->hard_reqs != NULL) fwupd_codec_string_append_hex(str, idt, "HardReqs", self->hard_reqs->len); fwupd_codec_string_append_hex(str, idt, "Priority", self->priority); return g_string_free(g_steal_pointer(&str), FALSE); } /** * fu_release_set_request: * @self: a #FuRelease * @request: (nullable): a #FuEngineRequest * * Sets the user request which created this operation. **/ void fu_release_set_request(FuRelease *self, FuEngineRequest *request) { g_return_if_fail(FU_IS_RELEASE(self)); g_set_object(&self->request, request); } /** * fu_release_get_request: * @self: a #FuRelease * * Gets the user request which created this operation. * * Returns: (transfer none) (nullable): request **/ FuEngineRequest * fu_release_get_request(FuRelease *self) { g_return_val_if_fail(FU_IS_RELEASE(self), NULL); return self->request; } /** * fu_release_get_device_version_old: * @self: a #FuRelease * * Gets the original [before update] device version. * * Returns: a string value, or %NULL if never set. **/ const gchar * fu_release_get_device_version_old(FuRelease *self) { g_return_val_if_fail(FU_IS_RELEASE(self), NULL); return self->device_version_old; } static void fu_release_set_device_version_old(FuRelease *self, const gchar *device_version_old) { g_return_if_fail(FU_IS_RELEASE(self)); /* not changed */ if (g_strcmp0(self->device_version_old, device_version_old) == 0) return; g_free(self->device_version_old); self->device_version_old = g_strdup(device_version_old); } /** * fu_release_set_device: * @self: a #FuRelease * @device: (nullable): a #FuDevice * * Sets the device this release should use when checking requirements. **/ void fu_release_set_device(FuRelease *self, FuDevice *device) { g_return_if_fail(FU_IS_RELEASE(self)); /* make tests easier */ fu_device_convert_instance_ids(device); g_set_object(&self->device, device); fu_release_set_device_version_old(self, fu_device_get_version(device)); } /** * fu_release_get_device: * @self: a #FuRelease * * Gets the device this release was loaded for. * * Returns: (transfer none) (nullable): device **/ FuDevice * fu_release_get_device(FuRelease *self) { g_return_val_if_fail(FU_IS_RELEASE(self), NULL); return self->device; } /** * fu_release_get_stream: * @self: a #FuRelease * * Gets the firmware stream to use when installing this release. * * Returns: (transfer none) (nullable): data **/ GInputStream * fu_release_get_stream(FuRelease *self) { g_return_val_if_fail(FU_IS_RELEASE(self), NULL); return self->stream; } /** * fu_release_get_soft_reqs: * @self: a #FuRelease * * Gets the additional soft requirements that need to be checked in the engine. * * Returns: (transfer none) (nullable) (element-type XbNode): nodes **/ GPtrArray * fu_release_get_soft_reqs(FuRelease *self) { g_return_val_if_fail(FU_IS_RELEASE(self), NULL); return self->soft_reqs; } /** * fu_release_get_soft_reqs: * @self: a #FuRelease * * Gets the additional hard requirements that need to be checked in the engine. * * Returns: (transfer none) (nullable) (element-type XbNode): nodes **/ GPtrArray * fu_release_get_hard_reqs(FuRelease *self) { g_return_val_if_fail(FU_IS_RELEASE(self), NULL); return self->hard_reqs; } /** * fu_release_get_update_request_id: * @self: a #FuRelease * * Gets the update request ID as specified from `LVFS::UpdateRequestId`. * * Returns: a string value, or %NULL if never set. **/ const gchar * fu_release_get_update_request_id(FuRelease *self) { g_return_val_if_fail(FU_IS_RELEASE(self), NULL); return self->update_request_id; } static void fu_release_set_update_request_id(FuRelease *self, const gchar *update_request_id) { g_return_if_fail(FU_IS_RELEASE(self)); /* not changed */ if (g_strcmp0(self->update_request_id, update_request_id) == 0) return; g_free(self->update_request_id); self->update_request_id = g_strdup(update_request_id); } /** * fu_release_set_remote: * @self: a #FuRelease * @remote: (nullable): a #FwupdRemote * * Sets the remote this release should use when loading. This is typically set by the engine by *watching the `remote-id` property to be set and then querying the internal cached list of *`FuRemote`s. **/ void fu_release_set_remote(FuRelease *self, FwupdRemote *remote) { g_return_if_fail(FU_IS_RELEASE(self)); g_set_object(&self->remote, remote); } /** * fu_release_set_config: * @self: a #FuRelease * @config: (nullable): a #FuEngineConfig * * Sets the config to use when loading. The config may be used for things like ordering attributes *like protocol priority. **/ void fu_release_set_config(FuRelease *self, FuEngineConfig *config) { g_return_if_fail(FU_IS_RELEASE(self)); g_set_object(&self->config, config); } static gchar * fu_release_get_localized_xpath(FuRelease *self, const gchar *element) { GString *xpath = g_string_new(element); const gchar *locale = NULL; /* optional; not set in tests */ if (self->request != NULL) locale = fu_engine_request_get_locale(self->request); /* prefer the users locale if set */ if (locale != NULL) { g_autofree gchar *xpath_locale = NULL; xpath_locale = g_strdup_printf("%s[@xml:lang='%s']|", element, locale); g_string_prepend(xpath, xpath_locale); } return g_string_free(xpath, FALSE); } /* convert hex and decimal versions to dotted style */ static gchar * fu_release_get_release_version(FuRelease *self, const gchar *version, GError **error) { FwupdVersionFormat fmt = fu_device_get_version_format(self->device); guint64 ver_uint32; g_autoptr(GError) error_local = NULL; /* already dotted notation */ if (g_strstr_len(version, -1, ".") != NULL) return g_strdup(version); /* don't touch my version! */ if (fmt == FWUPD_VERSION_FORMAT_PLAIN || fmt == FWUPD_VERSION_FORMAT_UNKNOWN) return g_strdup(version); /* parse as integer */ if (!fu_strtoull(version, &ver_uint32, 1, G_MAXUINT32, FU_INTEGER_BASE_AUTO, &error_local)) { g_warning("invalid release version %s: %s", version, error_local->message); return g_strdup(version); } /* convert to dotted decimal */ return fu_version_from_uint32((guint32)ver_uint32, fmt); } static gboolean fu_release_load_test_result(FuRelease *self, XbNode *n, GError **error) { const gchar *tmp; g_autoptr(FwupdReport) report = fwupd_report_new(); g_autoptr(GPtrArray) custom = NULL; g_autoptr(XbNode) os = NULL; g_autoptr(XbNode) vendor_name = NULL; tmp = xb_node_get_attr(n, "date"); if (tmp != NULL) { g_autoptr(GDateTime) dt = NULL; g_autofree gchar *iso8601 = g_strdup_printf("%sT00:00:00Z", tmp); dt = g_date_time_new_from_iso8601(iso8601, NULL); if (dt != NULL) fwupd_report_set_created(report, g_date_time_to_unix(dt)); } tmp = xb_node_query_text(n, "device", NULL); if (tmp != NULL) fwupd_report_set_device_name(report, tmp); tmp = xb_node_query_text(n, "previous_version", NULL); if (tmp != NULL) { fwupd_report_set_version_old(report, tmp); if (fu_version_compare(fu_release_get_version(self), tmp, FWUPD_VERSION_FORMAT_UNKNOWN) > 0) { fwupd_report_add_flag(report, FWUPD_REPORT_FLAG_IS_UPGRADE); } } vendor_name = xb_node_query_first(n, "vendor_name", NULL); if (vendor_name != NULL) { guint64 vendor_id = xb_node_get_attr_as_uint(vendor_name, "id"); fwupd_report_set_vendor(report, xb_node_get_text(vendor_name)); if (vendor_id != G_MAXUINT64) fwupd_report_set_vendor_id(report, vendor_id); } os = xb_node_query_first(n, "os", NULL); if (os != NULL) { tmp = xb_node_get_attr(os, "version"); if (tmp != NULL) fwupd_report_set_distro_version(report, tmp); tmp = xb_node_get_attr(os, "variant"); if (tmp != NULL) fwupd_report_set_distro_variant(report, tmp); fwupd_report_set_distro_id(report, xb_node_get_text(os)); } if (fu_release_get_remote_id(self) != NULL) fwupd_report_set_remote_id(report, fu_release_get_remote_id(self)); custom = xb_node_query(n, "custom/value", 0, NULL); if (custom != NULL) { for (guint i = 0; i < custom->len; i++) { XbNode *c = g_ptr_array_index(custom, i); if (g_strcmp0(xb_node_get_attr(c, "key"), "FromOEM") == 0) { fwupd_report_add_flag(report, FWUPD_REPORT_FLAG_FROM_OEM); continue; } if (xb_node_get_attr(c, "key") == NULL || xb_node_get_text(c) == NULL) { g_debug("ignoring metadata: %s=%s", xb_node_get_attr(c, "key"), xb_node_get_text(c)); continue; } fwupd_report_add_metadata_item(report, xb_node_get_attr(c, "key"), xb_node_get_text(c)); } } /* success */ fwupd_release_add_report(FWUPD_RELEASE(self), report); return TRUE; } static gboolean fu_release_load_artifact(FuRelease *self, XbNode *artifact, GError **error) { const gchar *filename; guint64 size; g_autoptr(GPtrArray) locations = NULL; g_autoptr(GPtrArray) checksums = NULL; g_autoptr(GPtrArray) test_result = NULL; /* filename */ filename = xb_node_query_text(artifact, "filename", NULL); if (filename != NULL && !g_str_has_suffix(filename, ".cab")) { /* some firmware archives was signed with where the * checksums were the *content* checksums, not the *container* checksum */ g_debug("ignoring non-binary artifact entry: %s", filename); return TRUE; } if (filename != NULL) fu_release_set_filename(self, filename); /* location */ locations = xb_node_query(artifact, "location", 0, NULL); if (locations != NULL) { for (guint i = 0; i < locations->len; i++) { XbNode *n = g_ptr_array_index(locations, i); /* check the scheme is allowed */ if (self->config != NULL) { g_autofree gchar *scheme = fu_release_uri_get_scheme(xb_node_get_text(n)); if (scheme != NULL) { guint prio = fu_engine_config_get_uri_scheme_prio(self->config, scheme); if (prio == G_MAXUINT) continue; } } /* build the complete URI */ if (self->remote != NULL) { g_autofree gchar *uri = NULL; uri = fwupd_remote_build_firmware_uri(self->remote, xb_node_get_text(n), NULL); if (uri != NULL) { fwupd_release_add_location(FWUPD_RELEASE(self), uri); continue; } } fwupd_release_add_location(FWUPD_RELEASE(self), xb_node_get_text(n)); } } /* checksum */ checksums = xb_node_query(artifact, "checksum", 0, NULL); if (checksums != NULL) { for (guint i = 0; i < checksums->len; i++) { XbNode *n = g_ptr_array_index(checksums, i); fwupd_release_add_checksum(FWUPD_RELEASE(self), xb_node_get_text(n)); } } /* test results */ test_result = xb_node_query(artifact, "testing/test_result", 0, NULL); if (test_result != NULL) { for (guint i = 0; i < test_result->len; i++) { XbNode *n = g_ptr_array_index(test_result, i); if (!fu_release_load_test_result(self, n, error)) return FALSE; } } /* size */ size = xb_node_query_text_as_uint(artifact, "size[@type='installed']", NULL); if (size != G_MAXUINT64) fwupd_release_set_size(FWUPD_RELEASE(self), size); /* success */ return TRUE; } static gint fu_release_scheme_compare_cb(gconstpointer a, gconstpointer b, gpointer user_data) { FuRelease *self = FU_RELEASE(user_data); const gchar *location1 = *((const gchar **)a); const gchar *location2 = *((const gchar **)b); g_autofree gchar *scheme1 = fu_release_uri_get_scheme(location1); g_autofree gchar *scheme2 = fu_release_uri_get_scheme(location2); guint prio1 = fu_engine_config_get_uri_scheme_prio(self->config, scheme1); guint prio2 = fu_engine_config_get_uri_scheme_prio(self->config, scheme2); if (prio1 < prio2) return -1; if (prio1 > prio2) return 1; return 0; } static gboolean fu_release_check_requirements_version_check(FuRelease *self, GError **error) { if (self->hard_reqs != NULL) { for (guint i = 0; i < self->hard_reqs->len; i++) { XbNode *req = g_ptr_array_index(self->hard_reqs, i); if (g_strcmp0(xb_node_get_element(req), "firmware") == 0 && xb_node_get_text(req) == NULL) { return TRUE; } } } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no firmware requirement"); return FALSE; } static gchar * fu_release_verfmts_to_string(GPtrArray *verfmts) { GString *str = g_string_new(NULL); for (guint i = 0; i < verfmts->len; i++) { XbNode *verfmt = g_ptr_array_index(verfmts, i); const gchar *tmp = xb_node_get_text(verfmt); g_string_append_printf(str, "%s;", tmp); } if (str->len > 0) g_string_truncate(str, str->len - 1); return g_string_free(str, FALSE); } static gboolean fu_release_check_verfmt_compatible(FuRelease *self, FwupdVersionFormat fmt_rel) { FwupdVersionFormat fmt_dev = fu_device_get_version_format(self->device); if (fmt_dev == FWUPD_VERSION_FORMAT_BCD && fmt_rel == FWUPD_VERSION_FORMAT_PAIR) return TRUE; return fmt_dev == fmt_rel; } static gboolean fu_release_check_verfmt(FuRelease *self, GPtrArray *verfmts, FwupdInstallFlags flags, GError **error) { FwupdVersionFormat fmt_dev = fu_device_get_version_format(self->device); g_autofree gchar *verfmts_str = NULL; /* no device format */ if (fmt_dev == FWUPD_VERSION_FORMAT_UNKNOWN && (flags & FWUPD_INSTALL_FLAG_FORCE) == 0) { verfmts_str = fu_release_verfmts_to_string(verfmts); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "release version format '%s' but no device version format", verfmts_str); return FALSE; } /* compare all version formats */ for (guint i = 0; i < verfmts->len; i++) { XbNode *verfmt = g_ptr_array_index(verfmts, i); const gchar *tmp = xb_node_get_text(verfmt); FwupdVersionFormat fmt_rel = fwupd_version_format_from_string(tmp); if (fu_release_check_verfmt_compatible(self, fmt_rel)) return TRUE; } verfmts_str = fu_release_verfmts_to_string(verfmts); if ((flags & FWUPD_INSTALL_FLAG_FORCE) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Firmware version formats were different, " "device was '%s' and release is '%s'", fwupd_version_format_to_string(fmt_dev), verfmts_str); return FALSE; } g_warning("ignoring version format difference %s:%s", fwupd_version_format_to_string(fmt_dev), verfmts_str); return TRUE; } /* these can all be done without the daemon */ static gboolean fu_release_check_requirements(FuRelease *self, XbNode *component, FwupdInstallFlags install_flags, GError **error) { const gchar *branch_new; const gchar *branch_old; const gchar *protocol; gboolean matches_guid = FALSE; g_autoptr(GError) error_local = NULL; g_autoptr(GPtrArray) provides = NULL; /* does this component provide a GUID the device has */ provides = xb_node_query(component, "provides/firmware[@type='flashed']", 0, &error_local); if (provides == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "No supported devices found: %s", error_local->message); return FALSE; } for (guint i = 0; i < provides->len; i++) { XbNode *provide = g_ptr_array_index(provides, i); if (fu_device_has_guid(self->device, xb_node_get_text(provide))) { matches_guid = TRUE; break; } } if (!matches_guid) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "No supported devices found"); return FALSE; } /* device requires a version check */ if (fu_device_has_flag(self->device, FWUPD_DEVICE_FLAG_VERSION_CHECK_REQUIRED)) { if (!fu_release_check_requirements_version_check(self, error)) { g_prefix_error(error, "device requires firmware with a version check: "); return FALSE; } } /* does the protocol match */ protocol = xb_node_query_text(component, "custom/value[@key='LVFS::UpdateProtocol']", NULL); if (fu_device_get_protocols(self->device)->len != 0 && protocol != NULL && !fu_device_has_protocol(self->device, protocol) && (install_flags & FWUPD_INSTALL_FLAG_FORCE) == 0) { g_autofree gchar *str = NULL; str = fu_strjoin("|", fu_device_get_protocols(self->device)); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Device %s does not support %s, only %s", fu_device_get_name(self->device), protocol, str); return FALSE; } /* check the device is not locked */ if (fu_device_has_flag(self->device, FWUPD_DEVICE_FLAG_LOCKED)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Device %s [%s] is locked", fu_device_get_name(self->device), fu_device_get_id(self->device)); return FALSE; } /* check the branch is not switching */ branch_new = xb_node_query_text(component, "branch", NULL); branch_old = fu_device_get_branch(self->device); if ((install_flags & FWUPD_INSTALL_FLAG_ALLOW_BRANCH_SWITCH) == 0 && g_strcmp0(branch_old, branch_new) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Device %s [%s] would switch firmware branch from %s to %s", fu_device_get_name(self->device), fu_device_get_id(self->device), branch_old != NULL ? branch_old : "default", branch_new != NULL ? branch_new : "default"); return FALSE; } /* no update abilities */ if (!fu_engine_request_has_feature_flag(self->request, FWUPD_FEATURE_FLAG_SHOW_PROBLEMS) && !fu_device_has_flag(self->device, FWUPD_DEVICE_FLAG_UPDATABLE)) { g_autoptr(GString) str = g_string_new(NULL); g_string_append_printf(str, "Device %s [%s] does not currently allow updates", fu_device_get_name(self->device), fu_device_get_id(self->device)); if (fu_device_get_update_error(self->device) != NULL) { g_string_append_printf(str, ": %s", fu_device_get_update_error(self->device)); } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, str->str); return FALSE; } /* success */ return TRUE; } /** * fu_release_check_version: * @self: a #FuRelease * @component: (not nullable): a #XbNode * @install_flags: a #FwupdInstallFlags, e.g. %FWUPD_INSTALL_FLAG_FORCE * @error: (nullable): optional return location for an error * * Checks the component against this release, specifically that the device can be upgraded with this * new firmware version. * * Returns: %TRUE if the requirements passed **/ gboolean fu_release_check_version(FuRelease *self, XbNode *component, FwupdInstallFlags install_flags, GError **error) { const gchar *version; const gchar *version_lowest; gint vercmp; g_return_val_if_fail(FU_IS_RELEASE(self), FALSE); g_return_val_if_fail(XB_IS_NODE(component), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* skip */ if (self->device == NULL) return TRUE; if (self->request != NULL && fu_engine_request_has_flag(self->request, FU_ENGINE_REQUEST_FLAG_NO_REQUIREMENTS)) { return TRUE; } /* ensure device has a version */ version = fu_device_get_version(self->device); if (version == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Device %s [%s] has no firmware version", fu_device_get_name(self->device), fu_device_get_id(self->device)); return FALSE; } /* check the version formats match if set in the release */ if ((install_flags & FWUPD_INSTALL_FLAG_FORCE) == 0 && (install_flags & FWUPD_INSTALL_FLAG_ALLOW_BRANCH_SWITCH) == 0) { g_autoptr(GPtrArray) verfmts = xb_node_query(component, "custom/value[@key='LVFS::VersionFormat']", 0, NULL); if (verfmts != NULL) { if (!fu_release_check_verfmt(self, verfmts, install_flags, error)) return FALSE; } } /* compare to the lowest supported version, if it exists */ version_lowest = fu_device_get_version_lowest(self->device); if (version_lowest != NULL && fu_version_compare(version_lowest, fu_release_get_version(self), fu_device_get_version_format(self->device)) > 0 && (install_flags & FWUPD_INSTALL_FLAG_FORCE) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Specified firmware is older than the minimum " "required version '%s < %s'", fu_release_get_version(self), version_lowest); return FALSE; } /* is this a downgrade or re-install */ vercmp = fu_version_compare(version, fu_release_get_version(self), fu_device_get_version_format(self->device)); if (fu_device_has_flag(self->device, FWUPD_DEVICE_FLAG_ONLY_VERSION_UPGRADE) && vercmp > 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Device only supports version upgrades"); return FALSE; } if (vercmp == 0 && (install_flags & FWUPD_INSTALL_FLAG_ALLOW_REINSTALL) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_VERSION_SAME, "Specified firmware is already installed '%s'", fu_release_get_version(self)); return FALSE; } if (vercmp > 0) fu_release_add_flag(self, FWUPD_RELEASE_FLAG_IS_DOWNGRADE); if (fu_release_has_flag(self, FWUPD_RELEASE_FLAG_IS_DOWNGRADE) && (install_flags & FWUPD_INSTALL_FLAG_ALLOW_OLDER) == 0 && (install_flags & FWUPD_INSTALL_FLAG_ALLOW_BRANCH_SWITCH) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_VERSION_NEWER, "Specified firmware is older than installed '%s < %s'", fu_release_get_version(self), version); return FALSE; } /* success */ return TRUE; } void fu_release_set_priority(FuRelease *self, guint64 priority) { g_return_if_fail(FU_IS_RELEASE(self)); self->priority = priority; } guint64 fu_release_get_priority(FuRelease *self) { g_return_val_if_fail(FU_IS_RELEASE(self), 0); return self->priority; } /** * fu_release_load: * @self: a #FuRelease * @cabinet: a #FuCabinet * @component: (not nullable): a #XbNode * @rel_optional: (nullable): a #XbNode * @install_flags: a #FwupdInstallFlags, e.g. %FWUPD_INSTALL_FLAG_FORCE * @error: (nullable): optional return location for an error * * Loads then checks any requirements of this release. This will typically involve checking * that the device can accept the component (the GUIDs match) and that the device can be * upgraded with this firmware version. * * Returns: %TRUE if the release was loaded and the requirements passed **/ gboolean fu_release_load(FuRelease *self, FuCabinet *cabinet, XbNode *component, XbNode *rel_optional, FwupdInstallFlags install_flags, GError **error) { const gchar *tmp; guint64 tmp64; GBytes *blob_basename; g_autofree gchar *name_xpath = NULL; g_autofree gchar *namevs_xpath = NULL; g_autofree gchar *summary_xpath = NULL; g_autofree gchar *description_xpath = NULL; g_autoptr(GPtrArray) cats = NULL; g_autoptr(GPtrArray) tags = NULL; g_autoptr(GPtrArray) issues = NULL; g_autoptr(XbNode) artifact = NULL; g_autoptr(XbNode) description = NULL; g_autoptr(XbNode) rel = NULL; g_autoptr(GError) error_soft = NULL; g_autoptr(GError) error_hard = NULL; g_return_val_if_fail(FU_IS_RELEASE(self), FALSE); g_return_val_if_fail(cabinet == NULL || FU_IS_CABINET(cabinet), FALSE); g_return_val_if_fail(XB_IS_NODE(component), FALSE); g_return_val_if_fail(rel_optional == NULL || XB_IS_NODE(rel_optional), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* set from the component */ tmp = xb_node_query_text(component, "id", NULL); if (tmp != NULL) fwupd_release_set_appstream_id(FWUPD_RELEASE(self), tmp); tmp = xb_node_query_text(component, "url[@type='homepage']", NULL); if (tmp != NULL) fwupd_release_set_homepage(FWUPD_RELEASE(self), tmp); tmp = xb_node_query_text(component, "project_license", NULL); if (tmp != NULL) fwupd_release_set_license(FWUPD_RELEASE(self), tmp); name_xpath = fu_release_get_localized_xpath(self, "name"); tmp = xb_node_query_text(component, name_xpath, NULL); if (tmp != NULL) fwupd_release_set_name(FWUPD_RELEASE(self), tmp); summary_xpath = fu_release_get_localized_xpath(self, "summary"); tmp = xb_node_query_text(component, summary_xpath, NULL); if (tmp != NULL) fwupd_release_set_summary(FWUPD_RELEASE(self), tmp); namevs_xpath = fu_release_get_localized_xpath(self, "name_variant_suffix"); tmp = xb_node_query_text(component, namevs_xpath, NULL); if (tmp != NULL) fwupd_release_set_name_variant_suffix(FWUPD_RELEASE(self), tmp); tmp = xb_node_query_text(component, "branch", NULL); if (tmp != NULL) fwupd_release_set_branch(FWUPD_RELEASE(self), tmp); tmp = xb_node_query_text(component, "developer_name", NULL); if (tmp != NULL) fwupd_release_set_vendor(FWUPD_RELEASE(self), tmp); /* use default release */ if (rel_optional == NULL) { g_autoptr(GError) error_local = NULL; g_autoptr(XbQuery) query = NULL; query = xb_query_new_full(xb_node_get_silo(component), "releases/release", XB_QUERY_FLAG_FORCE_NODE_CACHE, error); if (query == NULL) return FALSE; rel = xb_node_query_first_full(component, query, &error_local); if (rel == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to get default release: %s", error_local->message); return FALSE; } } else { rel = g_object_ref(rel_optional); } /* find the remote */ tmp = xb_node_query_text(component, "../custom/value[@key='fwupd::RemoteId']", NULL); if (tmp != NULL) fwupd_release_set_remote_id(FWUPD_RELEASE(self), tmp); tmp = xb_node_query_text(component, "../custom/value[@key='LVFS::Distributor']", NULL); if (tmp != NULL && g_str_has_prefix(tmp, "community")) fwupd_release_add_flag(FWUPD_RELEASE(self), FWUPD_RELEASE_FLAG_IS_COMMUNITY); /* use the metadata to set the device attributes */ if (!fu_release_ensure_trust_flags(self, rel, error)) return FALSE; /* per-release priority wins, but fallback to per-component priority */ tmp64 = xb_node_get_attr_as_uint(rel, "priority"); if (tmp64 == G_MAXUINT64) tmp64 = xb_node_get_attr_as_uint(component, "priority"); if (tmp64 != G_MAXUINT64) fu_release_set_priority(self, tmp64); /* the version is fixed up with the device format */ tmp = xb_node_get_attr(rel, "version"); if (tmp == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "version unset"); return FALSE; } if (self->device != NULL) { g_autofree gchar *version_rel = NULL; version_rel = fu_release_get_release_version(self, tmp, error); if (version_rel == NULL) return FALSE; fwupd_release_set_version(FWUPD_RELEASE(self), version_rel); } else { fwupd_release_set_version(FWUPD_RELEASE(self), tmp); } /* optional release ID -- currently a integer but maybe namespaced in the future */ fwupd_release_set_id(FWUPD_RELEASE(self), xb_node_get_attr(rel, "id")); /* this is the more modern way to do this */ artifact = xb_node_query_first(rel, "artifacts/artifact[@type='binary']", NULL); if (artifact != NULL) { if (!fu_release_load_artifact(self, artifact, error)) return FALSE; } description_xpath = fu_release_get_localized_xpath(self, "description"); description = xb_node_query_first(rel, description_xpath, NULL); if (description != NULL) { g_autofree gchar *xml = NULL; g_autoptr(GString) str = NULL; xml = xb_node_export(description, XB_NODE_EXPORT_FLAG_ONLY_CHILDREN, NULL); str = g_string_new(xml); if (self->device != NULL && self->request != NULL && fu_device_has_flag(self->device, FWUPD_DEVICE_FLAG_AFFECTS_FDE) && !fu_engine_request_has_feature_flag(self->request, FWUPD_FEATURE_FLAG_FDE_WARNING)) { g_string_prepend( str, "

    Some of the platform secrets may be invalidated when " "updating this firmware. Please ensure you have the volume " "recovery key before continuing.

    "); } if (fwupd_release_has_flag(FWUPD_RELEASE(self), FWUPD_RELEASE_FLAG_IS_COMMUNITY) && self->request != NULL && !fu_engine_request_has_feature_flag(self->request, FWUPD_FEATURE_FLAG_COMMUNITY_TEXT)) { g_string_prepend( str, "

    This firmware is provided by LVFS community " "members and is not provided (or supported) by the original " "hardware vendor. " "Installing this update may also void any device warranty.

    "); } if (str->len > 0) fwupd_release_set_description(FWUPD_RELEASE(self), str->str); } if (fwupd_release_get_locations(FWUPD_RELEASE(self))->len == 0) { tmp = xb_node_query_text(rel, "location", NULL); if (tmp != NULL) { g_autofree gchar *uri = NULL; if (self->remote != NULL) uri = fwupd_remote_build_firmware_uri(self->remote, tmp, NULL); if (uri == NULL) uri = g_strdup(tmp); fwupd_release_add_location(FWUPD_RELEASE(self), uri); } } if (fwupd_release_get_locations(FWUPD_RELEASE(self))->len == 0 && self->remote != NULL && fwupd_remote_get_kind(self->remote) == FWUPD_REMOTE_KIND_DIRECTORY) { tmp = xb_node_query_text(component, "../custom/value[@key='fwupd::FilenameCache']", NULL); if (tmp != NULL) { g_autofree gchar *uri = g_strdup_printf("file://%s", tmp); fwupd_release_add_location(FWUPD_RELEASE(self), uri); } } if (fu_release_get_filename(self) == NULL) { tmp = xb_node_query_attr(rel, "checksum[@target='container']", "filename", NULL); if (tmp != NULL) fu_release_set_filename(self, tmp); } tmp = xb_node_query_text(rel, "url[@type='details']", NULL); if (tmp != NULL) fwupd_release_set_details_url(FWUPD_RELEASE(self), tmp); tmp = xb_node_query_text(rel, "url[@type='source']", NULL); if (tmp != NULL) fwupd_release_set_source_url(FWUPD_RELEASE(self), tmp); tmp = xb_node_query_text(rel, "url[@type='sbom']", NULL); if (tmp != NULL) fwupd_release_set_sbom_url(FWUPD_RELEASE(self), tmp); if (fwupd_release_get_checksums(FWUPD_RELEASE(self))->len == 0) { g_autoptr(GPtrArray) checksums = NULL; checksums = xb_node_query(rel, "checksum[@target='container']", 0, NULL); if (checksums != NULL) { for (guint i = 0; i < checksums->len; i++) { XbNode *n = g_ptr_array_index(checksums, i); if (xb_node_get_text(n) == NULL) continue; fwupd_release_add_checksum(FWUPD_RELEASE(self), xb_node_get_text(n)); } } } if (fwupd_release_get_size(FWUPD_RELEASE(self)) == 0) { tmp64 = xb_node_query_text_as_uint(rel, "size[@type='installed']", NULL); if (tmp64 != G_MAXUINT64) fwupd_release_set_size(FWUPD_RELEASE(self), tmp64); } if (fwupd_release_get_size(FWUPD_RELEASE(self)) == 0) { GBytes *sz = xb_node_get_data(rel, "fwupd::ReleaseSize"); if (sz != NULL) { const guint64 *sizeptr = g_bytes_get_data(sz, NULL); fwupd_release_set_size(FWUPD_RELEASE(self), *sizeptr); } } tmp = xb_node_get_attr(rel, "urgency"); if (tmp != NULL) fwupd_release_set_urgency(FWUPD_RELEASE(self), fwupd_release_urgency_from_string(tmp)); tmp64 = xb_node_get_attr_as_uint(rel, "install_duration"); if (tmp64 != G_MAXUINT64) fwupd_release_set_install_duration(FWUPD_RELEASE(self), tmp64); tmp64 = xb_node_get_attr_as_uint(rel, "timestamp"); if (tmp64 != G_MAXUINT64) fwupd_release_set_created(FWUPD_RELEASE(self), tmp64); cats = xb_node_query(component, "categories/category", 0, NULL); if (cats != NULL) { for (guint i = 0; i < cats->len; i++) { XbNode *n = g_ptr_array_index(cats, i); fwupd_release_add_category(FWUPD_RELEASE(self), xb_node_get_text(n)); } } tags = xb_node_query(component, "tags/tag[@namespace=$'lvfs']", 0, NULL); if (tags != NULL) { for (guint i = 0; i < tags->len; i++) { XbNode *tag = g_ptr_array_index(tags, i); fwupd_release_add_tag(FWUPD_RELEASE(self), xb_node_get_text(tag)); } } issues = xb_node_query(rel, "issues/issue", 0, NULL); if (issues != NULL) { for (guint i = 0; i < issues->len; i++) { XbNode *n = g_ptr_array_index(issues, i); fwupd_release_add_issue(FWUPD_RELEASE(self), xb_node_get_text(n)); } } tmp = xb_node_query_text(component, "screenshots/screenshot/caption", NULL); if (tmp != NULL) fwupd_release_set_detach_caption(FWUPD_RELEASE(self), tmp); tmp = xb_node_query_text(component, "screenshots/screenshot/image", NULL); if (tmp != NULL) { if (self->remote != NULL) { g_autofree gchar *img = NULL; img = fwupd_remote_build_firmware_uri(self->remote, tmp, error); if (img == NULL) return FALSE; fwupd_release_set_detach_image(FWUPD_RELEASE(self), img); } else { fwupd_release_set_detach_image(FWUPD_RELEASE(self), tmp); } } tmp = xb_node_query_text(component, "custom/value[@key='LVFS::UpdateProtocol']", NULL); if (tmp != NULL) fwupd_release_set_protocol(FWUPD_RELEASE(self), tmp); tmp = xb_node_query_text(component, "custom/value[@key='LVFS::UpdateMessage']", NULL); if (tmp != NULL) fwupd_release_set_update_message(FWUPD_RELEASE(self), tmp); tmp = xb_node_query_text(component, "custom/value[@key='LVFS::UpdateImage']", NULL); if (tmp != NULL) { if (self->remote != NULL) { g_autofree gchar *img = NULL; img = fwupd_remote_build_firmware_uri(self->remote, tmp, error); if (img == NULL) return FALSE; fwupd_release_set_update_image(FWUPD_RELEASE(self), img); } else { fwupd_release_set_update_image(FWUPD_RELEASE(self), tmp); } } tmp = xb_node_query_text(component, "custom/value[@key='LVFS::UpdateRequestId']", NULL); if (tmp != NULL) fu_release_set_update_request_id(self, tmp); /* hard and soft requirements */ self->hard_reqs = xb_node_query(component, "requires/*", 0, &error_hard); if (self->hard_reqs == NULL) { if (!g_error_matches(error_hard, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) && !g_error_matches(error_hard, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) { g_propagate_error(error, g_steal_pointer(&error_hard)); fwupd_error_convert(error); return FALSE; } } self->soft_reqs = xb_node_query(component, "suggests/*|recommends/*", 0, &error_soft); if (self->soft_reqs == NULL) { if (!g_error_matches(error_soft, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) && !g_error_matches(error_soft, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT)) { g_propagate_error(error, g_steal_pointer(&error_soft)); fwupd_error_convert(error); return FALSE; } } /* get per-release firmware stream */ blob_basename = xb_node_get_data(rel, "fwupd::FirmwareBasename"); if (cabinet != NULL && blob_basename != NULL) { const gchar *basename = (const gchar *)g_bytes_get_data(blob_basename, NULL); g_autoptr(FuFirmware) img = NULL; img = fu_firmware_get_image_by_id(FU_FIRMWARE(cabinet), basename, error); if (img == NULL) { g_prefix_error(error, "failed to find %s: ", basename); return FALSE; } self->stream = fu_firmware_get_stream(img, error); if (self->stream == NULL) return FALSE; } /* to build the firmware */ tmp = g_object_get_data(G_OBJECT(component), "fwupd::BuilderScript"); if (tmp != NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "fwupd::BuilderScript is no longer supported"); return FALSE; } /* sort the locations by scheme */ if (self->config != NULL) { g_ptr_array_sort_with_data(fwupd_release_get_locations(FWUPD_RELEASE(self)), fu_release_scheme_compare_cb, self); } /* check requirements for device */ if (self->device != NULL && self->request != NULL && !fu_engine_request_has_flag(self->request, FU_ENGINE_REQUEST_FLAG_NO_REQUIREMENTS)) { if (!fu_release_check_requirements(self, component, install_flags, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_release_ensure_trust_flags(FuRelease *self, XbNode *rel, GError **error) { GBytes *blob; g_return_val_if_fail(FU_IS_RELEASE(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* in the self tests */ if (g_getenv("FWUPD_SELF_TEST") != NULL) { fu_release_add_flag(self, FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD); fu_release_add_flag(self, FWUPD_RELEASE_FLAG_TRUSTED_METADATA); return TRUE; } /* populated from an actual cab archive */ blob = g_object_get_data(G_OBJECT(rel), "fwupd::ReleaseFlags"); if (blob != NULL) { FwupdReleaseFlags flags = FWUPD_RELEASE_FLAG_NONE; if (!fu_memcpy_safe((guint8 *)&flags, sizeof(flags), 0x0, /* dst */ g_bytes_get_data(blob, NULL), g_bytes_get_size(blob), 0x0, /* src */ sizeof(flags), error)) return FALSE; if (flags & FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD) fu_release_add_flag(self, FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD); if (flags & FWUPD_RELEASE_FLAG_TRUSTED_METADATA) fu_release_add_flag(self, FWUPD_RELEASE_FLAG_TRUSTED_METADATA); } /* do not require signatures for anything installed to the immutable datadir */ if (fu_release_get_flags(self) == FWUPD_RELEASE_FLAG_NONE && self->remote != NULL) { g_debug("remote %s has kind=%s and so marking as trusted", fwupd_remote_get_id(self->remote), fwupd_remote_kind_to_string(fwupd_remote_get_kind(self->remote))); if (fwupd_remote_get_kind(self->remote) == FWUPD_REMOTE_KIND_LOCAL || fwupd_remote_get_kind(self->remote) == FWUPD_REMOTE_KIND_DIRECTORY) { fu_release_add_flag(self, FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD); fu_release_add_flag(self, FWUPD_RELEASE_FLAG_TRUSTED_METADATA); } else { fu_release_add_flag(self, FWUPD_RELEASE_FLAG_TRUSTED_METADATA); } } /* success */ return TRUE; } /** * fu_release_get_action_id: * @self: a #FuEngine * * Gets the PolicyKit action ID to use for the install operation. * * Returns: string, e.g. `org.freedesktop.fwupd.update-internal-trusted` **/ const gchar * fu_release_get_action_id(FuRelease *self) { /* relax authentication checks for removable devices */ if (!fu_device_has_flag(self->device, FWUPD_DEVICE_FLAG_INTERNAL)) { if (fu_release_has_flag(self, FWUPD_RELEASE_FLAG_IS_DOWNGRADE)) { if (fu_release_has_flag(self, FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD)) return "org.freedesktop.fwupd.downgrade-hotplug-trusted"; return "org.freedesktop.fwupd.downgrade-hotplug"; } if (fu_release_has_flag(self, FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD)) return "org.freedesktop.fwupd.update-hotplug-trusted"; return "org.freedesktop.fwupd.update-hotplug"; } /* internal device */ if (fu_release_has_flag(self, FWUPD_RELEASE_FLAG_IS_DOWNGRADE)) { if (fu_release_has_flag(self, FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD)) return "org.freedesktop.fwupd.downgrade-internal-trusted"; return "org.freedesktop.fwupd.downgrade-internal"; } if (fu_release_has_flag(self, FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD)) return "org.freedesktop.fwupd.update-internal-trusted"; return "org.freedesktop.fwupd.update-internal"; } /** * fu_release_compare: * @release1: first task to compare. * @release2: second task to compare. * * Compares two releases. * * Returns: 1, 0 or -1 if @release1 is greater, equal, or less than @release2, respectively. **/ gint fu_release_compare(FuRelease *release1, FuRelease *release2) { FuDevice *device1 = fu_release_get_device(release1); FuDevice *device2 = fu_release_get_device(release2); /* device order, lower is better */ if (device1 != NULL && device2 != NULL && device1 != device2) { if (fu_device_get_order(device1) < fu_device_get_order(device2)) return -1; if (fu_device_get_order(device1) > fu_device_get_order(device2)) return 1; } /* release priority, higher is better */ if (release1->priority > release2->priority) return -1; if (release1->priority < release2->priority) return 1; /* remote priority, higher is better */ if (release1->remote != NULL && release2->remote != NULL) { if (fwupd_remote_get_priority(release1->remote) > fwupd_remote_get_priority(release2->remote)) return -1; if (fwupd_remote_get_priority(release1->remote) < fwupd_remote_get_priority(release2->remote)) return 1; } /* FWUPD_DEVICE_FLAG_INSTALL_ALL_RELEASES has to be from oldest to newest */ return fu_version_compare(fu_release_get_version(release1), fu_release_get_version(release2), fu_device_get_version_format(device1)); } static void fu_release_init(FuRelease *self) { fu_release_set_flags(self, FWUPD_RELEASE_FLAG_NONE); } static void fu_release_finalize(GObject *obj) { FuRelease *self = FU_RELEASE(obj); g_free(self->update_request_id); g_free(self->device_version_old); if (self->request != NULL) g_object_unref(self->request); if (self->device != NULL) g_object_unref(self->device); if (self->remote != NULL) g_object_unref(self->remote); if (self->config != NULL) g_object_unref(self->config); if (self->stream != NULL) g_object_unref(self->stream); if (self->soft_reqs != NULL) g_ptr_array_unref(self->soft_reqs); if (self->hard_reqs != NULL) g_ptr_array_unref(self->hard_reqs); G_OBJECT_CLASS(fu_release_parent_class)->finalize(obj); } static void fu_release_class_init(FuReleaseClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_release_finalize; } FuRelease * fu_release_new(void) { FuRelease *self; self = g_object_new(FU_TYPE_RELEASE, NULL); return FU_RELEASE(self); } fwupd-2.0.10/src/fu-release.h000066400000000000000000000104711501337203100157050ustar00rootroot00000000000000/* * Copyright 2018 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-cabinet.h" #include "fu-engine-config.h" #include "fu-engine-request.h" #define FU_TYPE_RELEASE (fu_release_get_type()) G_DECLARE_FINAL_TYPE(FuRelease, fu_release, FU, RELEASE, FwupdRelease) FuRelease * fu_release_new(void); #define fu_release_get_appstream_id(r) fwupd_release_get_appstream_id(FWUPD_RELEASE(r)) #define fu_release_get_filename(r) fwupd_release_get_filename(FWUPD_RELEASE(r)) #define fu_release_get_version(r) fwupd_release_get_version(FWUPD_RELEASE(r)) #define fu_release_get_branch(r) fwupd_release_get_branch(FWUPD_RELEASE(r)) #define fu_release_get_remote_id(r) fwupd_release_get_remote_id(FWUPD_RELEASE(r)) #define fu_release_get_checksums(r) fwupd_release_get_checksums(FWUPD_RELEASE(r)) #define fu_release_get_reports(r) fwupd_release_get_reports(FWUPD_RELEASE(r)) #define fu_release_get_flags(r) fwupd_release_get_flags(FWUPD_RELEASE(r)) #define fu_release_add_flag(r, v) fwupd_release_add_flag(FWUPD_RELEASE(r), v) #define fu_release_has_flag(r, v) fwupd_release_has_flag(FWUPD_RELEASE(r), v) #define fu_release_add_tag(r, v) fwupd_release_add_tag(FWUPD_RELEASE(r), v) #define fu_release_add_metadata(r, v) fwupd_release_add_metadata(FWUPD_RELEASE(r), v) #define fu_release_set_branch(r, v) fwupd_release_set_branch(FWUPD_RELEASE(r), v) #define fu_release_set_description(r, v) fwupd_release_set_description(FWUPD_RELEASE(r), v) #define fu_release_set_flags(r, v) fwupd_release_set_flags(FWUPD_RELEASE(r), v) #define fu_release_set_filename(r, v) fwupd_release_set_filename(FWUPD_RELEASE(r), v) #define fu_release_add_metadata_item(r, k, v) \ fwupd_release_add_metadata_item(FWUPD_RELEASE(r), k, v) #define fu_release_set_version(r, v) fwupd_release_set_version(FWUPD_RELEASE(r), v) #define fu_release_set_protocol(r, v) fwupd_release_set_protocol(FWUPD_RELEASE(r), v) #define fu_release_set_appstream_id(r, v) fwupd_release_set_appstream_id(FWUPD_RELEASE(r), v) #define fu_release_add_checksum(r, v) fwupd_release_add_checksum(FWUPD_RELEASE(r), v) #define fu_release_set_id(r, v) fwupd_release_set_id(FWUPD_RELEASE(r), v) #define fu_release_set_remote_id(r, v) fwupd_release_set_remote_id(FWUPD_RELEASE(r), v) #define fu_release_set_filename(r, v) fwupd_release_set_filename(FWUPD_RELEASE(r), v) #define fu_release_get_metadata_item(r, v) fwupd_release_get_metadata_item(FWUPD_RELEASE(r), v) #define fu_release_get_protocol(r) fwupd_release_get_protocol(FWUPD_RELEASE(r)) #define fu_release_get_metadata(r) fwupd_release_get_metadata(FWUPD_RELEASE(r)) #define fu_release_get_id(r) fwupd_release_get_id(FWUPD_RELEASE(r)) gchar * fu_release_to_string(FuRelease *self) G_GNUC_NON_NULL(1); FuDevice * fu_release_get_device(FuRelease *self) G_GNUC_NON_NULL(1); GInputStream * fu_release_get_stream(FuRelease *self) G_GNUC_NON_NULL(1); FuEngineRequest * fu_release_get_request(FuRelease *self) G_GNUC_NON_NULL(1); GPtrArray * fu_release_get_soft_reqs(FuRelease *self) G_GNUC_NON_NULL(1); GPtrArray * fu_release_get_hard_reqs(FuRelease *self) G_GNUC_NON_NULL(1); const gchar * fu_release_get_update_request_id(FuRelease *self) G_GNUC_NON_NULL(1); const gchar * fu_release_get_device_version_old(FuRelease *self) G_GNUC_NON_NULL(1); void fu_release_set_request(FuRelease *self, FuEngineRequest *request) G_GNUC_NON_NULL(1); void fu_release_set_device(FuRelease *self, FuDevice *device) G_GNUC_NON_NULL(1); void fu_release_set_remote(FuRelease *self, FwupdRemote *remote) G_GNUC_NON_NULL(1); void fu_release_set_config(FuRelease *self, FuEngineConfig *config) G_GNUC_NON_NULL(1); gboolean fu_release_load(FuRelease *self, FuCabinet *cabinet, XbNode *component, XbNode *rel, FwupdInstallFlags flags, GError **error) G_GNUC_NON_NULL(1, 3); gboolean fu_release_check_version(FuRelease *self, XbNode *component, FwupdInstallFlags install_flags, GError **error) G_GNUC_NON_NULL(1, 2); const gchar * fu_release_get_action_id(FuRelease *self) G_GNUC_NON_NULL(1); gint fu_release_compare(FuRelease *release1, FuRelease *release2) G_GNUC_NON_NULL(1, 2); void fu_release_set_priority(FuRelease *self, guint64 priority) G_GNUC_NON_NULL(1); guint64 fu_release_get_priority(FuRelease *self) G_GNUC_NON_NULL(1); fwupd-2.0.10/src/fu-remote-list.c000066400000000000000000000600511501337203100165230ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuRemoteList" #include "config.h" #include #include #include #ifdef HAVE_INOTIFY_H #include #include #endif #include "fwupd-remote-private.h" #include "fu-remote-list.h" #include "fu-remote.h" enum { SIGNAL_CHANGED, SIGNAL_ADDED, SIGNAL_LAST }; static guint signals[SIGNAL_LAST] = {0}; static gboolean fu_remote_list_reload(FuRemoteList *self, GError **error); static void fu_remote_list_finalize(GObject *obj); struct _FuRemoteList { GObject parent_instance; GPtrArray *array; /* (element-type FwupdRemote) */ GPtrArray *monitors; /* (element-type GFileMonitor) */ gboolean testing_remote; gboolean fix_metadata_uri; XbSilo *silo; gchar *lvfs_metadata_format; }; G_DEFINE_TYPE(FuRemoteList, fu_remote_list, G_TYPE_OBJECT) static void fu_remote_list_emit_changed(FuRemoteList *self) { g_debug("::remote_list changed"); g_signal_emit(self, signals[SIGNAL_CHANGED], 0); } static void fu_remote_list_emit_added(FuRemoteList *self, FwupdRemote *remote) { g_debug("::remote_list changed"); g_signal_emit(self, signals[SIGNAL_ADDED], 0, remote); } void fu_remote_list_set_lvfs_metadata_format(FuRemoteList *self, const gchar *lvfs_metadata_format) { g_return_if_fail(FU_IS_REMOTE_LIST(self)); g_return_if_fail(lvfs_metadata_format != NULL); if (g_strcmp0(lvfs_metadata_format, self->lvfs_metadata_format) == 0) return; g_free(self->lvfs_metadata_format); self->lvfs_metadata_format = g_strdup(lvfs_metadata_format); } static void fu_remote_list_monitor_changed_cb(GFileMonitor *monitor, GFile *file, GFile *other_file, GFileMonitorEvent event_type, gpointer user_data) { FuRemoteList *self = FU_REMOTE_LIST(user_data); g_autoptr(GError) error = NULL; g_autofree gchar *filename = g_file_get_path(file); g_info("%s changed, reloading all remotes", filename); if (!fu_remote_list_reload(self, &error)) g_warning("failed to rescan remotes: %s", error->message); fu_remote_list_emit_changed(self); } static guint64 _fwupd_remote_get_mtime(FwupdRemote *remote) { g_autoptr(GFile) file = NULL; g_autoptr(GFileInfo) info = NULL; file = g_file_new_for_path(fwupd_remote_get_filename_cache(remote)); if (!g_file_query_exists(file, NULL)) return G_MAXUINT64; info = g_file_query_info(file, G_FILE_ATTRIBUTE_TIME_MODIFIED, G_FILE_QUERY_INFO_NONE, NULL, NULL); if (info == NULL) return G_MAXUINT64; return g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_TIME_MODIFIED); } /* GLib only returns the very unhelpful "Unable to find default local file monitor type" * when /proc/sys/fs/inotify/max_user_instances is set too low; detect this and set a proper * error prefix to aid debugging when the daemon fails to start */ static void fu_remote_list_fixup_inotify_error(GError **error) { #ifdef HAVE_INOTIFY_H int fd; int wd; const gchar *fn = "/proc/sys/fs/inotify/max_user_instances"; fd = inotify_init(); if (fd == -1) { g_prefix_error(error, "Could not initialize inotify, check %s: ", fn); return; } wd = inotify_add_watch(fd, fn, IN_MODIFY); if (wd < 0) { if (errno == ENOSPC) g_prefix_error(error, "No space for inotify, check %s: ", fn); } else { inotify_rm_watch(fd, wd); } close(fd); #endif } static gboolean fu_remote_list_add_inotify(FuRemoteList *self, const gchar *filename, GError **error) { GFileMonitor *monitor; g_autoptr(GFile) file = g_file_new_for_path(filename); /* set up a notify watch */ monitor = g_file_monitor(file, G_FILE_MONITOR_NONE, NULL, error); if (monitor == NULL) { fu_remote_list_fixup_inotify_error(error); return FALSE; } g_signal_connect(G_FILE_MONITOR(monitor), "changed", G_CALLBACK(fu_remote_list_monitor_changed_cb), self); g_ptr_array_add(self->monitors, monitor); return TRUE; } static GString * _fwupd_remote_get_agreement_default(FwupdRemote *self, GError **error) { GString *str = g_string_new(NULL); /* this is designed as a fallback; the actual warning should ideally * come from the LVFS instance that is serving the remote */ g_string_append_printf(str, "

    %s

    ", /* TRANSLATORS: show the user a warning */ _("Your distributor may not have verified any of " "the firmware updates for compatibility with your " "system or connected devices.")); g_string_append_printf(str, "

    %s

    ", /* TRANSLATORS: show the user a warning */ _("Enabling this remote is done at your own risk.")); return str; } static GString * _fwupd_remote_get_agreement_for_app(FwupdRemote *self, XbNode *component, GError **error) { g_autofree gchar *tmp = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(XbNode) n = NULL; /* manually find the first agreement section */ n = xb_node_query_first(component, "agreement/agreement_section/description/*", &error_local); if (n == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "No agreement description found: %s", error_local->message); return NULL; } tmp = xb_node_export(n, XB_NODE_EXPORT_FLAG_INCLUDE_SIBLINGS, error); if (tmp == NULL) return NULL; return g_string_new(tmp); } static gchar * _fwupd_remote_build_component_id(FwupdRemote *remote) { return g_strdup_printf("org.freedesktop.fwupd.remotes.%s", fwupd_remote_get_id(remote)); } static gchar * fu_remote_list_get_last_ext(const gchar *filename) { gchar *tmp; g_return_val_if_fail(filename != NULL, NULL); tmp = g_strrstr(filename, "."); if (tmp == NULL) return NULL; return g_strdup(tmp + 1); } static gboolean fu_remote_list_remote_filename_cache_fn_is_obsolete(FuRemoteList *self, const gchar *fn) { g_autofree gchar *ext = fu_remote_list_get_last_ext(fn); g_autofree gchar *basename = g_path_get_basename(fn); /* fwupd >= 2.0.0 calls this firmware.xml.* so that we can validate with jcat-tool */ if (g_str_has_prefix(basename, "metadata.")) return TRUE; /* in a format that we no longer use */ if (g_strcmp0(ext, "jcat") == 0) return FALSE; return g_strcmp0(ext, self->lvfs_metadata_format) != 0; } static gboolean fu_remote_list_cleanup_lvfs_remote(FuRemoteList *self, FwupdRemote *remote, GError **error) { const gchar *fn_cache = fwupd_remote_get_filename_cache(remote); g_autofree gchar *dirname = NULL; g_autoptr(GPtrArray) files = NULL; /* sanity check */ if (fn_cache == NULL) return TRUE; if (self->lvfs_metadata_format == NULL) return TRUE; /* get all files */ dirname = g_path_get_dirname(fn_cache); files = fu_path_get_files(dirname, NULL); if (files == NULL) return TRUE; /* delete any obsolete ones */ for (guint i = 0; i < files->len; i++) { const gchar *fn = g_ptr_array_index(files, i); if (fu_remote_list_remote_filename_cache_fn_is_obsolete(self, fn)) { g_info("deleting obsolete %s", fn); if (g_unlink(fn) == -1) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to delete obsolete %s", fn); return FALSE; } } } /* success */ return TRUE; } void fu_remote_list_add_remote(FuRemoteList *self, FwupdRemote *remote) { g_return_if_fail(FU_IS_REMOTE_LIST(self)); g_return_if_fail(FWUPD_IS_REMOTE(remote)); fu_remote_list_emit_added(self, remote); g_ptr_array_add(self->array, g_object_ref(remote)); } static gboolean fu_remote_list_is_remote_origin_lvfs(FwupdRemote *remote) { if (fwupd_remote_get_id(remote) != NULL && g_strstr_len(fwupd_remote_get_id(remote), -1, "lvfs") != NULL) return TRUE; if (fwupd_remote_get_metadata_uri(remote) != NULL && g_strstr_len(fwupd_remote_get_metadata_uri(remote), -1, "fwupd.org") != NULL) return TRUE; return FALSE; } static gboolean fu_remote_list_add_for_file(FuRemoteList *self, const gchar *filename, GError **error) { FwupdRemote *remote_tmp; g_autofree gchar *remotesdir = NULL; g_autoptr(FwupdRemote) remote = fu_remote_new(); /* set directory to store data */ remotesdir = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR_METADATA); fwupd_remote_set_remotes_dir(remote, remotesdir); /* load from keyfile */ g_info("loading remote from %s", filename); if (!fu_remote_load_from_filename(remote, filename, NULL, error)) { g_prefix_error(error, "failed to load %s: ", filename); return FALSE; } /* does it already exist */ remote_tmp = fu_remote_list_get_by_id(self, fwupd_remote_get_id(remote)); if (remote_tmp != NULL) { g_debug("remote %s already added from %s", fwupd_remote_get_id(remote), fwupd_remote_get_filename_source(remote_tmp)); return TRUE; } /* auto-fix before setup */ if (self->fix_metadata_uri && fu_remote_list_is_remote_origin_lvfs(remote)) { const gchar *metadata_url = fwupd_remote_get_metadata_uri(remote); g_autofree gchar *ext = fu_remote_list_get_last_ext(metadata_url); if (g_strcmp0(ext, self->lvfs_metadata_format) != 0) { g_autoptr(GString) str = g_string_new(metadata_url); g_autofree gchar *metadata_ext = g_strdup_printf(".%s", self->lvfs_metadata_format); g_string_replace(str, ".gz", metadata_ext, 0); g_string_replace(str, ".xz", metadata_ext, 0); g_string_replace(str, ".zst", metadata_ext, 0); g_info("auto-fixing remote %s MetadataURI from %s to %s", fwupd_remote_get_id(remote), metadata_url, str->str); fwupd_remote_set_metadata_uri(remote, str->str); } } /* load remote */ if (!fwupd_remote_setup(remote, error)) { g_prefix_error(error, "failed to setup %s: ", filename); return FALSE; } /* delete the obsolete files if the remote is now set up to use a new metadata format */ if (fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_ENABLED) && fwupd_remote_get_kind(remote) == FWUPD_REMOTE_KIND_DOWNLOAD && fu_remote_list_is_remote_origin_lvfs(remote)) { if (!fu_remote_list_cleanup_lvfs_remote(self, remote, error)) return FALSE; } /* watch the remote_list file and the XML file itself */ if (!fu_remote_list_add_inotify(self, filename, error)) return FALSE; if (!fu_remote_list_add_inotify(self, fwupd_remote_get_filename_cache(remote), error)) return FALSE; /* try to find a custom agreement, falling back to a generic warning */ if (fwupd_remote_get_kind(remote) == FWUPD_REMOTE_KIND_DOWNLOAD) { g_autofree gchar *component_id = _fwupd_remote_build_component_id(remote); g_autofree gchar *xpath = NULL; g_autoptr(GString) agreement_markup = NULL; g_autoptr(XbNode) component = NULL; struct { const gchar *key; const gchar *search; const gchar *fallback; } distro_kv[] = { { G_OS_INFO_KEY_NAME, "$OS_RELEASE:NAME$", "this distribution", }, { G_OS_INFO_KEY_BUG_REPORT_URL, "$OS_RELEASE:BUG_REPORT_URL$", "https://github.com/fwupd/fwupd/issues", }, }; xpath = g_strdup_printf("component/id[text()='%s']/..", component_id); component = xb_silo_query_first(self->silo, xpath, NULL); if (component != NULL) { agreement_markup = _fwupd_remote_get_agreement_for_app(remote, component, error); } else { agreement_markup = _fwupd_remote_get_agreement_default(remote, error); } if (agreement_markup == NULL) return FALSE; /* replace any dynamic values from os-release */ for (guint i = 0; i < G_N_ELEMENTS(distro_kv); i++) { g_autofree gchar *os_replace = g_get_os_info(distro_kv[i].key); if (os_replace == NULL) os_replace = g_strdup(distro_kv[i].fallback); g_string_replace(agreement_markup, distro_kv[i].search, os_replace, 0); } } /* set mtime */ fwupd_remote_set_mtime(remote, _fwupd_remote_get_mtime(remote)); fu_remote_list_add_remote(self, remote); /* success */ return TRUE; } static gboolean fu_remote_list_add_for_path(FuRemoteList *self, const gchar *path, GError **error) { g_autofree gchar *path_remotes = NULL; g_autoptr(GPtrArray) paths = NULL; path_remotes = g_build_filename(path, "remotes.d", NULL); if (!g_file_test(path_remotes, G_FILE_TEST_EXISTS)) { g_debug("path %s does not exist", path_remotes); return TRUE; } if (!fu_remote_list_add_inotify(self, path_remotes, error)) return FALSE; paths = fu_path_glob(path_remotes, "*.conf", NULL); if (paths == NULL) return TRUE; for (guint i = 0; i < paths->len; i++) { const gchar *filename = g_ptr_array_index(paths, i); if (g_str_has_suffix(filename, "fwupd-tests.conf") && !self->testing_remote) continue; if (!fu_remote_list_add_for_file(self, filename, error)) return FALSE; } return TRUE; } gboolean fu_remote_list_set_key_value(FuRemoteList *self, const gchar *remote_id, const gchar *key, const gchar *value, GError **error) { FwupdRemote *remote; const gchar *filename; g_autofree gchar *filename_new = NULL; g_autofree gchar *value_old = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(GKeyFile) keyfile = g_key_file_new(); /* check remote is valid */ remote = fu_remote_list_get_by_id(self, remote_id); if (remote == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "remote %s not found", remote_id); return FALSE; } /* modify the remote */ filename = fwupd_remote_get_filename_source(remote); if (!g_key_file_load_from_file(keyfile, filename, G_KEY_FILE_KEEP_COMMENTS, error)) { g_prefix_error(error, "failed to load %s: ", filename); return FALSE; } value_old = g_key_file_get_string(keyfile, "fwupd Remote", key, NULL); if (g_strcmp0(value_old, value) == 0) return TRUE; g_key_file_set_string(keyfile, "fwupd Remote", key, value); /* try existing file first, then call back to the mutable location */ if (!g_key_file_save_to_file(keyfile, filename, &error_local)) { if (g_error_matches(error_local, G_FILE_ERROR, G_FILE_ERROR_PERM)) { g_autofree gchar *basename = g_path_get_basename(filename); g_autofree gchar *remotesdir_mut = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR_PKG); filename_new = g_build_filename(remotesdir_mut, "remotes.d", basename, NULL); if (!fu_path_mkdir_parent(filename_new, error)) return FALSE; g_info("falling back from %s to %s", filename, filename_new); if (!g_key_file_save_to_file(keyfile, filename_new, error)) return FALSE; } else { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } else { filename_new = g_strdup(filename); } /* reload values */ if (!fu_remote_load_from_filename(remote, filename_new, NULL, error)) { g_prefix_error(error, "failed to load %s: ", filename_new); return FALSE; } fu_remote_list_emit_changed(self); return TRUE; } static gint fu_remote_list_sort_cb(gconstpointer a, gconstpointer b) { FwupdRemote *remote_a = *((FwupdRemote **)a); FwupdRemote *remote_b = *((FwupdRemote **)b); /* use priority first */ if (fwupd_remote_get_priority(remote_a) < fwupd_remote_get_priority(remote_b)) return 1; if (fwupd_remote_get_priority(remote_a) > fwupd_remote_get_priority(remote_b)) return -1; /* fall back to name */ return g_strcmp0(fwupd_remote_get_id(remote_a), fwupd_remote_get_id(remote_b)); } static guint fu_remote_list_depsolve_order_before(FuRemoteList *self) { guint cnt = 0; for (guint i = 0; i < self->array->len; i++) { FwupdRemote *remote = g_ptr_array_index(self->array, i); gchar **order = fwupd_remote_get_order_before(remote); if (order == NULL) continue; for (guint j = 0; order[j] != NULL; j++) { FwupdRemote *remote2; if (g_strcmp0(order[j], fwupd_remote_get_id(remote)) == 0) { g_debug("ignoring self-dep remote %s", order[j]); continue; } remote2 = fu_remote_list_get_by_id(self, order[j]); if (remote2 == NULL) { g_debug("ignoring unfound remote %s", order[j]); continue; } if (fwupd_remote_get_priority(remote) > fwupd_remote_get_priority(remote2)) continue; g_debug("ordering %s=%s+1", fwupd_remote_get_id(remote), fwupd_remote_get_id(remote2)); fwupd_remote_set_priority(remote, fwupd_remote_get_priority(remote2) + 1); cnt++; } } return cnt; } static guint fu_remote_list_depsolve_order_after(FuRemoteList *self) { guint cnt = 0; for (guint i = 0; i < self->array->len; i++) { FwupdRemote *remote = g_ptr_array_index(self->array, i); gchar **order = fwupd_remote_get_order_after(remote); if (order == NULL) continue; for (guint j = 0; order[j] != NULL; j++) { FwupdRemote *remote2; if (g_strcmp0(order[j], fwupd_remote_get_id(remote)) == 0) { g_debug("ignoring self-dep remote %s", order[j]); continue; } remote2 = fu_remote_list_get_by_id(self, order[j]); if (remote2 == NULL) { g_debug("ignoring unfound remote %s", order[j]); continue; } if (fwupd_remote_get_priority(remote) < fwupd_remote_get_priority(remote2)) continue; g_debug("ordering %s=%s+1", fwupd_remote_get_id(remote2), fwupd_remote_get_id(remote)); fwupd_remote_set_priority(remote2, fwupd_remote_get_priority(remote) + 1); cnt++; } } return cnt; } static gboolean fu_remote_list_reload(FuRemoteList *self, GError **error) { guint depsolve_check; g_autofree gchar *remotesdir = NULL; g_autofree gchar *remotesdir_mut = NULL; g_autofree gchar *remotesdir_immut = NULL; g_autoptr(GString) str = g_string_new(NULL); /* clear */ g_ptr_array_set_size(self->array, 0); g_ptr_array_set_size(self->monitors, 0); /* search mutable, and then fall back to /etc and immutable */ remotesdir_mut = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR_PKG); if (!fu_remote_list_add_for_path(self, remotesdir_mut, error)) return FALSE; remotesdir = fu_path_from_kind(FU_PATH_KIND_SYSCONFDIR_PKG); if (!fu_remote_list_add_for_path(self, remotesdir, error)) return FALSE; remotesdir_immut = fu_path_from_kind(FU_PATH_KIND_DATADIR_PKG); if (!fu_remote_list_add_for_path(self, remotesdir_immut, error)) return FALSE; /* depsolve */ for (depsolve_check = 0; depsolve_check < 100; depsolve_check++) { guint cnt = 0; cnt += fu_remote_list_depsolve_order_before(self); cnt += fu_remote_list_depsolve_order_after(self); if (cnt == 0) break; } if (depsolve_check == 100) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Cannot depsolve remotes ordering"); return FALSE; } /* order these by priority, then name */ g_ptr_array_sort(self->array, fu_remote_list_sort_cb); /* print to the console */ for (guint i = 0; i < self->array->len; i++) { FwupdRemote *remote = g_ptr_array_index(self->array, i); if (!fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_ENABLED)) continue; if (str->len > 0) g_string_append(str, ", "); g_string_append_printf(str, "%s[%i]", fwupd_remote_get_id(remote), fwupd_remote_get_priority(remote)); } g_info("enabled remotes: %s", str->str); /* success */ return TRUE; } static gboolean fu_remote_list_load_metainfos(XbBuilder *builder, GError **error) { const gchar *fn; g_autofree gchar *datadir = NULL; g_autofree gchar *metainfo_path = NULL; g_autoptr(GDir) dir = NULL; /* pkg metainfo dir */ datadir = fu_path_from_kind(FU_PATH_KIND_DATADIR_PKG); metainfo_path = g_build_filename(datadir, "metainfo", NULL); if (!g_file_test(metainfo_path, G_FILE_TEST_EXISTS)) return TRUE; g_debug("loading %s", metainfo_path); dir = g_dir_open(metainfo_path, 0, error); if (dir == NULL) return FALSE; while ((fn = g_dir_read_name(dir)) != NULL) { if (g_str_has_suffix(fn, ".metainfo.xml")) { g_autofree gchar *filename = g_build_filename(metainfo_path, fn, NULL); g_autoptr(GFile) file = g_file_new_for_path(filename); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); if (!xb_builder_source_load_file(source, file, XB_BUILDER_SOURCE_FLAG_NONE, NULL, error)) { fwupd_error_convert(error); return FALSE; } xb_builder_import_source(builder, source); } } return TRUE; } gboolean fu_remote_list_set_testing_remote_enabled(FuRemoteList *self, gboolean enable, GError **error) { g_return_val_if_fail(FU_IS_REMOTE_LIST(self), FALSE); /* not yet initialized */ if (self->silo == NULL) return TRUE; if (self->testing_remote == enable) return TRUE; self->testing_remote = enable; if (!fu_remote_list_reload(self, error)) return FALSE; fu_remote_list_emit_changed(self); return TRUE; } gboolean fu_remote_list_load(FuRemoteList *self, FuRemoteListLoadFlags flags, GError **error) { const gchar *const *locales = g_get_language_names(); g_autoptr(GFile) xmlb = NULL; g_autoptr(XbBuilder) builder = xb_builder_new(); XbBuilderCompileFlags compile_flags = XB_BUILDER_COMPILE_FLAG_SINGLE_LANG | XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID; g_return_val_if_fail(FU_IS_REMOTE_LIST(self), FALSE); g_return_val_if_fail(self->silo == NULL, FALSE); /* enable testing only remotes */ if (flags & FU_REMOTE_LIST_LOAD_FLAG_TEST_REMOTE) self->testing_remote = TRUE; /* autofix on reload too */ if (flags & FU_REMOTE_LIST_LOAD_FLAG_FIX_METADATA_URI) self->fix_metadata_uri = TRUE; /* load AppStream about the remote_list */ if (!fu_remote_list_load_metainfos(builder, error)) return FALSE; /* add the locales, which is really only going to be 'C' or 'en' */ for (guint i = 0; locales[i] != NULL; i++) xb_builder_add_locale(builder, locales[i]); /* on a read-only filesystem don't care about the cache GUID */ if (flags & FU_REMOTE_LIST_LOAD_FLAG_READONLY_FS) compile_flags |= XB_BUILDER_COMPILE_FLAG_IGNORE_GUID; /* build the metainfo silo */ if (flags & FU_REMOTE_LIST_LOAD_FLAG_NO_CACHE) { g_autoptr(GFileIOStream) iostr = NULL; xmlb = g_file_new_tmp(NULL, &iostr, error); if (xmlb == NULL) return FALSE; } else { g_autofree gchar *cachedirpkg = fu_path_from_kind(FU_PATH_KIND_CACHEDIR_PKG); g_autofree gchar *xmlbfn = g_build_filename(cachedirpkg, "metainfo.xmlb", NULL); xmlb = g_file_new_for_path(xmlbfn); } self->silo = xb_builder_ensure(builder, xmlb, compile_flags, NULL, error); if (self->silo == NULL) return FALSE; /* load remote_list */ return fu_remote_list_reload(self, error); } GPtrArray * fu_remote_list_get_all(FuRemoteList *self) { g_return_val_if_fail(FU_IS_REMOTE_LIST(self), NULL); return self->array; } FwupdRemote * fu_remote_list_get_by_id(FuRemoteList *self, const gchar *remote_id) { g_return_val_if_fail(FU_IS_REMOTE_LIST(self), NULL); for (guint i = 0; i < self->array->len; i++) { FwupdRemote *remote = g_ptr_array_index(self->array, i); if (g_strcmp0(remote_id, fwupd_remote_get_id(remote)) == 0) return remote; } return NULL; } static void fu_remote_list_class_init(FuRemoteListClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_remote_list_finalize; /** * FuRemoteList::changed: * @self: the #FuRemoteList instance that emitted the signal * * The ::changed signal is emitted when the list of remotes has * changed, for instance when a remote has been added or removed. **/ signals[SIGNAL_CHANGED] = g_signal_new("changed", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); /** * FuRemoteList::added: * @self: the #FuRemoteList instance that emitted the signal * @remote: the #FwupdRemote that was added **/ signals[SIGNAL_ADDED] = g_signal_new("added", G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 1, FWUPD_TYPE_REMOTE); } static void fu_remote_list_monitor_unref(GFileMonitor *monitor) { g_file_monitor_cancel(monitor); g_object_unref(monitor); } static void fu_remote_list_init(FuRemoteList *self) { self->array = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); self->monitors = g_ptr_array_new_with_free_func((GDestroyNotify)fu_remote_list_monitor_unref); } static void fu_remote_list_finalize(GObject *obj) { FuRemoteList *self = FU_REMOTE_LIST(obj); if (self->silo != NULL) g_object_unref(self->silo); g_ptr_array_unref(self->array); g_ptr_array_unref(self->monitors); g_free(self->lvfs_metadata_format); G_OBJECT_CLASS(fu_remote_list_parent_class)->finalize(obj); } FuRemoteList * fu_remote_list_new(void) { FuRemoteList *self; self = g_object_new(FU_TYPE_REMOTE_LIST, NULL); return FU_REMOTE_LIST(self); } fwupd-2.0.10/src/fu-remote-list.h000066400000000000000000000036361501337203100165360ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_REMOTE_LIST (fu_remote_list_get_type()) G_DECLARE_FINAL_TYPE(FuRemoteList, fu_remote_list, FU, REMOTE_LIST, GObject) /** * FuRemoteListLoadFlags: * @FU_REMOTE_LIST_LOAD_FLAG_NONE: No flags set * @FU_REMOTE_LIST_LOAD_FLAG_READONLY_FS: Ignore readonly filesystem errors * @FU_REMOTE_LIST_LOAD_FLAG_NO_CACHE: Do not save persistent xmlb silos * @FU_REMOTE_LIST_LOAD_FLAG_TEST_REMOTE: Enable test mode remotes * @FU_REMOTE_LIST_LOAD_FLAG_FIX_METADATA_URI: Auto-fix to use the newest supported metadata * * The flags to use when loading a remote_listuration file. **/ typedef enum { FU_REMOTE_LIST_LOAD_FLAG_NONE = 0, FU_REMOTE_LIST_LOAD_FLAG_READONLY_FS = 1 << 0, FU_REMOTE_LIST_LOAD_FLAG_NO_CACHE = 1 << 1, FU_REMOTE_LIST_LOAD_FLAG_TEST_REMOTE = 1 << 2, FU_REMOTE_LIST_LOAD_FLAG_FIX_METADATA_URI = 1 << 3, /*< private >*/ FU_REMOTE_LIST_LOAD_FLAG_LAST } FuRemoteListLoadFlags; FuRemoteList * fu_remote_list_new(void); gboolean fu_remote_list_set_testing_remote_enabled(FuRemoteList *self, gboolean enable, GError **error) G_GNUC_NON_NULL(1); gboolean fu_remote_list_load(FuRemoteList *self, FuRemoteListLoadFlags flags, GError **error) G_GNUC_NON_NULL(1); gboolean fu_remote_list_set_key_value(FuRemoteList *self, const gchar *remote_id, const gchar *key, const gchar *value, GError **error) G_GNUC_NON_NULL(1, 2, 3, 4); GPtrArray * fu_remote_list_get_all(FuRemoteList *self) G_GNUC_NON_NULL(1); FwupdRemote * fu_remote_list_get_by_id(FuRemoteList *self, const gchar *remote_id) G_GNUC_NON_NULL(1, 2); void fu_remote_list_set_lvfs_metadata_format(FuRemoteList *self, const gchar *lvfs_metadata_format); /* for the self tests */ void fu_remote_list_add_remote(FuRemoteList *self, FwupdRemote *remote) G_GNUC_NON_NULL(1, 2); fwupd-2.0.10/src/fu-remote.c000066400000000000000000000212741501337203100155560ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuRemote" #include "config.h" #include "fwupd-remote-private.h" #include "fu-remote.h" struct _FuRemote { FwupdRemote parent_instance; }; G_DEFINE_TYPE(FuRemote, fu_remote, FWUPD_TYPE_REMOTE) #define FWUPD_REMOTE_CONFIG_DEFAULT_REFRESH_INTERVAL 86400 /* 24h */ /** * fu_remote_load_from_filename: * @self: a #FwupdRemote * @filename: (not nullable): a filename * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Loads metadata about the remote from a keyfile. * This can be called zero or multiple times for each remote. * * Returns: %TRUE for success **/ gboolean fu_remote_load_from_filename(FwupdRemote *self, const gchar *filename, GCancellable *cancellable, GError **error) { const gchar *group = "fwupd Remote"; g_autofree gchar *id = NULL; g_autoptr(GKeyFile) kf = NULL; g_return_val_if_fail(FWUPD_IS_REMOTE(self), FALSE); g_return_val_if_fail(filename != NULL, FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* set ID */ id = g_path_get_basename(filename); fwupd_remote_set_id(self, id); /* load file */ kf = g_key_file_new(); if (!g_key_file_load_from_file(kf, filename, G_KEY_FILE_NONE, error)) return FALSE; /* the first remote sets the URI, even if it's file:// to the cache */ if (g_key_file_has_key(kf, group, "MetadataURI", NULL)) { g_autofree gchar *tmp = g_key_file_get_string(kf, group, "MetadataURI", NULL); if (g_str_has_prefix(tmp, "file://")) { const gchar *filename_cache = tmp; if (g_str_has_prefix(filename_cache, "file://")) filename_cache += 7; if (g_file_test(filename_cache, G_FILE_TEST_IS_DIR)) fwupd_remote_set_kind(self, FWUPD_REMOTE_KIND_DIRECTORY); else fwupd_remote_set_kind(self, FWUPD_REMOTE_KIND_LOCAL); fwupd_remote_set_filename_cache(self, filename_cache); } else if (g_str_has_prefix(tmp, "http://") || g_str_has_prefix(tmp, "https://") || g_str_has_prefix(tmp, "ipfs://") || g_str_has_prefix(tmp, "ipns://")) { fwupd_remote_set_kind(self, FWUPD_REMOTE_KIND_DOWNLOAD); fwupd_remote_set_refresh_interval( self, FWUPD_REMOTE_CONFIG_DEFAULT_REFRESH_INTERVAL); fwupd_remote_set_metadata_uri(self, tmp); } } /* all keys are optional */ if (g_key_file_has_key(kf, group, "Enabled", NULL)) { if (g_key_file_get_boolean(kf, group, "Enabled", NULL)) fwupd_remote_add_flag(self, FWUPD_REMOTE_FLAG_ENABLED); else fwupd_remote_remove_flag(self, FWUPD_REMOTE_FLAG_ENABLED); } if (g_key_file_has_key(kf, group, "ApprovalRequired", NULL)) { if (g_key_file_get_boolean(kf, group, "ApprovalRequired", NULL)) fwupd_remote_add_flag(self, FWUPD_REMOTE_FLAG_APPROVAL_REQUIRED); else fwupd_remote_remove_flag(self, FWUPD_REMOTE_FLAG_APPROVAL_REQUIRED); } if (g_key_file_has_key(kf, group, "Title", NULL)) { g_autofree gchar *tmp = g_key_file_get_string(kf, group, "Title", NULL); fwupd_remote_set_title(self, tmp); } if (g_key_file_has_key(kf, group, "PrivacyURI", NULL)) { g_autofree gchar *tmp = g_key_file_get_string(kf, group, "PrivacyURI", NULL); fwupd_remote_set_privacy_uri(self, tmp); } if (g_key_file_has_key(kf, group, "RefreshInterval", NULL)) { fwupd_remote_set_refresh_interval( self, g_key_file_get_uint64(kf, group, "RefreshInterval", NULL)); } if (g_key_file_has_key(kf, group, "ReportURI", NULL)) { g_autofree gchar *tmp = g_key_file_get_string(kf, group, "ReportURI", NULL); fwupd_remote_set_report_uri(self, tmp); } if (g_key_file_has_key(kf, group, "Username", NULL)) { g_autofree gchar *tmp = g_key_file_get_string(kf, group, "Username", NULL); fwupd_remote_set_username(self, tmp); } if (g_key_file_has_key(kf, group, "Password", NULL)) { g_autofree gchar *tmp = g_key_file_get_string(kf, group, "Password", NULL); fwupd_remote_set_password(self, tmp); } if (g_key_file_has_key(kf, group, "FirmwareBaseURI", NULL)) { g_autofree gchar *tmp = g_key_file_get_string(kf, group, "FirmwareBaseURI", NULL); fwupd_remote_set_firmware_base_uri(self, tmp); } if (g_key_file_has_key(kf, group, "OrderBefore", NULL)) { g_autofree gchar *tmp = g_key_file_get_string(kf, group, "OrderBefore", NULL); fwupd_remote_set_order_before(self, tmp); } if (g_key_file_has_key(kf, group, "OrderAfter", NULL)) { g_autofree gchar *tmp = g_key_file_get_string(kf, group, "OrderAfter", NULL); fwupd_remote_set_order_after(self, tmp); } if (g_key_file_has_key(kf, group, "AutomaticReports", NULL)) { if (g_key_file_get_boolean(kf, group, "AutomaticReports", NULL)) fwupd_remote_add_flag(self, FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS); else fwupd_remote_remove_flag(self, FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS); } if (g_key_file_has_key(kf, group, "AutomaticSecurityReports", NULL)) { if (g_key_file_get_boolean(kf, group, "AutomaticSecurityReports", NULL)) fwupd_remote_add_flag(self, FWUPD_REMOTE_FLAG_AUTOMATIC_SECURITY_REPORTS); else fwupd_remote_remove_flag(self, FWUPD_REMOTE_FLAG_AUTOMATIC_SECURITY_REPORTS); } /* old versions of fwupd used empty strings to mean "unset" and the package manager * might not have replaced the file marked as a config file due to modification */ if (g_strcmp0(fwupd_remote_get_username(self), "") == 0 && g_strcmp0(fwupd_remote_get_password(self), "") == 0) { fwupd_remote_set_username(self, NULL); fwupd_remote_set_password(self, NULL); } /* success */ fwupd_remote_set_filename_source(self, filename); return TRUE; } /** * fu_remote_save_to_filename: * @self: a #FwupdRemote * @filename: (not nullable): a filename * @cancellable: (nullable): optional #GCancellable * @error: (nullable): optional return location for an error * * Saves metadata about the remote to a keyfile. * * Returns: %TRUE for success **/ gboolean fu_remote_save_to_filename(FwupdRemote *self, const gchar *filename, GCancellable *cancellable, GError **error) { const gchar *group = "fwupd Remote"; g_autoptr(GKeyFile) kf = g_key_file_new(); g_return_val_if_fail(FWUPD_IS_REMOTE(self), FALSE); g_return_val_if_fail(filename != NULL, FALSE); g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* optional keys */ if (fwupd_remote_get_metadata_uri(self) != NULL) g_key_file_set_string(kf, group, "MetadataURI", fwupd_remote_get_metadata_uri(self)); if (fwupd_remote_get_title(self) != NULL) g_key_file_set_string(kf, group, "Title", fwupd_remote_get_title(self)); if (fwupd_remote_get_privacy_uri(self) != NULL) g_key_file_set_string(kf, group, "PrivacyURI", fwupd_remote_get_privacy_uri(self)); if (fwupd_remote_get_report_uri(self) != NULL) g_key_file_set_string(kf, group, "ReportURI", fwupd_remote_get_report_uri(self)); if (fwupd_remote_get_refresh_interval(self) != 0) g_key_file_set_uint64(kf, group, "RefreshInterval", fwupd_remote_get_refresh_interval(self)); if (fwupd_remote_get_username(self) != NULL) g_key_file_set_string(kf, group, "Username", fwupd_remote_get_username(self)); if (fwupd_remote_get_password(self) != NULL) g_key_file_set_string(kf, group, "Password", fwupd_remote_get_password(self)); if (fwupd_remote_get_firmware_base_uri(self) != NULL) g_key_file_set_string(kf, group, "FirmwareBaseURI", fwupd_remote_get_firmware_base_uri(self)); if (fwupd_remote_get_order_after(self) != NULL) { g_autofree gchar *str = g_strjoinv(";", fwupd_remote_get_order_after(self)); g_key_file_set_string(kf, group, "OrderAfter", str); } if (fwupd_remote_get_order_before(self) != NULL) { g_autofree gchar *str = g_strjoinv(";", fwupd_remote_get_order_before(self)); g_key_file_set_string(kf, group, "OrderBefore", str); } if (fwupd_remote_has_flag(self, FWUPD_REMOTE_FLAG_ENABLED)) g_key_file_set_boolean(kf, group, "Enabled", TRUE); if (fwupd_remote_has_flag(self, FWUPD_REMOTE_FLAG_APPROVAL_REQUIRED)) g_key_file_set_boolean(kf, group, "ApprovalRequired", TRUE); if (fwupd_remote_has_flag(self, FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS)) g_key_file_set_boolean(kf, group, "AutomaticReports", TRUE); if (fwupd_remote_has_flag(self, FWUPD_REMOTE_FLAG_AUTOMATIC_SECURITY_REPORTS)) g_key_file_set_boolean(kf, group, "AutomaticSecurityReports", TRUE); /* save file */ return g_key_file_save_to_file(kf, filename, error); } static void fu_remote_init(FuRemote *self) { } static void fu_remote_class_init(FuRemoteClass *klass) { } FwupdRemote * fu_remote_new(void) { return FWUPD_REMOTE(g_object_new(FU_TYPE_REMOTE, NULL)); } fwupd-2.0.10/src/fu-remote.h000066400000000000000000000011721501337203100155560ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_REMOTE (fu_remote_get_type()) G_DECLARE_FINAL_TYPE(FuRemote, fu_remote, FU, REMOTE, FwupdRemote) gboolean fu_remote_load_from_filename(FwupdRemote *self, const gchar *filename, GCancellable *cancellable, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_remote_save_to_filename(FwupdRemote *self, const gchar *filename, GCancellable *cancellable, GError **error) G_GNUC_NON_NULL(1, 2); FwupdRemote * fu_remote_new(void); fwupd-2.0.10/src/fu-security-attr-common.c000066400000000000000000000722101501337203100203640ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "fwupd-security-attr-private.h" #include "fu-security-attr-common.h" #include "fu-security-attrs-private.h" gchar * fu_security_attr_get_name(FwupdSecurityAttr *attr) { const gchar *appstream_id = fwupd_security_attr_get_appstream_id(attr); if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SPI_BIOSWE) == 0) { /* TRANSLATORS: Title: SPI refers to the flash chip in the computer */ return g_strdup(_("SPI write")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SPI_BLE) == 0) { /* TRANSLATORS: Title: SPI refers to the flash chip in the computer */ return g_strdup(_("SPI lock")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SPI_SMM_BWP) == 0) { /* TRANSLATORS: Title: SPI refers to the flash chip in the computer */ return g_strdup(_("SPI BIOS region")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SPI_DESCRIPTOR) == 0) { /* TRANSLATORS: Title: SPI refers to the flash chip in the computer */ return g_strdup(_("SPI BIOS Descriptor")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_PREBOOT_DMA_PROTECTION) == 0) { /* TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack */ return g_strdup(_("Pre-boot DMA protection")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_ENABLED) == 0) { /* TRANSLATORS: Title: BootGuard is a trademark from Intel */ return g_strdup(_("Intel BootGuard")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_VERIFIED) == 0) { /* TRANSLATORS: Title: BootGuard is a trademark from Intel, * verified boot refers to the way the boot process is verified */ return g_strdup(_("Intel BootGuard verified boot")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_ACM) == 0) { /* TRANSLATORS: Title: BootGuard is a trademark from Intel, * ACM means to verify the integrity of Initial Boot Block */ return g_strdup(_("Intel BootGuard ACM protected")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_POLICY) == 0) { /* TRANSLATORS: Title: BootGuard is a trademark from Intel, * error policy is what to do on failure */ return g_strdup(_("Intel BootGuard error policy")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_OTP) == 0) { /* TRANSLATORS: Title: BootGuard is a trademark from Intel, * OTP = one time programmable */ return g_strdup(_("Intel BootGuard OTP fuse")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_CET_ENABLED) == 0) { /* TRANSLATORS: Title: CET = Control-flow Enforcement Technology, * enabled means supported by the processor */ return g_strdup(_("CET Platform")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_CET_ACTIVE) == 0) { /* TRANSLATORS: Title: CET = Control-flow Enforcement Technology, * Utilized by OS means the distribution enabled it*/ return g_strdup(_("CET OS Support")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SMAP) == 0) { /* TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention */ return g_strdup(_("SMAP")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_ENCRYPTED_RAM) == 0) { /* TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME */ return g_strdup(_("Encrypted RAM")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_IOMMU) == 0) { /* TRANSLATORS: Title: * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit */ return g_strdup(_("IOMMU")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_KERNEL_LOCKDOWN) == 0) { /* TRANSLATORS: Title: lockdown is a security mode of the kernel */ return g_strdup(_("Linux kernel lockdown")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_KERNEL_TAINTED) == 0) { /* TRANSLATORS: Title: if it's tainted or not */ return g_strdup(_("Linux kernel")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_KERNEL_SWAP) == 0) { /* TRANSLATORS: Title: swap space or swap partition */ return g_strdup(_("Linux swap")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SUSPEND_TO_RAM) == 0) { /* TRANSLATORS: Title: sleep state */ return g_strdup(_("Suspend-to-ram")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SUSPEND_TO_IDLE) == 0) { /* TRANSLATORS: Title: a better sleep state */ return g_strdup(_("Suspend-to-idle")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_UEFI_PK) == 0) { /* TRANSLATORS: Title: PK is the 'platform key' for the machine */ return g_strdup(_("UEFI platform key")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_UEFI_SECUREBOOT) == 0) { /* TRANSLATORS: Title: SB is a way of locking down UEFI */ return g_strdup(_("UEFI secure boot")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_UEFI_BOOTSERVICE_VARS) == 0) { /* TRANSLATORS: Title: Bootservice is when only readable from early-boot */ return g_strdup(_("UEFI bootservice variables")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_TPM_EMPTY_PCR) == 0) { /* TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be empty */ return g_strdup(_("TPM empty PCRs")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_TPM_RECONSTRUCTION_PCR0) == 0) { /* TRANSLATORS: Title: the PCR is rebuilt from the TPM event log */ return g_strdup(_("TPM PCR0 reconstruction")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_TPM_VERSION_20) == 0) { /* TRANSLATORS: Title: TPM = Trusted Platform Module */ return g_strdup(_("TPM v2.0")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_MEI_MANUFACTURING_MODE) == 0) { const gchar *kind = fwupd_security_attr_get_metadata(attr, "kind"); if (kind != NULL) { /* TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT */ return g_strdup_printf(_("%s manufacturing mode"), kind); } /* TRANSLATORS: Title: MEI = Intel Management Engine */ return g_strdup(_("MEI manufacturing mode")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_MEI_OVERRIDE_STRAP) == 0) { const gchar *kind = fwupd_security_attr_get_metadata(attr, "kind"); if (kind != NULL) { /* TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT */ return g_strdup_printf(_("%s override"), kind); } /* TRANSLATORS: Title: MEI = Intel Management Engine, and the * "override" is the physical PIN that can be driven to * logic high -- luckily it is probably not accessible to * end users on consumer boards */ return g_strdup(_("MEI override")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_MEI_KEY_MANIFEST) == 0) { /* TRANSLATORS: Title: MEI = Intel Management Engine, and key refer * to the private/public key used to secure loading of firmware */ return g_strdup(_("MEI key manifest")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_MEI_VERSION) == 0) { /* TRANSLATORS: Title: MEI = Intel Management Engine */ const gchar *kind = fwupd_security_attr_get_metadata(attr, "kind"); const gchar *version = fwupd_security_attr_get_metadata(attr, "version"); if (kind != NULL && version != NULL) { /* TRANSLATORS: Title: %1 is ME kind, e.g. CSME/TXT, %2 is a version number */ return g_strdup_printf(_("%s v%s"), kind, version); } if (kind != NULL) { /* TRANSLATORS: Title: %s is ME kind, e.g. CSME/TXT */ return g_strdup_printf(_("%s version"), kind); } return g_strdup(_("MEI version")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_FWUPD_UPDATES) == 0) { /* TRANSLATORS: Title: if firmware updates are available */ return g_strdup(_("Firmware updates")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_FWUPD_ATTESTATION) == 0) { /* TRANSLATORS: Title: if we can verify the firmware checksums */ return g_strdup(_("Firmware attestation")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_FWUPD_PLUGINS) == 0) { /* TRANSLATORS: Title: if the fwupd plugins are all present and correct */ return g_strdup(_("fwupd plugins")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_PLATFORM_DEBUG_ENABLED) == 0 || g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_PLATFORM_DEBUG_LOCKED) == 0) { /* TRANSLATORS: Title: Allows debugging of parts using proprietary hardware */ return g_strdup(_("Platform debugging")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SUPPORTED_CPU) == 0) { /* TRANSLATORS: Title: if fwupd supports HSI on this chip */ return g_strdup(_("Supported CPU")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_AMD_ROLLBACK_PROTECTION) == 0) { /* TRANSLATORS: Title: if firmware enforces rollback protection */ return g_strdup(_("Processor rollback protection")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_AMD_SPI_REPLAY_PROTECTION) == 0) { /* TRANSLATORS: Title: if hardware enforces control of SPI replays */ return g_strdup(_("SPI replay protection")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_AMD_SPI_WRITE_PROTECTION) == 0) { /* TRANSLATORS: Title: if hardware enforces control of SPI writes */ return g_strdup(_("SPI write protection")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_PLATFORM_FUSED) == 0) { /* TRANSLATORS: Title: if the part has been fused */ return g_strdup(_("Fused platform")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_HOST_EMULATION) == 0) { /* TRANSLATORS: Title: if we are emulating a different host */ return g_strdup(_("Emulated host")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_BIOS_ROLLBACK_PROTECTION) == 0) { /* TRANSLATORS: Title: if firmware enforces rollback protection */ return g_strdup(_("BIOS rollback protection")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_INTEL_GDS) == 0) { /* TRANSLATORS: Title: GDS is where the CPU leaks information */ return g_strdup(_("Intel GDS mitigation")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_BIOS_CAPSULE_UPDATES) == 0) { /* TRANSLATORS: Title: Whether BIOS Firmware updates is enabled */ return g_strdup(_("BIOS firmware updates")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_AMD_SMM_LOCKED) == 0) { /* TRANSLATORS: Title: Whether firmware is locked down */ return g_strdup(_("SMM locked down")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_UEFI_MEMORY_PROTECTION) == 0) { /* TRANSLATORS: Title: is UEFI early-boot memory protection turned on */ return g_strdup(_("UEFI memory protection")); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_UEFI_DB) == 0) { /* TRANSLATORS: Title: is UEFI db up-to-date */ return g_strdup(_("UEFI db")); } /* we should not get here */ return g_strdup(fwupd_security_attr_get_name(attr)); } const gchar * fu_security_attr_get_title(FwupdSecurityAttr *attr) { const gchar *appstream_id = fwupd_security_attr_get_appstream_id(attr); if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SPI_BIOSWE) == 0) { /* TRANSLATORS: Title: firmware refers to the flash chip in the computer */ return _("Firmware Write Protection"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SPI_BLE) == 0) { /* TRANSLATORS: Title: firmware refers to the flash chip in the computer */ return _("Firmware Write Protection Lock"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SPI_SMM_BWP) == 0) { /* TRANSLATORS: Title: SPI refers to the flash chip in the computer */ return _("Firmware BIOS Region"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SPI_DESCRIPTOR) == 0) { /* TRANSLATORS: Title: firmware refers to the flash chip in the computer */ return _("Firmware BIOS Descriptor"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_PREBOOT_DMA_PROTECTION) == 0) { /* TRANSLATORS: Title: DMA as in https://en.wikipedia.org/wiki/DMA_attack */ return _("Pre-boot DMA Protection"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_ENABLED) == 0) { /* TRANSLATORS: Title: BootGuard is a trademark from Intel */ return _("Intel BootGuard"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_VERIFIED) == 0) { /* TRANSLATORS: Title: BootGuard is a trademark from Intel, * verified boot refers to the way the boot process is verified */ return _("Intel BootGuard Verified Boot"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_ACM) == 0) { /* TRANSLATORS: Title: BootGuard is a trademark from Intel, * ACM means to verify the integrity of Initial Boot Block */ return _("Intel BootGuard ACM Protected"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_POLICY) == 0) { /* TRANSLATORS: Title: BootGuard is a trademark from Intel, * error policy is what to do on failure */ return _("Intel BootGuard Error Policy"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_OTP) == 0) { /* TRANSLATORS: Title: BootGuard is a trademark from Intel */ return _("Intel BootGuard Fuse"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_CET_ENABLED) == 0 || g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_CET_ACTIVE) == 0) { /* TRANSLATORS: Title: CET = Control-flow Enforcement Technology */ return _("Control-flow Enforcement Technology"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SMAP) == 0) { /* TRANSLATORS: Title: SMAP = Supervisor Mode Access Prevention */ return _("Supervisor Mode Access Prevention"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_ENCRYPTED_RAM) == 0) { /* TRANSLATORS: Title: Memory contents are encrypted, e.g. Intel TME */ return _("Encrypted RAM"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_IOMMU) == 0) { /* TRANSLATORS: Title: * https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit */ return _("IOMMU Protection"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_KERNEL_LOCKDOWN) == 0) { /* TRANSLATORS: Title: lockdown is a security mode of the kernel */ return _("Linux Kernel Lockdown"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_KERNEL_TAINTED) == 0) { /* TRANSLATORS: Title: if it's tainted or not */ return _("Linux Kernel Verification"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_KERNEL_SWAP) == 0) { /* TRANSLATORS: Title: swap space or swap partition */ return _("Linux Swap"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SUSPEND_TO_RAM) == 0) { /* TRANSLATORS: Title: sleep state */ return _("Suspend To RAM"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SUSPEND_TO_IDLE) == 0) { /* TRANSLATORS: Title: a better sleep state */ return _("Suspend To Idle"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_UEFI_PK) == 0) { /* TRANSLATORS: Title: PK is the 'platform key' for the machine */ return _("UEFI Platform Key"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_UEFI_SECUREBOOT) == 0) { /* TRANSLATORS: Title: SB is a way of locking down UEFI */ return _("UEFI Secure Boot"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_UEFI_BOOTSERVICE_VARS) == 0) { /* TRANSLATORS: Title: Bootservice is when only readable from early-boot */ return _("UEFI Bootservice Variables"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_TPM_EMPTY_PCR) == 0) { /* TRANSLATORS: Title: PCRs (Platform Configuration Registers) shouldn't be empty */ return _("TPM Platform Configuration"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_TPM_RECONSTRUCTION_PCR0) == 0) { /* TRANSLATORS: Title: the PCR is rebuilt from the TPM event log */ return _("TPM Reconstruction"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_TPM_VERSION_20) == 0) { /* TRANSLATORS: Title: TPM = Trusted Platform Module */ return _("TPM v2.0"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_MEI_MANUFACTURING_MODE) == 0) { /* TRANSLATORS: Title: MEI = Intel Management Engine */ return _("Intel Management Engine Manufacturing Mode"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_MEI_OVERRIDE_STRAP) == 0) { /* TRANSLATORS: Title: MEI = Intel Management Engine, and the "override" is enabled * with a jumper -- luckily it is probably not accessible to end users on consumer * boards */ return _("Intel Management Engine Override"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_MEI_KEY_MANIFEST) == 0) { /* TRANSLATORS: Title: MEI = Intel Management Engine, and key refers * to the private/public key used to secure loading of firmware */ return _("MEI Key Manifest"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_MEI_VERSION) == 0) { /* TRANSLATORS: Title: MEI = Intel Management Engine */ return _("Intel Management Engine Version"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_FWUPD_UPDATES) == 0) { /* TRANSLATORS: Title: if firmware updates are available */ return _("Firmware Updates"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_FWUPD_ATTESTATION) == 0) { /* TRANSLATORS: Title: if we can verify the firmware checksums */ return _("Firmware Attestation"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_FWUPD_PLUGINS) == 0) { /* TRANSLATORS: Title: if the fwupd plugins are all present and correct */ return _("Firmware Updater Verification"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_PLATFORM_DEBUG_ENABLED) == 0 || g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_PLATFORM_DEBUG_LOCKED) == 0) { /* TRANSLATORS: Title: Allows debugging of parts using proprietary hardware */ return _("Platform Debugging"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SUPPORTED_CPU) == 0) { /* TRANSLATORS: Title: if fwupd supports HSI on this chip */ return _("Processor Security Checks"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_AMD_ROLLBACK_PROTECTION) == 0) { /* TRANSLATORS: Title: if firmware enforces rollback protection */ return _("AMD Secure Processor Rollback Protection"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_AMD_SPI_REPLAY_PROTECTION) == 0) { /* TRANSLATORS: Title: if hardware enforces control of SPI replays */ return _("AMD Firmware Replay Protection"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_AMD_SPI_WRITE_PROTECTION) == 0) { /* TRANSLATORS: Title: if hardware enforces control of SPI writes */ return _("AMD Firmware Write Protection"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_PLATFORM_FUSED) == 0) { /* TRANSLATORS: Title: if the part has been fused */ return _("Fused Platform"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_BIOS_ROLLBACK_PROTECTION) == 0) { /* TRANSLATORS: Title: if firmware enforces rollback protection */ return _("BIOS Rollback Protection"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_INTEL_GDS) == 0) { /* TRANSLATORS: Title: GDS is where the CPU leaks information */ return _("Intel GDS Mitigation"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_BIOS_CAPSULE_UPDATES) == 0) { /* TRANSLATORS: Title: Whether BIOS Firmware updates is enabled */ return _("BIOS Firmware Updates"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_AMD_SMM_LOCKED) == 0) { /* TRANSLATORS: Title: Whether firmware is locked down */ return _("System Management Mode"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_UEFI_MEMORY_PROTECTION) == 0) { /* TRANSLATORS: Title: is UEFI early-boot memory protection turned on */ return _("UEFI Memory Protection"); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_UEFI_DB) == 0) { /* TRANSLATORS: Title: is UEFI db up-to-date */ return _("UEFI db"); } return NULL; } /* one line describing the attribute */ const gchar * fu_security_attr_get_description(FwupdSecurityAttr *attr) { const gchar *appstream_id = fwupd_security_attr_get_appstream_id(attr); if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SPI_BIOSWE) == 0 || g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SPI_BLE) == 0 || g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_AMD_SPI_WRITE_PROTECTION) == 0) { /* TRANSLATORS: longer description */ return _("Firmware Write Protection protects device firmware memory from being " "tampered with."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SPI_SMM_BWP) == 0) { /* TRANSLATORS: longer description */ return _("Firmware BIOS Region protects device firmware memory from being " "tampered with."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SPI_DESCRIPTOR) == 0) { /* TRANSLATORS: longer description */ return _("Firmware BIOS Descriptor protects device firmware memory from being " "tampered with."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_PREBOOT_DMA_PROTECTION) == 0) { /* TRANSLATORS: longer description */ return _("Pre-boot DMA protection prevents devices from accessing system memory " "after being connected to the computer."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_ENABLED) == 0 || g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_ACM) == 0 || g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_OTP) == 0 || g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_VERIFIED) == 0) { /* TRANSLATORS: longer description */ return _("Intel BootGuard prevents unauthorized device software from operating " "when the device is started."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_INTEL_BOOTGUARD_POLICY) == 0) { /* TRANSLATORS: longer description */ return _( "Intel BootGuard Error Policy ensures the device does not continue to start if " "its device software has been tampered with."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_CET_ENABLED) == 0 || g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_CET_ACTIVE) == 0) { /* TRANSLATORS: longer description */ return _("Control-Flow Enforcement Technology detects and prevents certain " "methods for running malicious software on the device."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SMAP) == 0) { /* TRANSLATORS: longer description */ return _("Supervisor Mode Access Prevention ensures critical parts of " "device memory are not accessed by less secure programs."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_ENCRYPTED_RAM) == 0) { /* TRANSLATORS: longer description */ return _( "Encrypted RAM makes it impossible for information that is stored in device " "memory to be read if the memory chip is removed and accessed."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_IOMMU) == 0) { /* TRANSLATORS: longer description */ return _("IOMMU Protection prevents connected devices from accessing unauthorized " "parts of system memory."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_KERNEL_LOCKDOWN) == 0) { /* TRANSLATORS: longer description */ return _("Linux Kernel Lockdown mode prevents administrator (root) accounts from " "accessing and changing critical parts of system software."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_KERNEL_TAINTED) == 0) { /* TRANSLATORS: longer description */ return _( "Linux Kernel Verification makes sure that critical system software has " "not been tampered with. Using device drivers which are not provided with the " "system can prevent this security feature from working correctly."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_KERNEL_SWAP) == 0) { /* TRANSLATORS: longer description */ return _( "Linux Kernel Swap temporarily saves information to disk as you work. If the " "information is not protected, it could be accessed by someone if they " "obtained the disk."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SUSPEND_TO_RAM) == 0) { /* TRANSLATORS: longer description */ return _("Suspend to RAM allows the device to quickly go to sleep in order to save " "power. While the device has been suspended, its memory could be " "physically removed and its information accessed."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SUSPEND_TO_IDLE) == 0) { /* TRANSLATORS: longer description */ return _("Suspend to Idle allows the device to quickly go to sleep in order to " "save power. While the device has been suspended, its memory could be " "physically removed and its information accessed."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_UEFI_PK) == 0) { /* TRANSLATORS: longer description */ return _("The UEFI Platform Key is used to determine if device software comes from " "a trusted source."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_UEFI_SECUREBOOT) == 0) { /* TRANSLATORS: longer description */ return _("UEFI Secure Boot prevents malicious software from being loaded when the " "device starts."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_UEFI_BOOTSERVICE_VARS) == 0) { /* TRANSLATORS: longer description */ return _("UEFI boot service variables should not be readable from runtime mode."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_TPM_EMPTY_PCR) == 0) { /* TRANSLATORS: longer description */ return _("The TPM (Trusted Platform Module) Platform Configuration is used to " "check whether the device start process has been modified."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_TPM_RECONSTRUCTION_PCR0) == 0) { /* TRANSLATORS: longer description */ return _("The TPM (Trusted Platform Module) Reconstruction is used to check " "whether the device start process has been modified."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_TPM_VERSION_20) == 0) { /* TRANSLATORS: longer description */ return _("TPM (Trusted Platform Module) is a computer chip that detects when " "hardware components have been tampered with."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_MEI_MANUFACTURING_MODE) == 0 || g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_PLATFORM_FUSED) == 0) { /* TRANSLATORS: longer description */ return _("Manufacturing Mode is used when the device is manufactured and " "security features are not yet enabled."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_MEI_OVERRIDE_STRAP) == 0) { /* TRANSLATORS: longer description */ return _("Intel Management Engine Override disables checks for device software " "tampering."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_MEI_KEY_MANIFEST) == 0) { /* TRANSLATORS: longer description */ return _("The Intel Management Engine Key Manifest must be valid so " "that the device firmware can be trusted by the CPU."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_MEI_VERSION) == 0) { /* TRANSLATORS: longer description */ return _("The Intel Management Engine controls device components and needs " "to have a recent version to avoid security issues."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_FWUPD_UPDATES) == 0) { /* TRANSLATORS: longer description */ return _("Device software updates are provided for this device."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_FWUPD_ATTESTATION) == 0) { /* TRANSLATORS: longer description */ return _("Firmware Attestation checks device software using a reference copy, to " "make sure that it has not been changed."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_FWUPD_PLUGINS) == 0) { /* TRANSLATORS: longer description */ return _( "Firmware Updater Verification checks that software used for updating has not " "been tampered with."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_PLATFORM_DEBUG_ENABLED) == 0 || g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_PLATFORM_DEBUG_LOCKED) == 0) { /* TRANSLATORS: longer description */ return _("Platform Debugging allows device security features to be disabled. " "This should only be used by hardware manufacturers."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_SUPPORTED_CPU) == 0) { /* TRANSLATORS: longer description */ return _("Each system should have tests to ensure firmware security."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_AMD_ROLLBACK_PROTECTION) == 0 || g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_AMD_SPI_REPLAY_PROTECTION) == 0 || g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_BIOS_ROLLBACK_PROTECTION) == 0) { /* TRANSLATORS: longer description */ return _("Rollback Protection prevents device software from being downgraded " "to an older version that has security problems."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_INTEL_GDS) == 0) { /* TRANSLATORS: longer description */ return _("CPU Microcode must be updated to mitigate against various " "information-disclosure security issues."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_BIOS_CAPSULE_UPDATES) == 0) { /* TRANSLATORS: longer description */ return _("Enabling firmware updates for the BIOS allows fixing security issues."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_AMD_SMM_LOCKED) == 0) { /* TRANSLATORS: longer description */ return _("System management mode is used by the firmware to access " "resident BIOS code and data."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_UEFI_MEMORY_PROTECTION) == 0) { /* TRANSLATORS: longer description */ return _("The UEFI system can set up memory attributes at boot which prevent " "common exploits from running."); } if (g_strcmp0(appstream_id, FWUPD_SECURITY_ATTR_ID_UEFI_DB) == 0) { /* TRANSLATORS: longer description */ return _("The UEFI db contains the list of valid certificates that can be used to " "authorize what EFI binaries are allowed to run."); } return NULL; } fwupd-2.0.10/src/fu-security-attr-common.h000066400000000000000000000007431501337203100203730ustar00rootroot00000000000000/* * Copyright 2020 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include #include "fu-security-attrs-private.h" gchar * fu_security_attr_get_name(FwupdSecurityAttr *attr) G_GNUC_NON_NULL(1); const gchar * fu_security_attr_get_title(FwupdSecurityAttr *attr) G_GNUC_NON_NULL(1); const gchar * fu_security_attr_get_description(FwupdSecurityAttr *attr) G_GNUC_NON_NULL(1); fwupd-2.0.10/src/fu-self-test.c000066400000000000000000010501471501337203100161730ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include #include #include #include "fwupd-remote-private.h" #include "fwupd-security-attr-private.h" #include "../plugins/test/fu-test-plugin.h" #include "fu-bios-settings-private.h" #include "fu-cabinet.h" #include "fu-client-list.h" #include "fu-config-private.h" #include "fu-console.h" #include "fu-context-private.h" #include "fu-device-list.h" #include "fu-device-private.h" #include "fu-efivars-private.h" #include "fu-engine-config.h" #include "fu-engine-helper.h" #include "fu-engine-requirements.h" #include "fu-engine.h" #include "fu-history.h" #include "fu-idle.h" #include "fu-plugin-list.h" #include "fu-plugin-private.h" #include "fu-release-common.h" #include "fu-remote-list.h" #include "fu-remote.h" #include "fu-security-attr-common.h" #include "fu-smbios-private.h" #include "fu-usb-backend.h" #ifdef HAVE_GIO_UNIX #include "fu-unix-seekable-input-stream.h" #endif typedef struct { FuPlugin *plugin; FuContext *ctx; } FuTest; static GMainLoop *_test_loop = NULL; static guint _test_loop_timeout_id = 0; static gboolean fu_test_hang_check_cb(gpointer user_data) { g_main_loop_quit(_test_loop); _test_loop_timeout_id = 0; return G_SOURCE_REMOVE; } static void fu_test_loop_run_with_timeout(guint timeout_ms) { g_assert_cmpint(_test_loop_timeout_id, ==, 0); g_assert_null(_test_loop); _test_loop = g_main_loop_new(NULL, FALSE); _test_loop_timeout_id = g_timeout_add(timeout_ms, fu_test_hang_check_cb, NULL); g_main_loop_run(_test_loop); } static void fu_test_loop_quit(void) { if (_test_loop_timeout_id > 0) { g_source_remove(_test_loop_timeout_id); _test_loop_timeout_id = 0; } if (_test_loop != NULL) { g_main_loop_quit(_test_loop); g_main_loop_unref(_test_loop); _test_loop = NULL; } } static void fu_self_test_mkroot(void) { if (g_file_test("/tmp/fwupd-self-test", G_FILE_TEST_EXISTS)) { g_autoptr(GError) error = NULL; if (!fu_path_rmtree("/tmp/fwupd-self-test", &error)) g_warning("failed to mkroot: %s", error->message); } g_assert_cmpint(g_mkdir_with_parents("/tmp/fwupd-self-test/var/lib/fwupd", 0755), ==, 0); } static void fu_test_copy_file(const gchar *source, const gchar *target) { gboolean ret; g_autofree gchar *data = NULL; g_autoptr(GError) error = NULL; g_debug("copying %s to %s", source, target); ret = g_file_get_contents(source, &data, NULL, &error); g_assert_no_error(error); g_assert_true(ret); ret = g_file_set_contents(target, data, -1, &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_test_free(FuTest *self) { if (self->ctx != NULL) g_object_unref(self->ctx); g_free(self); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuTest, fu_test_free) #pragma clang diagnostic pop static void fu_client_list_func(void) { g_autoptr(FuClient) client_find = NULL; g_autoptr(FuClient) client = NULL; g_autoptr(FuClient) client_orig = NULL; g_autoptr(FuClientList) client_list = fu_client_list_new(NULL); g_autoptr(GPtrArray) clients_empty = NULL; g_autoptr(GPtrArray) clients_full = NULL; /* ensure empty */ clients_empty = fu_client_list_get_all(client_list); g_assert_cmpint(clients_empty->len, ==, 0); /* register a client, then find it */ client_orig = fu_client_list_register(client_list, ":hello"); g_assert_nonnull(client_orig); client_find = fu_client_list_get_by_sender(client_list, ":hello"); g_assert_nonnull(client_find); g_assert_true(client_orig == client_find); clients_full = fu_client_list_get_all(client_list); g_assert_cmpint(clients_full->len, ==, 1); /* register a duplicate, check properties */ client = fu_client_list_register(client_list, ":hello"); g_assert_nonnull(client); g_assert_true(client_orig == client); g_assert_cmpstr(fu_client_get_sender(client), ==, ":hello"); g_assert_cmpint(fu_client_get_feature_flags(client), ==, FWUPD_FEATURE_FLAG_NONE); g_assert_cmpstr(fu_client_lookup_hint(client, "key"), ==, NULL); g_assert_true(fu_client_has_flag(client, FU_CLIENT_FLAG_ACTIVE)); fu_client_insert_hint(client, "key", "value"); fu_client_set_feature_flags(client, FWUPD_FEATURE_FLAG_UPDATE_ACTION); g_assert_cmpstr(fu_client_lookup_hint(client, "key"), ==, "value"); g_assert_cmpint(fu_client_get_feature_flags(client), ==, FWUPD_FEATURE_FLAG_UPDATE_ACTION); /* emulate disconnect */ fu_client_remove_flag(client, FU_CLIENT_FLAG_ACTIVE); g_assert_false(fu_client_has_flag(client, FU_CLIENT_FLAG_ACTIVE)); } static void fu_idle_func(void) { guint token; g_autoptr(FuIdle) idle = fu_idle_new(); fu_idle_reset(idle); g_assert_false(fu_idle_has_inhibit(idle, FU_IDLE_INHIBIT_TIMEOUT)); g_assert_false(fu_idle_has_inhibit(idle, FU_IDLE_INHIBIT_SIGNALS)); token = fu_idle_inhibit(idle, FU_IDLE_INHIBIT_TIMEOUT | FU_IDLE_INHIBIT_SIGNALS, NULL); g_assert_true(fu_idle_has_inhibit(idle, FU_IDLE_INHIBIT_TIMEOUT)); g_assert_true(fu_idle_has_inhibit(idle, FU_IDLE_INHIBIT_SIGNALS)); /* wrong token */ fu_idle_uninhibit(idle, token + 1); g_assert_true(fu_idle_has_inhibit(idle, FU_IDLE_INHIBIT_SIGNALS)); /* correct token */ fu_idle_uninhibit(idle, token); g_assert_false(fu_idle_has_inhibit(idle, FU_IDLE_INHIBIT_TIMEOUT)); g_assert_false(fu_idle_has_inhibit(idle, FU_IDLE_INHIBIT_SIGNALS)); /* locker section */ { g_autoptr(FuIdleLocker) idle_locker1 = fu_idle_locker_new(idle, FU_IDLE_INHIBIT_TIMEOUT, NULL); g_autoptr(FuIdleLocker) idle_locker2 = fu_idle_locker_new(idle, FU_IDLE_INHIBIT_SIGNALS, NULL); g_assert_nonnull(idle_locker1); g_assert_nonnull(idle_locker2); g_assert_true(fu_idle_has_inhibit(idle, FU_IDLE_INHIBIT_TIMEOUT)); g_assert_true(fu_idle_has_inhibit(idle, FU_IDLE_INHIBIT_SIGNALS)); } g_assert_false(fu_idle_has_inhibit(idle, FU_IDLE_INHIBIT_TIMEOUT)); g_assert_false(fu_idle_has_inhibit(idle, FU_IDLE_INHIBIT_SIGNALS)); } static void fu_engine_generate_md_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; const gchar *tmp; gboolean ret; g_autofree gchar *filename = NULL; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GBytes) data = NULL; g_autoptr(GError) error = NULL; g_autoptr(XbNode) component = NULL; /* put cab file somewhere we can parse it */ filename = g_test_build_filename(G_TEST_BUILT, "..", "libfwupdplugin", "tests", "colorhug", "colorhug-als-3.0.2.cab", NULL); data = fu_bytes_get_contents(filename, &error); g_assert_no_error(error); g_assert_nonnull(data); ret = fu_bytes_set_contents("/tmp/fwupd-self-test/var/cache/fwupd/foo.cab", data, &error); g_assert_no_error(error); g_assert_true(ret); /* load engine and check the device was found */ ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); fu_device_add_instance_id(device, "12345678-1234-1234-1234-123456789012"); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "1.2.3"); component = fu_engine_get_component_by_guids(engine, device); g_assert_nonnull(component); /* check remote ID set */ tmp = xb_node_query_text(component, "../custom/value[@key='fwupd::RemoteId']", NULL); g_assert_cmpstr(tmp, ==, "directory"); /* verify checksums */ tmp = xb_node_query_text(component, "releases/release/checksum[@target='container']", NULL); g_assert_cmpstr(tmp, ==, "71aefb2a9b412833d8c519d5816ef4c5668e5e76"); tmp = xb_node_query_text(component, "releases/release/checksum[@target='content']", NULL); g_assert_cmpstr(tmp, ==, NULL); } static void fu_engine_requirements_missing_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo = NULL; g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error = NULL; const gchar *xml = "" " " " not.going.to.exist" " " " " " " " " ""; /* set up a dummy version */ fu_engine_add_runtime_version(engine, "org.test.dummy", "1.2.3"); /* make the component require one thing */ silo = xb_silo_new_from_xml(xml, &error); g_assert_no_error(error); g_assert_nonnull(silo); component = xb_silo_query_first(silo, "component", &error); g_assert_no_error(error); g_assert_nonnull(component); /* check this fails */ fu_release_set_request(release, request); ret = fu_release_load(release, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_requirements_check(engine, release, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND); g_assert_false(ret); } static void fu_engine_requirements_soft_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo = NULL; g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error = NULL; const gchar *xml = "" " " " not.going.to.exist" " " " " " " " " ""; /* set up a dummy version */ fu_engine_add_runtime_version(engine, "org.test.dummy", "1.2.3"); /* make the component require one thing */ silo = xb_silo_new_from_xml(xml, &error); g_assert_no_error(error); g_assert_nonnull(silo); component = xb_silo_query_first(silo, "component", &error); g_assert_no_error(error); g_assert_nonnull(component); /* check this passes */ fu_release_set_request(release, request); ret = fu_release_load(release, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_requirements_check(engine, release, FWUPD_INSTALL_FLAG_IGNORE_REQUIREMENTS, &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_engine_requirements_client_fail_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo = NULL; g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error = NULL; const gchar *xml = "" " " " detach-action" " " " " " " " " ""; /* make the component require one thing */ silo = xb_silo_new_from_xml(xml, &error); g_assert_no_error(error); g_assert_nonnull(silo); component = xb_silo_query_first(silo, "component", &error); g_assert_no_error(error); g_assert_nonnull(component); /* check this fails */ fu_release_set_request(release, request); ret = fu_release_load(release, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_requirements_check(engine, release, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_false(ret); } static void fu_engine_requirements_client_invalid_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo = NULL; g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error = NULL; const gchar *xml = "" " " " hello-dave" " org.freedesktop.fwupd\n" " " " " " " " " ""; /* make the component require one thing */ silo = xb_silo_new_from_xml(xml, &error); g_assert_no_error(error); g_assert_nonnull(silo); component = xb_silo_query_first(silo, "component", &error); g_assert_no_error(error); g_assert_nonnull(component); /* check this fails */ fu_release_set_request(release, request); ret = fu_release_load(release, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_requirements_check(engine, release, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND); g_assert_false(ret); } static void fu_engine_requirements_client_pass_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo = NULL; g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error = NULL; const gchar *xml = "" " " " detach-action" " org.freedesktop.fwupd\n" " " " " " " " " ""; /* set up a dummy version */ fu_engine_request_set_feature_flags(request, FWUPD_FEATURE_FLAG_DETACH_ACTION); /* make the component require one thing */ silo = xb_silo_new_from_xml(xml, &error); g_assert_no_error(error); g_assert_nonnull(silo); component = xb_silo_query_first(silo, "component", &error); g_assert_no_error(error); g_assert_nonnull(component); /* check this passes */ fu_release_set_request(release, request); ret = fu_release_load(release, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_requirements_check(engine, release, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_engine_requirements_not_hardware_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo = NULL; g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error = NULL; const gchar *xml = "" " " " ffffffff-ffff-ffff-ffff-ffffffffffff" " org.freedesktop.fwupd\n" " " " " " " " " ""; /* set up a dummy version */ fu_engine_request_set_feature_flags(request, FWUPD_FEATURE_FLAG_DETACH_ACTION); /* make the component require one thing */ silo = xb_silo_new_from_xml(xml, &error); g_assert_no_error(error); g_assert_nonnull(silo); component = xb_silo_query_first(silo, "component", &error); g_assert_no_error(error); g_assert_nonnull(component); /* check this passes */ fu_release_set_request(release, request); ret = fu_release_load(release, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_requirements_check(engine, release, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_engine_requirements_version_require_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo = NULL; const gchar *xml = "" " " " 12345678-1234-1234-1234-123456789012" " " " " " " " " " " ""; /* set up a dummy device */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "1.2.3"); fu_device_set_version_bootloader(device, "4.5.6"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_VERSION_CHECK_REQUIRED); fu_device_add_instance_id(device, "12345678-1234-1234-1234-123456789012"); /* make the component require one thing */ silo = xb_silo_new_from_xml(xml, &error); g_assert_no_error(error); g_assert_nonnull(silo); component = xb_silo_query_first(silo, "component", &error); g_assert_no_error(error); g_assert_nonnull(component); /* check this fails */ fu_release_set_device(release, device); fu_release_set_request(release, request); ret = fu_release_load(release, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_true( g_str_has_prefix(error->message, "device requires firmware with a version check")); g_assert_false(ret); } static void fu_engine_requirements_version_lowest_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo = NULL; const gchar *xml = "" " " " 12345678-1234-1234-1234-123456789012" " " " " " " " " " " ""; /* set up a dummy device */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "1.2.3"); fu_device_set_version_lowest(device, "1.2.3"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_instance_id(device, "12345678-1234-1234-1234-123456789012"); /* make the component require one thing */ silo = xb_silo_new_from_xml(xml, &error); g_assert_no_error(error); g_assert_nonnull(silo); component = xb_silo_query_first(silo, "component", &error); g_assert_no_error(error); g_assert_nonnull(component); /* check this fails */ fu_release_set_device(release, device); fu_release_set_request(release, request); ret = fu_release_load(release, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_release_check_version(release, component, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_true( g_str_has_prefix(error->message, "Specified firmware is older than the minimum")); g_assert_false(ret); } static void fu_engine_requirements_unsupported_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo = NULL; g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error = NULL; const gchar *xml = "" " " " " " " " " " " " " ""; /* set up a dummy version */ fu_engine_add_runtime_version(engine, "org.test.dummy", "1.2.3"); /* make the component require one thing that we don't support */ silo = xb_silo_new_from_xml(xml, &error); g_assert_no_error(error); g_assert_nonnull(silo); component = xb_silo_query_first(silo, "component", &error); g_assert_no_error(error); g_assert_nonnull(component); /* check this fails */ fu_release_set_request(release, request); ret = fu_release_load(release, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_requirements_check(engine, release, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_false(ret); } static void fu_engine_requirements_child_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuDevice) child = fu_device_new(NULL); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo = NULL; const gchar *xml = "" " " " not-child" " org.freedesktop.fwupd\n" " " " " " 12345678-1234-1234-1234-123456789012" " " " " " " " " " " " " ""; /* set up a dummy device */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "1.2.3"); fu_device_set_version_bootloader(device, "4.5.6"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_instance_id(device, "12345678-1234-1234-1234-123456789012"); fu_device_set_version_format(child, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(child, "0.0.999"); fu_device_set_physical_id(child, "dummy"); fu_device_add_child(device, child); /* make the component require three things */ silo = xb_silo_new_from_xml(xml, &error); g_assert_no_error(error); g_assert_nonnull(silo); component = xb_silo_query_first(silo, "component", &error); g_assert_no_error(error); g_assert_nonnull(component); /* check this passes */ fu_release_set_device(release, device); fu_release_set_request(release, request); ret = fu_release_load(release, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_requirements_check(engine, release, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_engine_requirements_child_fail_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuDevice) child = fu_device_new(NULL); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo = NULL; const gchar *xml = "" " " " not-child" " org.freedesktop.fwupd\n" " " " " " 12345678-1234-1234-1234-123456789012" " " " " " " " " " " " " ""; /* set up a dummy device */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "1.2.3"); fu_device_set_version_bootloader(device, "4.5.6"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_instance_id(device, "12345678-1234-1234-1234-123456789012"); fu_device_set_version_format(child, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(child, "0.0.1"); fu_device_set_physical_id(child, "dummy"); fu_device_add_child(device, child); /* make the component require three things */ silo = xb_silo_new_from_xml(xml, &error); g_assert_no_error(error); g_assert_nonnull(silo); component = xb_silo_query_first(silo, "component", &error); g_assert_no_error(error); g_assert_nonnull(component); /* check this passes */ fu_release_set_device(release, device); fu_release_set_request(release, request); ret = fu_release_load(release, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_requirements_check(engine, release, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_nonnull( g_strstr_len(error->message, -1, "Not compatible with child device version")); g_assert_false(ret); } static void fu_engine_requirements_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(GError) error = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo = NULL; const gchar *xml = "" " " " org.test.dummy" " 6ff95c9c-ae41-5f59-9d90-3ec1ea66091e" " org.freedesktop.fwupd\n" " org.freedesktop.fwupd\n" " " " " " " " " ""; /* set up some dummy versions */ fu_engine_add_runtime_version(engine, "org.test.dummy", "1.2.3"); fu_engine_add_runtime_version(engine, "com.hughski.colorhug", "7.8.9"); /* make the component require one thing */ silo = xb_silo_new_from_xml(xml, &error); g_assert_no_error(error); g_assert_nonnull(silo); component = xb_silo_query_first(silo, "component", &error); g_assert_no_error(error); g_assert_nonnull(component); /* check this passes */ fu_release_set_request(release, request); ret = fu_release_load(release, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_requirements_check(engine, release, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_engine_requirements_device_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo = NULL; const gchar *xml = "" " " " " " bootloader" " vendor-id" #ifdef __linux__ " org.kernel" #endif " org.freedesktop.fwupd\n" " " " " " 12345678-1234-1234-1234-123456789012" " " " " " " " " " " " " ""; /* set up a dummy device */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "1.2.3"); fu_device_set_version_bootloader(device, "4.5.6"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_build_vendor_id_u16(device, "PCI", 0x0000); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_VERSION_CHECK_REQUIRED); fu_device_add_instance_id(device, "12345678-1234-1234-1234-123456789012"); /* make the component require three things */ silo = xb_silo_new_from_xml(xml, &error); g_assert_no_error(error); g_assert_nonnull(silo); component = xb_silo_query_first(silo, "component", &error); g_assert_no_error(error); g_assert_nonnull(component); /* check this passes */ fu_release_set_device(release, device); fu_release_set_request(release, request); ret = fu_release_load(release, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_requirements_check(engine, release, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); /* check this fails, as the wrong requirement is specified */ fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_ENFORCE_REQUIRES); ret = fu_engine_requirements_check(engine, release, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_nonnull(g_strstr_len(error->message, -1, "child, parent or sibling requirement")); g_assert_false(ret); #ifndef SUPPORTED_BUILD /* we can force this */ g_clear_error(&error); ret = fu_engine_requirements_check(engine, release, FWUPD_INSTALL_FLAG_IGNORE_REQUIREMENTS, &error); g_assert_no_error(error); g_assert_true(ret); #endif } static void fu_engine_requirements_device_plain_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo = NULL; const gchar *xml = "" " " " 12345678-1234-1234-1234-123456789012" " " " " " " " " " " " " " " ""; /* set up a dummy device */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_PLAIN); fu_device_set_version(device, "5101AALB"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_instance_id(device, "12345678-1234-1234-1234-123456789012"); /* make the component require three things */ silo = xb_silo_new_from_xml(xml, &error); g_assert_no_error(error); g_assert_nonnull(silo); component = xb_silo_query_first(silo, "component", &error); g_assert_no_error(error); g_assert_nonnull(component); /* check this passes */ fu_release_set_device(release, device); fu_release_set_request(release, request); ret = fu_release_load(release, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpstr(fu_release_get_filename(release), ==, "bios.cab"); ret = fu_engine_requirements_check(engine, release, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_engine_requirements_version_format_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo = NULL; const gchar *xml = "" " " " 12345678-1234-1234-1234-123456789012" " " " " " " " " " " " " " " " triplet" " " ""; /* set up a dummy device */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_QUAD); fu_device_set_version(device, "1.2.3.4"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_instance_id(device, "12345678-1234-1234-1234-123456789012"); /* make the component require three things */ silo = xb_silo_new_from_xml(xml, &error); g_assert_no_error(error); g_assert_nonnull(silo); component = xb_silo_query_first(silo, "component", &error); g_assert_no_error(error); g_assert_nonnull(component); /* check this fails */ fu_release_set_device(release, device); fu_release_set_request(release, request); ret = fu_release_load(release, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_release_check_version(release, component, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_nonnull( g_strstr_len(error->message, -1, "Firmware version formats were different")); g_assert_false(ret); } static void fu_engine_requirements_only_upgrade_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo = NULL; const gchar *xml = "" " " " 12345678-1234-1234-1234-123456789012" " " " " " " " " ""; /* set up a dummy device */ fu_device_set_version(device, "1.2.4"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_ONLY_VERSION_UPGRADE); fu_device_add_instance_id(device, "12345678-1234-1234-1234-123456789012"); /* make the component require three things */ silo = xb_silo_new_from_xml(xml, &error); g_assert_no_error(error); g_assert_nonnull(silo); component = xb_silo_query_first(silo, "component", &error); g_assert_no_error(error); g_assert_nonnull(component); /* check this fails */ fu_release_set_device(release, device); fu_release_set_request(release, request); ret = fu_release_load(release, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_release_check_version(release, component, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_nonnull(g_strstr_len(error->message, -1, "Device only supports version upgrades")); g_assert_false(ret); } static void fu_engine_plugin_device_gtype(FuTest *self, GType gtype) { gboolean ret; g_autofree gchar *str = NULL; g_autoptr(FuDevice) device = NULL; g_autoptr(FuProgress) progress_tmp = fu_progress_new(G_STRLOC); g_autoptr(FuSecurityAttrs) attrs = fu_security_attrs_new(); g_autoptr(GError) error = NULL; g_autoptr(GHashTable) metadata_post = NULL; g_autoptr(GHashTable) metadata_pre = NULL; const gchar *nolocker[] = { "FuPciPspDevice", "FuSynapticsRmiPs2Device", "FuUefiSbatDevice", NULL, }; g_debug("loading %s", g_type_name(gtype)); device = g_object_new(gtype, "context", self->ctx, "physical-id", "/sys", NULL); g_assert_nonnull(device); fu_device_set_plugin(device, "test"); /* version convert */ if (fu_device_get_version_format(device) != FWUPD_VERSION_FORMAT_UNKNOWN) fu_device_set_version_raw(device, 0); /* progress steps */ fu_device_set_progress(device, progress_tmp); /* report metadata */ metadata_pre = fu_device_report_metadata_pre(device); if (metadata_pre != NULL) g_debug("got %u metadata items", g_hash_table_size(metadata_pre)); metadata_post = fu_device_report_metadata_post(device); if (metadata_post != NULL) g_debug("got %u metadata items", g_hash_table_size(metadata_post)); /* security attrs */ fu_device_add_security_attrs(device, attrs); /* quirk kvs */ ret = fu_device_set_quirk_kv(device, "NoGoingTo", "Exist", FU_CONTEXT_QUIRK_SOURCE_FALLBACK, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_false(ret); /* to string */ str = fu_device_to_string(device); g_assert_nonnull(str); /* ->probe() and ->setup */ if (!g_strv_contains(nolocker, g_type_name(gtype))) { g_autoptr(FuDeviceLocker) locker = fu_device_locker_new(device, NULL); if (locker != NULL) g_debug("did ->probe() and ->setup()!"); } } static void fu_engine_plugin_firmware_gtype(FuTest *self, GType gtype) { gboolean ret; g_autoptr(FuFirmware) firmware = NULL; g_autoptr(GBytes) blob = NULL; g_autoptr(GBytes) fw = g_bytes_new_static((const guint8 *)"x", 1); g_autoptr(GError) error = NULL; const gchar *noxml[] = { "FuArchiveFirmware", "FuGenesysUsbhubFirmware", "FuIntelThunderboltFirmware", "FuIntelThunderboltNvm", "FuUefiUpdateInfo", NULL, }; g_debug("loading %s", g_type_name(gtype)); firmware = g_object_new(gtype, NULL); g_assert_nonnull(firmware); /* ensure we have data set even if parsing fails */ fu_firmware_set_bytes(firmware, fw); /* version convert */ if (fu_firmware_get_version_format(firmware) != FWUPD_VERSION_FORMAT_UNKNOWN) fu_firmware_set_version_raw(firmware, 0); /* parse nonsense */ if (gtype != FU_TYPE_FIRMWARE && !fu_firmware_has_flag(FU_FIRMWARE(firmware), FU_FIRMWARE_FLAG_NO_AUTO_DETECTION)) { ret = fu_firmware_parse_bytes(firmware, fw, 0x0, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH | FU_FIRMWARE_PARSE_FLAG_CACHE_STREAM, NULL); g_assert_false(ret); } /* write */ blob = fu_firmware_write(firmware, NULL); if (blob != NULL && g_bytes_get_size(blob) > 0) g_debug("saved 0x%x bytes", (guint)g_bytes_get_size(blob)); /* export -> build */ if (!g_strv_contains(noxml, g_type_name(gtype))) { g_autofree gchar *xml = fu_firmware_export_to_xml( firmware, FU_FIRMWARE_EXPORT_FLAG_INCLUDE_DEBUG | FU_FIRMWARE_EXPORT_FLAG_ASCII_DATA, NULL); if (xml != NULL) { ret = fu_firmware_build_from_xml(firmware, xml, &error); g_assert_no_error(error); g_assert_true(ret); } } } static void fu_engine_plugin_gtypes_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; GPtrArray *plugins; gboolean ret; g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FuSecurityAttrs) attrs = fu_security_attrs_new(); g_autoptr(GArray) firmware_gtypes = NULL; g_autoptr(GError) error = NULL; g_autoptr(XbSilo) silo_empty = xb_silo_new(); const gchar *plugin_names_safe[] = { "linux_lockdown", "linux_sleep", "linux_swap", "linux_tainted", }; const gchar *external_plugins[] = { "flashrom", "modem-manager", }; /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* load these from the build directory, not the install directory */ if (g_getenv("G_TEST_BUILDDIR") != NULL) { g_autoptr(GPtrArray) external_plugin_dirs = g_ptr_array_new_with_free_func(g_free); g_autofree gchar *external_plugindir = NULL; for (guint i = 0; i < G_N_ELEMENTS(external_plugins); i++) { g_ptr_array_add(external_plugin_dirs, g_test_build_filename(G_TEST_BUILT, "..", "plugins", external_plugins[i], NULL)); } external_plugindir = fu_strjoin(",", external_plugin_dirs); (void)g_setenv("FWUPD_LIBDIR_PKG", external_plugindir, TRUE); } /* load all plugins */ ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS | FU_ENGINE_LOAD_FLAG_EXTERNAL_PLUGINS, progress, &error); g_assert_no_error(error); g_assert_true(ret); plugins = fu_engine_get_plugins(engine); g_assert_nonnull(plugins); g_assert_cmpint(plugins->len, >, 5); /* start up some "safe" plugins */ for (guint i = 0; i < G_N_ELEMENTS(plugin_names_safe); i++) { FuPlugin *plugin = fu_engine_get_plugin_by_name(engine, plugin_names_safe[i], NULL); if (plugin != NULL) { ret = fu_plugin_runner_startup(plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); } } /* add security attrs where possible */ for (guint i = 0; i < plugins->len; i++) { FuPlugin *plugin = g_ptr_array_index(plugins, i); fu_plugin_runner_add_security_attrs(plugin, attrs); } /* run the post-reboot action */ for (guint i = 0; i < plugins->len; i++) { FuPlugin *plugin = g_ptr_array_index(plugins, i); g_autoptr(FuDevice) device_nop = fu_device_new(self->ctx); g_autoptr(GError) error_local = NULL; if (!fu_plugin_runner_reboot_cleanup(plugin, device_nop, &error_local)) g_debug("ignoring: %s", error_local->message); } /* run the device unlock action */ for (guint i = 0; i < plugins->len; i++) { FuPlugin *plugin = g_ptr_array_index(plugins, i); g_autoptr(FuDevice) device_nop = fu_device_new(self->ctx); g_autoptr(GError) error_local = NULL; if (!fu_plugin_runner_unlock(plugin, device_nop, &error_local)) g_debug("ignoring: %s", error_local->message); } /* run the backend-device-added action */ for (guint i = 0; i < plugins->len; i++) { FuPlugin *plugin = g_ptr_array_index(plugins, i); GArray *device_gtypes = fu_plugin_get_device_gtypes(plugin); if (device_gtypes == NULL || device_gtypes->len == 0) { g_autoptr(FuDevice) device_nop = fu_device_new(self->ctx); g_autoptr(GError) error_local = NULL; if (!fu_plugin_runner_backend_device_added(plugin, device_nop, progress, &error_local)) g_debug("ignoring: %s", error_local->message); } } /* create each custom device with a context only */ for (guint i = 0; i < plugins->len; i++) { FuPlugin *plugin = g_ptr_array_index(plugins, i); GArray *device_gtypes = fu_plugin_get_device_gtypes(plugin); for (guint j = 0; device_gtypes != NULL && j < device_gtypes->len; j++) { GType gtype = g_array_index(device_gtypes, GType, j); fu_engine_plugin_device_gtype(self, gtype); } } /* create each firmware */ firmware_gtypes = fu_context_get_firmware_gtypes(self->ctx); for (guint j = 0; j < firmware_gtypes->len; j++) { GType gtype = g_array_index(firmware_gtypes, GType, j); fu_engine_plugin_firmware_gtype(self, gtype); } } static void fu_engine_requirements_sibling_device_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device1 = fu_device_new(self->ctx); g_autoptr(FuDevice) device2 = fu_device_new(self->ctx); g_autoptr(FuDevice) unrelated_device3 = fu_device_new(self->ctx); g_autoptr(FuDevice) parent = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuRelease) release1 = fu_release_new(); g_autoptr(FuRelease) release2 = fu_release_new(); g_autoptr(GError) error = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo = NULL; g_autoptr(XbSilo) silo_empty = xb_silo_new(); const gchar *xml = "" " " " 1ff60ab2-3905-06a1-b476-0371f00c9e9b" " org.freedesktop.fwupd\n" " " " " " 12345678-1234-1234-1234-123456789012" " " " " " " " " " " " " ""; /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* set up a dummy device */ fu_device_set_id(device1, "id1"); fu_device_set_version_format(device1, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device1, "1.2.3"); fu_device_build_vendor_id_u16(device1, "USB", 0xFFFF); fu_device_add_flag(device1, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device1, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_instance_id(device1, "12345678-1234-1234-1234-123456789012"); fu_device_add_protocol(device1, "com.acme"); fu_engine_add_device(engine, device1); /* setup the parent */ fu_device_set_id(parent, "parent"); fu_device_set_version_format(parent, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(parent, "1.0.0"); fu_device_add_flag(parent, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(parent, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_instance_id(parent, "42f3d696-0b6f-4d69-908f-357f98ef115e"); fu_device_add_protocol(parent, "com.acme"); fu_device_add_child(parent, device1); fu_engine_add_device(engine, parent); /* set up a different device */ fu_device_set_id(unrelated_device3, "id3"); fu_device_build_vendor_id(unrelated_device3, "USB", "FFFF"); fu_device_add_protocol(unrelated_device3, "com.acme"); fu_device_set_name(unrelated_device3, "Foo bar device"); fu_device_set_version_format(unrelated_device3, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(unrelated_device3, "1.5.3"); fu_device_add_flag(unrelated_device3, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(unrelated_device3, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_instance_id(unrelated_device3, "3e455c08-352e-4a16-84d3-f04287289fa2"); fu_engine_add_device(engine, unrelated_device3); /* import firmware metainfo */ silo = xb_silo_new_from_xml(xml, &error); g_assert_no_error(error); g_assert_nonnull(silo); component = xb_silo_query_first(silo, "component", &error); g_assert_no_error(error); g_assert_nonnull(component); /* check this fails */ fu_release_set_device(release1, device1); fu_release_set_request(release1, request); ret = fu_release_load(release1, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_requirements_check(engine, release1, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_false(ret); g_clear_error(&error); /* set up a sibling device */ fu_device_set_id(device2, "id2"); fu_device_build_vendor_id_u16(device2, "USB", 0xFFFF); fu_device_add_protocol(device2, "com.acme"); fu_device_set_name(device2, "Secondary firmware"); fu_device_set_version_format(device2, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device2, "4.5.6"); fu_device_add_flag(device2, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device2, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_instance_id(device2, "1ff60ab2-3905-06a1-b476-0371f00c9e9b"); fu_device_add_child(parent, device2); fu_engine_add_device(engine, device2); /* check this passes */ fu_release_set_device(release2, device1); fu_release_set_request(release2, request); ret = fu_release_load(release2, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_requirements_check(engine, release2, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); /* check this still works, as a child requirement is specified */ fu_device_add_private_flag(device1, FU_DEVICE_PRIVATE_FLAG_ENFORCE_REQUIRES); ret = fu_engine_requirements_check(engine, release2, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_engine_requirements_other_device_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device1 = fu_device_new(self->ctx); g_autoptr(FuDevice) device2 = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo = NULL; g_autoptr(XbSilo) silo_empty = xb_silo_new(); const gchar *xml = "" " " " 1ff60ab2-3905-06a1-b476-0371f00c9e9b" " org.freedesktop.fwupd\n" " " " " " 12345678-1234-1234-1234-123456789012" " " " " " " " " " " " " ""; /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* set up a dummy device */ fu_device_set_version_format(device1, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device1, "1.2.3"); fu_device_add_flag(device1, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device1, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_instance_id(device1, "12345678-1234-1234-1234-123456789012"); /* set up a different device */ fu_device_set_id(device2, "id2"); fu_device_build_vendor_id_u16(device2, "USB", 0xFFFF); fu_device_add_protocol(device2, "com.acme"); fu_device_set_name(device2, "Secondary firmware"); fu_device_set_version_format(device2, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device2, "4.5.6"); fu_device_add_instance_id(device2, "1ff60ab2-3905-06a1-b476-0371f00c9e9b"); fu_engine_add_device(engine, device2); /* import firmware metainfo */ silo = xb_silo_new_from_xml(xml, &error); g_assert_no_error(error); g_assert_nonnull(silo); component = xb_silo_query_first(silo, "component", &error); g_assert_no_error(error); g_assert_nonnull(component); /* check this passes */ fu_release_set_device(release, device1); fu_release_set_request(release, request); ret = fu_release_load(release, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_requirements_check(engine, release, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_engine_requirements_protocol_check_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; g_autoptr(FuDevice) device1 = fu_device_new(self->ctx); g_autoptr(FuDevice) device2 = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(GPtrArray) devices = NULL; g_autoptr(FuRelease) release1 = fu_release_new(); g_autoptr(FuRelease) release2 = fu_release_new(); g_autoptr(GError) error = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo = NULL; g_autoptr(XbSilo) silo_empty = xb_silo_new(); gboolean ret; const gchar *xml = "" " " " 12345678-1234-1234-1234-123456789012" " " " " " " " " " " " " " " " org.bar" " " ""; /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); fu_device_set_id(device1, "NVME"); fu_device_add_protocol(device1, "com.acme"); fu_device_set_name(device1, "NVME device"); fu_device_build_vendor_id(device1, "DMI", "ACME"); fu_device_set_version_format(device1, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device1, "1.2.3"); fu_device_add_instance_id(device1, "12345678-1234-1234-1234-123456789012"); fu_device_add_flag(device1, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device1, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_engine_add_device(engine, device1); fu_device_set_id(device2, "UEFI"); fu_device_add_protocol(device2, "org.bar"); fu_device_set_name(device2, "UEFI device"); fu_device_build_vendor_id(device2, "DMI", "ACME"); fu_device_set_version_format(device2, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device2, "1.2.3"); fu_device_add_instance_id(device2, "12345678-1234-1234-1234-123456789012"); fu_device_add_flag(device2, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device2, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_engine_add_device(engine, device2); /* make sure both devices added */ devices = fu_engine_get_devices(engine, &error); g_assert_no_error(error); g_assert_nonnull(devices); g_assert_cmpint(devices->len, ==, 2); /* import firmware metainfo */ silo = xb_silo_new_from_xml(xml, &error); g_assert_no_error(error); g_assert_nonnull(silo); component = xb_silo_query_first(silo, "component", &error); g_assert_no_error(error); g_assert_nonnull(component); /* check this fails */ fu_release_set_device(release1, device1); fu_release_set_request(release1, request); ret = fu_release_load(release1, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_false(ret); g_clear_error(&error); /* check this passes */ fu_release_set_device(release2, device2); fu_release_set_request(release2, request); ret = fu_release_load(release2, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_engine_requirements_parent_device_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device1 = fu_device_new(self->ctx); g_autoptr(FuDevice) device2 = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo = NULL; g_autoptr(XbSilo) silo_empty = xb_silo_new(); const gchar *xml = "" " " " " " 12345678-1234-1234-1234-123456789012" " org.freedesktop.fwupd\n" " " " " " 1ff60ab2-3905-06a1-b476-0371f00c9e9b" " " " " " " " " " " " " ""; /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* set up child device */ fu_device_set_id(device2, "child"); fu_device_set_name(device2, "child"); fu_device_set_version_format(device2, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device2, "4.5.6"); fu_device_add_flag(device2, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device2, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_instance_id(device2, "1ff60ab2-3905-06a1-b476-0371f00c9e9b"); /* set up a parent device */ fu_device_set_id(device1, "parent"); fu_device_build_vendor_id_u16(device1, "USB", 0xFFFF); fu_device_add_protocol(device1, "com.acme"); fu_device_set_name(device1, "parent"); fu_device_set_version_format(device1, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device1, "1.2.3"); fu_device_add_instance_id(device1, "12345678-1234-1234-1234-123456789012"); fu_device_add_child(device1, device2); fu_engine_add_device(engine, device1); /* import firmware metainfo */ silo = xb_silo_new_from_xml(xml, &error); g_assert_no_error(error); g_assert_nonnull(silo); component = xb_silo_query_first(silo, "component", &error); g_assert_no_error(error); g_assert_nonnull(component); /* check this passes */ fu_release_set_device(release, device2); fu_release_set_request(release, request); ret = fu_release_load(release, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_requirements_check(engine, release, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_engine_requirements_child_device_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device1 = fu_device_new(self->ctx); g_autoptr(FuDevice) device2 = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo = NULL; g_autoptr(XbSilo) silo_empty = xb_silo_new(); const gchar *xml = "" " " " 1ff60ab2-3905-06a1-b476-0371f00c9e9b" " org.freedesktop.fwupd\n" " " " " " 12345678-1234-1234-1234-123456789012" " " " " " " " " " " " " ""; /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* set up a parent device */ fu_device_set_id(device1, "parent"); fu_device_build_vendor_id_u16(device1, "USB", 0xFFFF); fu_device_add_protocol(device1, "com.acme"); fu_device_set_name(device1, "parent"); fu_device_set_version_format(device1, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device1, "1.2.3"); fu_device_add_instance_id(device1, "12345678-1234-1234-1234-123456789012"); fu_device_add_flag(device1, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device1, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); /* set up child device */ fu_device_set_id(device2, "child"); fu_device_set_name(device2, "child"); fu_device_set_version_format(device2, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device2, "4.5.6"); fu_device_add_instance_id(device2, "1ff60ab2-3905-06a1-b476-0371f00c9e9b"); fu_device_add_child(device1, device2); fu_engine_add_device(engine, device1); /* import firmware metainfo */ silo = xb_silo_new_from_xml(xml, &error); g_assert_no_error(error); g_assert_nonnull(silo); component = xb_silo_query_first(silo, "component", &error); g_assert_no_error(error); g_assert_nonnull(component); /* check this passes */ fu_release_set_device(release, device1); fu_release_set_request(release, request); ret = fu_release_load(release, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_requirements_check(engine, release, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_engine_device_parent_guid_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; g_autoptr(FuDevice) device1 = fu_device_new(self->ctx); g_autoptr(FuDevice) device2 = fu_device_new(self->ctx); g_autoptr(FuDevice) device3 = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(XbSilo) silo_empty = xb_silo_new(); /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* add child */ fu_device_set_id(device1, "child"); fu_device_build_vendor_id_u16(device2, "USB", 0xFFFF); fu_device_add_protocol(device2, "com.acme"); fu_device_add_instance_id(device1, "child-GUID-1"); fu_device_add_parent_guid(device1, "parent-GUID"); fu_engine_add_device(engine, device1); /* parent */ fu_device_set_id(device2, "parent"); fu_device_build_vendor_id_u16(device2, "USB", 0xFFFF); fu_device_add_protocol(device2, "com.acme"); fu_device_add_instance_id(device2, "parent-GUID"); fu_device_set_vendor(device2, "oem"); /* add another child */ fu_device_set_id(device3, "child2"); fu_device_add_instance_id(device3, "child-GUID-2"); fu_device_add_parent_guid(device3, "parent-GUID"); fu_device_add_child(device2, device3); /* add two together */ fu_engine_add_device(engine, device2); /* this is normally done by fu_plugin_device_add() */ fu_engine_add_device(engine, device3); /* verify both children were adopted */ g_assert_true(fu_device_get_parent(device3) == device2); g_assert_true(fu_device_get_parent(device1) == device2); g_assert_cmpstr(fu_device_get_vendor(device3), ==, "oem"); /* verify order */ g_assert_cmpint(fu_device_get_order(device1), ==, -1); g_assert_cmpint(fu_device_get_order(device2), ==, 0); g_assert_cmpint(fu_device_get_order(device3), ==, -1); } static void fu_engine_device_parent_id_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; g_autoptr(FuDevice) device1 = fu_device_new(self->ctx); g_autoptr(FuDevice) device2 = fu_device_new(self->ctx); g_autoptr(FuDevice) device3 = fu_device_new(self->ctx); g_autoptr(FuDevice) device4 = fu_device_new(self->ctx); g_autoptr(FuDevice) device5 = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(XbSilo) silo_empty = xb_silo_new(); /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* add child */ fu_device_set_id(device1, "child1"); fu_device_set_name(device1, "Child1"); fu_device_set_physical_id(device1, "child-ID1"); fu_device_build_vendor_id_u16(device1, "USB", 0xFFFF); fu_device_add_protocol(device1, "com.acme"); fu_device_add_instance_id(device1, "child-GUID-1"); fu_device_add_parent_physical_id(device1, "parent-ID-notfound"); fu_device_add_parent_physical_id(device1, "parent-ID"); fu_engine_add_device(engine, device1); /* parent */ fu_device_set_id(device2, "parent"); fu_device_set_name(device2, "Parent"); fu_device_set_backend_id(device2, "/sys/devices/foo/bar/baz"); fu_device_set_physical_id(device2, "parent-ID"); fu_device_build_vendor_id_u16(device2, "USB", 0xFFFF); fu_device_add_protocol(device2, "com.acme"); fu_device_add_instance_id(device2, "parent-GUID"); fu_device_set_vendor(device2, "oem"); fu_device_add_private_flag(device2, FU_DEVICE_PRIVATE_FLAG_AUTO_PARENT_CHILDREN); /* add another child */ fu_device_set_id(device3, "child2"); fu_device_set_name(device3, "Child2"); fu_device_set_physical_id(device3, "child-ID2"); fu_device_add_instance_id(device3, "child-GUID-2"); fu_device_add_parent_physical_id(device3, "parent-ID"); fu_device_add_child(device2, device3); /* add two together */ fu_engine_add_device(engine, device2); /* add non-child */ fu_device_set_id(device4, "child4"); fu_device_set_name(device4, "Child4"); fu_device_set_physical_id(device4, "child-ID4"); fu_device_build_vendor_id(device4, "USB", "FFFF"); fu_device_add_protocol(device4, "com.acme"); fu_device_add_instance_id(device4, "child-GUID-4"); fu_device_add_parent_physical_id(device4, "parent-ID"); fu_engine_add_device(engine, device4); /* this is normally done by fu_plugin_device_add() */ fu_engine_add_device(engine, device4); /* add child with the parent backend ID */ fu_device_set_id(device5, "child5"); fu_device_set_name(device5, "Child5"); fu_device_set_physical_id(device5, "child-ID5"); fu_device_build_vendor_id(device5, "USB", "FFFF"); fu_device_add_protocol(device5, "com.acme"); fu_device_add_instance_id(device5, "child-GUID-5"); fu_device_add_parent_backend_id(device5, "/sys/devices/foo/bar/baz"); fu_engine_add_device(engine, device5); /* this is normally done by fu_plugin_device_add() */ fu_engine_add_device(engine, device5); /* verify both children were adopted */ g_assert_true(fu_device_get_parent(device3) == device2); g_assert_true(fu_device_get_parent(device4) == device2); g_assert_true(fu_device_get_parent(device5) == device2); g_assert_true(fu_device_get_parent(device1) == device2); g_assert_cmpstr(fu_device_get_vendor(device3), ==, "oem"); } static void fu_engine_partial_hash_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device1 = fu_device_new(self->ctx); g_autoptr(FuDevice) device2 = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuPlugin) plugin = fu_plugin_new(NULL); g_autoptr(GError) error = NULL; g_autoptr(GError) error_none = NULL; g_autoptr(GError) error_both = NULL; g_autoptr(XbSilo) silo_empty = xb_silo_new(); /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* set up dummy plugin */ fu_plugin_set_name(plugin, "test"); fu_engine_add_plugin(engine, plugin); /* add two dummy devices */ fu_device_set_id(device1, "device1"); fu_device_build_vendor_id_u16(device1, "USB", 0xFFFF); fu_device_add_protocol(device1, "com.acme"); fu_device_set_plugin(device1, "test"); fu_device_add_instance_id(device1, "12345678-1234-1234-1234-123456789012"); fu_engine_add_device(engine, device1); fu_device_set_id(device2, "device21"); fu_device_build_vendor_id_u16(device2, "USB", 0xFFFF); fu_device_add_protocol(device2, "com.acme"); fu_device_set_plugin(device2, "test"); fu_device_set_equivalent_id(device2, "b92f5b7560b84ca005a79f5a15de3c003ce494cf"); fu_device_add_instance_id(device2, "87654321-1234-1234-1234-123456789012"); fu_engine_add_device(engine, device2); /* match nothing */ ret = fu_engine_unlock(engine, "deadbeef", &error_none); g_assert_error(error_none, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND); g_assert_false(ret); /* match both */ ret = fu_engine_unlock(engine, "9", &error_both); g_assert_error(error_both, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_false(ret); /* match one exactly */ fu_device_add_flag(device1, FWUPD_DEVICE_FLAG_LOCKED); fu_device_add_flag(device2, FWUPD_DEVICE_FLAG_LOCKED); ret = fu_engine_unlock(engine, "934b4162a6daa0b033d649c8d464529cec41d3de", &error); g_assert_no_error(error); g_assert_true(ret); /* match one partially */ fu_device_add_flag(device1, FWUPD_DEVICE_FLAG_LOCKED); fu_device_add_flag(device2, FWUPD_DEVICE_FLAG_LOCKED); ret = fu_engine_unlock(engine, "934b", &error); g_assert_no_error(error); g_assert_true(ret); /* match equivalent ID */ fu_device_add_flag(device1, FWUPD_DEVICE_FLAG_LOCKED); fu_device_add_flag(device2, FWUPD_DEVICE_FLAG_LOCKED); ret = fu_engine_unlock(engine, "b92f", &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_engine_device_unlock_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; FwupdRelease *rel; gboolean ret; g_autofree gchar *filename = NULL; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(GFile) file = NULL; g_autoptr(XbBuilder) builder = xb_builder_new(); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_autoptr(XbSilo) silo = NULL; /* load engine to get FuConfig set up */ ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* add the hardcoded 'fwupd' metadata */ filename = g_test_build_filename(G_TEST_DIST, "tests", "metadata.xml", NULL); file = g_file_new_for_path(filename); ret = xb_builder_source_load_file(source, file, XB_BUILDER_SOURCE_FLAG_NONE, NULL, &error); g_assert_no_error(error); g_assert_true(ret); xb_builder_import_source(builder, source); silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, &error); g_assert_no_error(error); g_assert_nonnull(silo); fu_engine_set_silo(engine, silo); /* add a dummy device */ fu_device_set_id(device, "UEFI-dummy-dev0"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_protocol(device, "com.acme"); fu_device_add_instance_id(device, "2d47f29b-83a2-4f31-a2e8-63474f4d4c2e"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_LOCKED); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_PLAIN); fu_engine_add_device(engine, device); /* ensure the metainfo was matched */ rel = fwupd_device_get_release_default(FWUPD_DEVICE(device)); g_assert_nonnull(rel); g_assert_false(fwupd_release_has_flag(rel, FWUPD_RELEASE_FLAG_TRUSTED_REPORT)); } static void fu_engine_device_equivalent_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device1 = fu_device_new(self->ctx); g_autoptr(FuDevice) device2 = fu_device_new(self->ctx); g_autoptr(FuDevice) device_best = NULL; g_autoptr(FuDevice) device_worst = NULL; g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GPtrArray) devices = NULL; g_autoptr(GError) error = NULL; /* load engine to get FuConfig set up */ ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* add a wireless (worse) device */ fu_device_set_id(device1, "99249eb1bd9ef0b6e192b271a8cb6a3090cfec7a"); fu_device_set_name(device1, "device1"); fu_device_build_vendor_id_u16(device1, "USB", 0xFFFF); fu_device_add_protocol(device1, "com.acme"); fu_device_add_instance_id(device1, "2d47f29b-83a2-4f31-a2e8-63474f4d4c2e"); fu_device_add_flag(device1, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device1, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_engine_add_device(engine, device1); /* add a wired (better) device */ fu_device_set_id(device2, "1a8d0d9a96ad3e67ba76cf3033623625dc6d6882"); fu_device_set_name(device2, "device2"); fu_device_set_equivalent_id(device2, "99249eb1bd9ef0b6e192b271a8cb6a3090cfec7a"); fu_device_set_priority(device2, 999); fu_device_build_vendor_id_u16(device2, "USB", 0xFFFF); fu_device_add_protocol(device2, "com.acme"); fu_device_add_instance_id(device2, "2d47f29b-83a2-4f31-a2e8-63474f4d4c2e"); fu_device_add_flag(device2, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device2, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_engine_add_device(engine, device2); /* make sure the daemon chooses the best device */ devices = fu_engine_get_devices(engine, &error); g_assert_no_error(error); g_assert_nonnull(devices); g_assert_cmpint(devices->len, ==, 2); device_best = fu_engine_get_device(engine, "9924", &error); g_assert_no_error(error); g_assert_nonnull(device_best); g_assert_cmpstr(fu_device_get_id(device_best), ==, "1a8d0d9a96ad3e67ba76cf3033623625dc6d6882"); g_assert_true(fu_device_has_flag(device_best, FWUPD_DEVICE_FLAG_UPDATABLE)); g_assert_false(fu_device_has_problem(device_best, FWUPD_DEVICE_PROBLEM_LOWER_PRIORITY)); /* get the worst device and make sure it's not updatable */ for (guint i = 0; i < devices->len; i++) { FuDevice *device_tmp = g_ptr_array_index(devices, i); if (device_tmp != device_best) { device_worst = g_object_ref(device_tmp); break; } } g_assert_nonnull(device_worst); g_assert_false(fu_device_has_flag(device_worst, FWUPD_DEVICE_FLAG_UPDATABLE)); g_assert_true(fu_device_has_problem(device_worst, FWUPD_DEVICE_PROBLEM_LOWER_PRIORITY)); } static void fu_engine_device_md_set_flags_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(XbBuilder) builder = xb_builder_new(); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_autoptr(XbSilo) silo = NULL; const gchar *xml = "\n" "\n" " \n" " org.fwupd.8330a096d9f1af8567c7374cb8403e1ce9cf3163.device\n" " \n" " 2d47f29b-83a2-4f31-a2e8-63474f4d4c2e\n" " \n" " \n" " \n" " \n" " \n" " save-into-backup-remote\n" " \n" " \n" "\n"; /* load engine to get FuConfig set up */ ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* add the XML metadata */ ret = xb_builder_source_load_xml(source, xml, XB_BUILDER_SOURCE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); xb_builder_import_source(builder, source); silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, &error); g_assert_no_error(error); g_assert_nonnull(silo); fu_engine_set_silo(engine, silo); /* add a dummy device */ fu_device_set_id(device, "UEFI-dummy-dev0"); fu_device_set_version(device, "0"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_protocol(device, "com.acme"); fu_device_add_instance_id(device, "2d47f29b-83a2-4f31-a2e8-63474f4d4c2e"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_MD_SET_FLAGS); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_PLAIN); fu_engine_add_device(engine, device); /* check the flag got set */ g_assert_true( fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_SAVE_INTO_BACKUP_REMOTE)); } static void fu_engine_device_md_checksum_set_version_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(XbBuilder) builder = xb_builder_new(); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_autoptr(XbSilo) silo = NULL; const gchar *xml = "\n" "\n" " \n" " org.fwupd.8330a096d9f1af8567c7374cb8403e1ce9cf3163.device\n" " \n" " 2d47f29b-83a2-4f31-a2e8-63474f4d4c2e\n" " \n" " \n" " \n" " https://test.org/foo.cab" " cdb7c90d3ab8833d5324f5d8516d41fa990b9ca721fe643fffaef9057d9f9e48\n" " \n" " \n" " \n" " com.acme\n" " plain" " \n" " \n" "\n"; /* load engine to get FuConfig set up */ ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* add the XML metadata */ ret = xb_builder_source_load_xml(source, xml, XB_BUILDER_SOURCE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); xb_builder_import_source(builder, source); silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, &error); g_assert_no_error(error); g_assert_nonnull(silo); fu_engine_set_silo(engine, silo); /* add a dummy device */ fu_device_set_id(device, "UEFI-dummy-dev0"); fu_device_set_version(device, "123"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_protocol(device, "com.acme"); fu_device_add_instance_id(device, "2d47f29b-83a2-4f31-a2e8-63474f4d4c2e"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_MD_ONLY_CHECKSUM); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_MD_SET_VERSION); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_MD_SET_VERFMT); fu_device_add_checksum(device, "cdb7c90d3ab8833d5324f5d8516d41fa990b9ca721fe643fffaef9057d9f9e48"); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_NUMBER); fu_engine_add_device(engine, device); /* check the version got set */ g_assert_cmpstr(fu_device_get_version(device), ==, "124"); g_assert_cmpint(fu_device_get_version_format(device), ==, FWUPD_VERSION_FORMAT_PLAIN); } static void fu_engine_device_md_checksum_set_version_wrong_proto_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(XbBuilder) builder = xb_builder_new(); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_autoptr(XbSilo) silo = NULL; const gchar *xml = "\n" "\n" " \n" " org.fwupd.8330a096d9f1af8567c7374cb8403e1ce9cf3163.device\n" " \n" " 2d47f29b-83a2-4f31-a2e8-63474f4d4c2e\n" " \n" " \n" " \n" " https://test.org/foo.cab" " cdb7c90d3ab8833d5324f5d8516d41fa990b9ca721fe643fffaef9057d9f9e48\n" " \n" " \n" " \n" " com.acme\n" " plain" " \n" " \n" "\n"; /* load engine to get FuConfig set up */ ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* add the XML metadata */ ret = xb_builder_source_load_xml(source, xml, XB_BUILDER_SOURCE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); xb_builder_import_source(builder, source); silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, &error); g_assert_no_error(error); g_assert_nonnull(silo); fu_engine_set_silo(engine, silo); /* add a dummy device */ fu_device_set_id(device, "UEFI-dummy-dev0"); fu_device_set_version(device, "123"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_protocol(device, "SOMETHING_ELSE_ENTIRELY"); fu_device_add_instance_id(device, "2d47f29b-83a2-4f31-a2e8-63474f4d4c2e"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_MD_ONLY_CHECKSUM); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_MD_SET_VERSION); fu_device_add_checksum(device, "cdb7c90d3ab8833d5324f5d8516d41fa990b9ca721fe643fffaef9057d9f9e48"); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_NUMBER); fu_engine_add_device(engine, device); /* check the version did not get set, because the protocol was different */ g_assert_cmpstr(fu_device_get_version(device), ==, "123"); g_assert_cmpint(fu_device_get_version_format(device), ==, FWUPD_VERSION_FORMAT_NUMBER); } static void fu_engine_require_hwid_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autofree gchar *filename = NULL; g_autoptr(FuCabinet) cabinet = NULL; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo_empty = xb_silo_new(); /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* load engine to get FuConfig set up */ ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* get generated file as a blob */ filename = g_test_build_filename(G_TEST_BUILT, "tests", "missing-hwid", "hwid-1.2.3.cab", NULL); stream = fu_input_stream_from_path(filename, &error); g_assert_no_error(error); g_assert_nonnull(stream); cabinet = fu_engine_build_cabinet_from_stream(engine, stream, &error); g_assert_no_error(error); g_assert_nonnull(cabinet); /* add a dummy device */ fu_device_set_id(device, "test_device"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_protocol(device, "com.acme"); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "1.2.2"); fu_device_add_instance_id(device, "12345678-1234-1234-1234-123456789012"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_engine_add_device(engine, device); /* get component */ component = fu_cabinet_get_component(cabinet, "com.hughski.test.firmware", &error); g_assert_no_error(error); g_assert_nonnull(component); /* check requirements */ fu_release_set_device(release, device); fu_release_set_request(release, request); ret = fu_release_load(release, NULL, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_requirements_check(engine, release, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE); g_assert_nonnull(error); g_assert_cmpstr(error->message, ==, "no HWIDs matched 9342d47a-1bab-5709-9869-c840b2eac501"); g_assert_false(ret); } static void fu_engine_get_details_added_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; FuDevice *device_tmp; FwupdRelease *release; gboolean ret; g_autofree gchar *checksum_sha256 = NULL; g_autofree gchar *filename = NULL; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(XbSilo) silo_empty = xb_silo_new(); /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* load engine to get FuConfig set up */ ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* add a dummy device */ fu_device_set_id(device, "test_device"); fu_device_set_name(device, "test device"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_protocol(device, "com.acme"); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "1.2.2"); fu_device_add_instance_id(device, "12345678-1234-1234-1234-123456789012"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_engine_add_device(engine, device); /* get details */ filename = g_test_build_filename(G_TEST_BUILT, "tests", "missing-hwid", "hwid-1.2.3.cab", NULL); stream = fu_input_stream_from_path(filename, &error); g_assert_no_error(error); g_assert_nonnull(stream); checksum_sha256 = fu_input_stream_compute_checksum(stream, G_CHECKSUM_SHA256, &error); g_assert_no_error(error); g_assert_nonnull(checksum_sha256); devices = fu_engine_get_details(engine, request, stream, &error); g_assert_no_error(error); g_assert_nonnull(devices); g_assert_cmpint(devices->len, ==, 1); device_tmp = g_ptr_array_index(devices, 0); g_assert_cmpstr(fu_device_get_name(device_tmp), ==, "test device"); release = fu_device_get_release_default(device_tmp); g_assert_nonnull(release); g_assert_cmpstr(fwupd_release_get_version(release), ==, "1.2.3"); g_assert_true(fwupd_release_has_checksum(release, checksum_sha256)); } static void fu_engine_get_details_missing_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; FuDevice *device_tmp; FwupdRelease *release; gboolean ret; g_autofree gchar *filename = NULL; g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GInputStream) stream = NULL; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(XbSilo) silo_empty = xb_silo_new(); /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* load engine to get FuConfig set up */ ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* get details */ filename = g_test_build_filename(G_TEST_BUILT, "tests", "missing-hwid", "hwid-1.2.3.cab", NULL); stream = fu_input_stream_from_path(filename, &error); g_assert_no_error(error); g_assert_nonnull(stream); devices = fu_engine_get_details(engine, request, stream, &error); g_assert_no_error(error); g_assert_nonnull(devices); g_assert_cmpint(devices->len, ==, 1); device_tmp = g_ptr_array_index(devices, 0); g_assert_cmpstr(fu_device_get_name(device_tmp), ==, NULL); release = fu_device_get_release_default(device_tmp); g_assert_nonnull(release); g_assert_cmpstr(fwupd_release_get_version(release), ==, "1.2.3"); } static void fu_engine_downgrade_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; FwupdRelease *rel; gboolean ret; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(GPtrArray) devices_pre = NULL; g_autoptr(GPtrArray) releases_dg = NULL; g_autoptr(GPtrArray) releases = NULL; g_autoptr(GPtrArray) releases_up = NULL; g_autoptr(GPtrArray) releases_up2 = NULL; g_autoptr(GPtrArray) remotes = NULL; g_autoptr(XbSilo) silo_empty = xb_silo_new(); /* ensure empty tree */ fu_self_test_mkroot(); /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* write a broken file */ ret = g_file_set_contents("/tmp/fwupd-self-test/broken.xml.gz", "this is not a valid", -1, &error); g_assert_no_error(error); g_assert_true(ret); /* write the main file */ ret = g_file_set_contents( "/tmp/fwupd-self-test/stable.xml", "" " " " test" " Test Device" " " " aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" " " " " " " " 123" " 456" " https://test.org/foo.cab" " deadbeefdeadbeefdeadbeefdead1111" " deadbeefdeadbeefdeadbeefdeadbeef" " " " " " 123" " 456" " https://test.org/foo.cab" " deadbeefdeadbeefdeadbeefdead2222" " deadbeefdeadbeefdeadbeefdeadbeef" " " " " " " "", -1, &error); g_assert_no_error(error); g_assert_true(ret); /* write the extra file */ ret = g_file_set_contents( "/tmp/fwupd-self-test/testing.xml", "" " " " test" " Test Device" " " " aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" " " " " " " " 123" " 456" " https://test.org/foo.cab" " deadbeefdeadbeefdeadbeefdead3333" " deadbeefdeadbeefdeadbeefdeadbeef" " " " " " 123" " 456" " https://test.org/foo.cab" " deadbeefdeadbeefdeadbeefdead4444" " deadbeefdeadbeefdeadbeefdeadbeef" " " " " " " "", -1, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); g_test_assert_expected_messages(); /* return all the remotes, even the broken one */ remotes = fu_engine_get_remotes(engine, &error); g_assert_no_error(error); g_assert_nonnull(remotes); g_assert_cmpint(remotes->len, ==, 7); /* ensure there are no devices already */ devices_pre = fu_engine_get_devices(engine, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO); g_assert_null(devices_pre); g_clear_error(&error); /* add a device so we can get upgrades and downgrades */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "1.2.3"); fu_device_set_id(device, "test_device"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_protocol(device, "com.acme"); fu_device_set_name(device, "Test Device"); fu_device_add_instance_id(device, "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_engine_add_device(engine, device); devices = fu_engine_get_devices(engine, &error); g_assert_no_error(error); g_assert_nonnull(devices); g_assert_cmpint(devices->len, ==, 1); g_assert_true(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_SUPPORTED)); g_assert_true(fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_REGISTERED)); /* get the releases for one device */ releases = fu_engine_get_releases(engine, request, fu_device_get_id(device), &error); g_assert_no_error(error); g_assert_nonnull(releases); g_assert_cmpint(releases->len, ==, 4); /* no upgrades, as no firmware is approved */ releases_up = fu_engine_get_upgrades(engine, request, fu_device_get_id(device), &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO); g_assert_null(releases_up); g_clear_error(&error); /* retry with approved firmware set */ fu_engine_add_approved_firmware(engine, "deadbeefdeadbeefdeadbeefdead1111"); fu_engine_add_approved_firmware(engine, "deadbeefdeadbeefdeadbeefdead2222"); fu_engine_add_approved_firmware(engine, "deadbeefdeadbeefdeadbeefdead3333"); fu_engine_add_approved_firmware(engine, "deadbeefdeadbeefdeadbeefdead4444"); fu_engine_add_approved_firmware(engine, "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"); /* upgrades */ releases_up = fu_engine_get_upgrades(engine, request, fu_device_get_id(device), &error); g_assert_no_error(error); g_assert_nonnull(releases_up); g_assert_cmpint(releases_up->len, ==, 2); /* ensure the list is sorted */ rel = FWUPD_RELEASE(g_ptr_array_index(releases_up, 0)); g_assert_cmpstr(fwupd_release_get_version(rel), ==, "1.2.5"); rel = FWUPD_RELEASE(g_ptr_array_index(releases_up, 1)); g_assert_cmpstr(fwupd_release_get_version(rel), ==, "1.2.4"); /* downgrades */ releases_dg = fu_engine_get_downgrades(engine, request, fu_device_get_id(device), &error); g_assert_no_error(error); g_assert_nonnull(releases_dg); g_assert_cmpint(releases_dg->len, ==, 1); rel = FWUPD_RELEASE(g_ptr_array_index(releases_dg, 0)); g_assert_cmpstr(fwupd_release_get_version(rel), ==, "1.2.2"); /* enforce that updates have to be explicit */ fu_device_add_flag(device, FWUPD_DEVICE_FLAG_ONLY_EXPLICIT_UPDATES); releases_up2 = fu_engine_get_upgrades(engine, request, fu_device_get_id(device), &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO); g_assert_null(releases_up2); } static void fu_engine_md_verfmt_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; FwupdRemote *remote; gboolean ret; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(GPtrArray) releases = NULL; g_autoptr(XbSilo) silo_empty = xb_silo_new(); /* ensure empty tree */ fu_self_test_mkroot(); /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* write the main file */ ret = g_file_set_contents( "/tmp/fwupd-self-test/stable.xml", "" " " " test" " Test Device" " computer" " ACME" " " " aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" " " " " " X-GraphicsTablet" " " " " " " " 123" " 456" " https://test.org/foo.cab" " deadbeefdeadbeefdeadbeefdeadbeef" " deadbeefdeadbeefdeadbeefdeadbeef" " " " " " " " triplet" " signed" " host-cpu,needs-shutdown" " " " " "", -1, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); g_test_assert_expected_messages(); /* pretend this has a signature */ remote = fu_engine_get_remote_by_id(engine, "stable", &error); g_assert_no_error(error); g_assert_nonnull(remote); /* add a device with no defined version format */ fu_device_set_version(device, "16908291"); fu_device_set_version_raw(device, 0x01020003); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_MD_SET_NAME_CATEGORY); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_MD_SET_ICON); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_MD_SET_VENDOR); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_MD_SET_SIGNED); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_MD_SET_VERFMT); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_MD_SET_FLAGS); fu_device_set_id(device, "test_device"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_protocol(device, "com.acme"); fu_device_add_instance_id(device, "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_engine_add_device(engine, device); /* ensure the version format was set from the metadata */ g_assert_cmpint(fu_device_get_version_format(device), ==, FWUPD_VERSION_FORMAT_TRIPLET); g_assert_cmpstr(fu_device_get_version(device), ==, "1.2.3"); g_assert_cmpstr(fu_device_get_name(device), ==, "Graphics Tablet"); g_assert_cmpstr(fu_device_get_vendor(device), ==, "ACME"); g_assert_true(fu_device_has_icon(device, "computer")); g_assert_true(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD)); g_assert_true(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN)); g_assert_true(fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_HOST_CPU)); /* ensure the device was added */ devices = fu_engine_get_devices(engine, &error); g_assert_no_error(error); g_assert_nonnull(devices); g_assert_cmpint(devices->len, ==, 1); g_assert_true(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_SUPPORTED)); g_assert_true(fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_REGISTERED)); /* ensure the releases are set */ releases = fu_engine_get_releases(engine, request, fu_device_get_id(device), &error); g_assert_no_error(error); g_assert_nonnull(releases); g_assert_cmpint(releases->len, ==, 1); } static void fu_engine_install_duration_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; FwupdRelease *rel; gboolean ret; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(GPtrArray) releases = NULL; g_autoptr(XbSilo) silo_empty = xb_silo_new(); /* ensure empty tree */ fu_self_test_mkroot(); /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* write the main file */ ret = g_file_set_contents( "/tmp/fwupd-self-test/stable.xml", "" " " " test" " " " aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" " " " " " " " https://test.org/foo.cab" " deadbeefdeadbeefdeadbeefdeadbeef" " deadbeefdeadbeefdeadbeefdeadbeef" " " " " " " "", -1, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* add a device so we can get the install duration */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "1.2.3"); fu_device_set_id(device, "test_device"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_protocol(device, "com.acme"); fu_device_add_instance_id(device, "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"); fu_device_set_install_duration(device, 999); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_engine_add_device(engine, device); devices = fu_engine_get_devices(engine, &error); g_assert_no_error(error); g_assert_nonnull(devices); g_assert_cmpint(devices->len, ==, 1); g_assert_true(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_SUPPORTED)); /* check the release install duration */ releases = fu_engine_get_releases(engine, request, fu_device_get_id(device), &error); g_assert_no_error(error); g_assert_nonnull(releases); g_assert_cmpint(releases->len, ==, 1); rel = FWUPD_RELEASE(g_ptr_array_index(releases, 0)); g_assert_cmpint(fwupd_release_get_install_duration(rel), ==, 120); } static void fu_engine_release_dedupe_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(GPtrArray) releases = NULL; g_autoptr(XbSilo) silo_empty = xb_silo_new(); /* ensure empty tree */ fu_self_test_mkroot(); /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* write the main file */ ret = g_file_set_contents( "/tmp/fwupd-self-test/stable.xml", "" " " " test" " " " aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" " " " " " " " https://test.org/foo.cab" " deadbeefdeadbeefdeadbeefdeadbeef" " deadbeefdeadbeefdeadbeefdeadbeef" " " " " " https://test.org/foo.cab" " deadbeefdeadbeefdeadbeefdeadbeef" " deadbeefdeadbeefdeadbeefdeadbeef" " " " " " " "", -1, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* add a device so we can get the install duration */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "1.2.3"); fu_device_set_id(device, "test_device"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_protocol(device, "com.acme"); fu_device_add_instance_id(device, "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"); fu_device_set_install_duration(device, 999); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_engine_add_device(engine, device); devices = fu_engine_get_devices(engine, &error); g_assert_no_error(error); g_assert_nonnull(devices); g_assert_cmpint(devices->len, ==, 1); g_assert_true(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_SUPPORTED)); /* check the release install duration */ releases = fu_engine_get_releases(engine, request, fu_device_get_id(device), &error); g_assert_no_error(error); g_assert_nonnull(releases); g_assert_cmpint(releases->len, ==, 1); } static void fu_engine_history_modify_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuHistory) history = fu_history_new(self->ctx); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error = NULL; /* add a new entry */ fu_device_set_id(device, "foobarbaz"); fu_history_remove_device(history, device, NULL); ret = fu_history_add_device(history, device, release, &error); g_assert_no_error(error); g_assert_true(ret); /* try to modify something that does exist */ ret = fu_history_modify_device(history, device, &error); g_assert_no_error(error); g_assert_true(ret); /* does not exist */ fu_device_set_id(device, "DOES-NOT-EXIST"); ret = fu_history_modify_device(history, device, &error); g_assert_false(ret); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND); } static void fu_engine_history_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autofree gchar *checksum = NULL; g_autofree gchar *device_str_expected = NULL; g_autofree gchar *device_str = NULL; g_autofree gchar *filename = NULL; g_autoptr(FuCabinet) cabinet = NULL; g_autoptr(FuDevice) device2 = NULL; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuHistory) history = NULL; g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(FuPlugin) plugin = fu_plugin_new_from_gtype(fu_test_plugin_get_type(), self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FwupdDevice) device3 = NULL; g_autoptr(FwupdDevice) device4 = NULL; g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo_empty = xb_silo_new(); /* ensure empty tree */ fu_self_test_mkroot(); /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* set up dummy plugin */ ret = fu_plugin_reset_config_values(plugin, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_plugin_set_config_value(plugin, "AnotherWriteRequired", "true", &error); g_assert_no_error(error); g_assert_true(ret); fu_engine_add_plugin(engine, plugin); ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* add a device so we can get upgrade it */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "1.2.2"); fu_device_set_id(device, "test_device"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_protocol(device, "com.acme"); fu_device_set_name(device, "Test Device"); fu_device_set_plugin(device, "test"); fu_device_add_instance_id(device, "12345678-1234-1234-1234-123456789012"); fu_device_add_checksum(device, "0123456789abcdef0123456789abcdef01234567"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_set_created_usec(device, 1515338000ull * G_USEC_PER_SEC); fu_engine_add_device(engine, device); devices = fu_engine_get_devices(engine, &error); g_assert_no_error(error); g_assert_nonnull(devices); g_assert_cmpint(devices->len, ==, 1); g_assert_true(fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_REGISTERED)); filename = g_test_build_filename(G_TEST_BUILT, "tests", "missing-hwid", "noreqs-1.2.3.cab", NULL); stream = fu_input_stream_from_path(filename, &error); g_assert_no_error(error); g_assert_nonnull(stream); cabinet = fu_engine_build_cabinet_from_stream(engine, stream, &error); g_assert_no_error(error); g_assert_nonnull(cabinet); /* get component */ component = fu_cabinet_get_component(cabinet, "com.hughski.test.firmware", &error); g_assert_no_error(error); g_assert_nonnull(component); /* set the counter */ fu_device_set_metadata_integer(device, "nr-update", 0); /* install it */ fu_release_set_device(release, device); ret = fu_release_load(release, cabinet, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_install_release(engine, release, stream, progress, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); /* check the write was done more than once */ g_assert_cmpint(fu_device_get_metadata_integer(device, "nr-update"), ==, 2); /* check the history database */ history = fu_history_new(self->ctx); device2 = fu_history_get_device_by_id(history, fu_device_get_id(device), &error); g_assert_no_error(error); g_assert_nonnull(device2); g_assert_cmpint(fu_device_get_update_state(device2), ==, FWUPD_UPDATE_STATE_SUCCESS); g_assert_cmpstr(fu_device_get_update_error(device2), ==, NULL); fu_device_set_modified_usec(device2, 1514338000ull * G_USEC_PER_SEC); g_hash_table_remove_all(fwupd_release_get_metadata(fu_device_get_release_default(device2))); device_str = fu_device_to_string(device2); checksum = fu_input_stream_compute_checksum(stream, G_CHECKSUM_SHA1, &error); g_assert_no_error(error); g_assert_nonnull(checksum); device_str_expected = g_strdup_printf("FuDevice:\n" " DeviceId: 894e8c17a29428b09d10cd90d1db74ea76fbcfe8\n" " Name: Test Device\n" " Guid: 12345678-1234-1234-1234-123456789012\n" " Plugin: test\n" " Flags: updatable|historical|unsigned-payload\n" " Version: 1.2.2\n" " VersionFormat: triplet\n" " Created: 2018-01-07\n" " Modified: 2017-12-27\n" " UpdateState: success\n" " FuRelease:\n" " AppstreamId: com.hughski.test.firmware\n" " Version: 1.2.3\n" " Checksum: SHA1(%s)\n" " Flags: trusted-payload|trusted-metadata\n" " InstanceId[vi]: 12345678-1234-1234-1234-123456789012\n" " AcquiesceDelay: 50\n", checksum); ret = g_strcmp0(device_str, device_str_expected) == 0; g_assert_true(ret); /* GetResults() */ device3 = fu_engine_get_results(engine, FWUPD_DEVICE_ID_ANY, &error); g_assert_no_error(error); g_assert_nonnull(device3); g_assert_cmpstr(fu_device_get_id(device3), ==, "894e8c17a29428b09d10cd90d1db74ea76fbcfe8"); g_assert_cmpint(fu_device_get_update_state(device3), ==, FWUPD_UPDATE_STATE_SUCCESS); g_assert_cmpstr(fu_device_get_update_error(device3), ==, NULL); /* ClearResults() */ ret = fu_engine_clear_results(engine, FWUPD_DEVICE_ID_ANY, &error); g_assert_no_error(error); g_assert_true(ret); /* GetResults() */ device4 = fu_engine_get_results(engine, FWUPD_DEVICE_ID_ANY, &error); g_assert_null(device4); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO); } static void fu_engine_history_verfmt_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device = g_object_new(FU_TYPE_DPAUX_DEVICE, "context", self->ctx, NULL); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuPlugin) plugin = fu_plugin_new_from_gtype(fu_test_plugin_get_type(), self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(XbSilo) silo_empty = xb_silo_new(); /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* set up dummy plugin */ fu_engine_add_plugin(engine, plugin); ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* absorb version format from the database */ fu_device_set_version_raw(device, 65563); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_NUMBER); fu_device_set_id(device, "test_device"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_protocol(device, "com.acme"); fu_device_set_plugin(device, "test"); fu_device_add_instance_id(device, "12345678-1234-1234-1234-123456789012"); fu_device_add_checksum(device, "0123456789abcdef0123456789abcdef01234567"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_MD_SET_VERFMT); fu_device_set_created_usec(device, 1515338000ull * G_USEC_PER_SEC); fu_engine_add_device(engine, device); g_assert_cmpint(fu_device_get_version_format(device), ==, FWUPD_VERSION_FORMAT_TRIPLET); g_assert_cmpstr(fu_device_get_version(device), ==, "0.1.27"); } static void fu_engine_install_loop_restart_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuPlugin) plugin = fu_plugin_new_from_gtype(fu_test_plugin_get_type(), self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream_fw = NULL; g_autoptr(XbSilo) silo_empty = xb_silo_new(); /* ensure empty tree */ fu_self_test_mkroot(); /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* set up dummy plugin */ ret = fu_plugin_reset_config_values(plugin, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_plugin_set_config_value(plugin, "InstallLoopRestart", "true", &error); g_assert_no_error(error); g_assert_true(ret); fu_engine_add_plugin(engine, plugin); ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* add a device so we can install it */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "1.2.2"); fu_device_set_id(device, "test_device"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_protocol(device, "com.acme"); fu_device_set_plugin(device, "test"); fu_device_add_instance_id(device, "12345678-1234-1234-1234-123456789012"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_engine_add_device(engine, device); /* set up counters */ fu_device_set_metadata_integer(device, "nr-update", 0); fu_device_set_metadata_integer(device, "nr-attach", 0); stream_fw = g_memory_input_stream_new_from_data((const guint8 *)"1.2.3", 5, NULL); ret = fu_engine_install_blob(engine, device, stream_fw, progress, FWUPD_INSTALL_FLAG_NO_HISTORY | FU_FIRMWARE_PARSE_FLAG_CACHE_STREAM, FWUPD_FEATURE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); /* check we did two write loops */ g_assert_cmpint(fu_device_get_metadata_integer(device, "nr-update"), ==, 2); /* check we only attached once */ g_assert_cmpint(fu_device_get_metadata_integer(device, "nr-attach"), ==, 1); } static void fu_engine_multiple_rels_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autofree gchar *filename = NULL; g_autoptr(FuCabinet) cabinet = NULL; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuPlugin) plugin = fu_plugin_new_from_gtype(fu_test_plugin_get_type(), self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo_empty = xb_silo_new(); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(GPtrArray) releases = NULL; g_autoptr(GPtrArray) rels = NULL; g_autoptr(XbQuery) query = NULL; #ifndef HAVE_LIBARCHIVE g_test_skip("no libarchive support"); return; #endif /* ensure empty tree */ fu_self_test_mkroot(); /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* set up dummy plugin */ ret = fu_plugin_reset_config_values(plugin, &error); g_assert_no_error(error); g_assert_true(ret); fu_engine_add_plugin(engine, plugin); ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* add a device so we can get upgrade it */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "1.2.2"); fu_device_set_id(device, "test_device"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_protocol(device, "com.acme"); fu_device_set_name(device, "Test Device"); fu_device_set_plugin(device, "test"); fu_device_add_instance_id(device, "12345678-1234-1234-1234-123456789012"); fu_device_add_checksum(device, "0123456789abcdef0123456789abcdef01234567"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_INSTALL_ALL_RELEASES); fu_device_set_created_usec(device, 1515338000ull * G_USEC_PER_SEC); fu_engine_add_device(engine, device); filename = g_test_build_filename(G_TEST_BUILT, "tests", "multiple-rels", "multiple-rels-1.2.4.cab", NULL); stream = fu_input_stream_from_path(filename, &error); g_assert_no_error(error); g_assert_nonnull(stream); cabinet = fu_engine_build_cabinet_from_stream(engine, stream, &error); g_assert_no_error(error); g_assert_nonnull(cabinet); /* get component */ component = fu_cabinet_get_component(cabinet, "com.hughski.test.firmware", &error); g_assert_no_error(error); g_assert_nonnull(component); /* set up counters */ fu_device_set_metadata_integer(device, "nr-update", 0); fu_device_set_metadata_integer(device, "nr-attach", 0); /* get all */ query = xb_query_new_full(xb_node_get_silo(component), "releases/release", XB_QUERY_FLAG_FORCE_NODE_CACHE, &error); g_assert_no_error(error); g_assert_nonnull(query); rels = xb_node_query_full(component, query, &error); g_assert_no_error(error); g_assert_nonnull(rels); releases = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); for (guint i = 0; i < rels->len; i++) { XbNode *rel = g_ptr_array_index(rels, i); g_autoptr(FuRelease) release = fu_release_new(); fu_release_set_device(release, device); ret = fu_release_load(release, cabinet, component, rel, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); g_ptr_array_add(releases, g_object_ref(release)); } /* install them */ fu_progress_reset(progress); ret = fu_engine_install_releases(engine, request, releases, cabinet, progress, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); /* check we did 1.2.2 -> 1.2.3 -> 1.2.4 */ g_assert_cmpint(fu_device_get_metadata_integer(device, "nr-update"), ==, 2); g_assert_cmpint(fu_device_get_metadata_integer(device, "nr-attach"), ==, 2); g_assert_cmpstr(fu_device_get_version(device), ==, "1.2.4"); /* reset the config back to defaults */ ret = fu_engine_reset_config(engine, "fwupd", &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_engine_history_inherit(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autofree gchar *filename = NULL; g_autofree gchar *localstatedir = NULL; g_autofree gchar *history_db = NULL; g_autoptr(FuCabinet) cabinet = NULL; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(FuPlugin) plugin = fu_plugin_new_from_gtype(fu_test_plugin_get_type(), self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo_empty = xb_silo_new(); /* delete history */ localstatedir = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR_PKG); history_db = g_build_filename(localstatedir, "pending.db", NULL); (void)g_unlink(history_db); /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* set up dummy plugin */ ret = fu_plugin_reset_config_values(plugin, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_plugin_set_config_value(plugin, "NeedsActivation", "true", &error); g_assert_no_error(error); g_assert_true(ret); fu_engine_add_plugin(engine, plugin); ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* add a device so we can get upgrade it */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "1.2.2"); fu_device_set_id(device, "test_device"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_protocol(device, "com.acme"); fu_device_set_name(device, "Test Device"); fu_device_set_plugin(device, "test"); fu_device_add_instance_id(device, "12345678-1234-1234-1234-123456789012"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_set_created_usec(device, 1515338000ull * G_USEC_PER_SEC); fu_engine_add_device(engine, device); devices = fu_engine_get_devices(engine, &error); g_assert_no_error(error); g_assert_nonnull(devices); g_assert_cmpint(devices->len, ==, 1); g_assert_true(fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_REGISTERED)); filename = g_test_build_filename(G_TEST_BUILT, "tests", "missing-hwid", "noreqs-1.2.3.cab", NULL); stream = fu_input_stream_from_path(filename, &error); g_assert_no_error(error); g_assert_nonnull(stream); cabinet = fu_engine_build_cabinet_from_stream(engine, stream, &error); g_assert_no_error(error); g_assert_nonnull(cabinet); /* get component */ component = fu_cabinet_get_component(cabinet, "com.hughski.test.firmware", &error); g_assert_no_error(error); g_assert_nonnull(component); /* install it */ fu_release_set_device(release, device); ret = fu_release_load(release, cabinet, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_install_release(engine, release, stream, progress, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); /* check the device requires an activation */ g_assert_true(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION)); g_assert_cmpstr(fu_device_get_version(device), ==, "1.2.2"); /* activate the device */ fu_progress_reset(progress); ret = fu_engine_activate(engine, fu_device_get_id(device), progress, &error); g_assert_no_error(error); g_assert_true(ret); /* check the device no longer requires an activation */ g_assert_false(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION)); g_assert_cmpstr(fu_device_get_version(device), ==, "1.2.3"); /* emulate getting the flag for a fresh boot on old firmware */ fu_progress_reset(progress); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "1.2.2"); ret = fu_engine_install_release(engine, release, stream, progress, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); g_object_unref(engine); g_object_unref(device); engine = fu_engine_new(self->ctx); fu_engine_set_silo(engine, silo_empty); fu_engine_add_plugin(engine, plugin); device = fu_device_new(self->ctx); fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_INHERIT_ACTIVATION); fu_device_set_id(device, "test_device"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_protocol(device, "com.acme"); fu_device_set_name(device, "Test Device"); fu_device_add_instance_id(device, "12345678-1234-1234-1234-123456789012"); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "1.2.2"); fu_engine_add_device(engine, device); g_assert_true(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION)); /* emulate not getting the flag */ g_object_unref(engine); g_object_unref(device); engine = fu_engine_new(self->ctx); fu_engine_set_silo(engine, silo_empty); fu_engine_add_plugin(engine, plugin); device = fu_device_new(self->ctx); fu_device_set_id(device, "test_device"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_protocol(device, "com.acme"); fu_device_set_name(device, "Test Device"); fu_device_add_instance_id(device, "12345678-1234-1234-1234-123456789012"); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "1.2.2"); fu_engine_add_device(engine, device); g_assert_false(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION)); } static void fu_engine_install_needs_reboot(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autofree gchar *filename = NULL; g_autoptr(FuCabinet) cabinet = NULL; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(FuPlugin) plugin = fu_plugin_new_from_gtype(fu_test_plugin_get_type(), self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo_empty = xb_silo_new(); /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* set up dummy plugin */ ret = fu_plugin_reset_config_values(plugin, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_plugin_set_config_value(plugin, "NeedsReboot", "true", &error); g_assert_no_error(error); g_assert_true(ret); fu_engine_add_plugin(engine, plugin); ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* add a device so we can get upgrade it */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "1.2.2"); fu_device_set_id(device, "test_device"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_protocol(device, "com.acme"); fu_device_set_name(device, "Test Device"); fu_device_set_plugin(device, "test"); fu_device_add_instance_id(device, "12345678-1234-1234-1234-123456789012"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_set_created_usec(device, 1515338000ull * G_USEC_PER_SEC); fu_engine_add_device(engine, device); devices = fu_engine_get_devices(engine, &error); g_assert_no_error(error); g_assert_nonnull(devices); g_assert_cmpint(devices->len, ==, 1); g_assert_true(fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_REGISTERED)); filename = g_test_build_filename(G_TEST_BUILT, "tests", "missing-hwid", "noreqs-1.2.3.cab", NULL); stream = fu_input_stream_from_path(filename, &error); g_assert_no_error(error); g_assert_nonnull(stream); cabinet = fu_engine_build_cabinet_from_stream(engine, stream, &error); g_assert_no_error(error); g_assert_nonnull(cabinet); /* get component */ component = fu_cabinet_get_component(cabinet, "com.hughski.test.firmware", &error); g_assert_no_error(error); g_assert_nonnull(component); /* install it */ fu_release_set_device(release, device); ret = fu_release_load(release, cabinet, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_install_release(engine, release, stream, progress, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); /* check the device requires reboot */ g_assert_true(fu_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT)); g_assert_cmpint(fu_device_get_update_state(device), ==, FWUPD_UPDATE_STATE_NEEDS_REBOOT); g_assert_cmpstr(fu_device_get_version(device), ==, "1.2.2"); } typedef struct { guint request_cnt; FwupdStatus last_status; } FuTestRequestHelper; static void fu_test_engine_status_changed_cb(FuProgress *progress, FwupdStatus status, gpointer user_data) { FuTestRequestHelper *helper = (FuTestRequestHelper *)user_data; g_debug("status now %s", fwupd_status_to_string(status)); helper->last_status = status; } static void fu_test_engine_request_cb(FuEngine *engine, FwupdRequest *request, gpointer user_data) { FuTestRequestHelper *helper = (FuTestRequestHelper *)user_data; g_assert_cmpint(fwupd_request_get_kind(request), ==, FWUPD_REQUEST_KIND_IMMEDIATE); g_assert_cmpstr(fwupd_request_get_id(request), ==, FWUPD_REQUEST_ID_REMOVE_REPLUG); g_assert_true(fwupd_request_has_flag(request, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE)); g_assert_nonnull(fwupd_request_get_message(request)); g_assert_cmpint(helper->last_status, ==, FWUPD_STATUS_WAITING_FOR_USER); helper->request_cnt++; } static void fu_engine_install_request(gconstpointer user_data) { FuTestRequestHelper helper = {.request_cnt = 0, .last_status = FWUPD_STATUS_UNKNOWN}; FuTest *self = (FuTest *)user_data; gboolean ret; g_autofree gchar *filename = NULL; g_autoptr(FuCabinet) cabinet = NULL; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(FuPlugin) plugin = fu_plugin_new_from_gtype(fu_test_plugin_get_type(), self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo_empty = xb_silo_new(); /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* set up dummy plugin */ ret = fu_plugin_reset_config_values(plugin, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_plugin_set_config_value(plugin, "RequestSupported", "true", &error); g_assert_no_error(error); g_assert_true(ret); fu_engine_add_plugin(engine, plugin); ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* add a device so we can get upgrade it */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "1.2.2"); fu_device_set_id(device, "test_device"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_protocol(device, "com.acme"); fu_device_set_name(device, "Test Device"); fu_device_set_plugin(device, "test"); fu_device_add_instance_id(device, "12345678-1234-1234-1234-123456789012"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_request_flag(device, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE); fu_device_set_created_usec(device, 1515338000ull * G_USEC_PER_SEC); fu_engine_add_device(engine, device); devices = fu_engine_get_devices(engine, &error); g_assert_no_error(error); g_assert_nonnull(devices); g_assert_cmpint(devices->len, ==, 1); g_assert_true(fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_REGISTERED)); filename = g_test_build_filename(G_TEST_BUILT, "tests", "missing-hwid", "noreqs-1.2.3.cab", NULL); stream = fu_input_stream_from_path(filename, &error); g_assert_no_error(error); g_assert_nonnull(stream); cabinet = fu_engine_build_cabinet_from_stream(engine, stream, &error); g_assert_no_error(error); g_assert_nonnull(cabinet); /* get component */ component = fu_cabinet_get_component(cabinet, "com.hughski.test.firmware", &error); g_assert_no_error(error); g_assert_nonnull(component); /* install it */ fu_release_set_device(release, device); ret = fu_release_load(release, cabinet, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); g_signal_connect(FU_ENGINE(engine), "device-request", G_CALLBACK(fu_test_engine_request_cb), &helper); g_signal_connect(FU_PROGRESS(progress), "status-changed", G_CALLBACK(fu_test_engine_status_changed_cb), &helper); ret = fu_engine_install_release(engine, release, stream, progress, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(helper.request_cnt, ==, 1); g_assert_cmpint(helper.last_status, ==, FWUPD_STATUS_DEVICE_BUSY); } static void fu_engine_history_error_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autofree gchar *checksum = NULL; g_autofree gchar *device_str_expected = NULL; g_autofree gchar *device_str = NULL; g_autofree gchar *filename = NULL; g_autoptr(FuCabinet) cabinet = NULL; g_autoptr(FuDevice) device2 = NULL; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuHistory) history = NULL; g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(FuPlugin) plugin = fu_plugin_new_from_gtype(fu_test_plugin_get_type(), self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error2 = NULL; g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbSilo) silo_empty = xb_silo_new(); /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* set up dummy plugin */ ret = fu_plugin_reset_config_values(plugin, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_plugin_set_config_value(plugin, "WriteSupported", "false", &error); g_assert_no_error(error); g_assert_true(ret); fu_engine_add_plugin(engine, plugin); ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* add a device so we can get upgrade it */ fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "1.2.2"); fu_device_set_id(device, "test_device"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_protocol(device, "com.acme"); fu_device_set_name(device, "Test Device"); fu_device_set_plugin(device, "test"); fu_device_add_instance_id(device, "12345678-1234-1234-1234-123456789012"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_set_created_usec(device, 1515338000ull * G_USEC_PER_SEC); fu_engine_add_device(engine, device); devices = fu_engine_get_devices(engine, &error); g_assert_no_error(error); g_assert_nonnull(devices); g_assert_cmpint(devices->len, ==, 1); g_assert_true(fu_device_has_private_flag(device, FU_DEVICE_PRIVATE_FLAG_REGISTERED)); /* install the wrong thing */ filename = g_test_build_filename(G_TEST_BUILT, "tests", "missing-hwid", "noreqs-1.2.3.cab", NULL); stream = fu_input_stream_from_path(filename, &error); g_assert_no_error(error); g_assert_nonnull(stream); cabinet = fu_engine_build_cabinet_from_stream(engine, stream, &error); g_assert_no_error(error); g_assert_nonnull(cabinet); component = fu_cabinet_get_component(cabinet, "com.hughski.test.firmware", &error); g_assert_no_error(error); g_assert_nonnull(component); fu_release_set_device(release, device); ret = fu_release_load(release, cabinet, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_engine_install_release(engine, release, stream, progress, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_cmpstr(error->message, ==, "failed to write-firmware: device was not in supported mode"); g_assert_false(ret); /* check the history database */ history = fu_history_new(self->ctx); device2 = fu_history_get_device_by_id(history, fu_device_get_id(device), &error2); g_assert_no_error(error2); g_assert_nonnull(device2); g_assert_cmpint(fu_device_get_update_state(device2), ==, FWUPD_UPDATE_STATE_FAILED); g_assert_cmpstr(fu_device_get_update_error(device2), ==, error->message); g_clear_error(&error); fu_device_set_modified_usec(device2, 1514338000ull * G_USEC_PER_SEC); g_hash_table_remove_all(fwupd_release_get_metadata(fu_device_get_release_default(device2))); device_str = fu_device_to_string(device2); checksum = fu_input_stream_compute_checksum(stream, G_CHECKSUM_SHA1, &error); g_assert_no_error(error); g_assert_nonnull(checksum); device_str_expected = g_strdup_printf( "FuDevice:\n" " DeviceId: 894e8c17a29428b09d10cd90d1db74ea76fbcfe8\n" " Name: Test Device\n" " Guid: 12345678-1234-1234-1234-123456789012\n" " Plugin: test\n" " Flags: updatable|historical|unsigned-payload\n" " Version: 1.2.2\n" " VersionFormat: triplet\n" " Created: 2018-01-07\n" " Modified: 2017-12-27\n" " UpdateState: failed\n" " UpdateError: failed to write-firmware: device was not in supported mode\n" " FuRelease:\n" " AppstreamId: com.hughski.test.firmware\n" " Version: 1.2.3\n" " Checksum: SHA1(%s)\n" " Flags: trusted-payload|trusted-metadata\n" " InstanceId[vi]: 12345678-1234-1234-1234-123456789012\n" " AcquiesceDelay: 50\n", checksum); ret = g_strcmp0(device_str, device_str_expected) == 0; g_assert_true(ret); } static void fu_test_device_list_count_cb(FuDeviceList *device_list, FuDevice *device, gpointer user_data) { guint *cnt = (guint *)user_data; (*cnt)++; } static void fu_device_list_no_auto_remove_children_func(gconstpointer user_data) { g_autoptr(FuDevice) child = fu_device_new(NULL); g_autoptr(FuDevice) parent = fu_device_new(NULL); g_autoptr(FuDeviceList) device_list = fu_device_list_new(); g_autoptr(GPtrArray) active1 = NULL; g_autoptr(GPtrArray) active2 = NULL; g_autoptr(GPtrArray) active3 = NULL; /* normal behavior, remove child with parent */ fu_device_set_id(parent, "parent"); fu_device_set_id(child, "child"); fu_device_add_child(parent, child); fu_device_list_add(device_list, parent); fu_device_list_add(device_list, child); fu_device_list_remove(device_list, parent); active1 = fu_device_list_get_active(device_list); g_assert_cmpint(active1->len, ==, 0); /* new-style behavior, do not remove child */ fu_device_add_private_flag(parent, FU_DEVICE_PRIVATE_FLAG_NO_AUTO_REMOVE_CHILDREN); fu_device_list_add(device_list, parent); fu_device_list_add(device_list, child); fu_device_list_remove(device_list, parent); active2 = fu_device_list_get_active(device_list); g_assert_cmpint(active2->len, ==, 1); fu_device_list_remove(device_list, child); active3 = fu_device_list_get_active(device_list); g_assert_cmpint(active3->len, ==, 0); } static void fu_device_list_delay_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; g_autoptr(FuDevice) device1 = fu_device_new(self->ctx); g_autoptr(FuDevice) device2 = fu_device_new(self->ctx); g_autoptr(FuDeviceList) device_list = fu_device_list_new(); guint added_cnt = 0; guint changed_cnt = 0; guint removed_cnt = 0; g_signal_connect(FU_DEVICE_LIST(device_list), "added", G_CALLBACK(fu_test_device_list_count_cb), &added_cnt); g_signal_connect(FU_DEVICE_LIST(device_list), "removed", G_CALLBACK(fu_test_device_list_count_cb), &removed_cnt); g_signal_connect(FU_DEVICE_LIST(device_list), "changed", G_CALLBACK(fu_test_device_list_count_cb), &changed_cnt); /* add one device */ fu_device_set_id(device1, "device1"); fu_device_add_instance_id(device1, "foobar"); fu_device_add_private_flag(device1, FU_DEVICE_PRIVATE_FLAG_DELAYED_REMOVAL); fu_device_set_remove_delay(device1, 100); fu_device_list_add(device_list, device1); g_assert_cmpint(added_cnt, ==, 1); g_assert_cmpint(removed_cnt, ==, 0); g_assert_cmpint(changed_cnt, ==, 0); /* add the same device again */ fu_device_list_add(device_list, device1); g_assert_cmpint(added_cnt, ==, 1); g_assert_cmpint(removed_cnt, ==, 0); g_assert_cmpint(changed_cnt, ==, 1); /* add a device with the same ID */ fu_device_set_id(device2, "device1"); fu_device_add_private_flag(device2, FU_DEVICE_PRIVATE_FLAG_DELAYED_REMOVAL); fu_device_list_add(device_list, device2); fu_device_set_remove_delay(device2, 100); g_assert_cmpint(added_cnt, ==, 1); g_assert_cmpint(removed_cnt, ==, 0); g_assert_cmpint(changed_cnt, ==, 2); /* spin a bit */ fu_test_loop_run_with_timeout(10); fu_test_loop_quit(); /* verify only a changed event was generated */ added_cnt = removed_cnt = changed_cnt = 0; fu_device_list_remove(device_list, device1); fu_device_list_add(device_list, device1); g_assert_cmpint(added_cnt, ==, 0); g_assert_cmpint(removed_cnt, ==, 0); g_assert_cmpint(changed_cnt, ==, 1); } typedef struct { FuDevice *device_new; FuDevice *device_old; FuDeviceList *device_list; } FuDeviceListReplugHelper; static gboolean fu_device_list_remove_cb(gpointer user_data) { FuDeviceListReplugHelper *helper = (FuDeviceListReplugHelper *)user_data; fu_device_list_remove(helper->device_list, helper->device_old); return FALSE; } static gboolean fu_device_list_add_cb(gpointer user_data) { FuDeviceListReplugHelper *helper = (FuDeviceListReplugHelper *)user_data; fu_device_list_add(helper->device_list, helper->device_new); return FALSE; } static void fu_device_list_replug_auto_func(gconstpointer user_data) { gboolean ret; g_autoptr(FuDevice) device1 = fu_device_new(NULL); g_autoptr(FuDevice) device2 = fu_device_new(NULL); g_autoptr(FuDevice) parent = fu_device_new(NULL); g_autoptr(FuDeviceList) device_list = fu_device_list_new(); g_autoptr(GError) error = NULL; FuDeviceListReplugHelper helper; /* parent */ fu_device_set_id(parent, "parent"); /* fake child devices */ fu_device_set_id(device1, "device1"); fu_device_add_private_flag(device1, FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_set_physical_id(device1, "ID"); fu_device_set_plugin(device1, "self-test"); fu_device_set_remove_delay(device1, FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_device_add_child(parent, device1); fu_device_set_id(device2, "device2"); fu_device_add_private_flag(device2, FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_set_physical_id(device2, "ID"); /* matches */ fu_device_set_plugin(device2, "self-test"); fu_device_set_remove_delay(device2, FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); /* not yet added */ ret = fu_device_list_wait_for_replug(device_list, &error); g_assert_no_error(error); g_assert_true(ret); /* add device */ fu_device_list_add(device_list, device1); /* not waiting */ ret = fu_device_list_wait_for_replug(device_list, &error); g_assert_no_error(error); g_assert_true(ret); /* waiting */ helper.device_old = device1; helper.device_new = device2; helper.device_list = device_list; g_timeout_add(100, fu_device_list_remove_cb, &helper); g_timeout_add(200, fu_device_list_add_cb, &helper); fu_device_add_flag(device1, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); ret = fu_device_list_wait_for_replug(device_list, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_false(fu_device_has_flag(device1, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG)); /* check device2 now has parent too */ g_assert_true(fu_device_get_parent(device2) == parent); /* waiting, failed */ fu_device_add_flag(device2, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); ret = fu_device_list_wait_for_replug(device_list, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND); g_assert_false(ret); g_assert_false(fu_device_has_flag(device1, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG)); } static void fu_device_list_replug_user_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device1 = fu_device_new(self->ctx); g_autoptr(FuDevice) device2 = fu_device_new(self->ctx); g_autoptr(FuDeviceList) device_list = fu_device_list_new(); g_autoptr(GError) error = NULL; FuDeviceListReplugHelper helper; /* fake devices */ fu_device_set_id(device1, "device1"); fu_device_set_name(device1, "device1"); fu_device_add_private_flag(device1, FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_add_instance_id(device1, "foo"); fu_device_add_instance_id(device1, "bar"); fu_device_set_plugin(device1, "self-test"); fu_device_set_remove_delay(device1, FU_DEVICE_REMOVE_DELAY_USER_REPLUG); fu_device_set_id(device2, "device2"); fu_device_set_name(device2, "device2"); fu_device_add_private_flag(device2, FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_add_instance_id(device2, "baz"); fu_device_add_instance_id_full(device2, "bar", FU_DEVICE_INSTANCE_FLAG_COUNTERPART); /* matches */ fu_device_set_plugin(device2, "self-test"); fu_device_set_remove_delay(device2, FU_DEVICE_REMOVE_DELAY_USER_REPLUG); /* not yet added */ ret = fu_device_list_wait_for_replug(device_list, &error); g_assert_no_error(error); g_assert_true(ret); /* add device */ fu_device_list_add(device_list, device1); /* add duplicate */ fu_device_add_flag(device1, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); fu_device_list_add(device_list, device1); g_assert_false(fu_device_has_flag(device1, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG)); /* not waiting */ ret = fu_device_list_wait_for_replug(device_list, &error); g_assert_no_error(error); g_assert_true(ret); /* waiting */ helper.device_old = device1; helper.device_new = device2; helper.device_list = device_list; g_timeout_add(100, fu_device_list_remove_cb, &helper); g_timeout_add(200, fu_device_list_add_cb, &helper); fu_device_add_flag(device1, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); ret = fu_device_list_wait_for_replug(device_list, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_false(fu_device_has_flag(device1, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG)); /* should not be possible, but here we are */ fu_device_add_flag(device1, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); fu_device_add_flag(device2, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); fu_device_list_add(device_list, device1); g_assert_false(fu_device_has_flag(device1, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG)); g_assert_false(fu_device_has_flag(device2, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG)); /* add back the old device */ fu_device_add_flag(device1, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); fu_device_add_flag(device2, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); fu_device_list_remove(device_list, device2); fu_device_list_add(device_list, device1); g_assert_false(fu_device_has_flag(device1, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG)); g_assert_false(fu_device_has_flag(device2, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG)); } static void fu_device_list_compatible_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; g_autoptr(FuDevice) device1 = fu_device_new(self->ctx); g_autoptr(FuDevice) device2 = fu_device_new(self->ctx); g_autoptr(FuDevice) device_old = NULL; g_autoptr(FuDeviceList) device_list = fu_device_list_new(); g_autoptr(GPtrArray) devices_all = NULL; g_autoptr(GPtrArray) devices_active = NULL; FuDevice *device; guint added_cnt = 0; guint changed_cnt = 0; guint removed_cnt = 0; g_signal_connect(FU_DEVICE_LIST(device_list), "added", G_CALLBACK(fu_test_device_list_count_cb), &added_cnt); g_signal_connect(FU_DEVICE_LIST(device_list), "removed", G_CALLBACK(fu_test_device_list_count_cb), &removed_cnt); g_signal_connect(FU_DEVICE_LIST(device_list), "changed", G_CALLBACK(fu_test_device_list_count_cb), &changed_cnt); /* add one device in runtime mode */ fu_device_set_id(device1, "device1"); fu_device_set_plugin(device1, "plugin-for-runtime"); fu_device_build_vendor_id(device1, "USB", "0x20A0"); fu_device_set_version_format(device1, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device1, "1.2.3"); fu_device_add_private_flag(device1, FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_add_private_flag(device1, FU_DEVICE_PRIVATE_FLAG_DELAYED_REMOVAL); fu_device_add_instance_id(device1, "foobar"); fu_device_add_instance_id_full(device1, "bootloader", FU_DEVICE_INSTANCE_FLAG_COUNTERPART); fu_device_set_remove_delay(device1, 100); fu_device_list_add(device_list, device1); g_assert_cmpint(added_cnt, ==, 1); g_assert_cmpint(removed_cnt, ==, 0); g_assert_cmpint(changed_cnt, ==, 0); /* add another device in bootloader mode */ fu_device_set_id(device2, "device2"); fu_device_set_plugin(device2, "plugin-for-bootloader"); fu_device_add_instance_id(device2, "bootloader"); fu_device_add_private_flag(device2, FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); /* verify only a changed event was generated */ added_cnt = removed_cnt = changed_cnt = 0; fu_device_list_remove(device_list, device1); fu_device_list_add(device_list, device2); g_assert_cmpint(added_cnt, ==, 0); g_assert_cmpint(removed_cnt, ==, 0); g_assert_cmpint(changed_cnt, ==, 1); /* device2 should inherit the vendor ID and version from device1 */ g_assert_true(fu_device_has_vendor_id(device2, "USB:0x20A0")); g_assert_cmpstr(fu_device_get_version(device2), ==, "1.2.3"); /* one device is active */ devices_active = fu_device_list_get_active(device_list); g_assert_cmpint(devices_active->len, ==, 1); device = g_ptr_array_index(devices_active, 0); g_assert_cmpstr(fu_device_get_id(device), ==, "1a8d0d9a96ad3e67ba76cf3033623625dc6d6882"); /* the list knows about both devices, list in order of active->old */ devices_all = fu_device_list_get_all(device_list); g_assert_cmpint(devices_all->len, ==, 2); device = g_ptr_array_index(devices_all, 0); g_assert_cmpstr(fu_device_get_id(device), ==, "1a8d0d9a96ad3e67ba76cf3033623625dc6d6882"); device = g_ptr_array_index(devices_all, 1); g_assert_cmpstr(fu_device_get_id(device), ==, "99249eb1bd9ef0b6e192b271a8cb6a3090cfec7a"); /* verify we can get the old device from the new device */ device_old = fu_device_list_get_old(device_list, device2); g_assert_true(device_old == device1); } static void fu_device_list_remove_chain_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; g_autoptr(FuDeviceList) device_list = fu_device_list_new(); g_autoptr(FuDevice) device_child = fu_device_new(self->ctx); g_autoptr(FuDevice) device_parent = fu_device_new(self->ctx); guint added_cnt = 0; guint changed_cnt = 0; guint removed_cnt = 0; g_signal_connect(FU_DEVICE_LIST(device_list), "added", G_CALLBACK(fu_test_device_list_count_cb), &added_cnt); g_signal_connect(FU_DEVICE_LIST(device_list), "removed", G_CALLBACK(fu_test_device_list_count_cb), &removed_cnt); g_signal_connect(FU_DEVICE_LIST(device_list), "changed", G_CALLBACK(fu_test_device_list_count_cb), &changed_cnt); /* add child */ fu_device_set_id(device_child, "child"); fu_device_add_instance_id(device_child, "child-GUID-1"); fu_device_list_add(device_list, device_child); g_assert_cmpint(added_cnt, ==, 1); g_assert_cmpint(removed_cnt, ==, 0); g_assert_cmpint(changed_cnt, ==, 0); /* add parent */ fu_device_set_id(device_parent, "parent"); fu_device_add_instance_id(device_parent, "parent-GUID-1"); fu_device_add_child(device_parent, device_child); fu_device_list_add(device_list, device_parent); g_assert_cmpint(added_cnt, ==, 2); g_assert_cmpint(removed_cnt, ==, 0); g_assert_cmpint(changed_cnt, ==, 0); /* make sure that removing the parent causes both to go; but the child to go first */ fu_device_list_remove(device_list, device_parent); g_assert_cmpint(added_cnt, ==, 2); g_assert_cmpint(removed_cnt, ==, 2); g_assert_cmpint(changed_cnt, ==, 0); } static void fu_device_list_explicit_order_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; g_autoptr(FuDevice) device_child = fu_device_new(self->ctx); g_autoptr(FuDevice) device_root = fu_device_new(self->ctx); g_autoptr(FuDeviceList) device_list = fu_device_list_new(); /* add both */ fu_device_set_id(device_root, "device"); fu_device_add_instance_id(device_root, "foobar"); fu_device_set_id(device_child, "device-child"); fu_device_add_instance_id(device_child, "baz"); fu_device_add_child(device_root, device_child); fu_device_list_add(device_list, device_root); fu_device_add_private_flag(device_root, FU_DEVICE_PRIVATE_FLAG_EXPLICIT_ORDER); fu_device_list_depsolve_order(device_list, device_root); g_assert_cmpint(fu_device_get_order(device_root), ==, G_MAXINT); g_assert_cmpint(fu_device_get_order(device_child), ==, G_MAXINT); } static void fu_device_list_explicit_order_post_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; g_autoptr(FuDevice) device_child = fu_device_new(self->ctx); g_autoptr(FuDevice) device_root = fu_device_new(self->ctx); g_autoptr(FuDeviceList) device_list = fu_device_list_new(); /* add both */ fu_device_set_id(device_root, "device"); fu_device_add_instance_id(device_root, "foobar"); fu_device_set_id(device_child, "device-child"); fu_device_add_instance_id(device_child, "baz"); fu_device_add_child(device_root, device_child); fu_device_list_add(device_list, device_root); fu_device_list_add(device_list, device_child); fu_device_list_depsolve_order(device_list, device_root); g_assert_cmpint(fu_device_get_order(device_root), ==, 0); g_assert_cmpint(fu_device_get_order(device_child), ==, -1); fu_device_add_private_flag(device_root, FU_DEVICE_PRIVATE_FLAG_EXPLICIT_ORDER); g_assert_cmpint(fu_device_get_order(device_root), ==, G_MAXINT); g_assert_cmpint(fu_device_get_order(device_child), ==, G_MAXINT); } static void fu_device_list_better_than_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device1 = fu_device_new(self->ctx); g_autoptr(FuDevice) device2 = fu_device_new(self->ctx); g_autoptr(FuDevice) device_best = NULL; g_autoptr(FuDevice) device_replug = NULL; g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuPlugin) plugin1 = fu_plugin_new(self->ctx); g_autoptr(FuPlugin) plugin2 = fu_plugin_new(self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; /* add a bad plugin */ fu_plugin_set_name(plugin2, "plugin2"); fu_engine_add_plugin(engine, plugin2); /* add a good plugin */ fu_plugin_set_name(plugin1, "plugin1"); fu_plugin_add_rule(plugin1, FU_PLUGIN_RULE_BETTER_THAN, "plugin2"); fu_engine_add_plugin(engine, plugin1); /* load the daemon */ ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* add a higher priority device */ fu_device_set_id(device1, "87ea5dfc8b8e384d848979496e706390b497e547"); fu_device_add_flag(device1, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device1, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_instance_id(device1, "12345678-1234-1234-1234-123456789012"); fu_device_add_protocol(device1, "com.acme"); fu_device_set_remove_delay(device1, FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE); fu_plugin_device_add(plugin1, device1); /* should be ignored */ fu_device_set_id(device2, "87ea5dfc8b8e384d848979496e706390b497e547"); fu_device_add_flag(device2, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device2, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_instance_id(device2, "12345678-1234-1234-1234-123456789012"); fu_device_add_protocol(device2, "com.acme"); fu_plugin_device_add(plugin2, device2); /* ensure we still have device1 */ device_best = fu_engine_get_device(engine, "87ea5dfc8b8e384d848979496e706390b497e547", &error); g_assert_no_error(error); g_assert_nonnull(device_best); g_assert_true(device_best == device1); /* should be replaced */ fu_device_add_flag(device1, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); fu_plugin_device_add(plugin2, device2); /* ensure we now have device2 */ device_replug = fu_engine_get_device(engine, "87ea5dfc8b8e384d848979496e706390b497e547", &error); g_assert_no_error(error); g_assert_nonnull(device_replug); g_assert_true(device_replug == device2); } static void fu_device_list_counterpart_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; g_autoptr(FuDeviceList) device_list = fu_device_list_new(); g_autoptr(FuDevice) device1 = fu_device_new(self->ctx); g_autoptr(FuDevice) device2 = fu_device_new(self->ctx); /* add and then remove runtime */ fu_device_set_id(device1, "device-runtime"); fu_device_add_instance_id(device1, "runtime"); /* 420dde7c-3102-5d8f-86bc-aaabd7920150 */ fu_device_add_instance_id_full(device1, "bootloader", FU_DEVICE_INSTANCE_FLAG_COUNTERPART); fu_device_set_remove_delay(device1, 100); fu_device_list_add(device_list, device1); fu_device_add_flag(device1, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG); fu_device_list_remove(device_list, device1); g_assert_true(fu_device_has_flag(device1, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG)); /* add bootloader */ fu_device_set_id(device2, "device-bootloader"); fu_device_add_instance_id(device2, "bootloader"); /* 015370aa-26f2-5daa-9661-a75bf4c1a913 */ fu_device_add_private_flag(device2, FU_DEVICE_PRIVATE_FLAG_REPLUG_MATCH_GUID); fu_device_add_private_flag(device2, FU_DEVICE_PRIVATE_FLAG_ADD_COUNTERPART_GUIDS); fu_device_list_add(device_list, device2); /* should have matched the runtime */ g_assert_false(fu_device_has_flag(device1, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG)); /* should not have *visible* GUID of runtime */ g_assert_false(fu_device_has_guid(device2, "runtime")); g_assert_false( fu_device_has_instance_id(device2, "runtime", FU_DEVICE_INSTANCE_FLAG_VISIBLE)); } static void fu_device_list_equivalent_id_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; g_autoptr(FuDevice) device1 = fu_device_new(self->ctx); g_autoptr(FuDevice) device2 = fu_device_new(self->ctx); g_autoptr(FuDevice) device = NULL; g_autoptr(FuDeviceList) device_list = fu_device_list_new(); g_autoptr(GError) error = NULL; fu_device_set_id(device1, "8e9cb71aeca70d2faedb5b8aaa263f6175086b2e"); fu_device_list_add(device_list, device1); fu_device_set_id(device2, "1a8d0d9a96ad3e67ba76cf3033623625dc6d6882"); fu_device_set_equivalent_id(device2, "8e9cb71aeca70d2faedb5b8aaa263f6175086b2e"); fu_device_set_priority(device2, 999); fu_device_list_add(device_list, device2); device = fu_device_list_get_by_id(device_list, "8e9c", &error); g_assert_no_error(error); g_assert_nonnull(device); g_assert_cmpstr(fu_device_get_id(device), ==, "1a8d0d9a96ad3e67ba76cf3033623625dc6d6882"); /* two devices with the 'same' priority */ fu_device_set_priority(device2, 0); device = fu_device_list_get_by_id(device_list, "8e9c", &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_assert_null(device); } static void fu_device_list_unconnected_no_delay_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; g_autoptr(FuDeviceList) device_list = fu_device_list_new(); g_autoptr(FuDevice) device1 = fu_device_new(self->ctx); g_autoptr(FuDevice) device2 = fu_device_new(self->ctx); fu_device_set_id(device1, "device1"); fu_device_add_flag(device1, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_instance_id(device1, "foobar"); fu_device_list_add(device_list, device1); g_assert_false(fu_device_has_private_flag(device1, FU_DEVICE_PRIVATE_FLAG_UNCONNECTED)); /* remove */ fu_device_list_remove(device_list, device1); g_assert_true(fu_device_has_private_flag(device1, FU_DEVICE_PRIVATE_FLAG_UNCONNECTED)); /* add back exact same device, then remove */ fu_device_list_add(device_list, device1); g_assert_false(fu_device_has_private_flag(device1, FU_DEVICE_PRIVATE_FLAG_UNCONNECTED)); fu_device_list_remove(device_list, device1); g_assert_true(fu_device_has_private_flag(device1, FU_DEVICE_PRIVATE_FLAG_UNCONNECTED)); /* add back device with same ID, then remove */ fu_device_set_id(device2, "device1"); fu_device_add_flag(device2, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_instance_id(device2, "foobar"); fu_device_list_add(device_list, device2); g_assert_false(fu_device_has_private_flag(device2, FU_DEVICE_PRIVATE_FLAG_UNCONNECTED)); fu_device_list_remove(device_list, device2); g_assert_true(fu_device_has_private_flag(device2, FU_DEVICE_PRIVATE_FLAG_UNCONNECTED)); } static void fu_device_list_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; g_autoptr(FuDeviceList) device_list = fu_device_list_new(); g_autoptr(FuDevice) device1 = fu_device_new(self->ctx); g_autoptr(FuDevice) device2 = fu_device_new(self->ctx); g_autoptr(GPtrArray) devices = NULL; g_autoptr(GPtrArray) devices2 = NULL; g_autoptr(GError) error = NULL; FuDevice *device; guint added_cnt = 0; guint changed_cnt = 0; guint removed_cnt = 0; g_signal_connect(FU_DEVICE_LIST(device_list), "added", G_CALLBACK(fu_test_device_list_count_cb), &added_cnt); g_signal_connect(FU_DEVICE_LIST(device_list), "removed", G_CALLBACK(fu_test_device_list_count_cb), &removed_cnt); g_signal_connect(FU_DEVICE_LIST(device_list), "changed", G_CALLBACK(fu_test_device_list_count_cb), &changed_cnt); /* add both */ fu_device_set_id(device1, "device1"); fu_device_add_instance_id(device1, "foobar"); fu_device_list_add(device_list, device1); fu_device_set_id(device2, "device2"); fu_device_add_instance_id(device2, "baz"); fu_device_list_add(device_list, device2); g_assert_cmpint(added_cnt, ==, 2); g_assert_cmpint(removed_cnt, ==, 0); g_assert_cmpint(changed_cnt, ==, 0); /* get all */ devices = fu_device_list_get_all(device_list); g_assert_cmpint(devices->len, ==, 2); device = g_ptr_array_index(devices, 0); g_assert_cmpstr(fu_device_get_id(device), ==, "99249eb1bd9ef0b6e192b271a8cb6a3090cfec7a"); /* find by ID */ device = fu_device_list_get_by_id(device_list, "99249eb1bd9ef0b6e192b271a8cb6a3090cfec7a", &error); g_assert_no_error(error); g_assert_nonnull(device); g_assert_cmpstr(fu_device_get_id(device), ==, "99249eb1bd9ef0b6e192b271a8cb6a3090cfec7a"); g_clear_object(&device); /* find by GUID */ device = fu_device_list_get_by_guid(device_list, "579a3b1c-d1db-5bdc-b6b9-e2c1b28d5b8a", &error); g_assert_no_error(error); g_assert_nonnull(device); g_assert_cmpstr(fu_device_get_id(device), ==, "1a8d0d9a96ad3e67ba76cf3033623625dc6d6882"); g_clear_object(&device); /* find by missing GUID */ device = fu_device_list_get_by_guid(device_list, "notfound", &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND); g_assert_null(device); /* remove device */ added_cnt = removed_cnt = changed_cnt = 0; fu_device_list_remove(device_list, device1); g_assert_cmpint(added_cnt, ==, 0); g_assert_cmpint(removed_cnt, ==, 1); g_assert_cmpint(changed_cnt, ==, 0); devices2 = fu_device_list_get_all(device_list); g_assert_cmpint(devices2->len, ==, 1); device = g_ptr_array_index(devices2, 0); g_assert_cmpstr(fu_device_get_id(device), ==, "1a8d0d9a96ad3e67ba76cf3033623625dc6d6882"); } static void fu_plugin_list_func(gconstpointer user_data) { GPtrArray *plugins; FuPlugin *plugin; g_autoptr(FuPluginList) plugin_list = fu_plugin_list_new(); g_autoptr(FuPlugin) plugin1 = fu_plugin_new(NULL); g_autoptr(FuPlugin) plugin2 = fu_plugin_new(NULL); g_autoptr(GError) error = NULL; fu_plugin_set_name(plugin1, "plugin1"); fu_plugin_set_name(plugin2, "plugin2"); /* get all the plugins */ fu_plugin_list_add(plugin_list, plugin1); fu_plugin_list_add(plugin_list, plugin2); plugins = fu_plugin_list_get_all(plugin_list); g_assert_cmpint(plugins->len, ==, 2); /* get a single plugin */ plugin = fu_plugin_list_find_by_name(plugin_list, "plugin1", &error); g_assert_no_error(error); g_assert_nonnull(plugin); g_assert_cmpstr(fu_plugin_get_name(plugin), ==, "plugin1"); /* does not exist */ plugin = fu_plugin_list_find_by_name(plugin_list, "nope", &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND); g_assert_null(plugin); } static void fu_plugin_list_depsolve_func(gconstpointer user_data) { GPtrArray *plugins; FuPlugin *plugin; gboolean ret; g_autoptr(FuPluginList) plugin_list = fu_plugin_list_new(); g_autoptr(FuPlugin) plugin1 = fu_plugin_new(NULL); g_autoptr(FuPlugin) plugin2 = fu_plugin_new(NULL); g_autoptr(GError) error = NULL; fu_plugin_set_name(plugin1, "plugin1"); fu_plugin_set_name(plugin2, "plugin2"); /* add rule then depsolve */ fu_plugin_list_add(plugin_list, plugin1); fu_plugin_list_add(plugin_list, plugin2); fu_plugin_add_rule(plugin1, FU_PLUGIN_RULE_RUN_AFTER, "plugin2"); ret = fu_plugin_list_depsolve(plugin_list, &error); g_assert_no_error(error); g_assert_true(ret); plugins = fu_plugin_list_get_all(plugin_list); g_assert_cmpint(plugins->len, ==, 2); plugin = g_ptr_array_index(plugins, 0); g_assert_cmpstr(fu_plugin_get_name(plugin), ==, "plugin2"); g_assert_cmpint(fu_plugin_get_order(plugin), ==, 0); g_assert_false(fu_plugin_has_flag(plugin, FWUPD_PLUGIN_FLAG_DISABLED)); /* add another rule, then re-depsolve */ fu_plugin_add_rule(plugin1, FU_PLUGIN_RULE_CONFLICTS, "plugin2"); ret = fu_plugin_list_depsolve(plugin_list, &error); g_assert_no_error(error); g_assert_true(ret); plugin = fu_plugin_list_find_by_name(plugin_list, "plugin1", &error); g_assert_no_error(error); g_assert_nonnull(plugin); g_assert_false(fu_plugin_has_flag(plugin, FWUPD_PLUGIN_FLAG_DISABLED)); plugin = fu_plugin_list_find_by_name(plugin_list, "plugin2", &error); g_assert_no_error(error); g_assert_nonnull(plugin); g_assert_true(fu_plugin_has_flag(plugin, FWUPD_PLUGIN_FLAG_DISABLED)); } static void fu_history_migrate_v1_func(gconstpointer user_data) { gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(GError) error = NULL; g_autoptr(GFile) file_dst = NULL; g_autoptr(GFile) file_src = NULL; g_autoptr(FuDevice) device = NULL; g_autoptr(FuHistory) history = NULL; g_autofree gchar *filename = NULL; /* load old version */ filename = g_test_build_filename(G_TEST_DIST, "tests", "history_v1.db", NULL); file_src = g_file_new_for_path(filename); file_dst = g_file_new_for_path("/tmp/fwupd-self-test/var/lib/fwupd/pending.db"); ret = g_file_copy(file_src, file_dst, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, &error); g_assert_no_error(error); g_assert_true(ret); /* create, migrating as required */ history = fu_history_new(ctx); g_assert_nonnull(history); /* get device */ device = fu_history_get_device_by_id(history, "2ba16d10df45823dd4494ff10a0bfccfef512c9d", &error); g_assert_no_error(error); g_assert_nonnull(device); g_assert_cmpstr(fu_device_get_id(device), ==, "2ba16d10df45823dd4494ff10a0bfccfef512c9d"); } static void fu_history_migrate_v2_func(gconstpointer user_data) { gboolean ret; g_autoptr(FuContext) ctx = fu_context_new(); g_autoptr(GError) error = NULL; g_autoptr(GFile) file_dst = NULL; g_autoptr(GFile) file_src = NULL; g_autoptr(FuDevice) device = NULL; g_autoptr(FuHistory) history = NULL; g_autofree gchar *filename = NULL; /* load old version */ filename = g_test_build_filename(G_TEST_DIST, "tests", "history_v2.db", NULL); file_src = g_file_new_for_path(filename); file_dst = g_file_new_for_path("/tmp/fwupd-self-test/var/lib/fwupd/pending.db"); ret = g_file_copy(file_src, file_dst, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, &error); g_assert_no_error(error); g_assert_true(ret); /* create, migrating as required */ history = fu_history_new(ctx); g_assert_nonnull(history); /* get device */ device = fu_history_get_device_by_id(history, "2ba16d10df45823dd4494ff10a0bfccfef512c9d", &error); g_assert_no_error(error); g_assert_nonnull(device); g_assert_cmpstr(fu_device_get_id(device), ==, "2ba16d10df45823dd4494ff10a0bfccfef512c9d"); } static void fu_test_plugin_device_added_cb(FuPlugin *plugin, FuDevice *device, gpointer user_data) { FuDevice **dev = (FuDevice **)user_data; *dev = g_object_ref(device); fu_test_loop_quit(); } static void fu_test_plugin_device_register_cb(FuPlugin *plugin, FuDevice *device, gpointer user_data) { /* fake being a daemon */ fu_plugin_runner_device_register(plugin, device); } static void fu_backend_usb_hotplug_cb(FuBackend *backend, FuDevice *device, gpointer user_data) { guint *cnt = (guint *)user_data; (*cnt)++; } static void fu_backend_usb_load_file(FuBackend *backend, const gchar *fn) { gboolean ret; g_autoptr(GError) error = NULL; g_autoptr(JsonParser) parser = json_parser_new(); ret = json_parser_load_from_file(parser, fn, &error); g_assert_no_error(error); g_assert_true(ret); ret = fwupd_codec_from_json(FWUPD_CODEC(backend), json_parser_get_root(parser), &error); g_assert_no_error(error); g_assert_true(ret); } /* * To generate the fwupd DS20 descriptor in the usb-devices.json file save fw-ds20.builder.xml: * * * 42 * 32 * * * Then run: * * fwupdtool firmware-build fw-ds20.builder.xml fw-ds20.bin * base64 fw-ds20.bin * * To generate the fake control transfer response, save fw-ds20.quirk: * * [USB\VID_273F&PID_1004] * Plugin = dfu * Icon = computer * * Then run: * * contrib/generate-ds20.py fw-ds20.quirk --bufsz 32 */ static void fu_backend_usb_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; guint cnt_added = 0; guint cnt_removed = 0; FuDevice *device_tmp; g_autofree gchar *usb_emulate_fn = NULL; g_autofree gchar *usb_emulate_fn2 = NULL; g_autofree gchar *usb_emulate_fn3 = NULL; g_autofree gchar *devicestr = NULL; g_autoptr(FuBackend) backend = fu_usb_backend_new(self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(GPtrArray) possible_plugins = NULL; /* check there were events */ g_object_set(backend, "device-gtype", FU_TYPE_USB_DEVICE, NULL); g_signal_connect(backend, "device-added", G_CALLBACK(fu_backend_usb_hotplug_cb), &cnt_added); g_signal_connect(backend, "device-removed", G_CALLBACK(fu_backend_usb_hotplug_cb), &cnt_removed); /* load the JSON into the backend */ g_assert_cmpstr(fu_backend_get_name(backend), ==, "usb"); g_assert_true(fu_backend_get_enabled(backend)); ret = fu_backend_setup(backend, FU_BACKEND_SETUP_FLAG_NONE, progress, &error); g_assert_cmpint(cnt_added, ==, 0); g_assert_cmpint(cnt_removed, ==, 0); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(cnt_added, ==, 0); g_assert_cmpint(cnt_removed, ==, 0); ret = fu_backend_coldplug(backend, progress, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(cnt_added, ==, 0); g_assert_cmpint(cnt_removed, ==, 0); usb_emulate_fn = g_test_build_filename(G_TEST_DIST, "tests", "usb-devices.json", NULL); g_assert_nonnull(usb_emulate_fn); fu_backend_usb_load_file(backend, usb_emulate_fn); g_assert_cmpint(cnt_added, ==, 1); g_assert_cmpint(cnt_removed, ==, 0); devices = fu_backend_get_devices(backend); g_assert_cmpint(devices->len, ==, 1); device_tmp = g_ptr_array_index(devices, 0); fu_device_set_context(device_tmp, self->ctx); ret = fu_device_probe(device_tmp, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_true(fu_device_has_flag(device_tmp, FWUPD_DEVICE_FLAG_EMULATED)); /* for debugging */ devicestr = fu_device_to_string(device_tmp); g_debug("%s", devicestr); /* check the fwupd DS20 descriptor was parsed */ g_assert_true(fu_device_has_icon(device_tmp, "computer")); possible_plugins = fu_device_get_possible_plugins(device_tmp); g_assert_cmpint(possible_plugins->len, ==, 1); g_assert_cmpstr(g_ptr_array_index(possible_plugins, 0), ==, "dfu"); /* load another device with the same VID:PID, and check that we did not get a replug */ usb_emulate_fn2 = g_test_build_filename(G_TEST_DIST, "tests", "usb-devices-replace.json", NULL); g_assert_nonnull(usb_emulate_fn2); fu_backend_usb_load_file(backend, usb_emulate_fn2); g_assert_cmpint(cnt_added, ==, 1); g_assert_cmpint(cnt_removed, ==, 0); /* load another device with a different VID:PID, and check that we *did* get a replug */ usb_emulate_fn3 = g_test_build_filename(G_TEST_DIST, "tests", "usb-devices-bootloader.json", NULL); g_assert_nonnull(usb_emulate_fn3); fu_backend_usb_load_file(backend, usb_emulate_fn3); g_assert_cmpint(cnt_added, ==, 2); g_assert_cmpint(cnt_removed, ==, 1); } static void fu_backend_usb_invalid_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; FuDevice *device_tmp; g_autofree gchar *usb_emulate_fn = NULL; g_autoptr(FuBackend) backend = fu_usb_backend_new(self->ctx); g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(JsonParser) parser = json_parser_new(); #ifndef SUPPORTED_BUILD g_test_expect_message("FuUsbDevice", G_LOG_LEVEL_WARNING, "*invalid platform version 0x0000000a, expected >= 0x00010805*"); g_test_expect_message("FuUsbDevice", G_LOG_LEVEL_WARNING, "failed to parse * BOS descriptor: *did not find magic*"); #endif /* load the JSON into the backend */ g_object_set(backend, "device-gtype", FU_TYPE_USB_DEVICE, NULL); usb_emulate_fn = g_test_build_filename(G_TEST_DIST, "tests", "usb-devices-invalid.json", NULL); ret = json_parser_load_from_file(parser, usb_emulate_fn, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_backend_setup(backend, FU_BACKEND_SETUP_FLAG_NONE, progress, &error); g_assert_no_error(error); g_assert_true(ret); ret = fwupd_codec_from_json(FWUPD_CODEC(backend), json_parser_get_root(parser), &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_backend_coldplug(backend, progress, &error); g_assert_no_error(error); g_assert_true(ret); devices = fu_backend_get_devices(backend); g_assert_cmpint(devices->len, ==, 1); device_tmp = g_ptr_array_index(devices, 0); fu_device_set_context(device_tmp, self->ctx); locker = fu_device_locker_new(device_tmp, &error); g_assert_no_error(error); g_assert_nonnull(locker); /* check the device was processed correctly by FuUsbDevice */ g_assert_cmpstr(fu_device_get_name(device_tmp), ==, "ColorHug2"); g_assert_true(fu_device_has_instance_id(device_tmp, "USB\\VID_273F&PID_1004", FU_DEVICE_INSTANCE_FLAG_VISIBLE)); g_assert_true(fu_device_has_vendor_id(device_tmp, "USB:0x273F")); /* check the fwupd DS20 descriptor was *not* parsed */ g_assert_false(fu_device_has_icon(device_tmp, "computer")); } static void fu_plugin_module_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; GError *error = NULL; gboolean ret; g_autoptr(FuDevice) device = NULL; g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuPlugin) plugin = fu_plugin_new_from_gtype(fu_test_plugin_get_type(), self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(XbSilo) silo_empty = xb_silo_new(); /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* create a fake device */ ret = fu_plugin_reset_config_values(plugin, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_plugin_set_config_value(plugin, "RegistrationSupported", "true", &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_plugin_runner_startup(plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); g_signal_connect(FU_PLUGIN(plugin), "device-added", G_CALLBACK(fu_test_plugin_device_added_cb), &device); g_signal_connect(FU_PLUGIN(plugin), "device-register", G_CALLBACK(fu_test_plugin_device_register_cb), &device); ret = fu_plugin_runner_coldplug(plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* check we did the right thing */ g_assert_nonnull(device); g_assert_cmpstr(fu_device_get_id(device), ==, "08d460be0f1f9f128413f816022a6439e0078018"); g_assert_cmpstr(fu_device_get_version_lowest(device), ==, "1.2.0"); g_assert_cmpstr(fu_device_get_version(device), ==, "1.2.2"); g_assert_cmpstr(fu_device_get_version_bootloader(device), ==, "0.1.2"); g_assert_cmpstr(fu_device_get_guid_default(device), ==, "b585990a-003e-5270-89d5-3705a17f9a43"); g_assert_cmpstr(fu_device_get_name(device), ==, "Integrated Webcamâ„¢"); g_signal_handlers_disconnect_by_data(plugin, &device); } static void fu_history_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; GPtrArray *checksums; gboolean ret; FuDevice *device; FuRelease *release; g_autoptr(FuDevice) device_found = NULL; g_autoptr(FuHistory) history = NULL; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) approved_firmware = NULL; g_autofree gchar *dirname = NULL; g_autofree gchar *filename = NULL; /* create */ history = fu_history_new(self->ctx); g_assert_nonnull(history); /* delete the database */ dirname = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR_PKG); if (!g_file_test(dirname, G_FILE_TEST_IS_DIR)) return; filename = g_build_filename(dirname, "pending.db", NULL); (void)g_unlink(filename); /* add a device */ device = fu_device_new(self->ctx); fu_device_set_id(device, "self-test"); fu_device_set_name(device, "ColorHug"), fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_device_set_version(device, "3.0.1"), fu_device_set_update_state(device, FWUPD_UPDATE_STATE_FAILED); fu_device_set_update_error(device, "word"); fu_device_add_instance_id(device, "827edddd-9bb6-5632-889f-2c01255503da"); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_INTERNAL); fu_device_set_created_usec(device, 1514338000ull * G_USEC_PER_SEC); fu_device_set_modified_usec(device, 1514338999ull * G_USEC_PER_SEC); release = fu_release_new(); fu_release_set_filename(release, "/var/lib/dave.cap"), fu_release_add_checksum(release, "abcdef"); fu_release_set_version(release, "3.0.2"); fu_release_add_metadata_item(release, "FwupdVersion", VERSION); ret = fu_history_add_device(history, device, release, &error); g_assert_no_error(error); g_assert_true(ret); g_object_unref(release); /* ensure database was created */ g_assert_true(g_file_test(filename, G_FILE_TEST_EXISTS)); g_object_unref(device); /* get device */ device = fu_history_get_device_by_id(history, "2ba16d10df45823dd4494ff10a0bfccfef512c9d", &error); g_assert_no_error(error); g_assert_nonnull(device); g_assert_cmpstr(fu_device_get_id(device), ==, "2ba16d10df45823dd4494ff10a0bfccfef512c9d"); g_assert_cmpstr(fu_device_get_name(device), ==, "ColorHug"); g_assert_cmpstr(fu_device_get_version(device), ==, "3.0.1"); g_assert_cmpint(fu_device_get_update_state(device), ==, FWUPD_UPDATE_STATE_FAILED); g_assert_cmpstr(fu_device_get_update_error(device), ==, "word"); g_assert_cmpstr(fu_device_get_guid_default(device), ==, "827edddd-9bb6-5632-889f-2c01255503da"); g_assert_cmpint(fu_device_get_flags(device), ==, FWUPD_DEVICE_FLAG_INTERNAL | FWUPD_DEVICE_FLAG_HISTORICAL); g_assert_cmpint(fu_device_get_created_usec(device), ==, 1514338000ull * G_USEC_PER_SEC); g_assert_cmpint(fu_device_get_modified_usec(device), ==, 1514338999ull * G_USEC_PER_SEC); release = FU_RELEASE(fu_device_get_release_default(device)); g_assert_nonnull(release); g_assert_cmpstr(fu_release_get_version(release), ==, "3.0.2"); g_assert_cmpstr(fu_release_get_filename(release), ==, "/var/lib/dave.cap"); g_assert_cmpstr(fu_release_get_metadata_item(release, "FwupdVersion"), ==, VERSION); checksums = fu_release_get_checksums(release); g_assert_nonnull(checksums); g_assert_cmpint(checksums->len, ==, 1); g_assert_cmpstr(fwupd_checksum_get_by_kind(checksums, G_CHECKSUM_SHA1), ==, "abcdef"); ret = fu_history_add_device(history, device, release, &error); g_assert_no_error(error); g_assert_true(ret); /* get device that does not exist */ device_found = fu_history_get_device_by_id(history, "XXXXXXXXXXXXX", &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND); g_assert_null(device_found); g_clear_error(&error); /* get device that does exist */ device_found = fu_history_get_device_by_id(history, "2ba16d10df45823dd4494ff10a0bfccfef512c9d", &error); g_assert_no_error(error); g_assert_nonnull(device_found); g_object_unref(device_found); /* remove device */ ret = fu_history_remove_device(history, device, &error); g_assert_no_error(error); g_assert_true(ret); g_object_unref(device); /* get device that does not exist */ device_found = fu_history_get_device_by_id(history, "2ba16d10df45823dd4494ff10a0bfccfef512c9d", &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND); g_assert_null(device_found); g_clear_error(&error); /* approved firmware */ ret = fu_history_clear_approved_firmware(history, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_history_add_approved_firmware(history, "foo", &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_history_add_approved_firmware(history, "bar", &error); g_assert_no_error(error); g_assert_true(ret); approved_firmware = fu_history_get_approved_firmware(history, &error); g_assert_no_error(error); g_assert_nonnull(approved_firmware); g_assert_cmpint(approved_firmware->len, ==, 2); g_assert_cmpstr(g_ptr_array_index(approved_firmware, 0), ==, "foo"); g_assert_cmpstr(g_ptr_array_index(approved_firmware, 1), ==, "bar"); /* emulation-tag */ ret = fu_history_add_emulation_tag(history, "id", &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_history_has_emulation_tag(history, "id", &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_history_has_emulation_tag(history, NULL, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_history_remove_emulation_tag(history, "id", &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_history_remove_emulation_tag(history, "id", &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_history_has_emulation_tag(history, "id", &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND); g_assert_false(ret); } static GBytes * fu_test_build_cab(gboolean compressed, ...) { va_list args; g_autoptr(FuCabFirmware) cabinet = fu_cab_firmware_new(); g_autoptr(GError) error = NULL; g_autoptr(GBytes) cabinet_blob = NULL; fu_cab_firmware_set_compressed(cabinet, compressed); /* add each file */ va_start(args, compressed); do { const gchar *fn; const gchar *text; g_autoptr(FuCabImage) img = fu_cab_image_new(); g_autoptr(GBytes) blob = NULL; /* get filename */ fn = va_arg(args, const gchar *); if (fn == NULL) break; /* get contents */ text = va_arg(args, const gchar *); if (text == NULL) break; g_debug("creating %s with %s", fn, text); /* add a GCabFile to the cabinet */ blob = g_bytes_new_static(text, strlen(text)); fu_firmware_set_id(FU_FIRMWARE(img), fn); fu_firmware_set_bytes(FU_FIRMWARE(img), blob); fu_firmware_add_image(FU_FIRMWARE(cabinet), FU_FIRMWARE(img)); } while (TRUE); va_end(args); /* write the archive to a blob */ cabinet_blob = fu_firmware_write(FU_FIRMWARE(cabinet), &error); g_assert_no_error(error); g_assert_nonnull(cabinet_blob); return g_steal_pointer(&cabinet_blob); } static void fu_test_plugin_composite_device_added_cb(FuPlugin *plugin, FuDevice *device, gpointer user_data) { GPtrArray *devices = (GPtrArray *)user_data; g_ptr_array_add(devices, g_object_ref(device)); } static gint fu_plugin_composite_release_sort_cb(gconstpointer a, gconstpointer b) { FuRelease *release1 = *((FuRelease **)a); FuRelease *release2 = *((FuRelease **)b); FuDevice *device1 = fu_release_get_device(release1); FuDevice *device2 = fu_release_get_device(release2); if (fu_device_get_order(device1) < fu_device_get_order(device2)) return 1; if (fu_device_get_order(device1) > fu_device_get_order(device2)) return -1; return 0; } static void fu_plugin_composite_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; FuDevice *dev_tmp; GError *error = NULL; gboolean ret; g_autoptr(FuCabinet) cabinet = fu_cabinet_new(); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuPlugin) plugin = fu_plugin_new_from_gtype(fu_test_plugin_get_type(), self->ctx); g_autoptr(GBytes) blob = NULL; g_autoptr(GPtrArray) components = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(GPtrArray) releases = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(XbSilo) silo_empty = xb_silo_new(); /* no metadata in daemon */ fu_engine_set_silo(engine, silo_empty); /* create CAB file */ blob = fu_test_build_cab( FALSE, "acme.metainfo.xml", "\n" " com.acme.example.firmware\n" " \n" " b585990a-003e-5270-89d5-3705a17f9a43\n" " \n" " \n" " \n" " \n" "", "acme.module1.metainfo.xml", "\n" " com.acme.example.firmware.module1\n" " \n" " 7fddead7-12b5-4fb9-9fa0-6d30305df755\n" " \n" " \n" " \n" " \n" " \n" " plain\n" " \n" "", "acme.module2.metainfo.xml", "\n" " com.acme.example.firmware.module2\n" " \n" " b8fe6b45-8702-4bcd-8120-ef236caac76f\n" " \n" " \n" " \n" " \n" " \n" " plain\n" " \n" "", "firmware.bin", "world", NULL); ret = fu_firmware_parse_bytes(FU_FIRMWARE(cabinet), blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); components = fu_cabinet_get_components(cabinet, &error); g_assert_no_error(error); g_assert_nonnull(components); g_assert_cmpint(components->len, ==, 3); /* set up dummy plugin */ ret = fu_plugin_reset_config_values(plugin, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_plugin_set_config_value(plugin, "CompositeChild", "true", &error); g_assert_no_error(error); g_assert_true(ret); fu_engine_add_plugin(engine, plugin); ret = fu_plugin_runner_startup(plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_signal_connect(FU_PLUGIN(plugin), "device-added", G_CALLBACK(fu_test_plugin_composite_device_added_cb), devices); ret = fu_plugin_runner_coldplug(plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* check we found all composite devices */ g_assert_cmpint(devices->len, ==, 3); for (guint i = 0; i < devices->len; i++) { FuDevice *device = g_ptr_array_index(devices, i); fu_engine_add_device(engine, device); if (g_strcmp0(fu_device_get_id(device), "08d460be0f1f9f128413f816022a6439e0078018") == 0) { g_assert_cmpstr(fu_device_get_version(device), ==, "1.2.2"); } else if (g_strcmp0(fu_device_get_id(device), "c0a0a4aa6480ac28eea1ce164fbb466ca934e1ff") == 0) { g_assert_cmpstr(fu_device_get_version(device), ==, "1"); g_assert_nonnull(fu_device_get_parent(device)); } else if (g_strcmp0(fu_device_get_id(device), "bf455e9f371d2608d1cb67660fd2b335d3f6ef73") == 0) { g_assert_cmpstr(fu_device_get_version(device), ==, "10"); g_assert_nonnull(fu_device_get_parent(device)); } } /* produce install tasks */ for (guint i = 0; i < components->len; i++) { XbNode *component = g_ptr_array_index(components, i); /* do any devices pass the requirements */ for (guint j = 0; j < devices->len; j++) { FuDevice *device = g_ptr_array_index(devices, j); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error_local = NULL; /* is this component valid for the device */ fu_release_set_device(release, device); fu_release_set_request(release, request); if (!fu_release_load(release, cabinet, component, NULL, FWUPD_INSTALL_FLAG_NONE, &error_local)) { g_debug("requirement on %s:%s failed: %s", fu_device_get_id(device), xb_node_query_text(component, "id", NULL), error_local->message); continue; } g_ptr_array_add(releases, g_steal_pointer(&release)); } } g_assert_cmpint(releases->len, ==, 3); /* sort these by version, forcing fu_engine_install_releases() to sort by device order */ g_ptr_array_sort(releases, fu_plugin_composite_release_sort_cb); dev_tmp = fu_release_get_device(FU_RELEASE(g_ptr_array_index(releases, 0))); g_assert_cmpstr(fu_device_get_logical_id(dev_tmp), ==, "child1"); dev_tmp = fu_release_get_device(FU_RELEASE(g_ptr_array_index(releases, 1))); g_assert_cmpstr(fu_device_get_logical_id(dev_tmp), ==, "child2"); dev_tmp = fu_release_get_device(FU_RELEASE(g_ptr_array_index(releases, 2))); g_assert_cmpstr(fu_device_get_logical_id(dev_tmp), ==, NULL); /* install the cab */ ret = fu_engine_install_releases(engine, request, releases, cabinet, progress, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); /* verify we installed the parent first */ dev_tmp = fu_release_get_device(FU_RELEASE(g_ptr_array_index(releases, 0))); g_assert_cmpstr(fu_device_get_logical_id(dev_tmp), ==, NULL); dev_tmp = fu_release_get_device(FU_RELEASE(g_ptr_array_index(releases, 1))); g_assert_cmpstr(fu_device_get_logical_id(dev_tmp), ==, "child2"); dev_tmp = fu_release_get_device(FU_RELEASE(g_ptr_array_index(releases, 2))); g_assert_cmpstr(fu_device_get_logical_id(dev_tmp), ==, "child1"); /* verify everything upgraded */ for (guint i = 0; i < devices->len; i++) { FuDevice *device = g_ptr_array_index(devices, i); const gchar *metadata; if (g_strcmp0(fu_device_get_id(device), "08d460be0f1f9f128413f816022a6439e0078018") == 0) { g_assert_cmpstr(fu_device_get_version(device), ==, "1.2.3"); } else if (g_strcmp0(fu_device_get_id(device), "c0a0a4aa6480ac28eea1ce164fbb466ca934e1ff") == 0) { g_assert_cmpstr(fu_device_get_version(device), ==, "2"); } else if (g_strcmp0(fu_device_get_id(device), "bf455e9f371d2608d1cb67660fd2b335d3f6ef73") == 0) { g_assert_cmpstr(fu_device_get_version(device), ==, "11"); } /* verify prepare and cleanup ran on all devices */ metadata = fu_device_get_metadata(device, "frimbulator"); g_assert_cmpstr(metadata, ==, "1"); metadata = fu_device_get_metadata(device, "frombulator"); g_assert_cmpstr(metadata, ==, "1"); } } static void fu_plugin_composite_multistep_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device = NULL; g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); g_autoptr(FuPlugin) plugin = fu_plugin_new_from_gtype(fu_test_plugin_get_type(), self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(GPtrArray) releases = NULL; g_autoptr(XbBuilder) builder = xb_builder_new(); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_autoptr(XbSilo) silo = NULL; /* load engine to get FuConfig set up */ ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* add the fake metadata */ ret = xb_builder_source_load_xml( source, "\n" "\n" "\n" " \n" " b585990a-003e-5270-89d5-3705a17f9a43\n" " \n" " \n" " triplet\n" " com.acme.test\n" " \n" " \n" " \n" " aaa\n" " \n" " \n" " file://filename.cab\n" " ccc\n" " \n" " \n" " \n" " \n" " bbb\n" " \n" " \n" " file://filename.cab\n" " ccc\n" " \n" " \n" " \n" " \n" "\n" "", XB_BUILDER_SOURCE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); xb_builder_import_source(builder, source); silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, &error); g_assert_no_error(error); g_assert_nonnull(silo); fu_engine_set_silo(engine, silo); /* set up dummy plugin */ ret = fu_plugin_reset_config_values(plugin, &error); g_assert_no_error(error); g_assert_true(ret); fu_engine_add_plugin(engine, plugin); ret = fu_plugin_runner_startup(plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_signal_connect(FU_PLUGIN(plugin), "device-added", G_CALLBACK(fu_test_plugin_composite_device_added_cb), devices); ret = fu_plugin_runner_coldplug(plugin, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* add all the found devices */ g_assert_cmpint(devices->len, ==, 1); for (guint i = 0; i < devices->len; i++) { FuDevice *device_tmp = g_ptr_array_index(devices, i); fu_engine_add_device(engine, device_tmp); } /* check we did not dedupe the composite cab */ device = fu_engine_get_device(engine, "08d460be0f1f9f128413f816022a6439e0078018", &error); g_assert_no_error(error); g_assert_nonnull(device); releases = fu_engine_get_releases(engine, request, "08d460be0f1f9f128413f816022a6439e0078018", &error); g_assert_no_error(error); g_assert_nonnull(releases); g_assert_cmpint(releases->len, ==, 2); } static void fu_security_attr_func(gconstpointer user_data) { gboolean ret; g_autofree gchar *json1 = NULL; g_autofree gchar *json2 = NULL; g_autoptr(FuSecurityAttrs) attrs1 = fu_security_attrs_new(); g_autoptr(FuSecurityAttrs) attrs2 = fu_security_attrs_new(); g_autoptr(FwupdSecurityAttr) attr1 = fwupd_security_attr_new("org.fwupd.hsi.foo"); g_autoptr(FwupdSecurityAttr) attr2 = fwupd_security_attr_new("org.fwupd.hsi.bar"); g_autoptr(GError) error = NULL; fwupd_security_attr_set_plugin(attr1, "foo"); fwupd_security_attr_set_created(attr1, 0); fwupd_security_attr_set_plugin(attr2, "bar"); fwupd_security_attr_set_created(attr2, 0); fu_security_attrs_append(attrs1, attr1); fu_security_attrs_append(attrs1, attr2); json1 = fwupd_codec_to_json_string(FWUPD_CODEC(attrs1), FWUPD_CODEC_FLAG_NONE, &error); g_assert_no_error(error); g_assert_nonnull(json1); ret = g_strcmp0(json1, "{\n" " \"SecurityAttributes\" : [\n" " {\n" " \"AppstreamId\" : \"org.fwupd.hsi.foo\",\n" " \"HsiLevel\" : 0,\n" " \"Plugin\" : \"foo\",\n" " \"Uri\" : " "\"https://fwupd.github.io/libfwupdplugin/hsi.html#org.fwupd.hsi.foo\"\n" " },\n" " {\n" " \"AppstreamId\" : \"org.fwupd.hsi.bar\",\n" " \"HsiLevel\" : 0,\n" " \"Plugin\" : \"bar\",\n" " \"Uri\" : " "\"https://fwupd.github.io/libfwupdplugin/hsi.html#org.fwupd.hsi.bar\"\n" " }\n" " ]\n" "}") == 0; g_assert_true(ret); ret = fwupd_codec_from_json_string(FWUPD_CODEC(attrs2), json1, &error); if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_test_skip(error->message); return; } g_assert_no_error(error); g_assert_true(ret); json2 = fwupd_codec_to_json_string(FWUPD_CODEC(attrs2), FWUPD_CODEC_FLAG_NONE, &error); g_assert_no_error(error); g_assert_nonnull(json2); ret = g_strcmp0(json2, json1) == 0; g_assert_true(ret); } static void fu_common_cabinet_func(void) { gboolean ret; g_autofree gchar *filename = NULL; g_autoptr(FuCabinet) cabinet = fu_cabinet_new(); g_autoptr(FuFirmware) img1 = NULL; g_autoptr(FuFirmware) img2 = NULL; g_autoptr(GBytes) blob = NULL; g_autoptr(GBytes) jcat_blob1 = g_bytes_new_static("hello", 6); g_autoptr(GBytes) jcat_blob2 = g_bytes_new_static("hellX", 6); g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = NULL; filename = g_test_build_filename(G_TEST_BUILT, "tests", "multiple-rels", "multiple-rels-1.2.4.cab", NULL); stream = fu_input_stream_from_path(filename, &error); g_assert_no_error(error); g_assert_nonnull(stream); ret = fu_firmware_parse_stream(FU_FIRMWARE(cabinet), stream, 0x0, FU_FIRMWARE_PARSE_FLAG_CACHE_STREAM, &error); g_assert_no_error(error); g_assert_true(ret); /* add */ fu_cabinet_add_file(cabinet, "firmware.jcat", jcat_blob1); /* replace */ fu_cabinet_add_file(cabinet, "firmware.jcat", jcat_blob2); /* get data */ img1 = fu_firmware_get_image_by_id(FU_FIRMWARE(cabinet), "firmware.jcat", &error); g_assert_no_error(error); g_assert_nonnull(img1); blob = fu_firmware_get_bytes(FU_FIRMWARE(img1), &error); g_assert_no_error(error); g_assert_nonnull(blob); g_assert_cmpstr(g_bytes_get_data(blob, NULL), ==, "hellX"); /* get data that does not exist */ img2 = fu_firmware_get_image_by_id(FU_FIRMWARE(cabinet), "foo.jcat", &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND); g_assert_null(img2); } static void fu_memcpy_func(gconstpointer user_data) { const guint8 src[] = {'a', 'b', 'c', 'd', 'e'}; gboolean ret; guint8 dst[4]; g_autoptr(GError) error = NULL; /* copy entire buffer */ ret = fu_memcpy_safe(dst, sizeof(dst), 0x0, src, sizeof(src), 0x0, 4, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(memcmp(src, dst, 4), ==, 0); /* copy first char */ ret = fu_memcpy_safe(dst, sizeof(dst), 0x0, src, sizeof(src), 0x0, 1, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(dst[0], ==, 'a'); /* copy last char */ ret = fu_memcpy_safe(dst, sizeof(dst), 0x0, src, sizeof(src), 0x4, 1, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(dst[0], ==, 'e'); /* copy nothing */ ret = fu_memcpy_safe(dst, sizeof(dst), 0x0, src, sizeof(src), 0x0, 0, &error); g_assert_no_error(error); g_assert_true(ret); /* write past the end of dst */ ret = fu_memcpy_safe(dst, sizeof(dst), 0x0, src, sizeof(src), 0x0, 5, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE); g_assert_false(ret); g_clear_error(&error); /* write past the end of dst with offset */ ret = fu_memcpy_safe(dst, sizeof(dst), 0x1, src, sizeof(src), 0x0, 4, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_WRITE); g_assert_false(ret); g_clear_error(&error); /* read past the end of dst */ ret = fu_memcpy_safe(dst, sizeof(dst), 0x0, src, sizeof(src), 0x0, 6, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_READ); g_assert_false(ret); g_clear_error(&error); /* read past the end of src with offset */ ret = fu_memcpy_safe(dst, sizeof(dst), 0x0, src, sizeof(src), 0x4, 4, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_READ); g_assert_false(ret); g_clear_error(&error); } static void fu_console_func(gconstpointer user_data) { g_autoptr(FuConsole) console = fu_console_new(); fu_console_set_status_length(console, 20); fu_console_set_percentage_length(console, 50); g_print("\n"); for (guint i = 0; i < 100; i++) { fu_console_set_progress(console, FWUPD_STATUS_DECOMPRESSING, i); g_usleep(10000); } fu_console_set_progress(console, FWUPD_STATUS_IDLE, 0); for (guint i = 0; i < 100; i++) { guint pc = (i > 25 && i < 75) ? 0 : i; fu_console_set_progress(console, FWUPD_STATUS_LOADING, pc); g_usleep(10000); } fu_console_set_progress(console, FWUPD_STATUS_IDLE, 0); for (guint i = 0; i < 5000; i++) { fu_console_set_progress(console, FWUPD_STATUS_LOADING, 0); g_usleep(1000); } fu_console_set_progress(console, FWUPD_STATUS_IDLE, 0); } static gint fu_release_compare_func_cb(gconstpointer a, gconstpointer b) { FuRelease *release1 = *((FuRelease **)a); FuRelease *release2 = *((FuRelease **)b); return fu_release_compare(release1, release2); } static void fu_release_compare_func(gconstpointer user_data) { g_autoptr(GPtrArray) releases = g_ptr_array_new(); g_autoptr(FuDevice) device1 = fu_device_new(NULL); g_autoptr(FuDevice) device2 = fu_device_new(NULL); g_autoptr(FuDevice) device3 = fu_device_new(NULL); g_autoptr(FuRelease) release1 = fu_release_new(); g_autoptr(FuRelease) release2 = fu_release_new(); g_autoptr(FuRelease) release3 = fu_release_new(); fu_device_set_order(device1, 33); fu_release_set_device(release1, device1); fu_release_set_priority(release1, 0); fu_release_set_branch(release1, "1"); fu_device_set_order(device2, 11); fu_release_set_device(release2, device2); fu_release_set_priority(release2, 0); fu_release_set_branch(release2, "2"); fu_device_set_order(device3, 11); fu_release_set_device(release3, device3); fu_release_set_priority(release3, 99); fu_release_set_branch(release3, "3"); g_ptr_array_add(releases, release1); g_ptr_array_add(releases, release2); g_ptr_array_add(releases, release3); /* order the install tasks */ g_ptr_array_sort(releases, fu_release_compare_func_cb); g_assert_cmpint(releases->len, ==, 3); g_assert_cmpstr(fu_release_get_branch(g_ptr_array_index(releases, 0)), ==, "3"); g_assert_cmpstr(fu_release_get_branch(g_ptr_array_index(releases, 1)), ==, "2"); g_assert_cmpstr(fu_release_get_branch(g_ptr_array_index(releases, 2)), ==, "1"); } static void fu_release_uri_scheme_func(void) { struct { const gchar *in; const gchar *op; } strs[] = {{"https://foo.bar/baz", "https"}, {"HTTP://FOO.BAR/BAZ", "http"}, {"ftp://", "ftp"}, {"ftp:", "ftp"}, {"foobarbaz", NULL}, {"", NULL}, {NULL, NULL}}; for (guint i = 0; strs[i].in != NULL; i++) { g_autofree gchar *tmp = fu_release_uri_get_scheme(strs[i].in); g_assert_cmpstr(tmp, ==, strs[i].op); } } static void fu_release_trusted_report_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; FwupdRelease *rel; gboolean ret; g_autofree gchar *filename = NULL; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FwupdRemote) remote = fwupd_remote_new(); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(GFile) file = NULL; g_autoptr(XbBuilder) builder = xb_builder_new(); g_autoptr(XbBuilderNode) custom = xb_builder_node_new("custom"); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_autoptr(XbSilo) silo = NULL; g_autoptr(GPtrArray) releases = NULL; g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); /* add fake LVFS remote */ fwupd_remote_set_id(remote, "lvfs"); fu_engine_add_remote(engine, remote); /* load engine to get FuConfig set up */ ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* metadata with vendor id=123 */ filename = g_test_build_filename(G_TEST_DIST, "tests", "metadata-report1.xml", NULL); file = g_file_new_for_path(filename); ret = xb_builder_source_load_file(source, file, XB_BUILDER_SOURCE_FLAG_NONE, NULL, &error); g_assert_no_error(error); g_assert_true(ret); xb_builder_node_insert_text(custom, "value", "lvfs", "key", "fwupd::RemoteId", NULL); xb_builder_source_set_info(source, custom); xb_builder_import_source(builder, source); silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, &error); g_assert_no_error(error); g_assert_nonnull(silo); fu_engine_set_silo(engine, silo); /* add a dummy device */ fu_device_set_id(device, "dummy"); fu_device_set_version(device, "1.2.2"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_protocol(device, "com.acme"); fu_device_add_instance_id(device, "2d47f29b-83a2-4f31-a2e8-63474f4d4c2e"); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_engine_add_device(engine, device); /* ensure we set this as trusted */ releases = fu_engine_get_releases_for_device(engine, request, device, &error); g_assert_no_error(error); g_assert_nonnull(releases); g_assert_cmpint(releases->len, ==, 1); rel = g_ptr_array_index(releases, 0); g_assert_true(fwupd_release_has_flag(rel, FWUPD_RELEASE_FLAG_TRUSTED_REPORT)); } static void fu_release_trusted_report_oem_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; FwupdRelease *rel; gboolean ret; g_autofree gchar *filename = NULL; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(GFile) file = NULL; g_autoptr(XbBuilder) builder = xb_builder_new(); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_autoptr(XbSilo) silo = NULL; g_autoptr(GPtrArray) releases = NULL; g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); /* load engine to get FuConfig set up */ ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* metadata with FromOEM */ filename = g_test_build_filename(G_TEST_DIST, "tests", "metadata-report2.xml", NULL); file = g_file_new_for_path(filename); ret = xb_builder_source_load_file(source, file, XB_BUILDER_SOURCE_FLAG_NONE, NULL, &error); g_assert_no_error(error); g_assert_true(ret); xb_builder_import_source(builder, source); silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, &error); g_assert_no_error(error); g_assert_nonnull(silo); fu_engine_set_silo(engine, silo); /* add a dummy device */ fu_device_set_id(device, "dummy"); fu_device_set_version(device, "1.2.2"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_protocol(device, "com.acme"); fu_device_add_instance_id(device, "2d47f29b-83a2-4f31-a2e8-63474f4d4c2e"); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_engine_add_device(engine, device); /* ensure we set this as trusted */ releases = fu_engine_get_releases_for_device(engine, request, device, &error); g_assert_no_error(error); g_assert_nonnull(releases); g_assert_cmpint(releases->len, ==, 1); rel = g_ptr_array_index(releases, 0); g_assert_true(fwupd_release_has_flag(rel, FWUPD_RELEASE_FLAG_TRUSTED_REPORT)); } static void fu_release_no_trusted_report_upgrade_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; FwupdRelease *rel; gboolean ret; g_autofree gchar *filename = NULL; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(GFile) file = NULL; g_autoptr(XbBuilder) builder = xb_builder_new(); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_autoptr(XbSilo) silo = NULL; g_autoptr(GPtrArray) releases = NULL; g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); /* load engine to get FuConfig set up */ ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* metadata with FromOEM, but *NOT* an upgrade */ filename = g_test_build_filename(G_TEST_DIST, "tests", "metadata-report4.xml", NULL); file = g_file_new_for_path(filename); ret = xb_builder_source_load_file(source, file, XB_BUILDER_SOURCE_FLAG_NONE, NULL, &error); g_assert_no_error(error); g_assert_true(ret); xb_builder_import_source(builder, source); silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, &error); g_assert_no_error(error); g_assert_nonnull(silo); fu_engine_set_silo(engine, silo); /* add a dummy device */ fu_device_set_id(device, "dummy"); fu_device_set_version(device, "1.2.3"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_protocol(device, "com.acme"); fu_device_add_instance_id(device, "2d47f29b-83a2-4f31-a2e8-63474f4d4c2e"); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_engine_add_device(engine, device); /* ensure we set this as trusted */ releases = fu_engine_get_releases_for_device(engine, request, device, &error); g_assert_no_error(error); g_assert_nonnull(releases); g_assert_cmpint(releases->len, ==, 1); rel = g_ptr_array_index(releases, 0); g_assert_false(fwupd_release_has_flag(rel, FWUPD_RELEASE_FLAG_TRUSTED_REPORT)); } static void fu_release_no_trusted_report_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; FwupdRelease *rel; gboolean ret; g_autofree gchar *filename = NULL; g_autoptr(FuDevice) device = fu_device_new(self->ctx); g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; g_autoptr(GFile) file = NULL; g_autoptr(XbBuilder) builder = xb_builder_new(); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_autoptr(XbSilo) silo = NULL; g_autoptr(GPtrArray) releases = NULL; g_autoptr(FuEngineRequest) request = fu_engine_request_new(NULL); /* load engine to get FuConfig set up */ ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* metadata without OEM or valid VendorId as per tests/fwupd.conf */ filename = g_test_build_filename(G_TEST_DIST, "tests", "metadata-report3.xml", NULL); file = g_file_new_for_path(filename); ret = xb_builder_source_load_file(source, file, XB_BUILDER_SOURCE_FLAG_NONE, NULL, &error); g_assert_no_error(error); g_assert_true(ret); xb_builder_import_source(builder, source); silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, &error); g_assert_no_error(error); g_assert_nonnull(silo); fu_engine_set_silo(engine, silo); /* add a dummy device */ fu_device_set_id(device, "dummy"); fu_device_set_version(device, "1.2.2"); fu_device_build_vendor_id_u16(device, "USB", 0xFFFF); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); fu_device_add_protocol(device, "com.acme"); fu_device_add_instance_id(device, "2d47f29b-83a2-4f31-a2e8-63474f4d4c2e"); fu_device_set_version_format(device, FWUPD_VERSION_FORMAT_TRIPLET); fu_engine_add_device(engine, device); /* ensure trusted reports flag is not set */ releases = fu_engine_get_releases_for_device(engine, request, device, &error); g_assert_no_error(error); g_assert_nonnull(releases); g_assert_cmpint(releases->len, ==, 1); rel = g_ptr_array_index(releases, 0); g_assert_false(fwupd_release_has_flag(rel, FWUPD_RELEASE_FLAG_TRUSTED_REPORT)); } static void fu_common_store_cab_func(void) { gboolean ret; GBytes *blob_tmp; g_autoptr(FuCabinet) cabinet = fu_cabinet_new(); g_autoptr(GBytes) blob = NULL; g_autoptr(GError) error = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbNode) csum = NULL; g_autoptr(XbNode) rel = NULL; g_autoptr(XbNode) req = NULL; g_autoptr(XbQuery) query = NULL; /* create silo */ blob = fu_test_build_cab( FALSE, "acme.metainfo.xml", "\n" " com.acme.example.firmware\n" " ACME Firmware\n" " \n" " ae56e3fb-6528-5bc4-8b03-012f124075d7\n" " \n" " \n" " \n" " 5\n" " 7c211433f02071597741e6ff5a8ea34789abbf43\n" "

    We fixed things

    \n" "
    \n" "
    \n" " \n" " org.freedesktop.fwupd\n" " \n" "
    ", "firmware.dfu", "world", "firmware.dfu.asc", "signature", NULL); ret = fu_firmware_parse_bytes(FU_FIRMWARE(cabinet), blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); /* verify */ component = fu_cabinet_get_component(cabinet, "com.acme.example.firmware", &error); g_assert_no_error(error); g_assert_nonnull(component); query = xb_query_new_full(xb_node_get_silo(component), "releases/release", XB_QUERY_FLAG_FORCE_NODE_CACHE, &error); g_assert_no_error(error); g_assert_nonnull(query); rel = xb_node_query_first_full(component, query, &error); g_assert_no_error(error); g_assert_nonnull(rel); g_assert_cmpstr(xb_node_get_attr(rel, "version"), ==, "1.2.3"); csum = xb_node_query_first(rel, "checksum[@target='content']", &error); g_assert_nonnull(csum); g_assert_cmpstr(xb_node_get_text(csum), ==, "7c211433f02071597741e6ff5a8ea34789abbf43"); blob_tmp = xb_node_get_data(rel, "fwupd::FirmwareBasename"); g_assert_nonnull(blob_tmp); req = xb_node_query_first(component, "requires/id", &error); g_assert_no_error(error); g_assert_nonnull(req); } static void fu_common_store_cab_artifact_func(void) { gboolean ret; g_autoptr(GBytes) blob1 = NULL; g_autoptr(GBytes) blob2 = NULL; g_autoptr(GBytes) blob3 = NULL; g_autoptr(GBytes) blob4 = NULL; g_autoptr(GError) error = NULL; g_autoptr(FuCabinet) cabinet1 = fu_cabinet_new(); g_autoptr(FuCabinet) cabinet2 = fu_cabinet_new(); g_autoptr(FuCabinet) cabinet3 = fu_cabinet_new(); g_autoptr(FuCabinet) cabinet4 = fu_cabinet_new(); /* create silo (sha256, using artifacts object) */ blob1 = fu_test_build_cab( FALSE, "acme.metainfo.xml", "\n" " com.acme.example.firmware\n" " \n" " \n" " \n" " \n" " firmware.dfu\n" " 486EA46224D1BB4FB680F34F7C9AD96A8F24EC88BE73EA8E5A6C65260E9CB8A7\n" " \n" " \n" " \n" " \n" "", "firmware.dfu", "world", "firmware.dfu.asc", "signature", NULL); ret = fu_firmware_parse_bytes(FU_FIRMWARE(cabinet1), blob1, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); /* create silo (sha1, using artifacts object; mixed case) */ blob2 = fu_test_build_cab(FALSE, "acme.metainfo.xml", "\n" " com.acme.example.firmware\n" " \n" " \n" " \n" " \n" " firmware.dfu\n" " 7c211433f02071597741e6ff5a8ea34789abbF43\n" " \n" " \n" " \n" " \n" "", "firmware.dfu", "world", "firmware.dfu.asc", "signature", NULL); ret = fu_firmware_parse_bytes(FU_FIRMWARE(cabinet2), blob2, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); /* create silo (sha512, using artifacts object; lower case) */ blob3 = fu_test_build_cab( FALSE, "acme.metainfo.xml", "\n" " com.acme.example.firmware\n" " \n" " \n" " \n" " \n" " firmware.dfu\n" " " "11853df40f4b2b919d3815f64792e58d08663767a494bcbb38c0b2389d9140bbb170281b" "4a847be7757bde12c9cd0054ce3652d0ad3a1a0c92babb69798246ee\n" " \n" " \n" " \n" " \n" "", "firmware.dfu", "world", "firmware.dfu.asc", "signature", NULL); ret = fu_firmware_parse_bytes(FU_FIRMWARE(cabinet3), blob3, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); /* create silo (legacy release object) */ blob4 = fu_test_build_cab(FALSE, "acme.metainfo.xml", "\n" " com.acme.example.firmware\n" " \n" " \n" " " "486EA46224D1BB4FB680F34F7C9AD96A8F24EC88BE73EA8E5A6C65260E9CB8A7\n" " \n" " \n" "", "firmware.dfu", "world", "firmware.dfu.asc", "signature", NULL); ret = fu_firmware_parse_bytes(FU_FIRMWARE(cabinet4), blob4, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_common_store_cab_unsigned_func(void) { GBytes *blob_tmp; gboolean ret; g_autoptr(FuCabinet) cabinet = fu_cabinet_new(); g_autoptr(GBytes) blob = NULL; g_autoptr(GError) error = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbNode) csum = NULL; g_autoptr(XbNode) rel = NULL; g_autoptr(XbQuery) query = NULL; /* create silo */ blob = fu_test_build_cab(FALSE, "acme.metainfo.xml", "\n" " com.acme.example.firmware\n" " \n" " \n" " \n" "", "firmware.bin", "world", NULL); ret = fu_firmware_parse_bytes(FU_FIRMWARE(cabinet), blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); /* verify */ component = fu_cabinet_get_component(cabinet, "com.acme.example.firmware", &error); g_assert_no_error(error); g_assert_nonnull(component); query = xb_query_new_full(xb_node_get_silo(component), "releases/release", XB_QUERY_FLAG_FORCE_NODE_CACHE, &error); g_assert_no_error(error); g_assert_nonnull(query); rel = xb_node_query_first_full(component, query, &error); g_assert_no_error(error); g_assert_nonnull(rel); g_assert_cmpstr(xb_node_get_attr(rel, "version"), ==, "1.2.3"); csum = xb_node_query_first(rel, "checksum[@target='content']", &error); g_assert_null(csum); blob_tmp = xb_node_get_data(rel, "fwupd::FirmwareBasename"); g_assert_nonnull(blob_tmp); } static void fu_common_store_cab_sha256_func(void) { gboolean ret; g_autoptr(FuCabinet) cabinet = fu_cabinet_new(); g_autoptr(GBytes) blob = NULL; g_autoptr(GError) error = NULL; /* create silo */ blob = fu_test_build_cab( FALSE, "acme.metainfo.xml", "\n" " com.acme.example.firmware\n" " \n" " \n" " 486ea46224d1bb4fb680f34f7c9ad96a8f24ec88be73ea8e5a6c65260e9cb8a7\n" " \n" " \n" "", "firmware.bin", "world", NULL); ret = fu_firmware_parse_bytes(FU_FIRMWARE(cabinet), blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); } static void fu_common_store_cab_folder_func(void) { GBytes *blob_tmp; gboolean ret; g_autoptr(FuCabinet) cabinet = fu_cabinet_new(); g_autoptr(GBytes) blob = NULL; g_autoptr(GError) error = NULL; g_autoptr(XbNode) component = NULL; g_autoptr(XbNode) rel = NULL; g_autoptr(XbQuery) query = NULL; /* create silo */ blob = fu_test_build_cab(FALSE, "lvfs\\acme.metainfo.xml", "\n" " com.acme.example.firmware\n" " \n" " \n" " \n" "", "lvfs\\firmware.bin", "world", NULL); ret = fu_firmware_parse_bytes(FU_FIRMWARE(cabinet), blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_no_error(error); g_assert_true(ret); /* verify */ component = fu_cabinet_get_component(cabinet, "com.acme.example.firmware", &error); g_assert_no_error(error); g_assert_nonnull(component); query = xb_query_new_full(xb_node_get_silo(component), "releases/release", XB_QUERY_FLAG_FORCE_NODE_CACHE, &error); g_assert_no_error(error); g_assert_nonnull(query); rel = xb_node_query_first_full(component, query, &error); g_assert_no_error(error); g_assert_nonnull(rel); g_assert_cmpstr(xb_node_get_attr(rel, "version"), ==, "1.2.3"); blob_tmp = xb_node_get_data(rel, "fwupd::FirmwareBasename"); g_assert_nonnull(blob_tmp); } static void fu_common_store_cab_error_no_metadata_func(void) { gboolean ret; g_autoptr(FuCabinet) cabinet = fu_cabinet_new(); g_autoptr(GBytes) blob = NULL; g_autoptr(GError) error = NULL; blob = fu_test_build_cab(FALSE, "foo.txt", "hello", "bar.txt", "world", NULL); ret = fu_firmware_parse_bytes(FU_FIRMWARE(cabinet), blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE); g_assert_false(ret); } static void fu_common_store_cab_error_wrong_size_func(void) { gboolean ret; g_autoptr(FuCabinet) cabinet = fu_cabinet_new(); g_autoptr(GBytes) blob = NULL; g_autoptr(GError) error = NULL; blob = fu_test_build_cab(FALSE, "acme.metainfo.xml", "\n" " com.acme.example.firmware\n" " \n" " \n" " 7004701\n" " deadbeef\n" " \n" " \n" "", "firmware.bin", "world", NULL); ret = fu_firmware_parse_bytes(FU_FIRMWARE(cabinet), blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE); g_assert_false(ret); } static void fu_common_store_cab_error_missing_file_func(void) { gboolean ret; g_autoptr(FuCabinet) cabinet = fu_cabinet_new(); g_autoptr(GBytes) blob = NULL; g_autoptr(GError) error = NULL; blob = fu_test_build_cab(FALSE, "acme.metainfo.xml", "\n" " com.acme.example.firmware\n" " \n" " \n" " \n" " \n" " \n" "", "firmware.bin", "world", NULL); ret = fu_firmware_parse_bytes(FU_FIRMWARE(cabinet), blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE); g_assert_false(ret); } static void fu_common_store_cab_error_size_func(void) { gboolean ret; g_autoptr(FuCabinet) cabinet = fu_cabinet_new(); g_autoptr(GBytes) blob = NULL; g_autoptr(GError) error = NULL; blob = fu_test_build_cab(FALSE, "acme.metainfo.xml", "\n" " com.acme.example.firmware\n" " \n" " \n" " \n" "", "firmware.bin", "world", NULL); fu_firmware_set_size_max(FU_FIRMWARE(cabinet), 123); ret = fu_firmware_parse_bytes(FU_FIRMWARE(cabinet), blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE); g_assert_false(ret); } static void fu_common_store_cab_error_wrong_checksum_func(void) { gboolean ret; g_autoptr(FuCabinet) cabinet = fu_cabinet_new(); g_autoptr(GBytes) blob = NULL; g_autoptr(GError) error = NULL; blob = fu_test_build_cab(FALSE, "acme.metainfo.xml", "\n" " com.acme.example.firmware\n" " \n" " \n" " deadbeef\n" " \n" " \n" "", "firmware.bin", "world", NULL); ret = fu_firmware_parse_bytes(FU_FIRMWARE(cabinet), blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE); g_assert_false(ret); } static void fu_engine_modify_bios_settings_func(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; const gchar *current; FwupdBiosSetting *attr1; FwupdBiosSetting *attr2; FwupdBiosSetting *attr3; FwupdBiosSetting *attr4; g_autofree gchar *test_dir = NULL; g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(GError) error = NULL; g_autoptr(FuBiosSettings) attrs = NULL; g_autoptr(GPtrArray) items = NULL; g_autoptr(GHashTable) bios_settings = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); #ifdef _WIN32 g_test_skip("BIOS settings not supported on Windows"); return; #endif /* Load contrived attributes */ test_dir = g_test_build_filename(G_TEST_DIST, "tests", "bios-attrs", NULL); (void)g_setenv("FWUPD_SYSFSFWATTRIBDIR", test_dir, TRUE); ret = fu_context_reload_bios_settings(fu_engine_get_context(engine), &error); g_assert_no_error(error); g_assert_true(ret); attrs = fu_context_get_bios_settings(fu_engine_get_context(engine)); items = fu_bios_settings_get_all(attrs); g_assert_cmpint(items->len, ==, 4); /* enumeration */ attr1 = fu_context_get_bios_setting(fu_engine_get_context(engine), "com.fwupd-internal.Absolute"); g_assert_nonnull(attr1); current = fwupd_bios_setting_get_current_value(attr1); g_assert_nonnull(current); g_hash_table_insert(bios_settings, g_strdup("Absolute"), g_strdup("Disabled")); ret = fu_engine_modify_bios_settings(engine, bios_settings, FALSE, &error); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO); g_assert_false(ret); g_clear_error(&error); g_hash_table_remove_all(bios_settings); g_hash_table_insert(bios_settings, g_strdup("Absolute"), g_strdup("Enabled")); ret = fu_engine_modify_bios_settings(engine, bios_settings, FALSE, &error); g_assert_no_error(error); g_assert_true(ret); g_hash_table_remove_all(bios_settings); g_hash_table_insert(bios_settings, g_strdup("Absolute"), g_strdup("off")); ret = fu_engine_modify_bios_settings(engine, bios_settings, FALSE, &error); g_assert_no_error(error); g_assert_true(ret); g_hash_table_remove_all(bios_settings); g_hash_table_insert(bios_settings, g_strdup("Absolute"), g_strdup("FOO")); ret = fu_engine_modify_bios_settings(engine, bios_settings, FALSE, &error); g_assert_false(ret); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_clear_error(&error); /* use BiosSettingId instead */ g_hash_table_remove_all(bios_settings); g_hash_table_insert(bios_settings, g_strdup("com.fwupd-internal.Absolute"), g_strdup("on")); ret = fu_engine_modify_bios_settings(engine, bios_settings, FALSE, &error); g_assert_no_error(error); g_assert_true(ret); g_hash_table_remove_all(bios_settings); g_hash_table_insert(bios_settings, g_strdup("com.fwupd-internal.Absolute"), g_strdup("off")); ret = fu_engine_modify_bios_settings(engine, bios_settings, FALSE, &error); g_assert_no_error(error); g_assert_true(ret); /* string */ attr2 = fu_context_get_bios_setting(fu_engine_get_context(engine), "com.fwupd-internal.Asset"); g_assert_nonnull(attr2); current = fwupd_bios_setting_get_current_value(attr2); g_assert_nonnull(current); g_hash_table_remove_all(bios_settings); g_hash_table_insert(bios_settings, g_strdup("Asset"), g_strdup("0")); ret = fu_engine_modify_bios_settings(engine, bios_settings, FALSE, &error); g_assert_no_error(error); g_assert_true(ret); g_hash_table_remove_all(bios_settings); g_hash_table_insert(bios_settings, g_strdup("Asset"), g_strdup("1")); ret = fu_engine_modify_bios_settings(engine, bios_settings, FALSE, &error); g_assert_no_error(error); g_assert_true(ret); g_hash_table_remove_all(bios_settings); g_hash_table_insert( bios_settings, g_strdup("Absolute"), g_strdup("1234567891123456789112345678911234567891123456789112345678911111")); ret = fu_engine_modify_bios_settings(engine, bios_settings, FALSE, &error); g_assert_false(ret); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_clear_error(&error); /* integer */ attr3 = fu_context_get_bios_setting(fu_engine_get_context(engine), "com.fwupd-internal.CustomChargeStop"); g_assert_nonnull(attr3); current = fwupd_bios_setting_get_current_value(attr3); g_assert_nonnull(current); g_hash_table_remove_all(bios_settings); g_hash_table_insert(bios_settings, g_strdup("CustomChargeStop"), g_strdup("75")); ret = fu_engine_modify_bios_settings(engine, bios_settings, FALSE, &error); g_assert_true(ret); g_hash_table_remove_all(bios_settings); g_hash_table_insert(bios_settings, g_strdup("CustomChargeStop"), g_strdup("110")); ret = fu_engine_modify_bios_settings(engine, bios_settings, FALSE, &error); g_assert_false(ret); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_clear_error(&error); g_hash_table_remove_all(bios_settings); g_hash_table_insert(bios_settings, g_strdup("CustomChargeStop"), g_strdup("1")); ret = fu_engine_modify_bios_settings(engine, bios_settings, FALSE, &error); g_assert_false(ret); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_clear_error(&error); /* force it to read only */ g_hash_table_remove_all(bios_settings); g_hash_table_insert(bios_settings, g_strdup("CustomChargeStop"), g_strdup("70")); ret = fu_engine_modify_bios_settings(engine, bios_settings, TRUE, &error); g_assert_no_error(error); g_assert_true(ret); /* Read Only */ attr4 = fu_context_get_bios_setting(fu_engine_get_context(engine), "com.fwupd-internal.pending_reboot"); g_assert_nonnull(attr4); current = fwupd_bios_setting_get_current_value(attr4); g_assert_nonnull(current); g_hash_table_remove_all(bios_settings); g_hash_table_insert(bios_settings, g_strdup("pending_reboot"), g_strdup("foo")); ret = fu_engine_modify_bios_settings(engine, bios_settings, FALSE, &error); g_assert_false(ret); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_clear_error(&error); g_hash_table_remove_all(bios_settings); g_hash_table_insert(bios_settings, g_strdup("CustomChargeStop"), g_strdup("80")); ret = fu_engine_modify_bios_settings(engine, bios_settings, FALSE, &error); g_assert_false(ret); g_assert_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED); g_clear_error(&error); } GFileInputStream * _g_local_file_input_stream_new(int fd); static void fu_unix_seekable_input_stream_func(void) { #ifdef HAVE_GIO_UNIX gssize ret; gint fd; guint8 buf[6] = {0}; g_autofree gchar *fn = NULL; g_autoptr(GError) error = NULL; g_autoptr(GInputStream) stream = NULL; fn = g_test_build_filename(G_TEST_DIST, "tests", "metadata.xml", NULL); g_assert_nonnull(fn); fd = g_open(fn, O_RDONLY, 0); g_assert_cmpint(fd, >=, 0); stream = fu_unix_seekable_input_stream_new(fd, TRUE); g_assert_nonnull(stream); /* first chuck */ ret = g_input_stream_read(stream, buf, sizeof(buf) - 1, NULL, &error); g_assert_no_error(error); g_assert_cmpint(ret, ==, 5); g_assert_cmpstr((const gchar *)buf, ==, ", 1000000); ret = fwupd_remote_setup(remote, &error); g_assert_no_error(error); g_assert_true(ret); order = fwupd_remote_get_order_before(remote); g_assert_nonnull(order); g_assert_cmpint(g_strv_length(order), ==, 1); g_assert_cmpstr(order[0], ==, "before"); order = fwupd_remote_get_order_after(remote); g_assert_nonnull(order); g_assert_cmpint(g_strv_length(order), ==, 1); g_assert_cmpstr(order[0], ==, "after"); /* to/from GVariant */ fwupd_remote_set_priority(remote, 999); data = fwupd_codec_to_variant(FWUPD_CODEC(remote), FWUPD_CODEC_FLAG_NONE); ret = fwupd_codec_from_variant(FWUPD_CODEC(remote2), data, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpstr(fwupd_remote_get_username(remote2), ==, "user"); g_assert_cmpint(fwupd_remote_get_priority(remote2), ==, 999); /* jcat-tool is not a hard dep, and the tests create an empty file if unfound */ ret = fwupd_remote_load_signature(remote, fwupd_remote_get_filename_cache_sig(remote), &error); if (!ret) { if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_READ)) { g_test_skip("no jcat-tool, so skipping test"); return; } } g_assert_no_error(error); g_assert_true(ret); /* to JSON */ fwupd_remote_set_filename_source(remote2, NULL); fwupd_remote_set_checksum_sig( remote2, "dd1b4fd2a59bb0e4d9ea760c658ac3cf9336c7b6729357bab443485b5cf071b2"); fwupd_remote_set_filename_cache(remote2, "./libfwupd/tests/auth/firmware.xml.gz"); json = fwupd_codec_to_json_string(FWUPD_CODEC(remote2), FWUPD_CODEC_FLAG_NONE, &error); g_assert_no_error(error); g_assert_nonnull(json); ret = g_strcmp0( json, "{\n" " \"Id\" : \"auth\",\n" " \"Kind\" : \"download\",\n" " \"ReportUri\" : \"https://fwupd.org/lvfs/firmware/report\",\n" " \"MetadataUri\" : \"https://cdn.fwupd.org/downloads/firmware.xml.gz\",\n" " \"MetadataUriSig\" : \"https://cdn.fwupd.org/downloads/firmware.xml.gz.jcat\",\n" " \"FirmwareBaseUri\" : \"https://my.fancy.cdn/\",\n" " \"Username\" : \"user\",\n" " \"Password\" : \"pass\",\n" " \"ChecksumSig\" : " "\"dd1b4fd2a59bb0e4d9ea760c658ac3cf9336c7b6729357bab443485b5cf071b2\",\n" " \"FilenameCache\" : \"./libfwupd/tests/auth/firmware.xml.gz\",\n" " \"FilenameCacheSig\" : \"./libfwupd/tests/auth/firmware.xml.gz.jcat\",\n" " \"Flags\" : 9,\n" " \"Enabled\" : true,\n" " \"ApprovalRequired\" : false,\n" " \"AutomaticReports\" : false,\n" " \"AutomaticSecurityReports\" : true,\n" " \"Priority\" : 999,\n" " \"Mtime\" : 0,\n" " \"RefreshInterval\" : 86400\n" "}") == 0; g_assert_true(ret); } static void fu_remote_duplicate_func(void) { gboolean ret; g_autofree gchar *fn2 = NULL; g_autofree gchar *fn = NULL; g_autoptr(FwupdRemote) remote = fwupd_remote_new(); g_autoptr(GError) error = NULL; fn = g_test_build_filename(G_TEST_DIST, "tests", "stable.conf", NULL); ret = fu_remote_load_from_filename(remote, fn, NULL, &error); g_assert_no_error(error); g_assert_true(ret); fn2 = g_test_build_filename(G_TEST_DIST, "tests", "disabled.conf", NULL); ret = fu_remote_load_from_filename(remote, fn2, NULL, &error); g_assert_no_error(error); g_assert_true(ret); ret = fwupd_remote_setup(remote, &error); g_assert_no_error(error); g_assert_true(ret); ret = fwupd_remote_setup(remote, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_false(fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_ENABLED)); g_assert_cmpstr(fwupd_remote_get_username(remote), ==, NULL); g_assert_cmpstr(fwupd_remote_get_password(remote), ==, ""); g_assert_cmpstr(fwupd_remote_get_filename_cache(remote), ==, "/tmp/fwupd-self-test/stable.xml"); } /* verify we used the metadata path for firmware */ static void fu_remote_nopath_func(void) { gboolean ret; g_autofree gchar *firmware_uri = NULL; g_autofree gchar *fn = NULL; g_autoptr(FwupdRemote) remote = NULL; g_autoptr(GError) error = NULL; g_autofree gchar *directory = NULL; remote = fwupd_remote_new(); directory = g_build_filename(FWUPD_LOCALSTATEDIR, "lib", "fwupd", "remotes2.d", NULL); fwupd_remote_set_remotes_dir(remote, directory); fn = g_test_build_filename(G_TEST_DIST, "tests", "firmware-nopath.conf", NULL); ret = fu_remote_load_from_filename(remote, fn, NULL, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(fwupd_remote_get_kind(remote), ==, FWUPD_REMOTE_KIND_DOWNLOAD); g_assert_cmpint(fwupd_remote_get_priority(remote), ==, 0); g_assert_true(fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_ENABLED)); g_assert_cmpstr(fwupd_remote_get_checksum(remote), ==, NULL); g_assert_cmpstr(fwupd_remote_get_metadata_uri(remote), ==, "https://s3.amazonaws.com/lvfsbucket/downloads/firmware.xml.gz"); g_assert_cmpstr(fwupd_remote_get_metadata_uri_sig(remote), ==, "https://s3.amazonaws.com/lvfsbucket/downloads/firmware.xml.gz.jcat"); firmware_uri = fwupd_remote_build_firmware_uri(remote, "firmware.cab", &error); g_assert_no_error(error); g_assert_cmpstr(firmware_uri, ==, "https://s3.amazonaws.com/lvfsbucket/downloads/firmware.cab"); } static void fu_remote_local_func(void) { gboolean ret; g_autofree gchar *fn = NULL; g_autofree gchar *json = NULL; g_autoptr(FwupdRemote) remote = NULL; g_autoptr(FwupdRemote) remote2 = fwupd_remote_new(); g_autoptr(GError) error = NULL; g_autoptr(GVariant) data = NULL; remote = fwupd_remote_new(); fn = g_test_build_filename(G_TEST_DIST, "tests", "dell-esrt.conf", NULL); ret = fu_remote_load_from_filename(remote, fn, NULL, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpint(fwupd_remote_get_kind(remote), ==, FWUPD_REMOTE_KIND_LOCAL); g_assert_true(fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_ENABLED)); g_assert_null(fwupd_remote_get_metadata_uri(remote)); g_assert_null(fwupd_remote_get_metadata_uri_sig(remote)); g_assert_null(fwupd_remote_get_report_uri(remote)); g_assert_cmpstr(fwupd_remote_get_title(remote), ==, "Enable UEFI capsule updates on Dell systems"); g_assert_cmpstr(fwupd_remote_get_filename_cache(remote), ==, "@datadir@/fwupd/remotes.d/dell-esrt/firmware.xml"); g_assert_cmpstr(fwupd_remote_get_filename_cache_sig(remote), ==, NULL); g_assert_cmpstr(fwupd_remote_get_checksum(remote), ==, NULL); /* to/from GVariant */ data = fwupd_codec_to_variant(FWUPD_CODEC(remote), FWUPD_CODEC_FLAG_NONE); ret = fwupd_codec_from_variant(FWUPD_CODEC(remote2), data, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_null(fwupd_remote_get_metadata_uri(remote)); /* to JSON */ fwupd_remote_set_filename_source(remote2, NULL); json = fwupd_codec_to_json_string(FWUPD_CODEC(remote2), FWUPD_CODEC_FLAG_NONE, &error); g_assert_no_error(error); g_assert_nonnull(json); ret = g_strcmp0( json, "{\n" " \"Id\" : \"dell-esrt\",\n" " \"Kind\" : \"local\",\n" " \"Title\" : \"Enable UEFI capsule updates on Dell systems\",\n" " \"FilenameCache\" : \"@datadir@/fwupd/remotes.d/dell-esrt/firmware.xml\",\n" " \"Flags\" : 1,\n" " \"Enabled\" : true,\n" " \"ApprovalRequired\" : false,\n" " \"AutomaticReports\" : false,\n" " \"AutomaticSecurityReports\" : false,\n" " \"Priority\" : 0,\n" " \"Mtime\" : 0,\n" " \"RefreshInterval\" : 0\n" "}") == 0; g_assert_true(ret); } static void fu_remote_list_repair_func(void) { FwupdRemote *remote; gboolean ret; g_autoptr(FuRemoteList) remote_list = fu_remote_list_new(); g_autoptr(GError) error = NULL; fu_remote_list_set_lvfs_metadata_format(remote_list, "zst"); ret = fu_remote_list_load(remote_list, FU_REMOTE_LIST_LOAD_FLAG_FIX_METADATA_URI, &error); g_assert_no_error(error); g_assert_true(ret); /* check .gz converted to .zst */ remote = fu_remote_list_get_by_id(remote_list, "legacy-lvfs"); g_assert_nonnull(remote); g_assert_cmpstr(fwupd_remote_get_metadata_uri(remote), ==, "http://localhost/stable.xml.zst"); /* check .xz converted to .zst */ remote = fu_remote_list_get_by_id(remote_list, "legacy-lvfs-xz"); g_assert_nonnull(remote); g_assert_cmpstr(fwupd_remote_get_metadata_uri(remote), ==, "http://localhost/stable.xml.zst"); /* check non-LVFS remote NOT .gz converted to .xz */ remote = fu_remote_list_get_by_id(remote_list, "legacy"); g_assert_nonnull(remote); g_assert_cmpstr(fwupd_remote_get_metadata_uri(remote), ==, "http://localhost/stable.xml.gz"); } static void fu_config_migrate_1_9_func(void) { const gchar *fake_localconf_fn = "/tmp/fwupd-self-test/var/etc/fwupd/fwupd.conf"; const gchar *fake_sysconf_fn = "/tmp/fwupd-self-test/fwupd/fwupd.conf"; gboolean ret; g_autoptr(FuConfig) config = FU_CONFIG(fu_engine_config_new()); g_autoptr(GError) error = NULL; /* ensure empty tree */ fu_self_test_mkroot(); (void)g_unsetenv("CONFIGURATION_DIRECTORY"); (void)g_setenv("FWUPD_SYSCONFDIR", "/tmp/fwupd-self-test", TRUE); ret = fu_path_mkdir_parent(fake_sysconf_fn, &error); g_assert_no_error(error); g_assert_true(ret); ret = g_file_set_contents(fake_sysconf_fn, "# use `man 5 fwupd.conf` for documentation\n" "[fwupd]\n" "DisabledPlugins=test;test_ble\n" "OnlyTrusted=true\n" "AllowEmulation=false\n", -1, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_config_load(config, &error); g_assert_no_error(error); g_assert_true(ret); /* ensure that all keys migrated */ ret = g_file_test(fake_localconf_fn, G_FILE_TEST_EXISTS); g_assert_false(ret); } static void fu_config_set_plugin_defaults(FuConfig *config) { /* these are correct for v2.0.0 */ fu_config_set_default(config, "msr", "MinimumSmeKernelVersion", "5.18.0"); fu_config_set_default(config, "redfish", "CACheck", "false"); fu_config_set_default(config, "redfish", "IpmiDisableCreateUser", "false"); fu_config_set_default(config, "redfish", "ManagerResetTimeout", "1800"); /* seconds */ fu_config_set_default(config, "redfish", "Password", NULL); fu_config_set_default(config, "redfish", "Uri", NULL); fu_config_set_default(config, "redfish", "Username", NULL); fu_config_set_default(config, "redfish", "UserUri", NULL); fu_config_set_default(config, "thunderbolt", "DelayedActivation", "false"); fu_config_set_default(config, "thunderbolt", "MinimumKernelVersion", "4.13.0"); fu_config_set_default(config, "uefi-capsule", "DisableCapsuleUpdateOnDisk", "false"); fu_config_set_default(config, "uefi-capsule", "DisableShimForSecureBoot", "false"); fu_config_set_default(config, "uefi-capsule", "EnableEfiDebugging", "false"); fu_config_set_default(config, "uefi-capsule", "EnableGrubChainLoad", "false"); fu_config_set_default(config, "uefi-capsule", "OverrideESPMountPoint", NULL); fu_config_set_default(config, "uefi-capsule", "RebootCleanup", "true"); fu_config_set_default(config, "uefi-capsule", "RequireESPFreeSpace", "0"); fu_config_set_default(config, "uefi-capsule", "ScreenWidth", "0"); fu_config_set_default(config, "uefi-capsule", "ScreenHeight", "0"); } static void fu_config_migrate_1_7_func(void) { const gchar *sysconfdir = "/tmp/fwupd-self-test/conf-migration-1.7/var/etc"; gboolean ret; const gchar *fn_merge[] = {"daemon.conf", "msr.conf", "redfish.conf", "thunderbolt.conf", "uefi_capsule.conf", NULL}; g_autofree gchar *localconf_data = NULL; g_autofree gchar *fn_mut = NULL; g_autofree gchar *testdatadir = NULL; g_autoptr(FuConfig) config = FU_CONFIG(fu_engine_config_new()); g_autoptr(GError) error = NULL; /* ensure empty tree */ fu_self_test_mkroot(); /* source directory and data */ testdatadir = g_test_build_filename(G_TEST_DIST, "tests", "conf-migration-1.7", NULL); if (!g_file_test(testdatadir, G_FILE_TEST_EXISTS)) { g_test_skip("missing fwupd 1.7.x migration test data"); return; } /* working directory */ (void)g_setenv("FWUPD_SYSCONFDIR", sysconfdir, TRUE); (void)g_unsetenv("CONFIGURATION_DIRECTORY"); fn_mut = g_build_filename(sysconfdir, "fwupd", "fwupd.conf", NULL); g_assert_nonnull(fn_mut); ret = fu_path_mkdir_parent(fn_mut, &error); g_assert_no_error(error); g_assert_true(ret); /* copy all files to working directory */ for (guint i = 0; fn_merge[i] != NULL; i++) { g_autofree gchar *source = g_build_filename(testdatadir, "fwupd", fn_merge[i], NULL); g_autofree gchar *target = g_build_filename(sysconfdir, "fwupd", fn_merge[i], NULL); fu_test_copy_file(source, target); } /* we don't want to run all the plugins just to get the _init() defaults */ fu_config_set_plugin_defaults(config); ret = fu_config_load(config, &error); g_assert_no_error(error); g_assert_true(ret); /* make sure all migrated files were renamed */ for (guint i = 0; fn_merge[i] != NULL; i++) { g_autofree gchar *old = g_build_filename(sysconfdir, "fwupd", fn_merge[i], NULL); g_autofree gchar *new = g_strdup_printf("%s.old", old); ret = g_file_test(old, G_FILE_TEST_EXISTS); g_assert_false(ret); ret = g_file_test(new, G_FILE_TEST_EXISTS); g_assert_true(ret); } /* ensure all default keys migrated */ ret = g_file_get_contents(fn_mut, &localconf_data, NULL, &error); g_assert_no_error(error); g_assert_true(ret); g_assert_cmpstr(localconf_data, ==, ""); } static void fu_engine_machine_hash_func(void) { gsize sz = 0; g_autofree gchar *buf = NULL; g_autofree gchar *mhash1 = NULL; g_autofree gchar *mhash2 = NULL; g_autoptr(GError) error = NULL; if (!g_file_test("/etc/machine-id", G_FILE_TEST_EXISTS)) { g_test_skip("Missing /etc/machine-id"); return; } if (!g_file_get_contents("/etc/machine-id", &buf, &sz, &error)) { g_test_skip("/etc/machine-id is unreadable"); return; } if (sz == 0) { g_test_skip("Empty /etc/machine-id"); return; } mhash1 = fu_engine_build_machine_id("salt1", &error); g_assert_no_error(error); g_assert_cmpstr(mhash1, !=, NULL); mhash2 = fu_engine_build_machine_id("salt2", &error); g_assert_no_error(error); g_assert_cmpstr(mhash2, !=, NULL); g_assert_cmpstr(mhash2, !=, mhash1); } static void fu_test_engine_fake_hidraw(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autofree gchar *value2 = NULL; g_autoptr(FuDevice) device = NULL; g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FuUdevDevice) udev_device2 = NULL; g_autoptr(FuUdevDevice) udev_device3 = NULL; g_autoptr(GError) error = NULL; /* non-linux */ if (!fu_context_has_backend(self->ctx, "udev")) { g_test_skip("no Udev backend"); return; } /* load engine and check the device was found */ fu_engine_add_plugin_filter(engine, "pixart_rf"); ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS | FU_ENGINE_LOAD_FLAG_NO_IDLE_SOURCES | FU_ENGINE_LOAD_FLAG_READONLY, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* hidraw -> pixart_rf */ device = fu_engine_get_device(engine, "6acd27f1feb25ba3b604063de4c13b604776b2f5", &error); g_assert_no_error(error); g_assert_nonnull(device); g_assert_cmpstr(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device)), ==, "hidraw"); g_assert_cmpstr(fu_udev_device_get_devtype(FU_UDEV_DEVICE(device)), ==, NULL); g_assert_cmpstr(fu_udev_device_get_driver(FU_UDEV_DEVICE(device)), ==, NULL); g_assert_cmpint(fu_device_get_vid(device), ==, 0x093a); g_assert_cmpint(fu_device_get_pid(device), ==, 0x2862); g_assert_cmpstr(fu_device_get_plugin(device), ==, "pixart_rf"); g_assert_cmpstr(fu_device_get_name(device), ==, "PIXART Pixart dual-mode mouse"); g_assert_cmpstr(fu_device_get_physical_id(device), ==, "usb-0000:00:14.0-1/input1"); g_assert_cmpstr(fu_device_get_logical_id(device), ==, NULL); /* check can read random files */ value2 = fu_udev_device_read_sysfs(FU_UDEV_DEVICE(device), "dev", FU_UDEV_DEVICE_ATTR_READ_TIMEOUT_DEFAULT, &error); g_assert_no_error(error); g_assert_cmpstr(value2, ==, "241:1"); /* get child, both specified */ udev_device2 = FU_UDEV_DEVICE( fu_device_get_backend_parent_with_subsystem(device, "usb:usb_interface", &error)); g_assert_no_error(error); g_assert_nonnull(udev_device2); g_assert_cmpstr(fu_udev_device_get_subsystem(udev_device2), ==, "usb"); /* get child, initially unprobed */ udev_device3 = FU_UDEV_DEVICE(fu_device_get_backend_parent_with_subsystem(device, "usb", &error)); g_assert_no_error(error); g_assert_nonnull(udev_device3); g_assert_cmpstr(fu_udev_device_get_subsystem(udev_device3), ==, "usb"); g_assert_cmpstr(fu_udev_device_get_driver(udev_device3), ==, "usb"); } static void fu_test_engine_fake_usb(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device = NULL; g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; /* non-linux */ if (!fu_context_has_backend(self->ctx, "udev")) { g_test_skip("no Udev backend"); return; } /* load engine and check the device was found */ fu_engine_add_plugin_filter(engine, "hughski_colorhug"); ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS | FU_ENGINE_LOAD_FLAG_NO_IDLE_SOURCES | FU_ENGINE_LOAD_FLAG_READONLY, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* USB -> colorhug */ device = fu_engine_get_device(engine, "d787669ee4a103fe0b361fe31c10ea037c72f27c", &error); g_assert_no_error(error); g_assert_nonnull(device); g_assert_cmpstr(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device)), ==, "usb"); g_assert_cmpstr(fu_udev_device_get_devtype(FU_UDEV_DEVICE(device)), ==, "usb_device"); g_assert_cmpstr(fu_udev_device_get_driver(FU_UDEV_DEVICE(device)), ==, "usb"); g_assert_cmpint(fu_device_get_vid(device), ==, 0x093a); g_assert_cmpint(fu_device_get_pid(device), ==, 0x2862); g_assert_cmpstr(fu_device_get_plugin(device), ==, "hughski_colorhug"); g_assert_cmpstr(fu_device_get_physical_id(device), ==, "1-1"); g_assert_cmpstr(fu_device_get_logical_id(device), ==, NULL); } static void fu_test_engine_fake_v4l(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device = NULL; g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; /* non-linux */ if (!fu_context_has_backend(self->ctx, "udev")) { g_test_skip("no Udev backend"); return; } /* load engine and check the device was found */ fu_engine_add_plugin_filter(engine, "logitech_tap"); ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS | FU_ENGINE_LOAD_FLAG_NO_IDLE_SOURCES | FU_ENGINE_LOAD_FLAG_READONLY, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* no -Dplugin_logitech_tap=enabled */ if (fu_engine_get_plugin_by_name(engine, "logitech_tap", &error) == NULL) { g_test_skip(error->message); return; } /* v4l -> logitech_tap */ device = fu_engine_get_device(engine, "d787669ee4a103fe0b361fe31c10ea037c72f27c", &error); g_assert_no_error(error); g_assert_nonnull(device); g_assert_cmpstr(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device)), ==, "video4linux"); g_assert_cmpstr(fu_udev_device_get_devtype(FU_UDEV_DEVICE(device)), ==, NULL); g_assert_cmpstr(fu_udev_device_get_driver(FU_UDEV_DEVICE(device)), ==, NULL); g_assert_cmpint(fu_device_get_vid(device), ==, 0x093A); g_assert_cmpint(fu_device_get_pid(device), ==, 0x2862); g_assert_cmpint(fu_v4l_device_get_index(FU_V4L_DEVICE(device)), ==, 0); g_assert_cmpint(fu_v4l_device_get_caps(FU_V4L_DEVICE(device)), ==, FU_V4L_CAP_NONE); g_assert_cmpstr(fu_device_get_name(device), ==, "Integrated Camera: Integrated C"); g_assert_cmpstr(fu_device_get_plugin(device), ==, "logitech_tap"); } static void fu_test_engine_fake_nvme(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device = NULL; g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; /* non-linux */ if (!fu_context_has_backend(self->ctx, "udev")) { g_test_skip("no Udev backend"); return; } /* load engine and check the device was found */ fu_engine_add_plugin_filter(engine, "nvme"); ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS | FU_ENGINE_LOAD_FLAG_NO_IDLE_SOURCES | FU_ENGINE_LOAD_FLAG_READONLY | FU_ENGINE_LOAD_FLAG_NO_CACHE, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* no linux/nvme_ioctl.h */ if (fu_engine_get_plugin_by_name(engine, "nvme", &error) == NULL) { g_test_skip(error->message); return; } /* NVMe -> nvme */ device = fu_engine_get_device(engine, "4c263c95f596030b430d65dc934f6722bcee5720", &error); g_assert_no_error(error); g_assert_nonnull(device); g_assert_cmpstr(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device)), ==, "nvme"); g_assert_cmpstr(fu_udev_device_get_devtype(FU_UDEV_DEVICE(device)), ==, NULL); g_assert_cmpint(fu_udev_device_get_number(FU_UDEV_DEVICE(device)), ==, 1); g_assert_cmpstr(fu_udev_device_get_driver(FU_UDEV_DEVICE(device)), ==, NULL); g_assert_cmpstr(fu_udev_device_get_device_file(FU_UDEV_DEVICE(device)), ==, "/dev/nvme1"); g_assert_cmpint(fu_device_get_vid(device), ==, 0x1179); g_assert_cmpint(fu_device_get_pid(device), ==, 0x010F); g_assert_true(fu_device_has_vendor_id(device, "PCI:0x1179")); g_assert_cmpstr(fu_device_get_vendor(device), ==, NULL); g_assert_cmpstr(fu_device_get_plugin(device), ==, "nvme"); g_assert_cmpstr(fu_device_get_physical_id(device), ==, "PCI_SLOT_NAME=0000:00:1b.0"); g_assert_cmpstr(fu_device_get_logical_id(device), ==, NULL); } static void fu_test_engine_fake_serio(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device = NULL; g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; /* non-linux */ if (!fu_context_has_backend(self->ctx, "udev")) { g_test_skip("no Udev backend"); return; } /* load engine and check the device was found */ fu_engine_add_plugin_filter(engine, "synaptics_rmi"); ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS | FU_ENGINE_LOAD_FLAG_NO_IDLE_SOURCES | FU_ENGINE_LOAD_FLAG_READONLY, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* no gnutls */ if (fu_engine_get_plugin_by_name(engine, "synaptics_rmi", &error) == NULL) { g_test_skip(error->message); return; } /* serio */ device = fu_engine_get_device(engine, "d8419b7614e50c6fb6162b5dca34df5236a62a8d", &error); g_assert_no_error(error); g_assert_nonnull(device); g_assert_cmpstr(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device)), ==, "serio"); g_assert_cmpstr(fu_udev_device_get_devtype(FU_UDEV_DEVICE(device)), ==, NULL); g_assert_cmpstr(fu_udev_device_get_driver(FU_UDEV_DEVICE(device)), ==, "psmouse"); g_assert_cmpstr(fu_udev_device_get_device_file(FU_UDEV_DEVICE(device)), ==, NULL); g_assert_cmpint(fu_device_get_vid(device), ==, 0x0); g_assert_cmpint(fu_device_get_pid(device), ==, 0x0); g_assert_cmpstr(fu_device_get_name(device), ==, "TouchStyk"); g_assert_cmpstr(fu_device_get_plugin(device), ==, "synaptics_rmi"); g_assert_cmpstr(fu_device_get_physical_id(device), ==, "DEVPATH=/devices/platform/i8042/serio1"); g_assert_cmpstr(fu_device_get_logical_id(device), ==, NULL); g_assert_true(fu_device_has_instance_id(device, "SERIO\\FWID_LEN0305-PNP0F13", FU_DEVICE_INSTANCE_FLAG_VISIBLE)); } static void fu_test_engine_fake_tpm(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device = NULL; g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; /* non-linux */ if (!fu_context_has_backend(self->ctx, "udev")) { g_test_skip("no Udev backend"); return; } /* load engine and check the device was found */ fu_engine_add_plugin_filter(engine, "tpm"); ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS | FU_ENGINE_LOAD_FLAG_NO_IDLE_SOURCES | FU_ENGINE_LOAD_FLAG_READONLY, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* no tss2-esys */ if (fu_engine_get_plugin_by_name(engine, "tpm", &error) == NULL) { g_test_skip(error->message); return; } /* tpm */ device = fu_engine_get_device(engine, "1d8d50a4dbc65618f5c399c2ae827b632b3ccc11", &error); g_assert_no_error(error); g_assert_nonnull(device); g_assert_cmpstr(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device)), ==, "tpm"); g_assert_cmpstr(fu_udev_device_get_devtype(FU_UDEV_DEVICE(device)), ==, NULL); g_assert_cmpstr(fu_udev_device_get_driver(FU_UDEV_DEVICE(device)), ==, NULL); g_assert_cmpstr(fu_udev_device_get_device_file(FU_UDEV_DEVICE(device)), ==, "/dev/tpm0"); g_assert_cmpint(fu_device_get_vid(device), ==, 0x0); g_assert_cmpint(fu_device_get_pid(device), ==, 0x0); g_assert_cmpstr(fu_device_get_plugin(device), ==, "tpm"); g_assert_cmpstr(fu_device_get_physical_id(device), ==, "DEVNAME=tpm0"); g_assert_cmpstr(fu_device_get_logical_id(device), ==, NULL); } static void fu_test_engine_fake_block(gconstpointer user_data) { FuTest *self = (FuTest *)user_data; gboolean ret; g_autoptr(FuDevice) device = NULL; g_autoptr(FuEngine) engine = fu_engine_new(self->ctx); g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(GError) error = NULL; /* non-linux */ if (!fu_context_has_backend(self->ctx, "udev")) { g_test_skip("no Udev backend"); return; } /* load engine and check the device was found */ fu_engine_add_plugin_filter(engine, "scsi"); ret = fu_engine_load(engine, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS | FU_ENGINE_LOAD_FLAG_NO_IDLE_SOURCES | FU_ENGINE_LOAD_FLAG_READONLY, progress, &error); g_assert_no_error(error); g_assert_true(ret); /* no Udev */ if (fu_engine_get_plugin_by_name(engine, "scsi", &error) == NULL) { g_test_skip(error->message); return; } /* block */ device = fu_engine_get_device(engine, "7772d9fe9419e3ea564216e12913a16e233378a6", &error); g_assert_no_error(error); g_assert_nonnull(device); g_assert_cmpstr(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device)), ==, "block"); g_assert_cmpstr(fu_udev_device_get_devtype(FU_UDEV_DEVICE(device)), ==, "disk"); g_assert_cmpstr(fu_udev_device_get_driver(FU_UDEV_DEVICE(device)), ==, NULL); g_assert_cmpstr(fu_udev_device_get_device_file(FU_UDEV_DEVICE(device)), ==, "/dev/sde"); g_assert_cmpstr(fu_device_get_plugin(device), ==, "scsi"); g_assert_cmpstr(fu_device_get_vendor(device), ==, "IBM-ESXS"); } int main(int argc, char **argv) { gboolean ret; g_autofree gchar *sysfsdir = NULL; g_autofree gchar *testdatadir = NULL; g_autoptr(GError) error = NULL; g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC); g_autoptr(FuTest) self = g_new0(FuTest, 1); (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); g_test_init(&argc, &argv, NULL); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); (void)g_setenv("G_MESSAGES_DEBUG", "all", TRUE); testdatadir = g_test_build_filename(G_TEST_DIST, "tests", NULL); (void)g_setenv("FWUPD_DATADIR", testdatadir, TRUE); (void)g_setenv("FWUPD_LIBDIR_PKG", testdatadir, TRUE); (void)g_setenv("FWUPD_SYSCONFDIR", testdatadir, TRUE); (void)g_setenv("FWUPD_SYSFSFWDIR", testdatadir, TRUE); (void)g_setenv("CONFIGURATION_DIRECTORY", testdatadir, TRUE); (void)g_setenv("FWUPD_LOCALSTATEDIR", "/tmp/fwupd-self-test/var", TRUE); (void)g_setenv("FWUPD_SYSFSFWATTRIBDIR", testdatadir, TRUE); (void)g_setenv("FWUPD_PROCFS", testdatadir, TRUE); sysfsdir = g_test_build_filename(G_TEST_DIST, "tests", "sys", NULL); (void)g_setenv("FWUPD_SYSFSDIR", sysfsdir, TRUE); (void)g_setenv("FWUPD_SELF_TEST", "1", TRUE); (void)g_setenv("FWUPD_MACHINE_ID", "test", TRUE); (void)g_setenv("FWUPD_EFIVARS", "dummy", TRUE); /* ensure empty tree */ fu_self_test_mkroot(); /* do not save silo */ self->ctx = fu_context_new(); ret = fu_context_load_quirks(self->ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE, &error); g_assert_no_error(error); g_assert_true(ret); /* build a plausible EFI system */ ret = fu_efivars_set_secure_boot(fu_context_get_efivars(self->ctx), TRUE, &error); g_assert_no_error(error); g_assert_true(ret); ret = fu_efivars_set_boot_current(fu_context_get_efivars(self->ctx), 0x0000, &error); g_assert_no_error(error); g_assert_true(ret); /* do not allow mounting disks in the self tests */ fu_context_add_flag(self->ctx, FU_CONTEXT_FLAG_INHIBIT_VOLUME_MOUNT); /* load dummy hwids */ ret = fu_context_load_hwinfo(self->ctx, progress, FU_CONTEXT_HWID_FLAG_LOAD_CONFIG, &error); g_assert_no_error(error); g_assert_true(ret); /* tests go here */ if (g_test_slow()) { g_test_add_data_func("/fwupd/console", self, fu_console_func); } g_test_add_func("/fwupd/idle", fu_idle_func); g_test_add_func("/fwupd/client-list", fu_client_list_func); g_test_add_func("/fwupd/remote{download}", fu_remote_download_func); g_test_add_func("/fwupd/remote{base-uri}", fu_remote_baseuri_func); g_test_add_func("/fwupd/remote{no-path}", fu_remote_nopath_func); g_test_add_func("/fwupd/remote{local}", fu_remote_local_func); g_test_add_func("/fwupd/remote{duplicate}", fu_remote_duplicate_func); g_test_add_func("/fwupd/remote{auth}", fu_remote_auth_func); g_test_add_func("/fwupd/remote-list{repair}", fu_remote_list_repair_func); g_test_add_func("/fwupd/unix-seekable-input-stream", fu_unix_seekable_input_stream_func); g_test_add_data_func("/fwupd/backend{usb}", self, fu_backend_usb_func); g_test_add_data_func("/fwupd/backend{usb-invalid}", self, fu_backend_usb_invalid_func); g_test_add_data_func("/fwupd/plugin{module}", self, fu_plugin_module_func); g_test_add_data_func("/fwupd/memcpy", self, fu_memcpy_func); g_test_add_func("/fwupd/cabinet", fu_common_cabinet_func); g_test_add_data_func("/fwupd/security-attr", self, fu_security_attr_func); g_test_add_data_func("/fwupd/device-list", self, fu_device_list_func); g_test_add_data_func("/fwupd/device-list{unconnected-no-delay}", self, fu_device_list_unconnected_no_delay_func); g_test_add_data_func("/fwupd/device-list{equivalent-id}", self, fu_device_list_equivalent_id_func); g_test_add_data_func("/fwupd/device-list{delay}", self, fu_device_list_delay_func); g_test_add_data_func("/fwupd/device-list{explicit-order}", self, fu_device_list_explicit_order_func); g_test_add_data_func("/fwupd/device-list{explicit-order-post}", self, fu_device_list_explicit_order_post_func); g_test_add_data_func("/fwupd/device-list{no-auto-remove-children}", self, fu_device_list_no_auto_remove_children_func); g_test_add_data_func("/fwupd/device-list{compatible}", self, fu_device_list_compatible_func); g_test_add_data_func("/fwupd/device-list{remove-chain}", self, fu_device_list_remove_chain_func); g_test_add_data_func("/fwupd/device-list{counterpart}", self, fu_device_list_counterpart_func); g_test_add_data_func("/fwupd/device-list{better-than}", self, fu_device_list_better_than_func); g_test_add_data_func("/fwupd/release{compare}", self, fu_release_compare_func); g_test_add_func("/fwupd/release{uri-scheme}", fu_release_uri_scheme_func); g_test_add_data_func("/fwupd/release{trusted-report}", self, fu_release_trusted_report_func); g_test_add_data_func("/fwupd/release{trusted-report-oem}", self, fu_release_trusted_report_oem_func); g_test_add_data_func("/fwupd/release{no-trusted-report-upgrade}", self, fu_release_no_trusted_report_upgrade_func); g_test_add_data_func("/fwupd/release{no-trusted-report}", self, fu_release_no_trusted_report_func); g_test_add_data_func("/fwupd/engine{get-details-added}", self, fu_engine_get_details_added_func); g_test_add_data_func("/fwupd/engine{get-details-missing}", self, fu_engine_get_details_missing_func); g_test_add_data_func("/fwupd/engine{device-unlock}", self, fu_engine_device_unlock_func); g_test_add_data_func("/fwupd/engine{device-equivalent}", self, fu_engine_device_equivalent_func); g_test_add_data_func("/fwupd/engine{device-md-set-flags}", self, fu_engine_device_md_set_flags_func); g_test_add_data_func("/fwupd/engine{device-md-checksum-set-version}", self, fu_engine_device_md_checksum_set_version_func); g_test_add_data_func("/fwupd/engine{device-md-checksum-set-version-wrong-proto}", self, fu_engine_device_md_checksum_set_version_wrong_proto_func); g_test_add_data_func("/fwupd/engine{multiple-releases}", self, fu_engine_multiple_rels_func); g_test_add_data_func("/fwupd/engine{install-loop-restart}", self, fu_engine_install_loop_restart_func); g_test_add_data_func("/fwupd/engine{install-request}", self, fu_engine_install_request); g_test_add_data_func("/fwupd/engine{history-success}", self, fu_engine_history_func); g_test_add_data_func("/fwupd/engine{history-verfmt}", self, fu_engine_history_verfmt_func); g_test_add_data_func("/fwupd/engine{history-modify}", self, fu_engine_history_modify_func); g_test_add_data_func("/fwupd/engine{history-error}", self, fu_engine_history_error_func); g_test_add_data_func("/fwupd/engine{fake-hidraw}", self, fu_test_engine_fake_hidraw); g_test_add_data_func("/fwupd/engine{fake-usb}", self, fu_test_engine_fake_usb); g_test_add_data_func("/fwupd/engine{fake-serio}", self, fu_test_engine_fake_serio); g_test_add_data_func("/fwupd/engine{fake-nvme}", self, fu_test_engine_fake_nvme); g_test_add_data_func("/fwupd/engine{fake-block}", self, fu_test_engine_fake_block); g_test_add_data_func("/fwupd/engine{fake-tpm}", self, fu_test_engine_fake_tpm); g_test_add_data_func("/fwupd/engine{fake-v4l}", self, fu_test_engine_fake_v4l); if (g_test_slow()) { g_test_add_data_func("/fwupd/device-list{replug-auto}", self, fu_device_list_replug_auto_func); } g_test_add_data_func("/fwupd/device-list{replug-user}", self, fu_device_list_replug_user_func); g_test_add_func("/fwupd/engine{machine-hash}", fu_engine_machine_hash_func); g_test_add_data_func("/fwupd/engine{require-hwid}", self, fu_engine_require_hwid_func); g_test_add_data_func("/fwupd/engine{requires-reboot}", self, fu_engine_install_needs_reboot); g_test_add_data_func("/fwupd/engine{history-inherit}", self, fu_engine_history_inherit); g_test_add_data_func("/fwupd/engine{partial-hash}", self, fu_engine_partial_hash_func); g_test_add_data_func("/fwupd/engine{downgrade}", self, fu_engine_downgrade_func); g_test_add_data_func("/fwupd/engine{md-verfmt}", self, fu_engine_md_verfmt_func); g_test_add_data_func("/fwupd/engine{requirements-success}", self, fu_engine_requirements_func); g_test_add_data_func("/fwupd/engine{requirements-soft}", self, fu_engine_requirements_soft_func); g_test_add_data_func("/fwupd/engine{requirements-missing}", self, fu_engine_requirements_missing_func); g_test_add_data_func("/fwupd/engine{requirements-client-fail}", self, fu_engine_requirements_client_fail_func); g_test_add_data_func("/fwupd/engine{requirements-client-invalid}", self, fu_engine_requirements_client_invalid_func); g_test_add_data_func("/fwupd/engine{requirements-client-pass}", self, fu_engine_requirements_client_pass_func); g_test_add_data_func("/fwupd/engine{requirements-not-hardware}", self, fu_engine_requirements_not_hardware_func); g_test_add_data_func("/fwupd/engine{requirements-version-require}", self, fu_engine_requirements_version_require_func); g_test_add_data_func("/fwupd/engine{requirements-version-lowest}", self, fu_engine_requirements_version_lowest_func); g_test_add_data_func("/fwupd/engine{requirements-parent-device}", self, fu_engine_requirements_parent_device_func); g_test_add_data_func("/fwupd/engine{requirements-child-device}", self, fu_engine_requirements_child_device_func); g_test_add_data_func("/fwupd/engine{requirements_protocol_check_func}", self, fu_engine_requirements_protocol_check_func); g_test_add_data_func("/fwupd/engine{requirements-not-child}", self, fu_engine_requirements_child_func); g_test_add_data_func("/fwupd/engine{requirements-not-child-fail}", self, fu_engine_requirements_child_fail_func); g_test_add_data_func("/fwupd/engine{requirements-unsupported}", self, fu_engine_requirements_unsupported_func); g_test_add_data_func("/fwupd/engine{requirements-device}", self, fu_engine_requirements_device_func); g_test_add_data_func("/fwupd/engine{requirements-device-plain}", self, fu_engine_requirements_device_plain_func); g_test_add_data_func("/fwupd/engine{requirements-version-format}", self, fu_engine_requirements_version_format_func); g_test_add_data_func("/fwupd/engine{requirements-only-upgrade}", self, fu_engine_requirements_only_upgrade_func); g_test_add_data_func("/fwupd/engine{device-auto-parent-id}", self, fu_engine_device_parent_id_func); g_test_add_data_func("/fwupd/engine{device-auto-parent-guid}", self, fu_engine_device_parent_guid_func); g_test_add_data_func("/fwupd/engine{install-duration}", self, fu_engine_install_duration_func); g_test_add_data_func("/fwupd/engine{release-dedupe}", self, fu_engine_release_dedupe_func); g_test_add_data_func("/fwupd/engine{generate-md}", self, fu_engine_generate_md_func); g_test_add_data_func("/fwupd/engine{requirements-other-device}", self, fu_engine_requirements_other_device_func); g_test_add_data_func("/fwupd/engine{fu_engine_requirements_sibling_device_func}", self, fu_engine_requirements_sibling_device_func); g_test_add_data_func("/fwupd/engine{plugin-gtypes}", self, fu_engine_plugin_gtypes_func); g_test_add_data_func("/fwupd/plugin{composite}", self, fu_plugin_composite_func); g_test_add_data_func("/fwupd/plugin{composite-multistep}", self, fu_plugin_composite_multistep_func); g_test_add_data_func("/fwupd/history", self, fu_history_func); g_test_add_data_func("/fwupd/history{migrate-v1}", self, fu_history_migrate_v1_func); g_test_add_data_func("/fwupd/history{migrate-v2}", self, fu_history_migrate_v2_func); g_test_add_data_func("/fwupd/plugin-list", self, fu_plugin_list_func); g_test_add_data_func("/fwupd/plugin-list{depsolve}", self, fu_plugin_list_depsolve_func); g_test_add_func("/fwupd/common{cab-success}", fu_common_store_cab_func); g_test_add_func("/fwupd/common{cab-success-artifact}", fu_common_store_cab_artifact_func); g_test_add_func("/fwupd/common{cab-success-unsigned}", fu_common_store_cab_unsigned_func); g_test_add_func("/fwupd/common{cab-success-folder}", fu_common_store_cab_folder_func); g_test_add_func("/fwupd/common{cab-success-sha256}", fu_common_store_cab_sha256_func); g_test_add_func("/fwupd/common{cab-error-no-metadata}", fu_common_store_cab_error_no_metadata_func); g_test_add_func("/fwupd/common{cab-error-wrong-size}", fu_common_store_cab_error_wrong_size_func); g_test_add_func("/fwupd/common{cab-error-wrong-checksum}", fu_common_store_cab_error_wrong_checksum_func); g_test_add_func("/fwupd/common{cab-error-missing-file}", fu_common_store_cab_error_missing_file_func); g_test_add_func("/fwupd/common{cab-error-size}", fu_common_store_cab_error_size_func); g_test_add_data_func("/fwupd/write-bios-attrs", self, fu_engine_modify_bios_settings_func); /* these need to be last as they overwrite stuff in the mkroot */ g_test_add_func("/fwupd/config_migrate_1_7", fu_config_migrate_1_7_func); g_test_add_func("/fwupd/config_migrate_1_9", fu_config_migrate_1_9_func); return g_test_run(); } fwupd-2.0.10/src/fu-systemd.c000066400000000000000000000056721501337203100157570ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include #include "fu-systemd.h" #define SYSTEMD_SERVICE "org.freedesktop.systemd1" #define SYSTEMD_OBJECT_PATH "/org/freedesktop/systemd1" #define SYSTEMD_INTERFACE "org.freedesktop.systemd1" #define SYSTEMD_MANAGER_INTERFACE "org.freedesktop.systemd1.Manager" #define SYSTEMD_UNIT_INTERFACE "org.freedesktop.systemd1.Unit" static GDBusProxy * fu_systemd_get_manager(GError **error) { g_autoptr(GDBusConnection) connection = NULL; g_autoptr(GDBusProxy) proxy = NULL; connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, error); if (connection == NULL) { g_prefix_error(error, "failed to get bus: "); return NULL; } proxy = g_dbus_proxy_new_sync(connection, G_DBUS_PROXY_FLAGS_NONE, NULL, SYSTEMD_SERVICE, SYSTEMD_OBJECT_PATH, SYSTEMD_MANAGER_INTERFACE, NULL, error); if (proxy == NULL) { g_prefix_error(error, "failed to find %s: ", SYSTEMD_SERVICE); return NULL; } return g_steal_pointer(&proxy); } static gchar * fu_systemd_unit_get_path(GDBusProxy *proxy_manager, const gchar *unit, GError **error) { g_autofree gchar *path = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_sync(proxy_manager, "GetUnit", g_variant_new("(s)", unit), G_DBUS_CALL_FLAGS_NONE, -1, NULL, error); if (val == NULL) { g_prefix_error(error, "failed to find %s: ", unit); return NULL; } g_variant_get(val, "(o)", &path); return g_steal_pointer(&path); } static GDBusProxy * fu_systemd_unit_get_proxy(GDBusProxy *proxy_manager, const gchar *unit, GError **error) { g_autofree gchar *path = NULL; g_autoptr(GDBusProxy) proxy_unit = NULL; path = fu_systemd_unit_get_path(proxy_manager, unit, error); if (path == NULL) return NULL; proxy_unit = g_dbus_proxy_new_sync(g_dbus_proxy_get_connection(proxy_manager), G_DBUS_PROXY_FLAGS_NONE, NULL, SYSTEMD_SERVICE, path, SYSTEMD_UNIT_INTERFACE, NULL, error); if (proxy_unit == NULL) { g_prefix_error(error, "failed to register proxy for %s: ", path); return NULL; } return g_steal_pointer(&proxy_unit); } gboolean fu_systemd_unit_stop(const gchar *unit, GError **error) { g_autoptr(GDBusProxy) proxy_manager = NULL; g_autoptr(GDBusProxy) proxy_unit = NULL; g_autoptr(GVariant) val = NULL; g_return_val_if_fail(unit != NULL, FALSE); proxy_manager = fu_systemd_get_manager(error); if (proxy_manager == NULL) return FALSE; proxy_unit = fu_systemd_unit_get_proxy(proxy_manager, unit, error); if (proxy_unit == NULL) return FALSE; val = g_dbus_proxy_call_sync(proxy_unit, "Stop", g_variant_new("(s)", "replace"), G_DBUS_CALL_FLAGS_NONE, -1, NULL, error); return val != NULL; } fwupd-2.0.10/src/fu-systemd.h000066400000000000000000000003471501337203100157560ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include gboolean fu_systemd_unit_stop(const gchar *unit, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/src/fu-tool.c000066400000000000000000005367711501337203100152550ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuMain" #include "config.h" #include #include #include #ifdef HAVE_GIO_UNIX #include #endif #include #include #include #include #include #include "fwupd-client-private.h" #include "fwupd-common-private.h" #include "fwupd-device-private.h" #include "fwupd-enums-private.h" #include "fwupd-remote-private.h" #include "fu-bios-settings-private.h" #include "fu-cabinet.h" #include "fu-console.h" #include "fu-context-private.h" #include "fu-debug.h" #include "fu-device-private.h" #include "fu-engine-helper.h" #include "fu-engine-requirements.h" #include "fu-engine.h" #include "fu-history.h" #include "fu-plugin-private.h" #include "fu-security-attr-common.h" #include "fu-security-attrs-private.h" #include "fu-smbios-private.h" #include "fu-util-bios-setting.h" #include "fu-util-common.h" #ifdef HAVE_SYSTEMD #include "fu-systemd.h" #define SYSTEMD_FWUPD_UNIT "fwupd.service" #define SYSTEMD_SNAP_FWUPD_UNIT "snap.fwupd.fwupd.service" #endif typedef enum { FU_UTIL_OPERATION_UNKNOWN, FU_UTIL_OPERATION_UPDATE, FU_UTIL_OPERATION_INSTALL, FU_UTIL_OPERATION_READ, FU_UTIL_OPERATION_LAST } FuUtilOperation; struct FuUtilPrivate { GCancellable *cancellable; GMainContext *main_ctx; GMainLoop *loop; GOptionContext *context; FuContext *ctx; FuEngine *engine; FuEngineRequest *request; FuProgress *progress; FuConsole *console; FwupdClient *client; gboolean as_json; gboolean no_reboot_check; gboolean no_safety_check; gboolean no_device_prompt; gboolean prepare_blob; gboolean cleanup_blob; gboolean enable_json_state; gboolean interactive; FwupdInstallFlags flags; FuFirmwareParseFlags parse_flags; gboolean show_all; gboolean disable_ssl_strict; gint lock_fd; /* only valid in update and downgrade */ FuUtilOperation current_operation; FwupdDevice *current_device; GPtrArray *post_requests; FwupdDeviceFlags completion_flags; FwupdDeviceFlags filter_device_include; FwupdDeviceFlags filter_device_exclude; FwupdReleaseFlags filter_release_include; FwupdReleaseFlags filter_release_exclude; }; static void fu_util_client_notify_cb(GObject *object, GParamSpec *pspec, FuUtilPrivate *priv) { if (priv->as_json) return; fu_console_set_progress(priv->console, fwupd_client_get_status(priv->client), fwupd_client_get_percentage(priv->client)); } static void fu_util_show_plugin_warnings(FuUtilPrivate *priv) { FwupdPluginFlags flags = FWUPD_PLUGIN_FLAG_NONE; GPtrArray *plugins; if (priv->as_json) return; /* get a superset so we do not show the same message more than once */ plugins = fu_engine_get_plugins(priv->engine); for (guint i = 0; i < plugins->len; i++) { FwupdPlugin *plugin = g_ptr_array_index(plugins, i); if (fwupd_plugin_has_flag(plugin, FWUPD_PLUGIN_FLAG_DISABLED)) continue; if (!fwupd_plugin_has_flag(plugin, FWUPD_PLUGIN_FLAG_USER_WARNING)) continue; flags |= fwupd_plugin_get_flags(plugin); } /* never show these, they're way too generic */ flags &= ~FWUPD_PLUGIN_FLAG_DISABLED; flags &= ~FWUPD_PLUGIN_FLAG_NO_HARDWARE; flags &= ~FWUPD_PLUGIN_FLAG_REQUIRE_HWID; flags &= ~FWUPD_PLUGIN_FLAG_MEASURE_SYSTEM_INTEGRITY; flags &= ~FWUPD_PLUGIN_FLAG_READY; /* print */ for (guint i = 0; i < 64; i++) { FwupdPluginFlags flag = (guint64)1 << i; g_autofree gchar *tmp = NULL; g_autofree gchar *url = NULL; if ((flags & flag) == 0) continue; tmp = fu_util_plugin_flag_to_string((guint64)1 << i); if (tmp == NULL) continue; fu_console_print_full(priv->console, FU_CONSOLE_PRINT_FLAG_WARNING, "%s\n", tmp); url = g_strdup_printf("https://github.com/fwupd/fwupd/wiki/PluginFlag:%s", fwupd_plugin_flag_to_string(flag)); /* TRANSLATORS: %s is a link to a website */ fu_console_print(priv->console, _("See %s for more information."), url); } } static gboolean fu_util_lock(FuUtilPrivate *priv, GError **error) { #ifdef HAVE_WRLCK struct flock lockp = { .l_type = F_WRLCK, .l_whence = SEEK_SET, }; g_autofree gchar *lockfn = NULL; gboolean use_user = FALSE; #ifdef HAVE_GETUID if (getuid() != 0 || geteuid() != 0) use_user = TRUE; #endif /* open file */ if (use_user) { lockfn = fu_util_get_user_cache_path("fwupdtool"); } else { g_autofree gchar *lockdir = fu_path_from_kind(FU_PATH_KIND_LOCKDIR); lockfn = g_build_filename(lockdir, "fwupdtool", NULL); } if (!fu_path_mkdir_parent(lockfn, error)) return FALSE; priv->lock_fd = g_open(lockfn, O_RDWR | O_CREAT, S_IRWXU); if (priv->lock_fd < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to open %s", lockfn); return FALSE; } /* write lock */ #ifdef HAVE_OFD if (fcntl(priv->lock_fd, F_OFD_SETLK, &lockp) < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "another instance has locked %s", lockfn); return FALSE; } #else if (fcntl(priv->lock_fd, F_SETLK, &lockp) < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "another instance has locked %s", lockfn); return FALSE; } #endif /* success */ g_debug("locked %s", lockfn); #endif return TRUE; } #ifdef HAVE_SYSTEMD static const gchar * fu_util_get_systemd_unit(void) { if (g_strcmp0(g_getenv("SNAP_NAME"), "fwupd") == 0) return SYSTEMD_SNAP_FWUPD_UNIT; return SYSTEMD_FWUPD_UNIT; } #endif static gboolean fu_util_start_engine(FuUtilPrivate *priv, FuEngineLoadFlags flags, FuProgress *progress, GError **error) { /* already done */ if (fu_engine_get_loaded(priv->engine)) return TRUE; if (!fu_util_lock(priv, error)) { /* TRANSLATORS: another fwupdtool instance is already running */ g_prefix_error(error, "%s: ", _("Failed to lock")); return FALSE; } #ifdef HAVE_SYSTEMD if (getuid() != 0 || geteuid() != 0) { g_info("not attempting to stop daemon when running as user"); } else { g_autoptr(GError) error_local = NULL; if (!fu_systemd_unit_stop(fu_util_get_systemd_unit(), &error_local)) g_info("failed to stop daemon: %s", error_local->message); } #endif flags |= FU_ENGINE_LOAD_FLAG_NO_IDLE_SOURCES; flags |= FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS; flags |= FU_ENGINE_LOAD_FLAG_EXTERNAL_PLUGINS; if (!fu_engine_load(priv->engine, flags, progress, error)) return FALSE; if (!priv->as_json) { fu_util_show_plugin_warnings(priv); fu_util_show_unsupported_warning(priv->console); } /* copy properties from engine to client */ if (flags & FU_ENGINE_LOAD_FLAG_HWINFO) { g_object_set(priv->client, "host-vendor", fu_engine_get_host_vendor(priv->engine), "host-product", fu_engine_get_host_product(priv->engine), "battery-level", fu_context_get_battery_level(fu_engine_get_context(priv->engine)), "battery-threshold", fu_context_get_battery_threshold(fu_engine_get_context(priv->engine)), NULL); } /* success */ return TRUE; } static void fu_util_maybe_prefix_sandbox_error(const gchar *value, GError **error) { g_autofree gchar *path = g_path_get_dirname(value); if (!g_file_test(path, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR)) { g_prefix_error(error, "Unable to access %s. You may need to copy %s to %s: ", path, value, g_getenv("HOME")); } } static void fu_util_cancelled_cb(GCancellable *cancellable, gpointer user_data) { FuUtilPrivate *priv = (FuUtilPrivate *)user_data; /* TRANSLATORS: this is when a device ctrl+c's a watch */ fu_console_print_literal(priv->console, _("Cancelled")); g_main_loop_quit(priv->loop); } static gboolean fu_util_smbios_dump(FuUtilPrivate *priv, gchar **values, GError **error) { g_autofree gchar *tmp = NULL; g_autoptr(FuSmbios) smbios = NULL; if (g_strv_length(values) < 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } smbios = fu_smbios_new(); if (!fu_smbios_setup_from_file(smbios, values[0], error)) return FALSE; tmp = fu_firmware_to_string(FU_FIRMWARE(smbios)); fu_console_print_literal(priv->console, tmp); return TRUE; } #ifdef HAVE_GIO_UNIX static gboolean fu_util_sigint_cb(gpointer user_data) { FuUtilPrivate *priv = (FuUtilPrivate *)user_data; g_info("handling SIGINT"); g_cancellable_cancel(priv->cancellable); return FALSE; } #endif static void fu_util_setup_signal_handlers(FuUtilPrivate *priv) { #ifdef HAVE_GIO_UNIX g_autoptr(GSource) source = g_unix_signal_source_new(SIGINT); g_source_set_callback(source, fu_util_sigint_cb, priv, NULL); g_source_attach(g_steal_pointer(&source), priv->main_ctx); #endif } static void fu_util_private_free(FuUtilPrivate *priv) { if (priv->current_device != NULL) g_object_unref(priv->current_device); if (priv->ctx != NULL) g_object_unref(priv->ctx); if (priv->engine != NULL) g_object_unref(priv->engine); if (priv->request != NULL) g_object_unref(priv->request); if (priv->client != NULL) g_object_unref(priv->client); if (priv->main_ctx != NULL) g_main_context_unref(priv->main_ctx); if (priv->loop != NULL) g_main_loop_unref(priv->loop); if (priv->cancellable != NULL) g_object_unref(priv->cancellable); if (priv->console != NULL) g_object_unref(priv->console); if (priv->progress != NULL) g_object_unref(priv->progress); if (priv->context != NULL) g_option_context_free(priv->context); if (priv->lock_fd >= 0) g_close(priv->lock_fd, NULL); g_ptr_array_unref(priv->post_requests); g_free(priv); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuUtilPrivate, fu_util_private_free) #pragma clang diagnostic pop static void fu_util_update_device_request_cb(FwupdClient *client, FwupdRequest *request, FuUtilPrivate *priv) { /* action has not been assigned yet */ if (priv->current_operation == FU_UTIL_OPERATION_UNKNOWN) return; /* nothing sensible to show */ if (fwupd_request_get_message(request) == NULL) return; /* show this now */ if (fwupd_request_get_kind(request) == FWUPD_REQUEST_KIND_IMMEDIATE) { g_autofree gchar *fmt = NULL; g_autofree gchar *tmp = NULL; /* TRANSLATORS: the user needs to do something, e.g. remove the device */ fmt = fu_console_color_format(_("Action Required:"), FU_CONSOLE_COLOR_RED); tmp = g_strdup_printf("%s %s", fmt, fwupd_request_get_message(request)); fu_console_set_progress_title(priv->console, tmp); fu_console_beep(priv->console, 5); } /* save for later */ if (fwupd_request_get_kind(request) == FWUPD_REQUEST_KIND_POST) g_ptr_array_add(priv->post_requests, g_object_ref(request)); } static void fu_util_engine_device_added_cb(FuEngine *engine, FuDevice *device, FuUtilPrivate *priv) { if (g_getenv("FWUPD_VERBOSE") != NULL) { g_autofree gchar *tmp = fu_device_to_string(device); g_debug("ADDED:\n%s", tmp); } } static void fu_util_engine_device_removed_cb(FuEngine *engine, FuDevice *device, FuUtilPrivate *priv) { if (g_getenv("FWUPD_VERBOSE") != NULL) { g_autofree gchar *tmp = fu_device_to_string(device); g_debug("REMOVED:\n%s", tmp); } } static void fu_util_engine_status_changed_cb(FuEngine *engine, FwupdStatus status, FuUtilPrivate *priv) { if (priv->as_json) return; fu_console_set_progress(priv->console, status, 0); } static void fu_util_progress_percentage_changed_cb(FuProgress *progress, guint percentage, FuUtilPrivate *priv) { if (priv->as_json) return; fu_console_set_progress(priv->console, fu_progress_get_status(progress), percentage); } static void fu_util_progress_status_changed_cb(FuProgress *progress, FwupdStatus status, FuUtilPrivate *priv) { if (priv->as_json) return; fu_console_set_progress(priv->console, status, fu_progress_get_percentage(progress)); } static gboolean fu_util_watch(FuUtilPrivate *priv, gchar **values, GError **error) { if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_DEVICE_HOTPLUG, priv->progress, error)) return FALSE; g_main_loop_run(priv->loop); return TRUE; } static gint fu_util_verfmt_sort_cb(gconstpointer a, gconstpointer b) { return g_strcmp0(*(const gchar **)a, *(const gchar **)b); } static gboolean fu_util_get_verfmts(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(GPtrArray) verfmts = g_ptr_array_new_with_free_func((GDestroyNotify)g_free); for (guint i = FWUPD_VERSION_FORMAT_PLAIN; i < FWUPD_VERSION_FORMAT_LAST; i++) { g_autofree gchar *format = g_strdup(fwupd_version_format_to_string(i)); if (format == NULL) continue; g_ptr_array_add(verfmts, g_steal_pointer(&format)); } g_ptr_array_sort(verfmts, (GCompareFunc)fu_util_verfmt_sort_cb); /* print */ if (priv->as_json) { g_autoptr(JsonBuilder) builder = json_builder_new(); json_builder_begin_array(builder); for (guint i = 0; i < verfmts->len; i++) { const gchar *verfmt = g_ptr_array_index(verfmts, i); json_builder_add_string_value(builder, verfmt); } json_builder_end_array(builder); return fu_util_print_builder(priv->console, builder, error); } /* print */ for (guint i = 0; i < verfmts->len; i++) { const gchar *verfmt = g_ptr_array_index(verfmts, i); fu_console_print_literal(priv->console, verfmt); } return TRUE; } static gboolean fu_util_get_plugins(FuUtilPrivate *priv, gchar **values, GError **error) { GPtrArray *plugins; /* load engine */ if (!fu_util_start_engine( priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_EXTERNAL_PLUGINS | FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS | FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; /* print */ plugins = fu_engine_get_plugins(priv->engine); g_ptr_array_sort(plugins, (GCompareFunc)fu_util_plugin_name_sort_cb); if (priv->as_json) { g_autoptr(JsonBuilder) builder = json_builder_new(); json_builder_begin_object(builder); fwupd_codec_array_to_json(plugins, "Plugins", builder, FWUPD_CODEC_FLAG_TRUSTED); json_builder_end_object(builder); return fu_util_print_builder(priv->console, builder, error); } /* print */ for (guint i = 0; i < plugins->len; i++) { FuPlugin *plugin = g_ptr_array_index(plugins, i); g_autofree gchar *str = fu_util_plugin_to_string(FWUPD_PLUGIN(plugin), 0); fu_console_print_literal(priv->console, str); } return TRUE; } static FuDevice * fu_util_prompt_for_device(FuUtilPrivate *priv, GPtrArray *devices_opt, GError **error) { FuDevice *dev; guint idx; g_autoptr(GPtrArray) devices = NULL; g_autoptr(GPtrArray) devices_filtered = NULL; /* get devices from daemon */ if (devices_opt != NULL) { devices = g_ptr_array_ref(devices_opt); } else { devices = fu_engine_get_devices(priv->engine, error); if (devices == NULL) return NULL; } fwupd_device_array_ensure_parents(devices); /* filter results */ devices_filtered = fwupd_device_array_filter_flags(devices, priv->filter_device_include, priv->filter_device_exclude, error); if (devices_filtered == NULL) return NULL; /* exactly one */ if (devices_filtered->len == 1) { dev = g_ptr_array_index(devices_filtered, 0); if (!priv->as_json) { fu_console_print( priv->console, "%s: %s", /* TRANSLATORS: device has been chosen by the daemon for the user */ _("Selected device"), fu_device_get_name(dev)); } return g_object_ref(dev); } /* no questions */ if (priv->no_device_prompt) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "can't prompt for devices"); return NULL; } /* TRANSLATORS: this is to abort the interactive prompt */ fu_console_print(priv->console, "0.\t%s", _("Cancel")); for (guint i = 0; i < devices_filtered->len; i++) { dev = g_ptr_array_index(devices_filtered, i); fu_console_print(priv->console, "%u.\t%s (%s)", i + 1, fu_device_get_id(dev), fu_device_get_name(dev)); } /* TRANSLATORS: get interactive prompt */ idx = fu_console_input_uint(priv->console, devices_filtered->len, "%s", _("Choose device")); if (idx == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "Request canceled"); return NULL; } dev = g_ptr_array_index(devices_filtered, idx - 1); return g_object_ref(dev); } static FuDevice * fu_util_get_device(FuUtilPrivate *priv, const gchar *id, GError **error) { if (fwupd_guid_is_valid(id)) { g_autoptr(GPtrArray) devices = NULL; devices = fu_engine_get_devices_by_guid(priv->engine, id, error); if (devices == NULL) return NULL; return fu_util_prompt_for_device(priv, devices, error); } /* did this look like a GUID? */ for (guint i = 0; id[i] != '\0'; i++) { if (id[i] == '-') { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return NULL; } } return fu_engine_get_device(priv->engine, id, error); } static gboolean fu_util_get_updates_as_json(FuUtilPrivate *priv, GPtrArray *devices, GError **error) { g_autoptr(JsonBuilder) builder = json_builder_new(); json_builder_begin_object(builder); json_builder_set_member_name(builder, "Devices"); json_builder_begin_array(builder); for (guint i = 0; i < devices->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices, i); g_autoptr(GPtrArray) rels = NULL; g_autoptr(GError) error_local = NULL; if (!fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_SUPPORTED)) continue; /* get the releases for this device and filter for validity */ rels = fu_engine_get_upgrades(priv->engine, priv->request, fwupd_device_get_id(dev), &error_local); if (rels == NULL) { g_debug("no upgrades: %s", error_local->message); continue; } for (guint j = 0; j < rels->len; j++) { FwupdRelease *rel = g_ptr_array_index(rels, j); if (!fwupd_release_match_flags(rel, priv->filter_release_include, priv->filter_release_exclude)) continue; fwupd_device_add_release(dev, rel); } /* add to builder */ json_builder_begin_object(builder); fwupd_codec_to_json(FWUPD_CODEC(dev), builder, FWUPD_CODEC_FLAG_TRUSTED); json_builder_end_object(builder); } json_builder_end_array(builder); json_builder_end_object(builder); return fu_util_print_builder(priv->console, builder, error); } static gboolean fu_util_get_updates(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(GPtrArray) devices = NULL; g_autoptr(FuUtilNode) root = g_node_new(NULL); g_autoptr(GPtrArray) devices_no_support = g_ptr_array_new(); g_autoptr(GPtrArray) devices_no_upgrades = g_ptr_array_new(); /* load engine */ if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; /* parse arguments */ if (g_strv_length(values) == 0) { devices = fu_engine_get_devices(priv->engine, error); if (devices == NULL) return FALSE; } else if (g_strv_length(values) == 1) { FuDevice *device; device = fu_util_get_device(priv, values[0], error); if (device == NULL) return FALSE; devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_ptr_array_add(devices, device); } else { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } /* not for human consumption */ if (priv->as_json) return fu_util_get_updates_as_json(priv, devices, error); fwupd_device_array_ensure_parents(devices); g_ptr_array_sort(devices, fu_util_sort_devices_by_flags_cb); for (guint i = 0; i < devices->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices, i); g_autoptr(GPtrArray) rels = NULL; g_autoptr(GError) error_local = NULL; FuUtilNode *child; /* not going to have results, so save a engine round-trip */ if (!fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE) && !fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN)) continue; if (!fwupd_device_match_flags(dev, priv->filter_device_include, priv->filter_device_exclude)) continue; if (!fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_SUPPORTED)) { g_ptr_array_add(devices_no_support, dev); continue; } /* get the releases for this device and filter for validity */ rels = fu_engine_get_upgrades(priv->engine, priv->request, fwupd_device_get_id(dev), &error_local); if (rels == NULL) { g_ptr_array_add(devices_no_upgrades, dev); /* discard the actual reason from user, but leave for debugging */ g_debug("%s", error_local->message); continue; } child = g_node_append_data(root, g_object_ref(dev)); for (guint j = 0; j < rels->len; j++) { FwupdRelease *rel = g_ptr_array_index(rels, j); if (!fwupd_release_match_flags(rel, priv->filter_release_include, priv->filter_release_exclude)) continue; g_node_append_data(child, g_object_ref(rel)); } } /* devices that have no updates available for whatever reason */ if (devices_no_support->len > 0) { fu_console_print_literal(priv->console, /* TRANSLATORS: message letting the user know no device * upgrade available due to missing on LVFS */ _("Devices with no available firmware updates: ")); for (guint i = 0; i < devices_no_support->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices_no_support, i); fu_console_print(priv->console, " • %s", fwupd_device_get_name(dev)); } } if (devices_no_upgrades->len > 0) { fu_console_print_literal( priv->console, /* TRANSLATORS: message letting the user know no device upgrade available */ _("Devices with the latest available firmware version:")); for (guint i = 0; i < devices_no_upgrades->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices_no_upgrades, i); fu_console_print(priv->console, " • %s", fwupd_device_get_name(dev)); } } /* updates */ if (g_node_n_nodes(root, G_TRAVERSE_ALL) <= 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, _("No updates available for remaining devices")); return FALSE; } fu_util_print_node(priv->console, priv->client, root); return TRUE; } static gboolean fu_util_get_details(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(GPtrArray) array = NULL; g_autoptr(FuUtilNode) root = g_node_new(NULL); g_autoptr(GInputStream) stream = NULL; /* load engine */ if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; /* check args */ if (g_strv_length(values) != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } /* implied, important for get-details on a device not in your system */ priv->show_all = TRUE; /* open file */ stream = fu_input_stream_from_path(values[0], error); if (stream == NULL) { fu_util_maybe_prefix_sandbox_error(values[0], error); return FALSE; } array = fu_engine_get_details(priv->engine, priv->request, stream, error); if (array == NULL) return FALSE; for (guint i = 0; i < array->len; i++) { FwupdDevice *dev = g_ptr_array_index(array, i); FwupdRelease *rel; FuUtilNode *child; if (!fwupd_device_match_flags(dev, priv->filter_device_include, priv->filter_device_exclude)) continue; child = g_node_append_data(root, g_object_ref(dev)); rel = fwupd_device_get_release_default(dev); if (rel != NULL) g_node_append_data(child, g_object_ref(rel)); } fu_util_print_node(priv->console, priv->client, root); return TRUE; } static gboolean fu_util_get_device_flags(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(GString) str = g_string_new(NULL); for (FwupdDeviceFlags i = FWUPD_DEVICE_FLAG_INTERNAL; i < FWUPD_DEVICE_FLAG_UNKNOWN; i <<= 1) { const gchar *tmp = fwupd_device_flag_to_string(i); if (tmp == NULL) break; if (i != FWUPD_DEVICE_FLAG_INTERNAL) g_string_append(str, " "); g_string_append(str, tmp); g_string_append(str, " ~"); g_string_append(str, tmp); } fu_console_print_literal(priv->console, str->str); return TRUE; } static void fu_util_build_device_tree(FuUtilPrivate *priv, FuUtilNode *root, GPtrArray *devs, FuDevice *dev) { for (guint i = 0; i < devs->len; i++) { FuDevice *dev_tmp = g_ptr_array_index(devs, i); if (!fwupd_device_match_flags(FWUPD_DEVICE(dev_tmp), priv->filter_device_include, priv->filter_device_exclude)) continue; if (!priv->show_all && !fu_util_is_interesting_device(FWUPD_DEVICE(dev_tmp))) continue; if (fu_device_get_parent(dev_tmp) == dev) { FuUtilNode *child = g_node_append_data(root, g_object_ref(dev_tmp)); fu_util_build_device_tree(priv, child, devs, dev_tmp); } } } static gboolean fu_util_get_devices_as_json(FuUtilPrivate *priv, GPtrArray *devs, GError **error) { g_autoptr(JsonBuilder) builder = json_builder_new(); json_builder_begin_object(builder); json_builder_set_member_name(builder, "Devices"); json_builder_begin_array(builder); for (guint i = 0; i < devs->len; i++) { FuDevice *dev = g_ptr_array_index(devs, i); g_autoptr(GPtrArray) rels = NULL; g_autoptr(GError) error_local = NULL; /* add all releases that could be applied */ rels = fu_engine_get_releases_for_device(priv->engine, priv->request, dev, &error_local); if (rels == NULL) { g_debug("not adding releases to device: %s", error_local->message); } else { for (guint j = 0; j < rels->len; j++) { FwupdRelease *rel = g_ptr_array_index(rels, j); if (!fwupd_release_match_flags(rel, priv->filter_release_include, priv->filter_release_exclude)) continue; fu_device_add_release(dev, rel); } } /* add to builder */ json_builder_begin_object(builder); fwupd_codec_to_json(FWUPD_CODEC(dev), builder, FWUPD_CODEC_FLAG_TRUSTED); json_builder_end_object(builder); } json_builder_end_array(builder); json_builder_end_object(builder); return fu_util_print_builder(priv->console, builder, error); } static gboolean fu_util_get_devices(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FuUtilNode) root = g_node_new(NULL); g_autoptr(GPtrArray) devs = NULL; /* load engine */ if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; /* get devices and build tree */ if (g_strv_length(values) > 0) { devs = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); for (guint i = 0; values[i] != NULL; i++) { FuDevice *device = fu_util_get_device(priv, values[i], error); if (device == NULL) return FALSE; g_ptr_array_add(devs, device); } } else { devs = fu_engine_get_devices(priv->engine, error); if (devs == NULL) return FALSE; } /* not for human consumption */ if (priv->as_json) return fu_util_get_devices_as_json(priv, devs, error); if (devs->len > 0) { fwupd_device_array_ensure_parents(devs); fu_util_build_device_tree(priv, root, devs, NULL); } /* print */ if (g_node_n_children(root) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, /* TRANSLATORS: nothing attached that can be upgraded */ _("No hardware detected with firmware update capability")); return FALSE; } fu_util_print_node(priv->console, priv->client, root); return TRUE; } static void fu_util_update_device_changed_cb(FwupdClient *client, FwupdDevice *device, FuUtilPrivate *priv) { g_autofree gchar *str = NULL; /* allowed to set whenever the device has changed */ if (fwupd_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN)) priv->completion_flags |= FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN; if (fwupd_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT)) priv->completion_flags |= FWUPD_DEVICE_FLAG_NEEDS_REBOOT; /* same as last time, so ignore */ if (priv->current_device == NULL || g_strcmp0(fwupd_device_get_composite_id(priv->current_device), fwupd_device_get_composite_id(device)) == 0) { g_set_object(&priv->current_device, device); return; } /* ignore indirect devices that might have changed */ if (fwupd_device_get_status(device) == FWUPD_STATUS_IDLE || fwupd_device_get_status(device) == FWUPD_STATUS_UNKNOWN) { g_debug("ignoring %s with status %s", fwupd_device_get_name(device), fwupd_status_to_string(fwupd_device_get_status(device))); return; } /* show message in console */ if (priv->current_operation == FU_UTIL_OPERATION_UPDATE) { /* TRANSLATORS: %1 is a device name */ str = g_strdup_printf(_("Updating %s…"), fwupd_device_get_name(device)); fu_console_set_progress_title(priv->console, str); } else if (priv->current_operation == FU_UTIL_OPERATION_INSTALL) { /* TRANSLATORS: %1 is a device name */ str = g_strdup_printf(_("Installing on %s…"), fwupd_device_get_name(device)); fu_console_set_progress_title(priv->console, str); } else if (priv->current_operation == FU_UTIL_OPERATION_READ) { /* TRANSLATORS: %1 is a device name */ str = g_strdup_printf(_("Reading from %s…"), fwupd_device_get_name(device)); fu_console_set_progress_title(priv->console, str); } else { g_warning("no FuUtilOperation set"); } g_set_object(&priv->current_device, device); } static void fu_util_display_current_message(FuUtilPrivate *priv) { if (priv->as_json) return; /* print all POST requests */ for (guint i = 0; i < priv->post_requests->len; i++) { FwupdRequest *request = g_ptr_array_index(priv->post_requests, i); fu_console_print_literal(priv->console, fu_util_request_get_message(request)); } } static gboolean fu_util_install_blob(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FuDevice) device = NULL; g_autoptr(GInputStream) stream_fw = NULL; /* progress */ fu_progress_set_id(priv->progress, G_STRLOC); fu_progress_add_flag(priv->progress, FU_PROGRESS_FLAG_NO_PROFILE); fu_progress_add_step(priv->progress, FWUPD_STATUS_LOADING, 2, "parse"); fu_progress_add_step(priv->progress, FWUPD_STATUS_LOADING, 30, "start-engine"); fu_progress_add_step(priv->progress, FWUPD_STATUS_DEVICE_WRITE, 68, NULL); /* invalid args */ if (g_strv_length(values) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } /* parse blob */ stream_fw = fu_input_stream_from_path(values[0], error); if (stream_fw == NULL) { fu_util_maybe_prefix_sandbox_error(values[0], error); return FALSE; } fu_progress_step_done(priv->progress); /* load engine */ if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_DEVICE_HOTPLUG | FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_HWINFO, fu_progress_get_child(priv->progress), error)) return FALSE; fu_progress_step_done(priv->progress); /* get device */ priv->filter_device_include |= FWUPD_DEVICE_FLAG_UPDATABLE; if (g_strv_length(values) >= 2) { device = fu_util_get_device(priv, values[1], error); if (device == NULL) return FALSE; } else { device = fu_util_prompt_for_device(priv, NULL, error); if (device == NULL) return FALSE; } priv->current_operation = FU_UTIL_OPERATION_INSTALL; g_signal_connect(FU_ENGINE(priv->engine), "device-changed", G_CALLBACK(fu_util_update_device_changed_cb), priv); /* write bare firmware */ if (priv->prepare_blob) { g_autoptr(GPtrArray) devices = NULL; devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_ptr_array_add(devices, g_object_ref(device)); if (!fu_engine_composite_prepare(priv->engine, devices, error)) { g_prefix_error(error, "failed to prepare composite action: "); return FALSE; } } priv->flags |= FWUPD_INSTALL_FLAG_NO_HISTORY; if (!fu_engine_install_blob(priv->engine, device, stream_fw, fu_progress_get_child(priv->progress), priv->flags, fu_engine_request_get_feature_flags(priv->request), error)) return FALSE; fu_progress_step_done(priv->progress); /* cleanup */ if (priv->cleanup_blob) { g_autoptr(FuDevice) device_new = NULL; g_autoptr(GError) error_local = NULL; /* get the possibly new device from the old ID */ device_new = fu_util_get_device(priv, fu_device_get_id(device), &error_local); if (device_new == NULL) { g_debug("failed to find new device: %s", error_local->message); } else { g_autoptr(GPtrArray) devices_new = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_ptr_array_add(devices_new, g_steal_pointer(&device_new)); if (!fu_engine_composite_cleanup(priv->engine, devices_new, error)) { g_prefix_error(error, "failed to cleanup composite action: "); return FALSE; } } } fu_util_display_current_message(priv); /* success */ return fu_util_prompt_complete(priv->console, priv->completion_flags, TRUE, error); } static gboolean fu_util_firmware_sign(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FuCabinet) cabinet = fu_cabinet_new(); g_autoptr(GBytes) archive_blob_new = NULL; g_autoptr(GBytes) cert = NULL; g_autoptr(GBytes) privkey = NULL; g_autoptr(GFile) archive_file_old = NULL; /* invalid args */ if (g_strv_length(values) != 3) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments, expected firmware.cab " "certificate.pem privatekey.pfx"); return FALSE; } /* load arguments */ cert = fu_bytes_get_contents(values[1], error); if (cert == NULL) return FALSE; privkey = fu_bytes_get_contents(values[2], error); if (privkey == NULL) return FALSE; /* load, sign, export */ archive_file_old = g_file_new_for_path(values[0]); if (!fu_firmware_parse_file(FU_FIRMWARE(cabinet), archive_file_old, FU_FIRMWARE_PARSE_FLAG_CACHE_STREAM, error)) return FALSE; if (!fu_cabinet_sign(cabinet, cert, privkey, FU_CABINET_SIGN_FLAG_NONE, error)) return FALSE; archive_blob_new = fu_firmware_write(FU_FIRMWARE(cabinet), error); if (archive_blob_new == NULL) return FALSE; return fu_bytes_set_contents(values[0], archive_blob_new, error); } static gboolean fu_util_firmware_dump(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FuDevice) device = NULL; g_autoptr(GBytes) blob_empty = g_bytes_new(NULL, 0); g_autoptr(GBytes) blob_fw = NULL; /* progress */ fu_progress_set_id(priv->progress, G_STRLOC); fu_progress_add_flag(priv->progress, FU_PROGRESS_FLAG_NO_PROFILE); fu_progress_add_step(priv->progress, FWUPD_STATUS_LOADING, 5, "start-engine"); fu_progress_add_step(priv->progress, FWUPD_STATUS_DEVICE_READ, 95, NULL); /* invalid args */ if (g_strv_length(values) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } /* file already exists */ if ((priv->flags & FWUPD_INSTALL_FLAG_FORCE) == 0 && g_file_test(values[0], G_FILE_TEST_EXISTS)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Filename already exists"); return FALSE; } /* write a zero length file to ensure the destination is writable to * avoid failing at the end of a potentially lengthy operation */ if (!fu_bytes_set_contents(values[0], blob_empty, error)) return FALSE; /* load engine */ if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_HWINFO, fu_progress_get_child(priv->progress), error)) return FALSE; fu_progress_step_done(priv->progress); /* get device */ priv->filter_device_include |= FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE; if (g_strv_length(values) >= 2) { device = fu_util_get_device(priv, values[1], error); if (device == NULL) return FALSE; } else { device = fu_util_prompt_for_device(priv, NULL, error); if (device == NULL) return FALSE; } priv->current_operation = FU_UTIL_OPERATION_READ; g_signal_connect(FU_ENGINE(priv->engine), "device-changed", G_CALLBACK(fu_util_update_device_changed_cb), priv); /* dump firmware */ blob_fw = fu_engine_firmware_dump(priv->engine, device, fu_progress_get_child(priv->progress), priv->flags, error); if (blob_fw == NULL) return FALSE; fu_progress_step_done(priv->progress); return fu_bytes_set_contents(values[0], blob_fw, error); } static gboolean fu_util_firmware_read(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FuDevice) device = NULL; g_autoptr(FuFirmware) fw = NULL; g_autoptr(GBytes) blob_empty = g_bytes_new(NULL, 0); g_autoptr(GBytes) blob_fw = NULL; /* progress */ fu_progress_set_id(priv->progress, G_STRLOC); fu_progress_add_flag(priv->progress, FU_PROGRESS_FLAG_NO_PROFILE); fu_progress_add_step(priv->progress, FWUPD_STATUS_LOADING, 5, "start-engine"); fu_progress_add_step(priv->progress, FWUPD_STATUS_DEVICE_READ, 95, NULL); /* invalid args */ if (g_strv_length(values) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } /* file already exists */ if ((priv->flags & FWUPD_INSTALL_FLAG_FORCE) == 0 && g_file_test(values[0], G_FILE_TEST_EXISTS)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Filename already exists"); return FALSE; } /* write a zero length file to ensure the destination is writable to * avoid failing at the end of a potentially lengthy operation */ if (!fu_bytes_set_contents(values[0], blob_empty, error)) return FALSE; /* load engine */ if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_DEVICE_HOTPLUG | FU_ENGINE_LOAD_FLAG_HWINFO, fu_progress_get_child(priv->progress), error)) return FALSE; fu_progress_step_done(priv->progress); /* get device */ priv->filter_device_include |= FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE; if (g_strv_length(values) >= 2) { device = fu_util_get_device(priv, values[1], error); if (device == NULL) return FALSE; } else { device = fu_util_prompt_for_device(priv, NULL, error); if (device == NULL) return FALSE; } priv->current_operation = FU_UTIL_OPERATION_READ; g_signal_connect(FU_ENGINE(priv->engine), "device-changed", G_CALLBACK(fu_util_update_device_changed_cb), priv); /* read firmware into the container format */ fw = fu_engine_firmware_read(priv->engine, device, fu_progress_get_child(priv->progress), priv->flags, error); if (fw == NULL) return FALSE; blob_fw = fu_firmware_write(fw, error); if (blob_fw == NULL) return FALSE; fu_progress_step_done(priv->progress); return fu_bytes_set_contents(values[0], blob_fw, error); } static gint fu_util_release_sort_cb(gconstpointer a, gconstpointer b) { FuRelease *release1 = *((FuRelease **)a); FuRelease *release2 = *((FuRelease **)b); return fu_release_compare(release1, release2); } static gchar * fu_util_download_if_required(FuUtilPrivate *priv, const gchar *perhapsfn, GError **error) { g_autofree gchar *filename = NULL; g_autoptr(GFile) file = NULL; /* a local file */ if (g_file_test(perhapsfn, G_FILE_TEST_EXISTS)) return g_strdup(perhapsfn); if (!fu_util_is_url(perhapsfn)) return g_strdup(perhapsfn); /* download the firmware to a cachedir */ filename = fu_util_get_user_cache_path(perhapsfn); if (!fu_path_mkdir_parent(filename, error)) return NULL; file = g_file_new_for_path(filename); if (!fwupd_client_download_file(priv->client, perhapsfn, file, FWUPD_CLIENT_DOWNLOAD_FLAG_NONE, priv->cancellable, error)) return NULL; return g_steal_pointer(&filename); } static gboolean fu_util_install_stream(FuUtilPrivate *priv, GInputStream *stream, GPtrArray *devices, FuProgress *progress, GError **error) { g_autoptr(FuCabinet) cabinet = NULL; g_autoptr(GPtrArray) components = NULL; g_autoptr(GPtrArray) errors = NULL; g_autoptr(GPtrArray) releases = NULL; cabinet = fu_engine_build_cabinet_from_stream(priv->engine, stream, error); if (cabinet == NULL) return FALSE; components = fu_cabinet_get_components(cabinet, error); if (components == NULL) return FALSE; /* for each component in the silo */ errors = g_ptr_array_new_with_free_func((GDestroyNotify)g_error_free); releases = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); for (guint i = 0; i < components->len; i++) { XbNode *component = g_ptr_array_index(components, i); /* do any devices pass the requirements */ for (guint j = 0; j < devices->len; j++) { FuDevice *device = g_ptr_array_index(devices, j); g_autoptr(FuRelease) release = fu_release_new(); g_autoptr(GError) error_local = NULL; /* is this component valid for the device */ fu_release_set_device(release, device); fu_release_set_request(release, priv->request); if (!fu_engine_load_release(priv->engine, release, cabinet, component, NULL, priv->flags, &error_local)) { g_debug("loading release failed on %s:%s failed: %s", fu_device_get_id(device), xb_node_query_text(component, "id", NULL), error_local->message); g_ptr_array_add(errors, g_steal_pointer(&error_local)); continue; } if (!fu_engine_requirements_check(priv->engine, release, priv->flags, &error_local)) { g_debug("requirement on %s:%s failed: %s", fu_device_get_id(device), xb_node_query_text(component, "id", NULL), error_local->message); g_ptr_array_add(errors, g_steal_pointer(&error_local)); continue; } /* if component should have an update message from CAB */ fu_device_ensure_from_component(device, component); fu_device_incorporate_from_component(device, component); /* success */ g_ptr_array_add(releases, g_steal_pointer(&release)); } } /* order the install tasks by the device priority */ g_ptr_array_sort(releases, fu_util_release_sort_cb); /* nothing suitable */ if (releases->len == 0) { GError *error_tmp = fu_engine_error_array_get_best(errors); g_propagate_error(error, error_tmp); return FALSE; } priv->current_operation = FU_UTIL_OPERATION_INSTALL; g_signal_connect(FU_ENGINE(priv->engine), "device-changed", G_CALLBACK(fu_util_update_device_changed_cb), priv); /* install all the tasks */ return fu_engine_install_releases(priv->engine, priv->request, releases, cabinet, fu_progress_get_child(priv->progress), priv->flags, error); } static gboolean fu_util_install(FuUtilPrivate *priv, gchar **values, GError **error) { g_autofree gchar *filename = NULL; g_autoptr(GInputStream) stream = NULL; g_autoptr(GPtrArray) devices_possible = NULL; /* progress */ fu_progress_set_id(priv->progress, G_STRLOC); fu_progress_add_flag(priv->progress, FU_PROGRESS_FLAG_NO_PROFILE); fu_progress_add_step(priv->progress, FWUPD_STATUS_LOADING, 50, "start-engine"); fu_progress_add_step(priv->progress, FWUPD_STATUS_DEVICE_WRITE, 50, NULL); /* load engine */ if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_DEVICE_HOTPLUG | FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_HWINFO, fu_progress_get_child(priv->progress), error)) return FALSE; fu_progress_step_done(priv->progress); /* handle both forms */ if (g_strv_length(values) == 1) { devices_possible = fu_engine_get_devices(priv->engine, error); if (devices_possible == NULL) return FALSE; fwupd_device_array_ensure_parents(devices_possible); } else if (g_strv_length(values) == 2) { FuDevice *device = fu_util_get_device(priv, values[1], error); if (device == NULL) return FALSE; if (!priv->no_safety_check) { if (!fu_util_prompt_warning_fde(priv->console, FWUPD_DEVICE(device), error)) return FALSE; } devices_possible = fu_engine_get_devices_by_composite_id(priv->engine, fu_device_get_composite_id(device), error); if (devices_possible == NULL) return FALSE; g_ptr_array_add(devices_possible, device); } else { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } /* download if required */ filename = fu_util_download_if_required(priv, values[0], error); if (filename == NULL) return FALSE; stream = fu_input_stream_from_path(filename, error); if (stream == NULL) { fu_util_maybe_prefix_sandbox_error(filename, error); return FALSE; } if (!fu_util_install_stream(priv, stream, devices_possible, fu_progress_get_child(priv->progress), error)) return FALSE; fu_progress_step_done(priv->progress); fu_util_display_current_message(priv); /* we don't want to ask anything */ if (priv->no_reboot_check) { g_debug("skipping reboot check"); return TRUE; } /* success */ return fu_util_prompt_complete(priv->console, priv->completion_flags, TRUE, error); } static gboolean fu_util_install_release(FuUtilPrivate *priv, FwupdDevice *dev, FwupdRelease *rel, GError **error) { FwupdRemote *remote; GPtrArray *locations; const gchar *remote_id; const gchar *uri_tmp; g_auto(GStrv) argv = NULL; if (!fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE)) { const gchar *name = fwupd_device_get_name(dev); g_autofree gchar *str = NULL; /* TRANSLATORS: the device has a reason it can't update, e.g. laptop lid closed */ str = g_strdup_printf(_("%s is not currently updatable"), name); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "%s: %s", str, fwupd_device_get_update_error(dev)); return FALSE; } /* get the default release only until other parts of fwupd can cope */ locations = fwupd_release_get_locations(rel); if (locations->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "release missing URI"); return FALSE; } uri_tmp = g_ptr_array_index(locations, 0); remote_id = fwupd_release_get_remote_id(rel); if (remote_id == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "failed to find remote for %s", uri_tmp); return FALSE; } remote = fu_engine_get_remote_by_id(priv->engine, remote_id, error); if (remote == NULL) return FALSE; argv = g_new0(gchar *, 2); /* local remotes may have the firmware already */ if (fwupd_remote_get_kind(remote) == FWUPD_REMOTE_KIND_LOCAL && !fu_util_is_url(uri_tmp)) { const gchar *fn_cache = fwupd_remote_get_filename_cache(remote); g_autofree gchar *path = g_path_get_dirname(fn_cache); argv[0] = g_build_filename(path, uri_tmp, NULL); } else if (fwupd_remote_get_kind(remote) == FWUPD_REMOTE_KIND_DIRECTORY) { argv[0] = g_strdup(uri_tmp + 7); /* web remote, fu_util_install will download file */ } else { argv[0] = fwupd_remote_build_firmware_uri(remote, uri_tmp, error); } /* reset progress before reusing it. */ fu_progress_reset(priv->progress); return fu_util_install(priv, argv, error); } static gboolean fu_util_update(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(GPtrArray) devices = NULL; g_autoptr(GPtrArray) devices_latest = g_ptr_array_new(); g_autoptr(GPtrArray) devices_pending = g_ptr_array_new(); g_autoptr(GPtrArray) devices_unsupported = g_ptr_array_new(); if (priv->flags & FWUPD_INSTALL_FLAG_ALLOW_OLDER) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "--allow-older is not supported for this command"); return FALSE; } if (priv->flags & FWUPD_INSTALL_FLAG_ALLOW_REINSTALL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "--allow-reinstall is not supported for this command"); return FALSE; } if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_DEVICE_HOTPLUG | FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; /* DEVICE-ID and GUID are acceptable args to update */ for (guint idx = 0; idx < g_strv_length(values); idx++) { if (!fwupd_guid_is_valid(values[idx]) && !fwupd_device_id_is_valid(values[idx])) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "'%s' is not a valid GUID nor DEVICE-ID", values[idx]); return FALSE; } } priv->current_operation = FU_UTIL_OPERATION_UPDATE; devices = fu_engine_get_devices(priv->engine, error); if (devices == NULL) return FALSE; fwupd_device_array_ensure_parents(devices); g_ptr_array_sort(devices, fu_util_sort_devices_by_flags_cb); for (guint i = 0; i < devices->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices, i); FwupdRelease *rel; const gchar *device_id = fu_device_get_id(dev); g_autoptr(GPtrArray) rels = NULL; g_autoptr(GError) error_local = NULL; gboolean dev_skip_byid = TRUE; /* only process particular DEVICE-ID or GUID if specified */ for (guint idx = 0; idx < g_strv_length(values); idx++) { const gchar *tmpid = values[idx]; if (fwupd_device_has_guid(dev, tmpid) || g_strcmp0(device_id, tmpid) == 0) { dev_skip_byid = FALSE; break; } } if (g_strv_length(values) > 0 && dev_skip_byid) continue; if (!fu_util_is_interesting_device(dev)) continue; /* only show stuff that has metadata available */ if (!fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE) && !fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN)) continue; if (!fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_SUPPORTED)) { g_ptr_array_add(devices_unsupported, dev); continue; } if (!fwupd_device_match_flags(dev, priv->filter_device_include, priv->filter_device_exclude)) continue; rels = fu_engine_get_upgrades(priv->engine, priv->request, device_id, &error_local); if (rels == NULL) { g_ptr_array_add(devices_latest, dev); /* discard the actual reason from user, but leave for debugging */ g_debug("%s", error_local->message); continue; } /* something is wrong */ if (fwupd_device_get_problems(dev) != FWUPD_DEVICE_PROBLEM_NONE) { g_ptr_array_add(devices_pending, dev); continue; } rel = g_ptr_array_index(rels, 0); if (!priv->no_safety_check) { g_autofree gchar *title = g_strdup_printf("%s %s", fu_engine_get_host_vendor(priv->engine), fu_engine_get_host_product(priv->engine)); if (!fu_util_prompt_warning(priv->console, dev, rel, title, error)) return FALSE; if (!fu_util_prompt_warning_fde(priv->console, dev, error)) return FALSE; } if (!fu_util_install_release(priv, dev, rel, &error_local)) { fu_console_print_literal(priv->console, error_local->message); continue; } fu_util_display_current_message(priv); } /* show warnings */ if (devices_latest->len > 0 && !priv->as_json) { fu_console_print_literal(priv->console, /* TRANSLATORS: message letting the user know no device * upgrade available */ _("Devices with the latest available firmware version:")); for (guint i = 0; i < devices_latest->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices_latest, i); fu_console_print(priv->console, " • %s", fwupd_device_get_name(dev)); } } if (devices_unsupported->len > 0 && !priv->as_json) { fu_console_print_literal(priv->console, /* TRANSLATORS: message letting the user know no * device upgrade available due to missing on LVFS */ _("Devices with no available firmware updates: ")); for (guint i = 0; i < devices_unsupported->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices_unsupported, i); fu_console_print(priv->console, " • %s", fwupd_device_get_name(dev)); } } if (devices_pending->len > 0 && !priv->as_json) { fu_console_print_literal( priv->console, /* TRANSLATORS: message letting the user there is an update * waiting, but there is a reason it cannot be deployed */ _("Devices with firmware updates that need user action: ")); for (guint i = 0; i < devices_pending->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices_pending, i); fu_console_print(priv->console, " • %s", fwupd_device_get_name(dev)); for (guint j = 0; j < 64; j++) { FwupdDeviceProblem problem = (guint64)1 << j; g_autofree gchar *desc = NULL; if (!fwupd_device_has_problem(dev, problem)) continue; desc = fu_util_device_problem_to_string(priv->client, dev, problem); if (desc == NULL) continue; fu_console_print(priv->console, " ‣ %s", desc); } } } /* we don't want to ask anything */ if (priv->no_reboot_check || priv->as_json) { g_debug("skipping reboot check"); return TRUE; } return fu_util_prompt_complete(priv->console, priv->completion_flags, TRUE, error); } static gboolean fu_util_reinstall(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FwupdRelease) rel = NULL; g_autoptr(GPtrArray) rels = NULL; g_autoptr(FuDevice) dev = NULL; if (g_strv_length(values) != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_DEVICE_HOTPLUG | FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; dev = fu_util_get_device(priv, values[0], error); if (dev == NULL) return FALSE; /* try to lookup/match release from client */ rels = fu_engine_get_releases_for_device(priv->engine, priv->request, dev, error); if (rels == NULL) return FALSE; for (guint j = 0; j < rels->len; j++) { FwupdRelease *rel_tmp = g_ptr_array_index(rels, j); if (!fwupd_release_match_flags(rel_tmp, priv->filter_release_include, priv->filter_release_exclude)) continue; if (fu_version_compare(fwupd_release_get_version(rel_tmp), fu_device_get_version(dev), fu_device_get_version_format(dev)) == 0) { rel = g_object_ref(rel_tmp); break; } } if (rel == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Unable to locate release for %s version %s", fu_device_get_name(dev), fu_device_get_version(dev)); return FALSE; } /* update the console if composite devices are also updated */ priv->current_operation = FU_UTIL_OPERATION_INSTALL; g_signal_connect(FU_ENGINE(priv->engine), "device-changed", G_CALLBACK(fu_util_update_device_changed_cb), priv); priv->flags |= FWUPD_INSTALL_FLAG_ALLOW_REINSTALL; if (!fu_util_install_release(priv, FWUPD_DEVICE(dev), rel, error)) return FALSE; fu_util_display_current_message(priv); /* we don't want to ask anything */ if (priv->no_reboot_check) { g_debug("skipping reboot check"); return TRUE; } return fu_util_prompt_complete(priv->console, priv->completion_flags, TRUE, error); } static gboolean fu_util_detach(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FuDevice) device = NULL; g_autoptr(FuDeviceLocker) locker = NULL; /* progress */ fu_progress_set_id(priv->progress, G_STRLOC); fu_progress_add_step(priv->progress, FWUPD_STATUS_LOADING, 95, "start-engine"); fu_progress_add_step(priv->progress, FWUPD_STATUS_DEVICE_BUSY, 5, NULL); /* load engine */ if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_DEVICE_HOTPLUG | FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_HWINFO, fu_progress_get_child(priv->progress), error)) return FALSE; fu_progress_step_done(priv->progress); /* get device */ priv->filter_device_exclude |= FWUPD_DEVICE_FLAG_IS_BOOTLOADER; if (g_strv_length(values) >= 1) { device = fu_util_get_device(priv, values[0], error); if (device == NULL) return FALSE; } else { device = fu_util_prompt_for_device(priv, NULL, error); if (device == NULL) return FALSE; } /* run vfunc */ locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; if (!fu_device_detach_full(device, fu_progress_get_child(priv->progress), error)) return FALSE; fu_progress_step_done(priv->progress); return TRUE; } static gboolean fu_util_unbind_driver(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FuDevice) device = NULL; g_autoptr(FuDeviceLocker) locker = NULL; /* load engine */ if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_DEVICE_HOTPLUG | FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; /* get device */ if (g_strv_length(values) == 1) { device = fu_util_get_device(priv, values[0], error); } else { device = fu_util_prompt_for_device(priv, NULL, error); } if (device == NULL) return FALSE; /* run vfunc */ locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; return fu_device_unbind_driver(device, error); } static gboolean fu_util_bind_driver(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FuDevice) device = NULL; g_autoptr(FuDeviceLocker) locker = NULL; /* load engine */ if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_DEVICE_HOTPLUG | FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; /* get device */ if (g_strv_length(values) == 3) { device = fu_util_get_device(priv, values[2], error); if (device == NULL) return FALSE; } else if (g_strv_length(values) == 2) { device = fu_util_prompt_for_device(priv, NULL, error); if (device == NULL) return FALSE; } else { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } /* run vfunc */ locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; return fu_device_bind_driver(device, values[0], values[1], error); } static gboolean fu_util_attach(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FuDevice) device = NULL; g_autoptr(FuDeviceLocker) locker = NULL; /* progress */ fu_progress_set_id(priv->progress, G_STRLOC); fu_progress_add_step(priv->progress, FWUPD_STATUS_LOADING, 95, "start-engine"); fu_progress_add_step(priv->progress, FWUPD_STATUS_DEVICE_BUSY, 5, NULL); /* load engine */ if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_DEVICE_HOTPLUG | FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_HWINFO, fu_progress_get_child(priv->progress), error)) return FALSE; fu_progress_step_done(priv->progress); /* get device */ if ((priv->flags & FWUPD_INSTALL_FLAG_FORCE) == 0) priv->filter_device_include |= FWUPD_DEVICE_FLAG_IS_BOOTLOADER; if (g_strv_length(values) >= 1) { device = fu_util_get_device(priv, values[0], error); if (device == NULL) return FALSE; } else { device = fu_util_prompt_for_device(priv, NULL, error); if (device == NULL) return FALSE; } /* run vfunc */ locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; if (!fu_device_attach_full(device, fu_progress_get_child(priv->progress), error)) return FALSE; fu_progress_step_done(priv->progress); /* success */ return TRUE; } static void fu_util_report_metadata_to_string(GHashTable *metadata, guint idt, GString *str) { g_autoptr(GList) keys = g_list_sort(g_hash_table_get_keys(metadata), (GCompareFunc)g_strcmp0); for (GList *l = keys; l != NULL; l = l->next) { const gchar *key = l->data; const gchar *value = g_hash_table_lookup(metadata, key); fwupd_codec_string_append(str, idt, key, value); } } static gboolean fu_util_get_report_metadata_as_json(FuUtilPrivate *priv, JsonBuilder *builder, GError **error) { GPtrArray *plugins; g_autoptr(GHashTable) metadata = NULL; g_autoptr(GPtrArray) devices = NULL; /* daemon metadata */ metadata = fu_engine_get_report_metadata(priv->engine, error); if (metadata == NULL) return FALSE; fwupd_codec_json_append_map(builder, "daemon", metadata); /* device metadata */ devices = fu_engine_get_devices(priv->engine, error); if (devices == NULL) return FALSE; json_builder_set_member_name(builder, "devices"); json_builder_begin_array(builder); for (guint i = 0; i < devices->len; i++) { FuDevice *device = g_ptr_array_index(devices, i); g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GHashTable) metadata_post = NULL; g_autoptr(GHashTable) metadata_pre = NULL; locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; metadata_pre = fu_device_report_metadata_pre(device); metadata_post = fu_device_report_metadata_post(device); if (metadata_pre == NULL && metadata_post == NULL) continue; json_builder_begin_object(builder); json_builder_set_member_name(builder, fu_device_get_id(device)); json_builder_begin_array(builder); if (metadata_pre != NULL) { json_builder_begin_object(builder); fwupd_codec_json_append_map(builder, "pre", metadata_pre); json_builder_end_object(builder); } if (metadata_post != NULL) { json_builder_begin_object(builder); fwupd_codec_json_append_map(builder, "post", metadata_post); json_builder_end_object(builder); } json_builder_end_array(builder); json_builder_end_object(builder); } json_builder_end_array(builder); /* plugin metadata */ plugins = fu_engine_get_plugins(priv->engine); json_builder_set_member_name(builder, "plugins"); json_builder_begin_array(builder); for (guint i = 0; i < plugins->len; i++) { FuPlugin *plugin = g_ptr_array_index(plugins, i); if (fu_plugin_has_flag(plugin, FWUPD_PLUGIN_FLAG_DISABLED)) continue; if (fu_plugin_get_report_metadata(plugin) == NULL) continue; json_builder_begin_object(builder); fwupd_codec_json_append_map(builder, fu_plugin_get_name(plugin), fu_plugin_get_report_metadata(plugin)); json_builder_end_object(builder); } json_builder_end_array(builder); /* success */ return TRUE; } static gboolean fu_util_get_report_metadata(FuUtilPrivate *priv, gchar **values, GError **error) { GPtrArray *plugins; g_autoptr(GHashTable) metadata = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(GString) str = g_string_new(NULL); /* progress */ fu_progress_set_id(priv->progress, G_STRLOC); fu_progress_add_step(priv->progress, FWUPD_STATUS_LOADING, 95, "start-engine"); fu_progress_add_step(priv->progress, FWUPD_STATUS_DEVICE_BUSY, 5, NULL); /* load engine */ if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_HWINFO, fu_progress_get_child(priv->progress), error)) return FALSE; fu_progress_step_done(priv->progress); /* not for human consumption */ if (priv->as_json) { g_autoptr(JsonBuilder) builder = json_builder_new(); json_builder_begin_object(builder); if (!fu_util_get_report_metadata_as_json(priv, builder, error)) return FALSE; json_builder_end_object(builder); return fu_util_print_builder(priv->console, builder, error); } /* daemon metadata */ metadata = fu_engine_get_report_metadata(priv->engine, error); if (metadata == NULL) return FALSE; fu_util_report_metadata_to_string(metadata, 0, str); /* device metadata */ devices = fu_engine_get_devices(priv->engine, error); if (devices == NULL) return FALSE; for (guint i = 0; i < devices->len; i++) { FuDevice *device = g_ptr_array_index(devices, i); g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GHashTable) metadata_post = NULL; g_autoptr(GHashTable) metadata_pre = NULL; locker = fu_device_locker_new(device, error); if (locker == NULL) return FALSE; metadata_pre = fu_device_report_metadata_pre(device); metadata_post = fu_device_report_metadata_post(device); if (metadata_pre != NULL || metadata_post != NULL) { fwupd_codec_string_append(str, 0, FWUPD_RESULT_KEY_DEVICE_ID, fu_device_get_id(device)); } if (metadata_pre != NULL) { fwupd_codec_string_append(str, 1, "pre", ""); fu_util_report_metadata_to_string(metadata_pre, 3, str); } if (metadata_post != NULL) { fwupd_codec_string_append(str, 1, "post", ""); fu_util_report_metadata_to_string(metadata_post, 3, str); } } /* plugin metadata */ plugins = fu_engine_get_plugins(priv->engine); for (guint i = 0; i < plugins->len; i++) { FuPlugin *plugin = g_ptr_array_index(plugins, i); if (fu_plugin_has_flag(plugin, FWUPD_PLUGIN_FLAG_DISABLED)) continue; if (fu_plugin_get_report_metadata(plugin) == NULL) continue; fwupd_codec_string_append(str, 1, fu_plugin_get_name(plugin), ""); fu_util_report_metadata_to_string(fu_plugin_get_report_metadata(plugin), 3, str); } fu_progress_step_done(priv->progress); /* display */ fu_console_print_literal(priv->console, str->str); /* success */ return TRUE; } static gboolean fu_util_modify_config(FuUtilPrivate *priv, gchar **values, GError **error) { /* start engine */ if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; /* check args */ if (g_strv_length(values) == 3) { if (!fu_engine_modify_config(priv->engine, values[0], values[1], values[2], error)) return FALSE; } else if (g_strv_length(values) == 2) { if (!fu_engine_modify_config(priv->engine, "fwupd", values[0], values[1], error)) return FALSE; } else { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments: [SECTION] KEY VALUE expected"); return FALSE; } if (priv->as_json) return TRUE; /* TRANSLATORS: success message -- a per-system setting value */ fu_console_print_literal(priv->console, _("Successfully modified configuration value")); return TRUE; } static gboolean fu_util_reset_config(FuUtilPrivate *priv, gchar **values, GError **error) { /* check args */ if (g_strv_length(values) != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments: SECTION"); return FALSE; } /* start engine */ if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_NONE, priv->progress, error)) return FALSE; if (!fu_engine_reset_config(priv->engine, values[0], error)) return FALSE; if (priv->as_json) return TRUE; /* TRANSLATORS: success message -- a per-system setting value */ fu_console_print_literal(priv->console, _("Successfully reset configuration section")); return TRUE; } static gboolean fu_util_remote_modify(FuUtilPrivate *priv, gchar **values, GError **error) { FwupdRemote *remote = NULL; if (g_strv_length(values) < 3) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; remote = fu_engine_get_remote_by_id(priv->engine, values[0], error); if (remote == NULL) return FALSE; if (!fu_engine_modify_remote(priv->engine, fwupd_remote_get_id(remote), values[1], values[2], error)) return FALSE; if (priv->as_json) return TRUE; fu_console_print_literal(priv->console, _("Successfully modified remote")); return TRUE; } static gboolean fu_util_remote_disable(FuUtilPrivate *priv, gchar **values, GError **error) { FwupdRemote *remote = NULL; if (g_strv_length(values) != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_REMOTES, priv->progress, error)) return FALSE; remote = fu_engine_get_remote_by_id(priv->engine, values[0], error); if (remote == NULL) return FALSE; if (!fu_engine_modify_remote(priv->engine, fwupd_remote_get_id(remote), "Enabled", "false", error)) return FALSE; if (priv->as_json) return TRUE; fu_console_print_literal(priv->console, _("Successfully disabled remote")); return TRUE; } static gboolean fu_util_vercmp(FuUtilPrivate *priv, gchar **values, GError **error) { FwupdVersionFormat verfmt = FWUPD_VERSION_FORMAT_UNKNOWN; gint rc; /* sanity check */ if (g_strv_length(values) < 2) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments, expected VER1 VER2"); return FALSE; } /* optional version format */ if (g_strv_length(values) > 2) { verfmt = fwupd_version_format_from_string(values[2]); if (verfmt == FWUPD_VERSION_FORMAT_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Version format %s not supported", values[2]); return FALSE; } } /* compare */ rc = fu_version_compare(values[0], values[1], verfmt); if (rc > 0) { fu_console_print(priv->console, "%s > %s", values[0], values[1]); } else if (rc < 0) { fu_console_print(priv->console, "%s < %s", values[0], values[1]); } else { fu_console_print(priv->console, "%s == %s", values[0], values[1]); } return TRUE; } static gboolean fu_util_remote_enable(FuUtilPrivate *priv, gchar **values, GError **error) { FwupdRemote *remote = NULL; if (g_strv_length(values) != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_REMOTES, priv->progress, error)) return FALSE; remote = fu_engine_get_remote_by_id(priv->engine, values[0], error); if (remote == NULL) return FALSE; if (!fu_util_modify_remote_warning(priv->console, remote, FALSE, error)) return FALSE; if (!fu_engine_modify_remote(priv->engine, fwupd_remote_get_id(remote), "Enabled", "true", error)) return FALSE; if (priv->as_json) return TRUE; fu_console_print_literal(priv->console, _("Successfully enabled remote")); return TRUE; } static gboolean fu_util_set_test_devices_enabled(FuUtilPrivate *priv, gboolean enable, GError **error) { return fu_engine_modify_config(priv->engine, "fwupd", "TestDevices", enable ? "true" : "false", error); } static gboolean fu_util_disable_test_devices(FuUtilPrivate *priv, gchar **values, GError **error) { if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; if (!fu_util_set_test_devices_enabled(priv, FALSE, error)) return FALSE; if (priv->as_json) return TRUE; /* TRANSLATORS: comment explaining result of command */ fu_console_print_literal(priv->console, _("Successfully disabled test devices")); return TRUE; } static gboolean fu_util_enable_test_devices(FuUtilPrivate *priv, gchar **values, GError **error) { gboolean found = FALSE; g_autoptr(GPtrArray) remotes = NULL; if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; if (!fu_util_set_test_devices_enabled(priv, TRUE, error)) return FALSE; /* verify remote is present */ remotes = fu_engine_get_remotes(priv->engine, error); if (remotes == NULL) return FALSE; for (guint i = 0; i < remotes->len; i++) { FwupdRemote *remote = g_ptr_array_index(remotes, i); if (!fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_ENABLED)) continue; if (g_strcmp0(fwupd_remote_get_id(remote), "fwupd-tests") == 0) { found = TRUE; break; } } if (!found) { if (!fu_util_set_test_devices_enabled(priv, FALSE, error)) return FALSE; g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to enable fwupd-tests remote"); return FALSE; } if (priv->as_json) return TRUE; /* TRANSLATORS: comment explaining result of command */ fu_console_print_literal(priv->console, _("Successfully enabled test devices")); return TRUE; } static gboolean fu_util_check_activation_needed(FuUtilPrivate *priv, GError **error) { gboolean has_pending = FALSE; g_autoptr(FuHistory) history = fu_history_new(priv->ctx); g_autoptr(GPtrArray) devices = fu_history_get_devices(history, error); if (devices == NULL) return FALSE; /* only start up the plugins needed */ for (guint i = 0; i < devices->len; i++) { FuDevice *dev = g_ptr_array_index(devices, i); if (fu_device_has_flag(dev, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION)) { fu_engine_add_plugin_filter(priv->engine, fu_device_get_plugin(dev)); has_pending = TRUE; } } if (!has_pending) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "No devices to activate"); return FALSE; } return TRUE; } static gboolean fu_util_activate(FuUtilPrivate *priv, gchar **values, GError **error) { gboolean has_pending = FALSE; g_autoptr(GPtrArray) devices = NULL; /* check the history database before starting the daemon */ if (!fu_util_check_activation_needed(priv, error)) return FALSE; /* progress */ fu_progress_set_id(priv->progress, G_STRLOC); fu_progress_add_step(priv->progress, FWUPD_STATUS_LOADING, 95, "start-engine"); fu_progress_add_step(priv->progress, FWUPD_STATUS_DEVICE_BUSY, 5, NULL); /* load engine */ if (!fu_util_start_engine( priv, FU_ENGINE_LOAD_FLAG_READONLY | FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_DEVICE_HOTPLUG | FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_EXTERNAL_PLUGINS | FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS | FU_ENGINE_LOAD_FLAG_HWINFO, fu_progress_get_child(priv->progress), error)) return FALSE; fu_progress_step_done(priv->progress); /* parse arguments */ if (g_strv_length(values) == 0) { devices = fu_engine_get_devices(priv->engine, error); if (devices == NULL) return FALSE; } else if (g_strv_length(values) == 1) { FuDevice *device; device = fu_util_get_device(priv, values[0], error); if (device == NULL) return FALSE; devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_ptr_array_add(devices, device); } else { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } /* activate anything with _NEEDS_ACTIVATION */ for (guint i = 0; i < devices->len; i++) { FuDevice *device = g_ptr_array_index(devices, i); if (!fwupd_device_match_flags(FWUPD_DEVICE(device), priv->filter_device_include, priv->filter_device_exclude)) continue; if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION)) continue; has_pending = TRUE; if (!priv->as_json) { fu_console_print( priv->console, "%s %s…", /* TRANSLATORS: shown when shutting down to switch to the new version */ _("Activating firmware update"), fu_device_get_name(device)); } if (!fu_engine_activate(priv->engine, fu_device_get_id(device), fu_progress_get_child(priv->progress), error)) return FALSE; } fu_progress_step_done(priv->progress); if (!has_pending) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "No devices to activate"); return FALSE; } return TRUE; } static gboolean fu_util_export_hwids(FuUtilPrivate *priv, gchar **values, GError **error) { FuContext *ctx = fu_engine_get_context(priv->engine); FuHwids *hwids = fu_context_get_hwids(ctx); g_autoptr(GKeyFile) kf = g_key_file_new(); g_autoptr(GPtrArray) hwid_keys = NULL; /* check args */ if (g_strv_length(values) != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments, expected HWIDS-FILE"); return FALSE; } /* setup default hwids */ if (!fu_context_load_hwinfo(ctx, priv->progress, FU_CONTEXT_HWID_FLAG_LOAD_ALL, error)) return FALSE; /* save all keys */ hwid_keys = fu_hwids_get_keys(hwids); for (guint i = 0; i < hwid_keys->len; i++) { const gchar *hwid_key = g_ptr_array_index(hwid_keys, i); const gchar *value = fu_hwids_get_value(hwids, hwid_key); if (value == NULL) continue; g_key_file_set_string(kf, "HwIds", hwid_key, value); } /* success */ return g_key_file_save_to_file(kf, values[0], error); } static gboolean fu_util_hwids(FuUtilPrivate *priv, gchar **values, GError **error) { FuContext *ctx = fu_engine_get_context(priv->engine); FuHwids *hwids = fu_context_get_hwids(ctx); g_autoptr(GPtrArray) chid_keys = fu_hwids_get_chid_keys(hwids); g_autoptr(GPtrArray) hwid_keys = fu_hwids_get_keys(hwids); /* a keyfile with overrides */ if (g_strv_length(values) == 1) { g_autoptr(GKeyFile) kf = g_key_file_new(); if (!g_key_file_load_from_file(kf, values[0], G_KEY_FILE_NONE, error)) return FALSE; for (guint i = 0; i < hwid_keys->len; i++) { const gchar *hwid_key = g_ptr_array_index(hwid_keys, i); g_autofree gchar *tmp = NULL; tmp = g_key_file_get_string(kf, "HwIds", hwid_key, NULL); fu_hwids_add_value(hwids, hwid_key, tmp); } } if (!fu_context_load_hwinfo(ctx, priv->progress, FU_CONTEXT_HWID_FLAG_LOAD_ALL, error)) return FALSE; /* show debug output */ fu_console_print_literal(priv->console, "Computer Information"); fu_console_print_literal(priv->console, "--------------------"); for (guint i = 0; i < hwid_keys->len; i++) { const gchar *hwid_key = g_ptr_array_index(hwid_keys, i); const gchar *value = fu_hwids_get_value(hwids, hwid_key); if (value == NULL) continue; if (g_strcmp0(hwid_key, FU_HWIDS_KEY_BIOS_MAJOR_RELEASE) == 0 || g_strcmp0(hwid_key, FU_HWIDS_KEY_BIOS_MINOR_RELEASE) == 0) { guint64 val = 0; if (!fu_strtoull(value, &val, 0, G_MAXUINT64, FU_INTEGER_BASE_16, error)) return FALSE; fu_console_print(priv->console, "%s: %" G_GUINT64_FORMAT, hwid_key, val); } else { fu_console_print(priv->console, "%s: %s", hwid_key, value); } } /* show GUIDs */ fu_console_print_literal(priv->console, "Hardware IDs"); fu_console_print_literal(priv->console, "------------"); for (guint i = 0; i < chid_keys->len; i++) { const gchar *key = g_ptr_array_index(chid_keys, i); const gchar *keys = NULL; g_autofree gchar *guid = NULL; g_autofree gchar *keys_str = NULL; g_auto(GStrv) keysv = NULL; g_autoptr(GError) error_local = NULL; /* filter */ if (!g_str_has_prefix(key, "HardwareID")) continue; /* get the GUID */ keys = fu_hwids_get_replace_keys(hwids, key); guid = fu_hwids_get_guid(hwids, key, &error_local); if (guid == NULL) { fu_console_print_literal(priv->console, error_local->message); continue; } /* show what makes up the GUID */ keysv = g_strsplit(keys, "&", -1); keys_str = g_strjoinv(" + ", keysv); fu_console_print(priv->console, "{%s} <- %s", guid, keys_str); } /* show extra GUIDs */ fu_console_print_literal(priv->console, "Extra Hardware IDs"); fu_console_print_literal(priv->console, "------------------"); for (guint i = 0; i < chid_keys->len; i++) { const gchar *key = g_ptr_array_index(chid_keys, i); const gchar *keys = NULL; g_autofree gchar *guid = NULL; g_autofree gchar *keys_str = NULL; g_auto(GStrv) keysv = NULL; g_autoptr(GError) error_local = NULL; /* filter */ if (g_str_has_prefix(key, "HardwareID")) continue; /* get the GUID */ keys = fu_hwids_get_replace_keys(hwids, key); guid = fu_hwids_get_guid(hwids, key, &error_local); if (guid == NULL) { fu_console_print_literal(priv->console, error_local->message); continue; } /* show what makes up the GUID */ keysv = g_strsplit(keys, "&", -1); keys_str = g_strjoinv(" + ", keysv); fu_console_print(priv->console, "{%s} <- %s", guid, keys_str); } return TRUE; } static gboolean fu_util_self_sign(FuUtilPrivate *priv, gchar **values, GError **error) { g_autofree gchar *sig = NULL; /* check args */ if (g_strv_length(values) != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments: value expected"); return FALSE; } /* start engine */ if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_ENSURE_CLIENT_CERT, priv->progress, error)) return FALSE; sig = fu_engine_self_sign(priv->engine, values[0], JCAT_SIGN_FLAG_ADD_TIMESTAMP | JCAT_SIGN_FLAG_ADD_CERT, error); if (sig == NULL) return FALSE; if (priv->as_json) fu_console_print(priv->console, "{\"signature\": \"%s\"}", sig); else fu_console_print(priv->console, "%s", sig); return TRUE; } static void fu_util_device_added_cb(FwupdClient *client, FwupdDevice *device, gpointer user_data) { FuUtilPrivate *priv = (FuUtilPrivate *)user_data; g_autofree gchar *tmp = NULL; if (priv->as_json) return; tmp = fu_util_device_to_string(priv->client, device, 0); /* TRANSLATORS: this is when a device is hotplugged */ fu_console_print(priv->console, "%s\n%s", _("Device added:"), tmp); } static void fu_util_device_removed_cb(FwupdClient *client, FwupdDevice *device, gpointer user_data) { FuUtilPrivate *priv = (FuUtilPrivate *)user_data; g_autofree gchar *tmp = NULL; if (priv->as_json) return; tmp = fu_util_device_to_string(priv->client, device, 0); /* TRANSLATORS: this is when a device is hotplugged */ fu_console_print(priv->console, "%s\n%s", _("Device removed:"), tmp); } static void fu_util_device_changed_cb(FwupdClient *client, FwupdDevice *device, gpointer user_data) { FuUtilPrivate *priv = (FuUtilPrivate *)user_data; g_autofree gchar *tmp = NULL; if (priv->as_json) return; tmp = fu_util_device_to_string(priv->client, device, 0); /* TRANSLATORS: this is when a device has been updated */ fu_console_print(priv->console, "%s\n%s", _("Device changed:"), tmp); } static void fu_util_changed_cb(FwupdClient *client, gpointer user_data) { FuUtilPrivate *priv = (FuUtilPrivate *)user_data; if (priv->as_json) return; /* TRANSLATORS: this is when the daemon state changes */ fu_console_print_literal(priv->console, _("Changed")); } static gboolean fu_util_monitor(FuUtilPrivate *priv, gchar **values, GError **error) { /* get all the devices */ if (!fwupd_client_connect(priv->client, priv->cancellable, error)) return FALSE; /* watch for any hotplugged device */ g_signal_connect(FWUPD_CLIENT(priv->client), "changed", G_CALLBACK(fu_util_changed_cb), priv); g_signal_connect(FWUPD_CLIENT(priv->client), "device-added", G_CALLBACK(fu_util_device_added_cb), priv); g_signal_connect(FWUPD_CLIENT(priv->client), "device-removed", G_CALLBACK(fu_util_device_removed_cb), priv); g_signal_connect(FWUPD_CLIENT(priv->client), "device-changed", G_CALLBACK(fu_util_device_changed_cb), priv); g_signal_connect(G_CANCELLABLE(priv->cancellable), "cancelled", G_CALLBACK(fu_util_cancelled_cb), priv); g_main_loop_run(priv->loop); return TRUE; } static gboolean fu_util_get_firmware_types(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(GPtrArray) firmware_types = NULL; /* load engine */ if (!fu_engine_load(priv->engine, FU_ENGINE_LOAD_FLAG_READONLY | FU_ENGINE_LOAD_FLAG_EXTERNAL_PLUGINS | FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS, priv->progress, error)) return FALSE; firmware_types = fu_context_get_firmware_gtype_ids(fu_engine_get_context(priv->engine)); for (guint i = 0; i < firmware_types->len; i++) { const gchar *id = g_ptr_array_index(firmware_types, i); fu_console_print_literal(priv->console, id); } if (firmware_types->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, /* TRANSLATORS: nothing found */ _("No firmware IDs found")); return FALSE; } return TRUE; } static gboolean fu_util_get_firmware_gtypes(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(GArray) firmware_types = NULL; /* load engine */ if (!fu_engine_load(priv->engine, FU_ENGINE_LOAD_FLAG_READONLY | FU_ENGINE_LOAD_FLAG_EXTERNAL_PLUGINS | FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS, priv->progress, error)) return FALSE; firmware_types = fu_context_get_firmware_gtypes(fu_engine_get_context(priv->engine)); for (guint i = 0; i < firmware_types->len; i++) { GType gtype = g_array_index(firmware_types, GType, i); fu_console_print_literal(priv->console, g_type_name(gtype)); } if (firmware_types->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, /* TRANSLATORS: nothing found */ _("No firmware found")); return FALSE; } return TRUE; } static gchar * fu_util_prompt_for_firmware_type(FuUtilPrivate *priv, GPtrArray *firmware_types, GError **error) { guint idx; /* no detected types */ if (firmware_types->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "No detected firmware types"); return NULL; } /* there is no point asking */ if (firmware_types->len == 1) { const gchar *id = g_ptr_array_index(firmware_types, 0); return g_strdup(id); } /* TRANSLATORS: this is to abort the interactive prompt */ fu_console_print(priv->console, "0.\t%s", _("Cancel")); for (guint i = 0; i < firmware_types->len; i++) { const gchar *id = g_ptr_array_index(firmware_types, i); fu_console_print(priv->console, "%u.\t%s", i + 1, id); } /* TRANSLATORS: get interactive prompt */ idx = fu_console_input_uint(priv->console, firmware_types->len, "%s", _("Choose firmware")); if (idx == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "Request canceled"); return NULL; } return g_strdup(g_ptr_array_index(firmware_types, idx - 1)); } static gboolean fu_util_firmware_parse(FuUtilPrivate *priv, gchar **values, GError **error) { FuContext *ctx = fu_engine_get_context(priv->engine); GType gtype; g_autoptr(FuFirmware) firmware = NULL; g_autoptr(GInputStream) stream = NULL; g_autofree gchar *firmware_type = NULL; g_autofree gchar *str = NULL; /* check args */ if (g_strv_length(values) == 0 || g_strv_length(values) > 2) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments: filename required"); return FALSE; } /* load file */ stream = fu_input_stream_from_path(values[0], error); if (stream == NULL) return FALSE; /* load engine */ if (!fu_engine_load(priv->engine, FU_ENGINE_LOAD_FLAG_READONLY | FU_ENGINE_LOAD_FLAG_EXTERNAL_PLUGINS | FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS, priv->progress, error)) return FALSE; /* find the GType to use */ if (g_strv_length(values) == 1) { g_autoptr(GPtrArray) firmware_types = fu_context_get_firmware_gtype_ids(ctx); firmware_type = fu_util_prompt_for_firmware_type(priv, firmware_types, error); if (firmware_type == NULL) return FALSE; } else if (g_strcmp0(values[1], "auto") == 0) { g_autoptr(GPtrArray) gtype_ids = fu_context_get_firmware_gtype_ids(ctx); g_autoptr(GPtrArray) firmware_auto_types = g_ptr_array_new_with_free_func(g_free); for (guint i = 0; i < gtype_ids->len; i++) { const gchar *gtype_id = g_ptr_array_index(gtype_ids, i); GType gtype_tmp; g_autofree gchar *firmware_str = NULL; g_autoptr(FuFirmware) firmware_tmp = NULL; g_autoptr(GError) error_local = NULL; if (g_strcmp0(gtype_id, "raw") == 0) continue; g_debug("parsing as %s", gtype_id); gtype_tmp = fu_context_get_firmware_gtype_by_id(ctx, gtype_id); if (gtype_tmp == G_TYPE_INVALID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "GType %s not supported", gtype_id); return FALSE; } firmware_tmp = g_object_new(gtype_tmp, NULL); if (fu_firmware_has_flag(firmware_tmp, FU_FIRMWARE_FLAG_NO_AUTO_DETECTION)) continue; if (!fu_firmware_parse_stream(firmware_tmp, stream, 0x0, FU_FIRMWARE_PARSE_FLAG_NO_SEARCH, &error_local)) { g_debug("failed to parse as %s: %s", gtype_id, error_local->message); continue; } firmware_str = fu_firmware_to_string(firmware_tmp); g_debug("parsed as %s: %s", gtype_id, firmware_str); g_ptr_array_add(firmware_auto_types, g_strdup(gtype_id)); } firmware_type = fu_util_prompt_for_firmware_type(priv, firmware_auto_types, error); if (firmware_type == NULL) return FALSE; } else { firmware_type = g_strdup(values[1]); } gtype = fu_context_get_firmware_gtype_by_id(ctx, firmware_type); if (gtype == G_TYPE_INVALID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "GType %s not supported", firmware_type); return FALSE; } /* match the behavior of the daemon as we're printing the children */ priv->parse_flags |= FU_FIRMWARE_PARSE_FLAG_CACHE_STREAM; /* does firmware specify an internal size */ firmware = g_object_new(gtype, NULL); if (fu_firmware_has_flag(firmware, FU_FIRMWARE_FLAG_HAS_STORED_SIZE)) { g_autoptr(FuFirmware) firmware_linear = fu_linear_firmware_new(gtype); g_autoptr(GPtrArray) imgs = NULL; if (!fu_firmware_parse_stream(firmware_linear, stream, 0x0, priv->parse_flags, error)) return FALSE; imgs = fu_firmware_get_images(firmware_linear); if (imgs->len == 1) { g_set_object(&firmware, g_ptr_array_index(imgs, 0)); } else { g_set_object(&firmware, firmware_linear); } } else { if (!fu_firmware_parse_stream(firmware, stream, 0x0, priv->parse_flags, error)) return FALSE; } str = fu_firmware_to_string(firmware); fu_console_print_literal(priv->console, str); return TRUE; } static gboolean fu_util_firmware_export(FuUtilPrivate *priv, gchar **values, GError **error) { FuContext *ctx = fu_engine_get_context(priv->engine); FuFirmwareExportFlags flags = FU_FIRMWARE_EXPORT_FLAG_NONE; GType gtype; g_autoptr(FuFirmware) firmware = NULL; g_autoptr(GFile) file = NULL; g_autofree gchar *firmware_type = NULL; g_autofree gchar *str = NULL; /* check args */ if (g_strv_length(values) == 0 || g_strv_length(values) > 2) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments: filename required"); return FALSE; } if (g_strv_length(values) == 2) firmware_type = g_strdup(values[1]); /* load engine */ if (!fu_engine_load(priv->engine, FU_ENGINE_LOAD_FLAG_READONLY | FU_ENGINE_LOAD_FLAG_EXTERNAL_PLUGINS | FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS, priv->progress, error)) return FALSE; /* find the GType to use */ if (firmware_type == NULL) { g_autoptr(GPtrArray) firmware_types = fu_context_get_firmware_gtype_ids(ctx); firmware_type = fu_util_prompt_for_firmware_type(priv, firmware_types, error); } if (firmware_type == NULL) return FALSE; gtype = fu_context_get_firmware_gtype_by_id(ctx, firmware_type); if (gtype == G_TYPE_INVALID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "GType %s not supported", firmware_type); return FALSE; } firmware = g_object_new(gtype, NULL); file = g_file_new_for_path(values[0]); if (!fu_firmware_parse_file(firmware, file, priv->parse_flags, error)) return FALSE; if (priv->show_all) flags |= FU_FIRMWARE_EXPORT_FLAG_INCLUDE_DEBUG; str = fu_firmware_export_to_xml(firmware, flags, error); if (str == NULL) return FALSE; fu_console_print_literal(priv->console, str); return TRUE; } static gboolean fu_util_firmware_extract(FuUtilPrivate *priv, gchar **values, GError **error) { FuContext *ctx = fu_engine_get_context(priv->engine); GType gtype; g_autofree gchar *firmware_type = NULL; g_autofree gchar *str = NULL; g_autoptr(FuFirmware) firmware = NULL; g_autoptr(GFile) file = NULL; g_autoptr(GPtrArray) images = NULL; /* check args */ if (g_strv_length(values) == 0 || g_strv_length(values) > 2) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments: filename required"); return FALSE; } if (g_strv_length(values) == 2) firmware_type = g_strdup(values[1]); /* load engine */ if (!fu_engine_load(priv->engine, FU_ENGINE_LOAD_FLAG_READONLY | FU_ENGINE_LOAD_FLAG_EXTERNAL_PLUGINS | FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS, priv->progress, error)) return FALSE; /* find the GType to use */ if (firmware_type == NULL) { g_autoptr(GPtrArray) firmware_types = fu_context_get_firmware_gtype_ids(ctx); firmware_type = fu_util_prompt_for_firmware_type(priv, firmware_types, error); } if (firmware_type == NULL) return FALSE; gtype = fu_context_get_firmware_gtype_by_id(ctx, firmware_type); if (gtype == G_TYPE_INVALID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "GType %s not supported", firmware_type); return FALSE; } firmware = g_object_new(gtype, NULL); file = g_file_new_for_path(values[0]); if (!fu_firmware_parse_file(firmware, file, priv->parse_flags, error)) return FALSE; str = fu_firmware_to_string(firmware); fu_console_print_literal(priv->console, str); images = fu_firmware_get_images(firmware); for (guint i = 0; i < images->len; i++) { FuFirmware *img = g_ptr_array_index(images, i); g_autofree gchar *fn = NULL; g_autoptr(GBytes) blob_img = NULL; /* get raw image without generated header, footer or crc */ blob_img = fu_firmware_get_bytes(img, error); if (blob_img == NULL) return FALSE; if (g_bytes_get_size(blob_img) == 0) continue; /* use suitable filename */ if (fu_firmware_get_filename(img) != NULL) { fn = g_strdup(fu_firmware_get_filename(img)); } else if (fu_firmware_get_id(img) != NULL) { fn = g_strdup_printf("id-%s.fw", fu_firmware_get_id(img)); } else if (fu_firmware_get_idx(img) != 0x0) { fn = g_strdup_printf("idx-0x%x.fw", (guint)fu_firmware_get_idx(img)); } else { fn = g_strdup_printf("img-0x%x.fw", i); } /* TRANSLATORS: decompressing images from a container firmware */ fu_console_print(priv->console, "%s : %s", _("Writing file:"), fn); if (!fu_bytes_set_contents(fn, blob_img, error)) return FALSE; } /* success */ return TRUE; } static gboolean fu_util_firmware_build(FuUtilPrivate *priv, gchar **values, GError **error) { GType gtype = FU_TYPE_FIRMWARE; const gchar *tmp; g_autofree gchar *str = NULL; g_autoptr(FuFirmware) firmware = NULL; g_autoptr(FuFirmware) firmware_dst = NULL; g_autoptr(GBytes) blob_dst = NULL; g_autoptr(GBytes) blob_src = NULL; g_autoptr(XbBuilder) builder = xb_builder_new(); g_autoptr(XbBuilderSource) source = xb_builder_source_new(); g_autoptr(XbNode) n = NULL; g_autoptr(XbSilo) silo = NULL; /* check args */ if (g_strv_length(values) != 2) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments: filename required"); return FALSE; } /* load file */ blob_src = fu_bytes_get_contents(values[0], error); if (blob_src == NULL) return FALSE; /* load engine */ if (!fu_engine_load(priv->engine, FU_ENGINE_LOAD_FLAG_READONLY | FU_ENGINE_LOAD_FLAG_EXTERNAL_PLUGINS | FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS, priv->progress, error)) return FALSE; /* parse XML */ if (!xb_builder_source_load_bytes(source, blob_src, XB_BUILDER_SOURCE_FLAG_NONE, error)) { g_prefix_error(error, "could not parse XML: "); fwupd_error_convert(error); return FALSE; } xb_builder_import_source(builder, source); silo = xb_builder_compile(builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, error); if (silo == NULL) { fwupd_error_convert(error); return FALSE; } /* create FuFirmware of specific GType */ n = xb_silo_query_first(silo, "firmware", error); if (n == NULL) { fwupd_error_convert(error); return FALSE; } tmp = xb_node_get_attr(n, "gtype"); if (tmp != NULL) { gtype = g_type_from_name(tmp); if (gtype == G_TYPE_INVALID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "GType %s not registered", tmp); return FALSE; } } tmp = xb_node_get_attr(n, "id"); if (tmp != NULL) { gtype = fu_context_get_firmware_gtype_by_id(fu_engine_get_context(priv->engine), tmp); if (gtype == G_TYPE_INVALID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "GType %s not supported", tmp); return FALSE; } } firmware = g_object_new(gtype, NULL); if (!fu_firmware_build(firmware, n, error)) return FALSE; /* write new file */ blob_dst = fu_firmware_write(firmware, error); if (blob_dst == NULL) return FALSE; if (!fu_bytes_set_contents(values[1], blob_dst, error)) return FALSE; /* show what we wrote */ firmware_dst = g_object_new(gtype, NULL); if (!fu_firmware_parse_bytes(firmware_dst, blob_dst, 0x0, priv->parse_flags, error)) return FALSE; str = fu_firmware_to_string(firmware_dst); fu_console_print_literal(priv->console, str); /* success */ return TRUE; } static gboolean fu_util_firmware_convert(FuUtilPrivate *priv, gchar **values, GError **error) { FuContext *ctx = fu_engine_get_context(priv->engine); GType gtype_dst; GType gtype_src; g_autofree gchar *firmware_type_dst = NULL; g_autofree gchar *firmware_type_src = NULL; g_autofree gchar *str_dst = NULL; g_autofree gchar *str_src = NULL; g_autoptr(FuFirmware) firmware_dst = NULL; g_autoptr(FuFirmware) firmware_src = NULL; g_autoptr(GBytes) blob_dst = NULL; g_autoptr(GFile) file_src = NULL; g_autoptr(GPtrArray) images = NULL; /* check args */ if (g_strv_length(values) < 2 || g_strv_length(values) > 4) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments: filename required"); return FALSE; } if (g_strv_length(values) > 2) firmware_type_src = g_strdup(values[2]); if (g_strv_length(values) > 3) firmware_type_dst = g_strdup(values[3]); /* load engine */ if (!fu_engine_load(priv->engine, FU_ENGINE_LOAD_FLAG_READONLY | FU_ENGINE_LOAD_FLAG_EXTERNAL_PLUGINS | FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS, priv->progress, error)) return FALSE; /* find the GType to use */ if (firmware_type_src == NULL) { g_autoptr(GPtrArray) firmware_types = fu_context_get_firmware_gtype_ids(ctx); firmware_type_src = fu_util_prompt_for_firmware_type(priv, firmware_types, error); } if (firmware_type_src == NULL) return FALSE; if (firmware_type_dst == NULL) { g_autoptr(GPtrArray) firmware_types = fu_context_get_firmware_gtype_ids(ctx); firmware_type_dst = fu_util_prompt_for_firmware_type(priv, firmware_types, error); } if (firmware_type_dst == NULL) return FALSE; gtype_src = fu_context_get_firmware_gtype_by_id(ctx, firmware_type_src); if (gtype_src == G_TYPE_INVALID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "GType %s not supported", firmware_type_src); return FALSE; } firmware_src = g_object_new(gtype_src, NULL); file_src = g_file_new_for_path(values[0]); if (!fu_firmware_parse_file(firmware_src, file_src, priv->parse_flags, error)) return FALSE; gtype_dst = fu_context_get_firmware_gtype_by_id(ctx, firmware_type_dst); if (gtype_dst == G_TYPE_INVALID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "GType %s not supported", firmware_type_dst); return FALSE; } str_src = fu_firmware_to_string(firmware_src); fu_console_print_literal(priv->console, str_src); /* copy images */ firmware_dst = g_object_new(gtype_dst, NULL); images = fu_firmware_get_images(firmware_src); for (guint i = 0; i < images->len; i++) { FuFirmware *img = g_ptr_array_index(images, i); fu_firmware_add_image(firmware_dst, img); } /* copy data as fallback, preferring a binary blob to the export */ if (images->len == 0) { g_autoptr(GBytes) fw = NULL; g_autoptr(FuFirmware) img = NULL; fw = fu_firmware_get_bytes(firmware_src, NULL); if (fw == NULL) { fw = fu_firmware_write(firmware_src, error); if (fw == NULL) return FALSE; } img = fu_firmware_new_from_bytes(fw); fu_firmware_add_image(firmware_dst, img); } /* write new file */ blob_dst = fu_firmware_write(firmware_dst, error); if (blob_dst == NULL) return FALSE; if (!fu_bytes_set_contents(values[1], blob_dst, error)) return FALSE; str_dst = fu_firmware_to_string(firmware_dst); fu_console_print_literal(priv->console, str_dst); /* success */ return TRUE; } static GBytes * fu_util_hex_string_to_bytes(const gchar *val, GError **error) { gsize valsz; g_autoptr(GByteArray) buf = g_byte_array_new(); /* sanity check */ if (val == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "nothing to parse"); return NULL; } /* parse each hex byte */ valsz = strlen(val); for (guint i = 0; i < valsz; i += 2) { guint8 tmp = 0; if (!fu_firmware_strparse_uint8_safe(val, valsz, i, &tmp, error)) return NULL; fu_byte_array_append_uint8(buf, tmp); } return g_bytes_new(buf->data, buf->len); } static gboolean fu_util_firmware_patch(FuUtilPrivate *priv, gchar **values, GError **error) { FuContext *ctx = fu_engine_get_context(priv->engine); GType gtype; g_autofree gchar *firmware_type = NULL; g_autofree gchar *str = NULL; g_autoptr(FuFirmware) firmware = NULL; g_autoptr(GBytes) blob_dst = NULL; g_autoptr(GBytes) patch = NULL; g_autoptr(GFile) file_src = NULL; guint64 offset = 0; /* check args */ if (g_strv_length(values) != 3 && g_strv_length(values) != 4) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments, expected %s", "FILENAME OFFSET DATA [FIRMWARE-TYPE]"); return FALSE; } /* hardcoded */ if (g_strv_length(values) == 4) firmware_type = g_strdup(values[3]); /* parse offset */ if (!fu_strtoull(values[1], &offset, 0x0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) { g_prefix_error(error, "failed to parse offset: "); return FALSE; } /* parse blob */ patch = fu_util_hex_string_to_bytes(values[2], error); if (patch == NULL) return FALSE; if (g_bytes_get_size(patch) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "no data provided"); return FALSE; } /* load engine */ if (!fu_engine_load(priv->engine, FU_ENGINE_LOAD_FLAG_READONLY | FU_ENGINE_LOAD_FLAG_EXTERNAL_PLUGINS | FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS, priv->progress, error)) return FALSE; /* find the GType to use */ if (firmware_type == NULL) { g_autoptr(GPtrArray) firmware_types = fu_context_get_firmware_gtype_ids(ctx); firmware_type = fu_util_prompt_for_firmware_type(priv, firmware_types, error); } if (firmware_type == NULL) return FALSE; gtype = fu_context_get_firmware_gtype_by_id(ctx, firmware_type); if (gtype == G_TYPE_INVALID) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "GType %s not supported", firmware_type); return FALSE; } firmware = g_object_new(gtype, NULL); file_src = g_file_new_for_path(values[0]); if (!fu_firmware_parse_file(firmware, file_src, priv->parse_flags, error)) return FALSE; /* add patch */ fu_firmware_add_patch(firmware, offset, patch); /* write new file */ blob_dst = fu_firmware_write(firmware, error); if (blob_dst == NULL) return FALSE; if (!fu_bytes_set_contents(values[0], blob_dst, error)) return FALSE; str = fu_firmware_to_string(firmware); fu_console_print_literal(priv->console, str); /* success */ return TRUE; } static gboolean fu_util_verify_update(FuUtilPrivate *priv, gchar **values, GError **error) { g_autofree gchar *str = NULL; g_autoptr(FuDevice) dev = NULL; /* progress */ fu_progress_set_id(priv->progress, G_STRLOC); fu_progress_add_step(priv->progress, FWUPD_STATUS_LOADING, 50, "start-engine"); fu_progress_add_step(priv->progress, FWUPD_STATUS_DEVICE_VERIFY, 50, "verify-update"); /* load engine */ if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_DEVICE_HOTPLUG | FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_HWINFO, fu_progress_get_child(priv->progress), error)) return FALSE; fu_progress_step_done(priv->progress); /* get device */ priv->filter_device_include |= FWUPD_DEVICE_FLAG_UPDATABLE; if (g_strv_length(values) == 1) { dev = fu_util_get_device(priv, values[0], error); if (dev == NULL) return FALSE; } else { dev = fu_util_prompt_for_device(priv, NULL, error); if (dev == NULL) return FALSE; } /* add checksums */ if (!fu_engine_verify_update(priv->engine, fu_device_get_id(dev), fu_progress_get_child(priv->progress), error)) return FALSE; fu_progress_step_done(priv->progress); /* show checksums */ str = fu_device_to_string(dev); fu_console_print_literal(priv->console, str); return TRUE; } static gboolean fu_util_get_history(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(GPtrArray) devices = NULL; g_autoptr(FuUtilNode) root = g_node_new(NULL); /* load engine */ if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; /* get all devices from the history database */ devices = fu_engine_get_history(priv->engine, error); if (devices == NULL) return FALSE; /* not for human consumption */ if (priv->as_json) { g_autoptr(JsonBuilder) builder = json_builder_new(); json_builder_begin_object(builder); fwupd_codec_array_to_json(devices, "Devices", builder, FWUPD_CODEC_FLAG_TRUSTED); json_builder_end_object(builder); return fu_util_print_builder(priv->console, builder, error); } /* show each device */ for (guint i = 0; i < devices->len; i++) { g_autoptr(GPtrArray) rels = NULL; FwupdDevice *dev = g_ptr_array_index(devices, i); FwupdRelease *rel; const gchar *remote; FuUtilNode *child; g_autoptr(GError) error_local = NULL; if (!fwupd_device_match_flags(dev, priv->filter_device_include, priv->filter_device_exclude)) continue; child = g_node_append_data(root, g_object_ref(dev)); rel = fwupd_device_get_release_default(dev); if (rel == NULL) continue; remote = fwupd_release_get_remote_id(rel); /* doesn't actually map to remote */ if (remote == NULL) { g_node_append_data(child, g_object_ref(rel)); continue; } /* try to lookup releases from client, falling back to the history release */ rels = fu_engine_get_releases(priv->engine, priv->request, fwupd_device_get_id(dev), &error_local); if (rels == NULL) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { rels = g_ptr_array_new(); g_ptr_array_add(rels, fwupd_device_get_release_default(dev)); } else { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } /* map to a release in client */ for (guint j = 0; j < rels->len; j++) { FwupdRelease *rel2 = g_ptr_array_index(rels, j); if (!fwupd_release_match_flags(rel2, priv->filter_release_include, priv->filter_release_exclude)) continue; if (g_strcmp0(remote, fwupd_release_get_remote_id(rel2)) != 0) continue; if (g_strcmp0(fwupd_release_get_version(rel), fwupd_release_get_version(rel2)) != 0) continue; g_node_append_data(child, g_object_ref(rel2)); rel = NULL; break; } /* didn't match anything */ if (rels->len == 0 || rel != NULL) { g_node_append_data(child, g_object_ref(rel)); continue; } } fu_util_print_node(priv->console, priv->client, root); return TRUE; } static gboolean fu_util_refresh_remote(FuUtilPrivate *priv, FwupdRemote *remote, GError **error) { g_autofree gchar *uri_raw = NULL; g_autofree gchar *uri_sig = NULL; g_autoptr(GBytes) bytes_raw = NULL; g_autoptr(GBytes) bytes_sig = NULL; /* signature */ if (fwupd_remote_get_metadata_uri_sig(remote) == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "no metadata signature URI available for %s", fwupd_remote_get_id(remote)); return FALSE; } uri_sig = fwupd_remote_build_metadata_sig_uri(remote, error); if (uri_sig == NULL) return FALSE; bytes_sig = fwupd_client_download_bytes(priv->client, uri_sig, FWUPD_CLIENT_DOWNLOAD_FLAG_NONE, priv->cancellable, error); if (bytes_sig == NULL) return FALSE; if (!fwupd_remote_load_signature_bytes(remote, bytes_sig, error)) return FALSE; /* payload */ if (fwupd_remote_get_metadata_uri(remote) == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "no metadata URI available for %s", fwupd_remote_get_id(remote)); return FALSE; } uri_raw = fwupd_remote_build_metadata_uri(remote, error); if (uri_raw == NULL) return FALSE; bytes_raw = fwupd_client_download_bytes(priv->client, uri_raw, FWUPD_CLIENT_DOWNLOAD_FLAG_NONE, priv->cancellable, error); if (bytes_raw == NULL) return FALSE; /* send to daemon */ g_info("updating %s", fwupd_remote_get_id(remote)); return fu_engine_update_metadata_bytes(priv->engine, fwupd_remote_get_id(remote), bytes_raw, bytes_sig, error); } static gboolean fu_util_refresh(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(GPtrArray) remotes = NULL; /* load engine */ if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; /* download new metadata */ remotes = fu_engine_get_remotes(priv->engine, error); if (remotes == NULL) return FALSE; for (guint i = 0; i < remotes->len; i++) { FwupdRemote *remote = g_ptr_array_index(remotes, i); if (!fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_ENABLED)) continue; if (fwupd_remote_get_kind(remote) != FWUPD_REMOTE_KIND_DOWNLOAD) continue; if ((priv->flags & FWUPD_INSTALL_FLAG_FORCE) == 0 && !fwupd_remote_needs_refresh(remote)) { g_debug("skipping as remote %s age is %us", fwupd_remote_get_id(remote), (guint)fwupd_remote_get_age(remote)); continue; } if (!fu_util_refresh_remote(priv, remote, error)) return FALSE; } return TRUE; } static gboolean fu_util_get_remotes(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FuUtilNode) root = g_node_new(NULL); g_autoptr(GPtrArray) remotes = NULL; /* load engine */ if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_REMOTES, priv->progress, error)) return FALSE; /* list remotes */ remotes = fu_engine_get_remotes(priv->engine, error); if (remotes == NULL) return FALSE; if (remotes->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "no remotes available"); return FALSE; } if (priv->as_json) { g_autoptr(JsonBuilder) builder = json_builder_new(); json_builder_begin_object(builder); fwupd_codec_array_to_json(remotes, "Remotes", builder, FWUPD_CODEC_FLAG_TRUSTED); json_builder_end_object(builder); return fu_util_print_builder(priv->console, builder, error); } for (guint i = 0; i < remotes->len; i++) { FwupdRemote *remote_tmp = g_ptr_array_index(remotes, i); g_node_append_data(root, g_object_ref(remote_tmp)); } fu_util_print_node(priv->console, priv->client, root); return TRUE; } static gboolean fu_util_security(FuUtilPrivate *priv, gchar **values, GError **error) { FuSecurityAttrToStringFlags flags = FU_SECURITY_ATTR_TO_STRING_FLAG_NONE; const gchar *fwupd_version = NULL; g_autoptr(FuSecurityAttrs) attrs = NULL; g_autoptr(FuSecurityAttrs) events = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(GPtrArray) items = NULL; g_autoptr(GPtrArray) events_array = NULL; g_autofree gchar *str = NULL; g_autofree gchar *host_security_id = NULL; #ifndef HAVE_HSI g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, /* TRANSLATORS: error message for unsupported feature */ _("Host Security ID (HSI) is not supported")); return FALSE; #endif /* HAVE_HSI */ /* optionally restrict by version */ if (g_strv_length(values) > 0) fwupd_version = values[0]; if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; /* show or hide different elements */ if (priv->show_all) { flags |= FU_SECURITY_ATTR_TO_STRING_FLAG_SHOW_OBSOLETES; flags |= FU_SECURITY_ATTR_TO_STRING_FLAG_SHOW_URLS; } attrs = fu_engine_get_host_security_attrs(priv->engine); items = fu_security_attrs_get_all(attrs, fwupd_version); /* print the "why" */ if (priv->as_json) { str = fwupd_codec_to_json_string(FWUPD_CODEC(attrs), FWUPD_CODEC_FLAG_NONE, error); if (str == NULL) return FALSE; fu_console_print_literal(priv->console, str); return TRUE; } host_security_id = fu_engine_get_host_security_id(priv->engine, fwupd_version); fu_console_print(priv->console, "%s \033[1m%s\033[0m", /* TRANSLATORS: this is a string like 'HSI:2-U' */ _("Host Security ID:"), host_security_id); str = fu_util_security_attrs_to_string(items, flags); fu_console_print_literal(priv->console, str); /* print the "when" */ events = fu_engine_get_host_security_events(priv->engine, 10, error); if (events == NULL) return FALSE; events_array = fu_security_attrs_get_all(events, fwupd_version); if (events_array->len > 0) { g_autofree gchar *estr = fu_util_security_events_to_string(events_array, flags); if (estr != NULL) fu_console_print_literal(priv->console, estr); } /* print the "also" */ devices = fu_engine_get_devices(priv->engine, error); if (devices == NULL) return FALSE; if (devices->len > 0) { g_autofree gchar *estr = fu_util_security_issues_to_string(devices); if (estr != NULL) fu_console_print_literal(priv->console, estr); } /* success */ return TRUE; } static FuVolume * fu_util_prompt_for_volume(FuUtilPrivate *priv, GError **error) { FuContext *ctx = fu_engine_get_context(priv->engine); FuVolume *volume; guint idx; g_autoptr(GPtrArray) volumes = NULL; /* exactly one */ volumes = fu_context_get_esp_volumes(ctx, error); if (volumes == NULL) return NULL; if (volumes->len == 1) { volume = g_ptr_array_index(volumes, 0); if (fu_volume_get_id(volume) != NULL) { fu_console_print(priv->console, "%s: %s", /* TRANSLATORS: Volume has been chosen by the user */ _("Selected volume"), fu_volume_get_id(volume)); } return g_object_ref(volume); } /* TRANSLATORS: this is to abort the interactive prompt */ fu_console_print(priv->console, "0.\t%s", _("Cancel")); for (guint i = 0; i < volumes->len; i++) { volume = g_ptr_array_index(volumes, i); fu_console_print(priv->console, "%u.\t%s", i + 1, fu_volume_get_id(volume)); } /* TRANSLATORS: get interactive prompt */ idx = fu_console_input_uint(priv->console, volumes->len, "%s", _("Choose volume")); if (idx == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "Request canceled"); return NULL; } volume = g_ptr_array_index(volumes, idx - 1); return g_object_ref(volume); } static gboolean fu_util_esp_mount(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FuVolume) volume = NULL; volume = fu_util_prompt_for_volume(priv, error); if (volume == NULL) return FALSE; return fu_volume_mount(volume, error); } static gboolean fu_util_esp_unmount(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FuVolume) volume = NULL; volume = fu_util_prompt_for_volume(priv, error); if (volume == NULL) return FALSE; return fu_volume_unmount(volume, error); } static gboolean fu_util_esp_list_as_json(FuUtilPrivate *priv, GError **error) { g_autoptr(JsonBuilder) builder = json_builder_new(); g_autoptr(GPtrArray) volumes = NULL; volumes = fu_context_get_esp_volumes(fu_engine_get_context(priv->engine), error); if (volumes == NULL) return FALSE; json_builder_begin_object(builder); fwupd_codec_array_to_json(volumes, "Volumes", builder, FWUPD_CODEC_FLAG_TRUSTED); json_builder_end_object(builder); return fu_util_print_builder(priv->console, builder, error); } static gboolean fu_util_esp_list(FuUtilPrivate *priv, gchar **values, GError **error) { g_autofree gchar *mount_point = NULL; g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(FuVolume) volume = NULL; g_autoptr(GPtrArray) files = NULL; if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; if (priv->as_json) return fu_util_esp_list_as_json(priv, error); volume = fu_util_prompt_for_volume(priv, error); if (volume == NULL) return FALSE; locker = fu_volume_locker(volume, error); if (locker == NULL) return FALSE; mount_point = fu_volume_get_mount_point(volume); if (mount_point == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no mountpoint for ESP"); return FALSE; } files = fu_path_get_files(mount_point, error); if (files == NULL) return FALSE; for (guint i = 0; i < files->len; i++) { const gchar *fn = g_ptr_array_index(files, i); fu_console_print_literal(priv->console, fn); } return TRUE; } static gboolean fu_util_modify_tag(FuUtilPrivate *priv, gchar **values, gboolean enable, GError **error) { g_autoptr(FuDevice) dev = NULL; const gchar *tag = enable ? "emulation-tag" : "~emulation-tag"; if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; /* set the flag */ priv->filter_device_include |= FWUPD_DEVICE_FLAG_CAN_EMULATION_TAG; if (g_strv_length(values) >= 1) { dev = fu_util_get_device(priv, values[0], error); if (dev == NULL) return FALSE; } else { dev = fu_util_prompt_for_device(priv, NULL, error); if (dev == NULL) return FALSE; } return fu_engine_modify_device(priv->engine, fu_device_get_id(dev), "Flags", tag, error); } static gboolean fu_util_emulation_tag(FuUtilPrivate *priv, gchar **values, GError **error) { return fu_util_modify_tag(priv, values, TRUE, error); } static gboolean fu_util_emulation_untag(FuUtilPrivate *priv, gchar **values, GError **error) { return fu_util_modify_tag(priv, values, FALSE, error); } static gboolean fu_util_emulation_load(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(GInputStream) stream = NULL; /* check args */ if (g_strv_length(values) < 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments, expected EMULATION-FILE [ARCHIVE-FILE]"); return FALSE; } /* progress */ fu_progress_set_id(priv->progress, G_STRLOC); fu_progress_add_step(priv->progress, FWUPD_STATUS_LOADING, 95, "start-engine"); fu_progress_add_step(priv->progress, FWUPD_STATUS_LOADING, 5, "load-emulation"); fu_progress_add_step(priv->progress, FWUPD_STATUS_DEVICE_WRITE, 5, "write"); /* load engine */ if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_HWINFO, fu_progress_get_child(priv->progress), error)) return FALSE; fu_progress_step_done(priv->progress); /* load emulation */ stream = fu_input_stream_from_path(values[0], error); if (stream == NULL) return FALSE; if (!fu_engine_emulation_load(priv->engine, stream, error)) return FALSE; fu_progress_step_done(priv->progress); /* "install" archive */ if (values[1] != NULL) { g_autoptr(GInputStream) stream_cab = NULL; g_autoptr(GPtrArray) devices_possible = NULL; stream_cab = fu_input_stream_from_path(values[1], error); if (stream_cab == NULL) return FALSE; devices_possible = fu_engine_get_devices(priv->engine, error); if (devices_possible == NULL) return FALSE; if (!fu_util_install_stream(priv, stream_cab, devices_possible, fu_progress_get_child(priv->progress), error)) return FALSE; } fu_progress_step_done(priv->progress); /* success */ return TRUE; } static gboolean _g_str_equal0(gconstpointer str1, gconstpointer str2) { return g_strcmp0(str1, str2) == 0; } static gboolean fu_util_switch_branch(FuUtilPrivate *priv, gchar **values, GError **error) { const gchar *branch; g_autoptr(FwupdRelease) rel = NULL; g_autoptr(GPtrArray) rels = NULL; g_autoptr(GPtrArray) branches = g_ptr_array_new_with_free_func(g_free); g_autoptr(FuDevice) dev = NULL; /* load engine */ if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_DEVICE_HOTPLUG | FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; /* find the device and check it has multiple branches */ priv->filter_device_include |= FWUPD_DEVICE_FLAG_HAS_MULTIPLE_BRANCHES; priv->filter_device_include |= FWUPD_DEVICE_FLAG_UPDATABLE; if (g_strv_length(values) == 1) dev = fu_util_get_device(priv, values[1], error); else dev = fu_util_prompt_for_device(priv, NULL, error); if (dev == NULL) return FALSE; if (!fu_device_has_flag(dev, FWUPD_DEVICE_FLAG_HAS_MULTIPLE_BRANCHES)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Multiple branches not available"); return FALSE; } /* get all releases, including the alternate branch versions */ rels = fu_engine_get_releases(priv->engine, priv->request, fu_device_get_id(dev), error); if (rels == NULL) return FALSE; /* get all the unique branches */ for (guint i = 0; i < rels->len; i++) { FwupdRelease *rel_tmp = g_ptr_array_index(rels, i); const gchar *branch_tmp = fwupd_release_get_branch(rel_tmp); if (!fwupd_release_match_flags(rel_tmp, priv->filter_release_include, priv->filter_release_exclude)) continue; if (g_ptr_array_find_with_equal_func(branches, branch_tmp, _g_str_equal0, NULL)) continue; g_ptr_array_add(branches, g_strdup(branch_tmp)); } /* branch name is optional */ if (g_strv_length(values) > 1) { branch = values[1]; } else if (branches->len == 1) { branch = g_ptr_array_index(branches, 0); } else { guint idx; /* TRANSLATORS: this is to abort the interactive prompt */ fu_console_print(priv->console, "0.\t%s", _("Cancel")); for (guint i = 0; i < branches->len; i++) { const gchar *branch_tmp = g_ptr_array_index(branches, i); fu_console_print(priv->console, "%u.\t%s", i + 1, fu_util_branch_for_display(branch_tmp)); } /* TRANSLATORS: get interactive prompt, where branch is the * supplier of the firmware, e.g. "non-free" or "free" */ idx = fu_console_input_uint(priv->console, branches->len, "%s", _("Choose branch")); if (idx == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "Request canceled"); return FALSE; } branch = g_ptr_array_index(branches, idx - 1); } /* sanity check */ if (g_strcmp0(branch, fu_device_get_branch(dev)) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Device %s is already on branch %s", fu_device_get_name(dev), fu_util_branch_for_display(branch)); return FALSE; } /* the releases are ordered by version */ for (guint j = 0; j < rels->len; j++) { FwupdRelease *rel_tmp = g_ptr_array_index(rels, j); if (g_strcmp0(fwupd_release_get_branch(rel_tmp), branch) == 0) { rel = g_object_ref(rel_tmp); break; } } if (rel == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "No releases for branch %s", fu_util_branch_for_display(branch)); return FALSE; } /* we're switching branch */ if (!fu_util_switch_branch_warning(priv->console, FWUPD_DEVICE(dev), rel, FALSE, error)) return FALSE; /* update the console if composite devices are also updated */ priv->current_operation = FU_UTIL_OPERATION_INSTALL; g_signal_connect(FU_ENGINE(priv->engine), "device-changed", G_CALLBACK(fu_util_update_device_changed_cb), priv); priv->flags |= FWUPD_INSTALL_FLAG_ALLOW_REINSTALL; priv->flags |= FWUPD_INSTALL_FLAG_ALLOW_BRANCH_SWITCH; if (!fu_util_install_release(priv, FWUPD_DEVICE(dev), rel, error)) return FALSE; fu_util_display_current_message(priv); /* we don't want to ask anything */ if (priv->no_reboot_check) { g_debug("skipping reboot check"); return TRUE; } return fu_util_prompt_complete(priv->console, priv->completion_flags, TRUE, error); } static gboolean fu_util_set_bios_setting(FuUtilPrivate *priv, gchar **input, GError **error) { g_autoptr(GHashTable) settings = fu_util_bios_settings_parse_argv(input, error); if (settings == NULL) return FALSE; if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; if (!fu_engine_modify_bios_settings(priv->engine, settings, FALSE, error)) { if (!g_error_matches(*error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) g_prefix_error(error, "failed to set BIOS setting: "); return FALSE; } if (!priv->as_json) { gpointer key, value; GHashTableIter iter; g_hash_table_iter_init(&iter, settings); while (g_hash_table_iter_next(&iter, &key, &value)) { g_autofree gchar *msg = /* TRANSLATORS: Configured a BIOS setting to a value */ g_strdup_printf(_("Set BIOS setting '%s' using '%s'."), (const gchar *)key, (const gchar *)value); fu_console_print_literal(priv->console, msg); } } priv->completion_flags |= FWUPD_DEVICE_FLAG_NEEDS_REBOOT; if (priv->no_reboot_check) { g_debug("skipping reboot check"); return TRUE; } return fu_util_prompt_complete(priv->console, priv->completion_flags, TRUE, error); } static gboolean fu_util_security_fix(FuUtilPrivate *priv, gchar **values, GError **error) { #ifndef HAVE_HSI g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, /* TRANSLATORS: error message for unsupported feature */ _("Host Security ID (HSI) is not supported")); return FALSE; #endif /* HAVE_HSI */ if (g_strv_length(values) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, /* TRANSLATOR: This is the error message for * incorrect parameter */ _("Invalid arguments, expected an AppStream ID")); return FALSE; } if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_EXTERNAL_PLUGINS | FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS | FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; if (!fu_engine_fix_host_security_attr(priv->engine, values[0], error)) return FALSE; /* TRANSLATORS: we've fixed a security problem on the machine */ fu_console_print_literal(priv->console, _("Fixed successfully")); return TRUE; } static gboolean fu_util_security_undo(FuUtilPrivate *priv, gchar **values, GError **error) { #ifndef HAVE_HSI g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, /* TRANSLATORS: error message for unsupported feature */ _("Host Security ID (HSI) is not supported")); return FALSE; #endif /* HAVE_HSI */ if (g_strv_length(values) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, /* TRANSLATOR: This is the error message for * incorrect parameter */ _("Invalid arguments, expected an AppStream ID")); return FALSE; } if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_REMOTES | FU_ENGINE_LOAD_FLAG_EXTERNAL_PLUGINS | FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS | FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; if (!fu_engine_undo_host_security_attr(priv->engine, values[0], error)) return FALSE; /* TRANSLATORS: we've fixed a security problem on the machine */ fu_console_print_literal(priv->console, _("Fix reverted successfully")); return TRUE; } static gboolean fu_util_get_bios_setting(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FuBiosSettings) attrs = NULL; g_autoptr(GPtrArray) items = NULL; FuContext *ctx = fu_engine_get_context(priv->engine); gboolean found = FALSE; /* load engine */ if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; attrs = fu_context_get_bios_settings(ctx); items = fu_bios_settings_get_all(attrs); if (priv->as_json) return fu_util_bios_setting_console_print(priv->console, values, items, error); for (guint i = 0; i < items->len; i++) { FwupdBiosSetting *attr = g_ptr_array_index(items, i); if (fu_util_bios_setting_matches_args(attr, values)) { g_autofree gchar *tmp = fu_util_bios_setting_to_string(attr, 0); fu_console_print_literal(priv->console, tmp); found = TRUE; } } if (items->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, /* TRANSLATORS: error message */ _("This system doesn't support firmware settings")); return FALSE; } if (!found) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "%s: '%s'", /* TRANSLATORS: error message */ _("Unable to find attribute"), values[0]); return FALSE; } return TRUE; } static gboolean fu_util_reboot_cleanup(FuUtilPrivate *priv, gchar **values, GError **error) { FuPlugin *plugin; g_autoptr(FuDevice) device = NULL; if (!fu_util_start_engine(priv, FU_ENGINE_LOAD_FLAG_COLDPLUG | FU_ENGINE_LOAD_FLAG_DEVICE_HOTPLUG | FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; /* both arguments are optional */ if (g_strv_length(values) >= 1) { device = fu_engine_get_device(priv->engine, values[1], error); if (device == NULL) return FALSE; } else { device = fu_util_prompt_for_device(priv, NULL, error); if (device == NULL) return FALSE; } plugin = fu_engine_get_plugin_by_name(priv->engine, fu_device_get_plugin(device), error); if (plugin == NULL) return FALSE; return fu_plugin_runner_reboot_cleanup(plugin, device, error); } static gboolean fu_util_efiboot_info_as_json(FuUtilPrivate *priv, GPtrArray *entries, GError **error) { FuEfivars *efivars = fu_context_get_efivars(priv->ctx); guint16 idx = 0; g_autoptr(JsonBuilder) builder = json_builder_new(); json_builder_begin_object(builder); if (fu_efivars_get_boot_current(efivars, &idx, NULL)) fwupd_codec_json_append_int(builder, "BootCurrent", idx); if (fu_efivars_get_boot_next(efivars, &idx, NULL)) fwupd_codec_json_append_int(builder, "BootNext", idx); json_builder_set_member_name(builder, "Entries"); json_builder_begin_object(builder); for (guint i = 0; i < entries->len; i++) { FuEfiLoadOption *entry = g_ptr_array_index(entries, i); g_autofree gchar *title = g_strdup_printf("Boot%04X", (guint)fu_firmware_get_idx(FU_FIRMWARE(entry))); json_builder_set_member_name(builder, title); json_builder_begin_array(builder); json_builder_begin_object(builder); fwupd_codec_to_json(FWUPD_CODEC(entry), builder, FWUPD_CODEC_FLAG_TRUSTED); json_builder_end_object(builder); json_builder_end_array(builder); } json_builder_end_object(builder); json_builder_end_object(builder); return fu_util_print_builder(priv->console, builder, error); } static gboolean fu_util_efiboot_next(FuUtilPrivate *priv, gchar **values, GError **error) { FuEfivars *efivars = fu_context_get_efivars(priv->ctx); guint64 value = 0; /* just show */ if (values[0] == NULL) { guint16 idx = 0; if (!fu_efivars_get_boot_next(efivars, &idx, error)) return FALSE; fu_console_print(priv->console, "Boot%04X", idx); return TRUE; } /* modify */ if (!fu_strtoull(values[0], &value, 0x0, G_MAXUINT16, FU_INTEGER_BASE_16, error)) return FALSE; return fu_efivars_set_boot_next(efivars, (guint16)value, error); } static gboolean fu_util_efiboot_order(FuUtilPrivate *priv, gchar **values, GError **error) { FuEfivars *efivars = fu_context_get_efivars(priv->ctx); g_auto(GStrv) split = NULL; g_autoptr(GArray) order = NULL; /* just show */ if (values[0] == NULL) { order = fu_efivars_get_boot_order(efivars, error); if (order == NULL) return FALSE; for (guint i = 0; i < order->len; i++) { guint16 idx = g_array_index(order, guint16, i); fu_console_print(priv->console, "Boot%04X", idx); } return TRUE; } /* modify */ order = g_array_new(FALSE, FALSE, sizeof(guint16)); split = g_strsplit(values[0], ",", -1); for (guint i = 0; split[i] != NULL; i++) { guint64 value = 0; guint16 value_as_u16; if (!fu_strtoull(split[i], &value, 0x0, G_MAXUINT16, FU_INTEGER_BASE_16, error)) return FALSE; value_as_u16 = (guint16)value; g_array_append_val(order, value_as_u16); } return fu_efivars_set_boot_order(efivars, order, error); } static gboolean fu_util_efiboot_create(FuUtilPrivate *priv, gchar **values, GError **error) { FuEfivars *efivars = fu_context_get_efivars(priv->ctx); g_autoptr(FuVolume) volume = NULL; guint64 idx = 0; /* check args */ if (g_strv_length(values) < 3) { g_set_error_literal( error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, /* TRANSLATORS: error message */ _("Invalid arguments, expected INDEX NAME TARGET [MOUNTPOINT]")); return FALSE; } /* check the index does not already exist */ if (!fu_strtoull(values[0], &idx, 0x0, G_MAXUINT16, FU_INTEGER_BASE_16, error)) return FALSE; if ((priv->flags & FWUPD_INSTALL_FLAG_FORCE) == 0) { g_autoptr(GBytes) blob = fu_efivars_get_boot_data(efivars, (guint16)idx, NULL); if (blob != NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, /* TRANSLATORS: error message */ _("Already exists, and no --force specified")); return FALSE; } } /* get volume */ if (values[3] == NULL) { volume = fu_util_prompt_for_volume(priv, error); if (volume == NULL) return FALSE; } else { g_autoptr(GPtrArray) volumes = NULL; volumes = fu_context_get_esp_volumes(priv->ctx, error); if (volumes == NULL) return FALSE; for (guint i = 0; i < volumes->len; i++) { FuVolume *volume_tmp = g_ptr_array_index(volumes, i); g_autofree gchar *mount_point = fu_volume_get_mount_point(volume_tmp); if (g_strcmp0(mount_point, values[3]) == 0) { volume = g_object_ref(volume_tmp); break; } } if (volume == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, /* TRANSLATORS: error message */ _("No volume matched %s"), values[3]); return FALSE; } } return fu_efivars_create_boot_entry_for_volume(efivars, (guint16)idx, volume, values[1], values[2], error); } static gboolean fu_util_efiboot_delete(FuUtilPrivate *priv, gchar **values, GError **error) { FuEfivars *efivars = fu_context_get_efivars(priv->ctx); guint64 value = 0; if (values[0] == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, /* TRANSLATORS: error message */ _("Invalid arguments, expected base-16 integer")); return FALSE; } if (!fu_strtoull(values[0], &value, 0x0, G_MAXUINT16, FU_INTEGER_BASE_16, error)) return FALSE; /* success */ return fu_efivars_set_boot_data(efivars, (guint16)value, NULL, error); } static gboolean fu_util_efiboot_hive_check_loadopt_is_shim(FuEfiLoadOption *loadopt, GError **error) { gboolean seen_shim = FALSE; g_autoptr(FuFirmware) firmware = NULL; g_autoptr(GPtrArray) dps = NULL; /* get FuEfiDevicePathList */ firmware = fu_firmware_get_image_by_idx(FU_FIRMWARE(loadopt), 0x0, error); if (firmware == NULL) return FALSE; dps = fu_firmware_get_images(firmware); for (guint i = 0; i < dps->len; i++) { FuFirmware *dp = g_ptr_array_index(dps, i); if (FU_IS_EFI_FILE_PATH_DEVICE_PATH(dp)) { g_autofree gchar *name = fu_efi_file_path_device_path_get_name(FU_EFI_FILE_PATH_DEVICE_PATH(dp), error); if (name == NULL) return FALSE; if (g_pattern_match_simple("*shim*.efi", name)) { seen_shim = TRUE; break; } } } if (!seen_shim) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Only the shim bootloader supports the hive format"); return FALSE; } /* success */ return TRUE; } static gboolean fu_util_efiboot_hive(FuUtilPrivate *priv, gchar **values, GError **error) { FuEfivars *efivars = fu_context_get_efivars(priv->ctx); g_autoptr(FuEfiLoadOption) loadopt = NULL; guint64 idx = 0; /* check args */ if (g_strv_length(values) < 2) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, /* TRANSLATORS: error message */ _("Invalid arguments, expected INDEX KEY [VALUE]")); return FALSE; } /* load the boot entry */ if (!fu_strtoull(values[0], &idx, 0x0, G_MAXUINT16, FU_INTEGER_BASE_16, error)) return FALSE; loadopt = fu_efivars_get_boot_entry(efivars, (guint16)idx, error); if (loadopt == NULL) return FALSE; /* get value */ if (values[2] == NULL) { const gchar *value; fu_console_print_full(priv->console, FU_CONSOLE_PRINT_FLAG_WARNING, "%s\n", /* TRANSLATORS: try to treat the legacy format as a hive */ _("The EFI boot entry was not in hive format, falling back")); value = fu_efi_load_option_get_metadata(loadopt, values[1], error); if (value == NULL) return FALSE; fu_console_print_literal(priv->console, value); return TRUE; } /* check this is actually shim */ if (!fu_util_efiboot_hive_check_loadopt_is_shim(loadopt, error)) return FALSE; /* change the format if required */ if (fu_efi_load_option_get_kind(loadopt) != FU_EFI_LOAD_OPTION_KIND_HIVE) { fu_console_print_full(priv->console, FU_CONSOLE_PRINT_FLAG_WARNING, "%s\n", /* TRANSLATORS: the boot entry was in a legacy format */ _("The EFI boot entry is not in hive format, " "and shim may not be new enough to read it.")); if ((priv->flags & FWUPD_INSTALL_FLAG_FORCE) == 0 && !fu_console_input_bool(priv->console, FALSE, "%s", /* TRANSLATORS: ask the user if it's okay to convert, * "it" being the data contained in the EFI boot entry */ _("Do you want to convert it now?"))) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_PERMISSION_DENIED, "User declined action"); return FALSE; } fu_efi_load_option_set_kind(loadopt, FU_EFI_LOAD_OPTION_KIND_HIVE); } /* set value */ fu_efi_load_option_set_metadata(loadopt, values[1], values[2]); return fu_efivars_set_boot_entry(efivars, (guint16)idx, loadopt, error); } static gboolean fu_util_efiboot_info(FuUtilPrivate *priv, gchar **values, GError **error) { FuEfivars *efivars = fu_context_get_efivars(priv->ctx); g_autoptr(GPtrArray) entries = NULL; g_autoptr(GString) str = g_string_new(NULL); guint16 idx = 0; entries = fu_efivars_get_boot_entries(efivars, error); if (entries == NULL) return FALSE; /* dump to the screen in the most appropriate format */ if (priv->as_json) return fu_util_efiboot_info_as_json(priv, entries, error); if (fu_efivars_get_boot_current(efivars, &idx, NULL)) fwupd_codec_string_append_hex(str, 0, "BootCurrent", idx); if (fu_efivars_get_boot_next(efivars, &idx, NULL)) fwupd_codec_string_append_hex(str, 0, "BootNext", idx); for (guint i = 0; i < entries->len; i++) { FuEfiLoadOption *entry = g_ptr_array_index(entries, i); g_autofree gchar *title = g_strdup_printf("Boot%04X", (guint)fu_firmware_get_idx(FU_FIRMWARE(entry))); fwupd_codec_string_append(str, 0, title, ""); fwupd_codec_add_string(FWUPD_CODEC(entry), 1, str); } /* success */ fu_console_print_literal(priv->console, str->str); return TRUE; } static gboolean fu_util_efivar_files_as_json(FuUtilPrivate *priv, GPtrArray *files, GError **error) { g_autoptr(JsonBuilder) builder = json_builder_new(); g_autoptr(GHashTable) hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_ptr_array_unref); GHashTableIter iter; gpointer key, value; /* convert an array of FuPeFirmware to a map with the BootXXXX ID as the hash key and the * filename as an array */ for (guint i = 0; i < files->len; i++) { FuFirmware *firmware = g_ptr_array_index(files, i); GPtrArray *array; g_autofree gchar *name = NULL; name = g_strdup_printf("Boot%04X", (guint)fu_firmware_get_idx(firmware)); array = g_hash_table_lookup(hash, name); if (array == NULL) { array = g_ptr_array_new_with_free_func(g_free); g_hash_table_insert(hash, g_steal_pointer(&name), array); } g_ptr_array_add(array, g_strdup(fu_firmware_get_filename(firmware))); } /* export */ json_builder_begin_object(builder); g_hash_table_iter_init(&iter, hash); while (g_hash_table_iter_next(&iter, &key, &value)) { const gchar *bootvar = (const gchar *)key; GPtrArray *array = (GPtrArray *)value; json_builder_set_member_name(builder, bootvar); json_builder_begin_array(builder); for (guint i = 0; i < array->len; i++) { const gchar *filename = g_ptr_array_index(array, i); json_builder_add_string_value(builder, filename); } json_builder_end_array(builder); } json_builder_end_object(builder); return fu_util_print_builder(priv->console, builder, error); } static gboolean fu_util_efivar_files(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(GPtrArray) files = NULL; files = fu_context_get_esp_files(priv->ctx, FU_CONTEXT_ESP_FILE_FLAG_INCLUDE_FIRST_STAGE | FU_CONTEXT_ESP_FILE_FLAG_INCLUDE_SECOND_STAGE | FU_CONTEXT_ESP_FILE_FLAG_INCLUDE_REVOCATIONS, error); if (files == NULL) return FALSE; if (priv->as_json) return fu_util_efivar_files_as_json(priv, files, error); for (guint i = 0; i < files->len; i++) { FuFirmware *firmware = g_ptr_array_index(files, i); g_autofree gchar *name = g_strdup_printf("Boot%04X", (guint)fu_firmware_get_idx(firmware)); fu_console_print(priv->console, "%s → %s", name, fu_firmware_get_filename(firmware)); } /* success */ return TRUE; } static gboolean fu_util_efivar_list(FuUtilPrivate *priv, gchar **values, GError **error) { FuEfivars *efivars = fu_context_get_efivars(priv->ctx); g_autoptr(GPtrArray) names = NULL; /* sanity check */ if (g_strv_length(values) < 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, /* TRANSLATORS: error message */ _("Invalid arguments, expected GUID")); return FALSE; } names = fu_efivars_get_names(efivars, values[0], error); if (names == NULL) return FALSE; for (guint i = 0; i < names->len; i++) { const gchar *name = g_ptr_array_index(names, i); fu_console_print(priv->console, "name: %s", name); } /* success */ return TRUE; } static gboolean fu_util_build_cabinet(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(GBytes) cab_blob = NULL; g_autoptr(FuCabinet) cab_file = fu_cabinet_new(); /* sanity check */ if (g_strv_length(values) < 3) { g_set_error_literal( error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, /* TRANSLATORS: error message */ _("Invalid arguments, expected at least ARCHIVE FIRMWARE METAINFO")); return FALSE; } /* file already exists */ if ((priv->flags & FWUPD_INSTALL_FLAG_FORCE) == 0 && g_file_test(values[0], G_FILE_TEST_EXISTS)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Filename already exists"); return FALSE; } /* add each file */ for (guint i = 1; values[i] != NULL; i++) { g_autoptr(GBytes) blob = NULL; g_autofree gchar *basename = g_path_get_basename(values[i]); blob = fu_bytes_get_contents(values[i], error); if (blob == NULL) return FALSE; if (g_bytes_get_size(blob) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "%s has zero size", values[i]); return FALSE; } fu_cabinet_add_file(cab_file, basename, blob); } /* export */ cab_blob = fu_firmware_write(FU_FIRMWARE(cab_file), error); if (cab_blob == NULL) return FALSE; /* sanity check JCat and XML MetaInfo files */ if (!fu_firmware_parse_bytes(FU_FIRMWARE(cab_file), cab_blob, 0x0, FU_FIRMWARE_PARSE_FLAG_NONE, error)) return FALSE; return fu_bytes_set_contents(values[0], cab_blob, error); } static gboolean fu_util_version(FuUtilPrivate *priv, GError **error) { g_autoptr(GHashTable) metadata = NULL; g_autofree gchar *str = NULL; /* load engine */ if (!fu_util_start_engine( priv, FU_ENGINE_LOAD_FLAG_READONLY | FU_ENGINE_LOAD_FLAG_EXTERNAL_PLUGINS | FU_ENGINE_LOAD_FLAG_BUILTIN_PLUGINS | FU_ENGINE_LOAD_FLAG_HWINFO, priv->progress, error)) return FALSE; /* get metadata */ metadata = fu_engine_get_report_metadata(priv->engine, error); if (metadata == NULL) return FALSE; /* dump to the screen in the most appropriate format */ if (priv->as_json) return fu_util_project_versions_as_json(priv->console, metadata, error); str = fu_util_project_versions_to_string(metadata); fu_console_print_literal(priv->console, str); return TRUE; } static gboolean fu_util_clear_history(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FuHistory) history = fu_history_new(priv->ctx); return fu_history_remove_all(history, error); } static gboolean fu_util_setup_interactive(FuUtilPrivate *priv, GError **error) { if (priv->as_json) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "using --json"); return FALSE; } return fu_console_setup(priv->console, error); } static void fu_util_print_error(FuUtilPrivate *priv, const GError *error) { if (priv->as_json) { fu_util_print_error_as_json(priv->console, error); return; } fu_console_print_full(priv->console, FU_CONSOLE_PRINT_FLAG_STDERR, "%s\n", error->message); } int main(int argc, char *argv[]) { /* nocheck:lines */ gboolean allow_branch_switch = FALSE; gboolean allow_older = FALSE; gboolean allow_reinstall = FALSE; gboolean force = FALSE; gboolean no_search = FALSE; gboolean ret; gboolean version = FALSE; gboolean ignore_checksum = FALSE; gboolean ignore_requirements = FALSE; gboolean ignore_vid_pid = FALSE; g_auto(GStrv) plugin_glob = NULL; g_autoptr(FuUtilPrivate) priv = g_new0(FuUtilPrivate, 1); g_autoptr(GError) error_console = NULL; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) cmd_array = fu_util_cmd_array_new(); g_autofree gchar *cmd_descriptions = NULL; g_autofree gchar *filter_device = NULL; g_autofree gchar *filter_release = NULL; const GOptionEntry options[] = { {"version", '\0', 0, G_OPTION_ARG_NONE, &version, /* TRANSLATORS: command line option */ N_("Show client and daemon versions"), NULL}, {"allow-reinstall", '\0', 0, G_OPTION_ARG_NONE, &allow_reinstall, /* TRANSLATORS: command line option */ N_("Allow reinstalling existing firmware versions"), NULL}, {"allow-older", '\0', 0, G_OPTION_ARG_NONE, &allow_older, /* TRANSLATORS: command line option */ N_("Allow downgrading firmware versions"), NULL}, {"allow-branch-switch", '\0', 0, G_OPTION_ARG_NONE, &allow_branch_switch, /* TRANSLATORS: command line option */ N_("Allow switching firmware branch"), NULL}, {"force", '\0', 0, G_OPTION_ARG_NONE, &force, /* TRANSLATORS: command line option */ N_("Force the action by relaxing some runtime checks"), NULL}, {"ignore-checksum", '\0', 0, G_OPTION_ARG_NONE, &ignore_checksum, /* TRANSLATORS: command line option */ N_("Ignore firmware checksum failures"), NULL}, {"ignore-vid-pid", '\0', 0, G_OPTION_ARG_NONE, &ignore_vid_pid, /* TRANSLATORS: command line option */ N_("Ignore firmware hardware mismatch failures"), NULL}, {"ignore-requirements", '\0', 0, G_OPTION_ARG_NONE, &ignore_requirements, /* TRANSLATORS: command line option */ N_("Ignore non-critical firmware requirements"), NULL}, {"no-reboot-check", '\0', 0, G_OPTION_ARG_NONE, &priv->no_reboot_check, /* TRANSLATORS: command line option */ N_("Do not check or prompt for reboot after update"), NULL}, {"no-search", '\0', 0, G_OPTION_ARG_NONE, &no_search, /* TRANSLATORS: command line option */ N_("Do not search the firmware when parsing"), NULL}, {"no-safety-check", '\0', 0, G_OPTION_ARG_NONE, &priv->no_safety_check, /* TRANSLATORS: command line option */ N_("Do not perform device safety checks"), NULL}, {"no-device-prompt", '\0', 0, G_OPTION_ARG_NONE, &priv->no_device_prompt, /* TRANSLATORS: command line option */ N_("Do not prompt for devices"), NULL}, {"show-all", '\0', 0, G_OPTION_ARG_NONE, &priv->show_all, /* TRANSLATORS: command line option */ N_("Show all results"), NULL}, {"show-all-devices", '\0', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &priv->show_all, /* TRANSLATORS: command line option */ N_("Show devices that are not updatable"), NULL}, {"plugins", '\0', 0, G_OPTION_ARG_STRING_ARRAY, &plugin_glob, /* TRANSLATORS: command line option */ N_("Manually enable specific plugins"), NULL}, {"plugin-whitelist", '\0', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING_ARRAY, &plugin_glob, /* TRANSLATORS: command line option */ N_("Manually enable specific plugins"), NULL}, {"prepare", '\0', 0, G_OPTION_ARG_NONE, &priv->prepare_blob, /* TRANSLATORS: command line option */ N_("Run the plugin composite prepare routine when using install-blob"), NULL}, {"cleanup", '\0', 0, G_OPTION_ARG_NONE, &priv->cleanup_blob, /* TRANSLATORS: command line option */ N_("Run the plugin composite cleanup routine when using install-blob"), NULL}, {"disable-ssl-strict", '\0', 0, G_OPTION_ARG_NONE, &priv->disable_ssl_strict, /* TRANSLATORS: command line option */ N_("Ignore SSL strict checks when downloading files"), NULL}, {"filter", '\0', 0, G_OPTION_ARG_STRING, &filter_device, /* TRANSLATORS: command line option */ N_("Filter with a set of device flags using a ~ prefix to " "exclude, e.g. 'internal,~needs-reboot'"), NULL}, {"filter-release", '\0', 0, G_OPTION_ARG_STRING, &filter_release, /* TRANSLATORS: command line option */ N_("Filter with a set of release flags using a ~ prefix to " "exclude, e.g. 'trusted-release,~trusted-metadata'"), NULL}, {"json", '\0', 0, G_OPTION_ARG_NONE, &priv->as_json, /* TRANSLATORS: command line option */ N_("Output in JSON format (disables all interactive prompts)"), NULL}, {NULL}}; #ifdef _WIN32 /* workaround Windows setting the codepage to 1252 */ (void)g_setenv("LANG", "C.UTF-8", FALSE); #endif setlocale(LC_ALL, ""); bindtextdomain(GETTEXT_PACKAGE, FWUPD_LOCALEDIR); bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8"); textdomain(GETTEXT_PACKAGE); g_set_prgname(fu_util_get_prgname(argv[0])); /* create helper object */ priv->lock_fd = -1; priv->main_ctx = g_main_context_new(); priv->loop = g_main_loop_new(priv->main_ctx, FALSE); priv->console = fu_console_new(); priv->post_requests = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); fu_console_set_main_context(priv->console, priv->main_ctx); priv->request = fu_engine_request_new(NULL); /* used for monitoring and downloading */ priv->client = fwupd_client_new(); fwupd_client_set_main_context(priv->client, priv->main_ctx); fwupd_client_set_daemon_version(priv->client, PACKAGE_VERSION); fwupd_client_set_user_agent_for_package(priv->client, "fwupdtool", PACKAGE_VERSION); g_signal_connect(FWUPD_CLIENT(priv->client), "notify::percentage", G_CALLBACK(fu_util_client_notify_cb), priv); g_signal_connect(FWUPD_CLIENT(priv->client), "notify::status", G_CALLBACK(fu_util_client_notify_cb), priv); /* when not using the engine */ priv->progress = fu_progress_new(G_STRLOC); g_signal_connect(priv->progress, "percentage-changed", G_CALLBACK(fu_util_progress_percentage_changed_cb), priv); g_signal_connect(priv->progress, "status-changed", G_CALLBACK(fu_util_progress_status_changed_cb), priv); /* add commands */ fu_util_cmd_array_add(cmd_array, "smbios-dump", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("FILE"), /* TRANSLATORS: command description */ _("Dump SMBIOS data from a file"), fu_util_smbios_dump); fu_util_cmd_array_add(cmd_array, "get-plugins", NULL, /* TRANSLATORS: command description */ _("Get all enabled plugins registered with the system"), fu_util_get_plugins); fu_util_cmd_array_add(cmd_array, "get-details", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("FILE"), /* TRANSLATORS: command description */ _("Gets details about a firmware file"), fu_util_get_details); fu_util_cmd_array_add(cmd_array, "get-history", NULL, /* TRANSLATORS: command description */ _("Show history of firmware updates"), fu_util_get_history); fu_util_cmd_array_add(cmd_array, "get-updates,get-upgrades", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[DEVICE-ID|GUID]"), /* TRANSLATORS: command description */ _("Gets the list of updates for connected hardware"), fu_util_get_updates); fu_util_cmd_array_add(cmd_array, "get-devices,get-topology", NULL, /* TRANSLATORS: command description */ _("Get all devices that support firmware updates"), fu_util_get_devices); fu_util_cmd_array_add(cmd_array, "get-device-flags", NULL, /* TRANSLATORS: command description */ _("Get all device flags supported by fwupd"), fu_util_get_device_flags); fu_util_cmd_array_add(cmd_array, "watch", NULL, /* TRANSLATORS: command description */ _("Watch for hardware changes"), fu_util_watch); fu_util_cmd_array_add(cmd_array, "install-blob", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("FILENAME DEVICE-ID"), /* TRANSLATORS: command description */ _("Install a raw firmware blob on a device"), fu_util_install_blob); fu_util_cmd_array_add(cmd_array, "install", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("FILE [DEVICE-ID|GUID]"), /* TRANSLATORS: command description */ _("Install a specific firmware on a device, all possible devices" " will also be installed once the CAB matches"), fu_util_install); fu_util_cmd_array_add(cmd_array, "reinstall", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("DEVICE-ID|GUID"), /* TRANSLATORS: command description */ _("Reinstall firmware on a device"), fu_util_reinstall); fu_util_cmd_array_add(cmd_array, "attach", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("DEVICE-ID|GUID"), /* TRANSLATORS: command description */ _("Attach to firmware mode"), fu_util_attach); fu_util_cmd_array_add(cmd_array, "get-report-metadata", NULL, /* TRANSLATORS: command description */ _("Get device report metadata"), fu_util_get_report_metadata); fu_util_cmd_array_add(cmd_array, "detach", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("DEVICE-ID|GUID"), /* TRANSLATORS: command description */ _("Detach to bootloader mode"), fu_util_detach); fu_util_cmd_array_add(cmd_array, "unbind-driver", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[DEVICE-ID|GUID]"), /* TRANSLATORS: command description */ _("Unbind current driver"), fu_util_unbind_driver); fu_util_cmd_array_add(cmd_array, "bind-driver", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("SUBSYSTEM DRIVER [DEVICE-ID|GUID]"), /* TRANSLATORS: command description */ _("Bind new kernel driver"), fu_util_bind_driver); fu_util_cmd_array_add(cmd_array, "activate", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[DEVICE-ID|GUID]"), /* TRANSLATORS: command description */ _("Activate pending devices"), fu_util_activate); fu_util_cmd_array_add(cmd_array, "hwids", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[SMBIOS-FILE|HWIDS-FILE]"), /* TRANSLATORS: command description */ _("Return all the hardware IDs for the machine"), fu_util_hwids); fu_util_cmd_array_add(cmd_array, "export-hwids", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("HWIDS-FILE"), /* TRANSLATORS: command description */ _("Save a file that allows generation of hardware IDs"), fu_util_export_hwids); fu_util_cmd_array_add(cmd_array, "monitor", NULL, /* TRANSLATORS: command description */ _("Monitor the daemon for events"), fu_util_monitor); fu_util_cmd_array_add(cmd_array, "update,upgrade", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[DEVICE-ID|GUID]"), /* TRANSLATORS: command description */ _("Updates all specified devices to latest firmware version, or all " "devices if unspecified"), fu_util_update); fu_util_cmd_array_add(cmd_array, "self-sign", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("TEXT"), /* TRANSLATORS: command description */ C_("command-description", "Sign data using the client certificate"), fu_util_self_sign); fu_util_cmd_array_add(cmd_array, "verify-update", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[DEVICE-ID|GUID]"), /* TRANSLATORS: command description */ _("Update the stored metadata with current contents"), fu_util_verify_update); fu_util_cmd_array_add(cmd_array, "firmware-sign", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("FILENAME CERTIFICATE PRIVATE-KEY"), /* TRANSLATORS: command description */ _("Sign a firmware with a new key"), fu_util_firmware_sign); fu_util_cmd_array_add(cmd_array, "firmware-dump", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("FILENAME [DEVICE-ID|GUID]"), /* TRANSLATORS: command description */ _("Read a firmware blob from a device"), fu_util_firmware_dump); fu_util_cmd_array_add(cmd_array, "firmware-read", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("FILENAME [DEVICE-ID|GUID]"), /* TRANSLATORS: command description */ _("Read a firmware from a device"), fu_util_firmware_read); fu_util_cmd_array_add(cmd_array, "firmware-patch", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("FILENAME OFFSET DATA [FIRMWARE-TYPE]"), /* TRANSLATORS: command description */ _("Patch a firmware blob at a known offset"), fu_util_firmware_patch); fu_util_cmd_array_add( cmd_array, "firmware-convert", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("FILENAME-SRC FILENAME-DST [FIRMWARE-TYPE-SRC] [FIRMWARE-TYPE-DST]"), /* TRANSLATORS: command description */ _("Convert a firmware file"), fu_util_firmware_convert); fu_util_cmd_array_add(cmd_array, "firmware-build", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("BUILDER-XML FILENAME-DST"), /* TRANSLATORS: command description */ _("Build a firmware file"), fu_util_firmware_build); fu_util_cmd_array_add(cmd_array, "firmware-parse", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("FILENAME [FIRMWARE-TYPE]"), /* TRANSLATORS: command description */ _("Parse and show details about a firmware file"), fu_util_firmware_parse); fu_util_cmd_array_add(cmd_array, "firmware-export", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("FILENAME [FIRMWARE-TYPE]"), /* TRANSLATORS: command description */ _("Export a firmware file structure to XML"), fu_util_firmware_export); fu_util_cmd_array_add(cmd_array, "firmware-extract", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("FILENAME [FIRMWARE-TYPE]"), /* TRANSLATORS: command description */ _("Extract a firmware blob to images"), fu_util_firmware_extract); fu_util_cmd_array_add(cmd_array, "get-firmware-types", NULL, /* TRANSLATORS: command description */ _("List the available firmware types"), fu_util_get_firmware_types); fu_util_cmd_array_add(cmd_array, "get-firmware-gtypes", NULL, /* TRANSLATORS: command description */ _("List the available firmware GTypes"), fu_util_get_firmware_gtypes); fu_util_cmd_array_add(cmd_array, "get-remotes", NULL, /* TRANSLATORS: command description */ _("Gets the configured remotes"), fu_util_get_remotes); fu_util_cmd_array_add(cmd_array, "refresh", NULL, /* TRANSLATORS: command description */ _("Refresh metadata from remote server"), fu_util_refresh); fu_util_cmd_array_add(cmd_array, "security", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[FWUPD-VERSION]"), /* TRANSLATORS: command description */ _("Gets the host security attributes"), fu_util_security); fu_util_cmd_array_add(cmd_array, "emulation-tag", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[DEVICE-ID|GUID]"), /* TRANSLATORS: command description */ _("Adds devices to watch for future emulation"), fu_util_emulation_tag); fu_util_cmd_array_add(cmd_array, "emulation-untag", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[DEVICE-ID|GUID]"), /* TRANSLATORS: command description */ _("Removes devices to watch for future emulation"), fu_util_emulation_untag); fu_util_cmd_array_add(cmd_array, "emulation-load", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("EMULATION-FILE [ARCHIVE-FILE]"), /* TRANSLATORS: command description */ _("Load device emulation data"), fu_util_emulation_load); fu_util_cmd_array_add(cmd_array, "esp-mount", NULL, /* TRANSLATORS: command description */ _("Mounts the ESP"), fu_util_esp_mount); fu_util_cmd_array_add(cmd_array, "esp-unmount", NULL, /* TRANSLATORS: command description */ _("Unmounts the ESP"), fu_util_esp_unmount); fu_util_cmd_array_add(cmd_array, "esp-list", NULL, /* TRANSLATORS: command description */ _("Lists files on the ESP"), fu_util_esp_list); fu_util_cmd_array_add(cmd_array, "switch-branch", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[DEVICE-ID|GUID] [BRANCH]"), /* TRANSLATORS: command description */ _("Switch the firmware branch on the device"), fu_util_switch_branch); fu_util_cmd_array_add(cmd_array, "clear-history", NULL, /* TRANSLATORS: command description */ _("Erase all firmware update history"), fu_util_clear_history); fu_util_cmd_array_add( cmd_array, "get-bios-settings,get-bios-setting", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[SETTING1] [SETTING2]..."), /* TRANSLATORS: command description */ _("Retrieve BIOS settings. If no arguments are passed all settings are returned"), fu_util_get_bios_setting); fu_util_cmd_array_add(cmd_array, "set-bios-setting", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("SETTING VALUE"), /* TRANSLATORS: command description */ _("Set a BIOS setting"), fu_util_set_bios_setting); fu_util_cmd_array_add(cmd_array, "build-cabinet", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("ARCHIVE FIRMWARE METAINFO [FIRMWARE] [METAINFO] [JCATFILE]"), /* TRANSLATORS: command description */ _("Build a cabinet archive from a firmware blob and XML metadata"), fu_util_build_cabinet); fu_util_cmd_array_add(cmd_array, "efivar-list", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ C_("command-argument", "GUID"), /* TRANSLATORS: command description */ _("List EFI variables with a specific GUID"), fu_util_efivar_list); fu_util_cmd_array_add(cmd_array, "efiboot-info,efivar-boot", /* TRANSLATORS: lowercase sub-command (do not translate): then * uppercase, spaces->dashes */ NULL, /* TRANSLATORS: command description */ _("List EFI boot parameters"), fu_util_efiboot_info); fu_util_cmd_array_add(cmd_array, "efiboot-next", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("INDEX"), /* TRANSLATORS: command description */ _("Set the EFI boot next"), fu_util_efiboot_next); fu_util_cmd_array_add(cmd_array, "efiboot-order", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("INDEX1,INDEX2"), /* TRANSLATORS: command description */ _("Set the EFI boot order"), fu_util_efiboot_order); fu_util_cmd_array_add(cmd_array, "efiboot-delete", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("INDEX"), /* TRANSLATORS: command description */ _("Delete an EFI boot entry"), fu_util_efiboot_delete); fu_util_cmd_array_add(cmd_array, "efiboot-create", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("INDEX NAME TARGET [MOUNTPOINT]"), /* TRANSLATORS: command description */ _("Create an EFI boot entry"), fu_util_efiboot_create); fu_util_cmd_array_add(cmd_array, "efiboot-hive", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("INDEX KEY [VALUE]"), /* TRANSLATORS: command description */ _("Set or remove an EFI boot hive entry"), fu_util_efiboot_hive); fu_util_cmd_array_add(cmd_array, "efiboot-files,efivar-files", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ NULL, /* TRANSLATORS: command description */ _("List EFI boot files"), fu_util_efivar_files); fu_util_cmd_array_add(cmd_array, "security-fix", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[APPSTREAM_ID]"), /* TRANSLATORS: command description */ _("Fix a specific host security attribute"), fu_util_security_fix); fu_util_cmd_array_add(cmd_array, "security-undo", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[APPSTREAM_ID]"), /* TRANSLATORS: command description */ _("Undo the host security attribute fix"), fu_util_security_undo); fu_util_cmd_array_add(cmd_array, "reboot-cleanup", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[DEVICE]"), /* TRANSLATORS: command description */ _("Run the post-reboot cleanup action"), fu_util_reboot_cleanup); fu_util_cmd_array_add(cmd_array, "modify-config", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[SECTION] KEY VALUE"), /* TRANSLATORS: sets something in the daemon configuration file */ _("Modifies a daemon configuration value"), fu_util_modify_config); fu_util_cmd_array_add(cmd_array, "reset-config", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("SECTION"), /* TRANSLATORS: sets something in the daemon configuration file */ _("Resets a daemon configuration section"), fu_util_reset_config); fu_util_cmd_array_add(cmd_array, "modify-remote", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("REMOTE-ID KEY VALUE"), /* TRANSLATORS: command description */ _("Modifies a given remote"), fu_util_remote_modify); fu_util_cmd_array_add(cmd_array, "enable-remote", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("REMOTE-ID"), /* TRANSLATORS: command description */ _("Enables a given remote"), fu_util_remote_enable); fu_util_cmd_array_add(cmd_array, "disable-remote", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("REMOTE-ID"), /* TRANSLATORS: command description */ _("Disables a given remote"), fu_util_remote_disable); fu_util_cmd_array_add(cmd_array, "enable-test-devices", NULL, /* TRANSLATORS: command description */ _("Enables virtual testing devices"), fu_util_enable_test_devices); fu_util_cmd_array_add(cmd_array, "disable-test-devices", NULL, /* TRANSLATORS: command description */ _("Disables virtual testing devices"), fu_util_disable_test_devices); fu_util_cmd_array_add(cmd_array, "get-version-formats", NULL, /* TRANSLATORS: command description */ _("Get all known version formats"), fu_util_get_verfmts); fu_util_cmd_array_add(cmd_array, "vercmp", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("VERSION1 VERSION2 [FORMAT]"), /* TRANSLATORS: command description */ _("Compares two versions for equality"), fu_util_vercmp); /* do stuff on ctrl+c */ priv->cancellable = g_cancellable_new(); fu_util_setup_signal_handlers(priv); g_signal_connect(G_CANCELLABLE(priv->cancellable), "cancelled", G_CALLBACK(fu_util_cancelled_cb), priv); /* sort by command name */ fu_util_cmd_array_sort(cmd_array); /* non-TTY consoles cannot answer questions */ if (!fu_util_setup_interactive(priv, &error_console)) { g_info("failed to initialize interactive console: %s", error_console->message); priv->no_reboot_check = TRUE; priv->no_safety_check = TRUE; priv->no_device_prompt = TRUE; } else { priv->interactive = TRUE; /* set our implemented feature set */ fu_engine_request_set_feature_flags( priv->request, FWUPD_FEATURE_FLAG_DETACH_ACTION | FWUPD_FEATURE_FLAG_SWITCH_BRANCH | FWUPD_FEATURE_FLAG_FDE_WARNING | FWUPD_FEATURE_FLAG_UPDATE_ACTION | FWUPD_FEATURE_FLAG_COMMUNITY_TEXT | FWUPD_FEATURE_FLAG_SHOW_PROBLEMS | FWUPD_FEATURE_FLAG_REQUESTS | FWUPD_FEATURE_FLAG_REQUESTS_NON_GENERIC); } fu_console_set_interactive(priv->console, priv->interactive); /* get a list of the commands */ priv->context = g_option_context_new(NULL); cmd_descriptions = fu_util_cmd_array_to_string(cmd_array); g_option_context_set_summary(priv->context, cmd_descriptions); g_option_context_set_description( priv->context, /* TRANSLATORS: CLI description */ _("This tool allows an administrator to use the fwupd plugins " "without being installed on the host system.")); /* TRANSLATORS: program name */ g_set_application_name(_("Firmware Utility")); g_option_context_add_main_entries(priv->context, options, NULL); g_option_context_add_group(priv->context, fu_debug_get_option_group()); ret = g_option_context_parse(priv->context, &argc, &argv, &error); if (!ret) { fu_console_print(priv->console, "%s: %s", /* TRANSLATORS: the user didn't read the man page */ _("Failed to parse arguments"), error->message); return EXIT_FAILURE; } fu_progress_set_profile(priv->progress, g_getenv("FWUPD_VERBOSE") != NULL); /* allow disabling SSL strict mode for broken corporate proxies */ if (priv->disable_ssl_strict) { fu_console_print_full(priv->console, FU_CONSOLE_PRINT_FLAG_WARNING, "%s\n", /* TRANSLATORS: try to help */ _("Ignoring SSL strict checks, " "to do this automatically in the future " "export DISABLE_SSL_STRICT in your environment")); (void)g_setenv("DISABLE_SSL_STRICT", "1", TRUE); } /* parse filter flags */ if (filter_device != NULL) { if (!fu_util_parse_filter_device_flags(filter_device, &priv->filter_device_include, &priv->filter_device_exclude, &error)) { g_autofree gchar *str = /* TRANSLATORS: the user didn't read the man page, %1 is '--filter' */ g_strdup_printf(_("Failed to parse flags for %s"), "--filter"); g_prefix_error(&error, "%s: ", str); fu_util_print_error(priv, error); return EXIT_FAILURE; } } if (filter_release != NULL) { if (!fu_util_parse_filter_release_flags(filter_release, &priv->filter_release_include, &priv->filter_release_exclude, &error)) { g_autofree gchar *str = /* TRANSLATORS: the user didn't read the man page, * %1 is '--filter-release' */ g_strdup_printf(_("Failed to parse flags for %s"), "--filter-release"); g_prefix_error(&error, "%s: ", str); fu_util_print_error(priv, error); return EXIT_FAILURE; } } /* set flags */ if (allow_reinstall) priv->flags |= FWUPD_INSTALL_FLAG_ALLOW_REINSTALL; if (allow_older) priv->flags |= FWUPD_INSTALL_FLAG_ALLOW_OLDER; if (allow_branch_switch) priv->flags |= FWUPD_INSTALL_FLAG_ALLOW_BRANCH_SWITCH; if (force) priv->flags |= FWUPD_INSTALL_FLAG_FORCE; if (no_search) priv->parse_flags |= FU_FIRMWARE_PARSE_FLAG_NO_SEARCH; if (ignore_checksum) priv->parse_flags |= FU_FIRMWARE_PARSE_FLAG_IGNORE_CHECKSUM; if (ignore_vid_pid) priv->parse_flags |= FU_FIRMWARE_PARSE_FLAG_IGNORE_VID_PID; if (ignore_requirements) priv->flags |= FWUPD_INSTALL_FLAG_IGNORE_REQUIREMENTS; /* load engine */ priv->ctx = fu_context_new(); priv->engine = fu_engine_new(priv->ctx); g_signal_connect(FU_ENGINE(priv->engine), "device-request", G_CALLBACK(fu_util_update_device_request_cb), priv); g_signal_connect(FU_ENGINE(priv->engine), "device-added", G_CALLBACK(fu_util_engine_device_added_cb), priv); g_signal_connect(FU_ENGINE(priv->engine), "device-removed", G_CALLBACK(fu_util_engine_device_removed_cb), priv); g_signal_connect(FU_ENGINE(priv->engine), "status-changed", G_CALLBACK(fu_util_engine_status_changed_cb), priv); /* just show versions and exit */ if (version) { if (!fu_util_version(priv, &error)) { fu_util_print_error(priv, error); return EXIT_FAILURE; } return EXIT_SUCCESS; } /* any plugin allowlist specified */ for (guint i = 0; plugin_glob != NULL && plugin_glob[i] != NULL; i++) fu_engine_add_plugin_filter(priv->engine, plugin_glob[i]); /* run the specified command */ ret = fu_util_cmd_array_run(cmd_array, priv, argv[1], (gchar **)&argv[2], &error); if (!ret) { #ifdef SUPPORTED_BUILD /* sanity check */ if (error == NULL) { g_critical("exec failed but no error set!"); return EXIT_FAILURE; } #endif fu_util_print_error(priv, error); if (!priv->as_json && g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS)) { fu_console_print(priv->console, /* TRANSLATORS: explain how to get help, %1 is * 'fwupdtool --help' */ _("Use %s for help"), "fwupdtool --help"); } else if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) { g_info("%s\n", error->message); return EXIT_NOTHING_TO_DO; } else if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_REACHABLE)) { g_info("%s\n", error->message); return EXIT_NOT_REACHABLE; } else if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_info("%s\n", error->message); return EXIT_NOT_FOUND; } #ifdef HAVE_GETUID /* if not root, then notify users on the error path */ if (priv->interactive && (getuid() != 0 || geteuid() != 0)) { fu_console_print_full(priv->console, FU_CONSOLE_PRINT_FLAG_STDERR | FU_CONSOLE_PRINT_FLAG_WARNING, "%s\n", /* TRANSLATORS: we're poking around as a power user */ _("This program may only work correctly as root")); } #endif return EXIT_FAILURE; } /* a good place to do the traceback */ if (fu_progress_get_profile(priv->progress)) { g_autofree gchar *str = fu_progress_traceback(priv->progress); if (str != NULL) fu_console_print_literal(priv->console, str); } /* success */ return EXIT_SUCCESS; } fwupd-2.0.10/src/fu-udev-backend.c000066400000000000000000000573071501337203100166210ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuBackend" #include "config.h" #include #include #include #include #include #include "fu-context-private.h" #include "fu-device-private.h" #include "fu-engine-struct.h" #include "fu-udev-backend.h" #include "fu-udev-device-private.h" struct _FuUdevBackend { FuBackend parent_instance; gint netlink_fd; GHashTable *map_paths; /* of str:None */ GPtrArray *dpaux_devices; /* of FuDpauxDevice */ guint dpaux_devices_rescan_id; gboolean done_coldplug; }; G_DEFINE_TYPE(FuUdevBackend, fu_udev_backend, FU_TYPE_BACKEND) #define FU_UDEV_BACKEND_DPAUX_RESCAN_DELAY 5 /* s */ static void fu_udev_backend_to_string(FuBackend *backend, guint idt, GString *str) { FuUdevBackend *self = FU_UDEV_BACKEND(backend); fwupd_codec_string_append_bool(str, idt, "DoneColdplug", self->done_coldplug); } static void fu_udev_backend_rescan_dpaux_device(FuUdevBackend *self, FuDevice *dpaux_device) { FuDevice *device_tmp; g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GError) error_local = NULL; /* find the device we enumerated */ g_debug("looking for %s", fu_device_get_backend_id(dpaux_device)); device_tmp = fu_backend_lookup_by_id(FU_BACKEND(self), fu_device_get_backend_id(dpaux_device)); /* open */ fu_device_probe_invalidate(dpaux_device); locker = fu_device_locker_new(dpaux_device, &error_local); if (locker == NULL) { g_debug("failed to open device %s: %s", fu_device_get_backend_id(dpaux_device), error_local->message); if (device_tmp != NULL) fu_backend_device_removed(FU_BACKEND(self), FU_DEVICE(device_tmp)); return; } if (device_tmp == NULL) { fu_backend_device_added(FU_BACKEND(self), FU_DEVICE(dpaux_device)); return; } } static gboolean fu_udev_backend_rescan_dpaux_devices_cb(gpointer user_data) { FuUdevBackend *self = FU_UDEV_BACKEND(user_data); for (guint i = 0; i < self->dpaux_devices->len; i++) { FuDevice *dpaux_device = g_ptr_array_index(self->dpaux_devices, i); fu_udev_backend_rescan_dpaux_device(self, dpaux_device); } self->dpaux_devices_rescan_id = 0; return FALSE; } static void fu_udev_backend_rescan_dpaux_devices(FuUdevBackend *self) { if (self->dpaux_devices_rescan_id != 0) g_source_remove(self->dpaux_devices_rescan_id); self->dpaux_devices_rescan_id = g_timeout_add_seconds(FU_UDEV_BACKEND_DPAUX_RESCAN_DELAY, fu_udev_backend_rescan_dpaux_devices_cb, self); } static FuUdevDevice * fu_udev_backend_create_device(FuUdevBackend *self, const gchar *fn, GError **error); static void fu_udev_backend_create_ddc_proxy(FuUdevBackend *self, FuUdevDevice *udev_device) { g_autofree gchar *proxy_sysfs_path = NULL; g_autofree gchar *proxy_sysfs_real = NULL; g_autoptr(FuUdevDevice) proxy = NULL; g_autoptr(GError) error_local = NULL; proxy_sysfs_path = g_build_filename(fu_udev_device_get_sysfs_path(udev_device), "ddc", NULL); proxy_sysfs_real = fu_path_make_absolute(proxy_sysfs_path, &error_local); if (proxy_sysfs_real == NULL) { g_debug("failed to resolve %s: %s", proxy_sysfs_path, error_local->message); return; } proxy = fu_udev_backend_create_device(self, proxy_sysfs_real, &error_local); if (proxy == NULL) { g_warning("failed to create DRM DDC device: %s", error_local->message); return; } fu_device_add_private_flag(FU_DEVICE(proxy), FU_I2C_DEVICE_PRIVATE_FLAG_NO_HWID_GUIDS); if (!fu_device_probe(FU_DEVICE(proxy), &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_TIMED_OUT)) return; g_warning("failed to probe DRM DDC device: %s", error_local->message); return; } fu_device_add_private_flag(FU_DEVICE(udev_device), FU_DEVICE_PRIVATE_FLAG_REFCOUNTED_PROXY); fu_device_set_proxy(FU_DEVICE(udev_device), FU_DEVICE(proxy)); } static GType fu_udev_backend_get_gtype_for_subsystem_devtype(const gchar *subsystem, const gchar *devtype) { struct { const gchar *subsystem; const gchar *devtype; GType gtype; } map[] = { {"mei", NULL, FU_TYPE_MEI_DEVICE}, {"drm", NULL, FU_TYPE_DRM_DEVICE}, {"usb", "usb_device", FU_TYPE_USB_DEVICE}, {"i2c", NULL, FU_TYPE_I2C_DEVICE}, {"i2c-dev", NULL, FU_TYPE_I2C_DEVICE}, {"drm_dp_aux_dev", NULL, FU_TYPE_DPAUX_DEVICE}, {"hidraw", NULL, FU_TYPE_HIDRAW_DEVICE}, {"block", "disk", FU_TYPE_BLOCK_DEVICE}, {"block", "partition", FU_TYPE_BLOCK_PARTITION}, {"serio", NULL, FU_TYPE_SERIO_DEVICE}, {"pci", NULL, FU_TYPE_PCI_DEVICE}, {"video4linux", NULL, FU_TYPE_V4L_DEVICE}, }; for (guint i = 0; i < G_N_ELEMENTS(map); i++) { if (g_strcmp0(subsystem, map[i].subsystem) == 0 && (map[i].devtype == NULL || g_strcmp0(devtype, map[i].devtype) == 0)) { return map[i].gtype; } } return FU_TYPE_UDEV_DEVICE; } static FuDevice * fu_udev_backend_create_device_for_donor(FuBackend *backend, FuDevice *donor, GError **error) { FuUdevBackend *self = FU_UDEV_BACKEND(backend); FuContext *ctx = fu_backend_get_context(FU_BACKEND(self)); g_autoptr(FuDevice) device = NULL; GType gtype = FU_TYPE_UDEV_DEVICE; /* ignore zram and loop block devices -- of which there are dozens on systems with snap */ if (g_strcmp0(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(donor)), "block") == 0) { g_autofree gchar *basename = g_path_get_basename(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(donor))); if (g_str_has_prefix(basename, "zram") || g_str_has_prefix(basename, "loop")) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "ignoring uninteresting %s block device", basename); return NULL; } } /* create actual device with correct GType */ gtype = fu_udev_backend_get_gtype_for_subsystem_devtype( fu_udev_device_get_subsystem(FU_UDEV_DEVICE(donor)), fu_udev_device_get_devtype(FU_UDEV_DEVICE(donor))); if (gtype == FU_TYPE_UDEV_DEVICE) { device = g_object_ref(donor); } else { device = g_object_new(gtype, "backend", FU_BACKEND(self), NULL); fu_device_incorporate(device, donor, FU_DEVICE_INCORPORATE_FLAG_ALL); if (!fu_device_probe(device, error)) { g_prefix_error(error, "failed to probe: "); return NULL; } } /* these are used without a subclass */ if (g_strcmp0(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device)), "msr") == 0) fu_udev_device_add_open_flag(FU_UDEV_DEVICE(device), FU_IO_CHANNEL_OPEN_FLAG_READ); /* the DRM device has a i2c device that is used for communicating with the scaler */ if (gtype == FU_TYPE_DRM_DEVICE) fu_udev_backend_create_ddc_proxy(self, FU_UDEV_DEVICE(device)); /* notify plugins using fu_plugin_add_udev_subsystem() */ if (fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device)) != NULL) { g_autoptr(GPtrArray) possible_plugins = NULL; g_autofree gchar *subsystem = fu_udev_device_get_subsystem_devtype(FU_UDEV_DEVICE(device)); possible_plugins = fu_context_get_plugin_names_for_udev_subsystem(ctx, subsystem, NULL); if (possible_plugins != NULL) { for (guint i = 0; i < possible_plugins->len; i++) { const gchar *plugin_name = g_ptr_array_index(possible_plugins, i); fu_device_add_possible_plugin(device, plugin_name); } } } /* set in fu-self-test */ if (g_getenv("FWUPD_SELF_TEST") != NULL) fu_device_add_private_flag(device, FU_DEVICE_PRIVATE_FLAG_IS_FAKE); return g_steal_pointer(&device); } static FuUdevDevice * fu_udev_backend_create_device(FuUdevBackend *self, const gchar *fn, GError **error) { FuContext *ctx = fu_backend_get_context(FU_BACKEND(self)); g_autoptr(FuUdevDevice) device_donor = fu_udev_device_new(ctx, fn); /* use a donor device to probe for the subsystem and devtype */ if (!fu_device_probe(FU_DEVICE(device_donor), error)) { g_prefix_error(error, "failed to probe donor: "); return NULL; } return FU_UDEV_DEVICE(fu_udev_backend_create_device_for_donor(FU_BACKEND(self), FU_DEVICE(device_donor), error)); } static void fu_udev_backend_device_add_from_device(FuUdevBackend *self, FuUdevDevice *device) { /* DP AUX devices are *weird* and can only read the DPCD when a DRM device is attached */ if (g_strcmp0(fu_udev_device_get_subsystem(device), "drm_dp_aux_dev") == 0) { /* add and rescan, regardless of if we can open it */ g_ptr_array_add(self->dpaux_devices, g_object_ref(device)); fu_udev_backend_rescan_dpaux_devices(self); /* open -- this might seem redundant, but it means the device is added at daemon * coldplug rather than a few seconds later */ if (!self->done_coldplug) { g_autoptr(FuDeviceLocker) locker = NULL; g_autoptr(GError) error_local = NULL; locker = fu_device_locker_new(device, &error_local); if (locker == NULL) { g_debug("failed to open device %s: %s", fu_device_get_backend_id(FU_DEVICE(device)), error_local->message); return; } fu_backend_device_added(FU_BACKEND(self), FU_DEVICE(device)); } return; } /* success */ fu_backend_device_added(FU_BACKEND(self), FU_DEVICE(device)); } static void fu_udev_backend_device_remove(FuUdevBackend *self, const gchar *sysfs_path) { FuDevice *device_tmp; /* find the device we enumerated */ device_tmp = fu_backend_lookup_by_id(FU_BACKEND(self), sysfs_path); if (device_tmp != NULL) { g_debug("UDEV %s removed", sysfs_path); /* rescan all the DP AUX devices if it or any DRM device disappears */ if (g_ptr_array_remove(self->dpaux_devices, device_tmp) || g_strcmp0(fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device_tmp)), "drm") == 0) { fu_udev_backend_rescan_dpaux_devices(self); } fu_backend_device_removed(FU_BACKEND(self), device_tmp); } } static gint fu_udev_backend_device_number_sort_cb(gconstpointer a, gconstpointer b) { FuUdevDevice *device1 = *((FuUdevDevice **)a); FuUdevDevice *device2 = *((FuUdevDevice **)b); if (fu_udev_device_get_number(device1) < fu_udev_device_get_number(device2)) return -1; if (fu_udev_device_get_number(device1) > fu_udev_device_get_number(device2)) return 1; return 0; } static void fu_udev_backend_coldplug_subsystem(FuUdevBackend *self, const gchar *fn) { const gchar *basename; g_autoptr(GDir) dir = NULL; g_autoptr(GError) error_dir = NULL; g_autoptr(GPtrArray) devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); dir = g_dir_open(fn, 0, &error_dir); if (dir == NULL) { if (!g_error_matches(error_dir, G_FILE_ERROR, G_FILE_ERROR_NOENT)) g_debug("ignoring: %s", error_dir->message); return; } while ((basename = g_dir_read_name(dir)) != NULL) { g_autofree gchar *fn_full = g_build_filename(fn, basename, NULL); g_autofree gchar *fn_real = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(FuUdevDevice) device = NULL; if (!g_file_test(fn_full, G_FILE_TEST_IS_DIR)) continue; fn_real = fu_path_make_absolute(fn_full, &error_local); if (fn_real == NULL) { g_warning("failed to get symlink target for %s: %s", fn_full, error_local->message); continue; } if (g_hash_table_contains(self->map_paths, fn_real)) { g_debug("skipping duplicate %s", fn_real); continue; } device = fu_udev_backend_create_device(self, fn_real, &error_local); if (device == NULL) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) continue; g_warning("failed to create device from %s: %s", fn_real, error_local->message); continue; } g_hash_table_add(self->map_paths, g_steal_pointer(&fn_real)); g_ptr_array_add(devices, g_steal_pointer(&device)); } /* sort by device order (so video0 comes before video9) and add as a device */ g_ptr_array_sort(devices, fu_udev_backend_device_number_sort_cb); for (guint i = 0; i < devices->len; i++) { FuUdevDevice *device = g_ptr_array_index(devices, i); fu_udev_backend_device_add_from_device(self, device); } } static gboolean fu_udev_backend_netlink_parse_blob(FuUdevBackend *self, GBytes *blob, GError **error) { FuContext *ctx = fu_backend_get_context(FU_BACKEND(self)); FuUdevAction action = FU_UDEV_ACTION_UNKNOWN; const guint8 *buf; gsize bufsz = 0; g_autofree gchar *sysfsdir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR); g_autoptr(FuStructUdevMonitorNetlinkHeader) st_hdr = NULL; g_autoptr(FuUdevDevice) device_donor = NULL; g_autoptr(GBytes) blob_payload = NULL; /* parse the buffer */ st_hdr = fu_struct_udev_monitor_netlink_header_parse_bytes(blob, 0x0, error); if (st_hdr == NULL) return FALSE; blob_payload = fu_bytes_new_offset(blob, fu_struct_udev_monitor_netlink_header_get_properties_off(st_hdr), fu_struct_udev_monitor_netlink_header_get_properties_len(st_hdr), error); if (blob_payload == NULL) return FALSE; /* split into lines */ buf = g_bytes_get_data(blob_payload, &bufsz); for (gsize i = 0; i < bufsz; i++) { g_autofree gchar *kvstr = NULL; g_auto(GStrv) kv = NULL; kvstr = fu_strsafe((const gchar *)buf + i, bufsz - i); if (kvstr == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "invalid ACSII buffer"); return FALSE; } kv = g_strsplit(kvstr, "=", 2); if (g_strcmp0(kv[0], "ACTION") == 0) { action = fu_udev_action_from_string(kv[1]); if (action == FU_UDEV_ACTION_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "unknown action %s", kv[1]); return FALSE; } /* we do not care about these */ if (action == FU_UDEV_ACTION_BIND || action == FU_UDEV_ACTION_UNBIND) return TRUE; } else if (g_strcmp0(kv[0], "DEVPATH") == 0) { g_autofree gchar *sysfspath = g_build_filename(sysfsdir, kv[1], NULL); /* something changed */ if (action == FU_UDEV_ACTION_CHANGE) { FuDevice *device_tmp = fu_backend_lookup_by_id(FU_BACKEND(self), sysfspath); if (device_tmp == NULL) return TRUE; if (g_strcmp0( fu_udev_device_get_subsystem(FU_UDEV_DEVICE(device_tmp)), "drm") == 0) fu_udev_backend_rescan_dpaux_devices(self); g_set_object(&device_donor, FU_UDEV_DEVICE(device_tmp)); } /* something got removed */ if (action == FU_UDEV_ACTION_REMOVE) { fu_udev_backend_device_remove(self, sysfspath); return TRUE; } /* something got added */ if (action == FU_UDEV_ACTION_ADD) { if (device_donor != NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "already have a donor device"); return FALSE; } device_donor = fu_udev_device_new(ctx, sysfspath); } } else if (g_strcmp0(kv[0], "SUBSYSTEM") == 0 && device_donor != NULL) { fu_udev_device_set_subsystem(device_donor, kv[1]); } else if (g_strcmp0(kv[0], "DEVTYPE") == 0 && device_donor != NULL) { fu_udev_device_set_devtype(device_donor, kv[1]); } else if (device_donor != NULL) { fu_udev_device_add_property(device_donor, kv[0], kv[1]); } /* next! */ i += strlen(kvstr); } /* notify the engine */ if (action == FU_UDEV_ACTION_CHANGE) { if (device_donor == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no device to change"); return FALSE; } fu_backend_device_changed(FU_BACKEND(self), FU_DEVICE(device_donor)); } /* now create the actual device from the donor */ if (action == FU_UDEV_ACTION_ADD) { g_autoptr(FuUdevDevice) device_actual = NULL; if (device_donor == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_DATA, "no new device to add"); return FALSE; } device_actual = FU_UDEV_DEVICE(fu_udev_backend_create_device_for_donor(FU_BACKEND(self), FU_DEVICE(device_donor), error)); if (device_actual == NULL) return FALSE; /* success */ fu_udev_backend_device_add_from_device(self, device_actual); } /* success */ return TRUE; } static gboolean fu_udev_backend_netlink_cb(gint fd, GIOCondition condition, gpointer user_data) { FuUdevBackend *self = FU_UDEV_BACKEND(user_data); gssize len; guint8 buf[10240] = {0x0}; g_autoptr(GBytes) blob = NULL; g_autoptr(GError) error_local = NULL; len = recv(fd, buf, sizeof(buf), MSG_DONTWAIT); if (len < 0) return TRUE; blob = g_bytes_new(buf, len); if (!fu_udev_backend_netlink_parse_blob(self, blob, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_debug("ignoring netlink message: %s", error_local->message); return TRUE; } g_warning("ignoring netlink message: %s", error_local->message); return TRUE; } return TRUE; } static gboolean fu_udev_backend_netlink_setup(FuUdevBackend *self, GError **error) { struct sockaddr_nl nls = { .nl_family = AF_NETLINK, .nl_pid = getpid(), .nl_groups = FU_UDEV_MONITOR_NETLINK_GROUP_UDEV, }; g_autoptr(GSource) source = NULL; /* minijail -p prevents access */ if (nls.nl_pid <= 2) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to get PID, perhaps sandboxed?"); return FALSE; } self->netlink_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_KOBJECT_UEVENT); if (self->netlink_fd < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to connect to netlink: %s", g_strerror(errno)); return FALSE; } if (bind(self->netlink_fd, (void *)&nls, sizeof(nls))) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "bind to udev socket failed: %s", g_strerror(errno)); return FALSE; } source = g_unix_fd_source_new(self->netlink_fd, G_IO_IN); g_source_set_static_name(source, "netlink"); g_source_set_callback(source, (GSourceFunc)fu_udev_backend_netlink_cb, self, NULL); g_source_set_can_recurse(source, TRUE); g_source_attach(source, NULL); /* success */ return TRUE; } static gboolean fu_udev_backend_coldplug(FuBackend *backend, FuProgress *progress, GError **error) { FuContext *ctx = fu_backend_get_context(backend); FuUdevBackend *self = FU_UDEV_BACKEND(backend); g_autofree gchar *sysfsdir = fu_path_from_kind(FU_PATH_KIND_SYSFSDIR); g_autoptr(GPtrArray) udev_subsystems = fu_context_get_udev_subsystems(ctx); /* get all devices of class */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, udev_subsystems->len); for (guint i = 0; i < udev_subsystems->len; i++) { const gchar *subsystem = g_ptr_array_index(udev_subsystems, i); g_autofree gchar *class_fn = NULL; g_autofree gchar *bus_fn = NULL; /* we only care about subsystems, not subsystem:devtype matches */ if (g_strstr_len(subsystem, -1, ":") != NULL) { fu_progress_step_done(progress); continue; } class_fn = g_build_filename(sysfsdir, "class", subsystem, NULL); if (g_file_test(class_fn, G_FILE_TEST_EXISTS)) { fu_udev_backend_coldplug_subsystem(self, class_fn); fu_progress_step_done(progress); continue; } bus_fn = g_build_filename(sysfsdir, "bus", subsystem, "devices", NULL); if (g_file_test(bus_fn, G_FILE_TEST_EXISTS)) { fu_udev_backend_coldplug_subsystem(self, bus_fn); fu_progress_step_done(progress); continue; } fu_progress_step_done(progress); } /* success */ self->done_coldplug = TRUE; return TRUE; } static gboolean fu_udev_backend_setup(FuBackend *backend, FuBackendSetupFlags flags, FuProgress *progress, GError **error) { FuUdevBackend *self = FU_UDEV_BACKEND(backend); /* set up hotplug events */ if (flags & FU_BACKEND_SETUP_FLAG_USE_HOTPLUG) { if (!fu_udev_backend_netlink_setup(self, error)) { g_prefix_error(error, "failed to set up netlink: "); return FALSE; } } /* success */ return TRUE; } static FuDevice * fu_udev_backend_get_device_parent(FuBackend *backend, FuDevice *device, const gchar *subsystem, GError **error) { FuUdevBackend *self = FU_UDEV_BACKEND(backend); g_autofree gchar *devtype_new = NULL; g_autofree gchar *sysfs_path = NULL; g_autoptr(FuUdevDevice) device_new = NULL; /* emulated */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { GType gtype = FU_TYPE_UDEV_DEVICE; g_autofree gchar *physical_id = NULL; if (subsystem != NULL) { g_auto(GStrv) split = g_strsplit(subsystem, ":", 2); gtype = fu_udev_backend_get_gtype_for_subsystem_devtype(split[0], split[1]); physical_id = g_strconcat(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device)), "-parent:", subsystem, NULL); } else { physical_id = g_strconcat(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device)), "-parent", NULL); } device_new = g_object_new(gtype, "context", fu_backend_get_context(backend), "physical-id", physical_id, NULL); fu_device_add_flag(FU_DEVICE(device_new), FWUPD_DEVICE_FLAG_EMULATED); return FU_DEVICE(g_steal_pointer(&device_new)); } sysfs_path = g_strdup(fu_udev_device_get_sysfs_path(FU_UDEV_DEVICE(device))); if (sysfs_path == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "sysfs path undefined"); return NULL; } if (!g_file_test(sysfs_path, G_FILE_TEST_EXISTS)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "sysfs path '%s' doesn't exist, perhaps a transient device.", sysfs_path); return NULL; } /* lets just walk up the directories */ while (1) { g_autofree gchar *dirname = NULL; g_autoptr(GError) error_local = NULL; /* done? */ dirname = g_path_get_dirname(sysfs_path); if (g_strcmp0(dirname, ".") == 0 || g_strcmp0(dirname, "/") == 0) break; /* check has matching subsystem and devtype */ device_new = fu_udev_backend_create_device(self, dirname, &error_local); if (device_new != NULL) { if (fu_udev_device_match_subsystem(device_new, subsystem)) { if (subsystem != NULL) { g_auto(GStrv) split = g_strsplit(subsystem, ":", 2); fu_udev_device_set_subsystem(device_new, split[0]); } return FU_DEVICE(g_steal_pointer(&device_new)); } } else { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) g_warning("failed to create device: %s", error_local->message); } /* just swap, and go deeper */ g_free(sysfs_path); sysfs_path = g_steal_pointer(&dirname); } /* failed */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no parent with subsystem %s", subsystem); return NULL; } static FuDevice * fu_udev_backend_create_device_impl(FuBackend *backend, const gchar *backend_id, GError **error) { FuUdevBackend *self = FU_UDEV_BACKEND(backend); return FU_DEVICE(fu_udev_backend_create_device(self, backend_id, error)); } static void fu_udev_backend_finalize(GObject *object) { FuUdevBackend *self = FU_UDEV_BACKEND(object); if (self->dpaux_devices_rescan_id != 0) g_source_remove(self->dpaux_devices_rescan_id); if (self->netlink_fd > 0) g_close(self->netlink_fd, NULL); g_hash_table_unref(self->map_paths); g_ptr_array_unref(self->dpaux_devices); G_OBJECT_CLASS(fu_udev_backend_parent_class)->finalize(object); } static void fu_udev_backend_init(FuUdevBackend *self) { self->map_paths = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); self->dpaux_devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); } static void fu_udev_backend_class_init(FuUdevBackendClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuBackendClass *backend_class = FU_BACKEND_CLASS(klass); object_class->finalize = fu_udev_backend_finalize; backend_class->coldplug = fu_udev_backend_coldplug; backend_class->setup = fu_udev_backend_setup; backend_class->to_string = fu_udev_backend_to_string; backend_class->get_device_parent = fu_udev_backend_get_device_parent; backend_class->create_device = fu_udev_backend_create_device_impl; backend_class->create_device_for_donor = fu_udev_backend_create_device_for_donor; } FuBackend * fu_udev_backend_new(FuContext *ctx) { return FU_BACKEND(g_object_new(FU_TYPE_UDEV_BACKEND, "name", "udev", "context", ctx, "device-gtype", FU_TYPE_UDEV_DEVICE, NULL)); } fwupd-2.0.10/src/fu-udev-backend.h000066400000000000000000000005501501337203100166120ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-backend.h" #define FU_TYPE_UDEV_BACKEND (fu_udev_backend_get_type()) G_DECLARE_FINAL_TYPE(FuUdevBackend, fu_udev_backend, FU, UDEV_BACKEND, FuBackend) FuBackend * fu_udev_backend_new(FuContext *ctx) G_GNUC_NON_NULL(1); fwupd-2.0.10/src/fu-uefi-backend.c000066400000000000000000000046131501337203100165760ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuBackend" #include "config.h" #include #include "fu-uefi-backend.h" #include "fu-uefi-device-private.h" struct _FuUefiBackend { FuBackend parent_instance; }; G_DEFINE_TYPE(FuUefiBackend, fu_uefi_backend, FU_TYPE_BACKEND) static gboolean fu_uefi_backend_coldplug(FuBackend *backend, FuProgress *progress, GError **error) { FuContext *ctx = fu_backend_get_context(backend); FuEfivars *efivars = fu_context_get_efivars(ctx); struct { const gchar *guid; const gchar *name; } guid_names[] = { {FU_EFIVARS_GUID_EFI_GLOBAL, "PK"}, {FU_EFIVARS_GUID_EFI_GLOBAL, "KEK"}, {FU_EFIVARS_GUID_SECURITY_DATABASE, "db"}, {FU_EFIVARS_GUID_SECURITY_DATABASE, "dbx"}, }; /* each important EFI key */ fu_progress_set_id(progress, G_STRLOC); fu_progress_set_steps(progress, G_N_ELEMENTS(guid_names)); for (guint i = 0; i < G_N_ELEMENTS(guid_names); i++) { g_autoptr(FuUefiDevice) uefi_device = NULL; if (!fu_efivars_exists(efivars, guid_names[i].guid, guid_names[i].name)) { fu_progress_step_done(progress); continue; } uefi_device = fu_uefi_device_new(guid_names[i].guid, guid_names[i].name); fu_backend_device_added(backend, FU_DEVICE(uefi_device)); fu_progress_step_done(progress); } /* success */ return TRUE; } static FuDevice * fu_uefi_backend_create_device(FuBackend *backend, const gchar *backend_id, GError **error) { g_auto(GStrv) split = g_strsplit(backend_id, "-", 2); g_autoptr(FuUefiDevice) uefi_device = NULL; uefi_device = g_object_new(FU_TYPE_UEFI_DEVICE, "backend", backend, "backend-id", backend_id, NULL); fu_uefi_device_set_guid(uefi_device, split[0]); fu_uefi_device_set_name(uefi_device, split[1]); return FU_DEVICE(g_steal_pointer(&uefi_device)); } static void fu_uefi_backend_init(FuUefiBackend *self) { } static void fu_uefi_backend_class_init(FuUefiBackendClass *klass) { FuBackendClass *backend_class = FU_BACKEND_CLASS(klass); backend_class->coldplug = fu_uefi_backend_coldplug; backend_class->create_device = fu_uefi_backend_create_device; } FuBackend * fu_uefi_backend_new(FuContext *ctx) { return FU_BACKEND(g_object_new(FU_TYPE_UEFI_BACKEND, "name", "uefi", "context", ctx, "device-gtype", FU_TYPE_UEFI_DEVICE, NULL)); } fwupd-2.0.10/src/fu-uefi-backend.h000066400000000000000000000005501501337203100165770ustar00rootroot00000000000000/* * Copyright 2025 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-backend.h" #define FU_TYPE_UEFI_BACKEND (fu_uefi_backend_get_type()) G_DECLARE_FINAL_TYPE(FuUefiBackend, fu_uefi_backend, FU, UEFI_BACKEND, FuBackend) FuBackend * fu_uefi_backend_new(FuContext *ctx) G_GNUC_NON_NULL(1); fwupd-2.0.10/src/fu-unix-seekable-input-stream.c000066400000000000000000000072221501337203100214420ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuUnixSeekableInputStream" #include "config.h" #include "fu-unix-seekable-input-stream.h" struct _FuUnixSeekableInputStream { GUnixInputStream parent_instance; }; static void fu_unix_seekable_input_stream_seekable_iface_init(GSeekableIface *iface); /* see https://gitlab.gnome.org/GNOME/glib/-/issues/3200 for why this is needed */ G_DEFINE_TYPE_WITH_CODE(FuUnixSeekableInputStream, fu_unix_seekable_input_stream, G_TYPE_UNIX_INPUT_STREAM, G_IMPLEMENT_INTERFACE(G_TYPE_SEEKABLE, fu_unix_seekable_input_stream_seekable_iface_init)) static goffset fu_unix_seekable_input_stream_tell(GSeekable *seekable) { goffset rc = lseek(g_unix_input_stream_get_fd(G_UNIX_INPUT_STREAM(seekable)), 0, SEEK_CUR); if (rc < 0) g_critical("cannot tell FuUnixSeekableInputStream: %s", g_strerror(errno)); return rc; } static gboolean fu_unix_seekable_input_stream_can_seek(GSeekable *seekable) { return lseek(g_unix_input_stream_get_fd(G_UNIX_INPUT_STREAM(seekable)), 0, SEEK_CUR) >= 0; } /* from glocalfileinputstream.c */ static int fu_unix_seekable_input_stream_seek_type_to_lseek(GSeekType type) { switch (type) { default: case G_SEEK_CUR: return SEEK_CUR; case G_SEEK_SET: return SEEK_SET; case G_SEEK_END: return SEEK_END; } } static gboolean fu_unix_seekable_input_stream_seek(GSeekable *seekable, goffset offset, GSeekType type, GCancellable *cancellable, GError **error) { FuUnixSeekableInputStream *self = FU_UNIX_SEEKABLE_INPUT_STREAM(seekable); goffset rc; g_return_val_if_fail(FU_IS_UNIX_SEEKABLE_INPUT_STREAM(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); rc = lseek(g_unix_input_stream_get_fd(G_UNIX_INPUT_STREAM(self)), offset, fu_unix_seekable_input_stream_seek_type_to_lseek(type)); if (rc < 0) { g_set_error(error, G_IO_ERROR, /* nocheck:error */ g_io_error_from_errno(errno), "Error seeking file descriptor: %s", g_strerror(errno)); return FALSE; } return TRUE; } static gboolean fu_unix_seekable_input_stream_can_truncate(GSeekable *seekable) { return FALSE; } static gboolean fu_unix_seekable_input_stream_truncate(GSeekable *seekable, goffset offset, GCancellable *cancellable, GError **error) { /* using GIOError here as this will eventually go into GLib */ g_set_error_literal(error, G_IO_ERROR, /* nocheck:error */ G_IO_ERROR_NOT_SUPPORTED, /* nocheck:error */ "cannot truncate FuUnixSeekableInputStream"); return FALSE; } static void fu_unix_seekable_input_stream_seekable_iface_init(GSeekableIface *iface) { iface->tell = fu_unix_seekable_input_stream_tell; iface->can_seek = fu_unix_seekable_input_stream_can_seek; iface->seek = fu_unix_seekable_input_stream_seek; iface->can_truncate = fu_unix_seekable_input_stream_can_truncate; iface->truncate_fn = fu_unix_seekable_input_stream_truncate; } /** * fu_unix_seekable_input_stream_new: * @fd: a UNIX file descriptor * @close_fd: %TRUE to close the file descriptor when done * * Creates a new seekable GUnixInputStream for the given fd. * * Returns: (transfer full): a #GInputStream * * Since: 2.0.0 **/ GInputStream * fu_unix_seekable_input_stream_new(gint fd, gboolean close_fd) { return g_object_new(FU_TYPE_UNIX_SEEKABLE_INPUT_STREAM, "fd", fd, "close-fd", close_fd, NULL); } static void fu_unix_seekable_input_stream_class_init(FuUnixSeekableInputStreamClass *klass) { } static void fu_unix_seekable_input_stream_init(FuUnixSeekableInputStream *self) { } fwupd-2.0.10/src/fu-unix-seekable-input-stream.h000066400000000000000000000007441501337203100214510ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #define FU_TYPE_UNIX_SEEKABLE_INPUT_STREAM (fu_unix_seekable_input_stream_get_type()) G_DECLARE_FINAL_TYPE(FuUnixSeekableInputStream, fu_unix_seekable_input_stream, FU, UNIX_SEEKABLE_INPUT_STREAM, GUnixInputStream) GInputStream * fu_unix_seekable_input_stream_new(gint fd, gboolean close_fd); fwupd-2.0.10/src/fu-usb-backend.c000066400000000000000000000312161501337203100164360ustar00rootroot00000000000000/* * Copyright 2011 Richard Hughes * Copyright 2011 Hans de Goede * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuBackend" #include "config.h" #include "fu-context-private.h" #include "fu-usb-backend.h" #include "fu-usb-device-private.h" struct _FuUsbBackend { FuBackend parent_instance; libusb_context *ctx; #ifndef HAVE_UDEV libusb_hotplug_callback_handle hotplug_id; GThread *thread_event; volatile gint thread_event_run; GPtrArray *idle_events; GMutex idle_events_mutex; guint idle_events_id; guint hotplug_poll_id; guint hotplug_poll_interval; #endif }; G_DEFINE_TYPE(FuUsbBackend, fu_usb_backend, FU_TYPE_BACKEND) #define FU_USB_BACKEND_POLL_INTERVAL_DEFAULT 1000 /* ms */ #define FU_USB_BACKEND_POLL_INTERVAL_WAIT_REPLUG 5 /* ms */ #ifndef HAVE_UDEV static gchar * fu_usb_backend_get_usb_device_backend_id(libusb_device *usb_device) { return g_strdup_printf("%02x:%02x", libusb_get_bus_number(usb_device), libusb_get_device_address(usb_device)); } static FuUsbDevice * fu_usb_backend_create_device(FuUsbBackend *self, libusb_device *usb_device) { g_autofree gchar *backend_id = fu_usb_backend_get_usb_device_backend_id(usb_device); return g_object_new(FU_TYPE_USB_DEVICE, "backend", FU_BACKEND(self), "backend-id", backend_id, "libusb-device", usb_device, NULL); } static void fu_usb_backend_add_device(FuUsbBackend *self, libusb_device *usb_device) { FuDevice *device_old; g_autofree gchar *backend_id = fu_usb_backend_get_usb_device_backend_id(usb_device); g_autoptr(FuUsbDevice) device = NULL; device_old = fu_backend_lookup_by_id(FU_BACKEND(self), backend_id); if (device_old != NULL) return; device = fu_usb_backend_create_device(self, usb_device); fu_backend_device_added(FU_BACKEND(self), FU_DEVICE(device)); } static void fu_usb_backend_remove_device(FuUsbBackend *self, libusb_device *usb_device) { g_autofree gchar *backend_id = fu_usb_backend_get_usb_device_backend_id(usb_device); FuDevice *device = fu_backend_lookup_by_id(FU_BACKEND(self), backend_id); if (device != NULL) fu_backend_device_removed(FU_BACKEND(self), device); } static void fu_usb_backend_rescan(FuUsbBackend *self) { libusb_device **dev_list = NULL; g_autoptr(GList) existing_devices = NULL; g_autoptr(GPtrArray) devices = fu_backend_get_devices(FU_BACKEND(self)); /* skip actual enumeration */ if (g_getenv("FWUPD_SELF_TEST") != NULL) return; /* copy to a context so we can remove from the array */ for (guint i = 0; i < devices->len; i++) { FuUsbDevice *device = g_ptr_array_index(devices, i); existing_devices = g_list_prepend(existing_devices, device); } /* look for any removed devices */ libusb_get_device_list(self->ctx, &dev_list); for (GList *l = existing_devices; l != NULL; l = l->next) { FuUsbDevice *device = FU_USB_DEVICE(l->data); gboolean found = FALSE; for (guint i = 0; dev_list != NULL && dev_list[i] != NULL; i++) { if (libusb_get_bus_number(dev_list[i]) == fu_usb_device_get_bus(device) && libusb_get_device_address(dev_list[i]) == fu_usb_device_get_address(device)) { found = TRUE; break; } } if (!found) fu_backend_device_removed(FU_BACKEND(self), FU_DEVICE(device)); } /* add any devices not yet added (duplicates will be filtered */ for (guint i = 0; dev_list != NULL && dev_list[i] != NULL; i++) fu_usb_backend_add_device(self, dev_list[i]); libusb_free_device_list(dev_list, 1); } static gboolean fu_usb_backend_rescan_cb(gpointer user_data) { FuUsbBackend *self = FU_USB_BACKEND(user_data); fu_usb_backend_rescan(self); return TRUE; } static void fu_usb_backend_ensure_rescan_timeout(FuUsbBackend *self) { if (self->hotplug_poll_id > 0) { g_source_remove(self->hotplug_poll_id); self->hotplug_poll_id = 0; } if (self->hotplug_poll_interval > 0) { self->hotplug_poll_id = g_timeout_add(self->hotplug_poll_interval, fu_usb_backend_rescan_cb, self); } } static void fu_usb_backend_set_hotplug_poll_interval(FuUsbBackend *self, guint hotplug_poll_interval) { /* same */ if (self->hotplug_poll_interval == hotplug_poll_interval) return; self->hotplug_poll_interval = hotplug_poll_interval; /* if already running then change the existing timeout */ if (self->hotplug_poll_id > 0) fu_usb_backend_ensure_rescan_timeout(self); } #ifdef _WIN32 static void fu_usb_backend_device_notify_flags_cb(FuDevice *device, GParamSpec *pspec, FuBackend *backend) { FuUsbBackend *self = FU_USB_BACKEND(backend); /* if waiting for a disconnect, set win32 to poll insanely fast -- and set it * back to the default when the device removal was detected */ if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG)) { g_debug("setting USB poll interval to %ums to detect replug", (guint)FU_USB_BACKEND_POLL_INTERVAL_WAIT_REPLUG); fu_usb_backend_set_hotplug_poll_interval(self, FU_USB_BACKEND_POLL_INTERVAL_WAIT_REPLUG); } else { fu_usb_backend_set_hotplug_poll_interval(self, FU_USB_BACKEND_POLL_INTERVAL_DEFAULT); } } #endif typedef struct { FuUsbBackend *self; libusb_device *dev; libusb_hotplug_event event; } FuUsbBackendIdleHelper; static gpointer fu_usb_backend_idle_helper_copy(gconstpointer src, gpointer user_data) { FuUsbBackendIdleHelper *helper_src = (FuUsbBackendIdleHelper *)src; FuUsbBackendIdleHelper *helper_dst = g_new0(FuUsbBackendIdleHelper, 1); helper_dst->self = g_object_ref(helper_src->self); helper_dst->dev = libusb_ref_device(helper_src->dev); helper_dst->event = helper_src->event; return helper_dst; } /* always in the main thread */ static gboolean fu_usb_backend_idle_hotplug_cb(gpointer user_data) { FuUsbBackend *self = FU_USB_BACKEND(user_data); g_autoptr(GPtrArray) idle_events = NULL; /* drain the idle events with the lock held */ g_mutex_lock(&self->idle_events_mutex); idle_events = g_ptr_array_copy(self->idle_events, fu_usb_backend_idle_helper_copy, NULL); g_ptr_array_set_size(self->idle_events, 0); self->idle_events_id = 0; g_mutex_unlock(&self->idle_events_mutex); /* run the callbacks when not locked */ for (guint i = 0; i < idle_events->len; i++) { FuUsbBackendIdleHelper *helper = g_ptr_array_index(idle_events, i); switch (helper->event) { case LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED: fu_usb_backend_add_device(helper->self, helper->dev); break; case LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT: fu_usb_backend_remove_device(helper->self, helper->dev); break; default: break; } } /* all done */ return FALSE; } /* this is run in the libusb thread */ static int LIBUSB_CALL fu_usb_backend_hotplug_cb(struct libusb_context *ctx, struct libusb_device *dev, libusb_hotplug_event event, void *user_data) { FuUsbBackend *self = FU_USB_BACKEND(user_data); FuUsbBackendIdleHelper *helper; g_autoptr(GMutexLocker) locker = g_mutex_locker_new(&self->idle_events_mutex); g_assert(locker != NULL); /* nocheck:blocked */ helper = g_new0(FuUsbBackendIdleHelper, 1); helper->self = g_object_ref(self); helper->dev = libusb_ref_device(dev); helper->event = event; g_ptr_array_add(self->idle_events, helper); if (self->idle_events_id == 0) self->idle_events_id = g_idle_add(fu_usb_backend_idle_hotplug_cb, self); return 0; } static gpointer fu_usb_backend_event_thread_cb(gpointer data) { FuUsbBackend *self = FU_USB_BACKEND(data); struct timeval tv = { .tv_usec = 0, .tv_sec = 2, }; while (g_atomic_int_get(&self->thread_event_run) > 0) libusb_handle_events_timeout_completed(self->ctx, &tv, NULL); return NULL; } #endif static gboolean fu_usb_backend_setup(FuBackend *backend, FuBackendSetupFlags flags, FuProgress *progress, GError **error) { FuUsbBackend *self = FU_USB_BACKEND(backend); FuContext *ctx = fu_backend_get_context(FU_BACKEND(self)); gint log_level = g_getenv("FWUPD_VERBOSE") != NULL ? 3 : 0; gint rc; #ifdef HAVE_LIBUSB_INIT_CONTEXT #ifdef HAVE_UDEV const struct libusb_init_option options[] = {{.option = LIBUSB_OPTION_NO_DEVICE_DISCOVERY, .value = { .ival = 1, }}}; #else const struct libusb_init_option options[] = {}; #endif rc = libusb_init_context(&self->ctx, options, G_N_ELEMENTS(options)); #else rc = libusb_init(&self->ctx); #endif if (rc < 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "failed to init libusb: %s [%i]", libusb_strerror(rc), rc); return FALSE; } #ifdef HAVE_LIBUSB_SET_OPTION libusb_set_option(self->ctx, LIBUSB_OPTION_LOG_LEVEL, log_level); #else libusb_set_debug(self->ctx, log_level); #endif fu_context_set_data(ctx, "libusb_context", self->ctx); fu_context_add_udev_subsystem(ctx, "usb", NULL); /* no hotplug required, probably in tests */ if ((flags & FU_BACKEND_SETUP_FLAG_USE_HOTPLUG) == 0) return TRUE; #ifndef HAVE_UDEV self->thread_event_run = 1; self->thread_event = g_thread_new("FuUsbBackendEvt", fu_usb_backend_event_thread_cb, self); /* watch for add/remove */ if (libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) { rc = libusb_hotplug_register_callback(self->ctx, LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED | LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT, 0, LIBUSB_HOTPLUG_MATCH_ANY, LIBUSB_HOTPLUG_MATCH_ANY, LIBUSB_HOTPLUG_MATCH_ANY, fu_usb_backend_hotplug_cb, self, &self->hotplug_id); if (rc != LIBUSB_SUCCESS) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "error creating a hotplug callback: %s [%i]", libusb_strerror(rc), rc); return FALSE; } } else { fu_usb_backend_set_hotplug_poll_interval(self, FU_USB_BACKEND_POLL_INTERVAL_DEFAULT); } #endif /* success */ return TRUE; } #ifndef HAVE_UDEV static gboolean fu_usb_backend_coldplug(FuBackend *backend, FuProgress *progress, GError **error) { FuUsbBackend *self = FU_USB_BACKEND(backend); fu_usb_backend_rescan(self); return TRUE; } static void fu_usb_backend_registered(FuBackend *backend, FuDevice *device) { #ifdef _WIN32 /* not required */ if (!FU_IS_USB_DEVICE(device)) return; /* on win32 we need to poll the context faster */ g_signal_connect(FU_DEVICE(device), "notify::flags", G_CALLBACK(fu_usb_backend_device_notify_flags_cb), backend); #endif } static FuDevice * fu_usb_backend_get_device_parent(FuBackend *backend, FuDevice *device, const gchar *subsystem, GError **error) { FuUsbBackend *self = FU_USB_BACKEND(backend); libusb_device *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device)); libusb_device *usb_parent; /* libusb or kernel */ if (usb_device == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no device"); return NULL; } usb_parent = libusb_get_parent(usb_device); if (usb_parent == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no parent"); return NULL; } return FU_DEVICE(fu_usb_backend_create_device(self, usb_parent)); } #endif static void fu_usb_backend_finalize(GObject *object) { #ifndef HAVE_UDEV FuUsbBackend *self = FU_USB_BACKEND(object); /* this is safe to call even when self->hotplug_id is unset */ if (g_atomic_int_dec_and_test(&self->thread_event_run)) { libusb_hotplug_deregister_callback(self->ctx, self->hotplug_id); g_thread_join(self->thread_event); } if (self->idle_events_id > 0) g_source_remove(self->idle_events_id); if (self->hotplug_poll_id > 0) g_source_remove(self->hotplug_poll_id); if (self->ctx != NULL) libusb_exit(self->ctx); g_clear_pointer(&self->idle_events, g_ptr_array_unref); g_mutex_clear(&self->idle_events_mutex); #endif G_OBJECT_CLASS(fu_usb_backend_parent_class)->finalize(object); } #ifndef HAVE_UDEV static void fu_usb_backend_idle_helper_free(FuUsbBackendIdleHelper *helper) { g_object_unref(helper->self); libusb_unref_device(helper->dev); g_free(helper); } #endif static void fu_usb_backend_init(FuUsbBackend *self) { #ifndef HAVE_UDEV /* to escape the thread into the mainloop */ g_mutex_init(&self->idle_events_mutex); self->idle_events = g_ptr_array_new_with_free_func((GDestroyNotify)fu_usb_backend_idle_helper_free); #endif } static void fu_usb_backend_class_init(FuUsbBackendClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); FuBackendClass *backend_class = FU_BACKEND_CLASS(klass); object_class->finalize = fu_usb_backend_finalize; backend_class->setup = fu_usb_backend_setup; #ifndef HAVE_UDEV backend_class->coldplug = fu_usb_backend_coldplug; backend_class->registered = fu_usb_backend_registered; backend_class->get_device_parent = fu_usb_backend_get_device_parent; #endif } FuBackend * fu_usb_backend_new(FuContext *ctx) { return FU_BACKEND(g_object_new(FU_TYPE_USB_BACKEND, "name", "usb", "context", ctx, NULL)); } fwupd-2.0.10/src/fu-usb-backend.h000066400000000000000000000005421501337203100164410ustar00rootroot00000000000000/* * Copyright 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "fu-backend.h" #define FU_TYPE_USB_BACKEND (fu_usb_backend_get_type()) G_DECLARE_FINAL_TYPE(FuUsbBackend, fu_usb_backend, FU, USB_BACKEND, FuBackend) FuBackend * fu_usb_backend_new(FuContext *ctx) G_GNUC_NON_NULL(1); fwupd-2.0.10/src/fu-util-bios-setting.c000066400000000000000000000150451501337203100176440ustar00rootroot00000000000000/* * Copyright 2022 Mario Limonciello * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuMain" #include "config.h" #include #include "fu-bios-settings-private.h" #include "fu-util-bios-setting.h" #include "fu-util-common.h" static void fu_util_bios_setting_update_description(FwupdBiosSetting *setting) { const gchar *new = NULL; /* try to look it up from translations */ new = gettext(fwupd_bios_setting_get_description(setting)); if (new != NULL) fwupd_bios_setting_set_description(setting, new); } static const gchar * fu_util_bios_setting_kind_to_string(FwupdBiosSettingKind kind) { if (kind == FWUPD_BIOS_SETTING_KIND_ENUMERATION) { /* TRANSLATORS: The BIOS setting can only be changed to fixed values */ return _("Enumeration"); } if (kind == FWUPD_BIOS_SETTING_KIND_INTEGER) { /* TRANSLATORS: The BIOS setting only accepts integers in a fixed range */ return _("Integer"); } if (kind == FWUPD_BIOS_SETTING_KIND_STRING) { /* TRANSLATORS: The BIOS setting accepts strings */ return _("String"); } return NULL; } gboolean fu_util_bios_setting_matches_args(FwupdBiosSetting *setting, gchar **values) { const gchar *name; /* no arguments set */ if (g_strv_length(values) == 0) return TRUE; name = fwupd_bios_setting_get_name(setting); /* check all arguments */ for (guint j = 0; j < g_strv_length(values); j++) { if (g_strcmp0(name, values[j]) == 0) return TRUE; } return FALSE; } gboolean fu_util_bios_setting_console_print(FuConsole *console, gchar **values, GPtrArray *settings, GError **error) { g_autoptr(JsonBuilder) builder = json_builder_new(); json_builder_begin_object(builder); json_builder_set_member_name(builder, "BiosSettings"); json_builder_begin_array(builder); for (guint i = 0; i < settings->len; i++) { FwupdBiosSetting *setting = g_ptr_array_index(settings, i); if (fu_util_bios_setting_matches_args(setting, values)) { fu_util_bios_setting_update_description(setting); json_builder_begin_object(builder); fwupd_codec_to_json(FWUPD_CODEC(setting), builder, FWUPD_CODEC_FLAG_NONE); json_builder_end_object(builder); } } json_builder_end_array(builder); json_builder_end_object(builder); return fu_util_print_builder(console, builder, error); } gchar * fu_util_bios_setting_to_string(FwupdBiosSetting *setting, guint idt) { const gchar *tmp; FwupdBiosSettingKind type; g_autofree gchar *debug_str = NULL; g_autofree gchar *current_value = NULL; g_autoptr(GString) str = g_string_new(NULL); debug_str = fwupd_codec_to_string(FWUPD_CODEC(setting)); g_debug("%s", debug_str); tmp = fwupd_bios_setting_get_name(setting); fwupd_codec_string_append(str, idt, tmp, ""); type = fwupd_bios_setting_get_kind(setting); fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: type of BIOS setting */ _("Setting type"), fu_util_bios_setting_kind_to_string(type)); tmp = fwupd_bios_setting_get_current_value(setting); if (tmp != NULL) { current_value = g_strdup(tmp); } else { /* TRANSLATORS: tell a user how to get information */ current_value = g_strdup_printf(_("Run without '%s' to see"), "--no-authenticate"); } /* TRANSLATORS: current value of a BIOS setting */ fwupd_codec_string_append(str, idt + 1, _("Current Value"), current_value); fu_util_bios_setting_update_description(setting); fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: description of BIOS setting */ _("Description"), fwupd_bios_setting_get_description(setting)); if (fwupd_bios_setting_get_read_only(setting)) { /* TRANSLATORS: item is TRUE */ tmp = _("True"); } else { /* TRANSLATORS: item is FALSE */ tmp = _("False"); } /* TRANSLATORS: BIOS setting is read only */ fwupd_codec_string_append(str, idt + 1, _("Read Only"), tmp); if (type == FWUPD_BIOS_SETTING_KIND_INTEGER || type == FWUPD_BIOS_SETTING_KIND_STRING) { g_autofree gchar *lower = g_strdup_printf("%" G_GUINT64_FORMAT, fwupd_bios_setting_get_lower_bound(setting)); g_autofree gchar *upper = g_strdup_printf("%" G_GUINT64_FORMAT, fwupd_bios_setting_get_upper_bound(setting)); if (type == FWUPD_BIOS_SETTING_KIND_INTEGER) { g_autofree gchar *scalar = g_strdup_printf("%" G_GUINT64_FORMAT, fwupd_bios_setting_get_scalar_increment(setting)); /* TRANSLATORS: Lowest valid integer for BIOS setting */ fwupd_codec_string_append(str, idt + 1, _("Minimum value"), lower); /* TRANSLATORS: Highest valid integer for BIOS setting */ fwupd_codec_string_append(str, idt + 1, _("Maximum value"), upper); fwupd_codec_string_append( str, idt + 1, /* TRANSLATORS: Scalar increment for integer BIOS setting */ _("Scalar Increment"), scalar); } else { /* TRANSLATORS: Shortest valid string for BIOS setting */ fwupd_codec_string_append(str, idt + 1, _("Minimum length"), lower); /* TRANSLATORS: Longest valid string for BIOS setting */ fwupd_codec_string_append(str, idt + 1, _("Maximum length"), upper); } } else if (type == FWUPD_BIOS_SETTING_KIND_ENUMERATION) { GPtrArray *values = fwupd_bios_setting_get_possible_values(setting); if (values != NULL && values->len > 0) { /* TRANSLATORS: Possible values for a bios setting */ fwupd_codec_string_append(str, idt + 1, _("Possible Values"), NULL); for (guint i = 0; i < values->len; i++) { const gchar *possible = g_ptr_array_index(values, i); g_autofree gchar *index = g_strdup_printf("%u", i); fwupd_codec_string_append(str, idt + 2, index, possible); } } } return g_string_free(g_steal_pointer(&str), FALSE); } GHashTable * fu_util_bios_settings_parse_argv(gchar **input, GError **error) { GHashTable *bios_settings; /* json input */ if (g_strv_length(input) == 1) { g_autofree gchar *data = NULL; g_autoptr(FuBiosSettings) new_bios_settings = fu_bios_settings_new(); if (!g_file_get_contents(input[0], &data, NULL, error)) return NULL; if (!fwupd_codec_from_json_string(FWUPD_CODEC(new_bios_settings), data, error)) return NULL; return fu_bios_settings_to_hash_kv(new_bios_settings); } if (g_strv_length(input) == 0 || g_strv_length(input) % 2) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, /* TRANSLATORS: error message */ _("Invalid arguments")); return NULL; } bios_settings = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); for (guint i = 0; i < g_strv_length(input); i += 2) g_hash_table_insert(bios_settings, g_strdup(input[i]), g_strdup(input[i + 1])); return bios_settings; } fwupd-2.0.10/src/fu-util-bios-setting.h000066400000000000000000000012041501337203100176410ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "fu-console.h" gchar * fu_util_bios_setting_to_string(FwupdBiosSetting *setting, guint idt) G_GNUC_NON_NULL(1); gboolean fu_util_bios_setting_matches_args(FwupdBiosSetting *setting, gchar **values) G_GNUC_NON_NULL(1, 2); gboolean fu_util_bios_setting_console_print(FuConsole *console, gchar **values, GPtrArray *settings, GError **error) G_GNUC_NON_NULL(1, 2, 3); GHashTable * fu_util_bios_settings_parse_argv(gchar **input, GError **error) G_GNUC_NON_NULL(1); fwupd-2.0.10/src/fu-util-common.c000066400000000000000000003072721501337203100165330ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuMain" #include "config.h" #include #include #include #include #include #include "fu-console.h" #include "fu-device-private.h" #include "fu-util-common.h" static gchar * fu_util_remote_to_string(FwupdRemote *remote, guint idt); static gchar * fu_util_release_to_string(FwupdRelease *rel, guint idt); static gchar * fu_util_convert_description(const gchar *xml, GError **error); gchar * fu_console_color_format(const gchar *text, FuConsoleColor fg_color) /* nocheck:name */ { if (g_getenv("NO_COLOR") != NULL) return g_strdup(text); return g_strdup_printf("\033[%um\033[1m%s\033[0m", fg_color, text); } typedef struct { FwupdClient *client; FuConsole *console; } FuUtilPrintTreeHelper; static gboolean fu_util_traverse_tree(FuUtilNode *n, gpointer data) { FuUtilPrintTreeHelper *helper = (FuUtilPrintTreeHelper *)data; guint idx = g_node_depth(n) - 1; g_autofree gchar *tmp = NULL; g_auto(GStrv) split = NULL; /* get split lines */ if (FWUPD_IS_DEVICE(n->data)) { FwupdDevice *dev = FWUPD_DEVICE(n->data); tmp = fu_util_device_to_string(helper->client, dev, idx); } else if (FWUPD_IS_REMOTE(n->data)) { FwupdRemote *remote = FWUPD_REMOTE(n->data); tmp = fu_util_remote_to_string(remote, idx); } else if (FWUPD_IS_RELEASE(n->data)) { FwupdRelease *release = FWUPD_RELEASE(n->data); tmp = fu_util_release_to_string(release, idx); g_info("%s", tmp); } /* root node */ if (n->parent == NULL && g_getenv("FWUPD_VERBOSE") == NULL) { g_autofree gchar *str = g_strdup_printf("%s %s", fwupd_client_get_host_vendor(helper->client), fwupd_client_get_host_product(helper->client)); fu_console_print_literal(helper->console, str); fu_console_print_literal(helper->console, "│"); return FALSE; } if (n->parent == NULL) return FALSE; if (tmp == NULL) return FALSE; split = g_strsplit(tmp, "\n", -1); for (guint i = 0; split[i] != NULL; i++) { g_autoptr(GString) str = g_string_new(NULL); /* header */ if (i == 0) { if (g_node_next_sibling(n) == NULL) g_string_prepend(str, "└─"); else g_string_prepend(str, "├─"); /* properties */ } else { g_string_prepend(str, n->children == NULL ? " " : " │"); g_string_prepend(str, g_node_next_sibling(n) == NULL ? " " : "│"); g_string_append(str, " "); } /* ancestors */ for (GNode *c = n->parent; c != NULL && c->parent != NULL; c = c->parent) { if (g_node_next_sibling(c) != NULL || idx == 0) { g_string_prepend(str, "│ "); continue; } g_string_prepend(str, " "); } /* empty line */ if (split[i][0] == '\0') { fu_console_print_literal(helper->console, str->str); continue; } /* dump to the console */ g_string_append(str, split[i] + (idx * 2)); fu_console_print_literal(helper->console, str->str); } return FALSE; } static gboolean fu_util_free_tree_cb(FuUtilNode *n, gpointer data) { if (n->data != NULL) g_object_unref(n->data); return FALSE; } void fu_util_free_node(FuUtilNode *n) { g_node_traverse(n, G_POST_ORDER, G_TRAVERSE_ALL, -1, fu_util_free_tree_cb, NULL); g_node_destroy(n); } void fu_util_print_node(FuConsole *console, FwupdClient *client, FuUtilNode *n) { FuUtilPrintTreeHelper helper = {.client = client, .console = console}; g_node_traverse(n, G_PRE_ORDER, G_TRAVERSE_ALL, -1, fu_util_traverse_tree, &helper); } static gboolean fu_util_is_interesting_child(FwupdDevice *dev) { GPtrArray *children = fwupd_device_get_children(dev); for (guint i = 0; i < children->len; i++) { FwupdDevice *child = g_ptr_array_index(children, i); if (fu_util_is_interesting_device(child)) return TRUE; } return FALSE; } gboolean fu_util_is_interesting_device(FwupdDevice *dev) { if (fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE)) return TRUE; if (fwupd_device_get_update_error(dev) != NULL) return TRUE; if (fwupd_device_get_version(dev) != NULL) return TRUE; /* device not plugged in, get-details */ if (fwupd_device_get_flags(dev) == 0) return TRUE; if (fu_util_is_interesting_child(dev)) return TRUE; return FALSE; } gchar * fu_util_get_user_cache_path(const gchar *fn) { const gchar *root = g_get_user_cache_dir(); g_autofree gchar *basename = g_path_get_basename(fn); g_autofree gchar *cachedir_legacy = NULL; /* if run from a systemd unit, use the cache directory set there */ if (g_getenv("CACHE_DIRECTORY") != NULL) root = g_getenv("CACHE_DIRECTORY"); /* return the legacy path if it exists rather than renaming it to * prevent problems when using old and new versions of fwupd */ cachedir_legacy = g_build_filename(root, "fwupdmgr", NULL); if (g_file_test(cachedir_legacy, G_FILE_TEST_IS_DIR)) return g_build_filename(cachedir_legacy, basename, NULL); return g_build_filename(root, "fwupd", basename, NULL); } static gboolean fu_util_update_shutdown(GError **error) { g_autoptr(GDBusConnection) connection = NULL; g_autoptr(GVariant) val = NULL; connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, error); if (connection == NULL) return FALSE; #ifdef HAVE_LOGIND /* shutdown using logind */ val = g_dbus_connection_call_sync(connection, "org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", "PowerOff", g_variant_new("(b)", TRUE), NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, error); #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "No way to perform the operation"); #endif return val != NULL; } static gboolean fu_util_update_reboot(GError **error) { g_autoptr(GDBusConnection) connection = NULL; g_autoptr(GVariant) val = NULL; connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, error); if (connection == NULL) return FALSE; #ifdef HAVE_LOGIND /* reboot using logind */ val = g_dbus_connection_call_sync(connection, "org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", "Reboot", g_variant_new("(b)", TRUE), NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, error); #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "No way to perform the operation"); #endif return val != NULL; } static gchar * fu_util_get_release_description_with_fallback(FwupdRelease *rel) { g_autoptr(GString) str = g_string_new(NULL); /* add what we've got from the vendor */ if (fwupd_release_get_description(rel) != NULL) g_string_append(str, fwupd_release_get_description(rel)); /* add this client side to get the translations */ if (fwupd_release_has_flag(rel, FWUPD_RELEASE_FLAG_IS_COMMUNITY)) { g_string_append_printf( str, "

    %s

    ", /* TRANSLATORS: the vendor did not upload this */ _("This firmware is provided by LVFS community members and is not " "provided (or supported) by the original hardware vendor.")); g_string_append_printf( str, "

    %s

    ", /* TRANSLATORS: if it breaks, you get to keep both pieces */ _("Installing this update may also void any device warranty.")); } /* this can't be from the LVFS, but the user could be installing a local file */ if (str->len == 0) { g_string_append_printf(str, "

    %s

    ", /* TRANSLATORS: naughty vendor */ _("The vendor did not supply any release notes.")); } return g_string_free(g_steal_pointer(&str), FALSE); } gboolean fu_util_prompt_warning(FuConsole *console, FwupdDevice *device, FwupdRelease *release, const gchar *machine, GError **error) { FwupdDeviceFlags flags; gint vercmp; g_autofree gchar *desc_fb = NULL; g_autoptr(GString) title = g_string_new(NULL); g_autoptr(GString) str = g_string_new(NULL); /* up, down, or re-install */ vercmp = fu_version_compare(fwupd_release_get_version(release), fu_device_get_version(device), fwupd_device_get_version_format(device)); if (vercmp < 0) { g_string_append_printf( title, /* TRANSLATORS: message letting the user know an downgrade is available * %1 is the device name and %2 and %3 are version strings */ _("Downgrade %s from %s to %s?"), fwupd_device_get_name(device), fwupd_device_get_version(device), fwupd_release_get_version(release)); } else if (vercmp > 0) { g_string_append_printf( title, /* TRANSLATORS: message letting the user know an upgrade is available * %1 is the device name and %2 and %3 are version strings */ _("Upgrade %s from %s to %s?"), fwupd_device_get_name(device), fwupd_device_get_version(device), fwupd_release_get_version(release)); } else { g_string_append_printf( title, /* TRANSLATORS: message letting the user know an upgrade is available * %1 is the device name and %2 is a version string */ _("Reinstall %s to %s?"), fwupd_device_get_name(device), fwupd_release_get_version(release)); } /* description is optional */ desc_fb = fu_util_get_release_description_with_fallback(release); if (desc_fb != NULL) { g_autofree gchar *desc = fu_util_convert_description(desc_fb, NULL); if (desc != NULL) g_string_append_printf(str, "\n%s", desc); } /* device is not already in bootloader mode so show warning */ flags = fwupd_device_get_flags(device); if ((flags & FWUPD_DEVICE_FLAG_IS_BOOTLOADER) == 0) { /* device may reboot */ if ((flags & FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE) == 0) { g_string_append(str, "\n\n"); g_string_append_printf( str, /* TRANSLATORS: warn the user before updating, %1 is a device name */ _("%s and all connected devices may not be usable while updating."), fwupd_device_get_name(device)); /* device can get bricked */ } else if ((flags & FWUPD_DEVICE_FLAG_SELF_RECOVERY) == 0) { g_string_append(str, "\n\n"); /* external device */ if ((flags & FWUPD_DEVICE_FLAG_INTERNAL) == 0) { g_string_append_printf(str, /* TRANSLATORS: warn the user before * updating, %1 is a device name */ _("%s must remain connected for the " "duration of the update to avoid damage."), fwupd_device_get_name(device)); } else if (flags & FWUPD_DEVICE_FLAG_REQUIRE_AC) { g_string_append_printf( str, /* TRANSLATORS: warn the user before updating, %1 is a machine * name */ _("%s must remain plugged into a power source for the duration " "of the update to avoid damage."), machine); } } } fu_console_box(console, title->str, str->str, 80); /* TRANSLATORS: prompt to apply the update */ if (!fu_console_input_bool(console, TRUE, "%s", _("Perform operation?"))) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "Request canceled"); return FALSE; } /* success */ return TRUE; } gboolean fu_util_prompt_complete(FuConsole *console, FwupdDeviceFlags flags, gboolean prompt, GError **error) { if (flags & FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN) { if (prompt) { if (!fu_console_input_bool(console, FALSE, "%s %s", /* TRANSLATORS: explain why */ _("An update requires the system to shutdown " "to complete."), /* TRANSLATORS: shutdown to apply the update */ _("Shutdown now?"))) return TRUE; } return fu_util_update_shutdown(error); } if (flags & FWUPD_DEVICE_FLAG_NEEDS_REBOOT) { if (prompt) { if (!fu_console_input_bool(console, FALSE, "%s %s", /* TRANSLATORS: explain why we want to reboot */ _("An update requires a reboot to complete."), /* TRANSLATORS: reboot to apply the update */ _("Restart now?"))) return TRUE; } return fu_util_update_reboot(error); } return TRUE; } static void fu_util_cmd_free(FuUtilCmd *item) { g_free(item->name); g_free(item->arguments); g_free(item->description); g_free(item); } GPtrArray * fu_util_cmd_array_new(void) { return g_ptr_array_new_with_free_func((GDestroyNotify)fu_util_cmd_free); } static gint fu_util_cmd_sort_cb(FuUtilCmd **item1, FuUtilCmd **item2) { return g_strcmp0((*item1)->name, (*item2)->name); } void fu_util_cmd_array_sort(GPtrArray *array) { g_ptr_array_sort(array, (GCompareFunc)fu_util_cmd_sort_cb); } void fu_util_cmd_array_add(GPtrArray *array, const gchar *name, const gchar *arguments, const gchar *description, FuUtilCmdFunc callback) { g_auto(GStrv) names = NULL; g_return_if_fail(name != NULL); g_return_if_fail(description != NULL); g_return_if_fail(callback != NULL); /* add each one */ names = g_strsplit(name, ",", -1); for (guint i = 0; names[i] != NULL; i++) { FuUtilCmd *item = g_new0(FuUtilCmd, 1); item->name = g_strdup(names[i]); if (i == 0) { item->description = g_strdup(description); } else { /* TRANSLATORS: this is a command alias, e.g. 'get-devices' */ item->description = g_strdup_printf(_("Alias to %s"), names[0]); } item->arguments = g_strdup(arguments); item->callback = callback; g_ptr_array_add(array, item); } } gboolean fu_util_cmd_array_run(GPtrArray *array, FuUtilPrivate *priv, const gchar *command, gchar **values, GError **error) { g_auto(GStrv) values_copy = g_new0(gchar *, g_strv_length(values) + 1); /* clear out bash completion sentinel */ for (guint i = 0; values[i] != NULL; i++) { if (g_strcmp0(values[i], "{") == 0) /* nocheck:depth */ break; values_copy[i] = g_strdup(values[i]); } /* find command */ for (guint i = 0; i < array->len; i++) { FuUtilCmd *item = g_ptr_array_index(array, i); if (g_strcmp0(item->name, command) == 0) return item->callback(priv, values_copy, error); } /* not found */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, /* TRANSLATORS: error message */ _("Command not found")); return FALSE; } gchar * fu_util_cmd_array_to_string(GPtrArray *array) { gsize len; const gsize max_len = 35; GString *string; /* print each command */ string = g_string_new(""); for (guint i = 0; i < array->len; i++) { FuUtilCmd *item = g_ptr_array_index(array, i); g_string_append(string, " "); g_string_append(string, item->name); len = fu_strwidth(item->name) + 2; if (item->arguments != NULL) { g_string_append(string, " "); g_string_append(string, item->arguments); len += fu_strwidth(item->arguments) + 1; } if (len < max_len) { for (gsize j = len; j < max_len + 1; j++) g_string_append_c(string, ' '); g_string_append(string, item->description); g_string_append_c(string, '\n'); } else { g_string_append_c(string, '\n'); for (gsize j = 0; j < max_len + 1; j++) g_string_append_c(string, ' '); g_string_append(string, item->description); g_string_append_c(string, '\n'); } } /* remove trailing newline */ if (string->len > 0) g_string_set_size(string, string->len - 1); return g_string_free(string, FALSE); } const gchar * fu_util_branch_for_display(const gchar *branch) { if (branch == NULL) { /* TRANSLATORS: this is the default branch name when unset */ return _("default"); } return branch; } static gchar * fu_util_release_get_name(FwupdRelease *release) { const gchar *name = fwupd_release_get_name(release); GPtrArray *cats = fwupd_release_get_categories(release); for (guint i = 0; i < cats->len; i++) { const gchar *cat = g_ptr_array_index(cats, i); if (g_strcmp0(cat, "X-Device") == 0) { /* TRANSLATORS: a specific part of hardware, * the first %s is the device name, e.g. 'Unifying Receiver` */ return g_strdup_printf(_("%s Device Update"), name); } if (g_strcmp0(cat, "X-Configuration") == 0) { /* TRANSLATORS: a specific part of hardware, * the first %s is the device name, e.g. 'Secure Boot` */ return g_strdup_printf(_("%s Configuration Update"), name); } if (g_strcmp0(cat, "X-System") == 0) { /* TRANSLATORS: the entire system, e.g. all internal devices, * the first %s is the device name, e.g. 'ThinkPad P50` */ return g_strdup_printf(_("%s System Update"), name); } if (g_strcmp0(cat, "X-EmbeddedController") == 0) { /* TRANSLATORS: the EC is typically the keyboard controller chip, * the first %s is the device name, e.g. 'ThinkPad P50` */ return g_strdup_printf(_("%s Embedded Controller Update"), name); } if (g_strcmp0(cat, "X-ManagementEngine") == 0) { /* TRANSLATORS: ME stands for Management Engine, the Intel AMT thing, * the first %s is the device name, e.g. 'ThinkPad P50` */ return g_strdup_printf(_("%s ME Update"), name); } if (g_strcmp0(cat, "X-CorporateManagementEngine") == 0) { /* TRANSLATORS: ME stands for Management Engine (with Intel AMT), * where the first %s is the device name, e.g. 'ThinkPad P50` */ return g_strdup_printf(_("%s Corporate ME Update"), name); } if (g_strcmp0(cat, "X-ConsumerManagementEngine") == 0) { /* TRANSLATORS: ME stands for Management Engine, where * the first %s is the device name, e.g. 'ThinkPad P50` */ return g_strdup_printf(_("%s Consumer ME Update"), name); } if (g_strcmp0(cat, "X-Controller") == 0) { /* TRANSLATORS: the controller is a device that has other devices * plugged into it, for example ThunderBolt, FireWire or USB, * the first %s is the device name, e.g. 'Intel ThunderBolt` */ return g_strdup_printf(_("%s Controller Update"), name); } if (g_strcmp0(cat, "X-ThunderboltController") == 0) { /* TRANSLATORS: the Thunderbolt controller is a device that * has other high speed Thunderbolt devices plugged into it; * the first %s is the system name, e.g. 'ThinkPad P50` */ return g_strdup_printf(_("%s Thunderbolt Controller Update"), name); } if (g_strcmp0(cat, "X-CpuMicrocode") == 0) { /* TRANSLATORS: the CPU microcode is firmware loaded onto the CPU * at system bootup */ return g_strdup_printf(_("%s CPU Microcode Update"), name); } if (g_strcmp0(cat, "X-Battery") == 0) { /* TRANSLATORS: battery refers to the system power source */ return g_strdup_printf(_("%s Battery Update"), name); } if (g_strcmp0(cat, "X-Camera") == 0) { /* TRANSLATORS: camera can refer to the laptop internal * camera in the bezel or external USB webcam */ return g_strdup_printf(_("%s Camera Update"), name); } if (g_strcmp0(cat, "X-TPM") == 0) { /* TRANSLATORS: TPM refers to a Trusted Platform Module */ return g_strdup_printf(_("%s TPM Update"), name); } if (g_strcmp0(cat, "X-Touchpad") == 0) { /* TRANSLATORS: TouchPad refers to a flat input device */ return g_strdup_printf(_("%s Touchpad Update"), name); } if (g_strcmp0(cat, "X-Mouse") == 0) { /* TRANSLATORS: Mouse refers to a handheld input device */ return g_strdup_printf(_("%s Mouse Update"), name); } if (g_strcmp0(cat, "X-Keyboard") == 0) { /* TRANSLATORS: Keyboard refers to an input device for typing */ return g_strdup_printf(_("%s Keyboard Update"), name); } if (g_strcmp0(cat, "X-StorageController") == 0) { /* TRANSLATORS: Storage Controller is typically a RAID or SAS adapter */ return g_strdup_printf(_("%s Storage Controller Update"), name); } if (g_strcmp0(cat, "X-NetworkInterface") == 0) { /* TRANSLATORS: Network Interface refers to the physical * PCI card, not the logical wired connection */ return g_strdup_printf(_("%s Network Interface Update"), name); } if (g_strcmp0(cat, "X-VideoDisplay") == 0) { /* TRANSLATORS: Video Display refers to the laptop internal display or * external monitor */ return g_strdup_printf(_("%s Display Update"), name); } if (g_strcmp0(cat, "X-BaseboardManagementController") == 0) { /* TRANSLATORS: BMC refers to baseboard management controller which * is the device that updates all the other firmware on the system */ return g_strdup_printf(_("%s BMC Update"), name); } if (g_strcmp0(cat, "X-UsbReceiver") == 0) { /* TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth * device that stays in the USB port so the wireless peripheral works */ return g_strdup_printf(_("%s USB Receiver Update"), name); } if (g_strcmp0(cat, "X-Drive") == 0) { /* TRANSLATORS: drive refers to a storage device, e.g. SATA disk */ return g_strdup_printf(_("%s Drive Update"), name); } if (g_strcmp0(cat, "X-FlashDrive") == 0) { /* TRANSLATORS: flash refers to solid state storage, e.g. UFS or eMMC */ return g_strdup_printf(_("%s Flash Drive Update"), name); } if (g_strcmp0(cat, "X-SolidStateDrive") == 0) { /* TRANSLATORS: SSD refers to a Solid State Drive, e.g. non-rotating * SATA or NVMe disk */ return g_strdup_printf(_("%s SSD Update"), name); } if (g_strcmp0(cat, "X-Gpu") == 0) { /* TRANSLATORS: GPU refers to a Graphics Processing Unit, e.g. * the "video card" */ return g_strdup_printf(_("%s GPU Update"), name); } if (g_strcmp0(cat, "X-Dock") == 0) { /* TRANSLATORS: Dock refers to the port replicator hardware laptops are * cradled in, or lowered onto */ return g_strdup_printf(_("%s Dock Update"), name); } if (g_strcmp0(cat, "X-UsbDock") == 0) { /* TRANSLATORS: Dock refers to the port replicator device connected * by plugging in a USB cable -- which may or may not also provide power */ return g_strdup_printf(_("%s USB Dock Update"), name); } if (g_strcmp0(cat, "X-FingerprintReader") == 0) { /* TRANSLATORS: a device that can read your fingerprint pattern */ return g_strdup_printf(_("%s Fingerprint Reader Update"), name); } if (g_strcmp0(cat, "X-GraphicsTablet") == 0) { /* TRANSLATORS: a large pressure-sensitive drawing area typically used * by artists and digital artists */ return g_strdup_printf(_("%s Graphics Tablet Update"), name); } if (g_strcmp0(cat, "X-InputController") == 0) { /* TRANSLATORS: an input device used by gamers, e.g. a joystick */ return g_strdup_printf(_("%s Input Controller Update"), name); } if (g_strcmp0(cat, "X-Headphones") == 0) { /* TRANSLATORS: two miniature speakers attached to your ears */ return g_strdup_printf(_("%s Headphones Update"), name); } if (g_strcmp0(cat, "X-Headset") == 0) { /* TRANSLATORS: headphones with an integrated microphone */ return g_strdup_printf(_("%s Headset Update"), name); } } /* TRANSLATORS: this is the fallback where we don't know if the release * is updating the system, the device, or a device class, or something else -- * the first %s is the device name, e.g. 'ThinkPad P50` */ return g_strdup_printf(_("%s Update"), name); } gboolean fu_util_parse_filter_device_flags(const gchar *filter, FwupdDeviceFlags *include, FwupdDeviceFlags *exclude, GError **error) { FwupdDeviceFlags tmp; g_auto(GStrv) strv = g_strsplit(filter, ",", -1); g_return_val_if_fail(include != NULL, FALSE); g_return_val_if_fail(exclude != NULL, FALSE); for (guint i = 0; strv[i] != NULL; i++) { if (g_str_has_prefix(strv[i], "~")) { tmp = fwupd_device_flag_from_string(strv[i] + 1); if (tmp == FWUPD_DEVICE_FLAG_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Unknown device flag %s", strv[i] + 1); return FALSE; } if ((tmp & *include) > 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Filter %s already included", fwupd_device_flag_to_string(tmp)); return FALSE; } if ((tmp & *exclude) > 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Filter %s already excluded", fwupd_device_flag_to_string(tmp)); return FALSE; } *exclude |= tmp; } else { tmp = fwupd_device_flag_from_string(strv[i]); if (tmp == FWUPD_DEVICE_FLAG_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Unknown device flag %s", strv[i]); return FALSE; } if ((tmp & *exclude) > 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Filter %s already excluded", fwupd_device_flag_to_string(tmp)); return FALSE; } if ((tmp & *include) > 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Filter %s already included", fwupd_device_flag_to_string(tmp)); return FALSE; } *include |= tmp; } } return TRUE; } gboolean fu_util_parse_filter_release_flags(const gchar *filter, FwupdReleaseFlags *include, FwupdReleaseFlags *exclude, GError **error) { FwupdReleaseFlags tmp; g_auto(GStrv) strv = g_strsplit(filter, ",", -1); g_return_val_if_fail(include != NULL, FALSE); g_return_val_if_fail(exclude != NULL, FALSE); for (guint i = 0; strv[i] != NULL; i++) { if (g_str_has_prefix(strv[i], "~")) { tmp = fwupd_release_flag_from_string(strv[i] + 1); if (tmp == FWUPD_RELEASE_FLAG_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Unknown release flag %s", strv[i] + 1); return FALSE; } if ((tmp & *include) > 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Filter %s already included", fwupd_release_flag_to_string(tmp)); return FALSE; } if ((tmp & *exclude) > 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Filter %s already excluded", fwupd_release_flag_to_string(tmp)); return FALSE; } *exclude |= tmp; } else { tmp = fwupd_release_flag_from_string(strv[i]); if (tmp == FWUPD_RELEASE_FLAG_UNKNOWN) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Unknown release flag %s", strv[i]); return FALSE; } if ((tmp & *exclude) > 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Filter %s already excluded", fwupd_release_flag_to_string(tmp)); return FALSE; } if ((tmp & *include) > 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Filter %s already included", fwupd_release_flag_to_string(tmp)); return FALSE; } *include |= tmp; } } return TRUE; } typedef struct { guint cnt; GString *str; } FuUtilConvertHelper; static gboolean fu_util_convert_description_head_cb(XbNode *n, gpointer user_data) { FuUtilConvertHelper *helper = (FuUtilConvertHelper *)user_data; helper->cnt++; /* start */ if (g_strcmp0(xb_node_get_element(n), "em") == 0) { g_string_append(helper->str, "\033[3m"); } else if (g_strcmp0(xb_node_get_element(n), "strong") == 0) { g_string_append(helper->str, "\033[1m"); } else if (g_strcmp0(xb_node_get_element(n), "code") == 0) { g_string_append(helper->str, "`"); } else if (g_strcmp0(xb_node_get_element(n), "li") == 0) { g_string_append(helper->str, "• "); } else if (g_strcmp0(xb_node_get_element(n), "p") == 0 || g_strcmp0(xb_node_get_element(n), "ul") == 0 || g_strcmp0(xb_node_get_element(n), "ol") == 0) { g_string_append(helper->str, "\n"); } /* text */ if (xb_node_get_text(n) != NULL) g_string_append(helper->str, xb_node_get_text(n)); return FALSE; } static gboolean fu_util_convert_description_tail_cb(XbNode *n, gpointer user_data) { FuUtilConvertHelper *helper = (FuUtilConvertHelper *)user_data; helper->cnt++; /* end */ if (g_strcmp0(xb_node_get_element(n), "em") == 0 || g_strcmp0(xb_node_get_element(n), "strong") == 0) { g_string_append(helper->str, "\033[0m"); } else if (g_strcmp0(xb_node_get_element(n), "code") == 0) { g_string_append(helper->str, "`"); } else if (g_strcmp0(xb_node_get_element(n), "li") == 0) { g_string_append(helper->str, "\n"); } else if (g_strcmp0(xb_node_get_element(n), "p") == 0) { g_string_append(helper->str, "\n"); } /* tail */ if (xb_node_get_tail(n) != NULL) g_string_append(helper->str, xb_node_get_tail(n)); return FALSE; } static gchar * fu_util_convert_description(const gchar *xml, GError **error) { g_autoptr(GString) str = g_string_new(NULL); g_autoptr(XbNode) n = NULL; g_autoptr(XbSilo) silo = NULL; FuUtilConvertHelper helper = { .cnt = 0, .str = str, }; /* parse XML */ silo = xb_silo_new_from_xml(xml, error); if (silo == NULL) return NULL; /* convert to something we can show on the console */ n = xb_silo_get_root(silo); xb_node_transmogrify(n, fu_util_convert_description_head_cb, fu_util_convert_description_tail_cb, &helper); /* success */ return fu_strstrip(str->str); } /** * fu_util_time_to_str: * @tmp: the time in seconds * * Converts a timestamp to a 'pretty' translated string * * Returns: (transfer full): A string **/ static gchar * fu_util_time_to_str(guint64 tmp) { g_return_val_if_fail(tmp != 0, NULL); /* seconds */ if (tmp < 60) { /* TRANSLATORS: duration in seconds */ return g_strdup_printf(ngettext("%u second", "%u seconds", (gint)tmp), (guint)tmp); } /* minutes */ tmp /= 60; if (tmp < 60) { /* TRANSLATORS: duration in minutes */ return g_strdup_printf(ngettext("%u minute", "%u minutes", (gint)tmp), (guint)tmp); } /* hours */ tmp /= 60; if (tmp < 60) { /* TRANSLATORS: duration in minutes */ return g_strdup_printf(ngettext("%u hour", "%u hours", (gint)tmp), (guint)tmp); } /* days */ tmp /= 24; /* TRANSLATORS: duration in days! */ return g_strdup_printf(ngettext("%u day", "%u days", (gint)tmp), (guint)tmp); } static gchar * fu_util_device_flag_to_string(guint64 device_flag) { if (device_flag == FWUPD_DEVICE_FLAG_NONE) { return NULL; } if (device_flag == FWUPD_DEVICE_FLAG_INTERNAL) { /* TRANSLATORS: Device cannot be removed easily*/ return _("Internal device"); } if (device_flag == FWUPD_DEVICE_FLAG_UPDATABLE || device_flag == FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN) { /* TRANSLATORS: Device is updatable in this or any other mode */ return _("Updatable"); } if (device_flag == FWUPD_DEVICE_FLAG_REQUIRE_AC) { /* TRANSLATORS: Must be plugged into an outlet */ return _("System requires external power source"); } if (device_flag == FWUPD_DEVICE_FLAG_LOCKED) { /* TRANSLATORS: Is locked and can be unlocked */ return _("Device is locked"); } if (device_flag == FWUPD_DEVICE_FLAG_SUPPORTED) { /* TRANSLATORS: Is found in current metadata */ return _("Supported on remote server"); } if (device_flag == FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER) { /* TRANSLATORS: Requires a bootloader mode to be manually enabled by the user */ return _("Requires a bootloader"); } if (device_flag == FWUPD_DEVICE_FLAG_NEEDS_REBOOT) { /* TRANSLATORS: Requires a reboot to apply firmware or to reload hardware */ return _("Needs a reboot after installation"); } if (device_flag == FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN) { /* TRANSLATORS: Requires system shutdown to apply firmware */ return _("Needs shutdown after installation"); } if (device_flag == FWUPD_DEVICE_FLAG_REPORTED) { /* TRANSLATORS: Has been reported to a metadata server */ return _("Reported to remote server"); } if (device_flag == FWUPD_DEVICE_FLAG_NOTIFIED) { /* TRANSLATORS: User has been notified */ return _("User has been notified"); } if (device_flag == FWUPD_DEVICE_FLAG_IS_BOOTLOADER) { /* TRANSLATORS: Is currently in bootloader mode */ return _("Is in bootloader mode"); } if (device_flag == FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG) { /* TRANSLATORS: the hardware is waiting to be replugged */ return _("Hardware is waiting to be replugged"); } if (device_flag == FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED) { /* skip */ return NULL; } if (device_flag == FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION) { /* TRANSLATORS: Device update needs to be separately activated */ return _("Device update needs activation"); } if (device_flag == FWUPD_DEVICE_FLAG_HISTORICAL) { /* skip */ return NULL; } if (device_flag == FWUPD_DEVICE_FLAG_WILL_DISAPPEAR) { /* TRANSLATORS: Device will not return after update completes */ return _("Device will not re-appear after update completes"); } if (device_flag == FWUPD_DEVICE_FLAG_CAN_VERIFY) { /* TRANSLATORS: Device supports some form of checksum verification */ return _("Cryptographic hash verification is available"); } if (device_flag == FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE) { /* skip */ return NULL; } if (device_flag == FWUPD_DEVICE_FLAG_DUAL_IMAGE) { /* TRANSLATORS: Device supports a safety mechanism for flashing */ return _("Device stages updates"); } if (device_flag == FWUPD_DEVICE_FLAG_SELF_RECOVERY) { /* TRANSLATORS: Device supports a safety mechanism for flashing */ return _("Device can recover flash failures"); } if (device_flag == FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE) { /* TRANSLATORS: Device remains usable during update */ return _("Device is usable for the duration of the update"); } if (device_flag == FWUPD_DEVICE_FLAG_VERSION_CHECK_REQUIRED) { /* TRANSLATORS: a version check is required for all firmware */ return _("Device firmware is required to have a version check"); } if (device_flag == FWUPD_DEVICE_FLAG_INSTALL_ALL_RELEASES) { /* TRANSLATORS: the device cannot update from A->C and has to go A->B->C */ return _("Device is required to install all provided releases"); } if (device_flag == FWUPD_DEVICE_FLAG_HAS_MULTIPLE_BRANCHES) { /* TRANSLATORS: there is more than one supplier of the firmware */ return _("Device supports switching to a different branch of firmware"); } if (device_flag == FWUPD_DEVICE_FLAG_BACKUP_BEFORE_INSTALL) { /* TRANSLATORS: save the old firmware to disk before installing the new one */ return _("Device will backup firmware before installing"); } if (device_flag == FWUPD_DEVICE_FLAG_WILDCARD_INSTALL) { /* TRANSLATORS: on some systems certain devices have to have matching versions, * e.g. the EFI driver for a given network card cannot be different */ return _("All devices of the same type will be updated at the same time"); } if (device_flag == FWUPD_DEVICE_FLAG_ONLY_VERSION_UPGRADE) { /* TRANSLATORS: some devices can only be updated to a new semver and cannot * be downgraded or reinstalled with the existing version */ return _("Only version upgrades are allowed"); } if (device_flag == FWUPD_DEVICE_FLAG_UNREACHABLE) { /* TRANSLATORS: currently unreachable, perhaps because it is in a lower power state * or is out of wireless range */ return _("Device is unreachable"); } if (device_flag == FWUPD_DEVICE_FLAG_AFFECTS_FDE) { /* TRANSLATORS: we might ask the user the recovery key when next booting Windows */ return _("Full disk encryption secrets may be invalidated when updating"); } if (device_flag == FWUPD_DEVICE_FLAG_END_OF_LIFE) { /* TRANSLATORS: the vendor is no longer supporting the device */ return _("End of life"); } if (device_flag == FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD) { /* TRANSLATORS: firmware is verified on-device the payload using strong crypto */ return _("Signed Payload"); } if (device_flag == FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD) { /* TRANSLATORS: firmware payload is unsigned and it is possible to modify it */ return _("Unsigned Payload"); } if (device_flag == FWUPD_DEVICE_FLAG_EMULATED) { /* TRANSLATORS: this device is not actually real */ return _("Emulated"); } if (device_flag == FWUPD_DEVICE_FLAG_EMULATION_TAG) { /* TRANSLATORS: we're saving all USB events for emulation */ return _("Tagged for emulation"); } if (device_flag == FWUPD_DEVICE_FLAG_ONLY_EXPLICIT_UPDATES) { /* TRANSLATORS: stay on one firmware version unless the new version is explicitly * specified */ return _("Installing a specific release is explicitly required"); } if (device_flag == FWUPD_DEVICE_FLAG_CAN_EMULATION_TAG) { /* TRANSLATORS: we can save all device enumeration events for emulation */ return _("Can tag for emulation"); } if (device_flag == FWUPD_DEVICE_FLAG_UNKNOWN) { return NULL; } return NULL; } static gchar * fu_util_request_flag_to_string(guint64 request_flag) { if (request_flag == FWUPD_REQUEST_FLAG_NONE) return NULL; if (request_flag == FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE) { /* TRANSLATORS: ask the user to do a simple task which should be translated */ return _("Message"); } if (request_flag == FWUPD_REQUEST_FLAG_ALLOW_GENERIC_IMAGE) { /* TRANSLATORS: show the user a generic image that can be themed */ return _("Image"); } if (request_flag == FWUPD_REQUEST_FLAG_NON_GENERIC_MESSAGE) { /* TRANSLATORS: ask the user a question, and it will not be translated */ return _("Message (custom)"); } if (request_flag == FWUPD_REQUEST_FLAG_NON_GENERIC_IMAGE) { /* TRANSLATORS: show the user a random image from the internet */ return _("Image (custom)"); } return NULL; } static const gchar * fu_util_update_state_to_string(FwupdUpdateState update_state) { if (update_state == FWUPD_UPDATE_STATE_PENDING) { /* TRANSLATORS: the update state of the specific device */ return _("Pending"); } if (update_state == FWUPD_UPDATE_STATE_SUCCESS) { /* TRANSLATORS: the update state of the specific device */ return _("Success"); } if (update_state == FWUPD_UPDATE_STATE_FAILED) { /* TRANSLATORS: the update state of the specific device */ return _("Failed"); } if (update_state == FWUPD_UPDATE_STATE_FAILED_TRANSIENT) { /* TRANSLATORS: the update state of the specific device */ return _("Transient failure"); } if (update_state == FWUPD_UPDATE_STATE_NEEDS_REBOOT) { /* TRANSLATORS: the update state of the specific device */ return _("Needs reboot"); } return NULL; } gchar * fu_util_device_problem_to_string(FwupdClient *client, FwupdDevice *dev, FwupdDeviceProblem problem) { if (problem == FWUPD_DEVICE_PROBLEM_NONE) return NULL; if (problem == FWUPD_DEVICE_PROBLEM_UNKNOWN) return NULL; if (problem == FWUPD_DEVICE_PROBLEM_SYSTEM_POWER_TOO_LOW) { if (fwupd_client_get_battery_level(client) == FWUPD_BATTERY_LEVEL_INVALID || fwupd_client_get_battery_threshold(client) == FWUPD_BATTERY_LEVEL_INVALID) { /* TRANSLATORS: as in laptop battery power */ return g_strdup(_("System power is too low")); } return g_strdup_printf( /* TRANSLATORS: as in laptop battery power */ _("System power is too low (%u%%, requires %u%%)"), fwupd_client_get_battery_level(client), fwupd_client_get_battery_threshold(client)); } if (problem == FWUPD_DEVICE_PROBLEM_UNREACHABLE) { /* TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode */ return g_strdup(_("Device is unreachable, or out of wireless range")); } if (problem == FWUPD_DEVICE_PROBLEM_POWER_TOO_LOW) { if (fwupd_device_get_battery_level(dev) == FWUPD_BATTERY_LEVEL_INVALID || fwupd_device_get_battery_threshold(dev) == FWUPD_BATTERY_LEVEL_INVALID) { /* TRANSLATORS: for example the batteries *inside* the Bluetooth mouse */ return g_strdup(_("Device battery power is too low")); } /* TRANSLATORS: for example the batteries *inside* the Bluetooth mouse */ return g_strdup_printf(_("Device battery power is too low (%u%%, requires %u%%)"), fwupd_device_get_battery_level(dev), fwupd_device_get_battery_threshold(dev)); } if (problem == FWUPD_DEVICE_PROBLEM_UPDATE_PENDING) { /* TRANSLATORS: usually this is when we're waiting for a reboot */ return g_strdup(_("Device is waiting for the update to be applied")); } if (problem == FWUPD_DEVICE_PROBLEM_REQUIRE_AC_POWER) { /* TRANSLATORS: as in, wired mains power for a laptop */ return g_strdup(_("Device requires AC power to be connected")); } if (problem == FWUPD_DEVICE_PROBLEM_LID_IS_CLOSED) { /* TRANSLATORS: lid means "laptop top cover" */ return g_strdup(_("Device cannot be updated while the lid is closed")); } if (problem == FWUPD_DEVICE_PROBLEM_IS_EMULATED) { /* TRANSLATORS: emulated means we are pretending to be a different model */ return g_strdup(_("Device is emulated")); } if (problem == FWUPD_DEVICE_PROBLEM_MISSING_LICENSE) { /* TRANSLATORS: The device cannot be updated due to missing vendor's license." */ return g_strdup(_("Device requires a software license to update")); } if (problem == FWUPD_DEVICE_PROBLEM_SYSTEM_INHIBIT) { /* TRANSLATORS: an application is preventing system updates */ return g_strdup(_("All devices are prevented from update by system inhibit")); } if (problem == FWUPD_DEVICE_PROBLEM_UPDATE_IN_PROGRESS) { /* TRANSLATORS: another application is updating the device already */ return g_strdup(_("An update is in progress")); } if (problem == FWUPD_DEVICE_PROBLEM_IN_USE) { /* TRANSLATORS: device cannot be interrupted, for instance taking a phone call */ return g_strdup(_("Device is in use")); } if (problem == FWUPD_DEVICE_PROBLEM_DISPLAY_REQUIRED) { /* TRANSLATORS: device does not have a display connected */ return g_strdup(_("Device requires a display to be plugged in")); } if (problem == FWUPD_DEVICE_PROBLEM_LOWER_PRIORITY) { /* TRANSLATORS: we have two ways of communicating with the device, so we hide one */ return g_strdup(_("Device is lower priority than an equivalent device")); } return NULL; } gchar * fu_util_device_to_string(FwupdClient *client, FwupdDevice *dev, guint idt) { FwupdUpdateState state; GPtrArray *guids = fwupd_device_get_guids(dev); GPtrArray *issues = fwupd_device_get_issues(dev); GPtrArray *vendor_ids = fwupd_device_get_vendor_ids(dev); GPtrArray *instance_ids = fwupd_device_get_instance_ids(dev); const gchar *tmp; const gchar *tmp2; guint64 flags = fwupd_device_get_flags(dev); guint64 modified = fwupd_device_get_modified(dev); guint64 request_flags = fwupd_device_get_request_flags(dev); g_autoptr(GHashTable) ids = NULL; g_autoptr(GString) str = g_string_new(NULL); /* some fields are intentionally not included and are only shown in --verbose */ if (g_getenv("FWUPD_VERBOSE") != NULL) { g_autofree gchar *debug_str = fwupd_codec_to_string(FWUPD_CODEC(dev)); g_info("%s", debug_str); return NULL; } tmp = fwupd_device_get_name(dev); if (tmp == NULL) { /* TRANSLATORS: Name of hardware */ tmp = _("Unknown Device"); } fwupd_codec_string_append(str, idt, tmp, ""); /* TRANSLATORS: ID for hardware, typically a SHA1 sum */ fwupd_codec_string_append(str, idt + 1, _("Device ID"), fwupd_device_get_id(dev)); /* TRANSLATORS: one line summary of device */ fwupd_codec_string_append(str, idt + 1, _("Summary"), fwupd_device_get_summary(dev)); /* versions */ tmp = fwupd_device_get_version(dev); if (tmp != NULL) { g_autoptr(GString) verstr = g_string_new(tmp); if (fwupd_device_get_version_build_date(dev) != 0) { guint64 value = fwupd_device_get_version_build_date(dev); g_autoptr(GDateTime) date = g_date_time_new_from_unix_utc((gint64)value); g_autofree gchar *datestr = g_date_time_format(date, "%F"); g_string_append_printf(verstr, " [%s]", datestr); } if (flags & FWUPD_DEVICE_FLAG_HISTORICAL) { fwupd_codec_string_append( str, idt + 1, /* TRANSLATORS: version number of previous firmware */ _("Previous version"), verstr->str); } else { /* TRANSLATORS: version number of current firmware */ fwupd_codec_string_append(str, idt + 1, _("Current version"), verstr->str); } } fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: smallest version number installable on device */ _("Minimum Version"), fwupd_device_get_version_lowest(dev)); fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: firmware version of bootloader */ _("Bootloader Version"), fwupd_device_get_version_bootloader(dev)); /* vendor */ tmp = fwupd_device_get_vendor(dev); if (tmp != NULL && vendor_ids->len > 0) { g_autofree gchar *strv = fu_strjoin(", ", vendor_ids); g_autofree gchar *both = g_strdup_printf("%s (%s)", tmp, strv); /* TRANSLATORS: manufacturer of hardware */ fwupd_codec_string_append(str, idt + 1, _("Vendor"), both); } else if (tmp != NULL) { /* TRANSLATORS: manufacturer of hardware */ fwupd_codec_string_append(str, idt + 1, _("Vendor"), tmp); } else if (vendor_ids->len > 0) { g_autofree gchar *strv = fu_strjoin("|", vendor_ids); /* TRANSLATORS: manufacturer of hardware */ fwupd_codec_string_append(str, idt + 1, _("Vendor"), strv); } /* branch */ fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: the stream of firmware, e.g. nonfree */ _("Release Branch"), fwupd_device_get_branch(dev)); /* install duration */ if (fwupd_device_get_install_duration(dev) > 0) { g_autofree gchar *time = fu_util_time_to_str(fwupd_device_get_install_duration(dev)); /* TRANSLATORS: length of time the update takes to apply */ fwupd_codec_string_append(str, idt + 1, _("Install Duration"), time); } /* TRANSLATORS: serial number of hardware */ fwupd_codec_string_append(str, idt + 1, _("Serial Number"), fwupd_device_get_serial(dev)); /* update state */ state = fwupd_device_get_update_state(dev); if (state != FWUPD_UPDATE_STATE_UNKNOWN) { fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: hardware state, e.g. "pending" */ _("Update State"), fu_util_update_state_to_string(state)); } /* battery, but only if we're not about to show the same info as an inhibit */ if (!fwupd_device_has_problem(dev, FWUPD_DEVICE_PROBLEM_POWER_TOO_LOW)) { if (fwupd_device_get_battery_level(dev) != FWUPD_BATTERY_LEVEL_INVALID && fwupd_device_get_battery_threshold(dev) != FWUPD_BATTERY_LEVEL_INVALID) { g_autofree gchar *val = NULL; /* TRANSLATORS: first percentage is current value, 2nd percentage is the * lowest limit the firmware update is allowed for the update to happen */ val = g_strdup_printf(_("%u%% (threshold %u%%)"), fwupd_device_get_battery_level(dev), fwupd_device_get_battery_threshold(dev)); /* TRANSLATORS: refers to the battery inside the peripheral device */ fwupd_codec_string_append(str, idt + 1, _("Battery"), val); } else if (fwupd_device_get_battery_level(dev) != FWUPD_BATTERY_LEVEL_INVALID) { g_autofree gchar *val = NULL; val = g_strdup_printf("%u%%", fwupd_device_get_battery_level(dev)); /* TRANSLATORS: refers to the battery inside the peripheral device */ fwupd_codec_string_append(str, idt + 1, _("Battery"), val); } } /* either show enumerated [translated] problems or the synthesized update error */ if (fwupd_device_get_problems(dev) == FWUPD_DEVICE_PROBLEM_NONE) { tmp = fwupd_device_get_update_error(dev); if (tmp != NULL) { g_autofree gchar *color = fu_console_color_format(tmp, FU_CONSOLE_COLOR_RED); /* TRANSLATORS: error message from last update attempt */ fwupd_codec_string_append(str, idt + 1, _("Update Error"), color); } } else { /* TRANSLATORS: reasons the device is not updatable */ tmp = _("Problems"); for (guint i = 0; i < 64; i++) { FwupdDeviceProblem problem = (guint64)1 << i; g_autofree gchar *bullet = NULL; g_autofree gchar *desc = NULL; g_autofree gchar *color = NULL; if (!fwupd_device_has_problem(dev, problem)) continue; desc = fu_util_device_problem_to_string(client, dev, problem); if (desc == NULL) continue; bullet = g_strdup_printf("• %s", desc); color = fu_console_color_format(bullet, FU_CONSOLE_COLOR_RED); fwupd_codec_string_append(str, idt + 1, tmp, color); tmp = ""; } } /* TRANSLATORS: the original time/date the device was modified */ fwupd_codec_string_append_time(str, idt + 1, _("Last modified"), modified); /* all GUIDs for this hardware, with IDs if available */ ids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); for (guint i = 0; i < instance_ids->len; i++) { const gchar *instance_id = g_ptr_array_index(instance_ids, i); g_hash_table_insert(ids, fwupd_guid_hash_string(instance_id), g_strdup(instance_id)); } for (guint i = 0; i < guids->len; i++) { const gchar *guid = g_ptr_array_index(guids, i); const gchar *instance_id = g_hash_table_lookup(ids, guid); g_autofree gchar *guid_src = NULL; /* instance IDs are only available as root */ if (instance_id == NULL) { guid_src = g_strdup(guid); } else { guid_src = g_strdup_printf("%s ↠%s", guid, instance_id); } if (i == 0) { fwupd_codec_string_append( str, idt + 1, /* TRANSLATORS: global ID common to all similar hardware */ ngettext("GUID", "GUIDs", guids->len), guid_src); } else { fwupd_codec_string_append(str, idt + 1, "", guid_src); } } /* TRANSLATORS: description of device ability */ tmp = _("Device Flags"); for (guint i = 0; i < 64; i++) { if ((flags & ((guint64)1 << i)) == 0) continue; tmp2 = fu_util_device_flag_to_string((guint64)1 << i); if (tmp2 == NULL) continue; /* header */ if (tmp != NULL) { g_autofree gchar *bullet = NULL; bullet = g_strdup_printf("• %s", tmp2); fwupd_codec_string_append(str, idt + 1, tmp, bullet); tmp = NULL; } else { g_autofree gchar *bullet = NULL; bullet = g_strdup_printf("• %s", tmp2); fwupd_codec_string_append(str, idt + 1, "", bullet); } } /* TRANSLATORS: description of the device requests */ tmp = _("Device Requests"); for (guint i = 0; i < 64; i++) { if ((request_flags & ((guint64)1 << i)) == 0) continue; tmp2 = fu_util_request_flag_to_string((guint64)1 << i); if (tmp2 == NULL) continue; /* header */ if (tmp != NULL) { g_autofree gchar *bullet = NULL; bullet = g_strdup_printf("• %s", tmp2); fwupd_codec_string_append(str, idt + 1, tmp, bullet); tmp = NULL; } else { g_autofree gchar *bullet = NULL; bullet = g_strdup_printf("• %s", tmp2); fwupd_codec_string_append(str, idt + 1, "", bullet); } } for (guint i = 0; i < issues->len; i++) { const gchar *issue = g_ptr_array_index(issues, i); fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: issue fixed with the release, e.g. CVE */ i == 0 ? ngettext("Issue", "Issues", issues->len) : "", issue); } return g_string_free(g_steal_pointer(&str), FALSE); } gint fu_util_plugin_name_sort_cb(FwupdPlugin **item1, FwupdPlugin **item2) { return g_strcmp0(fwupd_plugin_get_name(*item1), fwupd_plugin_get_name(*item2)); } gchar * fu_util_plugin_flag_to_string(FwupdPluginFlags plugin_flag) { if (plugin_flag == FWUPD_PLUGIN_FLAG_UNKNOWN) return NULL; if (plugin_flag == FWUPD_PLUGIN_FLAG_CLEAR_UPDATABLE) return NULL; if (plugin_flag == FWUPD_PLUGIN_FLAG_USER_WARNING) return NULL; if (plugin_flag == FWUPD_PLUGIN_FLAG_NONE) return NULL; if (plugin_flag == FWUPD_PLUGIN_FLAG_REQUIRE_HWID) { /* TRANSLATORS: Plugin is active only if hardware is found */ return g_strdup(_("Enabled if hardware matches")); } if (plugin_flag == FWUPD_PLUGIN_FLAG_READY) { /* TRANSLATORS: Plugin is active and in use */ return g_strdup(_("Ready")); } if (plugin_flag == FWUPD_PLUGIN_FLAG_DISABLED) { /* TRANSLATORS: Plugin is inactive and not used */ return g_strdup(_("Disabled")); } if (plugin_flag == FWUPD_PLUGIN_FLAG_NO_HARDWARE) { /* TRANSLATORS: not required for this system */ return g_strdup(_("Required hardware was not found")); } if (plugin_flag == FWUPD_PLUGIN_FLAG_LEGACY_BIOS) { /* TRANSLATORS: system is not booted in UEFI mode */ return g_strdup(_("UEFI firmware can not be updated in legacy BIOS mode")); } if (plugin_flag == FWUPD_PLUGIN_FLAG_CAPSULES_UNSUPPORTED) { return g_strdup( /* TRANSLATORS: capsule updates are an optional BIOS feature */ _("UEFI capsule updates not available or enabled in firmware setup")); } if (plugin_flag == FWUPD_PLUGIN_FLAG_UNLOCK_REQUIRED) { /* TRANSLATORS: user needs to run a command, %1 is 'fwupdmgr unlock' */ return g_strdup_printf(_("Firmware updates disabled; run '%s' to enable"), "fwupdmgr unlock"); } if (plugin_flag == FWUPD_PLUGIN_FLAG_AUTH_REQUIRED) { /* TRANSLATORS: user needs to run a command */ return g_strdup(_("Authentication details are required")); } if (plugin_flag == FWUPD_PLUGIN_FLAG_SECURE_CONFIG) { /* TRANSLATORS: no peeking */ return g_strdup(_("Configuration is only readable by the system administrator")); } if (plugin_flag == FWUPD_PLUGIN_FLAG_MODULAR) { /* TRANSLATORS: the plugin was created from a .so object, and was not built-in */ return g_strdup(_("Loaded from an external module")); } if (plugin_flag == FWUPD_PLUGIN_FLAG_MEASURE_SYSTEM_INTEGRITY) { /* TRANSLATORS: check various UEFI and ACPI tables are unchanged after the update */ return g_strdup(_("Will measure elements of system integrity around an update")); } if (plugin_flag == FWUPD_PLUGIN_FLAG_EFIVAR_NOT_MOUNTED) { /* TRANSLATORS: the user is using Gentoo/Arch and has screwed something up */ return g_strdup(_("Required efivarfs filesystem was not found")); } if (plugin_flag == FWUPD_PLUGIN_FLAG_ESP_NOT_FOUND) { /* TRANSLATORS: partition refers to something on disk, again, hey Arch users */ return g_strdup(_("UEFI ESP partition not detected or configured")); } if (plugin_flag == FWUPD_PLUGIN_FLAG_ESP_NOT_VALID) { /* TRANSLATORS: partition refers to something on disk, again, hey Arch users */ return g_strdup(_("UEFI ESP partition may not be set up correctly")); } if (plugin_flag == FWUPD_PLUGIN_FLAG_FAILED_OPEN) { /* TRANSLATORS: Failed to open plugin, hey Arch users */ return g_strdup(_("Plugin dependencies missing")); } if (plugin_flag == FWUPD_PLUGIN_FLAG_KERNEL_TOO_OLD) { /* TRANSLATORS: The kernel does not support this plugin */ return g_strdup(_("Running kernel is too old")); } if (plugin_flag == FWUPD_PLUGIN_FLAG_TEST_ONLY) { /* TRANSLATORS: The plugin is only for testing */ return g_strdup(_("Plugin is only for testing")); } /* fall back for unknown types */ return g_strdup(fwupd_plugin_flag_to_string(plugin_flag)); } static gchar * fu_util_plugin_flag_to_cli_text(FwupdPluginFlags plugin_flag) { g_autofree gchar *plugin_flag_str = fu_util_plugin_flag_to_string(plugin_flag); switch (plugin_flag) { case FWUPD_PLUGIN_FLAG_UNKNOWN: case FWUPD_PLUGIN_FLAG_CLEAR_UPDATABLE: case FWUPD_PLUGIN_FLAG_USER_WARNING: case FWUPD_PLUGIN_FLAG_NONE: return NULL; case FWUPD_PLUGIN_FLAG_READY: case FWUPD_PLUGIN_FLAG_REQUIRE_HWID: case FWUPD_PLUGIN_FLAG_MODULAR: case FWUPD_PLUGIN_FLAG_MEASURE_SYSTEM_INTEGRITY: case FWUPD_PLUGIN_FLAG_SECURE_CONFIG: return fu_console_color_format(plugin_flag_str, FU_CONSOLE_COLOR_GREEN); case FWUPD_PLUGIN_FLAG_DISABLED: case FWUPD_PLUGIN_FLAG_NO_HARDWARE: case FWUPD_PLUGIN_FLAG_TEST_ONLY: return fu_console_color_format(plugin_flag_str, FU_CONSOLE_COLOR_BLACK); case FWUPD_PLUGIN_FLAG_LEGACY_BIOS: case FWUPD_PLUGIN_FLAG_CAPSULES_UNSUPPORTED: case FWUPD_PLUGIN_FLAG_UNLOCK_REQUIRED: case FWUPD_PLUGIN_FLAG_AUTH_REQUIRED: case FWUPD_PLUGIN_FLAG_EFIVAR_NOT_MOUNTED: case FWUPD_PLUGIN_FLAG_ESP_NOT_FOUND: case FWUPD_PLUGIN_FLAG_ESP_NOT_VALID: case FWUPD_PLUGIN_FLAG_KERNEL_TOO_OLD: return fu_console_color_format(plugin_flag_str, FU_CONSOLE_COLOR_RED); default: break; } /* fall back for unknown types */ return g_steal_pointer(&plugin_flag_str); } gchar * fu_util_plugin_to_string(FwupdPlugin *plugin, guint idt) { GString *str = g_string_new(NULL); const gchar *hdr; guint64 flags = fwupd_plugin_get_flags(plugin); fwupd_codec_string_append(str, idt, fwupd_plugin_get_name(plugin), ""); /* TRANSLATORS: description of plugin state, e.g. disabled */ hdr = _("Flags"); if (flags == 0x0) { g_autofree gchar *tmp = fu_util_plugin_flag_to_cli_text(flags); g_autofree gchar *li = g_strdup_printf("• %s", tmp); fwupd_codec_string_append(str, idt + 1, hdr, li); } else { for (guint i = 0; i < 64; i++) { g_autofree gchar *li = NULL; g_autofree gchar *tmp = NULL; if ((flags & ((guint64)1 << i)) == 0) continue; tmp = fu_util_plugin_flag_to_cli_text((guint64)1 << i); if (tmp == NULL) continue; li = g_strdup_printf("• %s", tmp); fwupd_codec_string_append(str, idt + 1, hdr, li); /* clear header */ hdr = ""; } } return g_string_free(str, FALSE); } static gchar * fu_util_license_to_string(const gchar *spdx_license) { g_autofree const gchar **new = NULL; g_auto(GStrv) old = NULL; /* sanity check */ if (spdx_license == NULL) { /* TRANSLATORS: we don't know the license of the update */ return g_strdup(_("Unknown")); } /* replace any LicenseRef-proprietary with it's translated form */ old = g_strsplit(spdx_license, " AND ", -1); new = g_new0(const gchar *, g_strv_length(old) + 1); for (guint i = 0; old[i] != NULL; i++) { const gchar *license = old[i]; if (g_strcmp0(license, "LicenseRef-proprietary") == 0 || g_strcmp0(license, "proprietary") == 0) { /* TRANSLATORS: a non-free software license */ license = _("Proprietary"); } new[i] = license; } /* this is no longer SPDX */ return g_strjoinv(", ", (gchar **)new); } static const gchar * fu_util_release_urgency_to_string(FwupdReleaseUrgency release_urgency) { if (release_urgency == FWUPD_RELEASE_URGENCY_LOW) { /* TRANSLATORS: the release urgency */ return _("Low"); } if (release_urgency == FWUPD_RELEASE_URGENCY_MEDIUM) { /* TRANSLATORS: the release urgency */ return _("Medium"); } if (release_urgency == FWUPD_RELEASE_URGENCY_HIGH) { /* TRANSLATORS: the release urgency */ return _("High"); } if (release_urgency == FWUPD_RELEASE_URGENCY_CRITICAL) { /* TRANSLATORS: the release urgency */ return _("Critical"); } /* TRANSLATORS: unknown release urgency */ return _("Unknown"); } static const gchar * fu_util_release_flag_to_string(FwupdReleaseFlags release_flag) { if (release_flag == FWUPD_RELEASE_FLAG_NONE) return NULL; if (release_flag == FWUPD_RELEASE_FLAG_TRUSTED_PAYLOAD) { /* TRANSLATORS: We verified the payload against the server */ return _("Trusted payload"); } if (release_flag == FWUPD_RELEASE_FLAG_TRUSTED_METADATA) { /* TRANSLATORS: We verified the metadata against the server */ return _("Trusted metadata"); } if (release_flag == FWUPD_RELEASE_FLAG_IS_UPGRADE) { /* TRANSLATORS: version is newer */ return _("Is upgrade"); } if (release_flag == FWUPD_RELEASE_FLAG_IS_DOWNGRADE) { /* TRANSLATORS: version is older */ return _("Is downgrade"); } if (release_flag == FWUPD_RELEASE_FLAG_BLOCKED_VERSION) { /* TRANSLATORS: version cannot be installed due to policy */ return _("Blocked version"); } if (release_flag == FWUPD_RELEASE_FLAG_BLOCKED_APPROVAL) { /* TRANSLATORS: version cannot be installed due to policy */ return _("Not approved"); } if (release_flag == FWUPD_RELEASE_FLAG_IS_ALTERNATE_BRANCH) { /* TRANSLATORS: is not the main firmware stream */ return _("Alternate branch"); } if (release_flag == FWUPD_RELEASE_FLAG_IS_COMMUNITY) { /* TRANSLATORS: is not supported by the vendor */ return _("Community supported"); } if (release_flag == FWUPD_RELEASE_FLAG_TRUSTED_REPORT) { /* TRANSLATORS: someone we trust has tested this */ return _("Tested by trusted vendor"); } /* fall back for unknown types */ return fwupd_release_flag_to_string(release_flag); } static void fu_util_report_add_string(FwupdReport *report, guint idt, GString *str) { g_autofree gchar *title = NULL; /* TRANSLATORS: the %s is a vendor name, e.g. Lenovo */ title = g_strdup_printf(_("Tested by %s"), fwupd_report_get_vendor(report)); fwupd_codec_string_append(str, idt, title, NULL); /* TRANSLATORS: when the release was tested */ fwupd_codec_string_append_time(str, idt + 1, _("Tested"), fwupd_report_get_created(report)); if (fwupd_report_get_distro_id(report) != NULL) { g_autoptr(GString) str2 = g_string_new(fwupd_report_get_distro_id(report)); if (fwupd_report_get_distro_version(report) != NULL) g_string_append_printf(str2, " %s", fwupd_report_get_distro_version(report)); if (fwupd_report_get_distro_variant(report) != NULL) g_string_append_printf(str2, " (%s)", fwupd_report_get_distro_variant(report)); /* TRANSLATORS: the OS the release was tested on */ fwupd_codec_string_append(str, idt + 1, _("Distribution"), str2->str); } fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: the firmware old version */ _("Old version"), fwupd_report_get_version_old(report)); fwupd_codec_string_append( str, idt + 1, /* TRANSLATORS: the fwupd version the release was tested on */ _("Version[fwupd]"), fwupd_report_get_metadata_item(report, "RuntimeVersion(org.freedesktop.fwupd)")); } static gchar * fu_util_release_to_string(FwupdRelease *rel, guint idt) { const gchar *title; const gchar *tmp2; GPtrArray *checksums = fwupd_release_get_checksums(rel); GPtrArray *issues = fwupd_release_get_issues(rel); GPtrArray *tags = fwupd_release_get_tags(rel); GPtrArray *reports = fwupd_release_get_reports(rel); guint64 flags = fwupd_release_get_flags(rel); g_autofree gchar *desc_fb = NULL; g_autofree gchar *name = fu_util_release_get_name(rel); g_autoptr(GString) str = g_string_new(NULL); g_return_val_if_fail(FWUPD_IS_RELEASE(rel), NULL); fwupd_codec_string_append(str, idt, name, ""); /* TRANSLATORS: version number of new firmware */ fwupd_codec_string_append(str, idt + 1, _("New version"), fwupd_release_get_version(rel)); fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: the server the file is coming from */ _("Remote ID"), fwupd_release_get_remote_id(rel)); fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: the exact component on the server */ _("Release ID"), fwupd_release_get_id(rel)); fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: the stream of firmware, e.g. nonfree */ _("Branch"), fwupd_release_get_branch(rel)); fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: one line summary of device */ _("Summary"), fwupd_release_get_summary(rel)); fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: one line variant of release (e.g. 'China') */ _("Variant"), fwupd_release_get_name_variant_suffix(rel)); if (fwupd_release_get_license(rel) != NULL) { g_autofree gchar *license = fu_util_license_to_string(fwupd_release_get_license(rel)); /* TRANSLATORS: e.g. GPLv2+, Proprietary etc */ fwupd_codec_string_append(str, idt + 1, _("License"), license); } /* TRANSLATORS: file size of the download */ fwupd_codec_string_append_size(str, idt + 1, _("Size"), fwupd_release_get_size(rel)); /* TRANSLATORS: when the update was built */ fwupd_codec_string_append_time(str, idt + 1, _("Created"), fwupd_release_get_created(rel)); if (fwupd_release_get_urgency(rel) != FWUPD_RELEASE_URGENCY_UNKNOWN) { FwupdReleaseUrgency tmp = fwupd_release_get_urgency(rel); fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: how important the release is */ _("Urgency"), fu_util_release_urgency_to_string(tmp)); } for (guint i = 0; i < reports->len; i++) { FwupdReport *report = g_ptr_array_index(reports, i); fu_util_report_add_string(report, idt + 1, str); } fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: more details about the update link */ _("Details"), fwupd_release_get_details_url(rel)); fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: source (as in code) link */ _("Source"), fwupd_release_get_source_url(rel)); fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: Software Bill of Materials link */ _("SBOM"), fwupd_release_get_sbom_url(rel)); fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: manufacturer of hardware */ _("Vendor"), fwupd_release_get_vendor(rel)); if (fwupd_release_get_install_duration(rel) != 0) { g_autofree gchar *tmp = fu_util_time_to_str(fwupd_release_get_install_duration(rel)); /* TRANSLATORS: length of time the update takes to apply */ fwupd_codec_string_append(str, idt + 1, _("Duration"), tmp); } fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: helpful messages for the update */ _("Update Message"), fwupd_release_get_update_message(rel)); fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: helpful image for the update */ _("Update Image"), fwupd_release_get_update_image(rel)); /* TRANSLATORS: release attributes */ title = _("Release Flags"); for (guint i = 0; i < 64; i++) { g_autofree gchar *bullet = NULL; if ((flags & ((guint64)1 << i)) == 0) continue; tmp2 = fu_util_release_flag_to_string((guint64)1 << i); if (tmp2 == NULL) continue; bullet = g_strdup_printf("• %s", tmp2); fwupd_codec_string_append(str, idt + 1, title, bullet); title = ""; } desc_fb = fu_util_get_release_description_with_fallback(rel); if (desc_fb != NULL) { g_autofree gchar *desc = NULL; desc = fu_util_convert_description(desc_fb, NULL); if (desc == NULL) desc = g_strdup(fwupd_release_get_description(rel)); /* TRANSLATORS: multiline description of device */ fwupd_codec_string_append(str, idt + 1, _("Description"), desc); } for (guint i = 0; i < issues->len; i++) { const gchar *issue = g_ptr_array_index(issues, i); if (i == 0) { fwupd_codec_string_append( str, idt + 1, /* TRANSLATORS: issue fixed with the release, e.g. CVE */ ngettext("Issue", "Issues", issues->len), issue); } else { fwupd_codec_string_append(str, idt + 1, "", issue); } } if (tags->len > 0) { g_autofree gchar *tag_strs = fu_strjoin(", ", tags); fwupd_codec_string_append( str, idt + 1, /* TRANSLATORS: release tag set for release, e.g. lenovo-2021q3 */ ngettext("Tag", "Tags", tags->len), tag_strs); } for (guint i = 0; i < checksums->len; i++) { const gchar *checksum = g_ptr_array_index(checksums, i); GChecksumType checksum_type = fwupd_checksum_guess_kind(checksum); /* avoid showing brokwn checksums */ if (checksum_type == G_CHECKSUM_SHA1) continue; /* TRANSLATORS: hash to that exact firmware archive */ fwupd_codec_string_append(str, idt + 1, _("Checksum"), checksum); } return g_string_free(g_steal_pointer(&str), FALSE); } static gchar * fu_util_remote_to_string(FwupdRemote *remote, guint idt) { FwupdRemoteKind kind = fwupd_remote_get_kind(remote); const gchar *tmp; gint priority; g_autoptr(GString) str = g_string_new(NULL); g_return_val_if_fail(FWUPD_IS_REMOTE(remote), NULL); fwupd_codec_string_append(str, idt, fwupd_remote_get_title(remote), NULL); /* TRANSLATORS: remote identifier, e.g. lvfs-testing */ fwupd_codec_string_append(str, idt + 1, _("Remote ID"), fwupd_remote_get_id(remote)); /* TRANSLATORS: remote type, e.g. remote or local */ fwupd_codec_string_append(str, idt + 1, _("Type"), fwupd_remote_kind_to_string(kind)); fwupd_codec_string_append( str, idt + 1, /* TRANSLATORS: if the remote is enabled */ _("Enabled"), fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_ENABLED) ? "true" : "false"); if (fwupd_remote_get_kind(remote) == FWUPD_REMOTE_KIND_DOWNLOAD) { fwupd_codec_string_append( str, idt + 1, /* TRANSLATORS: if we can get metadata from peer-to-peer clients */ _("P2P Metadata"), fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_ALLOW_P2P_METADATA) ? "true" : "false"); fwupd_codec_string_append( str, idt + 1, /* TRANSLATORS: if we can get metadata from peer-to-peer clients */ _("P2P Firmware"), fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_ALLOW_P2P_FIRMWARE) ? "true" : "false"); } /* TRANSLATORS: remote checksum */ fwupd_codec_string_append(str, idt + 1, _("Checksum"), fwupd_remote_get_checksum(remote)); /* optional parameters */ if (kind == FWUPD_REMOTE_KIND_DOWNLOAD && fwupd_remote_get_age(remote) > 0 && fwupd_remote_get_age(remote) != G_MAXUINT64) { g_autofree gchar *age_str = fu_util_time_to_str(fwupd_remote_get_age(remote)); /* TRANSLATORS: the age of the metadata */ fwupd_codec_string_append(str, idt + 1, _("Age"), age_str); } if (kind == FWUPD_REMOTE_KIND_DOWNLOAD && fwupd_remote_get_refresh_interval(remote) > 0) { g_autofree gchar *age_str = fu_util_time_to_str(fwupd_remote_get_refresh_interval(remote)); /* TRANSLATORS: how often we should refresh the metadata */ fwupd_codec_string_append(str, idt + 1, _("Refresh Interval"), age_str); } priority = fwupd_remote_get_priority(remote); if (priority != 0) { g_autofree gchar *priority_str = NULL; priority_str = g_strdup_printf("%i", priority); /* TRANSLATORS: the numeric priority */ fwupd_codec_string_append(str, idt + 1, _("Priority"), priority_str); } /* TRANSLATORS: remote filename base */ fwupd_codec_string_append(str, idt + 1, _("Username"), fwupd_remote_get_username(remote)); tmp = fwupd_remote_get_password(remote); if (tmp != NULL) { g_autofree gchar *hidden = g_strnfill(fu_strwidth(tmp), '*'); /* TRANSLATORS: remote filename base */ fwupd_codec_string_append(str, idt + 1, _("Password"), hidden); } fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: filename of the local file */ _("Filename"), fwupd_remote_get_filename_cache(remote)); fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: filename of the local file */ _("Filename Signature"), fwupd_remote_get_filename_cache_sig(remote)); fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: full path of the remote.conf file */ _("Filename Source"), fwupd_remote_get_filename_source(remote)); fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: remote URI */ _("Metadata URI"), fwupd_remote_get_metadata_uri(remote)); fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: remote URI */ _("Metadata Signature"), fwupd_remote_get_metadata_uri_sig(remote)); fwupd_codec_string_append(str, idt + 1, /* TRANSLATORS: remote URI */ _("Firmware Base URI"), fwupd_remote_get_firmware_base_uri(remote)); tmp = fwupd_remote_get_report_uri(remote); if (tmp != NULL) { /* TRANSLATORS: URI to send success/failure reports */ fwupd_codec_string_append(str, idt + 1, _("Report URI"), tmp); fwupd_codec_string_append( str, idt + 1, /* TRANSLATORS: Boolean value to automatically send reports */ _("Automatic Reporting"), fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS) ? "true" : "false"); } return g_string_free(g_steal_pointer(&str), FALSE); } const gchar * fu_util_request_get_message(FwupdRequest *req) { if (fwupd_request_has_flag(req, FWUPD_REQUEST_FLAG_ALLOW_GENERIC_MESSAGE)) { if (g_strcmp0(fwupd_request_get_id(req), FWUPD_REQUEST_ID_REMOVE_REPLUG) == 0) { /* TRANSLATORS: warning message shown after update has been scheduled */ return _("The update will continue when the device USB cable has been " "unplugged and then re-inserted."); } if (g_strcmp0(fwupd_request_get_id(req), FWUPD_REQUEST_ID_REMOVE_USB_CABLE) == 0) { /* TRANSLATORS: warning message shown after update has been scheduled */ return _("The update will continue when the device USB cable has been " "unplugged."); } if (g_strcmp0(fwupd_request_get_id(req), FWUPD_REQUEST_ID_INSERT_USB_CABLE) == 0) { /* TRANSLATORS: warning message shown after update has been scheduled */ return _("The update will continue when the device USB cable has been " "re-inserted."); } if (g_strcmp0(fwupd_request_get_id(req), FWUPD_REQUEST_ID_PRESS_UNLOCK) == 0) { /* TRANSLATORS: warning message */ return _("Press unlock on the device to continue the update process."); } if (g_strcmp0(fwupd_request_get_id(req), FWUPD_REQUEST_ID_DO_NOT_POWER_OFF) == 0) { /* TRANSLATORS: warning message shown after update has been scheduled */ return _("Do not turn off your computer or remove the AC adaptor " "while the update is in progress."); } if (g_strcmp0(fwupd_request_get_id(req), FWUPD_REQUEST_ID_REPLUG_INSTALL) == 0) { /* TRANSLATORS: message shown after device has been marked for emulation */ return _("Unplug and replug the device to continue the update process."); } if (g_strcmp0(fwupd_request_get_id(req), FWUPD_REQUEST_ID_REPLUG_POWER) == 0) { /* TRANSLATORS: warning message */ return _("The update will continue when the device power cable has been " "removed and re-inserted."); } } return fwupd_request_get_message(req); } static const gchar * fu_util_security_attr_result_to_string(FwupdSecurityAttrResult result) { if (result == FWUPD_SECURITY_ATTR_RESULT_VALID) { /* TRANSLATORS: Suffix: the HSI result */ return _("Valid"); } if (result == FWUPD_SECURITY_ATTR_RESULT_NOT_VALID) { /* TRANSLATORS: Suffix: the HSI result */ return _("Invalid"); } if (result == FWUPD_SECURITY_ATTR_RESULT_ENABLED) { /* TRANSLATORS: Suffix: the HSI result */ return _("Enabled"); } if (result == FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED) { /* TRANSLATORS: Suffix: the HSI result */ return _("Disabled"); } if (result == FWUPD_SECURITY_ATTR_RESULT_LOCKED) { /* TRANSLATORS: Suffix: the HSI result */ return _("Locked"); } if (result == FWUPD_SECURITY_ATTR_RESULT_NOT_LOCKED) { /* TRANSLATORS: Suffix: the HSI result */ return _("Unlocked"); } if (result == FWUPD_SECURITY_ATTR_RESULT_ENCRYPTED) { /* TRANSLATORS: Suffix: the HSI result */ return _("Encrypted"); } if (result == FWUPD_SECURITY_ATTR_RESULT_NOT_ENCRYPTED) { /* TRANSLATORS: Suffix: the HSI result */ return _("Unencrypted"); } if (result == FWUPD_SECURITY_ATTR_RESULT_TAINTED) { /* TRANSLATORS: Suffix: the HSI result */ return _("Tainted"); } if (result == FWUPD_SECURITY_ATTR_RESULT_NOT_TAINTED) { /* TRANSLATORS: Suffix: the HSI result */ return _("Untainted"); } if (result == FWUPD_SECURITY_ATTR_RESULT_FOUND) { /* TRANSLATORS: Suffix: the HSI result */ return _("Found"); } if (result == FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND) { /* TRANSLATORS: Suffix: the HSI result */ return _("Not found"); } if (result == FWUPD_SECURITY_ATTR_RESULT_SUPPORTED) { /* TRANSLATORS: Suffix: the HSI result */ return _("Supported"); } if (result == FWUPD_SECURITY_ATTR_RESULT_NOT_SUPPORTED) { /* TRANSLATORS: Suffix: the HSI result */ return _("Not supported"); } return NULL; } static const gchar * fu_util_security_attr_get_result(FwupdSecurityAttr *attr) { const gchar *tmp; /* common case */ tmp = fu_util_security_attr_result_to_string(fwupd_security_attr_get_result(attr)); if (tmp != NULL) return tmp; /* fallback */ if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)) { /* TRANSLATORS: Suffix: the HSI result */ return _("OK"); } /* TRANSLATORS: Suffix: the fallback HSI result */ return _("Unknown"); } static void fu_util_security_attr_append_str(FwupdSecurityAttr *attr, GString *str, FuSecurityAttrToStringFlags flags) { const gchar *name; /* hide obsoletes by default */ if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_OBSOLETED) && (flags & FU_SECURITY_ATTR_TO_STRING_FLAG_SHOW_OBSOLETES) == 0) return; name = dgettext(GETTEXT_PACKAGE, fwupd_security_attr_get_name(attr)); if (name == NULL) name = fwupd_security_attr_get_appstream_id(attr); if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_OBSOLETED)) { g_string_append(str, "✦ "); } else if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)) { g_string_append(str, "✔ "); } else { g_string_append(str, "✘ "); } g_string_append_printf(str, "%s:", name); for (guint i = fu_strwidth(name); i < 30; i++) g_string_append(str, " "); if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_OBSOLETED)) { g_autofree gchar *fmt = fu_console_color_format(fu_util_security_attr_get_result(attr), FU_CONSOLE_COLOR_YELLOW); g_string_append(str, fmt); } else if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)) { g_autofree gchar *fmt = fu_console_color_format(fu_util_security_attr_get_result(attr), FU_CONSOLE_COLOR_GREEN); g_string_append(str, fmt); } else { g_autofree gchar *fmt = fu_console_color_format(fu_util_security_attr_get_result(attr), FU_CONSOLE_COLOR_RED); g_string_append(str, fmt); } if ((flags & FU_SECURITY_ATTR_TO_STRING_FLAG_SHOW_URLS) > 0 && fwupd_security_attr_get_url(attr) != NULL) { g_string_append_printf(str, ": %s", fwupd_security_attr_get_url(attr)); } if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_OBSOLETED)) { /* TRANSLATORS: this is shown as a suffix for obsoleted tests */ g_string_append_printf(str, " %s", _("(obsoleted)")); } g_string_append_printf(str, "\n"); } static gchar * fu_util_security_event_to_string(FwupdSecurityAttr *attr) { const gchar *name; struct { const gchar *appstream_id; FwupdSecurityAttrResult result_old; FwupdSecurityAttrResult result_new; const gchar *text; } items[] = {{FWUPD_SECURITY_ATTR_ID_IOMMU, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND, FWUPD_SECURITY_ATTR_RESULT_ENABLED, /* TRANSLATORS: HSI event title */ _("IOMMU device protection enabled")}, {FWUPD_SECURITY_ATTR_ID_IOMMU, FWUPD_SECURITY_ATTR_RESULT_ENABLED, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND, /* TRANSLATORS: HSI event title */ _("IOMMU device protection disabled")}, /* ------------------------------------------*/ {FWUPD_SECURITY_ATTR_ID_FWUPD_PLUGINS, FWUPD_SECURITY_ATTR_RESULT_TAINTED, FWUPD_SECURITY_ATTR_RESULT_NOT_TAINTED, NULL}, {FWUPD_SECURITY_ATTR_ID_FWUPD_PLUGINS, FWUPD_SECURITY_ATTR_RESULT_NOT_TAINTED, FWUPD_SECURITY_ATTR_RESULT_TAINTED, NULL}, {FWUPD_SECURITY_ATTR_ID_FWUPD_PLUGINS, FWUPD_SECURITY_ATTR_RESULT_UNKNOWN, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED, NULL}, /* ------------------------------------------*/ {FWUPD_SECURITY_ATTR_ID_KERNEL_TAINTED, FWUPD_SECURITY_ATTR_RESULT_TAINTED, FWUPD_SECURITY_ATTR_RESULT_NOT_TAINTED, /* TRANSLATORS: HSI event title */ _("Kernel is no longer tainted")}, {FWUPD_SECURITY_ATTR_ID_KERNEL_TAINTED, FWUPD_SECURITY_ATTR_RESULT_NOT_TAINTED, FWUPD_SECURITY_ATTR_RESULT_TAINTED, /* TRANSLATORS: HSI event title */ _("Kernel is tainted")}, /* ------------------------------------------*/ {FWUPD_SECURITY_ATTR_ID_KERNEL_LOCKDOWN, FWUPD_SECURITY_ATTR_RESULT_ENABLED, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED, /* TRANSLATORS: HSI event title */ _("Kernel lockdown disabled")}, {FWUPD_SECURITY_ATTR_ID_KERNEL_LOCKDOWN, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED, FWUPD_SECURITY_ATTR_RESULT_ENABLED, /* TRANSLATORS: HSI event title */ _("Kernel lockdown enabled")}, /* ------------------------------------------*/ {FWUPD_SECURITY_ATTR_ID_PREBOOT_DMA_PROTECTION, FWUPD_SECURITY_ATTR_RESULT_ENABLED, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED, /* TRANSLATORS: HSI event title */ _("Pre-boot DMA protection is disabled")}, {FWUPD_SECURITY_ATTR_ID_PREBOOT_DMA_PROTECTION, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED, FWUPD_SECURITY_ATTR_RESULT_ENABLED, /* TRANSLATORS: HSI event title */ _("Pre-boot DMA protection is enabled")}, /* ------------------------------------------*/ {FWUPD_SECURITY_ATTR_ID_UEFI_SECUREBOOT, FWUPD_SECURITY_ATTR_RESULT_ENABLED, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED, /* TRANSLATORS: HSI event title */ _("Secure Boot disabled")}, {FWUPD_SECURITY_ATTR_ID_UEFI_SECUREBOOT, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED, FWUPD_SECURITY_ATTR_RESULT_ENABLED, /* TRANSLATORS: HSI event title */ _("Secure Boot enabled")}, /* ------------------------------------------*/ {FWUPD_SECURITY_ATTR_ID_TPM_EMPTY_PCR, FWUPD_SECURITY_ATTR_RESULT_UNKNOWN, FWUPD_SECURITY_ATTR_RESULT_VALID, /* TRANSLATORS: HSI event title */ _("All TPM PCRs are valid")}, {FWUPD_SECURITY_ATTR_ID_TPM_EMPTY_PCR, FWUPD_SECURITY_ATTR_RESULT_VALID, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID, /* TRANSLATORS: HSI event title */ _("A TPM PCR is now an invalid value")}, {FWUPD_SECURITY_ATTR_ID_TPM_EMPTY_PCR, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID, FWUPD_SECURITY_ATTR_RESULT_VALID, /* TRANSLATORS: HSI event title */ _("All TPM PCRs are now valid")}, /* ------------------------------------------*/ {FWUPD_SECURITY_ATTR_ID_TPM_RECONSTRUCTION_PCR0, FWUPD_SECURITY_ATTR_RESULT_NOT_FOUND, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID, /* TRANSLATORS: HSI event title */ _("TPM PCR0 reconstruction is invalid")}, {FWUPD_SECURITY_ATTR_ID_TPM_RECONSTRUCTION_PCR0, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID, FWUPD_SECURITY_ATTR_RESULT_VALID, /* TRANSLATORS: HSI event title */ _("TPM PCR0 reconstruction is now valid")}, /* ------------------------------------------*/ {FWUPD_SECURITY_ATTR_ID_UEFI_MEMORY_PROTECTION, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED, FWUPD_SECURITY_ATTR_RESULT_NOT_LOCKED, /* TRANSLATORS: HSI event title */ _("UEFI memory protection enabled but not locked")}, {FWUPD_SECURITY_ATTR_ID_UEFI_MEMORY_PROTECTION, FWUPD_SECURITY_ATTR_RESULT_NOT_ENABLED, FWUPD_SECURITY_ATTR_RESULT_LOCKED, /* TRANSLATORS: HSI event title */ _("UEFI memory protection enabled and locked")}, {FWUPD_SECURITY_ATTR_ID_UEFI_MEMORY_PROTECTION, FWUPD_SECURITY_ATTR_RESULT_NOT_LOCKED, FWUPD_SECURITY_ATTR_RESULT_LOCKED, /* TRANSLATORS: HSI event title */ _("UEFI memory protection is now locked")}, {FWUPD_SECURITY_ATTR_ID_UEFI_MEMORY_PROTECTION, FWUPD_SECURITY_ATTR_RESULT_LOCKED, FWUPD_SECURITY_ATTR_RESULT_NOT_LOCKED, /* TRANSLATORS: HSI event title */ _("UEFI memory protection is now unlocked")}, {FWUPD_SECURITY_ATTR_ID_UEFI_DB, FWUPD_SECURITY_ATTR_RESULT_NOT_VALID, FWUPD_SECURITY_ATTR_RESULT_VALID, /* TRANSLATORS: HSI event title */ _("The UEFI certificate store is now up to date")}, {NULL, 0, 0, NULL}}; /* sanity check */ if (fwupd_security_attr_get_appstream_id(attr) == NULL) return NULL; if (fwupd_security_attr_get_result(attr) == FWUPD_SECURITY_ATTR_RESULT_UNKNOWN && fwupd_security_attr_get_result_fallback(attr) == FWUPD_SECURITY_ATTR_RESULT_UNKNOWN) return NULL; /* look for prepared text */ for (guint i = 0; items[i].appstream_id != NULL; i++) { if (g_strcmp0(fwupd_security_attr_get_appstream_id(attr), items[i].appstream_id) == 0 && fwupd_security_attr_get_result(attr) == items[i].result_new && fwupd_security_attr_get_result_fallback(attr) == items[i].result_old) return g_strdup(items[i].text); } /* disappeared */ if (fwupd_security_attr_get_result(attr) == FWUPD_SECURITY_ATTR_RESULT_UNKNOWN) { name = dgettext(GETTEXT_PACKAGE, fwupd_security_attr_get_name(attr)); return g_strdup_printf( /* TRANSLATORS: %1 refers to some kind of security test, e.g. "SPI BIOS region". %2 refers to a result value, e.g. "Invalid" */ _("%s disappeared: %s"), name, fu_util_security_attr_result_to_string( fwupd_security_attr_get_result_fallback(attr))); } /* appeared */ if (fwupd_security_attr_get_result_fallback(attr) == FWUPD_SECURITY_ATTR_RESULT_UNKNOWN) { name = dgettext(GETTEXT_PACKAGE, fwupd_security_attr_get_name(attr)); return g_strdup_printf( /* TRANSLATORS: %1 refers to some kind of security test, e.g. "Encrypted RAM". %2 refers to a result value, e.g. "Invalid" */ _("%s appeared: %s"), name, fu_util_security_attr_result_to_string(fwupd_security_attr_get_result(attr))); } /* fall back to something sensible */ name = dgettext(GETTEXT_PACKAGE, fwupd_security_attr_get_name(attr)); return g_strdup_printf( /* TRANSLATORS: %1 refers to some kind of security test, e.g. "UEFI platform key". * %2 and %3 refer to results value, e.g. "Valid" and "Invalid" */ _("%s changed: %s → %s"), name, fu_util_security_attr_result_to_string(fwupd_security_attr_get_result_fallback(attr)), fu_util_security_attr_result_to_string(fwupd_security_attr_get_result(attr))); } gchar * fu_util_security_events_to_string(GPtrArray *events, FuSecurityAttrToStringFlags strflags) { g_autoptr(GString) str = g_string_new(NULL); /* debugging */ if (g_getenv("FWUPD_VERBOSE") != NULL) { for (guint i = 0; i < events->len; i++) { FwupdSecurityAttr *attr = g_ptr_array_index(events, i); g_autofree gchar *tmp = fwupd_codec_to_string(FWUPD_CODEC(attr)); g_info("%s", tmp); } } for (guint i = 0; i < events->len; i++) { FwupdSecurityAttr *attr = g_ptr_array_index(events, i); g_autoptr(GDateTime) date = NULL; g_autofree gchar *dtstr = NULL; g_autofree gchar *check = NULL; g_autofree gchar *eventstr = NULL; /* skip events that have either been added or removed with no prior value */ if (fwupd_security_attr_get_result(attr) == FWUPD_SECURITY_ATTR_RESULT_UNKNOWN || fwupd_security_attr_get_result_fallback(attr) == FWUPD_SECURITY_ATTR_RESULT_UNKNOWN) continue; date = g_date_time_new_from_unix_utc((gint64)fwupd_security_attr_get_created(attr)); dtstr = g_date_time_format(date, "%F %T"); eventstr = fu_util_security_event_to_string(attr); if (eventstr == NULL) continue; if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)) { check = fu_console_color_format("✔", FU_CONSOLE_COLOR_GREEN); } else { check = fu_console_color_format("✘", FU_CONSOLE_COLOR_RED); } if (str->len == 0) { /* TRANSLATORS: title for host security events */ g_string_append_printf(str, "%s\n", _("Host Security Events")); } g_string_append_printf(str, " %s: %s %s\n", dtstr, check, eventstr); } /* no output required */ if (str->len == 0) return NULL; /* success */ return g_string_free(g_steal_pointer(&str), FALSE); } gchar * fu_util_security_issues_to_string(GPtrArray *devices) { g_autoptr(GString) str = g_string_new(NULL); for (guint i = 0; i < devices->len; i++) { FwupdDevice *device = g_ptr_array_index(devices, i); GPtrArray *issues = fwupd_device_get_issues(device); if (issues->len == 0) continue; if (str->len == 0) { g_string_append_printf( str, "%s\n", /* TRANSLATORS: now list devices with unfixed high-priority issues */ _("There are devices with issues:")); } g_string_append_printf(str, "\n %s — %s:\n", fwupd_device_get_vendor(device), fwupd_device_get_name(device)); for (guint j = 0; j < issues->len; j++) { const gchar *issue = g_ptr_array_index(issues, j); g_string_append_printf(str, " • %s\n", issue); } } /* no output required */ if (str->len == 0) return NULL; /* success */ return g_string_free(g_steal_pointer(&str), FALSE); } gchar * fu_util_security_attrs_to_string(GPtrArray *attrs, FuSecurityAttrToStringFlags strflags) { FwupdSecurityAttrFlags flags = FWUPD_SECURITY_ATTR_FLAG_NONE; const FwupdSecurityAttrFlags hpi_suffixes[] = { FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE, FWUPD_SECURITY_ATTR_FLAG_NONE, }; GString *str = g_string_new(NULL); gboolean low_help = FALSE; gboolean runtime_help = FALSE; gboolean pcr0_help = FALSE; for (guint j = 1; j <= FWUPD_SECURITY_ATTR_LEVEL_LAST; j++) { gboolean has_header = FALSE; for (guint i = 0; i < attrs->len; i++) { FwupdSecurityAttr *attr = g_ptr_array_index(attrs, i); if (fwupd_security_attr_get_level(attr) != j) continue; if (!has_header) { g_string_append_printf(str, "\n\033[1mHSI-%u\033[0m\n", j); has_header = TRUE; } fu_util_security_attr_append_str(attr, str, strflags); /* make sure they have at least HSI-1 */ if (j < FWUPD_SECURITY_ATTR_LEVEL_IMPORTANT && !fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_OBSOLETED) && !fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)) low_help = TRUE; /* check for PCR0 not matching */ if (g_strcmp0(fwupd_security_attr_get_appstream_id(attr), FWUPD_SECURITY_ATTR_ID_TPM_RECONSTRUCTION_PCR0) == 0 && fwupd_security_attr_get_result(attr) == FWUPD_SECURITY_ATTR_RESULT_NOT_VALID) pcr0_help = TRUE; } } for (guint i = 0; i < attrs->len; i++) { FwupdSecurityAttr *attr = g_ptr_array_index(attrs, i); flags |= fwupd_security_attr_get_flags(attr); } for (guint j = 0; hpi_suffixes[j] != FWUPD_SECURITY_ATTR_FLAG_NONE; j++) { if (flags & hpi_suffixes[j]) { g_string_append_printf(str, "\n\033[1m%s -%s\033[0m\n", /* TRANSLATORS: this is the HSI suffix */ _("Runtime Suffix"), fwupd_security_attr_flag_to_suffix(hpi_suffixes[j])); for (guint i = 0; i < attrs->len; i++) { FwupdSecurityAttr *attr = g_ptr_array_index(attrs, i); if (!fwupd_security_attr_has_flag(attr, hpi_suffixes[j])) continue; if (fwupd_security_attr_has_flag( attr, FWUPD_SECURITY_ATTR_FLAG_RUNTIME_ISSUE) && !fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)) runtime_help = TRUE; fu_util_security_attr_append_str(attr, str, strflags); } } } if (low_help) { g_string_append_printf( str, "\n%s\n » %s\n", /* TRANSLATORS: this is instructions on how to improve the HSI security level */ _("This system has a low HSI security level."), "https://fwupd.github.io/hsi.html#low-security-level"); } if (runtime_help) { g_string_append_printf( str, "\n%s\n » %s\n", /* TRANSLATORS: this is instructions on how to improve the HSI suffix */ _("This system has HSI runtime issues."), "https://fwupd.github.io/hsi.html#hsi-runtime-suffix"); } if (pcr0_help) { g_string_append_printf( str, "\n%s\n » %s\n", /* TRANSLATORS: this is more background on a security measurement problem */ _("The TPM PCR0 differs from reconstruction."), "https://fwupd.github.io/hsi.html#pcr0-tpm-event-log-reconstruction"); } return g_string_free(str, FALSE); } gint fu_util_sort_devices_by_flags_cb(gconstpointer a, gconstpointer b) { FuDevice *dev_a = *((FuDevice **)a); FuDevice *dev_b = *((FuDevice **)b); if ((!fu_device_has_flag(dev_a, FWUPD_DEVICE_FLAG_UPDATABLE) && fu_device_has_flag(dev_b, FWUPD_DEVICE_FLAG_UPDATABLE)) || (!fu_device_has_flag(dev_a, FWUPD_DEVICE_FLAG_SUPPORTED) && fu_device_has_flag(dev_b, FWUPD_DEVICE_FLAG_SUPPORTED))) return -1; if ((fu_device_has_flag(dev_a, FWUPD_DEVICE_FLAG_UPDATABLE) && !fu_device_has_flag(dev_b, FWUPD_DEVICE_FLAG_UPDATABLE)) || (fu_device_has_flag(dev_a, FWUPD_DEVICE_FLAG_SUPPORTED) && !fu_device_has_flag(dev_b, FWUPD_DEVICE_FLAG_SUPPORTED))) return 1; return 0; } gboolean fu_util_switch_branch_warning(FuConsole *console, FwupdDevice *dev, FwupdRelease *rel, gboolean assume_yes, GError **error) { const gchar *desc_markup = NULL; g_autofree gchar *desc_plain = NULL; g_autofree gchar *title = NULL; g_autoptr(GString) desc_full = g_string_new(NULL); /* warn the user if the vendor is different */ if (g_strcmp0(fwupd_device_get_vendor(dev), fwupd_release_get_vendor(rel)) != 0) { g_string_append_printf( desc_full, /* TRANSLATORS: %1 is the firmware vendor, %2 is the device vendor name */ _("The firmware from %s is not " "supplied by %s, the hardware vendor."), fwupd_release_get_vendor(rel), fwupd_device_get_vendor(dev)); g_string_append(desc_full, "\n\n"); g_string_append_printf(desc_full, /* TRANSLATORS: %1 is the device vendor name */ _("Your hardware may be damaged using this firmware, " "and installing this release may void any warranty " "with %s."), fwupd_device_get_vendor(dev)); g_string_append(desc_full, "\n\n"); } /* from the in the AppStream data */ desc_markup = fwupd_release_get_description(rel); if (desc_markup == NULL) return TRUE; desc_plain = fu_util_convert_description(desc_markup, error); if (desc_plain == NULL) return FALSE; g_string_append(desc_full, desc_plain); /* TRANSLATORS: show and ask user to confirm -- * %1 is the old branch name, %2 is the new branch name */ title = g_strdup_printf(_("Switch branch from %s to %s?"), fu_util_branch_for_display(fwupd_device_get_branch(dev)), fu_util_branch_for_display(fwupd_release_get_branch(rel))); fu_console_box(console, title, desc_full->str, 80); if (!assume_yes) { if (!fu_console_input_bool(console, FALSE, "%s", /* TRANSLATORS: should the branch be changed */ _("Do you understand the consequences " "of changing the firmware branch?"))) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "Declined branch switch"); return FALSE; } } return TRUE; } gboolean fu_util_prompt_warning_fde(FuConsole *console, FwupdDevice *dev, GError **error) { const gchar *url = "https://github.com/fwupd/fwupd/wiki/Full-Disk-Encryption-Detected"; g_autoptr(GString) str = g_string_new(NULL); if (!fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_AFFECTS_FDE)) return TRUE; g_string_append( str, /* TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM */ _("Some of the platform secrets may be invalidated when updating this firmware.")); g_string_append(str, " "); g_string_append(str, /* TRANSLATORS: 'recovery key' here refers to a code, rather than a physical metal thing */ _("Please ensure you have the volume recovery key before continuing.")); g_string_append(str, "\n\n"); g_string_append_printf(str, /* TRANSLATORS: the %1 is a URL to a wiki page */ _("See %s for more details."), url); /* TRANSLATORS: title text, shown as a warning */ fu_console_box(console, _("Full Disk Encryption Detected"), str->str, 80); /* TRANSLATORS: prompt to apply the update */ if (!fu_console_input_bool(console, TRUE, "%s", _("Perform operation?"))) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "Request canceled"); return FALSE; } return TRUE; } void fu_util_show_unsupported_warning(FuConsole *console) { #ifndef SUPPORTED_BUILD if (g_getenv("FWUPD_SUPPORTED") != NULL) return; /* TRANSLATORS: this is a prefix on the console */ fu_console_print_full(console, FU_CONSOLE_PRINT_FLAG_WARNING | FU_CONSOLE_PRINT_FLAG_STDERR, "%s\n", /* TRANSLATORS: unsupported build of the package */ _("This package has not been validated, it may not work properly.")); #endif } gboolean fu_util_modify_remote_warning(FuConsole *console, FwupdRemote *remote, gboolean assume_yes, GError **error) { const gchar *warning_markup = NULL; g_autofree gchar *warning_plain = NULL; /* get formatted text */ warning_markup = fwupd_remote_get_agreement(remote); if (warning_markup == NULL) return TRUE; warning_plain = fu_util_convert_description(warning_markup, error); if (warning_plain == NULL) return FALSE; /* TRANSLATORS: a remote here is like a 'repo' or software source */ fu_console_box(console, _("Enable new remote?"), warning_plain, 80); if (!assume_yes) { if (!fu_console_input_bool(console, TRUE, "%s", /* TRANSLATORS: should the remote still be enabled */ _("Agree and enable the remote?"))) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "Declined agreement"); return FALSE; } } return TRUE; } G_DEFINE_AUTOPTR_CLEANUP_FUNC(CURLU, curl_url_cleanup) gboolean fu_util_is_url(const gchar *perhaps_url) { g_autoptr(CURLU) h = curl_url(); return curl_url_set(h, CURLUPART_URL, perhaps_url, 0) == CURLUE_OK; } gboolean fu_util_print_builder(FuConsole *console, JsonBuilder *builder, GError **error) { g_autofree gchar *data = NULL; g_autoptr(JsonGenerator) json_generator = NULL; g_autoptr(JsonNode) json_root = NULL; /* export as a string */ json_root = json_builder_get_root(builder); json_generator = json_generator_new(); json_generator_set_pretty(json_generator, TRUE); json_generator_set_root(json_generator, json_root); data = json_generator_to_data(json_generator, NULL); if (data == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "Failed to convert to JSON string"); return FALSE; } /* just print */ fu_console_print_literal(console, data); return TRUE; } void fu_util_print_error_as_json(FuConsole *console, const GError *error) { g_autoptr(JsonBuilder) builder = json_builder_new(); json_builder_begin_object(builder); json_builder_set_member_name(builder, "Error"); json_builder_begin_object(builder); json_builder_set_member_name(builder, "Domain"); json_builder_add_string_value(builder, g_quark_to_string(error->domain)); json_builder_set_member_name(builder, "Code"); json_builder_add_int_value(builder, error->code); json_builder_set_member_name(builder, "Message"); json_builder_add_string_value(builder, error->message); json_builder_end_object(builder); json_builder_end_object(builder); fu_util_print_builder(console, builder, NULL); } typedef enum { FU_UTIL_DEPENDENCY_KIND_UNKNOWN, FU_UTIL_DEPENDENCY_KIND_RUNTIME, FU_UTIL_DEPENDENCY_KIND_COMPILE, } FuUtilDependencyKind; static const gchar * fu_util_dependency_kind_to_string(FuUtilDependencyKind dependency_kind) { if (dependency_kind == FU_UTIL_DEPENDENCY_KIND_RUNTIME) return "runtime"; if (dependency_kind == FU_UTIL_DEPENDENCY_KIND_COMPILE) return "compile"; return NULL; } static gchar * fu_util_parse_project_dependency(const gchar *str, FuUtilDependencyKind *dependency_kind) { g_return_val_if_fail(str != NULL, NULL); if (g_str_has_prefix(str, "RuntimeVersion(")) { gsize strsz = strlen(str); if (dependency_kind != NULL) *dependency_kind = FU_UTIL_DEPENDENCY_KIND_RUNTIME; return g_strndup(str + 15, strsz - 16); } if (g_str_has_prefix(str, "CompileVersion(")) { gsize strsz = strlen(str); if (dependency_kind != NULL) *dependency_kind = FU_UTIL_DEPENDENCY_KIND_COMPILE; return g_strndup(str + 15, strsz - 16); } return g_strdup(str); } static gboolean fu_util_print_version_key_valid(const gchar *key) { g_return_val_if_fail(key != NULL, FALSE); if (g_str_has_prefix(key, "RuntimeVersion")) return TRUE; if (g_str_has_prefix(key, "CompileVersion")) return TRUE; return FALSE; } gboolean fu_util_project_versions_as_json(FuConsole *console, GHashTable *metadata, GError **error) { GHashTableIter iter; const gchar *key; const gchar *value; g_autoptr(JsonBuilder) builder = json_builder_new(); json_builder_begin_object(builder); json_builder_set_member_name(builder, "Versions"); json_builder_begin_array(builder); g_hash_table_iter_init(&iter, metadata); while (g_hash_table_iter_next(&iter, (gpointer *)&key, (gpointer *)&value)) { FuUtilDependencyKind dependency_kind = FU_UTIL_DEPENDENCY_KIND_UNKNOWN; g_autofree gchar *project = NULL; /* add version keys */ if (!fu_util_print_version_key_valid(key)) continue; project = fu_util_parse_project_dependency(key, &dependency_kind); json_builder_begin_object(builder); if (dependency_kind != FU_UTIL_DEPENDENCY_KIND_UNKNOWN) { json_builder_set_member_name(builder, "Type"); json_builder_add_string_value( builder, fu_util_dependency_kind_to_string(dependency_kind)); } json_builder_set_member_name(builder, "AppstreamId"); json_builder_add_string_value(builder, project); json_builder_set_member_name(builder, "Version"); json_builder_add_string_value(builder, value); json_builder_end_object(builder); } json_builder_end_array(builder); json_builder_end_object(builder); return fu_util_print_builder(console, builder, error); } gchar * fu_util_project_versions_to_string(GHashTable *metadata) { GHashTableIter iter; const gchar *key; const gchar *value; g_autoptr(GString) str = g_string_new(NULL); g_hash_table_iter_init(&iter, metadata); while (g_hash_table_iter_next(&iter, (gpointer *)&key, (gpointer *)&value)) { FuUtilDependencyKind dependency_kind = FU_UTIL_DEPENDENCY_KIND_UNKNOWN; g_autofree gchar *project = NULL; /* print version keys */ if (!fu_util_print_version_key_valid(key)) continue; project = fu_util_parse_project_dependency(key, &dependency_kind); g_string_append_printf(str, "%-10s%-30s%s\n", fu_util_dependency_kind_to_string(dependency_kind), project, value); } return g_string_free(g_steal_pointer(&str), FALSE); } const gchar * fu_util_get_prgname(const gchar *argv0) { const gchar *prgname = (const gchar *)g_strrstr(argv0, " "); if (prgname != NULL) return prgname + 1; return argv0; } fwupd-2.0.10/src/fu-util-common.h000066400000000000000000000113441501337203100165300ustar00rootroot00000000000000/* * Copyright 2017 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include #include "fwupd-security-attr-private.h" #include "fu-console.h" /* custom return codes */ #define EXIT_NOTHING_TO_DO 2 #define EXIT_NOT_FOUND 3 #define EXIT_NOT_REACHABLE 101 /* ENETUNREACH */ /* this is only valid for tools */ #define FWUPD_ERROR_INVALID_ARGS (FWUPD_ERROR_LAST + 1) typedef struct FuUtilPrivate FuUtilPrivate; typedef gboolean (*FuUtilCmdFunc)(FuUtilPrivate *util, gchar **values, GError **error) G_GNUC_NON_NULL(1); typedef struct { gchar *name; gchar *arguments; gchar *description; FuUtilCmdFunc callback; } FuUtilCmd; typedef enum { FU_SECURITY_ATTR_TO_STRING_FLAG_NONE = 0, FU_SECURITY_ATTR_TO_STRING_FLAG_SHOW_OBSOLETES = 1 << 0, FU_SECURITY_ATTR_TO_STRING_FLAG_SHOW_URLS = 1 << 1, /*< private >*/ FU_SECURITY_ATTR_TO_STRING_FLAG_LAST } FuSecurityAttrToStringFlags; /* node with refcounted data */ typedef GNode FuUtilNode; void fu_util_print_node(FuConsole *console, FwupdClient *client, FuUtilNode *n) G_GNUC_NON_NULL(1, 2, 3); void fu_util_free_node(FuUtilNode *n) G_GNUC_NON_NULL(1); G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuUtilNode, fu_util_free_node) gboolean fu_util_is_interesting_device(FwupdDevice *dev) G_GNUC_NON_NULL(1); gchar * fu_util_get_user_cache_path(const gchar *fn) G_GNUC_NON_NULL(1); gboolean fu_util_prompt_warning(FuConsole *console, FwupdDevice *device, FwupdRelease *release, const gchar *machine, GError **error) G_GNUC_NON_NULL(1, 2, 3, 4); gboolean fu_util_prompt_warning_fde(FuConsole *console, FwupdDevice *dev, GError **error) G_GNUC_NON_NULL(1); gboolean fu_util_modify_remote_warning(FuConsole *console, FwupdRemote *remote, gboolean assume_yes, GError **error) G_GNUC_NON_NULL(1, 2); gboolean fu_util_prompt_complete(FuConsole *console, FwupdDeviceFlags flags, gboolean prompt, GError **error) G_GNUC_NON_NULL(1); GPtrArray * fu_util_cmd_array_new(void); void fu_util_cmd_array_add(GPtrArray *array, const gchar *name, const gchar *arguments, const gchar *description, FuUtilCmdFunc callback) G_GNUC_NON_NULL(1, 2, 4); gchar * fu_util_cmd_array_to_string(GPtrArray *array) G_GNUC_NON_NULL(1); void fu_util_cmd_array_sort(GPtrArray *array) G_GNUC_NON_NULL(1); gboolean fu_util_cmd_array_run(GPtrArray *array, FuUtilPrivate *priv, const gchar *command, gchar **values, GError **error) G_GNUC_NON_NULL(1, 2); const gchar * fu_util_branch_for_display(const gchar *branch); const gchar * fu_util_request_get_message(FwupdRequest *req) G_GNUC_NON_NULL(1); gboolean fu_util_parse_filter_device_flags(const gchar *filter, FwupdDeviceFlags *include, FwupdDeviceFlags *exclude, GError **error) G_GNUC_NON_NULL(1, 2, 3); gboolean fu_util_parse_filter_release_flags(const gchar *filter, FwupdReleaseFlags *include, FwupdReleaseFlags *exclude, GError **error) G_GNUC_NON_NULL(1, 2, 3); gchar * fu_util_device_to_string(FwupdClient *client, FwupdDevice *dev, guint idt) G_GNUC_NON_NULL(1, 2); gchar * fu_util_plugin_to_string(FwupdPlugin *plugin, guint idt) G_GNUC_NON_NULL(1); gchar * fu_util_plugin_flag_to_string(FwupdPluginFlags plugin_flag); gint fu_util_plugin_name_sort_cb(FwupdPlugin **item1, FwupdPlugin **item2); gchar * fu_util_device_problem_to_string(FwupdClient *client, FwupdDevice *dev, FwupdDeviceProblem problem) G_GNUC_NON_NULL(1, 2); gchar * fu_util_security_attrs_to_string(GPtrArray *attrs, FuSecurityAttrToStringFlags flags) G_GNUC_NON_NULL(1); gchar * fu_util_security_events_to_string(GPtrArray *events, FuSecurityAttrToStringFlags flags) G_GNUC_NON_NULL(1); gchar * fu_util_security_issues_to_string(GPtrArray *devices) G_GNUC_NON_NULL(1); gint fu_util_sort_devices_by_flags_cb(gconstpointer a, gconstpointer b) G_GNUC_NON_NULL(1, 2); gboolean fu_util_switch_branch_warning(FuConsole *console, FwupdDevice *dev, FwupdRelease *rel, gboolean assume_yes, GError **error) G_GNUC_NON_NULL(1, 2, 3); void fu_util_show_unsupported_warning(FuConsole *console) G_GNUC_NON_NULL(1); gboolean fu_util_is_url(const gchar *perhaps_url) G_GNUC_NON_NULL(1); gboolean fu_util_print_builder(FuConsole *console, JsonBuilder *builder, GError **error) G_GNUC_NON_NULL(1, 2); void fu_util_print_error_as_json(FuConsole *console, const GError *error) G_GNUC_NON_NULL(1); gchar * fu_util_project_versions_to_string(GHashTable *metadata) G_GNUC_NON_NULL(1); gboolean fu_util_project_versions_as_json(FuConsole *console, GHashTable *metadata, GError **error) G_GNUC_NON_NULL(1, 2); const gchar * fu_util_get_prgname(const gchar *argv0) G_GNUC_NON_NULL(1); fwupd-2.0.10/src/fu-util.c000066400000000000000000005447231501337203100152510ustar00rootroot00000000000000/* * Copyright 2015 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuMain" #include "config.h" #include #include #include #include #ifdef HAVE_GIO_UNIX #include #include #endif #include #include #include #include "fwupd-common-private.h" #include "fwupd-device-private.h" #include "fwupd-remote-private.h" #include "fu-console.h" #include "fu-plugin-private.h" #include "fu-polkit-agent.h" #include "fu-util-bios-setting.h" #include "fu-util-common.h" #ifdef HAVE_SYSTEMD #include "fu-systemd.h" #endif typedef enum { FU_UTIL_OPERATION_UNKNOWN, FU_UTIL_OPERATION_UPDATE, FU_UTIL_OPERATION_DOWNGRADE, FU_UTIL_OPERATION_INSTALL, FU_UTIL_OPERATION_LAST } FuUtilOperation; struct FuUtilPrivate { GCancellable *cancellable; GMainContext *main_ctx; GMainLoop *loop; GOptionContext *context; FwupdInstallFlags flags; FwupdClientDownloadFlags download_flags; FwupdClient *client; FuConsole *console; gboolean no_remote_check; gboolean no_metadata_check; gboolean no_reboot_check; gboolean no_unreported_check; gboolean no_safety_check; gboolean no_device_prompt; gboolean no_emulation_check; gboolean no_security_fix; gboolean assume_yes; gboolean sign; gboolean show_all; gboolean disable_ssl_strict; gboolean as_json; /* only valid in update and downgrade */ FuUtilOperation current_operation; FwupdDevice *current_device; GPtrArray *post_requests; FwupdDeviceFlags completion_flags; FwupdDeviceFlags filter_device_include; FwupdDeviceFlags filter_device_exclude; FwupdReleaseFlags filter_release_include; FwupdReleaseFlags filter_release_exclude; }; static gboolean fu_util_report_history(FuUtilPrivate *priv, gchar **values, GError **error); static FwupdDevice * fu_util_get_device_by_id(FuUtilPrivate *priv, const gchar *id, GError **error); static void fu_util_client_notify_cb(GObject *object, GParamSpec *pspec, FuUtilPrivate *priv) { if (priv->as_json) return; fu_console_set_progress(priv->console, fwupd_client_get_status(priv->client), fwupd_client_get_percentage(priv->client)); } static void fu_util_update_device_request_cb(FwupdClient *client, FwupdRequest *request, FuUtilPrivate *priv) { /* nothing sensible to show */ if (fwupd_request_get_message(request) == NULL) return; /* show this now */ if (fwupd_request_get_kind(request) == FWUPD_REQUEST_KIND_IMMEDIATE) { g_autofree gchar *fmt = NULL; g_autofree gchar *tmp = NULL; /* TRANSLATORS: the user needs to do something, e.g. remove the device */ fmt = fu_console_color_format(_("Action Required:"), FU_CONSOLE_COLOR_RED); tmp = g_strdup_printf("%s %s", fmt, fwupd_request_get_message(request)); fu_console_set_progress_title(priv->console, tmp); fu_console_beep(priv->console, 5); } /* save for later */ if (fwupd_request_get_kind(request) == FWUPD_REQUEST_KIND_POST) g_ptr_array_add(priv->post_requests, g_object_ref(request)); } static void fu_util_update_device_changed_cb(FwupdClient *client, FwupdDevice *device, FuUtilPrivate *priv) { g_autofree gchar *str = NULL; /* action has not been assigned yet */ if (priv->current_operation == FU_UTIL_OPERATION_UNKNOWN) return; /* allowed to set whenever the device has changed */ if (fwupd_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN)) priv->completion_flags |= FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN; if (fwupd_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT)) priv->completion_flags |= FWUPD_DEVICE_FLAG_NEEDS_REBOOT; /* same as last time, so ignore */ if (priv->current_device == NULL || g_strcmp0(fwupd_device_get_composite_id(priv->current_device), fwupd_device_get_composite_id(device)) == 0) { g_set_object(&priv->current_device, device); return; } /* ignore indirect devices that might have changed */ if (fwupd_device_get_status(device) == FWUPD_STATUS_IDLE || fwupd_device_get_status(device) == FWUPD_STATUS_UNKNOWN) { g_debug("ignoring %s with status %s", fwupd_device_get_name(device), fwupd_status_to_string(fwupd_device_get_status(device))); return; } /* show message in console */ if (priv->current_operation == FU_UTIL_OPERATION_UPDATE) { /* TRANSLATORS: %1 is a device name */ str = g_strdup_printf(_("Updating %s…"), fwupd_device_get_name(device)); fu_console_set_progress_title(priv->console, str); } else if (priv->current_operation == FU_UTIL_OPERATION_DOWNGRADE) { /* TRANSLATORS: %1 is a device name */ str = g_strdup_printf(_("Downgrading %s…"), fwupd_device_get_name(device)); fu_console_set_progress_title(priv->console, str); } else if (priv->current_operation == FU_UTIL_OPERATION_INSTALL) { /* TRANSLATORS: %1 is a device name */ str = g_strdup_printf(_("Installing on %s…"), fwupd_device_get_name(device)); fu_console_set_progress_title(priv->console, str); } else { g_warning("no FuUtilOperation set"); } g_set_object(&priv->current_device, device); } static FwupdDevice * fu_util_prompt_for_device(FuUtilPrivate *priv, GPtrArray *devices, GError **error) { FwupdDevice *dev; guint idx; g_autoptr(GPtrArray) devices_filtered = NULL; /* filter results */ devices_filtered = fwupd_device_array_filter_flags(devices, priv->filter_device_include, priv->filter_device_exclude, error); if (devices_filtered == NULL) return NULL; /* exactly one */ if (devices_filtered->len == 1) { dev = g_ptr_array_index(devices_filtered, 0); if (!priv->as_json) { fu_console_print( priv->console, "%s: %s", /* TRANSLATORS: device has been chosen by the daemon for the user */ _("Selected device"), fwupd_device_get_name(dev)); } return g_object_ref(dev); } /* no questions */ if (priv->no_device_prompt) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "can't prompt for devices"); return NULL; } /* TRANSLATORS: this is to abort the interactive prompt */ fu_console_print(priv->console, "0.\t%s", _("Cancel")); for (guint i = 0; i < devices_filtered->len; i++) { dev = g_ptr_array_index(devices_filtered, i); fu_console_print(priv->console, "%u.\t%s (%s)", i + 1, fwupd_device_get_id(dev), fwupd_device_get_name(dev)); } /* TRANSLATORS: get interactive prompt */ idx = fu_console_input_uint(priv->console, devices_filtered->len, "%s", _("Choose device")); if (idx == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "Request canceled"); return NULL; } dev = g_ptr_array_index(devices_filtered, idx - 1); return g_object_ref(dev); } static gboolean fu_util_perhaps_show_unreported(FuUtilPrivate *priv, GError **error) { g_autoptr(GError) error_local = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(GPtrArray) devices_failed = g_ptr_array_new(); g_autoptr(GPtrArray) devices_success = g_ptr_array_new(); g_autoptr(GPtrArray) remotes = NULL; g_autoptr(GHashTable) remote_id_uri_map = NULL; gboolean all_automatic = FALSE; /* we don't want to ask anything */ if (priv->no_unreported_check || priv->as_json) { g_debug("skipping unreported check"); return TRUE; } /* get all devices from the history database */ devices = fwupd_client_get_history(priv->client, priv->cancellable, &error_local); if (devices == NULL) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) return TRUE; g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } /* create a map of RemoteID to RemoteURI */ remotes = fwupd_client_get_remotes(priv->client, priv->cancellable, error); if (remotes == NULL) return FALSE; remote_id_uri_map = g_hash_table_new(g_str_hash, g_str_equal); for (guint i = 0; i < remotes->len; i++) { FwupdRemote *remote = g_ptr_array_index(remotes, i); gboolean remote_automatic; if (fwupd_remote_get_id(remote) == NULL) continue; if (fwupd_remote_get_report_uri(remote) == NULL) continue; g_debug("adding %s for %s", fwupd_remote_get_report_uri(remote), fwupd_remote_get_id(remote)); g_hash_table_insert(remote_id_uri_map, (gpointer)fwupd_remote_get_id(remote), (gpointer)fwupd_remote_get_report_uri(remote)); remote_automatic = fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS); g_debug("%s is %d", fwupd_remote_get_title(remote), remote_automatic); if (remote_automatic && !all_automatic) all_automatic = TRUE; if (!remote_automatic && all_automatic) { all_automatic = FALSE; break; } } /* check that they can be reported */ for (guint i = 0; i < devices->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices, i); FwupdRelease *rel = fwupd_device_get_release_default(dev); const gchar *remote_id; const gchar *remote_uri; if (!fwupd_device_match_flags(dev, priv->filter_device_include, priv->filter_device_exclude)) continue; if (fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_REPORTED)) continue; if (!fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_SUPPORTED)) continue; /* find the RemoteURI to use for the device */ remote_id = fwupd_release_get_remote_id(rel); if (remote_id == NULL) { g_debug("%s has no RemoteID", fwupd_device_get_id(dev)); continue; } remote_uri = g_hash_table_lookup(remote_id_uri_map, remote_id); if (remote_uri == NULL) { g_debug("%s has no RemoteURI", remote_id); continue; } /* only send success and failure */ if (fwupd_device_get_update_state(dev) == FWUPD_UPDATE_STATE_FAILED) { g_ptr_array_add(devices_failed, dev); } else if (fwupd_device_get_update_state(dev) == FWUPD_UPDATE_STATE_SUCCESS) { g_ptr_array_add(devices_success, dev); } else { g_debug("ignoring %s with UpdateState %s", fwupd_device_get_id(dev), fwupd_update_state_to_string(fwupd_device_get_update_state(dev))); } } /* nothing to do */ if (devices_failed->len == 0 && devices_success->len == 0) { g_debug("no unreported devices"); return TRUE; } g_debug("All automatic: %d", all_automatic); /* show the success and failures */ if (!priv->assume_yes && !all_automatic) { /* delimit */ fu_console_line(priv->console, 48); /* failures */ if (devices_failed->len > 0) { fu_console_print_literal(priv->console, /* TRANSLATORS: a list of failed updates */ _("Devices that were not updated correctly:")); for (guint i = 0; i < devices_failed->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices_failed, i); FwupdRelease *rel = fwupd_device_get_release_default(dev); fu_console_print(priv->console, " • %s (%s → %s)", fwupd_device_get_name(dev), fwupd_device_get_version(dev), fwupd_release_get_version(rel)); } } /* success */ if (devices_success->len > 0) { fu_console_print_literal(priv->console, /* TRANSLATORS: a list of successful updates */ _("Devices that have been updated successfully:")); for (guint i = 0; i < devices_success->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices_success, i); FwupdRelease *rel = fwupd_device_get_release_default(dev); fu_console_print(priv->console, " • %s (%s → %s)", fwupd_device_get_name(dev), fwupd_device_get_version(dev), fwupd_release_get_version(rel)); } } /* ask for permission */ fu_console_print_literal(priv->console, /* TRANSLATORS: explain why we want to upload */ _("Uploading firmware reports helps hardware vendors " "to quickly identify failing and successful updates " "on real devices.")); if (!fu_console_input_bool(priv->console, TRUE, "%s (%s)", /* TRANSLATORS: ask the user to upload */ _("Review and upload report now?"), /* TRANSLATORS: metadata is downloaded */ _("Requires internet connection"))) { if (fu_console_input_bool(priv->console, FALSE, "%s", /* TRANSLATORS: offer to disable this nag */ _("Do you want to disable this feature " "for future updates?"))) { for (guint i = 0; i < remotes->len; i++) { FwupdRemote *remote = g_ptr_array_index(remotes, i); const gchar *remote_id = fwupd_remote_get_id(remote); if (fwupd_remote_get_report_uri(remote) == NULL) continue; if (!fwupd_client_modify_remote(priv->client, remote_id, "ReportURI", "", priv->cancellable, error)) return FALSE; } } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "Declined upload"); return FALSE; } } /* upload */ if (!fu_util_report_history(priv, NULL, error)) return FALSE; /* offer to make automatic */ if (!priv->assume_yes && !all_automatic) { if (fu_console_input_bool(priv->console, FALSE, "%s", /* TRANSLATORS: offer to stop asking the question */ _("Do you want to upload reports automatically for " "future updates?"))) { for (guint i = 0; i < remotes->len; i++) { FwupdRemote *remote = g_ptr_array_index(remotes, i); const gchar *remote_id = fwupd_remote_get_id(remote); if (fwupd_remote_get_report_uri(remote) == NULL) continue; if (fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS)) continue; if (!fwupd_client_modify_remote(priv->client, remote_id, "AutomaticReports", "true", priv->cancellable, error)) return FALSE; } } } /* success */ return TRUE; } static void fu_util_build_device_tree_node(FuUtilPrivate *priv, FuUtilNode *root, FwupdDevice *dev) { FuUtilNode *root_child = g_node_append_data(root, g_object_ref(dev)); if (fwupd_device_get_release_default(dev) != NULL) g_node_append_data(root_child, g_object_ref(fwupd_device_get_release_default(dev))); } static gboolean fu_util_build_device_tree_cb(FuUtilNode *n, gpointer user_data) { FuUtilPrivate *priv = (FuUtilPrivate *)user_data; FwupdDevice *dev = n->data; /* root node */ if (dev == NULL) return FALSE; /* release */ if (FWUPD_IS_RELEASE(n->data)) return FALSE; /* an interesting child, so include the parent */ for (FuUtilNode *c = n->children; c != NULL; c = c->next) { if (c->data != NULL) return FALSE; } /* not interesting, clear the node data */ if (!fwupd_device_match_flags(dev, priv->filter_device_include, priv->filter_device_exclude)) g_clear_object(&n->data); else if (!priv->show_all && !fu_util_is_interesting_device(dev)) g_clear_object(&n->data); /* continue */ return FALSE; } static void fu_util_build_device_tree(FuUtilPrivate *priv, FuUtilNode *root, GPtrArray *devs) { /* add the top-level parents */ for (guint i = 0; i < devs->len; i++) { FwupdDevice *dev_tmp = g_ptr_array_index(devs, i); if (fwupd_device_get_parent(dev_tmp) != NULL) continue; fu_util_build_device_tree_node(priv, root, dev_tmp); } /* children */ for (guint i = 0; i < devs->len; i++) { FwupdDevice *dev_tmp = g_ptr_array_index(devs, i); FuUtilNode *root_parent; if (fwupd_device_get_parent(dev_tmp) == NULL) continue; root_parent = g_node_find(root, G_PRE_ORDER, G_TRAVERSE_ALL, fwupd_device_get_parent(dev_tmp)); if (root_parent == NULL) continue; fu_util_build_device_tree_node(priv, root_parent, dev_tmp); } /* prune children that are not updatable */ g_node_traverse(root, G_POST_ORDER, G_TRAVERSE_ALL, -1, fu_util_build_device_tree_cb, priv); } static gboolean fu_util_get_releases_as_json(FuUtilPrivate *priv, GPtrArray *rels, GError **error) { g_autoptr(JsonBuilder) builder = json_builder_new(); json_builder_begin_object(builder); json_builder_set_member_name(builder, "Releases"); json_builder_begin_array(builder); for (guint i = 0; i < rels->len; i++) { FwupdRelease *rel = g_ptr_array_index(rels, i); if (!fwupd_release_match_flags(rel, priv->filter_release_include, priv->filter_release_exclude)) continue; json_builder_begin_object(builder); fwupd_codec_to_json(FWUPD_CODEC(rel), builder, FWUPD_CODEC_FLAG_NONE); json_builder_end_object(builder); } json_builder_end_array(builder); json_builder_end_object(builder); return fu_util_print_builder(priv->console, builder, error); } static gboolean fu_util_get_devices_as_json(FuUtilPrivate *priv, GPtrArray *devs, GError **error) { g_autoptr(JsonBuilder) builder = json_builder_new(); json_builder_begin_object(builder); json_builder_set_member_name(builder, "Devices"); json_builder_begin_array(builder); for (guint i = 0; i < devs->len; i++) { FwupdDevice *dev = g_ptr_array_index(devs, i); g_autoptr(GPtrArray) rels = NULL; g_autoptr(GError) error_local = NULL; /* filter */ if (!fwupd_device_match_flags(dev, priv->filter_device_include, priv->filter_device_exclude)) continue; /* add all releases that could be applied */ rels = fwupd_client_get_releases(priv->client, fwupd_device_get_id(dev), priv->cancellable, &error_local); if (rels == NULL) { g_debug("not adding releases to device: %s", error_local->message); } else { for (guint j = 0; j < rels->len; j++) { FwupdRelease *rel = g_ptr_array_index(rels, j); if (!fwupd_release_match_flags(rel, priv->filter_release_include, priv->filter_release_exclude)) continue; fwupd_device_add_release(dev, rel); } } /* add to builder */ json_builder_begin_object(builder); fwupd_codec_to_json(FWUPD_CODEC(dev), builder, FWUPD_CODEC_FLAG_TRUSTED); json_builder_end_object(builder); } json_builder_end_array(builder); json_builder_end_object(builder); return fu_util_print_builder(priv->console, builder, error); } static gboolean fu_util_get_devices(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FuUtilNode) root = g_node_new(NULL); g_autoptr(GPtrArray) devs = NULL; /* get results from daemon */ if (g_strv_length(values) > 0) { devs = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); for (guint i = 0; values[i] != NULL; i++) { FwupdDevice *device = fu_util_get_device_by_id(priv, values[i], error); if (device == NULL) return FALSE; g_ptr_array_add(devs, device); } } else { devs = fwupd_client_get_devices(priv->client, priv->cancellable, error); if (devs == NULL) return FALSE; } /* not for human consumption */ if (priv->as_json) return fu_util_get_devices_as_json(priv, devs, error); /* print */ if (devs->len > 0) fu_util_build_device_tree(priv, root, devs); if (g_node_n_children(root) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, /* TRANSLATORS: nothing attached that can be upgraded */ _("No hardware detected with firmware update capability")); return FALSE; } fu_util_print_node(priv->console, priv->client, root); /* nag? */ if (!fu_util_perhaps_show_unreported(priv, error)) return FALSE; return TRUE; } static gboolean fu_util_get_plugins(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(GPtrArray) plugins = NULL; /* get results from daemon */ plugins = fwupd_client_get_plugins(priv->client, priv->cancellable, error); g_ptr_array_sort(plugins, (GCompareFunc)fu_util_plugin_name_sort_cb); if (plugins == NULL) return FALSE; if (priv->as_json) { g_autoptr(JsonBuilder) builder = json_builder_new(); json_builder_begin_object(builder); fwupd_codec_array_to_json(plugins, "Plugins", builder, FWUPD_CODEC_FLAG_TRUSTED); json_builder_end_object(builder); return fu_util_print_builder(priv->console, builder, error); } /* print */ for (guint i = 0; i < plugins->len; i++) { FuPlugin *plugin = g_ptr_array_index(plugins, i); g_autofree gchar *str = fu_util_plugin_to_string(FWUPD_PLUGIN(plugin), 0); fu_console_print_literal(priv->console, str); } /* success */ return TRUE; } static gchar * fu_util_download_if_required(FuUtilPrivate *priv, const gchar *perhapsfn, GError **error) { g_autofree gchar *filename = NULL; g_autoptr(GBytes) blob = NULL; /* a local file */ if (g_file_test(perhapsfn, G_FILE_TEST_EXISTS)) return g_strdup(perhapsfn); if (!fu_util_is_url(perhapsfn)) return g_strdup(perhapsfn); /* download the firmware to a cachedir */ filename = fu_util_get_user_cache_path(perhapsfn); if (g_file_test(filename, G_FILE_TEST_EXISTS)) return g_steal_pointer(&filename); if (!fu_path_mkdir_parent(filename, error)) return NULL; blob = fwupd_client_download_bytes(priv->client, perhapsfn, priv->download_flags, priv->cancellable, error); if (blob == NULL) return NULL; /* save file to cache */ if (!fu_bytes_set_contents(filename, blob, error)) return NULL; return g_steal_pointer(&filename); } static void fu_util_display_current_message(FuUtilPrivate *priv) { if (priv->as_json) return; /* TRANSLATORS: success message */ fu_console_print_literal(priv->console, _("Successfully installed firmware")); /* print all POST requests */ for (guint i = 0; i < priv->post_requests->len; i++) { FwupdRequest *request = g_ptr_array_index(priv->post_requests, i); fu_console_print_literal(priv->console, fu_util_request_get_message(request)); } } typedef struct { guint nr_success; guint nr_missing; guint nr_skipped; JsonBuilder *builder; const gchar *name; gboolean use_emulation; GHashTable *report_metadata; } FuUtilDeviceTestHelper; static void fu_util_device_test_helper_free(FuUtilDeviceTestHelper *helper) { if (helper->report_metadata != NULL) g_hash_table_unref(helper->report_metadata); if (helper->builder != NULL) g_object_unref(helper->builder); g_free(helper); } static FuUtilDeviceTestHelper * fu_util_device_test_helper_new(void) { FuUtilDeviceTestHelper *helper = g_new0(FuUtilDeviceTestHelper, 1); helper->builder = json_builder_new(); return helper; } G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuUtilDeviceTestHelper, fu_util_device_test_helper_free) static GPtrArray * fu_util_filter_devices(FuUtilPrivate *priv, GPtrArray *devices, GError **error) { g_autoptr(GPtrArray) devices_filtered = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); for (guint i = 0; i < devices->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices, i); if (!fwupd_device_match_flags(dev, priv->filter_device_include, priv->filter_device_exclude)) continue; g_ptr_array_add(devices_filtered, g_object_ref(dev)); } if (devices_filtered->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "failed to find any devices"); return NULL; } return g_steal_pointer(&devices_filtered); } static gboolean fu_util_device_test_component(FuUtilPrivate *priv, FuUtilDeviceTestHelper *helper, JsonObject *json_obj, GError **error) { JsonArray *json_array; const gchar *name = "component"; const gchar *protocol = NULL; g_autoptr(FwupdDevice) device = NULL; /* some elements are optional */ if (json_object_has_member(json_obj, "name")) { name = json_object_get_string_member(json_obj, "name"); json_builder_set_member_name(helper->builder, "name"); json_builder_add_string_value(helper->builder, name); } if (json_object_has_member(json_obj, "protocol")) { protocol = json_object_get_string_member(json_obj, "protocol"); json_builder_set_member_name(helper->builder, "protocol"); json_builder_add_string_value(helper->builder, protocol); } /* find the device with any of the matching GUIDs */ if (!json_object_has_member(json_obj, "guids")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "JSON invalid as has no 'guids'"); return FALSE; } json_array = json_object_get_array_member(json_obj, "guids"); json_builder_set_member_name(helper->builder, "guids"); json_builder_begin_array(helper->builder); for (guint i = 0; i < json_array_get_length(json_array); i++) { JsonNode *json_node = json_array_get_element(json_array, i); FwupdDevice *device_tmp; const gchar *guid = json_node_get_string(json_node); g_autoptr(GPtrArray) devices = NULL; g_autoptr(GPtrArray) devices_filtered = NULL; g_debug("looking for guid %s", guid); devices = fwupd_client_get_devices_by_guid(priv->client, guid, priv->cancellable, NULL); if (devices == NULL) continue; devices_filtered = fu_util_filter_devices(priv, devices, NULL); if (devices_filtered == NULL) continue; if (devices_filtered->len > 1) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "multiple devices with GUID %s", guid); return FALSE; } device_tmp = g_ptr_array_index(devices_filtered, 0); if (protocol != NULL && !fwupd_device_has_protocol(device_tmp, protocol)) continue; device = g_object_ref(device_tmp); json_builder_add_string_value(helper->builder, guid); break; } json_builder_end_array(helper->builder); if (device == NULL) { if (!priv->as_json) { g_autofree gchar *msg = NULL; msg = fu_console_color_format( /* TRANSLATORS: this is for the device tests */ _("Did not find any devices with matching GUIDs"), FU_CONSOLE_COLOR_RED); fu_console_print(priv->console, "%s: %s", name, msg); } g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no devices found"); return FALSE; } /* verify the version matches what we expected */ if (json_object_has_member(json_obj, "version")) { const gchar *version = json_object_get_string_member(json_obj, "version"); if (g_strcmp0(version, fwupd_device_get_version(device)) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "version did not match: got %s, expected %s", fwupd_device_get_version(device), version); return FALSE; } } /* verify the bootloader version matches what we expected */ if (json_object_has_member(json_obj, "version-bootloader")) { const gchar *version = json_object_get_string_member(json_obj, "version-bootloader"); if (g_strcmp0(version, fwupd_device_get_version_bootloader(device)) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "bootloader version did not match: got %s, expected %s", fwupd_device_get_version_bootloader(device), version); return FALSE; } } /* verify the branch matches what we expected */ if (json_object_has_member(json_obj, "branch")) { const gchar *version = json_object_get_string_member(json_obj, "branch"); if (g_strcmp0(version, fwupd_device_get_branch(device)) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "branch did not match: got %s, expected %s", fwupd_device_get_branch(device), version); return FALSE; } } /* success */ if (!priv->as_json) { g_autofree gchar *msg = NULL; /* TRANSLATORS: this is for the device tests */ msg = fu_console_color_format(_("OK!"), FU_CONSOLE_COLOR_GREEN); if (g_strcmp0(name, "component") != 0) { fu_console_print(priv->console, "%s [%s]: %s", helper->name, name, msg); } else { fu_console_print(priv->console, "%s: %s", helper->name, msg); } } helper->nr_success++; return TRUE; } static gboolean fu_util_device_test_remove_emulated_devices(FuUtilPrivate *priv, GError **error) { g_autoptr(GPtrArray) devices = NULL; devices = fwupd_client_get_devices(priv->client, priv->cancellable, error); if (devices == NULL) return FALSE; for (guint i = 0; i < devices->len; i++) { FwupdDevice *device = g_ptr_array_index(devices, i); g_autoptr(GError) error_local = NULL; if (!fwupd_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) continue; if (!fwupd_client_modify_device(priv->client, fwupd_device_get_id(device), "Flags", "~emulated", priv->cancellable, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { g_debug("ignoring: %s", error_local->message); continue; } g_propagate_prefixed_error(error, g_steal_pointer(&error_local), "failed to modify device: "); return FALSE; } } /* success */ return TRUE; } static gchar * fu_util_maybe_expand_basename(FuUtilPrivate *priv, const gchar *maybe_basename, GError **error) { g_autoptr(FwupdRemote) remote = NULL; if (g_str_has_prefix(maybe_basename, "https://")) return g_strdup(maybe_basename); if (g_str_has_prefix(maybe_basename, "/")) return g_strdup(maybe_basename); /* find LVFS remote */ remote = fwupd_client_get_remote_by_id(priv->client, "lvfs", priv->cancellable, error); if (remote == NULL) return NULL; if (fwupd_remote_get_firmware_base_uri(remote)) { g_debug("no FirmwareBaseURI set in lvfs.conf, using default"); return g_strdup_printf("https://fwupd.org/downloads/%s", maybe_basename); } return g_strdup_printf("%s/%s", fwupd_remote_get_firmware_base_uri(remote), maybe_basename); } static gboolean fu_util_device_test_step(FuUtilPrivate *priv, FuUtilDeviceTestHelper *helper, JsonObject *json_obj, GError **error) { JsonArray *json_array; /* send this data to the daemon */ if (helper->use_emulation) { g_autofree gchar *emulation_filename = NULL; g_autofree gchar *emulation_url = NULL; /* just ignore anything without emulation data */ if (json_object_has_member(json_obj, "emulation-url")) { const gchar *url_tmp = json_object_get_string_member(json_obj, "emulation-url"); emulation_url = fu_util_maybe_expand_basename(priv, url_tmp, error); if (emulation_url == NULL) return FALSE; emulation_filename = fu_util_download_if_required(priv, emulation_url, error); if (emulation_filename == NULL) { g_prefix_error(error, "failed to download %s: ", emulation_url); return FALSE; } } else if (json_object_has_member(json_obj, "emulation-file")) { emulation_filename = g_strdup(json_object_get_string_member(json_obj, "emulation-file")); } else { return TRUE; } /* log */ if (emulation_url != NULL) { json_builder_set_member_name(helper->builder, "emulation-url"); json_builder_add_string_value(helper->builder, emulation_url); } json_builder_set_member_name(helper->builder, "emulation-file"); json_builder_add_string_value(helper->builder, emulation_filename); if (!fwupd_client_emulation_load(priv->client, emulation_filename, priv->cancellable, error)) { g_prefix_error(error, "failed to load %s: ", emulation_filename); return FALSE; } } /* download file if required */ if (json_object_has_member(json_obj, "url")) { const gchar *url_tmp = json_object_get_string_member(json_obj, "url"); g_autofree gchar *filename = NULL; g_autofree gchar *url = NULL; g_autoptr(GError) error_local = NULL; url = fu_util_maybe_expand_basename(priv, url_tmp, error); if (url == NULL) return FALSE; filename = fu_util_download_if_required(priv, url, error); if (filename == NULL) { g_prefix_error(error, "failed to download %s: ", url); return FALSE; } /* log */ json_builder_set_member_name(helper->builder, "url"); json_builder_add_string_value(helper->builder, url); /* install file */ priv->flags |= FWUPD_INSTALL_FLAG_ALLOW_OLDER; priv->flags |= FWUPD_INSTALL_FLAG_ALLOW_REINSTALL; if (!fwupd_client_install(priv->client, FWUPD_DEVICE_ID_ANY, filename, priv->flags, priv->cancellable, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) { if (priv->as_json) { json_builder_set_member_name(helper->builder, "info"); json_builder_add_string_value(helper->builder, error_local->message); } else { g_autofree gchar *msg = NULL; msg = fu_console_color_format(error_local->message, FU_CONSOLE_COLOR_YELLOW); fu_console_print(priv->console, "%s: %s", helper->name, msg); } helper->nr_missing++; return TRUE; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } /* process each step */ if (!json_object_has_member(json_obj, "components")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "JSON invalid as has no 'components'"); return FALSE; } json_array = json_object_get_array_member(json_obj, "components"); for (guint i = 0; i < json_array_get_length(json_array); i++) { JsonNode *json_node = json_array_get_element(json_array, i); JsonObject *json_obj_tmp = json_node_get_object(json_node); if (!fu_util_device_test_component(priv, helper, json_obj_tmp, error)) return FALSE; } /* remove emulated devices */ if (helper->use_emulation) { if (!fu_util_device_test_remove_emulated_devices(priv, error)) { g_prefix_error(error, "failed to remove emulated devices: "); return FALSE; } } /* success */ json_builder_set_member_name(helper->builder, "success"); json_builder_add_boolean_value(helper->builder, TRUE); return TRUE; } static gboolean fu_util_device_test_filename(FuUtilPrivate *priv, FuUtilDeviceTestHelper *helper, const gchar *filename, GError **error) { JsonNode *json_root; JsonNode *json_steps; JsonObject *json_obj; guint repeat = 1; g_autoptr(JsonParser) parser = json_parser_new(); /* log */ json_builder_set_member_name(helper->builder, "filename"); json_builder_add_string_value(helper->builder, filename); /* parse JSON */ if (!json_parser_load_from_file(parser, filename, error)) { g_prefix_error(error, "test not in JSON format: "); return FALSE; } json_root = json_parser_get_root(parser); if (json_root == NULL || !JSON_NODE_HOLDS_OBJECT(json_root)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "JSON invalid as has no root"); return FALSE; } json_obj = json_node_get_object(json_root); if (!json_object_has_member(json_obj, "steps")) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "JSON invalid as has no 'steps'"); return FALSE; } json_steps = json_object_get_member(json_obj, "steps"); if (!JSON_NODE_HOLDS_ARRAY(json_steps)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "JSON invalid as has 'steps' is not an array"); return FALSE; } /* some elements are optional */ if (json_object_has_member(json_obj, "name")) { helper->name = json_object_get_string_member(json_obj, "name"); json_builder_set_member_name(helper->builder, "name"); json_builder_add_string_value(helper->builder, helper->name); } if (json_object_has_member(json_obj, "interactive")) { gboolean interactive = json_object_get_boolean_member(json_obj, "interactive"); json_builder_set_member_name(helper->builder, "interactive"); json_builder_add_boolean_value(helper->builder, interactive); } if (json_object_has_member(json_obj, "cpu-architectures")) { JsonArray *json_array = json_object_get_array_member(json_obj, "cpu-architectures"); gboolean matched = FALSE; const gchar *arch = g_hash_table_lookup(helper->report_metadata, "CpuArchitecture"); for (guint i = 0; i < json_array_get_length(json_array); i++) { const gchar *arch_tmp = json_array_get_string_element(json_array, i); if (g_strcmp0(arch, arch_tmp) == 0) { matched = TRUE; break; } } if (!matched) { helper->nr_skipped++; return TRUE; } } /* process each step */ if (json_object_has_member(json_obj, "repeat")) { repeat = json_object_get_int_member(json_obj, "repeat"); json_builder_set_member_name(helper->builder, "repeat"); json_builder_add_int_value(helper->builder, repeat); } json_builder_set_member_name(helper->builder, "steps"); json_builder_begin_array(helper->builder); for (guint j = 0; j < repeat; j++) { JsonArray *json_array = json_node_get_array(json_steps); for (guint i = 0; i < json_array_get_length(json_array); i++) { JsonNode *json_node = json_array_get_element(json_array, i); json_obj = json_node_get_object(json_node); json_builder_begin_object(helper->builder); if (!fu_util_device_test_step(priv, helper, json_obj, error)) return FALSE; json_builder_end_object(helper->builder); } } json_builder_end_array(helper->builder); /* success */ return TRUE; } typedef struct { FuUtilPrivate *priv; gchar *inhibit_id; } FuUtilInhibitHelper; static void fu_util_inhibit_helper_free(FuUtilInhibitHelper *helper) { g_free(helper->inhibit_id); g_free(helper); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuUtilInhibitHelper, fu_util_inhibit_helper_free) static gboolean fu_util_inhibit_timeout_cb(FuUtilInhibitHelper *helper) { FuUtilPrivate *priv = helper->priv; g_autoptr(GError) error_local = NULL; if (!fwupd_client_uninhibit(priv->client, helper->inhibit_id, priv->cancellable, &error_local)) { g_warning("failed to auto-uninhibit: %s", error_local->message); } g_main_loop_quit(priv->loop); return G_SOURCE_REMOVE; } static gboolean fu_util_inhibit(FuUtilPrivate *priv, gchar **values, GError **error) { const gchar *reason = "not set"; guint64 timeout_ms = 0; g_autoptr(FuUtilInhibitHelper) helper = g_new0(FuUtilInhibitHelper, 1); g_autoptr(GString) str = g_string_new(NULL); if (g_strv_length(values) > 0) reason = values[0]; if (g_strv_length(values) > 1) { if (!fu_strtoull(values[1], &timeout_ms, 0, G_MAXUINT32, FU_INTEGER_BASE_AUTO, error)) return FALSE; } /* inhibit then wait */ helper->priv = priv; helper->inhibit_id = fwupd_client_inhibit(priv->client, reason, priv->cancellable, error); if (helper->inhibit_id == NULL) return FALSE; if (timeout_ms > 0) { g_autoptr(GSource) source = g_timeout_source_new(timeout_ms); g_source_set_callback(source, (GSourceFunc)fu_util_inhibit_timeout_cb, helper, NULL); g_source_attach(source, priv->main_ctx); } /* TRANSLATORS: the inhibit ID is a short string like dbus-123456 */ g_string_append_printf(str, _("Inhibit ID is %s."), helper->inhibit_id); g_string_append(str, "\n"); if (timeout_ms > 0) { g_string_append_printf(str, /* TRANSLATORS: we can auto-uninhibit after a timeout */ _("Automatically uninhibiting in %ums…"), (guint)timeout_ms); g_string_append(str, "\n"); } /* TRANSLATORS: CTRL^C [holding control, and then pressing C] will exit the program */ g_string_append(str, _("Use CTRL^C to cancel.")); /* TRANSLATORS: this CLI tool is now preventing system updates */ fu_console_box(priv->console, _("System Update Inhibited"), str->str, 80); g_main_loop_run(priv->loop); return TRUE; } static gboolean fu_util_uninhibit(FuUtilPrivate *priv, gchar **values, GError **error) { /* one argument required */ if (g_strv_length(values) != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments, expected INHIBIT-ID"); return FALSE; } /* just uninhibit with the token */ return fwupd_client_uninhibit(priv->client, values[0], priv->cancellable, error); } typedef struct { FuUtilPrivate *priv; const gchar *value; FwupdDevice *device; /* no-ref */ } FuUtilWaitHelper; static void fu_util_device_wait_added_cb(FwupdClient *client, FwupdDevice *device, FuUtilWaitHelper *helper) { FuUtilPrivate *priv = helper->priv; if (g_strcmp0(fwupd_device_get_id(device), helper->value) == 0 || fwupd_device_has_guid(device, helper->value)) { helper->device = device; g_main_loop_quit(priv->loop); return; } } static gboolean fu_util_device_wait_timeout_cb(gpointer user_data) { FuUtilPrivate *priv = (FuUtilPrivate *)user_data; g_main_loop_quit(priv->loop); return G_SOURCE_REMOVE; } static gboolean fu_util_device_wait(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FwupdDevice) device = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(GSource) source = g_timeout_source_new_seconds(30); g_autoptr(GTimer) timer = g_timer_new(); FuUtilWaitHelper helper = {.priv = priv, .value = values[0]}; /* one argument required */ if (g_strv_length(values) != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments, expected GUID|DEVICE-ID"); return FALSE; } /* check if the device already exists */ device = fwupd_client_get_device_by_id(priv->client, helper.value, NULL, NULL); if (device != NULL) { /* TRANSLATORS: the device is already connected */ fu_console_print_literal(priv->console, _("Device already exists")); return TRUE; } devices = fwupd_client_get_devices_by_guid(priv->client, helper.value, NULL, NULL); if (devices != NULL) { /* TRANSLATORS: the device is already connected */ fu_console_print_literal(priv->console, _("Device already exists")); return TRUE; } /* wait for device to show up */ fu_console_set_progress(priv->console, FWUPD_STATUS_IDLE, 0); g_signal_connect(FWUPD_CLIENT(priv->client), "device-added", G_CALLBACK(fu_util_device_wait_added_cb), &helper); g_source_set_callback(source, fu_util_device_wait_timeout_cb, priv, NULL); g_source_attach(source, priv->main_ctx); g_main_loop_run(priv->loop); /* timed out */ if (helper.device == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Stopped waiting for %s after %.0fms", helper.value, g_timer_elapsed(timer, NULL) * 1000.f); return FALSE; } /* success */ fu_console_print(priv->console, /* TRANSLATORS: the device showed up in time */ _("Successfully waited %.0fms for device"), g_timer_elapsed(timer, NULL) * 1000.f); return TRUE; } static gboolean fu_util_quit(FuUtilPrivate *priv, gchar **values, GError **error) { /* success */ return fwupd_client_quit(priv->client, priv->cancellable, error); } static gboolean fu_util_device_test_full(FuUtilPrivate *priv, gchar **values, FuUtilDeviceTestHelper *helper, GError **error) { /* required for interactive devices */ priv->current_operation = FU_UTIL_OPERATION_UPDATE; /* at least one argument required */ if (g_strv_length(values) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } /* get the report metadata */ helper->report_metadata = fwupd_client_get_report_metadata(priv->client, priv->cancellable, error); if (helper->report_metadata == NULL) return FALSE; /* prepare to save the data as JSON */ json_builder_begin_object(helper->builder); /* process all the files */ json_builder_set_member_name(helper->builder, "results"); json_builder_begin_array(helper->builder); for (guint i = 0; values[i] != NULL; i++) { json_builder_begin_object(helper->builder); if (!fu_util_device_test_filename(priv, helper, values[i], error)) return FALSE; json_builder_end_object(helper->builder); } json_builder_end_array(helper->builder); /* dump to screen as JSON format */ json_builder_end_object(helper->builder); if (priv->as_json) { if (!fu_util_print_builder(priv->console, helper->builder, error)) return FALSE; } /* just warning */ if (helper->nr_skipped > 0) { g_autoptr(GString) str = g_string_new(NULL); g_string_append_printf( str, /* TRANSLATORS: device tests can be specific to a CPU type */ ngettext("%u test was skipped", "%u tests were skipped", helper->nr_skipped), helper->nr_skipped); fu_console_print_full(priv->console, FU_CONSOLE_PRINT_FLAG_WARNING, "%s\n", str->str); } /* we need all to pass for a zero return code */ if (helper->nr_missing > 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "%u devices required for %u tests were not found", helper->nr_missing, g_strv_length(values)); return FALSE; } if (helper->nr_success == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "None of the tests were successful"); return FALSE; } /* nag? */ if (!fu_util_perhaps_show_unreported(priv, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_util_device_emulate(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FuUtilDeviceTestHelper) helper = fu_util_device_test_helper_new(); helper->use_emulation = TRUE; priv->flags |= FWUPD_INSTALL_FLAG_ONLY_EMULATED; priv->filter_device_include |= FWUPD_DEVICE_FLAG_EMULATED; return fu_util_device_test_full(priv, values, helper, error); } static gboolean fu_util_device_test(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FuUtilDeviceTestHelper) helper = fu_util_device_test_helper_new(); priv->filter_device_exclude |= FWUPD_DEVICE_FLAG_EMULATED; return fu_util_device_test_full(priv, values, helper, error); } static gboolean fu_util_download(FuUtilPrivate *priv, gchar **values, GError **error) { g_autofree gchar *basename = NULL; g_autoptr(GBytes) blob = NULL; /* one argument required */ if (g_strv_length(values) != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } /* file already exists */ basename = g_path_get_basename(values[0]); if ((priv->flags & FWUPD_INSTALL_FLAG_FORCE) == 0 && g_file_test(basename, G_FILE_TEST_EXISTS)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "%s already exists", basename); return FALSE; } blob = fwupd_client_download_bytes(priv->client, values[0], priv->download_flags, priv->cancellable, error); if (blob == NULL) return FALSE; return g_file_set_contents(basename, g_bytes_get_data(blob, NULL), g_bytes_get_size(blob), error); } static gboolean fu_util_local_install(FuUtilPrivate *priv, gchar **values, GError **error) { const gchar *id; g_autofree gchar *filename = NULL; g_autoptr(FwupdDevice) dev = NULL; /* handle both forms */ if (g_strv_length(values) == 1) { id = FWUPD_DEVICE_ID_ANY; } else if (g_strv_length(values) == 2) { dev = fu_util_get_device_by_id(priv, values[1], error); if (dev == NULL) return FALSE; id = fwupd_device_get_id(dev); } else { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } priv->current_operation = FU_UTIL_OPERATION_INSTALL; /* install with flags chosen by the user */ filename = fu_util_download_if_required(priv, values[0], error); if (filename == NULL) return FALSE; /* detect bitlocker */ if (dev != NULL && !priv->no_safety_check && !priv->assume_yes) { if (!fu_util_prompt_warning_fde(priv->console, dev, error)) return FALSE; } if (!fwupd_client_install(priv->client, id, filename, priv->flags, priv->cancellable, error)) return FALSE; fu_util_display_current_message(priv); /* we don't want to ask anything */ if (priv->no_reboot_check) { g_debug("skipping reboot check"); return TRUE; } /* show reboot if needed */ return fu_util_prompt_complete(priv->console, priv->completion_flags, TRUE, error); } static gboolean fu_util_get_details(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(GPtrArray) array = NULL; g_autoptr(FuUtilNode) root = g_node_new(NULL); /* check args */ if (g_strv_length(values) != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } /* implied, important for get-details on a device not in your system */ priv->show_all = TRUE; array = fwupd_client_get_details(priv->client, values[0], priv->cancellable, error); if (array == NULL) return FALSE; if (priv->as_json) { g_autoptr(JsonBuilder) builder = json_builder_new(); json_builder_begin_object(builder); fwupd_codec_array_to_json(array, "Devices", builder, FWUPD_CODEC_FLAG_TRUSTED); json_builder_end_object(builder); return fu_util_print_builder(priv->console, builder, error); } fu_util_build_device_tree(priv, root, array); fu_util_print_node(priv->console, priv->client, root); return TRUE; } static gboolean fu_util_report_history_for_remote(FuUtilPrivate *priv, GPtrArray *devices, FwupdRemote *remote_filter, FwupdRemote *remote_upload, GError **error) { g_autofree gchar *data = NULL; g_autofree gchar *report_uri = NULL; g_autofree gchar *sig = NULL; g_autofree gchar *uri = NULL; g_autoptr(GHashTable) metadata = NULL; /* convert to JSON */ metadata = fwupd_client_get_report_metadata(priv->client, priv->cancellable, error); if (metadata == NULL) return FALSE; data = fwupd_client_build_report_history(priv->client, devices, remote_filter, metadata, error); if (data == NULL) return FALSE; /* self sign data */ if (priv->sign) { sig = fwupd_client_self_sign(priv->client, data, FWUPD_SELF_SIGN_FLAG_ADD_TIMESTAMP, priv->cancellable, error); if (sig == NULL) return FALSE; } /* ask for permission */ report_uri = fwupd_remote_build_report_uri(remote_upload, error); if (report_uri == NULL) return FALSE; if (!priv->assume_yes && !fwupd_remote_has_flag(remote_upload, FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS)) { fu_console_print_kv(priv->console, _("Target"), report_uri); fu_console_print_kv(priv->console, _("Payload"), data); if (sig != NULL) fu_console_print_kv(priv->console, _("Signature"), sig); if (!fu_console_input_bool(priv->console, TRUE, "%s", _("Proceed with upload?"))) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_PERMISSION_DENIED, "User declined action"); return FALSE; } } /* POST request and parse reply */ uri = fwupd_client_upload_report(priv->client, report_uri, data, sig, FWUPD_CLIENT_UPLOAD_FLAG_NONE, priv->cancellable, error); if (uri == NULL) return FALSE; /* server wanted us to see a message */ if (g_strcmp0(uri, "") != 0) { fu_console_print( priv->console, "%s %s", /* TRANSLATORS: the server sent the user a small message */ _("Update failure is a known issue, visit this URL for more information:"), uri); } /* success */ return TRUE; } static gboolean fu_util_report_history_force(FuUtilPrivate *priv, GError **error) { g_autoptr(FwupdRemote) remote_upload = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(GString) str = g_string_new(NULL); /* get all devices */ devices = fwupd_client_get_history(priv->client, priv->cancellable, error); if (devices == NULL) return FALSE; /* just assume every report goes to this remote */ remote_upload = fwupd_client_get_remote_by_id(priv->client, "lvfs", priv->cancellable, error); if (remote_upload == NULL) return FALSE; if (!fu_util_report_history_for_remote(priv, devices, NULL, /* no filter */ remote_upload, error)) return FALSE; /* mark each device as reported */ for (guint i = 0; i < devices->len; i++) { FwupdDevice *device = g_ptr_array_index(devices, i); g_debug("setting flag on %s", fwupd_device_get_id(device)); if (!fwupd_client_modify_device(priv->client, fwupd_device_get_id(device), "Flags", "reported", priv->cancellable, error)) return FALSE; } /* success */ g_string_append_printf(str, /* TRANSLATORS: success message -- where the user has uploaded * success and/or failure reports to the remote server */ ngettext("Successfully uploaded %u report", "Successfully uploaded %u reports", devices->len), devices->len); fu_console_print_literal(priv->console, str->str); return TRUE; } static gboolean fu_util_report_export(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(GHashTable) metadata = NULL; g_autoptr(GPtrArray) devices_filtered = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_autoptr(GPtrArray) devices = NULL; /* get all devices from the history database, then filter them and export to JSON */ devices = fwupd_client_get_history(priv->client, priv->cancellable, error); if (devices == NULL) return FALSE; g_debug("%u devices with history", devices->len); for (guint i = 0; i < devices->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices, i); gboolean dev_skip_byid = TRUE; /* only process particular DEVICE-ID or GUID if specified */ for (guint idx = 0; idx < g_strv_length(values); idx++) { const gchar *tmpid = values[idx]; const gchar *device_id = fwupd_device_get_id(dev); if (fwupd_device_has_guid(dev, tmpid) || g_strcmp0(device_id, tmpid) == 0) { dev_skip_byid = FALSE; break; } } if (g_strv_length(values) > 0 && dev_skip_byid) continue; /* filter, if not forcing */ if (!fwupd_device_match_flags(dev, priv->filter_device_include, priv->filter_device_exclude)) continue; if ((priv->flags & FWUPD_INSTALL_FLAG_FORCE) == 0) { if (fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_REPORTED)) { g_debug("%s has already been reported", fwupd_device_get_id(dev)); continue; } } /* only send success and failure */ if (fwupd_device_get_update_state(dev) != FWUPD_UPDATE_STATE_FAILED && fwupd_device_get_update_state(dev) != FWUPD_UPDATE_STATE_SUCCESS) { g_debug("ignoring %s with UpdateState %s", fwupd_device_get_id(dev), fwupd_update_state_to_string(fwupd_device_get_update_state(dev))); continue; } g_ptr_array_add(devices_filtered, g_object_ref(dev)); } /* nothing to report, but try harder with --force */ if (devices_filtered->len == 0 && (priv->flags & FWUPD_INSTALL_FLAG_FORCE) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "No reports require uploading"); return FALSE; } /* get metadata */ metadata = fwupd_client_get_report_metadata(priv->client, priv->cancellable, error); if (metadata == NULL) return FALSE; /* write each device report as a new file */ for (guint i = 0; i < devices->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices, i); g_autofree gchar *data = NULL; g_autofree gchar *filename = NULL; g_autoptr(FuFirmware) archive = fu_archive_firmware_new(); g_autoptr(FuFirmware) payload_img = NULL; g_autoptr(GBytes) payload_blob = NULL; g_autoptr(GFile) file = NULL; g_autoptr(GPtrArray) devices_tmp = g_ptr_array_new(); /* convert single device to JSON */ g_ptr_array_add(devices_tmp, dev); data = fwupd_client_build_report_history(priv->client, devices, NULL, /* remote */ metadata, error); if (data == NULL) return FALSE; payload_blob = g_bytes_new(data, strlen(data)); payload_img = fu_firmware_new_from_bytes(payload_blob); fu_firmware_set_id(payload_img, "report.json"); fu_firmware_add_image(archive, payload_img); /* self sign data */ if (priv->sign) { g_autofree gchar *sig = NULL; g_autoptr(FuFirmware) sig_img = NULL; g_autoptr(GBytes) sig_blob = NULL; sig = fwupd_client_self_sign(priv->client, data, FWUPD_SELF_SIGN_FLAG_ADD_TIMESTAMP, priv->cancellable, error); if (sig == NULL) return FALSE; sig_blob = g_bytes_new(sig, strlen(sig)); sig_img = fu_firmware_new_from_bytes(sig_blob); fu_firmware_set_id(sig_img, "report.json.p7c"); fu_firmware_add_image(archive, sig_img); } /* save to local file */ fu_archive_firmware_set_format(FU_ARCHIVE_FIRMWARE(archive), FU_ARCHIVE_FORMAT_ZIP); fu_archive_firmware_set_compression(FU_ARCHIVE_FIRMWARE(archive), FU_ARCHIVE_COMPRESSION_GZIP); filename = g_strdup_printf("%s.fwupdreport", fwupd_device_get_id(dev)); file = g_file_new_for_path(filename); if (!fu_firmware_write_file(archive, file, error)) return FALSE; /* TRANSLATORS: key for a offline report filename */ fu_console_print_kv(priv->console, _("Saved report"), filename); } /* success */ return TRUE; } static gboolean fu_util_report_history_full(FuUtilPrivate *priv, gboolean only_automatic_reports, GError **error) { guint cnt = 0; g_autoptr(GPtrArray) devices = NULL; g_autoptr(GPtrArray) remotes = NULL; /* get all devices from the history database, then filter them, * adding to a hash map of report-ids */ devices = fwupd_client_get_history(priv->client, priv->cancellable, error); if (devices == NULL) return FALSE; g_debug("%u devices with history", devices->len); /* ignore the previous reported flag */ if (priv->flags & FWUPD_INSTALL_FLAG_FORCE) { for (guint i = 0; i < devices->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices, i); fwupd_device_remove_flag(dev, FWUPD_DEVICE_FLAG_REPORTED); } } /* needs an extra action, show something to the user */ for (guint i = 0; i < devices->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices, i); if (fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION)) { g_autofree gchar *cmd = g_strdup_printf("%s activate", g_get_prgname()); fu_console_print( priv->console, /* TRANSLATORS: %1 is a device name, e.g. "ThinkPad Universal * ThunderBolt 4 Dock" and %2 is "fwupdmgr activate" */ _("%s is pending activation; use %s to complete the update."), fwupd_device_get_name(dev), cmd); } } /* get all remotes */ remotes = fwupd_client_get_remotes(priv->client, priv->cancellable, error); if (remotes == NULL) return FALSE; for (guint i = 0; i < remotes->len; i++) { FwupdRemote *remote = g_ptr_array_index(remotes, i); g_autoptr(GError) error_local = NULL; /* filter this so we can use it from fwupd-refresh */ if (only_automatic_reports && !fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS)) { g_debug("%s has no AutomaticReports set", fwupd_remote_get_id(remote)); continue; } /* try to upload */ if (!fu_util_report_history_for_remote(priv, devices, remote, /* filter */ remote, /* upload */ &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) continue; g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } /* keep track to make sure *something* worked */ cnt += 1; } /* nothing to report, but try harder with --force */ if (cnt == 0) { if (!only_automatic_reports && priv->flags & FWUPD_INSTALL_FLAG_FORCE) return fu_util_report_history_force(priv, error); g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "No reports require uploading"); return FALSE; } /* mark each device as reported */ for (guint i = 0; i < devices->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices, i); g_debug("setting flag on %s", fwupd_device_get_id(dev)); if (!fwupd_client_modify_device(priv->client, fwupd_device_get_id(dev), "Flags", "reported", priv->cancellable, error)) return FALSE; } /* TRANSLATORS: where the user has uploaded success and/or failure report to the server */ fu_console_print_literal(priv->console, "Successfully uploaded report"); return TRUE; } static gboolean fu_util_report_history(FuUtilPrivate *priv, gchar **values, GError **error) { if (values != NULL && g_strv_length(values) != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } return fu_util_report_history_full(priv, FALSE, error); } static gboolean fu_util_get_history(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(GPtrArray) devices = NULL; g_autoptr(FuUtilNode) root = g_node_new(NULL); /* get all devices from the history database */ devices = fwupd_client_get_history(priv->client, priv->cancellable, error); if (devices == NULL) return FALSE; /* not for human consumption */ if (priv->as_json) { g_autoptr(JsonBuilder) builder = json_builder_new(); json_builder_begin_object(builder); fwupd_codec_array_to_json(devices, "Devices", builder, FWUPD_CODEC_FLAG_TRUSTED); json_builder_end_object(builder); return fu_util_print_builder(priv->console, builder, error); } /* show each device */ for (guint i = 0; i < devices->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices, i); FwupdRelease *rel; FuUtilNode *child; if (!fwupd_device_match_flags(dev, priv->filter_device_include, priv->filter_device_exclude)) continue; child = g_node_append_data(root, g_object_ref(dev)); rel = fwupd_device_get_release_default(dev); if (rel == NULL) continue; g_node_append_data(child, g_object_ref(rel)); } fu_util_print_node(priv->console, priv->client, root); return TRUE; } static FwupdDevice * fu_util_get_device_by_id(FuUtilPrivate *priv, const gchar *id, GError **error) { if (fwupd_guid_is_valid(id)) { g_autoptr(GPtrArray) devices = NULL; devices = fwupd_client_get_devices_by_guid(priv->client, id, priv->cancellable, error); if (devices == NULL) return NULL; return fu_util_prompt_for_device(priv, devices, error); } /* did this look like a GUID? */ for (guint i = 0; id[i] != '\0'; i++) { if (id[i] == '-') { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return NULL; } } return fwupd_client_get_device_by_id(priv->client, id, priv->cancellable, error); } static FwupdDevice * fu_util_get_device_or_prompt(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(GPtrArray) devices = NULL; /* get device to use */ if (g_strv_length(values) >= 1) { if (g_strv_length(values) > 1) { for (guint i = 1; i < g_strv_length(values); i++) g_debug("ignoring extra input %s", values[i]); } return fu_util_get_device_by_id(priv, values[0], error); } if (priv->as_json) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "device ID required"); return NULL; } /* get all devices from daemon */ devices = fwupd_client_get_devices(priv->client, priv->cancellable, error); if (devices == NULL) return NULL; return fu_util_prompt_for_device(priv, devices, error); } static FwupdRelease * fu_util_get_release_for_device_version(FuUtilPrivate *priv, FwupdDevice *device, const gchar *version, GError **error) { g_autoptr(GPtrArray) releases = NULL; /* get all releases */ releases = fwupd_client_get_releases(priv->client, fwupd_device_get_id(device), priv->cancellable, error); if (releases == NULL) return NULL; /* find using vercmp */ for (guint j = 0; j < releases->len; j++) { FwupdRelease *release = g_ptr_array_index(releases, j); if (fu_version_compare(fwupd_release_get_version(release), version, fwupd_device_get_version_format(device)) == 0) { return g_object_ref(release); } } /* did not find */ g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Unable to locate release %s for %s", version, fwupd_device_get_name(device)); return NULL; } static gboolean fu_util_clear_results(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FwupdDevice) dev = NULL; dev = fu_util_get_device_or_prompt(priv, values, error); if (dev == NULL) return FALSE; return fwupd_client_clear_results(priv->client, fwupd_device_get_id(dev), priv->cancellable, error); } static gboolean fu_util_verify_update(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FwupdDevice) dev = NULL; priv->filter_device_include |= FWUPD_DEVICE_FLAG_CAN_VERIFY; dev = fu_util_get_device_or_prompt(priv, values, error); if (dev == NULL) return FALSE; if (!fwupd_client_verify_update(priv->client, fwupd_device_get_id(dev), priv->cancellable, error)) { g_prefix_error(error, "failed to verify update %s: ", fwupd_device_get_name(dev)); return FALSE; } /* TRANSLATORS: success message when user refreshes device checksums */ fu_console_print_literal(priv->console, _("Successfully updated device checksums")); return TRUE; } static gboolean fu_util_download_metadata_enable_lvfs(FuUtilPrivate *priv, GError **error) { g_autoptr(FwupdRemote) remote = NULL; /* is the LVFS available but disabled? */ remote = fwupd_client_get_remote_by_id(priv->client, "lvfs", priv->cancellable, error); if (remote == NULL) return TRUE; fu_console_print_literal( priv->console, /* TRANSLATORS: explain why no metadata available */ _("No remotes are currently enabled so no metadata is available.")); fu_console_print_literal( priv->console, /* TRANSLATORS: explain why no metadata available */ _("Metadata can be obtained from the Linux Vendor Firmware Service.")); /* TRANSLATORS: Turn on the remote */ if (!fu_console_input_bool(priv->console, TRUE, "%s", _("Enable this remote?"))) return TRUE; if (!fwupd_client_modify_remote(priv->client, fwupd_remote_get_id(remote), "Enabled", "true", priv->cancellable, error)) return FALSE; if (!fu_util_modify_remote_warning(priv->console, remote, priv->assume_yes, error)) return FALSE; /* refresh the newly-enabled remote */ return fwupd_client_refresh_remote(priv->client, remote, priv->download_flags, priv->cancellable, error); } static gboolean fu_util_check_oldest_remote(FuUtilPrivate *priv, guint64 *age_oldest, GError **error) { g_autoptr(GPtrArray) remotes = NULL; gboolean checked = FALSE; /* get the age of the oldest enabled remotes */ remotes = fwupd_client_get_remotes(priv->client, priv->cancellable, error); if (remotes == NULL) return FALSE; for (guint i = 0; i < remotes->len; i++) { FwupdRemote *remote = g_ptr_array_index(remotes, i); if (!fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_ENABLED)) continue; if (fwupd_remote_get_kind(remote) != FWUPD_REMOTE_KIND_DOWNLOAD) continue; checked = TRUE; if (!fwupd_remote_needs_refresh(remote)) continue; g_debug("%s is age %u", fwupd_remote_get_id(remote), (guint)fwupd_remote_get_age(remote)); if (fwupd_remote_get_age(remote) > *age_oldest) *age_oldest = fwupd_remote_get_age(remote); } if (!checked) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, /* TRANSLATORS: error message for a user who ran fwupdmgr refresh recently but no remotes */ "No remotes enabled."); return FALSE; } return TRUE; } static gboolean fu_util_download_metadata(FuUtilPrivate *priv, GError **error) { gboolean download_remote_enabled = FALSE; guint devices_supported_cnt = 0; guint devices_updatable_cnt = 0; guint refresh_cnt = 0; g_autoptr(GPtrArray) devs = NULL; g_autoptr(GPtrArray) remotes = NULL; g_autoptr(GString) str = g_string_new(NULL); g_autoptr(GError) error_local = NULL; remotes = fwupd_client_get_remotes(priv->client, priv->cancellable, error); if (remotes == NULL) return FALSE; for (guint i = 0; i < remotes->len; i++) { FwupdRemote *remote = g_ptr_array_index(remotes, i); if (!fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_ENABLED)) continue; if (fwupd_remote_get_kind(remote) != FWUPD_REMOTE_KIND_DOWNLOAD) continue; download_remote_enabled = TRUE; if ((priv->flags & FWUPD_INSTALL_FLAG_FORCE) == 0 && !fwupd_remote_needs_refresh(remote)) { g_debug("skipping as remote %s age is %us", fwupd_remote_get_id(remote), (guint)fwupd_remote_get_age(remote)); continue; } if (!priv->as_json) fu_console_print(priv->console, "%s %s", _("Updating"), fwupd_remote_get_id(remote)); if (!fwupd_client_refresh_remote(priv->client, remote, priv->download_flags, priv->cancellable, error)) return FALSE; refresh_cnt++; } /* no web remote is declared; try to enable LVFS */ if (!download_remote_enabled) { /* we don't want to ask anything */ if (priv->no_remote_check) { g_debug("skipping remote check"); return TRUE; } if (!fu_util_download_metadata_enable_lvfs(priv, error)) return FALSE; } /* metadata refreshed recently */ if ((priv->flags & FWUPD_INSTALL_FLAG_FORCE) == 0 && refresh_cnt == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, /* TRANSLATORS: error message for a user who ran fwupdmgr * refresh recently -- %1 is '--force' */ _("Metadata is up to date; use %s to refresh again."), "--force"); return FALSE; } if (priv->as_json) return TRUE; /* get devices from daemon */ devs = fwupd_client_get_devices(priv->client, priv->cancellable, error); if (devs == NULL) return FALSE; /* get results */ for (guint i = 0; i < devs->len; i++) { FwupdDevice *dev = g_ptr_array_index(devs, i); if (fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_SUPPORTED)) devices_supported_cnt++; if (fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE)) devices_updatable_cnt++; } /* TRANSLATORS: success message -- where 'metadata' is information * about available firmware on the remote server */ g_string_append(str, _("Successfully downloaded new metadata: ")); g_string_append_printf(str, /* TRANSLATORS: how many local devices can expect updates now */ ngettext("Updates have been published for %u local device", "Updates have been published for %u of %u local devices", devices_supported_cnt), devices_supported_cnt, devices_updatable_cnt); fu_console_print_literal(priv->console, str->str); /* auto-upload any reports */ if (!fu_util_report_history_full(priv, TRUE, &error_local)) { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } g_debug("failed to auto-upload reports: %s", error_local->message); } /* success */ return TRUE; } static gboolean fu_util_refresh(FuUtilPrivate *priv, gchar **values, GError **error) { if (g_strv_length(values) == 0) return fu_util_download_metadata(priv, error); if (g_strv_length(values) != 3) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } /* open file */ if (!fwupd_client_update_metadata(priv->client, values[2], values[0], values[1], priv->cancellable, error)) return FALSE; if (priv->as_json) return TRUE; /* TRANSLATORS: success message -- the user can do this by-hand too */ fu_console_print_literal(priv->console, _("Successfully refreshed metadata manually")); return TRUE; } static gboolean fu_util_get_results(FuUtilPrivate *priv, gchar **values, GError **error) { g_autofree gchar *tmp = NULL; g_autoptr(FwupdDevice) dev = NULL; g_autoptr(FwupdDevice) rel = NULL; dev = fu_util_get_device_or_prompt(priv, values, error); if (dev == NULL) return FALSE; rel = fwupd_client_get_results(priv->client, fwupd_device_get_id(dev), priv->cancellable, error); if (rel == NULL) return FALSE; if (priv->as_json) { g_autoptr(JsonBuilder) builder = json_builder_new(); json_builder_begin_object(builder); fwupd_codec_to_json(FWUPD_CODEC(rel), builder, FWUPD_CODEC_FLAG_TRUSTED); json_builder_end_object(builder); return fu_util_print_builder(priv->console, builder, error); } tmp = fu_util_device_to_string(priv->client, rel, 0); fu_console_print_literal(priv->console, tmp); return TRUE; } static gboolean fu_util_get_releases(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FwupdDevice) dev = NULL; g_autoptr(GPtrArray) rels = NULL; priv->filter_device_include |= FWUPD_DEVICE_FLAG_SUPPORTED; dev = fu_util_get_device_or_prompt(priv, values, error); if (dev == NULL) return FALSE; /* get the releases for this device */ rels = fwupd_client_get_releases(priv->client, fwupd_device_get_id(dev), priv->cancellable, error); if (rels == NULL) return FALSE; /* not for human consumption */ if (priv->as_json) return fu_util_get_releases_as_json(priv, rels, error); if (rels->len == 0) { /* TRANSLATORS: no repositories to download from */ fu_console_print_literal(priv->console, _("No releases available")); return TRUE; } if (g_getenv("FWUPD_VERBOSE") != NULL) { for (guint i = 0; i < rels->len; i++) { FwupdRelease *rel = g_ptr_array_index(rels, i); g_autofree gchar *tmp = NULL; if (!fwupd_release_match_flags(rel, priv->filter_release_include, priv->filter_release_exclude)) continue; tmp = fwupd_codec_to_string(FWUPD_CODEC(rel)); fu_console_print_literal(priv->console, tmp); } } else { g_autoptr(FuUtilNode) root = g_node_new(NULL); for (guint i = 0; i < rels->len; i++) { FwupdRelease *rel = g_ptr_array_index(rels, i); if (!fwupd_release_match_flags(rel, priv->filter_release_include, priv->filter_release_exclude)) continue; g_node_append_data(root, g_object_ref(rel)); } fu_util_print_node(priv->console, priv->client, root); } return TRUE; } static FwupdRelease * fu_util_prompt_for_release(FuUtilPrivate *priv, GPtrArray *rels_unfiltered, GError **error) { FwupdRelease *rel; guint idx; g_autoptr(GPtrArray) rels = NULL; /* filter */ rels = fwupd_release_array_filter_flags(rels_unfiltered, priv->filter_release_include, priv->filter_release_exclude, error); if (rels == NULL) return NULL; /* exactly one */ if (rels->len == 1) { rel = g_ptr_array_index(rels, 0); return g_object_ref(rel); } /* TRANSLATORS: this is to abort the interactive prompt */ fu_console_print(priv->console, "0.\t%s", _("Cancel")); for (guint i = 0; i < rels->len; i++) { FwupdRelease *rel_tmp = g_ptr_array_index(rels, i); fu_console_print(priv->console, "%u.\t%s", i + 1, fwupd_release_get_version(rel_tmp)); } /* TRANSLATORS: get interactive prompt */ idx = fu_console_input_uint(priv->console, rels->len, "%s", _("Choose release")); if (idx == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "Request canceled"); return NULL; } rel = g_ptr_array_index(rels, idx - 1); return g_object_ref(rel); } static gboolean fu_util_verify(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FwupdDevice) dev = NULL; priv->filter_device_include |= FWUPD_DEVICE_FLAG_CAN_VERIFY; dev = fu_util_get_device_or_prompt(priv, values, error); if (dev == NULL) return FALSE; if (!fwupd_client_verify(priv->client, fwupd_device_get_id(dev), priv->cancellable, error)) { g_prefix_error(error, "failed to verify %s: ", fwupd_device_get_name(dev)); return FALSE; } /* TRANSLATORS: success message when user verified device checksums */ fu_console_print_literal(priv->console, _("Successfully verified device checksums")); return TRUE; } static gboolean fu_util_unlock(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FwupdDevice) dev = NULL; priv->filter_device_include |= FWUPD_DEVICE_FLAG_LOCKED; dev = fu_util_get_device_or_prompt(priv, values, error); if (dev == NULL) return FALSE; if (!fwupd_client_unlock(priv->client, fwupd_device_get_id(dev), priv->cancellable, error)) return FALSE; /* check flags after unlocking in case the operation changes them */ if (fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN)) priv->completion_flags |= FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN; if (fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_NEEDS_REBOOT)) priv->completion_flags |= FWUPD_DEVICE_FLAG_NEEDS_REBOOT; return fu_util_prompt_complete(priv->console, priv->completion_flags, TRUE, error); } static gboolean fu_util_perhaps_refresh_remotes(FuUtilPrivate *priv, GError **error) { guint64 age_oldest = 0; const guint64 age_limit_days = 30; /* we don't want to ask anything */ if (priv->no_metadata_check || priv->as_json) { g_debug("skipping metadata check"); return TRUE; } if (!fu_util_check_oldest_remote(priv, &age_oldest, NULL)) return TRUE; /* metadata is new enough */ if (age_oldest < 60 * 60 * 24 * age_limit_days) return TRUE; /* ask for permission */ if (!priv->assume_yes) { fu_console_print( priv->console, /* TRANSLATORS: the metadata is very out of date; %u is a number > 1 */ ngettext("Firmware metadata has not been updated for %u" " day and may not be up to date.", "Firmware metadata has not been updated for %u" " days and may not be up to date.", (gint)age_limit_days), (guint)age_limit_days); if (!fu_console_input_bool(priv->console, FALSE, "%s (%s)", /* TRANSLATORS: ask if we can update metadata */ _("Update now?"), /* TRANSLATORS: metadata is downloaded */ _("Requires internet connection"))) return TRUE; } /* downloads new metadata */ return fu_util_download_metadata(priv, error); } static gboolean fu_util_get_updates_as_json(FuUtilPrivate *priv, GPtrArray *devices, GError **error) { g_autoptr(JsonBuilder) builder = json_builder_new(); json_builder_begin_object(builder); json_builder_set_member_name(builder, "Devices"); json_builder_begin_array(builder); for (guint i = 0; i < devices->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices, i); g_autoptr(GPtrArray) rels = NULL; g_autoptr(GError) error_local = NULL; /* not going to have results, so save a D-Bus round-trip */ if (!fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_SUPPORTED)) continue; /* get the releases for this device and filter for validity */ rels = fwupd_client_get_upgrades(priv->client, fwupd_device_get_id(dev), priv->cancellable, &error_local); if (rels == NULL) { g_debug("no upgrades: %s", error_local->message); continue; } for (guint j = 0; j < rels->len; j++) { FwupdRelease *rel = g_ptr_array_index(rels, j); if (!fwupd_release_match_flags(rel, priv->filter_release_include, priv->filter_release_exclude)) continue; fwupd_device_add_release(dev, rel); } /* add to builder */ json_builder_begin_object(builder); fwupd_codec_to_json(FWUPD_CODEC(dev), builder, FWUPD_CODEC_FLAG_TRUSTED); json_builder_end_object(builder); } json_builder_end_array(builder); json_builder_end_object(builder); return fu_util_print_builder(priv->console, builder, error); } static gboolean fu_util_get_updates(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(GPtrArray) devices = NULL; gboolean supported = FALSE; g_autoptr(FuUtilNode) root = g_node_new(NULL); g_autoptr(GPtrArray) devices_no_support = g_ptr_array_new(); g_autoptr(GPtrArray) devices_no_upgrades = g_ptr_array_new(); /* are the remotes very old */ if (!fu_util_perhaps_refresh_remotes(priv, error)) return FALSE; /* handle both forms */ if (g_strv_length(values) == 0) { devices = fwupd_client_get_devices(priv->client, priv->cancellable, error); if (devices == NULL) return FALSE; } else if (g_strv_length(values) == 1) { FwupdDevice *device = fu_util_get_device_by_id(priv, values[0], error); if (device == NULL) return FALSE; devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_ptr_array_add(devices, device); } else { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } g_ptr_array_sort(devices, fu_util_sort_devices_by_flags_cb); /* not for human consumption */ if (priv->as_json) return fu_util_get_updates_as_json(priv, devices, error); for (guint i = 0; i < devices->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices, i); g_autoptr(GPtrArray) rels = NULL; g_autoptr(GError) error_local = NULL; FuUtilNode *child; /* not going to have results, so save a D-Bus round-trip */ if (!fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE) && !fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN)) continue; if (!fwupd_device_match_flags(dev, priv->filter_device_include, priv->filter_device_exclude)) continue; if (!fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_SUPPORTED)) { g_ptr_array_add(devices_no_support, dev); continue; } supported = TRUE; /* get the releases for this device and filter for validity */ rels = fwupd_client_get_upgrades(priv->client, fwupd_device_get_id(dev), priv->cancellable, &error_local); if (rels == NULL) { g_ptr_array_add(devices_no_upgrades, dev); /* discard the actual reason from user, but leave for debugging */ g_debug("%s", error_local->message); continue; } child = g_node_append_data(root, g_object_ref(dev)); /* add all releases */ for (guint j = 0; j < rels->len; j++) { FwupdRelease *rel = g_ptr_array_index(rels, j); if (!fwupd_release_match_flags(rel, priv->filter_release_include, priv->filter_release_exclude)) continue; g_node_append_data(child, g_object_ref(rel)); } } /* devices that have no updates available for whatever reason */ if (devices_no_support->len > 0) { fu_console_print_literal(priv->console, /* TRANSLATORS: message letting the user know no device * upgrade available due to missing on LVFS */ _("Devices with no available firmware updates: ")); for (guint i = 0; i < devices_no_support->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices_no_support, i); fu_console_print(priv->console, " • %s", fwupd_device_get_name(dev)); } } if (devices_no_upgrades->len > 0) { fu_console_print_literal( priv->console, /* TRANSLATORS: message letting the user know no device upgrade available */ _("Devices with the latest available firmware version:")); for (guint i = 0; i < devices_no_upgrades->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices_no_upgrades, i); fu_console_print(priv->console, " • %s", fwupd_device_get_name(dev)); } } /* nag? */ if (!fu_util_perhaps_show_unreported(priv, error)) return FALSE; /* no devices supported by LVFS or all are filtered */ if (!supported) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, /* TRANSLATORS: this is an error string */ _("No updatable devices")); return FALSE; } /* no updates available */ if (g_node_n_nodes(root, G_TRAVERSE_ALL) <= 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, /* TRANSLATORS: this is an error string */ _("No updates available")); return FALSE; } fu_util_print_node(priv->console, priv->client, root); /* success */ return TRUE; } static gboolean fu_util_get_remotes(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FuUtilNode) root = g_node_new(NULL); g_autoptr(GPtrArray) remotes = NULL; remotes = fwupd_client_get_remotes(priv->client, priv->cancellable, error); if (remotes == NULL) return FALSE; if (priv->as_json) { g_autoptr(JsonBuilder) builder = json_builder_new(); json_builder_begin_object(builder); fwupd_codec_array_to_json(remotes, "Remotes", builder, FWUPD_CODEC_FLAG_TRUSTED); json_builder_end_object(builder); return fu_util_print_builder(priv->console, builder, error); } if (remotes->len == 0) { /* TRANSLATORS: no repositories to download from */ fu_console_print_literal(priv->console, _("No remotes available")); return TRUE; } for (guint i = 0; i < remotes->len; i++) { FwupdRemote *remote_tmp = g_ptr_array_index(remotes, i); g_node_append_data(root, g_object_ref(remote_tmp)); } fu_util_print_node(priv->console, priv->client, root); return TRUE; } static FwupdRelease * fu_util_get_release_with_tag(FuUtilPrivate *priv, FwupdDevice *dev, const gchar *host_bkc, GError **error) { g_autoptr(GPtrArray) rels = NULL; g_auto(GStrv) host_bkcs = g_strsplit(host_bkc, ",", -1); /* find the newest release that matches */ rels = fwupd_client_get_releases(priv->client, fwupd_device_get_id(dev), priv->cancellable, error); if (rels == NULL) return NULL; for (guint i = 0; i < rels->len; i++) { FwupdRelease *rel = g_ptr_array_index(rels, i); if (!fwupd_release_match_flags(rel, priv->filter_release_include, priv->filter_release_exclude)) continue; for (guint j = 0; host_bkcs[j] != NULL; j++) { if (fwupd_release_has_tag(rel, host_bkcs[j])) return g_object_ref(rel); } } /* no match */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no matching releases for device"); return NULL; } static FwupdRelease * fu_util_get_release_with_branch(FuUtilPrivate *priv, FwupdDevice *dev, const gchar *branch, GError **error) { g_autoptr(GPtrArray) rels = NULL; /* find the newest release that matches */ rels = fwupd_client_get_releases(priv->client, fwupd_device_get_id(dev), priv->cancellable, error); if (rels == NULL) return NULL; for (guint i = 0; i < rels->len; i++) { FwupdRelease *rel = g_ptr_array_index(rels, i); if (!fwupd_release_match_flags(rel, priv->filter_release_include, priv->filter_release_exclude)) continue; if (g_strcmp0(branch, fwupd_release_get_branch(rel)) == 0) return g_object_ref(rel); } /* no match */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "no matching releases for device"); return NULL; } static gboolean fu_util_prompt_warning_bkc(FuUtilPrivate *priv, FwupdDevice *dev, FwupdRelease *rel, GError **error) { const gchar *host_bkc = fwupd_client_get_host_bkc(priv->client); g_autofree gchar *cmd = g_strdup_printf("%s sync", g_get_prgname()); g_autoptr(FwupdRelease) rel_bkc = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(GString) str = g_string_new(NULL); /* nothing to do */ if (host_bkc == NULL) return TRUE; /* get the release that corresponds with the host BKC */ rel_bkc = fu_util_get_release_with_tag(priv, dev, host_bkc, &error_local); if (rel_bkc == NULL) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) { g_debug("ignoring %s: %s", fwupd_device_get_id(dev), error_local->message); return TRUE; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } /* device is already on a different release */ if (g_strcmp0(fwupd_device_get_version(dev), fwupd_release_get_version(rel)) != 0) return TRUE; /* TRANSLATORS: BKC is the industry name for the best known configuration and is a set * of firmware that works together */ g_string_append_printf(str, _("Your system is set up to the BKC of %s."), host_bkc); g_string_append(str, "\n\n"); g_string_append_printf( str, /* TRANSLATORS: %1 is the current device version number, and %2 is the command name, e.g. `fwupdmgr sync` */ _("This device will be reverted back to %s when the %s command is performed."), fwupd_release_get_version(rel), cmd); fu_console_box( priv->console, /* TRANSLATORS: the best known configuration is a set of software that we know works * well together. In the OEM and ODM industries it is often called a BKC */ _("Deviate from the best known configuration?"), str->str, 80); /* TRANSLATORS: prompt to apply the update */ if (!fu_console_input_bool(priv->console, TRUE, "%s", _("Perform operation?"))) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "Request canceled"); return FALSE; } /* success */ return TRUE; } static gboolean fu_util_prompt_warning_composite(FuUtilPrivate *priv, FwupdDevice *dev, FwupdRelease *rel, GError **error) { const gchar *rel_csum; g_autoptr(GPtrArray) devices = NULL; /* get the default checksum */ rel_csum = fwupd_checksum_get_best(fwupd_release_get_checksums(rel)); if (rel_csum == NULL) { g_debug("no checksum for release!"); return TRUE; } /* find other devices matching the composite ID and the release checksum */ devices = fwupd_client_get_devices(priv->client, priv->cancellable, error); if (devices == NULL) return FALSE; for (guint i = 0; i < devices->len; i++) { FwupdDevice *dev_tmp = g_ptr_array_index(devices, i); g_autoptr(GError) error_local = NULL; g_autoptr(GPtrArray) rels = NULL; /* not the parent device */ if (g_strcmp0(fwupd_device_get_id(dev), fwupd_device_get_id(dev_tmp)) == 0) continue; /* not the same composite device */ if (g_strcmp0(fwupd_device_get_composite_id(dev), fwupd_device_get_composite_id(dev_tmp)) != 0) continue; /* get releases */ if (!fwupd_device_has_flag(dev_tmp, FWUPD_DEVICE_FLAG_UPDATABLE)) continue; rels = fwupd_client_get_releases(priv->client, fwupd_device_get_id(dev_tmp), priv->cancellable, &error_local); if (rels == NULL) { g_debug("ignoring: %s", error_local->message); continue; } /* do any releases match this checksum */ for (guint j = 0; j < rels->len; j++) { FwupdRelease *rel_tmp = g_ptr_array_index(rels, j); if (fwupd_release_has_checksum(rel_tmp, rel_csum)) { g_autofree gchar *title = g_strdup_printf("%s %s", fwupd_client_get_host_product(priv->client), fwupd_client_get_host_product(priv->client)); if (!fu_util_prompt_warning(priv->console, dev_tmp, rel_tmp, title, error)) return FALSE; break; } } } /* success */ return TRUE; } static gboolean fu_util_update_device_with_release(FuUtilPrivate *priv, FwupdDevice *dev, FwupdRelease *rel, GError **error) { if (!fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE)) { const gchar *name = fwupd_device_get_name(dev); g_autofree gchar *str = NULL; /* TRANSLATORS: the device has a reason it can't update, e.g. laptop lid closed */ str = g_strdup_printf(_("%s is not currently updatable"), name); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "%s: %s", str, fwupd_device_get_update_error(dev)); return FALSE; } if (!priv->as_json && !priv->no_safety_check && !priv->assume_yes) { const gchar *title = fwupd_client_get_host_product(priv->client); if (!fu_util_prompt_warning(priv->console, dev, rel, title, error)) return FALSE; if (!fu_util_prompt_warning_fde(priv->console, dev, error)) return FALSE; if (!fu_util_prompt_warning_composite(priv, dev, rel, error)) return FALSE; if (!fu_util_prompt_warning_bkc(priv, dev, rel, error)) return FALSE; } return fwupd_client_install_release(priv->client, dev, rel, priv->flags, priv->download_flags, priv->cancellable, error); } static gboolean fu_util_maybe_send_reports(FuUtilPrivate *priv, FwupdRelease *rel, GError **error) { g_autoptr(FwupdRemote) remote = NULL; g_autoptr(GError) error_local = NULL; if (fwupd_release_get_remote_id(rel) == NULL) { g_debug("not sending reports, no remote"); return TRUE; } remote = fwupd_client_get_remote_by_id(priv->client, fwupd_release_get_remote_id(rel), priv->cancellable, error); if (remote == NULL) return FALSE; if (fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_AUTOMATIC_REPORTS)) { if (!fu_util_report_history(priv, NULL, &error_local)) if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) g_warning("%s", error_local->message); } return TRUE; } static gboolean fu_util_update(FuUtilPrivate *priv, gchar **values, GError **error) { gboolean supported = FALSE; g_autoptr(GPtrArray) devices = NULL; g_autoptr(GPtrArray) devices_latest = g_ptr_array_new(); g_autoptr(GPtrArray) devices_pending = g_ptr_array_new(); g_autoptr(GPtrArray) devices_unsupported = g_ptr_array_new(); if (priv->flags & FWUPD_INSTALL_FLAG_ALLOW_OLDER) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "--allow-older is not supported for this command"); return FALSE; } if (priv->flags & FWUPD_INSTALL_FLAG_ALLOW_REINSTALL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "--allow-reinstall is not supported for this command"); return FALSE; } /* DEVICE-ID and GUID are acceptable args to update */ for (guint idx = 0; idx < g_strv_length(values); idx++) { if (!fwupd_guid_is_valid(values[idx]) && !fwupd_device_id_is_valid(values[idx])) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "'%s' is not a valid GUID nor DEVICE-ID", values[idx]); return FALSE; } } /* get devices from daemon */ devices = fwupd_client_get_devices(priv->client, priv->cancellable, error); if (devices == NULL) return FALSE; priv->current_operation = FU_UTIL_OPERATION_UPDATE; g_ptr_array_sort(devices, fu_util_sort_devices_by_flags_cb); for (guint i = 0; i < devices->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices, i); const gchar *device_id = fwupd_device_get_id(dev); g_autoptr(FwupdRelease) rel = NULL; g_autoptr(GPtrArray) rels = NULL; g_autoptr(GError) error_install = NULL; g_autoptr(GError) error_report = NULL; gboolean dev_skip_byid = TRUE; gboolean ret; /* not going to have results, so save a D-Bus round-trip */ if (!fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE) && !fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN)) continue; if (!fwupd_device_has_flag(dev, FWUPD_DEVICE_FLAG_SUPPORTED)) { g_ptr_array_add(devices_unsupported, dev); continue; } /* only process particular DEVICE-ID or GUID if specified */ for (guint idx = 0; idx < g_strv_length(values); idx++) { const gchar *tmpid = values[idx]; if (fwupd_device_has_guid(dev, tmpid) || g_strcmp0(device_id, tmpid) == 0) { dev_skip_byid = FALSE; break; } } if (g_strv_length(values) > 0 && dev_skip_byid) continue; if (!fwupd_device_match_flags(dev, priv->filter_device_include, priv->filter_device_exclude)) continue; supported = TRUE; /* get the releases for this device and filter for validity */ rels = fwupd_client_get_upgrades(priv->client, fwupd_device_get_id(dev), priv->cancellable, &error_install); if (rels == NULL) { g_ptr_array_add(devices_latest, dev); /* discard the actual reason from user, but leave for debugging */ g_debug("%s", error_install->message); continue; } for (guint j = 0; j < rels->len; j++) { FwupdRelease *rel_tmp = g_ptr_array_index(rels, j); if (!fwupd_release_match_flags(rel_tmp, priv->filter_release_include, priv->filter_release_exclude)) continue; rel = g_object_ref(rel_tmp); break; } if (rel == NULL) continue; /* something is wrong */ if (fwupd_device_get_problems(dev) != FWUPD_DEVICE_PROBLEM_NONE) { g_ptr_array_add(devices_pending, dev); continue; } ret = fu_util_update_device_with_release(priv, dev, rel, &error_install); if (!ret && g_error_matches(error_install, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) { g_debug("ignoring %s: %s", fwupd_device_get_id(dev), error_install->message); continue; } if (ret) fu_util_display_current_message(priv); /* send report if we're supposed to */ if (!fu_util_maybe_send_reports(priv, rel, &error_report)) { /* install failed, report failed */ if (!ret) { g_warning("%s", error_report->message); /* install succeeded, but report failed */ } else { g_propagate_error(error, g_steal_pointer(&error_report)); return FALSE; } } if (!ret) { g_propagate_error(error, g_steal_pointer(&error_install)); return FALSE; } } /* show warnings */ if (devices_latest->len > 0 && !priv->as_json) { fu_console_print_literal(priv->console, /* TRANSLATORS: message letting the user know no device * upgrade available */ _("Devices with the latest available firmware version:")); for (guint i = 0; i < devices_latest->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices_latest, i); fu_console_print(priv->console, " • %s", fwupd_device_get_name(dev)); } } if (devices_unsupported->len > 0 && !priv->as_json) { fu_console_print_literal(priv->console, /* TRANSLATORS: message letting the user know no * device upgrade available due to missing on LVFS */ _("Devices with no available firmware updates: ")); for (guint i = 0; i < devices_unsupported->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices_unsupported, i); fu_console_print(priv->console, " • %s", fwupd_device_get_name(dev)); } } if (devices_pending->len > 0 && !priv->as_json) { fu_console_print_literal( priv->console, /* TRANSLATORS: message letting the user there is an update * waiting, but there is a reason it cannot be deployed */ _("Devices with firmware updates that need user action: ")); for (guint i = 0; i < devices_pending->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices_pending, i); fu_console_print(priv->console, " • %s", fwupd_device_get_name(dev)); for (guint j = 0; j < 64; j++) { FwupdDeviceProblem problem = (guint64)1 << j; g_autofree gchar *desc = NULL; if (!fwupd_device_has_problem(dev, problem)) continue; desc = fu_util_device_problem_to_string(priv->client, dev, problem); if (desc == NULL) continue; fu_console_print(priv->console, " ‣ %s", desc); } } } /* no devices supported by LVFS or all are filtered */ if (!supported) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "No updatable devices"); return FALSE; } /* we don't want to ask anything */ if (priv->no_reboot_check || priv->as_json) { g_debug("skipping reboot check"); return TRUE; } return fu_util_prompt_complete(priv->console, priv->completion_flags, TRUE, error); } static gboolean fu_util_remote_modify(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FwupdRemote) remote = NULL; if (g_strv_length(values) < 3) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } /* ensure the remote exists */ remote = fwupd_client_get_remote_by_id(priv->client, values[0], priv->cancellable, error); if (remote == NULL) return FALSE; if (!fwupd_client_modify_remote(priv->client, fwupd_remote_get_id(remote), values[1], values[2], priv->cancellable, error)) return FALSE; if (priv->as_json) return TRUE; /* TRANSLATORS: success message for a per-remote setting change */ fu_console_print_literal(priv->console, _("Successfully modified remote")); return TRUE; } static gboolean fu_util_remote_enable(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FwupdRemote) remote = NULL; if (g_strv_length(values) != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } remote = fwupd_client_get_remote_by_id(priv->client, values[0], priv->cancellable, error); if (remote == NULL) return FALSE; if (!fu_util_modify_remote_warning(priv->console, remote, priv->assume_yes, error)) return FALSE; if (!fwupd_client_modify_remote(priv->client, fwupd_remote_get_id(remote), "Enabled", "true", priv->cancellable, error)) return FALSE; if (priv->as_json) return TRUE; /* ask for permission to refresh */ if (priv->no_remote_check || fwupd_remote_get_kind(remote) != FWUPD_REMOTE_KIND_DOWNLOAD) { /* TRANSLATORS: success message */ fu_console_print_literal(priv->console, _("Successfully enabled remote")); return TRUE; } if (!priv->assume_yes) { if (!fu_console_input_bool(priv->console, TRUE, "%s (%s)", /* TRANSLATORS: ask if we can update the metadata */ _("Do you want to refresh this remote now?"), /* TRANSLATORS: metadata is downloaded */ _("Requires internet connection"))) { /* TRANSLATORS: success message */ fu_console_print_literal(priv->console, _("Successfully enabled remote")); return TRUE; } } if (!fwupd_client_refresh_remote(priv->client, remote, priv->download_flags, priv->cancellable, error)) return FALSE; /* TRANSLATORS: success message */ fu_console_print_literal(priv->console, _("Successfully enabled and refreshed remote")); return TRUE; } static gboolean fu_util_remote_disable(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FwupdRemote) remote = NULL; if (g_strv_length(values) != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } /* ensure the remote exists */ remote = fwupd_client_get_remote_by_id(priv->client, values[0], priv->cancellable, error); if (remote == NULL) return FALSE; if (!fwupd_client_modify_remote(priv->client, values[0], "Enabled", "false", priv->cancellable, error)) return FALSE; if (priv->as_json) return TRUE; /* TRANSLATORS: success message */ fu_console_print_literal(priv->console, _("Successfully disabled remote")); return TRUE; } static gboolean fu_util_downgrade(FuUtilPrivate *priv, gchar **values, GError **error) { gboolean ret; g_autoptr(FwupdDevice) dev = NULL; g_autoptr(FwupdRelease) rel = NULL; g_autoptr(GPtrArray) rels = NULL; g_autoptr(GError) error_report = NULL; if (priv->flags & FWUPD_INSTALL_FLAG_ALLOW_REINSTALL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "--allow-reinstall is not supported for this command"); return FALSE; } priv->filter_device_include |= FWUPD_DEVICE_FLAG_SUPPORTED; dev = fu_util_get_device_or_prompt(priv, values, error); if (dev == NULL) return FALSE; /* get the releases for this device and filter for validity */ rels = fwupd_client_get_downgrades(priv->client, fwupd_device_get_id(dev), priv->cancellable, error); if (rels == NULL) { g_autofree gchar *downgrade_str = /* TRANSLATORS: message letting the user know no device downgrade available * %1 is the device name */ g_strdup_printf(_("No downgrades for %s"), fwupd_device_get_name(dev)); g_prefix_error(error, "%s: ", downgrade_str); return FALSE; } /* get the chosen release */ rel = fu_util_prompt_for_release(priv, rels, error); if (rel == NULL) return FALSE; /* update the console if composite devices are also updated */ priv->current_operation = FU_UTIL_OPERATION_DOWNGRADE; priv->flags |= FWUPD_INSTALL_FLAG_ALLOW_OLDER; ret = fu_util_update_device_with_release(priv, dev, rel, error); if (ret) fu_util_display_current_message(priv); /* send report if we're supposed to */ if (!fu_util_maybe_send_reports(priv, rel, &error_report)) { /* install failed, report failed */ if (!ret) { g_warning("%s", error_report->message); /* install succeeded, but report failed */ } else { g_propagate_error(error, g_steal_pointer(&error_report)); return FALSE; } } /* install failed */ if (!ret) return FALSE; /* we don't want to ask anything */ if (priv->no_reboot_check) { g_debug("skipping reboot check"); return TRUE; } return fu_util_prompt_complete(priv->console, priv->completion_flags, TRUE, error); } static gboolean fu_util_reinstall(FuUtilPrivate *priv, gchar **values, GError **error) { gboolean ret; g_autoptr(FwupdRelease) rel = NULL; g_autoptr(FwupdDevice) dev = NULL; g_autoptr(GError) error_report = NULL; priv->filter_device_include |= FWUPD_DEVICE_FLAG_SUPPORTED; dev = fu_util_get_device_or_prompt(priv, values, error); if (dev == NULL) return FALSE; /* try to lookup/match release from client */ rel = fu_util_get_release_for_device_version(priv, dev, fwupd_device_get_version(dev), error); if (rel == NULL) return FALSE; /* update the console if composite devices are also updated */ priv->current_operation = FU_UTIL_OPERATION_INSTALL; priv->flags |= FWUPD_INSTALL_FLAG_ALLOW_REINSTALL; ret = fu_util_update_device_with_release(priv, dev, rel, error); if (ret) fu_util_display_current_message(priv); /* send report if we're supposed to */ if (!fu_util_maybe_send_reports(priv, rel, &error_report)) { /* install failed, report failed */ if (!ret) { g_warning("%s", error_report->message); /* install succeeded, but report failed */ } else { g_propagate_error(error, g_steal_pointer(&error_report)); return FALSE; } } /* install failed */ if (!ret) return FALSE; /* we don't want to ask anything */ if (priv->no_reboot_check) { g_debug("skipping reboot check"); return TRUE; } return fu_util_prompt_complete(priv->console, priv->completion_flags, TRUE, error); } static gboolean fu_util_install(FuUtilPrivate *priv, gchar **values, GError **error) { gboolean ret; g_autoptr(FwupdDevice) dev = NULL; g_autoptr(FwupdRelease) rel = NULL; g_autoptr(GError) error_report = NULL; /* fall back for CLI compatibility */ if (g_strv_length(values) >= 1) { if (g_file_test(values[0], G_FILE_TEST_EXISTS) || fu_util_is_url(values[0])) return fu_util_local_install(priv, values, error); } /* find device */ priv->filter_device_include |= FWUPD_DEVICE_FLAG_SUPPORTED; dev = fu_util_get_device_or_prompt(priv, values, error); if (dev == NULL) return FALSE; /* find release */ if (g_strv_length(values) >= 2) { rel = fu_util_get_release_for_device_version(priv, dev, values[1], error); if (rel == NULL) return FALSE; } else { g_autoptr(GPtrArray) rels = NULL; rels = fwupd_client_get_releases(priv->client, fwupd_device_get_id(dev), priv->cancellable, error); if (rels == NULL) return FALSE; rel = fu_util_prompt_for_release(priv, rels, error); if (rel == NULL) return FALSE; } /* allow all actions */ priv->current_operation = FU_UTIL_OPERATION_INSTALL; ret = fu_util_update_device_with_release(priv, dev, rel, error); if (ret) fu_util_display_current_message(priv); /* send report if we're supposed to */ if (!fu_util_maybe_send_reports(priv, rel, &error_report)) { /* install failed, report failed */ if (!ret) { g_warning("%s", error_report->message); /* install succeeded, but report failed */ } else { g_propagate_error(error, g_steal_pointer(&error_report)); return FALSE; } } /* install failed */ if (!ret) return FALSE; /* we don't want to ask anything */ if (priv->no_reboot_check) { g_debug("skipping reboot check"); return TRUE; } return fu_util_prompt_complete(priv->console, priv->completion_flags, TRUE, error); } static gboolean _g_str_equal0(gconstpointer str1, gconstpointer str2) { return g_strcmp0(str1, str2) == 0; } static gboolean fu_util_switch_branch(FuUtilPrivate *priv, gchar **values, GError **error) { const gchar *branch; gboolean ret; g_autoptr(FwupdRelease) rel = NULL; g_autoptr(GPtrArray) rels = NULL; g_autoptr(GPtrArray) branches = g_ptr_array_new_with_free_func(g_free); g_autoptr(FwupdDevice) dev = NULL; g_autoptr(GError) error_report = NULL; /* find the device and check it has multiple branches */ priv->filter_device_include |= FWUPD_DEVICE_FLAG_HAS_MULTIPLE_BRANCHES; priv->filter_device_include |= FWUPD_DEVICE_FLAG_UPDATABLE; dev = fu_util_get_device_or_prompt(priv, values, error); if (dev == NULL) return FALSE; /* get all releases, including the alternate branch versions */ rels = fwupd_client_get_releases(priv->client, fwupd_device_get_id(dev), priv->cancellable, error); if (rels == NULL) return FALSE; /* get all the unique branches */ for (guint i = 0; i < rels->len; i++) { FwupdRelease *rel_tmp = g_ptr_array_index(rels, i); const gchar *branch_tmp = fwupd_release_get_branch(rel_tmp); if (!fwupd_release_match_flags(rel_tmp, priv->filter_release_include, priv->filter_release_exclude)) continue; if (g_ptr_array_find_with_equal_func(branches, branch_tmp, _g_str_equal0, NULL)) continue; g_ptr_array_add(branches, g_strdup(branch_tmp)); } /* branch name is optional */ if (g_strv_length(values) > 1) { branch = values[1]; } else if (branches->len == 1) { branch = g_ptr_array_index(branches, 0); } else { guint idx; /* TRANSLATORS: this is to abort the interactive prompt */ fu_console_print(priv->console, "0.\t%s", _("Cancel")); for (guint i = 0; i < branches->len; i++) { const gchar *branch_tmp = g_ptr_array_index(branches, i); fu_console_print(priv->console, "%u.\t%s", i + 1, fu_util_branch_for_display(branch_tmp)); } /* TRANSLATORS: get interactive prompt, where branch is the * supplier of the firmware, e.g. "non-free" or "free" */ idx = fu_console_input_uint(priv->console, branches->len, "%s", _("Choose branch")); if (idx == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "Request canceled"); return FALSE; } branch = g_ptr_array_index(branches, idx - 1); } /* sanity check */ if (g_strcmp0(branch, fwupd_device_get_branch(dev)) == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Device %s is already on branch %s", fwupd_device_get_name(dev), fu_util_branch_for_display(branch)); return FALSE; } /* the releases are ordered by version */ for (guint j = 0; j < rels->len; j++) { FwupdRelease *rel_tmp = g_ptr_array_index(rels, j); if (g_strcmp0(fwupd_release_get_branch(rel_tmp), branch) == 0) { rel = g_object_ref(rel_tmp); break; } } if (rel == NULL) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "No releases for branch %s", fu_util_branch_for_display(branch)); return FALSE; } /* we're switching branch */ if (!fu_util_switch_branch_warning(priv->console, dev, rel, priv->assume_yes, error)) return FALSE; /* update the console if composite devices are also updated */ priv->current_operation = FU_UTIL_OPERATION_INSTALL; priv->flags |= FWUPD_INSTALL_FLAG_ALLOW_REINSTALL; priv->flags |= FWUPD_INSTALL_FLAG_ALLOW_BRANCH_SWITCH; ret = fu_util_update_device_with_release(priv, dev, rel, error); if (ret) fu_util_display_current_message(priv); /* send report if we're supposed to */ if (!fu_util_maybe_send_reports(priv, rel, &error_report)) { /* install failed, report failed */ if (!ret) { g_warning("%s", error_report->message); /* install succeeded, but report failed */ } else { g_propagate_error(error, g_steal_pointer(&error_report)); return FALSE; } } /* install failed */ if (!ret) return FALSE; /* we don't want to ask anything */ if (priv->no_reboot_check) { g_debug("skipping reboot check"); return TRUE; } return fu_util_prompt_complete(priv->console, priv->completion_flags, TRUE, error); } static gboolean fu_util_activate(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(GPtrArray) devices = NULL; gboolean has_pending = FALSE; /* handle both forms */ if (g_strv_length(values) == 0) { /* activate anything with _NEEDS_ACTIVATION */ devices = fwupd_client_get_devices(priv->client, priv->cancellable, error); if (devices == NULL) return FALSE; for (guint i = 0; i < devices->len; i++) { FwupdDevice *device = g_ptr_array_index(devices, i); if (fwupd_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION)) { has_pending = TRUE; break; } } } else if (g_strv_length(values) == 1) { FwupdDevice *device = fu_util_get_device_by_id(priv, values[0], error); if (device == NULL) return FALSE; devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_ptr_array_add(devices, device); if (fwupd_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION)) has_pending = TRUE; } else { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments"); return FALSE; } /* nothing to do */ if (!has_pending) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "No firmware to activate"); return FALSE; } /* activate anything with _NEEDS_ACTIVATION */ for (guint i = 0; i < devices->len; i++) { FwupdDevice *device = g_ptr_array_index(devices, i); if (!fwupd_device_match_flags(device, priv->filter_device_include, priv->filter_device_exclude)) continue; if (!fwupd_device_has_flag(device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION)) continue; fu_console_print( priv->console, "%s %s…", /* TRANSLATORS: shown when shutting down to switch to the new version */ _("Activating firmware update for"), fwupd_device_get_name(device)); if (!fwupd_client_activate(priv->client, priv->cancellable, fwupd_device_get_id(device), error)) return FALSE; } if (priv->as_json) return TRUE; /* TRANSLATORS: success message -- where activation is making the new * firmware take effect, usually after updating offline */ fu_console_print_literal(priv->console, _("Successfully activated all devices")); return TRUE; } static gboolean fu_util_set_approved_firmware(FuUtilPrivate *priv, gchar **values, GError **error) { g_auto(GStrv) checksums = NULL; /* check args */ if (g_strv_length(values) != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments: filename or list of checksums expected"); return FALSE; } /* filename */ if (g_file_test(values[0], G_FILE_TEST_EXISTS)) { g_autofree gchar *data = NULL; if (!g_file_get_contents(values[0], &data, NULL, error)) return FALSE; checksums = g_strsplit(data, "\n", -1); } else { checksums = g_strsplit(values[0], ",", -1); } /* call into daemon */ return fwupd_client_set_approved_firmware(priv->client, checksums, priv->cancellable, error); } static gboolean fu_util_get_checksums_as_json(FuUtilPrivate *priv, gchar **csums, GError **error) { g_autoptr(JsonBuilder) builder = json_builder_new(); json_builder_begin_object(builder); json_builder_set_member_name(builder, "Checksums"); json_builder_begin_array(builder); for (guint i = 0; csums[i] != NULL; i++) json_builder_add_string_value(builder, csums[i]); json_builder_end_array(builder); json_builder_end_object(builder); return fu_util_print_builder(priv->console, builder, error); } static gboolean fu_util_get_approved_firmware(FuUtilPrivate *priv, gchar **values, GError **error) { g_auto(GStrv) checksums = NULL; /* check args */ if (g_strv_length(values) != 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments: none expected"); return FALSE; } /* call into daemon */ checksums = fwupd_client_get_approved_firmware(priv->client, priv->cancellable, error); if (checksums == NULL) return FALSE; if (priv->as_json) return fu_util_get_checksums_as_json(priv, checksums, error); if (g_strv_length(checksums) == 0) { /* TRANSLATORS: approved firmware has been checked by * the domain administrator */ fu_console_print_literal(priv->console, _("There is no approved firmware.")); } else { fu_console_print_literal( priv->console, /* TRANSLATORS: approved firmware has been checked by * the domain administrator */ ngettext("Approved firmware:", "Approved firmware:", g_strv_length(checksums))); for (guint i = 0; checksums[i] != NULL; i++) fu_console_print(priv->console, " * %s", checksums[i]); } return TRUE; } static gboolean fu_util_modify_config(FuUtilPrivate *priv, gchar **values, GError **error) { /* check args */ if (g_strv_length(values) == 3) { if (!fwupd_client_modify_config(priv->client, values[0], values[1], values[2], priv->cancellable, error)) return FALSE; } else if (g_strv_length(values) == 2) { if (!fwupd_client_modify_config(priv->client, "fwupd", values[0], values[1], priv->cancellable, error)) return FALSE; } else { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments: [SECTION] KEY VALUE expected"); return FALSE; } if (priv->as_json) return TRUE; if (!priv->assume_yes) { if (!fu_console_input_bool(priv->console, FALSE, "%s", /* TRANSLATORS: changes only take effect on restart */ _("Restart the daemon to make the change effective?"))) return TRUE; } if (!fu_util_quit(priv, NULL, error)) return FALSE; if (!fwupd_client_connect(priv->client, priv->cancellable, error)) return FALSE; /* TRANSLATORS: success message -- a per-system setting value */ fu_console_print_literal(priv->console, _("Successfully modified configuration value")); return TRUE; } static gboolean fu_util_reset_config(FuUtilPrivate *priv, gchar **values, GError **error) { /* check args */ if (g_strv_length(values) != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments: SECTION expected"); return FALSE; } if (!fwupd_client_reset_config(priv->client, values[0], priv->cancellable, error)) return FALSE; if (priv->as_json) return TRUE; if (!priv->assume_yes) { if (!fu_console_input_bool(priv->console, FALSE, "%s", /* TRANSLATORS: changes only take effect on restart */ _("Restart the daemon to make the change effective?"))) return TRUE; } if (!fu_util_quit(priv, NULL, error)) return FALSE; if (!fwupd_client_connect(priv->client, priv->cancellable, error)) return FALSE; /* TRANSLATORS: success message -- a per-system setting value */ fu_console_print_literal(priv->console, _("Successfully reset configuration values")); return TRUE; } static FwupdRemote * fu_util_get_remote_with_report_uri(FuUtilPrivate *priv, GError **error) { g_autoptr(GPtrArray) remotes = NULL; /* get all remotes */ remotes = fwupd_client_get_remotes(priv->client, priv->cancellable, error); if (remotes == NULL) return NULL; for (guint i = 0; i < remotes->len; i++) { FwupdRemote *remote = g_ptr_array_index(remotes, i); if (!fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_ENABLED)) continue; if (fwupd_remote_get_report_uri(remote) != NULL) return g_object_ref(remote); } /* failed */ g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "No remotes specified ReportURI"); return NULL; } static gboolean fu_util_upload_security(FuUtilPrivate *priv, GPtrArray *attrs, GError **error) { g_autofree gchar *data = NULL; g_autofree gchar *report_uri = NULL; g_autofree gchar *sig = NULL; g_autofree gchar *uri = NULL; g_autoptr(FwupdRemote) remote = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(GHashTable) metadata = NULL; /* can we find a remote with a security attr */ remote = fu_util_get_remote_with_report_uri(priv, &error_local); if (remote == NULL) { g_debug("failed to find suitable remote: %s", error_local->message); return TRUE; } /* export as a string */ metadata = fwupd_client_get_report_metadata(priv->client, priv->cancellable, error); if (metadata == NULL) return FALSE; data = fwupd_client_build_report_security(priv->client, attrs, metadata, error); if (data == NULL) return FALSE; /* ask for permission */ if (!priv->assume_yes && !fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_AUTOMATIC_SECURITY_REPORTS)) { if (!fu_console_input_bool(priv->console, FALSE, /* TRANSLATORS: ask the user to share, %s is something * like: "Linux Vendor Firmware Service" */ _("Upload these anonymous results to the %s to help " "other users?"), fwupd_remote_get_title(remote))) { return TRUE; } } /* self sign data */ if (priv->sign) { sig = fwupd_client_self_sign(priv->client, data, FWUPD_SELF_SIGN_FLAG_ADD_TIMESTAMP, priv->cancellable, error); if (sig == NULL) return FALSE; } /* ask for permission */ if (!priv->assume_yes && !fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_AUTOMATIC_SECURITY_REPORTS)) { fu_console_print_kv(priv->console, _("Target"), fwupd_remote_get_report_uri(remote)); fu_console_print_kv(priv->console, _("Payload"), data); if (sig != NULL) fu_console_print_kv(priv->console, _("Signature"), sig); if (!fu_console_input_bool(priv->console, TRUE, "%s", _("Proceed with upload?"))) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_PERMISSION_DENIED, "User declined action"); return FALSE; } } /* POST request */ report_uri = fwupd_remote_build_report_uri(remote, error); if (report_uri == NULL) return FALSE; uri = fwupd_client_upload_report(priv->client, report_uri, data, sig, FWUPD_CLIENT_UPLOAD_FLAG_ALWAYS_MULTIPART, priv->cancellable, error); if (uri == NULL) return FALSE; fu_console_print_literal(priv->console, /* TRANSLATORS: success, so say thank you to the user */ _("Host Security ID attributes uploaded successfully, thanks!")); /* as this worked, ask if the user want to do this every time */ if (!fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_AUTOMATIC_SECURITY_REPORTS)) { if (fu_console_input_bool(priv->console, FALSE, "%s", /* TRANSLATORS: can we JFDI? */ _("Automatically upload every time?"))) { if (!fwupd_client_modify_remote(priv->client, fwupd_remote_get_id(remote), "AutomaticSecurityReports", "true", priv->cancellable, error)) return FALSE; } } return TRUE; } static gboolean fu_util_security_as_json(FuUtilPrivate *priv, GPtrArray *attrs, GPtrArray *events, GPtrArray *devices, GError **error) { g_autoptr(GPtrArray) devices_issues = NULL; g_autoptr(JsonBuilder) builder = json_builder_new(); json_builder_begin_object(builder); /* attrs */ fwupd_codec_array_to_json(attrs, "SecurityAttributes", builder, FWUPD_CODEC_FLAG_TRUSTED); /* events */ if (events != NULL && events->len > 0) { fwupd_codec_array_to_json(events, "SecurityEvents", builder, FWUPD_CODEC_FLAG_TRUSTED); } /* devices */ devices_issues = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); for (guint i = 0; devices != NULL && i < devices->len; i++) { FwupdDevice *device = g_ptr_array_index(devices, i); GPtrArray *issues = fwupd_device_get_issues(device); if (issues->len == 0) continue; g_ptr_array_add(devices_issues, g_object_ref(device)); } if (devices_issues->len > 0) { fwupd_codec_array_to_json(devices_issues, "Devices", builder, FWUPD_CODEC_FLAG_TRUSTED); } json_builder_end_object(builder); return fu_util_print_builder(priv->console, builder, error); } static gboolean fu_util_sync(FuUtilPrivate *priv, gchar **values, GError **error) { const gchar *host_bkc = fwupd_client_get_host_bkc(priv->client); guint cnt = 0; g_autoptr(GPtrArray) devices = NULL; /* update the console if composite devices are also updated */ priv->current_operation = FU_UTIL_OPERATION_INSTALL; priv->flags |= FWUPD_INSTALL_FLAG_ALLOW_OLDER; devices = fwupd_client_get_devices(priv->client, NULL, error); if (devices == NULL) return FALSE; for (guint i = 0; i < devices->len; i++) { FwupdDevice *dev = g_ptr_array_index(devices, i); g_autoptr(FwupdRelease) rel = NULL; g_autoptr(GError) error_local = NULL; /* find the release that matches the tag */ if (host_bkc != NULL) { rel = fu_util_get_release_with_tag(priv, dev, host_bkc, &error_local); } else if (fu_device_get_branch(dev) != NULL) { rel = fu_util_get_release_with_branch(priv, dev, fu_device_get_branch(dev), &error_local); } else { g_set_error_literal(&error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "No device branch or system HostBkc set"); } if (rel == NULL) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED) || g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) { g_debug("ignoring %s: %s", fwupd_device_get_id(dev), error_local->message); continue; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } /* ignore if already on that release */ if (g_strcmp0(fwupd_device_get_version(dev), fwupd_release_get_version(rel)) == 0) continue; /* install this new release */ g_debug("need to move %s from %s to %s", fwupd_device_get_id(dev), fwupd_device_get_version(dev), fwupd_release_get_version(rel)); if (!fu_util_update_device_with_release(priv, dev, rel, &error_local)) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) { g_debug("ignoring %s: %s", fwupd_device_get_id(dev), error_local->message); continue; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } fu_util_display_current_message(priv); cnt++; } /* nothing was done */ if (cnt == 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "No devices required modification"); return FALSE; } /* we don't want to ask anything */ if (priv->no_reboot_check) { g_debug("skipping reboot check"); return TRUE; } /* show reboot if needed */ return fu_util_prompt_complete(priv->console, priv->completion_flags, TRUE, error); } static gboolean fu_util_security_fix_attr(FuUtilPrivate *priv, FwupdSecurityAttr *attr, GError **error) { g_autoptr(GString) body = g_string_new(NULL); g_autoptr(GString) title = g_string_new(NULL); g_string_append_printf(title, "%s: %s", /* TRANSLATORS: title prefix for the BIOS settings dialog */ _("Configuration Change Suggested"), fwupd_security_attr_get_title(attr)); g_string_append(body, fwupd_security_attr_get_description(attr)); if (fwupd_security_attr_get_bios_setting_id(attr) != NULL && fwupd_security_attr_get_bios_setting_current_value(attr) != NULL && fwupd_security_attr_get_bios_setting_target_value(attr) != NULL) { g_string_append(body, "\n\n"); g_string_append_printf(body, /* TRANSLATORS: the %1 is a BIOS setting name. * %2 and %3 are the values, e.g. "True" or "Windows10" */ _("This tool can change the BIOS setting '%s' from '%s' " "to '%s' automatically, but it will only be active after " "restarting the computer."), fwupd_security_attr_get_bios_setting_id(attr), fwupd_security_attr_get_bios_setting_current_value(attr), fwupd_security_attr_get_bios_setting_target_value(attr)); g_string_append(body, "\n\n"); g_string_append(body, /* TRANSLATORS: the user has to manually recover; we can't do it */ _("You should ensure you are comfortable restoring the setting " "from the system firmware setup, as this change may cause the " "system to not boot into Linux or cause other system " "instability.")); } else if (fwupd_security_attr_get_kernel_target_value(attr) != NULL) { g_string_append(body, "\n\n"); if (fwupd_security_attr_get_kernel_current_value(attr) != NULL) { g_string_append_printf( body, /* TRANSLATORS: the %1 is a kernel command line key=value */ _("This tool can change the kernel argument from '%s' to '%s', but " "it will only be active after restarting the computer."), fwupd_security_attr_get_kernel_current_value(attr), fwupd_security_attr_get_kernel_target_value(attr)); } else { g_string_append_printf( body, /* TRANSLATORS: the %1 is a kernel command line key=value */ _("This tool can add a kernel argument of '%s', but it will " "only be active after restarting the computer."), fwupd_security_attr_get_kernel_target_value(attr)); } g_string_append(body, "\n\n"); g_string_append(body, /* TRANSLATORS: the user has to manually recover; we can't do it */ _("You should ensure you are comfortable restoring the setting " "from a recovery or installation disk, as this change may cause " "the system to not boot into Linux or cause other system " "instability.")); } fu_console_box(priv->console, title->str, body->str, 80); /* TRANSLATORS: prompt to apply the update */ if (!fu_console_input_bool(priv->console, FALSE, "%s", _("Perform operation?"))) return TRUE; if (!fwupd_client_fix_host_security_attr(priv->client, fwupd_security_attr_get_appstream_id(attr), priv->cancellable, error)) return FALSE; /* do not offer to upload the report */ priv->completion_flags |= FWUPD_DEVICE_FLAG_NEEDS_REBOOT; return TRUE; } static gboolean fu_util_security(FuUtilPrivate *priv, gchar **values, GError **error) { FuSecurityAttrToStringFlags flags = FU_SECURITY_ATTR_TO_STRING_FLAG_NONE; g_autoptr(GPtrArray) attrs = NULL; g_autoptr(GPtrArray) devices = NULL; g_autoptr(GPtrArray) events = NULL; g_autoptr(GError) error_local = NULL; g_autofree gchar *str = NULL; #ifndef HAVE_HSI g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, /* TRANSLATORS: error message for unsupported feature */ _("Host Security ID (HSI) is not supported")); return FALSE; #endif /* HAVE_HSI */ /* the "why" */ attrs = fwupd_client_get_host_security_attrs(priv->client, priv->cancellable, error); if (attrs == NULL) return FALSE; /* the "when" */ events = fwupd_client_get_host_security_events(priv->client, 10, priv->cancellable, &error_local); if (events == NULL) { if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) { g_debug("ignoring failed events: %s", error_local->message); } else { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } /* the "also" */ devices = fwupd_client_get_devices(priv->client, priv->cancellable, &error_local); if (devices == NULL) { if (!g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) { g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } /* not for human consumption */ if (priv->as_json) return fu_util_security_as_json(priv, attrs, events, devices, error); fu_console_print(priv->console, "%s \033[1m%s\033[0m", /* TRANSLATORS: this is a string like 'HSI:2-U' */ _("Host Security ID:"), fwupd_client_get_host_security_id(priv->client)); /* show or hide different elements */ if (priv->show_all) { flags |= FU_SECURITY_ATTR_TO_STRING_FLAG_SHOW_OBSOLETES; flags |= FU_SECURITY_ATTR_TO_STRING_FLAG_SHOW_URLS; } str = fu_util_security_attrs_to_string(attrs, flags); fu_console_print_literal(priv->console, str); /* events */ if (events != NULL && events->len > 0) { g_autofree gchar *estr = fu_util_security_events_to_string(events, flags); if (estr != NULL) fu_console_print_literal(priv->console, estr); } /* known CVEs */ if (devices != NULL && devices->len > 0) { g_autofree gchar *estr = fu_util_security_issues_to_string(devices); if (estr != NULL) fu_console_print_literal(priv->console, estr); } /* host emulation */ for (guint j = 0; j < attrs->len; j++) { FwupdSecurityAttr *attr = g_ptr_array_index(attrs, j); if (g_strcmp0(fwupd_security_attr_get_appstream_id(attr), FWUPD_SECURITY_ATTR_ID_HOST_EMULATION) == 0) { priv->no_unreported_check = TRUE; break; } } /* any things we can fix? */ if (!priv->no_security_fix) { for (guint j = 0; j < attrs->len; j++) { FwupdSecurityAttr *attr = g_ptr_array_index(attrs, j); if (fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_CAN_FIX) && !fwupd_security_attr_has_flag(attr, FWUPD_SECURITY_ATTR_FLAG_SUCCESS)) { if (!fu_util_security_fix_attr(priv, attr, error)) return FALSE; } } } /* upload, with confirmation */ if (!priv->no_unreported_check) { if (!fu_util_upload_security(priv, attrs, error)) return FALSE; } /* reboot is required? */ if (!priv->no_reboot_check && (priv->completion_flags & FWUPD_DEVICE_FLAG_NEEDS_REBOOT) > 0) { if (!fu_util_prompt_complete(priv->console, priv->completion_flags, TRUE, error)) return FALSE; } /* success */ return TRUE; } static void fu_util_ignore_cb(const gchar *log_domain, GLogLevelFlags log_level, const gchar *message, gpointer user_data) { } #ifdef HAVE_GIO_UNIX static gboolean fu_util_sigint_cb(gpointer user_data) { FuUtilPrivate *priv = (FuUtilPrivate *)user_data; g_debug("Handling SIGINT"); g_cancellable_cancel(priv->cancellable); return FALSE; } #endif static void fu_util_setup_signal_handlers(FuUtilPrivate *priv) { #ifdef HAVE_GIO_UNIX g_autoptr(GSource) source = g_unix_signal_source_new(SIGINT); g_source_set_callback(source, fu_util_sigint_cb, priv, NULL); g_source_attach(g_steal_pointer(&source), priv->main_ctx); #endif } static void fu_util_private_free(FuUtilPrivate *priv) { if (priv->client != NULL) { /* when destroying GDBusProxy in a custom GMainContext, the context must be * iterated enough after finalization of the proxies that any pending D-Bus traffic * can be freed */ fwupd_client_disconnect(priv->client, NULL); while (g_main_context_iteration(priv->main_ctx, FALSE)) { /* nothing needs to be done here */ }; g_object_unref(priv->client); } if (priv->current_device != NULL) g_object_unref(priv->current_device); g_ptr_array_unref(priv->post_requests); g_main_loop_unref(priv->loop); g_main_context_unref(priv->main_ctx); g_object_unref(priv->cancellable); g_object_unref(priv->console); g_option_context_free(priv->context); g_free(priv); } static gboolean fu_util_check_daemon_version(FuUtilPrivate *priv, GError **error) { const gchar *daemon = fwupd_client_get_daemon_version(priv->client); if (daemon == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, /* TRANSLATORS: error message */ _("Unable to connect to service")); return FALSE; } if (g_strcmp0(daemon, PACKAGE_VERSION) != 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, /* TRANSLATORS: error message */ _("Unsupported daemon version %s, client version is %s"), daemon, PACKAGE_VERSION); return FALSE; } return TRUE; } static gboolean fu_util_check_polkit_actions(GError **error) { #ifdef HAVE_POLKIT g_autofree gchar *directory = NULL; g_autofree gchar *filename = NULL; if (g_getenv("FWUPD_POLKIT_NOCHECK") != NULL) return TRUE; directory = fu_path_from_kind(FU_PATH_KIND_POLKIT_ACTIONS); filename = g_build_filename(directory, "org.freedesktop.fwupd.policy", NULL); if (!g_file_test(filename, G_FILE_TEST_IS_REGULAR)) { g_set_error_literal( error, FWUPD_ERROR, FWUPD_ERROR_AUTH_FAILED, "PolicyKit files are missing, see " "https://github.com/fwupd/fwupd/wiki/PolicyKit-files-are-missing"); return FALSE; } #endif return TRUE; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuUtilPrivate, fu_util_private_free) #pragma clang diagnostic pop static gchar * fu_util_get_history_checksum(FuUtilPrivate *priv, GError **error) { const gchar *csum; g_autoptr(FwupdDevice) device = NULL; g_autoptr(FwupdRelease) release = NULL; g_autoptr(GPtrArray) devices = NULL; devices = fwupd_client_get_history(priv->client, priv->cancellable, error); if (devices == NULL) return NULL; device = fu_util_prompt_for_device(priv, devices, error); if (device == NULL) return NULL; release = fu_util_prompt_for_release(priv, fwupd_device_get_releases(device), error); if (release == NULL) return NULL; csum = fwupd_checksum_get_best(fwupd_release_get_checksums(release)); if (csum == NULL) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "No suitable checksums"); return NULL; } return g_strdup(csum); } static gboolean fu_util_block_firmware(FuUtilPrivate *priv, gchar **values, GError **error) { guint idx = 0; g_autofree gchar *csum = NULL; g_auto(GStrv) csums_new = NULL; g_auto(GStrv) csums = NULL; /* get existing checksums */ csums = fwupd_client_get_blocked_firmware(priv->client, priv->cancellable, error); if (csums == NULL) return FALSE; /* get new value */ if (g_strv_length(values) == 0) { csum = fu_util_get_history_checksum(priv, error); if (csum == NULL) return FALSE; } else { csum = g_strdup(values[0]); } /* ensure it's not already there */ if (g_strv_contains((const gchar *const *)csums, csum)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, /* TRANSLATORS: user selected something not possible */ _("Firmware is already blocked")); return FALSE; } /* TRANSLATORS: we will not offer this firmware to the user */ fu_console_print(priv->console, "%s %s", _("Blocking firmware:"), csum); /* remove it from the new list */ csums_new = g_new0(gchar *, g_strv_length(csums) + 2); for (guint i = 0; csums[i] != NULL; i++) { if (g_strcmp0(csums[i], csum) != 0) csums_new[idx++] = g_strdup(csums[i]); } csums_new[idx] = g_strdup(csum); return fwupd_client_set_blocked_firmware(priv->client, csums_new, priv->cancellable, error); } static gboolean fu_util_unblock_firmware(FuUtilPrivate *priv, gchar **values, GError **error) { guint idx = 0; g_auto(GStrv) csums = NULL; g_auto(GStrv) csums_new = NULL; g_autofree gchar *csum = NULL; /* get existing checksums */ csums = fwupd_client_get_blocked_firmware(priv->client, priv->cancellable, error); if (csums == NULL) return FALSE; /* empty list */ if (g_strv_length(csums) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, /* TRANSLATORS: nothing to show */ _("There are no blocked firmware files")); return FALSE; } /* get new value */ if (g_strv_length(values) == 0) { csum = fu_util_get_history_checksum(priv, error); if (csum == NULL) return FALSE; } else { csum = g_strdup(values[0]); } /* ensure it's there */ if (!g_strv_contains((const gchar *const *)csums, csum)) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, /* TRANSLATORS: user selected something not possible */ _("Firmware is not already blocked")); return FALSE; } /* TRANSLATORS: we will now offer this firmware to the user */ fu_console_print(priv->console, "%s %s", _("Unblocking firmware:"), csum); /* remove it from the new list */ csums_new = g_new0(gchar *, g_strv_length(csums)); for (guint i = 0; csums[i] != NULL; i++) { if (g_strcmp0(csums[i], csum) != 0) csums_new[idx++] = g_strdup(csums[i]); } return fwupd_client_set_blocked_firmware(priv->client, csums_new, priv->cancellable, error); } static gboolean fu_util_get_blocked_firmware(FuUtilPrivate *priv, gchar **values, GError **error) { g_auto(GStrv) csums = NULL; /* get checksums */ csums = fwupd_client_get_blocked_firmware(priv->client, priv->cancellable, error); if (csums == NULL) return FALSE; if (priv->as_json) return fu_util_get_checksums_as_json(priv, csums, error); /* empty list */ if (g_strv_length(csums) == 0) { /* TRANSLATORS: nothing to show */ fu_console_print_literal(priv->console, _("There are no blocked firmware files")); return TRUE; } /* TRANSLATORS: there follows a list of hashes */ fu_console_print_literal(priv->console, _("Blocked firmware files:")); for (guint i = 0; csums[i] != NULL; i++) { fu_console_print(priv->console, "%u.\t%s", i + 1, csums[i]); } /* success */ return TRUE; } static void fu_util_show_plugin_warnings(FuUtilPrivate *priv) { FwupdPluginFlags flags = FWUPD_PLUGIN_FLAG_NONE; g_autoptr(GPtrArray) plugins = NULL; if (priv->as_json) return; /* get plugins from daemon, ignoring if the daemon is too old */ plugins = fwupd_client_get_plugins(priv->client, priv->cancellable, NULL); if (plugins == NULL) return; /* get a superset so we do not show the same message more than once */ for (guint i = 0; i < plugins->len; i++) { FwupdPlugin *plugin = g_ptr_array_index(plugins, i); if (fwupd_plugin_has_flag(plugin, FWUPD_PLUGIN_FLAG_DISABLED)) continue; if (!fwupd_plugin_has_flag(plugin, FWUPD_PLUGIN_FLAG_USER_WARNING)) continue; flags |= fwupd_plugin_get_flags(plugin); } /* never show these, they're way too generic */ flags &= ~FWUPD_PLUGIN_FLAG_DISABLED; flags &= ~FWUPD_PLUGIN_FLAG_NO_HARDWARE; flags &= ~FWUPD_PLUGIN_FLAG_REQUIRE_HWID; flags &= ~FWUPD_PLUGIN_FLAG_MEASURE_SYSTEM_INTEGRITY; flags &= ~FWUPD_PLUGIN_FLAG_READY; /* print */ for (guint i = 0; i < 64; i++) { FwupdPluginFlags flag = (guint64)1 << i; g_autofree gchar *tmp = NULL; g_autofree gchar *url = NULL; g_autoptr(GString) str = g_string_new(NULL); if ((flags & flag) == 0) continue; tmp = fu_util_plugin_flag_to_string(flag); if (tmp == NULL) continue; g_string_append_printf(str, "%s\n", tmp); url = g_strdup_printf("https://github.com/fwupd/fwupd/wiki/PluginFlag:%s", fwupd_plugin_flag_to_string(flag)); /* TRANSLATORS: %s is a link to a website */ g_string_append_printf(str, _("See %s for more information."), url); fu_console_print_full(priv->console, FU_CONSOLE_PRINT_FLAG_WARNING, "%s\n", str->str); } } static gboolean fu_util_set_bios_setting(FuUtilPrivate *priv, gchar **input, GError **error) { g_autoptr(GHashTable) settings = fu_util_bios_settings_parse_argv(input, error); if (settings == NULL) return FALSE; if (!fwupd_client_modify_bios_setting(priv->client, settings, priv->cancellable, error)) { if (!g_error_matches(*error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) g_prefix_error(error, "failed to set BIOS setting: "); return FALSE; } if (!priv->as_json) { gpointer key, value; GHashTableIter iter; g_hash_table_iter_init(&iter, settings); while (g_hash_table_iter_next(&iter, &key, &value)) { g_autofree gchar *msg = /* TRANSLATORS: Configured a BIOS setting to a value */ g_strdup_printf(_("Set BIOS setting '%s' using '%s'."), (const gchar *)key, (const gchar *)value); fu_console_print_literal(priv->console, msg); } } priv->completion_flags |= FWUPD_DEVICE_FLAG_NEEDS_REBOOT; if (priv->no_reboot_check) { g_debug("skipping reboot check"); return TRUE; } return fu_util_prompt_complete(priv->console, priv->completion_flags, TRUE, error); } static gboolean fu_util_get_bios_setting(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(GPtrArray) attrs = NULL; gboolean found = FALSE; attrs = fwupd_client_get_bios_settings(priv->client, priv->cancellable, error); if (attrs == NULL) return FALSE; if (priv->as_json) return fu_util_bios_setting_console_print(priv->console, values, attrs, error); for (guint i = 0; i < attrs->len; i++) { FwupdBiosSetting *attr = g_ptr_array_index(attrs, i); if (fu_util_bios_setting_matches_args(attr, values)) { g_autofree gchar *tmp = fu_util_bios_setting_to_string(attr, 0); fu_console_print_literal(priv->console, tmp); found = TRUE; } } if (attrs->len == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, /* TRANSLATORS: error message */ _("This system doesn't support firmware settings")); return FALSE; } if (!found) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, /* TRANSLATORS: error message */ _("Unable to find attribute")); return FALSE; } return TRUE; } static gboolean fu_util_security_fix(FuUtilPrivate *priv, gchar **values, GError **error) { #ifndef HAVE_HSI g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, /* TRANSLATORS: error message for unsupported feature */ _("Host Security ID (HSI) is not supported")); return FALSE; #endif /* HAVE_HSI */ if (g_strv_length(values) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, /* TRANSLATOR: This is the error message for * incorrect parameter */ _("Invalid arguments, expected an AppStream ID")); return FALSE; } if (!fwupd_client_fix_host_security_attr(priv->client, values[0], priv->cancellable, error)) return FALSE; if (priv->as_json) return TRUE; /* TRANSLATORS: we've fixed a security problem on the machine */ fu_console_print_literal(priv->console, _("Fixed successfully")); return TRUE; } static gboolean fu_util_report_devices(FuUtilPrivate *priv, gchar **values, GError **error) { g_autofree gchar *data = NULL; g_autofree gchar *report_uri = NULL; g_autofree gchar *uri = NULL; g_autoptr(FwupdRemote) remote = NULL; g_autoptr(GHashTable) metadata = NULL; g_autoptr(GPtrArray) devices = NULL; /* we only know how to upload to the LVFS */ remote = fwupd_client_get_remote_by_id(priv->client, "lvfs", priv->cancellable, error); if (remote == NULL) return FALSE; report_uri = fwupd_remote_build_report_uri(remote, error); if (report_uri == NULL) return FALSE; /* include all the devices */ devices = fwupd_client_get_devices(priv->client, priv->cancellable, error); if (devices == NULL) return FALSE; metadata = fwupd_client_get_report_metadata(priv->client, priv->cancellable, error); if (metadata == NULL) return FALSE; data = fwupd_client_build_report_devices(priv->client, devices, metadata, error); if (data == NULL) return FALSE; if (priv->as_json) { if (!priv->assume_yes) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "pass --yes to enable uploads"); return FALSE; } } else { /* show the user the entire data blob */ fu_console_print_kv(priv->console, _("Target"), report_uri); fu_console_print_kv(priv->console, _("Payload"), data); fu_console_print(priv->console, /* TRANSLATORS: explain why we want to upload */ _("Uploading a device list allows the %s team to know what hardware " "exists, and allows us to put pressure on vendors that do not upload " "firmware updates for their hardware."), fwupd_remote_get_title(remote)); if (!fu_console_input_bool(priv->console, TRUE, "%s (%s)", /* TRANSLATORS: ask the user to upload */ _("Upload data now?"), /* TRANSLATORS: metadata is downloaded */ _("Requires internet connection"))) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO, "Declined upload"); return FALSE; } } /* send to the LVFS */ uri = fwupd_client_upload_report(priv->client, report_uri, data, NULL, FWUPD_CLIENT_UPLOAD_FLAG_ALWAYS_MULTIPART, priv->cancellable, error); if (uri == NULL) return FALSE; /* success */ if (!priv->as_json) { fu_console_print_literal(priv->console, /* TRANSLATORS: success, so say thank you to the user */ _("Device list uploaded successfully, thanks!")); } return TRUE; } static gboolean fu_util_security_undo(FuUtilPrivate *priv, gchar **values, GError **error) { #ifndef HAVE_HSI g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, /* TRANSLATORS: error message for unsupported feature */ _("Host Security ID (HSI) is not supported")); return FALSE; #endif /* HAVE_HSI */ if (g_strv_length(values) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, /* TRANSLATOR: This is the error message for * incorrect parameter */ _("Invalid arguments, expected an AppStream ID")); return FALSE; } if (!fwupd_client_undo_host_security_attr(priv->client, values[0], priv->cancellable, error)) return FALSE; if (priv->as_json) return TRUE; /* TRANSLATORS: we've fixed a security problem on the machine */ fu_console_print_literal(priv->console, _("Fix reverted successfully")); return TRUE; } static gboolean fu_util_emulation_tag(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FwupdDevice) dev = NULL; /* set the flag */ priv->filter_device_include |= FWUPD_DEVICE_FLAG_CAN_EMULATION_TAG; dev = fu_util_get_device_or_prompt(priv, values, error); if (dev == NULL) return FALSE; return fwupd_client_modify_device(priv->client, fwupd_device_get_id(dev), "Flags", "emulation-tag", priv->cancellable, error); } static gboolean fu_util_emulation_untag(FuUtilPrivate *priv, gchar **values, GError **error) { g_autoptr(FwupdDevice) dev = NULL; /* set the flag */ priv->filter_device_include |= FWUPD_DEVICE_FLAG_EMULATION_TAG; dev = fu_util_get_device_or_prompt(priv, values, error); if (dev == NULL) return FALSE; return fwupd_client_modify_device(priv->client, fwupd_device_get_id(dev), "Flags", "~emulation-tag", priv->cancellable, error); } static gboolean fu_util_emulation_save(FuUtilPrivate *priv, gchar **values, GError **error) { /* check args */ if (g_strv_length(values) != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments, expected FILENAME"); return FALSE; } /* save */ return fwupd_client_emulation_save(priv->client, values[0], priv->cancellable, error); } static gboolean fu_util_emulation_load(FuUtilPrivate *priv, gchar **values, GError **error) { /* check args */ if (g_strv_length(values) != 1) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS, "Invalid arguments, expected FILENAME"); return FALSE; } return fwupd_client_emulation_load(priv->client, values[0], priv->cancellable, error); } static gboolean fu_util_version(FuUtilPrivate *priv, GError **error) { g_autoptr(GHashTable) metadata = NULL; g_autofree gchar *str = NULL; /* get metadata */ metadata = fwupd_client_get_report_metadata(priv->client, priv->cancellable, error); if (metadata == NULL) return FALSE; /* dump to the screen in the most appropriate format */ if (priv->as_json) return fu_util_project_versions_as_json(priv->console, metadata, error); str = fu_util_project_versions_to_string(metadata); fu_console_print_literal(priv->console, str); return TRUE; } static gboolean fu_util_setup_interactive(FuUtilPrivate *priv, GError **error) { if (priv->as_json) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "using --json"); return FALSE; } return fu_console_setup(priv->console, error); } static void fu_util_cancelled_cb(GCancellable *cancellable, gpointer user_data) { FuUtilPrivate *priv = (FuUtilPrivate *)user_data; if (!g_main_loop_is_running(priv->loop)) return; /* TRANSLATORS: this is from ctrl+c */ fu_console_print_literal(priv->console, _("Cancelled")); g_main_loop_quit(priv->loop); } static void fu_util_print_error(FuUtilPrivate *priv, const GError *error) { if (priv->as_json) { fu_util_print_error_as_json(priv->console, error); return; } fu_console_print_full(priv->console, FU_CONSOLE_PRINT_FLAG_STDERR, "%s\n", error->message); } int main(int argc, char *argv[]) { /* nocheck:lines */ gboolean force = FALSE; gboolean allow_branch_switch = FALSE; gboolean allow_older = FALSE; gboolean allow_reinstall = FALSE; gboolean only_emulated = FALSE; gboolean only_p2p = FALSE; gboolean is_interactive = FALSE; gboolean no_history = FALSE; gboolean no_authenticate = FALSE; gboolean ret; gboolean verbose = FALSE; gboolean version = FALSE; guint download_retries = 0; g_autoptr(FuUtilPrivate) priv = g_new0(FuUtilPrivate, 1); g_autoptr(GDateTime) dt_now = g_date_time_new_now_utc(); g_autoptr(GError) error = NULL; g_autoptr(GError) error_console = NULL; g_autoptr(GPtrArray) cmd_array = fu_util_cmd_array_new(); #ifdef HAVE_POLKIT g_autoptr(FuPolkitAgent) polkit_agent = fu_polkit_agent_new(); #endif g_autofree gchar *cmd_descriptions = NULL; g_autofree gchar *filter_device = NULL; g_autofree gchar *filter_release = NULL; const GOptionEntry options[] = { {"verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose, /* TRANSLATORS: command line option */ N_("Show extra debugging information"), NULL}, {"version", '\0', 0, G_OPTION_ARG_NONE, &version, /* TRANSLATORS: command line option */ N_("Show client and daemon versions"), NULL}, {"download-retries", '\0', 0, G_OPTION_ARG_INT, &download_retries, /* TRANSLATORS: command line option */ N_("Set the download retries for transient errors"), NULL}, {"allow-reinstall", '\0', 0, G_OPTION_ARG_NONE, &allow_reinstall, /* TRANSLATORS: command line option */ N_("Allow reinstalling existing firmware versions"), NULL}, {"allow-older", '\0', 0, G_OPTION_ARG_NONE, &allow_older, /* TRANSLATORS: command line option */ N_("Allow downgrading firmware versions"), NULL}, {"allow-branch-switch", '\0', 0, G_OPTION_ARG_NONE, &allow_branch_switch, /* TRANSLATORS: command line option */ N_("Allow switching firmware branch"), NULL}, {"only-emulated", '\0', 0, G_OPTION_ARG_NONE, &only_emulated, /* TRANSLATORS: command line option */ N_("Only install onto emulated devices"), NULL}, {"force", '\0', 0, G_OPTION_ARG_NONE, &force, /* TRANSLATORS: command line option */ N_("Force the action by relaxing some runtime checks"), NULL}, {"assume-yes", 'y', 0, G_OPTION_ARG_NONE, &priv->assume_yes, /* TRANSLATORS: command line option */ N_("Answer yes to all questions"), NULL}, {"sign", '\0', 0, G_OPTION_ARG_NONE, &priv->sign, /* TRANSLATORS: command line option */ N_("Sign the uploaded data with the client certificate"), NULL}, {"no-unreported-check", '\0', 0, G_OPTION_ARG_NONE, &priv->no_unreported_check, /* TRANSLATORS: command line option */ N_("Do not check for unreported history"), NULL}, {"no-metadata-check", '\0', 0, G_OPTION_ARG_NONE, &priv->no_metadata_check, /* TRANSLATORS: command line option */ N_("Do not check for old metadata"), NULL}, {"no-remote-check", '\0', 0, G_OPTION_ARG_NONE, &priv->no_remote_check, /* TRANSLATORS: command line option */ N_("Do not check if download remotes should be enabled"), NULL}, {"no-reboot-check", '\0', 0, G_OPTION_ARG_NONE, &priv->no_reboot_check, /* TRANSLATORS: command line option */ N_("Do not check or prompt for reboot after update"), NULL}, {"no-safety-check", '\0', 0, G_OPTION_ARG_NONE, &priv->no_safety_check, /* TRANSLATORS: command line option */ N_("Do not perform device safety checks"), NULL}, {"no-device-prompt", '\0', 0, G_OPTION_ARG_NONE, &priv->no_device_prompt, /* TRANSLATORS: command line option */ N_("Do not prompt for devices"), NULL}, {"no-history", '\0', 0, G_OPTION_ARG_NONE, &no_history, /* TRANSLATORS: command line option */ N_("Do not write to the history database"), NULL}, {"show-all", '\0', 0, G_OPTION_ARG_NONE, &priv->show_all, /* TRANSLATORS: command line option */ N_("Show all results"), NULL}, {"show-all-devices", '\0', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &priv->show_all, /* TRANSLATORS: command line option */ N_("Show devices that are not updatable"), NULL}, {"disable-ssl-strict", '\0', 0, G_OPTION_ARG_NONE, &priv->disable_ssl_strict, /* TRANSLATORS: command line option */ N_("Ignore SSL strict checks when downloading files"), NULL}, {"p2p", '\0', 0, G_OPTION_ARG_NONE, &only_p2p, /* TRANSLATORS: command line option */ N_("Only use peer-to-peer networking when downloading files"), NULL}, {"filter", '\0', 0, G_OPTION_ARG_STRING, &filter_device, /* TRANSLATORS: command line option */ N_("Filter with a set of device flags using a ~ prefix to " "exclude, e.g. 'internal,~needs-reboot'"), NULL}, {"filter-release", '\0', 0, G_OPTION_ARG_STRING, &filter_release, /* TRANSLATORS: command line option */ N_("Filter with a set of release flags using a ~ prefix to " "exclude, e.g. 'trusted-release,~trusted-metadata'"), NULL}, {"json", '\0', 0, G_OPTION_ARG_NONE, &priv->as_json, /* TRANSLATORS: command line option */ N_("Output in JSON format (disables all interactive prompts)"), NULL}, {"no-security-fix", '\0', 0, G_OPTION_ARG_NONE, &priv->no_security_fix, /* TRANSLATORS: command line option */ N_("Do not prompt to fix security issues"), NULL}, {"no-authenticate", '\0', 0, G_OPTION_ARG_NONE, &no_authenticate, /* TRANSLATORS: command line option */ N_("Don't prompt for authentication (less details may be shown)"), NULL}, {NULL}}; FwupdFeatureFlags feature_flags = FWUPD_FEATURE_FLAG_CAN_REPORT | FWUPD_FEATURE_FLAG_SWITCH_BRANCH | FWUPD_FEATURE_FLAG_FDE_WARNING | FWUPD_FEATURE_FLAG_COMMUNITY_TEXT | FWUPD_FEATURE_FLAG_SHOW_PROBLEMS; #ifdef _WIN32 /* workaround Windows setting the codepage to 1252 */ (void)g_setenv("LANG", "C.UTF-8", FALSE); #endif setlocale(LC_ALL, ""); bindtextdomain(GETTEXT_PACKAGE, FWUPD_LOCALEDIR); bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8"); textdomain(GETTEXT_PACKAGE); g_set_prgname(fu_util_get_prgname(argv[0])); /* ensure D-Bus errors are registered */ (void)fwupd_error_quark(); /* create helper object */ priv->main_ctx = g_main_context_new(); priv->loop = g_main_loop_new(priv->main_ctx, FALSE); priv->console = fu_console_new(); priv->post_requests = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); fu_console_set_main_context(priv->console, priv->main_ctx); /* add commands */ fu_util_cmd_array_add(cmd_array, "get-devices,get-topology", NULL, /* TRANSLATORS: command description */ _("Get all devices that support firmware updates"), fu_util_get_devices); fu_util_cmd_array_add(cmd_array, "get-history", NULL, /* TRANSLATORS: command description */ _("Show history of firmware updates"), fu_util_get_history); fu_util_cmd_array_add(cmd_array, "report-history", NULL, /* TRANSLATORS: command description */ _("Share firmware history with the developers"), fu_util_report_history); fu_util_cmd_array_add(cmd_array, "report-export", NULL, /* TRANSLATORS: command description */ _("Export firmware history for manual upload"), fu_util_report_export); fu_util_cmd_array_add(cmd_array, "install", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[DEVICE-ID|GUID] [VERSION]"), /* TRANSLATORS: command description */ _("Install a specific firmware file on all devices that match"), fu_util_install); fu_util_cmd_array_add(cmd_array, "local-install", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("FILE [DEVICE-ID|GUID]"), /* TRANSLATORS: command description */ _("Install a firmware file in cabinet format on this hardware"), fu_util_local_install); fu_util_cmd_array_add(cmd_array, "get-details", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("FILE"), /* TRANSLATORS: command description */ _("Gets details about a firmware file"), fu_util_get_details); fu_util_cmd_array_add(cmd_array, "get-updates,get-upgrades", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[DEVICE-ID|GUID]"), /* TRANSLATORS: command description */ _("Gets the list of updates for connected hardware"), fu_util_get_updates); fu_util_cmd_array_add(cmd_array, "update,upgrade", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[DEVICE-ID|GUID]"), /* TRANSLATORS: command description */ _("Updates all specified devices to latest firmware version, or all " "devices if unspecified"), fu_util_update); fu_util_cmd_array_add(cmd_array, "verify", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[DEVICE-ID|GUID]"), /* TRANSLATORS: command description */ _("Checks cryptographic hash matches firmware"), fu_util_verify); fu_util_cmd_array_add(cmd_array, "unlock", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("DEVICE-ID|GUID"), /* TRANSLATORS: command description */ _("Unlocks the device for firmware access"), fu_util_unlock); fu_util_cmd_array_add(cmd_array, "clear-results", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("DEVICE-ID|GUID"), /* TRANSLATORS: command description */ _("Clears the results from the last update"), fu_util_clear_results); fu_util_cmd_array_add(cmd_array, "get-results", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("DEVICE-ID|GUID"), /* TRANSLATORS: command description */ _("Gets the results from the last update"), fu_util_get_results); fu_util_cmd_array_add(cmd_array, "get-releases", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[DEVICE-ID|GUID]"), /* TRANSLATORS: command description */ _("Gets the releases for a device"), fu_util_get_releases); fu_util_cmd_array_add(cmd_array, "get-remotes", NULL, /* TRANSLATORS: command description */ _("Gets the configured remotes"), fu_util_get_remotes); fu_util_cmd_array_add(cmd_array, "downgrade", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[DEVICE-ID|GUID]"), /* TRANSLATORS: command description */ _("Downgrades the firmware on a device"), fu_util_downgrade); fu_util_cmd_array_add(cmd_array, "refresh", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[FILE FILE_SIG REMOTE-ID]"), /* TRANSLATORS: command description */ _("Refresh metadata from remote server"), fu_util_refresh); fu_util_cmd_array_add(cmd_array, "verify-update", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[DEVICE-ID|GUID]"), /* TRANSLATORS: command description */ _("Update the stored cryptographic hash with current ROM contents"), fu_util_verify_update); fu_util_cmd_array_add(cmd_array, "modify-remote", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("REMOTE-ID KEY VALUE"), /* TRANSLATORS: command description */ _("Modifies a given remote"), fu_util_remote_modify); fu_util_cmd_array_add(cmd_array, "enable-remote", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("REMOTE-ID"), /* TRANSLATORS: command description */ _("Enables a given remote"), fu_util_remote_enable); fu_util_cmd_array_add(cmd_array, "disable-remote", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("REMOTE-ID"), /* TRANSLATORS: command description */ _("Disables a given remote"), fu_util_remote_disable); fu_util_cmd_array_add(cmd_array, "activate", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[DEVICE-ID|GUID]"), /* TRANSLATORS: command description */ _("Activate devices"), fu_util_activate); fu_util_cmd_array_add(cmd_array, "get-approved-firmware", NULL, /* TRANSLATORS: firmware approved by the admin */ _("Gets the list of approved firmware"), fu_util_get_approved_firmware); fu_util_cmd_array_add(cmd_array, "set-approved-firmware", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("FILENAME|CHECKSUM1[,CHECKSUM2][,CHECKSUM3]"), /* TRANSLATORS: firmware approved by the admin */ _("Sets the list of approved firmware"), fu_util_set_approved_firmware); fu_util_cmd_array_add(cmd_array, "modify-config", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[SECTION] KEY VALUE"), /* TRANSLATORS: sets something in the daemon configuration file */ _("Modifies a daemon configuration value"), fu_util_modify_config); fu_util_cmd_array_add(cmd_array, "reset-config", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("SECTION"), /* TRANSLATORS: sets something in the daemon configuration file */ _("Resets a daemon configuration section"), fu_util_reset_config); fu_util_cmd_array_add(cmd_array, "reinstall", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[DEVICE-ID|GUID]"), /* TRANSLATORS: command description */ _("Reinstall current firmware on the device"), fu_util_reinstall); fu_util_cmd_array_add(cmd_array, "switch-branch", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[DEVICE-ID|GUID] [BRANCH]"), /* TRANSLATORS: command description */ _("Switch the firmware branch on the device"), fu_util_switch_branch); fu_util_cmd_array_add(cmd_array, "security", NULL, /* TRANSLATORS: command description */ _("Gets the host security attributes"), fu_util_security); fu_util_cmd_array_add(cmd_array, "sync,sync-bkc", NULL, /* TRANSLATORS: command description */ _("Sync firmware versions to the chosen configuration"), fu_util_sync); fu_util_cmd_array_add(cmd_array, "block-firmware", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[CHECKSUM]"), /* TRANSLATORS: command description */ _("Blocks a specific firmware from being installed"), fu_util_block_firmware); fu_util_cmd_array_add(cmd_array, "unblock-firmware", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[CHECKSUM]"), /* TRANSLATORS: command description */ _("Unblocks a specific firmware from being installed"), fu_util_unblock_firmware); fu_util_cmd_array_add(cmd_array, "get-blocked-firmware", NULL, /* TRANSLATORS: command description */ _("Gets the list of blocked firmware"), fu_util_get_blocked_firmware); fu_util_cmd_array_add(cmd_array, "get-plugins", NULL, /* TRANSLATORS: command description */ _("Get all enabled plugins registered with the system"), fu_util_get_plugins); fu_util_cmd_array_add(cmd_array, "download", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("LOCATION"), /* TRANSLATORS: command description */ _("Download a file"), fu_util_download); fu_util_cmd_array_add(cmd_array, "device-test", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[FILENAME1] [FILENAME2]"), /* TRANSLATORS: command description */ _("Test a device using a JSON manifest"), fu_util_device_test); fu_util_cmd_array_add(cmd_array, "device-emulate", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[FILENAME1] [FILENAME2]"), /* TRANSLATORS: command description */ _("Emulate a device using a JSON manifest"), fu_util_device_emulate); fu_util_cmd_array_add(cmd_array, "inhibit", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[REASON] [TIMEOUT]"), /* TRANSLATORS: command description */ _("Inhibit the system to prevent upgrades"), fu_util_inhibit); fu_util_cmd_array_add(cmd_array, "uninhibit", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("INHIBIT-ID"), /* TRANSLATORS: command description */ _("Uninhibit the system to allow upgrades"), fu_util_uninhibit); fu_util_cmd_array_add(cmd_array, "device-wait", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("GUID|DEVICE-ID"), /* TRANSLATORS: command description */ _("Wait for a device to appear"), fu_util_device_wait); fu_util_cmd_array_add(cmd_array, "quit", NULL, /* TRANSLATORS: command description */ _("Asks the daemon to quit"), fu_util_quit); fu_util_cmd_array_add( cmd_array, "get-bios-settings,get-bios-setting", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[SETTING1] [SETTING2] [--no-authenticate]"), /* TRANSLATORS: command description */ _("Retrieve BIOS settings. If no arguments are passed all settings are returned"), fu_util_get_bios_setting); fu_util_cmd_array_add(cmd_array, "set-bios-setting", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("SETTING1 VALUE1 [SETTING2] [VALUE2]"), /* TRANSLATORS: command description */ _("Sets one or more BIOS settings"), fu_util_set_bios_setting); fu_util_cmd_array_add(cmd_array, "emulation-load", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("FILENAME"), /* TRANSLATORS: command description */ _("Load device emulation data"), fu_util_emulation_load); fu_util_cmd_array_add(cmd_array, "emulation-save", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("FILENAME"), /* TRANSLATORS: command description */ _("Save device emulation data"), fu_util_emulation_save); fu_util_cmd_array_add(cmd_array, "emulation-tag", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[DEVICE-ID|GUID]"), /* TRANSLATORS: command description */ _("Adds devices to watch for future emulation"), fu_util_emulation_tag); fu_util_cmd_array_add(cmd_array, "emulation-untag", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[DEVICE-ID|GUID]"), /* TRANSLATORS: command description */ _("Removes devices to watch for future emulation"), fu_util_emulation_untag); fu_util_cmd_array_add(cmd_array, "security-fix", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[APPSTREAM_ID]"), /* TRANSLATORS: command description */ _("Fix a specific host security attribute"), fu_util_security_fix); fu_util_cmd_array_add(cmd_array, "security-undo", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ _("[APPSTREAM_ID]"), /* TRANSLATORS: command description */ _("Undo the host security attribute fix"), fu_util_security_undo); fu_util_cmd_array_add(cmd_array, "report-devices", NULL, /* TRANSLATORS: command description */ _("Upload the list of updatable devices to a remote server"), fu_util_report_devices); /* do stuff on ctrl+c */ priv->cancellable = g_cancellable_new(); g_signal_connect(G_CANCELLABLE(priv->cancellable), "cancelled", G_CALLBACK(fu_util_cancelled_cb), priv); /* sort by command name */ fu_util_cmd_array_sort(cmd_array); /* non-TTY consoles cannot answer questions */ if (!fu_util_setup_interactive(priv, &error_console)) { g_info("failed to initialize interactive console: %s", error_console->message); priv->no_unreported_check = TRUE; priv->no_metadata_check = TRUE; priv->no_reboot_check = TRUE; priv->no_safety_check = TRUE; priv->no_remote_check = TRUE; priv->no_device_prompt = TRUE; priv->no_emulation_check = TRUE; priv->no_security_fix = TRUE; } else { is_interactive = TRUE; } fu_console_set_interactive(priv->console, is_interactive); /* get a list of the commands */ priv->context = g_option_context_new(NULL); cmd_descriptions = fu_util_cmd_array_to_string(cmd_array); g_option_context_set_summary(priv->context, cmd_descriptions); g_option_context_set_description( priv->context, /* TRANSLATORS: CLI description */ _("This tool allows an administrator to query and control the " "fwupd daemon, allowing them to perform actions such as " "installing or downgrading firmware.")); /* TRANSLATORS: program name */ g_set_application_name(_("Firmware Utility")); g_option_context_add_main_entries(priv->context, options, NULL); ret = g_option_context_parse(priv->context, &argc, &argv, &error); if (!ret) { fu_console_print(priv->console, "%s: %s", /* TRANSLATORS: the user didn't read the man page */ _("Failed to parse arguments"), error->message); return EXIT_FAILURE; } /* allow disabling SSL strict mode for broken corporate proxies */ if (priv->disable_ssl_strict) { fu_console_print_full(priv->console, FU_CONSOLE_PRINT_FLAG_WARNING, "%s\n", /* TRANSLATORS: try to help */ _("Ignoring SSL strict checks, " "to do this automatically in the future " "export DISABLE_SSL_STRICT in your environment")); (void)g_setenv("DISABLE_SSL_STRICT", "1", TRUE); } /* this doesn't have to be precise (e.g. using the build-year) as we just * want to check the clock is not set to the default of 1970-01-01... */ if (g_date_time_get_year(dt_now) < 2021) { fu_console_print_full( priv->console, FU_CONSOLE_PRINT_FLAG_WARNING, "%s\n", /* TRANSLATORS: try to help */ _("The system clock has not been set correctly and downloading " "files may fail.")); } /* parse filter flags */ if (filter_device != NULL) { if (!fu_util_parse_filter_device_flags(filter_device, &priv->filter_device_include, &priv->filter_device_exclude, &error)) { g_autofree gchar *str = /* TRANSLATORS: the user didn't read the man page, %1 is '--filter' */ g_strdup_printf(_("Failed to parse flags for %s"), "--filter"); g_prefix_error(&error, "%s: ", str); fu_util_print_error(priv, error); return EXIT_FAILURE; } } if (filter_release != NULL) { if (!fu_util_parse_filter_release_flags(filter_release, &priv->filter_release_include, &priv->filter_release_exclude, &error)) { g_autofree gchar *str = /* TRANSLATORS: the user didn't read the man page, * %1 is '--filter-release' */ g_strdup_printf(_("Failed to parse flags for %s"), "--filter-release"); g_prefix_error(&error, "%s: ", str); fu_util_print_error(priv, error); return EXIT_FAILURE; } } /* set verbose? */ if (verbose) { (void)g_setenv("G_MESSAGES_DEBUG", "all", FALSE); (void)g_setenv("FWUPD_VERBOSE", "1", FALSE); } else { g_log_set_handler(G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, fu_util_ignore_cb, NULL); } /* set up ctrl+c */ fu_util_setup_signal_handlers(priv); /* set flags */ if (allow_reinstall) priv->flags |= FWUPD_INSTALL_FLAG_ALLOW_REINSTALL; if (allow_older) priv->flags |= FWUPD_INSTALL_FLAG_ALLOW_OLDER; if (allow_branch_switch) priv->flags |= FWUPD_INSTALL_FLAG_ALLOW_BRANCH_SWITCH; if (only_emulated) priv->flags |= FWUPD_INSTALL_FLAG_ONLY_EMULATED; if (force) { priv->flags |= FWUPD_INSTALL_FLAG_FORCE; priv->flags |= FWUPD_INSTALL_FLAG_IGNORE_REQUIREMENTS; } if (no_history) priv->flags |= FWUPD_INSTALL_FLAG_NO_HISTORY; /* use peer-to-peer for metadata and firmware *only* if specified */ if (only_p2p) priv->download_flags |= FWUPD_CLIENT_DOWNLOAD_FLAG_ONLY_P2P; #ifdef HAVE_POLKIT /* start polkit tty agent to listen for password requests */ if (is_interactive) { g_autoptr(GError) error_polkit = NULL; if (!fu_polkit_agent_open(polkit_agent, &error_polkit)) { fu_console_print(priv->console, "Failed to open polkit agent: %s", error_polkit->message); } } #endif /* connect to the daemon */ priv->client = fwupd_client_new(); fwupd_client_set_main_context(priv->client, priv->main_ctx); fwupd_client_download_set_retries(priv->client, download_retries); g_signal_connect(FWUPD_CLIENT(priv->client), "notify::percentage", G_CALLBACK(fu_util_client_notify_cb), priv); g_signal_connect(FWUPD_CLIENT(priv->client), "notify::status", G_CALLBACK(fu_util_client_notify_cb), priv); g_signal_connect(FWUPD_CLIENT(priv->client), "device-changed", G_CALLBACK(fu_util_update_device_changed_cb), priv); g_signal_connect(FWUPD_CLIENT(priv->client), "device-request", G_CALLBACK(fu_util_update_device_request_cb), priv); /* show a warning if the daemon is tainted */ if (!fwupd_client_connect(priv->client, priv->cancellable, &error)) { #ifdef _WIN32 fu_console_print_literal( priv->console, /* TRANSLATORS: error message for Windows */ _("Failed to connect to Windows service, please ensure it's running.")); g_debug("%s", error->message); #else /* TRANSLATORS: could not contact the fwupd service over D-Bus */ g_prefix_error(&error, "%s: ", _("Failed to connect to daemon")); fu_util_print_error(priv, error); #endif return EXIT_FAILURE; } if (fwupd_client_get_tainted(priv->client)) { fu_console_print_full(priv->console, FU_CONSOLE_PRINT_FLAG_WARNING, "%s\n", /* TRANSLATORS: the user is SOL for support... */ _("The daemon has loaded 3rd party code and " "is no longer supported by the upstream developers!")); } /* just show versions and exit */ if (version) { if (!fu_util_version(priv, &error)) { fu_util_print_error(priv, error); return EXIT_FAILURE; } return EXIT_SUCCESS; } if (!priv->as_json) { /* show user-visible warnings from the plugins */ fu_util_show_plugin_warnings(priv); /* show any unsupported warnings */ fu_util_show_unsupported_warning(priv->console); } /* we know the runtime daemon version now */ fwupd_client_set_user_agent_for_package(priv->client, g_get_prgname(), PACKAGE_VERSION); /* check that we have at least this version daemon running */ if ((priv->flags & FWUPD_INSTALL_FLAG_FORCE) == 0 && !fu_util_check_daemon_version(priv, &error)) { fu_util_print_error(priv, error); return EXIT_FAILURE; } /* make sure polkit actions were installed */ if (!fu_util_check_polkit_actions(&error)) { fu_util_print_error(priv, error); return EXIT_FAILURE; } /* send our implemented feature set */ if (is_interactive) { feature_flags |= FWUPD_FEATURE_FLAG_REQUESTS | FWUPD_FEATURE_FLAG_REQUESTS_NON_GENERIC | FWUPD_FEATURE_FLAG_UPDATE_ACTION | FWUPD_FEATURE_FLAG_DETACH_ACTION; if (!no_authenticate) feature_flags |= FWUPD_FEATURE_FLAG_ALLOW_AUTHENTICATION; } if (!fwupd_client_set_feature_flags(priv->client, feature_flags, priv->cancellable, &error)) { /* TRANSLATORS: a feature is something like "can show an image" */ g_prefix_error(&error, "%s: ", _("Failed to set front-end features")); fu_util_print_error(priv, error); return EXIT_FAILURE; } /* run the specified command */ ret = fu_util_cmd_array_run(cmd_array, priv, argv[1], (gchar **)&argv[2], &error); if (!ret) { #ifdef SUPPORTED_BUILD /* sanity check */ if (error == NULL) { g_critical("exec failed but no error set!"); return EXIT_FAILURE; } #endif fu_util_print_error(priv, error); if (!priv->as_json && g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_ARGS)) { g_autofree gchar *cmd = g_strdup_printf("%s --help", g_get_prgname()); g_autoptr(GString) str = g_string_new("\n"); /* TRANSLATORS: explain how to get help, * where $1 is something like 'fwupdmgr --help' */ g_string_append_printf(str, _("Use %s for help"), cmd); fu_console_print_literal(priv->console, str->str); } else if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO)) return EXIT_NOTHING_TO_DO; else if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_REACHABLE)) return EXIT_NOT_REACHABLE; else if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) return EXIT_NOT_FOUND; return EXIT_FAILURE; } /* success */ return EXIT_SUCCESS; } fwupd-2.0.10/src/fwupd-windows.mc000066400000000000000000000003271501337203100166410ustar00rootroot00000000000000MessageId=0x0 SymbolicName=FWUPD_CATEGORY_GENERIC Language=English Generic Events . MessageId=0x100 SymbolicName=FWUPD_MESSAGE_GENERIC Language=English No specific message -- see the EventData in the Details tab . fwupd-2.0.10/src/fwupd.gresource.xml000066400000000000000000000003251501337203100173450ustar00rootroot00000000000000 org.freedesktop.fwupd.xml fwupd-2.0.10/src/fwupdmgr.md000066400000000000000000000037131501337203100156620ustar00rootroot00000000000000--- title: fwupdmgr client command line utility --- % fwupdmgr(1) {{PACKAGE_VERSION}} | fwupdmgr man page ## NAME **fwupdmgr** — firmware update manager client utility ## SYNOPSIS | **fwupdmgr** [CMD] ## DESCRIPTION `fwupdmgr` is a command line fwupd client intended to be used interactively. The terminal output between versions of fwupd is not guaranteed to be stable, but if you plan on parsing the results then adding **\-\-json** might be just what you need. There are also graphical tools to firmware available for various desktop environments. These applications may be more useful to many users compared to using the command line. * **GNOME Software**: * **GNOME Firmware**: * **KDE Discover**: * **Canonical Firmware Updater**: * **System76 Firmware Manager**: On most systems fwupd is configured to download metadata from the Linux Vendor Firmware Service and more information about the LVFS is available here: Most users who want to just update all devices to the latest versions can do `fwupdmgr refresh` and then `fwupdmgr update`. At this point the system will asking for confirmation, update some devices, and may then reboot to deploy updates that require a restart. ## OPTIONS The fwupdmgr command takes various options depending on the action. Run **fwupdmgr \-\-help** for the full list. ## EXIT STATUS Commands that successfully execute will return "0", with generic failure as "1". There are also several other exit codes used: A return code of "2" is used for commands that have no actions but were successfully executed, and "3" is used when a resource was not found. ## BUGS See GitHub Issues: ## SEE ALSO fwupd-2.0.10/src/fwupdtool.md000066400000000000000000000025321501337203100160500ustar00rootroot00000000000000--- title: fwupdtool standalone command line utility --- % fwupdtool(1) {{PACKAGE_VERSION}} | standalone firmware update utility man page ## NAME **fwupdtool** — standalone firmware update utility ## SYNOPSIS | **fwupdtool** [CMD] ## DESCRIPTION This tool allows an administrator to use fwupd plugins directly without using the daemon process, which may be faster or easier to use when creating or debugging specific plugins. For most end-users, **fwupdmgr** is a more suitable program to use in almost all cases. Additionally **fwupdtool** can be used to convert firmware from various different formats, or to modify the images contained inside the container firmware file. For example, you can convert DFU or Intel HEX firmware into the vendor-specific format. ## OPTIONS The fwupdtool command takes various options depending on the action. Run **fwupdtool \-\-help** for the full list. Note that some runtimes failures can be ignored using **\-\-force**. ## EXIT STATUS Commands that successfully execute will return "0", with generic failure as "1". There are also several other exit codes used: A return code of "2" is used for commands that have no actions but were successfully executed, and "3" is used when a resource was not found. ## BUGS See GitHub Issues: ## SEE ALSO fwupd-2.0.10/src/meson.build000066400000000000000000000176431501337203100156560ustar00rootroot00000000000000if get_option('tests') subdir('tests') endif client_src = [] systemd_src = [] engine_dep = [ libarchive, libjcat, libxmlb, giounix, gmodule, libusb, libjsonglib, polkit, sqlite, cbor, fwupdplugin_rs_dep, ] if get_option('passim').allowed() engine_dep += passim endif client_dep = [ libcurl, libjcat, libjsonglib, libxmlb, sqlite, readline, fwupdplugin_rs_dep, ] if libsystemd.found() systemd_src += 'fu-systemd.c' endif if polkit.found() client_src += 'fu-polkit-agent.c' endif fwupd_engine_src = [ 'fu-cabinet.c', 'fu-debug.c', 'fu-device-list.c', 'fu-engine.c', 'fu-engine-config.c', 'fu-engine-emulator.c', 'fu-engine-helper.c', 'fu-engine-request.c', 'fu-history.c', 'fu-idle.c', 'fu-polkit-authority.c', 'fu-release.c', 'fu-engine-requirements.c', 'fu-release-common.c', 'fu-plugin-list.c', 'fu-remote.c', 'fu-remote-list.c', 'fu-security-attr-common.c', 'fu-uefi-backend.c', 'fu-usb-backend.c', 'fu-client.c', 'fu-client-list.c', ] + systemd_src if giounix.found() fwupd_engine_src += 'fu-unix-seekable-input-stream.c' endif if host_machine.system() == 'linux' fwupd_engine_src += 'fu-udev-backend.c' endif if bluez.allowed() fwupd_engine_src += 'fu-bluez-backend.c' endif # include event message file if host_machine.system() == 'windows' windmc = find_program('windmc') fwupd_rc = custom_target('fwupd-rc', input: 'fwupd-windows.mc', output: 'fwupd-windows.rc', command: [ windmc, '@INPUT@', '--rcdir', meson.current_build_dir(), ], ) windows = import('windows') fwupd_engine_src += windows.compile_resources(fwupd_rc) endif fwupdutil = library( 'fwupdutil', sources: [ 'fu-console.c', 'fu-util-bios-setting.c', 'fu-util-common.c', systemd_src, ], install: true, install_rpath: libdir_pkg, install_dir: libdir_pkg, include_directories: [ root_incdir, fwupd_incdir, fwupdplugin_incdir, ], dependencies: [ client_dep, ], link_with: [ fwupd, fwupdplugin, ], ) dbus_interface = custom_target('fwupd-generate-dbus-interface', input : 'org.freedesktop.fwupd.xml', output : 'org.freedesktop.fwupd.xml', command : [ generate_dbus_interface, '@INPUT@', '@OUTPUT@', ], install: build_daemon, install_dir: join_paths(datadir, 'dbus-1', 'interfaces'), ) if build_daemon fwupdmgr = executable( 'fwupdmgr', sources: [ 'fu-util.c', client_src, ], include_directories: [ root_incdir, fwupd_incdir, fwupdplugin_incdir, ], dependencies: [ libfwupd_deps, client_dep, ], link_with: [ fwupd, fwupdplugin, fwupdutil, ], install: true, install_rpath: libdir_pkg, install_dir: bindir ) endif resources_src = gnome.compile_resources( 'fwupd-resources', 'fwupd.gresource.xml', c_name: 'fu', dependencies: [dbus_interface], ) # generate a header file that allows us to instantiate the plugins without copy-pasting or # duplicating the meson build logic in the engine plugin_names = [] foreach lib : plugin_builtins plugin_names += lib.full_path() endforeach plugins_hdr = custom_target('fwupd-generate-plugins-header', output : 'fu-plugin-builtin.h', command : [ generate_plugins_header, '@OUTPUT@', '.', ','.join(plugin_names), ], ) # build all the plugins and engine into one installed library fwupdengine_rs = custom_target('fu-engine-rs', input: 'fu-engine.rs', output: ['fu-engine-struct.c', 'fu-engine-struct.h'], command: [ python3, join_paths(meson.project_source_root(), 'libfwupdplugin', 'rustgen.py'), '@INPUT@', '@OUTPUT0@', '@OUTPUT1@', ], ) fwupdengine = library( 'fwupdengine', fwupdengine_rs, resources_src, plugins_hdr, sources: fwupd_engine_src, install: true, install_rpath: libdir_pkg, install_dir: libdir_pkg, include_directories: plugin_incdirs, dependencies: [ engine_dep, ], link_whole: [ plugin_builtins, ], link_with: [ fwupd, fwupdplugin, ], ) fwupdtool = executable( 'fwupdtool', fwupdengine_rs, plugins_hdr, export_dynamic: true, sources: [ 'fu-tool.c', ], include_directories: [ root_incdir, fwupd_incdir, fwupdplugin_incdir, ], dependencies: [ libfwupd_deps, libarchive, client_dep, valgrind, ], link_with: [ fwupdengine, fwupdutil, plugin_libs, ], install: true, install_rpath: libdir_pkg, install_dir: bindir ) if build_daemon if get_option('man') custom_target('fwupdmgr.1', input: 'fwupdmgr.md', output: 'fwupdmgr.1', command: [ generate_man, '@INPUT@', '-o', '@OUTPUT@', '--replace', 'PACKAGE_VERSION', fwupd_version, ], install: true, install_dir: join_paths(mandir, 'man1'), ) endif if build_docs md_targets += custom_target('fwupdmgr.md', input: 'fwupdmgr.md', output: 'fwupdmgr.md', command: [ generate_man, '@INPUT@', '-o', '@OUTPUT@', '--replace', 'PACKAGE_VERSION', fwupd_version, '--md', ], ) man_md += ['"fwupdmgr.md"'] endif endif if build_standalone if get_option('man') custom_target('fwupdtool.1', input: 'fwupdtool.md', output: 'fwupdtool.1', command: [ generate_man, '@INPUT@', '-o', '@OUTPUT@', '--replace', 'PACKAGE_VERSION', fwupd_version, ], install: true, install_dir: join_paths(mandir, 'man1'), ) endif if build_docs md_targets += custom_target('fwupdtool.md', input: 'fwupdtool.md', output: 'fwupdtool.md', command: [ generate_man, '@INPUT@', '-o', '@OUTPUT@', '--replace', 'PACKAGE_VERSION', fwupd_version, '--md', ], ) man_md += ['"fwupdtool.md"'] endif endif if build_daemon # the StartServiceCtrlDispatcherA design is so different use a different source file if host_machine.system() == 'windows' daemon_loader_src = 'fu-main-windows.c' else daemon_loader_src = 'fu-main.c' endif fwupddaemon_rs = custom_target('fu-daemon-rs', input: 'fu-daemon.rs', output: ['fu-daemon-struct.c', 'fu-daemon-struct.h'], command: [ python3, join_paths(meson.project_source_root(), 'libfwupdplugin', 'rustgen.py'), '@INPUT@', '@OUTPUT0@', '@OUTPUT1@', ], ) executable( 'fwupd', fwupdengine_rs, fwupddaemon_rs, plugins_hdr, sources: [ daemon_loader_src, 'fu-daemon.c', 'fu-dbus-daemon.c', ], include_directories: [ root_incdir, fwupd_incdir, fwupdplugin_incdir, ], dependencies: [ valgrind, libsystemd, engine_dep, ], link_with: [ fwupdengine, plugin_libs, ], install: true, install_rpath: libdir_pkg, install_dir: daemon_dir ) endif if get_option('tests') env = environment() env.set('G_DEBUG', 'fatal-criticals') env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) env.set('FWUPD_LOCALSTATEDIR', '/tmp/fwupd-self-test/var') e = executable( 'fu-self-test', fwupdengine_rs, colorhug_test_firmware, hwid_test_firmware, multiple_rels_test_firmware, noreqs_test_firmware, plugins_hdr, firmware_xml_gz_jcat, sources: [ 'fu-self-test.c', ], include_directories: [ root_incdir, fwupd_incdir, fwupdplugin_incdir, ], dependencies: [ engine_dep, ], link_with: [ fwupdengine, fwupdutil, plugin_libs, ], c_args: [ '-DSRCDIR="' + meson.current_source_dir() + '"', ], ) test('fu-self-test', e, is_parallel: false, timeout: 180, env: env) if polkit.found() e = executable( 'fu-polkit-test', sources: [ 'fu-polkit-test.c', 'fu-polkit-agent.c', ], include_directories: [ root_incdir, fwupd_incdir, ], dependencies: [ libfwupd_deps, ], link_with: [ fwupd, ], ) test('fu-self-test', e, env: {'G_DEBUG': 'fatal-criticals'}) endif endif fwupd-2.0.10/src/org.freedesktop.fwupd.xml000066400000000000000000001042121501337203100204500ustar00rootroot00000000000000 The interface used for querying firmware for the system. The daemon version. The optional best known configuration to use when syncing back to a known state, e.g. vendor-factory-2021q1,mycompany-2023. The vendor name string for the host. The product name string for the host. The machine ID for the host. The Host Security ID, for instance HSI:2 If the daemon has been tainted with a 3rd party plugin. If the daemon is running on an interactive terminal. The daemon status, e.g. decompressing. The job percentage completion, or 0 for unknown. Returns the system battery level, or 101 for unknown. Returns the system battery threshold under which a firmware update cannot be performed. If the daemon requires trusted payloads. Gets a list of all the devices that are supported. An array of devices, with any properties set on each. Gets a list of all the plugins being used by the daemon. An array of plugins, with any properties set on each. Gets a list of all the releases for a specific device. A device ID. An array of releases (with the release number as the key), with any properties set on each. Gets a list of all the downgrades possible for a specific device. A device ID. An array of releases (with the release number as the key), with any properties set on each. Gets a list of all the upgrades possible for a specific device. A device ID. An array of releases (with the release number as the key), with any properties set on each. Gets details about a local firmware file. An index into the array of file descriptors that may have been sent with the DBus message. An array of results, with any properties set on each. Gets a list of all the past firmware updates. An array of devices, with any properties set on each. Gets a list of all the Host Security ID attributes. An array of HSI attributes, with any properties set on each. Gets a list of all the Host Security ID events. The maximum number of events, or 0 for no limit. An array of HSI attributes, with any properties set on each. Gets metadata to include with the firmware and security reports. An array of string key values. Sets optional hints from the client that may affect the list of devices. A typical hint might be locale and unknown hints should be ignored. An array of string key values. Schedules a firmware to be installed. An ID, typically a GUID of the hardware to update, or the string * to match any applicable hardware. An index into the array of file descriptors that may have been sent with the DBus message. Options to be used when constructing the profile, e.g. offline=True. Verifies firmware on a device by reading it back and performing a cryptographic hash, typically SHA1. An ID, typically a GUID of the hardware. Updates the cryptographic hash stored for a device. An ID, typically a GUID of the hardware. Unlock the device to allow firmware access. An ID, typically a GUID of the hardware. Activate a firmware update on the device. An ID, typically the sha hash of the device string. Gets the results of an offline update. An ID, typically a GUID of the hardware that was updated, or the string * to match any hardware. Results about the update, e.g. success=True Gets the list of remotes. The array remotes, with properties Gets the list of approved firmware that can be applied to devices. In an enterprise this will be configured by a domain administrator. The checksums of the archives Sets the list of approved firmware that can be applied to devices. In an enterprise this will be configured by a domain administrator. The checksums of the archives Gets the list of blocked firmware. The checksums of the archives Sets the list of blocked firmware that can be applied to devices. The checksums of the archives Sets the features the client supports. This allows firmware to depend on specific front-end features, for instance showing the user an image on how to detach the hardware. The features the front end supports Clears the results of an offline update. An ID, typically a GUID of the hardware that was updated, or the string * to match any hardware. Modifies a remote in some way. A device ID, or the string * to match any hardware. The key, e.g. 'Flags'. The value of the correct type, e.g. a URL. Modify persistent configuration for daemon The config section, e.g. 'fwupd'. The key, e.g. 'DisabledPlugins'. The value of the correct type, e.g. a URL. Reset persistent configuration for daemon The config section, e.g. 'fwupd'. Adds AppStream resource information from a session client. Remote ID to tag the metadata objects with, e.g. 'lvfs-testing'. File handle to AppStream metadata. File handle to AppStream metadata GPG signature. Modifies a remote in some way. Remote ID, e.g. 'lvfs-testing'. The key, e.g. 'Enabled'. The value of the correct type, e.g. a URL. Fix a specific HSI attribute. The fwupd AppStream ID, e.g. 'org.fwupd.hsi.Kernel.Lockdown'. Revert the fix for a specific HSI attribute. The fwupd AppStream ID, e.g. 'org.fwupd.hsi.Kernel.Lockdown'. Signs some text, typically using a self-signed PKCS-7 certificate. String input data, certainly *NOT* binary data. Options to be used when signing, e.g. add-cert=True or add-timestamp=True. The detached signature string. Modify BIOS setting An array of BIOS settings and their new values. Gets a list of all the BIOS settings. An array of BIOS settings, with any properties set on each. Marks the system as unavailable for update. A reason, e.g. device is being used to capture content. The token that was used for inhibiting. Removes the inhibit token added by the application, but only if there is no other inhibit imposed by other applications or by the system (e.g. low power state). The token to use for uninhibiting. Ask the daemon to quit. This can only be called by the root user. Load emulation data. An index into the array of file descriptors that has been sent with the DBus message. JSON data of each phase packaged as a ZIP archive (e.g. setup.json, install.json, reload.json). Return emulation data. An index into the array of file descriptors that has been sent with the DBus message. The compressed ZIP archive will be written to the file descriptor on success. JSON data of each phase packaged as a ZIP archive (e.g. setup.json, install.json, reload.json). Some value on the interface or the number of devices or profiles has changed. A device structure. A device has been added. A device structure. A device has been removed. A device structure. A device has been changed. A device request. A device request to the client. fwupd-2.0.10/src/tests/000077500000000000000000000000001501337203100146435ustar00rootroot00000000000000fwupd-2.0.10/src/tests/auth.conf000066400000000000000000000004151501337203100164530ustar00rootroot00000000000000[fwupd Remote] Enabled=true MetadataURI=https://cdn.fwupd.org/downloads/firmware.xml.gz ReportURI=https://fwupd.org/lvfs/firmware/report AutomaticSecurityReports=true Username=user Password=pass OrderBefore=before OrderAfter=after FirmwareBaseURI=https://my.fancy.cdn/ fwupd-2.0.10/src/tests/auth/000077500000000000000000000000001501337203100156045ustar00rootroot00000000000000fwupd-2.0.10/src/tests/auth/firmware.xml.gz000066400000000000000000000000141501337203100205540ustar00rootroot00000000000000hello world fwupd-2.0.10/src/tests/auth/meson.build000066400000000000000000000010051501337203100177420ustar00rootroot00000000000000jcat_tool = find_program('jcat-tool', required: false) if jcat_tool.found() firmware_xml_gz_jcat = custom_target('firmware-xml-gz-jcat', input: [ 'firmware.xml.gz', ], output: 'firmware.xml.gz.jcat', command: [ jcat_tool, '--basename', '--appstream-id', 'localhost', 'self-sign', '@OUTPUT@', '@INPUT@', ], ) else firmware_xml_gz_jcat = custom_target('firmware-xml-gz-jcat', input: [ 'firmware.xml.gz', ], output: 'firmware.xml.gz.jcat', command: [ 'touch', '@OUTPUT@', ], ) endif fwupd-2.0.10/src/tests/bios-attrs/000077500000000000000000000000001501337203100167325ustar00rootroot00000000000000fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/000077500000000000000000000000001501337203100216715ustar00rootroot00000000000000fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/000077500000000000000000000000001501337203100240575ustar00rootroot00000000000000fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/Absolute/000077500000000000000000000000001501337203100256355ustar00rootroot00000000000000fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/Absolute/current_value000066400000000000000000000000101501337203100304250ustar00rootroot00000000000000Disabledfwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/Absolute/default_value000066400000000000000000000000101501337203100303670ustar00rootroot00000000000000Enabled fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/Absolute/dell_modifier000066400000000000000000000000011501337203100303450ustar00rootroot00000000000000 fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/Absolute/dell_value_modifier000066400000000000000000000000011501337203100315410ustar00rootroot00000000000000 fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/Absolute/display_name000066400000000000000000000000111501337203100302150ustar00rootroot00000000000000Absolute fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/Absolute/display_name_language_code000066400000000000000000000000061501337203100330560ustar00rootroot00000000000000en-US fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/Absolute/possible_values000066400000000000000000000000461501337203100307570ustar00rootroot00000000000000Enabled;Disabled;PermanentlyDisabled; fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/Absolute/type000066400000000000000000000000141501337203100265340ustar00rootroot00000000000000enumeration fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/Asset/000077500000000000000000000000001501337203100251365ustar00rootroot00000000000000fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/Asset/current_value000066400000000000000000000000011501337203100277260ustar00rootroot000000000000001fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/Asset/default_value000066400000000000000000000000121501337203100276720ustar00rootroot00000000000000Asset Tag fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/Asset/dell_modifier000066400000000000000000000000011501337203100276460ustar00rootroot00000000000000 fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/Asset/display_name000066400000000000000000000000121501337203100275170ustar00rootroot00000000000000Asset Tag fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/Asset/display_name_language_code000066400000000000000000000000061501337203100323570ustar00rootroot00000000000000en-US fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/Asset/max_length000066400000000000000000000000031501337203100272000ustar00rootroot0000000000000064 fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/Asset/min_length000066400000000000000000000000021501337203100271750ustar00rootroot000000000000001 fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/Asset/type000066400000000000000000000000071501337203100260370ustar00rootroot00000000000000string fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/CustomChargeStop/000077500000000000000000000000001501337203100273115ustar00rootroot00000000000000fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/CustomChargeStop/current_value000066400000000000000000000000031501337203100321030ustar00rootroot0000000000000070 fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/CustomChargeStop/default_value000066400000000000000000000000031501337203100320450ustar00rootroot0000000000000090 fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/CustomChargeStop/dell_modifier000066400000000000000000000000011501337203100320210ustar00rootroot00000000000000 fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/CustomChargeStop/display_name000066400000000000000000000000231501337203100316740ustar00rootroot00000000000000Custom Charge Stop display_name_language_code000066400000000000000000000000061501337203100344530ustar00rootroot00000000000000fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/CustomChargeStopen-US fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/CustomChargeStop/max_value000066400000000000000000000000041501337203100312070ustar00rootroot00000000000000100 fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/CustomChargeStop/min_value000066400000000000000000000000031501337203100312040ustar00rootroot0000000000000055 fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/CustomChargeStop/scalar_increment000066400000000000000000000000021501337203100325350ustar00rootroot000000000000001 fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/CustomChargeStop/type000066400000000000000000000000101501337203100302040ustar00rootroot00000000000000integer fwupd-2.0.10/src/tests/bios-attrs/fwupd-internal/attributes/pending_reboot000066400000000000000000000000021501337203100267700ustar00rootroot000000000000000 fwupd-2.0.10/src/tests/conf-migration-1.7/000077500000000000000000000000001501337203100200625ustar00rootroot00000000000000fwupd-2.0.10/src/tests/conf-migration-1.7/fwupd/000077500000000000000000000000001501337203100212075ustar00rootroot00000000000000fwupd-2.0.10/src/tests/conf-migration-1.7/fwupd/daemon.conf000066400000000000000000000036121501337203100233230ustar00rootroot00000000000000[fwupd] # Allow blocking specific devices by their GUID # Uses semicolons as delimiter DisabledDevices= # Allow blocking specific plugins # Uses semicolons as delimiter DisabledPlugins=test;test_ble;invalid # Maximum archive size that can be loaded in Mb, with 0 for the default ArchiveSizeMax=0 # Idle time in seconds to shut down the daemon -- note some plugins might # inhibit the auto-shutdown, for instance thunderbolt. # # A value of 0 specifies 'never' IdleTimeout=7200 # Comma separated list of domains to log in verbose mode # If unset, no domains # If set to FuValue, FuValue domain (same as --domain-verbose=FuValue) # If set to *, all domains (same as --verbose) VerboseDomains= # Update the message of the day (MOTD) on device and metadata changes UpdateMotd=true # For some plugins, enumerate only devices supported by metadata EnumerateAllDevices=false # A list of firmware checksums that has been approved by the site admin # If unset, all firmware is approved ApprovedFirmware= # Allow blocking specific devices by their checksum, either SHA1 or SHA256 # Uses semicolons as delimiter BlockedFirmware= # Allowed URI schemes in the preference order; failed downloads from the first # scheme will be retried with the next in order until no choices remain. # # If unset or no schemes are listed, the default will be: file,https,http,ipfs UriSchemes= # Ignore power levels of devices when running updates IgnorePower=false # Only support installing firmware signed with a trusted key OnlyTrusted=true # A host best known configuration is used when using `fwupdmgr sync` which can # downgrade firmware to factory versions or upgrade firmware to a supported # config level. e.g. `vendor-factory-2021q1` HostBkc= # these are only required when the SMBIOS or Device Tree data is invalid or missing #Manufacturer= #ProductName= #ProductSku= #Family= #EnclosureKind= #BaseboardProduct= #BaseboardManufacturer= fwupd-2.0.10/src/tests/conf-migration-1.7/fwupd/msr.conf000066400000000000000000000001361501337203100226570ustar00rootroot00000000000000[msr] # Minimum kernel version to allow probing for sme flag MinimumSmeKernelVersion=5.18.0 fwupd-2.0.10/src/tests/conf-migration-1.7/fwupd/redfish.conf000066400000000000000000000007461501337203100235110ustar00rootroot00000000000000[redfish] # The URI to the Redfish service in the format ://: # ex: https://192.168.0.133:443 #Uri= # The username and password to the Redfish service #Username= #Password= # Whether to verify the server certificate or not # Expected value: TRUE or FALSE # Default: FALSE #CACheck= # Do not use IPMI KCS to create an initial user account if no SMBIOS data IpmiDisableCreateUser=False # Amount of time in seconds to wait for a BMC restart ManagerResetTimeout=1800 fwupd-2.0.10/src/tests/conf-migration-1.7/fwupd/thunderbolt.conf000066400000000000000000000004221501337203100244060ustar00rootroot00000000000000[thunderbolt] # Minimum kernel version to allow use of this plugin # It's important that all backports from this kernel have been # made if using an older kernel MinimumKernelVersion=4.13.0 # Forces delaying activation until shutdown/logout/reboot DelayedActivation=false fwupd-2.0.10/src/tests/conf-migration-1.7/fwupd/uefi_capsule.conf000066400000000000000000000011571501337203100245260ustar00rootroot00000000000000[uefi_capsule] # use GRUB to launch fwupdx64.efi #EnableGrubChainLoad=false # the shim loader is required to chainload the fwupd EFI binary unless # the fwupd.efi file has been self-signed manually #DisableShimForSecureBoot=true # amount of free space required on the ESP, for example using 0x2000000 for 32Mb #RequireESPFreeSpace= # with the UEFI removable path enabled, the default esp path is set to /EFI/boot # the shim EFI binary and presumably this is $ESP/EFI/boot/bootx64.efi #FallbacktoRemovablePath=false # allow ignoring the CapsuleOnDisk support advertised by the firmware #DisableCapsuleUpdateOnDisk=true fwupd-2.0.10/src/tests/dell-esrt.conf000066400000000000000000000003511501337203100174040ustar00rootroot00000000000000[fwupd Remote] # this remote provides metadata shipped with the fwupd package Enabled=true Title=Enable UEFI capsule updates on Dell systems MetadataURI=file://@datadir@/fwupd/remotes.d/dell-esrt/firmware.xml ApprovalRequired=false fwupd-2.0.10/src/tests/disabled.conf000066400000000000000000000000471501337203100172620ustar00rootroot00000000000000[fwupd Remote] Enabled=false Password= fwupd-2.0.10/src/tests/efi/000077500000000000000000000000001501337203100154065ustar00rootroot00000000000000fwupd-2.0.10/src/tests/efi/mok-variables/000077500000000000000000000000001501337203100201425ustar00rootroot00000000000000fwupd-2.0.10/src/tests/efi/mok-variables/HSIStatus000077700000000000000000000000001501337203100326142../../../../plugins/uefi-mok/tests/HSIStatus-nx-validustar00rootroot00000000000000fwupd-2.0.10/src/tests/fakedevice123.bin000077700000000000000000000000001501337203100253532../../data/tests/fakedevice123.binustar00rootroot00000000000000fwupd-2.0.10/src/tests/firmware-base-uri.conf000066400000000000000000000002321501337203100210300ustar00rootroot00000000000000[fwupd Remote] Enabled=true Type=download MetadataURI=https://s3.amazonaws.com/lvfsbucket/downloads/firmware.xml.gz FirmwareBaseURI=https://my.fancy.cdn/ fwupd-2.0.10/src/tests/firmware-nopath.conf000066400000000000000000000001641501337203100206160ustar00rootroot00000000000000[fwupd Remote] Enabled=true Type=download MetadataURI=https://s3.amazonaws.com/lvfsbucket/downloads/firmware.xml.gz fwupd-2.0.10/src/tests/fwupd.conf000066400000000000000000000002601501337203100166350ustar00rootroot00000000000000[fwupd] TrustedReports=VendorId=123&DistroId=fedora&RemoteId=lvfs&DistroVariant=workstation;VendorId=$OEM&DistroId=chromeos&Flags=is-upgrade Manufacturer=ACME TestDevices=true fwupd-2.0.10/src/tests/history_v1.db000066400000000000000000000300001501337203100172520ustar00rootroot00000000000000SQLite format 3@ .á ø==Ƀ …itablehistoryhistoryCREATE TABLE history ( device_id TEXT PRIMARY KEY, update_state INTEGER DEFAULT 0, update_error TEXT, filename TEXT, display_name TEXT, plugin TEXT, device_created INTEGER DEFAULT 0, device_modified INTEGER DEFAULT 0, checksum TEXT DEFAULT NULL, flags INTEGER DEFAULT 0, metadata TEXT DEFAULT NULL, guid_default TEXT DEFAULT NULL, version_old TEXT, version_new TEXT)-Aindexsqlite_autoindex_history_1history ÇÇ7]2ba16d10df45823dd4494ff10a0bfccfef512c9d ÔÔ+] 2ba16d10df45823dd4494ff10a0bfccfef512c9dfwupd-2.0.10/src/tests/history_v2.db000066400000000000000000000500001501337203100172550ustar00rootroot00000000000000SQLite format 3@ .jÐ / = ¯ = = =‚o…5tablehistoryhistoryCREATE TABLE history (device_id TEXT,update_state INTEGER DEFAULT 0,update_error TEXT,filename TEXT,display_name TEXT,plugin TEXT,device_created INTEGER DEFAULT 0,device_modified INTEGER DEFAULT 0,checksum TEXT DEFAULT NULL,flags INTEGER DEFAULT 0,metadata TEXT DEFAULT NULL,guid_default TEXT DEFAULT NULL,version_old TEXT,version_new TEXT)~WtableschemaschemaCREATE TABLE schema (created timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,version INTEGER DEFAULT 0)Ñ çç32024-02-19 10:09:46 ÇÇ7]2ba16d10df45823dd4494ff10a0bfccfef512c9dfwupd-2.0.10/src/tests/host-emulate/000077500000000000000000000000001501337203100172525ustar00rootroot00000000000000fwupd-2.0.10/src/tests/host-emulate/meson.build000066400000000000000000000006111501337203100214120ustar00rootroot00000000000000if build_standalone gzip = find_program('gzip') foreach input_file : [ 'thinkpad-p1-iommu.json', ] custom_target(input_file, input: input_file, output: '@0@.gz'.format(input_file), capture: true, command: [gzip, '-k', '--stdout', '@INPUT@'], install: true, install_dir: join_paths(datadir, 'fwupd', 'host-emulate.d'), ) endforeach endif fwupd-2.0.10/src/tests/host-emulate/thinkpad-p1-iommu.json000066400000000000000000000437151501337203100234230ustar00rootroot00000000000000{ "SecurityAttributes": [ { "AppstreamId": "org.fwupd.hsi.Kernel.Tainted", "HsiResult": "not-tainted", "Plugin": "linux_tainted", "Flags": [ "success", "runtime-issue" ] }, { "AppstreamId": "org.fwupd.hsi.Kernel.Lockdown", "HsiResult": "enabled", "Plugin": "linux_lockdown", "Flags": [ "success", "runtime-issue" ] }, { "AppstreamId": "org.fwupd.hsi.Kernel.Swap", "HsiResult": "encrypted", "Plugin": "linux_swap", "Flags": [ "success", "runtime-issue" ] }, { "AppstreamId": "org.fwupd.hsi.Uefi.SecureBoot", "HsiResult": "enabled", "Plugin": "uefi_capsule", "Flags": [ "success" ] }, { "AppstreamId": "org.fwupd.hsi.Fwupd.Plugins", "HsiResult": "not-tainted", "Plugin": "core", "Flags": [ "success", "runtime-issue" ] }, { "AppstreamId": "org.fwupd.hsi.Mei.ManufacturingMode", "HsiResult": "locked", "Plugin": "pci_mei", "Flags": [ "success" ] }, { "AppstreamId": "org.fwupd.hsi.Mei.OverrideStrap", "HsiResult": "locked", "Plugin": "pci_mei", "Flags": [ "success" ] }, { "AppstreamId": "org.fwupd.hsi.Mei.Version", "HsiResult": "valid", "Plugin": "pci_mei", "Flags": [ "success" ] }, { "AppstreamId": "org.fwupd.hsi.PlatformDebugEnabled", "HsiResult": "not-enabled", "Plugin": "msr", "Flags": [ "success" ] }, { "AppstreamId": "org.fwupd.hsi.Spi.SmmBwp", "HsiResult": "locked", "Plugin": "pci_bcr", "Flags": [ "success" ] }, { "AppstreamId": "org.fwupd.hsi.Spi.Ble", "HsiResult": "enabled", "Plugin": "pci_bcr", "Flags": [ "success" ] }, { "AppstreamId": "org.fwupd.hsi.Spi.Bioswe", "HsiResult": "not-enabled", "Plugin": "pci_bcr", "Flags": [ "success" ] }, { "AppstreamId": "org.fwupd.hsi.SupportedCpu", "HsiResult": "valid", "Plugin": "cpu", "Flags": [ "success" ] }, { "AppstreamId": "org.fwupd.hsi.Tpm.EmptyPcr", "HsiResult": "valid", "Plugin": "tpm", "Flags": [ "success" ] }, { "AppstreamId": "org.fwupd.hsi.Tpm.Version20", "HsiResult": "found", "Plugin": "tpm", "Flags": [ "success" ] }, { "AppstreamId": "org.fwupd.hsi.Uefi.Pk", "HsiResult": "valid", "Plugin": "uefi_pk", "Flags": [ "success" ] }, { "AppstreamId": "org.fwupd.hsi.IntelBootguard.Enabled", "HsiResult": "enabled", "Plugin": "pci_mei", "Flags": [ "success" ] }, { "AppstreamId": "org.fwupd.hsi.IntelBootguard.Acm", "HsiResult": "valid", "Plugin": "pci_mei", "Flags": [ "success" ] }, { "AppstreamId": "org.fwupd.hsi.IntelBootguard.Otp", "HsiResult": "valid", "Plugin": "pci_mei", "Flags": [ "success" ] }, { "AppstreamId": "org.fwupd.hsi.IntelBootguard.Verified", "HsiResult": "valid", "Plugin": "pci_mei", "Flags": [ "success" ] }, { "AppstreamId": "org.fwupd.hsi.PlatformDebugLocked", "HsiResult": "locked", "Plugin": "msr", "Flags": [ "success" ] }, { "AppstreamId": "org.fwupd.hsi.Tpm.ReconstructionPcr0", "HsiResult": "valid", "Plugin": "tpm", "Flags": [ "success" ] }, { "AppstreamId": "org.fwupd.hsi.Iommu", "HsiResult": "found", "Plugin": "iommu", "Flags": [ "success" ] }, { "AppstreamId": "org.fwupd.hsi.IntelBootguard.Policy", "HsiResult": "valid", "Plugin": "pci_mei", "Flags": [ "success" ] }, { "AppstreamId": "org.fwupd.hsi.Cet.Enabled", "HsiResult": "not-supported", "Plugin": "cpu" }, { "AppstreamId": "org.fwupd.hsi.PrebootDma", "HsiResult": "not-enabled", "Plugin": "acpi_dmar", "Flags": [ "action-contact-oem", "action-config-fw" ] }, { "AppstreamId": "org.fwupd.hsi.SuspendToIdle", "HsiResult": "not-enabled", "Plugin": "acpi_facp", "Flags": [ "action-config-fw", "action-config-os" ] }, { "AppstreamId": "org.fwupd.hsi.SuspendToRam", "HsiResult": "enabled", "Plugin": "linux_sleep", "Flags": [ "action-config-fw", "action-config-os" ] }, { "AppstreamId": "org.fwupd.hsi.Smap", "HsiResult": "enabled", "Plugin": "cpu", "Flags": [ "success" ] }, { "AppstreamId": "org.fwupd.hsi.EncryptedRam", "HsiResult": "not-supported", "Plugin": "cpu" } ], "Devices": [ { "DeviceId": "4a4907dbb1b96c6a1177dfd1b95eb41c781d1265", "InstanceIds": [ "GPIO\\ID_INT3450:00" ], "Guid": [ "1199a818-4c52-5137-b536-d59e2e2cada9" ], "Flags": [ "registered" ] }, { "Name": "AMT [unprovisioned]", "DeviceId": "5fed1486be004d67ea79838d2e83aaa11bb72645", "ParentDeviceId": "a45df35ac0e948ee180fe216a5f703f32dda163f", "CompositeId": "a45df35ac0e948ee180fe216a5f703f32dda163f", "InstanceIds": [ "MEI\\VEN_8086&DEV_06E0", "MEI\\VEN_8086&DEV_06E0&REV_00", "MEI\\VEN_8086&DEV_06E0&SUBSYS_17AA22C2", "MEI\\VEN_8086&DEV_06E0&SUBSYS_17AA22C2&REV_00" ], "Guid": [ "2800f812-b7b4-2d4b-aca8-46e0ff65814c", "15c7ef4d-12fc-5e25-aba6-49b30f5ab130", "a65d125e-0c76-5876-bad3-6956a2f25e5e", "a8f5ca2d-e46c-5c9b-819b-d64c73c9e48d", "45bcbef5-0630-5121-a4d9-881054b2916f" ], "Summary": "Hardware and firmware technology for remote out-of-band management", "Flags": [ "internal", "registered" ], "Vendor": "Intel Corporation", "VendorId": "MEI:0x8086", "Version": "14.1.53.1649", "VersionBootloader": "14.1.53.1649", "VersionFormat": "intel-me", "Icons": [ "computer" ] }, { "Name": "CometLake-H GT2 [UHD Graphics]", "DeviceId": "5792b48846ce271fab11c4a545f7a3df0d36e00a", "InstanceIds": [ "PCI\\VEN_8086&DEV_9BC4", "PCI\\VEN_8086&DEV_9BC4&REV_05", "PCI\\VEN_8086&DEV_9BC4&SUBSYS_17AA22C2", "PCI\\VEN_8086&DEV_9BC4&SUBSYS_17AA22C2&REV_05", "PCI\\VEN_8086&DEV_9BC4&REV_00", "PCI\\VEN_8086&DEV_9BC4&SUBSYS_17AA22C2&REV_00" ], "Guid": [ "3777783a-3f83-56a5-95f4-533eb6a2bd19", "6c3dbf6c-4e6f-5309-9954-c5ab7aca617e", "5fde5d20-db24-5f21-afdd-247c1bf1efa1", "07168636-0f3b-565c-8fe1-0f0a77d82cd8", "7ffe1cb7-395a-52a9-a172-70ec6caaf310", "b813dc18-ddf2-508d-a7eb-0e2fc8752b03" ], "Flags": [ "internal", "registered", "can-verify", "can-verify-image" ], "Vendor": "Intel Corporation", "VendorId": "PCI:0x8086", "Version": "05", "VersionFormat": "plain" }, { "Name": "Core\u2122 i7-10850H CPU @ 2.70GHz", "DeviceId": "4bde70ba4e39b28f9eab1628f9dd6e6244c03027", "InstanceIds": [ "CPUID\\PRO_0&FAM_06", "CPUID\\PRO_0&FAM_06&MOD_A5", "CPUID\\PRO_0&FAM_06&MOD_A5&STP_2" ], "Guid": [ "30249f37-d140-5d3e-9319-186b1bd5cac3", "a45b0522-5722-54bd-b802-86cd044262df", "7b9b6e8c-226c-5db6-86cb-ea3187578013" ], "Flags": [ "internal", "registered" ], "Vendor": "Intel", "Version": "0x000000f0", "VersionFormat": "hex", "VersionRaw": 240, "Icons": [ "computer" ] }, { "Name": "Embedded Controller", "DeviceId": "2292ae5236790b47884e37cf162dcf23bfcd1c60", "Guid": [ "b616d3d6-cca9-40bd-964e-b86ffb62744d" ], "Summary": "UEFI ESRT device", "Protocol": "org.uefi.capsule", "Flags": [ "internal", "updatable", "require-ac", "supported", "registered", "needs-reboot", "usable-during-update" ], "Vendor": "Lenovo", "VendorId": "DMI:LENOVO", "Version": "0.1.11", "VersionLowest": "0.1.11", "VersionFormat": "triplet", "VersionRaw": 65547, "VersionLowestRaw": 65547, "UpdateState": 2 }, { "Name": "Integrated Camera", "DeviceId": "0fef0a0c55f6442bffaebd774ae771341c89571b", "InstanceIds": [ "USB\\VID_13D3&PID_5405", "USB\\VID_13D3&PID_5405&REV_6004" ], "Guid": [ "9284c551-0b4c-51ee-905a-168b8787290c", "6f7c4f56-0085-5aa6-b443-823852a1cdbc" ], "Serial": "0000", "Protocol": "org.usb.dfu", "Flags": [ "updatable", "registered", "add-counterpart-guids" ], "Vendor": "Azurewave", "VendorId": "USB:0x13D3", "Version": "60.4", "VersionFormat": "bcd", "Icons": [ "camera-web" ] }, { "Name": "Intel Management Engine", "DeviceId": "349bb341230b1a86e5effe7dfe4337e1590227bd", "Guid": [ "5695cc48-4f4f-4677-8ffb-9f496d3ad9d3" ], "Summary": "UEFI ESRT device", "Protocol": "org.uefi.capsule", "Flags": [ "internal", "updatable", "require-ac", "supported", "registered", "needs-reboot", "usable-during-update" ], "Vendor": "Lenovo", "VendorId": "DMI:LENOVO", "Version": "225.53.1649", "VersionLowest": "0.0.1", "VersionFormat": "triplet", "VersionRaw": 3778348657, "VersionLowestRaw": 1, "UpdateState": 2 }, { "Name": "System Firmware", "DeviceId": "a45df35ac0e948ee180fe216a5f703f32dda163f", "Guid": [ "6e58e73d-8061-44e4-8949-33b7f0d5c726" ], "Summary": "UEFI ESRT device", "Protocol": "org.uefi.capsule", "Flags": [ "internal", "updatable", "require-ac", "supported", "registered", "needs-reboot", "can-verify", "usable-during-update" ], "Checksums": [ "73319eae91b5838c5a587c54ffb625b58746238b", "6d244e0fadc7cc866e902517b4fe24505e52d1023934d487b039641c726abc46" ], "Vendor": "Lenovo", "VendorId": "DMI:LENOVO", "Version": "0.1.23", "VersionLowest": "0.1.11", "VersionFormat": "triplet", "VersionRaw": 65559, "VersionLowestRaw": 65547, "Icons": [ "computer" ], "UpdateState": 2 }, { "Name": "THNSN5512GPU7 TOSHIBA", "DeviceId": "03281da317dccd2b18de2bd1cc70a782df40ed7e", "InstanceIds": [ "NVME\\VEN_1179&DEV_010F", "NVME\\VEN_1179&DEV_010F&REV_01", "NVME\\VEN_1179&DEV_010F&SUBSYS_11790001", "NVME\\VEN_1179&DEV_010F&SUBSYS_11790001&REV_01", "THNSN5512GPU7 TOSHIBA" ], "Guid": [ "83991323-9951-5adf-b743-d93e882a41e1", "e22c4520-43dc-5bb3-8245-5787fead9b63", "87178ed9-f82b-5895-bcb0-09713abc842c", "2060b01b-f6aa-5fea-9acd-804de1765920", "e1409b09-50cf-5aef-8ad8-760b9022f88d" ], "Serial": "37RS11EATAHT", "Summary": "NVM Express solid state drive", "Protocol": "org.nvmexpress", "Flags": [ "internal", "updatable", "require-ac", "registered", "needs-reboot", "usable-during-update", "signed-payload" ], "Vendor": "Toshiba Corporation", "VendorId": "NVME:0x1179", "Version": "410557LA", "VersionFormat": "plain", "Icons": [ "drive-harddisk" ] }, { "Name": "TPM", "DeviceId": "c6a80ac3a22083423992a3cb15018989f37834d6", "InstanceIds": [ "TPM\\VEN_STM&DEV_0001", "TPM\\VEN_STM&MOD_", "TPM\\VEN_STM&DEV_0001&VER_2.0", "TPM\\VEN_STM&MOD_&VER_2.0" ], "Guid": [ "ff71992e-52f7-5eea-94ef-883e56e034c6", "84df3581-f896-54d2-bd1a-372602f04c32", "bfaed10a-bbc1-525b-a329-35da2f63e918", "70b7b833-7e1a-550a-a291-b94a12d0f319", "06f005e9-cb62-5d1a-82d9-13c534c53c48" ], "Flags": [ "internal", "registered" ], "Vendor": "ST Microelectronics", "VendorId": "TPM:STM", "Version": "1.258.0.0", "VersionFormat": "quad", "VersionRaw": 282583078273024, "Icons": [ "computer" ] }, { "Name": "Thunderbolt host controller", "DeviceId": "2cff15412fb2877637de2c23a10f841bca114e03", "InstanceIds": [ "THUNDERBOLT\\VEN_0109&DEV_1913", "THUNDERBOLT\\VEN_0109&DEV_1913&REV_00", "TBT-01091913-native", "TBT-01091913-native-controller0-0" ], "Guid": [ "b510dc43-dc5d-5449-9ccc-5edd80338954", "b595a681-7c5b-5842-bba7-1d448c261a6e", "10216d57-c796-5f3c-83d3-21baf70bfc54", "f8543f13-e164-5332-8681-4d5ef3ffbff0" ], "Summary": "Unmatched performance for high-speed I/O", "Protocol": "com.intel.thunderbolt", "Flags": [ "internal", "updatable", "require-ac", "registered", "dual-image", "signed-payload" ], "Vendor": "Lenovo", "VendorId": "THUNDERBOLT:0x0109|TBT:0x0109", "VendorIds": [ "THUNDERBOLT:0x0109", "TBT:0x0109" ], "Version": "62.00", "VersionFormat": "pair", "Icons": [ "thunderbolt" ] }, { "Name": "UEFI Device Firmware", "DeviceId": "f95c9218acd12697af946874bfe4239587209232", "Guid": [ "439d54f4-5548-4698-a8b0-46a047c0e66e" ], "Summary": "UEFI ESRT device", "Protocol": "org.uefi.capsule", "Flags": [ "internal", "updatable", "require-ac", "registered", "needs-reboot", "usable-during-update" ], "VendorId": "DMI:LENOVO", "Version": "16842759", "VersionLowest": "1", "VersionFormat": "number", "VersionRaw": 16842759, "VersionLowestRaw": 1, "UpdateState": 2 }, { "Name": "UEFI Device Firmware", "DeviceId": "d96de5c124b60ed6241ebcb6bb2c839cb5580786", "Guid": [ "3fb9a55d-d7f1-4d1b-b216-74e328e28f51" ], "Summary": "UEFI ESRT device", "Protocol": "org.uefi.capsule", "Flags": [ "internal", "updatable", "require-ac", "registered", "needs-reboot", "usable-during-update" ], "VendorId": "DMI:LENOVO", "Version": "65794", "VersionLowest": "65794", "VersionFormat": "number", "VersionRaw": 65794, "VersionLowestRaw": 65794, "UpdateState": 2 }, { "Name": "UEFI Device Firmware", "DeviceId": "f37fb01122dd62c773f4e84ec89737e059712d59", "Guid": [ "33967546-da89-4c51-9c95-5242bcb854e8" ], "Summary": "UEFI ESRT device", "Protocol": "org.uefi.capsule", "Flags": [ "internal", "updatable", "require-ac", "registered", "needs-reboot", "usable-during-update" ], "VendorId": "DMI:LENOVO", "Version": "24580", "VersionLowest": "1", "VersionFormat": "number", "VersionRaw": 24580, "VersionLowestRaw": 1, "UpdateState": 2 }, { "Name": "UEFI dbx", "DeviceId": "362301da643102b9f38477387e2193e57abaa590", "ParentDeviceId": "a45df35ac0e948ee180fe216a5f703f32dda163f", "CompositeId": "a45df35ac0e948ee180fe216a5f703f32dda163f", "InstanceIds": [ "UEFI\\CRT_A9087D1044AD18F7A94916D284CBC01827CF23CD8F60B79072C9CAA1FEF4D649", "UEFI\\CRT_A9087D1044AD18F7A94916D284CBC01827CF23CD8F60B79072C9CAA1FEF4D649&ARCH_X64", "UEFI\\CRT_A1117F516A32CEFCBA3F2D1ACE10A87972FD6BBE8FE0D0B996E09E65D802A503", "UEFI\\CRT_A1117F516A32CEFCBA3F2D1ACE10A87972FD6BBE8FE0D0B996E09E65D802A503&ARCH_X64" ], "Guid": [ "14503b3d-73ce-5d06-8137-77c68972a341", "5971a208-da00-5fce-b5f5-1234342f9cf7", "c6682ade-b5ec-57c4-b687-676351208742", "f8ba2887-9411-5c36-9cee-88995bb39731" ], "Summary": "UEFI revocation database", "Protocol": "org.uefi.dbx", "Flags": [ "internal", "updatable", "registered", "needs-reboot", "only-version-upgrade", "signed-payload" ], "VendorId": "UEFI:Linux Foundation", "Version": "238", "VersionLowest": "238", "VersionFormat": "number", "Icons": [ "computer" ], "InstallDuration": 1 }, { "Name": "WDC PC SN720 SDAQNTW-256G-1001", "DeviceId": "08e1798bf5d9cb56a0290b552cab6c1a371b5089", "InstanceIds": [ "NVME\\VEN_15B7&DEV_5002", "NVME\\VEN_15B7&DEV_5002&REV_00", "NVME\\VEN_15B7&DEV_5002&SUBSYS_15B75002", "NVME\\VEN_15B7&DEV_5002&SUBSYS_15B75002&REV_00", "WDC PC SN720 SDAQNTW-256G-1001" ], "Guid": [ "ff2112dc-038c-596d-90ca-d43c5077c6ec", "137520ce-3603-53e6-9165-56694ed744e7", "c528df4b-7972-5880-8cb1-330415e2dc6a", "06a6f1f7-4ce0-57ef-8154-0705d936e4a6", "237776ee-0bcd-5fe9-8dc8-6984a2d36ba0" ], "Serial": "183985804591", "Summary": "NVM Express solid state drive", "Protocol": "org.nvmexpress", "Flags": [ "internal", "updatable", "require-ac", "supported", "registered", "needs-reboot", "usable-during-update" ], "Vendor": "Sandisk Corp", "VendorId": "NVME:0x15B7", "Version": "10190101", "VersionFormat": "plain", "Icons": [ "drive-harddisk" ] } ] } fwupd-2.0.10/src/tests/meson.build000066400000000000000000000001251501337203100170030ustar00rootroot00000000000000subdir('auth') subdir('missing-hwid') subdir('multiple-rels') subdir('host-emulate') fwupd-2.0.10/src/tests/metadata-report1.xml000066400000000000000000000023231501337203100205370ustar00rootroot00000000000000 org.fwupd.something.device 2d47f29b-83a2-4f31-a2e8-63474f4d4c2e

    Applying will enable UEFI firmware reporting

    https://fwupd.org/something.cab 0628376cb0583335a7ff5875dd2d4b09fc5653057f644783a477553ca9981dcd Hughski LENOVO ThinkPad X1 Carbon 7th fedora 1.2.6 1.6.2
    fwupd-2.0.10/src/tests/metadata-report2.xml000066400000000000000000000023271501337203100205440ustar00rootroot00000000000000 org.fwupd.something.device 2d47f29b-83a2-4f31-a2e8-63474f4d4c2e

    Applying will enable UEFI firmware reporting

    https://fwupd.org/something.cab 0628376cb0583335a7ff5875dd2d4b09fc5653057f644783a477553ca9981dcd Google ChromeBook chromeos 1.2.2 1.9.1
    fwupd-2.0.10/src/tests/metadata-report3.xml000066400000000000000000000021621501337203100205420ustar00rootroot00000000000000 org.fwupd.something.device 2d47f29b-83a2-4f31-a2e8-63474f4d4c2e

    Applying will enable UEFI firmware reporting

    https://fwupd.org/something.cab 0628376cb0583335a7ff5875dd2d4b09fc5653057f644783a477553ca9981dcd Google ChromeBook chromeos 1.9.1
    fwupd-2.0.10/src/tests/metadata-report4.xml000066400000000000000000000024201501337203100205400ustar00rootroot00000000000000 org.fwupd.something.device 2d47f29b-83a2-4f31-a2e8-63474f4d4c2e

    Applying will enable UEFI firmware reporting

    https://fwupd.org/something.cab 0628376cb0583335a7ff5875dd2d4b09fc5653057f644783a477553ca9981dcd Google ChromeBook chromeos 1.2.3 1.9.1
    fwupd-2.0.10/src/tests/metadata.xml000066400000000000000000000007731501337203100171540ustar00rootroot00000000000000 org.fwupd.8330a096d9f1af8567c7374cb8403e1ce9cf3163.device 2d47f29b-83a2-4f31-a2e8-63474f4d4c2e

    Applying will enable UEFI firmware reporting

    fwupd-2.0.10/src/tests/missing-hwid/000077500000000000000000000000001501337203100172455ustar00rootroot00000000000000fwupd-2.0.10/src/tests/missing-hwid/firmware.bin000066400000000000000000000000141501337203100215460ustar00rootroot00000000000000Hello World fwupd-2.0.10/src/tests/missing-hwid/firmware.metainfo.xml000066400000000000000000000010041501337203100233770ustar00rootroot00000000000000 com.hughski.test.firmware 12345678-1234-1234-1234-123456789012 9342d47a-1bab-5709-9869-c840b2eac501 org.freedesktop.fwupd fwupd-2.0.10/src/tests/missing-hwid/firmware2.metainfo.xml000066400000000000000000000005531501337203100234710ustar00rootroot00000000000000 com.hughski.test.firmware 12345678-1234-1234-1234-123456789012 fwupd-2.0.10/src/tests/missing-hwid/meson.build000066400000000000000000000007151501337203100214120ustar00rootroot00000000000000hwid_test_firmware = custom_target('hwid-test-firmware', input: [ 'firmware.bin', 'firmware.metainfo.xml', ], output: 'hwid-1.2.3.cab', command: [ gcab, '--create', '--nopath', '@OUTPUT@', '@INPUT@', ], ) noreqs_test_firmware = custom_target('noreqs-test-firmware', input: [ 'firmware.bin', 'firmware2.metainfo.xml', ], output: 'noreqs-1.2.3.cab', command: [ gcab, '--create', '--nopath', '@OUTPUT@', '@INPUT@', ], ) fwupd-2.0.10/src/tests/multiple-rels/000077500000000000000000000000001501337203100174415ustar00rootroot00000000000000fwupd-2.0.10/src/tests/multiple-rels/firmware-123.bin000066400000000000000000000000111501337203100222420ustar00rootroot000000000000000x1020003fwupd-2.0.10/src/tests/multiple-rels/firmware-124.bin000066400000000000000000000000111501337203100222430ustar00rootroot000000000000000x1020004fwupd-2.0.10/src/tests/multiple-rels/firmware.metainfo.xml000066400000000000000000000010731501337203100236010ustar00rootroot00000000000000 com.hughski.test.firmware 12345678-1234-1234-1234-123456789012 fwupd-2.0.10/src/tests/multiple-rels/meson.build000066400000000000000000000004321501337203100216020ustar00rootroot00000000000000multiple_rels_test_firmware = custom_target('multiple-rels-test-firmware', input: [ 'firmware-123.bin', 'firmware-124.bin', 'firmware.metainfo.xml', ], output: 'multiple-rels-1.2.4.cab', command: [ gcab, '--create', '--nopath', '@OUTPUT@', '@INPUT@', ], ) fwupd-2.0.10/src/tests/os-release000066400000000000000000000002321501337203100166220ustar00rootroot00000000000000ID_LIKE=chromiumos GOOGLE_CRASH_ID=ChromeOS NAME=Chrome OS ID=chromeos HOME_URL=https://www.chromium.org/chromium-os BUG_REPORT_URL=https://crbug.com/new fwupd-2.0.10/src/tests/quirks.d/000077500000000000000000000000001501337203100164035ustar00rootroot00000000000000fwupd-2.0.10/src/tests/quirks.d/test.quirk000066400000000000000000000005761501337203100204470ustar00rootroot00000000000000[HIDRAW\VEN_093A&DEV_2862] Plugin = pixart_rf GType = FuPxiReceiverDevice [USB\VID_093A&PID_2862] Plugin = hughski_colorhug [VIDEO4LINUX\VEN_093A&DEV_2862] Plugin = logitech_tap GType = FuLogitechTapHdmiDevice [SERIO\FWID_LEN0305-PNP0F13] Plugin = synaptics_rmi GType = FuSynapticsRmiPs2Device [12f80028-b4b7-4b2d-aca8-46e0ff65814c] Plugin = intel_me GType = FuIntelMeAmtDevice fwupd-2.0.10/src/tests/remotes.d/000077500000000000000000000000001501337203100165435ustar00rootroot00000000000000fwupd-2.0.10/src/tests/remotes.d/broken.conf000066400000000000000000000001221501337203100206650ustar00rootroot00000000000000[fwupd Remote] Enabled=true MetadataURI=file:///tmp/fwupd-self-test/broken.xml.gz fwupd-2.0.10/src/tests/remotes.d/directory.conf000066400000000000000000000001241501337203100214130ustar00rootroot00000000000000[fwupd Remote] Enabled=true MetadataURI=file:///tmp/fwupd-self-test/var/cache/fwupd fwupd-2.0.10/src/tests/remotes.d/legacy-lvfs-xz.conf000066400000000000000000000001101501337203100222550ustar00rootroot00000000000000[fwupd Remote] Enabled=false MetadataURI=http://localhost/stable.xml.xz fwupd-2.0.10/src/tests/remotes.d/legacy-lvfs.conf000066400000000000000000000001101501337203100216160ustar00rootroot00000000000000[fwupd Remote] Enabled=false MetadataURI=http://localhost/stable.xml.gz fwupd-2.0.10/src/tests/remotes.d/legacy.conf000066400000000000000000000001101501337203100206460ustar00rootroot00000000000000[fwupd Remote] Enabled=false MetadataURI=http://localhost/stable.xml.gz fwupd-2.0.10/src/tests/remotes.d/stable.conf000066400000000000000000000001171501337203100206630ustar00rootroot00000000000000[fwupd Remote] Enabled=true MetadataURI=file:///tmp/fwupd-self-test/stable.xml fwupd-2.0.10/src/tests/remotes.d/testing.conf000066400000000000000000000001461501337203100210700ustar00rootroot00000000000000[fwupd Remote] Enabled=true MetadataURI=file:///tmp/fwupd-self-test/testing.xml ApprovalRequired=true fwupd-2.0.10/src/tests/remotes2.d000077700000000000000000000000001501337203100220042../../data/remotes.d/ustar00rootroot00000000000000fwupd-2.0.10/src/tests/stable.conf000077700000000000000000000000001501337203100253572../../src/tests/remotes.d/stable.confustar00rootroot00000000000000fwupd-2.0.10/src/tests/swaps000066400000000000000000000001511501337203100157200ustar00rootroot00000000000000Filename Type Size Used Priority /dev/zram0 partition 8388604 0 100 fwupd-2.0.10/src/tests/sys/000077500000000000000000000000001501337203100154615ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/000077500000000000000000000000001501337203100162525ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/hid/000077500000000000000000000000001501337203100170165ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/hid/drivers/000077500000000000000000000000001501337203100204745ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/hid/drivers/hid-generic/000077500000000000000000000000001501337203100226525ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/hid/drivers/hid-generic/.gitignore000066400000000000000000000000001501337203100246300ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/mei/000077500000000000000000000000001501337203100170245ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/mei/.gitignore000066400000000000000000000000001501337203100210020ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/pci/000077500000000000000000000000001501337203100170255ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/pci/.gitignore000066400000000000000000000000001501337203100210030ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/pci/devices/000077500000000000000000000000001501337203100204475ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/pci/devices/0000:00:14.0000077700000000000000000000000001501337203100265732../../../devices/pci0000:00/0000:00:14.0/ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/pci/drivers/000077500000000000000000000000001501337203100205035ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/pci/drivers/mei_me/000077500000000000000000000000001501337203100217365ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/pci/drivers/mei_me/.gitignore000066400000000000000000000000001501337203100237140ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/pci/drivers/mpt3sas/000077500000000000000000000000001501337203100220755ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/pci/drivers/mpt3sas/.gitignore000066400000000000000000000000001501337203100240530ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/pci/drivers/nvme/000077500000000000000000000000001501337203100214505ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/pci/drivers/nvme/.gitignore000066400000000000000000000000001501337203100234260ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/pci/drivers/pcieport/000077500000000000000000000000001501337203100223305ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/pci/drivers/pcieport/.gitignore000066400000000000000000000000001501337203100243060ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/platform/000077500000000000000000000000001501337203100200765ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/platform/.gitignore000066400000000000000000000000001501337203100220540ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/platform/drivers/000077500000000000000000000000001501337203100215545ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/platform/drivers/i8042/000077500000000000000000000000001501337203100223225ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/platform/drivers/i8042/.gitignore000066400000000000000000000000001501337203100243000ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/platform/drivers/tpm_tis/000077500000000000000000000000001501337203100232335ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/platform/drivers/tpm_tis/.gitignore000066400000000000000000000000001501337203100252110ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/scsi/000077500000000000000000000000001501337203100172135ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/scsi/.gitignore000066400000000000000000000000001501337203100211710ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/scsi/devices/000077500000000000000000000000001501337203100206355ustar00rootroot00000000000000b9683cc33cbae413d86e507f692bdb6111455033.paxheader00006660000000000000000000000172150133720310020337xustar00rootroot00000000000000122 linkpath=../../../devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/end_device-17:3/target17:0:3/17:0:3:0 fwupd-2.0.10/src/tests/sys/bus/scsi/devices/17:0:3:0000077700000000000000000000000001501337203100314142see b9683cc33cbae413d86e507f692bdb6111455033.paxheaderustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/scsi/drivers/000077500000000000000000000000001501337203100206715ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/scsi/drivers/sd/000077500000000000000000000000001501337203100212775ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/scsi/drivers/sd/.gitignore000066400000000000000000000000001501337203100232550ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/serio/000077500000000000000000000000001501337203100173735ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/serio/.gitignore000066400000000000000000000000001501337203100213510ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/serio/devices/000077500000000000000000000000001501337203100210155ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/serio/devices/serio1000077700000000000000000000000001501337203100302162../../../devices/platform/i8042/serio1/ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/serio/drivers/000077500000000000000000000000001501337203100210515ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/serio/drivers/psmouse/000077500000000000000000000000001501337203100225445ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/serio/drivers/psmouse/.gitignore000066400000000000000000000000001501337203100245220ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/usb/000077500000000000000000000000001501337203100170435ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/usb/devices/000077500000000000000000000000001501337203100204655ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/usb/devices/1-1000077700000000000000000000000001501337203100270512../../../devices/pci0000:00/0000:00:14.0/usb1/1-1/ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/usb/drivers/000077500000000000000000000000001501337203100205215ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/usb/drivers/usb/000077500000000000000000000000001501337203100213125ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/bus/usb/drivers/usb/.gitignore000066400000000000000000000000001501337203100232700ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/class/000077500000000000000000000000001501337203100165665ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/class/block/000077500000000000000000000000001501337203100176605ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/class/block/.gitignore000066400000000000000000000000001501337203100216360ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/class/block/nvme1000077700000000000000000000000001501337203100306652../../devices/pci0000:00/0000:00:1b.0/0000:02:00.0/nvme/nvme1/ustar00rootroot00000000000000b85879e95be83620595022e1f7cba416553260ea.paxheader00006660000000000000000000000202150133720310020211xustar00rootroot00000000000000130 linkpath=../../devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/end_device-17:3/target17:0:3/17:0:3:0/block/sde/ fwupd-2.0.10/src/tests/sys/class/block/sde000077700000000000000000000000001501337203100301422see b85879e95be83620595022e1f7cba416553260ea.paxheaderustar00rootroot000000000000001061b11603c459cccea116c0f522d2f34e302679.paxheader00006660000000000000000000000207150133720310020232xustar00rootroot00000000000000135 linkpath=../../devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/end_device-17:3/target17:0:3/17:0:3:0/block/sde/sde5/ fwupd-2.0.10/src/tests/sys/class/block/sde5000077700000000000000000000000001501337203100302432see 1061b11603c459cccea116c0f522d2f34e302679.paxheaderustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/class/hidraw/000077500000000000000000000000001501337203100200445ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/class/hidraw/hidraw1000077700000000000000000000000001501337203100346442../../devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/0003:093A:2862.0076/hidraw/hidraw1ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/class/nvme/000077500000000000000000000000001501337203100175335ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/class/nvme/nvme1000077700000000000000000000000001501337203100305402../../devices/pci0000:00/0000:00:1b.0/0000:02:00.0/nvme/nvme1/ustar00rootroot00000000000000b85879e95be83620595022e1f7cba416553260ea.paxheader00006660000000000000000000000202150133720310020211xustar00rootroot00000000000000130 linkpath=../../devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/end_device-17:3/target17:0:3/17:0:3:0/block/sde/ fwupd-2.0.10/src/tests/sys/class/nvme/sde000077700000000000000000000000001501337203100300152see b85879e95be83620595022e1f7cba416553260ea.paxheaderustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/class/tpm/000077500000000000000000000000001501337203100173665ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/class/tpm/tpm0000077700000000000000000000000001501337203100267472../../devices/platform/STM0125:00/tpm/tpm0/ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/class/video4linux/000077500000000000000000000000001501337203100210405ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/class/video4linux/video0000077700000000000000000000000001501337203100342722../../devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/video4linux/video0ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/000077500000000000000000000000001501337203100171035ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/bus/000077500000000000000000000000001501337203100176745ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/bus/pci/000077500000000000000000000000001501337203100204475ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/bus/pci/drivers/000077500000000000000000000000001501337203100221255ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/bus/pci/drivers/pcieport/000077500000000000000000000000001501337203100237525ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/bus/pci/drivers/pcieport/.gitignore000066400000000000000000000000001501337203100257300ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/bus/usb/000077500000000000000000000000001501337203100204655ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/bus/usb/drivers/000077500000000000000000000000001501337203100221435ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/bus/usb/drivers/usb/000077500000000000000000000000001501337203100227345ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/bus/usb/drivers/usb/.gitignore000066400000000000000000000000001501337203100247120ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/000077500000000000000000000000001501337203100204105ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/000077500000000000000000000000001501337203100216165ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/class000066400000000000000000000000111501337203100226360ustar00rootroot000000000000000x0c0330 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/device000066400000000000000000000000071501337203100227750ustar00rootroot000000000000000x06ed fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/revision000066400000000000000000000000051501337203100233720ustar00rootroot000000000000000x00 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/rom000066400000000000000000000000141501337203100223310ustar00rootroot00000000000000hello world fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/subsystem000077700000000000000000000000001501337203100257222../../../bus/pciustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/subsystem_device000066400000000000000000000000071501337203100251130ustar00rootroot000000000000000x22c2 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/subsystem_vendor000066400000000000000000000000071501337203100251510ustar00rootroot000000000000000x17aa fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/uevent000066400000000000000000000002431501337203100230460ustar00rootroot00000000000000DRIVER=xhci_hcd PCI_CLASS=C0330 PCI_ID=8086:06ED PCI_SUBSYS_ID=17AA:22C2 PCI_SLOT_NAME=0000:00:14.0 MODALIAS=pci:v00008086d000006EDsv000017AAsd000022C2bc0Csc03i30 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/000077500000000000000000000000001501337203100224705ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/000077500000000000000000000000001501337203100227665ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/000077500000000000000000000000001501337203100235765ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/0003:093A:2862.0076/000077500000000000000000000000001501337203100256165ustar00rootroot00000000000000driver000077700000000000000000000000001501337203100360012../../../../../../../bus/hid/drivers/hid-genericustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/0003:093A:2862.0076hidraw/000077500000000000000000000000001501337203100270155ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/0003:093A:2862.0076hidraw1/000077500000000000000000000000001501337203100303545ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/0003:093A:2862.0076/hidrawdev000066400000000000000000000000061501337203100310510ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/0003:093A:2862.0076/hidraw/hidraw1241:1 device000077700000000000000000000000001501337203100343352../../../0003:093A:2862.0076ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/0003:093A:2862.0076/hidraw/hidraw1subsystem000077700000000000000000000000001501337203100372012../../../../../../../../../class/hidrawustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/0003:093A:2862.0076/hidraw/hidraw1uevent000066400000000000000000000000421501337203100316010ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/0003:093A:2862.0076/hidraw/hidraw1MAJOR=241 MINOR=1 DEVNAME=hidraw1 modalias000066400000000000000000000000411501337203100272460ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/0003:093A:2862.0076hid:b0003g0001v0000093Ap00002862 subsystem000077700000000000000000000000001501337203100327102../../../../../../../bus/hidustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/0003:093A:2862.0076uevent000066400000000000000000000002571501337203100267740ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/0003:093A:2862.0076DRIVER=hid-generic HID_ID=0003:0000093A:00002862 HID_NAME=PIXART Pixart dual-mode mouse HID_PHYS=usb-0000:00:14.0-1/input1 HID_UNIQ= MODALIAS=hid:b0003g0001v0000093Ap00002862 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/bInterfaceClass000066400000000000000000000000031501337203100265420ustar00rootroot0000000000000003 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/bInterfaceNumber000066400000000000000000000000031501337203100267250ustar00rootroot0000000000000001 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/bInterfaceProtocol000066400000000000000000000000031501337203100272760ustar00rootroot0000000000000002 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/bInterfaceSubClass000066400000000000000000000000031501337203100272140ustar00rootroot0000000000000001 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/driver000077700000000000000000000000001501337203100320522../../../../../bus/usb/drivers/usbustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/modalias000066400000000000000000000000621501337203100253100ustar00rootroot00000000000000usb:v093Ap2862d0000dc00dsc00dp00ic03isc01ip02in01 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/subsystem000077700000000000000000000000001501337203100303462../../../../../bus/usbustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/uevent000066400000000000000000000002151501337203100250250ustar00rootroot00000000000000DEVTYPE=usb_interface DRIVER=usbhid PRODUCT=93a/2862/0 TYPE=0/0/0 INTERFACE=3/1/2 MODALIAS=usb:v093Ap2862d0000dc00dsc00dp00ic03isc01ip02in01 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/video4linux/000077500000000000000000000000001501337203100260505ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/video4linux/video0/000077500000000000000000000000001501337203100272365ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/video4linux/video0/dev000066400000000000000000000000051501337203100277320ustar00rootroot0000000000000081:0 dev_debug000066400000000000000000000000021501337203100310160ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/video4linux/video00 device000077700000000000000000000000001501337203100317302../../../1-1:1.1ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/video4linux/video0fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/video4linux/video0/index000066400000000000000000000000021501337203100302600ustar00rootroot000000000000000 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/video4linux/video0/name000066400000000000000000000000401501337203100300730ustar00rootroot00000000000000Integrated Camera: Integrated C subsystem000077700000000000000000000000001501337203100365652../../../../../../../../class/video4linuxustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/video4linux/video0uevent000066400000000000000000000000401501337203100304020ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/video4linux/video0MAJOR=81 MINOR=0 DEVNAME=video0 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/bDeviceClass000066400000000000000000000000031501337203100252310ustar00rootroot0000000000000000 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/bDeviceProtocol000066400000000000000000000000031501337203100257650ustar00rootroot0000000000000000 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/bDeviceSubClass000066400000000000000000000000031501337203100257030ustar00rootroot0000000000000000 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/bcdDevice000066400000000000000000000000051501337203100245540ustar00rootroot000000000000000000 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/busnum000066400000000000000000000000021501337203100242120ustar00rootroot000000000000001 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/descriptors000066400000000000000000000001151501337203100252470ustar00rootroot00000000000000@: b( ; 1  !"A@  !"‚@fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/dev000066400000000000000000000000071501337203100234640ustar00rootroot00000000000000189:23 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/devnum000066400000000000000000000000031501337203100242000ustar00rootroot0000000000000024 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/driver000077700000000000000000000000001501337203100312422../../../../../bus/usb/drivers/usbustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/idProduct000066400000000000000000000000051501337203100246410ustar00rootroot000000000000002862 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/idVendor000066400000000000000000000000051501337203100244560ustar00rootroot00000000000000093a fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/manufacturer000066400000000000000000000000071501337203100254020ustar00rootroot00000000000000PIXART fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/product000066400000000000000000000000271501337203100243700ustar00rootroot00000000000000Pixart dual-mode mouse fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/removable000066400000000000000000000000121501337203100246560ustar00rootroot00000000000000removable fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/subsystem000077700000000000000000000000001501337203100275362../../../../../bus/usbustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/uevent000066400000000000000000000001751501337203100242220ustar00rootroot00000000000000MAJOR=189 MINOR=23 DEVNAME=bus/usb/001/024 DEVTYPE=usb_device DRIVER=usb PRODUCT=93a/2862/0 TYPE=0/0/0 BUSNUM=001 DEVNUM=024 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/version000066400000000000000000000000061501337203100243720ustar00rootroot00000000000000 1.10 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/bDeviceClass000066400000000000000000000000031501337203100247330ustar00rootroot0000000000000009 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/bDeviceProtocol000066400000000000000000000000031501337203100254670ustar00rootroot0000000000000001 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/bDeviceSubClass000066400000000000000000000000031501337203100254050ustar00rootroot0000000000000000 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/bcdDevice000066400000000000000000000000051501337203100242560ustar00rootroot000000000000000608 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/busnum000066400000000000000000000000021501337203100237140ustar00rootroot000000000000001 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/dev000066400000000000000000000000061501337203100231650ustar00rootroot00000000000000189:0 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/devnum000066400000000000000000000000021501337203100237010ustar00rootroot000000000000001 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/devpath000066400000000000000000000000021501337203100240360ustar00rootroot000000000000000 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/driver000077700000000000000000000000001501337203100305312../../../../bus/usb/drivers/usbustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/idProduct000066400000000000000000000000051501337203100243430ustar00rootroot000000000000000002 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/idVendor000066400000000000000000000000051501337203100241600ustar00rootroot000000000000001d6b fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/manufacturer000066400000000000000000000000461501337203100251070ustar00rootroot00000000000000Linux 6.8.11-300.fc40.x86_64 xhci-hcd fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/subsystem000077700000000000000000000000001501337203100270252../../../../bus/usbustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/uevent000066400000000000000000000001741501337203100237230ustar00rootroot00000000000000MAJOR=189 MINOR=0 DEVNAME=bus/usb/001/001 DEVTYPE=usb_device DRIVER=usb PRODUCT=1d6b/2/608 TYPE=9/0/1 BUSNUM=001 DEVNUM=001 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/usb1/version000066400000000000000000000000061501337203100240740ustar00rootroot00000000000000 2.00 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:14.0/vendor000066400000000000000000000000071501337203100230330ustar00rootroot000000000000000x8086 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/000077500000000000000000000000001501337203100216745ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/0000:02:00.0/000077500000000000000000000000001501337203100230775ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/0000:02:00.0/class000066400000000000000000000000111501337203100241170ustar00rootroot000000000000000x010802 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/0000:02:00.0/device000066400000000000000000000000071501337203100242560ustar00rootroot000000000000000x010f fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/0000:02:00.0/driver000077700000000000000000000000001501337203100317432../../../bus/pci/drivers/pcieportustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/0000:02:00.0/nvme/000077500000000000000000000000001501337203100240445ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/0000:02:00.0/nvme/nvme1/000077500000000000000000000000001501337203100250725ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/0000:02:00.0/nvme/nvme1/address000066400000000000000000000000151501337203100264360ustar00rootroot000000000000000000:02:00.0 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/0000:02:00.0/nvme/nvme1/dev000066400000000000000000000000061501337203100255670ustar00rootroot00000000000000237:1 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/0000:02:00.0/nvme/nvme1/device000077700000000000000000000000001501337203100302362../../../0000:02:00.0ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/0000:02:00.0/nvme/nvme1/firmware_rev000066400000000000000000000000111501337203100274750ustar00rootroot00000000000000410557LA fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/0000:02:00.0/nvme/nvme1/model000066400000000000000000000000511501337203100261110ustar00rootroot00000000000000THNSN5512GPU7 TOSHIBA fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/0000:02:00.0/nvme/nvme1/serial000066400000000000000000000000251501337203100262710ustar00rootroot00000000000000 37RS11EATAHT fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/0000:02:00.0/nvme/nvme1/subsystem000077700000000000000000000000001501337203100325452../../../../../../class/nvmeustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/0000:02:00.0/nvme/nvme1/uevent000066400000000000000000000000611501337203100263200ustar00rootroot00000000000000MAJOR=237 MINOR=1 DEVNAME=nvme1 NVME_TRTYPE=pcie fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/0000:02:00.0/revision000066400000000000000000000000051501337203100246530ustar00rootroot000000000000000x01 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/0000:02:00.0/subsystem000077700000000000000000000000001501337203100272032../../../bus/pciustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/0000:02:00.0/subsystem_device000066400000000000000000000000071501337203100263740ustar00rootroot000000000000000x0001 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/0000:02:00.0/subsystem_vendor000066400000000000000000000000071501337203100264320ustar00rootroot000000000000000x1179 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/0000:02:00.0/uevent000066400000000000000000000002431501337203100243270ustar00rootroot00000000000000DRIVER=pcieport PCI_CLASS=60400 PCI_ID=8086:06C0 PCI_SUBSYS_ID=17AA:22C2 PCI_SLOT_NAME=0000:00:1b.0 MODALIAS=pci:v00008086d000006C0sv000017AAsd000022C2bc06sc04i00 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/0000:02:00.0/vendor000066400000000000000000000000071501337203100243140ustar00rootroot000000000000000x1179 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/class000066400000000000000000000000111501337203100227140ustar00rootroot000000000000000x060400 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/device000066400000000000000000000000071501337203100230530ustar00rootroot000000000000000x06c0 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/driver000077700000000000000000000000001501337203100305402../../../bus/pci/drivers/pcieportustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/revision000066400000000000000000000000051501337203100234500ustar00rootroot000000000000000xf0 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/subsystem000077700000000000000000000000001501337203100260002../../../bus/pciustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/subsystem_device000066400000000000000000000000071501337203100251710ustar00rootroot000000000000000x22c2 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/subsystem_vendor000066400000000000000000000000071501337203100252270ustar00rootroot000000000000000x17aa fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/uevent000066400000000000000000000002431501337203100231240ustar00rootroot00000000000000DRIVER=pcieport PCI_CLASS=60400 PCI_ID=8086:06C0 PCI_SUBSYS_ID=17AA:22C2 PCI_SLOT_NAME=0000:00:1b.0 MODALIAS=pci:v00008086d000006C0sv000017AAsd000022C2bc06sc04i00 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/0000:00:1b.0/vendor000066400000000000000000000000071501337203100231110ustar00rootroot000000000000000x8086 fwupd-2.0.10/src/tests/sys/devices/pci0000:00/uevent000066400000000000000000000000001501337203100216270ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/000077500000000000000000000000001501337203100204155ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/000077500000000000000000000000001501337203100216255ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/000077500000000000000000000000001501337203100230345ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/class000066400000000000000000000000111501337203100240540ustar00rootroot000000000000000x010700 fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/device000066400000000000000000000000071501337203100242130ustar00rootroot000000000000000x00af fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/driver000077700000000000000000000000001501337203100316602../../../../bus/pci/drivers/mpt3sasustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/000077500000000000000000000000001501337203100241615ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/000077500000000000000000000000001501337203100255475ustar00rootroot00000000000000end_device-17:3/000077500000000000000000000000001501337203100301575ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3target17:0:3/000077500000000000000000000000001501337203100321445ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/end_device-17:317:0:3:0/000077500000000000000000000000001501337203100330745ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/end_device-17:3/target17:0:3block/000077500000000000000000000000001501337203100341665ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/end_device-17:3/target17:0:3/17:0:3:0sde/000077500000000000000000000000001501337203100347415ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/end_device-17:3/target17:0:3/17:0:3:0/blockro000066400000000000000000000000021501337203100352740ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/end_device-17:3/target17:0:3/17:0:3:0/block/sde0 sde5/000077500000000000000000000000001501337203100356015ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/end_device-17:3/target17:0:3/17:0:3:0/block/sdedev000066400000000000000000000000051501337203100362750ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/end_device-17:3/target17:0:3/17:0:3:0/block/sde/sde58:69 subsystem000077700000000000000000000000001501337203100451032../../../../../../../../../../../../class/blockustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/end_device-17:3/target17:0:3/17:0:3:0/block/sde/sde5uevent000066400000000000000000000001021501337203100370230ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/end_device-17:3/target17:0:3/17:0:3:0/block/sde/sde5MAJOR=8 MINOR=69 DEVNAME=sde5 DEVTYPE=partition DISKSEQ=5 PARTN=5 subsystem000077700000000000000000000000001501337203100440302../../../../../../../../../../../class/blockustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/end_device-17:3/target17:0:3/17:0:3:0/block/sdeuevent000066400000000000000000000000641501337203100361720ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/end_device-17:3/target17:0:3/17:0:3:0/block/sdeMAJOR=8 MINOR=64 DEVNAME=sde DEVTYPE=disk DISKSEQ=5 driver000077700000000000000000000000001501337203100424112../../../../../../../../../bus/scsi/drivers/sdustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/end_device-17:3/target17:0:3/17:0:3:0model000066400000000000000000000000211501337203100341100ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/end_device-17:3/target17:0:3/17:0:3:0ST1000NM0001 subsystem000077700000000000000000000000001501337203100410702../../../../../../../../../bus/scsiustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/end_device-17:3/target17:0:3/17:0:3:0uevent000066400000000000000000000000631501337203100343240ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/end_device-17:3/target17:0:3/17:0:3:0DEVTYPE=scsi_device DRIVER=sd MODALIAS=scsi:t-0x00 vendor000066400000000000000000000000111501337203100343040ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/end_device-17:3/target17:0:3/17:0:3:0IBM-ESXS subsystem000077700000000000000000000000001501337203100377252../../../../../../../../bus/scsiustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/end_device-17:3/target17:0:3uevent000066400000000000000000000000241501337203100333710ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/end_device-17:3/target17:0:3DEVTYPE=scsi_target uevent000066400000000000000000000000001501337203100313760ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/end_device-17:3fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/host17/port-17:3/uevent000066400000000000000000000000001501337203100267660ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/revision000066400000000000000000000000051501337203100246100ustar00rootroot000000000000000x01 fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/subsystem000077700000000000000000000000001501337203100273532../../../../bus/pciustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/subsystem_device000066400000000000000000000000071501337203100263310ustar00rootroot000000000000000x0200 fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/subsystem_vendor000066400000000000000000000000071501337203100263670ustar00rootroot000000000000000x1d49 fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/uevent000066400000000000000000000002421501337203100242630ustar00rootroot00000000000000DRIVER=mpt3sas PCI_CLASS=10700 PCI_ID=1000:00AF PCI_SUBSYS_ID=1D49:0200 PCI_SLOT_NAME=0000:51:00.0 MODALIAS=pci:v00001000d000000AFsv00001D49sd00000200bc01sc07i00 fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/0000:51:00.0/vendor000066400000000000000000000000071501337203100242510ustar00rootroot000000000000000x1000 fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/class000066400000000000000000000000111501337203100226450ustar00rootroot000000000000000x060400 fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/device000066400000000000000000000000071501337203100230040ustar00rootroot000000000000000x347a fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/driver000077700000000000000000000000001501337203100304712../../../bus/pci/drivers/pcieportustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/subsystem000077700000000000000000000000001501337203100257312../../../bus/pciustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/subsystem_device000066400000000000000000000000071501337203100251220ustar00rootroot000000000000000x7810 fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/subsystem_vendor000066400000000000000000000000071501337203100251600ustar00rootroot000000000000000x17aa fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/uevent000066400000000000000000000002431501337203100230550ustar00rootroot00000000000000DRIVER=pcieport PCI_CLASS=60400 PCI_ID=8086:347A PCI_SUBSYS_ID=17AA:7810 PCI_SLOT_NAME=0000:50:02.0 MODALIAS=pci:v00008086d0000347Asv000017AAsd00007810bc06sc04i00 fwupd-2.0.10/src/tests/sys/devices/pci0000:50/0000:50:02.0/vendor000066400000000000000000000000071501337203100230420ustar00rootroot000000000000000x8086 fwupd-2.0.10/src/tests/sys/devices/platform/000077500000000000000000000000001501337203100207275ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/platform/STM0125:00/000077500000000000000000000000001501337203100221345ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/platform/STM0125:00/driver000077700000000000000000000000001501337203100317032../../../bus/platform/drivers/tpm_tisustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/platform/STM0125:00/subsystem000077700000000000000000000000001501337203100273112../../../bus/platformustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/platform/STM0125:00/tpm/000077500000000000000000000000001501337203100227345ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/platform/STM0125:00/tpm/tpm0/000077500000000000000000000000001501337203100236145ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/platform/STM0125:00/tpm/tpm0/dev000066400000000000000000000000071501337203100243120ustar00rootroot0000000000000010:224 fwupd-2.0.10/src/tests/sys/devices/platform/STM0125:00/tpm/tpm0/device000077700000000000000000000000001501337203100267622../../../STM0125:00ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/platform/STM0125:00/tpm/tpm0/subsystem000077700000000000000000000000001501337203100307072../../../../../class/tpmustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/platform/STM0125:00/tpm/tpm0/tpm_version_major000066400000000000000000000000021501337203100272640ustar00rootroot000000000000002 fwupd-2.0.10/src/tests/sys/devices/platform/STM0125:00/tpm/tpm0/uevent000066400000000000000000000000401501337203100250370ustar00rootroot00000000000000MAJOR=10 MINOR=224 DEVNAME=tpm0 fwupd-2.0.10/src/tests/sys/devices/platform/STM0125:00/uevent000066400000000000000000000000571501337203100233670ustar00rootroot00000000000000DRIVER=tpm_tis MODALIAS=acpi:STM0125:MSFT0101: fwupd-2.0.10/src/tests/sys/devices/platform/i8042/000077500000000000000000000000001501337203100214755ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/platform/i8042/driver000077700000000000000000000000001501337203100303332../../../bus/platform/drivers/i8042ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/platform/i8042/serio1/000077500000000000000000000000001501337203100226775ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/platform/i8042/serio1/description000066400000000000000000000000171501337203100251430ustar00rootroot00000000000000i8042 AUX port fwupd-2.0.10/src/tests/sys/devices/platform/i8042/serio1/driver000077700000000000000000000000001501337203100321722../../../../bus/serio/drivers/psmouseustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/platform/i8042/serio1/firmware_id000066400000000000000000000000251501337203100251070ustar00rootroot00000000000000PNP: LEN0305 PNP0f13 fwupd-2.0.10/src/tests/sys/devices/platform/i8042/serio1/protocol000066400000000000000000000000071501337203100244600ustar00rootroot00000000000000TPPS/2 fwupd-2.0.10/src/tests/sys/devices/platform/i8042/serio1/subsystem000077700000000000000000000000001501337203100275642../../../../bus/serioustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/platform/i8042/serio1/uevent000066400000000000000000000002161501337203100241270ustar00rootroot00000000000000DRIVER=psmouse SERIO_TYPE=01 SERIO_PROTO=00 SERIO_ID=00 SERIO_EXTRA=00 MODALIAS=serio:ty01pr00id00ex00 SERIO_FIRMWARE_ID=PNP: LEN0305 PNP0f13 fwupd-2.0.10/src/tests/sys/devices/platform/i8042/subsystem000077700000000000000000000000001501337203100266522../../../bus/platformustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/devices/platform/i8042/uevent000066400000000000000000000000451501337203100227250ustar00rootroot00000000000000DRIVER=i8042 MODALIAS=platform:i8042 fwupd-2.0.10/src/tests/sys/devices/platform/uevent000066400000000000000000000000001501337203100221460ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/kernel/000077500000000000000000000000001501337203100167415ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/kernel/security/000077500000000000000000000000001501337203100206105ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/kernel/security/lockdown000066400000000000000000000000411501337203100223460ustar00rootroot00000000000000[none] integrity confidentiality fwupd-2.0.10/src/tests/sys/kernel/tainted000066400000000000000000000000021501337203100203040ustar00rootroot000000000000000 fwupd-2.0.10/src/tests/sys/power/000077500000000000000000000000001501337203100166155ustar00rootroot00000000000000fwupd-2.0.10/src/tests/sys/power/mem_sleep000066400000000000000000000000071501337203100205030ustar00rootroot00000000000000[deep] fwupd-2.0.10/src/tests/usb-devices-bootloader.json000066400000000000000000000002501501337203100220740ustar00rootroot00000000000000{ "UsbDevices": [ { "PlatformId": "usb:01:00:06", "Created": "9999-02-03T13:39:21.538713Z", "IdVendor": 999, "IdProduct": 999 } ] } fwupd-2.0.10/src/tests/usb-devices-invalid.json000066400000000000000000000032051501337203100213730ustar00rootroot00000000000000{ "UsbDevices": [ { "PlatformId": "usb:00", "Created": "2023-02-03T13:39:21.538713Z", "IdVendor": 10047, "IdProduct": 4100, "Device": 2, "USB": 513, "Manufacturer": 1, "Product": 2, "UsbBosDescriptors": [ { "Comment": "version invalid", "DevCapabilityType": 5, "ExtraData": "AGPsCgF09c1SndooUlUNlPAKAAAAIAAqAA==" }, { "Comment": "UUID invalid", "DevCapabilityType": 5, "ExtraData": "AAAAAAAAAAAAAAAAAAAAAAAFCAEAIAAqAA==" }, { "Comment": "plugin invalid", "DevCapabilityType": 5, "ExtraData": "AGPsCgF09c1SndooUlUNlPAFCAEAIAArAA==" } ], "UsbEvents": [ { "Id": "GetStringDescriptor:DescIndex=0x02", "Data": "Q29sb3JIdWcyAEcAAAAAAACwA4pgfwAAAN+Vneb9GHkAAAAAAAAAAEA42QAAAAAAwHVdKPx/AAAAAAAAAAAAAJiCXSj8fwAAR9bLiWB/AADAdV0o/H8AAOrE/IlgfwAAgHW/AAAAAAAQw9UAAAAAAJiCXSj8fwAAytnLiQEAAAA=" }, { "Comment": "Plugin=dfu\nIcon=computer\n", "Id": "ControlTransfer:Direction=0x00,RequestType=0x02,Recipient=0x00,Request=0x2a,Value=0x0000,Idx=0x0007,Data=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=,Length=0x20", "Data": "UGx1Z2luPWRmdQpJY29uPWNvbXB1dGVyCgAAAAAAAAA=" }, { "Comment": "Plugin=XXX", "Id": "ControlTransfer:Direction=0x00,RequestType=0x02,Recipient=0x00,Request=0x2b,Value=0x0000,Idx=0x0007,Data=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=,Length=0x20", "Data": "UGx1Z2luPVhYWAoAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" } ] } ] } fwupd-2.0.10/src/tests/usb-devices-replace.json000066400000000000000000000002531501337203100213600ustar00rootroot00000000000000{ "UsbDevices": [ { "PlatformId": "usb:01:00:06", "Created": "2023-02-03T13:39:21.538713Z", "IdVendor": 10047, "IdProduct": 4100 } ] } fwupd-2.0.10/src/tests/usb-devices.json000066400000000000000000000151761501337203100177610ustar00rootroot00000000000000{ "FwupdVersion": "1.2.3", "UsbDevices": [ { "PlatformId": "usb:01:00:06", "Created": "2023-02-03T13:39:21.538713Z", "IdVendor": 10047, "IdProduct": 4100, "Device": 2, "USB": 513, "Manufacturer": 1, "Product": 2, "UsbBosDescriptors": [ { "DevCapabilityType": 5, "ExtraData": "AN9g3diJRcdMnNJlnZ5kip8AAAMG4AQVAA==" }, { "DevCapabilityType": 5, "ExtraData": "AGPsCgF09c1SndooUlUNlPAFCAEAIAAqAA==" }, { "DevCapabilityType": 17, "ExtraData": "AQMAAAA=" } ], "UsbInterfaces": [ { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 1, "InterfaceClass": 255, "InterfaceSubClass": 70, "InterfaceProtocol": 87, "Interface": 3 }, { "Length": 9, "DescriptorType": 4, "InterfaceNumber": 2, "InterfaceClass": 255, "InterfaceSubClass": 71, "InterfaceProtocol": 85, "Interface": 4 }, { "Length": 9, "DescriptorType": 4, "InterfaceClass": 3, "UsbEndpoints": [ { "DescriptorType": 5, "EndpointAddress": 129, "Interval": 1, "MaxPacketSize": 64 }, { "DescriptorType": 5, "EndpointAddress": 1, "Interval": 1, "MaxPacketSize": 64 } ], "ExtraData": "CSERAQABIh0A" } ], "UsbEvents": [ { "Id": "GetStringDescriptor:DescIndex=0x01", "Data": "SHVnaHNraSBMdGQuAAAAAAAAAAAAAAAAIFjfAAAAAAAAAAAAAAAAAEA42QAAAAAAwHVdKPx/AAAAAAAAAAAAAJiCXSj8fwAAR9bLiWB/AADAdV0o/H8AAOrE/IlgfwAAgHW/AAAAAAAQw9UAAAAAAJiCXSj8fwAAytnLiQEAAAA=" }, { "Id": "GetStringDescriptor:DescIndex=0x02", "Data": "Q29sb3JIdWcyAEcAAAAAAACwA4pgfwAAAN+Vneb9GHkAAAAAAAAAAEA42QAAAAAAwHVdKPx/AAAAAAAAAAAAAJiCXSj8fwAAR9bLiWB/AADAdV0o/H8AAOrE/IlgfwAAgHW/AAAAAAAQw9UAAAAAAJiCXSj8fwAAytnLiQEAAAA=" }, { "Id": "GetCustomIndex:ClassId=0xff,SubclassId=0x46,ProtocolId=0x57", "Data": "Aw==" }, { "Id": "GetStringDescriptor:DescIndex=0x03", "Data": "Mi4wLjcAAAAD0WmJYH8AAP8AAAAAAAAAA9FpiWB/AACQRNkAAAAAAGCj2wAAAAAAUHZdKPx/AACNC7qJYH8AAAMAAAAAAAAANougiWB/AACYgl0o/H8AAAAAAAAAAAAA/wAAAPx/V0Zgo9sAAAAAAEh5XSj8fwAAEMPVAAAAAAM=" }, { "Id": "GetCustomIndex:ClassId=0xff,SubclassId=0x47,ProtocolId=0x55", "Data": "BA==" }, { "Id": "ControlTransfer:Direction=0x00,RequestType=0x02,Recipient=0x00,Request=0x15,Value=0x0000,Idx=0x0007,Data=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA,Length=0x4e0", "Data": "CgAAAAAAAwbgBAgAAQAAANYECAACAAAAkgGAAAQAAQAoAFUAVgBDAC0ARgBTAFMAZQBuAHMAbwByAEcAcgBvAHUAcABJAEQAAABOAHsARgA2ADYARgBBADYANwA0AC0ARgAyAEIARQAtADQARAAxADkALQA5ADgAQwA1AC0ARAAxADMAMgAzADIAOQBDADYANAAyAEYAfQAAAF4ABAABACwAVQBWAEMALQBGAFMAUwBlAG4AcwBvAHIARwByAG8AdQBwAE4AYQBtAGUAAAAoAEwAZQBuAG8AdgBvACAAQwBhAG0AZQByAGEAIABHAHIAbwB1AHAAAAA8AAQABAAuAFUAVgBDAC0ARQBuAGEAYgBsAGUAUABsAGEAdABmAG8AcgBtAEQAbQBmAHQAAAAEAAEAAAA+AAQABAAwAEUAbgBhAGIAbABlAEQAcwBoAG8AdwBSAGUAZABpAHIAZQBjAHQAaQBvAG4AAAAAAAQAAQAAADIABAAEACQAVQBWAEMALQBDAFAAVgAyAEYAYQBjAGUAQQB1AHQAaAAAAAAABAD//wAACAACAAIAwAGAAAQAAQAoAFUAVgBDAC0ARgBTAFMAZQBuAHMAbwByAEcAcgBvAHUAcABJAEQAAABOAHsARgA2ADYARgBBADYANwA0AC0ARgAyAEIARQAtADQARAAxADkALQA5ADgAQwA1AC0ARAAxADMAMgAzADIAOQBDADYANAAyAEYAfQAAAF4ABAABACwAVQBWAEMALQBGAFMAUwBlAG4AcwBvAHIARwByAG8AdQBwAE4AYQBtAGUAAAAoAEwAZQBuAG8AdgBvACAAQwBhAG0AZQByAGEAIABHAHIAbwB1AHAAAAAwAAQABAAiAFMAZQBuAHMAbwByAEMAYQBtAGUAcgBhAE0AbwBkAGUAAAAEAAEAAAA6AAQABAAsAFMAawBpAHAAQwBhAG0AZQByAGEARQBuAHUAbQBlAHIAYQB0AGkAbwBuAAAABAABAAAAPgAEAAQAMABFAG4AYQBiAGwAZQBEAHMAaABvAHcAUgBlAGQAaQByAGUAYwB0AGkAbwBuAAAAAAAEAAEAAAAyAAQABAAkAFUAVgBDAC0AQwBQAFYAMgBGAGEAYwBlAEEAdQB0AGgAAAAAAAQAAAD//wgAAgAEAHwBMgAEAAQAJABEAGUAdgBpAGMAZQBJAGQAbABlAEUAbgBhAGIAbABlAGQAAAAEAAEAAAAyAAQABAAkAEQAZQBmAGEAdQBsAHQASQBkAGwAZQBTAHQAYQB0AGUAAAAAAAQAAQAAADYABAAEACgARABlAGYAYQB1AGwAdABJAGQAbABlAFQAaQBtAGUAbwB1AHQAAAAAAAQAiBMAAEYABAAEADgARABlAHYAaQBjAGUASQBkAGwAZQBJAGcAbgBvAHIAZQBXAGEAawBlAEUAbgBhAGIAbABlAAAAAAAEAAEAAAAUAAMAV0lOVVNCAAAAAAAAAAAAAIAABAABACgARABlAHYAaQBjAGUASQBuAHQAZQByAGYAYQBjAGUARwBVAEkARAAAAE4AewBlAGMAYwBlAGYAZgAzADUALQAxADQANgAzAC0ANABmAGYAMwAtAGEAYwBkADkALQA4AGYAOQA5ADIAZAAwADkAYQBjAGQAZAB9AAAA" }, { "Id": "ControlTransfer:Direction=0x00,RequestType=0x02,Recipient=0x00,Request=0x2a,Value=0x0000,Idx=0x0007,Data=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=,Length=0x20", "Data": "UGx1Z2luPWRmdQpJY29uPWNvbXB1dGVyCgAAAAAAAAA=" }, { "Id": "GetStringDescriptor:DescIndex=0x04", "Data": "MjA4MmI1ZTAtN2E2NC00NzhhLWIxYjItZTM0MDRmYWI2ZGFkAAAAAICg2QAAAAAAUHZdKPx/AACNC7qJYH8AAAQAAAAAAAAANougiWB/AAAAsAOKYH8AAAAAAAAAAAAA/wAAAAAAVUeAoNkAAAAAAFB2XSj8fwAAsKPbAAAAAAQ=" } ] } ] } fwupd-2.0.10/subprojects/000077500000000000000000000000001501337203100152555ustar00rootroot00000000000000fwupd-2.0.10/subprojects/.gitignore000066400000000000000000000000711501337203100172430ustar00rootroot00000000000000gusb gi-docgen flashrom libjcat libxmlb fwupd-efi passim fwupd-2.0.10/subprojects/flashrom.wrap000066400000000000000000000001351501337203100177620ustar00rootroot00000000000000[wrap-git] directory = flashrom url = https://github.com/flashrom/flashrom revision = v1.4.0 fwupd-2.0.10/subprojects/fwupd-efi.wrap000066400000000000000000000001311501337203100200310ustar00rootroot00000000000000[wrap-git] directory = fwupd-efi url = https://github.com/fwupd/fwupd-efi revision = 1.6 fwupd-2.0.10/subprojects/gi-docgen.wrap000066400000000000000000000001421501337203100200010ustar00rootroot00000000000000[wrap-git] directory=gi-docgen url=https://github.com/GNOME/gi-docgen.git revision=2024.1 depth=1 fwupd-2.0.10/subprojects/json-glib.wrap000066400000000000000000000002141501337203100200310ustar00rootroot00000000000000[wrap-git] directory = json-glib url = https://github.com/GNOME/json-glib.git revision = 1.10.0 [provide] dependency_names = json-glib-1.0 fwupd-2.0.10/subprojects/libjcat.wrap000066400000000000000000000001351501337203100175570ustar00rootroot00000000000000[wrap-git] directory = libjcat url = https://github.com/hughsie/libjcat.git revision = 0.2.2 fwupd-2.0.10/subprojects/libxmlb.wrap000066400000000000000000000001361501337203100176010ustar00rootroot00000000000000[wrap-git] directory = libxmlb url = https://github.com/hughsie/libxmlb.git revision = 0.3.21 fwupd-2.0.10/subprojects/passim.wrap000066400000000000000000000001331501337203100174410ustar00rootroot00000000000000[wrap-git] directory = passim url = https://github.com/hughsie/passim.git revision = 0.1.8